Merge branch 'cassandra-3.0' into cassandra-3.11
diff --git a/.build/build-rat.xml b/.build/build-rat.xml
index 6cc7943..3926660 100644
--- a/.build/build-rat.xml
+++ b/.build/build-rat.xml
@@ -50,30 +50,33 @@
     <target name="rat-check" depends="_rat_copy_versioned_files" unless="${rat.skip}" description="License checks on source" >
         <rat:report reportFile="${build.dir}/rat.txt">
             <fileset dir="." includesfile="${build.dir}/.ratinclude">
-                <!-- Config files with not much creativity -->
-                <exclude name="**/ide/**/*"/>
-                <exclude name="**/metrics-reporter-config-sample.yaml"/>
-                <exclude name="**/cassandra.yaml"/>
-                <exclude name="**/cassandra-murmur.yaml"/>
-                <exclude name="**/cassandra-seeds.yaml"/>
-                <exclude name="**/test/conf/cassandra.yaml"/>
-                <exclude name="**/test/conf/cassandra_encryption.yaml"/>
-                <exclude name="**/test/conf/cdc.yaml"/>
-                <exclude name="**/test/conf/commitlog_compression_LZ4.yaml"/>
-                <exclude name="**/test/conf/commitlog_compression_Zstd.yaml"/>
-                <exclude name="**/test/conf/system_keyspaces_directory.yaml"/>
-                <exclude name="**/test/conf/unit-test-conf/test-native-port.yaml"/>
-                <exclude name="**/test/data/jmxdump/cassandra-3.0-jmx.yaml"/>
-                <exclude name="**/test/data/jmxdump/cassandra-3.11-jmx.yaml"/>
-                <exclude name="**/test/data/jmxdump/cassandra-4.0-jmx.yaml"/>
-                <exclude name="**/tools/cqlstress-counter-example.yaml"/>
-                <exclude name="**/tools/cqlstress-example.yaml"/>
-                <exclude name="**/tools/cqlstress-insanity-example.yaml"/>
-                <exclude name="**/tools/cqlstress-lwt-example.yaml"/>
-                <!-- NOTICE files -->
-                <exclude NAME="**/NOTICE.md"/>
-                <!-- LICENSE files -->
-                <exclude NAME="**/LICENSE.md"/>
+                 <!-- Config files with not much creativity -->
+                 <exclude name="**/ide/**/*"/>
+                 <exclude name="**/metrics-reporter-config-sample.yaml"/>
+                 <exclude name="**/cassandra.yaml"/>
+                 <exclude name="**/cassandra-murmur.yaml"/>
+                 <exclude name="**/cassandra-seeds.yaml"/>
+                 <exclude NAME="**/doc/antora.yml"/>
+                 <exclude name="**/test/conf/cassandra.yaml"/>
+                 <exclude name="**/test/conf/cassandra_encryption.yaml"/>
+                 <exclude name="**/test/conf/cdc.yaml"/>
+                 <exclude name="**/test/conf/commitlog_compression_LZ4.yaml"/>
+                 <exclude name="**/test/conf/commitlog_compression_Zstd.yaml"/>
+                 <exclude name="**/test/conf/system_keyspaces_directory.yaml"/>
+                 <exclude name="**/test/conf/unit-test-conf/test-native-port.yaml"/>
+                 <exclude name="**/test/data/jmxdump/cassandra-3.0-jmx.yaml"/>
+                 <exclude name="**/test/data/jmxdump/cassandra-3.11-jmx.yaml"/>
+                 <exclude name="**/test/data/jmxdump/cassandra-4.0-jmx.yaml"/>
+                 <exclude name="**/tools/cqlstress-counter-example.yaml"/>
+                 <exclude name="**/tools/cqlstress-example.yaml"/>
+                 <exclude name="**/tools/cqlstress-insanity-example.yaml"/>
+                 <exclude name="**/tools/cqlstress-lwt-example.yaml"/>
+                 <!-- Documentation files -->
+                 <exclude NAME="**/doc/modules/**/*"/>
+                 <!-- NOTICE files -->
+                 <exclude NAME="**/NOTICE.md"/>
+                 <!-- LICENSE files -->
+                 <exclude NAME="**/LICENSE.md"/>
             </fileset>
         </rat:report>
         <exec executable="grep" outputproperty="rat.failed.files" failifexecutionfails="false">
diff --git a/.build/build-resolver.xml b/.build/build-resolver.xml
index de98eed..99bfe4b 100644
--- a/.build/build-resolver.xml
+++ b/.build/build-resolver.xml
@@ -23,13 +23,14 @@
     <property name="resolver-ant-tasks.version" value="1.3.0" />
     <property name="resolver-ant-tasks.local" value="${local.repository}/org/apache/maven/resolver/maven-resolver-ant-tasks/${resolver-ant-tasks.version}/maven-resolver-ant-tasks-${resolver-ant-tasks.version}.jar"/>
     <property name="resolver-ant-tasks.url" value="https://repo1.maven.org/maven2/org/apache/maven/resolver/maven-resolver-ant-tasks" />
+
+    <!-- version of lib/ downloads -->
+    <property name="lib.download.sha" value="27de93ebb1285e731570eb8585d66832c79c4289"/>
+
     <condition property="resolver-ant-tasks.jar.exists">
         <available file="${resolver-ant-tasks.local}" />
     </condition>
 
-    <!-- version of lib/ downloads -->
-    <property name="lib.download.sha" value="6c29ee84a2f62ccd05c328bbaa0c364eb1a7a821"/>
-
     <path id="resolver-ant-tasks.classpath" path="${resolver-ant-tasks.local}" />
 
     <!--
@@ -50,6 +51,8 @@
         <resolver:remoterepos id="all">
             <remoterepo id="resolver-central" url="${artifact.remoteRepository.central}"/>
             <remoterepo id="resolver-apache" url="${artifact.remoteRepository.apache}"/>
+            <!-- Only needed for PR builds - remove before commit -->
+            <!-- <remoterepo id="resolver-apache-snapshot" url="https://repository.apache.org/content/repositories/snapshots" releases="false" snapshots="true" updates="always" checksums="fail" />-->
         </resolver:remoterepos>
 
         <resolver:resolve>
@@ -83,6 +86,13 @@
             </dependencies>
             <path refid="cql3-grammar.classpath" classpath="runtime"/>
         </resolver:resolve>
+        <resolver:resolve>
+            <remoterepos refid="all"/>
+            <dependencies>
+                <dependency groupId="de.jflex" artifactId="jflex" version="1.6.0" />
+            </dependencies>
+            <path refid="jflex.classpath" classpath="runtime"/>
+        </resolver:resolve>
 
         <macrodef name="install">
             <attribute name="pomFile"/>
@@ -143,6 +153,7 @@
             <files dir="${test.lib}/jars" layout="{artifactId}-{version}-{classifier}.{extension}" scopes="test,!provide,!system"/>
         </resolver:resolve>
 
+
         <!-- jacoco agent jar comes wrapped in a jar -->
         <unzip src="${local.repository}/org/jacoco/org.jacoco.agent/${jacoco.version}/org.jacoco.agent-${jacoco.version}.jar" dest="${build.dir.lib}/jars">
             <patternset>
diff --git a/.build/dependency-check-suppressions.xml b/.build/dependency-check-suppressions.xml
index 5a87f57..98efddc 100644
--- a/.build/dependency-check-suppressions.xml
+++ b/.build/dependency-check-suppressions.xml
@@ -24,12 +24,13 @@
         <!--  https://issues.apache.org/jira/browse/CASSANDRA-16150 -->
         <packageUrl regex="true">^pkg:maven/org\.yaml/snakeyaml@.*$</packageUrl>
         <cve>CVE-2023-2251</cve>
-        <cve>CVE-2022-38752</cve>
-        <cve>CVE-2022-38751</cve>
-        <cve>CVE-2022-38750</cve>
-        <cve>CVE-2022-41854</cve>
+        <cve>CVE-2017-18640</cve>
         <cve>CVE-2022-25857</cve>
         <cve>CVE-2022-38749</cve>
+        <cve>CVE-2022-38750</cve>
+        <cve>CVE-2022-38751</cve>
+        <cve>CVE-2022-38752</cve>
+        <cve>CVE-2022-41854</cve>
         <cve>CVE-2022-1471</cve>
         <cve>CVE-2022-3064</cve>
         <cve>CVE-2021-4235</cve>
@@ -108,21 +109,16 @@
         <cve>CVE-2019-0205</cve>
     </suppress>
 
-    <!-- https://issues.apache.org/jira/browse/CASSANDRA-16056 -->
-    <!-- https://issues.apache.org/jira/browse/CASSANDRA-15416 -->
+    <!-- https://issues.apache.org/jira/browse/CASSANDRA-17966 -->
     <suppress>
-        <packageUrl regex="true">^pkg:maven/org\.codehaus\.jackson/jackson\-mapper\-asl@.*$</packageUrl>
-        <cve>CVE-2017-7525</cve>
-        <cve>CVE-2017-15095</cve>
-        <cve>CVE-2017-17485</cve>
-        <cve>CVE-2018-5968</cve>
-        <cve>CVE-2018-14718</cve>
-        <cve>CVE-2018-1000873</cve>
-        <cve>CVE-2018-7489</cve>
-        <cve>CVE-2019-10172</cve>
-        <cve>CVE-2019-14540</cve>
-        <cve>CVE-2019-14893</cve>
-        <cve>CVE-2019-16335</cve>
-        <cve>CVE-2019-17267</cve>
+        <packageUrl regex="true">^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@.*$</packageUrl>
+        <cve>CVE-2022-42003</cve>
+        <cve>CVE-2022-42004</cve>
     </suppress>
+    <!-- https://issues.apache.org/jira/browse/CASSANDRA-18389 -->
+    <suppress>
+        <packageUrl regex="true">^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-core.*$</packageUrl>
+        <cve>CVE-2022-45688</cve>
+    </suppress>
+
 </suppressions>
diff --git a/.circleci/config.yml b/.circleci/config.yml
index e6183f2..50f3240 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -113,6 +113,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -186,7 +188,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=test-compression
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -208,8 +212,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -294,6 +298,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -371,6 +377,193 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  utests_stress_repeat:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 4
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Log Environment Information
+        command: |
+          echo '*** id ***'
+          id
+          echo '*** cat /proc/cpuinfo ***'
+          cat /proc/cpuinfo
+          echo '*** free -m ***'
+          free -m
+          echo '*** df -m ***'
+          df -m
+          echo '*** ifconfig -a ***'
+          ifconfig -a
+          echo '*** uname -a ***'
+          uname -a
+          echo '*** mount ***'
+          mount
+          echo '*** env ***'
+          env
+          echo '*** java ***'
+          which java
+          java -version
+    - run:
+        name: Repeatedly run new or modifed JUnit tests
+        no_output_timeout: 15m
+        command: |
+          set -x
+          export PATH=$JAVA_HOME/bin:$PATH
+          time mv ~/cassandra /tmp
+          cd /tmp/cassandra
+          if [ -d ~/dtest_jars ]; then
+            cp ~/dtest_jars/dtest* /tmp/cassandra/build/
+          fi
+
+          # Calculate the number of test iterations to be run by the current parallel runner.
+          count=$((${REPEATED_UTESTS_STRESS_COUNT} / CIRCLE_NODE_TOTAL))
+          if (($CIRCLE_NODE_INDEX < (${REPEATED_UTESTS_STRESS_COUNT} % CIRCLE_NODE_TOTAL))); then
+            count=$((count+1))
+          fi
+
+          # Put manually specified tests and automatically detected tests together, removing duplicates
+          tests=$(echo ${REPEATED_UTESTS_STRESS} | sed -e "s/<nil>//" | sed -e "s/ //" | tr "," "\n" | tr " " "\n" | sort -n | uniq -u)
+          echo "Tests to be repeated: ${tests}"
+
+          # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
+          target=stress-test-some
+          testtag=""
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
+            testtag="compression"
+          fi
+
+          # Run each test class as many times as requested.
+          exit_code="$?"
+          for test in $tests; do
+
+              # Split class and method names from the test name
+              if [[ $test =~ "#" ]]; then
+                class=${test%"#"*}
+                method=${test#*"#"}
+              else
+                class=$test
+                method=""
+              fi
+
+              # Prepare the -Dtest.name argument.
+              # It can be the fully qualified class name or the short class name, depending on the target.
+              if [[ $target == "test" || \
+                    $target == "test-cdc" || \
+                    $target == "test-compression" || \
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
+                name_arg="-Dtest.name=${class##*.}"
+              else
+                name_arg="-Dtest.name=$class"
+              fi
+
+              # Prepare the -Dtest.methods argument, which is optional
+              if [[ $method == "" ]]; then
+                methods_arg=""
+              else
+                methods_arg="-Dtest.methods=$method"
+              fi
+
+              for i in $(seq -w 1 $count); do
+                echo "Running test $test, iteration $i of $count"
+
+                # run the test
+                status="passes"
+                if !( set -o pipefail && \
+                      ant stress-test-some $name_arg $methods_arg -Dno-build-test=true | \
+                      tee stdout.txt \
+                    ); then
+                  status="fails"
+                  exit_code=1
+                fi
+
+                # move the stdout output file
+                dest=/tmp/results/repeated_utests/stdout/${status}/${i}
+                mkdir -p $dest
+                mv stdout.txt $dest/${test}.txt
+
+                # move the XML output files
+                source=build/test/output/${testtag}
+                dest=/tmp/results/repeated_utests/output/${status}/${i}
+                mkdir -p $dest
+                if [[ -d $source && -n "$(ls $source)" ]]; then
+                  mv $source/* $dest/
+                fi
+
+                # move the log files
+                source=build/test/logs/${testtag}
+                dest=/tmp/results/repeated_utests/logs/${status}/${i}
+                mkdir -p $dest
+                if [[ -d $source && -n "$(ls $source)" ]]; then
+                  mv $source/* $dest/
+                fi
+
+                # maybe stop iterations on test failure
+                if [[ ${REPEATED_TESTS_STOP_ON_FAILURE} = true ]] && (( $exit_code > 0 )); then
+                  break
+                fi
+              done
+          done
+          (exit ${exit_code})
+    - store_test_results:
+        path: /tmp/results/repeated_utests/output
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/stdout
+        destination: stdout
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/output
+        destination: junitxml
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/logs
+        destination: logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -428,6 +621,135 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  j8_dtests_offheap_repeat:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 4
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Clone Cassandra dtest Repository (via git)
+        command: |
+          git clone --single-branch --branch $DTEST_BRANCH --depth 1 $DTEST_REPO ~/cassandra-dtest
+    - run:
+        name: Configure virtualenv and python Dependencies
+        command: |
+          # note, this should be super quick as all dependencies should be pre-installed in the docker image
+          # if additional dependencies were added to requirmeents.txt and the docker image hasn't been updated
+          # we'd have to install it here at runtime -- which will make things slow, so do yourself a favor and
+          # rebuild the docker image! (it automatically pulls the latest requirements.txt on build)
+          source ~/env3.6/bin/activate
+          export PATH=$JAVA_HOME/bin:$PATH
+          pip3 install --upgrade -r ~/cassandra-dtest/requirements.txt
+          pip3 freeze
+    - run:
+        name: Run repeated Python DTests
+        no_output_timeout: 15m
+        command: |
+          if [ "${REPEATED_DTESTS}" == "<nil>" ]; then
+            echo "Repeated dtest name hasn't been defined, exiting without running any test"
+          elif [ "${REPEATED_DTESTS_COUNT}" == "<nil>" ]; then
+            echo "Repeated dtest count hasn't been defined, exiting without running any test"
+          elif [ "${REPEATED_DTESTS_COUNT}" -le 0 ]; then
+            echo "Repeated dtest count is lesser or equals than zero, exiting without running any test"
+          else
+
+            # Calculate the number of test iterations to be run by the current parallel runner.
+            # Since we are running the same test multiple times there is no need to use `circleci tests split`.
+            count=$((${REPEATED_DTESTS_COUNT} / CIRCLE_NODE_TOTAL))
+            if (($CIRCLE_NODE_INDEX < (${REPEATED_DTESTS_COUNT} % CIRCLE_NODE_TOTAL))); then
+              count=$((count+1))
+            fi
+
+            if (($count <= 0)); then
+              echo "No tests to run in this runner"
+            else
+              echo "Running ${REPEATED_DTESTS} $count times"
+
+              source ~/env3.6/bin/activate
+              export PATH=$JAVA_HOME/bin:$PATH
+
+              java -version
+              cd ~/cassandra-dtest
+              mkdir -p /tmp/dtest
+
+              echo "env: $(env)"
+              echo "** done env"
+              mkdir -p /tmp/results/dtests
+
+              tests_arg=$(echo ${REPEATED_DTESTS} | sed -e "s/,/ /g")
+
+              stop_on_failure_arg=""
+              if ${REPEATED_TESTS_STOP_ON_FAILURE}; then
+                stop_on_failure_arg="-x"
+              fi
+
+              vnodes_args=""
+              if true; then
+                vnodes_args="--use-vnodes --num-tokens=32"
+              fi
+
+              upgrade_arg=""
+              if false; then
+                upgrade_arg="--execute-upgrade-tests --upgrade-target-version-only --upgrade-version-selection all"
+              fi
+
+              # we need the "set -o pipefail" here so that the exit code that circleci will actually use is from pytest and not the exit code from tee
+              set -o pipefail && cd ~/cassandra-dtest && pytest $vnodes_args --count=$count $stop_on_failure_arg $upgrade_arg --log-cli-level=DEBUG --junit-xml=/tmp/results/dtests/pytest_result.xml -s --cassandra-dir=/home/cassandra/cassandra --keep-test-dir --use-off-heap-memtables --skip-resource-intensive-tests $tests_arg | tee /tmp/dtest/stdout.txt
+            fi
+          fi
+    - store_test_results:
+        path: /tmp/results
+    - store_artifacts:
+        path: /tmp/dtest
+        destination: dtest
+    - store_artifacts:
+        path: ~/cassandra-dtest/logs
+        destination: dtest_logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -553,6 +875,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -626,7 +950,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=long-testsome
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -648,8 +974,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -734,6 +1060,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -807,7 +1135,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=testsome
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -829,8 +1159,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -915,6 +1245,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -992,6 +1324,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1117,6 +1451,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1217,6 +1553,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1342,6 +1680,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1419,6 +1759,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1492,7 +1834,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=test-jvm-dtest-some
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -1514,8 +1858,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -1600,6 +1944,289 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  utests_cdc:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 4
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Determine unit Tests to Run
+        command: |
+          # reminder: this code (along with all the steps) is independently executed on every circle container
+          # so the goal here is to get the circleci script to return the tests *this* container will run
+          # which we do via the `circleci` cli tool.
+
+          rm -fr ~/cassandra-dtest/upgrade_tests
+          echo "***java tests***"
+
+          # get all of our unit test filenames
+          set -eo pipefail && circleci tests glob "$HOME/cassandra/test/unit/**/*.java" > /tmp/all_java_unit_tests.txt
+
+          # split up the unit tests into groups based on the number of containers we have
+          set -eo pipefail && circleci tests split --split-by=timings --timings-type=filename --index=${CIRCLE_NODE_INDEX} --total=${CIRCLE_NODE_TOTAL} /tmp/all_java_unit_tests.txt > /tmp/java_tests_${CIRCLE_NODE_INDEX}.txt
+          set -eo pipefail && cat /tmp/java_tests_${CIRCLE_NODE_INDEX}.txt | sed "s;^/home/cassandra/cassandra/test/unit/;;g" | grep "Test\.java$"  > /tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt
+          echo "** /tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt"
+          cat /tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt
+        no_output_timeout: 15m
+    - run:
+        name: Log Environment Information
+        command: |
+          echo '*** id ***'
+          id
+          echo '*** cat /proc/cpuinfo ***'
+          cat /proc/cpuinfo
+          echo '*** free -m ***'
+          free -m
+          echo '*** df -m ***'
+          df -m
+          echo '*** ifconfig -a ***'
+          ifconfig -a
+          echo '*** uname -a ***'
+          uname -a
+          echo '*** mount ***'
+          mount
+          echo '*** env ***'
+          env
+          echo '*** java ***'
+          which java
+          java -version
+    - run:
+        name: Run Unit Tests (testclasslist-cdc)
+        command: |
+          set -x
+          export PATH=$JAVA_HOME/bin:$PATH
+          time mv ~/cassandra /tmp
+          cd /tmp/cassandra
+          if [ -d ~/dtest_jars ]; then
+            cp ~/dtest_jars/dtest* /tmp/cassandra/build/
+          fi
+          test_timeout=$(grep 'name="test.unit.timeout"' build.xml | awk -F'"' '{print $4}' || true)
+          if [ -z "$test_timeout" ]; then
+            test_timeout=$(grep 'name="test.timeout"' build.xml | awk -F'"' '{print $4}')
+          fi
+          ant testclasslist-cdc -Dtest.timeout="$test_timeout" -Dtest.classlistfile=/tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt  -Dtest.classlistprefix=unit -Dno-build-test=true
+        no_output_timeout: 15m
+    - store_test_results:
+        path: /tmp/cassandra/build/test/output/
+    - store_artifacts:
+        path: /tmp/cassandra/build/test/output
+        destination: junitxml
+    - store_artifacts:
+        path: /tmp/cassandra/build/test/logs
+        destination: logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  utests_stress:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 1
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Run Unit Tests (stress-test)
+        command: |
+          export PATH=$JAVA_HOME/bin:$PATH
+          time mv ~/cassandra /tmp
+          cd /tmp/cassandra
+          if [ -d ~/dtest_jars ]; then
+            cp ~/dtest_jars/dtest* /tmp/cassandra/build/
+          fi
+          ant stress-test -Dtest.classlistfile=/tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt  -Dtest.classlistprefix=unit -Dno-build-test=true
+        no_output_timeout: 15m
+    - store_test_results:
+        path: /tmp/cassandra/build/test/output/
+    - store_artifacts:
+        path: /tmp/cassandra/build/test/output
+        destination: junitxml
+    - store_artifacts:
+        path: /tmp/cassandra/build/test/logs
+        destination: logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  j8_dtests_offheap:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 4
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Log Environment Information
+        command: |
+          echo '*** id ***'
+          id
+          echo '*** cat /proc/cpuinfo ***'
+          cat /proc/cpuinfo
+          echo '*** free -m ***'
+          free -m
+          echo '*** df -m ***'
+          df -m
+          echo '*** ifconfig -a ***'
+          ifconfig -a
+          echo '*** uname -a ***'
+          uname -a
+          echo '*** mount ***'
+          mount
+          echo '*** env ***'
+          env
+          echo '*** java ***'
+          which java
+          java -version
+    - run:
+        name: Clone Cassandra dtest Repository (via git)
+        command: |
+          git clone --single-branch --branch $DTEST_BRANCH --depth 1 $DTEST_REPO ~/cassandra-dtest
+    - run:
+        name: Configure virtualenv and python Dependencies
+        command: |
+          # note, this should be super quick as all dependencies should be pre-installed in the docker image
+          # if additional dependencies were added to requirmeents.txt and the docker image hasn't been updated
+          # we'd have to install it here at runtime -- which will make things slow, so do yourself a favor and
+          # rebuild the docker image! (it automatically pulls the latest requirements.txt on build)
+          source ~/env3.6/bin/activate
+          export PATH=$JAVA_HOME/bin:$PATH
+          pip3 install --upgrade -r ~/cassandra-dtest/requirements.txt
+          pip3 freeze
+    - run:
+        name: Determine Tests to Run (j8_dtests_offheap)
+        no_output_timeout: 5m
+        command: "# reminder: this code (along with all the steps) is independently executed on every circle container\n# so the goal here is to get the circleci script to return the tests *this* container will run\n# which we do via the `circleci` cli tool.\n\ncd cassandra-dtest\nsource ~/env3.6/bin/activate\nexport PATH=$JAVA_HOME/bin:$PATH\n\nif [ -n '' ]; then\n  export \nfi\n\necho \"***Collected DTests (j8_dtests_offheap)***\"\nset -eo pipefail && ./run_dtests.py --use-vnodes --use-off-heap-memtables --skip-resource-intensive-tests --dtest-print-tests-only --dtest-print-tests-output=/tmp/all_dtest_tests_j8_dtests_offheap_raw --cassandra-dir=../cassandra\nif [ -z '' ]; then\n  mv /tmp/all_dtest_tests_j8_dtests_offheap_raw /tmp/all_dtest_tests_j8_dtests_offheap\nelse\n  grep -e '' /tmp/all_dtest_tests_j8_dtests_offheap_raw > /tmp/all_dtest_tests_j8_dtests_offheap || { echo \"Filter did not match any tests! Exiting build.\"; exit 0; }\nfi\nset -eo pipefail && circleci tests split --split-by=timings --timings-type=classname /tmp/all_dtest_tests_j8_dtests_offheap > /tmp/split_dtest_tests_j8_dtests_offheap.txt\ncat /tmp/split_dtest_tests_j8_dtests_offheap.txt | tr '\\n' ' ' > /tmp/split_dtest_tests_j8_dtests_offheap_final.txt\ncat /tmp/split_dtest_tests_j8_dtests_offheap_final.txt\n"
+    - run:
+        name: Run dtests (j8_dtests_offheap)
+        no_output_timeout: 15m
+        command: "echo \"cat /tmp/split_dtest_tests_j8_dtests_offheap_final.txt\"\ncat /tmp/split_dtest_tests_j8_dtests_offheap_final.txt\n\nsource ~/env3.6/bin/activate\nexport PATH=$JAVA_HOME/bin:$PATH\nif [ -n '' ]; then\n  export \nfi\n\njava -version\ncd ~/cassandra-dtest\nmkdir -p /tmp/dtest\n\necho \"env: $(env)\"\necho \"** done env\"\nmkdir -p /tmp/results/dtests\n# we need the \"set -o pipefail\" here so that the exit code that circleci will actually use is from pytest and not the exit code from tee\nexport SPLIT_TESTS=`cat /tmp/split_dtest_tests_j8_dtests_offheap_final.txt`\nif [ ! -z \"$SPLIT_TESTS\" ]; then\n  set -o pipefail && cd ~/cassandra-dtest && pytest --use-vnodes --num-tokens=16 --use-off-heap-memtables --skip-resource-intensive-tests --log-level=\"DEBUG\" --junit-xml=/tmp/results/dtests/pytest_result_j8_dtests_offheap.xml -s --cassandra-dir=/home/cassandra/cassandra --keep-test-dir $SPLIT_TESTS 2>&1 | tee /tmp/dtest/stdout.txt\nelse\n  echo \"Tune your parallelism, there are more containers than test classes. Nothing to do in this container\"\n  (exit 1)\nfi\n"
+    - store_test_results:
+        path: /tmp/results
+    - store_artifacts:
+        path: /tmp/dtest
+        destination: dtest_j8_dtests_offheap
+    - store_artifacts:
+        path: ~/cassandra-dtest/logs
+        destination: dtest_j8_dtests_offheap_logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1711,6 +2338,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1788,6 +2417,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1865,6 +2496,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1938,7 +2571,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=test-jvm-dtest-some
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -1960,8 +2595,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -2046,6 +2681,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2135,8 +2772,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name="-Dtest.name=$class_name"
               else
                 name="-Dtest.name=$class_path"
@@ -2224,6 +2861,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2349,6 +2988,193 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  utests_cdc_repeat:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 4
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Log Environment Information
+        command: |
+          echo '*** id ***'
+          id
+          echo '*** cat /proc/cpuinfo ***'
+          cat /proc/cpuinfo
+          echo '*** free -m ***'
+          free -m
+          echo '*** df -m ***'
+          df -m
+          echo '*** ifconfig -a ***'
+          ifconfig -a
+          echo '*** uname -a ***'
+          uname -a
+          echo '*** mount ***'
+          mount
+          echo '*** env ***'
+          env
+          echo '*** java ***'
+          which java
+          java -version
+    - run:
+        name: Repeatedly run new or modifed JUnit tests
+        no_output_timeout: 15m
+        command: |
+          set -x
+          export PATH=$JAVA_HOME/bin:$PATH
+          time mv ~/cassandra /tmp
+          cd /tmp/cassandra
+          if [ -d ~/dtest_jars ]; then
+            cp ~/dtest_jars/dtest* /tmp/cassandra/build/
+          fi
+
+          # Calculate the number of test iterations to be run by the current parallel runner.
+          count=$((${REPEATED_UTESTS_COUNT} / CIRCLE_NODE_TOTAL))
+          if (($CIRCLE_NODE_INDEX < (${REPEATED_UTESTS_COUNT} % CIRCLE_NODE_TOTAL))); then
+            count=$((count+1))
+          fi
+
+          # Put manually specified tests and automatically detected tests together, removing duplicates
+          tests=$(echo ${REPEATED_UTESTS} | sed -e "s/<nil>//" | sed -e "s/ //" | tr "," "\n" | tr " " "\n" | sort -n | uniq -u)
+          echo "Tests to be repeated: ${tests}"
+
+          # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
+          target=test-cdc
+          testtag=""
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
+            testtag="compression"
+          fi
+
+          # Run each test class as many times as requested.
+          exit_code="$?"
+          for test in $tests; do
+
+              # Split class and method names from the test name
+              if [[ $test =~ "#" ]]; then
+                class=${test%"#"*}
+                method=${test#*"#"}
+              else
+                class=$test
+                method=""
+              fi
+
+              # Prepare the -Dtest.name argument.
+              # It can be the fully qualified class name or the short class name, depending on the target.
+              if [[ $target == "test" || \
+                    $target == "test-cdc" || \
+                    $target == "test-compression" || \
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
+                name_arg="-Dtest.name=${class##*.}"
+              else
+                name_arg="-Dtest.name=$class"
+              fi
+
+              # Prepare the -Dtest.methods argument, which is optional
+              if [[ $method == "" ]]; then
+                methods_arg=""
+              else
+                methods_arg="-Dtest.methods=$method"
+              fi
+
+              for i in $(seq -w 1 $count); do
+                echo "Running test $test, iteration $i of $count"
+
+                # run the test
+                status="passes"
+                if !( set -o pipefail && \
+                      ant test-cdc $name_arg $methods_arg -Dno-build-test=true | \
+                      tee stdout.txt \
+                    ); then
+                  status="fails"
+                  exit_code=1
+                fi
+
+                # move the stdout output file
+                dest=/tmp/results/repeated_utests/stdout/${status}/${i}
+                mkdir -p $dest
+                mv stdout.txt $dest/${test}.txt
+
+                # move the XML output files
+                source=build/test/output/${testtag}
+                dest=/tmp/results/repeated_utests/output/${status}/${i}
+                mkdir -p $dest
+                if [[ -d $source && -n "$(ls $source)" ]]; then
+                  mv $source/* $dest/
+                fi
+
+                # move the log files
+                source=build/test/logs/${testtag}
+                dest=/tmp/results/repeated_utests/logs/${status}/${i}
+                mkdir -p $dest
+                if [[ -d $source && -n "$(ls $source)" ]]; then
+                  mv $source/* $dest/
+                fi
+
+                # maybe stop iterations on test failure
+                if [[ ${REPEATED_TESTS_STOP_ON_FAILURE} = true ]] && (( $exit_code > 0 )); then
+                  break
+                fi
+              done
+          done
+          (exit ${exit_code})
+    - store_test_results:
+        path: /tmp/results/repeated_utests/output
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/stdout
+        destination: stdout
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/output
+        destination: junitxml
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/logs
+        destination: logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2460,6 +3286,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2517,7 +3345,7 @@
           if [ -d ~/dtest_jars ]; then
             cp ~/dtest_jars/dtest* /tmp/cassandra/build/
           fi
-          ant long-test -Dno-build-test=true
+          ant long-test -Dtest.classlistfile=/tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt  -Dtest.classlistprefix=unit -Dno-build-test=true
         no_output_timeout: 15m
     - store_test_results:
         path: /tmp/cassandra/build/test/output/
@@ -2547,6 +3375,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2603,6 +3433,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2728,6 +3560,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2839,6 +3673,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2936,6 +3772,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2991,18 +3829,30 @@
         requires:
         - start_utests_long
         - build
+    - start_utests_cdc:
+        type: approval
+    - utests_cdc:
+        requires:
+        - start_utests_cdc
+        - build
     - start_utests_compression:
         type: approval
     - utests_compression:
         requires:
         - start_utests_compression
         - build
+    - start_utests_stress:
+        type: approval
+    - utests_stress:
+        requires:
+        - start_utests_stress
+        - build
     - start_j8_dtest_jars_build:
         type: approval
     - j8_dtest_jars_build:
         requires:
-        - start_j8_dtest_jars_build
         - build
+        - start_j8_dtest_jars_build
     - start_jvm_upgrade_dtests:
         type: approval
     - j8_jvm_upgrade_dtests:
@@ -3033,11 +3883,17 @@
         requires:
         - start_j8_dtests_large_vnode
         - build
-    - start_j8_upgrade_dtests:
+    - start_upgrade_dtests:
         type: approval
     - j8_upgrade_dtests:
         requires:
-        - start_j8_upgrade_dtests
+        - start_upgrade_dtests
+        - build
+    - start_j8_dtests_offheap:
+        type: approval
+    - j8_dtests_offheap:
+        requires:
+        - start_j8_dtests_offheap
         - build
   pre-commit_tests:
     jobs:
@@ -3064,18 +3920,30 @@
         requires:
         - start_utests_long
         - build
+    - start_utests_cdc:
+        type: approval
+    - utests_cdc:
+        requires:
+        - start_utests_cdc
+        - build
     - start_utests_compression:
         type: approval
     - utests_compression:
         requires:
         - start_utests_compression
         - build
+    - start_utests_stress:
+        type: approval
+    - utests_stress:
+        requires:
+        - start_utests_stress
+        - build
     - start_jvm_upgrade_dtests:
         type: approval
     - j8_dtest_jars_build:
         requires:
-        - start_jvm_upgrade_dtests
         - build
+        - start_jvm_upgrade_dtests
     - j8_jvm_upgrade_dtests:
         requires:
         - j8_dtest_jars_build
@@ -3095,9 +3963,15 @@
         requires:
         - start_j8_dtests_large
         - build
-    - start_upgrade_dtests:
+    - start_upgrade_tests:
         type: approval
     - j8_upgrade_dtests:
         requires:
-        - start_upgrade_dtests
+        - start_upgrade_tests
+        - build
+    - start_j8_dtests_offheap:
+        type: approval
+    - j8_dtests_offheap:
+        requires:
+        - start_j8_dtests_offheap
         - build
diff --git a/.circleci/config.yml.FREE b/.circleci/config.yml.FREE
index e6183f2..50f3240 100644
--- a/.circleci/config.yml.FREE
+++ b/.circleci/config.yml.FREE
@@ -113,6 +113,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -186,7 +188,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=test-compression
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -208,8 +212,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -294,6 +298,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -371,6 +377,193 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  utests_stress_repeat:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 4
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Log Environment Information
+        command: |
+          echo '*** id ***'
+          id
+          echo '*** cat /proc/cpuinfo ***'
+          cat /proc/cpuinfo
+          echo '*** free -m ***'
+          free -m
+          echo '*** df -m ***'
+          df -m
+          echo '*** ifconfig -a ***'
+          ifconfig -a
+          echo '*** uname -a ***'
+          uname -a
+          echo '*** mount ***'
+          mount
+          echo '*** env ***'
+          env
+          echo '*** java ***'
+          which java
+          java -version
+    - run:
+        name: Repeatedly run new or modifed JUnit tests
+        no_output_timeout: 15m
+        command: |
+          set -x
+          export PATH=$JAVA_HOME/bin:$PATH
+          time mv ~/cassandra /tmp
+          cd /tmp/cassandra
+          if [ -d ~/dtest_jars ]; then
+            cp ~/dtest_jars/dtest* /tmp/cassandra/build/
+          fi
+
+          # Calculate the number of test iterations to be run by the current parallel runner.
+          count=$((${REPEATED_UTESTS_STRESS_COUNT} / CIRCLE_NODE_TOTAL))
+          if (($CIRCLE_NODE_INDEX < (${REPEATED_UTESTS_STRESS_COUNT} % CIRCLE_NODE_TOTAL))); then
+            count=$((count+1))
+          fi
+
+          # Put manually specified tests and automatically detected tests together, removing duplicates
+          tests=$(echo ${REPEATED_UTESTS_STRESS} | sed -e "s/<nil>//" | sed -e "s/ //" | tr "," "\n" | tr " " "\n" | sort -n | uniq -u)
+          echo "Tests to be repeated: ${tests}"
+
+          # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
+          target=stress-test-some
+          testtag=""
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
+            testtag="compression"
+          fi
+
+          # Run each test class as many times as requested.
+          exit_code="$?"
+          for test in $tests; do
+
+              # Split class and method names from the test name
+              if [[ $test =~ "#" ]]; then
+                class=${test%"#"*}
+                method=${test#*"#"}
+              else
+                class=$test
+                method=""
+              fi
+
+              # Prepare the -Dtest.name argument.
+              # It can be the fully qualified class name or the short class name, depending on the target.
+              if [[ $target == "test" || \
+                    $target == "test-cdc" || \
+                    $target == "test-compression" || \
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
+                name_arg="-Dtest.name=${class##*.}"
+              else
+                name_arg="-Dtest.name=$class"
+              fi
+
+              # Prepare the -Dtest.methods argument, which is optional
+              if [[ $method == "" ]]; then
+                methods_arg=""
+              else
+                methods_arg="-Dtest.methods=$method"
+              fi
+
+              for i in $(seq -w 1 $count); do
+                echo "Running test $test, iteration $i of $count"
+
+                # run the test
+                status="passes"
+                if !( set -o pipefail && \
+                      ant stress-test-some $name_arg $methods_arg -Dno-build-test=true | \
+                      tee stdout.txt \
+                    ); then
+                  status="fails"
+                  exit_code=1
+                fi
+
+                # move the stdout output file
+                dest=/tmp/results/repeated_utests/stdout/${status}/${i}
+                mkdir -p $dest
+                mv stdout.txt $dest/${test}.txt
+
+                # move the XML output files
+                source=build/test/output/${testtag}
+                dest=/tmp/results/repeated_utests/output/${status}/${i}
+                mkdir -p $dest
+                if [[ -d $source && -n "$(ls $source)" ]]; then
+                  mv $source/* $dest/
+                fi
+
+                # move the log files
+                source=build/test/logs/${testtag}
+                dest=/tmp/results/repeated_utests/logs/${status}/${i}
+                mkdir -p $dest
+                if [[ -d $source && -n "$(ls $source)" ]]; then
+                  mv $source/* $dest/
+                fi
+
+                # maybe stop iterations on test failure
+                if [[ ${REPEATED_TESTS_STOP_ON_FAILURE} = true ]] && (( $exit_code > 0 )); then
+                  break
+                fi
+              done
+          done
+          (exit ${exit_code})
+    - store_test_results:
+        path: /tmp/results/repeated_utests/output
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/stdout
+        destination: stdout
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/output
+        destination: junitxml
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/logs
+        destination: logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -428,6 +621,135 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  j8_dtests_offheap_repeat:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 4
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Clone Cassandra dtest Repository (via git)
+        command: |
+          git clone --single-branch --branch $DTEST_BRANCH --depth 1 $DTEST_REPO ~/cassandra-dtest
+    - run:
+        name: Configure virtualenv and python Dependencies
+        command: |
+          # note, this should be super quick as all dependencies should be pre-installed in the docker image
+          # if additional dependencies were added to requirmeents.txt and the docker image hasn't been updated
+          # we'd have to install it here at runtime -- which will make things slow, so do yourself a favor and
+          # rebuild the docker image! (it automatically pulls the latest requirements.txt on build)
+          source ~/env3.6/bin/activate
+          export PATH=$JAVA_HOME/bin:$PATH
+          pip3 install --upgrade -r ~/cassandra-dtest/requirements.txt
+          pip3 freeze
+    - run:
+        name: Run repeated Python DTests
+        no_output_timeout: 15m
+        command: |
+          if [ "${REPEATED_DTESTS}" == "<nil>" ]; then
+            echo "Repeated dtest name hasn't been defined, exiting without running any test"
+          elif [ "${REPEATED_DTESTS_COUNT}" == "<nil>" ]; then
+            echo "Repeated dtest count hasn't been defined, exiting without running any test"
+          elif [ "${REPEATED_DTESTS_COUNT}" -le 0 ]; then
+            echo "Repeated dtest count is lesser or equals than zero, exiting without running any test"
+          else
+
+            # Calculate the number of test iterations to be run by the current parallel runner.
+            # Since we are running the same test multiple times there is no need to use `circleci tests split`.
+            count=$((${REPEATED_DTESTS_COUNT} / CIRCLE_NODE_TOTAL))
+            if (($CIRCLE_NODE_INDEX < (${REPEATED_DTESTS_COUNT} % CIRCLE_NODE_TOTAL))); then
+              count=$((count+1))
+            fi
+
+            if (($count <= 0)); then
+              echo "No tests to run in this runner"
+            else
+              echo "Running ${REPEATED_DTESTS} $count times"
+
+              source ~/env3.6/bin/activate
+              export PATH=$JAVA_HOME/bin:$PATH
+
+              java -version
+              cd ~/cassandra-dtest
+              mkdir -p /tmp/dtest
+
+              echo "env: $(env)"
+              echo "** done env"
+              mkdir -p /tmp/results/dtests
+
+              tests_arg=$(echo ${REPEATED_DTESTS} | sed -e "s/,/ /g")
+
+              stop_on_failure_arg=""
+              if ${REPEATED_TESTS_STOP_ON_FAILURE}; then
+                stop_on_failure_arg="-x"
+              fi
+
+              vnodes_args=""
+              if true; then
+                vnodes_args="--use-vnodes --num-tokens=32"
+              fi
+
+              upgrade_arg=""
+              if false; then
+                upgrade_arg="--execute-upgrade-tests --upgrade-target-version-only --upgrade-version-selection all"
+              fi
+
+              # we need the "set -o pipefail" here so that the exit code that circleci will actually use is from pytest and not the exit code from tee
+              set -o pipefail && cd ~/cassandra-dtest && pytest $vnodes_args --count=$count $stop_on_failure_arg $upgrade_arg --log-cli-level=DEBUG --junit-xml=/tmp/results/dtests/pytest_result.xml -s --cassandra-dir=/home/cassandra/cassandra --keep-test-dir --use-off-heap-memtables --skip-resource-intensive-tests $tests_arg | tee /tmp/dtest/stdout.txt
+            fi
+          fi
+    - store_test_results:
+        path: /tmp/results
+    - store_artifacts:
+        path: /tmp/dtest
+        destination: dtest
+    - store_artifacts:
+        path: ~/cassandra-dtest/logs
+        destination: dtest_logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -553,6 +875,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -626,7 +950,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=long-testsome
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -648,8 +974,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -734,6 +1060,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -807,7 +1135,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=testsome
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -829,8 +1159,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -915,6 +1245,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -992,6 +1324,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1117,6 +1451,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1217,6 +1553,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1342,6 +1680,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1419,6 +1759,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1492,7 +1834,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=test-jvm-dtest-some
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -1514,8 +1858,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -1600,6 +1944,289 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  utests_cdc:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 4
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Determine unit Tests to Run
+        command: |
+          # reminder: this code (along with all the steps) is independently executed on every circle container
+          # so the goal here is to get the circleci script to return the tests *this* container will run
+          # which we do via the `circleci` cli tool.
+
+          rm -fr ~/cassandra-dtest/upgrade_tests
+          echo "***java tests***"
+
+          # get all of our unit test filenames
+          set -eo pipefail && circleci tests glob "$HOME/cassandra/test/unit/**/*.java" > /tmp/all_java_unit_tests.txt
+
+          # split up the unit tests into groups based on the number of containers we have
+          set -eo pipefail && circleci tests split --split-by=timings --timings-type=filename --index=${CIRCLE_NODE_INDEX} --total=${CIRCLE_NODE_TOTAL} /tmp/all_java_unit_tests.txt > /tmp/java_tests_${CIRCLE_NODE_INDEX}.txt
+          set -eo pipefail && cat /tmp/java_tests_${CIRCLE_NODE_INDEX}.txt | sed "s;^/home/cassandra/cassandra/test/unit/;;g" | grep "Test\.java$"  > /tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt
+          echo "** /tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt"
+          cat /tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt
+        no_output_timeout: 15m
+    - run:
+        name: Log Environment Information
+        command: |
+          echo '*** id ***'
+          id
+          echo '*** cat /proc/cpuinfo ***'
+          cat /proc/cpuinfo
+          echo '*** free -m ***'
+          free -m
+          echo '*** df -m ***'
+          df -m
+          echo '*** ifconfig -a ***'
+          ifconfig -a
+          echo '*** uname -a ***'
+          uname -a
+          echo '*** mount ***'
+          mount
+          echo '*** env ***'
+          env
+          echo '*** java ***'
+          which java
+          java -version
+    - run:
+        name: Run Unit Tests (testclasslist-cdc)
+        command: |
+          set -x
+          export PATH=$JAVA_HOME/bin:$PATH
+          time mv ~/cassandra /tmp
+          cd /tmp/cassandra
+          if [ -d ~/dtest_jars ]; then
+            cp ~/dtest_jars/dtest* /tmp/cassandra/build/
+          fi
+          test_timeout=$(grep 'name="test.unit.timeout"' build.xml | awk -F'"' '{print $4}' || true)
+          if [ -z "$test_timeout" ]; then
+            test_timeout=$(grep 'name="test.timeout"' build.xml | awk -F'"' '{print $4}')
+          fi
+          ant testclasslist-cdc -Dtest.timeout="$test_timeout" -Dtest.classlistfile=/tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt  -Dtest.classlistprefix=unit -Dno-build-test=true
+        no_output_timeout: 15m
+    - store_test_results:
+        path: /tmp/cassandra/build/test/output/
+    - store_artifacts:
+        path: /tmp/cassandra/build/test/output
+        destination: junitxml
+    - store_artifacts:
+        path: /tmp/cassandra/build/test/logs
+        destination: logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  utests_stress:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 1
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Run Unit Tests (stress-test)
+        command: |
+          export PATH=$JAVA_HOME/bin:$PATH
+          time mv ~/cassandra /tmp
+          cd /tmp/cassandra
+          if [ -d ~/dtest_jars ]; then
+            cp ~/dtest_jars/dtest* /tmp/cassandra/build/
+          fi
+          ant stress-test -Dtest.classlistfile=/tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt  -Dtest.classlistprefix=unit -Dno-build-test=true
+        no_output_timeout: 15m
+    - store_test_results:
+        path: /tmp/cassandra/build/test/output/
+    - store_artifacts:
+        path: /tmp/cassandra/build/test/output
+        destination: junitxml
+    - store_artifacts:
+        path: /tmp/cassandra/build/test/logs
+        destination: logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  j8_dtests_offheap:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 4
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Log Environment Information
+        command: |
+          echo '*** id ***'
+          id
+          echo '*** cat /proc/cpuinfo ***'
+          cat /proc/cpuinfo
+          echo '*** free -m ***'
+          free -m
+          echo '*** df -m ***'
+          df -m
+          echo '*** ifconfig -a ***'
+          ifconfig -a
+          echo '*** uname -a ***'
+          uname -a
+          echo '*** mount ***'
+          mount
+          echo '*** env ***'
+          env
+          echo '*** java ***'
+          which java
+          java -version
+    - run:
+        name: Clone Cassandra dtest Repository (via git)
+        command: |
+          git clone --single-branch --branch $DTEST_BRANCH --depth 1 $DTEST_REPO ~/cassandra-dtest
+    - run:
+        name: Configure virtualenv and python Dependencies
+        command: |
+          # note, this should be super quick as all dependencies should be pre-installed in the docker image
+          # if additional dependencies were added to requirmeents.txt and the docker image hasn't been updated
+          # we'd have to install it here at runtime -- which will make things slow, so do yourself a favor and
+          # rebuild the docker image! (it automatically pulls the latest requirements.txt on build)
+          source ~/env3.6/bin/activate
+          export PATH=$JAVA_HOME/bin:$PATH
+          pip3 install --upgrade -r ~/cassandra-dtest/requirements.txt
+          pip3 freeze
+    - run:
+        name: Determine Tests to Run (j8_dtests_offheap)
+        no_output_timeout: 5m
+        command: "# reminder: this code (along with all the steps) is independently executed on every circle container\n# so the goal here is to get the circleci script to return the tests *this* container will run\n# which we do via the `circleci` cli tool.\n\ncd cassandra-dtest\nsource ~/env3.6/bin/activate\nexport PATH=$JAVA_HOME/bin:$PATH\n\nif [ -n '' ]; then\n  export \nfi\n\necho \"***Collected DTests (j8_dtests_offheap)***\"\nset -eo pipefail && ./run_dtests.py --use-vnodes --use-off-heap-memtables --skip-resource-intensive-tests --dtest-print-tests-only --dtest-print-tests-output=/tmp/all_dtest_tests_j8_dtests_offheap_raw --cassandra-dir=../cassandra\nif [ -z '' ]; then\n  mv /tmp/all_dtest_tests_j8_dtests_offheap_raw /tmp/all_dtest_tests_j8_dtests_offheap\nelse\n  grep -e '' /tmp/all_dtest_tests_j8_dtests_offheap_raw > /tmp/all_dtest_tests_j8_dtests_offheap || { echo \"Filter did not match any tests! Exiting build.\"; exit 0; }\nfi\nset -eo pipefail && circleci tests split --split-by=timings --timings-type=classname /tmp/all_dtest_tests_j8_dtests_offheap > /tmp/split_dtest_tests_j8_dtests_offheap.txt\ncat /tmp/split_dtest_tests_j8_dtests_offheap.txt | tr '\\n' ' ' > /tmp/split_dtest_tests_j8_dtests_offheap_final.txt\ncat /tmp/split_dtest_tests_j8_dtests_offheap_final.txt\n"
+    - run:
+        name: Run dtests (j8_dtests_offheap)
+        no_output_timeout: 15m
+        command: "echo \"cat /tmp/split_dtest_tests_j8_dtests_offheap_final.txt\"\ncat /tmp/split_dtest_tests_j8_dtests_offheap_final.txt\n\nsource ~/env3.6/bin/activate\nexport PATH=$JAVA_HOME/bin:$PATH\nif [ -n '' ]; then\n  export \nfi\n\njava -version\ncd ~/cassandra-dtest\nmkdir -p /tmp/dtest\n\necho \"env: $(env)\"\necho \"** done env\"\nmkdir -p /tmp/results/dtests\n# we need the \"set -o pipefail\" here so that the exit code that circleci will actually use is from pytest and not the exit code from tee\nexport SPLIT_TESTS=`cat /tmp/split_dtest_tests_j8_dtests_offheap_final.txt`\nif [ ! -z \"$SPLIT_TESTS\" ]; then\n  set -o pipefail && cd ~/cassandra-dtest && pytest --use-vnodes --num-tokens=16 --use-off-heap-memtables --skip-resource-intensive-tests --log-level=\"DEBUG\" --junit-xml=/tmp/results/dtests/pytest_result_j8_dtests_offheap.xml -s --cassandra-dir=/home/cassandra/cassandra --keep-test-dir $SPLIT_TESTS 2>&1 | tee /tmp/dtest/stdout.txt\nelse\n  echo \"Tune your parallelism, there are more containers than test classes. Nothing to do in this container\"\n  (exit 1)\nfi\n"
+    - store_test_results:
+        path: /tmp/results
+    - store_artifacts:
+        path: /tmp/dtest
+        destination: dtest_j8_dtests_offheap
+    - store_artifacts:
+        path: ~/cassandra-dtest/logs
+        destination: dtest_j8_dtests_offheap_logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1711,6 +2338,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1788,6 +2417,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1865,6 +2496,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1938,7 +2571,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=test-jvm-dtest-some
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -1960,8 +2595,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -2046,6 +2681,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2135,8 +2772,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name="-Dtest.name=$class_name"
               else
                 name="-Dtest.name=$class_path"
@@ -2224,6 +2861,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2349,6 +2988,193 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  utests_cdc_repeat:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 4
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Log Environment Information
+        command: |
+          echo '*** id ***'
+          id
+          echo '*** cat /proc/cpuinfo ***'
+          cat /proc/cpuinfo
+          echo '*** free -m ***'
+          free -m
+          echo '*** df -m ***'
+          df -m
+          echo '*** ifconfig -a ***'
+          ifconfig -a
+          echo '*** uname -a ***'
+          uname -a
+          echo '*** mount ***'
+          mount
+          echo '*** env ***'
+          env
+          echo '*** java ***'
+          which java
+          java -version
+    - run:
+        name: Repeatedly run new or modifed JUnit tests
+        no_output_timeout: 15m
+        command: |
+          set -x
+          export PATH=$JAVA_HOME/bin:$PATH
+          time mv ~/cassandra /tmp
+          cd /tmp/cassandra
+          if [ -d ~/dtest_jars ]; then
+            cp ~/dtest_jars/dtest* /tmp/cassandra/build/
+          fi
+
+          # Calculate the number of test iterations to be run by the current parallel runner.
+          count=$((${REPEATED_UTESTS_COUNT} / CIRCLE_NODE_TOTAL))
+          if (($CIRCLE_NODE_INDEX < (${REPEATED_UTESTS_COUNT} % CIRCLE_NODE_TOTAL))); then
+            count=$((count+1))
+          fi
+
+          # Put manually specified tests and automatically detected tests together, removing duplicates
+          tests=$(echo ${REPEATED_UTESTS} | sed -e "s/<nil>//" | sed -e "s/ //" | tr "," "\n" | tr " " "\n" | sort -n | uniq -u)
+          echo "Tests to be repeated: ${tests}"
+
+          # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
+          target=test-cdc
+          testtag=""
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
+            testtag="compression"
+          fi
+
+          # Run each test class as many times as requested.
+          exit_code="$?"
+          for test in $tests; do
+
+              # Split class and method names from the test name
+              if [[ $test =~ "#" ]]; then
+                class=${test%"#"*}
+                method=${test#*"#"}
+              else
+                class=$test
+                method=""
+              fi
+
+              # Prepare the -Dtest.name argument.
+              # It can be the fully qualified class name or the short class name, depending on the target.
+              if [[ $target == "test" || \
+                    $target == "test-cdc" || \
+                    $target == "test-compression" || \
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
+                name_arg="-Dtest.name=${class##*.}"
+              else
+                name_arg="-Dtest.name=$class"
+              fi
+
+              # Prepare the -Dtest.methods argument, which is optional
+              if [[ $method == "" ]]; then
+                methods_arg=""
+              else
+                methods_arg="-Dtest.methods=$method"
+              fi
+
+              for i in $(seq -w 1 $count); do
+                echo "Running test $test, iteration $i of $count"
+
+                # run the test
+                status="passes"
+                if !( set -o pipefail && \
+                      ant test-cdc $name_arg $methods_arg -Dno-build-test=true | \
+                      tee stdout.txt \
+                    ); then
+                  status="fails"
+                  exit_code=1
+                fi
+
+                # move the stdout output file
+                dest=/tmp/results/repeated_utests/stdout/${status}/${i}
+                mkdir -p $dest
+                mv stdout.txt $dest/${test}.txt
+
+                # move the XML output files
+                source=build/test/output/${testtag}
+                dest=/tmp/results/repeated_utests/output/${status}/${i}
+                mkdir -p $dest
+                if [[ -d $source && -n "$(ls $source)" ]]; then
+                  mv $source/* $dest/
+                fi
+
+                # move the log files
+                source=build/test/logs/${testtag}
+                dest=/tmp/results/repeated_utests/logs/${status}/${i}
+                mkdir -p $dest
+                if [[ -d $source && -n "$(ls $source)" ]]; then
+                  mv $source/* $dest/
+                fi
+
+                # maybe stop iterations on test failure
+                if [[ ${REPEATED_TESTS_STOP_ON_FAILURE} = true ]] && (( $exit_code > 0 )); then
+                  break
+                fi
+              done
+          done
+          (exit ${exit_code})
+    - store_test_results:
+        path: /tmp/results/repeated_utests/output
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/stdout
+        destination: stdout
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/output
+        destination: junitxml
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/logs
+        destination: logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2460,6 +3286,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2517,7 +3345,7 @@
           if [ -d ~/dtest_jars ]; then
             cp ~/dtest_jars/dtest* /tmp/cassandra/build/
           fi
-          ant long-test -Dno-build-test=true
+          ant long-test -Dtest.classlistfile=/tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt  -Dtest.classlistprefix=unit -Dno-build-test=true
         no_output_timeout: 15m
     - store_test_results:
         path: /tmp/cassandra/build/test/output/
@@ -2547,6 +3375,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2603,6 +3433,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2728,6 +3560,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2839,6 +3673,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2936,6 +3772,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2991,18 +3829,30 @@
         requires:
         - start_utests_long
         - build
+    - start_utests_cdc:
+        type: approval
+    - utests_cdc:
+        requires:
+        - start_utests_cdc
+        - build
     - start_utests_compression:
         type: approval
     - utests_compression:
         requires:
         - start_utests_compression
         - build
+    - start_utests_stress:
+        type: approval
+    - utests_stress:
+        requires:
+        - start_utests_stress
+        - build
     - start_j8_dtest_jars_build:
         type: approval
     - j8_dtest_jars_build:
         requires:
-        - start_j8_dtest_jars_build
         - build
+        - start_j8_dtest_jars_build
     - start_jvm_upgrade_dtests:
         type: approval
     - j8_jvm_upgrade_dtests:
@@ -3033,11 +3883,17 @@
         requires:
         - start_j8_dtests_large_vnode
         - build
-    - start_j8_upgrade_dtests:
+    - start_upgrade_dtests:
         type: approval
     - j8_upgrade_dtests:
         requires:
-        - start_j8_upgrade_dtests
+        - start_upgrade_dtests
+        - build
+    - start_j8_dtests_offheap:
+        type: approval
+    - j8_dtests_offheap:
+        requires:
+        - start_j8_dtests_offheap
         - build
   pre-commit_tests:
     jobs:
@@ -3064,18 +3920,30 @@
         requires:
         - start_utests_long
         - build
+    - start_utests_cdc:
+        type: approval
+    - utests_cdc:
+        requires:
+        - start_utests_cdc
+        - build
     - start_utests_compression:
         type: approval
     - utests_compression:
         requires:
         - start_utests_compression
         - build
+    - start_utests_stress:
+        type: approval
+    - utests_stress:
+        requires:
+        - start_utests_stress
+        - build
     - start_jvm_upgrade_dtests:
         type: approval
     - j8_dtest_jars_build:
         requires:
-        - start_jvm_upgrade_dtests
         - build
+        - start_jvm_upgrade_dtests
     - j8_jvm_upgrade_dtests:
         requires:
         - j8_dtest_jars_build
@@ -3095,9 +3963,15 @@
         requires:
         - start_j8_dtests_large
         - build
-    - start_upgrade_dtests:
+    - start_upgrade_tests:
         type: approval
     - j8_upgrade_dtests:
         requires:
-        - start_upgrade_dtests
+        - start_upgrade_tests
+        - build
+    - start_j8_dtests_offheap:
+        type: approval
+    - j8_dtests_offheap:
+        requires:
+        - start_j8_dtests_offheap
         - build
diff --git a/.circleci/config.yml.PAID b/.circleci/config.yml.PAID
index bac1534..ee255c1 100644
--- a/.circleci/config.yml.PAID
+++ b/.circleci/config.yml.PAID
@@ -21,10 +21,10 @@
   j8_jvm_upgrade_dtests:
     docker:
     - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
-    resource_class: medium
+    resource_class: xlarge
     working_directory: ~/
     shell: /bin/bash -eo pipefail -l
-    parallelism: 1
+    parallelism: 4
     steps:
     - attach_workspace:
         at: /home/cassandra
@@ -113,6 +113,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -186,7 +188,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=test-compression
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -208,8 +212,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -294,6 +298,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -371,6 +377,193 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  utests_stress_repeat:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 25
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Log Environment Information
+        command: |
+          echo '*** id ***'
+          id
+          echo '*** cat /proc/cpuinfo ***'
+          cat /proc/cpuinfo
+          echo '*** free -m ***'
+          free -m
+          echo '*** df -m ***'
+          df -m
+          echo '*** ifconfig -a ***'
+          ifconfig -a
+          echo '*** uname -a ***'
+          uname -a
+          echo '*** mount ***'
+          mount
+          echo '*** env ***'
+          env
+          echo '*** java ***'
+          which java
+          java -version
+    - run:
+        name: Repeatedly run new or modifed JUnit tests
+        no_output_timeout: 15m
+        command: |
+          set -x
+          export PATH=$JAVA_HOME/bin:$PATH
+          time mv ~/cassandra /tmp
+          cd /tmp/cassandra
+          if [ -d ~/dtest_jars ]; then
+            cp ~/dtest_jars/dtest* /tmp/cassandra/build/
+          fi
+
+          # Calculate the number of test iterations to be run by the current parallel runner.
+          count=$((${REPEATED_UTESTS_STRESS_COUNT} / CIRCLE_NODE_TOTAL))
+          if (($CIRCLE_NODE_INDEX < (${REPEATED_UTESTS_STRESS_COUNT} % CIRCLE_NODE_TOTAL))); then
+            count=$((count+1))
+          fi
+
+          # Put manually specified tests and automatically detected tests together, removing duplicates
+          tests=$(echo ${REPEATED_UTESTS_STRESS} | sed -e "s/<nil>//" | sed -e "s/ //" | tr "," "\n" | tr " " "\n" | sort -n | uniq -u)
+          echo "Tests to be repeated: ${tests}"
+
+          # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
+          target=stress-test-some
+          testtag=""
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
+            testtag="compression"
+          fi
+
+          # Run each test class as many times as requested.
+          exit_code="$?"
+          for test in $tests; do
+
+              # Split class and method names from the test name
+              if [[ $test =~ "#" ]]; then
+                class=${test%"#"*}
+                method=${test#*"#"}
+              else
+                class=$test
+                method=""
+              fi
+
+              # Prepare the -Dtest.name argument.
+              # It can be the fully qualified class name or the short class name, depending on the target.
+              if [[ $target == "test" || \
+                    $target == "test-cdc" || \
+                    $target == "test-compression" || \
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
+                name_arg="-Dtest.name=${class##*.}"
+              else
+                name_arg="-Dtest.name=$class"
+              fi
+
+              # Prepare the -Dtest.methods argument, which is optional
+              if [[ $method == "" ]]; then
+                methods_arg=""
+              else
+                methods_arg="-Dtest.methods=$method"
+              fi
+
+              for i in $(seq -w 1 $count); do
+                echo "Running test $test, iteration $i of $count"
+
+                # run the test
+                status="passes"
+                if !( set -o pipefail && \
+                      ant stress-test-some $name_arg $methods_arg -Dno-build-test=true | \
+                      tee stdout.txt \
+                    ); then
+                  status="fails"
+                  exit_code=1
+                fi
+
+                # move the stdout output file
+                dest=/tmp/results/repeated_utests/stdout/${status}/${i}
+                mkdir -p $dest
+                mv stdout.txt $dest/${test}.txt
+
+                # move the XML output files
+                source=build/test/output/${testtag}
+                dest=/tmp/results/repeated_utests/output/${status}/${i}
+                mkdir -p $dest
+                if [[ -d $source && -n "$(ls $source)" ]]; then
+                  mv $source/* $dest/
+                fi
+
+                # move the log files
+                source=build/test/logs/${testtag}
+                dest=/tmp/results/repeated_utests/logs/${status}/${i}
+                mkdir -p $dest
+                if [[ -d $source && -n "$(ls $source)" ]]; then
+                  mv $source/* $dest/
+                fi
+
+                # maybe stop iterations on test failure
+                if [[ ${REPEATED_TESTS_STOP_ON_FAILURE} = true ]] && (( $exit_code > 0 )); then
+                  break
+                fi
+              done
+          done
+          (exit ${exit_code})
+    - store_test_results:
+        path: /tmp/results/repeated_utests/output
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/stdout
+        destination: stdout
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/output
+        destination: junitxml
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/logs
+        destination: logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -428,6 +621,135 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  j8_dtests_offheap_repeat:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: large
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 25
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Clone Cassandra dtest Repository (via git)
+        command: |
+          git clone --single-branch --branch $DTEST_BRANCH --depth 1 $DTEST_REPO ~/cassandra-dtest
+    - run:
+        name: Configure virtualenv and python Dependencies
+        command: |
+          # note, this should be super quick as all dependencies should be pre-installed in the docker image
+          # if additional dependencies were added to requirmeents.txt and the docker image hasn't been updated
+          # we'd have to install it here at runtime -- which will make things slow, so do yourself a favor and
+          # rebuild the docker image! (it automatically pulls the latest requirements.txt on build)
+          source ~/env3.6/bin/activate
+          export PATH=$JAVA_HOME/bin:$PATH
+          pip3 install --upgrade -r ~/cassandra-dtest/requirements.txt
+          pip3 freeze
+    - run:
+        name: Run repeated Python DTests
+        no_output_timeout: 15m
+        command: |
+          if [ "${REPEATED_DTESTS}" == "<nil>" ]; then
+            echo "Repeated dtest name hasn't been defined, exiting without running any test"
+          elif [ "${REPEATED_DTESTS_COUNT}" == "<nil>" ]; then
+            echo "Repeated dtest count hasn't been defined, exiting without running any test"
+          elif [ "${REPEATED_DTESTS_COUNT}" -le 0 ]; then
+            echo "Repeated dtest count is lesser or equals than zero, exiting without running any test"
+          else
+
+            # Calculate the number of test iterations to be run by the current parallel runner.
+            # Since we are running the same test multiple times there is no need to use `circleci tests split`.
+            count=$((${REPEATED_DTESTS_COUNT} / CIRCLE_NODE_TOTAL))
+            if (($CIRCLE_NODE_INDEX < (${REPEATED_DTESTS_COUNT} % CIRCLE_NODE_TOTAL))); then
+              count=$((count+1))
+            fi
+
+            if (($count <= 0)); then
+              echo "No tests to run in this runner"
+            else
+              echo "Running ${REPEATED_DTESTS} $count times"
+
+              source ~/env3.6/bin/activate
+              export PATH=$JAVA_HOME/bin:$PATH
+
+              java -version
+              cd ~/cassandra-dtest
+              mkdir -p /tmp/dtest
+
+              echo "env: $(env)"
+              echo "** done env"
+              mkdir -p /tmp/results/dtests
+
+              tests_arg=$(echo ${REPEATED_DTESTS} | sed -e "s/,/ /g")
+
+              stop_on_failure_arg=""
+              if ${REPEATED_TESTS_STOP_ON_FAILURE}; then
+                stop_on_failure_arg="-x"
+              fi
+
+              vnodes_args=""
+              if true; then
+                vnodes_args="--use-vnodes --num-tokens=32"
+              fi
+
+              upgrade_arg=""
+              if false; then
+                upgrade_arg="--execute-upgrade-tests --upgrade-target-version-only --upgrade-version-selection all"
+              fi
+
+              # we need the "set -o pipefail" here so that the exit code that circleci will actually use is from pytest and not the exit code from tee
+              set -o pipefail && cd ~/cassandra-dtest && pytest $vnodes_args --count=$count $stop_on_failure_arg $upgrade_arg --log-cli-level=DEBUG --junit-xml=/tmp/results/dtests/pytest_result.xml -s --cassandra-dir=/home/cassandra/cassandra --keep-test-dir --use-off-heap-memtables --skip-resource-intensive-tests $tests_arg | tee /tmp/dtest/stdout.txt
+            fi
+          fi
+    - store_test_results:
+        path: /tmp/results
+    - store_artifacts:
+        path: /tmp/dtest
+        destination: dtest
+    - store_artifacts:
+        path: ~/cassandra-dtest/logs
+        destination: dtest_logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -553,6 +875,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -626,7 +950,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=long-testsome
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -648,8 +974,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -734,6 +1060,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -807,7 +1135,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=testsome
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -829,8 +1159,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -915,6 +1245,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -992,6 +1324,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1117,6 +1451,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1217,6 +1553,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1342,6 +1680,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1419,6 +1759,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1438,7 +1780,7 @@
   j8_jvm_upgrade_dtests_repeat:
     docker:
     - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
-    resource_class: medium
+    resource_class: large
     working_directory: ~/
     shell: /bin/bash -eo pipefail -l
     parallelism: 25
@@ -1492,7 +1834,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=test-jvm-dtest-some
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -1514,8 +1858,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -1600,6 +1944,289 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  utests_cdc:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 25
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Determine unit Tests to Run
+        command: |
+          # reminder: this code (along with all the steps) is independently executed on every circle container
+          # so the goal here is to get the circleci script to return the tests *this* container will run
+          # which we do via the `circleci` cli tool.
+
+          rm -fr ~/cassandra-dtest/upgrade_tests
+          echo "***java tests***"
+
+          # get all of our unit test filenames
+          set -eo pipefail && circleci tests glob "$HOME/cassandra/test/unit/**/*.java" > /tmp/all_java_unit_tests.txt
+
+          # split up the unit tests into groups based on the number of containers we have
+          set -eo pipefail && circleci tests split --split-by=timings --timings-type=filename --index=${CIRCLE_NODE_INDEX} --total=${CIRCLE_NODE_TOTAL} /tmp/all_java_unit_tests.txt > /tmp/java_tests_${CIRCLE_NODE_INDEX}.txt
+          set -eo pipefail && cat /tmp/java_tests_${CIRCLE_NODE_INDEX}.txt | sed "s;^/home/cassandra/cassandra/test/unit/;;g" | grep "Test\.java$"  > /tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt
+          echo "** /tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt"
+          cat /tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt
+        no_output_timeout: 15m
+    - run:
+        name: Log Environment Information
+        command: |
+          echo '*** id ***'
+          id
+          echo '*** cat /proc/cpuinfo ***'
+          cat /proc/cpuinfo
+          echo '*** free -m ***'
+          free -m
+          echo '*** df -m ***'
+          df -m
+          echo '*** ifconfig -a ***'
+          ifconfig -a
+          echo '*** uname -a ***'
+          uname -a
+          echo '*** mount ***'
+          mount
+          echo '*** env ***'
+          env
+          echo '*** java ***'
+          which java
+          java -version
+    - run:
+        name: Run Unit Tests (testclasslist-cdc)
+        command: |
+          set -x
+          export PATH=$JAVA_HOME/bin:$PATH
+          time mv ~/cassandra /tmp
+          cd /tmp/cassandra
+          if [ -d ~/dtest_jars ]; then
+            cp ~/dtest_jars/dtest* /tmp/cassandra/build/
+          fi
+          test_timeout=$(grep 'name="test.unit.timeout"' build.xml | awk -F'"' '{print $4}' || true)
+          if [ -z "$test_timeout" ]; then
+            test_timeout=$(grep 'name="test.timeout"' build.xml | awk -F'"' '{print $4}')
+          fi
+          ant testclasslist-cdc -Dtest.timeout="$test_timeout" -Dtest.classlistfile=/tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt  -Dtest.classlistprefix=unit -Dno-build-test=true
+        no_output_timeout: 15m
+    - store_test_results:
+        path: /tmp/cassandra/build/test/output/
+    - store_artifacts:
+        path: /tmp/cassandra/build/test/output
+        destination: junitxml
+    - store_artifacts:
+        path: /tmp/cassandra/build/test/logs
+        destination: logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  utests_stress:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 1
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Run Unit Tests (stress-test)
+        command: |
+          export PATH=$JAVA_HOME/bin:$PATH
+          time mv ~/cassandra /tmp
+          cd /tmp/cassandra
+          if [ -d ~/dtest_jars ]; then
+            cp ~/dtest_jars/dtest* /tmp/cassandra/build/
+          fi
+          ant stress-test -Dtest.classlistfile=/tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt  -Dtest.classlistprefix=unit -Dno-build-test=true
+        no_output_timeout: 15m
+    - store_test_results:
+        path: /tmp/cassandra/build/test/output/
+    - store_artifacts:
+        path: /tmp/cassandra/build/test/output
+        destination: junitxml
+    - store_artifacts:
+        path: /tmp/cassandra/build/test/logs
+        destination: logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  j8_dtests_offheap:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: large
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 50
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Log Environment Information
+        command: |
+          echo '*** id ***'
+          id
+          echo '*** cat /proc/cpuinfo ***'
+          cat /proc/cpuinfo
+          echo '*** free -m ***'
+          free -m
+          echo '*** df -m ***'
+          df -m
+          echo '*** ifconfig -a ***'
+          ifconfig -a
+          echo '*** uname -a ***'
+          uname -a
+          echo '*** mount ***'
+          mount
+          echo '*** env ***'
+          env
+          echo '*** java ***'
+          which java
+          java -version
+    - run:
+        name: Clone Cassandra dtest Repository (via git)
+        command: |
+          git clone --single-branch --branch $DTEST_BRANCH --depth 1 $DTEST_REPO ~/cassandra-dtest
+    - run:
+        name: Configure virtualenv and python Dependencies
+        command: |
+          # note, this should be super quick as all dependencies should be pre-installed in the docker image
+          # if additional dependencies were added to requirmeents.txt and the docker image hasn't been updated
+          # we'd have to install it here at runtime -- which will make things slow, so do yourself a favor and
+          # rebuild the docker image! (it automatically pulls the latest requirements.txt on build)
+          source ~/env3.6/bin/activate
+          export PATH=$JAVA_HOME/bin:$PATH
+          pip3 install --upgrade -r ~/cassandra-dtest/requirements.txt
+          pip3 freeze
+    - run:
+        name: Determine Tests to Run (j8_dtests_offheap)
+        no_output_timeout: 5m
+        command: "# reminder: this code (along with all the steps) is independently executed on every circle container\n# so the goal here is to get the circleci script to return the tests *this* container will run\n# which we do via the `circleci` cli tool.\n\ncd cassandra-dtest\nsource ~/env3.6/bin/activate\nexport PATH=$JAVA_HOME/bin:$PATH\n\nif [ -n '' ]; then\n  export \nfi\n\necho \"***Collected DTests (j8_dtests_offheap)***\"\nset -eo pipefail && ./run_dtests.py --use-vnodes --use-off-heap-memtables --skip-resource-intensive-tests --dtest-print-tests-only --dtest-print-tests-output=/tmp/all_dtest_tests_j8_dtests_offheap_raw --cassandra-dir=../cassandra\nif [ -z '' ]; then\n  mv /tmp/all_dtest_tests_j8_dtests_offheap_raw /tmp/all_dtest_tests_j8_dtests_offheap\nelse\n  grep -e '' /tmp/all_dtest_tests_j8_dtests_offheap_raw > /tmp/all_dtest_tests_j8_dtests_offheap || { echo \"Filter did not match any tests! Exiting build.\"; exit 0; }\nfi\nset -eo pipefail && circleci tests split --split-by=timings --timings-type=classname /tmp/all_dtest_tests_j8_dtests_offheap > /tmp/split_dtest_tests_j8_dtests_offheap.txt\ncat /tmp/split_dtest_tests_j8_dtests_offheap.txt | tr '\\n' ' ' > /tmp/split_dtest_tests_j8_dtests_offheap_final.txt\ncat /tmp/split_dtest_tests_j8_dtests_offheap_final.txt\n"
+    - run:
+        name: Run dtests (j8_dtests_offheap)
+        no_output_timeout: 15m
+        command: "echo \"cat /tmp/split_dtest_tests_j8_dtests_offheap_final.txt\"\ncat /tmp/split_dtest_tests_j8_dtests_offheap_final.txt\n\nsource ~/env3.6/bin/activate\nexport PATH=$JAVA_HOME/bin:$PATH\nif [ -n '' ]; then\n  export \nfi\n\njava -version\ncd ~/cassandra-dtest\nmkdir -p /tmp/dtest\n\necho \"env: $(env)\"\necho \"** done env\"\nmkdir -p /tmp/results/dtests\n# we need the \"set -o pipefail\" here so that the exit code that circleci will actually use is from pytest and not the exit code from tee\nexport SPLIT_TESTS=`cat /tmp/split_dtest_tests_j8_dtests_offheap_final.txt`\nif [ ! -z \"$SPLIT_TESTS\" ]; then\n  set -o pipefail && cd ~/cassandra-dtest && pytest --use-vnodes --num-tokens=16 --use-off-heap-memtables --skip-resource-intensive-tests --log-level=\"DEBUG\" --junit-xml=/tmp/results/dtests/pytest_result_j8_dtests_offheap.xml -s --cassandra-dir=/home/cassandra/cassandra --keep-test-dir $SPLIT_TESTS 2>&1 | tee /tmp/dtest/stdout.txt\nelse\n  echo \"Tune your parallelism, there are more containers than test classes. Nothing to do in this container\"\n  (exit 1)\nfi\n"
+    - store_test_results:
+        path: /tmp/results
+    - store_artifacts:
+        path: /tmp/dtest
+        destination: dtest_j8_dtests_offheap
+    - store_artifacts:
+        path: ~/cassandra-dtest/logs
+        destination: dtest_j8_dtests_offheap_logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1711,6 +2338,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1788,6 +2417,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1865,6 +2496,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -1938,7 +2571,9 @@
           # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
           target=test-jvm-dtest-some
           testtag=""
-          if [[ $target == "test-compression" ]]; then
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
             testtag="compression"
           fi
 
@@ -1960,8 +2595,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name_arg="-Dtest.name=${class##*.}"
               else
                 name_arg="-Dtest.name=$class"
@@ -2046,6 +2681,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2135,8 +2772,8 @@
               if [[ $target == "test" || \
                     $target == "test-cdc" || \
                     $target == "test-compression" || \
-                    $target == "test-system-keyspace-directory" || \
-                    $target == "long-test" ]]; then
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
                 name="-Dtest.name=$class_name"
               else
                 name="-Dtest.name=$class_path"
@@ -2224,6 +2861,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2349,6 +2988,193 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
+    - REPEATED_JVM_DTESTS: null
+    - REPEATED_JVM_DTESTS_COUNT: 500
+    - REPEATED_JVM_UPGRADE_DTESTS: null
+    - REPEATED_JVM_UPGRADE_DTESTS_COUNT: 500
+    - REPEATED_DTESTS: null
+    - REPEATED_DTESTS_COUNT: 500
+    - REPEATED_LARGE_DTESTS: null
+    - REPEATED_LARGE_DTESTS_COUNT: 100
+    - REPEATED_UPGRADE_DTESTS: null
+    - REPEATED_UPGRADE_DTESTS_COUNT: 25
+    - REPEATED_ANT_TEST_TARGET: testsome
+    - REPEATED_ANT_TEST_CLASS: null
+    - REPEATED_ANT_TEST_METHODS: null
+    - REPEATED_ANT_TEST_COUNT: 500
+    - JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - JDK_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+  utests_cdc_repeat:
+    docker:
+    - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
+    resource_class: medium
+    working_directory: ~/
+    shell: /bin/bash -eo pipefail -l
+    parallelism: 25
+    steps:
+    - attach_workspace:
+        at: /home/cassandra
+    - run:
+        name: Log Environment Information
+        command: |
+          echo '*** id ***'
+          id
+          echo '*** cat /proc/cpuinfo ***'
+          cat /proc/cpuinfo
+          echo '*** free -m ***'
+          free -m
+          echo '*** df -m ***'
+          df -m
+          echo '*** ifconfig -a ***'
+          ifconfig -a
+          echo '*** uname -a ***'
+          uname -a
+          echo '*** mount ***'
+          mount
+          echo '*** env ***'
+          env
+          echo '*** java ***'
+          which java
+          java -version
+    - run:
+        name: Repeatedly run new or modifed JUnit tests
+        no_output_timeout: 15m
+        command: |
+          set -x
+          export PATH=$JAVA_HOME/bin:$PATH
+          time mv ~/cassandra /tmp
+          cd /tmp/cassandra
+          if [ -d ~/dtest_jars ]; then
+            cp ~/dtest_jars/dtest* /tmp/cassandra/build/
+          fi
+
+          # Calculate the number of test iterations to be run by the current parallel runner.
+          count=$((${REPEATED_UTESTS_COUNT} / CIRCLE_NODE_TOTAL))
+          if (($CIRCLE_NODE_INDEX < (${REPEATED_UTESTS_COUNT} % CIRCLE_NODE_TOTAL))); then
+            count=$((count+1))
+          fi
+
+          # Put manually specified tests and automatically detected tests together, removing duplicates
+          tests=$(echo ${REPEATED_UTESTS} | sed -e "s/<nil>//" | sed -e "s/ //" | tr "," "\n" | tr " " "\n" | sort -n | uniq -u)
+          echo "Tests to be repeated: ${tests}"
+
+          # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
+          target=test-cdc
+          testtag=""
+          if [[ $target == "test-cdc" ]]; then
+            testtag="cdc"
+          elif [[ $target == "test-compression" ]]; then
+            testtag="compression"
+          fi
+
+          # Run each test class as many times as requested.
+          exit_code="$?"
+          for test in $tests; do
+
+              # Split class and method names from the test name
+              if [[ $test =~ "#" ]]; then
+                class=${test%"#"*}
+                method=${test#*"#"}
+              else
+                class=$test
+                method=""
+              fi
+
+              # Prepare the -Dtest.name argument.
+              # It can be the fully qualified class name or the short class name, depending on the target.
+              if [[ $target == "test" || \
+                    $target == "test-cdc" || \
+                    $target == "test-compression" || \
+                    $target == "long-test" || \
+                    $target == "stress-test" ]]; then
+                name_arg="-Dtest.name=${class##*.}"
+              else
+                name_arg="-Dtest.name=$class"
+              fi
+
+              # Prepare the -Dtest.methods argument, which is optional
+              if [[ $method == "" ]]; then
+                methods_arg=""
+              else
+                methods_arg="-Dtest.methods=$method"
+              fi
+
+              for i in $(seq -w 1 $count); do
+                echo "Running test $test, iteration $i of $count"
+
+                # run the test
+                status="passes"
+                if !( set -o pipefail && \
+                      ant test-cdc $name_arg $methods_arg -Dno-build-test=true | \
+                      tee stdout.txt \
+                    ); then
+                  status="fails"
+                  exit_code=1
+                fi
+
+                # move the stdout output file
+                dest=/tmp/results/repeated_utests/stdout/${status}/${i}
+                mkdir -p $dest
+                mv stdout.txt $dest/${test}.txt
+
+                # move the XML output files
+                source=build/test/output/${testtag}
+                dest=/tmp/results/repeated_utests/output/${status}/${i}
+                mkdir -p $dest
+                if [[ -d $source && -n "$(ls $source)" ]]; then
+                  mv $source/* $dest/
+                fi
+
+                # move the log files
+                source=build/test/logs/${testtag}
+                dest=/tmp/results/repeated_utests/logs/${status}/${i}
+                mkdir -p $dest
+                if [[ -d $source && -n "$(ls $source)" ]]; then
+                  mv $source/* $dest/
+                fi
+
+                # maybe stop iterations on test failure
+                if [[ ${REPEATED_TESTS_STOP_ON_FAILURE} = true ]] && (( $exit_code > 0 )); then
+                  break
+                fi
+              done
+          done
+          (exit ${exit_code})
+    - store_test_results:
+        path: /tmp/results/repeated_utests/output
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/stdout
+        destination: stdout
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/output
+        destination: junitxml
+    - store_artifacts:
+        path: /tmp/results/repeated_utests/logs
+        destination: logs
+    environment:
+    - JAVA8_HOME: /usr/lib/jvm/java-8-openjdk-amd64
+    - ANT_HOME: /usr/share/ant
+    - LANG: en_US.UTF-8
+    - KEEP_TEST_DIR: true
+    - DEFAULT_DIR: /home/cassandra/cassandra-dtest
+    - PYTHONIOENCODING: utf-8
+    - PYTHONUNBUFFERED: true
+    - CASS_DRIVER_NO_EXTENSIONS: true
+    - CASS_DRIVER_NO_CYTHON: true
+    - CASSANDRA_SKIP_SYNC: true
+    - DTEST_REPO: https://github.com/apache/cassandra-dtest.git
+    - DTEST_BRANCH: trunk
+    - CCM_MAX_HEAP_SIZE: 1024M
+    - CCM_HEAP_NEWSIZE: 256M
+    - REPEATED_TESTS_STOP_ON_FAILURE: false
+    - REPEATED_UTESTS: null
+    - REPEATED_UTESTS_COUNT: 500
+    - REPEATED_UTESTS_LONG: null
+    - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2368,10 +3194,10 @@
   j8_jvm_dtests:
     docker:
     - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest
-    resource_class: medium
+    resource_class: large
     working_directory: ~/
     shell: /bin/bash -eo pipefail -l
-    parallelism: 1
+    parallelism: 10
     steps:
     - attach_workspace:
         at: /home/cassandra
@@ -2460,6 +3286,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2517,7 +3345,7 @@
           if [ -d ~/dtest_jars ]; then
             cp ~/dtest_jars/dtest* /tmp/cassandra/build/
           fi
-          ant long-test -Dno-build-test=true
+          ant long-test -Dtest.classlistfile=/tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt  -Dtest.classlistprefix=unit -Dno-build-test=true
         no_output_timeout: 15m
     - store_test_results:
         path: /tmp/cassandra/build/test/output/
@@ -2547,6 +3375,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2603,6 +3433,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2728,6 +3560,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2839,6 +3673,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2936,6 +3772,8 @@
     - REPEATED_UTESTS_COUNT: 500
     - REPEATED_UTESTS_LONG: null
     - REPEATED_UTESTS_LONG_COUNT: 100
+    - REPEATED_UTESTS_STRESS: null
+    - REPEATED_UTESTS_STRESS_COUNT: 500
     - REPEATED_JVM_DTESTS: null
     - REPEATED_JVM_DTESTS_COUNT: 500
     - REPEATED_JVM_UPGRADE_DTESTS: null
@@ -2991,18 +3829,30 @@
         requires:
         - start_utests_long
         - build
+    - start_utests_cdc:
+        type: approval
+    - utests_cdc:
+        requires:
+        - start_utests_cdc
+        - build
     - start_utests_compression:
         type: approval
     - utests_compression:
         requires:
         - start_utests_compression
         - build
+    - start_utests_stress:
+        type: approval
+    - utests_stress:
+        requires:
+        - start_utests_stress
+        - build
     - start_j8_dtest_jars_build:
         type: approval
     - j8_dtest_jars_build:
         requires:
-        - start_j8_dtest_jars_build
         - build
+        - start_j8_dtest_jars_build
     - start_jvm_upgrade_dtests:
         type: approval
     - j8_jvm_upgrade_dtests:
@@ -3033,11 +3883,17 @@
         requires:
         - start_j8_dtests_large_vnode
         - build
-    - start_j8_upgrade_dtests:
+    - start_upgrade_dtests:
         type: approval
     - j8_upgrade_dtests:
         requires:
-        - start_j8_upgrade_dtests
+        - start_upgrade_dtests
+        - build
+    - start_j8_dtests_offheap:
+        type: approval
+    - j8_dtests_offheap:
+        requires:
+        - start_j8_dtests_offheap
         - build
   pre-commit_tests:
     jobs:
@@ -3064,18 +3920,30 @@
         requires:
         - start_utests_long
         - build
+    - start_utests_cdc:
+        type: approval
+    - utests_cdc:
+        requires:
+        - start_utests_cdc
+        - build
     - start_utests_compression:
         type: approval
     - utests_compression:
         requires:
         - start_utests_compression
         - build
+    - start_utests_stress:
+        type: approval
+    - utests_stress:
+        requires:
+        - start_utests_stress
+        - build
     - start_jvm_upgrade_dtests:
         type: approval
     - j8_dtest_jars_build:
         requires:
-        - start_jvm_upgrade_dtests
         - build
+        - start_jvm_upgrade_dtests
     - j8_jvm_upgrade_dtests:
         requires:
         - j8_dtest_jars_build
@@ -3095,9 +3963,15 @@
         requires:
         - start_j8_dtests_large
         - build
-    - start_upgrade_dtests:
+    - start_upgrade_tests:
         type: approval
     - j8_upgrade_dtests:
         requires:
-        - start_upgrade_dtests
+        - start_upgrade_tests
+        - build
+    - start_j8_dtests_offheap:
+        type: approval
+    - j8_dtests_offheap:
+        requires:
+        - start_j8_dtests_offheap
         - build
diff --git a/.circleci/config_template.yml b/.circleci/config_template.yml
index a4455c3..a5006c4 100644
--- a/.circleci/config_template.yml
+++ b/.circleci/config_template.yml
@@ -63,6 +63,14 @@
     # The number of times that new, modified or manually specified long unit tests should be run.
     REPEATED_UTESTS_LONG_COUNT: 100
 
+    # Comma-separated list of tests that should be included in the repeated run for stress unit tests,
+    # in addition to automatically detected new and modified tests. For example:
+    # REPEATED_UTESTS_STRESS: org.apache.cassandra.stress.generate.DistributionGaussianTest
+    # REPEATED_UTESTS_STRESS: org.apache.cassandra.stress.generate.DistributionGaussianTest#simpleGaussian
+    REPEATED_UTESTS_STRESS:
+    # The number of times that new, modified or manually specified stress unit tests should be run.
+    REPEATED_UTESTS_STRESS_COUNT: 500
+
     # Comma-separated list of tests that should be included in the repeated run for JVM dtests,
     # in addition to automatically detected new and modified tests. For example:
     # REPEATED_JVM_DTESTS: org.apache.cassandra.distributed.test.PagingTest
@@ -110,7 +118,6 @@
     # REPEATED_ANT_TEST_TARGET: test-jvm-dtest-some
     # REPEATED_ANT_TEST_TARGET: test-cdc
     # REPEATED_ANT_TEST_TARGET: test-compression
-    # REPEATED_ANT_TEST_TARGET: test-system-keyspace-directory
     REPEATED_ANT_TEST_TARGET: testsome
     # The name of JUnit class to be run multiple times, for example:
     # REPEATED_ANT_TEST_CLASS: org.apache.cassandra.cql3.ViewTest
@@ -218,7 +225,7 @@
         requires:
           - start_j8_cqlshlib_cython_tests
           - build
-    # specialized unit tests (all run using Java 8)
+    # specialized unit tests (all run on request)
     - start_utests_long:
         type: approval
     - utests_long:
@@ -231,6 +238,18 @@
         requires:
           - start_utests_long_repeat
           - build
+    - start_utests_cdc:
+        type: approval
+    - utests_cdc:
+        requires:
+          - start_utests_cdc
+          - build
+    - start_utests_cdc_repeat:
+        type: approval
+    - utests_cdc_repeat:
+        requires:
+          - start_utests_cdc_repeat
+          - build
     - start_utests_compression:
         type: approval
     - utests_compression:
@@ -243,12 +262,24 @@
         requires:
           - start_utests_compression_repeat
           - build
+    - start_utests_stress:
+        type: approval
+    - utests_stress:
+        requires:
+          - start_utests_stress
+          - build
+    - start_utests_stress_repeat:
+        type: approval
+    - utests_stress_repeat:
+        requires:
+          - start_utests_stress_repeat
+          - build
     - start_j8_dtest_jars_build:
         type: approval
     - j8_dtest_jars_build:
         requires:
-          - start_j8_dtest_jars_build
           - build
+          - start_j8_dtest_jars_build
     - start_jvm_upgrade_dtests:
         type: approval
     - j8_jvm_upgrade_dtests:
@@ -311,12 +342,12 @@
         requires:
           - start_j8_dtests_large_vnode_repeat
           - build
-    # Python upgrade tests
-    - start_j8_upgrade_dtests:
+    # Java 8 upgrade tests
+    - start_upgrade_dtests:
         type: approval
     - j8_upgrade_dtests:
         requires:
-          - start_j8_upgrade_dtests
+          - start_upgrade_dtests
           - build
     - start_j8_upgrade_dtests_repeat:
         type: approval
@@ -331,7 +362,19 @@
         requires:
           - start_j8_repeated_ant_test
           - build
-
+    # Java 8 off-heap dtests
+    - start_j8_dtests_offheap:
+        type: approval
+    - j8_dtests_offheap:
+        requires:
+          - start_j8_dtests_offheap
+          - build
+    - start_j8_dtests_offheap_repeat:
+        type: approval
+    - j8_dtests_offheap_repeat:
+        requires:
+          - start_j8_dtests_offheap_repeat
+          - build
 
 pre-commit_jobs: &pre-commit_jobs
   jobs:
@@ -340,7 +383,7 @@
     - build:
         requires:
           - start_pre-commit_tests
-    # Java 8 unit tests will be run automatically
+    # Java 8 unit tests
     - j8_unit_tests:
         requires:
           - build
@@ -359,7 +402,7 @@
     - j8_cqlshlib_cython_tests:
         requires:
           - build
-    # specialized unit tests (all run on request using Java 8)
+    # specialized unit tests (all run on request)
     - start_utests_long:
         type: approval
     - utests_long:
@@ -370,6 +413,16 @@
         requires:
           - start_utests_long
           - build
+    - start_utests_cdc:
+        type: approval
+    - utests_cdc:
+        requires:
+          - start_utests_cdc
+          - build
+    - utests_cdc_repeat:
+        requires:
+          - start_utests_cdc
+          - build
     - start_utests_compression:
         type: approval
     - utests_compression:
@@ -380,12 +433,22 @@
         requires:
           - start_utests_compression
           - build
+    - start_utests_stress:
+        type: approval
+    - utests_stress:
+        requires:
+          - start_utests_stress
+          - build
+    - utests_stress_repeat:
+        requires:
+          - start_utests_stress
+          - build
     - start_jvm_upgrade_dtests:
         type: approval
     - j8_dtest_jars_build:
         requires:
-          - start_jvm_upgrade_dtests
           - build
+          - start_jvm_upgrade_dtests
     - j8_jvm_upgrade_dtests:
         requires:
           - j8_dtest_jars_build
@@ -425,15 +488,30 @@
           - start_j8_dtests_large
           - build
     # Java 8 upgrade tests (on request)
-    - start_upgrade_dtests:
+    - start_upgrade_tests:
         type: approval
     - j8_upgrade_dtests:
         requires:
-          - start_upgrade_dtests
+          - start_upgrade_tests
           - build
     - j8_upgrade_dtests_repeat:
         requires:
-          - start_upgrade_dtests
+          - start_upgrade_tests
+          - build
+    # Java 8 repeated utest
+    - j8_repeated_ant_test:
+        requires:
+          - build
+    # Java 8 off-heap ddtests
+    - start_j8_dtests_offheap:
+        type: approval
+    - j8_dtests_offheap:
+        requires:
+          - start_j8_dtests_offheap
+          - build
+    - j8_dtests_offheap_repeat:
+        requires:
+          - start_j8_dtests_offheap
           - build
 
 workflows:
@@ -508,7 +586,7 @@
       - run_cqlshlib_cython_tests
 
   j8_jvm_dtests:
-    <<: *j8_seq_executor
+    <<: *j8_small_par_executor
     steps:
       - attach_workspace:
           at: /home/cassandra
@@ -520,7 +598,7 @@
           classlistprefix: distributed
 
   j8_jvm_upgrade_dtests:
-    <<: *j8_seq_executor
+    <<: *j8_medium_par_executor
     steps:
       - attach_workspace:
           at: /home/cassandra
@@ -540,6 +618,16 @@
       - run_junit_tests:
           target: long-test
 
+  utests_cdc:
+    <<: *j8_par_executor
+    steps:
+      - attach_workspace:
+          at: /home/cassandra
+      - create_junit_containers
+      - log_environment
+      - run_parallel_junit_tests:
+          target: testclasslist-cdc
+
   utests_compression:
     <<: *j8_par_executor
     steps:
@@ -550,6 +638,14 @@
       - run_parallel_junit_tests:
           target: testclasslist-compression
 
+  utests_stress:
+    <<: *j8_seq_executor
+    steps:
+      - attach_workspace:
+          at: /home/cassandra
+      - run_junit_tests:
+          target: stress-test
+
   j8_dtests_vnode:
     <<: *j8_par_executor
     steps:
@@ -628,6 +724,14 @@
       - log_environment
       - run_unit_tests_repeat
 
+  utests_cdc_repeat:
+    <<: *j8_repeated_utest_executor
+    steps:
+      - attach_workspace:
+          at: /home/cassandra
+      - log_environment
+      - run_utests_cdc_repeat
+
   utests_compression_repeat:
     <<: *j8_repeated_utest_executor
     steps:
@@ -636,14 +740,6 @@
       - log_environment
       - run_utests_compression_repeat
 
-  utests_system_keyspace_directory_repeat:
-    <<: *j8_repeated_utest_executor
-    steps:
-      - attach_workspace:
-          at: /home/cassandra
-      - log_environment
-      - run_utests_system_keyspace_directory_repeat
-
   utests_long_repeat:
     <<: *j8_repeated_utest_executor
     steps:
@@ -652,6 +748,14 @@
       - log_environment
       - run_utests_long_repeat
 
+  utests_stress_repeat:
+    <<: *j8_repeated_utest_executor
+    steps:
+      - attach_workspace:
+          at: /home/cassandra
+      - log_environment
+      - run_utests_stress_repeat
+
   j8_jvm_dtests_repeat:
     <<: *j8_repeated_utest_executor
     steps:
@@ -753,6 +857,36 @@
           stop_on_failure: ${REPEATED_TESTS_STOP_ON_FAILURE}
           count: ${REPEATED_UPGRADE_DTESTS_COUNT}
 
+  j8_dtests_offheap:
+    <<: *j8_par_executor
+    steps:
+      - attach_workspace:
+          at: /home/cassandra
+      - log_environment
+      - clone_dtest
+      - create_venv
+      - create_dtest_containers:
+          file_tag: j8_dtests_offheap
+          run_dtests_extra_args: "--use-vnodes --use-off-heap-memtables --skip-resource-intensive-tests"
+      - run_dtests:
+          file_tag: j8_dtests_offheap
+          pytest_extra_args: '--use-vnodes --num-tokens=16 --use-off-heap-memtables --skip-resource-intensive-tests'
+
+  j8_dtests_offheap_repeat:
+    <<: *j8_repeated_dtest_executor
+    steps:
+      - attach_workspace:
+          at: /home/cassandra
+      - clone_dtest
+      - create_venv
+      - run_repeated_dtest:
+          tests: ${REPEATED_DTESTS}
+          vnodes: "true"
+          upgrade: "false"
+          count: ${REPEATED_DTESTS_COUNT}
+          stop_on_failure: ${REPEATED_TESTS_STOP_ON_FAILURE}
+          extra_dtest_args: "--use-off-heap-memtables --skip-resource-intensive-tests"
+
 commands:
   log_environment:
     steps:
@@ -911,6 +1045,9 @@
       no_output_timeout:
         type: string
         default: 15m
+      classlistprefix:
+        type: string
+        default: unit
     steps:
     - run:
         name: Run Unit Tests (<<parameters.target>>)
@@ -921,7 +1058,7 @@
           if [ -d ~/dtest_jars ]; then
             cp ~/dtest_jars/dtest* /tmp/cassandra/build/
           fi
-          ant <<parameters.target>> -Dno-build-test=true
+          ant <<parameters.target>> -Dtest.classlistfile=/tmp/java_tests_${CIRCLE_NODE_INDEX}_final.txt  -Dtest.classlistprefix=<<parameters.classlistprefix>> -Dno-build-test=true
         no_output_timeout: <<parameters.no_output_timeout>>
     - store_test_results:
         path: /tmp/cassandra/build/test/output/
@@ -1115,18 +1252,18 @@
           count: ${REPEATED_UTESTS_COUNT}
           stop_on_failure: ${REPEATED_TESTS_STOP_ON_FAILURE}
 
-  run_utests_compression_repeat:
+  run_utests_cdc_repeat:
     steps:
       - run_repeated_utests:
-          target: test-compression
+          target: test-cdc
           tests: ${REPEATED_UTESTS}
           count: ${REPEATED_UTESTS_COUNT}
           stop_on_failure: ${REPEATED_TESTS_STOP_ON_FAILURE}
 
-  run_utests_system_keyspace_directory_repeat:
+  run_utests_compression_repeat:
     steps:
       - run_repeated_utests:
-          target: test-system-keyspace-directory
+          target: test-compression
           tests: ${REPEATED_UTESTS}
           count: ${REPEATED_UTESTS_COUNT}
           stop_on_failure: ${REPEATED_TESTS_STOP_ON_FAILURE}
@@ -1139,6 +1276,14 @@
           count: ${REPEATED_UTESTS_LONG_COUNT}
           stop_on_failure: ${REPEATED_TESTS_STOP_ON_FAILURE}
 
+  run_utests_stress_repeat:
+    steps:
+      - run_repeated_utests:
+          target: stress-test-some
+          tests: ${REPEATED_UTESTS_STRESS}
+          count: ${REPEATED_UTESTS_STRESS_COUNT}
+          stop_on_failure: ${REPEATED_TESTS_STOP_ON_FAILURE}
+
   run_jvm_dtests_repeat:
     steps:
       - run_repeated_utests:
@@ -1187,11 +1332,13 @@
             # Put manually specified tests and automatically detected tests together, removing duplicates
             tests=$(echo <<parameters.tests>> | sed -e "s/<nil>//" | sed -e "s/ //" | tr "," "\n" | tr " " "\n" | sort -n | uniq -u)
             echo "Tests to be repeated: ${tests}"
-            
+
             # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
             target=<<parameters.target>>
             testtag=""
-            if [[ $target == "test-compression" ]]; then
+            if [[ $target == "test-cdc" ]]; then
+              testtag="cdc"
+            elif [[ $target == "test-compression" ]]; then
               testtag="compression"
             fi
 
@@ -1213,8 +1360,8 @@
                 if [[ $target == "test" || \
                       $target == "test-cdc" || \
                       $target == "test-compression" || \
-                      $target == "test-system-keyspace-directory" || \
-                      $target == "long-test" ]]; then
+                      $target == "long-test" || \
+                      $target == "stress-test" ]]; then
                   name_arg="-Dtest.name=${class##*.}"
                 else
                   name_arg="-Dtest.name=$class"
@@ -1334,8 +1481,8 @@
                 if [[ $target == "test" || \
                       $target == "test-cdc" || \
                       $target == "test-compression" || \
-                      $target == "test-system-keyspace-directory" || \
-                      $target == "long-test" ]]; then
+                      $target == "long-test" || \
+                      $target == "stress-test" ]]; then
                   name="-Dtest.name=$class_name"
                 else
                   name="-Dtest.name=$class_path"
diff --git a/.circleci/config_template.yml.PAID.patch b/.circleci/config_template.yml.PAID.patch
index 81f501d..262d2ce 100644
--- a/.circleci/config_template.yml.PAID.patch
+++ b/.circleci/config_template.yml.PAID.patch
@@ -1,9 +1,10 @@
---- config-2_1.yml	2023-02-07 19:05:10.000000000 -0500
-+++ config-2_1.yml.MIDRES	2023-02-07 19:17:39.000000000 -0500
-@@ -128,13 +128,15 @@
+--- config-2_1.yml	2023-02-02 21:03:34.000000000 -0500
++++ config-2_1.yml.MIDRES	2023-02-02 21:04:03.000000000 -0500
+@@ -134,14 +134,14 @@
+ j8_par_executor: &j8_par_executor
    executor:
      name: java8-executor
-     #exec_resource_class: xlarge
+-    #exec_resource_class: xlarge
 -  parallelism: 4
 +    exec_resource_class: medium
 +  parallelism: 25
@@ -11,14 +12,14 @@
  j8_small_par_executor: &j8_small_par_executor
    executor:
      name: java8-executor
-     #exec_resource_class: xlarge
+-    #exec_resource_class: xlarge
 -  parallelism: 1
 +    exec_resource_class: large
 +  parallelism: 10
  
  j8_small_executor: &j8_small_executor
    executor:
-@@ -145,34 +147,51 @@
+@@ -152,34 +152,51 @@
  j8_medium_par_executor: &j8_medium_par_executor
    executor:
      name: java8-executor
@@ -71,13 +72,13 @@
    executor:
      name: java8-executor
 -  parallelism: 4
-+    exec_resource_class: medium
++    exec_resource_class: large
 +  parallelism: 25
  
  separate_jobs: &separate_jobs
    jobs:
-@@ -551,7 +570,7 @@
-           target: testclasslist-compression
+@@ -647,7 +664,7 @@
+           target: stress-test
  
    j8_dtests_vnode:
 -    <<: *j8_par_executor
@@ -85,7 +86,7 @@
      steps:
        - attach_workspace:
            at: /home/cassandra
-@@ -565,7 +584,7 @@
+@@ -661,7 +678,7 @@
            pytest_extra_args: '--use-vnodes --num-tokens=32 --skip-resource-intensive-tests'
  
    j8_dtests:
@@ -94,7 +95,7 @@
      steps:
        - attach_workspace:
            at: /home/cassandra
-@@ -579,7 +598,7 @@
+@@ -675,7 +692,7 @@
            pytest_extra_args: '--skip-resource-intensive-tests'
  
    j8_dtests_large_vnode:
@@ -103,7 +104,7 @@
      steps:
        - attach_workspace:
            at: /home/cassandra
-@@ -593,7 +612,7 @@
+@@ -689,7 +706,7 @@
            pytest_extra_args: '--use-vnodes --num-tokens=32 --only-resource-intensive-tests --force-resource-intensive-tests'
  
    j8_dtests_large:
@@ -112,7 +113,7 @@
      steps:
        - attach_workspace:
            at: /home/cassandra
-@@ -607,7 +626,7 @@
+@@ -703,7 +720,7 @@
            pytest_extra_args: '--num-tokens=32 --only-resource-intensive-tests --force-resource-intensive-tests'
  
    j8_upgrade_dtests:
@@ -121,12 +122,12 @@
      steps:
        - attach_workspace:
            at: /home/cassandra
-@@ -1187,7 +1206,7 @@
-             # Put manually specified tests and automatically detected tests together, removing duplicates
-             tests=$(echo <<parameters.tests>> | sed -e "s/<nil>//" | sed -e "s/ //" | tr "," "\n" | tr " " "\n" | sort -n | uniq -u)
-             echo "Tests to be repeated: ${tests}"
--            
-+
-             # Prepare the testtag for the target, used by the test macro in build.xml to group the output files
-             target=<<parameters.target>>
-             testtag=""
+@@ -858,7 +875,7 @@
+           count: ${REPEATED_UPGRADE_DTESTS_COUNT}
+ 
+   j8_dtests_offheap:
+-    <<: *j8_par_executor
++    <<: *j8_large_par_executor
+     steps:
+       - attach_workspace:
+           at: /home/cassandra
diff --git a/.circleci/generate.sh b/.circleci/generate.sh
index ee62a9b..64d443e 100755
--- a/.circleci/generate.sh
+++ b/.circleci/generate.sh
@@ -18,7 +18,7 @@
 #
 
 BASEDIR=`dirname $0`
-BASE_BRANCH=cassandra-3.0
+BASE_BRANCH=cassandra-3.11
 set -e
 
 die ()
@@ -44,6 +44,8 @@
   echo "                   -e REPEATED_UTESTS_COUNT=500"
   echo "                   -e REPEATED_UTESTS_LONG=org.apache.cassandra.db.commitlog.CommitLogStressTest"
   echo "                   -e REPEATED_UTESTS_LONG_COUNT=100"
+  echo "                   -e REPEATED_UTESTS_STRESS=org.apache.cassandra.stress.generate.DistributionGaussianTest"
+  echo "                   -e REPEATED_UTESTS_STRESS_COUNT=500"
   echo "                   -e REPEATED_JVM_DTESTS=org.apache.cassandra.distributed.test.PagingTest"
   echo "                   -e REPEATED_JVM_DTESTS_COUNT=500"
   echo "                   -e REPEATED_JVM_UPGRADE_DTESTS=org.apache.cassandra.distributed.upgrade.GroupByTest"
@@ -108,6 +110,8 @@
        [ "$key" != "REPEATED_UTESTS_COUNT" ] &&
        [ "$key" != "REPEATED_UTESTS_LONG" ] &&
        [ "$key" != "REPEATED_UTESTS_LONG_COUNT" ] &&
+       [ "$key" != "REPEATED_UTESTS_STRESS" ] &&
+       [ "$key" != "REPEATED_UTESTS_STRESS_COUNT" ] &&
        [ "$key" != "REPEATED_JVM_DTESTS" ] &&
        [ "$key" != "REPEATED_JVM_DTESTS_COUNT" ] &&
        [ "$key" != "REPEATED_JVM_UPGRADE_DTESTS" ]  &&
@@ -197,6 +201,7 @@
   echo "Detecting new or modified tests with git diff --diff-filter=AMR ${BASE_BRANCH}...HEAD:"
   add_diff_tests "REPEATED_UTESTS" "test/unit/" "org.apache.cassandra"
   add_diff_tests "REPEATED_UTESTS_LONG" "test/long/" "org.apache.cassandra"
+  add_diff_tests "REPEATED_UTESTS_STRESS" "tools/stress/test/unit/" "org.apache.cassandra.stress"
   add_diff_tests "REPEATED_JVM_DTESTS" "test/distributed/" "org.apache.cassandra.distributed.test"
   add_diff_tests "REPEATED_JVM_UPGRADE_DTESTS" "test/distributed/" "org.apache.cassandra.distributed.upgrade"
 fi
@@ -237,12 +242,16 @@
   if (! (echo "$env_vars" | grep -q "REPEATED_UTESTS=" )); then
     delete_job "$1" "j8_unit_tests_repeat"
     delete_job "$1" "j11_unit_tests_repeat"
+    delete_job "$1" "utests_cdc_repeat"
     delete_job "$1" "utests_compression_repeat"
     delete_job "$1" "utests_system_keyspace_directory_repeat"
   fi
   if (! (echo "$env_vars" | grep -q "REPEATED_UTESTS_LONG=")); then
     delete_job "$1" "utests_long_repeat"
   fi
+  if (! (echo "$env_vars" | grep -q "REPEATED_UTESTS_STRESS=")); then
+    delete_job "$1" "utests_stress_repeat"
+  fi
   if (! (echo "$env_vars" | grep -q "REPEATED_JVM_DTESTS=")); then
     delete_job "$1" "j8_jvm_dtests_repeat"
     delete_job "$1" "j8_jvm_dtests_vnode_repeat"
@@ -256,6 +265,7 @@
   if (! (echo "$env_vars" | grep -q "REPEATED_DTESTS=")); then
     delete_job "$1" "j8_dtests_repeat"
     delete_job "$1" "j8_dtests_vnode_repeat"
+    delete_job "$1" "j8_dtests_offheap_repeat"
     delete_job "$1" "j11_dtests_repeat"
     delete_job "$1" "j11_dtests_vnode_repeat"
   fi
diff --git a/.circleci/readme.md b/.circleci/readme.md
index e65be51..7391ed1 100644
--- a/.circleci/readme.md
+++ b/.circleci/readme.md
@@ -71,12 +71,13 @@
 ## Running tests in a loop
 Running the `generate.sh` script with use `git diff` to find the new or modified tests.
 The script will then create jobs to run each of these new or modified tests for a certain
-number of times, to verify that they are stable. You can use environment variables to 
+number of times, to verify that they are stable. You can use environment variables to
 specify the number of iterations of each type of test:
 ```
 generate.sh -p \
   -e REPEATED_UTESTS_COUNT=500 \
   -e REPEATED_UTESTS_LONG_COUNT=100 \
+  -e REPEATED_UTESTS_STRESS_COUNT=500 \
   -e REPEATED_SIMULATOR_DTESTS_COUNT=500 \
   -e REPEATED_JVM_DTESTS_COUNT=500 \
   -e REPEATED_JVM_UPGRADE_DTESTS_COUNT=500 \
@@ -95,6 +96,7 @@
 generate.sh -p \
   -e REPEATED_UTESTS=org.apache.cassandra.cql3.ViewTest,org.apache.cassandra.db.CellTest \
   -e REPEATED_UTESTS_LONG=org.apache.cassandra.io.sstable.CQLSSTableWriterLongTest#testWideRow \
+  -e REPEATED_UTESTS_STRESS=org.apache.cassandra.stress.generate.DistributionGaussianTest \
   -e REPEATED_DTESTS=cql_test.py,consistency_test.py::TestAvailability::test_simple_strategy \
   -e REPEATED_LARGE_DTESTS=replace_address_test.py::TestReplaceAddress::test_replace_stopped_node \
   -e REPEATED_JVM_DTESTS=org.apache.cassandra.distributed.test.PagingTest#testPaging \
@@ -118,6 +120,8 @@
   -e REPEATED_UTESTS_COUNT=500 \
   -e REPEATED_UTESTS_LONG=org.apache.cassandra.io.sstable.CQLSSTableWriterLongTest#testWideRow \
   -e REPEATED_UTESTS_LONG_COUNT=100 \
+  -e REPEATED_UTESTS_STRESS=org.apache.cassandra.stress.generate.DistributionGaussianTest \
+  -e REPEATED_UTESTS_STRESS_COUNT=500 \
   -e REPEATED_DTESTS=cql_test.py,consistency_test.py::TestAvailability::test_simple_strategy \
   -e REPEATED_DTESTS_COUNT=500 \
   -e REPEATED_LARGE_DTESTS=replace_address_test.py,materialized_views_test.py \
diff --git a/.gitignore b/.gitignore
index 310be77..71dadaa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@
 data/
 conf/hotspot_compiler
 doc/cql3/CQL.html
+doc/build/
 pylib/src/
 lib/
 
@@ -64,20 +65,14 @@
 .DS_Store
 Thumbs.db
 
-# JSR223
-lib/jsr223/clojure/*.jar
-lib/jsr223/groovy/*.jar
-lib/jsr223/jaskell/*.jar
-lib/jsr223/jruby/*.jar
-lib/jsr223/jruby/jni
-lib/jsr223/jruby/ruby
-lib/jsr223/jython/*.jar
-lib/jsr223/jython/cachedir
-lib/jsr223/scala/*.jar
-
 /.ant-targets-build.xml
 .ant_targets
 
+# Generated files from the documentation
+doc/modules/cassandra/pages/configuration/cass_yaml_file.adoc
+doc/modules/cassandra/pages/tools/nodetool/
+doc/modules/cassandra/examples/TEXT/NODETOOL/
+
 # Python virtual environment
 venv/
 
diff --git a/.jenkins/Jenkinsfile b/.jenkins/Jenkinsfile
index 4ea5de3..75b9a66 100644
--- a/.jenkins/Jenkinsfile
+++ b/.jenkins/Jenkinsfile
@@ -26,30 +26,56 @@
 pipeline {
   agent { label 'cassandra' }
   stages {
-      stage('Init') {
+    stage('Init') {
         steps {
           cleanWs()
           script {
               currentBuild.result='SUCCESS'
           }
         }
-      }
-      stage('Build') {
-        steps {
-          script {
-            def attempt = 1
-            retry(2) {
-              if (attempt > 1) {
-                sleep(60 * attempt)
-              }
-              attempt = attempt + 1
-              build job: "${env.JOB_NAME}-artifacts"
+    }
+    stage('Build') {
+      steps {
+        script {
+          def attempt = 1
+          retry(2) {
+            if (attempt > 1) {
+              sleep(60 * attempt)
             }
+            attempt = attempt + 1
+              build job: "${env.JOB_NAME}-artifacts"
           }
         }
       }
-      stage('Test') {
+    }
+    stage('Test') {
         parallel {
+          stage('stress') {
+            steps {
+              script {
+                def attempt = 1
+                while (attempt <=2) {
+                  if (attempt > 1) {
+                    sleep(60 * attempt)
+                  }
+                  attempt = attempt + 1
+                  stress = build job: "${env.JOB_NAME}-stress-test", propagate: false
+                  if (stress.result != 'FAILURE') break
+                }
+                if (stress.result != 'SUCCESS') unstable('stress test failures')
+                if (stress.result == 'FAILURE') currentBuild.result='FAILURE'
+              }
+            }
+            post {
+              always {
+                  warnError('missing test xml files') {
+                      script {
+                          copyTestResults('stress-test', stress.getNumber())
+                      }
+                  }
+              }
+            }
+          }
           stage('units') {
             steps {
               script {
@@ -128,6 +154,32 @@
               }
             }
           }
+          stage('cdc') {
+            steps {
+              script {
+                def attempt = 1
+                while (attempt <=2) {
+                  if (attempt > 1) {
+                    sleep(60 * attempt)
+                  }
+                  attempt = attempt + 1
+                  cdc = build job: "${env.JOB_NAME}-test-cdc", propagate: false
+                  if (cdc.result != 'FAILURE') break
+                }
+                if (cdc.result != 'SUCCESS') unstable('cdc failures')
+                if (cdc.result == 'FAILURE') currentBuild.result='FAILURE'
+              }
+            }
+            post {
+              always {
+                  warnError('missing test xml files') {
+                      script {
+                          copyTestResults('test-cdc', cdc.getNumber())
+                      }
+                  }
+              }
+            }
+          }
           stage('compression') {
             steps {
               script {
@@ -177,12 +229,12 @@
                           copyTestResults('cqlsh-tests', cqlsh.getNumber())
                       }
                   }
-              }
+                }
             }
           }
         }
-      }
-      stage('Distributed Test') {
+    }
+    stage('Distributed Test') {
         parallel {
           stage('jvm-dtest') {
             steps {
@@ -314,6 +366,32 @@
               }
             }
           }
+          stage('dtest-offheap') {
+            steps {
+              script {
+                def attempt = 1
+                while (attempt <=2) {
+                  if (attempt > 1) {
+                    sleep(60 * attempt)
+                  }
+                  attempt = attempt + 1
+                  dtest_offheap = build job: "${env.JOB_NAME}-dtest-offheap", propagate: false
+                  if (dtest_offheap.result != 'FAILURE') break
+                }
+                if (dtest_offheap.result != 'SUCCESS') unstable('dtest-offheap failures')
+                if (dtest_offheap.result == 'FAILURE') currentBuild.result='FAILURE'
+              }
+            }
+            post {
+              always {
+                warnError('missing test xml files') {
+                    script {
+                        copyTestResults('dtest-offheap', dtest_offheap.getNumber())
+                    }
+                }
+              }
+            }
+          }
           stage('dtest-large-novnode') {
             steps {
               script {
diff --git a/CHANGES.txt b/CHANGES.txt
index a45c3f9..0aed2d9 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,10 +1,17 @@
-3.0.30
+3.11.16
+ * Remove unnecessary String.format invocation in QueryProcessor when getting a prepared statement from cache (CASSANDRA-17202)
+Merged from 3.0:
  * Remove dh_python use in Debian packaging (CASSANDRA-18558)
  * Pass down all contact points to driver for cassandra-stress (CASSANDRA-18025)
  * Validate the existence of a datacenter in nodetool rebuild (CASSANDRA-14319)
-
-3.0.29
  * Suppress CVE-2023-2251 (CASSANDRA-18497)
+
+3.11.15
+ * Fix the capital P usage in the CQL parser (CASSANDRA-17919)
+ * Fix sstable_count metric missing from tablestats json/yaml output (CASSANDRA-18448)
+ * Suppress CVE-2022-45688 (CASSANDRA-18389)
+ * Fix Splitter sometimes creating more splits than requested (CASSANDRA-18013)
+Merged from 3.0:
  * Do not remove SSTables when cause of FSReadError is OutOfMemoryError while using best_effort disk failure policy (CASSANDRA-18336)
  * Do not remove truncated_at entry in system.local while dropping an index (CASSANDRA-18105)
  * Save host id to system.local and flush immediately after startup (CASSANDRA-18153)
@@ -25,16 +32,22 @@
  * Fix incorrect resource name in LIST PERMISSION output (CASSANDRA-17848)
  * Suppress CVE-2022-41854 and similar (CASSANDRA-18083)
  * Fix running Ant rat targets without git (CASSANDRA-17974)
- * Fix intermittent failure in nodetool toppartitions (CASSANDRA-17254)
 
 
-3.0.28
+3.11.14
+ * Suppress CVE-2022-42003 and CVE-2022-42004 (CASSANDRA-17966)
+ * Make LongBufferPoolTest insensitive to timing (CASSANDRA-16681)
+ * Suppress CVE-2022-25857 and other snakeyaml CVEs (CASSANDRA-17907)
+ * Fix potential IndexOutOfBoundsException in PagingState in mixed mode clusters (CASSANDRA-17840)
+ * Document usage of closed token intervals in manual compaction (CASSANDRA-17575)
+ * Creating of a keyspace on insufficient number of replicas should filter out gosspping-only members (CASSANDRA-17759)
+ * Only use statically defined subcolumns when determining column definition for supercolumn cell (CASSANDRA-14113)
+Merged from 3.0:
  * Harden JMX by resolving beanshooter issues (CASSANDRA-17921)
  * Suppress CVE-2019-2684 (CASSANDRA-17965)
  * Fix auto-completing "WITH" when creating a materialized view (CASSANDRA-17879)
  * Fix scrubber falling into infinite loop when the last partition is broken (CASSANDRA-17862)
  * Improve libjemalloc resolution in bin/cassandra (CASSANDRA-15767)
- * Fix missing state resetting on CompressedRandomAccessReader read errors (CASSANDRA-17314)
  * Fix restarting of services on gossipping-only member (CASSANDRA-17752)
  * Fix writetime and ttl functions forbidden for collections instead of multicell columns (CASSANDRA-17628)
  * Supress CVE-2020-7238 (CASSANDRA-17697)
@@ -43,7 +56,15 @@
  * fsync TOC and digest files (CASSANDRA-10709)
 
 
-3.0.27
+3.11.13
+ * Upgrade jackson-databind to 2.13.2.2 (CASSANDRA-17556)
+ * Upgrade slf4j to 1.7.25 (CASSANDRA-17474)
+ * Upgrade jackson to 2.13.2 (CASSANDRA-17492)
+ * emit warning on keyspace creation when replication factor is bigger than the number of nodes (CASSANDRA-16747)
+ * Fix snapshot true size calculation (CASSANDRA-17267)
+ * Validate existence of DCs when repairing (CASSANDRA-17407)
+ * dropping of a materialized view creates a snapshot with dropped- prefix (CASSANDRA-17415)
+Merged from 3.0:
  * Fix URISyntaxException in nodetool with updated Java (CASSANDRA-17581)
  * Schema mutations may not be completed on drain (CASSANDRA-17524)
  * Fix data corruption in AbstractCompositeType due to static boolean byte buffers (CASSANDRA-14752)
@@ -56,12 +77,24 @@
  * Suppress inapplicable CVEs (CASSANDRA-17368)
  * Fix flaky test - test_cqlsh_completion.TestCqlshCompletion (CASSANDRA-17338)
  * Fixed TestCqlshOutput failing tests (CASSANDRA-17386)
- * LeveledCompactionStrategy disk space check improvements (CASSANDRA-17272)
  * Lazy transaction log replica creation allows incorrect replica content divergence during anticompaction (CASSANDRA-17273)
+ * LeveledCompactionStrategy disk space check improvements (CASSANDRA-17272)
 
 
-3.0.26
+3.11.12
  * Extend operator control over the UDF threading model for CVE-2021-44521 (CASSANDRA-17352)
+ * Upgrade snakeyaml to 1.26 in 3.11 (CASSANDRA=17028)
+ * Add key validation to ssstablescrub (CASSANDRA-16969)
+ * Update Jackson from 2.9.10 to 2.12.5 (CASSANDRA-16851)
+ * Include SASI components to snapshots (CASSANDRA-15134)
+ * Make assassinate more resilient to missing tokens (CASSANDRA-16847)
+ * Exclude Jackson 1.x transitive dependency of hadoop* provided dependencies (CASSANDRA-16854)
+ * Validate SASI tokenizer options before adding index to schema (CASSANDRA-15135)
+ * Fixup scrub output when no data post-scrub and clear up old use of row, which really means partition (CASSANDRA-16835)
+ * Fix ant-junit dependency issue (CASSANDRA-16827)
+ * Reduce thread contention in CommitLogSegment and HintsBuffer (CASSANDRA-16072)
+ * Avoid sending CDC column if not enabled (CASSANDRA-16770)
+Merged from 3.0:
  * Fix conversion from megabits to bytes in streaming rate limiter (CASSANDRA-17243)
  * Upgrade logback to 1.2.9 (CASSANDRA-17204)
  * Avoid race in AbstractReplicationStrategy endpoint caching (CASSANDRA-16673)
@@ -94,7 +127,20 @@
  * Add python2 location to RPMs (CASSANDRA-16822)
 
 
-3.0.25:
+3.11.11
+ * Make cqlsh use the same set of reserved keywords than the server uses (CASSANDRA-15663)
+ * Optimize bytes skipping when reading SSTable files (CASSANDRA-14415)
+ * Enable tombstone compactions when unchecked_tombstone_compaction is set in TWCS (CASSANDRA-14496)
+ * Read only the required SSTables for single partition queries (CASSANDRA-16737)
+ * Fix LeveledCompactionStrategy compacts last level throw an ArrayIndexOutOfBoundsException (CASSANDRA-15669)
+ * Maps $CASSANDRA_LOG_DIR to cassandra.logdir java property when executing nodetool (CASSANDRA-16199)
+ * Nodetool garbagecollect should retain SSTableLevel for LCS (CASSANDRA-16634)
+ * Ignore stale acks received in the shadow round (CASSANDRA-16588)
+ * Add autocomplete and error messages for provide_overlapping_tombstones (CASSANDRA-16350)
+ * Add StorageServiceMBean.getKeyspaceReplicationInfo(keyspaceName) (CASSANDRA-16447)
+ * Make sure sstables with moved starts are removed correctly in LeveledGenerations (CASSANDRA-16552)
+ * Upgrade jackson-databind to 2.9.10.8 (CASSANDRA-16462)
+Merged from 3.0:
  * Binary releases no longer bundle the apidocs (javadoc) (CASSANDRA-16557)
  * Migrate dependency handling from maven-ant-tasks to resolver-ant-tasks, removing lib/ directory from version control (CASSANDRA-16557)
  * Don't allow seeds to replace without using unsafe (CASSANDRA-14463)
@@ -105,7 +151,6 @@
  * Ensure java executable is on the path (CASSANDRA-14325)
  * Make speculative retry parameter case-insensitive for backward compatibility with 2.1 (CASSANDRA-16467)
  * Push digest mismatch exceptions to trace (CASSANDRA-14900)
- * Support long names in nodetool output (CASSANDRA-14162)
  * Handle correctly the exceptions thrown by custom QueryHandler constructors (CASSANDRA-16703)
  * Adding columns via ALTER TABLE can generate corrupt sstables (CASSANDRA-16735)
  * Add flag to disable ALTER...DROP COMPACT STORAGE statements (CASSANDRA-16733)
@@ -114,7 +159,7 @@
  * Ensure that existing empty rows are properly returned (CASSANDRA-16671)
  * Invalidate prepared statements on DROP COMPACT (CASSANDRA-16712)
  * Failure to execute queries should emit a KPI other than read timeout/unavailable so it can be alerted/tracked (CASSANDRA-16581)
- * Don't wait on schema versions from replacement target when replacing (CASSANDRA-16692)
+ * Don't wait on schema versions from replacement target when replacing a node (CASSANDRA-16692)
  * StandaloneVerifier does not fail when unable to verify SSTables, it only fails if Corruption is thrown (CASSANDRA-16683)
  * Fix bloom filter false ratio calculation by including true negatives (CASSANDRA-15834)
  * Prevent loss of commit log data when moving sstables between nodes (CASSANDRA-16619)
@@ -131,13 +176,18 @@
  * Fix centos packaging for arm64, >=4.0 rpm's now require python3 (CASSANDRA-16477)
  * Make TokenMetadata's ring version increments atomic (CASSANDRA-16286)
 
-
-3.0.24:
- * Prevent unbounded number of pending flushing tasks; Add PendingFlushTasks metric (CASSANDRA-16261)
+3.11.10
+ * Fix digest computation for queries with fetched but non queried columns (CASSANDRA-15962)
+ * Reduce amount of allocations during batch statement execution (CASSANDRA-16201)
+ * Update jflex-1.6.0.jar to match upstream (CASSANDRA-16393)
+ * Fix DecimalDeserializer#toString OOM (CASSANDRA-14925)
+ * Rate limit validation compactions using compaction_throughput_mb_per_sec (CASSANDRA-16161)
+ * SASI's `max_compaction_flush_memory_in_mb` settings over 100GB revert to default of 1GB (CASSANDRA-16071)
+Merged from 3.0:
+ * Prevent unbounded number of pending flushing tasks (CASSANDRA-16261)
  * Improve empty hint file handling during startup (CASSANDRA-16162)
  * Allow empty string in collections with COPY FROM in cqlsh (CASSANDRA-16372)
  * Fix skipping on pre-3.0 created compact storage sstables due to missing primary key liveness (CASSANDRA-16226)
- * Fix DecimalDeserializer#toString OOM (CASSANDRA-14925)
  * Extend the exclusion of replica filtering protection to other indices instead of just SASI (CASSANDRA-16311)
  * Synchronize transaction logs for JBOD (CASSANDRA-16225)
  * Fix the counting of cells per partition (CASSANDRA-16259)
@@ -146,46 +196,59 @@
  * Improved check of num_tokens against the length of initial_token (CASSANDRA-14477)
  * Fix a race condition on ColumnFamilyStore and TableMetrics (CASSANDRA-16228)
  * Remove the SEPExecutor blocking behavior (CASSANDRA-16186)
- * Wait for schema agreement when bootstrapping (CASSANDRA-15158)
  * Fix invalid cell value skipping when reading from disk (CASSANDRA-16223)
  * Prevent invoking enable/disable gossip when not in NORMAL (CASSANDRA-16146)
+ * Wait for schema agreement when bootstrapping (CASSANDRA-15158)
 Merged from 2.2:
- * Remove OpenJDK log warning (CASSANDRA-15563)
  * Fix the histogram merge of the table metrics (CASSANDRA-16259)
 
-3.0.23:
+3.11.9
+ * Synchronize Keyspace instance store/clear (CASSANDRA-16210)
+ * Fix ColumnFilter to avoid querying cells of unselected complex columns (CASSANDRA-15977)
+ * Fix memory leak in CompressedChunkReader (CASSANDRA-15880)
+ * Don't attempt value skipping with mixed version cluster (CASSANDRA-15833)
+ * Avoid failing compactions with very large partitions (CASSANDRA-15164)
+ * Make sure LCS handles duplicate sstable added/removed notifications correctly (CASSANDRA-14103)
+Merged from 3.0:
  * Fix OOM when terminating repair session (CASSANDRA-15902)
  * Avoid marking shutting down nodes as up after receiving gossip shutdown message (CASSANDRA-16094)
  * Check SSTables for latest version before dropping compact storage (CASSANDRA-16063)
  * Handle unexpected columns due to schema races (CASSANDRA-15899)
- * Avoid failing compactions with very large partitions (CASSANDRA-15164)
- * Use IF NOT EXISTS for index and UDT create statements in snapshot schema files (CASSANDRA-13935)
  * Add flag to ignore unreplicated keyspaces during repair (CASSANDRA-15160)
 Merged from 2.2:
  * Package tools/bin scripts as executable (CASSANDRA-16151)
  * Fixed a NullPointerException when calling nodetool enablethrift (CASSANDRA-16127)
 
-3.0.22:
+3.11.8
+ * Correctly interpret SASI's `max_compaction_flush_memory_in_mb` setting in megabytes not bytes (CASSANDRA-16071)
+ * Fix short read protection for GROUP BY queries (CASSANDRA-15459)
+ * Frozen RawTuple is not annotated with frozen in the toString method (CASSANDRA-15857)
+Merged from 3.0:
+ * Use IF NOT EXISTS for index and UDT create statements in snapshot schema files (CASSANDRA-13935)
  * Fix gossip shutdown order (CASSANDRA-15816)
  * Remove broken 'defrag-on-read' optimization (CASSANDRA-15432)
  * Check for endpoint collision with hibernating nodes (CASSANDRA-14599)
  * Operational improvements and hardening for replica filtering protection (CASSANDRA-15907)
  * stop_paranoid disk failure policy is ignored on CorruptSSTableException after node is up (CASSANDRA-15191)
- * 3.x fails to start if commit log has range tombstones from a column which is also deleted (CASSANDRA-15970)
  * Forbid altering UDTs used in partition keys (CASSANDRA-15933)
  * Fix empty/null json string representation (CASSANDRA-15896)
+ * 3.x fails to start if commit log has range tombstones from a column which is also deleted (CASSANDRA-15970)
  * Handle difference in timestamp precision between java8 and java11 in LogFIle.java (CASSANDRA-16050)
 Merged from 2.2:
  * Fix CQL parsing of collections when the column type is reversed (CASSANDRA-15814)
 Merged from 2.1:
  * Only allow strings to be passed to JMX authentication (CASSANDRA-16077)
 
-3.0.21
+3.11.7
+ * Fix cqlsh output when fetching all rows in batch mode (CASSANDRA-15905)
+ * Upgrade Jackson to 2.9.10 (CASSANDRA-15867)
+ * Fix CQL formatting of read command restrictions for slow query log (CASSANDRA-15503)
+ * Allow sstableloader to use SSL on the native port (CASSANDRA-14904)
+Merged from 3.0:
  * Backport CASSANDRA-12189: escape string literals (CASSANDRA-15948)
  * Avoid hinted handoff per-host throttle being arounded to 0 in large cluster (CASSANDRA-15859)
  * Avoid emitting empty range tombstones from RangeTombstoneList (CASSANDRA-15924)
  * Avoid thread starvation, and improve compare-and-swap performance, in the slab allocators (CASSANDRA-15922)
- * Fix broken KEYS 2i queries after DROP COMPACT STORAGE (CASSANDRA-15906)
  * Add token to tombstone warning and error messages (CASSANDRA-15890)
  * Fixed range read concurrency factor computation and capped as 10 times tpc cores (CASSANDRA-15752)
  * Catch exception on bootstrap resume and init native transport (CASSANDRA-15863)
@@ -199,6 +262,7 @@
  * Fix Debian init start/stop (CASSANDRA-15770)
  * Fix infinite loop on index query paging in tables with clustering (CASSANDRA-14242)
  * Fix chunk index overflow due to large sstable with small chunk length (CASSANDRA-15595)
+ * Allow selecting static column only when querying static index (CASSANDRA-14242)
  * cqlsh return non-zero status when STDIN CQL fails (CASSANDRA-15623)
  * Don't skip sstables in slice queries based only on local min/max/deletion timestamp (CASSANDRA-15690)
  * Memtable memory allocations may deadlock (CASSANDRA-15367)
@@ -217,7 +281,13 @@
  * Allow EXTRA_CLASSPATH to work on tar/source installations (CASSANDRA-15567)
 
 
-3.0.20
+3.11.6
+ * Fix bad UDT sstable metadata serialization headers written by C* 3.0 on upgrade and in sstablescrub (CASSANDRA-15035)
+ * Fix nodetool compactionstats showing extra pending task for TWCS - patch implemented (CASSANDRA-15409)
+ * Fix SELECT JSON formatting for the "duration" type (CASSANDRA-15075)
+ * Fix LegacyLayout to have same behavior as 2.x when handling unknown column names (CASSANDRA-15081)
+ * Update nodetool help stop output (CASSANDRA-15401)
+Merged from 3.0:
  * Run in-jvm upgrade dtests in circleci (CASSANDRA-15506)
  * Include updates to static column in mutation size calculations (CASSANDRA-15293)
  * Fix point-in-time recoevery ignoring timestamp of updates to static columns (CASSANDRA-15292)
@@ -240,13 +310,20 @@
  * Fix SELECT JSON output for empty blobs (CASSANDRA-15435)
  * In-JVM DTest: Set correct internode message version for upgrade test (CASSANDRA-15371)
  * In-JVM DTest: Support NodeTool in dtest (CASSANDRA-15429)
+ * Fix NativeLibrary.tryOpenDirectory callers for Windows (CASSANDRA-15426)
 
 
-3.0.19
+3.11.5
+ * Fix SASI non-literal string comparisons (range operators) (CASSANDRA-15169)
+ * Make sure user defined compaction transactions are always closed (CASSANDRA-15123)
+ * Fix cassandra-env.sh to use $CASSANDRA_CONF to find cassandra-jaas.config (CASSANDRA-14305)
+ * Fixed nodetool cfstats printing index name twice (CASSANDRA-14903)
+ * Add flag to disable SASI indexes, and warnings on creation (CASSANDRA-14866)
+Merged from 3.0:
  * Add ability to cap max negotiable protocol version (CASSANDRA-15193)
  * Gossip tokens on startup if available (CASSANDRA-15335)
  * Fix resource leak in CompressedSequentialWriter (CASSANDRA-15340)
- * Fix merge which reverted CASSANDRA-14993 (CASSANDRA-15289)
+ * Fix bad merge that reverted CASSANDRA-14993 (CASSANDRA-15289)
  * Fix LegacyLayout RangeTombstoneList IndexOutOfBoundsException when upgrading and RangeTombstone bounds are asymmetric (CASSANDRA-15172)
  * Fix NPE when using allocate_tokens_for_keyspace on new DC/rack (CASSANDRA-14952)
  * Filter sstables earlier when running cleanup (CASSANDRA-15100)
@@ -267,14 +344,13 @@
  * Fix assorted gossip races and add related runtime checks (CASSANDRA-15059)
  * Fix mixed mode partition range scans with limit (CASSANDRA-15072)
  * cassandra-stress works with frozen collections: list and set (CASSANDRA-14907)
- * For nodetool listsnapshots output, put spaces between columns, and increase snapshot padding (CASSANDRA-14876)
  * Fix handling FS errors on writing and reading flat files - LogTransaction and hints (CASSANDRA-15053)
  * Avoid double closing the iterator to avoid overcounting the number of requests (CASSANDRA-15058)
  * Improve `nodetool status -r` speed (CASSANDRA-14847)
  * Improve merkle tree size and time on heap (CASSANDRA-14096)
- * Add missing commands to nodetool-completion (CASSANDRA-14916)
+ * Add missing commands to nodetool_completion (CASSANDRA-14916)
  * Anti-compaction temporarily corrupts sstable state for readers (CASSANDRA-15004)
- Merged from 2.2:
+Merged from 2.2:
  * Catch non-IOException in FileUtils.close to make sure that all resources are closed (CASSANDRA-15225)
  * Handle exceptions during authentication/authorization (CASSANDRA-15041)
  * Support cross version messaging in in-jvm upgrade dtests (CASSANDRA-15078)
@@ -286,7 +362,10 @@
  * Add support for network topology and query tracing for inJVM dtest (CASSANDRA-15319)
 
 
-3.0.18
+3.11.4
+ * Make stop-server.bat wait for Cassandra to terminate (CASSANDRA-14829)
+ * Correct sstable sorting for garbagecollect and levelled compaction (CASSANDRA-14870)
+Merged from 3.0:
  * Severe concurrency issues in STCS,DTCS,TWCS,TMD.Topology,TypeParser
  * Add a script to make running the cqlsh tests in cassandra repo easier (CASSANDRA-14951)
  * If SizeEstimatesRecorder misses a 'onDropTable' notification, the size_estimates table will never be cleared for that table. (CASSANDRA-14905)
@@ -294,7 +373,6 @@
  * Streaming needs to synchronise access to LifecycleTransaction (CASSANDRA-14554)
  * Fix cassandra-stress write hang with default options (CASSANDRA-14616)
  * Differentiate between slices and RTs when decoding legacy bounds (CASSANDRA-14919)
- * CommitLogReplayer.handleReplayError should print stack traces (CASSANDRA-14589)
  * Netty epoll IOExceptions caused by unclean client disconnects being logged at INFO (CASSANDRA-14909)
  * Unfiltered.isEmpty conflicts with Row extends AbstractCollection.isEmpty (CASSANDRA-14588)
  * RangeTombstoneList doesn't properly clean up mergeable or superseded rts in some cases (CASSANDRA-14894)
@@ -319,7 +397,7 @@
  * Fix static column order for SELECT * wildcard queries (CASSANDRA-14638)
  * sstableloader should use discovered broadcast address to connect intra-cluster (CASSANDRA-14522)
  * Fix reading columns with non-UTF names from schema (CASSANDRA-14468)
- Merged from 2.2:
+Merged from 2.2:
  * CircleCI docker image should bake in more dependencies (CASSANDRA-14985)
  * Don't enable client transports when bootstrap is pending (CASSANDRA-14525)
  * MigrationManager attempts to pull schema from different major version nodes (CASSANDRA-14928)
@@ -330,10 +408,28 @@
  * Update release checksum algorithms to SHA-256, SHA-512 (CASSANDRA-14970)
 
 
-3.0.17
+3.11.3
+ * Validate supported column type with SASI analyzer (CASSANDRA-13669)
+ * Remove BTree.Builder Recycler to reduce memory usage (CASSANDRA-13929)
+ * Reduce nodetool GC thread count (CASSANDRA-14475)
+ * Fix New SASI view creation during Index Redistribution (CASSANDRA-14055)
+ * Remove string formatting lines from BufferPool hot path (CASSANDRA-14416)
+ * Update metrics to 3.1.5 (CASSANDRA-12924)
+ * Detect OpenJDK jvm type and architecture (CASSANDRA-12793)
+ * Don't use guava collections in the non-system keyspace jmx attributes (CASSANDRA-12271)
+ * Allow existing nodes to use all peers in shadow round (CASSANDRA-13851)
+ * Fix cqlsh to read connection.ssl cqlshrc option again (CASSANDRA-14299)
+ * Downgrade log level to trace for CommitLogSegmentManager (CASSANDRA-14370)
+ * CQL fromJson(null) throws NullPointerException (CASSANDRA-13891)
+ * Serialize empty buffer as empty string for json output format (CASSANDRA-14245)
+ * Allow logging implementation to be interchanged for embedded testing (CASSANDRA-13396)
+ * SASI tokenizer for simple delimiter based entries (CASSANDRA-14247)
+ * Fix Loss of digits when doing CAST from varint/bigint to decimal (CASSANDRA-14170)
+ * RateBasedBackPressure unnecessarily invokes a lock on the Guava RateLimiter (CASSANDRA-14163)
+ * Fix wildcard GROUP BY queries (CASSANDRA-14209)
+Merged from 3.0:
  * Fix corrupted static collection deletions in 3.0 -> 2.{1,2} messages (CASSANDRA-14568)
  * Fix potential IndexOutOfBoundsException with counters (CASSANDRA-14167)
- * Restore resumable hints delivery, backport CASSANDRA-11960 (CASSANDRA-14419)
  * Always close RT markers returned by ReadCommand#executeLocally() (CASSANDRA-14515)
  * Reverse order queries with range tombstones can cause data loss (CASSANDRA-14513)
  * Fix regression of lagging commitlog flush log message (CASSANDRA-14451)
@@ -368,20 +464,41 @@
  * Fix compaction failure caused by reading un-flushed data (CASSANDRA-12743)
  * Use Bounds instead of Range for sstables in anticompaction (CASSANDRA-14411)
  * Fix JSON queries with IN restrictions and ORDER BY clause (CASSANDRA-14286)
- * CQL fromJson(null) throws NullPointerException (CASSANDRA-13891)
  * Backport circleci yaml (CASSANDRA-14240)
 Merged from 2.1:
  * Check checksum before decompressing data (CASSANDRA-14284)
  * CVE-2017-5929 Security vulnerability in Logback warning in NEWS.txt (CASSANDRA-14183)
 
 
-3.0.16
- * Fix unit test failures in ViewComplexTest (CASSANDRA-14219)
- * Add MinGW uname check to start scripts (CASSANDRA-12940)
- * Protect against overflow of local expiration time (CASSANDRA-14092)
+3.11.2
+ * Fix ReadCommandTest (CASSANDRA-14234)
+ * Remove trailing period from latency reports at keyspace level (CASSANDRA-14233)
+ * Backport CASSANDRA-13080: Use new token allocation for non bootstrap case as well (CASSANDRA-14212)
+ * Remove dependencies on JVM internal classes from JMXServerUtils (CASSANDRA-14173)
+ * Add DEFAULT, UNSET, MBEAN and MBEANS to `ReservedKeywords` (CASSANDRA-14205)
+ * Add Unittest for schema migration fix (CASSANDRA-14140)
+ * Print correct snitch info from nodetool describecluster (CASSANDRA-13528)
+ * Close socket on error during connect on OutboundTcpConnection (CASSANDRA-9630)
+ * Enable CDC unittest (CASSANDRA-14141)
+ * Acquire read lock before accessing CompactionStrategyManager fields (CASSANDRA-14139)
+ * Split CommitLogStressTest to avoid timeout (CASSANDRA-14143)
+ * Avoid invalidating disk boundaries unnecessarily (CASSANDRA-14083)
+ * Avoid exposing compaction strategy index externally (CASSANDRA-14082)
+ * Prevent continuous schema exchange between 3.0 and 3.11 nodes (CASSANDRA-14109)
+ * Fix imbalanced disks when replacing node with same address with JBOD (CASSANDRA-14084)
+ * Reload compaction strategies when disk boundaries are invalidated (CASSANDRA-13948)
+ * Remove OpenJDK log warning (CASSANDRA-13916)
+ * Prevent compaction strategies from looping indefinitely (CASSANDRA-14079)
+ * Cache disk boundaries (CASSANDRA-13215)
+ * Add asm jar to build.xml for maven builds (CASSANDRA-11193)
+ * Round buffer size to powers of 2 for the chunk cache (CASSANDRA-13897)
+ * Update jackson JSON jars (CASSANDRA-13949)
+ * Avoid locks when checking LCS fanout and if we should defrag (CASSANDRA-13930)
+ * Correctly count range tombstones in traces and tombstone thresholds (CASSANDRA-8527)
+Merged from 3.0:
+ * Add MinGW uname check to start scripts (CASSANDRA-12840)
  * Use the correct digest file and reload sstable metadata in nodetool verify (CASSANDRA-14217)
  * Handle failure when mutating repaired status in Verifier (CASSANDRA-13933)
- * Close socket on error during connect on OutboundTcpConnection (CASSANDRA-9630)
  * Set encoding for javadoc generation (CASSANDRA-14154)
  * Fix index target computation for dense composite tables with dropped compact storage (CASSANDRA-14104)
  * Improve commit log chain marker updating (CASSANDRA-14108)
@@ -390,7 +507,7 @@
  * Accept role names containing forward-slash (CASSANDRA-14088)
  * Optimize CRC check chance probability calculations (CASSANDRA-14094)
  * Fix cleanup on keyspace with no replicas (CASSANDRA-13526)
- * Fix updating base table rows with TTL not removing materialized view entries (CASSANDRA-14071)
+ * Fix updating base table rows with TTL not removing view entries (CASSANDRA-14071)
  * Reduce garbage created by DynamicSnitch (CASSANDRA-14091)
  * More frequent commitlog chained markers (CASSANDRA-13987)
  * Fix serialized size of DataLimits (CASSANDRA-14057)
@@ -407,12 +524,27 @@
  * Fix the inspectJvmOptions startup check (CASSANDRA-14112)
  * Fix race that prevents submitting compaction for a table when executor is full (CASSANDRA-13801)
  * Rely on the JVM to handle OutOfMemoryErrors (CASSANDRA-13006)
+ * Grab refs during scrub/index redistribution/cleanup (CASSANDRA-13873)
 Merged from 2.1:
- * More PEP8 compliance for cqlsh (CASSANDRA-14021)
+ * Protect against overflow of local expiration time (CASSANDRA-14092)
  * RPM package spec: fix permissions for installed jars and config files (CASSANDRA-14181)
+ * More PEP8 compiance for cqlsh (CASSANDRA-14021)
 
 
-3.0.15
+3.11.1
+ * Fix the computation of cdc_total_space_in_mb for exabyte filesystems (CASSANDRA-13808)
+ * AbstractTokenTreeBuilder#serializedSize returns wrong value when there is a single leaf and overflow collisions (CASSANDRA-13869)
+ * Add a compaction option to TWCS to ignore sstables overlapping checks (CASSANDRA-13418)
+ * BTree.Builder memory leak (CASSANDRA-13754)
+ * Revert CASSANDRA-10368 of supporting non-pk column filtering due to correctness (CASSANDRA-13798)
+ * Add a skip read validation flag to cassandra-stress (CASSANDRA-13772)
+ * Fix cassandra-stress hang issues when an error during cluster connection happens (CASSANDRA-12938)
+ * Better bootstrap failure message when blocked by (potential) range movement (CASSANDRA-13744)
+ * "ignore" option is ignored in sstableloader (CASSANDRA-13721)
+ * Deadlock in AbstractCommitLogSegmentManager (CASSANDRA-13652)
+ * Duplicate the buffer before passing it to analyser in SASI operation (CASSANDRA-13512)
+ * Properly evict pstmts from prepared statements cache (CASSANDRA-13641)
+Merged from 3.0:
  * Improve TRUNCATE performance (CASSANDRA-13909)
  * Implement short read protection on partition boundaries (CASSANDRA-13595)
  * Fix ISE thrown by UPI.Serializer.hasNext() for some SELECT queries (CASSANDRA-13911)
@@ -439,9 +571,8 @@
  * Better handle corrupt final commitlog segment (CASSANDRA-11995)
  * StreamingHistogram is not thread safe (CASSANDRA-13756)
  * Fix MV timestamp issues (CASSANDRA-11500)
- * Better tolerate improperly formatted bcrypt hashes (CASSANDRA-13626) 
+ * Better tolerate improperly formatted bcrypt hashes (CASSANDRA-13626)
  * Fix race condition in read command serialization (CASSANDRA-13363)
- * Enable segement creation before recovering commitlogs (CASSANDRA-13587)
  * Fix AssertionError in short read protection (CASSANDRA-13747)
  * Don't skip corrupted sstables on startup (CASSANDRA-13620)
  * Fix the merging of cells with different user type versions (CASSANDRA-13776)
@@ -459,7 +590,6 @@
  * Fix invalid writetime for null cells (CASSANDRA-13711)
  * Fix ALTER TABLE statement to atomically propagate changes to the table and its MVs (CASSANDRA-12952)
  * Fixed ambiguous output of nodetool tablestats command (CASSANDRA-13722)
- * JMXEnabledThreadPoolExecutor with corePoolSize equal to maxPoolSize (Backport CASSANDRA-13329)
  * Fix Digest mismatch Exception if hints file has UnknownColumnFamily (CASSANDRA-13696)
  * Purge tombstones created by expired cells (CASSANDRA-13643)
  * Make concat work with iterators that have different subsets of columns (CASSANDRA-13482)
@@ -481,11 +611,49 @@
  * Fix toJSONString for the UDT, tuple and collection types (CASSANDRA-13592)
  * Fix nested Tuples/UDTs validation (CASSANDRA-13646)
 Merged from 2.1:
- * Remove stress-test target in CircleCI as it's not existing (CASSANDRA-13775)
  * Clone HeartBeatState when building gossip messages. Make its generation/version volatile (CASSANDRA-13700)
 
 
-3.0.14
+3.11.0
+ * Allow native function calls in CQLSSTableWriter (CASSANDRA-12606)
+ * Replace string comparison with regex/number checks in MessagingService test (CASSANDRA-13216)
+ * Fix formatting of duration columns in CQLSH (CASSANDRA-13549)
+ * Fix the problem with duplicated rows when using paging with SASI (CASSANDRA-13302)
+ * Allow CONTAINS statements filtering on the partition key and it’s parts (CASSANDRA-13275)
+ * Fall back to even ranges calculation in clusters with vnodes when tokens are distributed unevenly (CASSANDRA-13229)
+ * Fix duration type validation to prevent overflow (CASSANDRA-13218)
+ * Forbid unsupported creation of SASI indexes over partition key columns (CASSANDRA-13228)
+ * Reject multiple values for a key in CQL grammar. (CASSANDRA-13369)
+ * UDA fails without input rows (CASSANDRA-13399)
+ * Fix compaction-stress by using daemonInitialization (CASSANDRA-13188)
+ * V5 protocol flags decoding broken (CASSANDRA-13443)
+ * Use write lock not read lock for removing sstables from compaction strategies. (CASSANDRA-13422)
+ * Use corePoolSize equal to maxPoolSize in JMXEnabledThreadPoolExecutors (CASSANDRA-13329)
+ * Avoid rebuilding SASI indexes containing no values (CASSANDRA-12962)
+ * Add charset to Analyser input stream (CASSANDRA-13151)
+ * Fix testLimitSSTables flake caused by concurrent flush (CASSANDRA-12820)
+ * cdc column addition strikes again (CASSANDRA-13382)
+ * Fix static column indexes (CASSANDRA-13277)
+ * DataOutputBuffer.asNewBuffer broken (CASSANDRA-13298)
+ * unittest CipherFactoryTest failed on MacOS (CASSANDRA-13370)
+ * Forbid SELECT restrictions and CREATE INDEX over non-frozen UDT columns (CASSANDRA-13247)
+ * Default logging we ship will incorrectly print "?:?" for "%F:%L" pattern (CASSANDRA-13317)
+ * Possible AssertionError in UnfilteredRowIteratorWithLowerBound (CASSANDRA-13366)
+ * Support unaligned memory access for AArch64 (CASSANDRA-13326)
+ * Improve SASI range iterator efficiency on intersection with an empty range (CASSANDRA-12915).
+ * Fix equality comparisons of columns using the duration type (CASSANDRA-13174)
+ * Obfuscate password in stress-graphs (CASSANDRA-12233)
+ * Move to FastThreadLocalThread and FastThreadLocal (CASSANDRA-13034)
+ * nodetool stopdaemon errors out (CASSANDRA-13030)
+ * Tables in system_distributed should not use gcgs of 0 (CASSANDRA-12954)
+ * Fix primary index calculation for SASI (CASSANDRA-12910)
+ * More fixes to the TokenAllocator (CASSANDRA-12990)
+ * NoReplicationTokenAllocator should work with zero replication factor (CASSANDRA-12983)
+ * Address message coalescing regression (CASSANDRA-12676)
+ * Delete illegal character from StandardTokenizerImpl.jflex (CASSANDRA-13417)
+ * Fix cqlsh automatic protocol downgrade regression (CASSANDRA-13307)
+ * Tracing payload not passed from QueryMessage to tracing session (CASSANDRA-12835)
+Merged from 3.0:
  * Ensure int overflow doesn't occur when calculating large partition warning size (CASSANDRA-13172)
  * Ensure consistent view of partition columns between coordinator and replica in ColumnFilter (CASSANDRA-13004)
  * Failed unregistering mbean during drop keyspace (CASSANDRA-13346)
@@ -506,13 +674,8 @@
  * Avoid name clashes in CassandraIndexTest (CASSANDRA-13427)
  * Handling partially written hint files (CASSANDRA-12728)
  * Interrupt replaying hints on decommission (CASSANDRA-13308)
- * Fix schema version calculation for rolling upgrades (CASSANDRA-13441)
-Merged from 2.2:
- * Nodes started with join_ring=False should be able to serve requests when authentication is enabled (CASSANDRA-11381)
- * cqlsh COPY FROM: increment error count only for failures, not for attempts (CASSANDRA-13209)
-
-
-3.0.13
+ * Handling partially written hint files (CASSANDRA-12728)
+ * Fix NPE issue in StorageService (CASSANDRA-13060)
  * Make reading of range tombstones more reliable (CASSANDRA-12811)
  * Fix startup problems due to schema tables not completely flushed (CASSANDRA-12213)
  * Fix view builder bug that can filter out data on restart (CASSANDRA-13405)
@@ -521,6 +684,7 @@
  * Fix hint delivery when using ext+internal IPs with prefer_local enabled (CASSANDRA-13020)
  * Fix possible NPE on upgrade to 3.0/3.X in case of IO errors (CASSANDRA-13389)
  * Legacy deserializer can create empty range tombstones (CASSANDRA-13341)
+ * Legacy caching options can prevent 3.0 upgrade (CASSANDRA-13384)
  * Use the Kernel32 library to retrieve the PID on Windows and fix startup checks (CASSANDRA-13333)
  * Fix code to not exchange schema across major versions (CASSANDRA-13274)
  * Dropping column results in "corrupt" SSTable (CASSANDRA-13337)
@@ -530,12 +694,35 @@
  * Propagate row deletions in 2i tables on upgrade (CASSANDRA-13320)
  * Slice.isEmpty() returns false for some empty slices (CASSANDRA-13305)
  * Add formatted row output to assertEmpty in CQL Tester (CASSANDRA-13238)
- * Legacy caching options can prevent 3.0 upgrade (CASSANDRA-13384)
+ * Prevent data loss on upgrade 2.1 - 3.0 by adding component separator to LogRecord absolute path (CASSANDRA-13294)
+ * Improve testing on macOS by eliminating sigar logging (CASSANDRA-13233)
+ * Cqlsh copy-from should error out when csv contains invalid data for collections (CASSANDRA-13071)
+ * Fix "multiple versions of ant detected..." when running ant test (CASSANDRA-13232)
+ * Coalescing strategy sleeps too much (CASSANDRA-13090)
+ * Faster StreamingHistogram (CASSANDRA-13038)
+ * Legacy deserializer can create unexpected boundary range tombstones (CASSANDRA-13237)
+ * Remove unnecessary assertion from AntiCompactionTest (CASSANDRA-13070)
+ * Fix cqlsh COPY for dates before 1900 (CASSANDRA-13185)
+ * Use keyspace replication settings on system.size_estimates table (CASSANDRA-9639)
+ * Add vm.max_map_count StartupCheck (CASSANDRA-13008)
+ * Hint related logging should include the IP address of the destination in addition to
+   host ID (CASSANDRA-13205)
+ * Reloading logback.xml does not work (CASSANDRA-13173)
+ * Lightweight transactions temporarily fail after upgrade from 2.1 to 3.0 (CASSANDRA-13109)
+ * Duplicate rows after upgrading from 2.1.16 to 3.0.10/3.9 (CASSANDRA-13125)
+ * Fix UPDATE queries with empty IN restrictions (CASSANDRA-13152)
+ * Fix handling of partition with partition-level deletion plus
+   live rows in sstabledump (CASSANDRA-13177)
+ * Provide user workaround when system_schema.columns does not contain entries
+   for a table that's in system_schema.tables (CASSANDRA-13180)
  * Nodetool upgradesstables/scrub/compact ignores system tables (CASSANDRA-13410)
- * Fix NPE issue in StorageService (CASSANDRA-13060)
+ * Fix schema version calculation for rolling upgrades (CASSANDRA-13441)
 Merged from 2.2:
+ * Nodes started with join_ring=False should be able to serve requests when authentication is enabled (CASSANDRA-11381)
+ * cqlsh COPY FROM: increment error count only for failures, not for attempts (CASSANDRA-13209)
  * Avoid starting gossiper in RemoveTest (CASSANDRA-13407)
  * Fix weightedSize() for row-cache reported by JMX and NodeTool (CASSANDRA-13393)
+ * Fix JVM metric names (CASSANDRA-13103)
  * Honor truststore-password parameter in cassandra-stress (CASSANDRA-12773)
  * Discard in-flight shadow round responses (CASSANDRA-12653)
  * Don't anti-compact repaired data to avoid inconsistencies (CASSANDRA-13153)
@@ -543,57 +730,147 @@
  * Commitlog replay may fail if last mutation is within 4 bytes of end of segment (CASSANDRA-13282)
  * Fix queries updating multiple time the same list (CASSANDRA-13130)
  * Fix GRANT/REVOKE when keyspace isn't specified (CASSANDRA-13053)
-Merged from 2.1:
- * Fix 2ndary index queries on partition keys for tables with static columns CASSANDRA-13147
- * Fix ParseError unhashable type list in cqlsh copy from (CASSANDRA-13364)
-
-
-3.0.12
- * Prevent data loss on upgrade 2.1 - 3.0 by adding component separator to LogRecord absolute path (CASSANDRA-13294)
- * Improve testing on macOS by eliminating sigar logging (CASSANDRA-13233)
- * Cqlsh copy-from should error out when csv contains invalid data for collections (CASSANDRA-13071)
- * Update c.yaml doc for offheap memtables (CASSANDRA-13179)
- * Faster StreamingHistogram (CASSANDRA-13038)
- * Legacy deserializer can create unexpected boundary range tombstones (CASSANDRA-13237)
- * Remove unnecessary assertion from AntiCompactionTest (CASSANDRA-13070)
- * Fix cqlsh COPY for dates before 1900 (CASSANDRA-13185)
-Merged from 2.2:
- * Avoid race on receiver by starting streaming sender thread after sending init message (CASSANDRA-12886)
- * Fix "multiple versions of ant detected..." when running ant test (CASSANDRA-13232)
- * Coalescing strategy sleeps too much (CASSANDRA-13090)
  * Fix flaky LongLeveledCompactionStrategyTest (CASSANDRA-12202)
  * Fix failing COPY TO STDOUT (CASSANDRA-12497)
  * Fix ColumnCounter::countAll behaviour for reverse queries (CASSANDRA-13222)
  * Exceptions encountered calling getSeeds() breaks OTC thread (CASSANDRA-13018)
+ * Fix negative mean latency metric (CASSANDRA-12876)
+ * Use only one file pointer when creating commitlog segments (CASSANDRA-12539)
 Merged from 2.1:
+ * Fix 2ndary index queries on partition keys for tables with static columns (CASSANDRA-13147)
+ * Fix ParseError unhashable type list in cqlsh copy from (CASSANDRA-13364)
  * Remove unused repositories (CASSANDRA-13278)
  * Log stacktrace of uncaught exceptions (CASSANDRA-13108)
+ * Use portable stderr for java error in startup (CASSANDRA-13211)
+ * Fix Thread Leak in OutboundTcpConnection (CASSANDRA-13204)
+ * Coalescing strategy can enter infinite loop (CASSANDRA-13159)
 
 
-3.0.11
- * Use keyspace replication settings on system.size_estimates table (CASSANDRA-9639)
- * Add vm.max_map_count StartupCheck (CASSANDRA-13008)
- * Hint related logging should include the IP address of the destination in addition to 
-   host ID (CASSANDRA-13205)
- * Reloading logback.xml does not work (CASSANDRA-13173)
- * Lightweight transactions temporarily fail after upgrade from 2.1 to 3.0 (CASSANDRA-13109)
- * Duplicate rows after upgrading from 2.1.16 to 3.0.10/3.9 (CASSANDRA-13125)
- * Fix UPDATE queries with empty IN restrictions (CASSANDRA-13152)
- * Abort or retry on failed hints delivery (CASSANDRA-13124)
- * Fix handling of partition with partition-level deletion plus
-   live rows in sstabledump (CASSANDRA-13177)
- * Provide user workaround when system_schema.columns does not contain entries
-   for a table that's in system_schema.tables (CASSANDRA-13180)
+3.10
+ * Fix secondary index queries regression (CASSANDRA-13013)
+ * Add duration type to the protocol V5 (CASSANDRA-12850)
+ * Fix duration type validation (CASSANDRA-13143)
+ * Fix flaky GcCompactionTest (CASSANDRA-12664)
+ * Fix TestHintedHandoff.hintedhandoff_decom_test (CASSANDRA-13058)
+ * Fixed query monitoring for range queries (CASSANDRA-13050)
+ * Remove outboundBindAny configuration property (CASSANDRA-12673)
+ * Use correct bounds for all-data range when filtering (CASSANDRA-12666)
+ * Remove timing window in test case (CASSANDRA-12875)
+ * Resolve unit testing without JCE security libraries installed (CASSANDRA-12945)
+ * Fix inconsistencies in cassandra-stress load balancing policy (CASSANDRA-12919)
+ * Fix validation of non-frozen UDT cells (CASSANDRA-12916)
+ * Don't shut down socket input/output on StreamSession (CASSANDRA-12903)
+ * Fix Murmur3PartitionerTest (CASSANDRA-12858)
+ * Move cqlsh syntax rules into separate module and allow easier customization (CASSANDRA-12897)
+ * Fix CommitLogSegmentManagerTest (CASSANDRA-12283)
+ * Fix cassandra-stress truncate option (CASSANDRA-12695)
+ * Fix crossNode value when receiving messages (CASSANDRA-12791)
+ * Don't load MX4J beans twice (CASSANDRA-12869)
+ * Extend native protocol request flags, add versions to SUPPORTED, and introduce ProtocolVersion enum (CASSANDRA-12838)
+ * Set JOINING mode when running pre-join tasks (CASSANDRA-12836)
+ * remove net.mintern.primitive library due to license issue (CASSANDRA-12845)
+ * Properly format IPv6 addresses when logging JMX service URL (CASSANDRA-12454)
+ * Optimize the vnode allocation for single replica per DC (CASSANDRA-12777)
+ * Use non-token restrictions for bounds when token restrictions are overridden (CASSANDRA-12419)
+ * Fix CQLSH auto completion for PER PARTITION LIMIT (CASSANDRA-12803)
+ * Use different build directories for Eclipse and Ant (CASSANDRA-12466)
+ * Avoid potential AttributeError in cqlsh due to no table metadata (CASSANDRA-12815)
+ * Fix RandomReplicationAwareTokenAllocatorTest.testExistingCluster (CASSANDRA-12812)
+ * Upgrade commons-codec to 1.9 (CASSANDRA-12790)
+ * Make the fanout size for LeveledCompactionStrategy to be configurable (CASSANDRA-11550)
+ * Add duration data type (CASSANDRA-11873)
+ * Fix timeout in ReplicationAwareTokenAllocatorTest (CASSANDRA-12784)
+ * Improve sum aggregate functions (CASSANDRA-12417)
+ * Make cassandra.yaml docs for batch_size_*_threshold_in_kb reflect changes in CASSANDRA-10876 (CASSANDRA-12761)
+ * cqlsh fails to format collections when using aliases (CASSANDRA-11534)
+ * Check for hash conflicts in prepared statements (CASSANDRA-12733)
+ * Exit query parsing upon first error (CASSANDRA-12598)
+ * Fix cassandra-stress to use single seed in UUID generation (CASSANDRA-12729)
+ * CQLSSTableWriter does not allow Update statement (CASSANDRA-12450)
+ * Config class uses boxed types but DD exposes primitive types (CASSANDRA-12199)
+ * Add pre- and post-shutdown hooks to Storage Service (CASSANDRA-12461)
+ * Add hint delivery metrics (CASSANDRA-12693)
+ * Remove IndexInfo cache from FileIndexInfoRetriever (CASSANDRA-12731)
+ * ColumnIndex does not reuse buffer (CASSANDRA-12502)
+ * cdc column addition still breaks schema migration tasks (CASSANDRA-12697)
+ * Upgrade metrics-reporter dependencies (CASSANDRA-12089)
+ * Tune compaction thread count via nodetool (CASSANDRA-12248)
+ * Add +=/-= shortcut syntax for update queries (CASSANDRA-12232)
+ * Include repair session IDs in repair start message (CASSANDRA-12532)
+ * Add a blocking task to Index, run before joining the ring (CASSANDRA-12039)
+ * Fix NPE when using CQLSSTableWriter (CASSANDRA-12667)
+ * Support optional backpressure strategies at the coordinator (CASSANDRA-9318)
+ * Make randompartitioner work with new vnode allocation (CASSANDRA-12647)
+ * Fix cassandra-stress graphing (CASSANDRA-12237)
+ * Allow filtering on partition key columns for queries without secondary indexes (CASSANDRA-11031)
+ * Fix Cassandra Stress reporting thread model and precision (CASSANDRA-12585)
+ * Add JMH benchmarks.jar (CASSANDRA-12586)
+ * Cleanup uses of AlterTableStatementColumn (CASSANDRA-12567)
+ * Add keep-alive to streaming (CASSANDRA-11841)
+ * Tracing payload is passed through newSession(..) (CASSANDRA-11706)
+ * avoid deleting non existing sstable files and improve related log messages (CASSANDRA-12261)
+ * json/yaml output format for nodetool compactionhistory (CASSANDRA-12486)
+ * Retry all internode messages once after a connection is
+   closed and reopened (CASSANDRA-12192)
+ * Add support to rebuild from targeted replica (CASSANDRA-9875)
+ * Add sequence distribution type to cassandra stress (CASSANDRA-12490)
+ * "SELECT * FROM foo LIMIT ;" does not error out (CASSANDRA-12154)
+ * Define executeLocally() at the ReadQuery Level (CASSANDRA-12474)
+ * Extend read/write failure messages with a map of replica addresses
+   to error codes in the v5 native protocol (CASSANDRA-12311)
+ * Fix rebuild of SASI indexes with existing index files (CASSANDRA-12374)
+ * Let DatabaseDescriptor not implicitly startup services (CASSANDRA-9054, 12550)
+ * Fix clustering indexes in presence of static columns in SASI (CASSANDRA-12378)
+ * Fix queries on columns with reversed type on SASI indexes (CASSANDRA-12223)
+ * Added slow query log (CASSANDRA-12403)
+ * Count full coordinated request against timeout (CASSANDRA-12256)
+ * Allow TTL with null value on insert and update (CASSANDRA-12216)
+ * Make decommission operation resumable (CASSANDRA-12008)
+ * Add support to one-way targeted repair (CASSANDRA-9876)
+ * Remove clientutil jar (CASSANDRA-11635)
+ * Fix compaction throughput throttle (CASSANDRA-12366, CASSANDRA-12717)
+ * Delay releasing Memtable memory on flush until PostFlush has finished running (CASSANDRA-12358)
+ * Cassandra stress should dump all setting on startup (CASSANDRA-11914)
+ * Make it possible to compact a given token range (CASSANDRA-10643)
+ * Allow updating DynamicEndpointSnitch properties via JMX (CASSANDRA-12179)
+ * Collect metrics on queries by consistency level (CASSANDRA-7384)
+ * Add support for GROUP BY to SELECT statement (CASSANDRA-10707)
+ * Deprecate memtable_cleanup_threshold and update default for memtable_flush_writers (CASSANDRA-12228)
+ * Upgrade to OHC 0.4.4 (CASSANDRA-12133)
+ * Add version command to cassandra-stress (CASSANDRA-12258)
+ * Create compaction-stress tool (CASSANDRA-11844)
+ * Garbage-collecting compaction operation and schema option (CASSANDRA-7019)
+ * Add beta protocol flag for v5 native protocol (CASSANDRA-12142)
+ * Support filtering on non-PRIMARY KEY columns in the CREATE
+   MATERIALIZED VIEW statement's WHERE clause (CASSANDRA-10368)
+ * Unify STDOUT and SYSTEMLOG logback format (CASSANDRA-12004)
+ * COPY FROM should raise error for non-existing input files (CASSANDRA-12174)
+ * Faster write path (CASSANDRA-12269)
+ * Option to leave omitted columns in INSERT JSON unset (CASSANDRA-11424)
+ * Support json/yaml output in nodetool tpstats (CASSANDRA-12035)
+ * Expose metrics for successful/failed authentication attempts (CASSANDRA-10635)
+ * Prepend snapshot name with "truncated" or "dropped" when a snapshot
+   is taken before truncating or dropping a table (CASSANDRA-12178)
+ * Optimize RestrictionSet (CASSANDRA-12153)
+ * cqlsh does not automatically downgrade CQL version (CASSANDRA-12150)
+ * Omit (de)serialization of state variable in UDAs (CASSANDRA-9613)
+ * Create a system table to expose prepared statements (CASSANDRA-8831)
+ * Reuse DataOutputBuffer from ColumnIndex (CASSANDRA-11970)
+ * Remove DatabaseDescriptor dependency from SegmentedFile (CASSANDRA-11580)
+ * Add supplied username to authentication error messages (CASSANDRA-12076)
+ * Remove pre-startup check for open JMX port (CASSANDRA-12074)
+ * Remove compaction Severity from DynamicEndpointSnitch (CASSANDRA-11738)
+ * Restore resumable hints delivery (CASSANDRA-11960)
+ * Properly report LWT contention (CASSANDRA-12626)
+Merged from 3.0:
  * Dump threads when unit tests time out (CASSANDRA-13117)
  * Better error when modifying function permissions without explicit keyspace (CASSANDRA-12925)
  * Indexer is not correctly invoked when building indexes over sstables (CASSANDRA-13075)
  * Read repair is not blocking repair to finish in foreground repair (CASSANDRA-13115)
- * Stress daemon help is incorrect (CASSANDRA-12563)
+ * Stress daemon help is incorrect(CASSANDRA-12563)
  * Remove ALTER TYPE support (CASSANDRA-12443)
  * Fix assertion for certain legacy range tombstone pattern (CASSANDRA-12203)
- * Set javac encoding to utf-8 (CASSANDRA-11077)
  * Replace empty strings with null values if they cannot be converted (CASSANDRA-12794)
- * Fixed flacky SSTableRewriterTest: check file counts before calling validateCFS (CASSANDRA-12348)
  * Fix deserialization of 2.x DeletedCells (CASSANDRA-12620)
  * Add parent repair session id to anticompaction log message (CASSANDRA-12186)
  * Improve contention handling on failure to acquire MV lock for streaming and hints (CASSANDRA-12905)
@@ -603,6 +880,7 @@
  * Nodetool compactionstats fails with NullPointerException (CASSANDRA-13021)
  * Thread local pools never cleaned up (CASSANDRA-13033)
  * Set RPC_READY to false when draining or if a node is marked as shutdown (CASSANDRA-12781)
+ * CQL often queries static columns unnecessarily (CASSANDRA-12768)
  * Make sure sstables only get committed when it's safe to discard commit log records (CASSANDRA-12956)
  * Reject default_time_to_live option when creating or altering MVs (CASSANDRA-12868)
  * Nodetool should use a more sane max heap size (CASSANDRA-12739)
@@ -610,43 +888,14 @@
  * AnticompactionRequestSerializer serializedSize is incorrect (CASSANDRA-12934)
  * Prevent reloading of logback.xml from UDF sandbox (CASSANDRA-12535)
  * Reenable HeapPool (CASSANDRA-12900)
-Merged from 2.2:
- * Fix JVM metric names (CASSANDRA-13103)
- * Fix negative mean latency metric (CASSANDRA-12876)
- * Use only one file pointer when creating commitlog segments (CASSANDRA-12539)
- * Fix speculative retry bugs (CASSANDRA-13009)
- * Fix handling of nulls and unsets in IN conditions (CASSANDRA-12981)
- * Fix race causing infinite loop if Thrift server is stopped before it starts listening (CASSANDRA-12856)
- * CompactionTasks now correctly drops sstables out of compaction when not enough disk space is available (CASSANDRA-12979)
- * Remove support for non-JavaScript UDFs (CASSANDRA-12883)
- * Fix DynamicEndpointSnitch noop in multi-datacenter situations (CASSANDRA-13074)
- * cqlsh copy-from: encode column names to avoid primary key parsing errors (CASSANDRA-12909)
- * Temporarily fix bug that creates commit log when running offline tools (CASSANDRA-8616)
- * Reduce granuality of OpOrder.Group during index build (CASSANDRA-12796)
- * Test bind parameters and unset parameters in InsertUpdateIfConditionTest (CASSANDRA-12980)
- * Do not specify local address on outgoing connection when listen_on_broadcast_address is set (CASSANDRA-12673)
- * Use saved tokens when setting local tokens on StorageService.joinRing (CASSANDRA-12935)
- * cqlsh: fix DESC TYPES errors (CASSANDRA-12914)
- * Fix leak on skipped SSTables in sstableupgrade (CASSANDRA-12899)
- * Avoid blocking gossip during pending range calculation (CASSANDRA-12281)
-Merged from 2.1:
- * Use portable stderr for java error in startup (CASSANDRA-13211)
- * Fix Thread Leak in OutboundTcpConnection (CASSANDRA-13204)
- * Coalescing strategy can enter infinite loop (CASSANDRA-13159)
- * Upgrade netty version to fix memory leak with client encryption (CASSANDRA-13114)
- * cqlsh copy-from: sort user type fields in csv (CASSANDRA-12959)
-
-
-3.0.10
  * Disallow offheap_buffers memtable allocation (CASSANDRA-11039)
  * Fix CommitLogSegmentManagerTest (CASSANDRA-12283)
  * Pass root cause to CorruptBlockException when uncompression failed (CASSANDRA-12889)
- * Fix partition count log during compaction (CASSANDRA-12184)
  * Batch with multiple conditional updates for the same partition causes AssertionError (CASSANDRA-12867)
  * Make AbstractReplicationStrategy extendable from outside its package (CASSANDRA-12788)
- * Fix CommitLogTest.testDeleteIfNotDirty (CASSANDRA-12854)
  * Don't tell users to turn off consistent rangemovements during rebuild. (CASSANDRA-12296)
- * Avoid deadlock due to materialized view lock contention (CASSANDRA-12689)
+ * Fix CommitLogTest.testDeleteIfNotDirty (CASSANDRA-12854)
+ * Avoid deadlock due to MV lock contention (CASSANDRA-12689)
  * Fix for KeyCacheCqlTest flakiness (CASSANDRA-12801)
  * Include SSTable filename in compacting large row message (CASSANDRA-12384)
  * Fix potential socket leak (CASSANDRA-12329, CASSANDRA-12330)
@@ -665,14 +914,42 @@
  * Fix failure in LogTransactionTest (CASSANDRA-12632)
  * Fix potentially incomplete non-frozen UDT values when querying with the
    full primary key specified (CASSANDRA-12605)
+ * Make sure repaired tombstones are dropped when only_purge_repaired_tombstones is enabled (CASSANDRA-12703)
  * Skip writing MV mutations to commitlog on mutation.applyUnsafe() (CASSANDRA-11670)
  * Establish consistent distinction between non-existing partition and NULL value for LWTs on static columns (CASSANDRA-12060)
  * Extend ColumnIdentifier.internedInstances key to include the type that generated the byte buffer (CASSANDRA-12516)
- * Backport CASSANDRA-10756 (race condition in NativeTransportService shutdown) (CASSANDRA-12472)
+ * Handle composite prefixes with final EOC=0 as in 2.x and refactor LegacyLayout.decodeBound (CASSANDRA-12423)
+ * select_distinct_with_deletions_test failing on non-vnode environments (CASSANDRA-11126)
+ * Stack Overflow returned to queries while upgrading (CASSANDRA-12527)
+ * Fix legacy regex for temporary files from 2.2 (CASSANDRA-12565)
+ * Add option to state current gc_grace_seconds to tools/bin/sstablemetadata (CASSANDRA-12208)
+ * Fix file system race condition that may cause LogAwareFileLister to fail to classify files (CASSANDRA-11889)
+ * Fix file handle leaks due to simultaneous compaction/repair and
+   listing snapshots, calculating snapshot sizes, or making schema
+   changes (CASSANDRA-11594)
+ * Fix nodetool repair exits with 0 for some errors (CASSANDRA-12508)
+ * Do not shut down BatchlogManager twice during drain (CASSANDRA-12504)
+ * Disk failure policy should not be invoked on out of space (CASSANDRA-12385)
+ * Calculate last compacted key on startup (CASSANDRA-6216)
+ * Add schema to snapshot manifest, add USING TIMESTAMP clause to ALTER TABLE statements (CASSANDRA-7190)
  * If CF has no clustering columns, any row cache is full partition cache (CASSANDRA-12499)
  * Correct log message for statistics of offheap memtable flush (CASSANDRA-12776)
  * Explicitly set locale for string validation (CASSANDRA-12541,CASSANDRA-12542,CASSANDRA-12543,CASSANDRA-12545)
 Merged from 2.2:
+ * Fix speculative retry bugs (CASSANDRA-13009)
+ * Fix handling of nulls and unsets in IN conditions (CASSANDRA-12981)
+ * Fix race causing infinite loop if Thrift server is stopped before it starts listening (CASSANDRA-12856)
+ * CompactionTasks now correctly drops sstables out of compaction when not enough disk space is available (CASSANDRA-12979)
+ * Remove support for non-JavaScript UDFs (CASSANDRA-12883)
+ * Fix DynamicEndpointSnitch noop in multi-datacenter situations (CASSANDRA-13074)
+ * cqlsh copy-from: encode column names to avoid primary key parsing errors (CASSANDRA-12909)
+ * Temporarily fix bug that creates commit log when running offline tools (CASSANDRA-8616)
+ * Reduce granuality of OpOrder.Group during index build (CASSANDRA-12796)
+ * Test bind parameters and unset parameters in InsertUpdateIfConditionTest (CASSANDRA-12980)
+ * Use saved tokens when setting local tokens on StorageService.joinRing (CASSANDRA-12935)
+ * cqlsh: fix DESC TYPES errors (CASSANDRA-12914)
+ * Fix leak on skipped SSTables in sstableupgrade (CASSANDRA-12899)
+ * Avoid blocking gossip during pending range calculation (CASSANDRA-12281)
  * Fix purgeability of tombstones with max timestamp (CASSANDRA-12792)
  * Fail repair if participant dies during sync or anticompaction (CASSANDRA-12901)
  * cqlsh COPY: unprotected pk values before converting them if not using prepared statements (CASSANDRA-12863)
@@ -689,35 +966,57 @@
  * Fix exceptions when enabling gossip on nodes that haven't joined the ring (CASSANDRA-12253)
  * Fix authentication problem when invoking cqlsh copy from a SOURCE command (CASSANDRA-12642)
  * Decrement pending range calculator jobs counter in finally block
-  (CASSANDRA-12554)
+ * cqlshlib tests: increase default execute timeout (CASSANDRA-12481)
+ * Forward writes to replacement node when replace_address != broadcast_address (CASSANDRA-8523)
+ * Fail repair on non-existing table (CASSANDRA-12279)
+ * Enable repair -pr and -local together (fix regression of CASSANDRA-7450) (CASSANDRA-12522)
  * Split consistent range movement flag correction (CASSANDRA-12786)
 Merged from 2.1:
- * Add system property to set the max number of native transport requests in queue (CASSANDRA-11363)
+ * Upgrade netty version to fix memory leak with client encryption (CASSANDRA-13114)
+ * cqlsh copy-from: sort user type fields in csv (CASSANDRA-12959)
  * Don't skip sstables based on maxLocalDeletionTime (CASSANDRA-12765)
 
 
-3.0.9
- * Handle composite prefixes with final EOC=0 as in 2.x and refactor LegacyLayout.decodeBound (CASSANDRA-12423)
+3.8, 3.9
+ * Fix value skipping with counter columns (CASSANDRA-11726)
+ * Fix nodetool tablestats miss SSTable count (CASSANDRA-12205)
+ * Fixed flacky SSTablesIteratedTest (CASSANDRA-12282)
+ * Fixed flacky SSTableRewriterTest: check file counts before calling validateCFS (CASSANDRA-12348)
+ * cqlsh: Fix handling of $$-escaped strings (CASSANDRA-12189)
+ * Fix SSL JMX requiring truststore containing server cert (CASSANDRA-12109)
+ * RTE from new CDC column breaks in flight queries (CASSANDRA-12236)
+ * Fix hdr logging for single operation workloads (CASSANDRA-12145)
+ * Fix SASI PREFIX search in CONTAINS mode with partial terms (CASSANDRA-12073)
+ * Increase size of flushExecutor thread pool (CASSANDRA-12071)
+ * Partial revert of CASSANDRA-11971, cannot recycle buffer in SP.sendMessagesToNonlocalDC (CASSANDRA-11950)
+ * Upgrade netty to 4.0.39 (CASSANDRA-12032, CASSANDRA-12034)
+ * Improve details in compaction log message (CASSANDRA-12080)
+ * Allow unset values in CQLSSTableWriter (CASSANDRA-11911)
+ * Chunk cache to request compressor-compatible buffers if pool space is exhausted (CASSANDRA-11993)
+ * Remove DatabaseDescriptor dependencies from SequentialWriter (CASSANDRA-11579)
+ * Move skip_stop_words filter before stemming (CASSANDRA-12078)
+ * Support seek() in EncryptedFileSegmentInputStream (CASSANDRA-11957)
+ * SSTable tools mishandling LocalPartitioner (CASSANDRA-12002)
+ * When SEPWorker assigned work, set thread name to match pool (CASSANDRA-11966)
+ * Add cross-DC latency metrics (CASSANDRA-11569)
+ * Allow terms in selection clause (CASSANDRA-10783)
+ * Add bind variables to trace (CASSANDRA-11719)
+ * Switch counter shards' clock to timestamps (CASSANDRA-9811)
+ * Introduce HdrHistogram and response/service/wait separation to stress tool (CASSANDRA-11853)
+ * entry-weighers in QueryProcessor should respect partitionKeyBindIndexes field (CASSANDRA-11718)
+ * Support older ant versions (CASSANDRA-11807)
+ * Estimate compressed on disk size when deciding if sstable size limit reached (CASSANDRA-11623)
+ * cassandra-stress profiles should support case sensitive schemas (CASSANDRA-11546)
+ * Remove DatabaseDescriptor dependency from FileUtils (CASSANDRA-11578)
+ * Faster streaming (CASSANDRA-9766)
+ * Add prepared query parameter to trace for "Execute CQL3 prepared query" session (CASSANDRA-11425)
+ * Add repaired percentage metric (CASSANDRA-11503)
+ * Add Change-Data-Capture (CASSANDRA-8844)
+Merged from 3.0:
  * Fix paging for 2.x to 3.x upgrades (CASSANDRA-11195)
- * select_distinct_with_deletions_test failing on non-vnode environments (CASSANDRA-11126)
- * Stack Overflow returned to queries while upgrading (CASSANDRA-12527)
- * Fix legacy regex for temporary files from 2.2 (CASSANDRA-12565)
- * Add option to state current gc_grace_seconds to tools/bin/sstablemetadata (CASSANDRA-12208)
- * Fix file system race condition that may cause LogAwareFileLister to fail to classify files (CASSANDRA-11889)
- * Fix file handle leaks due to simultaneous compaction/repair and
-   listing snapshots, calculating snapshot sizes, or making schema
-   changes (CASSANDRA-11594)
- * Fix nodetool repair exits with 0 for some errors (CASSANDRA-12508)
- * Do not shut down BatchlogManager twice during drain (CASSANDRA-12504)
- * Disk failure policy should not be invoked on out of space (CASSANDRA-12385)
- * Calculate last compacted key on startup (CASSANDRA-6216)
- * Add schema to snapshot manifest, add USING TIMESTAMP clause to ALTER TABLE statements (CASSANDRA-7190)
  * Fix clean interval not sent to commit log for empty memtable flush (CASSANDRA-12436)
  * Fix potential resource leak in RMIServerSocketFactoryImpl (CASSANDRA-12331)
- * Backport CASSANDRA-12002 (CASSANDRA-12177)
  * Make sure compaction stats are updated when compaction is interrupted (CASSANDRA-12100)
- * Fix potential bad messaging service message for paged range reads
-   within mixed-version 3.x clusters (CASSANDRA-12249)
  * Change commitlog and sstables to track dirty and clean intervals (CASSANDRA-11828)
  * NullPointerException during compaction on table with static columns (CASSANDRA-12336)
  * Fixed ConcurrentModificationException when reading metrics in GraphiteReporter (CASSANDRA-11823)
@@ -734,6 +1033,8 @@
    to connect with too low of a protocol version (CASSANDRA-11464)
  * NullPointerExpception when reading/compacting table (CASSANDRA-11988)
  * Fix problem with undeleteable rows on upgrade to new sstable format (CASSANDRA-12144)
+ * Fix potential bad messaging service message for paged range reads
+   within mixed-version 3.x clusters (CASSANDRA-12249)
  * Fix paging logic for deleted partitions with static columns (CASSANDRA-12107)
  * Wait until the message is being send to decide which serializer must be used (CASSANDRA-11393)
  * Fix migration of static thrift column names with non-text comparators (CASSANDRA-12147)
@@ -747,14 +1048,16 @@
    those static columns in query results (CASSANDRA-12123)
  * Avoid digest mismatch with empty but static rows (CASSANDRA-12090)
  * Fix EOF exception when altering column type (CASSANDRA-11820)
+ * Fix potential race in schema during new table creation (CASSANDRA-12083)
+ * cqlsh: fix error handling in rare COPY FROM failure scenario (CASSANDRA-12070)
+ * Disable autocompaction during drain (CASSANDRA-11878)
+ * Add a metrics timer to MemtablePool and use it to track time spent blocked on memory in MemtableAllocator (CASSANDRA-11327)
+ * Fix upgrading schema with super columns with non-text subcomparators (CASSANDRA-12023)
+ * Add TimeWindowCompactionStrategy (CASSANDRA-9666)
  * Fix JsonTransformer output of partition with deletion info (CASSANDRA-12418)
  * Fix NPE in SSTableLoader when specifying partial directory path (CASSANDRA-12609)
 Merged from 2.2:
  * Add local address entry in PropertyFileSnitch (CASSANDRA-11332)
- * cqlshlib tests: increase default execute timeout (CASSANDRA-12481)
- * Forward writes to replacement node when replace_address != broadcast_address (CASSANDRA-8523)
- * Enable repair -pr and -local together (fix regression of CASSANDRA-7450) (CASSANDRA-12522)
- * Fail repair on non-existing table (CASSANDRA-12279)
  * cqlsh copy: fix missing counter values (CASSANDRA-12476)
  * Move migration tasks to non-periodic queue, assure flush executor shutdown after non-periodic executor (CASSANDRA-12251)
  * cqlsh copy: fixed possible race in initializing feeding thread (CASSANDRA-11701)
@@ -795,6 +1098,12 @@
  * Don't send erroneous NEW_NODE notifications on restart (CASSANDRA-11038)
  * StorageService shutdown hook should use a volatile variable (CASSANDRA-11984)
 Merged from 2.1:
+ * Add system property to set the max number of native transport requests in queue (CASSANDRA-11363)
+ * Fix queries with empty ByteBuffer values in clustering column restrictions (CASSANDRA-12127)
+ * Disable passing control to post-flush after flush failure to prevent data loss (CASSANDRA-11828)
+ * Allow STCS-in-L0 compactions to reduce scope with LCS (CASSANDRA-12040)
+ * cannot use cql since upgrading python to 2.7.11+ (CASSANDRA-11850)
+ * Fix filtering on clustering columns when 2i is used (CASSANDRA-11907)
  * Avoid stalling paxos when the paxos state expires (CASSANDRA-12043)
  * Remove finished incoming streaming connections from MessagingService (CASSANDRA-11854)
  * Don't try to get sstables for non-repairing column families (CASSANDRA-12077)
@@ -808,11 +1117,15 @@
  * cqlsh COPY FROM: shutdown parent cluster after forking, to avoid corrupting SSL connections (CASSANDRA-11749)
 
 
-3.0.7
+3.7
+ * Support multiple folders for user defined compaction tasks (CASSANDRA-11765)
+ * Fix race in CompactionStrategyManager's pause/resume (CASSANDRA-11922)
+Merged from 3.0:
  * Fix legacy serialization of Thrift-generated non-compound range tombstones
    when communicating with 2.x nodes (CASSANDRA-11930)
  * Fix Directories instantiations where CFS.initialDirectories should be used (CASSANDRA-11849)
  * Avoid referencing DatabaseDescriptor in AbstractType (CASSANDRA-11912)
+ * Don't use static dataDirectories field in Directories instances (CASSANDRA-11647)
  * Fix sstables not being protected from removal during index build (CASSANDRA-11905)
  * cqlsh: Suppress stack trace from Read/WriteFailures (CASSANDRA-11032)
  * Remove unneeded code to repair index summaries that have
@@ -826,17 +1139,18 @@
  * Update Java Driver (CASSANDRA-11615)
 Merged from 2.2:
  * Persist local metadata earlier in startup sequence (CASSANDRA-11742)
- * Run CommitLog tests with different compression settings (CASSANDRA-9039)
  * cqlsh: fix tab completion for case-sensitive identifiers (CASSANDRA-11664)
  * Avoid showing estimated key as -1 in tablestats (CASSANDRA-11587)
  * Fix possible race condition in CommitLog.recover (CASSANDRA-11743)
  * Enable client encryption in sstableloader with cli options (CASSANDRA-11708)
  * Possible memory leak in NIODataInputStream (CASSANDRA-11867)
  * Add seconds to cqlsh tracing session duration (CASSANDRA-11753)
+ * Fix commit log replay after out-of-order flush completion (CASSANDRA-9669)
  * Prohibit Reversed Counter type as part of the PK (CASSANDRA-9395)
+ * cqlsh: correctly handle non-ascii chars in error messages (CASSANDRA-11626)
 Merged from 2.1:
+ * Run CommitLog tests with different compression settings (CASSANDRA-9039)
  * cqlsh: apply current keyspace to source command (CASSANDRA-11152)
- * Backport CASSANDRA-11578 (CASSANDRA-11750)
  * Clear out parent repair session if repair coordinator dies (CASSANDRA-11824)
  * Set default streaming_socket_timeout_in_ms to 24 hours (CASSANDRA-11840)
  * Do not consider local node a valid source during replace (CASSANDRA-11848)
@@ -844,7 +1158,77 @@
  * Avoid holding SSTableReaders for duration of incremental repair (CASSANDRA-11739)
 
 
-3.0.6
+3.6
+ * Correctly migrate schema for frozen UDTs during 2.x -> 3.x upgrades
+   (does not affect any released versions) (CASSANDRA-11613)
+ * Allow server startup if JMX is configured directly (CASSANDRA-11725)
+ * Prevent direct memory OOM on buffer pool allocations (CASSANDRA-11710)
+ * Enhanced Compaction Logging (CASSANDRA-10805)
+ * Make prepared statement cache size configurable (CASSANDRA-11555)
+ * Integrated JMX authentication and authorization (CASSANDRA-10091)
+ * Add units to stress ouput (CASSANDRA-11352)
+ * Fix PER PARTITION LIMIT for single and multi partitions queries (CASSANDRA-11603)
+ * Add uncompressed chunk cache for RandomAccessReader (CASSANDRA-5863)
+ * Clarify ClusteringPrefix hierarchy (CASSANDRA-11213)
+ * Always perform collision check before joining ring (CASSANDRA-10134)
+ * SSTableWriter output discrepancy (CASSANDRA-11646)
+ * Fix potential timeout in NativeTransportService.testConcurrentDestroys (CASSANDRA-10756)
+ * Support large partitions on the 3.0 sstable format (CASSANDRA-11206,11763)
+ * Add support to rebuild from specific range (CASSANDRA-10406)
+ * Optimize the overlapping lookup by calculating all the
+   bounds in advance (CASSANDRA-11571)
+ * Support json/yaml output in nodetool tablestats (CASSANDRA-5977)
+ * (stress) Add datacenter option to -node options (CASSANDRA-11591)
+ * Fix handling of empty slices (CASSANDRA-11513)
+ * Make number of cores used by cqlsh COPY visible to testing code (CASSANDRA-11437)
+ * Allow filtering on clustering columns for queries without secondary indexes (CASSANDRA-11310)
+ * Refactor Restriction hierarchy (CASSANDRA-11354)
+ * Eliminate allocations in R/W path (CASSANDRA-11421)
+ * Update Netty to 4.0.36 (CASSANDRA-11567)
+ * Fix PER PARTITION LIMIT for queries requiring post-query ordering (CASSANDRA-11556)
+ * Allow instantiation of UDTs and tuples in UDFs (CASSANDRA-10818)
+ * Support UDT in CQLSSTableWriter (CASSANDRA-10624)
+ * Support for non-frozen user-defined types, updating
+   individual fields of user-defined types (CASSANDRA-7423)
+ * Make LZ4 compression level configurable (CASSANDRA-11051)
+ * Allow per-partition LIMIT clause in CQL (CASSANDRA-7017)
+ * Make custom filtering more extensible with UserExpression (CASSANDRA-11295)
+ * Improve field-checking and error reporting in cassandra.yaml (CASSANDRA-10649)
+ * Print CAS stats in nodetool proxyhistograms (CASSANDRA-11507)
+ * More user friendly error when providing an invalid token to nodetool (CASSANDRA-9348)
+ * Add static column support to SASI index (CASSANDRA-11183)
+ * Support EQ/PREFIX queries in SASI CONTAINS mode without tokenization (CASSANDRA-11434)
+ * Support LIKE operator in prepared statements (CASSANDRA-11456)
+ * Add a command to see if a Materialized View has finished building (CASSANDRA-9967)
+ * Log endpoint and port associated with streaming operation (CASSANDRA-8777)
+ * Print sensible units for all log messages (CASSANDRA-9692)
+ * Upgrade Netty to version 4.0.34 (CASSANDRA-11096)
+ * Break the CQL grammar into separate Parser and Lexer (CASSANDRA-11372)
+ * Compress only inter-dc traffic by default (CASSANDRA-8888)
+ * Add metrics to track write amplification (CASSANDRA-11420)
+ * cassandra-stress: cannot handle "value-less" tables (CASSANDRA-7739)
+ * Add/drop multiple columns in one ALTER TABLE statement (CASSANDRA-10411)
+ * Add require_endpoint_verification opt for internode encryption (CASSANDRA-9220)
+ * Add auto import java.util for UDF code block (CASSANDRA-11392)
+ * Add --hex-format option to nodetool getsstables (CASSANDRA-11337)
+ * sstablemetadata should print sstable min/max token (CASSANDRA-7159)
+ * Do not wrap CassandraException in TriggerExecutor (CASSANDRA-9421)
+ * COPY TO should have higher double precision (CASSANDRA-11255)
+ * Stress should exit with non-zero status after failure (CASSANDRA-10340)
+ * Add client to cqlsh SHOW_SESSION (CASSANDRA-8958)
+ * Fix nodetool tablestats keyspace level metrics (CASSANDRA-11226)
+ * Store repair options in parent_repair_history (CASSANDRA-11244)
+ * Print current leveling in sstableofflinerelevel (CASSANDRA-9588)
+ * Change repair message for keyspaces with RF 1 (CASSANDRA-11203)
+ * Remove hard-coded SSL cipher suites and protocols (CASSANDRA-10508)
+ * Improve concurrency in CompactionStrategyManager (CASSANDRA-10099)
+ * (cqlsh) interpret CQL type for formatting blobs (CASSANDRA-11274)
+ * Refuse to start and print txn log information in case of disk
+   corruption (CASSANDRA-10112)
+ * Resolve some eclipse-warnings (CASSANDRA-11086)
+ * (cqlsh) Show static columns in a different color (CASSANDRA-11059)
+ * Allow to remove TTLs on table with default_time_to_live (CASSANDRA-11207)
+Merged from 3.0:
  * Disallow creating view with a static column (CASSANDRA-11602)
  * Reduce the amount of object allocations caused by the getFunctions methods (CASSANDRA-11593)
  * Potential error replaying commitlog with smallint/tinyint/date/time types (CASSANDRA-11618)
@@ -865,8 +1249,6 @@
    header is received (CASSANDRA-11464)
  * Validate that num_tokens and initial_token are consistent with one another (CASSANDRA-10120)
 Merged from 2.2:
- * Fix commit log replay after out-of-order flush completion (CASSANDRA-9669)
- * cqlsh: correctly handle non-ascii chars in error messages (CASSANDRA-11626)
  * Exit JVM if JMX server fails to startup (CASSANDRA-11540)
  * Produce a heap dump when exiting on OOM (CASSANDRA-9861)
  * Restore ability to filter on clustering columns when using a 2i (CASSANDRA-11510)
@@ -894,7 +1276,12 @@
  * Validate levels when building LeveledScanner to avoid overlaps with orphaned sstables (CASSANDRA-9935)
 
 
-3.0.5
+3.5
+ * StaticTokenTreeBuilder should respect posibility of duplicate tokens (CASSANDRA-11525)
+ * Correctly fix potential assertion error during compaction (CASSANDRA-11353)
+ * Avoid index segment stitching in RAM which lead to OOM on big SSTable files (CASSANDRA-11383)
+ * Fix clustering and row filters for LIKE queries on clustering columns (CASSANDRA-11397)
+Merged from 3.0:
  * Fix rare NPE on schema upgrade from 2.x to 3.x (CASSANDRA-10943)
  * Improve backoff policy for cqlsh COPY FROM (CASSANDRA-11320)
  * Improve IF NOT EXISTS check in CREATE INDEX (CASSANDRA-11131)
@@ -902,7 +1289,7 @@
  * Enable SO_REUSEADDR for JMX RMI server sockets (CASSANDRA-11093)
  * Allocate merkletrees with the correct size (CASSANDRA-11390)
  * Support streaming pre-3.0 sstables (CASSANDRA-10990)
- * Add backpressure to compressed commit log (CASSANDRA-10971)
+ * Add backpressure to compressed or encrypted commit log (CASSANDRA-10971)
  * SSTableExport supports secondary index tables (CASSANDRA-11330)
  * Fix sstabledump to include missing info in debug output (CASSANDRA-11321)
  * Establish and implement canonical bulk reading workload(s) (CASSANDRA-10331)
@@ -923,19 +1310,49 @@
  * Fix bloom filter sizing with LCS (CASSANDRA-11344)
  * (cqlsh) Fix error when result is 0 rows with EXPAND ON (CASSANDRA-11092)
  * Add missing newline at end of bin/cqlsh (CASSANDRA-11325)
- * Fix AE in nodetool cfstats (backport CASSANDRA-10859) (CASSANDRA-11297)
  * Unresolved hostname leads to replace being ignored (CASSANDRA-11210)
  * Only log yaml config once, at startup (CASSANDRA-11217)
  * Reference leak with parallel repairs on the same table (CASSANDRA-11215)
 Merged from 2.1:
  * Add a -j parameter to scrub/cleanup/upgradesstables to state how
    many threads to use (CASSANDRA-11179)
- * Backport CASSANDRA-10679 (CASSANDRA-9598)
- * InvalidateKeys should have a weak ref to key cache (CASSANDRA-11176)
  * COPY FROM on large datasets: fix progress report and debug performance (CASSANDRA-11053)
+ * InvalidateKeys should have a weak ref to key cache (CASSANDRA-11176)
 
-3.0.4
- * Preserve order for preferred SSL cipher suites (CASSANDRA-11164)
+
+3.4
+ * (cqlsh) add cqlshrc option to always connect using ssl (CASSANDRA-10458)
+ * Cleanup a few resource warnings (CASSANDRA-11085)
+ * Allow custom tracing implementations (CASSANDRA-10392)
+ * Extract LoaderOptions to be able to be used from outside (CASSANDRA-10637)
+ * fix OnDiskIndexTest to properly treat empty ranges (CASSANDRA-11205)
+ * fix TrackerTest to handle new notifications (CASSANDRA-11178)
+ * add SASI validation for partitioner and complex columns (CASSANDRA-11169)
+ * Add caching of encrypted credentials in PasswordAuthenticator (CASSANDRA-7715)
+ * fix SASI memtable switching on flush (CASSANDRA-11159)
+ * Remove duplicate offline compaction tracking (CASSANDRA-11148)
+ * fix EQ semantics of analyzed SASI indexes (CASSANDRA-11130)
+ * Support long name output for nodetool commands (CASSANDRA-7950)
+ * Encrypted hints (CASSANDRA-11040)
+ * SASI index options validation (CASSANDRA-11136)
+ * Optimize disk seek using min/max column name meta data when the LIMIT clause is used
+   (CASSANDRA-8180)
+ * Add LIKE support to CQL3 (CASSANDRA-11067)
+ * Generic Java UDF types (CASSANDRA-10819)
+ * cqlsh: Include sub-second precision in timestamps by default (CASSANDRA-10428)
+ * Set javac encoding to utf-8 (CASSANDRA-11077)
+ * Integrate SASI index into Cassandra (CASSANDRA-10661)
+ * Add --skip-flush option to nodetool snapshot
+ * Skip values for non-queried columns (CASSANDRA-10657)
+ * Add support for secondary indexes on static columns (CASSANDRA-8103)
+ * CommitLogUpgradeTestMaker creates broken commit logs (CASSANDRA-11051)
+ * Add metric for number of dropped mutations (CASSANDRA-10866)
+ * Simplify row cache invalidation code (CASSANDRA-10396)
+ * Support user-defined compaction through nodetool (CASSANDRA-10660)
+ * Stripe view locks by key and table ID to reduce contention (CASSANDRA-10981)
+ * Add nodetool gettimeout and settimeout commands (CASSANDRA-10953)
+ * Add 3.0 metadata to sstablemetadata output (CASSANDRA-10838)
+Merged from 3.0:
  * MV should only query complex columns included in the view (CASSANDRA-11069)
  * Failed aggregate creation breaks server permanently (CASSANDRA-11064)
  * Add sstabledump tool (CASSANDRA-7464)
@@ -957,6 +1374,7 @@
    properly (CASSANDRA-11050)
  * Fix NPE when using forceRepairRangeAsync without DC (CASSANDRA-11239)
 Merged from 2.2:
+ * Preserve order for preferred SSL cipher suites (CASSANDRA-11164)
  * Range.compareTo() violates the contract of Comparable (CASSANDRA-11216)
  * Avoid NPE when serializing ErrorMessage with null message (CASSANDRA-11167)
  * Replacing an aggregate with a new version doesn't reset INITCOND (CASSANDRA-10840)
@@ -975,6 +1393,7 @@
  * (cqlsh) Support utf-8/cp65001 encoding on Windows (CASSANDRA-11030)
  * Fix paging on DISTINCT queries repeats result when first row in partition changes
    (CASSANDRA-10010)
+ * (cqlsh) Support timezone conversion using pytz (CASSANDRA-10397)
  * cqlsh: change default encoding to UTF-8 (CASSANDRA-11124)
 Merged from 2.1:
  * Checking if an unlogged batch is local is inefficient (CASSANDRA-11529)
@@ -992,11 +1411,14 @@
  * Gossiper#isEnabled is not thread safe (CASSANDRA-11116)
  * Avoid major compaction mixing repaired and unrepaired sstables in DTCS (CASSANDRA-11113)
  * Make it clear what DTCS timestamp_resolution is used for (CASSANDRA-11041)
- * (cqlsh) Support timezone conversion using pytz (CASSANDRA-10397)
  * (cqlsh) Display milliseconds when datetime overflows (CASSANDRA-10625)
 
 
-3.0.3
+3.3
+ * Avoid infinite loop if owned range is smaller than number of
+   data dirs (CASSANDRA-11034)
+ * Avoid bootstrap hanging when existing nodes have no data to stream (CASSANDRA-11010)
+Merged from 3.0:
  * Remove double initialization of newly added tables (CASSANDRA-11027)
  * Filter keys searcher results by target range (CASSANDRA-11104)
  * Fix deserialization of legacy read commands (CASSANDRA-11087)
@@ -1017,24 +1439,17 @@
  * Remove checksum files after replaying hints (CASSANDRA-10947)
  * Support passing base table metadata to custom 2i validation (CASSANDRA-10924)
  * Ensure stale index entries are purged during reads (CASSANDRA-11013)
+ * (cqlsh) Also apply --connect-timeout to control connection
+   timeout (CASSANDRA-10959)
  * Fix AssertionError when removing from list using UPDATE (CASSANDRA-10954)
  * Fix UnsupportedOperationException when reading old sstable with range
    tombstone (CASSANDRA-10743)
  * MV should use the maximum timestamp of the primary key (CASSANDRA-10910)
  * Fix potential assertion error during compaction (CASSANDRA-10944)
- * Fix counting of received sstables in streaming (CASSANDRA-10949)
- * Implement hints compression (CASSANDRA-9428)
- * Fix potential assertion error when reading static columns (CASSANDRA-10903)
- * Avoid NoSuchElementException when executing empty batch (CASSANDRA-10711)
- * Avoid building PartitionUpdate in toString (CASSANDRA-10897)
- * Reduce heap spent when receiving many SSTables (CASSANDRA-10797)
- * Add back support for 3rd party auth providers to bulk loader (CASSANDRA-10873)
- * Eliminate the dependency on jgrapht for UDT resolution (CASSANDRA-10653)
- * (Hadoop) Close Clusters and Sessions in Hadoop Input/Output classes (CASSANDRA-10837)
- * Fix sstableloader not working with upper case keyspace name (CASSANDRA-10806)
 Merged from 2.2:
  * maxPurgeableTimestamp needs to check memtables too (CASSANDRA-9949)
  * Apply change to compaction throughput in real time (CASSANDRA-10025)
+ * (cqlsh) encode input correctly when saving history
  * Fix potential NPE on ORDER BY queries with IN (CASSANDRA-10955)
  * Start L0 STCS-compactions even if there is a L0 -> L1 compaction
    going (CASSANDRA-10979)
@@ -1042,22 +1457,11 @@
  * Avoid NPE when performing sstable tasks (scrub etc.) (CASSANDRA-10980)
  * Make sure client gets tombstone overwhelmed warning (CASSANDRA-9465)
  * Fix error streaming section more than 2GB (CASSANDRA-10961)
- * (cqlsh) Also apply --connect-timeout to control connection
-   timeout (CASSANDRA-10959)
  * Histogram buckets exposed in jmx are sorted incorrectly (CASSANDRA-10975)
  * Enable GC logging by default (CASSANDRA-10140)
  * Optimize pending range computation (CASSANDRA-9258)
  * Skip commit log and saved cache directories in SSTable version startup check (CASSANDRA-10902)
  * drop/alter user should be case sensitive (CASSANDRA-10817)
- * jemalloc detection fails due to quoting issues in regexv (CASSANDRA-10946)
- * (cqlsh) show correct column names for empty result sets (CASSANDRA-9813)
- * Add new types to Stress (CASSANDRA-9556)
- * Add property to allow listening on broadcast interface (CASSANDRA-9748)
- * Fix regression in split size on CqlInputFormat (CASSANDRA-10835)
- * Better handling of SSL connection errors inter-node (CASSANDRA-10816)
- * Disable reloading of GossipingPropertyFileSnitch (CASSANDRA-9474)
- * Verify tables in pseudo-system keyspaces at startup (CASSANDRA-10761)
- * (cqlsh) encode input correctly when saving history
 Merged from 2.1:
  * test_bulk_round_trip_blogposts is failing occasionally (CASSANDRA-10938)
  * Fix isJoined return true only after becoming cluster member (CASANDRA-11007)
@@ -1073,6 +1477,55 @@
  * Retry sending gossip syn multiple times during shadow round (CASSANDRA-8072)
  * Fix pending range calculation during moves (CASSANDRA-10887)
  * Sane default (200Mbps) for inter-DC streaming througput (CASSANDRA-8708)
+
+
+
+3.2
+ * Make sure tokens don't exist in several data directories (CASSANDRA-6696)
+ * Add requireAuthorization method to IAuthorizer (CASSANDRA-10852)
+ * Move static JVM options to conf/jvm.options file (CASSANDRA-10494)
+ * Fix CassandraVersion to accept x.y version string (CASSANDRA-10931)
+ * Add forceUserDefinedCleanup to allow more flexible cleanup (CASSANDRA-10708)
+ * (cqlsh) allow setting TTL with COPY (CASSANDRA-9494)
+ * Fix counting of received sstables in streaming (CASSANDRA-10949)
+ * Implement hints compression (CASSANDRA-9428)
+ * Fix potential assertion error when reading static columns (CASSANDRA-10903)
+ * Fix EstimatedHistogram creation in nodetool tablehistograms (CASSANDRA-10859)
+ * Establish bootstrap stream sessions sequentially (CASSANDRA-6992)
+ * Sort compactionhistory output by timestamp (CASSANDRA-10464)
+ * More efficient BTree removal (CASSANDRA-9991)
+ * Make tablehistograms accept the same syntax as tablestats (CASSANDRA-10149)
+ * Group pending compactions based on table (CASSANDRA-10718)
+ * Add compressor name in sstablemetadata output (CASSANDRA-9879)
+ * Fix type casting for counter columns (CASSANDRA-10824)
+ * Prevent running Cassandra as root (CASSANDRA-8142)
+ * bound maximum in-flight commit log replay mutation bytes to 64 megabytes (CASSANDRA-8639)
+ * Normalize all scripts (CASSANDRA-10679)
+ * Make compression ratio much more accurate (CASSANDRA-10225)
+ * Optimize building of Clustering object when only one is created (CASSANDRA-10409)
+ * Make index building pluggable (CASSANDRA-10681)
+ * Add sstable flush observer (CASSANDRA-10678)
+ * Improve NTS endpoints calculation (CASSANDRA-10200)
+ * Improve performance of the folderSize function (CASSANDRA-10677)
+ * Add support for type casting in selection clause (CASSANDRA-10310)
+ * Added graphing option to cassandra-stress (CASSANDRA-7918)
+ * Abort in-progress queries that time out (CASSANDRA-7392)
+ * Add transparent data encryption core classes (CASSANDRA-9945)
+Merged from 3.0:
+ * Better handling of SSL connection errors inter-node (CASSANDRA-10816)
+ * Avoid NoSuchElementException when executing empty batch (CASSANDRA-10711)
+ * Avoid building PartitionUpdate in toString (CASSANDRA-10897)
+ * Reduce heap spent when receiving many SSTables (CASSANDRA-10797)
+ * Add back support for 3rd party auth providers to bulk loader (CASSANDRA-10873)
+ * Eliminate the dependency on jgrapht for UDT resolution (CASSANDRA-10653)
+ * (Hadoop) Close Clusters and Sessions in Hadoop Input/Output classes (CASSANDRA-10837)
+ * Fix sstableloader not working with upper case keyspace name (CASSANDRA-10806)
+Merged from 2.2:
+ * jemalloc detection fails due to quoting issues in regexv (CASSANDRA-10946)
+ * (cqlsh) show correct column names for empty result sets (CASSANDRA-9813)
+ * Add new types to Stress (CASSANDRA-9556)
+ * Add property to allow listening on broadcast interface (CASSANDRA-9748)
+Merged from 2.1:
  * Match cassandra-loader options in COPY FROM (CASSANDRA-9303)
  * Fix binding to any address in CqlBulkRecordWriter (CASSANDRA-9309)
  * cqlsh fails to decode utf-8 characters for text typed columns (CASSANDRA-10875)
@@ -1085,12 +1538,14 @@
  * Allow cancellation of index summary redistribution (CASSANDRA-8805)
 
 
-3.0.2
- * Fix upgrade data loss due to range tombstone deleting more data than then should
-   (CASSANDRA-10822)
+3.1.1
+Merged from 3.0:
+  * Fix upgrade data loss due to range tombstone deleting more data than then should
+    (CASSANDRA-10822)
 
 
-3.0.1
+3.1
+Merged from 3.0:
  * Avoid MV race during node decommission (CASSANDRA-10674)
  * Disable reloading of GossipingPropertyFileSnitch (CASSANDRA-9474)
  * Handle single-column deletions correction in materialized views
@@ -1490,7 +1945,7 @@
  * (cqlsh) Fix bad check for CQL compatibility when DESCRIBE'ing
    COMPACT STORAGE tables with no clustering columns
  * Eliminate strong self-reference chains in sstable ref tidiers (CASSANDRA-9656)
- * Ensure StreamSession uses canonical sstable reader instances (CASSANDRA-9700) 
+ * Ensure StreamSession uses canonical sstable reader instances (CASSANDRA-9700)
  * Ensure memtable book keeping is not corrupted in the event we shrink usage (CASSANDRA-9681)
  * Update internal python driver for cqlsh (CASSANDRA-9064)
  * Fix IndexOutOfBoundsException when inserting tuple with too many
@@ -1549,7 +2004,7 @@
    error responses to unsupported protocol versions (CASSANDRA-9451)
  * Default commitlog_sync_batch_window_in_ms changed to 2ms (CASSANDRA-9504)
  * Fix empty partition assertion in unsorted sstable writing tools (CASSANDRA-9071)
- * Ensure truncate without snapshot cannot produce corrupt responses (CASSANDRA-9388) 
+ * Ensure truncate without snapshot cannot produce corrupt responses (CASSANDRA-9388)
  * Consistent error message when a table mixes counter and non-counter
    columns (CASSANDRA-9492)
  * Avoid getting unreadable keys during anticompaction (CASSANDRA-9508)
@@ -1601,7 +2056,7 @@
  * Add support for SELECT JSON, INSERT JSON syntax and new toJson(), fromJson()
    functions (CASSANDRA-7970)
  * Optimise max purgeable timestamp calculation in compaction (CASSANDRA-8920)
- * Constrain internode message buffer sizes, and improve IO class hierarchy (CASSANDRA-8670) 
+ * Constrain internode message buffer sizes, and improve IO class hierarchy (CASSANDRA-8670)
  * New tool added to validate all sstables in a node (CASSANDRA-5791)
  * Push notification when tracing completes for an operation (CASSANDRA-7807)
  * Delay "node up" and "node added" notifications until native protocol server is started (CASSANDRA-8236)
@@ -1670,7 +2125,7 @@
  * Allow compilation in java 8 (CASSANDRA-7028)
  * Make incremental repair default (CASSANDRA-7250)
  * Enable code coverage thru JaCoCo (CASSANDRA-7226)
- * Switch external naming of 'column families' to 'tables' (CASSANDRA-4369) 
+ * Switch external naming of 'column families' to 'tables' (CASSANDRA-4369)
  * Shorten SSTable path (CASSANDRA-6962)
  * Use unsafe mutations for most unit tests (CASSANDRA-6969)
  * Fix race condition during calculation of pending ranges (CASSANDRA-7390)
@@ -1805,7 +2260,7 @@
  * Show progress of streaming in nodetool netstats (CASSANDRA-8886)
  * IndexSummaryBuilder utilises offheap memory, and shares data between
    each IndexSummary opened from it (CASSANDRA-8757)
- * markCompacting only succeeds if the exact SSTableReader instances being 
+ * markCompacting only succeeds if the exact SSTableReader instances being
    marked are in the live set (CASSANDRA-8689)
  * cassandra-stress support for varint (CASSANDRA-8882)
  * Fix Adler32 digest for compressed sstables (CASSANDRA-8778)
@@ -1927,7 +2382,7 @@
  * Invalidate affected prepared statements when a table's columns
    are altered (CASSANDRA-7910)
  * Stress - user defined writes should populate sequentally (CASSANDRA-8524)
- * Fix regression in SSTableRewriter causing some rows to become unreadable 
+ * Fix regression in SSTableRewriter causing some rows to become unreadable
    during compaction (CASSANDRA-8429)
  * Run major compactions for repaired/unrepaired in parallel (CASSANDRA-8510)
  * (cqlsh) Fix compression options in DESCRIBE TABLE output when compression
@@ -2194,7 +2649,7 @@
  * Fix possible overflow while sorting CL segments for replay (CASSANDRA-7992)
  * Increase nodetool Xmx (CASSANDRA-7956)
  * Archive any commitlog segments present at startup (CASSANDRA-6904)
- * CrcCheckChance should adjust based on live CFMetadata not 
+ * CrcCheckChance should adjust based on live CFMetadata not
    sstable metadata (CASSANDRA-7978)
  * token() should only accept columns in the partitioning
    key order (CASSANDRA-6075)
@@ -2365,7 +2820,7 @@
 
 2.1.0-rc4
  * Fix word count hadoop example (CASSANDRA-7200)
- * Updated memtable_cleanup_threshold and memtable_flush_writers defaults 
+ * Updated memtable_cleanup_threshold and memtable_flush_writers defaults
    (CASSANDRA-7551)
  * (Windows) fix startup when WMI memory query fails (CASSANDRA-7505)
  * Anti-compaction proceeds if any part of the repair failed (CASSANDRA-7521)
@@ -2418,7 +2873,7 @@
 
 
 2.1.0-rc2
- * Fix heap size calculation for CompoundSparseCellName and 
+ * Fix heap size calculation for CompoundSparseCellName and
    CompoundSparseCellName.WithCollection (CASSANDRA-7421)
  * Allow counter mutations in UNLOGGED batches (CASSANDRA-7351)
  * Modify reconcile logic to always pick a tombstone over a counter cell
@@ -2492,7 +2947,7 @@
  * Warn when 'USING TIMESTAMP' is used on a CAS BATCH (CASSANDRA-7067)
  * return all cpu values from BackgroundActivityMonitor.readAndCompute (CASSANDRA-7183)
  * Correctly delete scheduled range xfers (CASSANDRA-7143)
- * return all cpu values from BackgroundActivityMonitor.readAndCompute (CASSANDRA-7183)  
+ * return all cpu values from BackgroundActivityMonitor.readAndCompute (CASSANDRA-7183)
  * reduce garbage creation in calculatePendingRanges (CASSANDRA-7191)
  * fix c* launch issues on Russian os's due to output of linux 'free' cmd (CASSANDRA-6162)
  * Fix disabling autocompaction (CASSANDRA-7187)
@@ -2502,7 +2957,7 @@
  * Fix IllegalStateException in CqlPagingRecordReader (CASSANDRA-7198)
  * Fix the InvertedIndex trigger example (CASSANDRA-7211)
  * Add --resolve-ip option to 'nodetool ring' (CASSANDRA-7210)
- * reduce garbage on codec flag deserialization (CASSANDRA-7244) 
+ * reduce garbage on codec flag deserialization (CASSANDRA-7244)
  * Fix duplicated error messages on directory creation error at startup (CASSANDRA-5818)
  * Proper null handle for IF with map element access (CASSANDRA-7155)
  * Improve compaction visibility (CASSANDRA-7242)
@@ -2537,10 +2992,10 @@
  * Fix BTree.clear for large updates (CASSANDRA-6943)
  * Fail write instead of logging a warning when unable to append to CL
    (CASSANDRA-6764)
- * Eliminate possibility of CL segment appearing twice in active list 
+ * Eliminate possibility of CL segment appearing twice in active list
    (CASSANDRA-6557)
  * Apply DONTNEED fadvise to commitlog segments (CASSANDRA-6759)
- * Switch CRC component to Adler and include it for compressed sstables 
+ * Switch CRC component to Adler and include it for compressed sstables
    (CASSANDRA-4165)
  * Allow cassandra-stress to set compaction strategy options (CASSANDRA-6451)
  * Add broadcast_rpc_address option to cassandra.yaml (CASSANDRA-5899)
@@ -2637,7 +3092,7 @@
  * Use LOCAL_QUORUM for data reads at LOCAL_SERIAL (CASSANDRA-6939)
  * Log a warning for large batches (CASSANDRA-6487)
  * Put nodes in hibernate when join_ring is false (CASSANDRA-6961)
- * Avoid early loading of non-system keyspaces before compaction-leftovers 
+ * Avoid early loading of non-system keyspaces before compaction-leftovers
    cleanup at startup (CASSANDRA-6913)
  * Restrict Windows to parallel repairs (CASSANDRA-6907)
  * (Hadoop) Allow manually specifying start/end tokens in CFIF (CASSANDRA-6436)
@@ -2720,7 +3175,7 @@
  * add listsnapshots command to nodetool (CASSANDRA-5742)
  * Introduce AtomicBTreeColumns (CASSANDRA-6271, 6692)
  * Multithreaded commitlog (CASSANDRA-3578)
- * allocate fixed index summary memory pool and resample cold index summaries 
+ * allocate fixed index summary memory pool and resample cold index summaries
    to use less memory (CASSANDRA-5519)
  * Removed multithreaded compaction (CASSANDRA-6142)
  * Parallelize fetching rows for low-cardinality indexes (CASSANDRA-1337)
@@ -2822,13 +3277,13 @@
  * Improve batchlog write performance with vnodes (CASSANDRA-6488)
  * cqlsh: quote single quotes in strings inside collections (CASSANDRA-6172)
  * Improve gossip performance for typical messages (CASSANDRA-6409)
- * Throw IRE if a prepared statement has more markers than supported 
+ * Throw IRE if a prepared statement has more markers than supported
    (CASSANDRA-5598)
  * Expose Thread metrics for the native protocol server (CASSANDRA-6234)
- * Change snapshot response message verb to INTERNAL to avoid dropping it 
+ * Change snapshot response message verb to INTERNAL to avoid dropping it
    (CASSANDRA-6415)
  * Warn when collection read has > 65K elements (CASSANDRA-5428)
- * Fix cache persistence when both row and key cache are enabled 
+ * Fix cache persistence when both row and key cache are enabled
    (CASSANDRA-6413)
  * (Hadoop) add describe_local_ring (CASSANDRA-6268)
  * Fix handling of concurrent directory creation failure (CASSANDRA-6459)
@@ -2875,7 +3330,7 @@
 Merged from 1.2:
  * Optimize FD phi calculation (CASSANDRA-6386)
  * Improve initial FD phi estimate when starting up (CASSANDRA-6385)
- * Don't list CQL3 table in CLI describe even if named explicitely 
+ * Don't list CQL3 table in CLI describe even if named explicitely
    (CASSANDRA-5750)
  * Invalidate row cache when dropping CF (CASSANDRA-6351)
  * add non-jamm path for cached statements (CASSANDRA-6293)
@@ -3078,13 +3533,13 @@
  * Allow empty IN relations in SELECT/UPDATE/DELETE statements (CASSANDRA-5626)
  * cqlsh: fix crashing on Windows due to libedit detection (CASSANDRA-5812)
  * fix bulk-loading compressed sstables (CASSANDRA-5820)
- * (Hadoop) fix quoting in CqlPagingRecordReader and CqlRecordWriter 
+ * (Hadoop) fix quoting in CqlPagingRecordReader and CqlRecordWriter
    (CASSANDRA-5824)
  * update default LCS sstable size to 160MB (CASSANDRA-5727)
  * Allow compacting 2Is via nodetool (CASSANDRA-5670)
  * Hex-encode non-String keys in OPP (CASSANDRA-5793)
  * nodetool history logging (CASSANDRA-5823)
- * (Hadoop) fix support for Thrift tables in CqlPagingRecordReader 
+ * (Hadoop) fix support for Thrift tables in CqlPagingRecordReader
    (CASSANDRA-5752)
  * add "all time blocked" to StatusLogger output (CASSANDRA-5825)
  * Future-proof inter-major-version schema migrations (CASSANDRA-5845)
@@ -3160,7 +3615,7 @@
    (CASSANDRA-5511)
  * removed PBSPredictor (CASSANDRA-5455)
  * CAS support (CASSANDRA-5062, 5441, 5442, 5443, 5619, 5667)
- * Leveled compaction performs size-tiered compactions in L0 
+ * Leveled compaction performs size-tiered compactions in L0
    (CASSANDRA-5371, 5439)
  * Add yaml network topology snitch for mixed ec2/other envs (CASSANDRA-5339)
  * Log when a node is down longer than the hint window (CASSANDRA-4554)
@@ -3176,7 +3631,7 @@
  * add memtable_flush_period_in_ms (CASSANDRA-4237)
  * replace supercolumns internally by composites (CASSANDRA-3237, 5123)
  * upgrade thrift to 0.9.0 (CASSANDRA-3719)
- * drop unnecessary keyspace parameter from user-defined compaction API 
+ * drop unnecessary keyspace parameter from user-defined compaction API
    (CASSANDRA-5139)
  * more robust solution to incomplete compactions + counters (CASSANDRA-5151)
  * Change order of directory searching for c*.in.sh (CASSANDRA-3983)
@@ -3237,7 +3692,7 @@
 
 
 1.2.6
- * Fix tracing when operation completes before all responses arrive 
+ * Fix tracing when operation completes before all responses arrive
    (CASSANDRA-5668)
  * Fix cross-DC mutation forwarding (CASSANDRA-5632)
  * Reduce SSTableLoader memory usage (CASSANDRA-5555)
@@ -3255,7 +3710,7 @@
  * Reuse prepared statements in hot auth queries (CASSANDRA-5594)
  * cqlsh: add vertical output option (see EXPAND) (CASSANDRA-5597)
  * Add a rate limit option to stress (CASSANDRA-5004)
- * have BulkLoader ignore snapshots directories (CASSANDRA-5587) 
+ * have BulkLoader ignore snapshots directories (CASSANDRA-5587)
  * fix SnitchProperties logging context (CASSANDRA-5602)
  * Expose whether jna is enabled and memory is locked via JMX (CASSANDRA-5508)
  * cqlsh: fix COPY FROM with ReversedType (CASSANDRA-5610)
@@ -3265,7 +3720,7 @@
  * Correct blob literal + ReversedType parsing (CASSANDRA-5629)
  * Allow GPFS to prefer the internal IP like EC2MRS (CASSANDRA-5630)
  * fix help text for -tspw cassandra-cli (CASSANDRA-5643)
- * don't throw away initial causes exceptions for internode encryption issues 
+ * don't throw away initial causes exceptions for internode encryption issues
    (CASSANDRA-5644)
  * Fix message spelling errors for cql select statements (CASSANDRA-5647)
  * Suppress custom exceptions thru jmx (CASSANDRA-5652)
@@ -3273,7 +3728,7 @@
  * Fix PermissionDetails.equals() method (CASSANDRA-5655)
  * Never allow partition key ranges in CQL3 without token() (CASSANDRA-5666)
  * Gossiper incorrectly drops AppState for an upgrading node (CASSANDRA-5660)
- * Connection thrashing during multi-region ec2 during upgrade, due to 
+ * Connection thrashing during multi-region ec2 during upgrade, due to
    messaging version (CASSANDRA-5669)
  * Avoid over reconnecting in EC2MRS (CASSANDRA-5678)
  * Fix ReadResponseSerializer.serializedSize() for digest reads (CASSANDRA-5476)
@@ -3325,9 +3780,9 @@
 1.2.4
  * Ensure that PerRowSecondaryIndex updates see the most recent values
    (CASSANDRA-5397)
- * avoid duplicate index entries ind PrecompactedRow and 
+ * avoid duplicate index entries ind PrecompactedRow and
    ParallelCompactionIterable (CASSANDRA-5395)
- * remove the index entry on oldColumn when new column is a tombstone 
+ * remove the index entry on oldColumn when new column is a tombstone
    (CASSANDRA-5395)
  * Change default stream throughput from 400 to 200 mbps (CASSANDRA-5036)
  * Gossiper logs DOWN for symmetry with UP (CASSANDRA-5187)
@@ -3339,10 +3794,10 @@
  * Validate that provided CQL3 collection value are < 64K (CASSANDRA-5355)
  * Make upgradeSSTable skip current version sstables by default (CASSANDRA-5366)
  * Optimize min/max timestamp collection (CASSANDRA-5373)
- * Invalid streamId in cql binary protocol when using invalid CL 
+ * Invalid streamId in cql binary protocol when using invalid CL
    (CASSANDRA-5164)
  * Fix validation for IN where clauses with collections (CASSANDRA-5376)
- * Copy resultSet on count query to avoid ConcurrentModificationException 
+ * Copy resultSet on count query to avoid ConcurrentModificationException
    (CASSANDRA-5382)
  * Correctly typecheck in CQL3 even with ReversedType (CASSANDRA-5386)
  * Fix streaming compressed files when using encryption (CASSANDRA-5391)
@@ -3360,7 +3815,7 @@
 Merged from 1.1:
  * cli: Quote ks and cf names in schema output when needed (CASSANDRA-5052)
  * Fix bad default for min/max timestamp in SSTableMetadata (CASSANDRA-5372)
- * Fix cf name extraction from manifest in Directories.migrateFile() 
+ * Fix cf name extraction from manifest in Directories.migrateFile()
    (CASSANDRA-5242)
  * Support pluggable internode authentication (CASSANDRA-5401)
 
@@ -3368,7 +3823,7 @@
 1.2.3
  * add check for sstable overlap within a level on startup (CASSANDRA-5327)
  * replace ipv6 colons in jmx object names (CASSANDRA-5298, 5328)
- * Avoid allocating SSTableBoundedScanner during repair when the range does 
+ * Avoid allocating SSTableBoundedScanner during repair when the range does
    not intersect the sstable (CASSANDRA-5249)
  * Don't lowercase property map keys (this breaks NTS) (CASSANDRA-5292)
  * Fix composite comparator with super columns (CASSANDRA-5287)
@@ -3450,17 +3905,17 @@
  * fix validation compaction of empty rows (CASSANDRA-5136)
  * nodetool methods to enable/disable hint storage/delivery (CASSANDRA-4750)
  * disallow bloom filter false positive chance of 0 (CASSANDRA-5013)
- * add threadpool size adjustment methods to JMXEnabledThreadPoolExecutor and 
+ * add threadpool size adjustment methods to JMXEnabledThreadPoolExecutor and
    CompactionManagerMBean (CASSANDRA-5044)
  * fix hinting for dropped local writes (CASSANDRA-4753)
  * off-heap cache doesn't need mutable column container (CASSANDRA-5057)
- * apply disk_failure_policy to bad disks on initial directory creation 
+ * apply disk_failure_policy to bad disks on initial directory creation
    (CASSANDRA-4847)
  * Optimize name-based queries to use ArrayBackedSortedColumns (CASSANDRA-5043)
  * Fall back to old manifest if most recent is unparseable (CASSANDRA-5041)
  * pool [Compressed]RandomAccessReader objects on the partitioned read path
    (CASSANDRA-4942)
- * Add debug logging to list filenames processed by Directories.migrateFile 
+ * Add debug logging to list filenames processed by Directories.migrateFile
    method (CASSANDRA-4939)
  * Expose black-listed directories via JMX (CASSANDRA-4848)
  * Log compaction merge counts (CASSANDRA-4894)
@@ -3619,13 +4074,13 @@
  * add PBSPredictor consistency modeler (CASSANDRA-4261)
  * remove vestiges of Thrift unframed mode (CASSANDRA-4729)
  * optimize single-row PK lookups (CASSANDRA-4710)
- * adjust blockFor calculation to account for pending ranges due to node 
+ * adjust blockFor calculation to account for pending ranges due to node
    movement (CASSANDRA-833)
  * Change CQL version to 3.0.0 and stop accepting 3.0.0-beta1 (CASSANDRA-4649)
- * (CQL3) Make prepared statement global instead of per connection 
+ * (CQL3) Make prepared statement global instead of per connection
    (CASSANDRA-4449)
  * Fix scrubbing of CQL3 created tables (CASSANDRA-4685)
- * (CQL3) Fix validation when using counter and regular columns in the same 
+ * (CQL3) Fix validation when using counter and regular columns in the same
    table (CASSANDRA-4706)
  * Fix bug starting Cassandra with simple authentication (CASSANDRA-4648)
  * Add support for batchlog in CQL3 (CASSANDRA-4545, 4738)
@@ -3647,7 +4102,7 @@
  * Fix binary protocol NEW_NODE event (CASSANDRA-4679)
  * Fix potential infinite loop in tombstone compaction (CASSANDRA-4781)
  * Remove system tables accounting from schema (CASSANDRA-4850)
- * (cql3) Force provided columns in clustering key order in 
+ * (cql3) Force provided columns in clustering key order in
    'CLUSTERING ORDER BY' (CASSANDRA-4881)
  * Fix composite index bug (CASSANDRA-4884)
  * Fix short read protection for CQL3 (CASSANDRA-4882)
@@ -3757,13 +4212,13 @@
    explicitly provide (CASSANDRA-4700)
  * Improve IAuthority interface by introducing fine-grained
    access permissions and grant/revoke commands (CASSANDRA-4490, 4644)
- * fix assumption error in CLI when updating/describing keyspace 
+ * fix assumption error in CLI when updating/describing keyspace
    (CASSANDRA-4322)
  * Adds offline sstablescrub to debian packaging (CASSANDRA-4642)
  * Automatic fixing of overlapping leveled sstables (CASSANDRA-4644)
  * fix error when using ORDER BY with extended selections (CASSANDRA-4689)
  * (CQL3) Fix validation for IN queries for non-PK cols (CASSANDRA-4709)
- * fix re-created keyspace disappering after 1.1.5 upgrade 
+ * fix re-created keyspace disappering after 1.1.5 upgrade
    (CASSANDRA-4698, 4752)
  * (CLI) display elapsed time in 2 fraction digits (CASSANDRA-3460)
  * add authentication support to sstableloader (CASSANDRA-4712)
@@ -3805,7 +4260,7 @@
 
 1.1.4
  * fix offline scrub to catch >= out of order rows (CASSANDRA-4411)
- * fix cassandra-env.sh on RHEL and other non-dash-based systems 
+ * fix cassandra-env.sh on RHEL and other non-dash-based systems
    (CASSANDRA-4494)
 Merged from 1.0:
  * (Hadoop) fix setting key length for old-style mapred api (CASSANDRA-4534)
@@ -3818,7 +4273,7 @@
  * munmap commitlog segments before rename (CASSANDRA-4337)
  * (JMX) rename getRangeKeySample to sampleKeyRange to avoid returning
    multi-MB results as an attribute (CASSANDRA-4452)
- * flush based on data size, not throughput; overwritten columns no 
+ * flush based on data size, not throughput; overwritten columns no
    longer artificially inflate liveRatio (CASSANDRA-4399)
  * update default commitlog segment size to 32MB and total commitlog
    size to 32/1024 MB for 32/64 bit JVMs, respectively (CASSANDRA-4422)
@@ -3914,7 +4369,7 @@
  * clean up and optimize DataOutputBuffer, used by CQL compression and
    CompositeType (CASSANDRA-4072)
  * optimize commitlog checksumming (CASSANDRA-3610)
- * identify and blacklist corrupted SSTables from future compactions 
+ * identify and blacklist corrupted SSTables from future compactions
    (CASSANDRA-2261)
  * Move CfDef and KsDef validation out of thrift (CASSANDRA-4037)
  * Expose API to repair a user provided range (CASSANDRA-3912)
@@ -3965,7 +4420,7 @@
    (CASSANDRA-3985)
  * synchronize LCS getEstimatedTasks to avoid CME (CASSANDRA-4255)
  * ensure unique streaming session id's (CASSANDRA-4223)
- * kick off background compaction when min/max thresholds change 
+ * kick off background compaction when min/max thresholds change
    (CASSANDRA-4279)
  * improve ability of STCS.getBuckets to deal with 100s of 1000s of
    sstables, such as when convertinb back from LCS (CASSANDRA-4287)
@@ -4005,9 +4460,9 @@
  * fix KEYS index from skipping results (CASSANDRA-3996)
  * Remove sliced_buffer_size_in_kb dead option (CASSANDRA-4076)
  * make loadNewSStable preserve sstable version (CASSANDRA-4077)
- * Respect 1.0 cache settings as much as possible when upgrading 
+ * Respect 1.0 cache settings as much as possible when upgrading
    (CASSANDRA-4088)
- * relax path length requirement for sstable files when upgrading on 
+ * relax path length requirement for sstable files when upgrading on
    non-Windows platforms (CASSANDRA-4110)
  * fix terminination of the stress.java when errors were encountered
    (CASSANDRA-4128)
@@ -4093,7 +4548,7 @@
    + add ALTER COLUMNFAMILY WITH (CASSANDRA-3523)
    + bundle Python dependencies with Cassandra (CASSANDRA-3507)
    + added to Debian package (CASSANDRA-3458)
-   + display byte data instead of erroring out on decode failure 
+   + display byte data instead of erroring out on decode failure
      (CASSANDRA-3874)
  * add nodetool rebuild_index (CASSANDRA-3583)
  * add nodetool rangekeysample (CASSANDRA-2917)
@@ -4110,7 +4565,7 @@
  * multithreaded streaming (CASSANDRA-3494)
  * removed in-tree redhat spec (CASSANDRA-3567)
  * "defragment" rows for name-based queries under STCS, again (CASSANDRA-2503)
- * Recycle commitlog segments for improved performance 
+ * Recycle commitlog segments for improved performance
    (CASSANDRA-3411, 3543, 3557, 3615)
  * update size-tiered compaction to prioritize small tiers (CASSANDRA-2407)
  * add message expiration logic to OutboundTcpConnection (CASSANDRA-3005)
@@ -4179,7 +4634,7 @@
  * fix race between cleanup and flush on secondary index CFSes (CASSANDRA-3712)
  * avoid including non-queried nodes in rangeslice read repair
    (CASSANDRA-3843)
- * Only snapshot CF being compacted for snapshot_before_compaction 
+ * Only snapshot CF being compacted for snapshot_before_compaction
    (CASSANDRA-3803)
  * Log active compactions in StatusLogger (CASSANDRA-3703)
  * Compute more accurate compaction score per level (CASSANDRA-3790)
@@ -4279,9 +4734,9 @@
  * fix default value validation usage in CLI SET command (CASSANDRA-3553)
  * Optimize componentsFor method for compaction and startup time
    (CASSANDRA-3532)
- * (CQL) Proper ColumnFamily metadata validation on CREATE COLUMNFAMILY 
+ * (CQL) Proper ColumnFamily metadata validation on CREATE COLUMNFAMILY
    (CASSANDRA-3565)
- * fix compression "chunk_length_kb" option to set correct kb value for 
+ * fix compression "chunk_length_kb" option to set correct kb value for
    thrift/avro (CASSANDRA-3558)
  * fix missing response during range slice repair (CASSANDRA-3551)
  * 'describe ring' moved from CLI to nodetool and available through JMX (CASSANDRA-3220)
@@ -4297,7 +4752,7 @@
    CL.ONE or CL.LOCAL_QUORUM (CASSANDRA-3577, 3585)
  * detect misuses of CounterColumnType (CASSANDRA-3422)
  * turn off string interning in json2sstable, take 2 (CASSANDRA-2189)
- * validate compression parameters on add/update of the ColumnFamily 
+ * validate compression parameters on add/update of the ColumnFamily
    (CASSANDRA-3573)
  * Check for 0.0.0.0 is incorrect in CFIF (CASSANDRA-3584)
  * Increase vm.max_map_count in debian packaging (CASSANDRA-3563)
@@ -4347,7 +4802,7 @@
  * (CQL) fix for counter decrement syntax (CASSANDRA-3418)
  * Fix race introduced by CASSANDRA-2503 (CASSANDRA-3482)
  * Fix incomplete deletion of delivered hints (CASSANDRA-3466)
- * Avoid rescheduling compactions when no compaction was executed 
+ * Avoid rescheduling compactions when no compaction was executed
    (CASSANDRA-3484)
  * fix handling of the chunk_length_kb compression options (CASSANDRA-3492)
 Merged from 0.8:
@@ -4369,7 +4824,7 @@
  * cleanup usage of StorageService.setMode() (CASSANDRA-3388)
  * Avoid large array allocation for compressed chunk offsets (CASSANDRA-3432)
  * fix DecimalType bytebuffer marshalling (CASSANDRA-3421)
- * fix bug that caused first column in per row indexes to be ignored 
+ * fix bug that caused first column in per row indexes to be ignored
    (CASSANDRA-3441)
  * add JMX call to clean (failed) repair sessions (CASSANDRA-3316)
  * fix sstableloader reference acquisition bug (CASSANDRA-3438)
@@ -4397,11 +4852,11 @@
    in lower case (CASSANDRA-3366)
  * fix Deflate compression when compression actually makes the data bigger
    (CASSANDRA-3370)
- * optimize UUIDGen to avoid lock contention on InetAddress.getLocalHost 
+ * optimize UUIDGen to avoid lock contention on InetAddress.getLocalHost
    (CASSANDRA-3387)
  * tolerate index being dropped mid-mutation (CASSANDRA-3334, 3313)
  * CompactionManager is now responsible for checking for new candidates
-   post-task execution, enabling more consistent leveled compaction 
+   post-task execution, enabling more consistent leveled compaction
    (CASSANDRA-3391)
  * Cache HSHA threads (CASSANDRA-3372)
  * use CF/KS names as snapshot prefix for drop + truncate operations
@@ -4412,7 +4867,7 @@
  * Make reloading the compaction strategy safe (CASSANDRA-3409)
  * ignore 0.8 hints even if compaction begins before we try to purge
    them (CASSANDRA-3385)
- * remove procrun (bin\daemon) from Cassandra source tree and 
+ * remove procrun (bin\daemon) from Cassandra source tree and
    artifacts (CASSANDRA-3331)
  * make cassandra compile under JDK7 (CASSANDRA-3275)
  * remove dependency of clientutil.jar to FBUtilities (CASSANDRA-3299)
@@ -4453,7 +4908,7 @@
  * fix assertionError during repair with ordered partitioners (CASSANDRA-3369)
  * correctly serialize key_validation_class for avro (CASSANDRA-3391)
  * don't expire counter tombstone after streaming (CASSANDRA-3394)
- * prevent nodes that failed to join from hanging around forever 
+ * prevent nodes that failed to join from hanging around forever
    (CASSANDRA-3351)
  * remove incorrect optimization from slice read path (CASSANDRA-3390)
  * Fix race in AntiEntropyService (CASSANDRA-3400)
@@ -4498,7 +4953,7 @@
  * ignore any CF ids sent by client for adding CF/KS (CASSANDRA-3288)
  * remove obsolete hints on first startup (CASSANDRA-3291)
  * use correct ISortedColumns for time-optimized reads (CASSANDRA-3289)
- * Evict gossip state immediately when a token is taken over by a new IP 
+ * Evict gossip state immediately when a token is taken over by a new IP
    (CASSANDRA-3259)
 
 
@@ -4510,7 +4965,7 @@
  * Log message when a full repair operation completes (CASSANDRA-3207)
  * Fix streamOutSession keeping sstables references forever if the remote end
    dies (CASSANDRA-3216)
- * Remove dynamic_snitch boolean from example configuration (defaulting to 
+ * Remove dynamic_snitch boolean from example configuration (defaulting to
    true) and set default badness threshold to 0.1 (CASSANDRA-3229)
  * Base choice of random or "balanced" token on bootstrap on whether
    schema definitions were found (CASSANDRA-3219)
@@ -4522,7 +4977,7 @@
  * Don't allow any cache loading exceptions to halt startup (CASSANDRA-3218)
  * Fix sstableloader --ignores option (CASSANDRA-3247)
  * File descriptor limit increased in packaging (CASSANDRA-3206)
- * Fix deadlock in commit log during flush (CASSANDRA-3253) 
+ * Fix deadlock in commit log during flush (CASSANDRA-3253)
 
 
 1.0.0-beta1
@@ -4530,9 +4985,9 @@
  * add commitlog_total_space_in_mb to prevent fragmented logs (CASSANDRA-2427)
  * removed commitlog_rotation_threshold_in_mb configuration (CASSANDRA-2771)
  * make AbstractBounds.normalize de-overlapp overlapping ranges (CASSANDRA-2641)
- * replace CollatingIterator, ReducingIterator with MergeIterator 
+ * replace CollatingIterator, ReducingIterator with MergeIterator
    (CASSANDRA-2062)
- * Fixed the ability to set compaction strategy in cli using create column 
+ * Fixed the ability to set compaction strategy in cli using create column
    family command (CASSANDRA-2778)
  * clean up tmp files after failed compaction (CASSANDRA-2468)
  * restrict repair streaming to specific columnfamilies (CASSANDRA-2280)
@@ -4544,7 +4999,7 @@
    (CASSANDRA-2521, 3179)
  * store hints as serialized mutations instead of pointers to data row
    (CASSANDRA-2045)
- * store hints in the coordinator node instead of in the closest replica 
+ * store hints in the coordinator node instead of in the closest replica
    (CASSANDRA-2914)
  * add row_cache_keys_to_save CF option (CASSANDRA-1966)
  * check column family validity in nodetool repair (CASSANDRA-2933)
@@ -4660,7 +5115,7 @@
    (CASSANDRA-3129)
  * CustomTThreadPoolServer to log TTransportException at DEBUG level
    (CASSANDRA-3142)
- * allow topology sort to work with non-unique rack names between 
+ * allow topology sort to work with non-unique rack names between
    datacenters (CASSANDRA-3152)
  * Improve caching of same-version Messages on digest and repair paths
    (CASSANDRA-3158)
@@ -4693,7 +5148,7 @@
    on create/update of the ColumnFamily and CQL 'ALTER' statement (CASSANDRA-3036)
  * return an InvalidRequestException if an indexed column is assigned
    a value larger than 64KB (CASSANDRA-3057)
- * fix of numeric-only and string column names handling in CLI "drop index" 
+ * fix of numeric-only and string column names handling in CLI "drop index"
    (CASSANDRA-3054)
  * prune index scan resultset back to original request for lazy
    resultset expansion case (CASSANDRA-2964)
@@ -4701,7 +5156,7 @@
    has not (CASSANDRA-2388)
  * fix dynamic snitch ignoring nodes when read_repair_chance is zero
    (CASSANDRA-2662)
- * avoid retaining references to dropped CFS objects in 
+ * avoid retaining references to dropped CFS objects in
    CompactionManager.estimatedCompactions (CASSANDRA-2708)
  * expose rpc timeouts per host in MessagingServiceMBean (CASSANDRA-2941)
  * avoid including cwd in classpath for deb and rpm packages (CASSANDRA-2881)
@@ -4736,7 +5191,7 @@
 
 
 0.8.4
- * change TokenRing.endpoints to be a list of rpc addresses instead of 
+ * change TokenRing.endpoints to be a list of rpc addresses instead of
    listen/broadcast addresses (CASSANDRA-1777)
  * include files-to-be-streamed in StreamInSession.getSources (CASSANDRA-2972)
  * use JAVA env var in cassandra-env.sh (CASSANDRA-2785, 2992)
@@ -4757,17 +5212,17 @@
  * expose data_dir though jmx (CASSANDRA-2770)
  * don't include tmp files as sstable when create cfs (CASSANDRA-2929)
  * log Java classpath on startup (CASSANDRA-2895)
- * keep gossipped version in sync with actual on migration coordinator 
+ * keep gossipped version in sync with actual on migration coordinator
    (CASSANDRA-2946)
  * use lazy initialization instead of class initialization in NodeId
    (CASSANDRA-2953)
  * check column family validity in nodetool repair (CASSANDRA-2933)
  * speedup bytes to hex conversions dramatically (CASSANDRA-2850)
- * Flush memtables on shutdown when durable writes are disabled 
+ * Flush memtables on shutdown when durable writes are disabled
    (CASSANDRA-2958)
  * improved POSIX compatibility of start scripts (CASsANDRA-2965)
  * add counter support to Hadoop InputFormat (CASSANDRA-2981)
- * fix bug where dirty commitlog segments were removed (and avoid keeping 
+ * fix bug where dirty commitlog segments were removed (and avoid keeping
    segments with no post-flush activity permanently dirty) (CASSANDRA-2829)
  * fix throwing exception with batch mutation of counter super columns
    (CASSANDRA-2949)
@@ -4779,9 +5234,9 @@
  * don't sample the system table when choosing a bootstrap token
    (CASSANDRA-2825)
  * gossiper notifies of local state changes (CASSANDRA-2948)
- * add asynchronous and half-sync/half-async (hsha) thrift servers 
+ * add asynchronous and half-sync/half-async (hsha) thrift servers
    (CASSANDRA-1405)
- * fix potential use of free'd native memory in SerializingCache 
+ * fix potential use of free'd native memory in SerializingCache
    (CASSANDRA-2951)
  * prune index scan resultset back to original request for lazy
    resultset expansion case (CASSANDRA-2964)
@@ -4790,7 +5245,7 @@
 
 
 0.8.2
- * CQL: 
+ * CQL:
    - include only one row per unique key for IN queries (CASSANDRA-2717)
    - respect client timestamp on full row deletions (CASSANDRA-2912)
  * improve thread-safety in StreamOutSession (CASSANDRA-2792)
@@ -4849,7 +5304,7 @@
  * add CompositeType and DynamicCompositeType (CASSANDRA-2231)
  * optimize batches containing multiple updates to the same row
    (CASSANDRA-2583)
- * adjust hinted handoff page size to avoid OOM with large columns 
+ * adjust hinted handoff page size to avoid OOM with large columns
    (CASSANDRA-2652)
  * mark BRAF buffer invalid post-flush so we don't re-flush partial
    buffers again, especially on CL writes (CASSANDRA-2660)
@@ -4931,19 +5386,19 @@
 
 
 0.8.0-rc1
- * faster flushes and compaction from fixing excessively pessimistic 
+ * faster flushes and compaction from fixing excessively pessimistic
    rebuffering in BRAF (CASSANDRA-2581)
  * fix returning null column values in the python cql driver (CASSANDRA-2593)
  * fix merkle tree splitting exiting early (CASSANDRA-2605)
  * snapshot_before_compaction directory name fix (CASSANDRA-2598)
- * Disable compaction throttling during bootstrap (CASSANDRA-2612) 
+ * Disable compaction throttling during bootstrap (CASSANDRA-2612)
  * fix CQL treatment of > and < operators in range slices (CASSANDRA-2592)
  * fix potential double-application of counter updates on commitlog replay
    by moving replay position from header to sstable metadata (CASSANDRA-2419)
  * JDBC CQL driver exposes getColumn for access to timestamp
  * JDBC ResultSetMetadata properties added to AbstractType
  * r/m clustertool (CASSANDRA-2607)
- * add support for presenting row key as a column in CQL result sets 
+ * add support for presenting row key as a column in CQL result sets
    (CASSANDRA-2622)
  * Don't allow {LOCAL|EACH}_QUORUM unless strategy is NTS (CASSANDRA-2627)
  * validate keyspace strategy_options during CQL create (CASSANDRA-2624)
@@ -4965,7 +5420,7 @@
 
 0.8.0-beta2
  * fix NPE compacting index CFs (CASSANDRA-2528)
- * Remove checking all column families on startup for compaction candidates 
+ * Remove checking all column families on startup for compaction candidates
    (CASSANDRA-2444)
  * validate CQL create keyspace options (CASSANDRA-2525)
  * fix nodetool setcompactionthroughput (CASSANDRA-2550)
@@ -4981,21 +5436,21 @@
  * forceUserDefinedCompaction will attempt to compact what it is given
    even if the pessimistic estimate is that there is not enough disk space;
    automatic compactions will only compact 2 or more sstables (CASSANDRA-2575)
- * refuse to apply migrations with older timestamps than the current 
+ * refuse to apply migrations with older timestamps than the current
    schema (CASSANDRA-2536)
  * remove unframed Thrift transport option
  * include indexes in snapshots (CASSANDRA-2596)
  * improve ignoring of obsolete mutations in index maintenance (CASSANDRA-2401)
  * recognize attempt to drop just the index while leaving the column
    definition alone (CASSANDRA-2619)
-  
+
 
 0.8.0-beta1
  * remove Avro RPC support (CASSANDRA-926)
- * support for columns that act as incr/decr counters 
+ * support for columns that act as incr/decr counters
    (CASSANDRA-1072, 1937, 1944, 1936, 2101, 2093, 2288, 2105, 2384, 2236, 2342,
    2454)
- * CQL (CASSANDRA-1703, 1704, 1705, 1706, 1707, 1708, 1710, 1711, 1940, 
+ * CQL (CASSANDRA-1703, 1704, 1705, 1706, 1707, 1708, 1710, 1711, 1940,
    2124, 2302, 2277, 2493)
  * avoid double RowMutation serialization on write path (CASSANDRA-1800)
  * make NetworkTopologyStrategy the default (CASSANDRA-1960)
@@ -5006,7 +5461,7 @@
  * atomic switch of memtables and sstables (CASSANDRA-2284)
  * add pluggable SeedProvider (CASSANDRA-1669)
  * Fix clustertool to not throw exception when calling get_endpoints (CASSANDRA-2437)
- * upgrade to thrift 0.6 (CASSANDRA-2412) 
+ * upgrade to thrift 0.6 (CASSANDRA-2412)
  * repair works on a token range instead of full ring (CASSANDRA-2324)
  * purge tombstones from row cache (CASSANDRA-2305)
  * push replication_factor into strategy_options (CASSANDRA-1263)
@@ -5086,7 +5541,7 @@
  * support LOCAL_QUORUM, EACH_QUORUM CLs outside of NTS (CASSANDRA-2516)
  * preserve version when streaming data from old sstables (CASSANDRA-2283)
  * fix backslash substitutions in CLI (CASSANDRA-2492)
- * count a row deletion as one operation towards memtable threshold 
+ * count a row deletion as one operation towards memtable threshold
    (CASSANDRA-2519)
  * support LOCAL_QUORUM, EACH_QUORUM CLs outside of NTS (CASSANDRA-2516)
 
@@ -5098,7 +5553,7 @@
  * add ability to write to Cassandra from Pig (CASSANDRA-1828)
  * add rpc_[min|max]_threads (CASSANDRA-2176)
  * add CL.TWO, CL.THREE (CASSANDRA-2013)
- * avoid exporting an un-requested row in sstable2json, when exporting 
+ * avoid exporting an un-requested row in sstable2json, when exporting
    a key that does not exist (CASSANDRA-2168)
  * add incremental_backups option (CASSANDRA-1872)
  * add configurable row limit to Pig loadfunc (CASSANDRA-2276)
@@ -5130,12 +5585,12 @@
  * Handle whole-row deletions in CFOutputFormat (CASSANDRA-2014)
  * Make memtable_flush_writers flush in parallel (CASSANDRA-2178)
  * Add compaction_preheat_key_cache option (CASSANDRA-2175)
- * refactor stress.py to have only one copy of the format string 
+ * refactor stress.py to have only one copy of the format string
    used for creating row keys (CASSANDRA-2108)
  * validate index names for \w+ (CASSANDRA-2196)
- * Fix Cassandra cli to respect timeout if schema does not settle 
+ * Fix Cassandra cli to respect timeout if schema does not settle
    (CASSANDRA-2187)
- * fix for compaction and cleanup writing old-format data into new-version 
+ * fix for compaction and cleanup writing old-format data into new-version
    sstable (CASSANDRA-2211, -2216)
  * add nodetool scrub (CASSANDRA-2217, -2240)
  * fix sstable2json large-row pagination (CASSANDRA-2188)
@@ -5148,7 +5603,7 @@
  * fix for reversed slice queries on large rows (CASSANDRA-2212)
  * fat clients were writing local data (CASSANDRA-2223)
  * set DEFAULT_MEMTABLE_LIFETIME_IN_MINS to 24h
- * improve detection and cleanup of partially-written sstables 
+ * improve detection and cleanup of partially-written sstables
    (CASSANDRA-2206)
  * fix supercolumn de/serialization when subcolumn comparator is different
    from supercolumn's (CASSANDRA-2104)
@@ -5184,7 +5639,7 @@
  * distributed test harness (CASSANDRA-1859, 1964)
  * reduce flush lock contention (CASSANDRA-1930)
  * optimize supercolumn deserialization (CASSANDRA-1891)
- * fix CFMetaData.apply to only compare objects of the same class 
+ * fix CFMetaData.apply to only compare objects of the same class
    (CASSANDRA-1962)
  * allow specifying specific SSTables to compact from JMX (CASSANDRA-1963)
  * fix race condition in MessagingService.targets (CASSANDRA-1959, 2094, 2081)
@@ -5218,7 +5673,7 @@
  * fix math in RandomPartitioner.describeOwnership (CASSANDRA-2071)
  * fix deletion of sstable non-data components (CASSANDRA-2059)
  * avoid blocking gossip while deleting handoff hints (CASSANDRA-2073)
- * ignore messages from newer versions, keep track of nodes in gossip 
+ * ignore messages from newer versions, keep track of nodes in gossip
    regardless of version (CASSANDRA-1970)
  * cache writing moved to CompactionManager to reduce i/o contention and
    updated to use non-cache-polluting writes (CASSANDRA-2053)
@@ -5226,9 +5681,9 @@
  * add flush_largest_memtables_at and reduce_cache_sizes_at options
    (CASSANDRA-2142)
  * add cli 'describe cluster' command (CASSANDRA-2127)
- * add cli support for setting username/password at 'connect' command 
+ * add cli support for setting username/password at 'connect' command
    (CASSANDRA-2111)
- * add -D option to Stress.java to allow reading hosts from a file 
+ * add -D option to Stress.java to allow reading hosts from a file
    (CASSANDRA-2149)
  * bound hints CF throughput between 32M and 256M (CASSANDRA-2148)
  * continue starting when invalid saved cache entries are encountered
@@ -5242,14 +5697,14 @@
 
 0.7.0-rc4
  * fix cli crash after backgrounding (CASSANDRA-1875)
- * count timeouts in storageproxy latencies, and include latency 
+ * count timeouts in storageproxy latencies, and include latency
    histograms in StorageProxyMBean (CASSANDRA-1893)
  * fix CLI get recognition of supercolumns (CASSANDRA-1899)
  * enable keepalive on intra-cluster sockets (CASSANDRA-1766)
  * count timeouts towards dynamicsnitch latencies (CASSANDRA-1905)
  * Expose index-building status in JMX + cli schema description
    (CASSANDRA-1871)
- * allow [LOCAL|EACH]_QUORUM to be used with non-NetworkTopology 
+ * allow [LOCAL|EACH]_QUORUM to be used with non-NetworkTopology
    replication Strategies
  * increased amount of index locks for faster commitlog replay
  * collect secondary index tombstones immediately (CASSANDRA-1914)
@@ -5275,7 +5730,7 @@
    column families (CASSANDRA-1835)
  * unregister index MBeans when index is dropped (CASSANDRA-1843)
  * make ByteBufferUtil.clone thread-safe (CASSANDRA-1847)
- * change exception for read requests during bootstrap from 
+ * change exception for read requests during bootstrap from
    InvalidRequest to Unavailable (CASSANDRA-1862)
  * respect row-level tombstones post-flush in range scans
    (CASSANDRA-1837)
@@ -5291,7 +5746,7 @@
 
 
 0.7.0-rc2
- * fix live-column-count of slice ranges including tombstoned supercolumn 
+ * fix live-column-count of slice ranges including tombstoned supercolumn
    with live subcolumn (CASSANDRA-1591)
  * rename o.a.c.internal.AntientropyStage -> AntiEntropyStage,
    o.a.c.request.Request_responseStage -> RequestResponseStage,
@@ -5330,7 +5785,7 @@
 
 0.7.0-rc1
  * fix compaction and flush races with schema updates (CASSANDRA-1715)
- * add clustertool, config-converter, sstablekeys, and schematool 
+ * add clustertool, config-converter, sstablekeys, and schematool
    Windows .bat files (CASSANDRA-1723)
  * reject range queries received during bootstrap (CASSANDRA-1739)
  * fix wrapping-range queries on non-minimum token (CASSANDRA-1700)
@@ -5363,7 +5818,7 @@
  * add friendlier error for UnknownHostException on startup (CASSANDRA-1697)
  * include jna dependency in RPM package (CASSANDRA-1690)
  * add --skip-keys option to stress.py (CASSANDRA-1696)
- * improve cli handling of non-string keys and column names 
+ * improve cli handling of non-string keys and column names
    (CASSANDRA-1701, -1693)
  * r/m extra subcomparator line in cli keyspaces output (CASSANDRA-1712)
  * add read repair chance to cli "show keyspaces"
@@ -5380,7 +5835,7 @@
  * fix service initialization order deadlock (CASSANDRA-1756)
  * multi-line cli commands (CASSANDRA-1742)
  * fix race between snapshot and compaction (CASSANDRA-1736)
- * add listEndpointsPendingHints, deleteHintsForEndpoint JMX methods 
+ * add listEndpointsPendingHints, deleteHintsForEndpoint JMX methods
    (CASSANDRA-1551)
 
 
@@ -5399,7 +5854,7 @@
  * take advantage of AVRO-495 to simplify our avro IDL (CASSANDRA-1436)
  * extend authorization hierarchy to column family (CASSANDRA-1554)
  * deletion support in secondary indexes (CASSANDRA-1571)
- * meaningful error message for invalid replication strategy class 
+ * meaningful error message for invalid replication strategy class
    (CASSANDRA-1566)
  * allow keyspace creation with RF > N (CASSANDRA-1428)
  * improve cli error handling (CASSANDRA-1580)
@@ -5422,10 +5877,10 @@
  * include CF metadata in cli 'show keyspaces' (CASSANDRA-1613)
  * switch from Properties to HashMap in PropertyFileSnitch to
    avoid synchronization bottleneck (CASSANDRA-1481)
- * PropertyFileSnitch configuration file renamed to 
+ * PropertyFileSnitch configuration file renamed to
    cassandra-topology.properties
  * add cli support for get_range_slices (CASSANDRA-1088, CASSANDRA-1619)
- * Make memtable flush thresholds per-CF instead of global 
+ * Make memtable flush thresholds per-CF instead of global
    (CASSANDRA-1007, 1637)
  * add cli support for binary data without CfDef hints (CASSANDRA-1603)
  * fix building SSTable statistics post-stream (CASSANDRA-1620)
@@ -5455,9 +5910,9 @@
  * remove cassandra.yaml dependency from Hadoop and Pig (CASSADRA-1322)
  * expose CfDef metadata in describe_keyspaces (CASSANDRA-1363)
  * restore use of mmap_index_only option (CASSANDRA-1241)
- * dropping a keyspace with no column families generated an error 
+ * dropping a keyspace with no column families generated an error
    (CASSANDRA-1378)
- * rename RackAwareStrategy to OldNetworkTopologyStrategy, RackUnawareStrategy 
+ * rename RackAwareStrategy to OldNetworkTopologyStrategy, RackUnawareStrategy
    to SimpleStrategy, DatacenterShardStrategy to NetworkTopologyStrategy,
    AbstractRackAwareSnitch to AbstractNetworkTopologySnitch (CASSANDRA-1392)
  * merge StorageProxy.mutate, mutateBlocking (CASSANDRA-1396)
@@ -5469,7 +5924,7 @@
  * add support for GT/E, LT/E in subordinate index clauses (CASSANDRA-1401)
  * cfId counter got out of sync when CFs were added (CASSANDRA-1403)
  * less chatty schema updates (CASSANDRA-1389)
- * rename column family mbeans. 'type' will now include either 
+ * rename column family mbeans. 'type' will now include either
    'IndexColumnFamilies' or 'ColumnFamilies' depending on the CFS type.
    (CASSANDRA-1385)
  * disallow invalid keyspace and column family names. This includes name that
@@ -5477,9 +5932,9 @@
  * use JNA, if present, to take snapshots (CASSANDRA-1371)
  * truncate hints if starting 0.7 for the first time (CASSANDRA-1414)
  * fix FD leak in single-row slicepredicate queries (CASSANDRA-1416)
- * allow index expressions against columns that are not part of the 
+ * allow index expressions against columns that are not part of the
    SlicePredicate (CASSANDRA-1410)
- * config-converter properly handles snitches and framed support 
+ * config-converter properly handles snitches and framed support
    (CASSANDRA-1420)
  * remove keyspace argument from multiget_count (CASSANDRA-1422)
  * allow specifying cassandra.yaml location as (local or remote) URL
@@ -5506,12 +5961,12 @@
  * remove failed bootstrap attempt from pending ranges when gossip times
    it out after 1h (CASSANDRA-1463)
  * eager-create tcp connections to other cluster members (CASSANDRA-1465)
- * enumerate stages and derive stage from message type instead of 
+ * enumerate stages and derive stage from message type instead of
    transmitting separately (CASSANDRA-1465)
  * apply reversed flag during collation from different data sources
    (CASSANDRA-1450)
  * make failure to remove commitlog segment non-fatal (CASSANDRA-1348)
- * correct ordering of drain operations so CL.recover is no longer 
+ * correct ordering of drain operations so CL.recover is no longer
    necessary (CASSANDRA-1408)
  * removed keyspace from describe_splits method (CASSANDRA-1425)
  * rename check_schema_agreement to describe_schema_versions
@@ -5530,9 +5985,9 @@
  * clean up of Streaming system (CASSANDRA-1503, 1504, 1506)
  * add options to configure Thrift socket keepalive and buffer sizes (CASSANDRA-1426)
  * make contrib CassandraServiceDataCleaner recursive (CASSANDRA-1509)
- * min, max compaction threshold are configurable and persistent 
+ * min, max compaction threshold are configurable and persistent
    per-ColumnFamily (CASSANDRA-1468)
- * fix replaying the last mutation in a commitlog unnecessarily 
+ * fix replaying the last mutation in a commitlog unnecessarily
    (CASSANDRA-1512)
  * invoke getDefaultUncaughtExceptionHandler from DTPE with the original
    exception rather than the ExecutionException wrapper (CASSANDRA-1226)
@@ -5588,8 +6043,8 @@
  * add truncate thrift method (CASSANDRA-531)
  * http mini-interface using mx4j (CASSANDRA-1068)
  * optimize away copy of sliced row on memtable read path (CASSANDRA-1046)
- * replace constant-size 2GB mmaped segments and special casing for index 
-   entries spanning segment boundaries, with SegmentedFile that computes 
+ * replace constant-size 2GB mmaped segments and special casing for index
+   entries spanning segment boundaries, with SegmentedFile that computes
    segments that always contain entire entries/rows (CASSANDRA-1117)
  * avoid reading large rows into memory during compaction (CASSANDRA-16)
  * added hadoop OutputFormat (CASSANDRA-1101)
@@ -5601,7 +6056,7 @@
  * add joining/leaving status to nodetool ring (CASSANDRA-1115)
  * allow multiple repair sessions per node (CASSANDRA-1190)
  * optimize away MessagingService for local range queries (CASSANDRA-1261)
- * make framed transport the default so malformed requests can't OOM the 
+ * make framed transport the default so malformed requests can't OOM the
    server (CASSANDRA-475)
  * significantly faster reads from row cache (CASSANDRA-1267)
  * take advantage of row cache during range queries (CASSANDRA-1302)
@@ -5611,7 +6066,7 @@
  * page within a single row during hinted handoff (CASSANDRA-1327)
  * push DatacenterShardStrategy configuration into keyspace definition,
    eliminating datacenter.properties. (CASSANDRA-1066)
- * optimize forward slices starting with '' and single-index-block name 
+ * optimize forward slices starting with '' and single-index-block name
    queries by skipping the column index (CASSANDRA-1338)
  * streaming refactor (CASSANDRA-1189)
  * faster comparison for UUID types (CASSANDRA-1043)
@@ -5621,9 +6076,9 @@
 
 0.6.6
  * Allow using DynamicEndpointSnitch with RackAwareStrategy (CASSANDRA-1429)
- * remove the remaining vestiges of the unfinished DatacenterShardStrategy 
+ * remove the remaining vestiges of the unfinished DatacenterShardStrategy
    (replaced by NetworkTopologyStrategy in 0.7)
-   
+
 
 0.6.5
  * fix key ordering in range query results with RandomPartitioner
@@ -5666,7 +6121,7 @@
    (CASSANDRA-1280, CASSANDRA-1047)
  * log thread pool stats when GC is excessive (CASSANDRA-1275)
  * remove gossip message size limit (CASSANDRA-1138)
- * parallelize local and remote reads during multiget, and respect snitch 
+ * parallelize local and remote reads during multiget, and respect snitch
    when determining whether to do local read for CL.ONE (CASSANDRA-1317)
  * fix read repair to use requested consistency level on digest mismatch,
    rather than assuming QUORUM (CASSANDRA-1316)
@@ -5688,7 +6143,7 @@
  * avoid preserving login information after client disconnects
    (CASSANDRA-1057)
  * prefer sun jdk to openjdk in debian init script (CASSANDRA-1174)
- * detect partioner config changes between restarts and fail fast 
+ * detect partioner config changes between restarts and fail fast
    (CASSANDRA-1146)
  * use generation time to resolve node token reassignment disagreements
    (CASSANDRA-1118)
@@ -5713,7 +6168,7 @@
 
 0.6.2
  * fix contrib/word_count build. (CASSANDRA-992)
- * split CommitLogExecutorService into BatchCommitLogExecutorService and 
+ * split CommitLogExecutorService into BatchCommitLogExecutorService and
    PeriodicCommitLogExecutorService (CASSANDRA-1014)
  * add latency histograms to CFSMBean (CASSANDRA-1024)
  * make resolving timestamp ties deterministic by using value bytes
@@ -5728,9 +6183,9 @@
  * install json2sstable, sstable2json, and sstablekeys to Debian package
  * StreamingService.StreamDestinations wouldn't empty itself after streaming
    finished (CASSANDRA-1076)
- * added Collections.shuffle(splits) before returning the splits in 
+ * added Collections.shuffle(splits) before returning the splits in
    ColumnFamilyInputFormat (CASSANDRA-1096)
- * do not recalculate cache capacity post-compaction if it's been manually 
+ * do not recalculate cache capacity post-compaction if it's been manually
    modified (CASSANDRA-1079)
  * better defaults for flush sorter + writer executor queue sizes
    (CASSANDRA-1100)
@@ -5772,12 +6227,12 @@
  * fix merging row versions in range_slice for CL > ONE (CASSANDRA-884)
  * default write ConsistencyLeven chaned from ZERO to ONE
  * fix for index entries spanning mmap buffer boundaries (CASSANDRA-857)
- * use lexical comparison if time part of TimeUUIDs are the same 
+ * use lexical comparison if time part of TimeUUIDs are the same
    (CASSANDRA-907)
  * bound read, mutation, and response stages to fix possible OOM
    during log replay (CASSANDRA-885)
  * Use microseconds-since-epoch (UTC) in cli, instead of milliseconds
- * Treat batch_mutate Deletion with null supercolumn as "apply this predicate 
+ * Treat batch_mutate Deletion with null supercolumn as "apply this predicate
    to top level supercolumns" (CASSANDRA-834)
  * Streaming destination nodes do not update their JMX status (CASSANDRA-916)
  * Fix internal RPC timeout calculation (CASSANDRA-911)
@@ -5792,7 +6247,7 @@
  * add invalidateRowCache to ColumnFamilyStoreMBean (CASSANDRA-761)
  * send Handoff hints to natural locations to reduce load on
    remaining nodes in a failure scenario (CASSANDRA-822)
- * Add RowWarningThresholdInMB configuration option to warn before very 
+ * Add RowWarningThresholdInMB configuration option to warn before very
    large rows get big enough to threaten node stability, and -x option to
    be able to remove them with sstable2json if the warning is unheeded
    until it's too late (CASSANDRA-843)
@@ -5806,7 +6261,7 @@
    are first tried (CASSANDRA-867)
  * fix race condition handling rpc timeout in the coordinator
    (CASSANDRA-864)
- * Remove CalloutLocation and StagingFileDirectory from storage-conf files 
+ * Remove CalloutLocation and StagingFileDirectory from storage-conf files
    since those settings are no longer used (CASSANDRA-878)
  * Parse a long from RowWarningThresholdInMB instead of an int (CASSANDRA-882)
  * Remove obsolete ControlPort code from DatabaseDescriptor (CASSANDRA-886)
@@ -5825,7 +6280,7 @@
  * add optional login() Thrift call for authentication (CASSANDRA-547)
  * support fat clients using gossiper and StorageProxy to perform
    replication in-process [jvm-only] (CASSANDRA-535)
- * support mmapped I/O for reads, on by default on 64bit JVMs 
+ * support mmapped I/O for reads, on by default on 64bit JVMs
    (CASSANDRA-408, CASSANDRA-669)
  * improve insert concurrency, particularly during Hinted Handoff
    (CASSANDRA-658)
@@ -5842,13 +6297,13 @@
  * support get_range_slice for RandomPartitioner (CASSANDRA-745)
  * per-keyspace replication factory and replication strategy (CASSANDRA-620)
  * track latency in microseconds (CASSANDRA-733)
- * add describe_ Thrift methods, deprecating get_string_property and 
+ * add describe_ Thrift methods, deprecating get_string_property and
    get_string_list_property
  * jmx interface for tracking operation mode and streams in general.
    (CASSANDRA-709)
  * keep memtables in sorted order to improve range query performance
    (CASSANDRA-799)
- * use while loop instead of recursion when trimming sstables compaction list 
+ * use while loop instead of recursion when trimming sstables compaction list
    to avoid blowing stack in pathological cases (CASSANDRA-804)
  * basic Hadoop map/reduce support (CASSANDRA-342)
 
@@ -5864,7 +6319,7 @@
  * change streaming chunk size to 32MB to accomodate Windows XP limitations
    (was 64MB) (CASSANDRA-795)
  * fix get_range_slice returning results in the wrong order (CASSANDRA-781)
- 
+
 
 0.5.0 final
  * avoid attempting to delete temporary bootstrap files twice (CASSANDRA-681)
@@ -5875,7 +6330,7 @@
    performance (CASSANDRA-675)
  * wait for table flush before streaming data back to a bootstrapping node.
    (CASSANDRA-696)
- * keep track of bootstrapping sources by table so that bootstrapping doesn't 
+ * keep track of bootstrapping sources by table so that bootstrapping doesn't
    give the indication of finishing early (CASSANDRA-673)
 
 
@@ -5884,15 +6339,15 @@
 
 
 0.5.0 RC2 (unreleased)
- * fix bugs in converting get_range_slice results to Thrift 
+ * fix bugs in converting get_range_slice results to Thrift
    (CASSANDRA-647, CASSANDRA-649)
  * expose java.util.concurrent.TimeoutException in StorageProxy methods
    (CASSANDRA-600)
- * TcpConnectionManager was holding on to disconnected connections, 
+ * TcpConnectionManager was holding on to disconnected connections,
    giving the false indication they were being used. (CASSANDRA-651)
  * Remove duplicated write. (CASSANDRA-662)
  * Abort bootstrap if IP is already in the token ring (CASSANDRA-663)
- * increase default commitlog sync period, and wait for last sync to 
+ * increase default commitlog sync period, and wait for last sync to
    finish before submitting another (CASSANDRA-668)
 
 
@@ -5944,7 +6399,7 @@
 
 
 0.5.0 beta
- * Allow multiple simultaneous flushes, improving flush throughput 
+ * Allow multiple simultaneous flushes, improving flush throughput
    on multicore systems (CASSANDRA-401)
  * Split up locks to improve write and read throughput on multicore systems
    (CASSANDRA-444, CASSANDRA-414)
@@ -5955,7 +6410,7 @@
  * Unless a token is manually specified in the configuration xml,
    a bootstraping node will use a token that gives it half the
    keys from the most-heavily-loaded node in the cluster,
-   instead of generating a random token. 
+   instead of generating a random token.
    (CASSANDRA-385, CASSANDRA-517)
  * Miscellaneous bootstrap fixes (several tickets)
  * Ability to change a node's token even after it has data on it
@@ -5971,15 +6426,15 @@
  * Add key cache to improve read performance (CASSANDRA-423)
  * Simplified construction of custom ReplicationStrategy classes
    (CASSANDRA-497)
- * Graphical application (Swing) for ring integrity verification and 
+ * Graphical application (Swing) for ring integrity verification and
    visualization was added to contrib (CASSANDRA-252)
  * Add DCQUORUM, DCQUORUMSYNC consistency levels and corresponding
    ReplicationStrategy / EndpointSnitch classes.  Experimental.
    (CASSANDRA-492)
  * Web client interface added to contrib (CASSANDRA-457)
- * More-efficient flush for Random, CollatedOPP partitioners 
+ * More-efficient flush for Random, CollatedOPP partitioners
    for normal writes (CASSANDRA-446) and bulk load (CASSANDRA-420)
- * Add MemtableFlushAfterMinutes, a global replacement for the old 
+ * Add MemtableFlushAfterMinutes, a global replacement for the old
    per-CF FlushPeriodInMinutes setting (CASSANDRA-463)
  * optimizations to slice reading (CASSANDRA-350) and supercolumn
    queries (CASSANDRA-510)
@@ -5991,7 +6446,7 @@
  * optimized local-node writes (CASSANDRA-558)
  * added get_range_slice, deprecating get_key_range (CASSANDRA-344)
  * expose TimedOutException to thrift (CASSANDRA-563)
- 
+
 
 0.4.2
  * Add validation disallowing null keys (CASSANDRA-486)
@@ -6046,7 +6501,7 @@
  * change default comparator to BytesType (CASSANDRA-400)
  * add forwards-compatible ConsistencyLevel parameter to get_key_range
    (CASSANDRA-322)
- * r/m special case of blocking for local destination when writing with 
+ * r/m special case of blocking for local destination when writing with
    ConsistencyLevel.ZERO (CASSANDRA-399)
  * Fixes to make BinaryMemtable [bulk load interface] useful (CASSANDRA-337);
    see contrib/bmt_example for an example of using it.
@@ -6086,7 +6541,7 @@
     - column-name-set slice with no names given now returns zero columns
       instead of all of them.  ("all" can run your server out of memory.
       use a range-based slice with a high max column count instead.)
- * Removed the web interface. Node information can now be obtained by 
+ * Removed the web interface. Node information can now be obtained by
    using the newly introduced nodeprobe utility.
  * More JMX stats
  * Remove magic values from internals (e.g. special key to indicate
diff --git a/NEWS.txt b/NEWS.txt
index f0a88ca..1559aa8 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -60,8 +60,8 @@
 'sstableloader' tool. You can upgrade the file format of your snapshots
 using the provided 'sstableupgrade' tool.
 
-3.0.26
-======
+3.11.12
+=======
 
 Upgrading
 ---------
@@ -83,17 +83,17 @@
       to 3.0.26 or higher. In case of a mixed version cluster, different major versions will be taken as a minimum
       required version.
 
-3.0.25
-======
+3.11.11
+=======
 
 ALTER ... DROP COMPACT STORAGE
 ------------------------------
-   - Following a discussion regarding concerns about the safety of the 'ALTER ... DROP COMPACT STORAGE' statement,
+   - Following a discussion regarding concerns about the safety of the 'ALTER ... DROP COMPACT STORAGE' statement, 
      the C* development community does not recommend its use in production and considers it experimental
      (see https://www.mail-archive.com/dev@cassandra.apache.org/msg16789.html).
    - An 'enable_drop_compact_storage' flag has been added to cassandra.yaml to allow operators to prevent its use.
 
-3.0.24
+3.11.10
 ======
 
 Upgrading
@@ -109,16 +109,56 @@
     - In cassandra.yaml, when using vnodes num_tokens must be defined if initial_token is defined.
       If it is not defined, or not equal to the numbers of tokens defined in initial_tokens,
       the node will not start. See CASSANDRA-14477 for details.
+    - SASI's `max_compaction_flush_memory_in_mb` setting was previously getting interpreted in bytes. From 3.11.8
+      it is correctly interpreted in megabytes, but prior to 3.11.10 previous configurations of this setting will
+      lead to nodes OOM during compaction. From 3.11.10 previous configurations will be detected as incorrect,
+      logged, and the setting reverted to the default value of 1GB. It is up to the user to correct the setting
+      after an upgrade, via dropping and recreating the index. See CASSANDRA-16071 for details.
 
-3.0.20
+3.11.9
+======
+Upgrading
+---------
+   - Custom compaction strategies must handle getting sstables added/removed notifications for
+     sstables already added/removed - see CASSANDRA-14103 for details. This has been a requirement
+     for correct operation since 3.11.0 due to an issue in CompactionStrategyManager.
+
+3.11.7
 ======
 
 Upgrading
 ---------
     - Nothing specific to this release, but please see previous upgrading sections,
-      especially if you are upgrading from 2.2.
+      especially if you are upgrading from 3.0.
 
-3.0.19
+
+3.11.6
+======
+
+Upgrading
+---------
+    - Sstables for tables using with a frozen UDT written by C* 3.0 appear as corrupted.
+
+      Background: The serialization-header in the -Statistics.db sstable component contains the type information
+      of the table columns. C* 3.0 write incorrect type information for frozen UDTs by omitting the
+      "frozen" information. Non-frozen UDTs were introduced by CASSANDRA-7423 in C* 3.6. Since then, the missing
+      "frozen" information leads to deserialization issues that result in CorruptSSTableExceptions, potentially other
+      exceptions as well.
+
+      As a mitigation, the sstable serialization-headers are rewritten to contain the missing "frozen" information for
+      UDTs once, when an upgrade from C* 3.0 is detected. This migration does not touch snapshots or backups.
+
+      The sstablescrub tool now performs a check of the sstable serialization-header against the schema. A mismatch of
+      the types in the serialization-header and the schema will cause sstablescrub to error out and stop by default.
+      See the new `-e` option. `-e off` disables the new validation code. `-e fix` or `-e fix-only`, e.g.
+      `sstablescrub -e fix keyspace table`, will validate the serialization-header, rewrite the non-frozen UDTs
+      in the serialzation-header to frozen UDTs, if that matches the schema, and continue with scrub.
+      See `sstablescrub -h`.
+      (CASSANDRA-15035)
+    - repair_session_max_tree_depth setting has been added to cassandra.yaml to allow operators to reduce
+      merkle tree size if repair is creating too much heap pressure. See CASSANDRA-14096 for details.
+
+3.11.5
 ======
 
 Upgrading
@@ -126,12 +166,19 @@
     - repair_session_max_tree_depth setting has been added to cassandra.yaml to allow operators to reduce
       merkle tree size if repair is creating too much heap pressure. See CASSANDRA-14096 for details.
     - native_transport_max_negotiable_protocol_version has been added to cassandra.yaml to allow operators to
-      enforce an upper limit on the version of the native protocol that servers will negotiate with clients.
+      enforce an upper limit on the version of the native protocol that servers will negotiate with clients. 
       This can be used during upgrades from 2.1 to 3.0 to prevent errors due to incompatible paging state formats
       between the two versions. See CASSANDRA-15193 for details.
 
+Experimental features
+---------------------
+    - An 'enable_sasi_indexes' flag, true by default, has been added to cassandra.yaml to allow operators to prevent
+      the creation of new SASI indexes, which are considered experimental and are not recommended for production use.
+      (See https://www.mail-archive.com/dev@cassandra.apache.org/msg13582.html)
+    - The flags 'enable_sasi_indexes' and 'enable_materialized_views' have been grouped under an experimental features
+      section in cassandra.yaml.
 
-3.0.18
+3.11.4
 ======
 
 Upgrading
@@ -141,36 +188,47 @@
       SELECT * queries, and have both simple and collection static columns in those tables, and are upgrading from an
       earlier 3.0 version, then you might be affected by this change. Please see CASSANDRA-14638 for details.
 
-3.0.17
+3.11.3
 =====
 
 Upgrading
 ---------
-    - Materialized view users upgrading from 3.0.15 or later that have performed range movements (join, decommission, move, etc),
-      should run repair on the base tables, and subsequently on the views to ensure data affected by CASSANDRA-14251 is correctly
-      propagated to all replicas.
-    - Changes to bloom_filter_fp_chance will no longer take effect on existing sstables when the node is restarted. Only
-      compactions/upgradesstables regenerates bloom filters and Summaries sstable components. See CASSANDRA-11163
+    - Materialized view users upgrading from 3.0.15 (3.0.X series) or 3.11.1 (3.11.X series) and
+      later that have performed range movements (join, decommission, move, etc), should run repair
+      on the base tables, and subsequently on the views to ensure data affected by CASSANDRA-14251
+      is correctly propagated to all replicas.
+    - Changes to bloom_filter_fp_chance will no longer take effect on existing sstables when the
+      node is restarted. Only compactions/upgradesstables regenerates bloom filters and Summaries
+      sstable components. See CASSANDRA-11163
 
 Deprecation
 -----------
-    - Background read repair has been deprecated. dclocal_read_repair_chance and
-      read_repair_chance table options have been deprecated, and will be removed entirely in 4.0.
-      See CASSANDRA-13910 for details.
+    - Background read repair has been deprecated. dclocal_read_repair_chance and read_repair_chance
+      table options have been deprecated, and will be removed entirely in 4.0. See CASSANDRA-13910
+      for details.
 
-3.0.16
-=====
+3.11.2
+======
 
 Upgrading
 ---------
    - See MAXIMUM TTL EXPIRATION DATE NOTICE above.
-   - Cassandra is now relying on the JVM options to properly shutdown on OutOfMemoryError. By default it will
-     rely on the OnOutOfMemoryError option as the ExitOnOutOfMemoryError and CrashOnOutOfMemoryError options
-     are not supported by the older 1.7 and 1.8 JVMs. A warning will be logged at startup if none of those JVM
-     options are used. See CASSANDRA-13006 for more details.
-   - Cassandra is not logging anymore by default an Heap histogram on OutOfMemoryError. To enable that behavior
-     set the 'cassandra.printHeapHistogramOnOutOfMemoryError' System property to 'true'. See CASSANDRA-13006
-     for more details.
+    - Cassandra is now relying on the JVM options to properly shutdown on OutOfMemoryError. By default it will
+      rely on the OnOutOfMemoryError option as the ExitOnOutOfMemoryError and CrashOnOutOfMemoryError options
+      are not supported by the older 1.7 and 1.8 JVMs. A warning will be logged at startup if none of those JVM
+      options are used. See CASSANDRA-13006 for more details
+    - Cassandra is not logging anymore by default an Heap histogram on OutOfMemoryError. To enable that behavior
+      set the 'cassandra.printHeapHistogramOnOutOfMemoryError' System property to 'true'. See CASSANDRA-13006
+      for more details.
+    - Upgrades from 3.0 might have produced unnecessary schema migrations while
+      there was at least one 3.0 node in the cluster. It is therefore highly
+      recommended to upgrade from 3.0 to at least 3.11.2. The root cause of
+      this schema mismatch was a difference in the way how schema digests were computed
+      in 3.0 and 3.11.2. To mitigate this issue, 3.11.2 and newer announce
+      3.0 compatible digests as long as there is at least one 3.0 node in the
+      cluster. Once all nodes have been upgraded, the "real" schema version will be
+      announced. Note: this fix is only necessary in 3.11.2 and therefore only applies
+      to 3.11. (CASSANDRA-14109)
 
 Materialized Views
 -------------------
@@ -180,15 +238,19 @@
    - An 'enable_materialized_views' flag has been added to cassandra.yaml to allow operators to prevent creation of
      views
 
-3.0.15
-=====
+3.11.1
+======
 
 Upgrading
 ---------
-   - Nothing specific to this release, but please see previous upgrading sections,
-     especially if you are upgrading from 2.2.
+    - Creating Materialized View with filtering on non-primary-key base column
+      (added in CASSANDRA-10368) is disabled, because the liveness of view row
+      is depending on multiple filtered base non-key columns and base non-key
+      column used in view primary-key. This semantic cannot be supported without
+      storage format change, see CASSANDRA-13826. For append-only use case, you
+      may still use this feature with a startup flag: "-Dcassandra.mv.allow_filtering_nonkey_columns_unsafe=true"
 
-Compact Storage
+Compact Storage (only when upgrading from 3.X or any version lower than 3.0.15)
 ---------------
     - Starting version 4.0, Thrift is no longer supported.
       Starting version 5.0, COMPACT STORAGE will no longer be supported.
@@ -206,8 +268,12 @@
       executed. After dropping compact storage, ’NO_COMPACT' option will have no effect
       after that.
 
+
 Materialized Views
 -------------------
+
+Materialized Views (only when upgrading from 3.X or any version lower than 3.0.15)
+---------------------------------------------------------------------------------------
     - Cassandra will no longer allow dropping columns on tables with Materialized Views.
     - A change was made in the way the Materialized View timestamp is computed, which
       may cause an old deletion to a base column which is view primary key (PK) column
@@ -224,7 +290,7 @@
       situations so we advise against doing deletions on base columns not selected in views
       until this is fixed on CASSANDRA-13826.
 
-3.0.14
+3.11.0
 ======
 
 Upgrading
@@ -232,19 +298,19 @@
    - ALTER TABLE (ADD/DROP COLUMN) operations concurrent with a read might
      result into data corruption (see CASSANDRA-13004 for more details).
      Fixing this bug required a messaging protocol version bump. By default,
-     Cassandra 3.0.14 will use 3014 version for messaging.
+     Cassandra 3.11 will use 3014 version for messaging.
 
      Since Schema Migrations rely the on exact messaging protocol version
      match between nodes, if you need schema changes during the upgrade
      process, you have to start your nodes with `-Dcassandra.force_3_0_protocol_version=true`
      first, in order to temporarily force a backwards compatible protocol.
-     After the whole cluster is upgraded to 3.0.14, do a rolling
+     After the whole cluster is upgraded to 3.11, do a rolling
      restart of the cluster without setting that flag.
 
-     3.0.14 nodes with and withouot the flag set will be able to do schema
+     3.11 nodes with and withouot the flag set will be able to do schema
      migrations with other 3.x and 3.0.x releases.
 
-     While running the cluster with the flag set to true on 3.0.14 (in
+     While running the cluster with the flag set to true on 3.11 (in
      compatibility mode), avoid adding or removing any columns to/from
      existing tables.
 
@@ -252,61 +318,151 @@
      time, just start the cluster normally without setting aforementioned
      flag.
 
-   - If performing a rolling upgrade from 3.0.13, there will be a schema mismatch caused
-     by a bug with the schema digest calculation in 3.0.13. This will cause unnecessary
-     but otherwise harmless schema updates, see CASSANDRA-13559 for more details.
-
-3.0.13
-======
-
-Upgrading
----------
+     If you are upgrading from 3.0.14+ (of 3.0.x branch), you do not have
+     to set an flag while upgrading to ensure schema migrations.
    - The NativeAccessMBean isAvailable method will only return true if the
      native library has been successfully linked. Previously it was returning
      true if JNA could be found but was not taking into account link failures.
-
-3.0.12
-======
-
-Upgrading
----------
+   - Primary ranges in the system.size_estimates table are now based on the keyspace
+     replication settings and adjacent ranges are no longer merged (CASSANDRA-9639).
    - In 2.1, the default for otc_coalescing_strategy was 'DISABLED'.
      In 2.2 and 3.0, it was changed to 'TIMEHORIZON', but that value was shown
      to be a performance regression. The default for 3.11.0 and newer has
-     been reverted to 'DISABLED'. Users upgrading to Cassandra 3.0 should
-     consider setting otc_coalescing_strategy to 'DISABLED'.
+     been reverted to 'DISABLED'. Users upgrading from Cassandra 2.2 or 3.0 should
+     be aware that the default has changed.
+   - The StorageHook interface has been modified to allow to retrieve read information from
+     SSTableReader (CASSANDRA-13120).
 
-3.0.11
-======
+3.10
+====
+
+New features
+------------
+   - New `DurationType` (cql duration). See CASSANDRA-11873
+   - Runtime modification of concurrent_compactors is now available via nodetool
+   - Support for the assignment operators +=/-= has been added for update queries.
+   - An Index implementation may now provide a task which runs prior to joining
+     the ring. See CASSANDRA-12039
+   - Filtering on partition key columns is now also supported for queries without
+     secondary indexes.
+   - A slow query log has been added: slow queries will be logged at DEBUG level.
+     For more details refer to CASSANDRA-12403 and slow_query_log_timeout_in_ms
+     in cassandra.yaml.
+   - Support for GROUP BY queries has been added.
+   - A new compaction-stress tool has been added to test the throughput of compaction
+     for any cassandra-stress user schema.  see compaction-stress help for how to use.
+   - Compaction can now take into account overlapping tables that don't take part
+     in the compaction to look for deleted or overwritten data in the compacted tables.
+     Then such data is found, it can be safely discarded, which in turn should enable
+     the removal of tombstones over that data.
+
+     The behavior can be engaged in two ways:
+       - as a "nodetool garbagecollect -g CELL/ROW" operation, which applies
+         single-table compaction on all sstables to discard deleted data in one step.
+       - as a "provide_overlapping_tombstones:CELL/ROW/NONE" compaction strategy flag,
+         which uses overlapping tables as a source of deletions/overwrites during all
+         compactions.
+     The argument specifies the granularity at which deleted data is to be found:
+       - If ROW is specified, only whole deleted rows (or sets of rows) will be
+         discarded.
+       - If CELL is specified, any columns whose value is overwritten or deleted
+         will also be discarded.
+       - NONE (default) specifies the old behavior, overlapping tables are not used to
+         decide when to discard data.
+     Which option to use depends on your workload, both ROW and CELL increase the
+     disk load on compaction (especially with the size-tiered compaction strategy),
+     with CELL being more resource-intensive. Both should lead to better read
+     performance if deleting rows (resp. overwriting or deleting cells) is common.
+   - Prepared statements are now persisted in the table prepared_statements in
+     the system keyspace. Upon startup, this table is used to preload all
+     previously prepared statements - i.e. in many cases clients do not need to
+     re-prepare statements against restarted nodes.
+   - cqlsh can now connect to older Cassandra versions by downgrading the native
+     protocol version. Please note that this is currently not part of our release
+     testing and, as a consequence, it is not guaranteed to work in all cases.
+     See CASSANDRA-12150 for more details.
+   - Snapshots that are automatically taken before a table is dropped or truncated
+     will have a "dropped" or "truncated" prefix on their snapshot tag name.
+   - Metrics are exposed for successful and failed authentication attempts.
+     These can be located using the object names org.apache.cassandra.metrics:type=Client,name=AuthSuccess
+     and org.apache.cassandra.metrics:type=Client,name=AuthFailure respectively.
+   - Add support to "unset" JSON fields in prepared statements by specifying DEFAULT UNSET.
+     See CASSANDRA-11424 for details
+   - Allow TTL with null value on insert and update. It will be treated as equivalent to inserting a 0.
+   - Removed outboundBindAny configuration property. See CASSANDRA-12673 for details.
 
 Upgrading
 ---------
-   - Support for alter types of already defined tables and of UDTs fields has been disabled.
-     If it is necessary to return a different type, please use casting instead. See
-     CASSANDRA-12443 for more details.
-   - Specifying the default_time_to_live option when creating or altering a
-     materialized view was erroneously accepted (and ignored). It is now
-     properly rejected.
-   - Only Java and JavaScript are now supported UDF languages.
-     The sandbox in 3.0 already prevented the use of script languages except Java
-     and JavaScript.
-   - Compaction now correctly drops sstables out of CompactionTask when there
-     isn't enough disk space to perform the full compaction.  This should reduce
-     pending compaction tasks on systems with little remaining disk space.
-   - Primary ranges in the system.size_estimates table are now based on the keyspace
-     replication settings and adjacent ranges are no longer merged (CASSANDRA-9639).
+    - Support for alter types of already defined tables and of UDTs fields has been disabled.
+      If it is necessary to return a different type, please use casting instead. See
+      CASSANDRA-12443 for more details.
+    - Specifying the default_time_to_live option when creating or altering a
+      materialized view was erroneously accepted (and ignored). It is now
+      properly rejected.
+    - Only Java and JavaScript are now supported UDF languages.
+      The sandbox in 3.0 already prevented the use of script languages except Java
+      and JavaScript.
+    - Compaction now correctly drops sstables out of CompactionTask when there
+      isn't enough disk space to perform the full compaction.  This should reduce
+      pending compaction tasks on systems with little remaining disk space.
+    - Request timeouts in cassandra.yaml (read_request_timeout_in_ms, etc) now apply to the
+      "full" request time on the coordinator.  Previously, they only covered the time from
+      when the coordinator sent a message to a replica until the time that the replica
+      responded.  Additionally, the previous behavior was to reset the timeout when performing
+      a read repair, making a second read to fix a short read, and when subranges were read
+      as part of a range scan or secondary index query.  In 3.10 and higher, the timeout
+      is no longer reset for these "subqueries".  The entire request must complete within
+      the specified timeout.  As a consequence, your timeouts may need to be adjusted
+      to account for this.  See CASSANDRA-12256 for more details.
+    - Logs written to stdout are now consistent with logs written to files.
+      Time is now local (it was UTC on the console and local in files). Date, thread, file
+      and line info where added to stdout. (see CASSANDRA-12004)
+    - The 'clientutil' jar, which has been somewhat broken on the 3.x branch, is not longer provided.
+      The features provided by that jar are provided by any good java driver and we advise relying on drivers rather on
+      that jar, but if you need that jar for backward compatiblity until you do so, you should use the version provided
+      on previous Cassandra branch, like the 3.0 branch (by design, the functionality provided by that jar are stable
+      accross versions so using the 3.0 jar for a client connecting to 3.x should work without issues).
+    - (Tools development) DatabaseDescriptor no longer implicitly startups components/services like
+      commit log replay. This may break existing 3rd party tools and clients. In order to startup
+      a standalone tool or client application, use the DatabaseDescriptor.toolInitialization() or
+      DatabaseDescriptor.clientInitialization() methods. Tool initialization sets up partitioner,
+      snitch, encryption context. Client initialization just applies the configuration but does not
+      setup anything. Instead of using Config.setClientMode() or Config.isClientMode(), which are
+      deprecated now, use one of the appropiate new methods in DatabaseDescriptor.
+    - Application layer keep-alives were added to the streaming protocol to prevent idle incoming connections from
+      timing out and failing the stream session (CASSANDRA-11839). This effectively deprecates the streaming_socket_timeout_in_ms
+      property in favor of streaming_keep_alive_period_in_secs. See cassandra.yaml for more details about this property.
+    - Duration litterals support the ISO 8601 format. By consequence, identifiers matching that format
+      (e.g P2Y or P1MT6H) will not be supported anymore (CASSANDRA-11873).
 
-3.0.10
-======
+3.8
+===
 
-Upgrading
----------
-   - memtable_allocation_type: offheap_buffers is no longer allowed to be specified in the 3.0 series.
-     This was an oversight that can cause segfaults. Offheap was re-introduced in 3.4 see CASSANDRA-11039
-     and CASSANDRA-9472 for details.
-
-3.0.9
-=====
+New features
+------------
+   - Shared pool threads are now named according to the stage they are executing
+     tasks for. Thread names mentioned in traced queries change accordingly.
+   - A new option has been added to cassandra-stress "-rate fixed={number}/s"
+     that forces a scheduled rate of operations/sec over time. Using this, stress can
+     accurately account for coordinated ommission from the stress process.
+   - The cassandra-stress "-rate limit=" option has been renamed to "-rate throttle="
+   - hdr histograms have been added to stress runs, it's output can be saved to disk using:
+     "-log hdrfile=" option. This histogram includes response/service/wait times when used with the
+     fixed or throttle rate options.  The histogram file can be plotted on
+     http://hdrhistogram.github.io/HdrHistogram/plotFiles.html
+   - TimeWindowCompactionStrategy has been added. This has proven to be a better approach
+     to time series compaction and new tables should use this instead of DTCS. See
+     CASSANDRA-9666 for details.
+   - Change-Data-Capture is now available. See cassandra.yaml and for cdc-specific flags and
+     a brief explanation of on-disk locations for archived data in CommitLog form. This can
+     be enabled via ALTER TABLE ... WITH cdc=true.
+     Upon flush, CommitLogSegments containing data for CDC-enabled tables are moved to
+     the data/cdc_raw directory until removed by the user and writes to CDC-enabled tables
+     will be rejected with a WriteTimeoutException once cdc_total_space_in_mb is reached
+     between unflushed CommitLogSegments and cdc_raw.
+     NOTE: CDC is disabled by default in the .yaml file. Do not enable CDC on a mixed-version
+     cluster as it will lead to exceptions which can interrupt traffic. Once all nodes
+     have been upgraded to 3.8 it is safe to enable this feature and restart the cluster.
 
 Upgrading
 ---------
@@ -314,17 +470,24 @@
      BYTES type containing empty value. Scrub should be run on the existing
      SSTables containing a descending clustering column of BYTES type to correct
      their ordering. See CASSANDRA-12127 for more details.
-
-3.0.8
-=====
-
-Upgrading
----------
    - Ec2MultiRegionSnitch will no longer automatically set broadcast_rpc_address
      to the public instance IP if this property is defined on cassandra.yaml.
+   - The name "json" and "distinct" are not valid anymore a user-defined function
+     names (they are still valid as column name however). In the unlikely case where
+     you had defined functions with such names, you will need to recreate
+     those under a different name, change your code to use the new names and
+     drop the old versions, and this _before_ upgrade (see CASSANDRA-10783 for more
+     details).
 
-3.0.7
-=====
+Deprecation
+-----------
+   - DateTieredCompactionStrategy has been deprecated - new tables should use
+     TimeWindowCompactionStrategy. Note that migrating an existing DTCS-table to TWCS might
+     cause increased compaction load for a while after the migration so make sure you run
+     tests before migrating. Read CASSANDRA-9666 for background on this.
+
+3.7
+===
 
 Upgrading
 ---------
@@ -334,64 +497,93 @@
      value of native_transport_max_frame_size_in_mb. SSTables will be considered corrupt if
      they contain values whose size exceeds this limit. See CASSANDRA-9530 for more details.
 
+
+3.6
+=====
+
+New features
+------------
+   - JMX connections can now use the same auth mechanisms as CQL clients. New options
+     in cassandra-env.(sh|ps1) enable JMX authentication and authorization to be delegated
+     to the IAuthenticator and IAuthorizer configured in cassandra.yaml. The default settings
+     still only expose JMX locally, and use the JVM's own security mechanisms when remote
+     connections are permitted. For more details on how to enable the new options, see the
+     comments in cassandra-env.sh. A new class of IResource, JMXResource, is provided for
+     the purposes of GRANT/REVOKE via CQL. See CASSANDRA-10091 for more details.
+     Also, directly setting JMX remote port via the com.sun.management.jmxremote.port system
+     property at startup is deprecated. See CASSANDRA-11725 for more details.
+   - JSON timestamps are now in UTC and contain the timezone information, see CASSANDRA-11137 for more details.
+   - Collision checks are performed when joining the token ring, regardless of whether
+     the node should bootstrap. Additionally, replace_address can legitimately be used
+     without bootstrapping to help with recovery of nodes with partially failed disks.
+     See CASSANDRA-10134 for more details.
+   - Key cache will only hold indexed entries up to the size configured by
+     column_index_cache_size_in_kb in cassandra.yaml in memory. Larger indexed entries
+     will never go into memory. See CASSANDRA-11206 for more details.
+   - For tables having a default_time_to_live specifying a TTL of 0 will remove the TTL
+     from the inserted or updated values.
+   - Startup is now aborted if corrupted transaction log files are found. The details
+     of the affected log files are now logged, allowing the operator to decide how
+     to resolve the situation.
+   - Filtering expressions are made more pluggable and can be added programatically via
+     a QueryHandler implementation. See CASSANDRA-11295 for more details.
+
+
+
+3.4
+===
+
+New features
+------------
+    - Internal authentication now supports caching of encrypted credentials.
+      Reference cassandra.yaml:credentials_validity_in_ms
+    - Remote configuration of auth caches via JMX can be disabled using the
+      the system property cassandra.disable_auth_caches_remote_configuration
+    - sstabledump tool is added to be 3.0 version of former sstable2json. The tool only
+      supports v3.0+ SSTables. See tool's help for more detail.
+
+Upgrading
+---------
+    - Nothing specific to 3.4 but please see previous versions upgrading section,
+      especially if you are upgrading from 2.2.
+
 Deprecation
 -----------
-   - DateTieredCompactionStrategy has been deprecated - new tables should use
-     TimeWindowCompactionStrategy. Note that migrating an existing DTCS-table to TWCS might
-     cause increased compaction load for a while after the migration so make sure you run
-     tests before migrating. Read CASSANDRA-9666 for background on this.
+    - The mbean interfaces org.apache.cassandra.auth.PermissionsCacheMBean and
+      org.apache.cassandra.auth.RolesCacheMBean are deprecated in favor of
+      org.apache.cassandra.auth.AuthCacheMBean. This generalized interface is
+      common across all caches in the auth subsystem. The specific mbean interfaces
+      for each individual cache will be removed in a subsequent major version.
+
+
+3.2
+===
 
 New features
 ------------
-   - TimeWindowCompactionStrategy has been added. This has proven to be a better approach
-     to time series compaction and new tables should use this instead of DTCS. See
-     CASSANDRA-9666 for details.
-
-3.0.6
-=====
-
-New features
-------------
-   - JSON timestamps are now in UTC and contain the timezone information, see
-     CASSANDRA-11137 for more details.
-
-3.0.5
-=====
-
-Upgrading
----------
-   - Nothing specific to this release, but please see previous versions upgrading section,
-     especially if you are upgrading from 2.2.
-
-3.0.4
-=====
-
-New features
-------------
-   - sstabledump tool is added to be 3.0 version of former sstable2json. The tool only
-     supports v3.0+ SSTables. See tool's help for more detail.
-
-Upgrading
----------
-   - Nothing specific to this release, but please see previous versions upgrading section,
-     especially if you are upgrading from 2.2.
-
-
-3.0.3
-=====
-
-New features
-------------
+   - We now make sure that a token does not exist in several data directories. This
+     means that we run one compaction strategy per data_file_directory and we use
+     one thread per directory to flush. Use nodetool relocatesstables to make sure your
+     tokens are in the correct place, or just wait and compaction will handle it. See
+     CASSANDRA-6696 for more details.
+   - bound maximum in-flight commit log replay mutation bytes to 64 megabytes
+     tunable via cassandra.commitlog_max_outstanding_replay_bytes
+   - Support for type casting has been added to the selection clause.
    - Hinted handoff now supports compression. Reference cassandra.yaml:hints_compression.
      Note: hints compression is currently disabled by default.
 
 Upgrading
 ---------
-    - Nothing specific to 3.0.3 but please see previous versions upgrading section,
-      especially if you are upgrading from 2.2.
+   - The compression ratio metrics computation has been modified to be more accurate.
+   - Running Cassandra as root is prevented by default.
+   - JVM options are moved from cassandra-env.(sh|ps1) to jvm.options file
+
+Deprecation
+-----------
+   - The Thrift API is deprecated and will be removed in Cassandra 4.0.
 
 
-3.0.1
+3.1
 =====
 
 Upgrading
@@ -617,8 +809,8 @@
      the legacy tables, so clients experience no disruption. Issuing DCL
      statements during an upgrade is not supported.
      Once all nodes are upgraded, an operator with superuser privileges should
-     drop the legacy tables, system_auth.users, system_auth.credentials and 
-     system_auth.permissions. Doing so will prompt Cassandra to switch over to 
+     drop the legacy tables, system_auth.users, system_auth.credentials and
+     system_auth.permissions. Doing so will prompt Cassandra to switch over to
      the new tables without requiring any further intervention.
      While the legacy tables are present a restarted node will re-run the data
      conversion and report the outcome so that operators can verify that it is
@@ -787,8 +979,8 @@
     - cqlsh will now display timestamps with a UTC timezone. Previously,
       timestamps were displayed with the local timezone.
     - Commit log files are no longer recycled by default, due to negative
-      performance implications. This can be enabled again with the 
-      commitlog_segment_recycling option in your cassandra.yaml 
+      performance implications. This can be enabled again with the
+      commitlog_segment_recycling option in your cassandra.yaml
     - JMX methods set/getCompactionStrategyClass have been deprecated, use
       set/getCompactionParameters/set/getCompactionParametersJson instead
 
@@ -917,7 +1109,7 @@
 Upgrading
 ---------
    - commitlog_sync_batch_window_in_ms behavior has changed from the
-     maximum time to wait between fsync to the minimum time.  We are 
+     maximum time to wait between fsync to the minimum time.  We are
      working on making this more user-friendly (see CASSANDRA-9533) but in the
      meantime, this means 2.1 needs a much smaller batch window to keep
      writer threads from starving.  The suggested default is now 2ms.
@@ -2379,4 +2571,3 @@
 compaction deserializes each row before merging.
 
 See https://issues.apache.org/jira/browse/CASSANDRA-16
-
diff --git a/NOTICE.txt b/NOTICE.txt
index d13fda1..f5bc5fd 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -83,3 +83,9 @@
 ASM
 (http://asm.ow2.org/)
 Copyright (c) 2000-2011 INRIA, France Telecom
+
+HdrHistogram
+http://hdrhistogram.org
+
+JCTools
+http://jctools.github.io/JCTools/
diff --git a/README.asc b/README.asc
index 4c76580..6224b42 100644
--- a/README.asc
+++ b/README.asc
@@ -1,7 +1,7 @@
-Executive summary
+Apache Cassandra
 -----------------
 
-Cassandra is a partitioned row store.  Rows are organized into tables with a required primary key.
+Apache Cassandra is a highly-scalable partitioned row store. Rows are organized into tables with a required primary key.
 
 https://cwiki.apache.org/confluence/display/CASSANDRA2/Partitioners[Partitioning] means that Cassandra can distribute your data across multiple machines in an application-transparent matter. Cassandra will automatically repartition as machines are added and removed from the cluster.
 
@@ -18,14 +18,14 @@
 ---------------
 
 This short guide will walk you through getting a basic one node cluster up
-and running, and demonstrate some simple reads and writes.
+and running, and demonstrate some simple reads and writes. For a more-complete guide, please see the Apache Cassandra website's http://cassandra.apache.org/doc/latest/getting_started/[Getting Started Guide].
 
 First, we'll unpack our archive:
 
   $ tar -zxvf apache-cassandra-$VERSION.tar.gz
   $ cd apache-cassandra-$VERSION
 
-After that we start the server.  Running the startup script with the -f argument will cause
+After that we start the server. Running the startup script with the -f argument will cause
 Cassandra to remain in the foreground and log to standard out; it can be stopped with ctrl-C.
 
   $ bin/cassandra -f
@@ -79,14 +79,12 @@
 cluster is operational!
 
 For more on what commands are supported by CQL, see
-https://github.com/apache/cassandra/blob/trunk/doc/cql3/CQL.textile[the CQL reference].  A
+http://cassandra.apache.org/doc/latest/cql/[the CQL reference]. A
 reasonable way to think of it is as, "SQL minus joins and subqueries, plus collections."
 
 Wondering where to go from here?
 
-  * Getting started: http://wiki.apache.org/cassandra/GettingStarted
   * Join us in #cassandra on irc.freenode.net and ask questions
   * Subscribe to the Users mailing list by sending a mail to
     user-subscribe@cassandra.apache.org
-  * Planet Cassandra aggregates Cassandra articles and news:
-    http://planetcassandra.org/
+  * Visit the http://cassandra.apache.org/community/[community section] of the Cassandra website for more information on getting involved.
diff --git a/bin/cassandra b/bin/cassandra
index d5bdc8d..d479366 100755
--- a/bin/cassandra
+++ b/bin/cassandra
@@ -234,7 +234,7 @@
 }
 
 # Parse any command line options.
-args=`getopt vfhp:bD:H:E: "$@"`
+args=`getopt vRfhp:bD:H:E: "$@"`
 eval set -- "$args"
 
 classname="org.apache.cassandra.service.CassandraDaemon"
@@ -257,6 +257,10 @@
             "$JAVA" -cp "$CLASSPATH" "-Dlogback.configurationFile=logback-tools.xml" org.apache.cassandra.tools.GetVersion
             exit 0
         ;;
+        -R)
+            allow_root="yes"
+            shift
+        ;;
         -D)
             properties="$properties -D$2"
             shift 2
@@ -271,20 +275,25 @@
         ;;
         --)
             shift
+            if [ "x$*" != "x" ] ; then
+                echo "Error parsing arguments! Unknown argument \"$*\"" >&2
+                exit 1
+            fi
             break
         ;;
         *)
-            echo "Error parsing arguments!" >&2
+            echo "Error parsing arguments! Unknown argument \"$1\"" >&2
             exit 1
         ;;
     esac
 done
 
-# see CASSANDRA-7254
-"$JAVA" -cp "$CLASSPATH" $JVM_OPTS 2>&1 | grep -q 'Error: Exception thrown by the agent : java.lang.NullPointerException'
-if [ $? -ne "1" ]; then 
-    echo Unable to bind JMX, is Cassandra already running?
-    exit 1;
+if [ "x$allow_root" != "xyes" ] ; then
+    if [ "`id -u`" = "0" ] || [ "`id -g`" = "0" ] ; then
+        echo "Running Cassandra as root user or group is not recommended - please start Cassandra using a different system user."
+        echo "If you really want to force running Cassandra as root, use -R command line option."
+        exit 1
+    fi
 fi
 
 # Start up the service
diff --git a/bin/cassandra.ps1 b/bin/cassandra.ps1
index 5d10994..ee3b566 100644
--- a/bin/cassandra.ps1
+++ b/bin/cassandra.ps1
@@ -109,7 +109,7 @@
 

     if (-Not (Test-Path $PATH_PRUNSRV\prunsrv.exe))

     {

-        Write-Warning "Cannot find $PATH_PRUNSRV\prunsrv.exe.  Please download package from http://www.apache.org/dist/commons/daemon/binaries/windows/ to install as a service."

+        Write-Warning "Cannot find $PATH_PRUNSRV\prunsrv.exe.  Please download package from https://downloads.apache.org/commons/daemon/binaries/windows/ to install as a service."

         Break

     }

 

diff --git a/bin/cqlsh.py b/bin/cqlsh.py
index 03fe0c5..f1b16cd 100644
--- a/bin/cqlsh.py
+++ b/bin/cqlsh.py
@@ -51,6 +51,10 @@
 if sys.version_info[0] != 2 or sys.version_info[1] != 7:
     sys.exit("\nCQL Shell supports only Python 2.7\n")
 
+# see CASSANDRA-10428
+if platform.python_implementation().startswith('Jython'):
+    sys.exit("\nCQL Shell does not run on Jython\n")
+
 UTF8 = 'utf-8'
 CP65001 = 'cp65001'  # Win utf-8 variant
 
@@ -70,7 +74,7 @@
 CQL_LIB_PREFIX = 'cassandra-driver-internal-only-'
 
 CASSANDRA_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')
-CASSANDRA_CQL_HTML_FALLBACK = 'https://cassandra.apache.org/doc/cql3/CQL-3.0.html'
+CASSANDRA_CQL_HTML_FALLBACK = 'https://cassandra.apache.org/doc/cql3/CQL-3.2.html'
 
 # default location of local CQL.html
 if os.path.exists(CASSANDRA_PATH + '/doc/cql3/CQL.html'):
@@ -148,6 +152,7 @@
 
 from cassandra.auth import PlainTextAuthProvider
 from cassandra.cluster import Cluster
+from cassandra.cqltypes import cql_typename
 from cassandra.marshal import int64_unpack
 from cassandra.metadata import (ColumnMetadata, KeyspaceMetadata,
                                 TableMetadata, protect_name, protect_names)
@@ -161,23 +166,24 @@
 if os.path.isdir(cqlshlibdir):
     sys.path.insert(0, cqlshlibdir)
 
-from cqlshlib import cql3handling, cqlhandling, pylexotron, sslhandling
+from cqlshlib import cql3handling, cqlhandling, pylexotron, sslhandling, cqlshhandling
 from cqlshlib.copyutil import ExportTask, ImportTask
 from cqlshlib.displaying import (ANSI_RESET, BLUE, COLUMN_NAME_COLORS, CYAN,
-                                 RED, FormattedValue, colorme)
+                                 RED, WHITE, FormattedValue, colorme)
 from cqlshlib.formatting import (DEFAULT_DATE_FORMAT, DEFAULT_NANOTIME_FORMAT,
-                                 DEFAULT_TIMESTAMP_FORMAT, DateTimeFormat,
-                                 format_by_type, format_value_utype,
-                                 formatter_for)
+                                 DEFAULT_TIMESTAMP_FORMAT, CqlType, DateTimeFormat,
+                                 format_by_type, formatter_for)
 from cqlshlib.tracing import print_trace, print_trace_session
 from cqlshlib.util import get_file_encoding_bomsize, trim_if_present
 
 DEFAULT_HOST = '127.0.0.1'
 DEFAULT_PORT = 9042
+DEFAULT_SSL = False
 DEFAULT_CONNECT_TIMEOUT_SECONDS = 5
 DEFAULT_REQUEST_TIMEOUT_SECONDS = 10
 
 DEFAULT_FLOAT_PRECISION = 5
+DEFAULT_DOUBLE_PRECISION = 5
 DEFAULT_MAX_TRACE_WAIT = 10
 
 if readline is not None and readline.__doc__ is not None and 'libedit' in readline.__doc__:
@@ -274,231 +280,6 @@
 
 debug_completion = bool(os.environ.get('CQLSH_DEBUG_COMPLETION', '') == 'YES')
 
-# we want the cql parser to understand our cqlsh-specific commands too
-my_commands_ending_with_newline = (
-    'help',
-    '?',
-    'consistency',
-    'serial',
-    'describe',
-    'desc',
-    'show',
-    'source',
-    'capture',
-    'login',
-    'debug',
-    'tracing',
-    'expand',
-    'paging',
-    'exit',
-    'quit',
-    'clear',
-    'cls'
-)
-
-
-cqlsh_syntax_completers = []
-
-
-def cqlsh_syntax_completer(rulename, termname):
-    def registrator(f):
-        cqlsh_syntax_completers.append((rulename, termname, f))
-        return f
-    return registrator
-
-
-cqlsh_extra_syntax_rules = r'''
-<cqlshCommand> ::= <CQL_Statement>
-                 | <specialCommand> ( ";" | "\n" )
-                 ;
-
-<specialCommand> ::= <describeCommand>
-                   | <consistencyCommand>
-                   | <serialConsistencyCommand>
-                   | <showCommand>
-                   | <sourceCommand>
-                   | <captureCommand>
-                   | <copyCommand>
-                   | <loginCommand>
-                   | <debugCommand>
-                   | <helpCommand>
-                   | <tracingCommand>
-                   | <expandCommand>
-                   | <exitCommand>
-                   | <pagingCommand>
-                   | <clearCommand>
-                   ;
-
-<describeCommand> ::= ( "DESCRIBE" | "DESC" )
-                                  ( "FUNCTIONS"
-                                  | "FUNCTION" udf=<anyFunctionName>
-                                  | "AGGREGATES"
-                                  | "AGGREGATE" uda=<userAggregateName>
-                                  | "KEYSPACES"
-                                  | "KEYSPACE" ksname=<keyspaceName>?
-                                  | ( "COLUMNFAMILY" | "TABLE" ) cf=<columnFamilyName>
-                                  | "INDEX" idx=<indexName>
-                                  | "MATERIALIZED" "VIEW" mv=<materializedViewName>
-                                  | ( "COLUMNFAMILIES" | "TABLES" )
-                                  | "FULL"? "SCHEMA"
-                                  | "CLUSTER"
-                                  | "TYPES"
-                                  | "TYPE" ut=<userTypeName>
-                                  | (ksname=<keyspaceName> | cf=<columnFamilyName> | idx=<indexName> | mv=<materializedViewName>))
-                    ;
-
-<consistencyCommand> ::= "CONSISTENCY" ( level=<consistencyLevel> )?
-                       ;
-
-<consistencyLevel> ::= "ANY"
-                     | "ONE"
-                     | "TWO"
-                     | "THREE"
-                     | "QUORUM"
-                     | "ALL"
-                     | "LOCAL_QUORUM"
-                     | "EACH_QUORUM"
-                     | "SERIAL"
-                     | "LOCAL_SERIAL"
-                     | "LOCAL_ONE"
-                     ;
-
-<serialConsistencyCommand> ::= "SERIAL" "CONSISTENCY" ( level=<serialConsistencyLevel> )?
-                             ;
-
-<serialConsistencyLevel> ::= "SERIAL"
-                           | "LOCAL_SERIAL"
-                           ;
-
-<showCommand> ::= "SHOW" what=( "VERSION" | "HOST" | "SESSION" sessionid=<uuid> )
-                ;
-
-<sourceCommand> ::= "SOURCE" fname=<stringLiteral>
-                  ;
-
-<captureCommand> ::= "CAPTURE" ( fname=( <stringLiteral> | "OFF" ) )?
-                   ;
-
-<copyCommand> ::= "COPY" cf=<columnFamilyName>
-                         ( "(" [colnames]=<colname> ( "," [colnames]=<colname> )* ")" )?
-                         ( dir="FROM" ( fname=<stringLiteral> | "STDIN" )
-                         | dir="TO"   ( fname=<stringLiteral> | "STDOUT" ) )
-                         ( "WITH" <copyOption> ( "AND" <copyOption> )* )?
-                ;
-
-<copyOption> ::= [optnames]=(<identifier>|<reserved_identifier>) "=" [optvals]=<copyOptionVal>
-               ;
-
-<copyOptionVal> ::= <identifier>
-                  | <reserved_identifier>
-                  | <term>
-                  ;
-
-# avoiding just "DEBUG" so that this rule doesn't get treated as a terminal
-<debugCommand> ::= "DEBUG" "THINGS"?
-                 ;
-
-<helpCommand> ::= ( "HELP" | "?" ) [topic]=( /[a-z_]*/ )*
-                ;
-
-<tracingCommand> ::= "TRACING" ( switch=( "ON" | "OFF" ) )?
-                   ;
-
-<expandCommand> ::= "EXPAND" ( switch=( "ON" | "OFF" ) )?
-                   ;
-
-<pagingCommand> ::= "PAGING" ( switch=( "ON" | "OFF" | /[0-9]+/) )?
-                  ;
-
-<loginCommand> ::= "LOGIN" username=<username> (password=<stringLiteral>)?
-                 ;
-
-<exitCommand> ::= "exit" | "quit"
-                ;
-
-<clearCommand> ::= "CLEAR" | "CLS"
-                 ;
-
-<qmark> ::= "?" ;
-'''
-
-
-@cqlsh_syntax_completer('helpCommand', 'topic')
-def complete_help(ctxt, cqlsh):
-    return sorted([t.upper() for t in cqldocs.get_help_topics() + cqlsh.get_help_topics()])
-
-
-def complete_source_quoted_filename(ctxt, cqlsh):
-    partial_path = ctxt.get_binding('partial', '')
-    head, tail = os.path.split(partial_path)
-    exhead = os.path.expanduser(head)
-    try:
-        contents = os.listdir(exhead or '.')
-    except OSError:
-        return ()
-    matches = filter(lambda f: f.startswith(tail), contents)
-    annotated = []
-    for f in matches:
-        match = os.path.join(head, f)
-        if os.path.isdir(os.path.join(exhead, f)):
-            match += '/'
-        annotated.append(match)
-    return annotated
-
-
-cqlsh_syntax_completer('sourceCommand', 'fname')(complete_source_quoted_filename)
-cqlsh_syntax_completer('captureCommand', 'fname')(complete_source_quoted_filename)
-
-
-@cqlsh_syntax_completer('copyCommand', 'fname')
-def copy_fname_completer(ctxt, cqlsh):
-    lasttype = ctxt.get_binding('*LASTTYPE*')
-    if lasttype == 'unclosedString':
-        return complete_source_quoted_filename(ctxt, cqlsh)
-    partial_path = ctxt.get_binding('partial')
-    if partial_path == '':
-        return ["'"]
-    return ()
-
-
-@cqlsh_syntax_completer('copyCommand', 'colnames')
-def complete_copy_column_names(ctxt, cqlsh):
-    existcols = map(cqlsh.cql_unprotect_name, ctxt.get_binding('colnames', ()))
-    ks = cqlsh.cql_unprotect_name(ctxt.get_binding('ksname', None))
-    cf = cqlsh.cql_unprotect_name(ctxt.get_binding('cfname'))
-    colnames = cqlsh.get_column_names(ks, cf)
-    if len(existcols) == 0:
-        return [colnames[0]]
-    return set(colnames[1:]) - set(existcols)
-
-
-COPY_COMMON_OPTIONS = ['DELIMITER', 'QUOTE', 'ESCAPE', 'HEADER', 'NULL', 'DATETIMEFORMAT',
-                       'MAXATTEMPTS', 'REPORTFREQUENCY', 'DECIMALSEP', 'THOUSANDSSEP', 'BOOLSTYLE',
-                       'NUMPROCESSES', 'CONFIGFILE', 'RATEFILE']
-COPY_FROM_OPTIONS = ['CHUNKSIZE', 'INGESTRATE', 'MAXBATCHSIZE', 'MINBATCHSIZE', 'MAXROWS',
-                     'SKIPROWS', 'SKIPCOLS', 'MAXPARSEERRORS', 'MAXINSERTERRORS', 'ERRFILE', 'PREPAREDSTATEMENTS']
-COPY_TO_OPTIONS = ['ENCODING', 'PAGESIZE', 'PAGETIMEOUT', 'BEGINTOKEN', 'ENDTOKEN', 'MAXOUTPUTSIZE', 'MAXREQUESTS']
-
-
-@cqlsh_syntax_completer('copyOption', 'optnames')
-def complete_copy_options(ctxt, cqlsh):
-    optnames = map(str.upper, ctxt.get_binding('optnames', ()))
-    direction = ctxt.get_binding('dir').upper()
-    if direction == 'FROM':
-        opts = set(COPY_COMMON_OPTIONS + COPY_FROM_OPTIONS) - set(optnames)
-    elif direction == 'TO':
-        opts = set(COPY_COMMON_OPTIONS + COPY_TO_OPTIONS) - set(optnames)
-    return opts
-
-
-@cqlsh_syntax_completer('copyOption', 'optvals')
-def complete_copy_opt_values(ctxt, cqlsh):
-    optnames = ctxt.get_binding('optnames', ())
-    lastopt = optnames[-1].lower()
-    if lastopt == 'header':
-        return ['true', 'false']
-    return [cqlhandling.Hint('<single_character_string>')]
-
 
 class NoKeyspaceError(Exception):
     pass
@@ -574,14 +355,14 @@
     return ver, vertuple
 
 
-def format_value(val, output_encoding, addcolor=False, date_time_format=None,
+def format_value(val, cqltype, encoding, addcolor=False, date_time_format=None,
                  float_precision=None, colormap=None, nullval=None):
     if isinstance(val, DecodeError):
         if addcolor:
             return colorme(repr(val.thebytes), colormap, 'error')
         else:
             return FormattedValue(repr(val.thebytes))
-    return format_by_type(type(val), val, output_encoding, colormap=colormap,
+    return format_by_type(val, cqltype=cqltype, encoding=encoding, colormap=colormap,
                           addcolor=addcolor, nullval=nullval, date_time_format=date_time_format,
                           float_precision=float_precision)
 
@@ -600,30 +381,6 @@
 
 
 def insert_driver_hooks():
-    extend_cql_deserialization()
-    auto_format_udts()
-
-
-def extend_cql_deserialization():
-    """
-    The python driver returns BLOBs as string, but we expect them as bytearrays
-    the implementation of cassandra.cqltypes.BytesType.deserialize.
-
-    The deserializers package exists only when the driver has been compiled with cython extensions and
-    cassandra.deserializers.DesBytesType replaces cassandra.cqltypes.BytesType.deserialize.
-
-    DesBytesTypeByteArray is a fast deserializer that converts blobs into bytearrays but it was
-    only introduced recently (3.1.0). If it is available we use it, otherwise we remove
-    cassandra.deserializers.DesBytesType so that we fall back onto cassandra.cqltypes.BytesType.deserialize
-    just like in the case where no cython extensions are present.
-    """
-    if hasattr(cassandra, 'deserializers'):
-        if hasattr(cassandra.deserializers, 'DesBytesTypeByteArray'):
-            cassandra.deserializers.DesBytesType = cassandra.deserializers.DesBytesTypeByteArray
-        else:
-            del cassandra.deserializers.DesBytesType
-
-    cassandra.cqltypes.BytesType.deserialize = staticmethod(lambda byts, protocol_version: bytearray(byts))
 
     class DateOverFlowWarning(RuntimeWarning):
         pass
@@ -634,7 +391,8 @@
         try:
             return datetime_from_timestamp(timestamp_ms / 1000.0)
         except OverflowError:
-            warnings.warn(DateOverFlowWarning("Some timestamps are larger than Python datetime can represent. Timestamps are displayed in milliseconds from epoch."))
+            warnings.warn(DateOverFlowWarning("Some timestamps are larger than Python datetime can represent. "
+                                              "Timestamps are displayed in milliseconds from epoch."))
             return timestamp_ms
 
     cassandra.cqltypes.DateType.deserialize = staticmethod(deserialize_date_fallback_int)
@@ -646,27 +404,6 @@
     cassandra.cqltypes.CassandraType.support_empty_values = True
 
 
-def auto_format_udts():
-    # when we see a new user defined type, set up the shell formatting for it
-    udt_apply_params = cassandra.cqltypes.UserType.apply_parameters
-
-    def new_apply_params(cls, *args, **kwargs):
-        udt_class = udt_apply_params(*args, **kwargs)
-        formatter_for(udt_class.typename)(format_value_utype)
-        return udt_class
-
-    cassandra.cqltypes.UserType.udt_apply_parameters = classmethod(new_apply_params)
-
-    make_udt_class = cassandra.cqltypes.UserType.make_udt_class
-
-    def new_make_udt_class(cls, *args, **kwargs):
-        udt_class = make_udt_class(*args, **kwargs)
-        formatter_for(udt_class.tuple_type.__name__)(format_value_utype)
-        return udt_class
-
-    cassandra.cqltypes.UserType.make_udt_class = classmethod(new_make_udt_class)
-
-
 class FrozenType(cassandra.cqltypes._ParameterizedType):
     """
     Needed until the bundled python driver adds FrozenType.
@@ -712,6 +449,7 @@
                  display_timestamp_format=DEFAULT_TIMESTAMP_FORMAT,
                  display_date_format=DEFAULT_DATE_FORMAT,
                  display_float_precision=DEFAULT_FLOAT_PRECISION,
+                 display_double_precision=DEFAULT_DOUBLE_PRECISION,
                  display_timezone=None,
                  max_trace_wait=DEFAULT_MAX_TRACE_WAIT,
                  ssl=False,
@@ -739,10 +477,9 @@
             kwargs = {}
             if protocol_version is not None:
                 kwargs['protocol_version'] = protocol_version
-            if cqlver is not None:
-                kwargs['cql_version'] = cqlver
-            self.conn = Cluster(contact_points=(self.hostname,), port=self.port,
-                                auth_provider=self.auth_provider, no_compact=no_compact,
+            self.conn = Cluster(contact_points=(self.hostname,), port=self.port, cql_version=cqlver,
+                                auth_provider=self.auth_provider,
+                                no_compact=no_compact,
                                 ssl_options=sslhandling.ssl_settings(hostname, CONFIG_FILE) if ssl else None,
                                 load_balancing_policy=WhiteListRoundRobinPolicy([self.hostname]),
                                 control_connection_timeout=connect_timeout,
@@ -765,6 +502,7 @@
         self.display_date_format = display_date_format
 
         self.display_float_precision = display_float_precision
+        self.display_double_precision = display_double_precision
 
         self.display_timezone = display_timezone
 
@@ -776,10 +514,6 @@
 
         self.current_keyspace = keyspace
 
-        self.display_timestamp_format = display_timestamp_format
-        self.display_nanotime_format = display_nanotime_format
-        self.display_date_format = display_date_format
-
         self.max_trace_wait = max_trace_wait
         self.session.max_trace_wait = max_trace_wait
 
@@ -837,20 +571,22 @@
     def cqlver_atleast(self, major, minor=0, patch=0):
         return self.cql_ver_tuple[:3] >= (major, minor, patch)
 
-    def myformat_value(self, val, **kwargs):
+    def myformat_value(self, val, cqltype=None, **kwargs):
         if isinstance(val, DecodeError):
             self.decoding_errors.append(val)
         try:
             dtformats = DateTimeFormat(timestamp_format=self.display_timestamp_format,
                                        date_format=self.display_date_format, nanotime_format=self.display_nanotime_format,
                                        timezone=self.display_timezone)
-            return format_value(val, self.output_codec.name,
+            precision = self.display_double_precision if cqltype is not None and cqltype.type_name == 'double' \
+                else self.display_float_precision
+            return format_value(val, cqltype=cqltype, encoding=self.output_codec.name,
                                 addcolor=self.color, date_time_format=dtformats,
-                                float_precision=self.display_float_precision, **kwargs)
+                                float_precision=precision, **kwargs)
         except Exception, e:
             err = FormatError(val, e)
             self.decoding_errors.append(err)
-            return format_value(err, self.output_codec.name, addcolor=self.color)
+            return format_value(err, cqltype=cqltype, encoding=self.output_codec.name, addcolor=self.color)
 
     def myformat_colname(self, name, table_meta=None):
         column_colors = COLUMN_NAME_COLORS.copy()
@@ -860,6 +596,8 @@
                 column_colors.default_factory = lambda: RED
             elif name in [col.name for col in table_meta.clustering_key]:
                 column_colors.default_factory = lambda: CYAN
+            elif name in table_meta.columns and table_meta.columns[name].is_static:
+                column_colors.default_factory = lambda: WHITE
         return self.myformat_value(name, colormap=column_colors)
 
     def report_connection(self):
@@ -936,8 +674,7 @@
         except KeyError:
             raise UserTypeNotFound("User type %r not found" % typename)
 
-        return [(field_name, field_type.cql_parameterized_type())
-                for field_name, field_type in zip(user_type.field_names, user_type.field_types)]
+        return zip(user_type.field_names, user_type.field_types)
 
     def get_userfunction_names(self, ksname=None):
         if ksname is None:
@@ -1341,7 +1078,7 @@
         elif result:
             # CAS INSERT/UPDATE
             self.writeresult("")
-            self.print_static_result(result.column_names, list(result), self.parse_for_update_meta(statement.query_string))
+            self.print_static_result(result, self.parse_for_update_meta(statement.query_string), with_header=True, tty=self.tty)
         self.flush_output()
         return True, future
 
@@ -1349,22 +1086,30 @@
         self.decoding_errors = []
 
         self.writeresult("")
-        if result.has_more_pages and self.tty:
+
+        def print_all(result, table_meta, tty):
+            # Return the number of rows in total
             num_rows = 0
+            isFirst = True
             while True:
-                page = result.current_rows
-                if page:
-                    num_rows += len(page)
-                    self.print_static_result(result.column_names, page, table_meta)
+                # Always print for the first page even it is empty
+                if result.current_rows or isFirst:
+                    num_rows += len(result.current_rows)
+                    with_header = isFirst or tty
+                    self.print_static_result(result, table_meta, with_header, tty)
                 if result.has_more_pages:
-                    raw_input("---MORE---")
+                    if self.shunted_query_out is None and tty:
+                        # Only pause when not capturing.
+                        raw_input("---MORE---")
                     result.fetch_next_page()
                 else:
+                    if not tty:
+                        self.writeresult("")
                     break
-        else:
-            rows = list(result)
-            num_rows = len(rows)
-            self.print_static_result(result.column_names, rows, table_meta)
+                isFirst = False
+            return num_rows
+
+        num_rows = print_all(result, table_meta, self.tty)
         self.writeresult("(%d rows)" % num_rows)
 
         if self.decoding_errors:
@@ -1374,24 +1119,31 @@
                 self.writeresult('%d more decoding errors suppressed.'
                                  % (len(self.decoding_errors) - 2), color=RED)
 
-    def print_static_result(self, column_names, rows, table_meta):
-        if not column_names and not table_meta:
+    def print_static_result(self, result, table_meta, with_header, tty):
+        if not result.column_names and not table_meta:
             return
 
-        column_names = column_names or table_meta.columns.keys()
+        column_names = result.column_names or table_meta.columns.keys()
         formatted_names = [self.myformat_colname(name, table_meta) for name in column_names]
-        if not rows:
+        if not result.current_rows:
             # print header only
-            self.print_formatted_result(formatted_names, None)
+            self.print_formatted_result(formatted_names, None, with_header=True, tty=tty)
             return
-        formatted_values = [map(self.myformat_value, [row[column] for column in column_names]) for row in rows]
+
+        cql_types = []
+        if result.column_types:
+            ks_name = table_meta.keyspace_name if table_meta else self.current_keyspace
+            ks_meta = self.conn.metadata.keyspaces.get(ks_name, None)
+            cql_types = [CqlType(cql_typename(t), ks_meta) for t in result.column_types]
+
+        formatted_values = [map(self.myformat_value, [row[column] for column in column_names], cql_types) for row in result.current_rows]
 
         if self.expand_enabled:
             self.print_formatted_result_vertically(formatted_names, formatted_values)
         else:
-            self.print_formatted_result(formatted_names, formatted_values)
+            self.print_formatted_result(formatted_names, formatted_values, with_header, tty)
 
-    def print_formatted_result(self, formatted_names, formatted_values):
+    def print_formatted_result(self, formatted_names, formatted_values, with_header, tty):
         # determine column widths
         widths = [n.displaywidth for n in formatted_names]
         if formatted_values is not None:
@@ -1400,9 +1152,10 @@
                     widths[num] = max(widths[num], col.displaywidth)
 
         # print header
-        header = ' | '.join(hdr.ljust(w, color=self.color) for (hdr, w) in zip(formatted_names, widths))
-        self.writeresult(' ' + header.rstrip())
-        self.writeresult('-%s-' % '-+-'.join('-' * w for w in widths))
+        if with_header:
+            header = ' | '.join(hdr.ljust(w, color=self.color) for (hdr, w) in zip(formatted_names, widths))
+            self.writeresult(' ' + header.rstrip())
+            self.writeresult('-%s-' % '-+-'.join('-' * w for w in widths))
 
         # stop if there are no rows
         if formatted_values is None:
@@ -1414,7 +1167,8 @@
             line = ' | '.join(col.rjust(w, color=self.color) for (col, w) in zip(row, widths))
             self.writeresult(' ' + line)
 
-        self.writeresult("")
+        if tty:
+            self.writeresult("")
 
     def print_formatted_result_vertically(self, formatted_names, formatted_values):
         max_col_width = max([n.displaywidth for n in formatted_names])
@@ -1657,7 +1411,6 @@
         except KeyError:
             raise UserTypeNotFound("User type %r not found" % typename)
         print usertype.export_as_string()
-        print
 
     def _columnize_unicode(self, name_list, quote=False):
         """
@@ -1903,6 +1656,7 @@
                                     have to compile every batch statement. For large and oversized clusters
                                     this will result in a faster import but for smaller clusters it may generate
                                     timeouts.
+          TTL=3600                - the time to live in seconds, by default data will not expire
 
         Available COPY TO options and defaults:
 
@@ -1915,6 +1669,8 @@
           MAXOUTPUTSIZE='-1'       - the maximum size of the output file measured in number of lines,
                                      beyond this maximum the output file will be split into segments,
                                      -1 means unlimited.
+          FLOATPRECISION=5         - the number of digits displayed after the decimal point for cql float values
+          DOUBLEPRECISION=12       - the number of digits displayed after the decimal point for cql double values
 
         When entering CSV data on STDIN, you can use the sequence "\."
         on a line by itself to end the data input.
@@ -2024,6 +1780,7 @@
                          display_timestamp_format=self.display_timestamp_format,
                          display_date_format=self.display_date_format,
                          display_float_precision=self.display_float_precision,
+                         display_double_precision=self.display_double_precision,
                          display_timezone=self.display_timezone,
                          max_trace_wait=self.max_trace_wait, ssl=self.ssl,
                          request_timeout=self.session.default_timeout,
@@ -2493,6 +2250,8 @@
                                                     DEFAULT_DATE_FORMAT)
     optvalues.float_precision = option_with_default(configs.getint, 'ui', 'float_precision',
                                                     DEFAULT_FLOAT_PRECISION)
+    optvalues.double_precision = option_with_default(configs.getint, 'ui', 'double_precision',
+                                                     DEFAULT_DOUBLE_PRECISION)
     optvalues.field_size_limit = option_with_default(configs.getint, 'csv', 'field_size_limit', csv.field_size_limit())
     optvalues.max_trace_wait = option_with_default(configs.getfloat, 'tracing', 'max_trace_wait',
                                                    DEFAULT_MAX_TRACE_WAIT)
@@ -2500,13 +2259,13 @@
 
     optvalues.debug = False
     optvalues.file = None
-    optvalues.ssl = False
+    optvalues.ssl = option_with_default(configs.getboolean, 'connection', 'ssl', DEFAULT_SSL)
     optvalues.no_compact = False
     optvalues.encoding = option_with_default(configs.get, 'ui', 'encoding', UTF8)
 
     optvalues.tty = option_with_default(configs.getboolean, 'ui', 'tty', sys.stdin.isatty())
-    optvalues.cqlversion = option_with_default(configs.get, 'cql', 'version', None)
     optvalues.protocol_version = option_with_default(configs.getint, 'protocol', 'version', None)
+    optvalues.cqlversion = option_with_default(configs.get, 'cql', 'version', None)
     optvalues.connect_timeout = option_with_default(configs.getint, 'connection', 'timeout', DEFAULT_CONNECT_TIMEOUT_SECONDS)
     optvalues.request_timeout = option_with_default(configs.getint, 'connection', 'request_timeout', DEFAULT_REQUEST_TIMEOUT_SECONDS)
     optvalues.execute = None
@@ -2566,10 +2325,10 @@
 def setup_cqlruleset(cqlmodule):
     global cqlruleset
     cqlruleset = cqlmodule.CqlRuleSet
-    cqlruleset.append_rules(cqlsh_extra_syntax_rules)
-    for rulename, termname, func in cqlsh_syntax_completers:
+    cqlruleset.append_rules(cqlshhandling.cqlsh_extra_syntax_rules)
+    for rulename, termname, func in cqlshhandling.cqlsh_syntax_completers:
         cqlruleset.completer_for(rulename, termname)(func)
-    cqlruleset.commands_end_with_newline.update(my_commands_ending_with_newline)
+    cqlruleset.commands_end_with_newline.update(cqlshhandling.my_commands_ending_with_newline)
 
 
 def setup_cqldocs(cqlmodule):
@@ -2617,6 +2376,7 @@
         sys.stderr.write("Using CQL driver: %s\n" % (cassandra,))
         sys.stderr.write("Using connect timeout: %s seconds\n" % (options.connect_timeout,))
         sys.stderr.write("Using '%s' encoding\n" % (options.encoding,))
+        sys.stderr.write("Using ssl: %s\n" % (options.ssl,))
 
     # create timezone based on settings, environment or auto-detection
     timezone = None
@@ -2666,6 +2426,7 @@
                       display_nanotime_format=options.nanotime_format,
                       display_date_format=options.date_format,
                       display_float_precision=options.float_precision,
+                      display_double_precision=options.double_precision,
                       display_timezone=timezone,
                       max_trace_wait=options.max_trace_wait,
                       ssl=options.ssl,
diff --git a/bin/nodetool b/bin/nodetool
index b1cfba5..6c3cc30 100755
--- a/bin/nodetool
+++ b/bin/nodetool
@@ -106,7 +106,9 @@
 fi
 
 "$JAVA" $JAVA_AGENT -ea -cp "$CLASSPATH" $JVM_OPTS -Xmx$MAX_HEAP_SIZE \
+        -XX:ParallelGCThreads=1 \
         -Dcassandra.storagedir="$cassandra_storagedir" \
+        -Dcassandra.logdir="$CASSANDRA_LOG_DIR" \
         -Dlogback.configurationFile=logback-tools.xml \
         $JVM_ARGS \
         org.apache.cassandra.tools.NodeTool -p $JMX_PORT $ARGS
diff --git a/bin/stop-server.bat b/bin/stop-server.bat
index 66a55fd..acd88d0 100644
--- a/bin/stop-server.bat
+++ b/bin/stop-server.bat
@@ -38,7 +38,7 @@
 

 REM Start with /B -> the control+c event we generate in stop-server.ps1 percolates

 REM up and hits this external batch file if we call powershell directly.

-start /B powershell /file "%CASSANDRA_HOME%/bin/stop-server.ps1" -batchpid %PID% %*

+start /WAIT /B powershell /file "%CASSANDRA_HOME%/bin/stop-server.ps1" -batchpid %PID% %*

 goto finally

 

 REM -----------------------------------------------------------------------------

diff --git a/build.xml b/build.xml
index dd75f17..8f26c5e 100644
--- a/build.xml
+++ b/build.xml
@@ -33,7 +33,7 @@
     <property name="debuglevel" value="source,lines,vars"/>
 
     <!-- default version and SCM information -->
-    <property name="base.version" value="3.0.30"/>
+    <property name="base.version" value="3.11.16"/>
     <property name="scm.connection" value="scm:https://gitbox.apache.org/repos/asf/cassandra.git"/>
     <property name="scm.developerConnection" value="scm:https://gitbox.apache.org/repos/asf/cassandra.git"/>
     <property name="scm.url" value="https://gitbox.apache.org/repos/asf?p=cassandra.git;a=tree"/>
@@ -42,6 +42,7 @@
     <property name="basedir" value="."/>
     <property name="build.src" value="${basedir}/src"/>
     <property name="build.src.java" value="${basedir}/src/java"/>
+    <property name="build.src.antlr" value="${basedir}/src/antlr"/>
     <property name="build.src.jdkoverride" value="${basedir}/src/jdkoverride" />
     <property name="build.src.resources" value="${basedir}/src/resources"/>
     <property name="build.src.gen-java" value="${basedir}/src/gen-java"/>
@@ -77,6 +78,8 @@
     <property name="dist.dir" value="${build.dir}/dist"/>
     <property name="tmp.dir" value="${java.io.tmpdir}"/>
 
+    <property name="doc.dir" value="${basedir}/doc"/>
+
     <property name="source.version" value="1.8"/>
     <property name="target.version" value="1.8"/>
 
@@ -143,10 +146,6 @@
       <available file="${build.src.java}" type="dir" />
     </condition>
 
-    <tstamp>
-      <format property="YEAR" pattern="yyyy"/>
-    </tstamp>
-
     <!-- Check if all tests are being run or just one. If it's all tests don't spam the console with test output.
          If it's an individual test print the output from the test under the assumption someone is debugging the test
          and wants to know what is going on without having to context switch to the log file that is generated.
@@ -207,6 +206,7 @@
         <mkdir dir="${build.classes.thrift}"/>
         <mkdir dir="${test.lib}"/>
         <mkdir dir="${test.classes}"/>
+        <mkdir dir="${stress.test.classes}"/>
         <mkdir dir="${build.src.gen-java}"/>
         <mkdir dir="${build.dir.lib}"/>
         <mkdir dir="${jacoco.export.dir}"/>
@@ -233,18 +233,21 @@
     -->
     <target name="check-gen-cql3-grammar">
         <uptodate property="cql3current"
-                srcfile="${build.src.java}/org/apache/cassandra/cql3/Cql.g"
-                targetfile="${build.src.gen-java}/org/apache/cassandra/cql3/Cql.tokens"/>
+                targetfile="${build.src.gen-java}/org/apache/cassandra/cql3/Cql.tokens">
+            <srcfiles dir="${build.src.antlr}">
+                <include name="*.g"/>
+            </srcfiles>
+        </uptodate>
     </target>
 
     <target name="gen-cql3-grammar" depends="check-gen-cql3-grammar" unless="cql3current">
-      <echo>Building Grammar ${build.src.java}/org/apache/cassandra/cql3/Cql.g  ...</echo>
+      <echo>Building Grammar ${build.src.antlr}/Cql.g  ...</echo>
       <java classname="org.antlr.Tool"
             classpathref="cql3-grammar.classpath"
             failonerror="true">
          <arg value="-Xconversiontimeout" />
          <arg value="10000" />
-         <arg value="${build.src.java}/org/apache/cassandra/cql3/Cql.g" />
+         <arg value="${build.src.antlr}/Cql.g" />
          <arg value="-fo" />
          <arg value="${build.src.gen-java}/org/apache/cassandra/cql3/" />
          <arg value="-Xmaxinlinedfastates"/>
@@ -261,6 +264,27 @@
         </wikitext-to-html>
     </target>
 
+    <target name="gen-asciidoc" description="Generate dynamic asciidoc pages" depends="jar" unless="ant.gen-doc.skip">
+        <exec executable="make" osfamily="unix" dir="${doc.dir}">
+            <arg value="gen-asciidoc"/>
+        </exec>
+    </target>
+
+    <target name="gen-doc" description="Generate documentation" depends="gen-asciidoc,generate-cql-html" unless="ant.gen-doc.skip">
+        <exec executable="make" osfamily="unix" dir="${doc.dir}">
+            <arg value="html"/>
+        </exec>
+    </target>
+
+    <!--
+        Generates Java sources for tokenization support from jflex
+        grammar files
+    -->
+    <target name="generate-jflex-java" description="Generate Java from jflex grammar">
+      <taskdef classname="jflex.anttask.JFlexTask" classpathref="jflex.classpath" name="jflex" />
+        <jflex file="${build.src.java}/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerImpl.jflex" destdir="${build.src.gen-java}/" />
+    </target>
+
     <!--
        Fetch Maven Ant Tasks and Cassandra's dependencies
        These targets are intentionally free of dependencies so that they
@@ -320,8 +344,9 @@
             <exclusion groupId="org.checkerframework" artifactId="checker-qual" />
             <exclusion groupId="com.google.errorprone" artifactId="error_prone_annotations" />
           </dependency>
+          <dependency groupId="org.hdrhistogram" artifactId="HdrHistogram" version="2.1.9"/>
           <dependency groupId="commons-cli" artifactId="commons-cli" version="1.1"/>
-          <dependency groupId="commons-codec" artifactId="commons-codec" version="1.2"/>
+          <dependency groupId="commons-codec" artifactId="commons-codec" version="1.9"/>
           <dependency groupId="commons-io" artifactId="commons-io" version="2.6" scope="test"/>
           <dependency groupId="org.apache.commons" artifactId="commons-lang3" version="3.1"/>
           <dependency groupId="org.apache.commons" artifactId="commons-math3" version="3.2"/>
@@ -333,13 +358,14 @@
           <dependency groupId="org.antlr" artifactId="antlr-runtime" version="3.5.2">
             <exclusion groupId="org.antlr" artifactId="stringtemplate"/>
           </dependency>
-          <dependency groupId="org.slf4j" artifactId="slf4j-api" version="1.7.7"/>
-          <dependency groupId="org.slf4j" artifactId="log4j-over-slf4j" version="1.7.7"/>
-          <dependency groupId="org.slf4j" artifactId="jcl-over-slf4j" version="1.7.7" />
+          <dependency groupId="org.slf4j" artifactId="slf4j-api" version="1.7.25"/>
+          <dependency groupId="org.slf4j" artifactId="log4j-over-slf4j" version="1.7.25"/>
+          <dependency groupId="org.slf4j" artifactId="jcl-over-slf4j" version="1.7.25" />
           <dependency groupId="ch.qos.logback" artifactId="logback-core" version="1.2.9"/>
           <dependency groupId="ch.qos.logback" artifactId="logback-classic" version="1.2.9"/>
-          <dependency groupId="org.codehaus.jackson" artifactId="jackson-core-asl" version="1.9.2"/>
-          <dependency groupId="org.codehaus.jackson" artifactId="jackson-mapper-asl" version="1.9.2"/>
+          <dependency groupId="com.fasterxml.jackson.core" artifactId="jackson-core" version="2.13.2"/>
+          <dependency groupId="com.fasterxml.jackson.core" artifactId="jackson-databind" version="2.13.2.2"/>
+          <dependency groupId="com.fasterxml.jackson.core" artifactId="jackson-annotations" version="2.13.2"/>
           <dependency groupId="com.googlecode.json-simple" artifactId="json-simple" version="1.1"/>
           <dependency groupId="com.boundary" artifactId="high-scale-lib" version="1.0.6"/>
           <dependency groupId="com.github.jbellis" artifactId="jamm" version="${jamm.version}"/>
@@ -347,30 +373,30 @@
             <exclusion groupId="org.slf4j" artifactId="slf4j-log4j12"/>
             <exclusion groupId="junit" artifactId="junit"/>
           </dependency>
-          <dependency groupId="org.yaml" artifactId="snakeyaml" version="1.11"/>
+          <dependency groupId="org.yaml" artifactId="snakeyaml" version="1.26"/>
           <dependency groupId="org.apache.thrift" artifactId="libthrift" version="0.9.2">
 	         <exclusion groupId="commons-logging" artifactId="commons-logging"/>
 	         <exclusion groupId="org.apache.httpcomponents" artifactId="httpclient"/>
 	         <exclusion groupId="org.apache.httpcomponents" artifactId="httpcore"/>
           </dependency>
-          <dependency groupId="junit" artifactId="junit" version="4.6" scope="test">
+          <dependency groupId="junit" artifactId="junit" version="4.12" scope="test">
             <exclusion groupId="org.hamcrest" artifactId="hamcrest-core"/>
           </dependency>
-          <dependency groupId="org.apache.rat" artifactId="apache-rat" version="0.10">
-             <exclusion groupId="commons-lang" artifactId="commons-lang"/>
-          </dependency>
           <dependency groupId="org.mockito" artifactId="mockito-core" version="3.2.4" scope="test"/>
-          <dependency groupId="org.apache.cassandra" artifactId="dtest-api" version="0.0.13" scope="test"/>
+          <dependency groupId="org.apache.cassandra" artifactId="dtest-api" version="0.0.15" scope="test"/>
           <dependency groupId="org.reflections" artifactId="reflections" version="0.10.2" scope="test"/>
           <dependency groupId="org.quicktheories" artifactId="quicktheories" version="0.25" scope="test"/>
           <dependency groupId="org.apache.hadoop" artifactId="hadoop-core" version="1.0.3" scope="provided">
           	<exclusion groupId="org.mortbay.jetty" artifactId="servlet-api"/>
           	<exclusion groupId="commons-logging" artifactId="commons-logging"/>
           	<exclusion groupId="org.eclipse.jdt" artifactId="core"/>
-		        <exclusion groupId="ant" artifactId="ant"/>
+		    <exclusion groupId="ant" artifactId="ant"/>
+		    <exclusion groupId="junit" artifactId="junit"/>
+            <exclusion groupId="org.codehaus.jackson" artifactId="jackson-mapper-asl"/>
           </dependency>
           <dependency groupId="org.apache.hadoop" artifactId="hadoop-minicluster" version="1.0.3" scope="provided">
-		        <exclusion groupId="asm" artifactId="asm"/> <!-- this is the outdated version 3.1 -->
+		    <exclusion groupId="asm" artifactId="asm"/> <!-- this is the outdated version 3.1 -->
+            <exclusion groupId="org.codehaus.jackson" artifactId="jackson-mapper-asl"/>
           </dependency>
           <dependency groupId="net.java.dev.jna" artifactId="jna" version="4.2.2"/>
 
@@ -388,12 +414,14 @@
           <dependency groupId="org.openjdk.jmh" artifactId="jmh-core" version="1.21" scope="test"/>
           <dependency groupId="org.openjdk.jmh" artifactId="jmh-generator-annprocess" version="1.21" scope="test"/>
 
+          <dependency groupId="org.apache.ant" artifactId="ant-junit" version="1.9.4" scope="test"/>
+
           <dependency groupId="org.apache.cassandra" artifactId="cassandra-all" version="${version}" />
           <!--dependency groupId="org.apache.cassandra" artifactId="cassandra-thrift" version="${version}" scope="provided"/-->
-          <dependency groupId="io.dropwizard.metrics" artifactId="metrics-core" version="3.1.0" />
-          <dependency groupId="io.dropwizard.metrics" artifactId="metrics-jvm" version="3.1.0" />
-          <dependency groupId="io.dropwizard.metrics" artifactId="metrics-logback" version="3.1.0" />
-          <dependency groupId="com.addthis.metrics" artifactId="reporter-config3" version="3.0.0">
+          <dependency groupId="io.dropwizard.metrics" artifactId="metrics-core" version="3.1.5" />
+          <dependency groupId="io.dropwizard.metrics" artifactId="metrics-jvm" version="3.1.5" />
+          <dependency groupId="io.dropwizard.metrics" artifactId="metrics-logback" version="3.1.5"/>
+          <dependency groupId="com.addthis.metrics" artifactId="reporter-config3" version="3.0.3">
             <exclusion groupId="org.hibernate" artifactId="hibernate-validator" />
           </dependency>
           <dependency groupId="org.mindrot" artifactId="jbcrypt" version="0.4" />
@@ -415,18 +443,27 @@
             <exclusion groupId="com.github.jnr" artifactId="jnr-posix"/>
           </dependency>
           <dependency groupId="org.eclipse.jdt.core.compiler" artifactId="ecj" version="4.4.2" />
-          <dependency groupId="org.caffinitas.ohc" artifactId="ohc-core" version="0.4.3" />
-          <dependency groupId="org.caffinitas.ohc" artifactId="ohc-core-j8" version="0.4.3" />
+          <dependency groupId="org.caffinitas.ohc" artifactId="ohc-core" version="0.4.4" />
+          <dependency groupId="org.caffinitas.ohc" artifactId="ohc-core-j8" version="0.4.4" />
           <dependency groupId="net.ju-n.compile-command-annotations" artifactId="compile-command-annotations" version="1.2.0" scope="provided"/>
           <dependency groupId="org.fusesource" artifactId="sigar" version="1.6.4">
           	<exclusion groupId="log4j" artifactId="log4j"/>
           </dependency>
           <dependency groupId="joda-time" artifactId="joda-time" version="2.4" />
+          <dependency groupId="com.carrotsearch" artifactId="hppc" version="0.5.4" />
+          <dependency groupId="de.jflex" artifactId="jflex" version="1.6.0">
+            <exclusion groupId="org.apache.ant" artifactId="ant"/>
+          </dependency>
+          <dependency groupId="com.github.rholder" artifactId="snowball-stemmer" version="1.3.0.581.1" />
+          <dependency groupId="com.googlecode.concurrent-trees" artifactId="concurrent-trees" version="2.4.0" />
+          <dependency groupId="com.github.ben-manes.caffeine" artifactId="caffeine" version="2.2.6" />
+          <dependency groupId="org.jctools" artifactId="jctools-core" version="1.2.1"/>
           <dependency groupId="org.ow2.asm" artifactId="asm" version="${asm.version}"/>
+          <dependency groupId="org.ow2.asm" artifactId="asm-tree" version="${asm.version}" scope="test"/>
+          <dependency groupId="org.ow2.asm" artifactId="asm-commons" version="${asm.version}" scope="test"/>
           <dependency groupId="javax.inject" artifactId="javax.inject" version="1"/>
           <dependency groupId="com.google.j2objc" artifactId="j2objc-annotations" version="1.3" scope="provided"/>
           <dependency groupId="org.junit" artifactId="junit-bom" version="5.6.0" type="pom"/>
-          <dependency groupId="org.apache.ant" artifactId="ant-junit" version="1.9.4" scope="test"/>
           <!-- when updating assertj, make sure to also update the corresponding junit-bom dependency -->
           <dependency groupId="org.assertj" artifactId="assertj-core" version="3.15.0" scope="test"/>
           <dependency groupId="org.hamcrest" artifactId="hamcrest" version="2.2" scope="test"/>
@@ -493,11 +530,6 @@
         <dependency groupId="org.apache.cassandra" artifactId="dtest-api" />
         <dependency groupId="org.reflections" artifactId="reflections" />
       	<dependency groupId="com.google.code.findbugs" artifactId="jsr305"/>
-        <dependency groupId="org.antlr" artifactId="antlr"/>
-        <dependency groupId="com.datastax.cassandra" artifactId="cassandra-driver-core" classifier="shaded"/>
-        <dependency groupId="org.eclipse.jdt.core.compiler" artifactId="ecj"/>
-        <dependency groupId="org.caffinitas.ohc" artifactId="ohc-core" />
-        <dependency groupId="org.caffinitas.ohc" artifactId="ohc-core-j8" />
         <dependency groupId="org.openjdk.jmh" artifactId="jmh-core"/>
         <dependency groupId="org.openjdk.jmh" artifactId="jmh-generator-annprocess"/>
         <dependency groupId="net.ju-n.compile-command-annotations" artifactId="compile-command-annotations"/>
@@ -505,35 +537,11 @@
         <!-- adding this dependency is necessary for assertj. When updating assertj, need to also update the version of
              this that the new assertj's `assertj-parent-pom` depends on. -->
         <dependency groupId="org.junit" artifactId="junit-bom" type="pom"/>
-        <dependency groupId="org.assertj" artifactId="assertj-core"/>
-      </artifact:pom>
-      <!-- this build-deps-pom-sources "artifact" is the same as build-deps-pom but only with those
-           artifacts that have "-source.jar" files -->
-      <artifact:pom id="build-deps-pom-sources"
-                    artifactId="cassandra-build-deps">
-        <parent groupId="org.apache.cassandra"
-                artifactId="cassandra-parent"
-                version="${version}"/>
-        <dependency groupId="junit" artifactId="junit"/>
-        <dependency groupId="org.mockito" artifactId="mockito-core" />
-        <dependency groupId="org.reflections" artifactId="reflections" />
-        <dependency groupId="com.datastax.cassandra" artifactId="cassandra-driver-core" classifier="shaded"/>
-        <dependency groupId="org.eclipse.jdt.core.compiler" artifactId="ecj"/>
-        <dependency groupId="org.caffinitas.ohc" artifactId="ohc-core"/>
-        <dependency groupId="org.openjdk.jmh" artifactId="jmh-core"/>
-        <dependency groupId="org.openjdk.jmh" artifactId="jmh-generator-annprocess"/>
-        <dependency groupId="net.ju-n.compile-command-annotations" artifactId="compile-command-annotations"/>
-        <dependency groupId="org.apache.ant" artifactId="ant-junit" />
-        <dependency groupId="org.assertj" artifactId="assertj-core"/>
-      </artifact:pom>
+        <!-- coverage debs -->
+        <dependency groupId="org.jacoco" artifactId="org.jacoco.agent"/>
+        <dependency groupId="org.jacoco" artifactId="org.jacoco.ant"/>
 
-      <artifact:pom id="coverage-deps-pom"
-                    artifactId="cassandra-coverage-deps">
-        <parent groupId="org.apache.cassandra"
-                artifactId="cassandra-parent"
-                version="${version}"/>
-
-        <dependency groupId="org.junit" artifactId="junit-bom" type="pom"/>
+        <dependency groupId="org.junit" artifactId="junit-bom" type="pom" scope="test"/>
         <dependency groupId="org.assertj" artifactId="assertj-core" scope="test"/>
         <dependency groupId="org.hamcrest" artifactId="hamcrest" scope="test"/>
         <!-- coverage debs -->
@@ -567,8 +575,9 @@
         <dependency groupId="org.slf4j" artifactId="slf4j-api"/>
         <dependency groupId="org.slf4j" artifactId="log4j-over-slf4j"/>
         <dependency groupId="org.slf4j" artifactId="jcl-over-slf4j"/>
-        <dependency groupId="org.codehaus.jackson" artifactId="jackson-core-asl"/>
-        <dependency groupId="org.codehaus.jackson" artifactId="jackson-mapper-asl"/>
+        <dependency groupId="com.fasterxml.jackson.core" artifactId="jackson-core"/>
+        <dependency groupId="com.fasterxml.jackson.core" artifactId="jackson-databind"/>
+        <dependency groupId="com.fasterxml.jackson.core" artifactId="jackson-annotations"/>
         <dependency groupId="com.googlecode.json-simple" artifactId="json-simple"/>
         <dependency groupId="com.boundary" artifactId="high-scale-lib"/>
         <dependency groupId="org.yaml" artifactId="snakeyaml"/>
@@ -606,9 +615,17 @@
         <dependency groupId="org.eclipse.jdt.core.compiler" artifactId="ecj"/>
         <dependency groupId="org.caffinitas.ohc" artifactId="ohc-core"/>
         <dependency groupId="org.caffinitas.ohc" artifactId="ohc-core-j8"/>
+        <dependency groupId="com.github.ben-manes.caffeine" artifactId="caffeine" />
+        <dependency groupId="org.jctools" artifactId="jctools-core"/>
         <dependency groupId="org.ow2.asm" artifactId="asm" />
         <dependency groupId="javax.inject" artifactId="javax.inject"/>
         <dependency groupId="com.google.j2objc" artifactId="j2objc-annotations"/>
+        <dependency groupId="org.hdrhistogram" artifactId="HdrHistogram"/>
+
+        <!-- sasi deps -->
+        <dependency groupId="de.jflex" artifactId="jflex" />
+        <dependency groupId="com.github.rholder" artifactId="snowball-stemmer" />
+        <dependency groupId="com.googlecode.concurrent-trees" artifactId="concurrent-trees" />
 
         <!-- compile tools -->
         <dependency groupId="com.google.code.findbugs" artifactId="jsr305" scope="provided"/>
@@ -633,22 +650,17 @@
         <dependency groupId="org.slf4j" artifactId="log4j-over-slf4j"/>
         <dependency groupId="org.slf4j" artifactId="jcl-over-slf4j"/>
         <dependency groupId="org.apache.thrift" artifactId="libthrift"/>
-      </artifact:pom>
-      <artifact:pom id="clientutil-pom"
-                    artifactId="cassandra-clientutil"
-                    url="https://cassandra.apache.org"
-                    name="Apache Cassandra">
-        <parent groupId="org.apache.cassandra"
-                artifactId="cassandra-parent"
-                version="${version}"/>
-        <scm connection="${scm.connection}" developerConnection="${scm.developerConnection}" url="${scm.url}"/>
-        <dependency groupId="com.google.guava" artifactId="guava"/>
+        <dependency groupId="com.carrotsearch" artifactId="hppc"/>
       </artifact:pom>
     </target>
 
     <!-- deprecated: legacy compatibility for build scripts in other repositories -->
     <target name="maven-ant-tasks-retrieve-build" depends="resolver-retrieve-build"/>
 
+    <target name="echo-base-version">
+        <echo message="${base.version}" />
+    </target>
+
     <!--
        Generate thrift code.  We have targets to build java because
        Cassandra depends on it, and python because that is what the system
@@ -711,7 +723,7 @@
     <target name="build" depends="resolver-retrieve-build,build-project" description="Compile Cassandra classes"/>
     <target name="codecoverage" depends="jacoco-run,jacoco-report" description="Create code coverage report"/>
 
-    <target depends="init,gen-cql3-grammar,generate-cql-html,rat-check"
+    <target depends="init,gen-cql3-grammar,generate-cql-html,generate-jflex-java,rat-check"
             name="build-project">
         <echo message="${ant.project.name}: ${ant.file}"/>
         <!-- Order matters! -->
@@ -739,8 +751,24 @@
 
     <!-- Stress build file -->
     <property name="stress.build.src" value="${basedir}/tools/stress/src" />
+    <property name="stress.test.src" value="${basedir}/tools/stress/test/unit" />
     <property name="stress.build.classes" value="${build.classes}/stress" />
-	  <property name="stress.manifest" value="${stress.build.classes}/MANIFEST.MF" />
+    <property name="stress.test.classes" value="${build.dir}/test/stress-classes" />
+	<property name="stress.manifest" value="${stress.build.classes}/MANIFEST.MF" />
+
+    <target name="stress-build-test" depends="stress-build" description="Compile stress tests">
+        <javac debug="true" debuglevel="${debuglevel}" destdir="${stress.test.classes}"
+               includeantruntime="false"
+               source="${source.version}"
+               target="${target.version}"
+               encoding="utf-8">
+            <classpath>
+                <path refid="cassandra.classpath.test"/>
+                <pathelement location="${stress.build.classes}" />
+            </classpath>
+            <src path="${stress.test.src}"/>
+        </javac>
+    </target>
 
     <target name="stress-build" depends="build" description="build stress tool">
         <mkdir dir="${stress.build.classes}" />
@@ -750,13 +778,33 @@
                 <path refid="cassandra.classpath" />
             </classpath>
         </javac>
+        <copy todir="${stress.build.classes}">
+            <fileset dir="${stress.build.src}/resources" />
+        </copy>
+    </target>
+
+    <target name="stress-test" depends="maybe-build-test" description="Runs stress tests">
+        <testmacro inputdir="${stress.test.src}"
+                       timeout="${test.timeout}">
+        </testmacro>
+    </target>
+
+    <!-- Use this with an FQDN for test class, and an optional csv list of methods like this:
+      ant stress-test-some -Dtest.name=org.apache.cassandra.stress.settings.SettingsNodeTest
+      ant stress-test-some -Dtest.name=org.apache.cassandra.stress.settings.SettingsNodeTest -Dtest.methods=testDefaults
+    -->
+    <target name="stress-test-some" depends="maybe-build-test" description="Runs stress tests">
+      <testmacro inputdir="${stress.test.src}"
+                 timeout="${test.timeout}">
+        <test unless:blank="${test.methods}" name="${test.name}" methods="${test.methods}" outfile="build/test/output/TEST-${test.name}-${test.methods}"/>
+        <test if:blank="${test.methods}" name="${test.name}" outfile="build/test/output/TEST-${test.name}"/>
+      </testmacro>
     </target>
 
 	<target name="_write-poms" depends="maven-declare-dependencies">
 	    <artifact:writepom pomRefId="parent-pom" file="${build.dir}/${final.name}-parent.pom"/>
 	    <artifact:writepom pomRefId="thrift-pom" file="${build.dir}/${ant.project.name}-thrift-${version}.pom"/>
 	    <artifact:writepom pomRefId="all-pom" file="${build.dir}/${final.name}.pom"/>
-	    <artifact:writepom pomRefId="clientutil-pom" file="${build.dir}/${ant.project.name}-clientutil-${version}.pom"/>
 	    <artifact:writepom pomRefId="build-deps-pom" file="${build.dir}/tmp-${final.name}-deps.pom"/>
 	</target>
 
@@ -809,42 +857,11 @@
           <attribute name="Premain-Class"
                      value="org.apache.cassandra.infrastructure.continuations.CAgent"/>
           <attribute name="Class-Path"
-                     value="${ant.project.name}-clientutil-${version}.jar ${ant.project.name}-thrift-${version}.jar" />
+                     value="${ant.project.name}-thrift-${version}.jar" />
         <!-- </section> -->
         </manifest>
       </jar>
 
-      <!-- Test Jar -->
-      <jar jarfile="${build.dir}/${ant.project.name}-test-${version}.jar">
-        <fileset dir="${test.classes}"/>
-        <manifest>
-          <attribute name="Implementation-Title" value="Cassandra"/>
-          <attribute name="Implementation-Version" value="${version}"/>
-          <attribute name="Implementation-Vendor" value="Apache"/>
-        </manifest>
-      </jar>
-
-      <!-- Clientutil Jar -->
-      <!-- TODO: write maven pom here -->
-      <jar jarfile="${build.dir}/${ant.project.name}-clientutil-${version}.jar">
-        <fileset dir="${build.classes.main}">
-          <include name="org/apache/cassandra/serializers/*" />
-          <include name="org/apache/cassandra/utils/ByteBufferUtil*.class" />
-          <include name="org/apache/cassandra/utils/Hex.class" />
-          <include name="org/apache/cassandra/utils/UUIDGen*.class" />
-          <include name="org/apache/cassandra/utils/FBUtilities*.class" />
-          <include name="org/apache/cassandra/exceptions/*.class" />
-          <include name="org/apache/cassandra/utils/CloseableIterator.class" />
-          <include name="org/apache/cassandra/io/util/*.class" />
-          <include name="org/apache/cassandra/utils/SigarLibrary.class" />
-        </fileset>
-        <manifest>
-          <attribute name="Implementation-Title" value="Cassandra"/>
-          <attribute name="Implementation-Version" value="${version}"/>
-          <attribute name="Implementation-Vendor" value="Apache"/>
-        </manifest>
-      </jar>
-
       <!-- Stress jar -->
       <mkdir dir="${stress.build.classes}" />
       <manifest file="${stress.manifest}">
@@ -884,19 +901,6 @@
         </filesets>
       </create-javadoc>
       <jar jarfile="${build.dir}/${final.name}-javadoc.jar" basedir="${javadoc.jars.dir}/main"/>
-
-      <create-javadoc destdir="${javadoc.jars.dir}/clientutil">
-        <filesets>
-          <fileset dir="${build.src.java}" defaultexcludes="yes">
-            <include name="org/apache/cassandra/serializers/*" />
-            <include name="org/apache/cassandra/utils/ByteBufferUtil*.java" />
-            <include name="org/apache/cassandra/utils/Hex.java" />
-            <include name="org/apache/cassandra/utils/UUIDGen*.java" />
-          </fileset>
-        </filesets>
-      </create-javadoc>
-      <jar jarfile="${build.dir}/${ant.project.name}-clientutil-${version}-javadoc.jar"
-           basedir="${javadoc.jars.dir}/clientutil"/>
       <!-- javadoc task always rebuilds so might as well remove the generated docs to prevent
            being pulled into the distribution by accident -->
       <delete quiet="true" dir="${javadoc.jars.dir}"/>
@@ -919,18 +923,10 @@
           <include name="org/apache/**/*.java"/>
         </fileset>
       </jar>
-      <jar jarfile="${build.dir}/${ant.project.name}-clientutil-${version}-sources.jar">
-        <fileset dir="${build.src.java}" defaultexcludes="yes">
-          <include name="org/apache/cassandra/serializers/*" />
-          <include name="org/apache/cassandra/utils/ByteBufferUtil*.java" />
-          <include name="org/apache/cassandra/utils/Hex.java" />
-          <include name="org/apache/cassandra/utils/UUIDGen*.java" />
-        </fileset>
-      </jar>
     </target>
 
     <!-- creates release tarballs -->
-    <target name="artifacts" depends="jar,build-test,sources-jar"
+    <target name="artifacts" depends="jar,gen-doc,build-test,sources-jar"
             description="Create Cassandra release artifacts">
       <mkdir dir="${dist.dir}"/>
       <!-- fix the control linefeed so that builds on windows works on linux -->
@@ -942,15 +938,20 @@
         <fileset dir="${build.dir}">
           <include name="${final.name}.jar" />
           <include name="${ant.project.name}-thrift-${version}.jar" />
-          <include name="${ant.project.name}-clientutil-${version}.jar" />
         </fileset>
       </copy>
 
       <copy todir="${dist.dir}/doc" failonerror="false">
         <fileset dir="doc">
-          <exclude name="cql3/CQL.textile"/>
+          <include name="cql3/CQL.html" />
+          <include name="cql3/CQL.css" />
+          <include name="SASI.md" />
         </fileset>
       </copy>
+      <copy todir="${dist.dir}/doc/html">
+        <fileset dir="doc" />
+        <globmapper from="build/html/*" to="*"/>
+      </copy>
       <copy todir="${dist.dir}/bin">
         <fileset dir="bin"/>
       </copy>
@@ -1069,6 +1070,25 @@
       </checksum>
     </target>
 
+  <target name="build-jmh" depends="build-test" description="Create JMH uber jar">
+      <jar jarfile="${build.test.dir}/deps.jar">
+          <zipgroupfileset dir="${build.dir.lib}/jars">
+              <include name="*jmh*.jar"/>
+              <include name="jopt*.jar"/>
+              <include name="commons*.jar"/>
+          </zipgroupfileset>
+          <zipgroupfileset dir="${build.lib}" includes="*.jar"/>
+      </jar>
+      <jar jarfile="${build.test.dir}/benchmarks.jar">
+          <manifest>
+              <attribute name="Main-Class" value="org.openjdk.jmh.Main"/>
+          </manifest>
+          <zipfileset src="${build.test.dir}/deps.jar" excludes="META-INF/*.SF" />
+          <fileset dir="${build.classes.main}"/>
+          <fileset dir="${test.classes}"/>
+      </jar>
+  </target>
+
   <!-- Wrapper of build-test without dependencies, so both that target and its dependencies are skipped if the property
     no-build-test is true. This is meant to be used to run tests without actually building them, provided that they have
     been built before. All test targets depend on this, so one can run them using the no-build-test property.
@@ -1085,8 +1105,9 @@
     <antcall target="build-test" inheritRefs="true"/>
   </target>
 
-  <target name="build-test" depends="jar,stress-build" description="Compile test classes">
+  <target name="build-test" depends="jar,stress-build-test" description="Compile test classes">
     <javac
+     compiler="modern"
      debug="true"
      debuglevel="${debuglevel}"
      destdir="${test.classes}"
@@ -1208,7 +1229,7 @@
              algorithm to limit the metaspace size and clean up SoftReferences
              more aggressively rather than waiting. See CASSANDRA-14922 for more details.
         -->
-        <jvmarg value="-XX:MaxMetaspaceSize=256M" />
+        <jvmarg value="-XX:MaxMetaspaceSize=384M" />
         <jvmarg value="-XX:SoftRefLRUPolicyMSPerMB=0" />
         <jvmarg value="-Dcassandra.memtable_row_overhead_computation_step=100"/>
         <jvmarg value="-Dcassandra.test.use_prepared=${cassandra.test.use_prepared}"/>
@@ -1221,10 +1242,14 @@
         <!-- disable shrinks in quicktheories CASSANDRA-15554 -->
         <jvmarg value="-DQT_SHRINKS=0"/>
 	<optjvmargs/>
+	<!-- Uncomment to debug unittest, attach debugger to port 1416 -->
+	<!--jvmarg line="-agentlib:jdwp=transport=dt_socket,address=localhost:1416,server=y,suspend=y" /-->
         <classpath>
           <pathelement path="${java.class.path}"/>
+          <pathelement location="${stress.build.classes}"/>
           <path refid="cassandra.classpath" />
           <pathelement location="${test.classes}"/>
+          <pathelement location="${stress.test.classes}"/>
           <pathelement location="${test.conf}"/>
           <fileset dir="${test.lib}">
             <include name="**/*.jar" />
@@ -1236,55 +1261,15 @@
             <filelist dir="@{inputdir}" files="@{filelist}"/>
         </batchtest>
       </junit-timeout>
+
       <delete quiet="true" failonerror="false" dir="${build.test.dir}/cassandra/commitlog"/>
+      <delete quiet="true" failonerror="false" dir="${build.test.dir}/cassandra/cdc_raw"/>
       <delete quiet="true" failonerror="false" dir="${build.test.dir}/cassandra/data"/>
       <delete quiet="true" failonerror="false" dir="${build.test.dir}/cassandra/saved_caches"/>
+      <delete quiet="true" failonerror="false" dir="${build.test.dir}/cassandra/hints"/>
     </sequential>
   </macrodef>
 
-  <!--
-    This test target is a bit different.  It's purpose is to exercise the
-    clientutil jar in order to expose any new dependencies.  For that
-    reason we use the classes from the jar, and a carefully constructed
-    classpath which only contains what we expect users to need.
-  -->
-  <target name="test-clientutil-jar" depends="build-test,jar" description="Test clientutil jar">
-    <mkdir dir="${build.test.dir}/output"/>
-    <junit fork="on" forkmode="perTest" failureproperty="testfailed" maxmemory="1024m" timeout="${test.timeout}">
-      <!-- Note that the test pass without that next line, but it prints an ugly error message -->
-      <jvmarg value="-Djava.library.path=${build.lib}/sigar-bin"/>
-      <test name="org.apache.cassandra.serializers.ClientUtilsTest" todir="${build.test.dir}/output"/>
-      <formatter type="brief" usefile="false" />
-      <formatter type="xml" usefile="true"/>
-      <classpath>
-        <pathelement location="${test.classes}" />
-        <pathelement location="${build.dir}/${ant.project.name}-clientutil-${version}.jar" />
-        <pathelement location="${build.dir}/${ant.project.name}-thrift-${version}.jar" />
-        <pathelement location="${build.lib}/libthrift-0.9.0.jar" />
-        <pathelement location="${build.lib}/slf4j-api-1.7.7.jar" />
-        <pathelement location="${build.lib}/log4j-over-slf4j.jar" />
-        <pathelement location="${build.lib}/logback-core-1.1.3.jar" />
-        <pathelement location="${build.lib}/logback-classic-1.1.3.jar" />
-        <pathelement location="${build.lib}/jackson-core-asl-1.9.2.jar" />
-        <pathelement location="${build.lib}/jackson-mapper-asl-1.9.2.jar" />
-        <pathelement location="${build.lib}/sigar-1.6.4.jar" />
-        <fileset dir="${build.dir.lib}">
-          <include name="**/junit*.jar" />
-        </fileset>
-      </classpath>
-    </junit>
-      <fail message="Clientutil test(s) failed.">
-          <condition>
-              <and>
-                  <isset property="testfailed"/>
-                  <not>
-                      <isset property="ant.test.failure.ignore"/>
-                  </not>
-              </and>
-          </condition>
-      </fail>
-  </target>
-
   <target name="testold" depends="maybe-build-test" description="Execute unit tests">
     <testmacro inputdir="${test.unit.src}" timeout="${test.timeout}">
       <jvmarg value="-Dlegacy-sstable-root=${test.data}/legacy-sstables"/>
@@ -1335,6 +1320,27 @@
     </sequential>
   </macrodef>
 
+  <macrodef name="testlist-cdc">
+    <attribute name="test.file.list" />
+    <sequential>
+      <property name="cdc_yaml" value="${build.test.dir}/cassandra.cdc.yaml"/>
+      <concat destfile="${cdc_yaml}">
+        <fileset file="${test.conf}/cassandra.yaml"/>
+        <fileset file="${test.conf}/cdc.yaml"/>
+      </concat>
+      <testmacrohelper inputdir="${test.unit.src}" filelist="@{test.file.list}"
+                       exclude="**/*.java" timeout="${test.timeout}" testtag="cdc">
+        <jvmarg value="-Dlegacy-sstable-root=${test.data}/legacy-sstables"/>
+        <jvmarg value="-Dinvalid-legacy-sstable-root=${test.data}/invalid-legacy-sstables"/>
+        <jvmarg value="-Dmigration-sstable-root=${test.data}/migration-sstables"/>
+        <jvmarg value="-Dcassandra.ring_delay_ms=1000"/>
+        <jvmarg value="-Dcassandra.tolerate_sstable_size=true"/>
+        <jvmarg value="-Dcassandra.config=file:///${cdc_yaml}"/>
+        <jvmarg value="-Dcassandra.skip_sync=true" />
+      </testmacrohelper>
+    </sequential>
+  </macrodef>
+
   <!--
     Run named ant task with jacoco, such as "ant jacoco-run -Dtaskname=test"
     the target run must enable the jacoco agent if usejacoco is 'yes' -->
@@ -1347,7 +1353,7 @@
     </antcall>
   </target>
 
-  <!-- Use this with an FQDN for test class, and a csv list of methods like this:
+  <!-- Use this with an FQDN for test class, and an optional csv list of methods like this:
     ant testsome -Dtest.name=org.apache.cassandra.service.StorageServiceServerTest
     ant testsome -Dtest.name=org.apache.cassandra.service.StorageServiceServerTest -Dtest.methods=testRegularMode,testGetAllRangesEmpty
   -->
@@ -1377,7 +1383,7 @@
     </testmacro>
   </target>
 
-  <!-- Use this with an FQDN for test class, and a csv list of methods like this:
+  <!-- Use this with an FQDN for test class, and an optional csv list of methods like this:
     ant burn-testsome -Dtest.name=org.apache.cassandra.utils.memory.LongBufferPoolTest
     ant burn-testsome -Dtest.name=org.apache.cassandra.utils.memory.LongBufferPoolTest -Dtest.methods=testAllocate
   -->
@@ -1387,12 +1393,7 @@
       <test if:blank="${test.methods}" name="${test.name}"/>
     </testmacro>
   </target>
-  <target name="test-compression" depends="maybe-build-test" description="Execute unit tests with sstable compression enabled">
-    <property name="compressed_yaml" value="${build.test.dir}/cassandra.compressed.yaml"/>
-    <concat destfile="${compressed_yaml}">
-      <fileset file="${test.conf}/cassandra.yaml"/>
-      <fileset file="${test.conf}/commitlog_compression.yaml"/>
-    </concat>
+  <target name="test-compression" depends="maybe-build-test,stress-build" description="Execute unit tests with sstable compression enabled">
     <path id="all-test-classes-path">
       <fileset dir="${test.unit.src}" includes="**/${test.name}.java" />
       <fileset dir="${test.distributed.src}" includes="**/${test.name}.java" />
@@ -1401,6 +1402,14 @@
     <testhelper testdelegate="testlist-compression" />
   </target>
 
+  <target name="test-cdc" depends="maybe-build-test" description="Execute unit tests with change-data-capture enabled">
+    <path id="all-test-classes-path">
+      <fileset dir="${test.unit.src}" includes="**/${test.name}.java" />
+    </path>
+    <property name="all-test-classes" refid="all-test-classes-path"/>
+    <testhelper testdelegate="testlist-cdc" />
+  </target>
+
   <target name="msg-ser-gen-test" depends="maybe-build-test" description="Generates message serializations">
     <testmacro inputdir="${test.unit.src}"
         timeout="${test.timeout}" filter="**/SerializationsTest.java">
@@ -1638,6 +1647,14 @@
       <testhelper testdelegate="testlist-compression"/>
   </target>
 
+  <target name="testclasslist-cdc" depends="maybe-build-test" description="Run tests given in file -Dtest.classlistfile (one-class-per-line, e.g. org/apache/cassandra/db/SomeTest.java)">
+      <path id="all-test-classes-path">
+          <fileset dir="${test.dir}/${test.classlistprefix}" includesfile="${test.classlistfile}"/>
+      </path>
+      <property name="all-test-classes" refid="all-test-classes-path"/>
+      <testhelper testdelegate="testlist-cdc"/>
+  </target>
+
   <target name="dtest-jar" depends="build-test, build" description="Create dtest-compatible jar, including all dependencies">
       <jar jarfile="${build.dir}/dtest-${base.version}.jar">
           <zipgroupfileset dir="${build.lib}" includes="*.jar" excludes="META-INF/*.SF"/>
@@ -1667,7 +1684,7 @@
     </testmacro>
   </target>
 
-  <!-- Use this with an FQDN for test class, and a csv list of methods like this:
+  <!-- Use this with an FQDN for test class, and an optional csv list of methods like this:
       ant test-jvm-dtest-some -Dtest.name=org.apache.cassandra.distributed.test.ResourceLeakTest
       ant test-jvm-dtest-some -Dtest.name=org.apache.cassandra.distributed.test.ResourceLeakTest -Dtest.methods=looperTest
     -->
@@ -1683,7 +1700,7 @@
   </target>
 
   <!-- run microbenchmarks suite -->
-  <target name="microbench" depends="build-test">
+  <target name="microbench" depends="build-jmh">
       <java classname="org.openjdk.jmh.Main"
             fork="true"
             failonerror="true">
@@ -1703,6 +1720,10 @@
           <arg value="${build.test.dir}/jmh-result.json"/>
           <arg value="-v"/>
           <arg value="EXTRA"/>
+
+          <!-- Broken: MutationBench,FastThreadLocalBench  (FIXME) -->
+          <arg value="-e"/><arg value="MutationBench|FastThreadLocalBench"/>
+
           <arg value=".*microbench.*${benchmark.name}"/>
       </java>
   </target>
@@ -1774,14 +1795,16 @@
   <classpathentry kind="src" path="src/java"/>
   <classpathentry kind="src" path="src/resources"/>
   <classpathentry kind="src" path="src/gen-java"/>
+  <classpathentry kind="src" path="conf" including="hotspot_compiler"/>
   <classpathentry kind="src" path="interface/thrift/gen-java"/>
   <classpathentry kind="src" output="build/test/classes" path="test/unit"/>
   <classpathentry kind="src" output="build/test/classes" path="test/long"/>
   <classpathentry kind="src" output="build/test/classes" path="test/distributed"/>
   <classpathentry kind="src" output="build/test/classes" path="test/resources" />
   <classpathentry kind="src" path="tools/stress/src"/>
+  <classpathentry kind="src" output="build/test/stress-classes" path="tools/stress/test/unit" />
   <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
-  <classpathentry kind="output" path="build/classes/main"/>
+  <classpathentry kind="output" path="build/classes/eclipse"/>
   <classpathentry kind="lib" path="build/classes/thrift" sourcepath="interface/thrift/gen-java/"/>
   <classpathentry kind="lib" path="test/conf"/>
   <classpathentry kind="lib" path="${java.home}/../lib/tools.jar"/>
@@ -1899,16 +1922,6 @@
              file="${build.dir}/${ant.project.name}-thrift-${version}-javadoc.jar"
              classifier="javadoc"/>
 
-    <!-- the cassandra-clientutil jar -->
-    <install pomFile="${build.dir}/${ant.project.name}-clientutil-${version}.pom"
-             file="${build.dir}/${ant.project.name}-clientutil-${version}.jar"/>
-    <install pomFile="${build.dir}/${ant.project.name}-clientutil-${version}.pom"
-             file="${build.dir}/${ant.project.name}-clientutil-${version}-sources.jar"
-             classifier="sources"/>
-    <install pomFile="${build.dir}/${ant.project.name}-clientutil-${version}.pom"
-             file="${build.dir}/${ant.project.name}-clientutil-${version}-javadoc.jar"
-             classifier="javadoc"/>
-
     <!-- the cassandra-all jar -->
     <install pomFile="${build.dir}/${final.name}.pom"
              file="${build.dir}/${final.name}.jar"/>
@@ -1941,16 +1954,6 @@
             file="${build.dir}/${ant.project.name}-thrift-${version}-javadoc.jar"
             classifier="javadoc"/>
 
-    <!-- the cassandra-clientutil jar -->
-    <deploy pomFile="${build.dir}/${ant.project.name}-clientutil-${version}.pom"
-            file="${build.dir}/${ant.project.name}-clientutil-${version}.jar"/>
-    <deploy pomFile="${build.dir}/${ant.project.name}-clientutil-${version}.pom"
-             file="${build.dir}/${ant.project.name}-clientutil-${version}-sources.jar"
-             classifier="sources"/>
-    <deploy pomFile="${build.dir}/${ant.project.name}-clientutil-${version}.pom"
-             file="${build.dir}/${ant.project.name}-clientutil-${version}-javadoc.jar"
-             classifier="javadoc"/>
-
     <!-- the cassandra-all jar -->
     <deploy pomFile="${build.dir}/${final.name}.pom"
             file="${build.dir}/${final.name}.jar"/>
diff --git a/conf/cassandra-env.ps1 b/conf/cassandra-env.ps1
index 74511f0..c78a3fc 100644
--- a/conf/cassandra-env.ps1
+++ b/conf/cassandra-env.ps1
@@ -393,33 +393,6 @@
         $env:JVM_OPTS="$env:JVM_OPTS -XX:HeapDumpPath=""$env:CASSANDRA_HEAPDUMP_DIR\cassandra-$unixTimestamp-pid$pid.hprof"""
     }
 
-    if ($env:JVM_VERSION.CompareTo("1.8.0") -eq -1 -or [convert]::ToInt32($env:JVM_PATCH_VERSION) -lt 40)
-    {
-        echo "Cassandra 3.0 and later require Java 8u40 or later."
-        exit
-    }
-
-    # enable assertions.  disabling this in production will give a modest
-    # performance benefit (around 5%).
-    $env:JVM_OPTS = "$env:JVM_OPTS -ea"
-
-    # Specifies the default port over which Cassandra will be available for
-    # JMX connections.
-    $JMX_PORT="7199"
-
-    # store in env to check if it's avail in verification
-    $env:JMX_PORT=$JMX_PORT
-
-    # enable thread priorities, primarily so we can give periodic tasks
-    # a lower priority to avoid interfering with client workload
-    $env:JVM_OPTS="$env:JVM_OPTS -XX:+UseThreadPriorities"
-    # allows lowering thread priority without being root on linux - probably
-    # not necessary on Windows but doesn't harm anything.
-    # see http://tech.stolsvik.com/2010/01/linux-java-thread-priorities-workar
-    $env:JVM_OPTS="$env:JVM_OPTS -XX:ThreadPriorityPolicy=42"
-
-    $env:JVM_OPTS="$env:JVM_OPTS -XX:+HeapDumpOnOutOfMemoryError"
-
     # stop the jvm on OutOfMemoryError as it can result in some data corruption
     # uncomment the preferred option
     # ExitOnOutOfMemoryError and CrashOnOutOfMemoryError require a JRE greater or equals to 1.7 update 101 or 1.8 update 92
@@ -430,40 +403,24 @@
     # print an heap histogram on OutOfMemoryError
     # $env:JVM_OPTS="$env:JVM_OPTS -Dcassandra.printHeapHistogramOnOutOfMemoryError=true"
 
-    # Per-thread stack size.
-    $env:JVM_OPTS="$env:JVM_OPTS -Xss256k"
+    if ($env:JVM_VERSION.CompareTo("1.8.0") -eq -1 -or [convert]::ToInt32($env:JVM_PATCH_VERSION) -lt 40)
+    {
+        echo "Cassandra 3.0 and later require Java 8u40 or later."
+        exit
+    }
 
-    # Larger interned string table, for gossip's benefit (CASSANDRA-6410)
-    $env:JVM_OPTS="$env:JVM_OPTS -XX:StringTableSize=1000003"
+    # Specifies the default port over which Cassandra will be available for
+    # JMX connections.
+    $JMX_PORT="7199"
 
-    # Make sure all memory is faulted and zeroed on startup.
-    # This helps prevent soft faults in containers and makes
-    # transparent hugepage allocation more effective.
-    #$env:JVM_OPTS="$env:JVM_OPTS -XX:+AlwaysPreTouch"
-
-    # Biased locking does not benefit Cassandra.
-    $env:JVM_OPTS="$env:JVM_OPTS -XX:-UseBiasedLocking"
-
-    # Enable thread-local allocation blocks and allow the JVM to automatically
-    # resize them at runtime.
-    $env:JVM_OPTS="$env:JVM_OPTS -XX:+UseTLAB -XX:+ResizeTLAB"
-
-    # http://www.evanjones.ca/jvm-mmap-pause.html
-    $env:JVM_OPTS="$env:JVM_OPTS -XX:+PerfDisableSharedMem"
+    # store in env to check if it's avail in verification
+    $env:JMX_PORT=$JMX_PORT
 
     # Configure the following for JEMallocAllocator and if jemalloc is not available in the system
     # library path.
     # set LD_LIBRARY_PATH=<JEMALLOC_HOME>/lib/
     # $env:JVM_OPTS="$env:JVM_OPTS -Djava.library.path=<JEMALLOC_HOME>/lib/"
 
-    # uncomment to have Cassandra JVM listen for remote debuggers/profilers on port 1414
-    # $env:JVM_OPTS="$env:JVM_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1414"
-
-    # Prefer binding to IPv4 network intefaces (when net.ipv6.bindv6only=1). See
-    # http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6342561 (short version:
-    # comment out this entry to enable IPv6 support).
-    $env:JVM_OPTS="$env:JVM_OPTS -Djava.net.preferIPv4Stack=true"
-
     # jmx: metrics and administration interface
     #
     # add this if you're having trouble connecting:
@@ -479,12 +436,37 @@
     # with authentication and ssl enabled. See https://wiki.apache.org/cassandra/JmxSecurity
     #
     #$env:JVM_OPTS="$env:JVM_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT"
-    #$env:JVM_OPTS="$env:JVM_OPTS -Dcom.sun.management.jmxremote.ssl=false"
+    #$env:JVM_OPTS="$env:JVM_OPTS -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT"
+    #
+    # JMX SSL options
+    #$env:JVM_OPTS="$env:JVM_OPTS -Dcom.sun.management.jmxremote.ssl=true"
+    #$env:JVM_OPTS="$env:JVM_OPTS -Dcom.sun.management.jmxremote.ssl.need.client.auth=true"
+    #$env:JVM_OPTS="$env:JVM_OPTS -Dcom.sun.management.jmxremote.ssl.enabled.protocols=<enabled-protocols>"
+    #$env:JVM_OPTS="$env:JVM_OPTS -Dcom.sun.management.jmxremote.ssl.enabled.cipher.suites=<enabled-cipher-suites>"
+    #$env:JVM_OPTS="$env:JVM_OPTS -Djavax.net.ssl.keyStore=C:/keystore"
+    #$env:JVM_OPTS="$env:JVM_OPTS -Djavax.net.ssl.keyStorePassword=<keystore-password>"
+    #$env:JVM_OPTS="$env:JVM_OPTS -Djavax.net.ssl.trustStore=C:/truststore"
+    #$env:JVM_OPTS="$env:JVM_OPTS -Djavax.net.ssl.trustStorePassword=<truststore-password>"
+    #
+    # JMX auth options
     #$env:JVM_OPTS="$env:JVM_OPTS -Dcom.sun.management.jmxremote.authenticate=true"
+    ## Basic file based authn & authz
     #$env:JVM_OPTS="$env:JVM_OPTS -Dcom.sun.management.jmxremote.password.file=C:/jmxremote.password"
-    $env:JVM_OPTS="$env:JVM_OPTS -Dcassandra.jmx.local.port=$JMX_PORT -XX:+DisableExplicitGC"
+    #$env:JVM_OPTS="$env:JVM_OPTS -Dcom.sun.management.jmxremote.access.file=C:/jmxremote.access"
+
+    ## Custom auth settings which can be used as alternatives to JMX's out of the box auth utilities.
+    ## JAAS login modules can be used for authentication by uncommenting these two properties.
+    ## Cassandra ships with a LoginModule implementation - org.apache.cassandra.auth.CassandraLoginModule -
+    ## which delegates to the IAuthenticator configured in cassandra.yaml
+    #$env:JVM_OPTS="$env:JVM_OPTS -Dcassandra.jmx.remote.login.config=CassandraLogin"
+    #$env:JVM_OPTS="$env:JVM_OPTS -Djava.security.auth.login.config=C:/cassandra-jaas.config"
+
+    ## Cassandra also ships with a helper for delegating JMX authz calls to the configured IAuthorizer,
+    ## uncomment this to use it. Requires one of the two authentication options to be enabled
+    #$env:JVM_OPTS="$env:JVM_OPTS -Dcassandra.jmx.authorizer=org.apache.cassandra.auth.jmx.AuthorizationProxy"
+
+    # Default JMX setup, bound to local loopback address only
+    $env:JVM_OPTS="$env:JVM_OPTS -Dcassandra.jmx.local.port=$JMX_PORT"
 
     $env:JVM_OPTS="$env:JVM_OPTS $env:JVM_EXTRA_OPTS"
-
-    #$env:JVM_OPTS="$env:JVM_OPTS -XX:+UnlockCommercialFeatures -XX:+FlightRecorder"
 }
diff --git a/conf/cassandra-env.sh b/conf/cassandra-env.sh
index a5ac96c..7df3598 100644
--- a/conf/cassandra-env.sh
+++ b/conf/cassandra-env.sh
@@ -88,7 +88,7 @@
 
 # Determine the sort of JVM we'll be running on.
 java_ver_output=`"${JAVA:-java}" -version 2>&1`
-jvmver=`echo "$java_ver_output" | grep '[openjdk|java] version' | awk -F'"' 'NR==1 {print $2}'`
+jvmver=`echo "$java_ver_output" | grep '[openjdk|java] version' | awk -F'"' 'NR==1 {print $2}' | cut -d\- -f1`
 JVM_VERSION=${jvmver%_*}
 JVM_PATCH_VERSION=${jvmver#*_}
 
@@ -102,7 +102,7 @@
     exit 1;
 fi
 
-jvm=`echo "$java_ver_output" | grep -A 1 'java version' | awk 'NR==2 {print $1}'`
+jvm=`echo "$java_ver_output" | grep -A 1 '[openjdk|java] version' | awk 'NR==2 {print $1}'`
 case "$jvm" in
     OpenJDK)
         JVM_VENDOR=OpenJDK
@@ -208,46 +208,13 @@
     JVM_OPTS="$JVM_OPTS -XX:+UseCondCardMark"
 fi
 
-# enable assertions.  disabling this in production will give a modest
-# performance benefit (around 5%).
-JVM_OPTS="$JVM_OPTS -ea"
-
-# Per-thread stack size.
-JVM_OPTS="$JVM_OPTS -Xss256k"
-
-# Make sure all memory is faulted and zeroed on startup.
-# This helps prevent soft faults in containers and makes
-# transparent hugepage allocation more effective.
-JVM_OPTS="$JVM_OPTS -XX:+AlwaysPreTouch"
-
-# Biased locking does not benefit Cassandra.
-JVM_OPTS="$JVM_OPTS -XX:-UseBiasedLocking"
-
-# Larger interned string table, for gossip's benefit (CASSANDRA-6410)
-JVM_OPTS="$JVM_OPTS -XX:StringTableSize=1000003"
-
-# Enable thread-local allocation blocks and allow the JVM to automatically
-# resize them at runtime.
-JVM_OPTS="$JVM_OPTS -XX:+UseTLAB -XX:+ResizeTLAB"
-
-# http://www.evanjones.ca/jvm-mmap-pause.html
-JVM_OPTS="$JVM_OPTS -XX:+PerfDisableSharedMem"
-
 # provides hints to the JIT compiler
 JVM_OPTS="$JVM_OPTS -XX:CompileCommandFile=$CASSANDRA_CONF/hotspot_compiler"
 
 # add the jamm javaagent
 JVM_OPTS="$JVM_OPTS -javaagent:$CASSANDRA_HOME/lib/jamm-0.3.0.jar"
 
-# enable thread priorities, primarily so we can give periodic tasks
-# a lower priority to avoid interfering with client workload
-JVM_OPTS="$JVM_OPTS -XX:+UseThreadPriorities"
-# allows lowering thread priority without being root.  see
-# http://tech.stolsvik.com/2010/01/linux-java-thread-priorities-workaround.html
-JVM_OPTS="$JVM_OPTS -XX:ThreadPriorityPolicy=42"
-
 # set jvm HeapDumpPath with CASSANDRA_HEAPDUMP_DIR
-JVM_OPTS="$JVM_OPTS -XX:+HeapDumpOnOutOfMemoryError"
 if [ "x$CASSANDRA_HEAPDUMP_DIR" != "x" ]; then
     JVM_OPTS="$JVM_OPTS -XX:HeapDumpPath=$CASSANDRA_HEAPDUMP_DIR/cassandra-`date +%s`-pid$$.hprof"
 fi
@@ -264,18 +231,6 @@
 # print an heap histogram on OutOfMemoryError
 # JVM_OPTS="$JVM_OPTS -Dcassandra.printHeapHistogramOnOutOfMemoryError=true"
 
-# uncomment to have Cassandra JVM listen for remote debuggers/profilers on port 1414
-# JVM_OPTS="$JVM_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1414"
-
-# uncomment to have Cassandra JVM log internal method compilation (developers only)
-# JVM_OPTS="$JVM_OPTS -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation"
-# JVM_OPTS="$JVM_OPTS -XX:+UnlockCommercialFeatures -XX:+FlightRecorder"
-
-# Prefer binding to IPv4 network intefaces (when net.ipv6.bindv6only=1). See
-# http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6342561 (short version:
-# comment out this entry to enable IPv6 support).
-JVM_OPTS="$JVM_OPTS -Djava.net.preferIPv4Stack=true"
-
 # jmx: metrics and administration interface
 #
 # add this if you're having trouble connecting:
@@ -294,33 +249,51 @@
     LOCAL_JMX=yes
 fi
 
-## Cassandra also ships with a helper for protecting against security gaps in a default JMX configuration. To use it,
-## uncomment line below.
-#JVM_OPTS="$JVM_OPTS -Dcassandra.jmx.authorizer=org.apache.cassandra.auth.jmx.AuthorizationProxy"
-
 # Specifies the default port over which Cassandra will be available for
 # JMX connections.
 # For security reasons, you should not expose this port to the internet.  Firewall it if needed.
 JMX_PORT="7199"
 
 if [ "$LOCAL_JMX" = "yes" ]; then
-  JVM_OPTS="$JVM_OPTS -Dcassandra.jmx.local.port=$JMX_PORT -XX:+DisableExplicitGC"
+  JVM_OPTS="$JVM_OPTS -Dcassandra.jmx.local.port=$JMX_PORT"
+  JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.authenticate=false"
 else
-  JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT"
+  JVM_OPTS="$JVM_OPTS -Dcassandra.jmx.remote.port=$JMX_PORT"
+  # if ssl is enabled the same port cannot be used for both jmx and rmi so either
+  # pick another value for this property or comment out to use a random port (though see CASSANDRA-7087 for origins)
   JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT"
-  JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl=false"
+
+  # turn on JMX authentication. See below for further options
   JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.authenticate=true"
-  JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.password.file=/etc/cassandra/jmxremote.password"
-#  JVM_OPTS="$JVM_OPTS -Djavax.net.ssl.keyStore=/path/to/keystore"
-#  JVM_OPTS="$JVM_OPTS -Djavax.net.ssl.keyStorePassword=<keystore-password>"
-#  JVM_OPTS="$JVM_OPTS -Djavax.net.ssl.trustStore=/path/to/truststore"
-#  JVM_OPTS="$JVM_OPTS -Djavax.net.ssl.trustStorePassword=<truststore-password>"
-#  JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.need.client.auth=true"
-#  JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.registry.ssl=true"
-#  JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.enabled.protocols=<enabled-protocols>"
-#  JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.enabled.cipher.suites=<enabled-cipher-suites>"
+
+  # jmx ssl options
+  #JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl=true"
+  #JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.need.client.auth=true"
+  #JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.enabled.protocols=<enabled-protocols>"
+  #JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.enabled.cipher.suites=<enabled-cipher-suites>"
+  #JVM_OPTS="$JVM_OPTS -Djavax.net.ssl.keyStore=/path/to/keystore"
+  #JVM_OPTS="$JVM_OPTS -Djavax.net.ssl.keyStorePassword=<keystore-password>"
+  #JVM_OPTS="$JVM_OPTS -Djavax.net.ssl.trustStore=/path/to/truststore"
+  #JVM_OPTS="$JVM_OPTS -Djavax.net.ssl.trustStorePassword=<truststore-password>"
 fi
 
+# jmx authentication and authorization options. By default, auth is only
+# activated for remote connections but they can also be enabled for local only JMX
+## Basic file based authn & authz
+JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.password.file=/etc/cassandra/jmxremote.password"
+#JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.access.file=/etc/cassandra/jmxremote.access"
+## Custom auth settings which can be used as alternatives to JMX's out of the box auth utilities.
+## JAAS login modules can be used for authentication by uncommenting these two properties.
+## Cassandra ships with a LoginModule implementation - org.apache.cassandra.auth.CassandraLoginModule -
+## which delegates to the IAuthenticator configured in cassandra.yaml. See the sample JAAS configuration
+## file cassandra-jaas.config
+#JVM_OPTS="$JVM_OPTS -Dcassandra.jmx.remote.login.config=CassandraLogin"
+#JVM_OPTS="$JVM_OPTS -Djava.security.auth.login.config=$CASSANDRA_CONF/cassandra-jaas.config"
+
+## Cassandra also ships with a helper for delegating JMX authz calls to the configured IAuthorizer,
+## uncomment this to use it. Requires one of the two authentication options to be enabled
+#JVM_OPTS="$JVM_OPTS -Dcassandra.jmx.authorizer=org.apache.cassandra.auth.jmx.AuthorizationProxy"
+
 # To use mx4j, an HTML interface for JMX, add mx4j-tools.jar to the lib/
 # directory.
 # See http://cassandra.apache.org/doc/3.11/operating/metrics.html#jmx
diff --git a/conf/cassandra-jaas.config b/conf/cassandra-jaas.config
new file mode 100644
index 0000000..f3a9bf7
--- /dev/null
+++ b/conf/cassandra-jaas.config
@@ -0,0 +1,4 @@
+// Delegates authentication to Cassandra's configured IAuthenticator
+CassandraLogin {
+  org.apache.cassandra.auth.CassandraLoginModule REQUIRED;
+};
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index ec2157b..d00c3d9 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -1,4 +1,5 @@
-# Cassandra storage config YAML 
+
+# Cassandra storage config YAML
 
 # NOTE:
 #   See http://wiki.apache.org/cassandra/StorageConfiguration for
@@ -35,20 +36,22 @@
 # Only supported with the Murmur3Partitioner.
 # allocate_tokens_for_keyspace: KEYSPACE
 
-# initial_token allows you to specify tokens manually.  While you can use # it with
+# initial_token allows you to specify tokens manually.  While you can use it with
 # vnodes (num_tokens > 1, above) -- in which case you should provide a 
-# comma-separated list -- it's primarily used when adding nodes # to legacy clusters 
+# comma-separated list -- it's primarily used when adding nodes to legacy clusters 
 # that do not have vnodes enabled.
 # initial_token:
 
 # See http://wiki.apache.org/cassandra/HintedHandoff
 # May either be "true" or "false" to enable globally
 hinted_handoff_enabled: true
+
 # When hinted_handoff_enabled is true, a black list of data centers that will not
 # perform hinted handoff
-#hinted_handoff_disabled_datacenters:
+# hinted_handoff_disabled_datacenters:
 #    - DC1
 #    - DC2
+
 # this defines the maximum amount of time a dead host will have hints
 # generated.  After it has been dead this long, new hints for it will not be
 # created until it has been seen alive and gone down again.
@@ -120,11 +123,11 @@
 #   increase system_auth keyspace replication factor if you use this role manager.
 role_manager: CassandraRoleManager
 
-# Validity period for roles cache (fetching permissions can be an
-# expensive operation depending on the authorizer). Granted roles are cached for
-# authenticated sessions in AuthenticatedUser and after the period specified
-# here, become eligible for (async) reload.
-# Defaults to 2000, set to 0 to disable.
+# Validity period for roles cache (fetching granted roles can be an expensive
+# operation depending on the role manager, CassandraRoleManager is one example)
+# Granted roles are cached for authenticated sessions in AuthenticatedUser and
+# after the period specified here, become eligible for (async) reload.
+# Defaults to 2000, set to 0 to disable caching entirely.
 # Will be disabled automatically for AllowAllAuthenticator.
 roles_validity_in_ms: 2000
 
@@ -134,7 +137,7 @@
 # completes. If roles_validity_in_ms is non-zero, then this must be
 # also.
 # Defaults to the same value as roles_validity_in_ms.
-# roles_update_interval_in_ms: 1000
+# roles_update_interval_in_ms: 2000
 
 # Validity period for permissions cache (fetching permissions can be an
 # expensive operation depending on the authorizer, CassandraAuthorizer is
@@ -148,7 +151,26 @@
 # completes. If permissions_validity_in_ms is non-zero, then this must be
 # also.
 # Defaults to the same value as permissions_validity_in_ms.
-# permissions_update_interval_in_ms: 1000
+# permissions_update_interval_in_ms: 2000
+
+# Validity period for credentials cache. This cache is tightly coupled to
+# the provided PasswordAuthenticator implementation of IAuthenticator. If
+# another IAuthenticator implementation is configured, this cache will not
+# be automatically used and so the following settings will have no effect.
+# Please note, credentials are cached in their encrypted form, so while
+# activating this cache may reduce the number of queries made to the
+# underlying table, it may not  bring a significant reduction in the
+# latency of individual authentication attempts.
+# Defaults to 2000, set to 0 to disable credentials caching.
+credentials_validity_in_ms: 2000
+
+# Refresh interval for credentials cache (if enabled).
+# After this interval, cache entries become eligible for refresh. Upon next
+# access, an async reload is scheduled and the old value returned until it
+# completes. If credentials_validity_in_ms is non-zero, then this must be
+# also.
+# Defaults to the same value as credentials_validity_in_ms.
+# credentials_update_interval_in_ms: 2000
 
 # The partitioner is responsible for distributing groups of rows (by
 # partition key) across nodes in the cluster.  You should leave this
@@ -174,28 +196,85 @@
 # If not set, the default directory is $CASSANDRA_HOME/data/commitlog.
 # commitlog_directory: /var/lib/cassandra/commitlog
 
-# policy for data disk failures:
-# die: shut down gossip and client transports and kill the JVM for any fs errors or
-#      single-sstable errors, so the node can be replaced.
-# stop_paranoid: shut down gossip and client transports even for single-sstable errors,
-#                kill the JVM for errors during startup.
-# stop: shut down gossip and client transports, leaving the node effectively dead, but
-#       can still be inspected via JMX, kill the JVM for errors during startup.
-# best_effort: stop using the failed disk and respond to requests based on
-#              remaining available sstables.  This means you WILL see obsolete
-#              data at CL.ONE!
-# ignore: ignore fatal errors and let requests fail, as in pre-1.2 Cassandra
+# Enable / disable CDC functionality on a per-node basis. This modifies the logic used
+# for write path allocation rejection (standard: never reject. cdc: reject Mutation
+# containing a CDC-enabled table if at space limit in cdc_raw_directory).
+cdc_enabled: false
+
+# CommitLogSegments are moved to this directory on flush if cdc_enabled: true and the
+# segment contains mutations for a CDC-enabled table. This should be placed on a
+# separate spindle than the data directories. If not set, the default directory is
+# $CASSANDRA_HOME/data/cdc_raw.
+# cdc_raw_directory: /var/lib/cassandra/cdc_raw
+
+# Policy for data disk failures:
+#
+# die
+#   shut down gossip and client transports and kill the JVM for any fs errors or
+#   single-sstable errors, so the node can be replaced.
+#
+# stop_paranoid
+#   shut down gossip and client transports even for single-sstable errors,
+#   kill the JVM for errors during startup.
+#
+# stop
+#   shut down gossip and client transports, leaving the node effectively dead, but
+#   can still be inspected via JMX, kill the JVM for errors during startup.
+#
+# best_effort
+#    stop using the failed disk and respond to requests based on
+#    remaining available sstables.  This means you WILL see obsolete
+#    data at CL.ONE!
+#
+# ignore
+#    ignore fatal errors and let requests fail, as in pre-1.2 Cassandra
 disk_failure_policy: stop
 
-# policy for commit disk failures:
-# die: shut down gossip and Thrift and kill the JVM, so the node can be replaced.
-# stop: shut down gossip and Thrift, leaving the node effectively dead, but
-#       can still be inspected via JMX.
-# stop_commit: shutdown the commit log, letting writes collect but
-#              continuing to service reads, as in pre-2.0.5 Cassandra
-# ignore: ignore fatal errors and let the batches fail
+# Policy for commit disk failures:
+#
+# die
+#   shut down gossip and Thrift and kill the JVM, so the node can be replaced.
+#
+# stop
+#   shut down gossip and Thrift, leaving the node effectively dead, but
+#   can still be inspected via JMX.
+#
+# stop_commit
+#   shutdown the commit log, letting writes collect but
+#   continuing to service reads, as in pre-2.0.5 Cassandra
+#
+# ignore
+#   ignore fatal errors and let the batches fail
 commit_failure_policy: stop
 
+# Maximum size of the native protocol prepared statement cache
+#
+# Valid values are either "auto" (omitting the value) or a value greater 0.
+#
+# Note that specifying a too large value will result in long running GCs and possbily
+# out-of-memory errors. Keep the value at a small fraction of the heap.
+#
+# If you constantly see "prepared statements discarded in the last minute because
+# cache limit reached" messages, the first step is to investigate the root cause
+# of these messages and check whether prepared statements are used correctly -
+# i.e. use bind markers for variable parts.
+#
+# Do only change the default value, if you really have more prepared statements than
+# fit in the cache. In most cases it is not neccessary to change this value.
+# Constantly re-preparing statements is a performance penalty.
+#
+# Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater
+prepared_statements_cache_size_mb:
+
+# Maximum size of the Thrift prepared statement cache
+#
+# If you do not use Thrift at all, it is safe to leave this value at "auto".
+#
+# See description of 'prepared_statements_cache_size_mb' above for more information.
+#
+# Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater
+thrift_prepared_statements_cache_size_mb:
+
 # Maximum size of the key cache in memory.
 #
 # Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the
@@ -225,11 +304,14 @@
 # Disabled by default, meaning all keys are going to be saved
 # key_cache_keys_to_save: 100
 
-# Row cache implementation class name.
-# Available implementations:
-#   org.apache.cassandra.cache.OHCProvider                Fully off-heap row cache implementation (default).
-#   org.apache.cassandra.cache.SerializingCacheProvider   This is the row cache implementation availabile
-#                                                         in previous releases of Cassandra.
+# Row cache implementation class name. Available implementations:
+#
+# org.apache.cassandra.cache.OHCProvider
+#   Fully off-heap row cache implementation (default).
+#
+# org.apache.cassandra.cache.SerializingCacheProvider
+#   This is the row cache implementation availabile
+#   in previous releases of Cassandra.
 # row_cache_class_name: org.apache.cassandra.cache.OHCProvider
 
 # Maximum size of the row cache in memory.
@@ -330,7 +412,7 @@
 # Compression to apply to the commit log. If omitted, the commit log
 # will be written uncompressed.  LZ4, Snappy, and Deflate compressors
 # are supported.
-#commitlog_compression:
+# commitlog_compression:
 #   - class_name: LZ4Compressor
 #     parameters:
 #         -
@@ -367,9 +449,14 @@
 # be limited by the less of concurrent reads or concurrent writes.
 concurrent_materialized_view_writes: 32
 
-# Maximum memory to use for pooling sstable buffers. Defaults to the smaller
-# of 1/4 of heap or 512MB. This pool is allocated off-heap, so is in addition
-# to the memory allocated for heap. Memory is only allocated as needed.
+# Maximum memory to use for sstable chunk cache and buffer pooling.
+# 32MB of this are reserved for pooling buffers, the rest is used as an
+# cache that holds uncompressed sstable chunks.
+# Defaults to the smaller of 1/4 of heap or 512MB. This pool is allocated off-heap,
+# so is in addition to the memory allocated for heap. The cache also has on-heap
+# overhead which is roughly 128 bytes per chunk (i.e. 0.2% of the reserved size
+# if the default 64k chunk size is used).
+# Memory is only allocated when needed.
 # file_cache_size_in_mb: 512
 
 # Flag indicating whether to allocate on or off heap when the sstable buffer
@@ -391,6 +478,10 @@
 # memtable_heap_space_in_mb: 2048
 # memtable_offheap_space_in_mb: 2048
 
+# memtable_cleanup_threshold is deprecated. The default calculation
+# is the only reasonable choice. See the comments on  memtable_flush_writers
+# for more information.
+#
 # Ratio of occupied non-flushing memtable size to total permitted size
 # that will trigger a flush of the largest memtable. Larger mct will
 # mean larger flushes and hence less compaction, but also less concurrent
@@ -402,12 +493,15 @@
 
 # Specify the way Cassandra allocates and manages memtable memory.
 # Options are:
-#   heap_buffers:    on heap nio buffers
 #
-# Note: offheap_buffers are not supported in Cassandra 3.0 - 3.3.
-# They have been re-introduced in Cassandra 3.4. For details see
-# https://issues.apache.org/jira/browse/CASSANDRA-9472 and
-# https://issues.apache.org/jira/browse/CASSANDRA-11039
+# heap_buffers
+#   on heap nio buffers
+#
+# offheap_buffers
+#   off heap (direct) nio buffers
+#
+# offheap_objects
+#    off heap objects
 memtable_allocation_type: heap_buffers
 
 # Limits the maximum Merkle tree depth to avoid consuming too much
@@ -434,16 +528,49 @@
 #
 # commitlog_total_space_in_mb: 8192
 
-# This sets the amount of memtable flush writer threads.  These will
-# be blocked by disk io, and each one will hold a memtable in memory
-# while blocked. 
+# This sets the number of memtable flush writer threads per disk
+# as well as the total number of memtables that can be flushed concurrently.
+# These are generally a combination of compute and IO bound.
 #
-# memtable_flush_writers defaults to the smaller of (number of disks,
-# number of cores), with a minimum of 2 and a maximum of 8.
-# 
-# If your data directories are backed by SSD, you should increase this
-# to the number of cores.
-#memtable_flush_writers: 8
+# Memtable flushing is more CPU efficient than memtable ingest and a single thread
+# can keep up with the ingest rate of a whole server on a single fast disk
+# until it temporarily becomes IO bound under contention typically with compaction.
+# At that point you need multiple flush threads. At some point in the future
+# it may become CPU bound all the time.
+#
+# You can tell if flushing is falling behind using the MemtablePool.BlockedOnAllocation
+# metric which should be 0, but will be non-zero if threads are blocked waiting on flushing
+# to free memory.
+#
+# memtable_flush_writers defaults to two for a single data directory.
+# This means that two  memtables can be flushed concurrently to the single data directory.
+# If you have multiple data directories the default is one memtable flushing at a time
+# but the flush will use a thread per data directory so you will get two or more writers.
+#
+# Two is generally enough to flush on a fast disk [array] mounted as a single data directory.
+# Adding more flush writers will result in smaller more frequent flushes that introduce more
+# compaction overhead.
+#
+# There is a direct tradeoff between number of memtables that can be flushed concurrently
+# and flush size and frequency. More is not better you just need enough flush writers
+# to never stall waiting for flushing to free memory.
+#
+#memtable_flush_writers: 2
+
+# Total space to use for change-data-capture logs on disk.
+#
+# If space gets above this value, Cassandra will throw WriteTimeoutException
+# on Mutations including tables with CDC enabled. A CDCCompactor is responsible
+# for parsing the raw CDC logs and deleting them when parsing is completed.
+#
+# The default value is the min of 4096 mb and 1/8th of the total space
+# of the drive where cdc_raw_directory resides.
+# cdc_total_space_in_mb: 4096
+
+# When we hit our cdc_raw limit and the CDCCompactor is either running behind
+# or experiencing backpressure, we check at the following interval to see if any
+# new space for cdc-tracked tables has been made available. Default to 250ms
+# cdc_free_space_check_interval_ms: 250
 
 # A fixed memory pool size in MB for for SSTable index summaries. If left
 # empty, this will default to 5% of the heap size. If the memory usage of
@@ -479,8 +606,7 @@
 # Address or interface to bind to and tell other Cassandra nodes to connect to.
 # You _must_ change this if you want multiple nodes to be able to communicate!
 #
-# Set listen_address OR listen_interface, not both. Interfaces must correspond
-# to a single address, IP aliasing is not supported.
+# Set listen_address OR listen_interface, not both.
 #
 # Leaving it blank leaves it up to InetAddress.getLocalHost(). This
 # will always do the Right Thing _if_ the node is properly configured
@@ -489,12 +615,16 @@
 #
 # Setting listen_address to 0.0.0.0 is always wrong.
 #
+listen_address: localhost
+
+# Set listen_address OR listen_interface, not both. Interfaces must correspond
+# to a single address, IP aliasing is not supported.
+# listen_interface: eth0
+
 # If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address
 # you can specify which should be chosen using listen_interface_prefer_ipv6. If false the first ipv4
 # address will be used. If true the first ipv6 address will be used. Defaults to false preferring
 # ipv4. If there is only one address it will be selected regardless of ipv4/ipv6.
-listen_address: localhost
-# listen_interface: eth0
 # listen_interface_prefer_ipv6: false
 
 # Address to broadcast to other Cassandra nodes
@@ -553,8 +683,7 @@
 # The address or interface to bind the Thrift RPC service and native transport
 # server to.
 #
-# Set rpc_address OR rpc_interface, not both. Interfaces must correspond
-# to a single address, IP aliasing is not supported.
+# Set rpc_address OR rpc_interface, not both.
 #
 # Leaving rpc_address blank has the same effect as on listen_address
 # (i.e. it will be based on the configured hostname of the node).
@@ -563,13 +692,16 @@
 # set broadcast_rpc_address to a value other than 0.0.0.0.
 #
 # For security reasons, you should not expose this port to the internet.  Firewall it if needed.
-#
+rpc_address: localhost
+
+# Set rpc_address OR rpc_interface, not both. Interfaces must correspond
+# to a single address, IP aliasing is not supported.
+# rpc_interface: eth1
+
 # If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address
 # you can specify which should be chosen using rpc_interface_prefer_ipv6. If false the first ipv4
 # address will be used. If true the first ipv6 address will be used. Defaults to false preferring
 # ipv4. If there is only one address it will be selected regardless of ipv4/ipv6.
-rpc_address: localhost
-# rpc_interface: eth1
 # rpc_interface_prefer_ipv6: false
 
 # port for Thrift to listen for clients on
@@ -586,16 +718,18 @@
 
 # Cassandra provides two out-of-the-box options for the RPC Server:
 #
-# sync  -> One thread per thrift connection. For a very large number of clients, memory
-#          will be your limiting factor. On a 64 bit JVM, 180KB is the minimum stack size
-#          per thread, and that will correspond to your use of virtual memory (but physical memory
-#          may be limited depending on use of stack space).
+# sync
+#   One thread per thrift connection. For a very large number of clients, memory
+#   will be your limiting factor. On a 64 bit JVM, 180KB is the minimum stack size
+#   per thread, and that will correspond to your use of virtual memory (but physical memory
+#   may be limited depending on use of stack space).
 #
-# hsha  -> Stands for "half synchronous, half asynchronous." All thrift clients are handled
-#          asynchronously using a small number of threads that does not vary with the amount
-#          of thrift clients (and thus scales well to many clients). The rpc requests are still
-#          synchronous (one thread per active request). If hsha is selected then it is essential
-#          that rpc_max_threads is changed from the default value of unlimited.
+# hsha
+#   Stands for "half synchronous, half asynchronous." All thrift clients are handled
+#   asynchronously using a small number of threads that does not vary with the amount
+#   of thrift clients (and thus scales well to many clients). The rpc requests are still
+#   synchronous (one thread per active request). If hsha is selected then it is essential
+#   that rpc_max_threads is changed from the default value of unlimited.
 #
 # The default is sync because on Windows hsha is about 30% slower.  On Linux,
 # sync/hsha performance is about the same, with hsha of course using less memory.
@@ -624,13 +758,17 @@
 # Uncomment to set socket buffer size for internode communication
 # Note that when setting this, the buffer size is limited by net.core.wmem_max
 # and when not setting it it is defined by net.ipv4.tcp_wmem
-# See:
+# See also:
 # /proc/sys/net/core/wmem_max
 # /proc/sys/net/core/rmem_max
 # /proc/sys/net/ipv4/tcp_wmem
 # /proc/sys/net/ipv4/tcp_wmem
-# and: man tcp
+# and 'man tcp'
 # internode_send_buff_size_in_bytes:
+
+# Uncomment to set socket buffer size for internode communication
+# Note that when setting this, the buffer size is limited by net.core.wmem_max
+# and when not setting it it is defined by net.ipv4.tcp_wmem
 # internode_recv_buff_size_in_bytes:
 
 # Frame size for thrift (maximum message length).
@@ -654,59 +792,26 @@
 # lose data on truncation or drop.
 auto_snapshot: true
 
-# When executing a scan, within or across a partition, we need to keep the
-# tombstones seen in memory so we can return them to the coordinator, which
-# will use them to make sure other replicas also know about the deleted rows.
-# With workloads that generate a lot of tombstones, this can cause performance
-# problems and even exaust the server heap.
-# (http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets)
-# Adjust the thresholds here if you understand the dangers and want to
-# scan more tombstones anyway.  These thresholds may also be adjusted at runtime
-# using the StorageService mbean.
-tombstone_warn_threshold: 1000
-tombstone_failure_threshold: 100000
-
-# Filtering and secondary index queries at read consistency levels above ONE/LOCAL_ONE use a
-# mechanism called replica filtering protection to ensure that results from stale replicas do
-# not violate consistency. (See CASSANDRA-8272 and CASSANDRA-15907 for more details.) This 
-# mechanism materializes replica results by partition on-heap at the coordinator. The more possibly
-# stale results returned by the replicas, the more rows materialized during the query.
-replica_filtering_protection:
-    # These thresholds exist to limit the damage severely out-of-date replicas can cause during these
-    # queries. They limit the number of rows from all replicas individual index and filtering queries
-    # can materialize on-heap to return correct results at the desired read consistency level.
-    #
-    # "cached_replica_rows_warn_threshold" is the per-query threshold at which a warning will be logged.
-    # "cached_replica_rows_fail_threshold" is the per-query threshold at which the query will fail.
-    #
-    # These thresholds may also be adjusted at runtime using the StorageService mbean.
-    # 
-    # If the failure threshold is breached, it is likely that either the current page/fetch size
-    # is too large or one or more replicas is severely out-of-sync and in need of repair.
-    cached_rows_warn_threshold: 2000
-    cached_rows_fail_threshold: 32000
-
 # Granularity of the collation index of rows within a partition.
 # Increase if your rows are large, or if you have a very large
 # number of rows per partition.  The competing goals are these:
-#   1) a smaller granularity means more index entries are generated
-#      and looking up rows withing the partition by collation column
-#      is faster
-#   2) but, Cassandra will keep the collation index in memory for hot
-#      rows (as part of the key cache), so a larger granularity means
-#      you can cache more hot rows
+#
+# - a smaller granularity means more index entries are generated
+#   and looking up rows withing the partition by collation column
+#   is faster
+# - but, Cassandra will keep the collation index in memory for hot
+#   rows (as part of the key cache), so a larger granularity means
+#   you can cache more hot rows
 column_index_size_in_kb: 64
 
-
-# Log WARN on any batch size exceeding this value. 5kb per batch by default.
-# Caution should be taken on increasing the size of this threshold as it can lead to node instability.
-batch_size_warn_threshold_in_kb: 5
-
-# Fail any batch exceeding this value. 50kb (10x warn threshold) by default.
-batch_size_fail_threshold_in_kb: 50
-
-# Log WARN on any batches not of type LOGGED than span across more partitions than this limit
-unlogged_batch_across_partitions_warn_threshold: 10
+# Per sstable indexed key cache entries (the collation index in memory
+# mentioned above) exceeding this size will not be held on heap.
+# This means that only partition information is held on heap and the
+# index entries are read from disk.
+#
+# Note that this size refers to the size of the
+# serialized index information and not the size of the partition.
+column_index_cache_size_in_kb: 2
 
 # Number of simultaneous compactions to allow, NOT including
 # validation "compactions" for anti-entropy repair.  Simultaneous
@@ -732,9 +837,6 @@
 # of compaction, including validation compaction.
 compaction_throughput_mb_per_sec: 16
 
-# Log a warning when compacting partitions larger than this value
-compaction_large_partition_warning_threshold_mb: 100
-
 # When compacting, the replacement sstable(s) can be opened before they
 # are completely written, and used in place of the prior sstables for
 # any range that has been written. This helps to smoothly transfer reads 
@@ -755,6 +857,19 @@
 # When unset, the default is 200 Mbps or 25 MB/s
 # inter_dc_stream_throughput_outbound_megabits_per_sec: 200
 
+# Server side timeouts for requests. The server will return a timeout exception
+# to the client if it can't complete an operation within the corresponding
+# timeout. Those settings are a protection against:
+#   1) having client wait on an operation that might never terminate due to some
+#      failures.
+#   2) operations that use too much CPU/read too much data (leading to memory build
+#      up) by putting a limit to how long an operation will execute.
+# For this reason, you should avoid putting these settings too high. In other words, 
+# if you are timing out requests because of underlying resource constraints then 
+# increasing the timeout will just cause more problems. Of course putting them too 
+# low is equally ill-advised since clients could get timeouts even for successful 
+# operations just because the timeout setting is too tight.
+
 # How long the coordinator should wait for read operations to complete
 read_request_timeout_in_ms: 5000
 # How long the coordinator should wait for seq or index scans to complete
@@ -773,6 +888,11 @@
 # The default timeout for other, miscellaneous operations
 request_timeout_in_ms: 10000
 
+# How long before a node logs slow queries. Select queries that take longer than
+# this timeout to execute, will generate an aggregated log message, so that slow queries
+# can be identified. Set this value to zero to disable slow query logging.
+slow_query_log_timeout_in_ms: 500
+
 # Enable operation timeout information exchange between nodes to accurately
 # measure request timeouts.  If disabled, replicas will assume that requests
 # were forwarded to them instantly by the coordinator, which means that
@@ -783,13 +903,13 @@
 # and the times are synchronized between the nodes.
 cross_node_timeout: false
 
-# Set socket timeout for streaming operation.
-# The stream session is failed if no data/ack is received by any of the participants
-# within that period, which means this should also be sufficient to stream a large
-# sstable or rebuild table indexes.
-# Default value is 86400000ms, which means stale streams timeout after 24 hours.
-# A value of zero means stream sockets should never time out.
-# streaming_socket_timeout_in_ms: 86400000
+# Set keep-alive period for streaming
+# This node will send a keep-alive message periodically with this period.
+# If the node does not receive a keep-alive message from the peer for
+# 2 keep-alive cycles the stream session times out and fail
+# Default value is 300s (5 minutes), which means stalled stream
+# times out in 10 minutes by default
+# streaming_keep_alive_period_in_secs: 300
 
 # phi value that must be reached for a host to be marked down.
 # most users should never need to adjust this.
@@ -797,6 +917,7 @@
 
 # endpoint_snitch -- Set this to a class that implements
 # IEndpointSnitch.  The snitch has two functions:
+#
 # - it teaches Cassandra enough about your network topology to route
 #   requests efficiently
 # - it allows Cassandra to spread replicas around your cluster to avoid
@@ -815,34 +936,40 @@
 # under Ec2Snitch (which will locate them in a new "datacenter") and
 # decommissioning the old ones.
 #
-# Out of the box, Cassandra provides
-#  - SimpleSnitch:
+# Out of the box, Cassandra provides:
+#
+# SimpleSnitch:
 #    Treats Strategy order as proximity. This can improve cache
 #    locality when disabling read repair.  Only appropriate for
 #    single-datacenter deployments.
-#  - GossipingPropertyFileSnitch
+#
+# GossipingPropertyFileSnitch
 #    This should be your go-to snitch for production use.  The rack
 #    and datacenter for the local node are defined in
 #    cassandra-rackdc.properties and propagated to other nodes via
 #    gossip.  If cassandra-topology.properties exists, it is used as a
 #    fallback, allowing migration from the PropertyFileSnitch.
-#  - PropertyFileSnitch:
+#
+# PropertyFileSnitch:
 #    Proximity is determined by rack and data center, which are
 #    explicitly configured in cassandra-topology.properties.
-#  - Ec2Snitch:
+#
+# Ec2Snitch:
 #    Appropriate for EC2 deployments in a single Region. Loads Region
 #    and Availability Zone information from the EC2 API. The Region is
 #    treated as the datacenter, and the Availability Zone as the rack.
 #    Only private IPs are used, so this will not work across multiple
 #    Regions.
-#  - Ec2MultiRegionSnitch:
+#
+# Ec2MultiRegionSnitch:
 #    Uses public IPs as broadcast_address to allow cross-region
 #    connectivity.  (Thus, you should set seed addresses to the public
 #    IP as well.) You will need to open the storage_port or
 #    ssl_storage_port on the public IP firewall.  (For intra-Region
 #    traffic, Cassandra will switch to the private IP after
 #    establishing a connection.)
-#  - RackInferringSnitch:
+#
+# RackInferringSnitch:
 #    Proximity is determined by rack and data center, which are
 #    assumed to correspond to the 3rd and 2nd octet of each node's IP
 #    address, respectively.  Unless this happens to match your
@@ -882,20 +1009,26 @@
 request_scheduler: org.apache.cassandra.scheduler.NoScheduler
 
 # Scheduler Options vary based on the type of scheduler
-# NoScheduler - Has no options
+#
+# NoScheduler
+#   Has no options
+#
 # RoundRobin
-#  - throttle_limit -- The throttle_limit is the number of in-flight
-#                      requests per client.  Requests beyond 
-#                      that limit are queued up until
-#                      running requests can complete.
-#                      The value of 80 here is twice the number of
-#                      concurrent_reads + concurrent_writes.
-#  - default_weight -- default_weight is optional and allows for
-#                      overriding the default which is 1.
-#  - weights -- Weights are optional and will default to 1 or the
-#               overridden default_weight. The weight translates into how
-#               many requests are handled during each turn of the
-#               RoundRobin, based on the scheduler id.
+#   throttle_limit
+#     The throttle_limit is the number of in-flight
+#     requests per client.  Requests beyond 
+#     that limit are queued up until
+#     running requests can complete.
+#     The value of 80 here is twice the number of
+#     concurrent_reads + concurrent_writes.
+#   default_weight
+#     default_weight is optional and allows for
+#     overriding the default which is 1.
+#   weights
+#     Weights are optional and will default to 1 or the
+#     overridden default_weight. The weight translates into how
+#     many requests are handled during each turn of the
+#     RoundRobin, based on the scheduler id.
 #
 # request_scheduler_options:
 #    throttle_limit: 80
@@ -909,11 +1042,15 @@
 # request_scheduler_id: keyspace
 
 # Enable or disable inter-node encryption
-# Default settings are TLS v1, RSA 1024-bit keys (it is imperative that
-# users generate their own keys) TLS_RSA_WITH_AES_128_CBC_SHA as the cipher
-# suite for authentication, key exchange and encryption of the actual data transfers.
-# Use the DHE/ECDHE ciphers if running in FIPS 140 compliant mode.
-# NOTE: No custom encryption options are enabled at the moment
+# JVM defaults for supported SSL socket protocols and cipher suites can
+# be replaced using custom encryption options. This is not recommended
+# unless you have policies in place that dictate certain settings, or
+# need to disable vulnerable ciphers or protocols in case the JVM cannot
+# be updated.
+# FIPS compliant settings can be configured at JVM level and should not
+# involve changing encryption settings here:
+# https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/FIPS.html
+# *NOTE* No custom encryption options are enabled at the moment
 # The available internode options are : all, none, dc, rack
 #
 # If set to dc cassandra will encrypt the traffic between the DCs
@@ -935,6 +1072,7 @@
     # store_type: JKS
     # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA]
     # require_client_auth: false
+    # require_endpoint_verification: false
 
 # enable or disable client/server encryption.
 client_encryption_options:
@@ -955,10 +1093,17 @@
 
 # internode_compression controls whether traffic between nodes is
 # compressed.
-# can be:  all  - all traffic is compressed
-#          dc   - traffic between different datacenters is compressed
-#          none - nothing is compressed.
-internode_compression: all
+# Can be:
+#
+# all
+#   all traffic is compressed
+#
+# dc
+#   traffic between different datacenters is compressed
+#
+# none
+#   nothing is compressed.
+internode_compression: dc
 
 # Enable or disable tcp_nodelay for inter-dc communication.
 # Disabling it will result in larger (but fewer) network packets being sent,
@@ -974,12 +1119,8 @@
 # This threshold can be adjusted to minimize logging if necessary
 # gc_log_threshold_in_ms: 200
 
-# GC Pauses greater than gc_warn_threshold_in_ms will be logged at WARN level
 # If unset, all GC Pauses greater than gc_log_threshold_in_ms will log at
 # INFO level
-# Adjust the threshold based on your application throughput requirement
-gc_warn_threshold_in_ms: 1000
-
 # UDFs (user defined functions) are disabled by default.
 # As of Cassandra 3.0 there is a sandbox in place that should prevent execution of evil code.
 enable_user_defined_functions: false
@@ -990,14 +1131,6 @@
 # This option has no effect, if enable_user_defined_functions is false.
 enable_scripted_user_defined_functions: false
 
-# Enables materialized view creation on this node.
-# Materialized views are considered experimental and are not recommended for production use.
-enable_materialized_views: true
-
-# Enables the used of 'ALTER ... DROP COMPACT STORAGE' statements on this node.
-# 'ALTER ... DROP COMPACT STORAGE' is considered experimental and is not recommended for production use.
-enable_drop_compact_storage: false
-
 # The default Windows kernel timer and scheduling resolution is 15.6ms for power conservation.
 # Lowering this value on Windows can provide much tighter latency and better throughput, however
 # some virtualized environments may see a negative performance impact from changing this setting
@@ -1005,11 +1138,116 @@
 # setting.
 windows_timer_interval: 1
 
+
+# Enables encrypting data at-rest (on disk). Different key providers can be plugged in, but the default reads from
+# a JCE-style keystore. A single keystore can hold multiple keys, but the one referenced by
+# the "key_alias" is the only key that will be used for encrypt opertaions; previously used keys
+# can still (and should!) be in the keystore and will be used on decrypt operations
+# (to handle the case of key rotation).
+#
+# It is strongly recommended to download and install Java Cryptography Extension (JCE)
+# Unlimited Strength Jurisdiction Policy Files for your version of the JDK.
+# (current link: http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html)
+#
+# Currently, only the following file types are supported for transparent data encryption, although
+# more are coming in future cassandra releases: commitlog, hints
+transparent_data_encryption_options:
+    enabled: false
+    chunk_length_kb: 64
+    cipher: AES/CBC/PKCS5Padding
+    key_alias: testing:1
+    # CBC IV length for AES needs to be 16 bytes (which is also the default size)
+    # iv_length: 16
+    key_provider: 
+      - class_name: org.apache.cassandra.security.JKSKeyProvider
+        parameters: 
+          - keystore: conf/.keystore
+            keystore_password: cassandra
+            store_type: JCEKS
+            key_password: cassandra
+
+
+#####################
+# SAFETY THRESHOLDS #
+#####################
+
+# When executing a scan, within or across a partition, we need to keep the
+# tombstones seen in memory so we can return them to the coordinator, which
+# will use them to make sure other replicas also know about the deleted rows.
+# With workloads that generate a lot of tombstones, this can cause performance
+# problems and even exaust the server heap.
+# (http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets)
+# Adjust the thresholds here if you understand the dangers and want to
+# scan more tombstones anyway.  These thresholds may also be adjusted at runtime
+# using the StorageService mbean.
+tombstone_warn_threshold: 1000
+tombstone_failure_threshold: 100000
+
+# Filtering and secondary index queries at read consistency levels above ONE/LOCAL_ONE use a
+# mechanism called replica filtering protection to ensure that results from stale replicas do
+# not violate consistency. (See CASSANDRA-8272 and CASSANDRA-15907 for more details.) This
+# mechanism materializes replica results by partition on-heap at the coordinator. The more possibly
+# stale results returned by the replicas, the more rows materialized during the query.
+replica_filtering_protection:
+    # These thresholds exist to limit the damage severely out-of-date replicas can cause during these
+    # queries. They limit the number of rows from all replicas individual index and filtering queries
+    # can materialize on-heap to return correct results at the desired read consistency level.
+    #
+    # "cached_replica_rows_warn_threshold" is the per-query threshold at which a warning will be logged.
+    # "cached_replica_rows_fail_threshold" is the per-query threshold at which the query will fail.
+    #
+    # These thresholds may also be adjusted at runtime using the StorageService mbean.
+    #
+    # If the failure threshold is breached, it is likely that either the current page/fetch size
+    # is too large or one or more replicas is severely out-of-sync and in need of repair.
+    cached_rows_warn_threshold: 2000
+    cached_rows_fail_threshold: 32000
+
+# Log WARN on any multiple-partition batch size exceeding this value. 5kb per batch by default.
+# Caution should be taken on increasing the size of this threshold as it can lead to node instability.
+batch_size_warn_threshold_in_kb: 5
+
+# Fail any multiple-partition batch exceeding this value. 50kb (10x warn threshold) by default.
+batch_size_fail_threshold_in_kb: 50
+
+# Log WARN on any batches not of type LOGGED than span across more partitions than this limit
+unlogged_batch_across_partitions_warn_threshold: 10
+
+# Log a warning when compacting partitions larger than this value
+compaction_large_partition_warning_threshold_mb: 100
+
+# GC Pauses greater than gc_warn_threshold_in_ms will be logged at WARN level
+# Adjust the threshold based on your application throughput requirement
+# By default, Cassandra logs GC Pauses greater than 200 ms at INFO level
+gc_warn_threshold_in_ms: 1000
+
 # Maximum size of any value in SSTables. Safety measure to detect SSTable corruption
 # early. Any value size larger than this threshold will result into marking an SSTable
 # as corrupted. This should be positive and less than 2048.
 # max_value_size_in_mb: 256
 
+# Back-pressure settings #
+# If enabled, the coordinator will apply the back-pressure strategy specified below to each mutation
+# sent to replicas, with the aim of reducing pressure on overloaded replicas.
+back_pressure_enabled: false
+# The back-pressure strategy applied.
+# The default implementation, RateBasedBackPressure, takes three arguments:
+# high ratio, factor, and flow type, and uses the ratio between incoming mutation responses and outgoing mutation requests.
+# If below high ratio, outgoing mutations are rate limited according to the incoming rate decreased by the given factor;
+# if above high ratio, the rate limiting is increased by the given factor;
+# such factor is usually best configured between 1 and 10, use larger values for a faster recovery
+# at the expense of potentially more dropped mutations;
+# the rate limiting is applied according to the flow type: if FAST, it's rate limited at the speed of the fastest replica,
+# if SLOW at the speed of the slowest one.
+# New strategies can be added. Implementors need to implement org.apache.cassandra.net.BackpressureStrategy and
+# provide a public constructor accepting a Map<String, Object>.
+back_pressure_strategy:
+    - class_name: org.apache.cassandra.net.RateBasedBackPressure
+      parameters:
+        - high_ratio: 0.90
+          factor: 5
+          flow: FAST
+
 # Coalescing Strategies #
 # Coalescing multiples messages turns out to significantly boost message processing throughput (think doubling or more).
 # On bare metal, the floor for packet processing throughput is high enough that many applications won't notice, but in
@@ -1024,9 +1262,9 @@
 # See CASSANDRA-8692 for details.
 
 # Strategy to use for coalescing messages in OutboundTcpConnection.
-# Can be fixed, movingaverage, timehorizon (default), disabled.
+# Can be fixed, movingaverage, timehorizon, disabled (default).
 # You can also specify a subclass of CoalescingStrategies.CoalescingStrategy by name.
-# otc_coalescing_strategy: TIMEHORIZON
+# otc_coalescing_strategy: DISABLED
 
 # How many microseconds to wait for coalescing. For fixed strategy this is the amount of time after the first
 # message is received before it will be sent with any accompanying messages. For moving average this is the
@@ -1044,4 +1282,21 @@
 # time and queue contention while iterating the backlog of messages.
 # An interval of 0 disables any wait time, which is the behavior of former Cassandra versions.
 #
-# otc_backlog_expiration_interval_ms: 200
\ No newline at end of file
+# otc_backlog_expiration_interval_ms: 200
+
+
+#########################
+# EXPERIMENTAL FEATURES #
+#########################
+
+# Enables materialized view creation on this node.
+# Materialized views are considered experimental and are not recommended for production use.
+enable_materialized_views: true
+
+# Enables SASI index creation on this node.
+# SASI indexes are considered experimental and are not recommended for production use.
+enable_sasi_indexes: true
+
+# Enables the used of 'ALTER ... DROP COMPACT STORAGE' statements on this node.
+# 'ALTER ... DROP COMPACT STORAGE' is considered experimental and is not recommended for production use.
+enable_drop_compact_storage: false
diff --git a/conf/cqlshrc.sample b/conf/cqlshrc.sample
index cb02b04..0bf926f 100644
--- a/conf/cqlshrc.sample
+++ b/conf/cqlshrc.sample
@@ -35,9 +35,10 @@
 ;; Display timezone
 ;timezone = Etc/UTC
 
-;; The number of digits displayed after the decimal point
+;; The number of digits displayed after the decimal point for single and double precision numbers
 ;; (note that increasing this to large numbers can result in unusual values)
-; float_precision = 5
+;float_precision = 5
+;double_precision = 12
 
 ;; Used for automatic completion and suggestions
 ; completekey = tab
@@ -76,6 +77,9 @@
 ;; The port to connect to (9042 is the native protocol default)
 port = 9042
 
+;; Always connect using SSL - false by default
+; ssl = true
+
 ;; A timeout in seconds for opening new connections
 ; timeout = 10
 
diff --git a/conf/jvm.options b/conf/jvm.options
index eb2ad19..01bb168 100644
--- a/conf/jvm.options
+++ b/conf/jvm.options
@@ -8,6 +8,143 @@
 # - dynamic flags will be appended to these on cassandra-env              #
 ###########################################################################
 
+######################
+# STARTUP PARAMETERS #
+######################
+
+# Uncomment any of the following properties to enable specific startup parameters
+
+# In a multi-instance deployment, multiple Cassandra instances will independently assume that all
+# CPU processors are available to it. This setting allows you to specify a smaller set of processors
+# and perhaps have affinity.
+#-Dcassandra.available_processors=number_of_processors
+
+# The directory location of the cassandra.yaml file.
+#-Dcassandra.config=directory
+
+# Sets the initial partitioner token for a node the first time the node is started.
+#-Dcassandra.initial_token=token
+
+# Set to false to start Cassandra on a node but not have the node join the cluster.
+#-Dcassandra.join_ring=true|false
+
+# Set to false to clear all gossip state for the node on restart. Use when you have changed node
+# information in cassandra.yaml (such as listen_address).
+#-Dcassandra.load_ring_state=true|false
+
+# Enable pluggable metrics reporter. See Pluggable metrics reporting in Cassandra 2.0.2.
+#-Dcassandra.metricsReporterConfigFile=file
+
+# Set the port on which the CQL native transport listens for clients. (Default: 9042)
+#-Dcassandra.native_transport_port=port
+
+# Overrides the partitioner. (Default: org.apache.cassandra.dht.Murmur3Partitioner)
+#-Dcassandra.partitioner=partitioner
+
+# To replace a node that has died, restart a new node in its place specifying the address of the
+# dead node. The new node must not have any data in its data directory, that is, it must be in the
+# same state as before bootstrapping.
+#-Dcassandra.replace_address=listen_address or broadcast_address of dead node
+
+# Allow restoring specific tables from an archived commit log.
+#-Dcassandra.replayList=table
+
+# Allows overriding of the default RING_DELAY (30000ms), which is the amount of time a node waits
+# before joining the ring.
+#-Dcassandra.ring_delay_ms=ms
+
+# Set the port for the Thrift RPC service, which is used for client connections. (Default: 9160)
+#-Dcassandra.rpc_port=port
+
+# Set the SSL port for encrypted communication. (Default: 7001)
+#-Dcassandra.ssl_storage_port=port
+
+# Enable or disable the native transport server. See start_native_transport in cassandra.yaml.
+# cassandra.start_native_transport=true|false
+
+# Enable or disable the Thrift RPC server. (Default: true)
+#-Dcassandra.start_rpc=true/false
+
+# Set the port for inter-node communication. (Default: 7000)
+#-Dcassandra.storage_port=port
+
+# Set the default location for the trigger JARs. (Default: conf/triggers)
+#-Dcassandra.triggers_dir=directory
+
+# For testing new compaction and compression strategies. It allows you to experiment with different
+# strategies and benchmark write performance differences without affecting the production workload. 
+#-Dcassandra.write_survey=true
+
+# To disable configuration via JMX of auth caches (such as those for credentials, permissions and
+# roles). This will mean those config options can only be set (persistently) in cassandra.yaml
+# and will require a restart for new values to take effect.
+#-Dcassandra.disable_auth_caches_remote_configuration=true
+
+# To disable dynamic calculation of the page size used when indexing an entire partition (during
+# initial index build/rebuild). If set to true, the page size will be fixed to the default of
+# 10000 rows per page.
+#-Dcassandra.force_default_indexing_page_size=true
+
+########################
+# GENERAL JVM SETTINGS #
+########################
+
+# enable assertions. highly suggested for correct application functionality.
+-ea
+
+# enable thread priorities, primarily so we can give periodic tasks
+# a lower priority to avoid interfering with client workload
+-XX:+UseThreadPriorities
+
+# allows lowering thread priority without being root on linux - probably
+# not necessary on Windows but doesn't harm anything.
+# see http://tech.stolsvik.com/2010/01/linux-java-thread-priorities-workar
+-XX:ThreadPriorityPolicy=42
+
+# Enable heap-dump if there's an OOM
+-XX:+HeapDumpOnOutOfMemoryError
+
+# Per-thread stack size.
+-Xss256k
+
+# Larger interned string table, for gossip's benefit (CASSANDRA-6410)
+-XX:StringTableSize=1000003
+
+# Make sure all memory is faulted and zeroed on startup.
+# This helps prevent soft faults in containers and makes
+# transparent hugepage allocation more effective.
+-XX:+AlwaysPreTouch
+
+# Disable biased locking as it does not benefit Cassandra.
+-XX:-UseBiasedLocking
+
+# Enable thread-local allocation blocks and allow the JVM to automatically
+# resize them at runtime.
+-XX:+UseTLAB
+-XX:+ResizeTLAB
+-XX:+UseNUMA
+
+# http://www.evanjones.ca/jvm-mmap-pause.html
+-XX:+PerfDisableSharedMem
+
+# Prefer binding to IPv4 network intefaces (when net.ipv6.bindv6only=1). See
+# http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6342561 (short version:
+# comment out this entry to enable IPv6 support).
+-Djava.net.preferIPv4Stack=true
+
+### Debug options
+
+# uncomment to enable flight recorder
+#-XX:+UnlockCommercialFeatures
+#-XX:+FlightRecorder
+
+# uncomment to have Cassandra JVM listen for remote debuggers/profilers on port 1414
+#-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1414
+
+# uncomment to have Cassandra JVM log internal method compilation (developers only)
+#-XX:+UnlockDiagnosticVMOptions
+#-XX:+LogCompilation
+
 #################
 # HEAP SETTINGS #
 #################
diff --git a/conf/logback-tools.xml b/conf/logback-tools.xml
index e47985e..12b59fb 100644
--- a/conf/logback-tools.xml
+++ b/conf/logback-tools.xml
@@ -21,7 +21,7 @@
   <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
     <target>System.err</target>
     <encoder>
-      <pattern>%-5level %date{HH:mm:ss,SSS} %msg%n</pattern>
+      <pattern>%-5level %date{"HH:mm:ss,SSS"} %msg%n</pattern>
     </encoder>
     <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
       <level>WARN</level>
diff --git a/conf/logback.xml b/conf/logback.xml
index 9f1e49a..7bd1c6d 100644
--- a/conf/logback.xml
+++ b/conf/logback.xml
@@ -80,7 +80,7 @@
       <level>INFO</level>
     </filter>
     <encoder>
-      <pattern>%-5level %date{HH:mm:ss,SSS} %msg%n</pattern>
+      <pattern>%-5level [%thread] %date{ISO8601} %F:%L - %msg%n</pattern>
     </encoder>
   </appender>
 
diff --git a/debian/changelog b/debian/changelog
index fe2bb9b..34f8b53 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,170 +1,140 @@
-cassandra (3.0.29) unstable; urgency=medium
+cassandra (3.11.16) UNRELEASED; urgency=medium
 
   * New release
 
- -- Stefan Miklosovic <smiklosovic@apache.org>  Fri, 05 May 2023 13:22:25 +0200
+ -- Brandon Williams <brandonwilliams@apache.org>  Fri, 05 May 2023 05:56:38 -0500
 
-cassandra (3.0.28) unstable; urgency=medium
+cassandra (3.11.15) unstable; urgency=medium
 
   * New release
 
- -- Mick Semb Wever <mck@apache.org>  Wed, 19 Oct 2022 09:58:43 +0200
+ -- Stefan Miklosovic <smiklosovic@apache.org>  Fri, 28 Apr 2023 12:29:32 +0200
 
-cassandra (3.0.27) unstable; urgency=medium
+cassandra (3.11.14) unstable; urgency=medium
 
   * New release
 
- -- Mick Semb Wever <mck@apache.org>  Fri, 06 May 2022 16:19:37 +0200
+ -- Mick Semb Wever <mck@apache.org>  Wed, 19 Oct 2022 10:27:23 +0200
 
-cassandra (3.0.26) unstable; urgency=medium
+cassandra (3.11.13) unstable; urgency=medium
 
   * New release
 
- -- Mick Semb Wever <mck@apache.org>  Mon, 07 Feb 2022 13:12:52 +0100
+ -- Mick Semb Wever <mck@apache.org>  Fri, 06 May 2022 17:04:47 +0200
 
-cassandra (3.0.25) unstable; urgency=medium
+cassandra (3.11.12) unstable; urgency=medium
 
   * New release
 
- -- Brandon Williams <brandonwilliams@apache.org>  Sun, 25 Jul 2021 11:43:54 -0500
+ -- Mick Semb Wever <mck@apache.org>  Mon, 07 Feb 2022 13:55:35 +0100
 
-cassandra (3.0.24) unstable; urgency=medium
+cassandra (3.11.11) unstable; urgency=medium
 
   * New release
 
- -- Alex Petrov <ifesdjeen@apache.org>  Fri, 29 Jan 2021 12:21:05 +0100
+ -- Brandon Williams <brandonwilliams@apache.org>  Sun, 25 Jul 2021 12:39:10 -0500
 
-cassandra (3.0.23) unstable; urgency=medium
+cassandra (3.11.10) unstable; urgency=medium
 
   * New release
 
- -- Mick Semb Wever <mck@apache.org>  Thu, 29 Oct 2020 12:03:40 +0100
+ -- Alex Petrov <ifesdjeen@apache.org>  Fri, 29 Jan 2021 13:27:39 +0100
 
-cassandra (3.0.22) unstable; urgency=medium
+cassandra (3.11.9) unstable; urgency=medium
 
   * New release
 
- -- Mick Semb Wever <mck@apache.org>  Fri, 28 Aug 2020 14:48:07 +0200
+ -- Mick Semb Wever <mck@apache.org>  Thu, 29 Oct 2020 12:32:12 +0100
 
-cassandra (3.0.21) unstable; urgency=medium
+cassandra (3.11.8) unstable; urgency=medium
 
   * New release
 
- -- Mick Semb Wever <mck@apache.org>  Tue, 14 Jul 2020 22:15:16 +0200
+ -- Mick Semb Wever <mck@apache.org>  Fri, 28 Aug 2020 15:12:12 +0200
 
-cassandra (3.0.20) unstable; urgency=medium
+cassandra (3.11.7) unstable; urgency=medium
 
   * New release
 
- -- Mick Semb Wever <mck@apache.org>  Mon, 10 Feb 2020 22:53:30 +0100
+ -- Mick Semb Wever <mck@apache.org>  Tue, 14 Jul 2020 22:58:55 +0200
 
-cassandra (3.0.19) unstable; urgency=medium
+cassandra (3.11.6) unstable; urgency=medium
 
   * New release
 
- -- Michael Shuler <mshuler@apache.org>  Thu, 24 Oct 2019 08:56:34 -0500
+ -- Mick Semb Wever <mck@apache.org>  Mon, 10 Feb 2020 23:54:00 +0100
 
-cassandra (3.0.18) unstable; urgency=medium
+cassandra (3.11.5) unstable; urgency=medium
 
   * New release
 
- -- Michael Shuler <mshuler@apache.org>  Fri, 01 Feb 2019 12:37:12 -0600
+ -- Michael Shuler <mshuler@apache.org>  Thu, 24 Oct 2019 08:59:29 -0500
 
-cassandra (3.0.17) unstable; urgency=medium
+cassandra (3.11.4) unstable; urgency=medium
 
   * New release
 
- -- Michael Shuler <michael@pbandjelly.org>  Mon, 02 Jul 2018 13:29:49 -0500
+ -- Michael Shuler <mshuler@apache.org>  Fri, 01 Feb 2019 12:39:21 -0600
 
-cassandra (3.0.16) unstable; urgency=medium
+cassandra (3.11.3) unstable; urgency=medium
 
   * New release
 
- -- Michael Shuler <michael@pbandjelly.org>  Tue, 10 Oct 2017 17:13:31 -0500
+ -- Michael Shuler <michael@pbandjelly.org>  Mon, 02 Jul 2018 13:36:16 -0500
 
-cassandra (3.0.15) unstable; urgency=medium
+cassandra (3.11.2) unstable; urgency=medium
 
   * New release
 
- -- Michael Shuler <michael@pbandjelly.org>  Mon, 26 Jun 2017 18:47:08 -0500
+ -- Michael Shuler <michael@pbandjelly.org>  Tue, 10 Oct 2017 17:18:26 -0500
 
-cassandra (3.0.14) unstable; urgency=medium
+cassandra (3.11.1) unstable; urgency=medium
 
   * New release
 
- -- Michael Shuler <michael@pbandjelly.org>  Fri, 14 Apr 2017 20:01:02 -0500
+ -- Michael Shuler <michael@pbandjelly.org>  Mon, 26 Jun 2017 18:51:37 -0500
 
-cassandra (3.0.13) unstable; urgency=medium
+cassandra (3.11.0) unstable; urgency=medium
 
   * New release
 
- -- Michael Shuler <michael@pbandjelly.org>  Tue, 11 Apr 2017 12:52:26 -0500
+ -- Michael Shuler <michael@pbandjelly.org>  Mon, 19 Jun 2017 17:42:32 -0500
 
-cassandra (3.0.12) unstable; urgency=medium
+cassandra (3.10) unstable; urgency=medium
 
   * New release
 
- -- Michael Shuler <michael@pbandjelly.org>  Tue, 07 Mar 2017 09:28:21 -0600
+ -- Michael Shuler <michael@pbandjelly.org>  Mon, 31 Oct 2016 08:54:45 -0500
 
-cassandra (3.0.11) unstable; urgency=medium
+cassandra (3.8) unstable; urgency=medium
 
   * New release
 
- -- Michael Shuler <michael@pbandjelly.org>  Wed, 15 Feb 2017 18:15:14 -0600
+ -- Michael Shuler <mshuler@apache.org>  Mon, 26 Sep 2016 08:51:39 -0500
 
-cassandra (3.0.10) unstable; urgency=medium
+cassandra (3.6) unstable; urgency=medium
 
   * New release
 
- -- Michael Shuler <michael@pbandjelly.org>  Mon, 31 Oct 2016 10:33:44 -0500
+ -- Jake Luciani <jake@apache.org>  Tue, 03 May 2016 09:12:18 -0400
 
-cassandra (3.0.9) unstable; urgency=medium
+cassandra (3.4) unstable; urgency=medium
 
   * New release
 
- -- Michael Shuler <mshuler@apache.org>  Tue, 16 Aug 2016 18:02:20 -0500
+ -- Jake Luciani <jake@apache.org>  Mon, 29 Feb 2016 10:39:33 -0500
 
-cassandra (3.0.8) unstable; urgency=medium
-
-  * New release 
-
- -- Jake Luciani <jake@apache.org>  Tue, 28 Jun 2016 20:12:22 -0400
-
-cassandra (3.0.7) unstable; urgency=medium
+cassandra (3.2) unstable; urgency=medium
 
   * New release
 
- -- Jake Luciani <jake@apache.org>  Mon, 06 Jun 2016 14:27:28 -0400
+ -- Jake Luciani <jake@apache.org>  Tue, 05 Jan 2016 10:24:04 -0500
 
-cassandra (3.0.6) unstable; urgency=medium
+cassandra (3.1) unstable; urgency=medium
 
   * New release
 
- -- Jake Luciani <jake@apache.org>  Tue, 03 May 2016 09:29:31 -0400
-
-cassandra (3.0.5) unstable; urgency=medium
-
-  * New release
-
- -- Jake Luciani <jake@apache.org>  Sat, 02 Apr 2016 07:57:16 -0400
-
-cassandra (3.0.4) unstable; urgency=medium
-
-  * New release
-
- -- Jake Luciani <jake@apache.org>  Mon, 29 Feb 2016 10:36:33 -0500
-
-cassandra (3.0.3) unstable; urgency=medium
-
-  * New release 
-
- -- Jake Luciani <jake@apache.org>  Wed, 03 Feb 2016 08:54:57 -0500
-
-cassandra (3.0.1) unstable; urgency=medium
-
-  * New release
-
- -- Jake Luciani <jake@apache.org>  Fri, 04 Dec 2015 15:56:02 -0500
+ -- Jake Luciani <jake@apache.org>  Fri, 04 Dec 2015 16:00:04 -0500
 
 cassandra (3.0.0) unstable; urgency=medium
 
diff --git a/debian/copyright b/debian/copyright
index f8b42d0..d132a6c 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -1,7 +1,7 @@
 This package was debianized by Eric Evans <eevans@apache.org> on
 Sun, 26 Jul 2009 15:12:10 +0000
 
-Upstream Author: Cassandra Developers <cassandra-dev@incubator.apache.org>
+Upstream Author: Cassandra Developers <dev@cassandra.apache.org>
 
 Copyright (c) 2009, The Apache Software Foundation.
 
diff --git a/debian/nodetool-completion b/debian/nodetool-completion
index f6f3d5b..732f4f4 100644
--- a/debian/nodetool-completion
+++ b/debian/nodetool-completion
@@ -69,14 +69,16 @@
             enablebinary
             enablegossip
             enablehandoff
-            enablehintsfordc
             enablethrift
+            enablehintsfordc
             failuredetector
             gcstats
             getcompactionthroughput
+            getconcurrentcompactors
             getinterdcstreamthroughput
             getlogginglevels
             getstreamthroughput
+            gettimeout
             gettraceprobability
             gossipinfo
             help
@@ -95,15 +97,17 @@
             resetlocalschema
             resumehandoff
             ring
+            setconcurrentcompactors
             sethintedhandoffthrottlekb
             setinterdcstreamthroughput
             setlogginglevel
+            settimeout
             status
             statusbackup
             statusbinary
+            statusthrift
             statusgossip
             statushandoff
-            statusthrift
             stopdaemon
             tablestats
             tpstats
@@ -119,6 +123,7 @@
             disableautocompaction
             enableautocompaction
             flush
+            garbagecollect
             getcompactionthreshold
             getendpoints
             getsstables
@@ -128,6 +133,7 @@
             rebuild
             rebuild_index
             refresh
+            relocatesstables
             removenode
             repair
             scrub
@@ -144,6 +150,7 @@
             truncatehints
             upgradesstables
             verify
+            viewbuildstatus
 	    '
 
         local optwks='
@@ -152,11 +159,13 @@
             compact
             describering
             flush
+            garbagecollect
             getcompactionthreshold
             getendpoints
             getsstables
             rebuild_index
             refresh
+            relocatesstables
             repair
             scrub
             setcompactionthreshold
@@ -164,6 +173,7 @@
             tablehistograms
             toppartitions
             verify
+            viewbuildstatus
             '
 
         local optwcfs='
@@ -172,6 +182,8 @@
             disableautocompaction
             enableautocompaction
             flush
+            garbagecollect
+            relocatesstables
             repair
             scrub
             toppartitions
@@ -218,7 +230,7 @@
             fi
         elif [[ $COMP_CWORD -eq 3 ]] ; then
             case "${COMP_WORDS[1]}" in
-                cleanup|compact|flush|getcompactionthreshold|getendpoints|getsstables|rebuild_index|refresh|repair|scrub|setcompactionthreshold|tablehistograms|toppartitions|verify)
+                cleanup|compact|flush|garbagecollect|getcompactionthreshold|getendpoints|getsstables|rebuild_index|refresh|relocatesstables|repair|scrub|setcompactionthreshold|tablehistograms|toppartitions|verify)
                     show_cfs ${prev} ${cur}
                     return 0
                     ;;
diff --git a/debian/patches/002cassandra_logdir_fix.dpatch b/debian/patches/002cassandra_logdir_fix.dpatch
index 2a3a47d..a0ff45f 100755
--- a/debian/patches/002cassandra_logdir_fix.dpatch
+++ b/debian/patches/002cassandra_logdir_fix.dpatch
@@ -25,7 +25,7 @@
  # Sets the path where logback and GC logs are written.
  if [ "x$CASSANDRA_LOG_DIR" = "x" ] ; then
 -    CASSANDRA_LOG_DIR="$CASSANDRA_HOME/logs"
-+    CASSANDRA_LOG_DIR="/var/log/cassandra/"
++    CASSANDRA_LOG_DIR="/var/log/cassandra"
  fi
 
  #GC log path has to be defined here because it needs to access CASSANDRA_HOME
\ No newline at end of file
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..43acc1e
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,26 @@
+# 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.
+
+GENERATE_NODETOOL_DOCS = ./scripts/gen-nodetool-docs.py
+MAKE_CASSANDRA_YAML = ./scripts/convert_yaml_to_adoc.py ../conf/cassandra.yaml ./modules/cassandra/pages/configuration/cass_yaml_file.adoc
+
+.PHONY: html
+html:
+	@# hack until a local basic antora build is put in
+
+.PHONY: gen-asciidoc
+gen-asciidoc:
+	@mkdir -p modules/cassandra/pages/tools/nodetool
+	@mkdir -p modules/cassandra/examples/TEXT/NODETOOL
+	python3 $(GENERATE_NODETOOL_DOCS)
+	python3 $(MAKE_CASSANDRA_YAML)
diff --git a/doc/README.md b/doc/README.md
new file mode 100644
index 0000000..608d236
--- /dev/null
+++ b/doc/README.md
@@ -0,0 +1,61 @@
+<!--
+#
+# 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.
+#
+-->
+
+Apache Cassandra documentation directory
+========================================
+
+This directory contains the documentation maintained in-tree for Apache
+Cassandra. This directory contains the following documents:
+- The source of the official Cassandra documentation, in the `source/modules`
+  subdirectory. See below for more details on how to edit/build that
+  documentation.
+- The specification(s) for the supported versions of native transport protocol.
+
+
+Official documentation
+----------------------
+
+The source for the official documentation for Apache Cassandra can be found in
+the `modules/cassandra/pages` subdirectory. The documentation uses [antora](http://www.antora.org/)
+and is thus written in [asciidoc](http://asciidoc.org).
+
+To generate the asciidoc files for cassandra.yaml and the nodetool commands, run (from project root):
+```bash
+ant gen-asciidoc
+```
+or (from this directory):
+
+```bash
+make gen-asciidoc
+```
+
+
+(The following has not yet been implemented, for now see the build instructions in the [cassandra-website](https://github.com/apache/cassandra-website) repo.)
+To build the documentation, run (from project root):
+
+```bash
+ant gen-doc
+```
+or (from this directory):
+
+```bash
+make html
+```
+
diff --git a/doc/antora.yml b/doc/antora.yml
new file mode 100644
index 0000000..4ce17d5
--- /dev/null
+++ b/doc/antora.yml
@@ -0,0 +1,18 @@
+name: Cassandra
+title: Cassandra
+version: '3.11'
+display_version: '3.11'
+asciidoc:
+  attributes:
+    sectanchors: ''
+    sectlinks: ''
+    cass_url: 'http://cassandra.apache.org/'
+    cass-docker-tag-3x: latest
+    cass-tag-3x: '3.11'
+    311_version: '3.11.10'
+    30_version: '3.0.24'
+    22_version: '2.2.19'
+    21_version: '2.1.22'
+nav:
+- modules/ROOT/nav.adoc
+- modules/cassandra/nav.adoc
diff --git a/doc/cql3/CQL.textile b/doc/cql3/CQL.textile
index 7c7afa7..26cf66a 100644
--- a/doc/cql3/CQL.textile
+++ b/doc/cql3/CQL.textile
@@ -18,9 +18,7 @@
 #
 -->
 
-<link rel="StyleSheet" href="CQL.css" type="text/css" media="screen">
-
-h1. Cassandra Query Language (CQL) v3.4.0
+h1. Cassandra Query Language (CQL) v3.4.4
 
 
 
@@ -418,7 +416,9 @@
 <alter-table-stmt> ::= ALTER (TABLE | COLUMNFAMILY) <tablename> <instruction>
 
 <instruction> ::= ADD   <identifier> <type>
+                | ADD   ( <identifier> <type> ( , <identifier> <type> )* )
                 | DROP  <identifier>
+                | DROP  ( <identifier> ( , <identifier> )* )
                 | WITH  <option> ( AND <option> )*
 p. 
 __Sample:__
@@ -440,6 +440,28 @@
 * @DROP@: Removes a column from the table. Dropped columns will immediately become unavailable in the queries and will not be included in compacted sstables in the future. If a column is readded, queries won't return values written before the column was last dropped. It is assumed that timestamps represent actual time, so if this is not your case, you should NOT readd previously dropped columns. Columns can't be dropped from tables defined with the @COMPACT STORAGE@ option.
 * @WITH@: Allows to update the options of the table. The "supported @<option>@":#createTableOptions (and syntax) are the same as for the @CREATE TABLE@ statement except that @COMPACT STORAGE@ is not supported. Note that setting any @compaction@ sub-options has the effect of erasing all previous @compaction@ options, so you  need to re-specify all the sub-options if you want to keep them. The same note applies to the set of @compression@ sub-options.
 
+h4. CQL type compatibility:
+
+CQL data types may be converted only as the following table.
+
+|_. Data type may be altered to:|_.Data type|
+|timestamp|bigint|
+|ascii, bigint, boolean, date, decimal, double, float, inet, int, smallint, text, time, timestamp, timeuuid, tinyint, uuid, varchar, varint|blob|
+|int|date|
+|ascii, varchar|text|
+|bigint|time|
+|bigint|timestamp|
+|timeuuid|uuid|
+|ascii, text|varchar|
+|bigint, int, timestamp|varint|
+
+Clustering columns have stricter requirements, only the below conversions are allowed.
+
+|_. Data type may be altered to:|_.Data type|
+|ascii, text, varchar|blob|
+|ascii, varchar|text|
+|ascii, text|varchar|
+
 h3(#dropTableStmt). DROP TABLE
 
 __Syntax:__
@@ -557,7 +579,7 @@
 bc(syntax). <alter-materialized-view-stmt> ::= ALTER MATERIALIZED VIEW <viewname>
                                                  WITH <option> ( AND <option> )*
 
-p.
+p. 
 The @ALTER MATERIALIZED VIEW@ statement allows options to be update; these options are the same as <a href="#createTableOptions">@CREATE TABLE@'s options</a>.
 
 
@@ -841,16 +863,13 @@
 bc(syntax).. 
 <insertStatement> ::= INSERT INTO <tablename>
                       ( ( <name-list> VALUES <value-list> )
-                      | ( JSON <string> ))
+                      | ( JSON <string> [ DEFAULT ( NULL | UNSET ) ]))
                       ( IF NOT EXISTS )?
                       ( USING <option> ( AND <option> )* )?
 
 <names-list> ::= '(' <identifier> ( ',' <identifier> )* ')'
 
-<value-list> ::= '(' <term-or-literal> ( ',' <term-or-literal> )* ')'
-
-<term-or-literal> ::= <term>
-                    | <collection-literal>
+<value-list> ::= '(' <term> ( ',' <term> )* ')'
 
 <option> ::= TIMESTAMP <integer>
            | TTL <integer>
@@ -889,13 +908,17 @@
                | <identifier> '=' <identifier> ('+' | '-') (<int-term> | <set-literal> | <list-literal>)
                | <identifier> '=' <identifier> '+' <map-literal>
                | <identifier> '[' <term> ']' '=' <term>
+               | <identifier> '.' <field> '=' <term>
 
 <condition> ::= <identifier> <op> <term>
-              | <identifier> IN (<variable> | '(' ( <term> ( ',' <term> )* )? ')')
+              | <identifier> IN <in-values>
               | <identifier> '[' <term> ']' <op> <term>
-              | <identifier> '[' <term> ']' IN <term>
+              | <identifier> '[' <term> ']' IN <in-values>
+              | <identifier> '.' <field> <op> <term>
+              | <identifier> '.' <field> IN <in-values>
 
 <op> ::= '<' | '<=' | '=' | '!=' | '>=' | '>'
+<in-values> ::= (<variable> | '(' ( <term> ( ',' <term> )* )? ')')
 
 <where-clause> ::= <relation> ( AND <relation> )*
 
@@ -932,11 +955,13 @@
 
 The @id = id + <collection-literal>@ and @id[value1] = value2@ forms of @<assignment>@ are for collections. Please refer to the "relevant section":#collections for more details.
 
+The @id.field = <term>@ form of @<assignemt>@ is for setting the value of a single field on a non-frozen user-defined types.
+
 h4(#updateOptions). @<options>@
 
 The @UPDATE@ and @INSERT@ statements support the following options:
 * @TIMESTAMP@: sets the timestamp for the operation. If not specified, the coordinator will use the current time (in microseconds) at the start of statement execution as the timestamp. This is usually a suitable default.
-* @TTL@: specifies an optional Time To Live (in seconds) for the inserted values. If set, the inserted values are automatically removed from the database after the specified time. Note that the TTL concerns the inserted values, not the columns themselves. This means that any subsequent update of the column will also reset the TTL (to whatever TTL is specified in that update). By default, values never expire. A TTL of 0 or a negative value is equivalent to no TTL.
+* @TTL@: specifies an optional Time To Live (in seconds) for the inserted values. If set, the inserted values are automatically removed from the database after the specified time. Note that the TTL concerns the inserted values, not the columns themselves. This means that any subsequent update of the column will also reset the TTL (to whatever TTL is specified in that update). By default, values never expire. A TTL of 0 is equivalent to no TTL. If the table has a default_time_to_live, a TTL of 0 will remove the TTL for the inserted or updated values. A TTL of @null@ is equivalent to inserting with a TTL of 0.
 
 
 h3(#deleteStmt). DELETE
@@ -950,7 +975,9 @@
                   WHERE <where-clause>
                   ( IF ( EXISTS | ( <condition> ( AND <condition> )*) ) )?
 
-<selection> ::= <identifier> ( '[' <term> ']' )?
+<selection> ::= <identifier>
+              | <identifier> '[' <term> ']'
+              | <identifier> '.' <field>
 
 <where-clause> ::= <relation> ( AND <relation> )*
 
@@ -962,11 +989,14 @@
              | '(' <identifier> (',' <identifier>)* ')' IN <variable>
 
 <op> ::= '=' | '<' | '>' | '<=' | '>='
+<in-values> ::= (<variable> | '(' ( <term> ( ',' <term> )* )? ')')
 
 <condition> ::= <identifier> (<op> | '!=') <term>
-              | <identifier> IN (<variable> | '(' ( <term> ( ',' <term> )* )? ')')
+              | <identifier> IN <in-values>
               | <identifier> '[' <term> ']' (<op> | '!=') <term>
-              | <identifier> '[' <term> ']' IN <term>
+              | <identifier> '[' <term> ']' IN <in-values>
+              | <identifier> '.' <field> (<op> | '!=') <term>
+              | <identifier> '.' <field> IN <in-values>
 
 p. 
 __Sample:__
@@ -976,7 +1006,7 @@
 
 DELETE phone FROM Users WHERE userid IN (C73DE1D3-AF08-40F3-B124-3FF3E5109F22, B70DE1D0-9908-4AE3-BE34-5573E5B09F14);
 p. 
-The @DELETE@ statement deletes columns and rows. If column names are provided directly after the @DELETE@ keyword, only those columns are deleted from the row indicated by the @<where-clause>@ (the @id[value]@ syntax in @<selection>@ is for collection, please refer to the "collection section":#collections for more details).  Otherwise, whole rows are removed. The @<where-clause>@ specifies which rows are to be deleted.  Multiple rows may be deleted with one statement by using an @IN@ clause.  A range of rows may be deleted using an inequality operator (such as @>=@).
+The @DELETE@ statement deletes columns and rows. If column names are provided directly after the @DELETE@ keyword, only those columns are deleted from the row indicated by the @<where-clause>@.  The @id[value]@ syntax in @<selection>@ is for non-frozen collections (please refer to the "collection section":#collections for more details).  The @id.field@ syntax is for the deletion of non-frozen user-defined types.  Otherwise, whole rows are removed. The @<where-clause>@ specifies which rows are to be deleted.  Multiple rows may be deleted with one statement by using an @IN@ clause.  A range of rows may be deleted using an inequality operator (such as @>=@).
 
 @DELETE@ supports the @TIMESTAMP@ option with the same semantics as the "@UPDATE@":#updateStmt statement.
 
@@ -1047,19 +1077,23 @@
 <select-stmt> ::= SELECT ( JSON )? <select-clause>
                   FROM <tablename>
                   ( WHERE <where-clause> )?
+                  ( GROUP BY <group-by>)?
                   ( ORDER BY <order-by> )?
+                  ( PER PARTITION LIMIT <integer> )?
                   ( LIMIT <integer> )?
                   ( ALLOW FILTERING )?
 
 <select-clause> ::= DISTINCT? <selection-list>
-                  | COUNT '(' ( '*' | '1' ) ')' (AS <identifier>)?
 
 <selection-list> ::= <selector> (AS <identifier>)? ( ',' <selector> (AS <identifier>)? )*
                    | '*'
 
 <selector> ::= <identifier>
+             | <term>
              | WRITETIME '(' <identifier> ')'
+             | COUNT '(' '*' ')'
              | TTL '(' <identifier> ')'
+             | CAST '(' <selector> AS <type> ')'
              | <function> '(' (<selector> (',' <selector>)*)? ')'
 
 <where-clause> ::= <relation> ( AND <relation> )*
@@ -1071,6 +1105,7 @@
              | TOKEN '(' <identifier> ( ',' <identifer>)* ')' <op> <term>
 
 <op> ::= '=' | '<' | '>' | '<=' | '>=' | CONTAINS | CONTAINS KEY
+<group-by> ::= <identifier> (',' <identifier>)*
 <order-by> ::= <ordering> ( ',' <odering> )*
 <ordering> ::= <identifer> ( ASC | DESC )?
 <term-tuple> ::= '(' <term> (',' <term>)* ')'
@@ -1101,7 +1136,7 @@
 
 The @<select-clause>@ determines which columns needs to be queried and returned in the result-set. It consists of either the comma-separated list of <selector> or the wildcard character (@*@) to select all the columns defined for the table. Please note that for wildcard @SELECT@ queries the order of columns returned is not specified and is not guaranteed to be stable between Cassandra versions.
 
-A @<selector>@ is either a column name to retrieve or a @<function>@ of one or more @<term>@s. The function allowed are the same as for @<term>@ and are described in the "function section":#functions. In addition to these generic functions, the @WRITETIME@ (resp. @TTL@) function allows to select the timestamp of when the column was inserted (resp. the time to live (in seconds) for the column (or null if the column has no expiration set)). The `WRITETIME` and `TTL` functions can't be used on multi-cell columns such as non-frozen collections or non-frozen user-defined types.
+A @<selector>@ is either a column name to retrieve or a @<function>@ of one or more @<term>@s. The function allowed are the same as for @<term>@ and are described in the "function section":#functions. In addition to these generic functions, the @WRITETIME@ (resp. @TTL@) function allows to select the timestamp of when the column was inserted (resp. the time to live (in seconds) for the column (or null if the column has no expiration set)) and the "@CAST@":#castFun function can be used to convert one data type to another.
 
 Any @<selector>@ can be aliased using @AS@ keyword (see examples). Please note that @<where-clause>@ and @<order-by>@ clause should refer to the columns by their original names and not by their aliases.
 
@@ -1165,9 +1200,19 @@
 * if the table has been defined without any specific @CLUSTERING ORDER@, then then allowed orderings are the order induced by the clustering columns and the reverse of that one.
 * otherwise, the orderings allowed are the order of the @CLUSTERING ORDER@ option and the reversed one.
 
-h4(#selectLimit). @LIMIT@
+h4(#selectGroupBy). @<group-by>@
 
-The @LIMIT@ option to a @SELECT@ statement limits the number of rows returned by a query.
+The @GROUP BY@ option allows to condense into a single row all selected rows that share the same values for a set of columns.
+
+Using the @GROUP BY@ option, it is only possible to group rows at the partition key level or at a clustering column level. By consequence, the @GROUP BY@ option only accept as arguments primary key column names in the primary key order. If a primary key column is restricted by an equality restriction it is not required to be present in the @GROUP BY@ clause.
+
+Aggregate functions will produce a separate value for each group. If no @GROUP BY@ clause is specified, aggregates functions will produce a single value for all the rows.
+
+If a column is selected without an aggregate function, in a statement with a @GROUP BY@, the first value encounter in each group will be returned.
+
+h4(#selectLimit). @LIMIT@ and @PER PARTITION LIMIT@
+
+The @LIMIT@ option to a @SELECT@ statement limits the number of rows returned by a query, while the @PER PARTITION LIMIT@ option limits the number of rows returned for a given partition by the query. Note that both type of limit can used in the same statement.
 
 h4(#selectAllowFiltering). @ALLOW FILTERING@
 
@@ -1458,6 +1503,7 @@
 * The hierarchy of Data resources, Keyspaces and Tables has the structure @ALL KEYSPACES@ -> @KEYSPACE@ -> @TABLE@
 * Function resources have the structure @ALL FUNCTIONS@ -> @KEYSPACE@ -> @FUNCTION@
 * Resources representing roles have the structure @ALL ROLES@ -> @ROLE@
+* Resources representing JMX ObjectNames, which map to sets of MBeans/MXBeans, have the structure @ALL MBEANS@ -> @MBEAN@
 
 Permissions can be granted at any level of these hierarchies and they flow downwards. So granting a permission on a resource higher up the chain automatically grants that same permission on all resources lower down. For example, granting @SELECT@ on a @KEYSPACE@ automatically grants it on all @TABLES@ in that @KEYSPACE@. Likewise, granting a permission on @ALL FUNCTIONS@ grants it on every defined function, regardless of which keyspace it is scoped in. It is also possible to grant permissions on all functions scoped to a particular keyspace. 
 
@@ -1473,7 +1519,7 @@
 * @DESCRIBE@
 * @EXECUTE@
 
-Not all permissions are applicable to every type of resource. For instance, @EXECUTE@ is only relevant in the context of functions; granting @EXECUTE@ on a resource representing a table is nonsensical. Attempting to @GRANT@ a permission on resource to which it cannot be applied results in an error response. The following illustrates which permissions can be granted on which types of resource, and which statements are enabled by that permission.
+Not all permissions are applicable to every type of resource. For instance, @EXECUTE@ is only relevant in the context of functions or mbeans; granting @EXECUTE@ on a resource representing a table is nonsensical. Attempting to @GRANT@ a permission on resource to which it cannot be applied results in an error response. The following illustrates which permissions can be granted on which types of resource, and which statements are enabled by that permission.
 
 |_. permission |_. resource                   |_. operations        |
 | @CREATE@     | @ALL KEYSPACES@              |@CREATE KEYSPACE@ ==<br>== @CREATE TABLE@ in any keyspace|
@@ -1500,9 +1546,15 @@
 | @SELECT@     | @ALL KEYSPACES@              |@SELECT@ on any table|
 | @SELECT@     | @KEYSPACE@                   |@SELECT@ on any table in keyspace|
 | @SELECT@     | @TABLE@                      |@SELECT@ on specified table|
+| @SELECT@     | @ALL MBEANS@                 |Call getter methods on any mbean|
+| @SELECT@     | @MBEANS@                     |Call getter methods on any mbean matching a wildcard pattern|
+| @SELECT@     | @MBEAN@                      |Call getter methods on named mbean|
 | @MODIFY@     | @ALL KEYSPACES@              |@INSERT@ on any table ==<br>== @UPDATE@ on any table ==<br>== @DELETE@ on any table ==<br>== @TRUNCATE@ on any table|
-| @MODIFY@     | @KEYSPACE@                  |@INSERT@ on any table in keyspace ==<br>== @UPDATE@ on any table in keyspace ==<br>  == @DELETE@ on any table in keyspace ==<br>== @TRUNCATE@ on any table in keyspace
+| @MODIFY@     | @KEYSPACE@                   |@INSERT@ on any table in keyspace ==<br>== @UPDATE@ on any table in keyspace ==<br>  == @DELETE@ on any table in keyspace ==<br>== @TRUNCATE@ on any table in keyspace
 | @MODIFY@     | @TABLE@                      |@INSERT@ ==<br>== @UPDATE@ ==<br>== @DELETE@ ==<br>== @TRUNCATE@|
+| @MODIFY@     | @ALL MBEANS@                 |Call setter methods on any mbean|
+| @MODIFY@     | @MBEANS@                     |Call setter methods on any mbean matching a wildcard pattern|
+| @MODIFY@     | @MBEAN@                      |Call setter methods on named mbean|
 | @AUTHORIZE@  | @ALL KEYSPACES@              |@GRANT PERMISSION@ on any table ==<br>== @REVOKE PERMISSION@ on any table|
 | @AUTHORIZE@  | @KEYSPACE@                   |@GRANT PERMISSION@ on table in keyspace ==<br>== @REVOKE PERMISSION@ on table in keyspace|
 | @AUTHORIZE@  | @TABLE@                      |@GRANT PERMISSION@ ==<br>== @REVOKE PERMISSION@ |
@@ -1510,12 +1562,21 @@
 | @AUTHORIZE@  | @ALL FUNCTIONS IN KEYSPACE@  |@GRANT PERMISSION@ in keyspace ==<br>== @REVOKE PERMISSION@ in keyspace|
 | @AUTHORIZE@  | @ALL FUNCTIONS IN KEYSPACE@  |@GRANT PERMISSION@ in keyspace ==<br>== @REVOKE PERMISSION@ in keyspace|
 | @AUTHORIZE@  | @FUNCTION@                   |@GRANT PERMISSION@ ==<br>== @REVOKE PERMISSION@|
+| @AUTHORIZE@  | @ALL MBEANS@                 |@GRANT PERMISSION@ on any mbean ==<br>== @REVOKE PERMISSION@ on any mbean|
+| @AUTHORIZE@  | @MBEANS@                     |@GRANT PERMISSION@ on any mbean matching a wildcard pattern ==<br>== @REVOKE PERMISSION@ on any mbean matching a wildcard pattern|
+| @AUTHORIZE@  | @MBEAN@                      |@GRANT PERMISSION@ on named mbean ==<br>== @REVOKE PERMISSION@ on named mbean|
 | @AUTHORIZE@  | @ALL ROLES@                  |@GRANT ROLE@ grant any role ==<br>== @REVOKE ROLE@ revoke any role|
 | @AUTHORIZE@  | @ROLES@                      |@GRANT ROLE@ grant role ==<br>== @REVOKE ROLE@ revoke role|
 | @DESCRIBE@   | @ALL ROLES@                  |@LIST ROLES@ all roles or only roles granted to another, specified role|
+| @DESCRIBE@   | @ALL MBEANS                  |Retrieve metadata about any mbean from the platform's MBeanServer|
+| @DESCRIBE@   | @MBEANS                      |Retrieve metadata about any mbean matching a wildcard patter from the platform's MBeanServer|
+| @DESCRIBE@   | @MBEAN                       |Retrieve metadata about a named mbean from the platform's MBeanServer|
 | @EXECUTE@    | @ALL FUNCTIONS@              |@SELECT@, @INSERT@, @UPDATE@ using any function ==<br>== use of any function in @CREATE AGGREGATE@|
 | @EXECUTE@    | @ALL FUNCTIONS IN KEYSPACE@  |@SELECT@, @INSERT@, @UPDATE@ using any function in keyspace ==<br>== use of any function in keyspace in @CREATE AGGREGATE@|
 | @EXECUTE@    | @FUNCTION@                   |@SELECT@, @INSERT@, @UPDATE@ using function ==<br>== use of function in @CREATE AGGREGATE@|
+| @EXECUTE@    | @ALL MBEANS@                 |Execute operations on any mbean|
+| @EXECUTE@    | @MBEANS@                     |Execute operations on any mbean matching a wildcard pattern|
+| @EXECUTE@    | @MBEAN@                      |Execute operations on named mbean|
 
 
 h3(#grantPermissionsStmt). GRANT PERMISSION
@@ -1534,6 +1595,8 @@
              | ROLE <identifier>
              | ALL FUNCTIONS ( IN KEYSPACE <identifier> )?
              | FUNCTION <functionname>
+             | ALL MBEANS
+             | ( MBEAN | MBEANS ) <objectName>
 p. 
 
 __Sample:__ 
@@ -1588,6 +1651,8 @@
              | ROLE <identifier>
              | ALL FUNCTIONS ( IN KEYSPACE <identifier> )?
              | FUNCTION <functionname>
+             | ALL MBEANS
+             | ( MBEAN | MBEANS ) <objectName>
 p. 
 
 __Sample:__ 
@@ -1616,6 +1681,8 @@
              | ROLE <identifier>
              | ALL FUNCTIONS ( IN KEYSPACE <identifier> )?
              | FUNCTION <functionname>
+             | ALL MBEANS
+             | ( MBEAN | MBEANS ) <objectName>
 p. 
 
 __Sample:__
@@ -1637,7 +1704,9 @@
 
 h2(#types). Data Types
 
-CQL supports a rich set of data types for columns defined in a table, including collection types. On top of those native and collection types, users can also provide custom types (through a JAVA class extending @AbstractType@ loadable by Cassandra). The syntax of types is thus:
+CQL supports a rich set of data types for columns defined in a table, including collection types. On top of those native
+and collection types, users can also provide custom types (through a JAVA class extending @AbstractType@ loadable by
+Cassandra). The syntax of types is thus:
 
 bc(syntax).. 
 <type> ::= <native-type>
@@ -1653,6 +1722,7 @@
                 | date
                 | decimal
                 | double
+                | duration
                 | float
                 | inet
                 | int
@@ -1683,6 +1753,7 @@
 |@date@     |   integers, strings  |A date (with no corresponding time value).  See "Working with dates":#usingdates below for more information.|
 |@decimal@  |   integers, floats   |Variable-precision decimal|
 |@double@   |   integers           |64-bit IEEE-754 floating point|
+|@duration@ |   duration           |A duration with nanosecond precision. See "Working with durations":#usingdurations below for details.|
 |@float@    |   integers, floats   |32-bit IEEE-754 floating point|
 |@inet@     |   strings            |An IP address. It can be either 4 bytes long (IPv4) or 16 bytes long (IPv6). There is no @inet@ constant, IP address should be inputed as strings|
 |@int@      |   integers           |32-bit signed int|
@@ -1756,6 +1827,44 @@
 * @08:12:54.123456@
 * @08:12:54.123456789@
 
+h3(#usingdurations). Working with durations
+
+Values of the @duration@ type are encoded as 3 signed integers of variable lengths. The first integer represents the
+number of months, the second the number of days and the third the number of nanoseconds. This is due to the fact that
+the number of days in a month can change, and a day can have 23 or 25 hours depending on the daylight saving.
+
+A duration can be input as:
+
+* (quantity unit)+ like @12h30m@ where the unit can be:
+** @y@: years (12 months)
+** @mo@: months (1 month)
+** @w@: weeks (7 days)
+** @d@: days (1 day)
+** @h@: hours (3,600,000,000,000 nanoseconds)
+** @m@: minutes (60,000,000,000 nanoseconds)
+** @s@: seconds (1,000,000,000 nanoseconds)
+** @ms@: milliseconds (1,000,000 nanoseconds)
+** @us@ or @µs@ : microseconds (1000 nanoseconds)
+** @ns@: nanoseconds (1 nanosecond)
+* ISO 8601 format:  @P[n]Y[n]M[n]DT[n]H[n]M[n]S@ or @P[n]W@
+* ISO 8601 alternative format: @P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]@
+
+For example:
+
+bc(sample). 
+    INSERT INTO RiderResults (rider, race, result) VALUES ('Christopher Froome', 'Tour de France', 89h4m48s);
+    INSERT INTO RiderResults (rider, race, result) VALUES ('BARDET Romain', 'Tour de France', PT89H8M53S);
+    INSERT INTO RiderResults (rider, race, result) VALUES ('QUINTANA Nairo', 'Tour de France', P0000-00-00T89:09:09);
+
+h4. duration-limitation:
+
+Duration columns cannot be used in a table's @PRIMARY KEY@. This limitation is due to the fact that
+durations cannot be ordered. It is effectively not possible to know if @1mo@ is greater than @29d@ without a date
+context.
+
+A @1d@ duration is not equals to a @24h@ one as the duration type has been created to be able to support daylight
+saving.
+
 
 h3(#counters). Counters
 
@@ -1884,6 +1993,37 @@
 
 CQL3 distinguishes between built-in functions (so called 'native functions') and "user-defined functions":#udfs.  CQL3 includes several native functions, described below:
 
+h3(#castFun). Cast
+
+The @cast@ function can be used to converts one native datatype to another.
+
+The following table describes the conversions supported by the @cast@ function. Cassandra will silently ignore any cast converting a datatype into its own datatype.
+
+|_. from    |_. to   |
+|@ascii@   |@text@, @varchar@                                                                                    |
+|@bigint@   |@tinyint@, @smallint@, @int@, @float@, @double@, @decimal@, @varint@, @text@, @varchar@             |
+|@boolean@  |@text@, @varchar@                                                                                   |
+|@counter@  |@tinyint@, @smallint@, @int@, @bigint@, @float@, @double@, @decimal@, @varint@, @text@, @varchar@   |
+|@date@      |@timestamp@                                                                                        |
+|@decimal@  |@tinyint@, @smallint@, @int@, @bigint@, @float@, @double@, @varint@, @text@, @varchar@              |
+|@double@   |@tinyint@, @smallint@, @int@, @bigint@, @float@, @decimal@, @varint@, @text@, @varchar@             |
+|@float@     |@tinyint@, @smallint@, @int@, @bigint@, @double@, @decimal@, @varint@, @text@, @varchar@           |
+|@inet@      |@text@, @varchar@                                                                                  |
+|@int@       |@tinyint@, @smallint@, @bigint@, @float@, @double@, @decimal@, @varint@, @text@, @varchar@         |
+|@smallint@ |@tinyint@, @int@, @bigint@, @float@, @double@, @decimal@, @varint@, @text@, @varchar@               |
+|@time@      |@text@, @varchar@                                                                                  |
+|@timestamp@|@date@, @text@, @varchar@                                                                           |
+|@timeuuid@ |@timestamp@, @date@, @text@, @varchar@                                                              |
+|@tinyint@  |@tinyint@, @smallint@, @int@, @bigint@, @float@, @double@, @decimal@, @varint@, @text@, @varchar@   |
+|@uuid@      |@text@, @varchar@                                                                                  |
+|@varint@   |@tinyint@, @smallint@, @int@, @bigint@, @float@, @double@, @decimal@, @text@, @varchar@             |
+
+
+The conversions rely strictly on Java's semantics. For example, the double value 1 will be converted to the text value '1.0'.
+
+bc(sample). 
+SELECT avg(cast(count as double)) FROM myTable
+
 h3(#tokenFun). Token
 
 The @token@ function allows to compute the token for a given partition key. The exact signature of the token function depends on the table concerned and of the partitioner used by the cluster.
@@ -2034,6 +2174,49 @@
 
 User-defined functions can be used in "@SELECT@":#selectStmt, "@INSERT@":#insertStmt and "@UPDATE@":#updateStmt statements.
 
+The implicitly available @udfContext@ field (or binding for script UDFs) provides the neccessary functionality to create new UDT and tuple values.
+
+bc(sample). 
+CREATE TYPE custom_type (txt text, i int);
+CREATE FUNCTION fct_using_udt ( somearg int )
+  RETURNS NULL ON NULL INPUT
+  RETURNS custom_type
+  LANGUAGE java
+  AS $$
+    UDTValue udt = udfContext.newReturnUDTValue();
+    udt.setString("txt", "some string");
+    udt.setInt("i", 42);
+    return udt;
+  $$;
+
+The definition of the @UDFContext@ interface can be found in the Apache Cassandra source code for @org.apache.cassandra.cql3.functions.UDFContext@.
+
+bc(sample). 
+public interface UDFContext
+{
+    UDTValue newArgUDTValue(String argName);
+    UDTValue newArgUDTValue(int argNum);
+    UDTValue newReturnUDTValue();
+    UDTValue newUDTValue(String udtName);
+    TupleValue newArgTupleValue(String argName);
+    TupleValue newArgTupleValue(int argNum);
+    TupleValue newReturnTupleValue();
+    TupleValue newTupleValue(String cqlDefinition);
+}
+
+Java UDFs already have some imports for common interfaces and classes defined. These imports are:
+Please note, that these convenience imports are not available for script UDFs.
+
+bc(sample). 
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import com.datastax.driver.core.TypeCodec;
+import com.datastax.driver.core.TupleValue;
+import com.datastax.driver.core.UDTValue;
+
 See "@CREATE FUNCTION@":#createFunctionStmt and "@DROP FUNCTION@":#dropFunctionStmt.
 
 h2(#udas). User-Defined Aggregates
@@ -2110,7 +2293,7 @@
 bc(sample). 
 INSERT INTO mytable JSON '{"\"myKey\"": 0, "value": 0}'
 
-Any columns which are ommitted from the @JSON@ map will be defaulted to a @NULL@ value (which will result in a tombstone being created).
+By default (or if @DEFAULT NULL@ is explicitly used), a column omitted from the @JSON@ map will be set to @NULL@, meaning that any pre-existing value for that column will be removed (resulting in a tombstone being created). Alternatively, if the @DEFAULT UNSET@ directive is used after the value, omitted column values will be left unset, meaning that pre-existing values for those column will be preserved.
 
 h3(#jsonEncoding). JSON Encoding of Cassandra Data Types
 
@@ -2175,6 +2358,7 @@
 | @BOOLEAN@      | no  |
 | @BY@           | yes |
 | @CALLED@       | no  |
+| @CAST@         | no  |
 | @CLUSTERING@   | no  |
 | @COLUMNFAMILY@ | yes |
 | @COMPACT@      | no  |
@@ -2185,12 +2369,14 @@
 | @CUSTOM@       | no  |
 | @DATE@         | no  |
 | @DECIMAL@      | no  |
+| @DEFAULT@      | yes |
 | @DELETE@       | yes |
 | @DESC@         | yes |
 | @DESCRIBE@     | yes |
 | @DISTINCT@     | no  |
 | @DOUBLE@       | no  |
 | @DROP@         | yes |
+| @DURATION@     | no  |
 | @ENTRIES@      | yes |
 | @EXECUTE@      | yes |
 | @EXISTS@       | no  |
@@ -2203,6 +2389,7 @@
 | @FUNCTION@     | no  |
 | @FUNCTIONS@    | no  |
 | @GRANT@        | yes |
+| @GROUP@        | no  |
 | @IF@           | yes |
 | @IN@           | yes |
 | @INDEX@        | yes |
@@ -2220,11 +2407,14 @@
 | @KEYSPACE@     | yes |
 | @KEYSPACES@    | no  |
 | @LANGUAGE@     | no  |
+| @LIKE@         | no  |
 | @LIMIT@        | yes |
 | @LIST@         | no  |
 | @LOGIN@        | no  |
 | @MAP@          | no  |
 | @MATERIALIZED@ | yes |
+| @MBEAN@        | yes |
+| @MBEANS@       | yes |
 | @MODIFY@       | yes |
 | @NAN@          | yes |
 | @NOLOGIN@      | no  |
@@ -2237,7 +2427,9 @@
 | @OPTIONS@      | no  |
 | @OR@           | yes |
 | @ORDER@        | yes |
+| @PARTITION@    | no  |
 | @PASSWORD@     | no  |
+| @PER@          | no  |
 | @PERMISSION@   | no  |
 | @PERMISSIONS@  | no  |
 | @PRIMARY@      | yes |
@@ -2270,6 +2462,7 @@
 | @TUPLE@        | no  |
 | @TYPE@         | no  |
 | @UNLOGGED@     | yes |
+| @UNSET@        | yes |
 | @UPDATE@       | yes |
 | @USE@          | yes |
 | @USER@         | no  |
@@ -2301,6 +2494,30 @@
 
 The following describes the changes in each version of CQL.
 
+h3. 3.4.4
+
+* @ALTER TABLE ALTER@ has been removed; a column’s type may not be changed after creation (see "CASSANDRA-12443":https://issues.apache.org/jira/browse/CASSANDRA-12443).
+* @ALTER TYPE ALTER@ has been removed; a field’s type may not be changed after creation (see "CASSANDRA-12443":https://issues.apache.org/jira/browse/CASSANDRA-12443).
+
+h3. 3.4.3
+
+* Adds a new @duration@ data type See "Data Types":#types (see "CASSANDRA-11873":https://issues.apache.org/jira/browse/CASSANDRA-11873).
+* Support for @GROUP BY@. See "@<group-by>@":#selectGroupBy (see "CASSANDRA-10707":https://issues.apache.org/jira/browse/CASSANDRA-10707).
+* Adds a @DEFAULT UNSET@ option for @INSERT JSON@ to ignore omitted columns (see "CASSANDRA-11424":https://issues.apache.org/jira/browse/CASSANDRA-11424).
+* Allows @null@ as a legal value for TTL on insert and update. It will be treated as equivalent to inserting a 0 (see "CASSANDRA-12216":https://issues.apache.org/jira/browse/CASSANDRA-12216).
+
+h3. 3.4.2
+
+* "@INSERT/UPDATE options@":#updateOptions for tables having a default_time_to_live specifying a TTL of 0 will remove the TTL from the inserted or updated values
+* "@ALTER TABLE@":#alterTableStmt @ADD@ and @DROP@ now allow mutiple columns to be added/removed
+* New "@PER PARTITION LIMIT@":#selectLimit option (see "CASSANDRA-7017":https://issues.apache.org/jira/browse/CASSANDRA-7017).
+* "User-defined functions":#udfs can now instantiate @UDTValue@ and @TupleValue@ instances via the new @UDFContext@ interface (see "CASSANDRA-10818":https://issues.apache.org/jira/browse/CASSANDRA-10818).
+* "User-defined types"#createTypeStmt may now be stored in a non-frozen form, allowing individual fields to be updated and deleted in "@UPDATE@ statements":#updateStmt and "@DELETE@ statements":#deleteStmt, respectively. ("CASSANDRA-7423":https://issues.apache.org/jira/browse/CASSANDRA-7423)
+
+h3. 3.4.1
+
+* Adds @CAST@ functions. See "@Cast@":#castFun.
+
 h3. 3.4.0
 
 * Support for "materialized views":#createMVStmt
diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc
new file mode 100644
index 0000000..4c80eca
--- /dev/null
+++ b/doc/modules/ROOT/nav.adoc
@@ -0,0 +1,4 @@
+* xref:index.adoc[Main]
+** xref:master@_:ROOT:glossary.adoc[Glossary]
+** xref:master@_:ROOT:bugs.adoc[How to report bugs]
+** xref:master@_:ROOT:contactus.adoc[Contact us]
\ No newline at end of file
diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc
new file mode 100644
index 0000000..183bf9e
--- /dev/null
+++ b/doc/modules/ROOT/pages/index.adoc
@@ -0,0 +1,48 @@
+= Welcome to Apache Cassandra's documentation!
+
+:description: Starting page for Apache Cassandra documentation.
+:keywords: Apache, Cassandra, NoSQL, database
+:cass-url: http://cassandra.apache.org
+:cass-contrib-url: https://wiki.apache.org/cassandra/HowToContribute
+
+This is the official documentation for {cass-url}[Apache Cassandra]. 
+If you would like to contribute to this documentation, you are welcome 
+to do so by submitting your contribution like any other patch following
+{cass-contrib-url}[these instructions].
+
+== Main documentation
+
+[cols="a,a"]
+|===
+
+| xref:cassandra:getting_started/index.adoc[Getting started] | Newbie starting point
+
+| xref:cassandra:architecture/index.adoc[Architecture] | Cassandra's big picture
+
+| xref:cassandra:data_modeling/index.adoc[Data modeling] | Hint: it's not relational
+
+| xref:cassandra:cql/index.adoc[Cassandra Query Language (CQL)] | CQL reference documentation
+
+| xref:cassandra:configuration/index.adoc[Configuration] | Cassandra's handles and knobs
+
+| xref:cassandra:operating/index.adoc[Operation] | The operator's corner
+
+| xref:cassandra:tools/index.adoc[Tools] | cqlsh, nodetool, and others
+
+| xref:cassandra:troubleshooting/index.adoc[Troubleshooting] | What to look for when you have a problem
+
+| xref:cassandra:faq/index.adoc[FAQ] | Frequently asked questions
+
+| xref:cassandra:plugins/index.adoc[Plug-ins] | Third-party plug-ins
+
+| xref:master@_:ROOT:native_protocol.adoc[Native Protocols] | Native Cassandra protocol specifications
+
+|===
+
+== Meta information
+* xref:master@_:ROOT:bugs.adoc[Reporting bugs]
+* xref:master@_:ROOT:contactus.adoc[Contact us]
+* xref:master@_:ROOT:development/index.adoc[Contributing code]
+* xref:master@_:ROOT:docdev/index.adoc[Contributing to the docs]
+* xref:master@_:ROOT:community.adoc[Community]
+* xref:master@_:ROOT:download.adoc[Download]
diff --git a/doc/modules/cassandra/assets/images/Figure_1_backups.jpg b/doc/modules/cassandra/assets/images/Figure_1_backups.jpg
new file mode 100644
index 0000000..160013d
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/Figure_1_backups.jpg
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/Figure_1_data_model.jpg b/doc/modules/cassandra/assets/images/Figure_1_data_model.jpg
new file mode 100644
index 0000000..a3b330e
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/Figure_1_data_model.jpg
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/Figure_1_guarantees.jpg b/doc/modules/cassandra/assets/images/Figure_1_guarantees.jpg
new file mode 100644
index 0000000..859342d
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/Figure_1_guarantees.jpg
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/Figure_1_read_repair.jpg b/doc/modules/cassandra/assets/images/Figure_1_read_repair.jpg
new file mode 100644
index 0000000..d771550
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/Figure_1_read_repair.jpg
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/Figure_2_data_model.jpg b/doc/modules/cassandra/assets/images/Figure_2_data_model.jpg
new file mode 100644
index 0000000..7acdeac
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/Figure_2_data_model.jpg
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/Figure_2_read_repair.jpg b/doc/modules/cassandra/assets/images/Figure_2_read_repair.jpg
new file mode 100644
index 0000000..29a912b
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/Figure_2_read_repair.jpg
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/Figure_3_read_repair.jpg b/doc/modules/cassandra/assets/images/Figure_3_read_repair.jpg
new file mode 100644
index 0000000..f5cc189
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/Figure_3_read_repair.jpg
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/Figure_4_read_repair.jpg b/doc/modules/cassandra/assets/images/Figure_4_read_repair.jpg
new file mode 100644
index 0000000..25bdb34
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/Figure_4_read_repair.jpg
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/Figure_5_read_repair.jpg b/doc/modules/cassandra/assets/images/Figure_5_read_repair.jpg
new file mode 100644
index 0000000..d9c0485
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/Figure_5_read_repair.jpg
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/Figure_6_read_repair.jpg b/doc/modules/cassandra/assets/images/Figure_6_read_repair.jpg
new file mode 100644
index 0000000..6bb4d1e
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/Figure_6_read_repair.jpg
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/data_modeling_chebotko_logical.png b/doc/modules/cassandra/assets/images/data_modeling_chebotko_logical.png
new file mode 100755
index 0000000..e54b5f2
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/data_modeling_chebotko_logical.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/data_modeling_chebotko_physical.png b/doc/modules/cassandra/assets/images/data_modeling_chebotko_physical.png
new file mode 100644
index 0000000..bfdaec5
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/data_modeling_chebotko_physical.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/data_modeling_hotel_bucketing.png b/doc/modules/cassandra/assets/images/data_modeling_hotel_bucketing.png
new file mode 100644
index 0000000..8b53e38
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/data_modeling_hotel_bucketing.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/data_modeling_hotel_erd.png b/doc/modules/cassandra/assets/images/data_modeling_hotel_erd.png
new file mode 100755
index 0000000..e86fe68
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/data_modeling_hotel_erd.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/data_modeling_hotel_logical.png b/doc/modules/cassandra/assets/images/data_modeling_hotel_logical.png
new file mode 100755
index 0000000..e920f12
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/data_modeling_hotel_logical.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/data_modeling_hotel_physical.png b/doc/modules/cassandra/assets/images/data_modeling_hotel_physical.png
new file mode 100644
index 0000000..2d20a6d
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/data_modeling_hotel_physical.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/data_modeling_hotel_queries.png b/doc/modules/cassandra/assets/images/data_modeling_hotel_queries.png
new file mode 100755
index 0000000..2434db3
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/data_modeling_hotel_queries.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/data_modeling_hotel_relational.png b/doc/modules/cassandra/assets/images/data_modeling_hotel_relational.png
new file mode 100755
index 0000000..43e784e
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/data_modeling_hotel_relational.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/data_modeling_reservation_logical.png b/doc/modules/cassandra/assets/images/data_modeling_reservation_logical.png
new file mode 100755
index 0000000..0460633
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/data_modeling_reservation_logical.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/data_modeling_reservation_physical.png b/doc/modules/cassandra/assets/images/data_modeling_reservation_physical.png
new file mode 100755
index 0000000..1e6e76c
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/data_modeling_reservation_physical.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/docs_commit.png b/doc/modules/cassandra/assets/images/docs_commit.png
new file mode 100644
index 0000000..d90d96a
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/docs_commit.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/docs_create_branch.png b/doc/modules/cassandra/assets/images/docs_create_branch.png
new file mode 100644
index 0000000..a04cb54
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/docs_create_branch.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/docs_create_file.png b/doc/modules/cassandra/assets/images/docs_create_file.png
new file mode 100644
index 0000000..b51e370
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/docs_create_file.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/docs_editor.png b/doc/modules/cassandra/assets/images/docs_editor.png
new file mode 100644
index 0000000..5b9997b
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/docs_editor.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/docs_fork.png b/doc/modules/cassandra/assets/images/docs_fork.png
new file mode 100644
index 0000000..20a592a
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/docs_fork.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/docs_pr.png b/doc/modules/cassandra/assets/images/docs_pr.png
new file mode 100644
index 0000000..211eb25
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/docs_pr.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/docs_preview.png b/doc/modules/cassandra/assets/images/docs_preview.png
new file mode 100644
index 0000000..207f0ac
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/docs_preview.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/eclipse_debug0.png b/doc/modules/cassandra/assets/images/eclipse_debug0.png
new file mode 100644
index 0000000..79fc5fd
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/eclipse_debug0.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/eclipse_debug1.png b/doc/modules/cassandra/assets/images/eclipse_debug1.png
new file mode 100644
index 0000000..87b8756
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/eclipse_debug1.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/eclipse_debug2.png b/doc/modules/cassandra/assets/images/eclipse_debug2.png
new file mode 100644
index 0000000..df4eddb
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/eclipse_debug2.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/eclipse_debug3.png b/doc/modules/cassandra/assets/images/eclipse_debug3.png
new file mode 100644
index 0000000..2317814
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/eclipse_debug3.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/eclipse_debug4.png b/doc/modules/cassandra/assets/images/eclipse_debug4.png
new file mode 100644
index 0000000..5063d48
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/eclipse_debug4.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/eclipse_debug5.png b/doc/modules/cassandra/assets/images/eclipse_debug5.png
new file mode 100644
index 0000000..ab68e68
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/eclipse_debug5.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/eclipse_debug6.png b/doc/modules/cassandra/assets/images/eclipse_debug6.png
new file mode 100644
index 0000000..61ef30b
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/eclipse_debug6.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/example-stress-graph.png b/doc/modules/cassandra/assets/images/example-stress-graph.png
new file mode 100644
index 0000000..a65b08b
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/example-stress-graph.png
Binary files differ
diff --git a/doc/modules/cassandra/assets/images/hints.svg b/doc/modules/cassandra/assets/images/hints.svg
new file mode 100644
index 0000000..5e952e7
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/hints.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="661.2000122070312" height="422.26666259765625" style="
+        width:661.2000122070312px;
+        height:422.26666259765625px;
+        background: transparent;
+        fill: none;
+">
+        <svg xmlns="http://www.w3.org/2000/svg" class="role-diagram-draw-area"><g class="shapes-region" style="stroke: black; fill: none;"><g class="composite-shape"><path class="real" d=" M40,60 C40,43.43 53.43,30 70,30 C86.57,30 100,43.43 100,60 C100,76.57 86.57,90 70,90 C53.43,90 40,76.57 40,60 Z" style="stroke-width: 1px; stroke: rgb(0, 0, 0); fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M70,300 L70,387" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="#000" transform="matrix(-1.8369701987210297e-16,-1,1,-1.8369701987210297e-16,70,390)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M10.72,-5.15 L0,0 L10.72,5.15 L7.12,0 Z"/></g></g><g class="composite-shape"><path class="real" d=" M300,58.5 C300,41.93 313.43,28.5 330,28.5 C346.57,28.5 360,41.93 360,58.5 C360,75.07 346.57,88.5 330,88.5 C313.43,88.5 300,75.07 300,58.5 Z" style="stroke-width: 1px; stroke: rgb(0, 0, 0); fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M80,120 L197,118.54" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="rgba(0,0,0,1)" transform="matrix(-0.9999210442038161,0.01256603988335397,-0.01256603988335397,-0.9999210442038161,200,118.5)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M8.93,-4.29 L0,0 L8.93,4.29 Z"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M330,300 L330,385.5" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="#000" transform="matrix(-1.8369701987210297e-16,-1,1,-1.8369701987210297e-16,330,388.49999999999994)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M10.72,-5.15 L0,0 L10.72,5.15 L7.12,0 Z"/></g></g><g class="composite-shape"><path class="real" d=" M420,60 C420,43.43 433.43,30 450,30 C466.57,30 480,43.43 480,60 C480,76.57 466.57,90 450,90 C433.43,90 420,76.57 420,60 Z" style="stroke-width: 1px; stroke: rgb(0, 0, 0); fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M570,300 L570,385.5" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="#000" transform="matrix(-1.8369701987210297e-16,-1,1,-1.8369701987210297e-16,570,388.5)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M10.72,-5.15 L0,0 L10.72,5.15 L7.12,0 Z"/></g></g><g class="composite-shape"><path class="real" d=" M540,60 C540,43.43 553.43,30 570,30 C586.57,30 600,43.43 600,60 C600,76.57 586.57,90 570,90 C553.43,90 540,76.57 540,60 Z" style="stroke-width: 1px; stroke: rgb(0, 0, 0); fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M450,100 L450,128.5" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M450,320 L450,385.5" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="#000" transform="matrix(-1.8369701987210297e-16,-1,1,-1.8369701987210297e-16,450,388.49999999999994)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M10.72,-5.15 L0,0 L10.72,5.15 L7.12,0 Z"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="1.125 3.35" d="  M450,135.1 L450,220" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="#000" transform="matrix(6.123233995736766e-17,1,-1,6.123233995736766e-17,449.99999999999994,135.10000000000002)" style="stroke: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M0,5.59 L0,-5.59 M-5.03,5.59 L-5.03,-5.59"/></g></g><g class="composite-shape"><path class="real" d=" M180,56.5 C180,39.93 193.43,26.5 210,26.5 C226.57,26.5 240,39.93 240,56.5 C240,73.07 226.57,86.5 210,86.5 C193.43,86.5 180,73.07 180,56.5 Z" style="stroke-width: 1px; stroke: rgb(0, 0, 0); fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M210,300 L210,383.5" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="#000" transform="matrix(-1.8369701987210297e-16,-1,1,-1.8369701987210297e-16,210,386.49999999999994)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M10.72,-5.15 L0,0 L10.72,5.15 L7.12,0 Z"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M220,119.5 L317,119.5" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="rgba(0,0,0,1)" transform="matrix(-1,1.2246467991473532e-16,-1.2246467991473532e-16,-1,320,119.49999999999999)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M8.93,-4.29 L0,0 L8.93,4.29 Z"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M220,141 L437,140.01" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="rgba(0,0,0,1)" transform="matrix(-0.9999897039488793,0.004537840481175345,-0.004537840481175345,-0.9999897039488793,440,140)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M8.93,-4.29 L0,0 L8.93,4.29 Z"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M220,160 L557,160" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="rgba(0,0,0,1)" transform="matrix(-1,1.2246467991473532e-16,-1.2246467991473532e-16,-1,560,160)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M8.93,-4.29 L0,0 L8.93,4.29 Z"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M330,190 L223,190" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="rgba(0,0,0,1)" transform="matrix(1,-2.4492935982947064e-16,2.4492935982947064e-16,1,220,190)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M8.93,-4.29 L0,0 L8.93,4.29 Z"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M570,200 L223,200" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="rgba(0,0,0,1)" transform="matrix(1,-2.4492935982947064e-16,2.4492935982947064e-16,1,220,200)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M8.93,-4.29 L0,0 L8.93,4.29 Z"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M200,200 L83,200" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="rgba(0,0,0,1)" transform="matrix(1,-2.4492935982947064e-16,2.4492935982947064e-16,1,80,200)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M8.93,-4.29 L0,0 L8.93,4.29 Z"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M220,260 C248.94,258.95 251.69,269.27 222.77,269.96" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" transform="matrix(0.9999965730559848,-0.0026179908874171876,0.0026179908874171876,0.9999965730559848,220,270)" style="stroke: none; stroke-width: 1px; fill: rgb(0, 0, 0);" fill="#000"><path d=" M8.93,-4.29 L0,0 L8.93,4.29 Z"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M220,360 L437,360" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="rgba(0,0,0,1)" transform="matrix(-1,1.2246467991473532e-16,-1.2246467991473532e-16,-1,439.99999999999994,360.00000000000006)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M8.93,-4.29 L0,0 L8.93,4.29 Z"/></g></g><g class="grouped-shape"><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M60,220 Q62.5,217.5 65,220 Q67.5,222.5 70,220 Q72.5,217.5 75,220 Q77.5,222.5 80,220 L80,220" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M60,230 Q62.5,227.5 65,230 Q67.5,232.5 70,230 Q72.5,227.5 75,230 Q77.5,232.5 80,230 L80,230" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M70,100 L70,220" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M200,220 Q202.5,217.5 205,220 Q207.5,222.5 210,220 Q212.5,217.5 215,220 Q217.5,222.5 220,220 L220,220" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M200,230 Q202.5,227.5 205,230 Q207.5,232.5 210,230 Q212.5,227.5 215,230 Q217.5,232.5 220,230 L220,230" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="grouped-shape"><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M320,220 Q322.5,217.5 325,220 Q327.5,222.5 330,220 Q332.5,217.5 335,220 Q337.5,222.5 340,220 L340,220" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M320,230 Q322.5,227.5 325,230 Q327.5,232.5 330,230 Q332.5,227.5 335,230 Q337.5,232.5 340,230 L340,230" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M440,220 Q442.5,217.5 445,220 Q447.5,222.5 450,220 Q452.5,217.5 455,220 Q457.5,222.5 460,220 L460,220" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M440,230 Q442.5,227.5 445,230 Q447.5,232.5 450,230 Q452.5,227.5 455,230 Q457.5,232.5 460,230 L460,230" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M560,220 Q562.5,217.5 565,220 Q567.5,222.5 570,220 Q572.5,217.5 575,220 Q577.5,222.5 580,220 L580,220" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M560,230 Q562.5,227.5 565,230 Q567.5,232.5 570,230 Q572.5,227.5 575,230 Q577.5,232.5 580,230 L580,230" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M210,100 L210,220" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M330,100 L330,220" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M570,100 L570,220" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="1.125 3.35" d="  M450,315 L450,300" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="#000" transform="matrix(-1.8369701987210297e-16,-1,1,-1.8369701987210297e-16,450,314.99999999999994)" style="stroke: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M0,5.59 L0,-5.59 M-5.03,5.59 L-5.03,-5.59"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M440,330 L223,330" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/><g stroke="none" fill="rgba(0,0,0,1)" transform="matrix(1,-2.4492935982947064e-16,2.4492935982947064e-16,1,220,330)" style="stroke: none; fill: rgb(0, 0, 0); stroke-width: 1px;"><path d=" M8.93,-4.29 L0,0 L8.93,4.29 Z"/></g></g><g class="grouped-shape"><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M60,290 Q62.5,287.5 65,290 Q67.5,292.5 70,290 Q72.5,287.5 75,290 Q77.5,292.5 80,290 L80,290" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M60,300 Q62.5,297.5 65,300 Q67.5,302.5 70,300 Q72.5,297.5 75,300 Q77.5,302.5 80,300 L80,300" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M70,230 L70,290" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/></g><g class="grouped-shape"><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M200,290 Q202.5,287.5 205,290 Q207.5,292.5 210,290 Q212.5,287.5 215,290 Q217.5,292.5 220,290 L220,290" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M200,300 Q202.5,297.5 205,300 Q207.5,302.5 210,300 Q212.5,297.5 215,300 Q217.5,302.5 220,300 L220,300" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g></g><g class="grouped-shape"><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M320,290 Q322.5,287.5 325,290 Q327.5,292.5 330,290 Q332.5,287.5 335,290 Q337.5,292.5 340,290 L340,290" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M320,300 Q322.5,297.5 325,300 Q327.5,302.5 330,300 Q332.5,297.5 335,300 Q337.5,302.5 340,300 L340,300" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M210,230 L210,290" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M330,230 L330,290" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/></g><g class="grouped-shape"><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M440,290 Q442.5,287.5 445,290 Q447.5,292.5 450,290 Q452.5,287.5 455,290 Q457.5,292.5 460,290 L460,290" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M440,300 Q442.5,297.5 445,300 Q447.5,302.5 450,300 Q452.5,297.5 455,300 Q457.5,302.5 460,300 L460,300" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="1.125 3.35" d="  M450,230 L450,290" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/></g><g class="grouped-shape"><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M560,290 Q562.5,287.5 565,290 Q567.5,292.5 570,290 Q572.5,287.5 575,290 Q577.5,292.5 580,290 L580,290" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M560,300 Q562.5,297.5 565,300 Q567.5,302.5 570,300 Q572.5,297.5 575,300 Q577.5,302.5 580,300 L580,300" style="stroke: rgb(0, 0, 0); stroke-width: 3px; fill: none;"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M570,230 L570,290" style="stroke: rgb(0, 0, 0); stroke-width: 1px; fill: none;"/></g><g/></g><g/><g/><g/></svg>
+        <svg xmlns="http://www.w3.org/2000/svg" width="660" height="421.066650390625" style="width:660px;height:421.066650390625px;font-family:Asana-Math, Asana;background:transparent;"><g><g><g style="transform:matrix(1,0,0,1,47.266693115234375,65.81666564941406);"><path d="M342 330L365 330C373 395 380 432 389 458C365 473 330 482 293 482C248 483 175 463 118 400C64 352 25 241 25 136C25 40 67 -11 147 -11C201 -11 249 9 304 54L354 95L346 115L331 105C259 57 221 40 186 40C130 40 101 80 101 159C101 267 136 371 185 409C206 425 230 433 261 433C306 433 342 414 342 390ZM657 722L645 733C593 707 557 698 485 691L481 670L529 670C553 670 563 663 563 648C563 645 563 640 560 622C549 567 476 182 461 132C448 82 442 52 442 31C442 6 453 -9 472 -9C498 -9 534 12 632 85L622 103L596 86C567 67 545 56 535 56C528 56 522 66 522 76C522 82 523 89 526 104ZM717 388L724 368L756 389C793 412 796 414 803 414C813 414 821 404 821 391C821 384 817 361 813 347L747 107C739 76 734 49 734 30C734 6 745 -9 764 -9C790 -9 826 12 924 85L914 103L888 86C859 67 836 56 827 56C820 56 814 66 814 76C814 86 816 95 821 116L898 420C902 437 904 448 904 456C904 473 895 482 879 482C857 482 820 461 745 408ZM911 712C882 712 853 679 853 645C853 620 868 604 892 604C923 604 947 633 947 671C947 695 932 712 911 712ZM1288 111L1264 94C1211 56 1163 36 1127 36C1080 36 1051 73 1051 133C1051 158 1054 185 1059 214C1076 218 1185 248 1210 259C1295 296 1334 342 1334 404C1334 451 1300 482 1250 482C1182 496 1072 423 1035 349C1005 299 975 180 975 113C975 35 1019 -11 1091 -11C1148 -11 1204 17 1296 92ZM1073 274C1090 343 1110 386 1139 412C1157 428 1188 440 1212 440C1241 440 1260 420 1260 388C1260 344 1225 297 1173 272C1145 258 1109 247 1064 237ZM1372 388L1379 368L1411 389C1448 412 1451 414 1458 414C1469 414 1476 404 1476 389C1476 338 1435 145 1394 2L1401 -9C1426 -2 1449 4 1471 8C1490 134 1511 199 1557 268C1611 352 1686 414 1731 414C1742 414 1748 405 1748 390C1748 372 1745 351 1737 319L1685 107C1676 70 1672 47 1672 31C1672 6 1683 -9 1702 -9C1728 -9 1764 12 1862 85L1852 103L1826 86C1797 67 1775 56 1765 56C1758 56 1752 65 1752 76C1752 81 1753 92 1754 96L1820 372C1827 401 1831 429 1831 446C1831 469 1820 482 1800 482C1758 482 1689 444 1630 389C1592 354 1564 320 1512 247L1550 408C1554 426 1556 438 1556 449C1556 470 1548 482 1533 482C1512 482 1473 460 1400 408ZM2028 390L1972 107C1971 99 1959 61 1959 31C1959 6 1970 -9 1989 -9C2024 -9 2059 11 2137 74L2168 99L2158 117L2113 86C2084 66 2064 56 2053 56C2044 56 2039 64 2039 76C2039 102 2053 183 2082 328L2095 390L2202 390L2213 440C2175 436 2141 434 2103 434C2119 528 2130 577 2148 631L2137 646C2117 634 2090 622 2059 610L2034 440C1990 419 1964 408 1946 403L1944 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,85.80001831054688,68.81665649414063);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g><g><g><g style="transform:matrix(1,0,0,1,303.5000305175781,64.31666564941406);"><path d="M368 365C371 403 376 435 384 476C373 481 369 482 364 482C333 482 302 458 266 407C227 351 188 291 172 256L204 408C208 425 210 438 210 450C210 470 202 482 187 482C166 482 128 461 54 408L26 388L33 368L65 389C93 407 104 412 113 412C123 412 130 403 130 390C130 332 87 126 47 -2L57 -9C72 -4 88 -1 111 4L124 6L150 126C168 209 191 262 235 319C269 363 296 384 318 384C333 384 343 379 354 365ZM716 111L692 94C639 56 591 36 555 36C508 36 479 73 479 133C479 158 482 185 487 214C504 218 613 248 638 259C723 296 762 342 762 404C762 451 728 482 678 482C610 496 500 423 463 349C433 299 403 180 403 113C403 35 447 -11 519 -11C576 -11 632 17 724 92ZM501 274C518 343 538 386 567 412C585 428 616 440 640 440C669 440 688 420 688 388C688 344 653 297 601 272C573 258 537 247 492 237ZM952 -11C1008 -11 1123 62 1166 125C1207 186 1241 296 1241 371C1241 438 1218 482 1182 482C1140 482 1087 456 1035 409C994 374 974 348 938 289L962 408C965 425 967 440 967 452C967 471 959 482 945 482C924 482 886 461 812 408L784 388L791 368L823 389C851 407 862 412 871 412C881 412 888 403 888 389C888 381 886 361 884 351L826 8C816 -52 797 -143 777 -233L769 -270L776 -276C797 -269 817 -264 849 -259L891 3C912 -4 934 -11 952 -11ZM919 165C941 293 1048 424 1131 424C1157 424 1169 402 1169 356C1169 275 1129 156 1076 80C1056 51 1024 36 983 36C952 36 927 43 901 59ZM1526 722L1514 733C1462 707 1426 698 1354 691L1350 670L1398 670C1422 670 1432 663 1432 648C1432 645 1432 640 1429 622C1418 567 1345 182 1330 132C1317 82 1311 52 1311 31C1311 6 1322 -9 1341 -9C1367 -9 1403 12 1501 85L1491 103L1465 86C1436 67 1414 56 1404 56C1397 56 1391 66 1391 76C1391 82 1392 89 1395 104ZM1586 388L1593 368L1625 389C1662 412 1665 414 1672 414C1682 414 1690 404 1690 391C1690 384 1686 361 1682 347L1616 107C1608 76 1603 49 1603 30C1603 6 1614 -9 1633 -9C1659 -9 1695 12 1793 85L1783 103L1757 86C1728 67 1705 56 1696 56C1689 56 1683 66 1683 76C1683 86 1685 95 1690 116L1767 420C1771 437 1773 448 1773 456C1773 473 1764 482 1748 482C1726 482 1689 461 1614 408ZM1780 712C1751 712 1722 679 1722 645C1722 620 1737 604 1761 604C1792 604 1816 633 1816 671C1816 695 1801 712 1780 712ZM2171 330L2194 330C2202 395 2209 432 2218 458C2194 473 2159 482 2122 482C2077 483 2004 463 1947 400C1893 352 1854 241 1854 136C1854 40 1896 -11 1976 -11C2030 -11 2078 9 2133 54L2183 95L2175 115L2160 105C2088 57 2050 40 2015 40C1959 40 1930 80 1930 159C1930 267 1965 371 2014 409C2035 425 2059 433 2090 433C2135 433 2171 414 2171 390ZM2506 204L2477 77C2473 60 2471 42 2471 26C2471 4 2480 -9 2495 -9C2518 -9 2559 17 2641 85L2634 106C2610 86 2581 59 2559 59C2550 59 2544 68 2544 82C2544 87 2544 90 2545 93L2637 472L2627 481L2594 463C2553 478 2536 482 2509 482C2481 482 2461 477 2434 464C2372 433 2339 403 2314 354C2270 265 2239 145 2239 67C2239 23 2254 -11 2273 -11C2310 -11 2390 41 2506 204ZM2554 414C2532 305 2513 253 2479 201C2422 117 2361 59 2329 59C2317 59 2311 72 2311 99C2311 163 2339 280 2374 360C2398 415 2421 433 2469 433C2492 433 2510 429 2554 414Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,349.5666809082031,67.31665649414063);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g><g><g><g style="transform:matrix(1,0,0,1,423.5000305175781,65.81666564941406);"><path d="M368 365C371 403 376 435 384 476C373 481 369 482 364 482C333 482 302 458 266 407C227 351 188 291 172 256L204 408C208 425 210 438 210 450C210 470 202 482 187 482C166 482 128 461 54 408L26 388L33 368L65 389C93 407 104 412 113 412C123 412 130 403 130 390C130 332 87 126 47 -2L57 -9C72 -4 88 -1 111 4L124 6L150 126C168 209 191 262 235 319C269 363 296 384 318 384C333 384 343 379 354 365ZM716 111L692 94C639 56 591 36 555 36C508 36 479 73 479 133C479 158 482 185 487 214C504 218 613 248 638 259C723 296 762 342 762 404C762 451 728 482 678 482C610 496 500 423 463 349C433 299 403 180 403 113C403 35 447 -11 519 -11C576 -11 632 17 724 92ZM501 274C518 343 538 386 567 412C585 428 616 440 640 440C669 440 688 420 688 388C688 344 653 297 601 272C573 258 537 247 492 237ZM952 -11C1008 -11 1123 62 1166 125C1207 186 1241 296 1241 371C1241 438 1218 482 1182 482C1140 482 1087 456 1035 409C994 374 974 348 938 289L962 408C965 425 967 440 967 452C967 471 959 482 945 482C924 482 886 461 812 408L784 388L791 368L823 389C851 407 862 412 871 412C881 412 888 403 888 389C888 381 886 361 884 351L826 8C816 -52 797 -143 777 -233L769 -270L776 -276C797 -269 817 -264 849 -259L891 3C912 -4 934 -11 952 -11ZM919 165C941 293 1048 424 1131 424C1157 424 1169 402 1169 356C1169 275 1129 156 1076 80C1056 51 1024 36 983 36C952 36 927 43 901 59ZM1526 722L1514 733C1462 707 1426 698 1354 691L1350 670L1398 670C1422 670 1432 663 1432 648C1432 645 1432 640 1429 622C1418 567 1345 182 1330 132C1317 82 1311 52 1311 31C1311 6 1322 -9 1341 -9C1367 -9 1403 12 1501 85L1491 103L1465 86C1436 67 1414 56 1404 56C1397 56 1391 66 1391 76C1391 82 1392 89 1395 104ZM1586 388L1593 368L1625 389C1662 412 1665 414 1672 414C1682 414 1690 404 1690 391C1690 384 1686 361 1682 347L1616 107C1608 76 1603 49 1603 30C1603 6 1614 -9 1633 -9C1659 -9 1695 12 1793 85L1783 103L1757 86C1728 67 1705 56 1696 56C1689 56 1683 66 1683 76C1683 86 1685 95 1690 116L1767 420C1771 437 1773 448 1773 456C1773 473 1764 482 1748 482C1726 482 1689 461 1614 408ZM1780 712C1751 712 1722 679 1722 645C1722 620 1737 604 1761 604C1792 604 1816 633 1816 671C1816 695 1801 712 1780 712ZM2171 330L2194 330C2202 395 2209 432 2218 458C2194 473 2159 482 2122 482C2077 483 2004 463 1947 400C1893 352 1854 241 1854 136C1854 40 1896 -11 1976 -11C2030 -11 2078 9 2133 54L2183 95L2175 115L2160 105C2088 57 2050 40 2015 40C1959 40 1930 80 1930 159C1930 267 1965 371 2014 409C2035 425 2059 433 2090 433C2135 433 2171 414 2171 390ZM2506 204L2477 77C2473 60 2471 42 2471 26C2471 4 2480 -9 2495 -9C2518 -9 2559 17 2641 85L2634 106C2610 86 2581 59 2559 59C2550 59 2544 68 2544 82C2544 87 2544 90 2545 93L2637 472L2627 481L2594 463C2553 478 2536 482 2509 482C2481 482 2461 477 2434 464C2372 433 2339 403 2314 354C2270 265 2239 145 2239 67C2239 23 2254 -11 2273 -11C2310 -11 2390 41 2506 204ZM2554 414C2532 305 2513 253 2479 201C2422 117 2361 59 2329 59C2317 59 2311 72 2311 99C2311 163 2339 280 2374 360C2398 415 2421 433 2469 433C2492 433 2510 429 2554 414Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,469.5666809082031,68.81665649414063);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g><g><g><g style="transform:matrix(1,0,0,1,543.5000305175781,65.81666564941406);"><path d="M368 365C371 403 376 435 384 476C373 481 369 482 364 482C333 482 302 458 266 407C227 351 188 291 172 256L204 408C208 425 210 438 210 450C210 470 202 482 187 482C166 482 128 461 54 408L26 388L33 368L65 389C93 407 104 412 113 412C123 412 130 403 130 390C130 332 87 126 47 -2L57 -9C72 -4 88 -1 111 4L124 6L150 126C168 209 191 262 235 319C269 363 296 384 318 384C333 384 343 379 354 365ZM716 111L692 94C639 56 591 36 555 36C508 36 479 73 479 133C479 158 482 185 487 214C504 218 613 248 638 259C723 296 762 342 762 404C762 451 728 482 678 482C610 496 500 423 463 349C433 299 403 180 403 113C403 35 447 -11 519 -11C576 -11 632 17 724 92ZM501 274C518 343 538 386 567 412C585 428 616 440 640 440C669 440 688 420 688 388C688 344 653 297 601 272C573 258 537 247 492 237ZM952 -11C1008 -11 1123 62 1166 125C1207 186 1241 296 1241 371C1241 438 1218 482 1182 482C1140 482 1087 456 1035 409C994 374 974 348 938 289L962 408C965 425 967 440 967 452C967 471 959 482 945 482C924 482 886 461 812 408L784 388L791 368L823 389C851 407 862 412 871 412C881 412 888 403 888 389C888 381 886 361 884 351L826 8C816 -52 797 -143 777 -233L769 -270L776 -276C797 -269 817 -264 849 -259L891 3C912 -4 934 -11 952 -11ZM919 165C941 293 1048 424 1131 424C1157 424 1169 402 1169 356C1169 275 1129 156 1076 80C1056 51 1024 36 983 36C952 36 927 43 901 59ZM1526 722L1514 733C1462 707 1426 698 1354 691L1350 670L1398 670C1422 670 1432 663 1432 648C1432 645 1432 640 1429 622C1418 567 1345 182 1330 132C1317 82 1311 52 1311 31C1311 6 1322 -9 1341 -9C1367 -9 1403 12 1501 85L1491 103L1465 86C1436 67 1414 56 1404 56C1397 56 1391 66 1391 76C1391 82 1392 89 1395 104ZM1586 388L1593 368L1625 389C1662 412 1665 414 1672 414C1682 414 1690 404 1690 391C1690 384 1686 361 1682 347L1616 107C1608 76 1603 49 1603 30C1603 6 1614 -9 1633 -9C1659 -9 1695 12 1793 85L1783 103L1757 86C1728 67 1705 56 1696 56C1689 56 1683 66 1683 76C1683 86 1685 95 1690 116L1767 420C1771 437 1773 448 1773 456C1773 473 1764 482 1748 482C1726 482 1689 461 1614 408ZM1780 712C1751 712 1722 679 1722 645C1722 620 1737 604 1761 604C1792 604 1816 633 1816 671C1816 695 1801 712 1780 712ZM2171 330L2194 330C2202 395 2209 432 2218 458C2194 473 2159 482 2122 482C2077 483 2004 463 1947 400C1893 352 1854 241 1854 136C1854 40 1896 -11 1976 -11C2030 -11 2078 9 2133 54L2183 95L2175 115L2160 105C2088 57 2050 40 2015 40C1959 40 1930 80 1930 159C1930 267 1965 371 2014 409C2035 425 2059 433 2090 433C2135 433 2171 414 2171 390ZM2506 204L2477 77C2473 60 2471 42 2471 26C2471 4 2480 -9 2495 -9C2518 -9 2559 17 2641 85L2634 106C2610 86 2581 59 2559 59C2550 59 2544 68 2544 82C2544 87 2544 90 2545 93L2637 472L2627 481L2594 463C2553 478 2536 482 2509 482C2481 482 2461 477 2434 464C2372 433 2339 403 2314 354C2270 265 2239 145 2239 67C2239 23 2254 -11 2273 -11C2310 -11 2390 41 2506 204ZM2554 414C2532 305 2513 253 2479 201C2422 117 2361 59 2329 59C2317 59 2311 72 2311 99C2311 163 2339 280 2374 360C2398 415 2421 433 2469 433C2492 433 2510 429 2554 414Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,589.5666809082031,68.81665649414063);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,461.8333435058594,134.81666564941406);"><path d="M157 214C157 314 229 386 327 388L327 455C238 454 183 405 152 359L152 450L82 450L82 0L157 0ZM754 219C754 421 647 461 576 461C465 461 375 355 375 226C375 94 471 -11 591 -11C654 -11 711 13 750 41L744 106C681 54 615 50 592 50C512 50 448 121 445 219ZM450 274C466 350 519 400 576 400C628 400 684 366 697 274ZM1143 128C1143 183 1106 217 1104 220C1065 255 1038 261 988 270C933 281 887 291 887 340C887 402 959 402 972 402C1004 402 1057 398 1114 364L1126 429C1074 453 1033 461 982 461C957 461 816 461 816 330C816 281 845 249 870 230C901 208 923 204 978 193C1014 186 1072 174 1072 121C1072 52 993 52 978 52C897 52 841 89 823 101L811 33C843 17 898 -11 979 -11C1117 -11 1143 75 1143 128ZM1340 386L1481 386L1481 444L1340 444L1340 571L1271 571L1271 444L1184 444L1184 386L1268 386L1268 119C1268 59 1282 -11 1351 -11C1421 -11 1472 14 1497 27L1481 86C1455 65 1423 53 1391 53C1354 53 1340 83 1340 136ZM1924 289C1924 391 1851 461 1759 461C1694 461 1649 445 1602 418L1608 352C1660 389 1710 402 1759 402C1806 402 1846 362 1846 288L1846 245C1696 243 1569 201 1569 113C1569 70 1596 -11 1683 -11C1697 -11 1791 -9 1849 36L1849 0L1924 0ZM1846 132C1846 113 1846 88 1812 69C1783 51 1745 50 1734 50C1686 50 1641 73 1641 115C1641 185 1803 192 1846 194ZM2161 214C2161 314 2233 386 2331 388L2331 455C2242 454 2187 405 2156 359L2156 450L2086 450L2086 0L2161 0ZM2519 386L2660 386L2660 444L2519 444L2519 571L2450 571L2450 444L2363 444L2363 386L2447 386L2447 119C2447 59 2461 -11 2530 -11C2600 -11 2651 14 2676 27L2660 86C2634 65 2602 53 2570 53C2533 53 2519 83 2519 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,507.8166809082031,134.81666564941406);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,513.4667053222656,134.81666564941406);"><path d="M368 365C371 403 376 435 384 476C373 481 369 482 364 482C333 482 302 458 266 407C227 351 188 291 172 256L204 408C208 425 210 438 210 450C210 470 202 482 187 482C166 482 128 461 54 408L26 388L33 368L65 389C93 407 104 412 113 412C123 412 130 403 130 390C130 332 87 126 47 -2L57 -9C72 -4 88 -1 111 4L124 6L150 126C168 209 191 262 235 319C269 363 296 384 318 384C333 384 343 379 354 365Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,520.5833435058594,137.81665649414063);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,527.5333557128906,134.81666564941406);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g></g></g></g></g><g><g><g style="transform:matrix(1,0,0,1,191.48330688476562,63.29998779296875);"><path d="M342 330L365 330C373 395 380 432 389 458C365 473 330 482 293 482C248 483 175 463 118 400C64 352 25 241 25 136C25 40 67 -11 147 -11C201 -11 249 9 304 54L354 95L346 115L331 105C259 57 221 40 186 40C130 40 101 80 101 159C101 267 136 371 185 409C206 425 230 433 261 433C306 433 342 414 342 390ZM423 152C423 46 468 -11 551 -11C606 -11 666 15 711 57C773 116 817 230 817 331C817 425 767 482 684 482C580 482 423 382 423 152ZM647 444C708 444 741 399 741 315C741 219 710 113 666 60C648 39 622 27 591 27C533 27 499 72 499 151C499 264 538 387 587 427C600 438 623 444 647 444ZM866 152C866 46 911 -11 994 -11C1049 -11 1109 15 1154 57C1216 116 1260 230 1260 331C1260 425 1210 482 1127 482C1023 482 866 382 866 152ZM1090 444C1151 444 1184 399 1184 315C1184 219 1153 113 1109 60C1091 39 1065 27 1034 27C976 27 942 72 942 151C942 264 981 387 1030 427C1043 438 1066 444 1090 444ZM1660 365C1663 403 1668 435 1676 476C1665 481 1661 482 1656 482C1625 482 1594 458 1558 407C1519 351 1480 291 1464 256L1496 408C1500 425 1502 438 1502 450C1502 470 1494 482 1479 482C1458 482 1420 461 1346 408L1318 388L1325 368L1357 389C1385 407 1396 412 1405 412C1415 412 1422 403 1422 390C1422 332 1379 126 1339 -2L1349 -9C1364 -4 1380 -1 1403 4L1416 6L1442 126C1460 209 1483 262 1527 319C1561 363 1588 384 1610 384C1625 384 1635 379 1646 365ZM2163 722L2151 733C2099 707 2063 698 1991 691L1987 670L2035 670C2059 670 2069 663 2069 646C2069 638 2068 629 2067 622L2039 468C2009 477 1982 482 1957 482C1888 482 1794 410 1748 323C1717 265 1697 170 1697 86C1697 21 1712 -11 1741 -11C1768 -11 1805 6 1839 33C1893 77 1926 116 1993 217L1970 126C1959 82 1954 50 1954 24C1954 3 1963 -9 1980 -9C1997 -9 2021 3 2055 28L2136 88L2126 107L2082 76C2068 66 2052 59 2043 59C2035 59 2029 68 2029 82C2029 90 2030 99 2037 128ZM1794 59C1778 59 1769 73 1769 98C1769 224 1815 380 1864 418C1877 428 1896 433 1923 433C1967 433 1996 427 2029 410L2017 351C1980 171 1839 59 1794 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,79.63333129882812,386.40000915527344);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,231.00003051757812,115.31666564941406);"><path d="M260 229L443 444L361 444L227 279L89 444L6 444L194 229L0 0L82 0L227 188L377 0L460 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,246.44998168945312,115.31666564941406);"><path d="M949 241L949 300L179 300L304 452L272 486L65 269L272 55L304 89L179 241Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,267.0666809082031,115.31666564941406);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,275.5500183105469,115.31666564941406);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,283.1833190917969,115.31666564941406);"><path d="M424 386L565 386L565 444L424 444L424 571L355 571L355 444L268 444L268 386L352 386L352 119C352 59 366 -11 435 -11C505 -11 556 14 581 27L565 86C539 65 507 53 475 53C438 53 424 83 424 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,294.0500183105469,118.31665649414063);"><path d="M457 331C457 412 453 506 411 588C370 665 301 689 250 689C191 689 121 662 80 571C47 497 42 413 42 331C42 251 46 177 76 103C116 5 192 -22 249 -22C322 -22 385 19 417 89C447 155 457 223 457 331ZM250 40C198 40 157 78 137 151C121 209 120 264 120 343C120 407 120 468 137 524C143 544 168 627 249 627C327 627 353 550 360 531C379 475 379 406 379 343C379 276 379 212 361 148C335 56 282 40 250 40Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,250.25003051757812,184.81666564941406);"><path d="M399 289C399 391 326 461 234 461C169 461 124 445 77 418L83 352C135 389 185 402 234 402C281 402 321 362 321 288L321 245C171 243 44 201 44 113C44 70 71 -11 158 -11C172 -11 266 -9 324 36L324 0L399 0ZM321 132C321 113 321 88 287 69C258 51 220 50 209 50C161 50 116 73 116 115C116 185 278 192 321 194ZM889 418C830 452 796 461 735 461C596 461 515 340 515 222C515 98 606 -11 731 -11C785 -11 840 3 894 40L888 107C837 67 783 53 732 53C649 53 593 125 593 223C593 301 630 397 736 397C788 397 822 389 877 353ZM1203 272L1371 444L1281 444L1078 236L1078 694L1006 694L1006 0L1075 0L1075 141L1155 224L1311 0L1393 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,274.2167053222656,184.81666564941406);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,279.8666687011719,184.81666564941406);"><path d="M157 214C157 314 229 386 327 388L327 455C238 454 183 405 152 359L152 450L82 450L82 0L157 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,286.1666564941406,187.81665649414063);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,293.1166687011719,184.81666564941406);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,479.7500305175781,194.81666564941406);"><path d="M399 289C399 391 326 461 234 461C169 461 124 445 77 418L83 352C135 389 185 402 234 402C281 402 321 362 321 288L321 245C171 243 44 201 44 113C44 70 71 -11 158 -11C172 -11 266 -9 324 36L324 0L399 0ZM321 132C321 113 321 88 287 69C258 51 220 50 209 50C161 50 116 73 116 115C116 185 278 192 321 194ZM889 418C830 452 796 461 735 461C596 461 515 340 515 222C515 98 606 -11 731 -11C785 -11 840 3 894 40L888 107C837 67 783 53 732 53C649 53 593 125 593 223C593 301 630 397 736 397C788 397 822 389 877 353ZM1203 272L1371 444L1281 444L1078 236L1078 694L1006 694L1006 0L1075 0L1075 141L1155 224L1311 0L1393 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,503.7167053222656,194.81666564941406);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,509.3666687011719,194.81666564941406);"><path d="M157 214C157 314 229 386 327 388L327 455C238 454 183 405 152 359L152 450L82 450L82 0L157 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,515.6666564941406,197.8166717529297);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,522.6166687011719,194.81666564941406);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,81.03335571289062,182.5);"><path d="M399 289C399 391 326 461 234 461C169 461 124 445 77 418L83 352C135 389 185 402 234 402C281 402 321 362 321 288L321 245C171 243 44 201 44 113C44 70 71 -11 158 -11C172 -11 266 -9 324 36L324 0L399 0ZM321 132C321 113 321 88 287 69C258 51 220 50 209 50C161 50 116 73 116 115C116 185 278 192 321 194ZM889 418C830 452 796 461 735 461C596 461 515 340 515 222C515 98 606 -11 731 -11C785 -11 840 3 894 40L888 107C837 67 783 53 732 53C649 53 593 125 593 223C593 301 630 397 736 397C788 397 822 389 877 353ZM1203 272L1371 444L1281 444L1078 236L1078 694L1006 694L1006 0L1075 0L1075 141L1155 224L1311 0L1393 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,105.51669311523438,185.44998779296876);"><path d="M434 455L359 455L359 387C349 400 300 455 222 455C122 455 36 356 36 221C36 94 109 -11 205 -11C261 -11 314 12 356 50L356 -194L434 -194ZM359 140C359 122 359 120 349 107C323 67 286 50 250 50C174 50 114 128 114 221C114 323 186 391 259 391C328 391 359 320 359 280ZM950 444L872 444L872 154C872 79 816 44 752 44C681 44 674 70 674 113L674 444L596 444L596 109C596 37 619 -11 702 -11C755 -11 826 5 875 48L875 0L950 0ZM1499 220C1499 354 1399 461 1280 461C1157 461 1060 351 1060 220C1060 88 1162 -11 1279 -11C1399 -11 1499 90 1499 220ZM1279 53C1210 53 1138 109 1138 230C1138 351 1214 400 1279 400C1349 400 1421 348 1421 230C1421 112 1353 53 1279 53ZM1686 214C1686 314 1758 386 1856 388L1856 455C1767 454 1712 405 1681 359L1681 450L1611 450L1611 0L1686 0ZM2304 444L2226 444L2226 154C2226 79 2170 44 2106 44C2035 44 2028 70 2028 113L2028 444L1950 444L1950 109C1950 37 1973 -11 2056 -11C2109 -11 2180 5 2229 48L2229 0L2304 0ZM3097 298C3097 365 3081 455 2960 455C2900 455 2848 427 2811 373C2785 449 2715 455 2683 455C2611 455 2564 414 2537 378L2537 450L2465 450L2465 0L2543 0L2543 245C2543 313 2570 394 2644 394C2737 394 2742 329 2742 291L2742 0L2820 0L2820 245C2820 313 2847 394 2921 394C3014 394 3019 329 3019 291L3019 0L3097 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g><g><svg x="145.88333129882812" style="overflow:visible;" y="164.5" height="24" width="8.5"><path d=" M 6.61 1.70 q 0.00 -0.08 -0.08 -0.08 q -0.03 0.00 -0.08 0.02 q -1.01 0.55 -1.75 1.30 t -1.37 1.87 t -0.95 2.78 t -0.33 3.79 v 0.62 h 1.68 v -0.62 q 0.00 -1.20 0.04 -2.08 t 0.22 -2.04 t 0.50 -2.03 t 0.91 -1.74 t 1.43 -1.49 q 0.12 -0.09 0.12 -0.29 z   M 0.90 12.00 v 0.00 h 1.68 v 0.00 z" style="fill:rgb(0, 0, 0);stroke-width:1px;stroke:none;"></path><path d=" M 6.61 22.30 q 0.00 0.08 -0.08 0.08 q -0.03 0.00 -0.08 -0.02 q -1.01 -0.55 -1.75 -1.30 t -1.37 -1.87 t -0.95 -2.78 t -0.33 -3.79 v -0.62 h 1.68 v 0.62 q 0.00 1.20 0.04 2.08 t 0.22 2.04 t 0.50 2.03 t 0.91 1.74 t 1.43 1.49 q 0.12 0.09 0.12 0.29 z" style="fill:rgb(0, 0, 0);stroke-width:1px;stroke:none;"></path></svg></g><g style="transform:matrix(1,0,0,1,152.68331909179688,182.5);"><path d="M260 229L443 444L361 444L227 279L89 444L6 444L194 229L0 0L82 0L227 188L377 0L460 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,163.89999389648438,182.5);"><path d="M949 241L949 300L179 300L304 452L272 486L65 269L272 55L304 89L179 241Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,184.51669311523438,182.5);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><svg x="191.30001831054688" style="overflow:visible;" y="164.5" height="24" width="8.5"><path d=" M 1.69 1.70 q 0.00 -0.08 0.08 -0.08 q 0.03 0.00 0.08 0.02 q 1.01 0.55 1.75 1.30 t 1.37 1.87 t 0.95 2.78 t 0.33 3.79 v 0.62 h -1.68 v -0.62 q 0.00 -1.20 -0.04 -2.08 t -0.22 -2.04 t -0.50 -2.03 t -0.91 -1.74 t -1.43 -1.49 q -0.12 -0.09 -0.12 -0.29 z  M 7.40 12.00 v 0.00 h -1.68 v 0.00 z" style="fill:rgb(0, 0, 0);stroke-width:1px;stroke:none;"></path><path d=" M 1.69 22.30 q 0.00 0.08 0.08 0.08 q 0.03 0.00 0.08 -0.02 q 1.01 -0.55 1.75 -1.30 t 1.37 -1.87 t 0.95 -2.78 t 0.33 -3.79 v -0.62 h -1.68 v 0.62 q 0.00 1.20 -0.04 2.08 t -0.22 2.04 t -0.50 2.03 t -0.91 1.74 t -1.43 1.49 q -0.12 0.09 -0.12 0.29 z" style="fill:rgb(0, 0, 0);stroke-width:1px;stroke:none;"></path></svg></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,35.45001220703125,116.81666564941406);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,41.616668701171875,119.81665649414063);"><path d="M263 689C108 689 29 566 29 324C29 207 50 106 85 57C120 8 176 -20 238 -20C389 -20 465 110 465 366C465 585 400 689 263 689ZM245 654C342 654 381 556 381 316C381 103 343 15 251 15C154 15 113 116 113 360C113 571 150 654 245 654Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,35.45001220703125,206.81666564941406);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,41.616668701171875,209.8166717529297);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,35.45001220703125,256.81666564941406);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,41.616668701171875,259.81667175292966);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,35.45001220703125,336.81666564941406);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,41.616668701171875,339.81667175292966);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,217.80001831054688,249.49998474121094);"><path d="M435 298C435 364 420 455 298 455C236 455 188 424 156 383L156 694L81 694L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0ZM678 680L589 680L589 591L678 591ZM671 444L596 444L596 0L671 0ZM1187 298C1187 364 1172 455 1050 455C960 455 911 387 905 379L905 450L833 450L833 0L911 0L911 245C911 311 936 394 1012 394C1108 394 1109 323 1109 291L1109 0L1187 0ZM1442 386L1583 386L1583 444L1442 444L1442 571L1373 571L1373 444L1286 444L1286 386L1370 386L1370 119C1370 59 1384 -11 1453 -11C1523 -11 1574 14 1599 27L1583 86C1557 65 1525 53 1493 53C1456 53 1442 83 1442 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><svg x="246.98330688476562" style="overflow:visible;" y="231.49998474121094" height="26" width="8.5"><path d=" M 6.61 1.70 q 0.00 -0.09 -0.08 -0.09 q -0.03 0.00 -0.08 0.03 q -1.01 0.61 -1.75 1.42 t -1.37 2.06 t -0.95 3.05 t -0.33 4.16 v 0.68 h 1.68 v -0.68 q 0.00 -1.31 0.04 -2.28 t 0.22 -2.24 t 0.50 -2.23 t 0.91 -1.91 t 1.43 -1.63 q 0.12 -0.10 0.12 -0.32 z   M 0.90 13.00 v 0.00 h 1.68 v 0.00 z" style="fill:rgb(0, 0, 0);stroke-width:1px;stroke:none;"></path><path d=" M 6.61 24.30 q 0.00 0.09 -0.08 0.09 q -0.03 0.00 -0.08 -0.03 q -1.01 -0.61 -1.75 -1.42 t -1.37 -2.06 t -0.95 -3.05 t -0.33 -4.16 v -0.68 h 1.68 v 0.68 q 0.00 1.31 0.04 2.28 t 0.22 2.24 t 0.50 2.23 t 0.91 1.91 t 1.43 1.63 q 0.12 0.10 0.12 0.32 z" style="fill:rgb(0, 0, 0);stroke-width:1px;stroke:none;"></path></svg></g><g style="transform:matrix(1,0,0,1,253.78335571289062,249.49998474121094);"><path d="M157 214C157 314 229 386 327 388L327 455C238 454 183 405 152 359L152 450L82 450L82 0L157 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,260.0833435058594,252.49999084472657);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,267.0333557128906,249.49998474121094);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,274.6666564941406,249.49998474121094);"><path d="M509 229L692 444L610 444L476 279L338 444L255 444L443 229L249 0L331 0L476 188L626 0L709 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,290.1166687011719,249.49998474121094);"><path d="M949 241L949 300L179 300L304 452L272 486L65 269L272 55L304 89L179 241Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,310.7333068847656,249.49998474121094);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><svg x="317.5166931152344" style="overflow:visible;" y="231.49998474121094" height="26" width="8.5"><path d=" M 1.69 1.70 q 0.00 -0.09 0.08 -0.09 q 0.03 0.00 0.08 0.03 q 1.01 0.61 1.75 1.42 t 1.37 2.06 t 0.95 3.05 t 0.33 4.16 v 0.68 h -1.68 v -0.68 q 0.00 -1.31 -0.04 -2.28 t -0.22 -2.24 t -0.50 -2.23 t -0.91 -1.91 t -1.43 -1.63 q -0.12 -0.10 -0.12 -0.32 z  M 7.40 13.00 v 0.00 h -1.68 v 0.00 z" style="fill:rgb(0, 0, 0);stroke-width:1px;stroke:none;"></path><path d=" M 1.69 24.30 q 0.00 0.09 0.08 0.09 q 0.03 0.00 0.08 -0.03 q 1.01 -0.61 1.75 -1.42 t 1.37 -2.06 t 0.95 -3.05 t 0.33 -4.16 v -0.68 h -1.68 v 0.68 q 0.00 1.31 -0.04 2.28 t -0.22 2.24 t -0.50 2.23 t -0.91 1.91 t -1.43 1.63 q -0.12 0.10 -0.12 0.32 z" style="fill:rgb(0, 0, 0);stroke-width:1px;stroke:none;"></path></svg></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,351.9499816894531,316.81666564941406);"><path d="M399 289C399 391 326 461 234 461C169 461 124 445 77 418L83 352C135 389 185 402 234 402C281 402 321 362 321 288L321 245C171 243 44 201 44 113C44 70 71 -11 158 -11C172 -11 266 -9 324 36L324 0L399 0ZM321 132C321 113 321 88 287 69C258 51 220 50 209 50C161 50 116 73 116 115C116 185 278 192 321 194ZM635 694L560 694L560 0L635 0ZM879 680L790 680L790 591L879 591ZM872 444L797 444L797 0L872 0ZM1399 444L1324 444C1272 299 1181 47 1185 53L1184 53L1045 444L967 444L1139 0L1227 0ZM1827 219C1827 421 1720 461 1649 461C1538 461 1448 355 1448 226C1448 94 1544 -11 1664 -11C1727 -11 1784 13 1823 41L1817 106C1754 54 1688 50 1665 50C1585 50 1521 121 1518 219ZM1523 274C1539 350 1592 400 1649 400C1701 400 1757 366 1770 274Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,383.5166931152344,316.81666564941406);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,389.1666564941406,316.81666564941406);"><path d="M157 214C157 314 229 386 327 388L327 455C238 454 183 405 152 359L152 450L82 450L82 0L157 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,395.4667053222656,319.81667175292966);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,402.4166564941406,316.81666564941406);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,35.45001220703125,366.81666564941406);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,41.616668701171875,369.81667175292966);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,231.50003051757812,134.81666564941406);"><path d="M260 229L443 444L361 444L227 279L89 444L6 444L194 229L0 0L82 0L227 188L377 0L460 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,246.94998168945312,134.81666564941406);"><path d="M949 241L949 300L179 300L304 452L272 486L65 269L272 55L304 89L179 241Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,267.5666809082031,134.81666564941406);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,276.0500183105469,134.81666564941406);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,283.6833190917969,134.81666564941406);"><path d="M424 386L565 386L565 444L424 444L424 571L355 571L355 444L268 444L268 386L352 386L352 119C352 59 366 -11 435 -11C505 -11 556 14 581 27L565 86C539 65 507 53 475 53C438 53 424 83 424 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,294.5500183105469,137.81665649414063);"><path d="M457 331C457 412 453 506 411 588C370 665 301 689 250 689C191 689 121 662 80 571C47 497 42 413 42 331C42 251 46 177 76 103C116 5 192 -22 249 -22C322 -22 385 19 417 89C447 155 457 223 457 331ZM250 40C198 40 157 78 137 151C121 209 120 264 120 343C120 407 120 468 137 524C143 544 168 627 249 627C327 627 353 550 360 531C379 475 379 406 379 343C379 276 379 212 361 148C335 56 282 40 250 40Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,231.50003051757812,156.81666564941406);"><path d="M260 229L443 444L361 444L227 279L89 444L6 444L194 229L0 0L82 0L227 188L377 0L460 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,246.94998168945312,156.81666564941406);"><path d="M949 241L949 300L179 300L304 452L272 486L65 269L272 55L304 89L179 241Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,267.5666809082031,156.81666564941406);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,276.0500183105469,156.81666564941406);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,283.6833190917969,156.81666564941406);"><path d="M424 386L565 386L565 444L424 444L424 571L355 571L355 444L268 444L268 386L352 386L352 119C352 59 366 -11 435 -11C505 -11 556 14 581 27L565 86C539 65 507 53 475 53C438 53 424 83 424 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,294.5500183105469,159.81665649414063);"><path d="M457 331C457 412 453 506 411 588C370 665 301 689 250 689C191 689 121 662 80 571C47 497 42 413 42 331C42 251 46 177 76 103C116 5 192 -22 249 -22C322 -22 385 19 417 89C447 155 457 223 457 331ZM250 40C198 40 157 78 137 151C121 209 120 264 120 343C120 407 120 468 137 524C143 544 168 627 249 627C327 627 353 550 360 531C379 475 379 406 379 343C379 276 379 212 361 148C335 56 282 40 250 40Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,101.5,114.81666564941406);"><path d="M260 229L443 444L361 444L227 279L89 444L6 444L194 229L0 0L82 0L227 188L377 0L460 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,116.95001220703125,114.81666564941406);"><path d="M949 241L949 300L179 300L304 452L272 486L65 269L272 55L304 89L179 241Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,137.56668090820312,114.81666564941406);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,146.05001831054688,114.81666564941406);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,153.68331909179688,114.81666564941406);"><path d="M424 386L565 386L565 444L424 444L424 571L355 571L355 444L268 444L268 386L352 386L352 119C352 59 366 -11 435 -11C505 -11 556 14 581 27L565 86C539 65 507 53 475 53C438 53 424 83 424 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,164.55001831054688,117.81665649414063);"><path d="M457 331C457 412 453 506 411 588C370 665 301 689 250 689C191 689 121 662 80 571C47 497 42 413 42 331C42 251 46 177 76 103C116 5 192 -22 249 -22C322 -22 385 19 417 89C447 155 457 223 457 331ZM250 40C198 40 157 78 137 151C121 209 120 264 120 343C120 407 120 468 137 524C143 544 168 627 249 627C327 627 353 550 360 531C379 475 379 406 379 343C379 276 379 212 361 148C335 56 282 40 250 40Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,231.50003051757812,354.81666564941406);"><path d="M260 229L443 444L361 444L227 279L89 444L6 444L194 229L0 0L82 0L227 188L377 0L460 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,246.94998168945312,354.81666564941406);"><path d="M949 241L949 300L179 300L304 452L272 486L65 269L272 55L304 89L179 241Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,267.5666809082031,354.81666564941406);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,276.0500183105469,354.81666564941406);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g style="transform:matrix(1,0,0,1,283.6833190917969,354.81666564941406);"><path d="M424 386L565 386L565 444L424 444L424 571L355 571L355 444L268 444L268 386L352 386L352 119C352 59 366 -11 435 -11C505 -11 556 14 581 27L565 86C539 65 507 53 475 53C438 53 424 83 424 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,294.5500183105469,357.81667175292966);"><path d="M457 331C457 412 453 506 411 588C370 665 301 689 250 689C191 689 121 662 80 571C47 497 42 413 42 331C42 251 46 177 76 103C116 5 192 -22 249 -22C322 -22 385 19 417 89C447 155 457 223 457 331ZM250 40C198 40 157 78 137 151C121 209 120 264 120 343C120 407 120 468 137 524C143 544 168 627 249 627C327 627 353 550 360 531C379 475 379 406 379 343C379 276 379 212 361 148C335 56 282 40 250 40Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,219.63333129882812,386.40000915527344);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,339.6166687011719,386.40000915527344);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,459.6166687011719,386.40000915527344);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,579.6166687011719,386.40000915527344);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g></g></g></g></g></g></svg>
+</svg>
diff --git a/doc/modules/cassandra/assets/images/ring.svg b/doc/modules/cassandra/assets/images/ring.svg
new file mode 100644
index 0000000..d0db8c5
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/ring.svg
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="651" height="709.4583740234375" style="
+        width:651px;
+        height:709.4583740234375px;
+        background: transparent;
+        fill: none;
+">
+        
+        
+        <svg xmlns="http://www.w3.org/2000/svg" class="role-diagram-draw-area"><g class="shapes-region" style="stroke: black; fill: none;"><g class="composite-shape"><path class="real" d=" M223.5,655 C223.5,634.84 239.84,618.5 260,618.5 C280.16,618.5 296.5,634.84 296.5,655 C296.5,675.16 280.16,691.5 260,691.5 C239.84,691.5 223.5,675.16 223.5,655 Z" style="stroke-width: 1; stroke: rgb(103, 148, 135); fill: rgb(103, 148, 135);"/></g><g class="composite-shape"><path class="real" d=" M229.26,655 C229.26,638.02 243.02,624.26 260,624.26 C276.98,624.26 290.74,638.02 290.74,655 C290.74,671.98 276.98,685.74 260,685.74 C243.02,685.74 229.26,671.98 229.26,655 Z" style="stroke-width: 1; stroke: rgb(202, 194, 126); fill: rgb(202, 194, 126);"/></g><g class="composite-shape"><path class="real" d=" M377.5,595 C377.5,571.53 396.53,552.5 420,552.5 C443.47,552.5 462.5,571.53 462.5,595 C462.5,618.47 443.47,637.5 420,637.5 C396.53,637.5 377.5,618.47 377.5,595 Z" style="stroke-width: 1; stroke: rgb(103, 148, 135); fill: rgb(103, 148, 135);"/></g><g class="composite-shape"><path class="real" d=" M384.06,595 C384.06,575.15 400.15,559.06 420,559.06 C439.85,559.06 455.94,575.15 455.94,595 C455.94,614.85 439.85,630.94 420,630.94 C400.15,630.94 384.06,614.85 384.06,595 Z" style="stroke-width: 1; stroke: rgb(202, 194, 126); fill: rgb(202, 194, 126);"/></g><g class="composite-shape"><path class="real" d=" M390,595 C390,578.43 403.43,565 420,565 C436.57,565 450,578.43 450,595 C450,611.57 436.57,625 420,625 C403.43,625 390,611.57 390,595 Z" style="stroke-width: 1; stroke: rgb(130, 192, 233); fill: rgb(130, 192, 233);"/></g><g class="composite-shape"><path class="real" d=" M444.06,435 C444.06,415.15 460.15,399.06 480,399.06 C499.85,399.06 515.94,415.15 515.94,435 C515.94,454.85 499.85,470.94 480,470.94 C460.15,470.94 444.06,454.85 444.06,435 Z" style="stroke-width: 1; stroke: rgb(202, 194, 126); fill: rgb(202, 194, 126);"/></g><g class="composite-shape"><path class="real" d=" M450,435 C450,418.43 463.43,405 480,405 C496.57,405 510,418.43 510,435 C510,451.57 496.57,465 480,465 C463.43,465 450,451.57 450,435 Z" style="stroke-width: 1; stroke: rgb(130, 192, 233); fill: rgb(130, 192, 233);"/></g><g class="composite-shape"><path class="real" d=" M390,275 C390,258.43 403.43,245 420,245 C436.57,245 450,258.43 450,275 C450,291.57 436.57,305 420,305 C403.43,305 390,291.57 390,275 Z" style="stroke-width: 1; stroke: rgb(130, 192, 233); fill: rgb(130, 192, 233);"/></g><g class="composite-shape"><path class="real" d=" M40,435 C40,313.5 138.5,215 260,215 C381.5,215 480,313.5 480,435 C480,556.5 381.5,655 260,655 C138.5,655 40,556.5 40,435 Z" style="stroke-width: 1; stroke: rgba(0, 0, 0, 0.52); fill: none; stroke-dasharray: 1.125, 3.35;"/></g><g class="grouped-shape"><g class="composite-shape"><path class="real" d=" M90,435 C90,341.11 166.11,265 260,265 C353.89,265 430,341.11 430,435 C430,528.89 353.89,605 260,605 C166.11,605 90,528.89 90,435 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: none;"/></g><g class="composite-shape"><path class="real" d=" M111.25,435 C111.25,352.85 177.85,286.25 260,286.25 C342.15,286.25 408.75,352.85 408.75,435 C408.75,517.15 342.15,583.75 260,583.75 C177.85,583.75 111.25,517.15 111.25,435 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: none;"/></g><g class="composite-shape"><path class="real" d=" M132.5,435 C132.5,364.58 189.58,307.5 260,307.5 C330.42,307.5 387.5,364.58 387.5,435 C387.5,505.42 330.42,562.5 260,562.5 C189.58,562.5 132.5,505.42 132.5,435 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: none;"/></g></g><g class="composite-shape"><path class="real" d=" M235,655 C235,641.19 246.19,630 260,630 C273.81,630 285,641.19 285,655 C285,668.81 273.81,680 260,680 C246.19,680 235,668.81 235,655 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="grouped-shape"><g class="composite-shape"><path class="real" d=" M235,215 C235,201.19 246.19,190 260,190 C273.81,190 285,201.19 285,215 C285,228.81 273.81,240 260,240 C246.19,240 235,228.81 235,215 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g></g><g class="composite-shape"><path class="real" d=" M455,435 C455,421.19 466.19,410 480,410 C493.81,410 505,421.19 505,435 C505,448.81 493.81,460 480,460 C466.19,460 455,448.81 455,435 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M15,435 C15,421.19 26.19,410 40,410 C53.81,410 65,421.19 65,435 C65,448.81 53.81,460 40,460 C26.19,460 15,448.81 15,435 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M395,275 C395,261.19 406.19,250 420,250 C433.81,250 445,261.19 445,275 C445,288.81 433.81,300 420,300 C406.19,300 395,288.81 395,275 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M395,595 C395,581.19 406.19,570 420,570 C433.81,570 445,581.19 445,595 C445,608.81 433.81,620 420,620 C406.19,620 395,608.81 395,595 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M75,595 C75,581.19 86.19,570 100,570 C113.81,570 125,581.19 125,595 C125,608.81 113.81,620 100,620 C86.19,620 75,608.81 75,595 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="grouped-shape"><g class="composite-shape"><path class="real" d=" M75,275 C75,261.19 86.19,250 100,250 C113.81,250 125,261.19 125,275 C125,288.81 113.81,300 100,300 C86.19,300 75,288.81 75,275 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M268.22,265.22 C401.65,271.85 490.67,424.28 380,555" style="stroke: rgb(130, 192, 233); stroke-width: 6; fill: none;"/><g stroke="rgba(130,192,233,1)" transform="matrix(0.9999897039488793,0.00453784048117526,-0.00453784048117526,0.9999897039488793,260,265)" style="stroke: rgb(130, 192, 233); stroke-width: 6;"><circle cx="0" cy="0" r="9.045000000000002"/></g><g stroke="rgba(130,192,233,1)" fill="rgba(130,192,233,1)" transform="matrix(-0.6461239796429636,0.763232469782529,-0.763232469782529,-0.6461239796429636,380,555)" style="stroke: rgb(130, 192, 233); fill: rgb(130, 192, 233); stroke-width: 6;"><circle cx="0" cy="0" r="9.045000000000002"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M366.47,330.7 C447.68,407.15 414.54,574.62 260,583.75" style="stroke: rgb(202, 194, 126); stroke-width: 6; fill: none;"/><g stroke="rgba(202,194,126,1)" transform="matrix(0.7722902597301384,0.6352698282824042,-0.6352698282824042,0.7722902597301384,360,325)" style="stroke: rgb(202, 194, 126); stroke-width: 6;"><circle cx="0" cy="0" r="9.045000000000002"/></g><g stroke="rgba(202,194,126,1)" fill="rgba(202,194,126,1)" transform="matrix(-0.9982604689368237,0.05895791853545039,-0.05895791853545039,-0.9982604689368237,260,583.7499999999999)" style="stroke: rgb(202, 194, 126); fill: rgb(202, 194, 126); stroke-width: 6;"><circle cx="0" cy="0" r="9.045000000000002"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M387.19,443.58 C380.1,535.72 254.02,615.51 160,515" style="stroke: rgb(103, 148, 135); stroke-width: 6; fill: none;"/><g stroke="rgba(103,148,135,1)" transform="matrix(0.004537840481175261,0.9999897039488793,-0.9999897039488793,0.004537840481175261,387.5,435)" style="stroke: rgb(103, 148, 135); stroke-width: 6;"><circle cx="0" cy="0" r="9.045000000000002"/></g><g stroke="rgba(103,148,135,1)" fill="rgba(103,148,135,1)" transform="matrix(-0.6831463259165826,-0.7302815192695721,0.7302815192695721,-0.6831463259165826,160,515)" style="stroke: rgb(103, 148, 135); fill: rgb(103, 148, 135); stroke-width: 6;"><circle cx="0" cy="0" r="9.045000000000002"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M345,195 L316.4,271.25" style="stroke: rgb(130, 192, 233); stroke-width: 3; fill: none;"/><g stroke="rgba(130,192,233,1)" transform="matrix(0.3511880698803796,-0.9363049394154095,0.9363049394154095,0.3511880698803796,315,275)" style="stroke: rgb(130, 192, 233); stroke-width: 3;"><path d=" M17.49,-5.26 Q7.94,-0.72 0,0 Q7.94,0.72 17.49,5.26"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M485,330 L403.62,368.3" style="stroke: rgb(202, 194, 126); stroke-width: 3; fill: none;"/><g stroke="rgba(202,194,126,1)" transform="matrix(0.9048270524660194,-0.425779291565073,0.425779291565073,0.9048270524660194,400,370)" style="stroke: rgb(202, 194, 126); stroke-width: 3;"><path d=" M17.49,-5.26 Q7.94,-0.72 0,0 Q7.94,0.72 17.49,5.26"/></g></g><g/></g><g/><g/><g/></svg>
+        <svg xmlns="http://www.w3.org/2000/svg" width="649" height="707.4583740234375" style="width:649px;height:707.4583740234375px;font-family:Asana-Math, Asana;background:transparent;"><g><g><g><g><g><g style="transform:matrix(1,0,0,1,12.171875,40.31333587646485);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136ZM829 220C829 354 729 461 610 461C487 461 390 351 390 220C390 88 492 -11 609 -11C729 -11 829 90 829 220ZM609 53C540 53 468 109 468 230C468 351 544 400 609 400C679 400 751 348 751 230C751 112 683 53 609 53ZM1140 272L1308 444L1218 444L1015 236L1015 694L943 694L943 0L1012 0L1012 141L1092 224L1248 0L1330 0ZM1760 219C1760 421 1653 461 1582 461C1471 461 1381 355 1381 226C1381 94 1477 -11 1597 -11C1660 -11 1717 13 1756 41L1750 106C1687 54 1621 50 1598 50C1518 50 1454 121 1451 219ZM1456 274C1472 350 1525 400 1582 400C1634 400 1690 366 1703 274ZM2224 298C2224 364 2209 455 2087 455C1997 455 1948 387 1942 379L1942 450L1870 450L1870 0L1948 0L1948 245C1948 311 1973 394 2049 394C2145 394 2146 323 2146 291L2146 0L2224 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,68.578125,40.31333587646485);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,76.71356201171875,40.31333587646485);"><path d="M435 298C435 364 420 455 298 455C208 455 159 387 153 379L153 450L81 450L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,90.05731201171875,44.635999999999996);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,100.078125,40.31333587646485);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,115.55731201171875,40.31333587646485);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,139.2552490234375,40.31333587646485);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,148.80731201171875,44.635999999999996);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g></g><g><g style="transform:matrix(1,0,0,1,12.171875,71.98);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136ZM829 220C829 354 729 461 610 461C487 461 390 351 390 220C390 88 492 -11 609 -11C729 -11 829 90 829 220ZM609 53C540 53 468 109 468 230C468 351 544 400 609 400C679 400 751 348 751 230C751 112 683 53 609 53ZM1140 272L1308 444L1218 444L1015 236L1015 694L943 694L943 0L1012 0L1012 141L1092 224L1248 0L1330 0ZM1760 219C1760 421 1653 461 1582 461C1471 461 1381 355 1381 226C1381 94 1477 -11 1597 -11C1660 -11 1717 13 1756 41L1750 106C1687 54 1621 50 1598 50C1518 50 1454 121 1451 219ZM1456 274C1472 350 1525 400 1582 400C1634 400 1690 366 1703 274ZM2224 298C2224 364 2209 455 2087 455C1997 455 1948 387 1942 379L1942 450L1870 450L1870 0L1948 0L1948 245C1948 311 1973 394 2049 394C2145 394 2146 323 2146 291L2146 0L2224 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,68.578125,71.98);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,76.71356201171875,71.98);"><path d="M435 298C435 364 420 455 298 455C208 455 159 387 153 379L153 450L81 450L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,90.05731201171875,76.30266412353515);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,100.078125,71.98);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,115.55731201171875,71.98);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,139.2552490234375,71.98);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,148.80731201171875,76.30266412353515);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g></g><g><g style="transform:matrix(1,0,0,1,12.171875,103.64666412353516);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136ZM829 220C829 354 729 461 610 461C487 461 390 351 390 220C390 88 492 -11 609 -11C729 -11 829 90 829 220ZM609 53C540 53 468 109 468 230C468 351 544 400 609 400C679 400 751 348 751 230C751 112 683 53 609 53ZM1140 272L1308 444L1218 444L1015 236L1015 694L943 694L943 0L1012 0L1012 141L1092 224L1248 0L1330 0ZM1760 219C1760 421 1653 461 1582 461C1471 461 1381 355 1381 226C1381 94 1477 -11 1597 -11C1660 -11 1717 13 1756 41L1750 106C1687 54 1621 50 1598 50C1518 50 1454 121 1451 219ZM1456 274C1472 350 1525 400 1582 400C1634 400 1690 366 1703 274ZM2224 298C2224 364 2209 455 2087 455C1997 455 1948 387 1942 379L1942 450L1870 450L1870 0L1948 0L1948 245C1948 311 1973 394 2049 394C2145 394 2146 323 2146 291L2146 0L2224 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,68.578125,103.64666412353516);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,76.71356201171875,103.64666412353516);"><path d="M435 298C435 364 420 455 298 455C208 455 159 387 153 379L153 450L81 450L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,90.05731201171875,107.96933587646484);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,100.078125,103.64666412353516);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,115.55731201171875,103.64666412353516);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,139.2552490234375,103.64666412353516);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,148.80731201171875,107.96933587646484);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g></g><g><g style="transform:matrix(1,0,0,1,12.171875,135.31333587646483);"><path d="M124 111C94 111 67 83 67 53C67 23 94 -5 123 -5C155 -5 183 22 183 53C183 83 155 111 124 111ZM373 111C343 111 316 83 316 53C316 23 343 -5 372 -5C404 -5 432 22 432 53C432 83 404 111 373 111ZM622 111C592 111 565 83 565 53C565 23 592 -5 621 -5C653 -5 681 22 681 53C681 83 653 111 622 111Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,245.3802490234375,659.4047891235351);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,262.578125,665.2589131469726);"><path d="M459 253C459 366 378 446 264 446C216 446 180 443 127 396L127 605L432 604L432 689L75 690L75 322L95 316C142 363 169 377 218 377C314 377 374 309 374 201C374 90 310 25 201 25C147 25 97 43 83 69L37 151L13 137C36 80 48 48 62 4C90 -11 130 -20 173 -20C301 -20 459 89 459 253Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,465.3802490234375,444.4047891235352);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,482.578125,450.2588826293945);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,25.380218505859375,444.4047891235352);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,42.578125,450.2588826293945);"><path d="M409 603L47 -1L157 -1L497 659L497 689L44 689L44 477L74 477L81 533C89 595 96 603 142 603Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,405.3802490234375,284.4047891235352);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,422.578125,290.2588826293945);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,405.3802490234375,604.4047891235351);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,422.578125,610.2589131469726);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,85.38021850585938,604.4047891235351);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,102.578125,610.2589131469726);"><path d="M131 331C152 512 241 611 421 665L379 689C283 657 242 637 191 593C88 506 32 384 32 247C32 82 112 -20 241 -20C371 -20 468 83 468 219C468 334 399 409 293 409C216 409 184 370 131 331ZM255 349C331 349 382 283 382 184C382 80 331 13 254 13C169 13 123 86 123 220C123 255 127 274 138 291C160 325 207 349 255 349Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,332.04168701171875,37.480000000000004);"><path d="M157 214C157 314 229 386 327 388L327 455C238 454 183 405 152 359L152 450L82 450L82 0L157 0ZM739 289C739 391 666 461 574 461C509 461 464 445 417 418L423 352C475 389 525 402 574 402C621 402 661 362 661 288L661 245C511 243 384 201 384 113C384 70 411 -11 498 -11C512 -11 606 -9 664 36L664 0L739 0ZM661 132C661 113 661 88 627 69C598 51 560 50 549 50C501 50 456 73 456 115C456 185 618 192 661 194ZM1254 298C1254 364 1239 455 1117 455C1027 455 978 387 972 379L972 450L900 450L900 0L978 0L978 245C978 311 1003 394 1079 394C1175 394 1176 323 1176 291L1176 0L1254 0ZM1686 391C1708 391 1736 395 1760 395C1778 395 1817 392 1819 392L1808 455C1738 455 1680 436 1650 423C1629 440 1595 455 1555 455C1469 455 1396 383 1396 292C1396 255 1409 219 1429 193C1400 152 1400 113 1400 108C1400 82 1409 53 1426 32C1374 1 1362 -45 1362 -71C1362 -146 1461 -206 1583 -206C1706 -206 1805 -147 1805 -70C1805 69 1638 69 1599 69L1511 69C1498 69 1453 69 1453 122C1453 133 1457 149 1464 158C1485 143 1518 129 1555 129C1645 129 1715 203 1715 292C1715 340 1693 377 1682 392ZM1555 186C1518 186 1466 209 1466 292C1466 375 1518 398 1555 398C1598 398 1645 370 1645 292C1645 214 1598 186 1555 186ZM1600 -3C1622 -3 1735 -3 1735 -72C1735 -116 1666 -149 1584 -149C1503 -149 1432 -118 1432 -71C1432 -68 1432 -3 1510 -3ZM2247 219C2247 421 2140 461 2069 461C1958 461 1868 355 1868 226C1868 94 1964 -11 2084 -11C2147 -11 2204 13 2243 41L2237 106C2174 54 2108 50 2085 50C2005 50 1941 121 1938 219ZM1943 274C1959 350 2012 400 2069 400C2121 400 2177 366 2190 274Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,387.76043701171875,37.480000000000004);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,395.8958740234375,37.480000000000004);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,405.44793701171875,41.80266412353515);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,415.46875,37.480000000000004);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,426.46875,37.480000000000004);"><path d="M424 386L565 386L565 444L424 444L424 571L355 571L355 444L268 444L268 386L352 386L352 119C352 59 366 -11 435 -11C505 -11 556 14 581 27L565 86C539 65 507 53 475 53C438 53 424 83 424 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,442.1146240234375,41.80266412353515);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,452.13543701171875,37.480000000000004);"><path d="M245 -184L254 -171C248 -140 246 -112 246 -44L246 578C246 628 250 665 250 691C250 706 249 717 245 726L51 726L45 720L45 694L49 689L87 689C114 689 136 683 148 673C157 664 160 649 160 616L160 -75C160 -108 157 -122 148 -131C136 -141 114 -147 87 -147L49 -147L45 -152L45 -178L51 -184Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,460.2708740234375,37.480000000000004);"><path d="" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,471.2708740234375,37.480000000000004);"><path d="M949 272L743 486L711 452L836 300L65 300L65 241L836 241L711 89L743 55Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,500.96875,37.480000000000004);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,510.32293701171875,37.480000000000004);"><path d="M435 298C435 364 420 455 298 455C208 455 159 387 153 379L153 450L81 450L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,523.6666870117188,41.80266412353515);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,533.6875,37.480000000000004);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,544.6875,37.480000000000004);"><path d="M684 298C684 364 669 455 547 455C457 455 408 387 402 379L402 450L330 450L330 0L408 0L408 245C408 311 433 394 509 394C605 394 606 323 606 291L606 0L684 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,564.125,41.80266412353515);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,574.1458740234375,37.480000000000004);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,585.1458740234375,37.480000000000004);"><path d="M684 298C684 364 669 455 547 455C457 455 408 387 402 379L402 450L330 450L330 0L408 0L408 245C408 311 433 394 509 394C605 394 606 323 606 291L606 0L684 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,604.5833740234375,41.80266412353515);"><path d="M372 174L471 174L471 236L372 236L372 667L281 667L28 236L28 174L293 174L293 0L372 0ZM106 236C158 323 299 564 299 622L299 236Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,615.822998046875,37.480000000000004);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g><g><g><g><g><g style="transform:matrix(1,0,0,1,332.04168701171875,70.14666412353516);"><path d="M157 214C157 314 229 386 327 388L327 455C238 454 183 405 152 359L152 450L82 450L82 0L157 0ZM739 289C739 391 666 461 574 461C509 461 464 445 417 418L423 352C475 389 525 402 574 402C621 402 661 362 661 288L661 245C511 243 384 201 384 113C384 70 411 -11 498 -11C512 -11 606 -9 664 36L664 0L739 0ZM661 132C661 113 661 88 627 69C598 51 560 50 549 50C501 50 456 73 456 115C456 185 618 192 661 194ZM1254 298C1254 364 1239 455 1117 455C1027 455 978 387 972 379L972 450L900 450L900 0L978 0L978 245C978 311 1003 394 1079 394C1175 394 1176 323 1176 291L1176 0L1254 0ZM1686 391C1708 391 1736 395 1760 395C1778 395 1817 392 1819 392L1808 455C1738 455 1680 436 1650 423C1629 440 1595 455 1555 455C1469 455 1396 383 1396 292C1396 255 1409 219 1429 193C1400 152 1400 113 1400 108C1400 82 1409 53 1426 32C1374 1 1362 -45 1362 -71C1362 -146 1461 -206 1583 -206C1706 -206 1805 -147 1805 -70C1805 69 1638 69 1599 69L1511 69C1498 69 1453 69 1453 122C1453 133 1457 149 1464 158C1485 143 1518 129 1555 129C1645 129 1715 203 1715 292C1715 340 1693 377 1682 392ZM1555 186C1518 186 1466 209 1466 292C1466 375 1518 398 1555 398C1598 398 1645 370 1645 292C1645 214 1598 186 1555 186ZM1600 -3C1622 -3 1735 -3 1735 -72C1735 -116 1666 -149 1584 -149C1503 -149 1432 -118 1432 -71C1432 -68 1432 -3 1510 -3ZM2247 219C2247 421 2140 461 2069 461C1958 461 1868 355 1868 226C1868 94 1964 -11 2084 -11C2147 -11 2204 13 2243 41L2237 106C2174 54 2108 50 2085 50C2005 50 1941 121 1938 219ZM1943 274C1959 350 2012 400 2069 400C2121 400 2177 366 2190 274Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,387.76043701171875,70.14666412353516);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,395.8958740234375,70.14666412353516);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,405.44793701171875,74.46933587646484);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,415.46875,70.14666412353516);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,426.46875,70.14666412353516);"><path d="M424 386L565 386L565 444L424 444L424 571L355 571L355 444L268 444L268 386L352 386L352 119C352 59 366 -11 435 -11C505 -11 556 14 581 27L565 86C539 65 507 53 475 53C438 53 424 83 424 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,442.1146240234375,74.46933587646484);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,452.13543701171875,70.14666412353516);"><path d="M245 -184L254 -171C248 -140 246 -112 246 -44L246 578C246 628 250 665 250 691C250 706 249 717 245 726L51 726L45 720L45 694L49 689L87 689C114 689 136 683 148 673C157 664 160 649 160 616L160 -75C160 -108 157 -122 148 -131C136 -141 114 -147 87 -147L49 -147L45 -152L45 -178L51 -184Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,460.2708740234375,70.14666412353516);"><path d="" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,471.2708740234375,70.14666412353516);"><path d="M949 272L743 486L711 452L836 300L65 300L65 241L836 241L711 89L743 55Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,500.96875,70.14666412353516);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,510.32293701171875,70.14666412353516);"><path d="M435 298C435 364 420 455 298 455C208 455 159 387 153 379L153 450L81 450L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,523.6666870117188,74.46933587646484);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,533.6875,70.14666412353516);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,544.6875,70.14666412353516);"><path d="M684 298C684 364 669 455 547 455C457 455 408 387 402 379L402 450L330 450L330 0L408 0L408 245C408 311 433 394 509 394C605 394 606 323 606 291L606 0L684 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,564.125,74.46933587646484);"><path d="M372 174L471 174L471 236L372 236L372 667L281 667L28 236L28 174L293 174L293 0L372 0ZM106 236C158 323 299 564 299 622L299 236Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,574.1458740234375,70.14666412353516);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,585.1458740234375,70.14666412353516);"><path d="M684 298C684 364 669 455 547 455C457 455 408 387 402 379L402 450L330 450L330 0L408 0L408 245C408 311 433 394 509 394C605 394 606 323 606 291L606 0L684 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,604.5833740234375,74.46933587646484);"><path d="M153 602L416 602L416 667L81 667L81 292L147 292C164 332 201 372 259 372C306 372 360 330 360 208C360 40 238 40 229 40C162 40 101 79 72 134L39 77C80 19 149 -22 230 -22C349 -22 449 78 449 206C449 333 363 434 260 434C220 434 182 419 153 392Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,615.822998046875,70.14666412353516);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g><g><g><g><g><g style="transform:matrix(1,0,0,1,332.04168701171875,102.81333587646485);"><path d="M157 214C157 314 229 386 327 388L327 455C238 454 183 405 152 359L152 450L82 450L82 0L157 0ZM739 289C739 391 666 461 574 461C509 461 464 445 417 418L423 352C475 389 525 402 574 402C621 402 661 362 661 288L661 245C511 243 384 201 384 113C384 70 411 -11 498 -11C512 -11 606 -9 664 36L664 0L739 0ZM661 132C661 113 661 88 627 69C598 51 560 50 549 50C501 50 456 73 456 115C456 185 618 192 661 194ZM1254 298C1254 364 1239 455 1117 455C1027 455 978 387 972 379L972 450L900 450L900 0L978 0L978 245C978 311 1003 394 1079 394C1175 394 1176 323 1176 291L1176 0L1254 0ZM1686 391C1708 391 1736 395 1760 395C1778 395 1817 392 1819 392L1808 455C1738 455 1680 436 1650 423C1629 440 1595 455 1555 455C1469 455 1396 383 1396 292C1396 255 1409 219 1429 193C1400 152 1400 113 1400 108C1400 82 1409 53 1426 32C1374 1 1362 -45 1362 -71C1362 -146 1461 -206 1583 -206C1706 -206 1805 -147 1805 -70C1805 69 1638 69 1599 69L1511 69C1498 69 1453 69 1453 122C1453 133 1457 149 1464 158C1485 143 1518 129 1555 129C1645 129 1715 203 1715 292C1715 340 1693 377 1682 392ZM1555 186C1518 186 1466 209 1466 292C1466 375 1518 398 1555 398C1598 398 1645 370 1645 292C1645 214 1598 186 1555 186ZM1600 -3C1622 -3 1735 -3 1735 -72C1735 -116 1666 -149 1584 -149C1503 -149 1432 -118 1432 -71C1432 -68 1432 -3 1510 -3ZM2247 219C2247 421 2140 461 2069 461C1958 461 1868 355 1868 226C1868 94 1964 -11 2084 -11C2147 -11 2204 13 2243 41L2237 106C2174 54 2108 50 2085 50C2005 50 1941 121 1938 219ZM1943 274C1959 350 2012 400 2069 400C2121 400 2177 366 2190 274Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,387.76043701171875,102.81333587646485);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,395.8958740234375,102.81333587646485);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,405.44793701171875,107.13600762939453);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,415.46875,102.81333587646485);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,426.46875,102.81333587646485);"><path d="M424 386L565 386L565 444L424 444L424 571L355 571L355 444L268 444L268 386L352 386L352 119C352 59 366 -11 435 -11C505 -11 556 14 581 27L565 86C539 65 507 53 475 53C438 53 424 83 424 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,442.1146240234375,107.13600762939453);"><path d="M372 174L471 174L471 236L372 236L372 667L281 667L28 236L28 174L293 174L293 0L372 0ZM106 236C158 323 299 564 299 622L299 236Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,452.13543701171875,102.81333587646485);"><path d="M245 -184L254 -171C248 -140 246 -112 246 -44L246 578C246 628 250 665 250 691C250 706 249 717 245 726L51 726L45 720L45 694L49 689L87 689C114 689 136 683 148 673C157 664 160 649 160 616L160 -75C160 -108 157 -122 148 -131C136 -141 114 -147 87 -147L49 -147L45 -152L45 -178L51 -184Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,460.2708740234375,102.81333587646485);"><path d="" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,471.2708740234375,102.81333587646485);"><path d="M949 272L743 486L711 452L836 300L65 300L65 241L836 241L711 89L743 55Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,500.96875,102.81333587646485);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,510.32293701171875,102.81333587646485);"><path d="M435 298C435 364 420 455 298 455C208 455 159 387 153 379L153 450L81 450L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,523.6666870117188,107.13600762939453);"><path d="M372 174L471 174L471 236L372 236L372 667L281 667L28 236L28 174L293 174L293 0L372 0ZM106 236C158 323 299 564 299 622L299 236Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,533.6875,102.81333587646485);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,544.6875,102.81333587646485);"><path d="M684 298C684 364 669 455 547 455C457 455 408 387 402 379L402 450L330 450L330 0L408 0L408 245C408 311 433 394 509 394C605 394 606 323 606 291L606 0L684 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,564.125,107.13600762939453);"><path d="M153 602L416 602L416 667L81 667L81 292L147 292C164 332 201 372 259 372C306 372 360 330 360 208C360 40 238 40 229 40C162 40 101 79 72 134L39 77C80 19 149 -22 230 -22C349 -22 449 78 449 206C449 333 363 434 260 434C220 434 182 419 153 392Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,574.1458740234375,102.81333587646485);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,585.1458740234375,102.81333587646485);"><path d="M684 298C684 364 669 455 547 455C457 455 408 387 402 379L402 450L330 450L330 0L408 0L408 245C408 311 433 394 509 394C605 394 606 323 606 291L606 0L684 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,604.5833740234375,107.13600762939453);"><path d="M415 669C364 689 323 689 309 689C168 689 42 546 42 327C42 40 166 -22 251 -22C311 -22 354 1 393 47C438 99 457 145 457 226C457 356 391 469 295 469C263 469 187 461 126 385C139 549 218 630 310 630C348 630 380 623 415 609ZM127 223C127 237 127 239 128 251C128 326 173 407 256 407C304 407 332 382 352 344C373 307 375 271 375 226C375 191 375 144 351 103C334 74 308 40 251 40C145 40 130 189 127 223Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,615.822998046875,102.81333587646485);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g><g><text x="332.04168701171875" y="113.66666412353516" style="white-space:pre;stroke:none;fill:rgb(0, 0, 0);font-size:21.6px;font-family:Arial, Helvetica, sans-serif;font-weight:400;font-style:normal;dominant-baseline:text-before-edge;text-decoration:none solid rgb(0, 0, 0);">                                           ...</text></g></g><g><g><g><g><g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,284.9114990234375,163.04063262939454);"><path d="M435 298C435 364 420 455 298 455C236 455 188 424 156 383L156 694L81 694L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0ZM914 289C914 391 841 461 749 461C684 461 639 445 592 418L598 352C650 389 700 402 749 402C796 402 836 362 836 288L836 245C686 243 559 201 559 113C559 70 586 -11 673 -11C687 -11 781 -9 839 36L839 0L914 0ZM836 132C836 113 836 88 802 69C773 51 735 50 724 50C676 50 631 73 631 115C631 185 793 192 836 194ZM1354 128C1354 183 1317 217 1315 220C1276 255 1249 261 1199 270C1144 281 1098 291 1098 340C1098 402 1170 402 1183 402C1215 402 1268 398 1325 364L1337 429C1285 453 1244 461 1193 461C1168 461 1027 461 1027 330C1027 281 1056 249 1081 230C1112 208 1134 204 1189 193C1225 186 1283 174 1283 121C1283 52 1204 52 1189 52C1108 52 1052 89 1034 101L1022 33C1054 17 1109 -11 1190 -11C1328 -11 1354 75 1354 128ZM1811 298C1811 364 1796 455 1674 455C1612 455 1564 424 1532 383L1532 694L1457 694L1457 0L1535 0L1535 245C1535 311 1560 394 1636 394C1732 394 1733 323 1733 291L1733 0L1811 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,323.46356201171875,163.70728912353516);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,330.234375,163.04063262939454);"><path d="M105 469L137 665C138 667 138 669 138 671C138 689 116 709 95 709C74 709 52 689 52 670L52 665L85 469ZM286 469L318 665C319 667 319 669 319 670C319 689 297 709 276 709C254 709 233 690 233 670L233 665L266 469ZM546 386L656 386L656 444L543 444L543 563C543 637 610 644 636 644C656 644 683 642 717 627L717 694C705 697 674 705 637 705C543 705 471 634 471 534L471 444L397 444L397 386L471 386L471 0L546 0ZM1143 220C1143 354 1043 461 924 461C801 461 704 351 704 220C704 88 806 -11 923 -11C1043 -11 1143 90 1143 220ZM923 53C854 53 782 109 782 230C782 351 858 400 923 400C993 400 1065 348 1065 230C1065 112 997 53 923 53ZM1642 220C1642 354 1542 461 1423 461C1300 461 1203 351 1203 220C1203 88 1305 -11 1422 -11C1542 -11 1642 90 1642 220ZM1422 53C1353 53 1281 109 1281 230C1281 351 1357 400 1422 400C1492 400 1564 348 1564 230C1564 112 1496 53 1422 53ZM1777 469L1809 665C1810 667 1810 669 1810 671C1810 689 1788 709 1767 709C1746 709 1724 689 1724 670L1724 665L1757 469ZM1958 469L1990 665C1991 667 1991 669 1991 670C1991 689 1969 709 1948 709C1926 709 1905 690 1905 670L1905 665L1938 469Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,371.86981201171875,163.70728912353516);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g></g><g><g style="transform:matrix(1,0,0,1,284.9114990234375,187.04063262939454);"><path d="" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,324.52606201171875,187.04063262939454);"><path d="M949 272L743 486L711 452L836 300L65 300L65 241L836 241L711 89L743 55Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,349.2552490234375,187.70728912353516);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,357.0364990234375,187.04063262939454);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,368.96356201171875,191.30603912353516);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.01428,0,0,-0.01428,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,377.30731201171875,187.04063262939454);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,386.46356201171875,187.04063262939454);"><path d="M273 388L280 368L312 389C349 412 352 414 359 414C370 414 377 404 377 389C377 338 336 145 295 2L302 -9C327 -2 350 4 372 8C391 134 412 199 458 268C512 352 587 414 632 414C643 414 649 405 649 390C649 372 646 351 638 319L586 107C577 70 573 47 573 31C573 6 584 -9 603 -9C629 -9 665 12 763 85L753 103L727 86C698 67 676 56 666 56C659 56 653 65 653 76C653 81 654 92 655 96L721 372C728 401 732 429 732 446C732 469 721 482 701 482C659 482 590 444 531 389C493 354 465 320 413 247L451 408C455 426 457 438 457 449C457 470 449 482 434 482C413 482 374 460 301 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,403.46356201171875,191.30603912353516);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.01428,0,0,-0.01428,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,411.80731201171875,187.04063262939454);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,420.96356201171875,187.04063262939454);"><path d="M273 388L280 368L312 389C349 412 352 414 359 414C370 414 377 404 377 389C377 338 336 145 295 2L302 -9C327 -2 350 4 372 8C391 134 412 199 458 268C512 352 587 414 632 414C643 414 649 405 649 390C649 372 646 351 638 319L586 107C577 70 573 47 573 31C573 6 584 -9 603 -9C629 -9 665 12 763 85L753 103L727 86C698 67 676 56 666 56C659 56 653 65 653 76C653 81 654 92 655 96L721 372C728 401 732 429 732 446C732 469 721 482 701 482C659 482 590 444 531 389C493 354 465 320 413 247L451 408C455 426 457 438 457 449C457 470 449 482 434 482C413 482 374 460 301 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,437.96356201171875,191.30603912353516);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.01428,0,0,-0.01428,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,447.3177490234375,187.70728912353516);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,245.3802490234375,224.40478912353515);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,262.578125,230.25888262939452);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,85.38021850585938,284.4047891235352);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,102.578125,290.2588826293945);"><path d="M168 345C127 326 110 315 88 294C50 256 30 211 30 159C30 56 113 -20 226 -20C356 -20 464 82 464 206C464 286 427 329 313 381C404 443 436 485 436 545C436 631 365 689 259 689C140 689 53 613 53 508C53 440 80 402 168 345ZM284 295C347 267 385 218 385 164C385 80 322 14 241 14C156 14 101 74 101 167C101 240 130 286 204 331ZM223 423C160 454 128 494 128 544C128 610 176 655 247 655C320 655 368 607 368 534C368 477 343 438 278 396Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,457.4114990234375,316.56666412353513);"><path d="M435 298C435 364 420 455 298 455C236 455 188 424 156 383L156 694L81 694L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0ZM914 289C914 391 841 461 749 461C684 461 639 445 592 418L598 352C650 389 700 402 749 402C796 402 836 362 836 288L836 245C686 243 559 201 559 113C559 70 586 -11 673 -11C687 -11 781 -9 839 36L839 0L914 0ZM836 132C836 113 836 88 802 69C773 51 735 50 724 50C676 50 631 73 631 115C631 185 793 192 836 194ZM1354 128C1354 183 1317 217 1315 220C1276 255 1249 261 1199 270C1144 281 1098 291 1098 340C1098 402 1170 402 1183 402C1215 402 1268 398 1325 364L1337 429C1285 453 1244 461 1193 461C1168 461 1027 461 1027 330C1027 281 1056 249 1081 230C1112 208 1134 204 1189 193C1225 186 1283 174 1283 121C1283 52 1204 52 1189 52C1108 52 1052 89 1034 101L1022 33C1054 17 1109 -11 1190 -11C1328 -11 1354 75 1354 128ZM1811 298C1811 364 1796 455 1674 455C1612 455 1564 424 1532 383L1532 694L1457 694L1457 0L1535 0L1535 245C1535 311 1560 394 1636 394C1732 394 1733 323 1733 291L1733 0L1811 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,495.96356201171875,317.2333511352539);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,502.734375,316.56666412353513);"><path d="M105 469L137 665C138 667 138 669 138 671C138 689 116 709 95 709C74 709 52 689 52 670L52 665L85 469ZM286 469L318 665C319 667 319 669 319 670C319 689 297 709 276 709C254 709 233 690 233 670L233 665L266 469ZM527 694L452 694L452 0L530 0L530 46C554 24 597 -11 664 -11C764 -11 850 89 850 223C850 347 782 455 688 455C649 455 587 445 527 396ZM530 335C546 359 582 394 637 394C696 394 772 351 772 223C772 93 688 50 627 50C588 50 555 68 530 114ZM1284 289C1284 391 1211 461 1119 461C1054 461 1009 445 962 418L968 352C1020 389 1070 402 1119 402C1166 402 1206 362 1206 288L1206 245C1056 243 929 201 929 113C929 70 956 -11 1043 -11C1057 -11 1151 -9 1209 36L1209 0L1284 0ZM1206 132C1206 113 1206 88 1172 69C1143 51 1105 50 1094 50C1046 50 1001 73 1001 115C1001 185 1163 192 1206 194ZM1521 214C1521 314 1593 386 1691 388L1691 455C1602 454 1547 405 1516 359L1516 450L1446 450L1446 0L1521 0ZM1809 469L1841 665C1842 667 1842 669 1842 671C1842 689 1820 709 1799 709C1778 709 1756 689 1756 670L1756 665L1789 469ZM1990 469L2022 665C2023 667 2023 669 2023 670C2023 689 2001 709 1980 709C1958 709 1937 690 1937 670L1937 665L1970 469Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,545.015625,317.2333511352539);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g></g></g></g></g><g><g><g><g><g style="transform:matrix(1,0,0,1,457.4114990234375,343.2333511352539);"><path d="" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,497.02606201171875,343.2333511352539);"><path d="M949 272L743 486L711 452L836 300L65 300L65 241L836 241L711 89L743 55Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,521.7552490234375,343.9000076293945);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,529.5364990234375,343.2333511352539);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,541.4635620117188,347.4987576293945);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.01428,0,0,-0.01428,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,549.8073120117188,343.2333511352539);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,558.9635620117188,343.2333511352539);"><path d="M273 388L280 368L312 389C349 412 352 414 359 414C370 414 377 404 377 389C377 338 336 145 295 2L302 -9C327 -2 350 4 372 8C391 134 412 199 458 268C512 352 587 414 632 414C643 414 649 405 649 390C649 372 646 351 638 319L586 107C577 70 573 47 573 31C573 6 584 -9 603 -9C629 -9 665 12 763 85L753 103L727 86C698 67 676 56 666 56C659 56 653 65 653 76C653 81 654 92 655 96L721 372C728 401 732 429 732 446C732 469 721 482 701 482C659 482 590 444 531 389C493 354 465 320 413 247L451 408C455 426 457 438 457 449C457 470 449 482 434 482C413 482 374 460 301 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,575.9635620117188,347.4987576293945);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.01428,0,0,-0.01428,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,584.3073120117188,343.2333511352539);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,593.4635620117188,343.2333511352539);"><path d="M273 388L280 368L312 389C349 412 352 414 359 414C370 414 377 404 377 389C377 338 336 145 295 2L302 -9C327 -2 350 4 372 8C391 134 412 199 458 268C512 352 587 414 632 414C643 414 649 405 649 390C649 372 646 351 638 319L586 107C577 70 573 47 573 31C573 6 584 -9 603 -9C629 -9 665 12 763 85L753 103L727 86C698 67 676 56 666 56C659 56 653 65 653 76C653 81 654 92 655 96L721 372C728 401 732 429 732 446C732 469 721 482 701 482C659 482 590 444 531 389C493 354 465 320 413 247L451 408C455 426 457 438 457 449C457 470 449 482 434 482C413 482 374 460 301 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,610.463623046875,347.4987576293945);"><path d="M459 253C459 366 378 446 264 446C216 446 180 443 127 396L127 605L432 604L432 689L75 690L75 322L95 316C142 363 169 377 218 377C314 377 374 309 374 201C374 90 310 25 201 25C147 25 97 43 83 69L37 151L13 137C36 80 48 48 62 4C90 -11 130 -20 173 -20C301 -20 459 89 459 253Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.01428,0,0,-0.01428,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,619.8177490234375,343.9000076293945);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,254.71356201171875,324.78125762939453);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,261.33856201171875,328.4521011352539);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,331.71356201171875,354.78125762939453);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,338.33856201171875,358.4521011352539);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,359.71356201171875,439.78125762939453);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,366.33856201171875,443.4521011352539);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g></svg>
+</svg>
diff --git a/doc/modules/cassandra/assets/images/vnodes.svg b/doc/modules/cassandra/assets/images/vnodes.svg
new file mode 100644
index 0000000..71b4fa2
--- /dev/null
+++ b/doc/modules/cassandra/assets/images/vnodes.svg
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="651" height="384.66668701171875" style="
+        width:651px;
+        height:384.66668701171875px;
+        background: transparent;
+        fill: none;
+">
+        
+        
+        <svg xmlns="http://www.w3.org/2000/svg" class="role-diagram-draw-area"><g class="shapes-region" style="stroke: black; fill: none;"><g class="composite-shape"><path class="real" d=" M40.4,190 C40.4,107.38 107.38,40.4 190,40.4 C272.62,40.4 339.6,107.38 339.6,190 C339.6,272.62 272.62,339.6 190,339.6 C107.38,339.6 40.4,272.62 40.4,190 Z" style="stroke-width: 1; stroke: rgba(0, 0, 0, 0.52); fill: none; stroke-dasharray: 1.125, 3.35;"/></g><g class="composite-shape"><path class="real" d=" M160,340 C160,323.43 173.43,310 190,310 C206.57,310 220,323.43 220,340 C220,356.57 206.57,370 190,370 C173.43,370 160,356.57 160,340 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(130, 192, 233); stroke-dasharray: 6, 6;"/></g><g class="composite-shape"><path class="real" d=" M160,40 C160,23.43 173.43,10 190,10 C206.57,10 220,23.43 220,40 C220,56.57 206.57,70 190,70 C173.43,70 160,56.57 160,40 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(130, 192, 233); stroke-dasharray: 6, 6;"/></g><g class="composite-shape"><path class="real" d=" M310,190 C310,173.43 323.43,160 340,160 C356.57,160 370,173.43 370,190 C370,206.57 356.57,220 340,220 C323.43,220 310,206.57 310,190 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(103, 148, 135); stroke-dasharray: 1.125, 3.35;"/></g><g class="composite-shape"><path class="real" d=" M10,190 C10,173.43 23.43,160 40,160 C56.57,160 70,173.43 70,190 C70,206.57 56.57,220 40,220 C23.43,220 10,206.57 10,190 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(103, 148, 135); stroke-dasharray: 1.125, 3.35;"/></g><g class="composite-shape"><path class="real" d=" M270,80 C270,63.43 283.43,50 300,50 C316.57,50 330,63.43 330,80 C330,96.57 316.57,110 300,110 C283.43,110 270,96.57 270,80 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(202, 194, 126);"/></g><g class="composite-shape"><path class="real" d=" M270,300 C270,283.43 283.43,270 300,270 C316.57,270 330,283.43 330,300 C330,316.57 316.57,330 300,330 C283.43,330 270,316.57 270,300 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M50,300 C50,283.43 63.43,270 80,270 C96.57,270 110,283.43 110,300 C110,316.57 96.57,330 80,330 C63.43,330 50,316.57 50,300 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(202, 194, 126);"/></g><g class="composite-shape"><path class="real" d=" M50,80 C50,63.43 63.43,50 80,50 C96.57,50 110,63.43 110,80 C110,96.57 96.57,110 80,110 C63.43,110 50,96.57 50,80 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M380,158.4 C380,146.47 389.67,136.8 401.6,136.8 C413.53,136.8 423.2,146.47 423.2,158.4 C423.2,170.33 413.53,180 401.6,180 C389.67,180 380,170.33 380,158.4 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(103, 148, 135); stroke-dasharray: 1.125, 3.35;"/></g><g class="composite-shape"><path class="real" d=" M380,101.6 C380,89.67 389.67,80 401.6,80 C413.53,80 423.2,89.67 423.2,101.6 C423.2,113.53 413.53,123.2 401.6,123.2 C389.67,123.2 380,113.53 380,101.6 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(130, 192, 233); stroke-dasharray: 6, 6;"/></g><g class="composite-shape"><path class="real" d=" M380,218.4 C380,206.47 389.67,196.8 401.6,196.8 C413.53,196.8 423.2,206.47 423.2,218.4 C423.2,230.33 413.53,240 401.6,240 C389.67,240 380,230.33 380,218.4 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(202, 194, 126);"/></g><g class="composite-shape"><path class="real" d=" M380,278.4 C380,266.47 389.67,256.8 401.6,256.8 C413.53,256.8 423.2,266.47 423.2,278.4 C423.2,290.33 413.53,300 401.6,300 C389.67,300 380,290.33 380,278.4 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M430,80 L640,80 L640,300 L430,300 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: none;"/></g><g/></g><g/><g/><g/></svg>
+        <svg xmlns="http://www.w3.org/2000/svg" width="649" height="382.66668701171875" style="width:649px;height:382.66668701171875px;font-family:Asana-Math, Asana;background:transparent;"><g><g><g><g><g><g style="transform:matrix(1,0,0,1,178.65625,348.9985620117188);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,189.3021240234375,354.852625);"><path d="M459 253C459 366 378 446 264 446C216 446 180 443 127 396L127 605L432 604L432 689L75 690L75 322L95 316C142 363 169 377 218 377C314 377 374 309 374 201C374 90 310 25 201 25C147 25 97 43 83 69L37 151L13 137C36 80 48 48 62 4C90 -11 130 -20 173 -20C301 -20 459 89 459 253Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,178.65625,49.800625);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,189.3021240234375,55.65471850585938);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,328.25,199.40481201171875);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,338.8958740234375,205.258875);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,29.052093505859375,199.40481201171875);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,39.69793701171875,205.258875);"><path d="M409 603L47 -1L157 -1L497 659L497 689L44 689L44 477L74 477L81 533C89 595 96 603 142 603Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,287.44793701171875,90.60271850585937);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,298.09375,96.45679675292969);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,287.44793701171875,308.1964685058594);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,298.09375,314.05056201171874);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,69.85418701171875,308.1964685058594);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,80.5,314.05056201171874);"><path d="M131 331C152 512 241 611 421 665L379 689C283 657 242 637 191 593C88 506 32 384 32 247C32 82 112 -20 241 -20C371 -20 468 83 468 219C468 334 399 409 293 409C216 409 184 370 131 331ZM255 349C331 349 382 283 382 184C382 80 331 13 254 13C169 13 123 86 123 220C123 255 127 274 138 291C160 325 207 349 255 349Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,69.85418701171875,90.60271850585937);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,80.5,96.45679675292969);"><path d="M168 345C127 326 110 315 88 294C50 256 30 211 30 159C30 56 113 -20 226 -20C356 -20 464 82 464 206C464 286 427 329 313 381C404 443 436 485 436 545C436 631 365 689 259 689C140 689 53 613 53 508C53 440 80 402 168 345ZM284 295C347 267 385 218 385 164C385 80 322 14 241 14C156 14 101 74 101 167C101 240 130 286 204 331ZM223 423C160 454 128 494 128 544C128 610 176 655 247 655C320 655 368 607 368 534C368 477 343 438 278 396Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,386.9739990234375,167.800625);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,404.171875,173.65471850585936);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,386.9739990234375,110.99856201171875);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,404.171875,116.852625);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,386.9739990234375,227.800625);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,404.171875,233.65471850585936);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,386.9739990234375,287.800625);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,404.171875,293.65471850585936);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,438.125,111.64668701171875);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390ZM349 152C349 46 394 -11 477 -11C532 -11 592 15 637 57C699 116 743 230 743 331C743 425 693 482 610 482C506 482 349 382 349 152ZM573 444C634 444 667 399 667 315C667 219 636 113 592 60C574 39 548 27 517 27C459 27 425 72 425 151C425 264 464 387 513 427C526 438 549 444 573 444ZM1015 722L1003 733C951 707 915 698 843 691L839 670L887 670C911 670 921 663 921 646C921 638 920 629 919 622L871 354C857 279 841 210 789 1L798 -9L865 8L888 132C897 180 921 225 955 259C1024 59 1056 -9 1081 -9C1094 -9 1118 4 1162 35L1208 67L1200 86L1157 62C1143 54 1136 52 1128 52C1118 52 1111 58 1102 75C1067 139 1045 193 1006 316L1020 330C1081 391 1127 416 1177 416C1185 416 1196 414 1213 410L1230 473C1212 479 1194 482 1182 482C1116 482 1033 405 908 230ZM1571 111L1547 94C1494 56 1446 36 1410 36C1363 36 1334 73 1334 133C1334 158 1337 185 1342 214C1359 218 1468 248 1493 259C1578 296 1617 342 1617 404C1617 451 1583 482 1533 482C1465 496 1355 423 1318 349C1288 299 1258 180 1258 113C1258 35 1302 -11 1374 -11C1431 -11 1487 17 1579 92ZM1356 274C1373 343 1393 386 1422 412C1440 428 1471 440 1495 440C1524 440 1543 420 1543 388C1543 344 1508 297 1456 272C1428 258 1392 247 1347 237ZM1655 388L1662 368L1694 389C1731 412 1734 414 1741 414C1752 414 1759 404 1759 389C1759 338 1718 145 1677 2L1684 -9C1709 -2 1732 4 1754 8C1773 134 1794 199 1840 268C1894 352 1969 414 2014 414C2025 414 2031 405 2031 390C2031 372 2028 351 2020 319L1968 107C1959 70 1955 47 1955 31C1955 6 1966 -9 1985 -9C2011 -9 2047 12 2145 85L2135 103L2109 86C2080 67 2058 56 2048 56C2041 56 2035 65 2035 76C2035 81 2036 92 2037 96L2103 372C2110 401 2114 429 2114 446C2114 469 2103 482 2083 482C2041 482 1972 444 1913 389C1875 354 1847 320 1795 247L1833 408C1837 426 1839 438 1839 449C1839 470 1831 482 1816 482C1795 482 1756 460 1683 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,491.6458740234375,111.64668701171875);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,499.78125,111.64668701171875);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,514.1041870117188,115.96934350585937);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,524.125,111.64668701171875);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,539.6041870117188,111.64668701171875);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,563.3021240234375,111.64668701171875);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,572.65625,111.64668701171875);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,581.5208740234375,115.96934350585937);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,591.5416870117188,111.64668701171875);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,602.541748046875,111.64668701171875);"><path d="M374 390L318 107C317 99 305 61 305 31C305 6 316 -9 335 -9C370 -9 405 11 483 74L514 99L504 117L459 86C430 66 410 56 399 56C390 56 385 64 385 76C385 102 399 183 428 328L441 390L548 390L559 440C521 436 487 434 449 434C465 528 476 577 494 631L483 646C463 634 436 622 405 610L380 440C336 419 310 408 292 403L290 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,617.5,115.96934350585937);"><path d="M459 253C459 366 378 446 264 446C216 446 180 443 127 396L127 605L432 604L432 689L75 690L75 322L95 316C142 363 169 377 218 377C314 377 374 309 374 201C374 90 310 25 201 25C147 25 97 43 83 69L37 151L13 137C36 80 48 48 62 4C90 -11 130 -20 173 -20C301 -20 459 89 459 253Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,628.7396240234375,111.64668701171875);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,439.125,164.64668701171874);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390ZM349 152C349 46 394 -11 477 -11C532 -11 592 15 637 57C699 116 743 230 743 331C743 425 693 482 610 482C506 482 349 382 349 152ZM573 444C634 444 667 399 667 315C667 219 636 113 592 60C574 39 548 27 517 27C459 27 425 72 425 151C425 264 464 387 513 427C526 438 549 444 573 444ZM1015 722L1003 733C951 707 915 698 843 691L839 670L887 670C911 670 921 663 921 646C921 638 920 629 919 622L871 354C857 279 841 210 789 1L798 -9L865 8L888 132C897 180 921 225 955 259C1024 59 1056 -9 1081 -9C1094 -9 1118 4 1162 35L1208 67L1200 86L1157 62C1143 54 1136 52 1128 52C1118 52 1111 58 1102 75C1067 139 1045 193 1006 316L1020 330C1081 391 1127 416 1177 416C1185 416 1196 414 1213 410L1230 473C1212 479 1194 482 1182 482C1116 482 1033 405 908 230ZM1571 111L1547 94C1494 56 1446 36 1410 36C1363 36 1334 73 1334 133C1334 158 1337 185 1342 214C1359 218 1468 248 1493 259C1578 296 1617 342 1617 404C1617 451 1583 482 1533 482C1465 496 1355 423 1318 349C1288 299 1258 180 1258 113C1258 35 1302 -11 1374 -11C1431 -11 1487 17 1579 92ZM1356 274C1373 343 1393 386 1422 412C1440 428 1471 440 1495 440C1524 440 1543 420 1543 388C1543 344 1508 297 1456 272C1428 258 1392 247 1347 237ZM1655 388L1662 368L1694 389C1731 412 1734 414 1741 414C1752 414 1759 404 1759 389C1759 338 1718 145 1677 2L1684 -9C1709 -2 1732 4 1754 8C1773 134 1794 199 1840 268C1894 352 1969 414 2014 414C2025 414 2031 405 2031 390C2031 372 2028 351 2020 319L1968 107C1959 70 1955 47 1955 31C1955 6 1966 -9 1985 -9C2011 -9 2047 12 2145 85L2135 103L2109 86C2080 67 2058 56 2048 56C2041 56 2035 65 2035 76C2035 81 2036 92 2037 96L2103 372C2110 401 2114 429 2114 446C2114 469 2103 482 2083 482C2041 482 1972 444 1913 389C1875 354 1847 320 1795 247L1833 408C1837 426 1839 438 1839 449C1839 470 1831 482 1816 482C1795 482 1756 460 1683 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,492.6458740234375,164.64668701171874);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,500.78125,164.64668701171874);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,515.1041870117188,168.96934350585937);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,525.125,164.64668701171874);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,540.6041870117188,164.64668701171874);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,564.3021240234375,164.64668701171874);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,573.65625,164.64668701171874);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,582.5208740234375,168.96934350585937);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,592.5416870117188,164.64668701171874);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,603.541748046875,164.64668701171874);"><path d="M374 390L318 107C317 99 305 61 305 31C305 6 316 -9 335 -9C370 -9 405 11 483 74L514 99L504 117L459 86C430 66 410 56 399 56C390 56 385 64 385 76C385 102 399 183 428 328L441 390L548 390L559 440C521 436 487 434 449 434C465 528 476 577 494 631L483 646C463 634 436 622 405 610L380 440C336 419 310 408 292 403L290 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,618.5,168.96934350585937);"><path d="M409 603L47 -1L157 -1L497 659L497 689L44 689L44 477L74 477L81 533C89 595 96 603 142 603Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,629.7396240234375,164.64668701171874);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,439.125,221.64668701171874);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390ZM349 152C349 46 394 -11 477 -11C532 -11 592 15 637 57C699 116 743 230 743 331C743 425 693 482 610 482C506 482 349 382 349 152ZM573 444C634 444 667 399 667 315C667 219 636 113 592 60C574 39 548 27 517 27C459 27 425 72 425 151C425 264 464 387 513 427C526 438 549 444 573 444ZM1015 722L1003 733C951 707 915 698 843 691L839 670L887 670C911 670 921 663 921 646C921 638 920 629 919 622L871 354C857 279 841 210 789 1L798 -9L865 8L888 132C897 180 921 225 955 259C1024 59 1056 -9 1081 -9C1094 -9 1118 4 1162 35L1208 67L1200 86L1157 62C1143 54 1136 52 1128 52C1118 52 1111 58 1102 75C1067 139 1045 193 1006 316L1020 330C1081 391 1127 416 1177 416C1185 416 1196 414 1213 410L1230 473C1212 479 1194 482 1182 482C1116 482 1033 405 908 230ZM1571 111L1547 94C1494 56 1446 36 1410 36C1363 36 1334 73 1334 133C1334 158 1337 185 1342 214C1359 218 1468 248 1493 259C1578 296 1617 342 1617 404C1617 451 1583 482 1533 482C1465 496 1355 423 1318 349C1288 299 1258 180 1258 113C1258 35 1302 -11 1374 -11C1431 -11 1487 17 1579 92ZM1356 274C1373 343 1393 386 1422 412C1440 428 1471 440 1495 440C1524 440 1543 420 1543 388C1543 344 1508 297 1456 272C1428 258 1392 247 1347 237ZM1655 388L1662 368L1694 389C1731 412 1734 414 1741 414C1752 414 1759 404 1759 389C1759 338 1718 145 1677 2L1684 -9C1709 -2 1732 4 1754 8C1773 134 1794 199 1840 268C1894 352 1969 414 2014 414C2025 414 2031 405 2031 390C2031 372 2028 351 2020 319L1968 107C1959 70 1955 47 1955 31C1955 6 1966 -9 1985 -9C2011 -9 2047 12 2145 85L2135 103L2109 86C2080 67 2058 56 2048 56C2041 56 2035 65 2035 76C2035 81 2036 92 2037 96L2103 372C2110 401 2114 429 2114 446C2114 469 2103 482 2083 482C2041 482 1972 444 1913 389C1875 354 1847 320 1795 247L1833 408C1837 426 1839 438 1839 449C1839 470 1831 482 1816 482C1795 482 1756 460 1683 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,492.6458740234375,221.64668701171874);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,500.78125,221.64668701171874);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,515.1041870117188,225.96934350585937);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,525.125,221.64668701171874);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,540.6041870117188,221.64668701171874);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,564.3021240234375,221.64668701171874);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,573.65625,221.64668701171874);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,582.5208740234375,225.96934350585937);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,592.5416870117188,221.64668701171874);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,603.541748046875,221.64668701171874);"><path d="M374 390L318 107C317 99 305 61 305 31C305 6 316 -9 335 -9C370 -9 405 11 483 74L514 99L504 117L459 86C430 66 410 56 399 56C390 56 385 64 385 76C385 102 399 183 428 328L441 390L548 390L559 440C521 436 487 434 449 434C465 528 476 577 494 631L483 646C463 634 436 622 405 610L380 440C336 419 310 408 292 403L290 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,618.5,225.96934350585937);"><path d="M131 331C152 512 241 611 421 665L379 689C283 657 242 637 191 593C88 506 32 384 32 247C32 82 112 -20 241 -20C371 -20 468 83 468 219C468 334 399 409 293 409C216 409 184 370 131 331ZM255 349C331 349 382 283 382 184C382 80 331 13 254 13C169 13 123 86 123 220C123 255 127 274 138 291C160 325 207 349 255 349Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,629.7396240234375,221.64668701171874);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,439.125,281.64668701171877);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390ZM349 152C349 46 394 -11 477 -11C532 -11 592 15 637 57C699 116 743 230 743 331C743 425 693 482 610 482C506 482 349 382 349 152ZM573 444C634 444 667 399 667 315C667 219 636 113 592 60C574 39 548 27 517 27C459 27 425 72 425 151C425 264 464 387 513 427C526 438 549 444 573 444ZM1015 722L1003 733C951 707 915 698 843 691L839 670L887 670C911 670 921 663 921 646C921 638 920 629 919 622L871 354C857 279 841 210 789 1L798 -9L865 8L888 132C897 180 921 225 955 259C1024 59 1056 -9 1081 -9C1094 -9 1118 4 1162 35L1208 67L1200 86L1157 62C1143 54 1136 52 1128 52C1118 52 1111 58 1102 75C1067 139 1045 193 1006 316L1020 330C1081 391 1127 416 1177 416C1185 416 1196 414 1213 410L1230 473C1212 479 1194 482 1182 482C1116 482 1033 405 908 230ZM1571 111L1547 94C1494 56 1446 36 1410 36C1363 36 1334 73 1334 133C1334 158 1337 185 1342 214C1359 218 1468 248 1493 259C1578 296 1617 342 1617 404C1617 451 1583 482 1533 482C1465 496 1355 423 1318 349C1288 299 1258 180 1258 113C1258 35 1302 -11 1374 -11C1431 -11 1487 17 1579 92ZM1356 274C1373 343 1393 386 1422 412C1440 428 1471 440 1495 440C1524 440 1543 420 1543 388C1543 344 1508 297 1456 272C1428 258 1392 247 1347 237ZM1655 388L1662 368L1694 389C1731 412 1734 414 1741 414C1752 414 1759 404 1759 389C1759 338 1718 145 1677 2L1684 -9C1709 -2 1732 4 1754 8C1773 134 1794 199 1840 268C1894 352 1969 414 2014 414C2025 414 2031 405 2031 390C2031 372 2028 351 2020 319L1968 107C1959 70 1955 47 1955 31C1955 6 1966 -9 1985 -9C2011 -9 2047 12 2145 85L2135 103L2109 86C2080 67 2058 56 2048 56C2041 56 2035 65 2035 76C2035 81 2036 92 2037 96L2103 372C2110 401 2114 429 2114 446C2114 469 2103 482 2083 482C2041 482 1972 444 1913 389C1875 354 1847 320 1795 247L1833 408C1837 426 1839 438 1839 449C1839 470 1831 482 1816 482C1795 482 1756 460 1683 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,492.6458740234375,281.64668701171877);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,500.78125,281.64668701171877);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,515.1041870117188,285.9693435058594);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,525.125,281.64668701171877);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,540.6041870117188,281.64668701171877);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,564.3021240234375,281.64668701171877);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,573.65625,281.64668701171877);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,582.5208740234375,285.9693435058594);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,592.5416870117188,281.64668701171877);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,603.541748046875,281.64668701171877);"><path d="M374 390L318 107C317 99 305 61 305 31C305 6 316 -9 335 -9C370 -9 405 11 483 74L514 99L504 117L459 86C430 66 410 56 399 56C390 56 385 64 385 76C385 102 399 183 428 328L441 390L548 390L559 440C521 436 487 434 449 434C465 528 476 577 494 631L483 646C463 634 436 622 405 610L380 440C336 419 310 408 292 403L290 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,618.5,285.9693435058594);"><path d="M168 345C127 326 110 315 88 294C50 256 30 211 30 159C30 56 113 -20 226 -20C356 -20 464 82 464 206C464 286 427 329 313 381C404 443 436 485 436 545C436 631 365 689 259 689C140 689 53 613 53 508C53 440 80 402 168 345ZM284 295C347 267 385 218 385 164C385 80 322 14 241 14C156 14 101 74 101 167C101 240 130 286 204 331ZM223 423C160 454 128 494 128 544C128 610 176 655 247 655C320 655 368 607 368 534C368 477 343 438 278 396Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,629.7396240234375,281.64668701171877);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g></g><g><g><g style="transform:matrix(1,0,0,1,471.734375,61.95);"><path d="M35 623L177 623L295 624L295 0L384 0L384 624L501 623L643 623L643 689L35 689ZM627 221C627 85 732 -11 846 -11C960 -11 1066 84 1066 220C1066 345 974 461 846 461C728 461 627 357 627 221ZM705 231C705 352 782 399 846 399C910 399 987 353 987 231C987 106 913 53 846 53C780 53 705 105 705 231ZM1180 0L1250 0L1250 142L1330 224L1486 0L1568 0L1378 273L1546 445L1456 445L1252 236L1252 722L1180 722ZM1592 226C1592 102 1679 -11 1810 -11C1873 -11 1925 11 1968 40L1962 106C1918 74 1873 51 1810 51C1734 51 1666 117 1662 220L1972 220L1972 249C1963 420 1865 461 1794 461C1688 461 1592 360 1592 226ZM1667 275C1678 324 1718 399 1794 399C1823 399 1898 386 1915 275ZM2082 0L2160 0L2160 240C2160 314 2185 394 2262 394C2360 394 2360 319 2360 281L2360 0L2438 0L2438 288C2438 358 2429 455 2301 455C2213 455 2169 398 2155 380L2155 450L2082 450ZM2952 0L3030 0L3030 624C3039 580 3063 517 3102 411L3252 22L3324 22L3478 423L3538 587L3548 624L3549 592L3549 0L3627 0L3627 694L3512 694L3358 292C3309 164 3293 114 3290 92L3289 92C3285 115 3261 182 3220 293L3066 694L2952 694ZM3771 113C3771 67 3801 -11 3885 -11C3995 -11 4050 34 4051 35L4051 0L4127 0L4127 305C4122 389 4056 461 3961 461C3898 461 3852 445 3804 417L3810 350C3843 373 3885 401 3960 401C3973 401 3999 400 4023 371C4048 341 4048 307 4049 279L4049 245C3946 244 3771 219 3771 113ZM3844 114C3844 177 3977 192 4049 193L4049 130C4049 65 3980 51 3937 51C3883 51 3844 78 3844 114ZM4288 -195L4366 -195L4366 46C4399 15 4442 -11 4501 -11C4598 -11 4687 83 4687 224C4687 343 4624 455 4529 455C4495 455 4427 448 4363 396L4363 445L4288 445ZM4366 126L4366 334C4372 341 4406 391 4472 391C4554 391 4608 310 4608 222C4608 116 4533 50 4463 50C4404 50 4366 102 4366 126Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02595,0,0,-0.02595,0,0);"></path></g></g></g></svg>
+</svg>
diff --git a/doc/modules/cassandra/examples/BASH/add_repo_keys.sh b/doc/modules/cassandra/examples/BASH/add_repo_keys.sh
new file mode 100644
index 0000000..4c1d5f1
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/add_repo_keys.sh
@@ -0,0 +1 @@
+$ curl https://downloads.apache.org/cassandra/KEYS | sudo apt-key add -
diff --git a/doc/modules/cassandra/examples/BASH/apt-get_cass.sh b/doc/modules/cassandra/examples/BASH/apt-get_cass.sh
new file mode 100644
index 0000000..9614b29
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/apt-get_cass.sh
@@ -0,0 +1 @@
+$ sudo apt-get install cassandra
diff --git a/doc/modules/cassandra/examples/BASH/apt-get_update.sh b/doc/modules/cassandra/examples/BASH/apt-get_update.sh
new file mode 100644
index 0000000..b50b7ac
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/apt-get_update.sh
@@ -0,0 +1 @@
+$ sudo apt-get update
diff --git a/doc/modules/cassandra/examples/BASH/check_backups.sh b/doc/modules/cassandra/examples/BASH/check_backups.sh
new file mode 100644
index 0000000..212c3d2
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/check_backups.sh
@@ -0,0 +1 @@
+$ cd ./cassandra/data/data/cqlkeyspace/t-d132e240c21711e9bbee19821dcea330/backups && ls -l
diff --git a/doc/modules/cassandra/examples/BASH/cqlsh_localhost.sh b/doc/modules/cassandra/examples/BASH/cqlsh_localhost.sh
new file mode 100644
index 0000000..7bc1c39
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/cqlsh_localhost.sh
@@ -0,0 +1 @@
+$ bin/cqlsh localhost
diff --git a/doc/modules/cassandra/examples/BASH/curl_install.sh b/doc/modules/cassandra/examples/BASH/curl_install.sh
new file mode 100644
index 0000000..23e7c01
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/curl_install.sh
@@ -0,0 +1 @@
+$ curl -OL http://apache.mirror.digitalpacific.com.au/cassandra/{cass-tag-3x}/apache-cassandra-{cass-tag-3x}-bin.tar.gz
diff --git a/doc/modules/cassandra/examples/BASH/curl_verify_sha.sh b/doc/modules/cassandra/examples/BASH/curl_verify_sha.sh
new file mode 100644
index 0000000..bde80ca
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/curl_verify_sha.sh
@@ -0,0 +1 @@
+$ curl -L https://downloads.apache.org/cassandra/{cass-tag-3x}/apache-cassandra-{cass-tag-3x}-bin.tar.gz.sha256
diff --git a/doc/modules/cassandra/examples/BASH/docker_cqlsh.sh b/doc/modules/cassandra/examples/BASH/docker_cqlsh.sh
new file mode 100644
index 0000000..92a4a8f
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/docker_cqlsh.sh
@@ -0,0 +1 @@
+docker exec -it cass_cluster cqlsh
diff --git a/doc/modules/cassandra/examples/BASH/docker_pull.sh b/doc/modules/cassandra/examples/BASH/docker_pull.sh
new file mode 100644
index 0000000..67e5e22
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/docker_pull.sh
@@ -0,0 +1 @@
+docker pull cassandra:{cass-docker-tag-3x}
diff --git a/doc/modules/cassandra/examples/BASH/docker_remove.sh b/doc/modules/cassandra/examples/BASH/docker_remove.sh
new file mode 100644
index 0000000..bf95630
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/docker_remove.sh
@@ -0,0 +1 @@
+docker rm cassandra
diff --git a/doc/modules/cassandra/examples/BASH/docker_run.sh b/doc/modules/cassandra/examples/BASH/docker_run.sh
new file mode 100644
index 0000000..bb4ecdb
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/docker_run.sh
@@ -0,0 +1 @@
+docker run --name cass_cluster cassandra:{cass-docker-tag-3x}
diff --git a/doc/modules/cassandra/examples/BASH/docker_run_qs.sh b/doc/modules/cassandra/examples/BASH/docker_run_qs.sh
new file mode 100644
index 0000000..7416f5d
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/docker_run_qs.sh
@@ -0,0 +1,3 @@
+docker run --rm -it -v /<currentdir>/scripts:/scripts  \
+-v /<currentdir/cqlshrc:/.cassandra/cqlshrc  \
+--env CQLSH_HOST=host.docker.internal --env CQLSH_PORT=9042  nuvo/docker-cqlsh
diff --git a/doc/modules/cassandra/examples/BASH/find_backups.sh b/doc/modules/cassandra/examples/BASH/find_backups.sh
new file mode 100644
index 0000000..56744bb
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/find_backups.sh
@@ -0,0 +1 @@
+$ find -name backups
diff --git a/doc/modules/cassandra/examples/BASH/find_snapshots.sh b/doc/modules/cassandra/examples/BASH/find_snapshots.sh
new file mode 100644
index 0000000..7abae2b4
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/find_snapshots.sh
@@ -0,0 +1 @@
+$ find -name snapshots
diff --git a/doc/modules/cassandra/examples/BASH/find_sstables.sh b/doc/modules/cassandra/examples/BASH/find_sstables.sh
new file mode 100644
index 0000000..5156903
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/find_sstables.sh
@@ -0,0 +1 @@
+find /var/lib/cassandra/data/ -type f | grep -v -- -ib- | grep -v "/snapshots"
diff --git a/doc/modules/cassandra/examples/BASH/find_two_snapshots.sh b/doc/modules/cassandra/examples/BASH/find_two_snapshots.sh
new file mode 100644
index 0000000..6e97b4b
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/find_two_snapshots.sh
@@ -0,0 +1 @@
+$ cd ./cassandra/data/data/catalogkeyspace/journal-296a2d30c22a11e9b1350d927649052c/snapshots && ls -l
diff --git a/doc/modules/cassandra/examples/BASH/flush_and_check.sh b/doc/modules/cassandra/examples/BASH/flush_and_check.sh
new file mode 100644
index 0000000..5f966e3
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/flush_and_check.sh
@@ -0,0 +1,2 @@
+$ nodetool flush cqlkeyspace t
+$ cd ./cassandra/data/data/cqlkeyspace/t-d132e240c21711e9bbee19821dcea330/backups && ls -l
diff --git a/doc/modules/cassandra/examples/BASH/get_deb_package.sh b/doc/modules/cassandra/examples/BASH/get_deb_package.sh
new file mode 100644
index 0000000..0b866a8
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/get_deb_package.sh
@@ -0,0 +1,2 @@
+$ echo "deb https://debian.cassandra.apache.org 311x main" | sudo tee -a /etc/apt/sources.list.d/cassandra.sources.list
+deb https://debian.cassandra.apache.org 311x main
diff --git a/doc/modules/cassandra/examples/BASH/java_verify.sh b/doc/modules/cassandra/examples/BASH/java_verify.sh
new file mode 100644
index 0000000..da7832f
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/java_verify.sh
@@ -0,0 +1 @@
+$ java -version
diff --git a/doc/modules/cassandra/examples/BASH/nodetool_clearsnapshot.sh b/doc/modules/cassandra/examples/BASH/nodetool_clearsnapshot.sh
new file mode 100644
index 0000000..a327ad1
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/nodetool_clearsnapshot.sh
@@ -0,0 +1 @@
+$ nodetool clearsnapshot -t magazine cqlkeyspace
diff --git a/doc/modules/cassandra/examples/BASH/nodetool_clearsnapshot_all.sh b/doc/modules/cassandra/examples/BASH/nodetool_clearsnapshot_all.sh
new file mode 100644
index 0000000..a22841d
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/nodetool_clearsnapshot_all.sh
@@ -0,0 +1 @@
+$ nodetool clearsnapshot -all cqlkeyspace
diff --git a/doc/modules/cassandra/examples/BASH/nodetool_flush.sh b/doc/modules/cassandra/examples/BASH/nodetool_flush.sh
new file mode 100644
index 0000000..960b852
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/nodetool_flush.sh
@@ -0,0 +1,3 @@
+$ nodetool flush cqlkeyspace t
+$ nodetool flush cqlkeyspace t2
+$ nodetool flush catalogkeyspace journal magazine
diff --git a/doc/modules/cassandra/examples/BASH/nodetool_flush_table.sh b/doc/modules/cassandra/examples/BASH/nodetool_flush_table.sh
new file mode 100644
index 0000000..2c236de
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/nodetool_flush_table.sh
@@ -0,0 +1 @@
+$ nodetool flush cqlkeyspace t
diff --git a/doc/modules/cassandra/examples/BASH/nodetool_list_snapshots.sh b/doc/modules/cassandra/examples/BASH/nodetool_list_snapshots.sh
new file mode 100644
index 0000000..76633f0
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/nodetool_list_snapshots.sh
@@ -0,0 +1 @@
+$ nodetool listsnapshots
diff --git a/doc/modules/cassandra/examples/BASH/nodetool_snapshot.sh b/doc/modules/cassandra/examples/BASH/nodetool_snapshot.sh
new file mode 100644
index 0000000..c74e467
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/nodetool_snapshot.sh
@@ -0,0 +1 @@
+$ nodetool help snapshot
diff --git a/doc/modules/cassandra/examples/BASH/nodetool_status.sh b/doc/modules/cassandra/examples/BASH/nodetool_status.sh
new file mode 100644
index 0000000..a9b768d
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/nodetool_status.sh
@@ -0,0 +1 @@
+$ bin/nodetool status
diff --git a/doc/modules/cassandra/examples/BASH/nodetool_status_nobin.sh b/doc/modules/cassandra/examples/BASH/nodetool_status_nobin.sh
new file mode 100644
index 0000000..d7adbd3
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/nodetool_status_nobin.sh
@@ -0,0 +1 @@
+$ nodetool status
diff --git a/doc/modules/cassandra/examples/BASH/run_cqlsh.sh b/doc/modules/cassandra/examples/BASH/run_cqlsh.sh
new file mode 100644
index 0000000..ae8cbbd
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/run_cqlsh.sh
@@ -0,0 +1 @@
+$ bin/cqlsh
diff --git a/doc/modules/cassandra/examples/BASH/run_cqlsh_nobin.sh b/doc/modules/cassandra/examples/BASH/run_cqlsh_nobin.sh
new file mode 100644
index 0000000..5517fbf
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/run_cqlsh_nobin.sh
@@ -0,0 +1 @@
+$ cqlsh
diff --git a/doc/modules/cassandra/examples/BASH/snapshot_backup2.sh b/doc/modules/cassandra/examples/BASH/snapshot_backup2.sh
new file mode 100644
index 0000000..6d29f0a
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/snapshot_backup2.sh
@@ -0,0 +1 @@
+$ nodetool snapshot --tag catalog-ks catalogkeyspace
diff --git a/doc/modules/cassandra/examples/BASH/snapshot_both_backups.sh b/doc/modules/cassandra/examples/BASH/snapshot_both_backups.sh
new file mode 100644
index 0000000..0966070
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/snapshot_both_backups.sh
@@ -0,0 +1 @@
+$ nodetool snapshot --tag catalog-cql-ks catalogkeyspace, cqlkeyspace
diff --git a/doc/modules/cassandra/examples/BASH/snapshot_files.sh b/doc/modules/cassandra/examples/BASH/snapshot_files.sh
new file mode 100644
index 0000000..916f0e5
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/snapshot_files.sh
@@ -0,0 +1 @@
+$ cd catalog-ks && ls -l
diff --git a/doc/modules/cassandra/examples/BASH/snapshot_mult_ks.sh b/doc/modules/cassandra/examples/BASH/snapshot_mult_ks.sh
new file mode 100644
index 0000000..fed3d3c
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/snapshot_mult_ks.sh
@@ -0,0 +1 @@
+$ nodetool snapshot --kt-list catalogkeyspace.journal,cqlkeyspace.t --tag multi-ks
diff --git a/doc/modules/cassandra/examples/BASH/snapshot_mult_tables.sh b/doc/modules/cassandra/examples/BASH/snapshot_mult_tables.sh
new file mode 100644
index 0000000..ad3a0d2
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/snapshot_mult_tables.sh
@@ -0,0 +1 @@
+$ nodetool snapshot --kt-list cqlkeyspace.t,cqlkeyspace.t2 --tag multi-table
diff --git a/doc/modules/cassandra/examples/BASH/snapshot_mult_tables_again.sh b/doc/modules/cassandra/examples/BASH/snapshot_mult_tables_again.sh
new file mode 100644
index 0000000..f676f5b
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/snapshot_mult_tables_again.sh
@@ -0,0 +1 @@
+$ nodetool snapshot --kt-list cqlkeyspace.t, cqlkeyspace.t2 --tag multi-table-2
diff --git a/doc/modules/cassandra/examples/BASH/snapshot_one_table.sh b/doc/modules/cassandra/examples/BASH/snapshot_one_table.sh
new file mode 100644
index 0000000..05484a9
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/snapshot_one_table.sh
@@ -0,0 +1 @@
+$ nodetool snapshot --tag <tag> --table <table>  --<keyspace>
diff --git a/doc/modules/cassandra/examples/BASH/snapshot_one_table2.sh b/doc/modules/cassandra/examples/BASH/snapshot_one_table2.sh
new file mode 100644
index 0000000..7387710
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/snapshot_one_table2.sh
@@ -0,0 +1 @@
+$ nodetool snapshot --tag magazine --table magazine  catalogkeyspace
diff --git a/doc/modules/cassandra/examples/BASH/start_tarball.sh b/doc/modules/cassandra/examples/BASH/start_tarball.sh
new file mode 100644
index 0000000..6331270
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/start_tarball.sh
@@ -0,0 +1 @@
+$ cd apache-cassandra-{cass-tag-3x}/ && bin/cassandra
diff --git a/doc/modules/cassandra/examples/BASH/tail_syslog.sh b/doc/modules/cassandra/examples/BASH/tail_syslog.sh
new file mode 100644
index 0000000..b475750
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/tail_syslog.sh
@@ -0,0 +1 @@
+$ tail -f logs/system.log
diff --git a/doc/modules/cassandra/examples/BASH/tail_syslog_package.sh b/doc/modules/cassandra/examples/BASH/tail_syslog_package.sh
new file mode 100644
index 0000000..c9f00ed
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/tail_syslog_package.sh
@@ -0,0 +1 @@
+$ tail -f /var/log/cassandra/system.log
diff --git a/doc/modules/cassandra/examples/BASH/tarball.sh b/doc/modules/cassandra/examples/BASH/tarball.sh
new file mode 100644
index 0000000..0ef448a
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/tarball.sh
@@ -0,0 +1 @@
+$ tar xzvf apache-cassandra-{cass-tag-3x}-bin.tar.gz
diff --git a/doc/modules/cassandra/examples/BASH/verify_gpg.sh b/doc/modules/cassandra/examples/BASH/verify_gpg.sh
new file mode 100644
index 0000000..9a503da
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/verify_gpg.sh
@@ -0,0 +1 @@
+$ gpg --print-md SHA256 apache-cassandra-{cass-tag-3x}-bin.tar.gz
diff --git a/doc/modules/cassandra/examples/BASH/yum_cass.sh b/doc/modules/cassandra/examples/BASH/yum_cass.sh
new file mode 100644
index 0000000..cd8217b
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/yum_cass.sh
@@ -0,0 +1 @@
+$ sudo yum install cassandra
diff --git a/doc/modules/cassandra/examples/BASH/yum_start.sh b/doc/modules/cassandra/examples/BASH/yum_start.sh
new file mode 100644
index 0000000..4930d1a
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/yum_start.sh
@@ -0,0 +1 @@
+$ sudo service cassandra start
diff --git a/doc/modules/cassandra/examples/BASH/yum_update.sh b/doc/modules/cassandra/examples/BASH/yum_update.sh
new file mode 100644
index 0000000..2e815b2
--- /dev/null
+++ b/doc/modules/cassandra/examples/BASH/yum_update.sh
@@ -0,0 +1 @@
+$ sudo yum update
diff --git a/doc/modules/cassandra/examples/BNF/aggregate_name.bnf b/doc/modules/cassandra/examples/BNF/aggregate_name.bnf
new file mode 100644
index 0000000..a7ccdc3
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/aggregate_name.bnf
@@ -0,0 +1 @@
+aggregate_name::= [keyspace_name '.' ] name
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/alter_ks.bnf b/doc/modules/cassandra/examples/BNF/alter_ks.bnf
new file mode 100644
index 0000000..5f82d34
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/alter_ks.bnf
@@ -0,0 +1,2 @@
+alter_keyspace_statement::= ALTER KEYSPACE keyspace_name
+	WITH options
diff --git a/doc/modules/cassandra/examples/BNF/alter_mv_statement.bnf b/doc/modules/cassandra/examples/BNF/alter_mv_statement.bnf
new file mode 100644
index 0000000..ff97edb
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/alter_mv_statement.bnf
@@ -0,0 +1 @@
+alter_materialized_view_statement::= ALTER MATERIALIZED VIEW view_name WITH table_options
diff --git a/doc/modules/cassandra/examples/BNF/alter_role_statement.bnf b/doc/modules/cassandra/examples/BNF/alter_role_statement.bnf
new file mode 100644
index 0000000..36958d7
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/alter_role_statement.bnf
@@ -0,0 +1 @@
+alter_role_statement ::= ALTER ROLE role_name WITH role_options
diff --git a/doc/modules/cassandra/examples/BNF/alter_table.bnf b/doc/modules/cassandra/examples/BNF/alter_table.bnf
new file mode 100644
index 0000000..bf1b4b7
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/alter_table.bnf
@@ -0,0 +1,4 @@
+alter_table_statement::= ALTER TABLE table_name alter_table_instruction 
+alter_table_instruction::= ADD column_name cql_type ( ',' column_name cql_type )* 
+	| DROP column_name ( column_name )*  
+	| WITH options
diff --git a/doc/modules/cassandra/examples/BNF/alter_udt_statement.bnf b/doc/modules/cassandra/examples/BNF/alter_udt_statement.bnf
new file mode 100644
index 0000000..4f409e6
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/alter_udt_statement.bnf
@@ -0,0 +1,3 @@
+alter_type_statement::= ALTER TYPE udt_name alter_type_modification
+alter_type_modification::= ADD field_definition
+        | RENAME identifier TO identifier( identifier TO identifier )*
diff --git a/doc/modules/cassandra/examples/BNF/alter_user_statement.bnf b/doc/modules/cassandra/examples/BNF/alter_user_statement.bnf
new file mode 100644
index 0000000..129607c
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/alter_user_statement.bnf
@@ -0,0 +1 @@
+alter_user_statement ::= ALTER USER role_name [ WITH PASSWORD string] [ user_option]
diff --git a/doc/modules/cassandra/examples/BNF/batch_statement.bnf b/doc/modules/cassandra/examples/BNF/batch_statement.bnf
new file mode 100644
index 0000000..2cc2559
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/batch_statement.bnf
@@ -0,0 +1,5 @@
+batch_statement ::=     BEGIN [ UNLOGGED | COUNTER ] BATCH
+                        [ USING update_parameter( AND update_parameter)* ]
+                        modification_statement ( ';' modification_statement )*
+                        APPLY BATCH
+modification_statement ::= insert_statement | update_statement | delete_statement
diff --git a/doc/modules/cassandra/examples/BNF/collection_literal.bnf b/doc/modules/cassandra/examples/BNF/collection_literal.bnf
new file mode 100644
index 0000000..83a46a2
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/collection_literal.bnf
@@ -0,0 +1,4 @@
+collection_literal::= map_literal | set_literal | list_literal 
+map_literal::= '\{' [ term ':' term (',' term : term)* ] '}' 
+set_literal::= '\{' [ term (',' term)* ] '}' 
+list_literal::= '[' [ term (',' term)* ] ']'
diff --git a/doc/modules/cassandra/examples/BNF/collection_type.bnf b/doc/modules/cassandra/examples/BNF/collection_type.bnf
new file mode 100644
index 0000000..37e6cd1
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/collection_type.bnf
@@ -0,0 +1,3 @@
+collection_type::= MAP '<' cql_type',' cql_type'>' 
+	| SET '<' cql_type '>' 
+	| LIST '<' cql_type'>'
diff --git a/doc/modules/cassandra/examples/BNF/column.bnf b/doc/modules/cassandra/examples/BNF/column.bnf
new file mode 100644
index 0000000..136a45c
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/column.bnf
@@ -0,0 +1 @@
+column_name::= identifier
diff --git a/doc/modules/cassandra/examples/BNF/constant.bnf b/doc/modules/cassandra/examples/BNF/constant.bnf
new file mode 100644
index 0000000..4a2953a
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/constant.bnf
@@ -0,0 +1,8 @@
+constant::= string | integer | float | boolean | uuid | blob | NULL
+string::= ''' (any character where ' can appear if doubled)+ ''' : '$$' (any character other than '$$') '$$'
+integer::= re('-?[0-9]+')
+float::= re('-?[0-9]+(.[0-9]*)?([eE][+-]?[0-9+])?') | NAN | INFINITY
+boolean::= TRUE | FALSE
+uuid::= hex\{8}-hex\{4}-hex\{4}-hex\{4}-hex\{12}
+hex::= re("[0-9a-fA-F]")
+blob::= '0' ('x' | 'X') hex+
diff --git a/doc/modules/cassandra/examples/BNF/cql_statement.bnf b/doc/modules/cassandra/examples/BNF/cql_statement.bnf
new file mode 100644
index 0000000..8d4ae21
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/cql_statement.bnf
@@ -0,0 +1,48 @@
+cql_statement::= statement [ ';' ]
+statement:=: ddl_statement :
+        | dml_statement
+        | secondary_index_statement
+        | materialized_view_statement
+        | role_or_permission_statement
+        | udf_statement
+        | udt_statement
+        | trigger_statement
+ddl_statement::= use_statement
+        | create_keyspace_statement
+        | alter_keyspace_statement
+        | drop_keyspace_statement
+        | create_table_statement
+        | alter_table_statement
+        | drop_table_statement
+        | truncate_statement
+dml_statement::= select_statement
+        | insert_statement
+        | update_statement
+        | delete_statement
+        | batch_statement
+secondary_index_statement::= create_index_statement
+        | drop_index_statement
+materialized_view_statement::= create_materialized_view_statement
+        | drop_materialized_view_statement
+role_or_permission_statement::= create_role_statement
+        | alter_role_statement
+        | drop_role_statement
+        | grant_role_statement
+        | revoke_role_statement
+        | list_roles_statement
+        | grant_permission_statement
+        | revoke_permission_statement
+        | list_permissions_statement
+        | create_user_statement
+        | alter_user_statement
+        | drop_user_statement
+        | list_users_statement
+udf_statement::= create_function_statement
+        | drop_function_statement
+        | create_aggregate_statement
+        | drop_aggregate_statement
+udt_statement::= create_type_statement
+        | alter_type_statement
+        | drop_type_statement
+trigger_statement::= create_trigger_statement
+        | drop_trigger_statement
diff --git a/doc/modules/cassandra/examples/BNF/cql_type.bnf b/doc/modules/cassandra/examples/BNF/cql_type.bnf
new file mode 100644
index 0000000..4e2e5d1
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/cql_type.bnf
@@ -0,0 +1 @@
+cql_type::= native_type| collection_type| user_defined_type | tuple_type | custom_type
diff --git a/doc/modules/cassandra/examples/BNF/create_aggregate_statement.bnf b/doc/modules/cassandra/examples/BNF/create_aggregate_statement.bnf
new file mode 100644
index 0000000..c0126a2
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/create_aggregate_statement.bnf
@@ -0,0 +1,6 @@
+create_aggregate_statement ::= CREATE [ OR REPLACE ] AGGREGATE [ IF NOT EXISTS ]
+                                function_name '(' arguments_signature')'
+                                SFUNC function_name
+                                STYPE cql_type:
+                                [ FINALFUNC function_name]
+                                [ INITCOND term ]
diff --git a/doc/modules/cassandra/examples/BNF/create_function_statement.bnf b/doc/modules/cassandra/examples/BNF/create_function_statement.bnf
new file mode 100644
index 0000000..0da769a
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/create_function_statement.bnf
@@ -0,0 +1,6 @@
+create_function_statement::= CREATE [ OR REPLACE ] FUNCTION [ IF NOT EXISTS] 
+	function_name '(' arguments_declaration ')' 
+	[ CALLED | RETURNS NULL ] ON NULL INPUT 
+	RETURNS cql_type 
+	LANGUAGE identifier 
+	AS string arguments_declaration: identifier cql_type ( ',' identifier cql_type )*
diff --git a/doc/modules/cassandra/examples/BNF/create_index_statement.bnf b/doc/modules/cassandra/examples/BNF/create_index_statement.bnf
new file mode 100644
index 0000000..6e76947
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/create_index_statement.bnf
@@ -0,0 +1,5 @@
+create_index_statement::= CREATE [ CUSTOM ] INDEX [ IF NOT EXISTS ] [ index_name ] 
+	ON table_name '(' index_identifier ')' 
+	[ USING string [ WITH OPTIONS = map_literal ] ] 
+index_identifier::= column_name 
+	| ( KEYS | VALUES | ENTRIES | FULL ) '(' column_name ')'
diff --git a/doc/modules/cassandra/examples/BNF/create_ks.bnf b/doc/modules/cassandra/examples/BNF/create_ks.bnf
new file mode 100644
index 0000000..ba3e240
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/create_ks.bnf
@@ -0,0 +1,2 @@
+create_keyspace_statement::= CREATE KEYSPACE [ IF NOT EXISTS ] keyspace_name 
+	WITH options
diff --git a/doc/modules/cassandra/examples/BNF/create_mv_statement.bnf b/doc/modules/cassandra/examples/BNF/create_mv_statement.bnf
new file mode 100644
index 0000000..9bdb60d
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/create_mv_statement.bnf
@@ -0,0 +1,4 @@
+create_materialized_view_statement::= CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] view_name
+	AS select_statement
+	PRIMARY KEY '(' primary_key')' 
+	WITH table_options
diff --git a/doc/modules/cassandra/examples/BNF/create_role_statement.bnf b/doc/modules/cassandra/examples/BNF/create_role_statement.bnf
new file mode 100644
index 0000000..bc93fbc
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/create_role_statement.bnf
@@ -0,0 +1,9 @@
+create_role_statement ::= CREATE ROLE [ IF NOT EXISTS ] role_name
+                          [ WITH role_options# ]
+role_options ::= role_option ( AND role_option)*
+role_option ::= PASSWORD '=' string
+                | LOGIN '=' boolean
+                | SUPERUSER '=' boolean
+                | OPTIONS '=' map_literal
+                | ACCESS TO DATACENTERS set_literal
+                | ACCESS TO ALL DATACENTERS
diff --git a/doc/modules/cassandra/examples/BNF/create_table.bnf b/doc/modules/cassandra/examples/BNF/create_table.bnf
new file mode 100644
index 0000000..840573c
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/create_table.bnf
@@ -0,0 +1,12 @@
+create_table_statement::= CREATE TABLE [ IF NOT EXISTS ] table_name '(' 
+	column_definition  ( ',' column_definition )*  
+	[ ',' PRIMARY KEY '(' primary_key ')' ] 
+	 ')' [ WITH table_options ] 
+column_definition::= column_name cql_type [ STATIC ] [ PRIMARY KEY] 
+primary_key::= partition_key [ ',' clustering_columns ] 
+partition_key::= column_name  | '(' column_name ( ',' column_name )* ')' 
+clustering_columns::= column_name ( ',' column_name )* 
+table_options:=: COMPACT STORAGE [ AND table_options ]  
+	| CLUSTERING ORDER BY '(' clustering_order ')' 
+	[ AND table_options ]  | options
+clustering_order::= column_name (ASC | DESC) ( ',' column_name (ASC | DESC) )*
diff --git a/doc/modules/cassandra/examples/BNF/create_trigger_statement.bnf b/doc/modules/cassandra/examples/BNF/create_trigger_statement.bnf
new file mode 100644
index 0000000..f7442da
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/create_trigger_statement.bnf
@@ -0,0 +1,3 @@
+create_trigger_statement ::= CREATE TRIGGER [ IF NOT EXISTS ] trigger_name
+	ON table_name
+	USING string
diff --git a/doc/modules/cassandra/examples/BNF/create_type.bnf b/doc/modules/cassandra/examples/BNF/create_type.bnf
new file mode 100644
index 0000000..aebe9eb
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/create_type.bnf
@@ -0,0 +1,3 @@
+create_type_statement::= CREATE TYPE [ IF NOT EXISTS ] udt_name
+        '(' field_definition ( ',' field_definition)* ')'
+field_definition::= identifier cql_type
diff --git a/doc/modules/cassandra/examples/BNF/create_user_statement.bnf b/doc/modules/cassandra/examples/BNF/create_user_statement.bnf
new file mode 100644
index 0000000..19f9903
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/create_user_statement.bnf
@@ -0,0 +1,4 @@
+create_user_statement ::= CREATE USER [ IF NOT EXISTS ] role_name
+                          [ WITH PASSWORD string ]
+                          [ user_option ]
+user_option: SUPERUSER | NOSUPERUSER
diff --git a/doc/modules/cassandra/examples/BNF/custom_type.bnf b/doc/modules/cassandra/examples/BNF/custom_type.bnf
new file mode 100644
index 0000000..ce4890f
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/custom_type.bnf
@@ -0,0 +1 @@
+custom_type::= string
diff --git a/doc/modules/cassandra/examples/BNF/delete_statement.bnf b/doc/modules/cassandra/examples/BNF/delete_statement.bnf
new file mode 100644
index 0000000..5f456ba
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/delete_statement.bnf
@@ -0,0 +1,5 @@
+delete_statement::= DELETE [ simple_selection ( ',' simple_selection ) ] 
+	FROM table_name 
+	[ USING update_parameter ( AND update_parameter# )* ] 
+	WHERE where_clause 
+	[ IF ( EXISTS | condition ( AND condition)*) ]
diff --git a/doc/modules/cassandra/examples/BNF/describe_aggregate_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_aggregate_statement.bnf
new file mode 100644
index 0000000..b94526b
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_aggregate_statement.bnf
@@ -0,0 +1 @@
+describe_aggregate_statement::= DESCRIBE AGGREGATE aggregate_name;
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_aggregates_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_aggregates_statement.bnf
new file mode 100644
index 0000000..049afef
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_aggregates_statement.bnf
@@ -0,0 +1 @@
+describe_aggregates_statement::= DESCRIBE AGGREGATES;
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_cluster_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_cluster_statement.bnf
new file mode 100644
index 0000000..8f58ac8
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_cluster_statement.bnf
@@ -0,0 +1 @@
+describe_cluster_statement::= DESCRIBE CLUSTER;
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_function_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_function_statement.bnf
new file mode 100644
index 0000000..9145e92
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_function_statement.bnf
@@ -0,0 +1 @@
+describe_function_statement::= DESCRIBE FUNCTION function_name;
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_functions_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_functions_statement.bnf
new file mode 100644
index 0000000..4e3b822
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_functions_statement.bnf
@@ -0,0 +1 @@
+describe_functions_statement::= DESCRIBE FUNCTIONS;
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_index_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_index_statement.bnf
new file mode 100644
index 0000000..907c175
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_index_statement.bnf
@@ -0,0 +1 @@
+describe_index_statement::= DESCRIBE INDEX index;
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_keyspace_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_keyspace_statement.bnf
new file mode 100644
index 0000000..771e755
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_keyspace_statement.bnf
@@ -0,0 +1 @@
+describe_keyspace_statement::= DESCRIBE [ONLY] KEYSPACE [keyspace_name] [WITH INTERNALS];
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_keyspaces_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_keyspaces_statement.bnf
new file mode 100644
index 0000000..51b3c26
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_keyspaces_statement.bnf
@@ -0,0 +1 @@
+describe_keyspaces_statement::= DESCRIBE KEYSPACES;
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_materialized_view_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_materialized_view_statement.bnf
new file mode 100644
index 0000000..3297c0e
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_materialized_view_statement.bnf
@@ -0,0 +1 @@
+describe_materialized_view_statement::= DESCRIBE MATERIALIZED VIEW materialized_view;
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_object_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_object_statement.bnf
new file mode 100644
index 0000000..d8addae
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_object_statement.bnf
@@ -0,0 +1 @@
+describe_object_statement::= DESCRIBE object_name [WITH INTERNALS];
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_schema_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_schema_statement.bnf
new file mode 100644
index 0000000..7344081
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_schema_statement.bnf
@@ -0,0 +1 @@
+describe_schema_statement::= DESCRIBE [FULL] SCHEMA [WITH INTERNALS];
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_table_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_table_statement.bnf
new file mode 100644
index 0000000..1a0cd73
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_table_statement.bnf
@@ -0,0 +1 @@
+describe_table_statement::= DESCRIBE TABLE table_name [WITH INTERNALS];
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_tables_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_tables_statement.bnf
new file mode 100644
index 0000000..061452c
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_tables_statement.bnf
@@ -0,0 +1 @@
+describe_tables_statement::= DESCRIBE TABLES;
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_type_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_type_statement.bnf
new file mode 100644
index 0000000..f592af4
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_type_statement.bnf
@@ -0,0 +1 @@
+describe_type_statement::= DESCRIBE TYPE udt_name;
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/describe_types_statement.bnf b/doc/modules/cassandra/examples/BNF/describe_types_statement.bnf
new file mode 100644
index 0000000..73f2827
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/describe_types_statement.bnf
@@ -0,0 +1 @@
+describe_types_statement::= DESCRIBE TYPES;
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/drop_aggregate_statement.bnf b/doc/modules/cassandra/examples/BNF/drop_aggregate_statement.bnf
new file mode 100644
index 0000000..28e8a4f
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/drop_aggregate_statement.bnf
@@ -0,0 +1,2 @@
+drop_aggregate_statement::= DROP AGGREGATE [ IF EXISTS ] function_name[ '(' arguments_signature ')'
+]
diff --git a/doc/modules/cassandra/examples/BNF/drop_function_statement.bnf b/doc/modules/cassandra/examples/BNF/drop_function_statement.bnf
new file mode 100644
index 0000000..2639bd0
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/drop_function_statement.bnf
@@ -0,0 +1,2 @@
+drop_function_statement::= DROP FUNCTION [ IF EXISTS ] function_name [ '(' arguments_signature ')' ] 
+arguments_signature::= cql_type ( ',' cql_type )*
diff --git a/doc/modules/cassandra/examples/BNF/drop_index_statement.bnf b/doc/modules/cassandra/examples/BNF/drop_index_statement.bnf
new file mode 100644
index 0000000..49f36d1
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/drop_index_statement.bnf
@@ -0,0 +1 @@
+drop_index_statement::= DROP INDEX [ IF EXISTS ] index_name
diff --git a/doc/modules/cassandra/examples/BNF/drop_ks.bnf b/doc/modules/cassandra/examples/BNF/drop_ks.bnf
new file mode 100644
index 0000000..4e21b7b
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/drop_ks.bnf
@@ -0,0 +1 @@
+drop_keyspace_statement::= DROP KEYSPACE [ IF EXISTS ] keyspace_name
diff --git a/doc/modules/cassandra/examples/BNF/drop_mv_statement.bnf b/doc/modules/cassandra/examples/BNF/drop_mv_statement.bnf
new file mode 100644
index 0000000..1a9d8dc
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/drop_mv_statement.bnf
@@ -0,0 +1 @@
+drop_materialized_view_statement::= DROP MATERIALIZED VIEW [ IF EXISTS ] view_name;
diff --git a/doc/modules/cassandra/examples/BNF/drop_role_statement.bnf b/doc/modules/cassandra/examples/BNF/drop_role_statement.bnf
new file mode 100644
index 0000000..15e1791
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/drop_role_statement.bnf
@@ -0,0 +1 @@
+drop_role_statement ::= DROP ROLE [ IF EXISTS ] role_name
diff --git a/doc/modules/cassandra/examples/BNF/drop_table.bnf b/doc/modules/cassandra/examples/BNF/drop_table.bnf
new file mode 100644
index 0000000..cabd17a
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/drop_table.bnf
@@ -0,0 +1 @@
+drop_table_statement::= DROP TABLE [ IF EXISTS ] table_name
diff --git a/doc/modules/cassandra/examples/BNF/drop_trigger_statement.bnf b/doc/modules/cassandra/examples/BNF/drop_trigger_statement.bnf
new file mode 100644
index 0000000..c1d3e59
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/drop_trigger_statement.bnf
@@ -0,0 +1 @@
+drop_trigger_statement ::= DROP TRIGGER [ IF EXISTS ] trigger_nameON table_name
diff --git a/doc/modules/cassandra/examples/BNF/drop_udt_statement.bnf b/doc/modules/cassandra/examples/BNF/drop_udt_statement.bnf
new file mode 100644
index 0000000..276b57c
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/drop_udt_statement.bnf
@@ -0,0 +1 @@
+drop_type_statement::= DROP TYPE [ IF EXISTS ] udt_name
diff --git a/doc/modules/cassandra/examples/BNF/drop_user_statement.bnf b/doc/modules/cassandra/examples/BNF/drop_user_statement.bnf
new file mode 100644
index 0000000..9b22608
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/drop_user_statement.bnf
@@ -0,0 +1 @@
+drop_user_statement ::= DROP USER [ IF EXISTS ] role_name
diff --git a/doc/modules/cassandra/examples/BNF/function.bnf b/doc/modules/cassandra/examples/BNF/function.bnf
new file mode 100644
index 0000000..7e05430
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/function.bnf
@@ -0,0 +1 @@
+function_name ::= [ keyspace_name'.' ] name
diff --git a/doc/modules/cassandra/examples/BNF/grant_permission_statement.bnf b/doc/modules/cassandra/examples/BNF/grant_permission_statement.bnf
new file mode 100644
index 0000000..40f1df3
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/grant_permission_statement.bnf
@@ -0,0 +1,12 @@
+grant_permission_statement ::= GRANT permissions ON resource TO role_name
+permissions ::= ALL [ PERMISSIONS ] | permission [ PERMISSION ]
+permission ::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESCRIBE | EXECUTE
+resource ::=    ALL KEYSPACES
+                | KEYSPACE keyspace_name
+                | [ TABLE ] table_name
+                | ALL ROLES
+                | ROLE role_name
+                | ALL FUNCTIONS [ IN KEYSPACE keyspace_name ]
+                | FUNCTION function_name '(' [ cql_type( ',' cql_type )* ] ')'
+                | ALL MBEANS
+                | ( MBEAN | MBEANS ) string
diff --git a/doc/modules/cassandra/examples/BNF/grant_role_statement.bnf b/doc/modules/cassandra/examples/BNF/grant_role_statement.bnf
new file mode 100644
index 0000000..d965cc2
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/grant_role_statement.bnf
@@ -0,0 +1 @@
+grant_role_statement ::= GRANT role_name TO role_name
diff --git a/doc/modules/cassandra/examples/BNF/identifier.bnf b/doc/modules/cassandra/examples/BNF/identifier.bnf
new file mode 100644
index 0000000..7bc3431
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/identifier.bnf
@@ -0,0 +1,3 @@
+identifier::= unquoted_identifier | quoted_identifier
+unquoted_identifier::= re('[a-zA-Z][link:[a-zA-Z0-9]]*')
+quoted_identifier::= '"' (any character where " can appear if doubled)+ '"'
diff --git a/doc/modules/cassandra/examples/BNF/index.bnf b/doc/modules/cassandra/examples/BNF/index.bnf
new file mode 100644
index 0000000..7083501
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/index.bnf
@@ -0,0 +1 @@
+index::= [keyspace_name '.' ] index_name
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/index_name.bnf b/doc/modules/cassandra/examples/BNF/index_name.bnf
new file mode 100644
index 0000000..c322755
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/index_name.bnf
@@ -0,0 +1 @@
+index_name::= re('[a-zA-Z_0-9]+')
diff --git a/doc/modules/cassandra/examples/BNF/insert_statement.bnf b/doc/modules/cassandra/examples/BNF/insert_statement.bnf
new file mode 100644
index 0000000..ed80c3e
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/insert_statement.bnf
@@ -0,0 +1,6 @@
+insert_statement::= INSERT INTO table_name ( names_values | json_clause ) 
+	[ IF NOT EXISTS ] 
+	[ USING update_parameter ( AND update_parameter )* ] 
+names_values::= names VALUES tuple_literal 
+json_clause::= JSON string [ DEFAULT ( NULL | UNSET ) ] 
+names::= '(' column_name ( ',' column_name )* ')'
diff --git a/doc/modules/cassandra/examples/BNF/ks_table.bnf b/doc/modules/cassandra/examples/BNF/ks_table.bnf
new file mode 100644
index 0000000..20ee6da
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/ks_table.bnf
@@ -0,0 +1,5 @@
+keyspace_name::= name
+table_name::= [keyspace_name '.' ] name
+name::= unquoted_name | quoted_name
+unquoted_name::= re('[a-zA-Z_0-9]\{1, 48}')
+quoted_name::= '"' unquoted_name '"'
diff --git a/doc/modules/cassandra/examples/BNF/list_permissions_statement.bnf b/doc/modules/cassandra/examples/BNF/list_permissions_statement.bnf
new file mode 100644
index 0000000..a11e2cc
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/list_permissions_statement.bnf
@@ -0,0 +1 @@
+list_permissions_statement ::= LIST permissions [ ON resource] [ OF role_name[ NORECURSIVE ] ]
diff --git a/doc/modules/cassandra/examples/BNF/list_roles_statement.bnf b/doc/modules/cassandra/examples/BNF/list_roles_statement.bnf
new file mode 100644
index 0000000..bbe3d9b
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/list_roles_statement.bnf
@@ -0,0 +1 @@
+list_roles_statement ::= LIST ROLES [ OF role_name] [ NORECURSIVE ]
diff --git a/doc/modules/cassandra/examples/BNF/list_users_statement.bnf b/doc/modules/cassandra/examples/BNF/list_users_statement.bnf
new file mode 100644
index 0000000..5750de6
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/list_users_statement.bnf
@@ -0,0 +1 @@
+list_users_statement::= LIST USERS
diff --git a/doc/modules/cassandra/examples/BNF/materialized_view.bnf b/doc/modules/cassandra/examples/BNF/materialized_view.bnf
new file mode 100644
index 0000000..48543a3
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/materialized_view.bnf
@@ -0,0 +1 @@
+materialized_view::= [keyspace_name '.' ] view_name
\ No newline at end of file
diff --git a/doc/modules/cassandra/examples/BNF/native_type.bnf b/doc/modules/cassandra/examples/BNF/native_type.bnf
new file mode 100644
index 0000000..c4e9c26
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/native_type.bnf
@@ -0,0 +1,4 @@
+native_type::= ASCII | BIGINT | BLOB | BOOLEAN | COUNTER | DATE
+| DECIMAL | DOUBLE | DURATION | FLOAT | INET | INT |
+SMALLINT | TEXT | TIME | TIMESTAMP | TIMEUUID | TINYINT |
+UUID | VARCHAR | VARINT
diff --git a/doc/modules/cassandra/examples/BNF/options.bnf b/doc/modules/cassandra/examples/BNF/options.bnf
new file mode 100644
index 0000000..9887165
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/options.bnf
@@ -0,0 +1,4 @@
+options::= option ( AND option )* 
+option::= identifier '=' ( identifier 
+	| constant 
+	| map_literal )
diff --git a/doc/modules/cassandra/examples/BNF/revoke_permission_statement.bnf b/doc/modules/cassandra/examples/BNF/revoke_permission_statement.bnf
new file mode 100644
index 0000000..fd061f9
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/revoke_permission_statement.bnf
@@ -0,0 +1 @@
+revoke_permission_statement ::= REVOKE permissions ON resource FROM role_name
diff --git a/doc/modules/cassandra/examples/BNF/revoke_role_statement.bnf b/doc/modules/cassandra/examples/BNF/revoke_role_statement.bnf
new file mode 100644
index 0000000..c344eb0
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/revoke_role_statement.bnf
@@ -0,0 +1 @@
+revoke_role_statement ::= REVOKE role_name FROM role_name
diff --git a/doc/modules/cassandra/examples/BNF/role_name.bnf b/doc/modules/cassandra/examples/BNF/role_name.bnf
new file mode 100644
index 0000000..103f84b
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/role_name.bnf
@@ -0,0 +1 @@
+role_name ::= identifier | string
diff --git a/doc/modules/cassandra/examples/BNF/select_statement.bnf b/doc/modules/cassandra/examples/BNF/select_statement.bnf
new file mode 100644
index 0000000..f53da41
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/select_statement.bnf
@@ -0,0 +1,21 @@
+select_statement::= SELECT [ JSON | DISTINCT ] ( select_clause | '*' ) 
+	FROM `table_name`  
+	[ WHERE `where_clause` ] 
+	[ GROUP BY `group_by_clause` ]  
+	[ ORDER BY `ordering_clause` ]  
+	[ PER PARTITION LIMIT (`integer` | `bind_marker`) ]  
+	[ LIMIT (`integer` | `bind_marker`) ]  
+	[ ALLOW FILTERING ]
+select_clause::= `selector` [ AS `identifier` ] ( ',' `selector` [ AS `identifier` ] ) 
+selector::== `column_name` 
+	| `term`  
+	| CAST '(' `selector` AS `cql_type` ')' 
+	| `function_name` '(' [ `selector` ( ',' `selector` )_ ] ')'  
+	| COUNT '(' '_' ')' 
+where_clause::= `relation` ( AND `relation` )*
+relation::= column_name operator term
+	'(' column_name ( ',' column_name )* ')' operator tuple_literal 
+	TOKEN '(' column_name# ( ',' column_name )* ')' operator term 
+operator::= '=' | '<' | '>' | '<=' | '>=' | '!=' | IN | CONTAINS | CONTAINS KEY 
+group_by_clause::= column_name ( ',' column_name )* 
+ordering_clause::= column_name [ ASC | DESC ] ( ',' column_name [ ASC | DESC ] )*
diff --git a/doc/modules/cassandra/examples/BNF/term.bnf b/doc/modules/cassandra/examples/BNF/term.bnf
new file mode 100644
index 0000000..504c4c4
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/term.bnf
@@ -0,0 +1,6 @@
+term::= constant | literal | function_call | arithmetic_operation | type_hint | bind_marker
+literal::= collection_literal | udt_literal | tuple_literal
+function_call::= identifier '(' [ term (',' term)* ] ')'
+arithmetic_operation::= '-' term | term ('+' | '-' | '*' | '/' | '%') term
+type_hint::= '(' cql_type ')' term
+bind_marker::= '?' | ':' identifier
diff --git a/doc/modules/cassandra/examples/BNF/trigger_name.bnf b/doc/modules/cassandra/examples/BNF/trigger_name.bnf
new file mode 100644
index 0000000..18a4a7e
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/trigger_name.bnf
@@ -0,0 +1 @@
+trigger_name ::= identifier
diff --git a/doc/modules/cassandra/examples/BNF/truncate_table.bnf b/doc/modules/cassandra/examples/BNF/truncate_table.bnf
new file mode 100644
index 0000000..9c7d301
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/truncate_table.bnf
@@ -0,0 +1 @@
+truncate_statement::= TRUNCATE [ TABLE ] table_name
diff --git a/doc/modules/cassandra/examples/BNF/tuple.bnf b/doc/modules/cassandra/examples/BNF/tuple.bnf
new file mode 100644
index 0000000..f339d57
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/tuple.bnf
@@ -0,0 +1,2 @@
+tuple_type::= TUPLE '<' cql_type( ',' cql_type)* '>'
+tuple_literal::= '(' term( ',' term )* ')'
diff --git a/doc/modules/cassandra/examples/BNF/udt.bnf b/doc/modules/cassandra/examples/BNF/udt.bnf
new file mode 100644
index 0000000..c06a5f6
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/udt.bnf
@@ -0,0 +1,2 @@
+user_defined_type::= udt_name
+udt_name::= [ keyspace_name '.' ] identifier
diff --git a/doc/modules/cassandra/examples/BNF/udt_literal.bnf b/doc/modules/cassandra/examples/BNF/udt_literal.bnf
new file mode 100644
index 0000000..8c996e5
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/udt_literal.bnf
@@ -0,0 +1 @@
+udt_literal::= '{' identifier ':' term ( ',' identifier ':' term)* '}'
diff --git a/doc/modules/cassandra/examples/BNF/update_statement.bnf b/doc/modules/cassandra/examples/BNF/update_statement.bnf
new file mode 100644
index 0000000..1a9bdb4
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/update_statement.bnf
@@ -0,0 +1,13 @@
+update_statement ::=    UPDATE table_name
+                        [ USING update_parameter ( AND update_parameter )* ]
+                        SET assignment( ',' assignment )*
+                        WHERE where_clause
+                        [ IF ( EXISTS | condition ( AND condition)*) ]
+update_parameter ::= ( TIMESTAMP | TTL ) ( integer | bind_marker )
+assignment: simple_selection'=' term
+                `| column_name'=' column_name ( '+' | '-' ) term
+                | column_name'=' list_literal'+' column_name
+simple_selection ::= column_name
+                        | column_name '[' term']'
+                        | column_name'.' field_name
+condition ::= `simple_selection operator term
diff --git a/doc/modules/cassandra/examples/BNF/use_ks.bnf b/doc/modules/cassandra/examples/BNF/use_ks.bnf
new file mode 100644
index 0000000..0347e52
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/use_ks.bnf
@@ -0,0 +1 @@
+use_statement::= USE keyspace_name
diff --git a/doc/modules/cassandra/examples/BNF/view_name.bnf b/doc/modules/cassandra/examples/BNF/view_name.bnf
new file mode 100644
index 0000000..6925367
--- /dev/null
+++ b/doc/modules/cassandra/examples/BNF/view_name.bnf
@@ -0,0 +1 @@
+view_name::= re('[a-zA-Z_0-9]+')
diff --git a/doc/modules/cassandra/examples/CQL/allow_filtering.cql b/doc/modules/cassandra/examples/CQL/allow_filtering.cql
new file mode 100644
index 0000000..c3bf3c6
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/allow_filtering.cql
@@ -0,0 +1,9 @@
+CREATE TABLE users (
+    username text PRIMARY KEY,
+    firstname text,
+    lastname text,
+    birth_year int,
+    country text
+);
+
+CREATE INDEX ON users(birth_year);
diff --git a/doc/modules/cassandra/examples/CQL/alter_ks.cql b/doc/modules/cassandra/examples/CQL/alter_ks.cql
new file mode 100644
index 0000000..319ed24
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/alter_ks.cql
@@ -0,0 +1,2 @@
+ALTER KEYSPACE excelsior
+    WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 4};
diff --git a/doc/modules/cassandra/examples/CQL/alter_role.cql b/doc/modules/cassandra/examples/CQL/alter_role.cql
new file mode 100644
index 0000000..c5f7d3d
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/alter_role.cql
@@ -0,0 +1 @@
+ALTER ROLE bob WITH PASSWORD = 'PASSWORD_B' AND SUPERUSER = false;
diff --git a/doc/modules/cassandra/examples/CQL/alter_table_add_column.cql b/doc/modules/cassandra/examples/CQL/alter_table_add_column.cql
new file mode 100644
index 0000000..e7703ed
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/alter_table_add_column.cql
@@ -0,0 +1 @@
+ALTER TABLE addamsFamily ADD gravesite varchar;
diff --git a/doc/modules/cassandra/examples/CQL/alter_table_spec_retry.cql b/doc/modules/cassandra/examples/CQL/alter_table_spec_retry.cql
new file mode 100644
index 0000000..bb9aa61
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/alter_table_spec_retry.cql
@@ -0,0 +1 @@
+ALTER TABLE users WITH speculative_retry = '10ms';
diff --git a/doc/modules/cassandra/examples/CQL/alter_table_spec_retry_percent.cql b/doc/modules/cassandra/examples/CQL/alter_table_spec_retry_percent.cql
new file mode 100644
index 0000000..a5351c6
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/alter_table_spec_retry_percent.cql
@@ -0,0 +1 @@
+ALTER TABLE users WITH speculative_retry = '99PERCENTILE';
diff --git a/doc/modules/cassandra/examples/CQL/alter_table_with_comment.cql b/doc/modules/cassandra/examples/CQL/alter_table_with_comment.cql
new file mode 100644
index 0000000..bf0bec9
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/alter_table_with_comment.cql
@@ -0,0 +1,3 @@
+ALTER TABLE addamsFamily
+   WITH comment = 'A most excellent and useful table'
+   AND read_repair_chance = 0.2;
diff --git a/doc/modules/cassandra/examples/CQL/alter_user.cql b/doc/modules/cassandra/examples/CQL/alter_user.cql
new file mode 100644
index 0000000..97de7ba
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/alter_user.cql
@@ -0,0 +1,2 @@
+ALTER USER alice WITH PASSWORD 'PASSWORD_A';
+ALTER USER bob SUPERUSER;
diff --git a/doc/modules/cassandra/examples/CQL/as.cql b/doc/modules/cassandra/examples/CQL/as.cql
new file mode 100644
index 0000000..a8b9f03
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/as.cql
@@ -0,0 +1,13 @@
+// Without alias
+SELECT intAsBlob(4) FROM t;
+
+//  intAsBlob(4)
+// --------------
+//  0x00000004
+
+// With alias
+SELECT intAsBlob(4) AS four FROM t;
+
+//  four
+// ------------
+//  0x00000004
diff --git a/doc/modules/cassandra/examples/CQL/autoexpand_exclude_dc.cql b/doc/modules/cassandra/examples/CQL/autoexpand_exclude_dc.cql
new file mode 100644
index 0000000..c320c52
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/autoexpand_exclude_dc.cql
@@ -0,0 +1,4 @@
+CREATE KEYSPACE excalibur
+   WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor' : 3, 'DC2': 0};
+
+DESCRIBE KEYSPACE excalibur;
diff --git a/doc/modules/cassandra/examples/CQL/autoexpand_ks.cql b/doc/modules/cassandra/examples/CQL/autoexpand_ks.cql
new file mode 100644
index 0000000..d5bef55
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/autoexpand_ks.cql
@@ -0,0 +1,4 @@
+CREATE KEYSPACE excalibur
+    WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor' : 3};
+
+DESCRIBE KEYSPACE excalibur;
diff --git a/doc/modules/cassandra/examples/CQL/autoexpand_ks_override.cql b/doc/modules/cassandra/examples/CQL/autoexpand_ks_override.cql
new file mode 100644
index 0000000..d6800fb
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/autoexpand_ks_override.cql
@@ -0,0 +1,4 @@
+CREATE KEYSPACE excalibur
+   WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor' : 3, 'DC2': 2};
+
+DESCRIBE KEYSPACE excalibur;
diff --git a/doc/modules/cassandra/examples/CQL/avg.cql b/doc/modules/cassandra/examples/CQL/avg.cql
new file mode 100644
index 0000000..2882327
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/avg.cql
@@ -0,0 +1 @@
+SELECT AVG (players) FROM plays;
diff --git a/doc/modules/cassandra/examples/CQL/batch_statement.cql b/doc/modules/cassandra/examples/CQL/batch_statement.cql
new file mode 100644
index 0000000..e9148e8
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/batch_statement.cql
@@ -0,0 +1,6 @@
+BEGIN BATCH
+   INSERT INTO users (userid, password, name) VALUES ('user2', 'ch@ngem3b', 'second user');
+   UPDATE users SET password = 'ps22dhds' WHERE userid = 'user3';
+   INSERT INTO users (userid, password) VALUES ('user4', 'ch@ngem3c');
+   DELETE name FROM users WHERE userid = 'user1';
+APPLY BATCH;
diff --git a/doc/modules/cassandra/examples/CQL/caching_option.cql b/doc/modules/cassandra/examples/CQL/caching_option.cql
new file mode 100644
index 0000000..b48b171
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/caching_option.cql
@@ -0,0 +1,6 @@
+CREATE TABLE simple (
+id int,
+key text,
+value text,
+PRIMARY KEY (key, value)
+) WITH caching = {'keys': 'ALL', 'rows_per_partition': 10};
diff --git a/doc/modules/cassandra/examples/CQL/chunk_length.cql b/doc/modules/cassandra/examples/CQL/chunk_length.cql
new file mode 100644
index 0000000..b3504fe
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/chunk_length.cql
@@ -0,0 +1,6 @@
+CREATE TABLE simple (
+   id int,
+   key text,
+   value text,
+   PRIMARY KEY (key, value)
+) WITH compression = {'class': 'LZ4Compressor', 'chunk_length_in_kb': 4};
diff --git a/doc/modules/cassandra/examples/CQL/count.cql b/doc/modules/cassandra/examples/CQL/count.cql
new file mode 100644
index 0000000..1993c0e
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/count.cql
@@ -0,0 +1,2 @@
+SELECT COUNT (*) FROM plays;
+SELECT COUNT (1) FROM plays;
diff --git a/doc/modules/cassandra/examples/CQL/count_nonnull.cql b/doc/modules/cassandra/examples/CQL/count_nonnull.cql
new file mode 100644
index 0000000..6543b99
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/count_nonnull.cql
@@ -0,0 +1 @@
+SELECT COUNT (scores) FROM plays;
diff --git a/doc/modules/cassandra/examples/CQL/create_function.cql b/doc/modules/cassandra/examples/CQL/create_function.cql
new file mode 100644
index 0000000..e7d5823
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_function.cql
@@ -0,0 +1,15 @@
+CREATE OR REPLACE FUNCTION somefunction(somearg int, anotherarg text, complexarg frozen<someUDT>, listarg list)
+    RETURNS NULL ON NULL INPUT
+    RETURNS text
+    LANGUAGE java
+    AS $$
+        // some Java code
+    $$;
+
+CREATE FUNCTION IF NOT EXISTS akeyspace.fname(someArg int)
+    CALLED ON NULL INPUT
+    RETURNS text
+    LANGUAGE java
+    AS $$
+        // some Java code
+    $$;
diff --git a/doc/modules/cassandra/examples/CQL/create_index.cql b/doc/modules/cassandra/examples/CQL/create_index.cql
new file mode 100644
index 0000000..f84452a
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_index.cql
@@ -0,0 +1,8 @@
+CREATE INDEX userIndex ON NerdMovies (user);
+CREATE INDEX ON Mutants (abilityId);
+CREATE INDEX ON users (keys(favs));
+CREATE CUSTOM INDEX ON users (email) 
+   USING 'path.to.the.IndexClass';
+CREATE CUSTOM INDEX ON users (email) 
+   USING 'path.to.the.IndexClass' 
+   WITH OPTIONS = {'storage': '/mnt/ssd/indexes/'};
diff --git a/doc/modules/cassandra/examples/CQL/create_ks.cql b/doc/modules/cassandra/examples/CQL/create_ks.cql
new file mode 100644
index 0000000..e81d7f7
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_ks.cql
@@ -0,0 +1,6 @@
+CREATE KEYSPACE excelsior
+   WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
+
+CREATE KEYSPACE excalibur
+   WITH replication = {'class': 'NetworkTopologyStrategy', 'DC1' : 1, 'DC2' : 3}
+   AND durable_writes = false;
diff --git a/doc/modules/cassandra/examples/CQL/create_ks2_backup.cql b/doc/modules/cassandra/examples/CQL/create_ks2_backup.cql
new file mode 100644
index 0000000..52f9308
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_ks2_backup.cql
@@ -0,0 +1,2 @@
+CREATE KEYSPACE catalogkeyspace
+   WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
diff --git a/doc/modules/cassandra/examples/CQL/create_ks_backup.cql b/doc/modules/cassandra/examples/CQL/create_ks_backup.cql
new file mode 100644
index 0000000..5934904
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_ks_backup.cql
@@ -0,0 +1,2 @@
+CREATE KEYSPACE cqlkeyspace
+   WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
diff --git a/doc/modules/cassandra/examples/CQL/create_ks_trans_repl.cql b/doc/modules/cassandra/examples/CQL/create_ks_trans_repl.cql
new file mode 100644
index 0000000..afff433
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_ks_trans_repl.cql
@@ -0,0 +1,2 @@
+CREATE KEYSPACE some_keyspace
+   WITH replication = {'class': 'NetworkTopologyStrategy', 'DC1' : '3/1'', 'DC2' : '5/2'};
diff --git a/doc/modules/cassandra/examples/CQL/create_mv_statement.cql b/doc/modules/cassandra/examples/CQL/create_mv_statement.cql
new file mode 100644
index 0000000..0792c3e
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_mv_statement.cql
@@ -0,0 +1,5 @@
+CREATE MATERIALIZED VIEW monkeySpecies_by_population AS
+   SELECT * FROM monkeySpecies
+   WHERE population IS NOT NULL AND species IS NOT NULL
+   PRIMARY KEY (population, species)
+   WITH comment='Allow query by population instead of species';
diff --git a/doc/modules/cassandra/examples/CQL/create_role.cql b/doc/modules/cassandra/examples/CQL/create_role.cql
new file mode 100644
index 0000000..c8d0d64
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_role.cql
@@ -0,0 +1,6 @@
+CREATE ROLE new_role;
+CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true;
+CREATE ROLE bob WITH PASSWORD = 'password_b' AND LOGIN = true AND SUPERUSER = true;
+CREATE ROLE carlos WITH OPTIONS = { 'custom_option1' : 'option1_value', 'custom_option2' : 99 };
+CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true AND ACCESS TO DATACENTERS {'DC1', 'DC3'};
+CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true AND ACCESS TO ALL DATACENTERS;
diff --git a/doc/modules/cassandra/examples/CQL/create_role_ifnotexists.cql b/doc/modules/cassandra/examples/CQL/create_role_ifnotexists.cql
new file mode 100644
index 0000000..0b9600f
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_role_ifnotexists.cql
@@ -0,0 +1,2 @@
+CREATE ROLE other_role;
+CREATE ROLE IF NOT EXISTS other_role;
diff --git a/doc/modules/cassandra/examples/CQL/create_static_column.cql b/doc/modules/cassandra/examples/CQL/create_static_column.cql
new file mode 100644
index 0000000..95e8ff2
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_static_column.cql
@@ -0,0 +1,7 @@
+CREATE TABLE t (
+    pk int,
+    t int,
+    v text,
+    s text static,
+    PRIMARY KEY (pk, t)
+);
diff --git a/doc/modules/cassandra/examples/CQL/create_table.cql b/doc/modules/cassandra/examples/CQL/create_table.cql
new file mode 100644
index 0000000..4368d4c
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_table.cql
@@ -0,0 +1,24 @@
+CREATE TABLE monkey_species (
+    species text PRIMARY KEY,
+    common_name text,
+    population varint,
+    average_size int
+) WITH comment='Important biological records'
+    AND read_repair_chance = 1.0;
+
+CREATE TABLE timeline (
+    userid uuid,
+    posted_month int,
+    posted_time uuid,
+    body text,
+    posted_by text,
+    PRIMARY KEY (userid, posted_month, posted_time)
+) WITH compaction = { 'class' : 'LeveledCompactionStrategy' };
+
+CREATE TABLE loads (
+    machine inet,
+    cpu int,
+    mtime timeuuid,
+    load float,
+    PRIMARY KEY ((machine, cpu), mtime)
+) WITH CLUSTERING ORDER BY (mtime DESC);
diff --git a/doc/modules/cassandra/examples/CQL/create_table2_backup.cql b/doc/modules/cassandra/examples/CQL/create_table2_backup.cql
new file mode 100644
index 0000000..f339300
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_table2_backup.cql
@@ -0,0 +1,14 @@
+USE catalogkeyspace;
+CREATE TABLE journal (
+   id int,
+   name text,
+   publisher text,
+   PRIMARY KEY (id)
+);
+
+CREATE TABLE magazine (
+   id int,
+   name text,
+   publisher text,
+   PRIMARY KEY (id)
+);
diff --git a/doc/modules/cassandra/examples/CQL/create_table_backup.cql b/doc/modules/cassandra/examples/CQL/create_table_backup.cql
new file mode 100644
index 0000000..c80b999
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_table_backup.cql
@@ -0,0 +1,13 @@
+USE cqlkeyspace;
+CREATE TABLE t (
+   id int,
+   k int,
+   v text,
+   PRIMARY KEY (id)
+);
+CREATE TABLE t2 (
+   id int,
+   k int,
+   v text,
+   PRIMARY KEY (id)
+);
diff --git a/doc/modules/cassandra/examples/CQL/create_table_clustercolumn.cql b/doc/modules/cassandra/examples/CQL/create_table_clustercolumn.cql
new file mode 100644
index 0000000..f7de266
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_table_clustercolumn.cql
@@ -0,0 +1,7 @@
+CREATE TABLE t2 (
+    a int,
+    b int,
+    c int,
+    d int,
+    PRIMARY KEY (a, b, c)
+);
diff --git a/doc/modules/cassandra/examples/CQL/create_table_compound_pk.cql b/doc/modules/cassandra/examples/CQL/create_table_compound_pk.cql
new file mode 100644
index 0000000..eb199c7
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_table_compound_pk.cql
@@ -0,0 +1,7 @@
+CREATE TABLE t (
+    a int,
+    b int,
+    c int,
+    d int,
+    PRIMARY KEY ((a, b), c, d)
+);
diff --git a/doc/modules/cassandra/examples/CQL/create_table_simple.cql b/doc/modules/cassandra/examples/CQL/create_table_simple.cql
new file mode 100644
index 0000000..0ebe747
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_table_simple.cql
@@ -0,0 +1,4 @@
+CREATE TABLE users (
+    userid text PRIMARY KEY,
+    username text,
+);
diff --git a/doc/modules/cassandra/examples/CQL/create_table_single_pk.cql b/doc/modules/cassandra/examples/CQL/create_table_single_pk.cql
new file mode 100644
index 0000000..ce6fff8
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_table_single_pk.cql
@@ -0,0 +1 @@
+CREATE TABLE t (k text PRIMARY KEY);
diff --git a/doc/modules/cassandra/examples/CQL/create_trigger.cql b/doc/modules/cassandra/examples/CQL/create_trigger.cql
new file mode 100644
index 0000000..9bbf2f2
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_trigger.cql
@@ -0,0 +1 @@
+CREATE TRIGGER myTrigger ON myTable USING 'org.apache.cassandra.triggers.InvertedIndex';
diff --git a/doc/modules/cassandra/examples/CQL/create_user.cql b/doc/modules/cassandra/examples/CQL/create_user.cql
new file mode 100644
index 0000000..b6531eb
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_user.cql
@@ -0,0 +1,2 @@
+CREATE USER alice WITH PASSWORD 'password_a' SUPERUSER;
+CREATE USER bob WITH PASSWORD 'password_b' NOSUPERUSER;
diff --git a/doc/modules/cassandra/examples/CQL/create_user_role.cql b/doc/modules/cassandra/examples/CQL/create_user_role.cql
new file mode 100644
index 0000000..810f76c
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/create_user_role.cql
@@ -0,0 +1,14 @@
+CREATE USER alice WITH PASSWORD 'password_a' SUPERUSER;
+CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true AND SUPERUSER = true;
+
+CREATE USER IF NOT EXISTS alice WITH PASSWORD 'password_a' SUPERUSER;
+CREATE ROLE IF NOT EXISTS alice WITH PASSWORD = 'password_a' AND LOGIN = true AND SUPERUSER = true;
+
+CREATE USER alice WITH PASSWORD 'password_a' NOSUPERUSER;
+CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true AND SUPERUSER = false;
+
+CREATE USER alice WITH PASSWORD 'password_a' NOSUPERUSER;
+CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true;
+
+CREATE USER alice WITH PASSWORD 'password_a';
+CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true;
diff --git a/doc/modules/cassandra/examples/CQL/currentdate.cql b/doc/modules/cassandra/examples/CQL/currentdate.cql
new file mode 100644
index 0000000..0bed1b2
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/currentdate.cql
@@ -0,0 +1 @@
+SELECT * FROM myTable WHERE date >= currentDate() - 2d;
diff --git a/doc/modules/cassandra/examples/CQL/datetime_arithmetic.cql b/doc/modules/cassandra/examples/CQL/datetime_arithmetic.cql
new file mode 100644
index 0000000..310bf3b
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/datetime_arithmetic.cql
@@ -0,0 +1 @@
+SELECT * FROM myTable WHERE t = '2017-01-01' - 2d;
diff --git a/doc/modules/cassandra/examples/CQL/delete_all_elements_list.cql b/doc/modules/cassandra/examples/CQL/delete_all_elements_list.cql
new file mode 100644
index 0000000..3d02668
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/delete_all_elements_list.cql
@@ -0,0 +1 @@
+UPDATE plays SET scores = scores - [ 12, 21 ] WHERE id = '123-afde';
diff --git a/doc/modules/cassandra/examples/CQL/delete_element_list.cql b/doc/modules/cassandra/examples/CQL/delete_element_list.cql
new file mode 100644
index 0000000..26b3e58
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/delete_element_list.cql
@@ -0,0 +1 @@
+DELETE scores[1] FROM plays WHERE id = '123-afde';
diff --git a/doc/modules/cassandra/examples/CQL/delete_map.cql b/doc/modules/cassandra/examples/CQL/delete_map.cql
new file mode 100644
index 0000000..e16b134
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/delete_map.cql
@@ -0,0 +1,2 @@
+DELETE favs['author'] FROM users WHERE id = 'jsmith';
+UPDATE users SET favs = favs - { 'movie', 'band'} WHERE id = 'jsmith';
diff --git a/doc/modules/cassandra/examples/CQL/delete_set.cql b/doc/modules/cassandra/examples/CQL/delete_set.cql
new file mode 100644
index 0000000..308da3c
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/delete_set.cql
@@ -0,0 +1 @@
+UPDATE images SET tags = tags - { 'cat' } WHERE name = 'cat.jpg';
diff --git a/doc/modules/cassandra/examples/CQL/delete_statement.cql b/doc/modules/cassandra/examples/CQL/delete_statement.cql
new file mode 100644
index 0000000..b574e71
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/delete_statement.cql
@@ -0,0 +1,5 @@
+DELETE FROM NerdMovies USING TIMESTAMP 1240003134
+ WHERE movie = 'Serenity';
+
+DELETE phone FROM Users
+ WHERE userid IN (C73DE1D3-AF08-40F3-B124-3FF3E5109F22, B70DE1D0-9908-4AE3-BE34-5573E5B09F14);
diff --git a/doc/modules/cassandra/examples/CQL/drop_aggregate.cql b/doc/modules/cassandra/examples/CQL/drop_aggregate.cql
new file mode 100644
index 0000000..f05b69a
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/drop_aggregate.cql
@@ -0,0 +1,4 @@
+DROP AGGREGATE myAggregate;
+DROP AGGREGATE myKeyspace.anAggregate;
+DROP AGGREGATE someAggregate ( int );
+DROP AGGREGATE someAggregate ( text );
diff --git a/doc/modules/cassandra/examples/CQL/drop_function.cql b/doc/modules/cassandra/examples/CQL/drop_function.cql
new file mode 100644
index 0000000..6d444c1
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/drop_function.cql
@@ -0,0 +1,4 @@
+DROP FUNCTION myfunction;
+DROP FUNCTION mykeyspace.afunction;
+DROP FUNCTION afunction ( int );
+DROP FUNCTION afunction ( text );
diff --git a/doc/modules/cassandra/examples/CQL/drop_ks.cql b/doc/modules/cassandra/examples/CQL/drop_ks.cql
new file mode 100644
index 0000000..46a920d
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/drop_ks.cql
@@ -0,0 +1 @@
+DROP KEYSPACE excelsior;
diff --git a/doc/modules/cassandra/examples/CQL/drop_trigger.cql b/doc/modules/cassandra/examples/CQL/drop_trigger.cql
new file mode 100644
index 0000000..05a7a95
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/drop_trigger.cql
@@ -0,0 +1 @@
+DROP TRIGGER myTrigger ON myTable;
diff --git a/doc/modules/cassandra/examples/CQL/function_dollarsign.cql b/doc/modules/cassandra/examples/CQL/function_dollarsign.cql
new file mode 100644
index 0000000..878d044
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/function_dollarsign.cql
@@ -0,0 +1,15 @@
+CREATE FUNCTION some_function ( arg int )
+    RETURNS NULL ON NULL INPUT
+    RETURNS int
+    LANGUAGE java
+    AS $$ return arg; $$;
+
+SELECT some_function(column) FROM atable ...;
+UPDATE atable SET col = some_function(?) ...;
+
+CREATE TYPE custom_type (txt text, i int);
+CREATE FUNCTION fct_using_udt ( udtarg frozen )
+    RETURNS NULL ON NULL INPUT
+    RETURNS text
+    LANGUAGE java
+    AS $$ return udtarg.getString("txt"); $$;
diff --git a/doc/modules/cassandra/examples/CQL/function_overload.cql b/doc/modules/cassandra/examples/CQL/function_overload.cql
new file mode 100644
index 0000000..d70e8e9
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/function_overload.cql
@@ -0,0 +1,2 @@
+CREATE FUNCTION sample ( arg int ) ...;
+CREATE FUNCTION sample ( arg text ) ...;
diff --git a/doc/modules/cassandra/examples/CQL/function_udfcontext.cql b/doc/modules/cassandra/examples/CQL/function_udfcontext.cql
new file mode 100644
index 0000000..87f89fe
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/function_udfcontext.cql
@@ -0,0 +1,11 @@
+CREATE TYPE custom_type (txt text, i int);
+CREATE FUNCTION fct\_using\_udt ( somearg int )
+    RETURNS NULL ON NULL INPUT
+    RETURNS custom_type
+    LANGUAGE java
+    AS $$
+        UDTValue udt = udfContext.newReturnUDTValue();
+        udt.setString("txt", "some string");
+        udt.setInt("i", 42);
+        return udt;
+    $$;
diff --git a/doc/modules/cassandra/examples/CQL/grant_describe.cql b/doc/modules/cassandra/examples/CQL/grant_describe.cql
new file mode 100644
index 0000000..7218145
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/grant_describe.cql
@@ -0,0 +1 @@
+GRANT DESCRIBE ON ALL ROLES TO role_admin;
diff --git a/doc/modules/cassandra/examples/CQL/grant_drop.cql b/doc/modules/cassandra/examples/CQL/grant_drop.cql
new file mode 100644
index 0000000..745369d
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/grant_drop.cql
@@ -0,0 +1 @@
+GRANT DROP ON keyspace1.table1 TO schema_owner;
diff --git a/doc/modules/cassandra/examples/CQL/grant_execute.cql b/doc/modules/cassandra/examples/CQL/grant_execute.cql
new file mode 100644
index 0000000..96b34de
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/grant_execute.cql
@@ -0,0 +1 @@
+GRANT EXECUTE ON FUNCTION keyspace1.user_function( int ) TO report_writer;
diff --git a/doc/modules/cassandra/examples/CQL/grant_modify.cql b/doc/modules/cassandra/examples/CQL/grant_modify.cql
new file mode 100644
index 0000000..7f9a30b
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/grant_modify.cql
@@ -0,0 +1 @@
+GRANT MODIFY ON KEYSPACE keyspace1 TO data_writer;
diff --git a/doc/modules/cassandra/examples/CQL/grant_perm.cql b/doc/modules/cassandra/examples/CQL/grant_perm.cql
new file mode 100644
index 0000000..1dc9a7b
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/grant_perm.cql
@@ -0,0 +1 @@
+GRANT SELECT ON ALL KEYSPACES TO data_reader;
diff --git a/doc/modules/cassandra/examples/CQL/grant_role.cql b/doc/modules/cassandra/examples/CQL/grant_role.cql
new file mode 100644
index 0000000..1adffb3
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/grant_role.cql
@@ -0,0 +1 @@
+GRANT report_writer TO alice;
diff --git a/doc/modules/cassandra/examples/CQL/insert_data2_backup.cql b/doc/modules/cassandra/examples/CQL/insert_data2_backup.cql
new file mode 100644
index 0000000..35e20a3
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/insert_data2_backup.cql
@@ -0,0 +1,5 @@
+INSERT INTO journal (id, name, publisher) VALUES (0, 'Apache Cassandra Magazine', 'Apache Cassandra');
+INSERT INTO journal (id, name, publisher) VALUES (1, 'Couchbase Magazine', 'Couchbase');
+
+INSERT INTO magazine (id, name, publisher) VALUES (0, 'Apache Cassandra Magazine', 'Apache Cassandra');
+INSERT INTO magazine (id, name, publisher) VALUES (1, 'Couchbase Magazine', 'Couchbase');
diff --git a/doc/modules/cassandra/examples/CQL/insert_data_backup.cql b/doc/modules/cassandra/examples/CQL/insert_data_backup.cql
new file mode 100644
index 0000000..15eb375
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/insert_data_backup.cql
@@ -0,0 +1,6 @@
+INSERT INTO t (id, k, v) VALUES (0, 0, 'val0');
+INSERT INTO t (id, k, v) VALUES (1, 1, 'val1');
+
+INSERT INTO t2 (id, k, v) VALUES (0, 0, 'val0');
+INSERT INTO t2 (id, k, v) VALUES (1, 1, 'val1');
+INSERT INTO t2 (id, k, v) VALUES (2, 2, 'val2');
diff --git a/doc/modules/cassandra/examples/CQL/insert_duration.cql b/doc/modules/cassandra/examples/CQL/insert_duration.cql
new file mode 100644
index 0000000..b52801b
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/insert_duration.cql
@@ -0,0 +1,6 @@
+INSERT INTO RiderResults (rider, race, result)
+   VALUES ('Christopher Froome', 'Tour de France', 89h4m48s);
+INSERT INTO RiderResults (rider, race, result)
+   VALUES ('BARDET Romain', 'Tour de France', PT89H8M53S);
+INSERT INTO RiderResults (rider, race, result)
+   VALUES ('QUINTANA Nairo', 'Tour de France', P0000-00-00T89:09:09);
diff --git a/doc/modules/cassandra/examples/CQL/insert_json.cql b/doc/modules/cassandra/examples/CQL/insert_json.cql
new file mode 100644
index 0000000..d3a5dec
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/insert_json.cql
@@ -0,0 +1 @@
+INSERT INTO mytable JSON '{ "\"myKey\"": 0, "value": 0}';
diff --git a/doc/modules/cassandra/examples/CQL/insert_statement.cql b/doc/modules/cassandra/examples/CQL/insert_statement.cql
new file mode 100644
index 0000000..0f7a943
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/insert_statement.cql
@@ -0,0 +1,5 @@
+INSERT INTO NerdMovies (movie, director, main_actor, year)
+   VALUES ('Serenity', 'Joss Whedon', 'Nathan Fillion', 2005)
+   USING TTL 86400;
+
+INSERT INTO NerdMovies JSON '{"movie": "Serenity", "director": "Joss Whedon", "year": 2005}';
diff --git a/doc/modules/cassandra/examples/CQL/insert_static_data.cql b/doc/modules/cassandra/examples/CQL/insert_static_data.cql
new file mode 100644
index 0000000..c6a588f
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/insert_static_data.cql
@@ -0,0 +1,2 @@
+INSERT INTO t (pk, t, v, s) VALUES (0, 0, 'val0', 'static0');
+INSERT INTO t (pk, t, v, s) VALUES (0, 1, 'val1', 'static1');
diff --git a/doc/modules/cassandra/examples/CQL/insert_table_cc_addl.cql b/doc/modules/cassandra/examples/CQL/insert_table_cc_addl.cql
new file mode 100644
index 0000000..f574d53
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/insert_table_cc_addl.cql
@@ -0,0 +1 @@
+INSERT INTO t3 (a,b,c,d) VALUES (0,0,0,9);
diff --git a/doc/modules/cassandra/examples/CQL/insert_table_clustercolumn.cql b/doc/modules/cassandra/examples/CQL/insert_table_clustercolumn.cql
new file mode 100644
index 0000000..449f921
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/insert_table_clustercolumn.cql
@@ -0,0 +1,5 @@
+INSERT INTO t2 (a, b, c, d) VALUES (0,0,0,0);
+INSERT INTO t2 (a, b, c, d) VALUES (0,0,1,1);
+INSERT INTO t2 (a, b, c, d) VALUES (0,1,2,2);
+INSERT INTO t2 (a, b, c, d) VALUES (0,1,3,3);
+INSERT INTO t2 (a, b, c, d) VALUES (1,1,4,4);
diff --git a/doc/modules/cassandra/examples/CQL/insert_table_clustercolumn2.cql b/doc/modules/cassandra/examples/CQL/insert_table_clustercolumn2.cql
new file mode 100644
index 0000000..a048c9f
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/insert_table_clustercolumn2.cql
@@ -0,0 +1,5 @@
+INSERT INTO t3 (a, b, c, d) VALUES (0,0,0,0);
+INSERT INTO t3 (a, b, c, d) VALUES (0,0,1,1);
+INSERT INTO t3 (a, b, c, d) VALUES (0,1,2,2);
+INSERT INTO t3 (a, b, c, d) VALUES (0,1,3,3);
+INSERT INTO t3 (a, b, c, d) VALUES (1,1,4,4);
diff --git a/doc/modules/cassandra/examples/CQL/insert_table_compound_pk.cql b/doc/modules/cassandra/examples/CQL/insert_table_compound_pk.cql
new file mode 100644
index 0000000..3ce1953
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/insert_table_compound_pk.cql
@@ -0,0 +1,5 @@
+INSERT INTO t (a, b, c, d) VALUES (0,0,0,0);
+INSERT INTO t (a, b, c, d) VALUES (0,0,1,1);
+INSERT INTO t (a, b, c, d) VALUES (0,1,2,2);
+INSERT INTO t (a, b, c, d) VALUES (0,1,3,3);
+INSERT INTO t (a, b, c, d) VALUES (1,1,4,4);
diff --git a/doc/modules/cassandra/examples/CQL/insert_udt.cql b/doc/modules/cassandra/examples/CQL/insert_udt.cql
new file mode 100644
index 0000000..5c6f176
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/insert_udt.cql
@@ -0,0 +1,17 @@
+INSERT INTO user (name, addresses)
+   VALUES ('z3 Pr3z1den7', {
+     'home' : {
+        street: '1600 Pennsylvania Ave NW',
+        city: 'Washington',
+        zip: '20500',
+        phones: { 'cell' : { country_code: 1, number: '202 456-1111' },
+                  'landline' : { country_code: 1, number: '...' } }
+     },
+     'work' : {
+        street: '1600 Pennsylvania Ave NW',
+        city: 'Washington',
+        zip: '20500',
+        phones: { 'fax' : { country_code: 1, number: '...' } }
+     }
+  }
+);
diff --git a/doc/modules/cassandra/examples/CQL/list.cql b/doc/modules/cassandra/examples/CQL/list.cql
new file mode 100644
index 0000000..4d1ef13
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/list.cql
@@ -0,0 +1,12 @@
+CREATE TABLE plays (
+    id text PRIMARY KEY,
+    game text,
+    players int,
+    scores list<int> // A list of integers
+)
+
+INSERT INTO plays (id, game, players, scores)
+           VALUES ('123-afde', 'quake', 3, [17, 4, 2]);
+
+// Replace the existing list entirely
+UPDATE plays SET scores = [ 3, 9, 4] WHERE id = '123-afde';
diff --git a/doc/modules/cassandra/examples/CQL/list_all_perm.cql b/doc/modules/cassandra/examples/CQL/list_all_perm.cql
new file mode 100644
index 0000000..efbcfc8
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/list_all_perm.cql
@@ -0,0 +1 @@
+LIST ALL PERMISSIONS ON keyspace1.table1 OF bob;
diff --git a/doc/modules/cassandra/examples/CQL/list_perm.cql b/doc/modules/cassandra/examples/CQL/list_perm.cql
new file mode 100644
index 0000000..094bf09
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/list_perm.cql
@@ -0,0 +1 @@
+LIST ALL PERMISSIONS OF alice;
diff --git a/doc/modules/cassandra/examples/CQL/list_roles.cql b/doc/modules/cassandra/examples/CQL/list_roles.cql
new file mode 100644
index 0000000..5c0f063
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/list_roles.cql
@@ -0,0 +1 @@
+LIST ROLES;
diff --git a/doc/modules/cassandra/examples/CQL/list_roles_nonrecursive.cql b/doc/modules/cassandra/examples/CQL/list_roles_nonrecursive.cql
new file mode 100644
index 0000000..eea6218
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/list_roles_nonrecursive.cql
@@ -0,0 +1 @@
+LIST ROLES OF bob NORECURSIVE;
diff --git a/doc/modules/cassandra/examples/CQL/list_roles_of.cql b/doc/modules/cassandra/examples/CQL/list_roles_of.cql
new file mode 100644
index 0000000..c338ca3
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/list_roles_of.cql
@@ -0,0 +1 @@
+LIST ROLES OF alice;
diff --git a/doc/modules/cassandra/examples/CQL/list_select_perm.cql b/doc/modules/cassandra/examples/CQL/list_select_perm.cql
new file mode 100644
index 0000000..c085df4
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/list_select_perm.cql
@@ -0,0 +1 @@
+LIST SELECT PERMISSIONS OF carlos;
diff --git a/doc/modules/cassandra/examples/CQL/map.cql b/doc/modules/cassandra/examples/CQL/map.cql
new file mode 100644
index 0000000..ca9ca5e
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/map.cql
@@ -0,0 +1,11 @@
+CREATE TABLE users (
+   id text PRIMARY KEY,
+   name text,
+   favs map<text, text> // A map of text keys, and text values
+);
+
+INSERT INTO users (id, name, favs)
+   VALUES ('jsmith', 'John Smith', { 'fruit' : 'Apple', 'band' : 'Beatles' });
+
+// Replace the existing map entirely.
+UPDATE users SET favs = { 'fruit' : 'Banana' } WHERE id = 'jsmith';
diff --git a/doc/modules/cassandra/examples/CQL/min_max.cql b/doc/modules/cassandra/examples/CQL/min_max.cql
new file mode 100644
index 0000000..3f31cc5
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/min_max.cql
@@ -0,0 +1 @@
+SELECT MIN (players), MAX (players) FROM plays WHERE game = 'quake';
diff --git a/doc/modules/cassandra/examples/CQL/mv_table_def.cql b/doc/modules/cassandra/examples/CQL/mv_table_def.cql
new file mode 100644
index 0000000..106fe11
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/mv_table_def.cql
@@ -0,0 +1,8 @@
+CREATE TABLE t (
+    k int,
+    c1 int,
+    c2 int,
+    v1 int,
+    v2 int,
+    PRIMARY KEY (k, c1, c2)
+);
diff --git a/doc/modules/cassandra/examples/CQL/mv_table_error.cql b/doc/modules/cassandra/examples/CQL/mv_table_error.cql
new file mode 100644
index 0000000..e7560f9
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/mv_table_error.cql
@@ -0,0 +1,13 @@
+// Error: cannot include both v1 and v2 in the primary key as both are not in the base table primary key
+
+CREATE MATERIALIZED VIEW mv1 AS
+   SELECT * FROM t 
+   WHERE k IS NOT NULL AND c1 IS NOT NULL AND c2 IS NOT NULL AND v1 IS NOT NULL
+   PRIMARY KEY (v1, v2, k, c1, c2);
+
+// Error: must include k in the primary as it's a base table primary key column
+
+CREATE MATERIALIZED VIEW mv1 AS
+   SELECT * FROM t 
+   WHERE c1 IS NOT NULL AND c2 IS NOT NULL
+   PRIMARY KEY (c1, c2);
diff --git a/doc/modules/cassandra/examples/CQL/mv_table_from_base.cql b/doc/modules/cassandra/examples/CQL/mv_table_from_base.cql
new file mode 100644
index 0000000..bd2f9f2
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/mv_table_from_base.cql
@@ -0,0 +1,9 @@
+CREATE MATERIALIZED VIEW mv1 AS
+   SELECT * FROM t 
+   WHERE k IS NOT NULL AND c1 IS NOT NULL AND c2 IS NOT NULL
+   PRIMARY KEY (c1, k, c2);
+
+CREATE MATERIALIZED VIEW mv1 AS
+  SELECT * FROM t 
+  WHERE k IS NOT NULL AND c1 IS NOT NULL AND c2 IS NOT NULL
+  PRIMARY KEY (v1, k, c1, c2);
diff --git a/doc/modules/cassandra/examples/CQL/no_revoke.cql b/doc/modules/cassandra/examples/CQL/no_revoke.cql
new file mode 100644
index 0000000..b6a044c
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/no_revoke.cql
@@ -0,0 +1,5 @@
+* `system_schema.keyspaces`
+* `system_schema.columns`
+* `system_schema.tables`
+* `system.local`
+* `system.peers`
diff --git a/doc/modules/cassandra/examples/CQL/qs_create_ks.cql b/doc/modules/cassandra/examples/CQL/qs_create_ks.cql
new file mode 100644
index 0000000..2dba1bd
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/qs_create_ks.cql
@@ -0,0 +1,2 @@
+# Create a keyspace
+CREATE KEYSPACE IF NOT EXISTS store WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : '1' };
diff --git a/doc/modules/cassandra/examples/CQL/qs_create_table.cql b/doc/modules/cassandra/examples/CQL/qs_create_table.cql
new file mode 100644
index 0000000..daeef5f
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/qs_create_table.cql
@@ -0,0 +1,6 @@
+# Create a table
+CREATE TABLE IF NOT EXISTS store.shopping_cart  (
+	userid text PRIMARY KEY,
+	item_count int,
+	last_update_timestamp timestamp
+);
diff --git a/doc/modules/cassandra/examples/CQL/qs_insert_data.cql b/doc/modules/cassandra/examples/CQL/qs_insert_data.cql
new file mode 100644
index 0000000..130f901
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/qs_insert_data.cql
@@ -0,0 +1,7 @@
+# Insert some data
+INSERT INTO store.shopping_cart
+(userid, item_count, last_update_timestamp)
+VALUES ('9876', 2, toTimeStamp(toDate(now))));
+INSERT INTO store.shopping_cart
+(userid, item_count, last_update_timestamp)
+VALUES (1234, 5, toTimeStamp(toDate(now))));
diff --git a/doc/modules/cassandra/examples/CQL/qs_insert_data_again.cql b/doc/modules/cassandra/examples/CQL/qs_insert_data_again.cql
new file mode 100644
index 0000000..b95473f
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/qs_insert_data_again.cql
@@ -0,0 +1 @@
+INSERT (userid, item_count) VALUES (4567, 20) INTO store.shopping_cart;
diff --git a/doc/modules/cassandra/examples/CQL/qs_select_data.cql b/doc/modules/cassandra/examples/CQL/qs_select_data.cql
new file mode 100644
index 0000000..e9e55db
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/qs_select_data.cql
@@ -0,0 +1 @@
+SELECT * FROM store.shopping_cart;
diff --git a/doc/modules/cassandra/examples/CQL/query_allow_filtering.cql b/doc/modules/cassandra/examples/CQL/query_allow_filtering.cql
new file mode 100644
index 0000000..c4aaf39
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/query_allow_filtering.cql
@@ -0,0 +1,5 @@
+// All users are returned
+SELECT * FROM users;
+
+// All users with a particular birth year are returned
+SELECT * FROM users WHERE birth_year = 1981;
diff --git a/doc/modules/cassandra/examples/CQL/query_fail_allow_filtering.cql b/doc/modules/cassandra/examples/CQL/query_fail_allow_filtering.cql
new file mode 100644
index 0000000..2e6c63b
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/query_fail_allow_filtering.cql
@@ -0,0 +1 @@
+SELECT * FROM users WHERE birth_year = 1981 AND country = 'FR';
diff --git a/doc/modules/cassandra/examples/CQL/query_nofail_allow_filtering.cql b/doc/modules/cassandra/examples/CQL/query_nofail_allow_filtering.cql
new file mode 100644
index 0000000..88aed56
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/query_nofail_allow_filtering.cql
@@ -0,0 +1 @@
+SELECT * FROM users WHERE birth_year = 1981 AND country = 'FR' ALLOW FILTERING;
diff --git a/doc/modules/cassandra/examples/CQL/rename_udt_field.cql b/doc/modules/cassandra/examples/CQL/rename_udt_field.cql
new file mode 100644
index 0000000..7718788
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/rename_udt_field.cql
@@ -0,0 +1 @@
+ALTER TYPE address RENAME zip TO zipcode;
diff --git a/doc/modules/cassandra/examples/CQL/revoke_perm.cql b/doc/modules/cassandra/examples/CQL/revoke_perm.cql
new file mode 100644
index 0000000..d4ac1ed
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/revoke_perm.cql
@@ -0,0 +1,5 @@
+REVOKE SELECT ON ALL KEYSPACES FROM data_reader;
+REVOKE MODIFY ON KEYSPACE keyspace1 FROM data_writer;
+REVOKE DROP ON keyspace1.table1 FROM schema_owner;
+REVOKE EXECUTE ON FUNCTION keyspace1.user_function( int ) FROM report_writer;
+REVOKE DESCRIBE ON ALL ROLES FROM role_admin;
diff --git a/doc/modules/cassandra/examples/CQL/revoke_role.cql b/doc/modules/cassandra/examples/CQL/revoke_role.cql
new file mode 100644
index 0000000..acf5066
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/revoke_role.cql
@@ -0,0 +1 @@
+REVOKE report_writer FROM alice;
diff --git a/doc/modules/cassandra/examples/CQL/role_error.cql b/doc/modules/cassandra/examples/CQL/role_error.cql
new file mode 100644
index 0000000..fa061a2
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/role_error.cql
@@ -0,0 +1,6 @@
+GRANT role_a TO role_b;
+GRANT role_b TO role_a;
+
+GRANT role_a TO role_b;
+GRANT role_b TO role_c;
+GRANT role_c TO role_a;
diff --git a/doc/modules/cassandra/examples/CQL/select_data2_backup.cql b/doc/modules/cassandra/examples/CQL/select_data2_backup.cql
new file mode 100644
index 0000000..7a409d7
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/select_data2_backup.cql
@@ -0,0 +1,2 @@
+SELECT * FROM catalogkeyspace.journal;
+SELECT * FROM catalogkeyspace.magazine;
diff --git a/doc/modules/cassandra/examples/CQL/select_data_backup.cql b/doc/modules/cassandra/examples/CQL/select_data_backup.cql
new file mode 100644
index 0000000..4468467
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/select_data_backup.cql
@@ -0,0 +1,2 @@
+SELECT * FROM t;
+SELECT * FROM t2;
diff --git a/doc/modules/cassandra/examples/CQL/select_range.cql b/doc/modules/cassandra/examples/CQL/select_range.cql
new file mode 100644
index 0000000..fcf3bd5
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/select_range.cql
@@ -0,0 +1 @@
+SELECT * FROM t2 WHERE a = 0 AND b > 0 and b <= 3;
diff --git a/doc/modules/cassandra/examples/CQL/select_statement.cql b/doc/modules/cassandra/examples/CQL/select_statement.cql
new file mode 100644
index 0000000..cee5a19
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/select_statement.cql
@@ -0,0 +1,11 @@
+SELECT name, occupation FROM users WHERE userid IN (199, 200, 207);
+SELECT JSON name, occupation FROM users WHERE userid = 199;
+SELECT name AS user_name, occupation AS user_occupation FROM users;
+
+SELECT time, value
+FROM events
+WHERE event_type = 'myEvent'
+  AND time > '2011-02-03'
+  AND time <= '2012-01-01'
+
+SELECT COUNT (*) AS user_count FROM users;
diff --git a/doc/modules/cassandra/examples/CQL/select_static_data.cql b/doc/modules/cassandra/examples/CQL/select_static_data.cql
new file mode 100644
index 0000000..8bca937
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/select_static_data.cql
@@ -0,0 +1 @@
+SELECT * FROM t;
diff --git a/doc/modules/cassandra/examples/CQL/select_table_clustercolumn.cql b/doc/modules/cassandra/examples/CQL/select_table_clustercolumn.cql
new file mode 100644
index 0000000..60bb2cf
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/select_table_clustercolumn.cql
@@ -0,0 +1 @@
+SELECT * FROM t2;
diff --git a/doc/modules/cassandra/examples/CQL/select_table_compound_pk.cql b/doc/modules/cassandra/examples/CQL/select_table_compound_pk.cql
new file mode 100644
index 0000000..8bca937
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/select_table_compound_pk.cql
@@ -0,0 +1 @@
+SELECT * FROM t;
diff --git a/doc/modules/cassandra/examples/CQL/set.cql b/doc/modules/cassandra/examples/CQL/set.cql
new file mode 100644
index 0000000..607981b
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/set.cql
@@ -0,0 +1,11 @@
+CREATE TABLE images (
+   name text PRIMARY KEY,
+   owner text,
+   tags set<text> // A set of text values
+);
+
+INSERT INTO images (name, owner, tags)
+   VALUES ('cat.jpg', 'jsmith', { 'pet', 'cute' });
+
+// Replace the existing set entirely
+UPDATE images SET tags = { 'kitten', 'cat', 'lol' } WHERE name = 'cat.jpg';
diff --git a/doc/modules/cassandra/examples/CQL/spec_retry_values.cql b/doc/modules/cassandra/examples/CQL/spec_retry_values.cql
new file mode 100644
index 0000000..bcd8d26
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/spec_retry_values.cql
@@ -0,0 +1,6 @@
+min(99percentile,50ms)
+max(99p,50MS)
+MAX(99P,50ms)
+MIN(99.9PERCENTILE,50ms)
+max(90percentile,100MS)
+MAX(100.0PERCENTILE,60ms)
diff --git a/doc/modules/cassandra/examples/CQL/sum.cql b/doc/modules/cassandra/examples/CQL/sum.cql
new file mode 100644
index 0000000..bccfcbc
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/sum.cql
@@ -0,0 +1 @@
+SELECT SUM (players) FROM plays;
diff --git a/doc/modules/cassandra/examples/CQL/table_for_where.cql b/doc/modules/cassandra/examples/CQL/table_for_where.cql
new file mode 100644
index 0000000..f5ed500
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/table_for_where.cql
@@ -0,0 +1,9 @@
+CREATE TABLE posts (
+    userid text,
+    blog_title text,
+    posted_at timestamp,
+    entry_title text,
+    content text,
+    category int,
+    PRIMARY KEY (userid, blog_title, posted_at)
+);
diff --git a/doc/modules/cassandra/examples/CQL/timeuuid_min_max.cql b/doc/modules/cassandra/examples/CQL/timeuuid_min_max.cql
new file mode 100644
index 0000000..81353f5
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/timeuuid_min_max.cql
@@ -0,0 +1,3 @@
+SELECT * FROM myTable
+ WHERE t > maxTimeuuid('2013-01-01 00:05+0000')
+   AND t < minTimeuuid('2013-02-02 10:00+0000');
diff --git a/doc/modules/cassandra/examples/CQL/timeuuid_now.cql b/doc/modules/cassandra/examples/CQL/timeuuid_now.cql
new file mode 100644
index 0000000..54c2cc4
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/timeuuid_now.cql
@@ -0,0 +1 @@
+SELECT * FROM myTable WHERE t = now();
diff --git a/doc/modules/cassandra/examples/CQL/token.cql b/doc/modules/cassandra/examples/CQL/token.cql
new file mode 100644
index 0000000..b5c7f8b
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/token.cql
@@ -0,0 +1,2 @@
+SELECT * FROM posts
+ WHERE token(userid) > token('tom') AND token(userid) < token('bob');
diff --git a/doc/modules/cassandra/examples/CQL/tuple.cql b/doc/modules/cassandra/examples/CQL/tuple.cql
new file mode 100644
index 0000000..b612d07
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/tuple.cql
@@ -0,0 +1,6 @@
+CREATE TABLE durations (
+  event text,
+  duration tuple<int, text>,
+);
+
+INSERT INTO durations (event, duration) VALUES ('ev1', (3, 'hours'));
diff --git a/doc/modules/cassandra/examples/CQL/uda.cql b/doc/modules/cassandra/examples/CQL/uda.cql
new file mode 100644
index 0000000..b40dd11
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/uda.cql
@@ -0,0 +1,41 @@
+CREATE OR REPLACE FUNCTION test.averageState(state tuple<int,bigint>, val int)
+    CALLED ON NULL INPUT
+    RETURNS tuple
+    LANGUAGE java
+    AS $$
+        if (val != null) {
+            state.setInt(0, state.getInt(0)+1);
+            state.setLong(1, state.getLong(1)+val.intValue());
+        }
+        return state;
+    $$;
+
+CREATE OR REPLACE FUNCTION test.averageFinal (state tuple<int,bigint>)
+    CALLED ON NULL INPUT
+    RETURNS double
+    LANGUAGE java
+    AS $$
+        double r = 0;
+        if (state.getInt(0) == 0) return null;
+        r = state.getLong(1);
+        r /= state.getInt(0);
+        return Double.valueOf(r);
+    $$;
+
+CREATE OR REPLACE AGGREGATE test.average(int)
+    SFUNC averageState
+    STYPE tuple
+    FINALFUNC averageFinal
+    INITCOND (0, 0);
+
+CREATE TABLE test.atable (
+    pk int PRIMARY KEY,
+    val int
+);
+
+INSERT INTO test.atable (pk, val) VALUES (1,1);
+INSERT INTO test.atable (pk, val) VALUES (2,2);
+INSERT INTO test.atable (pk, val) VALUES (3,3);
+INSERT INTO test.atable (pk, val) VALUES (4,4);
+
+SELECT test.average(val) FROM atable;
diff --git a/doc/modules/cassandra/examples/CQL/udt.cql b/doc/modules/cassandra/examples/CQL/udt.cql
new file mode 100644
index 0000000..defcc82
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/udt.cql
@@ -0,0 +1,16 @@
+CREATE TYPE phone (
+    country_code int,
+    number text,
+);
+
+CREATE TYPE address (
+    street text,
+    city text,
+    zip text,
+    phones map<text, phone>
+);
+
+CREATE TABLE user (
+    name text PRIMARY KEY,
+    addresses map<text, frozen<address>>
+);
diff --git a/doc/modules/cassandra/examples/CQL/update_list.cql b/doc/modules/cassandra/examples/CQL/update_list.cql
new file mode 100644
index 0000000..70aacf5
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/update_list.cql
@@ -0,0 +1,2 @@
+UPDATE plays SET players = 5, scores = scores + [ 14, 21 ] WHERE id = '123-afde';
+UPDATE plays SET players = 6, scores = [ 3 ] + scores WHERE id = '123-afde';
diff --git a/doc/modules/cassandra/examples/CQL/update_map.cql b/doc/modules/cassandra/examples/CQL/update_map.cql
new file mode 100644
index 0000000..870f463
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/update_map.cql
@@ -0,0 +1,2 @@
+UPDATE users SET favs['author'] = 'Ed Poe' WHERE id = 'jsmith';
+UPDATE users SET favs = favs + { 'movie' : 'Cassablanca', 'band' : 'ZZ Top' } WHERE id = 'jsmith';
diff --git a/doc/modules/cassandra/examples/CQL/update_particular_list_element.cql b/doc/modules/cassandra/examples/CQL/update_particular_list_element.cql
new file mode 100644
index 0000000..604ad34
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/update_particular_list_element.cql
@@ -0,0 +1 @@
+UPDATE plays SET scores[1] = 7 WHERE id = '123-afde';
diff --git a/doc/modules/cassandra/examples/CQL/update_set.cql b/doc/modules/cassandra/examples/CQL/update_set.cql
new file mode 100644
index 0000000..16e6eb2
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/update_set.cql
@@ -0,0 +1 @@
+UPDATE images SET tags = tags + { 'gray', 'cuddly' } WHERE name = 'cat.jpg';
diff --git a/doc/modules/cassandra/examples/CQL/update_statement.cql b/doc/modules/cassandra/examples/CQL/update_statement.cql
new file mode 100644
index 0000000..7e1cfa7
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/update_statement.cql
@@ -0,0 +1,10 @@
+UPDATE NerdMovies USING TTL 400
+   SET director   = 'Joss Whedon',
+       main_actor = 'Nathan Fillion',
+       year       = 2005
+ WHERE movie = 'Serenity';
+
+UPDATE UserActions
+   SET total = total + 2
+   WHERE user = B70DE1D0-9908-4AE3-BE34-5573E5B09F14
+     AND action = 'click';
diff --git a/doc/modules/cassandra/examples/CQL/update_ttl_map.cql b/doc/modules/cassandra/examples/CQL/update_ttl_map.cql
new file mode 100644
index 0000000..d2db9bd
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/update_ttl_map.cql
@@ -0,0 +1 @@
+UPDATE users USING TTL 10 SET favs['color'] = 'green' WHERE id = 'jsmith';
diff --git a/doc/modules/cassandra/examples/CQL/use_ks.cql b/doc/modules/cassandra/examples/CQL/use_ks.cql
new file mode 100644
index 0000000..b3aaaf3
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/use_ks.cql
@@ -0,0 +1 @@
+USE excelsior;
diff --git a/doc/modules/cassandra/examples/CQL/where.cql b/doc/modules/cassandra/examples/CQL/where.cql
new file mode 100644
index 0000000..22d4bca
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/where.cql
@@ -0,0 +1,4 @@
+SELECT entry_title, content FROM posts
+ WHERE userid = 'john doe'
+   AND blog_title='John''s Blog'
+   AND posted_at >= '2012-01-01' AND posted_at < '2012-01-31';
diff --git a/doc/modules/cassandra/examples/CQL/where_fail.cql b/doc/modules/cassandra/examples/CQL/where_fail.cql
new file mode 100644
index 0000000..57413df
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/where_fail.cql
@@ -0,0 +1,5 @@
+// Needs a blog_title to be set to select ranges of posted_at
+
+SELECT entry_title, content FROM posts
+ WHERE userid = 'john doe'
+   AND posted_at >= '2012-01-01' AND posted_at < '2012-01-31';
diff --git a/doc/modules/cassandra/examples/CQL/where_group_cluster_columns.cql b/doc/modules/cassandra/examples/CQL/where_group_cluster_columns.cql
new file mode 100644
index 0000000..1efb55e
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/where_group_cluster_columns.cql
@@ -0,0 +1,3 @@
+SELECT * FROM posts
+ WHERE userid = 'john doe'
+   AND (blog_title, posted_at) > ('John''s Blog', '2012-01-01');
diff --git a/doc/modules/cassandra/examples/CQL/where_in_tuple.cql b/doc/modules/cassandra/examples/CQL/where_in_tuple.cql
new file mode 100644
index 0000000..1d55804
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/where_in_tuple.cql
@@ -0,0 +1,3 @@
+SELECT * FROM posts
+ WHERE userid = 'john doe'
+   AND (blog_title, posted_at) IN (('John''s Blog', '2012-01-01'), ('Extreme Chess', '2014-06-01'));
diff --git a/doc/modules/cassandra/examples/CQL/where_no_group_cluster_columns.cql b/doc/modules/cassandra/examples/CQL/where_no_group_cluster_columns.cql
new file mode 100644
index 0000000..6681ba5
--- /dev/null
+++ b/doc/modules/cassandra/examples/CQL/where_no_group_cluster_columns.cql
@@ -0,0 +1,4 @@
+SELECT * FROM posts
+ WHERE userid = 'john doe'
+   AND blog_title > 'John''s Blog'
+   AND posted_at > '2012-01-01';
diff --git a/doc/modules/cassandra/examples/JAVA/udf_imports.java b/doc/modules/cassandra/examples/JAVA/udf_imports.java
new file mode 100644
index 0000000..6b883bf
--- /dev/null
+++ b/doc/modules/cassandra/examples/JAVA/udf_imports.java
@@ -0,0 +1,8 @@
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import com.datastax.driver.core.TypeCodec;
+import com.datastax.driver.core.TupleValue;
+import com.datastax.driver.core.UDTValue;
diff --git a/doc/modules/cassandra/examples/JAVA/udfcontext.java b/doc/modules/cassandra/examples/JAVA/udfcontext.java
new file mode 100644
index 0000000..65e0c7f
--- /dev/null
+++ b/doc/modules/cassandra/examples/JAVA/udfcontext.java
@@ -0,0 +1,11 @@
+public interface UDFContext
+{
+    UDTValue newArgUDTValue(String argName);
+    UDTValue newArgUDTValue(int argNum);
+    UDTValue newReturnUDTValue();
+    UDTValue newUDTValue(String udtName);
+    TupleValue newArgTupleValue(String argName);
+    TupleValue newArgTupleValue(int argNum);
+    TupleValue newReturnTupleValue();
+    TupleValue newTupleValue(String cqlDefinition);
+}
diff --git a/doc/modules/cassandra/examples/RESULTS/add_repo_keys.result b/doc/modules/cassandra/examples/RESULTS/add_repo_keys.result
new file mode 100644
index 0000000..4736ecea
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/add_repo_keys.result
@@ -0,0 +1,4 @@
+% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
+                                 Dload  Upload   Total   Spent    Left  Speed
+100  266k  100  266k    0     0   320k      0 --:--:-- --:--:-- --:--:--  320k
+OK
diff --git a/doc/modules/cassandra/examples/RESULTS/add_yum_repo.result b/doc/modules/cassandra/examples/RESULTS/add_yum_repo.result
new file mode 100644
index 0000000..47ff95f
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/add_yum_repo.result
@@ -0,0 +1,6 @@
+[cassandra]
+name=Apache Cassandra
+baseurl=https://redhat.cassandra.apache.org/311x/
+gpgcheck=1
+repo_gpgcheck=1
+gpgkey=https://downloads.apache.org/cassandra/KEYS
diff --git a/doc/modules/cassandra/examples/RESULTS/autoexpand_exclude_dc.result b/doc/modules/cassandra/examples/RESULTS/autoexpand_exclude_dc.result
new file mode 100644
index 0000000..6d5a8a4
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/autoexpand_exclude_dc.result
@@ -0,0 +1 @@
+CREATE KEYSPACE excalibur WITH replication = {'class': 'NetworkTopologyStrategy', 'DC1': '3'} AND durable_writes = true;
diff --git a/doc/modules/cassandra/examples/RESULTS/autoexpand_ks.result b/doc/modules/cassandra/examples/RESULTS/autoexpand_ks.result
new file mode 100644
index 0000000..fcc8855
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/autoexpand_ks.result
@@ -0,0 +1 @@
+CREATE KEYSPACE excalibur WITH replication = {'class': 'NetworkTopologyStrategy', 'DC1': '3', 'DC2': '3'} AND durable_writes = true;
diff --git a/doc/modules/cassandra/examples/RESULTS/autoexpand_ks_override.result b/doc/modules/cassandra/examples/RESULTS/autoexpand_ks_override.result
new file mode 100644
index 0000000..b76189d
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/autoexpand_ks_override.result
@@ -0,0 +1 @@
+CREATE KEYSPACE excalibur WITH replication = {'class': 'NetworkTopologyStrategy', 'DC1': '3', 'DC2': '2'} AND durable_writes = true;
diff --git a/doc/modules/cassandra/examples/RESULTS/cqlsh_localhost.result b/doc/modules/cassandra/examples/RESULTS/cqlsh_localhost.result
new file mode 100644
index 0000000..b5a1908
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/cqlsh_localhost.result
@@ -0,0 +1,11 @@
+Connected to Test Cluster at localhost:9042.
+[cqlsh 5.0.1 | Cassandra 3.8 | CQL spec 3.4.2 | Native protocol v4]
+Use HELP for help.
+cqlsh> SELECT cluster_name, listen_address FROM system.local;
+
+ cluster_name | listen_address
+--------------+----------------
+ Test Cluster |      127.0.0.1
+
+(1 rows)
+cqlsh>
diff --git a/doc/modules/cassandra/examples/RESULTS/curl_verify_sha.result b/doc/modules/cassandra/examples/RESULTS/curl_verify_sha.result
new file mode 100644
index 0000000..ac77d26
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/curl_verify_sha.result
@@ -0,0 +1 @@
+28757dde589f70410f9a6a95c39ee7e6cde63440e2b06b91ae6b200614fa364d
diff --git a/doc/modules/cassandra/examples/RESULTS/find_backups.result b/doc/modules/cassandra/examples/RESULTS/find_backups.result
new file mode 100644
index 0000000..156b569
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/find_backups.result
@@ -0,0 +1,4 @@
+./cassandra/data/data/cqlkeyspace/t-d132e240c21711e9bbee19821dcea330/backups
+./cassandra/data/data/cqlkeyspace/t2-d993a390c22911e9b1350d927649052c/backups
+./cassandra/data/data/catalogkeyspace/journal-296a2d30c22a11e9b1350d927649052c/backups
+./cassandra/data/data/catalogkeyspace/magazine-446eae30c22a11e9b1350d927649052c/backups
diff --git a/doc/modules/cassandra/examples/RESULTS/find_backups_table.result b/doc/modules/cassandra/examples/RESULTS/find_backups_table.result
new file mode 100644
index 0000000..7e01fa6
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/find_backups_table.result
@@ -0,0 +1 @@
+./cassandra/data/data/cqlkeyspace/t-d132e240c21711e9bbee19821dcea330/backups
diff --git a/doc/modules/cassandra/examples/RESULTS/find_two_snapshots.result b/doc/modules/cassandra/examples/RESULTS/find_two_snapshots.result
new file mode 100644
index 0000000..9cfb693
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/find_two_snapshots.result
@@ -0,0 +1,3 @@
+total 0
+drwxrwxr-x. 2 ec2-user ec2-user 265 Aug 19 02:44 catalog-ks
+drwxrwxr-x. 2 ec2-user ec2-user 265 Aug 19 02:52 multi-ks
diff --git a/doc/modules/cassandra/examples/RESULTS/flush_and_check.result b/doc/modules/cassandra/examples/RESULTS/flush_and_check.result
new file mode 100644
index 0000000..33863ad
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/flush_and_check.result
@@ -0,0 +1,9 @@
+total 36
+-rw-rw-r--. 2 ec2-user ec2-user   47 Aug 19 00:32 na-1-big-CompressionInfo.db
+-rw-rw-r--. 2 ec2-user ec2-user   43 Aug 19 00:32 na-1-big-Data.db
+-rw-rw-r--. 2 ec2-user ec2-user   10 Aug 19 00:32 na-1-big-Digest.crc32
+-rw-rw-r--. 2 ec2-user ec2-user   16 Aug 19 00:32 na-1-big-Filter.db
+-rw-rw-r--. 2 ec2-user ec2-user    8 Aug 19 00:32 na-1-big-Index.db
+-rw-rw-r--. 2 ec2-user ec2-user 4673 Aug 19 00:32 na-1-big-Statistics.db
+-rw-rw-r--. 2 ec2-user ec2-user   56 Aug 19 00:32 na-1-big-Summary.db
+-rw-rw-r--. 2 ec2-user ec2-user   92 Aug 19 00:32 na-1-big-TOC.txt
diff --git a/doc/modules/cassandra/examples/RESULTS/flush_and_check2.result b/doc/modules/cassandra/examples/RESULTS/flush_and_check2.result
new file mode 100644
index 0000000..d89b991
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/flush_and_check2.result
@@ -0,0 +1,17 @@
+total 72
+-rw-rw-r--. 2 ec2-user ec2-user   47 Aug 19 00:32 na-1-big-CompressionInfo.db
+-rw-rw-r--. 2 ec2-user ec2-user   43 Aug 19 00:32 na-1-big-Data.db
+-rw-rw-r--. 2 ec2-user ec2-user   10 Aug 19 00:32 na-1-big-Digest.crc32
+-rw-rw-r--. 2 ec2-user ec2-user   16 Aug 19 00:32 na-1-big-Filter.db
+-rw-rw-r--. 2 ec2-user ec2-user    8 Aug 19 00:32 na-1-big-Index.db
+-rw-rw-r--. 2 ec2-user ec2-user 4673 Aug 19 00:32 na-1-big-Statistics.db
+-rw-rw-r--. 2 ec2-user ec2-user   56 Aug 19 00:32 na-1-big-Summary.db
+-rw-rw-r--. 2 ec2-user ec2-user   92 Aug 19 00:32 na-1-big-TOC.txt
+-rw-rw-r--. 2 ec2-user ec2-user   47 Aug 19 00:35 na-2-big-CompressionInfo.db
+-rw-rw-r--. 2 ec2-user ec2-user   41 Aug 19 00:35 na-2-big-Data.db
+-rw-rw-r--. 2 ec2-user ec2-user   10 Aug 19 00:35 na-2-big-Digest.crc32
+-rw-rw-r--. 2 ec2-user ec2-user   16 Aug 19 00:35 na-2-big-Filter.db
+-rw-rw-r--. 2 ec2-user ec2-user    8 Aug 19 00:35 na-2-big-Index.db
+-rw-rw-r--. 2 ec2-user ec2-user 4673 Aug 19 00:35 na-2-big-Statistics.db
+-rw-rw-r--. 2 ec2-user ec2-user   56 Aug 19 00:35 na-2-big-Summary.db
+-rw-rw-r--. 2 ec2-user ec2-user   92 Aug 19 00:35 na-2-big-TOC.txt
diff --git a/doc/modules/cassandra/examples/RESULTS/insert_data2_backup.result b/doc/modules/cassandra/examples/RESULTS/insert_data2_backup.result
new file mode 100644
index 0000000..23e3902
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/insert_data2_backup.result
@@ -0,0 +1,13 @@
+id | name                      | publisher
+----+---------------------------+------------------
+ 1 |        Couchbase Magazine |        Couchbase
+ 0 | Apache Cassandra Magazine | Apache Cassandra
+
+ (2 rows)
+
+id | name                      | publisher
+----+---------------------------+------------------
+ 1 |        Couchbase Magazine |        Couchbase
+ 0 | Apache Cassandra Magazine | Apache Cassandra
+
+ (2 rows)
diff --git a/doc/modules/cassandra/examples/RESULTS/insert_table_cc_addl.result b/doc/modules/cassandra/examples/RESULTS/insert_table_cc_addl.result
new file mode 100644
index 0000000..d9af0c6
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/insert_table_cc_addl.result
@@ -0,0 +1,9 @@
+ a | b | c | d
+---+---+---+---
+ 1 | 1 | 4 | 4
+ 0 | 0 | 0 | 9	<1>
+ 0 | 0 | 1 | 1
+ 0 | 1 | 2 | 2
+ 0 | 1 | 3 | 3
+
+(5 rows)
diff --git a/doc/modules/cassandra/examples/RESULTS/java_verify.result b/doc/modules/cassandra/examples/RESULTS/java_verify.result
new file mode 100644
index 0000000..3ea9625
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/java_verify.result
@@ -0,0 +1,3 @@
+openjdk version "1.8.0_222"							
+OpenJDK Runtime Environment (build 1.8.0_222-8u222-b10-1ubuntu1~16.04.1-b10)	
+OpenJDK 64-Bit Server VM (build 25.222-b10, mixed mode)				
diff --git a/doc/modules/cassandra/examples/RESULTS/no_bups.result b/doc/modules/cassandra/examples/RESULTS/no_bups.result
new file mode 100644
index 0000000..9281104
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/no_bups.result
@@ -0,0 +1 @@
+total 0
diff --git a/doc/modules/cassandra/examples/RESULTS/nodetool_list_snapshots.result b/doc/modules/cassandra/examples/RESULTS/nodetool_list_snapshots.result
new file mode 100644
index 0000000..15503ed
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/nodetool_list_snapshots.result
@@ -0,0 +1,13 @@
+Snapshot Details:
+Snapshot name Keyspace name   Column family name True size Size on disk
+multi-table   cqlkeyspace     t2                 4.86 KiB  5.67 KiB
+multi-table   cqlkeyspace     t                  4.89 KiB  5.7 KiB
+multi-ks      cqlkeyspace     t                  4.89 KiB  5.7 KiB
+multi-ks      catalogkeyspace journal            4.9 KiB   5.73 KiB
+magazine      catalogkeyspace magazine           4.9 KiB   5.73 KiB
+multi-table-2 cqlkeyspace     t2                 4.86 KiB  5.67 KiB
+multi-table-2 cqlkeyspace     t                  4.89 KiB  5.7 KiB
+catalog-ks    catalogkeyspace journal            4.9 KiB   5.73 KiB
+catalog-ks    catalogkeyspace magazine           4.9 KiB   5.73 KiB
+
+Total TrueDiskSpaceUsed: 44.02 KiB
diff --git a/doc/modules/cassandra/examples/RESULTS/nodetool_snapshot_help.result b/doc/modules/cassandra/examples/RESULTS/nodetool_snapshot_help.result
new file mode 100644
index 0000000..a583608
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/nodetool_snapshot_help.result
@@ -0,0 +1,54 @@
+NAME
+       nodetool snapshot - Take a snapshot of specified keyspaces or a snapshot
+       of the specified table
+
+SYNOPSIS
+       nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]
+               [(-pp | --print-port)] [(-pw <password> | --password <password>)]
+               [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]
+               [(-u <username> | --username <username>)] snapshot
+               [(-cf <table> | --column-family <table> | --table <table>)]
+               [(-kt <ktlist> | --kt-list <ktlist> | -kc <ktlist> | --kc.list <ktlist>)]
+               [(-sf | --skip-flush)] [(-t <tag> | --tag <tag>)] [--] [<keyspaces...>]
+
+OPTIONS
+       -cf <table>, --column-family <table>, --table <table>
+           The table name (you must specify one and only one keyspace for using
+           this option)
+
+       -h <host>, --host <host>
+           Node hostname or ip address
+
+       -kt <ktlist>, --kt-list <ktlist>, -kc <ktlist>, --kc.list <ktlist>
+           The list of Keyspace.table to take snapshot.(you must not specify
+           only keyspace)
+
+       -p <port>, --port <port>
+           Remote jmx agent port number
+
+       -pp, --print-port
+           Operate in 4.0 mode with hosts disambiguated by port number
+
+       -pw <password>, --password <password>
+           Remote jmx agent password
+
+       -pwf <passwordFilePath>, --password-file <passwordFilePath>
+           Path to the JMX password file
+
+       -sf, --skip-flush
+           Do not flush memtables before snapshotting (snapshot will not
+           contain unflushed data)
+
+       -t <tag>, --tag <tag>
+           The name of the snapshot
+
+       -u <username>, --username <username>
+           Remote jmx agent username
+
+       --
+           This option can be used to separate command-line options from the
+           list of argument, (useful when arguments might be mistaken for
+           command-line options
+
+       [<keyspaces...>]
+           List of keyspaces. By default, all keyspaces
diff --git a/doc/modules/cassandra/examples/RESULTS/select_data2_backup.result b/doc/modules/cassandra/examples/RESULTS/select_data2_backup.result
new file mode 100644
index 0000000..23e3902
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/select_data2_backup.result
@@ -0,0 +1,13 @@
+id | name                      | publisher
+----+---------------------------+------------------
+ 1 |        Couchbase Magazine |        Couchbase
+ 0 | Apache Cassandra Magazine | Apache Cassandra
+
+ (2 rows)
+
+id | name                      | publisher
+----+---------------------------+------------------
+ 1 |        Couchbase Magazine |        Couchbase
+ 0 | Apache Cassandra Magazine | Apache Cassandra
+
+ (2 rows)
diff --git a/doc/modules/cassandra/examples/RESULTS/select_data_backup.result b/doc/modules/cassandra/examples/RESULTS/select_data_backup.result
new file mode 100644
index 0000000..5d6a9e3
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/select_data_backup.result
@@ -0,0 +1,15 @@
+id | k | v
+----+---+------
+ 1 | 1 | val1
+ 0 | 0 | val0
+
+ (2 rows)
+
+
+id | k | v
+----+---+------
+ 1 | 1 | val1
+ 0 | 0 | val0
+ 2 | 2 | val2
+
+ (3 rows)
diff --git a/doc/modules/cassandra/examples/RESULTS/select_range.result b/doc/modules/cassandra/examples/RESULTS/select_range.result
new file mode 100644
index 0000000..a3d1c76
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/select_range.result
@@ -0,0 +1,6 @@
+ a | b | c | d
+---+---+---+---
+ 0 | 1 | 2 | 2
+ 0 | 1 | 3 | 3
+
+(2 rows)
diff --git a/doc/modules/cassandra/examples/RESULTS/select_static_data.result b/doc/modules/cassandra/examples/RESULTS/select_static_data.result
new file mode 100644
index 0000000..f1e8dec
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/select_static_data.result
@@ -0,0 +1,4 @@
+   pk | t | v      | s
+  ----+---+--------+-----------
+   0  | 0 | 'val0' | 'static1'
+   0  | 1 | 'val1' | 'static1'
diff --git a/doc/modules/cassandra/examples/RESULTS/select_table_clustercolumn.result b/doc/modules/cassandra/examples/RESULTS/select_table_clustercolumn.result
new file mode 100644
index 0000000..1d3899d
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/select_table_clustercolumn.result
@@ -0,0 +1,9 @@
+ a | b | c | d
+---+---+---+---
+ 1 | 1 | 4 | 4	<1>
+ 0 | 0 | 0 | 0	
+ 0 | 0 | 1 | 1	
+ 0 | 1 | 2 | 2	
+ 0 | 1 | 3 | 3	
+
+(5 rows)
diff --git a/doc/modules/cassandra/examples/RESULTS/select_table_compound_pk.result b/doc/modules/cassandra/examples/RESULTS/select_table_compound_pk.result
new file mode 100644
index 0000000..d098516
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/select_table_compound_pk.result
@@ -0,0 +1,9 @@
+ a | b | c | d
+---+---+---+---
+ 0 | 0 | 0 | 0 	<1>
+ 0 | 0 | 1 | 1	
+ 0 | 1 | 2 | 2	<2>
+ 0 | 1 | 3 | 3	
+ 1 | 1 | 4 | 4  <3>
+
+(5 rows)
diff --git a/doc/modules/cassandra/examples/RESULTS/snapshot_all.result b/doc/modules/cassandra/examples/RESULTS/snapshot_all.result
new file mode 100644
index 0000000..6ec55a0
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/snapshot_all.result
@@ -0,0 +1,4 @@
+./cassandra/data/data/cqlkeyspace/t-d132e240c21711e9bbee19821dcea330/snapshots
+./cassandra/data/data/cqlkeyspace/t2-d993a390c22911e9b1350d927649052c/snapshots
+./cassandra/data/data/catalogkeyspace/journal-296a2d30c22a11e9b1350d927649052c/snapshots
+./cassandra/data/data/catalogkeyspace/magazine-446eae30c22a11e9b1350d927649052c/snapshots
diff --git a/doc/modules/cassandra/examples/RESULTS/snapshot_backup2.result b/doc/modules/cassandra/examples/RESULTS/snapshot_backup2.result
new file mode 100644
index 0000000..8276d52
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/snapshot_backup2.result
@@ -0,0 +1,3 @@
+Requested creating snapshot(s) for [catalogkeyspace] with snapshot name [catalog-ks] and
+options {skipFlush=false}
+Snapshot directory: catalog-ks
diff --git a/doc/modules/cassandra/examples/RESULTS/snapshot_backup2_find.result b/doc/modules/cassandra/examples/RESULTS/snapshot_backup2_find.result
new file mode 100644
index 0000000..88b5499
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/snapshot_backup2_find.result
@@ -0,0 +1,2 @@
+./cassandra/data/data/catalogkeyspace/journal-296a2d30c22a11e9b1350d927649052c/snapshots
+./cassandra/data/data/catalogkeyspace/magazine-446eae30c22a11e9b1350d927649052c/snapshots
diff --git a/doc/modules/cassandra/examples/RESULTS/snapshot_files.result b/doc/modules/cassandra/examples/RESULTS/snapshot_files.result
new file mode 100644
index 0000000..8dd91b5
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/snapshot_files.result
@@ -0,0 +1,11 @@
+total 44
+-rw-rw-r--. 1 ec2-user ec2-user   31 Aug 19 02:44 manifest.jsonZ
+-rw-rw-r--. 4 ec2-user ec2-user   47 Aug 19 02:38 na-1-big-CompressionInfo.db
+-rw-rw-r--. 4 ec2-user ec2-user   97 Aug 19 02:38 na-1-big-Data.db
+-rw-rw-r--. 4 ec2-user ec2-user   10 Aug 19 02:38 na-1-big-Digest.crc32
+-rw-rw-r--. 4 ec2-user ec2-user   16 Aug 19 02:38 na-1-big-Filter.db
+-rw-rw-r--. 4 ec2-user ec2-user   16 Aug 19 02:38 na-1-big-Index.db
+-rw-rw-r--. 4 ec2-user ec2-user 4687 Aug 19 02:38 na-1-big-Statistics.db
+-rw-rw-r--. 4 ec2-user ec2-user   56 Aug 19 02:38 na-1-big-Summary.db
+-rw-rw-r--. 4 ec2-user ec2-user   92 Aug 19 02:38 na-1-big-TOC.txt
+-rw-rw-r--. 1 ec2-user ec2-user  814 Aug 19 02:44 schema.cql
diff --git a/doc/modules/cassandra/examples/RESULTS/snapshot_mult_ks.result b/doc/modules/cassandra/examples/RESULTS/snapshot_mult_ks.result
new file mode 100644
index 0000000..61dff93
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/snapshot_mult_ks.result
@@ -0,0 +1,3 @@
+Requested creating snapshot(s) for [catalogkeyspace.journal,cqlkeyspace.t] with snapshot
+name [multi-ks] and options {skipFlush=false}
+Snapshot directory: multi-ks
diff --git a/doc/modules/cassandra/examples/RESULTS/snapshot_mult_tables.result b/doc/modules/cassandra/examples/RESULTS/snapshot_mult_tables.result
new file mode 100644
index 0000000..557a6a4
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/snapshot_mult_tables.result
@@ -0,0 +1,3 @@
+Requested creating snapshot(s) for ["CQLKeyspace".t,"CQLKeyspace".t2] with snapshot name [multi-
+table] and options {skipFlush=false}
+Snapshot directory: multi-table
diff --git a/doc/modules/cassandra/examples/RESULTS/snapshot_mult_tables_again.result b/doc/modules/cassandra/examples/RESULTS/snapshot_mult_tables_again.result
new file mode 100644
index 0000000..6c09e71
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/snapshot_mult_tables_again.result
@@ -0,0 +1,3 @@
+Requested creating snapshot(s) for ["CQLKeyspace".t,"CQLKeyspace".t2] with snapshot name [multi-
+table-2] and options {skipFlush=false}
+Snapshot directory: multi-table-2
diff --git a/doc/modules/cassandra/examples/RESULTS/snapshot_one_table2.result b/doc/modules/cassandra/examples/RESULTS/snapshot_one_table2.result
new file mode 100644
index 0000000..c147889
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/snapshot_one_table2.result
@@ -0,0 +1,3 @@
+Requested creating snapshot(s) for [catalogkeyspace] with snapshot name [magazine] and
+options {skipFlush=false}
+Snapshot directory: magazine
diff --git a/doc/modules/cassandra/examples/RESULTS/tail_syslog.result b/doc/modules/cassandra/examples/RESULTS/tail_syslog.result
new file mode 100644
index 0000000..cb32dc0
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/tail_syslog.result
@@ -0,0 +1 @@
+INFO  [main] 2019-12-17 03:03:37,526 Server.java:156 - Starting listening for CQL clients on localhost/127.0.0.1:9042 (unencrypted)...
diff --git a/doc/modules/cassandra/examples/RESULTS/verify_gpg.result b/doc/modules/cassandra/examples/RESULTS/verify_gpg.result
new file mode 100644
index 0000000..da62736
--- /dev/null
+++ b/doc/modules/cassandra/examples/RESULTS/verify_gpg.result
@@ -0,0 +1,2 @@
+apache-cassandra-3.11.10-bin.tar.gz: 28757DDE 589F7041 0F9A6A95 C39EE7E6
+                                   CDE63440 E2B06B91 AE6B2006 14FA364D
diff --git a/doc/modules/cassandra/examples/TEXT/tarball_install_dirs.txt b/doc/modules/cassandra/examples/TEXT/tarball_install_dirs.txt
new file mode 100644
index 0000000..99b1a14
--- /dev/null
+++ b/doc/modules/cassandra/examples/TEXT/tarball_install_dirs.txt
@@ -0,0 +1,11 @@
+<tarball_installation>/
+    bin/		<1>
+    conf/		<2>
+    data/		<3>
+    doc/
+    interface/
+    javadoc/
+    lib/
+    logs/		<4>
+    pylib/
+    tools/		<5>
diff --git a/doc/modules/cassandra/examples/YAML/auto_snapshot.yaml b/doc/modules/cassandra/examples/YAML/auto_snapshot.yaml
new file mode 100644
index 0000000..8f5033d
--- /dev/null
+++ b/doc/modules/cassandra/examples/YAML/auto_snapshot.yaml
@@ -0,0 +1 @@
+auto_snapshot: false
diff --git a/doc/modules/cassandra/examples/YAML/incremental_bups.yaml b/doc/modules/cassandra/examples/YAML/incremental_bups.yaml
new file mode 100644
index 0000000..95fccdb
--- /dev/null
+++ b/doc/modules/cassandra/examples/YAML/incremental_bups.yaml
@@ -0,0 +1 @@
+incremental_backups: true
diff --git a/doc/modules/cassandra/examples/YAML/snapshot_before_compaction.yaml b/doc/modules/cassandra/examples/YAML/snapshot_before_compaction.yaml
new file mode 100644
index 0000000..4ee1b17
--- /dev/null
+++ b/doc/modules/cassandra/examples/YAML/snapshot_before_compaction.yaml
@@ -0,0 +1 @@
+snapshot_before_compaction: false
diff --git a/doc/modules/cassandra/examples/YAML/stress-example.yaml b/doc/modules/cassandra/examples/YAML/stress-example.yaml
new file mode 100644
index 0000000..4a67102
--- /dev/null
+++ b/doc/modules/cassandra/examples/YAML/stress-example.yaml
@@ -0,0 +1,62 @@
+#
+# 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.
+#
+
+spacenam: example # idenitifier for this spec if running with multiple yaml files
+keyspace: example
+
+# Would almost always be network topology unless running something locally
+keyspace_definition: |
+  CREATE KEYSPACE example WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};
+
+table: staff_activities
+
+# The table under test. Start with a partition per staff member
+# Is this a good idea?
+table_definition: |
+  CREATE TABLE staff_activities (
+        name text,
+        when timeuuid,
+        what text,
+        PRIMARY KEY(name, when)
+  ) 
+
+columnspec:
+  - name: name
+    size: uniform(5..10) # The names of the staff members are between 5-10 characters
+    population: uniform(1..10) # 10 possible staff members to pick from 
+  - name: when
+    cluster: uniform(20..500) # Staff members do between 20 and 500 events
+  - name: what
+    size: normal(10..100,50)
+
+insert:
+  # we only update a single partition in any given insert 
+  partitions: fixed(1) 
+  # we want to insert a single row per partition and we have between 20 and 500
+  # rows per partition
+  select: fixed(1)/500 
+  batchtype: UNLOGGED             # Single partition unlogged batches are essentially noops
+
+queries:
+   events:
+      cql: select *  from staff_activities where name = ?
+      fields: samerow
+   latest_event:
+      cql: select * from staff_activities where name = ?  LIMIT 1
+      fields: samerow
+
diff --git a/doc/modules/cassandra/examples/YAML/stress-lwt-example.yaml b/doc/modules/cassandra/examples/YAML/stress-lwt-example.yaml
new file mode 100644
index 0000000..1f12c24
--- /dev/null
+++ b/doc/modules/cassandra/examples/YAML/stress-lwt-example.yaml
@@ -0,0 +1,88 @@
+#
+# 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.
+#
+
+# Keyspace Name
+keyspace: stresscql
+
+# The CQL for creating a keyspace (optional if it already exists)
+# Would almost always be network topology unless running something locall
+keyspace_definition: |
+  CREATE KEYSPACE stresscql WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
+
+# Table name
+table: blogposts
+
+# The CQL for creating a table you wish to stress (optional if it already exists)
+table_definition: |
+  CREATE TABLE blogposts (
+        domain text,
+        published_date timeuuid,
+        url text,
+        author text,
+        title text,
+        body text,
+        PRIMARY KEY(domain, published_date)
+  ) WITH CLUSTERING ORDER BY (published_date DESC) 
+    AND compaction = { 'class':'LeveledCompactionStrategy' } 
+    AND comment='A table to hold blog posts'
+
+### Column Distribution Specifications ###
+ 
+columnspec:
+  - name: domain
+    size: gaussian(5..100)       #domain names are relatively short
+    population: uniform(1..10M)  #10M possible domains to pick from
+
+  - name: published_date
+    cluster: fixed(1000)         #under each domain we will have max 1000 posts
+
+  - name: url
+    size: uniform(30..300)       
+
+  - name: title                  #titles shouldn't go beyond 200 chars
+    size: gaussian(10..200)
+
+  - name: author
+    size: uniform(5..20)         #author names should be short
+
+  - name: body
+    size: gaussian(100..5000)    #the body of the blog post can be long
+   
+### Batch Ratio Distribution Specifications ###
+
+insert:
+  partitions: fixed(1)            # Our partition key is the domain so only insert one per batch
+
+  select:    fixed(1)/1000        # We have 1000 posts per domain so 1/1000 will allow 1 post per batch
+
+  batchtype: UNLOGGED             # Unlogged batches
+
+
+#
+# A list of queries you wish to run against the schema
+#
+queries:
+   singlepost:
+      cql: select * from blogposts where domain = ? LIMIT 1
+      fields: samerow
+   regularupdate:
+      cql: update blogposts set author = ? where domain = ? and published_date = ?
+      fields: samerow
+   updatewithlwt:
+      cql: update blogposts set author = ? where domain = ? and published_date = ? IF body = ? AND url = ?
+      fields: samerow
diff --git a/doc/modules/cassandra/nav.adoc b/doc/modules/cassandra/nav.adoc
new file mode 100644
index 0000000..cb85393
--- /dev/null
+++ b/doc/modules/cassandra/nav.adoc
@@ -0,0 +1,96 @@
+* Cassandra
+** xref:getting_started/index.adoc[Getting Started]
+*** xref:getting_started/installing.adoc[Installing Cassandra]
+*** xref:getting_started/configuring.adoc[Configuring Cassandra]
+*** xref:getting_started/querying.adoc[Inserting and querying]
+*** xref:getting_started/drivers.adoc[Client drivers]
+*** xref:getting_started/production.adoc[Production recommendations]
+
+** xref:architecture/index.adoc[Architecture]
+*** xref:architecture/overview.adoc[Overview]
+*** xref:architecture/dynamo.adoc[Dynamo]
+*** xref:architecture/storage_engine.adoc[Storage engine]
+*** xref:architecture/guarantees.adoc[Guarantees]
+
+** xref:data_modeling/index.adoc[Data modeling]
+*** xref:data_modeling/intro.adoc[Introduction]
+*** xref:data_modeling/data_modeling_conceptual.adoc[Conceptual data modeling]
+*** xref:data_modeling/data_modeling_rdbms.adoc[RDBMS design]
+*** xref:data_modeling/data_modeling_queries.adoc[Defining application queries]
+*** xref:data_modeling/data_modeling_logical.adoc[Logical data modeling]
+*** xref:data_modeling/data_modeling_physical.adoc[Physical data modeling]
+*** xref:data_modeling/data_modeling_refining.adoc[Evaluating and refining data models]
+*** xref:data_modeling/data_modeling_schema.adoc[Defining database schema]
+*** xref:data_modeling/data_modeling_tools.adoc[Cassandra data modeling tools]
+
+** xref:cql/index.adoc[Cassandra Query Language (CQL)]
+*** xref:cql/definitions.adoc[Definitions]
+*** xref:cql/types.adoc[Data types]
+*** xref:cql/ddl.adoc[Data definition (DDL)]
+*** xref:cql/dml.adoc[Data manipulation (DML)]
+*** xref:cql/operators.adoc[Operators]
+*** xref:cql/indexes.adoc[Secondary indexes]
+*** xref:cql/mvs.adoc[Materialized views]
+*** xref:cql/functions.adoc[Functions]
+*** xref:cql/json.adoc[JSON]
+*** xref:cql/security.adoc[Security]
+*** xref:cql/triggers.adoc[Triggers]
+*** xref:cql/appendices.adoc[Appendices]
+*** xref:cql/changes.adoc[Changes]
+*** xref:cql/SASI.adoc[SASI]
+*** xref:cql/cql_singlefile.adoc[Single file of CQL information]
+
+** xref:configuration/index.adoc[Configuration]
+*** xref:configuration/cass_yaml_file.adoc[cassandra.yaml]
+*** xref:configuration/cass_rackdc_file.adoc[cassandra-rackdc.properties]
+*** xref:configuration/cass_env_sh_file.adoc[cassandra-env.sh]
+*** xref:configuration/cass_topo_file.adoc[cassandra-topologies.properties]
+*** xref:configuration/cass_cl_archive_file.adoc[commitlog-archiving.properties]
+*** xref:configuration/cass_logback_xml_file.adoc[logback.xml]
+*** xref:configuration/cass_jvm_options_file.adoc[jvm-* files]
+
+** xref:operating/index.adoc[Operating]
+*** xref:operating/snitch.adoc[Snitches]
+*** xref:operating/topo_changes.adoc[Topology changes]
+*** xref:operating/repair.adoc[Repair]
+*** xref:operating/hints.adoc[Hints]
+*** xref:operating/bloom_filters.adoc[Bloom filters]
+*** xref:operating/compression.adoc[Compression]
+*** xref:operating/cdc.adoc[Change Data Capture (CDC)]
+*** xref:operating/backups.adoc[Backups]
+*** xref:operating/bulk_loading.adoc[Bulk loading]
+*** xref:operating/metrics.adoc[Metrics]
+*** xref:operating/security.adoc[Security]
+*** xref:operating/hardware.adoc[Hardware]
+*** xref:operating/audit_logging.adoc[Audit logging]
+*** xref:operating/compaction/index.adoc[Compaction]
+
+** xref:tools/index.adoc[Tools]
+*** xref:tools/cqlsh.adoc[cqlsh: the CQL shell]
+*** xref:tools/nodetool/nodetool.adoc[nodetool]
+*** xref:tools/sstable/index.adoc[SSTable tools]
+*** xref:tools/cassandra_stress.adoc[cassandra-stress]
+
+** xref:troubleshooting/index.adoc[Troubleshooting]
+*** xref:troubleshooting/finding_nodes.adoc[Finding misbehaving nodes]
+*** xref:troubleshooting/reading_logs.adoc[Reading Cassandra logs]
+*** xref:troubleshooting/use_nodetool.adoc[Using nodetool]
+*** xref:troubleshooting/use_tools.adoc[Using external tools to deep-dive]
+
+** xref:master@_:ROOT:development/index.adoc[Development]
+*** xref:master@_:ROOT:development/gettingstarted.adoc[Getting started]
+*** xref:master@_:ROOT:development/ide.adoc[Building and IDE integration]
+*** xref:master@_:ROOT:development/testing.adoc[Testing]
+*** xref:master@_:ROOT:development/patches.adoc[Contributing code changes]
+*** xref:master@_:ROOT:development/code_style.adoc[Code style]
+*** xref:master@_:ROOT:development/how_to_review.adoc[Review checklist]
+*** xref:master@_:ROOT:development/how_to_commit.adoc[How to commit]
+*** xref:master@_:ROOT:development/documentation.adoc[Working on documentation]
+*** xref:master@_:ROOT:development/ci.adoc[Jenkins CI environment]
+*** xref:master@_:ROOT:development/dependencies.adoc[Dependency management]
+*** xref:master@_:ROOT:development/release_process.adoc[Release process]
+
+** xref:faq/index.adoc[FAQ]
+
+** xref:plugins/index.adoc[Plug-ins]
+
diff --git a/doc/modules/cassandra/pages/architecture/dynamo.adoc b/doc/modules/cassandra/pages/architecture/dynamo.adoc
new file mode 100644
index 0000000..e90390a
--- /dev/null
+++ b/doc/modules/cassandra/pages/architecture/dynamo.adoc
@@ -0,0 +1,531 @@
+= Dynamo
+
+Apache Cassandra relies on a number of techniques from Amazon's
+http://courses.cse.tamu.edu/caverlee/csce438/readings/dynamo-paper.pdf[Dynamo]
+distributed storage key-value system. Each node in the Dynamo system has
+three main components:
+
+* Request coordination over a partitioned dataset
+* Ring membership and failure detection
+* A local persistence (storage) engine
+
+Cassandra primarily draws from the first two clustering components,
+while using a storage engine based on a Log Structured Merge Tree
+(http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.44.2782&rep=rep1&type=pdf[LSM]).
+In particular, Cassandra relies on Dynamo style:
+
+* Dataset partitioning using consistent hashing
+* Multi-master replication using versioned data and tunable consistency
+* Distributed cluster membership and failure detection via a gossip
+protocol
+* Incremental scale-out on commodity hardware
+
+Cassandra was designed this way to meet large-scale (PiB+)
+business-critical storage requirements. In particular, as applications
+demanded full global replication of petabyte scale datasets along with
+always available low-latency reads and writes, it became imperative to
+design a new kind of database model as the relational database systems
+of the time struggled to meet the new requirements of global scale
+applications.
+
+== Dataset Partitioning: Consistent Hashing
+
+Cassandra achieves horizontal scalability by
+https://en.wikipedia.org/wiki/Partition_(database)[partitioning] all
+data stored in the system using a hash function. Each partition is
+replicated to multiple physical nodes, often across failure domains such
+as racks and even datacenters. As every replica can independently accept
+mutations to every key that it owns, every key must be versioned. Unlike
+in the original Dynamo paper where deterministic versions and vector
+clocks were used to reconcile concurrent updates to a key, Cassandra
+uses a simpler last write wins model where every mutation is timestamped
+(including deletes) and then the latest version of data is the "winning"
+value. Formally speaking, Cassandra uses a Last-Write-Wins Element-Set
+conflict-free replicated data type for each CQL row, or 
+https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type LWW-Element-Set_(Last-Write-Wins-Element-Set)[LWW-Element-Set
+CRDT], to resolve conflicting mutations on replica sets.
+
+=== Consistent Hashing using a Token Ring
+
+Cassandra partitions data over storage nodes using a special form of
+hashing called
+https://en.wikipedia.org/wiki/Consistent_hashing[consistent hashing]. In
+naive data hashing, you typically allocate keys to buckets by taking a
+hash of the key modulo the number of buckets. For example, if you want
+to distribute data to 100 nodes using naive hashing you might assign
+every node to a bucket between 0 and 100, hash the input key modulo 100,
+and store the data on the associated bucket. In this naive scheme,
+however, adding a single node might invalidate almost all of the
+mappings.
+
+Cassandra instead maps every node to one or more tokens on a continuous
+hash ring, and defines ownership by hashing a key onto the ring and then
+"walking" the ring in one direction, similar to the
+https://pdos.csail.mit.edu/papers/chord:sigcomm01/chord_sigcomm.pdf[Chord]
+algorithm. The main difference of consistent hashing to naive data
+hashing is that when the number of nodes (buckets) to hash into changes,
+consistent hashing only has to move a small fraction of the keys.
+
+For example, if we have an eight node cluster with evenly spaced tokens,
+and a replication factor (RF) of 3, then to find the owning nodes for a
+key we first hash that key to generate a token (which is just the hash
+of the key), and then we "walk" the ring in a clockwise fashion until we
+encounter three distinct nodes, at which point we have found all the
+replicas of that key. This example of an eight node cluster with
+gRF=3 can be visualized as follows:
+
+image::ring.svg[image]
+
+You can see that in a Dynamo like system, ranges of keys, also known as
+*token ranges*, map to the same physical set of nodes. In this example,
+all keys that fall in the token range excluding token 1 and including
+token 2 (grange(t1, t2]) are stored on nodes 2, 3 and 4.
+
+=== Multiple Tokens per Physical Node (vnodes)
+
+Simple single token consistent hashing works well if you have many
+physical nodes to spread data over, but with evenly spaced tokens and a
+small number of physical nodes, incremental scaling (adding just a few
+nodes of capacity) is difficult because there are no token selections
+for new nodes that can leave the ring balanced. Cassandra seeks to avoid
+token imbalance because uneven token ranges lead to uneven request load.
+For example, in the previous example there is no way to add a ninth
+token without causing imbalance; instead we would have to insert `8`
+tokens in the midpoints of the existing ranges.
+
+The Dynamo paper advocates for the use of "virtual nodes" to solve this
+imbalance problem. Virtual nodes solve the problem by assigning multiple
+tokens in the token ring to each physical node. By allowing a single
+physical node to take multiple positions in the ring, we can make small
+clusters look larger and therefore even with a single physical node
+addition we can make it look like we added many more nodes, effectively
+taking many smaller pieces of data from more ring neighbors when we add
+even a single node.
+
+Cassandra introduces some nomenclature to handle these concepts:
+
+* *Token*: A single position on the dynamo style hash
+ring.
+* *Endpoint*: A single physical IP and port on the network.
+* *Host ID*: A unique identifier for a single "physical" node, usually
+present at one gEndpoint and containing one or more
+gTokens.
+* *Virtual Node* (or *vnode*): A gToken on the hash ring
+owned by the same physical node, one with the same gHost
+ID.
+
+The mapping of *Tokens* to *Endpoints* gives rise to the *Token Map*
+where Cassandra keeps track of what ring positions map to which physical
+endpoints. For example, in the following figure we can represent an
+eight node cluster using only four physical nodes by assigning two
+tokens to every node:
+
+image::vnodes.svg[image]
+
+Multiple tokens per physical node provide the following benefits:
+
+[arabic]
+. When a new node is added it accepts approximately equal amounts of
+data from other nodes in the ring, resulting in equal distribution of
+data across the cluster.
+. When a node is decommissioned, it loses data roughly equally to other
+members of the ring, again keeping equal distribution of data across the
+cluster.
+. If a node becomes unavailable, query load (especially token aware
+query load), is evenly distributed across many other nodes.
+
+Multiple tokens, however, can also have disadvantages:
+
+[arabic]
+. Every token introduces up to `2 * (RF - 1)` additional neighbors on
+the token ring, which means that there are more combinations of node
+failures where we lose availability for a portion of the token ring. The
+more tokens you have,
+https://jolynch.github.io/pdf/cassandra-availability-virtual.pdf[the
+higher the probability of an outage].
+. Cluster-wide maintenance operations are often slowed. For example, as
+the number of tokens per node is increased, the number of discrete
+repair operations the cluster must do also increases.
+. Performance of operations that span token ranges could be affected.
+
+Note that in Cassandra `2.x`, the only token allocation algorithm
+available was picking random tokens, which meant that to keep balance
+the default number of tokens per node had to be quite high, at `256`.
+This had the effect of coupling many physical endpoints together,
+increasing the risk of unavailability. That is why in `3.x +` the new
+deterministic token allocator was added which intelligently picks tokens
+such that the ring is optimally balanced while requiring a much lower
+number of tokens per physical node.
+
+== Multi-master Replication: Versioned Data and Tunable Consistency
+
+Cassandra replicates every partition of data to many nodes across the
+cluster to maintain high availability and durability. When a mutation
+occurs, the coordinator hashes the partition key to determine the token
+range the data belongs to and then replicates the mutation to the
+replicas of that data according to the
+`Replication Strategy`.
+
+All replication strategies have the notion of a *replication factor*
+(`RF`), which indicates to Cassandra how many copies of the partition
+should exist. For example with a `RF=3` keyspace, the data will be
+written to three distinct *replicas*. Replicas are always chosen such
+that they are distinct physical nodes which is achieved by skipping
+virtual nodes if needed. Replication strategies may also choose to skip
+nodes present in the same failure domain such as racks or datacenters so
+that Cassandra clusters can tolerate failures of whole racks and even
+datacenters of nodes.
+
+=== Replication Strategy
+
+Cassandra supports pluggable *replication strategies*, which determine
+which physical nodes act as replicas for a given token range. Every
+keyspace of data has its own replication strategy. All production
+deployments should use the `NetworkTopologyStrategy` while the
+`SimpleStrategy` replication strategy is useful only for testing
+clusters where you do not yet know the datacenter layout of the cluster.
+
+[[network-topology-strategy]]
+==== `NetworkTopologyStrategy`
+
+`NetworkTopologyStrategy` requires a specified replication factor 
+for each datacenter in the cluster. Even if your cluster only uses a
+single datacenter, `NetworkTopologyStrategy` is recommended over
+`SimpleStrategy` to make it easier to add new physical or virtual
+datacenters to the cluster later, if required.
+
+In addition to allowing the replication factor to be specified
+individually by datacenter, `NetworkTopologyStrategy` also attempts to
+choose replicas within a datacenter from different racks as specified by
+the `Snitch`. If the number of racks is greater than or equal
+to the replication factor for the datacenter, each replica is guaranteed
+to be chosen from a different rack. Otherwise, each rack will hold at
+least one replica, but some racks may hold more than one. Note that this
+rack-aware behavior has some potentially
+https://issues.apache.org/jira/browse/CASSANDRA-3810[surprising
+implications]. For example, if there are not an even number of nodes in
+each rack, the data load on the smallest rack may be much higher.
+Similarly, if a single node is bootstrapped into a brand new rack, it
+will be considered a replica for the entire ring. For this reason, many
+operators choose to configure all nodes in a single availability zone or
+similar failure domain as a single "rack".
+
+[[simple-strategy]]
+==== `SimpleStrategy`
+
+`SimpleStrategy` allows a single integer `replication_factor` to be
+defined. This determines the number of nodes that should contain a copy
+of each row. For example, if `replication_factor` is 3, then three
+different nodes should store a copy of each row.
+
+`SimpleStrategy` treats all nodes identically, ignoring any configured
+datacenters or racks. To determine the replicas for a token range,
+Cassandra iterates through the tokens in the ring, starting with the
+token range of interest. For each token, it checks whether the owning
+node has been added to the set of replicas, and if it has not, it is
+added to the set. This process continues until `replication_factor`
+distinct nodes have been added to the set of replicas.
+
+==== Transient Replication
+
+Transient replication is an experimental feature in Cassandra {40_version} not
+present in the original Dynamo paper. This feature allows configuration of a
+subset of replicas to replicate only data that hasn't been incrementally
+repaired. This configuration decouples data redundancy from availability.
+For instance, if you have a keyspace replicated at RF=3, and alter it to
+RF=5 with two transient replicas, you go from tolerating one
+failed replica to tolerating two, without corresponding
+increase in storage usage. Now, three nodes will replicate all
+the data for a given token range, and the other two will only replicate
+data that hasn't been incrementally repaired.
+
+To use transient replication, first enable the option in
+`cassandra.yaml`. Once enabled, both `SimpleStrategy` and
+`NetworkTopologyStrategy` can be configured to transiently replicate
+data. Configure it by specifying replication factor as
+`<total_replicas>/<transient_replicas` Both `SimpleStrategy` and
+`NetworkTopologyStrategy` support configuring transient replication.
+
+Transiently replicated keyspaces only support tables created with
+`read_repair` set to `NONE`; monotonic reads are not currently
+supported. You also can't use `LWT`, logged batches, or counters in {40_version}.
+You will possibly never be able to use materialized views with
+transiently replicated keyspaces and probably never be able to use
+secondary indices with them.
+
+Transient replication is an experimental feature that is not ready
+for production use. The expected audience is experienced users of
+Cassandra capable of fully validating a deployment of their particular
+application. That means being able check that operations like reads,
+writes, decommission, remove, rebuild, repair, and replace all work with
+your queries, data, configuration, operational practices, and
+availability requirements.
+
+Anticipated additional features in `4.next` are support for monotonic reads with
+transient replication, as well as LWT, logged batches, and counters.
+
+=== Data Versioning
+
+Cassandra uses mutation timestamp versioning to guarantee eventual
+consistency of data. Specifically all mutations that enter the system do
+so with a timestamp provided either from a client clock or, absent a
+client provided timestamp, from the coordinator node's clock. Updates
+resolve according to the conflict resolution rule of last write wins.
+Cassandra's correctness does depend on these clocks, so make sure a
+proper time synchronization process is running such as NTP.
+
+Cassandra applies separate mutation timestamps to every column of every
+row within a CQL partition. Rows are guaranteed to be unique by primary
+key, and each column in a row resolve concurrent mutations according to
+last-write-wins conflict resolution. This means that updates to
+different primary keys within a partition can actually resolve without
+conflict! Furthermore the CQL collection types such as maps and sets use
+this same conflict free mechanism, meaning that concurrent updates to
+maps and sets are guaranteed to resolve as well.
+
+==== Replica Synchronization
+
+As replicas in Cassandra can accept mutations independently, it is
+possible for some replicas to have newer data than others. Cassandra has
+many best-effort techniques to drive convergence of replicas including
+`Replica read repair <read-repair>` in the read path and
+`Hinted handoff <hints>` in the write path.
+
+These techniques are only best-effort, however, and to guarantee
+eventual consistency Cassandra implements `anti-entropy
+repair <repair>` where replicas calculate hierarchical hash-trees over
+their datasets called https://en.wikipedia.org/wiki/Merkle_tree[Merkle
+trees] that can then be compared across replicas to identify mismatched
+data. Like the original Dynamo paper Cassandra supports full repairs
+where replicas hash their entire dataset, create Merkle trees, send them
+to each other and sync any ranges that don't match.
+
+Unlike the original Dynamo paper, Cassandra also implements sub-range
+repair and incremental repair. Sub-range repair allows Cassandra to
+increase the resolution of the hash trees (potentially down to the
+single partition level) by creating a larger number of trees that span
+only a portion of the data range. Incremental repair allows Cassandra to
+only repair the partitions that have changed since the last repair.
+
+=== Tunable Consistency
+
+Cassandra supports a per-operation tradeoff between consistency and
+availability through *Consistency Levels*. Cassandra's consistency
+levels are a version of Dynamo's `R + W > N` consistency mechanism where
+operators could configure the number of nodes that must participate in
+reads (`R`) and writes (`W`) to be larger than the replication factor
+(`N`). In Cassandra, you instead choose from a menu of common
+consistency levels which allow the operator to pick `R` and `W` behavior
+without knowing the replication factor. Generally writes will be visible
+to subsequent reads when the read consistency level contains enough
+nodes to guarantee a quorum intersection with the write consistency
+level.
+
+The following consistency levels are available:
+
+`ONE`::
+  Only a single replica must respond.
+`TWO`::
+  Two replicas must respond.
+`THREE`::
+  Three replicas must respond.
+`QUORUM`::
+  A majority (n/2 + 1) of the replicas must respond.
+`ALL`::
+  All of the replicas must respond.
+`LOCAL_QUORUM`::
+  A majority of the replicas in the local datacenter (whichever
+  datacenter the coordinator is in) must respond.
+`EACH_QUORUM`::
+  A majority of the replicas in each datacenter must respond.
+`LOCAL_ONE`::
+  Only a single replica must respond. In a multi-datacenter cluster,
+  this also gaurantees that read requests are not sent to replicas in a
+  remote datacenter.
+`ANY`::
+  A single replica may respond, or the coordinator may store a hint. If
+  a hint is stored, the coordinator will later attempt to replay the
+  hint and deliver the mutation to the replicas. This consistency level
+  is only accepted for write operations.
+
+Write operations *are always sent to all replicas*, regardless of
+consistency level. The consistency level simply controls how many
+responses the coordinator waits for before responding to the client.
+
+For read operations, the coordinator generally only issues read commands
+to enough replicas to satisfy the consistency level. The one exception
+to this is when speculative retry may issue a redundant read request to
+an extra replica if the original replicas have not responded within a
+specified time window.
+
+==== Picking Consistency Levels
+
+It is common to pick read and write consistency levels such that the
+replica sets overlap, resulting in all acknowledged writes being visible
+to subsequent reads. This is typically expressed in the same terms
+Dynamo does, in that `W + R > RF`, where `W` is the write consistency
+level, `R` is the read consistency level, and `RF` is the replication
+factor. For example, if `RF = 3`, a `QUORUM` request will require
+responses from at least `2/3` replicas. If `QUORUM` is used for both
+writes and reads, at least one of the replicas is guaranteed to
+participate in _both_ the write and the read request, which in turn
+guarantees that the quorums will overlap and the write will be visible
+to the read.
+
+In a multi-datacenter environment, `LOCAL_QUORUM` can be used to provide
+a weaker but still useful guarantee: reads are guaranteed to see the
+latest write from within the same datacenter. This is often sufficient
+as clients homed to a single datacenter will read their own writes.
+
+If this type of strong consistency isn't required, lower consistency
+levels like `LOCAL_ONE` or `ONE` may be used to improve throughput,
+latency, and availability. With replication spanning multiple
+datacenters, `LOCAL_ONE` is typically less available than `ONE` but is
+faster as a rule. Indeed `ONE` will succeed if a single replica is
+available in any datacenter.
+
+== Distributed Cluster Membership and Failure Detection
+
+The replication protocols and dataset partitioning rely on knowing which
+nodes are alive and dead in the cluster so that write and read
+operations can be optimally routed. In Cassandra liveness information is
+shared in a distributed fashion through a failure detection mechanism
+based on a gossip protocol.
+
+=== Gossip
+
+Gossip is how Cassandra propagates basic cluster bootstrapping
+information such as endpoint membership and internode network protocol
+versions. In Cassandra's gossip system, nodes exchange state information
+not only about themselves but also about other nodes they know about.
+This information is versioned with a vector clock of
+`(generation, version)` tuples, where the generation is a monotonic
+timestamp and version is a logical clock the increments roughly every
+second. These logical clocks allow Cassandra gossip to ignore old
+versions of cluster state just by inspecting the logical clocks
+presented with gossip messages.
+
+Every node in the Cassandra cluster runs the gossip task independently
+and periodically. Every second, every node in the cluster:
+
+[arabic]
+. Updates the local node's heartbeat state (the version) and constructs
+the node's local view of the cluster gossip endpoint state.
+. Picks a random other node in the cluster to exchange gossip endpoint
+state with.
+. Probabilistically attempts to gossip with any unreachable nodes (if
+one exists)
+. Gossips with a seed node if that didn't happen in step 2.
+
+When an operator first bootstraps a Cassandra cluster they designate
+certain nodes as seed nodes. Any node can be a seed node and the only
+difference between seed and non-seed nodes is seed nodes are allowed to
+bootstrap into the ring without seeing any other seed nodes.
+Furthermore, once a cluster is bootstrapped, seed nodes become
+hotspots for gossip due to step 4 above.
+
+As non-seed nodes must be able to contact at least one seed node in
+order to bootstrap into the cluster, it is common to include multiple
+seed nodes, often one for each rack or datacenter. Seed nodes are often
+chosen using existing off-the-shelf service discovery mechanisms.
+
+[NOTE]
+.Note
+====
+Nodes do not have to agree on the seed nodes, and indeed once a cluster
+is bootstrapped, newly launched nodes can be configured to use any
+existing nodes as seeds. The only advantage to picking the same nodes
+as seeds is it increases their usefullness as gossip hotspots.
+====
+
+Currently, gossip also propagates token metadata and schema
+_version_ information. This information forms the control plane for
+scheduling data movements and schema pulls. For example, if a node sees
+a mismatch in schema version in gossip state, it will schedule a schema
+sync task with the other nodes. As token information propagates via
+gossip it is also the control plane for teaching nodes which endpoints
+own what data.
+
+=== Ring Membership and Failure Detection
+
+Gossip forms the basis of ring membership, but the *failure detector*
+ultimately makes decisions about if nodes are `UP` or `DOWN`. Every node
+in Cassandra runs a variant of the
+https://www.computer.org/csdl/proceedings-article/srds/2004/22390066/12OmNvT2phv[Phi
+Accrual Failure Detector], in which every node is constantly making an
+independent decision of if their peer nodes are available or not. This
+decision is primarily based on received heartbeat state. For example, if
+a node does not see an increasing heartbeat from a node for a certain
+amount of time, the failure detector "convicts" that node, at which
+point Cassandra will stop routing reads to it (writes will typically be
+written to hints). If/when the node starts heartbeating again, Cassandra
+will try to reach out and connect, and if it can open communication
+channels it will mark that node as available.
+
+[NOTE]
+.Note
+====
+`UP` and `DOWN` state are local node decisions and are not propagated with
+gossip. Heartbeat state is propagated with gossip, but nodes will not
+consider each other as `UP` until they can successfully message each
+other over an actual network channel.
+====
+
+Cassandra will never remove a node from gossip state without
+explicit instruction from an operator via a decommission operation or a
+new node bootstrapping with a `replace_address_first_boot` option. This
+choice is intentional to allow Cassandra nodes to temporarily fail
+without causing data to needlessly re-balance. This also helps to
+prevent simultaneous range movements, where multiple replicas of a token
+range are moving at the same time, which can violate monotonic
+consistency and can even cause data loss.
+
+== Incremental Scale-out on Commodity Hardware
+
+Cassandra scales-out to meet the requirements of growth in data size and
+request rates. Scaling-out means adding additional nodes to the ring,
+and every additional node brings linear improvements in compute and
+storage. In contrast, scaling-up implies adding more capacity to the
+existing database nodes. Cassandra is also capable of scale-up, and in
+certain environments it may be preferable depending on the deployment.
+Cassandra gives operators the flexibility to chose either scale-out or
+scale-up.
+
+One key aspect of Dynamo that Cassandra follows is to attempt to run on
+commodity hardware, and many engineering choices are made under this
+assumption. For example, Cassandra assumes nodes can fail at any time,
+auto-tunes to make the best use of CPU and memory resources available
+and makes heavy use of advanced compression and caching techniques to
+get the most storage out of limited memory and storage capabilities.
+
+=== Simple Query Model
+
+Cassandra, like Dynamo, chooses not to provide cross-partition
+transactions that are common in SQL Relational Database Management
+Systems (RDBMS). This both gives the programmer a simpler read and write
+API, and allows Cassandra to more easily scale horizontally since
+multi-partition transactions spanning multiple nodes are notoriously
+difficult to implement and typically very latent.
+
+Instead, Cassanda chooses to offer fast, consistent, latency at any
+scale for single partition operations, allowing retrieval of entire
+partitions or only subsets of partitions based on primary key filters.
+Furthermore, Cassandra does support single partition compare and swap
+functionality via the lightweight transaction CQL API.
+
+=== Simple Interface for Storing Records
+
+Cassandra, in a slight departure from Dynamo, chooses a storage
+interface that is more sophisticated then "simple key value" stores but
+significantly less complex than SQL relational data models. Cassandra
+presents a wide-column store interface, where partitions of data contain
+multiple rows, each of which contains a flexible set of individually
+typed columns. Every row is uniquely identified by the partition key and
+one or more clustering keys, and every row can have as many columns as
+needed.
+
+This allows users to flexibly add new columns to existing datasets as
+new requirements surface. Schema changes involve only metadata changes
+and run fully concurrently with live workloads. Therefore, users can
+safely add columns to existing Cassandra databases while remaining
+confident that query performance will not degrade.
diff --git a/doc/modules/cassandra/pages/architecture/guarantees.adoc b/doc/modules/cassandra/pages/architecture/guarantees.adoc
new file mode 100644
index 0000000..3313a11
--- /dev/null
+++ b/doc/modules/cassandra/pages/architecture/guarantees.adoc
@@ -0,0 +1,108 @@
+= Guarantees
+
+Apache Cassandra is a highly scalable and reliable database. Cassandra
+is used in web based applications that serve large number of clients and
+the quantity of data processed is web-scale (Petabyte) large. Cassandra
+makes some guarantees about its scalability, availability and
+reliability. To fully understand the inherent limitations of a storage
+system in an environment in which a certain level of network partition
+failure is to be expected and taken into account when designing the
+system it is important to first briefly introduce the CAP theorem.
+
+== What is CAP?
+
+According to the CAP theorem it is not possible for a distributed data
+store to provide more than two of the following guarantees
+simultaneously.
+
+* Consistency: Consistency implies that every read receives the most
+recent write or errors out
+* Availability: Availability implies that every request receives a
+response. It is not guaranteed that the response contains the most
+recent write or data.
+* Partition tolerance: Partition tolerance refers to the tolerance of a
+storage system to failure of a network partition. Even if some of the
+messages are dropped or delayed the system continues to operate.
+
+CAP theorem implies that when using a network partition, with the
+inherent risk of partition failure, one has to choose between
+consistency and availability and both cannot be guaranteed at the same
+time. CAP theorem is illustrated in Figure 1.
+
+image::Figure_1_guarantees.jpg[image]
+
+Figure 1. CAP Theorem
+
+High availability is a priority in web based applications and to this
+objective Cassandra chooses Availability and Partition Tolerance from
+the CAP guarantees, compromising on data Consistency to some extent.
+
+Cassandra makes the following guarantees.
+
+* High Scalability
+* High Availability
+* Durability
+* Eventual Consistency of writes to a single table
+* Lightweight transactions with linearizable consistency
+* Batched writes across multiple tables are guaranteed to succeed
+completely or not at all
+* Secondary indexes are guaranteed to be consistent with their local
+replicas data
+
+== High Scalability
+
+Cassandra is a highly scalable storage system in which nodes may be
+added/removed as needed. Using gossip-based protocol a unified and
+consistent membership list is kept at each node.
+
+== High Availability
+
+Cassandra guarantees high availability of data by implementing a
+fault-tolerant storage system. Failure detection in a node is detected
+using a gossip-based protocol.
+
+== Durability
+
+Cassandra guarantees data durability by using replicas. Replicas are
+multiple copies of a data stored on different nodes in a cluster. In a
+multi-datacenter environment the replicas may be stored on different
+datacenters. If one replica is lost due to unrecoverable node/datacenter
+failure the data is not completely lost as replicas are still available.
+
+== Eventual Consistency
+
+Meeting the requirements of performance, reliability, scalability and
+high availability in production Cassandra is an eventually consistent
+storage system. Eventually consistent implies that all updates reach all
+replicas eventually. Divergent versions of the same data may exist
+temporarily but they are eventually reconciled to a consistent state.
+Eventual consistency is a tradeoff to achieve high availability and it
+involves some read and write latencies.
+
+== Lightweight transactions with linearizable consistency
+
+Data must be read and written in a sequential order. Paxos consensus
+protocol is used to implement lightweight transactions. Paxos protocol
+implements lightweight transactions that are able to handle concurrent
+operations using linearizable consistency. Linearizable consistency is
+sequential consistency with real-time constraints and it ensures
+transaction isolation with compare and set (CAS) transaction. With CAS
+replica data is compared and data that is found to be out of date is set
+to the most consistent value. Reads with linearizable consistency allow
+reading the current state of the data, which may possibly be
+uncommitted, without making a new addition or update.
+
+== Batched Writes
+
+The guarantee for batched writes across multiple tables is that they
+will eventually succeed, or none will. Batch data is first written to
+batchlog system data, and when the batch data has been successfully
+stored in the cluster the batchlog data is removed. The batch is
+replicated to another node to ensure the full batch completes in the
+event the coordinator node fails.
+
+== Secondary Indexes
+
+A secondary index is an index on a column and is used to query a table
+that is normally not queryable. Secondary indexes when built are
+guaranteed to be consistent with their local replicas.
diff --git a/doc/modules/cassandra/pages/architecture/images/ring.svg b/doc/modules/cassandra/pages/architecture/images/ring.svg
new file mode 100644
index 0000000..d0db8c5
--- /dev/null
+++ b/doc/modules/cassandra/pages/architecture/images/ring.svg
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="651" height="709.4583740234375" style="
+        width:651px;
+        height:709.4583740234375px;
+        background: transparent;
+        fill: none;
+">
+        
+        
+        <svg xmlns="http://www.w3.org/2000/svg" class="role-diagram-draw-area"><g class="shapes-region" style="stroke: black; fill: none;"><g class="composite-shape"><path class="real" d=" M223.5,655 C223.5,634.84 239.84,618.5 260,618.5 C280.16,618.5 296.5,634.84 296.5,655 C296.5,675.16 280.16,691.5 260,691.5 C239.84,691.5 223.5,675.16 223.5,655 Z" style="stroke-width: 1; stroke: rgb(103, 148, 135); fill: rgb(103, 148, 135);"/></g><g class="composite-shape"><path class="real" d=" M229.26,655 C229.26,638.02 243.02,624.26 260,624.26 C276.98,624.26 290.74,638.02 290.74,655 C290.74,671.98 276.98,685.74 260,685.74 C243.02,685.74 229.26,671.98 229.26,655 Z" style="stroke-width: 1; stroke: rgb(202, 194, 126); fill: rgb(202, 194, 126);"/></g><g class="composite-shape"><path class="real" d=" M377.5,595 C377.5,571.53 396.53,552.5 420,552.5 C443.47,552.5 462.5,571.53 462.5,595 C462.5,618.47 443.47,637.5 420,637.5 C396.53,637.5 377.5,618.47 377.5,595 Z" style="stroke-width: 1; stroke: rgb(103, 148, 135); fill: rgb(103, 148, 135);"/></g><g class="composite-shape"><path class="real" d=" M384.06,595 C384.06,575.15 400.15,559.06 420,559.06 C439.85,559.06 455.94,575.15 455.94,595 C455.94,614.85 439.85,630.94 420,630.94 C400.15,630.94 384.06,614.85 384.06,595 Z" style="stroke-width: 1; stroke: rgb(202, 194, 126); fill: rgb(202, 194, 126);"/></g><g class="composite-shape"><path class="real" d=" M390,595 C390,578.43 403.43,565 420,565 C436.57,565 450,578.43 450,595 C450,611.57 436.57,625 420,625 C403.43,625 390,611.57 390,595 Z" style="stroke-width: 1; stroke: rgb(130, 192, 233); fill: rgb(130, 192, 233);"/></g><g class="composite-shape"><path class="real" d=" M444.06,435 C444.06,415.15 460.15,399.06 480,399.06 C499.85,399.06 515.94,415.15 515.94,435 C515.94,454.85 499.85,470.94 480,470.94 C460.15,470.94 444.06,454.85 444.06,435 Z" style="stroke-width: 1; stroke: rgb(202, 194, 126); fill: rgb(202, 194, 126);"/></g><g class="composite-shape"><path class="real" d=" M450,435 C450,418.43 463.43,405 480,405 C496.57,405 510,418.43 510,435 C510,451.57 496.57,465 480,465 C463.43,465 450,451.57 450,435 Z" style="stroke-width: 1; stroke: rgb(130, 192, 233); fill: rgb(130, 192, 233);"/></g><g class="composite-shape"><path class="real" d=" M390,275 C390,258.43 403.43,245 420,245 C436.57,245 450,258.43 450,275 C450,291.57 436.57,305 420,305 C403.43,305 390,291.57 390,275 Z" style="stroke-width: 1; stroke: rgb(130, 192, 233); fill: rgb(130, 192, 233);"/></g><g class="composite-shape"><path class="real" d=" M40,435 C40,313.5 138.5,215 260,215 C381.5,215 480,313.5 480,435 C480,556.5 381.5,655 260,655 C138.5,655 40,556.5 40,435 Z" style="stroke-width: 1; stroke: rgba(0, 0, 0, 0.52); fill: none; stroke-dasharray: 1.125, 3.35;"/></g><g class="grouped-shape"><g class="composite-shape"><path class="real" d=" M90,435 C90,341.11 166.11,265 260,265 C353.89,265 430,341.11 430,435 C430,528.89 353.89,605 260,605 C166.11,605 90,528.89 90,435 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: none;"/></g><g class="composite-shape"><path class="real" d=" M111.25,435 C111.25,352.85 177.85,286.25 260,286.25 C342.15,286.25 408.75,352.85 408.75,435 C408.75,517.15 342.15,583.75 260,583.75 C177.85,583.75 111.25,517.15 111.25,435 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: none;"/></g><g class="composite-shape"><path class="real" d=" M132.5,435 C132.5,364.58 189.58,307.5 260,307.5 C330.42,307.5 387.5,364.58 387.5,435 C387.5,505.42 330.42,562.5 260,562.5 C189.58,562.5 132.5,505.42 132.5,435 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: none;"/></g></g><g class="composite-shape"><path class="real" d=" M235,655 C235,641.19 246.19,630 260,630 C273.81,630 285,641.19 285,655 C285,668.81 273.81,680 260,680 C246.19,680 235,668.81 235,655 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="grouped-shape"><g class="composite-shape"><path class="real" d=" M235,215 C235,201.19 246.19,190 260,190 C273.81,190 285,201.19 285,215 C285,228.81 273.81,240 260,240 C246.19,240 235,228.81 235,215 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g></g><g class="composite-shape"><path class="real" d=" M455,435 C455,421.19 466.19,410 480,410 C493.81,410 505,421.19 505,435 C505,448.81 493.81,460 480,460 C466.19,460 455,448.81 455,435 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M15,435 C15,421.19 26.19,410 40,410 C53.81,410 65,421.19 65,435 C65,448.81 53.81,460 40,460 C26.19,460 15,448.81 15,435 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M395,275 C395,261.19 406.19,250 420,250 C433.81,250 445,261.19 445,275 C445,288.81 433.81,300 420,300 C406.19,300 395,288.81 395,275 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M395,595 C395,581.19 406.19,570 420,570 C433.81,570 445,581.19 445,595 C445,608.81 433.81,620 420,620 C406.19,620 395,608.81 395,595 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M75,595 C75,581.19 86.19,570 100,570 C113.81,570 125,581.19 125,595 C125,608.81 113.81,620 100,620 C86.19,620 75,608.81 75,595 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="grouped-shape"><g class="composite-shape"><path class="real" d=" M75,275 C75,261.19 86.19,250 100,250 C113.81,250 125,261.19 125,275 C125,288.81 113.81,300 100,300 C86.19,300 75,288.81 75,275 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M268.22,265.22 C401.65,271.85 490.67,424.28 380,555" style="stroke: rgb(130, 192, 233); stroke-width: 6; fill: none;"/><g stroke="rgba(130,192,233,1)" transform="matrix(0.9999897039488793,0.00453784048117526,-0.00453784048117526,0.9999897039488793,260,265)" style="stroke: rgb(130, 192, 233); stroke-width: 6;"><circle cx="0" cy="0" r="9.045000000000002"/></g><g stroke="rgba(130,192,233,1)" fill="rgba(130,192,233,1)" transform="matrix(-0.6461239796429636,0.763232469782529,-0.763232469782529,-0.6461239796429636,380,555)" style="stroke: rgb(130, 192, 233); fill: rgb(130, 192, 233); stroke-width: 6;"><circle cx="0" cy="0" r="9.045000000000002"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M366.47,330.7 C447.68,407.15 414.54,574.62 260,583.75" style="stroke: rgb(202, 194, 126); stroke-width: 6; fill: none;"/><g stroke="rgba(202,194,126,1)" transform="matrix(0.7722902597301384,0.6352698282824042,-0.6352698282824042,0.7722902597301384,360,325)" style="stroke: rgb(202, 194, 126); stroke-width: 6;"><circle cx="0" cy="0" r="9.045000000000002"/></g><g stroke="rgba(202,194,126,1)" fill="rgba(202,194,126,1)" transform="matrix(-0.9982604689368237,0.05895791853545039,-0.05895791853545039,-0.9982604689368237,260,583.7499999999999)" style="stroke: rgb(202, 194, 126); fill: rgb(202, 194, 126); stroke-width: 6;"><circle cx="0" cy="0" r="9.045000000000002"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M387.19,443.58 C380.1,535.72 254.02,615.51 160,515" style="stroke: rgb(103, 148, 135); stroke-width: 6; fill: none;"/><g stroke="rgba(103,148,135,1)" transform="matrix(0.004537840481175261,0.9999897039488793,-0.9999897039488793,0.004537840481175261,387.5,435)" style="stroke: rgb(103, 148, 135); stroke-width: 6;"><circle cx="0" cy="0" r="9.045000000000002"/></g><g stroke="rgba(103,148,135,1)" fill="rgba(103,148,135,1)" transform="matrix(-0.6831463259165826,-0.7302815192695721,0.7302815192695721,-0.6831463259165826,160,515)" style="stroke: rgb(103, 148, 135); fill: rgb(103, 148, 135); stroke-width: 6;"><circle cx="0" cy="0" r="9.045000000000002"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M345,195 L316.4,271.25" style="stroke: rgb(130, 192, 233); stroke-width: 3; fill: none;"/><g stroke="rgba(130,192,233,1)" transform="matrix(0.3511880698803796,-0.9363049394154095,0.9363049394154095,0.3511880698803796,315,275)" style="stroke: rgb(130, 192, 233); stroke-width: 3;"><path d=" M17.49,-5.26 Q7.94,-0.72 0,0 Q7.94,0.72 17.49,5.26"/></g></g><g class="arrow-line"><path class="connection real" stroke-dasharray="" d="  M485,330 L403.62,368.3" style="stroke: rgb(202, 194, 126); stroke-width: 3; fill: none;"/><g stroke="rgba(202,194,126,1)" transform="matrix(0.9048270524660194,-0.425779291565073,0.425779291565073,0.9048270524660194,400,370)" style="stroke: rgb(202, 194, 126); stroke-width: 3;"><path d=" M17.49,-5.26 Q7.94,-0.72 0,0 Q7.94,0.72 17.49,5.26"/></g></g><g/></g><g/><g/><g/></svg>
+        <svg xmlns="http://www.w3.org/2000/svg" width="649" height="707.4583740234375" style="width:649px;height:707.4583740234375px;font-family:Asana-Math, Asana;background:transparent;"><g><g><g><g><g><g style="transform:matrix(1,0,0,1,12.171875,40.31333587646485);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136ZM829 220C829 354 729 461 610 461C487 461 390 351 390 220C390 88 492 -11 609 -11C729 -11 829 90 829 220ZM609 53C540 53 468 109 468 230C468 351 544 400 609 400C679 400 751 348 751 230C751 112 683 53 609 53ZM1140 272L1308 444L1218 444L1015 236L1015 694L943 694L943 0L1012 0L1012 141L1092 224L1248 0L1330 0ZM1760 219C1760 421 1653 461 1582 461C1471 461 1381 355 1381 226C1381 94 1477 -11 1597 -11C1660 -11 1717 13 1756 41L1750 106C1687 54 1621 50 1598 50C1518 50 1454 121 1451 219ZM1456 274C1472 350 1525 400 1582 400C1634 400 1690 366 1703 274ZM2224 298C2224 364 2209 455 2087 455C1997 455 1948 387 1942 379L1942 450L1870 450L1870 0L1948 0L1948 245C1948 311 1973 394 2049 394C2145 394 2146 323 2146 291L2146 0L2224 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,68.578125,40.31333587646485);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,76.71356201171875,40.31333587646485);"><path d="M435 298C435 364 420 455 298 455C208 455 159 387 153 379L153 450L81 450L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,90.05731201171875,44.635999999999996);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,100.078125,40.31333587646485);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,115.55731201171875,40.31333587646485);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,139.2552490234375,40.31333587646485);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,148.80731201171875,44.635999999999996);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g></g><g><g style="transform:matrix(1,0,0,1,12.171875,71.98);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136ZM829 220C829 354 729 461 610 461C487 461 390 351 390 220C390 88 492 -11 609 -11C729 -11 829 90 829 220ZM609 53C540 53 468 109 468 230C468 351 544 400 609 400C679 400 751 348 751 230C751 112 683 53 609 53ZM1140 272L1308 444L1218 444L1015 236L1015 694L943 694L943 0L1012 0L1012 141L1092 224L1248 0L1330 0ZM1760 219C1760 421 1653 461 1582 461C1471 461 1381 355 1381 226C1381 94 1477 -11 1597 -11C1660 -11 1717 13 1756 41L1750 106C1687 54 1621 50 1598 50C1518 50 1454 121 1451 219ZM1456 274C1472 350 1525 400 1582 400C1634 400 1690 366 1703 274ZM2224 298C2224 364 2209 455 2087 455C1997 455 1948 387 1942 379L1942 450L1870 450L1870 0L1948 0L1948 245C1948 311 1973 394 2049 394C2145 394 2146 323 2146 291L2146 0L2224 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,68.578125,71.98);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,76.71356201171875,71.98);"><path d="M435 298C435 364 420 455 298 455C208 455 159 387 153 379L153 450L81 450L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,90.05731201171875,76.30266412353515);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,100.078125,71.98);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,115.55731201171875,71.98);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,139.2552490234375,71.98);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,148.80731201171875,76.30266412353515);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g></g><g><g style="transform:matrix(1,0,0,1,12.171875,103.64666412353516);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136ZM829 220C829 354 729 461 610 461C487 461 390 351 390 220C390 88 492 -11 609 -11C729 -11 829 90 829 220ZM609 53C540 53 468 109 468 230C468 351 544 400 609 400C679 400 751 348 751 230C751 112 683 53 609 53ZM1140 272L1308 444L1218 444L1015 236L1015 694L943 694L943 0L1012 0L1012 141L1092 224L1248 0L1330 0ZM1760 219C1760 421 1653 461 1582 461C1471 461 1381 355 1381 226C1381 94 1477 -11 1597 -11C1660 -11 1717 13 1756 41L1750 106C1687 54 1621 50 1598 50C1518 50 1454 121 1451 219ZM1456 274C1472 350 1525 400 1582 400C1634 400 1690 366 1703 274ZM2224 298C2224 364 2209 455 2087 455C1997 455 1948 387 1942 379L1942 450L1870 450L1870 0L1948 0L1948 245C1948 311 1973 394 2049 394C2145 394 2146 323 2146 291L2146 0L2224 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,68.578125,103.64666412353516);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,76.71356201171875,103.64666412353516);"><path d="M435 298C435 364 420 455 298 455C208 455 159 387 153 379L153 450L81 450L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,90.05731201171875,107.96933587646484);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,100.078125,103.64666412353516);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,115.55731201171875,103.64666412353516);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,139.2552490234375,103.64666412353516);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,148.80731201171875,107.96933587646484);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g></g><g><g style="transform:matrix(1,0,0,1,12.171875,135.31333587646483);"><path d="M124 111C94 111 67 83 67 53C67 23 94 -5 123 -5C155 -5 183 22 183 53C183 83 155 111 124 111ZM373 111C343 111 316 83 316 53C316 23 343 -5 372 -5C404 -5 432 22 432 53C432 83 404 111 373 111ZM622 111C592 111 565 83 565 53C565 23 592 -5 621 -5C653 -5 681 22 681 53C681 83 653 111 622 111Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,245.3802490234375,659.4047891235351);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,262.578125,665.2589131469726);"><path d="M459 253C459 366 378 446 264 446C216 446 180 443 127 396L127 605L432 604L432 689L75 690L75 322L95 316C142 363 169 377 218 377C314 377 374 309 374 201C374 90 310 25 201 25C147 25 97 43 83 69L37 151L13 137C36 80 48 48 62 4C90 -11 130 -20 173 -20C301 -20 459 89 459 253Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,465.3802490234375,444.4047891235352);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,482.578125,450.2588826293945);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,25.380218505859375,444.4047891235352);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,42.578125,450.2588826293945);"><path d="M409 603L47 -1L157 -1L497 659L497 689L44 689L44 477L74 477L81 533C89 595 96 603 142 603Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,405.3802490234375,284.4047891235352);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,422.578125,290.2588826293945);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,405.3802490234375,604.4047891235351);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,422.578125,610.2589131469726);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,85.38021850585938,604.4047891235351);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,102.578125,610.2589131469726);"><path d="M131 331C152 512 241 611 421 665L379 689C283 657 242 637 191 593C88 506 32 384 32 247C32 82 112 -20 241 -20C371 -20 468 83 468 219C468 334 399 409 293 409C216 409 184 370 131 331ZM255 349C331 349 382 283 382 184C382 80 331 13 254 13C169 13 123 86 123 220C123 255 127 274 138 291C160 325 207 349 255 349Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,332.04168701171875,37.480000000000004);"><path d="M157 214C157 314 229 386 327 388L327 455C238 454 183 405 152 359L152 450L82 450L82 0L157 0ZM739 289C739 391 666 461 574 461C509 461 464 445 417 418L423 352C475 389 525 402 574 402C621 402 661 362 661 288L661 245C511 243 384 201 384 113C384 70 411 -11 498 -11C512 -11 606 -9 664 36L664 0L739 0ZM661 132C661 113 661 88 627 69C598 51 560 50 549 50C501 50 456 73 456 115C456 185 618 192 661 194ZM1254 298C1254 364 1239 455 1117 455C1027 455 978 387 972 379L972 450L900 450L900 0L978 0L978 245C978 311 1003 394 1079 394C1175 394 1176 323 1176 291L1176 0L1254 0ZM1686 391C1708 391 1736 395 1760 395C1778 395 1817 392 1819 392L1808 455C1738 455 1680 436 1650 423C1629 440 1595 455 1555 455C1469 455 1396 383 1396 292C1396 255 1409 219 1429 193C1400 152 1400 113 1400 108C1400 82 1409 53 1426 32C1374 1 1362 -45 1362 -71C1362 -146 1461 -206 1583 -206C1706 -206 1805 -147 1805 -70C1805 69 1638 69 1599 69L1511 69C1498 69 1453 69 1453 122C1453 133 1457 149 1464 158C1485 143 1518 129 1555 129C1645 129 1715 203 1715 292C1715 340 1693 377 1682 392ZM1555 186C1518 186 1466 209 1466 292C1466 375 1518 398 1555 398C1598 398 1645 370 1645 292C1645 214 1598 186 1555 186ZM1600 -3C1622 -3 1735 -3 1735 -72C1735 -116 1666 -149 1584 -149C1503 -149 1432 -118 1432 -71C1432 -68 1432 -3 1510 -3ZM2247 219C2247 421 2140 461 2069 461C1958 461 1868 355 1868 226C1868 94 1964 -11 2084 -11C2147 -11 2204 13 2243 41L2237 106C2174 54 2108 50 2085 50C2005 50 1941 121 1938 219ZM1943 274C1959 350 2012 400 2069 400C2121 400 2177 366 2190 274Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,387.76043701171875,37.480000000000004);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,395.8958740234375,37.480000000000004);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,405.44793701171875,41.80266412353515);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,415.46875,37.480000000000004);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,426.46875,37.480000000000004);"><path d="M424 386L565 386L565 444L424 444L424 571L355 571L355 444L268 444L268 386L352 386L352 119C352 59 366 -11 435 -11C505 -11 556 14 581 27L565 86C539 65 507 53 475 53C438 53 424 83 424 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,442.1146240234375,41.80266412353515);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,452.13543701171875,37.480000000000004);"><path d="M245 -184L254 -171C248 -140 246 -112 246 -44L246 578C246 628 250 665 250 691C250 706 249 717 245 726L51 726L45 720L45 694L49 689L87 689C114 689 136 683 148 673C157 664 160 649 160 616L160 -75C160 -108 157 -122 148 -131C136 -141 114 -147 87 -147L49 -147L45 -152L45 -178L51 -184Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,460.2708740234375,37.480000000000004);"><path d="" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,471.2708740234375,37.480000000000004);"><path d="M949 272L743 486L711 452L836 300L65 300L65 241L836 241L711 89L743 55Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,500.96875,37.480000000000004);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,510.32293701171875,37.480000000000004);"><path d="M435 298C435 364 420 455 298 455C208 455 159 387 153 379L153 450L81 450L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,523.6666870117188,41.80266412353515);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,533.6875,37.480000000000004);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,544.6875,37.480000000000004);"><path d="M684 298C684 364 669 455 547 455C457 455 408 387 402 379L402 450L330 450L330 0L408 0L408 245C408 311 433 394 509 394C605 394 606 323 606 291L606 0L684 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,564.125,41.80266412353515);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,574.1458740234375,37.480000000000004);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,585.1458740234375,37.480000000000004);"><path d="M684 298C684 364 669 455 547 455C457 455 408 387 402 379L402 450L330 450L330 0L408 0L408 245C408 311 433 394 509 394C605 394 606 323 606 291L606 0L684 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,604.5833740234375,41.80266412353515);"><path d="M372 174L471 174L471 236L372 236L372 667L281 667L28 236L28 174L293 174L293 0L372 0ZM106 236C158 323 299 564 299 622L299 236Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,615.822998046875,37.480000000000004);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g><g><g><g><g><g style="transform:matrix(1,0,0,1,332.04168701171875,70.14666412353516);"><path d="M157 214C157 314 229 386 327 388L327 455C238 454 183 405 152 359L152 450L82 450L82 0L157 0ZM739 289C739 391 666 461 574 461C509 461 464 445 417 418L423 352C475 389 525 402 574 402C621 402 661 362 661 288L661 245C511 243 384 201 384 113C384 70 411 -11 498 -11C512 -11 606 -9 664 36L664 0L739 0ZM661 132C661 113 661 88 627 69C598 51 560 50 549 50C501 50 456 73 456 115C456 185 618 192 661 194ZM1254 298C1254 364 1239 455 1117 455C1027 455 978 387 972 379L972 450L900 450L900 0L978 0L978 245C978 311 1003 394 1079 394C1175 394 1176 323 1176 291L1176 0L1254 0ZM1686 391C1708 391 1736 395 1760 395C1778 395 1817 392 1819 392L1808 455C1738 455 1680 436 1650 423C1629 440 1595 455 1555 455C1469 455 1396 383 1396 292C1396 255 1409 219 1429 193C1400 152 1400 113 1400 108C1400 82 1409 53 1426 32C1374 1 1362 -45 1362 -71C1362 -146 1461 -206 1583 -206C1706 -206 1805 -147 1805 -70C1805 69 1638 69 1599 69L1511 69C1498 69 1453 69 1453 122C1453 133 1457 149 1464 158C1485 143 1518 129 1555 129C1645 129 1715 203 1715 292C1715 340 1693 377 1682 392ZM1555 186C1518 186 1466 209 1466 292C1466 375 1518 398 1555 398C1598 398 1645 370 1645 292C1645 214 1598 186 1555 186ZM1600 -3C1622 -3 1735 -3 1735 -72C1735 -116 1666 -149 1584 -149C1503 -149 1432 -118 1432 -71C1432 -68 1432 -3 1510 -3ZM2247 219C2247 421 2140 461 2069 461C1958 461 1868 355 1868 226C1868 94 1964 -11 2084 -11C2147 -11 2204 13 2243 41L2237 106C2174 54 2108 50 2085 50C2005 50 1941 121 1938 219ZM1943 274C1959 350 2012 400 2069 400C2121 400 2177 366 2190 274Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,387.76043701171875,70.14666412353516);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,395.8958740234375,70.14666412353516);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,405.44793701171875,74.46933587646484);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,415.46875,70.14666412353516);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,426.46875,70.14666412353516);"><path d="M424 386L565 386L565 444L424 444L424 571L355 571L355 444L268 444L268 386L352 386L352 119C352 59 366 -11 435 -11C505 -11 556 14 581 27L565 86C539 65 507 53 475 53C438 53 424 83 424 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,442.1146240234375,74.46933587646484);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,452.13543701171875,70.14666412353516);"><path d="M245 -184L254 -171C248 -140 246 -112 246 -44L246 578C246 628 250 665 250 691C250 706 249 717 245 726L51 726L45 720L45 694L49 689L87 689C114 689 136 683 148 673C157 664 160 649 160 616L160 -75C160 -108 157 -122 148 -131C136 -141 114 -147 87 -147L49 -147L45 -152L45 -178L51 -184Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,460.2708740234375,70.14666412353516);"><path d="" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,471.2708740234375,70.14666412353516);"><path d="M949 272L743 486L711 452L836 300L65 300L65 241L836 241L711 89L743 55Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,500.96875,70.14666412353516);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,510.32293701171875,70.14666412353516);"><path d="M435 298C435 364 420 455 298 455C208 455 159 387 153 379L153 450L81 450L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,523.6666870117188,74.46933587646484);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,533.6875,70.14666412353516);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,544.6875,70.14666412353516);"><path d="M684 298C684 364 669 455 547 455C457 455 408 387 402 379L402 450L330 450L330 0L408 0L408 245C408 311 433 394 509 394C605 394 606 323 606 291L606 0L684 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,564.125,74.46933587646484);"><path d="M372 174L471 174L471 236L372 236L372 667L281 667L28 236L28 174L293 174L293 0L372 0ZM106 236C158 323 299 564 299 622L299 236Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,574.1458740234375,70.14666412353516);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,585.1458740234375,70.14666412353516);"><path d="M684 298C684 364 669 455 547 455C457 455 408 387 402 379L402 450L330 450L330 0L408 0L408 245C408 311 433 394 509 394C605 394 606 323 606 291L606 0L684 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,604.5833740234375,74.46933587646484);"><path d="M153 602L416 602L416 667L81 667L81 292L147 292C164 332 201 372 259 372C306 372 360 330 360 208C360 40 238 40 229 40C162 40 101 79 72 134L39 77C80 19 149 -22 230 -22C349 -22 449 78 449 206C449 333 363 434 260 434C220 434 182 419 153 392Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,615.822998046875,70.14666412353516);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g><g><g><g><g><g style="transform:matrix(1,0,0,1,332.04168701171875,102.81333587646485);"><path d="M157 214C157 314 229 386 327 388L327 455C238 454 183 405 152 359L152 450L82 450L82 0L157 0ZM739 289C739 391 666 461 574 461C509 461 464 445 417 418L423 352C475 389 525 402 574 402C621 402 661 362 661 288L661 245C511 243 384 201 384 113C384 70 411 -11 498 -11C512 -11 606 -9 664 36L664 0L739 0ZM661 132C661 113 661 88 627 69C598 51 560 50 549 50C501 50 456 73 456 115C456 185 618 192 661 194ZM1254 298C1254 364 1239 455 1117 455C1027 455 978 387 972 379L972 450L900 450L900 0L978 0L978 245C978 311 1003 394 1079 394C1175 394 1176 323 1176 291L1176 0L1254 0ZM1686 391C1708 391 1736 395 1760 395C1778 395 1817 392 1819 392L1808 455C1738 455 1680 436 1650 423C1629 440 1595 455 1555 455C1469 455 1396 383 1396 292C1396 255 1409 219 1429 193C1400 152 1400 113 1400 108C1400 82 1409 53 1426 32C1374 1 1362 -45 1362 -71C1362 -146 1461 -206 1583 -206C1706 -206 1805 -147 1805 -70C1805 69 1638 69 1599 69L1511 69C1498 69 1453 69 1453 122C1453 133 1457 149 1464 158C1485 143 1518 129 1555 129C1645 129 1715 203 1715 292C1715 340 1693 377 1682 392ZM1555 186C1518 186 1466 209 1466 292C1466 375 1518 398 1555 398C1598 398 1645 370 1645 292C1645 214 1598 186 1555 186ZM1600 -3C1622 -3 1735 -3 1735 -72C1735 -116 1666 -149 1584 -149C1503 -149 1432 -118 1432 -71C1432 -68 1432 -3 1510 -3ZM2247 219C2247 421 2140 461 2069 461C1958 461 1868 355 1868 226C1868 94 1964 -11 2084 -11C2147 -11 2204 13 2243 41L2237 106C2174 54 2108 50 2085 50C2005 50 1941 121 1938 219ZM1943 274C1959 350 2012 400 2069 400C2121 400 2177 366 2190 274Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,387.76043701171875,102.81333587646485);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,395.8958740234375,102.81333587646485);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,405.44793701171875,107.13600762939453);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,415.46875,102.81333587646485);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,426.46875,102.81333587646485);"><path d="M424 386L565 386L565 444L424 444L424 571L355 571L355 444L268 444L268 386L352 386L352 119C352 59 366 -11 435 -11C505 -11 556 14 581 27L565 86C539 65 507 53 475 53C438 53 424 83 424 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,442.1146240234375,107.13600762939453);"><path d="M372 174L471 174L471 236L372 236L372 667L281 667L28 236L28 174L293 174L293 0L372 0ZM106 236C158 323 299 564 299 622L299 236Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,452.13543701171875,102.81333587646485);"><path d="M245 -184L254 -171C248 -140 246 -112 246 -44L246 578C246 628 250 665 250 691C250 706 249 717 245 726L51 726L45 720L45 694L49 689L87 689C114 689 136 683 148 673C157 664 160 649 160 616L160 -75C160 -108 157 -122 148 -131C136 -141 114 -147 87 -147L49 -147L45 -152L45 -178L51 -184Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,460.2708740234375,102.81333587646485);"><path d="" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,471.2708740234375,102.81333587646485);"><path d="M949 272L743 486L711 452L836 300L65 300L65 241L836 241L711 89L743 55Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,500.96875,102.81333587646485);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,510.32293701171875,102.81333587646485);"><path d="M435 298C435 364 420 455 298 455C208 455 159 387 153 379L153 450L81 450L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,523.6666870117188,107.13600762939453);"><path d="M372 174L471 174L471 236L372 236L372 667L281 667L28 236L28 174L293 174L293 0L372 0ZM106 236C158 323 299 564 299 622L299 236Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,533.6875,102.81333587646485);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,544.6875,102.81333587646485);"><path d="M684 298C684 364 669 455 547 455C457 455 408 387 402 379L402 450L330 450L330 0L408 0L408 245C408 311 433 394 509 394C605 394 606 323 606 291L606 0L684 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,564.125,107.13600762939453);"><path d="M153 602L416 602L416 667L81 667L81 292L147 292C164 332 201 372 259 372C306 372 360 330 360 208C360 40 238 40 229 40C162 40 101 79 72 134L39 77C80 19 149 -22 230 -22C349 -22 449 78 449 206C449 333 363 434 260 434C220 434 182 419 153 392Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,574.1458740234375,102.81333587646485);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,585.1458740234375,102.81333587646485);"><path d="M684 298C684 364 669 455 547 455C457 455 408 387 402 379L402 450L330 450L330 0L408 0L408 245C408 311 433 394 509 394C605 394 606 323 606 291L606 0L684 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,604.5833740234375,107.13600762939453);"><path d="M415 669C364 689 323 689 309 689C168 689 42 546 42 327C42 40 166 -22 251 -22C311 -22 354 1 393 47C438 99 457 145 457 226C457 356 391 469 295 469C263 469 187 461 126 385C139 549 218 630 310 630C348 630 380 623 415 609ZM127 223C127 237 127 239 128 251C128 326 173 407 256 407C304 407 332 382 352 344C373 307 375 271 375 226C375 191 375 144 351 103C334 74 308 40 251 40C145 40 130 189 127 223Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,615.822998046875,102.81333587646485);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g><g><text x="332.04168701171875" y="113.66666412353516" style="white-space:pre;stroke:none;fill:rgb(0, 0, 0);font-size:21.6px;font-family:Arial, Helvetica, sans-serif;font-weight:400;font-style:normal;dominant-baseline:text-before-edge;text-decoration:none solid rgb(0, 0, 0);">                                           ...</text></g></g><g><g><g><g><g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,284.9114990234375,163.04063262939454);"><path d="M435 298C435 364 420 455 298 455C236 455 188 424 156 383L156 694L81 694L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0ZM914 289C914 391 841 461 749 461C684 461 639 445 592 418L598 352C650 389 700 402 749 402C796 402 836 362 836 288L836 245C686 243 559 201 559 113C559 70 586 -11 673 -11C687 -11 781 -9 839 36L839 0L914 0ZM836 132C836 113 836 88 802 69C773 51 735 50 724 50C676 50 631 73 631 115C631 185 793 192 836 194ZM1354 128C1354 183 1317 217 1315 220C1276 255 1249 261 1199 270C1144 281 1098 291 1098 340C1098 402 1170 402 1183 402C1215 402 1268 398 1325 364L1337 429C1285 453 1244 461 1193 461C1168 461 1027 461 1027 330C1027 281 1056 249 1081 230C1112 208 1134 204 1189 193C1225 186 1283 174 1283 121C1283 52 1204 52 1189 52C1108 52 1052 89 1034 101L1022 33C1054 17 1109 -11 1190 -11C1328 -11 1354 75 1354 128ZM1811 298C1811 364 1796 455 1674 455C1612 455 1564 424 1532 383L1532 694L1457 694L1457 0L1535 0L1535 245C1535 311 1560 394 1636 394C1732 394 1733 323 1733 291L1733 0L1811 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,323.46356201171875,163.70728912353516);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,330.234375,163.04063262939454);"><path d="M105 469L137 665C138 667 138 669 138 671C138 689 116 709 95 709C74 709 52 689 52 670L52 665L85 469ZM286 469L318 665C319 667 319 669 319 670C319 689 297 709 276 709C254 709 233 690 233 670L233 665L266 469ZM546 386L656 386L656 444L543 444L543 563C543 637 610 644 636 644C656 644 683 642 717 627L717 694C705 697 674 705 637 705C543 705 471 634 471 534L471 444L397 444L397 386L471 386L471 0L546 0ZM1143 220C1143 354 1043 461 924 461C801 461 704 351 704 220C704 88 806 -11 923 -11C1043 -11 1143 90 1143 220ZM923 53C854 53 782 109 782 230C782 351 858 400 923 400C993 400 1065 348 1065 230C1065 112 997 53 923 53ZM1642 220C1642 354 1542 461 1423 461C1300 461 1203 351 1203 220C1203 88 1305 -11 1422 -11C1542 -11 1642 90 1642 220ZM1422 53C1353 53 1281 109 1281 230C1281 351 1357 400 1422 400C1492 400 1564 348 1564 230C1564 112 1496 53 1422 53ZM1777 469L1809 665C1810 667 1810 669 1810 671C1810 689 1788 709 1767 709C1746 709 1724 689 1724 670L1724 665L1757 469ZM1958 469L1990 665C1991 667 1991 669 1991 670C1991 689 1969 709 1948 709C1926 709 1905 690 1905 670L1905 665L1938 469Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,371.86981201171875,163.70728912353516);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g></g><g><g style="transform:matrix(1,0,0,1,284.9114990234375,187.04063262939454);"><path d="" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,324.52606201171875,187.04063262939454);"><path d="M949 272L743 486L711 452L836 300L65 300L65 241L836 241L711 89L743 55Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,349.2552490234375,187.70728912353516);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,357.0364990234375,187.04063262939454);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,368.96356201171875,191.30603912353516);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.01428,0,0,-0.01428,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,377.30731201171875,187.04063262939454);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,386.46356201171875,187.04063262939454);"><path d="M273 388L280 368L312 389C349 412 352 414 359 414C370 414 377 404 377 389C377 338 336 145 295 2L302 -9C327 -2 350 4 372 8C391 134 412 199 458 268C512 352 587 414 632 414C643 414 649 405 649 390C649 372 646 351 638 319L586 107C577 70 573 47 573 31C573 6 584 -9 603 -9C629 -9 665 12 763 85L753 103L727 86C698 67 676 56 666 56C659 56 653 65 653 76C653 81 654 92 655 96L721 372C728 401 732 429 732 446C732 469 721 482 701 482C659 482 590 444 531 389C493 354 465 320 413 247L451 408C455 426 457 438 457 449C457 470 449 482 434 482C413 482 374 460 301 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,403.46356201171875,191.30603912353516);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.01428,0,0,-0.01428,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,411.80731201171875,187.04063262939454);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,420.96356201171875,187.04063262939454);"><path d="M273 388L280 368L312 389C349 412 352 414 359 414C370 414 377 404 377 389C377 338 336 145 295 2L302 -9C327 -2 350 4 372 8C391 134 412 199 458 268C512 352 587 414 632 414C643 414 649 405 649 390C649 372 646 351 638 319L586 107C577 70 573 47 573 31C573 6 584 -9 603 -9C629 -9 665 12 763 85L753 103L727 86C698 67 676 56 666 56C659 56 653 65 653 76C653 81 654 92 655 96L721 372C728 401 732 429 732 446C732 469 721 482 701 482C659 482 590 444 531 389C493 354 465 320 413 247L451 408C455 426 457 438 457 449C457 470 449 482 434 482C413 482 374 460 301 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,437.96356201171875,191.30603912353516);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.01428,0,0,-0.01428,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,447.3177490234375,187.70728912353516);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,245.3802490234375,224.40478912353515);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,262.578125,230.25888262939452);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,85.38021850585938,284.4047891235352);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,102.578125,290.2588826293945);"><path d="M168 345C127 326 110 315 88 294C50 256 30 211 30 159C30 56 113 -20 226 -20C356 -20 464 82 464 206C464 286 427 329 313 381C404 443 436 485 436 545C436 631 365 689 259 689C140 689 53 613 53 508C53 440 80 402 168 345ZM284 295C347 267 385 218 385 164C385 80 322 14 241 14C156 14 101 74 101 167C101 240 130 286 204 331ZM223 423C160 454 128 494 128 544C128 610 176 655 247 655C320 655 368 607 368 534C368 477 343 438 278 396Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,457.4114990234375,316.56666412353513);"><path d="M435 298C435 364 420 455 298 455C236 455 188 424 156 383L156 694L81 694L81 0L159 0L159 245C159 311 184 394 260 394C356 394 357 323 357 291L357 0L435 0ZM914 289C914 391 841 461 749 461C684 461 639 445 592 418L598 352C650 389 700 402 749 402C796 402 836 362 836 288L836 245C686 243 559 201 559 113C559 70 586 -11 673 -11C687 -11 781 -9 839 36L839 0L914 0ZM836 132C836 113 836 88 802 69C773 51 735 50 724 50C676 50 631 73 631 115C631 185 793 192 836 194ZM1354 128C1354 183 1317 217 1315 220C1276 255 1249 261 1199 270C1144 281 1098 291 1098 340C1098 402 1170 402 1183 402C1215 402 1268 398 1325 364L1337 429C1285 453 1244 461 1193 461C1168 461 1027 461 1027 330C1027 281 1056 249 1081 230C1112 208 1134 204 1189 193C1225 186 1283 174 1283 121C1283 52 1204 52 1189 52C1108 52 1052 89 1034 101L1022 33C1054 17 1109 -11 1190 -11C1328 -11 1354 75 1354 128ZM1811 298C1811 364 1796 455 1674 455C1612 455 1564 424 1532 383L1532 694L1457 694L1457 0L1535 0L1535 245C1535 311 1560 394 1636 394C1732 394 1733 323 1733 291L1733 0L1811 0Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,495.96356201171875,317.2333511352539);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,502.734375,316.56666412353513);"><path d="M105 469L137 665C138 667 138 669 138 671C138 689 116 709 95 709C74 709 52 689 52 670L52 665L85 469ZM286 469L318 665C319 667 319 669 319 670C319 689 297 709 276 709C254 709 233 690 233 670L233 665L266 469ZM527 694L452 694L452 0L530 0L530 46C554 24 597 -11 664 -11C764 -11 850 89 850 223C850 347 782 455 688 455C649 455 587 445 527 396ZM530 335C546 359 582 394 637 394C696 394 772 351 772 223C772 93 688 50 627 50C588 50 555 68 530 114ZM1284 289C1284 391 1211 461 1119 461C1054 461 1009 445 962 418L968 352C1020 389 1070 402 1119 402C1166 402 1206 362 1206 288L1206 245C1056 243 929 201 929 113C929 70 956 -11 1043 -11C1057 -11 1151 -9 1209 36L1209 0L1284 0ZM1206 132C1206 113 1206 88 1172 69C1143 51 1105 50 1094 50C1046 50 1001 73 1001 115C1001 185 1163 192 1206 194ZM1521 214C1521 314 1593 386 1691 388L1691 455C1602 454 1547 405 1516 359L1516 450L1446 450L1446 0L1521 0ZM1809 469L1841 665C1842 667 1842 669 1842 671C1842 689 1820 709 1799 709C1778 709 1756 689 1756 670L1756 665L1789 469ZM1990 469L2022 665C2023 667 2023 669 2023 670C2023 689 2001 709 1980 709C1958 709 1937 690 1937 670L1937 665L1970 469Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,545.015625,317.2333511352539);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g></g></g></g></g><g><g><g><g><g style="transform:matrix(1,0,0,1,457.4114990234375,343.2333511352539);"><path d="" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g style="transform:matrix(1,0,0,1,497.02606201171875,343.2333511352539);"><path d="M949 272L743 486L711 452L836 300L65 300L65 241L836 241L711 89L743 55Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g></g><g style="transform:matrix(1,0,0,1,521.7552490234375,343.9000076293945);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,529.5364990234375,343.2333511352539);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,541.4635620117188,347.4987576293945);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.01428,0,0,-0.01428,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,549.8073120117188,343.2333511352539);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,558.9635620117188,343.2333511352539);"><path d="M273 388L280 368L312 389C349 412 352 414 359 414C370 414 377 404 377 389C377 338 336 145 295 2L302 -9C327 -2 350 4 372 8C391 134 412 199 458 268C512 352 587 414 632 414C643 414 649 405 649 390C649 372 646 351 638 319L586 107C577 70 573 47 573 31C573 6 584 -9 603 -9C629 -9 665 12 763 85L753 103L727 86C698 67 676 56 666 56C659 56 653 65 653 76C653 81 654 92 655 96L721 372C728 401 732 429 732 446C732 469 721 482 701 482C659 482 590 444 531 389C493 354 465 320 413 247L451 408C455 426 457 438 457 449C457 470 449 482 434 482C413 482 374 460 301 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,575.9635620117188,347.4987576293945);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.01428,0,0,-0.01428,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,584.3073120117188,343.2333511352539);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g style="transform:matrix(1,0,0,1,593.4635620117188,343.2333511352539);"><path d="M273 388L280 368L312 389C349 412 352 414 359 414C370 414 377 404 377 389C377 338 336 145 295 2L302 -9C327 -2 350 4 372 8C391 134 412 199 458 268C512 352 587 414 632 414C643 414 649 405 649 390C649 372 646 351 638 319L586 107C577 70 573 47 573 31C573 6 584 -9 603 -9C629 -9 665 12 763 85L753 103L727 86C698 67 676 56 666 56C659 56 653 65 653 76C653 81 654 92 655 96L721 372C728 401 732 429 732 446C732 469 721 482 701 482C659 482 590 444 531 389C493 354 465 320 413 247L451 408C455 426 457 438 457 449C457 470 449 482 434 482C413 482 374 460 301 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,610.463623046875,347.4987576293945);"><path d="M459 253C459 366 378 446 264 446C216 446 180 443 127 396L127 605L432 604L432 689L75 690L75 322L95 316C142 363 169 377 218 377C314 377 374 309 374 201C374 90 310 25 201 25C147 25 97 43 83 69L37 151L13 137C36 80 48 48 62 4C90 -11 130 -20 173 -20C301 -20 459 89 459 253Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.01428,0,0,-0.01428,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,619.8177490234375,343.9000076293945);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020399999999999998,0,0,-0.020399999999999998,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,254.71356201171875,324.78125762939453);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,261.33856201171875,328.4521011352539);"><path d="M299 689L279 689C220 627 137 624 89 622L89 563C122 564 170 566 220 587L220 59L95 59L95 0L424 0L424 59L299 59Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,331.71356201171875,354.78125762939453);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,338.33856201171875,358.4521011352539);"><path d="M83 466C103 545 131 624 222 624C316 624 367 548 367 468C367 382 310 324 251 263L174 191L50 65L50 0L449 0L449 72L267 72C255 72 243 71 231 71L122 71C154 100 230 176 261 205C333 274 449 347 449 471C449 587 368 689 236 689C122 689 66 610 42 522C66 487 59 501 83 466Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,359.71356201171875,439.78125762939453);"><path d="M175 386L316 386L316 444L175 444L175 571L106 571L106 444L19 444L19 386L103 386L103 119C103 59 117 -11 186 -11C256 -11 307 14 332 27L316 86C290 65 258 53 226 53C189 53 175 83 175 136Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017,0,0,-0.017,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,366.33856201171875,443.4521011352539);"><path d="M92 522C121 593 186 630 247 630C299 630 348 600 348 535C348 473 307 413 246 398C240 397 238 397 167 391L167 329L238 329C346 329 368 235 368 184C368 105 322 40 245 40C176 40 97 75 53 144L42 83C115 -12 207 -22 247 -22C369 -22 457 76 457 183C457 275 387 338 319 360C395 401 430 471 430 535C430 622 347 689 248 689C171 689 98 648 56 577Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.0119,0,0,-0.0119,0,0);"></path></g></g></g></g></g></g></g></g></g></svg>
+</svg>
diff --git a/doc/modules/cassandra/pages/architecture/images/vnodes.svg b/doc/modules/cassandra/pages/architecture/images/vnodes.svg
new file mode 100644
index 0000000..71b4fa2
--- /dev/null
+++ b/doc/modules/cassandra/pages/architecture/images/vnodes.svg
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="651" height="384.66668701171875" style="
+        width:651px;
+        height:384.66668701171875px;
+        background: transparent;
+        fill: none;
+">
+        
+        
+        <svg xmlns="http://www.w3.org/2000/svg" class="role-diagram-draw-area"><g class="shapes-region" style="stroke: black; fill: none;"><g class="composite-shape"><path class="real" d=" M40.4,190 C40.4,107.38 107.38,40.4 190,40.4 C272.62,40.4 339.6,107.38 339.6,190 C339.6,272.62 272.62,339.6 190,339.6 C107.38,339.6 40.4,272.62 40.4,190 Z" style="stroke-width: 1; stroke: rgba(0, 0, 0, 0.52); fill: none; stroke-dasharray: 1.125, 3.35;"/></g><g class="composite-shape"><path class="real" d=" M160,340 C160,323.43 173.43,310 190,310 C206.57,310 220,323.43 220,340 C220,356.57 206.57,370 190,370 C173.43,370 160,356.57 160,340 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(130, 192, 233); stroke-dasharray: 6, 6;"/></g><g class="composite-shape"><path class="real" d=" M160,40 C160,23.43 173.43,10 190,10 C206.57,10 220,23.43 220,40 C220,56.57 206.57,70 190,70 C173.43,70 160,56.57 160,40 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(130, 192, 233); stroke-dasharray: 6, 6;"/></g><g class="composite-shape"><path class="real" d=" M310,190 C310,173.43 323.43,160 340,160 C356.57,160 370,173.43 370,190 C370,206.57 356.57,220 340,220 C323.43,220 310,206.57 310,190 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(103, 148, 135); stroke-dasharray: 1.125, 3.35;"/></g><g class="composite-shape"><path class="real" d=" M10,190 C10,173.43 23.43,160 40,160 C56.57,160 70,173.43 70,190 C70,206.57 56.57,220 40,220 C23.43,220 10,206.57 10,190 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(103, 148, 135); stroke-dasharray: 1.125, 3.35;"/></g><g class="composite-shape"><path class="real" d=" M270,80 C270,63.43 283.43,50 300,50 C316.57,50 330,63.43 330,80 C330,96.57 316.57,110 300,110 C283.43,110 270,96.57 270,80 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(202, 194, 126);"/></g><g class="composite-shape"><path class="real" d=" M270,300 C270,283.43 283.43,270 300,270 C316.57,270 330,283.43 330,300 C330,316.57 316.57,330 300,330 C283.43,330 270,316.57 270,300 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M50,300 C50,283.43 63.43,270 80,270 C96.57,270 110,283.43 110,300 C110,316.57 96.57,330 80,330 C63.43,330 50,316.57 50,300 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(202, 194, 126);"/></g><g class="composite-shape"><path class="real" d=" M50,80 C50,63.43 63.43,50 80,50 C96.57,50 110,63.43 110,80 C110,96.57 96.57,110 80,110 C63.43,110 50,96.57 50,80 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M380,158.4 C380,146.47 389.67,136.8 401.6,136.8 C413.53,136.8 423.2,146.47 423.2,158.4 C423.2,170.33 413.53,180 401.6,180 C389.67,180 380,170.33 380,158.4 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(103, 148, 135); stroke-dasharray: 1.125, 3.35;"/></g><g class="composite-shape"><path class="real" d=" M380,101.6 C380,89.67 389.67,80 401.6,80 C413.53,80 423.2,89.67 423.2,101.6 C423.2,113.53 413.53,123.2 401.6,123.2 C389.67,123.2 380,113.53 380,101.6 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(130, 192, 233); stroke-dasharray: 6, 6;"/></g><g class="composite-shape"><path class="real" d=" M380,218.4 C380,206.47 389.67,196.8 401.6,196.8 C413.53,196.8 423.2,206.47 423.2,218.4 C423.2,230.33 413.53,240 401.6,240 C389.67,240 380,230.33 380,218.4 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(202, 194, 126);"/></g><g class="composite-shape"><path class="real" d=" M380,278.4 C380,266.47 389.67,256.8 401.6,256.8 C413.53,256.8 423.2,266.47 423.2,278.4 C423.2,290.33 413.53,300 401.6,300 C389.67,300 380,290.33 380,278.4 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: rgb(187, 187, 187);"/></g><g class="composite-shape"><path class="real" d=" M430,80 L640,80 L640,300 L430,300 Z" style="stroke-width: 1; stroke: rgb(0, 0, 0); fill: none;"/></g><g/></g><g/><g/><g/></svg>
+        <svg xmlns="http://www.w3.org/2000/svg" width="649" height="382.66668701171875" style="width:649px;height:382.66668701171875px;font-family:Asana-Math, Asana;background:transparent;"><g><g><g><g><g><g style="transform:matrix(1,0,0,1,178.65625,348.9985620117188);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,189.3021240234375,354.852625);"><path d="M459 253C459 366 378 446 264 446C216 446 180 443 127 396L127 605L432 604L432 689L75 690L75 322L95 316C142 363 169 377 218 377C314 377 374 309 374 201C374 90 310 25 201 25C147 25 97 43 83 69L37 151L13 137C36 80 48 48 62 4C90 -11 130 -20 173 -20C301 -20 459 89 459 253Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,178.65625,49.800625);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,189.3021240234375,55.65471850585938);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,328.25,199.40481201171875);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,338.8958740234375,205.258875);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,29.052093505859375,199.40481201171875);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,39.69793701171875,205.258875);"><path d="M409 603L47 -1L157 -1L497 659L497 689L44 689L44 477L74 477L81 533C89 595 96 603 142 603Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,287.44793701171875,90.60271850585937);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,298.09375,96.45679675292969);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,287.44793701171875,308.1964685058594);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,298.09375,314.05056201171874);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,69.85418701171875,308.1964685058594);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,80.5,314.05056201171874);"><path d="M131 331C152 512 241 611 421 665L379 689C283 657 242 637 191 593C88 506 32 384 32 247C32 82 112 -20 241 -20C371 -20 468 83 468 219C468 334 399 409 293 409C216 409 184 370 131 331ZM255 349C331 349 382 283 382 184C382 80 331 13 254 13C169 13 123 86 123 220C123 255 127 274 138 291C160 325 207 349 255 349Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,69.85418701171875,90.60271850585937);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,80.5,96.45679675292969);"><path d="M168 345C127 326 110 315 88 294C50 256 30 211 30 159C30 56 113 -20 226 -20C356 -20 464 82 464 206C464 286 427 329 313 381C404 443 436 485 436 545C436 631 365 689 259 689C140 689 53 613 53 508C53 440 80 402 168 345ZM284 295C347 267 385 218 385 164C385 80 322 14 241 14C156 14 101 74 101 167C101 240 130 286 204 331ZM223 423C160 454 128 494 128 544C128 610 176 655 247 655C320 655 368 607 368 534C368 477 343 438 278 396Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,386.9739990234375,167.800625);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,404.171875,173.65471850585936);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,386.9739990234375,110.99856201171875);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,404.171875,116.852625);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,386.9739990234375,227.800625);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,404.171875,233.65471850585936);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,386.9739990234375,287.800625);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02941,0,0,-0.02941,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,404.171875,293.65471850585936);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.020587,0,0,-0.020587,0,0);"></path></g></g></g></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,438.125,111.64668701171875);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390ZM349 152C349 46 394 -11 477 -11C532 -11 592 15 637 57C699 116 743 230 743 331C743 425 693 482 610 482C506 482 349 382 349 152ZM573 444C634 444 667 399 667 315C667 219 636 113 592 60C574 39 548 27 517 27C459 27 425 72 425 151C425 264 464 387 513 427C526 438 549 444 573 444ZM1015 722L1003 733C951 707 915 698 843 691L839 670L887 670C911 670 921 663 921 646C921 638 920 629 919 622L871 354C857 279 841 210 789 1L798 -9L865 8L888 132C897 180 921 225 955 259C1024 59 1056 -9 1081 -9C1094 -9 1118 4 1162 35L1208 67L1200 86L1157 62C1143 54 1136 52 1128 52C1118 52 1111 58 1102 75C1067 139 1045 193 1006 316L1020 330C1081 391 1127 416 1177 416C1185 416 1196 414 1213 410L1230 473C1212 479 1194 482 1182 482C1116 482 1033 405 908 230ZM1571 111L1547 94C1494 56 1446 36 1410 36C1363 36 1334 73 1334 133C1334 158 1337 185 1342 214C1359 218 1468 248 1493 259C1578 296 1617 342 1617 404C1617 451 1583 482 1533 482C1465 496 1355 423 1318 349C1288 299 1258 180 1258 113C1258 35 1302 -11 1374 -11C1431 -11 1487 17 1579 92ZM1356 274C1373 343 1393 386 1422 412C1440 428 1471 440 1495 440C1524 440 1543 420 1543 388C1543 344 1508 297 1456 272C1428 258 1392 247 1347 237ZM1655 388L1662 368L1694 389C1731 412 1734 414 1741 414C1752 414 1759 404 1759 389C1759 338 1718 145 1677 2L1684 -9C1709 -2 1732 4 1754 8C1773 134 1794 199 1840 268C1894 352 1969 414 2014 414C2025 414 2031 405 2031 390C2031 372 2028 351 2020 319L1968 107C1959 70 1955 47 1955 31C1955 6 1966 -9 1985 -9C2011 -9 2047 12 2145 85L2135 103L2109 86C2080 67 2058 56 2048 56C2041 56 2035 65 2035 76C2035 81 2036 92 2037 96L2103 372C2110 401 2114 429 2114 446C2114 469 2103 482 2083 482C2041 482 1972 444 1913 389C1875 354 1847 320 1795 247L1833 408C1837 426 1839 438 1839 449C1839 470 1831 482 1816 482C1795 482 1756 460 1683 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,491.6458740234375,111.64668701171875);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,499.78125,111.64668701171875);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,514.1041870117188,115.96934350585937);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,524.125,111.64668701171875);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,539.6041870117188,111.64668701171875);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,563.3021240234375,111.64668701171875);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,572.65625,111.64668701171875);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,581.5208740234375,115.96934350585937);"><path d="M418 -3L418 27L366 30C311 33 301 44 301 96L301 700L60 598L67 548L217 614L217 96C217 44 206 33 152 30L96 27L96 -3C250 0 250 0 261 0C292 0 402 -3 418 -3Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,591.5416870117188,111.64668701171875);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,602.541748046875,111.64668701171875);"><path d="M374 390L318 107C317 99 305 61 305 31C305 6 316 -9 335 -9C370 -9 405 11 483 74L514 99L504 117L459 86C430 66 410 56 399 56C390 56 385 64 385 76C385 102 399 183 428 328L441 390L548 390L559 440C521 436 487 434 449 434C465 528 476 577 494 631L483 646C463 634 436 622 405 610L380 440C336 419 310 408 292 403L290 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,617.5,115.96934350585937);"><path d="M459 253C459 366 378 446 264 446C216 446 180 443 127 396L127 605L432 604L432 689L75 690L75 322L95 316C142 363 169 377 218 377C314 377 374 309 374 201C374 90 310 25 201 25C147 25 97 43 83 69L37 151L13 137C36 80 48 48 62 4C90 -11 130 -20 173 -20C301 -20 459 89 459 253Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,628.7396240234375,111.64668701171875);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,439.125,164.64668701171874);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390ZM349 152C349 46 394 -11 477 -11C532 -11 592 15 637 57C699 116 743 230 743 331C743 425 693 482 610 482C506 482 349 382 349 152ZM573 444C634 444 667 399 667 315C667 219 636 113 592 60C574 39 548 27 517 27C459 27 425 72 425 151C425 264 464 387 513 427C526 438 549 444 573 444ZM1015 722L1003 733C951 707 915 698 843 691L839 670L887 670C911 670 921 663 921 646C921 638 920 629 919 622L871 354C857 279 841 210 789 1L798 -9L865 8L888 132C897 180 921 225 955 259C1024 59 1056 -9 1081 -9C1094 -9 1118 4 1162 35L1208 67L1200 86L1157 62C1143 54 1136 52 1128 52C1118 52 1111 58 1102 75C1067 139 1045 193 1006 316L1020 330C1081 391 1127 416 1177 416C1185 416 1196 414 1213 410L1230 473C1212 479 1194 482 1182 482C1116 482 1033 405 908 230ZM1571 111L1547 94C1494 56 1446 36 1410 36C1363 36 1334 73 1334 133C1334 158 1337 185 1342 214C1359 218 1468 248 1493 259C1578 296 1617 342 1617 404C1617 451 1583 482 1533 482C1465 496 1355 423 1318 349C1288 299 1258 180 1258 113C1258 35 1302 -11 1374 -11C1431 -11 1487 17 1579 92ZM1356 274C1373 343 1393 386 1422 412C1440 428 1471 440 1495 440C1524 440 1543 420 1543 388C1543 344 1508 297 1456 272C1428 258 1392 247 1347 237ZM1655 388L1662 368L1694 389C1731 412 1734 414 1741 414C1752 414 1759 404 1759 389C1759 338 1718 145 1677 2L1684 -9C1709 -2 1732 4 1754 8C1773 134 1794 199 1840 268C1894 352 1969 414 2014 414C2025 414 2031 405 2031 390C2031 372 2028 351 2020 319L1968 107C1959 70 1955 47 1955 31C1955 6 1966 -9 1985 -9C2011 -9 2047 12 2145 85L2135 103L2109 86C2080 67 2058 56 2048 56C2041 56 2035 65 2035 76C2035 81 2036 92 2037 96L2103 372C2110 401 2114 429 2114 446C2114 469 2103 482 2083 482C2041 482 1972 444 1913 389C1875 354 1847 320 1795 247L1833 408C1837 426 1839 438 1839 449C1839 470 1831 482 1816 482C1795 482 1756 460 1683 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,492.6458740234375,164.64668701171874);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,500.78125,164.64668701171874);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,515.1041870117188,168.96934350585937);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,525.125,164.64668701171874);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,540.6041870117188,164.64668701171874);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,564.3021240234375,164.64668701171874);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,573.65625,164.64668701171874);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,582.5208740234375,168.96934350585937);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,592.5416870117188,164.64668701171874);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,603.541748046875,164.64668701171874);"><path d="M374 390L318 107C317 99 305 61 305 31C305 6 316 -9 335 -9C370 -9 405 11 483 74L514 99L504 117L459 86C430 66 410 56 399 56C390 56 385 64 385 76C385 102 399 183 428 328L441 390L548 390L559 440C521 436 487 434 449 434C465 528 476 577 494 631L483 646C463 634 436 622 405 610L380 440C336 419 310 408 292 403L290 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,618.5,168.96934350585937);"><path d="M409 603L47 -1L157 -1L497 659L497 689L44 689L44 477L74 477L81 533C89 595 96 603 142 603Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,629.7396240234375,164.64668701171874);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,439.125,221.64668701171874);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390ZM349 152C349 46 394 -11 477 -11C532 -11 592 15 637 57C699 116 743 230 743 331C743 425 693 482 610 482C506 482 349 382 349 152ZM573 444C634 444 667 399 667 315C667 219 636 113 592 60C574 39 548 27 517 27C459 27 425 72 425 151C425 264 464 387 513 427C526 438 549 444 573 444ZM1015 722L1003 733C951 707 915 698 843 691L839 670L887 670C911 670 921 663 921 646C921 638 920 629 919 622L871 354C857 279 841 210 789 1L798 -9L865 8L888 132C897 180 921 225 955 259C1024 59 1056 -9 1081 -9C1094 -9 1118 4 1162 35L1208 67L1200 86L1157 62C1143 54 1136 52 1128 52C1118 52 1111 58 1102 75C1067 139 1045 193 1006 316L1020 330C1081 391 1127 416 1177 416C1185 416 1196 414 1213 410L1230 473C1212 479 1194 482 1182 482C1116 482 1033 405 908 230ZM1571 111L1547 94C1494 56 1446 36 1410 36C1363 36 1334 73 1334 133C1334 158 1337 185 1342 214C1359 218 1468 248 1493 259C1578 296 1617 342 1617 404C1617 451 1583 482 1533 482C1465 496 1355 423 1318 349C1288 299 1258 180 1258 113C1258 35 1302 -11 1374 -11C1431 -11 1487 17 1579 92ZM1356 274C1373 343 1393 386 1422 412C1440 428 1471 440 1495 440C1524 440 1543 420 1543 388C1543 344 1508 297 1456 272C1428 258 1392 247 1347 237ZM1655 388L1662 368L1694 389C1731 412 1734 414 1741 414C1752 414 1759 404 1759 389C1759 338 1718 145 1677 2L1684 -9C1709 -2 1732 4 1754 8C1773 134 1794 199 1840 268C1894 352 1969 414 2014 414C2025 414 2031 405 2031 390C2031 372 2028 351 2020 319L1968 107C1959 70 1955 47 1955 31C1955 6 1966 -9 1985 -9C2011 -9 2047 12 2145 85L2135 103L2109 86C2080 67 2058 56 2048 56C2041 56 2035 65 2035 76C2035 81 2036 92 2037 96L2103 372C2110 401 2114 429 2114 446C2114 469 2103 482 2083 482C2041 482 1972 444 1913 389C1875 354 1847 320 1795 247L1833 408C1837 426 1839 438 1839 449C1839 470 1831 482 1816 482C1795 482 1756 460 1683 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,492.6458740234375,221.64668701171874);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,500.78125,221.64668701171874);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,515.1041870117188,225.96934350585937);"><path d="M462 224C462 345 355 366 308 374C388 436 418 482 418 541C418 630 344 689 233 689C165 689 120 670 72 622L43 498L74 498L92 554C103 588 166 622 218 622C283 622 336 569 336 506C336 431 277 368 206 368C198 368 187 369 174 370L159 371L147 318L154 312C192 329 211 334 238 334C321 334 369 281 369 190C369 88 308 21 215 21C169 21 128 36 98 64C74 86 61 109 42 163L15 153C36 92 44 56 50 6C103 -12 147 -20 184 -20C307 -20 462 87 462 224Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,525.125,221.64668701171874);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,540.6041870117188,221.64668701171874);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,564.3021240234375,221.64668701171874);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,573.65625,221.64668701171874);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,582.5208740234375,225.96934350585937);"><path d="M16 23L16 -3C203 -3 203 0 239 0C275 0 275 -3 468 -3L468 82C353 77 307 81 122 77L304 270C401 373 431 428 431 503C431 618 353 689 226 689C154 689 105 669 56 619L39 483L68 483L81 529C97 587 133 612 200 612C286 612 341 558 341 473C341 398 299 324 186 204Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,592.5416870117188,221.64668701171874);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,603.541748046875,221.64668701171874);"><path d="M374 390L318 107C317 99 305 61 305 31C305 6 316 -9 335 -9C370 -9 405 11 483 74L514 99L504 117L459 86C430 66 410 56 399 56C390 56 385 64 385 76C385 102 399 183 428 328L441 390L548 390L559 440C521 436 487 434 449 434C465 528 476 577 494 631L483 646C463 634 436 622 405 610L380 440C336 419 310 408 292 403L290 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,618.5,225.96934350585937);"><path d="M131 331C152 512 241 611 421 665L379 689C283 657 242 637 191 593C88 506 32 384 32 247C32 82 112 -20 241 -20C371 -20 468 83 468 219C468 334 399 409 293 409C216 409 184 370 131 331ZM255 349C331 349 382 283 382 184C382 80 331 13 254 13C169 13 123 86 123 220C123 255 127 274 138 291C160 325 207 349 255 349Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,629.7396240234375,221.64668701171874);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g></g><g><g><g><g><g><g style="transform:matrix(1,0,0,1,439.125,281.64668701171877);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390ZM349 152C349 46 394 -11 477 -11C532 -11 592 15 637 57C699 116 743 230 743 331C743 425 693 482 610 482C506 482 349 382 349 152ZM573 444C634 444 667 399 667 315C667 219 636 113 592 60C574 39 548 27 517 27C459 27 425 72 425 151C425 264 464 387 513 427C526 438 549 444 573 444ZM1015 722L1003 733C951 707 915 698 843 691L839 670L887 670C911 670 921 663 921 646C921 638 920 629 919 622L871 354C857 279 841 210 789 1L798 -9L865 8L888 132C897 180 921 225 955 259C1024 59 1056 -9 1081 -9C1094 -9 1118 4 1162 35L1208 67L1200 86L1157 62C1143 54 1136 52 1128 52C1118 52 1111 58 1102 75C1067 139 1045 193 1006 316L1020 330C1081 391 1127 416 1177 416C1185 416 1196 414 1213 410L1230 473C1212 479 1194 482 1182 482C1116 482 1033 405 908 230ZM1571 111L1547 94C1494 56 1446 36 1410 36C1363 36 1334 73 1334 133C1334 158 1337 185 1342 214C1359 218 1468 248 1493 259C1578 296 1617 342 1617 404C1617 451 1583 482 1533 482C1465 496 1355 423 1318 349C1288 299 1258 180 1258 113C1258 35 1302 -11 1374 -11C1431 -11 1487 17 1579 92ZM1356 274C1373 343 1393 386 1422 412C1440 428 1471 440 1495 440C1524 440 1543 420 1543 388C1543 344 1508 297 1456 272C1428 258 1392 247 1347 237ZM1655 388L1662 368L1694 389C1731 412 1734 414 1741 414C1752 414 1759 404 1759 389C1759 338 1718 145 1677 2L1684 -9C1709 -2 1732 4 1754 8C1773 134 1794 199 1840 268C1894 352 1969 414 2014 414C2025 414 2031 405 2031 390C2031 372 2028 351 2020 319L1968 107C1959 70 1955 47 1955 31C1955 6 1966 -9 1985 -9C2011 -9 2047 12 2145 85L2135 103L2109 86C2080 67 2058 56 2048 56C2041 56 2035 65 2035 76C2035 81 2036 92 2037 96L2103 372C2110 401 2114 429 2114 446C2114 469 2103 482 2083 482C2041 482 1972 444 1913 389C1875 354 1847 320 1795 247L1833 408C1837 426 1839 438 1839 449C1839 470 1831 482 1816 482C1795 482 1756 460 1683 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,492.6458740234375,281.64668701171877);"><path d="M146 266C146 526 243 632 301 700L282 726C225 675 60 542 60 266C60 159 85 58 133 -32C168 -99 200 -138 282 -215L301 -194C255 -137 146 -15 146 266Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,500.78125,281.64668701171877);"><path d="M24 388L31 368L63 389C100 412 103 414 110 414C121 414 128 404 128 389C128 338 87 145 46 2L53 -9C78 -2 101 4 123 8C142 134 163 199 209 268C263 352 338 414 383 414C394 414 400 405 400 390C400 372 397 351 389 319L337 107C328 70 324 47 324 31C324 6 335 -9 354 -9C380 -9 416 12 514 85L504 103L478 86C449 67 427 56 417 56C410 56 404 65 404 76C404 81 405 92 406 96L472 372C479 401 483 429 483 446C483 469 472 482 452 482C410 482 341 444 282 389C244 354 216 320 164 247L202 408C206 426 208 438 208 449C208 470 200 482 185 482C164 482 125 460 52 408Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,515.1041870117188,285.9693435058594);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,525.125,281.64668701171877);"><path d="M51 726L32 700C87 636 187 526 187 266C187 -10 83 -131 32 -194L51 -215C104 -165 273 -23 273 265C273 542 108 675 51 726Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,540.6041870117188,281.64668701171877);"><path d="M604 347L604 406L65 406L65 347ZM604 134L604 193L65 193L65 134Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,564.3021240234375,281.64668701171877);"><path d="M289 -175C226 -161 206 -117 206 -45L206 128C206 207 197 253 125 272L125 274C194 292 206 335 206 409L206 595C206 667 224 707 289 726C189 726 134 703 134 578L134 392C134 327 120 292 58 273C124 254 134 223 134 151L134 -17C134 -149 176 -175 289 -175Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,573.65625,281.64668701171877);"><path d="M125 390L69 107C68 99 56 61 56 31C56 6 67 -9 86 -9C121 -9 156 11 234 74L265 99L255 117L210 86C181 66 161 56 150 56C141 56 136 64 136 76C136 102 150 183 179 328L192 390L299 390L310 440C272 436 238 434 200 434C216 528 227 577 245 631L234 646C214 634 187 622 156 610L131 440C87 419 61 408 43 403L41 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,582.5208740234375,285.9693435058594);"><path d="M280 181L280 106C280 46 269 32 220 30L158 27L158 -3C291 0 291 0 315 0C339 0 339 0 472 -3L472 27L424 30C375 33 364 46 364 106L364 181C423 181 444 180 472 177L472 248L364 245L365 697L285 667L2 204L2 181ZM280 245L65 245L280 597Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,592.5416870117188,281.64668701171877);"><path d="M204 123C177 114 159 108 106 93C99 17 74 -48 16 -144L30 -155L71 -136C152 -31 190 32 218 109Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g style="transform:matrix(1,0,0,1,603.541748046875,281.64668701171877);"><path d="M374 390L318 107C317 99 305 61 305 31C305 6 316 -9 335 -9C370 -9 405 11 483 74L514 99L504 117L459 86C430 66 410 56 399 56C390 56 385 64 385 76C385 102 399 183 428 328L441 390L548 390L559 440C521 436 487 434 449 434C465 528 476 577 494 631L483 646C463 634 436 622 405 610L380 440C336 419 310 408 292 403L290 390Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g><g><g><g><g style="transform:matrix(1,0,0,1,618.5,285.9693435058594);"><path d="M168 345C127 326 110 315 88 294C50 256 30 211 30 159C30 56 113 -20 226 -20C356 -20 464 82 464 206C464 286 427 329 313 381C404 443 436 485 436 545C436 631 365 689 259 689C140 689 53 613 53 508C53 440 80 402 168 345ZM284 295C347 267 385 218 385 164C385 80 322 14 241 14C156 14 101 74 101 167C101 240 130 286 204 331ZM223 423C160 454 128 494 128 544C128 610 176 655 247 655C320 655 368 607 368 534C368 477 343 438 278 396Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.017136,0,0,-0.017136,0,0);"></path></g></g></g></g><g style="transform:matrix(1,0,0,1,629.7396240234375,281.64668701171877);"><path d="M275 273C213 292 199 327 199 392L199 578C199 703 144 726 44 726C109 707 127 667 127 595L127 409C127 335 139 292 208 274L208 272C136 253 127 207 127 128L127 -45C127 -117 107 -161 44 -175C157 -175 199 -149 199 -17L199 151C199 223 209 254 275 273Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.024480000000000002,0,0,-0.024480000000000002,0,0);"></path></g></g></g></g></g></g><g><g><g style="transform:matrix(1,0,0,1,471.734375,61.95);"><path d="M35 623L177 623L295 624L295 0L384 0L384 624L501 623L643 623L643 689L35 689ZM627 221C627 85 732 -11 846 -11C960 -11 1066 84 1066 220C1066 345 974 461 846 461C728 461 627 357 627 221ZM705 231C705 352 782 399 846 399C910 399 987 353 987 231C987 106 913 53 846 53C780 53 705 105 705 231ZM1180 0L1250 0L1250 142L1330 224L1486 0L1568 0L1378 273L1546 445L1456 445L1252 236L1252 722L1180 722ZM1592 226C1592 102 1679 -11 1810 -11C1873 -11 1925 11 1968 40L1962 106C1918 74 1873 51 1810 51C1734 51 1666 117 1662 220L1972 220L1972 249C1963 420 1865 461 1794 461C1688 461 1592 360 1592 226ZM1667 275C1678 324 1718 399 1794 399C1823 399 1898 386 1915 275ZM2082 0L2160 0L2160 240C2160 314 2185 394 2262 394C2360 394 2360 319 2360 281L2360 0L2438 0L2438 288C2438 358 2429 455 2301 455C2213 455 2169 398 2155 380L2155 450L2082 450ZM2952 0L3030 0L3030 624C3039 580 3063 517 3102 411L3252 22L3324 22L3478 423L3538 587L3548 624L3549 592L3549 0L3627 0L3627 694L3512 694L3358 292C3309 164 3293 114 3290 92L3289 92C3285 115 3261 182 3220 293L3066 694L2952 694ZM3771 113C3771 67 3801 -11 3885 -11C3995 -11 4050 34 4051 35L4051 0L4127 0L4127 305C4122 389 4056 461 3961 461C3898 461 3852 445 3804 417L3810 350C3843 373 3885 401 3960 401C3973 401 3999 400 4023 371C4048 341 4048 307 4049 279L4049 245C3946 244 3771 219 3771 113ZM3844 114C3844 177 3977 192 4049 193L4049 130C4049 65 3980 51 3937 51C3883 51 3844 78 3844 114ZM4288 -195L4366 -195L4366 46C4399 15 4442 -11 4501 -11C4598 -11 4687 83 4687 224C4687 343 4624 455 4529 455C4495 455 4427 448 4363 396L4363 445L4288 445ZM4366 126L4366 334C4372 341 4406 391 4472 391C4554 391 4608 310 4608 222C4608 116 4533 50 4463 50C4404 50 4366 102 4366 126Z" stroke="rgb(0, 0, 0)" stroke-width="8" fill="rgb(0, 0, 0)" style="transform:matrix(0.02595,0,0,-0.02595,0,0);"></path></g></g></g></svg>
+</svg>
diff --git a/doc/modules/cassandra/pages/architecture/index.adoc b/doc/modules/cassandra/pages/architecture/index.adoc
new file mode 100644
index 0000000..c4bef05
--- /dev/null
+++ b/doc/modules/cassandra/pages/architecture/index.adoc
@@ -0,0 +1,9 @@
+= Architecture
+
+This section describes the general architecture of Apache Cassandra.
+
+* xref:architecture/overview.adoc[Overview]
+* xref:architecture/dynamo.adoc[Dynamo]
+* xref:architecture/storage_engine.adoc[Storage Engine]
+* xref:architecture/guarantees.adoc[Guarantees]
+* xref:architecture/snitch.adoc[Snitches]
diff --git a/doc/modules/cassandra/pages/architecture/overview.adoc b/doc/modules/cassandra/pages/architecture/overview.adoc
new file mode 100644
index 0000000..605e347
--- /dev/null
+++ b/doc/modules/cassandra/pages/architecture/overview.adoc
@@ -0,0 +1,101 @@
+= Overview
+:exper: experimental
+
+Apache Cassandra is an open source, distributed, NoSQL database. It
+presents a partitioned wide column storage model with eventually
+consistent semantics.
+
+Apache Cassandra was initially designed at
+https://www.cs.cornell.edu/projects/ladis2009/papers/lakshman-ladis2009.pdf[Facebook]
+using a staged event-driven architecture
+(http://www.sosp.org/2001/papers/welsh.pdf[SEDA]) to implement a
+combination of Amazon’s
+http://courses.cse.tamu.edu/caverlee/csce438/readings/dynamo-paper.pdf[Dynamo]
+distributed storage and replication techniques and Google's
+https://static.googleusercontent.com/media/research.google.com/en//archive/bigtable-osdi06.pdf[Bigtable]
+data and storage engine model. Dynamo and Bigtable were both developed
+to meet emerging requirements for scalable, reliable and highly
+available storage systems, but each had areas that could be improved.
+
+Cassandra was designed as a best-in-class combination of both systems to
+meet emerging largescale, both in data footprint and query volume,
+storage requirements. As applications began to require full global
+replication and always available low-latency reads and writes, it became
+imperative to design a new kind of database model as the relational
+database systems of the time struggled to meet the new requirements of
+global scale applications.
+
+Systems like Cassandra are designed for these challenges and seek the
+following design objectives:
+
+* Full multi-master database replication
+* Global availability at low latency
+* Scaling out on commodity hardware
+* Linear throughput increase with each additional processor
+* Online load balancing and cluster growth
+* Partitioned key-oriented queries
+* Flexible schema
+
+== Features
+
+Cassandra provides the Cassandra Query Language (xref:cql/ddl.adoc[CQL]), an SQL-like
+language, to create and update database schema and access data. CQL
+allows users to organize data within a cluster of Cassandra nodes using:
+
+* *Keyspace*: Defines how a dataset is replicated, per datacenter. 
+Replication is the number of copies saved per cluster.
+Keyspaces contain tables.
+* *Table*: Defines the typed schema for a collection of partitions.
+Tables contain partitions, which contain rows, which contain columns.
+Cassandra tables can flexibly add new columns to tables with zero downtime. 
+* *Partition*: Defines the mandatory part of the primary key all rows in
+Cassandra must have to identify the node in a cluster where the row is stored. 
+All performant queries supply the partition key in the query.
+* *Row*: Contains a collection of columns identified by a unique primary
+key made up of the partition key and optionally additional clustering
+keys.
+* *Column*: A single datum with a type which belongs to a row.
+
+CQL supports numerous advanced features over a partitioned dataset such
+as:
+
+* Single partition lightweight transactions with atomic compare and set
+semantics.
+* User-defined types, functions and aggregates
+* Collection types including sets, maps, and lists.
+* Local secondary indices
+* (Experimental) materialized views
+
+Cassandra explicitly chooses not to implement operations that require
+cross partition coordination as they are typically slow and hard to
+provide highly available global semantics. For example Cassandra does
+not support:
+
+* Cross partition transactions
+* Distributed joins
+* Foreign keys or referential integrity.
+
+== Operating
+
+Apache Cassandra configuration settings are configured in the
+`cassandra.yaml` file that can be edited by hand or with the aid of
+configuration management tools. Some settings can be manipulated live
+using an online interface, but others require a restart of the database
+to take effect.
+
+Cassandra provides tools for managing a cluster. The `nodetool` command
+interacts with Cassandra's live control interface, allowing runtime
+manipulation of many settings from `cassandra.yaml`. The
+`auditlogviewer` is used to view the audit logs. The `fqltool` is used
+to view, replay and compare full query logs. The `auditlogviewer` and
+`fqltool` are new tools in Apache Cassandra {40_version}.
+
+In addition, Cassandra supports out of the box atomic snapshot
+functionality, which presents a point in time snapshot of Cassandra's
+data for easy integration with many backup tools. Cassandra also
+supports incremental backups where data can be backed up as it is
+written.
+
+Apache Cassandra {40_version} has added several new features including virtual
+tables, transient replication ({exper}), audit logging, full query logging, and
+support for Java 11 ({exper}). 
diff --git a/doc/modules/cassandra/pages/architecture/snitch.adoc b/doc/modules/cassandra/pages/architecture/snitch.adoc
new file mode 100644
index 0000000..90b32fb
--- /dev/null
+++ b/doc/modules/cassandra/pages/architecture/snitch.adoc
@@ -0,0 +1,74 @@
+= Snitch
+
+In cassandra, the snitch has two functions:
+
+* it teaches Cassandra enough about your network topology to route
+requests efficiently.
+* it allows Cassandra to spread replicas around your cluster to avoid
+correlated failures. It does this by grouping machines into
+"datacenters" and "racks." Cassandra will do its best not to have more
+than one replica on the same "rack" (which may not actually be a
+physical location).
+
+== Dynamic snitching
+
+The dynamic snitch monitor read latencies to avoid reading from hosts
+that have slowed down. The dynamic snitch is configured with the
+following properties on `cassandra.yaml`:
+
+* `dynamic_snitch`: whether the dynamic snitch should be enabled or
+disabled.
+* `dynamic_snitch_update_interval_in_ms`: controls how often to perform
+the more expensive part of host score calculation.
+* `dynamic_snitch_reset_interval_in_ms`: if set greater than zero, this
+will allow 'pinning' of replicas to hosts in order to increase cache
+capacity.
+* `dynamic_snitch_badness_threshold:`: The badness threshold will
+control how much worse the pinned host has to be before the dynamic
+snitch will prefer other replicas over it. This is expressed as a double
+which represents a percentage. Thus, a value of 0.2 means Cassandra
+would continue to prefer the static snitch values until the pinned host
+was 20% worse than the fastest.
+
+== Snitch classes
+
+The `endpoint_snitch` parameter in `cassandra.yaml` should be set to the
+class that implements `IEndPointSnitch` which will be wrapped by the
+dynamic snitch and decide if two endpoints are in the same data center
+or on the same rack. Out of the box, Cassandra provides the snitch
+implementations:
+
+GossipingPropertyFileSnitch::
+  This should be your go-to snitch for production use. The rack and
+  datacenter for the local node are defined in
+  cassandra-rackdc.properties and propagated to other nodes via gossip.
+  If `cassandra-topology.properties` exists, it is used as a fallback,
+  allowing migration from the PropertyFileSnitch.
+SimpleSnitch::
+  Treats Strategy order as proximity. This can improve cache locality
+  when disabling read repair. Only appropriate for single-datacenter
+  deployments.
+PropertyFileSnitch::
+  Proximity is determined by rack and data center, which are explicitly
+  configured in `cassandra-topology.properties`.
+Ec2Snitch::
+  Appropriate for EC2 deployments in a single Region, or in multiple
+  regions with inter-region VPC enabled (available since the end of
+  2017, see
+  https://aws.amazon.com/about-aws/whats-new/2017/11/announcing-support-for-inter-region-vpc-peering/[AWS
+  announcement]). Loads Region and Availability Zone information from
+  the EC2 API. The Region is treated as the datacenter, and the
+  Availability Zone as the rack. Only private IPs are used, so this will
+  work across multiple regions only if inter-region VPC is enabled.
+Ec2MultiRegionSnitch::
+  Uses public IPs as broadcast_address to allow cross-region
+  connectivity (thus, you should set seed addresses to the public IP as
+  well). You will need to open the `storage_port` or `ssl_storage_port`
+  on the public IP firewall (For intra-Region traffic, Cassandra will
+  switch to the private IP after establishing a connection).
+RackInferringSnitch::
+  Proximity is determined by rack and data center, which are assumed to
+  correspond to the 3rd and 2nd octet of each node's IP address,
+  respectively. Unless this happens to match your deployment
+  conventions, this is best used as an example of writing a custom
+  Snitch class and is provided in that spirit.
diff --git a/doc/modules/cassandra/pages/architecture/storage_engine.adoc b/doc/modules/cassandra/pages/architecture/storage_engine.adoc
new file mode 100644
index 0000000..77c52e5
--- /dev/null
+++ b/doc/modules/cassandra/pages/architecture/storage_engine.adoc
@@ -0,0 +1,225 @@
+= Storage Engine
+
+[[commit-log]]
+== CommitLog
+
+Commitlogs are an append only log of all mutations local to a Cassandra
+node. Any data written to Cassandra will first be written to a commit
+log before being written to a memtable. This provides durability in the
+case of unexpected shutdown. On startup, any mutations in the commit log
+will be applied to memtables.
+
+All mutations write optimized by storing in commitlog segments, reducing
+the number of seeks needed to write to disk. Commitlog Segments are
+limited by the `commitlog_segment_size_in_mb` option, once the size is
+reached, a new commitlog segment is created. Commitlog segments can be
+archived, deleted, or recycled once all its data has been flushed to
+SSTables. Commitlog segments are truncated when Cassandra has written
+data older than a certain point to the SSTables. Running "nodetool
+drain" before stopping Cassandra will write everything in the memtables
+to SSTables and remove the need to sync with the commitlogs on startup.
+
+* `commitlog_segment_size_in_mb`: The default size is 32, which is
+almost always fine, but if you are archiving commitlog segments (see
+commitlog_archiving.properties), then you probably want a finer
+granularity of archiving; 8 or 16 MB is reasonable. Max mutation size is
+also configurable via `max_mutation_size_in_kb` setting in `cassandra.yaml`.
+The default is half the size `commitlog_segment_size_in_mb * 1024`.
+
+**NOTE: If `max_mutation_size_in_kb` is set explicitly then
+`commitlog_segment_size_in_mb` must be set to at least twice the size of
+`max_mutation_size_in_kb / 1024`**.
+
+Commitlogs are an append only log of all mutations local to a Cassandra
+node. Any data written to Cassandra will first be written to a commit
+log before being written to a memtable. This provides durability in the
+case of unexpected shutdown. On startup, any mutations in the commit log
+will be applied.
+
+* `commitlog_sync`: may be either _periodic_ or _batch_.
+** `batch`: In batch mode, Cassandra won’t ack writes until the commit
+log has been fsynced to disk. It will wait
+"commitlog_sync_batch_window_in_ms" milliseconds between fsyncs. This
+window should be kept short because the writer threads will be unable to
+do extra work while waiting. You may need to increase concurrent_writes
+for the same reason.
++
+- `commitlog_sync_batch_window_in_ms`: Time to wait between "batch"
+fsyncs _Default Value:_ 2
+** `periodic`: In periodic mode, writes are immediately ack'ed, and the
+CommitLog is simply synced every "commitlog_sync_period_in_ms"
+milliseconds.
++
+- `commitlog_sync_period_in_ms`: Time to wait between "periodic" fsyncs
+_Default Value:_ 10000
+
+_Default Value:_ batch
+
+** NOTE: In the event of an unexpected shutdown, Cassandra can lose up
+to the sync period or more if the sync is delayed. If using "batch"
+mode, it is recommended to store commitlogs in a separate, dedicated
+device.*
+
+* `commitlog_directory`: This option is commented out by default When
+running on magnetic HDD, this should be a separate spindle than the data
+directories. If not set, the default directory is
+$CASSANDRA_HOME/data/commitlog.
+
+_Default Value:_ /var/lib/cassandra/commitlog
+
+* `commitlog_compression`: Compression to apply to the commitlog. If
+omitted, the commit log will be written uncompressed. LZ4, Snappy,
+Deflate and Zstd compressors are supported.
+
+(Default Value: (complex option):
+
+[source, yaml]
+----
+#   - class_name: LZ4Compressor
+#     parameters:
+----
+
+* `commitlog_total_space_in_mb`: Total space to use for commit logs on
+disk.
+
+If space gets above this value, Cassandra will flush every dirty CF in
+the oldest segment and remove it. So a small total commitlog space will
+tend to cause more flush activity on less-active columnfamilies.
+
+The default value is the smaller of 8192, and 1/4 of the total space of
+the commitlog volume.
+
+_Default Value:_ 8192
+
+== Memtables
+
+Memtables are in-memory structures where Cassandra buffers writes. In
+general, there is one active memtable per table. Eventually, memtables
+are flushed onto disk and become immutable link:#sstables[SSTables].
+This can be triggered in several ways:
+
+* The memory usage of the memtables exceeds the configured threshold
+(see `memtable_cleanup_threshold`)
+* The `commit-log` approaches its maximum size, and forces memtable
+flushes in order to allow commitlog segments to be freed
+
+Memtables may be stored entirely on-heap or partially off-heap,
+depending on `memtable_allocation_type`.
+
+== SSTables
+
+SSTables are the immutable data files that Cassandra uses for persisting
+data on disk.
+
+As SSTables are flushed to disk from `memtables` or are streamed from
+other nodes, Cassandra triggers compactions which combine multiple
+SSTables into one. Once the new SSTable has been written, the old
+SSTables can be removed.
+
+Each SSTable is comprised of multiple components stored in separate
+files:
+
+`Data.db`::
+  The actual data, i.e. the contents of rows.
+`Index.db`::
+  An index from partition keys to positions in the `Data.db` file. For
+  wide partitions, this may also include an index to rows within a
+  partition.
+`Summary.db`::
+  A sampling of (by default) every 128th entry in the `Index.db` file.
+`Filter.db`::
+  A Bloom Filter of the partition keys in the SSTable.
+`CompressionInfo.db`::
+  Metadata about the offsets and lengths of compression chunks in the
+  `Data.db` file.
+`Statistics.db`::
+  Stores metadata about the SSTable, including information about
+  timestamps, tombstones, clustering keys, compaction, repair,
+  compression, TTLs, and more.
+`Digest.crc32`::
+  A CRC-32 digest of the `Data.db` file.
+`TOC.txt`::
+  A plain text list of the component files for the SSTable.
+
+Within the `Data.db` file, rows are organized by partition. These
+partitions are sorted in token order (i.e. by a hash of the partition
+key when the default partitioner, `Murmur3Partition`, is used). Within a
+partition, rows are stored in the order of their clustering keys.
+
+SSTables can be optionally compressed using block-based compression.
+
+== SSTable Versions
+
+This section was created using the following
+https://gist.github.com/shyamsalimkumar/49a61e5bc6f403d20c55[gist] which
+utilized this original
+http://www.bajb.net/2013/03/cassandra-sstable-format-version-numbers/[source].
+
+The version numbers, to date are:
+
+=== Version 0
+
+* b (0.7.0): added version to sstable filenames
+* c (0.7.0): bloom filter component computes hashes over raw key bytes
+instead of strings
+* d (0.7.0): row size in data component becomes a long instead of int
+* e (0.7.0): stores undecorated keys in data and index components
+* f (0.7.0): switched bloom filter implementations in data component
+* g (0.8): tracks flushed-at context in metadata component
+
+=== Version 1
+
+* h (1.0): tracks max client timestamp in metadata component
+* hb (1.0.3): records compression ration in metadata component
+* hc (1.0.4): records partitioner in metadata component
+* hd (1.0.10): includes row tombstones in maxtimestamp
+* he (1.1.3): includes ancestors generation in metadata component
+* hf (1.1.6): marker that replay position corresponds to 1.1.5+
+millis-based id (see CASSANDRA-4782)
+* ia (1.2.0):
+** column indexes are promoted to the index file
+** records estimated histogram of deletion times in tombstones
+** bloom filter (keys and columns) upgraded to Murmur3
+* ib (1.2.1): tracks min client timestamp in metadata component
+* ic (1.2.5): omits per-row bloom filter of column names
+
+=== Version 2
+
+* ja (2.0.0):
+** super columns are serialized as composites (note that there is no
+real format change, this is mostly a marker to know if we should expect
+super columns or not. We do need a major version bump however, because
+we should not allow streaming of super columns into this new format)
+** tracks max local deletiontime in sstable metadata
+** records bloom_filter_fp_chance in metadata component
+** remove data size and column count from data file (CASSANDRA-4180)
+** tracks max/min column values (according to comparator)
+* jb (2.0.1):
+** switch from crc32 to adler32 for compression checksums
+** checksum the compressed data
+* ka (2.1.0):
+** new Statistics.db file format
+** index summaries can be downsampled and the sampling level is
+persisted
+** switch uncompressed checksums to adler32
+** tracks presense of legacy (local and remote) counter shards
+* la (2.2.0): new file name format
+* lb (2.2.7): commit log lower bound included
+
+=== Version 3
+
+* ma (3.0.0):
+** swap bf hash order
+** store rows natively
+* mb (3.0.7, 3.7): commit log lower bound included
+* mc (3.0.8, 3.9): commit log intervals included
+
+=== Example Code
+
+The following example is useful for finding all sstables that do not
+match the "ib" SSTable version
+
+[source,bash]
+----
+include:example$find_sstables.sh[]
+----
diff --git a/doc/modules/cassandra/pages/configuration/cass_cl_archive_file.adoc b/doc/modules/cassandra/pages/configuration/cass_cl_archive_file.adoc
new file mode 100644
index 0000000..392a10e
--- /dev/null
+++ b/doc/modules/cassandra/pages/configuration/cass_cl_archive_file.adoc
@@ -0,0 +1,60 @@
+= commitlog-archiving.properties file
+
+The `commitlog-archiving.properties` configuration file can optionally
+set commands that are executed when archiving or restoring a commitlog
+segment.
+
+== Options
+
+=== `archive_command`
+
+One command can be inserted with `%path`
+and `%name` arguments. `%path` is the fully qualified path of the commitlog
+segment to archive. `%name` is the filename of the commitlog. `STDOUT`,
+`STDIN`, or multiple commands cannot be executed. If multiple commands are
+required, add a pointer to a script in this option.
+
+*Example:* archive_command=/bin/ln %path /backup/%name
+
+*Default value:* blank
+
+=== `restore_command`
+
+One command can be inserted with `%from`
+and `%to` arguments. `%from` is the fully qualified path to an archived
+commitlog segment using the specified restore directories. `%to` defines
+the directory to the live commitlog location.
+
+*Example:* restore_command=/bin/cp -f %from %to
+
+*Default value:* blank
+
+=== `restore_directories`
+
+Defines the directory to scan the recovery files into.
+
+*Example:* restore_directories=/path/to/restore_dir_location
+
+*Default value:* blank
+
+=== `restore_point_in_time`
+
+Restore mutations created up
+to and including this timestamp in GMT in the format
+`yyyy:MM:dd HH:mm:ss`. Recovery will continue through the segment when
+the first client-supplied timestamp greater than this time is
+encountered, but only mutations less than or equal to this timestamp
+will be applied.
+
+*Example:* restore_point_in_time=2020:04:31 20:43:12
+
+*Default value:* blank
+
+=== `precision`
+
+Precision of the timestamp used
+in the inserts. Choice is generally MILLISECONDS or MICROSECONDS
+
+*Example:* precision=MICROSECONDS
+
+*Default value:* MICROSECONDS
diff --git a/doc/modules/cassandra/pages/configuration/cass_env_sh_file.adoc b/doc/modules/cassandra/pages/configuration/cass_env_sh_file.adoc
new file mode 100644
index 0000000..d895186
--- /dev/null
+++ b/doc/modules/cassandra/pages/configuration/cass_env_sh_file.adoc
@@ -0,0 +1,162 @@
+= cassandra-env.sh file
+
+The `cassandra-env.sh` bash script file can be used to pass additional
+options to the Java virtual machine (JVM), such as maximum and minimum
+heap size, rather than setting them in the environment. If the JVM
+settings are static and do not need to be computed from the node
+characteristics, the `cassandra-jvm-options` files should be used
+instead. For example, commonly computed values are the heap sizes, using
+the system values.
+
+For example, add
+`JVM_OPTS="$JVM_OPTS -Dcassandra.load_ring_state=false"` to the
+`cassandra_env.sh` file and run the command-line `cassandra` to start.
+The option is set from the `cassandra-env.sh` file, and is equivalent to
+starting Cassandra with the command-line option
+`cassandra -Dcassandra.load_ring_state=false`.
+
+The `-D` option specifies the start-up parameters in both the command
+line and `cassandra-env.sh` file. The following options are available:
+
+== `cassandra.auto_bootstrap=false`
+
+Facilitates setting auto_bootstrap to false on initial set-up of the
+cluster. The next time you start the cluster, you do not need to change
+the `cassandra.yaml` file on each node to revert to true, the default
+value.
+
+== `cassandra.available_processors=<number_of_processors>`
+
+In a multi-instance deployment, multiple Cassandra instances will
+independently assume that all CPU processors are available to it. This
+setting allows you to specify a smaller set of processors.
+
+== `cassandra.boot_without_jna=true`
+
+If JNA fails to initialize, Cassandra fails to boot. Use this command to
+boot Cassandra without JNA.
+
+== `cassandra.config=<directory>`
+
+The directory location of the `cassandra.yaml file`. The default
+location depends on the type of installation.
+
+== `cassandra.ignore_dynamic_snitch_severity=true|false`
+
+Setting this property to true causes the dynamic snitch to ignore the
+severity indicator from gossip when scoring nodes. Explore failure
+detection and recovery and dynamic snitching for more information.
+
+*Default:* false
+
+== `cassandra.initial_token=<token>`
+
+Use when virtual nodes (vnodes) are not used. Sets the initial
+partitioner token for a node the first time the node is started. Note:
+Vnodes are highly recommended as they automatically select tokens.
+
+*Default:* disabled
+
+== `cassandra.join_ring=true|false`
+
+Set to false to start Cassandra on a node but not have the node join the
+cluster. You can use `nodetool join` and a JMX call to join the ring
+afterwards.
+
+*Default:* true
+
+== `cassandra.load_ring_state=true|false`
+
+Set to false to clear all gossip state for the node on restart.
+
+*Default:* true
+
+== `cassandra.metricsReporterConfigFile=<filename>`
+
+Enable pluggable metrics reporter. Explore pluggable metrics reporting
+for more information.
+
+== `cassandra.partitioner=<partitioner>`
+
+Set the partitioner.
+
+*Default:* org.apache.cassandra.dht.Murmur3Partitioner
+
+== `cassandra.prepared_statements_cache_size_in_bytes=<cache_size>`
+
+Set the cache size for prepared statements.
+
+== `cassandra.replace_address=<listen_address of dead node>|<broadcast_address of dead node>`
+
+To replace a node that has died, restart a new node in its place
+specifying the `listen_address` or `broadcast_address` that the new node
+is assuming. The new node must not have any data in its data directory,
+the same state as before bootstrapping. Note: The `broadcast_address`
+defaults to the `listen_address` except when using the
+`Ec2MultiRegionSnitch`.
+
+== `cassandra.replayList=<table>`
+
+Allow restoring specific tables from an archived commit log.
+
+== `cassandra.ring_delay_ms=<number_of_ms>`
+
+Defines the amount of time a node waits to hear from other nodes before
+formally joining the ring.
+
+*Default:* 1000ms
+
+== `cassandra.native_transport_port=<port>`
+
+Set the port on which the CQL native transport listens for clients.
+
+*Default:* 9042
+
+== `cassandra.rpc_port=<port>`
+
+Set the port for the Thrift RPC service, which is used for client
+connections.
+
+*Default:* 9160
+
+== `cassandra.storage_port=<port>`
+
+Set the port for inter-node communication.
+
+*Default:* 7000
+
+== `cassandra.ssl_storage_port=<port>`
+
+Set the SSL port for encrypted communication.
+
+*Default:* 7001
+
+== `cassandra.start_native_transport=true|false`
+
+Enable or disable the native transport server. See
+`start_native_transport` in `cassandra.yaml`.
+
+*Default:* true
+
+== `cassandra.start_rpc=true|false`
+
+Enable or disable the Thrift RPC server.
+
+*Default:* true
+
+== `cassandra.triggers_dir=<directory>`
+
+Set the default location for the trigger JARs.
+
+*Default:* conf/triggers
+
+== `cassandra.write_survey=true`
+
+For testing new compaction and compression strategies. It allows you to
+experiment with different strategies and benchmark write performance
+differences without affecting the production workload.
+
+== `consistent.rangemovement=true|false`
+
+Set to true makes Cassandra perform bootstrap safely without violating
+consistency. False disables this.
diff --git a/doc/modules/cassandra/pages/configuration/cass_jvm_options_file.adoc b/doc/modules/cassandra/pages/configuration/cass_jvm_options_file.adoc
new file mode 100644
index 0000000..b9a312c
--- /dev/null
+++ b/doc/modules/cassandra/pages/configuration/cass_jvm_options_file.adoc
@@ -0,0 +1,22 @@
+= jvm-* files
+
+Several files for JVM configuration are included in Cassandra. The
+`jvm-server.options` file, and corresponding files `jvm8-server.options`
+and `jvm11-server.options` are the main file for settings that affect
+the operation of the Cassandra JVM on cluster nodes. The file includes
+startup parameters, general JVM settings such as garbage collection, and
+heap settings. The `jvm-clients.options` and corresponding
+`jvm8-clients.options` and `jvm11-clients.options` files can be used to
+configure JVM settings for clients like `nodetool` and the `sstable`
+tools.
+
+See each file for examples of settings.
+
+[NOTE]
+.Note
+====
+The `jvm-*` files replace the `cassandra-envsh` file used in Cassandra
+versions prior to Cassandra 3.0. The `cassandra-env.sh` bash script file
+is still useful if JVM settings must be dynamically calculated based on
+system settings. The `jvm-*` files only store static JVM settings.
+====
diff --git a/doc/modules/cassandra/pages/configuration/cass_logback_xml_file.adoc b/doc/modules/cassandra/pages/configuration/cass_logback_xml_file.adoc
new file mode 100644
index 0000000..e673622
--- /dev/null
+++ b/doc/modules/cassandra/pages/configuration/cass_logback_xml_file.adoc
@@ -0,0 +1,166 @@
+= logback.xml file
+
+The `logback.xml` configuration file can optionally set logging levels
+for the logs written to `system.log` and `debug.log`. The logging levels
+can also be set using `nodetool setlogginglevels`.
+
+== Options
+
+=== `appender name="<appender_choice>"...</appender>` 
+
+Specify log type and settings. Possible appender names are: `SYSTEMLOG`,
+`DEBUGLOG`, `ASYNCDEBUGLOG`, and `STDOUT`. `SYSTEMLOG` ensures that WARN
+and ERROR message are written synchronously to the specified file.
+`DEBUGLOG` and `ASYNCDEBUGLOG` ensure that DEBUG messages are written
+either synchronously or asynchronously, respectively, to the specified
+file. `STDOUT` writes all messages to the console in a human-readable
+format.
+
+*Example:* <appender name="SYSTEMLOG"
+class="ch.qos.logback.core.rolling.RollingFileAppender">
+
+=== `<file> <filename> </file>` 
+
+Specify the filename for a log.
+
+*Example:* <file>$\{cassandra.logdir}/system.log</file>
+
+=== `<level> <log_level> </level>`
+
+Specify the level for a log. Part of the filter. Levels are: `ALL`,
+`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF`. `TRACE` creates the
+most verbose log, `ERROR` the least.
+
+[NOTE]
+.Note
+====
+Note: Increasing logging levels can generate heavy logging output on
+a moderately trafficked cluster. You can use the
+`nodetool getlogginglevels` command to see the current logging
+configuration.
+====
+
+*Default:* INFO
+
+*Example:* <level>INFO</level>
+
+=== `<rollingPolicy class="<rolling_policy_choice>" <fileNamePattern><pattern_info></fileNamePattern> ... </rollingPolicy>`
+
+Specify the policy for rolling logs over to an archive.
+
+*Example:* <rollingPolicy
+class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+
+=== `<fileNamePattern> <pattern_info> </fileNamePattern>`
+
+Specify the pattern information for rolling over the log to archive.
+Part of the rolling policy.
+
+*Example:*
+<fileNamePattern>$\{cassandra.logdir}/system.log.%d\{yyyy-MM-dd}.%i.zip</fileNamePattern>
+
+=== `<maxFileSize> <size> </maxFileSize>`
+
+Specify the maximum file size to trigger rolling a log. Part of the
+rolling policy.
+
+*Example:* <maxFileSize>50MB</maxFileSize>
+
+=== `<maxHistory> <number_of_days> </maxHistory>`
+
+Specify the maximum history in days to trigger rolling a log. Part of
+the rolling policy.
+
+*Example:* <maxHistory>7</maxHistory>
+
+=== `<encoder> <pattern>...</pattern> </encoder>`
+
+Specify the format of the message. Part of the rolling policy.
+
+*Example:* <maxHistory>7</maxHistory> *Example:* <encoder>
+<pattern>%-5level [%thread] %date\{ISO8601} %F:%L - %msg%n</pattern>
+</encoder>
+
+=== Contents of default `logback.xml`
+
+[source,XML]
+----
+<configuration scan="true" scanPeriod="60 seconds">
+  <jmxConfigurator />
+
+  <!-- No shutdown hook; we run it ourselves in StorageService after shutdown -->
+
+  <!-- SYSTEMLOG rolling file appender to system.log (INFO level) -->
+
+  <appender name="SYSTEMLOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+  <level>INFO</level>
+    </filter>
+    <file>${cassandra.logdir}/system.log</file>
+    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+      <!-- rollover daily -->
+      <fileNamePattern>${cassandra.logdir}/system.log.%d{yyyy-MM-dd}.%i.zip</fileNamePattern>
+      <!-- each file should be at most 50MB, keep 7 days worth of history, but at most 5GB -->
+      <maxFileSize>50MB</maxFileSize>
+      <maxHistory>7</maxHistory>
+      <totalSizeCap>5GB</totalSizeCap>
+    </rollingPolicy>
+    <encoder>
+      <pattern>%-5level [%thread] %date{ISO8601} %F:%L - %msg%n</pattern>
+    </encoder>
+  </appender>
+
+  <!-- DEBUGLOG rolling file appender to debug.log (all levels) -->
+
+  <appender name="DEBUGLOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    <file>${cassandra.logdir}/debug.log</file>
+    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+      <!-- rollover daily -->
+      <fileNamePattern>${cassandra.logdir}/debug.log.%d{yyyy-MM-dd}.%i.zip</fileNamePattern>
+      <!-- each file should be at most 50MB, keep 7 days worth of history, but at most 5GB -->
+      <maxFileSize>50MB</maxFileSize>
+      <maxHistory>7</maxHistory>
+      <totalSizeCap>5GB</totalSizeCap>
+    </rollingPolicy>
+    <encoder>
+      <pattern>%-5level [%thread] %date{ISO8601} %F:%L - %msg%n</pattern>
+    </encoder>
+  </appender>
+
+  <!-- ASYNCLOG assynchronous appender to debug.log (all levels) -->
+
+  <appender name="ASYNCDEBUGLOG" class="ch.qos.logback.classic.AsyncAppender">
+    <queueSize>1024</queueSize>
+    <discardingThreshold>0</discardingThreshold>
+    <includeCallerData>true</includeCallerData>
+    <appender-ref ref="DEBUGLOG" />
+  </appender>
+
+  <!-- STDOUT console appender to stdout (INFO level) -->
+
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+      <level>INFO</level>
+    </filter>
+    <encoder>
+      <pattern>%-5level [%thread] %date{ISO8601} %F:%L - %msg%n</pattern>
+    </encoder>
+  </appender>
+
+  <!-- Uncomment bellow and corresponding appender-ref to activate logback metrics
+  <appender name="LogbackMetrics" class="com.codahale.metrics.logback.InstrumentedAppender" />
+   -->
+
+  <root level="INFO">
+    <appender-ref ref="SYSTEMLOG" />
+    <appender-ref ref="STDOUT" />
+    <appender-ref ref="ASYNCDEBUGLOG" /> <!-- Comment this line to disable debug.log -->
+    <!--
+    <appender-ref ref="LogbackMetrics" />
+    -->
+  </root>
+
+  <logger name="org.apache.cassandra" level="DEBUG"/>
+  <logger name="com.thinkaurelius.thrift" level="ERROR"/>
+</configuration>
+----
diff --git a/doc/modules/cassandra/pages/configuration/cass_rackdc_file.adoc b/doc/modules/cassandra/pages/configuration/cass_rackdc_file.adoc
new file mode 100644
index 0000000..0b370c9
--- /dev/null
+++ b/doc/modules/cassandra/pages/configuration/cass_rackdc_file.adoc
@@ -0,0 +1,79 @@
+= cassandra-rackdc.properties file
+
+Several `snitch` options use the `cassandra-rackdc.properties`
+configuration file to determine which `datacenters` and racks cluster
+nodes belong to. Information about the network topology allows requests
+to be routed efficiently and to distribute replicas evenly. The
+following snitches can be configured here:
+
+* GossipingPropertyFileSnitch
+* AWS EC2 single-region snitch
+* AWS EC2 multi-region snitch
+
+The GossipingPropertyFileSnitch is recommended for production. This
+snitch uses the datacenter and rack information configured in a local
+node's `cassandra-rackdc.properties` file and propagates the information
+to other nodes using `gossip`. It is the default snitch and the settings
+in this properties file are enabled.
+
+The AWS EC2 snitches are configured for clusters in AWS. This snitch
+uses the `cassandra-rackdc.properties` options to designate one of two
+AWS EC2 datacenter and rack naming conventions:
+
+* legacy: Datacenter name is the part of the availability zone name
+preceding the last "-" when the zone ends in -1 and includes the number
+if not -1. Rack name is the portion of the availability zone name
+following the last "-".
++
+____
+Examples: us-west-1a => dc: us-west, rack: 1a; us-west-2b => dc:
+us-west-2, rack: 2b;
+____
+* standard: Datacenter name is the standard AWS region name, including
+the number. Rack name is the region plus the availability zone letter.
++
+____
+Examples: us-west-1a => dc: us-west-1, rack: us-west-1a; us-west-2b =>
+dc: us-west-2, rack: us-west-2b;
+____
+
+Either snitch can set to use the local or internal IP address when
+multiple datacenters are not communicating.
+
+== GossipingPropertyFileSnitch
+
+=== `dc`
+
+Name of the datacenter. The value is case-sensitive.
+
+*Default value:* DC1
+
+=== `rack`
+
+Rack designation. The value is case-sensitive.
+
+*Default value:* RAC1
+
+== AWS EC2 snitch
+
+=== `ec2_naming_scheme`
+
+Datacenter and rack naming convention. Options are `legacy` or
+`standard` (default). *This option is commented out by default.*
+
+*Default value:* standard
+
+[NOTE]
+.Note
+====
+YOU MUST USE THE `legacy` VALUE IF YOU ARE UPGRADING A PRE-4.0 CLUSTER.
+====
+
+== Either snitch
+
+=== `prefer_local`
+
+Option to use the local or internal IP address when communication is not
+across different datacenters. *This option is commented out by default.*
+
+*Default value:* true
diff --git a/doc/modules/cassandra/pages/configuration/cass_topo_file.adoc b/doc/modules/cassandra/pages/configuration/cass_topo_file.adoc
new file mode 100644
index 0000000..4632485
--- /dev/null
+++ b/doc/modules/cassandra/pages/configuration/cass_topo_file.adoc
@@ -0,0 +1,52 @@
+= cassandra-topologies.properties file
+
+The `PropertyFileSnitch` `snitch` option uses the
+`cassandra-topologies.properties` configuration file to determine which
+`datacenters` and racks cluster nodes belong to. If other snitches are
+used, the xref:configuration/cass_rackdc_file.adoc[cassandra-rackdc.properties] must be used. The snitch determines
+network topology (proximity by rack and datacenter) so that requests are
+routed efficiently and allows the database to distribute replicas
+evenly.
+
+Include every node in the cluster in the properties file, defining your
+datacenter names as in the keyspace definition. The datacenter and rack
+names are case-sensitive.
+
+The `cassandra-topologies.properties` file must be copied identically to
+every node in the cluster.
+
+== Example
+
+This example uses three datacenters:
+
+[source,bash]
+----
+# datacenter One
+
+175.56.12.105=DC1:RAC1
+175.50.13.200=DC1:RAC1
+175.54.35.197=DC1:RAC1
+
+120.53.24.101=DC1:RAC2
+120.55.16.200=DC1:RAC2
+120.57.102.103=DC1:RAC2
+
+# datacenter Two
+
+110.56.12.120=DC2:RAC1
+110.50.13.201=DC2:RAC1
+110.54.35.184=DC2:RAC1
+
+50.33.23.120=DC2:RAC2
+50.45.14.220=DC2:RAC2
+50.17.10.203=DC2:RAC2
+
+# datacenter Three
+
+172.106.12.120=DC3:RAC1
+172.106.12.121=DC3:RAC1
+172.106.12.122=DC3:RAC1
+
+# default for unknown nodes 
+default =DC3:RAC1
+----
diff --git a/doc/modules/cassandra/pages/configuration/index.adoc b/doc/modules/cassandra/pages/configuration/index.adoc
new file mode 100644
index 0000000..b2e5e21
--- /dev/null
+++ b/doc/modules/cassandra/pages/configuration/index.adoc
@@ -0,0 +1,11 @@
+= Configuring Cassandra
+
+This section describes how to configure Apache Cassandra.
+
+* xref:configuration/cass_yaml_file.adoc[cassandra.yaml]
+* xref:configuration/cass_rackdc_file.adoc[cassandra-rackdc.properties]
+* xref:configuration/cass_env_sh_file.adoc[cassandra-env.sh]
+* xref:configuration/cass_topo_file.adoc[cassandra-topologies.properties]
+* xref:configuration/cass_cl_archive_file.adoc[commitlog-archiving.properties]
+* xref:configuration/cass_logback_xml_file.adoc[logback.xml]
+* xref:configuration/cass_jvm_options_file.adoc[jvm-* files]
diff --git a/doc/modules/cassandra/pages/cql/SASI.adoc b/doc/modules/cassandra/pages/cql/SASI.adoc
new file mode 100644
index 0000000..c24009a
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/SASI.adoc
@@ -0,0 +1,809 @@
+== SASIIndex
+
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/SASIIndex.java[`SASIIndex`],
+or ``SASI`` for short, is an implementation of Cassandra's `Index`
+interface that can be used as an alternative to the existing
+implementations. SASI's indexing and querying improves on existing
+implementations by tailoring it specifically to Cassandra’s needs. SASI
+has superior performance in cases where queries would previously require
+filtering. In achieving this performance, SASI aims to be significantly
+less resource intensive than existing implementations, in memory, disk,
+and CPU usage. In addition, SASI supports prefix and contains queries on
+strings (similar to SQL’s `LIKE = "foo*"` or `LIKE = "*foo*"'`).
+
+The following goes on describe how to get up and running with SASI,
+demonstrates usage with examples, and provides some details on its
+implementation.
+
+=== Using SASI
+
+The examples below walk through creating a table and indexes on its
+columns, and performing queries on some inserted data.
+
+The examples below assume the `demo` keyspace has been created and is in
+use.
+
+....
+cqlsh> CREATE KEYSPACE demo WITH replication = {
+   ... 'class': 'SimpleStrategy',
+   ... 'replication_factor': '1'
+   ... };
+cqlsh> USE demo;
+....
+
+All examples are performed on the `sasi` table:
+
+....
+cqlsh:demo> CREATE TABLE sasi (id uuid, first_name text, last_name text,
+        ... age int, height int, created_at bigint, primary key (id));
+....
+
+==== Creating Indexes
+
+To create SASI indexes use CQLs `CREATE CUSTOM INDEX` statement:
+
+....
+cqlsh:demo> CREATE CUSTOM INDEX ON sasi (first_name) USING 'org.apache.cassandra.index.sasi.SASIIndex'
+        ... WITH OPTIONS = {
+        ... 'analyzer_class':
+        ...   'org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer',
+        ... 'case_sensitive': 'false'
+        ... };
+
+cqlsh:demo> CREATE CUSTOM INDEX ON sasi (last_name) USING 'org.apache.cassandra.index.sasi.SASIIndex'
+        ... WITH OPTIONS = {'mode': 'CONTAINS'};
+
+cqlsh:demo> CREATE CUSTOM INDEX ON sasi (age) USING 'org.apache.cassandra.index.sasi.SASIIndex';
+
+cqlsh:demo> CREATE CUSTOM INDEX ON sasi (created_at) USING 'org.apache.cassandra.index.sasi.SASIIndex'
+        ...  WITH OPTIONS = {'mode': 'SPARSE'};
+....
+
+The indexes created have some options specified that customize their
+behaviour and potentially performance. The index on `first_name` is
+case-insensitive. The analyzers are discussed more in a subsequent
+example. The `NonTokenizingAnalyzer` performs no analysis on the text.
+Each index has a mode: `PREFIX`, `CONTAINS`, or `SPARSE`, the first
+being the default. The `last_name` index is created with the mode
+`CONTAINS` which matches terms on suffixes instead of prefix only.
+Examples of this are available below and more detail can be found in the
+section on link:#ondiskindexbuilder[OnDiskIndex].The `created_at` column
+is created with its mode set to `SPARSE`, which is meant to improve
+performance of querying large, dense number ranges like timestamps for
+data inserted every millisecond. Details of the `SPARSE` implementation
+can also be found in the section on the
+link:#ondiskindexbuilder[OnDiskIndex]. The `age` index is created with
+the default `PREFIX` mode and no case-sensitivity or text analysis
+options are specified since the field is numeric.
+
+After inserting the following data and performing a `nodetool flush`,
+SASI performing index flushes to disk can be seen in Cassandra’s logs –
+although the direct call to flush is not required (see
+link:#indexmemtable[IndexMemtable] for more details).
+
+....
+cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
+        ... VALUES (556ebd54-cbe5-4b75-9aae-bf2a31a24500, 'Pavel', 'Yaskevich', 27, 181, 1442959315018);
+
+cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
+        ... VALUES (5770382a-c56f-4f3f-b755-450e24d55217, 'Jordan', 'West', 26, 173, 1442959315019);
+
+cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
+        ... VALUES (96053844-45c3-4f15-b1b7-b02c441d3ee1, 'Mikhail', 'Stepura', 36, 173, 1442959315020);
+
+cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
+        ... VALUES (f5dfcabe-de96-4148-9b80-a1c41ed276b4, 'Michael', 'Kjellman', 26, 180, 1442959315021);
+
+cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
+        ... VALUES (2970da43-e070-41a8-8bcb-35df7a0e608a, 'Johnny', 'Zhang', 32, 175, 1442959315022);
+
+cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
+        ... VALUES (6b757016-631d-4fdb-ac62-40b127ccfbc7, 'Jason', 'Brown', 40, 182, 1442959315023);
+
+cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
+        ... VALUES (8f909e8a-008e-49dd-8d43-1b0df348ed44, 'Vijay', 'Parthasarathy', 34, 183, 1442959315024);
+
+cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi;
+
+ first_name | last_name     | age | height | created_at
+------------+---------------+-----+--------+---------------
+    Michael |      Kjellman |  26 |    180 | 1442959315021
+    Mikhail |       Stepura |  36 |    173 | 1442959315020
+      Jason |         Brown |  40 |    182 | 1442959315023
+      Pavel |     Yaskevich |  27 |    181 | 1442959315018
+      Vijay | Parthasarathy |  34 |    183 | 1442959315024
+     Jordan |          West |  26 |    173 | 1442959315019
+     Johnny |         Zhang |  32 |    175 | 1442959315022
+
+(7 rows)
+....
+
+==== Equality & Prefix Queries
+
+SASI supports all queries already supported by CQL, including LIKE
+statement for PREFIX, CONTAINS and SUFFIX searches.
+
+....
+cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi
+        ... WHERE first_name = 'Pavel';
+
+  first_name | last_name | age | height | created_at
+-------------+-----------+-----+--------+---------------
+       Pavel | Yaskevich |  27 |    181 | 1442959315018
+
+(1 rows)
+....
+
+....
+cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi
+       ... WHERE first_name = 'pavel';
+
+  first_name | last_name | age | height | created_at
+-------------+-----------+-----+--------+---------------
+       Pavel | Yaskevich |  27 |    181 | 1442959315018
+
+(1 rows)
+....
+
+....
+cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi
+        ... WHERE first_name LIKE 'M%';
+
+ first_name | last_name | age | height | created_at
+------------+-----------+-----+--------+---------------
+    Michael |  Kjellman |  26 |    180 | 1442959315021
+    Mikhail |   Stepura |  36 |    173 | 1442959315020
+
+(2 rows)
+....
+
+Of course, the case of the query does not matter for the `first_name`
+column because of the options provided at index creation time.
+
+....
+cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi
+        ... WHERE first_name LIKE 'm%';
+
+ first_name | last_name | age | height | created_at
+------------+-----------+-----+--------+---------------
+    Michael |  Kjellman |  26 |    180 | 1442959315021
+    Mikhail |   Stepura |  36 |    173 | 1442959315020
+
+(2 rows)
+....
+
+==== Compound Queries
+
+SASI supports queries with multiple predicates, however, due to the
+nature of the default indexing implementation, CQL requires the user to
+specify `ALLOW FILTERING` to opt-in to the potential performance
+pitfalls of such a query. With SASI, while the requirement to include
+`ALLOW FILTERING` remains, to reduce modifications to the grammar, the
+performance pitfalls do not exist because filtering is not performed.
+Details on how SASI joins data from multiple predicates is available
+below in the link:#implementation-details[Implementation Details]
+section.
+
+....
+cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi
+        ... WHERE first_name LIKE 'M%' and age < 30 ALLOW FILTERING;
+
+ first_name | last_name | age | height | created_at
+------------+-----------+-----+--------+---------------
+    Michael |  Kjellman |  26 |    180 | 1442959315021
+
+(1 rows)
+....
+
+==== Suffix Queries
+
+The next example demonstrates `CONTAINS` mode on the `last_name` column.
+By using this mode, predicates can search for any strings containing the
+search string as a sub-string. In this case the strings containing ``a''
+or ``an''.
+
+....
+cqlsh:demo> SELECT * FROM sasi WHERE last_name LIKE '%a%';
+
+ id                                   | age | created_at    | first_name | height | last_name
+--------------------------------------+-----+---------------+------------+--------+---------------
+ f5dfcabe-de96-4148-9b80-a1c41ed276b4 |  26 | 1442959315021 |    Michael |    180 |      Kjellman
+ 96053844-45c3-4f15-b1b7-b02c441d3ee1 |  36 | 1442959315020 |    Mikhail |    173 |       Stepura
+ 556ebd54-cbe5-4b75-9aae-bf2a31a24500 |  27 | 1442959315018 |      Pavel |    181 |     Yaskevich
+ 8f909e8a-008e-49dd-8d43-1b0df348ed44 |  34 | 1442959315024 |      Vijay |    183 | Parthasarathy
+ 2970da43-e070-41a8-8bcb-35df7a0e608a |  32 | 1442959315022 |     Johnny |    175 |         Zhang
+
+(5 rows)
+
+cqlsh:demo> SELECT * FROM sasi WHERE last_name LIKE '%an%';
+
+ id                                   | age | created_at    | first_name | height | last_name
+--------------------------------------+-----+---------------+------------+--------+-----------
+ f5dfcabe-de96-4148-9b80-a1c41ed276b4 |  26 | 1442959315021 |    Michael |    180 |  Kjellman
+ 2970da43-e070-41a8-8bcb-35df7a0e608a |  32 | 1442959315022 |     Johnny |    175 |     Zhang
+
+(2 rows)
+....
+
+==== Expressions on Non-Indexed Columns
+
+SASI also supports filtering on non-indexed columns like `height`. The
+expression can only narrow down an existing query using `AND`.
+
+....
+cqlsh:demo> SELECT * FROM sasi WHERE last_name LIKE '%a%' AND height >= 175 ALLOW FILTERING;
+
+ id                                   | age | created_at    | first_name | height | last_name
+--------------------------------------+-----+---------------+------------+--------+---------------
+ f5dfcabe-de96-4148-9b80-a1c41ed276b4 |  26 | 1442959315021 |    Michael |    180 |      Kjellman
+ 556ebd54-cbe5-4b75-9aae-bf2a31a24500 |  27 | 1442959315018 |      Pavel |    181 |     Yaskevich
+ 8f909e8a-008e-49dd-8d43-1b0df348ed44 |  34 | 1442959315024 |      Vijay |    183 | Parthasarathy
+ 2970da43-e070-41a8-8bcb-35df7a0e608a |  32 | 1442959315022 |     Johnny |    175 |         Zhang
+
+(4 rows)
+....
+
+==== Delimiter based Tokenization Analysis
+
+A simple text analysis provided is delimiter based tokenization. This
+provides an alternative to indexing collections, as delimiter separated
+text can be indexed without the overhead of `CONTAINS` mode nor using
+`PREFIX` or `SUFFIX` queries.
+
+....
+cqlsh:demo> ALTER TABLE sasi ADD aliases text;
+cqlsh:demo> CREATE CUSTOM INDEX on sasi (aliases) USING 'org.apache.cassandra.index.sasi.SASIIndex'
+        ... WITH OPTIONS = {
+        ... 'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.DelimiterAnalyzer',
+        ... 'delimiter': ',',
+        ... 'mode': 'prefix',
+        ... 'analyzed': 'true'};
+cqlsh:demo> UPDATE sasi SET aliases = 'Mike,Mick,Mikey,Mickey' WHERE id = f5dfcabe-de96-4148-9b80-a1c41ed276b4;
+cqlsh:demo> SELECT * FROM sasi WHERE aliases LIKE 'Mikey' ALLOW FILTERING;
+
+ id                                   | age | aliases                | created_at    | first_name | height | last_name
+--------------------------------------+-----+------------------------+---------------+------------+--------+-----------
+ f5dfcabe-de96-4148-9b80-a1c41ed276b4 |  26 | Mike,Mick,Mikey,Mickey | 1442959315021 |    Michael |    180 |  Kjellman
+....
+
+==== Text Analysis (Tokenization and Stemming)
+
+Lastly, to demonstrate text analysis an additional column is needed on
+the table. Its definition, index, and statements to update rows are
+shown below.
+
+....
+cqlsh:demo> ALTER TABLE sasi ADD bio text;
+cqlsh:demo> CREATE CUSTOM INDEX ON sasi (bio) USING 'org.apache.cassandra.index.sasi.SASIIndex'
+        ... WITH OPTIONS = {
+        ... 'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer',
+        ... 'tokenization_enable_stemming': 'true',
+        ... 'analyzed': 'true',
+        ... 'tokenization_normalize_lowercase': 'true',
+        ... 'tokenization_locale': 'en'
+        ... };
+cqlsh:demo> UPDATE sasi SET bio = 'Software Engineer, who likes distributed systems, doesnt like to argue.' WHERE id = 5770382a-c56f-4f3f-b755-450e24d55217;
+cqlsh:demo> UPDATE sasi SET bio = 'Software Engineer, works on the freight distribution at nights and likes arguing' WHERE id = 556ebd54-cbe5-4b75-9aae-bf2a31a24500;
+cqlsh:demo> SELECT * FROM sasi;
+
+ id                                   | age | bio                                                                              | created_at    | first_name | height | last_name
+--------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+---------------
+ f5dfcabe-de96-4148-9b80-a1c41ed276b4 |  26 |                                                                             null | 1442959315021 |    Michael |    180 |      Kjellman
+ 96053844-45c3-4f15-b1b7-b02c441d3ee1 |  36 |                                                                             null | 1442959315020 |    Mikhail |    173 |       Stepura
+ 6b757016-631d-4fdb-ac62-40b127ccfbc7 |  40 |                                                                             null | 1442959315023 |      Jason |    182 |         Brown
+ 556ebd54-cbe5-4b75-9aae-bf2a31a24500 |  27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 |      Pavel |    181 |     Yaskevich
+ 8f909e8a-008e-49dd-8d43-1b0df348ed44 |  34 |                                                                             null | 1442959315024 |      Vijay |    183 | Parthasarathy
+ 5770382a-c56f-4f3f-b755-450e24d55217 |  26 |          Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 |     Jordan |    173 |          West
+ 2970da43-e070-41a8-8bcb-35df7a0e608a |  32 |                                                                             null | 1442959315022 |     Johnny |    175 |         Zhang
+
+(7 rows)
+....
+
+Index terms and query search strings are stemmed for the `bio` column
+because it was configured to use the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java[`StandardAnalyzer`]
+and `analyzed` is set to `true`. The `tokenization_normalize_lowercase`
+is similar to the `case_sensitive` property but for the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java[`StandardAnalyzer`].
+These query demonstrates the stemming applied by
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java[`StandardAnalyzer`].
+
+....
+cqlsh:demo> SELECT * FROM sasi WHERE bio LIKE 'distributing';
+
+ id                                   | age | bio                                                                              | created_at    | first_name | height | last_name
+--------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+-----------
+ 556ebd54-cbe5-4b75-9aae-bf2a31a24500 |  27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 |      Pavel |    181 | Yaskevich
+ 5770382a-c56f-4f3f-b755-450e24d55217 |  26 |          Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 |     Jordan |    173 |      West
+
+(2 rows)
+
+cqlsh:demo> SELECT * FROM sasi WHERE bio LIKE 'they argued';
+
+ id                                   | age | bio                                                                              | created_at    | first_name | height | last_name
+--------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+-----------
+ 556ebd54-cbe5-4b75-9aae-bf2a31a24500 |  27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 |      Pavel |    181 | Yaskevich
+ 5770382a-c56f-4f3f-b755-450e24d55217 |  26 |          Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 |     Jordan |    173 |      West
+
+(2 rows)
+
+cqlsh:demo> SELECT * FROM sasi WHERE bio LIKE 'working at the company';
+
+ id                                   | age | bio                                                                              | created_at    | first_name | height | last_name
+--------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+-----------
+ 556ebd54-cbe5-4b75-9aae-bf2a31a24500 |  27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 |      Pavel |    181 | Yaskevich
+
+(1 rows)
+
+cqlsh:demo> SELECT * FROM sasi WHERE bio LIKE 'soft eng';
+
+ id                                   | age | bio                                                                              | created_at    | first_name | height | last_name
+--------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+-----------
+ 556ebd54-cbe5-4b75-9aae-bf2a31a24500 |  27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 |      Pavel |    181 | Yaskevich
+ 5770382a-c56f-4f3f-b755-450e24d55217 |  26 |          Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 |     Jordan |    173 |      West
+
+(2 rows)
+....
+
+=== Implementation Details
+
+While SASI, at the surface, is simply an implementation of the `Index`
+interface, at its core there are several data structures and algorithms
+used to satisfy it. These are described here. Additionally, the changes
+internal to Cassandra to support SASI’s integration are described.
+
+The `Index` interface divides responsibility of the implementer into two
+parts: Indexing and Querying. Further, Cassandra makes it possible to
+divide those responsibilities into the memory and disk components. SASI
+takes advantage of Cassandra’s write-once, immutable, ordered data model
+to build indexes along with the flushing of the memtable to disk – this
+is the origin of the name ``SSTable Attached Secondary Index''.
+
+The SASI index data structures are built in memory as the SSTable is
+being written and they are flushed to disk before the writing of the
+SSTable completes. The writing of each index file only requires
+sequential writes to disk. In some cases, partial flushes are performed,
+and later stitched back together, to reduce memory usage. These data
+structures are optimized for this use case.
+
+Taking advantage of Cassandra’s ordered data model, at query time,
+candidate indexes are narrowed down for searching, minimizing the amount
+of work done. Searching is then performed using an efficient method that
+streams data off disk as needed.
+
+==== Indexing
+
+Per SSTable, SASI writes an index file for each indexed column. The data
+for these files is built in memory using the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java[`OnDiskIndexBuilder`].
+Once flushed to disk, the data is read using the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java[`OnDiskIndex`]
+class. These are composed of bytes representing indexed terms, organized
+for efficient writing or searching respectively. The keys and values
+they hold represent tokens and positions in an SSTable and these are
+stored per-indexed term in
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java[`TokenTreeBuilder`]s
+for writing, and
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java[`TokenTree`]s
+for querying. These index files are memory mapped after being written to
+disk, for quicker access. For indexing data in the memtable, SASI uses
+its
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java[`IndexMemtable`]
+class.
+
+===== OnDiskIndex(Builder)
+
+Each
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java[`OnDiskIndex`]
+is an instance of a modified
+https://en.wikipedia.org/wiki/Suffix_array[Suffix Array] data structure.
+The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java[`OnDiskIndex`]
+is comprised of page-size blocks of sorted terms and pointers to the
+terms’ associated data, as well as the data itself, stored also in one
+or more page-sized blocks. The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java[`OnDiskIndex`]
+is structured as a tree of arrays, where each level describes the terms
+in the level below, the final level being the terms themselves. The
+`PointerLevel`s and their `PointerBlock`s contain terms and pointers to
+other blocks that _end_ with those terms. The `DataLevel`, the final
+level, and its `DataBlock`s contain terms and point to the data itself,
+contained in
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java[`TokenTree`]s.
+
+The terms written to the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java[`OnDiskIndex`]
+vary depending on its ``mode'': either `PREFIX`, `CONTAINS`, or
+`SPARSE`. In the `PREFIX` and `SPARSE` cases, terms’ exact values are
+written exactly once per `OnDiskIndex`. For example, when using a
+`PREFIX` index with terms `Jason`, `Jordan`, `Pavel`, all three will be
+included in the index. A `CONTAINS` index writes additional terms for
+each suffix of each term recursively. Continuing with the example, a
+`CONTAINS` index storing the previous terms would also store `ason`,
+`ordan`, `avel`, `son`, `rdan`, `vel`, etc. This allows for queries on
+the suffix of strings. The `SPARSE` mode differs from `PREFIX` in that
+for every 64 blocks of terms a
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java[`TokenTree`]
+is built merging all the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java[`TokenTree`]s
+for each term into a single one. This copy of the data is used for
+efficient iteration of large ranges of e.g. timestamps. The index
+``mode'' is configurable per column at index creation time.
+
+===== TokenTree(Builder)
+
+The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java[`TokenTree`]
+is an implementation of the well-known
+https://en.wikipedia.org/wiki/B%2B_tree[B+-tree] that has been modified
+to optimize for its use-case. In particular, it has been optimized to
+associate tokens, longs, with a set of positions in an SSTable, also
+longs. Allowing the set of long values accommodates the possibility of a
+hash collision in the token, but the data structure is optimized for the
+unlikely possibility of such a collision.
+
+To optimize for its write-once environment the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java[`TokenTreeBuilder`]
+completely loads its interior nodes as the tree is built and it uses the
+well-known algorithm optimized for bulk-loading the data structure.
+
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java[`TokenTree`]s
+provide the means to iterate over tokens, and file positions, that match
+a given term, and to skip forward in that iteration, an operation used
+heavily at query time.
+
+===== IndexMemtable
+
+The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java[`IndexMemtable`]
+handles indexing the in-memory data held in the memtable. The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java[`IndexMemtable`]
+in turn manages either a
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java[`TrieMemIndex`]
+or a
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java[`SkipListMemIndex`]
+per-column. The choice of which index type is used is data dependent.
+The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java[`TrieMemIndex`]
+is used for literal types. `AsciiType` and `UTF8Type` are literal types
+by default but any column can be configured as a literal type using the
+`is_literal` option at index creation time. For non-literal types the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java[`SkipListMemIndex`]
+is used. The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java[`TrieMemIndex`]
+is an implementation that can efficiently support prefix queries on
+character-like data. The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java[`SkipListMemIndex`],
+conversely, is better suited for other Cassandra data types like
+numbers.
+
+The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java[`TrieMemIndex`]
+is built using either the `ConcurrentRadixTree` or
+`ConcurrentSuffixTree` from the `com.goooglecode.concurrenttrees`
+package. The choice between the two is made based on the indexing mode,
+`PREFIX` or other modes, and `CONTAINS` mode, respectively.
+
+The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java[`SkipListMemIndex`]
+is built on top of `java.util.concurrent.ConcurrentSkipListSet`.
+
+==== Querying
+
+Responsible for converting the internal `IndexExpression` representation
+into SASI’s
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]
+and
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java[`Expression`]
+trees, optimizing the trees to reduce the amount of work done, and
+driving the query itself, the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`]
+is the work horse of SASI’s querying implementation. To efficiently
+perform union and intersection operations, SASI provides several
+iterators similar to Cassandra’s `MergeIterator`, but tailored
+specifically for SASI’s use while including more features. The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java[`RangeUnionIterator`],
+like its name suggests, performs set unions over sets of tokens/keys
+matching the query, only reading as much data as it needs from each set
+to satisfy the query. The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java[`RangeIntersectionIterator`],
+similar to its counterpart, performs set intersections over its data.
+
+===== QueryPlan
+
+The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`]
+instantiated per search query is at the core of SASI’s querying
+implementation. Its work can be divided in two stages: analysis and
+execution.
+
+During the analysis phase,
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`]
+converts from Cassandra’s internal representation of `IndexExpression`s,
+which has also been modified to support encoding queries that contain
+ORs and groupings of expressions using parentheses (see the
+link:#cassandra-internal-changes[Cassandra Internal Changes] section
+below for more details). This process produces a tree of
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]s,
+which in turn may contain
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java[`Expression`]s,
+all of which provide an alternative, more efficient, representation of
+the query.
+
+During execution, the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`]
+uses the `DecoratedKey`-generating iterator created from the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]
+tree. These keys are read from disk and a final check to ensure they
+satisfy the query is made, once again using the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]
+tree. At the point the desired amount of matching data has been found,
+or there is no more matching data, the result set is returned to the
+coordinator through the existing internal components.
+
+The number of queries (total/failed/timed-out), and their latencies, are
+maintined per-table/column family.
+
+SASI also supports concurrently iterating terms for the same index
+across SSTables. The concurrency factor is controlled by the
+`cassandra.search_concurrency_factor` system property. The default is
+`1`.
+
+====== QueryController
+
+Each
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`]
+references a
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java[`QueryController`]
+used throughout the execution phase. The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java[`QueryController`]
+has two responsibilities: to manage and ensure the proper cleanup of
+resources (indexes), and to strictly enforce the time bound per query,
+specified by the user via the range slice timeout. All indexes are
+accessed via the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java[`QueryController`]
+so that they can be safely released by it later. The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java[`QueryController`]’s
+`checkpoint` function is called in specific places in the execution path
+to ensure the time-bound is enforced.
+
+====== QueryPlan Optimizations
+
+While in the analysis phase, the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`]
+performs several potential optimizations to the query. The goal of these
+optimizations is to reduce the amount of work performed during the
+execution phase.
+
+The simplest optimization performed is compacting multiple expressions
+joined by logical intersections (`AND`) into a single
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]
+with three or more
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java[`Expression`]s.
+For example, the query
+`WHERE age < 100 AND fname = 'p*' AND first_name != 'pa*' AND age > 21`
+would, without modification, have the following tree:
+
+....
+                      ┌───────┐
+             ┌────────│  AND  │──────┐
+             │        └───────┘      │
+             ▼                       ▼
+          ┌───────┐             ┌──────────┐
+    ┌─────│  AND  │─────┐       │age < 100 │
+    │     └───────┘     │       └──────────┘
+    ▼                   ▼
+┌──────────┐          ┌───────┐
+│ fname=p* │        ┌─│  AND  │───┐
+└──────────┘        │ └───────┘   │
+                    ▼             ▼
+                ┌──────────┐  ┌──────────┐
+                │fname!=pa*│  │ age > 21 │
+                └──────────┘  └──────────┘
+....
+
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`]
+will remove the redundant right branch whose root is the final `AND` and
+has leaves `fname != pa*` and `age > 21`. These
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java[`Expression`]s
+will be compacted into the parent `AND`, a safe operation due to `AND`
+being associative and commutative. The resulting tree looks like the
+following:
+
+....
+                              ┌───────┐
+                     ┌────────│  AND  │──────┐
+                     │        └───────┘      │
+                     ▼                       ▼
+                  ┌───────┐             ┌──────────┐
+      ┌───────────│  AND  │────────┐    │age < 100 │
+      │           └───────┘        │    └──────────┘
+      ▼               │            ▼
+┌──────────┐          │      ┌──────────┐
+│ fname=p* │          ▼      │ age > 21 │
+└──────────┘    ┌──────────┐ └──────────┘
+                │fname!=pa*│
+                └──────────┘
+....
+
+When excluding results from the result set, using `!=`, the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`]
+determines the best method for handling it. For range queries, for
+example, it may be optimal to divide the range into multiple parts with
+a hole for the exclusion. For string queries, such as this one, it is
+more optimal, however, to simply note which data to skip, or exclude,
+while scanning the index. Following this optimization the tree looks
+like this:
+
+....
+                               ┌───────┐
+                      ┌────────│  AND  │──────┐
+                      │        └───────┘      │
+                      ▼                       ▼
+                   ┌───────┐             ┌──────────┐
+           ┌───────│  AND  │────────┐    │age < 100 │
+           │       └───────┘        │    └──────────┘
+           ▼                        ▼
+    ┌──────────────────┐         ┌──────────┐
+    │     fname=p*     │         │ age > 21 │
+    │ exclusions=[pa*] │         └──────────┘
+    └──────────────────┘
+....
+
+The last type of optimization applied, for this query, is to merge range
+expressions across branches of the tree – without modifying the meaning
+of the query, of course. In this case, because the query contains all
+`AND`s the `age` expressions can be collapsed. Along with this
+optimization, the initial collapsing of unneeded `AND`s can also be
+applied once more to result in this final tree using to execute the
+query:
+
+....
+                        ┌───────┐
+                 ┌──────│  AND  │───────┐
+                 │      └───────┘       │
+                 ▼                      ▼
+       ┌──────────────────┐    ┌────────────────┐
+       │     fname=p*     │    │ 21 < age < 100 │
+       │ exclusions=[pa*] │    └────────────────┘
+       └──────────────────┘
+....
+
+===== Operations and Expressions
+
+As discussed, the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`]
+optimizes a tree represented by
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]s
+as interior nodes, and
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java[`Expression`]s
+as leaves. The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]
+class, more specifically, can have zero, one, or two
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]s
+as children and an unlimited number of expressions. The iterators used
+to perform the queries, discussed below in the
+``Range(Union|Intersection)Iterator'' section, implement the necessary
+logic to merge results transparently regardless of the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]s
+children.
+
+Besides participating in the optimizations performed by the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`],
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]
+is also responsible for taking a row that has been returned by the query
+and performing a final validation that it in fact does match. This
+`satisfiesBy` operation is performed recursively from the root of the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java[`Operation`]
+tree for a given query. These checks are performed directly on the data
+in a given row. For more details on how `satisfiesBy` works, see the
+documentation
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java#L87-L123[in
+the code].
+
+===== Range(Union|Intersection)Iterator
+
+The abstract `RangeIterator` class provides a unified interface over the
+two main operations performed by SASI at various layers in the execution
+path: set intersection and union. These operations are performed in a
+iterated, or ``streaming'', fashion to prevent unneeded reads of
+elements from either set. In both the intersection and union cases the
+algorithms take advantage of the data being pre-sorted using the same
+sort order, e.g. term or token order.
+
+The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java[`RangeUnionIterator`]
+performs the ``Merge-Join'' portion of the
+https://en.wikipedia.org/wiki/Sort-merge_join[Sort-Merge-Join]
+algorithm, with the properties of an outer-join, or union. It is
+implemented with several optimizations to improve its performance over a
+large number of iterators – sets to union. Specifically, the iterator
+exploits the likely case of the data having many sub-groups of
+overlapping ranges and the unlikely case that all ranges will overlap
+each other. For more details see the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java#L9-L21[javadoc].
+
+The
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java[`RangeIntersectionIterator`]
+itself is not a subclass of `RangeIterator`. It is a container for
+several classes, one of which, `AbstractIntersectionIterator`,
+sub-classes `RangeIterator`. SASI supports two methods of performing the
+intersection operation, and the ability to be adaptive in choosing
+between them based on some properties of the data.
+
+`BounceIntersectionIterator`, and the `BOUNCE` strategy, works like the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java[`RangeUnionIterator`]
+in that it performs a ``Merge-Join'', however, its nature is similar to
+a inner-join, where like values are merged by a data-specific merge
+function (e.g. merging two tokens in a list to lookup in a SSTable
+later). See the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java#L88-L101[javadoc]
+for more details on its implementation.
+
+`LookupIntersectionIterator`, and the `LOOKUP` strategy, performs a
+different operation, more similar to a lookup in an associative data
+structure, or ``hash lookup'' in database terminology. Once again,
+details on the implementation can be found in the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java#L199-L208[javadoc].
+
+The choice between the two iterators, or the `ADAPTIVE` strategy, is
+based upon the ratio of data set sizes of the minimum and maximum range
+of the sets being intersected. If the number of the elements in minimum
+range divided by the number of elements is the maximum range is less
+than or equal to `0.01`, then the `ADAPTIVE` strategy chooses the
+`LookupIntersectionIterator`, otherwise the `BounceIntersectionIterator`
+is chosen.
+
+==== The SASIIndex Class
+
+The above components are glued together by the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/SASIIndex.java[`SASIIndex`]
+class which implements `Index`, and is instantiated per-table containing
+SASI indexes. It manages all indexes for a table via the
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/conf/DataTracker.java[`sasi.conf.DataTracker`]
+and
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/conf/view/View.java[`sasi.conf.view.View`]
+components, controls writing of all indexes for an SSTable via its
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriter.java[`PerSSTableIndexWriter`],
+and initiates searches with `Searcher`. These classes glue the
+previously mentioned indexing components together with Cassandra’s
+SSTable life-cycle ensuring indexes are not only written when Memtable’s
+flush, but also as SSTable’s are compacted. For querying, the `Searcher`
+does little but defer to
+https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java[`QueryPlan`]
+and update e.g. latency metrics exposed by SASI.
+
+==== Cassandra Internal Changes
+
+To support the above changes and integrate them into Cassandra a few
+minor internal changes were made to Cassandra itself. These are
+described here.
+
+===== SSTable Write Life-cycle Notifications
+
+The `SSTableFlushObserver` is an observer pattern-like interface, whose
+sub-classes can register to be notified about events in the life-cycle
+of writing out a SSTable. Sub-classes can be notified when a flush
+begins and ends, as well as when each next row is about to be written,
+and each next column. SASI’s `PerSSTableIndexWriter`, discussed above,
+is the only current subclass.
+
+==== Limitations and Caveats
+
+The following are items that can be addressed in future updates but are
+not available in this repository or are not currently implemented.
+
+* The cluster must be configured to use a partitioner that produces
+`LongToken`s, e.g. `Murmur3Partitioner`. Other existing partitioners
+which don’t produce LongToken e.g. `ByteOrderedPartitioner` and
+`RandomPartitioner` will not work with SASI.
+* Not Equals and OR support have been removed in this release while
+changes are made to Cassandra itself to support them.
+
+==== Contributors
+
+* https://github.com/xedin[Pavel Yaskevich]
+* https://github.com/jrwest[Jordan West]
+* https://github.com/mkjellman[Michael Kjellman]
+* https://github.com/jasobrown[Jason Brown]
+* https://github.com/mishail[Mikhail Stepura]
diff --git a/doc/modules/cassandra/pages/cql/appendices.adoc b/doc/modules/cassandra/pages/cql/appendices.adoc
new file mode 100644
index 0000000..7e17266
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/appendices.adoc
@@ -0,0 +1,179 @@
+= Appendices
+
+[[appendix-A]]
+== Appendix A: CQL Keywords
+
+CQL distinguishes between _reserved_ and _non-reserved_ keywords.
+Reserved keywords cannot be used as identifier, they are truly reserved
+for the language (but one can enclose a reserved keyword by
+double-quotes to use it as an identifier). Non-reserved keywords however
+only have a specific meaning in certain context but can used as
+identifier otherwise. The only _raison d’être_ of these non-reserved
+keywords is convenience: some keyword are non-reserved when it was
+always easy for the parser to decide whether they were used as keywords
+or not.
+
+[width="48%",cols="60%,40%",options="header",]
+|===
+|Keyword |Reserved?
+|`ADD` |yes
+|`AGGREGATE` |no
+|`ALL` |no
+|`ALLOW` |yes
+|`ALTER` |yes
+|`AND` |yes
+|`APPLY` |yes
+|`AS` |no
+|`ASC` |yes
+|`ASCII` |no
+|`AUTHORIZE` |yes
+|`BATCH` |yes
+|`BEGIN` |yes
+|`BIGINT` |no
+|`BLOB` |no
+|`BOOLEAN` |no
+|`BY` |yes
+|`CALLED` |no
+|`CLUSTERING` |no
+|`COLUMNFAMILY` |yes
+|`COMPACT` |no
+|`CONTAINS` |no
+|`COUNT` |no
+|`COUNTER` |no
+|`CREATE` |yes
+|`CUSTOM` |no
+|`DATE` |no
+|`DECIMAL` |no
+|`DELETE` |yes
+|`DESC` |yes
+|`DESCRIBE` |yes
+|`DISTINCT` |no
+|`DOUBLE` |no
+|`DROP` |yes
+|`ENTRIES` |yes
+|`EXECUTE` |yes
+|`EXISTS` |no
+|`FILTERING` |no
+|`FINALFUNC` |no
+|`FLOAT` |no
+|`FROM` |yes
+|`FROZEN` |no
+|`FULL` |yes
+|`FUNCTION` |no
+|`FUNCTIONS` |no
+|`GRANT` |yes
+|`IF` |yes
+|`IN` |yes
+|`INDEX` |yes
+|`INET` |no
+|`INFINITY` |yes
+|`INITCOND` |no
+|`INPUT` |no
+|`INSERT` |yes
+|`INT` |no
+|`INTO` |yes
+|`JSON` |no
+|`KEY` |no
+|`KEYS` |no
+|`KEYSPACE` |yes
+|`KEYSPACES` |no
+|`LANGUAGE` |no
+|`LIMIT` |yes
+|`LIST` |no
+|`LOGIN` |no
+|`MAP` |no
+|`MODIFY` |yes
+|`NAN` |yes
+|`NOLOGIN` |no
+|`NORECURSIVE` |yes
+|`NOSUPERUSER` |no
+|`NOT` |yes
+|`NULL` |yes
+|`OF` |yes
+|`ON` |yes
+|`OPTIONS` |no
+|`OR` |yes
+|`ORDER` |yes
+|`PASSWORD` |no
+|`PERMISSION` |no
+|`PERMISSIONS` |no
+|`PRIMARY` |yes
+|`RENAME` |yes
+|`REPLACE` |yes
+|`RETURNS` |no
+|`REVOKE` |yes
+|`ROLE` |no
+|`ROLES` |no
+|`SCHEMA` |yes
+|`SELECT` |yes
+|`SET` |yes
+|`SFUNC` |no
+|`SMALLINT` |no
+|`STATIC` |no
+|`STORAGE` |no
+|`STYPE` |no
+|`SUPERUSER` |no
+|`TABLE` |yes
+|`TEXT` |no
+|`TIME` |no
+|`TIMESTAMP` |no
+|`TIMEUUID` |no
+|`TINYINT` |no
+|`TO` |yes
+|`TOKEN` |yes
+|`TRIGGER` |no
+|`TRUNCATE` |yes
+|`TTL` |no
+|`TUPLE` |no
+|`TYPE` |no
+|`UNLOGGED` |yes
+|`UPDATE` |yes
+|`USE` |yes
+|`USER` |no
+|`USERS` |no
+|`USING` |yes
+|`UUID` |no
+|`VALUES` |no
+|`VARCHAR` |no
+|`VARINT` |no
+|`WHERE` |yes
+|`WITH` |yes
+|`WRITETIME` |no
+|===
+
+== Appendix B: CQL Reserved Types
+
+The following type names are not currently used by CQL, but are reserved
+for potential future use. User-defined types may not use reserved type
+names as their name.
+
+[width="25%",cols="100%",options="header",]
+|===
+|type
+|`bitstring`
+|`byte`
+|`complex`
+|`enum`
+|`interval`
+|`macaddr`
+|===
+
+== Appendix C: Dropping Compact Storage
+
+Starting version 4.0, Thrift and COMPACT STORAGE is no longer supported.
+
+`ALTER ... DROP COMPACT STORAGE` statement makes Compact Tables
+CQL-compatible, exposing internal structure of Thrift/Compact Tables:
+
+* CQL-created Compact Tables that have no clustering columns, will
+expose an additional clustering column `column1` with `UTF8Type`.
+* CQL-created Compact Tables that had no regular columns, will expose a
+regular column `value` with `BytesType`.
+* For CQL-Created Compact Tables, all columns originally defined as
+`regular` will be come `static`
+* CQL-created Compact Tables that have clustering but have no regular
+columns will have an empty value column (of `EmptyType`)
+* SuperColumn Tables (can only be created through Thrift) will expose a
+compact value map with an empty name.
+* Thrift-created Compact Tables will have types corresponding to their
+Thrift definition.
diff --git a/doc/modules/cassandra/pages/cql/changes.adoc b/doc/modules/cassandra/pages/cql/changes.adoc
new file mode 100644
index 0000000..1f89469
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/changes.adoc
@@ -0,0 +1,215 @@
+= Changes
+
+The following describes the changes in each version of CQL.
+
+== 3.4.5
+
+* Adds support for arithmetic operators (`11935`)
+* Adds support for `+` and `-` operations on dates (`11936`)
+* Adds `currentTimestamp`, `currentDate`, `currentTime` and
+`currentTimeUUID` functions (`13132`)
+
+== 3.4.4
+
+* `ALTER TABLE` `ALTER` has been removed; a column's type may not be
+changed after creation (`12443`).
+* `ALTER TYPE` `ALTER` has been removed; a field's type may not be
+changed after creation (`12443`).
+
+== 3.4.3
+
+* Adds a new `duration` `data types <data-types>` (`11873`).
+* Support for `GROUP BY` (`10707`).
+* Adds a `DEFAULT UNSET` option for `INSERT JSON` to ignore omitted
+columns (`11424`).
+* Allows `null` as a legal value for TTL on insert and update. It will
+be treated as equivalent to inserting a 0 (`12216`).
+
+== 3.4.2
+
+* If a table has a non zero `default_time_to_live`, then explicitly
+specifying a TTL of 0 in an `INSERT` or `UPDATE` statement will result
+in the new writes not having any expiration (that is, an explicit TTL of
+0 cancels the `default_time_to_live`). This wasn't the case before and
+the `default_time_to_live` was applied even though a TTL had been
+explicitly set.
+* `ALTER TABLE` `ADD` and `DROP` now allow multiple columns to be
+added/removed.
+* New `PER PARTITION LIMIT` option for `SELECT` statements (see
+https://issues.apache.org/jira/browse/CASSANDRA-7017)[CASSANDRA-7017].
+* `User-defined functions <cql-functions>` can now instantiate
+`UDTValue` and `TupleValue` instances via the new `UDFContext` interface
+(see
+https://issues.apache.org/jira/browse/CASSANDRA-10818)[CASSANDRA-10818].
+* `User-defined types <udts>` may now be stored in a non-frozen form,
+allowing individual fields to be updated and deleted in `UPDATE`
+statements and `DELETE` statements, respectively.
+(https://issues.apache.org/jira/browse/CASSANDRA-7423)[CASSANDRA-7423]).
+
+== 3.4.1
+
+* Adds `CAST` functions.
+
+== 3.4.0
+
+* Support for `materialized views <materialized-views>`.
+* `DELETE` support for inequality expressions and `IN` restrictions on
+any primary key columns.
+* `UPDATE` support for `IN` restrictions on any primary key columns.
+
+== 3.3.1
+
+* The syntax `TRUNCATE TABLE X` is now accepted as an alias for
+`TRUNCATE X`.
+
+== 3.3.0
+
+* `User-defined functions and aggregates <cql-functions>` are now
+supported.
+* Allows double-dollar enclosed strings literals as an alternative to
+single-quote enclosed strings.
+* Introduces Roles to supersede user based authentication and access
+control
+* New `date`, `time`, `tinyint` and `smallint` `data types <data-types>`
+have been added.
+* `JSON support <cql-json>` has been added
+* Adds new time conversion functions and deprecate `dateOf` and
+`unixTimestampOf`.
+
+== 3.2.0
+
+* `User-defined types <udts>` supported.
+* `CREATE INDEX` now supports indexing collection columns, including
+indexing the keys of map collections through the `keys()` function
+* Indexes on collections may be queried using the new `CONTAINS` and
+`CONTAINS KEY` operators
+* `Tuple types <tuples>` were added to hold fixed-length sets of typed
+positional fields.
+* `DROP INDEX` now supports optionally specifying a keyspace.
+
+== 3.1.7
+
+* `SELECT` statements now support selecting multiple rows in a single
+partition using an `IN` clause on combinations of clustering columns.
+* `IF NOT EXISTS` and `IF EXISTS` syntax is now supported by
+`CREATE USER` and `DROP USER` statements, respectively.
+
+== 3.1.6
+
+* A new `uuid()` method has been added.
+* Support for `DELETE ... IF EXISTS` syntax.
+
+== 3.1.5
+
+* It is now possible to group clustering columns in a relation, see
+`WHERE <where-clause>` clauses.
+* Added support for `static columns <static-columns>`.
+
+== 3.1.4
+
+* `CREATE INDEX` now allows specifying options when creating CUSTOM
+indexes.
+
+== 3.1.3
+
+* Millisecond precision formats have been added to the
+`timestamp <timestamps>` parser.
+
+== 3.1.2
+
+* `NaN` and `Infinity` has been added as valid float constants. They are
+now reserved keywords. In the unlikely case you we using them as a
+column identifier (or keyspace/table one), you will now need to double
+quote them.
+
+== 3.1.1
+
+* `SELECT` statement now allows listing the partition keys (using the
+`DISTINCT` modifier). See
+https://issues.apache.org/jira/browse/CASSANDRA-4536[CASSANDRA-4536].
+* The syntax `c IN ?` is now supported in `WHERE` clauses. In that case,
+the value expected for the bind variable will be a list of whatever type
+`c` is.
+* It is now possible to use named bind variables (using `:name` instead
+of `?`).
+
+== 3.1.0
+
+* `ALTER TABLE` `DROP` option added.
+* `SELECT` statement now supports aliases in select clause. Aliases in
+WHERE and ORDER BY clauses are not supported.
+* `CREATE` statements for `KEYSPACE`, `TABLE` and `INDEX` now supports
+an `IF NOT EXISTS` condition. Similarly, `DROP` statements support a
+`IF EXISTS` condition.
+* `INSERT` statements optionally supports a `IF NOT EXISTS` condition
+and `UPDATE` supports `IF` conditions.
+
+== 3.0.5
+
+* `SELECT`, `UPDATE`, and `DELETE` statements now allow empty `IN`
+relations (see
+https://issues.apache.org/jira/browse/CASSANDRA-5626)[CASSANDRA-5626].
+
+== 3.0.4
+
+* Updated the syntax for custom `secondary indexes <secondary-indexes>`.
+* Non-equal condition on the partition key are now never supported, even
+for ordering partitioner as this was not correct (the order was *not*
+the one of the type of the partition key). Instead, the `token` method
+should always be used for range queries on the partition key (see
+`WHERE clauses <where-clause>`).
+
+== 3.0.3
+
+* Support for custom `secondary indexes <secondary-indexes>` has been
+added.
+
+== 3.0.2
+
+* Type validation for the `constants <constants>` has been fixed. For
+instance, the implementation used to allow `'2'` as a valid value for an
+`int` column (interpreting it has the equivalent of `2`), or `42` as a
+valid `blob` value (in which case `42` was interpreted as an hexadecimal
+representation of the blob). This is no longer the case, type validation
+of constants is now more strict. See the `data types <data-types>`
+section for details on which constant is allowed for which type.
+* The type validation fixed of the previous point has lead to the
+introduction of blobs constants to allow the input of blobs. Do note
+that while the input of blobs as strings constant is still supported by
+this version (to allow smoother transition to blob constant), it is now
+deprecated and will be removed by a future version. If you were using
+strings as blobs, you should thus update your client code ASAP to switch
+blob constants.
+* A number of functions to convert native types to blobs have also been
+introduced. Furthermore the token function is now also allowed in select
+clauses. See the `section on functions <cql-functions>` for details.
+
+== 3.0.1
+
+* Date strings (and timestamps) are no longer accepted as valid
+`timeuuid` values. Doing so was a bug in the sense that date string are
+not valid `timeuuid`, and it was thus resulting in
+https://issues.apache.org/jira/browse/CASSANDRA-4936[confusing
+behaviors]. However, the following new methods have been added to help
+working with `timeuuid`: `now`, `minTimeuuid`, `maxTimeuuid` , `dateOf`
+and `unixTimestampOf`.
+* Float constants now support the exponent notation. In other words,
+`4.2E10` is now a valid floating point value.
+
+== Versioning
+
+Versioning of the CQL language adheres to the http://semver.org[Semantic
+Versioning] guidelines. Versions take the form X.Y.Z where X, Y, and Z
+are integer values representing major, minor, and patch level
+respectively. There is no correlation between Cassandra release versions
+and the CQL language version.
+
+[cols=",",options="header",]
+|===
+|version |description
+| Major | The major version _must_ be bumped when backward incompatible changes
+are introduced. This should rarely occur.
+| Minor | Minor version increments occur when new, but backward compatible,
+functionality is introduced.
+| Patch | The patch version is incremented when bugs are fixed.
+|===
diff --git a/doc/modules/cassandra/pages/cql/cql_singlefile.adoc b/doc/modules/cassandra/pages/cql/cql_singlefile.adoc
new file mode 100644
index 0000000..89ed359
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/cql_singlefile.adoc
@@ -0,0 +1,3906 @@
+== Cassandra Query Language (CQL) v3.4.3
+
+\{toc:maxLevel=3}
+
+=== CQL Syntax
+
+==== Preamble
+
+This document describes the Cassandra Query Language (CQL) version 3.
+CQL v3 is not backward compatible with CQL v2 and differs from it in
+numerous ways. Note that this document describes the last version of the
+languages. However, the link:#changes[changes] section provides the diff
+between the different versions of CQL v3.
+
+CQL v3 offers a model very close to SQL in the sense that data is put in
+_tables_ containing _rows_ of _columns_. For that reason, when used in
+this document, these terms (tables, rows and columns) have the same
+definition than they have in SQL. But please note that as such, they do
+*not* refer to the concept of rows and columns found in the internal
+implementation of Cassandra and in the thrift and CQL v2 API.
+
+==== Conventions
+
+To aid in specifying the CQL syntax, we will use the following
+conventions in this document:
+
+* Language rules will be given in a
+http://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form[BNF] -like
+notation:
+
+bc(syntax). ::= TERMINAL
+
+* Nonterminal symbols will have `<angle brackets>`.
+* As additional shortcut notations to BNF, we’ll use traditional regular
+expression’s symbols (`?`, `+` and `*`) to signify that a given symbol
+is optional and/or can be repeated. We’ll also allow parentheses to
+group symbols and the `[<characters>]` notation to represent any one of
+`<characters>`.
+* The grammar is provided for documentation purposes and leave some
+minor details out. For instance, the last column definition in a
+`CREATE TABLE` statement is optional but supported if present even
+though the provided grammar in this document suggest it is not
+supported.
+* Sample code will be provided in a code block:
+
+bc(sample). SELECT sample_usage FROM cql;
+
+* References to keywords or pieces of CQL code in running text will be
+shown in a `fixed-width font`.
+
+[[identifiers]]
+==== Identifiers and keywords
+
+The CQL language uses _identifiers_ (or _names_) to identify tables,
+columns and other objects. An identifier is a token matching the regular
+expression `[a-zA-Z]``[a-zA-Z0-9_]``*`.
+
+A number of such identifiers, like `SELECT` or `WITH`, are _keywords_.
+They have a fixed meaning for the language and most are reserved. The
+list of those keywords can be found in link:#appendixA[Appendix A].
+
+Identifiers and (unquoted) keywords are case insensitive. Thus `SELECT`
+is the same than `select` or `sElEcT`, and `myId` is the same than
+`myid` or `MYID` for instance. A convention often used (in particular by
+the samples of this documentation) is to use upper case for keywords and
+lower case for other identifiers.
+
+There is a second kind of identifiers called _quoted identifiers_
+defined by enclosing an arbitrary sequence of characters in
+double-quotes(`"`). Quoted identifiers are never keywords. Thus
+`"select"` is not a reserved keyword and can be used to refer to a
+column, while `select` would raise a parse error. Also, contrarily to
+unquoted identifiers and keywords, quoted identifiers are case sensitive
+(`"My Quoted Id"` is _different_ from `"my quoted id"`). A fully
+lowercase quoted identifier that matches `[a-zA-Z]``[a-zA-Z0-9_]``*` is
+equivalent to the unquoted identifier obtained by removing the
+double-quote (so `"myid"` is equivalent to `myid` and to `myId` but
+different from `"myId"`). Inside a quoted identifier, the double-quote
+character can be repeated to escape it, so `"foo "" bar"` is a valid
+identifier.
+
+*Warning*: _quoted identifiers_ allows to declare columns with arbitrary
+names, and those can sometime clash with specific names used by the
+server. For instance, when using conditional update, the server will
+respond with a result-set containing a special result named
+`"[applied]"`. If you’ve declared a column with such a name, this could
+potentially confuse some tools and should be avoided. In general,
+unquoted identifiers should be preferred but if you use quoted
+identifiers, it is strongly advised to avoid any name enclosed by
+squared brackets (like `"[applied]"`) and any name that looks like a
+function call (like `"f(x)"`).
+
+==== Constants
+
+CQL defines the following kind of _constants_: strings, integers,
+floats, booleans, uuids and blobs:
+
+* A string constant is an arbitrary sequence of characters characters
+enclosed by single-quote(`'`). One can include a single-quote in a
+string by repeating it, e.g. `'It''s raining today'`. Those are not to
+be confused with quoted identifiers that use double-quotes.
+* An integer constant is defined by `'-'?[0-9]+`.
+* A float constant is defined by
+`'-'?[0-9]+('.'[0-9]*)?([eE][+-]?[0-9+])?`. On top of that, `NaN` and
+`Infinity` are also float constants.
+* A boolean constant is either `true` or `false` up to
+case-insensitivity (i.e. `True` is a valid boolean constant).
+* A http://en.wikipedia.org/wiki/Universally_unique_identifier[UUID]
+constant is defined by `hex{8}-hex{4}-hex{4}-hex{4}-hex{12}` where `hex`
+is an hexadecimal character, e.g. `[0-9a-fA-F]` and `{4}` is the number
+of such characters.
+* A blob constant is an hexadecimal number defined by `0[xX](hex)+`
+where `hex` is an hexadecimal character, e.g. `[0-9a-fA-F]`.
+
+For how these constants are typed, see the link:#types[data types
+section].
+
+==== Comments
+
+A comment in CQL is a line beginning by either double dashes (`--`) or
+double slash (`//`).
+
+Multi-line comments are also supported through enclosure within `/*` and
+`*/` (but nesting is not supported).
+
+bc(sample). +
+— This is a comment +
+// This is a comment too +
+/* This is +
+a multi-line comment */
+
+==== Statements
+
+CQL consists of statements. As in SQL, these statements can be divided
+in 3 categories:
+
+* Data definition statements, that allow to set and change the way data
+is stored.
+* Data manipulation statements, that allow to change data
+* Queries, to look up data
+
+All statements end with a semicolon (`;`) but that semicolon can be
+omitted when dealing with a single statement. The supported statements
+are described in the following sections. When describing the grammar of
+said statements, we will reuse the non-terminal symbols defined below:
+
+bc(syntax).. +
+::= any quoted or unquoted identifier, excluding reserved keywords +
+::= ( `.')?
+
+::= a string constant +
+::= an integer constant +
+::= a float constant +
+::= |  +
+::= a uuid constant +
+::= a boolean constant +
+::= a blob constant
+
+::=  +
+|  +
+|  +
+|  +
+|  +
+::= `?' +
+| `:'  +
+::=  +
+|  +
+|  +
+| `(' ( (`,' )*)? `)'
+
+::=  +
+|  +
+|  +
+::= `\{' ( `:' ( `,' `:' )* )? `}' +
+::= `\{' ( ( `,' )* )? `}' +
+::= `[' ( ( `,' )* )? `]'
+
+::=
+
+::= (AND )* +
+::= `=' ( | | ) +
+p. +
+Please note that not every possible productions of the grammar above
+will be valid in practice. Most notably, `<variable>` and nested
+`<collection-literal>` are currently not allowed inside
+`<collection-literal>`.
+
+A `<variable>` can be either anonymous (a question mark (`?`)) or named
+(an identifier preceded by `:`). Both declare a bind variables for
+link:#preparedStatement[prepared statements]. The only difference
+between an anymous and a named variable is that a named one will be
+easier to refer to (how exactly depends on the client driver used).
+
+The `<properties>` production is use by statement that create and alter
+keyspaces and tables. Each `<property>` is either a _simple_ one, in
+which case it just has a value, or a _map_ one, in which case it’s value
+is a map grouping sub-options. The following will refer to one or the
+other as the _kind_ (_simple_ or _map_) of the property.
+
+A `<tablename>` will be used to identify a table. This is an identifier
+representing the table name that can be preceded by a keyspace name. The
+keyspace name, if provided, allow to identify a table in another
+keyspace than the currently active one (the currently active keyspace is
+set through the `USE` statement).
+
+For supported `<function>`, see the section on
+link:#functions[functions].
+
+Strings can be either enclosed with single quotes or two dollar
+characters. The second syntax has been introduced to allow strings that
+contain single quotes. Typical candidates for such strings are source
+code fragments for user-defined functions.
+
+_Sample:_
+
+bc(sample).. +
+`some string value'
+
+$$double-dollar string can contain single ’ quotes$$ +
+p.
+
+[[preparedStatement]]
+==== Prepared Statement
+
+CQL supports _prepared statements_. Prepared statement is an
+optimization that allows to parse a query only once but execute it
+multiple times with different concrete values.
+
+In a statement, each time a column value is expected (in the data
+manipulation and query statements), a `<variable>` (see above) can be
+used instead. A statement with bind variables must then be _prepared_.
+Once it has been prepared, it can executed by providing concrete values
+for the bind variables. The exact procedure to prepare a statement and
+execute a prepared statement depends on the CQL driver used and is
+beyond the scope of this document.
+
+In addition to providing column values, bind markers may be used to
+provide values for `LIMIT`, `TIMESTAMP`, and `TTL` clauses. If anonymous
+bind markers are used, the names for the query parameters will be
+`[limit]`, `[timestamp]`, and `[ttl]`, respectively.
+
+[[dataDefinition]]
+=== Data Definition
+
+[[createKeyspaceStmt]]
+==== CREATE KEYSPACE
+
+_Syntax:_
+
+bc(syntax).. +
+::= CREATE KEYSPACE (IF NOT EXISTS)? WITH  +
+p. +
+_Sample:_
+
+bc(sample).. +
+CREATE KEYSPACE Excelsior +
+WITH replication = \{’class’: `SimpleStrategy', `replication_factor' :
+3};
+
+CREATE KEYSPACE Excalibur +
+WITH replication = \{’class’: `NetworkTopologyStrategy', `DC1' : 1,
+`DC2' : 3} +
+AND durable_writes = false; +
+p. +
+The `CREATE KEYSPACE` statement creates a new top-level _keyspace_. A
+keyspace is a namespace that defines a replication strategy and some
+options for a set of tables. Valid keyspaces names are identifiers
+composed exclusively of alphanumerical characters and whose length is
+lesser or equal to 32. Note that as identifiers, keyspace names are case
+insensitive: use a quoted identifier for case sensitive keyspace names.
+
+The supported `<properties>` for `CREATE KEYSPACE` are:
+
+[cols=",,,,",options="header",]
+|===
+|name |kind |mandatory |default |description
+|`replication` |_map_ |yes | |The replication strategy and options to
+use for the keyspace.
+
+|`durable_writes` |_simple_ |no |true |Whether to use the commit log for
+updates on this keyspace (disable this option at your own risk!).
+|===
+
+The `replication` `<property>` is mandatory. It must at least contains
+the `'class'` sub-option which defines the replication strategy class to
+use. The rest of the sub-options depends on that replication strategy
+class. By default, Cassandra support the following `'class'`:
+
+* `'SimpleStrategy'`: A simple strategy that defines a simple
+replication factor for the whole cluster. The only sub-options supported
+is `'replication_factor'` to define that replication factor and is
+mandatory.
+* `'NetworkTopologyStrategy'`: A replication strategy that allows to set
+the replication factor independently for each data-center. The rest of
+the sub-options are key-value pairs where each time the key is the name
+of a datacenter and the value the replication factor for that
+data-center.
+
+Attempting to create an already existing keyspace will return an error
+unless the `IF NOT EXISTS` option is used. If it is used, the statement
+will be a no-op if the keyspace already exists.
+
+[[useStmt]]
+==== USE
+
+_Syntax:_
+
+bc(syntax). ::= USE
+
+_Sample:_
+
+bc(sample). USE myApp;
+
+The `USE` statement takes an existing keyspace name as argument and set
+it as the per-connection current working keyspace. All subsequent
+keyspace-specific actions will be performed in the context of the
+selected keyspace, unless link:#statements[otherwise specified], until
+another USE statement is issued or the connection terminates.
+
+[[alterKeyspaceStmt]]
+==== ALTER KEYSPACE
+
+_Syntax:_
+
+bc(syntax).. +
+::= ALTER KEYSPACE WITH  +
+p. +
+_Sample:_
+
+bc(sample).. +
+ALTER KEYSPACE Excelsior +
+WITH replication = \{’class’: `SimpleStrategy', `replication_factor' :
+4};
+
+The `ALTER KEYSPACE` statement alters the properties of an existing
+keyspace. The supported `<properties>` are the same as for the
+link:#createKeyspaceStmt[`CREATE KEYSPACE`] statement.
+
+[[dropKeyspaceStmt]]
+==== DROP KEYSPACE
+
+_Syntax:_
+
+bc(syntax). ::= DROP KEYSPACE ( IF EXISTS )?
+
+_Sample:_
+
+bc(sample). DROP KEYSPACE myApp;
+
+A `DROP KEYSPACE` statement results in the immediate, irreversible
+removal of an existing keyspace, including all column families in it,
+and all data contained in those column families.
+
+If the keyspace does not exists, the statement will return an error,
+unless `IF EXISTS` is used in which case the operation is a no-op.
+
+[[createTableStmt]]
+==== CREATE TABLE
+
+_Syntax:_
+
+bc(syntax).. +
+::= CREATE ( TABLE | COLUMNFAMILY ) ( IF NOT EXISTS )?  +
+`(' ( `,' )* `)' +
+( WITH ( AND )* )?
+
+::= ( STATIC )? ( PRIMARY KEY )? +
+| PRIMARY KEY `(' ( `,' )* `)'
+
+::=  +
+| `(' (`,' )* `)'
+
+::=  +
+| COMPACT STORAGE +
+| CLUSTERING ORDER +
+p. +
+_Sample:_
+
+bc(sample).. +
+CREATE TABLE monkeySpecies ( +
+species text PRIMARY KEY, +
+common_name text, +
+population varint, +
+average_size int +
+) WITH comment=`Important biological records';
+
+CREATE TABLE timeline ( +
+userid uuid, +
+posted_month int, +
+posted_time uuid, +
+body text, +
+posted_by text, +
+PRIMARY KEY (userid, posted_month, posted_time) +
+) WITH compaction = \{ `class' : `LeveledCompactionStrategy' }; +
+p. +
+The `CREATE TABLE` statement creates a new table. Each such table is a
+set of _rows_ (usually representing related entities) for which it
+defines a number of properties. A table is defined by a
+link:#createTableName[name], it defines the columns composing rows of
+the table and have a number of link:#createTableOptions[options]. Note
+that the `CREATE COLUMNFAMILY` syntax is supported as an alias for
+`CREATE TABLE` (for historical reasons).
+
+Attempting to create an already existing table will return an error
+unless the `IF NOT EXISTS` option is used. If it is used, the statement
+will be a no-op if the table already exists.
+
+[[createTableName]]
+===== `<tablename>`
+
+Valid table names are the same as valid
+link:#createKeyspaceStmt[keyspace names] (up to 32 characters long
+alphanumerical identifiers). If the table name is provided alone, the
+table is created within the current keyspace (see `USE`), but if it is
+prefixed by an existing keyspace name (see
+link:#statements[`<tablename>`] grammar), it is created in the specified
+keyspace (but does *not* change the current keyspace).
+
+[[createTableColumn]]
+===== `<column-definition>`
+
+A `CREATE TABLE` statement defines the columns that rows of the table
+can have. A _column_ is defined by its name (an identifier) and its type
+(see the link:#types[data types] section for more details on allowed
+types and their properties).
+
+Within a table, a row is uniquely identified by its `PRIMARY KEY` (or
+more simply the key), and hence all table definitions *must* define a
+PRIMARY KEY (and only one). A `PRIMARY KEY` is composed of one or more
+of the columns defined in the table. If the `PRIMARY KEY` is only one
+column, this can be specified directly after the column definition.
+Otherwise, it must be specified by following `PRIMARY KEY` by the
+comma-separated list of column names composing the key within
+parenthesis. Note that:
+
+bc(sample). +
+CREATE TABLE t ( +
+k int PRIMARY KEY, +
+other text +
+)
+
+is equivalent to
+
+bc(sample). +
+CREATE TABLE t ( +
+k int, +
+other text, +
+PRIMARY KEY (k) +
+)
+
+[[createTablepartitionClustering]]
+===== Partition key and clustering columns
+
+In CQL, the order in which columns are defined for the `PRIMARY KEY`
+matters. The first column of the key is called the _partition key_. It
+has the property that all the rows sharing the same partition key (even
+across table in fact) are stored on the same physical node. Also,
+insertion/update/deletion on rows sharing the same partition key for a
+given table are performed _atomically_ and in _isolation_. Note that it
+is possible to have a composite partition key, i.e. a partition key
+formed of multiple columns, using an extra set of parentheses to define
+which columns forms the partition key.
+
+The remaining columns of the `PRIMARY KEY` definition, if any, are
+called __clustering columns. On a given physical node, rows for a given
+partition key are stored in the order induced by the clustering columns,
+making the retrieval of rows in that clustering order particularly
+efficient (see `SELECT`).
+
+[[createTableStatic]]
+===== `STATIC` columns
+
+Some columns can be declared as `STATIC` in a table definition. A column
+that is static will be ``shared'' by all the rows belonging to the same
+partition (having the same partition key). For instance, in:
+
+bc(sample). +
+CREATE TABLE test ( +
+pk int, +
+t int, +
+v text, +
+s text static, +
+PRIMARY KEY (pk, t) +
+); +
+INSERT INTO test(pk, t, v, s) VALUES (0, 0, `val0', `static0'); +
+INSERT INTO test(pk, t, v, s) VALUES (0, 1, `val1', `static1'); +
+SELECT * FROM test WHERE pk=0 AND t=0;
+
+the last query will return `'static1'` as value for `s`, since `s` is
+static and thus the 2nd insertion modified this ``shared'' value. Note
+however that static columns are only static within a given partition,
+and if in the example above both rows where from different partitions
+(i.e. if they had different value for `pk`), then the 2nd insertion
+would not have modified the value of `s` for the first row.
+
+A few restrictions applies to when static columns are allowed:
+
+* tables with the `COMPACT STORAGE` option (see below) cannot have them
+* a table without clustering columns cannot have static columns (in a
+table without clustering columns, every partition has only one row, and
+so every column is inherently static).
+* only non `PRIMARY KEY` columns can be static
+
+[[createTableOptions]]
+===== `<option>`
+
+The `CREATE TABLE` statement supports a number of options that controls
+the configuration of a new table. These options can be specified after
+the `WITH` keyword.
+
+The first of these option is `COMPACT STORAGE`. This option is mainly
+targeted towards backward compatibility for definitions created before
+CQL3 (see
+http://www.datastax.com/dev/blog/thrift-to-cql3[www.datastax.com/dev/blog/thrift-to-cql3]
+for more details). The option also provides a slightly more compact
+layout of data on disk but at the price of diminished flexibility and
+extensibility for the table. Most notably, `COMPACT STORAGE` tables
+cannot have collections nor static columns and a `COMPACT STORAGE` table
+with at least one clustering column supports exactly one (as in not 0
+nor more than 1) column not part of the `PRIMARY KEY` definition (which
+imply in particular that you cannot add nor remove columns after
+creation). For those reasons, `COMPACT STORAGE` is not recommended
+outside of the backward compatibility reason evoked above.
+
+Another option is `CLUSTERING ORDER`. It allows to define the ordering
+of rows on disk. It takes the list of the clustering column names with,
+for each of them, the on-disk order (Ascending or descending). Note that
+this option affects link:#selectOrderBy[what `ORDER BY` are allowed
+during `SELECT`].
+
+Table creation supports the following other `<property>`:
+
+[cols=",,,",options="header",]
+|===
+|option |kind |default |description
+|`comment` |_simple_ |none |A free-form, human-readable comment.
+
+|`gc_grace_seconds` |_simple_ |864000 |Time to wait before garbage
+collecting tombstones (deletion markers).
+
+|`bloom_filter_fp_chance` |_simple_ |0.00075 |The target probability of
+false positive of the sstable bloom filters. Said bloom filters will be
+sized to provide the provided probability (thus lowering this value
+impact the size of bloom filters in-memory and on-disk)
+
+|`default_time_to_live` |_simple_ |0 |The default expiration time
+(``TTL'') in seconds for a table.
+
+|`compaction` |_map_ |_see below_ |Compaction options, see
+link:#compactionOptions[below].
+
+|`compression` |_map_ |_see below_ |Compression options, see
+link:#compressionOptions[below].
+
+|`caching` |_map_ |_see below_ |Caching options, see
+link:#cachingOptions[below].
+|===
+
+[[compactionOptions]]
+===== Compaction options
+
+The `compaction` property must at least define the `'class'` sub-option,
+that defines the compaction strategy class to use. The default supported
+class are `'SizeTieredCompactionStrategy'`,
+`'LeveledCompactionStrategy'`, `'DateTieredCompactionStrategy'` and
+`'TimeWindowCompactionStrategy'`. Custom strategy can be provided by
+specifying the full class name as a link:#constants[string constant].
+The rest of the sub-options depends on the chosen class. The sub-options
+supported by the default classes are:
+
+[cols=",,,",options="header",]
+|===
+|option |supported compaction strategy |default |description
+|`enabled` |_all_ |true |A boolean denoting whether compaction should be
+enabled or not.
+
+|`tombstone_threshold` |_all_ |0.2 |A ratio such that if a sstable has
+more than this ratio of gcable tombstones over all contained columns,
+the sstable will be compacted (with no other sstables) for the purpose
+of purging those tombstones.
+
+|`tombstone_compaction_interval` |_all_ |1 day |The minimum time to wait
+after an sstable creation time before considering it for ``tombstone
+compaction'', where ``tombstone compaction'' is the compaction triggered
+if the sstable has more gcable tombstones than `tombstone_threshold`.
+
+|`unchecked_tombstone_compaction` |_all_ |false |Setting this to true
+enables more aggressive tombstone compactions - single sstable tombstone
+compactions will run without checking how likely it is that they will be
+successful.
+
+|`min_sstable_size` |SizeTieredCompactionStrategy |50MB |The size tiered
+strategy groups SSTables to compact in buckets. A bucket groups SSTables
+that differs from less than 50% in size. However, for small sizes, this
+would result in a bucketing that is too fine grained. `min_sstable_size`
+defines a size threshold (in bytes) below which all SSTables belong to
+one unique bucket
+
+|`min_threshold` |SizeTieredCompactionStrategy |4 |Minimum number of
+SSTables needed to start a minor compaction.
+
+|`max_threshold` |SizeTieredCompactionStrategy |32 |Maximum number of
+SSTables processed by one minor compaction.
+
+|`bucket_low` |SizeTieredCompactionStrategy |0.5 |Size tiered consider
+sstables to be within the same bucket if their size is within
+[average_size * `bucket_low`, average_size * `bucket_high` ] (i.e the
+default groups sstable whose sizes diverges by at most 50%)
+
+|`bucket_high` |SizeTieredCompactionStrategy |1.5 |Size tiered consider
+sstables to be within the same bucket if their size is within
+[average_size * `bucket_low`, average_size * `bucket_high` ] (i.e the
+default groups sstable whose sizes diverges by at most 50%).
+
+|`sstable_size_in_mb` |LeveledCompactionStrategy |5MB |The target size
+(in MB) for sstables in the leveled strategy. Note that while sstable
+sizes should stay less or equal to `sstable_size_in_mb`, it is possible
+to exceptionally have a larger sstable as during compaction, data for a
+given partition key are never split into 2 sstables
+
+|`timestamp_resolution` |DateTieredCompactionStrategy |MICROSECONDS |The
+timestamp resolution used when inserting data, could be MILLISECONDS,
+MICROSECONDS etc (should be understandable by Java TimeUnit) - don’t
+change this unless you do mutations with USING TIMESTAMP (or equivalent
+directly in the client)
+
+|`base_time_seconds` |DateTieredCompactionStrategy |60 |The base size of
+the time windows.
+
+|`max_sstable_age_days` |DateTieredCompactionStrategy |365 |SSTables
+only containing data that is older than this will never be compacted.
+
+|`timestamp_resolution` |TimeWindowCompactionStrategy |MICROSECONDS |The
+timestamp resolution used when inserting data, could be MILLISECONDS,
+MICROSECONDS etc (should be understandable by Java TimeUnit) - don’t
+change this unless you do mutations with USING TIMESTAMP (or equivalent
+directly in the client)
+
+|`compaction_window_unit` |TimeWindowCompactionStrategy |DAYS |The Java
+TimeUnit used for the window size, set in conjunction with
+`compaction_window_size`. Must be one of DAYS, HOURS, MINUTES
+
+|`compaction_window_size` |TimeWindowCompactionStrategy |1 |The number
+of `compaction_window_unit` units that make up a time window.
+
+|`unsafe_aggressive_sstable_expiration` |TimeWindowCompactionStrategy
+|false |Expired sstables will be dropped without checking its data is
+shadowing other sstables. This is a potentially risky option that can
+lead to data loss or deleted data re-appearing, going beyond what
+`unchecked_tombstone_compaction` does for single sstable compaction. Due
+to the risk the jvm must also be started with
+`-Dcassandra.unsafe_aggressive_sstable_expiration=true`.
+|===
+
+[[compressionOptions]]
+===== Compression options
+
+For the `compression` property, the following sub-options are available:
+
+[cols=",,,,,",options="header",]
+|===
+|option |default |description | | |
+|`class` |LZ4Compressor |The compression algorithm to use. Default
+compressor are: LZ4Compressor, SnappyCompressor and DeflateCompressor.
+Use `'enabled' : false` to disable compression. Custom compressor can be
+provided by specifying the full class name as a link:#constants[string
+constant]. | | |
+
+|`enabled` |true |By default compression is enabled. To disable it, set
+`enabled` to `false` |`chunk_length_in_kb` |64KB |On disk SSTables are
+compressed by block (to allow random reads). This defines the size (in
+KB) of said block. Bigger values may improve the compression rate, but
+increases the minimum size of data to be read from disk for a read
+
+|`crc_check_chance` |1.0 |When compression is enabled, each compressed
+block includes a checksum of that block for the purpose of detecting
+disk bitrot and avoiding the propagation of corruption to other replica.
+This option defines the probability with which those checksums are
+checked during read. By default they are always checked. Set to 0 to
+disable checksum checking and to 0.5 for instance to check them every
+other read | | |
+|===
+
+[[cachingOptions]]
+===== Caching options
+
+For the `caching` property, the following sub-options are available:
+
+[cols=",,",options="header",]
+|===
+|option |default |description
+|`keys` |ALL |Whether to cache keys (``key cache'') for this table.
+Valid values are: `ALL` and `NONE`.
+
+|`rows_per_partition` |NONE |The amount of rows to cache per partition
+(``row cache''). If an integer `n` is specified, the first `n` queried
+rows of a partition will be cached. Other possible options are `ALL`, to
+cache all rows of a queried partition, or `NONE` to disable row caching.
+|===
+
+===== Other considerations:
+
+* When link:#insertStmt[inserting] / link:#updateStmt[updating] a given
+row, not all columns needs to be defined (except for those part of the
+key), and missing columns occupy no space on disk. Furthermore, adding
+new columns (see `ALTER TABLE`) is a constant time operation. There is
+thus no need to try to anticipate future usage (or to cry when you
+haven’t) when creating a table.
+
+[[alterTableStmt]]
+==== ALTER TABLE
+
+_Syntax:_
+
+bc(syntax).. +
+::= ALTER (TABLE | COLUMNFAMILY)
+
+::= ADD  +
+| ADD ( ( , )* ) +
+| DROP  +
+| DROP ( ( , )* ) +
+| WITH ( AND )* +
+p. +
+_Sample:_
+
+bc(sample).. +
+ALTER TABLE addamsFamily
+
+ALTER TABLE addamsFamily +
+ADD gravesite varchar;
+
+ALTER TABLE addamsFamily +
+WITH comment = `A most excellent and useful column family'; +
+p. +
+The `ALTER` statement is used to manipulate table definitions. It allows
+for adding new columns, dropping existing ones, or updating the table
+options. As with table creation, `ALTER COLUMNFAMILY` is allowed as an
+alias for `ALTER TABLE`.
+
+The `<tablename>` is the table name optionally preceded by the keyspace
+name. The `<instruction>` defines the alteration to perform:
+
+* `ADD`: Adds a new column to the table. The `<identifier>` for the new
+column must not conflict with an existing column. Moreover, columns
+cannot be added to tables defined with the `COMPACT STORAGE` option.
+* `DROP`: Removes a column from the table. Dropped columns will
+immediately become unavailable in the queries and will not be included
+in compacted sstables in the future. If a column is readded, queries
+won’t return values written before the column was last dropped. It is
+assumed that timestamps represent actual time, so if this is not your
+case, you should NOT readd previously dropped columns. Columns can’t be
+dropped from tables defined with the `COMPACT STORAGE` option.
+* `WITH`: Allows to update the options of the table. The
+link:#createTableOptions[supported `<option>`] (and syntax) are the same
+as for the `CREATE TABLE` statement except that `COMPACT STORAGE` is not
+supported. Note that setting any `compaction` sub-options has the effect
+of erasing all previous `compaction` options, so you need to re-specify
+all the sub-options if you want to keep them. The same note applies to
+the set of `compression` sub-options.
+
+===== CQL type compatibility:
+
+CQL data types may be converted only as the following table.
+
+[cols=",",options="header",]
+|===
+|Data type may be altered to: |Data type
+|timestamp |bigint
+
+|ascii, bigint, boolean, date, decimal, double, float, inet, int,
+smallint, text, time, timestamp, timeuuid, tinyint, uuid, varchar,
+varint |blob
+
+|int |date
+
+|ascii, varchar |text
+
+|bigint |time
+
+|bigint |timestamp
+
+|timeuuid |uuid
+
+|ascii, text |varchar
+
+|bigint, int, timestamp |varint
+|===
+
+Clustering columns have stricter requirements, only the below
+conversions are allowed.
+
+[cols=",",options="header",]
+|===
+|Data type may be altered to: |Data type
+|ascii, text, varchar |blob
+|ascii, varchar |text
+|ascii, text |varchar
+|===
+
+[[dropTableStmt]]
+==== DROP TABLE
+
+_Syntax:_
+
+bc(syntax). ::= DROP TABLE ( IF EXISTS )?
+
+_Sample:_
+
+bc(sample). DROP TABLE worldSeriesAttendees;
+
+The `DROP TABLE` statement results in the immediate, irreversible
+removal of a table, including all data contained in it. As for table
+creation, `DROP COLUMNFAMILY` is allowed as an alias for `DROP TABLE`.
+
+If the table does not exist, the statement will return an error, unless
+`IF EXISTS` is used in which case the operation is a no-op.
+
+[[truncateStmt]]
+==== TRUNCATE
+
+_Syntax:_
+
+bc(syntax). ::= TRUNCATE ( TABLE | COLUMNFAMILY )?
+
+_Sample:_
+
+bc(sample). TRUNCATE superImportantData;
+
+The `TRUNCATE` statement permanently removes all data from a table.
+
+[[createIndexStmt]]
+==== CREATE INDEX
+
+_Syntax:_
+
+bc(syntax).. +
+::= CREATE ( CUSTOM )? INDEX ( IF NOT EXISTS )? ( )? +
+ON `(' `)' +
+( USING ( WITH OPTIONS = )? )?
+
+::=  +
+| keys( ) +
+p. +
+_Sample:_
+
+bc(sample). +
+CREATE INDEX userIndex ON NerdMovies (user); +
+CREATE INDEX ON Mutants (abilityId); +
+CREATE INDEX ON users (keys(favs)); +
+CREATE CUSTOM INDEX ON users (email) USING `path.to.the.IndexClass'; +
+CREATE CUSTOM INDEX ON users (email) USING `path.to.the.IndexClass' WITH
+OPTIONS = \{’storage’: `/mnt/ssd/indexes/'};
+
+The `CREATE INDEX` statement is used to create a new (automatic)
+secondary index for a given (existing) column in a given table. A name
+for the index itself can be specified before the `ON` keyword, if
+desired. If data already exists for the column, it will be indexed
+asynchronously. After the index is created, new data for the column is
+indexed automatically at insertion time.
+
+Attempting to create an already existing index will return an error
+unless the `IF NOT EXISTS` option is used. If it is used, the statement
+will be a no-op if the index already exists.
+
+[[keysIndex]]
+===== Indexes on Map Keys
+
+When creating an index on a link:#map[map column], you may index either
+the keys or the values. If the column identifier is placed within the
+`keys()` function, the index will be on the map keys, allowing you to
+use `CONTAINS KEY` in `WHERE` clauses. Otherwise, the index will be on
+the map values.
+
+[[dropIndexStmt]]
+==== DROP INDEX
+
+_Syntax:_
+
+bc(syntax). ::= DROP INDEX ( IF EXISTS )? ( `.' )?
+
+_Sample:_
+
+bc(sample).. +
+DROP INDEX userIndex;
+
+DROP INDEX userkeyspace.address_index; +
+p. +
+The `DROP INDEX` statement is used to drop an existing secondary index.
+The argument of the statement is the index name, which may optionally
+specify the keyspace of the index.
+
+If the index does not exists, the statement will return an error, unless
+`IF EXISTS` is used in which case the operation is a no-op.
+
+[[createMVStmt]]
+==== CREATE MATERIALIZED VIEW
+
+_Syntax:_
+
+bc(syntax).. +
+::= CREATE MATERIALIZED VIEW ( IF NOT EXISTS )? AS +
+SELECT ( `(' ( `,' ) * `)' | `*' ) +
+FROM  +
+( WHERE )? +
+PRIMARY KEY `(' ( `,' )* `)' +
+( WITH ( AND )* )? +
+p. +
+_Sample:_
+
+bc(sample).. +
+CREATE MATERIALIZED VIEW monkeySpecies_by_population AS +
+SELECT * +
+FROM monkeySpecies +
+WHERE population IS NOT NULL AND species IS NOT NULL +
+PRIMARY KEY (population, species) +
+WITH comment=`Allow query by population instead of species'; +
+p. +
+The `CREATE MATERIALIZED VIEW` statement creates a new materialized
+view. Each such view is a set of _rows_ which corresponds to rows which
+are present in the underlying, or base, table specified in the `SELECT`
+statement. A materialized view cannot be directly updated, but updates
+to the base table will cause corresponding updates in the view.
+
+Attempting to create an already existing materialized view will return
+an error unless the `IF NOT EXISTS` option is used. If it is used, the
+statement will be a no-op if the materialized view already exists.
+
+[[createMVWhere]]
+===== `WHERE` Clause
+
+The `<where-clause>` is similar to the link:#selectWhere[where clause of
+a `SELECT` statement], with a few differences. First, the where clause
+must contain an expression that disallows `NULL` values in columns in
+the view’s primary key. If no other restriction is desired, this can be
+accomplished with an `IS NOT NULL` expression. Second, only columns
+which are in the base table’s primary key may be restricted with
+expressions other than `IS NOT NULL`. (Note that this second restriction
+may be lifted in the future.)
+
+[[alterMVStmt]]
+==== ALTER MATERIALIZED VIEW
+
+_Syntax:_
+
+bc(syntax). ::= ALTER MATERIALIZED VIEW  +
+WITH ( AND )*
+
+The `ALTER MATERIALIZED VIEW` statement allows options to be update;
+these options are the same as `CREATE TABLE`’s options.
+
+[[dropMVStmt]]
+==== DROP MATERIALIZED VIEW
+
+_Syntax:_
+
+bc(syntax). ::= DROP MATERIALIZED VIEW ( IF EXISTS )?
+
+_Sample:_
+
+bc(sample). DROP MATERIALIZED VIEW monkeySpecies_by_population;
+
+The `DROP MATERIALIZED VIEW` statement is used to drop an existing
+materialized view.
+
+If the materialized view does not exists, the statement will return an
+error, unless `IF EXISTS` is used in which case the operation is a
+no-op.
+
+[[createTypeStmt]]
+==== CREATE TYPE
+
+_Syntax:_
+
+bc(syntax).. +
+::= CREATE TYPE ( IF NOT EXISTS )?  +
+`(' ( `,' )* `)'
+
+::= ( `.' )?
+
+::=
+
+_Sample:_
+
+bc(sample).. +
+CREATE TYPE address ( +
+street_name text, +
+street_number int, +
+city text, +
+state text, +
+zip int +
+)
+
+CREATE TYPE work_and_home_addresses ( +
+home_address address, +
+work_address address +
+) +
+p. +
+The `CREATE TYPE` statement creates a new user-defined type. Each type
+is a set of named, typed fields. Field types may be any valid type,
+including collections and other existing user-defined types.
+
+Attempting to create an already existing type will result in an error
+unless the `IF NOT EXISTS` option is used. If it is used, the statement
+will be a no-op if the type already exists.
+
+[[createTypeName]]
+===== `<typename>`
+
+Valid type names are identifiers. The names of existing CQL types and
+link:#appendixB[reserved type names] may not be used.
+
+If the type name is provided alone, the type is created with the current
+keyspace (see `USE`). If it is prefixed by an existing keyspace name,
+the type is created within the specified keyspace instead of the current
+keyspace.
+
+[[alterTypeStmt]]
+==== ALTER TYPE
+
+_Syntax:_
+
+bc(syntax).. +
+::= ALTER TYPE
+
+::= ADD  +
+| RENAME TO ( AND TO )* +
+p. +
+_Sample:_
+
+bc(sample).. +
+ALTER TYPE address ADD country text
+
+ALTER TYPE address RENAME zip TO zipcode AND street_name TO street +
+p. +
+The `ALTER TYPE` statement is used to manipulate type definitions. It
+allows for adding new fields, renaming existing fields, or changing the
+type of existing fields.
+
+[[dropTypeStmt]]
+==== DROP TYPE
+
+_Syntax:_
+
+bc(syntax).. +
+::= DROP TYPE ( IF EXISTS )?  +
+p. +
+The `DROP TYPE` statement results in the immediate, irreversible removal
+of a type. Attempting to drop a type that is still in use by another
+type or a table will result in an error.
+
+If the type does not exist, an error will be returned unless `IF EXISTS`
+is used, in which case the operation is a no-op.
+
+[[createTriggerStmt]]
+==== CREATE TRIGGER
+
+_Syntax:_
+
+bc(syntax).. +
+::= CREATE TRIGGER ( IF NOT EXISTS )? ( )? +
+ON  +
+USING
+
+_Sample:_
+
+bc(sample). +
+CREATE TRIGGER myTrigger ON myTable USING
+`org.apache.cassandra.triggers.InvertedIndex';
+
+The actual logic that makes up the trigger can be written in any Java
+(JVM) language and exists outside the database. You place the trigger
+code in a `lib/triggers` subdirectory of the Cassandra installation
+directory, it loads during cluster startup, and exists on every node
+that participates in a cluster. The trigger defined on a table fires
+before a requested DML statement occurs, which ensures the atomicity of
+the transaction.
+
+[[dropTriggerStmt]]
+==== DROP TRIGGER
+
+_Syntax:_
+
+bc(syntax).. +
+::= DROP TRIGGER ( IF EXISTS )? ( )? +
+ON  +
+p. +
+_Sample:_
+
+bc(sample). +
+DROP TRIGGER myTrigger ON myTable;
+
+`DROP TRIGGER` statement removes the registration of a trigger created
+using `CREATE TRIGGER`.
+
+[[createFunctionStmt]]
+==== CREATE FUNCTION
+
+_Syntax:_
+
+bc(syntax).. +
+::= CREATE ( OR REPLACE )? +
+FUNCTION ( IF NOT EXISTS )? +
+( `.' )?  +
+`(' ( `,' )* `)' +
+( CALLED | RETURNS NULL ) ON NULL INPUT +
+RETURNS  +
+LANGUAGE  +
+AS
+
+_Sample:_
+
+bc(sample). +
+CREATE OR REPLACE FUNCTION somefunction +
+( somearg int, anotherarg text, complexarg frozen, listarg list ) +
+RETURNS NULL ON NULL INPUT +
+RETURNS text +
+LANGUAGE java +
+AS $$ +
+// some Java code +
+$$; +
+CREATE FUNCTION akeyspace.fname IF NOT EXISTS +
+( someArg int ) +
+CALLED ON NULL INPUT +
+RETURNS text +
+LANGUAGE java +
+AS $$ +
+// some Java code +
+$$;
+
+`CREATE FUNCTION` creates or replaces a user-defined function.
+
+[[functionSignature]]
+===== Function Signature
+
+Signatures are used to distinguish individual functions. The signature
+consists of:
+
+. The fully qualified function name - i.e _keyspace_ plus
+_function-name_
+. The concatenated list of all argument types
+
+Note that keyspace names, function names and argument types are subject
+to the default naming conventions and case-sensitivity rules.
+
+`CREATE FUNCTION` with the optional `OR REPLACE` keywords either creates
+a function or replaces an existing one with the same signature. A
+`CREATE FUNCTION` without `OR REPLACE` fails if a function with the same
+signature already exists.
+
+Behavior on invocation with `null` values must be defined for each
+function. There are two options:
+
+. `RETURNS NULL ON NULL INPUT` declares that the function will always
+return `null` if any of the input arguments is `null`.
+. `CALLED ON NULL INPUT` declares that the function will always be
+executed.
+
+If the optional `IF NOT EXISTS` keywords are used, the function will
+only be created if another function with the same signature does not
+exist.
+
+`OR REPLACE` and `IF NOT EXIST` cannot be used together.
+
+Functions belong to a keyspace. If no keyspace is specified in
+`<function-name>`, the current keyspace is used (i.e. the keyspace
+specified using the link:#useStmt[`USE`] statement). It is not possible
+to create a user-defined function in one of the system keyspaces.
+
+See the section on link:#udfs[user-defined functions] for more
+information.
+
+[[dropFunctionStmt]]
+==== DROP FUNCTION
+
+_Syntax:_
+
+bc(syntax).. +
+::= DROP FUNCTION ( IF EXISTS )? +
+( `.' )?  +
+( `(' ( `,' )* `)' )?
+
+_Sample:_
+
+bc(sample). +
+DROP FUNCTION myfunction; +
+DROP FUNCTION mykeyspace.afunction; +
+DROP FUNCTION afunction ( int ); +
+DROP FUNCTION afunction ( text );
+
+`DROP FUNCTION` statement removes a function created using
+`CREATE FUNCTION`. +
+You must specify the argument types (link:#functionSignature[signature]
+) of the function to drop if there are multiple functions with the same
+name but a different signature (overloaded functions).
+
+`DROP FUNCTION` with the optional `IF EXISTS` keywords drops a function
+if it exists.
+
+[[createAggregateStmt]]
+==== CREATE AGGREGATE
+
+_Syntax:_
+
+bc(syntax).. +
+::= CREATE ( OR REPLACE )? +
+AGGREGATE ( IF NOT EXISTS )? +
+( `.' )?  +
+`(' ( `,' )* `)' +
+SFUNC  +
+STYPE  +
+( FINALFUNC )? +
+( INITCOND )? +
+p. +
+_Sample:_
+
+bc(sample). +
+CREATE AGGREGATE myaggregate ( val text ) +
+SFUNC myaggregate_state +
+STYPE text +
+FINALFUNC myaggregate_final +
+INITCOND `foo';
+
+See the section on link:#udas[user-defined aggregates] for a complete
+example.
+
+`CREATE AGGREGATE` creates or replaces a user-defined aggregate.
+
+`CREATE AGGREGATE` with the optional `OR REPLACE` keywords either
+creates an aggregate or replaces an existing one with the same
+signature. A `CREATE AGGREGATE` without `OR REPLACE` fails if an
+aggregate with the same signature already exists.
+
+`CREATE AGGREGATE` with the optional `IF NOT EXISTS` keywords either
+creates an aggregate if it does not already exist.
+
+`OR REPLACE` and `IF NOT EXIST` cannot be used together.
+
+Aggregates belong to a keyspace. If no keyspace is specified in
+`<aggregate-name>`, the current keyspace is used (i.e. the keyspace
+specified using the link:#useStmt[`USE`] statement). It is not possible
+to create a user-defined aggregate in one of the system keyspaces.
+
+Signatures for user-defined aggregates follow the
+link:#functionSignature[same rules] as for user-defined functions.
+
+`STYPE` defines the type of the state value and must be specified.
+
+The optional `INITCOND` defines the initial state value for the
+aggregate. It defaults to `null`. A non-`null` `INITCOND` must be
+specified for state functions that are declared with
+`RETURNS NULL ON NULL INPUT`.
+
+`SFUNC` references an existing function to be used as the state
+modifying function. The type of first argument of the state function
+must match `STYPE`. The remaining argument types of the state function
+must match the argument types of the aggregate function. State is not
+updated for state functions declared with `RETURNS NULL ON NULL INPUT`
+and called with `null`.
+
+The optional `FINALFUNC` is called just before the aggregate result is
+returned. It must take only one argument with type `STYPE`. The return
+type of the `FINALFUNC` may be a different type. A final function
+declared with `RETURNS NULL ON NULL INPUT` means that the aggregate’s
+return value will be `null`, if the last state is `null`.
+
+If no `FINALFUNC` is defined, the overall return type of the aggregate
+function is `STYPE`. If a `FINALFUNC` is defined, it is the return type
+of that function.
+
+See the section on link:#udas[user-defined aggregates] for more
+information.
+
+[[dropAggregateStmt]]
+==== DROP AGGREGATE
+
+_Syntax:_
+
+bc(syntax).. +
+::= DROP AGGREGATE ( IF EXISTS )? +
+( `.' )?  +
+( `(' ( `,' )* `)' )? +
+p.
+
+_Sample:_
+
+bc(sample). +
+DROP AGGREGATE myAggregate; +
+DROP AGGREGATE myKeyspace.anAggregate; +
+DROP AGGREGATE someAggregate ( int ); +
+DROP AGGREGATE someAggregate ( text );
+
+The `DROP AGGREGATE` statement removes an aggregate created using
+`CREATE AGGREGATE`. You must specify the argument types of the aggregate
+to drop if there are multiple aggregates with the same name but a
+different signature (overloaded aggregates).
+
+`DROP AGGREGATE` with the optional `IF EXISTS` keywords drops an
+aggregate if it exists, and does nothing if a function with the
+signature does not exist.
+
+Signatures for user-defined aggregates follow the
+link:#functionSignature[same rules] as for user-defined functions.
+
+[[dataManipulation]]
+=== Data Manipulation
+
+[[insertStmt]]
+==== INSERT
+
+_Syntax:_
+
+bc(syntax).. +
+::= INSERT INTO  +
+( ( VALUES ) +
+| ( JSON )) +
+( IF NOT EXISTS )? +
+( USING ( AND )* )?
+
+::= `(' ( `,' )* `)'
+
+::= `(' ( `,' )* `)'
+
+::= TIMESTAMP  +
+| TTL  +
+p. +
+_Sample:_
+
+bc(sample).. +
+INSERT INTO NerdMovies (movie, director, main_actor, year) +
+VALUES (`Serenity', `Joss Whedon', `Nathan Fillion', 2005) +
+USING TTL 86400;
+
+INSERT INTO NerdMovies JSON `\{``movie'': ``Serenity'', ``director'':
+``Joss Whedon'', ``year'': 2005}' +
+p. +
+The `INSERT` statement writes one or more columns for a given row in a
+table. Note that since a row is identified by its `PRIMARY KEY`, at
+least the columns composing it must be specified. The list of columns to
+insert to must be supplied when using the `VALUES` syntax. When using
+the `JSON` syntax, they are optional. See the section on
+link:#insertJson[`INSERT JSON`] for more details.
+
+Note that unlike in SQL, `INSERT` does not check the prior existence of
+the row by default: the row is created if none existed before, and
+updated otherwise. Furthermore, there is no mean to know which of
+creation or update happened.
+
+It is however possible to use the `IF NOT EXISTS` condition to only
+insert if the row does not exist prior to the insertion. But please note
+that using `IF NOT EXISTS` will incur a non negligible performance cost
+(internally, Paxos will be used) so this should be used sparingly.
+
+All updates for an `INSERT` are applied atomically and in isolation.
+
+Please refer to the link:#updateOptions[`UPDATE`] section for
+information on the `<option>` available and to the
+link:#collections[collections] section for use of
+`<collection-literal>`. Also note that `INSERT` does not support
+counters, while `UPDATE` does.
+
+[[updateStmt]]
+==== UPDATE
+
+_Syntax:_
+
+bc(syntax).. +
+::= UPDATE  +
+( USING ( AND )* )? +
+SET ( `,' )* +
+WHERE  +
+( IF ( AND condition )* )?
+
+::= `='  +
+| `=' (`+' | `-') ( | | ) +
+| `=' `+'  +
+| `[' `]' `='  +
+| `.' `='
+
+::=  +
+| IN  +
+| `[' `]'  +
+| `[' `]' IN  +
+| `.'  +
+| `.' IN
+
+::= `<' | `<=' | `=' | `!=' | `>=' | `>' +
+::= ( | `(' ( ( `,' )* )? `)')
+
+::= ( AND )*
+
+::= `='  +
+| `(' (`,' )* `)' `='  +
+| IN `(' ( ( `,' )* )? `)' +
+| IN  +
+| `(' (`,' )* `)' IN `(' ( ( `,' )* )? `)' +
+| `(' (`,' )* `)' IN
+
+::= TIMESTAMP  +
+| TTL  +
+p. +
+_Sample:_
+
+bc(sample).. +
+UPDATE NerdMovies USING TTL 400 +
+SET director = `Joss Whedon', +
+main_actor = `Nathan Fillion', +
+year = 2005 +
+WHERE movie = `Serenity';
+
+UPDATE UserActions SET total = total + 2 WHERE user =
+B70DE1D0-9908-4AE3-BE34-5573E5B09F14 AND action = `click'; +
+p. +
+The `UPDATE` statement writes one or more columns for a given row in a
+table. The `<where-clause>` is used to select the row to update and must
+include all columns composing the `PRIMARY KEY`. Other columns values
+are specified through `<assignment>` after the `SET` keyword.
+
+Note that unlike in SQL, `UPDATE` does not check the prior existence of
+the row by default (except through the use of `<condition>`, see below):
+the row is created if none existed before, and updated otherwise.
+Furthermore, there are no means to know whether a creation or update
+occurred.
+
+It is however possible to use the conditions on some columns through
+`IF`, in which case the row will not be updated unless the conditions
+are met. But, please note that using `IF` conditions will incur a
+non-negligible performance cost (internally, Paxos will be used) so this
+should be used sparingly.
+
+In an `UPDATE` statement, all updates within the same partition key are
+applied atomically and in isolation.
+
+The `c = c + 3` form of `<assignment>` is used to increment/decrement
+counters. The identifier after the `=' sign *must* be the same than the
+one before the `=' sign (Only increment/decrement is supported on
+counters, not the assignment of a specific value).
+
+The `id = id + <collection-literal>` and `id[value1] = value2` forms of
+`<assignment>` are for collections. Please refer to the
+link:#collections[relevant section] for more details.
+
+The `id.field = <term>` form of `<assignemt>` is for setting the value
+of a single field on a non-frozen user-defined types.
+
+[[updateOptions]]
+===== `<options>`
+
+The `UPDATE` and `INSERT` statements support the following options:
+
+* `TIMESTAMP`: sets the timestamp for the operation. If not specified,
+the coordinator will use the current time (in microseconds) at the start
+of statement execution as the timestamp. This is usually a suitable
+default.
+* `TTL`: specifies an optional Time To Live (in seconds) for the
+inserted values. If set, the inserted values are automatically removed
+from the database after the specified time. Note that the TTL concerns
+the inserted values, not the columns themselves. This means that any
+subsequent update of the column will also reset the TTL (to whatever TTL
+is specified in that update). By default, values never expire. A TTL of
+0 is equivalent to no TTL. If the table has a default_time_to_live, a
+TTL of 0 will remove the TTL for the inserted or updated values.
+
+[[deleteStmt]]
+==== DELETE
+
+_Syntax:_
+
+bc(syntax).. +
+::= DELETE ( ( `,' )* )? +
+FROM  +
+( USING TIMESTAMP )? +
+WHERE  +
+( IF ( EXISTS | ( ( AND )*) ) )?
+
+::=  +
+| `[' `]' +
+| `.'
+
+::= ( AND )*
+
+::=  +
+| `(' (`,' )* `)'  +
+| IN `(' ( ( `,' )* )? `)' +
+| IN  +
+| `(' (`,' )* `)' IN `(' ( ( `,' )* )? `)' +
+| `(' (`,' )* `)' IN
+
+::= `=' | `<' | `>' | `<=' | `>=' +
+::= ( | `(' ( ( `,' )* )? `)')
+
+::= ( | `!=')  +
+| IN  +
+| `[' `]' ( | `!=')  +
+| `[' `]' IN  +
+| `.' ( | `!=')  +
+| `.' IN
+
+_Sample:_
+
+bc(sample).. +
+DELETE FROM NerdMovies USING TIMESTAMP 1240003134 WHERE movie =
+`Serenity';
+
+DELETE phone FROM Users WHERE userid IN
+(C73DE1D3-AF08-40F3-B124-3FF3E5109F22,
+B70DE1D0-9908-4AE3-BE34-5573E5B09F14); +
+p. +
+The `DELETE` statement deletes columns and rows. If column names are
+provided directly after the `DELETE` keyword, only those columns are
+deleted from the row indicated by the `<where-clause>`. The `id[value]`
+syntax in `<selection>` is for non-frozen collections (please refer to
+the link:#collections[collection section] for more details). The
+`id.field` syntax is for the deletion of non-frozen user-defined types.
+Otherwise, whole rows are removed. The `<where-clause>` specifies which
+rows are to be deleted. Multiple rows may be deleted with one statement
+by using an `IN` clause. A range of rows may be deleted using an
+inequality operator (such as `>=`).
+
+`DELETE` supports the `TIMESTAMP` option with the same semantics as the
+link:#updateStmt[`UPDATE`] statement.
+
+In a `DELETE` statement, all deletions within the same partition key are
+applied atomically and in isolation.
+
+A `DELETE` operation can be conditional through the use of an `IF`
+clause, similar to `UPDATE` and `INSERT` statements. However, as with
+`INSERT` and `UPDATE` statements, this will incur a non-negligible
+performance cost (internally, Paxos will be used) and so should be used
+sparingly.
+
+[[batchStmt]]
+==== BATCH
+
+_Syntax:_
+
+bc(syntax).. +
+::= BEGIN ( UNLOGGED | COUNTER ) BATCH +
+( USING ( AND )* )? +
+( `;' )* +
+APPLY BATCH
+
+::=  +
+|  +
+|
+
+::= TIMESTAMP  +
+p. +
+_Sample:_
+
+bc(sample). +
+BEGIN BATCH +
+INSERT INTO users (userid, password, name) VALUES (`user2', `ch@ngem3b',
+`second user'); +
+UPDATE users SET password = `ps22dhds' WHERE userid = `user3'; +
+INSERT INTO users (userid, password) VALUES (`user4', `ch@ngem3c'); +
+DELETE name FROM users WHERE userid = `user1'; +
+APPLY BATCH;
+
+The `BATCH` statement group multiple modification statements
+(insertions/updates and deletions) into a single statement. It serves
+several purposes:
+
+. It saves network round-trips between the client and the server (and
+sometimes between the server coordinator and the replicas) when batching
+multiple updates.
+. All updates in a `BATCH` belonging to a given partition key are
+performed in isolation.
+. By default, all operations in the batch are performed as `LOGGED`, to
+ensure all mutations eventually complete (or none will). See the notes
+on link:#unloggedBatch[`UNLOGGED`] for more details.
+
+Note that:
+
+* `BATCH` statements may only contain `UPDATE`, `INSERT` and `DELETE`
+statements.
+* Batches are _not_ a full analogue for SQL transactions.
+* If a timestamp is not specified for each operation, then all
+operations will be applied with the same timestamp. Due to Cassandra’s
+conflict resolution procedure in the case of
+http://wiki.apache.org/cassandra/FAQ#clocktie[timestamp ties],
+operations may be applied in an order that is different from the order
+they are listed in the `BATCH` statement. To force a particular
+operation ordering, you must specify per-operation timestamps.
+
+[[unloggedBatch]]
+===== `UNLOGGED`
+
+By default, Cassandra uses a batch log to ensure all operations in a
+batch eventually complete or none will (note however that operations are
+only isolated within a single partition).
+
+There is a performance penalty for batch atomicity when a batch spans
+multiple partitions. If you do not want to incur this penalty, you can
+tell Cassandra to skip the batchlog with the `UNLOGGED` option. If the
+`UNLOGGED` option is used, a failed batch might leave the patch only
+partly applied.
+
+[[counterBatch]]
+===== `COUNTER`
+
+Use the `COUNTER` option for batched counter updates. Unlike other
+updates in Cassandra, counter updates are not idempotent.
+
+[[batchOptions]]
+===== `<option>`
+
+`BATCH` supports both the `TIMESTAMP` option, with similar semantic to
+the one described in the link:#updateOptions[`UPDATE`] statement (the
+timestamp applies to all the statement inside the batch). However, if
+used, `TIMESTAMP` *must not* be used in the statements within the batch.
+
+=== Queries
+
+[[selectStmt]]
+==== SELECT
+
+_Syntax:_
+
+bc(syntax).. +
+::= SELECT ( JSON )?  +
+FROM  +
+( WHERE )? +
+( GROUP BY )? +
+( ORDER BY )? +
+( PER PARTITION LIMIT )? +
+( LIMIT )? +
+( ALLOW FILTERING )?
+
+::= DISTINCT?
+
+::= (AS )? ( `,' (AS )? )* +
+| `*'
+
+::=  +
+|  +
+| WRITETIME `(' `)' +
+| COUNT `(' `*' `)' +
+| TTL `(' `)' +
+| CAST `(' AS `)' +
+| `(' ( (`,' )*)? `)' +
+| `.'  +
+| `[' `]' +
+| `[' ? .. ? `]'
+
+::= ( AND )*
+
+::=  +
+| `(' (`,' )* `)'  +
+| IN `(' ( ( `,' )* )? `)' +
+| `(' (`,' )* `)' IN `(' ( ( `,' )* )? `)' +
+| TOKEN `(' ( `,' )* `)'
+
+::= `=' | `<' | `>' | `<=' | `>=' | CONTAINS | CONTAINS KEY +
+::= (`,' )* +
+::= ( `,' )* +
+::= ( ASC | DESC )? +
+::= `(' (`,' )* `)' +
+p. +
+_Sample:_
+
+bc(sample).. +
+SELECT name, occupation FROM users WHERE userid IN (199, 200, 207);
+
+SELECT JSON name, occupation FROM users WHERE userid = 199;
+
+SELECT name AS user_name, occupation AS user_occupation FROM users;
+
+SELECT time, value +
+FROM events +
+WHERE event_type = `myEvent' +
+AND time > `2011-02-03' +
+AND time <= `2012-01-01'
+
+SELECT COUNT (*) FROM users;
+
+SELECT COUNT (*) AS user_count FROM users;
+
+The `SELECT` statements reads one or more columns for one or more rows
+in a table. It returns a result-set of rows, where each row contains the
+collection of columns corresponding to the query. If the `JSON` keyword
+is used, the results for each row will contain only a single column
+named ``json''. See the section on link:#selectJson[`SELECT JSON`] for
+more details.
+
+[[selectSelection]]
+===== `<select-clause>`
+
+The `<select-clause>` determines which columns needs to be queried and
+returned in the result-set. It consists of either the comma-separated
+list of or the wildcard character (`*`) to select all the columns
+defined for the table. Please note that for wildcard `SELECT` queries
+the order of columns returned is not specified and is not guaranteed to
+be stable between Cassandra versions.
+
+A `<selector>` is either a column name to retrieve or a `<function>` of
+one or more `<term>`s. The function allowed are the same as for `<term>`
+and are described in the link:#functions[function section]. In addition
+to these generic functions, the `WRITETIME` (resp. `TTL`) function
+allows to select the timestamp of when the column was inserted (resp.
+the time to live (in seconds) for the column (or null if the column has
+no expiration set)) and the link:#castFun[`CAST`] function can be used
+to convert one data type to another. The `WRITETIME` and `TTL` functions
+can't be used on multi-cell columns such as non-frozen collections or
+non-frozen user-defined types.
+
+Additionally, individual values of maps and sets can be selected using
+`[ <term> ]`. For maps, this will return the value corresponding to the
+key, if such entry exists. For sets, this will return the key that is
+selected if it exists and is thus mainly a way to check element
+existence. It is also possible to select a slice of a set or map with
+`[ <term> ... <term> `], where both bound can be omitted.
+
+Any `<selector>` can be aliased using `AS` keyword (see examples).
+Please note that `<where-clause>` and `<order-by>` clause should refer
+to the columns by their original names and not by their aliases.
+
+The `COUNT` keyword can be used with parenthesis enclosing `*`. If so,
+the query will return a single result: the number of rows matching the
+query. Note that `COUNT(1)` is supported as an alias.
+
+[[selectWhere]]
+===== `<where-clause>`
+
+The `<where-clause>` specifies which rows must be queried. It is
+composed of relations on the columns that are part of the `PRIMARY KEY`
+and/or have a link:#createIndexStmt[secondary index] defined on them.
+
+Not all relations are allowed in a query. For instance, non-equal
+relations (where `IN` is considered as an equal relation) on a partition
+key are not supported (but see the use of the `TOKEN` method below to do
+non-equal queries on the partition key). Moreover, for a given partition
+key, the clustering columns induce an ordering of rows and relations on
+them is restricted to the relations that allow to select a *contiguous*
+(for the ordering) set of rows. For instance, given
+
+bc(sample). +
+CREATE TABLE posts ( +
+userid text, +
+blog_title text, +
+posted_at timestamp, +
+entry_title text, +
+content text, +
+category int, +
+PRIMARY KEY (userid, blog_title, posted_at) +
+)
+
+The following query is allowed:
+
+bc(sample). +
+SELECT entry_title, content FROM posts WHERE userid=`john doe' AND
+blog_title=`John'`s Blog' AND posted_at >= `2012-01-01' AND posted_at <
+`2012-01-31'
+
+But the following one is not, as it does not select a contiguous set of
+rows (and we suppose no secondary indexes are set):
+
+bc(sample). +
+// Needs a blog_title to be set to select ranges of posted_at +
+SELECT entry_title, content FROM posts WHERE userid=`john doe' AND
+posted_at >= `2012-01-01' AND posted_at < `2012-01-31'
+
+When specifying relations, the `TOKEN` function can be used on the
+`PARTITION KEY` column to query. In that case, rows will be selected
+based on the token of their `PARTITION_KEY` rather than on the value.
+Note that the token of a key depends on the partitioner in use, and that
+in particular the RandomPartitioner won’t yield a meaningful order. Also
+note that ordering partitioners always order token values by bytes (so
+even if the partition key is of type int, `token(-1) > token(0)` in
+particular). Example:
+
+bc(sample). +
+SELECT * FROM posts WHERE token(userid) > token(`tom') AND token(userid)
+< token(`bob')
+
+Moreover, the `IN` relation is only allowed on the last column of the
+partition key and on the last column of the full primary key.
+
+It is also possible to ``group'' `CLUSTERING COLUMNS` together in a
+relation using the tuple notation. For instance:
+
+bc(sample). +
+SELECT * FROM posts WHERE userid=`john doe' AND (blog_title, posted_at)
+> (`John'`s Blog', `2012-01-01')
+
+will request all rows that sorts after the one having ``John’s Blog'' as
+`blog_tile` and `2012-01-01' for `posted_at` in the clustering order. In
+particular, rows having a `post_at <= '2012-01-01'` will be returned as
+long as their `blog_title > 'John''s Blog'`, which wouldn’t be the case
+for:
+
+bc(sample). +
+SELECT * FROM posts WHERE userid=`john doe' AND blog_title > `John'`s
+Blog' AND posted_at > `2012-01-01'
+
+The tuple notation may also be used for `IN` clauses on
+`CLUSTERING COLUMNS`:
+
+bc(sample). +
+SELECT * FROM posts WHERE userid=`john doe' AND (blog_title, posted_at)
+IN ((`John'`s Blog', `2012-01-01), (’Extreme Chess', `2014-06-01'))
+
+The `CONTAINS` operator may only be used on collection columns (lists,
+sets, and maps). In the case of maps, `CONTAINS` applies to the map
+values. The `CONTAINS KEY` operator may only be used on map columns and
+applies to the map keys.
+
+[[selectOrderBy]]
+===== `<order-by>`
+
+The `ORDER BY` option allows to select the order of the returned
+results. It takes as argument a list of column names along with the
+order for the column (`ASC` for ascendant and `DESC` for descendant,
+omitting the order being equivalent to `ASC`). Currently the possible
+orderings are limited (which depends on the table
+link:#createTableOptions[`CLUSTERING ORDER`] ):
+
+* if the table has been defined without any specific `CLUSTERING ORDER`,
+then then allowed orderings are the order induced by the clustering
+columns and the reverse of that one.
+* otherwise, the orderings allowed are the order of the
+`CLUSTERING ORDER` option and the reversed one.
+
+[[selectGroupBy]]
+===== `<group-by>`
+
+The `GROUP BY` option allows to condense into a single row all selected
+rows that share the same values for a set of columns.
+
+Using the `GROUP BY` option, it is only possible to group rows at the
+partition key level or at a clustering column level. By consequence, the
+`GROUP BY` option only accept as arguments primary key column names in
+the primary key order. If a primary key column is restricted by an
+equality restriction it is not required to be present in the `GROUP BY`
+clause.
+
+Aggregate functions will produce a separate value for each group. If no
+`GROUP BY` clause is specified, aggregates functions will produce a
+single value for all the rows.
+
+If a column is selected without an aggregate function, in a statement
+with a `GROUP BY`, the first value encounter in each group will be
+returned.
+
+[[selectLimit]]
+===== `LIMIT` and `PER PARTITION LIMIT`
+
+The `LIMIT` option to a `SELECT` statement limits the number of rows
+returned by a query, while the `PER PARTITION LIMIT` option limits the
+number of rows returned for a given partition by the query. Note that
+both type of limit can used in the same statement.
+
+[[selectAllowFiltering]]
+===== `ALLOW FILTERING`
+
+By default, CQL only allows select queries that don’t involve
+``filtering'' server side, i.e. queries where we know that all (live)
+record read will be returned (maybe partly) in the result set. The
+reasoning is that those ``non filtering'' queries have predictable
+performance in the sense that they will execute in a time that is
+proportional to the amount of data *returned* by the query (which can be
+controlled through `LIMIT`).
+
+The `ALLOW FILTERING` option allows to explicitly allow (some) queries
+that require filtering. Please note that a query using `ALLOW FILTERING`
+may thus have unpredictable performance (for the definition above), i.e.
+even a query that selects a handful of records *may* exhibit performance
+that depends on the total amount of data stored in the cluster.
+
+For instance, considering the following table holding user profiles with
+their year of birth (with a secondary index on it) and country of
+residence:
+
+bc(sample).. +
+CREATE TABLE users ( +
+username text PRIMARY KEY, +
+firstname text, +
+lastname text, +
+birth_year int, +
+country text +
+)
+
+CREATE INDEX ON users(birth_year); +
+p.
+
+Then the following queries are valid:
+
+bc(sample). +
+SELECT * FROM users; +
+SELECT firstname, lastname FROM users WHERE birth_year = 1981;
+
+because in both case, Cassandra guarantees that these queries
+performance will be proportional to the amount of data returned. In
+particular, if no users are born in 1981, then the second query
+performance will not depend of the number of user profile stored in the
+database (not directly at least: due to secondary index implementation
+consideration, this query may still depend on the number of node in the
+cluster, which indirectly depends on the amount of data stored.
+Nevertheless, the number of nodes will always be multiple number of
+magnitude lower than the number of user profile stored). Of course, both
+query may return very large result set in practice, but the amount of
+data returned can always be controlled by adding a `LIMIT`.
+
+However, the following query will be rejected:
+
+bc(sample). +
+SELECT firstname, lastname FROM users WHERE birth_year = 1981 AND
+country = `FR';
+
+because Cassandra cannot guarantee that it won’t have to scan large
+amount of data even if the result to those query is small. Typically, it
+will scan all the index entries for users born in 1981 even if only a
+handful are actually from France. However, if you ``know what you are
+doing'', you can force the execution of this query by using
+`ALLOW FILTERING` and so the following query is valid:
+
+bc(sample). +
+SELECT firstname, lastname FROM users WHERE birth_year = 1981 AND
+country = `FR' ALLOW FILTERING;
+
+[[databaseRoles]]
+=== Database Roles
+
+[[createRoleStmt]]
+==== CREATE ROLE
+
+_Syntax:_
+
+bc(syntax).. +
+::= CREATE ROLE ( IF NOT EXISTS )? ( WITH ( AND )* )?
+
+::= PASSWORD =  +
+| LOGIN =  +
+| SUPERUSER =  +
+| OPTIONS =  +
+p.
+
+_Sample:_
+
+bc(sample). +
+CREATE ROLE new_role; +
+CREATE ROLE alice WITH PASSWORD = `password_a' AND LOGIN = true; +
+CREATE ROLE bob WITH PASSWORD = `password_b' AND LOGIN = true AND
+SUPERUSER = true; +
+CREATE ROLE carlos WITH OPTIONS = \{ `custom_option1' : `option1_value',
+`custom_option2' : 99 };
+
+By default roles do not possess `LOGIN` privileges or `SUPERUSER`
+status.
+
+link:#permissions[Permissions] on database resources are granted to
+roles; types of resources include keyspaces, tables, functions and roles
+themselves. Roles may be granted to other roles to create hierarchical
+permissions structures; in these hierarchies, permissions and
+`SUPERUSER` status are inherited, but the `LOGIN` privilege is not.
+
+If a role has the `LOGIN` privilege, clients may identify as that role
+when connecting. For the duration of that connection, the client will
+acquire any roles and privileges granted to that role.
+
+Only a client with with the `CREATE` permission on the database roles
+resource may issue `CREATE ROLE` requests (see the
+link:#permissions[relevant section] below), unless the client is a
+`SUPERUSER`. Role management in Cassandra is pluggable and custom
+implementations may support only a subset of the listed options.
+
+Role names should be quoted if they contain non-alphanumeric characters.
+
+[[createRolePwd]]
+===== Setting credentials for internal authentication
+
+Use the `WITH PASSWORD` clause to set a password for internal
+authentication, enclosing the password in single quotation marks. +
+If internal authentication has not been set up or the role does not have
+`LOGIN` privileges, the `WITH PASSWORD` clause is not necessary.
+
+[[createRoleConditional]]
+===== Creating a role conditionally
+
+Attempting to create an existing role results in an invalid query
+condition unless the `IF NOT EXISTS` option is used. If the option is
+used and the role exists, the statement is a no-op.
+
+bc(sample). +
+CREATE ROLE other_role; +
+CREATE ROLE IF NOT EXISTS other_role;
+
+[[alterRoleStmt]]
+==== ALTER ROLE
+
+_Syntax:_
+
+bc(syntax).. +
+::= ALTER ROLE ( WITH ( AND )* )?
+
+::= PASSWORD =  +
+| LOGIN =  +
+| SUPERUSER =  +
+| OPTIONS =  +
+p.
+
+_Sample:_
+
+bc(sample). +
+ALTER ROLE bob WITH PASSWORD = `PASSWORD_B' AND SUPERUSER = false;
+
+Conditions on executing `ALTER ROLE` statements:
+
+* A client must have `SUPERUSER` status to alter the `SUPERUSER` status
+of another role
+* A client cannot alter the `SUPERUSER` status of any role it currently
+holds
+* A client can only modify certain properties of the role with which it
+identified at login (e.g. `PASSWORD`)
+* To modify properties of a role, the client must be granted `ALTER`
+link:#permissions[permission] on that role
+
+[[dropRoleStmt]]
+==== DROP ROLE
+
+_Syntax:_
+
+bc(syntax).. +
+::= DROP ROLE ( IF EXISTS )?  +
+p.
+
+_Sample:_
+
+bc(sample). +
+DROP ROLE alice; +
+DROP ROLE IF EXISTS bob;
+
+`DROP ROLE` requires the client to have `DROP`
+link:#permissions[permission] on the role in question. In addition,
+client may not `DROP` the role with which it identified at login.
+Finaly, only a client with `SUPERUSER` status may `DROP` another
+`SUPERUSER` role. +
+Attempting to drop a role which does not exist results in an invalid
+query condition unless the `IF EXISTS` option is used. If the option is
+used and the role does not exist the statement is a no-op.
+
+[[grantRoleStmt]]
+==== GRANT ROLE
+
+_Syntax:_
+
+bc(syntax). +
+::= GRANT TO
+
+_Sample:_
+
+bc(sample). +
+GRANT report_writer TO alice;
+
+This statement grants the `report_writer` role to `alice`. Any
+permissions granted to `report_writer` are also acquired by `alice`. +
+Roles are modelled as a directed acyclic graph, so circular grants are
+not permitted. The following examples result in error conditions:
+
+bc(sample). +
+GRANT role_a TO role_b; +
+GRANT role_b TO role_a;
+
+bc(sample). +
+GRANT role_a TO role_b; +
+GRANT role_b TO role_c; +
+GRANT role_c TO role_a;
+
+[[revokeRoleStmt]]
+==== REVOKE ROLE
+
+_Syntax:_
+
+bc(syntax). +
+::= REVOKE FROM
+
+_Sample:_
+
+bc(sample). +
+REVOKE report_writer FROM alice;
+
+This statement revokes the `report_writer` role from `alice`. Any
+permissions that `alice` has acquired via the `report_writer` role are
+also revoked.
+
+[[listRolesStmt]]
+===== LIST ROLES
+
+_Syntax:_
+
+bc(syntax). +
+::= LIST ROLES ( OF )? ( NORECURSIVE )?
+
+_Sample:_
+
+bc(sample). +
+LIST ROLES;
+
+Return all known roles in the system, this requires `DESCRIBE`
+permission on the database roles resource.
+
+bc(sample). +
+LIST ROLES OF `alice`;
+
+Enumerate all roles granted to `alice`, including those transitively
+aquired.
+
+bc(sample). +
+LIST ROLES OF `bob` NORECURSIVE
+
+List all roles directly granted to `bob`.
+
+[[createUserStmt]]
+==== CREATE USER
+
+Prior to the introduction of roles in Cassandra 2.2, authentication and
+authorization were based around the concept of a `USER`. For backward
+compatibility, the legacy syntax has been preserved with `USER` centric
+statments becoming synonyms for the `ROLE` based equivalents.
+
+_Syntax:_
+
+bc(syntax).. +
+::= CREATE USER ( IF NOT EXISTS )? ( WITH PASSWORD )? ()?
+
+::= SUPERUSER +
+| NOSUPERUSER +
+p.
+
+_Sample:_
+
+bc(sample). +
+CREATE USER alice WITH PASSWORD `password_a' SUPERUSER; +
+CREATE USER bob WITH PASSWORD `password_b' NOSUPERUSER;
+
+`CREATE USER` is equivalent to `CREATE ROLE` where the `LOGIN` option is
+`true`. So, the following pairs of statements are equivalent:
+
+bc(sample).. +
+CREATE USER alice WITH PASSWORD `password_a' SUPERUSER; +
+CREATE ROLE alice WITH PASSWORD = `password_a' AND LOGIN = true AND
+SUPERUSER = true;
+
+CREATE USER IF NOT EXISTS alice WITH PASSWORD `password_a' SUPERUSER; +
+CREATE ROLE IF NOT EXISTS alice WITH PASSWORD = `password_a' AND LOGIN =
+true AND SUPERUSER = true;
+
+CREATE USER alice WITH PASSWORD `password_a' NOSUPERUSER; +
+CREATE ROLE alice WITH PASSWORD = `password_a' AND LOGIN = true AND
+SUPERUSER = false;
+
+CREATE USER alice WITH PASSWORD `password_a' NOSUPERUSER; +
+CREATE ROLE alice WITH PASSWORD = `password_a' AND LOGIN = true;
+
+CREATE USER alice WITH PASSWORD `password_a'; +
+CREATE ROLE alice WITH PASSWORD = `password_a' AND LOGIN = true; +
+p.
+
+[[alterUserStmt]]
+==== ALTER USER
+
+_Syntax:_
+
+bc(syntax).. +
+::= ALTER USER ( WITH PASSWORD )? ( )?
+
+::= SUPERUSER +
+| NOSUPERUSER +
+p.
+
+bc(sample). +
+ALTER USER alice WITH PASSWORD `PASSWORD_A'; +
+ALTER USER bob SUPERUSER;
+
+[[dropUserStmt]]
+==== DROP USER
+
+_Syntax:_
+
+bc(syntax).. +
+::= DROP USER ( IF EXISTS )?  +
+p.
+
+_Sample:_
+
+bc(sample). +
+DROP USER alice; +
+DROP USER IF EXISTS bob;
+
+[[listUsersStmt]]
+==== LIST USERS
+
+_Syntax:_
+
+bc(syntax). +
+::= LIST USERS;
+
+_Sample:_
+
+bc(sample). +
+LIST USERS;
+
+This statement is equivalent to
+
+bc(sample). +
+LIST ROLES;
+
+but only roles with the `LOGIN` privilege are included in the output.
+
+[[dataControl]]
+=== Data Control
+
+==== Permissions
+
+Permissions on resources are granted to roles; there are several
+different types of resources in Cassandra and each type is modelled
+hierarchically:
+
+* The hierarchy of Data resources, Keyspaces and Tables has the
+structure `ALL KEYSPACES` -> `KEYSPACE` -> `TABLE`
+* Function resources have the structure `ALL FUNCTIONS` -> `KEYSPACE` ->
+`FUNCTION`
+* Resources representing roles have the structure `ALL ROLES` -> `ROLE`
+* Resources representing JMX ObjectNames, which map to sets of
+MBeans/MXBeans, have the structure `ALL MBEANS` -> `MBEAN`
+
+Permissions can be granted at any level of these hierarchies and they
+flow downwards. So granting a permission on a resource higher up the
+chain automatically grants that same permission on all resources lower
+down. For example, granting `SELECT` on a `KEYSPACE` automatically
+grants it on all `TABLES` in that `KEYSPACE`. Likewise, granting a
+permission on `ALL FUNCTIONS` grants it on every defined function,
+regardless of which keyspace it is scoped in. It is also possible to
+grant permissions on all functions scoped to a particular keyspace.
+
+Modifications to permissions are visible to existing client sessions;
+that is, connections need not be re-established following permissions
+changes.
+
+The full set of available permissions is:
+
+* `CREATE`
+* `ALTER`
+* `DROP`
+* `SELECT`
+* `MODIFY`
+* `AUTHORIZE`
+* `DESCRIBE`
+* `EXECUTE`
+
+Not all permissions are applicable to every type of resource. For
+instance, `EXECUTE` is only relevant in the context of functions or
+mbeans; granting `EXECUTE` on a resource representing a table is
+nonsensical. Attempting to `GRANT` a permission on resource to which it
+cannot be applied results in an error response. The following
+illustrates which permissions can be granted on which types of resource,
+and which statements are enabled by that permission.
+
+[cols=",,,,,",options="header",]
+|===
+|permission |resource |operations | | |
+|`CREATE` |`ALL KEYSPACES` |`CREATE KEYSPACE` <br> `CREATE TABLE` in any
+keyspace | | |
+
+|`CREATE` |`KEYSPACE` |`CREATE TABLE` in specified keyspace | | |
+
+|`CREATE` |`ALL FUNCTIONS` |`CREATE FUNCTION` in any keyspace <br>
+`CREATE AGGREGATE` in any keyspace | | |
+
+|`CREATE` |`ALL FUNCTIONS IN KEYSPACE` |`CREATE FUNCTION` in keyspace
+<br> `CREATE AGGREGATE` in keyspace | | |
+
+|`CREATE` |`ALL ROLES` |`CREATE ROLE` | | |
+
+|`ALTER` |`ALL KEYSPACES` |`ALTER KEYSPACE` <br> `ALTER TABLE` in any
+keyspace | | |
+
+|`ALTER` |`KEYSPACE` |`ALTER KEYSPACE` <br> `ALTER TABLE` in keyspace |
+| |
+
+|`ALTER` |`TABLE` |`ALTER TABLE` | | |
+
+|`ALTER` |`ALL FUNCTIONS` |`CREATE FUNCTION` replacing any existing <br>
+`CREATE AGGREGATE` replacing any existing | | |
+
+|`ALTER` |`ALL FUNCTIONS IN KEYSPACE` |`CREATE FUNCTION` replacing
+existing in keyspace <br> `CREATE AGGREGATE` replacing any existing in
+keyspace | | |
+
+|`ALTER` |`FUNCTION` |`CREATE FUNCTION` replacing existing <br>
+`CREATE AGGREGATE` replacing existing | | |
+
+|`ALTER` |`ALL ROLES` |`ALTER ROLE` on any role | | |
+
+|`ALTER` |`ROLE` |`ALTER ROLE` | | |
+
+|`DROP` |`ALL KEYSPACES` |`DROP KEYSPACE` <br> `DROP TABLE` in any
+keyspace | | |
+
+|`DROP` |`KEYSPACE` |`DROP TABLE` in specified keyspace | | |
+
+|`DROP` |`TABLE` |`DROP TABLE` | | |
+
+|`DROP` |`ALL FUNCTIONS` |`DROP FUNCTION` in any keyspace <br>
+`DROP AGGREGATE` in any existing | | |
+
+|`DROP` |`ALL FUNCTIONS IN KEYSPACE` |`DROP FUNCTION` in keyspace <br>
+`DROP AGGREGATE` in existing | | |
+
+|`DROP` |`FUNCTION` |`DROP FUNCTION` | | |
+
+|`DROP` |`ALL ROLES` |`DROP ROLE` on any role | | |
+
+|`DROP` |`ROLE` |`DROP ROLE` | | |
+
+|`SELECT` |`ALL KEYSPACES` |`SELECT` on any table | | |
+
+|`SELECT` |`KEYSPACE` |`SELECT` on any table in keyspace | | |
+
+|`SELECT` |`TABLE` |`SELECT` on specified table | | |
+
+|`SELECT` |`ALL MBEANS` |Call getter methods on any mbean | | |
+
+|`SELECT` |`MBEANS` |Call getter methods on any mbean matching a
+wildcard pattern | | |
+
+|`SELECT` |`MBEAN` |Call getter methods on named mbean | | |
+
+|`MODIFY` |`ALL KEYSPACES` |`INSERT` on any table <br> `UPDATE` on any
+table <br> `DELETE` on any table <br> `TRUNCATE` on any table | | |
+
+|`MODIFY` |`KEYSPACE` |`INSERT` on any table in keyspace <br> `UPDATE`
+on any table in keyspace <br>   `DELETE` on any table in keyspace <br>
+`TRUNCATE` on any table in keyspace |`MODIFY` |`TABLE` |`INSERT` <br>
+`UPDATE` <br> `DELETE` <br> `TRUNCATE`
+
+|`MODIFY` |`ALL MBEANS` |Call setter methods on any mbean | | |
+
+|`MODIFY` |`MBEANS` |Call setter methods on any mbean matching a
+wildcard pattern | | |
+
+|`MODIFY` |`MBEAN` |Call setter methods on named mbean | | |
+
+|`AUTHORIZE` |`ALL KEYSPACES` |`GRANT PERMISSION` on any table <br>
+`REVOKE PERMISSION` on any table | | |
+
+|`AUTHORIZE` |`KEYSPACE` |`GRANT PERMISSION` on table in keyspace <br>
+`REVOKE PERMISSION` on table in keyspace | | |
+
+|`AUTHORIZE` |`TABLE` |`GRANT PERMISSION` <br> `REVOKE PERMISSION` | | |
+
+|`AUTHORIZE` |`ALL FUNCTIONS` |`GRANT PERMISSION` on any function <br>
+`REVOKE PERMISSION` on any function | | |
+
+|`AUTHORIZE` |`ALL FUNCTIONS IN KEYSPACE` |`GRANT PERMISSION` in
+keyspace <br> `REVOKE PERMISSION` in keyspace | | |
+
+|`AUTHORIZE` |`ALL FUNCTIONS IN KEYSPACE` |`GRANT PERMISSION` in
+keyspace <br> `REVOKE PERMISSION` in keyspace | | |
+
+|`AUTHORIZE` |`FUNCTION` |`GRANT PERMISSION` <br> `REVOKE PERMISSION` |
+| |
+
+|`AUTHORIZE` |`ALL MBEANS` |`GRANT PERMISSION` on any mbean <br>
+`REVOKE PERMISSION` on any mbean | | |
+
+|`AUTHORIZE` |`MBEANS` |`GRANT PERMISSION` on any mbean matching a
+wildcard pattern <br> `REVOKE PERMISSION` on any mbean matching a
+wildcard pattern | | |
+
+|`AUTHORIZE` |`MBEAN` |`GRANT PERMISSION` on named mbean <br>
+`REVOKE PERMISSION` on named mbean | | |
+
+|`AUTHORIZE` |`ALL ROLES` |`GRANT ROLE` grant any role <br>
+`REVOKE ROLE` revoke any role | | |
+
+|`AUTHORIZE` |`ROLES` |`GRANT ROLE` grant role <br> `REVOKE ROLE` revoke
+role | | |
+
+|`DESCRIBE` |`ALL ROLES` |`LIST ROLES` all roles or only roles granted
+to another, specified role | | |
+
+|`DESCRIBE` |@ALL MBEANS |Retrieve metadata about any mbean from the
+platform’s MBeanServer | | |
+
+|`DESCRIBE` |@MBEANS |Retrieve metadata about any mbean matching a
+wildcard patter from the platform’s MBeanServer | | |
+
+|`DESCRIBE` |@MBEAN |Retrieve metadata about a named mbean from the
+platform’s MBeanServer | | |
+
+|`EXECUTE` |`ALL FUNCTIONS` |`SELECT`, `INSERT`, `UPDATE` using any
+function <br> use of any function in `CREATE AGGREGATE` | | |
+
+|`EXECUTE` |`ALL FUNCTIONS IN KEYSPACE` |`SELECT`, `INSERT`, `UPDATE`
+using any function in keyspace <br> use of any function in keyspace in
+`CREATE AGGREGATE` | | |
+
+|`EXECUTE` |`FUNCTION` |`SELECT`, `INSERT`, `UPDATE` using function <br>
+use of function in `CREATE AGGREGATE` | | |
+
+|`EXECUTE` |`ALL MBEANS` |Execute operations on any mbean | | |
+
+|`EXECUTE` |`MBEANS` |Execute operations on any mbean matching a
+wildcard pattern | | |
+
+|`EXECUTE` |`MBEAN` |Execute operations on named mbean | | |
+|===
+
+[[grantPermissionsStmt]]
+==== GRANT PERMISSION
+
+_Syntax:_
+
+bc(syntax).. +
+::= GRANT ( ALL ( PERMISSIONS )? | ( PERMISSION )? ) ON TO
+
+::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESRIBE |
+EXECUTE
+
+::= ALL KEYSPACES +
+| KEYSPACE  +
+| ( TABLE )?  +
+| ALL ROLES +
+| ROLE  +
+| ALL FUNCTIONS ( IN KEYSPACE )? +
+| FUNCTION  +
+| ALL MBEANS +
+| ( MBEAN | MBEANS )  +
+p.
+
+_Sample:_
+
+bc(sample). +
+GRANT SELECT ON ALL KEYSPACES TO data_reader;
+
+This gives any user with the role `data_reader` permission to execute
+`SELECT` statements on any table across all keyspaces
+
+bc(sample). +
+GRANT MODIFY ON KEYSPACE keyspace1 TO data_writer;
+
+This give any user with the role `data_writer` permission to perform
+`UPDATE`, `INSERT`, `UPDATE`, `DELETE` and `TRUNCATE` queries on all
+tables in the `keyspace1` keyspace
+
+bc(sample). +
+GRANT DROP ON keyspace1.table1 TO schema_owner;
+
+This gives any user with the `schema_owner` role permissions to `DROP`
+`keyspace1.table1`.
+
+bc(sample). +
+GRANT EXECUTE ON FUNCTION keyspace1.user_function( int ) TO
+report_writer;
+
+This grants any user with the `report_writer` role permission to execute
+`SELECT`, `INSERT` and `UPDATE` queries which use the function
+`keyspace1.user_function( int )`
+
+bc(sample). +
+GRANT DESCRIBE ON ALL ROLES TO role_admin;
+
+This grants any user with the `role_admin` role permission to view any
+and all roles in the system with a `LIST ROLES` statement
+
+[[grantAll]]
+===== GRANT ALL
+
+When the `GRANT ALL` form is used, the appropriate set of permissions is
+determined automatically based on the target resource.
+
+[[autoGrantPermissions]]
+===== Automatic Granting
+
+When a resource is created, via a `CREATE KEYSPACE`, `CREATE TABLE`,
+`CREATE FUNCTION`, `CREATE AGGREGATE` or `CREATE ROLE` statement, the
+creator (the role the database user who issues the statement is
+identified as), is automatically granted all applicable permissions on
+the new resource.
+
+[[revokePermissionsStmt]]
+==== REVOKE PERMISSION
+
+_Syntax:_
+
+bc(syntax).. +
+::= REVOKE ( ALL ( PERMISSIONS )? | ( PERMISSION )? ) ON FROM
+
+::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESRIBE |
+EXECUTE
+
+::= ALL KEYSPACES +
+| KEYSPACE  +
+| ( TABLE )?  +
+| ALL ROLES +
+| ROLE  +
+| ALL FUNCTIONS ( IN KEYSPACE )? +
+| FUNCTION  +
+| ALL MBEANS +
+| ( MBEAN | MBEANS )  +
+p.
+
+_Sample:_
+
+bc(sample).. +
+REVOKE SELECT ON ALL KEYSPACES FROM data_reader; +
+REVOKE MODIFY ON KEYSPACE keyspace1 FROM data_writer; +
+REVOKE DROP ON keyspace1.table1 FROM schema_owner; +
+REVOKE EXECUTE ON FUNCTION keyspace1.user_function( int ) FROM
+report_writer; +
+REVOKE DESCRIBE ON ALL ROLES FROM role_admin; +
+p.
+
+[[listPermissionsStmt]]
+===== LIST PERMISSIONS
+
+_Syntax:_
+
+bc(syntax).. +
+::= LIST ( ALL ( PERMISSIONS )? | ) +
+( ON )? +
+( OF ( NORECURSIVE )? )?
+
+::= ALL KEYSPACES +
+| KEYSPACE  +
+| ( TABLE )?  +
+| ALL ROLES +
+| ROLE  +
+| ALL FUNCTIONS ( IN KEYSPACE )? +
+| FUNCTION  +
+| ALL MBEANS +
+| ( MBEAN | MBEANS )  +
+p.
+
+_Sample:_
+
+bc(sample). +
+LIST ALL PERMISSIONS OF alice;
+
+Show all permissions granted to `alice`, including those acquired
+transitively from any other roles.
+
+bc(sample). +
+LIST ALL PERMISSIONS ON keyspace1.table1 OF bob;
+
+Show all permissions on `keyspace1.table1` granted to `bob`, including
+those acquired transitively from any other roles. This also includes any
+permissions higher up the resource hierarchy which can be applied to
+`keyspace1.table1`. For example, should `bob` have `ALTER` permission on
+`keyspace1`, that would be included in the results of this query. Adding
+the `NORECURSIVE` switch restricts the results to only those permissions
+which were directly granted to `bob` or one of `bob`’s roles.
+
+bc(sample). +
+LIST SELECT PERMISSIONS OF carlos;
+
+Show any permissions granted to `carlos` or any of `carlos`’s roles,
+limited to `SELECT` permissions on any resource.
+
+[[types]]
+=== Data Types
+
+CQL supports a rich set of data types for columns defined in a table,
+including collection types. On top of those native +
+and collection types, users can also provide custom types (through a
+JAVA class extending `AbstractType` loadable by +
+Cassandra). The syntax of types is thus:
+
+bc(syntax).. +
+::=  +
+|  +
+|  +
+| // Used for custom types. The fully-qualified name of a JAVA class
+
+::= ascii +
+| bigint +
+| blob +
+| boolean +
+| counter +
+| date +
+| decimal +
+| double +
+| float +
+| inet +
+| int +
+| smallint +
+| text +
+| time +
+| timestamp +
+| timeuuid +
+| tinyint +
+| uuid +
+| varchar +
+| varint
+
+::= list `<' `>' +
+| set `<' `>' +
+| map `<' `,' `>' +
+::= tuple `<' (`,' )* `>' +
+p. Note that the native types are keywords and as such are
+case-insensitive. They are however not reserved ones.
+
+The following table gives additional informations on the native data
+types, and on which kind of link:#constants[constants] each type
+supports:
+
+[cols=",,",options="header",]
+|===
+|type |constants supported |description
+|`ascii` |strings |ASCII character string
+
+|`bigint` |integers |64-bit signed long
+
+|`blob` |blobs |Arbitrary bytes (no validation)
+
+|`boolean` |booleans |true or false
+
+|`counter` |integers |Counter column (64-bit signed value). See
+link:#counters[Counters] for details
+
+|`date` |integers, strings |A date (with no corresponding time value).
+See link:#usingdates[Working with dates] below for more information.
+
+|`decimal` |integers, floats |Variable-precision decimal
+
+|`double` |integers |64-bit IEEE-754 floating point
+
+|`float` |integers, floats |32-bit IEEE-754 floating point
+
+|`inet` |strings |An IP address. It can be either 4 bytes long (IPv4) or
+16 bytes long (IPv6). There is no `inet` constant, IP address should be
+inputed as strings
+
+|`int` |integers |32-bit signed int
+
+|`smallint` |integers |16-bit signed int
+
+|`text` |strings |UTF8 encoded string
+
+|`time` |integers, strings |A time with nanosecond precision. See
+link:#usingtime[Working with time] below for more information.
+
+|`timestamp` |integers, strings |A timestamp. Strings constant are allow
+to input timestamps as dates, see link:#usingtimestamps[Working with
+timestamps] below for more information.
+
+|`timeuuid` |uuids |Type 1 UUID. This is generally used as a
+``conflict-free'' timestamp. Also see the link:#timeuuidFun[functions on
+Timeuuid]
+
+|`tinyint` |integers |8-bit signed int
+
+|`uuid` |uuids |Type 1 or type 4 UUID
+
+|`varchar` |strings |UTF8 encoded string
+
+|`varint` |integers |Arbitrary-precision integer
+|===
+
+For more information on how to use the collection types, see the
+link:#collections[Working with collections] section below.
+
+[[usingtimestamps]]
+==== Working with timestamps
+
+Values of the `timestamp` type are encoded as 64-bit signed integers
+representing a number of milliseconds since the standard base time known
+as ``the epoch'': January 1 1970 at 00:00:00 GMT.
+
+Timestamp can be input in CQL as simple long integers, giving the number
+of milliseconds since the epoch, as defined above.
+
+They can also be input as string literals in any of the following ISO
+8601 formats, each representing the time and date Mar 2, 2011, at
+04:05:00 AM, GMT.:
+
+* `2011-02-03 04:05+0000`
+* `2011-02-03 04:05:00+0000`
+* `2011-02-03 04:05:00.000+0000`
+* `2011-02-03T04:05+0000`
+* `2011-02-03T04:05:00+0000`
+* `2011-02-03T04:05:00.000+0000`
+
+The `+0000` above is an RFC 822 4-digit time zone specification; `+0000`
+refers to GMT. US Pacific Standard Time is `-0800`. The time zone may be
+omitted if desired— the date will be interpreted as being in the time
+zone under which the coordinating Cassandra node is configured.
+
+* `2011-02-03 04:05`
+* `2011-02-03 04:05:00`
+* `2011-02-03 04:05:00.000`
+* `2011-02-03T04:05`
+* `2011-02-03T04:05:00`
+* `2011-02-03T04:05:00.000`
+
+There are clear difficulties inherent in relying on the time zone
+configuration being as expected, though, so it is recommended that the
+time zone always be specified for timestamps when feasible.
+
+The time of day may also be omitted, if the date is the only piece that
+matters:
+
+* `2011-02-03`
+* `2011-02-03+0000`
+
+In that case, the time of day will default to 00:00:00, in the specified
+or default time zone.
+
+[[usingdates]]
+==== Working with dates
+
+Values of the `date` type are encoded as 32-bit unsigned integers
+representing a number of days with ``the epoch'' at the center of the
+range (2^31). Epoch is January 1st, 1970
+
+A date can be input in CQL as an unsigned integer as defined above.
+
+They can also be input as string literals in the following format:
+
+* `2014-01-01`
+
+[[usingtime]]
+==== Working with time
+
+Values of the `time` type are encoded as 64-bit signed integers
+representing the number of nanoseconds since midnight.
+
+A time can be input in CQL as simple long integers, giving the number of
+nanoseconds since midnight.
+
+They can also be input as string literals in any of the following
+formats:
+
+* `08:12:54`
+* `08:12:54.123`
+* `08:12:54.123456`
+* `08:12:54.123456789`
+
+==== Counters
+
+The `counter` type is used to define _counter columns_. A counter column
+is a column whose value is a 64-bit signed integer and on which 2
+operations are supported: incrementation and decrementation (see
+link:#updateStmt[`UPDATE`] for syntax). Note the value of a counter
+cannot be set. A counter doesn’t exist until first
+incremented/decremented, and the first incrementation/decrementation is
+made as if the previous value was 0. Deletion of counter columns is
+supported but have some limitations (see the
+http://wiki.apache.org/cassandra/Counters[Cassandra Wiki] for more
+information).
+
+The use of the counter type is limited in the following way:
+
+* It cannot be used for column that is part of the `PRIMARY KEY` of a
+table.
+* A table that contains a counter can only contain counters. In other
+words, either all the columns of a table outside the `PRIMARY KEY` have
+the counter type, or none of them have it.
+
+[[collections]]
+==== Working with collections
+
+===== Noteworthy characteristics
+
+Collections are meant for storing/denormalizing relatively small amount
+of data. They work well for things like ``the phone numbers of a given
+user'', ``labels applied to an email'', etc. But when items are expected
+to grow unbounded (``all the messages sent by a given user'', ``events
+registered by a sensor'', …), then collections are not appropriate
+anymore and a specific table (with clustering columns) should be used.
+Concretely, collections have the following limitations:
+
+* Collections are always read in their entirety (and reading one is not
+paged internally).
+* Collections cannot have more than 65535 elements. More precisely,
+while it may be possible to insert more than 65535 elements, it is not
+possible to read more than the 65535 first elements (see
+https://issues.apache.org/jira/browse/CASSANDRA-5428[CASSANDRA-5428] for
+details).
+* While insertion operations on sets and maps never incur a
+read-before-write internally, some operations on lists do (see the
+section on lists below for details). It is thus advised to prefer sets
+over lists when possible.
+
+Please note that while some of those limitations may or may not be
+loosen in the future, the general rule that collections are for
+denormalizing small amount of data is meant to stay.
+
+[[map]]
+===== Maps
+
+A `map` is a link:#types[typed] set of key-value pairs, where keys are
+unique. Furthermore, note that the map are internally sorted by their
+keys and will thus always be returned in that order. To create a column
+of type `map`, use the `map` keyword suffixed with comma-separated key
+and value types, enclosed in angle brackets. For example:
+
+bc(sample). +
+CREATE TABLE users ( +
+id text PRIMARY KEY, +
+given text, +
+surname text, +
+favs map<text, text> // A map of text keys, and text values +
+)
+
+Writing `map` data is accomplished with a JSON-inspired syntax. To write
+a record using `INSERT`, specify the entire map as a JSON-style
+associative array. _Note: This form will always replace the entire map._
+
+bc(sample). +
+// Inserting (or Updating) +
+INSERT INTO users (id, given, surname, favs) +
+VALUES (`jsmith', `John', `Smith', \{ `fruit' : `apple', `band' :
+`Beatles' })
+
+Adding or updating key-values of a (potentially) existing map can be
+accomplished either by subscripting the map column in an `UPDATE`
+statement or by adding a new map literal:
+
+bc(sample). +
+// Updating (or inserting) +
+UPDATE users SET favs[`author'] = `Ed Poe' WHERE id = `jsmith' +
+UPDATE users SET favs = favs + \{ `movie' : `Cassablanca' } WHERE id =
+`jsmith'
+
+Note that TTLs are allowed for both `INSERT` and `UPDATE`, but in both
+case the TTL set only apply to the newly inserted/updated _values_. In
+other words,
+
+bc(sample). +
+// Updating (or inserting) +
+UPDATE users USING TTL 10 SET favs[`color'] = `green' WHERE id =
+`jsmith'
+
+will only apply the TTL to the `{ 'color' : 'green' }` record, the rest
+of the map remaining unaffected.
+
+Deleting a map record is done with:
+
+bc(sample). +
+DELETE favs[`author'] FROM users WHERE id = `jsmith'
+
+[[set]]
+===== Sets
+
+A `set` is a link:#types[typed] collection of unique values. Sets are
+ordered by their values. To create a column of type `set`, use the `set`
+keyword suffixed with the value type enclosed in angle brackets. For
+example:
+
+bc(sample). +
+CREATE TABLE images ( +
+name text PRIMARY KEY, +
+owner text, +
+date timestamp, +
+tags set +
+);
+
+Writing a `set` is accomplished by comma separating the set values, and
+enclosing them in curly braces. _Note: An `INSERT` will always replace
+the entire set._
+
+bc(sample). +
+INSERT INTO images (name, owner, date, tags) +
+VALUES (`cat.jpg', `jsmith', `now', \{ `kitten', `cat', `pet' });
+
+Adding and removing values of a set can be accomplished with an `UPDATE`
+by adding/removing new set values to an existing `set` column.
+
+bc(sample). +
+UPDATE images SET tags = tags + \{ `cute', `cuddly' } WHERE name =
+`cat.jpg'; +
+UPDATE images SET tags = tags - \{ `lame' } WHERE name = `cat.jpg';
+
+As with link:#map[maps], TTLs if used only apply to the newly
+inserted/updated _values_.
+
+[[list]]
+===== Lists
+
+A `list` is a link:#types[typed] collection of non-unique values where
+elements are ordered by there position in the list. To create a column
+of type `list`, use the `list` keyword suffixed with the value type
+enclosed in angle brackets. For example:
+
+bc(sample). +
+CREATE TABLE plays ( +
+id text PRIMARY KEY, +
+game text, +
+players int, +
+scores list +
+)
+
+Do note that as explained below, lists have some limitations and
+performance considerations to take into account, and it is advised to
+prefer link:#set[sets] over lists when this is possible.
+
+Writing `list` data is accomplished with a JSON-style syntax. To write a
+record using `INSERT`, specify the entire list as a JSON array. _Note:
+An `INSERT` will always replace the entire list._
+
+bc(sample). +
+INSERT INTO plays (id, game, players, scores) +
+VALUES (`123-afde', `quake', 3, [17, 4, 2]);
+
+Adding (appending or prepending) values to a list can be accomplished by
+adding a new JSON-style array to an existing `list` column.
+
+bc(sample). +
+UPDATE plays SET players = 5, scores = scores + [ 14, 21 ] WHERE id =
+`123-afde'; +
+UPDATE plays SET players = 5, scores = [ 12 ] + scores WHERE id =
+`123-afde';
+
+It should be noted that append and prepend are not idempotent
+operations. This means that if during an append or a prepend the
+operation timeout, it is not always safe to retry the operation (as this
+could result in the record appended or prepended twice).
+
+Lists also provides the following operation: setting an element by its
+position in the list, removing an element by its position in the list
+and remove all the occurrence of a given value in the list. _However,
+and contrarily to all the other collection operations, these three
+operations induce an internal read before the update, and will thus
+typically have slower performance characteristics_. Those operations
+have the following syntax:
+
+bc(sample). +
+UPDATE plays SET scores[1] = 7 WHERE id = `123-afde'; // sets the 2nd
+element of scores to 7 (raises an error is scores has less than 2
+elements) +
+DELETE scores[1] FROM plays WHERE id = `123-afde'; // deletes the 2nd
+element of scores (raises an error is scores has less than 2 elements) +
+UPDATE plays SET scores = scores - [ 12, 21 ] WHERE id = `123-afde'; //
+removes all occurrences of 12 and 21 from scores
+
+As with link:#map[maps], TTLs if used only apply to the newly
+inserted/updated _values_.
+
+=== Functions
+
+CQL3 distinguishes between built-in functions (so called `native
+functions') and link:#udfs[user-defined functions]. CQL3 includes
+several native functions, described below:
+
+[[castFun]]
+==== Cast
+
+The `cast` function can be used to converts one native datatype to
+another.
+
+The following table describes the conversions supported by the `cast`
+function. Cassandra will silently ignore any cast converting a datatype
+into its own datatype.
+
+[cols=",",options="header",]
+|===
+|from |to
+|`ascii` |`text`, `varchar`
+
+|`bigint` |`tinyint`, `smallint`, `int`, `float`, `double`, `decimal`,
+`varint`, `text`, `varchar`
+
+|`boolean` |`text`, `varchar`
+
+|`counter` |`tinyint`, `smallint`, `int`, `bigint`, `float`, `double`,
+`decimal`, `varint`, `text`, `varchar`
+
+|`date` |`timestamp`
+
+|`decimal` |`tinyint`, `smallint`, `int`, `bigint`, `float`, `double`,
+`varint`, `text`, `varchar`
+
+|`double` |`tinyint`, `smallint`, `int`, `bigint`, `float`, `decimal`,
+`varint`, `text`, `varchar`
+
+|`float` |`tinyint`, `smallint`, `int`, `bigint`, `double`, `decimal`,
+`varint`, `text`, `varchar`
+
+|`inet` |`text`, `varchar`
+
+|`int` |`tinyint`, `smallint`, `bigint`, `float`, `double`, `decimal`,
+`varint`, `text`, `varchar`
+
+|`smallint` |`tinyint`, `int`, `bigint`, `float`, `double`, `decimal`,
+`varint`, `text`, `varchar`
+
+|`time` |`text`, `varchar`
+
+|`timestamp` |`date`, `text`, `varchar`
+
+|`timeuuid` |`timestamp`, `date`, `text`, `varchar`
+
+|`tinyint` |`tinyint`, `smallint`, `int`, `bigint`, `float`, `double`,
+`decimal`, `varint`, `text`, `varchar`
+
+|`uuid` |`text`, `varchar`
+
+|`varint` |`tinyint`, `smallint`, `int`, `bigint`, `float`, `double`,
+`decimal`, `text`, `varchar`
+|===
+
+The conversions rely strictly on Java’s semantics. For example, the
+double value 1 will be converted to the text value `1.0'.
+
+bc(sample). +
+SELECT avg(cast(count as double)) FROM myTable
+
+[[tokenFun]]
+==== Token
+
+The `token` function allows to compute the token for a given partition
+key. The exact signature of the token function depends on the table
+concerned and of the partitioner used by the cluster.
+
+The type of the arguments of the `token` depend on the type of the
+partition key columns. The return type depend on the partitioner in use:
+
+* For Murmur3Partitioner, the return type is `bigint`.
+* For RandomPartitioner, the return type is `varint`.
+* For ByteOrderedPartitioner, the return type is `blob`.
+
+For instance, in a cluster using the default Murmur3Partitioner, if a
+table is defined by
+
+bc(sample). +
+CREATE TABLE users ( +
+userid text PRIMARY KEY, +
+username text, +
+… +
+)
+
+then the `token` function will take a single argument of type `text` (in
+that case, the partition key is `userid` (there is no clustering columns
+so the partition key is the same than the primary key)), and the return
+type will be `bigint`.
+
+[[uuidFun]]
+==== Uuid
+
+The `uuid` function takes no parameters and generates a random type 4
+uuid suitable for use in INSERT or SET statements.
+
+[[timeuuidFun]]
+==== Timeuuid functions
+
+===== `now`
+
+The `now` function takes no arguments and generates, on the coordinator
+node, a new unique timeuuid (at the time where the statement using it is
+executed). Note that this method is useful for insertion but is largely
+non-sensical in `WHERE` clauses. For instance, a query of the form
+
+bc(sample). +
+SELECT * FROM myTable WHERE t = now()
+
+will never return any result by design, since the value returned by
+`now()` is guaranteed to be unique.
+
+===== `minTimeuuid` and `maxTimeuuid`
+
+The `minTimeuuid` (resp. `maxTimeuuid`) function takes a `timestamp`
+value `t` (which can be link:#usingtimestamps[either a timestamp or a
+date string] ) and return a _fake_ `timeuuid` corresponding to the
+_smallest_ (resp. _biggest_) possible `timeuuid` having for timestamp
+`t`. So for instance:
+
+bc(sample). +
+SELECT * FROM myTable WHERE t > maxTimeuuid(`2013-01-01 00:05+0000') AND
+t < minTimeuuid(`2013-02-02 10:00+0000')
+
+will select all rows where the `timeuuid` column `t` is strictly older
+than `2013-01-01 00:05+0000' but strictly younger than `2013-02-02
+10:00+0000'. Please note that
+`t >= maxTimeuuid('2013-01-01 00:05+0000')` would still _not_ select a
+`timeuuid` generated exactly at `2013-01-01 00:05+0000' and is
+essentially equivalent to `t > maxTimeuuid('2013-01-01 00:05+0000')`.
+
+_Warning_: We called the values generated by `minTimeuuid` and
+`maxTimeuuid` _fake_ UUID because they do no respect the Time-Based UUID
+generation process specified by the
+http://www.ietf.org/rfc/rfc4122.txt[RFC 4122]. In particular, the value
+returned by these 2 methods will not be unique. This means you should
+only use those methods for querying (as in the example above). Inserting
+the result of those methods is almost certainly _a bad idea_.
+
+[[timeFun]]
+==== Time conversion functions
+
+A number of functions are provided to ``convert'' a `timeuuid`, a
+`timestamp` or a `date` into another `native` type.
+
+[cols=",,",options="header",]
+|===
+|function name |input type |description
+|`toDate` |`timeuuid` |Converts the `timeuuid` argument into a `date`
+type
+
+|`toDate` |`timestamp` |Converts the `timestamp` argument into a `date`
+type
+
+|`toTimestamp` |`timeuuid` |Converts the `timeuuid` argument into a
+`timestamp` type
+
+|`toTimestamp` |`date` |Converts the `date` argument into a `timestamp`
+type
+
+|`toUnixTimestamp` |`timeuuid` |Converts the `timeuuid` argument into a
+`bigInt` raw value
+
+|`toUnixTimestamp` |`timestamp` |Converts the `timestamp` argument into
+a `bigInt` raw value
+
+|`toUnixTimestamp` |`date` |Converts the `date` argument into a `bigInt`
+raw value
+
+|`dateOf` |`timeuuid` |Similar to `toTimestamp(timeuuid)` (DEPRECATED)
+
+|`unixTimestampOf` |`timeuuid` |Similar to `toUnixTimestamp(timeuuid)`
+(DEPRECATED)
+|===
+
+[[blobFun]]
+==== Blob conversion functions
+
+A number of functions are provided to ``convert'' the native types into
+binary data (`blob`). For every `<native-type>` `type` supported by CQL3
+(a notable exceptions is `blob`, for obvious reasons), the function
+`typeAsBlob` takes a argument of type `type` and return it as a `blob`.
+Conversely, the function `blobAsType` takes a 64-bit `blob` argument and
+convert it to a `bigint` value. And so for instance, `bigintAsBlob(3)`
+is `0x0000000000000003` and `blobAsBigint(0x0000000000000003)` is `3`.
+
+=== Aggregates
+
+Aggregate functions work on a set of rows. They receive values for each
+row and returns one value for the whole set. +
+If `normal` columns, `scalar functions`, `UDT` fields, `writetime` or
+`ttl` are selected together with aggregate functions, the values
+returned for them will be the ones of the first row matching the query.
+
+CQL3 distinguishes between built-in aggregates (so called `native
+aggregates') and link:#udas[user-defined aggregates]. CQL3 includes
+several native aggregates, described below:
+
+[[countFct]]
+==== Count
+
+The `count` function can be used to count the rows returned by a query.
+Example:
+
+bc(sample). +
+SELECT COUNT (*) FROM plays; +
+SELECT COUNT (1) FROM plays;
+
+It also can be used to count the non null value of a given column.
+Example:
+
+bc(sample). +
+SELECT COUNT (scores) FROM plays;
+
+[[maxMinFcts]]
+==== Max and Min
+
+The `max` and `min` functions can be used to compute the maximum and the
+minimum value returned by a query for a given column.
+
+bc(sample). +
+SELECT MIN (players), MAX (players) FROM plays WHERE game = `quake';
+
+[[sumFct]]
+==== Sum
+
+The `sum` function can be used to sum up all the values returned by a
+query for a given column.
+
+bc(sample). +
+SELECT SUM (players) FROM plays;
+
+[[avgFct]]
+==== Avg
+
+The `avg` function can be used to compute the average of all the values
+returned by a query for a given column.
+
+bc(sample). +
+SELECT AVG (players) FROM plays;
+
+[[udfs]]
+=== User-Defined Functions
+
+User-defined functions allow execution of user-provided code in
+Cassandra. By default, Cassandra supports defining functions in _Java_
+and _JavaScript_. Support for other JSR 223 compliant scripting
+languages (such as Python, Ruby, and Scala) has been removed in 3.0.11.
+
+UDFs are part of the Cassandra schema. As such, they are automatically
+propagated to all nodes in the cluster.
+
+UDFs can be _overloaded_ - i.e. multiple UDFs with different argument
+types but the same function name. Example:
+
+bc(sample). +
+CREATE FUNCTION sample ( arg int ) …; +
+CREATE FUNCTION sample ( arg text ) …;
+
+User-defined functions are susceptible to all of the normal problems
+with the chosen programming language. Accordingly, implementations
+should be safe against null pointer exceptions, illegal arguments, or
+any other potential source of exceptions. An exception during function
+execution will result in the entire statement failing.
+
+It is valid to use _complex_ types like collections, tuple types and
+user-defined types as argument and return types. Tuple types and
+user-defined types are handled by the conversion functions of the
+DataStax Java Driver. Please see the documentation of the Java Driver
+for details on handling tuple types and user-defined types.
+
+Arguments for functions can be literals or terms. Prepared statement
+placeholders can be used, too.
+
+Note that you can use the double-quoted string syntax to enclose the UDF
+source code. For example:
+
+bc(sample).. +
+CREATE FUNCTION some_function ( arg int ) +
+RETURNS NULL ON NULL INPUT +
+RETURNS int +
+LANGUAGE java +
+AS $$ return arg; $$;
+
+SELECT some_function(column) FROM atable …; +
+UPDATE atable SET col = some_function(?) …; +
+p.
+
+bc(sample). +
+CREATE TYPE custom_type (txt text, i int); +
+CREATE FUNCTION fct_using_udt ( udtarg frozen ) +
+RETURNS NULL ON NULL INPUT +
+RETURNS text +
+LANGUAGE java +
+AS $$ return udtarg.getString(``txt''); $$;
+
+User-defined functions can be used in link:#selectStmt[`SELECT`],
+link:#insertStmt[`INSERT`] and link:#updateStmt[`UPDATE`] statements.
+
+The implicitly available `udfContext` field (or binding for script UDFs)
+provides the neccessary functionality to create new UDT and tuple
+values.
+
+bc(sample). +
+CREATE TYPE custom_type (txt text, i int); +
+CREATE FUNCTION fct_using_udt ( somearg int ) +
+RETURNS NULL ON NULL INPUT +
+RETURNS custom_type +
+LANGUAGE java +
+AS $$ +
+UDTValue udt = udfContext.newReturnUDTValue(); +
+udt.setString(``txt'', ``some string''); +
+udt.setInt(``i'', 42); +
+return udt; +
+$$;
+
+The definition of the `UDFContext` interface can be found in the Apache
+Cassandra source code for
+`org.apache.cassandra.cql3.functions.UDFContext`.
+
+bc(sample). +
+public interface UDFContext +
+\{ +
+UDTValue newArgUDTValue(String argName); +
+UDTValue newArgUDTValue(int argNum); +
+UDTValue newReturnUDTValue(); +
+UDTValue newUDTValue(String udtName); +
+TupleValue newArgTupleValue(String argName); +
+TupleValue newArgTupleValue(int argNum); +
+TupleValue newReturnTupleValue(); +
+TupleValue newTupleValue(String cqlDefinition); +
+}
+
+Java UDFs already have some imports for common interfaces and classes
+defined. These imports are: +
+Please note, that these convenience imports are not available for script
+UDFs.
+
+bc(sample). +
+import java.nio.ByteBuffer; +
+import java.util.List; +
+import java.util.Map; +
+import java.util.Set; +
+import org.apache.cassandra.cql3.functions.UDFContext; +
+import com.datastax.driver.core.TypeCodec; +
+import com.datastax.driver.core.TupleValue; +
+import com.datastax.driver.core.UDTValue;
+
+See link:#createFunctionStmt[`CREATE FUNCTION`] and
+link:#dropFunctionStmt[`DROP FUNCTION`].
+
+[[udas]]
+=== User-Defined Aggregates
+
+User-defined aggregates allow creation of custom aggregate functions
+using link:#udfs[UDFs]. Common examples of aggregate functions are
+_count_, _min_, and _max_.
+
+Each aggregate requires an _initial state_ (`INITCOND`, which defaults
+to `null`) of type `STYPE`. The first argument of the state function
+must have type `STYPE`. The remaining arguments of the state function
+must match the types of the user-defined aggregate arguments. The state
+function is called once for each row, and the value returned by the
+state function becomes the new state. After all rows are processed, the
+optional `FINALFUNC` is executed with last state value as its argument.
+
+`STYPE` is mandatory in order to be able to distinguish possibly
+overloaded versions of the state and/or final function (since the
+overload can appear after creation of the aggregate).
+
+User-defined aggregates can be used in link:#selectStmt[`SELECT`]
+statement.
+
+A complete working example for user-defined aggregates (assuming that a
+keyspace has been selected using the link:#useStmt[`USE`] statement):
+
+bc(sample).. +
+CREATE OR REPLACE FUNCTION averageState ( state tuple<int,bigint>, val
+int ) +
+CALLED ON NULL INPUT +
+RETURNS tuple<int,bigint> +
+LANGUAGE java +
+AS ’ +
+if (val != null) \{ +
+state.setInt(0, state.getInt(0)+1); +
+state.setLong(1, state.getLong(1)+val.intValue()); +
+} +
+return state; +
+’;
+
+CREATE OR REPLACE FUNCTION averageFinal ( state tuple<int,bigint> ) +
+CALLED ON NULL INPUT +
+RETURNS double +
+LANGUAGE java +
+AS ’ +
+double r = 0; +
+if (state.getInt(0) == 0) return null; +
+r = state.getLong(1); +
+r /= state.getInt(0); +
+return Double.valueOf®; +
+’;
+
+CREATE OR REPLACE AGGREGATE average ( int ) +
+SFUNC averageState +
+STYPE tuple<int,bigint> +
+FINALFUNC averageFinal +
+INITCOND (0, 0);
+
+CREATE TABLE atable ( +
+pk int PRIMARY KEY, +
+val int); +
+INSERT INTO atable (pk, val) VALUES (1,1); +
+INSERT INTO atable (pk, val) VALUES (2,2); +
+INSERT INTO atable (pk, val) VALUES (3,3); +
+INSERT INTO atable (pk, val) VALUES (4,4); +
+SELECT average(val) FROM atable; +
+p.
+
+See link:#createAggregateStmt[`CREATE AGGREGATE`] and
+link:#dropAggregateStmt[`DROP AGGREGATE`].
+
+[[json]]
+=== JSON Support
+
+Cassandra 2.2 introduces JSON support to link:#selectStmt[`SELECT`] and
+link:#insertStmt[`INSERT`] statements. This support does not
+fundamentally alter the CQL API (for example, the schema is still
+enforced), it simply provides a convenient way to work with JSON
+documents.
+
+[[selectJson]]
+==== SELECT JSON
+
+With `SELECT` statements, the new `JSON` keyword can be used to return
+each row as a single `JSON` encoded map. The remainder of the `SELECT`
+statment behavior is the same.
+
+The result map keys are the same as the column names in a normal result
+set. For example, a statement like ```SELECT JSON a, ttl(b) FROM ...`''
+would result in a map with keys `"a"` and `"ttl(b)"`. However, this is
+one notable exception: for symmetry with `INSERT JSON` behavior,
+case-sensitive column names with upper-case letters will be surrounded
+with double quotes. For example, ```SELECT JSON myColumn FROM ...`''
+would result in a map key `"\"myColumn\""` (note the escaped quotes).
+
+The map values will `JSON`-encoded representations (as described below)
+of the result set values.
+
+[[insertJson]]
+==== INSERT JSON
+
+With `INSERT` statements, the new `JSON` keyword can be used to enable
+inserting a `JSON` encoded map as a single row. The format of the `JSON`
+map should generally match that returned by a `SELECT JSON` statement on
+the same table. In particular, case-sensitive column names should be
+surrounded with double quotes. For example, to insert into a table with
+two columns named ``myKey'' and ``value'', you would do the following:
+
+bc(sample). +
+INSERT INTO mytable JSON `\{``\''myKey\``'': 0, ``value'': 0}'
+
+Any columns which are ommitted from the `JSON` map will be defaulted to
+a `NULL` value (which will result in a tombstone being created).
+
+[[jsonEncoding]]
+==== JSON Encoding of Cassandra Data Types
+
+Where possible, Cassandra will represent and accept data types in their
+native `JSON` representation. Cassandra will also accept string
+representations matching the CQL literal format for all single-field
+types. For example, floats, ints, UUIDs, and dates can be represented by
+CQL literal strings. However, compound types, such as collections,
+tuples, and user-defined types must be represented by native `JSON`
+collections (maps and lists) or a JSON-encoded string representation of
+the collection.
+
+The following table describes the encodings that Cassandra will accept
+in `INSERT JSON` values (and `fromJson()` arguments) as well as the
+format Cassandra will use when returning data for `SELECT JSON`
+statements (and `fromJson()`):
+
+[cols=",,,",options="header",]
+|===
+|type |formats accepted |return format |notes
+|`ascii` |string |string |Uses JSON’s `\u` character escape
+
+|`bigint` |integer, string |integer |String must be valid 64 bit integer
+
+|`blob` |string |string |String should be 0x followed by an even number
+of hex digits
+
+|`boolean` |boolean, string |boolean |String must be ``true'' or
+``false''
+
+|`date` |string |string |Date in format `YYYY-MM-DD`, timezone UTC
+
+|`decimal` |integer, float, string |float |May exceed 32 or 64-bit
+IEEE-754 floating point precision in client-side decoder
+
+|`double` |integer, float, string |float |String must be valid integer
+or float
+
+|`float` |integer, float, string |float |String must be valid integer or
+float
+
+|`inet` |string |string |IPv4 or IPv6 address
+
+|`int` |integer, string |integer |String must be valid 32 bit integer
+
+|`list` |list, string |list |Uses JSON’s native list representation
+
+|`map` |map, string |map |Uses JSON’s native map representation
+
+|`smallint` |integer, string |integer |String must be valid 16 bit
+integer
+
+|`set` |list, string |list |Uses JSON’s native list representation
+
+|`text` |string |string |Uses JSON’s `\u` character escape
+
+|`time` |string |string |Time of day in format `HH-MM-SS[.fffffffff]`
+
+|`timestamp` |integer, string |string |A timestamp. Strings constant are
+allow to input timestamps as dates, see link:#usingdates[Working with
+dates] below for more information. Datestamps with format
+`YYYY-MM-DD HH:MM:SS.SSS` are returned.
+
+|`timeuuid` |string |string |Type 1 UUID. See link:#constants[Constants]
+for the UUID format
+
+|`tinyint` |integer, string |integer |String must be valid 8 bit integer
+
+|`tuple` |list, string |list |Uses JSON’s native list representation
+
+|`UDT` |map, string |map |Uses JSON’s native map representation with
+field names as keys
+
+|`uuid` |string |string |See link:#constants[Constants] for the UUID
+format
+
+|`varchar` |string |string |Uses JSON’s `\u` character escape
+
+|`varint` |integer, string |integer |Variable length; may overflow 32 or
+64 bit integers in client-side decoder
+|===
+
+[[fromJson]]
+==== The fromJson() Function
+
+The `fromJson()` function may be used similarly to `INSERT JSON`, but
+for a single column value. It may only be used in the `VALUES` clause of
+an `INSERT` statement or as one of the column values in an `UPDATE`,
+`DELETE`, or `SELECT` statement. For example, it cannot be used in the
+selection clause of a `SELECT` statement.
+
+[[toJson]]
+==== The toJson() Function
+
+The `toJson()` function may be used similarly to `SELECT JSON`, but for
+a single column value. It may only be used in the selection clause of a
+`SELECT` statement.
+
+[[appendixA]]
+=== Appendix A: CQL Keywords
+
+CQL distinguishes between _reserved_ and _non-reserved_ keywords.
+Reserved keywords cannot be used as identifier, they are truly reserved
+for the language (but one can enclose a reserved keyword by
+double-quotes to use it as an identifier). Non-reserved keywords however
+only have a specific meaning in certain context but can used as
+identifer otherwise. The only _raison d’être_ of these non-reserved
+keywords is convenience: some keyword are non-reserved when it was
+always easy for the parser to decide whether they were used as keywords
+or not.
+
+[cols=",",options="header",]
+|===
+|Keyword |Reserved?
+|`ADD` |yes
+|`AGGREGATE` |no
+|`ALL` |no
+|`ALLOW` |yes
+|`ALTER` |yes
+|`AND` |yes
+|`APPLY` |yes
+|`AS` |no
+|`ASC` |yes
+|`ASCII` |no
+|`AUTHORIZE` |yes
+|`BATCH` |yes
+|`BEGIN` |yes
+|`BIGINT` |no
+|`BLOB` |no
+|`BOOLEAN` |no
+|`BY` |yes
+|`CALLED` |no
+|`CAST` |no
+|`CLUSTERING` |no
+|`COLUMNFAMILY` |yes
+|`COMPACT` |no
+|`CONTAINS` |no
+|`COUNT` |no
+|`COUNTER` |no
+|`CREATE` |yes
+|`CUSTOM` |no
+|`DATE` |no
+|`DECIMAL` |no
+|`DEFAULT` |yes
+|`DELETE` |yes
+|`DESC` |yes
+|`DESCRIBE` |yes
+|`DISTINCT` |no
+|`DOUBLE` |no
+|`DROP` |yes
+|`DURATION` |no
+|`ENTRIES` |yes
+|`EXECUTE` |yes
+|`EXISTS` |no
+|`FILTERING` |no
+|`FINALFUNC` |no
+|`FLOAT` |no
+|`FROM` |yes
+|`FROZEN` |no
+|`FULL` |yes
+|`FUNCTION` |no
+|`FUNCTIONS` |no
+|`GRANT` |yes
+|`GROUP` |no
+|`IF` |yes
+|`IN` |yes
+|`INDEX` |yes
+|`INET` |no
+|`INFINITY` |yes
+|`INITCOND` |no
+|`INPUT` |no
+|`INSERT` |yes
+|`INT` |no
+|`INTO` |yes
+|`IS` |yes
+|`JSON` |no
+|`KEY` |no
+|`KEYS` |no
+|`KEYSPACE` |yes
+|`KEYSPACES` |no
+|`LANGUAGE` |no
+|`LIKE` |no
+|`LIMIT` |yes
+|`LIST` |no
+|`LOGIN` |no
+|`MAP` |no
+|`MATERIALIZED` |yes
+|`MBEAN` |yes
+|`MBEANS` |yes
+|`MODIFY` |yes
+|`NAN` |yes
+|`NOLOGIN` |no
+|`NORECURSIVE` |yes
+|`NOSUPERUSER` |no
+|`NOT` |yes
+|`NULL` |yes
+|`OF` |yes
+|`ON` |yes
+|`OPTIONS` |no
+|`OR` |yes
+|`ORDER` |yes
+|`PARTITION` |no
+|`PASSWORD` |no
+|`PER` |no
+|`PERMISSION` |no
+|`PERMISSIONS` |no
+|`PRIMARY` |yes
+|`RENAME` |yes
+|`REPLACE` |yes
+|`RETURNS` |no
+|`REVOKE` |yes
+|`ROLE` |no
+|`ROLES` |no
+|`SCHEMA` |yes
+|`SELECT` |yes
+|`SET` |yes
+|`SFUNC` |no
+|`SMALLINT` |no
+|`STATIC` |no
+|`STORAGE` |no
+|`STYPE` |no
+|`SUPERUSER` |no
+|`TABLE` |yes
+|`TEXT` |no
+|`TIME` |no
+|`TIMESTAMP` |no
+|`TIMEUUID` |no
+|`TINYINT` |no
+|`TO` |yes
+|`TOKEN` |yes
+|`TRIGGER` |no
+|`TRUNCATE` |yes
+|`TTL` |no
+|`TUPLE` |no
+|`TYPE` |no
+|`UNLOGGED` |yes
+|`UNSET` |yes
+|`UPDATE` |yes
+|`USE` |yes
+|`USER` |no
+|`USERS` |no
+|`USING` |yes
+|`UUID` |no
+|`VALUES` |no
+|`VARCHAR` |no
+|`VARINT` |no
+|`VIEW` |yes
+|`WHERE` |yes
+|`WITH` |yes
+|`WRITETIME` |no
+|===
+
+[[appendixB]]
+=== Appendix B: CQL Reserved Types
+
+The following type names are not currently used by CQL, but are reserved
+for potential future use. User-defined types may not use reserved type
+names as their name.
+
+[cols="",options="header",]
+|===
+|type
+|`bitstring`
+|`byte`
+|`complex`
+|`date`
+|`enum`
+|`interval`
+|`macaddr`
+|===
+
+=== Changes
+
+The following describes the changes in each version of CQL.
+
+==== 3.4.3
+
+* Support for `GROUP BY`. See link:#selectGroupBy[`<group-by>`] (see
+https://issues.apache.org/jira/browse/CASSANDRA-10707)[CASSANDRA-10707].
+
+==== 3.4.2
+
+* Support for selecting elements and slices of a collection
+(https://issues.apache.org/jira/browse/CASSANDRA-7396)[CASSANDRA-7396].
+
+==== 3.4.2
+
+* link:#updateOptions[`INSERT/UPDATE options`] for tables having a
+default_time_to_live specifying a TTL of 0 will remove the TTL from the
+inserted or updated values
+* link:#alterTableStmt[`ALTER TABLE`] `ADD` and `DROP` now allow mutiple
+columns to be added/removed
+* New link:#selectLimit[`PER PARTITION LIMIT`] option (see
+https://issues.apache.org/jira/browse/CASSANDRA-7017)[CASSANDRA-7017].
+* link:#udfs[User-defined functions] can now instantiate `UDTValue` and
+`TupleValue` instances via the new `UDFContext` interface (see
+https://issues.apache.org/jira/browse/CASSANDRA-10818)[CASSANDRA-10818].
+* ``User-defined types''#createTypeStmt may now be stored in a
+non-frozen form, allowing individual fields to be updated and deleted in
+link:#updateStmt[`UPDATE` statements] and link:#deleteStmt[`DELETE`
+statements], respectively.
+(https://issues.apache.org/jira/browse/CASSANDRA-7423)[CASSANDRA-7423]
+
+==== 3.4.1
+
+* Adds `CAST` functions. See link:#castFun[`Cast`].
+
+==== 3.4.0
+
+* Support for link:#createMVStmt[materialized views]
+* link:#deleteStmt[`DELETE`] support for inequality expressions and `IN`
+restrictions on any primary key columns
+* link:#updateStmt[`UPDATE`] support for `IN` restrictions on any
+primary key columns
+
+==== 3.3.1
+
+* The syntax `TRUNCATE TABLE X` is now accepted as an alias for
+`TRUNCATE X`
+
+==== 3.3.0
+
+* Adds new link:#aggregates[aggregates]
+* User-defined functions are now supported through
+link:#createFunctionStmt[`CREATE FUNCTION`] and
+link:#dropFunctionStmt[`DROP FUNCTION`].
+* User-defined aggregates are now supported through
+link:#createAggregateStmt[`CREATE AGGREGATE`] and
+link:#dropAggregateStmt[`DROP AGGREGATE`].
+* Allows double-dollar enclosed strings literals as an alternative to
+single-quote enclosed strings.
+* Introduces Roles to supercede user based authentication and access
+control
+* link:#usingdates[`Date`] and link:usingtime[`Time`] data types have
+been added
+* link:#json[`JSON`] support has been added
+* `Tinyint` and `Smallint` data types have been added
+* Adds new time conversion functions and deprecate `dateOf` and
+`unixTimestampOf`. See link:#timeFun[`Time conversion functions`]
+
+==== 3.2.0
+
+* User-defined types are now supported through
+link:#createTypeStmt[`CREATE TYPE`], link:#alterTypeStmt[`ALTER TYPE`],
+and link:#dropTypeStmt[`DROP TYPE`]
+* link:#createIndexStmt[`CREATE INDEX`] now supports indexing collection
+columns, including indexing the keys of map collections through the
+`keys()` function
+* Indexes on collections may be queried using the new `CONTAINS` and
+`CONTAINS KEY` operators
+* Tuple types were added to hold fixed-length sets of typed positional
+fields (see the section on link:#types[types] )
+* link:#dropIndexStmt[`DROP INDEX`] now supports optionally specifying a
+keyspace
+
+==== 3.1.7
+
+* `SELECT` statements now support selecting multiple rows in a single
+partition using an `IN` clause on combinations of clustering columns.
+See link:#selectWhere[SELECT WHERE] clauses.
+* `IF NOT EXISTS` and `IF EXISTS` syntax is now supported by
+`CREATE USER` and `DROP USER` statmenets, respectively.
+
+==== 3.1.6
+
+* A new link:#uuidFun[`uuid` method] has been added.
+* Support for `DELETE ... IF EXISTS` syntax.
+
+==== 3.1.5
+
+* It is now possible to group clustering columns in a relatiion, see
+link:#selectWhere[SELECT WHERE] clauses.
+* Added support for `STATIC` columns, see link:#createTableStatic[static
+in CREATE TABLE].
+
+==== 3.1.4
+
+* `CREATE INDEX` now allows specifying options when creating CUSTOM
+indexes (see link:#createIndexStmt[CREATE INDEX reference] ).
+
+==== 3.1.3
+
+* Millisecond precision formats have been added to the timestamp parser
+(see link:#usingtimestamps[working with dates] ).
+
+==== 3.1.2
+
+* `NaN` and `Infinity` has been added as valid float contants. They are
+now reserved keywords. In the unlikely case you we using them as a
+column identifier (or keyspace/table one), you will noew need to double
+quote them (see link:#identifiers[quote identifiers] ).
+
+==== 3.1.1
+
+* `SELECT` statement now allows listing the partition keys (using the
+`DISTINCT` modifier). See
+https://issues.apache.org/jira/browse/CASSANDRA-4536[CASSANDRA-4536].
+* The syntax `c IN ?` is now supported in `WHERE` clauses. In that case,
+the value expected for the bind variable will be a list of whatever type
+`c` is.
+* It is now possible to use named bind variables (using `:name` instead
+of `?`).
+
+==== 3.1.0
+
+* link:#alterTableStmt[ALTER TABLE] `DROP` option has been reenabled for
+CQL3 tables and has new semantics now: the space formerly used by
+dropped columns will now be eventually reclaimed (post-compaction). You
+should not readd previously dropped columns unless you use timestamps
+with microsecond precision (see
+https://issues.apache.org/jira/browse/CASSANDRA-3919[CASSANDRA-3919] for
+more details).
+* `SELECT` statement now supports aliases in select clause. Aliases in
+WHERE and ORDER BY clauses are not supported. See the
+link:#selectStmt[section on select] for details.
+* `CREATE` statements for `KEYSPACE`, `TABLE` and `INDEX` now supports
+an `IF NOT EXISTS` condition. Similarly, `DROP` statements support a
+`IF EXISTS` condition.
+* `INSERT` statements optionally supports a `IF NOT EXISTS` condition
+and `UPDATE` supports `IF` conditions.
+
+==== 3.0.5
+
+* `SELECT`, `UPDATE`, and `DELETE` statements now allow empty `IN`
+relations (see
+https://issues.apache.org/jira/browse/CASSANDRA-5626)[CASSANDRA-5626].
+
+==== 3.0.4
+
+* Updated the syntax for custom link:#createIndexStmt[secondary
+indexes].
+* Non-equal condition on the partition key are now never supported, even
+for ordering partitioner as this was not correct (the order was *not*
+the one of the type of the partition key). Instead, the `token` method
+should always be used for range queries on the partition key (see
+link:#selectWhere[WHERE clauses] ).
+
+==== 3.0.3
+
+* Support for custom link:#createIndexStmt[secondary indexes] has been
+added.
+
+==== 3.0.2
+
+* Type validation for the link:#constants[constants] has been fixed. For
+instance, the implementation used to allow `'2'` as a valid value for an
+`int` column (interpreting it has the equivalent of `2`), or `42` as a
+valid `blob` value (in which case `42` was interpreted as an hexadecimal
+representation of the blob). This is no longer the case, type validation
+of constants is now more strict. See the link:#types[data types] section
+for details on which constant is allowed for which type.
+* The type validation fixed of the previous point has lead to the
+introduction of link:#constants[blobs constants] to allow inputing
+blobs. Do note that while inputing blobs as strings constant is still
+supported by this version (to allow smoother transition to blob
+constant), it is now deprecated (in particular the link:#types[data
+types] section does not list strings constants as valid blobs) and will
+be removed by a future version. If you were using strings as blobs, you
+should thus update your client code ASAP to switch blob constants.
+* A number of functions to convert native types to blobs have also been
+introduced. Furthermore the token function is now also allowed in select
+clauses. See the link:#functions[section on functions] for details.
+
+==== 3.0.1
+
+* link:#usingtimestamps[Date strings] (and timestamps) are no longer
+accepted as valid `timeuuid` values. Doing so was a bug in the sense
+that date string are not valid `timeuuid`, and it was thus resulting in
+https://issues.apache.org/jira/browse/CASSANDRA-4936[confusing
+behaviors]. However, the following new methods have been added to help
+working with `timeuuid`: `now`, `minTimeuuid`, `maxTimeuuid` , `dateOf`
+and `unixTimestampOf`. See the link:#timeuuidFun[section dedicated to
+these methods] for more detail.
+* ``Float constants''#constants now support the exponent notation. In
+other words, `4.2E10` is now a valid floating point value.
+
+=== Versioning
+
+Versioning of the CQL language adheres to the http://semver.org[Semantic
+Versioning] guidelines. Versions take the form X.Y.Z where X, Y, and Z
+are integer values representing major, minor, and patch level
+respectively. There is no correlation between Cassandra release versions
+and the CQL language version.
+
+[cols=",",options="header",]
+|===
+|version |description
+|Major |The major version _must_ be bumped when backward incompatible
+changes are introduced. This should rarely occur.
+
+|Minor |Minor version increments occur when new, but backward
+compatible, functionality is introduced.
+
+|Patch |The patch version is incremented when bugs are fixed.
+|===
diff --git a/doc/modules/cassandra/pages/cql/ddl.adoc b/doc/modules/cassandra/pages/cql/ddl.adoc
new file mode 100644
index 0000000..964780d
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/ddl.adoc
@@ -0,0 +1,772 @@
+= Data Definition
+:tabs:
+
+CQL stores data in _tables_, whose schema defines the layout of the
+data in the table. Tables are located in _keyspaces_.
+A keyspace defines options that apply to all the keyspace's tables.
+The xref:cql/ddl.adoc#replication-strategy[replication strategy] is an important keyspace option, as is the replication factor.
+A good general rule is one keyspace per application.
+It is common for a cluster to define only one keyspace for an actie application.
+
+This section describes the statements used to create, modify, and remove
+those keyspace and tables.
+
+== Common definitions
+
+The names of the keyspaces and tables are defined by the following
+grammar:
+
+[source,bnf]
+----
+include::example$BNF/ks_table.bnf[]
+----
+
+Both keyspace and table name should be comprised of only alphanumeric
+characters, cannot be empty and are limited in size to 48 characters
+(that limit exists mostly to avoid filenames (which may include the
+keyspace and table name) to go over the limits of certain file systems).
+By default, keyspace and table names are case-insensitive (`myTable` is
+equivalent to `mytable`) but case sensitivity can be forced by using
+double-quotes (`"myTable"` is different from `mytable`).
+
+Further, a table is always part of a keyspace and a table name can be
+provided fully-qualified by the keyspace it is part of. If is is not
+fully-qualified, the table is assumed to be in the _current_ keyspace
+(see xref:cql/ddl.adoc#use-statement[USE] statement.
+
+Further, the valid names for columns are defined as:
+
+[source,bnf]
+----
+include::example$BNF/column.bnf[]
+----
+
+We also define the notion of statement options for use in the following
+section:
+
+[source,bnf]
+----
+include::example$BNF/options.bnf[]
+----
+
+[[create-keyspace-statement]]
+== CREATE KEYSPACE
+
+A keyspace is created with a `CREATE KEYSPACE` statement:
+
+[source,bnf]
+----
+include::example$BNF/create_ks.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/create_ks.cql[]
+----
+
+Attempting to create a keyspace that already exists will return an error
+unless the `IF NOT EXISTS` option is used. If it is used, the statement
+will be a no-op if the keyspace already exists.
+
+The supported `options` are:
+
+[cols=",,,,",options="header",]
+|===
+|name | kind | mandatory | default | description
+|`replication` | _map_ | yes | n/a | The replication strategy and options to use for the keyspace (see
+details below).
+|`durable_writes` | _simple_ | no | true | Whether to use the commit log for updates on this keyspace (disable this
+option at your own risk!).
+|===
+
+The `replication` property is mandatory and must contain the `'class'` sub-option that defines the desired
+xref:cql/ddl.adoc#replication-strategy[replication strategy] class.
+The rest of the sub-options depend on which replication strategy is used.
+By default, Cassandra supports the following `'class'` values:
+
+[[replication-strategy]]
+=== `SimpleStrategy`
+
+A simple strategy that defines a replication factor for data to be
+spread across the entire cluster. This is generally not a wise choice
+for production, as it does not respect datacenter layouts and can
+lead to wildly varying query latency. For production, use
+`NetworkTopologyStrategy`. `SimpleStrategy` supports a single
+mandatory argument:
+
+[cols=",,,",options="header",]
+|===
+|sub-option |type |since |description
+|`'replication_factor'` | int | all | The number of replicas to store per range
+|===
+
+=== `NetworkTopologyStrategy`
+
+A production-ready replication strategy that sets the
+replication factor independently for each data-center. The rest of the
+sub-options are key-value pairs, with a key set to a data-center name and
+its value set to the associated replication factor. Options:
+
+[cols=",,,",options="header",]
+|===
+|sub-option |type |description
+|`'<datacenter>'` | int | The number of replicas to store per range in the provided datacenter.
+|`'replication_factor'` | int | The number of replicas to use as a default per datacenter if not
+specifically provided. Note that this always defers to existing
+definitions or explicit datacenter settings. For example, to have three
+replicas per datacenter, set a value of 3.
+|===
+
+When later altering keyspaces and changing the `replication_factor`,
+auto-expansion will only _add_ new datacenters for safety, it will not
+alter existing datacenters or remove any, even if they are no longer in
+the cluster. If you want to remove datacenters while setting the
+`replication_factor`, explicitly zero out the datacenter you want to
+have zero replicas.
+
+An example of auto-expanding datacenters with two datacenters: `DC1` and
+`DC2`:
+
+[source,cql]
+----
+include::example$CQL/autoexpand_ks.cql[]
+----
+will result in:
+[source,plaintext]
+----
+include::example$RESULTS/autoexpand_ks.result[]
+----
+
+An example of auto-expanding and overriding a datacenter:
+
+[source,cql]
+----
+include::example$CQL/autoexpand_ks_override.cql[]
+----
+will result in:
+[source,plaintext]
+----
+include::example$RESULTS/autoexpand_ks_override.result[]
+----
+
+An example that excludes a datacenter while using `replication_factor`:
+
+[source,cql]
+----
+include::example$CQL/autoexpand_exclude_dc.cql[]
+----
+will result in:
+[source,plaintext]
+----
+include::example$RESULTS/autoexpand_exclude_dc.result[]
+----
+
+If xref:new/transientreplication.adoc[transient replication] has been enabled, transient replicas can be
+configured for both `SimpleStrategy` and `NetworkTopologyStrategy` by
+defining replication factors in the format
+`'<total_replicas>/<transient_replicas>'`
+
+For instance, this keyspace will have 3 replicas in DC1, 1 of which is
+transient, and 5 replicas in DC2, 2 of which are transient:
+
+[source,cql]
+----
+include::example$CQL/create_ks_trans_repl.cql[]
+----
+
+[[use-statement]]
+== USE
+
+The `USE` statement changes the _current_ keyspace to the specified keyspace.
+A number of objects in CQL are bound to a keyspace (tables, user-defined types, functions, etc.) and the
+current keyspace is the default keyspace used when those objects are
+referred to in a query without a fully-qualified name (without a prefixed keyspace name).
+A `USE` statement specifies the keyspace to use as an argument:
+
+[source,bnf]
+----
+include::example$BNF/use_ks.bnf[]
+----
+Using CQL:
+[source,cql]
+----
+include::example$CQL/use_ks.cql[]
+----
+
+[[alter-keyspace-statement]]
+== ALTER KEYSPACE
+
+An `ALTER KEYSPACE` statement modifies the options of a keyspace:
+
+[source,bnf]
+----
+include::example$BNF/alter_ks.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/alter_ks.cql[]
+----
+
+The supported options are the same as for xref:cql/ddl.adoc#create-keyspace-statement[creating a keyspace].
+
+[[drop-keyspace-statement]]
+== DROP KEYSPACE
+
+Dropping a keyspace is done with the `DROP KEYSPACE` statement:
+
+[source,bnf]
+----
+include::example$BNF/drop_ks.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/drop_ks.cql[]
+----
+
+Dropping a keyspace results in the immediate, irreversible removal of
+that keyspace, including all the tables, user-defined types, user-defined functions, and
+all the data contained in those tables.
+
+If the keyspace does not exists, the statement will return an error,
+unless `IF EXISTS` is used in which case the operation is a no-op.
+
+[[create-table-statement]]
+== CREATE TABLE
+
+Creating a new table uses the `CREATE TABLE` statement:
+
+[source,bnf]
+----
+include::example$BNF/create_table.bnf[]
+----
+
+For example, here are some CQL statements to create tables:
+
+[source,cql]
+----
+include::example$CQL/create_table.cql[]
+----
+
+A CQL table has a name and is composed of a set of _rows_.
+Creating a table amounts to defining which xref:cql/ddl.adoc#column-definition[columns] each rows will have,
+which of those columns comprise the xref:cql/ddl.adoc#primary-key[primary key], as well as defined
+xref:cql/ddl.adoc#create-table-options[options] for the table.
+
+Attempting to create an already existing table will return an error
+unless the `IF NOT EXISTS` directive is used. If it is used, the
+statement will be a no-op if the table already exists.
+
+[[column-definition]]
+=== Column definitions
+
+Every row in a CQL table will have the predefined columns defined at table creation.
+Columns can be added later using an xref:cql/ddl.adoc#alter-table-statement[alter statement].
+
+A `column_definition` is comprised of the name of the column and its xref:cql/ddl.adoc#data-type[type],
+restricting the  values that are accepted for that column. Additionally, a column definition can have the
+following modifiers:
+
+* `STATIC`: declares the column as a xref:cql/ddl.adoc#static-column[static column]
+* `PRIMARY KEY`: declares the column as the sole component of the xref:cql/ddl.adoc#primary-key[primary key] of the table
+
+[[static-column]]
+==== Static columns
+
+Some columns can be declared as `STATIC` in a table definition. A column
+that is static will be “shared” by all the rows belonging to the same
+partition (having the same xref:cql/ddl.adoc#partition-key[partition key].
+
+For example:
+
+[{tabs}]
+====
+Code::
++
+--
+[source,cql]
+----
+include::example$CQL/create_static_column.cql[]
+include::example$CQL/insert_static_data.cql[]
+include::example$CQL/select_static_data.cql[]
+----
+--
+
+Results::
++
+--
+[source,cql]
+----
+include::example$RESULTS/select_static_data.result[]
+----
+--
+====
+
+As can be seen, the `s` value is the same (`static1`) for both of the
+rows in the partition (the partition key being `pk`, and both
+rows are in the same partition): the second insertion overrides the
+value for `s`.
+
+The use of static columns has the following restrictions:
+
+* A table without clustering columns cannot have static columns.
+In a table without clustering columns, every partition has only one row, and
+so every column is inherently static)
+* Only non-primary key columns can be static.
+
+[[primary-key]]
+=== The Primary key
+
+Within a table, a row is uniquely identified by its `PRIMARY KEY`, and
+hence all tables *must* define a single PRIMARY KEY.
+A `PRIMARY KEY` is composed of one or more of the defined columns in the table.
+Syntactically, the primary key is defined with the phrase `PRIMARY KEY`
+followed by a comma-separated list of the column names within parenthesis.
+If the primary key has only one column, you can alternatively add the `PRIMARY KEY` phrase to
+that column in the table definition.
+The order of the columns in the primary key definition defines the partition key and
+clustering columns.
+
+A CQL primary key is composed of two parts:
+
+xref:cql/ddl.adoc#partition-key[partition key]::
+* It is the first component of the primary key definition.
+It can be a single column or, using an additional set of parenthesis, can be multiple columns.
+A table must have at least one partition key, the smallest possible table definition is:
++
+[source,cql]
+----
+include::example$CQL/create_table_single_pk.cql[]
+----
+xref:cql/ddl.adoc#clustering-columns[clustering columns]::
+* The columns are the columns that follow the partition key in the primary key definition.
+The order of those columns define the _clustering order_.
+
+Some examples of primary key definition are:
+
+* `PRIMARY KEY (a)`: `a` is the single partition key and there are no clustering columns
+* `PRIMARY KEY (a, b, c)` : `a` is the single partition key and `b` and `c` are the clustering columns
+* `PRIMARY KEY ((a, b), c)` : `a` and `b` compose the _composite_ partition key and `c` is the clustering column
+
+[IMPORTANT]
+====
+The primary key uniquely identifies a row in the table, as described above.
+A consequence of this uniqueness is that if another row is inserted using the same primary key,
+then an `UPSERT` occurs and an existing row with the same primary key is replaced.
+Columns that are not part of the primary key cannot define uniqueness.
+====
+
+[[partition-key]]
+==== Partition key
+
+Within a table, CQL defines the notion of a _partition_ that defines the location of data within a Cassandra cluster.
+A partition is the set of rows that share the same value for their partition key.
+
+Note that if the partition key is composed of multiple columns, then rows belong to the same partition
+when they have the same values for all those partition key columns.
+A hash is computed from the partition key columns and that hash value defines the partition location.
+So, for instance, given the following table definition and content:
+
+[source,cql]
+----
+include::example$CQL/create_table_compound_pk.cql[]
+include::example$CQL/insert_table_compound_pk.cql[]
+include::example$CQL/select_table_compound_pk.cql[]
+----
+
+will result in
+[source,cql]
+----
+include::example$RESULTS/select_table_compound_pk.result[]
+----
+<1> Rows 1 and 2 are in the same partition, because both columns `a` and `b` are zero.
+<2> Rows 3 and 4 are in the same partition, but a different one, because column `a` is zero and column `b` is 1 in both rows.
+<3> Row 5 is in a third partition by itself, because both columns `a` and `b` are 1.
+
+Note that a table always has a partition key, and that if the table has
+no `clustering columns`, then every partition of that table has a single row.
+because the partition key, compound or otherwise, identifies a single location.
+
+The most important property of partition is that all the rows belonging
+to the same partition are guaranteed to be stored on the same set of
+replica nodes.
+In other words, the partition key of a table defines which rows will be localized on the same
+node in the cluster.
+The localization of data is important to the efficient retrieval of data, requiring the Cassandra coordinator
+to contact as few nodes as possible.
+However, there is a flip-side to this guarantee, and all rows sharing a partition key will be stored on the same
+node, creating a hotspot for both reading and writing.
+While selecting a primary key that groups table rows assists batch updates and can ensure that the updates are
+_atomic_ and done in _isolation_, the partitions must be sized "just right, not too big nor too small".
+
+Data modeling that considers the querying patterns and assigns primary keys based on the queries will have the lowest
+latency in fetching data.
+
+[[clustering-columns]]
+==== Clustering columns
+
+The clustering columns of a table define the clustering order for the partition of that table.
+For a given `partition`, all rows are ordered by that clustering order. Clustering columns also add uniqueness to
+a row in a table.
+
+For instance, given:
+
+[source,cql]
+----
+include::example$CQL/create_table_clustercolumn.cql[]
+include::example$CQL/insert_table_clustercolumn.cql[]
+include::example$CQL/select_table_clustercolumn.cql[]
+----
+
+will result in
+[source,cql]
+----
+include::example$RESULTS/select_table_clustercolumn.result[]
+----
+<1> Row 1 is in one partition, and Rows 2-5 are in a different one. The display order is also different.
+
+Looking more closely at the four rows in the same partition, the `b` clustering column defines the order in which those rows
+are displayed.
+Whereas the partition key of the table groups rows on the same node, the clustering columns control
+how those rows are stored on the node.
+
+That sorting allows the very efficient retrieval of a range of rows within a partition:
+
+[source,cql]
+----
+include::example$CQL/select_range.cql[]
+----
+
+will result in
+[source,cql]
+----
+include::example$RESULTS/select_range.result[]
+----
+
+[[create-table-options]]
+=== Table options
+
+A CQL table has a number of options that can be set at creation (and,
+for most of them, altered later). These options are specified after the
+`WITH` keyword.
+
+One important option that cannot be changed after creation, `CLUSTERING ORDER BY`, influences how queries can be done against the table. It is worth discussing in more detail here.
+
+[[clustering-order]]
+==== Clustering order
+
+The clustering order of a table is defined by the clustering columns.
+By default, the clustering order is ascending for the clustering column's data types.
+For example, integers order from 1, 2, ... n, while text orders from A to Z.
+
+The `CLUSTERING ORDER BY` table option uses a comma-separated list of the
+clustering columns, each set for either `ASC` (for _ascending_ order) or `DESC` (for _descending order).
+The default is ascending for all clustering columns if the `CLUSTERING ORDER BY` option is not set.
+
+This option is basically a hint for the storage engine that changes the order in which it stores the row.
+Beware of the consequences of setting this option:
+
+* It changes the default ascending order of results when queried with a `SELECT` statement with no `ORDER BY` clause.
+
+* It limits how the `ORDER BY` clause is used in `SELECT` statements on that table.
+Results can only be ordered with either the original clustering order or the reverse clustering order.
+Suppose you create a table with two clustering columns `a` and `b`, defined `WITH CLUSTERING ORDER BY (a DESC, b ASC)`.
+Queries on the table can use `ORDER BY (a DESC, b ASC)` or `ORDER BY (a ASC, b DESC)`.
+Mixed order, such as `ORDER BY (a ASC, b ASC)` or `ORDER BY (a DESC, b DESC)` will not return expected order.
+
+* It has a performance impact on queries. Queries in reverse clustering order are slower than the default ascending order.
+If you plan to query mostly in descending order, declare the clustering order in the table schema using `WITH CLUSTERING ORDER BY ()`.
+This optimization is common for time series, to retrieve the data from newest to oldest.
+
+[[create-table-general-options]]
+==== Other table options
+
+A table supports the following options:
+
+[width="100%",cols="30%,9%,11%,50%",options="header",]
+|===
+|option | kind | default | description
+
+| `comment` | _simple_ | none | A free-form, human-readable comment
+| `read_repair_chance` | _simple_ | 0.1 | The probability with which to query extra nodes
+(e.g. more nodes than required by the consistency level) for the purpose of read repairs.
+| `dclocal_read_repair_chance` | _simple_ | 0 | The probability with which to query extra nodes
+(e.g. more nodes than required by the consistency level) belonging to the same data center than
+the read coordinator for the purpose of read repairs.
+| xref:cql/ddl.adoc#spec_retry[`speculative_retry`] | _simple_ | 99PERCENTILE | Speculative retry options
+| `cdc` |_boolean_ |false |Create a Change Data Capture (CDC) log on the table
+| `additional_write_policy` |_simple_ |99PERCENTILE | Same as `speculative_retry`
+| `gc_grace_seconds` |_simple_ |864000 |Time to wait before garbage collecting tombstones (deletion markers)
+| `bloom_filter_fp_chance` |_simple_ |0.00075 |The target probability of
+false positive of the sstable bloom filters. Said bloom filters will be
+sized to provide the provided probability, thus lowering this value
+impact the size of bloom filters in-memory and on-disk.
+| `default_time_to_live` |_simple_ |0 |Default expiration time (“TTL”) in seconds for a table
+| `compaction` |_map_ |_see below_ | xref:operating/compaction/index.adoc#cql-compaction-options[Compaction options]
+| `compression` |_map_ |_see below_ | xref:operating/compression/index.adoc#cql-compression-options[Compression options]
+| `caching` |_map_ |_see below_ |Caching options
+| `memtable_flush_period_in_ms` |_simple_ |0 |Time (in ms) before Cassandra flushes memtables to disk
+|===
+
+[[spec_retry]]
+===== Speculative retry options
+
+By default, Cassandra read coordinators only query as many replicas as
+necessary to satisfy consistency levels: one for consistency level
+`ONE`, a quorum for `QUORUM`, and so on. `speculative_retry` determines
+when coordinators may query additional replicas, a useful action when
+replicas are slow or unresponsive. Speculative retries reduce the latency.
+The speculative_retry option configures rapid read protection, where a coordinator sends more
+requests than needed to satisfy the consistency level.
+
+[IMPORTANT]
+====
+Frequently reading from additional replicas can hurt cluster
+performance. When in doubt, keep the default `99PERCENTILE`.
+====
+
+Pre-Cassandra 4.0 speculative retry policy takes a single string as a parameter:
+
+* `NONE`
+* `ALWAYS`
+* `99PERCENTILE` (PERCENTILE)
+* `50MS` (CUSTOM)
+
+An example of setting speculative retry sets a custom value:
+
+[source,cql]
+----
+include::example$CQL/alter_table_spec_retry.cql[]
+----
+
+This example uses a percentile for the setting:
+
+[source,cql]
+----
+include::example$CQL/alter_table_spec_retry_percent.cql[]
+----
+
+A percentile setting can backfire. If a single host becomes unavailable, it can
+force up the percentiles. A value of `p99` will not speculate as intended because the
+value at the specified percentile has increased too much. If the consistency level is set to `ALL`, all
+replicas are queried regardless of the speculative retry setting.
+
+Cassandra 4.0 supports case-insensitivity for speculative retry values (https://issues.apache.org/jira/browse/CASSANDRA-14293[CASSANDRA-14293]). For example, assigning the value as `none`, `None`, or `NONE` has the same effect.
+
+Additionally, the following values are added:
+
+[cols=",,",options="header",]
+|===
+|Format |Example |Description
+| `XPERCENTILE` | 90.5PERCENTILE | Coordinators record average per-table response times
+for all replicas. If a replica takes longer than `X` percent of this
+table's average response time, the coordinator queries an additional
+replica. `X` must be between 0 and 100.
+| `XP` | 90.5P | Same as `XPERCENTILE`
+| `Yms` | 25ms | If a replica takes more than `Y` milliseconds to respond, the
+coordinator queries an additional replica.
+| `MIN(XPERCENTILE,YMS)` | MIN(99PERCENTILE,35MS) | A hybrid policy that uses either the
+specified percentile or fixed milliseconds depending on which value is
+lower at the time of calculation. Parameters are `XPERCENTILE`, `XP`, or
+`Yms`. This setting helps protect against a single slow instance.
+
+| `MAX(XPERCENTILE,YMS)` `ALWAYS` `NEVER` | MAX(90.5P,25ms) | A hybrid policy that uses either the specified
+percentile or fixed milliseconds depending on which value is higher at
+the time of calculation.
+|===
+
+Cassandra 4.0 adds support for hybrid `MIN()` and `MAX()` speculative retry policies, with a mix and match of either `MIN(), MAX()`, `MIN(), MIN()`, or `MAX(), MAX()` (https://issues.apache.org/jira/browse/CASSANDRA-14293[CASSANDRA-14293]).
+The hybrid mode will still speculate if the normal `p99` for the table is < 50ms, the minimum value.
+But if the `p99` level goes higher than the maximum value, then that value can be used.
+In a hybrid value, one value must be a fixed time (ms) value and the other a percentile value.
+
+To illustrate variations, the following examples are all valid:
+
+[source,cql]
+----
+include::example$CQL/spec_retry_values.cql[]
+----
+
+The `additional_write_policy` setting specifies the threshold at which a cheap
+quorum write will be upgraded to include transient replicas.
+
+[[cql-compaction-options]]
+===== Compaction options
+
+The `compaction` options must minimally define the `'class'` sub-option,
+to specify the compaction strategy class to use.
+The supported classes are:
+
+* `'SizeTieredCompactionStrategy'`, xref:operating/compaction/stcs.adoc#stcs[STCS] (Default)
+* `'LeveledCompactionStrategy'`, xref:operating/compaction/lcs.adoc#lcs[LCS]
+* `'TimeWindowCompactionStrategy'`, xref:operating/compaction/twcs.adoc#twcs[TWCS]
+
+The `'DateTieredCompactionStrategy'` is also supported but deprecated;
+`'TimeWindowCompactionStrategy'` should be used.
+If a custom strategies is required, specify the full class name as a xref:cql/definitions.adoc#constants[string constant].
+
+All default strategies support a number of xref:operating/compaction/index.adoc#compaction-options[common options], as well as options specific to the strategy chosen. See the section corresponding to your strategy for details: xref:operating/compaction/stcs.adoc#stcs_options[STCS], xref:operating/compaction/lcs.adoc#lcs_options[LCS], xref:operating/compaction/twcs.adoc#twcs_options[TWCS].
+
+[[cql-compression-options]]
+===== Compression options
+
+The `compression` options define if and how the SSTables of the table
+are compressed. Compression is configured on a per-table basis as an
+optional argument to `CREATE TABLE` or `ALTER TABLE`. The following
+sub-options are available:
+
+[cols=",,",options="header",]
+|===
+|Option |Default |Description
+| `class` | LZ4Compressor | The compression algorithm to use. Default compressor are: LZ4Compressor,
+SnappyCompressor, DeflateCompressor and ZstdCompressor.
+Use `'enabled' : false` to disable compression.
+Custom compressor can be provided by specifying the full class name as a xref:cql/definitions.adoc#constants[string constant].
+
+| `enabled` | true | Enable/disable sstable compression.
+If the `enabled` option is set to `false`, no other options must be specified.
+
+| `chunk_length_in_kb` | 64 | On disk SSTables are compressed by block (to allow random reads).
+This option defines the size (in KB) of said block. See xref:cql/ddl.adoc#chunk_note[note] for further information.
+
+| `crc_check_chance` | 1.0 | Determines how likely Cassandra is to verify the checksum on each
+compression chunk during reads.
+
+| `compression_level` | 3 | Compression level. Only applicable for `ZstdCompressor`.
+Accepts values between `-131072` and `22`.
+|===
+
+[[chunk_note]]
+[NOTE]
+====
+Bigger values may improve the compression rate, but will increase the minimum size of data to be read from
+disk for a read.
+The default value is an optimal value for compressing tables.
+Chunk length must be a power of 2 when computing the chunk number from an uncompressed file offset.
+Block size may be adjusted based on read/write access patterns such as:
+
+* How much data is typically requested at once
+* Average size of rows in the table
+====
+
+For instance, to create a table with LZ4Compressor and a `chunk_length_in_kb` of 4 KB:
+
+[source,cql]
+----
+include::example$CQL/chunk_length.cql[]
+----
+
+[[cql-caching-options]]
+===== Caching options
+
+Caching optimizes the use of cache memory of a table. The cached data is
+weighed by size and access frequency.
+The `caching` options can configure both the `key cache` and the `row cache` for the table.
+The following sub-options are available:
+
+[cols=",,",options="header",]
+|===
+|Option |Default |Description
+| `keys` | ALL | Whether to cache keys (key cache) for this table. Valid values are: `ALL` and `NONE`.
+
+| `rows_per_partition` | NONE | The amount of rows to cache per partition (row cache).
+If an integer `n` is specified, the first `n` queried rows of a partition will be cached.
+Valid values are: `ALL`, to cache all rows of a queried partition, or `NONE` to disable row caching.
+|===
+
+For instance, to create a table with both a key cache and 10 rows cached per partition:
+
+[source,cql]
+----
+include::example$CQL/caching_option.cql[]
+----
+
+===== Other considerations:
+
+* Adding new columns (see `ALTER TABLE` below) is a constant time
+operation. Thus, there is no need to anticipate future usage while initially creating a table.
+
+[[alter-table-statement]]
+== ALTER TABLE
+
+Altering an existing table uses the `ALTER TABLE` statement:
+
+[source,bnf]
+----
+include::example$BNF/alter_table.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/alter_table_add_column.cql[]
+include::example$CQL/alter_table_with_comment.cql[]
+----
+
+The `ALTER TABLE` statement can:
+
+* `ADD` a new column to a table. The primary key of a table cannot ever be altered.
+A new column, thus, cannot be part of the primary key.
+Adding a column is a constant-time operation based on the amount of data in the table.
+* `DROP` a column from a table. This command drops both the column and all
+its content. Be aware that, while the column becomes immediately
+unavailable, its content are removed lazily during compaction. Because of this lazy removal,
+the command is a constant-time operation based on the amount of data in the table.
+Also, it is important to know that once a column is dropped, a column with the same name can be re-added,
+unless the dropped column was a non-frozen column like a collection.
+
+[WARNING]
+.Warning
+====
+Dropping a column assumes that the timestamps used for the value of this
+column are "real" timestamp in microseconds. Using "real" timestamps in
+microseconds is the default is and is *strongly* recommended but as
+Cassandra allows the client to provide any timestamp on any table, it is
+theoretically possible to use another convention. Please be aware that
+if you do so, dropping a column will not correctly execute.
+====
+
+* Use `WITH` to change a table option. The xref:CQL/ddl.adoc#create-table-options[supported options]
+are the same as those used when creating a table, with the exception of `CLUSTERING ORDER`.
+However, setting any `compaction` sub-options will erase *ALL* previous `compaction` options, so you need to re-specify
+all the sub-options you wish to keep. The same is true for `compression` sub-options.
+
+[[drop-table-statement]]
+== DROP TABLE
+
+Dropping a table uses the `DROP TABLE` statement:
+
+[source,bnf]
+----
+include::example$BNF/drop_table.bnf[]
+----
+
+Dropping a table results in the immediate, irreversible removal of the
+table, including all data it contains.
+
+If the table does not exist, the statement will return an error, unless
+`IF EXISTS` is used, when the operation is a no-op.
+
+[[truncate-statement]]
+== TRUNCATE
+
+A table can be truncated using the `TRUNCATE` statement:
+
+[source,bnf]
+----
+include::example$BNF/truncate_table.bnf[]
+----
+
+`TRUNCATE TABLE foo` is the preferred syntax for consistency with other DDL
+statements.
+However, tables are the only object that can be truncated currently, and the `TABLE` keyword can be omitted.
+
+Truncating a table permanently removes all existing data from the table, but without removing the table itself.
diff --git a/doc/modules/cassandra/pages/cql/definitions.adoc b/doc/modules/cassandra/pages/cql/definitions.adoc
new file mode 100644
index 0000000..95be20f
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/definitions.adoc
@@ -0,0 +1,187 @@
+= Definitions
+
+== Conventions
+
+To aid in specifying the CQL syntax, we will use the following
+conventions in this document:
+
+* Language rules will be given in an informal
+http://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form#Variants[BNF
+variant] notation. In particular, we'll use square brakets (`[ item ]`)
+for optional items, `*` and `+` for repeated items (where `+` imply at
+least one).
+* The grammar will also use the following convention for convenience:
+non-terminal term will be lowercase (and link to their definition) while
+terminal keywords will be provided "all caps". Note however that
+keywords are `identifiers` and are thus case insensitive in practice. We
+will also define some early construction using regexp, which we'll
+indicate with `re(<some regular expression>)`.
+* The grammar is provided for documentation purposes and leave some
+minor details out. For instance, the comma on the last column definition
+in a `CREATE TABLE` statement is optional but supported if present even
+though the grammar in this document suggests otherwise. Also, not
+everything accepted by the grammar is necessarily valid CQL.
+* References to keywords or pieces of CQL code in running text will be
+shown in a `fixed-width font`.
+
+[[identifiers]]
+== Identifiers and keywords
+
+The CQL language uses _identifiers_ (or _names_) to identify tables,
+columns and other objects. An identifier is a token matching the regular
+expression `[a-zA-Z][a-zA-Z0-9_]*`.
+
+A number of such identifiers, like `SELECT` or `WITH`, are _keywords_.
+They have a fixed meaning for the language and most are reserved. The
+list of those keywords can be found in xref:cql/appendices.adoc#appendix-A[Appendix A].
+
+Identifiers and (unquoted) keywords are case insensitive. Thus `SELECT`
+is the same than `select` or `sElEcT`, and `myId` is the same than
+`myid` or `MYID`. A convention often used (in particular by the samples
+of this documentation) is to use uppercase for keywords and lowercase
+for other identifiers.
+
+There is a second kind of identifier called a _quoted identifier_
+defined by enclosing an arbitrary sequence of characters (non-empty) in
+double-quotes(`"`). Quoted identifiers are never keywords. Thus
+`"select"` is not a reserved keyword and can be used to refer to a
+column (note that using this is particularly ill-advised), while `select`
+would raise a parsing error. Also, unlike unquoted identifiers
+and keywords, quoted identifiers are case sensitive (`"My Quoted Id"` is
+_different_ from `"my quoted id"`). A fully lowercase quoted identifier
+that matches `[a-zA-Z][a-zA-Z0-9_]*` is however _equivalent_ to the
+unquoted identifier obtained by removing the double-quote (so `"myid"`
+is equivalent to `myid` and to `myId` but different from `"myId"`).
+Inside a quoted identifier, the double-quote character can be repeated
+to escape it, so `"foo "" bar"` is a valid identifier.
+
+[NOTE]
+.Note
+====
+The _quoted identifier_ can declare columns with arbitrary names, and
+these can sometime clash with specific names used by the server. For
+instance, when using conditional update, the server will respond with a
+result set containing a special result named `"[applied]"`. If you’ve
+declared a column with such a name, this could potentially confuse some
+tools and should be avoided. In general, unquoted identifiers should be
+preferred but if you use quoted identifiers, it is strongly advised that you
+avoid any name enclosed by squared brackets (like `"[applied]"`) and any
+name that looks like a function call (like `"f(x)"`).
+====
+
+More formally, we have:
+
+[source, bnf]
+----
+include::example$BNF/identifier.bnf[]
+----
+
+[[constants]]
+== Constants
+
+CQL defines the following _constants_:
+
+[source, bnf]
+----
+include::example$BNF/constant.bnf[]
+----
+
+In other words:
+
+* A string constant is an arbitrary sequence of characters enclosed by
+single-quote(`'`). A single-quote can be included by repeating it, e.g.
+`'It''s raining today'`. Those are not to be confused with quoted
+`identifiers` that use double-quotes. Alternatively, a string can be
+defined by enclosing the arbitrary sequence of characters by two dollar
+characters, in which case single-quote can be used without escaping
+(`$$It's raining today$$`). That latter form is often used when defining
+xref:cql/functions.adoc#udfs[user-defined functions] to avoid having to escape single-quote
+characters in function body (as they are more likely to occur than
+`$$`).
+* Integer, float and boolean constant are defined as expected. Note
+however than float allows the special `NaN` and `Infinity` constants.
+* CQL supports
+https://en.wikipedia.org/wiki/Universally_unique_identifier[UUID]
+constants.
+* Blobs content are provided in hexadecimal and prefixed by `0x`.
+* The special `NULL` constant denotes the absence of value.
+
+For how these constants are typed, see the xref:cql/types.adoc[Data types] section.
+
+== Terms
+
+CQL has the notion of a _term_, which denotes the kind of values that
+CQL support. Terms are defined by:
+
+[source, bnf]
+----
+include::example$BNF/term.bnf[]
+----
+
+A term is thus one of:
+
+* A xref:cql/defintions.adoc#constants[constant]
+* A literal for either a xref:cql/types.adoc#collections[collection],
+a xref:cql/types.adoc#udts[user-defined type] or a xref:cql/types.adoc#tuples[tuple]
+* A xref:cql/functions.adoc#cql-functions[function] call, either a xref:cql/functions.adoc#scalar-native-functions[native function]
+or a xref:cql/functions.adoc#user-defined-scalar-functions[user-defined function]
+* An xref:cql/operators.adoc#arithmetic_operators[arithmetic operation] between terms
+* A type hint
+* A bind marker, which denotes a variable to be bound at execution time.
+See the section on `prepared-statements` for details. A bind marker can
+be either anonymous (`?`) or named (`:some_name`). The latter form
+provides a more convenient way to refer to the variable for binding it
+and should generally be preferred.
+
+== Comments
+
+A comment in CQL is a line beginning by either double dashes (`--`) or
+double slash (`//`).
+
+Multi-line comments are also supported through enclosure within `/*` and
+`*/` (but nesting is not supported).
+
+[source,cql]
+----
+-- This is a comment
+// This is a comment too
+/* This is
+   a multi-line comment */
+----
+
+== Statements
+
+CQL consists of statements that can be divided in the following
+categories:
+
+* `data-definition` statements, to define and change how the data is
+stored (keyspaces and tables).
+* `data-manipulation` statements, for selecting, inserting and deleting
+data.
+* `secondary-indexes` statements.
+* `materialized-views` statements.
+* `cql-roles` statements.
+* `cql-permissions` statements.
+* `User-Defined Functions (UDFs)` statements.
+* `udts` statements.
+* `cql-triggers` statements.
+
+All the statements are listed below and are described in the rest of
+this documentation (see links above):
+
+[source, bnf]
+----
+include::example$BNF/cql_statement.bnf[]
+----
+
+== Prepared Statements
+
+CQL supports _prepared statements_. Prepared statements are an
+optimization that allows to parse a query only once but execute it
+multiple times with different concrete values.
+
+Any statement that uses at least one bind marker (see `bind_marker`)
+will need to be _prepared_. After which the statement can be _executed_
+by provided concrete values for each of its marker. The exact details of
+how a statement is prepared and then executed depends on the CQL driver
+used and you should refer to your driver documentation.
diff --git a/doc/modules/cassandra/pages/cql/dml.adoc b/doc/modules/cassandra/pages/cql/dml.adoc
new file mode 100644
index 0000000..d0517aa
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/dml.adoc
@@ -0,0 +1,461 @@
+= Data Manipulation
+
+This section describes the statements supported by CQL to insert,
+update, delete and query data.
+
+[[select-statement]]
+== SELECT
+
+Querying data from data is done using a `SELECT` statement:
+
+[source,bnf]
+----
+include::example$BNF/select_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/select_statement.cql[]
+----
+
+The `SELECT` statements reads one or more columns for one or more rows
+in a table. It returns a result-set of the rows matching the request,
+where each row contains the values for the selection corresponding to
+the query. Additionally, xref:cql/functions.adoc#cql-functions[functions] including
+xref:cql/functions.adoc#aggregate-functions[aggregations] can be applied to the result.
+
+A `SELECT` statement contains at least a xref:cql/dml.adoc#selection-clause[selection clause] and the name of the table on which
+the selection is executed. 
+CQL does *not* execute joins or sub-queries and a select statement only apply to a single table. 
+A select statement can also have a xref:cql/dml.adoc#where-clause[where clause] that can further narrow the query results.
+Additional clauses can xref:cql/dml.adoc#ordering-clause[order] or xref:cql/dml.adoc#limit-clause[limit] the results. 
+Lastly, xref:cql/dml.adoc#allow-filtering[queries that require full cluster filtering] can append `ALLOW FILTERING` to any query.
+
+[[selection-clause]]
+=== Selection clause
+
+The `select_clause` determines which columns will be queried and returned in the result set. 
+This clause can also apply transformations to apply to the result before returning. 
+The selection clause consists of a comma-separated list of specific _selectors_ or, alternatively, the wildcard character (`*`) to select all the columns defined in the table.
+
+==== Selectors
+
+A `selector` can be one of:
+
+* A column name of the table selected, to retrieve the values for that
+column.
+* A term, which is usually used nested inside other selectors like
+functions (if a term is selected directly, then the corresponding column
+of the result-set will simply have the value of this term for every row
+returned).
+* A casting, which allows to convert a nested selector to a (compatible)
+type.
+* A function call, where the arguments are selector themselves. See the
+section on xref:cql/functions.adoc#cql-functions[functions] for more details.
+* The special call `COUNT(*)` to the xref:cql/functions.adoc#count-function[COUNT function],
+which counts all non-null results.
+
+==== Aliases
+
+Every _top-level_ selector can also be aliased (using AS).
+If so, the name of the corresponding column in the result set will be
+that of the alias. For instance:
+
+[source,cql]
+----
+include::example$CQL/as.cql[]
+----
+
+[NOTE]
+====
+Currently, aliases aren't recognized in the `WHERE` or `ORDER BY` clauses in the statement.
+You must use the orignal column name instead.
+====
+
+[[writetime-and-ttl-function]]
+==== `WRITETIME` and `TTL` function
+
+Selection supports two special functions that aren't allowed anywhere
+else: `WRITETIME` and `TTL`. 
+Both functions take only one argument, a column name.
+These functions retrieve meta-information that is stored internally for each column:
+
+* `WRITETIME` stores the timestamp of the value of the column
+* `TTL` stores the remaining time to live (in seconds) for the value of the column if it is set to expire; otherwise the value is `null`.
+
+The `WRITETIME` and `TTL` functions can't be used on multi-cell columns such as non-frozen
+collections or non-frozen user-defined types.
+
+[[where-clause]]
+=== The `WHERE` clause
+
+The `WHERE` clause specifies which rows are queried. It specifies
+a relationship for `PRIMARY KEY` columns or a column that has
+a xref:cql/indexes.adoc#create-index-statement[secondary index] defined, along with a set value.
+
+Not all relationships are allowed in a query. For instance, only an equality
+is allowed on a partition key. The `IN` clause is considered an equality for one or more values.
+The `TOKEN` clause can be used to query for partition key non-equalities.
+A partition key must be specified before clustering columns in the `WHERE` clause. The relationship 
+for clustering columns must specify a *contiguous* set of rows to order.
+
+For instance, given:
+
+[source,cql]
+----
+include::example$CQL/table_for_where.cql[]
+----
+
+The following query is allowed:
+
+[source,cql]
+----
+include::example$CQL/where.cql[]
+----
+
+But the following one is not, as it does not select a contiguous set of
+rows (and we suppose no secondary indexes are set):
+
+[source,cql]
+----
+include::example$CQL/where_fail.cql[]
+----
+
+When specifying relationships, the `TOKEN` function can be applied to the `PARTITION KEY` column to query. 
+Rows will be selected based on the token of the `PARTITION_KEY` rather than on the value.
+[IMPORTANT]
+====
+The token of a key depends on the partitioner in use, and that
+in particular the `RandomPartitioner` won't yield a meaningful order. 
+Also note that ordering partitioners always order token values by bytes (so
+even if the partition key is of type int, `token(-1) > token(0)` in
+particular). 
+====
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/token.cql[]
+----
+
+The `IN` relationship is only allowed on the last column of the
+partition key or on the last column of the full primary key.
+
+It is also possible to “group” `CLUSTERING COLUMNS` together in a
+relation using the tuple notation. 
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/where_group_cluster_columns.cql[]
+----
+
+This query will return all rows that sort after the one having “John's Blog” as
+`blog_tile` and '2012-01-01' for `posted_at` in the clustering order. In
+particular, rows having a `post_at <= '2012-01-01'` will be returned, as
+long as their `blog_title > 'John''s Blog'`. 
+
+That would not be the case for this example:
+
+[source,cql]
+----
+include::example$CQL/where_no_group_cluster_columns.cql[]
+----
+
+The tuple notation may also be used for `IN` clauses on clustering columns:
+
+[source,cql]
+----
+include::example$CQL/where_in_tuple.cql[]
+----
+
+The `CONTAINS` operator may only be used for collection columns (lists,
+sets, and maps). In the case of maps, `CONTAINS` applies to the map
+values. The `CONTAINS KEY` operator may only be used on map columns and
+applies to the map keys.
+
+[[group-by-clause]]
+=== Grouping results
+
+The `GROUP BY` option can condense all selected
+rows that share the same values for a set of columns into a single row.
+
+Using the `GROUP BY` option, rows can be grouped at the partition key or clustering column level. 
+Consequently, the `GROUP BY` option only accepts primary key columns in defined order as arguments.
+If a primary key column is restricted by an equality restriction, it is not included in the `GROUP BY` clause.
+
+Aggregate functions will produce a separate value for each group. 
+If no `GROUP BY` clause is specified, aggregates functions will produce a single value for all the rows.
+
+If a column is selected without an aggregate function, in a statement
+with a `GROUP BY`, the first value encounter in each group will be
+returned.
+
+[[ordering-clause]]
+=== Ordering results
+
+The `ORDER BY` clause selects the order of the returned results. 
+The argument is a list of column names and each column's order 
+(`ASC` for ascendant and `DESC` for descendant,
+The possible orderings are limited by the xref:cql/ddl.adoc#clustering-order[clustering order] defined on the table:
+
+* if the table has been defined without any specific `CLUSTERING ORDER`, then the order is as defined by the clustering columns
+or the reverse
+* otherwise, the order is defined by the `CLUSTERING ORDER` option and the reversed one.
+
+[[limit-clause]]
+=== Limiting results
+
+The `LIMIT` option to a `SELECT` statement limits the number of rows
+returned by a query. The `PER PARTITION LIMIT` option limits the
+number of rows returned for a given partition by the query. Both types of limits can used in the same statement.
+
+[[allow-filtering]]
+=== Allowing filtering
+
+By default, CQL only allows select queries that don't involve a full scan of all partitions. 
+If all partitions are scanned, then returning the results may experience a significant latency proportional to the 
+amount of data in the table. The `ALLOW FILTERING` option explicitly executes a full scan. Thus, the performance of 
+the query can be unpredictable.
+
+For example, consider the following table of user profiles with birth year and country of residence. 
+The birth year has a secondary index defined.
+
+[source,cql]
+----
+include::example$CQL/allow_filtering.cql[]
+----
+
+The following queries are valid:
+
+[source,cql]
+----
+include::example$CQL/query_allow_filtering.cql[]
+----
+
+In both cases, the query performance is proportional to the amount of data returned. 
+The first query returns all rows, because all users are selected.
+The second query returns only the rows defined by the secondary index, a per-node implementation; the results will
+depend on the number of nodes in the cluster, and is indirectly proportional to the amount of data stored.
+The number of nodes will always be multiple number of magnitude lower than the number of user profiles stored. 
+Both queries may return very large result sets, but the addition of a `LIMIT` clause can reduced the latency.
+
+The following query will be rejected:
+
+[source,cql]
+----
+include::example$CQL/query_fail_allow_filtering.cql[]
+----
+
+Cassandra cannot guarantee that large amounts of data won't have to scanned amount of data, even if the result is small. 
+If you know that the dataset is small, and the performance will be reasonable, add `ALLOW FILTERING` to allow the query to 
+execute:
+
+[source,cql]
+----
+include::example$CQL/query_nofail_allow_filtering.cql[]
+----
+
+[[insert-statement]]
+== INSERT
+
+Inserting data for a row is done using an `INSERT` statement:
+
+[source,bnf]
+----
+include::example$BNF/insert_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/insert_statement.cql[]
+----
+
+The `INSERT` statement writes one or more columns for a given row in a
+table. 
+Since a row is identified by its `PRIMARY KEY`, at least one columns must be specified. 
+The list of columns to insert must be supplied with the `VALUES` syntax. 
+When using the `JSON` syntax, `VALUES` are optional. 
+See the section on xref:cql/dml.adoc#cql-json[JSON support] for more detail.
+All updates for an `INSERT` are applied atomically and in isolation.
+
+Unlike in SQL, `INSERT` does not check the prior existence of the row by default. 
+The row is created if none existed before, and updated otherwise. 
+Furthermore, there is no means of knowing which action occurred.
+
+The `IF NOT EXISTS` condition can restrict the insertion if the row does not exist. 
+However, note that using `IF NOT EXISTS` will incur a non-negligible performance cost, because Paxos is used,
+so this should be used sparingly.
+
+Please refer to the xref:cql/dml.adoc#update-parameters[UPDATE] section for informations on the `update_parameter`.
+Also note that `INSERT` does not support counters, while `UPDATE` does.
+
+[[update-statement]]
+== UPDATE
+
+Updating a row is done using an `UPDATE` statement:
+
+[source, bnf]
+----
+include::example$BNF/update_statement.bnf[]
+----
+
+For instance:
+
+[source,cql]
+----
+include::example$CQL/update_statement.cql[]
+----
+
+The `UPDATE` statement writes one or more columns for a given row in a
+table. 
+The `WHERE`clause is used to select the row to update and must include all columns of the `PRIMARY KEY`. 
+Non-primary key columns are set using the `SET` keyword.
+In an `UPDATE` statement, all updates within the same partition key are applied atomically and in isolation.
+
+Unlike in SQL, `UPDATE` does not check the prior existence of the row by default.
+The row is created if none existed before, and updated otherwise.
+Furthermore, there is no means of knowing which action occurred.
+
+The `IF` condition can be used to choose whether the row is updated or not if a particular condition is met.
+However, like the `IF NOT EXISTS` condition, a non-negligible performance cost can be incurred.
+
+Regarding the `SET` assignment:
+
+* `c = c + 3` will increment/decrement counters, the only operation allowed. 
+The column name after the '=' sign *must* be the same than the one before the '=' sign.
+Increment/decrement is only allowed on counters. 
+See the section on xref:cql/dml.adoc#counters[counters] for details.
+* `id = id + <some-collection>` and `id[value1] = value2` are for collections. 
+See the xref:cql/types.adoc#collections[collections] for details.  
+* `id.field = 3` is for setting the value of a field on a non-frozen user-defined types. 
+See the xref:cql/types.adoc#udts[UDTs] for details.
+
+=== Update parameters
+
+`UPDATE` and `INSERT` statements support the following parameters:
+
+* `TTL`: specifies an optional Time To Live (in seconds) for the
+inserted values. If set, the inserted values are automatically removed
+from the database after the specified time. Note that the TTL concerns
+the inserted values, not the columns themselves. This means that any
+subsequent update of the column will also reset the TTL (to whatever TTL
+is specified in that update). By default, values never expire. A TTL of
+0 is equivalent to no TTL. If the table has a default_time_to_live, a
+TTL of 0 will remove the TTL for the inserted or updated values. A TTL
+of `null` is equivalent to inserting with a TTL of 0.
+
+`UPDATE`, `INSERT`, `DELETE` and `BATCH` statements support the following parameters:
+
+* `TIMESTAMP`: sets the timestamp for the operation. If not specified,
+the coordinator will use the current time (in microseconds) at the start
+of statement execution as the timestamp. This is usually a suitable
+default.
+
+[[delete_statement]]
+== DELETE
+
+Deleting rows or parts of rows uses the `DELETE` statement:
+
+[source,bnf]
+----
+include::example$BNF/delete_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/delete_statement.cql[]
+----
+
+The `DELETE` statement deletes columns and rows. If column names are
+provided directly after the `DELETE` keyword, only those columns are
+deleted from the row indicated by the `WHERE` clause. Otherwise, whole
+rows are removed.
+
+The `WHERE` clause specifies which rows are to be deleted. Multiple rows
+may be deleted with one statement by using an `IN` operator. A range of
+rows may be deleted using an inequality operator (such as `>=`).
+
+`DELETE` supports the `TIMESTAMP` option with the same semantics as in
+xref:cql/dml.adoc#update-parameters[updates].
+
+In a `DELETE` statement, all deletions within the same partition key are
+applied atomically and in isolation.
+
+A `DELETE` operation can be conditional through the use of an `IF`
+clause, similar to `UPDATE` and `INSERT` statements. However, as with
+`INSERT` and `UPDATE` statements, this will incur a non-negligible
+performance cost because Paxos is used, and should be used sparingly.
+
+[[batch_statement]]
+== BATCH
+
+Multiple `INSERT`, `UPDATE` and `DELETE` can be executed in a single
+statement by grouping them through a `BATCH` statement:
+
+[source, bnf]
+----
+include::example$BNF/batch_statement.bnf[]
+----
+
+For instance:
+
+[source,cql]
+----
+include::example$CQL/batch_statement.cql[]
+----
+
+The `BATCH` statement group multiple modification statements
+(insertions/updates and deletions) into a single statement. It serves
+several purposes:
+
+* It saves network round-trips between the client and the server (and
+sometimes between the server coordinator and the replicas) when batching
+multiple updates.
+* All updates in a `BATCH` belonging to a given partition key are
+performed in isolation.
+* By default, all operations in the batch are performed as _logged_, to
+ensure all mutations eventually complete (or none will). See the notes
+on xref:cql/dml.adoc#unlogged-batches[UNLOGGED batches] for more details.
+
+Note that:
+
+* `BATCH` statements may only contain `UPDATE`, `INSERT` and `DELETE`
+statements (not other batches for instance).
+* Batches are _not_ a full analogue for SQL transactions.
+* If a timestamp is not specified for each operation, then all
+operations will be applied with the same timestamp (either one generated
+automatically, or the timestamp provided at the batch level). Due to
+Cassandra's conflict resolution procedure in the case of
+http://wiki.apache.org/cassandra/FAQ#clocktie[timestamp ties],
+operations may be applied in an order that is different from the order
+they are listed in the `BATCH` statement. To force a particular
+operation ordering, you must specify per-operation timestamps.
+* A LOGGED batch to a single partition will be converted to an UNLOGGED
+batch as an optimization.
+
+[[unlogged-batches]]
+=== `UNLOGGED` batches
+
+By default, Cassandra uses a batch log to ensure all operations in a
+batch eventually complete or none will (note however that operations are
+only isolated within a single partition).
+
+There is a performance penalty for batch atomicity when a batch spans
+multiple partitions. If you do not want to incur this penalty, you can
+tell Cassandra to skip the batchlog with the `UNLOGGED` option. If the
+`UNLOGGED` option is used, a failed batch might leave the patch only
+partly applied.
+
+=== `COUNTER` batches
+
+Use the `COUNTER` option for batched counter updates. Unlike other
+updates in Cassandra, counter updates are not idempotent.
diff --git a/doc/modules/cassandra/pages/cql/functions.adoc b/doc/modules/cassandra/pages/cql/functions.adoc
new file mode 100644
index 0000000..157f46a
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/functions.adoc
@@ -0,0 +1,504 @@
+// Need some intro for UDF and native functions in general and point those to it.  
+// [[cql-functions]][[native-functions]] 
+
+== Functions
+
+CQL supports 2 main categories of functions:
+
+* xref:cql/functions.adoc#scalar-functions[scalar functions] that take a number of values and produce an output
+* xref:cql/functions.adoc#aggregate-functions[aggregate functions] that aggregate multiple rows resulting from a `SELECT` statement
+
+In both cases, CQL provides a number of native "hard-coded" functions as
+well as the ability to create new user-defined functions.
+
+[NOTE]
+.Note
+====
+By default, the use of user-defined functions is disabled by default for
+security concerns (even when enabled, the execution of user-defined
+functions is sandboxed and a "rogue" function should not be allowed to
+do evil, but no sandbox is perfect so using user-defined functions is
+opt-in). See the `enable_user_defined_functions` in `cassandra.yaml` to
+enable them.
+====
+
+A function is identifier by its name:
+
+[source, bnf]
+----
+include::example$BNF/function.bnf[]
+----
+
+=== Scalar functions
+
+[[scalar-native-functions]]
+==== Native functions
+
+===== Cast
+
+The `cast` function can be used to converts one native datatype to
+another.
+
+The following table describes the conversions supported by the `cast`
+function. Cassandra will silently ignore any cast converting a datatype
+into its own datatype.
+
+[cols=",",options="header",]
+|===
+|From |To
+
+| `ascii` | `text`, `varchar`
+
+| `bigint` | `tinyint`, `smallint`, `int`, `float`, `double`, `decimal`, `varint`,
+`text`, `varchar`
+
+| `boolean` | `text`, `varchar`
+
+| `counter` | `tinyint`, `smallint`, `int`, `bigint`, `float`, `double`, `decimal`,
+`varint`, `text`, `varchar`
+
+| `date` | `timestamp`
+
+| `decimal` | `tinyint`, `smallint`, `int`, `bigint`, `float`, `double`, `varint`,
+`text`, `varchar`
+
+| `double` | `tinyint`, `smallint`, `int`, `bigint`, `float`, `decimal`, `varint`,
+`text`, `varchar`
+
+| `float` | `tinyint`, `smallint`, `int`, `bigint`, `double`, `decimal`, `varint`,
+`text`, `varchar`
+
+| `inet` | `text`, `varchar`
+
+| `int` | `tinyint`, `smallint`, `bigint`, `float`, `double`, `decimal`, `varint`,
+`text`, `varchar`
+
+| `smallint` | `tinyint`, `int`, `bigint`, `float`, `double`, `decimal`, `varint`,
+`text`, `varchar`
+
+| `time` | `text`, `varchar`
+
+| `timestamp` | `date`, `text`, `varchar`
+
+| `timeuuid` | `timestamp`, `date`, `text`, `varchar`
+
+| `tinyint` | `tinyint`, `smallint`, `int`, `bigint`, `float`, `double`, `decimal`,
+`varint`, `text`, `varchar`
+
+| `uuid` | `text`, `varchar`
+
+| `varint` | `tinyint`, `smallint`, `int`, `bigint`, `float`, `double`, `decimal`,
+`text`, `varchar`
+|===
+
+The conversions rely strictly on Java's semantics. For example, the
+double value 1 will be converted to the text value '1.0'. For instance:
+
+[source,cql]
+----
+SELECT avg(cast(count as double)) FROM myTable
+----
+
+===== Token
+
+The `token` function computes the token for a given partition key. 
+The exact signature of the token function depends on the table concerned and the partitioner used by the cluster.
+
+The type of the arguments of the `token` depend on the partition key column type. The returned type depends on the defined partitioner:
+
+[cols=",",options="header",]
+|===
+|Partitioner | Returned type
+| Murmur3Partitioner | `bigint`
+| RandomPartitioner | `varint`
+| ByteOrderedPartitioner | `blob`
+|===
+
+For example, consider the following table:
+
+[source,cql]
+----
+include::example$CQL/create_table_simple.cql[]
+----
+
+The table uses the default Murmur3Partitioner.
+The `token` function uses the single argument `text`, because the partition key is `userid` of text type.
+The returned type will be `bigint`.
+
+===== Uuid
+
+The `uuid` function takes no parameters and generates a random type 4
+uuid suitable for use in `INSERT` or `UPDATE` statements.
+
+===== Timeuuid functions
+
+====== `now`
+
+The `now` function takes no arguments and generates, on the coordinator
+node, a new unique timeuuid at the time the function is invoked. Note
+that this method is useful for insertion but is largely non-sensical in
+`WHERE` clauses. 
+
+For example, a query of the form:
+
+[source,cql]
+----
+include::example$CQL/timeuuid_now.cql[]
+----
+
+will not return a result, by design, since the value returned by
+`now()` is guaranteed to be unique.
+
+`currentTimeUUID` is an alias of `now`.
+
+====== `minTimeuuid` and `maxTimeuuid`
+
+The `minTimeuuid` function takes a `timestamp` value `t`, either a timestamp or a date string.
+It returns a _fake_ `timeuuid` corresponding to the _smallest_ possible `timeuuid` for timestamp `t`. 
+The `maxTimeuuid` works similarly, but returns the _largest_ possible `timeuuid`.
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/timeuuid_min_max.cql[]
+----
+
+will select all rows where the `timeuuid` column `t` is later than `'2013-01-01 00:05+0000'` and earlier than `'2013-02-02 10:00+0000'`. 
+The clause `t >= maxTimeuuid('2013-01-01 00:05+0000')` would still _not_ select a `timeuuid` generated exactly at '2013-01-01 00:05+0000', and is essentially equivalent to `t > maxTimeuuid('2013-01-01 00:05+0000')`.
+
+[NOTE]
+.Note
+====
+The values generated by `minTimeuuid` and `maxTimeuuid` are called _fake_ UUID because they do no respect the time-based UUID generation process
+specified by the http://www.ietf.org/rfc/rfc4122.txt[IETF RFC 4122]. 
+In particular, the value returned by these two methods will not be unique.
+Thus, only use these methods for *querying*, not for *insertion*, to prevent possible data overwriting. 
+====
+
+===== Datetime functions
+
+====== Retrieving the current date/time
+
+The following functions can be used to retrieve the date/time at the
+time where the function is invoked:
+
+[cols=",",options="header",]
+|===
+|Function name |Output type
+
+| `currentTimestamp` | `timestamp`
+
+| `currentDate` | `date`
+
+| `currentTime` | `time`
+
+| `currentTimeUUID` | `timeUUID`
+|===
+
+For example the last two days of data can be retrieved using:
+
+[source,cql]
+----
+include::example$CQL/currentdate.cql[]
+----
+
+====== Time conversion functions
+
+A number of functions are provided to convert a `timeuuid`, a `timestamp` or a `date` into another `native` type.
+
+[cols=",,",options="header",]
+|===
+|Function name |Input type |Description
+
+| `toDate` | `timeuuid` | Converts the `timeuuid` argument into a `date` type
+
+| `toDate` | `timestamp` | Converts the `timestamp` argument into a `date` type
+
+| `toTimestamp` | `timeuuid` | Converts the `timeuuid` argument into a `timestamp` type
+
+| `toTimestamp` | `date` | Converts the `date` argument into a `timestamp` type
+
+| `toUnixTimestamp` | `timeuuid` | Converts the `timeuuid` argument into a `bigInt` raw value
+
+| `toUnixTimestamp` | `timestamp` | Converts the `timestamp` argument into a `bigInt` raw value
+
+| `toUnixTimestamp` | `date` | Converts the `date` argument into a `bigInt` raw value
+
+| `dateOf` | `timeuuid` | Similar to `toTimestamp(timeuuid)` (DEPRECATED)
+
+| `unixTimestampOf` | `timeuuid` | Similar to `toUnixTimestamp(timeuuid)` (DEPRECATED)
+|===
+
+===== Blob conversion functions
+
+A number of functions are provided to convert the native types into
+binary data, or a `blob`. 
+For every xref:cql/types.adoc#native-types[type] supported by CQL, the function `typeAsBlob` takes a argument of type `type` and returns it as a `blob`.
+Conversely, the function `blobAsType` takes a 64-bit `blob` argument and converts it to a `bigint` value. 
+For example, `bigintAsBlob(3)` returns `0x0000000000000003` and `blobAsBigint(0x0000000000000003)` returns `3`.
+
+[[user-defined-scalar-functions]]
+==== User-defined functions
+
+User-defined functions (UDFs) execute user-provided code in Cassandra. 
+By default, Cassandra supports defining functions in _Java_ and _JavaScript_. 
+Support for other JSR 223 compliant scripting languages, such as Python, Ruby, and Scala, is possible by adding a JAR to the classpath.
+
+UDFs are part of the Cassandra schema, and are automatically propagated to all nodes in the cluster.  
+UDFs can be _overloaded_, so that multiple UDFs with different argument types can have the same function name. 
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/function_overload.cql[]
+----
+
+UDFs are susceptible to all of the normal problems with the chosen programming language. 
+Accordingly, implementations should be safe against null pointer exceptions, illegal arguments, or any other potential source of exceptions. 
+An exception during function execution will result in the entire statement failing.
+Valid queries for UDF use are `SELECT`, `INSERT` and `UPDATE` statements.
+
+_Complex_ types like collections, tuple types and user-defined types are valid argument and return types in UDFs. 
+Tuple types and user-defined types use the DataStax Java Driver conversion functions.
+Please see the Java Driver documentation for details on handling tuple types and user-defined types.
+
+Arguments for functions can be literals or terms. 
+Prepared statement placeholders can be used, too.
+
+Note the use the double dollar-sign syntax to enclose the UDF source code. 
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/function_dollarsign.cql[]
+----
+
+The implicitly available `udfContext` field (or binding for script UDFs) provides the necessary functionality to create new UDT and tuple values:
+
+[source,cql]
+----
+include::example$CQL/function_udfcontext.cql[]
+----
+
+The definition of the `UDFContext` interface can be found in the Apache Cassandra source code for `org.apache.cassandra.cql3.functions.UDFContext`.
+
+[source,java]
+----
+include::example$JAVA/udfcontext.java[]
+----
+
+Java UDFs already have some imports for common interfaces and classes defined. These imports are:
+
+[source,java]
+----
+include::example$JAVA/udf_imports.java[]
+----
+
+Please note, that these convenience imports are not available for script UDFs.
+
+[[create-function-statement]]
+==== CREATE FUNCTION statement
+
+Creating a new user-defined function uses the `CREATE FUNCTION` statement:
+
+[source,bnf]
+----
+include::example$BNF/create_function_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/create_function.cql[]
+----
+
+`CREATE FUNCTION` with the optional `OR REPLACE` keywords creates either a function or replaces an existing one with the same signature. 
+A `CREATE FUNCTION` without `OR REPLACE` fails if a function with the same signature already exists.  
+If the optional `IF NOT EXISTS` keywords are used, the function will only be created only if another function with the same signature does not
+exist.
+`OR REPLACE` and `IF NOT EXISTS` cannot be used together.
+
+Behavior for `null` input values must be defined for each function:
+
+* `RETURNS NULL ON NULL INPUT` declares that the function will always return `null` if any of the input arguments is `null`.
+* `CALLED ON NULL INPUT` declares that the function will always be executed.
+
+===== Function Signature
+
+Signatures are used to distinguish individual functions. The signature consists of a fully-qualified function name of the <keyspace>.<function_name> and a concatenated list of all the argument types.
+
+Note that keyspace names, function names and argument types are subject to the default naming conventions and case-sensitivity rules.
+
+Functions belong to a keyspace; if no keyspace is specified, the current keyspace is used. 
+User-defined functions are not allowed in the system keyspaces.
+
+[[drop-function-statement]]
+==== DROP FUNCTION statement
+
+Dropping a function uses the `DROP FUNCTION` statement:
+
+[source, bnf]
+----
+include::example$BNF/drop_function_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/drop_function.cql[]
+----
+
+You must specify the argument types of the function, the arguments_signature, in the drop command if there are multiple overloaded functions with the same name but different signatures.  
+`DROP FUNCTION` with the optional `IF EXISTS` keywords drops a function if it exists, but does not throw an error if it doesn't.
+
+[[aggregate-functions]]
+=== Aggregate functions
+
+Aggregate functions work on a set of rows. 
+Values for each row are input, to return a single value for the set of rows aggregated.
+
+If `normal` columns, `scalar functions`, `UDT` fields, `writetime`, or `ttl` are selected together with aggregate functions, the values
+returned for them will be the ones of the first row matching the query.
+
+==== Native aggregates
+
+[[count-function]]
+===== Count
+
+The `count` function can be used to count the rows returned by a query.
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/count.cql[]
+----
+
+It also can count the non-null values of a given column:
+
+[source,cql]
+----
+include::example$CQL/count_nonnull.cql[]
+----
+
+===== Max and Min
+
+The `max` and `min` functions compute the maximum and the minimum value returned by a query for a given column. 
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/min_max.cql[]
+----
+
+===== Sum
+
+The `sum` function sums up all the values returned by a query for a given column. 
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/sum.cql[]
+----
+
+===== Avg
+
+The `avg` function computes the average of all the values returned by a query for a given column. 
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/avg.cql[]
+----
+
+[[user-defined-aggregates-functions]]
+==== User-Defined Aggregates (UDAs)
+
+User-defined aggregates allow the creation of custom aggregate functions. 
+User-defined aggregates can be used in `SELECT` statement.
+
+Each aggregate requires an _initial state_ of type `STYPE` defined with the `INITCOND`value (default value: `null`). 
+The first argument of the state function must have type `STYPE`. 
+The remaining arguments of the state function must match the types of the user-defined aggregate arguments. 
+The state function is called once for each row, and the value returned by the state function becomes the new state. 
+After all rows are processed, the optional `FINALFUNC` is executed with last state value as its argument.
+
+The `STYPE` value is mandatory in order to distinguish possibly overloaded versions of the state and/or final function, since the
+overload can appear after creation of the aggregate.
+
+
+A complete working example for user-defined aggregates (assuming that a
+keyspace has been selected using the `USE` statement):
+
+[source,cql]
+----
+include::example$CQL/uda.cql[]
+----
+
+[[create-aggregate-statement]]
+==== CREATE AGGREGATE statement
+
+Creating (or replacing) a user-defined aggregate function uses the
+`CREATE AGGREGATE` statement:
+
+[source, bnf]
+----
+include::example$BNF/create_aggregate_statement.bnf[]
+----
+
+See above for a complete example.
+
+The `CREATE AGGREGATE` command with the optional `OR REPLACE` keywords creates either an aggregate or replaces an existing one with the same
+signature. 
+A `CREATE AGGREGATE` without `OR REPLACE` fails if an aggregate with the same signature already exists.
+The `CREATE AGGREGATE` command with the optional `IF NOT EXISTS` keywords creates an aggregate if it does not already exist.
+The `OR REPLACE` and `IF NOT EXISTS` phrases cannot be used together.
+
+The `STYPE` value defines the type of the state value and must be specified.
+The optional `INITCOND` defines the initial state value for the aggregate; the default value is `null`. 
+A non-null `INITCOND` must be specified for state functions that are declared with `RETURNS NULL ON NULL INPUT`.
+
+The `SFUNC` value references an existing function to use as the state-modifying function. 
+The first argument of the state function must have type `STYPE`.
+The remaining arguments of the state function must match the types of the user-defined aggregate arguments.
+The state function is called once for each row, and the value returned by the state function becomes the new state.
+State is not updated for state functions declared with `RETURNS NULL ON NULL INPUT` and called with `null`.
+After all rows are processed, the optional `FINALFUNC` is executed with last state value as its argument.
+It must take only one argument with type `STYPE`, but the return type of the `FINALFUNC` may be a different type. 
+A final function declared with `RETURNS NULL ON NULL INPUT` means that the aggregate's return value will be `null`, if the last state is `null`.
+
+If no `FINALFUNC` is defined, the overall return type of the aggregate function is `STYPE`. 
+If a `FINALFUNC` is defined, it is the return type of that function.
+
+[[drop-aggregate-statement]]
+==== DROP AGGREGATE statement
+
+Dropping an user-defined aggregate function uses the `DROP AGGREGATE`
+statement:
+
+[source, bnf]
+----
+include::example$BNF/drop_aggregate_statement.bnf[]
+----
+
+For instance:
+
+[source,cql]
+----
+include::example$CQL/drop_aggregate.cql[]
+----
+
+The `DROP AGGREGATE` statement removes an aggregate created using `CREATE AGGREGATE`. 
+You must specify the argument types of the aggregate to drop if there are multiple overloaded aggregates with the same name but a
+different signature.
+
+The `DROP AGGREGATE` command with the optional `IF EXISTS` keywords drops an aggregate if it exists, and does nothing if a function with the
+signature does not exist.
diff --git a/doc/modules/cassandra/pages/cql/index.adoc b/doc/modules/cassandra/pages/cql/index.adoc
new file mode 100644
index 0000000..4b43be3
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/index.adoc
@@ -0,0 +1,24 @@
+= The Cassandra Query Language (CQL)
+
+This document describes the Cassandra Query Language
+(CQL) version 3.
+Note that this document describes the last version of the language.
+However, the xref:cql/changes.adoc[changes] section provides the differences between the versions of CQL since version 3.0.
+
+CQL offers a model similar to SQL.
+The data is stored in *tables* containing *rows* of *columns*.
+For that reason, when used in this document, these terms (tables, rows and columns) have the same definition that they have in SQL.
+
+* xref:cql/definitions.adoc[Definitions]
+* xref:cql/types.adoc[Data types]
+* xref:cql/ddl.adoc[Data definition language]
+* xref:cql/dml.adoc[Data manipulation language]
+* xref:cql/operators.adoc[Operators]
+* xref:cql/indexes.adoc[Secondary indexes]
+* xref:cql/mvs.adoc[Materialized views]
+* xref:cql/functions.adoc[Functions]
+* xref:cql/json.adoc[JSON]
+* xref:cql/security.adoc[CQL security]
+* xref:cql/triggers.adoc[Triggers]
+* xref:cql/appendices.adoc[Appendices]
+* xref:cql/changes.adoc[Changes]
diff --git a/doc/modules/cassandra/pages/cql/indexes.adoc b/doc/modules/cassandra/pages/cql/indexes.adoc
new file mode 100644
index 0000000..32b45b5
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/indexes.adoc
@@ -0,0 +1,63 @@
+= Secondary Indexes
+
+CQL supports creating secondary indexes on tables, allowing queries on
+the table to use those indexes. A secondary index is identified by a
+name defined by:
+
+[source,bnf]
+----
+include::example$BNF/index_name.bnf[]
+----
+
+[[create-index-statement]]
+== CREATE INDEX
+
+Creating a secondary index on a table uses the `CREATE INDEX` statement:
+
+[source,bnf]
+----
+include::example$BNF/create_index_statement.bnf[]
+----
+
+For instance:
+
+[source,cql]
+----
+include::example$CQL/create_index.cql[]
+----
+
+The `CREATE INDEX` statement is used to create a new (automatic)
+secondary index for a given (existing) column in a given table. A name
+for the index itself can be specified before the `ON` keyword, if
+desired. If data already exists for the column, it will be indexed
+asynchronously. After the index is created, new data for the column is
+indexed automatically at insertion time.
+
+Attempting to create an already existing index will return an error
+unless the `IF NOT EXISTS` option is used. If it is used, the statement
+will be a no-op if the index already exists.
+
+=== Indexes on Map Keys
+
+When creating an index on a `maps <maps>`, you may index either the keys
+or the values. If the column identifier is placed within the `keys()`
+function, the index will be on the map keys, allowing you to use
+`CONTAINS KEY` in `WHERE` clauses. Otherwise, the index will be on the
+map values.
+
+[[drop-index-statement]]
+== DROP INDEX
+
+Dropping a secondary index uses the `DROP INDEX` statement:
+
+[source,bnf]
+----
+include::example$BNF/drop_index_statement.bnf[]
+----
+
+The `DROP INDEX` statement is used to drop an existing secondary index.
+The argument of the statement is the index name, which may optionally
+specify the keyspace of the index.
+
+If the index does not exists, the statement will return an error, unless
+`IF EXISTS` is used in which case the operation is a no-op.
diff --git a/doc/modules/cassandra/pages/cql/json.adoc b/doc/modules/cassandra/pages/cql/json.adoc
new file mode 100644
index 0000000..7d0aa26
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/json.adoc
@@ -0,0 +1,125 @@
+= JSON Support
+
+Cassandra 2.2 introduces JSON support to `SELECT <select-statement>` and
+`INSERT <insert-statement>` statements. 
+This support does not fundamentally alter the CQL API (for example, the schema is still
+enforced).
+It simply provides a convenient way to work with JSON documents.
+
+== SELECT JSON
+
+With `SELECT` statements, the `JSON` keyword is used to return each row as a single `JSON` encoded map. 
+The remainder of the `SELECT` statement behavior is the same.
+
+The result map keys match the column names in a normal result set. 
+For example, a statement like `SELECT JSON a, ttl(b) FROM ...` would result in a map with keys `"a"` and `"ttl(b)"`. 
+However, there is one notable exception: for symmetry with `INSERT JSON` behavior, case-sensitive column names with upper-case letters will be surrounded with double quotes. 
+For example, `SELECT JSON myColumn FROM ...` would result in a map key `"\"myColumn\""` with escaped quotes).
+
+The map values will JSON-encoded representations (as described below) of the result set values.
+
+== INSERT JSON
+
+With `INSERT` statements, the new `JSON` keyword can be used to enable
+inserting a `JSON` encoded map as a single row. The format of the `JSON`
+map should generally match that returned by a `SELECT JSON` statement on
+the same table. In particular, case-sensitive column names should be
+surrounded with double quotes. For example, to insert into a table with
+two columns named "myKey" and "value", you would do the following:
+
+[source,cql]
+----
+include::example$CQL/insert_json.cql[]
+----
+
+By default (or if `DEFAULT NULL` is explicitly used), a column omitted
+from the `JSON` map will be set to `NULL`, meaning that any pre-existing
+value for that column will be removed (resulting in a tombstone being
+created). Alternatively, if the `DEFAULT UNSET` directive is used after
+the value, omitted column values will be left unset, meaning that
+pre-existing values for those column will be preserved.
+
+== JSON Encoding of Cassandra Data Types
+
+Where possible, Cassandra will represent and accept data types in their
+native `JSON` representation. Cassandra will also accept string
+representations matching the CQL literal format for all single-field
+types. For example, floats, ints, UUIDs, and dates can be represented by
+CQL literal strings. However, compound types, such as collections,
+tuples, and user-defined types must be represented by native `JSON`
+collections (maps and lists) or a JSON-encoded string representation of
+the collection.
+
+The following table describes the encodings that Cassandra will accept
+in `INSERT JSON` values (and `fromJson()` arguments) as well as the
+format Cassandra will use when returning data for `SELECT JSON`
+statements (and `fromJson()`):
+
+[cols=",,,",options="header",]
+|===
+|Type |Formats accepted |Return format |Notes
+
+| `ascii` | string | string | Uses JSON's `\u` character escape
+
+| `bigint` | integer, string | integer | String must be valid 64 bit integer
+
+| `blob` | string | string | String should be 0x followed by an even number of hex digits
+
+| `boolean` | boolean, string | boolean | String must be "true" or "false"
+
+| `date` | string | string | Date in format `YYYY-MM-DD`, timezone UTC
+
+| `decimal` | integer, float, string | float | May exceed 32 or 64-bit IEEE-754 floating point precision in client-side decoder
+
+| `double` | integer, float, string | float | String must be valid integer or float
+
+| `float` | integer, float, string | float | String must be valid integer or float
+
+| `inet` | string | string | IPv4 or IPv6 address
+
+| `int` | integer, string | integer | String must be valid 32 bit integer
+
+| `list` | list, string | list | Uses JSON's native list representation
+
+| `map` | map, string | map | Uses JSON's native map representation
+
+| `smallint` | integer, string | integer | String must be valid 16 bit integer
+
+| `set` | list, string | list | Uses JSON's native list representation
+
+| `text` | string | string | Uses JSON's `\u` character escape
+
+| `time` | string | string | Time of day in format `HH-MM-SS[.fffffffff]`
+
+| `timestamp` | integer, string | string | A timestamp. Strings constant allows to input `timestamps
+as dates <timestamps>`. Datestamps with format `YYYY-MM-DD HH:MM:SS.SSS`
+are returned.
+
+| `timeuuid` | string | string | Type 1 UUID. See `constant` for the UUID format
+
+| `tinyint` | integer, string | integer | String must be valid 8 bit integer
+
+| `tuple` | list, string | list | Uses JSON's native list representation
+
+| `UDT` | map, string | map | Uses JSON's native map representation with field names as keys
+
+| `uuid` | string | string | See `constant` for the UUID format
+
+| `varchar` | string | string | Uses JSON's `\u` character escape
+
+| `varint` | integer, string | integer | Variable length; may overflow 32 or 64 bit integers in client-side decoder
+|===
+
+== The fromJson() Function
+
+The `fromJson()` function may be used similarly to `INSERT JSON`, but
+for a single column value. It may only be used in the `VALUES` clause of
+an `INSERT` statement or as one of the column values in an `UPDATE`,
+`DELETE`, or `SELECT` statement. For example, it cannot be used in the
+selection clause of a `SELECT` statement.
+
+== The toJson() Function
+
+The `toJson()` function may be used similarly to `SELECT JSON`, but for
+a single column value. It may only be used in the selection clause of a
+`SELECT` statement.
diff --git a/doc/modules/cassandra/pages/cql/mvs.adoc b/doc/modules/cassandra/pages/cql/mvs.adoc
new file mode 100644
index 0000000..6da0fa4
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/mvs.adoc
@@ -0,0 +1,158 @@
+= Materialized Views
+
+Materialized views names are defined by:
+
+[source,bnf]
+----
+include::example$BNF/view_name.bnf[]
+----
+
+[[create-materialized-view-statement]]
+== CREATE MATERIALIZED VIEW
+
+You can create a materialized view on a table using a
+`CREATE MATERIALIZED VIEW` statement:
+
+[source,bnf]
+----
+include::example$BNF/create_mv_statement.bnf[]
+----
+
+For instance:
+
+[source,cql]
+----
+include::example$CQL/create_mv_statement.cql[]
+----
+
+The `CREATE MATERIALIZED VIEW` statement creates a new materialized
+view. Each such view is a set of _rows_ which corresponds to rows which
+are present in the underlying, or base, table specified in the `SELECT`
+statement. A materialized view cannot be directly updated, but updates
+to the base table will cause corresponding updates in the view.
+
+Creating a materialized view has 3 main parts:
+
+* The xref:cql/mvs.adoc#mv-select[select statement] that restrict the data included in
+the view.
+* The xref:cql/mvs.adoc#mv-primary-key[primary key] definition for the view.
+* The xref:cql/mvs.adoc#mv-options[options] for the view.
+
+Attempting to create an already existing materialized view will return
+an error unless the `IF NOT EXISTS` option is used. If it is used, the
+statement will be a no-op if the materialized view already exists.
+
+[NOTE]
+.Note
+====
+By default, materialized views are built in a single thread. The initial
+build can be parallelized by increasing the number of threads specified
+by the property `concurrent_materialized_view_builders` in
+`cassandra.yaml`. This property can also be manipulated at runtime
+through both JMX and the `setconcurrentviewbuilders` and
+`getconcurrentviewbuilders` nodetool commands.
+====
+
+[[mv-select]]
+=== MV select statement
+
+The select statement of a materialized view creation defines which of
+the base table is included in the view. That statement is limited in a
+number of ways:
+
+* the xref:cql/mvs.adoc#selection-clause[selection] is limited to those that only
+select columns of the base table. In other words, you can't use any
+function (aggregate or not), casting, term, etc. Aliases are also not
+supported. 
+You can however use * as a shortcut of selecting all columns. 
+Further, xref:cql/types.adoc#static-columns[static columns] cannot be included in a materialized view.
+Thus, a `SELECT *` command isn't allowed if the base table has static columns.
+The `WHERE` clause has the following restrictions:
+
+** cannot include any `bind_marker`
+** cannot have columns that are not part of the _base table_ primary key that are not restricted by an `IS NOT NULL` restriction 
+** no other restriction is allowed
+** cannot have columns that are part of the _view_ primary key be null, they must always be at least restricted by a `IS NOT NULL`
+restriction (or any other restriction, but they must have one).
+* cannot have an xref:cql/dml.adoc#ordering-clause[ordering clause], a xref:cql/dml.adoc#limit-clause[limit], or xref:cql/dml.adoc#allow-filtering[ALLOW FILTERING
+
+=== MV primary key
+
+A view must have a primary key and that primary key must conform to the
+following restrictions:
+
+* it must contain all the primary key columns of the base table. This
+ensures that every row of the view correspond to exactly one row of the
+base table.
+* it can only contain a single column that is not a primary key column
+in the base table.
+
+So for instance, give the following base table definition:
+
+[source,cql]
+----
+include::example$CQL/mv_table_def.cql[]
+----
+
+then the following view definitions are allowed:
+
+[source,cql]
+----
+include::example$CQL/mv_table_from_base.cql[]
+----
+
+but the following ones are *not* allowed:
+
+[source,cql]
+----
+include::example$CQL/mv_table_error.cql[]
+----
+
+=== MV options
+
+A materialized view is internally implemented by a table and as such,
+creating a MV allows the `same options than
+creating a table <create-table-options>`.
+
+[[alter-materialized-view-statement]]
+== ALTER MATERIALIZED VIEW
+
+After creation, you can alter the options of a materialized view using
+the `ALTER MATERIALIZED VIEW` statement:
+
+[source,bnf]
+----
+include::example$BNF/alter_mv_statement.bnf[]
+----
+
+The options that can be updated are the same than at creation time and
+thus the `same than for tables
+<create-table-options>`.
+
+[[drop-materialized-view-statement]]
+== DROP MATERIALIZED VIEW
+
+Dropping a materialized view using the `DROP MATERIALIZED VIEW`
+statement:
+
+[source, bnf]
+----
+include::example$BNF/drop_mv_statement.bnf[]
+----
+
+If the materialized view does not exists, the statement will return an
+error, unless `IF EXISTS` is used in which case the operation is a
+no-op.
+
+=== MV Limitations
+
+[NOTE]
+.Note
+====
+Removal of columns not selected in the Materialized View (via
+`UPDATE base SET unselected_column = null` or
+`DELETE unselected_column FROM base`) may shadow missed updates to other
+columns received by hints or repair. For this reason, we advise against
+doing deletions on base columns not selected in views until this is
+fixed on CASSANDRA-13826.
+====
diff --git a/doc/modules/cassandra/pages/cql/operators.adoc b/doc/modules/cassandra/pages/cql/operators.adoc
new file mode 100644
index 0000000..9d858f9
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/operators.adoc
@@ -0,0 +1,68 @@
+= Arithmetic Operators
+
+CQL supports the following operators:
+
+[cols=",",options="header",]
+|===
+|Operator |Description
+
+| - (unary) | Negates operand
+
+| + | Addition
+
+| - | Substraction
+
+| * | Multiplication
+
+| / | Division
+
+| % | Returns the remainder of a division
+|===
+
+== Number Arithmetic
+
+All arithmetic operations are supported on numeric types or counters.
+
+The return type of the operation will be based on the operand types:
+
+[cols=",,,,,,,,,",options="header",]
+|===
+|left/right |tinyint |smallint |int |bigint |counter |float |double |varint |decimal
+
+| *tinyint* | tinyint | smallint | int | bigint | bigint | float | double | varint | decimal
+
+| *smallint* | smallint | smallint | int | bigint | bigint | float | double | varint | decimal
+
+| *int* | int | int | int | bigint | bigint | float | double | varint | decimal
+
+| *bigint* | bigint | bigint | bigint | bigint | bigint | double | double | varint | decimal
+
+| *counter* | bigint | bigint | bigint | bigint | bigint | double | double | varint | decimal
+
+| *float* | float | float | float | double | double | float | double | decimal | decimal
+
+| *double* | double | double | double | double | double | double | double | decimal | decimal
+
+| *varint* | varint | varint | varint | decimal | decimal | decimal | decimal | decimal | decimal
+
+| *decimal* | decimal | decimal | decimal | decimal | decimal | decimal | decimal | decimal | decimal
+|===
+
+`*`, `/` and `%` operators have a higher precedence level than `+` and
+`-` operator. By consequence, they will be evaluated before. If two
+operator in an expression have the same precedence level, they will be
+evaluated left to right based on their position in the expression.
+
+[[datetime--arithmetic]]
+== Datetime Arithmetic
+
+A `duration` can be added (+) or substracted (-) from a `timestamp` or a
+`date` to create a new `timestamp` or `date`. So for instance:
+
+[source,cql]
+----
+include::example$CQL/datetime_arithmetic.cql[]
+----
+
+will select all the records with a value of `t` which is in the last 2
+days of 2016.
diff --git a/doc/modules/cassandra/pages/cql/security.adoc b/doc/modules/cassandra/pages/cql/security.adoc
new file mode 100644
index 0000000..7ea0620
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/security.adoc
@@ -0,0 +1,611 @@
+role_name ::= identifier | string= Security
+
+[[cql-roles]]
+== Database Roles
+
+CQL uses database roles to represent users and group of users.
+Syntactically, a role is defined by:
+
+[source, bnf]
+----
+include::example$BNF/role_name.bnf[]
+----
+
+
+[[create-role-statement]]
+=== CREATE ROLE
+
+Creating a role uses the `CREATE ROLE` statement:
+
+[source, bnf]
+----
+include::example$BNF/create_role_statement.bnf[]
+----
+
+For instance:
+
+[source,cql]
+----
+include::example$CQL/create_role.cql[]
+----
+
+By default roles do not possess `LOGIN` privileges or `SUPERUSER`
+status.
+
+xref:cql/security.adoc#cql-permissions[Permissions] on database resources are granted to
+roles; types of resources include keyspaces, tables, functions and roles
+themselves. Roles may be granted to other roles to create hierarchical
+permissions structures; in these hierarchies, permissions and
+`SUPERUSER` status are inherited, but the `LOGIN` privilege is not.
+
+If a role has the `LOGIN` privilege, clients may identify as that role
+when connecting. For the duration of that connection, the client will
+acquire any roles and privileges granted to that role.
+
+Only a client with with the `CREATE` permission on the database roles
+resource may issue `CREATE ROLE` requests (see the
+xref:cql/security.adoc#cql-permissions[relevant section]), unless the client is a
+`SUPERUSER`. Role management in Cassandra is pluggable and custom
+implementations may support only a subset of the listed options.
+
+Role names should be quoted if they contain non-alphanumeric characters.
+
+==== Setting credentials for internal authentication
+
+Use the `WITH PASSWORD` clause to set a password for internal
+authentication, enclosing the password in single quotation marks.
+
+If internal authentication has not been set up or the role does not have
+`LOGIN` privileges, the `WITH PASSWORD` clause is not necessary.
+
+==== Restricting connections to specific datacenters
+
+If a `network_authorizer` has been configured, you can restrict login
+roles to specific datacenters with the `ACCESS TO DATACENTERS` clause
+followed by a set literal of datacenters the user can access. Not
+specifiying datacenters implicitly grants access to all datacenters. The
+clause `ACCESS TO ALL DATACENTERS` can be used for explicitness, but
+there's no functional difference.
+
+==== Creating a role conditionally
+
+Attempting to create an existing role results in an invalid query
+condition unless the `IF NOT EXISTS` option is used. If the option is
+used and the role exists, the statement is a no-op:
+
+[source,cql]
+----
+include::example$CQL/create_role_ifnotexists.cql[]
+----
+
+[[alter-role-statement]]
+=== ALTER ROLE
+
+Altering a role options uses the `ALTER ROLE` statement:
+
+[source, bnf]
+----
+include::example$BNF/alter_role_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/alter_role.cql[]
+----
+
+==== Restricting connections to specific datacenters
+
+If a `network_authorizer` has been configured, you can restrict login
+roles to specific datacenters with the `ACCESS TO DATACENTERS` clause
+followed by a set literal of datacenters the user can access. To remove
+any data center restrictions, use the `ACCESS TO ALL DATACENTERS`
+clause.
+
+Conditions on executing `ALTER ROLE` statements:
+
+* a client must have `SUPERUSER` status to alter the `SUPERUSER` status
+of another role
+* a client cannot alter the `SUPERUSER` status of any role it currently
+holds
+* a client can only modify certain properties of the role with which it
+identified at login (e.g. `PASSWORD`)
+* to modify properties of a role, the client must be granted `ALTER`
+`permission <cql-permissions>` on that role
+
+[[drop-role-statement]]
+=== DROP ROLE
+
+Dropping a role uses the `DROP ROLE` statement:
+
+[source, bnf]
+----
+include::example$BNF/drop_role_statement.bnf[]
+----
+
+`DROP ROLE` requires the client to have `DROP`
+`permission <cql-permissions>` on the role in question. In addition,
+client may not `DROP` the role with which it identified at login.
+Finally, only a client with `SUPERUSER` status may `DROP` another
+`SUPERUSER` role.
+
+Attempting to drop a role which does not exist results in an invalid
+query condition unless the `IF EXISTS` option is used. If the option is
+used and the role does not exist the statement is a no-op.
+
+[NOTE]
+.Note
+====
+DROP ROLE intentionally does not terminate any open user sessions.
+Currently connected sessions will remain connected and will retain the
+ability to perform any database actions which do not require
+xref:cql/security.adoc#authorization[authorization]. 
+However, if authorization is enabled, xref:cql/security.adoc#cql-permissions[permissions] of the dropped role are also revoked,
+subject to the xref:cql/security.adoc#auth-caching[caching options] configured in xref:cql/configuring.adoc#cassandra.yaml[cassandra-yaml] file. 
+Should a dropped role be subsequently recreated and have new xref:security.adoc#grant-permission-statement[permissions] or
+xref:security.adoc#grant-role-statement[roles]` granted to it, any client sessions still
+connected will acquire the newly granted permissions and roles.
+====
+
+[[grant-role-statement]]
+=== GRANT ROLE
+
+Granting a role to another uses the `GRANT ROLE` statement:
+
+[source, bnf]
+----
+include::example$BNF/grant_role_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/grant_role.cql[]
+----
+
+This statement grants the `report_writer` role to `alice`. Any
+permissions granted to `report_writer` are also acquired by `alice`.
+
+Roles are modelled as a directed acyclic graph, so circular grants are
+not permitted. The following examples result in error conditions:
+
+[source,cql]
+----
+include::example$CQL/role_error.cql[]
+----
+
+[[revoke-role-statement]]
+=== REVOKE ROLE
+
+Revoking a role uses the `REVOKE ROLE` statement:
+
+[source, bnf]
+----
+include::example$BNF/revoke_role_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/revoke_role.cql[]
+----
+
+This statement revokes the `report_writer` role from `alice`. Any
+permissions that `alice` has acquired via the `report_writer` role are
+also revoked.
+
+[[list-roles-statement]]
+=== LIST ROLES
+
+All the known roles (in the system or granted to specific role) can be
+listed using the `LIST ROLES` statement:
+
+[source, bnf]
+----
+include::example$BNF/list_roles_statement.bnf[]
+----
+
+For instance:
+
+[source,cql]
+----
+include::example$CQL/list_roles.cql[]
+----
+
+returns all known roles in the system, this requires `DESCRIBE`
+permission on the database roles resource. 
+
+This example enumerates all roles granted to `alice`, including those transitively
+acquired:
+
+[source,cql]
+----
+include::example$CQL/list_roles_of.cql[]
+----
+
+This example lists all roles directly granted to `bob` without including any of the
+transitively acquired ones:
+
+[source,cql]
+----
+include::example$CQL/list_roles_nonrecursive.cql[]
+----
+
+== Users
+
+Prior to the introduction of roles in Cassandra 2.2, authentication and
+authorization were based around the concept of a `USER`. For backward
+compatibility, the legacy syntax has been preserved with `USER` centric
+statements becoming synonyms for the `ROLE` based equivalents. In other
+words, creating/updating a user is just a different syntax for
+creating/updating a role.
+
+[[create-user-statement]]
+=== CREATE USER
+
+Creating a user uses the `CREATE USER` statement:
+
+[source, bnf]
+----
+include::example$BNF/create_user_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/create_user.cql[]
+----
+
+The `CREATE USER` command is equivalent to `CREATE ROLE` where the `LOGIN` option is `true`. 
+So, the following pairs of statements are equivalent:
+
+[source,cql]
+----
+include::example$CQL/create_user_role.cql[]
+----
+
+[[alter-user-statement]]
+=== ALTER USER
+
+Altering the options of a user uses the `ALTER USER` statement:
+
+[source, bnf]
+----
+include::example$BNF/alter_user_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/alter_user.cql[]
+----
+
+[[drop-user-statement]]
+=== DROP USER
+
+Dropping a user uses the `DROP USER` statement:
+
+[source, bnf]
+----
+include::example$BNF/drop_user_statement.bnf[]
+----
+
+[[list-users-statement]]
+=== LIST USERS
+
+Existing users can be listed using the `LIST USERS` statement:
+
+[source, bnf]
+----
+include::example$BNF/list_users_statement.bnf[]
+----
+
+Note that this statement is equivalent to xref:security.adoc#list-roles-statement[`LIST ROLES], but only roles with the `LOGIN` privilege are included in the output.
+
+== Data Control
+
+[[cql-permissions]]
+=== Permissions
+
+Permissions on resources are granted to roles; there are several
+different types of resources in Cassandra and each type is modelled
+hierarchically:
+
+* The hierarchy of Data resources, Keyspaces and Tables has the
+structure `ALL KEYSPACES` -> `KEYSPACE` -> `TABLE`.
+* Function resources have the structure `ALL FUNCTIONS` -> `KEYSPACE` ->
+`FUNCTION`
+* Resources representing roles have the structure `ALL ROLES` -> `ROLE`
+* Resources representing JMX ObjectNames, which map to sets of
+MBeans/MXBeans, have the structure `ALL MBEANS` -> `MBEAN`
+
+Permissions can be granted at any level of these hierarchies and they
+flow downwards. So granting a permission on a resource higher up the
+chain automatically grants that same permission on all resources lower
+down. For example, granting `SELECT` on a `KEYSPACE` automatically
+grants it on all `TABLES` in that `KEYSPACE`. Likewise, granting a
+permission on `ALL FUNCTIONS` grants it on every defined function,
+regardless of which keyspace it is scoped in. It is also possible to
+grant permissions on all functions scoped to a particular keyspace.
+
+Modifications to permissions are visible to existing client sessions;
+that is, connections need not be re-established following permissions
+changes.
+
+The full set of available permissions is:
+
+* `CREATE`
+* `ALTER`
+* `DROP`
+* `SELECT`
+* `MODIFY`
+* `AUTHORIZE`
+* `DESCRIBE`
+* `EXECUTE`
+
+Not all permissions are applicable to every type of resource. For
+instance, `EXECUTE` is only relevant in the context of functions or
+mbeans; granting `EXECUTE` on a resource representing a table is
+nonsensical. Attempting to `GRANT` a permission on resource to which it
+cannot be applied results in an error response. The following
+illustrates which permissions can be granted on which types of resource,
+and which statements are enabled by that permission.
+
+[cols=",,",options="header",]
+|===
+|Permission |Resource |Operations
+
+| `CREATE` | `ALL KEYSPACES` | `CREATE KEYSPACE` and `CREATE TABLE` in any keyspace
+
+| `CREATE` | `KEYSPACE` | `CREATE TABLE` in specified keyspace
+
+| `CREATE` | `ALL FUNCTIONS` | `CREATE FUNCTION` in any keyspace and `CREATE AGGREGATE` in any keyspace
+
+| `CREATE` | `ALL FUNCTIONS IN KEYSPACE` | `CREATE FUNCTION` and `CREATE AGGREGATE` in specified keyspace
+
+| `CREATE` | `ALL ROLES` | `CREATE ROLE`
+
+| `ALTER` | `ALL KEYSPACES` | `ALTER KEYSPACE` and `ALTER TABLE` in any keyspace
+
+| `ALTER` | `KEYSPACE` | `ALTER KEYSPACE` and `ALTER TABLE` in specified keyspace 
+
+| `ALTER` | `TABLE` | `ALTER TABLE`
+
+| `ALTER` | `ALL FUNCTIONS` | `CREATE FUNCTION` and `CREATE AGGREGATE`: replacing any existing
+
+| `ALTER` | `ALL FUNCTIONS IN KEYSPACE` | `CREATE FUNCTION` and `CREATE AGGREGATE`: replacing existing in specified keyspace
+
+| `ALTER` | `FUNCTION` | `CREATE FUNCTION` and `CREATE AGGREGATE`: replacing existing
+
+| `ALTER` | `ALL ROLES` | `ALTER ROLE` on any role
+
+| `ALTER` | `ROLE` | `ALTER ROLE`
+
+| `DROP` | `ALL KEYSPACES` | `DROP KEYSPACE` and `DROP TABLE` in any keyspace
+
+| `DROP` | `KEYSPACE` | `DROP TABLE` in specified keyspace
+
+| `DROP` | `TABLE` | `DROP TABLE`
+
+| `DROP` | `ALL FUNCTIONS` | `DROP FUNCTION` and `DROP AGGREGATE` in any keyspace
+
+| `DROP` | `ALL FUNCTIONS IN KEYSPACE` | `DROP FUNCTION` and `DROP AGGREGATE` in specified keyspace
+
+| `DROP` | `FUNCTION` | `DROP FUNCTION`
+
+| `DROP` | `ALL ROLES` | `DROP ROLE` on any role
+
+| `DROP` | `ROLE` | `DROP ROLE`
+
+| `SELECT` | `ALL KEYSPACES` | `SELECT` on any table
+
+| `SELECT` | `KEYSPACE` | `SELECT` on any table in specified keyspace
+
+| `SELECT` | `TABLE` | `SELECT` on specified table
+
+| `SELECT` | `ALL MBEANS` | Call getter methods on any mbean
+
+| `SELECT` | `MBEANS` | Call getter methods on any mbean matching a wildcard pattern 
+
+| `SELECT` | `MBEAN` | Call getter methods on named mbean
+
+| `MODIFY` | `ALL KEYSPACES` | `INSERT`, `UPDATE`, `DELETE` and `TRUNCATE` on any table
+
+| `MODIFY` | `KEYSPACE` | `INSERT`, `UPDATE`, `DELETE` and `TRUNCATE` on any table in specified
+keyspace
+
+| `MODIFY` | `TABLE` | `INSERT`, `UPDATE`, `DELETE` and `TRUNCATE` on specified table
+
+| `MODIFY` | `ALL MBEANS` | Call setter methods on any mbean
+
+| `MODIFY` | `MBEANS` | Call setter methods on any mbean matching a wildcard pattern
+
+| `MODIFY` | `MBEAN` | Call setter methods on named mbean
+
+| `AUTHORIZE` | `ALL KEYSPACES` | `GRANT PERMISSION` and `REVOKE PERMISSION` on any table
+
+| `AUTHORIZE` | `KEYSPACE` | `GRANT PERMISSION` and `REVOKE PERMISSION` on any table in specified keyspace
+
+| `AUTHORIZE` | `TABLE` | `GRANT PERMISSION` and `REVOKE PERMISSION` on specified table
+
+| `AUTHORIZE` | `ALL FUNCTIONS` | `GRANT PERMISSION` and `REVOKE PERMISSION` on any function
+
+| `AUTHORIZE` | `ALL FUNCTIONS IN KEYSPACE` | `GRANT PERMISSION` and `REVOKE PERMISSION` in specified keyspace
+
+| `AUTHORIZE` | `FUNCTION` | `GRANT PERMISSION` and `REVOKE PERMISSION` on specified function
+
+| `AUTHORIZE` | `ALL MBEANS` | `GRANT PERMISSION` and `REVOKE PERMISSION` on any mbean
+
+| `AUTHORIZE` | `MBEANS` | `GRANT PERMISSION` and `REVOKE PERMISSION` on any mbean matching a wildcard pattern
+
+| `AUTHORIZE` | `MBEAN` | `GRANT PERMISSION` and `REVOKE PERMISSION` on named mbean
+
+| `AUTHORIZE` | `ALL ROLES` | `GRANT ROLE` and `REVOKE ROLE` on any role
+
+| `AUTHORIZE` | `ROLES` | `GRANT ROLE` and `REVOKE ROLE` on specified roles
+
+| `DESCRIBE` | `ALL ROLES` | `LIST ROLES` on all roles or only roles granted to another, specified role
+
+| `DESCRIBE` | `ALL MBEANS` | Retrieve metadata about any mbean from the platform's MBeanServer
+
+
+| `DESCRIBE` | `MBEANS` | Retrieve metadata about any mbean matching a wildcard patter from the
+platform's MBeanServer
+
+| `DESCRIBE` | `MBEAN` | Retrieve metadata about a named mbean from the platform's MBeanServer
+
+| `EXECUTE` | `ALL FUNCTIONS` | `SELECT`, `INSERT` and `UPDATE` using any function, and use of any
+function in `CREATE AGGREGATE`
+
+| `EXECUTE` | `ALL FUNCTIONS IN KEYSPACE` | `SELECT`, `INSERT` and `UPDATE` using any function in specified keyspace
+and use of any function in keyspace in `CREATE AGGREGATE`
+
+| `EXECUTE` | `FUNCTION` | `SELECT`, `INSERT` and `UPDATE` using specified function and use of the function in `CREATE AGGREGATE`
+
+| `EXECUTE` | `ALL MBEANS` | Execute operations on any mbean
+
+| `EXECUTE` | `MBEANS` | Execute operations on any mbean matching a wildcard pattern
+
+| `EXECUTE` | `MBEAN` | Execute operations on named mbean
+|===
+
+[[grant-permission-statement]]
+=== GRANT PERMISSION
+
+Granting a permission uses the `GRANT PERMISSION` statement:
+
+[source, bnf]
+----
+include::example$BNF/grant_permission_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/grant_perm.cql[]
+----
+
+This example gives any user with the role `data_reader` permission to execute
+`SELECT` statements on any table across all keyspaces:
+
+[source,cql]
+----
+include::example$CQL/grant_modify.cql[]
+----
+
+To give any user with the role `data_writer` permission to perform
+`UPDATE`, `INSERT`, `UPDATE`, `DELETE` and `TRUNCATE` queries on all
+tables in the `keyspace1` keyspace:
+
+[source,cql]
+----
+include::example$CQL/grant_drop.cql[]
+----
+
+To give any user with the `schema_owner` role permissions to `DROP` a specific
+`keyspace1.table1`:
+
+[source,cql]
+----
+include::example$CQL/grant_execute.cql[]
+----
+
+This command grants any user with the `report_writer` role permission to execute
+`SELECT`, `INSERT` and `UPDATE` queries which use the function
+`keyspace1.user_function( int )`:
+
+[source,cql]
+----
+include::example$CQL/grant_describe.cql[]
+----
+
+This grants any user with the `role_admin` role permission to view any
+and all roles in the system with a `LIST ROLES` statement.
+
+==== GRANT ALL
+
+When the `GRANT ALL` form is used, the appropriate set of permissions is
+determined automatically based on the target resource.
+
+==== Automatic Granting
+
+When a resource is created, via a `CREATE KEYSPACE`, `CREATE TABLE`,
+`CREATE FUNCTION`, `CREATE AGGREGATE` or `CREATE ROLE` statement, the
+creator (the role the database user who issues the statement is
+identified as), is automatically granted all applicable permissions on
+the new resource.
+
+[[revoke-permission-statement]]
+=== REVOKE PERMISSION
+
+Revoking a permission from a role uses the `REVOKE PERMISSION`
+statement:
+
+[source, bnf]
+----
+include::example$BNF/revoke_permission_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/revoke_perm.cql[]
+----
+
+Because of their function in normal driver operations, certain tables
+cannot have their `SELECT` permissions revoked. The
+following tables will be available to all authorized users regardless of
+their assigned role:
+
+[source,cql]
+----
+include::example$CQL/no_revoke.cql[]
+----
+
+[[list-permissions-statement]]
+=== LIST PERMISSIONS
+
+Listing granted permissions uses the `LIST PERMISSIONS` statement:
+
+[source, bnf]
+----
+include::example$BNF/list_permissions_statement.bnf[]
+----
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/list_perm.cql[]
+----
+
+Show all permissions granted to `alice`, including those acquired
+transitively from any other roles:
+
+[source,cql]
+----
+include::example$CQL/list_all_perm.cql[]
+----
+
+Show all permissions on `keyspace1.table1` granted to `bob`, including
+those acquired transitively from any other roles. This also includes any
+permissions higher up the resource hierarchy which can be applied to
+`keyspace1.table1`. For example, should `bob` have `ALTER` permission on
+`keyspace1`, that would be included in the results of this query. Adding
+the `NORECURSIVE` switch restricts the results to only those permissions
+which were directly granted to `bob` or one of `bob`'s roles:
+
+[source,cql]
+----
+include::example$CQL/list_select_perm.cql[]
+----
+
+Show any permissions granted to `carlos` or any of `carlos`'s roles,
+limited to `SELECT` permissions on any resource.
diff --git a/doc/modules/cassandra/pages/cql/triggers.adoc b/doc/modules/cassandra/pages/cql/triggers.adoc
new file mode 100644
index 0000000..9ec6757
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/triggers.adoc
@@ -0,0 +1,50 @@
+= Triggers
+
+Triggers are identified with a name defined by:
+
+[source,bnf]
+----
+include::example$BNF/trigger_name.bnf[]
+----
+
+[[create-trigger-statement]]
+== CREATE TRIGGER
+
+Creating a new trigger uses the `CREATE TRIGGER` statement:
+
+[source,bnf]
+----
+include::example$BNF/create_trigger_statement.bnf[]
+----
+
+For instance:
+
+[source,cql]
+----
+include::example$CQL/create_trigger.cql[]
+----
+
+The actual logic that makes up the trigger can be written in any Java
+(JVM) language and exists outside the database. You place the trigger
+code in a `lib/triggers` subdirectory of the Cassandra installation
+directory, it loads during cluster startup, and exists on every node
+that participates in a cluster. The trigger defined on a table fires
+before a requested DML statement occurs, which ensures the atomicity of
+the transaction.
+
+[[drop-trigger-statement]]
+== DROP TRIGGER
+
+Dropping a trigger uses the `DROP TRIGGER` statement:
+
+[source,bnf]
+----
+include::example$BNF/drop_trigger_statement.bnf[]
+----
+
+For instance:
+
+[source,cql]
+----
+include::example$CQL/drop_trigger.cql[]
+----
diff --git a/doc/modules/cassandra/pages/cql/types.adoc b/doc/modules/cassandra/pages/cql/types.adoc
new file mode 100644
index 0000000..0cee1f3
--- /dev/null
+++ b/doc/modules/cassandra/pages/cql/types.adoc
@@ -0,0 +1,539 @@
+= Data Types
+
+CQL is a typed language and supports a rich set of data types, including
+xref:cql/types.adoc#native-types[native types], xref:cql/types.adoc#collections[collection types],
+xref:cql/types.adoc#udts[user-defined types], xref:cql/types.adoc#tuples[tuple types], and xref:cql/types.adoc#custom-types[custom
+types]:
+
+[source, bnf]
+----
+include::example$BNF/cql_type.bnf[]
+----
+
+== Native types
+
+The native types supported by CQL are:
+
+[source, bnf]
+----
+include::example$BNF/native_type.bnf[]
+----
+
+The following table gives additional informations on the native data
+types, and on which kind of xref:cql/definitions.adoc#constants[constants] each type supports:
+
+[cols=",,",options="header",]
+|===
+| Type | Constants supported | Description
+
+| `ascii` | `string` | ASCII character string
+| `bigint` | `integer` | 64-bit signed long
+| `blob` | `blob` | Arbitrary bytes (no validation)
+| `boolean` | `boolean` | Either `true` or `false` 
+| `counter` | `integer` | Counter column (64-bit signed value). See `counters` for details.
+| `date` | `integer`, `string` | A date (with no corresponding time value). See `dates` below for details.
+| `decimal` | `integer`, `float` | Variable-precision decimal
+| `double` | `integer` `float` | 64-bit IEEE-754 floating point
+| `duration` | `duration`, | A duration with nanosecond precision. See `durations` below for details.
+| `float` | `integer`, `float` | 32-bit IEEE-754 floating point
+| `inet` | `string` | An IP address, either IPv4 (4 bytes long) or IPv6 (16 bytes long). Note
+that there is no `inet` constant, IP address should be input as strings.
+| `int` | `integer` | 32-bit signed int
+| `smallint` | `integer` | 16-bit signed int
+| `text` | `string` | UTF8 encoded string
+| `time` | `integer`, `string` | A time (with no corresponding date value) with nanosecond precision. See
+`times` below for details.
+| `timestamp` | `integer`, `string` | A timestamp (date and time) with millisecond precision. See `timestamps`
+below for details.
+| `timeuuid` | `uuid` | Version 1 https://en.wikipedia.org/wiki/Universally_unique_identifier[UUID],
+generally used as a “conflict-free” timestamp. Also see `timeuuid-functions`.
+| `tinyint` | `integer` | 8-bit signed int
+| `uuid` | `uuid` | A https://en.wikipedia.org/wiki/Universally_unique_identifier[UUID] (of any version)
+| `varchar` | `string` | UTF8 encoded string
+| `varint` | `integer` | Arbitrary-precision integer
+|===
+
+=== Counters
+
+The `counter` type is used to define _counter columns_. A counter column
+is a column whose value is a 64-bit signed integer and on which 2
+operations are supported: incrementing and decrementing (see the
+xref:cql/dml.adoc#update-statement[UPDATE] statement for syntax). 
+Note that the value of a counter cannot
+be set: a counter does not exist until first incremented/decremented,
+and that first increment/decrement is made as if the prior value was 0.
+
+[[counter-limitations]]
+Counters have a number of important limitations:
+
+* They cannot be used for columns part of the `PRIMARY KEY` of a table.
+* A table that contains a counter can only contain counters. In other
+words, either all the columns of a table outside the `PRIMARY KEY` have
+the `counter` type, or none of them have it.
+* Counters do not support xref:cql/dml.adoc#writetime-and-ttl-function[expiration].
+* The deletion of counters is supported, but is only guaranteed to work
+the first time you delete a counter. In other words, you should not
+re-update a counter that you have deleted (if you do, proper behavior is
+not guaranteed).
+* Counter updates are, by nature, not
+https://en.wikipedia.org/wiki/Idempotence[idemptotent]. An important
+consequence is that if a counter update fails unexpectedly (timeout or
+loss of connection to the coordinator node), the client has no way to
+know if the update has been applied or not. In particular, replaying the
+update may or may not lead to an over count.
+
+[[timestamps]]
+== Working with timestamps
+
+Values of the `timestamp` type are encoded as 64-bit signed integers
+representing a number of milliseconds since the standard base time known
+as https://en.wikipedia.org/wiki/Unix_time[the epoch]: January 1 1970 at
+00:00:00 GMT.
+
+Timestamps can be input in CQL either using their value as an `integer`,
+or using a `string` that represents an
+https://en.wikipedia.org/wiki/ISO_8601[ISO 8601] date. For instance, all
+of the values below are valid `timestamp` values for Mar 2, 2011, at
+04:05:00 AM, GMT:
+
+* `1299038700000`
+* `'2011-02-03 04:05+0000'`
+* `'2011-02-03 04:05:00+0000'`
+* `'2011-02-03 04:05:00.000+0000'`
+* `'2011-02-03T04:05+0000'`
+* `'2011-02-03T04:05:00+0000'`
+* `'2011-02-03T04:05:00.000+0000'`
+
+The `+0000` above is an RFC 822 4-digit time zone specification; `+0000`
+refers to GMT. US Pacific Standard Time is `-0800`. The time zone may be
+omitted if desired (`'2011-02-03 04:05:00'`), and if so, the date will
+be interpreted as being in the time zone under which the coordinating
+Cassandra node is configured. There are however difficulties inherent in
+relying on the time zone configuration being as expected, so it is
+recommended that the time zone always be specified for timestamps when
+feasible.
+
+The time of day may also be omitted (`'2011-02-03'` or
+`'2011-02-03+0000'`), in which case the time of day will default to
+00:00:00 in the specified or default time zone. However, if only the
+date part is relevant, consider using the xref:cql/types.adoc#dates[date] type.
+
+[[dates]]
+== Date type
+
+Values of the `date` type are encoded as 32-bit unsigned integers
+representing a number of days with “the epoch” at the center of the
+range (2^31). Epoch is January 1st, 1970
+
+For xref:cql/types.adoc#timestamps[timestamps], a date can be input either as an
+`integer` or using a date `string`. In the later case, the format should
+be `yyyy-mm-dd` (so `'2011-02-03'` for instance).
+
+[[times]]
+== Time type
+
+Values of the `time` type are encoded as 64-bit signed integers
+representing the number of nanoseconds since midnight.
+
+For xref:cql/types.adoc#timestamps[timestamps], a time can be input either as an
+`integer` or using a `string` representing the time. In the later case,
+the format should be `hh:mm:ss[.fffffffff]` (where the sub-second
+precision is optional and if provided, can be less than the nanosecond).
+So for instance, the following are valid inputs for a time:
+
+* `'08:12:54'`
+* `'08:12:54.123'`
+* `'08:12:54.123456'`
+* `'08:12:54.123456789'`
+
+[[durations]]
+== Duration type
+
+Values of the `duration` type are encoded as 3 signed integer of
+variable lengths. The first integer represents the number of months, the
+second the number of days and the third the number of nanoseconds. This
+is due to the fact that the number of days in a month can change, and a
+day can have 23 or 25 hours depending on the daylight saving.
+Internally, the number of months and days are decoded as 32 bits
+integers whereas the number of nanoseconds is decoded as a 64 bits
+integer.
+
+A duration can be input as:
+
+* `(quantity unit)+` like `12h30m` where the unit can be:
+** `y`: years (12 months)
+** `mo`: months (1 month)
+** `w`: weeks (7 days)
+** `d`: days (1 day)
+** `h`: hours (3,600,000,000,000 nanoseconds)
+** `m`: minutes (60,000,000,000 nanoseconds)
+** `s`: seconds (1,000,000,000 nanoseconds)
+** `ms`: milliseconds (1,000,000 nanoseconds)
+** `us` or `µs` : microseconds (1000 nanoseconds)
+** `ns`: nanoseconds (1 nanosecond)
+* ISO 8601 format: `P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W`
+* ISO 8601 alternative format: `P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]`
+
+For example:
+
+[source,cql]
+----
+include::example$CQL/insert_duration.cql[]
+----
+
+[[duration-limitation]]
+Duration columns cannot be used in a table's `PRIMARY KEY`. This
+limitation is due to the fact that durations cannot be ordered. It is
+effectively not possible to know if `1mo` is greater than `29d` without
+a date context.
+
+A `1d` duration is not equal to a `24h` one as the duration type has
+been created to be able to support daylight saving.
+
+== Collections
+
+CQL supports three kinds of collections: `maps`, `sets` and `lists`. The
+types of those collections is defined by:
+
+[source,bnf]
+----
+include::example$BNF/collection_type.bnf[]
+----
+
+and their values can be inputd using collection literals:
+
+[source,bnf]
+----
+include::example$BNF/collection_literal.bnf[]
+----
+
+Note however that neither `bind_marker` nor `NULL` are supported inside
+collection literals.
+
+=== Noteworthy characteristics
+
+Collections are meant for storing/denormalizing relatively small amount
+of data. They work well for things like “the phone numbers of a given
+user”, “labels applied to an email”, etc. But when items are expected to
+grow unbounded (“all messages sent by a user”, “events registered by a
+sensor”...), then collections are not appropriate and a specific table
+(with clustering columns) should be used. Concretely, (non-frozen)
+collections have the following noteworthy characteristics and
+limitations:
+
+* Individual collections are not indexed internally. Which means that
+even to access a single element of a collection, the while collection
+has to be read (and reading one is not paged internally).
+* While insertion operations on sets and maps never incur a
+read-before-write internally, some operations on lists do. Further, some
+lists operations are not idempotent by nature (see the section on
+xref:cql/types.adoc#lists[lists] below for details), making their retry in case of
+timeout problematic. It is thus advised to prefer sets over lists when
+possible.
+
+Please note that while some of those limitations may or may not be
+removed/improved upon in the future, it is a anti-pattern to use a
+(single) collection to store large amounts of data.
+
+=== Maps
+
+A `map` is a (sorted) set of key-value pairs, where keys are unique and
+the map is sorted by its keys. You can define and insert a map with:
+
+[source,cql]
+----
+include::example$CQL/map.cql[]
+----
+
+Further, maps support:
+
+* Updating or inserting one or more elements:
++
+[source,cql]
+----
+include::example$CQL/update_map.cql[]
+----
+* Removing one or more element (if an element doesn't exist, removing it
+is a no-op but no error is thrown):
++
+[source,cql]
+----
+include::example$CQL/delete_map.cql[]
+----
++
+Note that for removing multiple elements in a `map`, you remove from it
+a `set` of keys.
+
+Lastly, TTLs are allowed for both `INSERT` and `UPDATE`, but in both
+case the TTL set only apply to the newly inserted/updated elements. In
+other words:
+
+[source,cql]
+----
+include::example$CQL/update_ttl_map.cql[]
+----
+
+will only apply the TTL to the `{ 'color' : 'green' }` record, the rest
+of the map remaining unaffected.
+
+=== Sets
+
+A `set` is a (sorted) collection of unique values. You can define and
+insert a map with:
+
+[source,cql]
+----
+include::example$CQL/set.cql[]
+----
+
+Further, sets support:
+
+* Adding one or multiple elements (as this is a set, inserting an
+already existing element is a no-op):
++
+[source,cql]
+----
+include::example$CQL/update_set.cql[]
+----
+* Removing one or multiple elements (if an element doesn't exist,
+removing it is a no-op but no error is thrown):
++
+[source,cql]
+----
+include::example$CQL/delete_set.cql[]
+----
+
+Lastly, for xref:cql/types.adoc#sets[sets], TTLs are only applied to newly inserted values.
+
+=== Lists
+
+[NOTE]
+.Note
+====
+As mentioned above and further discussed at the end of this section,
+lists have limitations and specific performance considerations that you
+should take into account before using them. In general, if you can use a
+xref:cql/types.adoc#sets[set] instead of list, always prefer a set.
+====
+
+A `list` is a (sorted) collection of non-unique values where
+elements are ordered by there position in the list. You can define and
+insert a list with:
+
+[source,cql]
+----
+include::example$CQL/list.cql[]
+----
+
+Further, lists support:
+
+* Appending and prepending values to a list:
++
+[source,cql]
+----
+include::example$CQL/update_list.cql[]
+----
+
+[WARNING]
+.Warning
+====
+The append and prepend operations are not idempotent by nature. So in
+particular, if one of these operation timeout, then retrying the
+operation is not safe and it may (or may not) lead to
+appending/prepending the value twice.
+====
+
+* Setting the value at a particular position in a list that has a pre-existing element for that position. An error
+will be thrown if the list does not have the position.:
++
+[source,cql]
+----
+include::example$CQL/update_particular_list_element.cql[]
+----
+* Removing an element by its position in the list that has a pre-existing element for that position. An error
+will be thrown if the list does not have the position. Further, as the operation removes an
+element from the list, the list size will decrease by one element, shifting
+the position of all the following elements one forward:
++
+[source,cql]
+----
+include::example$CQL/delete_element_list.cql[]
+----
+
+* Deleting _all_ the occurrences of particular values in the list (if a
+particular element doesn't occur at all in the list, it is simply
+ignored and no error is thrown):
++
+[source,cql]
+----
+include::example$CQL/delete_all_elements_list.cql[]
+----
+
+[WARNING]
+.Warning
+====
+Setting and removing an element by position and removing occurences of
+particular values incur an internal _read-before-write_. These operations will
+run slowly and use more resources than usual updates (with the
+exclusion of conditional write that have their own cost).
+====
+
+Lastly, for xref:cql/types.adoc#lists[lists], TTLs only apply to newly inserted values.
+
+[[udts]]
+== User-Defined Types (UDTs)
+
+CQL support the definition of user-defined types (UDTs). Such a
+type can be created, modified and removed using the
+`create_type_statement`, `alter_type_statement` and
+`drop_type_statement` described below. But once created, a UDT is simply
+referred to by its name:
+
+[source, bnf]
+----
+include::example$BNF/udt.bnf[]
+----
+
+=== Creating a UDT
+
+Creating a new user-defined type is done using a `CREATE TYPE` statement
+defined by:
+
+[source, bnf]
+----
+include::example$BNF/create_type.bnf[]
+----
+
+A UDT has a name (used to declared columns of that type) and is a set of
+named and typed fields. Fields name can be any type, including
+collections or other UDT. For instance:
+
+[source,cql]
+----
+include::example$CQL/udt.cql[]
+----
+
+Things to keep in mind about UDTs:
+
+* Attempting to create an already existing type will result in an error
+unless the `IF NOT EXISTS` option is used. If it is used, the statement
+will be a no-op if the type already exists.
+* A type is intrinsically bound to the keyspace in which it is created,
+and can only be used in that keyspace. At creation, if the type name is
+prefixed by a keyspace name, it is created in that keyspace. Otherwise,
+it is created in the current keyspace.
+* As of Cassandra , UDT have to be frozen in most cases, hence the
+`frozen<address>` in the table definition above. Please see the section
+on xref:cql/types.adoc#frozen[frozen] for more details.
+
+=== UDT literals
+
+Once a used-defined type has been created, value can be input using a
+UDT literal:
+
+[source,bnf]
+----
+include::example$BNF/udt_literal.bnf[]
+----
+
+In other words, a UDT literal is like a xref:cql/types.adoc#maps[map]` literal but its
+keys are the names of the fields of the type. For instance, one could
+insert into the table define in the previous section using:
+
+[source,cql]
+----
+include::example$CQL/insert_udt.cql[]
+----
+
+To be valid, a UDT literal can only include fields defined by the
+type it is a literal of, but it can omit some fields (these will be set to `NULL`).
+
+=== Altering a UDT
+
+An existing user-defined type can be modified using an `ALTER TYPE`
+statement:
+
+[source,bnf]
+----
+include::example$BNF/alter_udt_statement.bnf[]
+----
+
+You can:
+
+* Add a new field to the type (`ALTER TYPE address ADD country text`).
+That new field will be `NULL` for any values of the type created before
+the addition.
+* Rename the fields of the type.
+
+[source,cql]
+----
+include::example$CQL/rename_udt_field.cql[]
+----
+
+=== Dropping a UDT
+
+You can drop an existing user-defined type using a `DROP TYPE`
+statement:
+
+[source,bnf]
+----
+include::example$BNF/drop_udt_statement.bnf[]
+----
+
+Dropping a type results in the immediate, irreversible removal of that
+type. However, attempting to drop a type that is still in use by another
+type, table or function will result in an error.
+
+If the type dropped does not exist, an error will be returned unless
+`IF EXISTS` is used, in which case the operation is a no-op.
+
+== Tuples
+
+CQL also support tuples and tuple types (where the elements can be of
+different types). Functionally, tuples can be though as anonymous UDT
+with anonymous fields. Tuple types and tuple literals are defined by:
+
+[source,bnf]
+----
+include::example$BNF/tuple.bnf[]
+----
+
+and can be created:
+
+[source,cql]
+----
+include::example$CQL/tuple.cql[]
+----
+
+Unlike other composed types, like collections and UDTs, a tuple is always
+`frozen <frozen>` (without the need of the `frozen` keyword)
+and it is not possible to update only some elements of a tuple (without
+updating the whole tuple). Also, a tuple literal should always have the
+same number of value than declared in the type it is a tuple of (some of
+those values can be null but they need to be explicitly declared as so).
+
+== Custom Types
+
+[NOTE]
+.Note
+====
+Custom types exists mostly for backward compatibility purposes and their
+usage is discouraged. Their usage is complex, not user friendly and the
+other provided types, particularly xref:cql/types.adoc#udts[user-defined types], should
+almost always be enough.
+====
+
+A custom type is defined by:
+
+[source,bnf]
+----
+include::example$BNF/custom_type.bnf[]
+----
+
+A custom type is a `string` that contains the name of Java class that
+extends the server side `AbstractType` class and that can be loaded by
+Cassandra (it should thus be in the `CLASSPATH` of every node running
+Cassandra). That class will define what values are valid for the type
+and how the time sorts when used for a clustering column. For any other
+purpose, a value of a custom type is the same than that of a `blob`, and
+can in particular be input using the `blob` literal syntax.
diff --git a/doc/modules/cassandra/pages/data_modeling/data_modeling_conceptual.adoc b/doc/modules/cassandra/pages/data_modeling/data_modeling_conceptual.adoc
new file mode 100644
index 0000000..c1e1027
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/data_modeling_conceptual.adoc
@@ -0,0 +1,44 @@
+= Conceptual Data Modeling
+
+First, let’s create a simple domain model that is easy to understand in
+the relational world, and then see how you might map it from a
+relational to a distributed hashtable model in Cassandra.
+
+Let's use an example that is complex enough to show the various data
+structures and design patterns, but not something that will bog you down
+with details. Also, a domain that’s familiar to everyone will allow you
+to concentrate on how to work with Cassandra, not on what the
+application domain is all about.
+
+For example, let's use a domain that is easily understood and that
+everyone can relate to: making hotel reservations.
+
+The conceptual domain includes hotels, guests that stay in the hotels, a
+collection of rooms for each hotel, the rates and availability of those
+rooms, and a record of reservations booked for guests. Hotels typically
+also maintain a collection of “points of interest,” which are parks,
+museums, shopping galleries, monuments, or other places near the hotel
+that guests might want to visit during their stay. Both hotels and
+points of interest need to maintain geolocation data so that they can be
+found on maps for mashups, and to calculate distances.
+
+The conceptual domain is depicted below using the entity–relationship
+model popularized by Peter Chen. This simple diagram represents the
+entities in the domain with rectangles, and attributes of those entities
+with ovals. Attributes that represent unique identifiers for items are
+underlined. Relationships between entities are represented as diamonds,
+and the connectors between the relationship and each entity show the
+multiplicity of the connection.
+
+image::data_modeling_hotel_erd.png[image]
+
+Obviously, in the real world, there would be many more considerations
+and much more complexity. For example, hotel rates are notoriously
+dynamic, and calculating them involves a wide array of factors. Here
+you’re defining something complex enough to be interesting and touch on
+the important points, but simple enough to maintain the focus on
+learning Cassandra.
+
+_Material adapted from Cassandra, The Definitive Guide. Published by
+O'Reilly Media, Inc. Copyright © 2020 Jeff Carpenter, Eben Hewitt. All
+rights reserved. Used with permission._
diff --git a/doc/modules/cassandra/pages/data_modeling/data_modeling_logical.adoc b/doc/modules/cassandra/pages/data_modeling/data_modeling_logical.adoc
new file mode 100644
index 0000000..bcbfa78
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/data_modeling_logical.adoc
@@ -0,0 +1,195 @@
+= Logical Data Modeling
+
+Now that you have defined your queries, you’re ready to begin designing
+Cassandra tables. First, create a logical model containing a table for
+each query, capturing entities and relationships from the conceptual
+model.
+
+To name each table, you’ll identify the primary entity type for which
+you are querying and use that to start the entity name. If you are
+querying by attributes of other related entities, append those to the
+table name, separated with `_by_`. For example, `hotels_by_poi`.
+
+Next, you identify the primary key for the table, adding partition key
+columns based on the required query attributes, and clustering columns
+in order to guarantee uniqueness and support desired sort ordering.
+
+The design of the primary key is extremely important, as it will
+determine how much data will be stored in each partition and how that
+data is organized on disk, which in turn will affect how quickly
+Cassandra processes reads.
+
+Complete each table by adding any additional attributes identified by
+the query. If any of these additional attributes are the same for every
+instance of the partition key, mark the column as static.
+
+Now that was a pretty quick description of a fairly involved process, so
+it will be worthwhile to work through a detailed example. First, let’s
+introduce a notation that you can use to represent logical models.
+
+Several individuals within the Cassandra community have proposed
+notations for capturing data models in diagrammatic form. This document
+uses a notation popularized by Artem Chebotko which provides a simple,
+informative way to visualize the relationships between queries and
+tables in your designs. This figure shows the Chebotko notation for a
+logical data model.
+
+image::data_modeling_chebotko_logical.png[image]
+
+Each table is shown with its title and a list of columns. Primary key
+columns are identified via symbols such as *K* for partition key columns
+and **C**↑ or **C**↓ to represent clustering columns. Lines are shown
+entering tables or between tables to indicate the queries that each
+table is designed to support.
+
+== Hotel Logical Data Model
+
+The figure below shows a Chebotko logical data model for the queries
+involving hotels, points of interest, rooms, and amenities. One thing
+you'll notice immediately is that the Cassandra design doesn’t include
+dedicated tables for rooms or amenities, as you had in the relational
+design. This is because the workflow didn’t identify any queries
+requiring this direct access.
+
+image::data_modeling_hotel_logical.png[image]
+
+Let’s explore the details of each of these tables.
+
+The first query Q1 is to find hotels near a point of interest, so you’ll
+call this table `hotels_by_poi`. Searching by a named point of interest
+is a clue that the point of interest should be a part of the primary
+key. Let’s reference the point of interest by name, because according to
+the workflow that is how users will start their search.
+
+You’ll note that you certainly could have more than one hotel near a
+given point of interest, so you’ll need another component in the primary
+key in order to make sure you have a unique partition for each hotel. So
+you add the hotel key as a clustering column.
+
+An important consideration in designing your table’s primary key is
+making sure that it defines a unique data element. Otherwise you run the
+risk of accidentally overwriting data.
+
+Now for the second query (Q2), you’ll need a table to get information
+about a specific hotel. One approach would have been to put all of the
+attributes of a hotel in the `hotels_by_poi` table, but you added only
+those attributes that were required by the application workflow.
+
+From the workflow diagram, you know that the `hotels_by_poi` table is
+used to display a list of hotels with basic information on each hotel,
+and the application knows the unique identifiers of the hotels returned.
+When the user selects a hotel to view details, you can then use Q2,
+which is used to obtain details about the hotel. Because you already
+have the `hotel_id` from Q1, you use that as a reference to the hotel
+you’re looking for. Therefore the second table is just called `hotels`.
+
+Another option would have been to store a set of `poi_names` in the
+hotels table. This is an equally valid approach. You’ll learn through
+experience which approach is best for your application.
+
+Q3 is just a reverse of Q1—looking for points of interest near a hotel,
+rather than hotels near a point of interest. This time, however, you
+need to access the details of each point of interest, as represented by
+the `pois_by_hotel` table. As previously, you add the point of interest
+name as a clustering key to guarantee uniqueness.
+
+At this point, let’s now consider how to support query Q4 to help the
+user find available rooms at a selected hotel for the nights they are
+interested in staying. Note that this query involves both a start date
+and an end date. Because you’re querying over a range instead of a
+single date, you know that you’ll need to use the date as a clustering
+key. Use the `hotel_id` as a primary key to group room data for each
+hotel on a single partition, which should help searches be super fast.
+Let’s call this the `available_rooms_by_hotel_date` table.
+
+To support searching over a range, use `clustering columns
+<clustering-columns>` to store attributes that you need to access in a
+range query. Remember that the order of the clustering columns is
+important.
+
+The design of the `available_rooms_by_hotel_date` table is an instance
+of the *wide partition* pattern. This pattern is sometimes called the
+*wide row* pattern when discussing databases that support similar
+models, but wide partition is a more accurate description from a
+Cassandra perspective. The essence of the pattern is to group multiple
+related rows in a partition in order to support fast access to multiple
+rows within the partition in a single query.
+
+In order to round out the shopping portion of the data model, add the
+`amenities_by_room` table to support Q5. This will allow users to view
+the amenities of one of the rooms that is available for the desired stay
+dates.
+
+== Reservation Logical Data Model
+
+Now let's switch gears to look at the reservation queries. The figure
+shows a logical data model for reservations. You’ll notice that these
+tables represent a denormalized design; the same data appears in
+multiple tables, with differing keys.
+
+image::data_modeling_reservation_logical.png[image]
+
+In order to satisfy Q6, the `reservations_by_guest` table can be used to
+look up the reservation by guest name. You could envision query Q7 being
+used on behalf of a guest on a self-serve website or a call center agent
+trying to assist the guest. Because the guest name might not be unique,
+you include the guest ID here as a clustering column as well.
+
+Q8 and Q9 in particular help to remind you to create queries that
+support various stakeholders of the application, not just customers but
+staff as well, and perhaps even the analytics team, suppliers, and so
+on.
+
+The hotel staff might wish to see a record of upcoming reservations by
+date in order to get insight into how the hotel is performing, such as
+what dates the hotel is sold out or undersold. Q8 supports the retrieval
+of reservations for a given hotel by date.
+
+Finally, you create a `guests` table. This provides a single location
+that used to store guest information. In this case, you specify a
+separate unique identifier for guest records, as it is not uncommon for
+guests to have the same name. In many organizations, a customer database
+such as the `guests` table would be part of a separate customer
+management application, which is why other guest access patterns were
+omitted from the example.
+
+== Patterns and Anti-Patterns
+
+As with other types of software design, there are some well-known
+patterns and anti-patterns for data modeling in Cassandra. You’ve
+already used one of the most common patterns in this hotel model—the
+wide partition pattern.
+
+The *time series* pattern is an extension of the wide partition pattern.
+In this pattern, a series of measurements at specific time intervals are
+stored in a wide partition, where the measurement time is used as part
+of the partition key. This pattern is frequently used in domains
+including business analysis, sensor data management, and scientific
+experiments.
+
+The time series pattern is also useful for data other than measurements.
+Consider the example of a banking application. You could store each
+customer’s balance in a row, but that might lead to a lot of read and
+write contention as various customers check their balance or make
+transactions. You’d probably be tempted to wrap a transaction around
+writes just to protect the balance from being updated in error. In
+contrast, a time series–style design would store each transaction as a
+timestamped row and leave the work of calculating the current balance to
+the application.
+
+One design trap that many new users fall into is attempting to use
+Cassandra as a queue. Each item in the queue is stored with a timestamp
+in a wide partition. Items are appended to the end of the queue and read
+from the front, being deleted after they are read. This is a design that
+seems attractive, especially given its apparent similarity to the time
+series pattern. The problem with this approach is that the deleted items
+are now `tombstones <asynch-deletes>` that Cassandra must scan past in
+order to read from the front of the queue. Over time, a growing number
+of tombstones begins to degrade read performance.
+
+The queue anti-pattern serves as a reminder that any design that relies
+on the deletion of data is potentially a poorly performing design.
+
+_Material adapted from Cassandra, The Definitive Guide. Published by
+O'Reilly Media, Inc. Copyright © 2020 Jeff Carpenter, Eben Hewitt. All
+rights reserved. Used with permission._
diff --git a/doc/modules/cassandra/pages/data_modeling/data_modeling_physical.adoc b/doc/modules/cassandra/pages/data_modeling/data_modeling_physical.adoc
new file mode 100644
index 0000000..0934067
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/data_modeling_physical.adoc
@@ -0,0 +1,96 @@
+= Physical Data Modeling
+
+Once you have a logical data model defined, creating the physical model
+is a relatively simple process.
+
+You walk through each of the logical model tables, assigning types to
+each item. You can use any valid `CQL data type <data-types>`, including
+the basic types, collections, and user-defined types. You may identify
+additional user-defined types that can be created to simplify your
+design.
+
+After you’ve assigned data types, you analyze the model by performing
+size calculations and testing out how the model works. You may make some
+adjustments based on your findings. Once again let's cover the data
+modeling process in more detail by working through an example.
+
+Before getting started, let’s look at a few additions to the Chebotko
+notation for physical data models. To draw physical models, you need to
+be able to add the typing information for each column. This figure shows
+the addition of a type for each column in a sample table.
+
+image::data_modeling_chebotko_physical.png[image]
+
+The figure includes a designation of the keyspace containing each table
+and visual cues for columns represented using collections and
+user-defined types. Note the designation of static columns and secondary
+index columns. There is no restriction on assigning these as part of a
+logical model, but they are typically more of a physical data modeling
+concern.
+
+== Hotel Physical Data Model
+
+Now let’s get to work on the physical model. First, you need keyspaces
+to contain the tables. To keep the design relatively simple, create a
+`hotel` keyspace to contain tables for hotel and availability data, and
+a `reservation` keyspace to contain tables for reservation and guest
+data. In a real system, you might divide the tables across even more
+keyspaces in order to separate concerns.
+
+For the `hotels` table, use Cassandra’s `text` type to represent the
+hotel’s `id`. For the address, create an `address` user defined type.
+Use the `text` type to represent the phone number, as there is
+considerable variance in the formatting of numbers between countries.
+
+While it would make sense to use the `uuid` type for attributes such as
+the `hotel_id`, this document uses mostly `text` attributes as
+identifiers, to keep the samples simple and readable. For example, a
+common convention in the hospitality industry is to reference properties
+by short codes like "AZ123" or "NY229". This example uses these values
+for `hotel_ids`, while acknowledging they are not necessarily globally
+unique.
+
+You’ll find that it’s often helpful to use unique IDs to uniquely
+reference elements, and to use these `uuids` as references in tables
+representing other entities. This helps to minimize coupling between
+different entity types. This may prove especially effective if you are
+using a microservice architectural style for your application, in which
+there are separate services responsible for each entity type.
+
+As you work to create physical representations of various tables in the
+logical hotel data model, you use the same approach. The resulting
+design is shown in this figure:
+
+image::data_modeling_hotel_physical.png[image]
+
+Note that the `address` type is also included in the design. It is
+designated with an asterisk to denote that it is a user-defined type,
+and has no primary key columns identified. This type is used in the
+`hotels` and `hotels_by_poi` tables.
+
+User-defined types are frequently used to help reduce duplication of
+non-primary key columns, as was done with the `address` user-defined
+type. This can reduce complexity in the design.
+
+Remember that the scope of a UDT is the keyspace in which it is defined.
+To use `address` in the `reservation` keyspace defined below design,
+you’ll have to declare it again. This is just one of the many trade-offs
+you have to make in data model design.
+
+== Reservation Physical Data Model
+
+Now, let’s examine reservation tables in the design. Remember that the
+logical model contained three denormalized tables to support queries for
+reservations by confirmation number, guest, and hotel and date. For the
+first iteration of your physical data model design, assume you're going
+to manage this denormalization manually. Note that this design could be
+revised to use Cassandra’s (experimental) materialized view feature.
+
+image::data_modeling_reservation_physical.png[image]
+
+Note that the `address` type is reproduced in this keyspace and
+`guest_id` is modeled as a `uuid` type in all of the tables.
+
+_Material adapted from Cassandra, The Definitive Guide. Published by
+O'Reilly Media, Inc. Copyright © 2020 Jeff Carpenter, Eben Hewitt. All
+rights reserved. Used with permission._
diff --git a/doc/modules/cassandra/pages/data_modeling/data_modeling_queries.adoc b/doc/modules/cassandra/pages/data_modeling/data_modeling_queries.adoc
new file mode 100644
index 0000000..ed40fb8
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/data_modeling_queries.adoc
@@ -0,0 +1,60 @@
+= Defining Application Queries
+
+Let’s try the query-first approach to start designing the data model for
+a hotel application. The user interface design for the application is
+often a great artifact to use to begin identifying queries. Let’s assume
+that you’ve talked with the project stakeholders and your UX designers
+have produced user interface designs or wireframes for the key use
+cases. You’ll likely have a list of shopping queries like the following:
+
+* Q1. Find hotels near a given point of interest.
+* Q2. Find information about a given hotel, such as its name and
+location.
+* Q3. Find points of interest near a given hotel.
+* Q4. Find an available room in a given date range.
+* Q5. Find the rate and amenities for a room.
+
+It is often helpful to be able to refer to queries by a shorthand number
+rather that explaining them in full. The queries listed here are
+numbered Q1, Q2, and so on, which is how they are referenced in diagrams
+throughout the example.
+
+Now if the application is to be a success, you’ll certainly want
+customers to be able to book reservations at hotels. This includes steps
+such as selecting an available room and entering their guest
+information. So clearly you will also need some queries that address the
+reservation and guest entities from the conceptual data model. Even
+here, however, you’ll want to think not only from the customer
+perspective in terms of how the data is written, but also in terms of
+how the data will be queried by downstream use cases.
+
+Your natural tendency might be to focus first on designing the tables
+to store reservation and guest records, and only then start thinking
+about the queries that would access them. You may have felt a similar
+tension already when discussing the shopping queries before, thinking
+“but where did the hotel and point of interest data come from?” Don’t
+worry, you will see soon enough. Here are some queries that describe how
+users will access reservations:
+
+* Q6. Lookup a reservation by confirmation number.
+* Q7. Lookup a reservation by hotel, date, and guest name.
+* Q8. Lookup all reservations by guest name.
+* Q9. View guest details.
+
+All of the queries are shown in the context of the workflow of the
+application in the figure below. Each box on the diagram represents a
+step in the application workflow, with arrows indicating the flows
+between steps and the associated query. If you’ve modeled the
+application well, each step of the workflow accomplishes a task that
+“unlocks” subsequent steps. For example, the “View hotels near POI” task
+helps the application learn about several hotels, including their unique
+keys. The key for a selected hotel may be used as part of Q2, in order
+to obtain detailed description of the hotel. The act of booking a room
+creates a reservation record that may be accessed by the guest and hotel
+staff at a later time through various additional queries.
+
+image::data_modeling_hotel_queries.png[image]
+
+_Material adapted from Cassandra, The Definitive Guide. Published by
+O'Reilly Media, Inc. Copyright © 2020 Jeff Carpenter, Eben Hewitt. All
+rights reserved. Used with permission._
diff --git a/doc/modules/cassandra/pages/data_modeling/data_modeling_rdbms.adoc b/doc/modules/cassandra/pages/data_modeling/data_modeling_rdbms.adoc
new file mode 100644
index 0000000..2acd6cc
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/data_modeling_rdbms.adoc
@@ -0,0 +1,144 @@
+= RDBMS Design
+
+When you set out to build a new data-driven application that will use a
+relational database, you might start by modeling the domain as a set of
+properly normalized tables and use foreign keys to reference related
+data in other tables.
+
+The figure below shows how you might represent the data storage for your
+application using a relational database model. The relational model
+includes a couple of “join” tables in order to realize the many-to-many
+relationships from the conceptual model of hotels-to-points of interest,
+rooms-to-amenities, rooms-to-availability, and guests-to-rooms (via a
+reservation).
+
+image::data_modeling_hotel_relational.png[image]
+
+== Design Differences Between RDBMS and Cassandra
+
+Let’s take a minute to highlight some of the key differences in doing
+data modeling for Cassandra versus a relational database.
+
+=== No joins
+
+You cannot perform joins in Cassandra. If you have designed a data model
+and find that you need something like a join, you’ll have to either do
+the work on the client side, or create a denormalized second table that
+represents the join results for you. This latter option is preferred in
+Cassandra data modeling. Performing joins on the client should be a very
+rare case; you really want to duplicate (denormalize) the data instead.
+
+=== No referential integrity
+
+Although Cassandra supports features such as lightweight transactions
+and batches, Cassandra itself has no concept of referential integrity
+across tables. In a relational database, you could specify foreign keys
+in a table to reference the primary key of a record in another table.
+But Cassandra does not enforce this. It is still a common design
+requirement to store IDs related to other entities in your tables, but
+operations such as cascading deletes are not available.
+
+=== Denormalization
+
+In relational database design, you are often taught the importance of
+normalization. This is not an advantage when working with Cassandra
+because it performs best when the data model is denormalized. It is
+often the case that companies end up denormalizing data in relational
+databases as well. There are two common reasons for this. One is
+performance. Companies simply can’t get the performance they need when
+they have to do so many joins on years’ worth of data, so they
+denormalize along the lines of known queries. This ends up working, but
+goes against the grain of how relational databases are intended to be
+designed, and ultimately makes one question whether using a relational
+database is the best approach in these circumstances.
+
+A second reason that relational databases get denormalized on purpose is
+a business document structure that requires retention. That is, you have
+an enclosing table that refers to a lot of external tables whose data
+could change over time, but you need to preserve the enclosing document
+as a snapshot in history. The common example here is with invoices. You
+already have customer and product tables, and you’d think that you could
+just make an invoice that refers to those tables. But this should never
+be done in practice. Customer or price information could change, and
+then you would lose the integrity of the invoice document as it was on
+the invoice date, which could violate audits, reports, or laws, and
+cause other problems.
+
+In the relational world, denormalization violates Codd’s normal forms,
+and you try to avoid it. But in Cassandra, denormalization is, well,
+perfectly normal. It’s not required if your data model is simple. But
+don’t be afraid of it.
+
+Historically, denormalization in Cassandra has required designing and
+managing multiple tables using techniques described in this
+documentation. Beginning with the 3.0 release, Cassandra provides a
+feature known as `materialized views <materialized-views>` which allows
+you to create multiple denormalized views of data based on a base table
+design. Cassandra manages materialized views on the server, including
+the work of keeping the views in sync with the table.
+
+=== Query-first design
+
+Relational modeling, in simple terms, means that you start from the
+conceptual domain and then represent the nouns in the domain in tables.
+You then assign primary keys and foreign keys to model relationships.
+When you have a many-to-many relationship, you create the join tables
+that represent just those keys. The join tables don’t exist in the real
+world, and are a necessary side effect of the way relational models
+work. After you have all your tables laid out, you can start writing
+queries that pull together disparate data using the relationships
+defined by the keys. The queries in the relational world are very much
+secondary. It is assumed that you can always get the data you want as
+long as you have your tables modeled properly. Even if you have to use
+several complex subqueries or join statements, this is usually true.
+
+By contrast, in Cassandra you don’t start with the data model; you start
+with the query model. Instead of modeling the data first and then
+writing queries, with Cassandra you model the queries and let the data
+be organized around them. Think of the most common query paths your
+application will use, and then create the tables that you need to
+support them.
+
+Detractors have suggested that designing the queries first is overly
+constraining on application design, not to mention database modeling.
+But it is perfectly reasonable to expect that you should think hard
+about the queries in your application, just as you would, presumably,
+think hard about your relational domain. You may get it wrong, and then
+you’ll have problems in either world. Or your query needs might change
+over time, and then you’ll have to work to update your data set. But
+this is no different from defining the wrong tables, or needing
+additional tables, in an RDBMS.
+
+=== Designing for optimal storage
+
+In a relational database, it is frequently transparent to the user how
+tables are stored on disk, and it is rare to hear of recommendations
+about data modeling based on how the RDBMS might store tables on disk.
+However, that is an important consideration in Cassandra. Because
+Cassandra tables are each stored in separate files on disk, it’s
+important to keep related columns defined together in the same table.
+
+A key goal that you will see as you begin creating data models in
+Cassandra is to minimize the number of partitions that must be searched
+in order to satisfy a given query. Because the partition is a unit of
+storage that does not get divided across nodes, a query that searches a
+single partition will typically yield the best performance.
+
+=== Sorting is a design decision
+
+In an RDBMS, you can easily change the order in which records are
+returned to you by using `ORDER BY` in your query. The default sort
+order is not configurable; by default, records are returned in the order
+in which they are written. If you want to change the order, you just
+modify your query, and you can sort by any list of columns.
+
+In Cassandra, however, sorting is treated differently; it is a design
+decision. The sort order available on queries is fixed, and is
+determined entirely by the selection of clustering columns you supply in
+the `CREATE TABLE` command. The CQL `SELECT` statement does support
+`ORDER BY` semantics, but only in the order specified by the clustering
+columns.
+
+_Material adapted from Cassandra, The Definitive Guide. Published by
+O'Reilly Media, Inc. Copyright © 2020 Jeff Carpenter, Eben Hewitt. All
+rights reserved. Used with permission._
diff --git a/doc/modules/cassandra/pages/data_modeling/data_modeling_refining.adoc b/doc/modules/cassandra/pages/data_modeling/data_modeling_refining.adoc
new file mode 100644
index 0000000..045a80c
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/data_modeling_refining.adoc
@@ -0,0 +1,202 @@
+= Evaluating and Refining Data Models
+:stem: latexmath
+
+Once you’ve created a physical model, there are some steps you’ll want
+to take to evaluate and refine table designs to help ensure optimal
+performance.
+
+== Calculating Partition Size
+
+The first thing that you want to look for is whether your tables will
+have partitions that will be overly large, or to put it another way, too
+wide. Partition size is measured by the number of cells (values) that
+are stored in the partition. Cassandra’s hard limit is 2 billion cells
+per partition, but you’ll likely run into performance issues before
+reaching that limit.
+
+In order to calculate the size of partitions, use the following formula:
+
+[latexmath]
+++++
+\[N_v = N_r (N_c - N_{pk} - N_s) + N_s\]
+++++
+
+The number of values (or cells) in the partition (N~v~) is equal to the
+number of static columns (N~s~) plus the product of the number of rows
+(N~r~) and the number of of values per row. The number of values per row
+is defined as the number of columns (N~c~) minus the number of primary
+key columns (N~pk~) and static columns (N~s~).
+
+The number of columns tends to be relatively static, although it is
+possible to alter tables at runtime. For this reason, a primary driver
+of partition size is the number of rows in the partition. This is a key
+factor that you must consider in determining whether a partition has the
+potential to get too large. Two billion values sounds like a lot, but in
+a sensor system where tens or hundreds of values are measured every
+millisecond, the number of values starts to add up pretty fast.
+
+Let’s take a look at one of the tables to analyze the partition size.
+Because it has a wide partition design with one partition per hotel,
+look at the `available_rooms_by_hotel_date` table. The table has four
+columns total (N~c~ = 4), including three primary key columns (N~pk~ =
+3) and no static columns (N~s~ = 0). Plugging these values into the
+formula, the result is:
+
+[latexmath]
+++++
+\[N_v = N_r (4 - 3 - 0) + 0 = 1N_r\]
+++++
+
+Therefore the number of values for this table is equal to the number of
+rows. You still need to determine a number of rows. To do this, make
+estimates based on the application design. The table is storing a record
+for each room, in each of hotel, for every night. Let's assume the
+system will be used to store two years of inventory at a time, and there
+are 5,000 hotels in the system, with an average of 100 rooms in each
+hotel.
+
+Since there is a partition for each hotel, the estimated number of rows
+per partition is as follows:
+
+[latexmath]
+++++
+\[N_r = 100 rooms/hotel \times 730 days = 73,000 rows\]
+++++
+
+This relatively small number of rows per partition is not going to get
+you in too much trouble, but if you start storing more dates of
+inventory, or don’t manage the size of the inventory well using TTL, you
+could start having issues. You still might want to look at breaking up
+this large partition, which you'll see how to do shortly.
+
+When performing sizing calculations, it is tempting to assume the
+nominal or average case for variables such as the number of rows.
+Consider calculating the worst case as well, as these sorts of
+predictions have a way of coming true in successful systems.
+
+== Calculating Size on Disk
+
+In addition to calculating the size of a partition, it is also an
+excellent idea to estimate the amount of disk space that will be
+required for each table you plan to store in the cluster. In order to
+determine the size, use the following formula to determine the size S~t~
+of a partition:
+
+[latexmath]
+++++
+\[S_t = \displaystyle\sum_i sizeOf\big (c_{k_i}\big) + \displaystyle\sum_j sizeOf\big(c_{s_j}\big) + N_r\times \bigg(\displaystyle\sum_k sizeOf\big(c_{r_k}\big) + \displaystyle\sum_l sizeOf\big(c_{c_l}\big)\bigg) +\]
+++++
+
+[latexmath]
+++++
+\[N_v\times sizeOf\big(t_{avg}\big)\]
+++++
+
+This is a bit more complex than the previous formula, but let's break it
+down a bit at a time. Let’s take a look at the notation first:
+
+* In this formula, c~k~ refers to partition key columns, c~s~ to static
+columns, c~r~ to regular columns, and c~c~ to clustering columns.
+* The term t~avg~ refers to the average number of bytes of metadata
+stored per cell, such as timestamps. It is typical to use an estimate of
+8 bytes for this value.
+* You'll recognize the number of rows N~r~ and number of values N~v~
+from previous calculations.
+* The *sizeOf()* function refers to the size in bytes of the CQL data
+type of each referenced column.
+
+The first term asks you to sum the size of the partition key columns.
+For this example, the `available_rooms_by_hotel_date` table has a single
+partition key column, the `hotel_id`, which is of type `text`. Assuming
+that hotel identifiers are simple 5-character codes, you have a 5-byte
+value, so the sum of the partition key column sizes is 5 bytes.
+
+The second term asks you to sum the size of the static columns. This
+table has no static columns, so the size is 0 bytes.
+
+The third term is the most involved, and for good reason—it is
+calculating the size of the cells in the partition. Sum the size of the
+clustering columns and regular columns. The two clustering columns are
+the `date`, which is 4 bytes, and the `room_number`, which is a 2-byte
+short integer, giving a sum of 6 bytes. There is only a single regular
+column, the boolean `is_available`, which is 1 byte in size. Summing the
+regular column size (1 byte) plus the clustering column size (6 bytes)
+gives a total of 7 bytes. To finish up the term, multiply this value by
+the number of rows (73,000), giving a result of 511,000 bytes (0.51 MB).
+
+The fourth term is simply counting the metadata that that Cassandra
+stores for each cell. In the storage format used by Cassandra 3.0 and
+later, the amount of metadata for a given cell varies based on the type
+of data being stored, and whether or not custom timestamp or TTL values
+are specified for individual cells. For this table, reuse the number of
+values from the previous calculation (73,000) and multiply by 8, which
+gives 0.58 MB.
+
+Adding these terms together, you get a final estimate:
+
+[latexmath]
+++++
+\[Partition size = 16 bytes + 0 bytes + 0.51 MB + 0.58 MB = 1.1 MB\]
+++++
+
+This formula is an approximation of the actual size of a partition on
+disk, but is accurate enough to be quite useful. Remembering that the
+partition must be able to fit on a single node, it looks like the table
+design will not put a lot of strain on disk storage.
+
+Cassandra’s storage engine was re-implemented for the 3.0 release,
+including a new format for SSTable files. The previous format stored a
+separate copy of the clustering columns as part of the record for each
+cell. The newer format eliminates this duplication, which reduces the
+size of stored data and simplifies the formula for computing that size.
+
+Keep in mind also that this estimate only counts a single replica of
+data. You will need to multiply the value obtained here by the number of
+partitions and the number of replicas specified by the keyspace’s
+replication strategy in order to determine the total required total
+capacity for each table. This will come in handy when you plan your
+cluster.
+
+== Breaking Up Large Partitions
+
+As discussed previously, the goal is to design tables that can provide
+the data you need with queries that touch a single partition, or failing
+that, the minimum possible number of partitions. However, as shown in
+the examples, it is quite possible to design wide partition-style tables
+that approach Cassandra’s built-in limits. Performing sizing analysis on
+tables may reveal partitions that are potentially too large, either in
+number of values, size on disk, or both.
+
+The technique for splitting a large partition is straightforward: add an
+additional column to the partition key. In most cases, moving one of the
+existing columns into the partition key will be sufficient. Another
+option is to introduce an additional column to the table to act as a
+sharding key, but this requires additional application logic.
+
+Continuing to examine the available rooms example, if you add the `date`
+column to the partition key for the `available_rooms_by_hotel_date`
+table, each partition would then represent the availability of rooms at
+a specific hotel on a specific date. This will certainly yield
+partitions that are significantly smaller, perhaps too small, as the
+data for consecutive days will likely be on separate nodes.
+
+Another technique known as *bucketing* is often used to break the data
+into moderate-size partitions. For example, you could bucketize the
+`available_rooms_by_hotel_date` table by adding a `month` column to the
+partition key, perhaps represented as an integer. The comparision with
+the original design is shown in the figure below. While the `month`
+column is partially duplicative of the `date`, it provides a nice way of
+grouping related data in a partition that will not get too large.
+
+image::data_modeling_hotel_bucketing.png[image]
+
+If you really felt strongly about preserving a wide partition design,
+you could instead add the `room_id` to the partition key, so that each
+partition would represent the availability of the room across all dates.
+Because there was no query identified that involves searching
+availability of a specific room, the first or second design approach is
+most suitable to the application needs.
+
+_Material adapted from Cassandra, The Definitive Guide. Published by
+O'Reilly Media, Inc. Copyright © 2020 Jeff Carpenter, Eben Hewitt. All
+rights reserved. Used with permission._
diff --git a/doc/modules/cassandra/pages/data_modeling/data_modeling_schema.adoc b/doc/modules/cassandra/pages/data_modeling/data_modeling_schema.adoc
new file mode 100644
index 0000000..7b0cf5c
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/data_modeling_schema.adoc
@@ -0,0 +1,130 @@
+= Defining Database Schema
+
+Once you have finished evaluating and refining the physical model,
+you’re ready to implement the schema in CQL. Here is the schema for the
+`hotel` keyspace, using CQL’s comment feature to document the query
+pattern supported by each table:
+
+[source,cql]
+----
+CREATE KEYSPACE hotel WITH replication =
+  {‘class’: ‘SimpleStrategy’, ‘replication_factor’ : 3};
+
+CREATE TYPE hotel.address (
+  street text,
+  city text,
+  state_or_province text,
+  postal_code text,
+  country text );
+
+CREATE TABLE hotel.hotels_by_poi (
+  poi_name text,
+  hotel_id text,
+  name text,
+  phone text,
+  address frozen<address>,
+  PRIMARY KEY ((poi_name), hotel_id) )
+  WITH comment = ‘Q1. Find hotels near given poi’
+  AND CLUSTERING ORDER BY (hotel_id ASC) ;
+
+CREATE TABLE hotel.hotels (
+  id text PRIMARY KEY,
+  name text,
+  phone text,
+  address frozen<address>,
+  pois set )
+  WITH comment = ‘Q2. Find information about a hotel’;
+
+CREATE TABLE hotel.pois_by_hotel (
+  poi_name text,
+  hotel_id text,
+  description text,
+  PRIMARY KEY ((hotel_id), poi_name) )
+  WITH comment = Q3. Find pois near a hotel’;
+
+CREATE TABLE hotel.available_rooms_by_hotel_date (
+  hotel_id text,
+  date date,
+  room_number smallint,
+  is_available boolean,
+  PRIMARY KEY ((hotel_id), date, room_number) )
+  WITH comment = ‘Q4. Find available rooms by hotel date’;
+
+CREATE TABLE hotel.amenities_by_room (
+  hotel_id text,
+  room_number smallint,
+  amenity_name text,
+  description text,
+  PRIMARY KEY ((hotel_id, room_number), amenity_name) )
+  WITH comment = ‘Q5. Find amenities for a room’;
+----
+
+Notice that the elements of the partition key are surrounded with
+parentheses, even though the partition key consists of the single column
+`poi_name`. This is a best practice that makes the selection of
+partition key more explicit to others reading your CQL.
+
+Similarly, here is the schema for the `reservation` keyspace:
+
+[source,cql]
+----
+CREATE KEYSPACE reservation WITH replication = {‘class’:
+  ‘SimpleStrategy’, ‘replication_factor’ : 3};
+
+CREATE TYPE reservation.address (
+  street text,
+  city text,
+  state_or_province text,
+  postal_code text,
+  country text );
+
+CREATE TABLE reservation.reservations_by_confirmation (
+  confirm_number text,
+  hotel_id text,
+  start_date date,
+  end_date date,
+  room_number smallint,
+  guest_id uuid,
+  PRIMARY KEY (confirm_number) )
+  WITH comment = ‘Q6. Find reservations by confirmation number’;
+
+CREATE TABLE reservation.reservations_by_hotel_date (
+  hotel_id text,
+  start_date date,
+  end_date date,
+  room_number smallint,
+  confirm_number text,
+  guest_id uuid,
+  PRIMARY KEY ((hotel_id, start_date), room_number) )
+  WITH comment = ‘Q7. Find reservations by hotel and date’;
+
+CREATE TABLE reservation.reservations_by_guest (
+  guest_last_name text,
+  hotel_id text,
+  start_date date,
+  end_date date,
+  room_number smallint,
+  confirm_number text,
+  guest_id uuid,
+  PRIMARY KEY ((guest_last_name), hotel_id) )
+  WITH comment = ‘Q8. Find reservations by guest name’;
+
+CREATE TABLE reservation.guests (
+  guest_id uuid PRIMARY KEY,
+  first_name text,
+  last_name text,
+  title text,
+  emails set,
+  phone_numbers list,
+  addresses map<text,
+  frozen<address>,
+  confirm_number text )
+  WITH comment = ‘Q9. Find guest by ID’;
+----
+
+You now have a complete Cassandra schema for storing data for a hotel
+application.
+
+_Material adapted from Cassandra, The Definitive Guide. Published by
+O'Reilly Media, Inc. Copyright © 2020 Jeff Carpenter, Eben Hewitt. All
+rights reserved. Used with permission._
diff --git a/doc/modules/cassandra/pages/data_modeling/data_modeling_tools.adoc b/doc/modules/cassandra/pages/data_modeling/data_modeling_tools.adoc
new file mode 100644
index 0000000..0f3556f
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/data_modeling_tools.adoc
@@ -0,0 +1,44 @@
+= Cassandra Data Modeling Tools
+
+There are several tools available to help you design and manage your
+Cassandra schema and build queries.
+
+* https://hackolade.com/nosqldb.html#cassandra[Hackolade] is a data
+modeling tool that supports schema design for Cassandra and many other
+NoSQL databases. Hackolade supports the unique concepts of CQL such as
+partition keys and clustering columns, as well as data types including
+collections and UDTs. It also provides the ability to create Chebotko
+diagrams.
+* http://kdm.dataview.org/[Kashlev Data Modeler] is a Cassandra data
+modeling tool that automates the data modeling methodology described in
+this documentation, including identifying access patterns, conceptual,
+logical, and physical data modeling, and schema generation. It also
+includes model patterns that you can optionally leverage as a starting
+point for your designs.
+* DataStax DevCenter is a tool for managing schema, executing queries
+and viewing results. While the tool is no longer actively supported, it
+is still popular with many developers and is available as a
+https://academy.datastax.com/downloads[free download]. DevCenter
+features syntax highlighting for CQL commands, types, and name literals.
+DevCenter provides command completion as you type out CQL commands and
+interprets the commands you type, highlighting any errors you make. The
+tool provides panes for managing multiple CQL scripts and connections to
+multiple clusters. The connections are used to run CQL commands against
+live clusters and view the results. The tool also has a query trace
+feature that is useful for gaining insight into the performance of your
+queries.
+* IDE Plugins - There are CQL plugins available for several Integrated
+Development Environments (IDEs), such as IntelliJ IDEA and Apache
+NetBeans. These plugins typically provide features such as schema
+management and query execution.
+
+Some IDEs and tools that claim to support Cassandra do not actually
+support CQL natively, but instead access Cassandra using a JDBC/ODBC
+driver and interact with Cassandra as if it were a relational database
+with SQL support. Wnen selecting tools for working with Cassandra you’ll
+want to make sure they support CQL and reinforce Cassandra best
+practices for data modeling as presented in this documentation.
+
+_Material adapted from Cassandra, The Definitive Guide. Published by
+O'Reilly Media, Inc. Copyright © 2020 Jeff Carpenter, Eben Hewitt. All
+rights reserved. Used with permission._
diff --git a/doc/modules/cassandra/pages/data_modeling/images/Figure_1_data_model.jpg b/doc/modules/cassandra/pages/data_modeling/images/Figure_1_data_model.jpg
new file mode 100644
index 0000000..a3b330e
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/images/Figure_1_data_model.jpg
Binary files differ
diff --git a/doc/modules/cassandra/pages/data_modeling/images/Figure_2_data_model.jpg b/doc/modules/cassandra/pages/data_modeling/images/Figure_2_data_model.jpg
new file mode 100644
index 0000000..7acdeac
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/images/Figure_2_data_model.jpg
Binary files differ
diff --git a/doc/modules/cassandra/pages/data_modeling/images/data_modeling_chebotko_logical.png b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_chebotko_logical.png
new file mode 100755
index 0000000..e54b5f2
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_chebotko_logical.png
Binary files differ
diff --git a/doc/modules/cassandra/pages/data_modeling/images/data_modeling_chebotko_physical.png b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_chebotko_physical.png
new file mode 100644
index 0000000..bfdaec5
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_chebotko_physical.png
Binary files differ
diff --git a/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_bucketing.png b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_bucketing.png
new file mode 100644
index 0000000..8b53e38
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_bucketing.png
Binary files differ
diff --git a/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_erd.png b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_erd.png
new file mode 100755
index 0000000..e86fe68
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_erd.png
Binary files differ
diff --git a/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_logical.png b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_logical.png
new file mode 100755
index 0000000..e920f12
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_logical.png
Binary files differ
diff --git a/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_physical.png b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_physical.png
new file mode 100644
index 0000000..2d20a6d
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_physical.png
Binary files differ
diff --git a/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_queries.png b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_queries.png
new file mode 100755
index 0000000..2434db3
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_queries.png
Binary files differ
diff --git a/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_relational.png b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_relational.png
new file mode 100755
index 0000000..43e784e
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_hotel_relational.png
Binary files differ
diff --git a/doc/modules/cassandra/pages/data_modeling/images/data_modeling_reservation_logical.png b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_reservation_logical.png
new file mode 100755
index 0000000..0460633
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_reservation_logical.png
Binary files differ
diff --git a/doc/modules/cassandra/pages/data_modeling/images/data_modeling_reservation_physical.png b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_reservation_physical.png
new file mode 100755
index 0000000..1e6e76c
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/images/data_modeling_reservation_physical.png
Binary files differ
diff --git a/doc/modules/cassandra/pages/data_modeling/index.adoc b/doc/modules/cassandra/pages/data_modeling/index.adoc
new file mode 100644
index 0000000..105f5a3
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/index.adoc
@@ -0,0 +1,11 @@
+= Data Modeling
+
+* xref:data_modeling/intro.adoc[Introduction]
+* xref:data_modeling/data_modeling_rdbms.adoc[RDBMS]
+* xref:data_modeling/data_modeling_conceptual.adoc[Conceptual]
+* xref:data_modeling/data_modeling_logical.adoc[Logical]
+* xref:data_modeling/data_modeling_physical.adoc[Physical]
+* xref:data_modeling/data_modeling_schema.adoc[Schema]
+* xref:data_modeling/data_modeling_queries.adoc[Queries]
+* xref:data_modeling/data_modeling_refining.adoc[Refining]
+* xref:data_modeling/data_modeling_tools.adoc[Tools]
diff --git a/doc/modules/cassandra/pages/data_modeling/intro.adoc b/doc/modules/cassandra/pages/data_modeling/intro.adoc
new file mode 100644
index 0000000..92df845
--- /dev/null
+++ b/doc/modules/cassandra/pages/data_modeling/intro.adoc
@@ -0,0 +1,220 @@
+= Introduction
+
+Apache Cassandra stores data in tables, with each table consisting of
+rows and columns. CQL (Cassandra Query Language) is used to query the
+data stored in tables. Apache Cassandra data model is based around and
+optimized for querying. Cassandra does not support relational data
+modeling intended for relational databases.
+
+== What is Data Modeling?
+
+Data modeling is the process of identifying entities and their
+relationships. In relational databases, data is placed in normalized
+tables with foreign keys used to reference related data in other tables.
+Queries that the application will make are driven by the structure of
+the tables and related data are queried as table joins.
+
+In Cassandra, data modeling is query-driven. The data access patterns
+and application queries determine the structure and organization of data
+which then used to design the database tables.
+
+Data is modeled around specific queries. Queries are best designed to
+access a single table, which implies that all entities involved in a
+query must be in the same table to make data access (reads) very fast.
+Data is modeled to best suit a query or a set of queries. A table could
+have one or more entities as best suits a query. As entities do
+typically have relationships among them and queries could involve
+entities with relationships among them, a single entity may be included
+in multiple tables.
+
+== Query-driven modeling
+
+Unlike a relational database model in which queries make use of table
+joins to get data from multiple tables, joins are not supported in
+Cassandra so all required fields (columns) must be grouped together in a
+single table. Since each query is backed by a table, data is duplicated
+across multiple tables in a process known as denormalization. Data
+duplication and a high write throughput are used to achieve a high read
+performance.
+
+== Goals
+
+The choice of the primary key and partition key is important to
+distribute data evenly across the cluster. Keeping the number of
+partitions read for a query to a minimum is also important because
+different partitions could be located on different nodes and the
+coordinator would need to send a request to each node adding to the
+request overhead and latency. Even if the different partitions involved
+in a query are on the same node, fewer partitions make for a more
+efficient query.
+
+== Partitions
+
+Apache Cassandra is a distributed database that stores data across a
+cluster of nodes. A partition key is used to partition data among the
+nodes. Cassandra partitions data over the storage nodes using a variant
+of consistent hashing for data distribution. Hashing is a technique used
+to map data with which given a key, a hash function generates a hash
+value (or simply a hash) that is stored in a hash table. A partition key
+is generated from the first field of a primary key. Data partitioned
+into hash tables using partition keys provides for rapid lookup. Fewer
+the partitions used for a query faster is the response time for the
+query.
+
+As an example of partitioning, consider table `t` in which `id` is the
+only field in the primary key.
+
+....
+CREATE TABLE t (
+   id int,
+   k int,
+   v text,
+   PRIMARY KEY (id)
+);
+....
+
+The partition key is generated from the primary key `id` for data
+distribution across the nodes in a cluster.
+
+Consider a variation of table `t` that has two fields constituting the
+primary key to make a composite or compound primary key.
+
+....
+CREATE TABLE t (
+   id int,
+   c text,
+   k int,
+   v text,
+   PRIMARY KEY (id,c)
+);
+....
+
+For the table `t` with a composite primary key the first field `id` is
+used to generate the partition key and the second field `c` is the
+clustering key used for sorting within a partition. Using clustering
+keys to sort data makes retrieval of adjacent data more efficient.
+
+In general, the first field or component of a primary key is hashed to
+generate the partition key and the remaining fields or components are
+the clustering keys that are used to sort data within a partition.
+Partitioning data improves the efficiency of reads and writes. The other
+fields that are not primary key fields may be indexed separately to
+further improve query performance.
+
+The partition key could be generated from multiple fields if they are
+grouped as the first component of a primary key. As another variation of
+the table `t`, consider a table with the first component of the primary
+key made of two fields grouped using parentheses.
+
+....
+CREATE TABLE t (
+   id1 int,
+   id2 int,
+   c1 text,
+   c2 text
+   k int,
+   v text,
+   PRIMARY KEY ((id1,id2),c1,c2)
+);
+....
+
+For the preceding table `t` the first component of the primary key
+constituting fields `id1` and `id2` is used to generate the partition
+key and the rest of the fields `c1` and `c2` are the clustering keys
+used for sorting within a partition.
+
+== Comparing with Relational Data Model
+
+Relational databases store data in tables that have relations with other
+tables using foreign keys. A relational database’s approach to data
+modeling is table-centric. Queries must use table joins to get data from
+multiple tables that have a relation between them. Apache Cassandra does
+not have the concept of foreign keys or relational integrity. Apache
+Cassandra’s data model is based around designing efficient queries;
+queries that don’t involve multiple tables. Relational databases
+normalize data to avoid duplication. Apache Cassandra in contrast
+de-normalizes data by duplicating data in multiple tables for a
+query-centric data model. If a Cassandra data model cannot fully
+integrate the complexity of relationships between the different entities
+for a particular query, client-side joins in application code may be
+used.
+
+== Examples of Data Modeling
+
+As an example, a `magazine` data set consists of data for magazines with
+attributes such as magazine id, magazine name, publication frequency,
+publication date, and publisher. A basic query (Q1) for magazine data is
+to list all the magazine names including their publication frequency. As
+not all data attributes are needed for Q1 the data model would only
+consist of `id` ( for partition key), magazine name and publication
+frequency as shown in Figure 1.
+
+image::Figure_1_data_model.jpg[image]
+
+Figure 1. Data Model for Q1
+
+Another query (Q2) is to list all the magazine names by publisher. For
+Q2 the data model would consist of an additional attribute `publisher`
+for the partition key. The `id` would become the clustering key for
+sorting within a partition. Data model for Q2 is illustrated in Figure
+2.
+
+image::Figure_2_data_model.jpg[image]
+
+Figure 2. Data Model for Q2
+
+== Designing Schema
+
+After the conceptual data model has been created a schema may be
+designed for a query. For Q1 the following schema may be used.
+
+....
+CREATE TABLE magazine_name (id int PRIMARY KEY, name text, publicationFrequency text)
+....
+
+For Q2 the schema definition would include a clustering key for sorting.
+
+....
+CREATE TABLE magazine_publisher (publisher text,id int,name text, publicationFrequency text,  
+PRIMARY KEY (publisher, id)) WITH CLUSTERING ORDER BY (id DESC)
+....
+
+== Data Model Analysis
+
+The data model is a conceptual model that must be analyzed and optimized
+based on storage, capacity, redundancy and consistency. A data model may
+need to be modified as a result of the analysis. Considerations or
+limitations that are used in data model analysis include:
+
+* Partition Size
+* Data Redundancy
+* Disk space
+* Lightweight Transactions (LWT)
+
+The two measures of partition size are the number of values in a
+partition and partition size on disk. Though requirements for these
+measures may vary based on the application a general guideline is to
+keep number of values per partition to below 100,000 and disk space per
+partition to below 100MB.
+
+Data redundancies as duplicate data in tables and multiple partition
+replicates are to be expected in the design of a data model , but
+nevertheless should be kept in consideration as a parameter to keep to
+the minimum. LWT transactions (compare-and-set, conditional update)
+could affect performance and queries using LWT should be kept to the
+minimum.
+
+== Using Materialized Views
+
+[WARNING]
+.Warning
+====
+Materialized views (MVs) are experimental in the latest (4.0) release.
+====
+Materialized views (MVs) could be used to implement multiple queries
+for a single table. A materialized view is a table built from data from
+another table, the base table, with new primary key and new properties.
+Changes to the base table data automatically add and update data in a
+MV. Different queries may be implemented using a materialized view as an
+MV's primary key differs from the base table. Queries are optimized by
+the primary key definition.
diff --git a/doc/modules/cassandra/pages/faq/index.adoc b/doc/modules/cassandra/pages/faq/index.adoc
new file mode 100644
index 0000000..03868b3
--- /dev/null
+++ b/doc/modules/cassandra/pages/faq/index.adoc
@@ -0,0 +1,272 @@
+= Frequently Asked Questions
+
+[[why-cant-list-all]]
+== Why can't I set `listen_address` to listen on 0.0.0.0 (all my addresses)?
+
+Cassandra is a gossip-based distributed system and `listen_address` is
+the address a node tells other nodes to reach it at. Telling other nodes
+"contact me on any of my addresses" is a bad idea; if different nodes in
+the cluster pick different addresses for you, Bad Things happen.
+
+If you don't want to manually specify an IP to `listen_address` for each
+node in your cluster (understandable!), leave it blank and Cassandra
+will use `InetAddress.getLocalHost()` to pick an address. Then it's up
+to you or your ops team to make things resolve correctly (`/etc/hosts/`,
+dns, etc).
+
+One exception to this process is JMX, which by default binds to 0.0.0.0
+(Java bug 6425769).
+
+See `256` and `43` for more gory details.
+
+[[what-ports]]
+== What ports does Cassandra use?
+
+By default, Cassandra uses 7000 for cluster communication (7001 if SSL
+is enabled), 9042 for native protocol clients, and 7199 for JMX. The
+internode communication and native protocol ports are configurable in
+the `cassandra-yaml`. The JMX port is configurable in `cassandra-env.sh`
+(through JVM options). All ports are TCP.
+
+[[what-happens-on-joins]]
+== What happens to existing data in my cluster when I add new nodes?
+
+When a new nodes joins a cluster, it will automatically contact the
+other nodes in the cluster and copy the right data to itself. See
+`topology-changes`.
+
+[[asynch-deletes]]
+== I delete data from Cassandra, but disk usage stays the same. What gives?
+
+Data you write to Cassandra gets persisted to SSTables. Since SSTables
+are immutable, the data can't actually be removed when you perform a
+delete, instead, a marker (also called a "tombstone") is written to
+indicate the value's new status. Never fear though, on the first
+compaction that occurs between the data and the tombstone, the data will
+be expunged completely and the corresponding disk space recovered. See
+`compaction` for more detail.
+
+[[one-entry-ring]]
+== Why does nodetool ring only show one entry, even though my nodes logged that they see each other joining the ring?
+
+This happens when you have the same token assigned to each node. Don't
+do that.
+
+Most often this bites people who deploy by installing Cassandra on a VM
+(especially when using the Debian package, which auto-starts Cassandra
+after installation, thus generating and saving a token), then cloning
+that VM to other nodes.
+
+The easiest fix is to wipe the data and commitlog directories, thus
+making sure that each node will generate a random token on the next
+restart.
+
+[[change-replication-factor]]
+== Can I change the replication factor (a a keyspace) on a live cluster?
+
+Yes, but it will require running a full repair (or cleanup) to change
+the replica count of existing data:
+
+* `Alter <alter-keyspace-statement>` the replication factor for desired
+keyspace (using cqlsh for instance).
+* If you're reducing the replication factor, run `nodetool cleanup` on
+the cluster to remove surplus replicated data. Cleanup runs on a
+per-node basis.
+* If you're increasing the replication factor, run
+`nodetool repair -full` to ensure data is replicated according to the
+new configuration. Repair runs on a per-replica set basis. This is an
+intensive process that may result in adverse cluster performance. It's
+highly recommended to do rolling repairs, as an attempt to repair the
+entire cluster at once will most likely swamp it. Note that you will
+need to run a full repair (`-full`) to make sure that already repaired
+sstables are not skipped.
+
+[[can-large-blob]]
+== Can I Store (large) BLOBs in Cassandra?
+
+Cassandra isn't optimized for large file or BLOB storage and a single
+`blob` value is always read and send to the client entirely. As such,
+storing small blobs (less than single digit MB) should not be a problem,
+but it is advised to manually split large blobs into smaller chunks.
+
+Please note in particular that by default, any value greater than 16MB
+will be rejected by Cassandra due the `max_mutation_size_in_kb`
+configuration of the `cassandra-yaml` file (which default to half of
+`commitlog_segment_size_in_mb`, which itself default to 32MB).
+
+[[nodetool-connection-refused]]
+== Nodetool says "Connection refused to host: 127.0.1.1" for any remote host. What gives?
+
+Nodetool relies on JMX, which in turn relies on RMI, which in turn sets
+up its own listeners and connectors as needed on each end of the
+exchange. Normally all of this happens behind the scenes transparently,
+but incorrect name resolution for either the host connecting, or the one
+being connected to, can result in crossed wires and confusing
+exceptions.
+
+If you are not using DNS, then make sure that your `/etc/hosts` files
+are accurate on both ends. If that fails, try setting the
+`-Djava.rmi.server.hostname=<public name>` JVM option near the bottom of
+`cassandra-env.sh` to an interface that you can reach from the remote
+machine.
+
+[[to-batch-or-not-to-batch]]
+== Will batching my operations speed up my bulk load?
+
+No. Using batches to load data will generally just add "spikes" of
+latency. Use asynchronous INSERTs instead, or use true `bulk-loading`.
+
+An exception is batching updates to a single partition, which can be a
+Good Thing (as long as the size of a single batch stay reasonable). But
+never ever blindly batch everything!
+
+[[selinux]]
+== On RHEL nodes are unable to join the ring
+
+Check if https://en.wikipedia.org/wiki/Security-Enhanced_Linux[SELinux]
+is on; if it is, turn it off.
+
+[[how-to-unsubscribe]]
+== How do I unsubscribe from the email list?
+
+Send an email to `user-unsubscribe@cassandra.apache.org`.
+
+[[cassandra-eats-all-my-memory]]
+== Why does top report that Cassandra is using a lot more memory than the Java heap max?
+
+Cassandra uses https://en.wikipedia.org/wiki/Memory-mapped_file[Memory
+Mapped Files] (mmap) internally. That is, we use the operating system's
+virtual memory system to map a number of on-disk files into the
+Cassandra process' address space. This will "use" virtual memory; i.e.
+address space, and will be reported by tools like top accordingly, but
+on 64 bit systems virtual address space is effectively unlimited so you
+should not worry about that.
+
+What matters from the perspective of "memory use" in the sense as it is
+normally meant, is the amount of data allocated on brk() or mmap'd
+/dev/zero, which represent real memory used. The key issue is that for a
+mmap'd file, there is never a need to retain the data resident in
+physical memory. Thus, whatever you do keep resident in physical memory
+is essentially just there as a cache, in the same way as normal I/O will
+cause the kernel page cache to retain data that you read/write.
+
+The difference between normal I/O and mmap() is that in the mmap() case
+the memory is actually mapped to the process, thus affecting the virtual
+size as reported by top. The main argument for using mmap() instead of
+standard I/O is the fact that reading entails just touching memory - in
+the case of the memory being resident, you just read it - you don't even
+take a page fault (so no overhead in entering the kernel and doing a
+semi-context switch). This is covered in more detail
+http://www.varnish-cache.org/trac/wiki/ArchitectNotes[here].
+
+== What are seeds?
+
+Seeds are used during startup to discover the cluster.
+
+If you configure your nodes to refer some node as seed, nodes in your
+ring tend to send Gossip message to seeds more often (also see the
+`section on gossip <gossip>`) than to non-seeds. In other words, seeds
+are worked as hubs of Gossip network. With seeds, each node can detect
+status changes of other nodes quickly.
+
+Seeds are also referred by new nodes on bootstrap to learn other nodes
+in ring. When you add a new node to ring, you need to specify at least
+one live seed to contact. Once a node join the ring, it learns about the
+other nodes, so it doesn't need seed on subsequent boot.
+
+You can make a seed a node at any time. There is nothing special about
+seed nodes. If you list the node in seed list it is a seed
+
+Seeds do not auto bootstrap (i.e. if a node has itself in its seed list
+it will not automatically transfer data to itself) If you want a node to
+do that, bootstrap it first and then add it to seeds later. If you have
+no data (new install) you do not have to worry about bootstrap at all.
+
+Recommended usage of seeds:
+
+* pick two (or more) nodes per data center as seed nodes.
+* sync the seed list to all your nodes
+
+[[are-seeds-SPOF]]
+== Does single seed mean single point of failure?
+
+The ring can operate or boot without a seed; however, you will not be
+able to add new nodes to the cluster. It is recommended to configure
+multiple seeds in production system.
+
+[[cant-call-jmx-method]]
+== Why can't I call jmx method X on jconsole?
+
+Some of JMX operations use array argument and as jconsole doesn't
+support array argument, those operations can't be called with jconsole
+(the buttons are inactive for them). You need to write a JMX client to
+call such operations or need array-capable JMX monitoring tool.
+
+[[why-message-dropped]]
+== Why do I see "... messages dropped ..." in the logs?
+
+This is a symptom of load shedding -- Cassandra defending itself against
+more requests than it can handle.
+
+Internode messages which are received by a node, but do not get not to
+be processed within their proper timeout (see `read_request_timeout`,
+`write_request_timeout`, ... in the `cassandra-yaml`), are dropped
+rather than processed (since the as the coordinator node will no longer
+be waiting for a response).
+
+For writes, this means that the mutation was not applied to all replicas
+it was sent to. The inconsistency will be repaired by read repair, hints
+or a manual repair. The write operation may also have timeouted as a
+result.
+
+For reads, this means a read request may not have completed.
+
+Load shedding is part of the Cassandra architecture, if this is a
+persistent issue it is generally a sign of an overloaded node or
+cluster.
+
+[[oom-map-failed]]
+== Cassandra dies with `java.lang.OutOfMemoryError: Map failed`
+
+If Cassandra is dying *specifically* with the "Map failed" message, it
+means the OS is denying java the ability to lock more memory. In linux,
+this typically means memlock is limited. Check
+`/proc/<pid of cassandra>/limits` to verify this and raise it (eg, via
+ulimit in bash). You may also need to increase `vm.max_map_count.` Note
+that the debian package handles this for you automatically.
+
+[[what-on-same-timestamp-update]]
+== What happens if two updates are made with the same timestamp?
+
+Updates must be commutative, since they may arrive in different orders
+on different replicas. As long as Cassandra has a deterministic way to
+pick the winner (in a timestamp tie), the one selected is as valid as
+any other, and the specifics should be treated as an implementation
+detail. That said, in the case of a timestamp tie, Cassandra follows two
+rules: first, deletes take precedence over inserts/updates. Second, if
+there are two updates, the one with the lexically larger value is
+selected.
+
+[[why-bootstrapping-stream-error]]
+== Why bootstrapping a new node fails with a "Stream failed" error?
+
+Two main possibilities:
+
+. the GC may be creating long pauses disrupting the streaming process
+. compactions happening in the background hold streaming long enough
+that the TCP connection fails
+
+In the first case, regular GC tuning advices apply. In the second case,
+you need to set TCP keepalive to a lower value (default is very high on
+Linux). Try to just run the following:
+
+....
+$ sudo /sbin/sysctl -w net.ipv4.tcp_keepalive_time=60 net.ipv4.tcp_keepalive_intvl=60 net.ipv4.tcp_keepalive_probes=5
+....
+
+To make those settings permanent, add them to your `/etc/sysctl.conf`
+file.
+
+Note: https://cloud.google.com/compute/[GCE]'s firewall will always
+interrupt TCP connections that are inactive for more than 10 min.
+Running the above command is highly recommended in that environment.
diff --git a/doc/modules/cassandra/pages/getting_started/configuring.adoc b/doc/modules/cassandra/pages/getting_started/configuring.adoc
new file mode 100644
index 0000000..ba72f97
--- /dev/null
+++ b/doc/modules/cassandra/pages/getting_started/configuring.adoc
@@ -0,0 +1,84 @@
+= Configuring Cassandra
+
+The `Cassandra` configuration files location varies, depending on the
+type of installation:
+
+* docker: `/etc/cassandra` directory
+* tarball: `conf` directory within the tarball install location
+* package: `/etc/cassandra` directory
+
+Cassandra's default configuration file, `cassandra.yaml`, is sufficient
+to explore a simple single-node `cluster`. However, anything beyond
+running a single-node cluster locally requires additional configuration
+to various Cassandra configuration files. Some examples that require
+non-default configuration are deploying a multi-node cluster or using
+clients that are not running on a cluster node.
+
+* `cassandra.yaml`: the main configuration file for Cassandra
+* `cassandra-env.sh`: environment variables can be set
+* `cassandra-rackdc.properties` OR `cassandra-topology.properties`: set
+rack and datacenter information for a cluster
+* `logback.xml`: logging configuration including logging levels
+* `jvm-*`: a number of JVM configuration files for both the server and
+clients
+* `commitlog_archiving.properties`: set archiving parameters for the
+`commitlog`
+
+Two sample configuration files can also be found in `./conf`:
+
+* `metrics-reporter-config-sample.yaml`: configuring what the
+metrics-report will collect
+* `cqlshrc.sample`: how the CQL shell, cqlsh, can be configured
+
+== Main runtime properties
+
+Configuring Cassandra is done by setting yaml properties in the
+`cassandra.yaml` file. At a minimum you should consider setting the
+following properties:
+
+* `cluster_name`: Set the name of your cluster.
+* `seeds`: A comma separated list of the IP addresses of your cluster
+`seed nodes`.
+* `storage_port`: Check that you don't have the default port of 7000
+blocked by a firewall.
+* `listen_address`: The `listen address` is the IP address of a node
+that allows it to communicate with other nodes in the cluster. Set to
+[.title-ref]#localhost# by default. Alternatively, you can set
+`listen_interface` to tell Cassandra which interface to use, and
+consecutively which address to use. Set one property, not both.
+* `native_transport_port`: Check that you don't have the default port of
+9042 blocked by a firewall, so that clients like cqlsh can communicate
+with Cassandra on this port.
+
+== Changing the location of directories
+
+The following yaml properties control the location of directories:
+
+* `data_file_directories`: One or more directories where data files,
+like `SSTables` are located.
+* `commitlog_directory`: The directory where commitlog files are
+located.
+* `saved_caches_directory`: The directory where saved caches are
+located.
+* `hints_directory`: The directory where `hints` are located.
+
+For performance reasons, if you have multiple disks, consider putting
+commitlog and data files on different disks.
+
+== Environment variables
+
+JVM-level settings such as heap size can be set in `cassandra-env.sh`.
+You can add any additional JVM command line argument to the `JVM_OPTS`
+environment variable; when Cassandra starts, these arguments will be
+passed to the JVM.
+
+== Logging
+
+The default logger is [.title-ref]#logback#. By default it will log:
+
+* *INFO* level in `system.log`
+* *DEBUG* level in `debug.log`
+
+When running in the foreground, it will also log at INFO level to the
+console. You can change logging properties by editing `logback.xml` or
+by running the [.title-ref]#nodetool setlogginglevel# command.
diff --git a/doc/modules/cassandra/pages/getting_started/drivers.adoc b/doc/modules/cassandra/pages/getting_started/drivers.adoc
new file mode 100644
index 0000000..eb15a55
--- /dev/null
+++ b/doc/modules/cassandra/pages/getting_started/drivers.adoc
@@ -0,0 +1,90 @@
+= Client drivers
+
+Here are known Cassandra client drivers organized by language. Before
+choosing a driver, you should verify the Cassandra version and
+functionality supported by a specific driver.
+
+== Java
+
+* http://achilles.archinnov.info/[Achilles]
+* https://github.com/Netflix/astyanax/wiki/Getting-Started[Astyanax]
+* https://github.com/noorq/casser[Casser]
+* https://github.com/datastax/java-driver[Datastax Java driver]
+* https://github.com/impetus-opensource/Kundera[Kundera]
+* https://github.com/deanhiller/playorm[PlayORM]
+
+== Python
+
+* https://github.com/datastax/python-driver[Datastax Python driver]
+
+== Ruby
+
+* https://github.com/datastax/ruby-driver[Datastax Ruby driver]
+
+== C# / .NET
+
+* https://github.com/pchalamet/cassandra-sharp[Cassandra Sharp]
+* https://github.com/datastax/csharp-driver[Datastax C# driver]
+* https://github.com/managedfusion/fluentcassandra[Fluent Cassandra]
+
+== Nodejs
+
+* https://github.com/datastax/nodejs-driver[Datastax Nodejs driver]
+
+== PHP
+
+* http://code.google.com/a/apache-extras.org/p/cassandra-pdo[CQL | PHP]
+* https://github.com/datastax/php-driver/[Datastax PHP driver]
+* https://github.com/aparkhomenko/php-cassandra[PHP-Cassandra]
+* https://github.com/duoshuo/php-cassandra[PHP Library for Cassandra]
+
+== C++
+
+* https://github.com/datastax/cpp-driver[Datastax C++ driver]
+* http://sourceforge.net/projects/libqtcassandra[libQTCassandra]
+
+== Scala
+
+* https://github.com/datastax/spark-cassandra-connector[Datastax Spark
+connector]
+* https://github.com/newzly/phantom[Phantom]
+* https://github.com/getquill/quill[Quill]
+
+== Clojure
+
+* https://github.com/mpenet/alia[Alia]
+* https://github.com/clojurewerkz/cassaforte[Cassaforte]
+* https://github.com/mpenet/hayt[Hayt]
+
+== Erlang
+
+* https://github.com/matehat/cqerl[CQerl]
+* https://github.com/silviucpp/erlcass[Erlcass]
+
+== Go
+
+* https://github.com/relops/cqlc[CQLc]
+* https://github.com/hailocab/gocassa[Gocassa]
+* https://github.com/gocql/gocql[GoCQL]
+
+== Haskell
+
+* https://github.com/ozataman/cassy[Cassy]
+
+== Rust
+
+* https://github.com/neich/rust-cql[Rust CQL]
+
+== Perl
+
+* https://github.com/tvdw/perl-dbd-cassandra[Cassandra::Client and
+DBD::Cassandra]
+
+== Elixir
+
+* https://github.com/lexhide/xandra[Xandra]
+* https://github.com/matehat/cqex[CQEx]
+
+== Dart
+
+* https://github.com/achilleasa/dart_cassandra_cql[dart_cassandra_cql]
diff --git a/doc/modules/cassandra/pages/getting_started/index.adoc b/doc/modules/cassandra/pages/getting_started/index.adoc
new file mode 100644
index 0000000..af43c17
--- /dev/null
+++ b/doc/modules/cassandra/pages/getting_started/index.adoc
@@ -0,0 +1,30 @@
+= Getting Started
+
+This section covers how to get started using Apache Cassandra and should
+be the first thing to read if you are new to Cassandra.
+
+* xref:getting_started/installing.adoc[Installing Cassandra]: Installation instructions plus information on choosing a method.  
+** [ xref:getting_started/installing.adoc#installing-the-docker-image[Docker] ]
+[ xref:getting_started/installing.adoc#installing-the-binary-tarball[tarball] ]
+[ xref:getting_started/installing.adoc#installing-the-debian-packages[Debian] ]
+[ xref:getting_started/installing.adoc#installing-the-rpm-packages[RPM] ]
+* xref:getting_started/configuring.adoc[Configuring Cassandra]
+* xref:getting_started/querying.adoc[Inserting and querying data]
+* xref:getting_started/drivers.adoc[Client drivers]: Drivers for various languages.
+** [ xref:getting_started/drivers.adoc#java[Java] ]
+ [ xref:getting_started/drivers.adoc#python[Python] ]
+ [ xref:getting_started/drivers.adoc#ruby[Ruby] ]
+ [ xref:getting_started/drivers.adoc#c-net[C# / .NET] ]
+ [ xref:getting_started/drivers.adoc#nodejs[Node.js] ]
+ [ xref:getting_started/drivers.adoc#php[PHP] ]
+ [ xref:getting_started/drivers.adoc#c[C++] ]
+ [ xref:getting_started/drivers.adoc#scala[Scala] ]
+ [ xref:getting_started/drivers.adoc#clojure[Clojure] ]
+ [ xref:getting_started/drivers.adoc#erlang[Erlang] ]
+ [ xref:getting_started/drivers.adoc#go[Go] ]
+ [ xref:getting_started/drivers.adoc#haskell[Haskell] ]
+ [ xref:getting_started/drivers.adoc#rust[Rust] ]
+ [ xref:getting_started/drivers.adoc#perl[Perl] ]
+ [ xref:getting_started/drivers.adoc#elixir[Elixir] ]
+ [ xref:getting_started/drivers.adoc#dart[Dart] ]
+* xref:getting_started/production.adoc[Production recommendations]
diff --git a/doc/modules/cassandra/pages/getting_started/installing.adoc b/doc/modules/cassandra/pages/getting_started/installing.adoc
new file mode 100644
index 0000000..96dbe52
--- /dev/null
+++ b/doc/modules/cassandra/pages/getting_started/installing.adoc
@@ -0,0 +1,344 @@
+= Installing Cassandra
+:slug: Installing Cassandra
+:tabs:
+
+These are the instructions for deploying the supported releases of
+Apache Cassandra on Linux servers.
+
+Cassandra runs on a wide array of Linux distributions including (but not
+limited to):
+
+* Ubuntu, most notably LTS releases 16.04 to 18.04
+* CentOS & RedHat Enterprise Linux (RHEL) including 6.6 to 7.7
+* Amazon Linux AMIs including 2016.09 through to Linux 2
+* Debian versions 8 & 9
+* SUSE Enterprise Linux 12
+
+This is not an exhaustive list of operating system platforms, nor is it
+prescriptive. However users will be well-advised to conduct exhaustive
+tests of their own particularly for less-popular distributions of Linux.
+Deploying on older versions is not recommended unless you have previous
+experience with the older distribution in a production environment.
+
+== Prerequisites
+
+* Install the latest version of Java 8, either the
+http://www.oracle.com/technetwork/java/javase/downloads/index.html[Oracle
+Java Standard Edition 8] or http://openjdk.java.net/[OpenJDK 8]. To
+verify that you have the correct version of java installed, type
+`java -version`.
+* For using cqlsh, the latest version of
+https://www.python.org/downloads/[Python 2.7]. To verify
+that you have the correct version of Python installed, type
+`python --version`.
+
+== Choosing an installation method
+
+There are three methods of installing Cassandra that are common:
+
+* xref:getting_started/installing.adoc#docker[Docker image]
+* xref:getting_started/installing.adoc#tarball[Tarball binary file]
+* xref:getting_started/installing.adoc#package[Package installation (RPM, YUM)]
+
+If you are a current Docker user, installing a Docker image is simple.
+You'll need to install Docker Desktop for Mac, Docker Desktop for Windows,
+or have `docker` installed on Linux.
+Pull the appropriate image and then start Cassandra with a run command.
+
+For most users, installing the binary tarball is also a simple choice.
+The tarball unpacks all its contents into a single location with
+binaries and configuration files located in their own subdirectories.
+The most obvious attribute of the tarball installation is it does not
+require `root` permissions and can be installed on any Linux
+distribution.
+
+Packaged installations require `root` permissions, and are most appropriate for
+production installs.
+Install the RPM build on CentOS and RHEL-based distributions if you want to
+install Cassandra using YUM.
+Install the Debian build on Ubuntu and other Debian-based
+distributions if you want to install Cassandra using APT.
+Note that both the YUM and APT methods required `root` permissions and
+will install the binaries and configuration files as the `cassandra` OS user.
+
+[#docker]
+== Installing the docker image
+
+[arabic, start=1]
+. Pull the docker image. For the latest image, use:
+
+[source, shell, subs="attributes+"]
+----
+include::example$BASH/docker_pull.sh[]
+----
+
+This `docker pull` command will get the specified version of the 'Docker Official'
+Apache Cassandra image available from the https://hub.docker.com/_/cassandra[Dockerhub].
+
+[arabic, start=2]
+. Start Cassandra with a `docker run` command:
+
+[source, shell, subs="attributes+"]
+----
+include::example$BASH/docker_run.sh[]
+----
+
+The `--name` option will be the name of the Cassandra cluster created.
+
+[arabic, start=3]
+. Start the CQL shell, `cqlsh` to interact with the Cassandra node created:
+
+[source, shell, subs="attributes+"]
+----
+include::example$BASH/docker_cqlsh.sh[]
+----
+
+[#tarball]
+== Installing the binary tarball
+
+include::partial$java_version.adoc[]
+
+[arabic, start=2]
+. Download the binary tarball from one of the mirrors on the
+{cass_url}download/[Apache Cassandra Download] site.
+For example, to download Cassandra {40_version}:
+
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/curl_install.sh[]
+----
+
+NOTE: The mirrors only host the latest versions of each major supported
+release. To download an earlier version of Cassandra, visit the
+http://archive.apache.org/dist/cassandra/[Apache Archives].
+
+[arabic, start=3]
+. OPTIONAL: Verify the integrity of the downloaded tarball using one of
+the methods https://www.apache.org/dyn/closer.cgi#verify[here]. For
+example, to verify the hash of the downloaded file using GPG:
+
+[{tabs}]
+====
+Command::
++
+--
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/verify_gpg.sh[]
+----
+--
+
+Result::
++
+--
+[source,plaintext]
+----
+include::example$RESULTS/verify_gpg.result[]
+----
+--
+====
+
+Compare the signature with the SHA256 file from the Downloads site:
+
+[{tabs}]
+====
+Command::
++
+--
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/curl_verify_sha.sh[]
+----
+--
+
+Result::
++
+--
+[source,plaintext]
+----
+include::example$RESULTS/curl_verify_sha.result[]
+----
+--
+====
+
+[arabic, start=4]
+. Unpack the tarball:
+
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/tarball.sh[]
+----
+
+The files will be extracted to the `apache-cassandra-{cass-tag-3x}/` directory.
+This is the tarball installation location.
+
+[arabic, start=5]
+. Located in the tarball installation location are the directories for
+the scripts, binaries, utilities, configuration, data and log files:
+
+[source,plaintext]
+----
+include::example$TEXT/tarball_install_dirs.txt[]
+----
+<1> location of the commands to run cassandra, cqlsh, nodetool, and SSTable tools
+<2> location of cassandra.yaml and other configuration files
+<3> location of the commit logs, hints, and SSTables
+<4> location of system and debug logs
+<5>location of cassandra-stress tool
+
+For information on how to configure your installation, see
+{cass_url}doc/latest/getting_started/configuring.html[Configuring
+Cassandra].
+
+[arabic, start=6]
+. Start Cassandra:
+
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/start_tarball.sh[]
+----
+
+NOTE: This will run Cassandra as the authenticated Linux user.
+
+include::partial$tail_syslog.adoc[]
+You can monitor the progress of the startup with:
+
+[{tabs}]
+====
+Command::
++
+--
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/tail_syslog.sh[]
+----
+--
+
+Result::
++
+--
+Cassandra is ready when you see an entry like this in the `system.log`:
+
+[source,plaintext]
+----
+include::example$RESULTS/tail_syslog.result[]
+----
+--
+====
+
+include::partial$nodetool_and_cqlsh.adoc[]
+
+[#package]
+== Installing the Debian packages
+
+include::partial$java_version.adoc[]
+
+[arabic, start=2]
+. Add the Apache repository of Cassandra to the file
+`cassandra.sources.list`.
+include::partial$package_versions.adoc[]
+
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/get_deb_package.sh[]
+----
+
+[arabic, start=3]
+. Add the Apache Cassandra repository keys to the list of trusted keys
+on the server:
+
+[{tabs}]
+====
+Command::
++
+--
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/add_repo_keys.sh[]
+----
+--
+
+Result::
++
+--
+[source,plaintext]
+----
+include::example$RESULTS/add_repo_keys.result[]
+----
+--
+====
+
+[arabic, start=4]
+. Update the package index from sources:
+
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/apt-get_update.sh[]
+----
+
+[arabic, start=5]
+. Install Cassandra with APT:
+
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/apt-get_cass.sh[]
+----
+
+NOTE: For information on how to configure your installation, see
+{cass_url}doc/latest/getting_started/configuring.html[Configuring
+Cassandra].
+
+include::partial$tail_syslog.adoc[]
+
+include::partial$nodetool_and_cqlsh_nobin.adoc[]
+
+== Installing the RPM packages
+
+include::partial$java_version.adoc[]
+
+[arabic, start=2]
+. Add the Apache repository of Cassandra to the file
+`/etc/yum.repos.d/cassandra.repo` (as the `root` user).
+include::partial$package_versions.adoc[]
+
+[source,plaintext]
+----
+include::example$RESULTS/add_yum_repo.result[]
+----
+
+[arabic, start=3]
+. Update the package index from sources:
+
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/yum_update.sh[]
+----
+
+[arabic, start=4]
+. Install Cassandra with YUM:
+
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/yum_cass.sh[]
+----
+
+NOTE: A new Linux user `cassandra` will get created as part of the
+installation. The Cassandra service will also be run as this user.
+
+[arabic, start=5]
+. Start the Cassandra service:
+
+[source,shell, subs="attributes+"]
+----
+include::example$BASH/yum_start.sh[]
+----
+
+include::partial$tail_syslog.adoc[]
+
+include::partial$nodetool_and_cqlsh_nobin.adoc[]
+
+== Further installation info
+
+For help with installation issues, see the
+{cass_url}doc/latest/troubleshooting/index.html[Troubleshooting]
+section.
diff --git a/doc/modules/cassandra/pages/getting_started/production.adoc b/doc/modules/cassandra/pages/getting_started/production.adoc
new file mode 100644
index 0000000..93b2608
--- /dev/null
+++ b/doc/modules/cassandra/pages/getting_started/production.adoc
@@ -0,0 +1,163 @@
+= Production recommendations
+
+The `cassandra.yaml` and `jvm.options` files have a number of notes and
+recommendations for production usage.
+This page expands on some of the information in the files.
+
+== Tokens
+
+Using more than one token-range per node is referred to as virtual nodes, or vnodes.
+`vnodes` facilitate flexible expansion with more streaming peers when a new node bootstraps
+into a cluster.
+Limiting the negative impact of streaming (I/O and CPU overhead) enables incremental cluster expansion.
+However, more tokens leads to sharing data with more peers, and results in decreased availability.
+These two factors must be balanced based on a cluster's characteristic reads and writes.
+To learn more,
+https://github.com/jolynch/python_performance_toolkit/raw/master/notebooks/cassandra_availability/whitepaper/cassandra-availability-virtual.pdf[Cassandra Availability in Virtual Nodes, Joseph Lynch and Josh Snyder] is recommended reading.
+
+Change the number of tokens using the setting in the `cassandra.yaml` file:
+
+`num_tokens: 16`
+
+Here are the most common token counts with a brief explanation of when
+and why you would use each one.
+
+[width="100%",cols="13%,87%",options="header",]
+|===
+|Token Count |Description
+|1 |Maximum availablility, maximum cluster size, fewest peers, but
+inflexible expansion. Must always double size of cluster to expand and
+remain balanced.
+
+|4 |A healthy mix of elasticity and availability. Recommended for
+clusters which will eventually reach over 30 nodes. Requires adding
+approximately 20% more nodes to remain balanced. Shrinking a cluster may
+result in cluster imbalance.
+
+|8 | Using 8 vnodes distributes the workload between systems with a ~10% variance
+and has minimal impact on performance.
+
+|16 |Best for heavily elastic clusters which expand and shrink
+regularly, but may have issues availability with larger clusters. Not
+recommended for clusters over 50 nodes.
+|===
+
+In addition to setting the token count, it's extremely important that
+`allocate_tokens_for_local_replication_factor` in `cassandra.yaml` is set to an
+appropriate number of replicates, to ensure even token allocation.
+
+== Read ahead
+
+Read ahead is an operating system feature that attempts to keep as much
+data as possible loaded in the page cache.
+Spinning disks can have long seek times causing high latency, so additional
+throughput on reads using page cache can improve performance.
+By leveraging read ahead, the OS can pull additional data into memory without
+the cost of additional seeks.
+This method works well when the available RAM is greater than the size of the
+hot dataset, but can be problematic when the reverse is true (dataset > RAM).
+The larger the hot dataset, the less read ahead is useful.
+
+Read ahead is definitely not useful in the following cases:
+
+* Small partitions, such as tables with a single partition key
+* Solid state drives (SSDs)
+
+
+Read ahead can actually increase disk usage, and in some cases result in as much
+as a 5x latency and throughput performance penalty.
+Read-heavy, key/value tables with small (under 1KB) rows are especially prone
+to this problem.
+
+The recommended read ahead settings are:
+
+[width="59%",cols="40%,60%",options="header",]
+|===
+|Hardware |Initial Recommendation
+|Spinning Disks |64KB
+|SSD |4KB
+|===
+
+Read ahead can be adjusted on Linux systems using the `blockdev` tool.
+
+For example, set the read ahead of the disk `/dev/sda1\` to 4KB:
+
+[source, shell]
+----
+$ blockdev --setra 8 /dev/sda1
+----
+[NOTE]
+====
+The `blockdev` setting sets the number of 512 byte sectors to read ahead.
+The argument of 8 above is equivalent to 4KB, or 8 * 512 bytes.
+====
+
+All systems are different, so use these recommendations as a starting point and
+tune, based on your SLA and throughput requirements.
+To understand how read ahead impacts disk resource usage, we recommend carefully
+reading through the xref:troubleshooting/use_tools.adoc[Diving Deep, using external tools]
+section.
+
+== Compression
+
+Compressed data is stored by compressing fixed size byte buffers and writing the
+data to disk.
+The buffer size is determined by the `chunk_length_in_kb` element in the compression
+map of a table's schema settings for `WITH COMPRESSION`.
+The default setting is 16KB starting with Cassandra {40_version}.
+
+Since the entire compressed buffer must be read off-disk, using a compression
+chunk length that is too large can lead to significant overhead when reading small records.
+Combined with the default read ahead setting, the result can be massive
+read amplification for certain workloads. Therefore, picking an appropriate
+value for this setting is important.
+
+LZ4Compressor is the default and recommended compression algorithm.
+If you need additional information on compression, read
+https://thelastpickle.com/blog/2018/08/08/compression_performance.html[The Last Pickle blogpost on compression performance].
+
+== Compaction
+
+There are different xref:compaction/index.adoc[compaction] strategies available
+for different workloads.
+We recommend reading about the different strategies to understand which is the
+best for your environment.
+Different tables may, and frequently do use different compaction strategies in
+the same cluster.
+
+== Encryption
+
+It is significantly better to set up peer-to-peer encryption and client server
+encryption when setting up your production cluster.
+Setting it up after the cluster is serving production traffic is challenging
+to do correctly.
+If you ever plan to use network encryption of any type, we recommend setting it
+up when initially configuring your cluster.
+Changing these configurations later is not impossible, but mistakes can
+result in downtime or data loss.
+
+== Ensure keyspaces are created with NetworkTopologyStrategy
+
+Production clusters should never use `SimpleStrategy`.
+Production keyspaces should use the `NetworkTopologyStrategy` (NTS).
+For example:
+
+[source, cql]
+----
+CREATE KEYSPACE mykeyspace WITH replication =     {
+   'class': 'NetworkTopologyStrategy',
+   'datacenter1': 3
+};
+----
+
+Cassandra clusters initialized with `NetworkTopologyStrategy` can take advantage
+of the ability to configure multiple racks and data centers.
+
+== Configure racks and snitch
+
+**Correctly configuring or changing racks after a cluster has been provisioned is an unsupported process**.
+Migrating from a single rack to multiple racks is also unsupported and can
+result in data loss.
+Using `GossipingPropertyFileSnitch` is the most flexible solution for on
+premise or mixed cloud environments.
+`Ec2Snitch` is reliable for AWS EC2 only environments.
diff --git a/doc/modules/cassandra/pages/getting_started/querying.adoc b/doc/modules/cassandra/pages/getting_started/querying.adoc
new file mode 100644
index 0000000..a8b348a
--- /dev/null
+++ b/doc/modules/cassandra/pages/getting_started/querying.adoc
@@ -0,0 +1,31 @@
+= Inserting and querying
+
+The API for Cassandra is xref:cql/ddl.adoc[`CQL`, the Cassandra Query Language]. To
+use CQL, you will need to connect to the cluster, using either:
+
+* `cqlsh`, a shell for CQL
+* a client driver for Cassandra
+* for the adventurous, check out https://zeppelin.apache.org/docs/0.7.0/interpreter/cassandra.html[Apache Zeppelin], a notebook-style tool
+
+== CQLSH
+
+`cqlsh` is a command-line shell for interacting with Cassandra using
+CQL. It is shipped with every Cassandra package, and can be found in the
+`bin` directory alongside the `cassandra` executable. It connects to the
+single node specified on the command line. For example:
+
+[source, shell]
+----
+include::example$BASH/cqlsh_localhost.sh[]
+----
+[source, cql]
+----
+include::example$RESULTS/cqlsh_localhost.result[]
+----
+If the command is used without specifying a node, `localhost` is the default. See the xref:tools/cqlsh.adoc[`cqlsh` section] for full documentation.
+
+== Client drivers
+
+A lot of xref:getting_started/drivers.adoc[client drivers] are provided by the Community and a list of
+known drivers is provided. You should refer to the documentation of each driver
+for more information.
diff --git a/doc/modules/cassandra/pages/getting_started/quickstart.adoc b/doc/modules/cassandra/pages/getting_started/quickstart.adoc
new file mode 100644
index 0000000..0f2e5b0
--- /dev/null
+++ b/doc/modules/cassandra/pages/getting_started/quickstart.adoc
@@ -0,0 +1,100 @@
+= Apache Cassandra Quickstart
+:tabs:
+
+_Interested in getting started with Cassandra? Follow these instructions._
+
+*STEP 1: GET CASSANDRA USING DOCKER*
+
+You'll need to have Docker Desktop for Mac, Docker Desktop for Windows, or
+similar software installed on your computer.
+
+[source, shell, subs="attributes+"]
+----
+include::example$BASH/docker_pull.sh[]
+----
+
+Apache Cassandra is also available as a https://cassandra.apache.org/download/[tarball or package download].
+
+*STEP 2: START CASSANDRA*
+
+[source, shell, subs="attributes+"]
+----
+include::example$BASH/docker_run.sh[]
+----
+
+*STEP 3: CREATE FILES*
+
+In the directory where you plan to run the next step, create these two files
+so that some data can be automatically inserted in the next step.
+
+A _cqlshrc_ file will log into the Cassandra database with the default superuser:
+
+[source, plaintext]
+----
+[authentication]
+	username = cassandra
+	password = cassandra
+----
+
+Create a _scripts_ directory and change to that directory.
+The following _data.cql_ file will create a keyspace, the layer at which Cassandra
+replicates its data, a table to hold the data, and insert some data:
+
+[source, shell, subs="attributes+"]
+----
+include::example$CQL/qs_create_ks.cql[]
+include::example$CQL/qs_create_table.cql[]
+include::example$CQL/qs_insert_data.cql[]
+----
+
+You should now have a _cqlshrc_ file and _<currentdir>/scripts/data.cql_ file.
+
+*STEP 4: RUN CQLSH TO INTERACT*
+
+Cassandra is a distributed database that can read and write data across multiple
+nodes with  peer-to-peer replication. The Cassandra Query Language (CQL) is
+similar to SQL but suited for the JOINless structure of Cassandra. The CQL
+shell, or `cqlsh`, is one tool to use in interacting with the database.
+
+[source, shell, subs="attributes+"]
+----
+include::example$BASH/docker_run_qs.sh[]
+----
+
+For this quickstart, this cqlsh docker image also loads some data automatically,
+so you can start running queries.
+
+*STEP 5: READ SOME DATA*
+
+[source, shell, subs="attributes+"]
+----
+include::example$CQL/qs_select_data.cql[]
+----
+
+*STEP 6: WRITE SOME MORE DATA*
+
+[source, shell, subs="attributes+"]
+----
+include::example$CQL/qs_insert_data_again.cql[]
+----
+
+*STEP 7: TERMINATE CASSANDRA*
+
+[source, shell, subs="attributes+"]
+----
+include::example$BASH/docker_remove.sh[]
+----
+
+*CONGRATULATIONS!*
+
+Hey, that wasn't so hard, was it?
+
+To learn more, we suggest the following next steps:
+
+* Read through the *need link*[Overview] to learn main concepts and how Cassandra works at a
+high level.
+* To understand Cassandra in more detail, head over to the
+https://cassandra.apache.org/doc/latest/[Docs].
+* Browse through the https://cassandra.apache.org/case-studies/[Case Studies] to
+learn how other users in our worldwide community are getting value out of
+Cassandra.
diff --git a/doc/modules/cassandra/pages/operating/audit_logging.adoc b/doc/modules/cassandra/pages/operating/audit_logging.adoc
new file mode 100644
index 0000000..345e204
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/audit_logging.adoc
@@ -0,0 +1,224 @@
+= Audit Logging
+
+Audit logging in Cassandra logs every incoming CQL command request,
+as well as authentication (successful/unsuccessful login) to a Cassandra node.
+Currently, there are two implementations provided. 
+The custom logger can be implemented and injected with the class name as a parameter in
+the `cassandra.yaml` file.
+
+* `BinAuditLogger`: an efficient way to log events to file in a binary
+format (community-recommended logger for performance)
+* `FileAuditLogger`: logs events to `audit/audit.log` file using slf4j
+logger
+
+== What does audit logging captures
+
+Audit logging captures following events:
+
+* Successful as well as unsuccessful login attempts
+* All database commands executed via native CQL protocol attempted or
+successfully executed
+
+== Limitations
+
+Executing prepared statements will log the query as provided by the
+client in the prepare call, along with the execution timestamp and all
+other attributes (see below). 
+Actual values bound for prepared statement execution will not show up in the audit log.
+
+== What does audit logging logs
+
+Each audit log implementation has access to the following attributes,
+and for the default text based logger these fields are concatenated with
+pipes to yield the final message.
+
+* `user`: User name(if available)
+* `host`: Host IP, where the command is being executed
+* `source ip address`: Source IP address from where the request initiated
+* `source port`: Source port number from where the request initiated
+* `timestamp`: unix time stamp
+* `type`: Type of the request (SELECT, INSERT, etc.,)
+* `category` - Category of the request (DDL, DML, etc.,)
+* `keyspace` - Keyspace(If applicable) on which request is targeted to
+be executed
+* `scope` - Table/Aggregate name/ function name/ trigger name etc., as
+applicable
+* `operation` - CQL command being executed
+
+== How to configure
+
+Auditlog can be configured using the `cassandra.yaml` file. 
+To use audit logging on one node, either edit that file or enable and configure using `nodetool`.
+
+=== cassandra.yaml configurations for AuditLog
+
+The following options are supported:
+
+* `enabled`: This option enables/ disables audit log
+* `logger`: Class name of the logger/ custom logger.
+* `audit_logs_dir`: Auditlogs directory location, if not set, default to
+[.title-ref]#cassandra.logdir.audit# or [.title-ref]#cassandra.logdir# +
+/audit/
+* `included_keyspaces`: Comma separated list of keyspaces to be included
+in audit log, default - includes all keyspaces
+* `excluded_keyspaces`: Comma separated list of keyspaces to be excluded
+from audit log, default - excludes no keyspace except
+[.title-ref]#system#, [.title-ref]#system_schema# and
+[.title-ref]#system_virtual_schema#
+* `included_categories`: Comma separated list of Audit Log Categories to
+be included in audit log, default - includes all categories
+* `excluded_categories`: Comma separated list of Audit Log Categories to
+be excluded from audit log, default - excludes no category
+* `included_users`: Comma separated list of users to be included in
+audit log, default - includes all users
+* `excluded_users`: Comma separated list of users to be excluded from
+audit log, default - excludes no user
+
+List of available categories are: QUERY, DML, DDL, DCL, OTHER, AUTH,
+ERROR, PREPARE
+
+=== NodeTool command to enable AuditLog
+
+The `nodetool enableauditlog` command enables AuditLog with the `cassandra.yaml` file defaults. 
+Those defaults can be overridden using options with this nodetool command.
+
+[source,none]
+----
+nodetool enableauditlog
+----
+
+==== Options
+
+`--excluded-categories`::
+  Comma separated list of Audit Log Categories to be excluded for audit
+  log. If not set the value from cassandra.yaml will be used
+`--excluded-keyspaces`::
+  Comma separated list of keyspaces to be excluded for audit log. If not
+  set the value from cassandra.yaml will be used. Please remeber that
+  [.title-ref]#system#, [.title-ref]#system_schema# and
+  [.title-ref]#system_virtual_schema# are excluded by default, if you
+  are overwriting this option via nodetool, remember to add these
+  keyspaces back if you dont want them in audit logs
+`--excluded-users`::
+  Comma separated list of users to be excluded for audit log. If not set
+  the value from cassandra.yaml will be used
+`--included-categories`::
+  Comma separated list of Audit Log Categories to be included for audit
+  log. If not set the value from cassandra.yaml will be used
+`--included-keyspaces`::
+  Comma separated list of keyspaces to be included for audit log. If not
+  set the value from cassandra.yaml will be used
+`--included-users`::
+  Comma separated list of users to be included for audit log. If not set
+  the value from cassandra.yaml will be used
+`--logger`::
+  Logger name to be used for AuditLogging. Default BinAuditLogger. If
+  not set the value from cassandra.yaml will be used
+
+=== NodeTool command to disable AuditLog
+
+The `nodetool disableauditlog` command disables AuditLog.
+
+[source,none]
+----
+nodetool disableuditlog
+----
+
+=== NodeTool command to reload AuditLog filters
+
+The `nodetool enableauditlog` command can be used to reload auditlog filters with either defaults or previous `loggername` and
+updated filters:
+
+[source,none]
+----
+nodetool enableauditlog --loggername <Default/ existing loggerName> --included-keyspaces <New Filter values>
+----
+
+== View the contents of AuditLog Files
+
+The `auditlogviewer` is used to view the contents of the audit binlog file in human readable text format.
+
+[source,none]
+----
+auditlogviewer <path1> [<path2>...<pathN>] [options]
+----
+
+=== Options
+
+`-f,--follow`::
+  Upon reacahing the end of the log continue indefinitely;;
+    waiting for more records
+`-r,--roll_cycle`::
+  How often to roll the log file was rolled. May be;;
+    necessary for Chronicle to correctly parse file names. (MINUTELY,
+    HOURLY, DAILY). Default HOURLY.
+`-h,--help`::
+  display this help message
+
+For example, to dump the contents of audit log files to the console:
+
+[source,none]
+----
+auditlogviewer /logs/cassandra/audit
+----
+
+results in
+
+[source,none]
+----
+LogMessage: user:anonymous|host:localhost/X.X.X.X|source:/X.X.X.X|port:60878|timestamp:1521158923615|type:USE_KS|category:DDL|ks:dev1|operation:USE "dev1"
+----
+
+== Configuring BinAuditLogger
+
+To use `BinAuditLogger` as a logger in AuditLogging, set the logger to `BinAuditLogger` in the `cassandra.yaml` file
+ under the `audit_logging_options` section. 
+`BinAuditLogger` can be futher configued using its advanced options in `cassandra.yaml`.
+
+=== Advanced Options for BinAuditLogger
+
+`block`::
+  Indicates if the AuditLog should block if the it falls behind or
+  should drop audit log records. Default is set to `true` so that
+  AuditLog records wont be lost
+`max_queue_weight`::
+  Maximum weight of in memory queue for records waiting to be written to
+  the audit log file before blocking or dropping the log records.
+  Default is set to `256 * 1024 * 1024`
+`max_log_size`::
+  Maximum size of the rolled files to retain on disk before deleting the
+  oldest file. Default is set to `16L * 1024L * 1024L * 1024L`
+`roll_cycle`::
+  How often to roll Audit log segments so they can potentially be
+  reclaimed. Available options are: MINUTELY, HOURLY, DAILY,
+  LARGE_DAILY, XLARGE_DAILY, HUGE_DAILY.For more options, refer:
+  net.openhft.chronicle.queue.RollCycles. Default is set to `"HOURLY"`
+
+== Configuring FileAuditLogger
+
+To use `FileAuditLogger` as a logger in AuditLogging, set the class name in the `cassandra.yaml` file and configure
+the audit log events to flow through separate log file instead of system.log.
+
+[source,xml]
+----
+<!-- Audit Logging (FileAuditLogger) rolling file appender to audit.log -->
+<appender name="AUDIT" class="ch.qos.logback.core.rolling.RollingFileAppender">
+  <file>${cassandra.logdir}/audit/audit.log</file>
+  <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+    <!-- rollover daily -->
+    <fileNamePattern>${cassandra.logdir}/audit/audit.log.%d{yyyy-MM-dd}.%i.zip</fileNamePattern>
+    <!-- each file should be at most 50MB, keep 30 days worth of history, but at most 5GB -->
+    <maxFileSize>50MB</maxFileSize>
+    <maxHistory>30</maxHistory>
+    <totalSizeCap>5GB</totalSizeCap>
+  </rollingPolicy>
+  <encoder>
+    <pattern>%-5level [%thread] %date{ISO8601} %F:%L - %msg%n</pattern>
+  </encoder>
+</appender>
+
+<!-- Audit Logging additivity to redirect audt logging events to audit/audit.log -->
+<logger name="org.apache.cassandra.audit" additivity="false" level="INFO">
+    <appender-ref ref="AUDIT"/>
+</logger>
+----
diff --git a/doc/modules/cassandra/pages/operating/backups.adoc b/doc/modules/cassandra/pages/operating/backups.adoc
new file mode 100644
index 0000000..a083d5b
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/backups.adoc
@@ -0,0 +1,517 @@
+= Backups
+
+Apache Cassandra stores data in immutable SSTable files. Backups in
+Apache Cassandra database are backup copies of the database data that is
+stored as SSTable files. Backups are used for several purposes including
+the following:
+
+* To store a data copy for durability
+* To be able to restore a table if table data is lost due to
+node/partition/network failure
+* To be able to transfer the SSTable files to a different machine; for
+portability
+
+== Types of Backups
+
+Apache Cassandra supports two kinds of backup strategies.
+
+* Snapshots
+* Incremental Backups
+
+A _snapshot_ is a copy of a table’s SSTable files at a given time,
+created via hard links. 
+The DDL to create the table is stored as well.
+Snapshots may be created by a user or created automatically. 
+The setting `snapshot_before_compaction` in the `cassandra.yaml` file determines if
+snapshots are created before each compaction. 
+By default, `snapshot_before_compaction` is set to false. 
+Snapshots may be created automatically before keyspace truncation or dropping of a table by
+setting `auto_snapshot` to true (default) in `cassandra.yaml`. 
+Truncates could be delayed due to the auto snapshots and another setting in
+`cassandra.yaml` determines how long the coordinator should wait for
+truncates to complete. 
+By default Cassandra waits 60 seconds for auto snapshots to complete.
+
+An _incremental backup_ is a copy of a table’s SSTable files created by
+a hard link when memtables are flushed to disk as SSTables. 
+Typically incremental backups are paired with snapshots to reduce the backup time
+as well as reduce disk space. 
+Incremental backups are not enabled by default and must be enabled explicitly in `cassandra.yaml` (with
+`incremental_backups` setting) or with `nodetool`. 
+Once enabled, Cassandra creates a hard link to each SSTable flushed or streamed
+locally in a `backups/` subdirectory of the keyspace data. 
+Incremental backups of system tables are also created.
+
+== Data Directory Structure
+
+The directory structure of Cassandra data consists of different
+directories for keyspaces, and tables with the data files within the
+table directories. 
+Directories backups and snapshots to store backups
+and snapshots respectively for a particular table are also stored within
+the table directory. 
+The directory structure for Cassandra is illustrated in Figure 1.
+
+image::Figure_1_backups.jpg[Data directory structure for backups]
+
+Figure 1. Directory Structure for Cassandra Data
+
+=== Setting Up Example Tables for Backups and Snapshots
+
+In this section we shall create some example data that could be used to
+demonstrate incremental backups and snapshots. 
+We have used a three node Cassandra cluster. 
+First, the keyspaces are created. 
+Then tables are created within a keyspace and table data is added. 
+We have used two keyspaces `cqlkeyspace` and `catalogkeyspace` with two tables within
+each. 
+
+Create the keyspace `cqlkeyspace`:
+
+[source,cql]
+----
+include::example$CQL/create_ks_backup.cql[]
+----
+
+Create two tables `t` and `t2` in the `cqlkeyspace` keyspace.
+
+[source,cql]
+----
+include::example$CQL/create_table_backup.cql[]
+----
+
+Add data to the tables: 
+
+[source,cql]
+----
+include::example$CQL/insert_data_backup.cql[]
+----
+
+Query the table to list the data:
+
+[source,cql]
+----
+include::example$CQL/select_data_backup.cql[]
+----
+
+results in
+
+[source,cql]
+----
+include::example$RESULTS/select_data_backup.result[]
+----
+
+Create a second keyspace `catalogkeyspace`:
+
+[source,cql]
+----
+include::example$CQL/create_ks2_backup.cql[]
+----
+
+Create two tables `journal`  and `magazine` in `catalogkeyspace`:
+
+[source,cql]
+----
+include::example$CQL/create_table2_backup.cql[]
+----
+
+Add data to the tables:
+
+[source,cql]
+----
+include::example$CQL/insert_data2_backup.cql[]
+----
+
+Query the tables to list the data:
+
+[source,cql]
+----
+include::example$CQL/select_data2_backup.cql[]
+----
+
+results in 
+
+[source,cql]
+----
+include::example$RESULTS/select_data2_backup.result[]
+----
+
+== Snapshots
+
+In this section, we demonstrate creating snapshots. 
+The command used to create a snapshot is `nodetool snapshot` with the usage:
+
+[source,bash]
+----
+include::example$BASH/nodetool_snapshot.sh[]
+----
+
+results in
+
+[source, plaintext]
+----
+include::example$RESULTS/nodetool_snapshot_help.result[]
+----
+
+=== Configuring for Snapshots
+
+To demonstrate creating snapshots with Nodetool on the commandline we
+have set `auto_snapshots` setting to `false` in the `cassandra.yaml` file:
+
+[source,yaml]
+----
+include::example$YAML/auto_snapshot.yaml[]
+----
+
+Also set `snapshot_before_compaction` to `false` to disable creating
+snapshots automatically before compaction:
+
+[source,yaml]
+----
+include::example$YAML/snapshot_before_compaction.yaml[]
+----
+
+=== Creating Snapshots
+
+Before creating any snapshots, search for snapshots and none will be listed:
+
+[source,bash]
+----
+include::example$BASH/find_snapshots.sh[]
+----
+
+We shall be using the example keyspaces and tables to create snapshots.
+
+==== Taking Snapshots of all Tables in a Keyspace
+
+Using the syntax above, create a snapshot called `catalog-ks` for all the tables
+in the `catalogkeyspace` keyspace:
+
+[source,bash]
+----
+include::example$BASH/snapshot_backup2.sh[]
+----
+
+results in
+
+[source,none]
+----
+include::example$RESULTS/snapshot_backup2.result[]
+----
+
+Using the `find` command above, the snapshots and `snapshots` directories 
+are now found with listed files similar to:
+
+[source, plaintext]
+----
+include::example$RESULTS/snapshot_backup2_find.result[]
+----
+
+Snapshots of all tables in multiple keyspaces may be created similarly:
+
+[source,bash]
+----
+include::example$BASH/snapshot_both_backups.sh[]
+----
+
+==== Taking Snapshots of Single Table in a Keyspace
+
+To take a snapshot of a single table the `nodetool snapshot` command
+syntax becomes as follows:
+
+[source,bash]
+----
+include::example$BASH/snapshot_one_table.sh[]
+----
+
+Using the syntax above, create a snapshot for table `magazine` in keyspace `catalogkeyspace`:
+
+[source,bash]
+----
+include::example$BASH/snapshot_one_table2.sh[]
+----
+
+results in
+ 
+[source, plaintext]
+----
+include::example$RESULTS/snapshot_one_table2.result[]
+----
+
+==== Taking Snapshot of Multiple Tables from same Keyspace
+
+To take snapshots of multiple tables in a keyspace the list of
+_Keyspace.table_ must be specified with option `--kt-list`. 
+For example, create snapshots for tables `t` and `t2` in the `cqlkeyspace` keyspace:
+
+[source,bash]
+----
+include::example$BASH/snapshot_mult_tables.sh[]
+----
+
+results in
+
+[source,plaintext]
+----
+include::example$RESULTS/snapshot_mult_tables.result[]
+----
+
+Multiple snapshots of the same set of tables may be created and tagged with a different name. 
+As an example, create another snapshot for the same set of tables `t` and `t2` in the `cqlkeyspace` 
+keyspace and tag the snapshots differently:
+
+[source,bash]
+----
+include::example$BASH/snapshot_mult_tables_again.sh[]
+----
+
+results in
+
+[source, plaintext]
+----
+include::example$RESULTS/snapshot_mult_tables_again.result[]
+----
+
+==== Taking Snapshot of Multiple Tables from Different Keyspaces
+
+To take snapshots of multiple tables that are in different keyspaces the
+command syntax is the same as when multiple tables are in the same
+keyspace. 
+Each <keyspace>.<table> must be specified separately in the
+`--kt-list` option. 
+
+For example, create a snapshot for table `t` in
+the `cqlkeyspace` and table `journal` in the catalogkeyspace and tag the
+snapshot `multi-ks`.
+
+[source,bash]
+----
+include::example$BASH/snapshot_mult_ks.sh[]
+----
+
+results in
+[source, plaintext]
+----
+include::example$RESULTS/snapshot_mult_ks.result[]
+----
+
+=== Listing Snapshots
+
+To list snapshots use the `nodetool listsnapshots` command. All the
+snapshots that we created in the preceding examples get listed:
+
+[source,bash]
+----
+include::example$BASH/nodetool_list_snapshots.sh[]
+----
+
+results in
+
+[source, plaintext]
+----
+include::example$RESULTS/nodetool_list_snapshots.result[]
+----
+
+=== Finding Snapshots Directories
+
+The `snapshots` directories may be listed with `find –name snapshots`
+command:
+
+[source,bash]
+----
+include::example$BASH/find_snapshots.sh[]
+----
+
+results in
+
+[source, plaintext]
+----
+include::example$RESULTS/snapshot_all.result[]
+----
+
+To list the snapshots for a particular table first change to the snapshots directory for that table. 
+For example, list the snapshots for the `catalogkeyspace/journal` table:
+
+[source,bash]
+----
+include::example$BASH/find_two_snapshots.sh[]
+----
+
+results in
+
+[source, plaintext]
+----
+include::example$RESULTS/find_two_snapshots.result[]
+----
+
+A `snapshots` directory lists the SSTable files in the snapshot.
+A `schema.cql` file is also created in each snapshot that defines schema
+that can recreate the table with CQL when restoring from a snapshot:
+
+[source,bash]
+----
+include::example$BASH/snapshot_files.sh[]
+----
+
+results in
+
+[source, plaintext]
+----
+include::example$RESULTS/snapshot_files.result[]
+----
+
+=== Clearing Snapshots
+
+Snapshots may be cleared or deleted with the `nodetool clearsnapshot`
+command. Either a specific snapshot name must be specified or the `–all`
+option must be specified. 
+
+For example, delete a snapshot called `magazine` from keyspace `cqlkeyspace`:
+
+[source,bash]
+----
+include::example$BASH/nodetool_clearsnapshot.sh[]
+----
+
+or delete all snapshots from `cqlkeyspace` with the –all option:
+
+[source,bash]
+----
+include::example$BASH/nodetool_clearsnapshot_all.sh[]
+----
+
+== Incremental Backups
+
+In the following sections, we shall discuss configuring and creating
+incremental backups.
+
+=== Configuring for Incremental Backups
+
+To create incremental backups set `incremental_backups` to `true` in
+`cassandra.yaml`.
+
+[source,yaml]
+----
+include::example$YAML/incremental_bups.yaml[]
+----
+
+This is the only setting needed to create incremental backups. 
+By default `incremental_backups` setting is set to `false` because a new
+set of SSTable files is created for each data flush and if several CQL
+statements are to be run the `backups` directory could fill up quickly
+and use up storage that is needed to store table data. 
+Incremental backups may also be enabled on the command line with the nodetool
+command `nodetool enablebackup`. 
+Incremental backups may be disabled with `nodetool disablebackup` command. 
+Status of incremental backups, whether they are enabled may be checked with `nodetool statusbackup`.
+
+=== Creating Incremental Backups
+
+After each table is created flush the table data with `nodetool flush`
+command. Incremental backups get created.
+
+[source,bash]
+----
+include::example$BASH/nodetool_flush.sh[]
+----
+
+=== Finding Incremental Backups
+
+Incremental backups are created within the Cassandra’s `data` directory
+within a table directory. Backups may be found with following command.
+
+[source,bash]
+----
+include::example$BASH/find_backups.sh[]
+----
+results in
+[source,none]
+----
+include::example$RESULTS/find_backups.result[]
+----
+
+=== Creating an Incremental Backup
+
+This section discusses how incremental backups are created in more
+detail using the keyspace and table previously created.
+
+Flush the keyspace and table:
+
+[source,bash]
+----
+include::example$BASH/nodetool_flush_table.sh[]
+----
+
+A search for backups and a `backups` directory will list a backup directory,
+even if we have added no table data yet.
+
+[source,bash]
+----
+include::example$BASH/find_backups.sh[]
+----
+
+results in
+
+[source,plaintext]
+----
+include::example$RESULTS/find_backups_table.result[]
+----
+
+Checking the `backups` directory will show that there are also no backup files:
+
+[source,bash]
+----
+include::example$BASH/check_backups.sh[]
+----
+
+results in
+
+[source, plaintext]
+----
+include::example$RESULTS/no_bups.result[]
+----
+
+If a row of data is added to the data, running the `nodetool flush` command will
+flush the table data and an incremental backup will be created:
+
+[source,bash]
+----
+include::example$BASH/flush_and_check.sh[]
+----
+
+results in 
+
+[source, plaintext]
+----
+include::example$RESULTS/flush_and_check.result[]
+----
+
+[NOTE]
+.note
+====
+The `backups` directory for any table, such as `cqlkeyspace/t` is created in the
+`data` directory for that table.
+====
+
+Adding another row of data and flushing will result in another set of incremental backup files.
+The SSTable files are timestamped, which distinguishes the first incremental backup from the
+second:
+
+[source,none]
+----
+include::example$RESULTS/flush_and_check2.result[]
+----
+
+== Restoring from Incremental Backups and Snapshots
+
+The two main tools/commands for restoring a table after it has been
+dropped are:
+
+* sstableloader
+* nodetool import
+
+A snapshot contains essentially the same set of SSTable files as an
+incremental backup does with a few additional files. A snapshot includes
+a `schema.cql` file for the schema DDL to create a table in CQL. A table
+backup does not include DDL which must be obtained from a snapshot when
+restoring from an incremental backup.
diff --git a/doc/modules/cassandra/pages/operating/bloom_filters.adoc b/doc/modules/cassandra/pages/operating/bloom_filters.adoc
new file mode 100644
index 0000000..5ce5f8d
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/bloom_filters.adoc
@@ -0,0 +1,64 @@
+= Bloom Filters
+
+In the read path, Cassandra merges data on disk (in SSTables) with data
+in RAM (in memtables). To avoid checking every SSTable data file for the
+partition being requested, Cassandra employs a data structure known as a
+bloom filter.
+
+Bloom filters are a probabilistic data structure that allows Cassandra
+to determine one of two possible states: - The data definitely does not
+exist in the given file, or - The data probably exists in the given
+file.
+
+While bloom filters can not guarantee that the data exists in a given
+SSTable, bloom filters can be made more accurate by allowing them to
+consume more RAM. Operators have the opportunity to tune this behavior
+per table by adjusting the the `bloom_filter_fp_chance` to a float
+between 0 and 1.
+
+The default value for `bloom_filter_fp_chance` is 0.1 for tables using
+LeveledCompactionStrategy and 0.01 for all other cases.
+
+Bloom filters are stored in RAM, but are stored offheap, so operators
+should not consider bloom filters when selecting the maximum heap size.
+As accuracy improves (as the `bloom_filter_fp_chance` gets closer to 0),
+memory usage increases non-linearly - the bloom filter for
+`bloom_filter_fp_chance = 0.01` will require about three times as much
+memory as the same table with `bloom_filter_fp_chance = 0.1`.
+
+Typical values for `bloom_filter_fp_chance` are usually between 0.01
+(1%) to 0.1 (10%) false-positive chance, where Cassandra may scan an
+SSTable for a row, only to find that it does not exist on the disk. The
+parameter should be tuned by use case:
+
+* Users with more RAM and slower disks may benefit from setting the
+`bloom_filter_fp_chance` to a numerically lower number (such as 0.01) to
+avoid excess IO operations
+* Users with less RAM, more dense nodes, or very fast disks may tolerate
+a higher `bloom_filter_fp_chance` in order to save RAM at the expense of
+excess IO operations
+* In workloads that rarely read, or that only perform reads by scanning
+the entire data set (such as analytics workloads), setting the
+`bloom_filter_fp_chance` to a much higher number is acceptable.
+
+== Changing
+
+The bloom filter false positive chance is visible in the
+`DESCRIBE TABLE` output as the field `bloom_filter_fp_chance`. Operators
+can change the value with an `ALTER TABLE` statement: :
+
+[source,none]
+----
+ALTER TABLE keyspace.table WITH bloom_filter_fp_chance=0.01
+----
+
+Operators should be aware, however, that this change is not immediate:
+the bloom filter is calculated when the file is written, and persisted
+on disk as the Filter component of the SSTable. Upon issuing an
+`ALTER TABLE` statement, new files on disk will be written with the new
+`bloom_filter_fp_chance`, but existing sstables will not be modified
+until they are compacted - if an operator needs a change to
+`bloom_filter_fp_chance` to take effect, they can trigger an SSTable
+rewrite using `nodetool scrub` or `nodetool upgradesstables -a`, both of
+which will rebuild the sstables on disk, regenerating the bloom filters
+in the progress.
diff --git a/doc/modules/cassandra/pages/operating/bulk_loading.adoc b/doc/modules/cassandra/pages/operating/bulk_loading.adoc
new file mode 100644
index 0000000..2b11f27
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/bulk_loading.adoc
@@ -0,0 +1,842 @@
+= Bulk Loading
+
+Bulk loading Apache Cassandra data is supported by different tools. 
+The data to bulk load must be in the form of SSTables.
+Cassandra does not support loading data in any other format such as CSV,
+JSON, and XML directly. 
+Although the cqlsh `COPY` command can load CSV data, it is not a good option
+for amounts of data. 
+Bulk loading is used to:
+
+* Restore incremental backups and snapshots. Backups and snapshots are
+already in the form of SSTables.
+* Load existing SSTables into another cluster. The data can have a
+different number of nodes or replication strategy.
+* Load external data to a cluster.
+
+== Tools for Bulk Loading
+
+Cassandra provides two commands or tools for bulk loading data:
+
+* Cassandra Bulk loader, also called `sstableloader`
+* The `nodetool import` command
+
+The `sstableloader` and `nodetool import` are accessible if the
+Cassandra installation `bin` directory is in the `PATH` environment
+variable. 
+Or these may be accessed directly from the `bin` directory. 
+The examples use the keyspaces and tables created in xref:cql/operating/backups.adoc[Backups].
+
+== Using sstableloader
+
+The `sstableloader` is the main tool for bulk uploading data. 
+`sstableloader` streams SSTable data files to a running cluster, 
+conforming to the replication strategy and replication factor. 
+The table to upload data to does need not to be empty.
+
+The only requirements to run `sstableloader` are:
+
+* One or more comma separated initial hosts to connect to and get ring
+information
+* A directory path for the SSTables to load
+
+[source,bash]
+----
+sstableloader [options] <dir_path>
+----
+
+Sstableloader bulk loads the SSTables found in the directory
+`<dir_path>` to the configured cluster. 
+The `<dir_path>` is used as the target _keyspace/table_ name. 
+For example, to load an SSTable named `Standard1-g-1-Data.db` into `Keyspace1/Standard1`, 
+you will need to have the files `Standard1-g-1-Data.db` and `Standard1-g-1-Index.db` in a
+directory `/path/to/Keyspace1/Standard1/`.
+
+=== Sstableloader Option to accept Target keyspace name
+
+Often as part of a backup strategy, some Cassandra DBAs store an entire data directory. 
+When corruption in the data is found, restoring data in the same cluster (for large clusters 200 nodes) 
+is common, but with a different keyspace name.
+
+Currently `sstableloader` derives keyspace name from the folder structure. 
+As an option, to specify target keyspace name as part of `sstableloader`, 
+version 4.0 adds support for the `--target-keyspace` option
+(https://issues.apache.org/jira/browse/CASSANDRA-13884[CASSANDRA-13884]).
+
+The following options are supported, with `-d,--nodes <initial hosts>` required:
+
+[source,none]
+----
+-alg,--ssl-alg <ALGORITHM>                                   Client SSL: algorithm
+
+-ap,--auth-provider <auth provider>                          Custom
+                                                             AuthProvider class name for
+                                                             cassandra authentication
+-ciphers,--ssl-ciphers <CIPHER-SUITES>                       Client SSL:
+                                                             comma-separated list of
+                                                             encryption suites to use
+-cph,--connections-per-host <connectionsPerHost>             Number of
+                                                             concurrent connections-per-host.
+-d,--nodes <initial hosts>                                   Required.
+                                                             Try to connect to these hosts (comma separated) initially for ring information
+
+-f,--conf-path <path to config file>                         cassandra.yaml file path for streaming throughput and client/server SSL.
+
+-h,--help                                                    Display this help message
+
+-i,--ignore <NODES>                                          Don't stream to this (comma separated) list of nodes
+
+-idct,--inter-dc-throttle <inter-dc-throttle>                Inter-datacenter throttle speed in Mbits (default unlimited)
+
+-k,--target-keyspace <target keyspace name>                  Target
+                                                             keyspace name
+-ks,--keystore <KEYSTORE>                                    Client SSL:
+                                                             full path to keystore
+-kspw,--keystore-password <KEYSTORE-PASSWORD>                Client SSL:
+                                                             password of the keystore
+--no-progress                                                Don't
+                                                             display progress
+-p,--port <native transport port>                            Port used
+                                                             for native connection (default 9042)
+-prtcl,--ssl-protocol <PROTOCOL>                             Client SSL:
+                                                             connections protocol to use (default: TLS)
+-pw,--password <password>                                    Password for
+                                                             cassandra authentication
+-sp,--storage-port <storage port>                            Port used
+                                                             for internode communication (default 7000)
+-spd,--server-port-discovery <allow server port discovery>   Use ports
+                                                             published by server to decide how to connect. With SSL requires StartTLS
+                                                             to be used.
+-ssp,--ssl-storage-port <ssl storage port>                   Port used
+                                                             for TLS internode communication (default 7001)
+-st,--store-type <STORE-TYPE>                                Client SSL:
+                                                             type of store
+-t,--throttle <throttle>                                     Throttle
+                                                             speed in Mbits (default unlimited)
+-ts,--truststore <TRUSTSTORE>                                Client SSL:
+                                                             full path to truststore
+-tspw,--truststore-password <TRUSTSTORE-PASSWORD>            Client SSL:
+                                                             Password of the truststore
+-u,--username <username>                                     Username for
+                                                             cassandra authentication
+-v,--verbose                                                 verbose
+                                                             output
+----
+
+The `cassandra.yaml` file can be provided on the command-line with `-f` option to set up streaming throughput, client and server encryption
+options. 
+Only `stream_throughput_outbound_megabits_per_sec`, `server_encryption_options` and `client_encryption_options` are read
+from the `cassandra.yaml` file.
+You can override options read from `cassandra.yaml` with corresponding command line options.
+
+=== A sstableloader Demo
+
+An example shows how to use `sstableloader` to upload incremental backup data for the table `catalogkeyspace.magazine`.
+In addition, a snapshot of the same table is created to bulk upload, also with `sstableloader`. 
+
+The backups and snapshots for the `catalogkeyspace.magazine` table are listed as follows:
+
+[source,bash]
+----
+$ cd ./cassandra/data/data/catalogkeyspace/magazine-446eae30c22a11e9b1350d927649052c && ls -l
+----
+
+results in
+
+[source,none]
+----
+total 0
+drwxrwxr-x. 2 ec2-user ec2-user 226 Aug 19 02:38 backups
+drwxrwxr-x. 4 ec2-user ec2-user  40 Aug 19 02:45 snapshots
+----
+
+The directory path structure of SSTables to be uploaded using
+`sstableloader` is used as the target keyspace/table.
+You can directly upload from the `backups` and `snapshots`
+directories respectively, if the directory structure is in the format
+used by `sstableloader`. 
+But the directory path of backups and snapshots for SSTables is
+`/catalogkeyspace/magazine-446eae30c22a11e9b1350d927649052c/backups` and
+`/catalogkeyspace/magazine-446eae30c22a11e9b1350d927649052c/snapshots`
+respectively, and cannot be used to upload SSTables to
+`catalogkeyspace.magazine` table. 
+The directory path structure must be `/catalogkeyspace/magazine/` to use `sstableloader`. 
+Create a new directory structure to upload SSTables with `sstableloader` 
+located at `/catalogkeyspace/magazine` and set appropriate permissions.
+
+[source,bash]
+----
+$ sudo mkdir -p /catalogkeyspace/magazine
+$ sudo chmod -R 777 /catalogkeyspace/magazine
+----
+
+==== Bulk Loading from an Incremental Backup
+
+An incremental backup does not include the DDL for a table; the table must already exist. 
+If the table was dropped, it can be created using the `schema.cql` file generated with every snapshot of a table. 
+Prior to using `sstableloader` to load SSTables to the `magazine` table, the table must exist. 
+The table does not need to be empty but we have used an empty table as indicated by a CQL query:
+
+[source,cql]
+----
+SELECT * FROM magazine;
+----
+results in
+[source,cql]
+----
+id | name | publisher
+----+------+-----------
+
+(0 rows)
+----
+
+After creating the table to upload to, copy the SSTable files from the `backups` directory to the `/catalogkeyspace/magazine/` directory.
+
+[source,bash]
+----
+$ sudo cp ./cassandra/data/data/catalogkeyspace/magazine-446eae30c22a11e9b1350d927649052c/backups/* \
+/catalogkeyspace/magazine/
+----
+
+Run the `sstableloader` to upload SSTables from the
+`/catalogkeyspace/magazine/` directory.
+
+[source,bash]
+----
+$ sstableloader --nodes 10.0.2.238  /catalogkeyspace/magazine/
+----
+
+The output from the `sstableloader` command should be similar to this listing:
+
+[source,bash]
+----
+$ sstableloader --nodes 10.0.2.238  /catalogkeyspace/magazine/
+----
+
+results in
+ 
+[source,none]
+----
+Opening SSTables and calculating sections to stream
+Streaming relevant part of /catalogkeyspace/magazine/na-1-big-Data.db
+/catalogkeyspace/magazine/na-2-big-Data.db  to [35.173.233.153:7000, 10.0.2.238:7000,
+54.158.45.75:7000]
+progress: [35.173.233.153:7000]0:1/2 88 % total: 88% 0.018KiB/s (avg: 0.018KiB/s)
+progress: [35.173.233.153:7000]0:2/2 176% total: 176% 33.807KiB/s (avg: 0.036KiB/s)
+progress: [35.173.233.153:7000]0:2/2 176% total: 176% 0.000KiB/s (avg: 0.029KiB/s)
+progress: [35.173.233.153:7000]0:2/2 176% [10.0.2.238:7000]0:1/2 39 % total: 81% 0.115KiB/s
+(avg: 0.024KiB/s)
+progress: [35.173.233.153:7000]0:2/2 176% [10.0.2.238:7000]0:2/2 78 % total: 108%
+97.683KiB/s (avg: 0.033KiB/s)
+progress: [35.173.233.153:7000]0:2/2 176% [10.0.2.238:7000]0:2/2 78 %
+[54.158.45.75:7000]0:1/2 39 % total: 80% 0.233KiB/s (avg: 0.040KiB/s)
+progress: [35.173.233.153:7000]0:2/2 176% [10.0.2.238:7000]0:2/2 78 %
+[54.158.45.75:7000]0:2/2 78 % total: 96% 88.522KiB/s (avg: 0.049KiB/s)
+progress: [35.173.233.153:7000]0:2/2 176% [10.0.2.238:7000]0:2/2 78 %
+[54.158.45.75:7000]0:2/2 78 % total: 96% 0.000KiB/s (avg: 0.045KiB/s)
+progress: [35.173.233.153:7000]0:2/2 176% [10.0.2.238:7000]0:2/2 78 %
+[54.158.45.75:7000]0:2/2 78 % total: 96% 0.000KiB/s (avg: 0.044KiB/s)
+----
+
+After the `sstableloader` has finished loading the data, run a query the `magazine` table to check:
+
+[source,cql]
+----
+SELECT * FROM magazine;
+----
+results in
+[source,cql]
+----
+id | name                      | publisher
+----+---------------------------+------------------
+ 1 |        Couchbase Magazine |        Couchbase
+ 0 | Apache Cassandra Magazine | Apache Cassandra
+
+(2 rows)
+----
+
+==== Bulk Loading from a Snapshot
+
+Restoring a snapshot of a table to the same table can be easily accomplished:
+
+If the directory structure needed to load SSTables to `catalogkeyspace.magazine` does not exist create the
+directories and set appropriate permissions:
+
+[source,bash]
+----
+$ sudo mkdir -p /catalogkeyspace/magazine
+$ sudo chmod -R 777 /catalogkeyspace/magazine
+----
+
+Remove any files from the directory, so that the snapshot files can be copied without interference:
+
+[source,bash]
+----
+$ sudo rm /catalogkeyspace/magazine/*
+$ cd /catalogkeyspace/magazine/
+$ ls -l
+----
+
+results in
+
+[source,none]
+----
+total 0
+----
+
+Copy the snapshot files to the `/catalogkeyspace/magazine` directory.
+
+[source,bash]
+----
+$ sudo cp ./cassandra/data/data/catalogkeyspace/magazine-446eae30c22a11e9b1350d927649052c/snapshots/magazine/* \
+/catalogkeyspace/magazine
+----
+
+List the files in the `/catalogkeyspace/magazine` directory. 
+The `schema.cql` will also be listed.
+
+[source,bash]
+----
+$ cd /catalogkeyspace/magazine && ls -l
+----
+
+results in
+
+[source,none]
+----
+total 44
+-rw-r--r--. 1 root root   31 Aug 19 04:13 manifest.json
+-rw-r--r--. 1 root root   47 Aug 19 04:13 na-1-big-CompressionInfo.db
+-rw-r--r--. 1 root root   97 Aug 19 04:13 na-1-big-Data.db
+-rw-r--r--. 1 root root   10 Aug 19 04:13 na-1-big-Digest.crc32
+-rw-r--r--. 1 root root   16 Aug 19 04:13 na-1-big-Filter.db
+-rw-r--r--. 1 root root   16 Aug 19 04:13 na-1-big-Index.db
+-rw-r--r--. 1 root root 4687 Aug 19 04:13 na-1-big-Statistics.db
+-rw-r--r--. 1 root root   56 Aug 19 04:13 na-1-big-Summary.db
+-rw-r--r--. 1 root root   92 Aug 19 04:13 na-1-big-TOC.txt
+-rw-r--r--. 1 root root  815 Aug 19 04:13 schema.cql
+----
+
+Alternatively create symlinks to the snapshot folder instead of copying
+the data:
+
+[source,bash]
+----
+$ mkdir <keyspace_name>
+$ ln -s <path_to_snapshot_folder> <keyspace_name>/<table_name>
+----
+
+If the `magazine` table was dropped, run the DDL in the `schema.cql` to
+create the table. 
+Run the `sstableloader` with the following command:
+
+[source,bash]
+----
+$ sstableloader --nodes 10.0.2.238  /catalogkeyspace/magazine/
+----
+
+As the output from the command indicates, SSTables get streamed to the
+cluster:
+
+[source,none]
+----
+Established connection to initial hosts
+Opening SSTables and calculating sections to stream
+Streaming relevant part of /catalogkeyspace/magazine/na-1-big-Data.db  to
+[35.173.233.153:7000, 10.0.2.238:7000, 54.158.45.75:7000]
+progress: [35.173.233.153:7000]0:1/1 176% total: 176% 0.017KiB/s (avg: 0.017KiB/s)
+progress: [35.173.233.153:7000]0:1/1 176% total: 176% 0.000KiB/s (avg: 0.014KiB/s)
+progress: [35.173.233.153:7000]0:1/1 176% [10.0.2.238:7000]0:1/1 78 % total: 108% 0.115KiB/s
+(avg: 0.017KiB/s)
+progress: [35.173.233.153:7000]0:1/1 176% [10.0.2.238:7000]0:1/1 78 %
+[54.158.45.75:7000]0:1/1 78 % total: 96% 0.232KiB/s (avg: 0.024KiB/s)
+progress: [35.173.233.153:7000]0:1/1 176% [10.0.2.238:7000]0:1/1 78 %
+[54.158.45.75:7000]0:1/1 78 % total: 96% 0.000KiB/s (avg: 0.022KiB/s)
+progress: [35.173.233.153:7000]0:1/1 176% [10.0.2.238:7000]0:1/1 78 %
+[54.158.45.75:7000]0:1/1 78 % total: 96% 0.000KiB/s (avg: 0.021KiB/s)
+----
+
+Some other requirements of `sstableloader` that should be kept into
+consideration are:
+
+* The SSTables loaded must be compatible with the Cassandra
+version being loaded into.
+* Repairing tables that have been loaded into a different cluster does
+not repair the source tables.
+* Sstableloader makes use of port 7000 for internode communication.
+* Before restoring incremental backups, run `nodetool flush` to backup
+any data in memtables.
+
+== Using nodetool import
+
+Importing SSTables into a table using the `nodetool import` command is recommended instead of the deprecated
+`nodetool refresh` command. 
+The `nodetool import` command has an option to load new SSTables from a separate directory.
+
+The command usage is as follows:
+
+[source,none]
+----
+nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]
+       [(-pp | --print-port)] [(-pw <password> | --password <password>)]
+       [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]
+       [(-u <username> | --username <username>)] import
+       [(-c | --no-invalidate-caches)] [(-e | --extended-verify)]
+       [(-l | --keep-level)] [(-q | --quick)] [(-r | --keep-repaired)]
+       [(-t | --no-tokens)] [(-v | --no-verify)] [--] <keyspace> <table>
+       <directory> ...
+----
+
+The arguments `keyspace`, `table` name and `directory` are required.
+
+The following options are supported:
+
+[source,none]
+----
+-c, --no-invalidate-caches
+    Don't invalidate the row cache when importing
+
+-e, --extended-verify
+    Run an extended verify, verifying all values in the new SSTables
+
+-h <host>, --host <host>
+    Node hostname or ip address
+
+-l, --keep-level
+    Keep the level on the new SSTables
+
+-p <port>, --port <port>
+    Remote jmx agent port number
+
+-pp, --print-port
+    Operate in 4.0 mode with hosts disambiguated by port number
+
+-pw <password>, --password <password>
+    Remote jmx agent password
+
+-pwf <passwordFilePath>, --password-file <passwordFilePath>
+    Path to the JMX password file
+
+-q, --quick
+    Do a quick import without verifying SSTables, clearing row cache or
+    checking in which data directory to put the file
+
+-r, --keep-repaired
+    Keep any repaired information from the SSTables
+
+-t, --no-tokens
+    Don't verify that all tokens in the new SSTable are owned by the
+    current node
+
+-u <username>, --username <username>
+    Remote jmx agent username
+
+-v, --no-verify
+    Don't verify new SSTables
+
+--
+    This option can be used to separate command-line options from the
+    list of argument, (useful when arguments might be mistaken for
+    command-line options
+----
+
+Because the keyspace and table are specified on the command line for
+`nodetool import`, there is not the same requirement as with
+`sstableloader`, to have the SSTables in a specific directory path. 
+When importing snapshots or incremental backups with
+`nodetool import`, the SSTables don’t need to be copied to another
+directory.
+
+=== Importing Data from an Incremental Backup
+
+Using `nodetool import` to import SSTables from an incremental backup, and restoring
+the table is shown below. 
+
+[source,cql]
+----
+DROP table t;
+----
+
+An incremental backup for a table does not include the schema definition for the table. 
+If the schema definition is not kept as a separate
+backup, the `schema.cql` from a backup of the table may be used to
+create the table as follows:
+
+[source,cql]
+----
+CREATE TABLE IF NOT EXISTS cqlkeyspace.t (
+   id int PRIMARY KEY,
+   k int,
+   v text)
+   WITH ID = d132e240-c217-11e9-bbee-19821dcea330
+   AND bloom_filter_fp_chance = 0.01
+   AND crc_check_chance = 1.0
+   AND default_time_to_live = 0
+   AND gc_grace_seconds = 864000
+   AND min_index_interval = 128
+   AND max_index_interval = 2048
+   AND memtable_flush_period_in_ms = 0
+   AND speculative_retry = '99p'
+   AND additional_write_policy = '99p'
+   AND comment = ''
+   AND caching = { 'keys': 'ALL', 'rows_per_partition': 'NONE' }
+   AND compaction = { 'max_threshold': '32', 'min_threshold': '4',
+   'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' }
+   AND compression = { 'chunk_length_in_kb': '16', 'class':
+   'org.apache.cassandra.io.compress.LZ4Compressor' }
+   AND cdc = false
+   AND extensions = {  }
+;
+----
+
+Initially the table could be empty, but does not have to be.
+
+[source,cql]
+----
+SELECT * FROM t;
+----
+[source,cql]
+----
+id | k | v
+----+---+---
+
+(0 rows)
+----
+
+Run the `nodetool import` command, providing the keyspace, table and
+the backups directory. 
+Don’t copy the table backups to another directory, as with `sstableloader`.
+
+[source,bash]
+----
+$ nodetool import -- cqlkeyspace t \
+./cassandra/data/data/cqlkeyspace/t-d132e240c21711e9bbee19821dcea330/backups
+----
+
+The SSTables are imported into the table. Run a query in cqlsh to check:
+
+[source,cql]
+----
+SELECT * FROM t;
+----
+[source,cql]
+----
+id | k | v
+----+---+------
+ 1 | 1 | val1
+ 0 | 0 | val0
+
+(2 rows)
+----
+
+=== Importing Data from a Snapshot
+
+Importing SSTables from a snapshot with the `nodetool import` command is
+similar to importing SSTables from an incremental backup. 
+Shown here is an import of a snapshot for table `catalogkeyspace.journal`, after
+dropping the table to demonstrate the restore.
+
+[source,cql]
+----
+USE CATALOGKEYSPACE;
+DROP TABLE journal;
+----
+
+Use the `catalog-ks` snapshot for the `journal` table. 
+Check the files in the snapshot, and note the existence of the `schema.cql` file.
+
+[source,bash]
+----
+$ ls -l
+----
+[source,none]
+----
+total 44
+-rw-rw-r--. 1 ec2-user ec2-user   31 Aug 19 02:44 manifest.json
+-rw-rw-r--. 3 ec2-user ec2-user   47 Aug 19 02:38 na-1-big-CompressionInfo.db
+-rw-rw-r--. 3 ec2-user ec2-user   97 Aug 19 02:38 na-1-big-Data.db
+-rw-rw-r--. 3 ec2-user ec2-user   10 Aug 19 02:38 na-1-big-Digest.crc32
+-rw-rw-r--. 3 ec2-user ec2-user   16 Aug 19 02:38 na-1-big-Filter.db
+-rw-rw-r--. 3 ec2-user ec2-user   16 Aug 19 02:38 na-1-big-Index.db
+-rw-rw-r--. 3 ec2-user ec2-user 4687 Aug 19 02:38 na-1-big-Statistics.db
+-rw-rw-r--. 3 ec2-user ec2-user   56 Aug 19 02:38 na-1-big-Summary.db
+-rw-rw-r--. 3 ec2-user ec2-user   92 Aug 19 02:38 na-1-big-TOC.txt
+-rw-rw-r--. 1 ec2-user ec2-user  814 Aug 19 02:44 schema.cql
+----
+
+Copy the DDL from the `schema.cql` and run in cqlsh to create the
+`catalogkeyspace.journal` table:
+
+[source,cql]
+----
+CREATE TABLE IF NOT EXISTS catalogkeyspace.journal (
+   id int PRIMARY KEY,
+   name text,
+   publisher text)
+   WITH ID = 296a2d30-c22a-11e9-b135-0d927649052c
+   AND bloom_filter_fp_chance = 0.01
+   AND crc_check_chance = 1.0
+   AND default_time_to_live = 0
+   AND gc_grace_seconds = 864000
+   AND min_index_interval = 128
+   AND max_index_interval = 2048
+   AND memtable_flush_period_in_ms = 0
+   AND speculative_retry = '99p'
+   AND additional_write_policy = '99p'
+   AND comment = ''
+   AND caching = { 'keys': 'ALL', 'rows_per_partition': 'NONE' }
+   AND compaction = { 'min_threshold': '4', 'max_threshold':
+   '32', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy' }
+   AND compression = { 'chunk_length_in_kb': '16', 'class':
+   'org.apache.cassandra.io.compress.LZ4Compressor' }
+   AND cdc = false
+   AND extensions = {  }
+;
+----
+
+Run the `nodetool import` command to import the SSTables for the
+snapshot:
+
+[source,bash]
+----
+$ nodetool import -- catalogkeyspace journal \
+./cassandra/data/data/catalogkeyspace/journal-
+296a2d30c22a11e9b1350d927649052c/snapshots/catalog-ks/
+----
+
+Subsequently run a CQL query on the `journal` table to check the imported data:
+
+[source,cql]
+----
+SELECT * FROM journal;
+----
+[source,cql]
+----
+id | name                      | publisher
+----+---------------------------+------------------
+ 1 |        Couchbase Magazine |        Couchbase
+ 0 | Apache Cassandra Magazine | Apache Cassandra
+
+(2 rows)
+----
+
+== Bulk Loading External Data
+
+Bulk loading external data directly is not supported by any of the tools
+we have discussed which include `sstableloader` and `nodetool import`.
+The `sstableloader` and `nodetool import` require data to be in the form
+of SSTables. 
+Apache Cassandra supports a Java API for generating SSTables from input data, using the
+`org.apache.cassandra.io.sstable.CQLSSTableWriter` Java class.
+Subsequently, either `sstableloader` or `nodetool import` is used to bulk load the SSTables. 
+
+=== Generating SSTables with CQLSSTableWriter Java API
+
+To generate SSTables using the `CQLSSTableWriter` class the following are required:
+
+* An output directory to generate the SSTable in
+* The schema for the SSTable
+* A prepared statement for the `INSERT`
+* A partitioner
+
+The output directory must exist before starting. Create a directory
+(`/sstables` as an example) and set appropriate permissions.
+
+[source,bash]
+----
+$ sudo mkdir /sstables
+$ sudo chmod  777 -R /sstables
+----
+
+To use `CQLSSTableWriter` in a Java application, create a Java constant for the output directory.
+
+[source,java]
+----
+public static final String OUTPUT_DIR = "./sstables";
+----
+
+`CQLSSTableWriter` Java API can create a user-defined type. Create a new type to store `int` data:
+
+[source,java]
+----
+String type = "CREATE TYPE CQLKeyspace.intType (a int, b int)";
+// Define a String variable for the SSTable schema.
+String schema = "CREATE TABLE CQLKeyspace.t ("
+                 + "  id int PRIMARY KEY,"
+                 + "  k int,"
+                 + "  v1 text,"
+                 + "  v2 intType,"
+                 + ")";
+----
+
+Define a `String` variable for the prepared statement to use:
+
+[source,java]
+----
+String insertStmt = "INSERT INTO CQLKeyspace.t (id, k, v1, v2) VALUES (?, ?, ?, ?)";
+----
+
+The partitioner to use only needs setting if the default partitioner `Murmur3Partitioner` is not used.
+
+All these variables or settings are used by the builder class
+`CQLSSTableWriter.Builder` to create a `CQLSSTableWriter` object.
+
+Create a File object for the output directory.
+
+[source,java]
+----
+File outputDir = new File(OUTPUT_DIR + File.separator + "CQLKeyspace" + File.separator + "t");
+----
+
+Obtain a `CQLSSTableWriter.Builder` object using `static` method `CQLSSTableWriter.builder()`. 
+Set the following items:
+
+* output directory `File` object 
+* user-defined type 
+* SSTable schema 
+* buffer size 
+* prepared statement 
+* optionally any of the other builder options 
+
+and invoke the `build()` method to create a `CQLSSTableWriter` object:
+
+[source,java]
+----
+CQLSSTableWriter writer = CQLSSTableWriter.builder()
+                                             .inDirectory(outputDir)
+                                             .withType(type)
+                                             .forTable(schema)
+                                             .withBufferSizeInMB(256)
+                                             .using(insertStmt).build();
+----
+
+Set the SSTable data. If any user-defined types are used, obtain a
+`UserType` object for each type:
+
+[source,java]
+----
+UserType userType = writer.getUDType("intType");
+----
+
+Add data rows for the resulting SSTable:
+
+[source,java]
+----
+writer.addRow(0, 0, "val0", userType.newValue().setInt("a", 0).setInt("b", 0));
+   writer.addRow(1, 1, "val1", userType.newValue().setInt("a", 1).setInt("b", 1));
+   writer.addRow(2, 2, "val2", userType.newValue().setInt("a", 2).setInt("b", 2));
+----
+
+Close the writer, finalizing the SSTable:
+
+[source,java]
+----
+writer.close();
+----
+
+Other public methods the `CQLSSTableWriter` class provides are:
+
+[cols=",",options="header",]
+|===
+|Method |Description
+
+|addRow(java.util.List<java.lang.Object> values) |Adds a new row to the
+writer. Returns a CQLSSTableWriter object. Each provided value type
+should correspond to the types of the CQL column the value is for. The
+correspondence between java type and CQL type is the same one than the
+one documented at
+www.datastax.com/drivers/java/2.0/apidocs/com/datastax/driver/core/DataType.Name.html#asJavaC
+lass().
+
+|addRow(java.util.Map<java.lang.String,java.lang.Object> values) |Adds a
+new row to the writer. Returns a CQLSSTableWriter object. This is
+equivalent to the other addRow methods, but takes a map whose keys are
+the names of the columns to add instead of taking a list of the values
+in the order of the insert statement used during construction of this
+SSTable writer. The column names in the map keys must be in lowercase
+unless the declared column name is a case-sensitive quoted identifier in
+which case the map key must use the exact case of the column. The values
+parameter is a map of column name to column values representing the new
+row to add. If a column is not included in the map, it's value will be
+null. If the map contains keys that do not correspond to one of the
+columns of the insert statement used when creating this SSTable writer,
+the corresponding value is ignored.
+
+|addRow(java.lang.Object... values) |Adds a new row to the writer.
+Returns a CQLSSTableWriter object.
+
+|CQLSSTableWriter.builder() |Returns a new builder for a
+CQLSSTableWriter.
+
+|close() |Closes the writer.
+
+|rawAddRow(java.nio.ByteBuffer... values) |Adds a new row to the writer
+given already serialized binary values. Returns a CQLSSTableWriter
+object. The row values must correspond to the bind variables of the
+insertion statement used when creating by this SSTable writer.
+
+|rawAddRow(java.util.List<java.nio.ByteBuffer> values) |Adds a new row
+to the writer given already serialized binary values. Returns a
+CQLSSTableWriter object. The row values must correspond to the bind
+variables of the insertion statement used when creating by this SSTable
+writer. 
+
+|rawAddRow(java.util.Map<java.lang.String, java.nio.ByteBuffer> values)
+|Adds a new row to the writer given already serialized binary values.
+Returns a CQLSSTableWriter object. The row values must correspond to the
+bind variables of the insertion statement used when creating by this
+SSTable writer.
+
+|getUDType(String dataType) |Returns the User Defined type used in this
+SSTable Writer that can be used to create UDTValue instances.
+|===
+
+Other public methods the `CQLSSTableWriter.Builder` class provides are: 
+
+[cols=",",options="header",]
+|===
+|Method |Description
+|inDirectory(String directory) |The directory where to write the
+SSTables. This is a mandatory option. The directory to use should
+already exist and be writable.
+
+|inDirectory(File directory) |The directory where to write the SSTables.
+This is a mandatory option. The directory to use should already exist
+and be writable.
+
+|forTable(String schema) |The schema (CREATE TABLE statement) for the
+table for which SSTable is to be created. The provided CREATE TABLE
+statement must use a fully-qualified table name, one that includes the
+keyspace name. This is a mandatory option.
+
+|withPartitioner(IPartitioner partitioner) |The partitioner to use. By
+default, Murmur3Partitioner will be used. If this is not the partitioner
+used by the cluster for which the SSTables are created, the correct
+partitioner needs to be provided.
+
+|using(String insert) |The INSERT or UPDATE statement defining the order
+of the values to add for a given CQL row. The provided INSERT statement
+must use a fully-qualified table name, one that includes the keyspace
+name. Moreover, said statement must use bind variables since these
+variables will be bound to values by the resulting SSTable writer. This
+is a mandatory option.
+
+|withBufferSizeInMB(int size) |The size of the buffer to use. This
+defines how much data will be buffered before being written as a new
+SSTable. This corresponds roughly to the data size that will have the
+created SSTable. The default is 128MB, which should be reasonable for a
+1GB heap. If OutOfMemory exception gets generated while using the
+SSTable writer, should lower this value.
+
+|sorted() |Creates a CQLSSTableWriter that expects sorted inputs. If
+this option is used, the resulting SSTable writer will expect rows to be
+added in SSTable sorted order (and an exception will be thrown if that
+is not the case during row insertion). The SSTable sorted order means
+that rows are added such that their partition keys respect the
+partitioner order. This option should only be used if the rows can be
+provided in order, which is rarely the case. If the rows can be provided
+in order however, using this sorted might be more efficient. If this
+option is used, some option like withBufferSizeInMB will be ignored.
+
+|build() |Builds a CQLSSTableWriter object.
+|===
diff --git a/doc/modules/cassandra/pages/operating/cdc.adoc b/doc/modules/cassandra/pages/operating/cdc.adoc
new file mode 100644
index 0000000..b0d5c19
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/cdc.adoc
@@ -0,0 +1,86 @@
+= Change Data Capture
+
+== Overview
+
+Change data capture (CDC) provides a mechanism to flag specific tables
+for archival as well as rejecting writes to those tables once a
+configurable size-on-disk for the CDC log is reached. An operator can
+enable CDC on a table by setting the table property `cdc=true` (either
+when xref:cql/ddl.adoc#create-table[`creating the table`] or
+xref:cql/ddl.adoc#alter-table[`altering it`]). Upon CommitLogSegment creation,
+a hard-link to the segment is created in the directory specified in
+`cassandra.yaml`. On segment fsync to disk, if CDC data is present
+anywhere in the segment a <segment_name>_cdc.idx file is also created
+with the integer offset of how much data in the original segment is
+persisted to disk. Upon final segment flush, a second line with the
+human-readable word "COMPLETED" will be added to the _cdc.idx file
+indicating that Cassandra has completed all processing on the file.
+
+We we use an index file rather than just encouraging clients to parse
+the log realtime off a memory mapped handle as data can be reflected in
+a kernel buffer that is not yet persisted to disk. Parsing only up to
+the listed offset in the _cdc.idx file will ensure that you only parse
+CDC data for data that is durable.
+
+A threshold of total disk space allowed is specified in the yaml at
+which time newly allocated CommitLogSegments will not allow CDC data
+until a consumer parses and removes files from the specified cdc_raw
+directory.
+
+== Configuration
+
+=== Enabling or disabling CDC on a table
+
+CDC is enable or disable through the [.title-ref]#cdc# table property,
+for instance:
+
+[source,cql]
+----
+CREATE TABLE foo (a int, b text, PRIMARY KEY(a)) WITH cdc=true;
+
+ALTER TABLE foo WITH cdc=true;
+
+ALTER TABLE foo WITH cdc=false;
+----
+
+=== cassandra.yaml parameters
+
+The following cassandra.yaml options are available for CDC:
+
+`cdc_enabled` (default: false)::
+  Enable or disable CDC operations node-wide.
+`cdc_raw_directory` (default: `$CASSANDRA_HOME/data/cdc_raw`)::
+  Destination for CommitLogSegments to be moved after all corresponding
+  memtables are flushed.
+`cdc_free_space_in_mb`: (default: min of 4096 and 1/8th volume space)::
+  Calculated as sum of all active CommitLogSegments that permit CDC +
+  all flushed CDC segments in `cdc_raw_directory`.
+`cdc_free_space_check_interval_ms` (default: 250)::
+  When at capacity, we limit the frequency with which we re-calculate
+  the space taken up by `cdc_raw_directory` to prevent burning CPU
+  cycles unnecessarily. Default is to check 4 times per second.
+
+== Reading CommitLogSegments
+
+Use a
+https://github.com/apache/cassandra/blob/e31e216234c6b57a531cae607e0355666007deb2/src/java/org/apache/cassandra/db/commitlog/CommitLogReader.java[CommitLogReader.java].
+Usage is
+https://github.com/apache/cassandra/blob/e31e216234c6b57a531cae607e0355666007deb2/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java#L132-L140[fairly
+straightforward] with a
+https://github.com/apache/cassandra/blob/e31e216234c6b57a531cae607e0355666007deb2/src/java/org/apache/cassandra/db/commitlog/CommitLogReader.java#L71-L103[variety
+of signatures] available for use. In order to handle mutations read from
+disk, implement
+https://github.com/apache/cassandra/blob/e31e216234c6b57a531cae607e0355666007deb2/src/java/org/apache/cassandra/db/commitlog/CommitLogReadHandler.java[CommitLogReadHandler].
+
+== Warnings
+
+*Do not enable CDC without some kind of consumption process in-place.*
+
+If CDC is enabled on a node and then on a table, the
+`cdc_free_space_in_mb` will fill up and then writes to CDC-enabled
+tables will be rejected unless some consumption process is in place.
+
+== Further Reading
+
+* https://issues.apache.org/jira/browse/CASSANDRA-8844[JIRA ticket]
+* https://issues.apache.org/jira/browse/CASSANDRA-12148[JIRA ticket]
diff --git a/doc/modules/cassandra/pages/operating/compaction/index.adoc b/doc/modules/cassandra/pages/operating/compaction/index.adoc
new file mode 100644
index 0000000..e3c76b8
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/compaction/index.adoc
@@ -0,0 +1,339 @@
+= Compaction
+
+== Strategies
+
+Picking the right compaction strategy for your workload will ensure the
+best performance for both querying and for compaction itself.
+
+xref:operating/compaction/stcs.adoc[`Size Tiered Compaction Strategy (STCS)`]::
+  The default compaction strategy. Useful as a fallback when other
+  strategies don't fit the workload. Most useful for non pure time
+  series workloads with spinning disks, or when the I/O from `LCS`
+  is too high.
+xref:operating/compaction/lcs.adoc[`Leveled Compaction Strategy (LCS)`]::
+  Leveled Compaction Strategy (LCS) is optimized for read heavy
+  workloads, or workloads with lots of updates and deletes. It is not a
+  good choice for immutable time series data.
+xref:operating/compaction/twcs.adoc[`Time Window Compaction Strategy (TWCS)`]::
+  Time Window Compaction Strategy is designed for TTL'ed, mostly
+  immutable time series data.
+
+== Types of compaction
+
+The concept of compaction is used for different kinds of operations in
+Cassandra, the common thing about these operations is that it takes one
+or more SSTables and output new SSTables. The types of compactions are:
+
+Minor compaction::
+  triggered automatically in Cassandra.
+Major compaction::
+  a user executes a compaction over all SSTables on the node.
+User defined compaction::
+  a user triggers a compaction on a given set of SSTables.
+Scrub::
+  try to fix any broken SSTables. This can actually remove valid data if
+  that data is corrupted, if that happens you will need to run a full
+  repair on the node.
+UpgradeSSTables::
+  upgrade SSTables to the latest version. Run this after upgrading to a
+  new major version.
+Cleanup::
+  remove any ranges this node does not own anymore, typically triggered
+  on neighbouring nodes after a node has been bootstrapped since that
+  node will take ownership of some ranges from those nodes.
+Secondary index rebuild::
+  rebuild the secondary indexes on the node.
+Anticompaction::
+  after repair the ranges that were actually repaired are split out of
+  the SSTables that existed when repair started.
+Sub range compaction::
+  It is possible to only compact a given sub range - this could be
+  useful if you know a token that has been misbehaving - either
+  gathering many updates or many deletes.
+  (`nodetool compact -st x -et y`) will pick all SSTables containing the
+  range between x and y and issue a compaction for those SSTables. For
+  STCS this will most likely include all SSTables but with LCS it can
+  issue the compaction for a subset of the SSTables. With LCS the
+  resulting sstable will end up in L0.
+
+== When is a minor compaction triggered?
+
+* When an sstable is added to the node through flushing/streaming
+* When autocompaction is enabled after being disabled (`nodetool enableautocompaction`) 
+* When compaction adds new SSTables 
+* A check for new minor compactions every 5 minutes
+
+== Merging SSTables
+
+Compaction is about merging SSTables, since partitions in SSTables are
+sorted based on the hash of the partition key it is possible to
+efficiently merge separate SSTables. Content of each partition is also
+sorted so each partition can be merged efficiently.
+
+== Tombstones and Garbage Collection (GC) Grace
+
+=== Why Tombstones
+
+When a delete request is received by Cassandra it does not actually
+remove the data from the underlying store. Instead it writes a special
+piece of data known as a tombstone. The Tombstone represents the delete
+and causes all values which occurred before the tombstone to not appear
+in queries to the database. This approach is used instead of removing
+values because of the distributed nature of Cassandra.
+
+=== Deletes without tombstones
+
+Imagine a three node cluster which has the value [A] replicated to every
+node.:
+
+[source,none]
+----
+[A], [A], [A]
+----
+
+If one of the nodes fails and and our delete operation only removes
+existing values we can end up with a cluster that looks like:
+
+[source,none]
+----
+[], [], [A]
+----
+
+Then a repair operation would replace the value of [A] back onto the two
+nodes which are missing the value.:
+
+[source,none]
+----
+[A], [A], [A]
+----
+
+This would cause our data to be resurrected even though it had been
+deleted.
+
+=== Deletes with Tombstones
+
+Starting again with a three node cluster which has the value [A]
+replicated to every node.:
+
+[source,none]
+----
+[A], [A], [A]
+----
+
+If instead of removing data we add a tombstone record, our single node
+failure situation will look like this.:
+
+[source,none]
+----
+[A, Tombstone[A]], [A, Tombstone[A]], [A]
+----
+
+Now when we issue a repair the Tombstone will be copied to the replica,
+rather than the deleted data being resurrected.:
+
+[source,none]
+----
+[A, Tombstone[A]], [A, Tombstone[A]], [A, Tombstone[A]]
+----
+
+Our repair operation will correctly put the state of the system to what
+we expect with the record [A] marked as deleted on all nodes. This does
+mean we will end up accruing Tombstones which will permanently
+accumulate disk space. To avoid keeping tombstones forever we have a
+parameter known as `gc_grace_seconds` for every table in Cassandra.
+
+=== The gc_grace_seconds parameter and Tombstone Removal
+
+The table level `gc_grace_seconds` parameter controls how long Cassandra
+will retain tombstones through compaction events before finally removing
+them. This duration should directly reflect the amount of time a user
+expects to allow before recovering a failed node. After
+`gc_grace_seconds` has expired the tombstone may be removed (meaning
+there will no longer be any record that a certain piece of data was
+deleted), but as a tombstone can live in one sstable and the data it
+covers in another, a compaction must also include both sstable for a
+tombstone to be removed. More precisely, to be able to drop an actual
+tombstone the following needs to be true;
+
+* The tombstone must be older than `gc_grace_seconds`
+* If partition X contains the tombstone, the sstable containing the
+partition plus all SSTables containing data older than the tombstone
+containing X must be included in the same compaction. We don't need to
+care if the partition is in an sstable if we can guarantee that all data
+in that sstable is newer than the tombstone. If the tombstone is older
+than the data it cannot shadow that data.
+* If the option `only_purge_repaired_tombstones` is enabled, tombstones
+are only removed if the data has also been repaired.
+
+If a node remains down or disconnected for longer than
+`gc_grace_seconds` it's deleted data will be repaired back to the other
+nodes and re-appear in the cluster. This is basically the same as in the
+"Deletes without Tombstones" section. Note that tombstones will not be
+removed until a compaction event even if `gc_grace_seconds` has elapsed.
+
+The default value for `gc_grace_seconds` is 864000 which is equivalent
+to 10 days. This can be set when creating or altering a table using
+`WITH gc_grace_seconds`.
+
+== TTL
+
+Data in Cassandra can have an additional property called time to live -
+this is used to automatically drop data that has expired once the time
+is reached. Once the TTL has expired the data is converted to a
+tombstone which stays around for at least `gc_grace_seconds`. Note that
+if you mix data with TTL and data without TTL (or just different length
+of the TTL) Cassandra will have a hard time dropping the tombstones
+created since the partition might span many SSTables and not all are
+compacted at once.
+
+== Fully expired SSTables
+
+If an sstable contains only tombstones and it is guaranteed that that
+sstable is not shadowing data in any other sstable compaction can drop
+that sstable. If you see SSTables with only tombstones (note that TTL:ed
+data is considered tombstones once the time to live has expired) but it
+is not being dropped by compaction, it is likely that other SSTables
+contain older data. There is a tool called `sstableexpiredblockers` that
+will list which SSTables are droppable and which are blocking them from
+being dropped. This is especially useful for time series compaction with
+`TimeWindowCompactionStrategy` (and the deprecated
+`DateTieredCompactionStrategy`). With `TimeWindowCompactionStrategy` it
+is possible to remove the guarantee (not check for shadowing data) by
+enabling `unsafe_aggressive_sstable_expiration`.
+
+== Repaired/unrepaired data
+
+With incremental repairs Cassandra must keep track of what data is
+repaired and what data is unrepaired. With anticompaction repaired data
+is split out into repaired and unrepaired SSTables. To avoid mixing up
+the data again separate compaction strategy instances are run on the two
+sets of data, each instance only knowing about either the repaired or
+the unrepaired SSTables. This means that if you only run incremental
+repair once and then never again, you might have very old data in the
+repaired SSTables that block compaction from dropping tombstones in the
+unrepaired (probably newer) SSTables.
+
+== Data directories
+
+Since tombstones and data can live in different SSTables it is important
+to realize that losing an sstable might lead to data becoming live again
+- the most common way of losing SSTables is to have a hard drive break
+down. To avoid making data live tombstones and actual data are always in
+the same data directory. This way, if a disk is lost, all versions of a
+partition are lost and no data can get undeleted. To achieve this a
+compaction strategy instance per data directory is run in addition to
+the compaction strategy instances containing repaired/unrepaired data,
+this means that if you have 4 data directories there will be 8
+compaction strategy instances running. This has a few more benefits than
+just avoiding data getting undeleted:
+
+* It is possible to run more compactions in parallel - leveled
+compaction will have several totally separate levelings and each one can
+run compactions independently from the others.
+* Users can backup and restore a single data directory.
+* Note though that currently all data directories are considered equal,
+so if you have a tiny disk and a big disk backing two data directories,
+the big one will be limited the by the small one. One work around to
+this is to create more data directories backed by the big disk.
+
+== Single sstable tombstone compaction
+
+When an sstable is written a histogram with the tombstone expiry times
+is created and this is used to try to find SSTables with very many
+tombstones and run single sstable compaction on that sstable in hope of
+being able to drop tombstones in that sstable. Before starting this it
+is also checked how likely it is that any tombstones will actually will
+be able to be dropped how much this sstable overlaps with other
+SSTables. To avoid most of these checks the compaction option
+`unchecked_tombstone_compaction` can be enabled.
+
+[[compaction-options]]
+== Common options
+
+There is a number of common options for all the compaction strategies;
+
+`enabled` (default: true)::
+  Whether minor compactions should run. Note that you can have
+  'enabled': true as a compaction option and then do 'nodetool
+  enableautocompaction' to start running compactions.
+`tombstone_threshold` (default: 0.2)::
+  How much of the sstable should be tombstones for us to consider doing
+  a single sstable compaction of that sstable.
+`tombstone_compaction_interval` (default: 86400s (1 day))::
+  Since it might not be possible to drop any tombstones when doing a
+  single sstable compaction we need to make sure that one sstable is not
+  constantly getting recompacted - this option states how often we
+  should try for a given sstable.
+`log_all` (default: false)::
+  New detailed compaction logging, see
+  `below <detailed-compaction-logging>`.
+`unchecked_tombstone_compaction` (default: false)::
+  The single sstable compaction has quite strict checks for whether it
+  should be started, this option disables those checks and for some
+  usecases this might be needed. Note that this does not change anything
+  for the actual compaction, tombstones are only dropped if it is safe
+  to do so - it might just rewrite an sstable without being able to drop
+  any tombstones.
+`only_purge_repaired_tombstone` (default: false)::
+  Option to enable the extra safety of making sure that tombstones are
+  only dropped if the data has been repaired.
+`min_threshold` (default: 4)::
+  Lower limit of number of SSTables before a compaction is triggered.
+  Not used for `LeveledCompactionStrategy`.
+`max_threshold` (default: 32)::
+  Upper limit of number of SSTables before a compaction is triggered.
+  Not used for `LeveledCompactionStrategy`.
+
+Further, see the section on each strategy for specific additional
+options.
+
+== Compaction nodetool commands
+
+The `nodetool <nodetool>` utility provides a number of commands related
+to compaction:
+
+`enableautocompaction`::
+  Enable compaction.
+`disableautocompaction`::
+  Disable compaction.
+`setcompactionthroughput`::
+  How fast compaction should run at most - defaults to 16MB/s, but note
+  that it is likely not possible to reach this throughput.
+`compactionstats`::
+  Statistics about current and pending compactions.
+`compactionhistory`::
+  List details about the last compactions.
+`setcompactionthreshold`::
+  Set the min/max sstable count for when to trigger compaction, defaults
+  to 4/32.
+
+== Switching the compaction strategy and options using JMX
+
+It is possible to switch compaction strategies and its options on just a
+single node using JMX, this is a great way to experiment with settings
+without affecting the whole cluster. The mbean is:
+
+[source,none]
+----
+org.apache.cassandra.db:type=ColumnFamilies,keyspace=<keyspace_name>,columnfamily=<table_name>
+----
+
+and the attribute to change is `CompactionParameters` or
+`CompactionParametersJson` if you use jconsole or jmc. The syntax for
+the json version is the same as you would use in an
+`ALTER TABLE <alter-table-statement>` statement -for example:
+
+[source,none]
+----
+{ 'class': 'LeveledCompactionStrategy', 'sstable_size_in_mb': 123, 'fanout_size': 10}
+----
+
+The setting is kept until someone executes an
+`ALTER TABLE <alter-table-statement>` that touches the compaction
+settings or restarts the node.
+
+[[detailed-compaction-logging]]
+== More detailed compaction logging
+
+Enable with the compaction option `log_all` and a more detailed
+compaction log file will be produced in your log directory.
diff --git a/doc/modules/cassandra/pages/operating/compaction/lcs.adoc b/doc/modules/cassandra/pages/operating/compaction/lcs.adoc
new file mode 100644
index 0000000..5b0adb8
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/compaction/lcs.adoc
@@ -0,0 +1,81 @@
+= Leveled Compaction Strategy
+
+[[lcs]]
+The idea of `LeveledCompactionStrategy` (LCS) is that all sstables are
+put into different levels where we guarantee that no overlapping
+sstables are in the same level. By overlapping we mean that the
+first/last token of a single sstable are never overlapping with other
+sstables. This means that for a SELECT we will only have to look for the
+partition key in a single sstable per level. Each level is 10x the size
+of the previous one and each sstable is 160MB by default. L0 is where
+sstables are streamed/flushed - no overlap guarantees are given here.
+
+When picking compaction candidates we have to make sure that the
+compaction does not create overlap in the target level. This is done by
+always including all overlapping sstables in the next level. For example
+if we select an sstable in L3, we need to guarantee that we pick all
+overlapping sstables in L4 and make sure that no currently ongoing
+compactions will create overlap if we start that compaction. We can
+start many parallel compactions in a level if we guarantee that we wont
+create overlap. For L0 -> L1 compactions we almost always need to
+include all L1 sstables since most L0 sstables cover the full range. We
+also can't compact all L0 sstables with all L1 sstables in a single
+compaction since that can use too much memory.
+
+When deciding which level to compact LCS checks the higher levels first
+(with LCS, a "higher" level is one with a higher number, L0 being the
+lowest one) and if the level is behind a compaction will be started in
+that level.
+
+== Major compaction
+
+It is possible to do a major compaction with LCS - it will currently
+start by filling out L1 and then once L1 is full, it continues with L2
+etc. This is sub optimal and will change to create all the sstables in a
+high level instead, CASSANDRA-11817.
+
+== Bootstrapping
+
+During bootstrap sstables are streamed from other nodes. The level of
+the remote sstable is kept to avoid many compactions after the bootstrap
+is done. During bootstrap the new node also takes writes while it is
+streaming the data from a remote node - these writes are flushed to L0
+like all other writes and to avoid those sstables blocking the remote
+sstables from going to the correct level, we only do STCS in L0 until
+the bootstrap is done.
+
+== STCS in L0
+
+If LCS gets very many L0 sstables reads are going to hit all (or most)
+of the L0 sstables since they are likely to be overlapping. To more
+quickly remedy this LCS does STCS compactions in L0 if there are more
+than 32 sstables there. This should improve read performance more
+quickly compared to letting LCS do its L0 -> L1 compactions. If you keep
+getting too many sstables in L0 it is likely that LCS is not the best
+fit for your workload and STCS could work out better.
+
+== Starved sstables
+
+If a node ends up with a leveling where there are a few very high level
+sstables that are not getting compacted they might make it impossible
+for lower levels to drop tombstones etc. For example, if there are
+sstables in L6 but there is only enough data to actually get a L4 on the
+node the left over sstables in L6 will get starved and not compacted.
+This can happen if a user changes sstable_size_in_mb from 5MB to 160MB
+for example. To avoid this LCS tries to include those starved high level
+sstables in other compactions if there has been 25 compaction rounds
+where the highest level has not been involved.
+
+[[lcs_options]]
+== LCS options
+
+`sstable_size_in_mb` (default: 160MB)::
+  The target compressed (if using compression) sstable size - the
+  sstables can end up being larger if there are very large partitions on
+  the node.
+`fanout_size` (default: 10)::
+  The target size of levels increases by this fanout_size multiplier.
+  You can reduce the space amplification by tuning this option.
+
+LCS also support the `cassandra.disable_stcs_in_l0` startup option
+(`-Dcassandra.disable_stcs_in_l0=true`) to avoid doing STCS in L0.
diff --git a/doc/modules/cassandra/pages/operating/compaction/stcs.adoc b/doc/modules/cassandra/pages/operating/compaction/stcs.adoc
new file mode 100644
index 0000000..162d2fe
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/compaction/stcs.adoc
@@ -0,0 +1,42 @@
+= Size Tiered Compaction Strategy
+
+[[stcs]]
+The basic idea of `SizeTieredCompactionStrategy` (STCS) is to merge
+sstables of approximately the same size. All sstables are put in
+different buckets depending on their size. An sstable is added to the
+bucket if size of the sstable is within `bucket_low` and `bucket_high`
+of the current average size of the sstables already in the bucket. This
+will create several buckets and the most interesting of those buckets
+will be compacted. The most interesting one is decided by figuring out
+which bucket's sstables takes the most reads.
+
+== Major compaction
+
+When running a major compaction with STCS you will end up with two
+sstables per data directory (one for repaired data and one for
+unrepaired data). There is also an option (-s) to do a major compaction
+that splits the output into several sstables. The sizes of the sstables
+are approximately 50%, 25%, 12.5%... of the total size.
+
+[[stcs_options]]
+== STCS options
+
+`min_sstable_size` (default: 50MB)::
+  Sstables smaller than this are put in the same bucket.
+`bucket_low` (default: 0.5)::
+  How much smaller than the average size of a bucket a sstable should be
+  before not being included in the bucket. That is, if
+  `bucket_low * avg_bucket_size < sstable_size` (and the `bucket_high`
+  condition holds, see below), then the sstable is added to the bucket.
+`bucket_high` (default: 1.5)::
+  How much bigger than the average size of a bucket a sstable should be
+  before not being included in the bucket. That is, if
+  `sstable_size < bucket_high * avg_bucket_size` (and the `bucket_low`
+  condition holds, see above), then the sstable is added to the bucket.
+
+== Defragmentation
+
+Defragmentation is done when many sstables are touched during a read.
+The result of the read is put in to the memtable so that the next read
+will not have to touch as many sstables. This can cause writes on a
+read-only-cluster.
diff --git a/doc/modules/cassandra/pages/operating/compaction/twcs.adoc b/doc/modules/cassandra/pages/operating/compaction/twcs.adoc
new file mode 100644
index 0000000..21c44f5
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/compaction/twcs.adoc
@@ -0,0 +1,75 @@
+= Time Window CompactionStrategy
+
+[[twcs]]
+`TimeWindowCompactionStrategy` (TWCS) is designed specifically for
+workloads where it's beneficial to have data on disk grouped by the
+timestamp of the data, a common goal when the workload is time-series in
+nature or when all data is written with a TTL. In an expiring/TTL
+workload, the contents of an entire SSTable likely expire at
+approximately the same time, allowing them to be dropped completely, and
+space reclaimed much more reliably than when using
+`SizeTieredCompactionStrategy` or `LeveledCompactionStrategy`. The basic
+concept is that `TimeWindowCompactionStrategy` will create one sstable per
+file for a given window, where a window is simply calculated as the
+combination of two primary options:
+
+[[twcs_options]]
+
+`compaction_window_unit` (default: DAYS)::
+  A Java TimeUnit (MINUTES, HOURS, or DAYS).
+`compaction_window_size` (default: 1)::
+  The number of units that make up a window.
+`unsafe_aggressive_sstable_expiration` (default: false)::
+  Expired sstables will be dropped without checking its data is
+  shadowing other sstables. This is a potentially risky option that can
+  lead to data loss or deleted data re-appearing, going beyond what
+  unchecked_tombstone_compaction does for single sstable
+  compaction. Due to the risk the jvm must also be started with
+  `-Dcassandra.unsafe_aggressive_sstable_expiration=true`.
+
+Taken together, the operator can specify windows of virtually any size,
+and `TimeWindowCompactionStrategy` will work to create a
+single sstable for writes within that window. For efficiency during
+writing, the newest window will be compacted using
+`SizeTieredCompactionStrategy`.
+
+Ideally, operators should select a `compaction_window_unit` and
+`compaction_window_size` pair that produces approximately 20-30 windows
+- if writing with a 90 day TTL, for example, a 3 Day window would be a
+reasonable choice
+(`'compaction_window_unit':'DAYS','compaction_window_size':3`).
+
+== TimeWindowCompactionStrategy Operational Concerns
+
+The primary motivation for TWCS is to separate data on disk by timestamp
+and to allow fully expired SSTables to drop more efficiently. One
+potential way this optimal behavior can be subverted is if data is
+written to SSTables out of order, with new data and old data in the same
+SSTable. Out of order data can appear in two ways:
+
+* If the user mixes old data and new data in the traditional write path,
+the data will be comingled in the memtables and flushed into the same
+SSTable, where it will remain comingled.
+* If the user's read requests for old data cause read repairs that pull
+old data into the current memtable, that data will be comingled and
+flushed into the same SSTable.
+
+While TWCS tries to minimize the impact of comingled data, users should
+attempt to avoid this behavior. Specifically, users should avoid queries
+that explicitly set the timestamp via CQL `USING TIMESTAMP`.
+Additionally, users should run frequent repairs (which streams data in
+such a way that it does not become comingled).
+
+== Changing TimeWindowCompactionStrategy Options
+
+Operators wishing to enable `TimeWindowCompactionStrategy` on existing
+data should consider running a major compaction first, placing all
+existing data into a single (old) window. Subsequent newer writes will
+then create typical SSTables as expected.
+
+Operators wishing to change `compaction_window_unit` or
+`compaction_window_size` can do so, but may trigger additional
+compactions as adjacent windows are joined together. If the window size
+is decrease d (for example, from 24 hours to 12 hours), then the
+existing SSTables will not be modified - TWCS can not split existing
+SSTables into multiple windows.
diff --git a/doc/modules/cassandra/pages/operating/compression.adoc b/doc/modules/cassandra/pages/operating/compression.adoc
new file mode 100644
index 0000000..e6f8d50
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/compression.adoc
@@ -0,0 +1,187 @@
+= Compression
+
+Cassandra offers operators the ability to configure compression on a
+per-table basis. Compression reduces the size of data on disk by
+compressing the SSTable in user-configurable compression
+`chunk_length_in_kb`. As Cassandra SSTables are immutable, the CPU cost
+of compressing is only necessary when the SSTable is written -
+subsequent updates to data will land in different SSTables, so Cassandra
+will not need to decompress, overwrite, and recompress data when UPDATE
+commands are issued. On reads, Cassandra will locate the relevant
+compressed chunks on disk, decompress the full chunk, and then proceed
+with the remainder of the read path (merging data from disks and
+memtables, read repair, and so on).
+
+Compression algorithms typically trade off between the following three
+areas:
+
+* *Compression speed*: How fast does the compression algorithm compress
+data. This is critical in the flush and compaction paths because data
+must be compressed before it is written to disk.
+* *Decompression speed*: How fast does the compression algorithm
+de-compress data. This is critical in the read and compaction paths as
+data must be read off disk in a full chunk and decompressed before it
+can be returned.
+* *Ratio*: By what ratio is the uncompressed data reduced by. Cassandra
+typically measures this as the size of data on disk relative to the
+uncompressed size. For example a ratio of `0.5` means that the data on
+disk is 50% the size of the uncompressed data. Cassandra exposes this
+ratio per table as the `SSTable Compression Ratio` field of
+`nodetool tablestats`.
+
+Cassandra offers five compression algorithms by default that make
+different tradeoffs in these areas. While benchmarking compression
+algorithms depends on many factors (algorithm parameters such as
+compression level, the compressibility of the input data, underlying
+processor class, etc ...), the following table should help you pick a
+starting point based on your application's requirements with an
+extremely rough grading of the different choices by their performance in
+these areas (A is relatively good, F is relatively bad):
+
+[width="100%",cols="40%,19%,11%,13%,6%,11%",options="header",]
+|===
+|Compression Algorithm |Cassandra Class |Compression |Decompression
+|Ratio |C* Version
+
+|https://lz4.github.io/lz4/[LZ4] |`LZ4Compressor` | A+ | A+ | C+ | `>=1.2.2`
+
+|https://lz4.github.io/lz4/[LZ4HC] |`LZ4Compressor` | C+ | A+ | B+ | `>= 3.6`
+
+|https://facebook.github.io/zstd/[Zstd] |`ZstdCompressor` | A- | A- | A+ | `>= 4.0`
+
+|http://google.github.io/snappy/[Snappy] |`SnappyCompressor` | A- | A | C | `>= 1.0`
+
+|https://zlib.net[Deflate (zlib)] |`DeflateCompressor` | C | C | A | `>= 1.0`
+|===
+
+Generally speaking for a performance critical (latency or throughput)
+application `LZ4` is the right choice as it gets excellent ratio per CPU
+cycle spent. This is why it is the default choice in Cassandra.
+
+For storage critical applications (disk footprint), however, `Zstd` may
+be a better choice as it can get significant additional ratio to `LZ4`.
+
+`Snappy` is kept for backwards compatibility and `LZ4` will typically be
+preferable.
+
+`Deflate` is kept for backwards compatibility and `Zstd` will typically
+be preferable.
+
+== Configuring Compression
+
+Compression is configured on a per-table basis as an optional argument
+to `CREATE TABLE` or `ALTER TABLE`. Three options are available for all
+compressors:
+
+* `class` (default: `LZ4Compressor`): specifies the compression class to
+use. The two "fast" compressors are `LZ4Compressor` and
+`SnappyCompressor` and the two "good" ratio compressors are
+`ZstdCompressor` and `DeflateCompressor`.
+* `chunk_length_in_kb` (default: `16KiB`): specifies the number of
+kilobytes of data per compression chunk. The main tradeoff here is that
+larger chunk sizes give compression algorithms more context and improve
+their ratio, but require reads to deserialize and read more off disk.
+* `crc_check_chance` (default: `1.0`): determines how likely Cassandra
+is to verify the checksum on each compression chunk during reads to
+protect against data corruption. Unless you have profiles indicating
+this is a performance problem it is highly encouraged not to turn this
+off as it is Cassandra's only protection against bitrot.
+
+The `LZ4Compressor` supports the following additional options:
+
+* `lz4_compressor_type` (default `fast`): specifies if we should use the
+`high` (a.k.a `LZ4HC`) ratio version or the `fast` (a.k.a `LZ4`) version
+of `LZ4`. The `high` mode supports a configurable level, which can allow
+operators to tune the performance <-> ratio tradeoff via the
+`lz4_high_compressor_level` option. Note that in `4.0` and above it may
+be preferable to use the `Zstd` compressor.
+* `lz4_high_compressor_level` (default `9`): A number between `1` and
+`17` inclusive that represents how much CPU time to spend trying to get
+more compression ratio. Generally lower levels are "faster" but they get
+less ratio and higher levels are slower but get more compression ratio.
+
+The `ZstdCompressor` supports the following options in addition:
+
+* `compression_level` (default `3`): A number between `-131072` and `22`
+inclusive that represents how much CPU time to spend trying to get more
+compression ratio. The lower the level, the faster the speed (at the
+cost of ratio). Values from 20 to 22 are called "ultra levels" and
+should be used with caution, as they require more memory. The default of
+`3` is a good choice for competing with `Deflate` ratios and `1` is a
+good choice for competing with `LZ4`.
+
+Users can set compression using the following syntax:
+
+[source,cql]
+----
+CREATE TABLE keyspace.table (id int PRIMARY KEY) 
+   WITH compression = {'class': 'LZ4Compressor'};
+----
+
+Or
+
+[source,cql]
+----
+ALTER TABLE keyspace.table 
+   WITH compression = {'class': 'LZ4Compressor', 'chunk_length_in_kb': 64, 'crc_check_chance': 0.5};
+----
+
+Once enabled, compression can be disabled with `ALTER TABLE` setting
+`enabled` to `false`:
+
+[source,cql]
+----
+ALTER TABLE keyspace.table 
+   WITH compression = {'enabled':'false'};
+----
+
+Operators should be aware, however, that changing compression is not
+immediate. The data is compressed when the SSTable is written, and as
+SSTables are immutable, the compression will not be modified until the
+table is compacted. Upon issuing a change to the compression options via
+`ALTER TABLE`, the existing SSTables will not be modified until they are
+compacted - if an operator needs compression changes to take effect
+immediately, the operator can trigger an SSTable rewrite using
+`nodetool scrub` or `nodetool upgradesstables -a`, both of which will
+rebuild the SSTables on disk, re-compressing the data in the process.
+
+== Benefits and Uses
+
+Compression's primary benefit is that it reduces the amount of data
+written to disk. Not only does the reduced size save in storage
+requirements, it often increases read and write throughput, as the CPU
+overhead of compressing data is faster than the time it would take to
+read or write the larger volume of uncompressed data from disk.
+
+Compression is most useful in tables comprised of many rows, where the
+rows are similar in nature. Tables containing similar text columns (such
+as repeated JSON blobs) often compress very well. Tables containing data
+that has already been compressed or random data (e.g. benchmark
+datasets) do not typically compress well.
+
+== Operational Impact
+
+* Compression metadata is stored off-heap and scales with data on disk.
+This often requires 1-3GB of off-heap RAM per terabyte of data on disk,
+though the exact usage varies with `chunk_length_in_kb` and compression
+ratios.
+* Streaming operations involve compressing and decompressing data on
+compressed tables - in some code paths (such as non-vnode bootstrap),
+the CPU overhead of compression can be a limiting factor.
+* To prevent slow compressors (`Zstd`, `Deflate`, `LZ4HC`) from blocking
+flushes for too long, all three flush with the default fast `LZ4`
+compressor and then rely on normal compaction to re-compress the data
+into the desired compression strategy. See [.title-ref]#CASSANDRA-15379
+<https://issues.apache.org/jira/browse/CASSANDRA-15379># for more
+details.
+* The compression path checksums data to ensure correctness - while the
+traditional Cassandra read path does not have a way to ensure
+correctness of data on disk, compressed tables allow the user to set
+`crc_check_chance` (a float from 0.0 to 1.0) to allow Cassandra to
+probabilistically validate chunks on read to verify bits on disk are not
+corrupt.
+
+== Advanced Use
+
+Advanced users can provide their own compression class by implementing
+the interface at `org.apache.cassandra.io.compress.ICompressor`.
diff --git a/doc/modules/cassandra/pages/operating/hardware.adoc b/doc/modules/cassandra/pages/operating/hardware.adoc
new file mode 100644
index 0000000..24938ad
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/hardware.adoc
@@ -0,0 +1,100 @@
+= Hardware Choices
+
+Like most databases, Cassandra throughput improves with more CPU cores,
+more RAM, and faster disks. While Cassandra can be made to run on small
+servers for testing or development environments (including Raspberry
+Pis), a minimal production server requires at least 2 cores, and at
+least 8GB of RAM. Typical production servers have 8 or more cores and at
+least 32GB of RAM.
+
+== CPU
+
+Cassandra is highly concurrent, handling many simultaneous requests
+(both read and write) using multiple threads running on as many CPU
+cores as possible. The Cassandra write path tends to be heavily
+optimized (writing to the commitlog and then inserting the data into the
+memtable), so writes, in particular, tend to be CPU bound. Consequently,
+adding additional CPU cores often increases throughput of both reads and
+writes.
+
+== Memory
+
+Cassandra runs within a Java VM, which will pre-allocate a fixed size
+heap (java's Xmx system parameter). In addition to the heap, Cassandra
+will use significant amounts of RAM offheap for compression metadata,
+bloom filters, row, key, and counter caches, and an in process page
+cache. Finally, Cassandra will take advantage of the operating system's
+page cache, storing recently accessed portions files in RAM for rapid
+re-use.
+
+For optimal performance, operators should benchmark and tune their
+clusters based on their individual workload. However, basic guidelines
+suggest:
+
+* ECC RAM should always be used, as Cassandra has few internal
+safeguards to protect against bit level corruption
+* The Cassandra heap should be no less than 2GB, and no more than 50% of
+your system RAM
+* Heaps smaller than 12GB should consider ParNew/ConcurrentMarkSweep
+garbage collection
+* Heaps larger than 12GB should consider either:
+** 16GB heap with 8-10GB of new gen, a survivor ratio of 4-6, and a maximum
+tenuring threshold of 6
+** G1GC
+
+== Disks
+
+Cassandra persists data to disk for two very different purposes. The
+first is to the commitlog when a new write is made so that it can be
+replayed after a crash or system shutdown. The second is to the data
+directory when thresholds are exceeded and memtables are flushed to disk
+as SSTables.
+
+Commitlogs receive every write made to a Cassandra node and have the
+potential to block client operations, but they are only ever read on
+node start-up. SSTable (data file) writes on the other hand occur
+asynchronously, but are read to satisfy client look-ups. SSTables are
+also periodically merged and rewritten in a process called compaction.
+The data held in the commitlog directory is data that has not been
+permanently saved to the SSTable data directories - it will be
+periodically purged once it is flushed to the SSTable data files.
+
+Cassandra performs very well on both spinning hard drives and solid
+state disks. In both cases, Cassandra's sorted immutable SSTables allow
+for linear reads, few seeks, and few overwrites, maximizing throughput
+for HDDs and lifespan of SSDs by avoiding write amplification. However,
+when using spinning disks, it's important that the commitlog
+(`commitlog_directory`) be on one physical disk (not simply a partition,
+but a physical disk), and the data files (`data_file_directories`) be
+set to a separate physical disk. By separating the commitlog from the
+data directory, writes can benefit from sequential appends to the
+commitlog without having to seek around the platter as reads request
+data from various SSTables on disk.
+
+In most cases, Cassandra is designed to provide redundancy via multiple
+independent, inexpensive servers. For this reason, using NFS or a SAN
+for data directories is an antipattern and should typically be avoided.
+Similarly, servers with multiple disks are often better served by using
+RAID0 or JBOD than RAID1 or RAID5 - replication provided by Cassandra
+obsoletes the need for replication at the disk layer, so it's typically
+recommended that operators take advantage of the additional throughput
+of RAID0 rather than protecting against failures with RAID1 or RAID5.
+
+== Common Cloud Choices
+
+Many large users of Cassandra run in various clouds, including AWS,
+Azure, and GCE - Cassandra will happily run in any of these
+environments. Users should choose similar hardware to what would be
+needed in physical space. In EC2, popular options include:
+
+* i2 instances, which provide both a high RAM:CPU ratio and local
+ephemeral SSDs
+* i3 instances with NVMe disks
+** EBS works okay if you want easy backups and replacements
+* m4.2xlarge / c4.4xlarge instances, which provide modern CPUs, enhanced
+networking and work well with EBS GP2 (SSD) storage
+
+Generally, disk and network performance increases with instance size and
+generation, so newer generations of instances and larger instance types
+within each family often perform better than their smaller or older
+alternatives.
diff --git a/doc/modules/cassandra/pages/operating/hints.adoc b/doc/modules/cassandra/pages/operating/hints.adoc
new file mode 100644
index 0000000..567e0cd
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/hints.adoc
@@ -0,0 +1,247 @@
+= Hints
+
+Hinting is a data repair technique applied during write operations. When
+replica nodes are unavailable to accept a mutation, either due to
+failure or more commonly routine maintenance, coordinators attempting to
+write to those replicas store temporary hints on their local filesystem
+for later application to the unavailable replica. Hints are an important
+way to help reduce the duration of data inconsistency. Coordinators
+replay hints quickly after unavailable replica nodes return to the ring.
+Hints are best effort, however, and do not guarantee eventual
+consistency like xref:operating/repair.adoc[`anti-entropy repair`] does.
+
+Hints are useful because of how Apache Cassandra replicates data to
+provide fault tolerance, high availability and durability. Cassandra
+xref:architecture/dynamo.adoc#consistent-hashing-using-a-token-ring[`partitions data across the cluster`] using
+consistent hashing, and then replicates keys to multiple nodes along the
+hash ring. To guarantee availability, all replicas of a key can accept
+mutations without consensus, but this means it is possible for some
+replicas to accept a mutation while others do not. When this happens an
+inconsistency is introduced.
+
+Hints are one of the three ways, in addition to read-repair and
+full/incremental anti-entropy repair, that Cassandra implements the
+eventual consistency guarantee that all updates are eventually received
+by all replicas. Hints, like read-repair, are best effort and not an
+alternative to performing full repair, but they do help reduce the
+duration of inconsistency between replicas in practice.
+
+== Hinted Handoff
+
+Hinted handoff is the process by which Cassandra applies hints to
+unavailable nodes.
+
+For example, consider a mutation is to be made at `Consistency Level`
+`LOCAL_QUORUM` against a keyspace with `Replication Factor` of `3`.
+Normally the client sends the mutation to a single coordinator, who then
+sends the mutation to all three replicas, and when two of the three
+replicas acknowledge the mutation the coordinator responds successfully
+to the client. If a replica node is unavailable, however, the
+coordinator stores a hint locally to the filesystem for later
+application. New hints will be retained for up to
+`max_hint_window_in_ms` of downtime (defaults to `3 hours`). If the
+unavailable replica does return to the cluster before the window
+expires, the coordinator applies any pending hinted mutations against
+the replica to ensure that eventual consistency is maintained.
+
+image::hints.svg[Hinted Handoff in Action]
+
+* (`t0`): The write is sent by the client, and the coordinator sends it
+to the three replicas. Unfortunately `replica_2` is restarting and
+cannot receive the mutation.
+* (`t1`): The client receives a quorum acknowledgement from the
+coordinator. At this point the client believe the write to be durable
+and visible to reads (which it is).
+* (`t2`): After the write timeout (default `2s`), the coordinator
+decides that `replica_2` is unavailable and stores a hint to its local
+disk.
+* (`t3`): Later, when `replica_2` starts back up it sends a gossip
+message to all nodes, including the coordinator.
+* (`t4`): The coordinator replays hints including the missed mutation
+against `replica_2`.
+
+If the node does not return in time, the destination replica will be
+permanently out of sync until either read-repair or full/incremental
+anti-entropy repair propagates the mutation.
+
+=== Application of Hints
+
+Hints are streamed in bulk, a segment at a time, to the target replica
+node and the target node replays them locally. After the target node has
+replayed a segment it deletes the segment and receives the next segment.
+This continues until all hints are drained.
+
+=== Storage of Hints on Disk
+
+Hints are stored in flat files in the coordinator node’s
+`$CASSANDRA_HOME/data/hints` directory. A hint includes a hint id, the
+target replica node on which the mutation is meant to be stored, the
+serialized mutation (stored as a blob) that couldn't be delivered to the
+replica node, the mutation timestamp, and the Cassandra version used to
+serialize the mutation. By default hints are compressed using
+`LZ4Compressor`. Multiple hints are appended to the same hints file.
+
+Since hints contain the original unmodified mutation timestamp, hint
+application is idempotent and cannot overwrite a future mutation.
+
+=== Hints for Timed Out Write Requests
+
+Hints are also stored for write requests that time out. The
+`write_request_timeout_in_ms` setting in `cassandra.yaml` configures the
+timeout for write requests.
+
+[source,none]
+----
+write_request_timeout_in_ms: 2000
+----
+
+The coordinator waits for the configured amount of time for write
+requests to complete, at which point it will time out and generate a
+hint for the timed out request. The lowest acceptable value for
+`write_request_timeout_in_ms` is 10 ms.
+
+== Configuring Hints
+
+Hints are enabled by default as they are critical for data consistency.
+The `cassandra.yaml` configuration file provides several settings for
+configuring hints:
+
+Table 1. Settings for Hints
+
+[width="100%",cols="38%,36%,26%",]
+|===
+|Setting |Description |Default Value
+
+|`hinted_handoff_enabled` |Enables/Disables hinted handoffs |`true`
+
+|`hinted_handoff_disabled_datacenters` a|
+A list of data centers that do not perform hinted handoffs even when
+handoff is otherwise enabled. Example:
+
+[source,yaml]
+----
+hinted_handoff_disabled_datacenters:
+  - DC1
+  - DC2
+----
+
+|`unset`
+
+|`max_hint_window_in_ms` |Defines the maximum amount of time (ms) a node
+shall have hints generated after it has failed. |`10800000` # 3 hours
+
+|`hinted_handoff_throttle_in_kb` |Maximum throttle in KBs per second,
+per delivery thread. This will be reduced proportionally to the number
+of nodes in the cluster. (If there are two nodes in the cluster, each
+delivery thread will use the maximum rate; if there are 3, each will
+throttle to half of the maximum,since it is expected for two nodes to be
+delivering hints simultaneously.) |`1024`
+
+|`max_hints_delivery_threads` |Number of threads with which to deliver
+hints; Consider increasing this number when you have multi-dc
+deployments, since cross-dc handoff tends to be slower |`2`
+
+|`hints_directory` |Directory where Cassandra stores hints.
+|`$CASSANDRA_HOME/data/hints`
+
+|`hints_flush_period_in_ms` |How often hints should be flushed from the
+internal buffers to disk. Will _not_ trigger fsync. |`10000`
+
+|`max_hints_file_size_in_mb` |Maximum size for a single hints file, in
+megabytes. |`128`
+
+|`hints_compression` |Compression to apply to the hint files. If
+omitted, hints files will be written uncompressed. LZ4, Snappy, and
+Deflate compressors are supported. |`LZ4Compressor`
+|===
+
+== Configuring Hints at Runtime with `nodetool`
+
+`nodetool` provides several commands for configuring hints or getting
+hints related information. The nodetool commands override the
+corresponding settings if any in `cassandra.yaml` for the node running
+the command.
+
+Table 2. Nodetool Commands for Hints
+
+[width="100%",cols="43%,57%",]
+|===
+|Command |Description
+
+|`nodetool disablehandoff` |Disables storing and delivering hints
+
+|`nodetool disablehintsfordc` |Disables storing and delivering hints to
+a data center
+
+|`nodetool enablehandoff` |Re-enables future hints storing and delivery
+on the current node
+
+|`nodetool enablehintsfordc` |Enables hints for a data center that was
+previously disabled
+
+|`nodetool getmaxhintwindow` |Prints the max hint window in ms. New in
+Cassandra 4.0.
+
+|`nodetool handoffwindow` |Prints current hinted handoff window
+
+|`nodetool pausehandoff` |Pauses hints delivery process
+
+|`nodetool resumehandoff` |Resumes hints delivery process
+
+|`nodetool sethintedhandoffthrottlekb` |Sets hinted handoff throttle in
+kb per second, per delivery thread
+
+|`nodetool setmaxhintwindow` |Sets the specified max hint window in ms
+
+|`nodetool statushandoff` |Status of storing future hints on the current
+node
+
+|`nodetool truncatehints` |Truncates all hints on the local node, or
+truncates hints for the endpoint(s) specified.
+|===
+
+=== Make Hints Play Faster at Runtime
+
+The default of `1024 kbps` handoff throttle is conservative for most
+modern networks, and it is entirely possible that in a simple node
+restart you may accumulate many gigabytes hints that may take hours to
+play back. For example if you are ingesting `100 Mbps` of data per node,
+a single 10 minute long restart will create
+`10 minutes * (100 megabit / second) ~= 7 GiB` of data which at
+`(1024 KiB / second)` would take
+`7.5 GiB / (1024 KiB / second) = 2.03 hours` to play back. The exact
+math depends on the load balancing strategy (round robin is better than
+token aware), number of tokens per node (more tokens is better than
+fewer), and naturally the cluster's write rate, but regardless you may
+find yourself wanting to increase this throttle at runtime.
+
+If you find yourself in such a situation, you may consider raising the
+`hinted_handoff_throttle` dynamically via the
+`nodetool sethintedhandoffthrottlekb` command.
+
+=== Allow a Node to be Down Longer at Runtime
+
+Sometimes a node may be down for more than the normal
+`max_hint_window_in_ms`, (default of three hours), but the hardware and
+data itself will still be accessible. In such a case you may consider
+raising the `max_hint_window_in_ms` dynamically via the
+`nodetool setmaxhintwindow` command added in Cassandra 4.0
+(https://issues.apache.org/jira/browse/CASSANDRA-11720[CASSANDRA-11720]).
+This will instruct Cassandra to continue holding hints for the down
+endpoint for a longer amount of time.
+
+This command should be applied on all nodes in the cluster that may be
+holding hints. If needed, the setting can be applied permanently by
+setting the `max_hint_window_in_ms` setting in `cassandra.yaml` followed
+by a rolling restart.
+
+== Monitoring Hint Delivery
+
+Cassandra 4.0 adds histograms available to understand how long it takes
+to deliver hints which is useful for operators to better identify
+problems
+(https://issues.apache.org/jira/browse/CASSANDRA-13234[CASSANDRA-13234]).
+
+There are also metrics available for tracking
+`Hinted Handoff <handoff-metrics>` and
+`Hints Service <hintsservice-metrics>` metrics.
diff --git a/doc/modules/cassandra/pages/operating/index.adoc b/doc/modules/cassandra/pages/operating/index.adoc
new file mode 100644
index 0000000..46b7076
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/index.adoc
@@ -0,0 +1,14 @@
+== Operating Cassandra
+
+* xref:operating/hardware.adoc[Hardware]
+* xref:operating/security.adoc[Security]
+* xref:operating/topo_changes.adoc[Topology changes]
+* xref:operating/hints.adoc[Hints]
+* xref:operating/repair.adoc[Repair]
+* xref:operating/backups.adoc[Backups]
+* xref:operating/compression.adoc[Compression]
+* xref:operating/compaction/index.adoc[Compaction]
+* xref:operating/metrics.adoc[Monitoring]
+* xref:operating/bulk_loading.adoc[Bulk loading]
+* xref:operating/cdc.adoc[CDC]
+* xref:operating/bloom_filters.adoc[Bloom filters]
diff --git a/doc/modules/cassandra/pages/operating/metrics.adoc b/doc/modules/cassandra/pages/operating/metrics.adoc
new file mode 100644
index 0000000..1eb8156
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/metrics.adoc
@@ -0,0 +1,1088 @@
+= Monitoring
+
+Metrics in Cassandra are managed using the
+http://metrics.dropwizard.io[Dropwizard Metrics] library. These metrics
+can be queried via JMX or pushed to external monitoring systems using a
+number of
+http://metrics.dropwizard.io/3.1.0/getting-started/#other-reporting[built
+in] and http://metrics.dropwizard.io/3.1.0/manual/third-party/[third
+party] reporter plugins.
+
+Metrics are collected for a single node. It's up to the operator to use
+an external monitoring system to aggregate them.
+
+== Metric Types
+
+All metrics reported by cassandra fit into one of the following types.
+
+`Gauge`::
+  An instantaneous measurement of a value.
+`Counter`::
+  A gauge for an `AtomicLong` instance. Typically this is consumed by
+  monitoring the change since the last call to see if there is a large
+  increase compared to the norm.
+`Histogram`::
+  Measures the statistical distribution of values in a stream of data.
+  +
+  In addition to minimum, maximum, mean, etc., it also measures median,
+  75th, 90th, 95th, 98th, 99th, and 99.9th percentiles.
+`Timer`::
+  Measures both the rate that a particular piece of code is called and
+  the histogram of its duration.
+`Latency`::
+  Special type that tracks latency (in microseconds) with a `Timer` plus
+  a `Counter` that tracks the total latency accrued since starting. The
+  former is useful if you track the change in total latency since the
+  last check. Each metric name of this type will have 'Latency' and
+  'TotalLatency' appended to it.
+`Meter`::
+  A meter metric which measures mean throughput and one-, five-, and
+  fifteen-minute exponentially-weighted moving average throughputs.
+
+== Table Metrics
+
+Each table in Cassandra has metrics responsible for tracking its state
+and performance.
+
+The metric names are all appended with the specific `Keyspace` and
+`Table` name.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.Table.<MetricName>.<Keyspace>.<Table>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=Table keyspace=<Keyspace> scope=<Table> name=<MetricName>`
+
+[NOTE]
+.Note
+====
+There is a special table called '`all`' without a keyspace. This
+represents the aggregation of metrics across *all* tables and keyspaces
+on the node.
+====[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|MemtableOnHeapSize |Gauge<Long> |Total amount of data stored in the
+memtable that resides *on*-heap, including column related overhead and
+partitions overwritten.
+
+|MemtableOffHeapSize |Gauge<Long> |Total amount of data stored in the
+memtable that resides *off*-heap, including column related overhead and
+partitions overwritten.
+
+|MemtableLiveDataSize |Gauge<Long> |Total amount of live data stored in
+the memtable, excluding any data structure overhead.
+
+|AllMemtablesOnHeapSize |Gauge<Long> |Total amount of data stored in the
+memtables (2i and pending flush memtables included) that resides
+*on*-heap.
+
+|AllMemtablesOffHeapSize |Gauge<Long> |Total amount of data stored in
+the memtables (2i and pending flush memtables included) that resides
+*off*-heap.
+
+|AllMemtablesLiveDataSize |Gauge<Long> |Total amount of live data stored
+in the memtables (2i and pending flush memtables included) that resides
+off-heap, excluding any data structure overhead.
+
+|MemtableColumnsCount |Gauge<Long> |Total number of columns present in
+the memtable.
+
+|MemtableSwitchCount |Counter |Number of times flush has resulted in the
+memtable being switched out.
+
+|CompressionRatio |Gauge<Double> |Current compression ratio for all
+SSTables.
+
+|EstimatedPartitionSizeHistogram |Gauge<long[]> |Histogram of estimated
+partition size (in bytes).
+
+|EstimatedPartitionCount |Gauge<Long> |Approximate number of keys in
+table.
+
+|EstimatedColumnCountHistogram |Gauge<long[]> |Histogram of estimated
+number of columns.
+
+|SSTablesPerReadHistogram |Histogram |Histogram of the number of sstable
+data files accessed per single partition read. SSTables skipped due to
+Bloom Filters, min-max key or partition index lookup are not taken into
+acoount.
+
+|ReadLatency |Latency |Local read latency for this table.
+
+|RangeLatency |Latency |Local range scan latency for this table.
+
+|WriteLatency |Latency |Local write latency for this table.
+
+|CoordinatorReadLatency |Timer |Coordinator read latency for this table.
+
+|CoordinatorWriteLatency |Timer |Coordinator write latency for this
+table.
+
+|CoordinatorScanLatency |Timer |Coordinator range scan latency for this
+table.
+
+|PendingFlushes |Counter |Estimated number of flush tasks pending for
+this table.
+
+|BytesFlushed |Counter |Total number of bytes flushed since server
+[re]start.
+
+|CompactionBytesWritten |Counter |Total number of bytes written by
+compaction since server [re]start.
+
+|PendingCompactions |Gauge<Integer> |Estimate of number of pending
+compactions for this table.
+
+|LiveSSTableCount |Gauge<Integer> |Number of SSTables on disk for this
+table.
+
+|LiveDiskSpaceUsed |Counter |Disk space used by SSTables belonging to
+this table (in bytes).
+
+|TotalDiskSpaceUsed |Counter |Total disk space used by SSTables
+belonging to this table, including obsolete ones waiting to be GC'd.
+
+|MinPartitionSize |Gauge<Long> |Size of the smallest compacted partition
+(in bytes).
+
+|MaxPartitionSize |Gauge<Long> |Size of the largest compacted partition
+(in bytes).
+
+|MeanPartitionSize |Gauge<Long> |Size of the average compacted partition
+(in bytes).
+
+|BloomFilterFalsePositives |Gauge<Long> |Number of false positives on
+table's bloom filter.
+
+|BloomFilterFalseRatio |Gauge<Double> |False positive ratio of table's
+bloom filter.
+
+|BloomFilterDiskSpaceUsed |Gauge<Long> |Disk space used by bloom filter
+(in bytes).
+
+|BloomFilterOffHeapMemoryUsed |Gauge<Long> |Off-heap memory used by
+bloom filter.
+
+|IndexSummaryOffHeapMemoryUsed |Gauge<Long> |Off-heap memory used by
+index summary.
+
+|CompressionMetadataOffHeapMemoryUsed |Gauge<Long> |Off-heap memory used
+by compression meta data.
+
+|KeyCacheHitRate |Gauge<Double> |Key cache hit rate for this table.
+
+|TombstoneScannedHistogram |Histogram |Histogram of tombstones scanned
+in queries on this table.
+
+|LiveScannedHistogram |Histogram |Histogram of live cells scanned in
+queries on this table.
+
+|ColUpdateTimeDeltaHistogram |Histogram |Histogram of column update time
+delta on this table.
+
+|ViewLockAcquireTime |Timer |Time taken acquiring a partition lock for
+materialized view updates on this table.
+
+|ViewReadTime |Timer |Time taken during the local read of a materialized
+view update.
+
+|TrueSnapshotsSize |Gauge<Long> |Disk space used by snapshots of this
+table including all SSTable components.
+
+|RowCacheHitOutOfRange |Counter |Number of table row cache hits that do
+not satisfy the query filter, thus went to disk.
+
+|RowCacheHit |Counter |Number of table row cache hits.
+
+|RowCacheMiss |Counter |Number of table row cache misses.
+
+|CasPrepare |Latency |Latency of paxos prepare round.
+
+|CasPropose |Latency |Latency of paxos propose round.
+
+|CasCommit |Latency |Latency of paxos commit round.
+
+|PercentRepaired |Gauge<Double> |Percent of table data that is repaired
+on disk.
+
+|BytesRepaired |Gauge<Long> |Size of table data repaired on disk
+
+|BytesUnrepaired |Gauge<Long> |Size of table data unrepaired on disk
+
+|BytesPendingRepair |Gauge<Long> |Size of table data isolated for an
+ongoing incremental repair
+
+|SpeculativeRetries |Counter |Number of times speculative retries were
+sent for this table.
+
+|SpeculativeFailedRetries |Counter |Number of speculative retries that
+failed to prevent a timeout
+
+|SpeculativeInsufficientReplicas |Counter |Number of speculative retries
+that couldn't be attempted due to lack of replicas
+
+|SpeculativeSampleLatencyNanos |Gauge<Long> |Number of nanoseconds to
+wait before speculation is attempted. Value may be statically configured
+or updated periodically based on coordinator latency.
+
+|WaitingOnFreeMemtableSpace |Histogram |Histogram of time spent waiting
+for free memtable space, either on- or off-heap.
+
+|DroppedMutations |Counter |Number of dropped mutations on this table.
+
+|AnticompactionTime |Timer |Time spent anticompacting before a
+consistent repair.
+
+|ValidationTime |Timer |Time spent doing validation compaction during
+repair.
+
+|SyncTime |Timer |Time spent doing streaming during repair.
+
+|BytesValidated |Histogram |Histogram over the amount of bytes read
+during validation.
+
+|PartitionsValidated |Histogram |Histogram over the number of partitions
+read during validation.
+
+|BytesAnticompacted |Counter |How many bytes we anticompacted.
+
+|BytesMutatedAnticompaction |Counter |How many bytes we avoided
+anticompacting because the sstable was fully contained in the repaired
+range.
+
+|MutatedAnticompactionGauge |Gauge<Double> |Ratio of bytes mutated vs
+total bytes repaired.
+|===
+
+== Keyspace Metrics
+
+Each keyspace in Cassandra has metrics responsible for tracking its
+state and performance.
+
+Most of these metrics are the same as the `Table Metrics` above, only
+they are aggregated at the Keyspace level. The keyspace specific metrics
+are specified in the table below.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.keyspace.<MetricName>.<Keyspace>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=Keyspace scope=<Keyspace> name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|WriteFailedIdeaCL |Counter |Number of writes that failed to achieve the
+configured ideal consistency level or 0 if none is configured
+
+|IdealCLWriteLatency |Latency |Coordinator latency of writes at the
+configured ideal consistency level. No values are recorded if ideal
+consistency level is not configured
+
+|RepairTime |Timer |Total time spent as repair coordinator.
+
+|RepairPrepareTime |Timer |Total time spent preparing for repair.
+|===
+
+== ThreadPool Metrics
+
+Cassandra splits work of a particular type into its own thread pool.
+This provides back-pressure and asynchrony for requests on a node. It's
+important to monitor the state of these thread pools since they can tell
+you how saturated a node is.
+
+The metric names are all appended with the specific `ThreadPool` name.
+The thread pools are also categorized under a specific type.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.ThreadPools.<MetricName>.<Path>.<ThreadPoolName>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=ThreadPools path=<Path> scope=<ThreadPoolName> name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|ActiveTasks |Gauge<Integer> |Number of tasks being actively worked on
+by this pool.
+
+|PendingTasks |Gauge<Integer> |Number of queued tasks queued up on this
+pool.
+
+|CompletedTasks |Counter |Number of tasks completed.
+
+|TotalBlockedTasks |Counter |Number of tasks that were blocked due to
+queue saturation.
+
+|CurrentlyBlockedTask |Counter |Number of tasks that are currently
+blocked due to queue saturation but on retry will become unblocked.
+
+|MaxPoolSize |Gauge<Integer> |The maximum number of threads in this
+pool.
+
+|MaxTasksQueued |Gauge<Integer> |The maximum number of tasks queued
+before a task get blocked.
+|===
+
+The following thread pools can be monitored.
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|Native-Transport-Requests |transport |Handles client CQL requests
+
+|CounterMutationStage |request |Responsible for counter writes
+
+|ViewMutationStage |request |Responsible for materialized view writes
+
+|MutationStage |request |Responsible for all other writes
+
+|ReadRepairStage |request |ReadRepair happens on this thread pool
+
+|ReadStage |request |Local reads run on this thread pool
+
+|RequestResponseStage |request |Coordinator requests to the cluster run
+on this thread pool
+
+|AntiEntropyStage |internal |Builds merkle tree for repairs
+
+|CacheCleanupExecutor |internal |Cache maintenance performed on this
+thread pool
+
+|CompactionExecutor |internal |Compactions are run on these threads
+
+|GossipStage |internal |Handles gossip requests
+
+|HintsDispatcher |internal |Performs hinted handoff
+
+|InternalResponseStage |internal |Responsible for intra-cluster
+callbacks
+
+|MemtableFlushWriter |internal |Writes memtables to disk
+
+|MemtablePostFlush |internal |Cleans up commit log after memtable is
+written to disk
+
+|MemtableReclaimMemory |internal |Memtable recycling
+
+|MigrationStage |internal |Runs schema migrations
+
+|MiscStage |internal |Misceleneous tasks run here
+
+|PendingRangeCalculator |internal |Calculates token range
+
+|PerDiskMemtableFlushWriter_0 |internal |Responsible for writing a spec
+(there is one of these per disk 0-N)
+
+|Sampler |internal |Responsible for re-sampling the index summaries of
+SStables
+
+|SecondaryIndexManagement |internal |Performs updates to secondary
+indexes
+
+|ValidationExecutor |internal |Performs validation compaction or
+scrubbing
+
+|ViewBuildExecutor |internal |Performs materialized views initial build
+|===
+
+== Client Request Metrics
+
+Client requests have their own set of metrics that encapsulate the work
+happening at coordinator level.
+
+Different types of client requests are broken down by `RequestType`.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.ClientRequest.<MetricName>.<RequestType>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=ClientRequest scope=<RequestType> name=<MetricName>`
+
+RequestType::
+  CASRead
+Description::
+  Metrics related to transactional read requests.
+Metrics::
+  [cols=",,",options="header",]
+  |===
+  |Name |Type |Description
+  |Timeouts |Counter |Number of timeouts encountered.
+
+  |Failures |Counter |Number of transaction failures encountered.
+
+  |  |Latency |Transaction read latency.
+
+  |Unavailables |Counter |Number of unavailable exceptions encountered.
+
+  |UnfinishedCommit |Counter |Number of transactions that were committed
+  on read.
+
+  |ConditionNotMet |Counter |Number of transaction preconditions did not
+  match current values.
+
+  |ContentionHistogram |Histogram |How many contended reads were
+  encountered
+  |===
+RequestType::
+  CASWrite
+Description::
+  Metrics related to transactional write requests.
+Metrics::
+  [cols=",,",options="header",]
+  |===
+  |Name |Type |Description
+  |Timeouts |Counter |Number of timeouts encountered.
+
+  |Failures |Counter |Number of transaction failures encountered.
+
+  |  |Latency |Transaction write latency.
+
+  |UnfinishedCommit |Counter |Number of transactions that were committed
+  on write.
+
+  |ConditionNotMet |Counter |Number of transaction preconditions did not
+  match current values.
+
+  |ContentionHistogram |Histogram |How many contended writes were
+  encountered
+
+  |MutationSizeHistogram |Histogram |Total size in bytes of the requests
+  mutations.
+  |===
+RequestType::
+  Read
+Description::
+  Metrics related to standard read requests.
+Metrics::
+  [cols=",,",options="header",]
+  |===
+  |Name |Type |Description
+  |Timeouts |Counter |Number of timeouts encountered.
+  |Failures |Counter |Number of read failures encountered.
+  |  |Latency |Read latency.
+  |Unavailables |Counter |Number of unavailable exceptions encountered.
+  |===
+RequestType::
+  RangeSlice
+Description::
+  Metrics related to token range read requests.
+Metrics::
+  [cols=",,",options="header",]
+  |===
+  |Name |Type |Description
+  |Timeouts |Counter |Number of timeouts encountered.
+  |Failures |Counter |Number of range query failures encountered.
+  |  |Latency |Range query latency.
+  |Unavailables |Counter |Number of unavailable exceptions encountered.
+  |===
+RequestType::
+  Write
+Description::
+  Metrics related to regular write requests.
+Metrics::
+  [cols=",,",options="header",]
+  |===
+  |Name |Type |Description
+  |Timeouts |Counter |Number of timeouts encountered.
+
+  |Failures |Counter |Number of write failures encountered.
+
+  |  |Latency |Write latency.
+
+  |Unavailables |Counter |Number of unavailable exceptions encountered.
+
+  |MutationSizeHistogram |Histogram |Total size in bytes of the requests
+  mutations.
+  |===
+RequestType::
+  ViewWrite
+Description::
+  Metrics related to materialized view write wrtes.
+Metrics::
+  [cols=",,",]
+  |===
+  |Timeouts |Counter |Number of timeouts encountered.
+
+  |Failures |Counter |Number of transaction failures encountered.
+
+  |Unavailables |Counter |Number of unavailable exceptions encountered.
+
+  |ViewReplicasAttempted |Counter |Total number of attempted view
+  replica writes.
+
+  |ViewReplicasSuccess |Counter |Total number of succeded view replica
+  writes.
+
+  |ViewPendingMutations |Gauge<Long> |ViewReplicasAttempted -
+  ViewReplicasSuccess.
+
+  |ViewWriteLatency |Timer |Time between when mutation is applied to
+  base table and when CL.ONE is achieved on view.
+  |===
+
+== Cache Metrics
+
+Cassandra caches have metrics to track the effectivness of the caches.
+Though the `Table Metrics` might be more useful.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.Cache.<MetricName>.<CacheName>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=Cache scope=<CacheName> name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|Capacity |Gauge<Long> |Cache capacity in bytes.
+|Entries |Gauge<Integer> |Total number of cache entries.
+|FifteenMinuteCacheHitRate |Gauge<Double> |15m cache hit rate.
+|FiveMinuteCacheHitRate |Gauge<Double> |5m cache hit rate.
+|OneMinuteCacheHitRate |Gauge<Double> |1m cache hit rate.
+|HitRate |Gauge<Double> |All time cache hit rate.
+|Hits |Meter |Total number of cache hits.
+|Misses |Meter |Total number of cache misses.
+|MissLatency |Timer |Latency of misses.
+|Requests |Gauge<Long> |Total number of cache requests.
+|Size |Gauge<Long> |Total size of occupied cache, in bytes.
+|===
+
+The following caches are covered:
+
+[cols=",",options="header",]
+|===
+|Name |Description
+|CounterCache |Keeps hot counters in memory for performance.
+|ChunkCache |In process uncompressed page cache.
+|KeyCache |Cache for partition to sstable offsets.
+|RowCache |Cache for rows kept in memory.
+|===
+
+[NOTE]
+.Note
+====
+Misses and MissLatency are only defined for the ChunkCache
+====== CQL Metrics
+
+Metrics specific to CQL prepared statement caching.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.CQL.<MetricName>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=CQL name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|PreparedStatementsCount |Gauge<Integer> |Number of cached prepared
+statements.
+
+|PreparedStatementsEvicted |Counter |Number of prepared statements
+evicted from the prepared statement cache
+
+|PreparedStatementsExecuted |Counter |Number of prepared statements
+executed.
+
+|RegularStatementsExecuted |Counter |Number of *non* prepared statements
+executed.
+
+|PreparedStatementsRatio |Gauge<Double> |Percentage of statements that
+are prepared vs unprepared.
+|===
+
+[[dropped-metrics]]
+== DroppedMessage Metrics
+
+Metrics specific to tracking dropped messages for different types of
+requests. Dropped writes are stored and retried by `Hinted Handoff`
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.DroppedMessage.<MetricName>.<Type>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=DroppedMessage scope=<Type> name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|CrossNodeDroppedLatency |Timer |The dropped latency across nodes.
+|InternalDroppedLatency |Timer |The dropped latency within node.
+|Dropped |Meter |Number of dropped messages.
+|===
+
+The different types of messages tracked are:
+
+[cols=",",options="header",]
+|===
+|Name |Description
+|BATCH_STORE |Batchlog write
+|BATCH_REMOVE |Batchlog cleanup (after succesfully applied)
+|COUNTER_MUTATION |Counter writes
+|HINT |Hint replay
+|MUTATION |Regular writes
+|READ |Regular reads
+|READ_REPAIR |Read repair
+|PAGED_SLICE |Paged read
+|RANGE_SLICE |Token range read
+|REQUEST_RESPONSE |RPC Callbacks
+|_TRACE |Tracing writes
+|===
+
+== Streaming Metrics
+
+Metrics reported during `Streaming` operations, such as repair,
+bootstrap, rebuild.
+
+These metrics are specific to a peer endpoint, with the source node
+being the node you are pulling the metrics from.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.Streaming.<MetricName>.<PeerIP>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=Streaming scope=<PeerIP> name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|IncomingBytes |Counter |Number of bytes streamed to this node from the
+peer.
+
+|OutgoingBytes |Counter |Number of bytes streamed to the peer endpoint
+from this node.
+|===
+
+== Compaction Metrics
+
+Metrics specific to `Compaction` work.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.Compaction.<MetricName>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=Compaction name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|BytesCompacted |Counter |Total number of bytes compacted since server
+[re]start.
+
+|PendingTasks |Gauge<Integer> |Estimated number of compactions remaining
+to perform.
+
+|CompletedTasks |Gauge<Long> |Number of completed compactions since
+server [re]start.
+
+|TotalCompactionsCompleted |Meter |Throughput of completed compactions
+since server [re]start.
+
+|PendingTasksByTableName |Gauge<Map<String, Map<String, Integer>>>
+|Estimated number of compactions remaining to perform, grouped by
+keyspace and then table name. This info is also kept in `Table Metrics`.
+|===
+
+== CommitLog Metrics
+
+Metrics specific to the `CommitLog`
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.CommitLog.<MetricName>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=CommitLog name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|CompletedTasks |Gauge<Long> |Total number of commit log messages
+written since [re]start.
+
+|PendingTasks |Gauge<Long> |Number of commit log messages written but
+yet to be fsync'd.
+
+|TotalCommitLogSize |Gauge<Long> |Current size, in bytes, used by all
+the commit log segments.
+
+|WaitingOnSegmentAllocation |Timer |Time spent waiting for a
+CommitLogSegment to be allocated - under normal conditions this should
+be zero.
+
+|WaitingOnCommit |Timer |The time spent waiting on CL fsync; for
+Periodic this is only occurs when the sync is lagging its sync interval.
+|===
+
+== Storage Metrics
+
+Metrics specific to the storage engine.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.Storage.<MetricName>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=Storage name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|Exceptions |Counter |Number of internal exceptions caught. Under normal
+exceptions this should be zero.
+
+|Load |Counter |Size, in bytes, of the on disk data size this node
+manages.
+
+|TotalHints |Counter |Number of hint messages written to this node since
+[re]start. Includes one entry for each host to be hinted per hint.
+
+|TotalHintsInProgress |Counter |Number of hints attemping to be sent
+currently.
+|===
+
+[[handoff-metrics]]
+== HintedHandoff Metrics
+
+Metrics specific to Hinted Handoff. There are also some metrics related
+to hints tracked in `Storage Metrics`
+
+These metrics include the peer endpoint *in the metric name*
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.HintedHandOffManager.<MetricName>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=HintedHandOffManager name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|Hints_created-<PeerIP> a|
+____
+Counter
+____
+
+a|
+____
+Number of hints on disk for this peer.
+____
+
+|Hints_not_stored-<PeerIP> a|
+____
+Counter
+____
+
+a|
+____
+Number of hints not stored for this peer, due to being down past the
+configured hint window.
+____
+
+|===
+
+== HintsService Metrics
+
+Metrics specific to the Hints delivery service. There are also some
+metrics related to hints tracked in `Storage Metrics`
+
+These metrics include the peer endpoint *in the metric name*
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.HintsService.<MetricName>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=HintsService name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|HintsSucceeded a|
+____
+Meter
+____
+
+a|
+____
+A meter of the hints successfully delivered
+____
+
+|HintsFailed a|
+____
+Meter
+____
+
+a|
+____
+A meter of the hints that failed deliver
+____
+
+|HintsTimedOut a|
+____
+Meter
+____
+
+a|
+____
+A meter of the hints that timed out
+____
+
+|Hint_delays |Histogram |Histogram of hint delivery delays (in
+milliseconds)
+
+|Hint_delays-<PeerIP> |Histogram |Histogram of hint delivery delays (in
+milliseconds) per peer
+|===
+
+== SSTable Index Metrics
+
+Metrics specific to the SSTable index metadata.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.Index.<MetricName>.RowIndexEntry`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=Index scope=RowIndexEntry name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|IndexedEntrySize |Histogram |Histogram of the on-heap size, in bytes,
+of the index across all SSTables.
+
+|IndexInfoCount |Histogram |Histogram of the number of on-heap index
+entries managed across all SSTables.
+
+|IndexInfoGets |Histogram |Histogram of the number index seeks performed
+per SSTable.
+|===
+
+== BufferPool Metrics
+
+Metrics specific to the internal recycled buffer pool Cassandra manages.
+This pool is meant to keep allocations and GC lower by recycling on and
+off heap buffers.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.BufferPool.<MetricName>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=BufferPool name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|Size |Gauge<Long> |Size, in bytes, of the managed buffer pool
+
+|Misses |Meter a|
+____
+The rate of misses in the pool. The higher this is the more allocations
+incurred.
+____
+
+|===
+
+== Client Metrics
+
+Metrics specifc to client managment.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.Client.<MetricName>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=Client name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|connectedNativeClients |Gauge<Integer> |Number of clients connected to
+this nodes native protocol server
+
+|connections |Gauge<List<Map<String, String>> |List of all connections
+and their state information
+
+|connectedNativeClientsByUser |Gauge<Map<String, Int> |Number of
+connnective native clients by username
+|===
+
+== Batch Metrics
+
+Metrics specifc to batch statements.
+
+Reported name format:
+
+*Metric Name*::
+  `org.apache.cassandra.metrics.Batch.<MetricName>`
+*JMX MBean*::
+  `org.apache.cassandra.metrics:type=Batch name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|PartitionsPerCounterBatch |Histogram |Distribution of the number of
+partitions processed per counter batch
+
+|PartitionsPerLoggedBatch |Histogram |Distribution of the number of
+partitions processed per logged batch
+
+|PartitionsPerUnloggedBatch |Histogram |Distribution of the number of
+partitions processed per unlogged batch
+|===
+
+== JVM Metrics
+
+JVM metrics such as memory and garbage collection statistics can either
+be accessed by connecting to the JVM using JMX or can be exported using
+link:#metric-reporters[Metric Reporters].
+
+=== BufferPool
+
+*Metric Name*::
+  `jvm.buffers.<direct|mapped>.<MetricName>`
+*JMX MBean*::
+  `java.nio:type=BufferPool name=<direct|mapped>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|Capacity |Gauge<Long> |Estimated total capacity of the buffers in this
+pool
+
+|Count |Gauge<Long> |Estimated number of buffers in the pool
+
+|Used |Gauge<Long> |Estimated memory that the Java virtual machine is
+using for this buffer pool
+|===
+
+=== FileDescriptorRatio
+
+*Metric Name*::
+  `jvm.fd.<MetricName>`
+*JMX MBean*::
+  `java.lang:type=OperatingSystem name=<OpenFileDescriptorCount|MaxFileDescriptorCount>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|Usage |Ratio |Ratio of used to total file descriptors
+|===
+
+=== GarbageCollector
+
+*Metric Name*::
+  `jvm.gc.<gc_type>.<MetricName>`
+*JMX MBean*::
+  `java.lang:type=GarbageCollector name=<gc_type>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|Count |Gauge<Long> |Total number of collections that have occurred
+
+|Time |Gauge<Long> |Approximate accumulated collection elapsed time in
+milliseconds
+|===
+
+=== Memory
+
+*Metric Name*::
+  `jvm.memory.<heap/non-heap/total>.<MetricName>`
+*JMX MBean*::
+  `java.lang:type=Memory`
+
+[cols=",,",]
+|===
+|Committed |Gauge<Long> |Amount of memory in bytes that is committed for
+the JVM to use
+
+|Init |Gauge<Long> |Amount of memory in bytes that the JVM initially
+requests from the OS
+
+|Max |Gauge<Long> |Maximum amount of memory in bytes that can be used
+for memory management
+
+|Usage |Ratio |Ratio of used to maximum memory
+
+|Used |Gauge<Long> |Amount of used memory in bytes
+|===
+
+=== MemoryPool
+
+*Metric Name*::
+  `jvm.memory.pools.<memory_pool>.<MetricName>`
+*JMX MBean*::
+  `java.lang:type=MemoryPool name=<memory_pool>`
+
+[cols=",,",]
+|===
+|Committed |Gauge<Long> |Amount of memory in bytes that is committed for
+the JVM to use
+
+|Init |Gauge<Long> |Amount of memory in bytes that the JVM initially
+requests from the OS
+
+|Max |Gauge<Long> |Maximum amount of memory in bytes that can be used
+for memory management
+
+|Usage |Ratio |Ratio of used to maximum memory
+
+|Used |Gauge<Long> |Amount of used memory in bytes
+|===
+
+== JMX
+
+Any JMX based client can access metrics from cassandra.
+
+If you wish to access JMX metrics over http it's possible to download
+http://mx4j.sourceforge.net/[Mx4jTool] and place `mx4j-tools.jar` into
+the classpath. On startup you will see in the log:
+
+[source,none]
+----
+HttpAdaptor version 3.0.2 started on port 8081
+----
+
+To choose a different port (8081 is the default) or a different listen
+address (0.0.0.0 is not the default) edit `conf/cassandra-env.sh` and
+uncomment:
+
+[source,none]
+----
+#MX4J_ADDRESS="-Dmx4jaddress=0.0.0.0"
+
+#MX4J_PORT="-Dmx4jport=8081"
+----
+
+== Metric Reporters
+
+As mentioned at the top of this section on monitoring the Cassandra
+metrics can be exported to a number of monitoring system a number of
+http://metrics.dropwizard.io/3.1.0/getting-started/#other-reporting[built
+in] and http://metrics.dropwizard.io/3.1.0/manual/third-party/[third
+party] reporter plugins.
+
+The configuration of these plugins is managed by the
+https://github.com/addthis/metrics-reporter-config[metrics reporter
+config project]. There is a sample configuration file located at
+`conf/metrics-reporter-config-sample.yaml`.
+
+Once configured, you simply start cassandra with the flag
+`-Dcassandra.metricsReporterConfigFile=metrics-reporter-config.yaml`.
+The specified .yaml file plus any 3rd party reporter jars must all be in
+Cassandra's classpath.
diff --git a/doc/modules/cassandra/pages/operating/repair.adoc b/doc/modules/cassandra/pages/operating/repair.adoc
new file mode 100644
index 0000000..3c6b20d
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/repair.adoc
@@ -0,0 +1,222 @@
+= Repair
+
+Cassandra is designed to remain available if one of it's nodes is down
+or unreachable. However, when a node is down or unreachable, it needs to
+eventually discover the writes it missed. Hints attempt to inform a node
+of missed writes, but are a best effort, and aren't guaranteed to inform
+a node of 100% of the writes it missed. These inconsistencies can
+eventually result in data loss as nodes are replaced or tombstones
+expire.
+
+These inconsistencies are fixed with the repair process. Repair
+synchronizes the data between nodes by comparing their respective
+datasets for their common token ranges, and streaming the differences
+for any out of sync sections between the nodes. It compares the data
+with merkle trees, which are a hierarchy of hashes.
+
+== Incremental and Full Repairs
+
+There are 2 types of repairs: full repairs, and incremental repairs.
+Full repairs operate over all of the data in the token range being
+repaired. Incremental repairs only repair data that's been written since
+the previous incremental repair.
+
+Incremental repairs are the default repair type, and if run regularly,
+can significantly reduce the time and io cost of performing a repair.
+However, it's important to understand that once an incremental repair
+marks data as repaired, it won't try to repair it again. This is fine
+for syncing up missed writes, but it doesn't protect against things like
+disk corruption, data loss by operator error, or bugs in Cassandra. For
+this reason, full repairs should still be run occasionally.
+
+== Usage and Best Practices
+
+Since repair can result in a lot of disk and network io, it's not run
+automatically by Cassandra. It is run by the operator via nodetool.
+
+Incremental repair is the default and is run with the following command:
+
+[source,none]
+----
+nodetool repair
+----
+
+A full repair can be run with the following command:
+
+[source,none]
+----
+nodetool repair --full
+----
+
+Additionally, repair can be run on a single keyspace:
+
+[source,none]
+----
+nodetool repair [options] <keyspace_name>
+----
+
+Or even on specific tables:
+
+[source,none]
+----
+nodetool repair [options] <keyspace_name> <table1> <table2>
+----
+
+The repair command only repairs token ranges on the node being repaired,
+it doesn't repair the whole cluster. By default, repair will operate on
+all token ranges replicated by the node you're running repair on, which
+will cause duplicate work if you run it on every node. The `-pr` flag
+will only repair the "primary" ranges on a node, so you can repair your
+entire cluster by running `nodetool repair -pr` on each node in a single
+datacenter.
+
+The specific frequency of repair that's right for your cluster, of
+course, depends on several factors. However, if you're just starting out
+and looking for somewhere to start, running an incremental repair every
+1-3 days, and a full repair every 1-3 weeks is probably reasonable. If
+you don't want to run incremental repairs, a full repair every 5 days is
+a good place to start.
+
+At a minimum, repair should be run often enough that the gc grace period
+never expires on unrepaired data. Otherwise, deleted data could
+reappear. With a default gc grace period of 10 days, repairing every
+node in your cluster at least once every 7 days will prevent this, while
+providing enough slack to allow for delays.
+
+== Other Options
+
+`-pr, --partitioner-range`::
+  Restricts repair to the 'primary' token ranges of the node being
+  repaired. A primary range is just a token range for which a node is
+  the first replica in the ring.
+`-prv, --preview`::
+  Estimates the amount of streaming that would occur for the given
+  repair command. This builds the merkle trees, and prints the expected
+  streaming activity, but does not actually do any streaming. By
+  default, incremental repairs are estimated, add the `--full` flag to
+  estimate a full repair.
+`-vd, --validate`::
+  Verifies that the repaired data is the same across all nodes. Similiar
+  to `--preview`, this builds and compares merkle trees of repaired
+  data, but doesn't do any streaming. This is useful for
+  troubleshooting. If this shows that the repaired data is out of sync,
+  a full repair should be run.
+
+`nodetool repair docs <nodetool_repair>`
+
+== Full Repair Example
+
+Full repair is typically needed to redistribute data after increasing
+the replication factor of a keyspace or after adding a node to the
+cluster. Full repair involves streaming SSTables. To demonstrate full
+repair start with a three node cluster.
+
+[source,none]
+----
+[ec2-user@ip-10-0-2-238 ~]$ nodetool status
+Datacenter: us-east-1
+=====================
+Status=Up/Down
+|/ State=Normal/Leaving/Joining/Moving
+--  Address   Load        Tokens  Owns  Host ID                              Rack
+UN  10.0.1.115  547 KiB     256    ?  b64cb32a-b32a-46b4-9eeb-e123fa8fc287  us-east-1b
+UN  10.0.3.206  617.91 KiB  256    ?  74863177-684b-45f4-99f7-d1006625dc9e  us-east-1d
+UN  10.0.2.238  670.26 KiB  256    ?  4dcdadd2-41f9-4f34-9892-1f20868b27c7  us-east-1c
+----
+
+Create a keyspace with replication factor 3:
+
+[source,none]
+----
+cqlsh> DROP KEYSPACE cqlkeyspace;
+cqlsh> CREATE KEYSPACE CQLKeyspace
+  ... WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
+----
+
+Add a table to the keyspace:
+
+[source,none]
+----
+cqlsh> use cqlkeyspace;
+cqlsh:cqlkeyspace> CREATE TABLE t (
+           ...   id int,
+           ...   k int,
+           ...   v text,
+           ...   PRIMARY KEY (id)
+           ... );
+----
+
+Add table data:
+
+[source,none]
+----
+cqlsh:cqlkeyspace> INSERT INTO t (id, k, v) VALUES (0, 0, 'val0');
+cqlsh:cqlkeyspace> INSERT INTO t (id, k, v) VALUES (1, 1, 'val1');
+cqlsh:cqlkeyspace> INSERT INTO t (id, k, v) VALUES (2, 2, 'val2');
+----
+
+A query lists the data added:
+
+[source,none]
+----
+cqlsh:cqlkeyspace> SELECT * FROM t;
+
+id | k | v
+----+---+------
+ 1 | 1 | val1
+ 0 | 0 | val0
+ 2 | 2 | val2
+(3 rows)
+----
+
+Make the following changes to a three node cluster:
+
+[arabic]
+. Increase the replication factor from 3 to 4.
+. Add a 4th node to the cluster
+
+When the replication factor is increased the following message gets
+output indicating that a full repair is needed as per
+(https://issues.apache.org/jira/browse/CASSANDRA-13079[CASSANDRA-13079]):
+
+[source,none]
+----
+cqlsh:cqlkeyspace> ALTER KEYSPACE CQLKeyspace
+           ... WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 4};
+Warnings :
+When increasing replication factor you need to run a full (-full) repair to distribute the
+data.
+----
+
+Perform a full repair on the keyspace `cqlkeyspace` table `t` with
+following command:
+
+[source,none]
+----
+nodetool repair -full cqlkeyspace t
+----
+
+Full repair completes in about a second as indicated by the output:
+
+[source,none]
+----
+[ec2-user@ip-10-0-2-238 ~]$ nodetool repair -full cqlkeyspace t
+[2019-08-17 03:06:21,445] Starting repair command #1 (fd576da0-c09b-11e9-b00c-1520e8c38f00), repairing keyspace cqlkeyspace with repair options (parallelism: parallel, primary range: false, incremental: false, job threads: 1, ColumnFamilies: [t], dataCenters: [], hosts: [], previewKind: NONE, # of ranges: 1024, pull repair: false, force repair: false, optimise streams: false)
+[2019-08-17 03:06:23,059] Repair session fd8e5c20-c09b-11e9-b00c-1520e8c38f00 for range [(-8792657144775336505,-8786320730900698730], (-5454146041421260303,-5439402053041523135], (4288357893651763201,4324309707046452322], ... , (4350676211955643098,4351706629422088296]] finished (progress: 0%)
+[2019-08-17 03:06:23,077] Repair completed successfully
+[2019-08-17 03:06:23,077] Repair command #1 finished in 1 second
+[ec2-user@ip-10-0-2-238 ~]$
+----
+
+The `nodetool  tpstats` command should list a repair having been
+completed as `Repair-Task` > `Completed` column value of 1:
+
+[source,none]
+----
+[ec2-user@ip-10-0-2-238 ~]$ nodetool tpstats
+Pool Name Active   Pending Completed   Blocked  All time blocked
+ReadStage  0           0           99       0              0
+…
+Repair-Task 0       0           1        0              0
+RequestResponseStage                  0        0        2078        0               0
+----
diff --git a/doc/modules/cassandra/pages/operating/security.adoc b/doc/modules/cassandra/pages/operating/security.adoc
new file mode 100644
index 0000000..a74c042
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/security.adoc
@@ -0,0 +1,527 @@
+= Security
+
+There are three main components to the security features provided by
+Cassandra:
+
+* TLS/SSL encryption for client and inter-node communication
+* Client authentication
+* Authorization
+
+By default, these features are disabled as Cassandra is configured to
+easily find and be found by other members of a cluster. In other words,
+an out-of-the-box Cassandra installation presents a large attack surface
+for a bad actor. Enabling authentication for clients using the binary
+protocol is not sufficient to protect a cluster. Malicious users able to
+access internode communication and JMX ports can still:
+
+* Craft internode messages to insert users into authentication schema
+* Craft internode messages to truncate or drop schema
+* Use tools such as `sstableloader` to overwrite `system_auth` tables
+* Attach to the cluster directly to capture write traffic
+
+Correct configuration of all three security components should negate
+theses vectors. Therefore, understanding Cassandra's security features
+is crucial to configuring your cluster to meet your security needs.
+
+== TLS/SSL Encryption
+
+Cassandra provides secure communication between a client machine and a
+database cluster and between nodes within a cluster. Enabling encryption
+ensures that data in flight is not compromised and is transferred
+securely. The options for client-to-node and node-to-node encryption are
+managed separately and may be configured independently.
+
+In both cases, the JVM defaults for supported protocols and cipher
+suites are used when encryption is enabled. These can be overidden using
+the settings in `cassandra.yaml`, but this is not recommended unless
+there are policies in place which dictate certain settings or a need to
+disable vulnerable ciphers or protocols in cases where the JVM cannot be
+updated.
+
+FIPS compliant settings can be configured at the JVM level and should
+not involve changing encryption settings in cassandra.yaml. See
+https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/FIPS.html[the
+java document on FIPS] for more details.
+
+For information on generating the keystore and truststore files used in
+SSL communications, see the
+http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore[java
+documentation on creating keystores]
+
+== SSL Certificate Hot Reloading
+
+Beginning with Cassandra 4, Cassandra supports hot reloading of SSL
+Certificates. If SSL/TLS support is enabled in Cassandra, the node
+periodically polls the Trust and Key Stores specified in cassandra.yaml.
+When the files are updated, Cassandra will reload them and use them for
+subsequent connections. Please note that the Trust & Key Store passwords
+are part of the yaml so the updated files should also use the same
+passwords. The default polling interval is 10 minutes.
+
+Certificate Hot reloading may also be triggered using the
+`nodetool reloadssl` command. Use this if you want to Cassandra to
+immediately notice the changed certificates.
+
+=== Inter-node Encryption
+
+The settings for managing inter-node encryption are found in
+`cassandra.yaml` in the `server_encryption_options` section. To enable
+inter-node encryption, change the `internode_encryption` setting from
+its default value of `none` to one value from: `rack`, `dc` or `all`.
+
+=== Client to Node Encryption
+
+The settings for managing client to node encryption are found in
+`cassandra.yaml` in the `client_encryption_options` section. There are
+two primary toggles here for enabling encryption, `enabled` and
+`optional`.
+
+* If neither is set to `true`, client connections are entirely
+unencrypted.
+* If `enabled` is set to `true` and `optional` is set to `false`, all
+client connections must be secured.
+* If both options are set to `true`, both encrypted and unencrypted
+connections are supported using the same port. Client connections using
+encryption with this configuration will be automatically detected and
+handled by the server.
+
+As an alternative to the `optional` setting, separate ports can also be
+configured for secure and unsecure connections where operational
+requirements demand it. To do so, set `optional` to false and use the
+`native_transport_port_ssl` setting in `cassandra.yaml` to specify the
+port to be used for secure client communication.
+
+[[operation-roles]]
+== Roles
+
+Cassandra uses database roles, which may represent either a single user
+or a group of users, in both authentication and permissions management.
+Role management is an extension point in Cassandra and may be configured
+using the `role_manager` setting in `cassandra.yaml`. The default
+setting uses `CassandraRoleManager`, an implementation which stores role
+information in the tables of the `system_auth` keyspace.
+
+See also the xref:cql/security.adoc#database-roles[`CQL documentation on roles`].
+
+== Authentication
+
+Authentication is pluggable in Cassandra and is configured using the
+`authenticator` setting in `cassandra.yaml`. Cassandra ships with two
+options included in the default distribution.
+
+By default, Cassandra is configured with `AllowAllAuthenticator` which
+performs no authentication checks and therefore requires no credentials.
+It is used to disable authentication completely. Note that
+authentication is a necessary condition of Cassandra's permissions
+subsystem, so if authentication is disabled, effectively so are
+permissions.
+
+The default distribution also includes `PasswordAuthenticator`, which
+stores encrypted credentials in a system table. This can be used to
+enable simple username/password authentication.
+
+[[password-authentication]]
+=== Enabling Password Authentication
+
+Before enabling client authentication on the cluster, client
+applications should be pre-configured with their intended credentials.
+When a connection is initiated, the server will only ask for credentials
+once authentication is enabled, so setting up the client side config in
+advance is safe. In contrast, as soon as a server has authentication
+enabled, any connection attempt without proper credentials will be
+rejected which may cause availability problems for client applications.
+Once clients are setup and ready for authentication to be enabled,
+follow this procedure to enable it on the cluster.
+
+Pick a single node in the cluster on which to perform the initial
+configuration. Ideally, no clients should connect to this node during
+the setup process, so you may want to remove it from client config,
+block it at the network level or possibly add a new temporary node to
+the cluster for this purpose. On that node, perform the following steps:
+
+[arabic]
+. Open a `cqlsh` session and change the replication factor of the
+`system_auth` keyspace. By default, this keyspace uses
+`SimpleReplicationStrategy` and a `replication_factor` of 1. It is
+recommended to change this for any non-trivial deployment to ensure that
+should nodes become unavailable, login is still possible. Best practice
+is to configure a replication factor of 3 to 5 per-DC.
+
+[source,cql]
+----
+ALTER KEYSPACE system_auth WITH replication = {'class': 'NetworkTopologyStrategy', 'DC1': 3, 'DC2': 3};
+----
+
+[arabic, start=2]
+. Edit `cassandra.yaml` to change the `authenticator` option like so:
+
+[source,yaml]
+----
+authenticator: PasswordAuthenticator
+----
+
+[arabic, start=3]
+. Restart the node.
+. Open a new `cqlsh` session using the credentials of the default
+superuser:
+
+[source,bash]
+----
+$ cqlsh -u cassandra -p cassandra
+----
+
+[arabic, start=5]
+. During login, the credentials for the default superuser are read with
+a consistency level of `QUORUM`, whereas those for all other users
+(including superusers) are read at `LOCAL_ONE`. In the interests of
+performance and availability, as well as security, operators should
+create another superuser and disable the default one. This step is
+optional, but highly recommended. While logged in as the default
+superuser, create another superuser role which can be used to bootstrap
+further configuration.
+
+[source,cql]
+----
+# create a new superuser
+CREATE ROLE dba WITH SUPERUSER = true AND LOGIN = true AND PASSWORD = 'super';
+----
+
+[arabic, start=6]
+. Start a new cqlsh session, this time logging in as the new_superuser
+and disable the default superuser.
+
+[source,cql]
+----
+ALTER ROLE cassandra WITH SUPERUSER = false AND LOGIN = false;
+----
+
+[arabic, start=7]
+. Finally, set up the roles and credentials for your application users
+with xref:cql/security.adoc#create-role[`CREATE ROLE`] statements.
+
+At the end of these steps, the one node is configured to use password
+authentication. To roll that out across the cluster, repeat steps 2 and
+3 on each node in the cluster. Once all nodes have been restarted,
+authentication will be fully enabled throughout the cluster.
+
+Note that using `PasswordAuthenticator` also requires the use of
+xref:cql/security.adoc#operation-roles[`CassandraRoleManager`].
+
+See also: `setting-credentials-for-internal-authentication`,
+xref:cql/security.adoc#create-role[`CREATE ROLE`],
+xref:cql/security.adoc#alter-role[`ALTER ROLE`],
+xref:xref:cql/security.adoc#alter-keyspace[`ALTER KEYSPACE`] and 
+xref:cql/security.adoc#grant-permission[`GRANT PERMISSION`].
+
+== Authorization
+
+Authorization is pluggable in Cassandra and is configured using the
+`authorizer` setting in `cassandra.yaml`. Cassandra ships with two
+options included in the default distribution.
+
+By default, Cassandra is configured with `AllowAllAuthorizer` which
+performs no checking and so effectively grants all permissions to all
+roles. This must be used if `AllowAllAuthenticator` is the configured
+authenticator.
+
+The default distribution also includes `CassandraAuthorizer`, which does
+implement full permissions management functionality and stores its data
+in Cassandra system tables.
+
+=== Enabling Internal Authorization
+
+Permissions are modelled as a whitelist, with the default assumption
+that a given role has no access to any database resources. The
+implication of this is that once authorization is enabled on a node, all
+requests will be rejected until the required permissions have been
+granted. For this reason, it is strongly recommended to perform the
+initial setup on a node which is not processing client requests.
+
+The following assumes that authentication has already been enabled via
+the process outlined in `password-authentication`. Perform these steps
+to enable internal authorization across the cluster:
+
+[arabic]
+. On the selected node, edit `cassandra.yaml` to change the `authorizer`
+option like so:
+
+[source,yaml]
+----
+authorizer: CassandraAuthorizer
+----
+
+[arabic, start=2]
+. Restart the node.
+. Open a new `cqlsh` session using the credentials of a role with
+superuser credentials:
+
+[source,bash]
+----
+$ cqlsh -u dba -p super
+----
+
+[arabic, start=4]
+. Configure the appropriate access privileges for your clients using
+link:cql.html#grant-permission[GRANT PERMISSION] statements. On the
+other nodes, until configuration is updated and the node restarted, this
+will have no effect so disruption to clients is avoided.
+
+[source,cql]
+----
+GRANT SELECT ON ks.t1 TO db_user;
+----
+
+[arabic, start=5]
+. Once all the necessary permissions have been granted, repeat steps 1
+and 2 for each node in turn. As each node restarts and clients
+reconnect, the enforcement of the granted permissions will begin.
+
+See also: xref:cql/security.adoc#grant-permission[`GRANT PERMISSION`],
+xref:cql/security.adoc#grant-all[`GRANT ALL`] and 
+xref:cql/security.adoc#revoke-permission[`REVOKE PERMISSION`].
+
+[[auth-caching]]
+== Caching
+
+Enabling authentication and authorization places additional load on the
+cluster by frequently reading from the `system_auth` tables.
+Furthermore, these reads are in the critical paths of many client
+operations, and so has the potential to severely impact quality of
+service. To mitigate this, auth data such as credentials, permissions
+and role details are cached for a configurable period. The caching can
+be configured (and even disabled) from `cassandra.yaml` or using a JMX
+client. The JMX interface also supports invalidation of the various
+caches, but any changes made via JMX are not persistent and will be
+re-read from `cassandra.yaml` when the node is restarted.
+
+Each cache has 3 options which can be set:
+
+Validity Period::
+  Controls the expiration of cache entries. After this period, entries
+  are invalidated and removed from the cache.
+Refresh Rate::
+  Controls the rate at which background reads are performed to pick up
+  any changes to the underlying data. While these async refreshes are
+  performed, caches will continue to serve (possibly) stale data.
+  Typically, this will be set to a shorter time than the validity
+  period.
+Max Entries::
+  Controls the upper bound on cache size.
+
+The naming for these options in `cassandra.yaml` follows the convention:
+
+* `<type>_validity_in_ms`
+* `<type>_update_interval_in_ms`
+* `<type>_cache_max_entries`
+
+Where `<type>` is one of `credentials`, `permissions`, or `roles`.
+
+As mentioned, these are also exposed via JMX in the mbeans under the
+`org.apache.cassandra.auth` domain.
+
+== JMX access
+
+Access control for JMX clients is configured separately to that for CQL.
+For both authentication and authorization, two providers are available;
+the first based on standard JMX security and the second which integrates
+more closely with Cassandra's own auth subsystem.
+
+The default settings for Cassandra make JMX accessible only from
+localhost. To enable remote JMX connections, edit `cassandra-env.sh` (or
+`cassandra-env.ps1` on Windows) to change the `LOCAL_JMX` setting to
+`no`. Under the standard configuration, when remote JMX connections are
+enabled, `standard JMX authentication <standard-jmx-auth>` is also
+switched on.
+
+Note that by default, local-only connections are not subject to
+authentication, but this can be enabled.
+
+If enabling remote connections, it is recommended to also use
+xref:operating/security.adoc#jmx-with-ssl[`SSL`] connections.
+
+Finally, after enabling auth and/or SSL, ensure that tools which use
+JMX, such as xref:tools/nodetool/nodetools.adoc[`nodetool`] are correctly configured and working
+as expected.
+
+=== Standard JMX Auth
+
+Users permitted to connect to the JMX server are specified in a simple
+text file. The location of this file is set in `cassandra-env.sh` by the
+line:
+
+[source,bash]
+----
+JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.password.file=/etc/cassandra/jmxremote.password"
+----
+
+Edit the password file to add username/password pairs:
+
+[source,none]
+----
+jmx_user jmx_password
+----
+
+Secure the credentials file so that only the user running the Cassandra
+process can read it :
+
+[source,bash]
+----
+$ chown cassandra:cassandra /etc/cassandra/jmxremote.password
+$ chmod 400 /etc/cassandra/jmxremote.password
+----
+
+Optionally, enable access control to limit the scope of what defined
+users can do via JMX. Note that this is a fairly blunt instrument in
+this context as most operational tools in Cassandra require full
+read/write access. To configure a simple access file, uncomment this
+line in `cassandra-env.sh`:
+
+[source,bash]
+----
+#JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.access.file=/etc/cassandra/jmxremote.access"
+----
+
+Then edit the access file to grant your JMX user readwrite permission:
+
+[source,none]
+----
+jmx_user readwrite
+----
+
+Cassandra must be restarted to pick up the new settings.
+
+See also :
+http://docs.oracle.com/javase/7/docs/technotes/guides/management/agent.html#gdenv[Using
+File-Based Password Authentication In JMX]
+
+=== Cassandra Integrated Auth
+
+An alternative to the out-of-the-box JMX auth is to useeCassandra's own
+authentication and/or authorization providers for JMX clients. This is
+potentially more flexible and secure but it come with one major caveat.
+Namely that it is not available until [.title-ref]#after# a node has
+joined the ring, because the auth subsystem is not fully configured
+until that point However, it is often critical for monitoring purposes
+to have JMX access particularly during bootstrap. So it is recommended,
+where possible, to use local only JMX auth during bootstrap and then, if
+remote connectivity is required, to switch to integrated auth once the
+node has joined the ring and initial setup is complete.
+
+With this option, the same database roles used for CQL authentication
+can be used to control access to JMX, so updates can be managed
+centrally using just `cqlsh`. Furthermore, fine grained control over
+exactly which operations are permitted on particular MBeans can be
+acheived via xref:cql/security.adoc#grant-permission[`GRANT PERMISSION`].
+
+To enable integrated authentication, edit `cassandra-env.sh` to
+uncomment these lines:
+
+[source,bash]
+----
+#JVM_OPTS="$JVM_OPTS -Dcassandra.jmx.remote.login.config=CassandraLogin"
+#JVM_OPTS="$JVM_OPTS -Djava.security.auth.login.config=$CASSANDRA_HOME/conf/cassandra-jaas.config"
+----
+
+And disable the JMX standard auth by commenting this line:
+
+[source,bash]
+----
+JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.password.file=/etc/cassandra/jmxremote.password"
+----
+
+To enable integrated authorization, uncomment this line:
+
+[source,bash]
+----
+#JVM_OPTS="$JVM_OPTS -Dcassandra.jmx.authorizer=org.apache.cassandra.auth.jmx.AuthorizationProxy"
+----
+
+Check standard access control is off by ensuring this line is commented
+out:
+
+[source,bash]
+----
+#JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.access.file=/etc/cassandra/jmxremote.access"
+----
+
+With integrated authentication and authorization enabled, operators can
+define specific roles and grant them access to the particular JMX
+resources that they need. For example, a role with the necessary
+permissions to use tools such as jconsole or jmc in read-only mode would
+be defined as:
+
+[source,cql]
+----
+CREATE ROLE jmx WITH LOGIN = false;
+GRANT SELECT ON ALL MBEANS TO jmx;
+GRANT DESCRIBE ON ALL MBEANS TO jmx;
+GRANT EXECUTE ON MBEAN 'java.lang:type=Threading' TO jmx;
+GRANT EXECUTE ON MBEAN 'com.sun.management:type=HotSpotDiagnostic' TO jmx;
+
+# Grant the role with necessary permissions to use nodetool commands (including nodetool status) in read-only mode
+GRANT EXECUTE ON MBEAN 'org.apache.cassandra.db:type=EndpointSnitchInfo' TO jmx;
+GRANT EXECUTE ON MBEAN 'org.apache.cassandra.db:type=StorageService' TO jmx;
+
+# Grant the jmx role to one with login permissions so that it can access the JMX tooling
+CREATE ROLE ks_user WITH PASSWORD = 'password' AND LOGIN = true AND SUPERUSER = false;
+GRANT jmx TO ks_user;
+----
+
+Fine grained access control to individual MBeans is also supported:
+
+[source,cql]
+----
+GRANT EXECUTE ON MBEAN 'org.apache.cassandra.db:type=Tables,keyspace=test_keyspace,table=t1' TO ks_user;
+GRANT EXECUTE ON MBEAN 'org.apache.cassandra.db:type=Tables,keyspace=test_keyspace,table=*' TO ks_owner;
+----
+
+This permits the `ks_user` role to invoke methods on the MBean
+representing a single table in `test_keyspace`, while granting the same
+permission for all table level MBeans in that keyspace to the `ks_owner`
+role.
+
+Adding/removing roles and granting/revoking of permissions is handled
+dynamically once the initial setup is complete, so no further restarts
+are required if permissions are altered.
+
+See also: xref:cql/security.adoc#permissions[`Permissions`].
+
+=== JMX With SSL
+
+JMX SSL configuration is controlled by a number of system properties,
+some of which are optional. To turn on SSL, edit the relevant lines in
+`cassandra-env.sh` (or `cassandra-env.ps1` on Windows) to uncomment and
+set the values of these properties as required:
+
+`com.sun.management.jmxremote.ssl`::
+  set to true to enable SSL
+`com.sun.management.jmxremote.ssl.need.client.auth`::
+  set to true to enable validation of client certificates
+`com.sun.management.jmxremote.registry.ssl`::
+  enables SSL sockets for the RMI registry from which clients obtain the
+  JMX connector stub
+`com.sun.management.jmxremote.ssl.enabled.protocols`::
+  by default, the protocols supported by the JVM will be used, override
+  with a comma-separated list. Note that this is not usually necessary
+  and using the defaults is the preferred option.
+`com.sun.management.jmxremote.ssl.enabled.cipher.suites`::
+  by default, the cipher suites supported by the JVM will be used,
+  override with a comma-separated list. Note that this is not usually
+  necessary and using the defaults is the preferred option.
+`javax.net.ssl.keyStore`::
+  set the path on the local filesystem of the keystore containing server
+  private keys and public certificates
+`javax.net.ssl.keyStorePassword`::
+  set the password of the keystore file
+`javax.net.ssl.trustStore`::
+  if validation of client certificates is required, use this property to
+  specify the path of the truststore containing the public certificates
+  of trusted clients
+`javax.net.ssl.trustStorePassword`::
+  set the password of the truststore file
+
+See also:
+http://docs.oracle.com/javase/7/docs/technotes/guides/management/agent.html#gdemv[Oracle
+Java7 Docs],
+https://www.lullabot.com/articles/monitor-java-with-jmx[Monitor Java
+with JMX]
diff --git a/doc/modules/cassandra/pages/operating/topo_changes.adoc b/doc/modules/cassandra/pages/operating/topo_changes.adoc
new file mode 100644
index 0000000..368056d
--- /dev/null
+++ b/doc/modules/cassandra/pages/operating/topo_changes.adoc
@@ -0,0 +1,133 @@
+= Adding, replacing, moving and removing nodes
+
+== Bootstrap
+
+Adding new nodes is called "bootstrapping". The `num_tokens` parameter
+will define the amount of virtual nodes (tokens) the joining node will
+be assigned during bootstrap. The tokens define the sections of the ring
+(token ranges) the node will become responsible for.
+
+=== Token allocation
+
+With the default token allocation algorithm the new node will pick
+`num_tokens` random tokens to become responsible for. Since tokens are
+distributed randomly, load distribution improves with a higher amount of
+virtual nodes, but it also increases token management overhead. The
+default of 256 virtual nodes should provide a reasonable load balance
+with acceptable overhead.
+
+On 3.0+ a new token allocation algorithm was introduced to allocate
+tokens based on the load of existing virtual nodes for a given keyspace,
+and thus yield an improved load distribution with a lower number of
+tokens. To use this approach, the new node must be started with the JVM
+option `-Dcassandra.allocate_tokens_for_keyspace=<keyspace>`, where
+`<keyspace>` is the keyspace from which the algorithm can find the load
+information to optimize token assignment for.
+
+==== Manual token assignment
+
+You may specify a comma-separated list of tokens manually with the
+`initial_token` `cassandra.yaml` parameter, and if that is specified
+Cassandra will skip the token allocation process. This may be useful
+when doing token assignment with an external tool or when restoring a
+node with its previous tokens.
+
+=== Range streaming
+
+After the tokens are allocated, the joining node will pick current
+replicas of the token ranges it will become responsible for to stream
+data from. By default it will stream from the primary replica of each
+token range in order to guarantee data in the new node will be
+consistent with the current state.
+
+In the case of any unavailable replica, the consistent bootstrap process
+will fail. To override this behavior and potentially miss data from an
+unavailable replica, set the JVM flag
+`-Dcassandra.consistent.rangemovement=false`.
+
+=== Resuming failed/hanged bootstrap
+
+On 2.2+, if the bootstrap process fails, it's possible to resume
+bootstrap from the previous saved state by calling
+`nodetool bootstrap resume`. If for some reason the bootstrap hangs or
+stalls, it may also be resumed by simply restarting the node. In order
+to cleanup bootstrap state and start fresh, you may set the JVM startup
+flag `-Dcassandra.reset_bootstrap_progress=true`.
+
+On lower versions, when the bootstrap proces fails it is recommended to
+wipe the node (remove all the data), and restart the bootstrap process
+again.
+
+=== Manual bootstrapping
+
+It's possible to skip the bootstrapping process entirely and join the
+ring straight away by setting the hidden parameter
+`auto_bootstrap: false`. This may be useful when restoring a node from a
+backup or creating a new data-center.
+
+== Removing nodes
+
+You can take a node out of the cluster with `nodetool decommission` to a
+live node, or `nodetool removenode` (to any other machine) to remove a
+dead one. This will assign the ranges the old node was responsible for
+to other nodes, and replicate the appropriate data there. If
+decommission is used, the data will stream from the decommissioned node.
+If removenode is used, the data will stream from the remaining replicas.
+
+No data is removed automatically from the node being decommissioned, so
+if you want to put the node back into service at a different token on
+the ring, it should be removed manually.
+
+== Moving nodes
+
+When `num_tokens: 1` it's possible to move the node position in the ring
+with `nodetool move`. Moving is both a convenience over and more
+efficient than decommission + bootstrap. After moving a node,
+`nodetool cleanup` should be run to remove any unnecessary data.
+
+== Replacing a dead node
+
+In order to replace a dead node, start cassandra with the JVM startup
+flag `-Dcassandra.replace_address_first_boot=<dead_node_ip>`. Once this
+property is enabled the node starts in a hibernate state, during which
+all the other nodes will see this node to be DOWN (DN), however this
+node will see itself as UP (UN). Accurate replacement state can be found
+in `nodetool netstats`.
+
+The replacing node will now start to bootstrap the data from the rest of
+the nodes in the cluster. A replacing node will only receive writes
+during the bootstrapping phase if it has a different ip address to the
+node that is being replaced. (See CASSANDRA-8523 and CASSANDRA-12344)
+
+Once the bootstrapping is complete the node will be marked "UP".
+
+[NOTE]
+.Note
+====
+If any of the following cases apply, you *MUST* run repair to make the
+replaced node consistent again, since it missed ongoing writes
+during/prior to bootstrapping. The _replacement_ timeframe refers to the
+period from when the node initially dies to when a new node completes
+the replacement process.
+
+[arabic]
+. The node is down for longer than `max_hint_window_in_ms` before being
+replaced.
+. You are replacing using the same IP address as the dead node *and*
+replacement takes longer than `max_hint_window_in_ms`.
+====
+
+== Monitoring progress
+
+Bootstrap, replace, move and remove progress can be monitored using
+`nodetool netstats` which will show the progress of the streaming
+operations.
+
+== Cleanup data after range movements
+
+As a safety measure, Cassandra does not automatically remove data from
+nodes that "lose" part of their token range due to a range movement
+operation (bootstrap, move, replace). Run `nodetool cleanup` on the
+nodes that lost ranges to the joining node when you are satisfied the
+new node is up and working. If you do not do this the old data will
+still be counted against the load on that node.
diff --git a/doc/modules/cassandra/pages/plugins/index.adoc b/doc/modules/cassandra/pages/plugins/index.adoc
new file mode 100644
index 0000000..dbb048a
--- /dev/null
+++ b/doc/modules/cassandra/pages/plugins/index.adoc
@@ -0,0 +1,36 @@
+= Third-Party Plugins
+
+Available third-party plugins for Apache Cassandra
+
+== CAPI-Rowcache
+
+The Coherent Accelerator Process Interface (CAPI) is a general term for
+the infrastructure of attaching a Coherent accelerator to an IBM POWER
+system. A key innovation in IBM POWER8’s open architecture is the CAPI.
+It provides a high bandwidth, low latency path between external devices,
+the POWER8 core, and the system’s open memory architecture. IBM Data
+Engine for NoSQL is an integrated platform for large and fast growing
+NoSQL data stores. It builds on the CAPI capability of POWER8 systems
+and provides super-fast access to large flash storage capacity and
+addresses the challenges associated with typical x86 server based
+scale-out deployments.
+
+The official page for the
+https://github.com/ppc64le/capi-rowcache[CAPI-Rowcache plugin] contains
+further details how to build/run/download the plugin.
+
+== Stratio’s Cassandra Lucene Index
+
+Stratio’s Lucene index is a Cassandra secondary index implementation
+based on http://lucene.apache.org/[Apache Lucene]. It extends
+Cassandra’s functionality to provide near real-time distributed search
+engine capabilities such as with ElasticSearch or
+http://lucene.apache.org/solr/[Apache Solr], including full text search
+capabilities, free multivariable, geospatial and bitemporal search,
+relevance queries and sorting based on column value, relevance or
+distance. Each node indexes its own data, so high availability and
+scalability is guaranteed.
+
+The official Github repository
+http://www.github.com/stratio/cassandra-lucene-index[Cassandra Lucene
+Index] contains everything you need to build/run/configure the plugin.
diff --git a/doc/modules/cassandra/pages/tools/cassandra_stress.adoc b/doc/modules/cassandra/pages/tools/cassandra_stress.adoc
new file mode 100644
index 0000000..bcef193
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/cassandra_stress.adoc
@@ -0,0 +1,326 @@
+= Cassandra Stress
+
+The `cassandra-stress` tool is used to benchmark and load-test a Cassandra
+cluster. 
+`cassandra-stress` supports testing arbitrary CQL tables and queries, allowing users to benchmark their own data model.
+
+This documentation focuses on user mode to test personal schema.
+
+== Usage
+
+There are several operation types:
+
+* write-only, read-only, and mixed workloads of standard data
+* write-only and read-only workloads for counter columns
+* user configured workloads, running custom queries on custom schemas
+
+The syntax is `cassandra-stress <command> [options]`. 
+For more information on a given command or options, run `cassandra-stress help <command|option>`.
+
+Commands:::
+  read:;;
+    Multiple concurrent reads - the cluster must first be populated by a
+    write test
+  write:;;
+    Multiple concurrent writes against the cluster
+  mixed:;;
+    Interleaving of any basic commands, with configurable ratio and
+    distribution - the cluster must first be populated by a write test
+  counter_write:;;
+    Multiple concurrent updates of counters.
+  counter_read:;;
+    Multiple concurrent reads of counters. The cluster must first be
+    populated by a counterwrite test.
+  user:;;
+    Interleaving of user provided queries, with configurable ratio and
+    distribution.
+  help:;;
+    Print help for a command or option
+  print:;;
+    Inspect the output of a distribution definition
+  legacy:;;
+    Legacy support mode
+Primary Options:::
+  -pop:;;
+    Population distribution and intra-partition visit order
+  -insert:;;
+    Insert specific options relating to various methods for batching and
+    splitting partition updates
+  -col:;;
+    Column details such as size and count distribution, data generator,
+    names, comparator and if super columns should be used
+  -rate:;;
+    Thread count, rate limit or automatic mode (default is auto)
+  -mode:;;
+    Thrift or CQL with options
+  -errors:;;
+    How to handle errors when encountered during stress
+  -sample:;;
+    Specify the number of samples to collect for measuring latency
+  -schema:;;
+    Replication settings, compression, compaction, etc.
+  -node:;;
+    Nodes to connect to
+  -log:;;
+    Where to log progress to, and the interval at which to do it
+  -transport:;;
+    Custom transport factories
+  -port:;;
+    The port to connect to cassandra nodes on
+  -sendto:;;
+    Specify a stress server to send this command to
+  -graph:;;
+    Graph recorded metrics
+  -tokenrange:;;
+    Token range settings
+Suboptions:::
+  Every command and primary option has its own collection of suboptions.
+  These are too numerous to list here. For information on the suboptions
+  for each command or option, please use the help command,
+  `cassandra-stress help <command|option>`.
+
+== User mode
+
+User mode allows you to stress your own schemas, to save you time
+in the long run. Find out if your application can scale using stress test with your schema.
+
+=== Profile
+
+User mode defines a profile using YAML. 
+Multiple YAML files may be specified, in which case operations in the ops argument are referenced as
+specname.opname.
+
+An identifier for the profile:
+
+[source,yaml]
+----
+specname: staff_activities
+----
+
+The keyspace for the test:
+
+[source,yaml]
+----
+keyspace: staff
+----
+
+CQL for the keyspace. Optional if the keyspace already exists:
+
+[source,yaml]
+----
+keyspace_definition: |
+ CREATE KEYSPACE stresscql WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};
+----
+
+The table to be stressed:
+
+[source,yaml]
+----
+table: staff_activities
+----
+
+CQL for the table. Optional if the table already exists:
+
+[source,yaml]
+----
+table_definition: |
+  CREATE TABLE staff_activities (
+      name text,
+      when timeuuid,
+      what text,
+      PRIMARY KEY(name, when, what)
+  ) 
+----
+
+Optional meta-information on the generated columns in the above table.
+The min and max only apply to text and blob types. The distribution
+field represents the total unique population distribution of that column
+across rows:
+
+[source,yaml]
+----
+columnspec:
+  - name: name
+    size: uniform(5..10) # The names of the staff members are between 5-10 characters
+    population: uniform(1..10) # 10 possible staff members to pick from
+  - name: when
+    cluster: uniform(20..500) # Staff members do between 20 and 500 events
+  - name: what
+    size: normal(10..100,50)
+----
+
+Supported types are:
+
+An exponential distribution over the range [min..max]:
+
+[source,yaml]
+----
+EXP(min..max)
+----
+
+An extreme value (Weibull) distribution over the range [min..max]:
+
+[source,yaml]
+----
+EXTREME(min..max,shape)
+----
+
+A gaussian/normal distribution, where mean=(min+max)/2, and stdev is
+(mean-min)/stdvrng:
+
+[source,yaml]
+----
+GAUSSIAN(min..max,stdvrng)
+----
+
+A gaussian/normal distribution, with explicitly defined mean and stdev:
+
+[source,yaml]
+----
+GAUSSIAN(min..max,mean,stdev)
+----
+
+A uniform distribution over the range [min, max]:
+
+[source,yaml]
+----
+UNIFORM(min..max)
+----
+
+A fixed distribution, always returning the same value:
+
+[source,yaml]
+----
+FIXED(val)
+----
+
+If preceded by ~, the distribution is inverted
+
+Defaults for all columns are size: uniform(4..8), population:
+uniform(1..100B), cluster: fixed(1)
+
+Insert distributions:
+
+[source,yaml]
+----
+insert:
+  # How many partition to insert per batch
+  partitions: fixed(1)
+  # How many rows to update per partition
+  select: fixed(1)/500
+  # UNLOGGED or LOGGED batch for insert
+  batchtype: UNLOGGED
+----
+
+Currently all inserts are done inside batches.
+
+Read statements to use during the test:
+
+[source,yaml]
+----
+queries:
+   events:
+      cql: select *  from staff_activities where name = ?
+      fields: samerow
+   latest_event:
+      cql: select * from staff_activities where name = ?  LIMIT 1
+      fields: samerow
+----
+
+Running a user mode test:
+
+[source,yaml]
+----
+cassandra-stress user profile=./example.yaml duration=1m "ops(insert=1,latest_event=1,events=1)" truncate=once
+----
+
+This will create the schema then run tests for 1 minute with an equal
+number of inserts, latest_event queries and events queries. Additionally
+the table will be truncated once before the test.
+
+The full example can be found here:
+[source, yaml]
+----
+include::example$YAML/stress-example.yaml[]
+---- 
+
+Running a user mode test with multiple yaml files::::
+  cassandra-stress user profile=./example.yaml,./example2.yaml
+  duration=1m "ops(ex1.insert=1,ex1.latest_event=1,ex2.insert=2)"
+  truncate=once
+This will run operations as specified in both the example.yaml and
+example2.yaml files. example.yaml and example2.yaml can reference the
+same table, although care must be taken that the table definition is identical
+ (data generation specs can be different).
+
+=== Lightweight transaction support
+
+cassandra-stress supports lightweight transactions. 
+To use this feature, the command will first read current data from Cassandra, and then uses read values to
+fulfill lightweight transaction conditions.
+
+Lightweight transaction update query:
+
+[source,yaml]
+----
+queries:
+  regularupdate:
+      cql: update blogposts set author = ? where domain = ? and published_date = ?
+      fields: samerow
+  updatewithlwt:
+      cql: update blogposts set author = ? where domain = ? and published_date = ? IF body = ? AND url = ?
+      fields: samerow
+----
+
+The full example can be found here:
+[source, yaml]
+----
+include::example$YAML/stress-lwt-example.yaml[]
+----
+
+== Graphing
+
+Graphs can be generated for each run of stress.
+
+image::example-stress-graph.png[example cassandra-stress graph]
+
+To create a new graph:
+
+[source,yaml]
+----
+cassandra-stress user profile=./stress-example.yaml "ops(insert=1,latest_event=1,events=1)" -graph file=graph.html title="Awesome graph"
+----
+
+To add a new run to an existing graph point to an existing file and add
+a revision name:
+
+[source,yaml]
+----
+cassandra-stress user profile=./stress-example.yaml duration=1m "ops(insert=1,latest_event=1,events=1)" -graph file=graph.html title="Awesome graph" revision="Second run"
+----
+
+== FAQ
+
+*How do you use NetworkTopologyStrategy for the keyspace?*
+
+Use the schema option making sure to either escape the parenthesis or
+enclose in quotes:
+
+[source,yaml]
+----
+cassandra-stress write -schema "replication(strategy=NetworkTopologyStrategy,datacenter1=3)"
+----
+
+*How do you use SSL?*
+
+Use the transport option:
+
+[source,yaml]
+----
+cassandra-stress "write n=100k cl=ONE no-warmup" -transport "truststore=$HOME/jks/truststore.jks truststore-password=cassandra"
+----
+
+*Is Cassandra Stress a secured tool?*
+
+Cassandra stress is not a secured tool. Serialization and other aspects
+of the tool offer no security guarantees.
diff --git a/doc/modules/cassandra/pages/tools/cqlsh.adoc b/doc/modules/cassandra/pages/tools/cqlsh.adoc
new file mode 100644
index 0000000..162259a
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/cqlsh.adoc
@@ -0,0 +1,482 @@
+= cqlsh: the CQL shell
+
+`cqlsh` is a command-line interface for interacting with Cassandra using CQL (the Cassandra Query Language). 
+It is shipped with every Cassandra package, and can be found in the bin/ directory alongside the cassandra
+executable. 
+`cqlsh` is implemented with the Python native protocol driver, and connects to the single specified node.
+
+== Compatibility
+
+`cqlsh` is compatible with Python 2.7.
+
+In general, a given version of `cqlsh` is only guaranteed to work with the
+version of Cassandra that it was released with. 
+In some cases, `cqlsh` may work with older or newer versions of Cassandra, but this is not
+officially supported.
+
+== Optional Dependencies
+
+`cqlsh` ships with all essential dependencies. However, there are some
+optional dependencies that can be installed to improve the capabilities
+of `cqlsh`.
+
+=== pytz
+
+By default, `cqlsh` displays all timestamps with a UTC timezone. 
+To support display of timestamps with another timezone, install
+the http://pytz.sourceforge.net/[pytz] library. 
+See the `timezone` option in xref:cql/tools/cqlsh.adoc#cqlshrc[cqlshrc] for specifying a timezone to
+use.
+
+=== cython
+
+The performance of cqlsh's `COPY` operations can be improved by
+installing http://cython.org/[cython]. This will compile the python
+modules that are central to the performance of `COPY`.
+
+[[cqlshrc]]
+== cqlshrc
+
+The `cqlshrc` file holds configuration options for `cqlsh`. 
+By default, the file is locagted the user's home directory at `~/.cassandra/cqlsh`, but a
+custom location can be specified with the `--cqlshrc` option.
+
+Example config values and documentation can be found in the
+`conf/cqlshrc.sample` file of a tarball installation. 
+You can also view the latest version of the
+https://github.com/apache/cassandra/blob/trunk/conf/cqlshrc.sample[cqlshrc file online].
+
+== Command Line Options
+
+Usage:
+
+`cqlsh [options] [host [port]]`
+
+Options:
+
+`-C` `--color`::
+  Force color output
+`--no-color`::
+  Disable color output
+`--browser`::
+  Specify the browser to use for displaying cqlsh help. This can be one
+  of the https://docs.python.org/2/library/webbrowser.html[supported
+  browser names] (e.g. `firefox`) or a browser path followed by `%s`
+  (e.g. `/usr/bin/google-chrome-stable %s`).
+`--ssl`::
+  Use SSL when connecting to Cassandra
+`-u` `--user`::
+  Username to authenticate against Cassandra with
+`-p` `--password`::
+  Password to authenticate against Cassandra with, should be used in
+  conjunction with `--user`
+`-k` `--keyspace`::
+  Keyspace to authenticate to, should be used in conjunction with
+  `--user`
+`-f` `--file`::
+  Execute commands from the given file, then exit
+`--debug`::
+  Print additional debugging information
+`--encoding`::
+  Specify a non-default encoding for output (defaults to UTF-8)
+`--cqlshrc`::
+  Specify a non-default location for the `cqlshrc` file
+`-e` `--execute`::
+  Execute the given statement, then exit
+`--connect-timeout`::
+  Specify the connection timeout in seconds (defaults to 2s)
+`--python /path/to/python`::
+  Specify the full path to Python interpreter to override default on
+  systems with multiple interpreters installed
+`--request-timeout`::
+  Specify the request timeout in seconds (defaults to 10s)
+`-t` `--tty`::
+  Force tty mode (command prompt)
+
+== Special Commands
+
+In addition to supporting regular CQL statements, `cqlsh` also supports a
+number of special commands that are not part of CQL. These are detailed
+below.
+
+=== `CONSISTENCY`
+
+`Usage`: `CONSISTENCY <consistency level>`
+
+Sets the consistency level for operations to follow. Valid arguments
+include:
+
+* `ANY`
+* `ONE`
+* `TWO`
+* `THREE`
+* `QUORUM`
+* `ALL`
+* `LOCAL_QUORUM`
+* `LOCAL_ONE`
+* `SERIAL`
+* `LOCAL_SERIAL`
+
+=== `SERIAL CONSISTENCY`
+
+`Usage`: `SERIAL CONSISTENCY <consistency level>`
+
+Sets the serial consistency level for operations to follow. Valid
+arguments include:
+
+* `SERIAL`
+* `LOCAL_SERIAL`
+
+The serial consistency level is only used by conditional updates
+(`INSERT`, `UPDATE` and `DELETE` with an `IF` condition). For those, the
+serial consistency level defines the consistency level of the serial
+phase (or “paxos” phase) while the normal consistency level defines the
+consistency for the “learn” phase, i.e. what type of reads will be
+guaranteed to see the update right away. For example, if a conditional
+write has a consistency level of `QUORUM` (and is successful), then a
+`QUORUM` read is guaranteed to see that write. But if the regular
+consistency level of that write is `ANY`, then only a read with a
+consistency level of `SERIAL` is guaranteed to see it (even a read with
+consistency `ALL` is not guaranteed to be enough).
+
+=== `SHOW VERSION`
+
+Prints the `cqlsh`, Cassandra, CQL, and native protocol versions in use.
+Example:
+
+[source,none]
+----
+cqlsh> SHOW VERSION
+[cqlsh 5.0.1 | Cassandra 3.8 | CQL spec 3.4.2 | Native protocol v4]
+----
+
+=== `SHOW HOST`
+
+Prints the IP address and port of the Cassandra node that `cqlsh` is
+connected to in addition to the cluster name. Example:
+
+[source,none]
+----
+cqlsh> SHOW HOST
+Connected to Prod_Cluster at 192.0.0.1:9042.
+----
+
+=== `SHOW SESSION`
+
+Pretty prints a specific tracing session.
+
+`Usage`: `SHOW SESSION <session id>`
+
+Example usage:
+
+[source,none]
+----
+cqlsh> SHOW SESSION 95ac6470-327e-11e6-beca-dfb660d92ad8
+
+Tracing session: 95ac6470-327e-11e6-beca-dfb660d92ad8
+
+ activity                                                  | timestamp                  | source    | source_elapsed | client
+-----------------------------------------------------------+----------------------------+-----------+----------------+-----------
+                                        Execute CQL3 query | 2016-06-14 17:23:13.979000 | 127.0.0.1 |              0 | 127.0.0.1
+ Parsing SELECT * FROM system.local; [SharedPool-Worker-1] | 2016-06-14 17:23:13.982000 | 127.0.0.1 |           3843 | 127.0.0.1
+...
+----
+
+=== `SOURCE`
+
+Reads the contents of a file and executes each line as a CQL statement
+or special cqlsh command.
+
+`Usage`: `SOURCE <string filename>`
+
+Example usage:
+
+[source,none]
+----
+cqlsh> SOURCE '/home/calvinhobbs/commands.cql'
+----
+
+=== `CAPTURE`
+
+Begins capturing command output and appending it to a specified file.
+Output will not be shown at the console while it is captured.
+
+`Usage`:
+
+[source,none]
+----
+CAPTURE '<file>';
+CAPTURE OFF;
+CAPTURE;
+----
+
+That is, the path to the file to be appended to must be given inside a
+string literal. The path is interpreted relative to the current working
+directory. The tilde shorthand notation (`'~/mydir'`) is supported for
+referring to `$HOME`.
+
+Only query result output is captured. Errors and output from cqlsh-only
+commands will still be shown in the cqlsh session.
+
+To stop capturing output and show it in the cqlsh session again, use
+`CAPTURE OFF`.
+
+To inspect the current capture configuration, use `CAPTURE` with no
+arguments.
+
+=== `HELP`
+
+Gives information about cqlsh commands. To see available topics, enter
+`HELP` without any arguments. To see help on a topic, use
+`HELP <topic>`. Also see the `--browser` argument for controlling what
+browser is used to display help.
+
+=== `TRACING`
+
+Enables or disables tracing for queries. When tracing is enabled, once a
+query completes, a trace of the events during the query will be printed.
+
+`Usage`:
+
+[source,none]
+----
+TRACING ON
+TRACING OFF
+----
+
+=== `PAGING`
+
+Enables paging, disables paging, or sets the page size for read queries.
+When paging is enabled, only one page of data will be fetched at a time
+and a prompt will appear to fetch the next page. Generally, it's a good
+idea to leave paging enabled in an interactive session to avoid fetching
+and printing large amounts of data at once.
+
+`Usage`:
+
+[source,none]
+----
+PAGING ON
+PAGING OFF
+PAGING <page size in rows>
+----
+
+=== `EXPAND`
+
+Enables or disables vertical printing of rows. Enabling `EXPAND` is
+useful when many columns are fetched, or the contents of a single column
+are large.
+
+`Usage`:
+
+[source,none]
+----
+EXPAND ON
+EXPAND OFF
+----
+
+=== `LOGIN`
+
+Authenticate as a specified Cassandra user for the current session.
+
+`Usage`:
+
+[source,none]
+----
+LOGIN <username> [<password>]
+----
+
+=== `EXIT`
+
+Ends the current session and terminates the cqlsh process.
+
+`Usage`:
+
+[source,none]
+----
+EXIT
+QUIT
+----
+
+=== `CLEAR`
+
+Clears the console.
+
+`Usage`:
+
+[source,none]
+----
+CLEAR
+CLS
+----
+
+=== `DESCRIBE`
+
+Prints a description (typically a series of DDL statements) of a schema
+element or the cluster. This is useful for dumping all or portions of
+the schema.
+
+`Usage`:
+
+[source,none]
+----
+DESCRIBE CLUSTER
+DESCRIBE SCHEMA
+DESCRIBE KEYSPACES
+DESCRIBE KEYSPACE <keyspace name>
+DESCRIBE TABLES
+DESCRIBE TABLE <table name>
+DESCRIBE INDEX <index name>
+DESCRIBE MATERIALIZED VIEW <view name>
+DESCRIBE TYPES
+DESCRIBE TYPE <type name>
+DESCRIBE FUNCTIONS
+DESCRIBE FUNCTION <function name>
+DESCRIBE AGGREGATES
+DESCRIBE AGGREGATE <aggregate function name>
+----
+
+In any of the commands, `DESC` may be used in place of `DESCRIBE`.
+
+The `DESCRIBE CLUSTER` command prints the cluster name and partitioner:
+
+[source,none]
+----
+cqlsh> DESCRIBE CLUSTER
+
+Cluster: Test Cluster
+Partitioner: Murmur3Partitioner
+----
+
+The `DESCRIBE SCHEMA` command prints the DDL statements needed to
+recreate the entire schema. This is especially useful for dumping the
+schema in order to clone a cluster or restore from a backup.
+
+=== `COPY TO`
+
+Copies data from a table to a CSV file.
+
+`Usage`:
+
+[source,none]
+----
+COPY <table name> [(<column>, ...)] TO <file name> WITH <copy option> [AND <copy option> ...]
+----
+
+If no columns are specified, all columns from the table will be copied
+to the CSV file. A subset of columns to copy may be specified by adding
+a comma-separated list of column names surrounded by parenthesis after
+the table name.
+
+The `<file name>` should be a string literal (with single quotes)
+representing a path to the destination file. This can also the special
+value `STDOUT` (without single quotes) to print the CSV to stdout.
+
+See `shared-copy-options` for options that apply to both `COPY TO` and
+`COPY FROM`.
+
+==== Options for `COPY TO`
+
+`MAXREQUESTS`::
+  The maximum number token ranges to fetch simultaneously. Defaults to
+  6.
+`PAGESIZE`::
+  The number of rows to fetch in a single page. Defaults to 1000.
+`PAGETIMEOUT`::
+  By default the page timeout is 10 seconds per 1000 entries in the page
+  size or 10 seconds if pagesize is smaller.
+`BEGINTOKEN`, `ENDTOKEN`::
+  Token range to export. Defaults to exporting the full ring.
+`MAXOUTPUTSIZE`::
+  The maximum size of the output file measured in number of lines;
+  beyond this maximum the output file will be split into segments. -1
+  means unlimited, and is the default.
+`ENCODING`::
+  The encoding used for characters. Defaults to `utf8`.
+
+=== `COPY FROM`
+
+Copies data from a CSV file to table.
+
+`Usage`:
+
+[source,none]
+----
+COPY <table name> [(<column>, ...)] FROM <file name> WITH <copy option> [AND <copy option> ...]
+----
+
+If no columns are specified, all columns from the CSV file will be
+copied to the table. A subset of columns to copy may be specified by
+adding a comma-separated list of column names surrounded by parenthesis
+after the table name.
+
+The `<file name>` should be a string literal (with single quotes)
+representing a path to the source file. This can also the special value
+`STDIN` (without single quotes) to read the CSV data from stdin.
+
+See `shared-copy-options` for options that apply to both `COPY TO` and
+`COPY FROM`.
+
+==== Options for `COPY TO`
+
+`INGESTRATE`::
+  The maximum number of rows to process per second. Defaults to 100000.
+`MAXROWS`::
+  The maximum number of rows to import. -1 means unlimited, and is the
+  default.
+`SKIPROWS`::
+  A number of initial rows to skip. Defaults to 0.
+`SKIPCOLS`::
+  A comma-separated list of column names to ignore. By default, no
+  columns are skipped.
+`MAXPARSEERRORS`::
+  The maximum global number of parsing errors to ignore. -1 means
+  unlimited, and is the default.
+`MAXINSERTERRORS`::
+  The maximum global number of insert errors to ignore. -1 means
+  unlimited. The default is 1000.
+`ERRFILE` =::
+  A file to store all rows that could not be imported, by default this
+  is `import_<ks>_<table>.err` where `<ks>` is your keyspace and
+  `<table>` is your table name.
+`MAXBATCHSIZE`::
+  The max number of rows inserted in a single batch. Defaults to 20.
+`MINBATCHSIZE`::
+  The min number of rows inserted in a single batch. Defaults to 2.
+`CHUNKSIZE`::
+  The number of rows that are passed to child worker processes from the
+  main process at a time. Defaults to 1000.
+
+==== Shared COPY Options
+
+Options that are common to both `COPY TO` and `COPY FROM`.
+
+`NULLVAL`::
+  The string placeholder for null values. Defaults to `null`.
+`HEADER`::
+  For `COPY TO`, controls whether the first line in the CSV output file
+  will contain the column names. For COPY FROM, specifies whether the
+  first line in the CSV input file contains column names. Defaults to
+  `false`.
+`DECIMALSEP`::
+  The character that is used as the decimal point separator. Defaults to
+  `.`.
+`THOUSANDSSEP`::
+  The character that is used to separate thousands. Defaults to the
+  empty string.
+`BOOLSTYlE`::
+  The string literal format for boolean values. Defaults to
+  `True,False`.
+`NUMPROCESSES`::
+  The number of child worker processes to create for `COPY` tasks.
+  Defaults to a max of 4 for `COPY FROM` and 16 for `COPY TO`. However,
+  at most (num_cores - 1) processes will be created.
+`MAXATTEMPTS`::
+  The maximum number of failed attempts to fetch a range of data (when
+  using `COPY TO`) or insert a chunk of data (when using `COPY FROM`)
+  before giving up. Defaults to 5.
+`REPORTFREQUENCY`::
+  How often status updates are refreshed, in seconds. Defaults to 0.25.
+`RATEFILE`::
+  An optional file to output rate statistics to. By default, statistics
+  are not output to a file.
diff --git a/doc/modules/cassandra/pages/tools/index.adoc b/doc/modules/cassandra/pages/tools/index.adoc
new file mode 100644
index 0000000..a25af55
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/index.adoc
@@ -0,0 +1,9 @@
+= Cassandra Tools
+
+This section describes the command line tools provided with Apache
+Cassandra.
+
+* xref:tools/cqlsh.adoc[CQL shell]
+* xref:tools/nodetool/nodetool.adoc[nodetool]
+* xref:tools/sstable/index.adoc[SSTable tools] 
+* xref:tools/cassandra_stress.adoc[cassandra-stress tool]
diff --git a/doc/modules/cassandra/pages/tools/sstable/index.adoc b/doc/modules/cassandra/pages/tools/sstable/index.adoc
new file mode 100644
index 0000000..cb787ec
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/index.adoc
@@ -0,0 +1,20 @@
+= SSTable Tools
+
+This section describes the functionality of the various sstable tools.
+
+Cassandra must be stopped before these tools are executed, or unexpected
+results will occur. Note: the scripts do not verify that Cassandra is
+stopped.
+
+* xref:tools/sstable/sstabledump.adoc[sstabledump]
+* xref:tools/sstable/sstableexpiredblockers.adoc[sstableexpiredblockers]
+* xref:tools/sstable/sstablelevelreset.adoc[sstablelevelreset]
+* xref:tools/sstable/sstableloader.adoc[sstableloader]
+* xref:tools/sstable/sstablemetadata.adoc[sstablemetadata]
+* xref:tools/sstable/sstableofflinerelevel.adoc[sstableofflinerelevel]
+* xref:tools/sstable/sstablerepairedset.adoc[sstablerepairdset]
+* xref:tools/sstable/sstablescrub.adoc[sstablescrub]
+* xref:tools/sstable/sstablesplit.adoc[sstablesplit]
+* xref:tools/sstable/sstableupgrade.adoc[sstableupgrade]
+* xref:tools/sstable/sstableutil.adoc[sstableutil]
+* xref:tools/sstable/sstableverify.adoc[sstableverify]
diff --git a/doc/modules/cassandra/pages/tools/sstable/sstabledump.adoc b/doc/modules/cassandra/pages/tools/sstable/sstabledump.adoc
new file mode 100644
index 0000000..90f66b8
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/sstabledump.adoc
@@ -0,0 +1,286 @@
+= sstabledump
+
+Dump contents of a given SSTable to standard output in JSON format.
+
+You must supply exactly one sstable.
+
+Cassandra must be stopped before this tool is executed, or unexpected
+results will occur. Note: the script does not verify that Cassandra is
+stopped.
+
+== Usage
+
+sstabledump <options> <sstable file path>
+
+[cols=",",]
+|===
+|-d |CQL row per line internal representation
+|-e |Enumerate partition keys only
+|-k <arg> |Partition key
+|-x <arg> |Excluded partition key(s)
+|-t |Print raw timestamps instead of iso8601 date strings
+|-l |Output each row as a separate JSON object
+|===
+
+If necessary, use sstableutil first to find out the sstables used by a
+table.
+
+== Dump entire table
+
+Dump the entire table without any options.
+
+Example:
+
+....
+sstabledump /var/lib/cassandra/data/keyspace/eventlog-65c429e08c5a11e8939edf4f403979ef/mc-1-big-Data.db > eventlog_dump_2018Jul26
+
+cat eventlog_dump_2018Jul26
+[
+  {
+    "partition" : {
+      "key" : [ "3578d7de-c60d-4599-aefb-3f22a07b2bc6" ],
+      "position" : 0
+    },
+    "rows" : [
+      {
+        "type" : "row",
+        "position" : 61,
+        "liveness_info" : { "tstamp" : "2018-07-20T20:23:08.378711Z" },
+        "cells" : [
+          { "name" : "event", "value" : "party" },
+          { "name" : "insertedtimestamp", "value" : "2018-07-20 20:23:08.384Z" },
+          { "name" : "source", "value" : "asdf" }
+        ]
+      }
+    ]
+  },
+  {
+    "partition" : {
+      "key" : [ "d18250c0-84fc-4d40-b957-4248dc9d790e" ],
+      "position" : 62
+    },
+    "rows" : [
+      {
+        "type" : "row",
+        "position" : 123,
+        "liveness_info" : { "tstamp" : "2018-07-20T20:23:07.783522Z" },
+        "cells" : [
+          { "name" : "event", "value" : "party" },
+          { "name" : "insertedtimestamp", "value" : "2018-07-20 20:23:07.789Z" },
+          { "name" : "source", "value" : "asdf" }
+        ]
+      }
+    ]
+  },
+  {
+    "partition" : {
+      "key" : [ "cf188983-d85b-48d6-9365-25005289beb2" ],
+      "position" : 124
+    },
+    "rows" : [
+      {
+        "type" : "row",
+        "position" : 182,
+        "liveness_info" : { "tstamp" : "2018-07-20T20:22:27.028809Z" },
+        "cells" : [
+          { "name" : "event", "value" : "party" },
+          { "name" : "insertedtimestamp", "value" : "2018-07-20 20:22:27.055Z" },
+          { "name" : "source", "value" : "asdf" }
+        ]
+      }
+    ]
+  }
+]
+....
+
+== Dump table in a more manageable format
+
+Use the -l option to dump each row as a separate JSON object. This will
+make the output easier to manipulate for large data sets. ref:
+https://issues.apache.org/jira/browse/CASSANDRA-13848
+
+Example:
+
+....
+sstabledump /var/lib/cassandra/data/keyspace/eventlog-65c429e08c5a11e8939edf4f403979ef/mc-1-big-Data.db -l > eventlog_dump_2018Jul26_justlines
+
+cat eventlog_dump_2018Jul26_justlines
+[
+  {
+    "partition" : {
+      "key" : [ "3578d7de-c60d-4599-aefb-3f22a07b2bc6" ],
+      "position" : 0
+    },
+    "rows" : [
+      {
+        "type" : "row",
+        "position" : 61,
+        "liveness_info" : { "tstamp" : "2018-07-20T20:23:08.378711Z" },
+        "cells" : [
+          { "name" : "event", "value" : "party" },
+          { "name" : "insertedtimestamp", "value" : "2018-07-20 20:23:08.384Z" },
+          { "name" : "source", "value" : "asdf" }
+        ]
+      }
+    ]
+  },
+  {
+    "partition" : {
+      "key" : [ "d18250c0-84fc-4d40-b957-4248dc9d790e" ],
+      "position" : 62
+    },
+    "rows" : [
+      {
+        "type" : "row",
+        "position" : 123,
+        "liveness_info" : { "tstamp" : "2018-07-20T20:23:07.783522Z" },
+        "cells" : [
+          { "name" : "event", "value" : "party" },
+          { "name" : "insertedtimestamp", "value" : "2018-07-20 20:23:07.789Z" },
+          { "name" : "source", "value" : "asdf" }
+        ]
+      }
+    ]
+  },
+  {
+    "partition" : {
+      "key" : [ "cf188983-d85b-48d6-9365-25005289beb2" ],
+      "position" : 124
+    },
+    "rows" : [
+      {
+        "type" : "row",
+        "position" : 182,
+        "liveness_info" : { "tstamp" : "2018-07-20T20:22:27.028809Z" },
+        "cells" : [
+          { "name" : "event", "value" : "party" },
+          { "name" : "insertedtimestamp", "value" : "2018-07-20 20:22:27.055Z" },
+          { "name" : "source", "value" : "asdf" }
+        ]
+      }
+    ]
+  }
+....
+
+== Dump only keys
+
+Dump only the keys by using the -e option.
+
+Example:
+
+....
+sstabledump /var/lib/cassandra/data/keyspace/eventlog-65c429e08c5a11e8939edf4f403979ef/mc-1-big-Data.db -e > eventlog_dump_2018Jul26_justkeys
+
+cat eventlog_dump_2018Jul26b
+[ [ "3578d7de-c60d-4599-aefb-3f22a07b2bc6" ], [ "d18250c0-84fc-4d40-b957-4248dc9d790e" ], [ "cf188983-d85b-48d6-9365-25005289beb2" ]
+....
+
+== Dump row for a single key
+
+Dump a single key using the -k option.
+
+Example:
+
+....
+sstabledump /var/lib/cassandra/data/keyspace/eventlog-65c429e08c5a11e8939edf4f403979ef/mc-1-big-Data.db -k 3578d7de-c60d-4599-aefb-3f22a07b2bc6 > eventlog_dump_2018Jul26_singlekey
+
+cat eventlog_dump_2018Jul26_singlekey
+[
+  {
+    "partition" : {
+      "key" : [ "3578d7de-c60d-4599-aefb-3f22a07b2bc6" ],
+      "position" : 0
+    },
+    "rows" : [
+      {
+        "type" : "row",
+        "position" : 61,
+        "liveness_info" : { "tstamp" : "2018-07-20T20:23:08.378711Z" },
+        "cells" : [
+          { "name" : "event", "value" : "party" },
+          { "name" : "insertedtimestamp", "value" : "2018-07-20 20:23:08.384Z" },
+          { "name" : "source", "value" : "asdf" }
+        ]
+      }
+    ]
+  }
+....
+
+== Exclude a key or keys in dump of rows
+
+Dump a table except for the rows excluded with the -x option. Multiple
+keys can be used.
+
+Example:
+
+....
+sstabledump /var/lib/cassandra/data/keyspace/eventlog-65c429e08c5a11e8939edf4f403979ef/mc-1-big-Data.db -x 3578d7de-c60d-4599-aefb-3f22a07b2bc6 d18250c0-84fc-4d40-b957-4248dc9d790e  > eventlog_dump_2018Jul26_excludekeys
+
+cat eventlog_dump_2018Jul26_excludekeys
+[
+  {
+    "partition" : {
+      "key" : [ "cf188983-d85b-48d6-9365-25005289beb2" ],
+      "position" : 0
+    },
+    "rows" : [
+      {
+        "type" : "row",
+        "position" : 182,
+        "liveness_info" : { "tstamp" : "2018-07-20T20:22:27.028809Z" },
+        "cells" : [
+          { "name" : "event", "value" : "party" },
+          { "name" : "insertedtimestamp", "value" : "2018-07-20 20:22:27.055Z" },
+          { "name" : "source", "value" : "asdf" }
+        ]
+      }
+    ]
+  }
+....
+
+== Display raw timestamps
+
+By default, dates are displayed in iso8601 date format. Using the -t
+option will dump the data with the raw timestamp.
+
+Example:
+
+....
+sstabledump /var/lib/cassandra/data/keyspace/eventlog-65c429e08c5a11e8939edf4f403979ef/mc-1-big-Data.db -t -k cf188983-d85b-48d6-9365-25005289beb2 > eventlog_dump_2018Jul26_times
+
+cat eventlog_dump_2018Jul26_times
+[
+  {
+    "partition" : {
+      "key" : [ "cf188983-d85b-48d6-9365-25005289beb2" ],
+      "position" : 124
+    },
+    "rows" : [
+      {
+        "type" : "row",
+        "position" : 182,
+        "liveness_info" : { "tstamp" : "1532118147028809" },
+        "cells" : [
+          { "name" : "event", "value" : "party" },
+          { "name" : "insertedtimestamp", "value" : "2018-07-20 20:22:27.055Z" },
+          { "name" : "source", "value" : "asdf" }
+        ]
+      }
+    ]
+  }
+....
+
+== Display internal structure in output
+
+Dump the table in a format that reflects the internal structure.
+
+Example:
+
+....
+sstabledump /var/lib/cassandra/data/keyspace/eventlog-65c429e08c5a11e8939edf4f403979ef/mc-1-big-Data.db -d > eventlog_dump_2018Jul26_d
+
+cat eventlog_dump_2018Jul26_d
+[3578d7de-c60d-4599-aefb-3f22a07b2bc6]@0 Row[info=[ts=1532118188378711] ]:  | [event=party ts=1532118188378711], [insertedtimestamp=2018-07-20 20:23Z ts=1532118188378711], [source=asdf ts=1532118188378711]
+[d18250c0-84fc-4d40-b957-4248dc9d790e]@62 Row[info=[ts=1532118187783522] ]:  | [event=party ts=1532118187783522], [insertedtimestamp=2018-07-20 20:23Z ts=1532118187783522], [source=asdf ts=1532118187783522]
+[cf188983-d85b-48d6-9365-25005289beb2]@124 Row[info=[ts=1532118147028809] ]:  | [event=party ts=1532118147028809], [insertedtimestamp=2018-07-20 20:22Z ts=1532118147028809], [source=asdf ts=1532118147028809]
+....
diff --git a/doc/modules/cassandra/pages/tools/sstable/sstableexpiredblockers.adoc b/doc/modules/cassandra/pages/tools/sstable/sstableexpiredblockers.adoc
new file mode 100644
index 0000000..2090ad5
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/sstableexpiredblockers.adoc
@@ -0,0 +1,42 @@
+= sstableexpiredblockers
+
+During compaction, entire sstables can be dropped if they contain only
+expired tombstones, and if it is guaranteed that the data is not newer
+than the data in other sstables. An expired sstable can be blocked from
+getting dropped if its newest timestamp is newer than the oldest data in
+another sstable.
+
+This tool is used to list all sstables that are blocking other sstables
+from getting dropped (by having older data than the newest tombstone in
+an expired sstable) so a user can figure out why certain sstables are
+still on disk.
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-10015
+
+Cassandra must be stopped before this tool is executed, or unexpected
+results will occur. Note: the script does not verify that Cassandra is
+stopped.
+
+== Usage
+
+sstableexpiredblockers <keyspace> <table>
+
+== Output blocked sstables
+
+If the sstables exist for the table, but no tables have older data than
+the newest tombstone in an expired sstable, the script will return
+nothing.
+
+Otherwise, the script will return [.title-ref]#<sstable> blocks <#>
+expired sstables from getting dropped# followed by a list of the blocked
+sstables.
+
+Example:
+
+....
+sstableexpiredblockers keyspace1 standard1
+
+[BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-0665ae80b2d711e886c66d2c86545d91/mc-2-big-Data.db') (minTS = 5, maxTS = 5, maxLDT = 2147483647)],  blocks 1 expired sstables from getting dropped: [BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-0665ae80b2d711e886c66d2c86545d91/mc-3-big-Data.db') (minTS = 1536349775157606, maxTS = 1536349780311159, maxLDT = 1536349780)],
+
+[BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-0665ae80b2d711e886c66d2c86545d91/mc-1-big-Data.db') (minTS = 1, maxTS = 10, maxLDT = 2147483647)],  blocks 1 expired sstables from getting dropped: [BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-0665ae80b2d711e886c66d2c86545d91/mc-3-big-Data.db') (minTS = 1536349775157606, maxTS = 1536349780311159, maxLDT = 1536349780)],
+....
diff --git a/doc/modules/cassandra/pages/tools/sstable/sstablelevelreset.adoc b/doc/modules/cassandra/pages/tools/sstable/sstablelevelreset.adoc
new file mode 100644
index 0000000..65dc02e
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/sstablelevelreset.adoc
@@ -0,0 +1,69 @@
+= sstablelevelreset
+
+If LeveledCompactionStrategy is set, this script can be used to reset
+level to 0 on a given set of sstables. This is useful if you want to,
+for example, change the minimum sstable size, and therefore restart the
+compaction process using this new configuration.
+
+See
+http://cassandra.apache.org/doc/latest/operating/compaction.html#leveled-compaction-strategy
+for information on how levels are used in this compaction strategy.
+
+Cassandra must be stopped before this tool is executed, or unexpected
+results will occur. Note: the script does not verify that Cassandra is
+stopped.
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-5271
+
+== Usage
+
+sstablelevelreset --really-reset <keyspace> <table>
+
+The really-reset flag is required, to ensure this intrusive command is
+not run accidentally.
+
+== Table not found
+
+If the keyspace and/or table is not in the schema (e.g., if you
+misspelled the table name), the script will return an error.
+
+Example:
+
+....
+ColumnFamily not found: keyspace/evenlog.
+....
+
+== Table has no sstables
+
+Example:
+
+....
+Found no sstables, did you give the correct keyspace/table?
+....
+
+== Table already at level 0
+
+The script will not set the level if it is already set to 0.
+
+Example:
+
+....
+Skipped /var/lib/cassandra/data/keyspace/eventlog-65c429e08c5a11e8939edf4f403979ef/mc-1-big-Data.db since it is already on level 0
+....
+
+== Table levels reduced to 0
+
+If the level is not already 0, then this will reset it to 0.
+
+Example:
+
+....
+sstablemetadata /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-8-big-Data.db | grep -i level
+SSTable Level: 1
+
+sstablelevelreset --really-reset keyspace eventlog
+Changing level from 1 to 0 on /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-8-big-Data.db
+
+sstablemetadata /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-8-big-Data.db | grep -i level
+SSTable Level: 0
+....
diff --git a/doc/modules/cassandra/pages/tools/sstable/sstableloader.adoc b/doc/modules/cassandra/pages/tools/sstable/sstableloader.adoc
new file mode 100644
index 0000000..4234a0b
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/sstableloader.adoc
@@ -0,0 +1,316 @@
+= sstableloader
+
+Bulk-load the sstables found in the directory <dir_path> to the
+configured cluster. The parent directories of <dir_path> are used as the
+target keyspace/table name. For example, to load an sstable named
+ma-1-big-Data.db into keyspace1/standard1, you will need to have the
+files ma-1-big-Data.db and ma-1-big-Index.db in a directory
+/path/to/keyspace1/standard1/. The tool will create new sstables, and
+does not clean up your copied files.
+
+Several of the options listed below don't work quite as intended, and in
+those cases, workarounds are mentioned for specific use cases.
+
+To avoid having the sstable files to be loaded compacted while reading
+them, place the files in an alternate keyspace/table path than the data
+directory.
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-1278
+
+Cassandra must be stopped before this tool is executed, or unexpected
+results will occur. Note: the script does not verify that Cassandra is
+stopped.
+
+== Usage
+
+sstableloader <options> <dir_path>
+
+[cols=",",]
+|===
+|-d, --nodes <initial hosts> |Required. Try to connect to these hosts
+(comma-separated) initially for ring information
+
+|-u, --username <username> |username for Cassandra authentication
+
+|-pw, --password <password> |password for Cassandra authentication
+
+|-p, --port <native transport port> |port used for native connection
+(default 9042)
+
+|-sp, --storage-port <storage port> |port used for internode
+communication (default 7000)
+
+|-ssp, --ssl-storage-port <ssl storage port> |port used for TLS
+internode communication (default 7001)
+
+|--no-progress |don't display progress
+
+|-t, --throttle <throttle> |throttle speed in Mbits (default unlimited)
+
+|-idct, --inter-dc-throttle <inter-dc-throttle> |inter-datacenter
+throttle speed in Mbits (default unlimited)
+
+|-cph, --connections-per-host <connectionsPerHost> |number of concurrent
+connections-per-host
+
+|-i, --ignore <NODES> |don't stream to this (comma separated) list of
+nodes
+
+|-alg, --ssl-alg <ALGORITHM> |Client SSL: algorithm (default: SunX509)
+
+|-ciphers, --ssl-ciphers <CIPHER-SUITES> |Client SSL: comma-separated
+list of encryption suites to use
+
+|-ks, --keystore <KEYSTORE> |Client SSL: full path to keystore
+
+|-kspw, --keystore-password <KEYSTORE-PASSWORD> |Client SSL: password of
+the keystore
+
+|-st, --store-type <STORE-TYPE> |Client SSL: type of store
+
+|-ts, --truststore <TRUSTSTORE> |Client SSL: full path to truststore
+
+|-tspw, --truststore-password <TRUSTSTORE-PASSWORD> |Client SSL:
+password of the truststore
+
+|-prtcl, --ssl-protocol <PROTOCOL> |Client SSL: connections protocol to
+use (default: TLS)
+
+|-ap, --auth-provider <auth provider> |custom AuthProvider class name
+for cassandra authentication
+
+|-f, --conf-path <path to config file> |cassandra.yaml file path for
+streaming throughput and client/server SSL
+
+|-v, --verbose |verbose output
+
+|-h, --help |display this help message
+|===
+
+You can provide a cassandra.yaml file with the -f command line option to
+set up streaming throughput, and client and server encryption options.
+Only stream_throughput_outbound_megabits_per_sec,
+server_encryption_options, and client_encryption_options are read from
+yaml. You can override options read from cassandra.yaml with
+corresponding command line options.
+
+== Load sstables from a Snapshot
+
+Copy the snapshot sstables into an accessible directory and use
+sstableloader to restore them.
+
+Example:
+
+....
+cp snapshots/1535397029191/* /path/to/keyspace1/standard1/
+
+sstableloader --nodes 172.17.0.2 /var/lib/cassandra/loadme/keyspace1/standard1-f8a4fa30aa2a11e8af27091830ac5256/
+Established connection to initial hosts
+Opening sstables and calculating sections to stream
+Streaming relevant part of /var/lib/cassandra/loadme/keyspace1/standard1-f8a4fa30aa2a11e8af27091830ac5256/ma-3-big-Data.db to [/172.17.0.2]
+progress: [/172.17.0.2]0:1/1 100% total: 100% 0  MB/s(avg: 1 MB/s)
+Summary statistics:
+   Connections per host:         : 1
+   Total files transferred:      : 1
+   Total bytes transferred:      : 4700000
+   Total duration (ms):          : 4390
+   Average transfer rate (MB/s): : 1
+   Peak transfer rate (MB/s):    : 1
+....
+
+The -d or --nodes option is required, or the script will not run.
+
+Example:
+
+....
+sstableloader /var/lib/cassandra/loadme/keyspace1/standard1-f8a4fa30aa2a11e8af27091830ac5256/
+Initial hosts must be specified (-d)
+....
+
+== Use a Config File for SSL Clusters
+
+If SSL encryption is enabled in the cluster, use the --conf-path option
+with sstableloader to point the tool to the cassandra.yaml with the
+relevant server_encryption_options (e.g., truststore location,
+algorithm). This will work better than passing individual ssl options
+shown above to sstableloader on the command line.
+
+Example:
+
+....
+sstableloader --nodes 172.17.0.2 --conf-path /etc/cassandra/cassandra.yaml /var/lib/cassandra/loadme/keyspace1/standard1-0974e5a0aa5811e8a0a06d2c86545d91/snapshots/
+Established connection to initial hosts
+Opening sstables and calculating sections to stream
+Streaming relevant part of /var/lib/cassandra/loadme/keyspace1/standard1-0974e5a0aa5811e8a0a06d2c86545d91/mc-1-big-Data.db  to [/172.17.0.2]
+progress: [/172.17.0.2]0:0/1 1  % total: 1% 9.165KiB/s (avg: 9.165KiB/s)
+progress: [/172.17.0.2]0:0/1 2  % total: 2% 5.147MiB/s (avg: 18.299KiB/s)
+progress: [/172.17.0.2]0:0/1 4  % total: 4% 9.751MiB/s (avg: 27.423KiB/s)
+progress: [/172.17.0.2]0:0/1 5  % total: 5% 8.203MiB/s (avg: 36.524KiB/s)
+...
+progress: [/172.17.0.2]0:1/1 100% total: 100% 0.000KiB/s (avg: 480.513KiB/s)
+
+Summary statistics:
+   Connections per host    : 1
+   Total files transferred : 1
+   Total bytes transferred : 4.387MiB
+   Total duration          : 9356 ms
+   Average transfer rate   : 480.105KiB/s
+   Peak transfer rate      : 586.410KiB/s
+....
+
+== Hide Progress Output
+
+To hide the output of progress and the summary statistics (e.g., if you
+wanted to use this tool in a script), use the --no-progress option.
+
+Example:
+
+....
+sstableloader --nodes 172.17.0.2 --no-progress /var/lib/cassandra/loadme/keyspace1/standard1-f8a4fa30aa2a11e8af27091830ac5256/
+Established connection to initial hosts
+Opening sstables and calculating sections to stream
+Streaming relevant part of /var/lib/cassandra/loadme/keyspace1/standard1-f8a4fa30aa2a11e8af27091830ac5256/ma-4-big-Data.db to [/172.17.0.2]
+....
+
+== Get More Detail
+
+Using the --verbose option will provide much more progress output.
+
+Example:
+
+....
+sstableloader --nodes 172.17.0.2 --verbose /var/lib/cassandra/loadme/keyspace1/standard1-0974e5a0aa5811e8a0a06d2c86545d91/
+Established connection to initial hosts
+Opening sstables and calculating sections to stream
+Streaming relevant part of /var/lib/cassandra/loadme/keyspace1/standard1-0974e5a0aa5811e8a0a06d2c86545d91/mc-1-big-Data.db  to [/172.17.0.2]
+progress: [/172.17.0.2]0:0/1 1  % total: 1% 12.056KiB/s (avg: 12.056KiB/s)
+progress: [/172.17.0.2]0:0/1 2  % total: 2% 9.092MiB/s (avg: 24.081KiB/s)
+progress: [/172.17.0.2]0:0/1 4  % total: 4% 18.832MiB/s (avg: 36.099KiB/s)
+progress: [/172.17.0.2]0:0/1 5  % total: 5% 2.253MiB/s (avg: 47.882KiB/s)
+progress: [/172.17.0.2]0:0/1 7  % total: 7% 6.388MiB/s (avg: 59.743KiB/s)
+progress: [/172.17.0.2]0:0/1 8  % total: 8% 14.606MiB/s (avg: 71.635KiB/s)
+progress: [/172.17.0.2]0:0/1 9  % total: 9% 8.880MiB/s (avg: 83.465KiB/s)
+progress: [/172.17.0.2]0:0/1 11 % total: 11% 5.217MiB/s (avg: 95.176KiB/s)
+progress: [/172.17.0.2]0:0/1 12 % total: 12% 12.563MiB/s (avg: 106.975KiB/s)
+progress: [/172.17.0.2]0:0/1 14 % total: 14% 2.550MiB/s (avg: 118.322KiB/s)
+progress: [/172.17.0.2]0:0/1 15 % total: 15% 16.638MiB/s (avg: 130.063KiB/s)
+progress: [/172.17.0.2]0:0/1 17 % total: 17% 17.270MiB/s (avg: 141.793KiB/s)
+progress: [/172.17.0.2]0:0/1 18 % total: 18% 11.280MiB/s (avg: 153.452KiB/s)
+progress: [/172.17.0.2]0:0/1 19 % total: 19% 2.903MiB/s (avg: 164.603KiB/s)
+progress: [/172.17.0.2]0:0/1 21 % total: 21% 6.744MiB/s (avg: 176.061KiB/s)
+progress: [/172.17.0.2]0:0/1 22 % total: 22% 6.011MiB/s (avg: 187.440KiB/s)
+progress: [/172.17.0.2]0:0/1 24 % total: 24% 9.690MiB/s (avg: 198.920KiB/s)
+progress: [/172.17.0.2]0:0/1 25 % total: 25% 11.481MiB/s (avg: 210.412KiB/s)
+progress: [/172.17.0.2]0:0/1 27 % total: 27% 9.957MiB/s (avg: 221.848KiB/s)
+progress: [/172.17.0.2]0:0/1 28 % total: 28% 10.270MiB/s (avg: 233.265KiB/s)
+progress: [/172.17.0.2]0:0/1 29 % total: 29% 7.812MiB/s (avg: 244.571KiB/s)
+progress: [/172.17.0.2]0:0/1 31 % total: 31% 14.843MiB/s (avg: 256.021KiB/s)
+progress: [/172.17.0.2]0:0/1 32 % total: 32% 11.457MiB/s (avg: 267.394KiB/s)
+progress: [/172.17.0.2]0:0/1 34 % total: 34% 6.550MiB/s (avg: 278.536KiB/s)
+progress: [/172.17.0.2]0:0/1 35 % total: 35% 9.115MiB/s (avg: 289.782KiB/s)
+progress: [/172.17.0.2]0:0/1 37 % total: 37% 11.054MiB/s (avg: 301.064KiB/s)
+progress: [/172.17.0.2]0:0/1 38 % total: 38% 10.449MiB/s (avg: 312.307KiB/s)
+progress: [/172.17.0.2]0:0/1 39 % total: 39% 1.646MiB/s (avg: 321.665KiB/s)
+progress: [/172.17.0.2]0:0/1 41 % total: 41% 13.300MiB/s (avg: 332.872KiB/s)
+progress: [/172.17.0.2]0:0/1 42 % total: 42% 14.370MiB/s (avg: 344.082KiB/s)
+progress: [/172.17.0.2]0:0/1 44 % total: 44% 16.734MiB/s (avg: 355.314KiB/s)
+progress: [/172.17.0.2]0:0/1 45 % total: 45% 22.245MiB/s (avg: 366.592KiB/s)
+progress: [/172.17.0.2]0:0/1 47 % total: 47% 25.561MiB/s (avg: 377.882KiB/s)
+progress: [/172.17.0.2]0:0/1 48 % total: 48% 24.543MiB/s (avg: 389.155KiB/s)
+progress: [/172.17.0.2]0:0/1 49 % total: 49% 4.894MiB/s (avg: 399.688KiB/s)
+progress: [/172.17.0.2]0:0/1 51 % total: 51% 8.331MiB/s (avg: 410.559KiB/s)
+progress: [/172.17.0.2]0:0/1 52 % total: 52% 5.771MiB/s (avg: 421.150KiB/s)
+progress: [/172.17.0.2]0:0/1 54 % total: 54% 8.738MiB/s (avg: 431.983KiB/s)
+progress: [/172.17.0.2]0:0/1 55 % total: 55% 3.406MiB/s (avg: 441.911KiB/s)
+progress: [/172.17.0.2]0:0/1 56 % total: 56% 9.791MiB/s (avg: 452.730KiB/s)
+progress: [/172.17.0.2]0:0/1 58 % total: 58% 3.401MiB/s (avg: 462.545KiB/s)
+progress: [/172.17.0.2]0:0/1 59 % total: 59% 5.280MiB/s (avg: 472.840KiB/s)
+progress: [/172.17.0.2]0:0/1 61 % total: 61% 12.232MiB/s (avg: 483.663KiB/s)
+progress: [/172.17.0.2]0:0/1 62 % total: 62% 9.258MiB/s (avg: 494.325KiB/s)
+progress: [/172.17.0.2]0:0/1 64 % total: 64% 2.877MiB/s (avg: 503.640KiB/s)
+progress: [/172.17.0.2]0:0/1 65 % total: 65% 7.461MiB/s (avg: 514.078KiB/s)
+progress: [/172.17.0.2]0:0/1 66 % total: 66% 24.247MiB/s (avg: 525.018KiB/s)
+progress: [/172.17.0.2]0:0/1 68 % total: 68% 9.348MiB/s (avg: 535.563KiB/s)
+progress: [/172.17.0.2]0:0/1 69 % total: 69% 5.130MiB/s (avg: 545.563KiB/s)
+progress: [/172.17.0.2]0:0/1 71 % total: 71% 19.861MiB/s (avg: 556.392KiB/s)
+progress: [/172.17.0.2]0:0/1 72 % total: 72% 15.501MiB/s (avg: 567.122KiB/s)
+progress: [/172.17.0.2]0:0/1 74 % total: 74% 5.031MiB/s (avg: 576.996KiB/s)
+progress: [/172.17.0.2]0:0/1 75 % total: 75% 22.771MiB/s (avg: 587.813KiB/s)
+progress: [/172.17.0.2]0:0/1 76 % total: 76% 22.780MiB/s (avg: 598.619KiB/s)
+progress: [/172.17.0.2]0:0/1 78 % total: 78% 20.684MiB/s (avg: 609.386KiB/s)
+progress: [/172.17.0.2]0:0/1 79 % total: 79% 22.920MiB/s (avg: 620.173KiB/s)
+progress: [/172.17.0.2]0:0/1 81 % total: 81% 7.458MiB/s (avg: 630.333KiB/s)
+progress: [/172.17.0.2]0:0/1 82 % total: 82% 22.993MiB/s (avg: 641.090KiB/s)
+progress: [/172.17.0.2]0:0/1 84 % total: 84% 21.392MiB/s (avg: 651.814KiB/s)
+progress: [/172.17.0.2]0:0/1 85 % total: 85% 7.732MiB/s (avg: 661.938KiB/s)
+progress: [/172.17.0.2]0:0/1 86 % total: 86% 3.476MiB/s (avg: 670.892KiB/s)
+progress: [/172.17.0.2]0:0/1 88 % total: 88% 19.889MiB/s (avg: 681.521KiB/s)
+progress: [/172.17.0.2]0:0/1 89 % total: 89% 21.077MiB/s (avg: 692.162KiB/s)
+progress: [/172.17.0.2]0:0/1 91 % total: 91% 24.062MiB/s (avg: 702.835KiB/s)
+progress: [/172.17.0.2]0:0/1 92 % total: 92% 19.798MiB/s (avg: 713.431KiB/s)
+progress: [/172.17.0.2]0:0/1 94 % total: 94% 17.591MiB/s (avg: 723.965KiB/s)
+progress: [/172.17.0.2]0:0/1 95 % total: 95% 13.725MiB/s (avg: 734.361KiB/s)
+progress: [/172.17.0.2]0:0/1 96 % total: 96% 16.737MiB/s (avg: 744.846KiB/s)
+progress: [/172.17.0.2]0:0/1 98 % total: 98% 22.701MiB/s (avg: 755.443KiB/s)
+progress: [/172.17.0.2]0:0/1 99 % total: 99% 18.718MiB/s (avg: 765.954KiB/s)
+progress: [/172.17.0.2]0:1/1 100% total: 100% 6.613MiB/s (avg: 767.802KiB/s)
+progress: [/172.17.0.2]0:1/1 100% total: 100% 0.000KiB/s (avg: 670.295KiB/s)
+
+Summary statistics:
+   Connections per host    : 1
+   Total files transferred : 1
+   Total bytes transferred : 4.387MiB
+   Total duration          : 6706 ms
+   Average transfer rate   : 669.835KiB/s
+   Peak transfer rate      : 767.802KiB/s
+....
+
+== Throttling Load
+
+To prevent the table loader from overloading the system resources, you
+can throttle the process with the --throttle option. The default is
+unlimited (no throttling). Throttle units are in megabits. Note that the
+total duration is increased in the example below.
+
+Example:
+
+....
+sstableloader --nodes 172.17.0.2 --throttle 1 /var/lib/cassandra/loadme/keyspace1/standard1-f8a4fa30aa2a11e8af27091830ac5256/
+Established connection to initial hosts
+Opening sstables and calculating sections to stream
+Streaming relevant part of /var/lib/cassandra/loadme/keyspace1/standard1-f8a4fa30aa2a11e8af27091830ac5256/ma-6-big-Data.db to [/172.17.0.2]
+progress: [/172.17.0.2]0:1/1 100% total: 100% 0  MB/s(avg: 0 MB/s)
+Summary statistics:
+   Connections per host:         : 1
+   Total files transferred:      : 1
+   Total bytes transferred:      : 4595705
+   Total duration (ms):          : 37634
+   Average transfer rate (MB/s): : 0
+   Peak transfer rate (MB/s):    : 0
+....
+
+== Speeding up Load
+
+To speed up the load process, the number of connections per host can be
+increased.
+
+Example:
+
+....
+sstableloader --nodes 172.17.0.2 --connections-per-host 100 /var/lib/cassandra/loadme/keyspace1/standard1-f8a4fa30aa2a11e8af27091830ac5256/
+Established connection to initial hosts
+Opening sstables and calculating sections to stream
+Streaming relevant part of /var/lib/cassandra/loadme/keyspace1/standard1-f8a4fa30aa2a11e8af27091830ac5256/ma-9-big-Data.db to [/172.17.0.2]
+progress: [/172.17.0.2]0:1/1 100% total: 100% 0  MB/s(avg: 1 MB/s)
+Summary statistics:
+   Connections per host:         : 100
+   Total files transferred:      : 1
+   Total bytes transferred:      : 4595705
+   Total duration (ms):          : 3486
+   Average transfer rate (MB/s): : 1
+   Peak transfer rate (MB/s):    : 1
+....
+
+This small data set doesn't benefit much from the increase in
+connections per host, but note that the total duration has decreased in
+this example.
diff --git a/doc/modules/cassandra/pages/tools/sstable/sstablemetadata.adoc b/doc/modules/cassandra/pages/tools/sstable/sstablemetadata.adoc
new file mode 100644
index 0000000..0516bef
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/sstablemetadata.adoc
@@ -0,0 +1,320 @@
+= sstablemetadata
+
+Print information about an sstable from the related Statistics.db and
+Summary.db files to standard output.
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-7159 and
+https://issues.apache.org/jira/browse/CASSANDRA-10838
+
+Cassandra must be stopped before this tool is executed, or unexpected
+results will occur. Note: the script does not verify that Cassandra is
+stopped.
+
+== Usage
+
+sstablemetadata <options> <sstable filename(s)>
+
+[cols=",",]
+|===
+|--gc_grace_seconds <arg> |The gc_grace_seconds to use when calculating
+droppable tombstones
+|===
+
+== Print all the metadata
+
+Run sstablemetadata against the __Data.db file(s) related to a table. If
+necessary, find the__Data.db file(s) using sstableutil.
+
+Example:
+
+....
+sstableutil keyspace1 standard1 | grep Data
+/var/lib/cassandra/data/keyspace1/standard1-f6845640a6cb11e8b6836d2c86545d91/mc-1-big-Data.db
+
+sstablemetadata /var/lib/cassandra/data/keyspace1/standard1-f6845640a6cb11e8b6836d2c86545d91/mc-1-big-Data.db
+
+SSTable: /var/lib/cassandra/data/keyspace1/standard1-f6845640a6cb11e8b6836d2c86545d91/mc-1-big
+Partitioner: org.apache.cassandra.dht.Murmur3Partitioner
+Bloom Filter FP chance: 0.010000
+Minimum timestamp: 1535025576141000
+Maximum timestamp: 1535025604309000
+SSTable min local deletion time: 2147483647
+SSTable max local deletion time: 2147483647
+Compressor: org.apache.cassandra.io.compress.LZ4Compressor
+TTL min: 86400
+TTL max: 86400
+First token: -9223004712949498654 (key=39373333373831303130)
+Last token: 9222554117157811897 (key=4f3438394e39374d3730)
+Estimated droppable tombstones: 0.9188263888888889
+SSTable Level: 0
+Repaired at: 0
+Replay positions covered: {CommitLogPosition(segmentId=1535025390651, position=226400)=CommitLogPosition(segmentId=1535025390651, position=6849139)}
+totalColumnsSet: 100000
+totalRows: 20000
+Estimated tombstone drop times:
+1535039100:     80390
+1535039160:      5645
+1535039220:     13965
+Count               Row Size        Cell Count
+1                          0                 0
+2                          0                 0
+3                          0                 0
+4                          0                 0
+5                          0             20000
+6                          0                 0
+7                          0                 0
+8                          0                 0
+10                         0                 0
+12                         0                 0
+14                         0                 0
+17                         0                 0
+20                         0                 0
+24                         0                 0
+29                         0                 0
+35                         0                 0
+42                         0                 0
+50                         0                 0
+60                         0                 0
+72                         0                 0
+86                         0                 0
+103                        0                 0
+124                        0                 0
+149                        0                 0
+179                        0                 0
+215                        0                 0
+258                    20000                 0
+310                        0                 0
+372                        0                 0
+446                        0                 0
+535                        0                 0
+642                        0                 0
+770                        0                 0
+924                        0                 0
+1109                       0                 0
+1331                       0                 0
+1597                       0                 0
+1916                       0                 0
+2299                       0                 0
+2759                       0                 0
+3311                       0                 0
+3973                       0                 0
+4768                       0                 0
+5722                       0                 0
+6866                       0                 0
+8239                       0                 0
+9887                       0                 0
+11864                      0                 0
+14237                      0                 0
+17084                      0                 0
+20501                      0                 0
+24601                      0                 0
+29521                      0                 0
+35425                      0                 0
+42510                      0                 0
+51012                      0                 0
+61214                      0                 0
+73457                      0                 0
+88148                      0                 0
+105778                     0                 0
+126934                     0                 0
+152321                     0                 0
+182785                     0                 0
+219342                     0                 0
+263210                     0                 0
+315852                     0                 0
+379022                     0                 0
+454826                     0                 0
+545791                     0                 0
+654949                     0                 0
+785939                     0                 0
+943127                     0                 0
+1131752                    0                 0
+1358102                    0                 0
+1629722                    0                 0
+1955666                    0                 0
+2346799                    0                 0
+2816159                    0                 0
+3379391                    0                 0
+4055269                    0                 0
+4866323                    0                 0
+5839588                    0                 0
+7007506                    0                 0
+8409007                    0                 0
+10090808                   0                 0
+12108970                   0                 0
+14530764                   0                 0
+17436917                   0                 0
+20924300                   0                 0
+25109160                   0                 0
+30130992                   0                 0
+36157190                   0                 0
+43388628                   0                 0
+52066354                   0                 0
+62479625                   0                 0
+74975550                   0                 0
+89970660                   0                 0
+107964792                  0                 0
+129557750                  0                 0
+155469300                  0                 0
+186563160                  0                 0
+223875792                  0                 0
+268650950                  0                 0
+322381140                  0                 0
+386857368                  0                 0
+464228842                  0                 0
+557074610                  0                 0
+668489532                  0                 0
+802187438                  0                 0
+962624926                  0                 0
+1155149911                 0                 0
+1386179893                 0                 0
+1663415872                 0                 0
+1996099046                 0                 0
+2395318855                 0                 0
+2874382626                 0
+3449259151                 0
+4139110981                 0
+4966933177                 0
+5960319812                 0
+7152383774                 0
+8582860529                 0
+10299432635                 0
+12359319162                 0
+14831182994                 0
+17797419593                 0
+21356903512                 0
+25628284214                 0
+30753941057                 0
+36904729268                 0
+44285675122                 0
+53142810146                 0
+63771372175                 0
+76525646610                 0
+91830775932                 0
+110196931118                 0
+132236317342                 0
+158683580810                 0
+190420296972                 0
+228504356366                 0
+274205227639                 0
+329046273167                 0
+394855527800                 0
+473826633360                 0
+568591960032                 0
+682310352038                 0
+818772422446                 0
+982526906935                 0
+1179032288322                 0
+1414838745986                 0
+Estimated cardinality: 20196
+EncodingStats minTTL: 0
+EncodingStats minLocalDeletionTime: 1442880000
+EncodingStats minTimestamp: 1535025565275000
+KeyType: org.apache.cassandra.db.marshal.BytesType
+ClusteringTypes: [org.apache.cassandra.db.marshal.UTF8Type]
+StaticColumns: {C3:org.apache.cassandra.db.marshal.BytesType, C4:org.apache.cassandra.db.marshal.BytesType, C0:org.apache.cassandra.db.marshal.BytesType, C1:org.apache.cassandra.db.marshal.BytesType, C2:org.apache.cassandra.db.marshal.BytesType}
+RegularColumns: {}
+....
+
+== Specify gc grace seconds
+
+To see the ratio of droppable tombstones given a configured gc grace
+seconds, use the gc_grace_seconds option. Because the sstablemetadata
+tool doesn't access the schema directly, this is a way to more
+accurately estimate droppable tombstones -- for example, if you pass in
+gc_grace_seconds matching what is configured in the schema. The
+gc_grace_seconds value provided is subtracted from the curent machine
+time (in seconds).
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-12208
+
+Example:
+
+....
+sstablemetadata /var/lib/cassandra/data/keyspace1/standard1-41b52700b4ed11e896476d2c86545d91/mc-12-big-Data.db | grep "Estimated tombstone drop times" -A4
+Estimated tombstone drop times:
+1536599100:         1
+1536599640:         1
+1536599700:         2
+
+echo $(date +%s)
+1536602005
+
+# if gc_grace_seconds was configured at 100, all of the tombstones would be currently droppable 
+sstablemetadata --gc_grace_seconds 100 /var/lib/cassandra/data/keyspace1/standard1-41b52700b4ed11e896476d2c86545d91/mc-12-big-Data.db | grep "Estimated droppable tombstones"
+Estimated droppable tombstones: 4.0E-5
+
+# if gc_grace_seconds was configured at 4700, some of the tombstones would be currently droppable 
+sstablemetadata --gc_grace_seconds 4700 /var/lib/cassandra/data/keyspace1/standard1-41b52700b4ed11e896476d2c86545d91/mc-12-big-Data.db | grep "Estimated droppable tombstones"
+Estimated droppable tombstones: 9.61111111111111E-6
+
+# if gc_grace_seconds was configured at 100, none of the tombstones would be currently droppable 
+sstablemetadata --gc_grace_seconds 5000 /var/lib/cassandra/data/keyspace1/standard1-41b52700b4ed11e896476d2c86545d91/mc-12-big-Data.db | grep "Estimated droppable tombstones"
+Estimated droppable tombstones: 0.0
+....
+
+== Explanation of each value printed above
+
+|===
+|Value |Explanation
+
+
+|SSTable |prefix of the sstable filenames related to this sstable
+|Partitioner |partitioner type used to distribute data across nodes;
+defined in cassandra.yaml 
+|Bloom Filter FP |precision of Bloom filter used
+in reads; defined in the table definition 
+|Minimum timestamp |minimum
+timestamp of any entry in this sstable, in epoch microseconds 
+|Maximum
+timestamp |maximum timestamp of any entry in this sstable, in epoch
+microseconds 
+|SSTable min local deletion time |minimum timestamp of
+deletion date, based on TTL, in epoch seconds 
+|SSTable max local deletion
+time |maximum timestamp of deletion date, based on TTL, in epoch seconds
+|Compressor |blank (-) by default; if not blank, indicates type of
+compression enabled on the table 
+|TTL min |time-to-live in seconds;
+default 0 unless defined in the table definition 
+|TTL max |time-to-live in
+seconds; default 0 unless defined in the table definition 
+|First token |lowest token and related key found in the sstable summary 
+|Last token |highest token and related key found in the sstable summary 
+|Estimated
+droppable tombstones |ratio of tombstones to columns, using configured gc
+grace seconds if relevant 
+|SSTable level |compaction level of this
+sstable, if leveled compaction (LCS) is used 
+|Repaired at |the timestamp
+this sstable was marked as repaired via sstablerepairedset, in epoch
+milliseconds 
+|Replay positions covered |the interval of time and commitlog
+positions related to this sstable 
+|totalColumnsSet |number of cells in the
+table 
+|totalRows |number of rows in the table 
+|Estimated tombstone drop
+times |approximate number of rows that will expire, ordered by epoch
+seconds 
+|Count Row Size Cell Count |two histograms in two columns; one
+represents distribution of Row Size and the other represents
+distribution of Cell Count 
+|Estimated cardinality an estimate of unique
+values, used for compaction 
+|EncodingStats* minTTL |in epoch milliseconds
+|EncodingStats* minLocalDeletionTime |in epoch seconds 
+|EncodingStats*
+minTimestamp |in epoch microseconds 
+|KeyType |the type of partition key,
+useful in reading and writing data from/to storage; defined in the table
+definition 
+|ClusteringTypes |the type of clustering key, useful in reading
+and writing data from/to storage; defined in the table definition
+|StaticColumns |a list of the shared columns in the table 
+|RegularColumns |a
+list of non-static, non-key columns in the table
+|===
+
+`*` For the encoding stats values, the delta of this and the current epoch
+time is used when encoding and storing data in the most optimal way.
diff --git a/doc/modules/cassandra/pages/tools/sstable/sstableofflinerelevel.adoc b/doc/modules/cassandra/pages/tools/sstable/sstableofflinerelevel.adoc
new file mode 100644
index 0000000..71bffbf
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/sstableofflinerelevel.adoc
@@ -0,0 +1,94 @@
+= sstableofflinerelevel
+
+When using LeveledCompactionStrategy, sstables can get stuck at L0 on a
+recently bootstrapped node, and compactions may never catch up. This
+tool is used to bump sstables into the highest level possible.
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-8301
+
+The way this is done is: sstables are storted by their last token. Given
+an original leveling like this (note that [ ] indicates token
+boundaries, not sstable size on disk; all sstables are the same size):
+
+....
+L3 [][][][][][][][][][][]
+L2 [    ][    ][    ][  ]
+L1 [          ][        ]
+L0 [                    ]
+....
+
+Will look like this after being dropped to L0 and sorted by last token
+(and, to illustrate overlap, the overlapping ones are put on a new
+line):
+
+....
+[][][]
+[    ][][][]
+    [    ]
+[          ]
+...
+....
+
+Then, we start iterating from the smallest last-token and adding all
+sstables that do not cause an overlap to a level. We will reconstruct
+the original leveling top-down. Whenever we add an sstable to the level,
+we remove it from the sorted list. Once we reach the end of the sorted
+list, we have a full level, and can start over with the level below.
+
+If we end up with more levels than expected, we put all levels exceeding
+the expected in L0, for example, original L0 files will most likely be
+put in a level of its own since they most often overlap many other
+sstables.
+
+Cassandra must be stopped before this tool is executed, or unexpected
+results will occur. Note: the script does not verify that Cassandra is
+stopped.
+
+== Usage
+
+sstableofflinerelevel [--dry-run] <keyspace> <table>
+
+== Doing a dry run
+
+Use the --dry-run option to see the current level distribution and
+predicted level after the change.
+
+Example:
+
+....
+sstableofflinerelevel --dry-run keyspace eventlog
+For sstables in /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753:
+Current leveling:
+L0=2
+Potential leveling:
+L0=1
+L1=1
+....
+
+== Running a relevel
+
+Example:
+
+....
+sstableofflinerelevel keyspace eventlog
+For sstables in /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753:
+Current leveling:
+L0=2
+New leveling:
+L0=1
+L1=1
+....
+
+== Keyspace or table not found
+
+If an invalid keyspace and/or table is provided, an exception will be
+thrown.
+
+Example:
+
+....
+sstableofflinerelevel --dry-run keyspace evenlog
+
+Exception in thread "main" java.lang.IllegalArgumentException: Unknown keyspace/columnFamily keyspace1.evenlog
+    at org.apache.cassandra.tools.SSTableOfflineRelevel.main(SSTableOfflineRelevel.java:96)
+....
diff --git a/doc/modules/cassandra/pages/tools/sstable/sstablerepairedset.adoc b/doc/modules/cassandra/pages/tools/sstable/sstablerepairedset.adoc
new file mode 100644
index 0000000..e18859b
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/sstablerepairedset.adoc
@@ -0,0 +1,83 @@
+= sstablerepairedset
+
+Repairs can take a very long time in some environments, for large sizes
+of data. Use this tool to set the repairedAt status on a given set of
+sstables, so that repairs can be run on only un-repaired sstables if
+desired.
+
+Note that running a repair (e.g., via nodetool repair) doesn't set the
+status of this metadata. Only setting the status of this metadata via
+this tool does.
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-5351
+
+Cassandra must be stopped before this tool is executed, or unexpected
+results will occur. Note: the script does not verify that Cassandra is
+stopped.
+
+== Usage
+
+sstablerepairedset --really-set <options> [-f <sstable-list> |
+<sstables>]
+
+[cols=",",]
+|===
+|--really-set |required if you want to really set the status
+|--is-repaired |set the repairedAt status to the last modified time
+|--is-unrepaired |set the repairedAt status to 0
+|-f |use a file containing a list of sstables as the input
+|===
+
+== Set a lot of sstables to unrepaired status
+
+There are many ways to do this programmatically. This way would likely
+include variables for the keyspace and table.
+
+Example:
+
+....
+find /var/lib/cassandra/data/keyspace1/standard1-d936bd20a17c11e8bc92a55ed562cd82/* -name "*Data.db" -print0 | xargs -0 -I % sstablerepairedset --really-set --is-unrepaired %
+....
+
+== Set one to many sstables to repaired status
+
+Set the repairedAt status after a repair to mark the sstables as
+repaired. Again, using variables for the keyspace and table names is a
+good choice.
+
+Example:
+
+....
+nodetool repair keyspace1 standard1
+find /var/lib/cassandra/data/keyspace1/standard1-d936bd20a17c11e8bc92a55ed562cd82/* -name "*Data.db" -print0 | xargs -0 -I % sstablerepairedset --really-set --is-repaired %
+....
+
+== Print metadata showing repaired status
+
+sstablemetadata can be used to view the status set or unset using this
+command.
+
+Example:
+
+____
+sstablerepairedset --really-set --is-repaired
+/var/lib/cassandra/data/keyspace1/standard1-d936bd20a17c11e8bc92a55ed562cd82/mc-1-big-Data.db
+sstablemetadata
+/var/lib/cassandra/data/keyspace1/standard1-d936bd20a17c11e8bc92a55ed562cd82/mc-1-big-Data.db
+| grep "Repaired at" Repaired at: 1534443974000
+
+sstablerepairedset --really-set --is-unrepaired
+/var/lib/cassandra/data/keyspace1/standard1-d936bd20a17c11e8bc92a55ed562cd82/mc-1-big-Data.db
+sstablemetadata
+/var/lib/cassandra/data/keyspace1/standard1-d936bd20a17c11e8bc92a55ed562cd82/mc-1-big-Data.db
+| grep "Repaired at" Repaired at: 0
+____
+
+== Using command in a script
+
+If you know you ran repair 2 weeks ago, you can do something like the
+following:
+
+....
+sstablerepairset --is-repaired -f <(find /var/lib/cassandra/data/.../ -iname "*Data.db*" -mtime +14)
+....
diff --git a/doc/modules/cassandra/pages/tools/sstable/sstablescrub.adoc b/doc/modules/cassandra/pages/tools/sstable/sstablescrub.adoc
new file mode 100644
index 0000000..1826e9e
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/sstablescrub.adoc
@@ -0,0 +1,102 @@
+= sstablescrub
+
+Fix a broken sstable. The scrub process rewrites the sstable, skipping
+any corrupted rows. Because these rows are lost, follow this process
+with a repair.
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-4321
+
+Cassandra must be stopped before this tool is executed, or unexpected
+results will occur. Note: the script does not verify that Cassandra is
+stopped.
+
+== Usage
+
+sstablescrub <options> <keyspace> <table>
+
+[cols=",",]
+|===
+|--debug |display stack traces
+
+|-h,--help |display this help message
+
+|-m,--manifest-check |only check and repair the leveled manifest,
+without actually scrubbing the sstables
+
+|-n,--no-validate |do not validate columns using column validator
+
+|-r,--reinsert-overflowed-ttl |Rewrites rows with overflowed expiration
+date affected by CASSANDRA-14092 with the maximum supported expiration
+date of 2038-01-19T03:14:06+00:00. The rows are rewritten with the
+original timestamp incremented by one millisecond to override/supersede
+any potential tombstone that may have been generated during compaction
+of the affected rows.
+
+|-s,--skip-corrupted |skip corrupt rows in counter tables
+
+|-v,--verbose |verbose output
+|===
+
+== Basic Scrub
+
+The scrub without options will do a snapshot first, then write all
+non-corrupted files to a new sstable.
+
+Example:
+
+....
+sstablescrub keyspace1 standard1
+Pre-scrub sstables snapshotted into snapshot pre-scrub-1534424070883
+Scrubbing BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-6365332094dd11e88f324f9c503e4753/mc-5-big-Data.db') (17.142MiB)
+Scrub of BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-6365332094dd11e88f324f9c503e4753/mc-5-big-Data.db') complete: 73367 rows in new sstable and 0 empty (tombstoned) rows dropped
+Checking leveled manifest
+....
+
+== Scrub without Validation
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-9406
+
+Use the --no-validate option to retain data that may be misrepresented
+(e.g., an integer stored in a long field) but not corrupt. This data
+usually doesn not present any errors to the client.
+
+Example:
+
+....
+sstablescrub --no-validate keyspace1 standard1
+Pre-scrub sstables snapshotted into snapshot pre-scrub-1536243158517
+Scrubbing BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-bc9cf530b1da11e886c66d2c86545d91/mc-2-big-Data.db') (4.482MiB)
+Scrub of BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-bc9cf530b1da11e886c66d2c86545d91/mc-2-big-Data.db') complete; looks like all 0 rows were tombstoned
+....
+
+== Skip Corrupted Counter Tables
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-5930
+
+If counter tables are corrupted in a way that prevents sstablescrub from
+completing, you can use the --skip-corrupted option to skip scrubbing
+those counter tables. This workaround is not necessary in versions 2.0+.
+
+Example:
+
+....
+sstablescrub --skip-corrupted keyspace1 counter1
+....
+
+== Dealing with Overflow Dates
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-14092
+
+Using the option --reinsert-overflowed-ttl allows a rewriting of rows
+that had a max TTL going over the maximum (causing an overflow).
+
+Example:
+
+....
+sstablescrub --reinsert-overflowed-ttl keyspace1 counter1
+....
+
+== Manifest Check
+
+As of Cassandra version 2.0, this option is no longer relevant, since
+level data was moved from a separate manifest into the sstable metadata.
diff --git a/doc/modules/cassandra/pages/tools/sstable/sstablesplit.adoc b/doc/modules/cassandra/pages/tools/sstable/sstablesplit.adoc
new file mode 100644
index 0000000..f62b868
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/sstablesplit.adoc
@@ -0,0 +1,96 @@
+= sstablesplit
+
+Big sstable files can take up a lot of disk space. The sstablesplit tool
+can be used to split those large files into smaller files. It can be
+thought of as a type of anticompaction.
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-4766
+
+Cassandra must be stopped before this tool is executed, or unexpected
+results will occur. Note: the script does not verify that Cassandra is
+stopped.
+
+== Usage
+
+sstablesplit <options> <filename>
+
+[cols=",",]
+|===
+|--debug |display stack traces
+
+|-h, --help |display this help message
+
+|--no-snapshot |don't snapshot the sstables before splitting
+
+|-s, --size <size> |maximum size in MB for the output sstables (default:
+50)
+|===
+
+This command should be run with Cassandra stopped. Note: the script does
+not verify that Cassandra is stopped.
+
+== Split a File
+
+Split a large sstable into smaller sstables. By default, unless the
+option --no-snapshot is added, a snapshot will be done of the original
+sstable and placed in the snapshots folder.
+
+Example:
+
+....
+sstablesplit /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-8-big-Data.db
+
+Pre-split sstables snapshotted into snapshot pre-split-1533144514795
+....
+
+== Split Multiple Files
+
+Wildcards can be used in the filename portion of the command to split
+multiple files.
+
+Example:
+
+....
+sstablesplit --size 1 /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-1*
+....
+
+== Attempt to Split a Small File
+
+If the file is already smaller than the split size provided, the sstable
+will not be split.
+
+Example:
+
+....
+sstablesplit /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-8-big-Data.db
+Skipping /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-8-big-Data.db: it's size (1.442 MB) is less than the split size (50 MB)
+No sstables needed splitting.
+....
+
+== Split a File into Specified Size
+
+The default size used for splitting is 50MB. Specify another size with
+the --size option. The size is in megabytes (MB). Specify only the
+number, not the units. For example --size 50 is correct, but --size 50MB
+is not.
+
+Example:
+
+....
+sstablesplit --size 1 /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-9-big-Data.db
+Pre-split sstables snapshotted into snapshot pre-split-1533144996008
+....
+
+== Split Without Snapshot
+
+By default, sstablesplit will create a snapshot before splitting. If a
+snapshot is not needed, use the --no-snapshot option to skip it.
+
+Example:
+
+....
+sstablesplit --size 1 --no-snapshot /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-11-big-Data.db
+....
+
+Note: There is no output, but you can see the results in your file
+system.
diff --git a/doc/modules/cassandra/pages/tools/sstable/sstableupgrade.adoc b/doc/modules/cassandra/pages/tools/sstable/sstableupgrade.adoc
new file mode 100644
index 0000000..ad193e2
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/sstableupgrade.adoc
@@ -0,0 +1,136 @@
+= sstableupgrade
+
+Upgrade the sstables in the given table (or snapshot) to the current
+version of Cassandra. This process is typically done after a Cassandra
+version upgrade. This operation will rewrite the sstables in the
+specified table to match the currently installed version of Cassandra.
+The sstableupgrade command can also be used to downgrade sstables to a
+previous version.
+
+The snapshot option will only upgrade the specified snapshot. Upgrading
+snapshots is required before attempting to restore a snapshot taken in a
+major version older than the major version Cassandra is currently
+running. This will replace the files in the given snapshot as well as
+break any hard links to live sstables.
+
+Cassandra must be stopped before this tool is executed, or unexpected
+results will occur. Note: the script does not verify that Cassandra is
+stopped.
+
+== Usage
+
+sstableupgrade <options> <keyspace> <table> [snapshot_name]
+
+[cols=",",]
+|===
+|--debug |display stack traces
+|-h,--help |display this help message
+|-k,--keep-source |do not delete the source sstables
+|===
+
+== Rewrite tables to the current Cassandra version
+
+Start with a set of sstables in one version of Cassandra:
+
+....
+ls -al /tmp/cassandra/data/keyspace1/standard1-9695b790a63211e8a6fb091830ac5256/
+...
+-rw-r--r--   1 user  wheel      348 Aug 22 13:45 keyspace1-standard1-ka-1-CRC.db
+-rw-r--r--   1 user  wheel  5620000 Aug 22 13:45 keyspace1-standard1-ka-1-Data.db
+-rw-r--r--   1 user  wheel       10 Aug 22 13:45 keyspace1-standard1-ka-1-Digest.sha1
+-rw-r--r--   1 user  wheel    25016 Aug 22 13:45 keyspace1-standard1-ka-1-Filter.db
+-rw-r--r--   1 user  wheel   480000 Aug 22 13:45 keyspace1-standard1-ka-1-Index.db
+-rw-r--r--   1 user  wheel     9895 Aug 22 13:45 keyspace1-standard1-ka-1-Statistics.db
+-rw-r--r--   1 user  wheel     3562 Aug 22 13:45 keyspace1-standard1-ka-1-Summary.db
+-rw-r--r--   1 user  wheel       79 Aug 22 13:45 keyspace1-standard1-ka-1-TOC.txt
+....
+
+After upgrading the Cassandra version, upgrade the sstables:
+
+....
+sstableupgrade keyspace1 standard1
+Found 1 sstables that need upgrading.
+Upgrading BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-9695b790a63211e8a6fb091830ac5256/keyspace1-standard1-ka-1-Data.db')
+Upgrade of BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-9695b790a63211e8a6fb091830ac5256/keyspace1-standard1-ka-1-Data.db') complete.
+
+ls -al /tmp/cassandra/data/keyspace1/standard1-9695b790a63211e8a6fb091830ac5256/
+...
+drwxr-xr-x   2 user  wheel       64 Aug 22 13:48 backups
+-rw-r--r--   1 user  wheel      292 Aug 22 13:48 mc-2-big-CRC.db
+-rw-r--r--   1 user  wheel  4599475 Aug 22 13:48 mc-2-big-Data.db
+-rw-r--r--   1 user  wheel       10 Aug 22 13:48 mc-2-big-Digest.crc32
+-rw-r--r--   1 user  wheel    25256 Aug 22 13:48 mc-2-big-Filter.db
+-rw-r--r--   1 user  wheel   330807 Aug 22 13:48 mc-2-big-Index.db
+-rw-r--r--   1 user  wheel    10312 Aug 22 13:48 mc-2-big-Statistics.db
+-rw-r--r--   1 user  wheel     3506 Aug 22 13:48 mc-2-big-Summary.db
+-rw-r--r--   1 user  wheel       80 Aug 22 13:48 mc-2-big-TOC.txt
+....
+
+== Rewrite tables to the current Cassandra version, and keep tables in old version
+
+Again, starting with a set of sstables in one version:
+
+....
+ls -al /tmp/cassandra/data/keyspace1/standard1-db532690a63411e8b4ae091830ac5256/
+...
+-rw-r--r--   1 user  wheel      348 Aug 22 13:58 keyspace1-standard1-ka-1-CRC.db
+-rw-r--r--   1 user  wheel  5620000 Aug 22 13:58 keyspace1-standard1-ka-1-Data.db
+-rw-r--r--   1 user  wheel       10 Aug 22 13:58 keyspace1-standard1-ka-1-Digest.sha1
+-rw-r--r--   1 user  wheel    25016 Aug 22 13:58 keyspace1-standard1-ka-1-Filter.db
+-rw-r--r--   1 user  wheel   480000 Aug 22 13:58 keyspace1-standard1-ka-1-Index.db
+-rw-r--r--   1 user  wheel     9895 Aug 22 13:58 keyspace1-standard1-ka-1-Statistics.db
+-rw-r--r--   1 user  wheel     3562 Aug 22 13:58 keyspace1-standard1-ka-1-Summary.db
+-rw-r--r--   1 user  wheel       79 Aug 22 13:58 keyspace1-standard1-ka-1-TOC.txt
+....
+
+After upgrading the Cassandra version, upgrade the sstables, retaining
+the original sstables:
+
+....
+sstableupgrade keyspace1 standard1 -k
+Found 1 sstables that need upgrading.
+Upgrading BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-db532690a63411e8b4ae091830ac5256/keyspace1-standard1-ka-1-Data.db')
+Upgrade of BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-db532690a63411e8b4ae091830ac5256/keyspace1-standard1-ka-1-Data.db') complete.
+
+ls -al /tmp/cassandra/data/keyspace1/standard1-db532690a63411e8b4ae091830ac5256/
+...
+drwxr-xr-x   2 user  wheel       64 Aug 22 14:00 backups
+-rw-r--r--@  1 user  wheel      348 Aug 22 13:58 keyspace1-standard1-ka-1-CRC.db
+-rw-r--r--@  1 user  wheel  5620000 Aug 22 13:58 keyspace1-standard1-ka-1-Data.db
+-rw-r--r--@  1 user  wheel       10 Aug 22 13:58 keyspace1-standard1-ka-1-Digest.sha1
+-rw-r--r--@  1 user  wheel    25016 Aug 22 13:58 keyspace1-standard1-ka-1-Filter.db
+-rw-r--r--@  1 user  wheel   480000 Aug 22 13:58 keyspace1-standard1-ka-1-Index.db
+-rw-r--r--@  1 user  wheel     9895 Aug 22 13:58 keyspace1-standard1-ka-1-Statistics.db
+-rw-r--r--@  1 user  wheel     3562 Aug 22 13:58 keyspace1-standard1-ka-1-Summary.db
+-rw-r--r--@  1 user  wheel       79 Aug 22 13:58 keyspace1-standard1-ka-1-TOC.txt
+-rw-r--r--   1 user  wheel      292 Aug 22 14:01 mc-2-big-CRC.db
+-rw-r--r--   1 user  wheel  4596370 Aug 22 14:01 mc-2-big-Data.db
+-rw-r--r--   1 user  wheel       10 Aug 22 14:01 mc-2-big-Digest.crc32
+-rw-r--r--   1 user  wheel    25256 Aug 22 14:01 mc-2-big-Filter.db
+-rw-r--r--   1 user  wheel   330801 Aug 22 14:01 mc-2-big-Index.db
+-rw-r--r--   1 user  wheel    10312 Aug 22 14:01 mc-2-big-Statistics.db
+-rw-r--r--   1 user  wheel     3506 Aug 22 14:01 mc-2-big-Summary.db
+-rw-r--r--   1 user  wheel       80 Aug 22 14:01 mc-2-big-TOC.txt
+....
+
+== Rewrite a snapshot to the current Cassandra version
+
+Find the snapshot name:
+
+....
+nodetool listsnapshots
+
+Snapshot Details:
+Snapshot name       Keyspace name                Column family name           True size          Size on disk
+...
+1534962986979       keyspace1                    standard1                    5.85 MB            5.85 MB
+....
+
+Then rewrite the snapshot:
+
+....
+sstableupgrade keyspace1 standard1 1534962986979
+Found 1 sstables that need upgrading.
+Upgrading BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-5850e9f0a63711e8a5c5091830ac5256/snapshots/1534962986979/keyspace1-standard1-ka-1-Data.db')
+Upgrade of BigTableReader(path='/var/lib/cassandra/data/keyspace1/standard1-5850e9f0a63711e8a5c5091830ac5256/snapshots/1534962986979/keyspace1-standard1-ka-1-Data.db') complete.
+....
diff --git a/doc/modules/cassandra/pages/tools/sstable/sstableutil.adoc b/doc/modules/cassandra/pages/tools/sstable/sstableutil.adoc
new file mode 100644
index 0000000..9a718f1
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/sstableutil.adoc
@@ -0,0 +1,102 @@
+= sstableutil
+
+List sstable files for the provided table.
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-7066
+
+Cassandra must be stopped before this tool is executed, or unexpected
+results will occur. Note: the script does not verify that Cassandra is
+stopped.
+
+== Usage
+
+sstableutil <options> <keyspace> <table>
+
+[cols=",",]
+|===
+|-c, --cleanup |clean up any outstanding transactions
+
+|-d, --debug |display stack traces
+
+|-h, --help |display this help message
+
+|-o, --oplog |include operation logs
+
+|-t, --type <arg> |all (list all files, final or temporary), tmp (list
+temporary files only), final (list final files only),
+
+|-v, --verbose |verbose output
+|===
+
+== List all sstables
+
+The basic command lists the sstables associated with a given
+keyspace/table.
+
+Example:
+
+....
+sstableutil keyspace eventlog
+Listing files...
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-CRC.db
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Data.db
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Digest.crc32
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Filter.db
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Index.db
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Statistics.db
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Summary.db
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-TOC.txt
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-CRC.db
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Data.db
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Digest.crc32
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Filter.db
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Index.db
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Statistics.db
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Summary.db
+/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-TOC.txt
+....
+
+== List only temporary sstables
+
+Using the -t option followed by [.title-ref]#tmp# will list all
+temporary sstables, in the format above. Temporary sstables were used in
+pre-3.0 versions of Cassandra.
+
+== List only final sstables
+
+Using the -t option followed by [.title-ref]#final# will list all final
+sstables, in the format above. In recent versions of Cassandra, this is
+the same output as not using the -t option.
+
+== Include transaction logs
+
+Using the -o option will include transaction logs in the listing, in the
+format above.
+
+== Clean up sstables
+
+Using the -c option removes any transactions left over from incomplete
+writes or compactions.
+
+From the 3.0 upgrade notes:
+
+New transaction log files have been introduced to replace the
+compactions_in_progress system table, temporary file markers (tmp and
+tmplink) and sstable ancestors. Therefore, compaction metadata no longer
+contains ancestors. Transaction log files list sstable descriptors
+involved in compactions and other operations such as flushing and
+streaming. Use the sstableutil tool to list any sstable files currently
+involved in operations not yet completed, which previously would have
+been marked as temporary. A transaction log file contains one sstable
+per line, with the prefix "add:" or "remove:". They also contain a
+special line "commit", only inserted at the end when the transaction is
+committed. On startup we use these files to cleanup any partial
+transactions that were in progress when the process exited. If the
+commit line is found, we keep new sstables (those with the "add" prefix)
+and delete the old sstables (those with the "remove" prefix), vice-versa
+if the commit line is missing. Should you lose or delete these log
+files, both old and new sstable files will be kept as live files, which
+will result in duplicated sstables. These files are protected by
+incremental checksums so you should not manually edit them. When
+restoring a full backup or moving sstable files, you should clean-up any
+left over transactions and their temporary files first.
diff --git a/doc/modules/cassandra/pages/tools/sstable/sstableverify.adoc b/doc/modules/cassandra/pages/tools/sstable/sstableverify.adoc
new file mode 100644
index 0000000..0af2f15
--- /dev/null
+++ b/doc/modules/cassandra/pages/tools/sstable/sstableverify.adoc
@@ -0,0 +1,82 @@
+= sstableverify
+
+Check sstable(s) for errors or corruption, for the provided table.
+
+ref: https://issues.apache.org/jira/browse/CASSANDRA-5791
+
+Cassandra must be stopped before this tool is executed, or unexpected
+results will occur. Note: the script does not verify that Cassandra is
+stopped.
+
+== Usage
+
+sstableverify <options> <keyspace> <table>
+
+[cols=",",]
+|===
+|--debug |display stack traces
+|-e, --extended |extended verification
+|-h, --help |display this help message
+|-v, --verbose |verbose output
+|===
+
+== Basic Verification
+
+This is the basic verification. It is not a very quick process, and uses
+memory. You might need to increase your memory settings if you have many
+sstables.
+
+Example:
+
+....
+sstableverify keyspace eventlog
+Verifying BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Data.db') (7.353MiB)
+Deserializing sstable metadata for BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Data.db')
+Checking computed hash of BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Data.db')
+Verifying BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Data.db') (3.775MiB)
+Deserializing sstable metadata for BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Data.db')
+Checking computed hash of BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Data.db')
+....
+
+== Extended Verification
+
+During an extended verification, the individual values will be validated
+for errors or corruption. This of course takes more time.
+
+Example:
+
+....
+root@DC1C1:/# sstableverify -e keyspace eventlog
+WARN  14:08:06,255 Only 33.096GiB free across all data volumes. Consider adding more capacity to your cluster or removing obsolete snapshots
+Verifying BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Data.db') (7.353MiB)
+Deserializing sstable metadata for BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Data.db')
+Checking computed hash of BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Data.db')
+Extended Verify requested, proceeding to inspect values
+Verify of BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-32-big-Data.db') succeeded. All 33211 rows read successfully
+Verifying BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Data.db') (3.775MiB)
+Deserializing sstable metadata for BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Data.db')
+Checking computed hash of BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Data.db')
+Extended Verify requested, proceeding to inspect values
+Verify of BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-37-big-Data.db') succeeded. All 17068 rows read successfully
+....
+
+== Corrupted File
+
+Corrupted files are listed if they are detected by the script.
+
+Example:
+
+....
+sstableverify keyspace eventlog
+Verifying BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-40-big-Data.db') (7.416MiB)
+Deserializing sstable metadata for BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-40-big-Data.db')
+Checking computed hash of BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-40-big-Data.db')
+Error verifying BigTableReader(path='/var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-40-big-Data.db'): Corrupted: /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-40-big-Data.db
+....
+
+A similar (but less verbose) tool will show the suggested actions:
+
+....
+nodetool verify keyspace eventlog
+error: Invalid SSTable /var/lib/cassandra/data/keyspace/eventlog-6365332094dd11e88f324f9c503e4753/mc-40-big-Data.db, please force repair
+....
diff --git a/doc/modules/cassandra/pages/troubleshooting/finding_nodes.adoc b/doc/modules/cassandra/pages/troubleshooting/finding_nodes.adoc
new file mode 100644
index 0000000..522bbb9
--- /dev/null
+++ b/doc/modules/cassandra/pages/troubleshooting/finding_nodes.adoc
@@ -0,0 +1,133 @@
+= Find The Misbehaving Nodes
+
+The first step to troubleshooting a Cassandra issue is to use error
+messages, metrics and monitoring information to identify if the issue
+lies with the clients or the server and if it does lie with the server
+find the problematic nodes in the Cassandra cluster. The goal is to
+determine if this is a systemic issue (e.g. a query pattern that affects
+the entire cluster) or isolated to a subset of nodes (e.g. neighbors
+holding a shared token range or even a single node with bad hardware).
+
+There are many sources of information that help determine where the
+problem lies. Some of the most common are mentioned below.
+
+== Client Logs and Errors
+
+Clients of the cluster often leave the best breadcrumbs to follow.
+Perhaps client latencies or error rates have increased in a particular
+datacenter (likely eliminating other datacenter's nodes), or clients are
+receiving a particular kind of error code indicating a particular kind
+of problem. Troubleshooters can often rule out many failure modes just
+by reading the error messages. In fact, many Cassandra error messages
+include the last coordinator contacted to help operators find nodes to
+start with.
+
+Some common errors (likely culprit in parenthesis) assuming the client
+has similar error names as the Datastax `drivers <client-drivers>`:
+
+* `SyntaxError` (*client*). This and other `QueryValidationException`
+indicate that the client sent a malformed request. These are rarely
+server issues and usually indicate bad queries.
+* `UnavailableException` (*server*): This means that the Cassandra
+coordinator node has rejected the query as it believes that insufficent
+replica nodes are available. If many coordinators are throwing this
+error it likely means that there really are (typically) multiple nodes
+down in the cluster and you can identify them using `nodetool status
+<nodetool-status>` If only a single coordinator is throwing this error
+it may mean that node has been partitioned from the rest.
+* `OperationTimedOutException` (*server*): This is the most frequent
+timeout message raised when clients set timeouts and means that the
+query took longer than the supplied timeout. This is a _client side_
+timeout meaning that it took longer than the client specified timeout.
+The error message will include the coordinator node that was last tried
+which is usually a good starting point. This error usually indicates
+either aggressive client timeout values or latent server
+coordinators/replicas.
+* `ReadTimeoutException` or `WriteTimeoutException` (*server*): These
+are raised when clients do not specify lower timeouts and there is a
+_coordinator_ timeouts based on the values supplied in the
+`cassandra.yaml` configuration file. They usually indicate a serious
+server side problem as the default values are usually multiple seconds.
+
+== Metrics
+
+If you have Cassandra xref:operating/metrics.adoc[`metrics`] reporting to a
+centralized location such as https://graphiteapp.org/[Graphite] or
+https://grafana.com/[Grafana] you can typically use those to narrow down
+the problem. At this stage narrowing down the issue to a particular
+datacenter, rack, or even group of nodes is the main goal. Some helpful
+metrics to look at are:
+
+=== Errors
+
+Cassandra refers to internode messaging errors as "drops", and provided
+a number of xref:operating/metrics.adoc#droppedmessage-metrics[`Dropped Message Metrics`] to help narrow
+down errors. If particular nodes are dropping messages actively, they
+are likely related to the issue.
+
+=== Latency
+
+For timeouts or latency related issues you can start with operating/metrics.adoc#table-metrics[`table metrics`] 
+by comparing Coordinator level metrics e.g.
+`CoordinatorReadLatency` or `CoordinatorWriteLatency` with their
+associated replica metrics e.g. `ReadLatency` or `WriteLatency`. Issues
+usually show up on the `99th` percentile before they show up on the
+`50th` percentile or the `mean`. While `maximum` coordinator latencies
+are not typically very helpful due to the exponentially decaying
+reservoir used internally to produce metrics, `maximum` replica
+latencies that correlate with increased `99th` percentiles on
+coordinators can help narrow down the problem.
+
+There are usually three main possibilities:
+
+[arabic]
+. Coordinator latencies are high on all nodes, but only a few node's
+local read latencies are high. This points to slow replica nodes and the
+coordinator's are just side-effects. This usually happens when clients
+are not token aware.
+. Coordinator latencies and replica latencies increase at the same time
+on the a few nodes. If clients are token aware this is almost always
+what happens and points to slow replicas of a subset of token ranges
+(only part of the ring).
+. Coordinator and local latencies are high on many nodes. This usually
+indicates either a tipping point in the cluster capacity (too many
+writes or reads per second), or a new query pattern.
+
+It's important to remember that depending on the client's load balancing
+behavior and consistency levels coordinator and replica metrics may or
+may not correlate. In particular if you use `TokenAware` policies the
+same node's coordinator and replica latencies will often increase
+together, but if you just use normal `DCAwareRoundRobin` coordinator
+latencies can increase with unrelated replica node's latencies. For
+example:
+
+* `TokenAware` + `LOCAL_ONE`: should always have coordinator and replica
+latencies on the same node rise together
+* `TokenAware` + `LOCAL_QUORUM`: should always have coordinator and
+multiple replica latencies rise together in the same datacenter.
+* `TokenAware` + `QUORUM`: replica latencies in other datacenters can
+affect coordinator latencies.
+* `DCAwareRoundRobin` + `LOCAL_ONE`: coordinator latencies and unrelated
+replica node's latencies will rise together.
+* `DCAwareRoundRobin` + `LOCAL_QUORUM`: different coordinator and
+replica latencies will rise together with little correlation.
+
+=== Query Rates
+
+Sometimes the xref:operating/metrics.adoc#table-metrics[`table metric`] query rate metrics can help narrow
+down load issues as "small" increase in coordinator queries per second
+(QPS) may correlate with a very large increase in replica level QPS.
+This most often happens with `BATCH` writes, where a client may send a
+single `BATCH` query that might contain 50 statements in it, which if
+you have 9 copies (RF=3, three datacenters) means that every coordinator
+`BATCH` write turns into 450 replica writes! This is why keeping
+`BATCH`'s to the same partition is so critical, otherwise you can
+exhaust significant CPU capacitity with a "single" query.
+
+== Next Step: Investigate the Node(s)
+
+Once you have narrowed down the problem as much as possible (datacenter,
+rack , node), login to one of the nodes using SSH and proceed to debug
+using xref:reading_logs.adoc[`logs`], xref:use_nodetooladoc[`nodetool`], and
+xref:use_tools.adoc[`os tools`]. 
+If you are not able to login you may still have access to `logs` and `nodetool` remotely.
diff --git a/doc/modules/cassandra/pages/troubleshooting/index.adoc b/doc/modules/cassandra/pages/troubleshooting/index.adoc
new file mode 100644
index 0000000..f527965
--- /dev/null
+++ b/doc/modules/cassandra/pages/troubleshooting/index.adoc
@@ -0,0 +1,19 @@
+= Troubleshooting
+
+As any distributed database does, sometimes Cassandra breaks and you
+will have to troubleshoot what is going on. Generally speaking you can
+debug Cassandra like any other distributed Java program, meaning that
+you have to find which machines in your cluster are misbehaving and then
+isolate the problem using logs and tools. Luckily Cassandra had a great
+set of instrospection tools to help you.
+
+These pages include a number of command examples demonstrating various
+debugging and analysis techniques, mostly for Linux/Unix systems. If you
+don't have access to the machines running Cassandra, or are running on
+Windows or another operating system you may not be able to use the exact
+commands but there are likely equivalent tools you can use.
+
+* xref:troubleshooting/finding_nodes.adoc[Finding nodes] 
+* xref:troubleshooting/reading_logs.adoc[Reading logs] 
+* xref:troubleshooting/use_nodetool.adoc[Using nodetool] 
+* xref:troubleshooting/use_tools.adoc[Using tools]
diff --git a/doc/modules/cassandra/pages/troubleshooting/reading_logs.adoc b/doc/modules/cassandra/pages/troubleshooting/reading_logs.adoc
new file mode 100644
index 0000000..1736896
--- /dev/null
+++ b/doc/modules/cassandra/pages/troubleshooting/reading_logs.adoc
@@ -0,0 +1,247 @@
+= Cassandra Logs
+
+Cassandra has rich support for logging and attempts to give operators
+maximum insight into the database while at the same time limiting noise
+to the logs.
+
+== Common Log Files
+
+Cassandra has three main logs, the `system.log`, `debug.log` and
+`gc.log` which hold general logging messages, debugging logging
+messages, and java garbage collection logs respectively.
+
+These logs by default live in `$CASSANDRA_HOME/logs`, but most Linux
+distributions relocate logs to `/var/log/cassandra`. Operators can tune
+this location as well as what levels are logged using the provided
+`logback.xml` file.
+
+=== `system.log`
+
+This log is the default Cassandra log and is a good place to start any
+investigation. Some examples of activities logged to this log:
+
+* Uncaught exceptions. These can be very useful for debugging errors.
+* `GCInspector` messages indicating long garbage collector pauses. When
+long pauses happen Cassandra will print how long and also what was the
+state of the system (thread state) at the time of that pause. This can
+help narrow down a capacity issue (either not enough heap or not enough
+spare CPU).
+* Information about nodes joining and leaving the cluster as well as
+token metadata (data ownersip) changes. This is useful for debugging
+network partitions, data movements, and more.
+* Keyspace/Table creation, modification, deletion.
+* `StartupChecks` that ensure optimal configuration of the operating
+system to run Cassandra
+* Information about some background operational tasks (e.g. Index
+Redistribution).
+
+As with any application, looking for `ERROR` or `WARN` lines can be a
+great first step:
+
+[source, bash]
+----
+$ # Search for warnings or errors in the latest system.log
+$ grep 'WARN\|ERROR' system.log | tail
+...
+
+$ # Search for warnings or errors in all rotated system.log
+$ zgrep 'WARN\|ERROR' system.log.* | less
+...
+----
+
+=== `debug.log`
+
+This log contains additional debugging information that may be useful
+when troubleshooting but may be much noiser than the normal
+`system.log`. Some examples of activities logged to this log:
+
+* Information about compactions, including when they start, which
+sstables they contain, and when they finish.
+* Information about memtable flushes to disk, including when they
+happened, how large the flushes were, and which commitlog segments the
+flush impacted.
+
+This log can be _very_ noisy, so it is highly recommended to use `grep`
+and other log analysis tools to dive deep. For example:
+
+[source, bash]
+----
+# Search for messages involving a CompactionTask with 5 lines of context
+$ grep CompactionTask debug.log -C 5 
+
+# Look at the distribution of flush tasks per keyspace
+$ grep "Enqueuing flush" debug.log | cut -f 10 -d ' ' | sort | uniq -c
+    6 compaction_history:
+    1 test_keyspace:
+    2 local:
+    17 size_estimates:
+    17 sstable_activity:
+----
+
+=== `gc.log`
+
+The gc log is a standard Java GC log. With the default `jvm.options`
+settings you get a lot of valuable information in this log such as
+application pause times, and why pauses happened. This may help narrow
+down throughput or latency issues to a mistuned JVM. For example you can
+view the last few pauses:
+
+[source, bash]
+----
+$ grep stopped gc.log.0.current | tail
+2018-08-29T00:19:39.522+0000: 3022663.591: Total time for which application threads were stopped: 0.0332813 seconds, Stopping threads took: 0.0008189 seconds
+2018-08-29T00:19:44.369+0000: 3022668.438: Total time for which application threads were stopped: 0.0312507 seconds, Stopping threads took: 0.0007025 seconds
+2018-08-29T00:19:49.796+0000: 3022673.865: Total time for which application threads were stopped: 0.0307071 seconds, Stopping threads took: 0.0006662 seconds
+2018-08-29T00:19:55.452+0000: 3022679.521: Total time for which application threads were stopped: 0.0309578 seconds, Stopping threads took: 0.0006832 seconds
+2018-08-29T00:20:00.127+0000: 3022684.197: Total time for which application threads were stopped: 0.0310082 seconds, Stopping threads took: 0.0007090 seconds
+2018-08-29T00:20:06.583+0000: 3022690.653: Total time for which application threads were stopped: 0.0317346 seconds, Stopping threads took: 0.0007106 seconds
+2018-08-29T00:20:10.079+0000: 3022694.148: Total time for which application threads were stopped: 0.0299036 seconds, Stopping threads took: 0.0006889 seconds
+2018-08-29T00:20:15.739+0000: 3022699.809: Total time for which application threads were stopped: 0.0078283 seconds, Stopping threads took: 0.0006012 seconds
+2018-08-29T00:20:15.770+0000: 3022699.839: Total time for which application threads were stopped: 0.0301285 seconds, Stopping threads took: 0.0003789 seconds
+2018-08-29T00:20:15.798+0000: 3022699.867: Total time for which application threads were stopped: 0.0279407 seconds, Stopping threads took: 0.0003627 seconds
+----
+
+This shows a lot of valuable information including how long the
+application was paused (meaning zero user queries were being serviced
+during the e.g. 33ms JVM pause) as well as how long it took to enter the
+safepoint. You can use this raw data to e.g. get the longest pauses:
+
+[source, bash]
+----
+$ grep stopped gc.log.0.current | cut -f 11 -d ' ' | sort -n  | tail | xargs -IX grep X gc.log.0.current | sort -k 1
+2018-08-28T17:13:40.520-0700: 1.193: Total time for which application threads were stopped: 0.0157914 seconds, Stopping threads took: 0.0000355 seconds
+2018-08-28T17:13:41.206-0700: 1.879: Total time for which application threads were stopped: 0.0249811 seconds, Stopping threads took: 0.0000318 seconds
+2018-08-28T17:13:41.638-0700: 2.311: Total time for which application threads were stopped: 0.0561130 seconds, Stopping threads took: 0.0000328 seconds
+2018-08-28T17:13:41.677-0700: 2.350: Total time for which application threads were stopped: 0.0362129 seconds, Stopping threads took: 0.0000597 seconds
+2018-08-28T17:13:41.781-0700: 2.454: Total time for which application threads were stopped: 0.0442846 seconds, Stopping threads took: 0.0000238 seconds
+2018-08-28T17:13:41.976-0700: 2.649: Total time for which application threads were stopped: 0.0377115 seconds, Stopping threads took: 0.0000250 seconds
+2018-08-28T17:13:42.172-0700: 2.845: Total time for which application threads were stopped: 0.0475415 seconds, Stopping threads took: 0.0001018 seconds
+2018-08-28T17:13:42.825-0700: 3.498: Total time for which application threads were stopped: 0.0379155 seconds, Stopping threads took: 0.0000571 seconds
+2018-08-28T17:13:43.574-0700: 4.247: Total time for which application threads were stopped: 0.0323812 seconds, Stopping threads took: 0.0000574 seconds
+2018-08-28T17:13:44.602-0700: 5.275: Total time for which application threads were stopped: 0.0238975 seconds, Stopping threads took: 0.0000788 seconds
+----
+
+In this case any client waiting on a query would have experienced a
+56ms latency at 17:13:41.
+
+Note that GC pauses are not _link:[only] garbage collection, although
+generally speaking high pauses with fast safepoints indicate a lack of
+JVM heap or mistuned JVM GC algorithm. High pauses with slow safepoints
+typically indicate that the JVM is having trouble entering a safepoint
+which usually indicates slow disk drives (Cassandra makes heavy use of
+memory mapped reads which the JVM doesn't know could have disk latency,
+so the JVM safepoint logic doesn't handle a blocking memory mapped read
+particularly well).
+
+Using these logs you can even get a pause distribution with something
+like
+https://github.com/bitly/data_hacks/blob/master/data_hacks/histogram.py[histogram.py]:
+
+[source, bash]
+----
+$ grep stopped gc.log.0.current | cut -f 11 -d ' ' | sort -n | histogram.py
+# NumSamples = 410293; Min = 0.00; Max = 11.49
+# Mean = 0.035346; Variance = 0.002216; SD = 0.047078; Median 0.036498
+# each ∎ represents a count of 5470
+    0.0001 -     1.1496 [410255]: ∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
+    1.1496 -     2.2991 [    15]:
+    2.2991 -     3.4486 [     5]:
+    3.4486 -     4.5981 [     1]:
+    4.5981 -     5.7475 [     5]:
+    5.7475 -     6.8970 [     9]:
+    6.8970 -     8.0465 [     1]:
+    8.0465 -     9.1960 [     0]:
+    9.1960 -    10.3455 [     0]:
+   10.3455 -    11.4949 [     2]:
+----
+
+We can see in this case while we have very good average performance
+something is causing multi second JVM pauses ... In this case it was
+mostly safepoint pauses caused by slow disks:
+
+[source, bash]
+----
+$ grep stopped gc.log.0.current | cut -f 11 -d ' ' | sort -n | tail | xargs -IX grep X  gc.log.0.current| sort -k 1
+2018-07-27T04:52:27.413+0000: 187831.482: Total time for which application threads were stopped: 6.5037022 seconds, Stopping threads took: 0.0005212 seconds
+2018-07-30T23:38:18.354+0000: 514582.423: Total time for which application threads were stopped: 6.3262938 seconds, Stopping threads took: 0.0004882 seconds
+2018-08-01T02:37:48.380+0000: 611752.450: Total time for which application threads were stopped: 10.3879659 seconds, Stopping threads took: 0.0004475 seconds
+2018-08-06T22:04:14.990+0000: 1113739.059: Total time for which application threads were stopped: 6.0917409 seconds, Stopping threads took: 0.0005553 seconds
+2018-08-14T00:04:06.091+0000: 1725730.160: Total time for which application threads were stopped: 6.0141054 seconds, Stopping threads took: 0.0004976 seconds
+2018-08-17T06:23:06.755+0000: 2007670.824: Total time for which application threads were stopped: 6.0133694 seconds, Stopping threads took: 0.0006011 seconds
+2018-08-23T06:35:46.068+0000: 2526830.137: Total time for which application threads were stopped: 6.4767751 seconds, Stopping threads took: 6.4426849 seconds
+2018-08-23T06:36:29.018+0000: 2526873.087: Total time for which application threads were stopped: 11.4949489 seconds, Stopping threads took: 11.4638297 seconds
+2018-08-23T06:37:12.671+0000: 2526916.741: Total time for which application threads were stopped: 6.3867003 seconds, Stopping threads took: 6.3507166 seconds
+2018-08-23T06:37:47.156+0000: 2526951.225: Total time for which application threads were stopped: 7.9528200 seconds, Stopping threads took: 7.9197756 seconds
+----
+
+Sometimes reading and understanding java GC logs is hard, but you can
+take the raw GC files and visualize them using tools such as
+https://github.com/chewiebug/GCViewer[GCViewer] which take the Cassandra
+GC log as input and show you detailed visual information on your garbage
+collection performance. This includes pause analysis as well as
+throughput information. For a stable Cassandra JVM you probably want to
+aim for pauses less than 200ms and GC throughput greater
+than 99%.
+
+Java GC pauses are one of the leading causes of tail latency in
+Cassandra (along with drive latency) so sometimes this information can
+be crucial while debugging tail latency issues.
+
+== Getting More Information
+
+If the default logging levels are insuficient, `nodetool` can set higher
+or lower logging levels for various packages and classes using the
+`nodetool setlogginglevel` command. Start by viewing the current levels:
+
+[source, bash]
+----
+$ nodetool getlogginglevels
+
+Logger Name                                        Log Level
+ROOT                                                    INFO
+org.apache.cassandra                                   DEBUG
+----
+
+Perhaps the `Gossiper` is acting up and we wish to enable it at `TRACE`
+level for even more insight:
+
+[source, bash]
+----
+$ nodetool setlogginglevel org.apache.cassandra.gms.Gossiper TRACE
+
+$ nodetool getlogginglevels
+
+Logger Name                                        Log Level
+ROOT                                                    INFO
+org.apache.cassandra                                   DEBUG
+org.apache.cassandra.gms.Gossiper                      TRACE
+
+$ grep TRACE debug.log | tail -2
+TRACE [GossipStage:1] 2018-07-04 17:07:47,879 Gossiper.java:1234 - Updating
+heartbeat state version to 2344 from 2343 for 127.0.0.2:7000 ...
+TRACE [GossipStage:1] 2018-07-04 17:07:47,879 Gossiper.java:923 - local
+heartbeat version 2341 greater than 2340 for 127.0.0.1:7000
+----
+
+Note that any changes made this way are reverted on next Cassandra
+process restart. To make the changes permanent add the appropriate rule
+to `logback.xml`.
+
+[source,diff]
+----
+diff --git a/conf/logback.xml b/conf/logback.xml
+index b2c5b10..71b0a49 100644
+--- a/conf/logback.xml
++++ b/conf/logback.xml
+@@ -98,4 +98,5 @@ appender reference in the root level section below.
+   </root>
+
+   <logger name="org.apache.cassandra" level="DEBUG"/>
++  <logger name="org.apache.cassandra.gms.Gossiper" level="TRACE"/>
+ </configuration>
+----
+
+
+Note that if you want more information than this tool provides, there
+are other live capture options available such as
+xref:cql/troubleshooting/use_tools.adoc#packet-capture[`packet-capture`].
diff --git a/doc/modules/cassandra/pages/troubleshooting/use_nodetool.adoc b/doc/modules/cassandra/pages/troubleshooting/use_nodetool.adoc
new file mode 100644
index 0000000..f80d039
--- /dev/null
+++ b/doc/modules/cassandra/pages/troubleshooting/use_nodetool.adoc
@@ -0,0 +1,242 @@
+= Use Nodetool
+
+Cassandra's `nodetool` allows you to narrow problems from the cluster
+down to a particular node and gives a lot of insight into the state of
+the Cassandra process itself. There are dozens of useful commands (see
+`nodetool help` for all the commands), but briefly some of the most
+useful for troubleshooting:
+
+[[nodetool-status]]
+== Cluster Status
+
+You can use `nodetool status` to assess status of the cluster:
+
+[source, bash]
+----
+$ nodetool status <optional keyspace>
+
+Datacenter: dc1
+=======================
+Status=Up/Down
+|/ State=Normal/Leaving/Joining/Moving
+--  Address    Load       Tokens       Owns (effective)  Host ID                               Rack
+UN  127.0.1.1  4.69 GiB   1            100.0%            35ea8c9f-b7a2-40a7-b9c5-0ee8b91fdd0e  r1
+UN  127.0.1.2  4.71 GiB   1            100.0%            752e278f-b7c5-4f58-974b-9328455af73f  r2
+UN  127.0.1.3  4.69 GiB   1            100.0%            9dc1a293-2cc0-40fa-a6fd-9e6054da04a7  r3
+----
+
+In this case we can see that we have three nodes in one datacenter with
+about 4.6GB of data each and they are all "up". The up/down status of a
+node is independently determined by every node in the cluster, so you
+may have to run `nodetool status` on multiple nodes in a cluster to see
+the full view.
+
+You can use `nodetool status` plus a little grep to see which nodes are
+down:
+
+[source, bash]
+----
+$ nodetool status | grep -v '^UN'
+Datacenter: dc1
+===============
+Status=Up/Down
+|/ State=Normal/Leaving/Joining/Moving
+--  Address    Load       Tokens       Owns (effective)  Host ID                               Rack
+Datacenter: dc2
+===============
+Status=Up/Down
+|/ State=Normal/Leaving/Joining/Moving
+--  Address    Load       Tokens       Owns (effective)  Host ID                               Rack
+DN  127.0.0.5  105.73 KiB  1            33.3%             df303ac7-61de-46e9-ac79-6e630115fd75  r1
+----
+
+In this case there are two datacenters and there is one node down in
+datacenter `dc2` and rack `r1`. This may indicate an issue on
+`127.0.0.5` warranting investigation.
+
+[[nodetool-proxyhistograms]]
+== Coordinator Query Latency
+
+You can view latency distributions of coordinator read and write latency
+to help narrow down latency issues using `nodetool proxyhistograms`:
+
+[source, bash]
+----
+$ nodetool proxyhistograms
+Percentile       Read Latency      Write Latency      Range Latency   CAS Read Latency  CAS Write Latency View Write Latency
+                     (micros)           (micros)           (micros)           (micros)           (micros)           (micros)
+50%                    454.83             219.34               0.00               0.00               0.00               0.00
+75%                    545.79             263.21               0.00               0.00               0.00               0.00
+95%                    654.95             315.85               0.00               0.00               0.00               0.00
+98%                    785.94             379.02               0.00               0.00               0.00               0.00
+99%                   3379.39            2346.80               0.00               0.00               0.00               0.00
+Min                     42.51             105.78               0.00               0.00               0.00               0.00
+Max                  25109.16           43388.63               0.00               0.00               0.00               0.00
+----
+
+Here you can see the full latency distribution of reads, writes, range
+requests (e.g. `select * from keyspace.table`), CAS read (compare phase
+of CAS) and CAS write (set phase of compare and set). These can be
+useful for narrowing down high level latency problems, for example in
+this case if a client had a 20 millisecond timeout on their reads they
+might experience the occasional timeout from this node but less than 1%
+(since the 99% read latency is 3.3 milliseconds < 20 milliseconds).
+
+[[nodetool-tablehistograms]]
+== Local Query Latency
+
+If you know which table is having latency/error issues, you can use
+`nodetool tablehistograms` to get a better idea of what is happening
+locally on a node:
+
+[source, bash]
+----
+$ nodetool tablehistograms keyspace table
+Percentile  SSTables     Write Latency      Read Latency    Partition Size        Cell Count
+                              (micros)          (micros)           (bytes)
+50%             0.00             73.46            182.79             17084               103
+75%             1.00             88.15            315.85             17084               103
+95%             2.00            126.93            545.79             17084               103
+98%             2.00            152.32            654.95             17084               103
+99%             2.00            182.79            785.94             17084               103
+Min             0.00             42.51             24.60             14238                87
+Max             2.00          12108.97          17436.92             17084               103
+----
+
+This shows you percentile breakdowns particularly critical metrics.
+
+The first column contains how many sstables were read per logical read.
+A very high number here indicates that you may have chosen the wrong
+compaction strategy, e.g. `SizeTieredCompactionStrategy` typically has
+many more reads per read than `LeveledCompactionStrategy` does for
+update heavy workloads.
+
+The second column shows you a latency breakdown of _local_ write
+latency. In this case we see that while the p50 is quite good at 73
+microseconds, the maximum latency is quite slow at 12 milliseconds. High
+write max latencies often indicate a slow commitlog volume (slow to
+fsync) or large writes that quickly saturate commitlog segments.
+
+The third column shows you a latency breakdown of _local_ read latency.
+We can see that local Cassandra reads are (as expected) slower than
+local writes, and the read speed correlates highly with the number of
+sstables read per read.
+
+The fourth and fifth columns show distributions of partition size and
+column count per partition. These are useful for determining if the
+table has on average skinny or wide partitions and can help you isolate
+bad data patterns. For example if you have a single cell that is 2
+megabytes, that is probably going to cause some heap pressure when it's
+read.
+
+[[nodetool-tpstats]]
+== Threadpool State
+
+You can use `nodetool tpstats` to view the current outstanding requests
+on a particular node. This is useful for trying to find out which
+resource (read threads, write threads, compaction, request response
+threads) the Cassandra process lacks. For example:
+
+[source, bash]
+----
+$ nodetool tpstats
+Pool Name                         Active   Pending      Completed   Blocked  All time blocked
+ReadStage                              2         0             12         0                 0
+MiscStage                              0         0              0         0                 0
+CompactionExecutor                     0         0           1940         0                 0
+MutationStage                          0         0              0         0                 0
+GossipStage                            0         0          10293         0                 0
+Repair-Task                            0         0              0         0                 0
+RequestResponseStage                   0         0             16         0                 0
+ReadRepairStage                        0         0              0         0                 0
+CounterMutationStage                   0         0              0         0                 0
+MemtablePostFlush                      0         0             83         0                 0
+ValidationExecutor                     0         0              0         0                 0
+MemtableFlushWriter                    0         0             30         0                 0
+ViewMutationStage                      0         0              0         0                 0
+CacheCleanupExecutor                   0         0              0         0                 0
+MemtableReclaimMemory                  0         0             30         0                 0
+PendingRangeCalculator                 0         0             11         0                 0
+SecondaryIndexManagement               0         0              0         0                 0
+HintsDispatcher                        0         0              0         0                 0
+Native-Transport-Requests              0         0            192         0                 0
+MigrationStage                         0         0             14         0                 0
+PerDiskMemtableFlushWriter_0           0         0             30         0                 0
+Sampler                                0         0              0         0                 0
+ViewBuildExecutor                      0         0              0         0                 0
+InternalResponseStage                  0         0              0         0                 0
+AntiEntropyStage                       0         0              0         0                 0
+
+Message type           Dropped                  Latency waiting in queue (micros)
+                                             50%               95%               99%               Max
+READ                         0               N/A               N/A               N/A               N/A
+RANGE_SLICE                  0              0.00              0.00              0.00              0.00
+_TRACE                       0               N/A               N/A               N/A               N/A
+HINT                         0               N/A               N/A               N/A               N/A
+MUTATION                     0               N/A               N/A               N/A               N/A
+COUNTER_MUTATION             0               N/A               N/A               N/A               N/A
+BATCH_STORE                  0               N/A               N/A               N/A               N/A
+BATCH_REMOVE                 0               N/A               N/A               N/A               N/A
+REQUEST_RESPONSE             0              0.00              0.00              0.00              0.00
+PAGED_RANGE                  0               N/A               N/A               N/A               N/A
+READ_REPAIR                  0               N/A               N/A               N/A               N/A
+----
+
+This command shows you all kinds of interesting statistics. The first
+section shows a detailed breakdown of threadpools for each Cassandra
+stage, including how many threads are current executing (Active) and how
+many are waiting to run (Pending). Typically if you see pending
+executions in a particular threadpool that indicates a problem localized
+to that type of operation. For example if the `RequestResponseState`
+queue is backing up, that means that the coordinators are waiting on a
+lot of downstream replica requests and may indicate a lack of token
+awareness, or very high consistency levels being used on read requests
+(for example reading at `ALL` ties up RF `RequestResponseState` threads
+whereas `LOCAL_ONE` only uses a single thread in the `ReadStage`
+threadpool). On the other hand if you see a lot of pending compactions
+that may indicate that your compaction threads cannot keep up with the
+volume of writes and you may need to tune either the compaction strategy
+or the `concurrent_compactors` or `compaction_throughput` options.
+
+The second section shows drops (errors) and latency distributions for
+all the major request types. Drops are cumulative since process start,
+but if you have any that indicate a serious problem as the default
+timeouts to qualify as a drop are quite high (~5-10 seconds). Dropped
+messages often warrants further investigation.
+
+[[nodetool-compactionstats]]
+== Compaction State
+
+As Cassandra is a LSM datastore, Cassandra sometimes has to compact
+sstables together, which can have adverse effects on performance. In
+particular, compaction uses a reasonable quantity of CPU resources,
+invalidates large quantities of the OS
+https://en.wikipedia.org/wiki/Page_cache[page cache], and can put a lot
+of load on your disk drives. There are great `os tools <os-iostat>` to
+determine if this is the case, but often it's a good idea to check if
+compactions are even running using `nodetool compactionstats`:
+
+[source, bash]
+----
+$ nodetool compactionstats
+pending tasks: 2
+- keyspace.table: 2
+
+id                                   compaction type keyspace table completed total    unit  progress
+2062b290-7f3a-11e8-9358-cd941b956e60 Compaction      keyspace table 21848273  97867583 bytes 22.32%
+Active compaction remaining time :   0h00m04s
+----
+
+In this case there is a single compaction running on the
+`keyspace.table` table, has completed 21.8 megabytes of 97 and Cassandra
+estimates (based on the configured compaction throughput) that this will
+take 4 seconds. You can also pass `-H` to get the units in a human
+readable format.
+
+Generally each running compaction can consume a single core, but the
+more you do in parallel the faster data compacts. Compaction is crucial
+to ensuring good read performance so having the right balance of
+concurrent compactions such that compactions complete quickly but don't
+take too many resources away from query threads is very important for
+performance. If you notice compaction unable to keep up, try tuning
+Cassandra's `concurrent_compactors` or `compaction_throughput` options.
diff --git a/doc/modules/cassandra/pages/troubleshooting/use_tools.adoc b/doc/modules/cassandra/pages/troubleshooting/use_tools.adoc
new file mode 100644
index 0000000..b9ec42a
--- /dev/null
+++ b/doc/modules/cassandra/pages/troubleshooting/use_tools.adoc
@@ -0,0 +1,578 @@
+= Diving Deep, Use External Tools
+
+Machine access allows operators to dive even deeper than logs and
+`nodetool` allow. While every Cassandra operator may have their personal
+favorite toolsets for troubleshooting issues, this page contains some of
+the most common operator techniques and examples of those tools. Many of
+these commands work only on Linux, but if you are deploying on a
+different operating system you may have access to other substantially
+similar tools that assess similar OS level metrics and processes.
+
+== JVM Tooling
+
+The JVM ships with a number of useful tools. Some of them are useful for
+debugging Cassandra issues, especially related to heap and execution
+stacks.
+
+*NOTE*: There are two common gotchas with JVM tooling and Cassandra:
+
+[arabic]
+. By default Cassandra ships with `-XX:+PerfDisableSharedMem` set to
+prevent long pauses (see `CASSANDRA-9242` and `CASSANDRA-9483` for
+details). If you want to use JVM tooling you can instead have `/tmp`
+mounted on an in memory `tmpfs` which also effectively works around
+`CASSANDRA-9242`.
+. Make sure you run the tools as the same user as Cassandra is running
+as, e.g. if the database is running as `cassandra` the tool also has to
+be run as `cassandra`, e.g. via `sudo -u cassandra <cmd>`.
+
+=== Garbage Collection State (jstat)
+
+If you suspect heap pressure you can use `jstat` to dive deep into the
+garbage collection state of a Cassandra process. This command is always
+safe to run and yields detailed heap information including eden heap
+usage (E), old generation heap usage (O), count of eden collections
+(YGC), time spend in eden collections (YGCT), old/mixed generation
+collections (FGC) and time spent in old/mixed generation collections
+(FGCT):
+
+[source, bash]
+----
+jstat -gcutil <cassandra pid> 500ms
+ S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
+ 0.00   0.00  81.53  31.16  93.07  88.20     12    0.151     3    0.257    0.408
+ 0.00   0.00  82.36  31.16  93.07  88.20     12    0.151     3    0.257    0.408
+ 0.00   0.00  82.36  31.16  93.07  88.20     12    0.151     3    0.257    0.408
+ 0.00   0.00  83.19  31.16  93.07  88.20     12    0.151     3    0.257    0.408
+ 0.00   0.00  83.19  31.16  93.07  88.20     12    0.151     3    0.257    0.408
+ 0.00   0.00  84.19  31.16  93.07  88.20     12    0.151     3    0.257    0.408
+ 0.00   0.00  84.19  31.16  93.07  88.20     12    0.151     3    0.257    0.408
+ 0.00   0.00  85.03  31.16  93.07  88.20     12    0.151     3    0.257    0.408
+ 0.00   0.00  85.03  31.16  93.07  88.20     12    0.151     3    0.257    0.408
+ 0.00   0.00  85.94  31.16  93.07  88.20     12    0.151     3    0.257    0.408
+----
+
+In this case we see we have a relatively healthy heap profile, with
+31.16% old generation heap usage and 83% eden. If the old generation
+routinely is above 75% then you probably need more heap (assuming CMS
+with a 75% occupancy threshold). If you do have such persistently high
+old gen that often means you either have under-provisioned the old
+generation heap, or that there is too much live data on heap for
+Cassandra to collect (e.g. because of memtables). Another thing to watch
+for is time between young garbage collections (YGC), which indicate how
+frequently the eden heap is collected. Each young gc pause is about
+20-50ms, so if you have a lot of them your clients will notice in their
+high percentile latencies.
+
+=== Thread Information (jstack)
+
+To get a point in time snapshot of exactly what Cassandra is doing, run
+`jstack` against the Cassandra PID. *Note* that this does pause the JVM
+for a very brief period (<20ms).:
+
+[source, bash]
+----
+$ jstack <cassandra pid> > threaddump
+
+# display the threaddump
+$ cat threaddump
+
+# look at runnable threads
+$grep RUNNABLE threaddump -B 1
+"Attach Listener" #15 daemon prio=9 os_prio=0 tid=0x00007f829c001000 nid=0x3a74 waiting on condition [0x0000000000000000]
+   java.lang.Thread.State: RUNNABLE
+--
+"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x00007f82e800e000 nid=0x2a19 waiting on condition [0x0000000000000000]
+   java.lang.Thread.State: RUNNABLE
+--
+"JPS thread pool" #10 prio=5 os_prio=0 tid=0x00007f82e84d0800 nid=0x2a2c runnable [0x00007f82d0856000]
+   java.lang.Thread.State: RUNNABLE
+--
+"Service Thread" #9 daemon prio=9 os_prio=0 tid=0x00007f82e80d7000 nid=0x2a2a runnable [0x0000000000000000]
+   java.lang.Thread.State: RUNNABLE
+--
+"C1 CompilerThread3" #8 daemon prio=9 os_prio=0 tid=0x00007f82e80cc000 nid=0x2a29 waiting on condition [0x0000000000000000]
+   java.lang.Thread.State: RUNNABLE
+--
+
+# Note that the nid is the Linux thread id
+----
+
+Some of the most important information in the threaddumps are
+waiting/blocking threads, including what locks or monitors the thread is
+blocking/waiting on.
+
+== Basic OS Tooling
+
+A great place to start when debugging a Cassandra issue is understanding
+how Cassandra is interacting with system resources. The following are
+all resources that Cassandra makes heavy uses of:
+
+* CPU cores. For executing concurrent user queries
+* CPU processing time. For query activity (data decompression, row
+merging, etc.)
+* CPU processing time (low priority). For background tasks (compaction,
+streaming, etc ...)
+* RAM for Java Heap. Used to hold internal data-structures and by
+default the Cassandra memtables. Heap space is a crucial component of
+write performance as well as generally.
+* RAM for OS disk cache. Used to cache frequently accessed SSTable
+blocks. OS disk cache is a crucial component of read performance.
+* Disks. Cassandra cares a lot about disk read latency, disk write
+throughput, and of course disk space.
+* Network latency. Cassandra makes many internode requests, so network
+latency between nodes can directly impact performance.
+* Network throughput. Cassandra (as other databases) frequently have the
+so called "incast" problem where a small request (e.g.
+`SELECT * from foo.bar`) returns a massively large result set (e.g. the
+entire dataset). In such situations outgoing bandwidth is crucial.
+
+Often troubleshooting Cassandra comes down to troubleshooting what
+resource the machine or cluster is running out of. Then you create more
+of that resource or change the query pattern to make less use of that
+resource.
+
+=== High Level Resource Usage (top/htop)
+
+Cassandra makes signifiant use of system resources, and often the very
+first useful action is to run `top` or `htop`
+(https://hisham.hm/htop/[website])to see the state of the machine.
+
+Useful things to look at:
+
+* System load levels. While these numbers can be confusing, generally
+speaking if the load average is greater than the number of CPU cores,
+Cassandra probably won't have very good (sub 100 millisecond) latencies.
+See
+http://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html[Linux
+Load Averages] for more information.
+* CPU utilization. `htop` in particular can help break down CPU
+utilization into `user` (low and normal priority), `system` (kernel),
+and `io-wait` . Cassandra query threads execute as normal priority
+`user` threads, while compaction threads execute as low priority `user`
+threads. High `system` time could indicate problems like thread
+contention, and high `io-wait` may indicate slow disk drives. This can
+help you understand what Cassandra is spending processing resources
+doing.
+* Memory usage. Look for which programs have the most resident memory,
+it is probably Cassandra. The number for Cassandra is likely
+inaccurately high due to how Linux (as of 2018) accounts for memory
+mapped file memory.
+
+[[os-iostat]]
+=== IO Usage (iostat)
+
+Use iostat to determine how data drives are faring, including latency
+distributions, throughput, and utilization:
+
+[source, bash]
+----
+$ sudo iostat -xdm 2
+Linux 4.13.0-13-generic (hostname)     07/03/2018     _x86_64_    (8 CPU)
+
+Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
+sda               0.00     0.28    0.32    5.42     0.01     0.13    48.55     0.01    2.21    0.26    2.32   0.64   0.37
+sdb               0.00     0.00    0.00    0.00     0.00     0.00    79.34     0.00    0.20    0.20    0.00   0.16   0.00
+sdc               0.34     0.27    0.76    0.36     0.01     0.02    47.56     0.03   26.90    2.98   77.73   9.21   1.03
+
+Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
+sda               0.00     0.00    2.00   32.00     0.01     4.04   244.24     0.54   16.00    0.00   17.00   1.06   3.60
+sdb               0.00     0.00    0.00    0.00     0.00     0.00     0.00     0.00    0.00    0.00    0.00   0.00   0.00
+sdc               0.00    24.50    0.00  114.00     0.00    11.62   208.70     5.56   48.79    0.00   48.79   1.12  12.80
+----
+
+In this case we can see that `/dev/sdc1` is a very slow drive, having an
+`await` close to 50 milliseconds and an `avgqu-sz` close to 5 ios. The
+drive is not particularly saturated (utilization is only 12.8%), but we
+should still be concerned about how this would affect our p99 latency
+since 50ms is quite long for typical Cassandra operations. That being
+said, in this case most of the latency is present in writes (typically
+writes are more latent than reads), which due to the LSM nature of
+Cassandra is often hidden from the user.
+
+Important metrics to assess using iostat:
+
+* Reads and writes per second. These numbers will change with the
+workload, but generally speaking the more reads Cassandra has to do from
+disk the slower Cassandra read latencies are. Large numbers of reads per
+second can be a dead giveaway that the cluster has insufficient memory
+for OS page caching.
+* Write throughput. Cassandra's LSM model defers user writes and batches
+them together, which means that throughput to the underlying medium is
+the most important write metric for Cassandra.
+* Read latency (`r_await`). When Cassandra missed the OS page cache and
+reads from SSTables, the read latency directly determines how fast
+Cassandra can respond with the data.
+* Write latency. Cassandra is less sensitive to write latency except
+when it syncs the commit log. This typically enters into the very high
+percentiles of write latency.
+
+Note that to get detailed latency breakdowns you will need a more
+advanced tool such as xref:use_tools.adoc#bcc-tools[`bcc-tools`].
+
+=== OS page Cache Usage
+
+As Cassandra makes heavy use of memory mapped files, the health of the
+operating system's https://en.wikipedia.org/wiki/Page_cache[Page Cache]
+is crucial to performance. Start by finding how much available cache is
+in the system:
+
+[source, bash]
+----
+$ free -g
+              total        used        free      shared  buff/cache   available
+Mem:             15           9           2           0           3           5
+Swap:             0           0           0
+----
+
+In this case 9GB of memory is used by user processes (Cassandra heap)
+and 8GB is available for OS page cache. Of that, 3GB is actually used to
+cache files. If most memory is used and unavailable to the page cache,
+Cassandra performance can suffer significantly. This is why Cassandra
+starts with a reasonably small amount of memory reserved for the heap.
+
+If you suspect that you are missing the OS page cache frequently you can
+use advanced tools like xref:use_tools.adoc#use-bcc-tools[cachestat] or
+xref:use_tools.adoc#use-vmtouch[vmtouch] to dive deeper.
+
+=== Network Latency and Reliability
+
+Whenever Cassandra does writes or reads that involve other replicas,
+`LOCAL_QUORUM` reads for example, one of the dominant effects on latency
+is network latency. When trying to debug issues with multi machine
+operations, the network can be an important resource to investigate. You
+can determine internode latency using tools like `ping` and `traceroute`
+or most effectively `mtr`:
+
+[source, bash]
+----
+$ mtr -nr www.google.com
+Start: Sun Jul 22 13:10:28 2018
+HOST: hostname                     Loss%   Snt   Last   Avg  Best  Wrst StDev
+  1.|-- 192.168.1.1                0.0%    10    2.0   1.9   1.1   3.7   0.7
+  2.|-- 96.123.29.15               0.0%    10   11.4  11.0   9.0  16.4   1.9
+  3.|-- 68.86.249.21               0.0%    10   10.6  10.7   9.0  13.7   1.1
+  4.|-- 162.141.78.129             0.0%    10   11.5  10.6   9.6  12.4   0.7
+  5.|-- 162.151.78.253             0.0%    10   10.9  12.1  10.4  20.2   2.8
+  6.|-- 68.86.143.93               0.0%    10   12.4  12.6   9.9  23.1   3.8
+  7.|-- 96.112.146.18              0.0%    10   11.9  12.4  10.6  15.5   1.6
+  9.|-- 209.85.252.250             0.0%    10   13.7  13.2  12.5  13.9   0.0
+ 10.|-- 108.170.242.238            0.0%    10   12.7  12.4  11.1  13.0   0.5
+ 11.|-- 74.125.253.149             0.0%    10   13.4  13.7  11.8  19.2   2.1
+ 12.|-- 216.239.62.40              0.0%    10   13.4  14.7  11.5  26.9   4.6
+ 13.|-- 108.170.242.81             0.0%    10   14.4  13.2  10.9  16.0   1.7
+ 14.|-- 72.14.239.43               0.0%    10   12.2  16.1  11.0  32.8   7.1
+ 15.|-- 216.58.195.68              0.0%    10   25.1  15.3  11.1  25.1   4.8
+----
+
+In this example of `mtr`, we can rapidly assess the path that your
+packets are taking, as well as what their typical loss and latency are.
+Packet loss typically leads to between `200ms` and `3s` of additional
+latency, so that can be a common cause of latency issues.
+
+=== Network Throughput
+
+As Cassandra is sensitive to outgoing bandwidth limitations, sometimes
+it is useful to determine if network throughput is limited. One handy
+tool to do this is
+https://www.systutorials.com/docs/linux/man/8-iftop/[iftop] which shows
+both bandwidth usage as well as connection information at a glance. An
+example showing traffic during a stress run against a local `ccm`
+cluster:
+
+[source, bash]
+----
+$ # remove the -t for ncurses instead of pure text
+$ sudo iftop -nNtP -i lo
+interface: lo
+IP address is: 127.0.0.1
+MAC address is: 00:00:00:00:00:00
+Listening on lo
+   # Host name (port/service if enabled)            last 2s   last 10s   last 40s cumulative
+--------------------------------------------------------------------------------------------
+   1 127.0.0.1:58946                          =>      869Kb      869Kb      869Kb      217KB
+     127.0.0.3:9042                           <=         0b         0b         0b         0B
+   2 127.0.0.1:54654                          =>      736Kb      736Kb      736Kb      184KB
+     127.0.0.1:9042                           <=         0b         0b         0b         0B
+   3 127.0.0.1:51186                          =>      669Kb      669Kb      669Kb      167KB
+     127.0.0.2:9042                           <=         0b         0b         0b         0B
+   4 127.0.0.3:9042                           =>     3.30Kb     3.30Kb     3.30Kb       845B
+     127.0.0.1:58946                          <=         0b         0b         0b         0B
+   5 127.0.0.1:9042                           =>     2.79Kb     2.79Kb     2.79Kb       715B
+     127.0.0.1:54654                          <=         0b         0b         0b         0B
+   6 127.0.0.2:9042                           =>     2.54Kb     2.54Kb     2.54Kb       650B
+     127.0.0.1:51186                          <=         0b         0b         0b         0B
+   7 127.0.0.1:36894                          =>     1.65Kb     1.65Kb     1.65Kb       423B
+     127.0.0.5:7000                           <=         0b         0b         0b         0B
+   8 127.0.0.1:38034                          =>     1.50Kb     1.50Kb     1.50Kb       385B
+     127.0.0.2:7000                           <=         0b         0b         0b         0B
+   9 127.0.0.1:56324                          =>     1.50Kb     1.50Kb     1.50Kb       383B
+     127.0.0.1:7000                           <=         0b         0b         0b         0B
+  10 127.0.0.1:53044                          =>     1.43Kb     1.43Kb     1.43Kb       366B
+     127.0.0.4:7000                           <=         0b         0b         0b         0B
+--------------------------------------------------------------------------------------------
+Total send rate:                                     2.25Mb     2.25Mb     2.25Mb
+Total receive rate:                                      0b         0b         0b
+Total send and receive rate:                         2.25Mb     2.25Mb     2.25Mb
+--------------------------------------------------------------------------------------------
+Peak rate (sent/received/total):                     2.25Mb         0b     2.25Mb
+Cumulative (sent/received/total):                     576KB         0B      576KB
+============================================================================================
+----
+
+In this case we can see that bandwidth is fairly shared between many
+peers, but if the total was getting close to the rated capacity of the
+NIC or was focussed on a single client, that may indicate a clue as to
+what issue is occurring.
+
+== Advanced tools
+
+Sometimes as an operator you may need to really dive deep. This is where
+advanced OS tooling can come in handy.
+
+[[use-bcc-tools]]
+=== bcc-tools
+
+Most modern Linux distributions (kernels newer than `4.1`) support
+https://github.com/iovisor/bcc[bcc-tools] for diving deep into
+performance problems. First install `bcc-tools`, e.g. via `apt` on
+Debian:
+
+[source, bash]
+----
+$ apt install bcc-tools
+----
+
+Then you can use all the tools that `bcc-tools` contains. One of the
+most useful tools is `cachestat`
+(https://github.com/iovisor/bcc/blob/master/tools/cachestat_example.txt[cachestat
+examples]) which allows you to determine exactly how many OS page cache
+hits and misses are happening:
+
+[source, bash]
+----
+$ sudo /usr/share/bcc/tools/cachestat -T 1
+TIME        TOTAL   MISSES     HITS  DIRTIES   BUFFERS_MB  CACHED_MB
+18:44:08       66       66        0       64           88       4427
+18:44:09       40       40        0       75           88       4427
+18:44:10     4353       45     4308      203           88       4427
+18:44:11       84       77        7       13           88       4428
+18:44:12     2511       14     2497       14           88       4428
+18:44:13      101       98        3       18           88       4428
+18:44:14    16741        0    16741       58           88       4428
+18:44:15     1935       36     1899       18           88       4428
+18:44:16       89       34       55       18           88       4428
+----
+
+In this case there are not too many page cache `MISSES` which indicates
+a reasonably sized cache. These metrics are the most direct measurement
+of your Cassandra node's "hot" dataset. If you don't have enough cache,
+`MISSES` will be high and performance will be slow. If you have enough
+cache, `MISSES` will be low and performance will be fast (as almost all
+reads are being served out of memory).
+
+You can also measure disk latency distributions using `biolatency`
+(https://github.com/iovisor/bcc/blob/master/tools/biolatency_example.txt[biolatency
+examples]) to get an idea of how slow Cassandra will be when reads miss
+the OS page Cache and have to hit disks:
+
+[source, bash]
+----
+$ sudo /usr/share/bcc/tools/biolatency -D 10
+Tracing block device I/O... Hit Ctrl-C to end.
+
+
+disk = 'sda'
+     usecs               : count     distribution
+         0 -> 1          : 0        |                                        |
+         2 -> 3          : 0        |                                        |
+         4 -> 7          : 0        |                                        |
+         8 -> 15         : 0        |                                        |
+        16 -> 31         : 12       |****************************************|
+        32 -> 63         : 9        |******************************          |
+        64 -> 127        : 1        |***                                     |
+       128 -> 255        : 3        |**********                              |
+       256 -> 511        : 7        |***********************                 |
+       512 -> 1023       : 2        |******                                  |
+
+disk = 'sdc'
+     usecs               : count     distribution
+         0 -> 1          : 0        |                                        |
+         2 -> 3          : 0        |                                        |
+         4 -> 7          : 0        |                                        |
+         8 -> 15         : 0        |                                        |
+        16 -> 31         : 0        |                                        |
+        32 -> 63         : 0        |                                        |
+        64 -> 127        : 41       |************                            |
+       128 -> 255        : 17       |*****                                   |
+       256 -> 511        : 13       |***                                     |
+       512 -> 1023       : 2        |                                        |
+      1024 -> 2047       : 0        |                                        |
+      2048 -> 4095       : 0        |                                        |
+      4096 -> 8191       : 56       |*****************                       |
+      8192 -> 16383      : 131      |****************************************|
+     16384 -> 32767      : 9        |**                                      |
+----
+
+In this case most ios on the data drive (`sdc`) are fast, but many take
+between 8 and 16 milliseconds.
+
+Finally `biosnoop`
+(https://github.com/iovisor/bcc/blob/master/tools/biosnoop_example.txt[examples])
+can be used to dive even deeper and see per IO latencies:
+
+[source, bash]
+----
+$ sudo /usr/share/bcc/tools/biosnoop | grep java | head
+0.000000000    java           17427  sdc     R  3972458600 4096      13.58
+0.000818000    java           17427  sdc     R  3972459408 4096       0.35
+0.007098000    java           17416  sdc     R  3972401824 4096       5.81
+0.007896000    java           17416  sdc     R  3972489960 4096       0.34
+0.008920000    java           17416  sdc     R  3972489896 4096       0.34
+0.009487000    java           17427  sdc     R  3972401880 4096       0.32
+0.010238000    java           17416  sdc     R  3972488368 4096       0.37
+0.010596000    java           17427  sdc     R  3972488376 4096       0.34
+0.011236000    java           17410  sdc     R  3972488424 4096       0.32
+0.011825000    java           17427  sdc     R  3972488576 16384      0.65
+... time passes
+8.032687000    java           18279  sdc     R  10899712  122880     3.01
+8.033175000    java           18279  sdc     R  10899952  8192       0.46
+8.073295000    java           18279  sdc     R  23384320  122880     3.01
+8.073768000    java           18279  sdc     R  23384560  8192       0.46
+----
+
+With `biosnoop` you see every single IO and how long they take. This
+data can be used to construct the latency distributions in `biolatency`
+but can also be used to better understand how disk latency affects
+performance. For example this particular drive takes ~3ms to service a
+memory mapped read due to the large default value (`128kb`) of
+`read_ahead_kb`. To improve point read performance you may may want to
+decrease `read_ahead_kb` on fast data volumes such as SSDs while keeping
+the a higher value like `128kb` value is probably right for HDs. There
+are tradeoffs involved, see
+https://www.kernel.org/doc/Documentation/block/queue-sysfs.txt[queue-sysfs]
+docs for more information, but regardless `biosnoop` is useful for
+understanding _how_ Cassandra uses drives.
+
+[[use-vmtouch]]
+=== vmtouch
+
+Sometimes it's useful to know how much of the Cassandra data files are
+being cached by the OS. A great tool for answering this question is
+https://github.com/hoytech/vmtouch[vmtouch].
+
+First install it:
+
+[source, bash]
+----
+$ git clone https://github.com/hoytech/vmtouch.git
+$ cd vmtouch
+$ make
+----
+
+Then run it on the Cassandra data directory:
+
+[source, bash]
+----
+$ ./vmtouch /var/lib/cassandra/data/
+           Files: 312
+     Directories: 92
+  Resident Pages: 62503/64308  244M/251M  97.2%
+         Elapsed: 0.005657 seconds
+----
+
+In this case almost the entire dataset is hot in OS page Cache.
+Generally speaking the percentage doesn't really matter unless reads are
+missing the cache (per e.g. xref:cql/troubleshooting/use_tools.adoc#use-bcc-tools[cachestat] in which case
+having additional memory may help read performance.
+
+=== CPU Flamegraphs
+
+Cassandra often uses a lot of CPU, but telling _what_ it is doing can
+prove difficult. One of the best ways to analyze Cassandra on CPU time
+is to use
+http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html[CPU
+Flamegraphs] which display in a useful way which areas of Cassandra code
+are using CPU. This may help narrow down a compaction problem to a
+"compaction problem dropping tombstones" or just generally help you
+narrow down what Cassandra is doing while it is having an issue. To get
+CPU flamegraphs follow the instructions for
+http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html#Java[Java
+Flamegraphs].
+
+Generally:
+
+[arabic]
+. Enable the `-XX:+PreserveFramePointer` option in Cassandra's
+`jvm.options` configuation file. This has a negligible performance
+impact but allows you actually see what Cassandra is doing.
+. Run `perf` to get some data.
+. Send that data through the relevant scripts in the FlameGraph toolset
+and convert the data into a pretty flamegraph. View the resulting SVG
+image in a browser or other image browser.
+
+For example just cloning straight off github we first install the
+`perf-map-agent` to the location of our JVMs (assumed to be
+`/usr/lib/jvm`):
+
+[source, bash]
+----
+$ sudo bash
+$ export JAVA_HOME=/usr/lib/jvm/java-8-oracle/
+$ cd /usr/lib/jvm
+$ git clone --depth=1 https://github.com/jvm-profiling-tools/perf-map-agent
+$ cd perf-map-agent
+$ cmake .
+$ make
+----
+
+Now to get a flamegraph:
+
+[source, bash]
+----
+$ git clone --depth=1 https://github.com/brendangregg/FlameGraph
+$ sudo bash
+$ cd FlameGraph
+$ # Record traces of Cassandra and map symbols for all java processes
+$ perf record -F 49 -a -g -p <CASSANDRA PID> -- sleep 30; ./jmaps
+$ # Translate the data
+$ perf script > cassandra_stacks
+$ cat cassandra_stacks | ./stackcollapse-perf.pl | grep -v cpu_idle | \
+    ./flamegraph.pl --color=java --hash > cassandra_flames.svg
+----
+
+The resulting SVG is searchable, zoomable, and generally easy to
+introspect using a browser.
+
+=== Packet Capture
+
+Sometimes you have to understand what queries a Cassandra node is
+performing _right now_ to troubleshoot an issue. For these times trusty
+packet capture tools like `tcpdump` and
+https://www.wireshark.org/[Wireshark] can be very helpful to dissect
+packet captures. Wireshark even has native
+https://www.wireshark.org/docs/dfref/c/cql.html[CQL support] although it
+sometimes has compatibility issues with newer Cassandra protocol
+releases.
+
+To get a packet capture first capture some packets:
+
+[source, bash]
+----
+$ sudo tcpdump -U -s0 -i <INTERFACE> -w cassandra.pcap -n "tcp port 9042"
+----
+
+Now open it up with wireshark:
+
+[source, bash]
+----
+$ wireshark cassandra.pcap
+----
+
+If you don't see CQL like statements try telling to decode as CQL by
+right clicking on a packet going to 9042 -> `Decode as` -> select CQL
+from the dropdown for port 9042.
+
+If you don't want to do this manually or use a GUI, you can also use
+something like https://github.com/jolynch/cqltrace[cqltrace] to ease
+obtaining and parsing CQL packet captures.
diff --git a/doc/modules/cassandra/partials/java_version.adoc b/doc/modules/cassandra/partials/java_version.adoc
new file mode 100644
index 0000000..dddc113
--- /dev/null
+++ b/doc/modules/cassandra/partials/java_version.adoc
@@ -0,0 +1,23 @@
+[arabic, start=1]
+. Verify the version of Java installed. For example:
+
+[{tabs}]
+====
+Command::
++
+--
+[source,shell]
+----
+include::example$BASH/java_verify.sh[]
+----
+--
+
+Result::
++
+--
+[source,plaintext]
+----
+include::example$RESULTS/java_verify.result[]
+----
+--
+====
diff --git a/doc/modules/cassandra/partials/nodetool_and_cqlsh.adoc b/doc/modules/cassandra/partials/nodetool_and_cqlsh.adoc
new file mode 100644
index 0000000..d1c4e73
--- /dev/null
+++ b/doc/modules/cassandra/partials/nodetool_and_cqlsh.adoc
@@ -0,0 +1,21 @@
+NOTE: For information on how to configure your installation, see
+{cass_url}doc/latest/getting_started/configuring.html[Configuring
+Cassandra].
+
+[arabic, start=7]
+. Check the status of Cassandra:
+
+[source,shell]
+----
+include::example$BASH/nodetool_status.sh[]
+----
+
+The status column in the output should report `UN` which stands for
+"Up/Normal".
+
+Alternatively, connect to the database with:
+
+[source,shell]
+----
+include::example$BASH/run_cqlsh.sh[]
+----
diff --git a/doc/modules/cassandra/partials/nodetool_and_cqlsh_nobin.adoc b/doc/modules/cassandra/partials/nodetool_and_cqlsh_nobin.adoc
new file mode 100644
index 0000000..c17949c
--- /dev/null
+++ b/doc/modules/cassandra/partials/nodetool_and_cqlsh_nobin.adoc
@@ -0,0 +1,21 @@
+NOTE: For information on how to configure your installation, see
+{cass_url}doc/latest/getting_started/configuring.html[Configuring
+Cassandra].
+
+[arabic, start=7]
+. Check the status of Cassandra:
+
+[source,shell]
+----
+include::example$BASH/nodetool_status_nobin.sh[]
+----
+
+The status column in the output should report `UN` which stands for
+"Up/Normal".
+
+Alternatively, connect to the database with:
+
+[source,shell]
+----
+include::example$BASH/run_cqlsh_nobin.sh[]
+----
diff --git a/doc/modules/cassandra/partials/package_versions.adoc b/doc/modules/cassandra/partials/package_versions.adoc
new file mode 100644
index 0000000..f5c8968
--- /dev/null
+++ b/doc/modules/cassandra/partials/package_versions.adoc
@@ -0,0 +1,5 @@
+The latest major version is {311_version} and the
+corresponding distribution name is `311x` (with an "x" as the suffix).
+For older releases use `30x` for {30_version}, `22x` for {22_version} and
+`21x` for {21_version}. 
+For example, to add the repository for version {311_version} (`311x`):
diff --git a/doc/modules/cassandra/partials/tail_syslog.adoc b/doc/modules/cassandra/partials/tail_syslog.adoc
new file mode 100644
index 0000000..b5dd8f3
--- /dev/null
+++ b/doc/modules/cassandra/partials/tail_syslog.adoc
@@ -0,0 +1,25 @@
+[arabic, start=6]
+. Monitor the progress of the startup with:
+
+[{tabs}]
+====
+Command::
++
+--
+[source,shell]
+----
+include::example$BASH/tail_syslog.sh[]
+----
+--
+
+Result::
++
+--
+Cassandra is ready when you see an entry like this in the `system.log`:
+
+[source,plaintext]
+----
+include::example$RESULTS/tail_syslog.result[]
+----
+--
+====
diff --git a/doc/native_protocol_v3.spec b/doc/native_protocol_v3.spec
index f53a537..8187ca0 100644
--- a/doc/native_protocol_v3.spec
+++ b/doc/native_protocol_v3.spec
@@ -996,8 +996,7 @@
                              - "BATCH_LOG": the timeout occured during the
                                write to the batch log when a (logged) batch
                                write was requested.
-                             - "CAS": the timeout occured during the Compare And Set
-                               write/update.
+                             - "CAS": the timeout occured during the Compare And Set write/update.
     0x1200    Read_timeout: Timeout exception during a read request. The rest
               of the ERROR message body will be
                 <cl><received><blockfor><data_present>
diff --git a/doc/native_protocol_v4.spec b/doc/native_protocol_v4.spec
index fae48649..2220000 100644
--- a/doc/native_protocol_v4.spec
+++ b/doc/native_protocol_v4.spec
@@ -1099,6 +1099,12 @@
                              - "BATCH_LOG": the timeout occurred during the
                                write to the batch log when a (logged) batch
                                write was requested.
+                             - "CAS": the timeout occured during the Compare And Set write/update.
+                             - "VIEW": the timeout occured when a write involves
+                                VIEW update and failure to acqiure local view(MV)
+                                lock for key within timeout
+                             - "CDC": the timeout occured when cdc_total_space_in_mb is
+                                exceeded when doing a write to data tracked by cdc.
     0x1200    Read_timeout: Timeout exception during a read request. The rest
               of the ERROR message body will be
                 <cl><received><blockfor><data_present>
@@ -1166,8 +1172,12 @@
                              - "BATCH_LOG": the failure occured during the
                                write to the batch log when a (logged) batch
                                write was requested.
-                             - "CAS": the timeout occured during the Compare And Set
-                               write/update.
+                             - "CAS": the failure occured during the Compare And Set write/update.
+                             - "VIEW": the failure occured when a write involves
+                                VIEW update and failure to acqiure local view(MV)
+                                lock for key within timeout
+                             - "CDC": the failure occured when cdc_total_space_in_mb is
+                                exceeded when doing a write to data tracked by cdc.
 
     0x2000    Syntax_error: The submitted query has a syntax error.
     0x2100    Unauthorized: The logged user doesn't have the right to perform
diff --git a/doc/native_protocol_v5.spec b/doc/native_protocol_v5.spec
new file mode 100644
index 0000000..69ad7c3
--- /dev/null
+++ b/doc/native_protocol_v5.spec
@@ -0,0 +1,1255 @@
+#
+# 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.
+#
+
+                             CQL BINARY PROTOCOL v5
+
+
+Table of Contents
+
+  1. Overview
+  2. Frame header
+    2.1. version
+    2.2. flags
+    2.3. stream
+    2.4. opcode
+    2.5. length
+  3. Notations
+  4. Messages
+    4.1. Requests
+      4.1.1. STARTUP
+      4.1.2. AUTH_RESPONSE
+      4.1.3. OPTIONS
+      4.1.4. QUERY
+      4.1.5. PREPARE
+      4.1.6. EXECUTE
+      4.1.7. BATCH
+      4.1.8. REGISTER
+    4.2. Responses
+      4.2.1. ERROR
+      4.2.2. READY
+      4.2.3. AUTHENTICATE
+      4.2.4. SUPPORTED
+      4.2.5. RESULT
+        4.2.5.1. Void
+        4.2.5.2. Rows
+        4.2.5.3. Set_keyspace
+        4.2.5.4. Prepared
+        4.2.5.5. Schema_change
+      4.2.6. EVENT
+      4.2.7. AUTH_CHALLENGE
+      4.2.8. AUTH_SUCCESS
+  5. Compression
+  6. Data Type Serialization Formats
+  7. User Defined Type Serialization
+  8. Result paging
+  9. Error codes
+  10. Changes from v4
+
+
+1. Overview
+
+  The CQL binary protocol is a frame based protocol. Frames are defined as:
+
+      0         8        16        24        32         40
+      +---------+---------+---------+---------+---------+
+      | version |  flags  |      stream       | opcode  |
+      +---------+---------+---------+---------+---------+
+      |                length                 |
+      +---------+---------+---------+---------+
+      |                                       |
+      .            ...  body ...              .
+      .                                       .
+      .                                       .
+      +----------------------------------------
+
+  The protocol is big-endian (network byte order).
+
+  Each frame contains a fixed size header (9 bytes) followed by a variable size
+  body. The header is described in Section 2. The content of the body depends
+  on the header opcode value (the body can in particular be empty for some
+  opcode values). The list of allowed opcodes is defined in Section 2.4 and the
+  details of each corresponding message are described Section 4.
+
+  The protocol distinguishes two types of frames: requests and responses. Requests
+  are those frames sent by the client to the server. Responses are those frames sent
+  by the server to the client. Note, however, that the protocol supports server pushes
+  (events) so a response does not necessarily come right after a client request.
+
+  Note to client implementors: client libraries should always assume that the
+  body of a given frame may contain more data than what is described in this
+  document. It will however always be safe to ignore the remainder of the frame
+  body in such cases. The reason is that this may enable extending the protocol
+  with optional features without needing to change the protocol version.
+
+
+
+2. Frame header
+
+2.1. version
+
+  The version is a single byte that indicates both the direction of the message
+  (request or response) and the version of the protocol in use. The most
+  significant bit of version is used to define the direction of the message:
+  0 indicates a request, 1 indicates a response. This can be useful for protocol
+  analyzers to distinguish the nature of the packet from the direction in which
+  it is moving. The rest of that byte is the protocol version (5 for the protocol
+  defined in this document). In other words, for this version of the protocol,
+  version will be one of:
+    0x04    Request frame for this protocol version
+    0x84    Response frame for this protocol version
+
+  Please note that while every message ships with the version, only one version
+  of messages is accepted on a given connection. In other words, the first message
+  exchanged (STARTUP) sets the version for the connection for the lifetime of this
+  connection. The single exception to this behavior is when a startup message
+  is sent with a version that is higher than the current server version. In this
+  case, the server will respond with its current version.
+
+  This document describes version 5 of the protocol. For the changes made since
+  version 4, see Section 10.
+
+
+2.2. flags
+
+  Flags applying to this frame. The flags have the following meaning (described
+  by the mask that allows selecting them):
+    0x01: Compression flag. If set, the frame body is compressed. The actual
+          compression to use should have been set up beforehand through the
+          Startup message (which thus cannot be compressed; Section 4.1.1).
+    0x02: Tracing flag. For a request frame, this indicates the client requires
+          tracing of the request. Note that only QUERY, PREPARE and EXECUTE queries
+          support tracing. Other requests will simply ignore the tracing flag if
+          set. If a request supports tracing and the tracing flag is set, the response
+          to this request will have the tracing flag set and contain tracing
+          information.
+          If a response frame has the tracing flag set, its body contains
+          a tracing ID. The tracing ID is a [uuid] and is the first thing in
+          the frame body.
+    0x04: Custom payload flag. For a request or response frame, this indicates
+          that a generic key-value custom payload for a custom QueryHandler
+          implementation is present in the frame. Such a custom payload is simply
+          ignored by the default QueryHandler implementation.
+          Currently, only QUERY, PREPARE, EXECUTE and BATCH requests support
+          payload.
+          Type of custom payload is [bytes map] (see below). If either or both
+          of the tracing and warning flags are set, the custom payload will follow
+          those indicated elements in the frame body. If neither are set, the custom
+          payload will be the first value in the frame body.
+    0x08: Warning flag. The response contains warnings which were generated by the
+          server to go along with this response.
+          If a response frame has the warning flag set, its body will contain the
+          text of the warnings. The warnings are a [string list] and will be the
+          first value in the frame body if the tracing flag is not set, or directly
+          after the tracing ID if it is.
+    0x10: Use beta flag. Indicates that the client opts in to use protocol version
+          that is currently in beta. Server will respond with ERROR if protocol
+          version is marked as beta on server and client does not provide this flag.
+
+  The rest of flags is currently unused and ignored.
+
+2.3. stream
+
+  A frame has a stream id (a [short] value). When sending request messages, this
+  stream id must be set by the client to a non-negative value (negative stream id
+  are reserved for streams initiated by the server; currently all EVENT messages
+  (section 4.2.6) have a streamId of -1). If a client sends a request message
+  with the stream id X, it is guaranteed that the stream id of the response to
+  that message will be X.
+
+  This helps to enable the asynchronous nature of the protocol. If a client
+  sends multiple messages simultaneously (without waiting for responses), there
+  is no guarantee on the order of the responses. For instance, if the client
+  writes REQ_1, REQ_2, REQ_3 on the wire (in that order), the server might
+  respond to REQ_3 (or REQ_2) first. Assigning different stream ids to these 3
+  requests allows the client to distinguish to which request a received answer
+  responds to. As there can only be 32768 different simultaneous streams, it is up
+  to the client to reuse stream id.
+
+  Note that clients are free to use the protocol synchronously (i.e. wait for
+  the response to REQ_N before sending REQ_N+1). In that case, the stream id
+  can be safely set to 0. Clients should also feel free to use only a subset of
+  the 32768 maximum possible stream ids if it is simpler for its implementation.
+
+2.4. opcode
+
+  An integer byte that distinguishes the actual message:
+    0x00    ERROR
+    0x01    STARTUP
+    0x02    READY
+    0x03    AUTHENTICATE
+    0x05    OPTIONS
+    0x06    SUPPORTED
+    0x07    QUERY
+    0x08    RESULT
+    0x09    PREPARE
+    0x0A    EXECUTE
+    0x0B    REGISTER
+    0x0C    EVENT
+    0x0D    BATCH
+    0x0E    AUTH_CHALLENGE
+    0x0F    AUTH_RESPONSE
+    0x10    AUTH_SUCCESS
+
+  Messages are described in Section 4.
+
+  (Note that there is no 0x04 message in this version of the protocol)
+
+
+2.5. length
+
+  A 4 byte integer representing the length of the body of the frame (note:
+  currently a frame is limited to 256MB in length).
+
+
+3. Notations
+
+  To describe the layout of the frame body for the messages in Section 4, we
+  define the following:
+
+    [int]             A 4 bytes integer
+    [long]            A 8 bytes integer
+    [byte]            A 1 byte unsigned integer
+    [short]           A 2 bytes unsigned integer
+    [string]          A [short] n, followed by n bytes representing an UTF-8
+                      string.
+    [long string]     An [int] n, followed by n bytes representing an UTF-8 string.
+    [uuid]            A 16 bytes long uuid.
+    [string list]     A [short] n, followed by n [string].
+    [bytes]           A [int] n, followed by n bytes if n >= 0. If n < 0,
+                      no byte should follow and the value represented is `null`.
+    [value]           A [int] n, followed by n bytes if n >= 0.
+                      If n == -1 no byte should follow and the value represented is `null`.
+                      If n == -2 no byte should follow and the value represented is
+                      `not set` not resulting in any change to the existing value.
+                      n < -2 is an invalid value and results in an error.
+    [short bytes]     A [short] n, followed by n bytes if n >= 0.
+
+    [unsigned vint]   An unsigned variable length integer. A vint is encoded with the most significant byte (MSB) first.
+                      The most significant byte will contains the information about how many extra bytes need to be read
+                      as well as the most significant bits of the integer.
+                      The number of extra bytes to read is encoded as 1 bits on the left side.
+                      For example, if we need to read 2 more bytes the first byte will start with 110
+                      (e.g. 256 000 will be encoded on 3 bytes as [110]00011 11101000 00000000)
+                      If the encoded integer is 8 bytes long the vint will be encoded on 9 bytes and the first
+                      byte will be: 11111111
+
+   [vint]             A signed variable length integer. This is encoded using zig-zag encoding and then sent
+                      like an [unsigned vint]. Zig-zag encoding converts numbers as follows:
+                      0 = 0, -1 = 1, 1 = 2, -2 = 3, 2 = 4, -3 = 5, 3 = 6 and so forth.
+                      The purpose is to send small negative values as small unsigned values, so that we save bytes on the wire.
+                      To encode a value n use "(n >> 31) ^ (n << 1)" for 32 bit values, and "(n >> 63) ^ (n << 1)"
+                      for 64 bit values where "^" is the xor operation, "<<" is the left shift operation and ">>" is
+                      the arithemtic right shift operation (highest-order bit is replicated).
+                      Decode with "(n >> 1) ^ -(n & 1)".
+
+    [option]          A pair of <id><value> where <id> is a [short] representing
+                      the option id and <value> depends on that option (and can be
+                      of size 0). The supported id (and the corresponding <value>)
+                      will be described when this is used.
+    [option list]     A [short] n, followed by n [option].
+    [inet]            An address (ip and port) to a node. It consists of one
+                      [byte] n, that represents the address size, followed by n
+                      [byte] representing the IP address (in practice n can only be
+                      either 4 (IPv4) or 16 (IPv6)), following by one [int]
+                      representing the port.
+    [inetaddr]        An IP address (without a port) to a node. It consists of one
+                      [byte] n, that represents the address size, followed by n
+                      [byte] representing the IP address.
+    [consistency]     A consistency level specification. This is a [short]
+                      representing a consistency level with the following
+                      correspondance:
+                        0x0000    ANY
+                        0x0001    ONE
+                        0x0002    TWO
+                        0x0003    THREE
+                        0x0004    QUORUM
+                        0x0005    ALL
+                        0x0006    LOCAL_QUORUM
+                        0x0007    EACH_QUORUM
+                        0x0008    SERIAL
+                        0x0009    LOCAL_SERIAL
+                        0x000A    LOCAL_ONE
+
+    [string map]      A [short] n, followed by n pair <k><v> where <k> and <v>
+                      are [string].
+    [string multimap] A [short] n, followed by n pair <k><v> where <k> is a
+                      [string] and <v> is a [string list].
+    [bytes map]       A [short] n, followed by n pair <k><v> where <k> is a
+                      [string] and <v> is a [bytes].
+
+
+4. Messages
+
+  Dependant on the flags specified in the header, the layout of the message body must be:
+    [<tracing_id>][<warnings>][<custom_payload>]<message>
+  where:
+    - <tracing_id> is a UUID tracing ID, present if this is a request message and the Tracing flag is set.
+    - <warnings> is a string list of warnings (if this is a request message and the Warning flag is set.
+    - <custom_payload> is bytes map for the serialised custom payload present if this is one of the message types
+      which support custom payloads (QUERY, PREPARE, EXECUTE and BATCH) and the Custom payload flag is set.
+    - <message> as defined below through sections 4 and 5.
+
+4.1. Requests
+
+  Note that outside of their normal responses (described below), all requests
+  can get an ERROR message (Section 4.2.1) as response.
+
+4.1.1. STARTUP
+
+  Initialize the connection. The server will respond by either a READY message
+  (in which case the connection is ready for queries) or an AUTHENTICATE message
+  (in which case credentials will need to be provided using AUTH_RESPONSE).
+
+  This must be the first message of the connection, except for OPTIONS that can
+  be sent before to find out the options supported by the server. Once the
+  connection has been initialized, a client should not send any more STARTUP
+  messages.
+
+  The body is a [string map] of options. Possible options are:
+    - "CQL_VERSION": the version of CQL to use. This option is mandatory and
+      currently the only version supported is "3.0.0". Note that this is
+      different from the protocol version.
+    - "COMPRESSION": the compression algorithm to use for frames (See section 5).
+      This is optional; if not specified no compression will be used.
+
+
+4.1.2. AUTH_RESPONSE
+
+  Answers a server authentication challenge.
+
+  Authentication in the protocol is SASL based. The server sends authentication
+  challenges (a bytes token) to which the client answers with this message. Those
+  exchanges continue until the server accepts the authentication by sending a
+  AUTH_SUCCESS message after a client AUTH_RESPONSE. Note that the exchange
+  begins with the client sending an initial AUTH_RESPONSE in response to a
+  server AUTHENTICATE request.
+
+  The body of this message is a single [bytes] token. The details of what this
+  token contains (and when it can be null/empty, if ever) depends on the actual
+  authenticator used.
+
+  The response to a AUTH_RESPONSE is either a follow-up AUTH_CHALLENGE message,
+  an AUTH_SUCCESS message or an ERROR message.
+
+
+4.1.3. OPTIONS
+
+  Asks the server to return which STARTUP options are supported. The body of an
+  OPTIONS message should be empty and the server will respond with a SUPPORTED
+  message.
+
+
+4.1.4. QUERY
+
+  Performs a CQL query. The body of the message must be:
+    <query><query_parameters>
+  where <query> is a [long string] representing the query and
+  <query_parameters> must be
+    <consistency><flags>[<n>[name_1]<value_1>...[name_n]<value_n>][<result_page_size>][<paging_state>][<serial_consistency>][<timestamp>]
+  where:
+    - <consistency> is the [consistency] level for the operation.
+    - <flags> is a [int] whose bits define the options for this query and
+      in particular influence what the remainder of the message contains.
+      A flag is set if the bit corresponding to its `mask` is set. Supported
+      flags are, given their mask:
+        0x01: Values. If set, a [short] <n> followed by <n> [value]
+              values are provided. Those values are used for bound variables in
+              the query. Optionally, if the 0x40 flag is present, each value
+              will be preceded by a [string] name, representing the name of
+              the marker the value must be bound to.
+        0x02: Skip_metadata. If set, the Result Set returned as a response
+              to the query (if any) will have the NO_METADATA flag (see
+              Section 4.2.5.2).
+        0x04: Page_size. If set, <result_page_size> is an [int]
+              controlling the desired page size of the result (in CQL3 rows).
+              See the section on paging (Section 8) for more details.
+        0x08: With_paging_state. If set, <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). The query will be
+              executed but starting from a given paging state. This is also to
+              continue paging on a different node than the one where it
+              started (See Section 8 for more details).
+        0x10: With serial consistency. If set, <serial_consistency> should be
+              present. <serial_consistency> is the [consistency] level for the
+              serial phase of conditional updates. That consitency can only be
+              either SERIAL or LOCAL_SERIAL and if not present, it defaults to
+              SERIAL. This option will be ignored for anything else other than a
+              conditional update/insert.
+        0x20: With default timestamp. If set, <timestamp> should be present.
+              <timestamp> is a [long] representing the default timestamp for the query
+              in microseconds (negative values are forbidden). This will
+              replace the server side assigned timestamp as default timestamp.
+              Note that a timestamp in the query itself will still override
+              this timestamp. This is entirely optional.
+        0x40: With names for values. This only makes sense if the 0x01 flag is set and
+              is ignored otherwise. If present, the values from the 0x01 flag will
+              be preceded by a name (see above). Note that this is only useful for
+              QUERY requests where named bind markers are used; for EXECUTE statements,
+              since the names for the expected values was returned during preparation,
+              a client can always provide values in the right order without any names
+              and using this flag, while supported, is almost surely inefficient.
+
+  Note that the consistency is ignored by some queries (USE, CREATE, ALTER,
+  TRUNCATE, ...).
+
+  The server will respond to a QUERY message with a RESULT message, the content
+  of which depends on the query.
+
+
+4.1.5. PREPARE
+
+  Prepare a query for later execution (through EXECUTE). The body consists of
+  the CQL query to prepare as a [long string].
+
+  The server will respond with a RESULT message with a `prepared` kind (0x0004,
+  see Section 4.2.5).
+
+
+4.1.6. EXECUTE
+
+  Executes a prepared query. The body of the message must be:
+    <id><query_parameters>
+  where <id> is the prepared query ID. It's the [short bytes] returned as a
+  response to a PREPARE message. As for <query_parameters>, it has the exact
+  same definition as in QUERY (see Section 4.1.4).
+
+  The response from the server will be a RESULT message.
+
+
+4.1.7. BATCH
+
+  Allows executing a list of queries (prepared or not) as a batch (note that
+  only DML statements are accepted in a batch). The body of the message must
+  be:
+    <type><n><query_1>...<query_n><consistency><flags>[<serial_consistency>][<timestamp>]
+  where:
+    - <type> is a [byte] indicating the type of batch to use:
+        - If <type> == 0, the batch will be "logged". This is equivalent to a
+          normal CQL3 batch statement.
+        - If <type> == 1, the batch will be "unlogged".
+        - If <type> == 2, the batch will be a "counter" batch (and non-counter
+          statements will be rejected).
+    - <flags> is a [int] whose bits define the options for this query and
+      in particular influence what the remainder of the message contains. It is similar
+      to the <flags> from QUERY and EXECUTE methods, except that the 4 rightmost
+      bits must always be 0 as their corresponding options do not make sense for
+      Batch. A flag is set if the bit corresponding to its `mask` is set. Supported
+      flags are, given their mask:
+        0x10: With serial consistency. If set, <serial_consistency> should be
+              present. <serial_consistency> is the [consistency] level for the
+              serial phase of conditional updates. That consistency can only be
+              either SERIAL or LOCAL_SERIAL and if not present, it defaults to
+              SERIAL. This option will be ignored for anything else other than a
+              conditional update/insert.
+        0x20: With default timestamp. If set, <timestamp> should be present.
+              <timestamp> is a [long] representing the default timestamp for the query
+              in microseconds. This will replace the server side assigned
+              timestamp as default timestamp. Note that a timestamp in the query itself
+              will still override this timestamp. This is entirely optional.
+        0x40: With names for values. If set, then all values for all <query_i> must be
+              preceded by a [string] <name_i> that have the same meaning as in QUERY
+              requests [IMPORTANT NOTE: this feature does not work and should not be
+              used. It is specified in a way that makes it impossible for the server
+              to implement. This will be fixed in a future version of the native
+              protocol. See https://issues.apache.org/jira/browse/CASSANDRA-10246 for
+              more details].
+    - <n> is a [short] indicating the number of following queries.
+    - <query_1>...<query_n> are the queries to execute. A <query_i> must be of the
+      form:
+        <kind><string_or_id><n>[<name_1>]<value_1>...[<name_n>]<value_n>
+      where:
+       - <kind> is a [byte] indicating whether the following query is a prepared
+         one or not. <kind> value must be either 0 or 1.
+       - <string_or_id> depends on the value of <kind>. If <kind> == 0, it should be
+         a [long string] query string (as in QUERY, the query string might contain
+         bind markers). Otherwise (that is, if <kind> == 1), it should be a
+         [short bytes] representing a prepared query ID.
+       - <n> is a [short] indicating the number (possibly 0) of following values.
+       - <name_i> is the optional name of the following <value_i>. It must be present
+         if and only if the 0x40 flag is provided for the batch.
+       - <value_i> is the [value] to use for bound variable i (of bound variable <name_i>
+         if the 0x40 flag is used).
+    - <consistency> is the [consistency] level for the operation.
+    - <serial_consistency> is only present if the 0x10 flag is set. In that case,
+      <serial_consistency> is the [consistency] level for the serial phase of
+      conditional updates. That consitency can only be either SERIAL or
+      LOCAL_SERIAL and if not present will defaults to SERIAL. This option will
+      be ignored for anything else other than a conditional update/insert.
+
+  The server will respond with a RESULT message.
+
+
+4.1.8. REGISTER
+
+  Register this connection to receive some types of events. The body of the
+  message is a [string list] representing the event types to register for. See
+  section 4.2.6 for the list of valid event types.
+
+  The response to a REGISTER message will be a READY message.
+
+  Please note that if a client driver maintains multiple connections to a
+  Cassandra node and/or connections to multiple nodes, it is advised to
+  dedicate a handful of connections to receive events, but to *not* register
+  for events on all connections, as this would only result in receiving
+  multiple times the same event messages, wasting bandwidth.
+
+
+4.2. Responses
+
+  This section describes the content of the frame body for the different
+  responses. Please note that to make room for future evolution, clients should
+  support extra informations (that they should simply discard) to the one
+  described in this document at the end of the frame body.
+
+4.2.1. ERROR
+
+  Indicates an error processing a request. The body of the message will be an
+  error code ([int]) followed by a [string] error message. Then, depending on
+  the exception, more content may follow. The error codes are defined in
+  Section 9, along with their additional content if any.
+
+
+4.2.2. READY
+
+  Indicates that the server is ready to process queries. This message will be
+  sent by the server either after a STARTUP message if no authentication is
+  required (if authentication is required, the server indicates readiness by
+  sending a AUTH_RESPONSE message).
+
+  The body of a READY message is empty.
+
+
+4.2.3. AUTHENTICATE
+
+  Indicates that the server requires authentication, and which authentication
+  mechanism to use.
+
+  The authentication is SASL based and thus consists of a number of server
+  challenges (AUTH_CHALLENGE, Section 4.2.7) followed by client responses
+  (AUTH_RESPONSE, Section 4.1.2). The initial exchange is however boostrapped
+  by an initial client response. The details of that exchange (including how
+  many challenge-response pairs are required) are specific to the authenticator
+  in use. The exchange ends when the server sends an AUTH_SUCCESS message or
+  an ERROR message.
+
+  This message will be sent following a STARTUP message if authentication is
+  required and must be answered by a AUTH_RESPONSE message from the client.
+
+  The body consists of a single [string] indicating the full class name of the
+  IAuthenticator in use.
+
+
+4.2.4. SUPPORTED
+
+  Indicates which startup options are supported by the server. This message
+  comes as a response to an OPTIONS message.
+
+  The body of a SUPPORTED message is a [string multimap]. This multimap gives
+  for each of the supported STARTUP options, the list of supported values. It
+  also includes:
+      - "PROTOCOL_VERSIONS": the list of native protocol versions that are
+      supported, encoded as the version number followed by a slash and the
+      version description. For example: 3/v3, 4/v4, 5/v5-beta. If a version is
+      in beta, it will have the word "beta" in its description.
+
+
+4.2.5. RESULT
+
+  The result to a query (QUERY, PREPARE, EXECUTE or BATCH messages).
+
+  The first element of the body of a RESULT message is an [int] representing the
+  `kind` of result. The rest of the body depends on the kind. The kind can be
+  one of:
+    0x0001    Void: for results carrying no information.
+    0x0002    Rows: for results to select queries, returning a set of rows.
+    0x0003    Set_keyspace: the result to a `use` query.
+    0x0004    Prepared: result to a PREPARE message.
+    0x0005    Schema_change: the result to a schema altering query.
+
+  The body for each kind (after the [int] kind) is defined below.
+
+
+4.2.5.1. Void
+
+  The rest of the body for a Void result is empty. It indicates that a query was
+  successful without providing more information.
+
+
+4.2.5.2. Rows
+
+  Indicates a set of rows. The rest of the body of a Rows result is:
+    <metadata><rows_count><rows_content>
+  where:
+    - <metadata> is composed of:
+        <flags><columns_count>[<paging_state>][<global_table_spec>?<col_spec_1>...<col_spec_n>]
+      where:
+        - <flags> is an [int]. The bits of <flags> provides information on the
+          formatting of the remaining information. A flag is set if the bit
+          corresponding to its `mask` is set. Supported flags are, given their
+          mask:
+            0x0001    Global_tables_spec: if set, only one table spec (keyspace
+                      and table name) is provided as <global_table_spec>. If not
+                      set, <global_table_spec> is not present.
+            0x0002    Has_more_pages: indicates whether this is not the last
+                      page of results and more should be retrieved. If set, the
+                      <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 remainder of the result for
+                      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 flag) but
+                      no other information (so no <global_table_spec> nor <col_spec_i>).
+                      This will only ever be the case if this was requested
+                      during the query (see QUERY and RESULT messages).
+        - <columns_count> is an [int] representing the number of columns selected
+          by the query that produced this result. It defines the number of <col_spec_i>
+          elements in and the number of elements for each row in <rows_content>.
+        - <global_table_spec> is present if the Global_tables_spec is set in
+          <flags>. It is composed of two [string] representing the
+          (unique) keyspace name and table name the columns belong to.
+        - <col_spec_i> specifies the columns returned in the query. There are
+          <column_count> such column specifications that are composed of:
+            (<ksname><tablename>)?<name><type>
+          The initial <ksname> and <tablename> are two [string] and are only present
+          if the Global_tables_spec flag is not set. The <column_name> is a
+          [string] and <type> is an [option] that corresponds to the description
+          (what this description is depends a bit on the context: in results to
+          selects, this will be either the user chosen alias or the selection used
+          (often a colum name, but it can be a function call too). In results to
+          a PREPARE, this will be either the name of the corresponding bind variable
+          or the column name for the variable if it is "anonymous") and type of
+          the corresponding result. The option for <type> is either a native
+          type (see below), in which case the option has no value, or a
+          'custom' type, in which case the value is a [string] representing
+          the fully qualified class name of the type represented. Valid option
+          ids are:
+            0x0000    Custom: the value is a [string], see above.
+            0x0001    Ascii
+            0x0002    Bigint
+            0x0003    Blob
+            0x0004    Boolean
+            0x0005    Counter
+            0x0006    Decimal
+            0x0007    Double
+            0x0008    Float
+            0x0009    Int
+            0x000B    Timestamp
+            0x000C    Uuid
+            0x000D    Varchar
+            0x000E    Varint
+            0x000F    Timeuuid
+            0x0010    Inet
+            0x0011    Date
+            0x0012    Time
+            0x0013    Smallint
+            0x0014    Tinyint
+            0x0015    Duration
+            0x0020    List: the value is an [option], representing the type
+                            of the elements of the list.
+            0x0021    Map: the value is two [option], representing the types of the
+                           keys and values of the map
+            0x0022    Set: the value is an [option], representing the type
+                            of the elements of the set
+            0x0030    UDT: the value is <ks><udt_name><n><name_1><type_1>...<name_n><type_n>
+                           where:
+                              - <ks> is a [string] representing the keyspace name this
+                                UDT is part of.
+                              - <udt_name> is a [string] representing the UDT name.
+                              - <n> is a [short] representing the number of fields of
+                                the UDT, and thus the number of <name_i><type_i> pairs
+                                following
+                              - <name_i> is a [string] representing the name of the
+                                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 values 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.
+    - <rows_content> is composed of <row_1>...<row_m> where m is <rows_count>.
+      Each <row_i> is composed of <value_1>...<value_n> where n is
+      <columns_count> and where <value_j> is a [bytes] representing the value
+      returned for the jth column of the ith row. In other words, <rows_content>
+      is composed of (<rows_count> * <columns_count>) [bytes].
+
+
+4.2.5.3. Set_keyspace
+
+  The result to a `use` query. The body (after the kind [int]) is a single
+  [string] indicating the name of the keyspace that has been set.
+
+
+4.2.5.4. Prepared
+
+  The result to a PREPARE message. The body of a Prepared result is:
+    <id><metadata><result_metadata>
+  where:
+    - <id> is [short bytes] representing the prepared query ID.
+    - <metadata> is composed of:
+        <flags><columns_count><pk_count>[<pk_index_1>...<pk_index_n>][<global_table_spec>?<col_spec_1>...<col_spec_n>]
+      where:
+        - <flags> is an [int]. The bits of <flags> provides information on the
+          formatting of the remaining information. A flag is set if the bit
+          corresponding to its `mask` is set. Supported masks and their flags
+          are:
+            0x0001    Global_tables_spec: if set, only one table spec (keyspace
+                      and table name) is provided as <global_table_spec>. If not
+                      set, <global_table_spec> is not present.
+        - <columns_count> is an [int] representing the number of bind markers
+          in the prepared statement.  It defines the number of <col_spec_i>
+          elements.
+        - <pk_count> is an [int] representing the number of <pk_index_i>
+          elements to follow. If this value is zero, at least one of the
+          partition key columns in the table that the statement acts on
+          did not have a corresponding bind marker (or the bind marker
+          was wrapped in a function call).
+        - <pk_index_i> is a short that represents the index of the bind marker
+          that corresponds to the partition key column in position i.
+          For example, a <pk_index> sequence of [2, 0, 1] indicates that the
+          table has three partition key columns; the full partition key
+          can be constructed by creating a composite of the values for
+          the bind markers at index 2, at index 0, and at index 1.
+          This allows implementations with token-aware routing to correctly
+          construct the partition key without needing to inspect table
+          metadata.
+        - <global_table_spec> is present if the Global_tables_spec is set in
+          <flags>. If present, it is composed of two [string]s. The first
+          [string] is the name of the keyspace that the statement acts on.
+          The second [string] is the name of the table that the columns
+          represented by the bind markers belong to.
+        - <col_spec_i> specifies the bind markers in the prepared statement.
+          There are <column_count> such column specifications, each with the
+          following format:
+            (<ksname><tablename>)?<name><type>
+          The initial <ksname> and <tablename> are two [string] that are only
+          present if the Global_tables_spec flag is not set. The <name> field
+          is a [string] that holds the name of the bind marker (if named),
+          or the name of the column, field, or expression that the bind marker
+          corresponds to (if the bind marker is "anonymous").  The <type>
+          field is an [option] that represents the expected type of values for
+          the bind marker.  See the Rows documentation (section 4.2.5.2) for
+          full details on the <type> field.
+
+    - <result_metadata> is defined exactly the same as <metadata> in the Rows
+      documentation (section 4.2.5.2).  This describes the metadata for the
+      result set that will be returned when this prepared statement is executed.
+      Note that <result_metadata> may be empty (have the No_metadata flag and
+      0 columns, See section 4.2.5.2) and will be for any query that is not a
+      Select. In fact, there is never a guarantee that this will be non-empty, so
+      implementations should protect themselves accordingly. This result metadata
+      is an optimization that allows implementations to later execute the
+      prepared statement without requesting the metadata (see the Skip_metadata
+      flag in EXECUTE).  Clients can safely discard this metadata if they do not
+      want to take advantage of that optimization.
+
+  Note that the prepared query ID returned is global to the node on which the query
+  has been prepared. It can be used on any connection to that node
+  until the node is restarted (after which the query must be reprepared).
+
+4.2.5.5. Schema_change
+
+  The result to a schema altering query (creation/update/drop of a
+  keyspace/table/index). The body (after the kind [int]) is the same
+  as the body for a "SCHEMA_CHANGE" event, so 3 strings:
+    <change_type><target><options>
+  Please refer to section 4.2.6 below for the meaning of those fields.
+
+  Note that a query to create or drop an index is considered to be a change
+  to the table the index is on.
+
+
+4.2.6. EVENT
+
+  An event pushed by the server. A client will only receive events for the
+  types it has REGISTERed to. The body of an EVENT message will start with a
+  [string] representing the event type. The rest of the message depends on the
+  event type. The valid event types are:
+    - "TOPOLOGY_CHANGE": events related to change in the cluster topology.
+      Currently, events are sent when new nodes are added to the cluster, and
+      when nodes are removed. The body of the message (after the event type)
+      consists of a [string] and an [inet], corresponding respectively to the
+      type of change ("NEW_NODE" or "REMOVED_NODE") followed by the address of
+      the new/removed node.
+    - "STATUS_CHANGE": events related to change of node status. Currently,
+      up/down events are sent. The body of the message (after the event type)
+      consists of a [string] and an [inet], corresponding respectively to the
+      type of status change ("UP" or "DOWN") followed by the address of the
+      concerned node.
+    - "SCHEMA_CHANGE": events related to schema change. After the event type,
+      the rest of the message will be <change_type><target><options> where:
+        - <change_type> is a [string] representing the type of changed involved.
+          It will be one of "CREATED", "UPDATED" or "DROPPED".
+        - <target> is a [string] that can be one of "KEYSPACE", "TABLE", "TYPE",
+          "FUNCTION" or "AGGREGATE" and describes what has been modified
+          ("TYPE" stands for modifications related to user types, "FUNCTION"
+          for modifications related to user defined functions, "AGGREGATE"
+          for modifications related to user defined aggregates).
+        - <options> depends on the preceding <target>:
+          - If <target> is "KEYSPACE", then <options> will be a single [string]
+            representing the keyspace changed.
+          - If <target> is "TABLE" or "TYPE", then
+            <options> will be 2 [string]: the first one will be the keyspace
+            containing the affected object, and the second one will be the name
+            of said affected object (either the table, user type, function, or
+            aggregate name).
+          - If <target> is "FUNCTION" or "AGGREGATE", multiple arguments follow:
+            - [string] keyspace containing the user defined function / aggregate
+            - [string] the function/aggregate name
+            - [string list] one string for each argument type (as CQL type)
+
+  All EVENT messages have a streamId of -1 (Section 2.3).
+
+  Please note that "NEW_NODE" and "UP" events are sent based on internal Gossip
+  communication and as such may be sent a short delay before the binary
+  protocol server on the newly up node is fully started. Clients are thus
+  advised to wait a short time before trying to connect to the node (1 second
+  should be enough), otherwise they may experience a connection refusal at
+  first.
+
+4.2.7. AUTH_CHALLENGE
+
+  A server authentication challenge (see AUTH_RESPONSE (Section 4.1.2) for more
+  details).
+
+  The body of this message is a single [bytes] token. The details of what this
+  token contains (and when it can be null/empty, if ever) depends on the actual
+  authenticator used.
+
+  Clients are expected to answer the server challenge with an AUTH_RESPONSE
+  message.
+
+4.2.8. AUTH_SUCCESS
+
+  Indicates the success of the authentication phase. See Section 4.2.3 for more
+  details.
+
+  The body of this message is a single [bytes] token holding final information
+  from the server that the client may require to finish the authentication
+  process. What that token contains and whether it can be null depends on the
+  actual authenticator used.
+
+
+5. Compression
+
+  Frame compression is supported by the protocol, but then only the frame body
+  is compressed (the frame header should never be compressed).
+
+  Before being used, client and server must agree on a compression algorithm to
+  use, which is done in the STARTUP message. As a consequence, a STARTUP message
+  must never be compressed.  However, once the STARTUP frame has been received
+  by the server, messages can be compressed (including the response to the STARTUP
+  request). Frames do not have to be compressed, however, even if compression has
+  been agreed upon (a server may only compress frames above a certain size at its
+  discretion). A frame body should be compressed if and only if the compressed
+  flag (see Section 2.2) is set.
+
+  As of version 2 of the protocol, the following compressions are available:
+    - lz4 (https://code.google.com/p/lz4/). In that, note that the first four bytes
+      of the body will be the uncompressed length (followed by the compressed
+      bytes).
+    - snappy (https://code.google.com/p/snappy/). This compression might not be
+      available as it depends on a native lib (server-side) that might not be
+      avaivable on some installations.
+
+
+6. Data Type Serialization Formats
+
+  This sections describes the serialization formats for all CQL data types
+  supported by Cassandra through the native protocol.  These serialization
+  formats should be used by client drivers to encode values for EXECUTE
+  messages.  Cassandra will use these formats when returning values in
+  RESULT messages.
+
+  All values are represented as [bytes] in EXECUTE and RESULT messages.
+  The [bytes] format includes an int prefix denoting the length of the value.
+  For that reason, the serialization formats described here will not include
+  a length component.
+
+  For legacy compatibility reasons, note that most non-string types support
+  "empty" values (i.e. a value with zero length).  An empty value is distinct
+  from NULL, which is encoded with a negative length.
+
+  As with the rest of the native protocol, all encodings are big-endian.
+
+6.1. ascii
+
+  A sequence of bytes in the ASCII range [0, 127].  Bytes with values outside of
+  this range will result in a validation error.
+
+6.2 bigint
+
+  An eight-byte two's complement integer.
+
+6.3 blob
+
+  Any sequence of bytes.
+
+6.4 boolean
+
+  A single byte.  A value of 0 denotes "false"; any other value denotes "true".
+  (However, it is recommended that a value of 1 be used to represent "true".)
+
+6.5 date
+
+  An unsigned integer representing days with epoch centered at 2^31.
+  (unix epoch January 1st, 1970).
+  A few examples:
+    0:    -5877641-06-23
+    2^31: 1970-1-1
+    2^32: 5881580-07-11
+
+6.6 decimal
+
+  The decimal format represents an arbitrary-precision number.  It contains an
+  [int] "scale" component followed by a varint encoding (see section 6.17)
+  of the unscaled value.  The encoded value represents "<unscaled>E<-scale>".
+  In other words, "<unscaled> * 10 ^ (-1 * <scale>)".
+
+6.7 double
+
+  An 8 byte floating point number in the IEEE 754 binary64 format.
+
+6.8 duration
+
+  A duration is composed of 3 signed variable length integers ([vint]s).
+  The first [vint] represents a number of months, the second [vint] represents
+  a number of days, and the last [vint] represents a number of nanoseconds.
+  The number of months and days must be valid 32 bits integers whereas the
+  number of nanoseconds must be a valid 64 bits integer.
+  A duration can either be positive or negative. If a duration is positive
+  all the integers must be positive or zero. If a duration is
+  negative all the numbers must be negative or zero.
+
+6.9 float
+
+  A 4 byte floating point number in the IEEE 754 binary32 format.
+
+6.10 inet
+
+  A 4 byte or 16 byte sequence denoting an IPv4 or IPv6 address, respectively.
+
+6.11 int
+
+  A 4 byte two's complement integer.
+
+6.12 list
+
+  A [int] n indicating the number of elements in the list, followed by n
+  elements.  Each element is [bytes] representing the serialized value.
+
+6.13 map
+
+  A [int] n indicating the number of key/value pairs in the map, followed by
+  n entries.  Each entry is composed of two [bytes] representing the key
+  and value.
+
+6.14 set
+
+  A [int] n indicating the number of elements in the set, followed by n
+  elements.  Each element is [bytes] representing the serialized value.
+
+6.15 smallint
+
+  A 2 byte two's complement integer.
+
+6.16 text
+
+  A sequence of bytes conforming to the UTF-8 specifications.
+
+6.17 time
+
+  An 8 byte two's complement long representing nanoseconds since midnight.
+  Valid values are in the range 0 to 86399999999999
+
+6.18 timestamp
+
+  An 8 byte two's complement integer representing a millisecond-precision
+  offset from the unix epoch (00:00:00, January 1st, 1970).  Negative values
+  represent a negative offset from the epoch.
+
+6.19 timeuuid
+
+  A 16 byte sequence representing a version 1 UUID as defined by RFC 4122.
+
+6.20 tinyint
+
+  A 1 byte two's complement integer.
+
+6.21 tuple
+
+  A sequence of [bytes] values representing the items in a tuple.  The encoding
+  of each element depends on the data type for that position in the tuple.
+  Null values may be represented by using length -1 for the [bytes]
+  representation of an element.
+
+6.22 uuid
+
+  A 16 byte sequence representing any valid UUID as defined by RFC 4122.
+
+6.23 varchar
+
+  An alias of the "text" type.
+
+6.24 varint
+
+  A variable-length two's complement encoding of a signed integer.
+
+  The following examples may help implementors of this spec:
+
+  Value | Encoding
+  ------|---------
+      0 |     0x00
+      1 |     0x01
+    127 |     0x7F
+    128 |   0x0080
+    129 |   0x0081
+     -1 |     0xFF
+   -128 |     0x80
+   -129 |   0xFF7F
+
+  Note that positive numbers must use a most-significant byte with a value
+  less than 0x80, because a most-significant bit of 1 indicates a negative
+  value.  Implementors should pad positive values that have a MSB >= 0x80
+  with a leading 0x00 byte.
+
+
+7. User Defined Types
+
+  This section describes the serialization format for User defined types (UDT),
+  as described 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.
+
+
+8. Result paging
+
+  The protocol allows for paging the result of queries. For that, the QUERY and
+  EXECUTE messages have a <result_page_size> value that indicate the desired
+  page size in CQL3 rows.
+
+  If a positive value is provided for <result_page_size>, the result set of the
+  RESULT message returned for the query will contain at most the
+  <result_page_size> first rows of the query result. If that first page of results
+  contains the full result set for the query, the RESULT message (of kind `Rows`)
+  will have the Has_more_pages flag *not* set. However, if some results are not
+  part of the first response, the Has_more_pages flag will be set and the result
+  will contain a <paging_state> value. In that case, the <paging_state> value
+  should be used in a QUERY or EXECUTE message (that has the *same* query as
+  the original one or the behavior is undefined) to retrieve the next page of
+  results.
+
+  Only CQL3 queries that return a result set (RESULT message with a Rows `kind`)
+  support paging. For other type of queries, the <result_page_size> value is
+  ignored.
+
+  Note to client implementors:
+  - While <result_page_size> can be as low as 1, it will likely be detrimental
+    to performance to pick a value too low. A value below 100 is probably too
+    low for most use cases.
+  - Clients should not rely on the actual size of the result set returned to
+    decide if there are more results to fetch or not. Instead, they should always
+    check the Has_more_pages flag (unless they did not enable paging for the query
+    obviously). Clients should also not assert that no result will have more than
+    <result_page_size> results. While the current implementation always respects
+    the exact value of <result_page_size>, we reserve the right to return
+    slightly smaller or bigger pages in the future for performance reasons.
+  - The <paging_state> is specific to a protocol version and drivers should not
+    send a <paging_state> returned by a node using the protocol v3 to query a node
+    using the protocol v4 for instance.
+
+
+9. Error codes
+
+  Let us recall that an ERROR message is composed of <code><message>[...]
+  (see 4.2.1 for details). The supported error codes, as well as any additional
+  information the message may contain after the <message> are described below:
+    0x0000    Server error: something unexpected happened. This indicates a
+              server-side bug.
+    0x000A    Protocol error: some client message triggered a protocol
+              violation (for instance a QUERY message is sent before a STARTUP
+              one has been sent)
+    0x0100    Authentication error: authentication was required and failed. The
+              possible reason for failing depends on the authenticator in use,
+              which may or may not include more detail in the accompanying
+              error message.
+    0x1000    Unavailable exception. The rest of the ERROR message body will be
+                <cl><required><alive>
+              where:
+                <cl> is the [consistency] level of the query that triggered
+                     the exception.
+                <required> is an [int] representing the number of nodes that
+                           should be alive to respect <cl>
+                <alive> is an [int] representing the number of replicas that
+                        were known to be alive when the request had been
+                        processed (since an unavailable exception has been
+                        triggered, there will be <alive> < <required>)
+    0x1001    Overloaded: the request cannot be processed because the
+              coordinator node is overloaded
+    0x1002    Is_bootstrapping: the request was a read request but the
+              coordinator node is bootstrapping
+    0x1003    Truncate_error: error during a truncation error.
+    0x1100    Write_timeout: Timeout exception during a write request. The rest
+              of the ERROR message body will be
+                <cl><received><blockfor><writeType>
+              where:
+                <cl> is the [consistency] level of the query having triggered
+                     the exception.
+                <received> is an [int] representing the number of nodes having
+                           acknowledged the request.
+                <blockfor> is an [int] representing the number of replicas whose
+                           acknowledgement is required to achieve <cl>.
+                <writeType> is a [string] that describe the type of the write
+                            that timed out. The value of that string can be one
+                            of:
+                             - "SIMPLE": the write was a non-batched
+                               non-counter write.
+                             - "BATCH": the write was a (logged) batch write.
+                               If this type is received, it means the batch log
+                               has been successfully written (otherwise a
+                               "BATCH_LOG" type would have been sent instead).
+                             - "UNLOGGED_BATCH": the write was an unlogged
+                               batch. No batch log write has been attempted.
+                             - "COUNTER": the write was a counter write
+                               (batched or not).
+                             - "BATCH_LOG": the timeout occurred during the
+                               write to the batch log when a (logged) batch
+                               write was requested.
+                            - "CAS": the timeout occured during the Compare And Set write/update.
+                            - "VIEW": the timeout occured when a write involves
+                              VIEW update and failure to acqiure local view(MV)
+                              lock for key within timeout
+                            - "CDC": the timeout occured when cdc_total_space_in_mb is
+                              exceeded when doing a write to data tracked by cdc.
+    0x1200    Read_timeout: Timeout exception during a read request. The rest
+              of the ERROR message body will be
+                <cl><received><blockfor><data_present>
+              where:
+                <cl> is the [consistency] level of the query having triggered
+                     the exception.
+                <received> is an [int] representing the number of nodes having
+                           answered the request.
+                <blockfor> is an [int] representing the number of replicas whose
+                           response is required to achieve <cl>. Please note that
+                           it is possible to have <received> >= <blockfor> if
+                           <data_present> is false. Also in the (unlikely)
+                           case where <cl> is achieved but the coordinator node
+                           times out while waiting for read-repair acknowledgement.
+                <data_present> is a single byte. If its value is 0, it means
+                               the replica that was asked for data has not
+                               responded. Otherwise, the value is != 0.
+    0x1300    Read_failure: A non-timeout exception during a read request. The rest
+              of the ERROR message body will be
+                <cl><received><blockfor><reasonmap><data_present>
+              where:
+                <cl> is the [consistency] level of the query having triggered
+                     the exception.
+                <received> is an [int] representing the number of nodes having
+                           answered the request.
+                <blockfor> is an [int] representing the number of replicas whose
+                           acknowledgement is required to achieve <cl>.
+                <reasonmap> is a map of endpoint to failure reason codes. This maps
+                            the endpoints of the replica nodes that failed when
+                            executing the request to a code representing the reason
+                            for the failure. The map is encoded starting with an [int] n
+                            followed by n pairs of <endpoint><failurecode> where
+                            <endpoint> is an [inetaddr] and <failurecode> is a [short].
+                <data_present> is a single byte. If its value is 0, it means
+                               the replica that was asked for data had not
+                               responded. Otherwise, the value is != 0.
+    0x1400    Function_failure: A (user defined) function failed during execution.
+              The rest of the ERROR message body will be
+                <keyspace><function><arg_types>
+              where:
+                <keyspace> is the keyspace [string] of the failed function
+                <function> is the name [string] of the failed function
+                <arg_types> [string list] one string for each argument type (as CQL type) of the failed function
+    0x1500    Write_failure: A non-timeout exception during a write request. The rest
+              of the ERROR message body will be
+                <cl><received><blockfor><reasonmap><write_type>
+              where:
+                <cl> is the [consistency] level of the query having triggered
+                     the exception.
+                <received> is an [int] representing the number of nodes having
+                           answered the request.
+                <blockfor> is an [int] representing the number of replicas whose
+                           acknowledgement is required to achieve <cl>.
+                <reasonmap> is a map of endpoint to failure reason codes. This maps
+                            the endpoints of the replica nodes that failed when
+                            executing the request to a code representing the reason
+                            for the failure. The map is encoded starting with an [int] n
+                            followed by n pairs of <endpoint><failurecode> where
+                            <endpoint> is an [inetaddr] and <failurecode> is a [short].
+                <writeType> is a [string] that describes the type of the write
+                            that failed. The value of that string can be one
+                            of:
+                             - "SIMPLE": the write was a non-batched
+                               non-counter write.
+                             - "BATCH": the write was a (logged) batch write.
+                               If this type is received, it means the batch log
+                               has been successfully written (otherwise a
+                               "BATCH_LOG" type would have been sent instead).
+                             - "UNLOGGED_BATCH": the write was an unlogged
+                               batch. No batch log write has been attempted.
+                             - "COUNTER": the write was a counter write
+                               (batched or not).
+                             - "BATCH_LOG": the failure occured during the
+                               write to the batch log when a (logged) batch
+                               write was requested.
+                            - "CAS": the failure occured during the Compare And Set write/update.
+                            - "VIEW": the failure occured when a write involves
+                              VIEW update and failure to acqiure local view(MV)
+                              lock for key within timeout
+                            - "CDC": the failure occured when cdc_total_space_in_mb is
+                              exceeded when doing a write to data tracked by cdc.
+
+    0x2000    Syntax_error: The submitted query has a syntax error.
+    0x2100    Unauthorized: The logged user doesn't have the right to perform
+              the query.
+    0x2200    Invalid: The query is syntactically correct but invalid.
+    0x2300    Config_error: The query is invalid because of some configuration issue
+    0x2400    Already_exists: The query attempted to create a keyspace or a
+              table that was already existing. The rest of the ERROR message
+              body will be <ks><table> where:
+                <ks> is a [string] representing either the keyspace that
+                     already exists, or the keyspace in which the table that
+                     already exists is.
+                <table> is a [string] representing the name of the table that
+                        already exists. If the query was attempting to create a
+                        keyspace, <table> will be present but will be the empty
+                        string.
+    0x2500    Unprepared: Can be thrown while a prepared statement tries to be
+              executed if the provided prepared statement ID is not known by
+              this host. The rest of the ERROR message body will be [short
+              bytes] representing the unknown ID.
+
+10. Changes from v4
+
+  * Beta protocol flag for v5 native protocol is added (Section 2.2)
+  * <numfailures> in Read_failure and Write_failure error message bodies (Section 9)
+    has been replaced with <reasonmap>. The <reasonmap> maps node IP addresses to
+    a failure reason code which indicates why the request failed on that node.
+  * Enlarged flag's bitmaps for QUERY, EXECUTE and BATCH messages from [byte] to [int]
+    (Sections 4.1.4, 4.1.6 and 4.1.7).
+  * Add the duration data type
diff --git a/doc/scripts/convert_yaml_to_adoc.py b/doc/scripts/convert_yaml_to_adoc.py
new file mode 100644
index 0000000..5eff522
--- /dev/null
+++ b/doc/scripts/convert_yaml_to_adoc.py
@@ -0,0 +1,144 @@
+# 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.
+
+"""
+A script to convert cassandra.yaml into Asciidoc for
+the online documentation.
+
+Usage:
+
+YAML_INPUT=conf/cassandra.yaml
+YAML_OUTPUT=modules/cassandra/pages/configuration/cass_yaml_file.adoc
+
+    convert_yaml_to_adoc.py $YAML_INPUT $YAML_OUTPUT
+"""
+
+import sys
+import re
+
+# Detects options, whether commented or uncommented.
+# Group 1 will be non-empty if the option is commented out.
+# Group 2 will contain the option name.
+# Group 3 will contain the default value, if one exists.
+option_re = re.compile(r"^(# ?)?([a-z0-9_]+): ?([^/].*)")
+
+# Detects normal comment lines.
+commented_re = re.compile(r"^# ?(.*)")
+
+# A set of option names that have complex values (i.e. lists or dicts).
+# This list is hardcoded because there did not seem to be another
+# good way to reliably detect this case, especially considering
+# that these can be commented out (making it useless to use a yaml parser).
+COMPLEX_OPTIONS = (
+    'seed_provider',
+    'data_file_directories',
+    'commitlog_compression',
+    'hints_compression',
+    'server_encryption_options',
+    'client_encryption_options',
+    'transparent_data_encryption_options',
+    'hinted_handoff_disabled_datacenters'
+)
+
+def convert(yaml_file, dest_file):
+    with open(yaml_file, 'r') as f:
+        # Trim off the boilerplate header
+        lines = f.readlines()[7:]
+
+    with open(dest_file, 'w') as outfile:
+        outfile.write("= cassandra.yaml file configuration\n")
+
+        # since comments preceed an option, this holds all of the comment
+        # lines we've seen since the last option
+        comments_since_last_option = []
+        line_iter = iter(lines)
+        while True:
+            try:
+                line = next(line_iter)
+            except StopIteration:
+                break
+
+            match = option_re.match(line)
+            if match:
+                option_name = match.group(2)
+                is_commented = bool(match.group(1))
+
+                is_complex = option_name in COMPLEX_OPTIONS
+                complex_option = read_complex_option(line_iter) if is_complex else None
+
+                write_section_header(option_name, outfile)
+                write_comments(comments_since_last_option, is_commented, outfile)
+                if is_complex:
+                    write_complex_option(complex_option, outfile)
+                else:
+                    maybe_write_default_value(match, outfile)
+                comments_since_last_option = []
+            else:
+                comment_match = commented_re.match(line)
+                if comment_match:
+                    comments_since_last_option.append(comment_match.group(1))
+                elif line == "\n":
+                    comments_since_last_option.append('')
+
+
+def write_section_header(option_name, outfile):
+    outfile.write("\n")
+    outfile.write("== `%s`\n\n" % (option_name,))
+
+
+def write_comments(comment_lines, is_commented, outfile):
+    if is_commented:
+        outfile.write("*This option is commented out by default.*\n")
+
+    for comment in comment_lines:
+        if "SAFETY THRESHOLDS" not in comment_lines:
+            outfile.write(comment + "\n")
+
+
+def maybe_write_default_value(option_match, outfile):
+    default_value = option_match.group(3)
+    if default_value and default_value != "\n":
+        outfile.write("\n_Default Value:_ %s\n" % (default_value,))
+
+
+def read_complex_option(line_iter):
+    option_lines = []
+    try:
+        while True:
+            line = next(line_iter)
+            if line == '\n':
+                return option_lines
+            else:
+                option_lines.append(line)
+    except StopIteration:
+        return option_lines
+
+
+def write_complex_option(lines, outfile):
+    outfile.write("\n_Default Value (complex option)_:\n\n....\n")
+    for line in lines:
+        outfile.write((" " * 4) + line)
+    outfile.write("....\n")
+
+
+if __name__ == '__main__':
+    if len(sys.argv) != 3:
+        print >> sys.stderr, "Usage: %s <yaml source file> <adoc dest file>" % (sys.argv[0],)
+        sys.exit(1)
+
+    yaml_file = sys.argv[1]
+    dest_file = sys.argv[2]
+    convert(yaml_file, dest_file)
diff --git a/doc/scripts/gen-nodetool-docs.py b/doc/scripts/gen-nodetool-docs.py
new file mode 100644
index 0000000..d27e213
--- /dev/null
+++ b/doc/scripts/gen-nodetool-docs.py
@@ -0,0 +1,105 @@
+# 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.
+"""
+A script to use nodetool to generate documentation for nodetool
+"""
+from __future__ import print_function
+
+import os
+import re
+import sys
+import subprocess
+from subprocess import PIPE
+from subprocess import Popen
+from itertools import islice
+from threading import Thread
+
+batch_size = 3
+
+if(os.environ.get("SKIP_NODETOOL") == "1"):
+    sys.exit(0)
+
+
+nodetool = "../bin/nodetool"
+outdir = "modules/cassandra/pages/tools/nodetool"
+examplesdir = "modules/cassandra/examples/TEXT/NODETOOL"
+helpfilename = outdir + "/nodetool.txt"
+command_re = re.compile("(    )([_a-z]+)")
+commandADOCContent = "== {0}\n\n== Usage\n[source,plaintext]\n----\ninclude::example$TEXT/NODETOOL/{0}.txt[]\n----\n"
+
+# https://docs.python.org/3/library/itertools.html#itertools-recipes
+def batched(iterable, n):
+    "Batch data into tuples of length n. The last batch may be shorter."
+    # batched('ABCDEFG', 3) --> ABC DEF G
+    if n < 1:
+        raise ValueError('n must be at least one')
+    it = iter(iterable)
+    batch = tuple(islice(it, n))
+    while (batch):
+        yield batch
+        batch = tuple(islice(it, n))
+
+# create the documentation directory
+if not os.path.exists(outdir):
+    os.makedirs(outdir)
+
+# create the base help file to use for discovering the commands
+def create_help_file():
+    with open(helpfilename, "w+") as output_file:
+        try:
+            subprocess.check_call([nodetool, "help"], stdout=output_file)
+        except subprocess.CalledProcessError as cpe:
+            print(
+                'ERROR: Nodetool failed to run, you likely need to build '
+                'cassandra using ant jar from the top level directory'
+            )
+            raise cpe
+
+# for a given command, create the help file and an ADOC file to contain it
+def create_adoc(command):
+    if command:
+        cmdName = command.group(0).strip()
+        cmdFilename = examplesdir + "/" + cmdName + ".txt"
+        adocFilename = outdir + "/" + cmdName + ".adoc"
+        with open(cmdFilename, "wb+") as cmdFile:
+            proc = Popen([nodetool, "help", cmdName], stdin=PIPE, stdout=PIPE)
+            (out, err) = proc.communicate()
+            cmdFile.write(out)
+        with open(adocFilename, "w+") as adocFile:
+            adocFile.write(commandADOCContent.format(cmdName,cmdName,cmdName))
+
+# create base file
+create_help_file()
+
+# create the main usage page
+with open(outdir + "/nodetool.adoc", "w+") as output:
+    with open(helpfilename, "r+") as helpfile:
+        output.write("== Nodetool\n\n== Usage\n\n")
+        for commandLine in helpfile:
+            command = command_re.sub(r'\nxref:tools/nodetool/\2.adoc[\2] - ',commandLine)
+            output.write(command)
+
+# create the command usage pages
+with open(helpfilename, "r+") as helpfile:
+    for clis in batched(helpfile, batch_size):
+        threads = []
+        for commandLine in clis:
+            command = command_re.match(commandLine)
+            t = Thread(target=create_adoc, args=[command])
+            threads.append(t)
+            t.start()
+        for t in threads:
+            t.join()
diff --git a/examples/triggers/src/org/apache/cassandra/triggers/AuditTrigger.java b/examples/triggers/src/org/apache/cassandra/triggers/AuditTrigger.java
index 7739450..1efbf13 100644
--- a/examples/triggers/src/org/apache/cassandra/triggers/AuditTrigger.java
+++ b/examples/triggers/src/org/apache/cassandra/triggers/AuditTrigger.java
@@ -22,10 +22,11 @@
 import java.util.Collections;
 import java.util.Properties;
 
+import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.Mutation;
-import org.apache.cassandra.db.RowUpdateBuilder;
 import org.apache.cassandra.db.partitions.Partition;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.UUIDGen;
@@ -39,15 +40,15 @@
         String auditKeyspace = properties.getProperty("keyspace");
         String auditTable = properties.getProperty("table");
 
-        RowUpdateBuilder audit = new RowUpdateBuilder(Schema.instance.getCFMetaData(auditKeyspace, auditTable),
-                                                      FBUtilities.timestampMicros(),
-                                                      UUIDGen.getTimeUUID());
+        CFMetaData metadata = Schema.instance.getCFMetaData(auditKeyspace, auditTable);
+        PartitionUpdate.SimpleBuilder audit = PartitionUpdate.simpleBuilder(metadata, UUIDGen.getTimeUUID());
 
-        audit.add("keyspace_name", update.metadata().ksName);
-        audit.add("table_name", update.metadata().cfName);
-        audit.add("primary_key", update.metadata().getKeyValidator().getString(update.partitionKey().getKey()));
+        audit.row()
+             .add("keyspace_name", update.metadata().ksName)
+             .add("table_name", update.metadata().cfName)
+             .add("primary_key", update.metadata().getKeyValidator().getString(update.partitionKey().getKey()));
 
-        return Collections.singletonList(audit.build());
+        return Collections.singletonList(audit.buildAsMutation());
     }
 
     private static Properties loadProperties()
diff --git a/ide/idea-iml-file.xml b/ide/idea-iml-file.xml
index b4c2382..ce8119b 100644
--- a/ide/idea-iml-file.xml
+++ b/ide/idea-iml-file.xml
@@ -28,6 +28,7 @@
             <sourceFolder url="file://$MODULE_DIR$/src/resources" type="java-resource" />
             <sourceFolder url="file://$MODULE_DIR$/interface/thrift/gen-java" isTestSource="false" />
             <sourceFolder url="file://$MODULE_DIR$/tools/stress/src" isTestSource="false" />
+            <sourceFolder url="file://$MODULE_DIR$/tools/stress/test/unit" isTestSource="true" />
             <sourceFolder url="file://$MODULE_DIR$/test/unit" isTestSource="true" />
             <sourceFolder url="file://$MODULE_DIR$/test/long" isTestSource="true" />
             <sourceFolder url="file://$MODULE_DIR$/test/microbench" isTestSource="true" />
@@ -35,6 +36,7 @@
             <sourceFolder url="file://$MODULE_DIR$/test/conf" type="java-test-resource" />
             <sourceFolder url="file://$MODULE_DIR$/test/distributed" isTestSource="true" />
             <sourceFolder url="file://$MODULE_DIR$/test/resources" type="java-test-resource" />
+            <sourceFolder url="file://$MODULE_DIR$/test/conf" type="java-test-resource" />
             <excludeFolder url="file://$MODULE_DIR$/.idea" />
             <excludeFolder url="file://$MODULE_DIR$/.settings" />
             <excludeFolder url="file://$MODULE_DIR$/build" />
diff --git a/ide/idea/workspace.xml b/ide/idea/workspace.xml
index a3fda6a..f98c858 100644
--- a/ide/idea/workspace.xml
+++ b/ide/idea/workspace.xml
@@ -143,7 +143,7 @@
     <configuration default="true" type="Application" factoryName="Application">
       <extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
       <option name="MAIN_CLASS_NAME" value="" />
-      <option name="VM_PARAMETERS" value="-Dcassandra.config=file://$PROJECT_DIR$/conf/cassandra.yaml -Dcassandra.storagedir=$PROJECT_DIR$/data -Dlogback.configurationFile=file://$PROJECT_DIR$/conf/logback.xml -Dcassandra.logdir=$PROJECT_DIR$/data/logs -ea" />
+      <option name="VM_PARAMETERS" value="-Dcassandra.config=file://$PROJECT_DIR$/conf/cassandra.yaml -Dcassandra.storagedir=$PROJECT_DIR$/data -Dlogback.configurationFile=file://$PROJECT_DIR$/conf/logback.xml -Dcassandra.logdir=$PROJECT_DIR$/data/logs -Djava.library.path=$PROJECT_DIR$/lib/sigar-bin -ea" />
       <option name="PROGRAM_PARAMETERS" value="" />
       <option name="WORKING_DIRECTORY" value="" />
       <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
@@ -155,7 +155,6 @@
       <envs />
       <method>
         <option name="AntTarget" enabled="true" antfile="file://$PROJECT_DIR$/build.xml" target="gen-cql3-grammar" />
-        <option name="AntTarget" enabled="true" antfile="file://$PROJECT_DIR$/build.xml" target="gen-thrift-java" />
         <option name="Make" enabled="true" />
       </method>
     </configuration>
@@ -181,14 +180,13 @@
       <patterns />
       <method>
         <option name="AntTarget" enabled="true" antfile="file://$PROJECT_DIR$/build.xml" target="gen-cql3-grammar" />
-        <option name="AntTarget" enabled="true" antfile="file://$PROJECT_DIR$/build.xml" target="gen-thrift-java" />
         <option name="Make" enabled="true" />
       </method>
     </configuration>
     <configuration default="false" name="Cassandra" type="Application" factoryName="Application">
       <extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
       <option name="MAIN_CLASS_NAME" value="org.apache.cassandra.service.CassandraDaemon" />
-      <option name="VM_PARAMETERS" value="-Dcassandra-foreground=yes -Dcassandra.config=file://$PROJECT_DIR$/conf/cassandra.yaml -Dcassandra.storagedir=$PROJECT_DIR$/data -Dlogback.configurationFile=file://$PROJECT_DIR$/conf/logback.xml -Dcassandra.logdir=$PROJECT_DIR$/data/logs -Dcassandra.jmx.local.port=7199 -ea -Xmx1G" />
+      <option name="VM_PARAMETERS" value="-Dcassandra-foreground=yes -Dcassandra.config=file://$PROJECT_DIR$/conf/cassandra.yaml -Dcassandra.storagedir=$PROJECT_DIR$/data -Dlogback.configurationFile=file://$PROJECT_DIR$/conf/logback.xml -Dcassandra.logdir=$PROJECT_DIR$/data/logs -Djava.library.path=$PROJECT_DIR$/lib/sigar-bin -Dcassandra.jmx.local.port=7199 -ea -Xmx1G" />
       <option name="PROGRAM_PARAMETERS" value="" />
       <option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$" />
       <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
diff --git a/interface/cassandra.thrift b/interface/cassandra.thrift
index f5041c8..2970cff 100644
--- a/interface/cassandra.thrift
+++ b/interface/cassandra.thrift
@@ -648,7 +648,7 @@
                                         throws (1:InvalidRequestException ire, 2:UnavailableException ue, 3:TimedOutException te),
 
   /**
-    Perform a get_count in parallel on the given list<binary> keys. The return value maps keys to the count found.
+    Perform a get_count in parallel on the given {@code List<binary>} keys. The return value maps keys to the count found.
   */
   map<binary, i32> multiget_count(1:required list<binary> keys,
                 2:required ColumnParent column_parent,
diff --git a/interface/thrift/gen-java/org/apache/cassandra/thrift/AuthenticationException.java b/interface/thrift/gen-java/org/apache/cassandra/thrift/AuthenticationException.java
index b16c400..381b052 100644
--- a/interface/thrift/gen-java/org/apache/cassandra/thrift/AuthenticationException.java
+++ b/interface/thrift/gen-java/org/apache/cassandra/thrift/AuthenticationException.java
@@ -34,25 +34,12 @@
 
 import org.apache.thrift.scheme.TupleScheme;
 import org.apache.thrift.protocol.TTupleProtocol;
-import org.apache.thrift.protocol.TProtocolException;
-import org.apache.thrift.EncodingUtils;
 import org.apache.thrift.TException;
-import org.apache.thrift.async.AsyncMethodCallback;
-import org.apache.thrift.server.AbstractNonblockingServer.*;
-import java.util.List;
-import java.util.ArrayList;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.EnumMap;
-import java.util.Set;
-import java.util.HashSet;
 import java.util.EnumSet;
 import java.util.Collections;
-import java.util.BitSet;
-import java.nio.ByteBuffer;
-import java.util.Arrays;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * invalid authentication request (invalid keyspace, user does not exist, or credentials invalid)
diff --git a/interface/thrift/gen-java/org/apache/cassandra/thrift/CfSplit.java b/interface/thrift/gen-java/org/apache/cassandra/thrift/CfSplit.java
index f765b87..b654f86 100644
--- a/interface/thrift/gen-java/org/apache/cassandra/thrift/CfSplit.java
+++ b/interface/thrift/gen-java/org/apache/cassandra/thrift/CfSplit.java
@@ -34,25 +34,12 @@
 
 import org.apache.thrift.scheme.TupleScheme;
 import org.apache.thrift.protocol.TTupleProtocol;
-import org.apache.thrift.protocol.TProtocolException;
 import org.apache.thrift.EncodingUtils;
-import org.apache.thrift.TException;
-import org.apache.thrift.async.AsyncMethodCallback;
-import org.apache.thrift.server.AbstractNonblockingServer.*;
-import java.util.List;
-import java.util.ArrayList;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.EnumMap;
-import java.util.Set;
-import java.util.HashSet;
 import java.util.EnumSet;
 import java.util.Collections;
-import java.util.BitSet;
-import java.nio.ByteBuffer;
-import java.util.Arrays;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Represents input splits used by hadoop ColumnFamilyRecordReaders
diff --git a/interface/thrift/gen-java/org/apache/cassandra/thrift/Column.java b/interface/thrift/gen-java/org/apache/cassandra/thrift/Column.java
index b516a38..e24a4e4 100644
--- a/interface/thrift/gen-java/org/apache/cassandra/thrift/Column.java
+++ b/interface/thrift/gen-java/org/apache/cassandra/thrift/Column.java
@@ -34,25 +34,14 @@
 
 import org.apache.thrift.scheme.TupleScheme;
 import org.apache.thrift.protocol.TTupleProtocol;
-import org.apache.thrift.protocol.TProtocolException;
 import org.apache.thrift.EncodingUtils;
-import org.apache.thrift.TException;
-import org.apache.thrift.async.AsyncMethodCallback;
-import org.apache.thrift.server.AbstractNonblockingServer.*;
-import java.util.List;
-import java.util.ArrayList;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.EnumMap;
-import java.util.Set;
-import java.util.HashSet;
 import java.util.EnumSet;
 import java.util.Collections;
 import java.util.BitSet;
 import java.nio.ByteBuffer;
-import java.util.Arrays;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Basic unit of data within a ColumnFamily.
diff --git a/interface/thrift/gen-java/org/apache/cassandra/thrift/ColumnDef.java b/interface/thrift/gen-java/org/apache/cassandra/thrift/ColumnDef.java
index 951c967..409e4ac 100644
--- a/interface/thrift/gen-java/org/apache/cassandra/thrift/ColumnDef.java
+++ b/interface/thrift/gen-java/org/apache/cassandra/thrift/ColumnDef.java
@@ -34,25 +34,13 @@
 
 import org.apache.thrift.scheme.TupleScheme;
 import org.apache.thrift.protocol.TTupleProtocol;
-import org.apache.thrift.protocol.TProtocolException;
-import org.apache.thrift.EncodingUtils;
-import org.apache.thrift.TException;
-import org.apache.thrift.async.AsyncMethodCallback;
-import org.apache.thrift.server.AbstractNonblockingServer.*;
-import java.util.List;
-import java.util.ArrayList;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.EnumMap;
-import java.util.Set;
-import java.util.HashSet;
 import java.util.EnumSet;
 import java.util.Collections;
 import java.util.BitSet;
 import java.nio.ByteBuffer;
-import java.util.Arrays;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ColumnDef implements org.apache.thrift.TBase<ColumnDef, ColumnDef._Fields>, java.io.Serializable, Cloneable, Comparable<ColumnDef> {
   private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("ColumnDef");
diff --git a/interface/thrift/gen-java/org/apache/cassandra/thrift/ColumnOrSuperColumn.java b/interface/thrift/gen-java/org/apache/cassandra/thrift/ColumnOrSuperColumn.java
index 2de9d0e..261d93f 100644
--- a/interface/thrift/gen-java/org/apache/cassandra/thrift/ColumnOrSuperColumn.java
+++ b/interface/thrift/gen-java/org/apache/cassandra/thrift/ColumnOrSuperColumn.java
@@ -34,25 +34,12 @@
 
 import org.apache.thrift.scheme.TupleScheme;
 import org.apache.thrift.protocol.TTupleProtocol;
-import org.apache.thrift.protocol.TProtocolException;
-import org.apache.thrift.EncodingUtils;
-import org.apache.thrift.TException;
-import org.apache.thrift.async.AsyncMethodCallback;
-import org.apache.thrift.server.AbstractNonblockingServer.*;
-import java.util.List;
-import java.util.ArrayList;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.EnumMap;
-import java.util.Set;
-import java.util.HashSet;
 import java.util.EnumSet;
 import java.util.Collections;
 import java.util.BitSet;
-import java.nio.ByteBuffer;
-import java.util.Arrays;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Methods for fetching rows/records from Cassandra will return either a single instance of ColumnOrSuperColumn or a list
diff --git a/interface/thrift/gen-java/org/apache/cassandra/thrift/ColumnPath.java b/interface/thrift/gen-java/org/apache/cassandra/thrift/ColumnPath.java
index 627b9a0..997c1bb 100644
--- a/interface/thrift/gen-java/org/apache/cassandra/thrift/ColumnPath.java
+++ b/interface/thrift/gen-java/org/apache/cassandra/thrift/ColumnPath.java
@@ -34,25 +34,13 @@
 
 import org.apache.thrift.scheme.TupleScheme;
 import org.apache.thrift.protocol.TTupleProtocol;
-import org.apache.thrift.protocol.TProtocolException;
-import org.apache.thrift.EncodingUtils;
-import org.apache.thrift.TException;
-import org.apache.thrift.async.AsyncMethodCallback;
-import org.apache.thrift.server.AbstractNonblockingServer.*;
-import java.util.List;
-import java.util.ArrayList;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.EnumMap;
-import java.util.Set;
-import java.util.HashSet;
 import java.util.EnumSet;
 import java.util.Collections;
 import java.util.BitSet;
 import java.nio.ByteBuffer;
-import java.util.Arrays;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * The ColumnPath is the path to a single column in Cassandra. It might make sense to think of ColumnPath and
diff --git a/interface/thrift/gen-java/org/apache/cassandra/thrift/CounterColumn.java b/interface/thrift/gen-java/org/apache/cassandra/thrift/CounterColumn.java
index 6069218..cd9c593 100644
--- a/interface/thrift/gen-java/org/apache/cassandra/thrift/CounterColumn.java
+++ b/interface/thrift/gen-java/org/apache/cassandra/thrift/CounterColumn.java
@@ -34,7 +34,6 @@
 
 import org.apache.thrift.scheme.TupleScheme;
 import org.apache.thrift.protocol.TTupleProtocol;
-import org.apache.thrift.protocol.TProtocolException;
 import org.apache.thrift.EncodingUtils;
 import org.apache.thrift.TException;
 import org.apache.thrift.async.AsyncMethodCallback;
diff --git a/pylib/cassandra-cqlsh-tests.sh b/pylib/cassandra-cqlsh-tests.sh
index 46c3119..871c542 100755
--- a/pylib/cassandra-cqlsh-tests.sh
+++ b/pylib/cassandra-cqlsh-tests.sh
@@ -15,6 +15,7 @@
 # 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.
+#
 
 ################################
 #
diff --git a/pylib/cqlshlib/copyutil.py b/pylib/cqlshlib/copyutil.py
index 9692db5..b36fa83 100644
--- a/pylib/cqlshlib/copyutil.py
+++ b/pylib/cqlshlib/copyutil.py
@@ -54,7 +54,7 @@
 
 from cql3handling import CqlRuleSet
 from displaying import NO_COLOR_MAP
-from formatting import format_value_default, DateTimeFormat, EMPTY, get_formatter, BlobType
+from formatting import format_value_default, CqlType, DateTimeFormat, EMPTY, get_formatter, BlobType
 from sslhandling import ssl_settings
 
 PROFILE_ON = False
@@ -348,8 +348,10 @@
         copy_options['pagetimeout'] = int(opts.pop('pagetimeout', max(10, 10 * (copy_options['pagesize'] / 1000))))
         copy_options['maxattempts'] = int(opts.pop('maxattempts', 5))
         copy_options['dtformats'] = DateTimeFormat(opts.pop('datetimeformat', shell.display_timestamp_format),
-                                                   shell.display_date_format, shell.display_nanotime_format)
-        copy_options['float_precision'] = shell.display_float_precision
+                                                   shell.display_date_format, shell.display_nanotime_format,
+                                                   milliseconds_only=True)
+        copy_options['floatprecision'] = int(opts.pop('floatprecision', '5'))
+        copy_options['doubleprecision'] = int(opts.pop('doubleprecision', '12'))
         copy_options['chunksize'] = int(opts.pop('chunksize', 5000))
         copy_options['ingestrate'] = int(opts.pop('ingestrate', 100000))
         copy_options['maxbatchsize'] = int(opts.pop('maxbatchsize', 20))
@@ -371,6 +373,7 @@
         copy_options['ratefile'] = safe_normpath(opts.pop('ratefile', ''))
         copy_options['maxoutputsize'] = int(opts.pop('maxoutputsize', '-1'))
         copy_options['preparedstatements'] = bool(opts.pop('preparedstatements', 'true').lower() == 'true')
+        copy_options['ttl'] = int(opts.pop('ttl', -1))
 
         # Hidden properties, they do not appear in the documentation but can be set in config files
         # or on the cmd line but w/o completion
@@ -415,8 +418,11 @@
         """
         try:
             num_cores_for_testing = os.environ.get('CQLSH_COPY_TEST_NUM_CORES', '')
-            return int(num_cores_for_testing) if num_cores_for_testing else mp.cpu_count()
+            ret = int(num_cores_for_testing) if num_cores_for_testing else mp.cpu_count()
+            printdebugmsg("Detected %d core(s)" % (ret,))
+            return ret
         except NotImplementedError:
+            printdebugmsg("Failed to detect number of cores, returning 1")
             return 1
 
     @staticmethod
@@ -886,15 +892,18 @@
             try:
                 return open(fname, 'rb')
             except IOError, e:
-                printdebugmsg("Can't open %r for reading: %s" % (fname, e))
-                return None
+                raise IOError("Can't open %r for reading: %s" % (fname, e))
 
         for path in paths.split(','):
             path = path.strip()
             if os.path.isfile(path):
                 yield make_source(path)
             else:
-                for f in glob.glob(path):
+                result = glob.glob(path)
+                if len(result) == 0:
+                    raise IOError("Can't open %r for reading: no matching file found" % (path,))
+
+                for f in result:
                     yield (make_source(f))
 
     def start(self):
@@ -1327,7 +1336,11 @@
         self.on_fork()
 
         reader = self.reader
-        reader.start()
+        try:
+            reader.start()
+        except IOError, exc:
+            self.outmsg.send(ImportTaskError(exc.__class__.__name__, exc.message))
+
         channels = self.worker_channels
         max_pending_chunks = self.max_pending_chunks
         sent = 0
@@ -1533,7 +1546,8 @@
     def __init__(self, params):
         ChildProcess.__init__(self, params=params, target=self.run)
         options = params['options']
-        self.float_precision = options.copy['float_precision']
+        self.float_precision = options.copy['floatprecision']
+        self.double_precision = options.copy['doubleprecision']
         self.nullval = options.copy['nullval']
         self.max_requests = options.copy['maxrequests']
 
@@ -1657,12 +1671,17 @@
         return session
 
     def attach_callbacks(self, token_range, future, session):
+        metadata = session.cluster.metadata
+        ks_meta = metadata.keyspaces[self.ks]
+        table_meta = ks_meta.tables[self.table]
+        cql_types = [CqlType(table_meta.columns[c].cql_type, ks_meta) for c in self.columns]
+
         def result_callback(rows):
             if future.has_more_pages:
                 future.start_fetching_next_page()
-                self.write_rows_to_csv(token_range, rows)
+                self.write_rows_to_csv(token_range, rows, cql_types)
             else:
-                self.write_rows_to_csv(token_range, rows)
+                self.write_rows_to_csv(token_range, rows, cql_types)
                 self.send((None, None))
                 session.complete_request()
 
@@ -1672,7 +1691,7 @@
 
         future.add_callbacks(callback=result_callback, errback=err_callback)
 
-    def write_rows_to_csv(self, token_range, rows):
+    def write_rows_to_csv(self, token_range, rows, cql_types):
         if not rows:
             return  # no rows in this range
 
@@ -1681,7 +1700,7 @@
             writer = csv.writer(output, **self.options.dialect)
 
             for row in rows:
-                writer.writerow(map(self.format_value, row))
+                writer.writerow(map(self.format_value, row, cql_types))
 
             data = (output.getvalue(), len(rows))
             self.send((token_range, data))
@@ -1690,18 +1709,21 @@
         except Exception, e:
             self.report_error(e, token_range)
 
-    def format_value(self, val):
+    def format_value(self, val, cqltype):
         if val is None or val == EMPTY:
             return format_value_default(self.nullval, colormap=NO_COLOR_MAP)
 
-        ctype = type(val)
-        formatter = self.formatters.get(ctype, None)
+        formatter = self.formatters.get(cqltype, None)
         if not formatter:
-            formatter = get_formatter(ctype)
-            self.formatters[ctype] = formatter
+            formatter = get_formatter(val, cqltype)
+            self.formatters[cqltype] = formatter
 
-        return formatter(val, encoding=self.encoding, colormap=NO_COLOR_MAP, date_time_format=self.date_time_format,
-                         float_precision=self.float_precision, nullval=self.nullval, quote=False,
+        if not hasattr(cqltype, 'precision'):
+            cqltype.precision = self.double_precision if cqltype.type_name == 'double' else self.float_precision
+
+        return formatter(val, cqltype=cqltype,
+                         encoding=self.encoding, colormap=NO_COLOR_MAP, date_time_format=self.date_time_format,
+                         float_precision=cqltype.precision, nullval=self.nullval, quote=False,
                          decimal_sep=self.decimal_sep, thousands_sep=self.thousands_sep,
                          boolean_styles=self.boolean_styles)
 
@@ -1937,9 +1959,9 @@
 
             return ret
 
-        # this should match all possible CQL datetime formats
+        # this should match all possible CQL and CQLSH datetime formats
         p = re.compile(r"(\d{4})\-(\d{2})\-(\d{2})\s?(?:'T')?"  # YYYY-MM-DD[( |'T')]
-                       + r"(?:(\d{2}):(\d{2})(?::(\d{2}))?)?"  # [HH:MM[:SS]]
+                       + r"(?:(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,6}))?))?"  # [HH:MM[:SS[.NNNNNN]]]
                        + r"(?:([+\-])(\d{2}):?(\d{2}))?")  # [(+|-)HH[:]MM]]
 
         def convert_datetime(val, **_):
@@ -1966,13 +1988,16 @@
                                     int(m.group(6)) if m.group(6) else 0,  # second
                                     0, 1, -1))  # day of week, day of year, dst-flag
 
-            if m.group(7):
-                offset = (int(m.group(8)) * 3600 + int(m.group(9)) * 60) * int(m.group(7) + '1')
+            # convert sub-seconds (a number between 1 and 6 digits) to milliseconds
+            milliseconds = 0 if not m.group(7) else int(m.group(7)) * pow(10, 3 - len(m.group(7)))
+
+            if m.group(8):
+                offset = (int(m.group(9)) * 3600 + int(m.group(10)) * 60) * int(m.group(8) + '1')
             else:
                 offset = -time.timezone
 
             # scale seconds to millis for the raw value
-            return (timegm(tval) + offset) * 1e3
+            return ((timegm(tval) + offset) * 1e3) + milliseconds
 
         def convert_date(v, **_):
             return Date(v)
@@ -2264,6 +2289,7 @@
         self.min_batch_size = options.copy['minbatchsize']
         self.max_batch_size = options.copy['maxbatchsize']
         self.use_prepared_statements = options.copy['preparedstatements']
+        self.ttl = options.copy['ttl']
         self.max_inflight_messages = options.copy['maxinflightmessages']
         self.max_backoff_attempts = options.copy['maxbackoffattempts']
         self.request_timeout = options.copy['requesttimeout']
@@ -2333,7 +2359,8 @@
                                                             protect_name(self.table),
                                                             ', '.join(protect_names(self.valid_columns),),
                                                             ', '.join(['?' for _ in self.valid_columns]))
-
+            if self.ttl >= 0:
+                query += 'USING TTL %s' % (self.ttl,)
             query = self.session.prepare(query)
             query.consistency_level = self.consistency_level
             prepared_statement = query
@@ -2342,6 +2369,8 @@
             query = 'INSERT INTO %s.%s (%s) VALUES (%%s)' % (protect_name(self.ks),
                                                              protect_name(self.table),
                                                              ', '.join(protect_names(self.valid_columns),))
+            if self.ttl >= 0:
+                query += 'USING TTL %s' % (self.ttl,)
             make_statement = self.wrap_make_statement(self.make_non_prepared_batch_statement)
 
         conv = ImportConversion(self, table_meta, prepared_statement)
@@ -2402,25 +2431,20 @@
         return make_statement_with_failures if self.test_failures else make_statement
 
     def make_counter_batch_statement(self, query, conv, batch, replicas):
-        def make_full_query(r):
+        statement = BatchStatement(batch_type=BatchType.COUNTER, consistency_level=self.consistency_level)
+        statement.replicas = replicas
+        statement.keyspace = self.ks
+        for row in batch['rows']:
             where_clause = []
             set_clause = []
-            for i, value in enumerate(r):
+            for i, value in enumerate(row):
                 if i in conv.primary_key_indexes:
                     where_clause.append("%s=%s" % (self.valid_columns[i], value))
                 else:
                     set_clause.append("%s=%s+%s" % (self.valid_columns[i], self.valid_columns[i], value))
-            return query % (','.join(set_clause), ' AND '.join(where_clause))
 
-        if len(batch['rows']) == 1:
-            statement = SimpleStatement(make_full_query(batch['rows'][0]), consistency_level=self.consistency_level)
-        else:
-            statement = BatchStatement(batch_type=BatchType.COUNTER, consistency_level=self.consistency_level)
-            for row in batch['rows']:
-                statement.add(make_full_query(row))
-
-        statement.replicas = replicas
-        statement.keyspace = self.ks
+            full_query_text = query % (','.join(set_clause), ' AND '.join(where_clause))
+            statement.add(full_query_text)
         return statement
 
     def make_prepared_batch_statement(self, query, _, batch, replicas):
@@ -2434,25 +2458,17 @@
         We could optimize further by removing bound_statements altogether but we'd have to duplicate much
         more driver's code (BoundStatement.bind()).
         """
-        if len(batch['rows']) == 1:
-            statement = query.bind(batch['rows'][0])
-        else:
-            statement = BatchStatement(batch_type=BatchType.UNLOGGED, consistency_level=self.consistency_level)
-            statement._statements_and_parameters = [(True, query.query_id, query.bind(r).values) for r in batch['rows']]
-
+        statement = BatchStatement(batch_type=BatchType.UNLOGGED, consistency_level=self.consistency_level)
         statement.replicas = replicas
         statement.keyspace = self.ks
+        statement._statements_and_parameters = [(True, query.query_id, query.bind(r).values) for r in batch['rows']]
         return statement
 
     def make_non_prepared_batch_statement(self, query, _, batch, replicas):
-        if len(batch['rows']) == 1:
-            statement = SimpleStatement(query % (','.join(batch['rows'][0]),), consistency_level=self.consistency_level)
-        else:
-            statement = BatchStatement(batch_type=BatchType.UNLOGGED, consistency_level=self.consistency_level)
-            statement._statements_and_parameters = [(False, query % (','.join(r),), ()) for r in batch['rows']]
-
+        statement = BatchStatement(batch_type=BatchType.UNLOGGED, consistency_level=self.consistency_level)
         statement.replicas = replicas
         statement.keyspace = self.ks
+        statement._statements_and_parameters = [(False, query % (','.join(r),), ()) for r in batch['rows']]
         return statement
 
     def convert_rows(self, conv, chunk):
diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py
index f8d4e35..dbbbe81 100644
--- a/pylib/cqlshlib/cql3handling.py
+++ b/pylib/cqlshlib/cql3handling.py
@@ -18,8 +18,8 @@
 from cassandra.metadata import maybe_escape_name
 
 
-simple_cql_types = set(('ascii', 'bigint', 'blob', 'boolean', 'counter', 'date', 'decimal', 'double', 'float', 'inet', 'int',
-                        'smallint', 'text', 'time', 'timestamp', 'timeuuid', 'tinyint', 'uuid', 'varchar', 'varint'))
+simple_cql_types = set(('ascii', 'bigint', 'blob', 'boolean', 'counter', 'date', 'decimal', 'double', 'duration', 'float',
+                        'inet', 'int', 'smallint', 'text', 'time', 'timestamp', 'timeuuid', 'tinyint', 'uuid', 'varchar', 'varint'))
 simple_cql_types.difference_update(('set', 'map', 'list'))
 
 from . import helptopics
@@ -52,13 +52,14 @@
         ('default_time_to_live', None),
         ('speculative_retry', None),
         ('memtable_flush_period_in_ms', None),
+        ('cdc', None)
     )
 
     columnfamily_layout_map_options = (
         # (CQL3 option name, schema_columnfamilies column name (or None if same),
         #  list of known map keys)
         ('compaction', 'compaction_strategy_options',
-            ('class', 'max_threshold', 'tombstone_compaction_interval', 'tombstone_threshold', 'enabled', 'unchecked_tombstone_compaction', 'only_purge_repaired_tombstones')),
+            ('class', 'max_threshold', 'tombstone_compaction_interval', 'tombstone_threshold', 'enabled', 'unchecked_tombstone_compaction', 'only_purge_repaired_tombstones', 'provide_overlapping_tombstones')),
         ('compression', 'compression_parameters',
             ('sstable_compression', 'chunk_length_kb', 'crc_check_chance')),
         ('caching', None,
@@ -485,6 +486,8 @@
     if this_opt in ('min_compaction_threshold', 'max_compaction_threshold',
                     'gc_grace_seconds', 'min_index_interval', 'max_index_interval'):
         return [Hint('<integer>')]
+    if this_opt in ('cdc'):
+        return [Hint('<true|false>')]
     return [Hint('<option_value>')]
 
 
@@ -516,6 +519,7 @@
             opts.add('bucket_low')
         elif csc == 'LeveledCompactionStrategy':
             opts.add('sstable_size_in_mb')
+            opts.add('fanout_size')
         elif csc == 'DateTieredCompactionStrategy':
             opts.add('base_time_seconds')
             opts.add('max_sstable_age_days')
@@ -539,6 +543,8 @@
     if opt == 'compaction':
         if key == 'class':
             return map(escape_value, CqlRuleSet.available_compaction_classes)
+        if key == 'provide_overlapping_tombstones':
+            return [Hint('<NONE|ROW|CELL>')]
         return [Hint('<option_value>')]
     elif opt == 'compression':
         if key == 'sstable_compression':
@@ -572,13 +578,13 @@
 
 
 @completer_for('nonSystemKeyspaceName', 'ksname')
-def ks_name_completer(ctxt, cass):
+def non_system_ks_name_completer(ctxt, cass):
     ksnames = [n for n in cass.get_keyspace_names() if n not in SYSTEM_KEYSPACES]
     return map(maybe_escape_name, ksnames)
 
 
 @completer_for('alterableKeyspaceName', 'ksname')
-def ks_name_completer(ctxt, cass):
+def alterable_ks_name_completer(ctxt, cass):
     ksnames = [n for n in cass.get_keyspace_names() if n not in NONALTERBALE_KEYSPACES]
     return map(maybe_escape_name, ksnames)
 
@@ -689,7 +695,9 @@
 <selectStatement> ::= "SELECT" ( "JSON" )? <selectClause>
                         "FROM" (cf=<columnFamilyName> | mv=<materializedViewName>)
                           ( "WHERE" <whereClause> )?
+                          ( "GROUP" "BY" <groupByClause> ( "," <groupByClause> )* )?
                           ( "ORDER" "BY" <orderByClause> ( "," <orderByClause> )* )?
+                          ( "PER" "PARTITION" "LIMIT" perPartitionLimit=<wholenumber> )?
                           ( "LIMIT" limit=<wholenumber> )?
                           ( "ALLOW" "FILTERING" )?
                     ;
@@ -711,12 +719,16 @@
              | "WRITETIME" "(" [colname]=<cident> ")"
              | "TTL" "(" [colname]=<cident> ")"
              | "COUNT" "(" star=( "*" | "1" ) ")"
+             | "CAST" "(" <selector> "AS" <storageType> ")"
              | <functionName> <selectionFunctionArguments>
+             | <term>
              ;
 <selectionFunctionArguments> ::= "(" ( <selector> ( "," <selector> )* )? ")"
                           ;
 <orderByClause> ::= [ordercol]=<cident> ( "ASC" | "DESC" )?
                   ;
+<groupByClause> ::= [groupcol]=<cident>
+                  ;
 '''
 
 
@@ -797,6 +809,16 @@
     return [Hint('No more orderable columns here.')]
 
 
+@completer_for('groupByClause', 'groupcol')
+def select_group_column_completer(ctxt, cass):
+    prev_group_cols = ctxt.get_binding('groupcol', ())
+    layout = get_table_meta(ctxt, cass)
+    group_by_candidates = [col.name for col in layout.primary_key]
+    if len(group_by_candidates) > len(prev_group_cols):
+        return [maybe_escape_name(group_by_candidates[len(prev_group_cols)])]
+    return [Hint('No more columns here.')]
+
+
 @completer_for('relation', 'token')
 def relation_token_word_completer(ctxt, cass):
     return ['TOKEN(']
@@ -888,7 +910,6 @@
 
 @completer_for('insertStatement', 'valcomma')
 def insert_valcomma_completer(ctxt, cass):
-    layout = get_table_meta(ctxt, cass)
     numcols = len(ctxt.get_binding('colname', ()))
     numvals = len(ctxt.get_binding('newval', ()))
     if numcols > numvals:
@@ -913,21 +934,27 @@
                         ( "IF" ( "EXISTS" | <conditions> ))?
                     ;
 <assignment> ::= updatecol=<cident>
-                    ( "=" update_rhs=( <term> | <cident> )
+                    (( "=" update_rhs=( <term> | <cident> )
                                 ( counterop=( "+" | "-" ) inc=<wholenumber>
-                                | listadder="+" listcol=<cident> )?
-                    | indexbracket="[" <term> "]" "=" <term> )
+                                | listadder="+" listcol=<cident> )? )
+                    | ( indexbracket="[" <term> "]" "=" <term> )
+                    | ( udt_field_dot="." udt_field=<identifier> "=" <term> ))
                ;
 <conditions> ::=  <condition> ( "AND" <condition> )*
                ;
-<condition> ::= <cident> ( "[" <term> "]" )? (("=" | "<" | ">" | "<=" | ">=" | "!=") <term>
-                                             | "IN" "(" <term> ( "," <term> )* ")")
+<condition_op_and_rhs> ::= (("=" | "<" | ">" | "<=" | ">=" | "!=") <term>)
+                           | ("IN" "(" <term> ( "," <term> )* ")" )
+                         ;
+<condition> ::= conditioncol=<cident>
+                    ( (( indexbracket="[" <term> "]" )
+                      |( udt_field_dot="." udt_field=<identifier> )) )?
+                    <condition_op_and_rhs>
               ;
 '''
 
 
 @completer_for('updateStatement', 'updateopt')
-def insert_option_completer(ctxt, cass):
+def update_option_completer(ctxt, cass):
     opts = set('TIMESTAMP TTL'.split())
     for opt in ctxt.get_binding('updateopt', ()):
         opts.discard(opt.split()[0])
@@ -997,6 +1024,61 @@
     return []
 
 
+@completer_for('assignment', 'udt_field_dot')
+def update_udt_field_dot_completer(ctxt, cass):
+    layout = get_table_meta(ctxt, cass)
+    curcol = dequote_name(ctxt.get_binding('updatecol', ''))
+    return ["."] if _is_usertype(layout, curcol) else []
+
+
+@completer_for('assignment', 'udt_field')
+def assignment_udt_field_completer(ctxt, cass):
+    layout = get_table_meta(ctxt, cass)
+    curcol = dequote_name(ctxt.get_binding('updatecol', ''))
+    return _usertype_fields(ctxt, cass, layout, curcol)
+
+
+def _is_usertype(layout, curcol):
+    coltype = layout.columns[curcol].cql_type
+    return coltype not in simple_cql_types and coltype not in ('map', 'set', 'list')
+
+
+def _usertype_fields(ctxt, cass, layout, curcol):
+    if not _is_usertype(layout, curcol):
+        return []
+
+    coltype = layout.columns[curcol].cql_type
+    ks = ctxt.get_binding('ksname', None)
+    if ks is not None:
+        ks = dequote_name(ks)
+    user_type = cass.get_usertype_layout(ks, coltype)
+    return [field_name for (field_name, field_type) in user_type]
+
+
+@completer_for('condition', 'indexbracket')
+def condition_indexbracket_completer(ctxt, cass):
+    layout = get_table_meta(ctxt, cass)
+    curcol = dequote_name(ctxt.get_binding('conditioncol', ''))
+    coltype = layout.columns[curcol].cql_type
+    if coltype in ('map', 'list'):
+        return ['[']
+    return []
+
+
+@completer_for('condition', 'udt_field_dot')
+def condition_udt_field_dot_completer(ctxt, cass):
+    layout = get_table_meta(ctxt, cass)
+    curcol = dequote_name(ctxt.get_binding('conditioncol', ''))
+    return ["."] if _is_usertype(layout, curcol) else []
+
+
+@completer_for('condition', 'udt_field')
+def condition_udt_field_completer(ctxt, cass):
+    layout = get_table_meta(ctxt, cass)
+    curcol = dequote_name(ctxt.get_binding('conditioncol', ''))
+    return _usertype_fields(ctxt, cass, layout, curcol)
+
+
 syntax_rules += r'''
 <deleteStatement> ::= "DELETE" ( <deleteSelector> ( "," <deleteSelector> )* )?
                         "FROM" cf=<columnFamilyName>
@@ -1004,7 +1086,9 @@
                         "WHERE" <whereClause>
                         ( "IF" ( "EXISTS" | <conditions> ) )?
                     ;
-<deleteSelector> ::= delcol=<cident> ( memberbracket="[" memberselector=<term> "]" )?
+<deleteSelector> ::= delcol=<cident>
+                     ( ( "[" <term> "]" )
+                     | ( "." <identifier> ) )?
                    ;
 <deleteOption> ::= "TIMESTAMP" <wholenumber>
                  ;
@@ -1076,7 +1160,7 @@
                                 ;
 
 <cfamProperty> ::= <property>
-                 | "COMPACT" "STORAGE"
+                 | "COMPACT" "STORAGE" "CDC"
                  | "CLUSTERING" "ORDER" "BY" "(" <cfamOrdering>
                                                  ( "," <cfamOrdering> )* ")"
                  ;
@@ -1430,6 +1514,7 @@
 <resource> ::= <dataResource>
              | <roleResource>
              | <functionResource>
+             | <jmxResource>
              ;
 
 <dataResource> ::= ( "ALL" "KEYSPACES" )
@@ -1448,6 +1533,11 @@
                            ")" )
                        )
                      ;
+
+<jmxResource> ::= ( "ALL" "MBEANS")
+                | ( ( "MBEAN" | "MBEANS" ) <stringLiteral> )
+                ;
+
 '''
 
 
@@ -1500,7 +1590,7 @@
 
 
 @completer_for('dropTriggerStatement', 'triggername')
-def alter_type_field_completer(ctxt, cass):
+def drop_trigger_completer(ctxt, cass):
     names = get_trigger_names(ctxt, cass)
     return map(maybe_escape_name, names)
 
diff --git a/pylib/cqlshlib/cqlhandling.py b/pylib/cqlshlib/cqlhandling.py
index 51d9726..a9c9c6f 100644
--- a/pylib/cqlshlib/cqlhandling.py
+++ b/pylib/cqlshlib/cqlhandling.py
@@ -18,11 +18,25 @@
 # i.e., stuff that's not necessarily cqlsh-specific
 
 import traceback
-from cassandra.metadata import cql_keywords_reserved
+import cassandra
 from . import pylexotron, util
 
 Hint = pylexotron.Hint
 
+cql_keywords_reserved = set((
+    'add', 'allow', 'alter', 'and', 'apply', 'asc', 'authorize', 'batch', 'begin', 'by', 'columnfamily', 'create',
+    'default', 'delete', 'desc', 'describe', 'drop', 'entries', 'execute', 'from', 'full', 'grant', 'if', 'in', 'index',
+    'infinity', 'insert', 'into', 'is', 'keyspace', 'limit', 'materialized', 'mbean', 'mbeans', 'modify', 'nan',
+    'norecursive', 'not', 'null', 'of', 'on', 'or', 'order', 'primary', 'rename', 'replace', 'revoke', 'schema',
+    'select', 'set', 'table', 'to', 'token', 'truncate', 'unlogged', 'unset', 'update', 'use', 'using', 'view', 'where',
+    'with'
+))
+"""
+Set of reserved keywords in CQL.
+
+Derived from .../cassandra/src/java/org/apache/cassandra/cql3/ReservedKeywords.java
+"""
+
 
 class CqlParsingRuleSet(pylexotron.ParsingRuleSet):
 
@@ -57,14 +71,15 @@
 
         # note: commands_end_with_newline may be extended by callers.
         self.commands_end_with_newline = set()
-        self.set_reserved_keywords(cql_keywords_reserved)
+        self.set_reserved_keywords()
 
-    def set_reserved_keywords(self, keywords):
+    def set_reserved_keywords(self):
         """
-        We cannot let resreved cql keywords be simple 'identifier' since this caused
+        We cannot let reserved cql keywords be simple 'identifier' since this caused
         problems with completion, see CASSANDRA-10415
         """
-        syntax = '<reserved_identifier> ::= /(' + '|'.join(r'\b{}\b'.format(k) for k in keywords) + ')/ ;'
+        cassandra.metadata.cql_keywords_reserved = cql_keywords_reserved
+        syntax = '<reserved_identifier> ::= /(' + '|'.join(r'\b{}\b'.format(k) for k in cql_keywords_reserved) + ')/ ;'
         self.append_rules(syntax)
 
     def completer_for(self, rulename, symname):
diff --git a/pylib/cqlshlib/cqlshhandling.py b/pylib/cqlshlib/cqlshhandling.py
new file mode 100644
index 0000000..9545876
--- /dev/null
+++ b/pylib/cqlshlib/cqlshhandling.py
@@ -0,0 +1,310 @@
+# 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.
+
+import os
+import cqlhandling
+
+# we want the cql parser to understand our cqlsh-specific commands too
+my_commands_ending_with_newline = (
+    'help',
+    '?',
+    'consistency',
+    'serial',
+    'describe',
+    'desc',
+    'show',
+    'source',
+    'capture',
+    'login',
+    'debug',
+    'tracing',
+    'expand',
+    'paging',
+    'exit',
+    'quit',
+    'clear',
+    'cls'
+)
+
+cqlsh_syntax_completers = []
+
+
+def cqlsh_syntax_completer(rulename, termname):
+    def registrator(f):
+        cqlsh_syntax_completers.append((rulename, termname, f))
+        return f
+
+    return registrator
+
+
+cqlsh_cmd_syntax_rules = r'''
+<cqlshCommand> ::= <CQL_Statement>
+                 | <specialCommand> ( ";" | "\n" )
+                 ;
+'''
+
+cqlsh_special_cmd_command_syntax_rules = r'''
+<specialCommand> ::= <describeCommand>
+                   | <consistencyCommand>
+                   | <serialConsistencyCommand>
+                   | <showCommand>
+                   | <sourceCommand>
+                   | <captureCommand>
+                   | <copyCommand>
+                   | <loginCommand>
+                   | <debugCommand>
+                   | <helpCommand>
+                   | <tracingCommand>
+                   | <expandCommand>
+                   | <exitCommand>
+                   | <pagingCommand>
+                   | <clearCommand>
+                   ;
+'''
+
+cqlsh_describe_cmd_syntax_rules = r'''
+<describeCommand> ::= ( "DESCRIBE" | "DESC" )
+                                  ( "FUNCTIONS"
+                                  | "FUNCTION" udf=<anyFunctionName>
+                                  | "AGGREGATES"
+                                  | "AGGREGATE" uda=<userAggregateName>
+                                  | "KEYSPACES"
+                                  | "KEYSPACE" ksname=<keyspaceName>?
+                                  | ( "COLUMNFAMILY" | "TABLE" ) cf=<columnFamilyName>
+                                  | "INDEX" idx=<indexName>
+                                  | "MATERIALIZED" "VIEW" mv=<materializedViewName>
+                                  | ( "COLUMNFAMILIES" | "TABLES" )
+                                  | "FULL"? "SCHEMA"
+                                  | "CLUSTER"
+                                  | "TYPES"
+                                  | "TYPE" ut=<userTypeName>
+                                  | (ksname=<keyspaceName> | cf=<columnFamilyName> | idx=<indexName> | mv=<materializedViewName>))
+                    ;
+'''
+
+cqlsh_consistency_cmd_syntax_rules = r'''
+<consistencyCommand> ::= "CONSISTENCY" ( level=<consistencyLevel> )?
+                       ;
+'''
+
+cqlsh_consistency_level_syntax_rules = r'''
+<consistencyLevel> ::= "ANY"
+                     | "ONE"
+                     | "TWO"
+                     | "THREE"
+                     | "QUORUM"
+                     | "ALL"
+                     | "LOCAL_QUORUM"
+                     | "EACH_QUORUM"
+                     | "SERIAL"
+                     | "LOCAL_SERIAL"
+                     | "LOCAL_ONE"
+                     ;
+'''
+
+cqlsh_serial_consistency_cmd_syntax_rules = r'''
+<serialConsistencyCommand> ::= "SERIAL" "CONSISTENCY" ( level=<serialConsistencyLevel> )?
+                             ;
+'''
+
+cqlsh_serial_consistency_level_syntax_rules = r'''
+<serialConsistencyLevel> ::= "SERIAL"
+                           | "LOCAL_SERIAL"
+                           ;
+'''
+
+cqlsh_show_cmd_syntax_rules = r'''
+<showCommand> ::= "SHOW" what=( "VERSION" | "HOST" | "SESSION" sessionid=<uuid> )
+                ;
+'''
+
+cqlsh_source_cmd_syntax_rules = r'''
+<sourceCommand> ::= "SOURCE" fname=<stringLiteral>
+                  ;
+'''
+
+cqlsh_capture_cmd_syntax_rules = r'''
+<captureCommand> ::= "CAPTURE" ( fname=( <stringLiteral> | "OFF" ) )?
+                   ;
+'''
+
+cqlsh_copy_cmd_syntax_rules = r'''
+<copyCommand> ::= "COPY" cf=<columnFamilyName>
+                         ( "(" [colnames]=<colname> ( "," [colnames]=<colname> )* ")" )?
+                         ( dir="FROM" ( fname=<stringLiteral> | "STDIN" )
+                         | dir="TO"   ( fname=<stringLiteral> | "STDOUT" ) )
+                         ( "WITH" <copyOption> ( "AND" <copyOption> )* )?
+                ;
+'''
+
+cqlsh_copy_option_syntax_rules = r'''
+<copyOption> ::= [optnames]=(<identifier>|<reserved_identifier>) "=" [optvals]=<copyOptionVal>
+               ;
+'''
+
+cqlsh_copy_option_val_syntax_rules = r'''
+<copyOptionVal> ::= <identifier>
+                  | <reserved_identifier>
+                  | <term>
+                  ;
+'''
+
+cqlsh_debug_cmd_syntax_rules = r'''
+# avoiding just "DEBUG" so that this rule doesn't get treated as a terminal
+<debugCommand> ::= "DEBUG" "THINGS"?
+                 ;
+'''
+
+cqlsh_help_cmd_syntax_rules = r'''
+<helpCommand> ::= ( "HELP" | "?" ) [topic]=( /[a-z_]*/ )*
+                ;
+'''
+
+cqlsh_tracing_cmd_syntax_rules = r'''
+<tracingCommand> ::= "TRACING" ( switch=( "ON" | "OFF" ) )?
+                   ;
+'''
+
+cqlsh_expand_cmd_syntax_rules = r'''
+<expandCommand> ::= "EXPAND" ( switch=( "ON" | "OFF" ) )?
+                   ;
+'''
+
+cqlsh_paging_cmd_syntax_rules = r'''
+<pagingCommand> ::= "PAGING" ( switch=( "ON" | "OFF" | /[0-9]+/) )?
+                  ;
+'''
+
+cqlsh_login_cmd_syntax_rules = r'''
+<loginCommand> ::= "LOGIN" username=<username> (password=<stringLiteral>)?
+                 ;
+'''
+
+cqlsh_exit_cmd_syntax_rules = r'''
+<exitCommand> ::= "exit" | "quit"
+                ;
+'''
+
+cqlsh_clear_cmd_syntax_rules = r'''
+<clearCommand> ::= "CLEAR" | "CLS"
+                 ;
+'''
+
+cqlsh_question_mark = r'''
+<qmark> ::= "?" ;
+'''
+
+cqlsh_extra_syntax_rules = cqlsh_cmd_syntax_rules + \
+    cqlsh_special_cmd_command_syntax_rules + \
+    cqlsh_describe_cmd_syntax_rules + \
+    cqlsh_consistency_cmd_syntax_rules + \
+    cqlsh_consistency_level_syntax_rules + \
+    cqlsh_serial_consistency_cmd_syntax_rules + \
+    cqlsh_serial_consistency_level_syntax_rules + \
+    cqlsh_show_cmd_syntax_rules + \
+    cqlsh_source_cmd_syntax_rules + \
+    cqlsh_capture_cmd_syntax_rules + \
+    cqlsh_copy_cmd_syntax_rules + \
+    cqlsh_copy_option_syntax_rules + \
+    cqlsh_copy_option_val_syntax_rules + \
+    cqlsh_debug_cmd_syntax_rules + \
+    cqlsh_help_cmd_syntax_rules + \
+    cqlsh_tracing_cmd_syntax_rules + \
+    cqlsh_expand_cmd_syntax_rules + \
+    cqlsh_paging_cmd_syntax_rules + \
+    cqlsh_login_cmd_syntax_rules + \
+    cqlsh_exit_cmd_syntax_rules + \
+    cqlsh_clear_cmd_syntax_rules + \
+    cqlsh_question_mark
+
+
+def complete_source_quoted_filename(ctxt, cqlsh):
+    partial_path = ctxt.get_binding('partial', '')
+    head, tail = os.path.split(partial_path)
+    exhead = os.path.expanduser(head)
+    try:
+        contents = os.listdir(exhead or '.')
+    except OSError:
+        return ()
+    matches = filter(lambda f: f.startswith(tail), contents)
+    annotated = []
+    for f in matches:
+        match = os.path.join(head, f)
+        if os.path.isdir(os.path.join(exhead, f)):
+            match += '/'
+        annotated.append(match)
+    return annotated
+
+
+cqlsh_syntax_completer('sourceCommand', 'fname')(complete_source_quoted_filename)
+cqlsh_syntax_completer('captureCommand', 'fname')(complete_source_quoted_filename)
+
+
+@cqlsh_syntax_completer('copyCommand', 'fname')
+def copy_fname_completer(ctxt, cqlsh):
+    lasttype = ctxt.get_binding('*LASTTYPE*')
+    if lasttype == 'unclosedString':
+        return complete_source_quoted_filename(ctxt, cqlsh)
+    partial_path = ctxt.get_binding('partial')
+    if partial_path == '':
+        return ["'"]
+    return ()
+
+
+@cqlsh_syntax_completer('copyCommand', 'colnames')
+def complete_copy_column_names(ctxt, cqlsh):
+    existcols = map(cqlsh.cql_unprotect_name, ctxt.get_binding('colnames', ()))
+    ks = cqlsh.cql_unprotect_name(ctxt.get_binding('ksname', None))
+    cf = cqlsh.cql_unprotect_name(ctxt.get_binding('cfname'))
+    colnames = cqlsh.get_column_names(ks, cf)
+    if len(existcols) == 0:
+        return [colnames[0]]
+    return set(colnames[1:]) - set(existcols)
+
+
+COPY_COMMON_OPTIONS = ['DELIMITER', 'QUOTE', 'ESCAPE', 'HEADER', 'NULL', 'DATETIMEFORMAT',
+                       'MAXATTEMPTS', 'REPORTFREQUENCY', 'DECIMALSEP', 'THOUSANDSSEP', 'BOOLSTYLE',
+                       'NUMPROCESSES', 'CONFIGFILE', 'RATEFILE']
+COPY_FROM_OPTIONS = ['CHUNKSIZE', 'INGESTRATE', 'MAXBATCHSIZE', 'MINBATCHSIZE', 'MAXROWS',
+                     'SKIPROWS', 'SKIPCOLS', 'MAXPARSEERRORS', 'MAXINSERTERRORS', 'ERRFILE', 'PREPAREDSTATEMENTS',
+                     'TTL']
+COPY_TO_OPTIONS = ['ENCODING', 'PAGESIZE', 'PAGETIMEOUT', 'BEGINTOKEN', 'ENDTOKEN', 'MAXOUTPUTSIZE', 'MAXREQUESTS',
+                   'FLOATPRECISION', 'DOUBLEPRECISION']
+
+
+@cqlsh_syntax_completer('copyOption', 'optnames')
+def complete_copy_options(ctxt, cqlsh):
+    optnames = map(str.upper, ctxt.get_binding('optnames', ()))
+    direction = ctxt.get_binding('dir').upper()
+    if direction == 'FROM':
+        opts = set(COPY_COMMON_OPTIONS + COPY_FROM_OPTIONS) - set(optnames)
+    elif direction == 'TO':
+        opts = set(COPY_COMMON_OPTIONS + COPY_TO_OPTIONS) - set(optnames)
+    return opts
+
+
+@cqlsh_syntax_completer('copyOption', 'optvals')
+def complete_copy_opt_values(ctxt, cqlsh):
+    optnames = ctxt.get_binding('optnames', ())
+    lastopt = optnames[-1].lower()
+    if lastopt == 'header':
+        return ['true', 'false']
+    return [cqlhandling.Hint('<single_character_string>')]
+
+
+@cqlsh_syntax_completer('helpCommand', 'topic')
+def complete_help(ctxt, cqlsh):
+    return sorted([t.upper() for t in cqlsh.cqldocs.get_help_topics() + cqlsh.get_help_topics()])
diff --git a/pylib/cqlshlib/displaying.py b/pylib/cqlshlib/displaying.py
index 521e911..424d633 100644
--- a/pylib/cqlshlib/displaying.py
+++ b/pylib/cqlshlib/displaying.py
@@ -114,6 +114,7 @@
     inet=GREEN,
     boolean=GREEN,
     uuid=GREEN,
+    duration=GREEN,
     collection=BLUE,
     reset=ANSI_RESET,
 )
diff --git a/pylib/cqlshlib/formatting.py b/pylib/cqlshlib/formatting.py
index 9657305..4f60a86 100644
--- a/pylib/cqlshlib/formatting.py
+++ b/pylib/cqlshlib/formatting.py
@@ -16,9 +16,12 @@
 
 import binascii
 import calendar
+import datetime
 import math
+import os
 import re
 import sys
+import six
 import platform
 import wcwidth
 
@@ -30,8 +33,8 @@
 
 is_win = platform.system() == 'Windows'
 
-unicode_controlchars_re = re.compile(r'[\x00-\x31\x7f-\xa0]')
-controlchars_re = re.compile(r'[\x00-\x31\x7f-\xff]')
+unicode_controlchars_re = re.compile(r'[\x00-\x1f\x7f-\xa0]')
+controlchars_re = re.compile(r'[\x00-\x1f\x7f-\xff]')
 
 
 def _show_control_chars(match):
@@ -61,7 +64,7 @@
 empty_colormap = defaultdict(lambda: '')
 
 
-def format_by_type(cqltype, val, encoding, colormap=None, addcolor=False,
+def format_by_type(val, cqltype, encoding, colormap=None, addcolor=False,
                    nullval=None, date_time_format=None, float_precision=None,
                    decimal_sep=None, thousands_sep=None, boolean_styles=None):
     if nullval is None:
@@ -76,7 +79,7 @@
         date_time_format = DateTimeFormat()
     if float_precision is None:
         float_precision = default_float_precision
-    return format_value(cqltype, val, encoding=encoding, colormap=colormap,
+    return format_value(val, cqltype=cqltype, encoding=encoding, colormap=colormap,
                         date_time_format=date_time_format, float_precision=float_precision,
                         nullval=nullval, decimal_sep=decimal_sep, thousands_sep=thousands_sep,
                         boolean_styles=boolean_styles)
@@ -102,20 +105,102 @@
 
 DEFAULT_NANOTIME_FORMAT = '%H:%M:%S.%N'
 DEFAULT_DATE_FORMAT = '%Y-%m-%d'
-DEFAULT_TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S%z'
 
-if platform.system() == 'Windows':
-    DEFAULT_TIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z'
+DEFAULT_TIMESTAMP_FORMAT = os.environ.get('CQLSH_DEFAULT_TIMESTAMP_FORMAT', '')
+if not DEFAULT_TIMESTAMP_FORMAT:
+    DEFAULT_TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S.%f%z'
 
 
-class DateTimeFormat():
+class DateTimeFormat:
 
     def __init__(self, timestamp_format=DEFAULT_TIMESTAMP_FORMAT, date_format=DEFAULT_DATE_FORMAT,
-                 nanotime_format=DEFAULT_NANOTIME_FORMAT, timezone=None):
+                 nanotime_format=DEFAULT_NANOTIME_FORMAT, timezone=None, milliseconds_only=False):
         self.timestamp_format = timestamp_format
         self.date_format = date_format
         self.nanotime_format = nanotime_format
         self.timezone = timezone
+        self.milliseconds_only = milliseconds_only  # the microseconds part, .NNNNNN, wil be rounded to .NNN
+
+
+class CqlType(object):
+    """
+    A class for converting a string into a cql type name that can match a formatter
+    and a list of its sub-types, if any.
+    """
+    pattern = re.compile('^([^<]*)<(.*)>$')  # *<*>
+
+    def __init__(self, typestring, ksmeta=None):
+        self.type_name, self.sub_types, self.formatter = self.parse(typestring, ksmeta)
+
+    def __str__(self):
+        return "%s%s" % (self.type_name, self.sub_types or '')
+
+    __repr__ = __str__
+
+    def get_n_sub_types(self, num):
+        """
+        Return the sub-types if the requested number matches the length of the sub-types (tuples)
+        or the first sub-type times the number requested if the length of the sub-types is one (list, set),
+        otherwise raise an exception
+        """
+        if len(self.sub_types) == num:
+            return self.sub_types
+        elif len(self.sub_types) == 1:
+            return [self.sub_types[0]] * num
+        else:
+            raise Exception("Unexpected number of subtypes %d - %s" % (num, self.sub_types))
+
+    def parse(self, typestring, ksmeta):
+        """
+        Parse the typestring by looking at this pattern: *<*>. If there is no match then the type
+        is either a simple type or a user type, otherwise it must be a composite type
+        for which we need to look-up the sub-types. For user types the sub types can be extracted
+        from the keyspace metadata.
+        """
+        while True:
+            m = self.pattern.match(typestring)
+            if not m:  # no match, either a simple or a user type
+                name = typestring
+                if ksmeta and name in ksmeta.user_types:  # a user type, look at ks meta for sub types
+                    sub_types = [CqlType(t, ksmeta) for t in ksmeta.user_types[name].field_types]
+                    return name, sub_types, format_value_utype
+                else:
+                    return name, [], self._get_formatter(name)
+            else:
+                if m.group(1) == 'frozen':  # ignore frozen<>
+                    typestring = m.group(2)
+                    continue
+
+                name = m.group(1)  # a composite type, parse sub types
+                return name, self.parse_sub_types(m.group(2), ksmeta), self._get_formatter(name)
+
+    @staticmethod
+    def _get_formatter(name):
+        return _formatters.get(name.lower())
+
+    @staticmethod
+    def parse_sub_types(val, ksmeta):
+        """
+        Split val into sub-strings separated by commas but only if not within a <> pair
+        Return a list of CqlType instances where each instance is initialized with the sub-strings
+        that were found.
+        """
+        last = 0
+        level = 0
+        ret = []
+        for i, c in enumerate(val):
+            if c == '<':
+                level += 1
+            elif c == '>':
+                level -= 1
+            elif c == ',' and level == 0:
+                ret.append(val[last:i].strip())
+                last = i + 1
+
+        if last < len(val) - 1:
+            ret.append(val[last:].strip())
+
+        return [CqlType(r, ksmeta) for r in ret]
 
 
 def format_value_default(val, colormap, **_):
@@ -130,20 +215,24 @@
 _formatters = {}
 
 
-def format_value(type, val, **kwargs):
+def format_value(val, cqltype, **kwargs):
     if val == EMPTY:
         return format_value_default('', **kwargs)
-    formatter = _formatters.get(type.__name__, format_value_default)
-    return formatter(val, **kwargs)
+
+    formatter = get_formatter(val, cqltype)
+    return formatter(val, cqltype=cqltype, **kwargs)
 
 
-def get_formatter(type):
-    return _formatters.get(type.__name__, format_value_default)
+def get_formatter(val, cqltype):
+    if cqltype and cqltype.formatter:
+        return cqltype.formatter
+
+    return _formatters.get(type(val).__name__.lower(), format_value_default)
 
 
 def formatter_for(typname):
     def registrator(f):
-        _formatters[typname] = f
+        _formatters[typname.lower()] = f
         return f
     return registrator
 
@@ -164,6 +253,7 @@
 
 formatter_for('bytearray')(format_value_blob)
 formatter_for('buffer')(format_value_blob)
+formatter_for('blob')(format_value_blob)
 
 
 def format_python_formatted_type(val, colormap, color, quote=False):
@@ -185,6 +275,9 @@
     return format_python_formatted_type(val, colormap, 'uuid')
 
 
+formatter_for('timeuuid')(format_value_uuid)
+
+
 @formatter_for('inet')
 def formatter_value_inet(val, colormap, quote=False, **_):
     return format_python_formatted_type(val, colormap, 'inet', quote=quote)
@@ -197,6 +290,9 @@
     return format_python_formatted_type(val, colormap, 'boolean')
 
 
+formatter_for('boolean')(format_value_boolean)
+
+
 def format_floating_point_type(val, colormap, float_precision, decimal_sep=None, thousands_sep=None, **_):
     if math.isnan(val):
         bval = 'NaN'
@@ -225,6 +321,7 @@
 
 
 formatter_for('float')(format_floating_point_type)
+formatter_for('double')(format_floating_point_type)
 
 
 def format_integer_type(val, colormap, thousands_sep=None, **_):
@@ -249,18 +346,34 @@
 
 formatter_for('long')(format_integer_type)
 formatter_for('int')(format_integer_type)
+formatter_for('bigint')(format_integer_type)
+formatter_for('varint')(format_integer_type)
+formatter_for('duration')(format_integer_type)
 
 
 @formatter_for('datetime')
 def format_value_timestamp(val, colormap, date_time_format, quote=False, **_):
-    bval = strftime(date_time_format.timestamp_format, calendar.timegm(val.utctimetuple()), timezone=date_time_format.timezone)
+    if isinstance(val, datetime.datetime):
+        bval = strftime(date_time_format.timestamp_format,
+                        calendar.timegm(val.utctimetuple()),
+                        microseconds=val.microsecond,
+                        timezone=date_time_format.timezone)
+        if date_time_format.milliseconds_only:
+            bval = round_microseconds(bval)
+    else:
+        bval = str(val)
+
     if quote:
         bval = "'%s'" % bval
     return colorme(bval, colormap, 'timestamp')
 
 
-def strftime(time_format, seconds, timezone=None):
-    ret_dt = datetime_from_timestamp(seconds).replace(tzinfo=UTC())
+formatter_for('timestamp')(format_value_timestamp)
+
+
+def strftime(time_format, seconds, microseconds=0, timezone=None):
+    ret_dt = datetime_from_timestamp(seconds) + datetime.timedelta(microseconds=microseconds)
+    ret_dt = ret_dt.replace(tzinfo=UTC())
     if timezone:
         ret_dt = ret_dt.astimezone(timezone)
     try:
@@ -274,6 +387,24 @@
         return '%d' % (seconds * 1000.0)
 
 
+microseconds_regex = re.compile(r"(.*)(?:\.(\d{1,6}))(.*)")
+
+
+def round_microseconds(val):
+    """
+    For COPY TO, we need to round microsecond to milliseconds because server side
+    TimestampSerializer.dateStringPatterns only parses milliseconds. If we keep microseconds,
+    users may try to import with COPY FROM a file generated with COPY TO and have problems if
+    prepared statements are disabled, see CASSANDRA-11631.
+    """
+    m = microseconds_regex.match(val)
+    if not m:
+        return val
+
+    milliseconds = int(m.group(2)) * pow(10, 3 - len(m.group(2)))
+    return '%s.%03d%s' % (m.group(1), milliseconds, '' if not m.group(3) else m.group(3))
+
+
 @formatter_for('Date')
 def format_value_date(val, colormap, **_):
     return format_python_formatted_type(val, colormap, 'date')
@@ -284,6 +415,75 @@
     return format_python_formatted_type(val, colormap, 'time')
 
 
+@formatter_for('Duration')
+def format_value_duration(val, colormap, **_):
+    return format_python_formatted_type(duration_as_str(val.months, val.days, val.nanoseconds), colormap, 'duration')
+
+
+def duration_as_str(months, days, nanoseconds):
+    builder = list()
+    if months < 0 or days < 0 or nanoseconds < 0:
+        builder.append('-')
+
+    remainder = append(builder, abs(months), MONTHS_PER_YEAR, "y")
+    append(builder, remainder, 1, "mo")
+    append(builder, abs(days), 1, "d")
+
+    if nanoseconds != 0:
+        remainder = append(builder, abs(nanoseconds), NANOS_PER_HOUR, "h")
+        remainder = append(builder, remainder, NANOS_PER_MINUTE, "m")
+        remainder = append(builder, remainder, NANOS_PER_SECOND, "s")
+        remainder = append(builder, remainder, NANOS_PER_MILLI, "ms")
+        remainder = append(builder, remainder, NANOS_PER_MICRO, "us")
+        append(builder, remainder, 1, "ns")
+
+    return ''.join(builder)
+
+
+def append(builder, dividend, divisor, unit):
+    if dividend == 0 or dividend < divisor:
+        return dividend
+
+    builder.append(str(dividend / divisor))
+    builder.append(unit)
+    return dividend % divisor
+
+
+def decode_vint(buf):
+    return decode_zig_zag_64(decode_unsigned_vint(buf))
+
+
+def decode_unsigned_vint(buf):
+    """
+    Cassandra vints are encoded differently than the varints used in protocol buffer.
+    The Cassandra vints are encoded with the most significant group first. The most significant byte will contains
+    the information about how many extra bytes need to be read as well as the most significant bits of the integer.
+    The number extra bytes to read is encoded as 1 bits on the left side.
+    For example, if we need to read 3 more bytes the first byte will start with 1110.
+    """
+
+    first_byte = buf.next()
+    if (first_byte >> 7) == 0:
+        return first_byte
+
+    size = number_of_extra_bytes_to_read(first_byte)
+    retval = first_byte & (0xff >> size)
+    for i in range(size):
+        b = buf.next()
+        retval <<= 8
+        retval |= b & 0xff
+
+    return retval
+
+
+def number_of_extra_bytes_to_read(b):
+    return 8 - (~b & 0xff).bit_length()
+
+
+def decode_zig_zag_64(n):
+    return (n >> 1) ^ -(n & 1)
+
+
 @formatter_for('str')
 def format_value_text(val, encoding, colormap, quote=False, **_):
     escapedval = val.replace(u'\\', u'\\\\')
@@ -299,16 +499,18 @@
 
 # name alias
 formatter_for('unicode')(format_value_text)
+formatter_for('text')(format_value_text)
+formatter_for('ascii')(format_value_text)
 
 
-def format_simple_collection(val, lbracket, rbracket, encoding,
+def format_simple_collection(val, cqltype, lbracket, rbracket, encoding,
                              colormap, date_time_format, float_precision, nullval,
                              decimal_sep, thousands_sep, boolean_styles):
-    subs = [format_value(type(sval), sval, encoding=encoding, colormap=colormap,
+    subs = [format_value(sval, cqltype=stype, encoding=encoding, colormap=colormap,
                          date_time_format=date_time_format, float_precision=float_precision,
                          nullval=nullval, quote=True, decimal_sep=decimal_sep,
                          thousands_sep=thousands_sep, boolean_styles=boolean_styles)
-            for sval in val]
+            for sval, stype in zip(val, cqltype.get_n_sub_types(len(val)))]
     bval = lbracket + ', '.join(get_str(sval) for sval in subs) + rbracket
     if colormap is NO_COLOR_MAP:
         return bval
@@ -321,25 +523,25 @@
 
 
 @formatter_for('list')
-def format_value_list(val, encoding, colormap, date_time_format, float_precision, nullval,
+def format_value_list(val, cqltype, encoding, colormap, date_time_format, float_precision, nullval,
                       decimal_sep, thousands_sep, boolean_styles, **_):
-    return format_simple_collection(val, '[', ']', encoding, colormap,
+    return format_simple_collection(val, cqltype, '[', ']', encoding, colormap,
                                     date_time_format, float_precision, nullval,
                                     decimal_sep, thousands_sep, boolean_styles)
 
 
 @formatter_for('tuple')
-def format_value_tuple(val, encoding, colormap, date_time_format, float_precision, nullval,
+def format_value_tuple(val, cqltype, encoding, colormap, date_time_format, float_precision, nullval,
                        decimal_sep, thousands_sep, boolean_styles, **_):
-    return format_simple_collection(val, '(', ')', encoding, colormap,
+    return format_simple_collection(val, cqltype, '(', ')', encoding, colormap,
                                     date_time_format, float_precision, nullval,
                                     decimal_sep, thousands_sep, boolean_styles)
 
 
 @formatter_for('set')
-def format_value_set(val, encoding, colormap, date_time_format, float_precision, nullval,
+def format_value_set(val, cqltype, encoding, colormap, date_time_format, float_precision, nullval,
                      decimal_sep, thousands_sep, boolean_styles, **_):
-    return format_simple_collection(sorted(val), '{', '}', encoding, colormap,
+    return format_simple_collection(sorted(val), cqltype, '{', '}', encoding, colormap,
                                     date_time_format, float_precision, nullval,
                                     decimal_sep, thousands_sep, boolean_styles)
 
@@ -350,15 +552,15 @@
 
 
 @formatter_for('dict')
-def format_value_map(val, encoding, colormap, date_time_format, float_precision, nullval,
+def format_value_map(val, cqltype, encoding, colormap, date_time_format, float_precision, nullval,
                      decimal_sep, thousands_sep, boolean_styles, **_):
-    def subformat(v):
-        return format_value(type(v), v, encoding=encoding, colormap=colormap,
+    def subformat(v, t):
+        return format_value(v, cqltype=t, encoding=encoding, colormap=colormap,
                             date_time_format=date_time_format, float_precision=float_precision,
                             nullval=nullval, quote=True, decimal_sep=decimal_sep,
                             thousands_sep=thousands_sep, boolean_styles=boolean_styles)
 
-    subs = [(subformat(k), subformat(v)) for (k, v) in sorted(val.items())]
+    subs = [(subformat(k, cqltype.sub_types[0]), subformat(v, cqltype.sub_types[1])) for (k, v) in sorted(val.items())]
     bval = '{' + ', '.join(get_str(k) + ': ' + get_str(v) for (k, v) in subs) + '}'
     if colormap is NO_COLOR_MAP:
         return bval
@@ -375,14 +577,15 @@
 formatter_for('OrderedDict')(format_value_map)
 formatter_for('OrderedMap')(format_value_map)
 formatter_for('OrderedMapSerializedKey')(format_value_map)
+formatter_for('map')(format_value_map)
 
 
-def format_value_utype(val, encoding, colormap, date_time_format, float_precision, nullval,
+def format_value_utype(val, cqltype, encoding, colormap, date_time_format, float_precision, nullval,
                        decimal_sep, thousands_sep, boolean_styles, **_):
-    def format_field_value(v):
+    def format_field_value(v, t):
         if v is None:
             return colorme(nullval, colormap, 'error')
-        return format_value(type(v), v, encoding=encoding, colormap=colormap,
+        return format_value(v, cqltype=t, encoding=encoding, colormap=colormap,
                             date_time_format=date_time_format, float_precision=float_precision,
                             nullval=nullval, quote=True, decimal_sep=decimal_sep,
                             thousands_sep=thousands_sep, boolean_styles=boolean_styles)
@@ -390,7 +593,8 @@
     def format_field_name(name):
         return format_value_text(name, encoding=encoding, colormap=colormap, quote=False)
 
-    subs = [(format_field_name(k), format_field_value(v)) for (k, v) in val._asdict().items()]
+    subs = [(format_field_name(k), format_field_value(v, t)) for ((k, v), t) in zip(val._asdict().items(),
+                                                                                    cqltype.sub_types)]
     bval = '{' + ', '.join(get_str(k) + ': ' + get_str(v) for (k, v) in subs) + '}'
     if colormap is NO_COLOR_MAP:
         return bval
@@ -402,3 +606,11 @@
         + rb
     displaywidth = 4 * len(subs) + sum(k.displaywidth + v.displaywidth for (k, v) in subs)
     return FormattedValue(bval, coloredval, displaywidth)
+
+
+NANOS_PER_MICRO = 1000
+NANOS_PER_MILLI = 1000 * NANOS_PER_MICRO
+NANOS_PER_SECOND = 1000 * NANOS_PER_MILLI
+NANOS_PER_MINUTE = 60 * NANOS_PER_SECOND
+NANOS_PER_HOUR = 60 * NANOS_PER_MINUTE
+MONTHS_PER_YEAR = 12
diff --git a/pylib/cqlshlib/test/basecase.py b/pylib/cqlshlib/test/basecase.py
index d393769..2c50b9a 100644
--- a/pylib/cqlshlib/test/basecase.py
+++ b/pylib/cqlshlib/test/basecase.py
@@ -30,13 +30,14 @@
 except ImportError:
     import unittest
 
-rundir = dirname(__file__)
-cqlshdir = normpath(join(rundir, '..', '..', '..', 'bin'))
-path_to_cqlsh = normpath(join(cqlshdir, 'cqlsh.py'))
+test_dir = dirname(__file__)
+cassandra_dir = normpath(join(test_dir, '..', '..', '..'))
+cqlsh_dir = join(cassandra_dir, 'bin')
 
-sys.path.append(cqlshdir)
+sys.path.append(cqlsh_dir)
 
 import cqlsh
+
 cql = cqlsh.cassandra.cluster.Cluster
 policy = cqlsh.cassandra.policies.RoundRobinPolicy()
 quote_name = cqlsh.cassandra.metadata.maybe_escape_name
@@ -44,6 +45,7 @@
 TEST_HOST = os.environ.get('CQL_TEST_HOST', '127.0.0.1')
 TEST_PORT = int(os.environ.get('CQL_TEST_PORT', 9042))
 
+
 class BaseTestCase(unittest.TestCase):
     def assertNicelyFormattedTableHeader(self, line, msg=None):
         return self.assertRegexpMatches(line, r'^ +\w+( +\| \w+)*\s*$', msg=msg)
@@ -54,6 +56,7 @@
     def assertNicelyFormattedTableData(self, line, msg=None):
         return self.assertRegexpMatches(line, r'^ .* \| ', msg=msg)
 
+
 def dedent(s):
     lines = [ln.rstrip() for ln in s.splitlines()]
     if lines[0] == '':
@@ -62,5 +65,6 @@
     minspace = min(spaces if len(spaces) > 0 else (0,))
     return '\n'.join(line[minspace:] for line in lines)
 
+
 def at_a_time(i, num):
     return izip(*([iter(i)] * num))
diff --git a/pylib/cqlshlib/test/cassconnect.py b/pylib/cqlshlib/test/cassconnect.py
index 501850c..1cc561c 100644
--- a/pylib/cqlshlib/test/cassconnect.py
+++ b/pylib/cqlshlib/test/cassconnect.py
@@ -19,10 +19,10 @@
 import contextlib
 import tempfile
 import os.path
-from .basecase import cql, cqlsh, cqlshlog, TEST_HOST, TEST_PORT, rundir, policy, quote_name
+from .basecase import cql, cqlsh, cqlshlog, TEST_HOST, TEST_PORT, test_dir, policy, quote_name
 from .run_cqlsh import run_cqlsh, call_cqlsh
 
-test_keyspace_init = os.path.join(rundir, 'test_keyspace_init.cql')
+test_keyspace_init = os.path.join(test_dir, 'test_keyspace_init.cql')
 
 def get_cassandra_connection(cql_version=None):
     conn = cql((TEST_HOST,), TEST_PORT, cql_version=cql_version, load_balancing_policy=policy)
diff --git a/pylib/cqlshlib/test/run_cqlsh.py b/pylib/cqlshlib/test/run_cqlsh.py
index 3db673b..64cceb5 100644
--- a/pylib/cqlshlib/test/run_cqlsh.py
+++ b/pylib/cqlshlib/test/run_cqlsh.py
@@ -189,6 +189,8 @@
                    flags=0, ptty_timeout=None):
         if not isinstance(until, re._pattern_type):
             until = re.compile(until, flags)
+
+        cqlshlog.debug("Searching for %r" % (until.pattern,))
         got = self.readbuf
         self.readbuf = ''
         with timing_out(timeout):
@@ -237,7 +239,7 @@
             cqlsh_bin = 'cqlsh'
             if is_win():
                 cqlsh_bin = 'cqlsh.bat'
-            path = normpath(join(basecase.cqlshdir, cqlsh_bin))
+            path = join(basecase.cqlsh_dir, cqlsh_bin)
         if host is None:
             host = basecase.TEST_HOST
         if port is None:
diff --git a/pylib/cqlshlib/test/test_constants.py b/pylib/cqlshlib/test/test_constants.py
new file mode 100644
index 0000000..8107ffd
--- /dev/null
+++ b/pylib/cqlshlib/test/test_constants.py
@@ -0,0 +1,37 @@
+# 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.
+
+from os.path import join
+
+from .basecase import BaseTestCase, cassandra_dir
+from cqlshlib.cqlhandling import cql_keywords_reserved as cql_keywords_reserved
+
+RESERVED_KEYWORDS_SOURCE = join(cassandra_dir, 'src', 'resources', 'org', 'apache', 'cassandra', 'cql3', 'reserved_keywords.txt')
+
+
+class TestConstants(BaseTestCase):
+
+    def test_cql_reserved_keywords(self):
+        with open(RESERVED_KEYWORDS_SOURCE) as f:
+            source_reserved_keywords = set(line.rstrip().lower() for line in f)
+
+        cqlsh_not_source = cql_keywords_reserved - source_reserved_keywords
+        self.assertFalse(cqlsh_not_source, "Reserved keywords in cqlsh not read from source %s."
+                         % (RESERVED_KEYWORDS_SOURCE,))
+
+        source_not_cqlsh = source_reserved_keywords - cql_keywords_reserved
+        self.assertFalse(source_not_cqlsh, "Reserved keywords in source %s not appearing in cqlsh."
+                         % (RESERVED_KEYWORDS_SOURCE,))
\ No newline at end of file
diff --git a/pylib/cqlshlib/test/test_cqlsh_completion.py b/pylib/cqlshlib/test/test_cqlsh_completion.py
index 45238c1..0f46e3a 100644
--- a/pylib/cqlshlib/test/test_cqlsh_completion.py
+++ b/pylib/cqlshlib/test/test_cqlsh_completion.py
@@ -380,12 +380,12 @@
                             choices=['EXISTS', '<quotedName>', '<identifier>'])
 
         self.trycompletions("UPDATE empty_table SET lonelycol = 'eggs' WHERE TOKEN(lonelykey) <= TOKEN(13) IF EXISTS ",
-                            choices=['>=', '!=', '<=', 'IN', '[', ';', '=', '<', '>'])
+                            choices=['>=', '!=', '<=', 'IN', '[', ';', '=', '<', '>', '.'])
 
     def test_complete_in_delete(self):
         self.trycompletions('DELETE F', choices=['FROM', '<identifier>', '<quotedName>'])
 
-        self.trycompletions('DELETE a ', choices=['FROM', '[', ','])
+        self.trycompletions('DELETE a ', choices=['FROM', '[', '.', ','])
         self.trycompletions('DELETE a [',
                             choices=['<wholenumber>', 'false', '-', '<uuid>',
                                      '<pgStringLiteral>', '<float>', 'TOKEN',
@@ -462,7 +462,7 @@
                             choices=['EXISTS', '<identifier>', '<quotedName>'])
         self.trycompletions(('DELETE FROM twenty_rows_composite_table USING TIMESTAMP 0 WHERE '
                              'TOKEN(a) >= TOKEN(0) IF b '),
-                            choices=['>=', '!=', '<=', 'IN', '[', '=', '<', '>'])
+                            choices=['>=', '!=', '<=', 'IN', '=', '<', '>'])
         self.trycompletions(('DELETE FROM twenty_rows_composite_table USING TIMESTAMP 0 WHERE '
                              'TOKEN(a) >= TOKEN(0) IF b < 0 '),
                             choices=['AND', ';'])
@@ -608,7 +608,7 @@
                                      'memtable_flush_period_in_ms',
                                      'read_repair_chance', 'CLUSTERING',
                                      'COMPACT', 'caching', 'comment',
-                                     'min_index_interval', 'speculative_retry'])
+                                     'min_index_interval', 'speculative_retry', 'cdc'])
         self.trycompletions(prefix + ' new_table (col_a int PRIMARY KEY) WITH ',
                             choices=['bloom_filter_fp_chance', 'compaction',
                                      'compression',
@@ -618,7 +618,7 @@
                                      'memtable_flush_period_in_ms',
                                      'read_repair_chance', 'CLUSTERING',
                                      'COMPACT', 'caching', 'comment',
-                                     'min_index_interval', 'speculative_retry'])
+                                     'min_index_interval', 'speculative_retry', 'cdc'])
         self.trycompletions(prefix + ' new_table (col_a int PRIMARY KEY) WITH bloom_filter_fp_chance ',
                             immediate='= ')
         self.trycompletions(prefix + ' new_table (col_a int PRIMARY KEY) WITH bloom_filter_fp_chance = ',
@@ -652,7 +652,8 @@
                                      'tombstone_compaction_interval',
                                      'tombstone_threshold',
                                      'unchecked_tombstone_compaction',
-                                     'only_purge_repaired_tombstones'])
+                                     'only_purge_repaired_tombstones',
+                                     'provide_overlapping_tombstones'])
         self.trycompletions(prefix + " new_table (col_a int PRIMARY KEY) WITH compaction = "
                             + "{'class': 'SizeTieredCompactionStrategy'}",
                             choices=[';', 'AND'])
@@ -666,22 +667,22 @@
                                      'memtable_flush_period_in_ms',
                                      'read_repair_chance', 'CLUSTERING',
                                      'COMPACT', 'caching', 'comment',
-                                     'min_index_interval', 'speculative_retry'])
+                                     'min_index_interval', 'speculative_retry', 'cdc'])
         self.trycompletions(prefix + " new_table (col_a int PRIMARY KEY) WITH compaction = "
                             + "{'class': 'DateTieredCompactionStrategy', '",
                             choices=['base_time_seconds', 'max_sstable_age_days',
                                      'timestamp_resolution', 'min_threshold', 'class', 'max_threshold',
                                      'tombstone_compaction_interval', 'tombstone_threshold',
                                      'enabled', 'unchecked_tombstone_compaction',
-                                     'max_window_size_seconds', 'only_purge_repaired_tombstones'])
+                                     'max_window_size_seconds',
+                                     'only_purge_repaired_tombstones', 'provide_overlapping_tombstones'])
         self.trycompletions(prefix + " new_table (col_a int PRIMARY KEY) WITH compaction = "
                             + "{'class': 'TimeWindowCompactionStrategy', '",
                             choices=['compaction_window_unit', 'compaction_window_size',
                                      'timestamp_resolution', 'min_threshold', 'class', 'max_threshold',
                                      'tombstone_compaction_interval', 'tombstone_threshold',
                                      'enabled', 'unchecked_tombstone_compaction',
-                                     'only_purge_repaired_tombstones'])
-
+                                     'only_purge_repaired_tombstones','provide_overlapping_tombstones'])
 
     def test_complete_in_create_columnfamily(self):
         self.trycompletions('CREATE C', choices=['COLUMNFAMILY', 'CUSTOM'])
diff --git a/pylib/cqlshlib/test/test_cqlsh_output.py b/pylib/cqlshlib/test/test_cqlsh_output.py
index 38ed294..da16591 100644
--- a/pylib/cqlshlib/test/test_cqlsh_output.py
+++ b/pylib/cqlshlib/test/test_cqlsh_output.py
@@ -373,10 +373,10 @@
             ('''select timestampcol from has_all_types where num = 0;''', """
              timestampcol
              MMMMMMMMMMMM
-            --------------------------
+            ---------------------------------
 
-             2012-05-14 12:53:20+0000
-             GGGGGGGGGGGGGGGGGGGGGGGG
+             2012-05-14 12:53:20.000000+0000
+             GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
 
 
             (1 rows)
@@ -390,10 +390,10 @@
                 ('''select timestampcol from has_all_types where num = 0;''', """
                  timestampcol
                  MMMMMMMMMMMM
-                --------------------------
+                ---------------------------------
 
-                 2012-05-14 09:53:20-0300
-                 GGGGGGGGGGGGGGGGGGGGGGGG
+                 2012-05-14 09:53:20.000000-0300
+                 GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
 
 
                 (1 rows)
@@ -635,6 +635,7 @@
                 varintcol varint
             ) WITH bloom_filter_fp_chance = 0.01
                 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}
+                AND cdc = false
                 AND comment = ''
                 AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'}
                 AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'}
@@ -869,3 +870,18 @@
             nnnnnnnn
             """),
         ))
+
+        self.assertQueriesGiveColoredOutput((
+            ("select tags as my_tags from songs;", r"""
+             my_tags
+             MMMMMMM
+            -------------------------------------------------
+
+             {tags: {'genre': 'metal', 'origin': 'england'}}
+             BYYYYBBBYYYYYYYBBYYYYYYYBBYYYYYYYYBBYYYYYYYYYBB
+
+
+            (1 rows)
+            nnnnnnnn
+            """),
+        ))
diff --git a/pylib/cqlshlib/tracing.py b/pylib/cqlshlib/tracing.py
index cea3568..5b55367 100644
--- a/pylib/cqlshlib/tracing.py
+++ b/pylib/cqlshlib/tracing.py
@@ -16,6 +16,7 @@
 
 from cqlshlib.displaying import MAGENTA
 from datetime import datetime, timedelta
+from formatting import CqlType
 import time
 from cassandra.query import QueryTrace, TraceUnavailable
 
@@ -42,7 +43,7 @@
     if not rows:
         shell.printerr("No rows for session %s found." % (trace.trace_id,))
         return
-    names = ['activity', 'timestamp', 'source', 'source_elapsed']
+    names = ['activity', 'timestamp', 'source', 'source_elapsed', 'client']
 
     formatted_names = map(shell.myformat_colname, names)
     formatted_values = [map(shell.myformat_value, row) for row in rows]
@@ -51,7 +52,7 @@
     shell.writeresult('Tracing session: ', color=MAGENTA, newline=False)
     shell.writeresult(trace.trace_id)
     shell.writeresult('')
-    shell.print_formatted_result(formatted_names, formatted_values)
+    shell.print_formatted_result(formatted_names, formatted_values, with_header=True, tty=shell.tty)
     shell.writeresult('')
 
 
@@ -59,18 +60,19 @@
     if not trace.events:
         return []
 
-    rows = [[trace.request_type, str(datetime_from_utc_to_local(trace.started_at)), trace.coordinator, 0]]
+    rows = [[trace.request_type, str(datetime_from_utc_to_local(trace.started_at)), trace.coordinator, 0, trace.client]]
 
     # append main rows (from events table).
     for event in trace.events:
         rows.append(["%s [%s]" % (event.description, event.thread_name),
                      str(datetime_from_utc_to_local(event.datetime)),
                      event.source,
-                     total_micro_seconds(event.source_elapsed)])
+                     total_micro_seconds(event.source_elapsed),
+                     trace.client])
     # append footer row (from sessions table).
     if trace.duration:
         finished_at = (datetime_from_utc_to_local(trace.started_at) + trace.duration)
-        rows.append(['Request complete', str(finished_at), trace.coordinator, total_micro_seconds(trace.duration)])
+        rows.append(['Request complete', str(finished_at), trace.coordinator, total_micro_seconds(trace.duration), trace.client])
     else:
         finished_at = trace.duration = "--"
 
diff --git a/redhat/cassandra.spec b/redhat/cassandra.spec
index 2a56918..213a791 100644
--- a/redhat/cassandra.spec
+++ b/redhat/cassandra.spec
@@ -194,6 +194,7 @@
 %files tools
 %attr(755,root,root) %{_bindir}/sstabledump
 %attr(755,root,root) %{_bindir}/cassandra-stressd
+%attr(755,root,root) %{_bindir}/compaction-stress
 %attr(755,root,root) %{_bindir}/sstableexpiredblockers
 %attr(755,root,root) %{_bindir}/sstablelevelreset
 %attr(755,root,root) %{_bindir}/sstablemetadata
diff --git a/src/antlr/Cql.g b/src/antlr/Cql.g
new file mode 100644
index 0000000..a11f2fd
--- /dev/null
+++ b/src/antlr/Cql.g
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+
+grammar Cql;
+
+options {
+    language = Java;
+}
+
+import Parser,Lexer;
+
+@header {
+    package org.apache.cassandra.cql3;
+
+    import java.util.ArrayList;
+    import java.util.Arrays;
+    import java.util.Collections;
+    import java.util.EnumSet;
+    import java.util.HashSet;
+    import java.util.HashMap;
+    import java.util.LinkedHashMap;
+    import java.util.List;
+    import java.util.Map;
+    import java.util.Set;
+
+    import org.apache.cassandra.auth.*;
+    import org.apache.cassandra.config.ColumnDefinition;
+    import org.apache.cassandra.cql3.*;
+    import org.apache.cassandra.cql3.restrictions.CustomIndexExpression;
+    import org.apache.cassandra.cql3.statements.*;
+    import org.apache.cassandra.cql3.selection.*;
+    import org.apache.cassandra.cql3.functions.*;
+    import org.apache.cassandra.db.marshal.CollectionType;
+    import org.apache.cassandra.exceptions.ConfigurationException;
+    import org.apache.cassandra.exceptions.InvalidRequestException;
+    import org.apache.cassandra.exceptions.SyntaxException;
+    import org.apache.cassandra.utils.Pair;
+}
+
+@members {
+    public void addErrorListener(ErrorListener listener)
+    {
+        gParser.addErrorListener(listener);
+    }
+
+    public void removeErrorListener(ErrorListener listener)
+    {
+        gParser.removeErrorListener(listener);
+    }
+
+    public void displayRecognitionError(String[] tokenNames, RecognitionException e)
+    {
+        gParser.displayRecognitionError(tokenNames, e);
+    }
+
+    protected void addRecognitionError(String msg)
+    {
+        gParser.addRecognitionError(msg);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // Recovery methods are overridden to avoid wasting work on recovering from errors when the result will be
+    // ignored anyway.
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected Object recoverFromMismatchedToken(IntStream input, int ttype, BitSet follow) throws RecognitionException
+    {
+        throw new MismatchedTokenException(ttype, input);
+    }
+
+    @Override
+    public void recover(IntStream input, RecognitionException re)
+    {
+        // Do nothing.
+    }
+}
+
+@lexer::header {
+    package org.apache.cassandra.cql3;
+
+    import org.apache.cassandra.exceptions.SyntaxException;
+}
+
+@lexer::members {
+    List<Token> tokens = new ArrayList<Token>();
+
+    public void emit(Token token)
+    {
+        state.token = token;
+        tokens.add(token);
+    }
+
+    public Token nextToken()
+    {
+        super.nextToken();
+        if (tokens.size() == 0)
+            return new CommonToken(Token.EOF);
+        return tokens.remove(0);
+    }
+
+    private final List<ErrorListener> listeners = new ArrayList<ErrorListener>();
+
+    public void addErrorListener(ErrorListener listener)
+    {
+        this.listeners.add(listener);
+    }
+
+    public void removeErrorListener(ErrorListener listener)
+    {
+        this.listeners.remove(listener);
+    }
+
+    public void displayRecognitionError(String[] tokenNames, RecognitionException e)
+    {
+        for (int i = 0, m = listeners.size(); i < m; i++)
+            listeners.get(i).syntaxError(this, tokenNames, e);
+    }
+}
+
+query returns [ParsedStatement stmnt]
+    : st=cqlStatement (';')* EOF { $stmnt = st; }
+    ;
diff --git a/src/antlr/Lexer.g b/src/antlr/Lexer.g
new file mode 100644
index 0000000..94b2e8b
--- /dev/null
+++ b/src/antlr/Lexer.g
@@ -0,0 +1,372 @@
+/*
+ * 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.
+ */
+
+lexer grammar Lexer;
+
+@lexer::members {
+    List<Token> tokens = new ArrayList<Token>();
+
+    public void emit(Token token)
+    {
+        state.token = token;
+        tokens.add(token);
+    }
+
+    public Token nextToken()
+    {
+        super.nextToken();
+        if (tokens.size() == 0)
+            return new CommonToken(Token.EOF);
+        return tokens.remove(0);
+    }
+
+    private final List<ErrorListener> listeners = new ArrayList<ErrorListener>();
+
+    public void addErrorListener(ErrorListener listener)
+    {
+        this.listeners.add(listener);
+    }
+
+    public void removeErrorListener(ErrorListener listener)
+    {
+        this.listeners.remove(listener);
+    }
+
+    public void displayRecognitionError(String[] tokenNames, RecognitionException e)
+    {
+        for (int i = 0, m = listeners.size(); i < m; i++)
+            listeners.get(i).syntaxError(this, tokenNames, e);
+    }
+}
+
+// Case-insensitive keywords
+// When adding a new reserved keyword, add entry to o.a.c.cql3.ReservedKeywords and
+// pylib/cqlshlib/cqlhandling.py::cql_keywords_reserved.
+// When adding a new unreserved keyword, add entry to unreserved keywords in Parser.g.
+K_SELECT:      S E L E C T;
+K_FROM:        F R O M;
+K_AS:          A S;
+K_WHERE:       W H E R E;
+K_AND:         A N D;
+K_KEY:         K E Y;
+K_KEYS:        K E Y S;
+K_ENTRIES:     E N T R I E S;
+K_FULL:        F U L L;
+K_INSERT:      I N S E R T;
+K_UPDATE:      U P D A T E;
+K_WITH:        W I T H;
+K_LIMIT:       L I M I T;
+K_PER:         P E R;
+K_PARTITION:   P A R T I T I O N;
+K_USING:       U S I N G;
+K_USE:         U S E;
+K_DISTINCT:    D I S T I N C T;
+K_COUNT:       C O U N T;
+K_SET:         S E T;
+K_BEGIN:       B E G I N;
+K_UNLOGGED:    U N L O G G E D;
+K_BATCH:       B A T C H;
+K_APPLY:       A P P L Y;
+K_TRUNCATE:    T R U N C A T E;
+K_DELETE:      D E L E T E;
+K_IN:          I N;
+K_CREATE:      C R E A T E;
+K_KEYSPACE:    ( K E Y S P A C E
+                 | S C H E M A );
+K_KEYSPACES:   K E Y S P A C E S;
+K_COLUMNFAMILY:( C O L U M N F A M I L Y
+                 | T A B L E );
+K_MATERIALIZED:M A T E R I A L I Z E D;
+K_VIEW:        V I E W;
+K_INDEX:       I N D E X;
+K_CUSTOM:      C U S T O M;
+K_ON:          O N;
+K_TO:          T O;
+K_DROP:        D R O P;
+K_PRIMARY:     P R I M A R Y;
+K_INTO:        I N T O;
+K_VALUES:      V A L U E S;
+K_TIMESTAMP:   T I M E S T A M P;
+K_TTL:         T T L;
+K_CAST:        C A S T;
+K_ALTER:       A L T E R;
+K_RENAME:      R E N A M E;
+K_ADD:         A D D;
+K_TYPE:        T Y P E;
+K_COMPACT:     C O M P A C T;
+K_STORAGE:     S T O R A G E;
+K_ORDER:       O R D E R;
+K_BY:          B Y;
+K_ASC:         A S C;
+K_DESC:        D E S C;
+K_ALLOW:       A L L O W;
+K_FILTERING:   F I L T E R I N G;
+K_IF:          I F;
+K_IS:          I S;
+K_CONTAINS:    C O N T A I N S;
+K_GROUP:       G R O U P;
+
+K_GRANT:       G R A N T;
+K_ALL:         A L L;
+K_PERMISSION:  P E R M I S S I O N;
+K_PERMISSIONS: P E R M I S S I O N S;
+K_OF:          O F;
+K_REVOKE:      R E V O K E;
+K_MODIFY:      M O D I F Y;
+K_AUTHORIZE:   A U T H O R I Z E;
+K_DESCRIBE:    D E S C R I B E;
+K_EXECUTE:     E X E C U T E;
+K_NORECURSIVE: N O R E C U R S I V E;
+K_MBEAN:       M B E A N;
+K_MBEANS:      M B E A N S;
+
+K_USER:        U S E R;
+K_USERS:       U S E R S;
+K_ROLE:        R O L E;
+K_ROLES:       R O L E S;
+K_SUPERUSER:   S U P E R U S E R;
+K_NOSUPERUSER: N O S U P E R U S E R;
+K_PASSWORD:    P A S S W O R D;
+K_LOGIN:       L O G I N;
+K_NOLOGIN:     N O L O G I N;
+K_OPTIONS:     O P T I O N S;
+
+K_CLUSTERING:  C L U S T E R I N G;
+K_ASCII:       A S C I I;
+K_BIGINT:      B I G I N T;
+K_BLOB:        B L O B;
+K_BOOLEAN:     B O O L E A N;
+K_COUNTER:     C O U N T E R;
+K_DECIMAL:     D E C I M A L;
+K_DOUBLE:      D O U B L E;
+K_DURATION:    D U R A T I O N;
+K_FLOAT:       F L O A T;
+K_INET:        I N E T;
+K_INT:         I N T;
+K_SMALLINT:    S M A L L I N T;
+K_TINYINT:     T I N Y I N T;
+K_TEXT:        T E X T;
+K_UUID:        U U I D;
+K_VARCHAR:     V A R C H A R;
+K_VARINT:      V A R I N T;
+K_TIMEUUID:    T I M E U U I D;
+K_TOKEN:       T O K E N;
+K_WRITETIME:   W R I T E T I M E;
+K_DATE:        D A T E;
+K_TIME:        T I M E;
+
+K_NULL:        N U L L;
+K_NOT:         N O T;
+K_EXISTS:      E X I S T S;
+
+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;
+K_FROZEN:      F R O Z E N;
+
+K_FUNCTION:    F U N C T I O N;
+K_FUNCTIONS:   F U N C T I O N S;
+K_AGGREGATE:   A G G R E G A T E;
+K_SFUNC:       S F U N C;
+K_STYPE:       S T Y P E;
+K_FINALFUNC:   F I N A L F U N C;
+K_INITCOND:    I N I T C O N D;
+K_RETURNS:     R E T U R N S;
+K_CALLED:      C A L L E D;
+K_INPUT:       I N P U T;
+K_LANGUAGE:    L A N G U A G E;
+K_OR:          O R;
+K_REPLACE:     R E P L A C E;
+
+K_JSON:        J S O N;
+K_DEFAULT:     D E F A U L T;
+K_UNSET:       U N S E T;
+K_LIKE:        L I K E;
+
+// Case-insensitive alpha characters
+fragment A: ('a'|'A');
+fragment B: ('b'|'B');
+fragment C: ('c'|'C');
+fragment D: ('d'|'D');
+fragment E: ('e'|'E');
+fragment F: ('f'|'F');
+fragment G: ('g'|'G');
+fragment H: ('h'|'H');
+fragment I: ('i'|'I');
+fragment J: ('j'|'J');
+fragment K: ('k'|'K');
+fragment L: ('l'|'L');
+fragment M: ('m'|'M');
+fragment N: ('n'|'N');
+fragment O: ('o'|'O');
+fragment P: ('p'|'P');
+fragment Q: ('q'|'Q');
+fragment R: ('r'|'R');
+fragment S: ('s'|'S');
+fragment T: ('t'|'T');
+fragment U: ('u'|'U');
+fragment V: ('v'|'V');
+fragment W: ('w'|'W');
+fragment X: ('x'|'X');
+fragment Y: ('y'|'Y');
+fragment Z: ('z'|'Z');
+
+STRING_LITERAL
+    @init{
+        StringBuilder txt = new StringBuilder(); // temporary to build pg-style-string
+    }
+    @after{ setText(txt.toString()); }
+    :
+      /* pg-style string literal */
+      (
+        '\$' '\$'
+        ( /* collect all input until '$$' is reached again */
+          {  (input.size() - input.index() > 1)
+               && !"$$".equals(input.substring(input.index(), input.index() + 1)) }?
+             => c=. { txt.appendCodePoint(c); }
+        )*
+        '\$' '\$'
+      )
+      |
+      /* conventional quoted string literal */
+      (
+        '\'' (c=~('\'') { txt.appendCodePoint(c);} | '\'' '\'' { txt.appendCodePoint('\''); })* '\''
+      )
+    ;
+
+QUOTED_NAME
+    @init{ StringBuilder b = new StringBuilder(); }
+    @after{ setText(b.toString()); }
+    : '\"' (c=~('\"') { b.appendCodePoint(c); } | '\"' '\"' { b.appendCodePoint('\"'); })+ '\"'
+    ;
+
+EMPTY_QUOTED_NAME
+    : '\"' '\"'
+    ;
+
+fragment DIGIT
+    : '0'..'9'
+    ;
+
+fragment LETTER
+    : ('A'..'Z' | 'a'..'z')
+    ;
+
+fragment HEX
+    : ('A'..'F' | 'a'..'f' | '0'..'9')
+    ;
+
+fragment EXPONENT
+    : E ('+' | '-')? DIGIT+
+    ;
+
+fragment DURATION_ISO_8601_PERIOD_DESIGNATORS
+    : '-'? 'P' DIGIT+ 'Y' (DIGIT+ 'M')? (DIGIT+ 'D')?
+    | '-'? 'P' DIGIT+ 'M' (DIGIT+ 'D')?
+    | '-'? 'P' DIGIT+ 'D'
+    ;
+
+fragment DURATION_ISO_8601_TIME_DESIGNATORS
+    : 'T' DIGIT+ 'H' (DIGIT+ 'M')? (DIGIT+ 'S')?
+    | 'T' DIGIT+ 'M' (DIGIT+ 'S')?
+    | 'T' DIGIT+ 'S'
+    ;
+
+fragment DURATION_ISO_8601_WEEK_PERIOD_DESIGNATOR
+    : '-'? 'P' DIGIT+ 'W'
+    ;
+
+fragment DURATION_UNIT
+    : Y
+    | M O
+    | W
+    | D
+    | H
+    | M
+    | S
+    | M S
+    | U S
+    | '\u00B5' S
+    | N S
+    ;
+
+INTEGER
+    : '-'? DIGIT+
+    ;
+
+QMARK
+    : '?'
+    ;
+
+/*
+ * Normally a lexer only emits one token at a time, but ours is tricked out
+ * to support multiple (see @lexer::members near the top of the grammar).
+ */
+FLOAT
+    : INTEGER EXPONENT
+    | INTEGER '.' DIGIT* EXPONENT?
+    ;
+
+/*
+ * This has to be before IDENT so it takes precendence over it.
+ */
+BOOLEAN
+    : T R U E | F A L S E
+    ;
+
+DURATION
+    : '-'? DIGIT+ DURATION_UNIT (DIGIT+ DURATION_UNIT)*
+    | '-'? 'P' DIGIT DIGIT DIGIT DIGIT '-' DIGIT DIGIT '-' DIGIT DIGIT 'T' DIGIT DIGIT ':' DIGIT DIGIT ':' DIGIT DIGIT // ISO 8601 "alternative format"
+    | '-'? 'P' DURATION_ISO_8601_TIME_DESIGNATORS
+    | DURATION_ISO_8601_WEEK_PERIOD_DESIGNATOR
+    | DURATION_ISO_8601_PERIOD_DESIGNATORS DURATION_ISO_8601_TIME_DESIGNATORS?
+    ;
+
+IDENT
+    : LETTER (LETTER | DIGIT | '_')*
+    ;
+
+HEXNUMBER
+    : '0' X HEX*
+    ;
+
+UUID
+    : HEX HEX HEX HEX HEX HEX HEX HEX '-'
+      HEX HEX HEX HEX '-'
+      HEX HEX HEX HEX '-'
+      HEX HEX HEX HEX '-'
+      HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX
+    ;
+
+WS
+    : (' ' | '\t' | '\n' | '\r')+ { $channel = HIDDEN; }
+    ;
+
+COMMENT
+    : ('--' | '//') .* ('\n'|'\r') { $channel = HIDDEN; }
+    ;
+
+MULTILINE_COMMENT
+    : '/*' .* '*/' { $channel = HIDDEN; }
+    ;
diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g
new file mode 100644
index 0000000..26074b8
--- /dev/null
+++ b/src/antlr/Parser.g
@@ -0,0 +1,1688 @@
+/*
+ * 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.
+ */
+
+parser grammar Parser;
+
+options {
+    language = Java;
+}
+
+@members {
+    private final List<ErrorListener> listeners = new ArrayList<ErrorListener>();
+    protected final List<ColumnIdentifier> bindVariables = new ArrayList<ColumnIdentifier>();
+
+    public static final Set<String> reservedTypeNames = new HashSet<String>()
+    {{
+        add("byte");
+        add("complex");
+        add("enum");
+        add("date");
+        add("interval");
+        add("macaddr");
+        add("bitstring");
+    }};
+
+    public AbstractMarker.Raw newBindVariables(ColumnIdentifier name)
+    {
+        AbstractMarker.Raw marker = new AbstractMarker.Raw(bindVariables.size());
+        bindVariables.add(name);
+        return marker;
+    }
+
+    public AbstractMarker.INRaw newINBindVariables(ColumnIdentifier name)
+    {
+        AbstractMarker.INRaw marker = new AbstractMarker.INRaw(bindVariables.size());
+        bindVariables.add(name);
+        return marker;
+    }
+
+    public Tuples.Raw newTupleBindVariables(ColumnIdentifier name)
+    {
+        Tuples.Raw marker = new Tuples.Raw(bindVariables.size());
+        bindVariables.add(name);
+        return marker;
+    }
+
+    public Tuples.INRaw newTupleINBindVariables(ColumnIdentifier name)
+    {
+        Tuples.INRaw marker = new Tuples.INRaw(bindVariables.size());
+        bindVariables.add(name);
+        return marker;
+    }
+
+    public Json.Marker newJsonBindVariables(ColumnIdentifier name)
+    {
+        Json.Marker marker = new Json.Marker(bindVariables.size());
+        bindVariables.add(name);
+        return marker;
+    }
+
+    public void addErrorListener(ErrorListener listener)
+    {
+        this.listeners.add(listener);
+    }
+
+    public void removeErrorListener(ErrorListener listener)
+    {
+        this.listeners.remove(listener);
+    }
+
+    public void displayRecognitionError(String[] tokenNames, RecognitionException e)
+    {
+        for (int i = 0, m = listeners.size(); i < m; i++)
+            listeners.get(i).syntaxError(this, tokenNames, e);
+    }
+
+    protected void addRecognitionError(String msg)
+    {
+        for (int i = 0, m = listeners.size(); i < m; i++)
+            listeners.get(i).syntaxError(this, msg);
+    }
+
+    public Map<String, String> convertPropertyMap(Maps.Literal map)
+    {
+        if (map == null || map.entries == null || map.entries.isEmpty())
+            return Collections.<String, String>emptyMap();
+
+        Map<String, String> res = new HashMap<>(map.entries.size());
+
+        for (Pair<Term.Raw, Term.Raw> entry : map.entries)
+        {
+            // Because the parser tries to be smart and recover on error (to
+            // allow displaying more than one error I suppose), we have null
+            // entries in there. Just skip those, a proper error will be thrown in the end.
+            if (entry.left == null || entry.right == null)
+                break;
+
+            if (!(entry.left instanceof Constants.Literal))
+            {
+                String msg = "Invalid property name: " + entry.left;
+                if (entry.left instanceof AbstractMarker.Raw)
+                    msg += " (bind variables are not supported in DDL queries)";
+                addRecognitionError(msg);
+                break;
+            }
+            if (!(entry.right instanceof Constants.Literal))
+            {
+                String msg = "Invalid property value: " + entry.right + " for property: " + entry.left;
+                if (entry.right instanceof AbstractMarker.Raw)
+                    msg += " (bind variables are not supported in DDL queries)";
+                addRecognitionError(msg);
+                break;
+            }
+
+            if (res.put(((Constants.Literal)entry.left).getRawText(), ((Constants.Literal)entry.right).getRawText()) != null)
+            {
+                addRecognitionError(String.format("Multiple definition for property " + ((Constants.Literal)entry.left).getRawText()));
+            }
+        }
+
+        return res;
+    }
+
+    public void addRawUpdate(List<Pair<ColumnDefinition.Raw, Operation.RawUpdate>> operations, ColumnDefinition.Raw key, Operation.RawUpdate update)
+    {
+        for (Pair<ColumnDefinition.Raw, Operation.RawUpdate> p : operations)
+        {
+            if (p.left.equals(key) && !p.right.isCompatibleWith(update))
+                addRecognitionError("Multiple incompatible setting of column " + key);
+        }
+        operations.add(Pair.create(key, update));
+    }
+
+    public Set<Permission> filterPermissions(Set<Permission> permissions, IResource resource)
+    {
+        if (resource == null)
+            return Collections.emptySet();
+        Set<Permission> filtered = new HashSet<>(permissions);
+        filtered.retainAll(resource.applicablePermissions());
+        if (filtered.isEmpty())
+            addRecognitionError("Resource type " + resource.getClass().getSimpleName() +
+                                    " does not support any of the requested permissions");
+
+        return filtered;
+    }
+
+    public String canonicalizeObjectName(String s, boolean enforcePattern)
+    {
+        // these two conditions are here because technically they are valid
+        // ObjectNames, but we want to restrict their use without adding unnecessary
+        // work to JMXResource construction as that also happens on hotter code paths
+        if ("".equals(s))
+            addRecognitionError("Empty JMX object name supplied");
+
+        if ("*:*".equals(s))
+            addRecognitionError("Please use ALL MBEANS instead of wildcard pattern");
+
+        try
+        {
+            javax.management.ObjectName objectName = javax.management.ObjectName.getInstance(s);
+            if (enforcePattern && !objectName.isPattern())
+                addRecognitionError("Plural form used, but non-pattern JMX object name specified (" + s + ")");
+            return objectName.getCanonicalName();
+        }
+        catch (javax.management.MalformedObjectNameException e)
+        {
+          addRecognitionError(s + " is not a valid JMX object name");
+          return s;
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // Recovery methods are overridden to avoid wasting work on recovering from errors when the result will be
+    // ignored anyway.
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected Object recoverFromMismatchedToken(IntStream input, int ttype, BitSet follow) throws RecognitionException
+    {
+        throw new MismatchedTokenException(ttype, input);
+    }
+
+    @Override
+    public void recover(IntStream input, RecognitionException re)
+    {
+        // Do nothing.
+    }
+}
+
+/** STATEMENTS **/
+
+cqlStatement returns [ParsedStatement stmt]
+    @after{ if (stmt != null) stmt.setBoundVariables(bindVariables); }
+    : st1= selectStatement                 { $stmt = st1; }
+    | st2= insertStatement                 { $stmt = st2; }
+    | st3= updateStatement                 { $stmt = st3; }
+    | st4= batchStatement                  { $stmt = st4; }
+    | st5= deleteStatement                 { $stmt = st5; }
+    | st6= useStatement                    { $stmt = st6; }
+    | st7= truncateStatement               { $stmt = st7; }
+    | st8= createKeyspaceStatement         { $stmt = st8; }
+    | st9= createTableStatement            { $stmt = st9; }
+    | st10=createIndexStatement            { $stmt = st10; }
+    | st11=dropKeyspaceStatement           { $stmt = st11; }
+    | st12=dropTableStatement              { $stmt = st12; }
+    | st13=dropIndexStatement              { $stmt = st13; }
+    | st14=alterTableStatement             { $stmt = st14; }
+    | st15=alterKeyspaceStatement          { $stmt = st15; }
+    | st16=grantPermissionsStatement       { $stmt = st16; }
+    | st17=revokePermissionsStatement      { $stmt = st17; }
+    | st18=listPermissionsStatement        { $stmt = st18; }
+    | st19=createUserStatement             { $stmt = st19; }
+    | st20=alterUserStatement              { $stmt = st20; }
+    | st21=dropUserStatement               { $stmt = st21; }
+    | st22=listUsersStatement              { $stmt = st22; }
+    | st23=createTriggerStatement          { $stmt = st23; }
+    | st24=dropTriggerStatement            { $stmt = st24; }
+    | st25=createTypeStatement             { $stmt = st25; }
+    | st26=alterTypeStatement              { $stmt = st26; }
+    | st27=dropTypeStatement               { $stmt = st27; }
+    | st28=createFunctionStatement         { $stmt = st28; }
+    | st29=dropFunctionStatement           { $stmt = st29; }
+    | st30=createAggregateStatement        { $stmt = st30; }
+    | st31=dropAggregateStatement          { $stmt = st31; }
+    | st32=createRoleStatement             { $stmt = st32; }
+    | st33=alterRoleStatement              { $stmt = st33; }
+    | st34=dropRoleStatement               { $stmt = st34; }
+    | st35=listRolesStatement              { $stmt = st35; }
+    | st36=grantRoleStatement              { $stmt = st36; }
+    | st37=revokeRoleStatement             { $stmt = st37; }
+    | st38=createMaterializedViewStatement { $stmt = st38; }
+    | st39=dropMaterializedViewStatement   { $stmt = st39; }
+    | st40=alterMaterializedViewStatement  { $stmt = st40; }
+    ;
+
+/*
+ * USE <KEYSPACE>;
+ */
+useStatement returns [UseStatement stmt]
+    : K_USE ks=keyspaceName { $stmt = new UseStatement(ks); }
+    ;
+
+/**
+ * SELECT <expression>
+ * FROM <CF>
+ * WHERE KEY = "key1" AND COL > 1 AND COL < 100
+ * LIMIT <NUMBER>;
+ */
+selectStatement returns [SelectStatement.RawStatement expr]
+    @init {
+        boolean isDistinct = false;
+        Term.Raw limit = null;
+        Term.Raw perPartitionLimit = null;
+        Map<ColumnDefinition.Raw, Boolean> orderings = new LinkedHashMap<>();
+        List<ColumnDefinition.Raw> groups = new ArrayList<>();
+        boolean allowFiltering = false;
+        boolean isJson = false;
+    }
+    : K_SELECT
+      ( K_JSON { isJson = true; } )?
+      ( ( K_DISTINCT { isDistinct = true; } )? sclause=selectClause )
+      K_FROM cf=columnFamilyName
+      ( K_WHERE wclause=whereClause )?
+      ( K_GROUP K_BY groupByClause[groups] ( ',' groupByClause[groups] )* )?
+      ( K_ORDER K_BY orderByClause[orderings] ( ',' orderByClause[orderings] )* )?
+      ( K_PER K_PARTITION K_LIMIT rows=intValue { perPartitionLimit = rows; } )?
+      ( K_LIMIT rows=intValue { limit = rows; } )?
+      ( K_ALLOW K_FILTERING  { allowFiltering = true; } )?
+      {
+          SelectStatement.Parameters params = new SelectStatement.Parameters(orderings,
+                                                                             groups,
+                                                                             isDistinct,
+                                                                             allowFiltering,
+                                                                             isJson);
+          WhereClause where = wclause == null ? WhereClause.empty() : wclause.build();
+          $expr = new SelectStatement.RawStatement(cf, params, sclause, where, limit, perPartitionLimit);
+      }
+    ;
+
+selectClause returns [List<RawSelector> expr]
+    : t1=selector { $expr = new ArrayList<RawSelector>(); $expr.add(t1); } (',' tN=selector { $expr.add(tN); })*
+    | '\*' { $expr = Collections.<RawSelector>emptyList();}
+    ;
+
+selector returns [RawSelector s]
+    @init{ ColumnIdentifier alias = null; }
+    : us=unaliasedSelector (K_AS c=noncol_ident { alias = c; })? { $s = new RawSelector(us, alias); }
+    ;
+
+/*
+ * A single selection. The core of it is selecting a column, but we also allow any term and function, as well as
+ * sub-element selection for UDT.
+ */
+unaliasedSelector returns [Selectable.Raw s]
+    @init { Selectable.Raw tmp = null; }
+    :  ( c=cident                                  { tmp = c; }
+       | v=value                                   { tmp = new Selectable.WithTerm.Raw(v); }
+       | '(' ct=comparatorType ')' v=value         { tmp = new Selectable.WithTerm.Raw(new TypeCast(ct, v)); }
+       | K_COUNT '(' '\*' ')'                      { tmp = Selectable.WithFunction.Raw.newCountRowsFunction(); }
+       | K_WRITETIME '(' c=cident ')'              { tmp = new Selectable.WritetimeOrTTL.Raw(c, true); }
+       | K_TTL       '(' c=cident ')'              { tmp = new Selectable.WritetimeOrTTL.Raw(c, false); }
+       | K_CAST      '(' sn=unaliasedSelector K_AS t=native_type ')' {tmp = new Selectable.WithCast.Raw(sn, t);}
+       | f=functionName args=selectionFunctionArgs { tmp = new Selectable.WithFunction.Raw(f, args); }
+       ) ( '.' fi=fident { tmp = new Selectable.WithFieldSelection.Raw(tmp, fi); } )* { $s = tmp; }
+    ;
+
+selectionFunctionArgs returns [List<Selectable.Raw> a]
+    : '(' ')' { $a = Collections.emptyList(); }
+    | '(' s1=unaliasedSelector { List<Selectable.Raw> args = new ArrayList<Selectable.Raw>(); args.add(s1); }
+          ( ',' sn=unaliasedSelector { args.add(sn); } )*
+      ')' { $a = args; }
+    ;
+
+whereClause returns [WhereClause.Builder clause]
+    @init{ $clause = new WhereClause.Builder(); }
+    : relationOrExpression[$clause] (K_AND relationOrExpression[$clause])*
+    ;
+
+relationOrExpression [WhereClause.Builder clause]
+    : relation[$clause]
+    | customIndexExpression[$clause]
+    ;
+
+customIndexExpression [WhereClause.Builder clause]
+    @init{IndexName name = new IndexName();}
+    : 'expr(' idxName[name] ',' t=term ')' { clause.add(new CustomIndexExpression(name, t));}
+    ;
+
+orderByClause[Map<ColumnDefinition.Raw, Boolean> orderings]
+    @init{
+        boolean reversed = false;
+    }
+    : c=cident (K_ASC | K_DESC { reversed = true; })? { orderings.put(c, reversed); }
+    ;
+
+groupByClause[List<ColumnDefinition.Raw> groups]
+    : c=cident { groups.add(c); }
+    ;
+
+/**
+ * INSERT INTO <CF> (<column>, <column>, <column>, ...)
+ * VALUES (<value>, <value>, <value>, ...)
+ * USING TIMESTAMP <long>;
+ *
+ */
+insertStatement returns [ModificationStatement.Parsed expr]
+    : K_INSERT K_INTO cf=columnFamilyName
+        ( st1=normalInsertStatement[cf] { $expr = st1; }
+        | K_JSON st2=jsonInsertStatement[cf] { $expr = st2; })
+    ;
+
+normalInsertStatement [CFName cf] returns [UpdateStatement.ParsedInsert expr]
+    @init {
+        Attributes.Raw attrs = new Attributes.Raw();
+        List<ColumnDefinition.Raw> columnNames  = new ArrayList<>();
+        List<Term.Raw> values = new ArrayList<>();
+        boolean ifNotExists = false;
+    }
+    : '(' c1=cident { columnNames.add(c1); }  ( ',' cn=cident { columnNames.add(cn); } )* ')'
+      K_VALUES
+      '(' v1=term { values.add(v1); } ( ',' vn=term { values.add(vn); } )* ')'
+      ( K_IF K_NOT K_EXISTS { ifNotExists = true; } )?
+      ( usingClause[attrs] )?
+      {
+          $expr = new UpdateStatement.ParsedInsert(cf, attrs, columnNames, values, ifNotExists);
+      }
+    ;
+
+jsonInsertStatement [CFName cf] returns [UpdateStatement.ParsedInsertJson expr]
+    @init {
+        Attributes.Raw attrs = new Attributes.Raw();
+        boolean ifNotExists = false;
+        boolean defaultUnset = false;
+    }
+    : val=jsonValue
+      ( K_DEFAULT ( K_NULL | ( { defaultUnset = true; } K_UNSET) ) )?
+      ( K_IF K_NOT K_EXISTS { ifNotExists = true; } )?
+      ( usingClause[attrs] )?
+      {
+          $expr = new UpdateStatement.ParsedInsertJson(cf, attrs, val, defaultUnset, ifNotExists);
+      }
+    ;
+
+jsonValue returns [Json.Raw value]
+    : s=STRING_LITERAL { $value = new Json.Literal($s.text); }
+    | ':' id=noncol_ident     { $value = newJsonBindVariables(id); }
+    | QMARK            { $value = newJsonBindVariables(null); }
+    ;
+
+usingClause[Attributes.Raw attrs]
+    : K_USING usingClauseObjective[attrs] ( K_AND usingClauseObjective[attrs] )*
+    ;
+
+usingClauseObjective[Attributes.Raw attrs]
+    : K_TIMESTAMP ts=intValue { attrs.timestamp = ts; }
+    | K_TTL t=intValue { attrs.timeToLive = t; }
+    ;
+
+/**
+ * UPDATE <CF>
+ * USING TIMESTAMP <long>
+ * SET name1 = value1, name2 = value2
+ * WHERE key = value;
+ * [IF (EXISTS | name = value, ...)];
+ */
+updateStatement returns [UpdateStatement.ParsedUpdate expr]
+    @init {
+        Attributes.Raw attrs = new Attributes.Raw();
+        List<Pair<ColumnDefinition.Raw, Operation.RawUpdate>> operations = new ArrayList<>();
+        boolean ifExists = false;
+    }
+    : K_UPDATE cf=columnFamilyName
+      ( usingClause[attrs] )?
+      K_SET columnOperation[operations] (',' columnOperation[operations])*
+      K_WHERE wclause=whereClause
+      ( K_IF ( K_EXISTS { ifExists = true; } | conditions=updateConditions ))?
+      {
+          $expr = new UpdateStatement.ParsedUpdate(cf,
+                                                   attrs,
+                                                   operations,
+                                                   wclause.build(),
+                                                   conditions == null ? Collections.<Pair<ColumnDefinition.Raw, ColumnCondition.Raw>>emptyList() : conditions,
+                                                   ifExists);
+     }
+    ;
+
+updateConditions returns [List<Pair<ColumnDefinition.Raw, ColumnCondition.Raw>> conditions]
+    @init { conditions = new ArrayList<Pair<ColumnDefinition.Raw, ColumnCondition.Raw>>(); }
+    : columnCondition[conditions] ( K_AND columnCondition[conditions] )*
+    ;
+
+
+/**
+ * DELETE name1, name2
+ * FROM <CF>
+ * USING TIMESTAMP <long>
+ * WHERE KEY = keyname
+   [IF (EXISTS | name = value, ...)];
+ */
+deleteStatement returns [DeleteStatement.Parsed expr]
+    @init {
+        Attributes.Raw attrs = new Attributes.Raw();
+        List<Operation.RawDeletion> columnDeletions = Collections.emptyList();
+        boolean ifExists = false;
+    }
+    : K_DELETE ( dels=deleteSelection { columnDeletions = dels; } )?
+      K_FROM cf=columnFamilyName
+      ( usingClauseDelete[attrs] )?
+      K_WHERE wclause=whereClause
+      ( K_IF ( K_EXISTS { ifExists = true; } | conditions=updateConditions ))?
+      {
+          $expr = new DeleteStatement.Parsed(cf,
+                                             attrs,
+                                             columnDeletions,
+                                             wclause.build(),
+                                             conditions == null ? Collections.<Pair<ColumnDefinition.Raw, ColumnCondition.Raw>>emptyList() : conditions,
+                                             ifExists);
+      }
+    ;
+
+deleteSelection returns [List<Operation.RawDeletion> operations]
+    : { $operations = new ArrayList<Operation.RawDeletion>(); }
+          t1=deleteOp { $operations.add(t1); }
+          (',' tN=deleteOp { $operations.add(tN); })*
+    ;
+
+deleteOp returns [Operation.RawDeletion op]
+    : c=cident                { $op = new Operation.ColumnDeletion(c); }
+    | c=cident '[' t=term ']' { $op = new Operation.ElementDeletion(c, t); }
+    | c=cident '.' field=fident { $op = new Operation.FieldDeletion(c, field); }
+    ;
+
+usingClauseDelete[Attributes.Raw attrs]
+    : K_USING K_TIMESTAMP ts=intValue { attrs.timestamp = ts; }
+    ;
+
+/**
+ * BEGIN BATCH
+ *   UPDATE <CF> SET name1 = value1 WHERE KEY = keyname1;
+ *   UPDATE <CF> SET name2 = value2 WHERE KEY = keyname2;
+ *   UPDATE <CF> SET name3 = value3 WHERE KEY = keyname3;
+ *   ...
+ * APPLY BATCH
+ *
+ * OR
+ *
+ * BEGIN BATCH
+ *   INSERT INTO <CF> (KEY, <name>) VALUES ('<key>', '<value>');
+ *   INSERT INTO <CF> (KEY, <name>) VALUES ('<key>', '<value>');
+ *   ...
+ * APPLY BATCH
+ *
+ * OR
+ *
+ * BEGIN BATCH
+ *   DELETE name1, name2 FROM <CF> WHERE key = <key>
+ *   DELETE name3, name4 FROM <CF> WHERE key = <key>
+ *   ...
+ * APPLY BATCH
+ */
+batchStatement returns [BatchStatement.Parsed expr]
+    @init {
+        BatchStatement.Type type = BatchStatement.Type.LOGGED;
+        List<ModificationStatement.Parsed> statements = new ArrayList<ModificationStatement.Parsed>();
+        Attributes.Raw attrs = new Attributes.Raw();
+    }
+    : K_BEGIN
+      ( K_UNLOGGED { type = BatchStatement.Type.UNLOGGED; } | K_COUNTER { type = BatchStatement.Type.COUNTER; } )?
+      K_BATCH ( usingClause[attrs] )?
+          ( s=batchStatementObjective ';'? { statements.add(s); } )*
+      K_APPLY K_BATCH
+      {
+          $expr = new BatchStatement.Parsed(type, attrs, statements);
+      }
+    ;
+
+batchStatementObjective returns [ModificationStatement.Parsed statement]
+    : i=insertStatement  { $statement = i; }
+    | u=updateStatement  { $statement = u; }
+    | d=deleteStatement  { $statement = d; }
+    ;
+
+createAggregateStatement returns [CreateAggregateStatement expr]
+    @init {
+        boolean orReplace = false;
+        boolean ifNotExists = false;
+
+        List<CQL3Type.Raw> argsTypes = new ArrayList<>();
+    }
+    : K_CREATE (K_OR K_REPLACE { orReplace = true; })?
+      K_AGGREGATE
+      (K_IF K_NOT K_EXISTS { ifNotExists = true; })?
+      fn=functionName
+      '('
+        (
+          v=comparatorType { argsTypes.add(v); }
+          ( ',' v=comparatorType { argsTypes.add(v); } )*
+        )?
+      ')'
+      K_SFUNC sfunc = allowedFunctionName
+      K_STYPE stype = comparatorType
+      (
+        K_FINALFUNC ffunc = allowedFunctionName
+      )?
+      (
+        K_INITCOND ival = term
+      )?
+      { $expr = new CreateAggregateStatement(fn, argsTypes, sfunc, stype, ffunc, ival, orReplace, ifNotExists); }
+    ;
+
+dropAggregateStatement returns [DropAggregateStatement expr]
+    @init {
+        boolean ifExists = false;
+        List<CQL3Type.Raw> argsTypes = new ArrayList<>();
+        boolean argsPresent = false;
+    }
+    : K_DROP K_AGGREGATE
+      (K_IF K_EXISTS { ifExists = true; } )?
+      fn=functionName
+      (
+        '('
+          (
+            v=comparatorType { argsTypes.add(v); }
+            ( ',' v=comparatorType { argsTypes.add(v); } )*
+          )?
+        ')'
+        { argsPresent = true; }
+      )?
+      { $expr = new DropAggregateStatement(fn, argsTypes, argsPresent, ifExists); }
+    ;
+
+createFunctionStatement returns [CreateFunctionStatement expr]
+    @init {
+        boolean orReplace = false;
+        boolean ifNotExists = false;
+
+        List<ColumnIdentifier> argsNames = new ArrayList<>();
+        List<CQL3Type.Raw> argsTypes = new ArrayList<>();
+        boolean calledOnNullInput = false;
+    }
+    : K_CREATE (K_OR K_REPLACE { orReplace = true; })?
+      K_FUNCTION
+      (K_IF K_NOT K_EXISTS { ifNotExists = true; })?
+      fn=functionName
+      '('
+        (
+          k=noncol_ident v=comparatorType { argsNames.add(k); argsTypes.add(v); }
+          ( ',' k=noncol_ident v=comparatorType { argsNames.add(k); argsTypes.add(v); } )*
+        )?
+      ')'
+      ( (K_RETURNS K_NULL) | (K_CALLED { calledOnNullInput=true; })) K_ON K_NULL K_INPUT
+      K_RETURNS rt = comparatorType
+      K_LANGUAGE language = IDENT
+      K_AS body = STRING_LITERAL
+      { $expr = new CreateFunctionStatement(fn, $language.text.toLowerCase(), $body.text,
+                                            argsNames, argsTypes, rt, calledOnNullInput, orReplace, ifNotExists); }
+    ;
+
+dropFunctionStatement returns [DropFunctionStatement expr]
+    @init {
+        boolean ifExists = false;
+        List<CQL3Type.Raw> argsTypes = new ArrayList<>();
+        boolean argsPresent = false;
+    }
+    : K_DROP K_FUNCTION
+      (K_IF K_EXISTS { ifExists = true; } )?
+      fn=functionName
+      (
+        '('
+          (
+            v=comparatorType { argsTypes.add(v); }
+            ( ',' v=comparatorType { argsTypes.add(v); } )*
+          )?
+        ')'
+        { argsPresent = true; }
+      )?
+      { $expr = new DropFunctionStatement(fn, argsTypes, argsPresent, ifExists); }
+    ;
+
+/**
+ * CREATE KEYSPACE [IF NOT EXISTS] <KEYSPACE> WITH attr1 = value1 AND attr2 = value2;
+ */
+createKeyspaceStatement returns [CreateKeyspaceStatement expr]
+    @init {
+        KeyspaceAttributes attrs = new KeyspaceAttributes();
+        boolean ifNotExists = false;
+    }
+    : K_CREATE K_KEYSPACE (K_IF K_NOT K_EXISTS { ifNotExists = true; } )? ks=keyspaceName
+      K_WITH properties[attrs] { $expr = new CreateKeyspaceStatement(ks, attrs, ifNotExists); }
+    ;
+
+/**
+ * CREATE COLUMNFAMILY [IF NOT EXISTS] <CF> (
+ *     <name1> <type>,
+ *     <name2> <type>,
+ *     <name3> <type>
+ * ) WITH <property> = <value> AND ...;
+ */
+createTableStatement returns [CreateTableStatement.RawStatement expr]
+    @init { boolean ifNotExists = false; }
+    : K_CREATE K_COLUMNFAMILY (K_IF K_NOT K_EXISTS { ifNotExists = true; } )?
+      cf=columnFamilyName { $expr = new CreateTableStatement.RawStatement(cf, ifNotExists); }
+      cfamDefinition[expr]
+    ;
+
+cfamDefinition[CreateTableStatement.RawStatement expr]
+    : '(' cfamColumns[expr] ( ',' cfamColumns[expr]? )* ')'
+      ( K_WITH cfamProperty[expr.properties] ( K_AND cfamProperty[expr.properties] )*)?
+    ;
+
+cfamColumns[CreateTableStatement.RawStatement expr]
+    : k=ident v=comparatorType { boolean isStatic=false; } (K_STATIC {isStatic = true;})? { $expr.addDefinition(k, v, isStatic); }
+        (K_PRIMARY K_KEY { $expr.addKeyAliases(Collections.singletonList(k)); })?
+    | K_PRIMARY K_KEY '(' pkDef[expr] (',' c=ident { $expr.addColumnAlias(c); } )* ')'
+    ;
+
+pkDef[CreateTableStatement.RawStatement expr]
+    : k=ident { $expr.addKeyAliases(Collections.singletonList(k)); }
+    | '(' { List<ColumnIdentifier> l = new ArrayList<ColumnIdentifier>(); } k1=ident { l.add(k1); } ( ',' kn=ident { l.add(kn); } )* ')' { $expr.addKeyAliases(l); }
+    ;
+
+cfamProperty[CFProperties props]
+    : property[props.properties]
+    | K_COMPACT K_STORAGE { $props.setCompactStorage(); }
+    | K_CLUSTERING K_ORDER K_BY '(' cfamOrdering[props] (',' cfamOrdering[props])* ')'
+    ;
+
+cfamOrdering[CFProperties props]
+    @init{ boolean reversed=false; }
+    : k=ident (K_ASC | K_DESC { reversed=true;} ) { $props.setOrdering(k, reversed); }
+    ;
+
+
+/**
+ * CREATE TYPE foo (
+ *    <name1> <type1>,
+ *    <name2> <type2>,
+ *    ....
+ * )
+ */
+createTypeStatement returns [CreateTypeStatement expr]
+    @init { boolean ifNotExists = false; }
+    : K_CREATE K_TYPE (K_IF K_NOT K_EXISTS { ifNotExists = true; } )?
+         tn=userTypeName { $expr = new CreateTypeStatement(tn, ifNotExists); }
+         '(' typeColumns[expr] ( ',' typeColumns[expr]? )* ')'
+    ;
+
+typeColumns[CreateTypeStatement expr]
+    : k=fident v=comparatorType { $expr.addDefinition(k, v); }
+    ;
+
+
+/**
+ * CREATE INDEX [IF NOT EXISTS] [indexName] ON <columnFamily> (<columnName>);
+ * CREATE CUSTOM INDEX [IF NOT EXISTS] [indexName] ON <columnFamily> (<columnName>) USING <indexClass>;
+ */
+createIndexStatement returns [CreateIndexStatement expr]
+    @init {
+        IndexPropDefs props = new IndexPropDefs();
+        boolean ifNotExists = false;
+        IndexName name = new IndexName();
+        List<IndexTarget.Raw> targets = new ArrayList<>();
+    }
+    : K_CREATE (K_CUSTOM { props.isCustom = true; })? K_INDEX (K_IF K_NOT K_EXISTS { ifNotExists = true; } )?
+        (idxName[name])? K_ON cf=columnFamilyName '(' (indexIdent[targets] (',' indexIdent[targets])*)? ')'
+        (K_USING cls=STRING_LITERAL { props.customClass = $cls.text; })?
+        (K_WITH properties[props])?
+      { $expr = new CreateIndexStatement(cf, name, targets, props, ifNotExists); }
+    ;
+
+indexIdent [List<IndexTarget.Raw> targets]
+    : c=cident                   { $targets.add(IndexTarget.Raw.simpleIndexOn(c)); }
+    | K_VALUES '(' c=cident ')'  { $targets.add(IndexTarget.Raw.valuesOf(c)); }
+    | K_KEYS '(' c=cident ')'    { $targets.add(IndexTarget.Raw.keysOf(c)); }
+    | K_ENTRIES '(' c=cident ')' { $targets.add(IndexTarget.Raw.keysAndValuesOf(c)); }
+    | K_FULL '(' c=cident ')'    { $targets.add(IndexTarget.Raw.fullCollection(c)); }
+    ;
+
+/**
+ * CREATE MATERIALIZED VIEW <viewName> AS
+ *  SELECT <columns>
+ *  FROM <CF>
+ *  WHERE <pkColumns> IS NOT NULL
+ *  PRIMARY KEY (<pkColumns>)
+ *  WITH <property> = <value> AND ...;
+ */
+createMaterializedViewStatement returns [CreateViewStatement expr]
+    @init {
+        boolean ifNotExists = false;
+        List<ColumnDefinition.Raw> partitionKeys = new ArrayList<>();
+        List<ColumnDefinition.Raw> compositeKeys = new ArrayList<>();
+    }
+    : K_CREATE K_MATERIALIZED K_VIEW (K_IF K_NOT K_EXISTS { ifNotExists = true; })? cf=columnFamilyName K_AS
+        K_SELECT sclause=selectClause K_FROM basecf=columnFamilyName
+        (K_WHERE wclause=whereClause)?
+        K_PRIMARY K_KEY (
+        '(' '(' k1=cident { partitionKeys.add(k1); } ( ',' kn=cident { partitionKeys.add(kn); } )* ')' ( ',' c1=cident { compositeKeys.add(c1); } )* ')'
+    |   '(' k1=cident { partitionKeys.add(k1); } ( ',' cn=cident { compositeKeys.add(cn); } )* ')'
+        )
+        {
+             WhereClause where = wclause == null ? WhereClause.empty() : wclause.build();
+             $expr = new CreateViewStatement(cf, basecf, sclause, where, partitionKeys, compositeKeys, ifNotExists);
+        }
+        ( K_WITH cfamProperty[expr.properties] ( K_AND cfamProperty[expr.properties] )*)?
+    ;
+
+/**
+ * CREATE TRIGGER triggerName ON columnFamily USING 'triggerClass';
+ */
+createTriggerStatement returns [CreateTriggerStatement expr]
+    @init {
+        boolean ifNotExists = false;
+    }
+    : K_CREATE K_TRIGGER (K_IF K_NOT K_EXISTS { ifNotExists = true; } )? (name=ident)
+        K_ON cf=columnFamilyName K_USING cls=STRING_LITERAL
+      { $expr = new CreateTriggerStatement(cf, name.toString(), $cls.text, ifNotExists); }
+    ;
+
+/**
+ * DROP TRIGGER [IF EXISTS] triggerName ON columnFamily;
+ */
+dropTriggerStatement returns [DropTriggerStatement expr]
+     @init { boolean ifExists = false; }
+    : K_DROP K_TRIGGER (K_IF K_EXISTS { ifExists = true; } )? (name=ident) K_ON cf=columnFamilyName
+      { $expr = new DropTriggerStatement(cf, name.toString(), ifExists); }
+    ;
+
+/**
+ * ALTER KEYSPACE <KS> WITH <property> = <value>;
+ */
+alterKeyspaceStatement returns [AlterKeyspaceStatement expr]
+    @init { KeyspaceAttributes attrs = new KeyspaceAttributes(); }
+    : K_ALTER K_KEYSPACE ks=keyspaceName
+        K_WITH properties[attrs] { $expr = new AlterKeyspaceStatement(ks, attrs); }
+    ;
+
+/**
+ * ALTER COLUMN FAMILY <CF> ALTER <column> TYPE <newtype>;
+ * ALTER COLUMN FAMILY <CF> ADD <column> <newtype>; | ALTER COLUMN FAMILY <CF> ADD (<column> <newtype>,<column1> <newtype1>..... <column n> <newtype n>)
+ * ALTER COLUMN FAMILY <CF> DROP <column>; | ALTER COLUMN FAMILY <CF> DROP ( <column>,<column1>.....<column n>)
+ * ALTER COLUMN FAMILY <CF> WITH <property> = <value>;
+ * ALTER COLUMN FAMILY <CF> RENAME <column> TO <column>;
+ */
+alterTableStatement returns [AlterTableStatement expr]
+    @init {
+        AlterTableStatement.Type type = null;
+        TableAttributes attrs = new TableAttributes();
+        Map<ColumnDefinition.Raw, ColumnDefinition.Raw> renames = new HashMap<ColumnDefinition.Raw, ColumnDefinition.Raw>();
+        List<AlterTableStatementColumn> colNameList = new ArrayList<AlterTableStatementColumn>();
+        Long deleteTimestamp = null;
+    }
+    : K_ALTER K_COLUMNFAMILY cf=columnFamilyName
+          ( K_ALTER id=schema_cident  K_TYPE v=comparatorType  { type = AlterTableStatement.Type.ALTER; } { colNameList.add(new AlterTableStatementColumn(id,v)); }
+          | K_ADD  (        (aid=schema_cident  v=comparatorType   b1=cfisStatic { colNameList.add(new AlterTableStatementColumn(aid,v,b1)); })
+                     | ('('  id1=schema_cident  v1=comparatorType  b1=cfisStatic { colNameList.add(new AlterTableStatementColumn(id1,v1,b1)); }
+                       ( ',' idn=schema_cident  vn=comparatorType  bn=cfisStatic { colNameList.add(new AlterTableStatementColumn(idn,vn,bn)); } )* ')' ) ) { type = AlterTableStatement.Type.ADD; }
+          | K_DROP K_COMPACT K_STORAGE          { type = AlterTableStatement.Type.DROP_COMPACT_STORAGE; }        
+          | K_DROP ( (        id=schema_cident  { colNameList.add(new AlterTableStatementColumn(id)); }
+                      | ('('  id1=schema_cident { colNameList.add(new AlterTableStatementColumn(id1)); }
+                        ( ',' idn=schema_cident { colNameList.add(new AlterTableStatementColumn(idn)); } )* ')') )
+                     ( K_USING K_TIMESTAMP t=INTEGER { deleteTimestamp = Long.parseLong(Constants.Literal.integer($t.text).getText()); })? ) { type = AlterTableStatement.Type.DROP; }
+          | K_WITH  properties[attrs]                 { type = AlterTableStatement.Type.OPTS; }
+          | K_RENAME                                  { type = AlterTableStatement.Type.RENAME; }
+               id1=schema_cident K_TO toId1=schema_cident { renames.put(id1, toId1); }
+               ( K_AND idn=schema_cident K_TO toIdn=schema_cident { renames.put(idn, toIdn); } )*
+          )
+    {
+        $expr = new AlterTableStatement(cf, type, colNameList, attrs, renames, deleteTimestamp);
+    }
+    ;
+
+cfisStatic returns [boolean isStaticColumn]
+    @init{
+        boolean isStatic = false;
+    }
+    : (K_STATIC { isStatic=true; })? { $isStaticColumn = isStatic;
+    }
+    ;
+
+alterMaterializedViewStatement returns [AlterViewStatement expr]
+    @init {
+        TableAttributes attrs = new TableAttributes();
+    }
+    : K_ALTER K_MATERIALIZED K_VIEW name=columnFamilyName
+          K_WITH properties[attrs]
+    {
+        $expr = new AlterViewStatement(name, attrs);
+    }
+    ;
+
+
+/**
+ * ALTER TYPE <name> ALTER <field> TYPE <newtype>;
+ * ALTER TYPE <name> ADD <field> <newtype>;
+ * ALTER TYPE <name> RENAME <field> TO <newtype> AND ...;
+ */
+alterTypeStatement returns [AlterTypeStatement expr]
+    : K_ALTER K_TYPE name=userTypeName
+          ( K_ALTER f=fident K_TYPE v=comparatorType { $expr = AlterTypeStatement.alter(name, f, v); }
+          | K_ADD   f=fident v=comparatorType        { $expr = AlterTypeStatement.addition(name, f, v); }
+          | K_RENAME
+               { Map<FieldIdentifier, FieldIdentifier> renames = new HashMap<>(); }
+                 id1=fident K_TO toId1=fident { renames.put(id1, toId1); }
+                 ( K_AND idn=fident K_TO toIdn=fident { renames.put(idn, toIdn); } )*
+               { $expr = AlterTypeStatement.renames(name, renames); }
+          )
+    ;
+
+
+/**
+ * DROP KEYSPACE [IF EXISTS] <KSP>;
+ */
+dropKeyspaceStatement returns [DropKeyspaceStatement ksp]
+    @init { boolean ifExists = false; }
+    : K_DROP K_KEYSPACE (K_IF K_EXISTS { ifExists = true; } )? ks=keyspaceName { $ksp = new DropKeyspaceStatement(ks, ifExists); }
+    ;
+
+/**
+ * DROP COLUMNFAMILY [IF EXISTS] <CF>;
+ */
+dropTableStatement returns [DropTableStatement stmt]
+    @init { boolean ifExists = false; }
+    : K_DROP K_COLUMNFAMILY (K_IF K_EXISTS { ifExists = true; } )? cf=columnFamilyName { $stmt = new DropTableStatement(cf, ifExists); }
+    ;
+
+/**
+ * DROP TYPE <name>;
+ */
+dropTypeStatement returns [DropTypeStatement stmt]
+    @init { boolean ifExists = false; }
+    : K_DROP K_TYPE (K_IF K_EXISTS { ifExists = true; } )? name=userTypeName { $stmt = new DropTypeStatement(name, ifExists); }
+    ;
+
+/**
+ * DROP INDEX [IF EXISTS] <INDEX_NAME>
+ */
+dropIndexStatement returns [DropIndexStatement expr]
+    @init { boolean ifExists = false; }
+    : K_DROP K_INDEX (K_IF K_EXISTS { ifExists = true; } )? index=indexName
+      { $expr = new DropIndexStatement(index, ifExists); }
+    ;
+
+/**
+ * DROP MATERIALIZED VIEW [IF EXISTS] <view_name>
+ */
+dropMaterializedViewStatement returns [DropViewStatement expr]
+    @init { boolean ifExists = false; }
+    : K_DROP K_MATERIALIZED K_VIEW (K_IF K_EXISTS { ifExists = true; } )? cf=columnFamilyName
+      { $expr = new DropViewStatement(cf, ifExists); }
+    ;
+
+/**
+  * TRUNCATE <CF>;
+  */
+truncateStatement returns [TruncateStatement stmt]
+    : K_TRUNCATE (K_COLUMNFAMILY)? cf=columnFamilyName { $stmt = new TruncateStatement(cf); }
+    ;
+
+/**
+ * GRANT <permission> ON <resource> TO <rolename>
+ */
+grantPermissionsStatement returns [GrantPermissionsStatement stmt]
+    : K_GRANT
+          permissionOrAll
+      K_ON
+          resource
+      K_TO
+          grantee=userOrRoleName
+      { $stmt = new GrantPermissionsStatement(filterPermissions($permissionOrAll.perms, $resource.res), $resource.res, grantee); }
+    ;
+
+/**
+ * REVOKE <permission> ON <resource> FROM <rolename>
+ */
+revokePermissionsStatement returns [RevokePermissionsStatement stmt]
+    : K_REVOKE
+          permissionOrAll
+      K_ON
+          resource
+      K_FROM
+          revokee=userOrRoleName
+      { $stmt = new RevokePermissionsStatement(filterPermissions($permissionOrAll.perms, $resource.res), $resource.res, revokee); }
+    ;
+
+/**
+ * GRANT ROLE <rolename> TO <grantee>
+ */
+grantRoleStatement returns [GrantRoleStatement stmt]
+    : K_GRANT
+          role=userOrRoleName
+      K_TO
+          grantee=userOrRoleName
+      { $stmt = new GrantRoleStatement(role, grantee); }
+    ;
+
+/**
+ * REVOKE ROLE <rolename> FROM <revokee>
+ */
+revokeRoleStatement returns [RevokeRoleStatement stmt]
+    : K_REVOKE
+          role=userOrRoleName
+      K_FROM
+          revokee=userOrRoleName
+      { $stmt = new RevokeRoleStatement(role, revokee); }
+    ;
+
+listPermissionsStatement returns [ListPermissionsStatement stmt]
+    @init {
+        IResource resource = null;
+        boolean recursive = true;
+        RoleName grantee = new RoleName();
+    }
+    : K_LIST
+          permissionOrAll
+      ( K_ON resource { resource = $resource.res; } )?
+      ( K_OF roleName[grantee] )?
+      ( K_NORECURSIVE { recursive = false; } )?
+      { $stmt = new ListPermissionsStatement($permissionOrAll.perms, resource, grantee, recursive); }
+    ;
+
+permission returns [Permission perm]
+    : p=(K_CREATE | K_ALTER | K_DROP | K_SELECT | K_MODIFY | K_AUTHORIZE | K_DESCRIBE | K_EXECUTE)
+    { $perm = Permission.valueOf($p.text.toUpperCase()); }
+    ;
+
+permissionOrAll returns [Set<Permission> perms]
+    : K_ALL ( K_PERMISSIONS )?       { $perms = Permission.ALL; }
+    | p=permission ( K_PERMISSION )? { $perms = EnumSet.of($p.perm); }
+    ;
+
+resource returns [IResource res]
+    : d=dataResource { $res = $d.res; }
+    | r=roleResource { $res = $r.res; }
+    | f=functionResource { $res = $f.res; }
+    | j=jmxResource { $res = $j.res; }
+    ;
+
+dataResource returns [DataResource res]
+    : K_ALL K_KEYSPACES { $res = DataResource.root(); }
+    | K_KEYSPACE ks = keyspaceName { $res = DataResource.keyspace($ks.id); }
+    | ( K_COLUMNFAMILY )? cf = columnFamilyName
+      { $res = DataResource.table($cf.name.getKeyspace(), $cf.name.getColumnFamily()); }
+    ;
+
+jmxResource returns [JMXResource res]
+    : K_ALL K_MBEANS { $res = JMXResource.root(); }
+    // when a bean name (or pattern) is supplied, validate that it's a legal ObjectName
+    // also, just to be picky, if the "MBEANS" form is used, only allow a pattern style names
+    | K_MBEAN mbean { $res = JMXResource.mbean(canonicalizeObjectName($mbean.text, false)); }
+    | K_MBEANS mbean { $res = JMXResource.mbean(canonicalizeObjectName($mbean.text, true)); }
+    ;
+
+roleResource returns [RoleResource res]
+    : K_ALL K_ROLES { $res = RoleResource.root(); }
+    | K_ROLE role = userOrRoleName { $res = RoleResource.role($role.name.getName()); }
+    ;
+
+functionResource returns [FunctionResource res]
+    @init {
+        List<CQL3Type.Raw> argsTypes = new ArrayList<>();
+    }
+    : K_ALL K_FUNCTIONS { $res = FunctionResource.root(); }
+    | K_ALL K_FUNCTIONS K_IN K_KEYSPACE ks = keyspaceName { $res = FunctionResource.keyspace($ks.id); }
+    // Arg types are mandatory for DCL statements on Functions
+    | K_FUNCTION fn=functionName
+      (
+        '('
+          (
+            v=comparatorType { argsTypes.add(v); }
+            ( ',' v=comparatorType { argsTypes.add(v); } )*
+          )?
+        ')'
+      )
+      { $res = FunctionResource.functionFromCql($fn.s.keyspace, $fn.s.name, argsTypes); }
+    ;
+
+/**
+ * CREATE USER [IF NOT EXISTS] <username> [WITH PASSWORD <password>] [SUPERUSER|NOSUPERUSER]
+ */
+createUserStatement returns [CreateRoleStatement stmt]
+    @init {
+        RoleOptions opts = new RoleOptions();
+        opts.setOption(IRoleManager.Option.LOGIN, true);
+        boolean superuser = false;
+        boolean ifNotExists = false;
+        RoleName name = new RoleName();
+    }
+    : K_CREATE K_USER (K_IF K_NOT K_EXISTS { ifNotExists = true; })? u=username { name.setName($u.text, true); }
+      ( K_WITH userPassword[opts] )?
+      ( K_SUPERUSER { superuser = true; } | K_NOSUPERUSER { superuser = false; } )?
+      { opts.setOption(IRoleManager.Option.SUPERUSER, superuser);
+        $stmt = new CreateRoleStatement(name, opts, ifNotExists); }
+    ;
+
+/**
+ * ALTER USER <username> [WITH PASSWORD <password>] [SUPERUSER|NOSUPERUSER]
+ */
+alterUserStatement returns [AlterRoleStatement stmt]
+    @init {
+        RoleOptions opts = new RoleOptions();
+        RoleName name = new RoleName();
+    }
+    : K_ALTER K_USER u=username { name.setName($u.text, true); }
+      ( K_WITH userPassword[opts] )?
+      ( K_SUPERUSER { opts.setOption(IRoleManager.Option.SUPERUSER, true); }
+        | K_NOSUPERUSER { opts.setOption(IRoleManager.Option.SUPERUSER, false); } ) ?
+      {  $stmt = new AlterRoleStatement(name, opts); }
+    ;
+
+/**
+ * DROP USER [IF EXISTS] <username>
+ */
+dropUserStatement returns [DropRoleStatement stmt]
+    @init {
+        boolean ifExists = false;
+        RoleName name = new RoleName();
+    }
+    : K_DROP K_USER (K_IF K_EXISTS { ifExists = true; })? u=username { name.setName($u.text, true); $stmt = new DropRoleStatement(name, ifExists); }
+    ;
+
+/**
+ * LIST USERS
+ */
+listUsersStatement returns [ListRolesStatement stmt]
+    : K_LIST K_USERS { $stmt = new ListUsersStatement(); }
+    ;
+
+/**
+ * CREATE ROLE [IF NOT EXISTS] <rolename> [ [WITH] option [ [AND] option ]* ]
+ *
+ * where option can be:
+ *  PASSWORD = '<password>'
+ *  SUPERUSER = (true|false)
+ *  LOGIN = (true|false)
+ *  OPTIONS = { 'k1':'v1', 'k2':'v2'}
+ */
+createRoleStatement returns [CreateRoleStatement stmt]
+    @init {
+        RoleOptions opts = new RoleOptions();
+        boolean ifNotExists = false;
+    }
+    : K_CREATE K_ROLE (K_IF K_NOT K_EXISTS { ifNotExists = true; })? name=userOrRoleName
+      ( K_WITH roleOptions[opts] )?
+      {
+        // set defaults if they weren't explictly supplied
+        if (!opts.getLogin().isPresent())
+        {
+            opts.setOption(IRoleManager.Option.LOGIN, false);
+        }
+        if (!opts.getSuperuser().isPresent())
+        {
+            opts.setOption(IRoleManager.Option.SUPERUSER, false);
+        }
+        $stmt = new CreateRoleStatement(name, opts, ifNotExists);
+      }
+    ;
+
+/**
+ * ALTER ROLE <rolename> [ [WITH] option [ [AND] option ]* ]
+ *
+ * where option can be:
+ *  PASSWORD = '<password>'
+ *  SUPERUSER = (true|false)
+ *  LOGIN = (true|false)
+ *  OPTIONS = { 'k1':'v1', 'k2':'v2'}
+ */
+alterRoleStatement returns [AlterRoleStatement stmt]
+    @init {
+        RoleOptions opts = new RoleOptions();
+    }
+    : K_ALTER K_ROLE name=userOrRoleName
+      ( K_WITH roleOptions[opts] )?
+      {  $stmt = new AlterRoleStatement(name, opts); }
+    ;
+
+/**
+ * DROP ROLE [IF EXISTS] <rolename>
+ */
+dropRoleStatement returns [DropRoleStatement stmt]
+    @init {
+        boolean ifExists = false;
+    }
+    : K_DROP K_ROLE (K_IF K_EXISTS { ifExists = true; })? name=userOrRoleName
+      { $stmt = new DropRoleStatement(name, ifExists); }
+    ;
+
+/**
+ * LIST ROLES [OF <rolename>] [NORECURSIVE]
+ */
+listRolesStatement returns [ListRolesStatement stmt]
+    @init {
+        boolean recursive = true;
+        RoleName grantee = new RoleName();
+    }
+    : K_LIST K_ROLES
+      ( K_OF roleName[grantee])?
+      ( K_NORECURSIVE { recursive = false; } )?
+      { $stmt = new ListRolesStatement(grantee, recursive); }
+    ;
+
+roleOptions[RoleOptions opts]
+    : roleOption[opts] (K_AND roleOption[opts])*
+    ;
+
+roleOption[RoleOptions opts]
+    :  K_PASSWORD '=' v=STRING_LITERAL { opts.setOption(IRoleManager.Option.PASSWORD, $v.text); }
+    |  K_OPTIONS '=' m=mapLiteral { opts.setOption(IRoleManager.Option.OPTIONS, convertPropertyMap(m)); }
+    |  K_SUPERUSER '=' b=BOOLEAN { opts.setOption(IRoleManager.Option.SUPERUSER, Boolean.valueOf($b.text)); }
+    |  K_LOGIN '=' b=BOOLEAN { opts.setOption(IRoleManager.Option.LOGIN, Boolean.valueOf($b.text)); }
+    ;
+
+// for backwards compatibility in CREATE/ALTER USER, this has no '='
+userPassword[RoleOptions opts]
+    :  K_PASSWORD v=STRING_LITERAL { opts.setOption(IRoleManager.Option.PASSWORD, $v.text); }
+    ;
+
+/** DEFINITIONS **/
+
+// Column Identifiers.  These need to be treated differently from other
+// identifiers because the underlying comparator is not necessarily text. See
+// CASSANDRA-8178 for details.
+// Also, we need to support the internal of the super column map (for backward
+// compatibility) which is empty (we only want to allow this is in data manipulation
+// queries, not in schema defition etc).
+cident returns [ColumnDefinition.Raw id]
+    : EMPTY_QUOTED_NAME    { $id = ColumnDefinition.Raw.forQuoted(""); }
+    | t=IDENT              { $id = ColumnDefinition.Raw.forUnquoted($t.text); }
+    | t=QUOTED_NAME        { $id = ColumnDefinition.Raw.forQuoted($t.text); }
+    | k=unreserved_keyword { $id = ColumnDefinition.Raw.forUnquoted(k); }
+    ;
+
+schema_cident returns [ColumnDefinition.Raw id]
+    : t=IDENT              { $id = ColumnDefinition.Raw.forUnquoted($t.text); }
+    | t=QUOTED_NAME        { $id = ColumnDefinition.Raw.forQuoted($t.text); }
+    | k=unreserved_keyword { $id = ColumnDefinition.Raw.forUnquoted(k); }
+    ;
+
+// Column identifiers where the comparator is known to be text
+ident returns [ColumnIdentifier id]
+    : t=IDENT              { $id = ColumnIdentifier.getInterned($t.text, false); }
+    | t=QUOTED_NAME        { $id = ColumnIdentifier.getInterned($t.text, true); }
+    | k=unreserved_keyword { $id = ColumnIdentifier.getInterned(k, false); }
+    ;
+
+fident returns [FieldIdentifier id]
+    : t=IDENT              { $id = FieldIdentifier.forUnquoted($t.text); }
+    | t=QUOTED_NAME        { $id = FieldIdentifier.forQuoted($t.text); }
+    | k=unreserved_keyword { $id = FieldIdentifier.forUnquoted(k); }
+    ;
+
+// Identifiers that do not refer to columns
+noncol_ident returns [ColumnIdentifier id]
+    : t=IDENT              { $id = new ColumnIdentifier($t.text, false); }
+    | t=QUOTED_NAME        { $id = new ColumnIdentifier($t.text, true); }
+    | k=unreserved_keyword { $id = new ColumnIdentifier(k, false); }
+    ;
+
+// Keyspace & Column family names
+keyspaceName returns [String id]
+    @init { CFName name = new CFName(); }
+    : ksName[name] { $id = name.getKeyspace(); }
+    ;
+
+indexName returns [IndexName name]
+    @init { $name = new IndexName(); }
+    : (ksName[name] '.')? idxName[name]
+    ;
+
+columnFamilyName returns [CFName name]
+    @init { $name = new CFName(); }
+    : (ksName[name] '.')? cfName[name]
+    ;
+
+userTypeName returns [UTName name]
+    : (ks=noncol_ident '.')? ut=non_type_ident { $name = new UTName(ks, ut); }
+    ;
+
+userOrRoleName returns [RoleName name]
+    @init { RoleName role = new RoleName(); }
+    : roleName[role] {$name = role;}
+    ;
+
+ksName[KeyspaceElementName name]
+    : t=IDENT              { $name.setKeyspace($t.text, false);}
+    | t=QUOTED_NAME        { $name.setKeyspace($t.text, true);}
+    | k=unreserved_keyword { $name.setKeyspace(k, false);}
+    | QMARK {addRecognitionError("Bind variables cannot be used for keyspace names");}
+    ;
+
+cfName[CFName name]
+    : t=IDENT              { $name.setColumnFamily($t.text, false); }
+    | t=QUOTED_NAME        { $name.setColumnFamily($t.text, true); }
+    | k=unreserved_keyword { $name.setColumnFamily(k, false); }
+    | QMARK {addRecognitionError("Bind variables cannot be used for table names");}
+    ;
+
+idxName[IndexName name]
+    : t=IDENT              { $name.setIndex($t.text, false); }
+    | t=QUOTED_NAME        { $name.setIndex($t.text, true);}
+    | k=unreserved_keyword { $name.setIndex(k, false); }
+    | QMARK {addRecognitionError("Bind variables cannot be used for index names");}
+    ;
+
+roleName[RoleName name]
+    : t=IDENT              { $name.setName($t.text, false); }
+    | s=STRING_LITERAL     { $name.setName($s.text, true); }
+    | t=QUOTED_NAME        { $name.setName($t.text, true); }
+    | k=unreserved_keyword { $name.setName(k, false); }
+    | QMARK {addRecognitionError("Bind variables cannot be used for role names");}
+    ;
+
+constant returns [Constants.Literal constant]
+    : t=STRING_LITERAL { $constant = Constants.Literal.string($t.text); }
+    | t=INTEGER        { $constant = Constants.Literal.integer($t.text); }
+    | t=FLOAT          { $constant = Constants.Literal.floatingPoint($t.text); }
+    | t=BOOLEAN        { $constant = Constants.Literal.bool($t.text); }
+    | t=DURATION       { $constant = Constants.Literal.duration($t.text);}
+    | t=UUID           { $constant = Constants.Literal.uuid($t.text); }
+    | t=HEXNUMBER      { $constant = Constants.Literal.hex($t.text); }
+    | { String sign=""; } ('-' {sign = "-"; } )? t=(K_NAN | K_INFINITY) { $constant = Constants.Literal.floatingPoint(sign + $t.text); }
+    ;
+
+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); }
+    ;
+
+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); }
+    | { List<Term.Raw> s = new ArrayList<Term.Raw>(); s.add(t); }
+          ( ',' tn=term { s.add(tn); } )*
+      { $value = new Sets.Literal(s); }
+    ;
+
+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=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()); }
+    ;
+
+usertypeLiteral returns [UserTypes.Literal ut]
+    @init{ Map<FieldIdentifier, Term.Raw> m = new HashMap<>(); }
+    @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=fident ':' v1=term { m.put(k1, v1); } ( ',' kn=fident ':' 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=collectionLiteral  { $value = l; }
+    | u=usertypeLiteral    { $value = u; }
+    | t=tupleLiteral       { $value = t; }
+    | K_NULL               { $value = Constants.NULL_LITERAL; }
+    | ':' id=noncol_ident  { $value = newBindVariables(id); }
+    | QMARK                { $value = newBindVariables(null); }
+    ;
+
+intValue returns [Term.Raw value]
+    : t=INTEGER     { $value = Constants.Literal.integer($t.text); }
+    | ':' id=noncol_ident  { $value = newBindVariables(id); }
+    | QMARK         { $value = newBindVariables(null); }
+    ;
+
+functionName returns [FunctionName s]
+     // antlr might try to recover and give a null for f. It will still error out in the end, but FunctionName
+     // wouldn't be happy with that so we should bypass this for now or we'll have a weird user-facing error
+    : (ks=keyspaceName '.')? f=allowedFunctionName   { $s = f == null ? null : new FunctionName(ks, f); }
+    ;
+
+allowedFunctionName returns [String s]
+    : f=IDENT                       { $s = $f.text.toLowerCase(); }
+    | f=QUOTED_NAME                 { $s = $f.text; }
+    | u=unreserved_function_keyword { $s = u; }
+    | K_TOKEN                       { $s = "token"; }
+    | K_COUNT                       { $s = "count"; }
+    ;
+
+function returns [Term.Raw t]
+    : f=functionName '(' ')'                   { $t = new FunctionCall.Raw(f, Collections.<Term.Raw>emptyList()); }
+    | f=functionName '(' args=functionArgs ')' { $t = new FunctionCall.Raw(f, args); }
+    ;
+
+functionArgs returns [List<Term.Raw> args]
+    @init{ $args = new ArrayList<Term.Raw>(); }
+    : t1=term {args.add(t1); } ( ',' tn=term { args.add(tn); } )*
+    ;
+
+term returns [Term.Raw term]
+    : v=value                          { $term = v; }
+    | f=function                       { $term = f; }
+    | '(' c=comparatorType ')' t=term  { $term = new TypeCast(c, t); }
+    ;
+
+columnOperation[List<Pair<ColumnDefinition.Raw, Operation.RawUpdate>> operations]
+    : key=cident columnOperationDifferentiator[operations, key]
+    ;
+
+columnOperationDifferentiator[List<Pair<ColumnDefinition.Raw, Operation.RawUpdate>> operations, ColumnDefinition.Raw key]
+    : '=' normalColumnOperation[operations, key]
+    | shorthandColumnOperation[operations, key]
+    | '[' k=term ']' collectionColumnOperation[operations, key, k]
+    | '.' field=fident udtColumnOperation[operations, key, field]
+    ;
+
+normalColumnOperation[List<Pair<ColumnDefinition.Raw, Operation.RawUpdate>> operations, ColumnDefinition.Raw key]
+    : t=term ('+' c=cident )?
+      {
+          if (c == null)
+          {
+              addRawUpdate(operations, key, new Operation.SetValue(t));
+          }
+          else
+          {
+              if (!key.equals(c))
+                  addRecognitionError("Only expressions of the form X = <value> + X are supported.");
+              addRawUpdate(operations, key, new Operation.Prepend(t));
+          }
+      }
+    | c=cident sig=('+' | '-') t=term
+      {
+          if (!key.equals(c))
+              addRecognitionError("Only expressions of the form X = X " + $sig.text + "<value> are supported.");
+          addRawUpdate(operations, key, $sig.text.equals("+") ? new Operation.Addition(t) : new Operation.Substraction(t));
+      }
+    | c=cident i=INTEGER
+      {
+          // Note that this production *is* necessary because X = X - 3 will in fact be lexed as [ X, '=', X, INTEGER].
+          if (!key.equals(c))
+              // We don't yet allow a '+' in front of an integer, but we could in the future really, so let's be future-proof in our error message
+              addRecognitionError("Only expressions of the form X = X " + ($i.text.charAt(0) == '-' ? '-' : '+') + " <value> are supported.");
+          addRawUpdate(operations, key, new Operation.Addition(Constants.Literal.integer($i.text)));
+      }
+    ;
+
+shorthandColumnOperation[List<Pair<ColumnDefinition.Raw, Operation.RawUpdate>> operations, ColumnDefinition.Raw key]
+    : sig=('+=' | '-=') t=term
+      {
+          addRawUpdate(operations, key, $sig.text.equals("+=") ? new Operation.Addition(t) : new Operation.Substraction(t));
+      }
+    ;
+
+collectionColumnOperation[List<Pair<ColumnDefinition.Raw, Operation.RawUpdate>> operations, ColumnDefinition.Raw key, Term.Raw k]
+    : '=' t=term
+      {
+          addRawUpdate(operations, key, new Operation.SetElement(k, t));
+      }
+    ;
+
+udtColumnOperation[List<Pair<ColumnDefinition.Raw, Operation.RawUpdate>> operations, ColumnDefinition.Raw key, FieldIdentifier field]
+    : '=' t=term
+      {
+          addRawUpdate(operations, key, new Operation.SetField(field, t));
+      }
+    ;
+
+columnCondition[List<Pair<ColumnDefinition.Raw, ColumnCondition.Raw>> conditions]
+    // Note: we'll reject duplicates later
+    : key=cident
+        ( op=relationType t=term { conditions.add(Pair.create(key, ColumnCondition.Raw.simpleCondition(t, op))); }
+        | K_IN
+            ( values=singleColumnInValues { conditions.add(Pair.create(key, ColumnCondition.Raw.simpleInCondition(values))); }
+            | marker=inMarker { conditions.add(Pair.create(key, ColumnCondition.Raw.simpleInCondition(marker))); }
+            )
+        | '[' element=term ']'
+            ( op=relationType t=term { conditions.add(Pair.create(key, ColumnCondition.Raw.collectionCondition(t, element, op))); }
+            | K_IN
+                ( values=singleColumnInValues { conditions.add(Pair.create(key, ColumnCondition.Raw.collectionInCondition(element, values))); }
+                | marker=inMarker { conditions.add(Pair.create(key, ColumnCondition.Raw.collectionInCondition(element, marker))); }
+                )
+            )
+        | '.' field=fident
+            ( op=relationType t=term { conditions.add(Pair.create(key, ColumnCondition.Raw.udtFieldCondition(t, field, op))); }
+            | K_IN
+                ( values=singleColumnInValues { conditions.add(Pair.create(key, ColumnCondition.Raw.udtFieldInCondition(field, values))); }
+                | marker=inMarker { conditions.add(Pair.create(key, ColumnCondition.Raw.udtFieldInCondition(field, marker))); }
+                )
+            )
+        )
+    ;
+
+properties[PropertyDefinitions props]
+    : property[props] (K_AND property[props])*
+    ;
+
+property[PropertyDefinitions props]
+    : k=noncol_ident '=' simple=propertyValue { try { $props.addProperty(k.toString(), simple); } catch (SyntaxException e) { addRecognitionError(e.getMessage()); } }
+    | k=noncol_ident '=' map=mapLiteral { try { $props.addProperty(k.toString(), convertPropertyMap(map)); } catch (SyntaxException e) { addRecognitionError(e.getMessage()); } }
+    ;
+
+propertyValue returns [String str]
+    : c=constant           { $str = c.getRawText(); }
+    | u=unreserved_keyword { $str = u; }
+    ;
+
+relationType returns [Operator op]
+    : '='  { $op = Operator.EQ; }
+    | '<'  { $op = Operator.LT; }
+    | '<=' { $op = Operator.LTE; }
+    | '>'  { $op = Operator.GT; }
+    | '>=' { $op = Operator.GTE; }
+    | '!=' { $op = Operator.NEQ; }
+    ;
+
+relation[WhereClause.Builder clauses]
+    : name=cident type=relationType t=term { $clauses.add(new SingleColumnRelation(name, type, t)); }
+    | name=cident K_LIKE t=term { $clauses.add(new SingleColumnRelation(name, Operator.LIKE, t)); }
+    | name=cident K_IS K_NOT K_NULL { $clauses.add(new SingleColumnRelation(name, Operator.IS_NOT, Constants.NULL_LITERAL)); }
+    | K_TOKEN l=tupleOfIdentifiers type=relationType t=term
+        { $clauses.add(new TokenRelation(l, type, t)); }
+    | name=cident K_IN marker=inMarker
+        { $clauses.add(new SingleColumnRelation(name, Operator.IN, marker)); }
+    | name=cident K_IN inValues=singleColumnInValues
+        { $clauses.add(SingleColumnRelation.createInRelation($name.id, inValues)); }
+    | name=cident K_CONTAINS { Operator rt = Operator.CONTAINS; } (K_KEY { rt = Operator.CONTAINS_KEY; })?
+        t=term { $clauses.add(new SingleColumnRelation(name, rt, t)); }
+    | name=cident '[' key=term ']' type=relationType t=term { $clauses.add(new SingleColumnRelation(name, key, type, t)); }
+    | ids=tupleOfIdentifiers
+      ( K_IN
+          ( '(' ')'
+              { $clauses.add(MultiColumnRelation.createInRelation(ids, new ArrayList<Tuples.Literal>())); }
+          | tupleInMarker=inMarkerForTuple /* (a, b, c) IN ? */
+              { $clauses.add(MultiColumnRelation.createSingleMarkerInRelation(ids, tupleInMarker)); }
+          | literals=tupleOfTupleLiterals /* (a, b, c) IN ((1, 2, 3), (4, 5, 6), ...) */
+              {
+                  $clauses.add(MultiColumnRelation.createInRelation(ids, literals));
+              }
+          | markers=tupleOfMarkersForTuples /* (a, b, c) IN (?, ?, ...) */
+              { $clauses.add(MultiColumnRelation.createInRelation(ids, markers)); }
+          )
+      | type=relationType literal=tupleLiteral /* (a, b, c) > (1, 2, 3) or (a, b, c) > (?, ?, ?) */
+          {
+              $clauses.add(MultiColumnRelation.createNonInRelation(ids, type, literal));
+          }
+      | type=relationType tupleMarker=markerForTuple /* (a, b, c) >= ? */
+          { $clauses.add(MultiColumnRelation.createNonInRelation(ids, type, tupleMarker)); }
+      )
+    | '(' relation[$clauses] ')'
+    ;
+
+inMarker returns [AbstractMarker.INRaw marker]
+    : QMARK { $marker = newINBindVariables(null); }
+    | ':' name=noncol_ident { $marker = newINBindVariables(name); }
+    ;
+
+tupleOfIdentifiers returns [List<ColumnDefinition.Raw> ids]
+    @init { $ids = new ArrayList<ColumnDefinition.Raw>(); }
+    : '(' n1=cident { $ids.add(n1); } (',' ni=cident { $ids.add(ni); })* ')'
+    ;
+
+singleColumnInValues returns [List<Term.Raw> terms]
+    @init { $terms = new ArrayList<Term.Raw>(); }
+    : '(' ( t1 = term { $terms.add(t1); } (',' ti=term { $terms.add(ti); })* )? ')'
+    ;
+
+tupleOfTupleLiterals returns [List<Tuples.Literal> literals]
+    @init { $literals = new ArrayList<>(); }
+    : '(' t1=tupleLiteral { $literals.add(t1); } (',' ti=tupleLiteral { $literals.add(ti); })* ')'
+    ;
+
+markerForTuple returns [Tuples.Raw marker]
+    : QMARK { $marker = newTupleBindVariables(null); }
+    | ':' name=noncol_ident { $marker = newTupleBindVariables(name); }
+    ;
+
+tupleOfMarkersForTuples returns [List<Tuples.Raw> markers]
+    @init { $markers = new ArrayList<Tuples.Raw>(); }
+    : '(' m1=markerForTuple { $markers.add(m1); } (',' mi=markerForTuple { $markers.add(mi); })* ')'
+    ;
+
+inMarkerForTuple returns [Tuples.INRaw marker]
+    : QMARK { $marker = newTupleINBindVariables(null); }
+    | ':' name=noncol_ident { $marker = newTupleINBindVariables(name); }
+    ;
+
+comparatorType returns [CQL3Type.Raw t]
+    : n=native_type     { $t = CQL3Type.Raw.from(n); }
+    | c=collection_type { $t = c; }
+    | tt=tuple_type     { $t = tt; }
+    | id=userTypeName   { $t = CQL3Type.Raw.userType(id); }
+    | K_FROZEN '<' f=comparatorType '>'
+      {
+        try {
+            $t = CQL3Type.Raw.frozen(f);
+        } catch (InvalidRequestException e) {
+            addRecognitionError(e.getMessage());
+        }
+      }
+    | s=STRING_LITERAL
+      {
+        try {
+            $t = CQL3Type.Raw.from(new CQL3Type.Custom($s.text));
+        } catch (SyntaxException e) {
+            addRecognitionError("Cannot parse type " + $s.text + ": " + e.getMessage());
+        } catch (ConfigurationException e) {
+            addRecognitionError("Error setting type " + $s.text + ": " + e.getMessage());
+        }
+      }
+    ;
+
+native_type returns [CQL3Type t]
+    : K_ASCII     { $t = CQL3Type.Native.ASCII; }
+    | K_BIGINT    { $t = CQL3Type.Native.BIGINT; }
+    | K_BLOB      { $t = CQL3Type.Native.BLOB; }
+    | K_BOOLEAN   { $t = CQL3Type.Native.BOOLEAN; }
+    | K_COUNTER   { $t = CQL3Type.Native.COUNTER; }
+    | K_DECIMAL   { $t = CQL3Type.Native.DECIMAL; }
+    | K_DOUBLE    { $t = CQL3Type.Native.DOUBLE; }
+    | K_DURATION    { $t = CQL3Type.Native.DURATION; }
+    | K_FLOAT     { $t = CQL3Type.Native.FLOAT; }
+    | K_INET      { $t = CQL3Type.Native.INET;}
+    | K_INT       { $t = CQL3Type.Native.INT; }
+    | K_SMALLINT  { $t = CQL3Type.Native.SMALLINT; }
+    | K_TEXT      { $t = CQL3Type.Native.TEXT; }
+    | K_TIMESTAMP { $t = CQL3Type.Native.TIMESTAMP; }
+    | K_TINYINT   { $t = CQL3Type.Native.TINYINT; }
+    | K_UUID      { $t = CQL3Type.Native.UUID; }
+    | K_VARCHAR   { $t = CQL3Type.Native.VARCHAR; }
+    | K_VARINT    { $t = CQL3Type.Native.VARINT; }
+    | K_TIMEUUID  { $t = CQL3Type.Native.TIMEUUID; }
+    | K_DATE      { $t = CQL3Type.Native.DATE; }
+    | K_TIME      { $t = CQL3Type.Native.TIME; }
+    ;
+
+collection_type returns [CQL3Type.Raw pt]
+    : K_MAP  '<' t1=comparatorType ',' t2=comparatorType '>'
+        {
+            // if we can't parse either t1 or t2, antlr will "recover" and we may have t1 or t2 null.
+            if (t1 != null && t2 != null)
+                $pt = CQL3Type.Raw.map(t1, t2);
+        }
+    | K_LIST '<' t=comparatorType '>'
+        { if (t != null) $pt = CQL3Type.Raw.list(t); }
+    | K_SET  '<' t=comparatorType '>'
+        { if (t != null) $pt = CQL3Type.Raw.set(t); }
+    ;
+
+tuple_type returns [CQL3Type.Raw t]
+    : K_TUPLE '<' { List<CQL3Type.Raw> types = new ArrayList<>(); }
+         t1=comparatorType { types.add(t1); } (',' tn=comparatorType { types.add(tn); })*
+      '>' { $t = CQL3Type.Raw.tuple(types); }
+    ;
+
+username
+    : IDENT
+    | STRING_LITERAL
+    | QUOTED_NAME { addRecognitionError("Quoted strings are are not supported for user names and USER is deprecated, please use ROLE");}
+    ;
+
+mbean
+    : STRING_LITERAL
+    ;
+
+// Basically the same as cident, but we need to exlude existing CQL3 types
+// (which for some reason are not reserved otherwise)
+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 | K_KEY | K_CAST | K_JSON | K_DISTINCT) { $str = $k.text; }
+    ;
+
+unreserved_function_keyword returns [String str]
+    : u=basic_unreserved_keyword { $str = u; }
+    | t=native_type              { $str = t.toString(); }
+    ;
+
+basic_unreserved_keyword returns [String str]
+    : k=( K_KEYS
+        | K_AS
+        | K_CLUSTERING
+        | K_COMPACT
+        | K_STORAGE
+        | K_TYPE
+        | K_VALUES
+        | K_MAP
+        | K_LIST
+        | K_FILTERING
+        | K_PERMISSION
+        | K_PERMISSIONS
+        | K_KEYSPACES
+        | K_ALL
+        | K_USER
+        | K_USERS
+        | K_ROLE
+        | K_ROLES
+        | K_SUPERUSER
+        | K_NOSUPERUSER
+        | K_LOGIN
+        | K_NOLOGIN
+        | K_OPTIONS
+        | K_PASSWORD
+        | K_EXISTS
+        | K_CUSTOM
+        | K_TRIGGER
+        | K_CONTAINS
+        | K_STATIC
+        | K_FROZEN
+        | K_TUPLE
+        | K_FUNCTION
+        | K_FUNCTIONS
+        | K_AGGREGATE
+        | K_SFUNC
+        | K_STYPE
+        | K_FINALFUNC
+        | K_INITCOND
+        | K_RETURNS
+        | K_LANGUAGE
+        | K_CALLED
+        | K_INPUT
+        | K_LIKE
+        | K_PER
+        | K_PARTITION
+        | K_GROUP
+        ) { $str = $k.text; }
+    ;
diff --git a/src/java/com/datastax/driver/core/PreparedStatementHelper.java b/src/java/com/datastax/driver/core/PreparedStatementHelper.java
index 2b43f8c..020bf7e 100644
--- a/src/java/com/datastax/driver/core/PreparedStatementHelper.java
+++ b/src/java/com/datastax/driver/core/PreparedStatementHelper.java
@@ -18,10 +18,24 @@
 
 package com.datastax.driver.core;
 
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
 // Unfortunately, MD5Digest and fields in PreparedStatement are package-private, so the easiest way to test these
 // things while still using the driver was to create a class in DS package.
 public class PreparedStatementHelper
 {
+    private static final MessageDigest cachedDigest;
+
+    static {
+        try {
+            cachedDigest = MessageDigest.getInstance("MD5");
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
     private static MD5Digest id(PreparedStatement statement)
     {
         return statement.getPreparedId().id;
@@ -40,30 +54,28 @@
     public static void assertHashWithoutKeyspace(PreparedStatement statement, String queryString, String ks)
     {
         MD5Digest returned = id(statement);
-        MD5Digest expected = hashWithoutKeyspace(queryString, ks);
-        if (!returned.equals(expected))
+        if (!returned.equals(hashWithoutKeyspace(queryString, ks)))
         {
             if (returned.equals(hashWithKeyspace(queryString, ks)))
-                throw new AssertionError(String.format("Got hash with keyspace from the cluster: %s, expected %s",
-                                                       returned, expected));
+                throw new AssertionError(String.format("Got hash with keyspace from the cluster: %s, should have gotten %s",
+                                                       returned, hashWithoutKeyspace(queryString, ks)));
             else
-                throw new AssertionError(String.format("Got unrecognized hash: %s, expected %s",
-                                                       returned, expected));
+                throw new AssertionError(String.format("Got unrecognized hash: %s",
+                                                       returned));
         }
     }
 
     public static void assertHashWithKeyspace(PreparedStatement statement, String queryString, String ks)
     {
         MD5Digest returned = id(statement);
-        MD5Digest expected = hashWithKeyspace(queryString, ks);
-        if (!returned.equals(expected))
+        if (!returned.equals(hashWithKeyspace(queryString, ks)))
         {
             if (returned.equals(hashWithoutKeyspace(queryString, ks)))
-                throw new AssertionError(String.format("Got hash without keyspace from the cluster: %s, expected %s",
+                throw new AssertionError(String.format("Got hash without keyspace from the cluster: %s, should have gotten %s",
                                                        returned, hashWithKeyspace(queryString, ks)));
             else
-                throw new AssertionError(String.format("Got unrecognized hash: %s, expected %s",
-                                                       returned, expected));
+                throw new AssertionError(String.format("Got unrecognized hash: %s",
+                                                       returned));
         }
 
     }
@@ -93,6 +105,15 @@
     }
 
     public static MD5Digest compute(String toHash) {
-        return MD5Digest.wrap(org.apache.cassandra.utils.MD5Digest.compute(toHash).bytes);
+        try {
+            return compute(toHash.getBytes("UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+    public static synchronized MD5Digest compute(byte[] toHash) {
+        cachedDigest.reset();
+        return MD5Digest.wrap(cachedDigest.digest(toHash));
     }
 }
diff --git a/src/java/org/apache/cassandra/auth/AllowAllAuthorizer.java b/src/java/org/apache/cassandra/auth/AllowAllAuthorizer.java
index bc6fee4..3b40979 100644
--- a/src/java/org/apache/cassandra/auth/AllowAllAuthorizer.java
+++ b/src/java/org/apache/cassandra/auth/AllowAllAuthorizer.java
@@ -22,6 +22,12 @@
 
 public class AllowAllAuthorizer implements IAuthorizer
 {
+    @Override
+    public boolean requireAuthorization()
+    {
+        return false;
+    }
+
     public Set<Permission> authorize(AuthenticatedUser user, IResource resource)
     {
         return resource.applicablePermissions();
diff --git a/src/java/org/apache/cassandra/auth/AuthCache.java b/src/java/org/apache/cassandra/auth/AuthCache.java
new file mode 100644
index 0000000..80664d1
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/AuthCache.java
@@ -0,0 +1,212 @@
+/*
+ * 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.auth;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import com.google.common.base.Throwables;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListenableFutureTask;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.utils.MBeanWrapper;
+
+import org.apache.cassandra.concurrent.DebuggableThreadPoolExecutor;
+
+public class AuthCache<K, V> implements AuthCacheMBean
+{
+    private static final Logger logger = LoggerFactory.getLogger(AuthCache.class);
+
+    private static final String MBEAN_NAME_BASE = "org.apache.cassandra.auth:type=";
+
+    private volatile LoadingCache<K, V> cache;
+    private ThreadPoolExecutor cacheRefreshExecutor;
+
+    private final String name;
+    private final Consumer<Integer> setValidityDelegate;
+    private final Supplier<Integer> getValidityDelegate;
+    private final Consumer<Integer> setUpdateIntervalDelegate;
+    private final Supplier<Integer> getUpdateIntervalDelegate;
+    private final Consumer<Integer> setMaxEntriesDelegate;
+    private final Supplier<Integer> getMaxEntriesDelegate;
+    private final Function<K, V> loadFunction;
+    private final Supplier<Boolean> enableCache;
+
+    protected AuthCache(String name,
+                        Consumer<Integer> setValidityDelegate,
+                        Supplier<Integer> getValidityDelegate,
+                        Consumer<Integer> setUpdateIntervalDelegate,
+                        Supplier<Integer> getUpdateIntervalDelegate,
+                        Consumer<Integer> setMaxEntriesDelegate,
+                        Supplier<Integer> getMaxEntriesDelegate,
+                        Function<K, V> loadFunction,
+                        Supplier<Boolean> enableCache)
+    {
+        this.name = name;
+        this.setValidityDelegate = setValidityDelegate;
+        this.getValidityDelegate = getValidityDelegate;
+        this.setUpdateIntervalDelegate = setUpdateIntervalDelegate;
+        this.getUpdateIntervalDelegate = getUpdateIntervalDelegate;
+        this.setMaxEntriesDelegate = setMaxEntriesDelegate;
+        this.getMaxEntriesDelegate = getMaxEntriesDelegate;
+        this.loadFunction = loadFunction;
+        this.enableCache = enableCache;
+        init();
+    }
+
+    protected void init()
+    {
+        this.cacheRefreshExecutor = new DebuggableThreadPoolExecutor(name + "Refresh", Thread.NORM_PRIORITY)
+        {
+            protected void afterExecute(Runnable r, Throwable t)
+            {
+                // empty to avoid logging on background updates
+            }
+        };
+        this.cache = initCache(null);
+        MBeanWrapper.instance.registerMBean(this, getObjectName());
+    }
+
+    protected String getObjectName()
+    {
+        return MBEAN_NAME_BASE + name;
+    }
+
+    public V get(K k)
+    {
+        if (cache == null)
+            return loadFunction.apply(k);
+
+        try {
+            return cache.get(k);
+        }
+        catch (ExecutionException | UncheckedExecutionException e)
+        {
+            Throwables.propagateIfInstanceOf(e.getCause(), RuntimeException.class);
+            throw Throwables.propagate(e);
+        }
+    }
+
+    public void invalidate()
+    {
+        cache = initCache(null);
+    }
+
+    public void invalidate(K k)
+    {
+        if (cache != null)
+            cache.invalidate(k);
+    }
+
+    public void setValidity(int validityPeriod)
+    {
+        if (Boolean.getBoolean("cassandra.disable_auth_caches_remote_configuration"))
+            throw new UnsupportedOperationException("Remote configuration of auth caches is disabled");
+
+        setValidityDelegate.accept(validityPeriod);
+        cache = initCache(cache);
+    }
+
+    public int getValidity()
+    {
+        return getValidityDelegate.get();
+    }
+
+    public void setUpdateInterval(int updateInterval)
+    {
+        if (Boolean.getBoolean("cassandra.disable_auth_caches_remote_configuration"))
+            throw new UnsupportedOperationException("Remote configuration of auth caches is disabled");
+
+        setUpdateIntervalDelegate.accept(updateInterval);
+        cache = initCache(cache);
+    }
+
+    public int getUpdateInterval()
+    {
+        return getUpdateIntervalDelegate.get();
+    }
+
+    public void setMaxEntries(int maxEntries)
+    {
+        if (Boolean.getBoolean("cassandra.disable_auth_caches_remote_configuration"))
+            throw new UnsupportedOperationException("Remote configuration of auth caches is disabled");
+
+        setMaxEntriesDelegate.accept(maxEntries);
+        cache = initCache(cache);
+    }
+
+    public int getMaxEntries()
+    {
+        return getMaxEntriesDelegate.get();
+    }
+
+    private LoadingCache<K, V> initCache(LoadingCache<K, V> existing)
+    {
+        if (!enableCache.get())
+            return null;
+
+        if (getValidity() <= 0)
+            return null;
+
+        logger.info("(Re)initializing {} (validity period/update interval/max entries) ({}/{}/{})",
+                    name, getValidity(), getUpdateInterval(), getMaxEntries());
+
+        LoadingCache<K, V> newcache = CacheBuilder.newBuilder()
+                           .refreshAfterWrite(getUpdateInterval(), TimeUnit.MILLISECONDS)
+                           .expireAfterWrite(getValidity(), TimeUnit.MILLISECONDS)
+                           .maximumSize(getMaxEntries())
+                           .build(new CacheLoader<K, V>()
+                           {
+                               public V load(K k)
+                               {
+                                   return loadFunction.apply(k);
+                               }
+
+                               public ListenableFuture<V> reload(final K k, final V oldV)
+                               {
+                                   ListenableFutureTask<V> task = ListenableFutureTask.create(() -> {
+                                       try
+                                       {
+                                           return loadFunction.apply(k);
+                                       }
+                                       catch (Exception e)
+                                       {
+                                           logger.trace("Error performing async refresh of auth data in {}", name, e);
+                                           throw e;
+                                       }
+                                   });
+                                   cacheRefreshExecutor.execute(task);
+                                   return task;
+                               }
+                           });
+        if (existing != null)
+            newcache.putAll(existing.asMap());
+        return newcache;
+    }
+}
diff --git a/src/java/org/apache/cassandra/auth/AuthCacheMBean.java b/src/java/org/apache/cassandra/auth/AuthCacheMBean.java
new file mode 100644
index 0000000..43fb88e
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/AuthCacheMBean.java
@@ -0,0 +1,36 @@
+/*
+ * 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.auth;
+
+public interface AuthCacheMBean
+{
+    public void invalidate();
+
+    public void setValidity(int validityPeriod);
+
+    public int getValidity();
+
+    public void setUpdateInterval(int updateInterval);
+
+    public int getUpdateInterval();
+
+    public void setMaxEntries(int maxEntries);
+
+    public int getMaxEntries();
+}
diff --git a/src/java/org/apache/cassandra/auth/AuthConfig.java b/src/java/org/apache/cassandra/auth/AuthConfig.java
new file mode 100644
index 0000000..c389ae4
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/AuthConfig.java
@@ -0,0 +1,113 @@
+/*
+ * 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.auth;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.utils.FBUtilities;
+
+/**
+ * Only purpose is to Initialize authentication/authorization via {@link #applyAuth()}.
+ * This is in this separate class as it implicitly initializes schema stuff (via classes referenced in here).
+ */
+public final class AuthConfig
+{
+    private static final Logger logger = LoggerFactory.getLogger(AuthConfig.class);
+
+    private static boolean initialized;
+
+    public static void applyAuth()
+    {
+        // some tests need this
+        if (initialized)
+            return;
+
+        initialized = true;
+
+        Config conf = DatabaseDescriptor.getRawConfig();
+
+        IAuthenticator authenticator = new AllowAllAuthenticator();
+
+        /* Authentication, authorization and role management backend, implementing IAuthenticator, IAuthorizer & IRoleMapper*/
+        if (conf.authenticator != null)
+            authenticator = FBUtilities.newAuthenticator(conf.authenticator);
+
+        // the configuration options regarding credentials caching are only guaranteed to
+        // work with PasswordAuthenticator, so log a message if some other authenticator
+        // is in use and non-default values are detected
+        if (!(authenticator instanceof PasswordAuthenticator)
+            && (conf.credentials_update_interval_in_ms != -1
+                || conf.credentials_validity_in_ms != 2000
+                || conf.credentials_cache_max_entries != 1000))
+        {
+            logger.info("Configuration options credentials_update_interval_in_ms, credentials_validity_in_ms and " +
+                        "credentials_cache_max_entries may not be applicable for the configured authenticator ({})",
+                        authenticator.getClass().getName());
+        }
+
+        DatabaseDescriptor.setAuthenticator(authenticator);
+
+        // authorizer
+
+        IAuthorizer authorizer = new AllowAllAuthorizer();
+
+        if (conf.authorizer != null)
+            authorizer = FBUtilities.newAuthorizer(conf.authorizer);
+
+        if (!authenticator.requireAuthentication() && authorizer.requireAuthorization())
+            throw new ConfigurationException(conf.authenticator + " can't be used with " + conf.authorizer, false);
+
+        DatabaseDescriptor.setAuthorizer(authorizer);
+
+        // role manager
+
+        IRoleManager roleManager;
+        if (conf.role_manager != null)
+            roleManager = FBUtilities.newRoleManager(conf.role_manager);
+        else
+            roleManager = new CassandraRoleManager();
+
+        if (authenticator instanceof PasswordAuthenticator && !(roleManager instanceof CassandraRoleManager))
+            throw new ConfigurationException("CassandraRoleManager must be used with PasswordAuthenticator", false);
+
+        DatabaseDescriptor.setRoleManager(roleManager);
+
+        // authenticator
+
+        IInternodeAuthenticator internodeAuthenticator;
+        if (conf.internode_authenticator != null)
+            internodeAuthenticator = FBUtilities.construct(conf.internode_authenticator, "internode_authenticator");
+        else
+            internodeAuthenticator = new AllowAllInternodeAuthenticator();
+
+        DatabaseDescriptor.setInternodeAuthenticator(internodeAuthenticator);
+
+        // Validate at last to have authenticator, authorizer, role-manager and internode-auth setup
+        // in case these rely on each other.
+
+        authenticator.validateConfiguration();
+        authorizer.validateConfiguration();
+        roleManager.validateConfiguration();
+        internodeAuthenticator.validateConfiguration();
+    }
+}
diff --git a/src/java/org/apache/cassandra/auth/AuthKeyspace.java b/src/java/org/apache/cassandra/auth/AuthKeyspace.java
index d91b014..afa94a6 100644
--- a/src/java/org/apache/cassandra/auth/AuthKeyspace.java
+++ b/src/java/org/apache/cassandra/auth/AuthKeyspace.java
@@ -20,6 +20,7 @@
 import java.util.concurrent.TimeUnit;
 
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.schema.KeyspaceMetadata;
 import org.apache.cassandra.schema.KeyspaceParams;
 import org.apache.cassandra.schema.Tables;
@@ -30,8 +31,6 @@
     {
     }
 
-    public static final String NAME = "system_auth";
-
     /**
      * Generation is used as a timestamp for automatic table creation on startup.
      * If you make any changes to the tables below, make sure to increment the
@@ -87,13 +86,13 @@
 
     private static CFMetaData compile(String name, String description, String schema)
     {
-        return CFMetaData.compile(String.format(schema, name), NAME)
+        return CFMetaData.compile(String.format(schema, name), SchemaConstants.AUTH_KEYSPACE_NAME)
                          .comment(description)
                          .gcGraceSeconds((int) TimeUnit.DAYS.toSeconds(90));
     }
 
     public static KeyspaceMetadata metadata()
     {
-        return KeyspaceMetadata.create(NAME, KeyspaceParams.simple(1), Tables.of(Roles, RoleMembers, RolePermissions, ResourceRoleIndex));
+        return KeyspaceMetadata.create(SchemaConstants.AUTH_KEYSPACE_NAME, KeyspaceParams.simple(1), Tables.of(Roles, RoleMembers, RolePermissions, ResourceRoleIndex));
     }
 }
diff --git a/src/java/org/apache/cassandra/auth/CassandraAuthorizer.java b/src/java/org/apache/cassandra/auth/CassandraAuthorizer.java
index 7b1cf2e..516caed 100644
--- a/src/java/org/apache/cassandra/auth/CassandraAuthorizer.java
+++ b/src/java/org/apache/cassandra/auth/CassandraAuthorizer.java
@@ -31,6 +31,7 @@
 import org.apache.cassandra.concurrent.ScheduledExecutors;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.cql3.statements.BatchStatement;
 import org.apache.cassandra.cql3.statements.ModificationStatement;
@@ -41,6 +42,10 @@
 import org.apache.cassandra.serializers.SetSerializer;
 import org.apache.cassandra.serializers.UTF8Serializer;
 import org.apache.cassandra.service.ClientState;
+
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.service.QueryState;
 import org.apache.cassandra.transport.messages.ResultMessage;
 import org.apache.cassandra.utils.ByteBufferUtil;
@@ -116,7 +121,7 @@
         try
         {
             UntypedResultSet rows = process(String.format("SELECT resource FROM %s.%s WHERE role = '%s'",
-                                                          AuthKeyspace.NAME,
+                                                          SchemaConstants.AUTH_KEYSPACE_NAME,
                                                           AuthKeyspace.ROLE_PERMISSIONS,
                                                           escape(revokee.getRoleName())));
 
@@ -125,7 +130,7 @@
             {
                 statements.add(
                     QueryProcessor.getStatement(String.format("DELETE FROM %s.%s WHERE resource = '%s' AND role = '%s'",
-                                                              AuthKeyspace.NAME,
+                                                              SchemaConstants.AUTH_KEYSPACE_NAME,
                                                               AuthKeyspace.RESOURCE_ROLE_INDEX,
                                                               escape(row.getString("resource")),
                                                               escape(revokee.getRoleName())),
@@ -134,7 +139,7 @@
             }
 
             statements.add(QueryProcessor.getStatement(String.format("DELETE FROM %s.%s WHERE role = '%s'",
-                                                                     AuthKeyspace.NAME,
+                                                                     SchemaConstants.AUTH_KEYSPACE_NAME,
                                                                      AuthKeyspace.ROLE_PERMISSIONS,
                                                                      escape(revokee.getRoleName())),
                                                        ClientState.forInternalCalls()).statement);
@@ -155,7 +160,7 @@
         try
         {
             UntypedResultSet rows = process(String.format("SELECT role FROM %s.%s WHERE resource = '%s'",
-                                                          AuthKeyspace.NAME,
+                                                          SchemaConstants.AUTH_KEYSPACE_NAME,
                                                           AuthKeyspace.RESOURCE_ROLE_INDEX,
                                                           escape(droppedResource.getName())));
 
@@ -163,7 +168,7 @@
             for (UntypedResultSet.Row row : rows)
             {
                 statements.add(QueryProcessor.getStatement(String.format("DELETE FROM %s.%s WHERE role = '%s' AND resource = '%s'",
-                                                                         AuthKeyspace.NAME,
+                                                                         SchemaConstants.AUTH_KEYSPACE_NAME,
                                                                          AuthKeyspace.ROLE_PERMISSIONS,
                                                                          escape(row.getString("role")),
                                                                          escape(droppedResource.getName())),
@@ -171,9 +176,9 @@
             }
 
             statements.add(QueryProcessor.getStatement(String.format("DELETE FROM %s.%s WHERE resource = '%s'",
-                                                                                            AuthKeyspace.NAME,
-                                                                                            AuthKeyspace.RESOURCE_ROLE_INDEX,
-                                                                                            escape(droppedResource.getName())),
+                                                                     SchemaConstants.AUTH_KEYSPACE_NAME,
+                                                                     AuthKeyspace.RESOURCE_ROLE_INDEX,
+                                                                     escape(droppedResource.getName())),
                                                                                ClientState.forInternalCalls()).statement);
 
             executeLoggedBatch(statements);
@@ -194,7 +199,8 @@
                                                   Attributes.none());
         QueryProcessor.instance.processBatch(batch,
                                              QueryState.forInternalCalls(),
-                                             BatchQueryOptions.withoutPerStatementVariables(QueryOptions.DEFAULT));
+                                             BatchQueryOptions.withoutPerStatementVariables(QueryOptions.DEFAULT),
+                                             System.nanoTime());
 
     }
 
@@ -209,7 +215,7 @@
         SelectStatement statement;
         // If it exists, read from the legacy user permissions table to handle the case where the cluster
         // is being upgraded and so is running with mixed versions of the authz schema
-        if (Schema.instance.getCFMetaData(AuthKeyspace.NAME, USER_PERMISSIONS) == null)
+        if (Schema.instance.getCFMetaData(SchemaConstants.AUTH_KEYSPACE_NAME, USER_PERMISSIONS) == null)
             statement = authorizeRoleStatement;
         else
         {
@@ -218,8 +224,7 @@
                 legacyAuthorizeRoleStatement = prepare(USERNAME, USER_PERMISSIONS);
             statement = legacyAuthorizeRoleStatement;
         }
-
-        ResultMessage.Rows rows = statement.execute(QueryState.forInternalCalls(), options) ;
+        ResultMessage.Rows rows = statement.execute(QueryState.forInternalCalls(), options, System.nanoTime());
         UntypedResultSet result = UntypedResultSet.create(rows.result);
 
         if (!result.isEmpty() && result.one().has(PERMISSIONS))
@@ -236,7 +241,7 @@
             throws RequestExecutionException
     {
         process(String.format("UPDATE %s.%s SET permissions = permissions %s {%s} WHERE role = '%s' AND resource = '%s'",
-                              AuthKeyspace.NAME,
+                              SchemaConstants.AUTH_KEYSPACE_NAME,
                               AuthKeyspace.ROLE_PERMISSIONS,
                               op,
                               "'" + StringUtils.join(permissions, "','") + "'",
@@ -248,17 +253,17 @@
     private void removeLookupEntry(IResource resource, RoleResource role) throws RequestExecutionException
     {
         process(String.format("DELETE FROM %s.%s WHERE resource = '%s' and role = '%s'",
-                AuthKeyspace.NAME,
-                AuthKeyspace.RESOURCE_ROLE_INDEX,
-                escape(resource.getName()),
-                escape(role.getRoleName())));
+                              SchemaConstants.AUTH_KEYSPACE_NAME,
+                              AuthKeyspace.RESOURCE_ROLE_INDEX,
+                              escape(resource.getName()),
+                              escape(role.getRoleName())));
     }
 
     // Adds an entry to the inverted index table (from resource -> role with defined permissions)
     private void addLookupEntry(IResource resource, RoleResource role) throws RequestExecutionException
     {
         process(String.format("INSERT INTO %s.%s (resource, role) VALUES ('%s','%s')",
-                              AuthKeyspace.NAME,
+                              SchemaConstants.AUTH_KEYSPACE_NAME,
                               AuthKeyspace.RESOURCE_ROLE_INDEX,
                               escape(resource.getName()),
                               escape(role.getRoleName())));
@@ -301,7 +306,7 @@
         Set<PermissionDetails> details = new HashSet<>();
         // If it exists, try the legacy user permissions table first. This is to handle the case
         // where the cluster is being upgraded and so is running with mixed versions of the perms table
-        boolean useLegacyTable = Schema.instance.getCFMetaData(AuthKeyspace.NAME, USER_PERMISSIONS) != null;
+        boolean useLegacyTable = Schema.instance.getCFMetaData(SchemaConstants.AUTH_KEYSPACE_NAME, USER_PERMISSIONS) != null;
         String entityColumnName = useLegacyTable ? USERNAME : ROLE;
         for (UntypedResultSet.Row row : process(buildListQuery(resource, role, useLegacyTable)))
         {
@@ -324,7 +329,7 @@
     {
         String tableName = useLegacyTable ? USER_PERMISSIONS : AuthKeyspace.ROLE_PERMISSIONS;
         String entityName = useLegacyTable ? USERNAME : ROLE;
-        List<String> vars = Lists.newArrayList(AuthKeyspace.NAME, tableName);
+        List<String> vars = Lists.newArrayList(SchemaConstants.AUTH_KEYSPACE_NAME, tableName);
         List<String> conditions = new ArrayList<>();
 
         if (resource != null)
@@ -353,7 +358,7 @@
 
     public Set<DataResource> protectedResources()
     {
-        return ImmutableSet.of(DataResource.table(AuthKeyspace.NAME, AuthKeyspace.ROLE_PERMISSIONS));
+        return ImmutableSet.of(DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLE_PERMISSIONS));
     }
 
     public void validateConfiguration() throws ConfigurationException
@@ -366,7 +371,7 @@
 
         // If old user permissions table exists, migrate the legacy authz data to the new table
         // The delay is to give the node a chance to see its peers before attempting the conversion
-        if (Schema.instance.getCFMetaData(AuthKeyspace.NAME, "permissions") != null)
+        if (Schema.instance.getCFMetaData(SchemaConstants.AUTH_KEYSPACE_NAME, "permissions") != null)
         {
             legacyAuthorizeRoleStatement = prepare(USERNAME, USER_PERMISSIONS);
 
@@ -383,7 +388,7 @@
     private SelectStatement prepare(String entityname, String permissionsTable)
     {
         String query = String.format("SELECT permissions FROM %s.%s WHERE %s = ? AND resource = ?",
-                                     AuthKeyspace.NAME,
+                                     SchemaConstants.AUTH_KEYSPACE_NAME,
                                      permissionsTable,
                                      entityname);
         return (SelectStatement) QueryProcessor.getStatement(query, ClientState.forInternalCalls()).statement;
@@ -406,12 +411,12 @@
                 CQLStatement insertStatement =
                     QueryProcessor.getStatement(String.format("INSERT INTO %s.%s (role, resource, permissions) " +
                                                               "VALUES (?, ?, ?)",
-                                                              AuthKeyspace.NAME,
+                                                              SchemaConstants.AUTH_KEYSPACE_NAME,
                                                               AuthKeyspace.ROLE_PERMISSIONS),
                                                 ClientState.forInternalCalls()).statement;
                 CQLStatement indexStatement =
                     QueryProcessor.getStatement(String.format("INSERT INTO %s.%s (resource, role) VALUES (?,?)",
-                                                              AuthKeyspace.NAME,
+                                                              SchemaConstants.AUTH_KEYSPACE_NAME,
                                                               AuthKeyspace.RESOURCE_ROLE_INDEX),
                                                 ClientState.forInternalCalls()).statement;
 
@@ -433,12 +438,14 @@
                                             QueryOptions.forInternalCalls(ConsistencyLevel.ONE,
                                                                           Lists.newArrayList(row.getBytes("username"),
                                                                                              row.getBytes("resource"),
-                                                                                             serializer.serialize(filteredPerms))));
+                                                                                             serializer.serialize(filteredPerms))),
+                                            System.nanoTime());
 
                     indexStatement.execute(QueryState.forInternalCalls(),
                                            QueryOptions.forInternalCalls(ConsistencyLevel.ONE,
                                                                          Lists.newArrayList(row.getBytes("resource"),
-                                                                                            row.getBytes("username"))));
+                                                                                            row.getBytes("username"))),
+                                           System.nanoTime());
 
                 }
                 logger.info("Completed conversion of legacy permissions");
diff --git a/src/java/org/apache/cassandra/auth/CassandraLoginModule.java b/src/java/org/apache/cassandra/auth/CassandraLoginModule.java
new file mode 100644
index 0000000..d208300
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CassandraLoginModule.java
@@ -0,0 +1,257 @@
+/*
+ * 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.auth;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.*;
+import javax.security.auth.login.FailedLoginException;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.exceptions.AuthenticationException;
+import org.apache.cassandra.service.StorageService;
+
+/**
+ * LoginModule which authenticates a user towards the Cassandra database using
+ * the internal authentication mechanism.
+ */
+public class CassandraLoginModule implements LoginModule
+{
+    private static final Logger logger = LoggerFactory.getLogger(CassandraLoginModule.class);
+
+    // initial state
+    private Subject subject;
+    private CallbackHandler callbackHandler;
+
+    // the authentication status
+    private boolean succeeded = false;
+    private boolean commitSucceeded = false;
+
+    // username and password
+    private String username;
+    private char[] password;
+
+    private CassandraPrincipal principal;
+
+    /**
+     * Initialize this {@code}LoginModule{@code}.
+     *
+     * @param subject the {@code}Subject{@code} to be authenticated. <p>
+     * @param callbackHandler a {@code}CallbackHandler{@code} for communicating
+     *        with the end user (prompting for user names and passwords, for example)
+     * @param sharedState shared {@code}LoginModule{@code} state. This param is unused.
+     * @param options options specified in the login {@code}Configuration{@code} for this particular
+     *        {@code}LoginModule{@code}. This param is unused
+     */
+    @Override
+    public void initialize(Subject subject,
+                           CallbackHandler callbackHandler,
+                           Map<java.lang.String, ?> sharedState,
+                           Map<java.lang.String, ?> options)
+    {
+        this.subject = subject;
+        this.callbackHandler = callbackHandler;
+    }
+
+    /**
+     * Authenticate the user, obtaining credentials from the CallbackHandler
+     * supplied in {@code}initialize{@code}. As long as the configured
+     * {@code}IAuthenticator{@code} supports the optional
+     * {@code}legacyAuthenticate{@code} method, it can be used here.
+     *
+     * @return true in all cases since this {@code}LoginModule{@code}
+     *         should not be ignored.
+     * @exception FailedLoginException if the authentication fails.
+     * @exception LoginException if this {@code}LoginModule{@code} is unable to
+     * perform the authentication.
+     */
+    @Override
+    public boolean login() throws LoginException
+    {
+        // prompt for a user name and password
+        if (callbackHandler == null)
+        {
+            logger.info("No CallbackHandler available for authentication");
+            throw new LoginException("Authentication failed");
+        }
+
+        NameCallback nc = new NameCallback("username: ");
+        PasswordCallback pc = new PasswordCallback("password: ", false);
+        try
+        {
+            callbackHandler.handle(new Callback[]{nc, pc});
+            username = nc.getName();
+            char[] tmpPassword = pc.getPassword();
+            if (tmpPassword == null)
+                tmpPassword = new char[0];
+            password = new char[tmpPassword.length];
+            System.arraycopy(tmpPassword, 0, password, 0, tmpPassword.length);
+            pc.clearPassword();
+        }
+        catch (IOException | UnsupportedCallbackException e)
+        {
+            logger.info("Unexpected exception processing authentication callbacks", e);
+            throw new LoginException("Authentication failed");
+        }
+
+        // verify the credentials
+        try
+        {
+            authenticate();
+        }
+        catch (AuthenticationException e)
+        {
+            // authentication failed -- clean up
+            succeeded = false;
+            cleanUpInternalState();
+            throw new FailedLoginException(e.getMessage());
+        }
+
+        succeeded = true;
+        return true;
+    }
+
+    private void authenticate()
+    {
+        if (!StorageService.instance.isAuthSetupComplete())
+            throw new AuthenticationException("Cannot login as server authentication setup is not yet completed");
+
+        IAuthenticator authenticator = DatabaseDescriptor.getAuthenticator();
+        Map<String, String> credentials = new HashMap<>();
+        credentials.put(PasswordAuthenticator.USERNAME_KEY, username);
+        credentials.put(PasswordAuthenticator.PASSWORD_KEY, String.valueOf(password));
+        AuthenticatedUser user = authenticator.legacyAuthenticate(credentials);
+        // Only actual users should be allowed to authenticate for JMX
+        if (user.isAnonymous() || user.isSystem())
+            throw new AuthenticationException(String.format("Invalid user %s", user.getName()));
+
+        // The LOGIN privilege is required to authenticate - c.f. ClientState::login
+        if (!DatabaseDescriptor.getRoleManager().canLogin(user.getPrimaryRole()))
+            throw new AuthenticationException(user.getName() + " is not permitted to log in");
+    }
+
+    /**
+     * This method is called if the LoginContext's overall authentication succeeded
+     * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
+     * succeeded).
+     *
+     * If this LoginModule's own authentication attempt succeeded (checked by
+     * retrieving the private state saved by the {@code}login{@code} method),
+     * then this method associates a {@code}CassandraPrincipal{@code}
+     * with the {@code}Subject{@code}.
+     * If this LoginModule's own authentication attempted failed, then this
+     * method removes any state that was originally saved.
+     *
+     * @return true if this LoginModule's own login and commit attempts succeeded, false otherwise.
+     * @exception LoginException if the commit fails.
+     */
+    @Override
+    public boolean commit() throws LoginException
+    {
+        if (!succeeded)
+        {
+            return false;
+        }
+        else
+        {
+            // add a Principal (authenticated identity)
+            // to the Subject
+            principal = new CassandraPrincipal(username);
+            if (!subject.getPrincipals().contains(principal))
+                subject.getPrincipals().add(principal);
+
+            cleanUpInternalState();
+            commitSucceeded = true;
+            return true;
+        }
+    }
+
+    /**
+     * This method is called if the LoginContext's  overall authentication failed.
+     * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
+     * did not succeed).
+     *
+     * If this LoginModule's own authentication attempt succeeded (checked by
+     * retrieving the private state saved by the {@code}login{@code} and
+     * {@code}commit{@code} methods), then this method cleans up any state that
+     * was originally saved.
+     *
+     * @return false if this LoginModule's own login and/or commit attempts failed, true otherwise.
+     * @throws LoginException if the abort fails.
+     */
+    @Override
+    public boolean abort() throws LoginException
+    {
+        if (!succeeded)
+        {
+            return false;
+        }
+        else if (!commitSucceeded)
+        {
+            // login succeeded but overall authentication failed
+            succeeded = false;
+            cleanUpInternalState();
+            principal = null;
+        }
+        else
+        {
+            // overall authentication succeeded and commit succeeded,
+            // but someone else's commit failed
+            logout();
+        }
+        return true;
+    }
+
+    /**
+     * Logout the user.
+     *
+     * This method removes the principal that was added by the
+     * {@code}commit{@code} method.
+     *
+     * @return true in all cases since this {@code}LoginModule{@code}
+     *         should not be ignored.
+     * @throws LoginException if the logout fails.
+     */
+    @Override
+    public boolean logout() throws LoginException
+    {
+        subject.getPrincipals().remove(principal);
+        succeeded = false;
+        cleanUpInternalState();
+        principal = null;
+        return true;
+    }
+
+    private void cleanUpInternalState()
+    {
+        username = null;
+        if (password != null)
+        {
+            for (int i = 0; i < password.length; i++)
+                password[i] = ' ';
+            password = null;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/auth/CassandraPrincipal.java b/src/java/org/apache/cassandra/auth/CassandraPrincipal.java
new file mode 100644
index 0000000..41de802
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CassandraPrincipal.java
@@ -0,0 +1,134 @@
+/*
+ * 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.auth;
+
+import java.io.Serializable;
+import java.security.Principal;
+
+/**
+ * <p> This class implements the <code>Principal</code> interface
+ * and represents a user.
+ *
+ * <p> Principals such as this <code>CassPrincipal</code>
+ * may be associated with a particular <code>Subject</code>
+ * to augment that <code>Subject</code> with an additional
+ * identity.  Refer to the <code>Subject</code> class for more information
+ * on how to achieve this.  Authorization decisions can then be based upon
+ * the Principals associated with a <code>Subject</code>.
+ *
+ * @see java.security.Principal
+ * @see javax.security.auth.Subject
+ */
+public class CassandraPrincipal implements Principal, Serializable
+{
+
+    /**
+     *
+     */
+    private static final long serialVersionUID = 1L;
+    private final String name;
+
+    /**
+     * Create a CassPrincipal with a username.
+     *
+     * <p>
+     *
+     * @param name the username for this user.
+     *
+     * @exception NullPointerException if the <code>name</code>
+     *                  is <code>null</code>.
+     */
+    public CassandraPrincipal(String name)
+    {
+        if (name == null)
+            throw new NullPointerException("illegal null input");
+
+        this.name = name;
+    }
+
+    /**
+     * Return the username for this <code>CassPrincipal</code>.
+     *
+     * <p>
+     *
+     * @return the username for this <code>CassPrincipal</code>
+     */
+    @Override
+    public String getName()
+    {
+        return name;
+    }
+
+    /**
+     * Return a string representation of this <code>CassPrincipal</code>.
+     *
+     * <p>
+     *
+     * @return a string representation of this <code>CassPrincipal</code>.
+     */
+    @Override
+    public String toString()
+    {
+        return ("CassandraPrincipal:  " + name);
+    }
+
+    /**
+     * Compares the specified Object with this <code>CassPrincipal</code>
+     * for equality.  Returns true if the given object is also a
+     * <code>CassPrincipal</code> and the two CassPrincipals
+     * have the same username.
+     *
+     * <p>
+     *
+     * @param o Object to be compared for equality with this
+     *          <code>CassPrincipal</code>.
+     *
+     * @return true if the specified Object is equal equal to this
+     *          <code>CassPrincipal</code>.
+     */
+    @Override
+    public boolean equals(Object o)
+    {
+        if (o == null)
+            return false;
+
+        if (this == o)
+            return true;
+
+        if (!(o instanceof CassandraPrincipal))
+            return false;
+        CassandraPrincipal that = (CassandraPrincipal) o;
+
+        if (this.getName().equals(that.getName()))
+            return true;
+        return false;
+    }
+
+    /**
+     * Return a hash code for this <code>CassPrincipal</code>.
+     *
+     * <p>
+     *
+     * @return a hash code for this <code>CassPrincipal</code>.
+     */
+    @Override
+    public int hashCode()
+    {
+        return name.hashCode();
+    }
+}
diff --git a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
index 5354726..6884f35 100644
--- a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
+++ b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
@@ -34,6 +34,7 @@
 import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.cql3.statements.SelectStatement;
 import org.apache.cassandra.db.ConsistencyLevel;
@@ -67,7 +68,7 @@
  * in CREATE/ALTER ROLE statements.
  *
  * Such a configuration could be implemented using a custom IRoleManager that
- * extends CassandraRoleManager and which includes Option.PASSWORD in the Set<Option>
+ * extends CassandraRoleManager and which includes Option.PASSWORD in the {@code Set<Option>}
  * returned from supportedOptions/alterableOptions. Any additional processing
  * of the password itself (such as storing it in an alternative location) would
  * be added in overridden createRole and alterRole implementations.
@@ -158,14 +159,14 @@
     public void setup()
     {
         loadRoleStatement = (SelectStatement) prepare("SELECT * from %s.%s WHERE role = ?",
-                                                      AuthKeyspace.NAME,
+                                                      SchemaConstants.AUTH_KEYSPACE_NAME,
                                                       AuthKeyspace.ROLES);
         // If the old users table exists, we may need to migrate the legacy authn
         // data to the new table. We also need to prepare a statement to read from
         // it, so we can continue to use the old tables while the cluster is upgraded.
         // Otherwise, we may need to create a default superuser role to enable others
         // to be added.
-        if (Schema.instance.getCFMetaData(AuthKeyspace.NAME, "users") != null)
+        if (Schema.instance.getCFMetaData(SchemaConstants.AUTH_KEYSPACE_NAME, "users") != null)
         {
             legacySelectUserStatement = prepareLegacySelectUserStatement();
 
@@ -198,14 +199,14 @@
     {
         String insertCql = options.getPassword().isPresent()
                          ? String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) VALUES ('%s', %s, %s, '%s')",
-                                         AuthKeyspace.NAME,
+                                         SchemaConstants.AUTH_KEYSPACE_NAME,
                                          AuthKeyspace.ROLES,
                                          escape(role.getRoleName()),
                                          options.getSuperuser().or(false),
                                          options.getLogin().or(false),
                                          escape(hashpw(options.getPassword().get())))
                          : String.format("INSERT INTO %s.%s (role, is_superuser, can_login) VALUES ('%s', %s, %s)",
-                                         AuthKeyspace.NAME,
+                                         SchemaConstants.AUTH_KEYSPACE_NAME,
                                          AuthKeyspace.ROLES,
                                          escape(role.getRoleName()),
                                          options.getSuperuser().or(false),
@@ -216,7 +217,7 @@
     public void dropRole(AuthenticatedUser performer, RoleResource role) throws RequestValidationException, RequestExecutionException
     {
         process(String.format("DELETE FROM %s.%s WHERE role = '%s'",
-                              AuthKeyspace.NAME,
+                              SchemaConstants.AUTH_KEYSPACE_NAME,
                               AuthKeyspace.ROLES,
                               escape(role.getRoleName())),
                 consistencyForRole(role.getRoleName()));
@@ -232,7 +233,7 @@
         if (!Strings.isNullOrEmpty(assignments))
         {
             process(String.format("UPDATE %s.%s SET %s WHERE role = '%s'",
-                                  AuthKeyspace.NAME,
+                                  SchemaConstants.AUTH_KEYSPACE_NAME,
                                   AuthKeyspace.ROLES,
                                   assignments,
                                   escape(role.getRoleName())),
@@ -254,7 +255,7 @@
 
         modifyRoleMembership(grantee.getRoleName(), role.getRoleName(), "+");
         process(String.format("INSERT INTO %s.%s (role, member) values ('%s', '%s')",
-                              AuthKeyspace.NAME,
+                              SchemaConstants.AUTH_KEYSPACE_NAME,
                               AuthKeyspace.ROLE_MEMBERS,
                               escape(role.getRoleName()),
                               escape(grantee.getRoleName())),
@@ -271,7 +272,7 @@
 
         modifyRoleMembership(revokee.getRoleName(), role.getRoleName(), "-");
         process(String.format("DELETE FROM %s.%s WHERE role = '%s' and member = '%s'",
-                              AuthKeyspace.NAME,
+                              SchemaConstants.AUTH_KEYSPACE_NAME,
                               AuthKeyspace.ROLE_MEMBERS,
                               escape(role.getRoleName()),
                               escape(revokee.getRoleName())),
@@ -292,7 +293,7 @@
 
     public Set<RoleResource> getAllRoles() throws RequestValidationException, RequestExecutionException
     {
-        UntypedResultSet rows = process(String.format("SELECT role from %s.%s", AuthKeyspace.NAME, AuthKeyspace.ROLES), ConsistencyLevel.QUORUM);
+        UntypedResultSet rows = process(String.format("SELECT role from %s.%s", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES), ConsistencyLevel.QUORUM);
         Iterable<RoleResource> roles = Iterables.transform(rows, new Function<UntypedResultSet.Row, RoleResource>()
         {
             public RoleResource apply(UntypedResultSet.Row row)
@@ -341,8 +342,8 @@
 
     public Set<? extends IResource> protectedResources()
     {
-        return ImmutableSet.of(DataResource.table(AuthKeyspace.NAME, AuthKeyspace.ROLES),
-                               DataResource.table(AuthKeyspace.NAME, AuthKeyspace.ROLE_MEMBERS));
+        return ImmutableSet.of(DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES),
+                               DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLE_MEMBERS));
     }
 
     public void validateConfiguration() throws ConfigurationException
@@ -365,7 +366,7 @@
             {
                 QueryProcessor.process(String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) " +
                                                      "VALUES ('%s', true, true, '%s') USING TIMESTAMP 0",
-                                                     AuthKeyspace.NAME,
+                                                     SchemaConstants.AUTH_KEYSPACE_NAME,
                                                      AuthKeyspace.ROLES,
                                                      DEFAULT_SUPERUSER_NAME,
                                                      escape(hashpw(DEFAULT_SUPERUSER_PASSWORD))),
@@ -384,8 +385,8 @@
     public static boolean hasExistingRoles() throws RequestExecutionException
     {
         // Try looking up the 'cassandra' default role first, to avoid the range query if possible.
-        String defaultSUQuery = String.format("SELECT * FROM %s.%s WHERE role = '%s'", AuthKeyspace.NAME, AuthKeyspace.ROLES, DEFAULT_SUPERUSER_NAME);
-        String allUsersQuery = String.format("SELECT * FROM %s.%s LIMIT 1", AuthKeyspace.NAME, AuthKeyspace.ROLES);
+        String defaultSUQuery = String.format("SELECT * FROM %s.%s WHERE role = '%s'", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES, DEFAULT_SUPERUSER_NAME);
+        String allUsersQuery = String.format("SELECT * FROM %s.%s LIMIT 1", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES);
         return !QueryProcessor.process(defaultSUQuery, ConsistencyLevel.ONE).isEmpty()
                || !QueryProcessor.process(defaultSUQuery, ConsistencyLevel.QUORUM).isEmpty()
                || !QueryProcessor.process(allUsersQuery, ConsistencyLevel.QUORUM).isEmpty();
@@ -456,7 +457,7 @@
                 {
                     // Write the password directly into the table to avoid doubly encrypting it
                     QueryProcessor.process(String.format("UPDATE %s.%s SET salted_hash = '%s' WHERE role = '%s'",
-                                                         AuthKeyspace.NAME,
+                                                         SchemaConstants.AUTH_KEYSPACE_NAME,
                                                          AuthKeyspace.ROLES,
                                                          row.getString("salted_hash"),
                                                          row.getString("username")),
@@ -477,7 +478,7 @@
     private SelectStatement prepareLegacySelectUserStatement()
     {
         return (SelectStatement) prepare("SELECT * FROM %s.%s WHERE name = ?",
-                                         AuthKeyspace.NAME,
+                                         SchemaConstants.AUTH_KEYSPACE_NAME,
                                          LEGACY_USERS_TABLE);
     }
 
@@ -520,7 +521,7 @@
         // If it exists, try the legacy users table in case the cluster
         // is in the process of being upgraded and so is running with mixed
         // versions of the authn schema.
-        if (Schema.instance.getCFMetaData(AuthKeyspace.NAME, "users") == null)
+        if (Schema.instance.getCFMetaData(SchemaConstants.AUTH_KEYSPACE_NAME, "users") == null)
             return getRoleFromTable(name, loadRoleStatement, ROW_TO_ROLE);
         else
         {
@@ -536,7 +537,8 @@
         ResultMessage.Rows rows =
             statement.execute(QueryState.forInternalCalls(),
                               QueryOptions.forInternalCalls(consistencyForRole(name),
-                                                            Collections.singletonList(ByteBufferUtil.bytes(name))));
+                                                            Collections.singletonList(ByteBufferUtil.bytes(name))),
+                              System.nanoTime());
         if (rows.result.isEmpty())
             return NULL_ROLE;
 
@@ -551,7 +553,7 @@
     throws RequestExecutionException
     {
         process(String.format("UPDATE %s.%s SET member_of = member_of %s {'%s'} WHERE role = '%s'",
-                              AuthKeyspace.NAME,
+                              SchemaConstants.AUTH_KEYSPACE_NAME,
                               AuthKeyspace.ROLES,
                               op,
                               escape(role),
@@ -566,7 +568,7 @@
     {
         // Get the membership list of the the given role
         UntypedResultSet rows = process(String.format("SELECT member FROM %s.%s WHERE role = '%s'",
-                                                      AuthKeyspace.NAME,
+                                                      SchemaConstants.AUTH_KEYSPACE_NAME,
                                                       AuthKeyspace.ROLE_MEMBERS,
                                                       escape(role)),
                                         consistencyForRole(role));
@@ -579,7 +581,7 @@
 
         // Finally, remove the membership list for the dropped role
         process(String.format("DELETE FROM %s.%s WHERE role = '%s'",
-                              AuthKeyspace.NAME,
+                              SchemaConstants.AUTH_KEYSPACE_NAME,
                               AuthKeyspace.ROLE_MEMBERS,
                               escape(role)),
                 consistencyForRole(role));
diff --git a/src/java/org/apache/cassandra/auth/DataResource.java b/src/java/org/apache/cassandra/auth/DataResource.java
index f64ed93..0aa24db 100644
--- a/src/java/org/apache/cassandra/auth/DataResource.java
+++ b/src/java/org/apache/cassandra/auth/DataResource.java
@@ -54,31 +54,22 @@
                                                                                             Permission.MODIFY,
                                                                                             Permission.AUTHORIZE);
     private static final String ROOT_NAME = "data";
-    private static final DataResource ROOT_RESOURCE = new DataResource();
+    private static final DataResource ROOT_RESOURCE = new DataResource(Level.ROOT, null, null);
 
     private final Level level;
     private final String keyspace;
     private final String table;
 
-    private DataResource()
-    {
-        level = Level.ROOT;
-        keyspace = null;
-        table = null;
-    }
+    // memoized hashcode since DataRessource is immutable and used in hashmaps often
+    private final transient int hash;
 
-    private DataResource(String keyspace)
+    private DataResource(Level level, String keyspace, String table)
     {
-        level = Level.KEYSPACE;
-        this.keyspace = keyspace;
-        table = null;
-    }
-
-    private DataResource(String keyspace, String table)
-    {
-        level = Level.TABLE;
+        this.level = level;
         this.keyspace = keyspace;
         this.table = table;
+
+        this.hash = Objects.hashCode(level, keyspace, table);
     }
 
     /**
@@ -97,7 +88,7 @@
      */
     public static DataResource keyspace(String keyspace)
     {
-        return new DataResource(keyspace);
+        return new DataResource(Level.KEYSPACE, keyspace, null);
     }
 
     /**
@@ -109,7 +100,7 @@
      */
     public static DataResource table(String keyspace, String table)
     {
-        return new DataResource(keyspace, table);
+        return new DataResource(Level.TABLE, keyspace, table);
     }
 
     /**
@@ -272,6 +263,6 @@
     @Override
     public int hashCode()
     {
-        return Objects.hashCode(level, keyspace, table);
+        return hash;
     }
 }
diff --git a/src/java/org/apache/cassandra/auth/IAuthorizer.java b/src/java/org/apache/cassandra/auth/IAuthorizer.java
index 01c05af..a023e3e 100644
--- a/src/java/org/apache/cassandra/auth/IAuthorizer.java
+++ b/src/java/org/apache/cassandra/auth/IAuthorizer.java
@@ -29,6 +29,15 @@
 public interface IAuthorizer
 {
     /**
+     * Whether or not the authorizer will attempt authorization.
+     * If false the authorizer will not be called for authorization of resources.
+     */
+    default boolean requireAuthorization()
+    {
+        return true;
+    }
+
+    /**
      * Returns a set of permissions of a user on a resource.
      * Since Roles were introduced in version 2.2, Cassandra does not distinguish in any
      * meaningful way between users and roles. A role may or may not have login privileges
diff --git a/src/java/org/apache/cassandra/auth/IRoleManager.java b/src/java/org/apache/cassandra/auth/IRoleManager.java
index 5afc7f3..b27681d 100644
--- a/src/java/org/apache/cassandra/auth/IRoleManager.java
+++ b/src/java/org/apache/cassandra/auth/IRoleManager.java
@@ -170,7 +170,7 @@
 
     /**
      * Where an implementation supports OPTIONS in CREATE and ALTER operations
-     * this method should return the Map<String, String> representing the custom
+     * this method should return the {@code Map<String, String>} representing the custom
      * options associated with the role, as supplied to CREATE or ALTER.
      * It should never return null; if the implementation does not support
      * OPTIONS or if none were supplied then it should return an empty map.
diff --git a/src/java/org/apache/cassandra/auth/JMXResource.java b/src/java/org/apache/cassandra/auth/JMXResource.java
new file mode 100644
index 0000000..cb0ac41
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/JMXResource.java
@@ -0,0 +1,183 @@
+/*
+ * 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.auth;
+
+import java.lang.management.ManagementFactory;
+import java.util.Set;
+import javax.management.MBeanServer;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Sets;
+import org.apache.commons.lang3.StringUtils;
+
+public class JMXResource implements IResource
+{
+    enum Level
+    {
+        ROOT, MBEAN
+    }
+
+    private static final String ROOT_NAME = "mbean";
+    private static final JMXResource ROOT_RESOURCE = new JMXResource();
+    private final Level level;
+    private final String name;
+
+    // permissions which may be granted on Mbeans
+    private static final Set<Permission> JMX_PERMISSIONS = Sets.immutableEnumSet(Permission.AUTHORIZE,
+                                                                                 Permission.DESCRIBE,
+                                                                                 Permission.EXECUTE,
+                                                                                 Permission.MODIFY,
+                                                                                 Permission.SELECT);
+
+    private JMXResource()
+    {
+        level = Level.ROOT;
+        name = null;
+    }
+
+    private JMXResource(String name)
+    {
+        this.name = name;
+        level = Level.MBEAN;
+    }
+
+    public static JMXResource mbean(String name)
+    {
+        return new JMXResource(name);
+    }
+
+    /**
+     * Parses a role resource name into a RoleResource instance.
+     *
+     * @param name Name of the data resource.
+     * @return RoleResource instance matching the name.
+     */
+    public static JMXResource fromName(String name)
+    {
+        String[] parts = StringUtils.split(name, '/');
+
+        if (!parts[0].equals(ROOT_NAME) || parts.length > 2)
+            throw new IllegalArgumentException(String.format("%s is not a valid JMX resource name", name));
+
+        if (parts.length == 1)
+            return root();
+
+        return mbean(parts[1]);
+    }
+
+    @Override
+    public String getName()
+    {
+        if (level == Level.ROOT)
+            return ROOT_NAME;
+        else if (level == Level.MBEAN)
+            return String.format("%s/%s", ROOT_NAME, name);
+        throw new AssertionError();
+    }
+
+    /**
+     * @return for a non-root resource, return the short form of the resource name which represents an ObjectName
+     * (which may be of the pattern or exact kind). i.e. not the full "root/name" version returned by getName().
+     * Throws IllegalStateException if called on the root-level resource.
+     */
+    public String getObjectName()
+    {
+        if (level == Level.ROOT)
+            throw new IllegalStateException(String.format("%s JMX resource has no object name", level));
+        return name;
+    }
+
+    /**
+     * @return the root-level resource.
+     */
+    public static JMXResource root()
+    {
+        return ROOT_RESOURCE;
+    }
+
+    @Override
+    public IResource getParent()
+    {
+        if (level == Level.MBEAN)
+            return root();
+        throw new IllegalStateException("Root-level resource can't have a parent");
+    }
+
+    /**
+     * @return Whether or not the resource has a parent in the hierarchy.
+     */
+    @Override
+    public boolean hasParent()
+    {
+        return !level.equals(Level.ROOT);
+    }
+
+    @Override
+    public boolean exists()
+    {
+        if (!hasParent())
+            return true;
+        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
+        try
+        {
+            return !(mbs.queryNames(new ObjectName(name), null).isEmpty());
+        }
+        catch (MalformedObjectNameException e)
+        {
+            return false;
+        }
+        catch (NullPointerException e)
+        {
+            return false;
+        }
+    }
+
+    @Override
+    public Set<Permission> applicablePermissions()
+    {
+        return JMX_PERMISSIONS;
+    }
+
+    @Override
+    public String toString()
+    {
+        return level == Level.ROOT ? "<all mbeans>" : String.format("<mbean %s>", name);
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o)
+            return true;
+
+        if (!(o instanceof JMXResource))
+            return false;
+
+        JMXResource j = (JMXResource) o;
+
+        return Objects.equal(level, j.level) && Objects.equal(name, j.name);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hashCode(level, name);
+    }
+}
diff --git a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
index 602fea4..4bd3696 100644
--- a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
@@ -28,12 +28,16 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.cql3.statements.SelectStatement;
-import org.apache.cassandra.exceptions.*;
+import org.apache.cassandra.exceptions.AuthenticationException;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.exceptions.RequestExecutionException;
 import org.apache.cassandra.service.ClientState;
 import org.apache.cassandra.service.QueryState;
 import org.apache.cassandra.transport.messages.ResultMessage;
@@ -68,6 +72,8 @@
     public static final String LEGACY_CREDENTIALS_TABLE = "credentials";
     private SelectStatement legacyAuthenticateStatement;
 
+    private CredentialsCache cache;
+
     // No anonymous access.
     public boolean requireAuthentication()
     {
@@ -90,14 +96,39 @@
 
     private AuthenticatedUser authenticate(String username, String password) throws AuthenticationException
     {
+        String hash = cache.get(username);
+        if (!checkpw(password, hash))
+            throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username));
+
+        return new AuthenticatedUser(username);
+    }
+
+    private String queryHashedPassword(String username) throws AuthenticationException
+    {
         try
         {
             SelectStatement authenticationStatement = authenticationStatement();
-            return doAuthenticate(username, password, authenticationStatement);
+
+            ResultMessage.Rows rows =
+                authenticationStatement.execute(QueryState.forInternalCalls(),
+                                                QueryOptions.forInternalCalls(consistencyForRole(username),
+                                                                              Lists.newArrayList(ByteBufferUtil.bytes(username))),
+                                                System.nanoTime());
+
+            // If either a non-existent role name was supplied, or no credentials
+            // were found for that role we don't want to cache the result so we throw
+            // an exception.
+            if (rows.result.isEmpty())
+                throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username));
+
+            UntypedResultSet result = UntypedResultSet.create(rows.result);
+            if (!result.one().has(SALTED_HASH))
+                throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username));
+
+            return result.one().getString(SALTED_HASH);
         }
         catch (RequestExecutionException e)
         {
-            logger.trace("Error performing internal authentication", e);
             throw new AuthenticationException("Unable to perform authentication: " + e.getMessage(), e);
         }
     }
@@ -108,10 +139,11 @@
      */
     private SelectStatement authenticationStatement()
     {
-        if (Schema.instance.getCFMetaData(AuthKeyspace.NAME, LEGACY_CREDENTIALS_TABLE) == null)
+        if (Schema.instance.getCFMetaData(SchemaConstants.AUTH_KEYSPACE_NAME, LEGACY_CREDENTIALS_TABLE) == null)
             return authenticateStatement;
         else
         {
+            // the statement got prepared, we to try preparing it again.
             // If the credentials was initialised only after statement got prepared, re-prepare (CASSANDRA-12813).
             if (legacyAuthenticateStatement == null)
                 prepareLegacyAuthenticateStatement();
@@ -119,10 +151,11 @@
         }
     }
 
+
     public Set<DataResource> protectedResources()
     {
         // Also protected by CassandraRoleManager, but the duplication doesn't hurt and is more explicit
-        return ImmutableSet.of(DataResource.table(AuthKeyspace.NAME, AuthKeyspace.ROLES));
+        return ImmutableSet.of(DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES));
     }
 
     public void validateConfiguration() throws ConfigurationException
@@ -133,19 +166,21 @@
     {
         String query = String.format("SELECT %s FROM %s.%s WHERE role = ?",
                                      SALTED_HASH,
-                                     AuthKeyspace.NAME,
+                                     SchemaConstants.AUTH_KEYSPACE_NAME,
                                      AuthKeyspace.ROLES);
         authenticateStatement = prepare(query);
 
-        if (Schema.instance.getCFMetaData(AuthKeyspace.NAME, LEGACY_CREDENTIALS_TABLE) != null)
+        if (Schema.instance.getCFMetaData(SchemaConstants.AUTH_KEYSPACE_NAME, LEGACY_CREDENTIALS_TABLE) != null)
             prepareLegacyAuthenticateStatement();
+
+        cache = new CredentialsCache(this);
     }
 
     private void prepareLegacyAuthenticateStatement()
     {
         String query = String.format("SELECT %s from %s.%s WHERE username = ?",
                                      SALTED_HASH,
-                                     AuthKeyspace.NAME,
+                                     SchemaConstants.AUTH_KEYSPACE_NAME,
                                      LEGACY_CREDENTIALS_TABLE);
         legacyAuthenticateStatement = prepare(query);
     }
@@ -158,7 +193,7 @@
 
         String password = credentials.get(PASSWORD_KEY);
         if (password == null)
-            throw new AuthenticationException(String.format("Required key '%s' is missing", PASSWORD_KEY));
+            throw new AuthenticationException(String.format("Required key '%s' is missing for provided username %s", PASSWORD_KEY, username));
 
         return authenticate(username, password);
     }
@@ -168,21 +203,7 @@
         return new PlainTextSaslAuthenticator();
     }
 
-    private AuthenticatedUser doAuthenticate(String username, String password, SelectStatement authenticationStatement)
-    throws RequestExecutionException, AuthenticationException
-    {
-        ResultMessage.Rows rows = authenticationStatement.execute(QueryState.forInternalCalls(),
-                                                                  QueryOptions.forInternalCalls(consistencyForRole(username),
-                                                                                                Lists.newArrayList(ByteBufferUtil.bytes(username))));
-        UntypedResultSet result = UntypedResultSet.create(rows.result);
-
-        if ((result.isEmpty() || !result.one().has(SALTED_HASH)) || !checkpw(password, result.one().getString(SALTED_HASH)))
-            throw new AuthenticationException("Username and/or password are incorrect");
-
-        return new AuthenticatedUser(username);
-    }
-
-    private SelectStatement prepare(String query)
+    private static SelectStatement prepare(String query)
     {
         return (SelectStatement) QueryProcessor.getStatement(query, ClientState.forInternalCalls()).statement;
     }
@@ -221,9 +242,8 @@
          * a user being authorized to act on behalf of another with this IAuthenticator).
          *
          * @param bytes encoded credentials string sent by the client
-         * @return map containing the username/password pairs in the form an IAuthenticator
-         * would expect
-         * @throws javax.security.sasl.SaslException
+         * @throws org.apache.cassandra.exceptions.AuthenticationException if either the
+         *         authnId or password is null
          */
         private void decodeCredentials(byte[] bytes) throws AuthenticationException
         {
@@ -246,13 +266,39 @@
                 }
             }
 
-            if (user == null || user.length == 0)
-                throw new AuthenticationException("Authentication ID must not be null");
             if (pass == null || pass.length == 0)
                 throw new AuthenticationException("Password must not be null");
+            if (user == null || user.length == 0)
+                throw new AuthenticationException("Authentication ID must not be null");
 
             username = new String(user, StandardCharsets.UTF_8);
             password = new String(pass, StandardCharsets.UTF_8);
         }
     }
+
+    private static class CredentialsCache extends AuthCache<String, String> implements CredentialsCacheMBean
+    {
+        private CredentialsCache(PasswordAuthenticator authenticator)
+        {
+            super("CredentialsCache",
+                  DatabaseDescriptor::setCredentialsValidity,
+                  DatabaseDescriptor::getCredentialsValidity,
+                  DatabaseDescriptor::setCredentialsUpdateInterval,
+                  DatabaseDescriptor::getCredentialsUpdateInterval,
+                  DatabaseDescriptor::setCredentialsCacheMaxEntries,
+                  DatabaseDescriptor::getCredentialsCacheMaxEntries,
+                  authenticator::queryHashedPassword,
+                  () -> true);
+        }
+
+        public void invalidateCredentials(String roleName)
+        {
+            invalidate(roleName);
+        }
+    }
+
+    public static interface CredentialsCacheMBean extends AuthCacheMBean
+    {
+        public void invalidateCredentials(String roleName);
+    }
 }
diff --git a/src/java/org/apache/cassandra/auth/PermissionsCache.java b/src/java/org/apache/cassandra/auth/PermissionsCache.java
index ddd6348..981ede8 100644
--- a/src/java/org/apache/cassandra/auth/PermissionsCache.java
+++ b/src/java/org/apache/cassandra/auth/PermissionsCache.java
@@ -18,135 +18,27 @@
 package org.apache.cassandra.auth;
 
 import java.util.Set;
-import java.util.concurrent.*;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
-
-import com.google.common.base.Throwables;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListenableFutureTask;
-import com.google.common.util.concurrent.UncheckedExecutionException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.cassandra.concurrent.DebuggableThreadPoolExecutor;
-import org.apache.cassandra.utils.MBeanWrapper;
 import org.apache.cassandra.utils.Pair;
 
-public class PermissionsCache implements PermissionsCacheMBean
+public class PermissionsCache extends AuthCache<Pair<AuthenticatedUser, IResource>, Set<Permission>> implements PermissionsCacheMBean
 {
-    private static final Logger logger = LoggerFactory.getLogger(PermissionsCache.class);
-
-    private final String MBEAN_NAME = "org.apache.cassandra.auth:type=PermissionsCache";
-
-    private final ThreadPoolExecutor cacheRefreshExecutor = new DebuggableThreadPoolExecutor("PermissionsCacheRefresh",
-                                                                                             Thread.NORM_PRIORITY)
-    {
-        protected void afterExecute(Runnable r, Throwable t)
-        {
-            // empty to avoid logging on background updates
-        }
-    };
-    private final IAuthorizer authorizer;
-    private volatile LoadingCache<Pair<AuthenticatedUser, IResource>, Set<Permission>> cache;
-
     public PermissionsCache(IAuthorizer authorizer)
     {
-        this.authorizer = authorizer;
-        this.cache = initCache(null);
-        MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
+        super("PermissionsCache",
+              DatabaseDescriptor::setPermissionsValidity,
+              DatabaseDescriptor::getPermissionsValidity,
+              DatabaseDescriptor::setPermissionsUpdateInterval,
+              DatabaseDescriptor::getPermissionsUpdateInterval,
+              DatabaseDescriptor::setPermissionsCacheMaxEntries,
+              DatabaseDescriptor::getPermissionsCacheMaxEntries,
+              (p) -> authorizer.authorize(p.left, p.right),
+              () -> DatabaseDescriptor.getAuthorizer().requireAuthorization());
     }
 
     public Set<Permission> getPermissions(AuthenticatedUser user, IResource resource)
     {
-        if (cache == null)
-            return authorizer.authorize(user, resource);
-
-        try
-        {
-            return cache.get(Pair.create(user, resource));
-        }
-        catch (ExecutionException | UncheckedExecutionException e)
-        {
-            Throwables.propagateIfInstanceOf(e.getCause(), RuntimeException.class);
-            throw Throwables.propagate(e);
-        }
-    }
-
-    public void invalidate()
-    {
-        cache = initCache(null);
-    }
-
-    public void setValidity(int validityPeriod)
-    {
-        DatabaseDescriptor.setPermissionsValidity(validityPeriod);
-        cache = initCache(cache);
-    }
-
-    public int getValidity()
-    {
-        return DatabaseDescriptor.getPermissionsValidity();
-    }
-
-    public void setUpdateInterval(int updateInterval)
-    {
-        DatabaseDescriptor.setPermissionsUpdateInterval(updateInterval);
-        cache = initCache(cache);
-    }
-
-    public int getUpdateInterval()
-    {
-        return DatabaseDescriptor.getPermissionsUpdateInterval();
-    }
-
-    private LoadingCache<Pair<AuthenticatedUser, IResource>, Set<Permission>> initCache(
-                                                             LoadingCache<Pair<AuthenticatedUser, IResource>, Set<Permission>> existing)
-    {
-        if (authorizer instanceof AllowAllAuthorizer)
-            return null;
-
-        if (DatabaseDescriptor.getPermissionsValidity() <= 0)
-            return null;
-
-        LoadingCache<Pair<AuthenticatedUser, IResource>, Set<Permission>> newcache = CacheBuilder.newBuilder()
-                           .refreshAfterWrite(DatabaseDescriptor.getPermissionsUpdateInterval(), TimeUnit.MILLISECONDS)
-                           .expireAfterWrite(DatabaseDescriptor.getPermissionsValidity(), TimeUnit.MILLISECONDS)
-                           .maximumSize(DatabaseDescriptor.getPermissionsCacheMaxEntries())
-                           .build(new CacheLoader<Pair<AuthenticatedUser, IResource>, Set<Permission>>()
-                           {
-                               public Set<Permission> load(Pair<AuthenticatedUser, IResource> userResource)
-                               {
-                                   return authorizer.authorize(userResource.left, userResource.right);
-                               }
-
-                               public ListenableFuture<Set<Permission>> reload(final Pair<AuthenticatedUser, IResource> userResource,
-                                                                               final Set<Permission> oldValue)
-                               {
-                                   ListenableFutureTask<Set<Permission>> task = ListenableFutureTask.create(new Callable<Set<Permission>>()
-                                   {
-                                       public Set<Permission>call() throws Exception
-                                       {
-                                           try
-                                           {
-                                               return authorizer.authorize(userResource.left, userResource.right);
-                                           }
-                                           catch (Exception e)
-                                           {
-                                               logger.trace("Error performing async refresh of user permissions", e);
-                                               throw e;
-                                           }
-                                       }
-                                   });
-                                   cacheRefreshExecutor.execute(task);
-                                   return task;
-                               }
-                           });
-        if (existing != null)
-            newcache.putAll(existing.asMap());
-        return newcache;
+        return get(Pair.create(user, resource));
     }
 }
diff --git a/src/java/org/apache/cassandra/auth/PermissionsCacheMBean.java b/src/java/org/apache/cassandra/auth/PermissionsCacheMBean.java
index d07c98f..d370d06 100644
--- a/src/java/org/apache/cassandra/auth/PermissionsCacheMBean.java
+++ b/src/java/org/apache/cassandra/auth/PermissionsCacheMBean.java
@@ -17,15 +17,10 @@
  */
 package org.apache.cassandra.auth;
 
-public interface PermissionsCacheMBean
+/**
+ * Retained since CASSANDRA-7715 for backwards compatibility of MBean interface
+ * classes. This should be removed in the next major version (4.0)
+ */
+public interface PermissionsCacheMBean extends AuthCacheMBean
 {
-    public void invalidate();
-
-    public void setValidity(int validityPeriod);
-
-    public int getValidity();
-
-    public void setUpdateInterval(int updateInterval);
-
-    public int getUpdateInterval();
 }
diff --git a/src/java/org/apache/cassandra/auth/Resources.java b/src/java/org/apache/cassandra/auth/Resources.java
index ebcfc16..653cd46 100644
--- a/src/java/org/apache/cassandra/auth/Resources.java
+++ b/src/java/org/apache/cassandra/auth/Resources.java
@@ -58,6 +58,8 @@
             return DataResource.fromName(name);
         else if (name.startsWith(FunctionResource.root().getName()))
             return FunctionResource.fromName(name);
+        else if (name.startsWith(JMXResource.root().getName()))
+            return JMXResource.fromName(name);
         else
             throw new IllegalArgumentException(String.format("Name %s is not valid for any resource type", name));
     }
diff --git a/src/java/org/apache/cassandra/auth/RoleOptions.java b/src/java/org/apache/cassandra/auth/RoleOptions.java
index 9609ff3..1205d34 100644
--- a/src/java/org/apache/cassandra/auth/RoleOptions.java
+++ b/src/java/org/apache/cassandra/auth/RoleOptions.java
@@ -90,7 +90,7 @@
     }
 
     /**
-     * Return a Map<String, String> representing custom options
+     * Return a {@code Map<String, String>} representing custom options
      * It is the responsiblity of IRoleManager implementations which support
      * IRoleManager.Option.OPTION to handle type checking and conversion of these
      * values, if present
diff --git a/src/java/org/apache/cassandra/auth/RoleResource.java b/src/java/org/apache/cassandra/auth/RoleResource.java
index 4d66eec..ba91944 100644
--- a/src/java/org/apache/cassandra/auth/RoleResource.java
+++ b/src/java/org/apache/cassandra/auth/RoleResource.java
@@ -20,7 +20,6 @@
 import java.util.Set;
 
 import com.google.common.base.Objects;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import org.apache.commons.lang3.StringUtils;
 
diff --git a/src/java/org/apache/cassandra/auth/Roles.java b/src/java/org/apache/cassandra/auth/Roles.java
index 4b14531..2b1ff6e 100644
--- a/src/java/org/apache/cassandra/auth/Roles.java
+++ b/src/java/org/apache/cassandra/auth/Roles.java
@@ -35,7 +35,7 @@
     /**
      * Get all roles granted to the supplied Role, including both directly granted
      * and inherited roles.
-     * The returned roles may be cached if roles_validity_in_ms > 0
+     * The returned roles may be cached if {@code roles_validity_in_ms > 0}
      *
      * @param primaryRole the Role
      * @return set of all granted Roles for the primary Role
diff --git a/src/java/org/apache/cassandra/auth/RolesCache.java b/src/java/org/apache/cassandra/auth/RolesCache.java
index a8dae21..a02828a 100644
--- a/src/java/org/apache/cassandra/auth/RolesCache.java
+++ b/src/java/org/apache/cassandra/auth/RolesCache.java
@@ -18,132 +18,26 @@
 package org.apache.cassandra.auth;
 
 import java.util.Set;
-import java.util.concurrent.*;
 
-import com.google.common.base.Throwables;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListenableFutureTask;
-import com.google.common.util.concurrent.UncheckedExecutionException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.cassandra.concurrent.DebuggableThreadPoolExecutor;
 import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.utils.MBeanWrapper;
 
-public class RolesCache implements RolesCacheMBean
+public class RolesCache extends AuthCache<RoleResource, Set<RoleResource>> implements RolesCacheMBean
 {
-    private static final Logger logger = LoggerFactory.getLogger(RolesCache.class);
-
-    private final String MBEAN_NAME = "org.apache.cassandra.auth:type=RolesCache";
-    private final ThreadPoolExecutor cacheRefreshExecutor = new DebuggableThreadPoolExecutor("RolesCacheRefresh",
-                                                                                             Thread.NORM_PRIORITY)
-    {
-        protected void afterExecute(Runnable r, Throwable t)
-        {
-            // empty to avoid logging on background updates
-        }
-    };
-    private final IRoleManager roleManager;
-    private volatile LoadingCache<RoleResource, Set<RoleResource>> cache;
-
     public RolesCache(IRoleManager roleManager)
     {
-        this.roleManager = roleManager;
-        this.cache = initCache(null);
-        MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
+        super("RolesCache",
+              DatabaseDescriptor::setRolesValidity,
+              DatabaseDescriptor::getRolesValidity,
+              DatabaseDescriptor::setRolesUpdateInterval,
+              DatabaseDescriptor::getRolesUpdateInterval,
+              DatabaseDescriptor::setRolesCacheMaxEntries,
+              DatabaseDescriptor::getRolesCacheMaxEntries,
+              (r) -> roleManager.getRoles(r, true),
+              () -> DatabaseDescriptor.getAuthenticator().requireAuthentication());
     }
 
     public Set<RoleResource> getRoles(RoleResource role)
     {
-        if (cache == null)
-            return roleManager.getRoles(role, true);
-
-        try
-        {
-            return cache.get(role);
-        }
-        catch (ExecutionException | UncheckedExecutionException e)
-        {
-            Throwables.propagateIfInstanceOf(e.getCause(), RuntimeException.class);
-            throw Throwables.propagate(e);
-        }
-    }
-
-    public void invalidate()
-    {
-        cache = initCache(null);
-    }
-
-    public void setValidity(int validityPeriod)
-    {
-        DatabaseDescriptor.setRolesValidity(validityPeriod);
-        cache = initCache(cache);
-    }
-
-    public int getValidity()
-    {
-        return DatabaseDescriptor.getRolesValidity();
-    }
-
-    public void setUpdateInterval(int updateInterval)
-    {
-        DatabaseDescriptor.setRolesUpdateInterval(updateInterval);
-        cache = initCache(cache);
-    }
-
-    public int getUpdateInterval()
-    {
-        return DatabaseDescriptor.getRolesUpdateInterval();
-    }
-
-
-    private LoadingCache<RoleResource, Set<RoleResource>> initCache(LoadingCache<RoleResource, Set<RoleResource>> existing)
-    {
-        if (!DatabaseDescriptor.getAuthenticator().requireAuthentication())
-            return null;
-
-        if (DatabaseDescriptor.getRolesValidity() <= 0)
-            return null;
-
-        LoadingCache<RoleResource, Set<RoleResource>> newcache = CacheBuilder.newBuilder()
-                .refreshAfterWrite(DatabaseDescriptor.getRolesUpdateInterval(), TimeUnit.MILLISECONDS)
-                .expireAfterWrite(DatabaseDescriptor.getRolesValidity(), TimeUnit.MILLISECONDS)
-                .maximumSize(DatabaseDescriptor.getRolesCacheMaxEntries())
-                .build(new CacheLoader<RoleResource, Set<RoleResource>>()
-                {
-                    public Set<RoleResource> load(RoleResource primaryRole)
-                    {
-                        return roleManager.getRoles(primaryRole, true);
-                    }
-
-                    public ListenableFuture<Set<RoleResource>> reload(final RoleResource primaryRole,
-                                                                      final Set<RoleResource> oldValue)
-                    {
-                        ListenableFutureTask<Set<RoleResource>> task;
-                        task = ListenableFutureTask.create(new Callable<Set<RoleResource>>()
-                        {
-                            public Set<RoleResource> call() throws Exception
-                            {
-                                try
-                                {
-                                    return roleManager.getRoles(primaryRole, true);
-                                } catch (Exception e)
-                                {
-                                    logger.trace("Error performing async refresh of user roles", e);
-                                    throw e;
-                                }
-                            }
-                        });
-                        cacheRefreshExecutor.execute(task);
-                        return task;
-                    }
-                });
-        if (existing != null)
-            newcache.putAll(existing.asMap());
-        return newcache;
+        return get(role);
     }
 }
diff --git a/src/java/org/apache/cassandra/auth/RolesCacheMBean.java b/src/java/org/apache/cassandra/auth/RolesCacheMBean.java
index cf270e6..06482d7 100644
--- a/src/java/org/apache/cassandra/auth/RolesCacheMBean.java
+++ b/src/java/org/apache/cassandra/auth/RolesCacheMBean.java
@@ -17,15 +17,10 @@
  */
 package org.apache.cassandra.auth;
 
-public interface RolesCacheMBean
+/**
+ * Retained since CASSANDRA-7715 for backwards compatibility of MBean interface
+ * classes. This should be removed in the next major version (4.0)
+ */
+public interface RolesCacheMBean extends AuthCacheMBean
 {
-    public void invalidate();
-
-    public void setValidity(int validityPeriod);
-
-    public int getValidity();
-
-    public void setUpdateInterval(int updateInterval);
-
-    public int getUpdateInterval();
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/auth/jmx/AuthenticationProxy.java b/src/java/org/apache/cassandra/auth/jmx/AuthenticationProxy.java
new file mode 100644
index 0000000..0c13e3b
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/jmx/AuthenticationProxy.java
@@ -0,0 +1,157 @@
+/*
+ * 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.auth.jmx;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import javax.management.remote.JMXAuthenticator;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.*;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.ConfigurationException;
+
+/**
+ * An alternative to the JAAS based implementation of JMXAuthenticator provided
+ * by the JDK (JMXPluggableAuthenticator).
+ *
+ * Authentication is performed via delegation to a LoginModule. The JAAS login
+ * config is specified by passing its identifier in a custom system property:
+ *     cassandra.jmx.remote.login.config
+ *
+ * The location of the JAAS configuration file containing that config is
+ * specified in the standard way, using the java.security.auth.login.config
+ * system property.
+ *
+ * If authentication is successful then a Subject containing one or more
+ * Principals is returned. This Subject may then be used during authorization
+ * if a JMX authorization is enabled.
+ */
+public final class AuthenticationProxy implements JMXAuthenticator
+{
+    private static Logger logger = LoggerFactory.getLogger(AuthenticationProxy.class);
+
+    // Identifier of JAAS configuration to be used for subject authentication
+    private final String loginConfigName;
+
+    /**
+     * Creates an instance of <code>JMXPluggableAuthenticator</code>
+     * and initializes it with a {@link LoginContext}.
+     *
+     * @param loginConfigName name of the specifig JAAS login configuration to
+     *                        use when authenticating JMX connections
+     * @throws SecurityException if the authentication mechanism cannot be
+     *         initialized.
+     */
+    public AuthenticationProxy(String loginConfigName)
+    {
+        if (loginConfigName == null)
+            throw new ConfigurationException("JAAS login configuration missing for JMX authenticator setup");
+
+        this.loginConfigName = loginConfigName;
+    }
+
+    /**
+     * Perform authentication of the client opening the {@code}MBeanServerConnection{@code}
+     *
+     * @param credentials optionally these credentials may be supplied by the JMX user.
+     *                    Out of the box, the JDK's {@code}RMIServerImpl{@code} is capable
+     *                    of supplying a two element String[], containing username and password.
+     *                    If present, these credentials will be made available to configured
+     *                    {@code}LoginModule{@code}s via {@code}JMXCallbackHandler{@code}.
+     *
+     * @return the authenticated subject containing any {@code}Principal{@code}s added by
+     *the {@code}LoginModule{@code}s
+     *
+     * @throws SecurityException if the server cannot authenticate the user
+     *         with the provided credentials.
+     */
+    public Subject authenticate(Object credentials)
+    {
+        // The credentials object is expected to be a string array holding the subject's
+        // username & password. Those values are made accessible to LoginModules via the
+        // JMXCallbackHandler.
+        JMXCallbackHandler callbackHandler = new JMXCallbackHandler(credentials);
+        try
+        {
+            LoginContext loginContext = new LoginContext(loginConfigName, callbackHandler);
+            loginContext.login();
+            final Subject subject = loginContext.getSubject();
+            if (!subject.isReadOnly())
+            {
+                AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
+                    subject.setReadOnly();
+                    return null;
+                });
+            }
+
+            return subject;
+        }
+        catch (LoginException e)
+        {
+            logger.trace("Authentication exception", e);
+            throw new SecurityException("Authentication error", e);
+        }
+    }
+
+    /**
+     * This callback handler supplies the username and password (which was
+     * optionally supplied by the JMX user) to the JAAS login module performing
+     * the authentication, should it require them . No interactive user
+     * prompting is necessary because the credentials are already available to
+     * this class (via its enclosing class).
+     */
+    private static final class JMXCallbackHandler implements CallbackHandler
+    {
+        private char[] username;
+        private char[] password;
+        private JMXCallbackHandler(Object credentials)
+        {
+            // if username/password credentials were supplied, store them in
+            // the relevant variables to make them accessible to LoginModules
+            // via JMXCallbackHandler
+            if (credentials instanceof String[])
+            {
+                String[] strings = (String[]) credentials;
+                if (strings[0] != null)
+                    username = strings[0].toCharArray();
+                if (strings[1] != null)
+                    password = strings[1].toCharArray();
+            }
+        }
+
+        public void handle(Callback[] callbacks) throws UnsupportedCallbackException
+        {
+            for (int i = 0; i < callbacks.length; i++)
+            {
+                if (callbacks[i] instanceof NameCallback)
+                    ((NameCallback)callbacks[i]).setName(username == null ? null : new String(username));
+                else if (callbacks[i] instanceof PasswordCallback)
+                    ((PasswordCallback)callbacks[i]).setPassword(password == null ? null : password);
+                else
+                    throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback: " + callbacks[i].getClass().getName());
+            }
+        }
+    }
+}
+
diff --git a/src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java b/src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java
index 65f7d20..f266508 100644
--- a/src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java
+++ b/src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java
@@ -21,28 +21,122 @@
 import java.lang.reflect.*;
 import java.security.AccessControlContext;
 import java.security.AccessController;
+import java.security.Principal;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
 import javax.management.InstanceNotFoundException;
 import javax.management.MBeanServer;
+import javax.management.MalformedObjectNameException;
 import javax.management.ObjectName;
 import javax.security.auth.Subject;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.auth.*;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.service.StorageService;
+
 /**
- * Provides a proxy interface to the platform's MBeanServer instance to perform minimal authorization and prevent
- * certain known security issues. In Cassandra 3.11+, this goes even further to include resource-based authorization
- * controls.
+ * Provides a proxy interface to the platform's MBeanServer instance to perform
+ * role-based authorization on method invocation.
  *
- * Certain operations are never allowed for users and these are recorded in a deny list so that we can short circuit
- * authorization process if one is attempted by a remote subject.
+ * When used in conjunction with a suitable JMXAuthenticator, which attaches a CassandraPrincipal
+ * to authenticated Subjects, this class uses the configured IAuthorizer to verify that the
+ * subject has the required permissions to execute methods on the MBeanServer and the MBeans it
+ * manages.
+ *
+ * Because an ObjectName may contain wildcards, meaning it represents a set of individual MBeans,
+ * JMX resources don't fit well with the hierarchical approach modelled by other IResource
+ * implementations and utilised by ClientState::ensureHasPermission etc. To enable grants to use
+ * pattern-type ObjectNames, this class performs its own custom matching and filtering of resources
+ * rather than pushing that down to the configured IAuthorizer. To that end, during authorization
+ * it pulls back all permissions for the active subject, filtering them to retain only grants on
+ * JMXResources. It then uses ObjectName::apply to assert whether the target MBeans are wholly
+ * represented by the resources with permissions. This means that it cannot use the PermissionsCache
+ * as IAuthorizer can, so it manages its own cache locally.
+ *
+ * Methods are split into 2 categories; those which are to be invoked on the MBeanServer itself
+ * and those which apply to MBean instances. Actually, this is somewhat of a construct as in fact
+ * *all* invocations are performed on the MBeanServer instance, the distinction is made here on
+ * those methods which take an ObjectName as their first argument and those which do not.
+ * Invoking a method of the former type, e.g. MBeanServer::getAttribute(ObjectName name, String attribute),
+ * implies that the caller is concerned with a specific MBean. Conversely, invoking a method such as
+ * MBeanServer::getDomains is primarily a function of the MBeanServer itself. This class makes
+ * such a distinction in order to identify which JMXResource the subject requires permissions on.
+ *
+ * Certain operations are never allowed for users and these are recorded in a deny list so that we
+ * can short circuit authorization process if one is attempted by a remote subject.
+ *
  */
 public class AuthorizationProxy implements InvocationHandler
 {
     private static final Logger logger = LoggerFactory.getLogger(AuthorizationProxy.class);
 
+    /*
+     A list of permitted methods on the MBeanServer interface which *do not* take an ObjectName
+     as their first argument. These methods can be thought of as relating to the MBeanServer itself,
+     rather than to the MBeans it manages. All of the allowed methods are essentially descriptive,
+     hence they require the Subject to have the DESCRIBE permission on the root JMX resource.
+     */
+    private static final Set<String> MBEAN_SERVER_ALLOWED_METHODS = ImmutableSet.of("getDefaultDomain",
+                                                                                    "getDomains",
+                                                                                    "getMBeanCount",
+                                                                                    "hashCode",
+                                                                                    "queryMBeans",
+                                                                                    "queryNames",
+                                                                                    "toString");
+
+    /*
+     A list of method names which are never permitted to be executed by a remote user,
+     regardless of privileges they may be granted.
+     */
+    private static final Set<String> DENIED_METHODS = ImmutableSet.of("createMBean",
+                                                                      "deserialize",
+                                                                      "getClassLoader",
+                                                                      "getClassLoaderFor",
+                                                                      "instantiate",
+                                                                      "registerMBean",
+                                                                      "unregisterMBean");
+
+    private static final JMXPermissionsCache permissionsCache = new JMXPermissionsCache();
     private MBeanServer mbs;
 
+    /*
+     Used to check whether the Role associated with the authenticated Subject has superuser
+     status. By default, just delegates to Roles::hasSuperuserStatus, but can be overridden for testing.
+     */
+    protected Function<RoleResource, Boolean> isSuperuser = Roles::hasSuperuserStatus;
+
+    /*
+     Used to retrieve the set of all permissions granted to a given role. By default, this fetches
+     the permissions from the local cache, which in turn loads them from the configured IAuthorizer
+     but can be overridden for testing.
+     */
+    protected Function<RoleResource, Set<PermissionDetails>> getPermissions = permissionsCache::get;
+
+    /*
+     Used to decide whether authorization is enabled or not, usually this depends on the configured
+     IAuthorizer, but can be overridden for testing.
+     */
+    protected Supplier<Boolean> isAuthzRequired = () -> DatabaseDescriptor.getAuthorizer().requireAuthorization();
+
+    /*
+     Used to find matching MBeans when the invocation target is a pattern type ObjectName.
+     Defaults to querying the MBeanServer but can be overridden for testing. See checkPattern for usage.
+     */
+    protected Function<ObjectName, Set<ObjectName>> queryNames = (name) -> mbs.queryNames(name, null);
+
+    /*
+     Used to determine whether auth setup has completed so we know whether the expect the IAuthorizer
+     to be ready. Can be overridden for testing.
+     */
+    protected Supplier<Boolean> isAuthSetupComplete = () -> StorageService.instance.isAuthSetupComplete();
+
     @Override
     public Object invoke(Object proxy, Method method, Object[] args)
             throws Throwable
@@ -76,7 +170,272 @@
             return null;
         }
 
-        return invoke(method, args);
+        if (authorize(subject, methodName, args))
+            return invoke(method, args);
+
+        throw new SecurityException("Access Denied");
+    }
+
+    /**
+     * Performs the actual authorization of an identified subject to execute a remote method invocation.
+     * @param subject The principal making the execution request. A null value represents a local invocation
+     *                from the JMX connector itself
+     * @param methodName Name of the method being invoked
+     * @param args Array containing invocation argument. If the first element is an ObjectName instance, for
+     *             authz purposes we consider this an invocation of an MBean method, otherwise it is treated
+     *             as an invocation of a method on the MBeanServer.
+     */
+    @VisibleForTesting
+    boolean authorize(Subject subject, String methodName, Object[] args)
+    {
+        logger.trace("Authorizing JMX method invocation {} for {}",
+                     methodName,
+                     subject == null ? "" :subject.toString().replaceAll("\\n", " "));
+
+        if (!isAuthSetupComplete.get())
+        {
+            logger.trace("Auth setup is not complete, refusing access");
+            return false;
+        }
+
+        // Permissive authorization is enabled
+        if (!isAuthzRequired.get())
+            return true;
+
+        // Allow operations performed locally on behalf of the connector server itself
+        if (subject == null)
+            return true;
+
+        // Restrict access to certain methods by any remote user
+        if (DENIED_METHODS.contains(methodName))
+        {
+            logger.trace("Access denied to restricted method {}", methodName);
+            return false;
+        }
+
+        // Reject if the user has not authenticated
+        Set<Principal> principals = subject.getPrincipals();
+        if (principals == null || principals.isEmpty())
+            return false;
+
+        // Currently, we assume that the first Principal returned from the Subject
+        // is the one to use for authorization. It would be good to make this more
+        // robust, but we have no control over which Principals a given LoginModule
+        // might choose to associate with the Subject following successful authentication
+        RoleResource userResource = RoleResource.role(principals.iterator().next().getName());
+        // A role with superuser status can do anything
+        if (isSuperuser.apply(userResource))
+            return true;
+
+        // The method being invoked may be a method on an MBean, or it could belong
+        // to the MBeanServer itself
+        if (args != null && args[0] instanceof ObjectName)
+            return authorizeMBeanMethod(userResource, methodName, args);
+        else
+            return authorizeMBeanServerMethod(userResource, methodName);
+    }
+
+    /**
+     * Authorize execution of a method on the MBeanServer which does not take an MBean ObjectName
+     * as its first argument. The allowed methods that match this criteria are generally
+     * descriptive methods concerned with the MBeanServer itself, rather than with any particular
+     * set of MBeans managed by the server and so we check the DESCRIBE permission on the root
+     * JMXResource (representing the MBeanServer)
+     *
+     * @param subject
+     * @param methodName
+     * @return the result of the method invocation, if authorized
+     * @throws Throwable
+     * @throws SecurityException if authorization fails
+     */
+    private boolean authorizeMBeanServerMethod(RoleResource subject, String methodName)
+    {
+        logger.trace("JMX invocation of {} on MBeanServer requires permission {}", methodName, Permission.DESCRIBE);
+        return (MBEAN_SERVER_ALLOWED_METHODS.contains(methodName) &&
+                hasPermission(subject, Permission.DESCRIBE, JMXResource.root()));
+    }
+
+    /**
+     * Authorize execution of a method on an MBean (or set of MBeans) which may be
+     * managed by the MBeanServer. Note that this also includes the queryMBeans and queryNames
+     * methods of MBeanServer as those both take an ObjectName (possibly a pattern containing
+     * wildcards) as their first argument. They both of those methods also accept null arguments,
+     * in which case they will be handled by authorizedMBeanServerMethod
+     *
+     * @param role
+     * @param methodName
+     * @param args
+     * @return the result of the method invocation, if authorized
+     * @throws Throwable
+     * @throws SecurityException if authorization fails
+     */
+    private boolean authorizeMBeanMethod(RoleResource role, String methodName, Object[] args)
+    {
+        ObjectName targetBean = (ObjectName)args[0];
+
+        // work out which permission we need to execute the method being called on the mbean
+        Permission requiredPermission = getRequiredPermission(methodName);
+        if (null == requiredPermission)
+            return false;
+
+        logger.trace("JMX invocation of {} on {} requires permission {}", methodName, targetBean, requiredPermission);
+
+        // find any JMXResources upon which the authenticated subject has been granted the
+        // reqired permission. We'll do ObjectName-specific filtering & matching of resources later
+        Set<JMXResource> permittedResources = getPermittedResources(role, requiredPermission);
+
+        if (permittedResources.isEmpty())
+            return false;
+
+        // finally, check the JMXResource from the grants to see if we have either
+        // an exact match or a wildcard match for the target resource, whichever is
+        // applicable
+        return targetBean.isPattern()
+                ? checkPattern(targetBean, permittedResources)
+                : checkExact(targetBean, permittedResources);
+    }
+
+    /**
+     * Get any grants of the required permission for the authenticated subject, regardless
+     * of the resource the permission applies to as we'll do the filtering & matching in
+     * the calling method
+     * @param subject
+     * @param required
+     * @return the set of JMXResources upon which the subject has been granted the required permission
+     */
+    private Set<JMXResource> getPermittedResources(RoleResource subject, Permission required)
+    {
+        return getPermissions.apply(subject)
+               .stream()
+               .filter(details -> details.permission == required)
+               .map(details -> (JMXResource)details.resource)
+               .collect(Collectors.toSet());
+    }
+
+    /**
+     * Check whether a required permission has been granted to the authenticated subject on a specific resource
+     * @param subject
+     * @param permission
+     * @param resource
+     * @return true if the Subject has been granted the required permission on the specified resource; false otherwise
+     */
+    private boolean hasPermission(RoleResource subject, Permission permission, JMXResource resource)
+    {
+        return getPermissions.apply(subject)
+               .stream()
+               .anyMatch(details -> details.permission == permission && details.resource.equals(resource));
+    }
+
+    /**
+     * Given a set of JMXResources upon which the Subject has been granted a particular permission,
+     * check whether any match the pattern-type ObjectName representing the target of the method
+     * invocation. At this point, we are sure that whatever the required permission, the Subject
+     * has definitely been granted it against this set of JMXResources. The job of this method is
+     * only to verify that the target of the invocation is covered by the members of the set.
+     *
+     * @param target
+     * @param permittedResources
+     * @return true if all registered beans which match the target can also be matched by the
+     *         JMXResources the subject has been granted permissions on; false otherwise
+     */
+    private boolean checkPattern(ObjectName target, Set<JMXResource> permittedResources)
+    {
+        // if the required permission was granted on the root JMX resource, then we're done
+        if (permittedResources.contains(JMXResource.root()))
+            return true;
+
+        // Get the full set of beans which match the target pattern
+        Set<ObjectName> targetNames = queryNames.apply(target);
+
+        // Iterate over the resources the permission has been granted on. Some of these may
+        // be patterns, so query the server to retrieve the full list of matching names and
+        // remove those from the target set. Once the target set is empty (i.e. all required
+        // matches have been satisfied), the requirement is met.
+        // If there are still unsatisfied targets after all the JMXResources have been processed,
+        // there are insufficient grants to permit the operation.
+        for (JMXResource resource : permittedResources)
+        {
+            try
+            {
+                Set<ObjectName> matchingNames = queryNames.apply(ObjectName.getInstance(resource.getObjectName()));
+                targetNames.removeAll(matchingNames);
+                if (targetNames.isEmpty())
+                    return true;
+            }
+            catch (MalformedObjectNameException e)
+            {
+                logger.warn("Permissions for JMX resource contains invalid ObjectName {}", resource.getObjectName());
+            }
+        }
+
+        logger.trace("Subject does not have sufficient permissions on all MBeans matching the target pattern {}", target);
+        return false;
+    }
+
+    /**
+     * Given a set of JMXResources upon which the Subject has been granted a particular permission,
+     * check whether any match the ObjectName representing the target of the method invocation.
+     * At this point, we are sure that whatever the required permission, the Subject has definitely
+     * been granted it against this set of JMXResources. The job of this method is only to verify
+     * that the target of the invocation is matched by a member of the set.
+     *
+     * @param target
+     * @param permittedResources
+     * @return true if at least one of the permitted resources matches the target; false otherwise
+     */
+    private boolean checkExact(ObjectName target, Set<JMXResource> permittedResources)
+    {
+        // if the required permission was granted on the root JMX resource, then we're done
+        if (permittedResources.contains(JMXResource.root()))
+            return true;
+
+        for (JMXResource resource : permittedResources)
+        {
+            try
+            {
+                if (ObjectName.getInstance(resource.getObjectName()).apply(target))
+                    return true;
+            }
+            catch (MalformedObjectNameException e)
+            {
+                logger.warn("Permissions for JMX resource contains invalid ObjectName {}", resource.getObjectName());
+            }
+        }
+
+        logger.trace("Subject does not have sufficient permissions on target MBean {}", target);
+        return false;
+    }
+
+    /**
+     * Mapping between method names and the permission required to invoke them. Note, these
+     * names refer to methods on MBean instances invoked via the MBeanServer.
+     * @param methodName
+     * @return
+     */
+    private static Permission getRequiredPermission(String methodName)
+    {
+        switch (methodName)
+        {
+            case "getAttribute":
+            case "getAttributes":
+                return Permission.SELECT;
+            case "setAttribute":
+            case "setAttributes":
+                return Permission.MODIFY;
+            case "invoke":
+                return Permission.EXECUTE;
+            case "getInstanceOf":
+            case "getMBeanInfo":
+            case "hashCode":
+            case "isInstanceOf":
+            case "isRegistered":
+            case "queryMBeans":
+            case "queryNames":
+                return Permission.DESCRIBE;
+            default:
+                logger.debug("Access denied, method name {} does not map to any defined permission", methodName);
+                return null;
+        }
     }
 
     /**
@@ -103,6 +462,24 @@
         }
     }
 
+    /**
+     * Query the configured IAuthorizer for the set of all permissions granted on JMXResources to a specific subject
+     * @param subject
+     * @return All permissions granted to the specfied subject (including those transitively inherited from
+     *         any roles the subject has been granted), filtered to include only permissions granted on
+     *         JMXResources
+     */
+    private static Set<PermissionDetails> loadPermissions(RoleResource subject)
+    {
+        // get all permissions for the specified subject. We'll cache them as it's likely
+        // we'll receive multiple lookups for the same subject (but for different resources
+        // and permissions) in quick succession
+        return DatabaseDescriptor.getAuthorizer().list(AuthenticatedUser.SYSTEM_USER, Permission.ALL, null, subject)
+                                                 .stream()
+                                                 .filter(details -> details.resource instanceof JMXResource)
+                                                 .collect(Collectors.toSet());
+    }
+
     private void checkVulnerableMethods(Object args[])
     {
         assert args.length == 4;
@@ -158,7 +535,8 @@
         {
             if (!mbs.isInstanceOf(name, "javax.management.loading.MLet"))
                 return;
-        } catch (InstanceNotFoundException infe)
+        }
+        catch (InstanceNotFoundException infe)
         {
             return;
         }
@@ -166,5 +544,20 @@
         if (operation.equals("addURL") || operation.equals("getMBeansFromURL"))
             throw new SecurityException("Access is denied!");
     }
-}
 
+    private static final class JMXPermissionsCache extends AuthCache<RoleResource, Set<PermissionDetails>>
+    {
+        protected JMXPermissionsCache()
+        {
+            super("JMXPermissionsCache",
+                  DatabaseDescriptor::setPermissionsValidity,
+                  DatabaseDescriptor::getPermissionsValidity,
+                  DatabaseDescriptor::setPermissionsUpdateInterval,
+                  DatabaseDescriptor::getPermissionsUpdateInterval,
+                  DatabaseDescriptor::setPermissionsCacheMaxEntries,
+                  DatabaseDescriptor::getPermissionsCacheMaxEntries,
+                  AuthorizationProxy::loadPermissions,
+                  () -> true);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/batchlog/BatchlogManager.java b/src/java/org/apache/cassandra/batchlog/BatchlogManager.java
index 100c860..55993ef 100644
--- a/src/java/org/apache/cassandra/batchlog/BatchlogManager.java
+++ b/src/java/org/apache/cassandra/batchlog/BatchlogManager.java
@@ -44,12 +44,12 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.util.concurrent.RateLimiter;
-import org.apache.cassandra.db.RowUpdateBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.concurrent.DebuggableScheduledThreadPoolExecutor;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
@@ -134,20 +134,15 @@
 
     public static void store(Batch batch, boolean durableWrites)
     {
-        RowUpdateBuilder builder =
-            new RowUpdateBuilder(SystemKeyspace.Batches, batch.creationTime, batch.id)
-                .clustering()
-                .add("version", MessagingService.current_version);
-
-        for (ByteBuffer mutation : batch.encodedMutations)
-            builder.addListEntry("mutations", mutation);
+        List<ByteBuffer> mutations = new ArrayList<>(batch.encodedMutations.size() + batch.decodedMutations.size());
+        mutations.addAll(batch.encodedMutations);
 
         for (Mutation mutation : batch.decodedMutations)
         {
             try (DataOutputBuffer buffer = new DataOutputBuffer())
             {
                 Mutation.serializer.serialize(mutation, buffer, MessagingService.current_version);
-                builder.addListEntry("mutations", buffer.buffer());
+                mutations.add(buffer.buffer());
             }
             catch (IOException e)
             {
@@ -156,13 +151,19 @@
             }
         }
 
-        builder.build().apply(durableWrites);
+        PartitionUpdate.SimpleBuilder builder = PartitionUpdate.simpleBuilder(SystemKeyspace.Batches, batch.id);
+        builder.row()
+               .timestamp(batch.creationTime)
+               .add("version", MessagingService.current_version)
+               .appendAll("mutations", mutations);
+
+        builder.buildAsMutation().apply(durableWrites);
     }
 
     @VisibleForTesting
     public int countAllBatches()
     {
-        String query = String.format("SELECT count(*) FROM %s.%s", SystemKeyspace.NAME, SystemKeyspace.BATCHES);
+        String query = String.format("SELECT count(*) FROM %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.BATCHES);
         UntypedResultSet results = executeInternal(query);
         if (results == null || results.isEmpty())
             return 0;
@@ -208,13 +209,13 @@
         RateLimiter rateLimiter = RateLimiter.create(throttleInKB == 0 ? Double.MAX_VALUE : throttleInKB * 1024);
 
         UUID limitUuid = UUIDGen.maxTimeUUID(System.currentTimeMillis() - getBatchlogTimeout());
-        ColumnFamilyStore store = Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.BATCHES);
+        ColumnFamilyStore store = Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.BATCHES);
         int pageSize = calculatePageSize(store);
         // There cannot be any live content where token(id) <= token(lastReplayedUuid) as every processed batch is
         // deleted, but the tombstoned content may still be present in the tables. To avoid walking over it we specify
         // token(id) > token(lastReplayedUuid) as part of the query.
         String query = String.format("SELECT id, mutations, version FROM %s.%s WHERE token(id) > token(?) AND token(id) <= token(?)",
-                                     SystemKeyspace.NAME,
+                                     SchemaConstants.SYSTEM_KEYSPACE_NAME,
                                      SystemKeyspace.BATCHES);
         UntypedResultSet batches = executeInternalWithPaging(query, pageSize, lastReplayedUuid, limitUuid);
         processBatchlogEntries(batches, pageSize, rateLimiter);
@@ -467,7 +468,7 @@
             if (liveEndpoints.isEmpty())
                 return null;
 
-            ReplayWriteResponseHandler<Mutation> handler = new ReplayWriteResponseHandler<>(liveEndpoints);
+            ReplayWriteResponseHandler<Mutation> handler = new ReplayWriteResponseHandler<>(liveEndpoints, System.nanoTime());
             MessageOut<Mutation> message = mutation.createMessage();
             for (InetAddress endpoint : liveEndpoints)
                 MessagingService.instance().sendRR(message, endpoint, handler, false);
@@ -490,9 +491,9 @@
         {
             private final Set<InetAddress> undelivered = Collections.newSetFromMap(new ConcurrentHashMap<>());
 
-            ReplayWriteResponseHandler(Collection<InetAddress> writeEndpoints)
+            ReplayWriteResponseHandler(Collection<InetAddress> writeEndpoints, long queryStartNanoTime)
             {
-                super(writeEndpoints, Collections.<InetAddress>emptySet(), null, null, null, WriteType.UNLOGGED_BATCH);
+                super(writeEndpoints, Collections.<InetAddress>emptySet(), null, null, null, WriteType.UNLOGGED_BATCH, queryStartNanoTime);
                 undelivered.addAll(writeEndpoints);
             }
 
diff --git a/src/java/org/apache/cassandra/batchlog/LegacyBatchlogMigrator.java b/src/java/org/apache/cassandra/batchlog/LegacyBatchlogMigrator.java
index dd19f19..4592488 100644
--- a/src/java/org/apache/cassandra/batchlog/LegacyBatchlogMigrator.java
+++ b/src/java/org/apache/cassandra/batchlog/LegacyBatchlogMigrator.java
@@ -26,6 +26,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.db.*;
@@ -53,7 +54,7 @@
     @SuppressWarnings("deprecation")
     public static void migrate()
     {
-        ColumnFamilyStore store = Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.LEGACY_BATCHLOG);
+        ColumnFamilyStore store = Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.LEGACY_BATCHLOG);
 
         // nothing to migrate
         if (store.isEmpty())
@@ -63,7 +64,7 @@
 
         int convertedBatches = 0;
         String query = String.format("SELECT id, data, written_at, version FROM %s.%s",
-                                     SystemKeyspace.NAME,
+                                     SchemaConstants.SYSTEM_KEYSPACE_NAME,
                                      SystemKeyspace.LEGACY_BATCHLOG);
 
         int pageSize = BatchlogManager.calculatePageSize(store);
@@ -82,7 +83,7 @@
     @SuppressWarnings("deprecation")
     public static boolean isLegacyBatchlogMutation(Mutation mutation)
     {
-        return mutation.getKeyspaceName().equals(SystemKeyspace.NAME)
+        return mutation.getKeyspaceName().equals(SchemaConstants.SYSTEM_KEYSPACE_NAME)
             && mutation.getPartitionUpdate(SystemKeyspace.LegacyBatchlog.cfId) != null;
     }
 
@@ -137,14 +138,15 @@
         }
     }
 
-    public static void asyncRemoveFromBatchlog(Collection<InetAddress> endpoints, UUID uuid)
+    public static void asyncRemoveFromBatchlog(Collection<InetAddress> endpoints, UUID uuid, long queryStartNanoTime)
     {
         AbstractWriteResponseHandler<IMutation> handler = new WriteResponseHandler<>(endpoints,
                                                                                      Collections.<InetAddress>emptyList(),
                                                                                      ConsistencyLevel.ANY,
-                                                                                     Keyspace.open(SystemKeyspace.NAME),
+                                                                                     Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME),
                                                                                      null,
-                                                                                     WriteType.SIMPLE);
+                                                                                     WriteType.SIMPLE,
+                                                                                     queryStartNanoTime);
         Mutation mutation = getRemoveMutation(uuid);
 
         for (InetAddress target : endpoints)
@@ -162,12 +164,13 @@
     @SuppressWarnings("deprecation")
     static Mutation getStoreMutation(Batch batch, int version)
     {
-        return new RowUpdateBuilder(SystemKeyspace.LegacyBatchlog, batch.creationTime, batch.id)
-               .clustering()
+        PartitionUpdate.SimpleBuilder builder = PartitionUpdate.simpleBuilder(SystemKeyspace.LegacyBatchlog, batch.id);
+        builder.row()
+               .timestamp(batch.creationTime)
                .add("written_at", new Date(batch.creationTime / 1000))
                .add("data", getSerializedMutations(version, batch.decodedMutations))
-               .add("version", version)
-               .build();
+               .add("version", version);
+        return builder.buildAsMutation();
     }
 
     @SuppressWarnings("deprecation")
diff --git a/src/java/org/apache/cassandra/cache/AutoSavingCache.java b/src/java/org/apache/cassandra/cache/AutoSavingCache.java
index 3b7a6b8..e7cda16 100644
--- a/src/java/org/apache/cassandra/cache/AutoSavingCache.java
+++ b/src/java/org/apache/cassandra/cache/AutoSavingCache.java
@@ -37,15 +37,15 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.db.ColumnFamilyStore;
-import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.db.compaction.CompactionInfo;
 import org.apache.cassandra.db.compaction.CompactionManager;
 import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.db.compaction.CompactionInfo.Unit;
 import org.apache.cassandra.io.FSWriteError;
 import org.apache.cassandra.io.util.*;
-import org.apache.cassandra.io.util.ChecksummedRandomAccessReader.CorruptFileException;
+import org.apache.cassandra.io.util.CorruptFileException;
 import org.apache.cassandra.io.util.DataInputPlus.DataInputStreamPlus;
 import org.apache.cassandra.service.CacheService;
 import org.apache.cassandra.utils.JVMStabilityInspector;
@@ -78,19 +78,26 @@
      * a minor version letter.
      *
      * Sticking with "d" is fine for 3.0 since it has never been released or used by another version
+     *
+     * "e" introduced with CASSANDRA-11206, omits IndexInfo from key-cache, stores offset into index-file
      */
-    private static final String CURRENT_VERSION = "d";
+    private static final String CURRENT_VERSION = "e";
 
     private static volatile IStreamFactory streamFactory = new IStreamFactory()
     {
+        private final SequentialWriterOption writerOption = SequentialWriterOption.newBuilder()
+                                                                    .trickleFsync(DatabaseDescriptor.getTrickleFsync())
+                                                                    .trickleFsyncByteInterval(DatabaseDescriptor.getTrickleFsyncIntervalInKb() * 1024)
+                                                                    .finishOnClose(true).build();
+
         public InputStream getInputStream(File dataPath, File crcPath) throws IOException
         {
-            return new ChecksummedRandomAccessReader.Builder(dataPath, crcPath).build();
+            return ChecksummedRandomAccessReader.open(dataPath, crcPath);
         }
 
         public OutputStream getOutputStream(File dataPath, File crcPath)
         {
-            return SequentialWriter.open(dataPath, crcPath).finishOnClose();
+            return new ChecksummedSequentialWriter(dataPath, crcPath, null, writerOption);
         }
     };
 
@@ -153,12 +160,13 @@
         ListenableFuture<Integer> cacheLoad = es.submit(new Callable<Integer>()
         {
             @Override
-            public Integer call() throws Exception
+            public Integer call()
             {
                 return loadSaved();
             }
         });
-        cacheLoad.addListener(new Runnable() {
+        cacheLoad.addListener(new Runnable()
+        {
             @Override
             public void run()
             {
@@ -187,7 +195,7 @@
             DataInputStreamPlus in = null;
             try
             {
-                logger.info(String.format("reading saved cache %s", dataPath));
+                logger.info("reading saved cache {}", dataPath);
                 in = new DataInputStreamPlus(new LengthAvailableInputStream(new BufferedInputStream(streamFactory.getInputStream(dataPath, crcPath)), dataPath.length()));
 
                 //Check the schema has not changed since CFs are looked up by name which is ambiguous
@@ -303,7 +311,7 @@
             else
                 type = OperationType.UNKNOWN;
 
-            info = new CompactionInfo(CFMetaData.createFake(SystemKeyspace.NAME, cacheType.toString()),
+            info = new CompactionInfo(CFMetaData.createFake(SchemaConstants.SYSTEM_KEYSPACE_NAME, cacheType.toString()),
                                       type,
                                       0,
                                       keysEstimate,
diff --git a/src/java/org/apache/cassandra/cache/CacheSize.java b/src/java/org/apache/cassandra/cache/CacheSize.java
new file mode 100644
index 0000000..71365bb
--- /dev/null
+++ b/src/java/org/apache/cassandra/cache/CacheSize.java
@@ -0,0 +1,34 @@
+/*
+ *
+ * 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.cache;
+
+public interface CacheSize
+{
+
+    long capacity();
+
+    void setCapacity(long capacity);
+
+    int size();
+
+    long weightedSize();
+
+}
diff --git a/src/java/org/apache/cassandra/cache/ChunkCache.java b/src/java/org/apache/cassandra/cache/ChunkCache.java
new file mode 100644
index 0000000..4e7f848
--- /dev/null
+++ b/src/java/org/apache/cassandra/cache/ChunkCache.java
@@ -0,0 +1,322 @@
+/*
+ *
+ * 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.cache;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import com.github.benmanes.caffeine.cache.*;
+import com.codahale.metrics.Timer;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.io.sstable.CorruptSSTableException;
+import org.apache.cassandra.io.util.*;
+import org.apache.cassandra.metrics.CacheMissMetrics;
+import org.apache.cassandra.utils.memory.BufferPool;
+
+public class ChunkCache
+        implements CacheLoader<ChunkCache.Key, ChunkCache.Buffer>, RemovalListener<ChunkCache.Key, ChunkCache.Buffer>, CacheSize
+{
+    public static final int RESERVED_POOL_SPACE_IN_MB = 32;
+    public static final long cacheSize = 1024L * 1024L * Math.max(0, DatabaseDescriptor.getFileCacheSizeInMB() - RESERVED_POOL_SPACE_IN_MB);
+    public static final boolean roundUp = DatabaseDescriptor.getFileCacheRoundUp();
+
+    private static boolean enabled = cacheSize > 0;
+    public static final ChunkCache instance = enabled ? new ChunkCache() : null;
+
+    private final LoadingCache<Key, Buffer> cache;
+    public final CacheMissMetrics metrics;
+
+    static class Key
+    {
+        final ChunkReader file;
+        final String path;
+        final long position;
+
+        public Key(ChunkReader file, long position)
+        {
+            super();
+            this.file = file;
+            this.position = position;
+            this.path = file.channel().filePath();
+        }
+
+        public int hashCode()
+        {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + path.hashCode();
+            result = prime * result + file.getClass().hashCode();
+            result = prime * result + Long.hashCode(position);
+            return result;
+        }
+
+        public boolean equals(Object obj)
+        {
+            if (this == obj)
+                return true;
+            if (obj == null)
+                return false;
+
+            Key other = (Key) obj;
+            return (position == other.position)
+                    && file.getClass() == other.file.getClass()
+                    && path.equals(other.path);
+        }
+    }
+
+    static class Buffer implements Rebufferer.BufferHolder
+    {
+        private final ByteBuffer buffer;
+        private final long offset;
+        private final AtomicInteger references;
+
+        public Buffer(ByteBuffer buffer, long offset)
+        {
+            this.buffer = buffer;
+            this.offset = offset;
+            references = new AtomicInteger(1);  // start referenced.
+        }
+
+        Buffer reference()
+        {
+            int refCount;
+            do
+            {
+                refCount = references.get();
+                if (refCount == 0)
+                    // Buffer was released before we managed to reference it.
+                    return null;
+            } while (!references.compareAndSet(refCount, refCount + 1));
+
+            return this;
+        }
+
+        @Override
+        public ByteBuffer buffer()
+        {
+            assert references.get() > 0;
+            return buffer.duplicate();
+        }
+
+        @Override
+        public long offset()
+        {
+            return offset;
+        }
+
+        @Override
+        public void release()
+        {
+            if (references.decrementAndGet() == 0)
+                BufferPool.put(buffer);
+        }
+    }
+
+    public ChunkCache()
+    {
+        cache = Caffeine.newBuilder()
+                .maximumWeight(cacheSize)
+                .executor(MoreExecutors.directExecutor())
+                .weigher((key, buffer) -> ((Buffer) buffer).buffer.capacity())
+                .removalListener(this)
+                .build(this);
+        metrics = new CacheMissMetrics("ChunkCache", this);
+    }
+
+    @Override
+    public Buffer load(Key key) throws Exception
+    {
+        ChunkReader rebufferer = key.file;
+        metrics.misses.mark();
+        try (Timer.Context ctx = metrics.missLatency.time())
+        {
+            ByteBuffer buffer = BufferPool.get(key.file.chunkSize(), key.file.preferredBufferType());
+            assert buffer != null;
+            rebufferer.readChunk(key.position, buffer);
+            return new Buffer(buffer, key.position);
+        }
+    }
+
+    @Override
+    public void onRemoval(Key key, Buffer buffer, RemovalCause cause)
+    {
+        buffer.release();
+    }
+
+    public void close()
+    {
+        cache.invalidateAll();
+    }
+
+    public RebuffererFactory wrap(ChunkReader file)
+    {
+        return new CachingRebufferer(file);
+    }
+
+    public static RebuffererFactory maybeWrap(ChunkReader file)
+    {
+        if (!enabled)
+            return file;
+
+        return instance.wrap(file);
+    }
+
+    public void invalidatePosition(FileHandle dfile, long position)
+    {
+        if (!(dfile.rebuffererFactory() instanceof CachingRebufferer))
+            return;
+
+        ((CachingRebufferer) dfile.rebuffererFactory()).invalidate(position);
+    }
+
+    public void invalidateFile(String fileName)
+    {
+        cache.invalidateAll(Iterables.filter(cache.asMap().keySet(), x -> x.path.equals(fileName)));
+    }
+
+    @VisibleForTesting
+    public void enable(boolean enabled)
+    {
+        ChunkCache.enabled = enabled;
+        cache.invalidateAll();
+        metrics.reset();
+    }
+
+    // TODO: Invalidate caches for obsoleted/MOVED_START tables?
+
+    /**
+     * Rebufferer providing cached chunks where data is obtained from the specified ChunkReader.
+     * Thread-safe. One instance per SegmentedFile, created by ChunkCache.maybeWrap if the cache is enabled.
+     */
+    class CachingRebufferer implements Rebufferer, RebuffererFactory
+    {
+        private final ChunkReader source;
+        final long alignmentMask;
+
+        public CachingRebufferer(ChunkReader file)
+        {
+            source = file;
+            int chunkSize = file.chunkSize();
+            assert Integer.bitCount(chunkSize) == 1 : String.format("%d must be a power of two", chunkSize);
+            alignmentMask = -chunkSize;
+        }
+
+        @Override
+        public Buffer rebuffer(long position)
+        {
+            try
+            {
+                metrics.requests.mark();
+                long pageAlignedPos = position & alignmentMask;
+                Buffer buf;
+                do
+                    buf = cache.get(new Key(source, pageAlignedPos)).reference();
+                while (buf == null);
+
+                return buf;
+            }
+            catch (Throwable t)
+            {
+                Throwables.propagateIfInstanceOf(t.getCause(), CorruptSSTableException.class);
+                throw Throwables.propagate(t);
+            }
+        }
+
+        public void invalidate(long position)
+        {
+            long pageAlignedPos = position & alignmentMask;
+            cache.invalidate(new Key(source, pageAlignedPos));
+        }
+
+        @Override
+        public Rebufferer instantiateRebufferer()
+        {
+            return this;
+        }
+
+        @Override
+        public void close()
+        {
+            source.close();
+        }
+
+        @Override
+        public void closeReader()
+        {
+            // Instance is shared among readers. Nothing to release.
+        }
+
+        @Override
+        public ChannelProxy channel()
+        {
+            return source.channel();
+        }
+
+        @Override
+        public long fileLength()
+        {
+            return source.fileLength();
+        }
+
+        @Override
+        public double getCrcCheckChance()
+        {
+            return source.getCrcCheckChance();
+        }
+
+        @Override
+        public String toString()
+        {
+            return "CachingRebufferer:" + source.toString();
+        }
+    }
+
+    @Override
+    public long capacity()
+    {
+        return cacheSize;
+    }
+
+    @Override
+    public void setCapacity(long capacity)
+    {
+        throw new UnsupportedOperationException("Chunk cache size cannot be changed.");
+    }
+
+    @Override
+    public int size()
+    {
+        return cache.asMap().size();
+    }
+
+    @Override
+    public long weightedSize()
+    {
+        return cache.policy().eviction()
+                .map(policy -> policy.weightedSize().orElseGet(cache::estimatedSize))
+                .orElseGet(cache::estimatedSize);
+    }
+}
diff --git a/src/java/org/apache/cassandra/cache/ICache.java b/src/java/org/apache/cassandra/cache/ICache.java
index 37b55cd..7ca6b2e 100644
--- a/src/java/org/apache/cassandra/cache/ICache.java
+++ b/src/java/org/apache/cassandra/cache/ICache.java
@@ -24,12 +24,8 @@
  * and does not require put or remove to return values, which lets SerializingCache
  * be more efficient by avoiding deserialize except on get.
  */
-public interface ICache<K, V>
+public interface ICache<K, V> extends CacheSize
 {
-    public long capacity();
-
-    public void setCapacity(long capacity);
-
     public void put(K key, V value);
 
     public boolean putIfAbsent(K key, V value);
@@ -40,10 +36,6 @@
 
     public void remove(K key);
 
-    public int size();
-
-    public long weightedSize();
-
     public void clear();
 
     public Iterator<K> keyIterator();
diff --git a/src/java/org/apache/cassandra/cache/IMeasurableMemory.java b/src/java/org/apache/cassandra/cache/IMeasurableMemory.java
index 149bff6..df79d36 100644
--- a/src/java/org/apache/cassandra/cache/IMeasurableMemory.java
+++ b/src/java/org/apache/cassandra/cache/IMeasurableMemory.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.cache;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
diff --git a/src/java/org/apache/cassandra/cache/NopCacheProvider.java b/src/java/org/apache/cassandra/cache/NopCacheProvider.java
index 20f837a..9b8a3dc 100644
--- a/src/java/org/apache/cassandra/cache/NopCacheProvider.java
+++ b/src/java/org/apache/cassandra/cache/NopCacheProvider.java
@@ -36,6 +36,11 @@
 
         public void setCapacity(long capacity)
         {
+            if (capacity != 0)
+            {
+                throw new UnsupportedOperationException("Setting capacity of " + NopCache.class.getSimpleName()
+                                                        + " is not permitted as this cache is disabled. Check your yaml settings if you want to enable it.");
+            }
         }
 
         public void put(RowCacheKey key, IRowCacheEntry value)
diff --git a/src/java/org/apache/cassandra/cache/SerializingCache.java b/src/java/org/apache/cassandra/cache/SerializingCache.java
index 3651a0c..0ece686 100644
--- a/src/java/org/apache/cassandra/cache/SerializingCache.java
+++ b/src/java/org/apache/cassandra/cache/SerializingCache.java
@@ -31,6 +31,7 @@
 import org.apache.cassandra.io.util.MemoryInputStream;
 import org.apache.cassandra.io.util.MemoryOutputStream;
 import org.apache.cassandra.io.util.WrappedDataOutputStreamPlus;
+import org.apache.cassandra.utils.FBUtilities;
 
 /**
  * Serializes cache values off-heap.
@@ -99,7 +100,7 @@
     {
         long serializedSize = serializer.serializedSize(value);
         if (serializedSize > Integer.MAX_VALUE)
-            throw new IllegalArgumentException("Unable to allocate " + serializedSize + " bytes");
+            throw new IllegalArgumentException(String.format("Unable to allocate %s", FBUtilities.prettyPrintMemory(serializedSize)));
 
         RefCountedMemory freeableMemory;
         try
diff --git a/src/java/org/apache/cassandra/concurrent/DebuggableThreadPoolExecutor.java b/src/java/org/apache/cassandra/concurrent/DebuggableThreadPoolExecutor.java
index 1fb0690..92cbbf4 100644
--- a/src/java/org/apache/cassandra/concurrent/DebuggableThreadPoolExecutor.java
+++ b/src/java/org/apache/cassandra/concurrent/DebuggableThreadPoolExecutor.java
@@ -22,9 +22,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.tracing.TraceState;
-import org.apache.cassandra.tracing.Tracing;
-
 import static org.apache.cassandra.tracing.Tracing.isTracing;
 
 /**
diff --git a/src/java/org/apache/cassandra/concurrent/ExecutorLocal.java b/src/java/org/apache/cassandra/concurrent/ExecutorLocal.java
index 47826f3..6577b3d 100644
--- a/src/java/org/apache/cassandra/concurrent/ExecutorLocal.java
+++ b/src/java/org/apache/cassandra/concurrent/ExecutorLocal.java
@@ -19,7 +19,6 @@
 package org.apache.cassandra.concurrent;
 
 import org.apache.cassandra.service.ClientWarn;
-import org.apache.cassandra.tracing.TraceState;
 import org.apache.cassandra.tracing.Tracing;
 
 public interface ExecutorLocal<T>
@@ -27,7 +26,7 @@
     ExecutorLocal[] all = { Tracing.instance, ClientWarn.instance };
 
     /**
-     * This is called when scheduling the task, and also before calling {@link ExecutorLocal#set(T)} when running on a
+     * This is called when scheduling the task, and also before calling {@link #set(Object)} when running on a
      * executor thread.
      *
      * @return The thread-local value that we want to copy across executor boundaries; may be null if not set.
diff --git a/src/java/org/apache/cassandra/concurrent/NamedThreadFactory.java b/src/java/org/apache/cassandra/concurrent/NamedThreadFactory.java
index 27aa344..93d0c52 100644
--- a/src/java/org/apache/cassandra/concurrent/NamedThreadFactory.java
+++ b/src/java/org/apache/cassandra/concurrent/NamedThreadFactory.java
@@ -20,6 +20,8 @@
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import io.netty.util.concurrent.FastThreadLocal;
 import io.netty.util.concurrent.FastThreadLocalThread;
 
@@ -61,10 +63,8 @@
     public Thread newThread(Runnable runnable)
     {
         String name = id + ':' + n.getAndIncrement();
-        String prefix = globalPrefix;
-        Thread thread = new FastThreadLocalThread(threadGroup, threadLocalDeallocator(runnable), prefix != null ? prefix + name : name);
+        Thread thread = createThread(threadGroup, runnable, name, true);
         thread.setPriority(priority);
-        thread.setDaemon(true);
         if (contextClassLoader != null)
             thread.setContextClassLoader(contextClassLoader);
         return thread;
@@ -79,11 +79,45 @@
     {
         return () ->
         {
-            try {
+            try
+            {
                 r.run();
-            } finally {
+            }
+            finally
+            {
                 FastThreadLocal.removeAll();
             }
         };
     }
+
+    private static final AtomicInteger threadCounter = new AtomicInteger();
+
+    @VisibleForTesting
+    public static Thread createThread(Runnable runnable)
+    {
+        return createThread(null, runnable, "anonymous-" + threadCounter.incrementAndGet());
+    }
+
+    public static Thread createThread(Runnable runnable, String name)
+    {
+        return createThread(null, runnable, name);
+    }
+
+    public static Thread createThread(Runnable runnable, String name, boolean daemon)
+    {
+        return createThread(null, runnable, name, daemon);
+    }
+
+    public static Thread createThread(ThreadGroup threadGroup, Runnable runnable, String name)
+    {
+        return createThread(threadGroup, runnable, name, false);
+    }
+
+    public static Thread createThread(ThreadGroup threadGroup, Runnable runnable, String name, boolean daemon)
+    {
+        String prefix = globalPrefix;
+        Thread thread = new FastThreadLocalThread(threadGroup, threadLocalDeallocator(runnable), prefix != null ? prefix + name : name);
+        thread.setDaemon(daemon);
+        return thread;
+    }
 }
diff --git a/src/java/org/apache/cassandra/concurrent/SEPExecutor.java b/src/java/org/apache/cassandra/concurrent/SEPExecutor.java
index 190004d..4641a19 100644
--- a/src/java/org/apache/cassandra/concurrent/SEPExecutor.java
+++ b/src/java/org/apache/cassandra/concurrent/SEPExecutor.java
@@ -33,6 +33,7 @@
     private final SharedExecutorPool pool;
 
     public final int maxWorkers;
+    public final String name;
     private final SEPMetrics metrics;
 
     // stores both a set of work permits and task permits:
@@ -51,6 +52,7 @@
     SEPExecutor(SharedExecutorPool pool, int maxWorkers, String jmxPath, String name)
     {
         this.pool = pool;
+        this.name = name;
         this.maxWorkers = maxWorkers;
         this.permits.set(combine(0, maxWorkers));
         this.metrics = new SEPMetrics(this, jmxPath, name);
diff --git a/src/java/org/apache/cassandra/concurrent/SEPWorker.java b/src/java/org/apache/cassandra/concurrent/SEPWorker.java
index f7eb47a..cd7af63 100644
--- a/src/java/org/apache/cassandra/concurrent/SEPWorker.java
+++ b/src/java/org/apache/cassandra/concurrent/SEPWorker.java
@@ -24,11 +24,13 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import io.netty.util.concurrent.FastThreadLocalThread;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 
 final class SEPWorker extends AtomicReference<SEPWorker.Work> implements Runnable
 {
     private static final Logger logger = LoggerFactory.getLogger(SEPWorker.class);
+    private static final boolean SET_THREAD_NAME = Boolean.parseBoolean(System.getProperty("cassandra.set_sep_thread_name", "true"));
 
     final Long workerId;
     final Thread thread;
@@ -45,7 +47,7 @@
     {
         this.pool = pool;
         this.workerId = workerId;
-        thread = new Thread(this, pool.poolName + "-Worker-" + workerId);
+        thread = new FastThreadLocalThread(this, pool.poolName + "-Worker-" + workerId);
         thread.setDaemon(true);
         set(initialState);
         thread.start();
@@ -93,6 +95,8 @@
                 assigned = get().assigned;
                 if (assigned == null)
                     continue;
+                if (SET_THREAD_NAME)
+                    Thread.currentThread().setName(assigned.name + "-" + workerId);
                 task = assigned.tasks.poll();
 
                 // if we do have tasks assigned, nobody will change our state so we can simply set it to WORKING
diff --git a/src/java/org/apache/cassandra/concurrent/ScheduledExecutors.java b/src/java/org/apache/cassandra/concurrent/ScheduledExecutors.java
index 60501aa..ff9d1b4 100644
--- a/src/java/org/apache/cassandra/concurrent/ScheduledExecutors.java
+++ b/src/java/org/apache/cassandra/concurrent/ScheduledExecutors.java
@@ -30,6 +30,11 @@
 public class ScheduledExecutors
 {
     /**
+     * This pool is used for periodic fast (sub-microsecond) tasks.
+     */
+    public static final DebuggableScheduledThreadPoolExecutor scheduledFastTasks = new DebuggableScheduledThreadPoolExecutor("ScheduledFastTasks");
+
+    /**
      * This pool is used for periodic short (sub-second) tasks.
      */
      public static final DebuggableScheduledThreadPoolExecutor scheduledTasks = new DebuggableScheduledThreadPoolExecutor("ScheduledTasks");
@@ -47,6 +52,6 @@
     @VisibleForTesting
     public static void shutdownAndWait(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
     {
-        ExecutorUtils.shutdownNowAndWait(timeout, unit, scheduledTasks, nonPeriodicTasks, optionalTasks);
+        ExecutorUtils.shutdownNowAndWait(timeout, unit, scheduledFastTasks, scheduledTasks, nonPeriodicTasks, optionalTasks);
     }
 }
diff --git a/src/java/org/apache/cassandra/concurrent/SharedExecutorPool.java b/src/java/org/apache/cassandra/concurrent/SharedExecutorPool.java
index ff21771..b94090b 100644
--- a/src/java/org/apache/cassandra/concurrent/SharedExecutorPool.java
+++ b/src/java/org/apache/cassandra/concurrent/SharedExecutorPool.java
@@ -37,7 +37,7 @@
  * To keep producers from incurring unnecessary delays, once an executor is "spun up" (i.e. is processing tasks at a steady
  * rate), adding tasks to the executor often involves only placing the task on the work queue and updating the
  * task permits (which imposes our max queue length constraints). Only when it cannot be guaranteed the task will be serviced
- * promptly, and the maximum concurrency has not been reached, does the producer have to schedule a thread itself to perform 
+ * promptly, and the maximum concurrency has not been reached, does the producer have to schedule a thread itself to perform
  * the work ('promptly' in this context means we already have a worker spinning for work, as described next).
  *
  * Otherwise the worker threads schedule themselves: when they are assigned a task, they will attempt to spawn
@@ -47,11 +47,11 @@
  * random interval (based upon the number of threads in this mode, so that the total amount of non-sleeping time remains
  * approximately fixed regardless of the number of spinning threads), and upon waking will again try to assign itself to
  * an executor with outstanding tasks to perform. As a result of always scheduling a partner before committing to performing
- * any work, with a steady state of task arrival we should generally have either one spinning worker ready to promptly respond 
+ * any work, with a steady state of task arrival we should generally have either one spinning worker ready to promptly respond
  * to incoming work, or all possible workers actively committed to tasks.
- * 
+ *
  * In order to prevent this executor pool acting like a noisy neighbour to other processes on the system, workers also deschedule
- * themselves when it is detected that there are too many for the current rate of operation arrival. This is decided as a function 
+ * themselves when it is detected that there are too many for the current rate of operation arrival. This is decided as a function
  * of the total time spent spinning by all workers in an interval; as more workers spin, workers are descheduled more rapidly.
  */
 public class SharedExecutorPool
diff --git a/src/java/org/apache/cassandra/concurrent/StageManager.java b/src/java/org/apache/cassandra/concurrent/StageManager.java
index b609c6c..032a75d 100644
--- a/src/java/org/apache/cassandra/concurrent/StageManager.java
+++ b/src/java/org/apache/cassandra/concurrent/StageManager.java
@@ -61,7 +61,7 @@
         stages.put(Stage.TRACING, tracingExecutor());
     }
 
-    private static ExecuteOnlyExecutor tracingExecutor()
+    private static LocalAwareExecutorService tracingExecutor()
     {
         RejectedExecutionHandler reh = new RejectedExecutionHandler()
         {
@@ -70,13 +70,13 @@
                 MessagingService.instance().incrementDroppedMessages(MessagingService.Verb._TRACE);
             }
         };
-        return new ExecuteOnlyExecutor(1,
-                                       1,
-                                       KEEPALIVE,
-                                       TimeUnit.SECONDS,
-                                       new ArrayBlockingQueue<Runnable>(1000),
-                                       new NamedThreadFactory(Stage.TRACING.getJmxName()),
-                                       reh);
+        return new TracingExecutor(1,
+                                   1,
+                                   KEEPALIVE,
+                                   TimeUnit.SECONDS,
+                                   new ArrayBlockingQueue<Runnable>(1000),
+                                   new NamedThreadFactory(Stage.TRACING.getJmxName()),
+                                   reh);
     }
 
     private static JMXEnabledThreadPoolExecutor multiThreadedStage(Stage stage, int numThreads)
@@ -114,14 +114,6 @@
         }
     }
 
-    public final static Runnable NO_OP_TASK = new Runnable()
-    {
-        public void run()
-        {
-
-        }
-    };
-
     @VisibleForTesting
     public static void shutdownAndWait(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
     {
@@ -129,14 +121,11 @@
     }
 
     /**
-     * A TPE that disallows submit so that we don't need to worry about unwrapping exceptions on the
-     * tracing stage.  See CASSANDRA-1123 for background. We allow submitting NO_OP tasks, to allow
-     * a final wait on pending trace events since typically the tracing executor is single-threaded, see
-     * CASSANDRA-11465.
+     * The executor used for tracing.
      */
-    private static class ExecuteOnlyExecutor extends ThreadPoolExecutor implements LocalAwareExecutorService
+    private static class TracingExecutor extends ThreadPoolExecutor implements LocalAwareExecutorService
     {
-        public ExecuteOnlyExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
+        public TracingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
         {
             super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
         }
@@ -151,28 +140,5 @@
         {
             execute(command);
         }
-
-        @Override
-        public Future<?> submit(Runnable task)
-        {
-            if (task.equals(NO_OP_TASK))
-            {
-                assert getMaximumPoolSize() == 1 : "Cannot wait for pending tasks if running more than 1 thread";
-                return super.submit(task);
-            }
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public <T> Future<T> submit(Runnable task, T result)
-        {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public <T> Future<T> submit(Callable<T> task)
-        {
-            throw new UnsupportedOperationException();
-        }
     }
 }
diff --git a/src/java/org/apache/cassandra/config/CFMetaData.java b/src/java/org/apache/cassandra/config/CFMetaData.java
index 8dbd53a..1cffd38 100644
--- a/src/java/org/apache/cassandra/config/CFMetaData.java
+++ b/src/java/org/apache/cassandra/config/CFMetaData.java
@@ -24,6 +24,7 @@
 import java.util.*;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import javax.annotation.Nullable;
@@ -42,6 +43,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.auth.DataResource;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.SuperColumnCompatibility;
@@ -49,6 +51,7 @@
 import org.apache.cassandra.cql3.statements.CreateTableStatement;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.compaction.AbstractCompactionStrategy;
+import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.exceptions.ConfigurationException;
@@ -70,6 +73,8 @@
         SUPER, COUNTER, DENSE, COMPOUND
     }
 
+    private static final Pattern PATTERN_WORD_CHARS = Pattern.compile("\\w+");
+
     private static final Logger logger = LoggerFactory.getLogger(CFMetaData.class);
 
     public static final Serializer serializer = new Serializer();
@@ -123,6 +128,11 @@
 
     private volatile Set<ColumnDefinition> hiddenColumns;
 
+    public final DataResource resource;
+
+    //For hot path serialization it's often easier to store this info here
+    private volatile ColumnFilter allColumnFilter;
+
     /**
      * These two columns are "virtual" (e.g. not persisted together with schema).
      *
@@ -333,6 +343,9 @@
         // A compact table should always have a clustering
         assert isCQLTable() || !clusteringColumns.isEmpty() : String.format("For table %s.%s, isDense=%b, isCompound=%b, clustering=%s", ksName, cfName, isDense, isCompound, clusteringColumns);
 
+        // All tables should have a partition key
+        assert !partitionKeyColumns.isEmpty() : String.format("Have no partition keys for table %s.%s", ksName, cfName);
+
         this.partitionKeyColumns = partitionKeyColumns;
         this.clusteringColumns = clusteringColumns;
         this.partitionColumns = partitionColumns;
@@ -345,6 +358,7 @@
         rebuild();
 
         this.serializers = new Serializers(this);
+        this.resource = DataResource.table(ksName, cfName);
     }
 
     // This rebuild informations that are intrinsically duplicate of the table definition but
@@ -418,6 +432,8 @@
             hiddenColumns = Collections.emptySet();
         }
         this.hiddenColumns = hiddenColumns;
+
+        this.allColumnFilter = ColumnFilter.all(this);
     }
 
     public Indexes getIndexes()
@@ -425,6 +441,11 @@
         return indexes;
     }
 
+    public ColumnFilter getAllColumnFilter()
+    {
+        return allColumnFilter;
+    }
+
     public static CFMetaData create(String ksName,
                                     String name,
                                     UUID cfId,
@@ -475,9 +496,9 @@
                               null);
     }
 
-    private static List<AbstractType<?>> extractTypes(List<ColumnDefinition> clusteringColumns)
+    public static List<AbstractType<?>> extractTypes(Iterable<ColumnDefinition> clusteringColumns)
     {
-        List<AbstractType<?>> types = new ArrayList<>(clusteringColumns.size());
+        List<AbstractType<?>> types = new ArrayList<>();
         for (ColumnDefinition def : clusteringColumns)
             types.add(def.type);
         return types;
@@ -522,7 +543,7 @@
 
     /**
      * Generates deterministic UUID from keyspace/columnfamily name pair.
-     * This is used to generate the same UUID for C* version < 2.1
+     * This is used to generate the same UUID for {@code C* version < 2.1}
      *
      * Since 2.1, this is only used for system columnfamilies and tests.
      */
@@ -986,7 +1007,7 @@
         className = className.contains(".") ? className : "org.apache.cassandra.db.compaction." + className;
         Class<AbstractCompactionStrategy> strategyClass = FBUtilities.classForName(className, "compaction strategy");
         if (!AbstractCompactionStrategy.class.isAssignableFrom(strategyClass))
-            throw new ConfigurationException(String.format("Specified compaction strategy class (%s) is not derived from AbstractReplicationStrategy", className));
+            throw new ConfigurationException(String.format("Specified compaction strategy class (%s) is not derived from AbstractCompactionStrategy", className));
 
         return strategyClass;
     }
@@ -1037,7 +1058,8 @@
 
     public static boolean isNameValid(String name)
     {
-        return name != null && !name.isEmpty() && name.length() <= Schema.NAME_LENGTH && name.matches("\\w+");
+        return name != null && !name.isEmpty()
+               && name.length() <= SchemaConstants.NAME_LENGTH && PATTERN_WORD_CHARS.matcher(name).matches();
     }
 
     public CFMetaData validate() throws ConfigurationException
@@ -1045,9 +1067,9 @@
         rebuild();
 
         if (!isNameValid(ksName))
-            throw new ConfigurationException(String.format("Keyspace name must not be empty, more than %s characters long, or contain non-alphanumeric-underscore characters (got \"%s\")", Schema.NAME_LENGTH, ksName));
+            throw new ConfigurationException(String.format("Keyspace name must not be empty, more than %s characters long, or contain non-alphanumeric-underscore characters (got \"%s\")", SchemaConstants.NAME_LENGTH, ksName));
         if (!isNameValid(cfName))
-            throw new ConfigurationException(String.format("ColumnFamily name must not be empty, more than %s characters long, or contain non-alphanumeric-underscore characters (got \"%s\")", Schema.NAME_LENGTH, cfName));
+            throw new ConfigurationException(String.format("ColumnFamily name must not be empty, more than %s characters long, or contain non-alphanumeric-underscore characters (got \"%s\")", SchemaConstants.NAME_LENGTH, cfName));
 
         params.validate();
 
@@ -1158,15 +1180,15 @@
     /**
      * Adds the column definition as a dropped column, recording the drop with the provided timestamp.
      */
-    public void recordColumnDrop(ColumnDefinition def, long deleteTimestamp)
+    public void recordColumnDrop(ColumnDefinition def, long timeMicros)
     {
-        recordColumnDrop(def, deleteTimestamp, true);
+        recordColumnDrop(def, timeMicros, true);
     }
 
     @VisibleForTesting
-    public void recordColumnDrop(ColumnDefinition def, long deleteTimestamp, boolean preserveKind)
+    public void recordColumnDrop(ColumnDefinition def, long timeMicros, boolean preserveKind)
     {
-        droppedColumns.put(def.name.bytes, new DroppedColumn(def.name.toString(), preserveKind ? def.kind : null, def.type, deleteTimestamp));
+        droppedColumns.put(def.name.bytes, new DroppedColumn(def.name.toString(), preserveKind ? def.kind : null, def.type, timeMicros));
     }
 
     public void renameColumn(ColumnIdentifier from, ColumnIdentifier to) throws InvalidRequestException
@@ -1497,7 +1519,7 @@
 
         public Set<String> usedColumnNames()
         {
-            Set<String> usedNames = new HashSet<>();
+            Set<String> usedNames = Sets.newHashSetWithExpectedSize(partitionKeys.size() + clusteringColumns.size() + staticColumns.size() + regularColumns.size());
             for (Pair<ColumnIdentifier, AbstractType> p : partitionKeys)
                 usedNames.add(p.left.toString());
             for (Pair<ColumnIdentifier, AbstractType> p : clusteringColumns)
diff --git a/src/java/org/apache/cassandra/config/ColumnDefinition.java b/src/java/org/apache/cassandra/config/ColumnDefinition.java
index 6f7f749..0d6bdc8 100644
--- a/src/java/org/apache/cassandra/config/ColumnDefinition.java
+++ b/src/java/org/apache/cassandra/config/ColumnDefinition.java
@@ -22,17 +22,23 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 import com.google.common.collect.Collections2;
 
 import org.apache.cassandra.cql3.*;
+import org.apache.cassandra.cql3.selection.Selectable;
+import org.apache.cassandra.cql3.selection.Selector;
+import org.apache.cassandra.cql3.selection.SimpleSelector;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.utils.ByteBufferUtil;
 import org.github.jamm.Unmetered;
 
 @Unmetered
-public class ColumnDefinition extends ColumnSpecification implements Comparable<ColumnDefinition>
+public class ColumnDefinition extends ColumnSpecification implements Selectable, Comparable<ColumnDefinition>
 {
     public static final Comparator<Object> asymmetricColumnDataComparator =
         (a, b) -> ((ColumnData) a).column().compareTo((ColumnDefinition) b);
@@ -83,6 +89,8 @@
     private final Comparator<Object> asymmetricCellPathComparator;
     private final Comparator<? super Cell> cellComparator;
 
+    private int hash;
+
     /**
      * These objects are compared frequently, so we encode several of their comparison components
      * into a single long value so that this can be done efficiently
@@ -133,6 +141,11 @@
         return new ColumnDefinition(cfm, name, type, NO_POSITION, Kind.STATIC);
     }
 
+    public static ColumnDefinition staticDef(String ksName, String cfName, String name, AbstractType<?> type)
+    {
+        return new ColumnDefinition(ksName, cfName, ColumnIdentifier.getInterned(name, true), type, NO_POSITION, Kind.STATIC);
+    }
+
     public ColumnDefinition(CFMetaData cfm, ByteBuffer name, AbstractType<?> type, int position, Kind kind)
     {
         this(cfm.ksName,
@@ -165,10 +178,13 @@
 
     private static Comparator<CellPath> makeCellPathComparator(Kind kind, AbstractType<?> type)
     {
-        if (kind.isPrimaryKeyKind() || !type.isCollection() || !type.isMultiCell())
+        if (kind.isPrimaryKeyKind() || !type.isMultiCell())
             return null;
 
-        CollectionType collection = (CollectionType) type;
+        AbstractType<?> nameComparator = type.isCollection()
+                                       ? ((CollectionType) type).nameComparator()
+                                       : ((UserType) type).nameComparator();
+
 
         return new Comparator<CellPath>()
         {
@@ -185,7 +201,7 @@
 
                 // This will get more complicated once we have non-frozen UDT and nested collections
                 assert path1.size() == 1 && path2.size() == 1;
-                return collection.nameComparator().compare(path1.get(0), path2.get(0));
+                return nameComparator.compare(path1.get(0), path2.get(0));
             }
         };
     }
@@ -260,18 +276,36 @@
     @Override
     public int hashCode()
     {
-        return Objects.hashCode(ksName, cfName, name, type, kind, position);
+        // This achieves the same as Objects.hashcode, but avoids the object array allocation
+        // which features significantly in the allocation profile and caches the result.
+        int result = hash;
+        if(result == 0)
+        {
+            result = 31 + (ksName == null ? 0 : ksName.hashCode());
+            result = 31 * result + (cfName == null ? 0 : cfName.hashCode());
+            result = 31 * result + (name == null ? 0 : name.hashCode());
+            result = 31 * result + (type == null ? 0 : type.hashCode());
+            result = 31 * result + (kind == null ? 0 : kind.hashCode());
+            result = 31 * result + position;
+            hash = result;
+        }
+        return result;
     }
 
     @Override
     public String toString()
     {
-        return Objects.toStringHelper(this)
-                      .add("name", name)
-                      .add("type", type)
-                      .add("kind", kind)
-                      .add("position", position)
-                      .toString();
+        return name.toString();
+    }
+
+    public String debugString()
+    {
+        return MoreObjects.toStringHelper(this)
+                          .add("name", name)
+                          .add("type", type)
+                          .add("kind", kind)
+                          .add("position", position)
+                          .toString();
     }
 
     public boolean isPrimaryKeyColumn()
@@ -356,18 +390,39 @@
         return CollectionType.cellPathSerializer;
     }
 
-    public void validateCellValue(ByteBuffer value)
+    public void validateCell(Cell cell)
     {
-        type.validateCellValue(value);
+        if (cell.isTombstone())
+        {
+            if (cell.value().hasRemaining())
+                throw new MarshalException("A tombstone should not have a value");
+            if (cell.path() != null)
+                validateCellPath(cell.path());
+        }
+        else if(type.isUDT())
+        {
+            // To validate a non-frozen UDT field, both the path and the value
+            // are needed, the path being an index into an array of value types.
+            ((UserType)type).validateCell(cell);
+        }
+        else
+        {
+            type.validateCellValue(cell.value());
+            if (cell.path() != null)
+                validateCellPath(cell.path());
+        }
     }
 
-    public void validateCellPath(CellPath path)
+    private void validateCellPath(CellPath path)
     {
         if (!isComplex())
             throw new MarshalException("Only complex cells should have a cell path");
 
-        assert type instanceof CollectionType;
-        ((CollectionType)type).nameComparator().validate(path.get(0));
+        assert type.isMultiCell();
+        if (type.isCollection())
+            ((CollectionType)type).nameComparator().validate(path.get(0));
+        else
+            ((UserType)type).nameComparator().validate(path.get(0));
     }
 
     public static String toCQLString(Iterable<ColumnDefinition> defs)
@@ -410,4 +465,193 @@
             return ((CollectionType) type).valueComparator().isCounter();
         return type.isCounter();
     }
+
+    public Selector.Factory newSelectorFactory(CFMetaData cfm, AbstractType<?> expectedType, List<ColumnDefinition> defs, VariableSpecifications boundNames) throws InvalidRequestException
+    {
+        return SimpleSelector.newFactory(this, addAndGetIndex(this, defs));
+    }
+
+    public AbstractType<?> getExactTypeIfKnown(String keyspace)
+    {
+        return type;
+    }
+
+    /**
+     * Because Thrift-created tables may have a non-text comparator, we cannot determine the proper 'key' until
+     * we know the comparator. ColumnDefinition.Raw is a placeholder that can be converted to a real ColumnIdentifier
+     * once the comparator is known with prepare(). This should only be used with identifiers that are actual
+     * column names. See CASSANDRA-8178 for more background.
+     */
+    public static abstract class Raw extends Selectable.Raw
+    {
+        /**
+         * Creates a {@code ColumnDefinition.Raw} from an unquoted identifier string.
+         */
+        public static Raw forUnquoted(String text)
+        {
+            return new Literal(text, false);
+        }
+
+        /**
+         * Creates a {@code ColumnDefinition.Raw} from a quoted identifier string.
+         */
+        public static Raw forQuoted(String text)
+        {
+            return new Literal(text, true);
+        }
+
+        /**
+         * Creates a {@code ColumnDefinition.Raw} from a pre-existing {@code ColumnDefinition}
+         * (useful in the rare cases where we already have the column but need
+         * a {@code ColumnDefinition.Raw} for typing purposes).
+         */
+        public static Raw forColumn(ColumnDefinition column)
+        {
+            return new ForColumn(column);
+        }
+
+        /**
+         * Get the identifier corresponding to this raw column, without assuming this is an
+         * existing column (unlike {@link #prepare}).
+         */
+        public abstract ColumnIdentifier getIdentifier(CFMetaData cfm);
+
+        public abstract String rawText();
+
+        @Override
+        public abstract ColumnDefinition prepare(CFMetaData cfm);
+
+        @Override
+        public boolean processesSelection()
+        {
+            return false;
+        }
+
+        @Override
+        public final int hashCode()
+        {
+            return toString().hashCode();
+        }
+
+        @Override
+        public final boolean equals(Object o)
+        {
+            if(!(o instanceof Raw))
+                return false;
+
+            Raw that = (Raw)o;
+            return this.toString().equals(that.toString());
+        }
+
+        private static class Literal extends Raw
+        {
+            private final String text;
+
+            public Literal(String rawText, boolean keepCase)
+            {
+                this.text =  keepCase ? rawText : rawText.toLowerCase(Locale.US);
+            }
+
+            public ColumnIdentifier getIdentifier(CFMetaData cfm)
+            {
+                if (!cfm.isStaticCompactTable())
+                    return ColumnIdentifier.getInterned(text, true);
+
+                AbstractType<?> thriftColumnNameType = cfm.thriftColumnNameType();
+                if (thriftColumnNameType instanceof UTF8Type)
+                    return ColumnIdentifier.getInterned(text, true);
+
+                // We have a Thrift-created table with a non-text comparator. Check if we have a match column, otherwise assume we should use
+                // thriftColumnNameType
+                ByteBuffer bufferName = ByteBufferUtil.bytes(text);
+                for (ColumnDefinition def : cfm.allColumns())
+                {
+                    if (def.name.bytes.equals(bufferName))
+                        return def.name;
+                }
+                return ColumnIdentifier.getInterned(thriftColumnNameType, thriftColumnNameType.fromString(text), text);
+            }
+
+            public ColumnDefinition prepare(CFMetaData cfm)
+            {
+                if (!cfm.isStaticCompactTable())
+                    return find(cfm);
+
+                AbstractType<?> thriftColumnNameType = cfm.thriftColumnNameType();
+                if (thriftColumnNameType instanceof UTF8Type)
+                    return find(cfm);
+
+                // We have a Thrift-created table with a non-text comparator. Check if we have a match column, otherwise assume we should use
+                // thriftColumnNameType
+                ByteBuffer bufferName = ByteBufferUtil.bytes(text);
+                for (ColumnDefinition def : cfm.allColumns())
+                {
+                    if (def.name.bytes.equals(bufferName))
+                        return def;
+                }
+                return find(thriftColumnNameType.fromString(text), cfm);
+            }
+
+            private ColumnDefinition find(CFMetaData cfm)
+            {
+                return find(ByteBufferUtil.bytes(text), cfm);
+            }
+
+            private ColumnDefinition find(ByteBuffer id, CFMetaData cfm)
+            {
+                ColumnDefinition def = cfm.getColumnDefinitionForCQL(id);
+                if (def == null)
+                    throw new InvalidRequestException(String.format("Undefined column name %s", toString()));
+                return def;
+            }
+
+            public String rawText()
+            {
+                return text;
+            }
+
+            @Override
+            public String toString()
+            {
+                return ColumnIdentifier.maybeQuote(text);
+            }
+        }
+
+        // Use internally in the rare case where we need a ColumnDefinition.Raw for type-checking but
+        // actually already have the column itself.
+        private static class ForColumn extends Raw
+        {
+            private final ColumnDefinition column;
+
+            private ForColumn(ColumnDefinition column)
+            {
+                this.column = column;
+            }
+
+            public ColumnIdentifier getIdentifier(CFMetaData cfm)
+            {
+                return column.name;
+            }
+
+            public ColumnDefinition prepare(CFMetaData cfm)
+            {
+                assert cfm.getColumnDefinition(column.name) != null; // Sanity check that we're not doing something crazy
+                return column;
+            }
+
+            public String rawText()
+            {
+                return column.name.toString();
+            }
+
+            @Override
+            public String toString()
+            {
+                return column.name.toCQLString();
+            }
+        }
+    }
+
+
+
 }
diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java
index f3a2d2d..5b6a219 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -33,9 +33,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.config.EncryptionOptions.ClientEncryptionOptions;
-import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions;
-
 /**
  * A class that contains configuration properties for the cassandra node it runs within.
  *
@@ -50,25 +47,27 @@
      */
     public static final String PROPERTY_PREFIX = "cassandra.";
 
-
     public String cluster_name = "Test Cluster";
     public String authenticator;
     public String authorizer;
     public String role_manager;
     public volatile int permissions_validity_in_ms = 2000;
-    public int permissions_cache_max_entries = 1000;
+    public volatile int permissions_cache_max_entries = 1000;
     public volatile int permissions_update_interval_in_ms = -1;
     public volatile int roles_validity_in_ms = 2000;
-    public int roles_cache_max_entries = 1000;
+    public volatile int roles_cache_max_entries = 1000;
     public volatile int roles_update_interval_in_ms = -1;
+    public volatile int credentials_validity_in_ms = 2000;
+    public volatile int credentials_cache_max_entries = 1000;
+    public volatile int credentials_update_interval_in_ms = -1;
 
     /* Hashing strategy Random or OPHF */
     public String partitioner;
 
-    public Boolean auto_bootstrap = true;
+    public boolean auto_bootstrap = true;
     public volatile boolean hinted_handoff_enabled = true;
     public Set<String> hinted_handoff_disabled_datacenters = Sets.newConcurrentHashSet();
-    public volatile Integer max_hint_window_in_ms = 3 * 3600 * 1000; // three hours
+    public volatile int max_hint_window_in_ms = 3 * 3600 * 1000; // three hours
     public String hints_directory;
 
     public ParameterizedClass seed_provider;
@@ -83,112 +82,121 @@
     /** Triggers automatic allocation of tokens if set, using the replication strategy of the referenced keyspace */
     public String allocate_tokens_for_keyspace = null;
 
-    public volatile Long request_timeout_in_ms = 10000L;
+    public volatile long request_timeout_in_ms = 10000L;
 
-    public volatile Long read_request_timeout_in_ms = 5000L;
+    public volatile long read_request_timeout_in_ms = 5000L;
 
-    public volatile Long range_request_timeout_in_ms = 10000L;
+    public volatile long range_request_timeout_in_ms = 10000L;
 
-    public volatile Long write_request_timeout_in_ms = 2000L;
+    public volatile long write_request_timeout_in_ms = 2000L;
 
-    public volatile Long counter_write_request_timeout_in_ms = 5000L;
+    public volatile long counter_write_request_timeout_in_ms = 5000L;
 
-    public volatile Long cas_contention_timeout_in_ms = 1000L;
+    public volatile long cas_contention_timeout_in_ms = 1000L;
 
-    public volatile Long truncate_request_timeout_in_ms = 60000L;
+    public volatile long truncate_request_timeout_in_ms = 60000L;
 
-    public Integer streaming_socket_timeout_in_ms = 86400000; //24 hours
+    /**
+     * @deprecated use {@link this#streaming_keep_alive_period_in_secs} instead
+     */
+    @Deprecated
+    public int streaming_socket_timeout_in_ms = 86400000; //24 hours
+
+    public Integer streaming_keep_alive_period_in_secs = 300; //5 minutes
 
     public boolean cross_node_timeout = false;
 
-    public volatile Double phi_convict_threshold = 8.0;
+    public volatile long slow_query_log_timeout_in_ms = 500L;
 
-    public Integer concurrent_reads = 32;
-    public Integer concurrent_writes = 32;
-    public Integer concurrent_counter_writes = 32;
-    public Integer concurrent_materialized_view_writes = 32;
+    public volatile double phi_convict_threshold = 8.0;
+
+    public int concurrent_reads = 32;
+    public int concurrent_writes = 32;
+    public int concurrent_counter_writes = 32;
+    public int concurrent_materialized_view_writes = 32;
 
     @Deprecated
     public Integer concurrent_replicates = null;
 
-    public Integer memtable_flush_writers = null;
+    public int memtable_flush_writers = 0;
     public Integer memtable_heap_space_in_mb;
     public Integer memtable_offheap_space_in_mb;
     public Float memtable_cleanup_threshold = null;
 
     // Limit the maximum depth of repair session merkle trees
-    public volatile Integer repair_session_max_tree_depth = 18;
+    public volatile int repair_session_max_tree_depth = 18;
 
-    public Integer storage_port = 7000;
-    public Integer ssl_storage_port = 7001;
+    public int storage_port = 7000;
+    public int ssl_storage_port = 7001;
     public String listen_address;
     public String listen_interface;
-    public Boolean listen_interface_prefer_ipv6 = false;
+    public boolean listen_interface_prefer_ipv6 = false;
     public String broadcast_address;
-    public Boolean listen_on_broadcast_address = false;
+    public boolean listen_on_broadcast_address = false;
     public String internode_authenticator;
 
     /* intentionally left set to true, despite being set to false in stock 2.2 cassandra.yaml
        we don't want to surprise Thrift users who have the setting blank in the yaml during 2.1->2.2 upgrade */
-    public Boolean start_rpc = true;
+    public boolean start_rpc = true;
     public String rpc_address;
     public String rpc_interface;
-    public Boolean rpc_interface_prefer_ipv6 = false;
+    public boolean rpc_interface_prefer_ipv6 = false;
     public String broadcast_rpc_address;
-    public Integer rpc_port = 9160;
-    public Integer rpc_listen_backlog = 50;
+    public int rpc_port = 9160;
+    public int rpc_listen_backlog = 50;
     public String rpc_server_type = "sync";
-    public Boolean rpc_keepalive = true;
-    public Integer rpc_min_threads = 16;
-    public Integer rpc_max_threads = Integer.MAX_VALUE;
+    public boolean rpc_keepalive = true;
+    public int rpc_min_threads = 16;
+    public int rpc_max_threads = Integer.MAX_VALUE;
     public Integer rpc_send_buff_size_in_bytes;
     public Integer rpc_recv_buff_size_in_bytes;
-    public Integer internode_send_buff_size_in_bytes;
-    public Integer internode_recv_buff_size_in_bytes;
+    public int internode_send_buff_size_in_bytes = 0;
+    public int internode_recv_buff_size_in_bytes = 0;
 
-    public Boolean start_native_transport = false;
-    public Integer native_transport_port = 9042;
+    public boolean start_native_transport = false;
+    public int native_transport_port = 9042;
     public Integer native_transport_port_ssl = null;
-    public Integer native_transport_max_threads = 128;
-    public Integer native_transport_max_frame_size_in_mb = 256;
-    public volatile Long native_transport_max_concurrent_connections = -1L;
-    public volatile Long native_transport_max_concurrent_connections_per_ip = -1L;
+    public int native_transport_max_threads = 128;
+    public int native_transport_max_frame_size_in_mb = 256;
+    public volatile long native_transport_max_concurrent_connections = -1L;
+    public volatile long native_transport_max_concurrent_connections_per_ip = -1L;
     public boolean native_transport_flush_in_batches_legacy = true;
     public volatile long native_transport_max_concurrent_requests_in_bytes_per_ip = -1L;
     public volatile long native_transport_max_concurrent_requests_in_bytes = -1L;
     public Integer native_transport_max_negotiable_protocol_version = Integer.MIN_VALUE;
 
     @Deprecated
-    public Integer thrift_max_message_length_in_mb = 16;
+    public int thrift_max_message_length_in_mb = 16;
     /**
      * Max size of values in SSTables, in MegaBytes.
      * Default is the same as the native protocol frame limit: 256Mb.
      * See AbstractType for how it is used.
      */
-    public Integer max_value_size_in_mb = 256;
+    public int max_value_size_in_mb = 256;
 
-    public Integer thrift_framed_transport_size_in_mb = 15;
-    public Boolean snapshot_before_compaction = false;
-    public Boolean auto_snapshot = true;
+    public int thrift_framed_transport_size_in_mb = 15;
+    public boolean snapshot_before_compaction = false;
+    public boolean auto_snapshot = true;
 
     /* if the size of columns or super-columns are more than this, indexing will kick in */
-    public Integer column_index_size_in_kb = 64;
+    public int column_index_size_in_kb = 64;
+    public int column_index_cache_size_in_kb = 2;
     public volatile int batch_size_warn_threshold_in_kb = 5;
     public volatile int batch_size_fail_threshold_in_kb = 50;
     public Integer unlogged_batch_across_partitions_warn_threshold = 10;
-    public Integer concurrent_compactors;
-    public volatile Integer compaction_throughput_mb_per_sec = 16;
-    public volatile Integer compaction_large_partition_warning_threshold_mb = 100;
-    public Integer min_free_space_per_drive_in_mb = 50;
+    public volatile Integer concurrent_compactors;
+    public volatile int compaction_throughput_mb_per_sec = 16;
+    public volatile int compaction_large_partition_warning_threshold_mb = 100;
+    public int min_free_space_per_drive_in_mb = 50;
 
     /**
      * @deprecated retry support removed on CASSANDRA-10992
      */
     @Deprecated
-    public Integer max_streaming_retries = 3;
+    public int max_streaming_retries = 3;
 
-    public volatile Integer stream_throughput_outbound_megabits_per_sec = 200;
-    public volatile Integer inter_dc_stream_throughput_outbound_megabits_per_sec = 200;
+    public volatile int stream_throughput_outbound_megabits_per_sec = 200;
+    public volatile int inter_dc_stream_throughput_outbound_megabits_per_sec = 200;
 
     public String[] data_file_directories = new String[0];
 
@@ -198,31 +206,38 @@
     public String commitlog_directory;
     public Integer commitlog_total_space_in_mb;
     public CommitLogSync commitlog_sync;
-    public Double commitlog_sync_batch_window_in_ms;
-    public Integer commitlog_sync_period_in_ms;
+    public double commitlog_sync_batch_window_in_ms = Double.NaN;
+    public int commitlog_sync_period_in_ms;
     public int commitlog_segment_size_in_mb = 32;
     public ParameterizedClass commitlog_compression;
     public int commitlog_max_compression_buffers_in_pool = 3;
+    public TransparentDataEncryptionOptions transparent_data_encryption_options = new TransparentDataEncryptionOptions();
 
     public Integer max_mutation_size_in_kb;
 
+    // Change-data-capture logs
+    public boolean cdc_enabled = false;
+    public String cdc_raw_directory;
+    public int cdc_total_space_in_mb = 0;
+    public int cdc_free_space_check_interval_ms = 250;
+
     @Deprecated
     public int commitlog_periodic_queue_size = -1;
 
     public String endpoint_snitch;
-    public Boolean dynamic_snitch = true;
-    public Integer dynamic_snitch_update_interval_in_ms = 100;
-    public Integer dynamic_snitch_reset_interval_in_ms = 600000;
-    public Double dynamic_snitch_badness_threshold = 0.1;
+    public boolean dynamic_snitch = true;
+    public int dynamic_snitch_update_interval_in_ms = 100;
+    public int dynamic_snitch_reset_interval_in_ms = 600000;
+    public double dynamic_snitch_badness_threshold = 0.1;
 
     public String request_scheduler;
     public RequestSchedulerId request_scheduler_id;
     public RequestSchedulerOptions request_scheduler_options;
 
-    public ServerEncryptionOptions server_encryption_options = new ServerEncryptionOptions();
-    public ClientEncryptionOptions client_encryption_options = new ClientEncryptionOptions();
+    public EncryptionOptions.ServerEncryptionOptions server_encryption_options = new EncryptionOptions.ServerEncryptionOptions();
+    public EncryptionOptions.ClientEncryptionOptions client_encryption_options = new EncryptionOptions.ClientEncryptionOptions();
     // this encOptions is for backward compatibility (a warning is logged by DatabaseDescriptor)
-    public ServerEncryptionOptions encryption_options;
+    public EncryptionOptions.ServerEncryptionOptions encryption_options;
 
     public InternodeCompression internode_compression = InternodeCompression.none;
 
@@ -259,7 +274,18 @@
     private static boolean isClientMode = false;
     private static Supplier<Config> overrideLoadConfig = null;
 
-    public Integer file_cache_size_in_mb = 512;
+    public Integer file_cache_size_in_mb;
+
+    /**
+     * Because of the current {@link org.apache.cassandra.utils.memory.BufferPool} slab sizes of 64 kb, we
+     * store in the file cache buffers that divide 64 kb, so we need to round the buffer sizes to powers of two.
+     * This boolean controls weather they are rounded up or down. Set it to true to round up to the
+     * next power of two, set it to false to round down to the previous power of two. Note that buffer sizes are
+     * already rounded to 4 kb and capped between 4 kb minimum and 64 kb maximum by the {@link DiskOptimizationStrategy}.
+     * By default, this boolean is set to round down when {@link #disk_optimization_strategy} is {@code ssd},
+     * and to round up when it is {@code spinning}.
+     */
+    public Boolean file_cache_round_up;
 
     public boolean buffer_pool_use_heap_if_exhausted = true;
 
@@ -273,12 +299,6 @@
 
     public MemtableAllocationType memtable_allocation_type = MemtableAllocationType.heap_buffers;
 
-    /**
-     * @deprecated No longer needed for streaming protocol. See CASSANDRA-12673 for details.
-     */
-    @Deprecated
-    protected static boolean outboundBindAny = false;
-
     public volatile int tombstone_warn_threshold = 1000;
     public volatile int tombstone_failure_threshold = 100000;
 
@@ -299,7 +319,7 @@
      * Can be fixed, movingaverage, timehorizon, disabled. Setting is case and leading/trailing
      * whitespace insensitive. You can also specify a subclass of CoalescingStrategies.CoalescingStrategy by name.
      */
-    public String otc_coalescing_strategy = "TIMEHORIZON";
+    public String otc_coalescing_strategy = "DISABLED";
 
     /*
      * How many microseconds to wait for coalescing. For fixed strategy this is the amount of time after the first
@@ -316,14 +336,27 @@
      */
     public static final int otc_backlog_expiration_interval_ms_default = 200;
     public volatile int otc_backlog_expiration_interval_ms = otc_backlog_expiration_interval_ms_default;
- 
+
     public int windows_timer_interval = 0;
 
+    /**
+     * Size of the CQL prepared statements cache in MB.
+     * Defaults to 1/256th of the heap size or 10MB, whichever is greater.
+     */
+    public Long prepared_statements_cache_size_mb = null;
+    /**
+     * Size of the Thrift prepared statements cache in MB.
+     * Defaults to 1/256th of the heap size or 10MB, whichever is greater.
+     */
+    public Long thrift_prepared_statements_cache_size_mb = null;
+
     public boolean enable_user_defined_functions = false;
     public boolean enable_scripted_user_defined_functions = false;
 
     public boolean enable_materialized_views = true;
 
+    public boolean enable_sasi_indexes = true;
+
     public volatile boolean enable_drop_compact_storage = false;
 
     /**
@@ -370,14 +403,16 @@
      */
     public UserFunctionTimeoutPolicy user_function_timeout_policy = UserFunctionTimeoutPolicy.die;
 
-    public static boolean getOutboundBindAny()
-    {
-        return outboundBindAny;
-    }
+    public volatile boolean back_pressure_enabled = false;
+    public volatile ParameterizedClass back_pressure_strategy;
 
-    public static void setOutboundBindAny(boolean value)
+    /**
+     * @deprecated migrate to {@link DatabaseDescriptor#isClientInitialized()}
+     */
+    @Deprecated
+    public static boolean isClientMode()
     {
-        outboundBindAny = value;
+        return isClientMode;
     }
 
     /**
@@ -391,7 +426,6 @@
      * below to be enabled
      */
     public volatile boolean snapshot_on_duplicate_row_detection = false;
-
     /**
      * If these are enabled duplicate keys will get logged, and if snapshot_on_duplicate_row_detection
      * is enabled, the table will get snapshotted for offline investigation
@@ -400,12 +434,13 @@
     public volatile boolean check_for_duplicate_rows_during_compaction = true;
 
     public volatile boolean force_new_prepared_statement_behaviour = false;
-
-    public static boolean isClientMode()
-    {
-        return isClientMode;
-    }
-
+    /**
+     * Client mode means that the process is a pure client, that uses C* code base but does
+     * not read or write local C* database files.
+     *
+     * @deprecated migrate to {@link DatabaseDescriptor#clientInitialization(boolean)}
+     */
+    @Deprecated
     public static void setClientMode(boolean clientMode)
     {
         isClientMode = clientMode;
@@ -517,6 +552,6 @@
             configMap.put(name, value);
         }
 
-        logger.info("Node configuration:[" + Joiner.on("; ").join(configMap.entrySet()) + "]");
+        logger.info("Node configuration:[{}]", Joiner.on("; ").join(configMap.entrySet()));
     }
 }
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index ca946ce..d64d054 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -19,12 +19,14 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.lang.reflect.Constructor;
 import java.net.*;
 import java.nio.file.FileStore;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.*;
+import java.util.function.Supplier;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
@@ -34,30 +36,38 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.auth.*;
+import org.apache.cassandra.auth.AuthConfig;
+import org.apache.cassandra.auth.IAuthenticator;
+import org.apache.cassandra.auth.IAuthorizer;
+import org.apache.cassandra.auth.IInternodeAuthenticator;
+import org.apache.cassandra.auth.IRoleManager;
 import org.apache.cassandra.config.Config.CommitLogSync;
 import org.apache.cassandra.config.Config.RequestSchedulerId;
-import org.apache.cassandra.config.EncryptionOptions.ClientEncryptionOptions;
-import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions;
-import org.apache.cassandra.db.ColumnFamilyStore;
-import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.FSWriteError;
-import org.apache.cassandra.io.sstable.format.SSTableFormat;
+import org.apache.cassandra.io.util.DiskOptimizationStrategy;
 import org.apache.cassandra.io.util.FileUtils;
-import org.apache.cassandra.locator.*;
-import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.io.util.SpinningDiskOptimizationStrategy;
+import org.apache.cassandra.io.util.SsdDiskOptimizationStrategy;
+import org.apache.cassandra.locator.DynamicEndpointSnitch;
+import org.apache.cassandra.locator.EndpointSnitchInfo;
+import org.apache.cassandra.locator.IEndpointSnitch;
+import org.apache.cassandra.locator.SeedProvider;
+import org.apache.cassandra.net.BackPressureStrategy;
+import org.apache.cassandra.net.RateBasedBackPressure;
 import org.apache.cassandra.scheduler.IRequestScheduler;
 import org.apache.cassandra.scheduler.NoScheduler;
-import org.apache.cassandra.service.CacheService;
-import org.apache.cassandra.thrift.ThriftServer;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.service.CacheService.CacheType;
+import org.apache.cassandra.thrift.ThriftServer.ThriftServerType;
+import org.apache.cassandra.transport.ProtocolVersion;
+import org.apache.cassandra.transport.ProtocolVersionLimit;
 import org.apache.cassandra.utils.FBUtilities;
-import org.apache.cassandra.utils.memory.*;
+
+import org.apache.commons.lang3.StringUtils;
 
 import static org.apache.cassandra.io.util.FileUtils.ONE_GB;
-import static org.apache.cassandra.io.util.FileUtils.ONE_MB;
 
 public class DatabaseDescriptor
 {
@@ -69,6 +79,8 @@
      */
     private static final int MAX_NUM_TOKENS = 1536;
 
+    private static Config conf;
+
     private static IEndpointSnitch snitch;
     private static InetAddress listenAddress; // leave null so we can fall through to getLocalHost
     private static InetAddress broadcastAddress;
@@ -83,12 +95,8 @@
 
     private static Config.DiskAccessMode indexAccessMode;
 
-    private static Config conf;
-
-    private static SSTableFormat.Type sstable_format = SSTableFormat.Type.BIG;
-
-    private static IAuthenticator authenticator = new AllowAllAuthenticator();
-    private static IAuthorizer authorizer = new AllowAllAuthorizer();
+    private static IAuthenticator authenticator;
+    private static IAuthorizer authorizer;
     // Don't initialize the role manager until applying config. The options supported by CassandraRoleManager
     // depend on the configured IAuthenticator, so defer creating it until that's been set.
     private static IRoleManager roleManager;
@@ -97,61 +105,174 @@
     private static RequestSchedulerId requestSchedulerId;
     private static RequestSchedulerOptions requestSchedulerOptions;
 
+    private static long preparedStatementsCacheSizeInMB;
+    private static long thriftPreparedStatementsCacheSizeInMB;
+
     private static long keyCacheSizeInMB;
     private static long counterCacheSizeInMB;
     private static long indexSummaryCapacityInMB;
 
     private static String localDC;
     private static Comparator<InetAddress> localComparator;
+    private static EncryptionContext encryptionContext;
     private static boolean hasLoggedConfig;
 
+    private static BackPressureStrategy backPressureStrategy;
+    private static DiskOptimizationStrategy diskOptimizationStrategy;
+
+    private static boolean clientInitialized;
+    private static boolean toolInitialized;
     private static boolean daemonInitialized;
 
+    private static final int searchConcurrencyFactor = Integer.parseInt(System.getProperty(Config.PROPERTY_PREFIX + "search_concurrency_factor", "1"));
+
+    private static final boolean disableSTCSInL0 = Boolean.getBoolean(Config.PROPERTY_PREFIX + "disable_stcs_in_l0");
+    private static final boolean unsafeSystem = Boolean.getBoolean(Config.PROPERTY_PREFIX + "unsafesystem");
+
     // turns some warnings into exceptions for testing
     private static final boolean strictRuntimeChecks = Boolean.getBoolean("cassandra.strict.runtime.checks");
 
+    public static void daemonInitialization() throws ConfigurationException
+    {
+        daemonInitialization(DatabaseDescriptor::loadConfig);
+    }
+
+    public static void daemonInitialization(Supplier<Config> config) throws ConfigurationException
+    {
+        if (toolInitialized)
+            throw new AssertionError("toolInitialization() already called");
+        if (clientInitialized)
+            throw new AssertionError("clientInitialization() already called");
+
+        // Some unit tests require this :(
+        if (daemonInitialized)
+            return;
+        daemonInitialized = true;
+
+        setConfig(config.get());
+        applyAll();
+        AuthConfig.applyAuth();
+    }
+
+    /**
+     * Equivalent to {@link #toolInitialization(boolean) toolInitialization(true)}.
+     */
+    public static void toolInitialization()
+    {
+        toolInitialization(true);
+    }
+
+    /**
+     * Initializes this class as a tool, which means that the configuration is loaded
+     * using {@link #loadConfig()} and all non-daemon configuration parts will be setup.
+     *
+     * @param failIfDaemonOrClient if {@code true} and a call to {@link #daemonInitialization()} or
+     *                             {@link #clientInitialization()} has been performed before, an
+     *                             {@link AssertionError} will be thrown.
+     */
+    public static void toolInitialization(boolean failIfDaemonOrClient)
+    {
+        if (!failIfDaemonOrClient && (daemonInitialized || clientInitialized))
+        {
+            return;
+        }
+        else
+        {
+            if (daemonInitialized)
+                throw new AssertionError("daemonInitialization() already called");
+            if (clientInitialized)
+                throw new AssertionError("clientInitialization() already called");
+        }
+
+        if (toolInitialized)
+            return;
+        toolInitialized = true;
+
+        setConfig(loadConfig());
+
+        applySimpleConfig();
+
+        applyPartitioner();
+
+        applySnitch();
+
+        applyEncryptionContext();
+    }
+
+    /**
+     * Equivalent to {@link #clientInitialization(boolean) clientInitialization(true)}.
+     */
+    public static void clientInitialization()
+    {
+        clientInitialization(true);
+    }
+
+    /**
+     * Initializes this class as a client, which means that just an empty configuration will
+     * be used.
+     *
+     * @param failIfDaemonOrTool if {@code true} and a call to {@link #daemonInitialization()} or
+     *                           {@link #toolInitialization()} has been performed before, an
+     *                           {@link AssertionError} will be thrown.
+     */
+    public static void clientInitialization(boolean failIfDaemonOrTool)
+    {
+        if (!failIfDaemonOrTool && (daemonInitialized || toolInitialized))
+        {
+            return;
+        }
+        else
+        {
+            if (daemonInitialized)
+                throw new AssertionError("daemonInitialization() already called");
+            if (toolInitialized)
+                throw new AssertionError("toolInitialization() already called");
+        }
+
+        if (clientInitialized)
+            return;
+        clientInitialized = true;
+
+        Config.setClientMode(true);
+        conf = new Config();
+        diskOptimizationStrategy = new SpinningDiskOptimizationStrategy();
+    }
+
+    public static boolean isClientInitialized()
+    {
+        return clientInitialized;
+    }
+
+    public static boolean isToolInitialized()
+    {
+        return toolInitialized;
+    }
+
+    public static boolean isClientOrToolInitialized()
+    {
+        return clientInitialized || toolInitialized;
+    }
+
     public static boolean isDaemonInitialized()
     {
         return daemonInitialized;
     }
 
-    public static void setDaemonInitialized()
+    public static Config getRawConfig()
     {
-        daemonInitialized = true;
+        return conf;
     }
 
-    public static void forceStaticInitialization() {}
-    static
-    {
-        // In client mode, we use a default configuration. Note that the fields of this class will be
-        // left unconfigured however (the partitioner or localDC will be null for instance) so this
-        // should be used with care.
-        try
-        {
-            if (Config.isClientMode())
-            {
-                conf = new Config();
-            }
-            else
-            {
-                applyConfig(loadConfig());
-            }
-        }
-        catch (Exception e)
-        {
-            throw new ExceptionInInitializerError(e);
-        }
-    }
-
+    @VisibleForTesting
     public static Config loadConfig() throws ConfigurationException
     {
         if (Config.getOverrideLoadConfig() != null)
             return Config.getOverrideLoadConfig().get();
 
-        String loaderClass = System.getProperty("cassandra.config.loader");
+        String loaderClass = System.getProperty(Config.PROPERTY_PREFIX + "config.loader");
         ConfigurationLoader loader = loaderClass == null
                                      ? new YamlConfigurationLoader()
-                                   : FBUtilities.construct(loaderClass, "configuration loading");
+                                     : FBUtilities.construct(loaderClass, "configuration loading");
         Config config = loader.loadConfig();
 
         if (!hasLoggedConfig)
@@ -193,7 +314,507 @@
         }
     }
 
-    @VisibleForTesting
+    private static void setConfig(Config config)
+    {
+        conf = config;
+    }
+
+    private static void applyAll() throws ConfigurationException
+    {
+        applySimpleConfig();
+
+        applyPartitioner();
+
+        applyAddressConfig();
+
+        applyThriftHSHA();
+
+        applySnitch();
+
+        applyRequestScheduler();
+
+        applyTokensConfig();
+
+        applySeedProvider();
+
+        applyEncryptionContext();
+    }
+
+    private static void applySimpleConfig()
+    {
+
+        if (conf.commitlog_sync == null)
+        {
+            throw new ConfigurationException("Missing required directive CommitLogSync", false);
+        }
+
+        if (conf.commitlog_sync == Config.CommitLogSync.batch)
+        {
+            if (Double.isNaN(conf.commitlog_sync_batch_window_in_ms) || conf.commitlog_sync_batch_window_in_ms <= 0d)
+            {
+                throw new ConfigurationException("Missing value for commitlog_sync_batch_window_in_ms: positive double value expected.", false);
+            }
+            else if (conf.commitlog_sync_period_in_ms != 0)
+            {
+                throw new ConfigurationException("Batch sync specified, but commitlog_sync_period_in_ms found. Only specify commitlog_sync_batch_window_in_ms when using batch sync", false);
+            }
+            logger.debug("Syncing log with a batch window of {}", conf.commitlog_sync_batch_window_in_ms);
+        }
+        else
+        {
+            if (conf.commitlog_sync_period_in_ms <= 0)
+            {
+                throw new ConfigurationException("Missing value for commitlog_sync_period_in_ms: positive integer expected", false);
+            }
+            else if (!Double.isNaN(conf.commitlog_sync_batch_window_in_ms))
+            {
+                throw new ConfigurationException("commitlog_sync_period_in_ms specified, but commitlog_sync_batch_window_in_ms found.  Only specify commitlog_sync_period_in_ms when using periodic sync.", false);
+            }
+            logger.debug("Syncing log with a period of {}", conf.commitlog_sync_period_in_ms);
+        }
+
+        /* evaluate the DiskAccessMode Config directive, which also affects indexAccessMode selection */
+        if (conf.disk_access_mode == Config.DiskAccessMode.auto)
+        {
+            conf.disk_access_mode = hasLargeAddressSpace() ? Config.DiskAccessMode.mmap : Config.DiskAccessMode.standard;
+            indexAccessMode = conf.disk_access_mode;
+            logger.info("DiskAccessMode 'auto' determined to be {}, indexAccessMode is {}", conf.disk_access_mode, indexAccessMode);
+        }
+        else if (conf.disk_access_mode == Config.DiskAccessMode.mmap_index_only)
+        {
+            conf.disk_access_mode = Config.DiskAccessMode.standard;
+            indexAccessMode = Config.DiskAccessMode.mmap;
+            logger.info("DiskAccessMode is {}, indexAccessMode is {}", conf.disk_access_mode, indexAccessMode);
+        }
+        else
+        {
+            indexAccessMode = conf.disk_access_mode;
+            logger.info("DiskAccessMode is {}, indexAccessMode is {}", conf.disk_access_mode, indexAccessMode);
+        }
+
+        if (conf.gc_warn_threshold_in_ms < 0)
+        {
+            throw new ConfigurationException("gc_warn_threshold_in_ms must be a positive integer");
+        }
+
+        /* phi convict threshold for FailureDetector */
+        if (conf.phi_convict_threshold < 5 || conf.phi_convict_threshold > 16)
+        {
+            throw new ConfigurationException("phi_convict_threshold must be between 5 and 16, but was " + conf.phi_convict_threshold, false);
+        }
+
+        /* Thread per pool */
+        if (conf.concurrent_reads < 2)
+        {
+            throw new ConfigurationException("concurrent_reads must be at least 2, but was " + conf.concurrent_reads, false);
+        }
+
+        if (conf.concurrent_writes < 2 && System.getProperty("cassandra.test.fail_mv_locks_count", "").isEmpty())
+        {
+            throw new ConfigurationException("concurrent_writes must be at least 2, but was " + conf.concurrent_writes, false);
+        }
+
+        if (conf.concurrent_counter_writes < 2)
+            throw new ConfigurationException("concurrent_counter_writes must be at least 2, but was " + conf.concurrent_counter_writes, false);
+
+        if (conf.concurrent_replicates != null)
+            logger.warn("concurrent_replicates has been deprecated and should be removed from cassandra.yaml");
+
+        if (conf.file_cache_size_in_mb == null)
+            conf.file_cache_size_in_mb = Math.min(512, (int) (Runtime.getRuntime().maxMemory() / (4 * 1048576)));
+
+        // round down for SSDs and round up for spinning disks
+        if (conf.file_cache_round_up == null)
+            conf.file_cache_round_up = conf.disk_optimization_strategy == Config.DiskOptimizationStrategy.spinning;
+
+        if (conf.memtable_offheap_space_in_mb == null)
+            conf.memtable_offheap_space_in_mb = (int) (Runtime.getRuntime().maxMemory() / (4 * 1048576));
+        if (conf.memtable_offheap_space_in_mb < 0)
+            throw new ConfigurationException("memtable_offheap_space_in_mb must be positive, but was " + conf.memtable_offheap_space_in_mb, false);
+        // for the moment, we default to twice as much on-heap space as off-heap, as heap overhead is very large
+        if (conf.memtable_heap_space_in_mb == null)
+            conf.memtable_heap_space_in_mb = (int) (Runtime.getRuntime().maxMemory() / (4 * 1048576));
+        if (conf.memtable_heap_space_in_mb <= 0)
+            throw new ConfigurationException("memtable_heap_space_in_mb must be positive, but was " + conf.memtable_heap_space_in_mb, false);
+        logger.info("Global memtable on-heap threshold is enabled at {}MB", conf.memtable_heap_space_in_mb);
+        if (conf.memtable_offheap_space_in_mb == 0)
+            logger.info("Global memtable off-heap threshold is disabled, HeapAllocator will be used instead");
+        else
+            logger.info("Global memtable off-heap threshold is enabled at {}MB", conf.memtable_offheap_space_in_mb);
+
+        if (conf.repair_session_max_tree_depth < 10)
+            throw new ConfigurationException("repair_session_max_tree_depth should not be < 10, but was " + conf.repair_session_max_tree_depth);
+        if (conf.repair_session_max_tree_depth > 20)
+            logger.warn("repair_session_max_tree_depth of " + conf.repair_session_max_tree_depth + " > 20 could lead to excessive memory usage");
+
+        if (conf.thrift_framed_transport_size_in_mb <= 0)
+            throw new ConfigurationException("thrift_framed_transport_size_in_mb must be positive, but was " + conf.thrift_framed_transport_size_in_mb, false);
+
+        if (conf.native_transport_max_frame_size_in_mb <= 0)
+            throw new ConfigurationException("native_transport_max_frame_size_in_mb must be positive, but was " + conf.native_transport_max_frame_size_in_mb, false);
+        else if (conf.native_transport_max_frame_size_in_mb >= 2048)
+            throw new ConfigurationException("native_transport_max_frame_size_in_mb must be smaller than 2048, but was "
+                    + conf.native_transport_max_frame_size_in_mb, false);
+
+        // if data dirs, commitlog dir, or saved caches dir are set in cassandra.yaml, use that.  Otherwise,
+        // use -Dcassandra.storagedir (set in cassandra-env.sh) as the parent dir for data/, commitlog/, and saved_caches/
+        if (conf.commitlog_directory == null)
+        {
+            conf.commitlog_directory = storagedirFor("commitlog");
+        }
+
+        if (conf.hints_directory == null)
+        {
+            conf.hints_directory = storagedirFor("hints");
+        }
+
+        if (conf.native_transport_max_concurrent_requests_in_bytes <= 0)
+        {
+            conf.native_transport_max_concurrent_requests_in_bytes = Runtime.getRuntime().maxMemory() / 10;
+        }
+
+        if (conf.native_transport_max_concurrent_requests_in_bytes_per_ip <= 0)
+        {
+            conf.native_transport_max_concurrent_requests_in_bytes_per_ip = Runtime.getRuntime().maxMemory() / 40;
+        }
+
+        if (conf.cdc_raw_directory == null)
+        {
+            conf.cdc_raw_directory = storagedirFor("cdc_raw");
+        }
+
+        if (conf.commitlog_total_space_in_mb == null)
+        {
+            int preferredSize = 8192;
+            int minSize = 0;
+            try
+            {
+                // use 1/4 of available space.  See discussion on #10013 and #10199
+                minSize = Ints.saturatedCast((guessFileStore(conf.commitlog_directory).getTotalSpace() / 1048576) / 4);
+            }
+            catch (IOException e)
+            {
+                logger.debug("Error checking disk space", e);
+                throw new ConfigurationException(String.format("Unable to check disk space available to %s. Perhaps the Cassandra user does not have the necessary permissions",
+                                                               conf.commitlog_directory), e);
+            }
+            if (minSize < preferredSize)
+            {
+                logger.warn("Small commitlog volume detected at {}; setting commitlog_total_space_in_mb to {}.  You can override this in cassandra.yaml",
+                            conf.commitlog_directory, minSize);
+                conf.commitlog_total_space_in_mb = minSize;
+            }
+            else
+            {
+                conf.commitlog_total_space_in_mb = preferredSize;
+            }
+        }
+
+        if (conf.cdc_total_space_in_mb == 0)
+        {
+            int preferredSize = 4096;
+            int minSize;
+            try
+            {
+                // use 1/8th of available space.  See discussion on #10013 and #10199 on the CL, taking half that for CDC
+                minSize = Ints.saturatedCast((guessFileStore(conf.cdc_raw_directory).getTotalSpace() / 1048576) / 8);
+            }
+            catch (IOException e)
+            {
+                logger.debug("Error checking disk space", e);
+                throw new ConfigurationException(String.format("Unable to check disk space available to %s. Perhaps the Cassandra user does not have the necessary permissions",
+                                                               conf.cdc_raw_directory), e);
+            }
+            if (minSize < preferredSize)
+            {
+                logger.warn("Small cdc volume detected at {}; setting cdc_total_space_in_mb to {}.  You can override this in cassandra.yaml",
+                            conf.cdc_raw_directory, minSize);
+                conf.cdc_total_space_in_mb = minSize;
+            }
+            else
+            {
+                conf.cdc_total_space_in_mb = preferredSize;
+            }
+        }
+
+        if (conf.cdc_enabled)
+        {
+            logger.info("cdc_enabled is true. Starting casssandra node with Change-Data-Capture enabled.");
+        }
+
+        if (conf.saved_caches_directory == null)
+        {
+            conf.saved_caches_directory = storagedirFor("saved_caches");
+        }
+        if (conf.data_file_directories == null || conf.data_file_directories.length == 0)
+        {
+            conf.data_file_directories = new String[]{ storagedir("data_file_directories") + File.separator + "data" };
+        }
+
+        long dataFreeBytes = 0;
+        /* data file and commit log directories. they get created later, when they're needed. */
+        for (String datadir : conf.data_file_directories)
+        {
+            if (datadir == null)
+                throw new ConfigurationException("data_file_directories must not contain empty entry", false);
+            if (datadir.equals(conf.commitlog_directory))
+                throw new ConfigurationException("commitlog_directory must not be the same as any data_file_directories", false);
+            if (datadir.equals(conf.hints_directory))
+                throw new ConfigurationException("hints_directory must not be the same as any data_file_directories", false);
+            if (datadir.equals(conf.saved_caches_directory))
+                throw new ConfigurationException("saved_caches_directory must not be the same as any data_file_directories", false);
+
+            try
+            {
+                dataFreeBytes = saturatedSum(dataFreeBytes, guessFileStore(datadir).getUnallocatedSpace());
+            }
+            catch (IOException e)
+            {
+                logger.debug("Error checking disk space", e);
+                throw new ConfigurationException(String.format("Unable to check disk space available to %s. Perhaps the Cassandra user does not have the necessary permissions",
+                                                               datadir), e);
+            }
+        }
+        if (dataFreeBytes < 64 * ONE_GB) // 64 GB
+            logger.warn("Only {} free across all data volumes. Consider adding more capacity to your cluster or removing obsolete snapshots",
+                        FBUtilities.prettyPrintMemory(dataFreeBytes));
+
+        if (conf.commitlog_directory.equals(conf.saved_caches_directory))
+            throw new ConfigurationException("saved_caches_directory must not be the same as the commitlog_directory", false);
+        if (conf.commitlog_directory.equals(conf.hints_directory))
+            throw new ConfigurationException("hints_directory must not be the same as the commitlog_directory", false);
+        if (conf.hints_directory.equals(conf.saved_caches_directory))
+            throw new ConfigurationException("saved_caches_directory must not be the same as the hints_directory", false);
+
+        if (conf.memtable_flush_writers == 0)
+        {
+            conf.memtable_flush_writers = conf.data_file_directories.length == 1 ? 2 : 1;
+        }
+
+        if (conf.memtable_flush_writers < 1)
+            throw new ConfigurationException("memtable_flush_writers must be at least 1, but was " + conf.memtable_flush_writers, false);
+
+        if (conf.memtable_cleanup_threshold == null)
+        {
+            conf.memtable_cleanup_threshold = (float) (1.0 / (1 + conf.memtable_flush_writers));
+        }
+        else
+        {
+            logger.warn("memtable_cleanup_threshold has been deprecated and should be removed from cassandra.yaml");
+        }
+
+        if (conf.memtable_cleanup_threshold < 0.01f)
+            throw new ConfigurationException("memtable_cleanup_threshold must be >= 0.01, but was " + conf.memtable_cleanup_threshold, false);
+        if (conf.memtable_cleanup_threshold > 0.99f)
+            throw new ConfigurationException("memtable_cleanup_threshold must be <= 0.99, but was " + conf.memtable_cleanup_threshold, false);
+        if (conf.memtable_cleanup_threshold < 0.1f)
+            logger.warn("memtable_cleanup_threshold is set very low [{}], which may cause performance degradation", conf.memtable_cleanup_threshold);
+
+        if (conf.concurrent_compactors == null)
+            conf.concurrent_compactors = Math.min(8, Math.max(2, Math.min(FBUtilities.getAvailableProcessors(), conf.data_file_directories.length)));
+
+        if (conf.concurrent_compactors <= 0)
+            throw new ConfigurationException("concurrent_compactors should be strictly greater than 0, but was " + conf.concurrent_compactors, false);
+
+        if (conf.num_tokens != null && conf.num_tokens > MAX_NUM_TOKENS)
+            throw new ConfigurationException(String.format("A maximum number of %d tokens per node is supported", MAX_NUM_TOKENS), false);
+
+        try
+        {
+            // if prepared_statements_cache_size_mb option was set to "auto" then size of the cache should be "max(1/256 of Heap (in MB), 10MB)"
+            preparedStatementsCacheSizeInMB = (conf.prepared_statements_cache_size_mb == null)
+                                              ? Math.max(10, (int) (Runtime.getRuntime().maxMemory() / 1024 / 1024 / 256))
+                                              : conf.prepared_statements_cache_size_mb;
+
+            if (preparedStatementsCacheSizeInMB <= 0)
+                throw new NumberFormatException(); // to escape duplicating error message
+        }
+        catch (NumberFormatException e)
+        {
+            throw new ConfigurationException("prepared_statements_cache_size_mb option was set incorrectly to '"
+                                             + conf.prepared_statements_cache_size_mb + "', supported values are <integer> >= 0.", false);
+        }
+
+        try
+        {
+            // if thrift_prepared_statements_cache_size_mb option was set to "auto" then size of the cache should be "max(1/256 of Heap (in MB), 10MB)"
+            thriftPreparedStatementsCacheSizeInMB = (conf.thrift_prepared_statements_cache_size_mb == null)
+                                                    ? Math.max(10, (int) (Runtime.getRuntime().maxMemory() / 1024 / 1024 / 256))
+                                                    : conf.thrift_prepared_statements_cache_size_mb;
+
+            if (thriftPreparedStatementsCacheSizeInMB <= 0)
+                throw new NumberFormatException(); // to escape duplicating error message
+        }
+        catch (NumberFormatException e)
+        {
+            throw new ConfigurationException("thrift_prepared_statements_cache_size_mb option was set incorrectly to '"
+                                             + conf.thrift_prepared_statements_cache_size_mb + "', supported values are <integer> >= 0.", false);
+        }
+
+        try
+        {
+            // if key_cache_size_in_mb option was set to "auto" then size of the cache should be "min(5% of Heap (in MB), 100MB)
+            keyCacheSizeInMB = (conf.key_cache_size_in_mb == null)
+                               ? Math.min(Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.05 / 1024 / 1024)), 100)
+                               : conf.key_cache_size_in_mb;
+
+            if (keyCacheSizeInMB < 0)
+                throw new NumberFormatException(); // to escape duplicating error message
+        }
+        catch (NumberFormatException e)
+        {
+            throw new ConfigurationException("key_cache_size_in_mb option was set incorrectly to '"
+                                             + conf.key_cache_size_in_mb + "', supported values are <integer> >= 0.", false);
+        }
+
+        try
+        {
+            // if counter_cache_size_in_mb option was set to "auto" then size of the cache should be "min(2.5% of Heap (in MB), 50MB)
+            counterCacheSizeInMB = (conf.counter_cache_size_in_mb == null)
+                                   ? Math.min(Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.025 / 1024 / 1024)), 50)
+                                   : conf.counter_cache_size_in_mb;
+
+            if (counterCacheSizeInMB < 0)
+                throw new NumberFormatException(); // to escape duplicating error message
+        }
+        catch (NumberFormatException e)
+        {
+            throw new ConfigurationException("counter_cache_size_in_mb option was set incorrectly to '"
+                                             + conf.counter_cache_size_in_mb + "', supported values are <integer> >= 0.", false);
+        }
+
+        // if set to empty/"auto" then use 5% of Heap size
+        indexSummaryCapacityInMB = (conf.index_summary_capacity_in_mb == null)
+                                   ? Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.05 / 1024 / 1024))
+                                   : conf.index_summary_capacity_in_mb;
+
+        if (indexSummaryCapacityInMB < 0)
+            throw new ConfigurationException("index_summary_capacity_in_mb option was set incorrectly to '"
+                                             + conf.index_summary_capacity_in_mb + "', it should be a non-negative integer.", false);
+
+        if (conf.index_interval != null)
+            logger.warn("index_interval has been deprecated and should be removed from cassandra.yaml");
+
+        if (conf.encryption_options != null)
+        {
+            logger.warn("Please rename encryption_options as server_encryption_options in the yaml");
+            //operate under the assumption that server_encryption_options is not set in yaml rather than both
+            conf.server_encryption_options = conf.encryption_options;
+        }
+
+        conf.server_encryption_options.validate();
+
+        if (conf.user_defined_function_fail_timeout < 0)
+            throw new ConfigurationException("user_defined_function_fail_timeout must not be negative", false);
+        if (conf.user_defined_function_warn_timeout < 0)
+            throw new ConfigurationException("user_defined_function_warn_timeout must not be negative", false);
+
+        if (conf.user_defined_function_fail_timeout < conf.user_defined_function_warn_timeout)
+            throw new ConfigurationException("user_defined_function_warn_timeout must less than user_defined_function_fail_timeout", false);
+
+        if (!conf.allow_insecure_udfs && !conf.enable_user_defined_functions_threads)
+            throw new ConfigurationException("To be able to set enable_user_defined_functions_threads: false you need to set allow_insecure_udfs: true - this is an unsafe configuration and is not recommended.");
+
+        if (conf.allow_extra_insecure_udfs)
+            logger.warn("Allowing java.lang.System.* access in UDFs is dangerous and not recommended. Set allow_extra_insecure_udfs: false to disable.");
+
+        if (conf.commitlog_segment_size_in_mb <= 0)
+            throw new ConfigurationException("commitlog_segment_size_in_mb must be positive, but was "
+                    + conf.commitlog_segment_size_in_mb, false);
+        else if (conf.commitlog_segment_size_in_mb >= 2048)
+            throw new ConfigurationException("commitlog_segment_size_in_mb must be smaller than 2048, but was "
+                    + conf.commitlog_segment_size_in_mb, false);
+
+        if (conf.max_mutation_size_in_kb == null)
+            conf.max_mutation_size_in_kb = conf.commitlog_segment_size_in_mb * 1024 / 2;
+        else if (conf.commitlog_segment_size_in_mb * 1024 < 2 * conf.max_mutation_size_in_kb)
+            throw new ConfigurationException("commitlog_segment_size_in_mb must be at least twice the size of max_mutation_size_in_kb / 1024", false);
+
+        // native transport encryption options
+        if (conf.native_transport_port_ssl != null
+            && conf.native_transport_port_ssl != conf.native_transport_port
+            && !conf.client_encryption_options.enabled)
+        {
+            throw new ConfigurationException("Encryption must be enabled in client_encryption_options for native_transport_port_ssl", false);
+        }
+
+        // If max protocol version has been set, just validate it's within an acceptable range
+        if (conf.native_transport_max_negotiable_protocol_version != Integer.MIN_VALUE)
+        {
+            try
+            {
+                ProtocolVersion.decode(conf.native_transport_max_negotiable_protocol_version, ProtocolVersionLimit.SERVER_DEFAULT);
+                logger.info("Native transport max negotiable version statically limited to {}", conf.native_transport_max_negotiable_protocol_version);
+            }
+            catch (Exception e)
+            {
+                throw new ConfigurationException("Invalid setting for native_transport_max_negotiable_protocol_version; " +
+                                                 ProtocolVersion.invalidVersionMessage(conf.native_transport_max_negotiable_protocol_version));
+            }
+        }
+
+        if (conf.max_value_size_in_mb <= 0)
+            throw new ConfigurationException("max_value_size_in_mb must be positive", false);
+        else if (conf.max_value_size_in_mb >= 2048)
+            throw new ConfigurationException("max_value_size_in_mb must be smaller than 2048, but was "
+                    + conf.max_value_size_in_mb, false);
+
+        switch (conf.disk_optimization_strategy)
+        {
+            case ssd:
+                diskOptimizationStrategy = new SsdDiskOptimizationStrategy(conf.disk_optimization_page_cross_chance);
+                break;
+            case spinning:
+                diskOptimizationStrategy = new SpinningDiskOptimizationStrategy();
+                break;
+        }
+
+        try
+        {
+            ParameterizedClass strategy = conf.back_pressure_strategy != null ? conf.back_pressure_strategy : RateBasedBackPressure.withDefaultParams();
+            Class<?> clazz = Class.forName(strategy.class_name);
+            if (!BackPressureStrategy.class.isAssignableFrom(clazz))
+                throw new ConfigurationException(strategy + " is not an instance of " + BackPressureStrategy.class.getCanonicalName(), false);
+
+            Constructor<?> ctor = clazz.getConstructor(Map.class);
+            BackPressureStrategy instance = (BackPressureStrategy) ctor.newInstance(strategy.parameters);
+            logger.info("Back-pressure is {} with strategy {}.", backPressureEnabled() ? "enabled" : "disabled", conf.back_pressure_strategy);
+            backPressureStrategy = instance;
+        }
+        catch (ConfigurationException ex)
+        {
+            throw ex;
+        }
+        catch (Exception ex)
+        {
+            throw new ConfigurationException("Error configuring back-pressure strategy: " + conf.back_pressure_strategy, ex);
+        }
+
+        if (conf.otc_coalescing_enough_coalesced_messages > 128)
+            throw new ConfigurationException("otc_coalescing_enough_coalesced_messages must be smaller than 128", false);
+
+        if (conf.otc_coalescing_enough_coalesced_messages <= 0)
+            throw new ConfigurationException("otc_coalescing_enough_coalesced_messages must be positive", false);
+    }
+
+    private static String storagedirFor(String type)
+    {
+        return storagedir(type + "_directory") + File.separator + type;
+    }
+
+    private static String storagedir(String errMsgType)
+    {
+        String storagedir = System.getProperty(Config.PROPERTY_PREFIX + "storagedir", null);
+        if (storagedir == null)
+            throw new ConfigurationException(errMsgType + " is missing and -Dcassandra.storagedir is not set", false);
+        return storagedir;
+    }
+
+    public static void applyAddressConfig() throws ConfigurationException
+    {
+        applyAddressConfig(conf);
+    }
+
     public static void applyAddressConfig(Config config) throws ConfigurationException
     {
         listenAddress = null;
@@ -289,247 +910,84 @@
         }
     }
 
-    @VisibleForTesting
-    static void applyTokensConfig(Config config) throws ConfigurationException
+    public static void applyThriftHSHA()
     {
-        if (config.num_tokens != null && config.num_tokens > MAX_NUM_TOKENS)
-            throw new ConfigurationException(String.format("A maximum number of %d tokens per node is supported", MAX_NUM_TOKENS), false);
+        // fail early instead of OOMing (see CASSANDRA-8116)
+        if (ThriftServerType.HSHA.equals(conf.rpc_server_type) && conf.rpc_max_threads == Integer.MAX_VALUE)
+            throw new ConfigurationException("The hsha rpc_server_type is not compatible with an rpc_max_threads " +
+                                             "setting of 'unlimited'.  Please see the comments in cassandra.yaml " +
+                                             "for rpc_server_type and rpc_max_threads.",
+                                             false);
+        if (ThriftServerType.HSHA.equals(conf.rpc_server_type) && conf.rpc_max_threads > (FBUtilities.getAvailableProcessors() * 2 + 1024))
+            logger.warn("rpc_max_threads setting of {} may be too high for the hsha server and cause unnecessary thread contention, reducing performance", conf.rpc_max_threads);
+    }
 
-        if (config.initial_token != null)
+    public static void applyEncryptionContext()
+    {
+        // always attempt to load the cipher factory, as we could be in the situation where the user has disabled encryption,
+        // but has existing commitlogs and sstables on disk that are still encrypted (and still need to be read)
+        encryptionContext = new EncryptionContext(conf.transparent_data_encryption_options);
+    }
+
+    public static void applySeedProvider()
+    {
+        // load the seeds for node contact points
+        if (conf.seed_provider == null)
         {
-            Collection<String> tokens = tokensFromString(config.initial_token);
-            if (config.num_tokens == null)
+            throw new ConfigurationException("seeds configuration is missing; a minimum of one seed is required.", false);
+        }
+        try
+        {
+            Class<?> seedProviderClass = Class.forName(conf.seed_provider.class_name);
+            seedProvider = (SeedProvider)seedProviderClass.getConstructor(Map.class).newInstance(conf.seed_provider.parameters);
+        }
+        // there are about 5 checked exceptions that could be thrown here.
+        catch (Exception e)
+        {
+            throw new ConfigurationException(e.getMessage() + "\nFatal configuration error; unable to start server.  See log for stacktrace.", true);
+        }
+        if (seedProvider.getSeeds().size() == 0)
+            throw new ConfigurationException("The seed provider lists no seeds.", false);
+    }
+
+    public static void applyTokensConfig()
+    {
+        applyTokensConfig(conf);
+    }
+
+    static void applyTokensConfig(Config conf)
+    {
+        if (conf.initial_token != null)
+        {
+            Collection<String> tokens = tokensFromString(conf.initial_token);
+            if (conf.num_tokens == null)
             {
                 if (tokens.size() == 1)
-                    config.num_tokens = 1;
+                    conf.num_tokens = 1;
                 else
                     throw new ConfigurationException("initial_token was set but num_tokens is not!", false);
             }
 
-            if (tokens.size() != config.num_tokens)
+            if (tokens.size() != conf.num_tokens)
             {
                 throw new ConfigurationException(String.format("The number of initial tokens (by initial_token) specified (%s) is different from num_tokens value (%s)",
                                                                tokens.size(),
-                                                               config.num_tokens),
+                                                               conf.num_tokens),
                                                  false);
             }
 
             for (String token : tokens)
                 partitioner.getTokenFactory().validate(token);
         }
-        else if (config.num_tokens == null)
+        else if (conf.num_tokens == null)
         {
-            config.num_tokens = 1;
+            conf.num_tokens = 1;
         }
     }
 
-    public static void applyConfig(Config config) throws ConfigurationException
+    // Maybe safe for clients + tools
+    public static void applyRequestScheduler()
     {
-        conf = config;
-
-        if (conf.commitlog_sync == null)
-        {
-            throw new ConfigurationException("Missing required directive CommitLogSync", false);
-        }
-
-        if (conf.commitlog_sync == Config.CommitLogSync.batch)
-        {
-            if (conf.commitlog_sync_batch_window_in_ms == null)
-            {
-                throw new ConfigurationException("Missing value for commitlog_sync_batch_window_in_ms: Double expected.", false);
-            }
-            else if (conf.commitlog_sync_period_in_ms != null)
-            {
-                throw new ConfigurationException("Batch sync specified, but commitlog_sync_period_in_ms found. Only specify commitlog_sync_batch_window_in_ms when using batch sync", false);
-            }
-            logger.debug("Syncing log with a batch window of {}", conf.commitlog_sync_batch_window_in_ms);
-        }
-        else
-        {
-            if (conf.commitlog_sync_period_in_ms == null)
-            {
-                throw new ConfigurationException("Missing value for commitlog_sync_period_in_ms: Integer expected", false);
-            }
-            else if (conf.commitlog_sync_batch_window_in_ms != null)
-            {
-                throw new ConfigurationException("commitlog_sync_period_in_ms specified, but commitlog_sync_batch_window_in_ms found.  Only specify commitlog_sync_period_in_ms when using periodic sync.", false);
-            }
-            logger.debug("Syncing log with a period of {}", conf.commitlog_sync_period_in_ms);
-        }
-
-        /* evaluate the DiskAccessMode Config directive, which also affects indexAccessMode selection */
-        if (conf.disk_access_mode == Config.DiskAccessMode.auto)
-        {
-            conf.disk_access_mode = hasLargeAddressSpace() ? Config.DiskAccessMode.mmap : Config.DiskAccessMode.standard;
-            indexAccessMode = conf.disk_access_mode;
-            logger.info("DiskAccessMode 'auto' determined to be {}, indexAccessMode is {}", conf.disk_access_mode, indexAccessMode);
-        }
-        else if (conf.disk_access_mode == Config.DiskAccessMode.mmap_index_only)
-        {
-            conf.disk_access_mode = Config.DiskAccessMode.standard;
-            indexAccessMode = Config.DiskAccessMode.mmap;
-            logger.info("DiskAccessMode is {}, indexAccessMode is {}", conf.disk_access_mode, indexAccessMode);
-        }
-        else
-        {
-            indexAccessMode = conf.disk_access_mode;
-            logger.info("DiskAccessMode is {}, indexAccessMode is {}", conf.disk_access_mode, indexAccessMode);
-        }
-
-        /* Authentication, authorization and role management backend, implementing IAuthenticator, IAuthorizer & IRoleMapper*/
-        if (conf.authenticator != null)
-            authenticator = FBUtilities.newAuthenticator(conf.authenticator);
-
-        if (conf.authorizer != null)
-            authorizer = FBUtilities.newAuthorizer(conf.authorizer);
-
-        if (authenticator instanceof AllowAllAuthenticator && !(authorizer instanceof AllowAllAuthorizer))
-            throw new ConfigurationException("AllowAllAuthenticator can't be used with " +  conf.authorizer, false);
-
-        if (conf.role_manager != null)
-            roleManager = FBUtilities.newRoleManager(conf.role_manager);
-        else
-            roleManager = new CassandraRoleManager();
-
-        if (authenticator instanceof PasswordAuthenticator && !(roleManager instanceof CassandraRoleManager))
-            throw new ConfigurationException("CassandraRoleManager must be used with PasswordAuthenticator", false);
-
-        if (conf.internode_authenticator != null)
-            internodeAuthenticator = FBUtilities.construct(conf.internode_authenticator, "internode_authenticator");
-        else
-            internodeAuthenticator = new AllowAllInternodeAuthenticator();
-
-        authenticator.validateConfiguration();
-        authorizer.validateConfiguration();
-        roleManager.validateConfiguration();
-        internodeAuthenticator.validateConfiguration();
-
-        /* Hashing strategy */
-        if (conf.partitioner == null)
-        {
-            throw new ConfigurationException("Missing directive: partitioner", false);
-        }
-        try
-        {
-            partitioner = FBUtilities.newPartitioner(System.getProperty("cassandra.partitioner", conf.partitioner));
-        }
-        catch (Exception e)
-        {
-            throw new ConfigurationException("Invalid partitioner class " + conf.partitioner, false);
-        }
-        paritionerName = partitioner.getClass().getCanonicalName();
-
-        if (config.gc_log_threshold_in_ms < 0)
-        {
-            throw new ConfigurationException("gc_log_threshold_in_ms must be a positive integer");
-        }
-
-        if (conf.gc_warn_threshold_in_ms < 0)
-        {
-            throw new ConfigurationException("gc_warn_threshold_in_ms must be a positive integer");
-        }
-
-        if (conf.max_hint_window_in_ms == null)
-        {
-            throw new ConfigurationException("max_hint_window_in_ms cannot be set to null", false);
-        }
-
-        /* phi convict threshold for FailureDetector */
-        if (conf.phi_convict_threshold < 5 || conf.phi_convict_threshold > 16)
-        {
-            throw new ConfigurationException("phi_convict_threshold must be between 5 and 16, but was " + conf.phi_convict_threshold, false);
-        }
-
-        /* Thread per pool */
-        if (conf.concurrent_reads != null && conf.concurrent_reads < 2)
-        {
-            throw new ConfigurationException("concurrent_reads must be at least 2, but was " + conf.concurrent_reads, false);
-        }
-
-        if (conf.concurrent_writes != null && conf.concurrent_writes < 2 && System.getProperty("cassandra.test.fail_mv_locks_count", "").isEmpty())
-        {
-            throw new ConfigurationException("concurrent_writes must be at least 2, but was " + conf.concurrent_writes, false);
-        }
-
-        if (conf.concurrent_counter_writes != null && conf.concurrent_counter_writes < 2)
-            throw new ConfigurationException("concurrent_counter_writes must be at least 2, but was " + conf.concurrent_counter_writes, false);
-
-        if (conf.concurrent_replicates != null)
-            logger.warn("concurrent_replicates has been deprecated and should be removed from cassandra.yaml");
-
-        if (conf.file_cache_size_in_mb == null)
-            conf.file_cache_size_in_mb = Math.min(512, (int) (Runtime.getRuntime().maxMemory() / (4 * 1048576)));
-
-        if (conf.memtable_offheap_space_in_mb == null)
-            conf.memtable_offheap_space_in_mb = (int) (Runtime.getRuntime().maxMemory() / (4 * 1048576));
-        if (conf.memtable_offheap_space_in_mb < 0)
-            throw new ConfigurationException("memtable_offheap_space_in_mb must be positive, but was " + conf.memtable_offheap_space_in_mb, false);
-        // for the moment, we default to twice as much on-heap space as off-heap, as heap overhead is very large
-        if (conf.memtable_heap_space_in_mb == null)
-            conf.memtable_heap_space_in_mb = (int) (Runtime.getRuntime().maxMemory() / (4 * 1048576));
-        if (conf.memtable_heap_space_in_mb <= 0)
-            throw new ConfigurationException("memtable_heap_space_in_mb must be positive, but was " + conf.memtable_heap_space_in_mb, false);
-        logger.info("Global memtable on-heap threshold is enabled at {}MB", conf.memtable_heap_space_in_mb);
-        if (conf.memtable_offheap_space_in_mb == 0)
-            logger.info("Global memtable off-heap threshold is disabled, HeapAllocator will be used instead");
-        else
-            logger.info("Global memtable off-heap threshold is enabled at {}MB", conf.memtable_offheap_space_in_mb);
-
-        if (conf.repair_session_max_tree_depth < 10)
-            throw new ConfigurationException("repair_session_max_tree_depth should not be < 10, but was " + conf.repair_session_max_tree_depth);
-        if (conf.repair_session_max_tree_depth > 20)
-            logger.warn("repair_session_max_tree_depth of " + conf.repair_session_max_tree_depth + " > 20 could lead to excessive memory usage");
-
-        applyAddressConfig(config);
-
-        if (conf.thrift_framed_transport_size_in_mb <= 0)
-            throw new ConfigurationException("thrift_framed_transport_size_in_mb must be positive, but was " + conf.thrift_framed_transport_size_in_mb, false);
-
-        if (conf.native_transport_max_frame_size_in_mb <= 0)
-            throw new ConfigurationException("native_transport_max_frame_size_in_mb must be positive, but was " + conf.native_transport_max_frame_size_in_mb, false);
-        else if (conf.native_transport_max_frame_size_in_mb >= 2048)
-            throw new ConfigurationException("native_transport_max_frame_size_in_mb must be smaller than 2048, but was "
-                    + conf.native_transport_max_frame_size_in_mb, false);
-
-        // fail early instead of OOMing (see CASSANDRA-8116)
-        if (ThriftServer.HSHA.equals(conf.rpc_server_type) && conf.rpc_max_threads == Integer.MAX_VALUE)
-            throw new ConfigurationException("The hsha rpc_server_type is not compatible with an rpc_max_threads " +
-                                             "setting of 'unlimited'.  Please see the comments in cassandra.yaml " +
-                                             "for rpc_server_type and rpc_max_threads.",
-                                             false);
-        if (ThriftServer.HSHA.equals(conf.rpc_server_type) && conf.rpc_max_threads > (FBUtilities.getAvailableProcessors() * 2 + 1024))
-            logger.warn("rpc_max_threads setting of {} may be too high for the hsha server and cause unnecessary thread contention, reducing performance", conf.rpc_max_threads);
-
-        /* end point snitch */
-        if (conf.endpoint_snitch == null)
-        {
-            throw new ConfigurationException("Missing endpoint_snitch directive", false);
-        }
-
-        if (conf.native_transport_max_concurrent_requests_in_bytes <= 0)
-        {
-            conf.native_transport_max_concurrent_requests_in_bytes = Runtime.getRuntime().maxMemory() / 10;
-        }
-
-        if (conf.native_transport_max_concurrent_requests_in_bytes_per_ip <= 0)
-        {
-            conf.native_transport_max_concurrent_requests_in_bytes_per_ip = Runtime.getRuntime().maxMemory() / 40;
-        }
-
-        snitch = createEndpointSnitch(conf.endpoint_snitch);
-        EndpointSnitchInfo.create();
-
-        localDC = snitch.getDatacenter(FBUtilities.getBroadcastAddress());
-        localComparator = (endpoint1, endpoint2) -> {
-            boolean local1 = localDC.equals(snitch.getDatacenter(endpoint1));
-            boolean local2 = localDC.equals(snitch.getDatacenter(endpoint2));
-            if (local1 && !local2)
-                return -1;
-            if (local2 && !local1)
-                return 1;
-            return 0;
-        };
-
         /* Request Scheduler setup */
         requestSchedulerOptions = conf.request_scheduler_options;
         if (conf.request_scheduler != null)
@@ -566,254 +1024,49 @@
             // Default to Keyspace
             requestSchedulerId = RequestSchedulerId.keyspace;
         }
+    }
 
-        // if data dirs, commitlog dir, or saved caches dir are set in cassandra.yaml, use that.  Otherwise,
-        // use -Dcassandra.storagedir (set in cassandra-env.sh) as the parent dir for data/, commitlog/, and saved_caches/
-        if (conf.commitlog_directory == null)
+    // definitely not safe for tools + clients - implicitly instantiates StorageService
+    public static void applySnitch()
+    {
+        /* end point snitch */
+        if (conf.endpoint_snitch == null)
         {
-            conf.commitlog_directory = System.getProperty("cassandra.storagedir", null);
-            if (conf.commitlog_directory == null)
-                throw new ConfigurationException("commitlog_directory is missing and -Dcassandra.storagedir is not set", false);
-            conf.commitlog_directory += File.separator + "commitlog";
+            throw new ConfigurationException("Missing endpoint_snitch directive", false);
         }
+        snitch = createEndpointSnitch(conf.dynamic_snitch, conf.endpoint_snitch);
+        EndpointSnitchInfo.create();
 
-        if (conf.hints_directory == null)
+        localDC = snitch.getDatacenter(FBUtilities.getBroadcastAddress());
+        localComparator = (endpoint1, endpoint2) -> {
+            boolean local1 = localDC.equals(snitch.getDatacenter(endpoint1));
+            boolean local2 = localDC.equals(snitch.getDatacenter(endpoint2));
+            if (local1 && !local2)
+                return -1;
+            if (local2 && !local1)
+                return 1;
+            return 0;
+        };
+    }
+
+    // definitely not safe for tools + clients - implicitly instantiates schema
+    public static void applyPartitioner()
+    {
+        /* Hashing strategy */
+        if (conf.partitioner == null)
         {
-            conf.hints_directory = System.getProperty("cassandra.storagedir", null);
-            if (conf.hints_directory == null)
-                throw new ConfigurationException("hints_directory is missing and -Dcassandra.storagedir is not set", false);
-            conf.hints_directory += File.separator + "hints";
-        }
-
-        if (conf.commitlog_total_space_in_mb == null)
-        {
-            int preferredSize = 8192;
-            int minSize;
-            try
-            {
-                // use 1/4 of available space.  See discussion on #10013 and #10199
-                minSize = Ints.saturatedCast((guessFileStore(conf.commitlog_directory).getTotalSpace() / 1048576) / 4);
-            }
-            catch (IOException e)
-            {
-                logger.debug("Error checking disk space", e);
-                throw new ConfigurationException(String.format("Unable to check disk space available to %s. Perhaps the Cassandra user does not have the necessary permissions",
-                                                               conf.commitlog_directory), e);
-            }
-            if (minSize < preferredSize)
-            {
-                logger.warn("Small commitlog volume detected at {}; setting commitlog_total_space_in_mb to {}.  You can override this in cassandra.yaml",
-                            conf.commitlog_directory, minSize);
-                conf.commitlog_total_space_in_mb = minSize;
-            }
-            else
-            {
-                conf.commitlog_total_space_in_mb = preferredSize;
-            }
-        }
-
-        if (conf.saved_caches_directory == null)
-        {
-            conf.saved_caches_directory = System.getProperty("cassandra.storagedir", null);
-            if (conf.saved_caches_directory == null)
-                throw new ConfigurationException("saved_caches_directory is missing and -Dcassandra.storagedir is not set", false);
-            conf.saved_caches_directory += File.separator + "saved_caches";
-        }
-        if (conf.data_file_directories == null || conf.data_file_directories.length == 0)
-        {
-            String defaultDataDir = System.getProperty("cassandra.storagedir", null);
-            if (defaultDataDir == null)
-                throw new ConfigurationException("data_file_directories is not missing and -Dcassandra.storagedir is not set", false);
-            conf.data_file_directories = new String[]{ defaultDataDir + File.separator + "data" };
-        }
-
-        long dataFreeBytes = 0;
-        /* data file and commit log directories. they get created later, when they're needed. */
-        for (String datadir : conf.data_file_directories)
-        {
-            if (datadir == null)
-                throw new ConfigurationException("data_file_directories must not contain empty entry", false);
-            if (datadir.equals(conf.commitlog_directory))
-                throw new ConfigurationException("commitlog_directory must not be the same as any data_file_directories", false);
-            if (datadir.equals(conf.hints_directory))
-                throw new ConfigurationException("hints_directory must not be the same as any data_file_directories", false);
-            if (datadir.equals(conf.saved_caches_directory))
-                throw new ConfigurationException("saved_caches_directory must not be the same as any data_file_directories", false);
-
-            try
-            {
-                dataFreeBytes = saturatedSum(dataFreeBytes, guessFileStore(datadir).getUnallocatedSpace());
-            }
-            catch (IOException e)
-            {
-                logger.debug("Error checking disk space", e);
-                throw new ConfigurationException(String.format("Unable to check disk space available to %s. Perhaps the Cassandra user does not have the necessary permissions",
-                                                               datadir), e);
-            }
-        }
-        if (dataFreeBytes < 64 * ONE_GB)
-            logger.warn("Only {} MB free across all data volumes. Consider adding more capacity to your cluster or removing obsolete snapshots",
-                        dataFreeBytes / ONE_MB);
-
-
-        if (conf.commitlog_directory.equals(conf.saved_caches_directory))
-            throw new ConfigurationException("saved_caches_directory must not be the same as the commitlog_directory", false);
-        if (conf.commitlog_directory.equals(conf.hints_directory))
-            throw new ConfigurationException("hints_directory must not be the same as the commitlog_directory", false);
-        if (conf.hints_directory.equals(conf.saved_caches_directory))
-            throw new ConfigurationException("saved_caches_directory must not be the same as the hints_directory", false);
-
-        if (conf.memtable_flush_writers == null)
-            conf.memtable_flush_writers = Math.min(8, Math.max(2, Math.min(FBUtilities.getAvailableProcessors(), conf.data_file_directories.length)));
-
-        if (conf.memtable_flush_writers < 1)
-            throw new ConfigurationException("memtable_flush_writers must be at least 1, but was " + conf.memtable_flush_writers, false);
-
-        if (conf.memtable_cleanup_threshold == null)
-            conf.memtable_cleanup_threshold = (float) (1.0 / (1 + conf.memtable_flush_writers));
-
-        if (conf.memtable_cleanup_threshold < 0.01f)
-            throw new ConfigurationException("memtable_cleanup_threshold must be >= 0.01, but was " + conf.memtable_cleanup_threshold, false);
-        if (conf.memtable_cleanup_threshold > 0.99f)
-            throw new ConfigurationException("memtable_cleanup_threshold must be <= 0.99, but was " + conf.memtable_cleanup_threshold, false);
-        if (conf.memtable_cleanup_threshold < 0.1f)
-            logger.warn("memtable_cleanup_threshold is set very low [{}], which may cause performance degradation", conf.memtable_cleanup_threshold);
-
-        if (conf.concurrent_compactors == null)
-            conf.concurrent_compactors = Math.min(8, Math.max(2, Math.min(FBUtilities.getAvailableProcessors(), conf.data_file_directories.length)));
-
-        if (conf.concurrent_compactors <= 0)
-            throw new ConfigurationException("concurrent_compactors should be strictly greater than 0, but was " + conf.concurrent_compactors, false);
-
-        applyTokensConfig(config);
-
-        try
-        {
-            // if key_cache_size_in_mb option was set to "auto" then size of the cache should be "min(5% of Heap (in MB), 100MB)
-            keyCacheSizeInMB = (conf.key_cache_size_in_mb == null)
-                ? Math.min(Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.05 / 1024 / 1024)), 100)
-                : conf.key_cache_size_in_mb;
-
-            if (keyCacheSizeInMB < 0)
-                throw new NumberFormatException(); // to escape duplicating error message
-        }
-        catch (NumberFormatException e)
-        {
-            throw new ConfigurationException("key_cache_size_in_mb option was set incorrectly to '"
-                    + conf.key_cache_size_in_mb + "', supported values are <integer> >= 0.", false);
-        }
-
-        try
-        {
-            // if counter_cache_size_in_mb option was set to "auto" then size of the cache should be "min(2.5% of Heap (in MB), 50MB)
-            counterCacheSizeInMB = (conf.counter_cache_size_in_mb == null)
-                    ? Math.min(Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.025 / 1024 / 1024)), 50)
-                    : conf.counter_cache_size_in_mb;
-
-            if (counterCacheSizeInMB < 0)
-                throw new NumberFormatException(); // to escape duplicating error message
-        }
-        catch (NumberFormatException e)
-        {
-            throw new ConfigurationException("counter_cache_size_in_mb option was set incorrectly to '"
-                    + conf.counter_cache_size_in_mb + "', supported values are <integer> >= 0.", false);
-        }
-
-        // if set to empty/"auto" then use 5% of Heap size
-        indexSummaryCapacityInMB = (conf.index_summary_capacity_in_mb == null)
-            ? Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.05 / 1024 / 1024))
-            : conf.index_summary_capacity_in_mb;
-
-        if (indexSummaryCapacityInMB < 0)
-            throw new ConfigurationException("index_summary_capacity_in_mb option was set incorrectly to '"
-                    + conf.index_summary_capacity_in_mb + "', it should be a non-negative integer.", false);
-
-        if (conf.encryption_options != null)
-        {
-            logger.warn("Please rename encryption_options as server_encryption_options in the yaml");
-            //operate under the assumption that server_encryption_options is not set in yaml rather than both
-            conf.server_encryption_options = conf.encryption_options;
-        }
-
-        conf.server_encryption_options.validate();
-
-        // load the seeds for node contact points
-        if (conf.seed_provider == null)
-        {
-            throw new ConfigurationException("seeds configuration is missing; a minimum of one seed is required.", false);
+            throw new ConfigurationException("Missing directive: partitioner", false);
         }
         try
         {
-            Class<?> seedProviderClass = Class.forName(conf.seed_provider.class_name);
-            seedProvider = (SeedProvider)seedProviderClass.getConstructor(Map.class).newInstance(conf.seed_provider.parameters);
+            partitioner = FBUtilities.newPartitioner(System.getProperty(Config.PROPERTY_PREFIX + "partitioner", conf.partitioner));
         }
-        // there are about 5 checked exceptions that could be thrown here.
         catch (Exception e)
         {
-            throw new ConfigurationException(e.getMessage() + "\nFatal configuration error; unable to start server.  See log for stacktrace.", true);
-        }
-        if (seedProvider.getSeeds().size() == 0)
-            throw new ConfigurationException("The seed provider lists no seeds.", false);
-
-        if (!conf.allow_insecure_udfs && !conf.enable_user_defined_functions_threads)
-            throw new ConfigurationException("To be able to set enable_user_defined_functions_threads: false you need to set allow_insecure_udfs: true - this is an unsafe configuration and is not recommended.");
-
-        if (conf.allow_extra_insecure_udfs)
-            logger.warn("Allowing java.lang.System.* access in UDFs is dangerous and not recommended. Set allow_extra_insecure_udfs: false to disable.");
-
-        if (conf.user_defined_function_fail_timeout < 0)
-            throw new ConfigurationException("user_defined_function_fail_timeout must not be negative", false);
-        if (conf.user_defined_function_warn_timeout < 0)
-            throw new ConfigurationException("user_defined_function_warn_timeout must not be negative", false);
-
-        if (conf.user_defined_function_fail_timeout < conf.user_defined_function_warn_timeout)
-            throw new ConfigurationException("user_defined_function_warn_timeout must less than user_defined_function_fail_timeout", false);
-
-        if (conf.commitlog_segment_size_in_mb <= 0)
-            throw new ConfigurationException("commitlog_segment_size_in_mb must be positive, but was "
-                    + conf.commitlog_segment_size_in_mb, false);
-        else if (conf.commitlog_segment_size_in_mb >= 2048)
-            throw new ConfigurationException("commitlog_segment_size_in_mb must be smaller than 2048, but was "
-                    + conf.commitlog_segment_size_in_mb, false);
-
-        if (conf.max_mutation_size_in_kb == null)
-            conf.max_mutation_size_in_kb = conf.commitlog_segment_size_in_mb * 1024 / 2;
-        else if (conf.commitlog_segment_size_in_mb * 1024 < 2 * conf.max_mutation_size_in_kb)
-            throw new ConfigurationException("commitlog_segment_size_in_mb must be at least twice the size of max_mutation_size_in_kb / 1024", false);
-
-        // native transport encryption options
-        if (conf.native_transport_port_ssl != null
-            && conf.native_transport_port_ssl.intValue() != conf.native_transport_port.intValue()
-            && !conf.client_encryption_options.enabled)
-        {
-            throw new ConfigurationException("Encryption must be enabled in client_encryption_options for native_transport_port_ssl", false);
+            throw new ConfigurationException("Invalid partitioner class " + conf.partitioner, false);
         }
 
-        // If max protocol version has been set, just validate it's within an acceptable range
-        if (conf.native_transport_max_negotiable_protocol_version != Integer.MIN_VALUE)
-        {
-            if (conf.native_transport_max_negotiable_protocol_version < Server.MIN_SUPPORTED_VERSION
-                || conf.native_transport_max_negotiable_protocol_version > Server.CURRENT_VERSION)
-            {
-                throw new ConfigurationException(String.format("Invalid setting for native_transport_max_negotiable_version (%d); " +
-                                                               "Values between %s and %s are supported",
-                                                               conf.native_transport_max_negotiable_protocol_version,
-                                                               Server.MIN_SUPPORTED_VERSION,
-                                                               Server.CURRENT_VERSION));
-            }
-        }
-
-        if (conf.max_value_size_in_mb == null || conf.max_value_size_in_mb <= 0)
-            throw new ConfigurationException("max_value_size_in_mb must be positive", false);
-        else if (conf.max_value_size_in_mb >= 2048)
-            throw new ConfigurationException("max_value_size_in_mb must be smaller than 2048, but was "
-                    + conf.max_value_size_in_mb, false);
-
-        if (conf.otc_coalescing_enough_coalesced_messages > 128)
-            throw new ConfigurationException("otc_coalescing_enough_coalesced_messages must be smaller than 128", false);
-
-        if (conf.otc_coalescing_enough_coalesced_messages <= 0)
-            throw new ConfigurationException("otc_coalescing_enough_coalesced_messages must be positive", false);
+        paritionerName = partitioner.getClass().getCanonicalName();
     }
 
     /**
@@ -849,12 +1102,12 @@
         }
     }
 
-    private static IEndpointSnitch createEndpointSnitch(String snitchClassName) throws ConfigurationException
+    public static IEndpointSnitch createEndpointSnitch(boolean dynamic, String snitchClassName) throws ConfigurationException
     {
         if (!snitchClassName.contains("."))
             snitchClassName = "org.apache.cassandra.locator." + snitchClassName;
         IEndpointSnitch snitch = FBUtilities.construct(snitchClassName, "snitch");
-        return conf.dynamic_snitch ? new DynamicEndpointSnitch(snitch) : snitch;
+        return dynamic ? new DynamicEndpointSnitch(snitch) : snitch;
     }
 
     public static IAuthenticator getAuthenticator()
@@ -862,16 +1115,31 @@
         return authenticator;
     }
 
+    public static void setAuthenticator(IAuthenticator authenticator)
+    {
+        DatabaseDescriptor.authenticator = authenticator;
+    }
+
     public static IAuthorizer getAuthorizer()
     {
         return authorizer;
     }
 
+    public static void setAuthorizer(IAuthorizer authorizer)
+    {
+        DatabaseDescriptor.authorizer = authorizer;
+    }
+
     public static IRoleManager getRoleManager()
     {
         return roleManager;
     }
 
+    public static void setRoleManager(IRoleManager roleManager)
+    {
+        DatabaseDescriptor.roleManager = roleManager;
+    }
+
     public static int getPermissionsValidity()
     {
         return conf.permissions_validity_in_ms;
@@ -882,11 +1150,6 @@
         conf.permissions_validity_in_ms = timeout;
     }
 
-    public static int getPermissionsCacheMaxEntries()
-    {
-        return conf.permissions_cache_max_entries;
-    }
-
     public static int getPermissionsUpdateInterval()
     {
         return conf.permissions_update_interval_in_ms == -1
@@ -894,6 +1157,21 @@
              : conf.permissions_update_interval_in_ms;
     }
 
+    public static void setPermissionsUpdateInterval(int updateInterval)
+    {
+        conf.permissions_update_interval_in_ms = updateInterval;
+    }
+
+    public static int getPermissionsCacheMaxEntries()
+    {
+        return conf.permissions_cache_max_entries;
+    }
+
+    public static int setPermissionsCacheMaxEntries(int maxEntries)
+    {
+        return conf.permissions_cache_max_entries = maxEntries;
+    }
+
     public static int getRolesValidity()
     {
         return conf.roles_validity_in_ms;
@@ -904,11 +1182,6 @@
         conf.roles_validity_in_ms = validity;
     }
 
-    public static int getRolesCacheMaxEntries()
-    {
-        return conf.roles_cache_max_entries;
-    }
-
     public static int getRolesUpdateInterval()
     {
         return conf.roles_update_interval_in_ms == -1
@@ -921,9 +1194,46 @@
         conf.roles_update_interval_in_ms = interval;
     }
 
-    public static void setPermissionsUpdateInterval(int updateInterval)
+    public static int getRolesCacheMaxEntries()
     {
-        conf.permissions_update_interval_in_ms = updateInterval;
+        return conf.roles_cache_max_entries;
+    }
+
+    public static int setRolesCacheMaxEntries(int maxEntries)
+    {
+        return conf.roles_cache_max_entries = maxEntries;
+    }
+
+    public static int getCredentialsValidity()
+    {
+        return conf.credentials_validity_in_ms;
+    }
+
+    public static void setCredentialsValidity(int timeout)
+    {
+        conf.credentials_validity_in_ms = timeout;
+    }
+
+    public static int getCredentialsUpdateInterval()
+    {
+        return conf.credentials_update_interval_in_ms == -1
+               ? conf.credentials_validity_in_ms
+               : conf.credentials_update_interval_in_ms;
+    }
+
+    public static void setCredentialsUpdateInterval(int updateInterval)
+    {
+        conf.credentials_update_interval_in_ms = updateInterval;
+    }
+
+    public static int getCredentialsCacheMaxEntries()
+    {
+        return conf.credentials_cache_max_entries;
+    }
+
+    public static int setCredentialsCacheMaxEntries(int maxEntries)
+    {
+        return conf.credentials_cache_max_entries = maxEntries;
     }
 
     public static int getThriftFramedTransportSize()
@@ -965,6 +1275,13 @@
             if (conf.saved_caches_directory == null)
                 throw new ConfigurationException("saved_caches_directory must be specified", false);
             FileUtils.createDirectory(conf.saved_caches_directory);
+
+            if (conf.cdc_enabled)
+            {
+                if (conf.cdc_raw_directory == null)
+                    throw new ConfigurationException("cdc_raw_directory must be specified", false);
+                FileUtils.createDirectory(conf.cdc_raw_directory);
+            }
         }
         catch (ConfigurationException e)
         {
@@ -1023,6 +1340,23 @@
         return conf.column_index_size_in_kb * 1024;
     }
 
+    @VisibleForTesting
+    public static void setColumnIndexSize(int val)
+    {
+        conf.column_index_size_in_kb = val;
+    }
+
+    public static int getColumnIndexCacheSize()
+    {
+        return conf.column_index_cache_size_in_kb * 1024;
+    }
+
+    @VisibleForTesting
+    public static void setColumnIndexCacheSize(int val)
+    {
+        conf.column_index_cache_size_in_kb = val;
+    }
+
     public static int getBatchSizeWarnThreshold()
     {
         return conf.batch_size_warn_threshold_in_kb * 1024;
@@ -1055,24 +1389,24 @@
 
     public static Collection<String> getInitialTokens()
     {
-        return tokensFromString(System.getProperty("cassandra.initial_token", conf.initial_token));
+        return tokensFromString(System.getProperty(Config.PROPERTY_PREFIX + "initial_token", conf.initial_token));
     }
 
     public static String getAllocateTokensForKeyspace()
     {
-        return System.getProperty("cassandra.allocate_tokens_for_keyspace", conf.allocate_tokens_for_keyspace);
+        return System.getProperty(Config.PROPERTY_PREFIX + "allocate_tokens_for_keyspace", conf.allocate_tokens_for_keyspace);
     }
 
     public static Collection<String> tokensFromString(String tokenString)
     {
         List<String> tokens = new ArrayList<>();
         if (tokenString != null)
-            for (String token : tokenString.split(","))
-                tokens.add(token.replaceAll("^\\s+", "").replaceAll("\\s+$", ""));
+            for (String token : StringUtils.split(tokenString, ','))
+                tokens.add(token.trim());
         return tokens;
     }
 
-    public static Integer getNumTokens()
+    public static int getNumTokens()
     {
         return conf.num_tokens;
     }
@@ -1081,44 +1415,34 @@
     {
         try
         {
-            if (System.getProperty("cassandra.replace_address", null) != null)
-                return InetAddress.getByName(System.getProperty("cassandra.replace_address", null));
-            else if (System.getProperty("cassandra.replace_address_first_boot", null) != null)
-                return InetAddress.getByName(System.getProperty("cassandra.replace_address_first_boot", null));
+            if (System.getProperty(Config.PROPERTY_PREFIX + "replace_address", null) != null)
+                return InetAddress.getByName(System.getProperty(Config.PROPERTY_PREFIX + "replace_address", null));
+            else if (System.getProperty(Config.PROPERTY_PREFIX + "replace_address_first_boot", null) != null)
+                return InetAddress.getByName(System.getProperty(Config.PROPERTY_PREFIX + "replace_address_first_boot", null));
             return null;
         }
         catch (UnknownHostException e)
         {
-            throw new RuntimeException("Replacement ost name could not be resolved or scope_id was specified for a global IPv6 address", e);
+            throw new RuntimeException("Replacement host name could not be resolved or scope_id was specified for a global IPv6 address", e);
         }
     }
 
     public static Collection<String> getReplaceTokens()
     {
-        return tokensFromString(System.getProperty("cassandra.replace_token", null));
+        return tokensFromString(System.getProperty(Config.PROPERTY_PREFIX + "replace_token", null));
     }
 
     public static UUID getReplaceNode()
     {
         try
         {
-            return UUID.fromString(System.getProperty("cassandra.replace_node", null));
+            return UUID.fromString(System.getProperty(Config.PROPERTY_PREFIX + "replace_node", null));
         } catch (NullPointerException e)
         {
             return null;
         }
     }
 
-    public static boolean isReplacing()
-    {
-        if (System.getProperty("cassandra.replace_address_first_boot", null) != null && SystemKeyspace.bootstrapComplete())
-        {
-            logger.info("Replace address on first boot requested; this node is already bootstrapped");
-            return false;
-        }
-        return getReplaceAddress() != null;
-    }
-
     public static String getClusterName()
     {
         return conf.cluster_name;
@@ -1126,17 +1450,17 @@
 
     public static int getStoragePort()
     {
-        return Integer.parseInt(System.getProperty("cassandra.storage_port", conf.storage_port.toString()));
+        return Integer.parseInt(System.getProperty(Config.PROPERTY_PREFIX + "storage_port", Integer.toString(conf.storage_port)));
     }
 
     public static int getSSLStoragePort()
     {
-        return Integer.parseInt(System.getProperty("cassandra.ssl_storage_port", conf.ssl_storage_port.toString()));
+        return Integer.parseInt(System.getProperty(Config.PROPERTY_PREFIX + "ssl_storage_port", Integer.toString(conf.ssl_storage_port)));
     }
 
     public static int getRpcPort()
     {
-        return Integer.parseInt(System.getProperty("cassandra.rpc_port", conf.rpc_port.toString()));
+        return Integer.parseInt(System.getProperty(Config.PROPERTY_PREFIX + "rpc_port", Integer.toString(conf.rpc_port)));
     }
 
     public static int getRpcListenBacklog()
@@ -1149,7 +1473,7 @@
         return conf.request_timeout_in_ms;
     }
 
-    public static void setRpcTimeout(Long timeOutInMillis)
+    public static void setRpcTimeout(long timeOutInMillis)
     {
         conf.request_timeout_in_ms = timeOutInMillis;
     }
@@ -1159,7 +1483,7 @@
         return conf.read_request_timeout_in_ms;
     }
 
-    public static void setReadRpcTimeout(Long timeOutInMillis)
+    public static void setReadRpcTimeout(long timeOutInMillis)
     {
         conf.read_request_timeout_in_ms = timeOutInMillis;
     }
@@ -1169,7 +1493,7 @@
         return conf.range_request_timeout_in_ms;
     }
 
-    public static void setRangeRpcTimeout(Long timeOutInMillis)
+    public static void setRangeRpcTimeout(long timeOutInMillis)
     {
         conf.range_request_timeout_in_ms = timeOutInMillis;
     }
@@ -1179,7 +1503,7 @@
         return conf.write_request_timeout_in_ms;
     }
 
-    public static void setWriteRpcTimeout(Long timeOutInMillis)
+    public static void setWriteRpcTimeout(long timeOutInMillis)
     {
         conf.write_request_timeout_in_ms = timeOutInMillis;
     }
@@ -1189,7 +1513,7 @@
         return conf.counter_write_request_timeout_in_ms;
     }
 
-    public static void setCounterWriteRpcTimeout(Long timeOutInMillis)
+    public static void setCounterWriteRpcTimeout(long timeOutInMillis)
     {
         conf.counter_write_request_timeout_in_ms = timeOutInMillis;
     }
@@ -1199,7 +1523,7 @@
         return conf.cas_contention_timeout_in_ms;
     }
 
-    public static void setCasContentionTimeout(Long timeOutInMillis)
+    public static void setCasContentionTimeout(long timeOutInMillis)
     {
         conf.cas_contention_timeout_in_ms = timeOutInMillis;
     }
@@ -1209,7 +1533,7 @@
         return conf.truncate_request_timeout_in_ms;
     }
 
-    public static void setTruncateRpcTimeout(Long timeOutInMillis)
+    public static void setTruncateRpcTimeout(long timeOutInMillis)
     {
         conf.truncate_request_timeout_in_ms = timeOutInMillis;
     }
@@ -1219,32 +1543,9 @@
         return conf.cross_node_timeout;
     }
 
-    // not part of the Verb enum so we can change timeouts easily via JMX
-    public static long getTimeout(MessagingService.Verb verb)
+    public static long getSlowQueryTimeout()
     {
-        switch (verb)
-        {
-            case READ:
-                return getReadRpcTimeout();
-            case RANGE_SLICE:
-            case PAGED_RANGE:
-                return getRangeRpcTimeout();
-            case TRUNCATE:
-                return getTruncateRpcTimeout();
-            case READ_REPAIR:
-            case MUTATION:
-            case PAXOS_COMMIT:
-            case PAXOS_PREPARE:
-            case PAXOS_PROPOSE:
-            case HINT:
-            case BATCH_STORE:
-            case BATCH_REMOVE:
-                return getWriteRpcTimeout();
-            case COUNTER_MUTATION:
-                return getCounterWriteRpcTimeout();
-            default:
-                return getRpcTimeout();
-        }
+        return conf.slow_query_log_timeout_in_ms;
     }
 
     /**
@@ -1300,6 +1601,11 @@
         return conf.concurrent_compactors;
     }
 
+    public static void setConcurrentCompactors(int value)
+    {
+        conf.concurrent_compactors = value;
+    }
+
     public static int getCompactionThroughputMbPerSec()
     {
         return conf.compaction_throughput_mb_per_sec;
@@ -1319,7 +1625,7 @@
 
     public static boolean getDisableSTCSInL0()
     {
-        return Boolean.getBoolean("cassandra.disable_stcs_in_l0");
+        return disableSTCSInL0;
     }
 
     public static int getStreamThroughputOutboundMegabitsPerSec()
@@ -1352,6 +1658,12 @@
         return conf.commitlog_directory;
     }
 
+    @VisibleForTesting
+    public static void setCommitLogLocation(String value)
+    {
+        conf.commitlog_directory = value;
+    }
+
     public static ParameterizedClass getCommitLogCompression()
     {
         return conf.commitlog_compression;
@@ -1362,11 +1674,21 @@
         conf.commitlog_compression = compressor;
     }
 
+   /**
+    * Maximum number of buffers in the compression pool. The default value is 3, it should not be set lower than that
+    * (one segment in compression, one written to, one in reserve); delays in compression may cause the log to use
+    * more, depending on how soon the sync policy stops all writing threads.
+    */
     public static int getCommitLogMaxCompressionBuffersInPool()
     {
         return conf.commitlog_max_compression_buffers_in_pool;
     }
 
+    public static void setCommitLogMaxCompressionBuffersPerPool(int buffers)
+    {
+        conf.commitlog_max_compression_buffers_in_pool = buffers;
+    }
+
     public static int getMaxMutationSize()
     {
         return conf.max_mutation_size_in_kb * 1024;
@@ -1445,7 +1767,7 @@
         return broadcastAddress;
     }
 
-    public static Boolean shouldListenOnBroadcastAddress()
+    public static boolean shouldListenOnBroadcastAddress()
     {
         return conf.listen_on_broadcast_address;
     }
@@ -1455,6 +1777,11 @@
         return internodeAuthenticator;
     }
 
+    public static void setInternodeAuthenticator(IInternodeAuthenticator internodeAuthenticator)
+    {
+        DatabaseDescriptor.internodeAuthenticator = internodeAuthenticator;
+    }
+
     public static void setBroadcastAddress(InetAddress broadcastAdd)
     {
         broadcastAddress = broadcastAdd;
@@ -1513,12 +1840,12 @@
         return conf.rpc_recv_buff_size_in_bytes;
     }
 
-    public static Integer getInternodeSendBufferSize()
+    public static int getInternodeSendBufferSize()
     {
         return conf.internode_send_buff_size_in_bytes;
     }
 
-    public static Integer getInternodeRecvBufferSize()
+    public static int getInternodeRecvBufferSize()
     {
         return conf.internode_recv_buff_size_in_bytes;
     }
@@ -1530,7 +1857,7 @@
 
     public static int getNativeTransportPort()
     {
-        return Integer.parseInt(System.getProperty("cassandra.native_transport_port", conf.native_transport_port.toString()));
+        return Integer.parseInt(System.getProperty(Config.PROPERTY_PREFIX + "native_transport_port", Integer.toString(conf.native_transport_port)));
     }
 
     @VisibleForTesting
@@ -1550,7 +1877,7 @@
         conf.native_transport_port_ssl = port;
     }
 
-    public static Integer getNativeTransportMaxThreads()
+    public static int getNativeTransportMaxThreads()
     {
         return conf.native_transport_max_threads;
     }
@@ -1560,7 +1887,7 @@
         return conf.native_transport_max_frame_size_in_mb * 1024 * 1024;
     }
 
-    public static Long getNativeTransportMaxConcurrentConnections()
+    public static long getNativeTransportMaxConcurrentConnections()
     {
         return conf.native_transport_max_concurrent_connections;
     }
@@ -1570,7 +1897,8 @@
         conf.native_transport_max_concurrent_connections = nativeTransportMaxConcurrentConnections;
     }
 
-    public static Long getNativeTransportMaxConcurrentConnectionsPerIp() {
+    public static long getNativeTransportMaxConcurrentConnectionsPerIp()
+    {
         return conf.native_transport_max_concurrent_connections_per_ip;
     }
 
@@ -1688,7 +2016,8 @@
         return conf.snapshot_before_compaction;
     }
 
-    public static boolean isAutoSnapshot() {
+    public static boolean isAutoSnapshot()
+    {
         return conf.auto_snapshot;
     }
 
@@ -1705,7 +2034,7 @@
 
     public static boolean isAutoBootstrap()
     {
-        return Boolean.parseBoolean(System.getProperty("cassandra.auto_bootstrap", conf.auto_bootstrap.toString()));
+        return Boolean.parseBoolean(System.getProperty(Config.PROPERTY_PREFIX + "auto_bootstrap", Boolean.toString(conf.auto_bootstrap)));
     }
 
     public static void setHintedHandoffEnabled(boolean hintedHandoffEnabled)
@@ -1748,7 +2077,7 @@
         return new File(conf.hints_directory);
     }
 
-    public static File getSerializedCachePath(CacheService.CacheType cacheType, String version, String extension)
+    public static File getSerializedCachePath(CacheType cacheType, String version, String extension)
     {
         String name = cacheType.toString()
                 + (version == null ? "" : '-' + version + '.' + extension);
@@ -1759,7 +2088,7 @@
     {
         return conf.dynamic_snitch_update_interval_in_ms;
     }
-    public static void setDynamicUpdateInterval(Integer dynamicUpdateInterval)
+    public static void setDynamicUpdateInterval(int dynamicUpdateInterval)
     {
         conf.dynamic_snitch_update_interval_in_ms = dynamicUpdateInterval;
     }
@@ -1768,7 +2097,7 @@
     {
         return conf.dynamic_snitch_reset_interval_in_ms;
     }
-    public static void setDynamicResetInterval(Integer dynamicResetInterval)
+    public static void setDynamicResetInterval(int dynamicResetInterval)
     {
         conf.dynamic_snitch_reset_interval_in_ms = dynamicResetInterval;
     }
@@ -1778,17 +2107,17 @@
         return conf.dynamic_snitch_badness_threshold;
     }
 
-    public static void setDynamicBadnessThreshold(Double dynamicBadnessThreshold)
+    public static void setDynamicBadnessThreshold(double dynamicBadnessThreshold)
     {
         conf.dynamic_snitch_badness_threshold = dynamicBadnessThreshold;
     }
 
-    public static ServerEncryptionOptions getServerEncryptionOptions()
+    public static EncryptionOptions.ServerEncryptionOptions getServerEncryptionOptions()
     {
         return conf.server_encryption_options;
     }
 
-    public static ClientEncryptionOptions getClientEncryptionOptions()
+    public static EncryptionOptions.ClientEncryptionOptions getClientEncryptionOptions()
     {
         return conf.client_encryption_options;
     }
@@ -1803,7 +2132,7 @@
         return conf.batchlog_replay_throttle_in_kb;
     }
 
-    public static void setHintedHandoffThrottleInKB(Integer throttleInKB)
+    public static void setHintedHandoffThrottleInKB(int throttleInKB)
     {
         conf.hinted_handoff_throttle_in_kb = throttleInKB;
     }
@@ -1845,23 +2174,36 @@
 
     public static int getFileCacheSizeInMB()
     {
+        if (conf.file_cache_size_in_mb == null)
+        {
+            // In client mode the value is not set.
+            assert DatabaseDescriptor.isClientInitialized();
+            return 0;
+        }
+
         return conf.file_cache_size_in_mb;
     }
 
+    public static boolean getFileCacheRoundUp()
+    {
+        if (conf.file_cache_round_up == null)
+        {
+            // In client mode the value is not set.
+            assert DatabaseDescriptor.isClientInitialized();
+            return false;
+        }
+
+        return conf.file_cache_round_up;
+    }
+
     public static boolean getBufferPoolUseHeapIfExhausted()
     {
         return conf.buffer_pool_use_heap_if_exhausted;
     }
 
-    public static Config.DiskOptimizationStrategy getDiskOptimizationStrategy()
+    public static DiskOptimizationStrategy getDiskOptimizationStrategy()
     {
-        return conf.disk_optimization_strategy;
-    }
-
-    @VisibleForTesting
-    public static void setDiskOptimizationStrategy(Config.DiskOptimizationStrategy strategy)
-    {
-        conf.disk_optimization_strategy = strategy;
+        return diskOptimizationStrategy;
     }
 
     public static double getDiskOptimizationEstimatePercentile()
@@ -1869,17 +2211,6 @@
         return conf.disk_optimization_estimate_percentile;
     }
 
-    public static double getDiskOptimizationPageCrossChance()
-    {
-        return conf.disk_optimization_page_cross_chance;
-    }
-
-    @VisibleForTesting
-    public static void setDiskOptimizationPageCrossChance(double chance)
-    {
-        conf.disk_optimization_page_cross_chance = chance;
-    }
-
     public static long getTotalCommitlogSpaceInMB()
     {
         return conf.commitlog_total_space_in_mb;
@@ -1887,7 +2218,7 @@
 
     public static int getSSTablePreempiveOpenIntervalInMB()
     {
-        return FBUtilities.isWindows() ? -1 : conf.sstable_preemptive_open_interval_in_mb;
+        return FBUtilities.isWindows ? -1 : conf.sstable_preemptive_open_interval_in_mb;
     }
     public static void setSSTablePreempiveOpenIntervalInMB(int mb)
     {
@@ -2006,11 +2337,26 @@
         conf.counter_cache_keys_to_save = counterCacheKeysToSave;
     }
 
+    public static void setStreamingSocketTimeout(int value)
+    {
+        conf.streaming_socket_timeout_in_ms = value;
+    }
+
+    /**
+     * @deprecated use {@link this#getStreamingKeepAlivePeriod()} instead
+     * @return streaming_socket_timeout_in_ms property
+     */
+    @Deprecated
     public static int getStreamingSocketTimeout()
     {
         return conf.streaming_socket_timeout_in_ms;
     }
 
+    public static int getStreamingKeepAlivePeriod()
+    {
+        return conf.streaming_keep_alive_period_in_secs;
+    }
+
     public static String getLocalDataCenter()
     {
         return localDC;
@@ -2031,37 +2377,24 @@
         return conf.inter_dc_tcp_nodelay;
     }
 
-
-    public static SSTableFormat.Type getSSTableFormat()
+    public static long getMemtableHeapSpaceInMb()
     {
-        return sstable_format;
+        return conf.memtable_heap_space_in_mb;
     }
 
-    public static MemtablePool getMemtableAllocatorPool()
+    public static long getMemtableOffheapSpaceInMb()
     {
-        long heapLimit = ((long) conf.memtable_heap_space_in_mb) << 20;
-        long offHeapLimit = ((long) conf.memtable_offheap_space_in_mb) << 20;
-        final MemtableCleaner cleaner = ColumnFamilyStore::flushLargestMemtable;
-        switch (conf.memtable_allocation_type)
-        {
-            case unslabbed_heap_buffers:
-                return new HeapPool(heapLimit, conf.memtable_cleanup_threshold, cleaner);
-            case heap_buffers:
-                return new SlabPool(heapLimit, 0, conf.memtable_cleanup_threshold, cleaner);
-            case offheap_buffers:
-                throw new ConfigurationException("offheap_buffers are not available in 3.0. They will be re-introduced in a future release, see https://issues.apache.org/jira/browse/CASSANDRA-9472 for details");
+        return conf.memtable_offheap_space_in_mb;
+    }
 
-                /*if (!FileUtils.isCleanerAvailable())
-                {
-                    throw new IllegalStateException("Could not free direct byte buffer: offheap_buffers is not a safe memtable_allocation_type without this ability, please adjust your config. This feature is only guaranteed to work on an Oracle JVM. Refusing to start.");
-                }
-                return new SlabPool(heapLimit, offHeapLimit, conf.memtable_cleanup_threshold, new ColumnFamilyStore.FlushLargestColumnFamily());*/
-            case offheap_objects:
-                throw new ConfigurationException("offheap_objects are not available in 3.0. They will be re-introduced in a future release, see https://issues.apache.org/jira/browse/CASSANDRA-9472 for details");
-                // return new NativePool(heapLimit, offHeapLimit, conf.memtable_cleanup_threshold, new ColumnFamilyStore.FlushLargestColumnFamily());
-            default:
-                throw new AssertionError();
-        }
+    public static Config.MemtableAllocationType getMemtableAllocationType()
+    {
+        return conf.memtable_allocation_type;
+    }
+
+    public static Float getMemtableCleanupThreshold()
+    {
+        return conf.memtable_cleanup_threshold;
     }
 
     public static int getRepairSessionMaxTreeDepth()
@@ -2080,11 +2413,6 @@
         conf.repair_session_max_tree_depth = depth;
     }
 
-    public static boolean getOutboundBindAny()
-    {
-        return Config.outboundBindAny || conf.listen_on_broadcast_address;
-    }
-
     public static int getIndexSummaryResizeIntervalInMinutes()
     {
         return conf.index_summary_resize_interval_in_minutes;
@@ -2151,6 +2479,16 @@
         return conf.windows_timer_interval;
     }
 
+    public static long getPreparedStatementsCacheSizeMB()
+    {
+        return preparedStatementsCacheSizeInMB;
+    }
+
+    public static long getThriftPreparedStatementsCacheSizeMB()
+    {
+        return thriftPreparedStatementsCacheSizeInMB;
+    }
+
     public static boolean enableUserDefinedFunctions()
     {
         return conf.enable_user_defined_functions;
@@ -2191,11 +2529,26 @@
         return conf.allow_extra_insecure_udfs;
     }
 
-    public static boolean enableMaterializedViews()
+    public static boolean getEnableMaterializedViews()
     {
         return conf.enable_materialized_views;
     }
 
+    public static void setEnableMaterializedViews(boolean enableMaterializedViews)
+    {
+        conf.enable_materialized_views = enableMaterializedViews;
+    }
+
+    public static boolean getEnableSASIIndexes()
+    {
+        return conf.enable_sasi_indexes;
+    }
+
+    public static void setEnableSASIIndexes(boolean enableSASIIndexes)
+    {
+        conf.enable_sasi_indexes = enableSASIIndexes;
+    }
+
     public static boolean enableDropCompactStorage()
     {
         return conf.enable_drop_compact_storage;
@@ -2232,11 +2585,84 @@
         return conf.gc_log_threshold_in_ms;
     }
 
+    public static EncryptionContext getEncryptionContext()
+    {
+        return encryptionContext;
+    }
+
     public static long getGCWarnThreshold()
     {
         return conf.gc_warn_threshold_in_ms;
     }
 
+    public static boolean isCDCEnabled()
+    {
+        return conf.cdc_enabled;
+    }
+
+    public static void setCDCEnabled(boolean cdc_enabled)
+    {
+        conf.cdc_enabled = cdc_enabled;
+    }
+
+    public static String getCDCLogLocation()
+    {
+        return conf.cdc_raw_directory;
+    }
+
+    public static int getCDCSpaceInMB()
+    {
+        return conf.cdc_total_space_in_mb;
+    }
+
+    @VisibleForTesting
+    public static void setCDCSpaceInMB(int input)
+    {
+        conf.cdc_total_space_in_mb = input;
+    }
+
+    public static int getCDCDiskCheckInterval()
+    {
+        return conf.cdc_free_space_check_interval_ms;
+    }
+
+    @VisibleForTesting
+    public static void setEncryptionContext(EncryptionContext ec)
+    {
+        encryptionContext = ec;
+    }
+
+    public static int searchConcurrencyFactor()
+    {
+        return searchConcurrencyFactor;
+    }
+
+    public static boolean isUnsafeSystem()
+    {
+        return unsafeSystem;
+    }
+
+    public static void setBackPressureEnabled(boolean backPressureEnabled)
+    {
+        conf.back_pressure_enabled = backPressureEnabled;
+    }
+
+    public static boolean backPressureEnabled()
+    {
+        return conf.back_pressure_enabled;
+    }
+
+    @VisibleForTesting
+    public static void setBackPressureStrategy(BackPressureStrategy strategy)
+    {
+        backPressureStrategy = strategy;
+    }
+
+    public static BackPressureStrategy getBackPressureStrategy()
+    {
+        return backPressureStrategy;
+    }
+
     public static boolean strictRuntimeChecks()
     {
         return strictRuntimeChecks;
diff --git a/src/java/org/apache/cassandra/config/EncryptionOptions.java b/src/java/org/apache/cassandra/config/EncryptionOptions.java
index 497768f..1b1d8ce 100644
--- a/src/java/org/apache/cassandra/config/EncryptionOptions.java
+++ b/src/java/org/apache/cassandra/config/EncryptionOptions.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.config;
 
+import javax.net.ssl.SSLSocketFactory;
 import java.net.InetAddress;
 
 import org.slf4j.Logger;
@@ -33,15 +34,12 @@
     public String keystore_password = "cassandra";
     public String truststore = "conf/.truststore";
     public String truststore_password = "cassandra";
-    public String[] cipher_suites = {
-        "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA",
-        "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
-        "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" 
-    };
+    public String[] cipher_suites = ((SSLSocketFactory)SSLSocketFactory.getDefault()).getDefaultCipherSuites();
     public String protocol = "TLS";
     public String algorithm = "SunX509";
     public String store_type = "JKS";
     public boolean require_client_auth = false;
+    public boolean require_endpoint_verification = false;
 
     public static class ClientEncryptionOptions extends EncryptionOptions
     {
diff --git a/src/java/org/apache/cassandra/config/ParameterizedClass.java b/src/java/org/apache/cassandra/config/ParameterizedClass.java
index 3020e45..d0542f5 100644
--- a/src/java/org/apache/cassandra/config/ParameterizedClass.java
+++ b/src/java/org/apache/cassandra/config/ParameterizedClass.java
@@ -21,7 +21,6 @@
 import java.util.Map;
 
 import com.google.common.base.Objects;
-import com.google.common.collect.ImmutableMap;
 
 public class ParameterizedClass
 {
diff --git a/src/java/org/apache/cassandra/config/Schema.java b/src/java/org/apache/cassandra/config/Schema.java
index c733c8d..04c70e6 100644
--- a/src/java/org/apache/cassandra/config/Schema.java
+++ b/src/java/org/apache/cassandra/config/Schema.java
@@ -17,18 +17,15 @@
  */
 package org.apache.cassandra.config;
 
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.util.*;
 import java.util.stream.Collectors;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import org.cliffc.high_scale_lib.NonBlockingHashMap;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.auth.AuthKeyspace;
 import org.apache.cassandra.cql3.functions.*;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
@@ -37,16 +34,14 @@
 import org.apache.cassandra.db.compaction.CompactionManager;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.gms.Gossiper;
 import org.apache.cassandra.index.Index;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.locator.LocalStrategy;
-import org.apache.cassandra.repair.SystemDistributedKeyspace;
 import org.apache.cassandra.schema.*;
 import org.apache.cassandra.service.MigrationManager;
-import org.apache.cassandra.tracing.TraceKeyspace;
 import org.apache.cassandra.utils.ConcurrentBiMap;
 import org.apache.cassandra.utils.Pair;
-import org.cliffc.high_scale_lib.NonBlockingHashMap;
 
 public class Schema
 {
@@ -54,22 +49,6 @@
 
     public static final Schema instance = new Schema();
 
-    /* system keyspace names (the ones with LocalStrategy replication strategy) */
-    public static final Set<String> LOCAL_SYSTEM_KEYSPACE_NAMES =
-        ImmutableSet.of(SystemKeyspace.NAME, SchemaKeyspace.NAME);
-
-    /* replicate system keyspace names (the ones with a "true" replication strategy) */
-    public static final Set<String> REPLICATED_SYSTEM_KEYSPACE_NAMES =
-        ImmutableSet.of(TraceKeyspace.NAME, AuthKeyspace.NAME, SystemDistributedKeyspace.NAME);
-
-    /**
-     * longest permissible KS or CF name.  Our main concern is that filename not be more than 255 characters;
-     * the filename will contain both the KS and CF names. Since non-schema-name components only take up
-     * ~64 characters, we could allow longer names than this, but on Windows, the entire path should be not greater than
-     * 255 characters, so a lower limit here helps avoid problems.  See CASSANDRA-4110.
-     */
-    public static final int NAME_LENGTH = 48;
-
     /* metadata map for faster keyspace lookup */
     private final Map<String, KeyspaceMetadata> keyspaces = new NonBlockingHashMap<>();
 
@@ -80,29 +59,14 @@
     private final ConcurrentBiMap<Pair<String, String>, UUID> cfIdMap = new ConcurrentBiMap<>();
 
     private volatile UUID version;
-
-    // 59adb24e-f3cd-3e02-97f0-5b395827453f
-    public static final UUID emptyVersion;
-
-
-    static
-    {
-        try
-        {
-            emptyVersion = UUID.nameUUIDFromBytes(MessageDigest.getInstance("MD5").digest());
-        }
-        catch (NoSuchAlgorithmException e)
-        {
-            throw new AssertionError();
-        }
-    }
+    private volatile UUID altVersion;
 
     /**
      * Initialize empty schema object and load the hardcoded system tables
      */
     public Schema()
     {
-        if (!Config.isClientMode())
+        if (DatabaseDescriptor.isDaemonInitialized() || DatabaseDescriptor.isToolInitialized())
         {
             load(SchemaKeyspace.metadata());
             load(SystemKeyspace.metadata());
@@ -110,22 +74,6 @@
     }
 
     /**
-     * @return whether or not the keyspace is a really system one (w/ LocalStrategy, unmodifiable, hardcoded)
-     */
-    public static boolean isLocalSystemKeyspace(String keyspaceName)
-    {
-        return LOCAL_SYSTEM_KEYSPACE_NAMES.contains(keyspaceName.toLowerCase());
-    }
-
-    /**
-     * @return whether or not the keyspace is a replicated system keyspace (trace, auth, sys-ditributed)
-     */
-    public static boolean isReplicatedSystemKeyspace(String keyspaceName)
-    {
-        return REPLICATED_SYSTEM_KEYSPACE_NAMES.contains(keyspaceName.toLowerCase());
-    }
-
-    /**
      * load keyspace (keyspace) definitions, but do not initialize the keyspace instances.
      * Schema version may be updated as the result.
      */
@@ -195,7 +143,8 @@
      * @param ksNameAndCFName
      * @return The named CFS or null if the keyspace, base table, or index don't exist
      */
-    public ColumnFamilyStore getColumnFamilyStoreIncludingIndexes(Pair<String, String> ksNameAndCFName) {
+    public ColumnFamilyStore getColumnFamilyStoreIncludingIndexes(Pair<String, String> ksNameAndCFName)
+    {
         String ksName = ksNameAndCFName.left;
         String cfName = ksNameAndCFName.right;
         Pair<String, String> baseTable;
@@ -256,10 +205,8 @@
      */
     public void storeKeyspaceInstance(Keyspace keyspace)
     {
-        if (keyspaceInstances.containsKey(keyspace.getName()))
+        if (keyspaceInstances.putIfAbsent(keyspace.getName(), keyspace) != null)
             throw new IllegalArgumentException(String.format("Keyspace %s was already initialized.", keyspace.getName()));
-
-        keyspaceInstances.put(keyspace.getName(), keyspace);
     }
 
     /**
@@ -322,6 +269,11 @@
         return getCFMetaData(descriptor.ksname, descriptor.cfname);
     }
 
+    public int getNumberOfTables()
+    {
+        return cfIdMap.size();
+    }
+
     public ViewDefinition getView(String keyspaceName, String viewName)
     {
         assert keyspaceName != null;
@@ -344,7 +296,7 @@
 
     private Set<String> getNonSystemKeyspacesSet()
     {
-        return Sets.difference(keyspaces.keySet(), LOCAL_SYSTEM_KEYSPACE_NAMES);
+        return Sets.difference(keyspaces.keySet(), SchemaConstants.LOCAL_SYSTEM_KEYSPACE_NAMES);
     }
 
     /**
@@ -373,7 +325,7 @@
      */
     public List<String> getUserKeyspaces()
     {
-        return ImmutableList.copyOf(Sets.difference(getNonSystemKeyspacesSet(), REPLICATED_SYSTEM_KEYSPACE_NAMES));
+        return ImmutableList.copyOf(Sets.difference(getNonSystemKeyspacesSet(), SchemaConstants.REPLICATED_SYSTEM_KEYSPACE_NAMES));
     }
 
     public Keyspaces getReplicatedKeyspaces()
@@ -382,7 +334,7 @@
 
         keyspaces.values()
                  .stream()
-                 .filter(k -> !Schema.isLocalSystemKeyspace(k.name))
+                 .filter(k -> !SchemaConstants.isLocalSystemKeyspace(k.name))
                  .forEach(builder::add);
 
         return builder.build();
@@ -566,38 +518,82 @@
     /* Version control */
 
     /**
-     * @return current schema version
+     * The schema version to announce.
+     * This will be either the "real" schema version including the {@code cdc} column,
+     * if no node in the cluster is running at 3.0, or a 3.0 compatible
+     * schema version, with the {@code cdc} column excluded, if at least one node is
+     * running 3.0.
+     *
+     * @return "current" schema version
      */
     public UUID getVersion()
     {
+        return Gossiper.instance.isEnabled() && Gossiper.instance.isAnyNodeOn30()
+               ? altVersion
+               : version;
+    }
+
+    /**
+     * The 3.11 schema version, always includes the {@code cdc} column.
+     */
+    public UUID getRealVersion()
+    {
         return version;
     }
 
     /**
+     * The "alternative" schema version, compatible to 3.0, always excludes the
+     * {@code cdc} column.
+     */
+    public UUID getAltVersion()
+    {
+        return altVersion;
+    }
+
+    /**
+     * Checks whether the given schema version is the same as the current local schema
+     * version, either the 3.0 compatible or "real" one.
+     */
+    public boolean isSameVersion(UUID schemaVersion)
+    {
+        return schemaVersion != null
+               && (schemaVersion.equals(version) || schemaVersion.equals(altVersion));
+    }
+
+    /**
      * Checks whether the current schema is empty.
      */
     public boolean isEmpty()
     {
-        return emptyVersion.equals(version);
+        return SchemaConstants.emptyVersion.equals(version);
     }
 
     /**
      * Read schema from system keyspace and calculate MD5 digest of every row, resulting digest
      * will be converted into UUID which would act as content-based version of the schema.
+     *
+     * 3.11 note: we calculate the "real" schema version and the 3.0 compatible schema
+     * version here.
      */
     public void updateVersion()
     {
-        version = SchemaKeyspace.calculateSchemaDigest();
-        SystemKeyspace.updateSchemaVersion(version);
+        Pair<UUID, UUID> mixedVersions = SchemaKeyspace.calculateSchemaDigest();
+        version = mixedVersions.left;
+        altVersion = mixedVersions.right;
+        SystemKeyspace.updateSchemaVersion(getVersion());
     }
 
-    /*
+    /**
      * Like updateVersion, but also announces via gossip
+     *
+     * 3.11 note: we announce the "current" schema version, which can be either the 3.0
+     * compatible one, if at least one node is still running 3.0, or the "real" schema version.
      */
     public void updateVersionAndAnnounce()
     {
         updateVersion();
-        MigrationManager.passiveAnnounce(version);
+        UUID current = getVersion();
+        MigrationManager.passiveAnnounce(current, current == getAltVersion());
     }
 
     /**
@@ -634,7 +630,7 @@
     public void dropKeyspace(String ksName)
     {
         KeyspaceMetadata ksm = Schema.instance.getKSMetaData(ksName);
-        String snapshotName = Keyspace.getTimestampedSnapshotName(ksName);
+        String snapshotName = Keyspace.getTimestampedSnapshotNameWithPrefix(ksName, ColumnFamilyStore.SNAPSHOT_DROP_PREFIX);
 
         CompactionManager.instance.interruptCompactionFor(ksm.tablesAndViews(), true);
 
@@ -655,9 +651,11 @@
             droppedCfs.add(cfm.cfId);
         }
 
-        // remove the keyspace from the static instances.
-        Keyspace.clear(ksm.name);
-        clearKeyspaceMetadata(ksm);
+        synchronized (Keyspace.class) {
+            // Remove the keyspace from the static instances.
+            Keyspace.clear(ksm.name);
+            clearKeyspaceMetadata(ksm);
+        }
 
         Keyspace.writeOrder.awaitNewBarrier();
 
@@ -713,7 +711,7 @@
         CompactionManager.instance.interruptCompactionFor(Collections.singleton(cfm), true);
 
         if (DatabaseDescriptor.isAutoSnapshot())
-            cfs.snapshot(Keyspace.getTimestampedSnapshotName(cfs.name));
+            cfs.snapshot(Keyspace.getTimestampedSnapshotNameWithPrefix(cfs.name, ColumnFamilyStore.SNAPSHOT_DROP_PREFIX));
         Keyspace.open(ksName).dropCf(cfm.cfId);
         MigrationManager.instance.notifyDropColumnFamily(cfm);
 
@@ -761,7 +759,7 @@
         CompactionManager.instance.interruptCompactionFor(Collections.singleton(cfs.metadata), true);
 
         if (DatabaseDescriptor.isAutoSnapshot())
-            cfs.snapshot(Keyspace.getTimestampedSnapshotName(cfs.name));
+            cfs.snapshot(Keyspace.getTimestampedSnapshotNameWithPrefix(cfs.name, ColumnFamilyStore.SNAPSHOT_DROP_PREFIX));
 
         // reinitialize the keyspace.
         ViewDefinition view = oldKsm.views.get(viewName).get();
@@ -842,4 +840,17 @@
 
         return transformed;
     }
+
+    /**
+     * Converts the given schema version to a string. Returns {@code unknown}, if {@code version} is {@code null}
+     * or {@code "(empty)"}, if {@code version} refers to an {@link SchemaConstants#emptyVersion empty) schema.
+     */
+    public static String schemaVersionToString(UUID version)
+    {
+        return version == null
+               ? "unknown"
+               : SchemaConstants.emptyVersion.equals(version)
+                 ? "(empty)"
+                 : version.toString();
+    }
 }
diff --git a/src/java/org/apache/cassandra/config/SchemaConstants.java b/src/java/org/apache/cassandra/config/SchemaConstants.java
new file mode 100644
index 0000000..9e60b60
--- /dev/null
+++ b/src/java/org/apache/cassandra/config/SchemaConstants.java
@@ -0,0 +1,82 @@
+/*
+ * 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.config;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Set;
+import java.util.UUID;
+
+import com.google.common.collect.ImmutableSet;
+
+public final class SchemaConstants
+{
+    public static final String SYSTEM_KEYSPACE_NAME = "system";
+    public static final String SCHEMA_KEYSPACE_NAME = "system_schema";
+
+    public static final String TRACE_KEYSPACE_NAME = "system_traces";
+    public static final String AUTH_KEYSPACE_NAME = "system_auth";
+    public static final String DISTRIBUTED_KEYSPACE_NAME = "system_distributed";
+
+    /* system keyspace names (the ones with LocalStrategy replication strategy) */
+    public static final Set<String> LOCAL_SYSTEM_KEYSPACE_NAMES =
+        ImmutableSet.of(SYSTEM_KEYSPACE_NAME, SCHEMA_KEYSPACE_NAME);
+
+    /* replicate system keyspace names (the ones with a "true" replication strategy) */
+    public static final Set<String> REPLICATED_SYSTEM_KEYSPACE_NAMES =
+        ImmutableSet.of(TRACE_KEYSPACE_NAME, AUTH_KEYSPACE_NAME, DISTRIBUTED_KEYSPACE_NAME);
+    /**
+     * longest permissible KS or CF name.  Our main concern is that filename not be more than 255 characters;
+     * the filename will contain both the KS and CF names. Since non-schema-name components only take up
+     * ~64 characters, we could allow longer names than this, but on Windows, the entire path should be not greater than
+     * 255 characters, so a lower limit here helps avoid problems.  See CASSANDRA-4110.
+     */
+    public static final int NAME_LENGTH = 48;
+
+    // 59adb24e-f3cd-3e02-97f0-5b395827453f
+    public static final UUID emptyVersion;
+
+    static
+    {
+        try
+        {
+            emptyVersion = UUID.nameUUIDFromBytes(MessageDigest.getInstance("MD5").digest());
+        }
+        catch (NoSuchAlgorithmException e)
+        {
+            throw new AssertionError();
+        }
+    }
+
+    /**
+     * @return whether or not the keyspace is a really system one (w/ LocalStrategy, unmodifiable, hardcoded)
+     */
+    public static boolean isLocalSystemKeyspace(String keyspaceName)
+    {
+        return LOCAL_SYSTEM_KEYSPACE_NAMES.contains(keyspaceName.toLowerCase());
+    }
+
+    /**
+     * @return whether or not the keyspace is a replicated system ks (system_auth, system_traces, system_distributed)
+     */
+    public static boolean isReplicatedSystemKeyspace(String keyspaceName)
+    {
+        return REPLICATED_SYSTEM_KEYSPACE_NAMES.contains(keyspaceName.toLowerCase());
+    }
+}
diff --git a/src/java/org/apache/cassandra/config/TransparentDataEncryptionOptions.java b/src/java/org/apache/cassandra/config/TransparentDataEncryptionOptions.java
new file mode 100644
index 0000000..4ad0305
--- /dev/null
+++ b/src/java/org/apache/cassandra/config/TransparentDataEncryptionOptions.java
@@ -0,0 +1,76 @@
+/*
+ * 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.config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+
+public class TransparentDataEncryptionOptions
+{
+    public boolean enabled = false;
+    public int chunk_length_kb = 64;
+    public String cipher = "AES/CBC/PKCS5Padding";
+    public String key_alias;
+    public int iv_length = 16;
+
+    public ParameterizedClass key_provider;
+
+    public TransparentDataEncryptionOptions()
+    {   }
+
+    public TransparentDataEncryptionOptions(boolean enabled)
+    {
+        this.enabled = enabled;
+    }
+
+    public TransparentDataEncryptionOptions(String cipher, String keyAlias, ParameterizedClass keyProvider)
+    {
+        this(true, cipher, keyAlias, keyProvider);
+    }
+
+    public TransparentDataEncryptionOptions(boolean enabled, String cipher, String keyAlias, ParameterizedClass keyProvider)
+    {
+        this.enabled = enabled;
+        this.cipher = cipher;
+        key_alias = keyAlias;
+        key_provider = keyProvider;
+    }
+
+    public String get(String key)
+    {
+        return key_provider.parameters.get(key);
+    }
+
+    @VisibleForTesting
+    public void remove(String key)
+    {
+        key_provider.parameters.remove(key);
+    }
+
+    public boolean equals(Object o)
+    {
+        return o instanceof TransparentDataEncryptionOptions && equals((TransparentDataEncryptionOptions) o);
+    }
+
+    public boolean equals(TransparentDataEncryptionOptions other)
+    {
+        // not sure if this is a great equals() impl....
+        return Objects.equal(cipher, other.cipher) &&
+               Objects.equal(key_alias, other.key_alias);
+    }
+}
diff --git a/src/java/org/apache/cassandra/config/ViewDefinition.java b/src/java/org/apache/cassandra/config/ViewDefinition.java
index 5300f56..77cbcc9 100644
--- a/src/java/org/apache/cassandra/config/ViewDefinition.java
+++ b/src/java/org/apache/cassandra/config/ViewDefinition.java
@@ -128,8 +128,10 @@
     }
 
     /**
-     * Replace the column {@param from} with {@param to} in this materialized view definition's partition,
+     * Replace the column 'from' with 'to' in this materialized view definition's partition,
      * clustering, or included columns.
+     * @param from the existing column
+     * @param to the new column
      */
     public void renameColumn(ColumnIdentifier from, ColumnIdentifier to)
     {
@@ -137,8 +139,8 @@
 
         // convert whereClause to Relations, rename ids in Relations, then convert back to whereClause
         List<Relation> relations = whereClauseToRelations(whereClause);
-        ColumnIdentifier.Raw fromRaw = new ColumnIdentifier.Literal(from.toString(), true);
-        ColumnIdentifier.Raw toRaw = new ColumnIdentifier.Literal(to.toString(), true);
+        ColumnDefinition.Raw fromRaw = ColumnDefinition.Raw.forQuoted(from.toString());
+        ColumnDefinition.Raw toRaw = ColumnDefinition.Raw.forQuoted(to.toString());
         List<Relation> newRelations = relations.stream()
                 .map(r -> r.renameIdentifier(fromRaw, toRaw))
                 .collect(Collectors.toList());
diff --git a/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java b/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java
index 8376300..e55ec85 100644
--- a/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java
+++ b/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java
@@ -17,13 +17,15 @@
  */
 package org.apache.cassandra.config;
 
-import java.beans.IntrospectionException;
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.InputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
 import java.net.URL;
+import java.util.Collections;
 import java.util.HashSet;
+
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -32,6 +34,9 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.io.ByteStreams;
+
+import org.apache.commons.lang3.SystemUtils;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -90,11 +95,13 @@
         return url;
     }
 
-    private static final URL storageConfigURL = getStorageConfigURL();
+    private static URL storageConfigURL;
 
     @Override
     public Config loadConfig() throws ConfigurationException
     {
+        if (storageConfigURL == null)
+            storageConfigURL = getStorageConfigURL();
         return loadConfig(storageConfigURL);
     }
 
@@ -114,17 +121,19 @@
                 throw new AssertionError(e);
             }
 
-            SafeConstructor constructor = new CustomConstructor(Config.class);
-            MissingPropertiesChecker propertiesChecker = new MissingPropertiesChecker();
+
+            SafeConstructor constructor = new CustomConstructor(Config.class, Yaml.class.getClassLoader());
+            PropertiesChecker propertiesChecker = new PropertiesChecker();
             constructor.setPropertyUtils(propertiesChecker);
             Yaml yaml = new Yaml(constructor);
-            Config result = yaml.loadAs(new ByteArrayInputStream(configBytes), Config.class);
+            Config result = loadConfig(yaml, configBytes);
             propertiesChecker.check();
             return result;
         }
         catch (YAMLException e)
         {
-            throw new ConfigurationException("Invalid yaml: " + url, e);
+            throw new ConfigurationException("Invalid yaml: " + url + SystemUtils.LINE_SEPARATOR
+                                             +  " Error: " + e.getMessage(), false);
         }
     }
 
@@ -137,7 +146,7 @@
     public static <T> T fromMap(Map<String,Object> map, boolean shouldCheck, Class<T> klass)
     {
         SafeConstructor constructor = new YamlConfigurationLoader.CustomConstructor(klass, klass.getClassLoader());
-        YamlConfigurationLoader.MissingPropertiesChecker propertiesChecker = new YamlConfigurationLoader.MissingPropertiesChecker();
+        YamlConfigurationLoader.PropertiesChecker propertiesChecker = new YamlConfigurationLoader.PropertiesChecker();
         constructor.setPropertyUtils(propertiesChecker);
         Yaml yaml = new Yaml(constructor);
         Node node = yaml.represent(map);
@@ -157,11 +166,6 @@
 
     static class CustomConstructor extends CustomClassLoaderConstructor
     {
-        CustomConstructor(Class<?> theRoot)
-        {
-            this(theRoot, Yaml.class.getClassLoader());
-        }
-
         CustomConstructor(Class<?> theRoot, ClassLoader classLoader)
         {
             super(theRoot, classLoader);
@@ -178,7 +182,7 @@
         }
 
         @Override
-        protected Map<Object, Object> createDefaultMap()
+        protected Map<Object, Object> createDefaultMap(int initSize)
         {
             return Maps.newConcurrentMap();
         }
@@ -188,39 +192,89 @@
         {
             return Sets.newConcurrentHashSet();
         }
-
-        @Override
-        protected Set<Object> createDefaultSet()
-        {
-            return Sets.newConcurrentHashSet();
-        }
     }
 
-    private static class MissingPropertiesChecker extends PropertyUtils
+    private static Config loadConfig(Yaml yaml, byte[] configBytes)
+    {
+        Config config = yaml.loadAs(new ByteArrayInputStream(configBytes), Config.class);
+        // If the configuration file is empty yaml will return null. In this case we should use the default
+        // configuration to avoid hitting a NPE at a later stage.
+        return config == null ? new Config() : config;
+    }
+
+    /**
+     * Utility class to check that there are no extra properties and that properties that are not null by default
+     * are not set to null.
+     */
+    private static class PropertiesChecker extends PropertyUtils
     {
         private final Set<String> missingProperties = new HashSet<>();
 
-        public MissingPropertiesChecker()
+        private final Set<String> nullProperties = new HashSet<>();
+
+        public PropertiesChecker()
         {
             setSkipMissingProperties(true);
         }
 
         @Override
-        public Property getProperty(Class<? extends Object> type, String name) throws IntrospectionException
+        public Property getProperty(Class<? extends Object> type, String name)
         {
-            Property result = super.getProperty(type, name);
+            final Property result = super.getProperty(type, name);
+
             if (result instanceof MissingProperty)
             {
                 missingProperties.add(result.getName());
             }
-            return result;
+
+            return new Property(result.getName(), result.getType())
+            {
+                @Override
+                public void set(Object object, Object value) throws Exception
+                {
+                    if (value == null && get(object) != null)
+                    {
+                        nullProperties.add(getName());
+                    }
+                    result.set(object, value);
+                }
+
+                @Override
+                public Class<?>[] getActualTypeArguments()
+                {
+                    return result.getActualTypeArguments();
+                }
+
+                @Override
+                public Object get(Object object)
+                {
+                    return result.get(object);
+                }
+
+                @Override
+                public List<Annotation> getAnnotations()
+                {
+                    return Collections.EMPTY_LIST;
+                }
+
+                @Override
+                public <A extends Annotation> A getAnnotation(Class<A> aClass)
+                {
+                    return null;
+                }
+            };
         }
 
         public void check() throws ConfigurationException
         {
+            if (!nullProperties.isEmpty())
+            {
+                throw new ConfigurationException("Invalid yaml. Those properties " + nullProperties + " are not valid", false);
+            }
+
             if (!missingProperties.isEmpty())
             {
-                throw new ConfigurationException("Invalid yaml. Please remove properties " + missingProperties + " from your cassandra.yaml");
+                throw new ConfigurationException("Invalid yaml. Please remove properties " + missingProperties + " from your cassandra.yaml", false);
             }
         }
     }
diff --git a/src/java/org/apache/cassandra/cql3/AbstractMarker.java b/src/java/org/apache/cassandra/cql3/AbstractMarker.java
index 14170b1..a7fb073 100644
--- a/src/java/org/apache/cassandra/cql3/AbstractMarker.java
+++ b/src/java/org/apache/cassandra/cql3/AbstractMarker.java
@@ -20,6 +20,7 @@
 import java.util.List;
 
 import org.apache.cassandra.cql3.functions.Function;
+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.exceptions.InvalidRequestException;
@@ -66,23 +67,39 @@
 
         public NonTerminal prepare(String keyspace, ColumnSpecification receiver) throws InvalidRequestException
         {
-            if (!(receiver.type instanceof CollectionType))
-                return new Constants.Marker(bindIndex, receiver);
-
-            switch (((CollectionType)receiver.type).kind)
+            if (receiver.type.isCollection())
             {
-                case LIST: return new Lists.Marker(bindIndex, receiver);
-                case SET:  return new Sets.Marker(bindIndex, receiver);
-                case MAP:  return new Maps.Marker(bindIndex, receiver);
+                switch (((CollectionType) receiver.type).kind)
+                {
+                    case LIST:
+                        return new Lists.Marker(bindIndex, receiver);
+                    case SET:
+                        return new Sets.Marker(bindIndex, receiver);
+                    case MAP:
+                        return new Maps.Marker(bindIndex, receiver);
+                    default:
+                        throw new AssertionError();
+                }
             }
-            throw new AssertionError();
+            else if (receiver.type.isUDT())
+            {
+                return new UserTypes.Marker(bindIndex, receiver);
+            }
+
+            return new Constants.Marker(bindIndex, receiver);
         }
 
+        @Override
         public AssignmentTestable.TestResult testAssignment(String keyspace, ColumnSpecification receiver)
         {
             return AssignmentTestable.TestResult.WEAKLY_ASSIGNABLE;
         }
 
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            return null;
+        }
+
         @Override
         public String getText()
         {
diff --git a/src/java/org/apache/cassandra/cql3/Attributes.java b/src/java/org/apache/cassandra/cql3/Attributes.java
index 832d0a7..d4e230f 100644
--- a/src/java/org/apache/cassandra/cql3/Attributes.java
+++ b/src/java/org/apache/cassandra/cql3/Attributes.java
@@ -23,6 +23,7 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.db.ExpirationDateOverflowHandling;
+import org.apache.cassandra.db.LivenessInfo;
 import org.apache.cassandra.db.marshal.Int32Type;
 import org.apache.cassandra.db.marshal.LongType;
 import org.apache.cassandra.exceptions.InvalidRequestException;
@@ -109,11 +110,11 @@
 
         ByteBuffer tval = timeToLive.bindAndGet(options);
         if (tval == null)
-            throw new InvalidRequestException("Invalid null value of TTL");
-
-        if (tval == ByteBufferUtil.UNSET_BYTE_BUFFER) // treat as unlimited
             return 0;
 
+        if (tval == ByteBufferUtil.UNSET_BYTE_BUFFER)
+            return metadata.params.defaultTimeToLive;
+
         try
         {
             Int32Type.instance.validate(tval);
@@ -130,6 +131,9 @@
         if (ttl > MAX_TTL)
             throw new InvalidRequestException(String.format("ttl is too large. requested (%d) maximum (%d)", ttl, MAX_TTL));
 
+        if (metadata.params.defaultTimeToLive != LivenessInfo.NO_TTL && ttl == LivenessInfo.NO_TTL)
+            return LivenessInfo.NO_TTL;
+
         ExpirationDateOverflowHandling.maybeApplyExpirationDateOverflowPolicy(metadata, ttl, false);
 
         return ttl;
diff --git a/src/java/org/apache/cassandra/cql3/CQL3Type.java b/src/java/org/apache/cassandra/cql3/CQL3Type.java
index 95524d9..d1e6809 100644
--- a/src/java/org/apache/cassandra/cql3/CQL3Type.java
+++ b/src/java/org/apache/cassandra/cql3/CQL3Type.java
@@ -26,6 +26,7 @@
 
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.db.marshal.CollectionType.Kind;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.SyntaxException;
@@ -33,23 +34,32 @@
 import org.apache.cassandra.schema.Types;
 import org.apache.cassandra.serializers.CollectionSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public interface CQL3Type
 {
     static final Logger logger = LoggerFactory.getLogger(CQL3Type.class);
 
-    public boolean isCollection();
+    default boolean isCollection()
+    {
+        return false;
+    }
+
+    default boolean isUDT()
+    {
+        return false;
+    }
+
     public AbstractType<?> getType();
 
     /**
      * Generates CQL literal from a binary value of this type.
-     *
-     * @param buffer the value to convert to a CQL literal. This value must be
+     *  @param buffer the value to convert to a CQL literal. This value must be
      * serialized with {@code version} of the native protocol.
      * @param version the native protocol version in which {@code buffer} is encoded.
      */
-    public String toCQLLiteral(ByteBuffer buffer, int version);
+    public String toCQLLiteral(ByteBuffer buffer, ProtocolVersion version);
 
     public enum Native implements CQL3Type
     {
@@ -61,6 +71,7 @@
         DATE        (SimpleDateType.instance),
         DECIMAL     (DecimalType.instance),
         DOUBLE      (DoubleType.instance),
+        DURATION    (DurationType.instance),
         EMPTY       (EmptyType.instance),
         FLOAT       (FloatType.instance),
         INET        (InetAddressType.instance),
@@ -82,11 +93,6 @@
             this.type = type;
         }
 
-        public boolean isCollection()
-        {
-            return false;
-        }
-
         public AbstractType<?> getType()
         {
             return type;
@@ -99,7 +105,7 @@
          * {@link org.apache.cassandra.serializers.TypeSerializer#toString(Object)}
          * {@link org.apache.cassandra.serializers.TypeSerializer#deserialize(ByteBuffer)} implementations.
          */
-        public String toCQLLiteral(ByteBuffer buffer, int version)
+        public String toCQLLiteral(ByteBuffer buffer, ProtocolVersion version)
         {
             return type.getSerializer().toCQLLiteral(buffer);
         }
@@ -125,17 +131,12 @@
             this(TypeParser.parse(className));
         }
 
-        public boolean isCollection()
-        {
-            return false;
-        }
-
         public AbstractType<?> getType()
         {
             return type;
         }
 
-        public String toCQLLiteral(ByteBuffer buffer, int version)
+        public String toCQLLiteral(ByteBuffer buffer, ProtocolVersion version)
         {
             // *always* use the 'blob' syntax to express custom types in CQL
             return Native.BLOB.toCQLLiteral(buffer, version);
@@ -183,7 +184,7 @@
             return true;
         }
 
-        public String toCQLLiteral(ByteBuffer buffer, int version)
+        public String toCQLLiteral(ByteBuffer buffer, ProtocolVersion version)
         {
             if (buffer == null)
                 return "null";
@@ -215,7 +216,7 @@
             return target.toString();
         }
 
-        private void generateMapCQLLiteral(ByteBuffer buffer, int version, StringBuilder target, int size)
+        private void generateMapCQLLiteral(ByteBuffer buffer, ProtocolVersion version, StringBuilder target, int size)
         {
             CQL3Type keys = ((MapType) type).getKeysType().asCQL3Type();
             CQL3Type values = ((MapType) type).getValuesType().asCQL3Type();
@@ -231,7 +232,7 @@
             }
         }
 
-        private static void generateSetOrListCQLLiteral(ByteBuffer buffer, int version, StringBuilder target, int size, CQL3Type elements)
+        private static void generateSetOrListCQLLiteral(ByteBuffer buffer, ProtocolVersion version, StringBuilder target, int size, CQL3Type elements)
         {
             for (int i = 0; i < size; i++)
             {
@@ -305,9 +306,9 @@
             return new UserDefined(UTF8Type.instance.compose(type.name), type);
         }
 
-        public boolean isCollection()
+        public boolean isUDT()
         {
-            return false;
+            return true;
         }
 
         public AbstractType<?> getType()
@@ -315,7 +316,7 @@
             return type;
         }
 
-        public String toCQLLiteral(ByteBuffer buffer, int version)
+        public String toCQLLiteral(ByteBuffer buffer, ProtocolVersion version)
         {
             if (buffer == null)
                 return "null";
@@ -377,7 +378,10 @@
         @Override
         public String toString()
         {
-            return "frozen<" + ColumnIdentifier.maybeQuote(name) + '>';
+            if (type.isMultiCell())
+                return ColumnIdentifier.maybeQuote(name);
+            else
+                return "frozen<" + ColumnIdentifier.maybeQuote(name) + '>';
         }
     }
 
@@ -395,17 +399,12 @@
             return new Tuple(type);
         }
 
-        public boolean isCollection()
-        {
-            return false;
-        }
-
         public AbstractType<?> getType()
         {
             return type;
         }
 
-        public String toCQLLiteral(ByteBuffer buffer, int version)
+        public String toCQLLiteral(ByteBuffer buffer, ProtocolVersion version)
         {
             if (buffer == null)
                 return "null";
@@ -485,12 +484,7 @@
     {
         protected boolean frozen = false;
 
-        protected abstract boolean supportsFreezing();
-
-        public boolean isCollection()
-        {
-            return false;
-        }
+        public abstract boolean supportsFreezing();
 
         public boolean isFrozen()
         {
@@ -502,11 +496,26 @@
             return true;
         }
 
+        public boolean isDuration()
+        {
+            return false;
+        }
+
         public boolean isCounter()
         {
             return false;
         }
 
+        public boolean isUDT()
+        {
+            return false;
+        }
+
+        public boolean isTuple()
+        {
+            return false;
+        }
+
         public String keyspace()
         {
             return null;
@@ -588,7 +597,7 @@
                 return type;
             }
 
-            protected boolean supportsFreezing()
+            public boolean supportsFreezing()
             {
                 return false;
             }
@@ -598,6 +607,11 @@
                 return type == Native.COUNTER;
             }
 
+            public boolean isDuration()
+            {
+                return type == Native.DURATION;
+            }
+
             @Override
             public String toString()
             {
@@ -627,7 +641,7 @@
                 frozen = true;
             }
 
-            protected boolean supportsFreezing()
+            public boolean supportsFreezing()
             {
                 return true;
             }
@@ -651,35 +665,50 @@
             {
                 assert values != null : "Got null values type for a collection";
 
-                if (!frozen && values.supportsFreezing() && !values.frozen)
-                    throw new InvalidRequestException("Non-frozen collections are not allowed inside collections: " + this);
+                // skip if innerType is tuple, since tuple is implicitly forzen
+                if (!frozen && values.supportsFreezing() && !values.frozen && !values.isTuple())
+                    throwNestedNonFrozenError(values);
 
                 // we represent Thrift supercolumns as maps, internally, and we do allow counters in supercolumns. Thus,
                 // for internal type parsing (think schema) we have to make an exception and allow counters as (map) values
                 if (values.isCounter() && !isInternal)
                     throw new InvalidRequestException("Counters are not allowed inside collections: " + this);
 
+                if (values.isDuration() && kind == Kind.SET)
+                    throw new InvalidRequestException("Durations are not allowed inside sets: " + this);
+
                 if (keys != null)
                 {
                     if (keys.isCounter())
                         throw new InvalidRequestException("Counters are not allowed inside collections: " + this);
+                    if (keys.isDuration())
+                        throw new InvalidRequestException("Durations are not allowed as map keys: " + this);
                     if (!frozen && keys.supportsFreezing() && !keys.frozen)
-                        throw new InvalidRequestException("Non-frozen collections are not allowed inside collections: " + this);
+                        throwNestedNonFrozenError(keys);
                 }
 
+                AbstractType<?> valueType = values.prepare(keyspace, udts).getType();
                 switch (kind)
                 {
                     case LIST:
-                        return new Collection(ListType.getInstance(values.prepare(keyspace, udts).getType(), !frozen));
+                        return new Collection(ListType.getInstance(valueType, !frozen));
                     case SET:
-                        return new Collection(SetType.getInstance(values.prepare(keyspace, udts).getType(), !frozen));
+                        return new Collection(SetType.getInstance(valueType, !frozen));
                     case MAP:
                         assert keys != null : "Got null keys type for a collection";
-                        return new Collection(MapType.getInstance(keys.prepare(keyspace, udts).getType(), values.prepare(keyspace, udts).getType(), !frozen));
+                        return new Collection(MapType.getInstance(keys.prepare(keyspace, udts).getType(), valueType, !frozen));
                 }
                 throw new AssertionError();
             }
 
+            private void throwNestedNonFrozenError(Raw innerType)
+            {
+                if (innerType instanceof RawCollection)
+                    throw new InvalidRequestException("Non-frozen collections are not allowed inside collections: " + this);
+                else if (innerType.isUDT())
+                    throw new InvalidRequestException("Non-frozen UDTs are not allowed inside collections: " + this);
+            }
+
             public boolean referencesUserType(String name)
             {
                 return (keys != null && keys.referencesUserType(name)) || values.referencesUserType(name);
@@ -721,7 +750,7 @@
 
             public boolean canBeNonFrozen()
             {
-                return false;
+                return true;
             }
 
             public CQL3Type prepare(String keyspace, Types udts) throws InvalidRequestException
@@ -744,9 +773,8 @@
                 if (type == null)
                     throw new InvalidRequestException("Unknown type " + name);
 
-                if (!frozen)
-                    throw new InvalidRequestException("Non-frozen User-Defined types are not supported, please use frozen<>");
-
+                if (frozen)
+                    type = type.freeze();
                 return new UserDefined(name.toString(), type);
             }
 
@@ -755,7 +783,12 @@
                 return this.name.getStringTypeName().equals(name);
             }
 
-            protected boolean supportsFreezing()
+            public boolean supportsFreezing()
+            {
+                return true;
+            }
+
+            public boolean isUDT()
             {
                 return true;
             }
@@ -763,7 +796,10 @@
             @Override
             public String toString()
             {
-                return name.toString();
+                if (frozen)
+                    return "frozen<" + name.toString() + '>';
+                else
+                    return name.toString();
             }
         }
 
@@ -773,19 +809,16 @@
 
             private RawTuple(List<CQL3Type.Raw> types)
             {
+                frozen = true;
                 this.types = types;
+                freeze();
             }
 
-            protected boolean supportsFreezing()
+            public boolean supportsFreezing()
             {
                 return true;
             }
 
-            public boolean isCollection()
-            {
-                return false;
-            }
-
             public void freeze() throws InvalidRequestException
             {
                 for (CQL3Type.Raw t : types)
@@ -797,9 +830,6 @@
 
             public CQL3Type prepare(String keyspace, Types udts) throws InvalidRequestException
             {
-                if (!frozen)
-                    freeze();
-
                 List<AbstractType<?>> ts = new ArrayList<>(types.size());
                 for (CQL3Type.Raw t : types)
                 {
@@ -811,6 +841,11 @@
                 return new Tuple(new TupleType(ts));
             }
 
+            public boolean isTuple()
+            {
+                return true;
+            }
+
             public boolean referencesUserType(String name)
             {
                 return types.stream().anyMatch(t -> t.referencesUserType(name));
diff --git a/src/java/org/apache/cassandra/cql3/CQLStatement.java b/src/java/org/apache/cassandra/cql3/CQLStatement.java
index 02292ad..901ecd4 100644
--- a/src/java/org/apache/cassandra/cql3/CQLStatement.java
+++ b/src/java/org/apache/cassandra/cql3/CQLStatement.java
@@ -50,8 +50,9 @@
      *
      * @param state the current query state
      * @param options options for this query (consistency, variables, pageSize, ...)
+     * @param queryStartNanoTime the timestamp returned by System.nanoTime() when this statement was received
      */
-    public ResultMessage execute(QueryState state, QueryOptions options) throws RequestValidationException, RequestExecutionException;
+    public ResultMessage execute(QueryState state, QueryOptions options, long queryStartNanoTime) throws RequestValidationException, RequestExecutionException;
 
     /**
      * Variant of execute used for internal query against the system tables, and thus only query the local node.
diff --git a/src/java/org/apache/cassandra/cql3/ColumnCondition.java b/src/java/org/apache/cassandra/cql3/ColumnCondition.java
index c3a3af7..43b0135 100644
--- a/src/java/org/apache/cassandra/cql3/ColumnCondition.java
+++ b/src/java/org/apache/cassandra/cql3/ColumnCondition.java
@@ -22,16 +22,19 @@
 
 import com.google.common.collect.Iterators;
 
+import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.Term.Terminal;
 import org.apache.cassandra.cql3.functions.Function;
+import org.apache.cassandra.cql3.statements.RequestValidations;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
-import static com.google.common.collect.Lists.newArrayList;
+import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
+import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
 
 /**
  * A CQL3 condition on the value of a column or collection element.  For example, "UPDATE .. IF a = 0".
@@ -43,19 +46,24 @@
     // For collection, when testing the equality of a specific element, null otherwise.
     private final Term collectionElement;
 
+    // For UDT, when testing the equality of a specific field, null otherwise.
+    private final FieldIdentifier field;
+
     private final Term value;  // a single value or a marker for a list of IN values
     private final List<Term> inValues;
 
     public final Operator operator;
 
-    private ColumnCondition(ColumnDefinition column, Term collectionElement, Term value, List<Term> inValues, Operator op)
+    private ColumnCondition(ColumnDefinition column, Term collectionElement, FieldIdentifier field, Term value, List<Term> inValues, Operator op)
     {
         this.column = column;
         this.collectionElement = collectionElement;
+        this.field = field;
         this.value = value;
         this.inValues = inValues;
         this.operator = op;
 
+        assert field == null || collectionElement == null;
         if (operator != Operator.IN)
             assert this.inValues == null;
     }
@@ -68,32 +76,47 @@
 
     public static ColumnCondition condition(ColumnDefinition column, Term value, Operator op)
     {
-        return new ColumnCondition(column, null, value, null, op);
+        return new ColumnCondition(column, null, null, value, null, op);
     }
 
     public static ColumnCondition condition(ColumnDefinition column, Term collectionElement, Term value, Operator op)
     {
-        return new ColumnCondition(column, collectionElement, value, null, op);
+        return new ColumnCondition(column, collectionElement, null, value, null, op);
+    }
+
+    public static ColumnCondition condition(ColumnDefinition column, FieldIdentifier udtField, Term value, Operator op)
+    {
+        return new ColumnCondition(column, null, udtField, value, null, op);
     }
 
     public static ColumnCondition inCondition(ColumnDefinition column, List<Term> inValues)
     {
-        return new ColumnCondition(column, null, null, inValues, Operator.IN);
+        return new ColumnCondition(column, null, null, null, inValues, Operator.IN);
     }
 
     public static ColumnCondition inCondition(ColumnDefinition column, Term collectionElement, List<Term> inValues)
     {
-        return new ColumnCondition(column, collectionElement, null, inValues, Operator.IN);
+        return new ColumnCondition(column, collectionElement, null, null, inValues, Operator.IN);
+    }
+
+    public static ColumnCondition inCondition(ColumnDefinition column, FieldIdentifier udtField, List<Term> inValues)
+    {
+        return new ColumnCondition(column, null, udtField, null, inValues, Operator.IN);
     }
 
     public static ColumnCondition inCondition(ColumnDefinition column, Term inMarker)
     {
-        return new ColumnCondition(column, null, inMarker, null, Operator.IN);
+        return new ColumnCondition(column, null, null, inMarker, null, Operator.IN);
     }
 
     public static ColumnCondition inCondition(ColumnDefinition column, Term collectionElement, Term inMarker)
     {
-        return new ColumnCondition(column, collectionElement, inMarker, null, Operator.IN);
+        return new ColumnCondition(column, collectionElement, null, inMarker, null, Operator.IN);
+    }
+
+    public static ColumnCondition inCondition(ColumnDefinition column, FieldIdentifier udtField, Term inMarker)
+    {
+        return new ColumnCondition(column, null, udtField, inMarker, null, Operator.IN);
     }
 
     public void addFunctionsTo(List<Function> functions)
@@ -135,11 +158,19 @@
         boolean isInCondition = operator == Operator.IN;
         if (column.type instanceof CollectionType)
         {
-            if (collectionElement == null)
-                return isInCondition ? new CollectionInBound(this, options) : new CollectionBound(this, options);
-            else
+            if (collectionElement != null)
                 return isInCondition ? new ElementAccessInBound(this, options) : new ElementAccessBound(this, options);
+            else
+                return isInCondition ? new CollectionInBound(this, options) : new CollectionBound(this, options);
         }
+        else if (column.type.isUDT())
+        {
+            if (field != null)
+                return isInCondition ? new UDTFieldAccessInBound(this, options) : new UDTFieldAccessBound(this, options);
+            else
+                return isInCondition ? new UDTInBound(this, options) : new UDTBound(this, options);
+        }
+
         return isInCondition ? new SimpleInBound(this, options) : new SimpleBound(this, options);
     }
 
@@ -220,6 +251,35 @@
         return complexData == null ? Collections.<Cell>emptyIterator() : complexData.iterator();
     }
 
+    private static boolean evaluateComparisonWithOperator(int comparison, Operator operator)
+    {
+        // called when comparison != 0
+        switch (operator)
+        {
+            case EQ:
+                return false;
+            case LT:
+            case LTE:
+                return comparison < 0;
+            case GT:
+            case GTE:
+                return comparison > 0;
+            case NEQ:
+                return true;
+            default:
+                throw new AssertionError();
+        }
+    }
+
+    private static ByteBuffer cellValueAtIndex(Iterator<Cell> iter, int index)
+    {
+        int adv = Iterators.advance(iter, index);
+        if (adv == index && iter.hasNext())
+            return iter.next().value();
+        else
+            return null;
+    }
+
     /**
      * A condition on a single non-collection column. This does not support IN operators (see SimpleInBound).
      */
@@ -230,7 +290,7 @@
         private SimpleBound(ColumnCondition condition, QueryOptions options) throws InvalidRequestException
         {
             super(condition.column, condition.operator);
-            assert !(column.type instanceof CollectionType) && condition.collectionElement == null;
+            assert !(column.type instanceof CollectionType) && condition.field == null;
             assert condition.operator != Operator.IN;
             this.value = condition.value.bindAndGet(options);
         }
@@ -251,7 +311,7 @@
         private SimpleInBound(ColumnCondition condition, QueryOptions options) throws InvalidRequestException
         {
             super(condition.column, condition.operator);
-            assert !(column.type instanceof CollectionType) && condition.collectionElement == null;
+            assert !(column.type instanceof CollectionType) && condition.field == null;
             assert condition.operator == Operator.IN;
             if (condition.inValues == null)
             {
@@ -329,7 +389,7 @@
             ListType listType = (ListType) column.type;
             if (column.type.isMultiCell())
             {
-                ByteBuffer columnValue = getListItem(getCells(row, column), getListIndex(collectionElement));
+                ByteBuffer columnValue = cellValueAtIndex(getCells(row, column), getListIndex(collectionElement));
                 return compareWithOperator(operator, ((ListType)column.type).getElementsType(), value, columnValue);
             }
             else
@@ -348,15 +408,6 @@
             return idx;
         }
 
-        static ByteBuffer getListItem(Iterator<Cell> iter, int index)
-        {
-            int adv = Iterators.advance(iter, index);
-            if (adv == index && iter.hasNext())
-                return iter.next().value();
-            else
-                return null;
-        }
-
         public ByteBuffer getCollectionElementValue()
         {
             return collectionElement;
@@ -399,71 +450,46 @@
             if (collectionElement == null)
                 throw new InvalidRequestException("Invalid null value for " + (column.type instanceof MapType ? "map" : "list") + " element access");
 
+            ByteBuffer cellValue;
+            AbstractType<?> valueType;
             if (column.type instanceof MapType)
             {
                 MapType mapType = (MapType) column.type;
-                AbstractType<?> valueType = mapType.getValuesType();
+                valueType = mapType.getValuesType();
                 if (column.type.isMultiCell())
                 {
-                    Cell item = getCell(row, column, CellPath.create(collectionElement));
-                    for (ByteBuffer value : inValues)
-                    {
-                        if (isSatisfiedByValue(value, item, valueType, Operator.EQ))
-                            return true;
-                    }
-                    return false;
+                    Cell cell = getCell(row, column, CellPath.create(collectionElement));
+                    cellValue = cell == null ? null : cell.value();
                 }
                 else
                 {
                     Cell cell = getCell(row, column);
-                    ByteBuffer mapElementValue = cell == null
-                                               ? null
-                                               : mapType.getSerializer().getSerializedValue(cell.value(), collectionElement, mapType.getKeysType());
-                    for (ByteBuffer value : inValues)
-                    {
-                        if (value == null)
-                        {
-                            if (mapElementValue == null)
-                                return true;
-                            continue;
-                        }
-                        if (valueType.compare(value, mapElementValue) == 0)
-                            return true;
-                    }
-                    return false;
+                    cellValue = cell == null
+                              ? null
+                              : mapType.getSerializer().getSerializedValue(cell.value(), collectionElement, mapType.getKeysType());
+                }
+            }
+            else // ListType
+            {
+                ListType listType = (ListType) column.type;
+                valueType = listType.getElementsType();
+                if (column.type.isMultiCell())
+                {
+                    cellValue = cellValueAtIndex(getCells(row, column), ElementAccessBound.getListIndex(collectionElement));
+                }
+                else
+                {
+                    Cell cell = getCell(row, column);
+                    cellValue = cell == null
+                              ? null
+                              : listType.getSerializer().getElement(cell.value(), ElementAccessBound.getListIndex(collectionElement));
                 }
             }
 
-            ListType listType = (ListType) column.type;
-            AbstractType<?> elementsType = listType.getElementsType();
-            if (column.type.isMultiCell())
+            for (ByteBuffer value : inValues)
             {
-                ByteBuffer columnValue = ElementAccessBound.getListItem(getCells(row, column), ElementAccessBound.getListIndex(collectionElement));
-
-                for (ByteBuffer value : inValues)
-                {
-                    if (compareWithOperator(Operator.EQ, elementsType, value, columnValue))
-                        return true;
-                }
-            }
-            else
-            {
-                Cell cell = getCell(row, column);
-                ByteBuffer listElementValue = cell == null
-                                            ? null
-                                            : listType.getSerializer().getElement(cell.value(), ElementAccessBound.getListIndex(collectionElement));
-
-                for (ByteBuffer value : inValues)
-                {
-                    if (value == null)
-                    {
-                        if (listElementValue == null)
-                            return true;
-                        continue;
-                    }
-                    if (elementsType.compare(value, listElementValue) == 0)
-                        return true;
-                }
+                if (compareWithOperator(Operator.EQ, valueType, value, cellValue))
+                    return true;
             }
             return false;
         }
@@ -521,11 +547,11 @@
             // make sure we use v3 serialization format for comparison
             ByteBuffer conditionValue;
             if (type.kind == CollectionType.Kind.LIST)
-                conditionValue = ((Lists.Value) value).get(Server.VERSION_3);
+                conditionValue = ((Lists.Value) value).get(ProtocolVersion.V3);
             else if (type.kind == CollectionType.Kind.SET)
-                conditionValue = ((Sets.Value) value).get(Server.VERSION_3);
+                conditionValue = ((Sets.Value) value).get(ProtocolVersion.V3);
             else
-                conditionValue = ((Maps.Value) value).get(Server.VERSION_3);
+                conditionValue = ((Maps.Value) value).get(ProtocolVersion.V3);
 
             return compareWithOperator(operator, type, conditionValue, cell.value());
         }
@@ -571,26 +597,6 @@
             return operator == Operator.EQ || operator == Operator.LTE || operator == Operator.GTE;
         }
 
-        private static boolean evaluateComparisonWithOperator(int comparison, Operator operator)
-        {
-            // called when comparison != 0
-            switch (operator)
-            {
-                case EQ:
-                    return false;
-                case LT:
-                case LTE:
-                    return comparison < 0;
-                case GT:
-                case GTE:
-                    return comparison > 0;
-                case NEQ:
-                    return true;
-                default:
-                    throw new AssertionError();
-            }
-        }
-
         static boolean listAppliesTo(ListType type, Iterator<Cell> iter, List<ByteBuffer> elements, Operator operator)
         {
             return setOrListAppliesTo(type.getElementsType(), iter, elements.iterator(), operator, false);
@@ -734,7 +740,7 @@
                         if (cell == null)
                             return true;
                     }
-                    else if (type.compare(value.get(Server.VERSION_3), cell.value()) == 0)
+                    else if (type.compare(value.get(ProtocolVersion.V3), cell.value()) == 0)
                     {
                         return true;
                     }
@@ -744,6 +750,202 @@
         }
     }
 
+    /** A condition on a UDT field. IN operators are not supported here, see UDTFieldAccessInBound. */
+    static class UDTFieldAccessBound extends Bound
+    {
+        public final FieldIdentifier field;
+        public final ByteBuffer value;
+
+        private UDTFieldAccessBound(ColumnCondition condition, QueryOptions options) throws InvalidRequestException
+        {
+            super(condition.column, condition.operator);
+            assert column.type.isUDT() && condition.field != null;
+            assert condition.operator != Operator.IN;
+            this.field = condition.field;
+            this.value = condition.value.bindAndGet(options);
+        }
+
+        public boolean appliesTo(Row row) throws InvalidRequestException
+        {
+            UserType userType = (UserType) column.type;
+            int fieldPosition = userType.fieldPosition(field);
+            assert fieldPosition >= 0;
+
+            ByteBuffer cellValue;
+            if (column.type.isMultiCell())
+            {
+                Cell cell = getCell(row, column, userType.cellPathForField(field));
+                cellValue = cell == null ? null : cell.value();
+            }
+            else
+            {
+                Cell cell = getCell(row, column);
+                cellValue = cell == null
+                          ? null
+                          : userType.split(cell.value())[fieldPosition];
+            }
+            return compareWithOperator(operator, userType.fieldType(fieldPosition), value, cellValue);
+        }
+    }
+
+    /** An IN condition on a UDT field.  For example: IF user.name IN ('a', 'b') */
+    static class UDTFieldAccessInBound extends Bound
+    {
+        public final FieldIdentifier field;
+        public final List<ByteBuffer> inValues;
+
+        private UDTFieldAccessInBound(ColumnCondition condition, QueryOptions options) throws InvalidRequestException
+        {
+            super(condition.column, condition.operator);
+            assert column.type.isUDT() && condition.field != null;
+            this.field = condition.field;
+
+            if (condition.inValues == null)
+                this.inValues = ((Lists.Value) condition.value.bind(options)).getElements();
+            else
+            {
+                this.inValues = new ArrayList<>(condition.inValues.size());
+                for (Term value : condition.inValues)
+                    this.inValues.add(value.bindAndGet(options));
+            }
+        }
+
+        public boolean appliesTo(Row row) throws InvalidRequestException
+        {
+            UserType userType = (UserType) column.type;
+            int fieldPosition = userType.fieldPosition(field);
+            assert fieldPosition >= 0;
+
+            ByteBuffer cellValue;
+            if (column.type.isMultiCell())
+            {
+                Cell cell = getCell(row, column, userType.cellPathForField(field));
+                cellValue = cell == null ? null : cell.value();
+            }
+            else
+            {
+                Cell cell = getCell(row, column);
+                cellValue = cell == null ? null : userType.split(getCell(row, column).value())[fieldPosition];
+            }
+
+            AbstractType<?> valueType = userType.fieldType(fieldPosition);
+            for (ByteBuffer value : inValues)
+            {
+                if (compareWithOperator(Operator.EQ, valueType, value, cellValue))
+                    return true;
+            }
+            return false;
+        }
+    }
+
+    /** A non-IN condition on an entire UDT.  For example: IF user = {name: 'joe', age: 42}). */
+    static class UDTBound extends Bound
+    {
+        private final ByteBuffer value;
+        private final ProtocolVersion protocolVersion;
+
+        private UDTBound(ColumnCondition condition, QueryOptions options) throws InvalidRequestException
+        {
+            super(condition.column, condition.operator);
+            assert column.type.isUDT() && condition.field == null;
+            assert condition.operator != Operator.IN;
+            protocolVersion = options.getProtocolVersion();
+            value = condition.value.bindAndGet(options);
+        }
+
+        public boolean appliesTo(Row row) throws InvalidRequestException
+        {
+            UserType userType = (UserType) column.type;
+            ByteBuffer rowValue;
+            if (userType.isMultiCell())
+            {
+                Iterator<Cell> iter = getCells(row, column);
+                rowValue = iter.hasNext() ? userType.serializeForNativeProtocol(iter, protocolVersion) : null;
+            }
+            else
+            {
+                Cell cell = getCell(row, column);
+                rowValue = cell == null ? null : cell.value();
+            }
+
+            if (value == null)
+            {
+                if (operator == Operator.EQ)
+                    return rowValue == null;
+                else if (operator == Operator.NEQ)
+                    return rowValue != null;
+                else
+                    throw new InvalidRequestException(String.format("Invalid comparison with null for operator \"%s\"", operator));
+            }
+
+            return compareWithOperator(operator, userType, value, rowValue);
+        }
+    }
+
+    /** An IN condition on an entire UDT.  For example: IF user IN ({name: 'joe', age: 42}, {name: 'bob', age: 23}). */
+    public static class UDTInBound extends Bound
+    {
+        private final List<ByteBuffer> inValues;
+        private final ProtocolVersion protocolVersion;
+
+        private UDTInBound(ColumnCondition condition, QueryOptions options) throws InvalidRequestException
+        {
+            super(condition.column, condition.operator);
+            assert column.type.isUDT() && condition.field == null;
+            assert condition.operator == Operator.IN;
+            protocolVersion = options.getProtocolVersion();
+            inValues = new ArrayList<>();
+            if (condition.inValues == null)
+            {
+                Lists.Marker inValuesMarker = (Lists.Marker) condition.value;
+                Terminal terminal = inValuesMarker.bind(options);
+                if (terminal == null)
+                    throw new InvalidRequestException("Invalid null list in IN condition");
+
+                if (terminal == Constants.UNSET_VALUE)
+                    throw new InvalidRequestException("Invalid 'unset' value in condition");
+
+                for (ByteBuffer buffer : ((Lists.Value)terminal).elements)
+                    this.inValues.add(buffer);
+            }
+            else
+            {
+                for (Term value : condition.inValues)
+                    this.inValues.add(value.bindAndGet(options));
+            }
+        }
+
+        public boolean appliesTo(Row row) throws InvalidRequestException
+        {
+            UserType userType = (UserType) column.type;
+            ByteBuffer rowValue;
+            if (userType.isMultiCell())
+            {
+                Iterator<Cell> cells = getCells(row, column);
+                rowValue = cells.hasNext() ? userType.serializeForNativeProtocol(cells, protocolVersion) : null;
+            }
+            else
+            {
+                Cell cell = getCell(row, column);
+                rowValue = cell == null ? null : cell.value();
+            }
+
+            for (ByteBuffer value : inValues)
+            {
+                if (value == null || rowValue == null)
+                {
+                    if (value == rowValue) // both null
+                        return true;
+                }
+                else if (userType.compare(value, rowValue) == 0)
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
     public static class Raw
     {
         private final Term.Raw value;
@@ -753,59 +955,143 @@
         // Can be null, only used with the syntax "IF m[e] = ..." (in which case it's 'e')
         private final Term.Raw collectionElement;
 
+        // Can be null, only used with the syntax "IF udt.field = ..." (in which case it's 'field')
+        private final FieldIdentifier udtField;
+
         private final Operator operator;
 
-        private Raw(Term.Raw value, List<Term.Raw> inValues, AbstractMarker.INRaw inMarker, Term.Raw collectionElement, Operator op)
+        private Raw(Term.Raw value, List<Term.Raw> inValues, AbstractMarker.INRaw inMarker, Term.Raw collectionElement,
+                    FieldIdentifier udtField, Operator op)
         {
             this.value = value;
             this.inValues = inValues;
             this.inMarker = inMarker;
             this.collectionElement = collectionElement;
+            this.udtField = udtField;
             this.operator = op;
         }
 
         /** A condition on a column. For example: "IF col = 'foo'" */
         public static Raw simpleCondition(Term.Raw value, Operator op)
         {
-            return new Raw(value, null, null, null, op);
+            return new Raw(value, null, null, null, null, op);
         }
 
         /** An IN condition on a column. For example: "IF col IN ('foo', 'bar', ...)" */
         public static Raw simpleInCondition(List<Term.Raw> inValues)
         {
-            return new Raw(null, inValues, null, null, Operator.IN);
+            return new Raw(null, inValues, null, null, null, Operator.IN);
         }
 
         /** An IN condition on a column with a single marker. For example: "IF col IN ?" */
         public static Raw simpleInCondition(AbstractMarker.INRaw inMarker)
         {
-            return new Raw(null, null, inMarker, null, Operator.IN);
+            return new Raw(null, null, inMarker, null, null, Operator.IN);
         }
 
         /** A condition on a collection element. For example: "IF col['key'] = 'foo'" */
         public static Raw collectionCondition(Term.Raw value, Term.Raw collectionElement, Operator op)
         {
-            return new Raw(value, null, null, collectionElement, op);
+            return new Raw(value, null, null, collectionElement, null, op);
         }
 
         /** An IN condition on a collection element. For example: "IF col['key'] IN ('foo', 'bar', ...)" */
         public static Raw collectionInCondition(Term.Raw collectionElement, List<Term.Raw> inValues)
         {
-            return new Raw(null, inValues, null, collectionElement, Operator.IN);
+            return new Raw(null, inValues, null, collectionElement, null, Operator.IN);
         }
 
         /** An IN condition on a collection element with a single marker. For example: "IF col['key'] IN ?" */
         public static Raw collectionInCondition(Term.Raw collectionElement, AbstractMarker.INRaw inMarker)
         {
-            return new Raw(null, null, inMarker, collectionElement, Operator.IN);
+            return new Raw(null, null, inMarker, collectionElement, null, Operator.IN);
         }
 
-        public ColumnCondition prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException
+        /** A condition on a UDT field. For example: "IF col.field = 'foo'" */
+        public static Raw udtFieldCondition(Term.Raw value, FieldIdentifier udtField, Operator op)
+        {
+            return new Raw(value, null, null, null, udtField, op);
+        }
+
+        /** An IN condition on a collection element. For example: "IF col.field IN ('foo', 'bar', ...)" */
+        public static Raw udtFieldInCondition(FieldIdentifier udtField, List<Term.Raw> inValues)
+        {
+            return new Raw(null, inValues, null, null, udtField, Operator.IN);
+        }
+
+        /** An IN condition on a collection element with a single marker. For example: "IF col.field IN ?" */
+        public static Raw udtFieldInCondition(FieldIdentifier udtField, AbstractMarker.INRaw inMarker)
+        {
+            return new Raw(null, null, inMarker, null, udtField, Operator.IN);
+        }
+
+        public ColumnCondition prepare(String keyspace, ColumnDefinition receiver, CFMetaData cfm) throws InvalidRequestException
         {
             if (receiver.type instanceof CounterColumnType)
                 throw new InvalidRequestException("Conditions on counters are not supported");
 
-            if (collectionElement == null)
+            if (collectionElement != null)
+            {
+                if (!(receiver.type.isCollection()))
+                    throw new InvalidRequestException(String.format("Invalid element access syntax for non-collection column %s", receiver.name));
+
+                ColumnSpecification elementSpec, valueSpec;
+                switch ((((CollectionType) receiver.type).kind))
+                {
+                    case LIST:
+                        elementSpec = Lists.indexSpecOf(receiver);
+                        valueSpec = Lists.valueSpecOf(receiver);
+                        break;
+                    case MAP:
+                        elementSpec = Maps.keySpecOf(receiver);
+                        valueSpec = Maps.valueSpecOf(receiver);
+                        break;
+                    case SET:
+                        throw new InvalidRequestException(String.format("Invalid element access syntax for set column %s", receiver.name));
+                    default:
+                        throw new AssertionError();
+                }
+
+                if (operator == Operator.IN)
+                {
+                    if (inValues == null)
+                        return ColumnCondition.inCondition(receiver, collectionElement.prepare(keyspace, elementSpec), inMarker.prepare(keyspace, valueSpec));
+                    List<Term> terms = new ArrayList<>(inValues.size());
+                    for (Term.Raw value : inValues)
+                        terms.add(value.prepare(keyspace, valueSpec));
+                    return ColumnCondition.inCondition(receiver, collectionElement.prepare(keyspace, elementSpec), terms);
+                }
+                else
+                {
+                    validateOperationOnDurations(valueSpec.type);
+                    return ColumnCondition.condition(receiver, collectionElement.prepare(keyspace, elementSpec), value.prepare(keyspace, valueSpec), operator);
+                }
+            }
+            else if (udtField != null)
+            {
+                UserType userType = (UserType) receiver.type;
+                int fieldPosition = userType.fieldPosition(udtField);
+                if (fieldPosition == -1)
+                    throw new InvalidRequestException(String.format("Unknown field %s for column %s", udtField, receiver.name));
+
+                ColumnSpecification fieldReceiver = UserTypes.fieldSpecOf(receiver, fieldPosition);
+                if (operator == Operator.IN)
+                {
+                    if (inValues == null)
+                        return ColumnCondition.inCondition(receiver, udtField, inMarker.prepare(keyspace, fieldReceiver));
+
+                    List<Term> terms = new ArrayList<>(inValues.size());
+                    for (Term.Raw value : inValues)
+                        terms.add(value.prepare(keyspace, fieldReceiver));
+                    return ColumnCondition.inCondition(receiver, udtField, terms);
+                }
+                else
+                {
+                    validateOperationOnDurations(fieldReceiver.type);
+                    return ColumnCondition.condition(receiver, udtField, value.prepare(keyspace, fieldReceiver), operator);
+                }
+            }
+            else
             {
                 if (operator == Operator.IN)
                 {
@@ -818,41 +1104,20 @@
                 }
                 else
                 {
+                    validateOperationOnDurations(receiver.type);
                     return ColumnCondition.condition(receiver, value.prepare(keyspace, receiver), operator);
                 }
             }
+        }
 
-            if (!(receiver.type.isCollection()))
-                throw new InvalidRequestException(String.format("Invalid element access syntax for non-collection column %s", receiver.name));
-
-            ColumnSpecification elementSpec, valueSpec;
-            switch ((((CollectionType)receiver.type).kind))
+        private void validateOperationOnDurations(AbstractType<?> type)
+        {
+            if (type.referencesDuration() && operator.isSlice())
             {
-                case LIST:
-                    elementSpec = Lists.indexSpecOf(receiver);
-                    valueSpec = Lists.valueSpecOf(receiver);
-                    break;
-                case MAP:
-                    elementSpec = Maps.keySpecOf(receiver);
-                    valueSpec = Maps.valueSpecOf(receiver);
-                    break;
-                case SET:
-                    throw new InvalidRequestException(String.format("Invalid element access syntax for set column %s", receiver.name));
-                default:
-                    throw new AssertionError();
-            }
-            if (operator == Operator.IN)
-            {
-                if (inValues == null)
-                    return ColumnCondition.inCondition(receiver, collectionElement.prepare(keyspace, elementSpec), inMarker.prepare(keyspace, valueSpec));
-                List<Term> terms = new ArrayList<>(inValues.size());
-                for (Term.Raw value : inValues)
-                    terms.add(value.prepare(keyspace, valueSpec));
-                return ColumnCondition.inCondition(receiver, collectionElement.prepare(keyspace, elementSpec), terms);
-            }
-            else
-            {
-                return ColumnCondition.condition(receiver, collectionElement.prepare(keyspace, elementSpec), value.prepare(keyspace, valueSpec), operator);
+                checkFalse(type.isCollection(), "Slice conditions are not supported on collections containing durations");
+                checkFalse(type.isTuple(), "Slice conditions are not supported on tuples containing durations");
+                checkFalse(type.isUDT(), "Slice conditions are not supported on UDTs containing durations");
+                throw invalidRequest("Slice conditions are not supported on durations", operator);
             }
         }
     }
diff --git a/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java b/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java
index 5d4e992..ecf44e8 100644
--- a/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java
+++ b/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java
@@ -18,22 +18,16 @@
 package org.apache.cassandra.cql3;
 
 import java.nio.ByteBuffer;
-import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.ConcurrentMap;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.MapMaker;
 
 import org.apache.cassandra.cache.IMeasurableMemory;
-import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.cql3.selection.Selectable;
-import org.apache.cassandra.cql3.selection.Selector;
-import org.apache.cassandra.cql3.selection.SimpleSelector;
 import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.ObjectSizes;
@@ -43,10 +37,11 @@
  * Represents an identifer for a CQL column definition.
  * TODO : should support light-weight mode without text representation for when not interned
  */
-public class ColumnIdentifier extends Selectable implements IMeasurableMemory, Comparable<ColumnIdentifier>
+public class ColumnIdentifier implements IMeasurableMemory, Comparable<ColumnIdentifier>
 {
     private static final Pattern PATTERN_DOUBLE_QUOTE = Pattern.compile("\"", Pattern.LITERAL);
-
+    private static final String ESCAPED_DOUBLE_QUOTE = Matcher.quoteReplacement("\"\"");
+    
     public final ByteBuffer bytes;
     private final String text;
     /**
@@ -194,7 +189,7 @@
 
     /**
      * Returns a string representation of the identifier that is safe to use directly in CQL queries.
-     * In necessary, the string will be double-quoted, and any quotes inside the string will be escaped.
+     * If necessary, the string will be double-quoted, and any quotes inside the string will be escaped.
      */
     public String toCQLString()
     {
@@ -220,15 +215,6 @@
         return interned ? this : new ColumnIdentifier(allocator.clone(bytes), text, false);
     }
 
-    public Selector.Factory newSelectorFactory(CFMetaData cfm, List<ColumnDefinition> defs) throws InvalidRequestException
-    {
-        ColumnDefinition def = cfm.getColumnDefinitionForCQL(this);
-        if (def == null)
-            throw new InvalidRequestException(String.format("Undefined name %s in selection clause", this));
-
-        return SimpleSelector.newFactory(def, addAndGetIndex(def, defs));
-    }
-
     public int compareTo(ColumnIdentifier that)
     {
         int c = Long.compare(this.prefixComparison, that.prefixComparison);
@@ -239,139 +225,11 @@
         return ByteBufferUtil.compareUnsigned(this.bytes, that.bytes);
     }
 
-    /**
-     * Because Thrift-created tables may have a non-text comparator, we cannot determine the proper 'key' until
-     * we know the comparator. ColumnIdentifier.Raw is a placeholder that can be converted to a real ColumnIdentifier
-     * once the comparator is known with prepare(). This should only be used with identifiers that are actual
-     * column names. See CASSANDRA-8178 for more background.
-     */
-    public static interface Raw extends Selectable.Raw
-    {
-
-        public ColumnIdentifier prepare(CFMetaData cfm);
-
-        /**
-         * Returns a string representation of the identifier that is safe to use directly in CQL queries.
-         * In necessary, the string will be double-quoted, and any quotes inside the string will be escaped.
-         */
-        public String toCQLString();
-    }
-
-    public static class Literal implements Raw
-    {
-        private final String rawText;
-        private final String text;
-
-        public Literal(String rawText, boolean keepCase)
-        {
-            this.rawText = rawText;
-            this.text =  keepCase ? rawText : rawText.toLowerCase(Locale.US);
-        }
-
-        public ColumnIdentifier prepare(CFMetaData cfm)
-        {
-            if (!cfm.isStaticCompactTable())
-                return getInterned(text, true);
-
-            AbstractType<?> thriftColumnNameType = cfm.thriftColumnNameType();
-            if (thriftColumnNameType instanceof UTF8Type)
-                return getInterned(text, true);
-
-            // We have a Thrift-created table with a non-text comparator. Check if we have a match column, otherwise assume we should use
-            // thriftColumnNameType
-            ByteBuffer bufferName = ByteBufferUtil.bytes(text);
-            for (ColumnDefinition def : cfm.allColumns())
-            {
-                if (def.name.bytes.equals(bufferName))
-                    return def.name;
-            }
-            return getInterned(thriftColumnNameType, thriftColumnNameType.fromString(rawText), text);
-        }
-
-        public boolean processesSelection()
-        {
-            return false;
-        }
-
-        @Override
-        public final int hashCode()
-        {
-            return text.hashCode();
-        }
-
-        @Override
-        public final boolean equals(Object o)
-        {
-            if(!(o instanceof Literal))
-                return false;
-
-            Literal that = (Literal) o;
-            return text.equals(that.text);
-        }
-
-        @Override
-        public String toString()
-        {
-            return text;
-        }
-
-        public String toCQLString()
-        {
-            return maybeQuote(text);
-        }
-    }
-
-    public static class ColumnIdentifierValue implements Raw
-    {
-        private final ColumnIdentifier identifier;
-
-        public ColumnIdentifierValue(ColumnIdentifier identifier)
-        {
-            this.identifier = identifier;
-        }
-
-        public ColumnIdentifier prepare(CFMetaData cfm)
-        {
-            return identifier;
-        }
-
-        public boolean processesSelection()
-        {
-            return false;
-        }
-
-        @Override
-        public final int hashCode()
-        {
-            return identifier.hashCode();
-        }
-
-        @Override
-        public final boolean equals(Object o)
-        {
-            if(!(o instanceof ColumnIdentifierValue))
-                return false;
-            ColumnIdentifierValue that = (ColumnIdentifierValue) o;
-            return identifier.equals(that.identifier);
-        }
-
-        @Override
-        public String toString()
-        {
-            return identifier.toString();
-        }
-
-        public String toCQLString()
-        {
-            return maybeQuote(identifier.text);
-        }
-    }
-
+    @VisibleForTesting
     public static String maybeQuote(String text)
     {
         if (UNQUOTED_IDENTIFIER.matcher(text).matches() && !ReservedKeywords.isReserved(text))
             return text;
-
-        return '"' + PATTERN_DOUBLE_QUOTE.matcher(text).replaceAll(Matcher.quoteReplacement("\"\"")) + '"';
+        return '"' + PATTERN_DOUBLE_QUOTE.matcher(text).replaceAll(ESCAPED_DOUBLE_QUOTE) + '"';
     }
 }
diff --git a/src/java/org/apache/cassandra/cql3/ColumnSpecification.java b/src/java/org/apache/cassandra/cql3/ColumnSpecification.java
index e64f5f9..8cf869b 100644
--- a/src/java/org/apache/cassandra/cql3/ColumnSpecification.java
+++ b/src/java/org/apache/cassandra/cql3/ColumnSpecification.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.cql3;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 
 import org.apache.cassandra.db.marshal.AbstractType;
@@ -96,9 +97,9 @@
     @Override
     public String toString()
     {
-        return Objects.toStringHelper(this)
-                      .add("name", name)
-                      .add("type", type)
-                      .toString();
+        return MoreObjects.toStringHelper(this)
+                          .add("name", name)
+                          .add("type", type)
+                          .toString();
     }
 }
diff --git a/src/java/org/apache/cassandra/cql3/Constants.java b/src/java/org/apache/cassandra/cql3/Constants.java
index f37d900..d650c44 100644
--- a/src/java/org/apache/cassandra/cql3/Constants.java
+++ b/src/java/org/apache/cassandra/cql3/Constants.java
@@ -23,13 +23,10 @@
 import org.slf4j.LoggerFactory;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.db.marshal.BytesType;
-import org.apache.cassandra.db.marshal.CounterColumnType;
-import org.apache.cassandra.db.marshal.LongType;
-import org.apache.cassandra.db.marshal.ReversedType;
+import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 /**
@@ -41,9 +38,35 @@
 
     public enum Type
     {
-        STRING, INTEGER, UUID, FLOAT, DATE, TIME, BOOLEAN, HEX;
+        STRING, INTEGER, UUID, FLOAT, BOOLEAN, HEX, DURATION;
     }
 
+    private static class UnsetLiteral extends Term.Raw
+    {
+        public Term prepare(String keyspace, ColumnSpecification receiver) throws InvalidRequestException
+        {
+            return UNSET_VALUE;
+        }
+
+        public AssignmentTestable.TestResult testAssignment(String keyspace, ColumnSpecification receiver)
+        {
+            return AssignmentTestable.TestResult.NOT_ASSIGNABLE;
+        }
+
+        public String getText()
+        {
+            return "";
+        }
+
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            return null;
+        }
+    }
+
+    // We don't have "unset" literal in the syntax, but it's used implicitely for JSON "DEFAULT UNSET" option
+    public static final UnsetLiteral UNSET_LITERAL = new UnsetLiteral();
+
     public static final Value UNSET_VALUE = new Value(ByteBufferUtil.UNSET_BYTE_BUFFER);
 
     private static class NullLiteral extends Term.Raw
@@ -67,6 +90,11 @@
         {
             return "NULL";
         }
+
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            return null;
+        }
     }
 
     public static final NullLiteral NULL_LITERAL = new NullLiteral();
@@ -129,6 +157,11 @@
             return new Literal(Type.HEX, text);
         }
 
+        public static Literal duration(String text)
+        {
+            return new Literal(Type.DURATION, text);
+        }
+
         public Value prepare(String keyspace, ColumnSpecification receiver) throws InvalidRequestException
         {
             if (!testAssignment(keyspace, receiver).isAssignable())
@@ -159,10 +192,11 @@
             }
         }
 
+        @Override
         public AssignmentTestable.TestResult testAssignment(String keyspace, ColumnSpecification receiver)
         {
             CQL3Type receiverType = receiver.type.asCQL3Type();
-            if (receiverType.isCollection())
+            if (receiverType.isCollection() || receiverType.isUDT())
                 return AssignmentTestable.TestResult.NOT_ASSIGNABLE;
 
             if (!(receiverType instanceof CQL3Type.Native))
@@ -193,6 +227,7 @@
                         case DATE:
                         case DECIMAL:
                         case DOUBLE:
+                        case DURATION:
                         case FLOAT:
                         case INT:
                         case SMALLINT:
@@ -234,10 +269,27 @@
                             return AssignmentTestable.TestResult.WEAKLY_ASSIGNABLE;
                     }
                     break;
+                case DURATION:
+                    switch (nt)
+                    {
+                        case DURATION:
+                            return AssignmentTestable.TestResult.WEAKLY_ASSIGNABLE;
+                    }
+                    break;
             }
             return AssignmentTestable.TestResult.NOT_ASSIGNABLE;
         }
 
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            // Most constant are valid for more than one type (the extreme example being integer constants, which can
+            // be use for any numerical type, including date, time, ...) so they don't have an exact type. And in fact,
+            // for good or bad, any literal is valid for custom types, so we can never claim an exact type.
+            // But really, the reason it's fine to return null here is that getExactTypeIfKnown is only used to
+            // implement testAssignment() in Selectable and that method is overriden above.
+            return null;
+        }
+
         public String getRawText()
         {
             return text;
@@ -261,7 +313,7 @@
             this.bytes = bytes;
         }
 
-        public ByteBuffer get(int protocolVersion)
+        public ByteBuffer get(ProtocolVersion protocolVersion)
         {
             return bytes;
         }
diff --git a/src/java/org/apache/cassandra/cql3/Cql.g b/src/java/org/apache/cassandra/cql3/Cql.g
deleted file mode 100644
index 0234327..0000000
--- a/src/java/org/apache/cassandra/cql3/Cql.g
+++ /dev/null
@@ -1,1908 +0,0 @@
-/*
- * 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.
- */
-
-grammar Cql;
-
-options {
-    language = Java;
-}
-
-@header {
-    package org.apache.cassandra.cql3;
-
-    import java.util.ArrayList;
-    import java.util.Arrays;
-    import java.util.Collections;
-    import java.util.EnumSet;
-    import java.util.HashSet;
-    import java.util.HashMap;
-    import java.util.LinkedHashMap;
-    import java.util.List;
-    import java.util.Map;
-    import java.util.Set;
-
-    import org.apache.cassandra.auth.*;
-    import org.apache.cassandra.cql3.*;
-    import org.apache.cassandra.cql3.restrictions.CustomIndexExpression;
-    import org.apache.cassandra.cql3.statements.*;
-    import org.apache.cassandra.cql3.selection.*;
-    import org.apache.cassandra.cql3.functions.*;
-    import org.apache.cassandra.db.marshal.CollectionType;
-    import org.apache.cassandra.exceptions.ConfigurationException;
-    import org.apache.cassandra.exceptions.InvalidRequestException;
-    import org.apache.cassandra.exceptions.SyntaxException;
-    import org.apache.cassandra.utils.Pair;
-}
-
-@members {
-    private final List<ErrorListener> listeners = new ArrayList<ErrorListener>();
-    private final List<ColumnIdentifier> bindVariables = new ArrayList<ColumnIdentifier>();
-
-    public static final Set<String> reservedTypeNames = new HashSet<String>()
-    {{
-        add("byte");
-        add("complex");
-        add("enum");
-        add("date");
-        add("interval");
-        add("macaddr");
-        add("bitstring");
-    }};
-
-    public AbstractMarker.Raw newBindVariables(ColumnIdentifier name)
-    {
-        AbstractMarker.Raw marker = new AbstractMarker.Raw(bindVariables.size());
-        bindVariables.add(name);
-        return marker;
-    }
-
-    public AbstractMarker.INRaw newINBindVariables(ColumnIdentifier name)
-    {
-        AbstractMarker.INRaw marker = new AbstractMarker.INRaw(bindVariables.size());
-        bindVariables.add(name);
-        return marker;
-    }
-
-    public Tuples.Raw newTupleBindVariables(ColumnIdentifier name)
-    {
-        Tuples.Raw marker = new Tuples.Raw(bindVariables.size());
-        bindVariables.add(name);
-        return marker;
-    }
-
-    public Tuples.INRaw newTupleINBindVariables(ColumnIdentifier name)
-    {
-        Tuples.INRaw marker = new Tuples.INRaw(bindVariables.size());
-        bindVariables.add(name);
-        return marker;
-    }
-
-    public Json.Marker newJsonBindVariables(ColumnIdentifier name)
-    {
-        Json.Marker marker = new Json.Marker(bindVariables.size());
-        bindVariables.add(name);
-        return marker;
-    }
-
-    public void addErrorListener(ErrorListener listener)
-    {
-        this.listeners.add(listener);
-    }
-
-    public void removeErrorListener(ErrorListener listener)
-    {
-        this.listeners.remove(listener);
-    }
-
-    public void displayRecognitionError(String[] tokenNames, RecognitionException e)
-    {
-        for (int i = 0, m = listeners.size(); i < m; i++)
-            listeners.get(i).syntaxError(this, tokenNames, e);
-    }
-
-    private void addRecognitionError(String msg)
-    {
-        for (int i = 0, m = listeners.size(); i < m; i++)
-            listeners.get(i).syntaxError(this, msg);
-    }
-
-    public Map<String, String> convertPropertyMap(Maps.Literal map)
-    {
-        if (map == null || map.entries == null || map.entries.isEmpty())
-            return Collections.<String, String>emptyMap();
-
-        Map<String, String> res = new HashMap<String, String>(map.entries.size());
-
-        for (Pair<Term.Raw, Term.Raw> entry : map.entries)
-        {
-            // Because the parser tries to be smart and recover on error (to
-            // allow displaying more than one error I suppose), we have null
-            // entries in there. Just skip those, a proper error will be thrown in the end.
-            if (entry.left == null || entry.right == null)
-                break;
-
-            if (!(entry.left instanceof Constants.Literal))
-            {
-                String msg = "Invalid property name: " + entry.left;
-                if (entry.left instanceof AbstractMarker.Raw)
-                    msg += " (bind variables are not supported in DDL queries)";
-                addRecognitionError(msg);
-                break;
-            }
-            if (!(entry.right instanceof Constants.Literal))
-            {
-                String msg = "Invalid property value: " + entry.right + " for property: " + entry.left;
-                if (entry.right instanceof AbstractMarker.Raw)
-                    msg += " (bind variables are not supported in DDL queries)";
-                addRecognitionError(msg);
-                break;
-            }
-
-            res.put(((Constants.Literal)entry.left).getRawText(), ((Constants.Literal)entry.right).getRawText());
-        }
-
-        return res;
-    }
-
-    public void addRawUpdate(List<Pair<ColumnIdentifier.Raw, Operation.RawUpdate>> operations, ColumnIdentifier.Raw key, Operation.RawUpdate update)
-    {
-        for (Pair<ColumnIdentifier.Raw, Operation.RawUpdate> p : operations)
-        {
-            if (p.left.equals(key) && !p.right.isCompatibleWith(update))
-                addRecognitionError("Multiple incompatible setting of column " + key);
-        }
-        operations.add(Pair.create(key, update));
-    }
-
-    public Set<Permission> filterPermissions(Set<Permission> permissions, IResource resource)
-    {
-        if (resource == null)
-            return Collections.emptySet();
-        Set<Permission> filtered = new HashSet<>(permissions);
-        filtered.retainAll(resource.applicablePermissions());
-        if (filtered.isEmpty())
-            addRecognitionError("Resource type " + resource.getClass().getSimpleName() +
-                                    " does not support any of the requested permissions");
-
-        return filtered;
-    }
-}
-
-@lexer::header {
-    package org.apache.cassandra.cql3;
-
-    import org.apache.cassandra.exceptions.SyntaxException;
-}
-
-@lexer::members {
-    List<Token> tokens = new ArrayList<Token>();
-
-    public void emit(Token token)
-    {
-        state.token = token;
-        tokens.add(token);
-    }
-
-    public Token nextToken()
-    {
-        super.nextToken();
-        if (tokens.size() == 0)
-            return new CommonToken(Token.EOF);
-        return tokens.remove(0);
-    }
-
-    private final List<ErrorListener> listeners = new ArrayList<ErrorListener>();
-
-    public void addErrorListener(ErrorListener listener)
-    {
-        this.listeners.add(listener);
-    }
-
-    public void removeErrorListener(ErrorListener listener)
-    {
-        this.listeners.remove(listener);
-    }
-
-    public void displayRecognitionError(String[] tokenNames, RecognitionException e)
-    {
-        for (int i = 0, m = listeners.size(); i < m; i++)
-            listeners.get(i).syntaxError(this, tokenNames, e);
-    }
-}
-
-/** STATEMENTS **/
-
-query returns [ParsedStatement stmnt]
-    : st=cqlStatement (';')* EOF { $stmnt = st; }
-    ;
-
-cqlStatement returns [ParsedStatement stmt]
-    @after{ if (stmt != null) stmt.setBoundVariables(bindVariables); }
-    : st1= selectStatement                 { $stmt = st1; }
-    | st2= insertStatement                 { $stmt = st2; }
-    | st3= updateStatement                 { $stmt = st3; }
-    | st4= batchStatement                  { $stmt = st4; }
-    | st5= deleteStatement                 { $stmt = st5; }
-    | st6= useStatement                    { $stmt = st6; }
-    | st7= truncateStatement               { $stmt = st7; }
-    | st8= createKeyspaceStatement         { $stmt = st8; }
-    | st9= createTableStatement            { $stmt = st9; }
-    | st10=createIndexStatement            { $stmt = st10; }
-    | st11=dropKeyspaceStatement           { $stmt = st11; }
-    | st12=dropTableStatement              { $stmt = st12; }
-    | st13=dropIndexStatement              { $stmt = st13; }
-    | st14=alterTableStatement             { $stmt = st14; }
-    | st15=alterKeyspaceStatement          { $stmt = st15; }
-    | st16=grantPermissionsStatement       { $stmt = st16; }
-    | st17=revokePermissionsStatement      { $stmt = st17; }
-    | st18=listPermissionsStatement        { $stmt = st18; }
-    | st19=createUserStatement             { $stmt = st19; }
-    | st20=alterUserStatement              { $stmt = st20; }
-    | st21=dropUserStatement               { $stmt = st21; }
-    | st22=listUsersStatement              { $stmt = st22; }
-    | st23=createTriggerStatement          { $stmt = st23; }
-    | st24=dropTriggerStatement            { $stmt = st24; }
-    | st25=createTypeStatement             { $stmt = st25; }
-    | st26=alterTypeStatement              { $stmt = st26; }
-    | st27=dropTypeStatement               { $stmt = st27; }
-    | st28=createFunctionStatement         { $stmt = st28; }
-    | st29=dropFunctionStatement           { $stmt = st29; }
-    | st30=createAggregateStatement        { $stmt = st30; }
-    | st31=dropAggregateStatement          { $stmt = st31; }
-    | st32=createRoleStatement             { $stmt = st32; }
-    | st33=alterRoleStatement              { $stmt = st33; }
-    | st34=dropRoleStatement               { $stmt = st34; }
-    | st35=listRolesStatement              { $stmt = st35; }
-    | st36=grantRoleStatement              { $stmt = st36; }
-    | st37=revokeRoleStatement             { $stmt = st37; }
-    | st38=createMaterializedViewStatement { $stmt = st38; }
-    | st39=dropMaterializedViewStatement   { $stmt = st39; }
-    | st40=alterMaterializedViewStatement  { $stmt = st40; }
-    ;
-
-/*
- * USE <KEYSPACE>;
- */
-useStatement returns [UseStatement stmt]
-    : K_USE ks=keyspaceName { $stmt = new UseStatement(ks); }
-    ;
-
-/**
- * SELECT <expression>
- * FROM <CF>
- * WHERE KEY = "key1" AND COL > 1 AND COL < 100
- * LIMIT <NUMBER>;
- */
-selectStatement returns [SelectStatement.RawStatement expr]
-    @init {
-        boolean isDistinct = false;
-        Term.Raw limit = null;
-        Map<ColumnIdentifier.Raw, Boolean> orderings = new LinkedHashMap<ColumnIdentifier.Raw, Boolean>();
-        boolean allowFiltering = false;
-        boolean isJson = false;
-    }
-    : K_SELECT 
-      ( K_JSON { isJson = true; } )?
-      ( ( K_DISTINCT { isDistinct = true; } )? sclause=selectClause )
-      K_FROM cf=columnFamilyName
-      ( K_WHERE wclause=whereClause )?
-      ( K_ORDER K_BY orderByClause[orderings] ( ',' orderByClause[orderings] )* )?
-      ( K_LIMIT rows=intValue { limit = rows; } )?
-      ( K_ALLOW K_FILTERING  { allowFiltering = true; } )?
-      {
-          SelectStatement.Parameters params = new SelectStatement.Parameters(orderings,
-                                                                             isDistinct,
-                                                                             allowFiltering,
-                                                                             isJson);
-          WhereClause where = wclause == null ? WhereClause.empty() : wclause.build();
-          $expr = new SelectStatement.RawStatement(cf, params, sclause, where, limit);
-      }
-    ;
-
-selectClause returns [List<RawSelector> expr]
-    : t1=selector { $expr = new ArrayList<RawSelector>(); $expr.add(t1); } (',' tN=selector { $expr.add(tN); })*
-    | '\*' { $expr = Collections.<RawSelector>emptyList();}
-    ;
-
-selector returns [RawSelector s]
-    @init{ ColumnIdentifier alias = null; }
-    : us=unaliasedSelector (K_AS c=noncol_ident { alias = c; })? { $s = new RawSelector(us, alias); }
-    ;
-
-unaliasedSelector returns [Selectable.Raw s]
-    @init { Selectable.Raw tmp = null; }
-    :  ( c=cident                                  { tmp = c; }
-       | K_COUNT '(' countArgument ')'             { tmp = new Selectable.WithFunction.Raw(FunctionName.nativeFunction("countRows"), Collections.<Selectable.Raw>emptyList());}
-       | K_WRITETIME '(' c=cident ')'              { tmp = new Selectable.WritetimeOrTTL.Raw(c, true); }
-       | K_TTL       '(' c=cident ')'              { tmp = new Selectable.WritetimeOrTTL.Raw(c, false); }
-       | f=functionName args=selectionFunctionArgs { tmp = new Selectable.WithFunction.Raw(f, args); }
-       ) ( '.' fi=cident { tmp = new Selectable.WithFieldSelection.Raw(tmp, fi); } )* { $s = tmp; }
-    ;
-
-selectionFunctionArgs returns [List<Selectable.Raw> a]
-    : '(' ')' { $a = Collections.emptyList(); }
-    | '(' s1=unaliasedSelector { List<Selectable.Raw> args = new ArrayList<Selectable.Raw>(); args.add(s1); }
-          ( ',' sn=unaliasedSelector { args.add(sn); } )*
-      ')' { $a = args; }
-    ;
-
-countArgument
-    : '\*'
-    | i=INTEGER { if (!i.getText().equals("1")) addRecognitionError("Only COUNT(1) is supported, got COUNT(" + i.getText() + ")");}
-    ;
-
-whereClause returns [WhereClause.Builder clause]
-    @init{ $clause = new WhereClause.Builder(); }
-    : relationOrExpression[$clause] (K_AND relationOrExpression[$clause])*
-    ;
-
-relationOrExpression [WhereClause.Builder clause]
-    : relation[$clause]
-    | customIndexExpression[$clause]
-    ;
-
-customIndexExpression [WhereClause.Builder clause]
-    @init{IndexName name = new IndexName();}
-    : 'expr(' idxName[name] ',' t=term ')' { clause.add(new CustomIndexExpression(name, t));}
-    ;
-
-orderByClause[Map<ColumnIdentifier.Raw, Boolean> orderings]
-    @init{
-        boolean reversed = false;
-    }
-    : c=cident (K_ASC | K_DESC { reversed = true; })? { orderings.put(c, reversed); }
-    ;
-
-/**
- * INSERT INTO <CF> (<column>, <column>, <column>, ...)
- * VALUES (<value>, <value>, <value>, ...)
- * USING TIMESTAMP <long>;
- *
- */
-insertStatement returns [ModificationStatement.Parsed expr]
-    : K_INSERT K_INTO cf=columnFamilyName
-        ( st1=normalInsertStatement[cf] { $expr = st1; }
-        | K_JSON st2=jsonInsertStatement[cf] { $expr = st2; })
-    ;
-
-normalInsertStatement [CFName cf] returns [UpdateStatement.ParsedInsert expr]
-    @init {
-        Attributes.Raw attrs = new Attributes.Raw();
-        List<ColumnIdentifier.Raw> columnNames  = new ArrayList<ColumnIdentifier.Raw>();
-        List<Term.Raw> values = new ArrayList<Term.Raw>();
-        boolean ifNotExists = false;
-    }
-    : '(' c1=cident { columnNames.add(c1); }  ( ',' cn=cident { columnNames.add(cn); } )* ')'
-      K_VALUES
-      '(' v1=term { values.add(v1); } ( ',' vn=term { values.add(vn); } )* ')'
-      ( K_IF K_NOT K_EXISTS { ifNotExists = true; } )?
-      ( usingClause[attrs] )?
-      {
-          $expr = new UpdateStatement.ParsedInsert(cf, attrs, columnNames, values, ifNotExists);
-      }
-    ;
-
-jsonInsertStatement [CFName cf] returns [UpdateStatement.ParsedInsertJson expr]
-    @init {
-        Attributes.Raw attrs = new Attributes.Raw();
-        boolean ifNotExists = false;
-    }
-    : val=jsonValue
-      ( K_IF K_NOT K_EXISTS { ifNotExists = true; } )?
-      ( usingClause[attrs] )?
-      {
-          $expr = new UpdateStatement.ParsedInsertJson(cf, attrs, val, ifNotExists);
-      }
-    ;
-
-jsonValue returns [Json.Raw value]
-    :
-    | s=STRING_LITERAL { $value = new Json.Literal($s.text); }
-    | ':' id=noncol_ident     { $value = newJsonBindVariables(id); }
-    | QMARK            { $value = newJsonBindVariables(null); }
-    ;
-
-usingClause[Attributes.Raw attrs]
-    : K_USING usingClauseObjective[attrs] ( K_AND usingClauseObjective[attrs] )*
-    ;
-
-usingClauseObjective[Attributes.Raw attrs]
-    : K_TIMESTAMP ts=intValue { attrs.timestamp = ts; }
-    | K_TTL t=intValue { attrs.timeToLive = t; }
-    ;
-
-/**
- * UPDATE <CF>
- * USING TIMESTAMP <long>
- * SET name1 = value1, name2 = value2
- * WHERE key = value;
- * [IF (EXISTS | name = value, ...)];
- */
-updateStatement returns [UpdateStatement.ParsedUpdate expr]
-    @init {
-        Attributes.Raw attrs = new Attributes.Raw();
-        List<Pair<ColumnIdentifier.Raw, Operation.RawUpdate>> operations = new ArrayList<Pair<ColumnIdentifier.Raw, Operation.RawUpdate>>();
-        boolean ifExists = false;
-    }
-    : K_UPDATE cf=columnFamilyName
-      ( usingClause[attrs] )?
-      K_SET columnOperation[operations] (',' columnOperation[operations])*
-      K_WHERE wclause=whereClause
-      ( K_IF ( K_EXISTS { ifExists = true; } | conditions=updateConditions ))?
-      {
-          return new UpdateStatement.ParsedUpdate(cf,
-                                                  attrs,
-                                                  operations,
-                                                  wclause.build(),
-                                                  conditions == null ? Collections.<Pair<ColumnIdentifier.Raw, ColumnCondition.Raw>>emptyList() : conditions,
-                                                  ifExists);
-     }
-    ;
-
-updateConditions returns [List<Pair<ColumnIdentifier.Raw, ColumnCondition.Raw>> conditions]
-    @init { conditions = new ArrayList<Pair<ColumnIdentifier.Raw, ColumnCondition.Raw>>(); }
-    : columnCondition[conditions] ( K_AND columnCondition[conditions] )*
-    ;
-
-
-/**
- * DELETE name1, name2
- * FROM <CF>
- * USING TIMESTAMP <long>
- * WHERE KEY = keyname
-   [IF (EXISTS | name = value, ...)];
- */
-deleteStatement returns [DeleteStatement.Parsed expr]
-    @init {
-        Attributes.Raw attrs = new Attributes.Raw();
-        List<Operation.RawDeletion> columnDeletions = Collections.emptyList();
-        boolean ifExists = false;
-    }
-    : K_DELETE ( dels=deleteSelection { columnDeletions = dels; } )?
-      K_FROM cf=columnFamilyName
-      ( usingClauseDelete[attrs] )?
-      K_WHERE wclause=whereClause
-      ( K_IF ( K_EXISTS { ifExists = true; } | conditions=updateConditions ))?
-      {
-          return new DeleteStatement.Parsed(cf,
-                                            attrs,
-                                            columnDeletions,
-                                            wclause.build(),
-                                            conditions == null ? Collections.<Pair<ColumnIdentifier.Raw, ColumnCondition.Raw>>emptyList() : conditions,
-                                            ifExists);
-      }
-    ;
-
-deleteSelection returns [List<Operation.RawDeletion> operations]
-    : { $operations = new ArrayList<Operation.RawDeletion>(); }
-          t1=deleteOp { $operations.add(t1); }
-          (',' tN=deleteOp { $operations.add(tN); })*
-    ;
-
-deleteOp returns [Operation.RawDeletion op]
-    : c=cident                { $op = new Operation.ColumnDeletion(c); }
-    | c=cident '[' t=term ']' { $op = new Operation.ElementDeletion(c, t); }
-    ;
-
-usingClauseDelete[Attributes.Raw attrs]
-    : K_USING K_TIMESTAMP ts=intValue { attrs.timestamp = ts; }
-    ;
-
-/**
- * BEGIN BATCH
- *   UPDATE <CF> SET name1 = value1 WHERE KEY = keyname1;
- *   UPDATE <CF> SET name2 = value2 WHERE KEY = keyname2;
- *   UPDATE <CF> SET name3 = value3 WHERE KEY = keyname3;
- *   ...
- * APPLY BATCH
- *
- * OR
- *
- * BEGIN BATCH
- *   INSERT INTO <CF> (KEY, <name>) VALUES ('<key>', '<value>');
- *   INSERT INTO <CF> (KEY, <name>) VALUES ('<key>', '<value>');
- *   ...
- * APPLY BATCH
- *
- * OR
- *
- * BEGIN BATCH
- *   DELETE name1, name2 FROM <CF> WHERE key = <key>
- *   DELETE name3, name4 FROM <CF> WHERE key = <key>
- *   ...
- * APPLY BATCH
- */
-batchStatement returns [BatchStatement.Parsed expr]
-    @init {
-        BatchStatement.Type type = BatchStatement.Type.LOGGED;
-        List<ModificationStatement.Parsed> statements = new ArrayList<ModificationStatement.Parsed>();
-        Attributes.Raw attrs = new Attributes.Raw();
-    }
-    : K_BEGIN
-      ( K_UNLOGGED { type = BatchStatement.Type.UNLOGGED; } | K_COUNTER { type = BatchStatement.Type.COUNTER; } )?
-      K_BATCH ( usingClause[attrs] )?
-          ( s=batchStatementObjective ';'? { statements.add(s); } )*
-      K_APPLY K_BATCH
-      {
-          return new BatchStatement.Parsed(type, attrs, statements);
-      }
-    ;
-
-batchStatementObjective returns [ModificationStatement.Parsed statement]
-    : i=insertStatement  { $statement = i; }
-    | u=updateStatement  { $statement = u; }
-    | d=deleteStatement  { $statement = d; }
-    ;
-
-createAggregateStatement returns [CreateAggregateStatement expr]
-    @init {
-        boolean orReplace = false;
-        boolean ifNotExists = false;
-
-        List<CQL3Type.Raw> argsTypes = new ArrayList<>();
-    }
-    : K_CREATE (K_OR K_REPLACE { orReplace = true; })?
-      K_AGGREGATE
-      (K_IF K_NOT K_EXISTS { ifNotExists = true; })?
-      fn=functionName
-      '('
-        (
-          v=comparatorType { argsTypes.add(v); }
-          ( ',' v=comparatorType { argsTypes.add(v); } )*
-        )?
-      ')'
-      K_SFUNC sfunc = allowedFunctionName
-      K_STYPE stype = comparatorType
-      (
-        K_FINALFUNC ffunc = allowedFunctionName
-      )?
-      (
-        K_INITCOND ival = term
-      )?
-      { $expr = new CreateAggregateStatement(fn, argsTypes, sfunc, stype, ffunc, ival, orReplace, ifNotExists); }
-    ;
-
-dropAggregateStatement returns [DropAggregateStatement expr]
-    @init {
-        boolean ifExists = false;
-        List<CQL3Type.Raw> argsTypes = new ArrayList<>();
-        boolean argsPresent = false;
-    }
-    : K_DROP K_AGGREGATE
-      (K_IF K_EXISTS { ifExists = true; } )?
-      fn=functionName
-      (
-        '('
-          (
-            v=comparatorType { argsTypes.add(v); }
-            ( ',' v=comparatorType { argsTypes.add(v); } )*
-          )?
-        ')'
-        { argsPresent = true; }
-      )?
-      { $expr = new DropAggregateStatement(fn, argsTypes, argsPresent, ifExists); }
-    ;
-
-createFunctionStatement returns [CreateFunctionStatement expr]
-    @init {
-        boolean orReplace = false;
-        boolean ifNotExists = false;
-
-        List<ColumnIdentifier> argsNames = new ArrayList<>();
-        List<CQL3Type.Raw> argsTypes = new ArrayList<>();
-        boolean calledOnNullInput = false;
-    }
-    : K_CREATE (K_OR K_REPLACE { orReplace = true; })?
-      K_FUNCTION
-      (K_IF K_NOT K_EXISTS { ifNotExists = true; })?
-      fn=functionName
-      '('
-        (
-          k=noncol_ident v=comparatorType { argsNames.add(k); argsTypes.add(v); }
-          ( ',' k=noncol_ident v=comparatorType { argsNames.add(k); argsTypes.add(v); } )*
-        )?
-      ')'
-      ( (K_RETURNS K_NULL) | (K_CALLED { calledOnNullInput=true; })) K_ON K_NULL K_INPUT
-      K_RETURNS rt = comparatorType
-      K_LANGUAGE language = IDENT
-      K_AS body = STRING_LITERAL
-      { $expr = new CreateFunctionStatement(fn, $language.text.toLowerCase(), $body.text,
-                                            argsNames, argsTypes, rt, calledOnNullInput, orReplace, ifNotExists); }
-    ;
-
-dropFunctionStatement returns [DropFunctionStatement expr]
-    @init {
-        boolean ifExists = false;
-        List<CQL3Type.Raw> argsTypes = new ArrayList<>();
-        boolean argsPresent = false;
-    }
-    : K_DROP K_FUNCTION
-      (K_IF K_EXISTS { ifExists = true; } )?
-      fn=functionName
-      (
-        '('
-          (
-            v=comparatorType { argsTypes.add(v); }
-            ( ',' v=comparatorType { argsTypes.add(v); } )*
-          )?
-        ')'
-        { argsPresent = true; }
-      )?
-      { $expr = new DropFunctionStatement(fn, argsTypes, argsPresent, ifExists); }
-    ;
-
-/**
- * CREATE KEYSPACE [IF NOT EXISTS] <KEYSPACE> WITH attr1 = value1 AND attr2 = value2;
- */
-createKeyspaceStatement returns [CreateKeyspaceStatement expr]
-    @init {
-        KeyspaceAttributes attrs = new KeyspaceAttributes();
-        boolean ifNotExists = false;
-    }
-    : K_CREATE K_KEYSPACE (K_IF K_NOT K_EXISTS { ifNotExists = true; } )? ks=keyspaceName
-      K_WITH properties[attrs] { $expr = new CreateKeyspaceStatement(ks, attrs, ifNotExists); }
-    ;
-
-/**
- * CREATE COLUMNFAMILY [IF NOT EXISTS] <CF> (
- *     <name1> <type>,
- *     <name2> <type>,
- *     <name3> <type>
- * ) WITH <property> = <value> AND ...;
- */
-createTableStatement returns [CreateTableStatement.RawStatement expr]
-    @init { boolean ifNotExists = false; }
-    : K_CREATE K_COLUMNFAMILY (K_IF K_NOT K_EXISTS { ifNotExists = true; } )?
-      cf=columnFamilyName { $expr = new CreateTableStatement.RawStatement(cf, ifNotExists); }
-      cfamDefinition[expr]
-    ;
-
-cfamDefinition[CreateTableStatement.RawStatement expr]
-    : '(' cfamColumns[expr] ( ',' cfamColumns[expr]? )* ')'
-      ( K_WITH cfamProperty[expr.properties] ( K_AND cfamProperty[expr.properties] )*)?
-    ;
-
-cfamColumns[CreateTableStatement.RawStatement expr]
-    : k=ident v=comparatorType { boolean isStatic=false; } (K_STATIC {isStatic = true;})? { $expr.addDefinition(k, v, isStatic); }
-        (K_PRIMARY K_KEY { $expr.addKeyAliases(Collections.singletonList(k)); })?
-    | K_PRIMARY K_KEY '(' pkDef[expr] (',' c=ident { $expr.addColumnAlias(c); } )* ')'
-    ;
-
-pkDef[CreateTableStatement.RawStatement expr]
-    : k=ident { $expr.addKeyAliases(Collections.singletonList(k)); }
-    | '(' { List<ColumnIdentifier> l = new ArrayList<ColumnIdentifier>(); } k1=ident { l.add(k1); } ( ',' kn=ident { l.add(kn); } )* ')' { $expr.addKeyAliases(l); }
-    ;
-
-cfamProperty[CFProperties props]
-    : property[props.properties]
-    | K_COMPACT K_STORAGE { $props.setCompactStorage(); }
-    | K_CLUSTERING K_ORDER K_BY '(' cfamOrdering[props] (',' cfamOrdering[props])* ')'
-    ;
-
-cfamOrdering[CFProperties props]
-    @init{ boolean reversed=false; }
-    : k=ident (K_ASC | K_DESC { reversed=true;} ) { $props.setOrdering(k, reversed); }
-    ;
-
-
-/**
- * CREATE TYPE foo (
- *    <name1> <type1>,
- *    <name2> <type2>,
- *    ....
- * )
- */
-createTypeStatement returns [CreateTypeStatement expr]
-    @init { boolean ifNotExists = false; }
-    : K_CREATE K_TYPE (K_IF K_NOT K_EXISTS { ifNotExists = true; } )?
-         tn=userTypeName { $expr = new CreateTypeStatement(tn, ifNotExists); }
-         '(' typeColumns[expr] ( ',' typeColumns[expr]? )* ')'
-    ;
-
-typeColumns[CreateTypeStatement expr]
-    : k=noncol_ident v=comparatorType { $expr.addDefinition(k, v); }
-    ;
-
-
-/**
- * CREATE INDEX [IF NOT EXISTS] [indexName] ON <columnFamily> (<columnName>);
- * CREATE CUSTOM INDEX [IF NOT EXISTS] [indexName] ON <columnFamily> (<columnName>) USING <indexClass>;
- */
-createIndexStatement returns [CreateIndexStatement expr]
-    @init {
-        IndexPropDefs props = new IndexPropDefs();
-        boolean ifNotExists = false;
-        IndexName name = new IndexName();
-        List<IndexTarget.Raw> targets = new ArrayList<>();
-    }
-    : K_CREATE (K_CUSTOM { props.isCustom = true; })? K_INDEX (K_IF K_NOT K_EXISTS { ifNotExists = true; } )?
-        (idxName[name])? K_ON cf=columnFamilyName '(' (indexIdent[targets] (',' indexIdent[targets])*)? ')'
-        (K_USING cls=STRING_LITERAL { props.customClass = $cls.text; })?
-        (K_WITH properties[props])?
-      { $expr = new CreateIndexStatement(cf, name, targets, props, ifNotExists); }
-    ;
-
-indexIdent [List<IndexTarget.Raw> targets]
-    : c=cident                   { $targets.add(IndexTarget.Raw.simpleIndexOn(c)); }
-    | K_VALUES '(' c=cident ')'  { $targets.add(IndexTarget.Raw.valuesOf(c)); }
-    | K_KEYS '(' c=cident ')'    { $targets.add(IndexTarget.Raw.keysOf(c)); }
-    | K_ENTRIES '(' c=cident ')' { $targets.add(IndexTarget.Raw.keysAndValuesOf(c)); }
-    | K_FULL '(' c=cident ')'    { $targets.add(IndexTarget.Raw.fullCollection(c)); }
-    ;
-
-/**
- * CREATE MATERIALIZED VIEW <viewName> AS
- *  SELECT <columns>
- *  FROM <CF>
- *  WHERE <pkColumns> IS NOT NULL
- *  PRIMARY KEY (<pkColumns>)
- *  WITH <property> = <value> AND ...;
- */
-createMaterializedViewStatement returns [CreateViewStatement expr]
-    @init {
-        boolean ifNotExists = false;
-        List<ColumnIdentifier.Raw> partitionKeys = new ArrayList<>();
-        List<ColumnIdentifier.Raw> compositeKeys = new ArrayList<>();
-    }
-    : K_CREATE K_MATERIALIZED K_VIEW (K_IF K_NOT K_EXISTS { ifNotExists = true; })? cf=columnFamilyName K_AS
-        K_SELECT sclause=selectClause K_FROM basecf=columnFamilyName
-        (K_WHERE wclause=whereClause)?
-        K_PRIMARY K_KEY (
-        '(' '(' k1=cident { partitionKeys.add(k1); } ( ',' kn=cident { partitionKeys.add(kn); } )* ')' ( ',' c1=cident { compositeKeys.add(c1); } )* ')'
-    |   '(' k1=cident { partitionKeys.add(k1); } ( ',' cn=cident { compositeKeys.add(cn); } )* ')'
-        )
-        {
-             WhereClause where = wclause == null ? WhereClause.empty() : wclause.build();
-             $expr = new CreateViewStatement(cf, basecf, sclause, where, partitionKeys, compositeKeys, ifNotExists);
-        }
-        ( K_WITH cfamProperty[expr.properties] ( K_AND cfamProperty[expr.properties] )*)?
-    ;
-
-/**
- * CREATE TRIGGER triggerName ON columnFamily USING 'triggerClass';
- */
-createTriggerStatement returns [CreateTriggerStatement expr]
-    @init {
-        boolean ifNotExists = false;
-    }
-    : K_CREATE K_TRIGGER (K_IF K_NOT K_EXISTS { ifNotExists = true; } )? (name=noncol_ident)
-        K_ON cf=columnFamilyName K_USING cls=STRING_LITERAL
-      { $expr = new CreateTriggerStatement(cf, name.toString(), $cls.text, ifNotExists); }
-    ;
-
-/**
- * DROP TRIGGER [IF EXISTS] triggerName ON columnFamily;
- */
-dropTriggerStatement returns [DropTriggerStatement expr]
-     @init { boolean ifExists = false; }
-    : K_DROP K_TRIGGER (K_IF K_EXISTS { ifExists = true; } )? (name=noncol_ident) K_ON cf=columnFamilyName
-      { $expr = new DropTriggerStatement(cf, name.toString(), ifExists); }
-    ;
-
-/**
- * ALTER KEYSPACE <KS> WITH <property> = <value>;
- */
-alterKeyspaceStatement returns [AlterKeyspaceStatement expr]
-    @init { KeyspaceAttributes attrs = new KeyspaceAttributes(); }
-    : K_ALTER K_KEYSPACE ks=keyspaceName
-        K_WITH properties[attrs] { $expr = new AlterKeyspaceStatement(ks, attrs); }
-    ;
-
-
-/**
- * ALTER COLUMN FAMILY <CF> ALTER <column> TYPE <newtype>;
- * ALTER COLUMN FAMILY <CF> ADD <column> <newtype>;
- * ALTER COLUMN FAMILY <CF> DROP <column>;
- * ALTER COLUMN FAMILY <CF> WITH <property> = <value>;
- * ALTER COLUMN FAMILY <CF> RENAME <column> TO <column>;
- */
-alterTableStatement returns [AlterTableStatement expr]
-    @init {
-        AlterTableStatement.Type type = null;
-        TableAttributes attrs = new TableAttributes();
-        Map<ColumnIdentifier.Raw, ColumnIdentifier> renames = new HashMap<ColumnIdentifier.Raw, ColumnIdentifier>();
-        boolean isStatic = false;
-        Long dropTimestamp = null;
-    }
-    : K_ALTER K_COLUMNFAMILY cf=columnFamilyName
-          ( K_ALTER id=cident K_TYPE v=comparatorType { type = AlterTableStatement.Type.ALTER;  }
-          | K_ADD   aid=ident {id=new ColumnIdentifier.ColumnIdentifierValue(aid);} v=comparatorType ({ isStatic=true; } K_STATIC)? { type = AlterTableStatement.Type.ADD; }
-          | K_DROP  id=cident                               { type = AlterTableStatement.Type.DROP; }
-          | K_DROP  id=cident K_USING K_TIMESTAMP t=INTEGER { type = AlterTableStatement.Type.DROP;
-                                                              dropTimestamp = Long.parseLong(Constants.Literal.integer($t.text).getText()); }
-          | K_DROP  K_COMPACT K_STORAGE                     { type = AlterTableStatement.Type.DROP_COMPACT_STORAGE; }
-          | K_WITH  properties[attrs]                       { type = AlterTableStatement.Type.OPTS; }
-          | K_RENAME                                        { type = AlterTableStatement.Type.RENAME; }
-               id1=cident K_TO toId1=ident { renames.put(id1, toId1); }
-               ( K_AND idn=cident K_TO toIdn=ident { renames.put(idn, toIdn); } )*
-          )
-    {
-        $expr = new AlterTableStatement(cf, type, id, v, attrs, renames, isStatic, dropTimestamp);
-    }
-    ;
-
-alterMaterializedViewStatement returns [AlterViewStatement expr]
-    @init {
-        TableAttributes attrs = new TableAttributes();
-    }
-    : K_ALTER K_MATERIALIZED K_VIEW name=columnFamilyName
-          K_WITH properties[attrs]
-    {
-        $expr = new AlterViewStatement(name, attrs);
-    }
-    ;
-    
-
-/**
- * ALTER TYPE <name> ALTER <field> TYPE <newtype>;
- * ALTER TYPE <name> ADD <field> <newtype>;
- * ALTER TYPE <name> RENAME <field> TO <newtype> AND ...;
- */
-alterTypeStatement returns [AlterTypeStatement expr]
-    : K_ALTER K_TYPE name=userTypeName
-          ( K_ALTER f=noncol_ident K_TYPE v=comparatorType { $expr = AlterTypeStatement.alter(name, f, v); }
-          | K_ADD   f=noncol_ident v=comparatorType        { $expr = AlterTypeStatement.addition(name, f, v); }
-          | K_RENAME
-               { Map<ColumnIdentifier, ColumnIdentifier> renames = new HashMap<ColumnIdentifier, ColumnIdentifier>(); }
-                 id1=noncol_ident K_TO toId1=noncol_ident { renames.put(id1, toId1); }
-                 ( K_AND idn=noncol_ident K_TO toIdn=noncol_ident { renames.put(idn, toIdn); } )*
-               { $expr = AlterTypeStatement.renames(name, renames); }
-          )
-    ;
-
-
-/**
- * DROP KEYSPACE [IF EXISTS] <KSP>;
- */
-dropKeyspaceStatement returns [DropKeyspaceStatement ksp]
-    @init { boolean ifExists = false; }
-    : K_DROP K_KEYSPACE (K_IF K_EXISTS { ifExists = true; } )? ks=keyspaceName { $ksp = new DropKeyspaceStatement(ks, ifExists); }
-    ;
-
-/**
- * DROP COLUMNFAMILY [IF EXISTS] <CF>;
- */
-dropTableStatement returns [DropTableStatement stmt]
-    @init { boolean ifExists = false; }
-    : K_DROP K_COLUMNFAMILY (K_IF K_EXISTS { ifExists = true; } )? cf=columnFamilyName { $stmt = new DropTableStatement(cf, ifExists); }
-    ;
-
-/**
- * DROP TYPE <name>;
- */
-dropTypeStatement returns [DropTypeStatement stmt]
-    @init { boolean ifExists = false; }
-    : K_DROP K_TYPE (K_IF K_EXISTS { ifExists = true; } )? name=userTypeName { $stmt = new DropTypeStatement(name, ifExists); }
-    ;
-
-/**
- * DROP INDEX [IF EXISTS] <INDEX_NAME>
- */
-dropIndexStatement returns [DropIndexStatement expr]
-    @init { boolean ifExists = false; }
-    : K_DROP K_INDEX (K_IF K_EXISTS { ifExists = true; } )? index=indexName
-      { $expr = new DropIndexStatement(index, ifExists); }
-    ;
-
-/**
- * DROP MATERIALIZED VIEW [IF EXISTS] <view_name>
- */
-dropMaterializedViewStatement returns [DropViewStatement expr]
-    @init { boolean ifExists = false; }
-    : K_DROP K_MATERIALIZED K_VIEW (K_IF K_EXISTS { ifExists = true; } )? cf=columnFamilyName
-      { $expr = new DropViewStatement(cf, ifExists); }
-    ;
-
-/**
-  * TRUNCATE <CF>;
-  */
-truncateStatement returns [TruncateStatement stmt]
-    : K_TRUNCATE (K_COLUMNFAMILY)? cf=columnFamilyName { $stmt = new TruncateStatement(cf); }
-    ;
-
-/**
- * GRANT <permission> ON <resource> TO <rolename>
- */
-grantPermissionsStatement returns [GrantPermissionsStatement stmt]
-    : K_GRANT
-          permissionOrAll
-      K_ON
-          resource
-      K_TO
-          grantee=userOrRoleName
-      { $stmt = new GrantPermissionsStatement(filterPermissions($permissionOrAll.perms, $resource.res), $resource.res, grantee); }
-    ;
-
-/**
- * REVOKE <permission> ON <resource> FROM <rolename>
- */
-revokePermissionsStatement returns [RevokePermissionsStatement stmt]
-    : K_REVOKE
-          permissionOrAll
-      K_ON
-          resource
-      K_FROM
-          revokee=userOrRoleName
-      { $stmt = new RevokePermissionsStatement(filterPermissions($permissionOrAll.perms, $resource.res), $resource.res, revokee); }
-    ;
-
-/**
- * GRANT ROLE <rolename> TO <grantee>
- */
-grantRoleStatement returns [GrantRoleStatement stmt]
-    : K_GRANT
-          role=userOrRoleName
-      K_TO
-          grantee=userOrRoleName
-      { $stmt = new GrantRoleStatement(role, grantee); }
-    ;
-
-/**
- * REVOKE ROLE <rolename> FROM <revokee>
- */
-revokeRoleStatement returns [RevokeRoleStatement stmt]
-    : K_REVOKE
-          role=userOrRoleName
-      K_FROM
-          revokee=userOrRoleName
-      { $stmt = new RevokeRoleStatement(role, revokee); }
-    ;
-
-listPermissionsStatement returns [ListPermissionsStatement stmt]
-    @init {
-        IResource resource = null;
-        boolean recursive = true;
-        RoleName grantee = new RoleName();
-    }
-    : K_LIST
-          permissionOrAll
-      ( K_ON resource { resource = $resource.res; } )?
-      ( K_OF roleName[grantee] )?
-      ( K_NORECURSIVE { recursive = false; } )?
-      { $stmt = new ListPermissionsStatement($permissionOrAll.perms, resource, grantee, recursive); }
-    ;
-
-permission returns [Permission perm]
-    : p=(K_CREATE | K_ALTER | K_DROP | K_SELECT | K_MODIFY | K_AUTHORIZE | K_DESCRIBE | K_EXECUTE)
-    { $perm = Permission.valueOf($p.text.toUpperCase()); }
-    ;
-
-permissionOrAll returns [Set<Permission> perms]
-    : K_ALL ( K_PERMISSIONS )?       { $perms = Permission.ALL; }
-    | p=permission ( K_PERMISSION )? { $perms = EnumSet.of($p.perm); }
-    ;
-
-resource returns [IResource res]
-    : d=dataResource { $res = $d.res; }
-    | r=roleResource { $res = $r.res; }
-    | f=functionResource { $res = $f.res; }
-    ;
-
-dataResource returns [DataResource res]
-    : K_ALL K_KEYSPACES { $res = DataResource.root(); }
-    | K_KEYSPACE ks = keyspaceName { $res = DataResource.keyspace($ks.id); }
-    | ( K_COLUMNFAMILY )? cf = columnFamilyName
-      { $res = DataResource.table($cf.name.getKeyspace(), $cf.name.getColumnFamily()); }
-    ;
-
-roleResource returns [RoleResource res]
-    : K_ALL K_ROLES { $res = RoleResource.root(); }
-    | K_ROLE role = userOrRoleName { $res = RoleResource.role($role.name.getName()); }
-    ;
-
-functionResource returns [FunctionResource res]
-    @init {
-        List<CQL3Type.Raw> argsTypes = new ArrayList<>();
-    }
-    : K_ALL K_FUNCTIONS { $res = FunctionResource.root(); }
-    | K_ALL K_FUNCTIONS K_IN K_KEYSPACE ks = keyspaceName { $res = FunctionResource.keyspace($ks.id); }
-    // Arg types are mandatory for DCL statements on Functions
-    | K_FUNCTION fn=functionName
-      (
-        '('
-          (
-            v=comparatorType { argsTypes.add(v); }
-            ( ',' v=comparatorType { argsTypes.add(v); } )*
-          )?
-        ')'
-      )
-      { $res = FunctionResource.functionFromCql($fn.s.keyspace, $fn.s.name, argsTypes); }
-    ;
-
-/**
- * CREATE USER [IF NOT EXISTS] <username> [WITH PASSWORD <password>] [SUPERUSER|NOSUPERUSER]
- */
-createUserStatement returns [CreateRoleStatement stmt]
-    @init {
-        RoleOptions opts = new RoleOptions();
-        opts.setOption(IRoleManager.Option.LOGIN, true);
-        boolean superuser = false;
-        boolean ifNotExists = false;
-        RoleName name = new RoleName();
-    }
-    : K_CREATE K_USER (K_IF K_NOT K_EXISTS { ifNotExists = true; })? u=username { name.setName($u.text, true); }
-      ( K_WITH userPassword[opts] )?
-      ( K_SUPERUSER { superuser = true; } | K_NOSUPERUSER { superuser = false; } )?
-      { opts.setOption(IRoleManager.Option.SUPERUSER, superuser);
-        $stmt = new CreateRoleStatement(name, opts, ifNotExists); }
-    ;
-
-/**
- * ALTER USER <username> [WITH PASSWORD <password>] [SUPERUSER|NOSUPERUSER]
- */
-alterUserStatement returns [AlterRoleStatement stmt]
-    @init {
-        RoleOptions opts = new RoleOptions();
-        RoleName name = new RoleName();
-    }
-    : K_ALTER K_USER u=username { name.setName($u.text, true); }
-      ( K_WITH userPassword[opts] )?
-      ( K_SUPERUSER { opts.setOption(IRoleManager.Option.SUPERUSER, true); }
-        | K_NOSUPERUSER { opts.setOption(IRoleManager.Option.SUPERUSER, false); } ) ?
-      {  $stmt = new AlterRoleStatement(name, opts); }
-    ;
-
-/**
- * DROP USER [IF EXISTS] <username>
- */
-dropUserStatement returns [DropRoleStatement stmt]
-    @init {
-        boolean ifExists = false;
-        RoleName name = new RoleName();
-    }
-    : K_DROP K_USER (K_IF K_EXISTS { ifExists = true; })? u=username { name.setName($u.text, true); $stmt = new DropRoleStatement(name, ifExists); }
-    ;
-
-/**
- * LIST USERS
- */
-listUsersStatement returns [ListRolesStatement stmt]
-    : K_LIST K_USERS { $stmt = new ListUsersStatement(); }
-    ;
-
-/**
- * CREATE ROLE [IF NOT EXISTS] <rolename> [ [WITH] option [ [AND] option ]* ]
- *
- * where option can be:
- *  PASSWORD = '<password>'
- *  SUPERUSER = (true|false)
- *  LOGIN = (true|false)
- *  OPTIONS = { 'k1':'v1', 'k2':'v2'}
- */
-createRoleStatement returns [CreateRoleStatement stmt]
-    @init {
-        RoleOptions opts = new RoleOptions();
-        boolean ifNotExists = false;
-    }
-    : K_CREATE K_ROLE (K_IF K_NOT K_EXISTS { ifNotExists = true; })? name=userOrRoleName
-      ( K_WITH roleOptions[opts] )?
-      {
-        // set defaults if they weren't explictly supplied
-        if (!opts.getLogin().isPresent())
-        {
-            opts.setOption(IRoleManager.Option.LOGIN, false);
-        }
-        if (!opts.getSuperuser().isPresent())
-        {
-            opts.setOption(IRoleManager.Option.SUPERUSER, false);
-        }
-        $stmt = new CreateRoleStatement(name, opts, ifNotExists);
-      }
-    ;
-
-/**
- * ALTER ROLE <rolename> [ [WITH] option [ [AND] option ]* ]
- *
- * where option can be:
- *  PASSWORD = '<password>'
- *  SUPERUSER = (true|false)
- *  LOGIN = (true|false)
- *  OPTIONS = { 'k1':'v1', 'k2':'v2'}
- */
-alterRoleStatement returns [AlterRoleStatement stmt]
-    @init {
-        RoleOptions opts = new RoleOptions();
-    }
-    : K_ALTER K_ROLE name=userOrRoleName
-      ( K_WITH roleOptions[opts] )?
-      {  $stmt = new AlterRoleStatement(name, opts); }
-    ;
-
-/**
- * DROP ROLE [IF EXISTS] <rolename>
- */
-dropRoleStatement returns [DropRoleStatement stmt]
-    @init {
-        boolean ifExists = false;
-    }
-    : K_DROP K_ROLE (K_IF K_EXISTS { ifExists = true; })? name=userOrRoleName
-      { $stmt = new DropRoleStatement(name, ifExists); }
-    ;
-
-/**
- * LIST ROLES [OF <rolename>] [NORECURSIVE]
- */
-listRolesStatement returns [ListRolesStatement stmt]
-    @init {
-        boolean recursive = true;
-        RoleName grantee = new RoleName();
-    }
-    : K_LIST K_ROLES
-      ( K_OF roleName[grantee])?
-      ( K_NORECURSIVE { recursive = false; } )?
-      { $stmt = new ListRolesStatement(grantee, recursive); }
-    ;
-
-roleOptions[RoleOptions opts]
-    : roleOption[opts] (K_AND roleOption[opts])*
-    ;
-
-roleOption[RoleOptions opts]
-    :  K_PASSWORD '=' v=STRING_LITERAL { opts.setOption(IRoleManager.Option.PASSWORD, $v.text); }
-    |  K_OPTIONS '=' m=mapLiteral { opts.setOption(IRoleManager.Option.OPTIONS, convertPropertyMap(m)); }
-    |  K_SUPERUSER '=' b=BOOLEAN { opts.setOption(IRoleManager.Option.SUPERUSER, Boolean.valueOf($b.text)); }
-    |  K_LOGIN '=' b=BOOLEAN { opts.setOption(IRoleManager.Option.LOGIN, Boolean.valueOf($b.text)); }
-    ;
-
-// for backwards compatibility in CREATE/ALTER USER, this has no '='
-userPassword[RoleOptions opts]
-    :  K_PASSWORD v=STRING_LITERAL { opts.setOption(IRoleManager.Option.PASSWORD, $v.text); }
-    ;
-
-/** DEFINITIONS **/
-
-// Column Identifiers.  These need to be treated differently from other
-// identifiers because the underlying comparator is not necessarily text. See
-// CASSANDRA-8178 for details.
-// Also, we need to support the internal of the super column map (for backward
-// compatibility) which is empty (we only want to allow this is queries, not for
-// creating table or other).
-cident returns [ColumnIdentifier.Raw id]
-    : t=IDENT              { $id = new ColumnIdentifier.Literal($t.text, false); }
-    | t=QUOTED_NAME        { $id = new ColumnIdentifier.Literal($t.text, true); }
-    | k=unreserved_keyword { $id = new ColumnIdentifier.Literal(k, false); }
-    | EMPTY_QUOTED_NAME    { $id = new ColumnIdentifier.Literal("", false); }
-    ;
-
-// Column identifiers where the comparator is known to be text
-ident returns [ColumnIdentifier id]
-    : t=IDENT              { $id = ColumnIdentifier.getInterned($t.text, false); }
-    | t=QUOTED_NAME        { $id = ColumnIdentifier.getInterned($t.text, true); }
-    | k=unreserved_keyword { $id = ColumnIdentifier.getInterned(k, false); }
-    ;
-
-// Identifiers that do not refer to columns
-noncol_ident returns [ColumnIdentifier id]
-    : t=IDENT              { $id = new ColumnIdentifier($t.text, false); }
-    | t=QUOTED_NAME        { $id = new ColumnIdentifier($t.text, true); }
-    | k=unreserved_keyword { $id = new ColumnIdentifier(k, false); }
-    ;
-
-// Keyspace & Column family names
-keyspaceName returns [String id]
-    @init { CFName name = new CFName(); }
-    : ksName[name] { $id = name.getKeyspace(); }
-    ;
-
-indexName returns [IndexName name]
-    @init { $name = new IndexName(); }
-    : (ksName[name] '.')? idxName[name]
-    ;
-
-columnFamilyName returns [CFName name]
-    @init { $name = new CFName(); }
-    : (ksName[name] '.')? cfName[name]
-    ;
-
-userTypeName returns [UTName name]
-    : (ks=noncol_ident '.')? ut=non_type_ident { return new UTName(ks, ut); }
-    ;
-
-userOrRoleName returns [RoleName name]
-    @init { $name = new RoleName(); }
-    : roleName[name] {return $name;}
-    ;
-
-ksName[KeyspaceElementName name]
-    : t=IDENT              { $name.setKeyspace($t.text, false);}
-    | t=QUOTED_NAME        { $name.setKeyspace($t.text, true);}
-    | k=unreserved_keyword { $name.setKeyspace(k, false);}
-    | QMARK {addRecognitionError("Bind variables cannot be used for keyspace names");}
-    ;
-
-cfName[CFName name]
-    : t=IDENT              { $name.setColumnFamily($t.text, false); }
-    | t=QUOTED_NAME        { $name.setColumnFamily($t.text, true); }
-    | k=unreserved_keyword { $name.setColumnFamily(k, false); }
-    | QMARK {addRecognitionError("Bind variables cannot be used for table names");}
-    ;
-
-idxName[IndexName name]
-    : t=IDENT              { $name.setIndex($t.text, false); }
-    | t=QUOTED_NAME        { $name.setIndex($t.text, true);}
-    | k=unreserved_keyword { $name.setIndex(k, false); }
-    | QMARK {addRecognitionError("Bind variables cannot be used for index names");}
-    ;
-
-roleName[RoleName name]
-    : t=IDENT              { $name.setName($t.text, false); }
-    | s=STRING_LITERAL     { $name.setName($s.text, true); }
-    | t=QUOTED_NAME        { $name.setName($t.text, true); }
-    | k=unreserved_keyword { $name.setName(k, false); }
-    | QMARK {addRecognitionError("Bind variables cannot be used for role names");}
-    ;
-
-constant returns [Constants.Literal constant]
-    : t=STRING_LITERAL { $constant = Constants.Literal.string($t.text); }
-    | t=INTEGER        { $constant = Constants.Literal.integer($t.text); }
-    | t=FLOAT          { $constant = Constants.Literal.floatingPoint($t.text); }
-    | t=BOOLEAN        { $constant = Constants.Literal.bool($t.text); }
-    | t=UUID           { $constant = Constants.Literal.uuid($t.text); }
-    | t=HEXNUMBER      { $constant = Constants.Literal.hex($t.text); }
-    | { String sign=""; } ('-' {sign = "-"; } )? t=(K_NAN | K_INFINITY) { $constant = Constants.Literal.floatingPoint(sign + $t.text); }
-    ;
-
-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); }
-    ;
-
-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); }
-    | { List<Term.Raw> s = new ArrayList<Term.Raw>(); s.add(t); }
-          ( ',' tn=term { s.add(tn); } )*
-      { $value = new Sets.Literal(s); }
-    ;
-
-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=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()); }
-    ;
-
-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=noncol_ident ':' v1=term { m.put(k1, v1); } ( ',' kn=noncol_ident ':' 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=collectionLiteral  { $value = l; }
-    | u=usertypeLiteral    { $value = u; }
-    | t=tupleLiteral       { $value = t; }
-    | K_NULL               { $value = Constants.NULL_LITERAL; }
-    | ':' id=noncol_ident  { $value = newBindVariables(id); }
-    | QMARK                { $value = newBindVariables(null); }
-    ;
-
-intValue returns [Term.Raw value]
-    :
-    | t=INTEGER     { $value = Constants.Literal.integer($t.text); }
-    | ':' id=noncol_ident  { $value = newBindVariables(id); }
-    | QMARK         { $value = newBindVariables(null); }
-    ;
-
-functionName returns [FunctionName s]
-     // antlr might try to recover and give a null for f. It will still error out in the end, but FunctionName
-     // wouldn't be happy with that so we should bypass this for now or we'll have a weird user-facing error
-    : (ks=keyspaceName '.')? f=allowedFunctionName   { $s = f == null ? null : new FunctionName(ks, f); }
-    ;
-
-allowedFunctionName returns [String s]
-    : f=IDENT                       { $s = $f.text.toLowerCase(); }
-    | f=QUOTED_NAME                 { $s = $f.text; }
-    | u=unreserved_function_keyword { $s = u; }
-    | K_TOKEN                       { $s = "token"; }
-    | K_COUNT                       { $s = "count"; }
-    ;
-
-function returns [Term.Raw t]
-    : f=functionName '(' ')'                   { $t = new FunctionCall.Raw(f, Collections.<Term.Raw>emptyList()); }
-    | f=functionName '(' args=functionArgs ')' { $t = new FunctionCall.Raw(f, args); }
-    ;
-
-functionArgs returns [List<Term.Raw> args]
-    @init{ $args = new ArrayList<Term.Raw>(); }
-    : t1=term {args.add(t1); } ( ',' tn=term { args.add(tn); } )*
-    ;
-
-term returns [Term.Raw term]
-    : v=value                          { $term = v; }
-    | f=function                       { $term = f; }
-    | '(' c=comparatorType ')' t=term  { $term = new TypeCast(c, t); }
-    ;
-
-columnOperation[List<Pair<ColumnIdentifier.Raw, Operation.RawUpdate>> operations]
-    : key=cident columnOperationDifferentiator[operations, key]
-    ;
-
-columnOperationDifferentiator[List<Pair<ColumnIdentifier.Raw, Operation.RawUpdate>> operations, ColumnIdentifier.Raw key]
-    : '=' normalColumnOperation[operations, key]
-    | '[' k=term ']' specializedColumnOperation[operations, key, k]
-    ;
-
-normalColumnOperation[List<Pair<ColumnIdentifier.Raw, Operation.RawUpdate>> operations, ColumnIdentifier.Raw key]
-    : t=term ('+' c=cident )?
-      {
-          if (c == null)
-          {
-              addRawUpdate(operations, key, new Operation.SetValue(t));
-          }
-          else
-          {
-              if (!key.equals(c))
-                  addRecognitionError("Only expressions of the form X = <value> + X are supported.");
-              addRawUpdate(operations, key, new Operation.Prepend(t));
-          }
-      }
-    | c=cident sig=('+' | '-') t=term
-      {
-          if (!key.equals(c))
-              addRecognitionError("Only expressions of the form X = X " + $sig.text + "<value> are supported.");
-          addRawUpdate(operations, key, $sig.text.equals("+") ? new Operation.Addition(t) : new Operation.Substraction(t));
-      }
-    | c=cident i=INTEGER
-      {
-          // Note that this production *is* necessary because X = X - 3 will in fact be lexed as [ X, '=', X, INTEGER].
-          if (!key.equals(c))
-              // We don't yet allow a '+' in front of an integer, but we could in the future really, so let's be future-proof in our error message
-              addRecognitionError("Only expressions of the form X = X " + ($i.text.charAt(0) == '-' ? '-' : '+') + " <value> are supported.");
-          addRawUpdate(operations, key, new Operation.Addition(Constants.Literal.integer($i.text)));
-      }
-    ;
-
-specializedColumnOperation[List<Pair<ColumnIdentifier.Raw, Operation.RawUpdate>> operations, ColumnIdentifier.Raw key, Term.Raw k]
-    : '=' t=term
-      {
-          addRawUpdate(operations, key, new Operation.SetElement(k, t));
-      }
-    ;
-
-columnCondition[List<Pair<ColumnIdentifier.Raw, ColumnCondition.Raw>> conditions]
-    // Note: we'll reject duplicates later
-    : key=cident
-        ( op=relationType t=term { conditions.add(Pair.create(key, ColumnCondition.Raw.simpleCondition(t, op))); }
-        | K_IN
-            ( values=singleColumnInValues { conditions.add(Pair.create(key, ColumnCondition.Raw.simpleInCondition(values))); }
-            | marker=inMarker { conditions.add(Pair.create(key, ColumnCondition.Raw.simpleInCondition(marker))); }
-            )
-        | '[' element=term ']'
-            ( op=relationType t=term { conditions.add(Pair.create(key, ColumnCondition.Raw.collectionCondition(t, element, op))); }
-            | K_IN
-                ( values=singleColumnInValues { conditions.add(Pair.create(key, ColumnCondition.Raw.collectionInCondition(element, values))); }
-                | marker=inMarker { conditions.add(Pair.create(key, ColumnCondition.Raw.collectionInCondition(element, marker))); }
-                )
-            )
-        )
-    ;
-
-properties[PropertyDefinitions props]
-    : property[props] (K_AND property[props])*
-    ;
-
-property[PropertyDefinitions props]
-    : k=noncol_ident '=' simple=propertyValue { try { $props.addProperty(k.toString(), simple); } catch (SyntaxException e) { addRecognitionError(e.getMessage()); } }
-    | k=noncol_ident '=' map=mapLiteral { try { $props.addProperty(k.toString(), convertPropertyMap(map)); } catch (SyntaxException e) { addRecognitionError(e.getMessage()); } }
-    ;
-
-propertyValue returns [String str]
-    : c=constant           { $str = c.getRawText(); }
-    | u=unreserved_keyword { $str = u; }
-    ;
-
-relationType returns [Operator op]
-    : '='  { $op = Operator.EQ; }
-    | '<'  { $op = Operator.LT; }
-    | '<=' { $op = Operator.LTE; }
-    | '>'  { $op = Operator.GT; }
-    | '>=' { $op = Operator.GTE; }
-    | '!=' { $op = Operator.NEQ; }
-    ;
-
-relation[WhereClause.Builder clauses]
-    : name=cident type=relationType t=term { $clauses.add(new SingleColumnRelation(name, type, t)); }
-    | name=cident K_IS K_NOT K_NULL { $clauses.add(new SingleColumnRelation(name, Operator.IS_NOT, Constants.NULL_LITERAL)); }
-    | K_TOKEN l=tupleOfIdentifiers type=relationType t=term
-        { $clauses.add(new TokenRelation(l, type, t)); }
-    | name=cident K_IN marker=inMarker
-        { $clauses.add(new SingleColumnRelation(name, Operator.IN, marker)); }
-    | name=cident K_IN inValues=singleColumnInValues
-        { $clauses.add(SingleColumnRelation.createInRelation($name.id, inValues)); }
-    | name=cident K_CONTAINS { Operator rt = Operator.CONTAINS; } (K_KEY { rt = Operator.CONTAINS_KEY; })?
-        t=term { $clauses.add(new SingleColumnRelation(name, rt, t)); }
-    | name=cident '[' key=term ']' type=relationType t=term { $clauses.add(new SingleColumnRelation(name, key, type, t)); }
-    | ids=tupleOfIdentifiers
-      ( K_IN
-          ( '(' ')'
-              { $clauses.add(MultiColumnRelation.createInRelation(ids, new ArrayList<Tuples.Literal>())); }
-          | tupleInMarker=inMarkerForTuple /* (a, b, c) IN ? */
-              { $clauses.add(MultiColumnRelation.createSingleMarkerInRelation(ids, tupleInMarker)); }
-          | literals=tupleOfTupleLiterals /* (a, b, c) IN ((1, 2, 3), (4, 5, 6), ...) */
-              {
-                  $clauses.add(MultiColumnRelation.createInRelation(ids, literals));
-              }
-          | markers=tupleOfMarkersForTuples /* (a, b, c) IN (?, ?, ...) */
-              { $clauses.add(MultiColumnRelation.createInRelation(ids, markers)); }
-          )
-      | type=relationType literal=tupleLiteral /* (a, b, c) > (1, 2, 3) or (a, b, c) > (?, ?, ?) */
-          {
-              $clauses.add(MultiColumnRelation.createNonInRelation(ids, type, literal));
-          }
-      | type=relationType tupleMarker=markerForTuple /* (a, b, c) >= ? */
-          { $clauses.add(MultiColumnRelation.createNonInRelation(ids, type, tupleMarker)); }
-      )
-    | '(' relation[$clauses] ')'
-    ;
-
-inMarker returns [AbstractMarker.INRaw marker]
-    : QMARK { $marker = newINBindVariables(null); }
-    | ':' name=noncol_ident { $marker = newINBindVariables(name); }
-    ;
-
-tupleOfIdentifiers returns [List<ColumnIdentifier.Raw> ids]
-    @init { $ids = new ArrayList<ColumnIdentifier.Raw>(); }
-    : '(' n1=cident { $ids.add(n1); } (',' ni=cident { $ids.add(ni); })* ')'
-    ;
-
-singleColumnInValues returns [List<Term.Raw> terms]
-    @init { $terms = new ArrayList<Term.Raw>(); }
-    : '(' ( t1 = term { $terms.add(t1); } (',' ti=term { $terms.add(ti); })* )? ')'
-    ;
-
-tupleOfTupleLiterals returns [List<Tuples.Literal> literals]
-    @init { $literals = new ArrayList<>(); }
-    : '(' t1=tupleLiteral { $literals.add(t1); } (',' ti=tupleLiteral { $literals.add(ti); })* ')'
-    ;
-
-markerForTuple returns [Tuples.Raw marker]
-    : QMARK { $marker = newTupleBindVariables(null); }
-    | ':' name=noncol_ident { $marker = newTupleBindVariables(name); }
-    ;
-
-tupleOfMarkersForTuples returns [List<Tuples.Raw> markers]
-    @init { $markers = new ArrayList<Tuples.Raw>(); }
-    : '(' m1=markerForTuple { $markers.add(m1); } (',' mi=markerForTuple { $markers.add(mi); })* ')'
-    ;
-
-inMarkerForTuple returns [Tuples.INRaw marker]
-    : QMARK { $marker = newTupleINBindVariables(null); }
-    | ':' name=noncol_ident { $marker = newTupleINBindVariables(name); }
-    ;
-
-comparatorType returns [CQL3Type.Raw t]
-    : n=native_type     { $t = CQL3Type.Raw.from(n); }
-    | c=collection_type { $t = c; }
-    | tt=tuple_type     { $t = tt; }
-    | id=userTypeName   { $t = CQL3Type.Raw.userType(id); }
-    | K_FROZEN '<' f=comparatorType '>'
-      {
-        try {
-            $t = CQL3Type.Raw.frozen(f);
-        } catch (InvalidRequestException e) {
-            addRecognitionError(e.getMessage());
-        }
-      }
-    | s=STRING_LITERAL
-      {
-        try {
-            $t = CQL3Type.Raw.from(new CQL3Type.Custom($s.text));
-        } catch (SyntaxException e) {
-            addRecognitionError("Cannot parse type " + $s.text + ": " + e.getMessage());
-        } catch (ConfigurationException e) {
-            addRecognitionError("Error setting type " + $s.text + ": " + e.getMessage());
-        }
-      }
-    ;
-
-native_type returns [CQL3Type t]
-    : K_ASCII     { $t = CQL3Type.Native.ASCII; }
-    | K_BIGINT    { $t = CQL3Type.Native.BIGINT; }
-    | K_BLOB      { $t = CQL3Type.Native.BLOB; }
-    | K_BOOLEAN   { $t = CQL3Type.Native.BOOLEAN; }
-    | K_COUNTER   { $t = CQL3Type.Native.COUNTER; }
-    | K_DECIMAL   { $t = CQL3Type.Native.DECIMAL; }
-    | K_DOUBLE    { $t = CQL3Type.Native.DOUBLE; }
-    | K_FLOAT     { $t = CQL3Type.Native.FLOAT; }
-    | K_INET      { $t = CQL3Type.Native.INET;}
-    | K_INT       { $t = CQL3Type.Native.INT; }
-    | K_SMALLINT  { $t = CQL3Type.Native.SMALLINT; }
-    | K_TEXT      { $t = CQL3Type.Native.TEXT; }
-    | K_TIMESTAMP { $t = CQL3Type.Native.TIMESTAMP; }
-    | K_TINYINT   { $t = CQL3Type.Native.TINYINT; }
-    | K_UUID      { $t = CQL3Type.Native.UUID; }
-    | K_VARCHAR   { $t = CQL3Type.Native.VARCHAR; }
-    | K_VARINT    { $t = CQL3Type.Native.VARINT; }
-    | K_TIMEUUID  { $t = CQL3Type.Native.TIMEUUID; }
-    | K_DATE      { $t = CQL3Type.Native.DATE; }
-    | K_TIME      { $t = CQL3Type.Native.TIME; }
-    ;
-
-collection_type returns [CQL3Type.Raw pt]
-    : K_MAP  '<' t1=comparatorType ',' t2=comparatorType '>'
-        {
-            // if we can't parse either t1 or t2, antlr will "recover" and we may have t1 or t2 null.
-            if (t1 != null && t2 != null)
-                $pt = CQL3Type.Raw.map(t1, t2);
-        }
-    | K_LIST '<' t=comparatorType '>'
-        { if (t != null) $pt = CQL3Type.Raw.list(t); }
-    | K_SET  '<' t=comparatorType '>'
-        { if (t != null) $pt = CQL3Type.Raw.set(t); }
-    ;
-
-tuple_type returns [CQL3Type.Raw t]
-    : K_TUPLE '<' { List<CQL3Type.Raw> types = new ArrayList<>(); }
-         t1=comparatorType { types.add(t1); } (',' tn=comparatorType { types.add(tn); })*
-      '>' { $t = CQL3Type.Raw.tuple(types); }
-    ;
-
-username
-    : IDENT
-    | STRING_LITERAL
-    | QUOTED_NAME { addRecognitionError("Quoted strings are are not supported for user names and USER is deprecated, please use ROLE");}
-    ;
-
-// Basically the same as cident, but we need to exlude existing CQL3 types
-// (which for some reason are not reserved otherwise)
-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 | K_KEY) { $str = $k.text; }
-    ;
-
-unreserved_function_keyword returns [String str]
-    : u=basic_unreserved_keyword { $str = u; }
-    | t=native_type              { $str = t.toString(); }
-    ;
-
-basic_unreserved_keyword returns [String str]
-    : k=( K_KEYS
-        | K_AS
-        | K_CLUSTERING
-        | K_COMPACT
-        | K_STORAGE
-        | K_TYPE
-        | K_VALUES
-        | K_MAP
-        | K_LIST
-        | K_FILTERING
-        | K_PERMISSION
-        | K_PERMISSIONS
-        | K_KEYSPACES
-        | K_ALL
-        | K_USER
-        | K_USERS
-        | K_ROLE
-        | K_ROLES
-        | K_SUPERUSER
-        | K_NOSUPERUSER
-        | K_LOGIN
-        | K_NOLOGIN
-        | K_OPTIONS
-        | K_PASSWORD
-        | K_EXISTS
-        | K_CUSTOM
-        | K_TRIGGER
-        | K_DISTINCT
-        | K_CONTAINS
-        | K_STATIC
-        | K_FROZEN
-        | K_TUPLE
-        | K_FUNCTION
-        | K_FUNCTIONS
-        | K_AGGREGATE
-        | K_SFUNC
-        | K_STYPE
-        | K_FINALFUNC
-        | K_INITCOND
-        | K_RETURNS
-        | K_LANGUAGE
-        | K_JSON
-        | K_CALLED
-        | K_INPUT
-        ) { $str = $k.text; }
-    ;
-
-// Case-insensitive keywords
-// When adding a new reserved keyword, add entry to o.a.c.cql3.ReservedKeywords as well
-// When adding a new unreserved keyword, add entry to list above
-K_SELECT:      S E L E C T;
-K_FROM:        F R O M;
-K_AS:          A S;
-K_WHERE:       W H E R E;
-K_AND:         A N D;
-K_KEY:         K E Y;
-K_KEYS:        K E Y S;
-K_ENTRIES:     E N T R I E S;
-K_FULL:        F U L L;
-K_INSERT:      I N S E R T;
-K_UPDATE:      U P D A T E;
-K_WITH:        W I T H;
-K_LIMIT:       L I M I T;
-K_USING:       U S I N G;
-K_USE:         U S E;
-K_DISTINCT:    D I S T I N C T;
-K_COUNT:       C O U N T;
-K_SET:         S E T;
-K_BEGIN:       B E G I N;
-K_UNLOGGED:    U N L O G G E D;
-K_BATCH:       B A T C H;
-K_APPLY:       A P P L Y;
-K_TRUNCATE:    T R U N C A T E;
-K_DELETE:      D E L E T E;
-K_IN:          I N;
-K_CREATE:      C R E A T E;
-K_KEYSPACE:    ( K E Y S P A C E
-                 | S C H E M A );
-K_KEYSPACES:   K E Y S P A C E S;
-K_COLUMNFAMILY:( C O L U M N F A M I L Y
-                 | T A B L E );
-K_MATERIALIZED:M A T E R I A L I Z E D;
-K_VIEW:        V I E W;
-K_INDEX:       I N D E X;
-K_CUSTOM:      C U S T O M;
-K_ON:          O N;
-K_TO:          T O;
-K_DROP:        D R O P;
-K_PRIMARY:     P R I M A R Y;
-K_INTO:        I N T O;
-K_VALUES:      V A L U E S;
-K_TIMESTAMP:   T I M E S T A M P;
-K_TTL:         T T L;
-K_ALTER:       A L T E R;
-K_RENAME:      R E N A M E;
-K_ADD:         A D D;
-K_TYPE:        T Y P E;
-K_COMPACT:     C O M P A C T;
-K_STORAGE:     S T O R A G E;
-K_ORDER:       O R D E R;
-K_BY:          B Y;
-K_ASC:         A S C;
-K_DESC:        D E S C;
-K_ALLOW:       A L L O W;
-K_FILTERING:   F I L T E R I N G;
-K_IF:          I F;
-K_IS:          I S;
-K_CONTAINS:    C O N T A I N S;
-
-K_GRANT:       G R A N T;
-K_ALL:         A L L;
-K_PERMISSION:  P E R M I S S I O N;
-K_PERMISSIONS: P E R M I S S I O N S;
-K_OF:          O F;
-K_REVOKE:      R E V O K E;
-K_MODIFY:      M O D I F Y;
-K_AUTHORIZE:   A U T H O R I Z E;
-K_DESCRIBE:    D E S C R I B E;
-K_EXECUTE:     E X E C U T E;
-K_NORECURSIVE: N O R E C U R S I V E;
-
-K_USER:        U S E R;
-K_USERS:       U S E R S;
-K_ROLE:        R O L E;
-K_ROLES:       R O L E S;
-K_SUPERUSER:   S U P E R U S E R;
-K_NOSUPERUSER: N O S U P E R U S E R;
-K_PASSWORD:    P A S S W O R D;
-K_LOGIN:       L O G I N;
-K_NOLOGIN:     N O L O G I N;
-K_OPTIONS:     O P T I O N S;
-
-K_CLUSTERING:  C L U S T E R I N G;
-K_ASCII:       A S C I I;
-K_BIGINT:      B I G I N T;
-K_BLOB:        B L O B;
-K_BOOLEAN:     B O O L E A N;
-K_COUNTER:     C O U N T E R;
-K_DECIMAL:     D E C I M A L;
-K_DOUBLE:      D O U B L E;
-K_FLOAT:       F L O A T;
-K_INET:        I N E T;
-K_INT:         I N T;
-K_SMALLINT:    S M A L L I N T;
-K_TINYINT:     T I N Y I N T;
-K_TEXT:        T E X T;
-K_UUID:        U U I D;
-K_VARCHAR:     V A R C H A R;
-K_VARINT:      V A R I N T;
-K_TIMEUUID:    T I M E U U I D;
-K_TOKEN:       T O K E N;
-K_WRITETIME:   W R I T E T I M E;
-K_DATE:        D A T E;
-K_TIME:        T I M E;
-
-K_NULL:        N U L L;
-K_NOT:         N O T;
-K_EXISTS:      E X I S T S;
-
-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;
-K_FROZEN:      F R O Z E N;
-
-K_FUNCTION:    F U N C T I O N;
-K_FUNCTIONS:   F U N C T I O N S;
-K_AGGREGATE:   A G G R E G A T E;
-K_SFUNC:       S F U N C;
-K_STYPE:       S T Y P E;
-K_FINALFUNC:   F I N A L F U N C;
-K_INITCOND:    I N I T C O N D;
-K_RETURNS:     R E T U R N S;
-K_CALLED:      C A L L E D;
-K_INPUT:       I N P U T;
-K_LANGUAGE:    L A N G U A G E;
-K_OR:          O R;
-K_REPLACE:     R E P L A C E;
-
-K_JSON:        J S O N;
-
-// Case-insensitive alpha characters
-fragment A: ('a'|'A');
-fragment B: ('b'|'B');
-fragment C: ('c'|'C');
-fragment D: ('d'|'D');
-fragment E: ('e'|'E');
-fragment F: ('f'|'F');
-fragment G: ('g'|'G');
-fragment H: ('h'|'H');
-fragment I: ('i'|'I');
-fragment J: ('j'|'J');
-fragment K: ('k'|'K');
-fragment L: ('l'|'L');
-fragment M: ('m'|'M');
-fragment N: ('n'|'N');
-fragment O: ('o'|'O');
-fragment P: ('p'|'P');
-fragment Q: ('q'|'Q');
-fragment R: ('r'|'R');
-fragment S: ('s'|'S');
-fragment T: ('t'|'T');
-fragment U: ('u'|'U');
-fragment V: ('v'|'V');
-fragment W: ('w'|'W');
-fragment X: ('x'|'X');
-fragment Y: ('y'|'Y');
-fragment Z: ('z'|'Z');
-
-STRING_LITERAL
-    @init{
-        StringBuilder txt = new StringBuilder(); // temporary to build pg-style-string
-    }
-    @after{ setText(txt.toString()); }
-    :
-      /* pg-style string literal */
-      (
-        '\$' '\$'
-        ( /* collect all input until '$$' is reached again */
-          {  (input.size() - input.index() > 1)
-               && !"$$".equals(input.substring(input.index(), input.index() + 1)) }?
-             => c=. { txt.appendCodePoint(c); }
-        )*
-        '\$' '\$'
-      )
-      |
-      /* conventional quoted string literal */
-      (
-        '\'' (c=~('\'') { txt.appendCodePoint(c);} | '\'' '\'' { txt.appendCodePoint('\''); })* '\''
-      )
-    ;
-
-EMPTY_QUOTED_NAME
-    : '\"' '\"'
-    ;
-
-QUOTED_NAME
-    @init{ StringBuilder b = new StringBuilder(); }
-    @after{ setText(b.toString()); }
-    : '\"' (c=~('\"') { b.appendCodePoint(c); } | '\"' '\"' { b.appendCodePoint('\"'); })+ '\"'
-    ;
-
-fragment DIGIT
-    : '0'..'9'
-    ;
-
-fragment LETTER
-    : ('A'..'Z' | 'a'..'z')
-    ;
-
-fragment HEX
-    : ('A'..'F' | 'a'..'f' | '0'..'9')
-    ;
-
-fragment EXPONENT
-    : E ('+' | '-')? DIGIT+
-    ;
-
-INTEGER
-    : '-'? DIGIT+
-    ;
-
-QMARK
-    : '?'
-    ;
-
-/*
- * Normally a lexer only emits one token at a time, but ours is tricked out
- * to support multiple (see @lexer::members near the top of the grammar).
- */
-FLOAT
-    : INTEGER EXPONENT
-    | INTEGER '.' DIGIT* EXPONENT?
-    ;
-
-/*
- * This has to be before IDENT so it takes precendence over it.
- */
-BOOLEAN
-    : T R U E | F A L S E
-    ;
-
-IDENT
-    : LETTER (LETTER | DIGIT | '_')*
-    ;
-
-HEXNUMBER
-    : '0' X HEX*
-    ;
-
-UUID
-    : HEX HEX HEX HEX HEX HEX HEX HEX '-'
-      HEX HEX HEX HEX '-'
-      HEX HEX HEX HEX '-'
-      HEX HEX HEX HEX '-'
-      HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX
-    ;
-
-WS
-    : (' ' | '\t' | '\n' | '\r')+ { $channel = HIDDEN; }
-    ;
-
-COMMENT
-    : ('--' | '//') .* ('\n'|'\r') { $channel = HIDDEN; }
-    ;
-
-MULTILINE_COMMENT
-    : '/*' .* '*/' { $channel = HIDDEN; }
-    ;
diff --git a/src/java/org/apache/cassandra/cql3/CustomPayloadMirroringQueryHandler.java b/src/java/org/apache/cassandra/cql3/CustomPayloadMirroringQueryHandler.java
index 02a6df9..643c54b 100644
--- a/src/java/org/apache/cassandra/cql3/CustomPayloadMirroringQueryHandler.java
+++ b/src/java/org/apache/cassandra/cql3/CustomPayloadMirroringQueryHandler.java
@@ -38,9 +38,10 @@
     public ResultMessage process(String query,
                                  QueryState state,
                                  QueryOptions options,
-                                 Map<String, ByteBuffer> customPayload)
+                                 Map<String, ByteBuffer> customPayload,
+                                 long queryStartNanoTime)
     {
-        ResultMessage result = queryProcessor.process(query, state, options, customPayload);
+        ResultMessage result = queryProcessor.process(query, state, options, customPayload, queryStartNanoTime);
         result.setCustomPayload(customPayload);
         return result;
     }
@@ -65,9 +66,10 @@
     public ResultMessage processPrepared(CQLStatement statement,
                                          QueryState state,
                                          QueryOptions options,
-                                         Map<String, ByteBuffer> customPayload)
+                                         Map<String, ByteBuffer> customPayload,
+                                         long queryStartNanoTime)
     {
-        ResultMessage result = queryProcessor.processPrepared(statement, state, options, customPayload);
+        ResultMessage result = queryProcessor.processPrepared(statement, state, options, customPayload, queryStartNanoTime);
         result.setCustomPayload(customPayload);
         return result;
     }
@@ -75,9 +77,10 @@
     public ResultMessage processBatch(BatchStatement statement,
                                       QueryState state,
                                       BatchQueryOptions options,
-                                      Map<String, ByteBuffer> customPayload)
+                                      Map<String, ByteBuffer> customPayload,
+                                      long queryStartNanoTime)
     {
-        ResultMessage result = queryProcessor.processBatch(statement, state, options, customPayload);
+        ResultMessage result = queryProcessor.processBatch(statement, state, options, customPayload, queryStartNanoTime);
         result.setCustomPayload(customPayload);
         return result;
     }
diff --git a/src/java/org/apache/cassandra/cql3/Duration.java b/src/java/org/apache/cassandra/cql3/Duration.java
new file mode 100644
index 0000000..48f8850
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/Duration.java
@@ -0,0 +1,590 @@
+/*
+ * 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.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.google.common.base.Objects;
+
+import org.apache.cassandra.serializers.MarshalException;
+
+import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue;
+import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
+
+/**
+ * Represents a duration. A durations store separately months, days, and seconds due to the fact that
+ * the number of days in a month varies, and a day can have 23 or 25 hours if a daylight saving is involved.
+ */
+public final class Duration
+{
+    public static final long NANOS_PER_MICRO = 1000L;
+    public static final long NANOS_PER_MILLI = 1000 * NANOS_PER_MICRO;
+    public static final long NANOS_PER_SECOND = 1000 * NANOS_PER_MILLI;
+    public static final long NANOS_PER_MINUTE = 60 * NANOS_PER_SECOND;
+    public static final long NANOS_PER_HOUR = 60 * NANOS_PER_MINUTE;
+    public static final int DAYS_PER_WEEK = 7;
+    public static final int MONTHS_PER_YEAR = 12;
+
+    /**
+     * The Regexp used to parse the duration provided as String.
+     */
+    private static final Pattern STANDARD_PATTERN =
+            Pattern.compile("\\G(\\d+)(y|Y|mo|MO|mO|Mo|w|W|d|D|h|H|s|S|ms|MS|mS|Ms|us|US|uS|Us|µs|µS|ns|NS|nS|Ns|m|M)");
+
+    /**
+     * The Regexp used to parse the duration when provided in the ISO 8601 format with designators.
+     */
+    private static final Pattern ISO8601_PATTERN =
+            Pattern.compile("P((\\d+)Y)?((\\d+)M)?((\\d+)D)?(T((\\d+)H)?((\\d+)M)?((\\d+)S)?)?");
+
+    /**
+     * The Regexp used to parse the duration when provided in the ISO 8601 format with designators.
+     */
+    private static final Pattern ISO8601_WEEK_PATTERN = Pattern.compile("P(\\d+)W");
+
+    /**
+     * The Regexp used to parse the duration when provided in the ISO 8601 alternative format.
+     */
+    private static final Pattern ISO8601_ALTERNATIVE_PATTERN =
+            Pattern.compile("P(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})");
+
+    /**
+     * The number of months.
+     */
+    private final int months;
+
+    /**
+     * The number of days.
+     */
+    private final int days;
+
+    /**
+     * The number of nanoseconds.
+     */
+    private final long nanoseconds;
+
+    /**
+     * Creates a duration. A duration can be negative.
+     * In this case all the non zero values must be negatives.
+     *
+     * @param months the number of months
+     * @param days the number of days
+     * @param nanoseconds the number of nanoseconds
+     */
+    private Duration(int months, int days, long nanoseconds)
+    {
+        // Makes sure that all the values are negatives if one of them is
+        assert (months >= 0 && days >= 0 && nanoseconds >= 0)
+            || ((months <= 0 && days <=0 && nanoseconds <=0));
+
+        this.months = months;
+        this.days = days;
+        this.nanoseconds = nanoseconds;
+    }
+
+    public static Duration newInstance(int months, int days, long nanoseconds)
+    {
+        return new Duration(months, days, nanoseconds);
+    }
+
+    /**
+     * Converts a <code>String</code> into a duration.
+     * <p>The accepted formats are:
+     * <ul>
+     * <li>multiple digits followed by a time unit like: 12h30m where the time unit can be:
+     *   <ul>
+     *      <li>{@code y}: years</li>
+     *      <li>{@code m}: months</li>
+     *      <li>{@code w}: weeks</li>
+     *      <li>{@code d}: days</li>
+     *      <li>{@code h}: hours</li>
+     *      <li>{@code m}: minutes</li>
+     *      <li>{@code s}: seconds</li>
+     *      <li>{@code ms}: milliseconds</li>
+     *      <li>{@code us} or {@code µs}: microseconds</li>
+     *      <li>{@code ns}: nanoseconds</li>
+     *   </ul>
+     * </li>
+     * <li>ISO 8601 format:  P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W</li>
+     * <li>ISO 8601 alternative format: P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]</li>
+     * </ul>
+     *
+     * @param input the <code>String</code> to convert
+     * @return a number of nanoseconds
+     */
+    public static Duration from(String input)
+    {
+        boolean isNegative = input.startsWith("-");
+        String source = isNegative ? input.substring(1) : input;
+
+        if (source.startsWith("P"))
+        {
+            if (source.endsWith("W"))
+                return parseIso8601WeekFormat(isNegative, source);
+
+            if (source.contains("-"))
+                return parseIso8601AlternativeFormat(isNegative, source);
+
+            return parseIso8601Format(isNegative, source);
+        }
+        return parseStandardFormat(isNegative, source);
+    }
+
+    private static Duration parseIso8601Format(boolean isNegative, String source)
+    {
+        Matcher matcher = ISO8601_PATTERN.matcher(source);
+        if (!matcher.matches())
+            throw invalidRequest("Unable to convert '%s' to a duration", source);
+
+        Builder builder = new Builder(isNegative);
+        if (matcher.group(1) != null)
+            builder.addYears(groupAsLong(matcher, 2));
+
+        if (matcher.group(3) != null)
+            builder.addMonths(groupAsLong(matcher, 4));
+
+        if (matcher.group(5) != null)
+            builder.addDays(groupAsLong(matcher, 6));
+
+        // Checks if the String contains time information
+        if (matcher.group(7) != null)
+        {
+            if (matcher.group(8) != null)
+                builder.addHours(groupAsLong(matcher, 9));
+
+            if (matcher.group(10) != null)
+                builder.addMinutes(groupAsLong(matcher, 11));
+
+            if (matcher.group(12) != null)
+                builder.addSeconds(groupAsLong(matcher, 13));
+        }
+        return builder.build();
+    }
+
+    private static Duration parseIso8601AlternativeFormat(boolean isNegative, String source)
+    {
+        Matcher matcher = ISO8601_ALTERNATIVE_PATTERN.matcher(source);
+        if (!matcher.matches())
+            throw invalidRequest("Unable to convert '%s' to a duration", source);
+
+        return new Builder(isNegative).addYears(groupAsLong(matcher, 1))
+                                      .addMonths(groupAsLong(matcher, 2))
+                                      .addDays(groupAsLong(matcher, 3))
+                                      .addHours(groupAsLong(matcher, 4))
+                                      .addMinutes(groupAsLong(matcher, 5))
+                                      .addSeconds(groupAsLong(matcher, 6))
+                                      .build();
+    }
+
+    private static Duration parseIso8601WeekFormat(boolean isNegative, String source)
+    {
+        Matcher matcher = ISO8601_WEEK_PATTERN.matcher(source);
+        if (!matcher.matches())
+            throw invalidRequest("Unable to convert '%s' to a duration", source);
+
+        return new Builder(isNegative).addWeeks(groupAsLong(matcher, 1))
+                                      .build();
+    }
+
+    private static Duration parseStandardFormat(boolean isNegative, String source)
+    {
+        Matcher matcher = STANDARD_PATTERN.matcher(source);
+        if (!matcher.find())
+            throw invalidRequest("Unable to convert '%s' to a duration", source);
+
+        Builder builder = new Builder(isNegative);
+        boolean done = false;
+
+        do
+        {
+            long number = groupAsLong(matcher, 1);
+            String symbol = matcher.group(2);
+            add(builder, number, symbol);
+            done = matcher.end() == source.length();
+        }
+        while (matcher.find());
+
+        if (!done)
+            throw invalidRequest("Unable to convert '%s' to a duration", source);
+
+        return builder.build();
+    }
+
+    private static long groupAsLong(Matcher matcher, int group)
+    {
+        return Long.parseLong(matcher.group(group));
+    }
+
+    private static Builder add(Builder builder, long number, String symbol)
+    {
+        switch (symbol.toLowerCase())
+        {
+            case "y": return builder.addYears(number);
+            case "mo": return builder.addMonths(number);
+            case "w": return builder.addWeeks(number);
+            case "d": return builder.addDays(number);
+            case "h": return builder.addHours(number);
+            case "m": return builder.addMinutes(number);
+            case "s": return builder.addSeconds(number);
+            case "ms": return builder.addMillis(number);
+            case "us":
+            case "µs": return builder.addMicros(number);
+            case "ns": return builder.addNanos(number);
+        }
+        throw new MarshalException(String.format("Unknown duration symbol '%s'", symbol));
+    }
+
+    public int getMonths()
+    {
+        return months;
+    }
+
+    public int getDays()
+    {
+        return days;
+    }
+
+    public long getNanoseconds()
+    {
+        return nanoseconds;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hashCode(days, months, nanoseconds);
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+        if (!(obj instanceof Duration))
+            return false;
+
+        Duration other = (Duration) obj;
+        return days == other.days
+                && months == other.months
+                && nanoseconds == other.nanoseconds;
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder builder = new StringBuilder();
+
+        if (months < 0 || days < 0 || nanoseconds < 0)
+            builder.append('-');
+
+        long remainder = append(builder, Math.abs(months), MONTHS_PER_YEAR, "y");
+        append(builder, remainder, 1, "mo");
+
+        append(builder, Math.abs(days), 1, "d");
+
+        if (nanoseconds != 0)
+        {
+            remainder = append(builder, Math.abs(nanoseconds), NANOS_PER_HOUR, "h");
+            remainder = append(builder, remainder, NANOS_PER_MINUTE, "m");
+            remainder = append(builder, remainder, NANOS_PER_SECOND, "s");
+            remainder = append(builder, remainder, NANOS_PER_MILLI, "ms");
+            remainder = append(builder, remainder, NANOS_PER_MICRO, "us");
+            append(builder, remainder, 1, "ns");
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Appends the result of the division to the specified builder if the dividend is not zero.
+     *
+     * @param builder the builder to append to
+     * @param dividend the dividend
+     * @param divisor the divisor
+     * @param unit the time unit to append after the result of the division
+     * @return the remainder of the division
+     */
+    private static long append(StringBuilder builder, long dividend, long divisor, String unit)
+    {
+        if (dividend == 0 || dividend < divisor)
+            return dividend;
+
+        builder.append(dividend / divisor).append(unit);
+        return dividend % divisor;
+    }
+
+    private static class Builder
+    {
+        /**
+         * {@code true} if the duration is a negative one, {@code false} otherwise.
+         */
+        private final boolean isNegative;
+
+        /**
+         * The number of months.
+         */
+        private int months;
+
+        /**
+         * The number of days.
+         */
+        private int days;
+
+        /**
+         * The number of nanoseconds.
+         */
+        private long nanoseconds;
+
+        /**
+         * We need to make sure that the values for each units are provided in order.
+         */
+        private int currentUnitIndex;
+
+        public Builder(boolean isNegative)
+        {
+            this.isNegative = isNegative;
+        }
+
+        /**
+         * Adds the specified amount of years.
+         *
+         * @param numberOfYears the number of years to add.
+         * @return this {@code Builder}
+         */
+        public Builder addYears(long numberOfYears)
+        {
+            validateOrder(1);
+            validateMonths(numberOfYears, MONTHS_PER_YEAR);
+            months += numberOfYears * MONTHS_PER_YEAR;
+            return this;
+        }
+
+        /**
+         * Adds the specified amount of months.
+         *
+         * @param numberOfMonths the number of months to add.
+         * @return this {@code Builder}
+         */
+        public Builder addMonths(long numberOfMonths)
+        {
+            validateOrder(2);
+            validateMonths(numberOfMonths, 1);
+            months += numberOfMonths;
+            return this;
+        }
+
+        /**
+         * Adds the specified amount of weeks.
+         *
+         * @param numberOfWeeks the number of weeks to add.
+         * @return this {@code Builder}
+         */
+        public Builder addWeeks(long numberOfWeeks)
+        {
+            validateOrder(3);
+            validateDays(numberOfWeeks, DAYS_PER_WEEK);
+            days += numberOfWeeks * DAYS_PER_WEEK;
+            return this;
+        }
+
+        /**
+         * Adds the specified amount of days.
+         *
+         * @param numberOfDays the number of days to add.
+         * @return this {@code Builder}
+         */
+        public Builder addDays(long numberOfDays)
+        {
+            validateOrder(4);
+            validateDays(numberOfDays, 1);
+            days += numberOfDays;
+            return this;
+        }
+
+        /**
+         * Adds the specified amount of hours.
+         *
+         * @param numberOfHours the number of hours to add.
+         * @return this {@code Builder}
+         */
+        public Builder addHours(long numberOfHours)
+        {
+            validateOrder(5);
+            validateNanos(numberOfHours, NANOS_PER_HOUR);
+            nanoseconds += numberOfHours * NANOS_PER_HOUR;
+            return this;
+        }
+
+        /**
+         * Adds the specified amount of minutes.
+         *
+         * @param numberOfMinutes the number of minutes to add.
+         * @return this {@code Builder}
+         */
+        public Builder addMinutes(long numberOfMinutes)
+        {
+            validateOrder(6);
+            validateNanos(numberOfMinutes, NANOS_PER_MINUTE);
+            nanoseconds += numberOfMinutes * NANOS_PER_MINUTE;
+            return this;
+        }
+
+        /**
+         * Adds the specified amount of seconds.
+         *
+         * @param numberOfSeconds the number of seconds to add.
+         * @return this {@code Builder}
+         */
+        public Builder addSeconds(long numberOfSeconds)
+        {
+            validateOrder(7);
+            validateNanos(numberOfSeconds, NANOS_PER_SECOND);
+            nanoseconds += numberOfSeconds * NANOS_PER_SECOND;
+            return this;
+        }
+
+        /**
+         * Adds the specified amount of milliseconds.
+         *
+         * @param numberOfMillis the number of milliseconds to add.
+         * @return this {@code Builder}
+         */
+        public Builder addMillis(long numberOfMillis)
+        {
+            validateOrder(8);
+            validateNanos(numberOfMillis, NANOS_PER_MILLI);
+            nanoseconds += numberOfMillis * NANOS_PER_MILLI;
+            return this;
+        }
+
+        /**
+         * Adds the specified amount of microseconds.
+         *
+         * @param numberOfMicros the number of microseconds to add.
+         * @return this {@code Builder}
+         */
+        public Builder addMicros(long numberOfMicros)
+        {
+            validateOrder(9);
+            validateNanos(numberOfMicros, NANOS_PER_MICRO);
+            nanoseconds += numberOfMicros * NANOS_PER_MICRO;
+            return this;
+        }
+
+        /**
+         * Adds the specified amount of nanoseconds.
+         *
+         * @param numberOfNanos the number of nanoseconds to add.
+         * @return this {@code Builder}
+         */
+        public Builder addNanos(long numberOfNanos)
+        {
+            validateOrder(10);
+            validateNanos(numberOfNanos, 1);
+            nanoseconds += numberOfNanos;
+            return this;
+        }
+
+        /**
+         * Validates that the total number of months can be stored.
+         * @param units the number of units that need to be added
+         * @param monthsPerUnit the number of days per unit
+         */
+        private void validateMonths(long units, int monthsPerUnit)
+        {
+            validate(units, (Integer.MAX_VALUE - months) / monthsPerUnit, "months");
+        }
+
+        /**
+         * Validates that the total number of days can be stored.
+         * @param units the number of units that need to be added
+         * @param daysPerUnit the number of days per unit
+         */
+        private void validateDays(long units, int daysPerUnit)
+        {
+            validate(units, (Integer.MAX_VALUE - days) / daysPerUnit, "days");
+        }
+
+        /**
+         * Validates that the total number of nanoseconds can be stored.
+         * @param units the number of units that need to be added
+         * @param nanosPerUnit the number of nanoseconds per unit
+         */
+        private void validateNanos(long units, long nanosPerUnit)
+        {
+            validate(units, (Long.MAX_VALUE - nanoseconds) / nanosPerUnit, "nanoseconds");
+        }
+
+        /**
+         * Validates that the specified amount is less than the limit.
+         * @param units the number of units to check
+         * @param limit the limit on the number of units
+         * @param unitName the unit name
+         */
+        private void validate(long units, long limit, String unitName)
+        {
+            checkTrue(units <= limit,
+                      "Invalid duration. The total number of %s must be less or equal to %s",
+                      unitName,
+                      Integer.MAX_VALUE);
+        }
+
+        /**
+         * Validates that the duration values are added in the proper order.
+         * @param unitIndex the unit index (e.g. years=1, months=2, ...)
+         */
+        private void validateOrder(int unitIndex)
+        {
+            if (unitIndex == currentUnitIndex)
+                throw invalidRequest("Invalid duration. The %s are specified multiple times", getUnitName(unitIndex));
+
+            if (unitIndex <= currentUnitIndex)
+                throw invalidRequest("Invalid duration. The %s should be after %s",
+                      getUnitName(currentUnitIndex),
+                      getUnitName(unitIndex));
+
+            currentUnitIndex = unitIndex;
+        }
+
+        /**
+         * Returns the name of the unit corresponding to the specified index.
+         * @param unitIndex the unit index
+         * @return the name of the unit corresponding to the specified index.
+         */
+        private String getUnitName(int unitIndex)
+        {
+            switch (unitIndex)
+            {
+                case 1: return "years";
+                case 2: return "months";
+                case 3: return "weeks";
+                case 4: return "days";
+                case 5: return "hours";
+                case 6: return "minutes";
+                case 7: return "seconds";
+                case 8: return "milliseconds";
+                case 9: return "microseconds";
+                case 10: return "nanoseconds";
+                default: throw new AssertionError("unknown unit index: " + unitIndex);
+            }
+        }
+
+        public Duration build()
+        {
+            return isNegative ? new Duration(-months, -days, -nanoseconds) : new Duration(months, days, nanoseconds);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/cql3/FieldIdentifier.java b/src/java/org/apache/cassandra/cql3/FieldIdentifier.java
new file mode 100644
index 0000000..9f72fc4
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/FieldIdentifier.java
@@ -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 java.util.Locale;
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.exceptions.SyntaxException;
+import org.apache.cassandra.serializers.MarshalException;
+
+/**
+ * Identifies a field in a UDT.
+ */
+public class FieldIdentifier
+{
+    public final ByteBuffer bytes;
+
+    public FieldIdentifier(ByteBuffer bytes)
+    {
+        this.bytes = bytes;
+    }
+
+    /**
+     * Creates a {@code FieldIdentifier} from an unquoted identifier string.
+     */
+    public static FieldIdentifier forUnquoted(String text)
+    {
+        return new FieldIdentifier(convert(text.toLowerCase(Locale.US)));
+    }
+
+    /**
+     * Creates a {@code FieldIdentifier} from a quoted identifier string.
+     */
+    public static FieldIdentifier forQuoted(String text)
+    {
+        return new FieldIdentifier(convert(text));
+    }
+
+    /**
+     * Creates a {@code FieldIdentifier} from an internal string.
+     */
+    public static FieldIdentifier forInternalString(String text)
+    {
+        // If we store a field internally, we consider it as quoted, i.e. we preserve
+        // whatever case the text has.
+        return forQuoted(text);
+    }
+
+    private static ByteBuffer convert(String text)
+    {
+        try
+        {
+            return UTF8Type.instance.decompose(text);
+        }
+        catch (MarshalException e)
+        {
+            throw new SyntaxException(String.format("For field name %s: %s", text, e.getMessage()));
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        return UTF8Type.instance.compose(bytes);
+    }
+
+    @Override
+    public final int hashCode()
+    {
+        return bytes.hashCode();
+    }
+
+    @Override
+    public final boolean equals(Object o)
+    {
+        if(!(o instanceof FieldIdentifier))
+            return false;
+        FieldIdentifier that = (FieldIdentifier)o;
+        return this.bytes.equals(that.bytes);
+    }
+}
diff --git a/src/java/org/apache/cassandra/cql3/Json.java b/src/java/org/apache/cassandra/cql3/Json.java
index ab02fb6..af004a8 100644
--- a/src/java/org/apache/cassandra/cql3/Json.java
+++ b/src/java/org/apache/cassandra/cql3/Json.java
@@ -18,17 +18,17 @@
 package org.apache.cassandra.cql3;
 
 import java.io.IOException;
-import java.nio.ByteBuffer;
 import java.util.*;
 
+import com.fasterxml.jackson.core.io.JsonStringEncoder;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.functions.Function;
+import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.serializers.MarshalException;
-import org.codehaus.jackson.io.JsonStringEncoder;
-import org.codehaus.jackson.map.ObjectMapper;
 
 /** Term-related classes for INSERT JSON support. */
 public class Json
@@ -111,7 +111,7 @@
      */
     public static abstract class Prepared
     {
-        public abstract Term.Raw getRawTermForColumn(ColumnDefinition def);
+        public abstract Term.Raw getRawTermForColumn(ColumnDefinition def, boolean defaultUnset);
     }
 
     /**
@@ -126,10 +126,12 @@
             this.columnMap = columnMap;
         }
 
-        public Term.Raw getRawTermForColumn(ColumnDefinition def)
+        public Term.Raw getRawTermForColumn(ColumnDefinition def, boolean defaultUnset)
         {
             Term value = columnMap.get(def.name);
-            return value == null ? Constants.NULL_LITERAL : new ColumnValue(value);
+            return value == null
+                 ? (defaultUnset ? Constants.UNSET_LITERAL : Constants.NULL_LITERAL)
+                 : new ColumnValue(value);
         }
     }
 
@@ -147,9 +149,9 @@
             this.columns = columns;
         }
 
-        public RawDelayedColumnValue getRawTermForColumn(ColumnDefinition def)
+        public RawDelayedColumnValue getRawTermForColumn(ColumnDefinition def, boolean defaultUnset)
         {
-            return new RawDelayedColumnValue(this, def);
+            return new RawDelayedColumnValue(this, def, defaultUnset);
         }
     }
 
@@ -180,6 +182,11 @@
             return TestResult.NOT_ASSIGNABLE;
         }
 
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            return null;
+        }
+
         public String getText()
         {
             return term.toString();
@@ -193,17 +200,19 @@
     {
         private final PreparedMarker marker;
         private final ColumnDefinition column;
+        private final boolean defaultUnset;
 
-        public RawDelayedColumnValue(PreparedMarker prepared, ColumnDefinition column)
+        public RawDelayedColumnValue(PreparedMarker prepared, ColumnDefinition column, boolean defaultUnset)
         {
             this.marker = prepared;
             this.column = column;
+            this.defaultUnset = defaultUnset;
         }
 
         @Override
         public Term prepare(String keyspace, ColumnSpecification receiver) throws InvalidRequestException
         {
-            return new DelayedColumnValue(marker, column);
+            return new DelayedColumnValue(marker, column, defaultUnset);
         }
 
         @Override
@@ -212,6 +221,11 @@
             return TestResult.WEAKLY_ASSIGNABLE;
         }
 
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            return null;
+        }
+
         public String getText()
         {
             return marker.toString();
@@ -225,11 +239,13 @@
     {
         private final PreparedMarker marker;
         private final ColumnDefinition column;
+        private final boolean defaultUnset;
 
-        public DelayedColumnValue(PreparedMarker prepared, ColumnDefinition column)
+        public DelayedColumnValue(PreparedMarker prepared, ColumnDefinition column, boolean defaultUnset)
         {
             this.marker = prepared;
             this.column = column;
+            this.defaultUnset = defaultUnset;
         }
 
         @Override
@@ -248,7 +264,9 @@
         public Terminal bind(QueryOptions options) throws InvalidRequestException
         {
             Term term = options.getJsonColumnValue(marker.bindIndex, column.name, marker.columns);
-            return term == null ? null : term.bind(options);
+            return term == null
+                 ? (defaultUnset ? Constants.UNSET_VALUE : null)
+                 : term.bind(options);
         }
 
         @Override
@@ -274,10 +292,16 @@
             Map<ColumnIdentifier, Term> columnMap = new HashMap<>(expectedReceivers.size());
             for (ColumnSpecification spec : expectedReceivers)
             {
+                // We explicitely test containsKey() because the value itself can be null, and we want to distinguish an
+                // explicit null value from no value
+                if (!valueMap.containsKey(spec.name.toString()))
+                    continue;
+
                 Object parsedJsonObject = valueMap.remove(spec.name.toString());
                 if (parsedJsonObject == null)
                 {
-                    columnMap.put(spec.name, null);
+                    // This is an explicit user null
+                    columnMap.put(spec.name, Constants.NULL_VALUE);
                 }
                 else
                 {
diff --git a/src/java/org/apache/cassandra/cql3/Lists.java b/src/java/org/apache/cassandra/cql3/Lists.java
index f4b2294..b8fb825 100644
--- a/src/java/org/apache/cassandra/cql3/Lists.java
+++ b/src/java/org/apache/cassandra/cql3/Lists.java
@@ -38,7 +38,7 @@
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.serializers.CollectionSerializer;
 import org.apache.cassandra.serializers.MarshalException;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.UUIDGen;
 
@@ -129,6 +129,18 @@
             return AssignmentTestable.TestResult.testAll(keyspace, valueSpec, elements);
         }
 
+        @Override
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            for (Term.Raw term : elements)
+            {
+                AbstractType<?> type = term.getExactTypeIfKnown(keyspace);
+                if (type != null)
+                    return ListType.getInstance(type, false);
+            }
+            return null;
+        }
+
         public String getText()
         {
             return elements.stream().map(Term.Raw::getText).collect(Collectors.joining(", ", "[", "]"));
@@ -144,7 +156,7 @@
             this.elements = elements;
         }
 
-        public static Value fromSerialized(ByteBuffer value, ListType type, int version) throws InvalidRequestException
+        public static Value fromSerialized(ByteBuffer value, ListType type, ProtocolVersion version) throws InvalidRequestException
         {
             try
             {
@@ -163,7 +175,7 @@
             }
         }
 
-        public ByteBuffer get(int protocolVersion)
+        public ByteBuffer get(ProtocolVersion protocolVersion)
         {
             return CollectionSerializer.pack(elements, elements.size(), protocolVersion);
         }
@@ -404,13 +416,9 @@
 
             CellPath elementPath = existingRow.getComplexColumnData(column).getCellByIndex(idx).path();
             if (value == null)
-            {
                 params.addTombstone(column, elementPath);
-            }
             else if (value != ByteBufferUtil.UNSET_BYTE_BUFFER)
-            {
                 params.addCell(column, elementPath, value);
-            }
         }
     }
 
@@ -449,7 +457,7 @@
                 if (value == null)
                     params.addTombstone(column);
                 else
-                    params.addCell(column, value.get(Server.CURRENT_VERSION));
+                    params.addCell(column, value.get(ProtocolVersion.CURRENT));
             }
         }
     }
diff --git a/src/java/org/apache/cassandra/cql3/Maps.java b/src/java/org/apache/cassandra/cql3/Maps.java
index e56788e..e21fc6c 100644
--- a/src/java/org/apache/cassandra/cql3/Maps.java
+++ b/src/java/org/apache/cassandra/cql3/Maps.java
@@ -27,13 +27,11 @@
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.rows.*;
-import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.db.marshal.MapType;
-import org.apache.cassandra.db.marshal.ReversedType;
+import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.serializers.CollectionSerializer;
 import org.apache.cassandra.serializers.MarshalException;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.Pair;
 
@@ -146,6 +144,23 @@
             return res;
         }
 
+        @Override
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            AbstractType<?> keyType = null;
+            AbstractType<?> valueType = null;
+            for (Pair<Term.Raw, Term.Raw> entry : entries)
+            {
+                if (keyType == null)
+                    keyType = entry.left.getExactTypeIfKnown(keyspace);
+                if (valueType == null)
+                    valueType = entry.right.getExactTypeIfKnown(keyspace);
+                if (keyType != null && valueType != null)
+                    return MapType.getInstance(keyType, valueType, false);
+            }
+            return null;
+        }
+
         public String getText()
         {
             return entries.stream()
@@ -163,7 +178,7 @@
             this.map = map;
         }
 
-        public static Value fromSerialized(ByteBuffer value, MapType type, int version) throws InvalidRequestException
+        public static Value fromSerialized(ByteBuffer value, MapType type, ProtocolVersion version) throws InvalidRequestException
         {
             try
             {
@@ -182,7 +197,7 @@
             }
         }
 
-        public ByteBuffer get(int protocolVersion)
+        public ByteBuffer get(ProtocolVersion protocolVersion)
         {
             List<ByteBuffer> buffers = new ArrayList<>(2 * map.size());
             for (Map.Entry<ByteBuffer, ByteBuffer> entry : map.entrySet())
@@ -462,7 +477,7 @@
                 if (value == null)
                     params.addTombstone(column);
                 else
-                    params.addCell(column, value.get(Server.CURRENT_VERSION));
+                    params.addCell(column, value.get(ProtocolVersion.CURRENT));
             }
         }
     }
diff --git a/src/java/org/apache/cassandra/cql3/MultiColumnRelation.java b/src/java/org/apache/cassandra/cql3/MultiColumnRelation.java
index 1bfac3f..fb86e7b 100644
--- a/src/java/org/apache/cassandra/cql3/MultiColumnRelation.java
+++ b/src/java/org/apache/cassandra/cql3/MultiColumnRelation.java
@@ -47,7 +47,7 @@
  */
 public class MultiColumnRelation extends Relation
 {
-    private final List<ColumnIdentifier.Raw> entities;
+    private final List<ColumnDefinition.Raw> entities;
 
     /** A Tuples.Literal or Tuples.Raw marker */
     private final Term.MultiColumnRaw valuesOrMarker;
@@ -57,7 +57,7 @@
 
     private final Tuples.INRaw inMarker;
 
-    private MultiColumnRelation(List<ColumnIdentifier.Raw> entities, Operator relationType, Term.MultiColumnRaw valuesOrMarker, List<? extends Term.MultiColumnRaw> inValues, Tuples.INRaw inMarker)
+    private MultiColumnRelation(List<ColumnDefinition.Raw> entities, Operator relationType, Term.MultiColumnRaw valuesOrMarker, List<? extends Term.MultiColumnRaw> inValues, Tuples.INRaw inMarker)
     {
         this.entities = entities;
         this.relationType = relationType;
@@ -77,7 +77,7 @@
      * @param valuesOrMarker a Tuples.Literal instance or a Tuples.Raw marker
      * @return a new <code>MultiColumnRelation</code> instance
      */
-    public static MultiColumnRelation createNonInRelation(List<ColumnIdentifier.Raw> entities, Operator relationType, Term.MultiColumnRaw valuesOrMarker)
+    public static MultiColumnRelation createNonInRelation(List<ColumnDefinition.Raw> entities, Operator relationType, Term.MultiColumnRaw valuesOrMarker)
     {
         assert relationType != Operator.IN;
         return new MultiColumnRelation(entities, relationType, valuesOrMarker, null, null);
@@ -90,7 +90,7 @@
      * @param inValues a list of Tuples.Literal instances or a Tuples.Raw markers
      * @return a new <code>MultiColumnRelation</code> instance
      */
-    public static MultiColumnRelation createInRelation(List<ColumnIdentifier.Raw> entities, List<? extends Term.MultiColumnRaw> inValues)
+    public static MultiColumnRelation createInRelation(List<ColumnDefinition.Raw> entities, List<? extends Term.MultiColumnRaw> inValues)
     {
         return new MultiColumnRelation(entities, Operator.IN, null, inValues, null);
     }
@@ -102,12 +102,12 @@
      * @param inMarker a single IN marker
      * @return a new <code>MultiColumnRelation</code> instance
      */
-    public static MultiColumnRelation createSingleMarkerInRelation(List<ColumnIdentifier.Raw> entities, Tuples.INRaw inMarker)
+    public static MultiColumnRelation createSingleMarkerInRelation(List<ColumnDefinition.Raw> entities, Tuples.INRaw inMarker)
     {
         return new MultiColumnRelation(entities, Operator.IN, null, null, inMarker);
     }
 
-    public List<ColumnIdentifier.Raw> getEntities()
+    public List<ColumnDefinition.Raw> getEntities()
     {
         return entities;
     }
@@ -153,6 +153,10 @@
             Term term = toTerm(receivers, getValue(), cfm.ksName, boundNames);
             return new MultiColumnRestriction.InRestrictionWithMarker(receivers, (AbstractMarker) term);
         }
+
+        if (terms.size() == 1)
+            return new MultiColumnRestriction.EQRestriction(receivers, terms.get(0));
+
         return new MultiColumnRestriction.InRestrictionWithValues(receivers, terms);
     }
 
@@ -184,6 +188,12 @@
     }
 
     @Override
+    protected Restriction newLikeRestriction(CFMetaData cfm, VariableSpecifications boundNames, Operator operator) throws InvalidRequestException
+    {
+        throw invalidRequest("%s cannot be used for multi-column relations", operator());
+    }
+
+    @Override
     protected Term toTerm(List<? extends ColumnSpecification> receivers,
                           Raw raw,
                           String keyspace,
@@ -198,9 +208,9 @@
     {
         List<ColumnDefinition> names = new ArrayList<>(getEntities().size());
         int previousPosition = -1;
-        for (ColumnIdentifier.Raw raw : getEntities())
+        for (ColumnDefinition.Raw raw : getEntities())
         {
-            ColumnDefinition def = toColumnDefinition(cfm, raw);
+            ColumnDefinition def = raw.prepare(cfm);
             checkTrue(def.isClusteringColumn(), "Multi-column relations can only be applied to clustering columns but was applied to: %s", def.name);
             checkFalse(names.contains(def), "Column \"%s\" appeared twice in a relation: %s", def.name, this);
 
@@ -214,12 +224,12 @@
         return names;
     }
 
-    public Relation renameIdentifier(ColumnIdentifier.Raw from, ColumnIdentifier.Raw to)
+    public Relation renameIdentifier(ColumnDefinition.Raw from, ColumnDefinition.Raw to)
     {
         if (!entities.contains(from))
             return this;
 
-        List<ColumnIdentifier.Raw> newEntities = entities.stream().map(e -> e.equals(from) ? to : e).collect(Collectors.toList());
+        List<ColumnDefinition.Raw> newEntities = entities.stream().map(e -> e.equals(from) ? to : e).collect(Collectors.toList());
         return new MultiColumnRelation(newEntities, operator(), valuesOrMarker, inValues, inMarker);
     }
 
@@ -252,7 +262,7 @@
      */
     private class SuperColumnMultiColumnRelation extends MultiColumnRelation
     {
-        private SuperColumnMultiColumnRelation(List<ColumnIdentifier.Raw> entities, Operator relationType, MultiColumnRaw valuesOrMarker, List<? extends MultiColumnRaw> inValues, Tuples.INRaw inMarker)
+        private SuperColumnMultiColumnRelation(List<ColumnDefinition.Raw> entities, Operator relationType, MultiColumnRaw valuesOrMarker, List<? extends MultiColumnRaw> inValues, Tuples.INRaw inMarker)
         {
             super(entities, relationType, valuesOrMarker, inValues, inMarker);
         }
@@ -285,9 +295,9 @@
             assert cfm.isSuper() && cfm.isDense();
             List<ColumnDefinition> names = new ArrayList<>(getEntities().size());
 
-            for (ColumnIdentifier.Raw raw : getEntities())
+            for (ColumnDefinition.Raw raw : getEntities())
             {
-                ColumnDefinition def = toColumnDefinition(cfm, raw);
+                ColumnDefinition def = raw.prepare(cfm);
 
                 checkTrue(def.isClusteringColumn() ||
                           cfm.isSuperColumnKeyColumn(def),
diff --git a/src/java/org/apache/cassandra/cql3/Operation.java b/src/java/org/apache/cassandra/cql3/Operation.java
index 4b8d5ba..c005701 100644
--- a/src/java/org/apache/cassandra/cql3/Operation.java
+++ b/src/java/org/apache/cassandra/cql3/Operation.java
@@ -19,6 +19,7 @@
 
 import java.util.List;
 
+import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.db.DecoratedKey;
@@ -108,12 +109,10 @@
          * It returns an Operation which can be though as post-preparation well-typed
          * Operation.
          *
-         * @param receiver the "column" this operation applies to. Note that
-         * contrarly to the method of same name in Term.Raw, the receiver should always
-         * be a true column.
+         * @param receiver the column this operation applies to.
          * @return the prepared update operation.
          */
-        public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException;
+        public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException;
 
         /**
          * @return whether this operation can be applied alongside the {@code
@@ -134,7 +133,7 @@
         /**
          * The name of the column affected by this delete operation.
          */
-        public ColumnIdentifier.Raw affectedColumn();
+        public ColumnDefinition.Raw affectedColumn();
 
         /**
          * This method validates the operation (i.e. validate it is well typed)
@@ -147,7 +146,7 @@
          * @param receiver the "column" this operation applies to.
          * @return the prepared delete operation.
          */
-        public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException;
+        public Operation prepare(String keyspace, ColumnDefinition receiver, CFMetaData cfm) throws InvalidRequestException;
     }
 
     public static class SetValue implements RawUpdate
@@ -159,26 +158,32 @@
             this.value = value;
         }
 
-        public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException
+        public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException
         {
-            Term v = value.prepare(keyspace, receiver);
+            Term v = value.prepare(cfm.ksName, receiver);
 
             if (receiver.type instanceof CounterColumnType)
                 throw new InvalidRequestException(String.format("Cannot set the value of counter column %s (counters can only be incremented/decremented, not set)", receiver.name));
 
-            if (!(receiver.type.isCollection()))
-                return new Constants.Setter(receiver, v);
-
-            switch (((CollectionType)receiver.type).kind)
+            if (receiver.type.isCollection())
             {
-                case LIST:
-                    return new Lists.Setter(receiver, v);
-                case SET:
-                    return new Sets.Setter(receiver, v);
-                case MAP:
-                    return new Maps.Setter(receiver, v);
+                switch (((CollectionType) receiver.type).kind)
+                {
+                    case LIST:
+                        return new Lists.Setter(receiver, v);
+                    case SET:
+                        return new Sets.Setter(receiver, v);
+                    case MAP:
+                        return new Maps.Setter(receiver, v);
+                    default:
+                        throw new AssertionError();
+                }
             }
-            throw new AssertionError();
+
+            if (receiver.type.isUDT())
+                return new UserTypes.Setter(receiver, v);
+
+            return new Constants.Setter(receiver, v);
         }
 
         protected String toString(ColumnSpecification column)
@@ -210,7 +215,7 @@
             this.value = value;
         }
 
-        public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException
+        public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException
         {
             if (!(receiver.type instanceof CollectionType))
                 throw new InvalidRequestException(String.format("Invalid operation (%s) for non collection column %s", toString(receiver), receiver.name));
@@ -220,14 +225,14 @@
             switch (((CollectionType)receiver.type).kind)
             {
                 case LIST:
-                    Term idx = selector.prepare(keyspace, Lists.indexSpecOf(receiver));
-                    Term lval = value.prepare(keyspace, Lists.valueSpecOf(receiver));
+                    Term idx = selector.prepare(cfm.ksName, Lists.indexSpecOf(receiver));
+                    Term lval = value.prepare(cfm.ksName, Lists.valueSpecOf(receiver));
                     return new Lists.SetterByIndex(receiver, idx, lval);
                 case SET:
                     throw new InvalidRequestException(String.format("Invalid operation (%s) for set column %s", toString(receiver), receiver.name));
                 case MAP:
-                    Term key = selector.prepare(keyspace, Maps.keySpecOf(receiver));
-                    Term mval = value.prepare(keyspace, Maps.valueSpecOf(receiver));
+                    Term key = selector.prepare(cfm.ksName, Maps.keySpecOf(receiver));
+                    Term mval = value.prepare(cfm.ksName, Maps.valueSpecOf(receiver));
                     return new Maps.SetterByKey(receiver, key, mval);
             }
             throw new AssertionError();
@@ -246,6 +251,46 @@
         }
     }
 
+    public static class SetField implements RawUpdate
+    {
+        private final FieldIdentifier field;
+        private final Term.Raw value;
+
+        public SetField(FieldIdentifier field, Term.Raw value)
+        {
+            this.field = field;
+            this.value = value;
+        }
+
+        public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException
+        {
+            if (!receiver.type.isUDT())
+                throw new InvalidRequestException(String.format("Invalid operation (%s) for non-UDT column %s", toString(receiver), receiver.name));
+            else if (!receiver.type.isMultiCell())
+                throw new InvalidRequestException(String.format("Invalid operation (%s) for frozen UDT column %s", toString(receiver), receiver.name));
+
+            int fieldPosition = ((UserType) receiver.type).fieldPosition(field);
+            if (fieldPosition == -1)
+                throw new InvalidRequestException(String.format("UDT column %s does not have a field named %s", receiver.name, field));
+
+            Term val = value.prepare(cfm.ksName, UserTypes.fieldSpecOf(receiver, fieldPosition));
+            return new UserTypes.SetterByField(receiver, field, val);
+        }
+
+        protected String toString(ColumnSpecification column)
+        {
+            return String.format("%s.%s = %s", column.name, field, value);
+        }
+
+        public boolean isCompatibleWith(RawUpdate other)
+        {
+            if (other instanceof SetField)
+                return !((SetField) other).field.equals(field);
+            else
+                return !(other instanceof SetValue);
+        }
+    }
+
     // Currently only used internally counters support in SuperColumn families.
     // Addition on the element level inside the collections are otherwise not supported in the CQL.
     public static class ElementAddition implements RawUpdate
@@ -259,11 +304,11 @@
             this.value = value;
         }
 
-        public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException
+        public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException
         {
             assert receiver.type instanceof MapType;
-            Term k = selector.prepare(keyspace, Maps.keySpecOf(receiver));
-            Term v = value.prepare(keyspace, Maps.valueSpecOf(receiver));
+            Term k = selector.prepare(cfm.ksName, Maps.keySpecOf(receiver));
+            Term v = value.prepare(cfm.ksName, Maps.valueSpecOf(receiver));
 
             return new Maps.AdderByKey(receiver, v, k);
         }
@@ -292,11 +337,11 @@
             this.value = value;
         }
 
-        public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException
+        public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException
         {
             assert receiver.type instanceof MapType;
-            Term k = selector.prepare(keyspace, Maps.keySpecOf(receiver));
-            Term v = value.prepare(keyspace, Maps.valueSpecOf(receiver));
+            Term k = selector.prepare(cfm.ksName, Maps.keySpecOf(receiver));
+            Term v = value.prepare(cfm.ksName, Maps.valueSpecOf(receiver));
 
             return new Maps.SubtracterByKey(receiver, v, k);
         }
@@ -321,9 +366,9 @@
             this.value = value;
         }
 
-        public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException
+        public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException
         {
-            Term v = value.prepare(keyspace, receiver);
+            Term v = value.prepare(cfm.ksName, receiver);
 
             if (!(receiver.type instanceof CollectionType))
             {
@@ -371,13 +416,13 @@
             this.value = value;
         }
 
-        public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException
+        public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException
         {
             if (!(receiver.type instanceof CollectionType))
             {
                 if (!(receiver.type instanceof CounterColumnType))
                     throw new InvalidRequestException(String.format("Invalid operation (%s) for non counter column %s", toString(receiver), receiver.name));
-                return new Constants.Substracter(receiver, value.prepare(keyspace, receiver));
+                return new Constants.Substracter(receiver, value.prepare(cfm.ksName, receiver));
             }
             else if (!(receiver.type.isMultiCell()))
                 throw new InvalidRequestException(String.format("Invalid operation (%s) for frozen collection column %s", toString(receiver), receiver.name));
@@ -385,16 +430,16 @@
             switch (((CollectionType)receiver.type).kind)
             {
                 case LIST:
-                    return new Lists.Discarder(receiver, value.prepare(keyspace, receiver));
+                    return new Lists.Discarder(receiver, value.prepare(cfm.ksName, receiver));
                 case SET:
-                    return new Sets.Discarder(receiver, value.prepare(keyspace, receiver));
+                    return new Sets.Discarder(receiver, value.prepare(cfm.ksName, receiver));
                 case MAP:
                     // The value for a map subtraction is actually a set
                     ColumnSpecification vr = new ColumnSpecification(receiver.ksName,
                                                                      receiver.cfName,
                                                                      receiver.name,
                                                                      SetType.getInstance(((MapType)receiver.type).getKeysType(), false));
-                    return new Sets.Discarder(receiver, value.prepare(keyspace, vr));
+                    return new Sets.Discarder(receiver, value.prepare(cfm.ksName, vr));
             }
             throw new AssertionError();
         }
@@ -424,9 +469,9 @@
             this.value = value;
         }
 
-        public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException
+        public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException
         {
-            Term v = value.prepare(keyspace, receiver);
+            Term v = value.prepare(cfm.ksName, receiver);
 
             if (!(receiver.type instanceof ListType))
                 throw new InvalidRequestException(String.format("Invalid operation (%s) for non list column %s", toString(receiver), receiver.name));
@@ -449,19 +494,19 @@
 
     public static class ColumnDeletion implements RawDeletion
     {
-        private final ColumnIdentifier.Raw id;
+        private final ColumnDefinition.Raw id;
 
-        public ColumnDeletion(ColumnIdentifier.Raw id)
+        public ColumnDeletion(ColumnDefinition.Raw id)
         {
             this.id = id;
         }
 
-        public ColumnIdentifier.Raw affectedColumn()
+        public ColumnDefinition.Raw affectedColumn()
         {
             return id;
         }
 
-        public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException
+        public Operation prepare(String keyspace, ColumnDefinition receiver, CFMetaData cfm) throws InvalidRequestException
         {
             // No validation, deleting a column is always "well typed"
             return new Constants.Deleter(receiver);
@@ -470,21 +515,21 @@
 
     public static class ElementDeletion implements RawDeletion
     {
-        private final ColumnIdentifier.Raw id;
+        private final ColumnDefinition.Raw id;
         private final Term.Raw element;
 
-        public ElementDeletion(ColumnIdentifier.Raw id, Term.Raw element)
+        public ElementDeletion(ColumnDefinition.Raw id, Term.Raw element)
         {
             this.id = id;
             this.element = element;
         }
 
-        public ColumnIdentifier.Raw affectedColumn()
+        public ColumnDefinition.Raw affectedColumn()
         {
             return id;
         }
 
-        public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException
+        public Operation prepare(String keyspace, ColumnDefinition receiver, CFMetaData cfm) throws InvalidRequestException
         {
             if (!(receiver.type.isCollection()))
                 throw new InvalidRequestException(String.format("Invalid deletion operation for non collection column %s", receiver.name));
@@ -506,4 +551,34 @@
             throw new AssertionError();
         }
     }
+
+    public static class FieldDeletion implements RawDeletion
+    {
+        private final ColumnDefinition.Raw id;
+        private final FieldIdentifier field;
+
+        public FieldDeletion(ColumnDefinition.Raw id, FieldIdentifier field)
+        {
+            this.id = id;
+            this.field = field;
+        }
+
+        public ColumnDefinition.Raw affectedColumn()
+        {
+            return id;
+        }
+
+        public Operation prepare(String keyspace, ColumnDefinition receiver, CFMetaData cfm) throws InvalidRequestException
+        {
+            if (!receiver.type.isUDT())
+                throw new InvalidRequestException(String.format("Invalid field deletion operation for non-UDT column %s", receiver.name));
+            else if (!receiver.type.isMultiCell())
+                throw new InvalidRequestException(String.format("Frozen UDT column %s does not support field deletions", receiver.name));
+
+            if (((UserType) receiver.type).fieldPosition(field) == -1)
+                throw new InvalidRequestException(String.format("UDT column %s does not have a field named %s", receiver.name, field));
+
+            return new UserTypes.DeleterByField(receiver, field);
+        }
+    }
 }
diff --git a/src/java/org/apache/cassandra/cql3/Operator.java b/src/java/org/apache/cassandra/cql3/Operator.java
index 7b28a30..8c04bef 100644
--- a/src/java/org/apache/cassandra/cql3/Operator.java
+++ b/src/java/org/apache/cassandra/cql3/Operator.java
@@ -25,10 +25,8 @@
 import java.util.Map;
 import java.util.Set;
 
-import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.db.marshal.ListType;
-import org.apache.cassandra.db.marshal.MapType;
-import org.apache.cassandra.db.marshal.SetType;
+import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.utils.ByteBufferUtil;
 
 public enum Operator
 {
@@ -101,6 +99,46 @@
         {
             return "IS NOT";
         }
+    },
+    LIKE_PREFIX(10)
+    {
+        @Override
+        public String toString()
+        {
+            return "LIKE '<term>%'";
+        }
+    },
+    LIKE_SUFFIX(11)
+    {
+        @Override
+        public String toString()
+        {
+            return "LIKE '%<term>'";
+        }
+    },
+    LIKE_CONTAINS(12)
+    {
+        @Override
+        public String toString()
+        {
+            return "LIKE '%<term>%'";
+        }
+    },
+    LIKE_MATCHES(13)
+    {
+        @Override
+        public String toString()
+        {
+            return "LIKE '<term>'";
+        }
+    },
+    LIKE(14)
+    {
+        @Override
+        public String toString()
+        {
+            return "LIKE";
+        }
     };
 
     /**
@@ -193,8 +231,15 @@
             case CONTAINS_KEY:
                 Map map = (Map) type.getSerializer().deserialize(leftOperand);
                 return map.containsKey(((MapType) type).getKeysType().getSerializer().deserialize(rightOperand));
+            case LIKE_PREFIX:
+                return ByteBufferUtil.startsWith(leftOperand, rightOperand);
+            case LIKE_SUFFIX:
+                return ByteBufferUtil.endsWith(leftOperand, rightOperand);
+            case LIKE_MATCHES:
+            case LIKE_CONTAINS:
+                return ByteBufferUtil.contains(leftOperand, rightOperand);
             default:
-                // we shouldn't get CONTAINS, CONTAINS KEY, or IS NOT here
+                // we shouldn't get LIKE, CONTAINS, CONTAINS KEY, or IS NOT here
                 throw new AssertionError();
         }
     }
@@ -204,6 +249,15 @@
         return 4;
     }
 
+    /**
+     * Checks if this operator is a slice operator.
+     * @return {@code true} if this operator is a slice operator, {@code false} otherwise.
+     */
+    public boolean isSlice()
+    {
+        return this == LT || this == LTE || this == GT || this == GTE;
+    }
+
     @Override
     public String toString()
     {
diff --git a/src/java/org/apache/cassandra/cql3/QueryHandler.java b/src/java/org/apache/cassandra/cql3/QueryHandler.java
index 3c11c0e..2108d4c 100644
--- a/src/java/org/apache/cassandra/cql3/QueryHandler.java
+++ b/src/java/org/apache/cassandra/cql3/QueryHandler.java
@@ -33,7 +33,8 @@
     ResultMessage process(String query,
                           QueryState state,
                           QueryOptions options,
-                          Map<String, ByteBuffer> customPayload) throws RequestExecutionException, RequestValidationException;
+                          Map<String, ByteBuffer> customPayload,
+                          long queryStartNanoTime) throws RequestExecutionException, RequestValidationException;
 
     ResultMessage.Prepared prepare(String query,
                                    QueryState state,
@@ -46,10 +47,12 @@
     ResultMessage processPrepared(CQLStatement statement,
                                   QueryState state,
                                   QueryOptions options,
-                                  Map<String, ByteBuffer> customPayload) throws RequestExecutionException, RequestValidationException;
+                                  Map<String, ByteBuffer> customPayload,
+                                  long queryStartNanoTime) throws RequestExecutionException, RequestValidationException;
 
     ResultMessage processBatch(BatchStatement statement,
                                QueryState state,
                                BatchQueryOptions options,
-                               Map<String, ByteBuffer> customPayload) throws RequestExecutionException, RequestValidationException;
+                               Map<String, ByteBuffer> customPayload,
+                               long queryStartNanoTime) throws RequestExecutionException, RequestValidationException;
 }
diff --git a/src/java/org/apache/cassandra/cql3/QueryOptions.java b/src/java/org/apache/cassandra/cql3/QueryOptions.java
index a062567..a29c6ff 100644
--- a/src/java/org/apache/cassandra/cql3/QueryOptions.java
+++ b/src/java/org/apache/cassandra/cql3/QueryOptions.java
@@ -21,6 +21,7 @@
 import java.util.*;
 
 import com.google.common.collect.ImmutableList;
+
 import io.netty.buffer.ByteBuf;
 
 import org.apache.cassandra.config.ColumnDefinition;
@@ -32,7 +33,7 @@
 import org.apache.cassandra.transport.CBCodec;
 import org.apache.cassandra.transport.CBUtil;
 import org.apache.cassandra.transport.ProtocolException;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.Pair;
 
 /**
@@ -44,7 +45,7 @@
                                                                        Collections.<ByteBuffer>emptyList(),
                                                                        false,
                                                                        SpecificOptions.DEFAULT,
-                                                                       Server.CURRENT_VERSION);
+                                                                       ProtocolVersion.CURRENT);
 
     public static final CBCodec<QueryOptions> codec = new Codec();
 
@@ -53,32 +54,27 @@
 
     public static QueryOptions fromThrift(ConsistencyLevel consistency, List<ByteBuffer> values)
     {
-        return new DefaultQueryOptions(consistency, values, false, SpecificOptions.DEFAULT, Server.VERSION_3);
+        return new DefaultQueryOptions(consistency, values, false, SpecificOptions.DEFAULT, ProtocolVersion.V3);
     }
 
     public static QueryOptions forInternalCalls(ConsistencyLevel consistency, List<ByteBuffer> values)
     {
-        return new DefaultQueryOptions(consistency, values, false, SpecificOptions.DEFAULT, Server.VERSION_3);
+        return new DefaultQueryOptions(consistency, values, false, SpecificOptions.DEFAULT, ProtocolVersion.V3);
     }
 
     public static QueryOptions forInternalCalls(List<ByteBuffer> values)
     {
-        return new DefaultQueryOptions(ConsistencyLevel.ONE, values, false, SpecificOptions.DEFAULT, Server.VERSION_3);
+        return new DefaultQueryOptions(ConsistencyLevel.ONE, values, false, SpecificOptions.DEFAULT, ProtocolVersion.V3);
     }
 
-    public static QueryOptions forProtocolVersion(int protocolVersion)
+    public static QueryOptions forProtocolVersion(ProtocolVersion protocolVersion)
     {
         return new DefaultQueryOptions(null, null, true, null, protocolVersion);
     }
 
-    public static QueryOptions create(ConsistencyLevel consistency, List<ByteBuffer> values, boolean skipMetadata, int pageSize, PagingState pagingState, ConsistencyLevel serialConsistency)
+    public static QueryOptions create(ConsistencyLevel consistency, List<ByteBuffer> values, boolean skipMetadata, int pageSize, PagingState pagingState, ConsistencyLevel serialConsistency, ProtocolVersion version)
     {
-        return create(consistency, values, skipMetadata, pageSize, pagingState, serialConsistency, 0);
-    }
-
-    public static QueryOptions create(ConsistencyLevel consistency, List<ByteBuffer> values, boolean skipMetadata, int pageSize, PagingState pagingState, ConsistencyLevel serialConsistency, int protocolVersion)
-    {
-        return new DefaultQueryOptions(consistency, values, skipMetadata, new SpecificOptions(pageSize, pagingState, serialConsistency, Long.MIN_VALUE), protocolVersion);
+        return new DefaultQueryOptions(consistency, values, skipMetadata, new SpecificOptions(pageSize, pagingState, serialConsistency, Long.MIN_VALUE), version);
     }
 
     public static QueryOptions addColumnSpecifications(QueryOptions options, List<ColumnSpecification> columnSpecs)
@@ -157,7 +153,7 @@
         throw new UnsupportedOperationException();
     }
 
-    /**  The pageSize for this query. Will be <= 0 if not relevant for the query.  */
+    /**  The pageSize for this query. Will be {@code <= 0} if not relevant for the query.  */
     public int getPageSize()
     {
         return getSpecificOptions().pageSize;
@@ -185,7 +181,7 @@
      * The protocol version for the query. Will be 3 if the object don't come from
      * a native protocol request (i.e. it's been allocated locally or by CQL-over-thrift).
      */
-    public abstract int getProtocolVersion();
+    public abstract ProtocolVersion getProtocolVersion();
 
     // Mainly for the sake of BatchQueryOptions
     abstract SpecificOptions getSpecificOptions();
@@ -203,9 +199,9 @@
 
         private final SpecificOptions options;
 
-        private final transient int protocolVersion;
+        private final transient ProtocolVersion protocolVersion;
 
-        DefaultQueryOptions(ConsistencyLevel consistency, List<ByteBuffer> values, boolean skipMetadata, SpecificOptions options, int protocolVersion)
+        DefaultQueryOptions(ConsistencyLevel consistency, List<ByteBuffer> values, boolean skipMetadata, SpecificOptions options, ProtocolVersion protocolVersion)
         {
             this.consistency = consistency;
             this.values = values;
@@ -229,7 +225,7 @@
             return skipMetadata;
         }
 
-        public int getProtocolVersion()
+        public ProtocolVersion getProtocolVersion()
         {
             return protocolVersion;
         }
@@ -264,7 +260,7 @@
             return wrapped.skipMetadata();
         }
 
-        public int getProtocolVersion()
+        public ProtocolVersion getProtocolVersion()
         {
             return wrapped.getProtocolVersion();
         }
@@ -369,7 +365,7 @@
 
     private static class Codec implements CBCodec<QueryOptions>
     {
-        private static enum Flag
+        private enum Flag
         {
             // The order of that enum matters!!
             VALUES,
@@ -402,10 +398,12 @@
             }
         }
 
-        public QueryOptions decode(ByteBuf body, int version)
+        public QueryOptions decode(ByteBuf body, ProtocolVersion version)
         {
             ConsistencyLevel consistency = CBUtil.readConsistencyLevel(body);
-            EnumSet<Flag> flags = Flag.deserialize((int)body.readByte());
+            EnumSet<Flag> flags = Flag.deserialize(version.isGreaterOrEqualTo(ProtocolVersion.V5)
+                                                   ? (int)body.readUnsignedInt()
+                                                   : (int)body.readUnsignedByte());
 
             List<ByteBuffer> values = Collections.<ByteBuffer>emptyList();
             List<String> names = null;
@@ -448,12 +446,15 @@
             return names == null ? opts : new OptionsWithNames(opts, names);
         }
 
-        public void encode(QueryOptions options, ByteBuf dest, int version)
+        public void encode(QueryOptions options, ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeConsistencyLevel(options.getConsistency(), dest);
 
             EnumSet<Flag> flags = gatherFlags(options);
-            dest.writeByte((byte)Flag.serialize(flags));
+            if (version.isGreaterOrEqualTo(ProtocolVersion.V5))
+                dest.writeInt(Flag.serialize(flags));
+            else
+                dest.writeByte((byte)Flag.serialize(flags));
 
             if (flags.contains(Flag.VALUES))
                 CBUtil.writeValueList(options.getValues(), dest);
@@ -471,14 +472,14 @@
             // don't bother.
         }
 
-        public int encodedSize(QueryOptions options, int version)
+        public int encodedSize(QueryOptions options, ProtocolVersion version)
         {
             int size = 0;
 
             size += CBUtil.sizeOfConsistencyLevel(options.getConsistency());
 
             EnumSet<Flag> flags = gatherFlags(options);
-            size += 1;
+            size += (version.isGreaterOrEqualTo(ProtocolVersion.V5) ? 4 : 1);
 
             if (flags.contains(Flag.VALUES))
                 size += CBUtil.sizeOfValueList(options.getValues());
diff --git a/src/java/org/apache/cassandra/cql3/QueryProcessor.java b/src/java/org/apache/cassandra/cql3/QueryProcessor.java
index dc258d8..80f8286 100644
--- a/src/java/org/apache/cassandra/cql3/QueryProcessor.java
+++ b/src/java/org/apache/cassandra/cql3/QueryProcessor.java
@@ -33,12 +33,11 @@
 import org.slf4j.LoggerFactory;
 
 import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
-import com.googlecode.concurrentlinkedhashmap.EntryWeigher;
-import com.googlecode.concurrentlinkedhashmap.EvictionListener;
 import org.antlr.runtime.*;
 import org.apache.cassandra.concurrent.ScheduledExecutors;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.cql3.functions.FunctionName;
 import org.apache.cassandra.cql3.statements.*;
@@ -54,42 +53,23 @@
 import org.apache.cassandra.service.pager.QueryPager;
 import org.apache.cassandra.thrift.ThriftClientState;
 import org.apache.cassandra.tracing.Tracing;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.transport.messages.ResultMessage;
 import org.apache.cassandra.utils.*;
-import org.github.jamm.MemoryMeter;
+
+import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue;
 
 public class QueryProcessor implements QueryHandler
 {
-    public static final CassandraVersion CQL_VERSION = new CassandraVersion("3.4.0");
+    public static final CassandraVersion CQL_VERSION = new CassandraVersion("3.4.4");
 
     // See comments on QueryProcessor #prepare
-    public static final CassandraVersion NEW_PREPARED_STATEMENT_BEHAVIOUR_SINCE = new CassandraVersion("3.0.26");
+    public static final CassandraVersion NEW_PREPARED_STATEMENT_BEHAVIOUR_SINCE_30 = new CassandraVersion("3.0.26");
+    public static final CassandraVersion NEW_PREPARED_STATEMENT_BEHAVIOUR_SINCE_3X = new CassandraVersion("3.11.12");
 
     public static final QueryProcessor instance = new QueryProcessor();
 
     private static final Logger logger = LoggerFactory.getLogger(QueryProcessor.class);
-    private static final NoSpamLogger nospam = NoSpamLogger.getLogger(logger, 10, TimeUnit.MINUTES);
-    private static final MemoryMeter meter = new MemoryMeter().withGuessing(MemoryMeter.Guess.FALLBACK_BEST).ignoreKnownSingletons();
-    private static final long MAX_CACHE_PREPARED_MEMORY = Runtime.getRuntime().maxMemory() / 256;
-
-    private static final EntryWeigher<MD5Digest, ParsedStatement.Prepared> cqlMemoryUsageWeigher = new EntryWeigher<MD5Digest, ParsedStatement.Prepared>()
-    {
-        @Override
-        public int weightOf(MD5Digest key, ParsedStatement.Prepared value)
-        {
-            return Ints.checkedCast(measure(key) + measure(value.statement) + measure(value.boundNames));
-        }
-    };
-
-    private static final EntryWeigher<Integer, ParsedStatement.Prepared> thriftMemoryUsageWeigher = new EntryWeigher<Integer, ParsedStatement.Prepared>()
-    {
-        @Override
-        public int weightOf(Integer key, ParsedStatement.Prepared value)
-        {
-            return Ints.checkedCast(measure(key) + measure(value.statement) + measure(value.boundNames));
-        }
-    };
 
     private static final ConcurrentLinkedHashMap<MD5Digest, ParsedStatement.Prepared> preparedStatements;
     private static final ConcurrentLinkedHashMap<Integer, ParsedStatement.Prepared> thriftPreparedStatements;
@@ -103,45 +83,49 @@
     public static final CQLMetrics metrics = new CQLMetrics();
 
     private static final AtomicInteger lastMinuteEvictionsCount = new AtomicInteger(0);
+    private static final AtomicInteger thriftLastMinuteEvictionsCount = new AtomicInteger(0);
 
     static
     {
         preparedStatements = new ConcurrentLinkedHashMap.Builder<MD5Digest, ParsedStatement.Prepared>()
-                             .maximumWeightedCapacity(MAX_CACHE_PREPARED_MEMORY)
-                             .weigher(cqlMemoryUsageWeigher)
-                             .listener(new EvictionListener<MD5Digest, ParsedStatement.Prepared>()
-                             {
-                                 public void onEviction(MD5Digest md5Digest, ParsedStatement.Prepared prepared)
-                                 {
-                                     metrics.preparedStatementsEvicted.inc();
-                                     lastMinuteEvictionsCount.incrementAndGet();
-                                 }
+                             .maximumWeightedCapacity(capacityToBytes(DatabaseDescriptor.getPreparedStatementsCacheSizeMB()))
+                             .weigher(QueryProcessor::measure)
+                             .listener((md5Digest, prepared) -> {
+                                 metrics.preparedStatementsEvicted.inc();
+                                 lastMinuteEvictionsCount.incrementAndGet();
+                                 SystemKeyspace.removePreparedStatement(md5Digest);
                              }).build();
 
         thriftPreparedStatements = new ConcurrentLinkedHashMap.Builder<Integer, ParsedStatement.Prepared>()
-                                   .maximumWeightedCapacity(MAX_CACHE_PREPARED_MEMORY)
-                                   .weigher(thriftMemoryUsageWeigher)
-                                   .listener(new EvictionListener<Integer, ParsedStatement.Prepared>()
-                                   {
-                                       public void onEviction(Integer integer, ParsedStatement.Prepared prepared)
-                                       {
-                                           metrics.preparedStatementsEvicted.inc();
-                                           lastMinuteEvictionsCount.incrementAndGet();
-                                       }
+                                   .maximumWeightedCapacity(capacityToBytes(DatabaseDescriptor.getThriftPreparedStatementsCacheSizeMB()))
+                                   .weigher(QueryProcessor::measure)
+                                   .listener((integer, prepared) -> {
+                                       metrics.preparedStatementsEvicted.inc();
+                                       thriftLastMinuteEvictionsCount.incrementAndGet();
                                    })
                                    .build();
 
-        ScheduledExecutors.scheduledTasks.scheduleAtFixedRate(new Runnable()
-        {
-            public void run()
-            {
-                long count = lastMinuteEvictionsCount.getAndSet(0);
-                if (count > 0)
-                    logger.info("{} prepared statements discarded in the last minute because cache limit reached ({} bytes)",
-                                count,
-                                MAX_CACHE_PREPARED_MEMORY);
-            }
+        ScheduledExecutors.scheduledTasks.scheduleAtFixedRate(() -> {
+            long count = lastMinuteEvictionsCount.getAndSet(0);
+            if (count > 0)
+                logger.warn("{} prepared statements discarded in the last minute because cache limit reached ({} MB)",
+                            count,
+                            DatabaseDescriptor.getPreparedStatementsCacheSizeMB());
+            count = thriftLastMinuteEvictionsCount.getAndSet(0);
+            if (count > 0)
+                logger.warn("{} prepared Thrift statements discarded in the last minute because cache limit reached ({} MB)",
+                            count,
+                            DatabaseDescriptor.getThriftPreparedStatementsCacheSizeMB());
         }, 1, 1, TimeUnit.MINUTES);
+
+        logger.info("Initialized prepared statement caches with {} MB (native) and {} MB (Thrift)",
+                    DatabaseDescriptor.getPreparedStatementsCacheSizeMB(),
+                    DatabaseDescriptor.getThriftPreparedStatementsCacheSizeMB());
+    }
+
+    private static long capacityToBytes(long cacheSizeMB)
+    {
+        return cacheSizeMB * 1024 * 1024;
     }
 
     public static int preparedStatementsCount()
@@ -159,11 +143,51 @@
         InternalStateInstance()
         {
             ClientState state = ClientState.forInternalCalls();
-            state.setKeyspace(SystemKeyspace.NAME);
+            state.setKeyspace(SchemaConstants.SYSTEM_KEYSPACE_NAME);
             this.queryState = new QueryState(state);
         }
     }
 
+    public void preloadPreparedStatements()
+    {
+        int count = SystemKeyspace.loadPreparedStatements((id, query, keyspace) -> {
+            try
+            {
+                ClientState clientState = ClientState.forInternalCalls();
+                if (keyspace != null)
+                    clientState.setKeyspace(keyspace);
+                ParsedStatement.Prepared prepared = getStatement(query, clientState);
+                preparedStatements.putIfAbsent(id, prepared);
+                // Preload `null` statement for non-fully qualified statements, since it can't be parsed if loaded from cache and will be dropped
+                if (!prepared.fullyQualified)
+                    preparedStatements.putIfAbsent(computeId(query, null), getStatement(query, clientState));
+                return true;
+            }
+            catch (Throwable e)
+            {
+                JVMStabilityInspector.inspectThrowable(e);
+                logger.warn(String.format("Prepared statement recreation error, removing statement: %s %s %s", id, query, keyspace));
+                SystemKeyspace.removePreparedStatement(id);
+                return false;
+            }
+        });
+        logger.info("Preloaded {} prepared statements", count);
+    }
+
+
+    /**
+     * Clears the prepared statement cache.
+     * @param memoryOnly {@code true} if only the in memory caches must be cleared, {@code false} otherwise.
+     */
+    @VisibleForTesting
+    public static void clearPreparedStatements(boolean memoryOnly)
+    {
+        preparedStatements.clear();
+        thriftPreparedStatements.clear();
+        if (!memoryOnly)
+            SystemKeyspace.resetPreparedStatements();
+    }
+
     @VisibleForTesting
     public static QueryState internalQueryState()
     {
@@ -208,7 +232,7 @@
         }
     }
 
-    public ResultMessage processStatement(CQLStatement statement, QueryState queryState, QueryOptions options)
+    public ResultMessage processStatement(CQLStatement statement, QueryState queryState, QueryOptions options, long queryStartNanoTime)
     throws RequestExecutionException, RequestValidationException
     {
         logger.trace("Process {} @CL.{}", statement, options.getConsistency());
@@ -216,26 +240,26 @@
         statement.checkAccess(clientState);
         statement.validate(clientState);
 
-        ResultMessage result = statement.execute(queryState, options);
+        ResultMessage result = statement.execute(queryState, options, queryStartNanoTime);
         return result == null ? new ResultMessage.Void() : result;
     }
 
-    public static ResultMessage process(String queryString, ConsistencyLevel cl, QueryState queryState)
+    public static ResultMessage process(String queryString, ConsistencyLevel cl, QueryState queryState, long queryStartNanoTime)
     throws RequestExecutionException, RequestValidationException
     {
-        return instance.process(queryString, queryState, QueryOptions.forInternalCalls(cl, Collections.<ByteBuffer>emptyList()));
+        return instance.process(queryString, queryState, QueryOptions.forInternalCalls(cl, Collections.<ByteBuffer>emptyList()), queryStartNanoTime);
     }
 
     public ResultMessage process(String query,
                                  QueryState state,
                                  QueryOptions options,
-                                 Map<String, ByteBuffer> customPayload)
-                                         throws RequestExecutionException, RequestValidationException
+                                 Map<String, ByteBuffer> customPayload,
+                                 long queryStartNanoTime) throws RequestExecutionException, RequestValidationException
     {
-        return process(query, state, options);
+        return process(query, state, options, queryStartNanoTime);
     }
 
-    public ResultMessage process(String queryString, QueryState queryState, QueryOptions options)
+    public ResultMessage process(String queryString, QueryState queryState, QueryOptions options, long queryStartNanoTime)
     throws RequestExecutionException, RequestValidationException
     {
         ParsedStatement.Prepared p = getStatement(queryString, queryState.getClientState());
@@ -247,7 +271,7 @@
         if (!queryState.getClientState().isInternal)
             metrics.regularStatementsExecuted.inc();
 
-        return processStatement(prepared, queryState, options);
+        return processStatement(prepared, queryState, options, queryStartNanoTime);
     }
 
     public static ParsedStatement.Prepared parseStatement(String queryStr, QueryState queryState) throws RequestValidationException
@@ -262,7 +286,7 @@
 
     public static UntypedResultSet process(String query, ConsistencyLevel cl, List<ByteBuffer> values) throws RequestExecutionException
     {
-        ResultMessage result = instance.process(query, QueryState.forInternalCalls(), QueryOptions.forInternalCalls(cl, values));
+        ResultMessage result = instance.process(query, QueryState.forInternalCalls(), QueryOptions.forInternalCalls(cl, values), System.nanoTime());
         if (result instanceof ResultMessage.Rows)
             return UntypedResultSet.create(((ResultMessage.Rows)result).result);
         else
@@ -313,13 +337,19 @@
             return null;
     }
 
+    public static UntypedResultSet execute(String query, ConsistencyLevel cl, Object... values)
+    throws RequestExecutionException
+    {
+        return execute(query, cl, internalQueryState(), values);
+    }
+
     public static UntypedResultSet execute(String query, ConsistencyLevel cl, QueryState state, Object... values)
     throws RequestExecutionException
     {
         try
         {
             ParsedStatement.Prepared prepared = prepareInternal(query);
-            ResultMessage result = prepared.statement.execute(state, makeInternalOptions(prepared, values, cl));
+            ResultMessage result = prepared.statement.execute(state, makeInternalOptions(prepared, values, cl), System.nanoTime());
             if (result instanceof ResultMessage.Rows)
                 return UntypedResultSet.create(((ResultMessage.Rows)result).result);
             else
@@ -338,7 +368,7 @@
             throw new IllegalArgumentException("Only SELECTs can be paged");
 
         SelectStatement select = (SelectStatement)prepared.statement;
-        QueryPager pager = select.getQuery(makeInternalOptions(prepared, values), FBUtilities.nowInSeconds()).getPager(null, Server.CURRENT_VERSION);
+        QueryPager pager = select.getQuery(makeInternalOptions(prepared, values), FBUtilities.nowInSeconds()).getPager(null, ProtocolVersion.CURRENT);
         return UntypedResultSet.create(select, pager, pageSize);
     }
 
@@ -362,12 +392,12 @@
      * Note that this only make sense for Selects so this only accept SELECT statements and is only useful in rare
      * cases.
      */
-    public static UntypedResultSet executeInternalWithNow(int nowInSec, String query, Object... values)
+    public static UntypedResultSet executeInternalWithNow(int nowInSec, long queryStartNanoTime, String query, Object... values)
     {
         ParsedStatement.Prepared prepared = prepareInternal(query);
         assert prepared.statement instanceof SelectStatement;
         SelectStatement select = (SelectStatement)prepared.statement;
-        ResultMessage result = select.executeInternal(internalQueryState(), makeInternalOptions(prepared, values), nowInSec);
+        ResultMessage result = select.executeInternal(internalQueryState(), makeInternalOptions(prepared, values), nowInSec, queryStartNanoTime);
         assert result instanceof ResultMessage.Rows;
         return UntypedResultSet.create(((ResultMessage.Rows)result).result);
     }
@@ -409,9 +439,11 @@
         synchronized (this)
         {
             CassandraVersion minVersion = Gossiper.instance.getMinVersion(DatabaseDescriptor.getWriteRpcTimeout(), TimeUnit.MILLISECONDS);
-            if (minVersion != null && minVersion.compareTo(NEW_PREPARED_STATEMENT_BEHAVIOUR_SINCE) >= 0)
+            if (minVersion != null &&
+                ((minVersion.is30() && minVersion.compareTo(NEW_PREPARED_STATEMENT_BEHAVIOUR_SINCE_30) >= 0) ||
+                 (minVersion.compareTo(NEW_PREPARED_STATEMENT_BEHAVIOUR_SINCE_3X) >= 0)))
             {
-                logger.info("Fully upgraded to at least {}", NEW_PREPARED_STATEMENT_BEHAVIOUR_SINCE);
+                logger.info("Fully upgraded to at least {}", minVersion);
                 newPreparedStatementBehaviour = true;
             }
 
@@ -432,7 +464,7 @@
      *
      * The correct combination to return is 2/3 - the problem is during upgrades (assuming upgrading from < 3.0.26)
      * - Existing clients have hash 1 or 3
-     * - Query prepared on a 3.0.26 instance needs to return hash 1/3 to be able to execute it on a 3.0.25 instance
+     * - Query prepared on a 3.0.26/3.11.12 instance needs to return hash 1/3 to be able to execute it on a 3.0.25 instance
      * - This is handled by the useNewPreparedStatementBehaviour flag - while there still are 3.0.25 instances in
      *   the cluster we always return hash 1/3
      * - Once fully upgraded we start returning hash 2/3, this will cause a prepared statement id mismatch for existing
@@ -441,7 +473,8 @@
      */
     public ResultMessage.Prepared prepare(String queryString, ClientState clientState, boolean forThrift)
     {
-        boolean newPreparedStatementBehaviour = useNewPreparedStatementBehaviour();
+        boolean useNewPreparedStatementBehaviour = useNewPreparedStatementBehaviour();
+
         MD5Digest hashWithoutKeyspace = computeId(queryString, null);
         MD5Digest hashWithKeyspace = computeId(queryString, clientState.getRawKeyspace());
         ParsedStatement.Prepared cachedWithoutKeyspace = preparedStatements.get(hashWithoutKeyspace);
@@ -453,7 +486,7 @@
         {
             if (safeToReturnCached)
             {
-                if (newPreparedStatementBehaviour)
+                if (useNewPreparedStatementBehaviour)
                 {
                     if (cachedWithoutKeyspace.fullyQualified) // For fully qualified statements, we always skip keyspace to avoid digest switching
                         return new ResultMessage.Prepared(hashWithoutKeyspace, cachedWithoutKeyspace);
@@ -499,9 +532,9 @@
             clientState.warnAboutUseWithPreparedStatements(hashWithKeyspace, clientState.getRawKeyspace());
 
             ResultMessage.Prepared nonQualifiedWithKeyspace = storePreparedStatement(queryString, clientState.getRawKeyspace(), prepared, forThrift);
-            ResultMessage.Prepared nonQualifiedWithoutKeyspace = storePreparedStatement(queryString, null, prepared, forThrift);
-            if (!newPreparedStatementBehaviour)
-                return nonQualifiedWithoutKeyspace;
+            ResultMessage.Prepared nonQualifiedWithNullKeyspace = storePreparedStatement(queryString, null, prepared, forThrift);
+            if (!useNewPreparedStatementBehaviour)
+                return nonQualifiedWithNullKeyspace;
 
             return nonQualifiedWithKeyspace;
         }
@@ -527,13 +560,25 @@
         {
             Integer thriftStatementId = computeThriftId(queryString, clientKeyspace);
             ParsedStatement.Prepared existing = thriftPreparedStatements.get(thriftStatementId);
-            return existing == null ? null : ResultMessage.Prepared.forThrift(thriftStatementId, existing.boundNames);
+            if (existing == null)
+                return null;
+
+            checkTrue(queryString.equals(existing.rawCQLStatement),
+                      "MD5 hash collision: query with the same MD5 hash was already prepared. \n Existing: '%s'",
+                      existing.rawCQLStatement);
+            return ResultMessage.Prepared.forThrift(thriftStatementId, existing.boundNames);
         }
         else
         {
             MD5Digest statementId = computeId(queryString, clientKeyspace);
             ParsedStatement.Prepared existing = preparedStatements.get(statementId);
-            return existing == null ? null : new ResultMessage.Prepared(statementId, existing);
+            if (existing == null)
+                return null;
+
+            checkTrue(queryString.equals(existing.rawCQLStatement),
+                      "MD5 hash collision: query with the same MD5 hash was already prepared. \n Existing: '%s'",
+                      existing.rawCQLStatement);
+            return new ResultMessage.Prepared(statementId, existing);
         }
     }
 
@@ -543,22 +588,29 @@
     {
         // Concatenate the current keyspace so we don't mix prepared statements between keyspace (#5352).
         // (if the keyspace is null, queryString has to have a fully-qualified keyspace so it's fine.
-        long statementSize = measure(prepared.statement);
+        long statementSize = ObjectSizes.measureDeep(prepared.statement);
         // don't execute the statement if it's bigger than the allowed threshold
-        if (statementSize > MAX_CACHE_PREPARED_MEMORY)
-            throw new InvalidRequestException(String.format("Prepared statement of size %d bytes is larger than allowed maximum of %d bytes.",
-                                                            statementSize,
-                                                            MAX_CACHE_PREPARED_MEMORY));
         if (forThrift)
         {
+            if (statementSize > capacityToBytes(DatabaseDescriptor.getThriftPreparedStatementsCacheSizeMB()))
+                throw new InvalidRequestException(String.format("Prepared statement of size %d bytes is larger than allowed maximum of %d MB: %s...",
+                                                                statementSize,
+                                                                DatabaseDescriptor.getThriftPreparedStatementsCacheSizeMB(),
+                                                                queryString.substring(0, 200)));
             Integer statementId = computeThriftId(queryString, keyspace);
             thriftPreparedStatements.put(statementId, prepared);
             return ResultMessage.Prepared.forThrift(statementId, prepared.boundNames);
         }
         else
         {
+            if (statementSize > capacityToBytes(DatabaseDescriptor.getPreparedStatementsCacheSizeMB()))
+                throw new InvalidRequestException(String.format("Prepared statement of size %d bytes is larger than allowed maximum of %d MB: %s...",
+                                                                statementSize,
+                                                                DatabaseDescriptor.getPreparedStatementsCacheSizeMB(),
+                                                                queryString.substring(0, 200)));
             MD5Digest statementId = computeId(queryString, keyspace);
             preparedStatements.put(statementId, prepared);
+            SystemKeyspace.writePreparedStatement(keyspace, statementId, queryString);
             return new ResultMessage.Prepared(statementId, prepared);
         }
     }
@@ -566,13 +618,14 @@
     public ResultMessage processPrepared(CQLStatement statement,
                                          QueryState state,
                                          QueryOptions options,
-                                         Map<String, ByteBuffer> customPayload)
+                                         Map<String, ByteBuffer> customPayload,
+                                         long queryStartNanoTime)
                                                  throws RequestExecutionException, RequestValidationException
     {
-        return processPrepared(statement, state, options);
+        return processPrepared(statement, state, options, queryStartNanoTime);
     }
 
-    public ResultMessage processPrepared(CQLStatement statement, QueryState queryState, QueryOptions options)
+    public ResultMessage processPrepared(CQLStatement statement, QueryState queryState, QueryOptions options, long queryStartNanoTime)
     throws RequestExecutionException, RequestValidationException
     {
         List<ByteBuffer> variables = options.getValues();
@@ -592,26 +645,27 @@
         }
 
         metrics.preparedStatementsExecuted.inc();
-        return processStatement(statement, queryState, options);
+        return processStatement(statement, queryState, options, queryStartNanoTime);
     }
 
     public ResultMessage processBatch(BatchStatement statement,
                                       QueryState state,
                                       BatchQueryOptions options,
-                                      Map<String, ByteBuffer> customPayload)
+                                      Map<String, ByteBuffer> customPayload,
+                                      long queryStartNanoTime)
                                               throws RequestExecutionException, RequestValidationException
     {
-        return processBatch(statement, state, options);
+        return processBatch(statement, state, options, queryStartNanoTime);
     }
 
-    public ResultMessage processBatch(BatchStatement batch, QueryState queryState, BatchQueryOptions options)
+    public ResultMessage processBatch(BatchStatement batch, QueryState queryState, BatchQueryOptions options, long queryStartNanoTime)
     throws RequestExecutionException, RequestValidationException
     {
         ClientState clientState = queryState.getClientState();
         batch.checkAccess(clientState);
         batch.validate();
         batch.validate(clientState);
-        return batch.execute(queryState, options);
+        return batch.execute(queryState, options, queryStartNanoTime);
     }
 
     public static ParsedStatement.Prepared getStatement(String queryStr, ClientState clientState)
@@ -628,6 +682,22 @@
         return statement.prepare(clientState);
     }
 
+    public static <T extends ParsedStatement> T parseStatement(String queryStr, Class<T> klass, String type) throws SyntaxException
+    {
+        try
+        {
+            ParsedStatement stmt = parseStatement(queryStr);
+
+            if (!klass.isAssignableFrom(stmt.getClass()))
+                throw new IllegalArgumentException("Invalid query, must be a " + type + " statement but was: " + stmt.getClass());
+
+            return klass.cast(stmt);
+        }
+        catch (RequestValidationException e)
+        {
+            throw new IllegalArgumentException(e.getMessage(), e);
+        }
+    }
     public static ParsedStatement parseStatement(String queryStr) throws SyntaxException
     {
         try
@@ -652,9 +722,9 @@
         }
     }
 
-    private static long measure(Object key)
+    private static int measure(Object key, ParsedStatement.Prepared value)
     {
-        return meter.measureDeep(key);
+        return Ints.checkedCast(ObjectSizes.measureDeep(key) + ObjectSizes.measureDeep(value));
     }
 
     /**
@@ -680,14 +750,51 @@
 
     private static class MigrationSubscriber extends MigrationListener
     {
-        private void removeInvalidPreparedStatements(String ksName, String cfName)
+        private static void removeInvalidPreparedStatements(String ksName, String cfName)
         {
             removeInvalidPreparedStatements(internalStatements.values().iterator(), ksName, cfName);
-            removeInvalidPreparedStatements(preparedStatements.values().iterator(), ksName, cfName);
+            removeInvalidPersistentPreparedStatements(preparedStatements.entrySet().iterator(), ksName, cfName);
             removeInvalidPreparedStatements(thriftPreparedStatements.values().iterator(), ksName, cfName);
         }
 
-        private void removeInvalidPreparedStatements(Iterator<ParsedStatement.Prepared> iterator, String ksName, String cfName)
+        private static void removeInvalidPreparedStatementsForFunction(String ksName, String functionName)
+        {
+            Predicate<Function> matchesFunction = f -> ksName.equals(f.name().keyspace) && functionName.equals(f.name().name);
+
+            for (Iterator<Map.Entry<MD5Digest, ParsedStatement.Prepared>> iter = preparedStatements.entrySet().iterator();
+                 iter.hasNext();)
+            {
+                Map.Entry<MD5Digest, ParsedStatement.Prepared> pstmt = iter.next();
+                if (Iterables.any(pstmt.getValue().statement.getFunctions(), matchesFunction))
+                {
+                    SystemKeyspace.removePreparedStatement(pstmt.getKey());
+                    iter.remove();
+                }
+            }
+
+
+            Iterators.removeIf(internalStatements.values().iterator(),
+                               statement -> Iterables.any(statement.statement.getFunctions(), matchesFunction));
+
+            Iterators.removeIf(thriftPreparedStatements.values().iterator(),
+                               statement -> Iterables.any(statement.statement.getFunctions(), matchesFunction));
+        }
+
+        private static void removeInvalidPersistentPreparedStatements(Iterator<Map.Entry<MD5Digest, ParsedStatement.Prepared>> iterator,
+                                                                      String ksName, String cfName)
+        {
+            while (iterator.hasNext())
+            {
+                Map.Entry<MD5Digest, ParsedStatement.Prepared> entry = iterator.next();
+                if (shouldInvalidate(ksName, cfName, entry.getValue().statement))
+                {
+                    SystemKeyspace.removePreparedStatement(entry.getKey());
+                    iterator.remove();
+                }
+            }
+        }
+
+        private static void removeInvalidPreparedStatements(Iterator<ParsedStatement.Prepared> iterator, String ksName, String cfName)
         {
             while (iterator.hasNext())
             {
@@ -696,7 +803,7 @@
             }
         }
 
-        private boolean shouldInvalidate(String ksName, String cfName, CQLStatement statement)
+        private static boolean shouldInvalidate(String ksName, String cfName, CQLStatement statement)
         {
             String statementKsName;
             String statementCfName;
@@ -746,7 +853,7 @@
             // in case there are other overloads, we have to remove all overloads since argument type
             // matching may change (due to type casting)
             if (Schema.instance.getKSMetaData(ksName).functions.get(new FunctionName(ksName, functionName)).size() > 1)
-                removeAllInvalidPreparedStatementsForFunction(ksName, functionName);
+                removeInvalidPreparedStatementsForFunction(ksName, functionName);
         }
 
         public void onUpdateColumnFamily(String ksName, String cfName, boolean affectsStatements)
@@ -762,7 +869,7 @@
             // the new definition is picked (the function is resolved at preparation time).
             // TODO: if the function has multiple overload, we could invalidate only the statement refering to the overload
             // that was updated. This requires a few changes however and probably doesn't matter much in practice.
-            removeAllInvalidPreparedStatementsForFunction(ksName, functionName);
+            removeInvalidPreparedStatementsForFunction(ksName, functionName);
         }
 
         public void onUpdateAggregate(String ksName, String aggregateName, List<AbstractType<?>> argTypes)
@@ -771,7 +878,7 @@
             // the new definition is picked (the function is resolved at preparation time).
             // TODO: if the function has multiple overload, we could invalidate only the statement refering to the overload
             // that was updated. This requires a few changes however and probably doesn't matter much in practice.
-            removeAllInvalidPreparedStatementsForFunction(ksName, aggregateName);
+            removeInvalidPreparedStatementsForFunction(ksName, aggregateName);
         }
 
         public void onDropKeyspace(String ksName)
@@ -788,27 +895,12 @@
 
         public void onDropFunction(String ksName, String functionName, List<AbstractType<?>> argTypes)
         {
-            removeAllInvalidPreparedStatementsForFunction(ksName, functionName);
+            removeInvalidPreparedStatementsForFunction(ksName, functionName);
         }
 
         public void onDropAggregate(String ksName, String aggregateName, List<AbstractType<?>> argTypes)
         {
-            removeAllInvalidPreparedStatementsForFunction(ksName, aggregateName);
-        }
-
-        private static void removeAllInvalidPreparedStatementsForFunction(String ksName, String functionName)
-        {
-            removeInvalidPreparedStatementsForFunction(internalStatements.values().iterator(), ksName, functionName);
-            removeInvalidPreparedStatementsForFunction(preparedStatements.values().iterator(), ksName, functionName);
-            removeInvalidPreparedStatementsForFunction(thriftPreparedStatements.values().iterator(), ksName, functionName);
-        }
-
-        private static void removeInvalidPreparedStatementsForFunction(Iterator<ParsedStatement.Prepared> statements,
-                                                                       final String ksName,
-                                                                       final String functionName)
-        {
-            Predicate<Function> matchesFunction = f -> ksName.equals(f.name().keyspace) && functionName.equals(f.name().name);
-            Iterators.removeIf(statements, statement -> Iterables.any(statement.statement.getFunctions(), matchesFunction));
+            removeInvalidPreparedStatementsForFunction(ksName, aggregateName);
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/cql3/Relation.java b/src/java/org/apache/cassandra/cql3/Relation.java
index 005d984..9537ca1 100644
--- a/src/java/org/apache/cassandra/cql3/Relation.java
+++ b/src/java/org/apache/cassandra/cql3/Relation.java
@@ -25,13 +25,11 @@
 import org.apache.cassandra.cql3.restrictions.Restriction;
 import org.apache.cassandra.cql3.statements.Bound;
 import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.exceptions.UnrecognizedEntityException;
-import sun.reflect.generics.reflectiveObjects.NotImplementedException;
 
 import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
 
-public abstract class Relation {
-
+public abstract class Relation
+{
     protected Operator relationType;
 
     public Operator operator()
@@ -109,6 +107,15 @@
         return relationType == Operator.EQ;
     }
 
+    public final boolean isLIKE()
+    {
+        return relationType == Operator.LIKE_PREFIX
+                || relationType == Operator.LIKE_SUFFIX
+                || relationType == Operator.LIKE_CONTAINS
+                || relationType == Operator.LIKE_MATCHES
+                || relationType == Operator.LIKE;
+    }
+
     /**
      * Checks if the operator of this relation is a <code>Slice</code> (GT, GTE, LTE, LT).
      *
@@ -144,6 +151,12 @@
             case CONTAINS: return newContainsRestriction(cfm, boundNames, false);
             case CONTAINS_KEY: return newContainsRestriction(cfm, boundNames, true);
             case IS_NOT: return newIsNotRestriction(cfm, boundNames);
+            case LIKE_PREFIX:
+            case LIKE_SUFFIX:
+            case LIKE_CONTAINS:
+            case LIKE_MATCHES:
+            case LIKE:
+                return newLikeRestriction(cfm, boundNames, relationType);
             default: throw invalidRequest("Unsupported \"!=\" relation: %s", this);
         }
     }
@@ -210,6 +223,10 @@
     protected abstract Restriction newIsNotRestriction(CFMetaData cfm,
                                                        VariableSpecifications boundNames) throws InvalidRequestException;
 
+    protected abstract Restriction newLikeRestriction(CFMetaData cfm,
+                                                      VariableSpecifications boundNames,
+                                                      Operator operator) throws InvalidRequestException;
+
     /**
      * Converts the specified <code>Raw</code> into a <code>Term</code>.
      * @param receivers the columns to which the values must be associated at
@@ -252,31 +269,11 @@
     }
 
     /**
-     * Converts the specified entity into a column definition.
-     *
-     * @param cfm the column family meta data
-     * @param entity the entity to convert
-     * @return the column definition corresponding to the specified entity
-     * @throws InvalidRequestException if the entity cannot be recognized
-     */
-    protected final ColumnDefinition toColumnDefinition(CFMetaData cfm,
-                                                        ColumnIdentifier.Raw entity) throws InvalidRequestException
-    {
-        ColumnIdentifier identifier = entity.prepare(cfm);
-        ColumnDefinition def = cfm.getColumnDefinitionForCQL(identifier);
-
-        if (def == null)
-            throw new UnrecognizedEntityException(identifier, this);
-
-        return def;
-    }
-
-    /**
      * Renames an identifier in this Relation, if applicable.
      * @param from the old identifier
      * @param to the new identifier
      * @return this object, if the old identifier is not in the set of entities that this relation covers; otherwise
      *         a new Relation with "from" replaced by "to" is returned.
      */
-    public abstract Relation renameIdentifier(ColumnIdentifier.Raw from, ColumnIdentifier.Raw to);
+    public abstract Relation renameIdentifier(ColumnDefinition.Raw from, ColumnDefinition.Raw to);
 }
diff --git a/src/java/org/apache/cassandra/cql3/ReservedKeywords.java b/src/java/org/apache/cassandra/cql3/ReservedKeywords.java
index ee052a7..4dfc841 100644
--- a/src/java/org/apache/cassandra/cql3/ReservedKeywords.java
+++ b/src/java/org/apache/cassandra/cql3/ReservedKeywords.java
@@ -18,79 +18,47 @@
 
 package org.apache.cassandra.cql3;
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
 import java.util.Set;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
 
+import org.apache.cassandra.exceptions.ConfigurationException;
+
 class ReservedKeywords
 {
-    @VisibleForTesting
-    static final String[] reservedKeywords = new String[]
-                                                     {
-                                                     "SELECT",
-                                                     "FROM",
-                                                     "WHERE",
-                                                     "AND",
-                                                     "ENTRIES",
-                                                     "FULL",
-                                                     "INSERT",
-                                                     "UPDATE",
-                                                     "WITH",
-                                                     "LIMIT",
-                                                     "USING",
-                                                     "USE",
-                                                     "SET",
-                                                     "BEGIN",
-                                                     "UNLOGGED",
-                                                     "BATCH",
-                                                     "APPLY",
-                                                     "TRUNCATE",
-                                                     "DELETE",
-                                                     "IN",
-                                                     "CREATE",
-                                                     "KEYSPACE",
-                                                     "SCHEMA",
-                                                     "COLUMNFAMILY",
-                                                     "TABLE",
-                                                     "MATERIALIZED",
-                                                     "VIEW",
-                                                     "INDEX",
-                                                     "ON",
-                                                     "TO",
-                                                     "DROP",
-                                                     "PRIMARY",
-                                                     "INTO",
-                                                     "ALTER",
-                                                     "RENAME",
-                                                     "ADD",
-                                                     "ORDER",
-                                                     "BY",
-                                                     "ASC",
-                                                     "DESC",
-                                                     "ALLOW",
-                                                     "IF",
-                                                     "IS",
-                                                     "GRANT",
-                                                     "OF",
-                                                     "REVOKE",
-                                                     "MODIFY",
-                                                     "AUTHORIZE",
-                                                     "DESCRIBE",
-                                                     "EXECUTE",
-                                                     "NORECURSIVE",
-                                                     "TOKEN",
-                                                     "NULL",
-                                                     "NOT",
-                                                     "NAN",
-                                                     "INFINITY",
-                                                     "OR",
-                                                     "REPLACE" };
 
-    private static final Set<String> reservedSet = ImmutableSet.copyOf(reservedKeywords);
+    private static final String FILE_NAME = "reserved_keywords.txt";
+
+    @VisibleForTesting
+    static final Set<String> reservedKeywords = getFromResource();
+
+    private static Set<String> getFromResource()
+    {
+        ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+        try (InputStream is = ReservedKeywords.class.getResource(FILE_NAME).openConnection().getInputStream();
+             BufferedReader r = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)))
+        {
+            String line;
+            while ((line = r.readLine()) != null)
+            {
+                builder.add(line.trim());
+            }
+        }
+        catch (IOException e)
+        {
+            throw new ConfigurationException(String.format("Unable to read reserved keywords file '%s'", FILE_NAME), e);
+        }
+        return builder.build();
+    }
 
     static boolean isReserved(String text)
     {
-        return reservedSet.contains(text.toUpperCase());
+        return reservedKeywords.contains(text.toUpperCase());
     }
 }
diff --git a/src/java/org/apache/cassandra/cql3/ResultSet.java b/src/java/org/apache/cassandra/cql3/ResultSet.java
index 92d0595..f920559 100644
--- a/src/java/org/apache/cassandra/cql3/ResultSet.java
+++ b/src/java/org/apache/cassandra/cql3/ResultSet.java
@@ -32,6 +32,7 @@
 import org.apache.cassandra.thrift.CqlRow;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.service.pager.PagingState;
+
 import com.google.common.annotations.VisibleForTesting;
 
 public class ResultSet
@@ -178,7 +179,7 @@
          *   - rows count (4 bytes)
          *   - rows
          */
-        public ResultSet decode(ByteBuf body, int version)
+        public ResultSet decode(ByteBuf body, ProtocolVersion version)
         {
             ResultMetadata m = ResultMetadata.codec.decode(body, version);
             int rowCount = body.readInt();
@@ -192,7 +193,7 @@
             return rs;
         }
 
-        public void encode(ResultSet rs, ByteBuf dest, int version)
+        public void encode(ResultSet rs, ByteBuf dest, ProtocolVersion version)
         {
             ResultMetadata.codec.encode(rs.metadata, dest, version);
             dest.writeInt(rs.rows.size());
@@ -205,7 +206,7 @@
             }
         }
 
-        public int encodedSize(ResultSet rs, int version)
+        public int encodedSize(ResultSet rs, ProtocolVersion version)
         {
             int size = ResultMetadata.codec.encodedSize(rs.metadata, version) + 4;
             for (List<ByteBuffer> row : rs.rows)
@@ -308,6 +309,29 @@
         }
 
         @Override
+        public boolean equals(Object other)
+        {
+            if (this == other)
+                return true;
+
+            if (!(other instanceof ResultMetadata))
+                return false;
+
+            ResultMetadata that = (ResultMetadata) other;
+
+            return Objects.equals(flags, that.flags)
+                   && Objects.equals(names, that.names)
+                   && columnCount == that.columnCount
+                   && Objects.equals(pagingState, that.pagingState);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash(flags, names, columnCount, pagingState);
+        }
+
+        @Override
         public String toString()
         {
             StringBuilder sb = new StringBuilder();
@@ -332,7 +356,7 @@
 
         private static class Codec implements CBCodec<ResultMetadata>
         {
-            public ResultMetadata decode(ByteBuf body, int version)
+            public ResultMetadata decode(ByteBuf body, ProtocolVersion version)
             {
                 // flags & column count
                 int iflags = body.readInt();
@@ -370,13 +394,14 @@
                 return new ResultMetadata(flags, names, names.size(), state);
             }
 
-            public void encode(ResultMetadata m, ByteBuf dest, int version)
+            public void encode(ResultMetadata m, ByteBuf dest, ProtocolVersion version)
             {
                 boolean noMetadata = m.flags.contains(Flag.NO_METADATA);
                 boolean globalTablesSpec = m.flags.contains(Flag.GLOBAL_TABLES_SPEC);
                 boolean hasMorePages = m.flags.contains(Flag.HAS_MORE_PAGES);
 
-                assert version > 1 || (!hasMorePages && !noMetadata): "version = " + version + ", flags = " + m.flags;
+                assert version.isGreaterThan(ProtocolVersion.V1) || (!hasMorePages && !noMetadata)
+                    : "version = " + version + ", flags = " + m.flags;
 
                 dest.writeInt(Flag.serialize(m.flags));
                 dest.writeInt(m.columnCount);
@@ -406,7 +431,7 @@
                 }
             }
 
-            public int encodedSize(ResultMetadata m, int version)
+            public int encodedSize(ResultMetadata m, ProtocolVersion version)
             {
                 boolean noMetadata = m.flags.contains(Flag.NO_METADATA);
                 boolean globalTablesSpec = m.flags.contains(Flag.GLOBAL_TABLES_SPEC);
@@ -450,16 +475,16 @@
 
         private final EnumSet<Flag> flags;
         public final List<ColumnSpecification> names;
-        private final Short[] partitionKeyBindIndexes;
+        private final short[] partitionKeyBindIndexes;
 
-        public PreparedMetadata(List<ColumnSpecification> names, Short[] partitionKeyBindIndexes)
+        public PreparedMetadata(List<ColumnSpecification> names, short[] partitionKeyBindIndexes)
         {
             this(EnumSet.noneOf(Flag.class), names, partitionKeyBindIndexes);
             if (!names.isEmpty() && ColumnSpecification.allInSameTable(names))
                 flags.add(Flag.GLOBAL_TABLES_SPEC);
         }
 
-        private PreparedMetadata(EnumSet<Flag> flags, List<ColumnSpecification> names, Short[] partitionKeyBindIndexes)
+        private PreparedMetadata(EnumSet<Flag> flags, List<ColumnSpecification> names, short[] partitionKeyBindIndexes)
         {
             this.flags = flags;
             this.names = names;
@@ -474,6 +499,9 @@
         @Override
         public boolean equals(Object other)
         {
+            if (this == other)
+                return true;
+
             if (!(other instanceof PreparedMetadata))
                 return false;
 
@@ -484,6 +512,12 @@
         }
 
         @Override
+        public int hashCode()
+        {
+            return Objects.hash(names, flags) + Arrays.hashCode(partitionKeyBindIndexes);
+        }
+
+        @Override
         public String toString()
         {
             StringBuilder sb = new StringBuilder();
@@ -510,7 +544,7 @@
 
         private static class Codec implements CBCodec<PreparedMetadata>
         {
-            public PreparedMetadata decode(ByteBuf body, int version)
+            public PreparedMetadata decode(ByteBuf body, ProtocolVersion version)
             {
                 // flags & column count
                 int iflags = body.readInt();
@@ -518,13 +552,13 @@
 
                 EnumSet<Flag> flags = Flag.deserialize(iflags);
 
-                Short[] partitionKeyBindIndexes = null;
-                if (version >= Server.VERSION_4)
+                short[] partitionKeyBindIndexes = null;
+                if (version.isGreaterOrEqualTo(ProtocolVersion.V4))
                 {
                     int numPKNames = body.readInt();
                     if (numPKNames > 0)
                     {
-                        partitionKeyBindIndexes = new Short[numPKNames];
+                        partitionKeyBindIndexes = new short[numPKNames];
                         for (int i = 0; i < numPKNames; i++)
                             partitionKeyBindIndexes[i] = body.readShort();
                     }
@@ -553,13 +587,13 @@
                 return new PreparedMetadata(flags, names, partitionKeyBindIndexes);
             }
 
-            public void encode(PreparedMetadata m, ByteBuf dest, int version)
+            public void encode(PreparedMetadata m, ByteBuf dest, ProtocolVersion version)
             {
                 boolean globalTablesSpec = m.flags.contains(Flag.GLOBAL_TABLES_SPEC);
                 dest.writeInt(Flag.serialize(m.flags));
                 dest.writeInt(m.names.size());
 
-                if (version >= Server.VERSION_4)
+                if (version.isGreaterOrEqualTo(ProtocolVersion.V4))
                 {
                     // there's no point in providing partition key bind indexes if the statements affect multiple tables
                     if (m.partitionKeyBindIndexes == null || !globalTablesSpec)
@@ -592,7 +626,7 @@
                 }
             }
 
-            public int encodedSize(PreparedMetadata m, int version)
+            public int encodedSize(PreparedMetadata m, ProtocolVersion version)
             {
                 boolean globalTablesSpec = m.flags.contains(Flag.GLOBAL_TABLES_SPEC);
                 int size = 8;
@@ -602,7 +636,7 @@
                     size += CBUtil.sizeOfString(m.names.get(0).cfName);
                 }
 
-                if (m.partitionKeyBindIndexes != null && version >= 4)
+                if (m.partitionKeyBindIndexes != null && version.isGreaterOrEqualTo(ProtocolVersion.V4))
                     size += 4 + 2 * m.partitionKeyBindIndexes.length;
 
                 for (ColumnSpecification name : m.names)
@@ -620,7 +654,7 @@
         }
     }
 
-    public static enum Flag
+    public enum Flag
     {
         // The order of that enum matters!!
         GLOBAL_TABLES_SPEC,
diff --git a/src/java/org/apache/cassandra/cql3/Sets.java b/src/java/org/apache/cassandra/cql3/Sets.java
index 5627f6b..120c31b 100644
--- a/src/java/org/apache/cassandra/cql3/Sets.java
+++ b/src/java/org/apache/cassandra/cql3/Sets.java
@@ -31,7 +31,7 @@
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.serializers.CollectionSerializer;
 import org.apache.cassandra.serializers.MarshalException;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 /**
@@ -134,6 +134,18 @@
             return AssignmentTestable.TestResult.testAll(keyspace, valueSpec, elements);
         }
 
+        @Override
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            for (Term.Raw term : elements)
+            {
+                AbstractType<?> type = term.getExactTypeIfKnown(keyspace);
+                if (type != null)
+                    return SetType.getInstance(type, false);
+            }
+            return null;
+        }
+
         public String getText()
         {
             return elements.stream().map(Term.Raw::getText).collect(Collectors.joining(", ", "{", "}"));
@@ -149,7 +161,7 @@
             this.elements = elements;
         }
 
-        public static Value fromSerialized(ByteBuffer value, SetType type, int version) throws InvalidRequestException
+        public static Value fromSerialized(ByteBuffer value, SetType type, ProtocolVersion version) throws InvalidRequestException
         {
             try
             {
@@ -167,7 +179,7 @@
             }
         }
 
-        public ByteBuffer get(int protocolVersion)
+        public ByteBuffer get(ProtocolVersion protocolVersion)
         {
             return CollectionSerializer.pack(elements, elements.size(), protocolVersion);
         }
@@ -308,7 +320,7 @@
                 if (value == null)
                     params.addTombstone(column);
                 else
-                    params.addCell(column, value.get(Server.CURRENT_VERSION));
+                    params.addCell(column, value.get(ProtocolVersion.CURRENT));
             }
         }
     }
diff --git a/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java b/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java
index 455ae0c..2e9b41f 100644
--- a/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java
+++ b/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java
@@ -34,20 +34,21 @@
 
 import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
 import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue;
+import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
 
 /**
  * Relations encapsulate the relationship between an entity of some kind, and
- * a value (term). For example, <key> > "start" or "colname1" = "somevalue".
+ * a value (term). For example, {@code <key> > "start" or "colname1" = "somevalue"}.
  *
  */
 public class SingleColumnRelation extends Relation
 {
-    private final ColumnIdentifier.Raw entity;
+    private final ColumnDefinition.Raw entity;
     private final Term.Raw mapKey;
     private final Term.Raw value;
     private final List<Term.Raw> inValues;
 
-    private SingleColumnRelation(ColumnIdentifier.Raw entity, Term.Raw mapKey, Operator type, Term.Raw value, List<Term.Raw> inValues)
+    private SingleColumnRelation(ColumnDefinition.Raw entity, Term.Raw mapKey, Operator type, Term.Raw value, List<Term.Raw> inValues)
     {
         this.entity = entity;
         this.mapKey = mapKey;
@@ -67,7 +68,7 @@
      * @param type the type that describes how this entity relates to the value.
      * @param value the value being compared.
      */
-    public SingleColumnRelation(ColumnIdentifier.Raw entity, Term.Raw mapKey, Operator type, Term.Raw value)
+    public SingleColumnRelation(ColumnDefinition.Raw entity, Term.Raw mapKey, Operator type, Term.Raw value)
     {
         this(entity, mapKey, type, value, null);
     }
@@ -79,7 +80,7 @@
      * @param type the type that describes how this entity relates to the value.
      * @param value the value being compared.
      */
-    public SingleColumnRelation(ColumnIdentifier.Raw entity, Operator type, Term.Raw value)
+    public SingleColumnRelation(ColumnDefinition.Raw entity, Operator type, Term.Raw value)
     {
         this(entity, null, type, value);
     }
@@ -94,12 +95,12 @@
         return inValues;
     }
 
-    public static SingleColumnRelation createInRelation(ColumnIdentifier.Raw entity, List<Term.Raw> inValues)
+    public static SingleColumnRelation createInRelation(ColumnDefinition.Raw entity, List<Term.Raw> inValues)
     {
         return new SingleColumnRelation(entity, null, Operator.IN, null, inValues);
     }
 
-    public ColumnIdentifier.Raw getEntity()
+    public ColumnDefinition.Raw getEntity()
     {
         return entity;
     }
@@ -133,7 +134,7 @@
         }
     }
 
-    public Relation renameIdentifier(ColumnIdentifier.Raw from, ColumnIdentifier.Raw to)
+    public Relation renameIdentifier(ColumnDefinition.Raw from, ColumnDefinition.Raw to)
     {
         return entity.equals(from)
                ? new SingleColumnRelation(to, mapKey, operator(), value, inValues)
@@ -157,13 +158,13 @@
     protected Restriction newEQRestriction(CFMetaData cfm,
                                            VariableSpecifications boundNames) throws InvalidRequestException
     {
-        ColumnDefinition columnDef = toColumnDefinition(cfm, entity);
+        ColumnDefinition columnDef = entity.prepare(cfm);
         if (mapKey == null)
         {
-            Term term = toTerm(toReceivers(columnDef, cfm.isDense()), value, cfm.ksName, boundNames);
+            Term term = toTerm(toReceivers(columnDef), value, cfm.ksName, boundNames);
             return new SingleColumnRestriction.EQRestriction(columnDef, term);
         }
-        List<? extends ColumnSpecification> receivers = toReceivers(columnDef, cfm.isDense());
+        List<? extends ColumnSpecification> receivers = toReceivers(columnDef);
         Term entryKey = toTerm(Collections.singletonList(receivers.get(0)), mapKey, cfm.ksName, boundNames);
         Term entryValue = toTerm(Collections.singletonList(receivers.get(1)), value, cfm.ksName, boundNames);
         return new SingleColumnRestriction.ContainsRestriction(columnDef, entryKey, entryValue);
@@ -173,14 +174,19 @@
     protected Restriction newINRestriction(CFMetaData cfm,
                                            VariableSpecifications boundNames) throws InvalidRequestException
     {
-        ColumnDefinition columnDef = toColumnDefinition(cfm, entity);
-        List<? extends ColumnSpecification> receivers = toReceivers(columnDef, cfm.isDense());
+        ColumnDefinition columnDef = entity.prepare(cfm);
+        List<? extends ColumnSpecification> receivers = toReceivers(columnDef);
         List<Term> terms = toTerms(receivers, inValues, cfm.ksName, boundNames);
         if (terms == null)
         {
             Term term = toTerm(receivers, value, cfm.ksName, boundNames);
             return new SingleColumnRestriction.InRestrictionWithMarker(columnDef, (Lists.Marker) term);
         }
+
+        // An IN restrictions with only one element is the same than an EQ restriction
+        if (terms.size() == 1)
+            return new SingleColumnRestriction.EQRestriction(columnDef, terms.get(0));
+
         return new SingleColumnRestriction.InRestrictionWithValues(columnDef, terms);
     }
 
@@ -190,8 +196,17 @@
                                               Bound bound,
                                               boolean inclusive) throws InvalidRequestException
     {
-        ColumnDefinition columnDef = toColumnDefinition(cfm, entity);
-        Term term = toTerm(toReceivers(columnDef, cfm.isDense()), value, cfm.ksName, boundNames);
+        ColumnDefinition columnDef = entity.prepare(cfm);
+
+        if (columnDef.type.referencesDuration())
+        {
+            checkFalse(columnDef.type.isCollection(), "Slice restrictions are not supported on collections containing durations");
+            checkFalse(columnDef.type.isTuple(), "Slice restrictions are not supported on tuples containing durations");
+            checkFalse(columnDef.type.isUDT(), "Slice restrictions are not supported on UDTs containing durations");
+            throw invalidRequest("Slice restrictions are not supported on duration columns");
+        }
+
+        Term term = toTerm(toReceivers(columnDef), value, cfm.ksName, boundNames);
         return new SingleColumnRestriction.SliceRestriction(columnDef, bound, inclusive, term);
     }
 
@@ -200,8 +215,8 @@
                                                  VariableSpecifications boundNames,
                                                  boolean isKey) throws InvalidRequestException
     {
-        ColumnDefinition columnDef = toColumnDefinition(cfm, entity);
-        Term term = toTerm(toReceivers(columnDef, cfm.isDense()), value, cfm.ksName, boundNames);
+        ColumnDefinition columnDef = entity.prepare(cfm);
+        Term term = toTerm(toReceivers(columnDef), value, cfm.ksName, boundNames);
         return new SingleColumnRestriction.ContainsRestriction(columnDef, term, isKey);
     }
 
@@ -209,21 +224,31 @@
     protected Restriction newIsNotRestriction(CFMetaData cfm,
                                               VariableSpecifications boundNames) throws InvalidRequestException
     {
-        ColumnDefinition columnDef = toColumnDefinition(cfm, entity);
+        ColumnDefinition columnDef = entity.prepare(cfm);
         // currently enforced by the grammar
         assert value == Constants.NULL_LITERAL : "Expected null literal for IS NOT relation: " + this.toString();
         return new SingleColumnRestriction.IsNotNullRestriction(columnDef);
     }
 
+    @Override
+    protected Restriction newLikeRestriction(CFMetaData cfm, VariableSpecifications boundNames, Operator operator) throws InvalidRequestException
+    {
+        if (mapKey != null)
+            throw invalidRequest("%s can't be used with collections.", operator());
+
+        ColumnDefinition columnDef = entity.prepare(cfm);
+        Term term = toTerm(toReceivers(columnDef), value, cfm.ksName, boundNames);
+
+        return new SingleColumnRestriction.LikeRestriction(columnDef, operator, term);
+    }
+
     /**
      * Returns the receivers for this relation.
      * @param columnDef the column definition
-     * @param isDense whether the table is a dense one
-     *
      * @return the receivers for the specified relation.
      * @throws InvalidRequestException if the relation is invalid
      */
-    private List<? extends ColumnSpecification> toReceivers(ColumnDefinition columnDef, boolean isDense) throws InvalidRequestException
+    private List<? extends ColumnSpecification> toReceivers(ColumnDefinition columnDef) throws InvalidRequestException
     {
         ColumnSpecification receiver = columnDef;
 
@@ -236,17 +261,6 @@
             checkFalse(!columnDef.isPrimaryKeyColumn() && !canHaveOnlyOneValue(),
                        "IN predicates on non-primary-key columns (%s) is not yet supported", columnDef.name);
         }
-        else if (isSlice())
-        {
-            // Non EQ relation is not supported without token(), even if we have a 2ndary index (since even those
-            // are ordered by partitioner).
-            // Note: In theory we could allow it for 2ndary index queries with ALLOW FILTERING, but that would
-            // probably require some special casing
-            // Note bis: This is also why we don't bother handling the 'tuple' notation of #4851 for keys. If we
-            // lift the limitation for 2ndary
-            // index with filtering, we'll need to handle it though.
-            checkFalse(columnDef.isPartitionKey(), "Only EQ and IN relation are supported on the partition key (unless you use the token() function)");
-        }
 
         checkFalse(isContainsKey() && !(receiver.type instanceof MapType), "Cannot use CONTAINS KEY on non-map column %s", receiver.name);
         checkFalse(isContains() && !(receiver.type.isCollection()), "Cannot use CONTAINS on non-collection column %s", receiver.name);
@@ -259,6 +273,12 @@
             checkTrue(isEQ(), "Only EQ relations are supported on map entries");
         }
 
+        // Non-frozen UDTs don't support any operator
+        checkFalse(receiver.type.isUDT() && receiver.type.isMultiCell(),
+                   "Non-frozen UDT column '%s' (%s) cannot be restricted by any relation",
+                   receiver.name,
+                   receiver.type.asCQL3Type());
+
         if (receiver.type.isCollection())
         {
             // We don't support relations against entire collections (unless they're frozen), like "numbers = {1, 2, 3}"
@@ -301,7 +321,7 @@
 
     private boolean canHaveOnlyOneValue()
     {
-        return isEQ() || (isIN() && inValues != null && inValues.size() == 1);
+        return isEQ() || isLIKE() || (isIN() && inValues != null && inValues.size() == 1);
     }
 
     @Override
@@ -316,7 +336,7 @@
      */
     private class SuperColumnSingleColumnRelation extends SingleColumnRelation
     {
-        SuperColumnSingleColumnRelation(ColumnIdentifier.Raw entity, Raw mapKey, Operator type, Raw value)
+        SuperColumnSingleColumnRelation(ColumnDefinition.Raw entity, Raw mapKey, Operator type, Raw value)
         {
             super(entity, mapKey, type, value, inValues);
         }
@@ -327,10 +347,10 @@
                                                Bound bound,
                                                boolean inclusive) throws InvalidRequestException
         {
-            ColumnDefinition columnDef = toColumnDefinition(cfm, entity);
+            ColumnDefinition columnDef = entity.prepare(cfm);
             if (cfm.isSuperColumnKeyColumn(columnDef))
             {
-                Term term = toTerm(toReceivers(columnDef, cfm.isDense()), value, cfm.ksName, boundNames);
+                Term term = toTerm(toReceivers(columnDef), value, cfm.ksName, boundNames);
                 return new SingleColumnRestriction.SuperColumnKeySliceRestriction(cfm.superColumnKeyColumn(), bound, inclusive, term);
             }
             else
@@ -343,10 +363,10 @@
         protected Restriction newEQRestriction(CFMetaData cfm,
                                                VariableSpecifications boundNames) throws InvalidRequestException
         {
-            ColumnDefinition columnDef = toColumnDefinition(cfm, entity);
+            ColumnDefinition columnDef = entity.prepare(cfm);
             if (cfm.isSuperColumnKeyColumn(columnDef))
             {
-                Term term = toTerm(toReceivers(columnDef, cfm.isDense()), value, cfm.ksName, boundNames);
+                Term term = toTerm(toReceivers(columnDef), value, cfm.ksName, boundNames);
                 return new SingleColumnRestriction.SuperColumnKeyEQRestriction(cfm.superColumnKeyColumn(), term);
             }
             else
@@ -359,7 +379,7 @@
         protected Restriction newINRestriction(CFMetaData cfm,
                                                VariableSpecifications boundNames) throws InvalidRequestException
         {
-            ColumnDefinition columnDef = toColumnDefinition(cfm, entity);
+            ColumnDefinition columnDef = entity.prepare(cfm);
             if (cfm.isSuperColumnKeyColumn(columnDef))
             {
                 List<? extends ColumnSpecification> receivers = Collections.singletonList(cfm.superColumnKeyColumn());
diff --git a/src/java/org/apache/cassandra/cql3/SuperColumnCompatibility.java b/src/java/org/apache/cassandra/cql3/SuperColumnCompatibility.java
index d4c14df..1fe0af0 100644
--- a/src/java/org/apache/cassandra/cql3/SuperColumnCompatibility.java
+++ b/src/java/org/apache/cassandra/cql3/SuperColumnCompatibility.java
@@ -29,8 +29,8 @@
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.cql3.restrictions.Restriction;
 import org.apache.cassandra.cql3.restrictions.SingleColumnRestriction;
+import org.apache.cassandra.cql3.restrictions.SingleRestriction;
 import org.apache.cassandra.cql3.restrictions.TermSlice;
 import org.apache.cassandra.cql3.selection.Selection;
 import org.apache.cassandra.cql3.statements.Bound;
@@ -48,6 +48,7 @@
 import org.apache.cassandra.db.rows.ComplexColumnData;
 import org.apache.cassandra.db.rows.Row;
 import org.apache.cassandra.db.rows.RowIterator;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.Pair;
@@ -180,7 +181,7 @@
      *   | partition-key | clustering-key | key2 | value2 |
      *
      */
-    public static void processPartition(CFMetaData cfm, Selection selection, RowIterator partition, Selection.ResultSetBuilder result, int protocolVersion,
+    public static void processPartition(CFMetaData cfm, Selection selection, RowIterator partition, Selection.ResultSetBuilder result, ProtocolVersion protocolVersion,
                                         SuperColumnRestrictions restrictions, QueryOptions queryOptions)
     {
         assert cfm.isDense();
@@ -223,7 +224,7 @@
 
                     // Multi-column restriction on clustering+SuperColumn key
                     if (restrictions.multiSliceRestriction != null &&
-                        cfm.comparator.compare(row.clustering(), new Clustering(restrictions.multiSliceRestriction.firstValue)) == 0)
+                        cfm.comparator.compare(row.clustering(), Clustering.make(restrictions.multiSliceRestriction.firstValue)) == 0)
                     {
                         AbstractType t = ((MapType) cfm.compactValueColumn().type).getKeysType();
                         int cmp = t.compare(superColumnKey, restrictions.multiSliceRestriction.secondValue);
@@ -235,7 +236,8 @@
                     }
                 }
 
-                result.newRow(protocolVersion);
+                Row staticRow = partition.staticRow();
+                result.newRow(partition.partitionKey(), staticRow.clustering());
 
                 for (ColumnDefinition def : selection.getColumns())
                 {
@@ -283,7 +285,7 @@
      * or adding / overwriting the value for `key1`.
      */
     public static void prepareInsertOperations(CFMetaData cfm,
-                                               List<ColumnIdentifier.Raw> columnNames,
+                                               List<ColumnDefinition.Raw> columnNames,
                                                WhereClause.Builder whereClause,
                                                List<Term.Raw> columnValues,
                                                VariableSpecifications boundNames,
@@ -292,8 +294,8 @@
         List<ColumnDefinition> defs = new ArrayList<>(columnNames.size());
         for (int i = 0; i < columnNames.size(); i++)
         {
-            ColumnIdentifier id = columnNames.get(i).prepare(cfm);
-            defs.add(cfm.getColumnDefinition(id));
+            ColumnDefinition id = columnNames.get(i).prepare(cfm);
+            defs.add(id);
         }
 
         prepareInsertOperations(cfm, defs, boundNames, columnValues, whereClause, operations);
@@ -314,7 +316,7 @@
     {
         List<Term.Raw> columnValues = new ArrayList<>(defs.size());
         for (ColumnDefinition def : defs)
-            columnValues.add(prepared.getRawTermForColumn(def));
+            columnValues.add(prepared.getRawTermForColumn(def, true));
 
         prepareInsertOperations(cfm, defs, boundNames, columnValues, whereClause, operations);
     }
@@ -349,7 +351,7 @@
             }
             else if (def.isPrimaryKeyColumn())
             {
-                whereClause.add(new SingleColumnRelation(new ColumnIdentifier.ColumnIdentifierValue(def.name), Operator.EQ, raw));
+                whereClause.add(new SingleColumnRelation(ColumnDefinition.Raw.forColumn(def), Operator.EQ, raw));
             }
             else
             {
@@ -362,7 +364,7 @@
         checkTrue(superColumnKey != null,
                   "Column key is mandatory for SuperColumn tables");
 
-        Operation operation = new Operation.SetElement(superColumnKey, superColumnValue).prepare(cfm.ksName, cfm.compactValueColumn());
+        Operation operation = new Operation.SetElement(superColumnKey, superColumnValue).prepare(cfm, cfm.compactValueColumn());
         operations.add(operation);
     }
 
@@ -391,7 +393,7 @@
      */
     public static WhereClause prepareUpdateOperations(CFMetaData cfm,
                                                       WhereClause whereClause,
-                                                      List<Pair<ColumnIdentifier.Raw, Operation.RawUpdate>> updates,
+                                                      List<Pair<ColumnDefinition.Raw, Operation.RawUpdate>> updates,
                                                       VariableSpecifications boundNames,
                                                       Operations operations)
     {
@@ -403,8 +405,7 @@
         for (int i = 0; i < whereClause.relations.size(); i++)
         {
             SingleColumnRelation relation = (SingleColumnRelation) whereClause.relations.get(i);
-            ColumnIdentifier id = relation.getEntity().prepare(cfm);
-            ColumnDefinition def = cfm.getColumnDefinition(id);
+            ColumnDefinition def = relation.getEntity().prepare(cfm);
 
             if (cfm.isSuperColumnKeyColumn(def))
             {
@@ -420,10 +421,9 @@
         checkTrue(superColumnKey != null,
                   "Column key is mandatory for SuperColumn tables");
 
-        for (Pair<ColumnIdentifier.Raw, Operation.RawUpdate> entry : updates)
+        for (Pair<ColumnDefinition.Raw, Operation.RawUpdate> entry : updates)
         {
-            ColumnIdentifier id = entry.left.prepare(cfm);
-            ColumnDefinition def = cfm.getColumnDefinition(id);
+            ColumnDefinition def = entry.left.prepare(cfm);
 
             if (!cfm.isSuperColumnValueColumn(def))
                 throw invalidRequest("Column `%s` of type `%s` found in SET part", def.name, def.type.asCQL3Type());
@@ -435,21 +435,21 @@
                 Operation.Addition op = (Operation.Addition) entry.right;
                 superColumnValue = op.value();
 
-                operation = new Operation.ElementAddition(superColumnKey, superColumnValue).prepare(cfm.ksName, cfm.compactValueColumn());
+                operation = new Operation.ElementAddition(superColumnKey, superColumnValue).prepare(cfm, cfm.compactValueColumn());
             }
             else if (entry.right instanceof Operation.Substraction)
             {
                 Operation.Substraction op = (Operation.Substraction) entry.right;
                 superColumnValue = op.value();
 
-                operation = new Operation.ElementSubtraction(superColumnKey, superColumnValue).prepare(cfm.ksName, cfm.compactValueColumn());
+                operation = new Operation.ElementSubtraction(superColumnKey, superColumnValue).prepare(cfm, cfm.compactValueColumn());
             }
             else if (entry.right instanceof Operation.SetValue)
             {
                 Operation.SetValue op = (Operation.SetValue) entry.right;
                 superColumnValue = op.value();
 
-                operation = new Operation.SetElement(superColumnKey, superColumnValue).prepare(cfm.ksName, cfm.compactValueColumn());
+                operation = new Operation.SetElement(superColumnKey, superColumnValue).prepare(cfm, cfm.compactValueColumn());
             }
             else
             {
@@ -511,8 +511,7 @@
         for (int i = 0; i < relations.size(); i++)
         {
             SingleColumnRelation relation = (SingleColumnRelation) relations.get(i);
-            ColumnIdentifier id = relation.getEntity().prepare(cfm);
-            ColumnDefinition def = cfm.getColumnDefinition(id);
+            ColumnDefinition def = relation.getEntity().prepare(cfm);
 
             if (cfm.isSuperColumnKeyColumn(def))
                 return Pair.create(def, relation);
@@ -551,8 +550,7 @@
                        "Token relations cannot be used in WHERE clauses for UPDATE and DELETE statements: %s", orig);
 
             SingleColumnRelation relation = (SingleColumnRelation) orig;
-            ColumnIdentifier id = relation.getEntity().prepare(cfm);
-            ColumnDefinition def = cfm.getColumnDefinition(id);
+            ColumnDefinition def = relation.getEntity().prepare(cfm);
 
             if (cfm.isSuperColumnKeyColumn(def))
             {
@@ -701,7 +699,7 @@
          * For the restrictions taking a form of
          *   ... AND column2 > 5
          * (non-inclusive ones), the items that match `=` will be filtered out
-         * by {@link #processPartition(CFMetaData, Selection, RowIterator, Selection.ResultSetBuilder, int, SuperColumnRestrictions, QueryOptions)}
+         * by {@link #processPartition(CFMetaData, Selection, RowIterator, Selection.ResultSetBuilder, ProtocolVersion, SuperColumnRestrictions, QueryOptions)}
          *
          * Unfortunately, there are no good ways to do it other than here:
          * {@link RowFilter} can't be used in this case, since the complex collection cells are not yet rows by that
@@ -730,7 +728,7 @@
          */
         private final SingleColumnRestriction.SuperColumnKeyEQRestriction keyEQRestriction;
 
-        public SuperColumnRestrictions(Iterator<Restriction> restrictions)
+        public SuperColumnRestrictions(Iterator<SingleRestriction> restrictions)
         {
             // In order to keep the fields final, assignments have to be done outside the loop
             SingleColumnRestriction.SuperColumnMultiSliceRestriction multiSliceRestriction = null;
@@ -741,7 +739,7 @@
 
             while (restrictions.hasNext())
             {
-                Restriction restriction = restrictions.next();
+                SingleRestriction restriction = restrictions.next();
 
                 if (restriction instanceof SingleColumnRestriction.SuperColumnMultiSliceRestriction)
                     multiSliceRestriction = (SingleColumnRestriction.SuperColumnMultiSliceRestriction) restriction;
diff --git a/src/java/org/apache/cassandra/cql3/Term.java b/src/java/org/apache/cassandra/cql3/Term.java
index 5ae9c18..11b9860 100644
--- a/src/java/org/apache/cassandra/cql3/Term.java
+++ b/src/java/org/apache/cassandra/cql3/Term.java
@@ -18,12 +18,12 @@
 package org.apache.cassandra.cql3;
 
 import java.nio.ByteBuffer;
-import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
 import org.apache.cassandra.cql3.functions.Function;
+import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * A CQL3 term, i.e. a column value with or without bind variables.
@@ -100,6 +100,17 @@
          */
         public abstract String getText();
 
+        /**
+         * The type of the {@code term} if it can be infered.
+         *
+         * @param keyspace the keyspace on which the statement containing this term is on.
+         * @return the type of this {@code Term} if inferrable, or {@code null}
+         * otherwise (for instance, the type isn't inferable for a bind marker. Even for
+         * literals, the exact type is not inferrable since they are valid for many
+         * different types and so this will return {@code null} too).
+         */
+        public abstract AbstractType<?> getExactTypeIfKnown(String keyspace);
+
         @Override
         public String toString()
         {
@@ -146,7 +157,7 @@
          * @return the serialized value of this terminal.
          * @param protocolVersion
          */
-        public abstract ByteBuffer get(int protocolVersion) throws InvalidRequestException;
+        public abstract ByteBuffer get(ProtocolVersion protocolVersion) throws InvalidRequestException;
 
         public ByteBuffer bindAndGet(QueryOptions options) throws InvalidRequestException
         {
diff --git a/src/java/org/apache/cassandra/cql3/TokenRelation.java b/src/java/org/apache/cassandra/cql3/TokenRelation.java
index 2c13b19..42464ef 100644
--- a/src/java/org/apache/cassandra/cql3/TokenRelation.java
+++ b/src/java/org/apache/cassandra/cql3/TokenRelation.java
@@ -47,11 +47,11 @@
  */
 public final class TokenRelation extends Relation
 {
-    private final List<ColumnIdentifier.Raw> entities;
+    private final List<ColumnDefinition.Raw> entities;
 
     private final Term.Raw value;
 
-    public TokenRelation(List<ColumnIdentifier.Raw> entities, Operator type, Term.Raw value)
+    public TokenRelation(List<ColumnDefinition.Raw> entities, Operator type, Term.Raw value)
     {
         this.entities = entities;
         this.relationType = type;
@@ -112,6 +112,12 @@
     }
 
     @Override
+    protected Restriction newLikeRestriction(CFMetaData cfm, VariableSpecifications boundNames, Operator operator) throws InvalidRequestException
+    {
+        throw invalidRequest("%s cannot be used with the token function", operator);
+    }
+
+    @Override
     protected Term toTerm(List<? extends ColumnSpecification> receivers,
                           Raw raw,
                           String keyspace,
@@ -122,12 +128,12 @@
         return term;
     }
 
-    public Relation renameIdentifier(ColumnIdentifier.Raw from, ColumnIdentifier.Raw to)
+    public Relation renameIdentifier(ColumnDefinition.Raw from, ColumnDefinition.Raw to)
     {
         if (!entities.contains(from))
             return this;
 
-        List<ColumnIdentifier.Raw> newEntities = entities.stream().map(e -> e.equals(from) ? to : e).collect(Collectors.toList());
+        List<ColumnDefinition.Raw> newEntities = entities.stream().map(e -> e.equals(from) ? to : e).collect(Collectors.toList());
         return new TokenRelation(newEntities, operator(), value);
     }
 
@@ -146,11 +152,9 @@
      */
     private List<ColumnDefinition> getColumnDefinitions(CFMetaData cfm) throws InvalidRequestException
     {
-        List<ColumnDefinition> columnDefs = new ArrayList<>();
-        for ( ColumnIdentifier.Raw raw : entities)
-        {
-            columnDefs.add(toColumnDefinition(cfm, raw));
-        }
+        List<ColumnDefinition> columnDefs = new ArrayList<>(entities.size());
+        for ( ColumnDefinition.Raw raw : entities)
+            columnDefs.add(raw.prepare(cfm));
         return columnDefs;
     }
 
diff --git a/src/java/org/apache/cassandra/cql3/Tuples.java b/src/java/org/apache/cassandra/cql3/Tuples.java
index c7564d3..01f3466 100644
--- a/src/java/org/apache/cassandra/cql3/Tuples.java
+++ b/src/java/org/apache/cassandra/cql3/Tuples.java
@@ -26,11 +26,11 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.cql3.Term.MultiColumnRaw;
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 /**
@@ -111,8 +111,10 @@
             for (int i = 0; i < elements.size(); i++)
             {
                 if (i >= tt.size())
+                {
                     throw new InvalidRequestException(String.format("Invalid tuple literal for %s: too many elements. Type %s expects %d but got %d",
-                                                                    receiver.name, tt.asCQL3Type(), tt.size(), elements.size()));
+                            receiver.name, tt.asCQL3Type(), tt.size(), elements.size()));
+                }
 
                 Term.Raw value = elements.get(i);
                 ColumnSpecification spec = componentSpecOf(receiver, i);
@@ -134,6 +136,20 @@
             }
         }
 
+        @Override
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            List<AbstractType<?>> types = new ArrayList<>(elements.size());
+            for (Term.Raw term : elements)
+            {
+                AbstractType<?> type = term.getExactTypeIfKnown(keyspace);
+                if (type == null)
+                    return null;
+                types.add(type);
+            }
+            return new TupleType(types);
+        }
+
         public String getText()
         {
             return elements.stream().map(Term.Raw::getText).collect(Collectors.joining(", ", "(", ")"));
@@ -154,10 +170,17 @@
 
         public static Value fromSerialized(ByteBuffer bytes, TupleType type)
         {
+            ByteBuffer[] values = type.split(bytes);
+            if (values.length > type.size())
+            {
+                throw new InvalidRequestException(String.format(
+                        "Tuple value contained too many fields (expected %s, got %s)", type.size(), values.length));
+            }
+
             return new Value(type.split(bytes));
         }
 
-        public ByteBuffer get(int protocolVersion)
+        public ByteBuffer get(ProtocolVersion protocolVersion)
         {
             return TupleType.buildValue(elements);
         }
@@ -199,6 +222,10 @@
 
         private ByteBuffer[] bindInternal(QueryOptions options) throws InvalidRequestException
         {
+            if (elements.size() > type.size())
+                throw new InvalidRequestException(String.format(
+                        "Tuple value contained too many fields (expected %s, got %s)", type.size(), elements.size()));
+
             ByteBuffer[] buffers = new ByteBuffer[elements.size()];
             for (int i = 0; i < elements.size(); i++)
             {
@@ -270,7 +297,7 @@
             }
         }
 
-        public ByteBuffer get(int protocolVersion)
+        public ByteBuffer get(ProtocolVersion protocolVersion)
         {
             throw new UnsupportedOperationException();
         }
@@ -313,6 +340,11 @@
             return new ColumnSpecification(receivers.get(0).ksName, receivers.get(0).cfName, identifier, type);
         }
 
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            return null;
+        }
+
         public AbstractMarker prepare(String keyspace, List<? extends ColumnSpecification> receivers) throws InvalidRequestException
         {
             return new Tuples.Marker(bindIndex, makeReceiver(receivers));
@@ -352,6 +384,11 @@
             return new ColumnSpecification(receivers.get(0).ksName, receivers.get(0).cfName, identifier, ListType.getInstance(type, false));
         }
 
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            return null;
+        }
+
         public AbstractMarker prepare(String keyspace, List<? extends ColumnSpecification> receivers) throws InvalidRequestException
         {
             return new InMarker(bindIndex, makeInReceiver(receivers));
diff --git a/src/java/org/apache/cassandra/cql3/TypeCast.java b/src/java/org/apache/cassandra/cql3/TypeCast.java
index 890b34f..7b2f306 100644
--- a/src/java/org/apache/cassandra/cql3/TypeCast.java
+++ b/src/java/org/apache/cassandra/cql3/TypeCast.java
@@ -58,6 +58,11 @@
             return AssignmentTestable.TestResult.NOT_ASSIGNABLE;
     }
 
+    public AbstractType<?> getExactTypeIfKnown(String keyspace)
+    {
+        return type.prepare(keyspace).getType();
+    }
+
     public String getText()
     {
         return "(" + type + ")" + term;
diff --git a/src/java/org/apache/cassandra/cql3/UntypedResultSet.java b/src/java/org/apache/cassandra/cql3/UntypedResultSet.java
index dc4237d..9176b1f 100644
--- a/src/java/org/apache/cassandra/cql3/UntypedResultSet.java
+++ b/src/java/org/apache/cassandra/cql3/UntypedResultSet.java
@@ -25,6 +25,7 @@
 import com.google.common.annotations.VisibleForTesting;
 
 import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.AbstractIterator;
 
 import org.apache.cassandra.config.CFMetaData;
@@ -35,7 +36,6 @@
 import org.apache.cassandra.db.partitions.PartitionIterator;
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.service.pager.QueryPager;
-import org.apache.cassandra.transport.Server;
 import org.apache.cassandra.utils.FBUtilities;
 
 /** a utility for doing internal cql-based queries */
@@ -172,7 +172,7 @@
                         if (pager.isExhausted())
                             return endOfData();
 
-                        try (PartitionIterator iter = pager.fetchPage(pageSize, cl, clientState))
+                        try (PartitionIterator iter = pager.fetchPage(pageSize, cl, clientState, System.nanoTime()))
                         {
                             currentPage = select.process(iter, nowInSec).rows.iterator();
                         }
@@ -269,7 +269,8 @@
                         if (pager.isExhausted())
                             return endOfData();
 
-                        try (ReadOrderGroup orderGroup = pager.startOrderGroup(); PartitionIterator iter = pager.fetchPageInternal(pageSize, orderGroup))
+                        try (ReadExecutionController executionController = pager.executionController();
+                             PartitionIterator iter = pager.fetchPageInternal(pageSize, executionController))
                         {
                             currentPage = select.process(iter, nowInSec).rows.iterator();
                         }
@@ -326,7 +327,7 @@
                 {
                     ComplexColumnData complexData = row.getComplexColumnData(def);
                     if (complexData != null)
-                        data.put(def.name.toString(), ((CollectionType)def.type).serializeForNativeProtocol(def, complexData.iterator(), Server.VERSION_3));
+                        data.put(def.name.toString(), ((CollectionType)def.type).serializeForNativeProtocol(complexData.iterator(), ProtocolVersion.V3));
                 }
             }
 
@@ -379,6 +380,16 @@
             return data.get(column);
         }
 
+        public byte[] getByteArray(String column)
+        {
+            ByteBuffer buf = data.get(column);
+            byte[] arr = new byte[buf.remaining()];
+            for (int i = 0; i < arr.length; i++)
+                arr[i] = buf.get(buf.position() + i);
+
+            return arr;
+        }
+
         public InetAddress getInetAddress(String column)
         {
             return InetAddressType.instance.compose(data.get(column));
diff --git a/src/java/org/apache/cassandra/cql3/UpdateParameters.java b/src/java/org/apache/cassandra/cql3/UpdateParameters.java
index 7d09506..f9a1fae 100644
--- a/src/java/org/apache/cassandra/cql3/UpdateParameters.java
+++ b/src/java/org/apache/cassandra/cql3/UpdateParameters.java
@@ -60,13 +60,24 @@
                             long timestamp,
                             int ttl,
                             Map<DecoratedKey, Partition> prefetchedRows)
+    {
+        this(metadata, updatedColumns, options, timestamp, ttl, prefetchedRows, FBUtilities.nowInSeconds());
+    }
+
+    public UpdateParameters(CFMetaData metadata,
+                            PartitionColumns updatedColumns,
+                            QueryOptions options,
+                            long timestamp,
+                            int ttl,
+                            Map<DecoratedKey, Partition> prefetchedRows,
+                            int nowInSec)
     throws InvalidRequestException
     {
         this.metadata = metadata;
         this.updatedColumns = updatedColumns;
         this.options = options;
 
-        this.nowInSec = FBUtilities.nowInSeconds();
+        this.nowInSec = nowInSec;
         this.timestamp = timestamp;
         this.ttl = ttl;
 
@@ -116,7 +127,7 @@
 
     public void addPrimaryKeyLivenessInfo()
     {
-        builder.addPrimaryKeyLivenessInfo(LivenessInfo.create(metadata, timestamp, ttl, nowInSec));
+        builder.addPrimaryKeyLivenessInfo(LivenessInfo.create(timestamp, ttl, nowInSec));
     }
 
     public void addRowDeletion()
@@ -149,7 +160,7 @@
     public void addCell(ColumnDefinition column, CellPath path, ByteBuffer value) throws InvalidRequestException
     {
         Cell cell = ttl == LivenessInfo.NO_TTL
-                  ? BufferCell.live(metadata, column, timestamp, value, path)
+                  ? BufferCell.live(column, timestamp, value, path)
                   : BufferCell.expiring(column, timestamp, ttl, nowInSec, value, path);
         builder.addCell(cell);
     }
@@ -175,7 +186,7 @@
         //
         // We set counterid to a special value to differentiate between regular pre-2.0 local shards from pre-2.1 era
         // and "counter update" temporary state cells. Please see CounterContext.createUpdate() for further details.
-        builder.addCell(BufferCell.live(metadata, column, timestamp, CounterContext.instance().createUpdate(increment), path));
+        builder.addCell(BufferCell.live(column, timestamp, CounterContext.instance().createUpdate(increment), path));
     }
 
     public void setComplexDeletionTime(ColumnDefinition column)
diff --git a/src/java/org/apache/cassandra/cql3/UserTypes.java b/src/java/org/apache/cassandra/cql3/UserTypes.java
index 68a0513..4edd27f 100644
--- a/src/java/org/apache/cassandra/cql3/UserTypes.java
+++ b/src/java/org/apache/cassandra/cql3/UserTypes.java
@@ -20,12 +20,17 @@
 import java.nio.ByteBuffer;
 import java.util.*;
 
+import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.functions.Function;
-import org.apache.cassandra.db.marshal.UTF8Type;
-import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.db.rows.CellPath;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
+import static org.apache.cassandra.cql3.Constants.UNSET_VALUE;
+
 /**
  * Static helper methods and classes for user types.
  */
@@ -38,15 +43,15 @@
         UserType ut = (UserType)column.type;
         return new ColumnSpecification(column.ksName,
                                        column.cfName,
-                                       new ColumnIdentifier(column.name + "." + UTF8Type.instance.compose(ut.fieldName(field)), true),
+                                       new ColumnIdentifier(column.name + "." + ut.fieldName(field), true),
                                        ut.fieldType(field));
     }
 
     public static class Literal extends Term.Raw
     {
-        public final Map<ColumnIdentifier, Term.Raw> entries;
+        public final Map<FieldIdentifier, Term.Raw> entries;
 
-        public Literal(Map<ColumnIdentifier, Term.Raw> entries)
+        public Literal(Map<FieldIdentifier, Term.Raw> entries)
         {
             this.entries = entries;
         }
@@ -61,7 +66,7 @@
             int foundValues = 0;
             for (int i = 0; i < ut.size(); i++)
             {
-                ColumnIdentifier field = new ColumnIdentifier(ut.fieldName(i), UTF8Type.instance);
+                FieldIdentifier field = ut.fieldName(i);
                 Term.Raw raw = entries.get(field);
                 if (raw == null)
                     raw = Constants.NULL_LITERAL;
@@ -77,9 +82,11 @@
             if (foundValues != entries.size())
             {
                 // We had some field that are not part of the type
-                for (ColumnIdentifier id : entries.keySet())
-                    if (!ut.fieldNames().contains(id.bytes))
+                for (FieldIdentifier id : entries.keySet())
+                {
+                    if (!ut.fieldNames().contains(id))
                         throw new InvalidRequestException(String.format("Unknown field '%s' in value of user defined type %s", id, ut.getNameAsString()));
+                }
             }
 
             DelayedValue value = new DelayedValue(((UserType)receiver.type), values);
@@ -88,20 +95,23 @@
 
         private void validateAssignableTo(String keyspace, ColumnSpecification receiver) throws InvalidRequestException
         {
-            if (!(receiver.type instanceof UserType))
+            if (!receiver.type.isUDT())
                 throw new InvalidRequestException(String.format("Invalid user type literal for %s of type %s", receiver.name, receiver.type.asCQL3Type()));
 
             UserType ut = (UserType)receiver.type;
             for (int i = 0; i < ut.size(); i++)
             {
-                ColumnIdentifier field = new ColumnIdentifier(ut.fieldName(i), UTF8Type.instance);
+                FieldIdentifier field = ut.fieldName(i);
                 Term.Raw value = entries.get(field);
                 if (value == null)
                     continue;
 
                 ColumnSpecification fieldSpec = fieldSpecOf(receiver, i);
                 if (!value.testAssignment(keyspace, fieldSpec).isAssignable())
-                    throw new InvalidRequestException(String.format("Invalid user type literal for %s: field %s is not of type %s", receiver.name, field, fieldSpec.type.asCQL3Type()));
+                {
+                    throw new InvalidRequestException(String.format("Invalid user type literal for %s: field %s is not of type %s",
+                            receiver.name, field, fieldSpec.type.asCQL3Type()));
+                }
             }
         }
 
@@ -118,14 +128,19 @@
             }
         }
 
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            return null;
+        }
+
         public String getText()
         {
             StringBuilder sb = new StringBuilder();
             sb.append("{");
-            Iterator<Map.Entry<ColumnIdentifier, Term.Raw>> iter = entries.entrySet().iterator();
+            Iterator<Map.Entry<FieldIdentifier, Term.Raw>> iter = entries.entrySet().iterator();
             while (iter.hasNext())
             {
-                Map.Entry<ColumnIdentifier, Term.Raw> entry = iter.next();
+                Map.Entry<FieldIdentifier, Term.Raw> entry = iter.next();
                 sb.append(entry.getKey()).append(": ").append(entry.getValue().getText());
                 if (iter.hasNext())
                     sb.append(", ");
@@ -135,7 +150,46 @@
         }
     }
 
-    // Same purpose than Lists.DelayedValue, except we do handle bind marker in that case
+    public static class Value extends Term.MultiItemTerminal
+    {
+        private final UserType type;
+        public final ByteBuffer[] elements;
+
+        public Value(UserType type, ByteBuffer[] elements)
+        {
+            this.type = type;
+            this.elements = elements;
+        }
+
+        public static Value fromSerialized(ByteBuffer bytes, UserType type)
+        {
+            type.validate(bytes);
+            return new Value(type, type.split(bytes));
+        }
+
+        public ByteBuffer get(ProtocolVersion protocolVersion)
+        {
+            return TupleType.buildValue(elements);
+        }
+
+        public boolean equals(UserType userType, Value v)
+        {
+            if (elements.length != v.elements.length)
+                return false;
+
+            for (int i = 0; i < elements.length; i++)
+                if (userType.fieldType(i).compare(elements[i], v.elements[i]) != 0)
+                    return false;
+
+            return true;
+        }
+
+        public List<ByteBuffer> getElements()
+        {
+            return Arrays.asList(elements);
+        }
+    }
+
     public static class DelayedValue extends Term.NonTerminal
     {
         private final UserType type;
@@ -168,20 +222,27 @@
 
         private ByteBuffer[] bindInternal(QueryOptions options) throws InvalidRequestException
         {
+            if (values.size() > type.size())
+            {
+                throw new InvalidRequestException(String.format(
+                        "UDT value contained too many fields (expected %s, got %s)", type.size(), values.size()));
+            }
+
             ByteBuffer[] buffers = new ByteBuffer[values.size()];
             for (int i = 0; i < type.size(); i++)
             {
                 buffers[i] = values.get(i).bindAndGet(options);
-                // Since A UDT value is always written in its entirety Cassandra can't preserve a pre-existing value by 'not setting' the new value. Reject the query.
-                if (buffers[i] == ByteBufferUtil.UNSET_BYTE_BUFFER)
+                // Since a frozen UDT value is always written in its entirety Cassandra can't preserve a pre-existing
+                // value by 'not setting' the new value. Reject the query.
+                if (!type.isMultiCell() && buffers[i] == ByteBufferUtil.UNSET_BYTE_BUFFER)
                     throw new InvalidRequestException(String.format("Invalid unset value for field '%s' of user defined type %s", type.fieldNameAsString(i), type.getNameAsString()));
             }
             return buffers;
         }
 
-        public Constants.Value bind(QueryOptions options) throws InvalidRequestException
+        public Value bind(QueryOptions options) throws InvalidRequestException
         {
-            return new Constants.Value(bindAndGet(options));
+            return new Value(type, bindInternal(options));
         }
 
         @Override
@@ -190,4 +251,114 @@
             return UserType.buildValue(bindInternal(options));
         }
     }
+
+    public static class Marker extends AbstractMarker
+    {
+        protected Marker(int bindIndex, ColumnSpecification receiver)
+        {
+            super(bindIndex, receiver);
+            assert receiver.type.isUDT();
+        }
+
+        public Terminal bind(QueryOptions options) throws InvalidRequestException
+        {
+            ByteBuffer value = options.getValues().get(bindIndex);
+            if (value == null)
+                return null;
+            if (value == ByteBufferUtil.UNSET_BYTE_BUFFER)
+                return UNSET_VALUE;
+            return Value.fromSerialized(value, (UserType) receiver.type);
+        }
+    }
+
+    public static class Setter extends Operation
+    {
+        public Setter(ColumnDefinition column, Term t)
+        {
+            super(column, t);
+        }
+
+        public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException
+        {
+            Term.Terminal value = t.bind(params.options);
+            if (value == UNSET_VALUE)
+                return;
+
+            Value userTypeValue = (Value) value;
+            if (column.type.isMultiCell())
+            {
+                // setting a whole UDT at once means we overwrite all cells, so delete existing cells
+                params.setComplexDeletionTimeForOverwrite(column);
+                if (value == null)
+                    return;
+
+                Iterator<FieldIdentifier> fieldNameIter = userTypeValue.type.fieldNames().iterator();
+                for (ByteBuffer buffer : userTypeValue.elements)
+                {
+                    assert fieldNameIter.hasNext();
+                    FieldIdentifier fieldName = fieldNameIter.next();
+                    if (buffer == null)
+                        continue;
+
+                    CellPath fieldPath = userTypeValue.type.cellPathForField(fieldName);
+                    params.addCell(column, fieldPath, buffer);
+                }
+            }
+            else
+            {
+                // for frozen UDTs, we're overwriting the whole cell value
+                if (value == null)
+                    params.addTombstone(column);
+                else
+                    params.addCell(column, value.get(params.options.getProtocolVersion()));
+            }
+        }
+    }
+
+    public static class SetterByField extends Operation
+    {
+        private final FieldIdentifier field;
+
+        public SetterByField(ColumnDefinition column, FieldIdentifier field, Term t)
+        {
+            super(column, t);
+            this.field = field;
+        }
+
+        public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException
+        {
+            // we should not get here for frozen UDTs
+            assert column.type.isMultiCell() : "Attempted to set an individual field on a frozen UDT";
+
+            Term.Terminal value = t.bind(params.options);
+            if (value == UNSET_VALUE)
+                return;
+
+            CellPath fieldPath = ((UserType) column.type).cellPathForField(field);
+            if (value == null)
+                params.addTombstone(column, fieldPath);
+            else
+                params.addCell(column, fieldPath, value.get(params.options.getProtocolVersion()));
+        }
+    }
+
+    public static class DeleterByField extends Operation
+    {
+        private final FieldIdentifier field;
+
+        public DeleterByField(ColumnDefinition column, FieldIdentifier field)
+        {
+            super(column, null);
+            this.field = field;
+        }
+
+        public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException
+        {
+            // we should not get here for frozen UDTs
+            assert column.type.isMultiCell() : "Attempted to delete a single field from a frozen UDT";
+
+            CellPath fieldPath = ((UserType) column.type).cellPathForField(field);
+            params.addTombstone(column, fieldPath);
+        }
+    }
 }
diff --git a/src/java/org/apache/cassandra/cql3/VariableSpecifications.java b/src/java/org/apache/cassandra/cql3/VariableSpecifications.java
index 5304350..24f71e4 100644
--- a/src/java/org/apache/cassandra/cql3/VariableSpecifications.java
+++ b/src/java/org/apache/cassandra/cql3/VariableSpecifications.java
@@ -63,9 +63,10 @@
      *
      * Callers of this method should ensure that all statements operate on the same table.
      */
-    public Short[] getPartitionKeyBindIndexes(CFMetaData cfm)
+    public short[] getPartitionKeyBindIndexes(CFMetaData cfm)
     {
-        Short[] partitionKeyPositions = new Short[cfm.partitionKeyColumns().size()];
+        short[] partitionKeyPositions = new short[cfm.partitionKeyColumns().size()];
+        boolean[] set = new boolean[partitionKeyPositions.length];
         for (int i = 0; i < targetColumns.length; i++)
         {
             ColumnDefinition targetColumn = targetColumns[i];
@@ -73,14 +74,13 @@
             {
                 assert targetColumn.ksName.equals(cfm.ksName) && targetColumn.cfName.equals(cfm.cfName);
                 partitionKeyPositions[targetColumn.position()] = (short) i;
+                set[targetColumn.position()] = true;
             }
         }
 
-        for (Short bindIndex : partitionKeyPositions)
-        {
-            if (bindIndex == null)
+        for (boolean b : set)
+            if (!b)
                 return null;
-        }
 
         return partitionKeyPositions;
     }
diff --git a/src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java b/src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java
index 0cf11a5..aa7555f 100644
--- a/src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java
+++ b/src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java
@@ -24,6 +24,7 @@
 import org.apache.cassandra.cql3.AssignmentTestable;
 import org.apache.cassandra.cql3.ColumnSpecification;
 import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.commons.lang3.text.StrBuilder;
 
 /**
  * Base class for our native/hardcoded functions.
@@ -89,7 +90,7 @@
         // We should ignore the fact that the receiver type is frozen in our comparison as functions do not support
         // frozen types for return type
         AbstractType<?> returnType = returnType();
-        if (receiver.type.isFrozenCollection())
+        if (receiver.type.isFreezable() && !receiver.type.isMultiCell())
             returnType = returnType.freeze();
 
         if (receiver.type.equals(returnType))
@@ -115,4 +116,13 @@
         sb.append(") -> ").append(returnType.asCQL3Type());
         return sb.toString();
     }
+
+    @Override
+    public String columnName(List<String> columnNames)
+    {
+        return new StrBuilder(name().toString()).append('(')
+                                                .appendWithSeparators(columnNames, ", ")
+                                                .append(')')
+                                                .toString();
+    }
 }
diff --git a/src/java/org/apache/cassandra/cql3/functions/AggregateFcts.java b/src/java/org/apache/cassandra/cql3/functions/AggregateFcts.java
index 441fa58..85d3763 100644
--- a/src/java/org/apache/cassandra/cql3/functions/AggregateFcts.java
+++ b/src/java/org/apache/cassandra/cql3/functions/AggregateFcts.java
@@ -28,6 +28,7 @@
 import org.apache.cassandra.cql3.CQL3Type;
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Factory methods for aggregate functions.
@@ -85,17 +86,6 @@
     }
 
     /**
-     * Checks if the specified function is the count rows (e.g. COUNT(*) or COUNT(1)) function.
-     *
-     * @param function the function to check
-     * @return <code>true</code> if the specified function is the count rows one, <code>false</code> otherwise.
-     */
-    public static boolean isCountRows(Function function)
-    {
-        return function == countRowsFunction;
-    }
-
-    /**
      * The function used to count the number of rows of a result set. This function is called when COUNT(*) or COUNT(1)
      * is specified.
      */
@@ -113,17 +103,23 @@
                             count = 0;
                         }
 
-                        public ByteBuffer compute(int protocolVersion)
+                        public ByteBuffer compute(ProtocolVersion protocolVersion)
                         {
                             return LongType.instance.decompose(count);
                         }
 
-                        public void addInput(int protocolVersion, List<ByteBuffer> values)
+                        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                         {
                             count++;
                         }
                     };
                 }
+
+                @Override
+                public String columnName(List<String> columnNames)
+                {
+                    return "count";
+                }
             };
 
     /**
@@ -144,12 +140,12 @@
                             sum = BigDecimal.ZERO;
                         }
 
-                        public ByteBuffer compute(int protocolVersion)
+                        public ByteBuffer compute(ProtocolVersion protocolVersion)
                         {
                             return ((DecimalType) returnType()).decompose(sum);
                         }
 
-                        public void addInput(int protocolVersion, List<ByteBuffer> values)
+                        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                         {
                             ByteBuffer value = values.get(0);
 
@@ -183,12 +179,12 @@
                             avg = BigDecimal.ZERO;
                         }
 
-                        public ByteBuffer compute(int protocolVersion)
+                        public ByteBuffer compute(ProtocolVersion protocolVersion)
                         {
                             return DecimalType.instance.decompose(avg);
                         }
 
-                        public void addInput(int protocolVersion, List<ByteBuffer> values)
+                        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                         {
                             ByteBuffer value = values.get(0);
 
@@ -223,12 +219,12 @@
                             sum = BigInteger.ZERO;
                         }
 
-                        public ByteBuffer compute(int protocolVersion)
+                        public ByteBuffer compute(ProtocolVersion protocolVersion)
                         {
                             return ((IntegerType) returnType()).decompose(sum);
                         }
 
-                        public void addInput(int protocolVersion, List<ByteBuffer> values)
+                        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                         {
                             ByteBuffer value = values.get(0);
 
@@ -262,7 +258,7 @@
                             sum = BigInteger.ZERO;
                         }
 
-                        public ByteBuffer compute(int protocolVersion)
+                        public ByteBuffer compute(ProtocolVersion protocolVersion)
                         {
                             if (count == 0)
                                 return IntegerType.instance.decompose(BigInteger.ZERO);
@@ -270,7 +266,7 @@
                             return IntegerType.instance.decompose(sum.divide(BigInteger.valueOf(count)));
                         }
 
-                        public void addInput(int protocolVersion, List<ByteBuffer> values)
+                        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                         {
                             ByteBuffer value = values.get(0);
 
@@ -302,12 +298,12 @@
                             sum = 0;
                         }
 
-                        public ByteBuffer compute(int protocolVersion)
+                        public ByteBuffer compute(ProtocolVersion protocolVersion)
                         {
                             return ((ByteType) returnType()).decompose(sum);
                         }
 
-                        public void addInput(int protocolVersion, List<ByteBuffer> values)
+                        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                         {
                             ByteBuffer value = values.get(0);
 
@@ -331,7 +327,7 @@
                 {
                     return new AvgAggregate(ByteType.instance)
                     {
-                        public ByteBuffer compute(int protocolVersion) throws InvalidRequestException
+                        public ByteBuffer compute(ProtocolVersion protocolVersion) throws InvalidRequestException
                         {
                             return ByteType.instance.decompose((byte) computeInternal());
                         }
@@ -356,12 +352,12 @@
                             sum = 0;
                         }
 
-                        public ByteBuffer compute(int protocolVersion)
+                        public ByteBuffer compute(ProtocolVersion protocolVersion)
                         {
                             return ((ShortType) returnType()).decompose(sum);
                         }
 
-                        public void addInput(int protocolVersion, List<ByteBuffer> values)
+                        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                         {
                             ByteBuffer value = values.get(0);
 
@@ -385,7 +381,7 @@
                 {
                     return new AvgAggregate(ShortType.instance)
                     {
-                        public ByteBuffer compute(int protocolVersion)
+                        public ByteBuffer compute(ProtocolVersion protocolVersion)
                         {
                             return ShortType.instance.decompose((short) computeInternal());
                         }
@@ -410,12 +406,12 @@
                             sum = 0;
                         }
 
-                        public ByteBuffer compute(int protocolVersion)
+                        public ByteBuffer compute(ProtocolVersion protocolVersion)
                         {
                             return ((Int32Type) returnType()).decompose(sum);
                         }
 
-                        public void addInput(int protocolVersion, List<ByteBuffer> values)
+                        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                         {
                             ByteBuffer value = values.get(0);
 
@@ -439,7 +435,7 @@
                 {
                     return new AvgAggregate(Int32Type.instance)
                     {
-                        public ByteBuffer compute(int protocolVersion)
+                        public ByteBuffer compute(ProtocolVersion protocolVersion)
                         {
                             return Int32Type.instance.decompose((int) computeInternal());
                         }
@@ -469,7 +465,7 @@
                 {
                     return new AvgAggregate(LongType.instance)
                     {
-                        public ByteBuffer compute(int protocolVersion)
+                        public ByteBuffer compute(ProtocolVersion protocolVersion)
                         {
                             return LongType.instance.decompose(computeInternal());
                         }
@@ -485,29 +481,11 @@
             {
                 public Aggregate newAggregate()
                 {
-                    return new Aggregate()
+                    return new FloatSumAggregate(FloatType.instance)
                     {
-                        private float sum;
-
-                        public void reset()
+                        public ByteBuffer compute(ProtocolVersion protocolVersion) throws InvalidRequestException
                         {
-                            sum = 0;
-                        }
-
-                        public ByteBuffer compute(int protocolVersion)
-                        {
-                            return ((FloatType) returnType()).decompose(sum);
-                        }
-
-                        public void addInput(int protocolVersion, List<ByteBuffer> values)
-                        {
-                            ByteBuffer value = values.get(0);
-
-                            if (value == null)
-                                return;
-
-                            Number number = ((Number) argTypes().get(0).compose(value));
-                            sum += number.floatValue();
+                            return FloatType.instance.decompose((float) computeInternal());
                         }
                     };
                 }
@@ -523,7 +501,7 @@
                 {
                     return new FloatAvgAggregate(FloatType.instance)
                     {
-                        public ByteBuffer compute(int protocolVersion) throws InvalidRequestException
+                        public ByteBuffer compute(ProtocolVersion protocolVersion) throws InvalidRequestException
                         {
                             return FloatType.instance.decompose((float) computeInternal());
                         }
@@ -539,33 +517,68 @@
             {
                 public Aggregate newAggregate()
                 {
-                    return new Aggregate()
+                    return new FloatSumAggregate(DoubleType.instance)
                     {
-                        private double sum;
-
-                        public void reset()
+                        public ByteBuffer compute(ProtocolVersion protocolVersion) throws InvalidRequestException
                         {
-                            sum = 0;
-                        }
-
-                        public ByteBuffer compute(int protocolVersion)
-                        {
-                            return ((DoubleType) returnType()).decompose(sum);
-                        }
-
-                        public void addInput(int protocolVersion, List<ByteBuffer> values)
-                        {
-                            ByteBuffer value = values.get(0);
-
-                            if (value == null)
-                                return;
-
-                            Number number = ((Number) argTypes().get(0).compose(value));
-                            sum += number.doubleValue();
+                            return DoubleType.instance.decompose(computeInternal());
                         }
                     };
                 }
             };
+
+    /**
+     * Sum aggregate function for floating point numbers, using double arithmetics and
+     * Kahan's algorithm to improve result precision.
+     */
+    private static abstract class FloatSumAggregate implements AggregateFunction.Aggregate
+    {
+        private double sum;
+        private double compensation;
+        private double simpleSum;
+
+        private final AbstractType numberType;
+
+        public FloatSumAggregate(AbstractType numberType)
+        {
+            this.numberType = numberType;
+        }
+
+        public void reset()
+        {
+            sum = 0;
+            compensation = 0;
+            simpleSum = 0;
+        }
+
+        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
+        {
+            ByteBuffer value = values.get(0);
+
+            if (value == null)
+                return;
+
+            double number = ((Number) numberType.compose(value)).doubleValue();
+            simpleSum += number;
+            double tmp = number - compensation;
+            double rounded = sum + tmp;
+            compensation = (rounded - sum) - tmp;
+            sum = rounded;
+        }
+
+        public double computeInternal()
+        {
+            // correctly compute final sum if it's NaN from consequently
+            // adding same-signed infinite values.
+            double tmp = sum + compensation;
+
+            if (Double.isNaN(tmp) && Double.isInfinite(simpleSum))
+                return simpleSum;
+            else
+                return tmp;
+        }
+    }
+
     /**
      * Average aggregate for floating point umbers, using double arithmetics and Kahan's algorithm
      * to calculate sum by default, switching to BigDecimal on sum overflow. Resulting number is
@@ -623,7 +636,7 @@
             }
         }
 
-        public void addInput(int protocolVersion, List<ByteBuffer> values)
+        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
         {
             ByteBuffer value = values.get(0);
 
@@ -666,7 +679,7 @@
                 {
                     return new FloatAvgAggregate(DoubleType.instance)
                     {
-                        public ByteBuffer compute(int protocolVersion) throws InvalidRequestException
+                        public ByteBuffer compute(ProtocolVersion protocolVersion) throws InvalidRequestException
                         {
                             return DoubleType.instance.decompose(computeInternal());
                         }
@@ -696,7 +709,7 @@
         {
             return new AvgAggregate(LongType.instance)
             {
-                public ByteBuffer compute(int protocolVersion) throws InvalidRequestException
+                public ByteBuffer compute(ProtocolVersion protocolVersion) throws InvalidRequestException
                 {
                     return CounterColumnType.instance.decompose(computeInternal());
                 }
@@ -721,12 +734,12 @@
                     min = null;
                 }
 
-                public ByteBuffer compute(int protocolVersion)
+                public ByteBuffer compute(ProtocolVersion protocolVersion)
                 {
                     return min != null ? LongType.instance.decompose(min) : null;
                 }
 
-                public void addInput(int protocolVersion, List<ByteBuffer> values)
+                public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                 {
                     ByteBuffer value = values.get(0);
 
@@ -759,12 +772,12 @@
                     max = null;
                 }
 
-                public ByteBuffer compute(int protocolVersion)
+                public ByteBuffer compute(ProtocolVersion protocolVersion)
                 {
                     return max != null ? LongType.instance.decompose(max) : null;
                 }
 
-                public void addInput(int protocolVersion, List<ByteBuffer> values)
+                public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                 {
                     ByteBuffer value = values.get(0);
 
@@ -801,12 +814,12 @@
                         max = null;
                     }
 
-                    public ByteBuffer compute(int protocolVersion)
+                    public ByteBuffer compute(ProtocolVersion protocolVersion)
                     {
                         return max;
                     }
 
-                    public void addInput(int protocolVersion, List<ByteBuffer> values)
+                    public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                     {
                         ByteBuffer value = values.get(0);
 
@@ -842,12 +855,12 @@
                         min = null;
                     }
 
-                    public ByteBuffer compute(int protocolVersion)
+                    public ByteBuffer compute(ProtocolVersion protocolVersion)
                     {
                         return min;
                     }
 
-                    public void addInput(int protocolVersion, List<ByteBuffer> values)
+                    public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                     {
                         ByteBuffer value = values.get(0);
 
@@ -883,12 +896,12 @@
                         count = 0;
                     }
 
-                    public ByteBuffer compute(int protocolVersion)
+                    public ByteBuffer compute(ProtocolVersion protocolVersion)
                     {
                         return ((LongType) returnType()).decompose(count);
                     }
 
-                    public void addInput(int protocolVersion, List<ByteBuffer> values)
+                    public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
                     {
                         ByteBuffer value = values.get(0);
 
@@ -911,12 +924,12 @@
             sum = 0;
         }
 
-        public ByteBuffer compute(int protocolVersion)
+        public ByteBuffer compute(ProtocolVersion protocolVersion)
         {
             return LongType.instance.decompose(sum);
         }
 
-        public void addInput(int protocolVersion, List<ByteBuffer> values)
+        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
         {
             ByteBuffer value = values.get(0);
 
@@ -967,7 +980,7 @@
             }
         }
 
-        public void addInput(int protocolVersion, List<ByteBuffer> values)
+        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values)
         {
             ByteBuffer value = values.get(0);
 
diff --git a/src/java/org/apache/cassandra/cql3/functions/AggregateFunction.java b/src/java/org/apache/cassandra/cql3/functions/AggregateFunction.java
index ddbc9d1..b207563 100644
--- a/src/java/org/apache/cassandra/cql3/functions/AggregateFunction.java
+++ b/src/java/org/apache/cassandra/cql3/functions/AggregateFunction.java
@@ -21,6 +21,7 @@
 import java.util.List;
 
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Performs a calculation on a set of values and return a single value.
@@ -41,11 +42,10 @@
     {
         /**
          * Adds the specified input to this aggregate.
-         *
-         * @param protocolVersion native protocol version
+         *  @param protocolVersion native protocol version
          * @param values the values to add to the aggregate.
          */
-        public void addInput(int protocolVersion, List<ByteBuffer> values) throws InvalidRequestException;
+        public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values) throws InvalidRequestException;
 
         /**
          * Computes and returns the aggregate current value.
@@ -53,7 +53,7 @@
          * @param protocolVersion native protocol version
          * @return the aggregate current value.
          */
-        public ByteBuffer compute(int protocolVersion) throws InvalidRequestException;
+        public ByteBuffer compute(ProtocolVersion protocolVersion) throws InvalidRequestException;
 
         /**
          * Reset this aggregate.
diff --git a/src/java/org/apache/cassandra/cql3/functions/BytesConversionFcts.java b/src/java/org/apache/cassandra/cql3/functions/BytesConversionFcts.java
index d9c6a52..33771b7 100644
--- a/src/java/org/apache/cassandra/cql3/functions/BytesConversionFcts.java
+++ b/src/java/org/apache/cassandra/cql3/functions/BytesConversionFcts.java
@@ -26,6 +26,7 @@
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.BytesType;
 import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.serializers.MarshalException;
@@ -60,7 +61,7 @@
         String name = fromType.asCQL3Type() + "asblob";
         return new NativeScalarFunction(name, BytesType.instance, fromType)
         {
-            public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+            public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
             {
                 return parameters.get(0);
             }
@@ -72,7 +73,7 @@
         final String name = "blobas" + toType.asCQL3Type();
         return new NativeScalarFunction(name, toType, BytesType.instance)
         {
-            public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters) throws InvalidRequestException
+            public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters) throws InvalidRequestException
             {
                 ByteBuffer val = parameters.get(0);
                 try
@@ -92,7 +93,7 @@
 
     public static final Function VarcharAsBlobFct = new NativeScalarFunction("varcharasblob", BytesType.instance, UTF8Type.instance)
     {
-        public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
         {
             return parameters.get(0);
         }
@@ -100,7 +101,7 @@
 
     public static final Function BlobAsVarcharFct = new NativeScalarFunction("blobasvarchar", UTF8Type.instance, BytesType.instance)
     {
-        public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
         {
             return parameters.get(0);
         }
diff --git a/src/java/org/apache/cassandra/cql3/functions/CastFcts.java b/src/java/org/apache/cassandra/cql3/functions/CastFcts.java
new file mode 100644
index 0000000..9f825ee
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/functions/CastFcts.java
@@ -0,0 +1,361 @@
+/*
+ * 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.functions;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.cassandra.cql3.CQL3Type;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.AsciiType;
+import org.apache.cassandra.db.marshal.BooleanType;
+import org.apache.cassandra.db.marshal.ByteType;
+import org.apache.cassandra.db.marshal.CounterColumnType;
+import org.apache.cassandra.db.marshal.DecimalType;
+import org.apache.cassandra.db.marshal.DoubleType;
+import org.apache.cassandra.db.marshal.FloatType;
+import org.apache.cassandra.db.marshal.InetAddressType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.IntegerType;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.marshal.ShortType;
+import org.apache.cassandra.db.marshal.SimpleDateType;
+import org.apache.cassandra.db.marshal.TimeType;
+import org.apache.cassandra.db.marshal.TimeUUIDType;
+import org.apache.cassandra.db.marshal.TimestampType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.marshal.UUIDType;
+import org.apache.cassandra.transport.ProtocolVersion;
+
+import org.apache.commons.lang3.text.WordUtils;
+
+/**
+ * Casting functions
+ *
+ */
+public final class CastFcts
+{
+    private static final String FUNCTION_NAME_PREFIX = "castAs";
+
+    public static Collection<Function> all()
+    {
+        List<Function> functions = new ArrayList<>();
+
+        @SuppressWarnings("unchecked")
+        final AbstractType<? extends Number>[] numericTypes = new AbstractType[] {ByteType.instance,
+                                                                                  ShortType.instance,
+                                                                                  Int32Type.instance,
+                                                                                  LongType.instance,
+                                                                                  FloatType.instance,
+                                                                                  DoubleType.instance,
+                                                                                  DecimalType.instance,
+                                                                                  CounterColumnType.instance,
+                                                                                  IntegerType.instance};
+
+        for (AbstractType<? extends Number> inputType : numericTypes)
+        {
+            addFunctionIfNeeded(functions, inputType, ByteType.instance, Number::byteValue);
+            addFunctionIfNeeded(functions, inputType, ShortType.instance, Number::shortValue);
+            addFunctionIfNeeded(functions, inputType, Int32Type.instance, Number::intValue);
+            addFunctionIfNeeded(functions, inputType, LongType.instance, Number::longValue);
+            addFunctionIfNeeded(functions, inputType, FloatType.instance, Number::floatValue);
+            addFunctionIfNeeded(functions, inputType, DoubleType.instance, Number::doubleValue);
+            addFunctionIfNeeded(functions, inputType, DecimalType.instance, getDecimalConversionFunction(inputType));
+            addFunctionIfNeeded(functions, inputType, IntegerType.instance, p -> BigInteger.valueOf(p.longValue()));
+            functions.add(CastAsTextFunction.create(inputType, AsciiType.instance));
+            functions.add(CastAsTextFunction.create(inputType, UTF8Type.instance));
+        }
+
+        functions.add(JavaFunctionWrapper.create(AsciiType.instance, UTF8Type.instance, p -> p));
+
+        functions.add(CastAsTextFunction.create(InetAddressType.instance, AsciiType.instance));
+        functions.add(CastAsTextFunction.create(InetAddressType.instance, UTF8Type.instance));
+
+        functions.add(CastAsTextFunction.create(BooleanType.instance, AsciiType.instance));
+        functions.add(CastAsTextFunction.create(BooleanType.instance, UTF8Type.instance));
+
+        functions.add(CassandraFunctionWrapper.create(TimeUUIDType.instance, SimpleDateType.instance, TimeFcts.timeUuidtoDate));
+        functions.add(CassandraFunctionWrapper.create(TimeUUIDType.instance, TimestampType.instance, TimeFcts.timeUuidToTimestamp));
+        functions.add(CastAsTextFunction.create(TimeUUIDType.instance, AsciiType.instance));
+        functions.add(CastAsTextFunction.create(TimeUUIDType.instance, UTF8Type.instance));
+        functions.add(CassandraFunctionWrapper.create(TimestampType.instance, SimpleDateType.instance, TimeFcts.timestampToDate));
+        functions.add(CastAsTextFunction.create(TimestampType.instance, AsciiType.instance));
+        functions.add(CastAsTextFunction.create(TimestampType.instance, UTF8Type.instance));
+        functions.add(CassandraFunctionWrapper.create(SimpleDateType.instance, TimestampType.instance, TimeFcts.dateToTimestamp));
+        functions.add(CastAsTextFunction.create(SimpleDateType.instance, AsciiType.instance));
+        functions.add(CastAsTextFunction.create(SimpleDateType.instance, UTF8Type.instance));
+        functions.add(CastAsTextFunction.create(TimeType.instance, AsciiType.instance));
+        functions.add(CastAsTextFunction.create(TimeType.instance, UTF8Type.instance));
+
+        functions.add(CastAsTextFunction.create(UUIDType.instance, AsciiType.instance));
+        functions.add(CastAsTextFunction.create(UUIDType.instance, UTF8Type.instance));
+
+        return functions;
+    }
+
+    /**
+     * Returns the conversion function to convert the specified type into a Decimal type
+     *
+     * @param inputType the input type
+     * @return the conversion function to convert the specified type into a Decimal type
+     */
+    private static <I extends Number> java.util.function.Function<I, BigDecimal> getDecimalConversionFunction(AbstractType<? extends Number> inputType)
+    {
+        if (inputType == FloatType.instance || inputType == DoubleType.instance)
+            return p -> BigDecimal.valueOf(p.doubleValue());
+
+        if (inputType == IntegerType.instance)
+            return p -> new BigDecimal((BigInteger) p);
+
+        return p -> BigDecimal.valueOf(p.longValue());
+    }
+
+    /**
+     * Creates the name of the cast function use to cast to the specified type.
+     *
+     * @param outputType the output type
+     * @return the name of the cast function use to cast to the specified type
+     */
+    public static String getFunctionName(AbstractType<?> outputType)
+    {
+        return getFunctionName(outputType.asCQL3Type());
+    }
+
+    /**
+     * Creates the name of the cast function use to cast to the specified type.
+     *
+     * @param outputType the output type
+     * @return the name of the cast function use to cast to the specified type
+     */
+    public static String getFunctionName(CQL3Type outputType)
+    {
+        return FUNCTION_NAME_PREFIX + WordUtils.capitalize(toLowerCaseString(outputType));
+    }
+
+    /**
+     * Adds to the list a function converting the input type in to the output type if they are not the same.
+     *
+     * @param functions the list to add to
+     * @param inputType the input type
+     * @param outputType the output type
+     * @param converter the function use to convert the input type into the output type
+     */
+    private static <I, O> void addFunctionIfNeeded(List<Function> functions,
+                                                   AbstractType<I> inputType,
+                                                   AbstractType<O> outputType,
+                                                   java.util.function.Function<I, O> converter)
+    {
+        if (!inputType.equals(outputType))
+            functions.add(wrapJavaFunction(inputType, outputType, converter));
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <O, I> Function wrapJavaFunction(AbstractType<I> inputType,
+                                                    AbstractType<O> outputType,
+                                                    java.util.function.Function<I, O> converter)
+    {
+        return inputType.equals(CounterColumnType.instance)
+                ? JavaCounterFunctionWrapper.create(outputType, (java.util.function.Function<Long, O>) converter)
+                : JavaFunctionWrapper.create(inputType, outputType, converter);
+    }
+
+    private static String toLowerCaseString(CQL3Type type)
+    {
+        return type.toString().toLowerCase();
+    }
+
+    /**
+     * Base class for the CAST functions.
+     *
+     * @param <I> the input type
+     * @param <O> the output type
+     */
+    private static abstract class CastFunction<I, O> extends NativeScalarFunction
+    {
+        public CastFunction(AbstractType<I> inputType, AbstractType<O> outputType)
+        {
+            super(getFunctionName(outputType), outputType, inputType);
+        }
+
+        @Override
+        public String columnName(List<String> columnNames)
+        {
+            return String.format("cast(%s as %s)", columnNames.get(0), toLowerCaseString(outputType().asCQL3Type()));
+        }
+
+        @SuppressWarnings("unchecked")
+        protected AbstractType<O> outputType()
+        {
+            return (AbstractType<O>) returnType;
+        }
+
+        @SuppressWarnings("unchecked")
+        protected AbstractType<I> inputType()
+        {
+            return (AbstractType<I>) argTypes.get(0);
+        }
+    }
+
+    /**
+     * <code>CastFunction</code> that implements casting by wrapping a java <code>Function</code>.
+     *
+     * @param <I> the input parameter
+     * @param <O> the output parameter
+     */
+    private static class JavaFunctionWrapper<I, O> extends CastFunction<I, O>
+    {
+        /**
+         * The java function used to convert the input type into the output one.
+         */
+        private final java.util.function.Function<I, O> converter;
+
+        public static <I, O> JavaFunctionWrapper<I, O> create(AbstractType<I> inputType,
+                                                              AbstractType<O> outputType,
+                                                              java.util.function.Function<I, O> converter)
+        {
+            return new JavaFunctionWrapper<I, O>(inputType, outputType, converter);
+        }
+
+        protected JavaFunctionWrapper(AbstractType<I> inputType,
+                                      AbstractType<O> outputType,
+                                      java.util.function.Function<I, O> converter)
+        {
+            super(inputType, outputType);
+            this.converter = converter;
+        }
+
+        public final ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
+        {
+            ByteBuffer bb = parameters.get(0);
+            if (bb == null)
+                return null;
+
+            return outputType().decompose(converter.apply(compose(bb)));
+        }
+
+        protected I compose(ByteBuffer bb)
+        {
+            return inputType().compose(bb);
+        }
+    }
+
+    /**
+     * <code>JavaFunctionWrapper</code> for counter columns.
+     *
+     * <p>Counter columns need to be handled in a special way because their binary representation is converted into
+     * the one of a BIGINT before functions are applied.</p>
+     *
+     * @param <O> the output parameter
+     */
+    private static class JavaCounterFunctionWrapper<O> extends JavaFunctionWrapper<Long, O>
+    {
+        public static <O> JavaFunctionWrapper<Long, O> create(AbstractType<O> outputType,
+                                                              java.util.function.Function<Long, O> converter)
+        {
+            return new JavaCounterFunctionWrapper<O>(outputType, converter);
+        }
+
+        protected JavaCounterFunctionWrapper(AbstractType<O> outputType,
+                                            java.util.function.Function<Long, O> converter)
+        {
+            super(CounterColumnType.instance, outputType, converter);
+        }
+
+        protected Long compose(ByteBuffer bb)
+        {
+            return LongType.instance.compose(bb);
+        }
+    }
+
+    /**
+     * <code>CastFunction</code> that implements casting by wrapping an existing <code>NativeScalarFunction</code>.
+     *
+     * @param <I> the input parameter
+     * @param <O> the output parameter
+     */
+    private static final class CassandraFunctionWrapper<I, O> extends CastFunction<I, O>
+    {
+        /**
+         * The native scalar function used to perform the conversion.
+         */
+        private final NativeScalarFunction delegate;
+
+        public static <I, O> CassandraFunctionWrapper<I, O> create(AbstractType<I> inputType,
+                                                                   AbstractType<O> outputType,
+                                                                   NativeScalarFunction delegate)
+        {
+            return new CassandraFunctionWrapper<I, O>(inputType, outputType, delegate);
+        }
+
+        private CassandraFunctionWrapper(AbstractType<I> inputType,
+                                         AbstractType<O> outputType,
+                                         NativeScalarFunction delegate)
+        {
+            super(inputType, outputType);
+            assert delegate.argTypes().size() == 1 && inputType.equals(delegate.argTypes().get(0));
+            assert outputType.equals(delegate.returnType());
+            this.delegate = delegate;
+        }
+
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
+        {
+            return delegate.execute(protocolVersion, parameters);
+        }
+    }
+
+    /**
+     * <code>CastFunction</code> that can be used to cast a type into ascii or text types.
+     *
+     * @param <I> the input parameter
+     */
+    private static final class CastAsTextFunction<I> extends CastFunction<I, String>
+    {
+
+        public static <I> CastAsTextFunction<I> create(AbstractType<I> inputType,
+                                                       AbstractType<String> outputType)
+        {
+            return new CastAsTextFunction<I>(inputType, outputType);
+        }
+
+        private CastAsTextFunction(AbstractType<I> inputType,
+                                    AbstractType<String> outputType)
+        {
+            super(inputType, outputType);
+        }
+
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
+        {
+            ByteBuffer bb = parameters.get(0);
+            if (bb == null)
+                return null;
+
+            return outputType().decompose(inputType().getSerializer().toCQLLiteral(bb));
+        }
+    }
+
+    /**
+     * The class must not be instantiated as it contains only static variables.
+     */
+    private CastFcts()
+    {
+    }
+}
diff --git a/src/java/org/apache/cassandra/cql3/functions/FromJsonFct.java b/src/java/org/apache/cassandra/cql3/functions/FromJsonFct.java
index 2b9e8c6..8f07b38 100644
--- a/src/java/org/apache/cassandra/cql3/functions/FromJsonFct.java
+++ b/src/java/org/apache/cassandra/cql3/functions/FromJsonFct.java
@@ -28,6 +28,7 @@
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.exceptions.FunctionExecutionException;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 public class FromJsonFct extends NativeScalarFunction
 {
@@ -51,7 +52,7 @@
         super("fromjson", returnType, UTF8Type.instance);
     }
 
-    public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+    public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
     {
         assert parameters.size() == 1 : "Unexpectedly got " + parameters.size() + " arguments for fromJson()";
         ByteBuffer argument = parameters.get(0);
diff --git a/src/java/org/apache/cassandra/cql3/functions/Function.java b/src/java/org/apache/cassandra/cql3/functions/Function.java
index f93f14b..5d258af 100644
--- a/src/java/org/apache/cassandra/cql3/functions/Function.java
+++ b/src/java/org/apache/cassandra/cql3/functions/Function.java
@@ -47,4 +47,12 @@
     public void addFunctionsTo(List<Function> functions);
 
     public boolean hasReferenceTo(Function function);
+
+    /**
+     * Returns the name of the function to use within a ResultSet.
+     *
+     * @param columnNames the names of the columns used to call the function
+     * @return the name of the function to use within a ResultSet
+     */
+    public String columnName(List<String> columnNames);
 }
diff --git a/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java b/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java
index 4ccd4b2..c0d616e 100644
--- a/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java
+++ b/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java
@@ -27,6 +27,7 @@
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class FunctionCall extends Term.NonTerminal
@@ -69,7 +70,7 @@
         return executeInternal(options.getProtocolVersion(), fun, buffers);
     }
 
-    private static ByteBuffer executeInternal(int protocolVersion, ScalarFunction fun, List<ByteBuffer> params) throws InvalidRequestException
+    private static ByteBuffer executeInternal(ProtocolVersion protocolVersion, ScalarFunction fun, List<ByteBuffer> params) throws InvalidRequestException
     {
         ByteBuffer result = fun.execute(protocolVersion, params);
         try
@@ -96,20 +97,28 @@
         return false;
     }
 
-    private static Term.Terminal makeTerminal(Function fun, ByteBuffer result, int version) throws InvalidRequestException
+    private static Term.Terminal makeTerminal(Function fun, ByteBuffer result, ProtocolVersion version) throws InvalidRequestException
     {
         if (result == null)
             return null;
-        if (!(fun.returnType() instanceof CollectionType))
-            return new Constants.Value(result);
-
-        switch (((CollectionType)fun.returnType()).kind)
+        if (fun.returnType().isCollection())
         {
-            case LIST: return Lists.Value.fromSerialized(result, (ListType)fun.returnType(), version);
-            case SET:  return Sets.Value.fromSerialized(result, (SetType)fun.returnType(), version);
-            case MAP:  return Maps.Value.fromSerialized(result, (MapType)fun.returnType(), version);
+            switch (((CollectionType) fun.returnType()).kind)
+            {
+                case LIST:
+                    return Lists.Value.fromSerialized(result, (ListType) fun.returnType(), version);
+                case SET:
+                    return Sets.Value.fromSerialized(result, (SetType) fun.returnType(), version);
+                case MAP:
+                    return Maps.Value.fromSerialized(result, (MapType) fun.returnType(), version);
+            }
         }
-        throw new AssertionError();
+        else if (fun.returnType().isUDT())
+        {
+            return UserTypes.Value.fromSerialized(result, (UserType) fun.returnType());
+        }
+
+        return new Constants.Value(result);
     }
 
     public static class Raw extends Term.Raw
@@ -183,9 +192,19 @@
             }
         }
 
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            // We could implement this, but the method is only used in selection clause, where FunctionCall is not used 
+            // we use a Selectable.WithFunction instead). And if that method is later used in other places, better to
+            // let that future patch make sure this can be implemented properly (note in particular we don't have access
+            // to the receiver type, which FunctionResolver.get() takes) rather than provide an implementation that may
+            // not work in all cases.
+            throw new UnsupportedOperationException();
+        }
+
         public String getText()
         {
-            return name + terms.stream().map(Term.Raw::getText).collect(Collectors.joining(", ", "(", ")"));
+            return name.toCQLString() + terms.stream().map(Term.Raw::getText).collect(Collectors.joining(", ", "(", ")"));
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/cql3/functions/FunctionName.java b/src/java/org/apache/cassandra/cql3/functions/FunctionName.java
index 6fb3faa..68b3140 100644
--- a/src/java/org/apache/cassandra/cql3/functions/FunctionName.java
+++ b/src/java/org/apache/cassandra/cql3/functions/FunctionName.java
@@ -24,19 +24,23 @@
 
 import com.google.common.base.Objects;
 
-import org.apache.cassandra.db.SystemKeyspace;
+import org.apache.cassandra.config.SchemaConstants;
+import org.apache.cassandra.cql3.ColumnIdentifier;
 
 public final class FunctionName
 {
     private static final Set<Character> DISALLOWED_CHARACTERS = Collections.unmodifiableSet(
         new HashSet<>(Arrays.asList('/', '[', ']')));
 
+    // We special case the token function because that's the only function which name is a reserved keyword
+    private static final FunctionName TOKEN_FUNCTION_NAME = FunctionName.nativeFunction("token");
+
     public final String keyspace;
     public final String name;
 
     public static FunctionName nativeFunction(String name)
     {
-        return new FunctionName(SystemKeyspace.NAME, name);
+        return new FunctionName(SchemaConstants.SYSTEM_KEYSPACE_NAME, name);
     }
 
     /**
@@ -93,8 +97,8 @@
 
     public final boolean equalsNativeFunction(FunctionName nativeFunction)
     {
-        assert nativeFunction.keyspace.equals(SystemKeyspace.NAME);
-        if (this.hasKeyspace() && !this.keyspace.equals(SystemKeyspace.NAME))
+        assert nativeFunction.keyspace.equals(SchemaConstants.SYSTEM_KEYSPACE_NAME);
+        if (this.hasKeyspace() && !this.keyspace.equals(SchemaConstants.SYSTEM_KEYSPACE_NAME))
             return false;
 
         return Objects.equal(this.name, nativeFunction.name);
@@ -105,4 +109,18 @@
     {
         return keyspace == null ? name : keyspace + "." + name;
     }
+
+    /**
+     * Returns a string representation of the function name that is safe to use directly in CQL queries.
+     * If necessary, the name components will be double-quoted, and any quotes inside the names will be escaped.
+     */
+    public String toCQLString()
+    {
+        String maybeQuotedName = equalsNativeFunction(TOKEN_FUNCTION_NAME)
+               ? name
+               : ColumnIdentifier.maybeQuote(name);
+        return keyspace == null
+               ? maybeQuotedName
+               : ColumnIdentifier.maybeQuote(keyspace) + '.' + maybeQuotedName;
+    }
 }
diff --git a/src/java/org/apache/cassandra/cql3/functions/FunctionResolver.java b/src/java/org/apache/cassandra/cql3/functions/FunctionResolver.java
index be2daae..9e0b706 100644
--- a/src/java/org/apache/cassandra/cql3/functions/FunctionResolver.java
+++ b/src/java/org/apache/cassandra/cql3/functions/FunctionResolver.java
@@ -126,9 +126,11 @@
             }
         }
 
-        if (compatibles == null || compatibles.isEmpty())
+        if (compatibles == null)
+        {
             throw new InvalidRequestException(String.format("Invalid call to function %s, none of its type signatures match (known type signatures: %s)",
                                                             name, format(candidates)));
+        }
 
         if (compatibles.size() > 1)
             throw new InvalidRequestException(String.format("Ambiguous call to function %s (can be matched by following signatures: %s): use type casts to disambiguate",
diff --git a/src/java/org/apache/cassandra/cql3/functions/JavaBasedUDFunction.java b/src/java/org/apache/cassandra/cql3/functions/JavaBasedUDFunction.java
index 660d494..feb17e3 100644
--- a/src/java/org/apache/cassandra/cql3/functions/JavaBasedUDFunction.java
+++ b/src/java/org/apache/cassandra/cql3/functions/JavaBasedUDFunction.java
@@ -35,8 +35,12 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.io.ByteStreams;
+import com.google.common.reflect.TypeToken;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -45,7 +49,10 @@
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.security.SecurityThreadGroup;
+import org.apache.cassandra.security.ThreadAwareSecurityManager;
 import org.eclipse.jdt.core.compiler.IProblem;
 import org.eclipse.jdt.internal.compiler.*;
 import org.eclipse.jdt.internal.compiler.Compiler;
@@ -57,10 +64,12 @@
 import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
 import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
 
-final class JavaBasedUDFunction extends UDFunction
+public final class JavaBasedUDFunction extends UDFunction
 {
     private static final String BASE_PACKAGE = "org.apache.cassandra.cql3.udf.gen";
 
+    private static final Pattern JAVA_LANG_PREFIX = Pattern.compile("\\bjava\\.lang\\.");
+
     static final Logger logger = LoggerFactory.getLogger(JavaBasedUDFunction.class);
 
     private static final AtomicInteger classSequence = new AtomicInteger();
@@ -184,10 +193,10 @@
         super(name, argNames, argTypes, UDHelper.driverTypes(argTypes),
               returnType, UDHelper.driverType(returnType), calledOnNullInput, "java", body);
 
-        // javaParamTypes is just the Java representation for argTypes resp. argCodecs
-        Class<?>[] javaParamTypes = UDHelper.javaTypes(argCodecs, calledOnNullInput);
-        // javaReturnType is just the Java representation for returnType resp. returnCodec
-        Class<?> javaReturnType = UDHelper.asJavaClass(returnCodec);
+        // javaParamTypes is just the Java representation for argTypes resp. argDataTypes
+        TypeToken<?>[] javaParamTypes = UDHelper.typeTokens(argCodecs, calledOnNullInput);
+        // javaReturnType is just the Java representation for returnType resp. returnTypeCodec
+        TypeToken<?> javaReturnType = returnCodec.getJavaType();
 
         // put each UDF in a separate package to prevent cross-UDF code access
         String pkgName = BASE_PACKAGE + '.' + generateClassName(name, 'p');
@@ -217,7 +226,10 @@
                         s = body;
                         break;
                     case "arguments":
-                        s = generateArguments(javaParamTypes, argNames);
+                        s = generateArguments(javaParamTypes, argNames, false);
+                        break;
+                    case "arguments_aggregate":
+                        s = generateArguments(javaParamTypes, argNames, true);
                         break;
                     case "argument_list":
                         s = generateArgumentList(javaParamTypes, argNames);
@@ -244,7 +256,7 @@
         {
             EcjCompilationUnit compilationUnit = new EcjCompilationUnit(javaSource, targetClassName);
 
-            org.eclipse.jdt.internal.compiler.Compiler compiler = new Compiler(compilationUnit,
+            Compiler compiler = new Compiler(compilationUnit,
                                                                                errorHandlingPolicy,
                                                                                compilerOptions,
                                                                                compilationUnit,
@@ -289,17 +301,14 @@
             }
 
             // Verify the UDF bytecode against use of probably dangerous code
-            Set<String> errors = udfByteCodeVerifier.verify(targetClassLoader.classData(targetClassName));
+            Set<String> errors = udfByteCodeVerifier.verify(targetClassName, targetClassLoader.classData(targetClassName));
             String validDeclare = "not allowed method declared: " + executeInternalName + '(';
-            String validCall = "call to " + targetClassName.replace('.', '/') + '.' + executeInternalName + "()";
             for (Iterator<String> i = errors.iterator(); i.hasNext();)
             {
                 String error = i.next();
                 // we generate a random name of the private, internal execute method, which is detected by the byte-code verifier
-                if (error.startsWith(validDeclare) || error.equals(validCall))
-                {
+                if (error.startsWith(validDeclare))
                     i.remove();
-                }
             }
             if (!errors.isEmpty())
                 throw new InvalidRequestException("Java UDF validation failed: " + errors);
@@ -324,12 +333,12 @@
                     }
                 }
 
-                if (nonSyntheticMethodCount != 2 || cls.getDeclaredConstructors().length != 1)
+                if (nonSyntheticMethodCount != 3 || cls.getDeclaredConstructors().length != 1)
                     throw new InvalidRequestException("Check your source to not define additional Java methods or constructors");
                 MethodType methodType = MethodType.methodType(void.class)
-                                                  .appendParameterTypes(TypeCodec.class, TypeCodec[].class);
+                                                  .appendParameterTypes(TypeCodec.class, TypeCodec[].class, UDFContext.class);
                 MethodHandle ctor = MethodHandles.lookup().findConstructor(cls, methodType);
-                this.javaUDF = (JavaUDF) ctor.invokeWithArguments(returnCodec, argCodecs);
+                this.javaUDF = (JavaUDF) ctor.invokeWithArguments(returnCodec, argCodecs, udfContext);
             }
             finally
             {
@@ -341,12 +350,13 @@
             // in case of an ITE, use the cause
             throw new InvalidRequestException(String.format("Could not compile function '%s' from Java source: %s", name, e.getCause()));
         }
-        catch (VirtualMachineError e)
+        catch (InvalidRequestException | VirtualMachineError e)
         {
             throw e;
         }
         catch (Throwable e)
         {
+            logger.error(String.format("Could not compile function '%s' from Java source:%n%s", name, javaSource), e);
             throw new InvalidRequestException(String.format("Could not compile function '%s' from Java source: %s", name, e));
         }
     }
@@ -356,11 +366,15 @@
         return executor;
     }
 
-    protected ByteBuffer executeUserDefined(int protocolVersion, List<ByteBuffer> params)
+    protected ByteBuffer executeUserDefined(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         return javaUDF.executeImpl(protocolVersion, params);
     }
 
+    protected Object executeAggregateUserDefined(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        return javaUDF.executeAggregateImpl(protocolVersion, firstParam, params);
+    }
 
     private static int countNewlines(StringBuilder javaSource)
     {
@@ -392,13 +406,14 @@
         return sb.toString();
     }
 
-    private static String javaSourceName(Class<?> type)
+    @VisibleForTesting
+    public static String javaSourceName(TypeToken<?> type)
     {
-        String n = type.getName();
-        return n.startsWith("java.lang.") ? type.getSimpleName() : n;
+        String n = type.toString();
+        return JAVA_LANG_PREFIX.matcher(n).replaceAll("");
     }
 
-    private static String generateArgumentList(Class<?>[] paramTypes, List<ColumnIdentifier> argNames)
+    private static String generateArgumentList(TypeToken<?>[] paramTypes, List<ColumnIdentifier> argNames)
     {
         // initial builder size can just be a guess (prevent temp object allocations)
         StringBuilder code = new StringBuilder(32 * paramTypes.length);
@@ -413,29 +428,55 @@
         return code.toString();
     }
 
-    private static String generateArguments(Class<?>[] paramTypes, List<ColumnIdentifier> argNames)
+    /**
+     * Generate Java source code snippet for the arguments part to call the UDF implementation function -
+     * i.e. the {@code private #return_type# #execute_internal_name#(#argument_list#)} function
+     * (see {@code JavaSourceUDF.txt} template file for details).
+     * <p>
+     * This method generates the arguments code snippet for both {@code executeImpl} and
+     * {@code executeAggregateImpl}. General signature for both is the {@code protocolVersion} and
+     * then all UDF arguments. For aggregation UDF calls the first argument is always unserialized as
+     * that is the state variable.
+     * </p>
+     * <p>
+     * An example output for {@code executeImpl}:
+     * {@code (double) super.compose_double(protocolVersion, 0, params.get(0)), (double) super.compose_double(protocolVersion, 1, params.get(1))}
+     * </p>
+     * <p>
+     * Similar output for {@code executeAggregateImpl}:
+     * {@code firstParam, (double) super.compose_double(protocolVersion, 1, params.get(1))}
+     * </p>
+     */
+    private static String generateArguments(TypeToken<?>[] paramTypes, List<ColumnIdentifier> argNames, boolean forAggregate)
     {
         StringBuilder code = new StringBuilder(64 * paramTypes.length);
         for (int i = 0; i < paramTypes.length; i++)
         {
             if (i > 0)
+                // add separator, if not the first argument
                 code.append(",\n");
 
+            // add comment only if trace is enabled
             if (logger.isTraceEnabled())
                 code.append("            /* parameter '").append(argNames.get(i)).append("' */\n");
 
-            code
-                // cast to Java type
-                .append("            (").append(javaSourceName(paramTypes[i])).append(") ")
+            // cast to Java type
+            code.append("            (").append(javaSourceName(paramTypes[i])).append(") ");
+
+            if (forAggregate && i == 0)
+                // special case for aggregations where the state variable (1st arg to state + final function and
+                // return value from state function) is not re-serialized
+                code.append("firstParam");
+            else
                 // generate object representation of input parameter (call UDFunction.compose)
-                .append(composeMethod(paramTypes[i])).append("(protocolVersion, ").append(i).append(", params.get(").append(i).append("))");
+                code.append(composeMethod(paramTypes[i])).append("(protocolVersion, ").append(i).append(", params.get(").append(forAggregate ? i - 1 : i).append("))");
         }
         return code.toString();
     }
 
-    private static String composeMethod(Class<?> type)
+    private static String composeMethod(TypeToken<?> type)
     {
-        return (type.isPrimitive()) ? ("super.compose_" + type.getName()) : "super.compose";
+        return (type.isPrimitive()) ? ("super.compose_" + type.getRawType().getName()) : "super.compose";
     }
 
     // Java source UDFs are a very simple compilation task, which allows us to let one class implement
diff --git a/src/java/org/apache/cassandra/cql3/functions/JavaUDF.java b/src/java/org/apache/cassandra/cql3/functions/JavaUDF.java
index fcfd21c..fab29f3 100644
--- a/src/java/org/apache/cassandra/cql3/functions/JavaUDF.java
+++ b/src/java/org/apache/cassandra/cql3/functions/JavaUDF.java
@@ -22,6 +22,7 @@
 import java.util.List;
 
 import com.datastax.driver.core.TypeCodec;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Base class for all Java UDFs.
@@ -34,68 +35,73 @@
     private final TypeCodec<Object> returnCodec;
     private final TypeCodec<Object>[] argCodecs;
 
-    protected JavaUDF(TypeCodec<Object> returnCodec, TypeCodec<Object>[] argCodecs)
+    protected final UDFContext udfContext;
+
+    protected JavaUDF(TypeCodec<Object> returnCodec, TypeCodec<Object>[] argCodecs, UDFContext udfContext)
     {
         this.returnCodec = returnCodec;
         this.argCodecs = argCodecs;
+        this.udfContext = udfContext;
     }
 
-    protected abstract ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params);
+    protected abstract ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params);
 
-    protected Object compose(int protocolVersion, int argIndex, ByteBuffer value)
+    protected abstract Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params);
+
+    protected Object compose(ProtocolVersion protocolVersion, int argIndex, ByteBuffer value)
     {
         return UDFunction.compose(argCodecs, protocolVersion, argIndex, value);
     }
 
-    protected ByteBuffer decompose(int protocolVersion, Object value)
+    protected ByteBuffer decompose(ProtocolVersion protocolVersion, Object value)
     {
         return UDFunction.decompose(returnCodec, protocolVersion, value);
     }
 
     // do not remove - used by generated Java UDFs
-    protected float compose_float(int protocolVersion, int argIndex, ByteBuffer value)
+    protected float compose_float(ProtocolVersion protocolVersion, int argIndex, ByteBuffer value)
     {
         assert value != null && value.remaining() > 0;
         return (float) UDHelper.deserialize(TypeCodec.cfloat(), protocolVersion, value);
     }
 
     // do not remove - used by generated Java UDFs
-    protected double compose_double(int protocolVersion, int argIndex, ByteBuffer value)
+    protected double compose_double(ProtocolVersion protocolVersion, int argIndex, ByteBuffer value)
     {
         assert value != null && value.remaining() > 0;
         return (double) UDHelper.deserialize(TypeCodec.cdouble(), protocolVersion, value);
     }
 
     // do not remove - used by generated Java UDFs
-    protected byte compose_byte(int protocolVersion, int argIndex, ByteBuffer value)
+    protected byte compose_byte(ProtocolVersion protocolVersion, int argIndex, ByteBuffer value)
     {
         assert value != null && value.remaining() > 0;
         return (byte) UDHelper.deserialize(TypeCodec.tinyInt(), protocolVersion, value);
     }
 
     // do not remove - used by generated Java UDFs
-    protected short compose_short(int protocolVersion, int argIndex, ByteBuffer value)
+    protected short compose_short(ProtocolVersion protocolVersion, int argIndex, ByteBuffer value)
     {
         assert value != null && value.remaining() > 0;
         return (short) UDHelper.deserialize(TypeCodec.smallInt(), protocolVersion, value);
     }
 
     // do not remove - used by generated Java UDFs
-    protected int compose_int(int protocolVersion, int argIndex, ByteBuffer value)
+    protected int compose_int(ProtocolVersion protocolVersion, int argIndex, ByteBuffer value)
     {
         assert value != null && value.remaining() > 0;
         return (int) UDHelper.deserialize(TypeCodec.cint(), protocolVersion, value);
     }
 
     // do not remove - used by generated Java UDFs
-    protected long compose_long(int protocolVersion, int argIndex, ByteBuffer value)
+    protected long compose_long(ProtocolVersion protocolVersion, int argIndex, ByteBuffer value)
     {
         assert value != null && value.remaining() > 0;
         return (long) UDHelper.deserialize(TypeCodec.bigint(), protocolVersion, value);
     }
 
     // do not remove - used by generated Java UDFs
-    protected boolean compose_boolean(int protocolVersion, int argIndex, ByteBuffer value)
+    protected boolean compose_boolean(ProtocolVersion protocolVersion, int argIndex, ByteBuffer value)
     {
         assert value != null && value.remaining() > 0;
         return (boolean) UDHelper.deserialize(TypeCodec.cboolean(), protocolVersion, value);
diff --git a/src/java/org/apache/cassandra/cql3/functions/ScalarFunction.java b/src/java/org/apache/cassandra/cql3/functions/ScalarFunction.java
index ba258df..1f98372 100644
--- a/src/java/org/apache/cassandra/cql3/functions/ScalarFunction.java
+++ b/src/java/org/apache/cassandra/cql3/functions/ScalarFunction.java
@@ -21,6 +21,7 @@
 import java.util.List;
 
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Determines a single output value based on a single input value.
@@ -37,5 +38,5 @@
      * @return the result of applying this function to the parameter
      * @throws InvalidRequestException if this function cannot not be applied to the parameter
      */
-    public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters) throws InvalidRequestException;
+    public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters) throws InvalidRequestException;
 }
diff --git a/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDFunction.java b/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDFunction.java
index 47deafa..41035a4 100644
--- a/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDFunction.java
+++ b/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDFunction.java
@@ -27,6 +27,7 @@
 import java.util.concurrent.ExecutorService;
 import javax.script.*;
 
+import jdk.nashorn.api.scripting.AbstractJSObject;
 import jdk.nashorn.api.scripting.ClassFilter;
 import jdk.nashorn.api.scripting.NashornScriptEngine;
 import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
@@ -34,6 +35,9 @@
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.security.SecurityThreadGroup;
+import org.apache.cassandra.security.ThreadAwareSecurityManager;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 final class ScriptBasedUDFunction extends UDFunction
 {
@@ -122,6 +126,7 @@
     }
 
     private final CompiledScript script;
+    private final Object udfContextBinding;
 
     ScriptBasedUDFunction(FunctionName name,
                           List<ColumnIdentifier> argNames,
@@ -149,6 +154,10 @@
             throw new InvalidRequestException(
                                              String.format("Failed to compile function '%s' for language %s: %s", name, language, e));
         }
+
+        // It's not always possible to simply pass a plain Java object as a binding to Nashorn and
+        // let the script execute methods on it.
+        udfContextBinding = new UDFContextWrapper();
     }
 
     protected ExecutorService executor()
@@ -156,17 +165,41 @@
         return executor;
     }
 
-    public ByteBuffer executeUserDefined(int protocolVersion, List<ByteBuffer> parameters)
+    public ByteBuffer executeUserDefined(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
     {
         Object[] params = new Object[argTypes.size()];
         for (int i = 0; i < params.length; i++)
             params[i] = compose(protocolVersion, i, parameters.get(i));
 
+        Object result = executeScriptInternal(params);
+
+        return decompose(protocolVersion, result);
+    }
+
+    /**
+     * Like {@link UDFunction#executeUserDefined(ProtocolVersion, List)} but the first parameter is already in non-serialized form.
+     * Remaining parameters (2nd paramters and all others) are in {@code parameters}.
+     * This is used to prevent superfluous (de)serialization of the state of aggregates.
+     * Means: scalar functions of aggregates are called using this variant.
+     */
+    protected Object executeAggregateUserDefined(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> parameters)
+    {
+        Object[] params = new Object[argTypes.size()];
+        params[0] = firstParam;
+        for (int i = 1; i < params.length; i++)
+            params[i] = compose(protocolVersion, i, parameters.get(i - 1));
+
+        return executeScriptInternal(params);
+    }
+
+    private Object executeScriptInternal(Object[] params)
+    {
         ScriptContext scriptContext = new SimpleScriptContext();
         scriptContext.setAttribute("javax.script.filename", this.name.toString(), ScriptContext.ENGINE_SCOPE);
         Bindings bindings = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
         for (int i = 0; i < params.length; i++)
             bindings.put(argNames.get(i).toString(), params[i]);
+        bindings.put("udfContext", udfContextBinding);
 
         Object result;
         try
@@ -235,6 +268,70 @@
             }
         }
 
-        return decompose(protocolVersion, result);
+        return result;
+    }
+
+    private final class UDFContextWrapper extends AbstractJSObject
+    {
+        private final AbstractJSObject fRetUDT;
+        private final AbstractJSObject fArgUDT;
+        private final AbstractJSObject fRetTup;
+        private final AbstractJSObject fArgTup;
+
+        UDFContextWrapper()
+        {
+            fRetUDT = new AbstractJSObject()
+            {
+                public Object call(Object thiz, Object... args)
+                {
+                    return udfContext.newReturnUDTValue();
+                }
+            };
+            fArgUDT = new AbstractJSObject()
+            {
+                public Object call(Object thiz, Object... args)
+                {
+                    if (args[0] instanceof String)
+                        return udfContext.newArgUDTValue((String) args[0]);
+                    if (args[0] instanceof Number)
+                        return udfContext.newArgUDTValue(((Number) args[0]).intValue());
+                    return super.call(thiz, args);
+                }
+            };
+            fRetTup = new AbstractJSObject()
+            {
+                public Object call(Object thiz, Object... args)
+                {
+                    return udfContext.newReturnTupleValue();
+                }
+            };
+            fArgTup = new AbstractJSObject()
+            {
+                public Object call(Object thiz, Object... args)
+                {
+                    if (args[0] instanceof String)
+                        return udfContext.newArgTupleValue((String) args[0]);
+                    if (args[0] instanceof Number)
+                        return udfContext.newArgTupleValue(((Number) args[0]).intValue());
+                    return super.call(thiz, args);
+                }
+            };
+        }
+
+        public Object getMember(String name)
+        {
+            switch(name)
+            {
+                case "newReturnUDTValue":
+                    return fRetUDT;
+                case "newArgUDTValue":
+                    return fArgUDT;
+                case "newReturnTupleValue":
+                    return fRetTup;
+                case "newArgTupleValue":
+                    return fArgTup;
+            }
+            return super.getMember(name);
+        }
     }
 }
diff --git a/src/java/org/apache/cassandra/cql3/functions/SecurityThreadGroup.java b/src/java/org/apache/cassandra/cql3/functions/SecurityThreadGroup.java
deleted file mode 100644
index 8f50dc8..0000000
--- a/src/java/org/apache/cassandra/cql3/functions/SecurityThreadGroup.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.functions;
-
-import java.util.Set;
-
-/**
- * Used by {@link ThreadAwareSecurityManager} to determine whether access-control checks needs to be performed.
- */
-public final class SecurityThreadGroup extends ThreadGroup
-{
-    private final Set<String> allowedPackages;
-    private final ThreadInitializer threadInitializer;
-
-    public SecurityThreadGroup(String name, Set<String> allowedPackages, ThreadInitializer threadInitializer)
-    {
-        super(name);
-        this.allowedPackages = allowedPackages;
-        this.threadInitializer = threadInitializer;
-    }
-
-    public void initializeThread()
-    {
-        threadInitializer.initializeThread();
-    }
-
-    public boolean isPackageAllowed(String pkg)
-    {
-        return allowedPackages == null || allowedPackages.contains(pkg);
-    }
-
-    @FunctionalInterface
-    interface ThreadInitializer
-    {
-        void initializeThread();
-    }
-}
diff --git a/src/java/org/apache/cassandra/cql3/functions/ThreadAwareSecurityManager.java b/src/java/org/apache/cassandra/cql3/functions/ThreadAwareSecurityManager.java
deleted file mode 100644
index 782d500..0000000
--- a/src/java/org/apache/cassandra/cql3/functions/ThreadAwareSecurityManager.java
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * 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.functions;
-
-import java.security.AccessControlException;
-import java.security.AllPermission;
-import java.security.CodeSource;
-import java.security.Permission;
-import java.security.PermissionCollection;
-import java.security.Permissions;
-import java.security.Policy;
-import java.security.ProtectionDomain;
-import java.util.Collections;
-import java.util.Enumeration;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.spi.TurboFilterList;
-import ch.qos.logback.classic.turbo.ReconfigureOnChangeFilter;
-import ch.qos.logback.classic.turbo.TurboFilter;
-import org.apache.cassandra.config.DatabaseDescriptor;
-
-/**
- * Custom {@link SecurityManager} and {@link Policy} implementation that only performs access checks
- * if explicitly enabled.
- * <p>
- * This implementation gives no measurable performance panalty
- * (see <a href="http://cstar.datastax.com/tests/id/1d461628-12ba-11e5-918f-42010af0688f">see cstar test</a>).
- * This is better than the penalty of 1 to 3 percent using a standard {@code SecurityManager} with an <i>allow all</i> policy.
- * </p>
- */
-public final class ThreadAwareSecurityManager extends SecurityManager
-{
-    static final PermissionCollection noPermissions = new PermissionCollection()
-    {
-        public void add(Permission permission)
-        {
-            throw new UnsupportedOperationException();
-        }
-
-        public boolean implies(Permission permission)
-        {
-            return false;
-        }
-
-        public Enumeration<Permission> elements()
-        {
-            return Collections.emptyEnumeration();
-        }
-    };
-
-    private static final RuntimePermission CHECK_MEMBER_ACCESS_PERMISSION = new RuntimePermission("accessDeclaredMembers");
-    private static final RuntimePermission MODIFY_THREAD_PERMISSION = new RuntimePermission("modifyThread");
-    private static final RuntimePermission MODIFY_THREADGROUP_PERMISSION = new RuntimePermission("modifyThreadGroup");
-    private static final RuntimePermission SET_SECURITY_MANAGER_PERMISSION = new RuntimePermission("setSecurityManager");
-
-    private static volatile boolean installed;
-
-    public static void install()
-    {
-        if (installed)
-            return;
-        System.setSecurityManager(new ThreadAwareSecurityManager());
-
-        // The default logback configuration in conf/logback.xml allows reloading the
-        // configuration when the configuration file has changed (every 60 seconds by default).
-        // This requires logback to use file I/O APIs. But file I/O is not allowed from UDFs.
-        // I.e. if logback decides to check for a modification of the config file while
-        // executiing a sandbox thread, the UDF execution and therefore the whole request
-        // execution will fail with an AccessControlException.
-        // To work around this, a custom ReconfigureOnChangeFilter is installed, that simply
-        // prevents this configuration file check and possible reload of the configration,
-        // while executing sandboxed UDF code.
-        Logger l = LoggerFactory.getLogger(ThreadAwareSecurityManager.class);
-        ch.qos.logback.classic.Logger logbackLogger = (ch.qos.logback.classic.Logger) l;
-        LoggerContext ctx = logbackLogger.getLoggerContext();
-
-        TurboFilterList turboFilterList = ctx.getTurboFilterList();
-        for (int i = 0; i < turboFilterList.size(); i++)
-        {
-            TurboFilter turboFilter = turboFilterList.get(i);
-            if (turboFilter instanceof ReconfigureOnChangeFilter)
-            {
-                ReconfigureOnChangeFilter reconfigureOnChangeFilter = (ReconfigureOnChangeFilter) turboFilter;
-                turboFilterList.set(i, new SMAwareReconfigureOnChangeFilter(reconfigureOnChangeFilter));
-                break;
-            }
-        }
-
-        installed = true;
-    }
-
-    /**
-     * The purpose of this class is to prevent logback from checking for config file change,
-     * if the current thread is executing a sandboxed thread to avoid {@link AccessControlException}s.
-     */
-    private static class SMAwareReconfigureOnChangeFilter extends ReconfigureOnChangeFilter
-    {
-        SMAwareReconfigureOnChangeFilter(ReconfigureOnChangeFilter reconfigureOnChangeFilter)
-        {
-            setRefreshPeriod(reconfigureOnChangeFilter.getRefreshPeriod());
-            setName(reconfigureOnChangeFilter.getName());
-            setContext(reconfigureOnChangeFilter.getContext());
-            if (reconfigureOnChangeFilter.isStarted())
-            {
-                reconfigureOnChangeFilter.stop();
-                start();
-            }
-        }
-
-        protected boolean changeDetected(long now)
-        {
-            if (isSecuredThread())
-                return false;
-            return super.changeDetected(now);
-        }
-    }
-
-    static
-    {
-        //
-        // Use own security policy to be easier (and faster) since the C* has no fine grained permissions.
-        // Either code has access to everything or code has access to nothing (UDFs).
-        // This also removes the burden to maintain and configure policy files for production, unit tests etc.
-        //
-        // Note: a permission is only granted, if there is no objector. This means that
-        // AccessController/AccessControlContext collect all applicable ProtectionDomains - only if none of these
-        // applicable ProtectionDomains denies access, the permission is granted.
-        // A ProtectionDomain can have its origin at an oridinary code-source or provided via a
-        // AccessController.doPrivileded() call.
-        //
-        Policy.setPolicy(new Policy()
-        {
-            public PermissionCollection getPermissions(CodeSource codesource)
-            {
-                // contract of getPermissions() methods is to return a _mutable_ PermissionCollection
-
-                Permissions perms = new Permissions();
-
-                if (codesource == null || codesource.getLocation() == null)
-                    return perms;
-
-                switch (codesource.getLocation().getProtocol())
-                {
-                    case "file":
-                        // All JARs and class files reside on the file system - we can safely
-                        // assume that these classes are "good".
-                        perms.add(new AllPermission());
-                        return perms;
-                }
-
-                return perms;
-            }
-
-            public PermissionCollection getPermissions(ProtectionDomain domain)
-            {
-                return getPermissions(domain.getCodeSource());
-            }
-
-            public boolean implies(ProtectionDomain domain, Permission permission)
-            {
-                CodeSource codesource = domain.getCodeSource();
-                if (codesource == null || codesource.getLocation() == null)
-                    return false;
-
-                switch (codesource.getLocation().getProtocol())
-                {
-                    case "file":
-                        // All JARs and class files reside on the file system - we can safely
-                        // assume that these classes are "good".
-                        return true;
-                }
-
-                return false;
-            }
-        });
-    }
-
-    private static final ThreadLocal<Boolean> initializedThread = new ThreadLocal<>();
-
-    private ThreadAwareSecurityManager()
-    {
-    }
-
-    private static boolean isSecuredThread()
-    {
-        ThreadGroup tg = Thread.currentThread().getThreadGroup();
-        if (!(tg instanceof SecurityThreadGroup))
-            return false;
-        Boolean threadInitialized = initializedThread.get();
-        if (threadInitialized == null)
-        {
-            initializedThread.set(false);
-            ((SecurityThreadGroup) tg).initializeThread();
-            initializedThread.set(true);
-            threadInitialized = true;
-        }
-        return threadInitialized;
-    }
-
-    public void checkAccess(Thread t)
-    {
-        // need to override since the default implementation only checks the permission if the current thread's
-        // in the root-thread-group
-
-        if (isSecuredThread())
-            throw new AccessControlException("access denied: " + MODIFY_THREAD_PERMISSION, MODIFY_THREAD_PERMISSION);
-        super.checkAccess(t);
-    }
-
-    public void checkAccess(ThreadGroup g)
-    {
-        // need to override since the default implementation only checks the permission if the current thread's
-        // in the root-thread-group
-
-        if (isSecuredThread())
-            throw new AccessControlException("access denied: " + MODIFY_THREADGROUP_PERMISSION, MODIFY_THREADGROUP_PERMISSION);
-        super.checkAccess(g);
-    }
-
-    public void checkPermission(Permission perm)
-    {
-        if (!DatabaseDescriptor.enableUserDefinedFunctionsThreads() && !DatabaseDescriptor.allowExtraInsecureUDFs() && SET_SECURITY_MANAGER_PERMISSION.equals(perm))
-            throw new AccessControlException("Access denied");
-
-        if (!isSecuredThread())
-            return;
-
-        // required by JavaDriver 2.2.0-rc3 and 3.0.0-a2 or newer
-        // code in com.datastax.driver.core.CodecUtils uses Guava stuff, which in turns requires this permission
-        if (CHECK_MEMBER_ACCESS_PERMISSION.equals(perm))
-            return;
-
-        super.checkPermission(perm);
-    }
-
-    public void checkPermission(Permission perm, Object context)
-    {
-        if (isSecuredThread())
-            super.checkPermission(perm, context);
-    }
-
-    public void checkPackageAccess(String pkg)
-    {
-        if (!isSecuredThread())
-            return;
-
-        if (!((SecurityThreadGroup) Thread.currentThread().getThreadGroup()).isPackageAllowed(pkg))
-        {
-            RuntimePermission perm = new RuntimePermission("accessClassInPackage." + pkg);
-            throw new AccessControlException("access denied: " + perm, perm);
-        }
-
-        super.checkPackageAccess(pkg);
-    }
-}
diff --git a/src/java/org/apache/cassandra/cql3/functions/TimeFcts.java b/src/java/org/apache/cassandra/cql3/functions/TimeFcts.java
index 93d6d3b..e682dcd 100644
--- a/src/java/org/apache/cassandra/cql3/functions/TimeFcts.java
+++ b/src/java/org/apache/cassandra/cql3/functions/TimeFcts.java
@@ -27,7 +27,7 @@
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.db.marshal.*;
-import org.apache.cassandra.serializers.TimestampSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.UUIDGen;
 
@@ -53,7 +53,7 @@
 
     public static final Function nowFct = new NativeScalarFunction("now", TimeUUIDType.instance)
     {
-        public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
         {
             return ByteBuffer.wrap(UUIDGen.getTimeUUIDBytes());
         }
@@ -61,25 +61,25 @@
 
     public static final Function minTimeuuidFct = new NativeScalarFunction("mintimeuuid", TimeUUIDType.instance, TimestampType.instance)
     {
-        public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
         {
             ByteBuffer bb = parameters.get(0);
             if (bb == null)
                 return null;
 
-            return ByteBuffer.wrap(UUIDGen.decompose(UUIDGen.minTimeUUID(TimestampType.instance.compose(bb).getTime())));
+            return UUIDGen.toByteBuffer(UUIDGen.minTimeUUID(TimestampType.instance.compose(bb).getTime()));
         }
     };
 
     public static final Function maxTimeuuidFct = new NativeScalarFunction("maxtimeuuid", TimeUUIDType.instance, TimestampType.instance)
     {
-        public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
         {
             ByteBuffer bb = parameters.get(0);
             if (bb == null)
                 return null;
 
-            return ByteBuffer.wrap(UUIDGen.decompose(UUIDGen.maxTimeUUID(TimestampType.instance.compose(bb).getTime())));
+            return UUIDGen.toByteBuffer(UUIDGen.maxTimeUUID(TimestampType.instance.compose(bb).getTime()));
         }
     };
 
@@ -87,11 +87,11 @@
      * Function that convert a value of <code>TIMEUUID</code> into a value of type <code>TIMESTAMP</code>.
      * @deprecated Replaced by the {@link #timeUuidToTimestamp} function
      */
-    public static final Function dateOfFct = new NativeScalarFunction("dateof", TimestampType.instance, TimeUUIDType.instance)
+    public static final NativeScalarFunction dateOfFct = new NativeScalarFunction("dateof", TimestampType.instance, TimeUUIDType.instance)
     {
         private volatile boolean hasLoggedDeprecationWarning;
 
-        public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
         {
             if (!hasLoggedDeprecationWarning)
             {
@@ -113,11 +113,11 @@
      * Function that convert a value of type <code>TIMEUUID</code> into an UNIX timestamp.
      * @deprecated Replaced by the {@link #timeUuidToUnixTimestamp} function
      */
-    public static final Function unixTimestampOfFct = new NativeScalarFunction("unixtimestampof", LongType.instance, TimeUUIDType.instance)
+    public static final NativeScalarFunction unixTimestampOfFct = new NativeScalarFunction("unixtimestampof", LongType.instance, TimeUUIDType.instance)
     {
         private volatile boolean hasLoggedDeprecationWarning;
 
-        public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
         {
             if (!hasLoggedDeprecationWarning)
             {
@@ -137,9 +137,9 @@
     /**
      * Function that convert a value of <code>TIMEUUID</code> into a value of type <code>DATE</code>.
      */
-    public static final Function timeUuidtoDate = new NativeScalarFunction("todate", SimpleDateType.instance, TimeUUIDType.instance)
+    public static final NativeScalarFunction timeUuidtoDate = new NativeScalarFunction("todate", SimpleDateType.instance, TimeUUIDType.instance)
     {
-        public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
         {
             ByteBuffer bb = parameters.get(0);
             if (bb == null)
@@ -153,9 +153,9 @@
     /**
      * Function that convert a value of type <code>TIMEUUID</code> into a value of type <code>TIMESTAMP</code>.
      */
-    public static final Function timeUuidToTimestamp = new NativeScalarFunction("totimestamp", TimestampType.instance, TimeUUIDType.instance)
+    public static final NativeScalarFunction timeUuidToTimestamp = new NativeScalarFunction("totimestamp", TimestampType.instance, TimeUUIDType.instance)
     {
-        public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
         {
             ByteBuffer bb = parameters.get(0);
             if (bb == null)
@@ -169,9 +169,9 @@
     /**
      * Function that convert a value of type <code>TIMEUUID</code> into an UNIX timestamp.
      */
-    public static final Function timeUuidToUnixTimestamp = new NativeScalarFunction("tounixtimestamp", LongType.instance, TimeUUIDType.instance)
+    public static final NativeScalarFunction timeUuidToUnixTimestamp = new NativeScalarFunction("tounixtimestamp", LongType.instance, TimeUUIDType.instance)
     {
-        public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
         {
             ByteBuffer bb = parameters.get(0);
             if (bb == null)
@@ -184,9 +184,9 @@
     /**
      * Function that convert a value of type <code>TIMESTAMP</code> into an UNIX timestamp.
      */
-    public static final Function timestampToUnixTimestamp = new NativeScalarFunction("tounixtimestamp", LongType.instance, TimestampType.instance)
+    public static final NativeScalarFunction timestampToUnixTimestamp = new NativeScalarFunction("tounixtimestamp", LongType.instance, TimestampType.instance)
     {
-        public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
         {
             ByteBuffer bb = parameters.get(0);
             if (bb == null)
@@ -200,9 +200,9 @@
    /**
     * Function that convert a value of type <code>TIMESTAMP</code> into a <code>DATE</code>.
     */
-   public static final Function timestampToDate = new NativeScalarFunction("todate", SimpleDateType.instance, TimestampType.instance)
+   public static final NativeScalarFunction timestampToDate = new NativeScalarFunction("todate", SimpleDateType.instance, TimestampType.instance)
    {
-       public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+       public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
        {
            ByteBuffer bb = parameters.get(0);
            if (bb == null)
@@ -216,9 +216,9 @@
    /**
     * Function that convert a value of type <code>TIMESTAMP</code> into a <code>DATE</code>.
     */
-   public static final Function dateToTimestamp = new NativeScalarFunction("totimestamp", TimestampType.instance, SimpleDateType.instance)
+   public static final NativeScalarFunction dateToTimestamp = new NativeScalarFunction("totimestamp", TimestampType.instance, SimpleDateType.instance)
    {
-       public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+       public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
        {
            ByteBuffer bb = parameters.get(0);
            if (bb == null)
@@ -232,9 +232,9 @@
    /**
     * Function that convert a value of type <code>DATE</code> into an UNIX timestamp.
     */
-   public static final Function dateToUnixTimestamp = new NativeScalarFunction("tounixtimestamp", LongType.instance, SimpleDateType.instance)
+   public static final NativeScalarFunction dateToUnixTimestamp = new NativeScalarFunction("tounixtimestamp", LongType.instance, SimpleDateType.instance)
    {
-       public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+       public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
        {
            ByteBuffer bb = parameters.get(0);
            if (bb == null)
diff --git a/src/java/org/apache/cassandra/cql3/functions/ToJsonFct.java b/src/java/org/apache/cassandra/cql3/functions/ToJsonFct.java
index bcb4559..d0f2b0b 100644
--- a/src/java/org/apache/cassandra/cql3/functions/ToJsonFct.java
+++ b/src/java/org/apache/cassandra/cql3/functions/ToJsonFct.java
@@ -20,11 +20,10 @@
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 import java.nio.ByteBuffer;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -55,7 +54,7 @@
         super("tojson", UTF8Type.instance, argType);
     }
 
-    public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters) throws InvalidRequestException
+    public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters) throws InvalidRequestException
     {
         assert parameters.size() == 1 : "Expected 1 argument for toJson(), but got " + parameters.size();
         ByteBuffer parameter = parameters.get(0);
diff --git a/src/java/org/apache/cassandra/cql3/functions/TokenFct.java b/src/java/org/apache/cassandra/cql3/functions/TokenFct.java
index 283ac0b..1907641 100644
--- a/src/java/org/apache/cassandra/cql3/functions/TokenFct.java
+++ b/src/java/org/apache/cassandra/cql3/functions/TokenFct.java
@@ -25,6 +25,7 @@
 import org.apache.cassandra.db.CBuilder;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 public class TokenFct extends NativeScalarFunction
 {
@@ -45,7 +46,7 @@
         return types;
     }
 
-    public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters) throws InvalidRequestException
+    public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters) throws InvalidRequestException
     {
         CBuilder builder = CBuilder.create(cfm.getKeyValidatorAsClusteringComparator());
         for (int i = 0; i < parameters.size(); i++)
diff --git a/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java b/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java
index 96e19de..1a3174c 100644
--- a/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java
+++ b/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java
@@ -24,10 +24,12 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.schema.Functions;
 import org.apache.cassandra.tracing.Tracing;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Base class for user-defined-aggregates.
@@ -36,7 +38,9 @@
 {
     protected static final Logger logger = LoggerFactory.getLogger(UDAggregate.class);
 
-    protected final AbstractType<?> stateType;
+    private final AbstractType<?> stateType;
+    private final TypeCodec stateTypeCodec;
+    private final TypeCodec returnTypeCodec;
     protected final ByteBuffer initcond;
     private final ScalarFunction stateFunction;
     private final ScalarFunction finalFunction;
@@ -52,6 +56,8 @@
         this.stateFunction = stateFunc;
         this.finalFunction = finalFunc;
         this.stateType = stateFunc != null ? stateFunc.returnType() : null;
+        this.stateTypeCodec = stateType != null ? UDHelper.codecFor(UDHelper.driverType(stateType)) : null;
+        this.returnTypeCodec = returnType != null ? UDHelper.codecFor(UDHelper.driverType(returnType)) : null;
         this.initcond = initcond;
     }
 
@@ -68,7 +74,7 @@
         List<AbstractType<?>> stateTypes = new ArrayList<>(argTypes.size() + 1);
         stateTypes.add(stateType);
         stateTypes.addAll(argTypes);
-        List<AbstractType<?>> finalTypes = Collections.<AbstractType<?>>singletonList(stateType);
+        List<AbstractType<?>> finalTypes = Collections.singletonList(stateType);
         return new UDAggregate(name,
                                argTypes,
                                returnType,
@@ -81,7 +87,7 @@
                                            List<AbstractType<?>> argTypes,
                                            AbstractType<?> returnType,
                                            ByteBuffer initcond,
-                                           final InvalidRequestException reason)
+                                           InvalidRequestException reason)
     {
         return new UDAggregate(name, argTypes, returnType, null, null, initcond)
         {
@@ -150,48 +156,60 @@
             private long stateFunctionCount;
             private long stateFunctionDuration;
 
-            private ByteBuffer state;
-            {
-                reset();
-            }
+            private Object state;
+            private boolean needsInit = true;
 
-            public void addInput(int protocolVersion, List<ByteBuffer> values) throws InvalidRequestException
+            public void addInput(ProtocolVersion protocolVersion, List<ByteBuffer> values) throws InvalidRequestException
             {
+                maybeInit(protocolVersion);
+
                 long startTime = System.nanoTime();
                 stateFunctionCount++;
-                List<ByteBuffer> fArgs = new ArrayList<>(values.size() + 1);
-                fArgs.add(state);
-                fArgs.addAll(values);
                 if (stateFunction instanceof UDFunction)
                 {
                     UDFunction udf = (UDFunction)stateFunction;
-                    if (udf.isCallableWrtNullable(fArgs))
-                        state = udf.execute(protocolVersion, fArgs);
+                    if (udf.isCallableWrtNullable(values))
+                        state = udf.executeForAggregate(protocolVersion, state, values);
                 }
                 else
                 {
-                    state = stateFunction.execute(protocolVersion, fArgs);
+                    throw new UnsupportedOperationException("UDAs only support UDFs");
                 }
                 stateFunctionDuration += (System.nanoTime() - startTime) / 1000;
             }
 
-            public ByteBuffer compute(int protocolVersion) throws InvalidRequestException
+            private void maybeInit(ProtocolVersion protocolVersion)
             {
+                if (needsInit)
+                {
+                    state = initcond != null ? UDHelper.deserialize(stateTypeCodec, protocolVersion, initcond.duplicate()) : null;
+                    stateFunctionDuration = 0;
+                    stateFunctionCount = 0;
+                    needsInit = false;
+                }
+            }
+
+            public ByteBuffer compute(ProtocolVersion protocolVersion) throws InvalidRequestException
+            {
+                maybeInit(protocolVersion);
+
                 // final function is traced in UDFunction
                 Tracing.trace("Executed UDA {}: {} call(s) to state function {} in {}\u03bcs", name(), stateFunctionCount, stateFunction.name(), stateFunctionDuration);
                 if (finalFunction == null)
-                    return state;
+                    return UDFunction.decompose(stateTypeCodec, protocolVersion, state);
 
-                List<ByteBuffer> fArgs = Collections.singletonList(state);
-                ByteBuffer result = finalFunction.execute(protocolVersion, fArgs);
-                return result;
+                if (finalFunction instanceof UDFunction)
+                {
+                    UDFunction udf = (UDFunction)finalFunction;
+                    Object result = udf.executeForAggregate(protocolVersion, state, Collections.emptyList());
+                    return UDFunction.decompose(returnTypeCodec, protocolVersion, result);
+                }
+                throw new UnsupportedOperationException("UDAs only support UDFs");
             }
 
             public void reset()
             {
-                state = initcond != null ? initcond.duplicate() : null;
-                stateFunctionDuration = 0;
-                stateFunctionCount = 0;
+                needsInit = true;
             }
         };
     }
@@ -225,7 +243,7 @@
             && Functions.typesMatch(returnType, that.returnType)
             && Objects.equal(stateFunction, that.stateFunction)
             && Objects.equal(finalFunction, that.finalFunction)
-            && Objects.equal(stateType, that.stateType)
+            && ((stateType == that.stateType) || ((stateType != null) && stateType.equals(that.stateType, true)))  // ignore freezing
             && Objects.equal(initcond, that.initcond);
     }
 
diff --git a/src/java/org/apache/cassandra/cql3/functions/UDFByteCodeVerifier.java b/src/java/org/apache/cassandra/cql3/functions/UDFByteCodeVerifier.java
index 1314af3..234aed9 100644
--- a/src/java/org/apache/cassandra/cql3/functions/UDFByteCodeVerifier.java
+++ b/src/java/org/apache/cassandra/cql3/functions/UDFByteCodeVerifier.java
@@ -47,7 +47,7 @@
 
     public static final String JAVA_UDF_NAME = JavaUDF.class.getName().replace('.', '/');
     public static final String OBJECT_NAME = Object.class.getName().replace('.', '/');
-    public static final String CTOR_SIG = "(Lcom/datastax/driver/core/TypeCodec;[Lcom/datastax/driver/core/TypeCodec;)V";
+    public static final String CTOR_SIG = "(Lcom/datastax/driver/core/TypeCodec;[Lcom/datastax/driver/core/TypeCodec;Lorg/apache/cassandra/cql3/functions/UDFContext;)V";
 
     private final Set<String> disallowedClasses = new HashSet<>();
     private final Multimap<String, String> disallowedMethodCalls = HashMultimap.create();
@@ -80,8 +80,9 @@
         return this;
     }
 
-    public Set<String> verify(byte[] bytes)
+    public Set<String> verify(String clsName, byte[] bytes)
     {
+        String clsNameSl = clsName.replace('.', '/');
         Set<String> errors = new TreeSet<>(); // it's a TreeSet for unit tests
         ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5)
         {
@@ -100,11 +101,18 @@
                     // allowed constructor - JavaUDF(TypeCodec returnCodec, TypeCodec[] argCodecs)
                     return new ConstructorVisitor(errors);
                 }
-                if ("executeImpl".equals(name) && "(ILjava/util/List;)Ljava/nio/ByteBuffer;".equals(desc))
+                if ("executeImpl".equals(name) && "(Lorg/apache/cassandra/transport/ProtocolVersion;Ljava/util/List;)Ljava/nio/ByteBuffer;".equals(desc))
                 {
                     if (Opcodes.ACC_PROTECTED != access)
                         errors.add("executeImpl not protected");
-                    // the executeImpl method - ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+                    // the executeImpl method - ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
+                    return new ExecuteImplVisitor(errors);
+                }
+                if ("executeAggregateImpl".equals(name) && "(Lorg/apache/cassandra/transport/ProtocolVersion;Ljava/lang/Object;Ljava/util/List;)Ljava/lang/Object;".equals(desc))
+                {
+                    if (Opcodes.ACC_PROTECTED != access)
+                        errors.add("executeAggregateImpl not protected");
+                    // the executeImpl method - ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
                     return new ExecuteImplVisitor(errors);
                 }
                 if ("<clinit>".equals(name))
@@ -134,7 +142,8 @@
 
             public void visitInnerClass(String name, String outerName, String innerName, int access)
             {
-                errors.add("class declared as inner class");
+                if (clsNameSl.equals(outerName)) // outerName might be null, which is true for anonymous inner classes
+                    errors.add("class declared as inner class");
                 super.visitInnerClass(name, outerName, innerName, access);
             }
         };
diff --git a/src/java/org/apache/cassandra/cql3/functions/UDFContext.java b/src/java/org/apache/cassandra/cql3/functions/UDFContext.java
new file mode 100644
index 0000000..4465aec
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/functions/UDFContext.java
@@ -0,0 +1,105 @@
+/*
+ * 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.functions;
+
+import com.datastax.driver.core.TupleValue;
+import com.datastax.driver.core.UDTValue;
+
+/**
+ * Provides context information for a particular user defined function.
+ * Java UDFs can access implementations of this interface using the
+ * {@code udfContext} field, scripted UDFs can get it using the {@code udfContext}
+ * binding.
+ */
+public interface UDFContext
+{
+    /**
+     * Creates a new {@code UDTValue} instance for an argument.
+     *
+     * @param argName name of the argument as declared in the {@code CREATE FUNCTION} statement
+     * @return a new {@code UDTValue} instance
+     * @throws IllegalArgumentException if no argument for the given name exists
+     * @throws IllegalStateException    if the argument is not a UDT
+     */
+    UDTValue newArgUDTValue(String argName);
+
+    /**
+     * Creates a new {@code UDTValue} instance for an argument.
+     *
+     * @param argNum zero-based index of the argument as declared in the {@code CREATE FUNCTION} statement
+     * @return a new {@code UDTValue} instance
+     * @throws ArrayIndexOutOfBoundsException if no argument for the given index exists
+     * @throws IllegalStateException          if the argument is not a UDT
+     */
+    UDTValue newArgUDTValue(int argNum);
+
+    /**
+     * Creates a new {@code UDTValue} instance for the return value.
+     *
+     * @return a new {@code UDTValue} instance
+     * @throws IllegalStateException          if the return type is not a UDT
+     */
+    UDTValue newReturnUDTValue();
+
+    /**
+     * Creates a new {@code UDTValue} instance by name in the same keyspace.
+     *
+     * @param udtName name of the user defined type in the same keyspace as the function
+     * @return a new {@code UDTValue} instance
+     * @throws IllegalArgumentException if no UDT for the given name exists
+     */
+    UDTValue newUDTValue(String udtName);
+
+    /**
+     * Creates a new {@code TupleValue} instance for an argument.
+     *
+     * @param argName name of the argument as declared in the {@code CREATE FUNCTION} statement
+     * @return a new {@code TupleValue} instance
+     * @throws IllegalArgumentException if no argument for the given name exists
+     * @throws IllegalStateException    if the argument is not a tuple
+     */
+    TupleValue newArgTupleValue(String argName);
+
+    /**
+     * Creates a new {@code TupleValue} instance for an argument.
+     *
+     * @param argNum zero-based index of the argument as declared in the {@code CREATE FUNCTION} statement
+     * @return a new {@code TupleValue} instance
+     * @throws ArrayIndexOutOfBoundsException if no argument for the given index exists
+     * @throws IllegalStateException          if the argument is not a tuple
+     */
+    TupleValue newArgTupleValue(int argNum);
+
+    /**
+     * Creates a new {@code TupleValue} instance for the return value.
+     *
+     * @return a new {@code TupleValue} instance
+     * @throws IllegalStateException          if the return type is not a tuple
+     */
+    TupleValue newReturnTupleValue();
+
+    /**
+     * Creates a new {@code TupleValue} instance for the CQL type definition.
+     *
+     * @param cqlDefinition CQL tuple type definition like {@code tuple<int, text, bigint>}
+     * @return a new {@code TupleValue} instance
+     * @throws IllegalStateException          if cqlDefinition type is not a tuple or an invalid type
+     */
+    TupleValue newTupleValue(String cqlDefinition);
+}
diff --git a/src/java/org/apache/cassandra/cql3/functions/UDFContextImpl.java b/src/java/org/apache/cassandra/cql3/functions/UDFContextImpl.java
new file mode 100644
index 0000000..00625cd
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/functions/UDFContextImpl.java
@@ -0,0 +1,146 @@
+/*
+ * 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.functions;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import com.datastax.driver.core.DataType;
+import com.datastax.driver.core.TupleType;
+import com.datastax.driver.core.TupleValue;
+import com.datastax.driver.core.TypeCodec;
+import com.datastax.driver.core.UDTValue;
+import com.datastax.driver.core.UserType;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.schema.CQLTypeParser;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+/**
+ * Package private implementation of {@link UDFContext}
+ */
+public final class UDFContextImpl implements UDFContext
+{
+    private final KeyspaceMetadata keyspaceMetadata;
+    private final Map<String, TypeCodec<Object>> byName = new HashMap<>();
+    private final TypeCodec<Object>[] argCodecs;
+    private final TypeCodec<Object> returnCodec;
+
+    UDFContextImpl(List<ColumnIdentifier> argNames, TypeCodec<Object>[] argCodecs, TypeCodec<Object> returnCodec,
+                   KeyspaceMetadata keyspaceMetadata)
+    {
+        for (int i = 0; i < argNames.size(); i++)
+            byName.put(argNames.get(i).toString(), argCodecs[i]);
+        this.argCodecs = argCodecs;
+        this.returnCodec = returnCodec;
+        this.keyspaceMetadata = keyspaceMetadata;
+    }
+
+    public UDTValue newArgUDTValue(String argName)
+    {
+        return newUDTValue(codecFor(argName));
+    }
+
+    public UDTValue newArgUDTValue(int argNum)
+    {
+        return newUDTValue(codecFor(argNum));
+    }
+
+    public UDTValue newReturnUDTValue()
+    {
+        return newUDTValue(returnCodec);
+    }
+
+    public UDTValue newUDTValue(String udtName)
+    {
+        Optional<org.apache.cassandra.db.marshal.UserType> udtType = keyspaceMetadata.types.get(ByteBufferUtil.bytes(udtName));
+        DataType dataType = UDHelper.driverType(udtType.orElseThrow(
+                () -> new IllegalArgumentException("No UDT named " + udtName + " in keyspace " + keyspaceMetadata.name)
+            ));
+        return newUDTValue(dataType);
+    }
+
+    public TupleValue newArgTupleValue(String argName)
+    {
+        return newTupleValue(codecFor(argName));
+    }
+
+    public TupleValue newArgTupleValue(int argNum)
+    {
+        return newTupleValue(codecFor(argNum));
+    }
+
+    public TupleValue newReturnTupleValue()
+    {
+        return newTupleValue(returnCodec);
+    }
+
+    public TupleValue newTupleValue(String cqlDefinition)
+    {
+        AbstractType<?> abstractType = CQLTypeParser.parse(keyspaceMetadata.name, cqlDefinition, keyspaceMetadata.types);
+        DataType dataType = UDHelper.driverType(abstractType);
+        return newTupleValue(dataType);
+    }
+
+    private TypeCodec<Object> codecFor(int argNum)
+    {
+        if (argNum < 0 || argNum >= argCodecs.length)
+            throw new IllegalArgumentException("Function does not declare an argument with index " + argNum);
+        return argCodecs[argNum];
+    }
+
+    private TypeCodec<Object> codecFor(String argName)
+    {
+        TypeCodec<Object> codec = byName.get(argName);
+        if (codec == null)
+            throw new IllegalArgumentException("Function does not declare an argument named '" + argName + '\'');
+        return codec;
+    }
+
+    private static UDTValue newUDTValue(TypeCodec<Object> codec)
+    {
+        DataType dataType = codec.getCqlType();
+        return newUDTValue(dataType);
+    }
+
+    private static UDTValue newUDTValue(DataType dataType)
+    {
+        if (!(dataType instanceof UserType))
+            throw new IllegalStateException("Function argument is not a UDT but a " + dataType.getName());
+        UserType userType = (UserType) dataType;
+        return userType.newValue();
+    }
+
+    private static TupleValue newTupleValue(TypeCodec<Object> codec)
+    {
+        DataType dataType = codec.getCqlType();
+        return newTupleValue(dataType);
+    }
+
+    private static TupleValue newTupleValue(DataType dataType)
+    {
+        if (!(dataType instanceof TupleType))
+            throw new IllegalStateException("Function argument is not a tuple type but a " + dataType.getName());
+        TupleType tupleType = (TupleType) dataType;
+        return tupleType.newValue();
+    }
+}
diff --git a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java
index e45d6b5..6dab536 100644
--- a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java
+++ b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java
@@ -28,6 +28,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -54,6 +55,7 @@
 import org.apache.cassandra.service.ClientWarn;
 import org.apache.cassandra.service.MigrationManager;
 import org.apache.cassandra.tracing.Tracing;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 
@@ -75,6 +77,8 @@
     protected final TypeCodec<Object> returnCodec;
     protected final boolean calledOnNullInput;
 
+    protected final UDFContext udfContext;
+
     //
     // Access to classes is controlled via allow and disallow lists.
     //
@@ -108,7 +112,9 @@
     "java/time/",
     "java/util/",
     "org/apache/cassandra/cql3/functions/JavaUDF.class",
+    "org/apache/cassandra/cql3/functions/UDFContext.class",
     "org/apache/cassandra/exceptions/",
+    "org/apache/cassandra/transport/ProtocolVersion.class"
     };
     // Only need to disallow a pattern, if it would otherwise be allowed via allowedPatterns
     private static final String[] disallowedPatterns =
@@ -223,6 +229,9 @@
         this.argCodecs = UDHelper.codecsFor(argDataTypes);
         this.returnCodec = UDHelper.codecFor(returnDataType);
         this.calledOnNullInput = calledOnNullInput;
+        KeyspaceMetadata keyspaceMetadata = Schema.instance.getKSMetaData(name.keyspace);
+        this.udfContext = new UDFContextImpl(argNames, argCodecs, returnCodec,
+                                             keyspaceMetadata);
     }
 
     public static UDFunction create(FunctionName name,
@@ -269,17 +278,27 @@
                 return Executors.newSingleThreadExecutor();
             }
 
-            public ByteBuffer executeUserDefined(int protocolVersion, List<ByteBuffer> parameters)
+            protected Object executeAggregateUserDefined(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> parameters)
             {
-                throw new InvalidRequestException(String.format("Function '%s' exists but hasn't been loaded successfully "
-                                                                + "for the following reason: %s. Please see the server log for details",
-                                                                this,
-                                                                reason.getMessage()));
+                throw broken();
+            }
+
+            public ByteBuffer executeUserDefined(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
+            {
+                throw broken();
+            }
+
+            private InvalidRequestException broken()
+            {
+                return new InvalidRequestException(String.format("Function '%s' exists but hasn't been loaded successfully "
+                                                                 + "for the following reason: %s. Please see the server log for details",
+                                                                 this,
+                                                                 reason.getMessage()));
             }
         };
     }
 
-    public final ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+    public final ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
     {
         assertUdfsEnabled(language);
 
@@ -312,6 +331,44 @@
         }
     }
 
+    /**
+     * Like {@link ScalarFunction#execute(ProtocolVersion, List)} but the first parameter is already in non-serialized form.
+     * Remaining parameters (2nd paramters and all others) are in {@code parameters}.
+     * This is used to prevent superfluous (de)serialization of the state of aggregates.
+     * Means: scalar functions of aggregates are called using this variant.
+     */
+    public final Object executeForAggregate(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> parameters)
+    {
+        assertUdfsEnabled(language);
+
+        if (!calledOnNullInput && firstParam == null || !isCallableWrtNullable(parameters))
+            return null;
+
+        long tStart = System.nanoTime();
+        parameters = makeEmptyParametersNull(parameters);
+
+        try
+        {
+            // Using async UDF execution is expensive (adds about 100us overhead per invocation on a Core-i7 MBPr).
+            Object result = DatabaseDescriptor.enableUserDefinedFunctionsThreads()
+                                ? executeAggregateAsync(protocolVersion, firstParam, parameters)
+                                : executeAggregateUserDefined(protocolVersion, firstParam, parameters);
+            Tracing.trace("Executed UDF {} in {}\u03bcs", name(), (System.nanoTime() - tStart) / 1000);
+            return result;
+        }
+        catch (InvalidRequestException e)
+        {
+            throw e;
+        }
+        catch (Throwable t)
+        {
+            logger.debug("Invocation of user-defined function '{}' failed", this, t);
+            if (t instanceof VirtualMachineError)
+                throw (VirtualMachineError) t;
+            throw FunctionExecutionException.create(this, t);
+        }
+    }
+
     public static void assertUdfsEnabled(String language)
     {
         if (!DatabaseDescriptor.enableUserDefinedFunctions())
@@ -351,14 +408,35 @@
         }
     }
 
-    private ByteBuffer executeAsync(int protocolVersion, List<ByteBuffer> parameters)
+    private ByteBuffer executeAsync(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
     {
         ThreadIdAndCpuTime threadIdAndCpuTime = new ThreadIdAndCpuTime();
 
-        Future<ByteBuffer> future = executor().submit(() -> {
+        return async(threadIdAndCpuTime, () -> {
             threadIdAndCpuTime.setup();
             return executeUserDefined(protocolVersion, parameters);
         });
+    }
+
+    /**
+     * Like {@link #executeAsync(int, List)} but the first parameter is already in non-serialized form.
+     * Remaining parameters (2nd paramters and all others) are in {@code parameters}.
+     * This is used to prevent superfluous (de)serialization of the state of aggregates.
+     * Means: scalar functions of aggregates are called using this variant.
+     */
+    private Object executeAggregateAsync(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> parameters)
+    {
+        ThreadIdAndCpuTime threadIdAndCpuTime = new ThreadIdAndCpuTime();
+
+        return async(threadIdAndCpuTime, () -> {
+            threadIdAndCpuTime.setup();
+            return executeAggregateUserDefined(protocolVersion, firstParam, parameters);
+        });
+    }
+
+    private <T> T async(ThreadIdAndCpuTime threadIdAndCpuTime, Callable<T> callable)
+    {
+        Future<T> future = executor().submit(callable);
 
         try
         {
@@ -454,7 +532,9 @@
         return true;
     }
 
-    protected abstract ByteBuffer executeUserDefined(int protocolVersion, List<ByteBuffer> parameters);
+    protected abstract ByteBuffer executeUserDefined(ProtocolVersion protocolVersion, List<ByteBuffer> parameters);
+
+    protected abstract Object executeAggregateUserDefined(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> parameters);
 
     public boolean isAggregate()
     {
@@ -494,12 +574,12 @@
      * @param protocolVersion the native protocol version used for serialization
      * @param argIndex        index of the UDF input argument
      */
-    protected Object compose(int protocolVersion, int argIndex, ByteBuffer value)
+    protected Object compose(ProtocolVersion protocolVersion, int argIndex, ByteBuffer value)
     {
         return compose(argCodecs, protocolVersion, argIndex, value);
     }
 
-    protected static Object compose(TypeCodec<Object>[] codecs, int protocolVersion, int argIndex, ByteBuffer value)
+    protected static Object compose(TypeCodec<Object>[] codecs, ProtocolVersion protocolVersion, int argIndex, ByteBuffer value)
     {
         return value == null ? null : UDHelper.deserialize(codecs[argIndex], protocolVersion, value);
     }
@@ -511,12 +591,12 @@
      *
      * @param protocolVersion the native protocol version used for serialization
      */
-    protected ByteBuffer decompose(int protocolVersion, Object value)
+    protected ByteBuffer decompose(ProtocolVersion protocolVersion, Object value)
     {
         return decompose(returnCodec, protocolVersion, value);
     }
 
-    protected static ByteBuffer decompose(TypeCodec<Object> codec, int protocolVersion, Object value)
+    protected static ByteBuffer decompose(TypeCodec<Object> codec, ProtocolVersion protocolVersion, Object value)
     {
         return value == null ? null : UDHelper.serialize(codec, protocolVersion, value);
     }
diff --git a/src/java/org/apache/cassandra/cql3/functions/UDHelper.java b/src/java/org/apache/cassandra/cql3/functions/UDHelper.java
index 45c734f..ddeae41 100644
--- a/src/java/org/apache/cassandra/cql3/functions/UDHelper.java
+++ b/src/java/org/apache/cassandra/cql3/functions/UDHelper.java
@@ -23,17 +23,18 @@
 import java.nio.ByteBuffer;
 import java.util.List;
 
+import com.google.common.reflect.TypeToken;
+
 import com.datastax.driver.core.CodecRegistry;
 import com.datastax.driver.core.DataType;
-import com.datastax.driver.core.ProtocolVersion;
 import com.datastax.driver.core.TypeCodec;
 import com.datastax.driver.core.exceptions.InvalidTypeException;
 import org.apache.cassandra.cql3.CQL3Type;
 import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
- * Helper class for User Defined Functions + Aggregates.
+ * Helper class for User Defined Functions, Types and Aggregates.
  */
 public final class UDHelper
 {
@@ -45,7 +46,7 @@
         try
         {
             Class<?> cls = Class.forName("com.datastax.driver.core.DataTypeClassNameParser");
-            Method m = cls.getDeclaredMethod("parseOne", String.class, ProtocolVersion.class, CodecRegistry.class);
+            Method m = cls.getDeclaredMethod("parseOne", String.class, com.datastax.driver.core.ProtocolVersion.class, CodecRegistry.class);
             m.setAccessible(true);
             methodParseOne = MethodHandles.lookup().unreflect(m);
             codecRegistry = new CodecRegistry();
@@ -64,7 +65,7 @@
         return codecs;
     }
 
-    static TypeCodec<Object> codecFor(DataType dataType)
+    public static TypeCodec<Object> codecFor(DataType dataType)
     {
         return codecRegistry.codecFor(dataType);
     }
@@ -76,31 +77,32 @@
      * @param calledOnNullInput whether to allow {@code null} as an argument value
      * @return array of same size with UDF arguments
      */
-    public static Class<?>[] javaTypes(TypeCodec<Object>[] dataTypes, boolean calledOnNullInput)
+    public static TypeToken<?>[] typeTokens(TypeCodec<Object>[] dataTypes, boolean calledOnNullInput)
     {
-        Class<?>[] paramTypes = new Class[dataTypes.length];
+        TypeToken<?>[] paramTypes = new TypeToken[dataTypes.length];
         for (int i = 0; i < paramTypes.length; i++)
         {
-            Class<?> clazz = asJavaClass(dataTypes[i]);
+            TypeToken<?> typeToken = dataTypes[i].getJavaType();
             if (!calledOnNullInput)
             {
                 // only care about classes that can be used in a data type
+                Class<?> clazz = typeToken.getRawType();
                 if (clazz == Integer.class)
-                    clazz = int.class;
+                    typeToken = TypeToken.of(int.class);
                 else if (clazz == Long.class)
-                    clazz = long.class;
+                    typeToken = TypeToken.of(long.class);
                 else if (clazz == Byte.class)
-                    clazz = byte.class;
+                    typeToken = TypeToken.of(byte.class);
                 else if (clazz == Short.class)
-                    clazz = short.class;
+                    typeToken = TypeToken.of(short.class);
                 else if (clazz == Float.class)
-                    clazz = float.class;
+                    typeToken = TypeToken.of(float.class);
                 else if (clazz == Double.class)
-                    clazz = double.class;
+                    typeToken = TypeToken.of(double.class);
                 else if (clazz == Boolean.class)
-                    clazz = boolean.class;
+                    typeToken = TypeToken.of(boolean.class);
             }
-            paramTypes[i] = clazz;
+            paramTypes[i] = typeToken;
         }
         return paramTypes;
     }
@@ -126,10 +128,16 @@
     public static DataType driverType(AbstractType abstractType)
     {
         CQL3Type cqlType = abstractType.asCQL3Type();
+        String abstractTypeDef = cqlType.getType().toString();
+        return driverTypeFromAbstractType(abstractTypeDef);
+    }
+
+    public static DataType driverTypeFromAbstractType(String abstractTypeDef)
+    {
         try
         {
-            return (DataType) methodParseOne.invoke(cqlType.getType().toString(),
-                                                    ProtocolVersion.fromInt(Server.CURRENT_VERSION),
+            return (DataType) methodParseOne.invoke(abstractTypeDef,
+                                                    com.datastax.driver.core.ProtocolVersion.fromInt(ProtocolVersion.CURRENT.asInt()),
                                                     codecRegistry);
         }
         catch (RuntimeException | Error e)
@@ -139,21 +147,21 @@
         }
         catch (Throwable e)
         {
-            throw new RuntimeException("cannot parse driver type " + cqlType.getType().toString(), e);
+            throw new RuntimeException("cannot parse driver type " + abstractTypeDef, e);
         }
     }
 
-    public static Object deserialize(TypeCodec<?> codec, int protocolVersion, ByteBuffer value)
+    public static Object deserialize(TypeCodec<?> codec, ProtocolVersion protocolVersion, ByteBuffer value)
     {
-        return codec.deserialize(value, ProtocolVersion.fromInt(protocolVersion));
+        return codec.deserialize(value, com.datastax.driver.core.ProtocolVersion.fromInt(protocolVersion.asInt()));
     }
 
-    public static ByteBuffer serialize(TypeCodec<?> codec, int protocolVersion, Object value)
+    public static ByteBuffer serialize(TypeCodec<?> codec, ProtocolVersion protocolVersion, Object value)
     {
         if (!codec.getJavaType().getRawType().isAssignableFrom(value.getClass()))
             throw new InvalidTypeException("Invalid value for CQL type " + codec.getCqlType().getName().toString());
 
-        return ((TypeCodec)codec).serialize(value, ProtocolVersion.fromInt(protocolVersion));
+        return ((TypeCodec)codec).serialize(value, com.datastax.driver.core.ProtocolVersion.fromInt(protocolVersion.asInt()));
     }
 
     public static Class<?> asJavaClass(TypeCodec<?> codec)
diff --git a/src/java/org/apache/cassandra/cql3/functions/UuidFcts.java b/src/java/org/apache/cassandra/cql3/functions/UuidFcts.java
index 32adbdc..3d82ece 100644
--- a/src/java/org/apache/cassandra/cql3/functions/UuidFcts.java
+++ b/src/java/org/apache/cassandra/cql3/functions/UuidFcts.java
@@ -22,6 +22,7 @@
 
 import org.apache.cassandra.db.marshal.UUIDType;
 import org.apache.cassandra.serializers.UUIDSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 public abstract class UuidFcts
 {
@@ -32,7 +33,7 @@
 
     public static final Function uuidFct = new NativeScalarFunction("uuid", UUIDType.instance)
     {
-        public ByteBuffer execute(int protocolVersion, List<ByteBuffer> parameters)
+        public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
         {
             return UUIDSerializer.instance.serialize(UUID.randomUUID());
         }
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/AbstractPrimaryKeyRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/AbstractPrimaryKeyRestrictions.java
deleted file mode 100644
index f1b5a50..0000000
--- a/src/java/org/apache/cassandra/cql3/restrictions/AbstractPrimaryKeyRestrictions.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.restrictions;
-
-import java.nio.ByteBuffer;
-import java.util.*;
-
-import org.apache.cassandra.cql3.QueryOptions;
-import org.apache.cassandra.cql3.statements.Bound;
-import org.apache.cassandra.db.ClusteringPrefix;
-import org.apache.cassandra.db.ClusteringComparator;
-import org.apache.cassandra.exceptions.InvalidRequestException;
-
-/**
- * Base class for <code>PrimaryKeyRestrictions</code>.
- */
-abstract class AbstractPrimaryKeyRestrictions extends AbstractRestriction implements PrimaryKeyRestrictions
-{
-    /**
-     * The composite type.
-     */
-    protected final ClusteringComparator comparator;
-
-    public AbstractPrimaryKeyRestrictions(ClusteringComparator comparator)
-    {
-        this.comparator = comparator;
-    }
-
-    @Override
-    public List<ByteBuffer> bounds(Bound b, QueryOptions options) throws InvalidRequestException
-    {
-        return values(options);
-    }
-
-    @Override
-    public final boolean isEmpty()
-    {
-        return getColumnDefs().isEmpty();
-    }
-
-    @Override
-    public final int size()
-    {
-        return getColumnDefs().size();
-    }
-}
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/AbstractRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/AbstractRestriction.java
deleted file mode 100644
index df04331..0000000
--- a/src/java/org/apache/cassandra/cql3/restrictions/AbstractRestriction.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * 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.restrictions;
-
-import org.apache.cassandra.cql3.QueryOptions;
-import org.apache.cassandra.cql3.statements.Bound;
-import org.apache.cassandra.db.MultiCBuilder;
-
-import org.apache.cassandra.config.ColumnDefinition;
-
-/**
- * Base class for <code>Restriction</code>s
- */
-abstract class AbstractRestriction  implements Restriction
-{
-    @Override
-    public  boolean isOnToken()
-    {
-        return false;
-    }
-
-    @Override
-    public boolean isMultiColumn()
-    {
-        return false;
-    }
-
-    @Override
-    public boolean isSlice()
-    {
-        return false;
-    }
-
-    @Override
-    public boolean isEQ()
-    {
-        return false;
-    }
-
-    @Override
-    public boolean isIN()
-    {
-        return false;
-    }
-
-    @Override
-    public boolean isContains()
-    {
-        return false;
-    }
-
-    @Override
-    public boolean isNotNull()
-    {
-        return false;
-    }
-
-    @Override
-    public boolean hasBound(Bound b)
-    {
-        return true;
-    }
-
-    @Override
-    public MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options)
-    {
-        return appendTo(builder, options);
-    }
-
-    @Override
-    public boolean isInclusive(Bound b)
-    {
-        return true;
-    }
-
-    /**
-     * Reverses the specified bound if the column type is a reversed one.
-     *
-     * @param columnDefinition the column definition
-     * @param bound the bound
-     * @return the bound reversed if the column type was a reversed one or the original bound
-     */
-    protected static Bound reverseBoundIfNeeded(ColumnDefinition columnDefinition, Bound bound)
-    {
-        return columnDefinition.isReversedType() ? bound.reverse() : bound;
-    }
-}
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictions.java
new file mode 100644
index 0000000..a8cc6bd
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictions.java
@@ -0,0 +1,230 @@
+/*
+ * 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.restrictions;
+
+import java.util.*;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.statements.Bound;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.index.SecondaryIndexManager;
+import org.apache.cassandra.utils.btree.BTreeSet;
+
+import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
+import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
+
+/**
+ * A set of restrictions on the clustering key.
+ */
+final class ClusteringColumnRestrictions extends RestrictionSetWrapper
+{
+    /**
+     * The composite type.
+     */
+    protected final ClusteringComparator comparator;
+
+    /**
+     * <code>true</code> if filtering is allowed for this restriction, <code>false</code> otherwise
+     */
+    private final boolean allowFiltering;
+
+    public ClusteringColumnRestrictions(CFMetaData cfm)
+    {
+        this(cfm, false);
+    }
+
+    public ClusteringColumnRestrictions(CFMetaData cfm, boolean allowFiltering)
+    {
+        this(cfm.comparator, new RestrictionSet(), allowFiltering);
+    }
+
+    private ClusteringColumnRestrictions(ClusteringComparator comparator,
+                                         RestrictionSet restrictionSet,
+                                         boolean allowFiltering)
+    {
+        super(restrictionSet);
+        this.comparator = comparator;
+        this.allowFiltering = allowFiltering;
+    }
+
+    public ClusteringColumnRestrictions mergeWith(Restriction restriction) throws InvalidRequestException
+    {
+        SingleRestriction newRestriction = (SingleRestriction) restriction;
+        RestrictionSet newRestrictionSet = restrictions.addRestriction(newRestriction);
+
+        if (!isEmpty() && !allowFiltering)
+        {
+            SingleRestriction lastRestriction = restrictions.lastRestriction();
+            ColumnDefinition lastRestrictionStart = lastRestriction.getFirstColumn();
+            ColumnDefinition newRestrictionStart = restriction.getFirstColumn();
+
+            checkFalse(lastRestriction.isSlice() && newRestrictionStart.position() > lastRestrictionStart.position(),
+                       "Clustering column \"%s\" cannot be restricted (preceding column \"%s\" is restricted by a non-EQ relation)",
+                       newRestrictionStart.name,
+                       lastRestrictionStart.name);
+
+            if (newRestrictionStart.position() < lastRestrictionStart.position() && newRestriction.isSlice())
+                throw invalidRequest("PRIMARY KEY column \"%s\" cannot be restricted (preceding column \"%s\" is restricted by a non-EQ relation)",
+                                     restrictions.nextColumn(newRestrictionStart).name,
+                                     newRestrictionStart.name);
+        }
+
+        return new ClusteringColumnRestrictions(this.comparator, newRestrictionSet, allowFiltering);
+    }
+
+    private boolean hasMultiColumnSlice()
+    {
+        for (SingleRestriction restriction : restrictions)
+        {
+            if (restriction.isMultiColumn() && restriction.isSlice())
+                return true;
+        }
+        return false;
+    }
+
+    public NavigableSet<Clustering> valuesAsClustering(QueryOptions options) throws InvalidRequestException
+    {
+        MultiCBuilder builder = MultiCBuilder.create(comparator, hasIN());
+        for (SingleRestriction r : restrictions)
+        {
+            r.appendTo(builder, options);
+            if (builder.hasMissingElements())
+                break;
+        }
+        return builder.build();
+    }
+
+    public NavigableSet<ClusteringBound> boundsAsClustering(Bound bound, QueryOptions options) throws InvalidRequestException
+    {
+        MultiCBuilder builder = MultiCBuilder.create(comparator, hasIN() || hasMultiColumnSlice());
+        int keyPosition = 0;
+
+        for (SingleRestriction r : restrictions)
+        {
+            if (handleInFilter(r, keyPosition))
+                break;
+
+            if (r.isSlice())
+            {
+                r.appendBoundTo(builder, bound, options);
+                return builder.buildBoundForSlice(bound.isStart(),
+                                                  r.isInclusive(bound),
+                                                  r.isInclusive(bound.reverse()),
+                                                  r.getColumnDefs());
+            }
+
+            r.appendBoundTo(builder, bound, options);
+
+            if (builder.hasMissingElements())
+                return BTreeSet.empty(comparator);
+
+            keyPosition = r.getLastColumn().position() + 1;
+        }
+
+        // Everything was an equal (or there was nothing)
+        return builder.buildBound(bound.isStart(), true);
+    }
+
+    /**
+     * Checks if any of the underlying restriction is a CONTAINS or CONTAINS KEY.
+     *
+     * @return <code>true</code> if any of the underlying restriction is a CONTAINS or CONTAINS KEY,
+     * <code>false</code> otherwise
+     */
+    public final boolean hasContains()
+    {
+        for (SingleRestriction restriction : restrictions)
+        {
+            if (restriction.isContains())
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * Checks if any of the underlying restriction is a slice restrictions.
+     *
+     * @return <code>true</code> if any of the underlying restriction is a slice restrictions,
+     * <code>false</code> otherwise
+     */
+    public final boolean hasSlice()
+    {
+        for (SingleRestriction restriction : restrictions)
+        {
+            if (restriction.isSlice())
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * Checks if underlying restrictions would require filtering
+     *
+     * @return <code>true</code> if any underlying restrictions require filtering, <code>false</code>
+     * otherwise
+     */
+    public final boolean needFiltering()
+    {
+        int position = 0;
+
+        for (SingleRestriction restriction : restrictions)
+        {
+            if (handleInFilter(restriction, position))
+                return true;
+
+            if (!restriction.isSlice())
+                position = restriction.getLastColumn().position() + 1;
+        }
+        return hasContains();
+    }
+
+    @Override
+    public void addRowFilterTo(RowFilter filter,
+                               SecondaryIndexManager indexManager,
+                               QueryOptions options) throws InvalidRequestException
+    {
+        int position = 0;
+
+        for (SingleRestriction restriction : restrictions)
+        {
+            // We ignore all the clustering columns that can be handled by slices.
+            if (handleInFilter(restriction, position) || restriction.hasSupportingIndex(indexManager))
+            {
+                restriction.addRowFilterTo(filter, indexManager, options);
+                continue;
+            }
+
+            if (!restriction.isSlice())
+                position = restriction.getLastColumn().position() + 1;
+        }
+    }
+
+    private boolean handleInFilter(SingleRestriction restriction, int index)
+    {
+        return restriction.isContains() || restriction.isLIKE() || index != restriction.getFirstColumn().position();
+    }
+
+    public Iterator<SingleRestriction> iterator()
+    {
+        return restrictions.iterator();
+    }
+}
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/ForwardingPrimaryKeyRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/ForwardingPrimaryKeyRestrictions.java
deleted file mode 100644
index 71305b9..0000000
--- a/src/java/org/apache/cassandra/cql3/restrictions/ForwardingPrimaryKeyRestrictions.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * 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.restrictions;
-
-import java.nio.ByteBuffer;
-import java.util.List;
-import java.util.NavigableSet;
-
-import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.cql3.QueryOptions;
-import org.apache.cassandra.cql3.functions.Function;
-import org.apache.cassandra.cql3.statements.Bound;
-import org.apache.cassandra.db.Clustering;
-import org.apache.cassandra.db.MultiCBuilder;
-import org.apache.cassandra.db.Slice;
-import org.apache.cassandra.db.filter.RowFilter;
-import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.index.SecondaryIndexManager;
-
-/**
- * A <code>PrimaryKeyRestrictions</code> which forwards all its method calls to another 
- * <code>PrimaryKeyRestrictions</code>. Subclasses should override one or more methods to modify the behavior 
- * of the backing <code>PrimaryKeyRestrictions</code> as desired per the decorator pattern. 
- */
-abstract class ForwardingPrimaryKeyRestrictions implements PrimaryKeyRestrictions
-{
-    /**
-     * Returns the backing delegate instance that methods are forwarded to.
-     * @return the backing delegate instance that methods are forwarded to.
-     */
-    protected abstract PrimaryKeyRestrictions getDelegate();
-
-    @Override
-    public void addFunctionsTo(List<Function> functions)
-    {
-        getDelegate().addFunctionsTo(functions);
-    }
-
-    @Override
-    public List<ColumnDefinition> getColumnDefs()
-    {
-        return getDelegate().getColumnDefs();
-    }
-
-    @Override
-    public ColumnDefinition getFirstColumn()
-    {
-        return getDelegate().getFirstColumn();
-    }
-
-    @Override
-    public ColumnDefinition getLastColumn()
-    {
-        return getDelegate().getLastColumn();
-    }
-
-    @Override
-    public PrimaryKeyRestrictions mergeWith(Restriction restriction) throws InvalidRequestException
-    {
-        return getDelegate().mergeWith(restriction);
-    }
-
-    @Override
-    public boolean hasSupportingIndex(SecondaryIndexManager secondaryIndexManager)
-    {
-        return getDelegate().hasSupportingIndex(secondaryIndexManager);
-    }
-
-    @Override
-    public List<ByteBuffer> values(QueryOptions options) throws InvalidRequestException
-    {
-        return getDelegate().values(options);
-    }
-
-    @Override
-    public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options)
-    {
-        return getDelegate().appendTo(builder, options);
-    }
-
-    @Override
-    public NavigableSet<Clustering> valuesAsClustering(QueryOptions options) throws InvalidRequestException
-    {
-        return getDelegate().valuesAsClustering(options);
-    }
-
-    @Override
-    public List<ByteBuffer> bounds(Bound bound, QueryOptions options) throws InvalidRequestException
-    {
-        return getDelegate().bounds(bound, options);
-    }
-
-    @Override
-    public NavigableSet<Slice.Bound> boundsAsClustering(Bound bound, QueryOptions options) throws InvalidRequestException
-    {
-        return getDelegate().boundsAsClustering(bound, options);
-    }
-
-    @Override
-    public MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options)
-    {
-        return getDelegate().appendBoundTo(builder, bound, options);
-    }
-
-    @Override
-    public boolean isInclusive(Bound bound)
-    {
-        return getDelegate().isInclusive(bound.reverse());
-    }
-
-    @Override
-    public boolean isEmpty()
-    {
-        return getDelegate().isEmpty();
-    }
-
-    @Override
-    public int size()
-    {
-        return getDelegate().size();
-    }
-
-    @Override
-    public boolean isOnToken()
-    {
-        return getDelegate().isOnToken();
-    }
-
-    @Override
-    public boolean isSlice()
-    {
-        return getDelegate().isSlice();
-    }
-
-    @Override
-    public boolean isEQ()
-    {
-        return getDelegate().isEQ();
-    }
-
-    @Override
-    public boolean isIN()
-    {
-        return getDelegate().isIN();
-    }
-
-    @Override
-    public boolean isContains()
-    {
-        return getDelegate().isContains();
-    }
-
-    @Override
-    public boolean isNotNull()
-    {
-        return getDelegate().isNotNull();
-    }
-
-    @Override
-    public boolean isMultiColumn()
-    {
-        return getDelegate().isMultiColumn();
-    }
-
-    @Override
-    public boolean hasBound(Bound b)
-    {
-        return getDelegate().hasBound(b);
-    }
-
-    @Override
-    public void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexManager, QueryOptions options) throws InvalidRequestException
-    {
-        getDelegate().addRowFilterTo(filter, indexManager, options);
-    }
-}
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java
index 9d33bb1..b0cbdff 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java
@@ -27,7 +27,6 @@
 import org.apache.cassandra.cql3.statements.Bound;
 import org.apache.cassandra.db.MultiCBuilder;
 import org.apache.cassandra.db.filter.RowFilter;
-import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.index.Index;
 import org.apache.cassandra.index.SecondaryIndexManager;
 
@@ -36,7 +35,7 @@
 import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue;
 import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
 
-public abstract class MultiColumnRestriction extends AbstractRestriction
+public abstract class MultiColumnRestriction implements SingleRestriction
 {
     /**
      * The columns to which the restriction apply.
@@ -73,7 +72,7 @@
     }
 
     @Override
-    public final Restriction mergeWith(Restriction otherRestriction) throws InvalidRequestException
+    public final SingleRestriction mergeWith(SingleRestriction otherRestriction)
     {
         // We want to allow query like: (b,c) > (?, ?) AND b < ?
         if (!otherRestriction.isMultiColumn()
@@ -85,7 +84,7 @@
         return doMergeWith(otherRestriction);
     }
 
-    protected abstract Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException;
+    protected abstract SingleRestriction doMergeWith(SingleRestriction otherRestriction);
 
     /**
      * Returns the names of the columns that are specified within this <code>Restrictions</code> and the other one
@@ -150,7 +149,7 @@
         }
 
         @Override
-        public Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
+        public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
         {
             throw invalidRequest("%s cannot be restricted by more than one relation if it includes an Equal",
                                  getColumnsInCommons(otherRestriction));
@@ -179,7 +178,7 @@
         }
 
         @Override
-        public final void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexMananger, QueryOptions options) throws InvalidRequestException
+        public final void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexMananger, QueryOptions options)
         {
             Tuples.Value t = ((Tuples.Value) value.bind(options));
             List<ByteBuffer> values = t.getElements();
@@ -220,7 +219,7 @@
         }
 
         @Override
-        public Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
+        public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
         {
             throw invalidRequest("%s cannot be restricted by more than one relation if it includes a IN",
                                  getColumnsInCommons(otherRestriction));
@@ -238,20 +237,12 @@
         @Override
         public final void addRowFilterTo(RowFilter filter,
                                          SecondaryIndexManager indexManager,
-                                         QueryOptions options) throws InvalidRequestException
+                                         QueryOptions options)
         {
-            List<List<ByteBuffer>> splitInValues = splitValues(options);
-            checkTrue(splitInValues.size() == 1, "IN restrictions are not supported on indexed columns");
-            List<ByteBuffer> values = splitInValues.get(0);
-
-            for (int i = 0, m = columnDefs.size(); i < m; i++)
-            {
-                ColumnDefinition columnDef = columnDefs.get(i);
-                filter.add(columnDef, Operator.EQ, values.get(i));
-            }
+            throw  invalidRequest("IN restrictions are not supported on indexed columns");
         }
 
-        protected abstract List<List<ByteBuffer>> splitValues(QueryOptions options) throws InvalidRequestException;
+        protected abstract List<List<ByteBuffer>> splitValues(QueryOptions options);
     }
 
     /**
@@ -281,7 +272,7 @@
         }
 
         @Override
-        protected List<List<ByteBuffer>> splitValues(QueryOptions options) throws InvalidRequestException
+        protected List<List<ByteBuffer>> splitValues(QueryOptions options)
         {
             List<List<ByteBuffer>> buffers = new ArrayList<>(values.size());
             for (Term value : values)
@@ -319,7 +310,7 @@
         }
 
         @Override
-        protected List<List<ByteBuffer>> splitValues(QueryOptions options) throws InvalidRequestException
+        protected List<List<ByteBuffer>> splitValues(QueryOptions options)
         {
             Tuples.InMarker inMarker = (Tuples.InMarker) marker;
             Tuples.InValue inValue = inMarker.bind(options);
@@ -370,7 +361,7 @@
             for (int i = 0, m = columnDefs.size(); i < m; i++)
             {
                 ColumnDefinition column = columnDefs.get(i);
-                Bound b = reverseBoundIfNeeded(column, bound);
+                Bound b = bound.reverseIfNeeded(column);
 
                 // For mixed order columns, we need to create additional slices when 2 columns are in reverse order
                 if (reversed != column.isReversedType())
@@ -444,7 +435,7 @@
         }
 
         @Override
-        public Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
+        public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
         {
             checkTrue(otherRestriction.isSlice(),
                       "Column \"%s\" cannot be restricted by both an equality and an inequality relation",
@@ -475,7 +466,7 @@
         @Override
         public final void addRowFilterTo(RowFilter filter,
                                          SecondaryIndexManager indexManager,
-                                         QueryOptions options) throws InvalidRequestException
+                                         QueryOptions options)
         {
             throw invalidRequest("Multi-column slice restrictions cannot be used for filtering.");
         }
@@ -492,9 +483,8 @@
          * @param b the bound type
          * @param options the query options
          * @return one ByteBuffer per-component in the bound
-         * @throws InvalidRequestException if the components cannot be retrieved
          */
-        private List<ByteBuffer> componentBounds(Bound b, QueryOptions options) throws InvalidRequestException
+        private List<ByteBuffer> componentBounds(Bound b, QueryOptions options)
         {
             if (!slice.hasBound(b))
                 return Collections.emptyList();
@@ -541,7 +531,7 @@
         }
 
         @Override
-        public Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
+        public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
         {
             throw invalidRequest("%s cannot be restricted by a relation if it includes an IS NOT NULL clause",
                                  getColumnsInCommons(otherRestriction));
@@ -563,7 +553,7 @@
         }
 
         @Override
-        public final void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexMananger, QueryOptions options) throws InvalidRequestException
+        public final void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexMananger, QueryOptions options)
         {
             throw new UnsupportedOperationException("Secondary indexes do not support IS NOT NULL restrictions");
         }
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeyRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeyRestrictions.java
new file mode 100644
index 0000000..1ff45d0
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeyRestrictions.java
@@ -0,0 +1,68 @@
+/*
+ * 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.restrictions;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.statements.Bound;
+
+/**
+ * A set of restrictions on the partition key.
+ *
+ */
+interface PartitionKeyRestrictions extends Restrictions
+{
+    public PartitionKeyRestrictions mergeWith(Restriction restriction);
+
+    public List<ByteBuffer> values(QueryOptions options);
+
+    public List<ByteBuffer> bounds(Bound b, QueryOptions options);
+
+    /**
+     * Checks if the specified bound is set or not.
+     * @param b the bound type
+     * @return <code>true</code> if the specified bound is set, <code>false</code> otherwise
+     */
+    public boolean hasBound(Bound b);
+
+    /**
+     * Checks if the specified bound is inclusive or not.
+     * @param b the bound type
+     * @return <code>true</code> if the specified bound is inclusive, <code>false</code> otherwise
+     */
+    public boolean isInclusive(Bound b);
+
+    /**
+     * checks if specified restrictions require filtering
+     *
+     * @param cfm column family metadata
+     * @return <code>true</code> if filtering is required, <code>false</code> otherwise
+     */
+    public boolean needFiltering(CFMetaData cfm);
+
+    /**
+     * Checks if the partition key has unrestricted components.
+     *
+     * @param cfm column family metadata
+     * @return <code>true</code> if the partition key has unrestricted components, <code>false</code> otherwise.
+     */
+    public boolean hasUnrestrictedPartitionKeyComponents(CFMetaData cfm);
+}
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeySingleRestrictionSet.java b/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeySingleRestrictionSet.java
new file mode 100644
index 0000000..bd561a4
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeySingleRestrictionSet.java
@@ -0,0 +1,154 @@
+/*
+ * 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.restrictions;
+
+import java.nio.ByteBuffer;
+import java.util.*;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.statements.Bound;
+import org.apache.cassandra.db.ClusteringComparator;
+import org.apache.cassandra.db.ClusteringPrefix;
+import org.apache.cassandra.db.MultiCBuilder;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.index.SecondaryIndexManager;
+
+/**
+ * A set of single restrictions on the partition key.
+ * <p>This class can only contains <code>SingleRestriction</code> instances. Token restrictions will be handled by
+ * <code>TokenRestriction</code> class or by the <code>TokenFilter</code> class if the query contains a mix of token
+ * restrictions and single column restrictions on the partition key.
+ */
+final class PartitionKeySingleRestrictionSet extends RestrictionSetWrapper implements PartitionKeyRestrictions
+{
+    /**
+     * The composite type.
+     */
+    protected final ClusteringComparator comparator;
+
+    public PartitionKeySingleRestrictionSet(ClusteringComparator comparator)
+    {
+        super(new RestrictionSet());
+        this.comparator = comparator;
+    }
+
+    private PartitionKeySingleRestrictionSet(PartitionKeySingleRestrictionSet restrictionSet,
+                                       SingleRestriction restriction)
+    {
+        super(restrictionSet.restrictions.addRestriction(restriction));
+        this.comparator = restrictionSet.comparator;
+    }
+
+    private List<ByteBuffer> toByteBuffers(SortedSet<? extends ClusteringPrefix> clusterings)
+    {
+        List<ByteBuffer> l = new ArrayList<>(clusterings.size());
+        for (ClusteringPrefix clustering : clusterings)
+            l.add(CFMetaData.serializePartitionKey(clustering));
+        return l;
+    }
+
+    @Override
+    public PartitionKeyRestrictions mergeWith(Restriction restriction)
+    {
+        if (restriction.isOnToken())
+        {
+            if (isEmpty())
+                return (PartitionKeyRestrictions) restriction;
+
+            return new TokenFilter(this, (TokenRestriction) restriction);
+        }
+
+        return new PartitionKeySingleRestrictionSet(this, (SingleRestriction) restriction);
+    }
+
+    @Override
+    public List<ByteBuffer> values(QueryOptions options)
+    {
+        MultiCBuilder builder = MultiCBuilder.create(comparator, hasIN());
+        for (SingleRestriction r : restrictions)
+        {
+            r.appendTo(builder, options);
+            if (builder.hasMissingElements())
+                break;
+        }
+        return toByteBuffers(builder.build());
+    }
+
+    @Override
+    public List<ByteBuffer> bounds(Bound bound, QueryOptions options)
+    {
+        MultiCBuilder builder = MultiCBuilder.create(comparator, hasIN());
+        for (SingleRestriction r : restrictions)
+        {
+            r.appendBoundTo(builder, bound, options);
+            if (builder.hasMissingElements())
+                return Collections.emptyList();
+        }
+        return toByteBuffers(builder.buildBound(bound.isStart(), true));
+    }
+
+    @Override
+    public boolean hasBound(Bound b)
+    {
+        if (isEmpty())
+            return false;
+        return restrictions.lastRestriction().hasBound(b);
+    }
+
+    @Override
+    public boolean isInclusive(Bound b)
+    {
+        if (isEmpty())
+            return false;
+        return restrictions.lastRestriction().isInclusive(b);
+    }
+
+    @Override
+    public void addRowFilterTo(RowFilter filter,
+                               SecondaryIndexManager indexManager,
+                               QueryOptions options)
+    {
+        for (SingleRestriction restriction : restrictions)
+        {
+             restriction.addRowFilterTo(filter, indexManager, options);
+        }
+    }
+
+    @Override
+    public boolean needFiltering(CFMetaData cfm)
+    {
+        if (isEmpty())
+            return false;
+
+        // slice or has unrestricted key component
+        return hasUnrestrictedPartitionKeyComponents(cfm) || hasSlice() || hasContains();
+    }
+
+    @Override
+    public boolean hasUnrestrictedPartitionKeyComponents(CFMetaData cfm)
+    {
+        return size() < cfm.partitionKeyColumns().size();
+    }
+
+    @Override
+    public boolean hasSlice()
+    {
+        return restrictions.hasSlice();
+    }
+}
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictionSet.java b/src/java/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictionSet.java
deleted file mode 100644
index 860d3f0..0000000
--- a/src/java/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictionSet.java
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * 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.restrictions;
-
-import java.nio.ByteBuffer;
-import java.util.*;
-
-import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.cql3.QueryOptions;
-import org.apache.cassandra.cql3.functions.Function;
-import org.apache.cassandra.cql3.statements.Bound;
-import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.filter.RowFilter;
-import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.index.SecondaryIndexManager;
-import org.apache.cassandra.utils.btree.BTreeSet;
-
-/**
- * A set of single column restrictions on a primary key part (partition key or clustering key).
- */
-final class PrimaryKeyRestrictionSet extends AbstractPrimaryKeyRestrictions implements Iterable<Restriction>
-{
-    /**
-     * The restrictions.
-     */
-    private final RestrictionSet restrictions;
-
-    /**
-     * <code>true</code> if the restrictions are corresponding to an EQ, <code>false</code> otherwise.
-     */
-    private boolean eq;
-
-    /**
-     * <code>true</code> if the restrictions are corresponding to an IN, <code>false</code> otherwise.
-     */
-    private boolean in;
-
-    /**
-     * <code>true</code> if the restrictions are corresponding to a Slice, <code>false</code> otherwise.
-     */
-    private boolean slice;
-
-    /**
-     * <code>true</code> if the restrictions are corresponding to a Contains, <code>false</code> otherwise.
-     */
-    private boolean contains;
-
-    /**
-     * <code>true</code> if the restrictions corresponding to a partition key, <code>false</code> if it's clustering columns.
-     */
-    private boolean isPartitionKey;
-
-    public PrimaryKeyRestrictionSet(ClusteringComparator comparator, boolean isPartitionKey)
-    {
-        super(comparator);
-
-        this.restrictions = new RestrictionSet();
-        this.eq = true;
-        this.isPartitionKey = isPartitionKey;
-    }
-
-    private PrimaryKeyRestrictionSet(PrimaryKeyRestrictionSet primaryKeyRestrictions,
-                                     Restriction restriction) throws InvalidRequestException
-    {
-        super(primaryKeyRestrictions.comparator);
-        this.restrictions = primaryKeyRestrictions.restrictions.addRestriction(restriction);
-        this.isPartitionKey = primaryKeyRestrictions.isPartitionKey;
-
-        if (restriction.isSlice() || primaryKeyRestrictions.isSlice())
-            this.slice = true;
-        else if (restriction.isContains() || primaryKeyRestrictions.isContains())
-            this.contains = true;
-        else if (restriction.isIN() || primaryKeyRestrictions.isIN())
-            this.in = true;
-        else
-            this.eq = true;
-    }
-
-    private List<ByteBuffer> toByteBuffers(SortedSet<? extends ClusteringPrefix> clusterings)
-    {
-        // It's currently a tad hard to follow that this is only called for partition key so we should fix that
-        List<ByteBuffer> l = new ArrayList<>(clusterings.size());
-        for (ClusteringPrefix clustering : clusterings)
-            l.add(CFMetaData.serializePartitionKey(clustering));
-        return l;
-    }
-
-    @Override
-    public boolean isSlice()
-    {
-        return slice;
-    }
-
-    @Override
-    public boolean isEQ()
-    {
-        return eq;
-    }
-
-    @Override
-    public boolean isIN()
-    {
-        return in;
-    }
-
-    @Override
-    public boolean isOnToken()
-    {
-        return false;
-    }
-
-    @Override
-    public boolean isContains()
-    {
-        return contains;
-    }
-
-    @Override
-    public boolean isMultiColumn()
-    {
-        return false;
-    }
-
-    @Override
-    public void addFunctionsTo(List<Function> functions)
-    {
-        restrictions.addFunctionsTo(functions);
-    }
-
-    @Override
-    public PrimaryKeyRestrictions mergeWith(Restriction restriction) throws InvalidRequestException
-    {
-        if (restriction.isOnToken())
-        {
-            if (isEmpty())
-                return (PrimaryKeyRestrictions) restriction;
-
-            return new TokenFilter(this, (TokenRestriction) restriction);
-        }
-
-        return new PrimaryKeyRestrictionSet(this, restriction);
-    }
-
-    @Override
-    public NavigableSet<Clustering> valuesAsClustering(QueryOptions options) throws InvalidRequestException
-    {
-        return appendTo(MultiCBuilder.create(comparator), options).build();
-    }
-
-    @Override
-    public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options)
-    {
-        for (Restriction r : restrictions)
-        {
-            r.appendTo(builder, options);
-            if (builder.hasMissingElements())
-                break;
-        }
-        return builder;
-    }
-
-    @Override
-    public MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options)
-    {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public NavigableSet<Slice.Bound> boundsAsClustering(Bound bound, QueryOptions options) throws InvalidRequestException
-    {
-        MultiCBuilder builder = MultiCBuilder.create(comparator);
-        int keyPosition = 0;
-        for (Restriction r : restrictions)
-        {
-            ColumnDefinition def = r.getFirstColumn();
-
-            if (keyPosition != def.position() || r.isContains())
-                break;
-
-            if (r.isSlice())
-            {
-                r.appendBoundTo(builder, bound, options);
-                return builder.buildBoundForSlice(bound.isStart(),
-                                                  r.isInclusive(bound),
-                                                  r.isInclusive(bound.reverse()),
-                                                  r.getColumnDefs());
-            }
-
-            r.appendBoundTo(builder, bound, options);
-
-            if (builder.hasMissingElements())
-                return BTreeSet.empty(comparator);
-
-            keyPosition = r.getLastColumn().position() + 1;
-        }
-
-        // Everything was an equal (or there was nothing)
-        return builder.buildBound(bound.isStart(), true);
-    }
-
-    @Override
-    public List<ByteBuffer> values(QueryOptions options) throws InvalidRequestException
-    {
-        if (!isPartitionKey)
-            throw new UnsupportedOperationException();
-
-        return toByteBuffers(valuesAsClustering(options));
-    }
-
-    @Override
-    public List<ByteBuffer> bounds(Bound b, QueryOptions options) throws InvalidRequestException
-    {
-        if (!isPartitionKey)
-            throw new UnsupportedOperationException();
-
-        return toByteBuffers(boundsAsClustering(b, options));
-    }
-
-    @Override
-    public boolean hasBound(Bound b)
-    {
-        if (isEmpty())
-            return false;
-        return restrictions.lastRestriction().hasBound(b);
-    }
-
-    @Override
-    public boolean isInclusive(Bound b)
-    {
-        if (isEmpty())
-            return false;
-        return restrictions.lastRestriction().isInclusive(b);
-    }
-
-    @Override
-    public boolean hasSupportingIndex(SecondaryIndexManager indexManager)
-    {
-        return restrictions.hasSupportingIndex(indexManager);
-    }
-
-    @Override
-    public void addRowFilterTo(RowFilter filter,
-                               SecondaryIndexManager indexManager,
-                               QueryOptions options) throws InvalidRequestException
-    {
-        int position = 0;
-
-        for (Restriction restriction : restrictions)
-        {
-            // We ignore all the clustering columns that can be handled by slices.
-            if (isPartitionKey || handleInFilter(restriction, position) || restriction.hasSupportingIndex(indexManager))
-            {
-                restriction.addRowFilterTo(filter, indexManager, options);
-                continue;
-            }
-
-            if (!restriction.isSlice())
-                position = restriction.getLastColumn().position() + 1;
-        }
-    }
-
-    @Override
-    public List<ColumnDefinition> getColumnDefs()
-    {
-        return restrictions.getColumnDefs();
-    }
-
-    @Override
-    public ColumnDefinition getFirstColumn()
-    {
-        return restrictions.firstColumn();
-    }
-
-    @Override
-    public ColumnDefinition getLastColumn()
-    {
-        return restrictions.lastColumn();
-    }
-
-    public final boolean needsFiltering()
-    {
-        // Backported from ClusteringColumnRestrictions from CASSANDRA-11310 for 3.6
-        // As that suggests, this should only be called on clustering column
-        // and not partition key restrictions.
-        int position = 0;
-        for (Restriction restriction : restrictions)
-        {
-            if (handleInFilter(restriction, position))
-                return true;
-
-            if (!restriction.isSlice())
-                position = restriction.getLastColumn().position() + 1;
-        }
-
-        return false;
-    }
-
-    private boolean handleInFilter(Restriction restriction, int index)
-    {
-        return restriction.isContains() || index != restriction.getFirstColumn().position();
-    }
-
-    public Iterator<Restriction> iterator()
-    {
-        return restrictions.iterator();
-    }
-}
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictions.java
deleted file mode 100644
index 2f9cd7b..0000000
--- a/src/java/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictions.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.restrictions;
-
-import java.nio.ByteBuffer;
-import java.util.List;
-import java.util.NavigableSet;
-
-import org.apache.cassandra.cql3.QueryOptions;
-import org.apache.cassandra.cql3.statements.Bound;
-import org.apache.cassandra.db.Clustering;
-import org.apache.cassandra.db.Slice;
-import org.apache.cassandra.exceptions.InvalidRequestException;
-
-/**
- * A set of restrictions on a primary key part (partition key or clustering key).
- *
- */
-interface PrimaryKeyRestrictions extends Restriction, Restrictions
-{
-    @Override
-    public PrimaryKeyRestrictions mergeWith(Restriction restriction) throws InvalidRequestException;
-
-    public List<ByteBuffer> values(QueryOptions options) throws InvalidRequestException;
-
-    public NavigableSet<Clustering> valuesAsClustering(QueryOptions options) throws InvalidRequestException;
-
-    public List<ByteBuffer> bounds(Bound b, QueryOptions options) throws InvalidRequestException;
-
-    public NavigableSet<Slice.Bound> boundsAsClustering(Bound bound, QueryOptions options) throws InvalidRequestException;
-}
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/Restriction.java b/src/java/org/apache/cassandra/cql3/restrictions/Restriction.java
index 987fd30..fc7f5bc 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/Restriction.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/Restriction.java
@@ -22,27 +22,18 @@
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.functions.Function;
-import org.apache.cassandra.cql3.statements.Bound;
-import org.apache.cassandra.db.MultiCBuilder;
 import org.apache.cassandra.db.filter.RowFilter;
-import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.index.SecondaryIndexManager;
 
 /**
- * A restriction/clause on a column.
- * The goal of this class being to group all conditions for a column in a SELECT.
- *
- * <p>Implementation of this class must be immutable. See {@link #mergeWith(Restriction)} for more explanation.</p>
+ * <p>Implementation of this class must be immutable.</p>
  */
 public interface Restriction
 {
-    public boolean isOnToken();
-    public boolean isSlice();
-    public boolean isEQ();
-    public boolean isIN();
-    public boolean isContains();
-    public boolean isNotNull();
-    public boolean isMultiColumn();
+    public default boolean isOnToken()
+    {
+        return false;
+    }
 
     /**
      * Returns the definition of the first column.
@@ -70,33 +61,6 @@
     void addFunctionsTo(List<Function> functions);
 
     /**
-     * Checks if the specified bound is set or not.
-     * @param b the bound type
-     * @return <code>true</code> if the specified bound is set, <code>false</code> otherwise
-     */
-    public boolean hasBound(Bound b);
-
-    /**
-     * Checks if the specified bound is inclusive or not.
-     * @param b the bound type
-     * @return <code>true</code> if the specified bound is inclusive, <code>false</code> otherwise
-     */
-    public boolean isInclusive(Bound b);
-
-    /**
-     * Merges this restriction with the specified one.
-     *
-     * <p>Restriction are immutable. Therefore merging two restrictions result in a new one.
-     * The reason behind this choice is that it allow a great flexibility in the way the merging can done while
-     * preventing any side effect.</p>
-     *
-     * @param otherRestriction the restriction to merge into this one
-     * @return the restriction resulting of the merge
-     * @throws InvalidRequestException if the restrictions cannot be merged
-     */
-    public Restriction mergeWith(Restriction otherRestriction) throws InvalidRequestException;
-
-    /**
      * Check if the restriction is on indexed columns.
      *
      * @param indexManager the index manager
@@ -110,29 +74,8 @@
      * @param filter the row filter to add expressions to
      * @param indexManager the secondary index manager
      * @param options the query options
-     * @throws InvalidRequestException if this <code>Restriction</code> cannot be converted into a row filter
      */
     public void addRowFilterTo(RowFilter filter,
                                SecondaryIndexManager indexManager,
-                               QueryOptions options)
-                               throws InvalidRequestException;
-
-    /**
-     * Appends the values of this <code>Restriction</code> to the specified builder.
-     *
-     * @param builder the <code>MultiCBuilder</code> to append to.
-     * @param options the query options
-     * @return the <code>MultiCBuilder</code>
-     */
-    public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options);
-
-    /**
-     * Appends the values of the <code>Restriction</code> for the specified bound to the specified builder.
-     *
-     * @param builder the <code>MultiCBuilder</code> to append to.
-     * @param bound the bound
-     * @param options the query options
-     * @return the <code>MultiCBuilder</code>
-     */
-    public MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options);
+                               QueryOptions options);
 }
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSet.java b/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSet.java
index 9aeea69..2bbda38 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSet.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSet.java
@@ -19,6 +19,8 @@
 
 import java.util.*;
 
+import com.google.common.collect.AbstractIterator;
+
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.functions.Function;
@@ -30,10 +32,9 @@
 /**
  * Sets of column restrictions.
  *
- * <p>This class is immutable in order to be use within {@link PrimaryKeyRestrictionSet} which as
- * an implementation of {@link Restriction} need to be immutable.
+ * <p>This class is immutable.</p>
  */
-final class RestrictionSet implements Restrictions, Iterable<Restriction>
+final class RestrictionSet implements Restrictions, Iterable<SingleRestriction>
 {
     /**
      * The comparator used to sort the <code>Restriction</code>s.
@@ -48,30 +49,56 @@
         }
     };
 
+    private static final TreeMap<ColumnDefinition, SingleRestriction> EMPTY = new TreeMap<>(COLUMN_DEFINITION_COMPARATOR);
+
     /**
      * The restrictions per column.
      */
-    protected final TreeMap<ColumnDefinition, Restriction> restrictions;
+    protected final TreeMap<ColumnDefinition, SingleRestriction> restrictions;
+
+    /**
+     * {@code true} if it contains multi-column restrictions, {@code false} otherwise.
+     */
+    private final boolean hasMultiColumnRestrictions;
+
+    private final boolean hasIn;
+    private final boolean hasContains;
+    private final boolean hasSlice;
+    private final boolean hasOnlyEqualityRestrictions;
 
     public RestrictionSet()
     {
-        this(new TreeMap<ColumnDefinition, Restriction>(COLUMN_DEFINITION_COMPARATOR));
+        this(EMPTY, false,
+             false,
+             false,
+             false,
+             true);
     }
 
-    private RestrictionSet(TreeMap<ColumnDefinition, Restriction> restrictions)
+    private RestrictionSet(TreeMap<ColumnDefinition, SingleRestriction> restrictions,
+                           boolean hasMultiColumnRestrictions,
+                           boolean hasIn,
+                           boolean hasContains,
+                           boolean hasSlice,
+                           boolean hasOnlyEqualityRestrictions)
     {
         this.restrictions = restrictions;
+        this.hasMultiColumnRestrictions = hasMultiColumnRestrictions;
+        this.hasIn = hasIn;
+        this.hasContains = hasContains;
+        this.hasSlice = hasSlice;
+        this.hasOnlyEqualityRestrictions = hasOnlyEqualityRestrictions;
     }
 
     @Override
-    public final void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexManager, QueryOptions options) throws InvalidRequestException
+    public void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexManager, QueryOptions options) throws InvalidRequestException
     {
         for (Restriction restriction : restrictions.values())
             restriction.addRowFilterTo(filter, indexManager, options);
     }
 
     @Override
-    public final List<ColumnDefinition> getColumnDefs()
+    public List<ColumnDefinition> getColumnDefs()
     {
         return new ArrayList<>(restrictions.keySet());
     }
@@ -79,28 +106,35 @@
     @Override
     public void addFunctionsTo(List<Function> functions)
     {
-        Restriction previous = null;
-        for (Restriction restriction : restrictions.values())
+        for (Restriction restriction : this)
+            restriction.addFunctionsTo(functions);
+    }
+
+    @Override
+    public boolean isEmpty()
+    {
+        return restrictions.isEmpty();
+    }
+
+    @Override
+    public int size()
+    {
+        return restrictions.size();
+    }
+
+    /**
+     * Checks if one of the restrictions applies to a column of the specific kind.
+     * @param kind the column kind
+     * @return {@code true} if one of the restrictions applies to a column of the specific kind, {@code false} otherwise.
+     */
+    public boolean hasRestrictionFor(ColumnDefinition.Kind kind)
+    {
+        for (ColumnDefinition column : restrictions.keySet())
         {
-            // For muti-column restriction, we can have multiple time the same restriction.
-            if (!restriction.equals(previous))
-            {
-                previous = restriction;
-                restriction.addFunctionsTo(functions);
-            }
+            if (column.kind == kind)
+                return true;
         }
-    }
-
-    @Override
-    public final boolean isEmpty()
-    {
-        return getColumnDefs().isEmpty();
-    }
-
-    @Override
-    public final int size()
-    {
-        return getColumnDefs().size();
+        return false;
     }
 
     /**
@@ -108,21 +142,30 @@
      *
      * @param restriction the restriction to add
      * @return the new set of restrictions
-     * @throws InvalidRequestException if the new restriction cannot be added
      */
-    public RestrictionSet addRestriction(Restriction restriction) throws InvalidRequestException
+    public RestrictionSet addRestriction(SingleRestriction restriction)
     {
         // RestrictionSet is immutable so we need to clone the restrictions map.
-        TreeMap<ColumnDefinition, Restriction> newRestrictions = new TreeMap<>(this.restrictions);
-        return new RestrictionSet(mergeRestrictions(newRestrictions, restriction));
+        TreeMap<ColumnDefinition, SingleRestriction> newRestricitons = new TreeMap<>(this.restrictions);
+
+        boolean newHasIn = hasIn || restriction.isIN();
+        boolean newHasContains = hasContains || restriction.isContains();
+        boolean newHasSlice = hasSlice || restriction.isSlice();
+        boolean newHasOnlyEqualityRestrictions = hasOnlyEqualityRestrictions && (restriction.isEQ() || restriction.isIN());
+
+        return new RestrictionSet(mergeRestrictions(newRestricitons, restriction),
+                                  hasMultiColumnRestrictions || restriction.isMultiColumn(),
+                                  newHasIn,
+                                  newHasContains,
+                                  newHasSlice,
+                                  newHasOnlyEqualityRestrictions);
     }
 
-    private TreeMap<ColumnDefinition, Restriction> mergeRestrictions(TreeMap<ColumnDefinition, Restriction> restrictions,
-                                                                     Restriction restriction)
-                                                                     throws InvalidRequestException
+    private TreeMap<ColumnDefinition, SingleRestriction> mergeRestrictions(TreeMap<ColumnDefinition, SingleRestriction> restrictions,
+                                                                           SingleRestriction restriction)
     {
         Collection<ColumnDefinition> columnDefs = restriction.getColumnDefs();
-        Set<Restriction> existingRestrictions = getRestrictions(columnDefs);
+        Set<SingleRestriction> existingRestrictions = getRestrictions(columnDefs);
 
         if (existingRestrictions.isEmpty())
         {
@@ -131,9 +174,9 @@
         }
         else
         {
-            for (Restriction existing : existingRestrictions)
+            for (SingleRestriction existing : existingRestrictions)
             {
-                Restriction newRestriction = mergeRestrictions(existing, restriction);
+                SingleRestriction newRestriction = mergeRestrictions(existing, restriction);
 
                 for (ColumnDefinition columnDef : columnDefs)
                     restrictions.put(columnDef, newRestriction);
@@ -143,18 +186,25 @@
         return restrictions;
     }
 
+    @Override
+    public Set<Restriction> getRestrictions(ColumnDefinition columnDef)
+    {
+        Restriction existing = restrictions.get(columnDef);
+        return existing == null ? Collections.emptySet() : Collections.singleton(existing);
+    }
+
     /**
      * Returns all the restrictions applied to the specified columns.
      *
      * @param columnDefs the column definitions
      * @return all the restrictions applied to the specified columns
      */
-    private Set<Restriction> getRestrictions(Collection<ColumnDefinition> columnDefs)
+    private Set<SingleRestriction> getRestrictions(Collection<ColumnDefinition> columnDefs)
     {
-        Set<Restriction> set = new HashSet<>();
+        Set<SingleRestriction> set = new HashSet<>();
         for (ColumnDefinition columnDef : columnDefs)
         {
-            Restriction existing = restrictions.get(columnDef);
+            SingleRestriction existing = restrictions.get(columnDef);
             if (existing != null)
                 set.add(existing);
         }
@@ -183,22 +233,14 @@
         return restrictions.tailMap(columnDef, false).firstKey();
     }
 
-    /**
-     * Returns the definition of the first column.
-     *
-     * @return the definition of the first column.
-     */
-    ColumnDefinition firstColumn()
+    @Override
+    public ColumnDefinition getFirstColumn()
     {
         return isEmpty() ? null : this.restrictions.firstKey();
     }
 
-    /**
-     * Returns the definition of the last column.
-     *
-     * @return the definition of the last column.
-     */
-    ColumnDefinition lastColumn()
+    @Override
+    public ColumnDefinition getLastColumn()
     {
         return isEmpty() ? null : this.restrictions.lastKey();
     }
@@ -208,7 +250,7 @@
      *
      * @return the last restriction.
      */
-    Restriction lastRestriction()
+    SingleRestriction lastRestriction()
     {
         return isEmpty() ? null : this.restrictions.lastEntry().getValue();
     }
@@ -221,8 +263,8 @@
      * @return the merged restriction
      * @throws InvalidRequestException if the two restrictions cannot be merged
      */
-    private static Restriction mergeRestrictions(Restriction restriction,
-                                                 Restriction otherRestriction) throws InvalidRequestException
+    private static SingleRestriction mergeRestrictions(SingleRestriction restriction,
+                                                       SingleRestriction otherRestriction)
     {
         return restriction == null ? otherRestriction
                                    : restriction.mergeWith(otherRestriction);
@@ -237,7 +279,7 @@
     public final boolean hasMultipleContains()
     {
         int numberOfContains = 0;
-        for (Restriction restriction : restrictions.values())
+        for (SingleRestriction restriction : restrictions.values())
         {
             if (restriction.isContains())
             {
@@ -249,8 +291,77 @@
     }
 
     @Override
-    public Iterator<Restriction> iterator()
+    public Iterator<SingleRestriction> iterator()
     {
-        return new LinkedHashSet<>(restrictions.values()).iterator();
+        Iterator<SingleRestriction> iterator = restrictions.values().iterator();
+        return hasMultiColumnRestrictions ? new DistinctIterator<>(iterator) : iterator;
+    }
+
+    /**
+     * Checks if any of the underlying restriction is an IN.
+     * @return <code>true</code> if any of the underlying restriction is an IN, <code>false</code> otherwise
+     */
+    public final boolean hasIN()
+    {
+        return hasIn;
+    }
+
+    public boolean hasContains()
+    {
+        return hasContains;
+    }
+
+    public final boolean hasSlice()
+    {
+        return hasSlice;
+    }
+
+    /**
+     * Checks if all of the underlying restrictions are EQ or IN restrictions.
+     *
+     * @return <code>true</code> if all of the underlying restrictions are EQ or IN restrictions,
+     * <code>false</code> otherwise
+     */
+    public final boolean hasOnlyEqualityRestrictions()
+    {
+        return hasOnlyEqualityRestrictions;
+    }
+
+    /**
+     * {@code Iterator} decorator that removes duplicates in an ordered one.
+     *
+     * @param iterator the decorated iterator
+     * @param <E> the iterator element type.
+     */
+    private static final class DistinctIterator<E> extends AbstractIterator<E>
+    {
+        /**
+         * The decorated iterator.
+         */
+        private final Iterator<E> iterator;
+
+        /**
+         * The previous element.
+         */
+        private E previous;
+
+        public DistinctIterator(Iterator<E> iterator)
+        {
+            this.iterator = iterator;
+        }
+
+        protected E computeNext()
+        {
+            while(iterator.hasNext())
+            {
+                E next = iterator.next();
+                if (!next.equals(previous))
+                {
+                    previous = next;
+                    return next;
+                }
+            }
+            return endOfData();
+        }
     }
 }
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSetWrapper.java b/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSetWrapper.java
new file mode 100644
index 0000000..5157de0
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSetWrapper.java
@@ -0,0 +1,111 @@
+/*
+ * 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.restrictions;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.functions.Function;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.index.SecondaryIndexManager;
+
+/**
+ * A <code>RestrictionSet</code> wrapper that can be extended to allow to modify the <code>RestrictionSet</code>
+ * behaviour without breaking its immutability. Sub-classes should be immutables.
+ */
+class RestrictionSetWrapper implements Restrictions
+{
+    /**
+     * The wrapped <code>RestrictionSet</code>.
+     */
+    protected final RestrictionSet restrictions;
+
+    public RestrictionSetWrapper(RestrictionSet restrictions)
+    {
+        this.restrictions = restrictions;
+    }
+
+    public void addRowFilterTo(RowFilter filter,
+                               SecondaryIndexManager indexManager,
+                               QueryOptions options)
+    {
+        restrictions.addRowFilterTo(filter, indexManager, options);
+    }
+
+    public List<ColumnDefinition> getColumnDefs()
+    {
+        return restrictions.getColumnDefs();
+    }
+
+    public void addFunctionsTo(List<Function> functions)
+    {
+        restrictions.addFunctionsTo(functions);
+    }
+
+    public boolean isEmpty()
+    {
+        return restrictions.isEmpty();
+    }
+
+    public int size()
+    {
+        return restrictions.size();
+    }
+
+    public boolean hasSupportingIndex(SecondaryIndexManager indexManager)
+    {
+        return restrictions.hasSupportingIndex(indexManager);
+    }
+
+    public ColumnDefinition getFirstColumn()
+    {
+        return restrictions.getFirstColumn();
+    }
+
+    public ColumnDefinition getLastColumn()
+    {
+        return restrictions.getLastColumn();
+    }
+
+    public boolean hasIN()
+    {
+        return restrictions.hasIN();
+    }
+
+    public boolean hasContains()
+    {
+        return restrictions.hasContains();
+    }
+
+    public boolean hasSlice()
+    {
+        return restrictions.hasSlice();
+    }
+
+    public boolean hasOnlyEqualityRestrictions()
+    {
+        return restrictions.hasOnlyEqualityRestrictions();
+    }
+
+    public Set<Restriction> getRestrictions(ColumnDefinition columnDef)
+    {
+        return restrictions.getRestrictions(columnDef);
+    }
+}
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/Restrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/Restrictions.java
index 5fa3170..5d11e9f 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/Restrictions.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/Restrictions.java
@@ -17,56 +17,27 @@
  */
 package org.apache.cassandra.cql3.restrictions;
 
-import java.util.Collection;
-import java.util.List;
+import java.util.Set;
 
 import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.cql3.QueryOptions;
-import org.apache.cassandra.cql3.functions.Function;
-import org.apache.cassandra.db.filter.RowFilter;
-import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.index.SecondaryIndexManager;
 
 /**
  * Sets of restrictions
  */
-interface Restrictions
+public interface Restrictions extends Restriction
 {
     /**
-     * Returns the column definitions in position order.
-     * @return the column definitions in position order.
-     */
-    public Collection<ColumnDefinition> getColumnDefs();
-
-    /**
-     * Adds all functions (native and user-defined) used by any component of the restriction
-     * to the specified list.
-     * @param functions the list to add to
-     */
-    public void addFunctionsTo(List<Function> functions);
-
-    /**
-     * Check if the restriction is on indexed columns.
+     * Returns the restrictions applied to the specified column.
      *
-     * @param indexManager the index manager
-     * @return <code>true</code> if the restriction is on indexed columns, <code>false</code>
+     * @param columnDef the column definition
+     * @return the restrictions applied to the specified column
      */
-    public boolean hasSupportingIndex(SecondaryIndexManager indexManager);
+    Set<Restriction> getRestrictions(ColumnDefinition columnDef);
 
     /**
-     * Adds to the specified row filter the expressions corresponding to this <code>Restrictions</code>.
+     * Checks if this <code>Restrictions</code> is empty or not.
      *
-     * @param filter the row filter to add expressions to
-     * @param indexManager the secondary index manager
-     * @param options the query options
-     * @throws InvalidRequestException if this <code>Restrictions</code> cannot be converted into a row filter
-     */
-    public void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexManager, QueryOptions options) throws InvalidRequestException;
-
-    /**
-     * Checks if this <code>PrimaryKeyRestrictionSet</code> is empty or not.
-     *
-     * @return <code>true</code> if this <code>PrimaryKeyRestrictionSet</code> is empty, <code>false</code> otherwise.
+     * @return <code>true</code> if this <code>Restrictions</code> is empty, <code>false</code> otherwise.
      */
     boolean isEmpty();
 
@@ -76,4 +47,29 @@
      * @return the number of columns that have a restriction.
      */
     public int size();
+
+    /**
+     * Checks if any of the underlying restriction is an IN.
+     * @return <code>true</code> if any of the underlying restriction is an IN, <code>false</code> otherwise
+     */
+    public boolean hasIN();
+
+    /**
+     * Checks if any of the underlying restrictions is a CONTAINS / CONTAINS KEY restriction.
+     * @return <code>true</code> if any of the underlying restrictions is CONTAINS, <code>false</code> otherwise
+     */
+    public boolean hasContains();
+    /**
+     * Checks if any of the underlying restrictions is a slice.
+     * @return <code>true</code> if any of the underlying restrictions is a slice, <code>false</code> otherwise
+     */
+    public boolean hasSlice();
+
+    /**
+     * Checks if all of the underlying restrictions are EQ or IN restrictions.
+     *
+     * @return <code>true</code> if all of the underlying restrictions are EQ or IN restrictions,
+     * <code>false</code> otherwise
+     */
+    public boolean hasOnlyEqualityRestrictions();
 }
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java
index 5985962..09c02ed 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java
@@ -18,7 +18,9 @@
 package org.apache.cassandra.cql3.restrictions;
 
 import java.nio.ByteBuffer;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.*;
@@ -30,6 +32,8 @@
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.index.Index;
 import org.apache.cassandra.index.SecondaryIndexManager;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.Pair;
 
 import static org.apache.cassandra.cql3.statements.RequestValidations.checkBindValueSet;
 import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
@@ -37,7 +41,7 @@
 import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue;
 import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
 
-public abstract class SingleColumnRestriction extends AbstractRestriction
+public abstract class SingleColumnRestriction implements SingleRestriction
 {
     /**
      * The definition of the column to which apply the restriction.
@@ -78,7 +82,7 @@
     }
 
     @Override
-    public final Restriction mergeWith(Restriction otherRestriction) throws InvalidRequestException
+    public final SingleRestriction mergeWith(SingleRestriction otherRestriction)
     {
         // We want to allow query like: b > ? AND (b,c) < (?, ?)
         if (otherRestriction.isMultiColumn() && canBeConvertedToMultiColumnRestriction())
@@ -89,7 +93,7 @@
         return doMergeWith(otherRestriction);
     }
 
-    protected abstract Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException;
+    protected abstract SingleRestriction doMergeWith(SingleRestriction otherRestriction);
 
     /**
      * Converts this <code>SingleColumnRestriction</code> into a {@link MultiColumnRestriction}
@@ -170,7 +174,7 @@
         }
 
         @Override
-        public Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
+        public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
         {
             throw invalidRequest("%s cannot be restricted by more than one relation if it includes an Equal", columnDef.name);
         }
@@ -196,7 +200,7 @@
         }
 
         @Override
-        public final Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
+        public final SingleRestriction doMergeWith(SingleRestriction otherRestriction)
         {
             throw invalidRequest("%s cannot be restricted by more than one relation if it includes a IN", columnDef.name);
         }
@@ -213,12 +217,9 @@
         @Override
         public void addRowFilterTo(RowFilter filter,
                                    SecondaryIndexManager indexManager,
-                                   QueryOptions options) throws InvalidRequestException
+                                   QueryOptions options)
         {
-            List<ByteBuffer> values = getValues(options);
-            checkTrue(values.size() == 1, "IN restrictions are not supported on indexed columns");
-
-            filter.add(columnDef, Operator.EQ, values.get(0));
+            throw invalidRequest("IN restrictions are not supported on indexed columns");
         }
 
         @Override
@@ -227,7 +228,7 @@
             return index.supportsExpression(columnDef, Operator.IN);
         }
 
-        protected abstract List<ByteBuffer> getValues(QueryOptions options) throws InvalidRequestException;
+        protected abstract List<ByteBuffer> getValues(QueryOptions options);
     }
 
     public static class InRestrictionWithValues extends INRestriction
@@ -253,7 +254,7 @@
         }
 
         @Override
-        protected List<ByteBuffer> getValues(QueryOptions options) throws InvalidRequestException
+        protected List<ByteBuffer> getValues(QueryOptions options)
         {
             List<ByteBuffer> buffers = new ArrayList<>(values.size());
             for (Term value : values)
@@ -290,7 +291,7 @@
         }
 
         @Override
-        protected List<ByteBuffer> getValues(QueryOptions options) throws InvalidRequestException
+        protected List<ByteBuffer> getValues(QueryOptions options)
         {
             Terminal term = marker.bind(options);
             checkNotNull(term, "Invalid null value for column %s", columnDef.name);
@@ -349,7 +350,7 @@
         @Override
         public MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options)
         {
-            Bound b = reverseBoundIfNeeded(getFirstColumn(), bound);
+            Bound b = bound.reverseIfNeeded(getFirstColumn());
 
             if (!hasBound(b))
                 return builder;
@@ -367,7 +368,7 @@
         }
 
         @Override
-        public Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
+        public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
         {
             checkTrue(otherRestriction.isSlice(),
                       "Column \"%s\" cannot be restricted by both an equality and an inequality relation",
@@ -385,7 +386,7 @@
         }
 
         @Override
-        public void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexManager, QueryOptions options) throws InvalidRequestException
+        public void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexManager, QueryOptions options)
         {
             for (Bound b : Bound.values())
                 if (hasBound(b))
@@ -460,7 +461,7 @@
         }
 
         @Override
-        public Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
+        public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
         {
             checkTrue(otherRestriction.isContains(),
                       "Collection column %s can only be restricted by CONTAINS, CONTAINS KEY, or map-entry equality",
@@ -475,7 +476,7 @@
         }
 
         @Override
-        public void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexManager, QueryOptions options) throws InvalidRequestException
+        public void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexManager, QueryOptions options)
         {
             for (ByteBuffer value : bindAndGet(values, options))
                 filter.add(columnDef, Operator.CONTAINS, value);
@@ -560,9 +561,8 @@
          * @param terms the terms
          * @param options the query options
          * @return the value resulting from binding the query options to the specified terms
-         * @throws InvalidRequestException if a problem occurs while binding the query options
          */
-        private static List<ByteBuffer> bindAndGet(List<Term> terms, QueryOptions options) throws InvalidRequestException
+        private static List<ByteBuffer> bindAndGet(List<Term> terms, QueryOptions options)
         {
             List<ByteBuffer> buffers = new ArrayList<>(terms.size());
             for (Term value : terms)
@@ -635,7 +635,7 @@
         }
 
         @Override
-        public Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
+        public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
         {
             throw invalidRequest("%s cannot be restricted by a relation if it includes an IS NOT NULL", columnDef.name);
         }
@@ -647,6 +647,137 @@
         }
     }
 
+    public static final class LikeRestriction extends SingleColumnRestriction
+    {
+        private static final ByteBuffer LIKE_WILDCARD = ByteBufferUtil.bytes("%");
+        private final Operator operator;
+        private final Term value;
+
+        public LikeRestriction(ColumnDefinition columnDef, Operator operator, Term value)
+        {
+            super(columnDef);
+            this.operator = operator;
+            this.value = value;
+        }
+
+        @Override
+        public void addFunctionsTo(List<Function> functions)
+        {
+            value.addFunctionsTo(functions);
+        }
+
+        @Override
+        public boolean isEQ()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isLIKE()
+        {
+            return true;
+        }
+
+        @Override
+        public boolean canBeConvertedToMultiColumnRestriction()
+        {
+            return false;
+        }
+
+        @Override
+        MultiColumnRestriction toMultiColumnRestriction()
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void addRowFilterTo(RowFilter filter,
+                                   SecondaryIndexManager indexManager,
+                                   QueryOptions options)
+        {
+            Pair<Operator, ByteBuffer> operation = makeSpecific(value.bindAndGet(options));
+
+            // there must be a suitable INDEX for LIKE_XXX expressions
+            RowFilter.SimpleExpression expression = filter.add(columnDef, operation.left, operation.right);
+            indexManager.getBestIndexFor(expression)
+                        .orElseThrow(() -> invalidRequest("%s is only supported on properly indexed columns",
+                                                          expression));
+        }
+
+        @Override
+        public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options)
+        {
+            // LIKE can be used with clustering columns, but as it doesn't
+            // represent an actual clustering value, it can't be used in a
+            // clustering filter.
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public String toString()
+        {
+            return operator.toString();
+        }
+
+        @Override
+        public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
+        {
+            throw invalidRequest("%s cannot be restricted by more than one relation if it includes a %s", columnDef.name, operator);
+        }
+
+        @Override
+        protected boolean isSupportedBy(Index index)
+        {
+            return index.supportsExpression(columnDef, operator);
+        }
+
+        /**
+         * As the specific subtype of LIKE (LIKE_PREFIX, LIKE_SUFFIX, LIKE_CONTAINS, LIKE_MATCHES) can only be
+         * determined by examining the value, which in turn can only be known after binding, all LIKE restrictions
+         * are initially created with the generic LIKE operator. This function takes the bound value, trims the
+         * wildcard '%' chars from it and returns a tuple of the inferred operator subtype and the final value
+         * @param value the bound value for the LIKE operation
+         * @return  Pair containing the inferred LIKE subtype and the value with wildcards removed
+         */
+        private static Pair<Operator, ByteBuffer> makeSpecific(ByteBuffer value)
+        {
+            Operator operator;
+            int beginIndex = value.position();
+            int endIndex = value.limit() - 1;
+            if (ByteBufferUtil.endsWith(value, LIKE_WILDCARD))
+            {
+                if (ByteBufferUtil.startsWith(value, LIKE_WILDCARD))
+                {
+                    operator = Operator.LIKE_CONTAINS;
+                    beginIndex =+ 1;
+                }
+                else
+                {
+                    operator = Operator.LIKE_PREFIX;
+                }
+            }
+            else if (ByteBufferUtil.startsWith(value, LIKE_WILDCARD))
+            {
+                operator = Operator.LIKE_SUFFIX;
+                beginIndex += 1;
+                endIndex += 1;
+            }
+            else
+            {
+                operator = Operator.LIKE_MATCHES;
+                endIndex += 1;
+            }
+
+            if (endIndex == 0 || beginIndex == endIndex)
+                throw invalidRequest("LIKE value can't be empty.");
+
+            ByteBuffer newValue = value.duplicate();
+            newValue.position(beginIndex);
+            newValue.limit(endIndex);
+            return Pair.create(operator, newValue);
+        }
+    }
+
     /**
      * Super Column Compatibiltiy
      */
@@ -696,7 +827,7 @@
         @Override
         public MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options)
         {
-            Bound b = reverseBoundIfNeeded(getFirstColumn(), bound);
+            Bound b = bound.reverseIfNeeded(getFirstColumn());
 
             if (!hasBound(b))
                 return builder;
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/SingleRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/SingleRestriction.java
new file mode 100644
index 0000000..42b0b4e
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/restrictions/SingleRestriction.java
@@ -0,0 +1,117 @@
+/*
+ * 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.restrictions;
+
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.statements.Bound;
+import org.apache.cassandra.db.MultiCBuilder;
+
+/**
+ * A single restriction/clause on one or multiple column.
+ */
+public interface SingleRestriction extends Restriction
+{
+    public default boolean isSlice()
+    {
+        return false;
+    }
+
+    public default boolean isEQ()
+    {
+        return false;
+    }
+
+    public default boolean isLIKE()
+    {
+        return false;
+    }
+
+    public default boolean isIN()
+    {
+        return false;
+    }
+
+    public default boolean isContains()
+    {
+        return false;
+    }
+
+    public default boolean isNotNull()
+    {
+        return false;
+    }
+
+    public default boolean isMultiColumn()
+    {
+        return false;
+    }
+
+    /**
+     * Checks if the specified bound is set or not.
+     * @param b the bound type
+     * @return <code>true</code> if the specified bound is set, <code>false</code> otherwise
+     */
+    public default boolean hasBound(Bound b)
+    {
+        return true;
+    }
+
+    /**
+     * Checks if the specified bound is inclusive or not.
+     * @param b the bound type
+     * @return <code>true</code> if the specified bound is inclusive, <code>false</code> otherwise
+     */
+    public default boolean isInclusive(Bound b)
+    {
+        return true;
+    }
+
+    /**
+     * Merges this restriction with the specified one.
+     *
+     * <p>Restriction are immutable. Therefore merging two restrictions result in a new one.
+     * The reason behind this choice is that it allow a great flexibility in the way the merging can done while
+     * preventing any side effect.</p>
+     *
+     * @param otherRestriction the restriction to merge into this one
+     * @return the restriction resulting of the merge
+     */
+    public SingleRestriction mergeWith(SingleRestriction otherRestriction);
+
+    /**
+     * Appends the values of this <code>SingleRestriction</code> to the specified builder.
+     *
+     * @param builder the <code>MultiCBuilder</code> to append to.
+     * @param options the query options
+     * @return the <code>MultiCBuilder</code>
+     */
+    public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options);
+
+    /**
+     * Appends the values of the <code>SingleRestriction</code> for the specified bound to the specified builder.
+     *
+     * @param builder the <code>MultiCBuilder</code> to append to.
+     * @param bound the bound
+     * @param options the query options
+     * @return the <code>MultiCBuilder</code>
+     */
+    public default MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options)
+    {
+        return appendTo(builder, options);
+    }
+}
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java
index 73d2810..b738f8c 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java
@@ -21,11 +21,11 @@
 import java.util.*;
 
 import com.google.common.base.Joiner;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Iterators;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.ColumnDefinition.Kind;
 import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.cql3.statements.Bound;
@@ -38,8 +38,6 @@
 import org.apache.cassandra.index.Index;
 import org.apache.cassandra.index.SecondaryIndexManager;
 import org.apache.cassandra.net.MessagingService;
-import org.apache.cassandra.schema.IndexMetadata;
-import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.btree.BTreeSet;
 
 import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
@@ -69,30 +67,24 @@
     /**
      * Restrictions on partitioning columns
      */
-    private PrimaryKeyRestrictions partitionKeyRestrictions;
+    private PartitionKeyRestrictions partitionKeyRestrictions;
 
     /**
      * Restrictions on clustering columns
      */
-    private PrimaryKeyRestrictions clusteringColumnsRestrictions;
+    private ClusteringColumnRestrictions clusteringColumnsRestrictions;
 
     /**
      * Restriction on non-primary key columns (i.e. secondary index restrictions)
      */
     private RestrictionSet nonPrimaryKeyRestrictions;
 
-    /**
-     * <code>true</code> if nonPrimaryKeyRestrictions contains restriction on a regular column,
-     * <code>false</code> otherwise.
-     */
-    private boolean hasRegularColumnsRestriction = false;
-
     private Set<ColumnDefinition> notNullColumns;
 
     /**
      * The restrictions used to build the row filter
      */
-    private final IndexRestrictions indexRestrictions = new IndexRestrictions();
+    private final IndexRestrictions filterRestrictions = new IndexRestrictions();
 
     /**
      * <code>true</code> if the secondary index need to be queried, <code>false</code> otherwise
@@ -105,6 +97,12 @@
     private boolean isKeyRange;
 
     /**
+     * <code>true</code> if nonPrimaryKeyRestrictions contains restriction on a regular column,
+     * <code>false</code> otherwise.
+     */
+    private boolean hasRegularColumnsRestrictions;
+
+    /**
      * Creates a new empty <code>StatementRestrictions</code>.
      *
      * @param type the type of statement
@@ -113,15 +111,15 @@
      */
     public static StatementRestrictions empty(StatementType type, CFMetaData cfm)
     {
-        return new StatementRestrictions(type, cfm);
+        return new StatementRestrictions(type, cfm, false);
     }
 
-    private StatementRestrictions(StatementType type, CFMetaData cfm)
+    private StatementRestrictions(StatementType type, CFMetaData cfm, boolean allowFiltering)
     {
         this.type = type;
         this.cfm = cfm;
-        this.partitionKeyRestrictions = new PrimaryKeyRestrictionSet(cfm.getKeyValidatorAsClusteringComparator(), true);
-        this.clusteringColumnsRestrictions = new PrimaryKeyRestrictionSet(cfm.comparator, false);
+        this.partitionKeyRestrictions = new PartitionKeySingleRestrictionSet(cfm.getKeyValidatorAsClusteringComparator());
+        this.clusteringColumnsRestrictions = new ClusteringColumnRestrictions(cfm, allowFiltering);
         this.nonPrimaryKeyRestrictions = new RestrictionSet();
         this.notNullColumns = new HashSet<>();
     }
@@ -131,16 +129,20 @@
                                  WhereClause whereClause,
                                  VariableSpecifications boundNames,
                                  boolean selectsOnlyStaticColumns,
-                                 boolean selectACollection,
+                                 boolean selectsComplexColumn,
                                  boolean allowFiltering,
-                                 boolean forView) throws InvalidRequestException
+                                 boolean forView)
     {
-        this.type = type;
-        this.cfm = cfm;
-        this.partitionKeyRestrictions = new PrimaryKeyRestrictionSet(cfm.getKeyValidatorAsClusteringComparator(), true);
-        this.clusteringColumnsRestrictions = new PrimaryKeyRestrictionSet(cfm.comparator, false);
-        this.nonPrimaryKeyRestrictions = new RestrictionSet();
-        this.notNullColumns = new HashSet<>();
+        this(type, cfm, allowFiltering);
+
+        ColumnFamilyStore cfs;
+        SecondaryIndexManager secondaryIndexManager = null;
+
+        if (type.allowUseOfSecondaryIndices())
+        {
+            cfs = Keyspace.open(cfm.ksName).getColumnFamilyStore(cfm.cfName);
+            secondaryIndexManager = cfs.indexManager;
+        }
 
         /*
          * WHERE clause. For a given entity, rules are:
@@ -167,6 +169,17 @@
                 for (ColumnDefinition def : relation.toRestriction(cfm, boundNames).getColumnDefs())
                     this.notNullColumns.add(def);
             }
+            else if (relation.isLIKE())
+            {
+                Restriction restriction = relation.toRestriction(cfm, boundNames);
+
+                if (!type.allowUseOfSecondaryIndices() || !restriction.hasSupportingIndex(secondaryIndexManager))
+                    throw new InvalidRequestException(String.format("LIKE restriction is only supported on properly " +
+                                                                    "indexed columns. %s is not valid.",
+                                                                    relation.toString()));
+
+                addRestriction(restriction);
+            }
             else
             {
                 if (cfm.isSuper() && cfm.isDense() && !relation.onToken())
@@ -176,33 +189,32 @@
             }
         }
 
+        hasRegularColumnsRestrictions = nonPrimaryKeyRestrictions.hasRestrictionFor(Kind.REGULAR);
+
         boolean hasQueriableClusteringColumnIndex = false;
         boolean hasQueriableIndex = false;
 
         if (type.allowUseOfSecondaryIndices())
         {
-            ColumnFamilyStore cfs = Keyspace.open(cfm.ksName).getColumnFamilyStore(cfm.cfName);
-            SecondaryIndexManager secondaryIndexManager = cfs.indexManager;
-
             if (whereClause.containsCustomExpressions())
                 processCustomIndexExpressions(whereClause.expressions, boundNames, secondaryIndexManager);
 
             hasQueriableClusteringColumnIndex = clusteringColumnsRestrictions.hasSupportingIndex(secondaryIndexManager);
-            hasQueriableIndex = !indexRestrictions.getCustomIndexExpressions().isEmpty()
+            hasQueriableIndex = !filterRestrictions.getCustomIndexExpressions().isEmpty()
                     || hasQueriableClusteringColumnIndex
                     || partitionKeyRestrictions.hasSupportingIndex(secondaryIndexManager)
                     || nonPrimaryKeyRestrictions.hasSupportingIndex(secondaryIndexManager);
         }
 
         // At this point, the select statement if fully constructed, but we still have a few things to validate
-        processPartitionKeyRestrictions(hasQueriableIndex);
+        processPartitionKeyRestrictions(hasQueriableIndex, allowFiltering, forView);
 
         // Some but not all of the partition key columns have been specified;
         // hence we need turn these restrictions into a row filter.
-        if (usesSecondaryIndexing)
-            indexRestrictions.add(partitionKeyRestrictions);
+        if (usesSecondaryIndexing || partitionKeyRestrictions.needFiltering(cfm))
+            filterRestrictions.add(partitionKeyRestrictions);
 
-        if (selectsOnlyStaticColumns && hasClusteringColumnsRestriction())
+        if (selectsOnlyStaticColumns && hasClusteringColumnsRestrictions())
         {
             // If the only updated/deleted columns are static, then we don't need clustering columns.
             // And in fact, unless it is an INSERT, we reject if clustering colums are provided as that
@@ -221,16 +233,18 @@
                 throw invalidRequest("Cannot restrict clustering columns when selecting only static columns");
         }
 
-        processClusteringColumnsRestrictions(hasQueriableIndex, selectsOnlyStaticColumns, selectACollection, forView);
+        processClusteringColumnsRestrictions(hasQueriableIndex,
+                                             selectsOnlyStaticColumns,
+                                             selectsComplexColumn,
+                                             forView,
+                                             allowFiltering);
 
         // Covers indexes on the first clustering column (among others).
         if (isKeyRange && hasQueriableClusteringColumnIndex)
             usesSecondaryIndexing = true;
 
-        usesSecondaryIndexing = usesSecondaryIndexing || clusteringColumnsRestrictions.isContains();
-
-        if (usesSecondaryIndexing)
-            indexRestrictions.add(clusteringColumnsRestrictions);
+        if (usesSecondaryIndexing || clusteringColumnsRestrictions.needFiltering())
+            filterRestrictions.add(clusteringColumnsRestrictions);
 
         // Even if usesSecondaryIndexing is false at this point, we'll still have to use one if
         // there is restrictions not covered by the PK.
@@ -256,21 +270,22 @@
             checkFalse(clusteringColumnsRestrictions.isEmpty() && cfm.isSuper(),
                        "Filtering is not supported on SuperColumn tables");
 
-            indexRestrictions.add(nonPrimaryKeyRestrictions);
+            filterRestrictions.add(nonPrimaryKeyRestrictions);
         }
 
         if (usesSecondaryIndexing)
-            validateSecondaryIndexSelections(selectsOnlyStaticColumns);
+            validateSecondaryIndexSelections();
     }
 
     private void addRestriction(Restriction restriction)
     {
-        if (restriction.isMultiColumn())
-            clusteringColumnsRestrictions = clusteringColumnsRestrictions.mergeWith(restriction);
-        else if (restriction.isOnToken())
+        ColumnDefinition def = restriction.getFirstColumn();
+        if (def.isPartitionKey())
             partitionKeyRestrictions = partitionKeyRestrictions.mergeWith(restriction);
+        else if (def.isClusteringColumn())
+            clusteringColumnsRestrictions = clusteringColumnsRestrictions.mergeWith(restriction);
         else
-            addSingleColumnRestriction((SingleColumnRestriction) restriction);
+            nonPrimaryKeyRestrictions = nonPrimaryKeyRestrictions.addRestriction((SingleRestriction) restriction);
     }
 
     public void addFunctionsTo(List<Function> functions)
@@ -280,21 +295,10 @@
         nonPrimaryKeyRestrictions.addFunctionsTo(functions);
     }
 
-    private void addSingleColumnRestriction(SingleColumnRestriction restriction)
+    // may be used by QueryHandler implementations
+    public IndexRestrictions getIndexRestrictions()
     {
-        ColumnDefinition def = restriction.columnDef;
-        if (def.isPartitionKey())
-            partitionKeyRestrictions = partitionKeyRestrictions.mergeWith(restriction);
-        else if (def.isClusteringColumn())
-            clusteringColumnsRestrictions = clusteringColumnsRestrictions.mergeWith(restriction);
-        else
-        {
-            if (restriction.columnDef.kind == ColumnDefinition.Kind.REGULAR)
-            {
-                hasRegularColumnsRestriction = true;
-            }
-            nonPrimaryKeyRestrictions = nonPrimaryKeyRestrictions.addRestriction(restriction);
-        }
+        return filterRestrictions;
     }
 
     /**
@@ -305,7 +309,7 @@
     public Set<ColumnDefinition> nonPKRestrictedColumns(boolean includeNotNullRestrictions)
     {
         Set<ColumnDefinition> columns = new HashSet<>();
-        for (Restrictions r : indexRestrictions.getRestrictions())
+        for (Restrictions r : filterRestrictions.getRestrictions())
         {
             for (ColumnDefinition def : r.getColumnDefs())
                 if (!def.isPrimaryKeyColumn())
@@ -339,23 +343,19 @@
     {
         if (notNullColumns.contains(column))
             return true;
-        else if (column.isPartitionKey())
-            return partitionKeyRestrictions.getColumnDefs().contains(column);
-        else if (column.isClusteringColumn())
-            return clusteringColumnsRestrictions.getColumnDefs().contains(column);
-        else
-            return nonPrimaryKeyRestrictions.getColumnDefs().contains(column);
+
+        return getRestrictions(column.kind).getColumnDefs().contains(column);
     }
 
     /**
-     * Checks if the restrictions on the partition key is an IN restriction.
+     * Checks if the restrictions on the partition key has IN restrictions.
      *
-     * @return <code>true</code> the restrictions on the partition key is an IN restriction, <code>false</code>
+     * @return <code>true</code> the restrictions on the partition key has an IN restriction, <code>false</code>
      * otherwise.
      */
     public boolean keyIsInRelation()
     {
-        return partitionKeyRestrictions.isIN();
+        return partitionKeyRestrictions.hasIN();
     }
 
     /**
@@ -368,9 +368,35 @@
         return this.isKeyRange;
     }
 
-    public boolean hasRegularColumnsRestriction()
+    /**
+     * Checks if the specified column is restricted by an EQ restriction.
+     *
+     * @param columnDef the column definition
+     * @return <code>true</code> if the specified column is restricted by an EQ restiction, <code>false</code>
+     * otherwise.
+     */
+    public boolean isColumnRestrictedByEq(ColumnDefinition columnDef)
     {
-        return hasRegularColumnsRestriction;
+        Set<Restriction> restrictions = getRestrictions(columnDef.kind).getRestrictions(columnDef);
+        return restrictions.stream()
+                           .filter(SingleRestriction.class::isInstance)
+                           .anyMatch(p -> ((SingleRestriction) p).isEQ());
+    }
+
+    /**
+     * Returns the <code>Restrictions</code> for the specified type of columns.
+     *
+     * @param kind the column type
+     * @return the <code>Restrictions</code> for the specified type of columns
+     */
+    private Restrictions getRestrictions(ColumnDefinition.Kind kind)
+    {
+        switch (kind)
+        {
+            case PARTITION_KEY: return partitionKeyRestrictions;
+            case CLUSTERING: return clusteringColumnsRestrictions;
+            default: return nonPrimaryKeyRestrictions;
+        }
     }
 
     /**
@@ -383,36 +409,47 @@
         return this.usesSecondaryIndexing;
     }
 
-    private void processPartitionKeyRestrictions(boolean hasQueriableIndex)
+    private void processPartitionKeyRestrictions(boolean hasQueriableIndex, boolean allowFiltering, boolean forView)
     {
         if (!type.allowPartitionKeyRanges())
         {
             checkFalse(partitionKeyRestrictions.isOnToken(),
                        "The token function cannot be used in WHERE clauses for %s statements", type);
 
-            if (hasUnrestrictedPartitionKeyComponents())
+            if (partitionKeyRestrictions.hasUnrestrictedPartitionKeyComponents(cfm))
                 throw invalidRequest("Some partition key parts are missing: %s",
                                      Joiner.on(", ").join(getPartitionKeyUnrestrictedComponents()));
+
+            // slice query
+            checkFalse(partitionKeyRestrictions.hasSlice(),
+                    "Only EQ and IN relation are supported on the partition key (unless you use the token() function)"
+                            + " for %s statements", type);
         }
         else
         {
-        // If there is a queriable index, no special condition are required on the other restrictions.
-        // But we still need to know 2 things:
-        // - If we don't have a queriable index, is the query ok
-        // - Is it queriable without 2ndary index, which is always more efficient
-        // If a component of the partition key is restricted by a relation, all preceding
-        // components must have a EQ. Only the last partition key component can be in IN relation.
-        if (partitionKeyRestrictions.isOnToken())
-            isKeyRange = true;
+            // If there are no partition restrictions or there's only token restriction, we have to set a key range
+            if (partitionKeyRestrictions.isOnToken())
+                isKeyRange = true;
 
-            if (hasUnrestrictedPartitionKeyComponents())
+            if (partitionKeyRestrictions.isEmpty() && partitionKeyRestrictions.hasUnrestrictedPartitionKeyComponents(cfm))
             {
-                if (!partitionKeyRestrictions.isEmpty())
-                {
-                    if (!hasQueriableIndex)
-                        throw invalidRequest("Partition key parts: %s must be restricted as other parts are",
-                                             Joiner.on(", ").join(getPartitionKeyUnrestrictedComponents()));
-                }
+                isKeyRange = true;
+                usesSecondaryIndexing = hasQueriableIndex;
+            }
+
+            // If there is a queriable index, no special condition is required on the other restrictions.
+            // But we still need to know 2 things:
+            // - If we don't have a queriable index, is the query ok
+            // - Is it queriable without 2ndary index, which is always more efficient
+            // If a component of the partition key is restricted by a relation, all preceding
+            // components must have a EQ. Only the last partition key component can be in IN relation.
+            if (partitionKeyRestrictions.needFiltering(cfm))
+            {
+                if (!allowFiltering && !forView && !hasQueriableIndex)
+                    throw new InvalidRequestException(REQUIRES_ALLOW_FILTERING_MESSAGE);
+
+                if (partitionKeyRestrictions.hasIN())
+                    throw new InvalidRequestException("IN restrictions are not supported when the query involves filtering");
 
                 isKeyRange = true;
                 usesSecondaryIndexing = hasQueriableIndex;
@@ -420,15 +457,6 @@
         }
     }
 
-    /**
-     * Checks if the partition key has some unrestricted components.
-     * @return <code>true</code> if the partition key has some unrestricted components, <code>false</code> otherwise.
-     */
-    private boolean hasUnrestrictedPartitionKeyComponents()
-    {
-        return partitionKeyRestrictions.size() <  cfm.partitionKeyColumns().size();
-    }
-
     public boolean hasPartitionKeyRestrictions()
     {
         return !partitionKeyRestrictions.isEmpty();
@@ -473,7 +501,7 @@
      */
     public boolean clusteringKeyRestrictionsHasIN()
     {
-        return clusteringColumnsRestrictions.isIN();
+        return clusteringColumnsRestrictions.hasIN();
     }
 
     /**
@@ -482,20 +510,19 @@
      * @param hasQueriableIndex <code>true</code> if some of the queried data are indexed, <code>false</code> otherwise
      * @param selectsOnlyStaticColumns <code>true</code> if the selected or modified columns are all statics,
      * <code>false</code> otherwise.
-     * @param selectACollection <code>true</code> if the query should return a collection column
+     * @param selectsComplexColumn <code>true</code> if the query should return a collection column
      */
     private void processClusteringColumnsRestrictions(boolean hasQueriableIndex,
                                                       boolean selectsOnlyStaticColumns,
-                                                      boolean selectACollection,
-                                                      boolean forView) throws InvalidRequestException
+                                                      boolean selectsComplexColumn,
+                                                      boolean forView,
+                                                      boolean allowFiltering)
     {
-        validateClusteringRestrictions(hasQueriableIndex);
-
-        checkFalse(!type.allowClusteringColumnSlices() && clusteringColumnsRestrictions.isSlice(),
+        checkFalse(!type.allowClusteringColumnSlices() && clusteringColumnsRestrictions.hasSlice(),
                    "Slice restrictions are not supported on the clustering columns in %s statements", type);
 
         if (!type.allowClusteringColumnSlices()
-               && (!cfm.isCompactTable() || (cfm.isCompactTable() && !hasClusteringColumnsRestriction())))
+               && (!cfm.isCompactTable() || (cfm.isCompactTable() && !hasClusteringColumnsRestrictions())))
         {
             if (!selectsOnlyStaticColumns && hasUnrestrictedClusteringColumns())
                 throw invalidRequest("Some clustering keys are missing: %s",
@@ -503,74 +530,39 @@
         }
         else
         {
-            checkFalse(clusteringColumnsRestrictions.isIN() && selectACollection,
+            checkFalse(clusteringColumnsRestrictions.hasIN() && selectsComplexColumn,
                        "Cannot restrict clustering columns by IN relations when a collection is selected by the query");
-            checkFalse(clusteringColumnsRestrictions.isContains() && !hasQueriableIndex,
-                       "Cannot restrict clustering columns by a CONTAINS relation without a secondary index");
+            checkFalse(clusteringColumnsRestrictions.hasContains() && !hasQueriableIndex && !allowFiltering,
+                       "Clustering columns can only be restricted with CONTAINS with a secondary index or filtering");
 
-            if (hasClusteringColumnsRestriction() && clusteringRestrictionsNeedFiltering())
+            if (hasClusteringColumnsRestrictions() && clusteringColumnsRestrictions.needFiltering())
             {
                 if (hasQueriableIndex || forView)
                 {
                     usesSecondaryIndexing = true;
-                    return;
                 }
-
-                List<ColumnDefinition> clusteringColumns = cfm.clusteringColumns();
-                List<ColumnDefinition> restrictedColumns = new LinkedList<>(clusteringColumnsRestrictions.getColumnDefs());
-
-                for (int i = 0, m = restrictedColumns.size(); i < m; i++)
+                else if (!allowFiltering)
                 {
-                    ColumnDefinition clusteringColumn = clusteringColumns.get(i);
-                    ColumnDefinition restrictedColumn = restrictedColumns.get(i);
+                    List<ColumnDefinition> clusteringColumns = cfm.clusteringColumns();
+                    List<ColumnDefinition> restrictedColumns = new LinkedList<>(clusteringColumnsRestrictions.getColumnDefs());
 
-                    if (!clusteringColumn.equals(restrictedColumn))
+                    for (int i = 0, m = restrictedColumns.size(); i < m; i++)
                     {
-                        throw invalidRequest(
-                           "PRIMARY KEY column \"%s\" cannot be restricted as preceding column \"%s\" is not restricted",
-                            restrictedColumn.name,
-                            clusteringColumn.name);
+                        ColumnDefinition clusteringColumn = clusteringColumns.get(i);
+                        ColumnDefinition restrictedColumn = restrictedColumns.get(i);
+
+                        if (!clusteringColumn.equals(restrictedColumn))
+                        {
+                            throw invalidRequest("PRIMARY KEY column \"%s\" cannot be restricted as preceding column \"%s\" is not restricted",
+                                                 restrictedColumn.name,
+                                                 clusteringColumn.name);
+                        }
                     }
                 }
             }
+
         }
-    }
 
-    /**
-     * Validates whether or not restrictions are allowed for execution when secondary index is not used.
-     */
-    public final void validateClusteringRestrictions(boolean hasQueriableIndex)
-    {
-        assert clusteringColumnsRestrictions instanceof PrimaryKeyRestrictionSet;
-
-        // If there's a queriable index, filtering will take care of clustering restrictions
-        if (hasQueriableIndex)
-            return;
-
-        Iterator<Restriction> iter = ((PrimaryKeyRestrictionSet) clusteringColumnsRestrictions).iterator();
-        Restriction previousRestriction = null;
-        while (iter.hasNext())
-        {
-            Restriction restriction = iter.next();
-
-            if (previousRestriction != null)
-            {
-                ColumnDefinition lastRestrictionStart = previousRestriction.getFirstColumn();
-                ColumnDefinition newRestrictionStart = restriction.getFirstColumn();
-
-                if (previousRestriction.isSlice() && newRestrictionStart.position() > lastRestrictionStart.position())
-                    throw invalidRequest("Clustering column \"%s\" cannot be restricted (preceding column \"%s\" is restricted by a non-EQ relation)",
-                                         newRestrictionStart.name,
-                                         lastRestrictionStart.name);
-            }
-            previousRestriction = restriction;
-        }
-    }
-
-    public final boolean clusteringRestrictionsNeedFiltering()
-    {
-        assert clusteringColumnsRestrictions instanceof PrimaryKeyRestrictionSet;
-        return ((PrimaryKeyRestrictionSet) clusteringColumnsRestrictions).needsFiltering();
     }
 
     /**
@@ -627,19 +619,19 @@
 
         expression.prepareValue(cfm, expressionType, boundNames);
 
-        indexRestrictions.add(expression);
+        filterRestrictions.add(expression);
     }
 
     public RowFilter getRowFilter(SecondaryIndexManager indexManager, QueryOptions options)
     {
-        if (indexRestrictions.isEmpty())
+        if (filterRestrictions.isEmpty())
             return RowFilter.NONE;
 
         RowFilter filter = RowFilter.create();
-        for (Restrictions restrictions : indexRestrictions.getRestrictions())
+        for (Restrictions restrictions : filterRestrictions.getRestrictions())
             restrictions.addRowFilterTo(filter, indexManager, options);
 
-        for (CustomIndexExpression expression : indexRestrictions.getCustomIndexExpressions())
+        for (CustomIndexExpression expression : filterRestrictions.getCustomIndexExpressions())
             expression.addToRowFilter(filter, cfm, options);
 
         return filter;
@@ -665,11 +657,6 @@
      */
     private ByteBuffer getPartitionKeyBound(Bound b, QueryOptions options)
     {
-        // Deal with unrestricted partition key components (special-casing is required to deal with 2i queries on the
-        // first component of a composite partition key).
-        if (hasUnrestrictedPartitionKeyComponents())
-            return ByteBufferUtil.EMPTY_BYTE_BUFFER;
-
         // We deal with IN queries for keys in other places, so we know buildBound will return only one result
         return partitionKeyRestrictions.bounds(b, options).get(0);
     }
@@ -695,6 +682,11 @@
     private AbstractBounds<PartitionPosition> getPartitionKeyBounds(IPartitioner p,
                                                                     QueryOptions options)
     {
+        // Deal with unrestricted partition key components (special-casing is required to deal with 2i queries on the
+        // first component of a composite partition key) queries that filter on the partition key.
+        if (partitionKeyRestrictions.needFiltering(cfm))
+            return new Range<>(p.getMinimumToken().minKeyBound(), p.getMinimumToken().maxKeyBound());
+
         ByteBuffer startKeyBytes = getPartitionKeyBound(Bound.START, options);
         ByteBuffer finishKeyBytes = getPartitionKeyBound(Bound.END, options);
 
@@ -762,7 +754,7 @@
      * @return <code>true</code> if the query has some restrictions on the clustering columns,
      * <code>false</code> otherwise.
      */
-    public boolean hasClusteringColumnsRestriction()
+    public boolean hasClusteringColumnsRestrictions()
     {
         return !clusteringColumnsRestrictions.isEmpty();
     }
@@ -791,24 +783,12 @@
      * @param options the query options
      * @return the bounds (start or end) of the clustering columns
      */
-    public NavigableSet<Slice.Bound> getClusteringColumnsBounds(Bound b, QueryOptions options)
+    public NavigableSet<ClusteringBound> getClusteringColumnsBounds(Bound b, QueryOptions options)
     {
         return clusteringColumnsRestrictions.boundsAsClustering(b, options);
     }
 
     /**
-     * Checks if the bounds (start or end) of the clustering columns are inclusive.
-     *
-     * @param bound the bound type
-     * @return <code>true</code> if the bounds (start or end) of the clustering columns are inclusive,
-     * <code>false</code> otherwise
-     */
-    public boolean areRequestedBoundsInclusive(Bound bound)
-    {
-        return clusteringColumnsRestrictions.isInclusive(bound);
-    }
-
-    /**
      * Checks if the query returns a range of columns.
      *
      * @return <code>true</code> if the query returns a range of columns, <code>false</code> otherwise.
@@ -819,9 +799,9 @@
         // this would mean a 'SELECT *' on a static compact table would query whole partitions, even though we'll only return
         // the static part as far as CQL is concerned. This is thus mostly an optimization to use the query-by-name path).
         int numberOfClusteringColumns = cfm.isStaticCompactTable() ? 0 : cfm.clusteringColumns().size();
-        // it is a range query if it has at least one the column alias for which no relation is defined or is not EQ.
+        // it is a range query if it has at least one the column alias for which no relation is defined or is not EQ or IN.
         return clusteringColumnsRestrictions.size() < numberOfClusteringColumns
-            || (!clusteringColumnsRestrictions.isEQ() && !clusteringColumnsRestrictions.isIN());
+            || !clusteringColumnsRestrictions.hasOnlyEqualityRestrictions();
     }
 
     /**
@@ -830,8 +810,8 @@
      */
     public boolean needFiltering()
     {
-        int numberOfRestrictions = indexRestrictions.getCustomIndexExpressions().size();
-        for (Restrictions restrictions : indexRestrictions.getRestrictions())
+        int numberOfRestrictions = filterRestrictions.getCustomIndexExpressions().size();
+        for (Restrictions restrictions : filterRestrictions.getRestrictions())
             numberOfRestrictions += restrictions.size();
 
         return numberOfRestrictions > 1
@@ -840,24 +820,10 @@
                         && nonPrimaryKeyRestrictions.hasMultipleContains());
     }
 
-    private void validateSecondaryIndexSelections(boolean selectsOnlyStaticColumns)
+    private void validateSecondaryIndexSelections()
     {
         checkFalse(keyIsInRelation(),
                    "Select on indexed columns and with IN clause for the PRIMARY KEY are not supported");
-        // When the user only select static columns, the intent is that we don't query the whole partition but just
-        // the static parts. But 1) we don't have an easy way to do that with 2i and 2) since we don't support index on
-        // static columns so far, 2i means that you've restricted a non static column, so the query is somewhat
-        // non-sensical.
-        // Note: an exception is if the index is a KEYS one. Which can happen if the user had a KEYS index on
-        // a compact table, and subsequently DROP COMPACT STORAGE on that table. After which, the KEYS index will still
-        // work, but queries will effectively be only on now-static columns and we should let this work.
-        checkFalse(selectsOnlyStaticColumns && !hasKeysIndex(cfm),
-                   "Queries using 2ndary indexes don't support selecting only static columns");
-    }
-
-    private boolean hasKeysIndex(CFMetaData cfm)
-    {
-        return Iterables.any(cfm.getIndexes(), i -> i.kind == IndexMetadata.Kind.KEYS);
     }
 
     /**
@@ -869,12 +835,20 @@
     public boolean hasAllPKColumnsRestrictedByEqualities()
     {
         return !isPartitionKeyRestrictionsOnToken()
-               && !hasUnrestrictedPartitionKeyComponents()
-               && (partitionKeyRestrictions.isEQ() || partitionKeyRestrictions.isIN())
-               && !hasUnrestrictedClusteringColumns()
-               && (clusteringColumnsRestrictions.isEQ() || clusteringColumnsRestrictions.isIN());
+                && !partitionKeyRestrictions.hasUnrestrictedPartitionKeyComponents(cfm)
+                && (partitionKeyRestrictions.hasOnlyEqualityRestrictions())
+                && !hasUnrestrictedClusteringColumns()
+                && (clusteringColumnsRestrictions.hasOnlyEqualityRestrictions());
     }
 
+    /**
+     * Checks if one of the restrictions applies to a regular column.
+     * @return {@code true} if one of the restrictions applies to a regular column, {@code false} otherwise.
+     */
+    public boolean hasRegularColumnsRestrictions()
+    {
+        return hasRegularColumnsRestrictions;
+    }
 
     private SuperColumnCompatibility.SuperColumnRestrictions cached;
     public SuperColumnCompatibility.SuperColumnRestrictions getSuperColumnRestrictions()
@@ -882,7 +856,7 @@
         assert cfm.isSuper() && cfm.isDense();
 
         if (cached == null)
-            cached = new SuperColumnCompatibility.SuperColumnRestrictions(Iterators.concat(((PrimaryKeyRestrictionSet) clusteringColumnsRestrictions).iterator(),
+            cached = new SuperColumnCompatibility.SuperColumnRestrictions(Iterators.concat(clusteringColumnsRestrictions.iterator(),
                                                                                            nonPrimaryKeyRestrictions.iterator()));
         return cached;
     }
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/TermSlice.java b/src/java/org/apache/cassandra/cql3/restrictions/TermSlice.java
index 4b13877..c7a6daf 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/TermSlice.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/TermSlice.java
@@ -62,7 +62,7 @@
      */
     public static TermSlice newInstance(Bound bound, boolean include, Term term)
     {
-        return  bound.isStart() ? new TermSlice(term, include, null, false) 
+        return  bound.isStart() ? new TermSlice(term, include, null, false)
                                 : new TermSlice(null, false, term, include);
     }
 
@@ -112,14 +112,14 @@
         {
             assert !otherSlice.hasBound(Bound.START);
 
-            return new TermSlice(bound(Bound.START), 
+            return new TermSlice(bound(Bound.START),
                                   isInclusive(Bound.START),
                                   otherSlice.bound(Bound.END),
                                   otherSlice.isInclusive(Bound.END));
         }
         assert !otherSlice.hasBound(Bound.END);
 
-        return new TermSlice(otherSlice.bound(Bound.START), 
+        return new TermSlice(otherSlice.bound(Bound.START),
                               otherSlice.isInclusive(Bound.START),
                               bound(Bound.END),
                               isInclusive(Bound.END));
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/TokenFilter.java b/src/java/org/apache/cassandra/cql3/restrictions/TokenFilter.java
index 3258b26..13b3e3e 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/TokenFilter.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/TokenFilter.java
@@ -25,51 +25,76 @@
 import com.google.common.collect.Range;
 import com.google.common.collect.RangeSet;
 
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.cql3.statements.Bound;
-import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.filter.RowFilter;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.index.SecondaryIndexManager;
 
 import static org.apache.cassandra.cql3.statements.Bound.END;
 import static org.apache.cassandra.cql3.statements.Bound.START;
 
 /**
  * <code>Restriction</code> decorator used to merge non-token restriction and token restriction on partition keys.
+ *
+ * <p>If all partition key columns have non-token restrictions and do not need filtering, they take precedence
+ * when calculating bounds, incusiveness etc (see CASSANDRA-12149).</p>
  */
-final class TokenFilter extends ForwardingPrimaryKeyRestrictions
+final class TokenFilter implements PartitionKeyRestrictions
 {
     /**
      * The decorated restriction
      */
-    private PrimaryKeyRestrictions restrictions;
+    private final PartitionKeyRestrictions restrictions;
 
     /**
      * The restriction on the token
      */
-    private TokenRestriction tokenRestriction;
+    private final TokenRestriction tokenRestriction;
 
     /**
      * Partitioner to manage tokens, extracted from tokenRestriction metadata.
      */
     private final IPartitioner partitioner;
 
-    @Override
-    protected PrimaryKeyRestrictions getDelegate()
+    public boolean hasIN()
     {
-        return restrictions;
+        return isOnToken() ? false : restrictions.hasIN();
+    }
+
+    public boolean hasContains()
+    {
+        return isOnToken() ? false : restrictions.hasContains();
+    }
+
+    public boolean hasOnlyEqualityRestrictions()
+    {
+        return isOnToken() ? false : restrictions.hasOnlyEqualityRestrictions();
+    }
+
+    @Override
+    public Set<Restriction> getRestrictions(ColumnDefinition columnDef)
+    {
+        Set<Restriction> set = new HashSet<>();
+        set.addAll(restrictions.getRestrictions(columnDef));
+        set.addAll(tokenRestriction.getRestrictions(columnDef));
+        return set;
     }
 
     @Override
     public boolean isOnToken()
     {
-        // if all partition key columns have non-token restrictions, we can simply use the token range to filter
-        // those restrictions and then ignore the token range
-        return restrictions.size() < tokenRestriction.size();
+        // if all partition key columns have non-token restrictions and do not need filtering,
+        // we can simply use the token range to filter those restrictions and then ignore the token range
+        return needFiltering(tokenRestriction.metadata) || restrictions.size() < tokenRestriction.size();
     }
 
-    public TokenFilter(PrimaryKeyRestrictions restrictions, TokenRestriction tokenRestriction)
+    public TokenFilter(PartitionKeyRestrictions restrictions, TokenRestriction tokenRestriction)
     {
         this.restrictions = restrictions;
         this.tokenRestriction = tokenRestriction;
@@ -83,42 +108,30 @@
     }
 
     @Override
-    public NavigableSet<Clustering> valuesAsClustering(QueryOptions options) throws InvalidRequestException
-    {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public PrimaryKeyRestrictions mergeWith(Restriction restriction) throws InvalidRequestException
+    public PartitionKeyRestrictions mergeWith(Restriction restriction) throws InvalidRequestException
     {
         if (restriction.isOnToken())
             return new TokenFilter(restrictions, (TokenRestriction) tokenRestriction.mergeWith(restriction));
 
-        return new TokenFilter(super.mergeWith(restriction), tokenRestriction);
+        return new TokenFilter(restrictions.mergeWith(restriction), tokenRestriction);
     }
 
     @Override
     public boolean isInclusive(Bound bound)
     {
-        return tokenRestriction.isInclusive(bound);
+        return isOnToken() ? tokenRestriction.isInclusive(bound) : restrictions.isInclusive(bound);
     }
 
     @Override
-    public boolean hasBound(Bound b)
+    public boolean hasBound(Bound bound)
     {
-        return tokenRestriction.hasBound(b);
+        return isOnToken() ? tokenRestriction.hasBound(bound) : restrictions.hasBound(bound);
     }
 
     @Override
     public List<ByteBuffer> bounds(Bound bound, QueryOptions options) throws InvalidRequestException
     {
-        return tokenRestriction.bounds(bound, options);
-    }
-
-    @Override
-    public NavigableSet<Slice.Bound> boundsAsClustering(Bound bound, QueryOptions options) throws InvalidRequestException
-    {
-        return tokenRestriction.boundsAsClustering(bound, options);
+        return isOnToken() ? tokenRestriction.bounds(bound, options) : restrictions.bounds(bound, options);
     }
 
     /**
@@ -131,8 +144,8 @@
      */
     private List<ByteBuffer> filter(List<ByteBuffer> values, QueryOptions options) throws InvalidRequestException
     {
-        RangeSet<Token> rangeSet = tokenRestriction.isSlice() ? toRangeSet(tokenRestriction, options)
-                                                              : toRangeSet(tokenRestriction.values(options));
+        RangeSet<Token> rangeSet = tokenRestriction.hasSlice() ? toRangeSet(tokenRestriction, options)
+                                                               : toRangeSet(tokenRestriction.values(options));
 
         return filterWithRangeSet(rangeSet, values);
     }
@@ -233,4 +246,70 @@
     {
         return inclusive ? BoundType.CLOSED : BoundType.OPEN;
     }
+
+    @Override
+    public ColumnDefinition getFirstColumn()
+    {
+        return restrictions.getFirstColumn();
+    }
+
+    @Override
+    public ColumnDefinition getLastColumn()
+    {
+        return restrictions.getLastColumn();
+    }
+
+    @Override
+    public List<ColumnDefinition> getColumnDefs()
+    {
+        return restrictions.getColumnDefs();
+    }
+
+    @Override
+    public void addFunctionsTo(List<Function> functions)
+    {
+        restrictions.addFunctionsTo(functions);
+    }
+
+    @Override
+    public boolean hasSupportingIndex(SecondaryIndexManager indexManager)
+    {
+        return restrictions.hasSupportingIndex(indexManager);
+    }
+
+    @Override
+    public void addRowFilterTo(RowFilter filter, SecondaryIndexManager indexManager, QueryOptions options)
+    {
+        restrictions.addRowFilterTo(filter, indexManager, options);
+    }
+
+    @Override
+    public boolean isEmpty()
+    {
+        return restrictions.isEmpty();
+    }
+
+    @Override
+    public int size()
+    {
+        return restrictions.size();
+    }
+
+    @Override
+    public boolean needFiltering(CFMetaData cfm)
+    {
+        return restrictions.needFiltering(cfm);
+    }
+
+    @Override
+    public boolean hasUnrestrictedPartitionKeyComponents(CFMetaData cfm)
+    {
+        return restrictions.hasUnrestrictedPartitionKeyComponents(cfm);
+    }
+
+    @Override
+    public boolean hasSlice()
+    {
+        return restrictions.hasSlice();
+    }
 }
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/TokenRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/TokenRestriction.java
index 14d2cb7..82b27dd 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/TokenRestriction.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/TokenRestriction.java
@@ -28,9 +28,6 @@
 import org.apache.cassandra.cql3.Term;
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.cql3.statements.Bound;
-import org.apache.cassandra.db.Clustering;
-import org.apache.cassandra.db.MultiCBuilder;
-import org.apache.cassandra.db.Slice;
 import org.apache.cassandra.db.filter.RowFilter;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.index.SecondaryIndexManager;
@@ -40,14 +37,14 @@
 /**
  * <code>Restriction</code> using the token function.
  */
-public abstract class TokenRestriction extends AbstractPrimaryKeyRestrictions
+public abstract class TokenRestriction implements PartitionKeyRestrictions
 {
     /**
      * The definition of the columns to which apply the token restriction.
      */
     protected final List<ColumnDefinition> columnDefs;
 
-    final CFMetaData metadata;
+    protected final CFMetaData metadata;
 
     /**
      * Creates a new <code>TokenRestriction</code> that apply to the specified columns.
@@ -56,18 +53,51 @@
      */
     public TokenRestriction(CFMetaData metadata, List<ColumnDefinition> columnDefs)
     {
-        super(metadata.getKeyValidatorAsClusteringComparator());
         this.columnDefs = columnDefs;
         this.metadata = metadata;
     }
 
+    public boolean hasIN()
+    {
+        return false;
+    }
+
+    public boolean hasOnlyEqualityRestrictions()
+    {
+        return false;
+    }
+
     @Override
-    public  boolean isOnToken()
+    public Set<Restriction> getRestrictions(ColumnDefinition columnDef)
+    {
+        return Collections.singleton(this);
+    }
+
+    @Override
+    public final boolean isOnToken()
     {
         return true;
     }
 
     @Override
+    public boolean needFiltering(CFMetaData cfm)
+    {
+        return false;
+    }
+
+    @Override
+    public boolean hasSlice()
+    {
+        return false;
+    }
+
+    @Override
+    public boolean hasUnrestrictedPartitionKeyComponents(CFMetaData cfm)
+    {
+        return false;
+    }
+
+    @Override
     public List<ColumnDefinition> getColumnDefs()
     {
         return columnDefs;
@@ -98,21 +128,15 @@
     }
 
     @Override
-    public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options)
+    public final boolean isEmpty()
     {
-        throw new UnsupportedOperationException();
+        return getColumnDefs().isEmpty();
     }
 
     @Override
-    public NavigableSet<Clustering> valuesAsClustering(QueryOptions options) throws InvalidRequestException
+    public final int size()
     {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public NavigableSet<Slice.Bound> boundsAsClustering(Bound bound, QueryOptions options) throws InvalidRequestException
-    {
-        throw new UnsupportedOperationException();
+        return getColumnDefs().size();
     }
 
     /**
@@ -126,10 +150,10 @@
     }
 
     @Override
-    public final PrimaryKeyRestrictions mergeWith(Restriction otherRestriction) throws InvalidRequestException
+    public final PartitionKeyRestrictions mergeWith(Restriction otherRestriction) throws InvalidRequestException
     {
         if (!otherRestriction.isOnToken())
-            return new TokenFilter(toPrimaryKeyRestriction(otherRestriction), this);
+            return new TokenFilter(toPartitionKeyRestrictions(otherRestriction), this);
 
         return doMergeWith((TokenRestriction) otherRestriction);
     }
@@ -138,21 +162,21 @@
      * Merges this restriction with the specified <code>TokenRestriction</code>.
      * @param otherRestriction the <code>TokenRestriction</code> to merge with.
      */
-    protected abstract PrimaryKeyRestrictions doMergeWith(TokenRestriction otherRestriction) throws InvalidRequestException;
+    protected abstract PartitionKeyRestrictions doMergeWith(TokenRestriction otherRestriction) throws InvalidRequestException;
 
     /**
-     * Converts the specified restriction into a <code>PrimaryKeyRestrictions</code>.
+     * Converts the specified restriction into a <code>PartitionKeyRestrictions</code>.
      *
      * @param restriction the restriction to convert
-     * @return a <code>PrimaryKeyRestrictions</code>
+     * @return a <code>PartitionKeyRestrictions</code>
      * @throws InvalidRequestException if a problem occurs while converting the restriction
      */
-    private PrimaryKeyRestrictions toPrimaryKeyRestriction(Restriction restriction) throws InvalidRequestException
+    private PartitionKeyRestrictions toPartitionKeyRestrictions(Restriction restriction) throws InvalidRequestException
     {
-        if (restriction instanceof PrimaryKeyRestrictions)
-            return (PrimaryKeyRestrictions) restriction;
+        if (restriction instanceof PartitionKeyRestrictions)
+            return (PartitionKeyRestrictions) restriction;
 
-        return new PrimaryKeyRestrictionSet(comparator, true).mergeWith(restriction);
+        return new PartitionKeySingleRestrictionSet(metadata.getKeyValidatorAsClusteringComparator()).mergeWith(restriction);
     }
 
     public static final class EQRestriction extends TokenRestriction
@@ -166,29 +190,46 @@
         }
 
         @Override
-        public boolean isEQ()
-        {
-            return true;
-        }
-
-        @Override
         public void addFunctionsTo(List<Function> functions)
         {
             value.addFunctionsTo(functions);
         }
 
         @Override
-        protected PrimaryKeyRestrictions doMergeWith(TokenRestriction otherRestriction) throws InvalidRequestException
+        protected PartitionKeyRestrictions doMergeWith(TokenRestriction otherRestriction) throws InvalidRequestException
         {
             throw invalidRequest("%s cannot be restricted by more than one relation if it includes an Equal",
                                  Joiner.on(", ").join(ColumnDefinition.toIdentifiers(columnDefs)));
         }
 
         @Override
+        public List<ByteBuffer> bounds(Bound b, QueryOptions options) throws InvalidRequestException
+        {
+            return values(options);
+        }
+
+        @Override
+        public boolean hasBound(Bound b)
+        {
+            return true;
+        }
+
+        @Override
+        public boolean isInclusive(Bound b)
+        {
+            return true;
+        }
+
+        @Override
         public List<ByteBuffer> values(QueryOptions options) throws InvalidRequestException
         {
             return Collections.singletonList(value.bindAndGet(options));
         }
+
+        public boolean hasContains()
+        {
+            return false;
+        }
     }
 
     public static class SliceRestriction extends TokenRestriction
@@ -201,8 +242,13 @@
             slice = TermSlice.newInstance(bound, inclusive, term);
         }
 
+        public boolean hasContains()
+        {
+            return false;
+        }
+
         @Override
-        public boolean isSlice()
+        public boolean hasSlice()
         {
             return true;
         }
@@ -238,10 +284,10 @@
         }
 
         @Override
-        protected PrimaryKeyRestrictions doMergeWith(TokenRestriction otherRestriction)
+        protected PartitionKeyRestrictions doMergeWith(TokenRestriction otherRestriction)
         throws InvalidRequestException
         {
-            if (!otherRestriction.isSlice())
+            if (!(otherRestriction instanceof SliceRestriction))
                 throw invalidRequest("Columns \"%s\" cannot be restricted by both an equality and an inequality relation",
                                      getColumnNamesAsString());
 
diff --git a/src/java/org/apache/cassandra/cql3/selection/AbstractFunctionSelector.java b/src/java/org/apache/cassandra/cql3/selection/AbstractFunctionSelector.java
index c48b93c..498cf0f 100644
--- a/src/java/org/apache/cassandra/cql3/selection/AbstractFunctionSelector.java
+++ b/src/java/org/apache/cassandra/cql3/selection/AbstractFunctionSelector.java
@@ -22,12 +22,11 @@
 import java.util.List;
 
 import org.apache.commons.lang3.text.StrBuilder;
-
-import org.apache.cassandra.cql3.functions.AggregateFcts;
-
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.ColumnSpecification;
+import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.functions.Function;
+import org.apache.cassandra.cql3.statements.RequestValidations;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 
@@ -39,7 +38,7 @@
      * The list used to pass the function arguments is recycled to avoid the cost of instantiating a new list
      * with each function call.
      */
-    protected final List<ByteBuffer> args;
+    private final List<ByteBuffer> args;
     protected final List<Selector> argSelectors;
 
     public static Factory newFactory(final Function fun, final SelectorFactories factories) throws InvalidRequestException
@@ -54,13 +53,7 @@
         {
             protected String getColumnName()
             {
-                if (AggregateFcts.isCountRows(fun))
-                    return "count";
-
-                return new StrBuilder(fun.name().toString()).append('(')
-                                                            .appendWithSeparators(factories.getColumnNames(), ", ")
-                                                            .append(')')
-                                                            .toString();
+                return fun.columnName(factories.getColumnNames());
             }
 
             protected AbstractType<?> getReturnType()
@@ -89,10 +82,10 @@
                 factories.addFunctionsTo(functions);
             }
 
-            public Selector newInstance() throws InvalidRequestException
+            public Selector newInstance(QueryOptions options) throws InvalidRequestException
             {
-                return fun.isAggregate() ? new AggregateFunctionSelector(fun, factories.newInstances())
-                                         : new ScalarFunctionSelector(fun, factories.newInstances());
+                return fun.isAggregate() ? new AggregateFunctionSelector(fun, factories.newInstances(options))
+                                         : new ScalarFunctionSelector(fun, factories.newInstances(options));
             }
 
             public boolean isWritetimeSelectorFactory()
@@ -119,6 +112,19 @@
         this.args = Arrays.asList(new ByteBuffer[argSelectors.size()]);
     }
 
+    // Sets a given arg value. We should use that instead of directly setting the args list for the
+    // sake of validation.
+    protected void setArg(int i, ByteBuffer value) throws InvalidRequestException
+    {
+        RequestValidations.checkBindValueSet(value, "Invalid unset value for argument in call to function %s", fun.name().name);
+        args.set(i, value);
+    }
+
+    protected List<ByteBuffer> args()
+    {
+        return args;
+    }
+
     public AbstractType<?> getType()
     {
         return fun.returnType();
diff --git a/src/java/org/apache/cassandra/cql3/selection/AggregateFunctionSelector.java b/src/java/org/apache/cassandra/cql3/selection/AggregateFunctionSelector.java
index 27a8294..e3e5328 100644
--- a/src/java/org/apache/cassandra/cql3/selection/AggregateFunctionSelector.java
+++ b/src/java/org/apache/cassandra/cql3/selection/AggregateFunctionSelector.java
@@ -24,6 +24,7 @@
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.cql3.selection.Selection.ResultSetBuilder;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 final class AggregateFunctionSelector extends AbstractFunctionSelector<AggregateFunction>
 {
@@ -34,20 +35,20 @@
         return true;
     }
 
-    public void addInput(int protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
+    public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
     {
         // Aggregation of aggregation is not supported
         for (int i = 0, m = argSelectors.size(); i < m; i++)
         {
             Selector s = argSelectors.get(i);
             s.addInput(protocolVersion, rs);
-            args.set(i, s.getOutput(protocolVersion));
+            setArg(i, s.getOutput(protocolVersion));
             s.reset();
         }
-        this.aggregate.addInput(protocolVersion, args);
+        this.aggregate.addInput(protocolVersion, args());
     }
 
-    public ByteBuffer getOutput(int protocolVersion) throws InvalidRequestException
+    public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
     {
         return aggregate.compute(protocolVersion);
     }
diff --git a/src/java/org/apache/cassandra/cql3/selection/FieldSelector.java b/src/java/org/apache/cassandra/cql3/selection/FieldSelector.java
index 63b6cc6..d4b74ae 100644
--- a/src/java/org/apache/cassandra/cql3/selection/FieldSelector.java
+++ b/src/java/org/apache/cassandra/cql3/selection/FieldSelector.java
@@ -20,11 +20,12 @@
 import java.nio.ByteBuffer;
 
 import org.apache.cassandra.cql3.ColumnSpecification;
+import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.selection.Selection.ResultSetBuilder;
 import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.db.marshal.UserType;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 final class FieldSelector extends Selector
 {
@@ -38,9 +39,7 @@
         {
             protected String getColumnName()
             {
-                return String.format("%s.%s",
-                                     factory.getColumnName(),
-                                     UTF8Type.instance.getString(type.fieldName(field)));
+                return String.format("%s.%s", factory.getColumnName(), type.fieldName(field));
             }
 
             protected AbstractType<?> getReturnType()
@@ -53,9 +52,9 @@
                 factory.addColumnMapping(mapping, resultsColumn);
             }
 
-            public Selector newInstance() throws InvalidRequestException
+            public Selector newInstance(QueryOptions options) throws InvalidRequestException
             {
-                return new FieldSelector(type, field, factory.newInstance());
+                return new FieldSelector(type, field, factory.newInstance(options));
             }
 
             public boolean isAggregateSelectorFactory()
@@ -65,17 +64,12 @@
         };
     }
 
-    public boolean isAggregate()
-    {
-        return false;
-    }
-
-    public void addInput(int protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
+    public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
     {
         selected.addInput(protocolVersion, rs);
     }
 
-    public ByteBuffer getOutput(int protocolVersion) throws InvalidRequestException
+    public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
     {
         ByteBuffer value = selected.getOutput(protocolVersion);
         if (value == null)
@@ -97,7 +91,7 @@
     @Override
     public String toString()
     {
-        return String.format("%s.%s", selected, UTF8Type.instance.getString(type.fieldName(field)));
+        return String.format("%s.%s", selected, type.fieldName(field));
     }
 
     private FieldSelector(UserType type, int field, Selector selected)
diff --git a/src/java/org/apache/cassandra/cql3/selection/ScalarFunctionSelector.java b/src/java/org/apache/cassandra/cql3/selection/ScalarFunctionSelector.java
index bb56bb8..c05cdaa 100644
--- a/src/java/org/apache/cassandra/cql3/selection/ScalarFunctionSelector.java
+++ b/src/java/org/apache/cassandra/cql3/selection/ScalarFunctionSelector.java
@@ -24,6 +24,7 @@
 import org.apache.cassandra.cql3.functions.ScalarFunction;
 import org.apache.cassandra.cql3.selection.Selection.ResultSetBuilder;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 final class ScalarFunctionSelector extends AbstractFunctionSelector<ScalarFunction>
 {
@@ -36,7 +37,7 @@
         return argSelectors.get(0).isAggregate();
     }
 
-    public void addInput(int protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
+    public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
     {
         for (int i = 0, m = argSelectors.size(); i < m; i++)
         {
@@ -49,19 +50,19 @@
     {
     }
 
-    public ByteBuffer getOutput(int protocolVersion) throws InvalidRequestException
+    public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
     {
         for (int i = 0, m = argSelectors.size(); i < m; i++)
         {
             Selector s = argSelectors.get(i);
-            args.set(i, s.getOutput(protocolVersion));
+            setArg(i, s.getOutput(protocolVersion));
             s.reset();
         }
-        return fun.execute(protocolVersion, args);
+        return fun.execute(protocolVersion, args());
     }
 
     ScalarFunctionSelector(Function fun, List<Selector> argSelectors)
     {
         super((ScalarFunction) fun, argSelectors);
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/cql3/selection/Selectable.java b/src/java/org/apache/cassandra/cql3/selection/Selectable.java
index 6297f64..5cb9b6c 100644
--- a/src/java/org/apache/cassandra/cql3/selection/Selectable.java
+++ b/src/java/org/apache/cassandra/cql3/selection/Selectable.java
@@ -19,24 +19,41 @@
 package org.apache.cassandra.cql3.selection;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import org.apache.commons.lang3.text.StrBuilder;
-
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.cql3.functions.*;
-import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 
-public abstract class Selectable
+public interface Selectable extends AssignmentTestable
 {
-    public abstract Selector.Factory newSelectorFactory(CFMetaData cfm, List<ColumnDefinition> defs)
-            throws InvalidRequestException;
+    public Selector.Factory newSelectorFactory(CFMetaData cfm, AbstractType<?> expectedType, List<ColumnDefinition> defs, VariableSpecifications boundNames);
 
-    protected static int addAndGetIndex(ColumnDefinition def, List<ColumnDefinition> l)
+    /**
+     * The type of the {@code Selectable} if it can be infered.
+     *
+     * @param keyspace the keyspace on which the statement for which this is a
+     * {@code Selectable} is on.
+     * @return the type of this {@code Selectable} if inferrable, or {@code null}
+     * otherwise (for instance, the type isn't inferable for a bind marker. Even for
+     * literals, the exact type is not inferrable since they are valid for many
+     * different types and so this will return {@code null} too).
+     */
+    public AbstractType<?> getExactTypeIfKnown(String keyspace);
+
+    // Term.Raw overrides this since some literals can be WEAKLY_ASSIGNABLE
+    default public TestResult testAssignment(String keyspace, ColumnSpecification receiver)
+    {
+        AbstractType<?> type = getExactTypeIfKnown(keyspace);
+        return type == null ? TestResult.NOT_ASSIGNABLE : type.testAssignment(keyspace, receiver);
+    }
+
+    default int addAndGetIndex(ColumnDefinition def, List<ColumnDefinition> l)
     {
         int idx = l.indexOf(def);
         if (idx < 0)
@@ -47,58 +64,162 @@
         return idx;
     }
 
-    public static interface Raw
+    public static abstract class Raw
     {
-        public Selectable prepare(CFMetaData cfm);
+        public abstract Selectable prepare(CFMetaData cfm);
 
         /**
          * Returns true if any processing is performed on the selected column.
          **/
-        public boolean processesSelection();
+        public boolean processesSelection()
+        {
+            // ColumnIdentifier is the only case that returns false and override this
+            return true;
+        }
     }
 
-    public static class WritetimeOrTTL extends Selectable
+    public static class WithTerm implements Selectable
     {
-        public final ColumnIdentifier id;
+        /**
+         * The names given to unamed bind markers found in selection. In selection clause, we often don't have a good
+         * name for bind markers, typically if you have:
+         *   SELECT (int)? FROM foo;
+         * there isn't a good name for that marker. So we give the same name to all the markers. Note that we could try
+         * to differenciate the names by using some increasing number in the name (so [selection_1], [selection_2], ...)
+         * but it's actually not trivial to do in the current code and it's not really more helpful since if users wants
+         * to bind by position (which they will have to in this case), they can do so at the driver level directly. And
+         * so we don't bother.
+         * Note that users should really be using named bind markers if they want to be able to bind by names.
+         */
+        private static final ColumnIdentifier bindMarkerNameInSelection = new ColumnIdentifier("[selection]", true);
+
+        private final Term.Raw rawTerm;
+
+        public WithTerm(Term.Raw rawTerm)
+        {
+            this.rawTerm = rawTerm;
+        }
+
+        @Override
+        public TestResult testAssignment(String keyspace, ColumnSpecification receiver)
+        {
+            return rawTerm.testAssignment(keyspace, receiver);
+        }
+
+        public Selector.Factory newSelectorFactory(CFMetaData cfm, AbstractType<?> expectedType, List<ColumnDefinition> defs, VariableSpecifications boundNames) throws InvalidRequestException
+        {
+            /*
+             * expectedType will be null if we have no constraint on what the type should be. For instance, if this term is a bind marker:
+             *   - it will be null if we do "SELECT ? FROM foo"
+             *   - it won't be null (and be LongType) if we do "SELECT bigintAsBlob(?) FROM foo" because the function constrain it.
+             *
+             * In the first case, we have to error out: we need to infer the type of the metadata of a SELECT at preparation time, which we can't
+             * here (users will have to do "SELECT (varint)? FROM foo" for instance).
+             * But in the 2nd case, we're fine and can use the expectedType to "prepare" the bind marker/collect the bound type.
+             *
+             * Further, the term might not be a bind marker, in which case we sometimes can default to some most-general type. For instance, in
+             *   SELECT 3 FROM foo
+             * we'll just default the type to 'varint' as that's the most generic type for the literal '3' (this is mostly for convenience, the query
+             * is not terribly useful in practice and use can force the type as for the bind marker case through "SELECT (int)3 FROM foo").
+             * But note that not all literals can have such default type. For instance, there is no way to infer the type of a UDT literal in a vacuum,
+             * and so we simply error out if we have something like:
+             *   SELECT { foo: 'bar' } FROM foo
+             *
+             * Lastly, note that if the term is a terminal literal, we don't have to check it's compatibility with 'expectedType' as any incompatibility
+             * would have been found at preparation time.
+             */
+            AbstractType<?> type = getExactTypeIfKnown(cfm.ksName);
+            if (type == null)
+            {
+                type = expectedType;
+                if (type == null)
+                    throw new InvalidRequestException("Cannot infer type for term " + this + " in selection clause (try using a cast to force a type)");
+            }
+
+            // The fact we default the name to "[selection]" inconditionally means that any bind marker in a
+            // selection will have this name. Which isn't terribly helpful, but it's unclear how to provide
+            // something a lot more helpful and in practice user can bind those markers by position or, even better,
+            // use bind markers.
+            Term term = rawTerm.prepare(cfm.ksName, new ColumnSpecification(cfm.ksName, cfm.cfName, bindMarkerNameInSelection, type));
+            term.collectMarkerSpecification(boundNames);
+            return TermSelector.newFactory(rawTerm.getText(), term, type);
+        }
+
+        @Override
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            return rawTerm.getExactTypeIfKnown(keyspace);
+        }
+
+        @Override
+        public String toString()
+        {
+            return rawTerm.toString();
+        }
+
+        public static class Raw extends Selectable.Raw
+        {
+            private final Term.Raw term;
+
+            public Raw(Term.Raw term)
+            {
+                this.term = term;
+            }
+
+            public Selectable prepare(CFMetaData cfm)
+            {
+                return new WithTerm(term);
+            }
+        }
+    }
+
+    public static class WritetimeOrTTL implements Selectable
+    {
+        public final ColumnDefinition column;
         public final boolean isWritetime;
 
-        public WritetimeOrTTL(ColumnIdentifier id, boolean isWritetime)
+        public WritetimeOrTTL(ColumnDefinition column, boolean isWritetime)
         {
-            this.id = id;
+            this.column = column;
             this.isWritetime = isWritetime;
         }
 
         @Override
         public String toString()
         {
-            return (isWritetime ? "writetime" : "ttl") + "(" + id + ")";
+            return (isWritetime ? "writetime" : "ttl") + "(" + column.name + ")";
         }
 
         public Selector.Factory newSelectorFactory(CFMetaData cfm,
-                                                   List<ColumnDefinition> defs) throws InvalidRequestException
+                                                   AbstractType<?> expectedType,
+                                                   List<ColumnDefinition> defs,
+                                                   VariableSpecifications boundNames)
         {
-            ColumnDefinition def = cfm.getColumnDefinitionForCQL(id);
-            if (def == null)
-                throw new InvalidRequestException(String.format("Undefined name %s in selection clause", id));
-            if (def.isPrimaryKeyColumn())
+            if (column.isPrimaryKeyColumn())
                 throw new InvalidRequestException(
                         String.format("Cannot use selection function %s on PRIMARY KEY part %s",
                                       isWritetime ? "writeTime" : "ttl",
-                                      def.name));
-            if (def.type.isMultiCell())
-                throw new InvalidRequestException(String.format("Cannot use selection function %s on non-frozen collection %s",
+                                      column.name));
+            if (column.type.isMultiCell())
+                throw new InvalidRequestException(String.format("Cannot use selection function %s on non-frozen %s %s",
                                                                 isWritetime ? "writeTime" : "ttl",
-                                                                def.name));
+                                                                column.type.isCollection() ? "collection" : "UDT",
+                                                                column.name));
 
-            return WritetimeOrTTLSelector.newFactory(def, addAndGetIndex(def, defs), isWritetime);
+            return WritetimeOrTTLSelector.newFactory(column, addAndGetIndex(column, defs), isWritetime);
         }
 
-        public static class Raw implements Selectable.Raw
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
         {
-            private final ColumnIdentifier.Raw id;
+            return isWritetime ? LongType.instance : Int32Type.instance;
+        }
+
+        public static class Raw extends Selectable.Raw
+        {
+            private final ColumnDefinition.Raw id;
             private final boolean isWritetime;
 
-            public Raw(ColumnIdentifier.Raw id, boolean isWritetime)
+            public Raw(ColumnDefinition.Raw id, boolean isWritetime)
             {
                 this.id = id;
                 this.isWritetime = isWritetime;
@@ -108,59 +229,42 @@
             {
                 return new WritetimeOrTTL(id.prepare(cfm), isWritetime);
             }
-
-            public boolean processesSelection()
-            {
-                return true;
-            }
         }
     }
 
-    public static class WithFunction extends Selectable
+    public static class WithFunction implements Selectable
     {
-        public final FunctionName functionName;
+        public final Function function;
         public final List<Selectable> args;
 
-        public WithFunction(FunctionName functionName, List<Selectable> args)
+        public WithFunction(Function function, List<Selectable> args)
         {
-            this.functionName = functionName;
+            this.function = function;
             this.args = args;
         }
 
         @Override
         public String toString()
         {
-            return new StrBuilder().append(functionName)
+            return new StrBuilder().append(function.name())
                                    .append("(")
                                    .appendWithSeparators(args, ", ")
                                    .append(")")
                                    .toString();
         }
 
-        public Selector.Factory newSelectorFactory(CFMetaData cfm,
-                                                   List<ColumnDefinition> defs) throws InvalidRequestException
+        public Selector.Factory newSelectorFactory(CFMetaData cfm, AbstractType<?> expectedType, List<ColumnDefinition> defs, VariableSpecifications boundNames)
         {
-            SelectorFactories factories  =
-                    SelectorFactories.createFactoriesAndCollectColumnDefinitions(args, cfm, defs);
-
-            // We need to circumvent the normal function lookup process for toJson() because instances of the function
-            // are not pre-declared (because it can accept any type of argument).
-            Function fun;
-            if (functionName.equalsNativeFunction(ToJsonFct.NAME))
-                fun = ToJsonFct.getInstance(factories.getReturnTypes());
-            else
-                fun = FunctionResolver.get(cfm.ksName, functionName, factories.newInstances(), cfm.ksName, cfm.cfName, null);
-
-            if (fun == null)
-                throw new InvalidRequestException(String.format("Unknown function '%s'", functionName));
-            if (fun.returnType() == null)
-                throw new InvalidRequestException(String.format("Unknown function %s called in selection clause",
-                                                                functionName));
-
-            return AbstractFunctionSelector.newFactory(fun, factories);
+            SelectorFactories factories = SelectorFactories.createFactoriesAndCollectColumnDefinitions(args, function.argTypes(), cfm, defs, boundNames);
+            return AbstractFunctionSelector.newFactory(function, factories);
         }
 
-        public static class Raw implements Selectable.Raw
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            return function.returnType();
+        }
+
+        public static class Raw extends Selectable.Raw
         {
             private final FunctionName functionName;
             private final List<Selectable.Raw> args;
@@ -171,27 +275,153 @@
                 this.args = args;
             }
 
-            public WithFunction prepare(CFMetaData cfm)
+            public static Raw newCountRowsFunction()
+            {
+                return new Raw(AggregateFcts.countRowsFunction.name(),
+                               Collections.emptyList());
+            }
+
+            public Selectable prepare(CFMetaData cfm)
             {
                 List<Selectable> preparedArgs = new ArrayList<>(args.size());
                 for (Selectable.Raw arg : args)
                     preparedArgs.add(arg.prepare(cfm));
-                return new WithFunction(functionName, preparedArgs);
-            }
 
-            public boolean processesSelection()
-            {
-                return true;
+                FunctionName name = functionName;
+                // We need to circumvent the normal function lookup process for toJson() because instances of the function
+                // are not pre-declared (because it can accept any type of argument). We also have to wait until we have the
+                // selector factories of the argument so we can access their final type.
+                if (functionName.equalsNativeFunction(ToJsonFct.NAME))
+                {
+                    return new WithToJSonFunction(preparedArgs);
+                }
+                // Also, COUNT(x) is equivalent to COUNT(*) for any non-null term x (since count(x) don't care about it's argument outside of check for nullness) and
+                // for backward compatibilty we want to support COUNT(1), but we actually have COUNT(x) method for every existing (simple) input types so currently COUNT(1)
+                // will throw as ambiguous (since 1 works for any type). So we have have to special case COUNT.
+                else if (functionName.equalsNativeFunction(FunctionName.nativeFunction("count"))
+                        && preparedArgs.size() == 1
+                        && (preparedArgs.get(0) instanceof WithTerm)
+                        && (((WithTerm)preparedArgs.get(0)).rawTerm instanceof Constants.Literal))
+                {
+                    // Note that 'null' isn't a Constants.Literal
+                    name = AggregateFcts.countRowsFunction.name();
+                    preparedArgs = Collections.emptyList();
+                }
+
+                Function fun = FunctionResolver.get(cfm.ksName, name, preparedArgs, cfm.ksName, cfm.cfName, null);
+
+                if (fun == null)
+                    throw new InvalidRequestException(String.format("Unknown function '%s'", functionName));
+
+                if (fun.returnType() == null)
+                    throw new InvalidRequestException(String.format("Unknown function %s called in selection clause", functionName));
+
+                return new WithFunction(fun, preparedArgs);
             }
         }
     }
 
-    public static class WithFieldSelection extends Selectable
+    public static class WithToJSonFunction implements Selectable
+    {
+        public final List<Selectable> args;
+
+        private WithToJSonFunction(List<Selectable> args)
+        {
+            this.args = args;
+        }
+
+        @Override
+        public String toString()
+        {
+            return new StrBuilder().append(ToJsonFct.NAME)
+                                   .append("(")
+                                   .appendWithSeparators(args, ", ")
+                                   .append(")")
+                                   .toString();
+        }
+
+        public Selector.Factory newSelectorFactory(CFMetaData cfm, AbstractType<?> expectedType, List<ColumnDefinition> defs, VariableSpecifications boundNames)
+        {
+            SelectorFactories factories = SelectorFactories.createFactoriesAndCollectColumnDefinitions(args, null, cfm, defs, boundNames);
+            Function fun = ToJsonFct.getInstance(factories.getReturnTypes());
+            return AbstractFunctionSelector.newFactory(fun, factories);
+        }
+
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            return UTF8Type.instance;
+        }
+    }
+
+    public static class WithCast implements Selectable
+    {
+        private final CQL3Type type;
+        private final Selectable arg;
+
+        public WithCast(Selectable arg, CQL3Type type)
+        {
+            this.arg = arg;
+            this.type = type;
+        }
+
+        @Override
+        public String toString()
+        {
+            return String.format("cast(%s as %s)", arg, type.toString().toLowerCase());
+        }
+
+        public Selector.Factory newSelectorFactory(CFMetaData cfm, AbstractType<?> expectedType, List<ColumnDefinition> defs, VariableSpecifications boundNames)
+        {
+            List<Selectable> args = Collections.singletonList(arg);
+            SelectorFactories factories = SelectorFactories.createFactoriesAndCollectColumnDefinitions(args, null, cfm, defs, boundNames);
+
+            Selector.Factory factory = factories.get(0);
+
+            // If the user is trying to cast a type on its own type we simply ignore it.
+            if (type.getType().equals(factory.getReturnType()))
+                return factory;
+
+            FunctionName name = FunctionName.nativeFunction(CastFcts.getFunctionName(type));
+            Function fun = FunctionResolver.get(cfm.ksName, name, args, cfm.ksName, cfm.cfName, null);
+
+            if (fun == null)
+            {
+                    throw new InvalidRequestException(String.format("%s cannot be cast to %s",
+                                                                    defs.get(0).name,
+                                                                    type));
+            }
+            return AbstractFunctionSelector.newFactory(fun, factories);
+        }
+
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            return type.getType();
+        }
+
+        public static class Raw extends Selectable.Raw
+        {
+            private final CQL3Type type;
+            private final Selectable.Raw arg;
+
+            public Raw(Selectable.Raw arg, CQL3Type type)
+            {
+                this.arg = arg;
+                this.type = type;
+            }
+
+            public WithCast prepare(CFMetaData cfm)
+            {
+                return new WithCast(arg.prepare(cfm), type);
+            }
+        }
+    }
+
+    public static class WithFieldSelection implements Selectable
     {
         public final Selectable selected;
-        public final ColumnIdentifier field;
+        public final FieldIdentifier field;
 
-        public WithFieldSelection(Selectable selected, ColumnIdentifier field)
+        public WithFieldSelection(Selectable selected, FieldIdentifier field)
         {
             this.selected = selected;
             this.field = field;
@@ -203,36 +433,49 @@
             return String.format("%s.%s", selected, field);
         }
 
-        public Selector.Factory newSelectorFactory(CFMetaData cfm,
-                                                   List<ColumnDefinition> defs) throws InvalidRequestException
+        public Selector.Factory newSelectorFactory(CFMetaData cfm, AbstractType<?> expectedType, List<ColumnDefinition> defs, VariableSpecifications boundNames)
         {
-            Selector.Factory factory = selected.newSelectorFactory(cfm, defs);
-            AbstractType<?> type = factory.newInstance().getType();
-            if (!(type instanceof UserType))
+            Selector.Factory factory = selected.newSelectorFactory(cfm, null, defs, boundNames);
+            AbstractType<?> type = factory.getColumnSpecification(cfm).type;
+            if (!type.isUDT())
+            {
                 throw new InvalidRequestException(
                         String.format("Invalid field selection: %s of type %s is not a user type",
-                                      selected,
-                                      type.asCQL3Type()));
+                                selected,
+                                type.asCQL3Type()));
+            }
 
             UserType ut = (UserType) type;
-            for (int i = 0; i < ut.size(); i++)
+            int fieldIndex = ut.fieldPosition(field);
+            if (fieldIndex == -1)
             {
-                if (!ut.fieldName(i).equals(field.bytes))
-                    continue;
-                return FieldSelector.newFactory(ut, i, factory);
+                throw new InvalidRequestException(String.format("%s of type %s has no field %s",
+                        selected, type.asCQL3Type(), field));
             }
-            throw new InvalidRequestException(String.format("%s of type %s has no field %s",
-                                                            selected,
-                                                            type.asCQL3Type(),
-                                                            field));
+
+            return FieldSelector.newFactory(ut, fieldIndex, factory);
         }
 
-        public static class Raw implements Selectable.Raw
+        public AbstractType<?> getExactTypeIfKnown(String keyspace)
+        {
+            AbstractType<?> selectedType = selected.getExactTypeIfKnown(keyspace);
+            if (selectedType == null || !(selectedType instanceof UserType))
+                return null;
+
+            UserType ut = (UserType) selectedType;
+            int fieldIndex = ut.fieldPosition(field);
+            if (fieldIndex == -1)
+                return null;
+
+            return ut.fieldType(fieldIndex);
+        }
+
+        public static class Raw extends Selectable.Raw
         {
             private final Selectable.Raw selected;
-            private final ColumnIdentifier.Raw field;
+            private final FieldIdentifier field;
 
-            public Raw(Selectable.Raw selected, ColumnIdentifier.Raw field)
+            public Raw(Selectable.Raw selected, FieldIdentifier field)
             {
                 this.selected = selected;
                 this.field = field;
@@ -240,12 +483,7 @@
 
             public WithFieldSelection prepare(CFMetaData cfm)
             {
-                return new WithFieldSelection(selected.prepare(cfm), field.prepare(cfm));
-            }
-
-            public boolean processesSelection()
-            {
-                return true;
+                return new WithFieldSelection(selected.prepare(cfm), field);
             }
         }
     }
diff --git a/src/java/org/apache/cassandra/cql3/selection/Selection.java b/src/java/org/apache/cassandra/cql3/selection/Selection.java
index 510e11c..7e9b716 100644
--- a/src/java/org/apache/cassandra/cql3/selection/Selection.java
+++ b/src/java/org/apache/cassandra/cql3/selection/Selection.java
@@ -20,7 +20,7 @@
 import java.nio.ByteBuffer;
 import java.util.*;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Iterators;
@@ -30,10 +30,15 @@
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.cql3.functions.Function;
-import org.apache.cassandra.db.rows.Cell;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.aggregation.AggregationSpecification;
+import org.apache.cassandra.db.aggregation.GroupMaker;
 import org.apache.cassandra.db.context.CounterContext;
 import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.rows.Cell;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public abstract class Selection
@@ -76,7 +81,7 @@
     public boolean isWildcard()
     {
         return false;
-    }    
+    }
 
     /**
      * Checks if this selection contains static columns.
@@ -115,14 +120,14 @@
     }
 
     /**
-     * Checks if this selection contains a collection.
+     * Checks if this selection contains a complex column.
      *
-     * @return <code>true</code> if this selection contains a collection, <code>false</code> otherwise.
+     * @return <code>true</code> if this selection contains a multicell collection or UDT, <code>false</code> otherwise.
      */
-    public boolean containsACollection()
+    public boolean containsAComplexColumn()
     {
         for (ColumnDefinition def : getColumns())
-            if (def.type.isCollection() && def.type.isMultiCell())
+            if (def.isComplex())
                 return true;
 
         return false;
@@ -169,6 +174,18 @@
         return new SimpleSelection(cfm, all, true);
     }
 
+    public static Selection wildcardWithGroupBy(CFMetaData cfm, VariableSpecifications boundNames)
+    {
+        List<RawSelector> rawSelectors = new ArrayList<>(cfm.allColumns().size());
+        Iterator<ColumnDefinition> iter = cfm.allColumnsInSelectOrder();
+        while (iter.hasNext())
+        {
+            ColumnDefinition.Raw raw = ColumnDefinition.Raw.forColumn(iter.next());
+            rawSelectors.add(new RawSelector(raw, null));
+        }
+        return fromSelectors(cfm, rawSelectors, boundNames, true);
+    }
+
     public static Selection forColumns(CFMetaData cfm, List<ColumnDefinition> columns)
     {
         return new SimpleSelection(cfm, columns, false);
@@ -208,15 +225,15 @@
         return false;
     }
 
-    public static Selection fromSelectors(CFMetaData cfm, List<RawSelector> rawSelectors) throws InvalidRequestException
+    public static Selection fromSelectors(CFMetaData cfm, List<RawSelector> rawSelectors, VariableSpecifications boundNames, boolean hasGroupBy)
     {
         List<ColumnDefinition> defs = new ArrayList<>();
 
         SelectorFactories factories =
-                SelectorFactories.createFactoriesAndCollectColumnDefinitions(RawSelector.toSelectables(rawSelectors, cfm), cfm, defs);
+                SelectorFactories.createFactoriesAndCollectColumnDefinitions(RawSelector.toSelectables(rawSelectors, cfm), null, cfm, defs, boundNames);
         SelectionColumnMapping mapping = collectColumnMappings(cfm, rawSelectors, factories);
 
-        return (processesSelection(rawSelectors) || rawSelectors.size() != defs.size())
+        return (processesSelection(rawSelectors) || rawSelectors.size() != defs.size() || hasGroupBy)
                ? new SelectionWithProcessing(cfm, defs, mapping, factories)
                : new SimpleSelection(cfm, defs, mapping, false);
     }
@@ -260,7 +277,7 @@
         return selectionColumns;
     }
 
-    protected abstract Selectors newSelectors() throws InvalidRequestException;
+    protected abstract Selectors newSelectors(QueryOptions options) throws InvalidRequestException;
 
     /**
      * @return the list of CQL3 columns value this SelectionClause needs.
@@ -278,9 +295,15 @@
         return columnMapping;
     }
 
-    public ResultSetBuilder resultSetBuilder(boolean isJons) throws InvalidRequestException
+    public ResultSetBuilder resultSetBuilder(QueryOptions options, boolean isJson)
     {
-        return new ResultSetBuilder(isJons);
+        return new ResultSetBuilder(options, isJson);
+    }
+
+    public ResultSetBuilder resultSetBuilder(QueryOptions options, boolean isJson, AggregationSpecification aggregationSpec)
+    {
+        return aggregationSpec == null ? new ResultSetBuilder(options, isJson)
+                : new ResultSetBuilder(options, isJson, aggregationSpec.newGroupMaker());
     }
 
     public abstract boolean isAggregate();
@@ -288,18 +311,47 @@
     @Override
     public String toString()
     {
-        return Objects.toStringHelper(this)
-                .add("columns", columns)
-                .add("columnMapping", columnMapping)
-                .add("metadata", metadata)
-                .add("collectTimestamps", collectTimestamps)
-                .add("collectTTLs", collectTTLs)
-                .toString();
+        return MoreObjects.toStringHelper(this)
+                          .add("columns", columns)
+                          .add("columnMapping", columnMapping)
+                          .add("metadata", metadata)
+                          .add("collectTimestamps", collectTimestamps)
+                          .add("collectTTLs", collectTTLs)
+                          .toString();
+    }
+
+    public static List<ByteBuffer> rowToJson(List<ByteBuffer> row, ProtocolVersion protocolVersion, ResultSet.ResultMetadata metadata)
+    {
+        StringBuilder sb = new StringBuilder("{");
+        for (int i = 0; i < metadata.getColumnCount(); i++)
+        {
+            if (i > 0)
+                sb.append(", ");
+
+            ColumnSpecification spec = metadata.names.get(i);
+            String columnName = spec.name.toString();
+            if (!columnName.equals(columnName.toLowerCase(Locale.US)))
+                columnName = "\"" + columnName + "\"";
+
+            ByteBuffer buffer = row.get(i);
+            sb.append('"');
+            sb.append(Json.quoteAsJsonString(columnName));
+            sb.append("\": ");
+            if (buffer == null)
+                sb.append("null");
+            else
+                sb.append(spec.type.toJSONString(buffer, protocolVersion));
+        }
+        sb.append("}");
+        List<ByteBuffer> jsonRow = new ArrayList<>();
+        jsonRow.add(UTF8Type.instance.getSerializer().serialize(sb.toString()));
+        return jsonRow;
     }
 
     public class ResultSetBuilder
     {
         private final ResultSet resultSet;
+        private final ProtocolVersion protocolVersion;
 
         /**
          * As multiple thread can access a <code>Selection</code> instance each <code>ResultSetBuilder</code> will use
@@ -307,6 +359,11 @@
          */
         private final Selectors selectors;
 
+        /**
+         * The <code>GroupMaker</code> used to build the aggregates.
+         */
+        private final GroupMaker groupMaker;
+
         /*
          * We'll build CQL3 row one by one.
          * The currentRow is the values for the (CQL3) columns we've fetched.
@@ -321,10 +378,17 @@
 
         private final boolean isJson;
 
-        private ResultSetBuilder(boolean isJson) throws InvalidRequestException
+        private ResultSetBuilder(QueryOptions options, boolean isJson)
+        {
+            this(options, isJson, null);
+        }
+
+        private ResultSetBuilder(QueryOptions options, boolean isJson, GroupMaker groupMaker)
         {
             this.resultSet = new ResultSet(getResultMetadata(isJson).copy(), new ArrayList<List<ByteBuffer>>());
-            this.selectors = newSelectors();
+            this.protocolVersion = options.getProtocolVersion();
+            this.selectors = newSelectors(options);
+            this.groupMaker = groupMaker;
             this.timestamps = collectTimestamps ? new long[columns.size()] : null;
             this.ttls = collectTTLs ? new int[columns.size()] : null;
             this.isJson = isJson;
@@ -374,47 +438,60 @@
                  : c.value();
         }
 
-        public void newRow(int protocolVersion) throws InvalidRequestException
+        /**
+         * Notifies this <code>Builder</code> that a new row is being processed.
+         *
+         * @param partitionKey the partition key of the new row
+         * @param clustering the clustering of the new row
+         */
+        public void newRow(DecoratedKey partitionKey, Clustering clustering)
         {
+            // The groupMaker needs to be called for each row
+            boolean isNewAggregate = groupMaker == null || groupMaker.isNewGroup(partitionKey, clustering);
             if (current != null)
             {
                 selectors.addInputRow(protocolVersion, this);
-                if (!selectors.isAggregate())
+                if (isNewAggregate)
                 {
-                    resultSet.addRow(getOutputRow(protocolVersion));
+                    resultSet.addRow(getOutputRow());
                     selectors.reset();
                 }
             }
             current = new ArrayList<>(columns.size());
 
-            // Timestamps and TTLs are arrays per row, we must null them out between row
+            // Timestamps and TTLs are arrays per row, we must null them out between rows
             if (timestamps != null)
                 Arrays.fill(timestamps, Long.MIN_VALUE);
             if (ttls != null)
                 Arrays.fill(ttls, -1);
         }
 
-        public ResultSet build(int protocolVersion) throws InvalidRequestException
+        /**
+         * Builds the <code>ResultSet</code>
+         */
+        public ResultSet build()
         {
             if (current != null)
             {
                 selectors.addInputRow(protocolVersion, this);
-                resultSet.addRow(getOutputRow(protocolVersion));
+                resultSet.addRow(getOutputRow());
                 selectors.reset();
                 current = null;
             }
 
-            if (resultSet.isEmpty() && selectors.isAggregate())
-                resultSet.addRow(getOutputRow(protocolVersion));
+            // For aggregates we need to return a row even it no records have been found
+            if (resultSet.isEmpty() && groupMaker != null && groupMaker.returnAtLeastOneRow())
+                resultSet.addRow(getOutputRow());
             return resultSet;
         }
 
-        private List<ByteBuffer> getOutputRow(int protocolVersion)
+        private List<ByteBuffer> getOutputRow()
         {
             List<ByteBuffer> outputRow = selectors.getOutputRow(protocolVersion);
             if (isJson)
             {
-                List<ByteBuffer> jsonRow = rowToJson(outputRow, protocolVersion);
+                // Keep all columns around for possible post-query ordering. (CASSANDRA-14286)
+                List<ByteBuffer> jsonRow = rowToJson(outputRow, protocolVersion, metadata);
 
                 // Keep ordering columns around for possible post-query ordering. (CASSANDRA-14286)
                 if (orderingIndex != null)
@@ -426,34 +503,6 @@
             }
             return outputRow;
         }
-
-        private List<ByteBuffer> rowToJson(List<ByteBuffer> row, int protocolVersion)
-        {
-            StringBuilder sb = new StringBuilder("{");
-            for (int i = 0; i < metadata.getColumnCount(); i++)
-            {
-                if (i > 0)
-                    sb.append(", ");
-
-                ColumnSpecification spec = metadata.names.get(i);
-                String columnName = spec.name.toString();
-                if (!columnName.equals(columnName.toLowerCase(Locale.US)))
-                    columnName = "\"" + columnName + "\"";
-
-                ByteBuffer buffer = row.get(i);
-                sb.append('"');
-                sb.append(Json.quoteAsJsonString(columnName));
-                sb.append("\": ");
-                if (buffer == null)
-                    sb.append("null");
-                else
-                    sb.append(spec.type.toJSONString(buffer, protocolVersion));
-            }
-            sb.append("}");
-            List<ByteBuffer> jsonRow = new ArrayList<>();
-            jsonRow.add(UTF8Type.instance.getSerializer().serialize(sb.toString()));
-            return jsonRow;
-        }
     }
 
     private static interface Selectors
@@ -463,17 +512,18 @@
         /**
          * Adds the current row of the specified <code>ResultSetBuilder</code>.
          *
+         * @param protocolVersion
          * @param rs the <code>ResultSetBuilder</code>
          * @throws InvalidRequestException
          */
-        public void addInputRow(int protocolVersion, ResultSetBuilder rs) throws InvalidRequestException;
+        public void addInputRow(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException;
 
-        public List<ByteBuffer> getOutputRow(int protocolVersion) throws InvalidRequestException;
+        public List<ByteBuffer> getOutputRow(ProtocolVersion protocolVersion) throws InvalidRequestException;
 
         public void reset();
     }
 
-    // Special cased selection for when no function is used (this save some allocations).
+    // Special cased selection for when only columns are selected.
     private static class SimpleSelection extends Selection
     {
         private final boolean isWildcard;
@@ -508,7 +558,7 @@
             return false;
         }
 
-        protected Selectors newSelectors()
+        protected Selectors newSelectors(QueryOptions options)
         {
             return new Selectors()
             {
@@ -519,12 +569,12 @@
                     current = null;
                 }
 
-                public List<ByteBuffer> getOutputRow(int protocolVersion)
+                public List<ByteBuffer> getOutputRow(ProtocolVersion protocolVersion)
                 {
                     return current;
                 }
 
-                public void addInputRow(int protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
+                public void addInputRow(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
                 {
                     current = rs.current;
                 }
@@ -589,11 +639,11 @@
             return factories.doesAggregation();
         }
 
-        protected Selectors newSelectors() throws InvalidRequestException
+        protected Selectors newSelectors(final QueryOptions options) throws InvalidRequestException
         {
             return new Selectors()
             {
-                private final List<Selector> selectors = factories.newInstances();
+                private final List<Selector> selectors = factories.newInstances(options);
 
                 public void reset()
                 {
@@ -606,7 +656,7 @@
                     return factories.doesAggregation();
                 }
 
-                public List<ByteBuffer> getOutputRow(int protocolVersion) throws InvalidRequestException
+                public List<ByteBuffer> getOutputRow(ProtocolVersion protocolVersion) throws InvalidRequestException
                 {
                     List<ByteBuffer> outputRow = new ArrayList<>(selectors.size());
 
@@ -616,7 +666,7 @@
                     return outputRow;
                 }
 
-                public void addInputRow(int protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
+                public void addInputRow(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
                 {
                     for (Selector selector : selectors)
                         selector.addInput(protocolVersion, rs);
diff --git a/src/java/org/apache/cassandra/cql3/selection/SelectionColumnMapping.java b/src/java/org/apache/cassandra/cql3/selection/SelectionColumnMapping.java
index 4cfdefb..5072066 100644
--- a/src/java/org/apache/cassandra/cql3/selection/SelectionColumnMapping.java
+++ b/src/java/org/apache/cassandra/cql3/selection/SelectionColumnMapping.java
@@ -23,8 +23,6 @@
 import java.util.*;
 import java.util.stream.Collectors;
 
-import com.google.common.base.Function;
-import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
 import com.google.common.collect.*;
 
diff --git a/src/java/org/apache/cassandra/cql3/selection/Selector.java b/src/java/org/apache/cassandra/cql3/selection/Selector.java
index 7249d22..922b57f 100644
--- a/src/java/org/apache/cassandra/cql3/selection/Selector.java
+++ b/src/java/org/apache/cassandra/cql3/selection/Selector.java
@@ -21,22 +21,22 @@
 import java.util.List;
 
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.cql3.AssignmentTestable;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.cql3.ColumnSpecification;
+import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.cql3.selection.Selection.ResultSetBuilder;
 import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.db.marshal.ReversedType;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
- * A <code>Selector</code> is used to convert the data returned by the storage engine into the data requested by the 
+ * A <code>Selector</code> is used to convert the data returned by the storage engine into the data requested by the
  * user. They correspond to the &lt;selector&gt; elements from the select clause.
- * <p>Since the introduction of aggregation, <code>Selector</code>s cannot be called anymore by multiple threads 
+ * <p>Since the introduction of aggregation, <code>Selector</code>s cannot be called anymore by multiple threads
  * as they have an internal state.</p>
  */
-public abstract class Selector implements AssignmentTestable
+public abstract class Selector
 {
     /**
      * A factory for <code>Selector</code> instances.
@@ -58,16 +58,19 @@
         {
             return new ColumnSpecification(cfm.ksName,
                                            cfm.cfName,
-                                           ColumnIdentifier.getInterned(getColumnName(), true),
+                                           new ColumnIdentifier(getColumnName(), true), // note that the name is not necessarily
+                                                                                        // a true column name so we shouldn't intern it
                                            getReturnType());
         }
 
         /**
          * Creates a new <code>Selector</code> instance.
          *
+         * @param options the options of the query for which the instance is created (some selector
+         * depends on the bound values in particular).
          * @return a new <code>Selector</code> instance
          */
-        public abstract Selector newInstance() throws InvalidRequestException;
+        public abstract Selector newInstance(QueryOptions options) throws InvalidRequestException;
 
         /**
          * Checks if this factory creates selectors instances that creates aggregates.
@@ -150,7 +153,7 @@
      * @param rs the <code>ResultSetBuilder</code>
      * @throws InvalidRequestException if a problem occurs while add the input value
      */
-    public abstract void addInput(int protocolVersion, ResultSetBuilder rs) throws InvalidRequestException;
+    public abstract void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException;
 
     /**
      * Returns the selector output.
@@ -159,7 +162,7 @@
      * @return the selector output
      * @throws InvalidRequestException if a problem occurs while computing the output value
      */
-    public abstract ByteBuffer getOutput(int protocolVersion) throws InvalidRequestException;
+    public abstract ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException;
 
     /**
      * Returns the <code>Selector</code> output type.
@@ -183,24 +186,4 @@
      * Reset the internal state of this <code>Selector</code>.
      */
     public abstract void reset();
-
-    public final AssignmentTestable.TestResult testAssignment(String keyspace, ColumnSpecification receiver)
-    {
-        // We should ignore the fact that the output type is frozen in our comparison as functions do not support
-        // frozen types for arguments
-        AbstractType<?> receiverType = receiver.type;
-        if (getType().isFrozenCollection())
-            receiverType = receiverType.freeze();
-
-        if (getType().isReversed())
-            receiverType = ReversedType.getInstance(receiverType);
-
-        if (receiverType.equals(getType()))
-            return AssignmentTestable.TestResult.EXACT_MATCH;
-
-        if (receiverType.isValueCompatibleWith(getType()))
-            return AssignmentTestable.TestResult.WEAKLY_ASSIGNABLE;
-
-        return AssignmentTestable.TestResult.NOT_ASSIGNABLE;
-    }
 }
diff --git a/src/java/org/apache/cassandra/cql3/selection/SelectorFactories.java b/src/java/org/apache/cassandra/cql3/selection/SelectorFactories.java
index 97a1198..41bf193 100644
--- a/src/java/org/apache/cassandra/cql3/selection/SelectorFactories.java
+++ b/src/java/org/apache/cassandra/cql3/selection/SelectorFactories.java
@@ -23,6 +23,8 @@
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.VariableSpecifications;
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.cql3.selection.Selector.Factory;
 import org.apache.cassandra.db.marshal.AbstractType;
@@ -57,29 +59,40 @@
      * Creates a new <code>SelectorFactories</code> instance and collect the column definitions.
      *
      * @param selectables the <code>Selectable</code>s for which the factories must be created
+     * @param expectedTypes the returned types expected for each of the {@code selectables}, if there
+     * is any such expectations, or {@code null} otherwise. This will be {@code null} when called on
+     * the top-level selectables, but may not be for selectable nested within a function for instance
+     * (as the argument selectable will be expected to be of the type expected by the function).
      * @param cfm the Column Family Definition
      * @param defs the collector parameter for the column definitions
+     * @param boundNames the collector for the specification of bound markers in the selection
      * @return a new <code>SelectorFactories</code> instance
      * @throws InvalidRequestException if a problem occurs while creating the factories
      */
     public static SelectorFactories createFactoriesAndCollectColumnDefinitions(List<Selectable> selectables,
+                                                                               List<AbstractType<?>> expectedTypes,
                                                                                CFMetaData cfm,
-                                                                               List<ColumnDefinition> defs)
+                                                                               List<ColumnDefinition> defs,
+                                                                               VariableSpecifications boundNames)
                                                                                throws InvalidRequestException
     {
-        return new SelectorFactories(selectables, cfm, defs);
+        return new SelectorFactories(selectables, expectedTypes, cfm, defs, boundNames);
     }
 
     private SelectorFactories(List<Selectable> selectables,
+                              List<AbstractType<?>> expectedTypes,
                               CFMetaData cfm,
-                              List<ColumnDefinition> defs)
+                              List<ColumnDefinition> defs,
+                              VariableSpecifications boundNames)
                               throws InvalidRequestException
     {
         factories = new ArrayList<>(selectables.size());
 
-        for (Selectable selectable : selectables)
+        for (int i = 0; i < selectables.size(); i++)
         {
-            Factory factory = selectable.newSelectorFactory(cfm, defs);
+            Selectable selectable = selectables.get(i);
+            AbstractType<?> expectedType = expectedTypes == null ? null : expectedTypes.get(i);
+            Factory factory = selectable.newSelectorFactory(cfm, expectedType, defs, boundNames);
             containsWritetimeFactory |= factory.isWritetimeSelectorFactory();
             containsTTLFactory |= factory.isTTLSelectorFactory();
             if (factory.isAggregateSelectorFactory())
@@ -148,15 +161,15 @@
 
     /**
      * Creates a list of new <code>Selector</code> instances.
+     *
+     * @param options the query options for the query being executed.
      * @return a list of new <code>Selector</code> instances.
      */
-    public List<Selector> newInstances() throws InvalidRequestException
+    public List<Selector> newInstances(QueryOptions options) throws InvalidRequestException
     {
         List<Selector> selectors = new ArrayList<>(factories.size());
         for (Selector.Factory factory : factories)
-        {
-            selectors.add(factory.newInstance());
-        }
+            selectors.add(factory.newInstance(options));
         return selectors;
     }
 
diff --git a/src/java/org/apache/cassandra/cql3/selection/SimpleSelector.java b/src/java/org/apache/cassandra/cql3/selection/SimpleSelector.java
index e4040fa..8d5a305 100644
--- a/src/java/org/apache/cassandra/cql3/selection/SimpleSelector.java
+++ b/src/java/org/apache/cassandra/cql3/selection/SimpleSelector.java
@@ -21,9 +21,11 @@
 
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.ColumnSpecification;
+import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.selection.Selection.ResultSetBuilder;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 public final class SimpleSelector extends Selector
 {
@@ -55,7 +57,7 @@
             }
 
             @Override
-            public Selector newInstance()
+            public Selector newInstance(QueryOptions options)
             {
                 return new SimpleSelector(def.name.toString(), idx, def.type);
             }
@@ -69,7 +71,7 @@
     }
 
     @Override
-    public void addInput(int protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
+    public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
     {
         if (!isSet)
         {
@@ -79,7 +81,7 @@
     }
 
     @Override
-    public ByteBuffer getOutput(int protocolVersion) throws InvalidRequestException
+    public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
     {
         return current;
     }
diff --git a/src/java/org/apache/cassandra/cql3/selection/TermSelector.java b/src/java/org/apache/cassandra/cql3/selection/TermSelector.java
new file mode 100644
index 0000000..2b0e975
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/selection/TermSelector.java
@@ -0,0 +1,90 @@
+/*
+ * 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.selection;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.cql3.ColumnSpecification;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.Term;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.transport.ProtocolVersion;
+
+/**
+ * Selector representing a simple term (literals or bound variables).
+ * <p>
+ * Note that we know the term does not include function calls for instance (this is actually enforced by the parser), those
+ * being dealt with by their own Selector.
+ */
+public class TermSelector extends Selector
+{
+    private final ByteBuffer value;
+    private final AbstractType<?> type;
+
+    public static Factory newFactory(final String name, final Term term, final AbstractType<?> type)
+    {
+        return new Factory()
+        {
+            protected String getColumnName()
+            {
+                return name;
+            }
+
+            protected AbstractType<?> getReturnType()
+            {
+                return type;
+            }
+
+            protected void addColumnMapping(SelectionColumnMapping mapping, ColumnSpecification resultColumn)
+            {
+               mapping.addMapping(resultColumn, (ColumnDefinition)null);
+            }
+
+            public Selector newInstance(QueryOptions options)
+            {
+                return new TermSelector(term.bindAndGet(options), type);
+            }
+        };
+    }
+
+    private TermSelector(ByteBuffer value, AbstractType<?> type)
+    {
+        this.value = value;
+        this.type = type;
+    }
+
+    public void addInput(ProtocolVersion protocolVersion, Selection.ResultSetBuilder rs) throws InvalidRequestException
+    {
+    }
+
+    public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
+    {
+        return value;
+    }
+
+    public AbstractType<?> getType()
+    {
+        return type;
+    }
+
+    public void reset()
+    {
+    }
+}
diff --git a/src/java/org/apache/cassandra/cql3/selection/WritetimeOrTTLSelector.java b/src/java/org/apache/cassandra/cql3/selection/WritetimeOrTTLSelector.java
index 131827f..939f8c2 100644
--- a/src/java/org/apache/cassandra/cql3/selection/WritetimeOrTTLSelector.java
+++ b/src/java/org/apache/cassandra/cql3/selection/WritetimeOrTTLSelector.java
@@ -20,11 +20,13 @@
 import java.nio.ByteBuffer;
 
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.ColumnSpecification;
 import org.apache.cassandra.cql3.selection.Selection.ResultSetBuilder;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.Int32Type;
 import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 final class WritetimeOrTTLSelector extends Selector
@@ -54,7 +56,7 @@
                mapping.addMapping(resultsColumn, def);
             }
 
-            public Selector newInstance()
+            public Selector newInstance(QueryOptions options)
             {
                 return new WritetimeOrTTLSelector(def.name.toString(), idx, isWritetime);
             }
@@ -71,7 +73,7 @@
         };
     }
 
-    public void addInput(int protocolVersion, ResultSetBuilder rs)
+    public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs)
     {
         if (isSet)
             return;
@@ -90,7 +92,7 @@
         }
     }
 
-    public ByteBuffer getOutput(int protocolVersion)
+    public ByteBuffer getOutput(ProtocolVersion protocolVersion)
     {
         return current;
     }
diff --git a/src/java/org/apache/cassandra/cql3/statements/AlterKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/AlterKeyspaceStatement.java
index 3ca4d72..76c8d2f 100644
--- a/src/java/org/apache/cassandra/cql3/statements/AlterKeyspaceStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/AlterKeyspaceStatement.java
@@ -19,6 +19,7 @@
 
 import org.apache.cassandra.auth.Permission;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.exceptions.*;
 import org.apache.cassandra.locator.LocalStrategy;
 import org.apache.cassandra.schema.KeyspaceMetadata;
@@ -56,7 +57,7 @@
         KeyspaceMetadata ksm = Schema.instance.getKSMetaData(name);
         if (ksm == null)
             throw new InvalidRequestException("Unknown keyspace " + name);
-        if (Schema.isLocalSystemKeyspace(ksm.name))
+        if (SchemaConstants.isLocalSystemKeyspace(ksm.name))
             throw new InvalidRequestException("Cannot alter system keyspace");
 
         attrs.validate();
diff --git a/src/java/org/apache/cassandra/cql3/statements/AlterTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/AlterTableStatement.java
index 00d6fab..0a6dfd5 100644
--- a/src/java/org/apache/cassandra/cql3/statements/AlterTableStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/AlterTableStatement.java
@@ -30,9 +30,7 @@
 
 import org.apache.cassandra.auth.Permission;
 import org.apache.cassandra.config.*;
-import org.apache.cassandra.cql3.CFName;
-import org.apache.cassandra.cql3.CQL3Type;
-import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.marshal.AbstractType;
@@ -68,29 +66,23 @@
     }
 
     public final Type oType;
-    public final CQL3Type.Raw validator;
-    public final ColumnIdentifier.Raw rawColumnName;
     private final TableAttributes attrs;
-    private final Map<ColumnIdentifier.Raw, ColumnIdentifier> renames;
-    private final boolean isStatic; // Only for ALTER ADD
+    private final Map<ColumnDefinition.Raw, ColumnDefinition.Raw> renames;
+    private final List<AlterTableStatementColumn> colNameList;
     private final Long deleteTimestamp;
 
     public AlterTableStatement(CFName name,
                                Type type,
-                               ColumnIdentifier.Raw columnName,
-                               CQL3Type.Raw validator,
+                               List<AlterTableStatementColumn> colDataList,
                                TableAttributes attrs,
-                               Map<ColumnIdentifier.Raw, ColumnIdentifier> renames,
-                               boolean isStatic,
+                               Map<ColumnDefinition.Raw, ColumnDefinition.Raw> renames,
                                Long deleteTimestamp)
     {
         super(name);
         this.oType = type;
-        this.rawColumnName = columnName;
-        this.validator = validator; // used only for ADD/ALTER commands
+        this.colNameList = colDataList;
         this.attrs = attrs;
         this.renames = renames;
-        this.isStatic = isStatic;
         this.deleteTimestamp = deleteTimestamp;
     }
 
@@ -111,15 +103,11 @@
             throw new InvalidRequestException("Cannot use ALTER TABLE on Materialized View");
 
         CFMetaData cfm;
-
-        CQL3Type validator = this.validator == null ? null : this.validator.prepare(keyspace());
         ColumnIdentifier columnName = null;
         ColumnDefinition def = null;
-        if (rawColumnName != null)
-        {
-            columnName = rawColumnName.prepare(meta);
-            def = meta.getColumnDefinition(columnName);
-        }
+        CQL3Type.Raw dataType = null;
+        boolean isStatic = false;
+        CQL3Type validator = null;
 
         List<ViewDefinition> viewUpdates = null;
         Iterable<ViewDefinition> views = View.findAll(keyspace(), columnFamily());
@@ -127,167 +115,189 @@
         switch (oType)
         {
             case ALTER:
-                // We do not support altering of types and only allow this to for people who have already one
-                // through the upgrade of 2.x CQL-created SSTables with Thrift writes, affected by CASSANDRA-15778.
-                if (meta.isDense()
-                    && meta.compactValueColumn().equals(def)
-                    && meta.compactValueColumn().type instanceof EmptyType
-                    && validator != null)
+                cfm = null;
+                for (AlterTableStatementColumn colData : colNameList)
                 {
-                    if (validator.getType() instanceof BytesType)
-                    {
-                        cfm = meta.copyWithNewCompactValueType(validator.getType());
-                        break;
-                    }
+                    columnName = colData.getColumnName().getIdentifier(meta);
+                    def = meta.getColumnDefinition(columnName);
+                    dataType = colData.getColumnType();
+                    validator = dataType.prepare(keyspace());
 
-                    throw new InvalidRequestException(String.format("Compact value type can only be changed to BytesType, but %s was given.",
-                                                                    validator.getType()));
+                    // We do not support altering of types and only allow this to for people who have already one
+                    // through the upgrade of 2.x CQL-created SSTables with Thrift writes, affected by CASSANDRA-15778.
+                    if (meta.isDense()
+                        && meta.compactValueColumn().equals(def)
+                        && meta.compactValueColumn().type instanceof EmptyType
+                        && validator != null)
+                    {
+                        if (validator.getType() instanceof BytesType)
+                            cfm = meta.copyWithNewCompactValueType(validator.getType());
+                        else
+                            throw new InvalidRequestException(String.format("Compact value type can only be changed to BytesType, but %s was given.",
+                                                                            validator.getType()));
+                    }
                 }
-                else
-                {
+
+                if (cfm == null)
                     throw new InvalidRequestException("Altering of types is not allowed");
-                }
+                else
+                    break;
             case ADD:
-                assert columnName != null;
                 if (meta.isCompactTable())
                     throw new InvalidRequestException("Cannot add new column to a COMPACT STORAGE table");
 
                 cfm = meta.copy();
 
-                if (isStatic)
+                for (AlterTableStatementColumn colData : colNameList)
                 {
-                    if (!cfm.isCompound())
-                        throw new InvalidRequestException("Static columns are not allowed in COMPACT STORAGE tables");
-                    if (cfm.clusteringColumns().isEmpty())
-                        throw new InvalidRequestException("Static columns are only useful (and thus allowed) if the table has at least one clustering column");
-                }
+                    columnName = colData.getColumnName().getIdentifier(cfm);
+                    def = cfm.getColumnDefinition(columnName);
+                    dataType = colData.getColumnType();
+                    assert dataType != null;
+                    isStatic = colData.getStaticType();
+                    validator = dataType.prepare(keyspace());
 
-                if (def != null)
-                {
-                    switch (def.kind)
+
+                    if (isStatic)
                     {
-                        case PARTITION_KEY:
-                        case CLUSTERING:
-                            throw new InvalidRequestException(String.format("Invalid column name %s because it conflicts with a PRIMARY KEY part", columnName));
-                        default:
-                            throw new InvalidRequestException(String.format("Invalid column name %s because it conflicts with an existing column", columnName));
-                    }
-                }
-
-                AbstractType<?> type = validator.getType();
-                if (type.isCollection() && type.isMultiCell())
-                {
-                    if (!cfm.isCompound())
-                        throw new InvalidRequestException("Cannot use non-frozen collections in COMPACT STORAGE tables");
-                    if (cfm.isSuper())
-                        throw new InvalidRequestException("Cannot use non-frozen collections with super column families");
-                }
-
-                ColumnDefinition toAdd = isStatic
-                                       ? ColumnDefinition.staticDef(cfm, columnName.bytes, type)
-                                       : ColumnDefinition.regularDef(cfm, columnName.bytes, type);
-
-                CFMetaData.DroppedColumn droppedColumn = meta.getDroppedColumns().get(columnName.bytes);
-                if (null != droppedColumn)
-                {
-                    if (droppedColumn.kind != toAdd.kind)
-                    {
-                        String message =
-                            String.format("Cannot re-add previously dropped column '%s' of kind %s, incompatible with previous kind %s",
-                                          columnName,
-                                          toAdd.kind,
-                                          droppedColumn.kind == null ? "UNKNOWN" : droppedColumn.kind);
-                        throw new InvalidRequestException(message);
+                        if (!cfm.isCompound())
+                            throw new InvalidRequestException("Static columns are not allowed in COMPACT STORAGE tables");
+                        if (cfm.clusteringColumns().isEmpty())
+                            throw new InvalidRequestException("Static columns are only useful (and thus allowed) if the table has at least one clustering column");
                     }
 
-                    // After #8099, not safe to re-add columns of incompatible types - until *maybe* deser logic with dropped
-                    // columns is pushed deeper down the line. The latter would still be problematic in cases of schema races.
-                    if (!type.isValueCompatibleWith(droppedColumn.type))
+                    if (def != null)
                     {
-                        String message =
-                            String.format("Cannot re-add previously dropped column '%s' of type %s, incompatible with previous type %s",
-                                          columnName,
-                                          type.asCQL3Type(),
-                                          droppedColumn.type.asCQL3Type());
-                        throw new InvalidRequestException(message);
-                    }
-
-                    // Cannot re-add a dropped counter column. See #7831.
-                    if (meta.isCounter())
-                        throw new InvalidRequestException(String.format("Cannot re-add previously dropped counter column %s", columnName));
-                }
-
-                cfm.addColumnDefinition(toAdd);
-
-                // Adding a column to a table which has an include all view requires the column to be added to the view as well
-                if (!isStatic)
-                {
-                    for (ViewDefinition view : views)
-                    {
-                        if (view.includeAllColumns)
+                        switch (def.kind)
                         {
-                            ViewDefinition viewCopy = view.copy();
-                            viewCopy.metadata.addColumnDefinition(ColumnDefinition.regularDef(viewCopy.metadata, columnName.bytes, type));
-                            if (viewUpdates == null)
-                                viewUpdates = new ArrayList<>();
-                            viewUpdates.add(viewCopy);
+                            case PARTITION_KEY:
+                            case CLUSTERING:
+                                throw new InvalidRequestException(String.format("Invalid column name %s because it conflicts with a PRIMARY KEY part", columnName));
+                            default:
+                                throw new InvalidRequestException(String.format("Invalid column name %s because it conflicts with an existing column", columnName));
+                        }
+                    }
+
+                    AbstractType<?> type = validator.getType();
+                    if (type.isCollection() && type.isMultiCell())
+                    {
+                        if (!cfm.isCompound())
+                            throw new InvalidRequestException("Cannot use non-frozen collections in COMPACT STORAGE tables");
+                        if (cfm.isSuper())
+                            throw new InvalidRequestException("Cannot use non-frozen collections with super column families");
+                    }
+
+                    ColumnDefinition toAdd = isStatic
+                                           ? ColumnDefinition.staticDef(cfm, columnName.bytes, type)
+                                           : ColumnDefinition.regularDef(cfm, columnName.bytes, type);
+
+                    CFMetaData.DroppedColumn droppedColumn = meta.getDroppedColumns().get(columnName.bytes);
+                    if (null != droppedColumn)
+                    {
+                        if (droppedColumn.kind != toAdd.kind)
+                        {
+                            String message =
+                                String.format("Cannot re-add previously dropped column '%s' of kind %s, incompatible with previous kind %s",
+                                              columnName,
+                                              toAdd.kind,
+                                              droppedColumn.kind == null ? "UNKNOWN" : droppedColumn.kind);
+                            throw new InvalidRequestException(message);
+                        }
+                        // After #8099, not safe to re-add columns of incompatible types - until *maybe* deser logic with dropped
+                        // columns is pushed deeper down the line. The latter would still be problematic in cases of schema races.
+                        if (!type.isValueCompatibleWith(droppedColumn.type))
+                        {
+                            String message =
+                                String.format("Cannot re-add previously dropped column '%s' of type %s, incompatible with previous type %s",
+                                              columnName,
+                                              type.asCQL3Type(),
+                                              droppedColumn.type.asCQL3Type());
+                            throw new InvalidRequestException(message);
+                        }
+
+                        // Cannot re-add a dropped counter column. See #7831.
+                        if (meta.isCounter())
+                            throw new InvalidRequestException(String.format("Cannot re-add previously dropped counter column %s", columnName));
+                    }
+
+                    cfm.addColumnDefinition(toAdd);
+
+                    // Adding a column to a table which has an include all view requires the column to be added to the view as well
+                    if (!isStatic)
+                    {
+                        for (ViewDefinition view : views)
+                        {
+                            if (view.includeAllColumns)
+                            {
+                                ViewDefinition viewCopy = view.copy();
+                                viewCopy.metadata.addColumnDefinition(ColumnDefinition.regularDef(viewCopy.metadata, columnName.bytes, type));
+                                if (viewUpdates == null)
+                                    viewUpdates = new ArrayList<>();
+                                viewUpdates.add(viewCopy);
+                            }
                         }
                     }
                 }
                 break;
 
             case DROP:
-                assert columnName != null;
                 if (!meta.isCQLTable())
                     throw new InvalidRequestException("Cannot drop columns from a non-CQL3 table");
 
-                if (def == null)
-                    throw new InvalidRequestException(String.format("Column %s was not found in table %s", columnName, columnFamily()));
-
                 cfm = meta.copy();
 
-                switch (def.kind)
+                for (AlterTableStatementColumn colData : colNameList)
                 {
-                    case PARTITION_KEY:
-                    case CLUSTERING:
-                        throw new InvalidRequestException(String.format("Cannot drop PRIMARY KEY part %s", columnName));
-                    case REGULAR:
-                    case STATIC:
-                        ColumnDefinition toDelete = null;
-                        for (ColumnDefinition columnDef : cfm.partitionColumns())
-                        {
-                            if (columnDef.name.equals(columnName))
+                    columnName = colData.getColumnName().getIdentifier(cfm);
+                    def = cfm.getColumnDefinition(columnName);
+
+                    if (def == null)
+                        throw new InvalidRequestException(String.format("Column %s was not found in table %s", columnName, columnFamily()));
+
+                    switch (def.kind)
+                    {
+                        case PARTITION_KEY:
+                        case CLUSTERING:
+                            throw new InvalidRequestException(String.format("Cannot drop PRIMARY KEY part %s", columnName));
+                        case REGULAR:
+                        case STATIC:
+                            ColumnDefinition toDelete = null;
+                            for (ColumnDefinition columnDef : cfm.partitionColumns())
                             {
-                                toDelete = columnDef;
-                                break;
+                                if (columnDef.name.equals(columnName))
+                                {
+                                    toDelete = columnDef;
+                                    break;
+                                }
                             }
-                        }
-                        assert toDelete != null;
-                        cfm.removeColumnDefinition(toDelete);
-                        cfm.recordColumnDrop(toDelete, deleteTimestamp == null ? queryState.getTimestamp() : deleteTimestamp);
-                        break;
+                            assert toDelete != null;
+                            cfm.removeColumnDefinition(toDelete);
+                            cfm.recordColumnDrop(toDelete, deleteTimestamp == null ? queryState.getTimestamp() : deleteTimestamp);
+                            break;
+                    }
+
+                    // If the dropped column is required by any secondary indexes
+                    // we reject the operation, as the indexes must be dropped first
+                    Indexes allIndexes = cfm.getIndexes();
+                    if (!allIndexes.isEmpty())
+                    {
+                        ColumnFamilyStore store = Keyspace.openAndGetStore(cfm);
+                        Set<IndexMetadata> dependentIndexes = store.indexManager.getDependentIndexes(def);
+                        if (!dependentIndexes.isEmpty())
+                            throw new InvalidRequestException(String.format("Cannot drop column %s because it has " +
+                                                                            "dependent secondary indexes (%s)",
+                                                                            def,
+                                                                            dependentIndexes.stream()
+                                                                                            .map(i -> i.name)
+                                                                                            .collect(Collectors.joining(","))));
+                    }
+
+                    if (!Iterables.isEmpty(views))
+                        throw new InvalidRequestException(String.format("Cannot drop column %s on base table %s with materialized views.",
+                                                                        columnName.toString(),
+                                                                        columnFamily()));
                 }
 
-                // If the dropped column is required by any secondary indexes
-                // we reject the operation, as the indexes must be dropped first
-                Indexes allIndexes = cfm.getIndexes();
-                if (!allIndexes.isEmpty())
-                {
-                    ColumnFamilyStore store = Keyspace.openAndGetStore(cfm);
-                    Set<IndexMetadata> dependentIndexes = store.indexManager.getDependentIndexes(def);
-                    if (!dependentIndexes.isEmpty())
-                        throw new InvalidRequestException(String.format("Cannot drop column %s because it has " +
-                                                                        "dependent secondary indexes (%s)",
-                                                                        def,
-                                                                        dependentIndexes.stream()
-                                                                                        .map(i -> i.name)
-                                                                                        .collect(Collectors.joining(","))));
-                }
-
-                if (!Iterables.isEmpty(views))
-                    throw new InvalidRequestException(String.format("Cannot drop column %s on base table %s with materialized views.",
-                                                                    columnName.toString(),
-                                                                    columnFamily()));
                 break;
             case DROP_COMPACT_STORAGE:
 
@@ -328,11 +338,10 @@
             case RENAME:
                 cfm = meta.copy();
 
-                for (Map.Entry<ColumnIdentifier.Raw, ColumnIdentifier> entry : renames.entrySet())
+                for (Map.Entry<ColumnDefinition.Raw, ColumnDefinition.Raw> entry : renames.entrySet())
                 {
-                    ColumnIdentifier from = entry.getKey().prepare(cfm);
-                    ColumnIdentifier to = entry.getValue();
-
+                    ColumnIdentifier from = entry.getKey().getIdentifier(cfm);
+                    ColumnIdentifier to = entry.getValue().getIdentifier(cfm);
                     cfm.renameColumn(from, to);
 
                     // If the view includes a renamed column, it must be renamed in the view table and the definition.
@@ -341,8 +350,8 @@
                         if (!view.includes(from)) continue;
 
                         ViewDefinition viewCopy = view.copy();
-                        ColumnIdentifier viewFrom = entry.getKey().prepare(viewCopy.metadata);
-                        ColumnIdentifier viewTo = entry.getValue();
+                        ColumnIdentifier viewFrom = entry.getKey().getIdentifier(viewCopy.metadata);
+                        ColumnIdentifier viewTo = entry.getValue().getIdentifier(viewCopy.metadata);
                         viewCopy.renameColumn(viewFrom, viewTo);
 
                         if (viewUpdates == null)
@@ -394,7 +403,6 @@
 
             try
             {
-                // Note: 'storeRows' is basically a marker for 3.0+ sstables.
                 boolean has2xSStables = onComma.splitToList(sstableVersionsString)
                                                .stream()
                                                .map(VersionAndType::fromString)
@@ -431,10 +439,8 @@
 
     public String toString()
     {
-        return String.format("AlterTableStatement(name=%s, type=%s, column=%s, validator=%s)",
+        return String.format("AlterTableStatement(name=%s, type=%s)",
                              cfName,
-                             oType,
-                             rawColumnName,
-                             validator);
+                             oType);
     }
 }
diff --git a/src/java/org/apache/cassandra/cql3/statements/AlterTableStatementColumn.java b/src/java/org/apache/cassandra/cql3/statements/AlterTableStatementColumn.java
new file mode 100644
index 0000000..7dea565
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/AlterTableStatementColumn.java
@@ -0,0 +1,66 @@
+/*
+ * 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.statements;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.cql3.CQL3Type;
+
+/**
+ * Stores a column name and optionally type for an Alter Table statement definition.
+ *
+ * This is used by AlterTableStatement to store the added, altered or dropped columns.
+ */
+public class AlterTableStatementColumn
+{
+    private final CQL3Type.Raw dataType;
+    private final ColumnDefinition.Raw colName;
+    private final Boolean isStatic;
+
+    public AlterTableStatementColumn(ColumnDefinition.Raw colName, CQL3Type.Raw dataType, boolean isStatic)
+    {
+        assert colName != null;
+        this.dataType = dataType; // will be null when dropping columns, and never null otherwise (for ADD and ALTER).
+        this.colName = colName;
+        this.isStatic = isStatic;
+    }
+
+    public AlterTableStatementColumn(ColumnDefinition.Raw colName, CQL3Type.Raw dataType)
+    {
+        this(colName, dataType, false);
+    }
+
+    public AlterTableStatementColumn(ColumnDefinition.Raw colName)
+    {
+        this(colName, null, false);
+    }
+
+    public CQL3Type.Raw getColumnType()
+    {
+        return dataType;
+    }
+
+    public ColumnDefinition.Raw getColumnName()
+    {
+        return colName;
+    }
+
+    public Boolean getStaticType()
+    {
+        return isStatic;
+    }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java
index 0f506f7..d0e2f33 100644
--- a/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java
@@ -56,17 +56,17 @@
 
     protected abstract UserType makeUpdatedType(UserType toUpdate, KeyspaceMetadata ksm) throws InvalidRequestException;
 
-    public static AlterTypeStatement addition(UTName name, ColumnIdentifier fieldName, CQL3Type.Raw type)
+    public static AlterTypeStatement addition(UTName name, FieldIdentifier fieldName, CQL3Type.Raw type)
     {
         return new Add(name, fieldName, type);
     }
 
-    public static AlterTypeStatement alter(UTName name, ColumnIdentifier fieldName, CQL3Type.Raw type)
+    public static AlterTypeStatement alter(UTName name, FieldIdentifier fieldName, CQL3Type.Raw type)
     {
         throw new InvalidRequestException("Altering of types is not allowed");
     }
 
-    public static AlterTypeStatement renames(UTName name, Map<ColumnIdentifier, ColumnIdentifier> renames)
+    public static AlterTypeStatement renames(UTName name, Map<FieldIdentifier, FieldIdentifier> renames)
     {
         return new Renames(name, renames);
     }
@@ -142,14 +142,6 @@
         return new Event.SchemaChange(Event.SchemaChange.Change.UPDATED, Event.SchemaChange.Target.TYPE, keyspace(), name.getStringTypeName());
     }
 
-    private static int getIdxOfField(UserType type, ColumnIdentifier field)
-    {
-        for (int i = 0; i < type.size(); i++)
-            if (field.bytes.equals(type.fieldName(i)))
-                return i;
-        return -1;
-    }
-
     private boolean updateDefinition(CFMetaData cfm, ColumnDefinition def, String keyspace, ByteBuffer toReplace, UserType updated)
     {
         AbstractType<?> t = updateWith(def.type, keyspace, toReplace, updated);
@@ -171,11 +163,11 @@
 
             // If it's directly the type we've updated, then just use the new one.
             if (keyspace.equals(ut.keyspace) && toReplace.equals(ut.name))
-                return updated;
+                return type.isMultiCell() ? updated : updated.freeze();
 
             // Otherwise, check for nesting
             List<AbstractType<?>> updatedTypes = updateTypes(ut.fieldTypes(), keyspace, toReplace, updated);
-            return updatedTypes == null ? null : new UserType(ut.keyspace, ut.name, new ArrayList<>(ut.fieldNames()), updatedTypes);
+            return updatedTypes == null ? null : new UserType(ut.keyspace, ut.name, new ArrayList<>(ut.fieldNames()), updatedTypes, type.isMultiCell());
         }
         else if (type instanceof TupleType)
         {
@@ -251,10 +243,10 @@
 
     private static class Add extends AlterTypeStatement
     {
-        private final ColumnIdentifier fieldName;
+        private final FieldIdentifier fieldName;
         private final CQL3Type.Raw type;
 
-        public Add(UTName name, ColumnIdentifier fieldName, CQL3Type.Raw type)
+        public Add(UTName name, FieldIdentifier fieldName, CQL3Type.Raw type)
         {
             super(name);
             this.fieldName = fieldName;
@@ -263,12 +255,12 @@
 
         protected UserType makeUpdatedType(UserType toUpdate, KeyspaceMetadata ksm) throws InvalidRequestException
         {
-            if (getIdxOfField(toUpdate, fieldName) >= 0)
+            if (toUpdate.fieldPosition(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.size() + 1);
+            List<FieldIdentifier> newNames = new ArrayList<>(toUpdate.size() + 1);
             newNames.addAll(toUpdate.fieldNames());
-            newNames.add(fieldName.bytes);
+            newNames.add(fieldName);
 
             AbstractType<?> addType = type.prepare(keyspace()).getType();
             if (addType.referencesUserType(toUpdate.getNameAsString()))
@@ -288,7 +280,7 @@
             newTypes.addAll(toUpdate.fieldTypes());
             newTypes.add(addType);
 
-            return new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes);
+            return new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes, toUpdate.isMultiCell());
         }
 
         private static Collection<CFMetaData> findTablesReferencingTypeInPartitionKey(KeyspaceMetadata keyspace, String userTypeName)
@@ -303,9 +295,9 @@
 
     private static class Renames extends AlterTypeStatement
     {
-        private final Map<ColumnIdentifier, ColumnIdentifier> renames;
+        private final Map<FieldIdentifier, FieldIdentifier> renames;
 
-        public Renames(UTName name, Map<ColumnIdentifier, ColumnIdentifier> renames)
+        public Renames(UTName name, Map<FieldIdentifier, FieldIdentifier> renames)
         {
             super(name);
             this.renames = renames;
@@ -315,20 +307,20 @@
         {
             checkTypeNotUsedByAggregate(ksm);
 
-            List<ByteBuffer> newNames = new ArrayList<>(toUpdate.fieldNames());
+            List<FieldIdentifier> newNames = new ArrayList<>(toUpdate.fieldNames());
             List<AbstractType<?>> newTypes = new ArrayList<>(toUpdate.fieldTypes());
 
-            for (Map.Entry<ColumnIdentifier, ColumnIdentifier> entry : renames.entrySet())
+            for (Map.Entry<FieldIdentifier, FieldIdentifier> entry : renames.entrySet())
             {
-                ColumnIdentifier from = entry.getKey();
-                ColumnIdentifier to = entry.getValue();
-                int idx = getIdxOfField(toUpdate, from);
+                FieldIdentifier from = entry.getKey();
+                FieldIdentifier to = entry.getValue();
+                int idx = toUpdate.fieldPosition(from);
                 if (idx < 0)
                     throw new InvalidRequestException(String.format("Unknown field %s in type %s", from, name));
-                newNames.set(idx, to.bytes);
+                newNames.set(idx, to);
             }
 
-            UserType updated = new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes);
+            UserType updated = new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes, toUpdate.isMultiCell());
             CreateTypeStatement.checkForDuplicateNames(updated);
             return updated;
         }
diff --git a/src/java/org/apache/cassandra/cql3/statements/AlterViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/AlterViewStatement.java
index ea87cfd..91c5462 100644
--- a/src/java/org/apache/cassandra/cql3/statements/AlterViewStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/AlterViewStatement.java
@@ -79,9 +79,10 @@
 
         if (params.defaultTimeToLive > 0)
         {
-            throw new InvalidRequestException("Cannot set or alter default_time_to_live for a materialized view. " +
+            throw new InvalidRequestException("Forbidden default_time_to_live detected for a materialized view. " +
                                               "Data in a materialized view always expire at the same time than " +
-                                              "the corresponding data in the parent table.");
+                                              "the corresponding data in the parent table. default_time_to_live " +
+                                              "must be set to zero, see CASSANDRA-12868 for more information");
         }
 
         viewCopy.metadata.params(params);
diff --git a/src/java/org/apache/cassandra/cql3/statements/AuthenticationStatement.java b/src/java/org/apache/cassandra/cql3/statements/AuthenticationStatement.java
index 30ab6b0..646e0f4 100644
--- a/src/java/org/apache/cassandra/cql3/statements/AuthenticationStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/AuthenticationStatement.java
@@ -41,7 +41,7 @@
         return 0;
     }
 
-    public ResultMessage execute(QueryState state, QueryOptions options)
+    public ResultMessage execute(QueryState state, QueryOptions options, long queryStartNanoTime)
     throws RequestExecutionException, RequestValidationException
     {
         return execute(state.getClientState());
diff --git a/src/java/org/apache/cassandra/cql3/statements/AuthorizationStatement.java b/src/java/org/apache/cassandra/cql3/statements/AuthorizationStatement.java
index fa2a993..90fee36 100644
--- a/src/java/org/apache/cassandra/cql3/statements/AuthorizationStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/AuthorizationStatement.java
@@ -42,7 +42,7 @@
         return 0;
     }
 
-    public ResultMessage execute(QueryState state, QueryOptions options)
+    public ResultMessage execute(QueryState state, QueryOptions options, long queryStartNanoTime)
     throws RequestValidationException, RequestExecutionException
     {
         return execute(state.getClientState());
diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java
index 235e7e6..11a20f4 100644
--- a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java
@@ -21,8 +21,10 @@
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 
-import com.google.common.base.Function;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.HashMultiset;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.helpers.MessageFormatter;
@@ -38,6 +40,7 @@
 import org.apache.cassandra.service.*;
 import org.apache.cassandra.tracing.Tracing;
 import org.apache.cassandra.transport.messages.ResultMessage;
+import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.NoSpamLogger;
 import org.apache.cassandra.utils.Pair;
 
@@ -216,11 +219,24 @@
         return statements;
     }
 
-    private Collection<? extends IMutation> getMutations(BatchQueryOptions options, boolean local, long now)
+    @VisibleForTesting
+    public Collection<? extends IMutation> getMutations(BatchQueryOptions options, boolean local, long now, long queryStartNanoTime)
     throws RequestExecutionException, RequestValidationException
     {
         Set<String> tablesWithZeroGcGs = null;
-        UpdatesCollector collector = new UpdatesCollector(updatedColumns, updatedRows());
+        List<List<ByteBuffer>> partitionKeys = new ArrayList<>(statements.size());
+        Map<UUID, HashMultiset<ByteBuffer>> partitionCounts = Maps.newHashMapWithExpectedSize(updatedColumns.size());
+        for (int i = 0, isize = statements.size(); i < isize; i++)
+        {
+            ModificationStatement stmt = statements.get(i);
+            List<ByteBuffer> stmtPartitionKeys = stmt.buildPartitionKeyNames(options.forStatement(i));
+            partitionKeys.add(stmtPartitionKeys);
+            HashMultiset<ByteBuffer> perKeyCountsForTable = partitionCounts.computeIfAbsent(stmt.cfm.cfId, k -> HashMultiset.create());
+            for (int stmtIdx = 0, stmtSize = stmtPartitionKeys.size(); stmtIdx < stmtSize; stmtIdx++)
+                perKeyCountsForTable.add(stmtPartitionKeys.get(stmtIdx));
+        }
+
+        UpdatesCollector collector = new UpdatesCollector(updatedColumns, partitionCounts);
         for (int i = 0; i < statements.size(); i++)
         {
             ModificationStatement statement = statements.get(i);
@@ -232,7 +248,7 @@
             }
             QueryOptions statementOptions = options.forStatement(i);
             long timestamp = attrs.getTimestamp(now, statementOptions);
-            statement.addUpdates(collector, statementOptions, local, timestamp);
+            statement.addUpdates(collector, partitionKeys.get(i), statementOptions, local, timestamp, queryStartNanoTime);
         }
 
         if (tablesWithZeroGcGs != null)
@@ -248,67 +264,76 @@
         return collector.toMutations();
     }
 
-    private int updatedRows()
-    {
-        // Note: it's possible for 2 statements to actually apply to the same row, but that's just an estimation
-        // for sizing our PartitionUpdate backing array, so it's good enough.
-        return statements.size();
-    }
-
     /**
      * Checks batch size to ensure threshold is met. If not, a warning is logged.
      *
-     * @param cfs ColumnFamilies that will store the batch's mutations.
+     * @param mutations - the batch mutations.
      */
-    public static void verifyBatchSize(Iterable<PartitionUpdate> updates) throws InvalidRequestException
+    private static void verifyBatchSize(Collection<? extends IMutation> mutations) throws InvalidRequestException
     {
+        // We only warn for batch spanning multiple mutations (#10876)
+        if (mutations.size() <= 1)
+            return;
+
         long size = 0;
         long warnThreshold = DatabaseDescriptor.getBatchSizeWarnThreshold();
-        long failThreshold = DatabaseDescriptor.getBatchSizeFailThreshold();
 
-        for (PartitionUpdate update : updates)
-            size += update.dataSize();
+        for (IMutation mutation : mutations)
+        {
+            for (PartitionUpdate update : mutation.getPartitionUpdates())
+                size += update.dataSize();
+        }
 
         if (size > warnThreshold)
         {
             Set<String> tableNames = new HashSet<>();
-            for (PartitionUpdate update : updates)
-                tableNames.add(String.format("%s.%s", update.metadata().ksName, update.metadata().cfName));
+            for (IMutation mutation : mutations)
+            {
+                for (PartitionUpdate update : mutation.getPartitionUpdates())
+                    tableNames.add(String.format("%s.%s", update.metadata().ksName, update.metadata().cfName));
+            }
 
-            String format = "Batch of prepared statements for {} is of size {}, exceeding specified threshold of {} by {}.{}";
+            long failThreshold = DatabaseDescriptor.getBatchSizeFailThreshold();
+
+            String format = "Batch for {} is of size {}, exceeding specified threshold of {} by {}.{}";
             if (size > failThreshold)
             {
-                Tracing.trace(format, tableNames, size, failThreshold, size - failThreshold, " (see batch_size_fail_threshold_in_kb)");
-                logger.error(format, tableNames, size, failThreshold, size - failThreshold, " (see batch_size_fail_threshold_in_kb)");
+                Tracing.trace(format, tableNames, FBUtilities.prettyPrintMemory(size), FBUtilities.prettyPrintMemory(failThreshold),
+                              FBUtilities.prettyPrintMemory(size - failThreshold), " (see batch_size_fail_threshold_in_kb)");
+                logger.error(format, tableNames, FBUtilities.prettyPrintMemory(size), FBUtilities.prettyPrintMemory(failThreshold),
+                             FBUtilities.prettyPrintMemory(size - failThreshold), " (see batch_size_fail_threshold_in_kb)");
                 throw new InvalidRequestException("Batch too large");
             }
             else if (logger.isWarnEnabled())
             {
-                logger.warn(format, tableNames, size, warnThreshold, size - warnThreshold, "");
+                logger.warn(format, tableNames, FBUtilities.prettyPrintMemory(size), FBUtilities.prettyPrintMemory(warnThreshold),
+                            FBUtilities.prettyPrintMemory(size - warnThreshold), "");
             }
             ClientWarn.instance.warn(MessageFormatter.arrayFormat(format, new Object[] {tableNames, size, warnThreshold, size - warnThreshold, ""}).getMessage());
         }
     }
 
-    private void verifyBatchType(Iterable<PartitionUpdate> updates)
+    private void verifyBatchType(Collection<? extends IMutation> mutations)
     {
-        if (!isLogged() && Iterables.size(updates) > 1)
+        if (!isLogged() && mutations.size() > 1)
         {
             Set<DecoratedKey> keySet = new HashSet<>();
             Set<String> tableNames = new HashSet<>();
 
-            for (PartitionUpdate update : updates)
+            for (IMutation mutation : mutations)
             {
-                keySet.add(update.partitionKey());
+                for (PartitionUpdate update : mutation.getPartitionUpdates())
+                {
+                    keySet.add(update.partitionKey());
 
-                tableNames.add(String.format("%s.%s", update.metadata().ksName, update.metadata().cfName));
+                    tableNames.add(String.format("%s.%s", update.metadata().ksName, update.metadata().cfName));
+                }
             }
 
             // CASSANDRA-11529: log only if we have more than a threshold of keys, this was also suggested in the
             // original ticket that introduced this warning, CASSANDRA-9282
             if (keySet.size() > DatabaseDescriptor.getUnloggedBatchAcrossPartitionsWarnThreshold())
             {
-
                 NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.MINUTES, UNLOGGED_BATCH_WARNING,
                                  keySet.size(), tableNames.size() == 1 ? "" : "s", tableNames);
 
@@ -319,17 +344,17 @@
     }
 
 
-    public ResultMessage execute(QueryState queryState, QueryOptions options) throws RequestExecutionException, RequestValidationException
+    public ResultMessage execute(QueryState queryState, QueryOptions options, long queryStartNanoTime) throws RequestExecutionException, RequestValidationException
     {
-        return execute(queryState, BatchQueryOptions.withoutPerStatementVariables(options));
+        return execute(queryState, BatchQueryOptions.withoutPerStatementVariables(options), queryStartNanoTime);
     }
 
-    public ResultMessage execute(QueryState queryState, BatchQueryOptions options) throws RequestExecutionException, RequestValidationException
+    public ResultMessage execute(QueryState queryState, BatchQueryOptions options, long queryStartNanoTime) throws RequestExecutionException, RequestValidationException
     {
-        return execute(queryState, options, false, options.getTimestamp(queryState));
+        return execute(queryState, options, false, options.getTimestamp(queryState), queryStartNanoTime);
     }
 
-    private ResultMessage execute(QueryState queryState, BatchQueryOptions options, boolean local, long now)
+    private ResultMessage execute(QueryState queryState, BatchQueryOptions options, boolean local, long now, long queryStartNanoTime)
     throws RequestExecutionException, RequestValidationException
     {
         if (options.getConsistency() == null)
@@ -338,34 +363,25 @@
             throw new InvalidRequestException("Invalid empty serial consistency level");
 
         if (hasConditions)
-            return executeWithConditions(options, queryState);
+            return executeWithConditions(options, queryState, queryStartNanoTime);
 
-        executeWithoutConditions(getMutations(options, local, now), options.getConsistency());
+        executeWithoutConditions(getMutations(options, local, now, queryStartNanoTime), options.getConsistency(), queryStartNanoTime);
         return new ResultMessage.Void();
     }
 
-    private void executeWithoutConditions(Collection<? extends IMutation> mutations, ConsistencyLevel cl) throws RequestExecutionException, RequestValidationException
+    private void executeWithoutConditions(Collection<? extends IMutation> mutations, ConsistencyLevel cl, long queryStartNanoTime) throws RequestExecutionException, RequestValidationException
     {
         if (mutations.isEmpty())
             return;
 
-        // Extract each collection of updates from it's IMutation and then lazily concatenate all of them into a single Iterable.
-        Iterable<PartitionUpdate> updates = Iterables.concat(Iterables.transform(mutations, new Function<IMutation, Collection<PartitionUpdate>>()
-        {
-            public Collection<PartitionUpdate> apply(IMutation im)
-            {
-                return im.getPartitionUpdates();
-            }
-        }));
-
-        verifyBatchSize(updates);
-        verifyBatchType(updates);
+        verifyBatchSize(mutations);
+        verifyBatchType(mutations);
 
         boolean mutateAtomic = (isLogged() && mutations.size() > 1);
-        StorageProxy.mutateWithTriggers(mutations, cl, mutateAtomic);
+        StorageProxy.mutateWithTriggers(mutations, cl, mutateAtomic, queryStartNanoTime);
     }
 
-    private ResultMessage executeWithConditions(BatchQueryOptions options, QueryState state)
+    private ResultMessage executeWithConditions(BatchQueryOptions options, QueryState state, long queryStartNanoTime)
     throws RequestExecutionException, RequestValidationException
     {
         Pair<CQL3CasRequest, Set<ColumnDefinition>> p = makeCasRequest(options, state);
@@ -381,7 +397,8 @@
                                                    casRequest,
                                                    options.getSerialConsistency(),
                                                    options.getConsistency(),
-                                                   state.getClientState()))
+                                                   state.getClientState(),
+                                                   queryStartNanoTime))
         {
             return new ResultMessage.Rows(ModificationStatement.buildCasResultSet(ksName, tableName, result, columnsWithConditions, true, options.forStatement(0)));
         }
@@ -457,21 +474,14 @@
         if (hasConditions)
             return executeInternalWithConditions(BatchQueryOptions.withoutPerStatementVariables(options), queryState);
 
-        executeInternalWithoutCondition(queryState, options);
+        executeInternalWithoutCondition(queryState, options, System.nanoTime());
         return new ResultMessage.Void();
     }
 
-    private ResultMessage executeInternalWithoutCondition(QueryState queryState, QueryOptions options) throws RequestValidationException, RequestExecutionException
+    private ResultMessage executeInternalWithoutCondition(QueryState queryState, QueryOptions options, long queryStartNanoTime) throws RequestValidationException, RequestExecutionException
     {
-        for (IMutation mutation : getMutations(BatchQueryOptions.withoutPerStatementVariables(options), true, queryState.getTimestamp()))
-        {
-            assert mutation instanceof Mutation || mutation instanceof CounterMutation;
-
-            if (mutation instanceof Mutation)
-                ((Mutation) mutation).apply();
-            else if (mutation instanceof CounterMutation)
-                ((CounterMutation) mutation).apply();
-        }
+        for (IMutation mutation : getMutations(BatchQueryOptions.withoutPerStatementVariables(options), true, queryState.getTimestamp(), queryStartNanoTime))
+            mutation.apply();
         return null;
     }
 
@@ -552,7 +562,7 @@
 
             // Use the CFMetadata of the first statement for partition key bind indexes.  If the statements affect
             // multiple tables, we won't send partition key bind indexes.
-            Short[] partitionKeyBindIndexes = (haveMultipleCFs || batchStatement.statements.isEmpty())? null
+            short[] partitionKeyBindIndexes = (haveMultipleCFs || batchStatement.statements.isEmpty())? null
                                                               : boundNames.getPartitionKeyBindIndexes(batchStatement.statements.get(0).cfm);
 
             return new ParsedStatement.Prepared(batchStatement, boundNames, partitionKeyBindIndexes, null, isFullyQualified());
@@ -576,7 +586,7 @@
 
         public Map<UUID, PartitionColumns> build()
         {
-            Map<UUID, PartitionColumns> m = new HashMap<>();
+            Map<UUID, PartitionColumns> m = Maps.newHashMapWithExpectedSize(perTableBuilders.size());
             for (Map.Entry<UUID, PartitionColumns.Builder> p : perTableBuilders.entrySet())
                 m.put(p.getKey(), p.getValue().build());
             return m;
diff --git a/src/java/org/apache/cassandra/cql3/statements/Bound.java b/src/java/org/apache/cassandra/cql3/statements/Bound.java
index 7742642..824743c 100644
--- a/src/java/org/apache/cassandra/cql3/statements/Bound.java
+++ b/src/java/org/apache/cassandra/cql3/statements/Bound.java
@@ -17,6 +17,8 @@
  */
 package org.apache.cassandra.cql3.statements;
 
+import org.apache.cassandra.config.ColumnDefinition;
+
 public enum Bound
 {
     START(0), END(1);
@@ -28,6 +30,17 @@
         this.idx = idx;
     }
 
+    /**
+     * Reverses the bound if the column type is a reversed one.
+     *
+     * @param columnDefinition the column definition
+     * @return the bound reversed if the column type was a reversed one or the original bound
+     */
+    public Bound reverseIfNeeded(ColumnDefinition columnDefinition)
+    {
+        return columnDefinition.isReversedType() ? reverse() : this;
+    }
+
     public Bound reverse()
     {
         return isStart() ? END : START;
diff --git a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java
index e14ae6c..47920a4 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java
@@ -236,10 +236,6 @@
             upd.applyUpdates(current, update);
 
         Keyspace.openAndGetStore(cfm).indexManager.validate(update);
-
-        if (isBatch)
-            BatchStatement.verifyBatchSize(Collections.singleton(update));
-
         return update;
     }
 
@@ -289,6 +285,7 @@
 
         public void applyUpdates(FilteredPartition current, PartitionUpdate updates) throws InvalidRequestException
         {
+            // No slice statements currently require a read, but this maintains consistency with RowUpdate, and future proofs us
             Map<DecoratedKey, Partition> map = stmt.requiresRead() ? Collections.<DecoratedKey, Partition>singletonMap(key, current) : null;
             UpdateParameters params = new UpdateParameters(cfm, updates.columns(), options, timestamp, stmt.getTimeToLive(options), map);
             stmt.addUpdateForKey(updates, slice, params);
diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateAggregateStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateAggregateStatement.java
index 2210a2d..6b0cb93 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateAggregateStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateAggregateStatement.java
@@ -36,7 +36,7 @@
 import org.apache.cassandra.service.QueryState;
 import org.apache.cassandra.thrift.ThriftValidation;
 import org.apache.cassandra.transport.Event;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * A {@code CREATE AGGREGATE} statement parsed from a CQL query.
@@ -132,7 +132,7 @@
             }
 
             // Sanity check that converts the initcond to a CQL literal and parse it back to avoid getting in CASSANDRA-11064.
-            String initcondAsCql = stateType.asCQL3Type().toCQLLiteral(initcond, Server.CURRENT_VERSION);
+            String initcondAsCql = stateType.asCQL3Type().toCQLLiteral(initcond, ProtocolVersion.CURRENT);
             assert Objects.equals(initcond, Terms.asBytes(functionName.keyspace, initcondAsCql, stateType));
 
             if (Constants.NULL_LITERAL != ival && UDHelper.isNullOrEmpty(stateType, initcond))
@@ -144,7 +144,7 @@
 
     private AbstractType<?> prepareType(String typeName, CQL3Type.Raw rawType)
     {
-        if (rawType.isFrozen())
+        if (!rawType.isTuple() && rawType.isFrozen())
             throw new InvalidRequestException(String.format("The function %s should not be frozen; remove the frozen<> modifier", typeName));
 
         // UDT are not supported non frozen but we do not allow the frozen keyword for argument. So for the moment we
diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateFunctionStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateFunctionStatement.java
index fc65804..f9fae6f 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateFunctionStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateFunctionStatement.java
@@ -173,7 +173,7 @@
 
     private AbstractType<?> prepareType(String typeName, CQL3Type.Raw rawType)
     {
-        if (rawType.isFrozen())
+        if (!rawType.isTuple() && rawType.isFrozen())
             throw new InvalidRequestException(String.format("The function %s should not be frozen; remove the frozen<> modifier", typeName));
 
         // UDT are not supported non frozen but we do not allow the frozen keyword for argument. So for the moment we
diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateIndexStatement.java
index e0b9b02..f51ad44 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateIndexStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateIndexStatement.java
@@ -22,12 +22,14 @@
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.auth.Permission;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.cql3.CFName;
 import org.apache.cassandra.cql3.ColumnIdentifier;
@@ -36,14 +38,19 @@
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.exceptions.RequestValidationException;
 import org.apache.cassandra.exceptions.UnauthorizedException;
+import org.apache.cassandra.index.sasi.SASIIndex;
 import org.apache.cassandra.schema.IndexMetadata;
 import org.apache.cassandra.schema.Indexes;
 import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.ClientWarn;
 import org.apache.cassandra.service.MigrationManager;
 import org.apache.cassandra.service.QueryState;
 import org.apache.cassandra.thrift.ThriftValidation;
 import org.apache.cassandra.transport.Event;
 
+import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
+import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
+
 /** A <code>CREATE INDEX</code> statement parsed from a CQL query. */
 public class CreateIndexStatement extends SchemaAlteringStatement
 {
@@ -102,6 +109,14 @@
             if (cd == null)
                 throw new InvalidRequestException("No column definition found for column " + target.column);
 
+            if (cd.type.referencesDuration())
+            {
+                checkFalse(cd.type.isCollection(), "Secondary indexes are not supported on collections containing durations");
+                checkFalse(cd.type.isTuple(), "Secondary indexes are not supported on tuples containing durations");
+                checkFalse(cd.type.isUDT(), "Secondary indexes are not supported on UDTs containing durations");
+                throw invalidRequest("Secondary indexes are not supported on duration columns");
+            }
+
             // TODO: we could lift that limitation
             if (cfm.isCompactTable())
             {
@@ -111,15 +126,6 @@
                     throw new InvalidRequestException("Secondary indexes are not supported on compact value column of COMPACT STORAGE tables");
             }
 
-            // It would be possible to support 2ndary index on static columns (but not without modifications of at least ExtendedFilter and
-            // CompositesIndex) and maybe we should, but that means a query like:
-            //     SELECT * FROM foo WHERE static_column = 'bar'
-            // would pull the full partition every time the static column of partition is 'bar', which sounds like offering a
-            // fair potential for foot-shooting, so I prefer leaving that to a follow up ticket once we have identified cases where
-            // such indexing is actually useful.
-            if (!cfm.isCompactTable() && cd.isStatic())
-                throw new InvalidRequestException("Secondary indexes are not allowed on static columns");
-
             if (cd.kind == ColumnDefinition.Kind.PARTITION_KEY && cfm.getKeyValidatorAsClusteringComparator().size() == 1)
                 throw new InvalidRequestException(String.format("Cannot create secondary index on partition key column %s", target.column));
 
@@ -135,6 +141,8 @@
                 validateIsSimpleIndexIfTargetColumnNotCollection(cd, target);
                 validateTargetColumnIsMapIfIndexInvolvesKeys(isMap, target);
             }
+
+            checkFalse(cd.type.isUDT() && cd.type.isMultiCell(), "Secondary indexes are not supported on non-frozen UDTs");
         }
 
         if (!Strings.isNullOrEmpty(indexName))
@@ -188,7 +196,7 @@
         if (!properties.isCustom)
             throw new InvalidRequestException("Only CUSTOM indexes support multiple columns");
 
-        Set<ColumnIdentifier> columns = new HashSet<>();
+        Set<ColumnIdentifier> columns = Sets.newHashSetWithExpectedSize(targets.size());
         for (IndexTarget target : targets)
             if (!columns.add(target.column))
                 throw new InvalidRequestException("Duplicate column " + target.column + " in index target list");
@@ -223,6 +231,17 @@
         {
             kind = IndexMetadata.Kind.CUSTOM;
             indexOptions = properties.getOptions();
+
+            if (properties.customClass.equals(SASIIndex.class.getName()))
+            {
+                if (!DatabaseDescriptor.getEnableSASIIndexes())
+                    throw new InvalidRequestException("SASI indexes are disabled. Enable in cassandra.yaml to use.");
+
+                logger.warn("Creating SASI index {} for {}.{}. {}",
+                            acceptedName, cfm.ksName, cfm.cfName, SASIIndex.USAGE_WARNING);
+
+                ClientWarn.instance.warn(SASIIndex.USAGE_WARNING);
+            }
         }
         else
         {
diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateKeyspaceStatement.java
index 787dc73..b34ae26 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateKeyspaceStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateKeyspaceStatement.java
@@ -17,9 +17,11 @@
  */
 package org.apache.cassandra.cql3.statements;
 
+import java.util.regex.Pattern;
+
 import org.apache.cassandra.auth.*;
 import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.exceptions.*;
 import org.apache.cassandra.locator.LocalStrategy;
 import org.apache.cassandra.schema.KeyspaceMetadata;
@@ -31,6 +33,8 @@
 /** A <code>CREATE KEYSPACE</code> statement parsed from a CQL query. */
 public class CreateKeyspaceStatement extends SchemaAlteringStatement
 {
+    private static final Pattern PATTERN_WORD_CHARS = Pattern.compile("\\w+");
+
     private final String name;
     private final KeyspaceAttributes attrs;
     private final boolean ifNotExists;
@@ -73,10 +77,10 @@
         ThriftValidation.validateKeyspaceNotSystem(name);
 
         // keyspace name
-        if (!name.matches("\\w+"))
+        if (!PATTERN_WORD_CHARS.matcher(name).matches())
             throw new InvalidRequestException(String.format("\"%s\" is not a valid keyspace name", name));
-        if (name.length() > Schema.NAME_LENGTH)
-            throw new InvalidRequestException(String.format("Keyspace names shouldn't be more than %s characters long (got \"%s\")", Schema.NAME_LENGTH, name));
+        if (name.length() > SchemaConstants.NAME_LENGTH)
+            throw new InvalidRequestException(String.format("Keyspace names shouldn't be more than %s characters long (got \"%s\")", SchemaConstants.NAME_LENGTH, name));
 
         attrs.validate();
 
diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateTableStatement.java
index 9f14194..90ed288 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateTableStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateTableStatement.java
@@ -19,7 +19,7 @@
 
 import java.nio.ByteBuffer;
 import java.util.*;
-
+import java.util.regex.Pattern;
 import com.google.common.collect.HashMultiset;
 import com.google.common.collect.Multiset;
 import org.apache.commons.lang3.StringUtils;
@@ -41,10 +41,12 @@
 /** A {@code CREATE TABLE} parsed from a CQL query statement. */
 public class CreateTableStatement extends SchemaAlteringStatement
 {
+    private static final Pattern PATTERN_WORD_CHARS = Pattern.compile("\\w+");
+
     private List<AbstractType<?>> keyTypes;
     private List<AbstractType<?>> clusteringTypes;
 
-    private final Map<ByteBuffer, CollectionType> collections = new HashMap<>();
+    private final Map<ByteBuffer, AbstractType> multicellColumns = new HashMap<>();
 
     private final List<ColumnIdentifier> keyAliases = new ArrayList<>();
     private final List<ColumnIdentifier> columnAliases = new ArrayList<>();
@@ -202,10 +204,10 @@
         public ParsedStatement.Prepared prepare(Types udts) throws RequestValidationException
         {
             // Column family name
-            if (!columnFamily().matches("\\w+"))
+            if (!PATTERN_WORD_CHARS.matcher(columnFamily()).matches())
                 throw new InvalidRequestException(String.format("\"%s\" is not a valid table name (must be alphanumeric character or underscore only: [a-zA-Z_0-9]+)", columnFamily()));
-            if (columnFamily().length() > Schema.NAME_LENGTH)
-                throw new InvalidRequestException(String.format("Table names shouldn't be more than %s characters long (got \"%s\")", Schema.NAME_LENGTH, columnFamily()));
+            if (columnFamily().length() > SchemaConstants.NAME_LENGTH)
+                throw new InvalidRequestException(String.format("Table names shouldn't be more than %s characters long (got \"%s\")", SchemaConstants.NAME_LENGTH, columnFamily()));
 
             for (Multiset.Entry<ColumnIdentifier> entry : definedNames.entrySet())
                 if (entry.getCount() > 1)
@@ -221,10 +223,24 @@
             {
                 ColumnIdentifier id = entry.getKey();
                 CQL3Type pt = entry.getValue().prepare(keyspace(), udts);
-                if (pt.isCollection() && ((CollectionType)pt.getType()).isMultiCell())
-                    stmt.collections.put(id.bytes, (CollectionType)pt.getType());
+                if (pt.getType().isMultiCell())
+                    stmt.multicellColumns.put(id.bytes, pt.getType());
                 if (entry.getValue().isCounter())
                     stmt.hasCounters = true;
+
+                // check for non-frozen UDTs or collections in a non-frozen UDT
+                if (pt.getType().isUDT() && pt.getType().isMultiCell())
+                {
+                    for (AbstractType<?> innerType : ((UserType) pt.getType()).fieldTypes())
+                    {
+                        if (innerType.isMultiCell())
+                        {
+                            assert innerType.isCollection();  // shouldn't get this far with a nested non-frozen UDT
+                            throw new InvalidRequestException("Non-frozen UDTs with nested non-frozen collections are not supported");
+                        }
+                    }
+                }
+
                 stmt.columns.put(id, pt.getType()); // we'll remove what is not a column below
             }
 
@@ -243,6 +259,8 @@
                 AbstractType<?> t = getTypeAndRemove(stmt.columns, alias);
                 if (t.asCQL3Type().getType() instanceof CounterColumnType)
                     throw new InvalidRequestException(String.format("counter type is not supported for PRIMARY KEY part %s", alias));
+                if (t.asCQL3Type().getType().referencesDuration())
+                    throw new InvalidRequestException(String.format("duration type is not supported for PRIMARY KEY part %s", alias));
                 if (staticColumns.contains(alias))
                     throw new InvalidRequestException(String.format("Static column %s cannot be part of the PRIMARY KEY", alias));
                 stmt.keyTypes.add(t);
@@ -257,6 +275,8 @@
                 AbstractType<?> type = getTypeAndRemove(stmt.columns, t);
                 if (type.asCQL3Type().getType() instanceof CounterColumnType)
                     throw new InvalidRequestException(String.format("counter type is not supported for PRIMARY KEY part %s", t));
+                if (type.asCQL3Type().getType().referencesDuration())
+                    throw new InvalidRequestException(String.format("duration type is not supported for PRIMARY KEY part %s", t));
                 if (staticColumns.contains(t))
                     throw new InvalidRequestException(String.format("Static column %s cannot be part of the PRIMARY KEY", t));
                 stmt.clusteringTypes.add(type);
@@ -283,8 +303,8 @@
             // For COMPACT STORAGE, we reject any "feature" that we wouldn't be able to translate back to thrift.
             if (useCompactStorage)
             {
-                if (!stmt.collections.isEmpty())
-                    throw new InvalidRequestException("Non-frozen collection types are not supported with COMPACT STORAGE");
+                if (!stmt.multicellColumns.isEmpty())
+                    throw new InvalidRequestException("Non-frozen collections and UDTs are not supported with COMPACT STORAGE");
                 if (!staticColumns.isEmpty())
                     throw new InvalidRequestException("Static columns are not supported in COMPACT STORAGE tables");
 
@@ -348,8 +368,13 @@
             AbstractType type = columns.get(t);
             if (type == null)
                 throw new InvalidRequestException(String.format("Unknown definition %s referenced in PRIMARY KEY", t));
-            if (type.isCollection() && type.isMultiCell())
-                throw new InvalidRequestException(String.format("Invalid collection type for PRIMARY KEY component %s", t));
+            if (type.isMultiCell())
+            {
+                if (type.isCollection())
+                    throw new InvalidRequestException(String.format("Invalid non-frozen collection type for PRIMARY KEY component %s", t));
+                else
+                    throw new InvalidRequestException(String.format("Invalid non-frozen user-defined type for PRIMARY KEY component %s", t));
+            }
 
             columns.remove(t);
             Boolean isReversed = properties.definedOrdering.get(t);
diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java
index e7f8feb..ff9af75 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java
@@ -17,17 +17,17 @@
  */
 package org.apache.cassandra.cql3.statements;
 
-import java.nio.ByteBuffer;
 import java.util.*;
+import java.util.stream.Collectors;
 
 import org.apache.cassandra.auth.Permission;
 import org.apache.cassandra.config.*;
 import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.db.marshal.UserType;
 import org.apache.cassandra.exceptions.*;
 import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.Types;
 import org.apache.cassandra.service.ClientState;
 import org.apache.cassandra.service.MigrationManager;
 import org.apache.cassandra.service.QueryState;
@@ -36,7 +36,7 @@
 public class CreateTypeStatement extends SchemaAlteringStatement
 {
     private final UTName name;
-    private final List<ColumnIdentifier> columnNames = new ArrayList<>();
+    private final List<FieldIdentifier> columnNames = new ArrayList<>();
     private final List<CQL3Type.Raw> columnTypes = new ArrayList<>();
     private final boolean ifNotExists;
 
@@ -54,7 +54,7 @@
             name.setKeyspace(state.getKeyspace());
     }
 
-    public void addDefinition(ColumnIdentifier name, CQL3Type.Raw type)
+    public void addDefinition(FieldIdentifier name, CQL3Type.Raw type)
     {
         columnNames.add(name);
         columnTypes.add(type);
@@ -75,42 +75,47 @@
             throw new InvalidRequestException(String.format("A user type of name %s already exists", name));
 
         for (CQL3Type.Raw type : columnTypes)
+        {
             if (type.isCounter())
                 throw new InvalidRequestException("A user type cannot contain counters");
+            if (type.isUDT() && !type.isFrozen())
+                throw new InvalidRequestException("A user type cannot contain non-frozen UDTs");
+        }
     }
 
     public static void checkForDuplicateNames(UserType type) throws InvalidRequestException
     {
         for (int i = 0; i < type.size() - 1; i++)
         {
-            ByteBuffer fieldName = type.fieldName(i);
+            FieldIdentifier fieldName = type.fieldName(i);
             for (int j = i+1; j < type.size(); 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)));
+                    throw new InvalidRequestException(String.format("Duplicate field name %s in type %s", fieldName, type.name));
             }
         }
     }
 
+    public void addToRawBuilder(Types.RawBuilder builder) throws InvalidRequestException
+    {
+        builder.add(name.getStringTypeName(),
+                    columnNames.stream().map(FieldIdentifier::toString).collect(Collectors.toList()),
+                    columnTypes.stream().map(CQL3Type.Raw::toString).collect(Collectors.toList()));
+    }
+
     @Override
     public String keyspace()
     {
         return name.getKeyspace();
     }
 
-    private UserType createType() throws InvalidRequestException
+    public UserType createType() throws InvalidRequestException
     {
-        List<ByteBuffer> names = new ArrayList<>(columnNames.size());
-        for (ColumnIdentifier name : columnNames)
-            names.add(name.bytes);
-
         List<AbstractType<?>> types = new ArrayList<>(columnTypes.size());
         for (CQL3Type.Raw type : columnTypes)
             types.add(type.prepare(keyspace()).getType());
 
-        return new UserType(name.getKeyspace(), name.getUserTypeName(), names, types);
+        return new UserType(name.getKeyspace(), name.getUserTypeName(), columnNames, types, true);
     }
 
     public Event.SchemaChange announceMigration(QueryState queryState, boolean isLocalOnly) throws InvalidRequestException, ConfigurationException
diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateViewStatement.java
index 27f11ee..0bbe9f2 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateViewStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateViewStatement.java
@@ -22,6 +22,7 @@
 import java.util.stream.Collectors;
 
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -37,6 +38,7 @@
 import org.apache.cassandra.cql3.selection.RawSelector;
 import org.apache.cassandra.cql3.selection.Selectable;
 import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.DurationType;
 import org.apache.cassandra.db.marshal.ReversedType;
 import org.apache.cassandra.db.view.View;
 import org.apache.cassandra.exceptions.AlreadyExistsException;
@@ -58,8 +60,8 @@
     private final CFName baseName;
     private final List<RawSelector> selectClause;
     private final WhereClause whereClause;
-    private final List<ColumnIdentifier.Raw> partitionKeys;
-    private final List<ColumnIdentifier.Raw> clusteringKeys;
+    private final List<ColumnDefinition.Raw> partitionKeys;
+    private final List<ColumnDefinition.Raw> clusteringKeys;
     public final CFProperties properties = new CFProperties();
     private final boolean ifNotExists;
 
@@ -67,8 +69,8 @@
                                CFName baseName,
                                List<RawSelector> selectClause,
                                WhereClause whereClause,
-                               List<ColumnIdentifier.Raw> partitionKeys,
-                               List<ColumnIdentifier.Raw> clusteringKeys,
+                               List<ColumnDefinition.Raw> partitionKeys,
+                               List<ColumnDefinition.Raw> clusteringKeys,
                                boolean ifNotExists)
     {
         super(viewName);
@@ -93,7 +95,8 @@
         // We do validation in announceMigration to reduce doubling up of work
     }
 
-    private interface AddColumn {
+    private interface AddColumn
+    {
         void add(ColumnIdentifier identifier, AbstractType<?> type);
     }
 
@@ -120,7 +123,7 @@
 
     public Event.SchemaChange announceMigration(QueryState queryState, boolean isLocalOnly) throws RequestValidationException
     {
-        if (!DatabaseDescriptor.enableMaterializedViews())
+        if (!DatabaseDescriptor.getEnableMaterializedViews())
         {
             throw new InvalidRequestException("Materialized views are disabled. Enable in cassandra.yaml to use.");
         }
@@ -163,7 +166,7 @@
                                                             baseName.getColumnFamily()));
         }
 
-        Set<ColumnIdentifier> included = new HashSet<>();
+        Set<ColumnIdentifier> included = Sets.newHashSetWithExpectedSize(selectClause.size());
         for (RawSelector selector : selectClause)
         {
             Selectable.Raw selectable = selector.selectable;
@@ -173,40 +176,41 @@
                 throw new InvalidRequestException("Cannot use function when defining a materialized view");
             if (selectable instanceof Selectable.WritetimeOrTTL.Raw)
                 throw new InvalidRequestException("Cannot use function when defining a materialized view");
-            ColumnIdentifier identifier = (ColumnIdentifier) selectable.prepare(cfm);
             if (selector.alias != null)
-                throw new InvalidRequestException(String.format("Cannot alias column '%s' as '%s' when defining a materialized view", identifier.toString(), selector.alias.toString()));
+                throw new InvalidRequestException("Cannot use alias when defining a materialized view");
 
-            ColumnDefinition cdef = cfm.getColumnDefinition(identifier);
+            Selectable s = selectable.prepare(cfm);
+            if (s instanceof Term.Raw)
+                throw new InvalidRequestException("Cannot use terms in selection when defining a materialized view");
 
-            if (cdef == null)
-                throw new InvalidRequestException("Unknown column name detected in CREATE MATERIALIZED VIEW statement : "+identifier);
-
-            included.add(identifier);
+            ColumnDefinition cdef = (ColumnDefinition)s;
+            included.add(cdef.name);
         }
 
-        Set<ColumnIdentifier.Raw> targetPrimaryKeys = new HashSet<>();
-        for (ColumnIdentifier.Raw identifier : Iterables.concat(partitionKeys, clusteringKeys))
+        Set<ColumnDefinition.Raw> targetPrimaryKeys = new HashSet<>();
+        for (ColumnDefinition.Raw identifier : Iterables.concat(partitionKeys, clusteringKeys))
         {
             if (!targetPrimaryKeys.add(identifier))
                 throw new InvalidRequestException("Duplicate entry found in PRIMARY KEY: "+identifier);
 
-            ColumnDefinition cdef = cfm.getColumnDefinition(identifier.prepare(cfm));
+            ColumnDefinition cdef = identifier.prepare(cfm);
 
-            if (cdef == null)
-                throw new InvalidRequestException("Unknown column name detected in CREATE MATERIALIZED VIEW statement : "+identifier);
-
-            if (cfm.getColumnDefinition(identifier.prepare(cfm)).type.isMultiCell())
+            if (cdef.type.isMultiCell())
                 throw new InvalidRequestException(String.format("Cannot use MultiCell column '%s' in PRIMARY KEY of materialized view", identifier));
 
             if (cdef.isStatic())
                 throw new InvalidRequestException(String.format("Cannot use Static column '%s' in PRIMARY KEY of materialized view", identifier));
+
+            if (cdef.type instanceof DurationType)
+                throw new InvalidRequestException(String.format("Cannot use Duration column '%s' in PRIMARY KEY of materialized view", identifier));
         }
 
         // build the select statement
-        Map<ColumnIdentifier.Raw, Boolean> orderings = Collections.emptyMap();
-        SelectStatement.Parameters parameters = new SelectStatement.Parameters(orderings, false, true, false);
-        SelectStatement.RawStatement rawSelect = new SelectStatement.RawStatement(baseName, parameters, selectClause, whereClause, null);
+        Map<ColumnDefinition.Raw, Boolean> orderings = Collections.emptyMap();
+        List<ColumnDefinition.Raw> groups = Collections.emptyList();
+        SelectStatement.Parameters parameters = new SelectStatement.Parameters(orderings, groups, false, true, false);
+
+        SelectStatement.RawStatement rawSelect = new SelectStatement.RawStatement(baseName, parameters, selectClause, whereClause, null, null);
 
         ClientState state = ClientState.forInternalCalls();
         state.setKeyspace(keyspace());
@@ -221,12 +225,18 @@
         if (!prepared.boundNames.isEmpty())
             throw new InvalidRequestException("Cannot use query parameters in CREATE MATERIALIZED VIEW statements");
 
-        if (!restrictions.nonPKRestrictedColumns(false).isEmpty())
+        // SEE CASSANDRA-13798, use it if the use case is append-only.
+        final boolean allowFilteringNonKeyColumns = Boolean.parseBoolean(System.getProperty("cassandra.mv.allow_filtering_nonkey_columns_unsafe",
+                                                                                            "false"));
+        if (!restrictions.nonPKRestrictedColumns(false).isEmpty() && !allowFilteringNonKeyColumns)
         {
-            throw new InvalidRequestException(String.format(
-                    "Non-primary key columns cannot be restricted in the SELECT statement used for materialized view " +
-                    "creation (got restrictions on: %s)",
-                    restrictions.nonPKRestrictedColumns(false).stream().map(def -> def.name.toString()).collect(Collectors.joining(", "))));
+            throw new InvalidRequestException(
+                                              String.format("Non-primary key columns cannot be restricted in the SELECT statement used"
+                                                      + " for materialized view creation (got restrictions on: %s)",
+                                                            restrictions.nonPKRestrictedColumns(false)
+                                                                        .stream()
+                                                                        .map(def -> def.name.toString())
+                                                                        .collect(Collectors.joining(", "))));
         }
 
         if (whereClause.containsTokenRelations())
@@ -243,10 +253,10 @@
 
         // This is only used as an intermediate state; this is to catch whether multiple non-PK columns are used
         boolean hasNonPKColumn = false;
-        for (ColumnIdentifier.Raw raw : partitionKeys)
+        for (ColumnDefinition.Raw raw : partitionKeys)
             hasNonPKColumn |= getColumnIdentifier(cfm, basePrimaryKeyCols, hasNonPKColumn, raw, targetPartitionKeys, restrictions);
 
-        for (ColumnIdentifier.Raw raw : clusteringKeys)
+        for (ColumnDefinition.Raw raw : clusteringKeys)
             hasNonPKColumn |= getColumnIdentifier(cfm, basePrimaryKeyCols, hasNonPKColumn, raw, targetClusteringColumns, restrictions);
 
         // We need to include all of the primary key columns from the base table in order to make sure that we do not
@@ -267,13 +277,16 @@
                 throw new InvalidRequestException(String.format("Unable to include static column '%s' which would be included by Materialized View SELECT * statement", identifier));
             }
 
-            if (includeDef && !targetClusteringColumns.contains(identifier) && !targetPartitionKeys.contains(identifier))
+            boolean defInTargetPrimaryKey = targetClusteringColumns.contains(identifier)
+                                            || targetPartitionKeys.contains(identifier);
+
+            if (includeDef && !defInTargetPrimaryKey)
             {
                 includedColumns.add(identifier);
             }
             if (!def.isPrimaryKeyColumn()) continue;
 
-            if (!targetClusteringColumns.contains(identifier) && !targetPartitionKeys.contains(identifier))
+            if (!defInTargetPrimaryKey)
             {
                 if (missingClusteringColumns)
                     columnNames.append(',');
@@ -317,13 +330,12 @@
                                                        whereClauseText,
                                                        viewCfm);
 
-        logger.warn("Creating materialized view {} for {}.{}. " +
-                    "Materialized views are experimental and are not recommended for production use.",
-                    definition.viewName, cfm.ksName, cfm.cfName);
+        logger.warn("Creating materialized view {} for {}.{}. {}",
+                    definition.viewName, cfm.ksName, cfm.cfName, View.USAGE_WARNING);
 
         try
         {
-            ClientWarn.instance.warn("Materialized views are experimental and are not recommended for production use.");
+            ClientWarn.instance.warn(View.USAGE_WARNING);
             MigrationManager.announceNewView(definition, isLocalOnly);
             return new Event.SchemaChange(Event.SchemaChange.Change.CREATED, Event.SchemaChange.Target.TABLE, keyspace(), columnFamily());
         }
@@ -338,25 +350,24 @@
     private static boolean getColumnIdentifier(CFMetaData cfm,
                                                Set<ColumnIdentifier> basePK,
                                                boolean hasNonPKColumn,
-                                               ColumnIdentifier.Raw raw,
+                                               ColumnDefinition.Raw raw,
                                                List<ColumnIdentifier> columns,
                                                StatementRestrictions restrictions)
     {
-        ColumnIdentifier identifier = raw.prepare(cfm);
-        ColumnDefinition def = cfm.getColumnDefinition(identifier);
+        ColumnDefinition def = raw.prepare(cfm);
 
-        boolean isPk = basePK.contains(identifier);
+        boolean isPk = basePK.contains(def.name);
         if (!isPk && hasNonPKColumn)
-            throw new InvalidRequestException(String.format("Cannot include more than one non-primary key column '%s' in materialized view primary key", identifier));
+            throw new InvalidRequestException(String.format("Cannot include more than one non-primary key column '%s' in materialized view primary key", def.name));
 
         // We don't need to include the "IS NOT NULL" filter on a non-composite partition key
         // because we will never allow a single partition key to be NULL
-        boolean isSinglePartitionKey = cfm.getColumnDefinition(identifier).isPartitionKey()
+        boolean isSinglePartitionKey = def.isPartitionKey()
                                        && cfm.partitionKeyColumns().size() == 1;
         if (!isSinglePartitionKey && !restrictions.isRestricted(def))
-            throw new InvalidRequestException(String.format("Primary key column '%s' is required to be filtered by 'IS NOT NULL'", identifier));
+            throw new InvalidRequestException(String.format("Primary key column '%s' is required to be filtered by 'IS NOT NULL'", def.name));
 
-        columns.add(identifier);
+        columns.add(def.name);
         return !isPk;
     }
 }
diff --git a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java
index a0919d7..a343fb4 100644
--- a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java
@@ -122,7 +122,7 @@
                       Attributes.Raw attrs,
                       List<Operation.RawDeletion> deletions,
                       WhereClause whereClause,
-                      List<Pair<ColumnIdentifier.Raw, ColumnCondition.Raw>> conditions,
+                      List<Pair<ColumnDefinition.Raw, ColumnCondition.Raw>> conditions,
                       boolean ifExists)
         {
             super(name, StatementType.DELETE, attrs, conditions, false, ifExists);
@@ -154,7 +154,7 @@
                     // list. However, we support having the value name for coherence with the static/sparse case
                     checkFalse(def.isPrimaryKeyColumn(), "Invalid identifier %s for deletion (should not be a PRIMARY KEY part)", def.name);
 
-                    Operation op = deletion.prepare(cfm.ksName, def);
+                    Operation op = deletion.prepare(cfm.ksName, def, cfm);
                     op.collectMarkerSpecification(boundNames);
                     operations.add(op);
                 }
diff --git a/src/java/org/apache/cassandra/cql3/statements/DropAggregateStatement.java b/src/java/org/apache/cassandra/cql3/statements/DropAggregateStatement.java
index ae8ad8c..218a8ee 100644
--- a/src/java/org/apache/cassandra/cql3/statements/DropAggregateStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/DropAggregateStatement.java
@@ -139,7 +139,7 @@
 
     private AbstractType<?> prepareType(String typeName, CQL3Type.Raw rawType)
     {
-        if (rawType.isFrozen())
+        if (!rawType.isTuple() && rawType.isFrozen())
             throw new InvalidRequestException(String.format("The function %s should not be frozen; remove the frozen<> modifier", typeName));
 
         // UDT are not supported non frozen but we do not allow the frozen keyword for argument. So for the moment we
diff --git a/src/java/org/apache/cassandra/cql3/statements/DropFunctionStatement.java b/src/java/org/apache/cassandra/cql3/statements/DropFunctionStatement.java
index 8845a82..fd86e00 100644
--- a/src/java/org/apache/cassandra/cql3/statements/DropFunctionStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/DropFunctionStatement.java
@@ -70,7 +70,7 @@
             argTypes = new ArrayList<>(argRawTypes.size());
             for (CQL3Type.Raw rawType : argRawTypes)
             {
-                if (rawType.isFrozen())
+                if (!rawType.isTuple() && rawType.isFrozen())
                     throw new InvalidRequestException("The function arguments should not be frozen; remove the frozen<> modifier");
 
                 // UDT are not supported non frozen but we do not allow the frozen keyword for argument. So for the moment we
diff --git a/src/java/org/apache/cassandra/cql3/statements/DropIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/DropIndexStatement.java
index 35aee3c..fcd06d4 100644
--- a/src/java/org/apache/cassandra/cql3/statements/DropIndexStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/DropIndexStatement.java
@@ -64,7 +64,7 @@
     }
 
     @Override
-    public ResultMessage execute(QueryState state, QueryOptions options) throws RequestValidationException
+    public ResultMessage execute(QueryState state, QueryOptions options, long queryStartNanoTime) throws RequestValidationException
     {
         Event.SchemaChange ce = announceMigration(state, false);
         return ce == null ? null : new ResultMessage.SchemaChange(ce);
diff --git a/src/java/org/apache/cassandra/cql3/statements/DropKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/DropKeyspaceStatement.java
index 9ba68a6..7144f39 100644
--- a/src/java/org/apache/cassandra/cql3/statements/DropKeyspaceStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/DropKeyspaceStatement.java
@@ -17,10 +17,10 @@
  */
 package org.apache.cassandra.cql3.statements;
 
+import org.apache.cassandra.auth.Permission;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.exceptions.RequestValidationException;
-import org.apache.cassandra.auth.Permission;
 import org.apache.cassandra.exceptions.UnauthorizedException;
 import org.apache.cassandra.service.ClientState;
 import org.apache.cassandra.service.MigrationManager;
diff --git a/src/java/org/apache/cassandra/cql3/statements/DropViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/DropViewStatement.java
index b65c1b0..2f393d3 100644
--- a/src/java/org/apache/cassandra/cql3/statements/DropViewStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/DropViewStatement.java
@@ -20,8 +20,6 @@
 
 import org.apache.cassandra.auth.Permission;
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.Schema;
-import org.apache.cassandra.config.ViewDefinition;
 import org.apache.cassandra.cql3.CFName;
 import org.apache.cassandra.db.view.View;
 import org.apache.cassandra.exceptions.ConfigurationException;
diff --git a/src/java/org/apache/cassandra/cql3/statements/IndexTarget.java b/src/java/org/apache/cassandra/cql3/statements/IndexTarget.java
index 8cdf2c8..84af273 100644
--- a/src/java/org/apache/cassandra/cql3/statements/IndexTarget.java
+++ b/src/java/org/apache/cassandra/cql3/statements/IndexTarget.java
@@ -22,7 +22,6 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.ColumnIdentifier;
-import org.apache.cassandra.exceptions.InvalidRequestException;
 
 public class IndexTarget
 {
@@ -68,36 +67,36 @@
 
     public static class Raw
     {
-        private final ColumnIdentifier.Raw column;
+        private final ColumnDefinition.Raw column;
         private final Type type;
 
-        private Raw(ColumnIdentifier.Raw column, Type type)
+        private Raw(ColumnDefinition.Raw column, Type type)
         {
             this.column = column;
             this.type = type;
         }
 
-        public static Raw simpleIndexOn(ColumnIdentifier.Raw c)
+        public static Raw simpleIndexOn(ColumnDefinition.Raw c)
         {
             return new Raw(c, Type.SIMPLE);
         }
 
-        public static Raw valuesOf(ColumnIdentifier.Raw c)
+        public static Raw valuesOf(ColumnDefinition.Raw c)
         {
             return new Raw(c, Type.VALUES);
         }
 
-        public static Raw keysOf(ColumnIdentifier.Raw c)
+        public static Raw keysOf(ColumnDefinition.Raw c)
         {
             return new Raw(c, Type.KEYS);
         }
 
-        public static Raw keysAndValuesOf(ColumnIdentifier.Raw c)
+        public static Raw keysAndValuesOf(ColumnDefinition.Raw c)
         {
             return new Raw(c, Type.KEYS_AND_VALUES);
         }
 
-        public static Raw fullCollection(ColumnIdentifier.Raw c)
+        public static Raw fullCollection(ColumnDefinition.Raw c)
         {
             return new Raw(c, Type.FULL);
         }
@@ -109,13 +108,9 @@
             // same syntax as an index on a regular column (i.e. the 'values' in
             // 'CREATE INDEX on table(values(collection));' is optional). So we correct the target type
             // when the target column is a collection & the target type is SIMPLE.
-            ColumnIdentifier colId = column.prepare(cfm);
-            ColumnDefinition columnDef = cfm.getColumnDefinition(colId);
-            if (columnDef == null)
-                throw new InvalidRequestException("No column definition found for column " + colId);
-
+            ColumnDefinition columnDef = column.prepare(cfm);
             Type actualType = (type == Type.SIMPLE && columnDef.type.isCollection()) ? Type.VALUES : type;
-            return new IndexTarget(colId, actualType);
+            return new IndexTarget(columnDef.name, actualType);
         }
     }
 
diff --git a/src/java/org/apache/cassandra/cql3/statements/ListPermissionsStatement.java b/src/java/org/apache/cassandra/cql3/statements/ListPermissionsStatement.java
index 58f8e9c..b8f2f92 100644
--- a/src/java/org/apache/cassandra/cql3/statements/ListPermissionsStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/ListPermissionsStatement.java
@@ -21,6 +21,7 @@
 
 import org.apache.cassandra.auth.*;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.exceptions.InvalidRequestException;
@@ -31,7 +32,7 @@
 
 public class ListPermissionsStatement extends AuthorizationStatement
 {
-    private static final String KS = AuthKeyspace.NAME;
+    private static final String KS = SchemaConstants.AUTH_KEYSPACE_NAME;
     private static final String CF = "permissions"; // virtual cf to use for now.
 
     private static final List<ColumnSpecification> metadata;
diff --git a/src/java/org/apache/cassandra/cql3/statements/ListRolesStatement.java b/src/java/org/apache/cassandra/cql3/statements/ListRolesStatement.java
index 477aedc..3fee57a 100644
--- a/src/java/org/apache/cassandra/cql3/statements/ListRolesStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/ListRolesStatement.java
@@ -26,6 +26,7 @@
 
 import org.apache.cassandra.auth.*;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.db.marshal.BooleanType;
 import org.apache.cassandra.db.marshal.MapType;
@@ -37,7 +38,7 @@
 public class ListRolesStatement extends AuthorizationStatement
 {
     // pseudo-virtual cf as the actual datasource is dependent on the IRoleManager impl
-    private static final String KS = AuthKeyspace.NAME;
+    private static final String KS = SchemaConstants.AUTH_KEYSPACE_NAME;
     private static final String CF = AuthKeyspace.ROLES;
 
     private static final MapType optionsType = MapType.getInstance(UTF8Type.instance, UTF8Type.instance, false);
diff --git a/src/java/org/apache/cassandra/cql3/statements/ListUsersStatement.java b/src/java/org/apache/cassandra/cql3/statements/ListUsersStatement.java
index 7251980..0101363 100644
--- a/src/java/org/apache/cassandra/cql3/statements/ListUsersStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/ListUsersStatement.java
@@ -23,6 +23,7 @@
 
 import org.apache.cassandra.auth.*;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.cql3.ColumnSpecification;
 import org.apache.cassandra.cql3.ResultSet;
@@ -33,7 +34,7 @@
 public class ListUsersStatement extends ListRolesStatement
 {
     // pseudo-virtual cf as the actual datasource is dependent on the IRoleManager impl
-    private static final String KS = AuthKeyspace.NAME;
+    private static final String KS = SchemaConstants.AUTH_KEYSPACE_NAME;
     private static final String CF = "users";
 
     private static final List<ColumnSpecification> metadata =
diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
index f945791..d328ec1 100644
--- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
@@ -20,6 +20,7 @@
 import java.nio.ByteBuffer;
 import java.util.*;
 
+import com.google.common.collect.HashMultiset;
 import com.google.common.collect.Iterables;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -27,9 +28,10 @@
 import org.apache.cassandra.auth.Permission;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.ColumnDefinition.Raw;
 import org.apache.cassandra.config.ViewDefinition;
 import org.apache.cassandra.cql3.*;
-import org.apache.cassandra.cql3.ColumnIdentifier.Raw;
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
 import org.apache.cassandra.cql3.selection.Selection;
@@ -52,7 +54,6 @@
 import org.apache.cassandra.utils.UUIDGen;
 
 import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
-import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull;
 import static org.apache.cassandra.cql3.statements.RequestValidations.checkNull;
 
 /*
@@ -139,11 +140,6 @@
         this.requiresRead = requiresReadBuilder.build();
     }
 
-    public StatementRestrictions getRestrictions()
-    {
-        return restrictions;
-    }
-
     public Iterable<Function> getFunctions()
     {
         List<Function> functions = new ArrayList<>();
@@ -159,6 +155,14 @@
         conditions.addFunctionsTo(functions);
     }
 
+    /*
+     * May be used by QueryHandler implementations
+     */
+    public StatementRestrictions getRestrictions()
+    {
+        return restrictions;
+    }
+
     public abstract void addUpdateForKey(PartitionUpdate update, Clustering clustering, UpdateParameters params);
 
     public abstract void addUpdateForKey(PartitionUpdate update, Slice slice, UpdateParameters params);
@@ -205,21 +209,21 @@
 
     public void checkAccess(ClientState state) throws InvalidRequestException, UnauthorizedException
     {
-        state.hasColumnFamilyAccess(keyspace(), columnFamily(), Permission.MODIFY);
+        state.hasColumnFamilyAccess(cfm, Permission.MODIFY);
 
         // CAS updates can be used to simulate a SELECT query, so should require Permission.SELECT as well.
         if (hasConditions())
-            state.hasColumnFamilyAccess(keyspace(), columnFamily(), Permission.SELECT);
+            state.hasColumnFamilyAccess(cfm, Permission.SELECT);
 
         // MV updates need to get the current state from the table, and might update the views
         // Require Permission.SELECT on the base table, and Permission.MODIFY on the views
         Iterator<ViewDefinition> views = View.findAll(keyspace(), columnFamily()).iterator();
         if (views.hasNext())
         {
-            state.hasColumnFamilyAccess(keyspace(), columnFamily(), Permission.SELECT);
+            state.hasColumnFamilyAccess(cfm, Permission.SELECT);
             do
             {
-                state.hasColumnFamilyAccess(keyspace(), views.next().viewName, Permission.MODIFY);
+                state.hasColumnFamilyAccess(views.next().metadata, Permission.MODIFY);
             } while (views.hasNext());
         }
 
@@ -252,7 +256,7 @@
         // columns is if we set some static columns, and in that case no clustering
         // columns should be given. So in practice, it's enough to check if we have
         // either the table has no clustering or if it has at least one of them set.
-        return cfm.clusteringColumns().isEmpty() || restrictions.hasClusteringColumnsRestriction();
+        return cfm.clusteringColumns().isEmpty() || restrictions.hasClusteringColumnsRestrictions();
     }
 
     public boolean updatesStaticRow()
@@ -303,7 +307,7 @@
     public NavigableSet<Clustering> createClustering(QueryOptions options)
     throws InvalidRequestException
     {
-        if (appliesOnlyToStaticColumns() && !restrictions.hasClusteringColumnsRestriction())
+        if (appliesOnlyToStaticColumns() && !restrictions.hasClusteringColumnsRestrictions())
             return FBUtilities.singleton(CBuilder.STATIC_BUILDER.build(), cfm.comparator);
 
         return restrictions.getClusteringColumns(options);
@@ -331,19 +335,15 @@
 
     public boolean requiresRead()
     {
-        // Lists SET operation incurs a read.
-        for (Operation op : allOperations())
-            if (op.requiresRead())
-                return true;
-
-        return false;
+        return !requiresRead.isEmpty();
     }
 
     private Map<DecoratedKey, Partition> readRequiredLists(Collection<ByteBuffer> partitionKeys,
                                                            ClusteringIndexFilter filter,
                                                            DataLimits limits,
                                                            boolean local,
-                                                           ConsistencyLevel cl)
+                                                           ConsistencyLevel cl,
+                                                           long queryStartNanoTime)
     {
         if (!requiresRead())
             return null;
@@ -372,13 +372,14 @@
 
         if (local)
         {
-            try (ReadOrderGroup orderGroup = group.startOrderGroup(); PartitionIterator iter = group.executeInternal(orderGroup))
+            try (ReadExecutionController executionController = group.executionController();
+                 PartitionIterator iter = group.executeInternal(executionController))
             {
                 return asMaterializedMap(iter);
             }
         }
 
-        try (PartitionIterator iter = group.execute(cl, null))
+        try (PartitionIterator iter = group.execute(cl, null, queryStartNanoTime))
         {
             return asMaterializedMap(iter);
         }
@@ -405,22 +406,22 @@
     public boolean hasSlices()
     {
         return type.allowClusteringColumnSlices()
-               && getRestrictions().hasClusteringColumnsRestriction()
+               && getRestrictions().hasClusteringColumnsRestrictions()
                && getRestrictions().isColumnRange();
     }
 
-    public ResultMessage execute(QueryState queryState, QueryOptions options)
+    public ResultMessage execute(QueryState queryState, QueryOptions options, long queryStartNanoTime)
     throws RequestExecutionException, RequestValidationException
     {
         if (options.getConsistency() == null)
             throw new InvalidRequestException("Invalid empty consistency level");
 
         return hasConditions()
-             ? executeWithCondition(queryState, options)
-             : executeWithoutCondition(queryState, options);
+             ? executeWithCondition(queryState, options, queryStartNanoTime)
+             : executeWithoutCondition(queryState, options, queryStartNanoTime);
     }
 
-    private ResultMessage executeWithoutCondition(QueryState queryState, QueryOptions options)
+    private ResultMessage executeWithoutCondition(QueryState queryState, QueryOptions options, long queryStartNanoTime)
     throws RequestExecutionException, RequestValidationException
     {
         ConsistencyLevel cl = options.getConsistency();
@@ -429,14 +430,14 @@
         else
             cl.validateForWrite(cfm.ksName);
 
-        Collection<? extends IMutation> mutations = getMutations(options, false, options.getTimestamp(queryState));
+        Collection<? extends IMutation> mutations = getMutations(options, false, options.getTimestamp(queryState), queryStartNanoTime);
         if (!mutations.isEmpty())
-            StorageProxy.mutateWithTriggers(mutations, cl, false);
+            StorageProxy.mutateWithTriggers(mutations, cl, false, queryStartNanoTime);
 
         return null;
     }
 
-    public ResultMessage executeWithCondition(QueryState queryState, QueryOptions options)
+    public ResultMessage executeWithCondition(QueryState queryState, QueryOptions options, long queryStartNanoTime)
     throws RequestExecutionException, RequestValidationException
     {
         CQL3CasRequest request = makeCasRequest(queryState, options);
@@ -447,7 +448,8 @@
                                                    request,
                                                    options.getSerialConsistency(),
                                                    options.getConsistency(),
-                                                   queryState.getClientState()))
+                                                   queryState.getClientState(),
+                                                   queryStartNanoTime))
         {
             return new ResultMessage.Rows(buildCasResultSet(result, options));
         }
@@ -559,30 +561,26 @@
             selection = Selection.forColumns(cfm, new ArrayList<>(defs));
         }
 
-        Selection.ResultSetBuilder builder = selection.resultSetBuilder(false);
-        SelectStatement.forSelection(cfm, selection).processPartition(partition, options, builder, FBUtilities.nowInSeconds());
+        Selection.ResultSetBuilder builder = selection.resultSetBuilder(options, false);
+        SelectStatement.forSelection(cfm, selection).processPartition(partition,
+                                                                      options,
+                                                                      builder,
+                                                                      FBUtilities.nowInSeconds());
 
-        return builder.build(options.getProtocolVersion());
+        return builder.build();
     }
 
     public ResultMessage executeInternal(QueryState queryState, QueryOptions options) throws RequestValidationException, RequestExecutionException
     {
         return hasConditions()
                ? executeInternalWithCondition(queryState, options)
-               : executeInternalWithoutCondition(queryState, options);
+               : executeInternalWithoutCondition(queryState, options, System.nanoTime());
     }
 
-    public ResultMessage executeInternalWithoutCondition(QueryState queryState, QueryOptions options) throws RequestValidationException, RequestExecutionException
+    public ResultMessage executeInternalWithoutCondition(QueryState queryState, QueryOptions options, long queryStartNanoTime) throws RequestValidationException, RequestExecutionException
     {
-        for (IMutation mutation : getMutations(options, true, queryState.getTimestamp()))
-        {
-            assert mutation instanceof Mutation || mutation instanceof CounterMutation;
-
-            if (mutation instanceof Mutation)
-                ((Mutation) mutation).apply();
-            else if (mutation instanceof CounterMutation)
-                ((CounterMutation) mutation).apply();
-        }
+        for (IMutation mutation : getMutations(options, true, queryState.getTimestamp(), queryStartNanoTime))
+            mutation.apply();
         return null;
     }
 
@@ -601,7 +599,8 @@
 
         SinglePartitionReadCommand readCommand = request.readCommand(FBUtilities.nowInSeconds());
         FilteredPartition current;
-        try (ReadOrderGroup orderGroup = readCommand.startOrderGroup(); PartitionIterator iter = readCommand.executeInternal(orderGroup))
+        try (ReadExecutionController executionController = readCommand.executionController();
+             PartitionIterator iter = readCommand.executeInternal(executionController))
         {
             current = FilteredPartition.create(PartitionIterators.getOnlyElement(iter, readCommand));
         }
@@ -626,21 +625,27 @@
      *
      * @return list of the mutations
      */
-    private Collection<? extends IMutation> getMutations(QueryOptions options, boolean local, long now)
+    private Collection<? extends IMutation> getMutations(QueryOptions options, boolean local, long now, long queryStartNanoTime)
     {
-        UpdatesCollector collector = new UpdatesCollector(Collections.singletonMap(cfm.cfId, updatedColumns), 1);
-        addUpdates(collector, options, local, now);
+        List<ByteBuffer> keys = buildPartitionKeyNames(options);
+        HashMultiset<ByteBuffer> perPartitionKeyCounts = HashMultiset.create();
+        for (int i = 0; i < keys.size(); i++)
+            perPartitionKeyCounts.add(keys.get(i)); // avoid .addAll since that allocates an iterator
+
+        UpdatesCollector collector = new UpdatesCollector(Collections.singletonMap(cfm.cfId, updatedColumns), Collections.singletonMap(cfm.cfId, perPartitionKeyCounts));
+        addUpdates(collector, keys, options, local, now, queryStartNanoTime);
         collector.validateIndexedColumns();
 
         return collector.toMutations();
     }
 
     final void addUpdates(UpdatesCollector collector,
+                          List<ByteBuffer> keys,
                           QueryOptions options,
                           boolean local,
-                          long now)
+                          long now,
+                          long queryStartNanoTime)
     {
-        List<ByteBuffer> keys = buildPartitionKeyNames(options);
 
         if (hasSlices())
         {
@@ -655,9 +660,12 @@
                                                            options,
                                                            DataLimits.NONE,
                                                            local,
-                                                           now);
-            for (ByteBuffer key : keys)
+                                                           now,
+                                                           queryStartNanoTime,
+                                                           (int) (collector.createdAt / 1000));
+            for (int i = 0, isize = keys.size(); i < isize; i++)
             {
+                ByteBuffer key = keys.get(i);
                 ThriftValidation.validateKey(cfm, key);
                 DecoratedKey dk = cfm.decorateKey(key);
 
@@ -672,19 +680,20 @@
             NavigableSet<Clustering> clusterings = createClustering(options);
 
             // If some of the restrictions were unspecified (e.g. empty IN restrictions) we do not need to do anything.
-            if (restrictions.hasClusteringColumnsRestriction() && clusterings.isEmpty())
+            if (restrictions.hasClusteringColumnsRestrictions() && clusterings.isEmpty())
                 return;
 
-            UpdateParameters params = makeUpdateParameters(keys, clusterings, options, local, now);
+            UpdateParameters params = makeUpdateParameters(keys, clusterings, options, local, now, queryStartNanoTime, (int) (collector.createdAt / 1000));
 
-            for (ByteBuffer key : keys)
+            for (int i = 0, isize = keys.size(); i < isize; i++)
             {
+                ByteBuffer key = keys.get(i);
                 ThriftValidation.validateKey(cfm, key);
                 DecoratedKey dk = cfm.decorateKey(key);
 
                 PartitionUpdate upd = collector.getPartitionUpdate(cfm, dk, options.getConsistency());
 
-                if (!restrictions.hasClusteringColumnsRestriction())
+                if (!restrictions.hasClusteringColumnsRestrictions())
                 {
                     addUpdateForKey(upd, Clustering.EMPTY, params);
                 }
@@ -709,8 +718,8 @@
 
     Slices createSlices(QueryOptions options)
     {
-        SortedSet<Slice.Bound> startBounds = restrictions.getClusteringColumnsBounds(Bound.START, options);
-        SortedSet<Slice.Bound> endBounds = restrictions.getClusteringColumnsBounds(Bound.END, options);
+        SortedSet<ClusteringBound> startBounds = restrictions.getClusteringColumnsBounds(Bound.START, options);
+        SortedSet<ClusteringBound> endBounds = restrictions.getClusteringColumnsBounds(Bound.END, options);
 
         return toSlices(startBounds, endBounds);
     }
@@ -719,7 +728,9 @@
                                                   NavigableSet<Clustering> clusterings,
                                                   QueryOptions options,
                                                   boolean local,
-                                                  long now)
+                                                  long now,
+                                                  long queryStartNanoTime,
+                                                  int nowInSec)
     {
         if (clusterings.contains(Clustering.STATIC_CLUSTERING))
             return makeUpdateParameters(keys,
@@ -727,14 +738,18 @@
                                         options,
                                         DataLimits.cqlLimits(1),
                                         local,
-                                        now);
+                                        now,
+                                        queryStartNanoTime,
+                                        nowInSec);
 
         return makeUpdateParameters(keys,
                                     new ClusteringIndexNamesFilter(clusterings, false),
                                     options,
                                     DataLimits.NONE,
                                     local,
-                                    now);
+                                    now,
+                                    queryStartNanoTime,
+                                    nowInSec);
     }
 
     private UpdateParameters makeUpdateParameters(Collection<ByteBuffer> keys,
@@ -742,21 +757,23 @@
                                                   QueryOptions options,
                                                   DataLimits limits,
                                                   boolean local,
-                                                  long now)
+                                                  long now,
+                                                  long queryStartNanoTime,
+                                                  int nowInSec)
     {
         // Some lists operation requires reading
-        Map<DecoratedKey, Partition> lists = readRequiredLists(keys, filter, limits, local, options.getConsistency());
+        Map<DecoratedKey, Partition> lists = readRequiredLists(keys, filter, limits, local, options.getConsistency(), queryStartNanoTime);
         return new UpdateParameters(cfm, updatedColumns(), options, getTimestamp(now, options), getTimeToLive(options), lists);
     }
 
-    private Slices toSlices(SortedSet<Slice.Bound> startBounds, SortedSet<Slice.Bound> endBounds)
+    private Slices toSlices(SortedSet<ClusteringBound> startBounds, SortedSet<ClusteringBound> endBounds)
     {
         assert startBounds.size() == endBounds.size();
 
         Slices.Builder builder = new Slices.Builder(cfm.comparator);
 
-        Iterator<Slice.Bound> starts = startBounds.iterator();
-        Iterator<Slice.Bound> ends = endBounds.iterator();
+        Iterator<ClusteringBound> starts = startBounds.iterator();
+        Iterator<ClusteringBound> ends = endBounds.iterator();
 
         while (starts.hasNext())
         {
@@ -774,21 +791,21 @@
     {
         protected final StatementType type;
         private final Attributes.Raw attrs;
-        private final List<Pair<ColumnIdentifier.Raw, ColumnCondition.Raw>> conditions;
+        private final List<Pair<ColumnDefinition.Raw, ColumnCondition.Raw>> conditions;
         private final boolean ifNotExists;
         private final boolean ifExists;
 
         protected Parsed(CFName name,
                          StatementType type,
                          Attributes.Raw attrs,
-                         List<Pair<ColumnIdentifier.Raw, ColumnCondition.Raw>> conditions,
+                         List<Pair<ColumnDefinition.Raw, ColumnCondition.Raw>> conditions,
                          boolean ifNotExists,
                          boolean ifExists)
         {
             super(name);
             this.type = type;
             this.attrs = attrs;
-            this.conditions = conditions == null ? Collections.<Pair<ColumnIdentifier.Raw, ColumnCondition.Raw>>emptyList() : conditions;
+            this.conditions = conditions == null ? Collections.<Pair<ColumnDefinition.Raw, ColumnCondition.Raw>>emptyList() : conditions;
             this.ifNotExists = ifNotExists;
             this.ifExists = ifExists;
         }
@@ -859,16 +876,13 @@
 
             ColumnConditions.Builder builder = ColumnConditions.newBuilder();
 
-            for (Pair<ColumnIdentifier.Raw, ColumnCondition.Raw> entry : conditions)
+            for (Pair<ColumnDefinition.Raw, ColumnCondition.Raw> entry : conditions)
             {
-                ColumnIdentifier id = entry.left.prepare(metadata);
-                ColumnDefinition def = metadata.getColumnDefinitionForCQL(id);
-                checkNotNull(def, "Unknown identifier %s in IF conditions", id);
-
-                ColumnCondition condition = entry.right.prepare(keyspace(), def);
+                ColumnDefinition def = entry.left.prepare(metadata);
+                ColumnCondition condition = entry.right.prepare(keyspace(), def, metadata);
                 condition.collectMarkerSpecification(boundNames);
 
-                checkFalse(def.isPrimaryKeyColumn(), "PRIMARY KEY column '%s' cannot have IF conditions", id);
+                checkFalse(def.isPrimaryKeyColumn(), "PRIMARY KEY column '%s' cannot have IF conditions", def.name);
                 builder.add(condition);
             }
             return builder.build();
@@ -911,8 +925,7 @@
          */
         protected static ColumnDefinition getColumnDefinition(CFMetaData cfm, Raw rawId)
         {
-            ColumnIdentifier id = rawId.prepare(cfm);
-            return checkNotNull(cfm.getColumnDefinitionForCQL(id), "Unknown identifier %s", id);
+            return rawId.prepare(cfm);
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/cql3/statements/ParsedStatement.java b/src/java/org/apache/cassandra/cql3/statements/ParsedStatement.java
index 759a0c8..54a9dba 100644
--- a/src/java/org/apache/cassandra/cql3/statements/ParsedStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/ParsedStatement.java
@@ -53,7 +53,7 @@
     {
         /**
          * Contains the CQL statement source if the statement has been "regularly" perpared via
-         * {@link org.apache.cassandra.cql3.QueryProcessor#prepare(java.lang.String, org.apache.cassandra.service.ClientState, boolean)}
+         * {@link org.apache.cassandra.cql3.QueryProcessor#prepare(java.lang.String, org.apache.cassandra.service.ClientState, boolean)} /
          * {@link QueryHandler#prepare(java.lang.String, org.apache.cassandra.service.QueryState, java.util.Map)}.
          * Other usages of this class may or may not contain the CQL statement source.
          */
@@ -63,12 +63,12 @@
         public final List<ColumnSpecification> boundNames;
 
         @Nullable
-        public final Short[] partitionKeyBindIndexes;
+        public final short[] partitionKeyBindIndexes;
         public final boolean fullyQualified;
         @Nullable
         public final String keyspace;
 
-        protected Prepared(CQLStatement statement, List<ColumnSpecification> boundNames, Short[] partitionKeyBindIndexes, String keyspace, boolean fullyQualified)
+        protected Prepared(CQLStatement statement, List<ColumnSpecification> boundNames, short[] partitionKeyBindIndexes, String keyspace, boolean fullyQualified)
         {
             this.statement = statement;
             this.boundNames = boundNames;
@@ -78,7 +78,7 @@
             this.keyspace = keyspace;
         }
 
-        public Prepared(CQLStatement statement, VariableSpecifications names, Short[] partitionKeyBindIndexes, String keyspace, boolean fullyQualified)
+        public Prepared(CQLStatement statement, VariableSpecifications names, short[] partitionKeyBindIndexes, String keyspace, boolean fullyQualified)
         {
             this(statement, names.getSpecifications(), partitionKeyBindIndexes, keyspace, fullyQualified);
         }
diff --git a/src/java/org/apache/cassandra/cql3/statements/PermissionsManagementStatement.java b/src/java/org/apache/cassandra/cql3/statements/PermissionsManagementStatement.java
index 56a2f26..0d07e12 100644
--- a/src/java/org/apache/cassandra/cql3/statements/PermissionsManagementStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/PermissionsManagementStatement.java
@@ -21,8 +21,8 @@
 
 import org.apache.cassandra.auth.*;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.RoleName;
-import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.exceptions.RequestValidationException;
 import org.apache.cassandra.exceptions.UnauthorizedException;
@@ -55,7 +55,7 @@
 
         // altering permissions on builtin functions is not supported
         if (resource instanceof FunctionResource
-            && SystemKeyspace.NAME.equals(((FunctionResource)resource).getKeyspace()))
+            && SchemaConstants.SYSTEM_KEYSPACE_NAME.equals(((FunctionResource)resource).getKeyspace()))
         {
             throw new InvalidRequestException("Altering permissions on builtin functions is not supported");
         }
diff --git a/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java b/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java
index 793285b..590910f 100644
--- a/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java
+++ b/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java
@@ -18,6 +18,7 @@
 package org.apache.cassandra.cql3.statements;
 
 import java.util.*;
+import java.util.regex.Pattern;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -26,6 +27,8 @@
 
 public class PropertyDefinitions
 {
+    private static final Pattern PATTERN_POSITIVE = Pattern.compile("(1|true|yes)");
+    
     protected static final Logger logger = LoggerFactory.getLogger(PropertyDefinitions.class);
 
     protected final Map<String, Object> properties = new HashMap<String, Object>();
@@ -91,7 +94,7 @@
     public Boolean getBoolean(String key, Boolean defaultValue) throws SyntaxException
     {
         String value = getSimple(key);
-        return (value == null) ? defaultValue : value.toLowerCase().matches("(1|true|yes)");
+        return (value == null) ? defaultValue : PATTERN_POSITIVE.matcher(value.toLowerCase()).matches();
     }
 
     // Return a property value, typed as a double
diff --git a/src/java/org/apache/cassandra/cql3/statements/SchemaAlteringStatement.java b/src/java/org/apache/cassandra/cql3/statements/SchemaAlteringStatement.java
index e7ecb14..5079603 100644
--- a/src/java/org/apache/cassandra/cql3/statements/SchemaAlteringStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/SchemaAlteringStatement.java
@@ -116,7 +116,7 @@
      */
     protected abstract Event.SchemaChange announceMigration(QueryState queryState, boolean isLocalOnly) throws RequestValidationException;
 
-    public ResultMessage execute(QueryState state, QueryOptions options) throws RequestValidationException
+    public ResultMessage execute(QueryState state, QueryOptions options, long queryStartNanoTime) throws RequestValidationException
     {
         // If an IF [NOT] EXISTS clause was used, this may not result in an actual schema change.  To avoid doing
         // extra work in the drivers to handle schema changes, we return an empty message in this case. (CASSANDRA-7600)
diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
index cfdb353..aaa6919 100644
--- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
@@ -28,9 +28,8 @@
 import java.util.NavigableSet;
 import java.util.SortedSet;
 
-import com.google.common.base.Objects;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Iterables;
+import com.google.common.base.MoreObjects;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -52,6 +51,8 @@
 import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
 import org.apache.cassandra.cql3.selection.RawSelector;
 import org.apache.cassandra.cql3.selection.Selection;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.aggregation.AggregationSpecification;
 import org.apache.cassandra.db.Clustering;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.ConsistencyLevel;
@@ -60,7 +61,6 @@
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.PartitionPosition;
 import org.apache.cassandra.db.PartitionRangeReadCommand;
-import org.apache.cassandra.db.ReadOrderGroup;
 import org.apache.cassandra.db.ReadQuery;
 import org.apache.cassandra.db.SinglePartitionReadCommand;
 import org.apache.cassandra.db.Slice;
@@ -74,6 +74,7 @@
 import org.apache.cassandra.db.marshal.CollectionType;
 import org.apache.cassandra.db.marshal.CompositeType;
 import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.UserType;
 import org.apache.cassandra.db.partitions.PartitionIterator;
 import org.apache.cassandra.db.rows.ComplexColumnData;
 import org.apache.cassandra.db.rows.Row;
@@ -84,23 +85,24 @@
 import org.apache.cassandra.exceptions.RequestExecutionException;
 import org.apache.cassandra.exceptions.RequestValidationException;
 import org.apache.cassandra.exceptions.UnauthorizedException;
-import org.apache.cassandra.exceptions.UnrecognizedEntityException;
 import org.apache.cassandra.index.SecondaryIndexManager;
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.service.ClientState;
 import org.apache.cassandra.service.ClientWarn;
 import org.apache.cassandra.service.QueryState;
+import org.apache.cassandra.service.pager.AggregationQueryPager;
 import org.apache.cassandra.service.pager.PagingState;
 import org.apache.cassandra.service.pager.QueryPager;
 import org.apache.cassandra.thrift.ThriftValidation;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.transport.messages.ResultMessage;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
 
 import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
 import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull;
+import static org.apache.cassandra.cql3.statements.RequestValidations.checkNull;
 import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue;
-import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
 import static org.apache.cassandra.utils.ByteBufferUtil.UNSET_BYTE_BUFFER;
 
 /**
@@ -116,18 +118,25 @@
 {
     private static final Logger logger = LoggerFactory.getLogger(SelectStatement.class);
 
-    private static final int DEFAULT_COUNT_PAGE_SIZE = 10000;
+    public static final int DEFAULT_PAGE_SIZE = 10000;
+
     private final int boundTerms;
     public final CFMetaData cfm;
     public final Parameters parameters;
     private final Selection selection;
     private final Term limit;
+    private final Term perPartitionLimit;
 
     private final StatementRestrictions restrictions;
 
     private final boolean isReversed;
 
     /**
+     * The <code>AggregationSpecification</code> used to make the aggregates.
+     */
+    private final AggregationSpecification aggregationSpec;
+
+    /**
      * The comparator used to orders results when multiple keys are selected (using IN).
      */
     private final Comparator<List<ByteBuffer>> orderingComparator;
@@ -135,7 +144,11 @@
     private final ColumnFilter queriedColumns;
 
     // Used by forSelection below
-    private static final Parameters defaultParameters = new Parameters(Collections.<ColumnIdentifier.Raw, Boolean>emptyMap(), false, false, false);
+    private static final Parameters defaultParameters = new Parameters(Collections.emptyMap(),
+                                                                       Collections.emptyList(),
+                                                                       false,
+                                                                       false,
+                                                                       false);
 
     public SelectStatement(CFMetaData cfm,
                            int boundTerms,
@@ -143,17 +156,21 @@
                            Selection selection,
                            StatementRestrictions restrictions,
                            boolean isReversed,
+                           AggregationSpecification aggregationSpec,
                            Comparator<List<ByteBuffer>> orderingComparator,
-                           Term limit)
+                           Term limit,
+                           Term perPartitionLimit)
     {
         this.cfm = cfm;
         this.boundTerms = boundTerms;
         this.selection = selection;
         this.restrictions = restrictions;
         this.isReversed = isReversed;
+        this.aggregationSpec = aggregationSpec;
         this.orderingComparator = orderingComparator;
         this.parameters = parameters;
         this.limit = limit;
+        this.perPartitionLimit = perPartitionLimit;
         this.queriedColumns = gatherQueriedColumns();
     }
 
@@ -171,6 +188,9 @@
 
         if (limit != null)
             limit.addFunctionsTo(functions);
+
+        if (perPartitionLimit != null)
+            perPartitionLimit.addFunctionsTo(functions);
     }
 
     // Note that the queried columns internally is different from the one selected by the
@@ -211,6 +231,8 @@
                                    StatementRestrictions.empty(StatementType.SELECT, cfm),
                                    false,
                                    null,
+                                   null,
+                                   null,
                                    null);
     }
 
@@ -230,11 +252,11 @@
         {
             CFMetaData baseTable = View.findBaseTable(keyspace(), columnFamily());
             if (baseTable != null)
-                state.hasColumnFamilyAccess(keyspace(), baseTable.cfName, Permission.SELECT);
+                state.hasColumnFamilyAccess(baseTable, Permission.SELECT);
         }
         else
         {
-            state.hasColumnFamilyAccess(keyspace(), columnFamily(), Permission.SELECT);
+            state.hasColumnFamilyAccess(cfm, Permission.SELECT);
         }
 
         for (Function function : getFunctions())
@@ -246,7 +268,7 @@
         // Nothing to do, all validation has been done by RawStatement.prepare()
     }
 
-    public ResultMessage.Rows execute(QueryState state, QueryOptions options) throws RequestExecutionException, RequestValidationException
+    public ResultMessage.Rows execute(QueryState state, QueryOptions options, long queryStartNanoTime) throws RequestExecutionException, RequestValidationException
     {
         ConsistencyLevel cl = options.getConsistency();
         checkNotNull(cl, "Invalid empty consistency level");
@@ -255,39 +277,30 @@
 
         int nowInSec = FBUtilities.nowInSeconds();
         int userLimit = getLimit(options);
-        ReadQuery query = getQuery(options, nowInSec, userLimit);
-
-        int pageSize = getPageSize(options);
-
-        if (pageSize <= 0 || query.limits().count() <= pageSize)
-            return execute(query, options, state, nowInSec, userLimit);
-
-        QueryPager pager = query.getPager(options.getPagingState(), options.getProtocolVersion());
-        return execute(Pager.forDistributedQuery(pager, cl, state.getClientState()), options, pageSize, nowInSec, userLimit);
-    }
-
-    private int getPageSize(QueryOptions options)
-    {
+        int userPerPartitionLimit = getPerPartitionLimit(options);
         int pageSize = options.getPageSize();
+        ReadQuery query = getQuery(options, nowInSec, userLimit, userPerPartitionLimit, pageSize);
 
-        // An aggregation query will never be paged for the user, but we always page it internally to avoid OOM.
-        // If we user provided a pageSize we'll use that to page internally (because why not), otherwise we use our default
-        // Note that if there are some nodes in the cluster with a version less than 2.0, we can't use paging (CASSANDRA-6707).
-        if (selection.isAggregate() && pageSize <= 0)
-            pageSize = DEFAULT_COUNT_PAGE_SIZE;
+        if (aggregationSpec == null && (pageSize <= 0 || (query.limits().count() <= pageSize)))
+            return execute(query, options, state, nowInSec, userLimit, queryStartNanoTime);
 
-        return  pageSize;
+        QueryPager pager = getPager(query, options);
+
+        return execute(Pager.forDistributedQuery(pager, cl, state.getClientState()), options, pageSize, nowInSec, userLimit, queryStartNanoTime);
     }
 
     public ReadQuery getQuery(QueryOptions options, int nowInSec) throws RequestValidationException
     {
-        return getQuery(options, nowInSec, getLimit(options));
+        return getQuery(options, nowInSec, getLimit(options), getPerPartitionLimit(options), options.getPageSize());
     }
 
-    public ReadQuery getQuery(QueryOptions options, int nowInSec, int userLimit) throws RequestValidationException
+    public ReadQuery getQuery(QueryOptions options, int nowInSec, int userLimit, int perPartitionLimit, int pageSize)
     {
-        DataLimits limit = getDataLimits(userLimit);
-        if (restrictions.isKeyRange() || restrictions.usesSecondaryIndexing())
+        boolean isPartitionRangeQuery = restrictions.isKeyRange() || restrictions.usesSecondaryIndexing();
+
+        DataLimits limit = getDataLimits(userLimit, perPartitionLimit, pageSize);
+
+        if (isPartitionRangeQuery)
             return getRangeCommand(options, limit, nowInSec);
 
         return getSliceCommands(options, limit, nowInSec);
@@ -297,9 +310,9 @@
                                        QueryOptions options,
                                        QueryState state,
                                        int nowInSec,
-                                       int userLimit) throws RequestValidationException, RequestExecutionException
+                                       int userLimit, long queryStartNanoTime) throws RequestValidationException, RequestExecutionException
     {
-        try (PartitionIterator data = query.execute(options.getConsistency(), state.getClientState()))
+        try (PartitionIterator data = query.execute(options.getConsistency(), state.getClientState(), queryStartNanoTime))
         {
             return processResults(data, options, nowInSec, userLimit);
         }
@@ -315,9 +328,9 @@
             this.pager = pager;
         }
 
-        public static Pager forInternalQuery(QueryPager pager, ReadOrderGroup orderGroup)
+        public static Pager forInternalQuery(QueryPager pager, ReadExecutionController executionController)
         {
-            return new InternalPager(pager, orderGroup);
+            return new InternalPager(pager, executionController);
         }
 
         public static Pager forDistributedQuery(QueryPager pager, ConsistencyLevel consistency, ClientState clientState)
@@ -335,7 +348,7 @@
             return pager.state();
         }
 
-        public abstract PartitionIterator fetchPage(int pageSize);
+        public abstract PartitionIterator fetchPage(int pageSize, long queryStartNanoTime);
 
         public static class NormalPager extends Pager
         {
@@ -349,25 +362,25 @@
                 this.clientState = clientState;
             }
 
-            public PartitionIterator fetchPage(int pageSize)
+            public PartitionIterator fetchPage(int pageSize, long queryStartNanoTime)
             {
-                return pager.fetchPage(pageSize, consistency, clientState);
+                return pager.fetchPage(pageSize, consistency, clientState, queryStartNanoTime);
             }
         }
 
         public static class InternalPager extends Pager
         {
-            private final ReadOrderGroup orderGroup;
+            private final ReadExecutionController executionController;
 
-            private InternalPager(QueryPager pager, ReadOrderGroup orderGroup)
+            private InternalPager(QueryPager pager, ReadExecutionController executionController)
             {
                 super(pager);
-                this.orderGroup = orderGroup;
+                this.executionController = executionController;
             }
 
-            public PartitionIterator fetchPage(int pageSize)
+            public PartitionIterator fetchPage(int pageSize, long queryStartNanoTime)
             {
-                return pager.fetchPageInternal(pageSize, orderGroup);
+                return pager.fetchPageInternal(pageSize, executionController);
             }
         }
     }
@@ -376,18 +389,29 @@
                                        QueryOptions options,
                                        int pageSize,
                                        int nowInSec,
-                                       int userLimit) throws RequestValidationException, RequestExecutionException
+                                       int userLimit,
+                                       long queryStartNanoTime) throws RequestValidationException, RequestExecutionException
     {
-        if (selection.isAggregate())
-            return pageAggregateQuery(pager, options, pageSize, nowInSec);
+        if (aggregationSpec != null)
+        {
+            if (!restrictions.hasPartitionKeyRestrictions())
+            {
+                warn("Aggregation query used without partition key");
+            }
+            else if (restrictions.keyIsInRelation())
+            {
+                warn("Aggregation query used on multiple partition keys (IN restriction)");
+            }
+        }
 
         // We can't properly do post-query ordering if we page (see #6722)
-        checkFalse(needsPostQueryOrdering(),
+        // For GROUP BY or aggregation queries we always page internally even if the user has turned paging off
+        checkFalse(pageSize > 0 && needsPostQueryOrdering(),
                   "Cannot page queries with both ORDER BY and a IN restriction on the partition key;"
                   + " you must either remove the ORDER BY or the IN and sort client side, or disable paging for this query");
 
         ResultMessage.Rows msg;
-        try (PartitionIterator page = pager.fetchPage(pageSize))
+        try (PartitionIterator page = pager.fetchPage(pageSize, queryStartNanoTime))
         {
             msg = processResults(page, options, nowInSec, userLimit);
         }
@@ -400,35 +424,10 @@
         return msg;
     }
 
-    private ResultMessage.Rows pageAggregateQuery(Pager pager, QueryOptions options, int pageSize, int nowInSec)
-    throws RequestValidationException, RequestExecutionException
+    private void warn(String msg)
     {
-        if (!restrictions.hasPartitionKeyRestrictions())
-        {
-            logger.warn("Aggregation query used without partition key");
-            ClientWarn.instance.warn("Aggregation query used without partition key");
-        }
-        else if (restrictions.keyIsInRelation())
-        {
-            logger.warn("Aggregation query used on multiple partition keys (IN restriction)");
-            ClientWarn.instance.warn("Aggregation query used on multiple partition keys (IN restriction)");
-        }
-
-        Selection.ResultSetBuilder result = selection.resultSetBuilder(parameters.isJson);
-        while (!pager.isExhausted())
-        {
-            try (PartitionIterator iter = pager.fetchPage(pageSize))
-            {
-                while (iter.hasNext())
-                {
-                    try (RowIterator partition = iter.next())
-                    {
-                        processPartition(partition, options, result, nowInSec);
-                    }
-                }
-            }
-        }
-        return new ResultMessage.Rows(result.build(options.getProtocolVersion()));
+        logger.warn(msg);
+        ClientWarn.instance.warn(msg);
     }
 
     private ResultMessage.Rows processResults(PartitionIterator partitions,
@@ -442,32 +441,44 @@
 
     public ResultMessage.Rows executeInternal(QueryState state, QueryOptions options) throws RequestExecutionException, RequestValidationException
     {
-        return executeInternal(state, options, FBUtilities.nowInSeconds());
+        return executeInternal(state, options, FBUtilities.nowInSeconds(), System.nanoTime());
     }
 
-    public ResultMessage.Rows executeInternal(QueryState state, QueryOptions options, int nowInSec) throws RequestExecutionException, RequestValidationException
+    public ResultMessage.Rows executeInternal(QueryState state, QueryOptions options, int nowInSec, long queryStartNanoTime) throws RequestExecutionException, RequestValidationException
     {
         int userLimit = getLimit(options);
-        ReadQuery query = getQuery(options, nowInSec, userLimit);
-        int pageSize = getPageSize(options);
+        int userPerPartitionLimit = getPerPartitionLimit(options);
+        int pageSize = options.getPageSize();
+        ReadQuery query = getQuery(options, nowInSec, userLimit, userPerPartitionLimit, pageSize);
 
-        try (ReadOrderGroup orderGroup = query.startOrderGroup())
+        try (ReadExecutionController executionController = query.executionController())
         {
-            if (pageSize <= 0 || query.limits().count() <= pageSize)
+            if (aggregationSpec == null && (pageSize <= 0 || (query.limits().count() <= pageSize)))
             {
-                try (PartitionIterator data = query.executeInternal(orderGroup))
+                try (PartitionIterator data = query.executeInternal(executionController))
                 {
                     return processResults(data, options, nowInSec, userLimit);
                 }
             }
             else
             {
-                QueryPager pager = query.getPager(options.getPagingState(), options.getProtocolVersion());
-                return execute(Pager.forInternalQuery(pager, orderGroup), options, pageSize, nowInSec, userLimit);
+                QueryPager pager = getPager(query, options);
+
+                return execute(Pager.forInternalQuery(pager, executionController), options, pageSize, nowInSec, userLimit, queryStartNanoTime);
             }
         }
     }
 
+    private QueryPager getPager(ReadQuery query, QueryOptions options)
+    {
+        QueryPager pager = query.getPager(options.getPagingState(), options.getProtocolVersion());
+
+        if (aggregationSpec == null || query == ReadQuery.EMPTY)
+            return pager;
+
+        return new AggregationQueryPager(pager, query.limits());
+    }
+
     public ResultSet process(PartitionIterator partitions, int nowInSec) throws InvalidRequestException
     {
         return process(partitions, QueryOptions.DEFAULT, nowInSec, getLimit(QueryOptions.DEFAULT));
@@ -628,27 +639,27 @@
     private Slices makeSlices(QueryOptions options)
     throws InvalidRequestException
     {
-        SortedSet<Slice.Bound> startBounds = restrictions.getClusteringColumnsBounds(Bound.START, options);
-        SortedSet<Slice.Bound> endBounds = restrictions.getClusteringColumnsBounds(Bound.END, options);
+        SortedSet<ClusteringBound> startBounds = restrictions.getClusteringColumnsBounds(Bound.START, options);
+        SortedSet<ClusteringBound> endBounds = restrictions.getClusteringColumnsBounds(Bound.END, options);
         assert startBounds.size() == endBounds.size();
 
         // The case where startBounds == 1 is common enough that it's worth optimizing
         if (startBounds.size() == 1)
         {
-            Slice.Bound start = startBounds.first();
-            Slice.Bound end = endBounds.first();
+            ClusteringBound start = startBounds.first();
+            ClusteringBound end = endBounds.first();
             return cfm.comparator.compare(start, end) > 0
                  ? Slices.NONE
                  : Slices.with(cfm.comparator, Slice.make(start, end));
         }
 
         Slices.Builder builder = new Slices.Builder(cfm.comparator, startBounds.size());
-        Iterator<Slice.Bound> startIter = startBounds.iterator();
-        Iterator<Slice.Bound> endIter = endBounds.iterator();
+        Iterator<ClusteringBound> startIter = startBounds.iterator();
+        Iterator<ClusteringBound> endIter = endBounds.iterator();
         while (startIter.hasNext() && endIter.hasNext())
         {
-            Slice.Bound start = startIter.next();
-            Slice.Bound end = endIter.next();
+            ClusteringBound start = startIter.next();
+            ClusteringBound end = endIter.next();
 
             // Ignore slices that are nonsensical
             if (cfm.comparator.compare(start, end) > 0)
@@ -660,34 +671,69 @@
         return builder.build();
     }
 
-    private DataLimits getDataLimits(int userLimit)
+    private DataLimits getDataLimits(int userLimit, int perPartitionLimit, int pageSize)
     {
         int cqlRowLimit = DataLimits.NO_LIMIT;
+        int cqlPerPartitionLimit = DataLimits.NO_LIMIT;
 
-        // If we aggregate, the limit really apply to the number of rows returned to the user, not to what is queried, and
-        // since in practice we currently only aggregate at top level (we have no GROUP BY support yet), we'll only ever
-        // return 1 result and can therefore basically ignore the user LIMIT in this case.
-        // Whenever we support GROUP BY, we'll have to add a new DataLimits kind that knows how things are grouped and is thus
-        // able to apply the user limit properly.
         // If we do post ordering we need to get all the results sorted before we can trim them.
-        if (!selection.isAggregate() && !needsPostQueryOrdering())
-            cqlRowLimit = userLimit;
+        if (aggregationSpec != AggregationSpecification.AGGREGATE_EVERYTHING)
+        {
+            if (!needsPostQueryOrdering())
+                cqlRowLimit = userLimit;
+            cqlPerPartitionLimit = perPartitionLimit;
+        }
+
+        // Group by and aggregation queries will always be paged internally to avoid OOM.
+        // If the user provided a pageSize we'll use that to page internally (because why not), otherwise we use our default
+        if (pageSize <= 0)
+            pageSize = DEFAULT_PAGE_SIZE;
+
+        // Aggregation queries work fine on top of the group by paging but to maintain
+        // backward compatibility we need to use the old way.
+        if (aggregationSpec != null && aggregationSpec != AggregationSpecification.AGGREGATE_EVERYTHING)
+        {
+            if (parameters.isDistinct)
+                return DataLimits.distinctLimits(cqlRowLimit);
+
+            return DataLimits.groupByLimits(cqlRowLimit,
+                                            cqlPerPartitionLimit,
+                                            pageSize,
+                                            aggregationSpec);
+        }
 
         if (parameters.isDistinct)
             return cqlRowLimit == DataLimits.NO_LIMIT ? DataLimits.DISTINCT_NONE : DataLimits.distinctLimits(cqlRowLimit);
 
-        return cqlRowLimit == DataLimits.NO_LIMIT ? DataLimits.NONE : DataLimits.cqlLimits(cqlRowLimit);
+        return DataLimits.cqlLimits(cqlRowLimit, cqlPerPartitionLimit);
     }
 
     /**
      * Returns the limit specified by the user.
      * May be used by custom QueryHandler implementations
      *
-     * @return the limit specified by the user or <code>DataLimits.NO_LIMIT</code> if no value 
+     * @return the limit specified by the user or <code>DataLimits.NO_LIMIT</code> if no value
      * as been specified.
      */
     public int getLimit(QueryOptions options)
     {
+        return getLimit(limit, options);
+    }
+
+    /**
+     * Returns the per partition limit specified by the user.
+     * May be used by custom QueryHandler implementations
+     *
+     * @return the per partition limit specified by the user or <code>DataLimits.NO_LIMIT</code> if no value
+     * as been specified.
+     */
+    public int getPerPartitionLimit(QueryOptions options)
+    {
+        return getLimit(perPartitionLimit, options);
+    }
+
+    private int getLimit(Term limit, QueryOptions options)
+    {
         int userLimit = DataLimits.NO_LIMIT;
 
         if (limit != null)
@@ -735,7 +781,8 @@
                               int nowInSec,
                               int userLimit) throws InvalidRequestException
     {
-        Selection.ResultSetBuilder result = selection.resultSetBuilder(parameters.isJson);
+        Selection.ResultSetBuilder result = selection.resultSetBuilder(options, parameters.isJson, aggregationSpec);
+
         while (partitions.hasNext())
         {
             try (RowIterator partition = partitions.next())
@@ -744,7 +791,7 @@
             }
         }
 
-        ResultSet cqlRows = result.build(options.getProtocolVersion());
+        ResultSet cqlRows = result.build();
 
         orderResults(cqlRows);
 
@@ -776,7 +823,7 @@
             return;
         }
 
-        int protocolVersion = options.getProtocolVersion();
+        ProtocolVersion protocolVersion = options.getProtocolVersion();
 
         ByteBuffer[] keyComponents = getComponents(cfm, partition.partitionKey());
 
@@ -786,11 +833,9 @@
         // we want to include static columns and we're done.
         if (!partition.hasNext())
         {
-            if (!staticRow.isEmpty()
-                && (!restrictions.hasClusteringColumnsRestriction() || cfm.isStaticCompactTable())
-                && !restrictions.hasRegularColumnsRestriction())
+            if (!staticRow.isEmpty() && (queriesFullPartitions() || cfm.isStaticCompactTable()))
             {
-                result.newRow(protocolVersion);
+                result.newRow(partition.partitionKey(), staticRow.clustering());
                 for (ColumnDefinition def : selection.getColumns())
                 {
                     switch (def.kind)
@@ -812,7 +857,7 @@
         while (partition.hasNext())
         {
             Row row = partition.next();
-            result.newRow(protocolVersion);
+            result.newRow( partition.partitionKey(), row.clustering());
             // Respect selection order
             for (ColumnDefinition def : selection.getColumns())
             {
@@ -835,17 +880,27 @@
         }
     }
 
-    private static void addValue(Selection.ResultSetBuilder result, ColumnDefinition def, Row row, int nowInSec, int protocolVersion)
+    /**
+     * Checks if the query is a full partitions selection.
+     * @return {@code true} if the query is a full partitions selection, {@code false} otherwise.
+     */
+    private boolean queriesFullPartitions()
+    {
+        return !restrictions.hasClusteringColumnsRestrictions() && !restrictions.hasRegularColumnsRestrictions();
+    }
+
+    private static void addValue(Selection.ResultSetBuilder result, ColumnDefinition def, Row row, int nowInSec, ProtocolVersion protocolVersion)
     {
         if (def.isComplex())
         {
-            // Collections are the only complex types we have so far
-            assert def.type.isCollection() && def.type.isMultiCell();
+            assert def.type.isMultiCell();
             ComplexColumnData complexData = row.getComplexColumnData(def);
             if (complexData == null)
-                result.add((ByteBuffer)null);
+                result.add(null);
+            else if (def.type.isCollection())
+                result.add(((CollectionType) def.type).serializeForNativeProtocol(complexData.iterator(), protocolVersion));
             else
-                result.add(((CollectionType)def.type).serializeForNativeProtocol(def, complexData.iterator(), protocolVersion));
+                result.add(((UserType) def.type).serializeForNativeProtocol(complexData.iterator(), protocolVersion));
         }
         else
         {
@@ -883,14 +938,20 @@
         public final List<RawSelector> selectClause;
         public final WhereClause whereClause;
         public final Term.Raw limit;
+        public final Term.Raw perPartitionLimit;
 
-        public RawStatement(CFName cfName, Parameters parameters, List<RawSelector> selectClause, WhereClause whereClause, Term.Raw limit)
+        public RawStatement(CFName cfName, Parameters parameters,
+                            List<RawSelector> selectClause,
+                            WhereClause whereClause,
+                            Term.Raw limit,
+                            Term.Raw perPartitionLimit)
         {
             super(cfName);
             this.parameters = parameters;
             this.selectClause = selectClause;
             this.whereClause = whereClause;
             this.limit = limit;
+            this.perPartitionLimit = perPartitionLimit;
         }
 
         public ParsedStatement.Prepared prepare(ClientState clientState) throws InvalidRequestException
@@ -903,14 +964,23 @@
             CFMetaData cfm = ThriftValidation.validateColumnFamilyWithCompactMode(keyspace(), columnFamily(), clientState.isNoCompactMode());
             VariableSpecifications boundNames = getBoundVariables();
 
-            Selection selection = selectClause.isEmpty()
-                                  ? Selection.wildcard(cfm)
-                                  : Selection.fromSelectors(cfm, selectClause);
+            Selection selection = prepareSelection(cfm, boundNames);
 
             StatementRestrictions restrictions = prepareRestrictions(cfm, boundNames, selection, forView);
 
             if (parameters.isDistinct)
+            {
+                checkNull(perPartitionLimit, "PER PARTITION LIMIT is not allowed with SELECT DISTINCT queries");
                 validateDistinctSelection(cfm, selection, restrictions);
+            }
+
+            AggregationSpecification aggregationSpec = getAggregationSpecification(cfm,
+                                                                                   selection,
+                                                                                   restrictions,
+                                                                                   parameters.isDistinct);
+
+            checkFalse(aggregationSpec == AggregationSpecification.AGGREGATE_EVERYTHING && perPartitionLimit != null,
+                       "PER PARTITION LIMIT is not allowed with aggregate queries.");
 
             Comparator<List<ByteBuffer>> orderingComparator = null;
             boolean isReversed = false;
@@ -928,20 +998,39 @@
             checkNeedsFiltering(restrictions);
 
             SelectStatement stmt = new SelectStatement(cfm,
-                                                        boundNames.size(),
-                                                        parameters,
-                                                        selection,
-                                                        restrictions,
-                                                        isReversed,
-                                                        orderingComparator,
-                                                        prepareLimit(boundNames));
+                                                       boundNames.size(),
+                                                       parameters,
+                                                       selection,
+                                                       restrictions,
+                                                       isReversed,
+                                                       aggregationSpec,
+                                                       orderingComparator,
+                                                       prepareLimit(boundNames, limit, keyspace(), limitReceiver()),
+                                                       prepareLimit(boundNames, perPartitionLimit, keyspace(), perPartitionLimitReceiver()));
 
             return prepare(stmt, boundNames, cfm);
         }
 
         protected ParsedStatement.Prepared prepare(SelectStatement stmt, VariableSpecifications boundNames, CFMetaData cfm)
         {
-            return new ParsedStatement.Prepared(stmt, boundNames, boundNames.getPartitionKeyBindIndexes(cfm), cfm.ksName, isFullyQualified());
+            return new ParsedStatement. Prepared(stmt, boundNames, boundNames.getPartitionKeyBindIndexes(cfm), cfm.ksName, isFullyQualified());
+        }
+
+        /**
+         * Prepares the selection to use for the statement.
+         *
+         * @param cfm the table metadata
+         * @param boundNames the bound names
+         * @return the selection to use for the statement
+         */
+        private Selection prepareSelection(CFMetaData cfm, VariableSpecifications boundNames)
+        {
+            boolean hasGroupBy = !parameters.groups.isEmpty();
+
+            if (selectClause.isEmpty())
+                return hasGroupBy ? Selection.wildcardWithGroupBy(cfm, boundNames) : Selection.wildcard(cfm);
+
+            return Selection.fromSelectors(cfm, selectClause, boundNames, hasGroupBy);
         }
 
         /**
@@ -958,32 +1047,24 @@
                                                           Selection selection,
                                                           boolean forView) throws InvalidRequestException
         {
-            try
-            {
-                return new StatementRestrictions(StatementType.SELECT,
-                                                 cfm,
-                                                 whereClause,
-                                                 boundNames,
-                                                 selection.containsOnlyStaticColumns(),
-                                                 selection.containsACollection(),
-                                                 parameters.allowFiltering,
-                                                 forView);
-            }
-            catch (UnrecognizedEntityException e)
-            {
-                if (containsAlias(e.entity))
-                    throw invalidRequest("Aliases aren't allowed in the where clause ('%s')", e.relation);
-                throw e;
-            }
+            return new StatementRestrictions(StatementType.SELECT,
+                                             cfm,
+                                             whereClause,
+                                             boundNames,
+                                             selection.containsOnlyStaticColumns(),
+                                             selection.containsAComplexColumn(),
+                                             parameters.allowFiltering,
+                                             forView);
         }
 
         /** Returns a Term for the limit or null if no limit is set */
-        private Term prepareLimit(VariableSpecifications boundNames) throws InvalidRequestException
+        private Term prepareLimit(VariableSpecifications boundNames, Term.Raw limit,
+                                  String keyspace, ColumnSpecification limitReceiver) throws InvalidRequestException
         {
             if (limit == null)
                 return null;
 
-            Term prepLimit = limit.prepare(keyspace(), limitReceiver());
+            Term prepLimit = limit.prepare(keyspace, limitReceiver);
             prepLimit.collectMarkerSpecification(boundNames);
             return prepLimit;
         }
@@ -999,7 +1080,7 @@
                                                       StatementRestrictions restrictions)
                                                       throws InvalidRequestException
         {
-            checkFalse(restrictions.hasClusteringColumnsRestriction() ||
+            checkFalse(restrictions.hasClusteringColumnsRestrictions() ||
                        (restrictions.hasNonPrimaryKeyRestrictions() && !restrictions.nonPKRestrictedColumns(true).stream().allMatch(ColumnDefinition::isStatic)),
                        "SELECT DISTINCT with WHERE clause only supports restriction by partition key and/or static columns.");
 
@@ -1019,10 +1100,61 @@
                           "SELECT DISTINCT queries must request all the partition key columns (missing %s)", def.name);
         }
 
-        private void handleUnrecognizedOrderingColumn(ColumnIdentifier column) throws InvalidRequestException
+        /**
+         * Creates the <code>AggregationSpecification</code>s used to make the aggregates.
+         *
+         * @param cfm the column family metadata
+         * @param selection the selection
+         * @param restrictions the restrictions
+         * @param isDistinct <code>true</code> if the query is a DISTINCT one. 
+         * @return the <code>AggregationSpecification</code>s used to make the aggregates
+         */
+        private AggregationSpecification getAggregationSpecification(CFMetaData cfm,
+                                                                     Selection selection,
+                                                                     StatementRestrictions restrictions,
+                                                                     boolean isDistinct)
         {
-            checkFalse(containsAlias(column), "Aliases are not allowed in order by clause ('%s')", column);
-            checkFalse(true, "Order by on unknown column %s", column);
+            if (parameters.groups.isEmpty())
+                return selection.isAggregate() ? AggregationSpecification.AGGREGATE_EVERYTHING
+                                               : null;
+
+            int clusteringPrefixSize = 0;
+
+            Iterator<ColumnDefinition> pkColumns = cfm.primaryKeyColumns().iterator();
+            for (ColumnDefinition.Raw raw : parameters.groups)
+            {
+                ColumnDefinition def = raw.prepare(cfm);
+
+                checkTrue(def.isPartitionKey() || def.isClusteringColumn(),
+                          "Group by is currently only supported on the columns of the PRIMARY KEY, got %s", def.name);
+
+                while (true)
+                {
+                    checkTrue(pkColumns.hasNext(),
+                              "Group by currently only support groups of columns following their declared order in the PRIMARY KEY");
+
+                    ColumnDefinition pkColumn = pkColumns.next();
+
+                    if (pkColumn.isClusteringColumn())
+                        clusteringPrefixSize++;
+
+                    // As we do not support grouping on only part of the partition key, we only need to know
+                    // which clustering columns need to be used to build the groups
+                    if (pkColumn.equals(def))
+                        break;
+
+                    checkTrue(restrictions.isColumnRestrictedByEq(pkColumn),
+                              "Group by currently only support groups of columns following their declared order in the PRIMARY KEY");
+                }
+            }
+
+            checkFalse(pkColumns.hasNext() && pkColumns.next().isPartitionKey(),
+                       "Group by is not supported on only a part of the partition key");
+
+            checkFalse(clusteringPrefixSize > 0 && isDistinct,
+                       "Grouping on clustering columns is not allowed for SELECT DISTINCT queries");
+
+            return AggregationSpecification.aggregatePkPrefix(cfm.comparator, clusteringPrefixSize);
         }
 
         private Comparator<List<ByteBuffer>> getOrderingComparator(CFMetaData cfm,
@@ -1039,10 +1171,9 @@
             List<Integer> idToSort = new ArrayList<Integer>();
             List<Comparator<ByteBuffer>> sorters = new ArrayList<Comparator<ByteBuffer>>();
 
-            for (ColumnIdentifier.Raw raw : parameters.orderings.keySet())
+            for (ColumnDefinition.Raw raw : parameters.orderings.keySet())
             {
-                ColumnIdentifier identifier = raw.prepare(cfm);
-                ColumnDefinition orderingColumn = cfm.getColumnDefinitionForCQL(identifier);
+                ColumnDefinition orderingColumn = raw.prepare(cfm);
                 idToSort.add(orderingIndexes.get(orderingColumn));
                 sorters.add(orderingColumn.type);
             }
@@ -1056,12 +1187,9 @@
             // If we order post-query (see orderResults), the sorted column needs to be in the ResultSet for sorting,
             // even if we don't
             // ultimately ship them to the client (CASSANDRA-4911).
-            for (ColumnIdentifier.Raw raw : parameters.orderings.keySet())
+            for (ColumnDefinition.Raw raw : parameters.orderings.keySet())
             {
-                ColumnIdentifier column = raw.prepare(cfm);
-                final ColumnDefinition def = cfm.getColumnDefinitionForCQL(column);
-                if (def == null)
-                    handleUnrecognizedOrderingColumn(column);
+                final ColumnDefinition def = raw.prepare(cfm);
                 selection.addColumnForOrdering(def);
             }
             return selection.getOrderingIndex(isJson);
@@ -1071,17 +1199,13 @@
         {
             Boolean[] reversedMap = new Boolean[cfm.clusteringColumns().size()];
             int i = 0;
-            for (Map.Entry<ColumnIdentifier.Raw, Boolean> entry : parameters.orderings.entrySet())
+            for (Map.Entry<ColumnDefinition.Raw, Boolean> entry : parameters.orderings.entrySet())
             {
-                ColumnIdentifier column = entry.getKey().prepare(cfm);
+                ColumnDefinition def = entry.getKey().prepare(cfm);
                 boolean reversed = entry.getValue();
 
-                ColumnDefinition def = cfm.getColumnDefinitionForCQL(column);
-                if (def == null)
-                    handleUnrecognizedOrderingColumn(column);
-
                 checkTrue(def.isClusteringColumn(),
-                          "Order by is currently only supported on the clustered columns of the PRIMARY KEY, got %s", column);
+                          "Order by is currently only supported on the clustered columns of the PRIMARY KEY, got %s", def.name);
 
                 checkTrue(i++ == def.position(),
                           "Order by currently only support the ordering of columns following their declared order in the PRIMARY KEY");
@@ -1121,48 +1245,45 @@
             }
         }
 
-        private boolean containsAlias(final ColumnIdentifier name)
-        {
-            return Iterables.any(selectClause, new Predicate<RawSelector>()
-                                               {
-                                                   public boolean apply(RawSelector raw)
-                                                   {
-                                                       return name.equals(raw.alias);
-                                                   }
-                                               });
-        }
-
         private ColumnSpecification limitReceiver()
         {
             return new ColumnSpecification(keyspace(), columnFamily(), new ColumnIdentifier("[limit]", true), Int32Type.instance);
         }
 
+        private ColumnSpecification perPartitionLimitReceiver()
+        {
+            return new ColumnSpecification(keyspace(), columnFamily(), new ColumnIdentifier("[per_partition_limit]", true), Int32Type.instance);
+        }
+
         @Override
         public String toString()
         {
-            return Objects.toStringHelper(this)
-                          .add("name", cfName)
-                          .add("selectClause", selectClause)
-                          .add("whereClause", whereClause)
-                          .add("isDistinct", parameters.isDistinct)
-                          .toString();
+            return MoreObjects.toStringHelper(this)
+                              .add("name", cfName)
+                              .add("selectClause", selectClause)
+                              .add("whereClause", whereClause)
+                              .add("isDistinct", parameters.isDistinct)
+                              .toString();
         }
     }
 
     public static class Parameters
     {
         // Public because CASSANDRA-9858
-        public final Map<ColumnIdentifier.Raw, Boolean> orderings;
+        public final Map<ColumnDefinition.Raw, Boolean> orderings;
+        public final List<ColumnDefinition.Raw> groups;
         public final boolean isDistinct;
         public final boolean allowFiltering;
         public final boolean isJson;
 
-        public Parameters(Map<ColumnIdentifier.Raw, Boolean> orderings,
+        public Parameters(Map<ColumnDefinition.Raw, Boolean> orderings,
+                          List<ColumnDefinition.Raw> groups,
                           boolean isDistinct,
                           boolean allowFiltering,
                           boolean isJson)
         {
             this.orderings = orderings;
+            this.groups = groups;
             this.isDistinct = isDistinct;
             this.allowFiltering = allowFiltering;
             this.isJson = isJson;
diff --git a/src/java/org/apache/cassandra/cql3/statements/TableAttributes.java b/src/java/org/apache/cassandra/cql3/statements/TableAttributes.java
index 595fdb3..04b9532 100644
--- a/src/java/org/apache/cassandra/cql3/statements/TableAttributes.java
+++ b/src/java/org/apache/cassandra/cql3/statements/TableAttributes.java
@@ -154,6 +154,9 @@
         if (hasOption(Option.CRC_CHECK_CHANCE))
             builder.crcCheckChance(getDouble(Option.CRC_CHECK_CHANCE));
 
+        if (hasOption(Option.CDC))
+            builder.cdc(getBoolean(Option.CDC.toString(), false));
+
         return builder.build();
     }
 
@@ -171,7 +174,7 @@
         String value = compressionOpts.get(Option.CRC_CHECK_CHANCE.toString().toLowerCase());
         try
         {
-            return Double.parseDouble(value);
+            return Double.valueOf(value);
         }
         catch (NumberFormatException e)
         {
diff --git a/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java b/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java
index b697910..302d2e2 100644
--- a/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java
@@ -17,7 +17,6 @@
  */
 package org.apache.cassandra.cql3.statements;
 
-import java.io.IOException;
 import java.util.concurrent.TimeoutException;
 
 import org.apache.cassandra.auth.Permission;
@@ -60,7 +59,7 @@
         ThriftValidation.validateColumnFamily(keyspace(), columnFamily());
     }
 
-    public ResultMessage execute(QueryState state, QueryOptions options) throws InvalidRequestException, TruncateException
+    public ResultMessage execute(QueryState state, QueryOptions options, long queryStartNanoTime) throws InvalidRequestException, TruncateException
     {
         try
         {
@@ -70,7 +69,7 @@
 
             StorageProxy.truncateBlocking(keyspace(), columnFamily());
         }
-        catch (UnavailableException | TimeoutException | IOException e)
+        catch (UnavailableException | TimeoutException e)
         {
             throw new TruncateException(e);
         }
diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java
index 641b6bb..86fe990 100644
--- a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java
@@ -90,8 +90,8 @@
                 updates = Collections.<Operation>singletonList(new Constants.Setter(cfm.compactValueColumn(), EMPTY));
             }
 
-            for (Operation op : updates)
-                op.execute(update.partitionKey(), params);
+            for (int i = 0, isize = updates.size(); i < isize; i++)
+                updates.get(i).execute(update.partitionKey(), params);
 
             update.add(params.buildRow());
         }
@@ -99,8 +99,9 @@
         if (updatesStaticRow())
         {
             params.newRow(Clustering.STATIC_CLUSTERING);
-            for (Operation op : getStaticOperations())
-                op.execute(update.partitionKey(), params);
+            List<Operation> staticOps = getStaticOperations();
+            for (int i = 0, isize = staticOps.size(); i < isize; i++)
+                staticOps.get(i).execute(update.partitionKey(), params);
             update.add(params.buildRow());
         }
     }
@@ -113,7 +114,7 @@
 
     public static class ParsedInsert extends ModificationStatement.Parsed
     {
-        private final List<ColumnIdentifier.Raw> columnNames;
+        private final List<ColumnDefinition.Raw> columnNames;
         private final List<Term.Raw> columnValues;
 
         /**
@@ -127,7 +128,7 @@
          */
         public ParsedInsert(CFName name,
                             Attributes.Raw attrs,
-                            List<ColumnIdentifier.Raw> columnNames,
+                            List<ColumnDefinition.Raw> columnNames,
                             List<Term.Raw> columnValues,
                             boolean ifNotExists)
         {
@@ -178,14 +179,14 @@
                     }
                     else
                     {
-                        Operation operation = new Operation.SetValue(value).prepare(cfm.ksName, def);
+                        Operation operation = new Operation.SetValue(value).prepare(cfm, def);
                         operation.collectMarkerSpecification(boundNames);
                         operations.add(operation);
                     }
                 }
             }
 
-            boolean applyOnlyToStaticColumns = appliesOnlyToStaticColumns(operations, conditions) && !hasClusteringColumnsSet;
+            boolean applyOnlyToStaticColumns = !hasClusteringColumnsSet && appliesOnlyToStaticColumns(operations, conditions);
 
             StatementRestrictions restrictions = new StatementRestrictions(type,
                                                                            cfm,
@@ -212,11 +213,13 @@
     public static class ParsedInsertJson extends ModificationStatement.Parsed
     {
         private final Json.Raw jsonValue;
+        private final boolean defaultUnset;
 
-        public ParsedInsertJson(CFName name, Attributes.Raw attrs, Json.Raw jsonValue, boolean ifNotExists)
+        public ParsedInsertJson(CFName name, Attributes.Raw attrs, Json.Raw jsonValue, boolean defaultUnset, boolean ifNotExists)
         {
             super(name, StatementType.INSERT, attrs, null, ifNotExists, false);
             this.jsonValue = jsonValue;
+            this.defaultUnset = defaultUnset;
         }
 
         @Override
@@ -241,26 +244,35 @@
             }
             else
             {
-                for (ColumnDefinition def : defs)
-                {
-                    if (def.isClusteringColumn())
-                        hasClusteringColumnsSet = true;
 
-                    Term.Raw raw = prepared.getRawTermForColumn(def);
-                    if (def.isPrimaryKeyColumn())
-                    {
-                        whereClause.add(new SingleColumnRelation(new ColumnIdentifier.ColumnIdentifierValue(def.name), Operator.EQ, raw));
-                    }
-                    else
-                    {
-                        Operation operation = new Operation.SetValue(raw).prepare(cfm.ksName, def);
-                        operation.collectMarkerSpecification(boundNames);
-                        operations.add(operation);
-                    }
+
+
+// TODO: indent
+
+            for (ColumnDefinition def : defs)
+            {
+                if (def.isClusteringColumn())
+                    hasClusteringColumnsSet = true;
+
+                Term.Raw raw = prepared.getRawTermForColumn(def, defaultUnset);
+                if (def.isPrimaryKeyColumn())
+                {
+                    whereClause.add(new SingleColumnRelation(ColumnDefinition.Raw.forColumn(def), Operator.EQ, raw));
+                }
+                else
+                {
+                    Operation operation = new Operation.SetValue(raw).prepare(cfm, def);
+                    operation.collectMarkerSpecification(boundNames);
+                    operations.add(operation);
                 }
             }
 
-            boolean applyOnlyToStaticColumns = appliesOnlyToStaticColumns(operations, conditions) && !hasClusteringColumnsSet;
+
+
+
+            }
+
+            boolean applyOnlyToStaticColumns = !hasClusteringColumnsSet && appliesOnlyToStaticColumns(operations, conditions);
 
             StatementRestrictions restrictions = new StatementRestrictions(type,
                                                                            cfm,
@@ -284,7 +296,7 @@
     public static class ParsedUpdate extends ModificationStatement.Parsed
     {
         // Provided for an UPDATE
-        private final List<Pair<ColumnIdentifier.Raw, Operation.RawUpdate>> updates;
+        private final List<Pair<ColumnDefinition.Raw, Operation.RawUpdate>> updates;
         private WhereClause whereClause;
 
         /**
@@ -299,9 +311,9 @@
          * */
         public ParsedUpdate(CFName name,
                             Attributes.Raw attrs,
-                            List<Pair<ColumnIdentifier.Raw, Operation.RawUpdate>> updates,
+                            List<Pair<ColumnDefinition.Raw, Operation.RawUpdate>> updates,
                             WhereClause whereClause,
-                            List<Pair<ColumnIdentifier.Raw, ColumnCondition.Raw>> conditions,
+                            List<Pair<ColumnDefinition.Raw, ColumnCondition.Raw>> conditions,
                             boolean ifExists)
         {
             super(name, StatementType.UPDATE, attrs, conditions, false, ifExists);
@@ -324,13 +336,13 @@
             }
             else
             {
-                for (Pair<ColumnIdentifier.Raw, Operation.RawUpdate> entry : updates)
+                for (Pair<ColumnDefinition.Raw, Operation.RawUpdate> entry : updates)
                 {
                     ColumnDefinition def = getColumnDefinition(cfm, entry.left);
 
                     checkFalse(def.isPrimaryKeyColumn(), "PRIMARY KEY part %s found in SET part", def.name);
 
-                    Operation operation = entry.right.prepare(cfm.ksName, def);
+                    Operation operation = entry.right.prepare(cfm, def);
                     operation.collectMarkerSpecification(boundNames);
                     operations.add(operation);
                 }
diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdatesCollector.java b/src/java/org/apache/cassandra/cql3/statements/UpdatesCollector.java
index 1d65a78..4943abf 100644
--- a/src/java/org/apache/cassandra/cql3/statements/UpdatesCollector.java
+++ b/src/java/org/apache/cassandra/cql3/statements/UpdatesCollector.java
@@ -20,6 +20,9 @@
 import java.nio.ByteBuffer;
 import java.util.*;
 
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Maps;
+
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
@@ -39,20 +42,23 @@
     private final Map<UUID, PartitionColumns> updatedColumns;
 
     /**
-     * The estimated number of updated row.
+     * The estimated number of updated rows per partition.
      */
-    private final int updatedRows;
+    private final Map<UUID, HashMultiset<ByteBuffer>> partitionCounts;
+
+    public final long createdAt = System.currentTimeMillis();
 
     /**
      * The mutations per keyspace.
      */
-    private final Map<String, Map<ByteBuffer, IMutation>> mutations = new HashMap<>();
+    private final Map<String, Map<ByteBuffer, IMutation>> mutations;
 
-    public UpdatesCollector(Map<UUID, PartitionColumns> updatedColumns, int updatedRows)
+    public UpdatesCollector(Map<UUID, PartitionColumns> updatedColumns, Map<UUID, HashMultiset<ByteBuffer>> partitionCounts)
     {
         super();
         this.updatedColumns = updatedColumns;
-        this.updatedRows = updatedRows;
+        this.partitionCounts = partitionCounts;
+        mutations = Maps.newHashMapWithExpectedSize(partitionCounts.size()); // most often this is too big - optimised for the single-table case
     }
 
     /**
@@ -72,7 +78,7 @@
         {
             PartitionColumns columns = updatedColumns.get(cfm.cfId);
             assert columns != null;
-            upd = new PartitionUpdate(cfm, dk, columns, updatedRows);
+            upd = new PartitionUpdate(cfm, dk, columns, partitionCounts.get(cfm.cfId).count(dk.getKey()), (int)(createdAt / 1000));
             mut.add(upd);
         }
         return upd;
@@ -96,7 +102,7 @@
         IMutation mutation = keyspaceMap(ksName).get(dk.getKey());
         if (mutation == null)
         {
-            Mutation mut = new Mutation(ksName, dk);
+            Mutation mut = new Mutation(ksName, dk, createdAt, cfm.params.cdc);
             mutation = cfm.isCounter() ? new CounterMutation(mut, consistency) : mut;
             keyspaceMap(ksName).put(dk.getKey(), mutation);
             return mut;
@@ -114,7 +120,7 @@
         if (mutations.size() == 1)
             return mutations.values().iterator().next().values();
 
-        List<IMutation> ms = new ArrayList<>();
+        List<IMutation> ms = new ArrayList<>(mutations.size());
         for (Map<ByteBuffer, IMutation> ksMap : mutations.values())
             ms.addAll(ksMap.values());
 
diff --git a/src/java/org/apache/cassandra/cql3/statements/UseStatement.java b/src/java/org/apache/cassandra/cql3/statements/UseStatement.java
index e4685cc..0242f09 100644
--- a/src/java/org/apache/cassandra/cql3/statements/UseStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/UseStatement.java
@@ -53,7 +53,7 @@
     {
     }
 
-    public ResultMessage execute(QueryState state, QueryOptions options) throws InvalidRequestException
+    public ResultMessage execute(QueryState state, QueryOptions options, long queryStartNanoTime) throws InvalidRequestException
     {
         state.getClientState().setKeyspace(keyspace);
         return new ResultMessage.SetKeyspace(keyspace);
@@ -63,6 +63,6 @@
     {
         // In production, internal queries are exclusively on the system keyspace and 'use' is thus useless
         // but for some unit tests we need to set the keyspace (e.g. for tests with DROP INDEX)
-        return execute(state, options);
+        return execute(state, options, System.nanoTime());
     }
 }
diff --git a/src/java/org/apache/cassandra/db/AbstractBufferClusteringPrefix.java b/src/java/org/apache/cassandra/db/AbstractBufferClusteringPrefix.java
new file mode 100644
index 0000000..95bc777
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/AbstractBufferClusteringPrefix.java
@@ -0,0 +1,72 @@
+/*
+ * 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.db;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.utils.ObjectSizes;
+
+public abstract class AbstractBufferClusteringPrefix extends AbstractClusteringPrefix
+{
+    public static final ByteBuffer[] EMPTY_VALUES_ARRAY = new ByteBuffer[0];
+    private static final long EMPTY_SIZE = ObjectSizes.measure(Clustering.make(EMPTY_VALUES_ARRAY));
+
+    protected final Kind kind;
+    protected final ByteBuffer[] values;
+
+    protected AbstractBufferClusteringPrefix(Kind kind, ByteBuffer[] values)
+    {
+        this.kind = kind;
+        this.values = values;
+    }
+
+    public Kind kind()
+    {
+        return kind;
+    }
+
+    public ClusteringPrefix clustering()
+    {
+        return this;
+    }
+
+    public int size()
+    {
+        return values.length;
+    }
+
+    public ByteBuffer get(int i)
+    {
+        return values[i];
+    }
+
+    public ByteBuffer[] getRawValues()
+    {
+        return values;
+    }
+
+    public long unsharedHeapSize()
+    {
+        return EMPTY_SIZE + ObjectSizes.sizeOnHeapOf(values);
+    }
+
+    public long unsharedHeapSizeExcludingData()
+    {
+        return EMPTY_SIZE + ObjectSizes.sizeOnHeapExcludingData(values);
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/AbstractClusteringPrefix.java b/src/java/org/apache/cassandra/db/AbstractClusteringPrefix.java
index 2631b46..0b1daf7 100644
--- a/src/java/org/apache/cassandra/db/AbstractClusteringPrefix.java
+++ b/src/java/org/apache/cassandra/db/AbstractClusteringPrefix.java
@@ -22,48 +22,14 @@
 import java.util.Objects;
 
 import org.apache.cassandra.utils.FBUtilities;
-import org.apache.cassandra.utils.ObjectSizes;
 
 public abstract class AbstractClusteringPrefix implements ClusteringPrefix
 {
-    protected static final ByteBuffer[] EMPTY_VALUES_ARRAY = new ByteBuffer[0];
-
-    private static final long EMPTY_SIZE = ObjectSizes.measure(new Clustering(EMPTY_VALUES_ARRAY));
-
-    protected final Kind kind;
-    protected final ByteBuffer[] values;
-
-    protected AbstractClusteringPrefix(Kind kind, ByteBuffer[] values)
-    {
-        this.kind = kind;
-        this.values = values;
-    }
-
-    public Kind kind()
-    {
-        return kind;
-    }
-
     public ClusteringPrefix clustering()
     {
         return this;
     }
 
-    public int size()
-    {
-        return values.length;
-    }
-
-    public ByteBuffer get(int i)
-    {
-        return values[i];
-    }
-
-    public ByteBuffer[] getRawValues()
-    {
-        return values;
-    }
-
     public int dataSize()
     {
         int size = 0;
@@ -86,16 +52,6 @@
         FBUtilities.updateWithByte(digest, kind().ordinal());
     }
 
-    public long unsharedHeapSize()
-    {
-        return EMPTY_SIZE + ObjectSizes.sizeOnHeapOf(values);
-    }
-
-    public long unsharedHeapSizeExcludingData()
-    {
-        return EMPTY_SIZE + ObjectSizes.sizeOnHeapExcludingData(values);
-    }
-
     @Override
     public final int hashCode()
     {
diff --git a/src/java/org/apache/cassandra/db/AbstractReadCommandBuilder.java b/src/java/org/apache/cassandra/db/AbstractReadCommandBuilder.java
index a7f3319..ba91db8 100644
--- a/src/java/org/apache/cassandra/db/AbstractReadCommandBuilder.java
+++ b/src/java/org/apache/cassandra/db/AbstractReadCommandBuilder.java
@@ -45,8 +45,8 @@
     protected Set<ColumnIdentifier> columns;
     protected final RowFilter filter = RowFilter.create();
 
-    private Slice.Bound lowerClusteringBound;
-    private Slice.Bound upperClusteringBound;
+    private ClusteringBound lowerClusteringBound;
+    private ClusteringBound upperClusteringBound;
 
     private NavigableSet<Clustering> clusterings;
 
@@ -66,28 +66,28 @@
     public AbstractReadCommandBuilder fromIncl(Object... values)
     {
         assert lowerClusteringBound == null && clusterings == null;
-        this.lowerClusteringBound = Slice.Bound.create(cfs.metadata.comparator, true, true, values);
+        this.lowerClusteringBound = ClusteringBound.create(cfs.metadata.comparator, true, true, values);
         return this;
     }
 
     public AbstractReadCommandBuilder fromExcl(Object... values)
     {
         assert lowerClusteringBound == null && clusterings == null;
-        this.lowerClusteringBound = Slice.Bound.create(cfs.metadata.comparator, true, false, values);
+        this.lowerClusteringBound = ClusteringBound.create(cfs.metadata.comparator, true, false, values);
         return this;
     }
 
     public AbstractReadCommandBuilder toIncl(Object... values)
     {
         assert upperClusteringBound == null && clusterings == null;
-        this.upperClusteringBound = Slice.Bound.create(cfs.metadata.comparator, false, true, values);
+        this.upperClusteringBound = ClusteringBound.create(cfs.metadata.comparator, false, true, values);
         return this;
     }
 
     public AbstractReadCommandBuilder toExcl(Object... values)
     {
         assert upperClusteringBound == null && clusterings == null;
-        this.upperClusteringBound = Slice.Bound.create(cfs.metadata.comparator, false, false, values);
+        this.upperClusteringBound = ClusteringBound.create(cfs.metadata.comparator, false, false, values);
         return this;
     }
 
@@ -166,7 +166,7 @@
     @VisibleForTesting
     public AbstractReadCommandBuilder filterOn(String column, Operator op, Object value)
     {
-        ColumnDefinition def = cfs.metadata.getColumnDefinition(ColumnIdentifier.getInterned(column, true));
+        ColumnDefinition def = cfs.metadata.getColumnDefinitionForCQL(ColumnIdentifier.getInterned(column, true));
         assert def != null;
 
         AbstractType<?> type = def.type;
@@ -205,8 +205,8 @@
         }
         else
         {
-            Slice slice = Slice.make(lowerClusteringBound == null ? Slice.Bound.BOTTOM : lowerClusteringBound,
-                                     upperClusteringBound == null ? Slice.Bound.TOP : upperClusteringBound);
+            Slice slice = Slice.make(lowerClusteringBound == null ? ClusteringBound.BOTTOM : lowerClusteringBound,
+                                     upperClusteringBound == null ? ClusteringBound.TOP : upperClusteringBound);
             return new ClusteringIndexSliceFilter(Slices.with(cfs.metadata.comparator, slice), reversed);
         }
     }
diff --git a/src/java/org/apache/cassandra/db/BufferClustering.java b/src/java/org/apache/cassandra/db/BufferClustering.java
new file mode 100644
index 0000000..8cb8a59
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/BufferClustering.java
@@ -0,0 +1,51 @@
+/*
+ * 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.db;
+
+import java.nio.ByteBuffer;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+/**
+ * The clustering column values for a row.
+ * <p>
+ * A {@code Clustering} is a {@code ClusteringPrefix} that must always be "complete", i.e. have
+ * as many values as there is clustering columns in the table it is part of. It is the clustering
+ * prefix used by rows.
+ * <p>
+ * Note however that while it's size must be equal to the table clustering size, a clustering can have
+ * {@code null} values, and this mostly for thrift backward compatibility (in practice, if a value is null,
+ * all of the following ones will be too because that's what thrift allows, but it's never assumed by the
+ * code so we could start generally allowing nulls for clustering columns if we wanted to).
+ */
+public class BufferClustering extends AbstractBufferClusteringPrefix implements Clustering
+{
+    @VisibleForTesting
+    public BufferClustering(ByteBuffer... values)
+    {
+        super(Kind.CLUSTERING, values);
+    }
+
+    public ClusteringPrefix minimize()
+    {
+        if (!ByteBufferUtil.canMinimize(values))
+            return this;
+        return new BufferClustering(ByteBufferUtil.minimizeBuffers(values));    }
+}
diff --git a/src/java/org/apache/cassandra/db/CBuilder.java b/src/java/org/apache/cassandra/db/CBuilder.java
index 94feb93..be56394 100644
--- a/src/java/org/apache/cassandra/db/CBuilder.java
+++ b/src/java/org/apache/cassandra/db/CBuilder.java
@@ -24,7 +24,7 @@
 import org.apache.cassandra.db.marshal.AbstractType;
 
 /**
- * Allows to build ClusteringPrefixes, either Clustering or Slice.Bound.
+ * Allows to build ClusteringPrefixes, either Clustering or ClusteringBound.
  */
 public abstract class CBuilder
 {
@@ -60,7 +60,7 @@
             return Clustering.STATIC_CLUSTERING;
         }
 
-        public Slice.Bound buildBound(boolean isStart, boolean isInclusive)
+        public ClusteringBound buildBound(boolean isStart, boolean isInclusive)
         {
             throw new UnsupportedOperationException();
         }
@@ -80,12 +80,12 @@
             throw new UnsupportedOperationException();
         }
 
-        public Slice.Bound buildBoundWith(ByteBuffer value, boolean isStart, boolean isInclusive)
+        public ClusteringBound buildBoundWith(ByteBuffer value, boolean isStart, boolean isInclusive)
         {
             throw new UnsupportedOperationException();
         }
 
-        public Slice.Bound buildBoundWith(List<ByteBuffer> newValues, boolean isStart, boolean isInclusive)
+        public ClusteringBound buildBoundWith(List<ByteBuffer> newValues, boolean isStart, boolean isInclusive)
         {
             throw new UnsupportedOperationException();
         }
@@ -102,12 +102,12 @@
     public abstract CBuilder add(ByteBuffer value);
     public abstract CBuilder add(Object value);
     public abstract Clustering build();
-    public abstract Slice.Bound buildBound(boolean isStart, boolean isInclusive);
+    public abstract ClusteringBound buildBound(boolean isStart, boolean isInclusive);
     public abstract Slice buildSlice();
     public abstract Clustering buildWith(ByteBuffer value);
     public abstract Clustering buildWith(List<ByteBuffer> newValues);
-    public abstract Slice.Bound buildBoundWith(ByteBuffer value, boolean isStart, boolean isInclusive);
-    public abstract Slice.Bound buildBoundWith(List<ByteBuffer> newValues, boolean isStart, boolean isInclusive);
+    public abstract ClusteringBound buildBoundWith(ByteBuffer value, boolean isStart, boolean isInclusive);
+    public abstract ClusteringBound buildBoundWith(List<ByteBuffer> newValues, boolean isStart, boolean isInclusive);
 
     private static class ArrayBackedBuilder extends CBuilder
     {
@@ -162,20 +162,20 @@
             built = true;
 
             // Currently, only dense table can leave some clustering column out (see #7990)
-            return size == 0 ? Clustering.EMPTY : new Clustering(values);
+            return size == 0 ? Clustering.EMPTY : Clustering.make(values);
         }
 
-        public Slice.Bound buildBound(boolean isStart, boolean isInclusive)
+        public ClusteringBound buildBound(boolean isStart, boolean isInclusive)
         {
             // We don't allow to add more element to a builder that has been built so
             // that we don't have to copy values (even though we have to do it in most cases).
             built = true;
 
             if (size == 0)
-                return isStart ? Slice.Bound.BOTTOM : Slice.Bound.TOP;
+                return isStart ? ClusteringBound.BOTTOM : ClusteringBound.TOP;
 
-            return Slice.Bound.create(Slice.Bound.boundKind(isStart, isInclusive),
-                                      size == values.length ? values : Arrays.copyOfRange(values, 0, size));
+            return ClusteringBound.create(ClusteringBound.boundKind(isStart, isInclusive),
+                                size == values.length ? values : Arrays.copyOfRange(values, 0, size));
         }
 
         public Slice buildSlice()
@@ -196,7 +196,7 @@
 
             ByteBuffer[] newValues = Arrays.copyOf(values, type.size());
             newValues[size] = value;
-            return new Clustering(newValues);
+            return Clustering.make(newValues);
         }
 
         public Clustering buildWith(List<ByteBuffer> newValues)
@@ -207,24 +207,24 @@
             for (ByteBuffer value : newValues)
                 buffers[newSize++] = value;
 
-            return new Clustering(buffers);
+            return Clustering.make(buffers);
         }
 
-        public Slice.Bound buildBoundWith(ByteBuffer value, boolean isStart, boolean isInclusive)
+        public ClusteringBound buildBoundWith(ByteBuffer value, boolean isStart, boolean isInclusive)
         {
             ByteBuffer[] newValues = Arrays.copyOf(values, size+1);
             newValues[size] = value;
-            return Slice.Bound.create(Slice.Bound.boundKind(isStart, isInclusive), newValues);
+            return ClusteringBound.create(ClusteringBound.boundKind(isStart, isInclusive), newValues);
         }
 
-        public Slice.Bound buildBoundWith(List<ByteBuffer> newValues, boolean isStart, boolean isInclusive)
+        public ClusteringBound buildBoundWith(List<ByteBuffer> newValues, boolean isStart, boolean isInclusive)
         {
             ByteBuffer[] buffers = Arrays.copyOf(values, size + newValues.size());
             int newSize = size;
             for (ByteBuffer value : newValues)
                 buffers[newSize++] = value;
 
-            return Slice.Bound.create(Slice.Bound.boundKind(isStart, isInclusive), buffers);
+            return ClusteringBound.create(ClusteringBound.boundKind(isStart, isInclusive), buffers);
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/db/Clustering.java b/src/java/org/apache/cassandra/db/Clustering.java
index 62af0f1..fa38ce1 100644
--- a/src/java/org/apache/cassandra/db/Clustering.java
+++ b/src/java/org/apache/cassandra/db/Clustering.java
@@ -1,54 +1,91 @@
 /*
- * 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.
- */
+* 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.db;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.*;
+import java.util.List;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.io.util.*;
-import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.utils.memory.AbstractAllocator;
 
-/**
- * The clustering column values for a row.
- * <p>
- * A {@code Clustering} is a {@code ClusteringPrefix} that must always be "complete", i.e. have
- * as many values as there is clustering columns in the table it is part of. It is the clustering
- * prefix used by rows.
- * <p>
- * Note however that while it's size must be equal to the table clustering size, a clustering can have
- * {@code null} values, and this mostly for thrift backward compatibility (in practice, if a value is null,
- * all of the following ones will be too because that's what thrift allows, but it's never assumed by the
- * code so we could start generally allowing nulls for clustering columns if we wanted to).
- */
-public class Clustering extends AbstractClusteringPrefix
+import static org.apache.cassandra.db.AbstractBufferClusteringPrefix.EMPTY_VALUES_ARRAY;
+
+public interface Clustering extends ClusteringPrefix
 {
     public static final Serializer serializer = new Serializer();
 
+    public long unsharedHeapSizeExcludingData();
+
+    public default Clustering copy(AbstractAllocator allocator)
+    {
+        // Important for STATIC_CLUSTERING (but must copy empty native clustering types).
+        if (size() == 0)
+            return kind() == Kind.STATIC_CLUSTERING ? this : new BufferClustering(EMPTY_VALUES_ARRAY);
+
+        ByteBuffer[] newValues = new ByteBuffer[size()];
+        for (int i = 0; i < size(); i++)
+        {
+            ByteBuffer val = get(i);
+            newValues[i] = val == null ? null : allocator.clone(val);
+        }
+        return new BufferClustering(newValues);
+    }
+
+    public default String toString(CFMetaData metadata)
+    {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < size(); i++)
+        {
+            ColumnDefinition c = metadata.clusteringColumns().get(i);
+            sb.append(i == 0 ? "" : ", ").append(c.name).append('=').append(get(i) == null ? "null" : c.type.getString(get(i)));
+        }
+        return sb.toString();
+    }
+
+    public default String toCQLString(CFMetaData metadata)
+    {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < size(); i++)
+        {
+            ColumnDefinition c = metadata.clusteringColumns().get(i);
+            sb.append(i == 0 ? "" : ", ").append(c.type.getString(get(i)));
+        }
+        return sb.toString();
+    }
+
+    public static Clustering make(ByteBuffer... values)
+    {
+        return new BufferClustering(values);
+    }
+
     /**
      * The special cased clustering used by all static rows. It is a special case in the
      * sense that it's always empty, no matter how many clustering columns the table has.
      */
-    public static final Clustering STATIC_CLUSTERING = new Clustering(EMPTY_VALUES_ARRAY)
+    public static final Clustering STATIC_CLUSTERING = new BufferClustering(EMPTY_VALUES_ARRAY)
     {
         @Override
         public Kind kind()
@@ -70,7 +107,7 @@
     };
 
     /** Empty clustering for tables having no clustering columns. */
-    public static final Clustering EMPTY = new Clustering(EMPTY_VALUES_ARRAY)
+    public static final Clustering EMPTY = new BufferClustering(EMPTY_VALUES_ARRAY)
     {
         @Override
         public String toString(CFMetaData metadata)
@@ -79,57 +116,6 @@
         }
     };
 
-    public Clustering(ByteBuffer... values)
-    {
-        super(Kind.CLUSTERING, values);
-    }
-
-    public Kind kind()
-    {
-        return Kind.CLUSTERING;
-    }
-
-    public ClusteringPrefix minimize()
-    {
-        if (!ByteBufferUtil.canMinimize(values))
-            return this;
-        return new Clustering(ByteBufferUtil.minimizeBuffers(values));
-    }
-
-    public Clustering copy(AbstractAllocator allocator)
-    {
-        // Important for STATIC_CLUSTERING (but no point in being wasteful in general).
-        if (size() == 0)
-            return this;
-
-        ByteBuffer[] newValues = new ByteBuffer[size()];
-        for (int i = 0; i < size(); i++)
-            newValues[i] = values[i] == null ? null : allocator.clone(values[i]);
-        return new Clustering(newValues);
-    }
-
-    public String toString(CFMetaData metadata)
-    {
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < size(); i++)
-        {
-            ColumnDefinition c = metadata.clusteringColumns().get(i);
-            sb.append(i == 0 ? "" : ", ").append(c.name).append('=').append(get(i) == null ? "null" : c.type.getString(get(i)));
-        }
-        return sb.toString();
-    }
-
-    public String toCQLString(CFMetaData metadata)
-    {
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < size(); i++)
-        {
-            ColumnDefinition c = metadata.clusteringColumns().get(i);
-            sb.append(i == 0 ? "" : ", ").append(c.type.getString(get(i)));
-        }
-        return sb.toString();
-    }
-
     /**
      * Serializer for Clustering object.
      * <p>
@@ -163,13 +149,19 @@
             return ClusteringPrefix.serializer.valuesWithoutSizeSerializedSize(clustering, version, types);
         }
 
+        public void skip(DataInputPlus in, int version, List<AbstractType<?>> types) throws IOException
+        {
+            if (!types.isEmpty())
+                ClusteringPrefix.serializer.skipValuesWithoutSize(in, types.size(), version, types);
+        }
+
         public Clustering deserialize(DataInputPlus in, int version, List<AbstractType<?>> types) throws IOException
         {
             if (types.isEmpty())
                 return EMPTY;
 
             ByteBuffer[] values = ClusteringPrefix.serializer.deserializeValuesWithoutSize(in, types.size(), version, types);
-            return new Clustering(values);
+            return new BufferClustering(values);
         }
 
         public Clustering deserialize(ByteBuffer in, int version, List<AbstractType<?>> types)
diff --git a/src/java/org/apache/cassandra/db/ClusteringBound.java b/src/java/org/apache/cassandra/db/ClusteringBound.java
new file mode 100644
index 0000000..8bfeb32
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/ClusteringBound.java
@@ -0,0 +1,179 @@
+/*
+ *
+ * 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.db;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.memory.AbstractAllocator;
+
+/**
+ * The start or end of a range of clusterings, either inclusive or exclusive.
+ */
+public class ClusteringBound extends ClusteringBoundOrBoundary
+{
+    /** The smallest start bound, i.e. the one that starts before any row. */
+    public static final ClusteringBound BOTTOM = new ClusteringBound(Kind.INCL_START_BOUND, EMPTY_VALUES_ARRAY);
+    /** The biggest end bound, i.e. the one that ends after any row. */
+    public static final ClusteringBound TOP = new ClusteringBound(Kind.INCL_END_BOUND, EMPTY_VALUES_ARRAY);
+
+    protected ClusteringBound(Kind kind, ByteBuffer[] values)
+    {
+        super(kind, values);
+    }
+
+    public static ClusteringBound create(Kind kind, ByteBuffer[] values)
+    {
+        assert !kind.isBoundary();
+        return new ClusteringBound(kind, values);
+    }
+
+    public static Kind boundKind(boolean isStart, boolean isInclusive)
+    {
+        return isStart
+             ? (isInclusive ? Kind.INCL_START_BOUND : Kind.EXCL_START_BOUND)
+             : (isInclusive ? Kind.INCL_END_BOUND : Kind.EXCL_END_BOUND);
+    }
+
+    public static ClusteringBound inclusiveStartOf(ByteBuffer... values)
+    {
+        return create(Kind.INCL_START_BOUND, values);
+    }
+
+    public static ClusteringBound inclusiveEndOf(ByteBuffer... values)
+    {
+        return create(Kind.INCL_END_BOUND, values);
+    }
+
+    public static ClusteringBound exclusiveStartOf(ByteBuffer... values)
+    {
+        return create(Kind.EXCL_START_BOUND, values);
+    }
+
+    public static ClusteringBound exclusiveEndOf(ByteBuffer... values)
+    {
+        return create(Kind.EXCL_END_BOUND, values);
+    }
+
+    public static ClusteringBound inclusiveStartOf(ClusteringPrefix prefix)
+    {
+        ByteBuffer[] values = new ByteBuffer[prefix.size()];
+        for (int i = 0; i < prefix.size(); i++)
+            values[i] = prefix.get(i);
+        return inclusiveStartOf(values);
+    }
+
+    public static ClusteringBound exclusiveStartOf(ClusteringPrefix prefix)
+    {
+        ByteBuffer[] values = new ByteBuffer[prefix.size()];
+        for (int i = 0; i < prefix.size(); i++)
+            values[i] = prefix.get(i);
+        return exclusiveStartOf(values);
+    }
+
+    public static ClusteringBound inclusiveEndOf(ClusteringPrefix prefix)
+    {
+        ByteBuffer[] values = new ByteBuffer[prefix.size()];
+        for (int i = 0; i < prefix.size(); i++)
+            values[i] = prefix.get(i);
+        return inclusiveEndOf(values);
+    }
+
+    public static ClusteringBound create(ClusteringComparator comparator, boolean isStart, boolean isInclusive, Object... values)
+    {
+        CBuilder builder = CBuilder.create(comparator);
+        for (Object val : values)
+        {
+            if (val instanceof ByteBuffer)
+                builder.add((ByteBuffer) val);
+            else
+                builder.add(val);
+        }
+        return builder.buildBound(isStart, isInclusive);
+    }
+
+    @Override
+    public ClusteringBound invert()
+    {
+        return create(kind().invert(), values);
+    }
+
+    public ClusteringBound copy(AbstractAllocator allocator)
+    {
+        return (ClusteringBound) super.copy(allocator);
+    }
+
+    public ClusteringPrefix minimize()
+    {
+        if (!ByteBufferUtil.canMinimize(values))
+            return this;
+        return new ClusteringBound(kind, ByteBufferUtil.minimizeBuffers(values));
+    }
+
+    public boolean isStart()
+    {
+        return kind().isStart();
+    }
+
+    public boolean isEnd()
+    {
+        return !isStart();
+    }
+
+    public boolean isInclusive()
+    {
+        return kind == Kind.INCL_START_BOUND || kind == Kind.INCL_END_BOUND;
+    }
+
+    public boolean isExclusive()
+    {
+        return kind == Kind.EXCL_START_BOUND || kind == Kind.EXCL_END_BOUND;
+    }
+
+    // For use by intersects, it's called with the sstable bound opposite to the slice bound
+    // (so if the slice bound is a start, it's call with the max sstable bound)
+    int compareTo(ClusteringComparator comparator, List<ByteBuffer> sstableBound)
+    {
+        for (int i = 0; i < sstableBound.size(); i++)
+        {
+            // Say the slice bound is a start. It means we're in the case where the max
+            // sstable bound is say (1:5) while the slice start is (1). So the start
+            // does start before the sstable end bound (and intersect it). It's the exact
+            // inverse with a end slice bound.
+            if (i >= size())
+                return isStart() ? -1 : 1;
+
+            int cmp = comparator.compareComponent(i, get(i), sstableBound.get(i));
+            if (cmp != 0)
+                return cmp;
+        }
+
+        // Say the slice bound is a start. I means we're in the case where the max
+        // sstable bound is say (1), while the slice start is (1:5). This again means
+        // that the slice start before the end bound.
+        if (size() > sstableBound.size())
+            return isStart() ? -1 : 1;
+
+        // The slice bound is equal to the sstable bound. Results depends on whether the slice is inclusive or not
+        return isInclusive() ? 0 : (isStart() ? 1 : -1);
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/ClusteringBoundOrBoundary.java b/src/java/org/apache/cassandra/db/ClusteringBoundOrBoundary.java
new file mode 100644
index 0000000..7a2cce1
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/ClusteringBoundOrBoundary.java
@@ -0,0 +1,183 @@
+/*
+ *
+ * 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.db;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.utils.memory.AbstractAllocator;
+
+/**
+ * This class defines a threshold between ranges of clusterings. It can either be a start or end bound of a range, or
+ * the boundary between two different defined ranges.
+ * <p>
+ * The latter is used for range tombstones for 2 main reasons:
+ *   1) When merging multiple iterators having range tombstones (that are represented by their start and end markers),
+ *      we need to know when a range is close on an iterator, if it is reopened right away. Otherwise, we cannot
+ *      easily produce the markers on the merged iterators within risking to fail the sorting guarantees of an
+ *      iterator. See this comment for more details: https://goo.gl/yyB5mR.
+ *   2) This saves some storage space.
+ */
+public abstract class ClusteringBoundOrBoundary extends AbstractBufferClusteringPrefix
+{
+    public static final ClusteringBoundOrBoundary.Serializer serializer = new Serializer();
+
+    protected ClusteringBoundOrBoundary(Kind kind, ByteBuffer[] values)
+    {
+        super(kind, values);
+        assert values.length > 0 || !kind.isBoundary();
+    }
+
+    public static ClusteringBoundOrBoundary create(Kind kind, ByteBuffer[] values)
+    {
+        return kind.isBoundary()
+                ? new ClusteringBoundary(kind, values)
+                : new ClusteringBound(kind, values);
+    }
+
+    public boolean isBoundary()
+    {
+        return kind.isBoundary();
+    }
+
+    public boolean isOpen(boolean reversed)
+    {
+        return kind.isOpen(reversed);
+    }
+
+    public boolean isClose(boolean reversed)
+    {
+        return kind.isClose(reversed);
+    }
+
+    public static ClusteringBound inclusiveOpen(boolean reversed, ByteBuffer[] boundValues)
+    {
+        return new ClusteringBound(reversed ? Kind.INCL_END_BOUND : Kind.INCL_START_BOUND, boundValues);
+    }
+
+    public static ClusteringBound exclusiveOpen(boolean reversed, ByteBuffer[] boundValues)
+    {
+        return new ClusteringBound(reversed ? Kind.EXCL_END_BOUND : Kind.EXCL_START_BOUND, boundValues);
+    }
+
+    public static ClusteringBound inclusiveClose(boolean reversed, ByteBuffer[] boundValues)
+    {
+        return new ClusteringBound(reversed ? Kind.INCL_START_BOUND : Kind.INCL_END_BOUND, boundValues);
+    }
+
+    public static ClusteringBound exclusiveClose(boolean reversed, ByteBuffer[] boundValues)
+    {
+        return new ClusteringBound(reversed ? Kind.EXCL_START_BOUND : Kind.EXCL_END_BOUND, boundValues);
+    }
+
+    public static ClusteringBoundary inclusiveCloseExclusiveOpen(boolean reversed, ByteBuffer[] boundValues)
+    {
+        return new ClusteringBoundary(reversed ? Kind.EXCL_END_INCL_START_BOUNDARY : Kind.INCL_END_EXCL_START_BOUNDARY, boundValues);
+    }
+
+    public static ClusteringBoundary exclusiveCloseInclusiveOpen(boolean reversed, ByteBuffer[] boundValues)
+    {
+        return new ClusteringBoundary(reversed ? Kind.INCL_END_EXCL_START_BOUNDARY : Kind.EXCL_END_INCL_START_BOUNDARY, boundValues);
+    }
+
+    public ClusteringBoundOrBoundary copy(AbstractAllocator allocator)
+    {
+        ByteBuffer[] newValues = new ByteBuffer[size()];
+        for (int i = 0; i < size(); i++)
+            newValues[i] = allocator.clone(get(i));
+        return create(kind(), newValues);
+    }
+
+    public String toString(CFMetaData metadata)
+    {
+        return toString(metadata.comparator);
+    }
+
+    public String toString(ClusteringComparator comparator)
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.append(kind()).append('(');
+        for (int i = 0; i < size(); i++)
+        {
+            if (i > 0)
+                sb.append(", ");
+            sb.append(comparator.subtype(i).getString(get(i)));
+        }
+        return sb.append(')').toString();
+    }
+
+    /**
+     * Returns the inverse of the current bound.
+     * <p>
+     * This invert both start into end (and vice-versa) and inclusive into exclusive (and vice-versa).
+     *
+     * @return the invert of this bound. For instance, if this bound is an exlusive start, this return
+     * an inclusive end with the same values.
+     */
+    public abstract ClusteringBoundOrBoundary invert();
+
+    public static class Serializer
+    {
+        public void serialize(ClusteringBoundOrBoundary bound, DataOutputPlus out, int version, List<AbstractType<?>> types) throws IOException
+        {
+            out.writeByte(bound.kind().ordinal());
+            out.writeShort(bound.size());
+            ClusteringPrefix.serializer.serializeValuesWithoutSize(bound, out, version, types);
+        }
+
+        public long serializedSize(ClusteringBoundOrBoundary bound, int version, List<AbstractType<?>> types)
+        {
+            return 1 // kind ordinal
+                 + TypeSizes.sizeof((short)bound.size())
+                 + ClusteringPrefix.serializer.valuesWithoutSizeSerializedSize(bound, version, types);
+        }
+
+        public ClusteringBoundOrBoundary deserialize(DataInputPlus in, int version, List<AbstractType<?>> types) throws IOException
+        {
+            Kind kind = Kind.values()[in.readByte()];
+            return deserializeValues(in, kind, version, types);
+        }
+
+        public void skipValues(DataInputPlus in, Kind kind, int version, List<AbstractType<?>> types) throws IOException
+        {
+            int size = in.readUnsignedShort();
+            if (size == 0)
+                return;
+
+            ClusteringPrefix.serializer.skipValuesWithoutSize(in, size, version, types);
+        }
+
+        public ClusteringBoundOrBoundary deserializeValues(DataInputPlus in, Kind kind, int version, List<AbstractType<?>> types) throws IOException
+        {
+            int size = in.readUnsignedShort();
+            if (size == 0)
+                return kind.isStart() ? ClusteringBound.BOTTOM : ClusteringBound.TOP;
+
+            ByteBuffer[] values = ClusteringPrefix.serializer.deserializeValuesWithoutSize(in, size, version, types);
+            return create(kind, values);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/ClusteringBoundary.java b/src/java/org/apache/cassandra/db/ClusteringBoundary.java
new file mode 100644
index 0000000..7a6842b
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/ClusteringBoundary.java
@@ -0,0 +1,73 @@
+/*
+ *
+ * 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.db;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.memory.AbstractAllocator;
+
+/**
+ * The threshold between two different ranges, i.e. a shortcut for the combination of two ClusteringBounds -- one
+ * specifying the end of one of the ranges, and its (implicit) complement specifying the beginning of the other.
+ */
+public class ClusteringBoundary extends ClusteringBoundOrBoundary
+{
+    protected ClusteringBoundary(Kind kind, ByteBuffer[] values)
+    {
+        super(kind, values);
+    }
+
+    public static ClusteringBoundary create(Kind kind, ByteBuffer[] values)
+    {
+        assert kind.isBoundary();
+        return new ClusteringBoundary(kind, values);
+    }
+
+    @Override
+    public ClusteringBoundary invert()
+    {
+        return create(kind().invert(), values);
+    }
+
+    @Override
+    public ClusteringBoundary copy(AbstractAllocator allocator)
+    {
+        return (ClusteringBoundary) super.copy(allocator);
+    }
+
+    public ClusteringPrefix minimize()
+    {
+        if (!ByteBufferUtil.canMinimize(values))
+            return this;
+        return new ClusteringBoundary(kind, ByteBufferUtil.minimizeBuffers(values));
+    }
+
+    public ClusteringBound openBound(boolean reversed)
+    {
+        return ClusteringBound.create(kind.openBoundOfBoundary(reversed), values);
+    }
+
+    public ClusteringBound closeBound(boolean reversed)
+    {
+        return ClusteringBound.create(kind.closeBoundOfBoundary(reversed), values);
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/ClusteringComparator.java b/src/java/org/apache/cassandra/db/ClusteringComparator.java
index f3411cf..4374a46 100644
--- a/src/java/org/apache/cassandra/db/ClusteringComparator.java
+++ b/src/java/org/apache/cassandra/db/ClusteringComparator.java
@@ -18,7 +18,6 @@
 package org.apache.cassandra.db;
 
 import java.nio.ByteBuffer;
-import java.util.Arrays;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
@@ -29,10 +28,8 @@
 import org.apache.cassandra.db.rows.Row;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.serializers.MarshalException;
-import org.apache.cassandra.utils.ByteBufferUtil;
-import org.apache.cassandra.utils.FastByteOperations;
 
-import static org.apache.cassandra.io.sstable.IndexHelper.IndexInfo;
+import org.apache.cassandra.io.sstable.IndexInfo;
 
 /**
  * A comparator of clustering prefixes (or more generally of {@link Clusterable}}.
@@ -147,7 +144,21 @@
 
     public int compare(Clustering c1, Clustering c2)
     {
-        for (int i = 0; i < size(); i++)
+        return compare(c1, c2, size());
+    }
+
+    /**
+     * Compares the specified part of the specified clusterings.
+     *
+     * @param c1 the first clustering
+     * @param c2 the second clustering
+     * @param size the number of components to compare
+     * @return a negative integer, zero, or a positive integer as the first argument is less than,
+     * equal to, or greater than the second.
+     */
+    public int compare(Clustering c1, Clustering c2, int size)
+    {
+        for (int i = 0; i < size; i++)
         {
             int cmp = compareComponent(i, c1.get(i), c2.get(i));
             if (cmp != 0)
diff --git a/src/java/org/apache/cassandra/db/ClusteringPrefix.java b/src/java/org/apache/cassandra/db/ClusteringPrefix.java
index c1000e4..1f93ca8 100644
--- a/src/java/org/apache/cassandra/db/ClusteringPrefix.java
+++ b/src/java/org/apache/cassandra/db/ClusteringPrefix.java
@@ -37,10 +37,10 @@
  * a "kind" that allows us to implement slices with inclusive and exclusive bounds.
  * <p>
  * In practice, {@code ClusteringPrefix} is just the common parts to its 3 main subtype: {@link Clustering} and
- * {@link Slice.Bound}/{@link RangeTombstone.Bound}, where:
+ * {@link ClusteringBound}/{@link ClusteringBoundary}, where:
  *   1) {@code Clustering} represents the clustering values for a row, i.e. the values for it's clustering columns.
- *   2) {@code Slice.Bound} represents a bound (start or end) of a slice (of rows).
- *   3) {@code RangeTombstoneBoundMarker.Bound} represents a range tombstone marker "bound".
+ *   2) {@code ClusteringBound} represents a bound (start or end) of a slice (of rows) or a range tombstone.
+ *   3) {@code ClusteringBoundary} represents the threshold between two adjacent range tombstones.
  * See those classes for more details.
  */
 public interface ClusteringPrefix extends IMeasurableMemory, Clusterable
@@ -51,7 +51,7 @@
      * The kind of clustering prefix this actually is.
      *
      * The kind {@code STATIC_CLUSTERING} is only implemented by {@link Clustering#STATIC_CLUSTERING} and {@code CLUSTERING} is
-     * implemented by the {@link Clustering} class. The rest is used by {@link Slice.Bound} and {@link RangeTombstone.Bound}.
+     * implemented by the {@link Clustering} class. The rest is used by {@link ClusteringBound} and {@link ClusteringBoundary}.
      */
     public enum Kind
     {
@@ -74,7 +74,7 @@
          */
         public final int comparedToClustering;
 
-        private Kind(int comparison, int comparedToClustering)
+        Kind(int comparison, int comparedToClustering)
         {
             this.comparison = comparison;
             this.comparedToClustering = comparedToClustering;
@@ -122,8 +122,9 @@
                 case EXCL_START_BOUND:
                 case EXCL_END_BOUND:
                     return true;
+                default:
+                    return false;
             }
-            return false;
         }
 
         public boolean isBoundary()
@@ -133,8 +134,9 @@
                 case INCL_END_EXCL_START_BOUNDARY:
                 case EXCL_END_INCL_START_BOUNDARY:
                     return true;
+                default:
+                    return false;
             }
-            return false;
         }
 
         public boolean isStart()
@@ -265,10 +267,21 @@
             }
             else
             {
-                RangeTombstone.Bound.serializer.serialize((RangeTombstone.Bound)clustering, out, version, types);
+                ClusteringBoundOrBoundary.serializer.serialize((ClusteringBoundOrBoundary)clustering, out, version, types);
             }
         }
 
+        public void skip(DataInputPlus in, int version, List<AbstractType<?>> types) throws IOException
+        {
+            Kind kind = Kind.values()[in.readByte()];
+            // We shouldn't serialize static clusterings
+            assert kind != Kind.STATIC_CLUSTERING;
+            if (kind == Kind.CLUSTERING)
+                Clustering.serializer.skip(in, version, types);
+            else
+                ClusteringBoundOrBoundary.serializer.skipValues(in, kind, version, types);
+        }
+
         public ClusteringPrefix deserialize(DataInputPlus in, int version, List<AbstractType<?>> types) throws IOException
         {
             Kind kind = Kind.values()[in.readByte()];
@@ -277,7 +290,7 @@
             if (kind == Kind.CLUSTERING)
                 return Clustering.serializer.deserialize(in, version, types);
             else
-                return RangeTombstone.Bound.serializer.deserializeValues(in, kind, version, types);
+                return ClusteringBoundOrBoundary.serializer.deserializeValues(in, kind, version, types);
         }
 
         public long serializedSize(ClusteringPrefix clustering, int version, List<AbstractType<?>> types)
@@ -287,7 +300,7 @@
             if (clustering.kind() == Kind.CLUSTERING)
                 return 1 + Clustering.serializer.serializedSize((Clustering)clustering, version, types);
             else
-                return RangeTombstone.Bound.serializer.serializedSize((RangeTombstone.Bound)clustering, version, types);
+                return ClusteringBoundOrBoundary.serializer.serializedSize((ClusteringBoundOrBoundary)clustering, version, types);
         }
 
         void serializeValuesWithoutSize(ClusteringPrefix clustering, DataOutputPlus out, int version, List<AbstractType<?>> types) throws IOException
@@ -356,6 +369,24 @@
             return values;
         }
 
+        void skipValuesWithoutSize(DataInputPlus in, int size, int version, List<AbstractType<?>> types) throws IOException
+        {
+            // Callers of this method should handle the case where size = 0 (in all case we want to return a special value anyway).
+            assert size > 0;
+            int offset = 0;
+            while (offset < size)
+            {
+                long header = in.readUnsignedVInt();
+                int limit = Math.min(size, offset + 32);
+                while (offset < limit)
+                {
+                    if (!isNull(header, offset) && !isEmpty(header, offset))
+                         types.get(offset).skipValue(in);
+                    offset++;
+                }
+            }
+        }
+
         /**
          * Whatever the type of a given clustering column is, its value can always be either empty or null. So we at least need to distinguish those
          * 2 values, and because we want to be able to store fixed width values without appending their (fixed) size first, we need a way to encode
@@ -441,9 +472,9 @@
                 this.nextValues = new ByteBuffer[nextSize];
         }
 
-        public int compareNextTo(Slice.Bound bound) throws IOException
+        public int compareNextTo(ClusteringBoundOrBoundary bound) throws IOException
         {
-            if (bound == Slice.Bound.TOP)
+            if (bound == ClusteringBound.TOP)
                 return -1;
 
             for (int i = 0; i < bound.size(); i++)
@@ -495,11 +526,11 @@
                 continue;
         }
 
-        public RangeTombstone.Bound deserializeNextBound() throws IOException
+        public ClusteringBoundOrBoundary deserializeNextBound() throws IOException
         {
             assert !nextIsRow;
             deserializeAll();
-            RangeTombstone.Bound bound = new RangeTombstone.Bound(nextKind, nextValues);
+            ClusteringBoundOrBoundary bound = ClusteringBoundOrBoundary.create(nextKind, nextValues);
             nextValues = null;
             return bound;
         }
@@ -508,7 +539,7 @@
         {
             assert nextIsRow;
             deserializeAll();
-            Clustering clustering = new Clustering(nextValues);
+            Clustering clustering = Clustering.make(nextValues);
             nextValues = null;
             return clustering;
         }
diff --git a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java
index f9f96d8..2b5ad47 100644
--- a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java
+++ b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java
@@ -44,7 +44,7 @@
 import org.apache.cassandra.concurrent.*;
 import org.apache.cassandra.config.*;
 import org.apache.cassandra.db.commitlog.CommitLog;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.apache.cassandra.db.compaction.*;
 import org.apache.cassandra.db.filter.ClusteringIndexFilter;
 import org.apache.cassandra.db.filter.DataLimits;
@@ -56,6 +56,7 @@
 import org.apache.cassandra.dht.*;
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.exceptions.StartupException;
 import org.apache.cassandra.index.SecondaryIndexManager;
 import org.apache.cassandra.index.internal.CassandraIndex;
 import org.apache.cassandra.index.transactions.UpdateTransaction;
@@ -72,6 +73,7 @@
 import org.apache.cassandra.metrics.TableMetrics;
 import org.apache.cassandra.metrics.TableMetrics.Sampler;
 import org.apache.cassandra.schema.*;
+import org.apache.cassandra.schema.CompactionParams.TombstoneOption;
 import org.apache.cassandra.service.CacheService;
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.utils.*;
@@ -127,6 +129,14 @@
 
     private static final Logger logger = LoggerFactory.getLogger(ColumnFamilyStore.class);
 
+    /*
+    We keep a pool of threads for each data directory, size of each pool is memtable_flush_writers.
+    When flushing we start a Flush runnable in the flushExecutor. Flush calculates how to split the
+    memtable ranges over the existing data directories and creates a FlushRunnable for each of the directories.
+    The FlushRunnables are executed in the perDiskflushExecutors and the Flush will block until all FlushRunnables
+    are finished. By having flushExecutor size the same size as each of the perDiskflushExecutors we make sure we can
+    have that many flushes going at the same time.
+    */
     private static final ThreadPoolExecutor flushExecutor = new JMXEnabledThreadPoolExecutor(DatabaseDescriptor.getFlushWriters(),
                                                                                              StageManager.KEEPALIVE,
                                                                                              TimeUnit.SECONDS,
@@ -134,6 +144,20 @@
                                                                                              new NamedThreadFactory("MemtableFlushWriter"),
                                                                                              "internal");
 
+    private static final ExecutorService [] perDiskflushExecutors = new ExecutorService[DatabaseDescriptor.getAllDataFileLocations().length];
+    static
+    {
+        for (int i = 0; i < DatabaseDescriptor.getAllDataFileLocations().length; i++)
+        {
+            perDiskflushExecutors[i] = new JMXEnabledThreadPoolExecutor(DatabaseDescriptor.getFlushWriters(),
+                                                                        StageManager.KEEPALIVE,
+                                                                        TimeUnit.SECONDS,
+                                                                        new LinkedBlockingQueue<Runnable>(),
+                                                                        new NamedThreadFactory("PerDiskMemtableFlushWriter_"+i),
+                                                                        "internal");
+        }
+    }
+
     // post-flush executor is single threaded to provide guarantee that any flush Future on a CF will never return until prior flushes have completed
     private static final ThreadPoolExecutor postFlushExecutor = new JMXEnabledThreadPoolExecutor(1,
                                                                                                  StageManager.KEEPALIVE,
@@ -166,6 +190,9 @@
     private static final String SAMPLING_RESULTS_NAME = "SAMPLING_RESULTS";
     private static final CompositeType SAMPLING_RESULT;
 
+    public static final String SNAPSHOT_TRUNCATE_PREFIX = "truncated";
+    public static final String SNAPSHOT_DROP_PREFIX = "dropped";
+
     static
     {
         try
@@ -224,6 +251,15 @@
 
     private volatile boolean compactionSpaceCheck = true;
 
+    @VisibleForTesting
+    final DiskBoundaryManager diskBoundaryManager = new DiskBoundaryManager();
+
+    public static void shutdownFlushExecutor() throws InterruptedException
+    {
+        flushExecutor.shutdown();
+        flushExecutor.awaitTermination(60, TimeUnit.SECONDS);
+    }
+
     public static void shutdownPostFlushExecutor() throws InterruptedException
     {
         postFlushExecutor.shutdown();
@@ -232,7 +268,10 @@
 
     public static void shutdownExecutorsAndWait(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
     {
-        ExecutorUtils.shutdownAndWait(timeout, unit, reclaimExecutor, postFlushExecutor, flushExecutor);
+        List<ExecutorService> executors = new ArrayList<>(perDiskflushExecutors.length + 3);
+        Collections.addAll(executors, reclaimExecutor, postFlushExecutor, flushExecutor);
+        Collections.addAll(executors, perDiskflushExecutors);
+        ExecutorUtils.shutdownAndWait(timeout, unit, executors);
     }
 
     public void reload()
@@ -366,17 +405,6 @@
         return FBUtilities.json(getCompressionParameters());
     }
 
-    private ColumnFamilyStore(Keyspace keyspace,
-                              String columnFamilyName,
-                              int generation,
-                              CFMetaData metadata,
-                              Directories directories,
-                              boolean loadSSTables)
-    {
-        this(keyspace, columnFamilyName, generation, metadata, directories, loadSSTables, true);
-    }
-
-
     @VisibleForTesting
     public ColumnFamilyStore(Keyspace keyspace,
                              String columnFamilyName,
@@ -384,7 +412,8 @@
                              CFMetaData metadata,
                              Directories directories,
                              boolean loadSSTables,
-                             boolean registerBookkeeping)
+                             boolean registerBookeeping,
+                             boolean offline)
     {
         assert directories != null;
         assert metadata != null : "null metadata for " + keyspace + ':' + columnFamilyName;
@@ -405,7 +434,7 @@
         // Create Memtable only on online
         Memtable initialMemtable = null;
         if (DatabaseDescriptor.isDaemonInitialized())
-            initialMemtable = new Memtable(new AtomicReference<>(CommitLog.instance.getContext()), this);
+            initialMemtable = new Memtable(new AtomicReference<>(CommitLog.instance.getCurrentPosition()), this);
         data = new Tracker(initialMemtable, loadSSTables);
 
         // Note that this needs to happen before we load the first sstables, or the global sstable tracker will not
@@ -413,7 +442,6 @@
         data.subscribe(StorageService.instance.sstablesTracker);
 
         Collection<SSTableReader> sstables = null;
-
         // scan for sstables corresponding to this cf and load them
         if (data.loadsstables)
         {
@@ -422,8 +450,20 @@
             data.addInitialSSTablesWithoutUpdatingSize(sstables);
         }
 
+        /**
+         * When creating a CFS offline we change the default logic needed by CASSANDRA-8671
+         * and link the passed directories to be picked up by the compaction strategy
+         */
+        if (offline)
+            this.directories = directories;
+        else
+            this.directories = new Directories(metadata, Directories.dataDirectories);
+
+
         // compaction strategy should be created after the CFS has been prepared
         compactionStrategyManager = new CompactionStrategyManager(this);
+
+        // Since compaction can re-define data dir we need to reinit directories
         this.directories = compactionStrategyManager.getDirectories();
 
         if (maxCompactionThreshold.value() <= 0 || minCompactionThreshold.value() <=0)
@@ -443,7 +483,7 @@
             data.updateInitialSSTableSize(sstables);
         }
 
-        if (registerBookkeeping)
+        if (registerBookeeping)
         {
             // register the mbean
             mbeanName = String.format("org.apache.cassandra.db:type=%s,keyspace=%s,table=%s",
@@ -503,7 +543,7 @@
 
     public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, long keyCount, long repairedAt, MetadataCollector metadataCollector, SerializationHeader header, LifecycleNewTracker lifecycleNewTracker)
     {
-        return getCompactionStrategyManager().createSSTableMultiWriter(descriptor, keyCount, repairedAt, metadataCollector, header, lifecycleNewTracker);
+        return getCompactionStrategyManager().createSSTableMultiWriter(descriptor, keyCount, repairedAt, metadataCollector, header, indexManager.listIndexes(), lifecycleNewTracker);
     }
 
     public boolean supportsEarlyOpen()
@@ -583,8 +623,20 @@
                                                                          CFMetaData metadata,
                                                                          boolean loadSSTables)
     {
-        // get the max generation number, to prevent generation conflicts
         Directories directories = new Directories(metadata, initialDirectories);
+        return createColumnFamilyStore(keyspace, columnFamily, metadata, directories, loadSSTables, true, false);
+    }
+
+    /** This is only directly used by offline tools */
+    public static synchronized ColumnFamilyStore createColumnFamilyStore(Keyspace keyspace,
+                                                                         String columnFamily,
+                                                                         CFMetaData metadata,
+                                                                         Directories directories,
+                                                                         boolean loadSSTables,
+                                                                         boolean registerBookkeeping,
+                                                                         boolean offline)
+    {
+        // get the max generation number, to prevent generation conflicts
         Directories.SSTableLister lister = directories.sstableLister(Directories.OnTxnErr.IGNORE).includeBackups(true);
         List<Integer> generations = new ArrayList<>();
         for (Map.Entry<Descriptor, Set<Component>> entry : lister.list().entrySet())
@@ -598,14 +650,14 @@
         Collections.sort(generations);
         int value = (generations.size() > 0) ? (generations.get(generations.size() - 1)) : 0;
 
-        return new ColumnFamilyStore(keyspace, columnFamily, value, metadata, directories, loadSSTables);
+        return new ColumnFamilyStore(keyspace, columnFamily, value, metadata, directories, loadSSTables, registerBookkeeping, offline);
     }
 
     /**
      * Removes unnecessary files from the cf directory at startup: these include temp files, orphans, zero-length files
      * and compacted sstables. Files that cannot be recognized will be ignored.
      */
-    public static void scrubDataDirectories(CFMetaData metadata)
+    public static void  scrubDataDirectories(CFMetaData metadata) throws StartupException
     {
         Directories directories = new Directories(metadata, initialDirectories);
         Set<File> cleanedDirectories = new HashSet<>();
@@ -616,7 +668,12 @@
         directories.removeTemporaryDirectories();
 
         logger.trace("Removing temporary or obsoleted files from unfinished operations for table {}", metadata.cfName);
-        LifecycleTransaction.removeUnfinishedLeftovers(metadata);
+        if (!LifecycleTransaction.removeUnfinishedLeftovers(metadata))
+            throw new StartupException(StartupException.ERR_WRONG_DISK_STATE,
+                                       String.format("Cannot remove temporary or obsoleted files for %s.%s due to a problem with transaction " +
+                                                     "log files. Please check records with problems in the log messages above and fix them. " +
+                                                     "Refer to the 3.0 upgrading instructions in NEWS.txt " +
+                                                     "for a description of transaction log files.", metadata.ksName, metadata.cfName));
 
         logger.trace("Further extra check for orphan sstable files for {}", metadata.cfName);
         for (Map.Entry<Descriptor,Set<Component>> sstableFiles : directories.sstableLister(Directories.OnTxnErr.IGNORE).list().entrySet())
@@ -813,7 +870,7 @@
 
     public String getSSTablePath(File directory)
     {
-        return getSSTablePath(directory, DatabaseDescriptor.getSSTableFormat().info.getLatestVersion(), DatabaseDescriptor.getSSTableFormat());
+        return getSSTablePath(directory, SSTableFormat.Type.current().info.getLatestVersion(), SSTableFormat.Type.current());
     }
 
     public String getSSTablePath(File directory, SSTableFormat.Type format)
@@ -838,7 +895,7 @@
      *
      * @param memtable
      */
-    public ListenableFuture<ReplayPosition> switchMemtableIfCurrent(Memtable memtable)
+    public ListenableFuture<CommitLogPosition> switchMemtableIfCurrent(Memtable memtable)
     {
         synchronized (data)
         {
@@ -856,16 +913,15 @@
      * not complete until the Memtable (and all prior Memtables) have been successfully flushed, and the CL
      * marked clean up to the position owned by the Memtable.
      */
-    public ListenableFuture<ReplayPosition> switchMemtable()
+    public ListenableFuture<CommitLogPosition> switchMemtable()
     {
         synchronized (data)
         {
             logFlush();
             Flush flush = new Flush(false);
             flushExecutor.execute(flush);
-            ListenableFutureTask<ReplayPosition> task = ListenableFutureTask.create(flush.postFlush);
-            postFlushExecutor.execute(task);
-            return task;
+            postFlushExecutor.execute(flush.postFlushTask);
+            return flush.postFlushTask;
         }
     }
 
@@ -890,8 +946,13 @@
             offHeapTotal += allocator.offHeap().owns();
         }
 
-        logger.debug("Enqueuing flush of {}: {}", name, String.format("%d (%.0f%%) on-heap, %d (%.0f%%) off-heap",
-                                                                      onHeapTotal, onHeapRatio * 100, offHeapTotal, offHeapRatio * 100));
+        logger.debug("Enqueuing flush of {}: {}",
+                     name,
+                     String.format("%s (%.0f%%) on-heap, %s (%.0f%%) off-heap",
+                                   FBUtilities.prettyPrintMemory(onHeapTotal),
+                                   onHeapRatio * 100,
+                                   FBUtilities.prettyPrintMemory(offHeapTotal),
+                                   offHeapRatio * 100));
     }
 
 
@@ -901,7 +962,7 @@
      * @return a Future yielding the commit log position that can be guaranteed to have been successfully written
      *         to sstables for this table once the future completes
      */
-    public ListenableFuture<ReplayPosition> forceFlush()
+    public ListenableFuture<CommitLogPosition> forceFlush()
     {
         synchronized (data)
         {
@@ -920,7 +981,7 @@
      * @return a Future yielding the commit log position that can be guaranteed to have been successfully written
      *         to sstables for this table once the future completes
      */
-    public ListenableFuture<ReplayPosition> forceFlush(ReplayPosition flushIfDirtyBefore)
+    public ListenableFuture<?> forceFlush(CommitLogPosition flushIfDirtyBefore)
     {
         // we don't loop through the remaining memtables since here we only care about commit log dirtiness
         // and this does not vary between a table and its table-backed indexes
@@ -934,12 +995,12 @@
      * @return a Future yielding the commit log position that can be guaranteed to have been successfully written
      *         to sstables for this table once the future completes
      */
-    private ListenableFuture<ReplayPosition> waitForFlushes()
+    private ListenableFuture<CommitLogPosition> waitForFlushes()
     {
         // we grab the current memtable; once any preceding memtables have flushed, we know its
         // commitLogLowerBound has been set (as this it is set with the upper bound of the preceding memtable)
         final Memtable current = data.getView().getCurrentMemtable();
-        ListenableFutureTask<ReplayPosition> task = ListenableFutureTask.create(() -> {
+        ListenableFutureTask<CommitLogPosition> task = ListenableFutureTask.create(() -> {
             logger.debug("forceFlush requested but everything is clean in {}", name);
             return current.getCommitLogLowerBound();
         });
@@ -947,7 +1008,7 @@
         return task;
     }
 
-    public ReplayPosition forceBlockingFlush()
+    public CommitLogPosition forceBlockingFlush()
     {
         return FBUtilities.waitOnFuture(forceFlush());
     }
@@ -956,18 +1017,18 @@
      * Both synchronises custom secondary indexes and provides ordering guarantees for futures on switchMemtable/flush
      * etc, which expect to be able to wait until the flush (and all prior flushes) requested have completed.
      */
-    private final class PostFlush implements Callable<ReplayPosition>
+    private final class PostFlush implements Callable<CommitLogPosition>
     {
         final CountDownLatch latch = new CountDownLatch(1);
-        volatile Throwable flushFailure = null;
         final List<Memtable> memtables;
+        volatile Throwable flushFailure = null;
 
         private PostFlush(List<Memtable> memtables)
         {
             this.memtables = memtables;
         }
 
-        public ReplayPosition call()
+        public CommitLogPosition call()
         {
             try
             {
@@ -980,7 +1041,7 @@
                 throw new IllegalStateException();
             }
 
-            ReplayPosition commitLogUpperBound = ReplayPosition.NONE;
+            CommitLogPosition commitLogUpperBound = CommitLogPosition.NONE;
             // If a flush errored out but the error was ignored, make sure we don't discard the commit log.
             if (flushFailure == null && !memtables.isEmpty())
             {
@@ -992,7 +1053,7 @@
             metric.pendingFlushes.dec();
 
             if (flushFailure != null)
-                Throwables.propagate(flushFailure);
+                throw Throwables.propagate(flushFailure);
 
             return commitLogUpperBound;
         }
@@ -1010,6 +1071,7 @@
     {
         final OpOrder.Barrier writeBarrier;
         final List<Memtable> memtables = new ArrayList<>();
+        final ListenableFutureTask<CommitLogPosition> postFlushTask;
         final PostFlush postFlush;
         final boolean truncate;
 
@@ -1033,7 +1095,7 @@
             writeBarrier = Keyspace.writeOrder.newBarrier();
 
             // submit flushes for the memtable for any indexed sub-cfses, and our own
-            AtomicReference<ReplayPosition> commitLogUpperBound = new AtomicReference<>();
+            AtomicReference<CommitLogPosition> commitLogUpperBound = new AtomicReference<>();
             for (ColumnFamilyStore cfs : concatWithIndexes())
             {
                 // switch all memtables, regardless of their dirty status, setting the barrier
@@ -1051,12 +1113,10 @@
 
             // we then issue the barrier; this lets us wait for all operations started prior to the barrier to complete;
             // since this happens after wiring up the commitLogUpperBound, we also know all operations with earlier
-            // replay positions have also completed, i.e. the memtables are done and ready to flush
+            // commit log segment position have also completed, i.e. the memtables are done and ready to flush
             writeBarrier.issue();
             postFlush = new PostFlush(memtables);
-
-            if (logger.isTraceEnabled())
-                logger.trace("Created flush task {}@{}", hashCode(), name);
+            postFlushTask = ListenableFutureTask.create(postFlush);
         }
 
         public void run()
@@ -1082,42 +1142,135 @@
 
             try
             {
-                boolean flushNonCf2i = true;
-                for (Memtable memtable : memtables)
+                // Flush "data" memtable with non-cf 2i first;
+                flushMemtable(memtables.get(0), true);
+                for (int i = 1; i < memtables.size(); i++)
+                    flushMemtable(memtables.get(i), false);
+            }
+            catch (Throwable t)
+            {
+                JVMStabilityInspector.inspectThrowable(t);
+                postFlush.flushFailure = t;
+            }
+
+            if (logger.isTraceEnabled())
+                logger.trace("Flush task {}@{} signaling post flush task", hashCode(), name);
+
+            // signal the post-flush we've done our work
+            postFlush.latch.countDown();
+
+            if (logger.isTraceEnabled())
+                logger.trace("Flush task task {}@{} finished", hashCode(), name);
+        }
+
+        public Collection<SSTableReader> flushMemtable(Memtable memtable, boolean flushNonCf2i)
+        {
+            if (logger.isTraceEnabled())
+                logger.trace("Flush task task {}@{} flushing memtable {}", hashCode(), name, memtable);
+
+            if (memtable.isClean() || truncate)
+            {
+                memtable.cfs.replaceFlushed(memtable, Collections.emptyList());
+                reclaim(memtable);
+                return Collections.emptyList();
+            }
+
+            List<Future<SSTableMultiWriter>> futures = new ArrayList<>();
+            long totalBytesOnDisk = 0;
+            long maxBytesOnDisk = 0;
+            long minBytesOnDisk = Long.MAX_VALUE;
+            List<SSTableReader> sstables = new ArrayList<>();
+            try (LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.FLUSH))
+            {
+                List<Memtable.FlushRunnable> flushRunnables = null;
+                List<SSTableMultiWriter> flushResults = null;
+
+                try
                 {
-                    Collection<SSTableReader> readers = Collections.emptyList();
-                    if (!memtable.isClean() && !truncate)
+                    // flush the memtable
+                    flushRunnables = memtable.flushRunnables(txn);
+
+                    for (int i = 0; i < flushRunnables.size(); i++)
+                        futures.add(perDiskflushExecutors[i].submit(flushRunnables.get(i)));
+
+                    /**
+                     * we can flush 2is as soon as the barrier completes, as they will be consistent with (or ahead of) the
+                     * flushed memtables and CL position, which is as good as we can guarantee.
+                     * TODO: SecondaryIndex should support setBarrier(), so custom implementations can co-ordinate exactly
+                     * with CL as we do with memtables/CFS-backed SecondaryIndexes.
+                     */
+                    if (flushNonCf2i)
+                        indexManager.flushAllNonCFSBackedIndexesBlocking();
+
+                    flushResults = Lists.newArrayList(FBUtilities.waitOnFutures(futures));
+                }
+                catch (Throwable t)
+                {
+                    t = memtable.abortRunnables(flushRunnables, t);
+                    t = txn.abort(t);
+                    throw Throwables.propagate(t);
+                }
+
+                try
+                {
+                    Iterator<SSTableMultiWriter> writerIterator = flushResults.iterator();
+                    while (writerIterator.hasNext())
                     {
-                        // TODO: SecondaryIndex should support setBarrier(), so custom implementations can co-ordinate exactly
-                        // with CL as we do with memtables/CFS-backed SecondaryIndexes.
-                        if (flushNonCf2i)
+                        @SuppressWarnings("resource")
+                        SSTableMultiWriter writer = writerIterator.next();
+                        if (writer.getFilePointer() > 0)
                         {
-                            indexManager.flushAllNonCFSBackedIndexesBlocking();
-                            flushNonCf2i = false;
+                            writer.setOpenResult(true).prepareToCommit();
                         }
-                        readers = memtable.flush();
+                        else
+                        {
+                            maybeFail(writer.abort(null));
+                            writerIterator.remove();
+                        }
                     }
-                    memtable.cfs.replaceFlushed(memtable, readers);
-                    reclaim(memtable);
+                }
+                catch (Throwable t)
+                {
+                    for (SSTableMultiWriter writer : flushResults)
+                        t = writer.abort(t);
+                    t = txn.abort(t);
+                    Throwables.propagate(t);
+                }
+
+                txn.prepareToCommit();
+
+                Throwable accumulate = null;
+                for (SSTableMultiWriter writer : flushResults)
+                    accumulate = writer.commit(accumulate);
+
+                maybeFail(txn.commit(accumulate));
+
+                for (SSTableMultiWriter writer : flushResults)
+                {
+                    Collection<SSTableReader> flushedSSTables = writer.finished();
+                    for (SSTableReader sstable : flushedSSTables)
+                    {
+                        if (sstable != null)
+                        {
+                            sstables.add(sstable);
+                            long size = sstable.bytesOnDisk();
+                            totalBytesOnDisk += size;
+                            maxBytesOnDisk = Math.max(maxBytesOnDisk, size);
+                            minBytesOnDisk = Math.min(minBytesOnDisk, size);
+                        }
+                    }
                 }
             }
-            catch (Throwable e)
-            {
-                JVMStabilityInspector.inspectThrowable(e);
-                // If we weren't killed, try to continue work but do not allow CommitLog to be discarded.
-                postFlush.flushFailure = e;
-            }
-            finally
-            {
-                if (logger.isTraceEnabled())
-                    logger.trace("Flush task {}@{} signaling post flush task", hashCode(), name);
-
-                // signal the post-flush we've done our work
-                postFlush.latch.countDown();
-
-                if (logger.isTraceEnabled())
-                    logger.trace("Flush task task {}@{} finished", hashCode(), name);
-            }
+            memtable.cfs.replaceFlushed(memtable, sstables);
+            reclaim(memtable);
+            memtable.cfs.compactionStrategyManager.compactionLogger.flush(sstables);
+            logger.debug("Flushed to {} ({} sstables, {}), biggest {}, smallest {}",
+                         sstables,
+                         sstables.size(),
+                         FBUtilities.prettyPrintMemory(totalBytesOnDisk),
+                         FBUtilities.prettyPrintMemory(maxBytesOnDisk),
+                         FBUtilities.prettyPrintMemory(minBytesOnDisk));
+            return sstables;
         }
 
         private void reclaim(final Memtable memtable)
@@ -1125,28 +1278,28 @@
             // issue a read barrier for reclaiming the memory, and offload the wait to another thread
             final OpOrder.Barrier readBarrier = readOrdering.newBarrier();
             readBarrier.issue();
-            reclaimExecutor.execute(new WrappedRunnable()
+            postFlushTask.addListener(new WrappedRunnable()
             {
                 public void runMayThrow()
                 {
                     readBarrier.await();
                     memtable.setDiscarded();
                 }
-            });
+            }, reclaimExecutor);
         }
     }
 
     // atomically set the upper bound for the commit log
-    private static void setCommitLogUpperBound(AtomicReference<ReplayPosition> commitLogUpperBound)
+    private static void setCommitLogUpperBound(AtomicReference<CommitLogPosition> commitLogUpperBound)
     {
         // we attempt to set the holder to the current commit log context. at the same time all writes to the memtables are
         // also maintaining this value, so if somebody sneaks ahead of us somehow (should be rare) we simply retry,
         // so that we know all operations prior to the position have not reached it yet
-        ReplayPosition lastReplayPosition;
+        CommitLogPosition lastReplayPosition;
         while (true)
         {
-            lastReplayPosition = new Memtable.LastReplayPosition(CommitLog.instance.getContext());
-            ReplayPosition currentLast = commitLogUpperBound.get();
+            lastReplayPosition = new Memtable.LastCommitLogPosition((CommitLog.instance.getCurrentPosition()));
+            CommitLogPosition currentLast = commitLogUpperBound.get();
             if ((currentLast == null || currentLast.compareTo(lastReplayPosition) <= 0)
                 && commitLogUpperBound.compareAndSet(currentLast, lastReplayPosition))
                 break;
@@ -1207,7 +1360,7 @@
                          largest.cfs, ratio(usedOnHeap, usedOffHeap), ratio(liveOnHeap, liveOffHeap),
                          ratio(flushingOnHeap, flushingOffHeap), ratio(thisOnHeap, thisOffHeap));
 
-            ListenableFuture<ReplayPosition> flushFuture = largest.cfs.switchMemtableIfCurrent(largest);
+            ListenableFuture<CommitLogPosition> flushFuture = largest.cfs.switchMemtableIfCurrent(largest);
             flushFuture.addListener(() -> {
                 try
                 {
@@ -1235,15 +1388,6 @@
         return String.format("%.2f/%.2f", onHeap, offHeap);
     }
 
-    public void maybeUpdateRowCache(DecoratedKey key)
-    {
-        if (!isRowCacheEnabled())
-            return;
-
-        RowCacheKey cacheKey = new RowCacheKey(metadata.ksAndCFName, key);
-        invalidateCachedPartition(cacheKey);
-    }
-
     /**
      * Insert/Update the column family for this key.
      * Caller is responsible for acquiring Keyspace.switchLock
@@ -1251,17 +1395,18 @@
      * param @ key - key for update/insert
      * param @ columnFamily - columnFamily changes
      */
-    public void apply(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup, ReplayPosition replayPosition)
+    public void apply(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup, CommitLogPosition commitLogPosition)
 
     {
         long start = System.nanoTime();
-        Memtable mt = data.getMemtableFor(opGroup, replayPosition);
         try
         {
+            Memtable mt = data.getMemtableFor(opGroup, commitLogPosition);
             long timeDelta = mt.put(update, indexer, opGroup);
             DecoratedKey key = update.partitionKey();
-            maybeUpdateRowCache(key);
+            invalidateCachedPartition(key);
             metric.samplers.get(Sampler.WRITES).addSample(key.getKey(), key.hashCode(), 1);
+            StorageHook.instance.reportWrite(metadata.cfId, update);
             metric.writeLatency.addNano(System.nanoTime() - start);
             // CASSANDRA-11117 - certain resolution paths on memtable put can result in very
             // large time deltas, either through a variety of sentinel timestamps (used for empty values, ensuring
@@ -1277,7 +1422,6 @@
                                        + " for ks: "
                                        + keyspace.getName() + ", table: " + name, e);
         }
-
     }
 
     /**
@@ -1495,6 +1639,16 @@
         return CompactionManager.instance.performSSTableRewrite(ColumnFamilyStore.this, excludeCurrentVersion, jobs);
     }
 
+    public CompactionManager.AllSSTableOpStatus relocateSSTables(int jobs) throws ExecutionException, InterruptedException
+    {
+        return CompactionManager.instance.relocateSSTables(this, jobs);
+    }
+
+    public CompactionManager.AllSSTableOpStatus garbageCollect(TombstoneOption tombstoneOption, int jobs) throws ExecutionException, InterruptedException
+    {
+        return CompactionManager.instance.performGarbageCollection(this, tombstoneOption, jobs);
+    }
+
     public void markObsolete(Collection<SSTableReader> sstables, OperationType compactionType)
     {
         assert !sstables.isEmpty();
@@ -1601,7 +1755,13 @@
     // WARNING: this returns the set of LIVE sstables only, which may be only partially written
     public List<String> getSSTablesForKey(String key)
     {
-        DecoratedKey dk = decorateKey(metadata.getKeyValidator().fromString(key));
+        return getSSTablesForKey(key, false);
+    }
+
+    public List<String> getSSTablesForKey(String key, boolean hexFormat)
+    {
+        ByteBuffer keyBuffer = hexFormat ? ByteBufferUtil.hexToBytes(key) : metadata.getKeyValidator().fromString(key);
+        DecoratedKey dk = decorateKey(keyBuffer);
         try (OpOrder.Group op = readOrdering.start())
         {
             List<String> files = new ArrayList<>();
@@ -1710,9 +1870,9 @@
                 }
             }
         }
-        writeSnapshotManifest(filesJSONArr, snapshotName);
 
-        if (!Schema.isLocalSystemKeyspace(metadata.ksName) && !Schema.isReplicatedSystemKeyspace(metadata.ksName))
+        writeSnapshotManifest(filesJSONArr, snapshotName);
+        if (!SchemaConstants.isLocalSystemKeyspace(metadata.ksName) && !SchemaConstants.isReplicatedSystemKeyspace(metadata.ksName))
             writeSnapshotSchema(snapshotName);
 
         if (ephemeral)
@@ -1840,16 +2000,31 @@
      */
     public Set<SSTableReader> snapshot(String snapshotName)
     {
-        return snapshot(snapshotName, null, false);
+        return snapshot(snapshotName, false);
+    }
+
+    /**
+     * Take a snap shot of this columnfamily store.
+     *
+     * @param snapshotName the name of the associated with the snapshot
+     * @param skipFlush Skip blocking flush of memtable
+     */
+    public Set<SSTableReader> snapshot(String snapshotName, boolean skipFlush)
+    {
+        return snapshot(snapshotName, null, false, skipFlush);
     }
 
 
     /**
      * @param ephemeral If this flag is set to true, the snapshot will be cleaned up during next startup
+     * @param skipFlush Skip blocking flush of memtable
      */
-    public Set<SSTableReader> snapshot(String snapshotName, Predicate<SSTableReader> predicate, boolean ephemeral)
+    public Set<SSTableReader> snapshot(String snapshotName, Predicate<SSTableReader> predicate, boolean ephemeral, boolean skipFlush)
     {
-        forceBlockingFlush();
+        if (!skipFlush)
+        {
+            forceBlockingFlush();
+        }
         return snapshotWithoutFlush(snapshotName, predicate, ephemeral);
     }
 
@@ -1957,8 +2132,8 @@
 
     public void invalidateCachedPartition(DecoratedKey key)
     {
-        if (!Schema.instance.hasCF(metadata.ksAndCFName))
-            return; //2i don't cache rows
+        if (!isRowCacheEnabled())
+            return;
 
         invalidateCachedPartition(new RowCacheKey(metadata.ksAndCFName, key));
     }
@@ -1982,12 +2157,16 @@
         forceMajorCompaction(false);
     }
 
-
     public void forceMajorCompaction(boolean splitOutput)
-    {
+   {
         CompactionManager.instance.performMaximal(this, splitOutput);
     }
 
+    public void forceCompactionForTokenRange(Collection<Range<Token>> tokenRanges) throws ExecutionException, InterruptedException
+    {
+        CompactionManager.instance.forceCompactionForTokenRange(this, tokenRanges);
+    }
+
     public static Iterable<ColumnFamilyStore> all()
     {
         List<Iterable<ColumnFamilyStore>> stores = new ArrayList<>(Schema.instance.getKeyspaces().size());
@@ -2034,7 +2213,7 @@
         for (final ColumnFamilyStore cfs : concatWithIndexes())
         {
             cfs.runWithCompactionsDisabled((Callable<Void>) () -> {
-                cfs.data.reset(new Memtable(new AtomicReference<>(ReplayPosition.NONE), cfs));
+                cfs.data.reset(new Memtable(new AtomicReference<>(CommitLogPosition.NONE), cfs));
                 return null;
             }, true, false);
         }
@@ -2063,17 +2242,17 @@
         // recording the timestamp IN BETWEEN those actions. Any sstables created
         // with this timestamp or greater time, will not be marked for delete.
         //
-        // Bonus complication: since we store replay position in sstable metadata,
+        // Bonus complication: since we store commit log segment position in sstable metadata,
         // truncating those sstables means we will replay any CL segments from the
         // beginning if we restart before they [the CL segments] are discarded for
         // normal reasons post-truncate.  To prevent this, we store truncation
         // position in the System keyspace.
-        logger.trace("truncating {}", name);
+        logger.info("Truncating {}.{}", keyspace.getName(), name);
 
         viewManager.stopBuild();
 
         final long truncatedAt;
-        final ReplayPosition replayAfter;
+        final CommitLogPosition replayAfter;
 
         if (!noSnapshot && (keyspace.getMetadata().params.durableWrites || DatabaseDescriptor.isAutoSnapshot()))
         {
@@ -2106,7 +2285,7 @@
             data.notifyTruncated(truncatedAt);
 
             if (!noSnapshot && DatabaseDescriptor.isAutoSnapshot())
-                snapshot(Keyspace.getTimestampedSnapshotName(name));
+                snapshot(Keyspace.getTimestampedSnapshotNameWithPrefix(name, SNAPSHOT_TRUNCATE_PREFIX));
 
             discardSSTables(truncatedAt);
 
@@ -2122,19 +2301,20 @@
 
         viewManager.build();
 
-        logger.trace("truncate complete");
+        logger.info("Truncate of {}.{} is complete", keyspace.getName(), name);
     }
 
     /**
      * Drops current memtable without flushing to disk. This should only be called when truncating a column family which is not durable.
      */
-    public Future<ReplayPosition> dumpMemtable()
+    public Future<CommitLogPosition> dumpMemtable()
     {
         synchronized (data)
         {
             final Flush flush = new Flush(true);
             flushExecutor.execute(flush);
-            return postFlushExecutor.submit(flush.postFlush);
+            postFlushExecutor.execute(flush.postFlushTask);
+            return flush.postFlushTask;
         }
     }
 
@@ -2406,7 +2586,7 @@
     public Iterable<ColumnFamilyStore> concatWithIndexes()
     {
         // we return the main CFS first, which we rely on for simplicity in switchMemtable(), for getting the
-        // latest replay position
+        // latest commit log segment position
         return Iterables.concat(Collections.singleton(this), indexManager.getAllIndexColumnFamilyStores());
     }
 
@@ -2417,7 +2597,7 @@
 
     public int getUnleveledSSTables()
     {
-        return this.compactionStrategyManager.getUnleveledSSTables();
+        return compactionStrategyManager.getUnleveledSSTables();
     }
 
     public int[] getSSTableCountPerLevel()
@@ -2425,6 +2605,11 @@
         return compactionStrategyManager.getSSTableCountPerLevel();
     }
 
+    public int getLevelFanoutSize()
+    {
+        return compactionStrategyManager.getLevelFanoutSize();
+    }
+
     public static class ViewFragment
     {
         public final List<SSTableReader> sstables;
@@ -2571,4 +2756,14 @@
     {
         return Objects.requireNonNull(getIfExists(tableId)).metric;
     }
+
+    public DiskBoundaries getDiskBoundaries()
+    {
+        return diskBoundaryManager.getDiskBoundaries(this);
+    }
+
+    public void invalidateDiskBoundaries()
+    {
+        diskBoundaryManager.invalidate();
+    }
 }
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/db/ColumnFamilyStoreCQLHelper.java b/src/java/org/apache/cassandra/db/ColumnFamilyStoreCQLHelper.java
index f310f59..b452b0a 100644
--- a/src/java/org/apache/cassandra/db/ColumnFamilyStoreCQLHelper.java
+++ b/src/java/org/apache/cassandra/db/ColumnFamilyStoreCQLHelper.java
@@ -294,18 +294,18 @@
     @VisibleForTesting
     public static List<String> getUserTypesAsCQL(CFMetaData metadata)
     {
-        List<UserType> types = new ArrayList<>();
-        Set<UserType> typeSet = new HashSet<>();
+        List<AbstractType> types = new ArrayList<>();
+        Set<AbstractType> typeSet = new HashSet<>();
         for (ColumnDefinition cd: Iterables.concat(metadata.partitionKeyColumns(), metadata.clusteringColumns(), metadata.partitionColumns()))
         {
             AbstractType type = cd.type;
-            if (type instanceof UserType)
+            if (type.isUDT())
                 resolveUserType((UserType) type, typeSet, types);
         }
 
         List<String> typeStrings = new ArrayList<>();
-        for (UserType type: types)
-            typeStrings.add(toCQL(type));
+        for (AbstractType type: types)
+            typeStrings.add(toCQL((UserType) type));
         return typeStrings;
     }
 
@@ -406,13 +406,14 @@
         builder.append("\n\tAND caching = ").append(toCQL(tableParams.caching.asMap()));
         builder.append("\n\tAND compaction = ").append(toCQL(tableParams.compaction.asMap()));
         builder.append("\n\tAND compression = ").append(toCQL(tableParams.compression.asMap()));
+        builder.append("\n\tAND cdc = ").append(tableParams.cdc);
 
         builder.append("\n\tAND extensions = { ");
         for (Map.Entry<String, ByteBuffer> entry : tableParams.extensions.entrySet())
         {
             builder.append(singleQuote(entry.getKey()));
             builder.append(": ");
-            builder.append("0x" + ByteBufferUtil.bytesToHex(entry.getValue()));
+            builder.append("0x").append(ByteBufferUtil.bytesToHex(entry.getValue()));
         }
         builder.append(" }");
         return builder.toString();
@@ -470,17 +471,16 @@
                              droppedColumn.droppedTime);
     }
 
-    private static void resolveUserType(UserType type, Set<UserType> typeSet, List<UserType> types)
+    private static void resolveUserType(UserType type, Set<AbstractType> typeSet, List<AbstractType> types)
     {
         for (AbstractType subType: type.fieldTypes())
-            if (!typeSet.contains(subType) && subType instanceof UserType)
+            if (!typeSet.contains(subType) && subType.isUDT())
                 resolveUserType((UserType) subType, typeSet, types);
 
         if (!typeSet.contains(type))
         {
-            UserType t = type;
-            typeSet.add(t);
-            types.add(t);
+            typeSet.add(type);
+            types.add(type);
         }
     }
 
diff --git a/src/java/org/apache/cassandra/db/ColumnFamilyStoreMBean.java b/src/java/org/apache/cassandra/db/ColumnFamilyStoreMBean.java
index 8716b5c..07201c5 100644
--- a/src/java/org/apache/cassandra/db/ColumnFamilyStoreMBean.java
+++ b/src/java/org/apache/cassandra/db/ColumnFamilyStoreMBean.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.db;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
@@ -24,6 +25,9 @@
 import javax.management.openmbean.CompositeData;
 import javax.management.openmbean.OpenDataException;
 
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.dht.Token;
+
 /**
  * The MBean interface for ColumnFamilyStore
  */
@@ -45,6 +49,15 @@
     public void forceMajorCompaction(boolean splitOutput) throws ExecutionException, InterruptedException;
 
     /**
+     * Forces a major compaction of specified token ranges in this column family.
+     * <p>
+     * The token ranges will be interpreted as closed intervals to match the closed interval defined by the first and
+     * last keys of a sstable, even though the {@link Range} class is suppossed to be half-open by definition.
+     *
+     * @param tokenRanges The token ranges to be compacted, interpreted as closed intervals.
+     */
+    public void forceCompactionForTokenRange(Collection<Range<Token>> tokenRanges) throws ExecutionException, InterruptedException;
+    /**
      * Gets the minimum number of sstables in queue before compaction kicks off
      */
     public int getMinimumCompactionThreshold();
@@ -126,6 +139,14 @@
     public List<String> getSSTablesForKey(String key);
 
     /**
+     * Returns a list of filenames that contain the given key on this node
+     * @param key
+     * @param hexFormat if key is in hex string format
+     * @return list of filenames containing the key
+     */
+    public List<String> getSSTablesForKey(String key, boolean hexFormat);
+
+    /**
      * Scan through Keyspace/ColumnFamily's data directory
      * determine which SSTables should be loaded and load them
      */
@@ -143,6 +164,11 @@
     public int[] getSSTableCountPerLevel();
 
     /**
+     * @return sstable fanout size for level compaction strategy.
+     */
+    public int getLevelFanoutSize();
+
+    /**
      * Get the ratio of droppable tombstones to real columns (and non-droppable tombstones)
      * @return ratio
      */
diff --git a/src/java/org/apache/cassandra/db/ColumnIndex.java b/src/java/org/apache/cassandra/db/ColumnIndex.java
index ede3f79..de1b1df 100644
--- a/src/java/org/apache/cassandra/db/ColumnIndex.java
+++ b/src/java/org/apache/cassandra/db/ColumnIndex.java
@@ -15,164 +15,282 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.cassandra.db;
 
 import java.io.IOException;
+import java.nio.ByteBuffer;
 import java.util.*;
 
-import com.google.common.annotations.VisibleForTesting;
+import com.google.common.primitives.Ints;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.rows.*;
-import org.apache.cassandra.io.sstable.IndexHelper;
+import org.apache.cassandra.io.ISerializer;
+import org.apache.cassandra.io.sstable.IndexInfo;
+import org.apache.cassandra.io.sstable.format.SSTableFlushObserver;
 import org.apache.cassandra.io.sstable.format.Version;
+import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.io.util.SequentialWriter;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
+/**
+ * Column index builder used by {@link org.apache.cassandra.io.sstable.format.big.BigTableWriter}.
+ * For index entries that exceed {@link org.apache.cassandra.config.Config#column_index_cache_size_in_kb},
+ * this uses the serialization logic as in {@link RowIndexEntry}.
+ */
 public class ColumnIndex
 {
-    public final long partitionHeaderLength;
-    public final List<IndexHelper.IndexInfo> columnsIndex;
+    // used, if the row-index-entry reaches config column_index_cache_size_in_kb
+    private DataOutputBuffer buffer;
+    // used to track the size of the serialized size of row-index-entry (unused for buffer)
+    private int indexSamplesSerializedSize;
+    // used, until the row-index-entry reaches config column_index_cache_size_in_kb
+    private final List<IndexInfo> indexSamples = new ArrayList<>();
 
-    private static final ColumnIndex EMPTY = new ColumnIndex(-1, Collections.<IndexHelper.IndexInfo>emptyList());
+    private DataOutputBuffer reusableBuffer;
 
-    private ColumnIndex(long partitionHeaderLength, List<IndexHelper.IndexInfo> columnsIndex)
+    public int columnIndexCount;
+    private int[] indexOffsets;
+
+    private final SerializationHeader header;
+    private final int version;
+    private final SequentialWriter writer;
+    private long initialPosition;
+    private final  ISerializer<IndexInfo> idxSerializer;
+    public long headerLength;
+    private long startPosition;
+
+    private int written;
+    private long previousRowStart;
+
+    private ClusteringPrefix firstClustering;
+    private ClusteringPrefix lastClustering;
+
+    private DeletionTime openMarker;
+
+    private final Collection<SSTableFlushObserver> observers;
+
+    public ColumnIndex(SerializationHeader header,
+                        SequentialWriter writer,
+                        Version version,
+                        Collection<SSTableFlushObserver> observers,
+                        ISerializer<IndexInfo> indexInfoSerializer)
     {
-        assert columnsIndex != null;
-
-        this.partitionHeaderLength = partitionHeaderLength;
-        this.columnsIndex = columnsIndex;
+        this.header = header;
+        this.writer = writer;
+        this.version = version.correspondingMessagingVersion();
+        this.observers = observers;
+        this.idxSerializer = indexInfoSerializer;
     }
 
-    public static ColumnIndex writeAndBuildIndex(UnfilteredRowIterator iterator, SequentialWriter output, SerializationHeader header, Version version) throws IOException
+    public void reset()
     {
-        assert !iterator.isEmpty() && version.storeRows();
-
-        Builder builder = new Builder(iterator, output, header, version.correspondingMessagingVersion());
-        return builder.build();
+        this.initialPosition = writer.position();
+        this.headerLength = -1;
+        this.startPosition = -1;
+        this.previousRowStart = 0;
+        this.columnIndexCount = 0;
+        this.written = 0;
+        this.indexSamplesSerializedSize = 0;
+        this.indexSamples.clear();
+        this.firstClustering = null;
+        this.lastClustering = null;
+        this.openMarker = null;
+        if (this.buffer != null)
+            this.reusableBuffer = this.buffer;
+        this.buffer = null;
     }
 
-    @VisibleForTesting
-    public static ColumnIndex nothing()
+    public void buildRowIndex(UnfilteredRowIterator iterator) throws IOException
     {
-        return EMPTY;
+        writePartitionHeader(iterator);
+        this.headerLength = writer.position() - initialPosition;
+
+        while (iterator.hasNext())
+            add(iterator.next());
+
+        finish();
     }
 
-    /**
-     * Help to create an index for a column family based on size of columns,
-     * and write said columns to disk.
-     */
-    private static class Builder
+    private void writePartitionHeader(UnfilteredRowIterator iterator) throws IOException
     {
-        private final UnfilteredRowIterator iterator;
-        private final SequentialWriter writer;
-        private final SerializationHeader header;
-        private final int version;
-
-        private final List<IndexHelper.IndexInfo> columnsIndex = new ArrayList<>();
-        private final long initialPosition;
-        private long headerLength = -1;
-
-        private long startPosition = -1;
-
-        private int written;
-        private long previousRowStart;
-
-        private ClusteringPrefix firstClustering;
-        private ClusteringPrefix lastClustering;
-
-        private DeletionTime openMarker;
-
-        public Builder(UnfilteredRowIterator iterator,
-                       SequentialWriter writer,
-                       SerializationHeader header,
-                       int version)
+        ByteBufferUtil.writeWithShortLength(iterator.partitionKey().getKey(), writer);
+        DeletionTime.serializer.serialize(iterator.partitionLevelDeletion(), writer);
+        if (header.hasStatic())
         {
-            this.iterator = iterator;
-            this.writer = writer;
-            this.header = header;
-            this.version = version;
-            this.initialPosition = writer.position();
+            Row staticRow = iterator.staticRow();
+
+            UnfilteredSerializer.serializer.serializeStaticRow(staticRow, header, writer, version);
+            if (!observers.isEmpty())
+                observers.forEach((o) -> o.nextUnfilteredCluster(staticRow));
+        }
+    }
+
+    private long currentPosition()
+    {
+        return writer.position() - initialPosition;
+    }
+
+    public ByteBuffer buffer()
+    {
+        return buffer != null ? buffer.buffer() : null;
+    }
+
+    public List<IndexInfo> indexSamples()
+    {
+        if (indexSamplesSerializedSize + columnIndexCount * TypeSizes.sizeof(0) <= DatabaseDescriptor.getColumnIndexCacheSize())
+        {
+            return indexSamples;
         }
 
-        private void writePartitionHeader(UnfilteredRowIterator iterator) throws IOException
+        return null;
+    }
+
+    public int[] offsets()
+    {
+        return indexOffsets != null
+               ? Arrays.copyOf(indexOffsets, columnIndexCount)
+               : null;
+    }
+
+    private void addIndexBlock() throws IOException
+    {
+        IndexInfo cIndexInfo = new IndexInfo(firstClustering,
+                                             lastClustering,
+                                             startPosition,
+                                             currentPosition() - startPosition,
+                                             openMarker);
+
+        // indexOffsets is used for both shallow (ShallowIndexedEntry) and non-shallow IndexedEntry.
+        // For shallow ones, we need it to serialize the offsts in finish().
+        // For non-shallow ones, the offsts are passed into IndexedEntry, so we don't have to
+        // calculate the offsets again.
+
+        // indexOffsets contains the offsets of the serialized IndexInfo objects.
+        // I.e. indexOffsets[0] is always 0 so we don't have to deal with a special handling
+        // for index #0 and always subtracting 1 for the index (which could be error-prone).
+        if (indexOffsets == null)
+            indexOffsets = new int[10];
+        else
         {
-            ByteBufferUtil.writeWithShortLength(iterator.partitionKey().getKey(), writer);
-            DeletionTime.serializer.serialize(iterator.partitionLevelDeletion(), writer);
-            if (header.hasStatic())
-                UnfilteredSerializer.serializer.serializeStaticRow(iterator.staticRow(), header, writer, version);
-        }
+            if (columnIndexCount >= indexOffsets.length)
+                indexOffsets = Arrays.copyOf(indexOffsets, indexOffsets.length + 10);
 
-        public ColumnIndex build() throws IOException
-        {
-            writePartitionHeader(iterator);
-            this.headerLength = writer.position() - initialPosition;
-
-            while (iterator.hasNext())
-                add(iterator.next());
-
-            return close();
-        }
-
-        private long currentPosition()
-        {
-            return writer.position() - initialPosition;
-        }
-
-        private void addIndexBlock()
-        {
-            IndexHelper.IndexInfo cIndexInfo = new IndexHelper.IndexInfo(firstClustering,
-                                                                         lastClustering,
-                                                                         startPosition,
-                                                                         currentPosition() - startPosition,
-                                                                         openMarker);
-            columnsIndex.add(cIndexInfo);
-            firstClustering = null;
-        }
-
-        private void add(Unfiltered unfiltered) throws IOException
-        {
-            long pos = currentPosition();
-
-            if (firstClustering == null)
+            //the 0th element is always 0
+            if (columnIndexCount == 0)
             {
-                // Beginning of an index block. Remember the start and position
-                firstClustering = unfiltered.clustering();
-                startPosition = pos;
+                indexOffsets[columnIndexCount] = 0;
             }
-
-            UnfilteredSerializer.serializer.serialize(unfiltered, header, writer, pos - previousRowStart, version);
-            lastClustering = unfiltered.clustering();
-            previousRowStart = pos;
-            ++written;
-
-            if (unfiltered.kind() == Unfiltered.Kind.RANGE_TOMBSTONE_MARKER)
+            else
             {
-                RangeTombstoneMarker marker = (RangeTombstoneMarker)unfiltered;
-                openMarker = marker.isOpen(false) ? marker.openDeletionTime(false) : null;
+                indexOffsets[columnIndexCount] =
+                buffer != null
+                ? Ints.checkedCast(buffer.position())
+                : indexSamplesSerializedSize;
             }
-
-            // if we hit the column index size that we have to index after, go ahead and index it.
-            if (currentPosition() - startPosition >= DatabaseDescriptor.getColumnIndexSize())
-                addIndexBlock();
-
         }
+        columnIndexCount++;
 
-        private ColumnIndex close() throws IOException
+        // First, we collect the IndexInfo objects until we reach Config.column_index_cache_size_in_kb in an ArrayList.
+        // When column_index_cache_size_in_kb is reached, we switch to byte-buffer mode.
+        if (buffer == null)
         {
-            UnfilteredSerializer.serializer.writeEndOfPartition(writer);
-
-            // It's possible we add no rows, just a top level deletion
-            if (written == 0)
-                return ColumnIndex.EMPTY;
-
-            // the last column may have fallen on an index boundary already.  if not, index it explicitly.
-            if (firstClustering != null)
-                addIndexBlock();
-
-            // we should always have at least one computed index block, but we only write it out if there is more than that.
-            assert columnsIndex.size() > 0 && headerLength >= 0;
-            return new ColumnIndex(headerLength, columnsIndex);
+            indexSamplesSerializedSize += idxSerializer.serializedSize(cIndexInfo);
+            if (indexSamplesSerializedSize + columnIndexCount * TypeSizes.sizeof(0) > DatabaseDescriptor.getColumnIndexCacheSize())
+            {
+                buffer = reuseOrAllocateBuffer();
+                for (IndexInfo indexSample : indexSamples)
+                {
+                    idxSerializer.serialize(indexSample, buffer);
+                }
+            }
+            else
+            {
+                indexSamples.add(cIndexInfo);
+            }
         }
+        // don't put an else here...
+        if (buffer != null)
+        {
+            idxSerializer.serialize(cIndexInfo, buffer);
+        }
+
+        firstClustering = null;
+    }
+
+    private DataOutputBuffer reuseOrAllocateBuffer()
+    {
+        // Check whether a reusable DataOutputBuffer already exists for this
+        // ColumnIndex instance and return it.
+        if (reusableBuffer != null) {
+            DataOutputBuffer buffer = reusableBuffer;
+            buffer.clear();
+            return buffer;
+        }
+        // don't use the standard RECYCLER as that only recycles up to 1MB and requires proper cleanup
+        return new DataOutputBuffer(DatabaseDescriptor.getColumnIndexCacheSize() * 2);
+    }
+
+    private void add(Unfiltered unfiltered) throws IOException
+    {
+        long pos = currentPosition();
+
+        if (firstClustering == null)
+        {
+            // Beginning of an index block. Remember the start and position
+            firstClustering = unfiltered.clustering();
+            startPosition = pos;
+        }
+
+        UnfilteredSerializer.serializer.serialize(unfiltered, header, writer, pos - previousRowStart, version);
+
+        // notify observers about each new row
+        if (!observers.isEmpty())
+            observers.forEach((o) -> o.nextUnfilteredCluster(unfiltered));
+
+        lastClustering = unfiltered.clustering();
+        previousRowStart = pos;
+        ++written;
+
+        if (unfiltered.kind() == Unfiltered.Kind.RANGE_TOMBSTONE_MARKER)
+        {
+            RangeTombstoneMarker marker = (RangeTombstoneMarker) unfiltered;
+            openMarker = marker.isOpen(false) ? marker.openDeletionTime(false) : null;
+        }
+
+        // if we hit the column index size that we have to index after, go ahead and index it.
+        if (currentPosition() - startPosition >= DatabaseDescriptor.getColumnIndexSize())
+            addIndexBlock();
+    }
+
+    private void finish() throws IOException
+    {
+        UnfilteredSerializer.serializer.writeEndOfPartition(writer);
+
+        // It's possible we add no rows, just a top level deletion
+        if (written == 0)
+            return;
+
+        // the last column may have fallen on an index boundary already.  if not, index it explicitly.
+        if (firstClustering != null)
+            addIndexBlock();
+
+        // If we serialize the IndexInfo objects directly in the code above into 'buffer',
+        // we have to write the offsts to these here. The offsets have already been are collected
+        // in indexOffsets[]. buffer is != null, if it exceeds Config.column_index_cache_size_in_kb.
+        // In the other case, when buffer==null, the offsets are serialized in RowIndexEntry.IndexedEntry.serialize().
+        if (buffer != null)
+            RowIndexEntry.Serializer.serializeOffsets(buffer, indexOffsets, columnIndexCount);
+
+        // we should always have at least one computed index block, but we only write it out if there is more than that.
+        assert columnIndexCount > 0 && headerLength >= 0;
+    }
+
+    public int indexInfoSerializedSize()
+    {
+        return buffer != null
+               ? buffer.buffer().limit()
+               : indexSamplesSerializedSize + columnIndexCount * TypeSizes.sizeof(0);
     }
 }
diff --git a/src/java/org/apache/cassandra/db/Columns.java b/src/java/org/apache/cassandra/db/Columns.java
index 18e17d7..8efb848 100644
--- a/src/java/org/apache/cassandra/db/Columns.java
+++ b/src/java/org/apache/cassandra/db/Columns.java
@@ -19,6 +19,7 @@
 
 import java.io.IOException;
 import java.util.*;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.nio.ByteBuffer;
 import java.security.MessageDigest;
@@ -38,6 +39,7 @@
 import org.apache.cassandra.utils.SearchIterator;
 import org.apache.cassandra.utils.btree.BTree;
 import org.apache.cassandra.utils.btree.BTreeSearchIterator;
+import org.apache.cassandra.utils.btree.BTreeRemoval;
 import org.apache.cassandra.utils.btree.UpdateFunction;
 
 /**
@@ -355,7 +357,7 @@
         if (!contains(column))
             return this;
 
-        Object[] newColumns = BTree.<ColumnDefinition>transformAndFilter(columns, (c) -> c.equals(column) ? null : c);
+        Object[] newColumns = BTreeRemoval.<ColumnDefinition>remove(columns, Comparator.naturalOrder(), column);
         return new Columns(newColumns);
     }
 
@@ -377,6 +379,23 @@
             digest.update(c.name.bytes.duplicate());
     }
 
+    public void digest(MessageDigest digest, Set<ByteBuffer> columnsToExclude)
+    {
+        for (ColumnDefinition c : this)
+            if (!columnsToExclude.contains(c.name.bytes))
+                digest.update(c.name.bytes.duplicate());
+    }
+
+    /**
+     * Apply a function to each column definition in forwards or reversed order.
+     * @param function
+     * @param reversed
+     */
+    public void apply(Consumer<ColumnDefinition> function, boolean reversed)
+    {
+        BTree.apply(columns, function, reversed);
+    }
+
     @Override
     public boolean equals(Object other)
     {
@@ -441,6 +460,7 @@
                     // fail deserialization because of that. So we grab a "fake" ColumnDefinition that ensure proper
                     // deserialization. The column will be ignore later on anyway.
                     column = metadata.getDroppedColumnDefinition(name);
+
                     if (column == null)
                         throw new RuntimeException("Unknown column " + UTF8Type.instance.getString(name) + " during deserialization");
                 }
diff --git a/src/java/org/apache/cassandra/db/Conflicts.java b/src/java/org/apache/cassandra/db/Conflicts.java
index fa0e819..9e8bd9a 100644
--- a/src/java/org/apache/cassandra/db/Conflicts.java
+++ b/src/java/org/apache/cassandra/db/Conflicts.java
@@ -68,6 +68,15 @@
         if (!rightLive)
             return Resolution.RIGHT_WINS;
 
+        // Handle empty values. Counters can't truly have empty values, but we can have a counter cell that temporarily
+        // has one on read if the column for the cell is not queried by the user due to the optimization of #10657. We
+        // thus need to handle this (see #11726 too).
+        if (!leftValue.hasRemaining())
+            return rightValue.hasRemaining() || leftTimestamp > rightTimestamp ? Resolution.LEFT_WINS : Resolution.RIGHT_WINS;
+
+        if (!rightValue.hasRemaining())
+            return Resolution.RIGHT_WINS;
+
         return Resolution.MERGE;
     }
 
diff --git a/src/java/org/apache/cassandra/db/CounterMutation.java b/src/java/org/apache/cassandra/db/CounterMutation.java
index 8aafa5c..4e4a30d 100644
--- a/src/java/org/apache/cassandra/db/CounterMutation.java
+++ b/src/java/org/apache/cassandra/db/CounterMutation.java
@@ -43,7 +43,6 @@
 import org.apache.cassandra.service.CacheService;
 import org.apache.cassandra.tracing.Tracing;
 import org.apache.cassandra.utils.*;
-import org.apache.cassandra.utils.concurrent.OpOrder;
 import org.apache.cassandra.utils.btree.BTreeSet;
 
 public class CounterMutation implements IMutation
@@ -110,7 +109,7 @@
      *
      * @return the applied resulting Mutation
      */
-    public Mutation apply() throws WriteTimeoutException
+    public Mutation applyCounterMutation() throws WriteTimeoutException
     {
         Mutation result = new Mutation(getKeyspaceName(), key());
         Keyspace keyspace = Keyspace.open(getKeyspaceName());
@@ -132,6 +131,11 @@
         }
     }
 
+    public void apply()
+    {
+        applyCounterMutation();
+    }
+
     private void grabCounterLocks(Keyspace keyspace, List<Lock> locks) throws WriteTimeoutException
     {
         long startTime = System.nanoTime();
@@ -206,7 +210,7 @@
 
     private void updateWithCurrentValue(PartitionUpdate.CounterMark mark, ClockAndCount currentValue, ColumnFamilyStore cfs)
     {
-        long clock = currentValue.clock + 1L;
+        long clock = Math.max(FBUtilities.timestampMicros(), currentValue.clock + 1L);
         long count = currentValue.count + CounterContext.instance().total(mark.value());
 
         mark.setValue(CounterContext.instance().createGlobal(CounterId.getLocalId(), clock, count));
@@ -250,7 +254,8 @@
         ClusteringIndexNamesFilter filter = new ClusteringIndexNamesFilter(names.build(), false);
         SinglePartitionReadCommand cmd = SinglePartitionReadCommand.create(cfs.metadata, nowInSec, key(), builder.build(), filter);
         PeekingIterator<PartitionUpdate.CounterMark> markIter = Iterators.peekingIterator(marks.iterator());
-        try (OpOrder.Group op = cfs.readOrdering.start(); RowIterator partition = UnfilteredRowIterators.filter(cmd.queryMemtableAndDisk(cfs, op), nowInSec))
+        try (ReadExecutionController controller = cmd.executionController();
+             RowIterator partition = UnfilteredRowIterators.filter(cmd.queryMemtableAndDisk(cfs, controller), nowInSec))
         {
             updateForRow(markIter, partition.staticRow(), cfs);
 
diff --git a/src/java/org/apache/cassandra/db/CounterMutationVerbHandler.java b/src/java/org/apache/cassandra/db/CounterMutationVerbHandler.java
index f89480a..bd273e4 100644
--- a/src/java/org/apache/cassandra/db/CounterMutationVerbHandler.java
+++ b/src/java/org/apache/cassandra/db/CounterMutationVerbHandler.java
@@ -21,7 +21,6 @@
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.exceptions.RequestExecutionException;
 import org.apache.cassandra.net.IVerbHandler;
 import org.apache.cassandra.net.MessageIn;
 import org.apache.cassandra.net.MessagingService;
@@ -34,6 +33,7 @@
 
     public void doVerb(final MessageIn<CounterMutation> message, final int id)
     {
+        long queryStartNanoTime = System.nanoTime();
         final CounterMutation cm = message.payload;
         logger.trace("Applying forwarded {}", cm);
 
@@ -51,6 +51,6 @@
             {
                 MessagingService.instance().sendReply(WriteResponse.createMessage(), id, message.from);
             }
-        });
+        }, queryStartNanoTime);
     }
 }
diff --git a/src/java/org/apache/cassandra/db/DataRange.java b/src/java/org/apache/cassandra/db/DataRange.java
index f6776c4..a78d3b6 100644
--- a/src/java/org/apache/cassandra/db/DataRange.java
+++ b/src/java/org/apache/cassandra/db/DataRange.java
@@ -67,7 +67,7 @@
      */
     public static DataRange allData(IPartitioner partitioner)
     {
-        return forTokenRange(new Range<Token>(partitioner.getMinimumToken(), partitioner.getMinimumToken()));
+        return forTokenRange(new Range<>(partitioner.getMinimumToken(), partitioner.getMinimumToken()));
     }
 
     /**
@@ -105,7 +105,7 @@
      */
     public static DataRange allData(IPartitioner partitioner, ClusteringIndexFilter filter)
     {
-        return new DataRange(Range.makeRowRange(new Range<Token>(partitioner.getMinimumToken(), partitioner.getMinimumToken())), filter);
+        return new DataRange(Range.makeRowRange(new Range<>(partitioner.getMinimumToken(), partitioner.getMinimumToken())), filter);
     }
 
     /**
@@ -288,16 +288,19 @@
     {
         sb.append("token(");
         sb.append(ColumnDefinition.toCQLString(metadata.partitionKeyColumns()));
-        sb.append(") ").append(getOperator(isStart, isInclusive)).append(" ");
+        sb.append(") ");
         if (pos instanceof DecoratedKey)
         {
+            sb.append(getOperator(isStart, isInclusive)).append(" ");
             sb.append("token(");
             appendKeyString(sb, metadata.getKeyValidator(), ((DecoratedKey)pos).getKey());
             sb.append(")");
         }
         else
         {
-            sb.append(((Token.KeyBound)pos).getToken());
+            Token.KeyBound keyBound = (Token.KeyBound) pos;
+            sb.append(getOperator(isStart, isStart == keyBound.isMinimumBound)).append(" ");
+            sb.append(keyBound.getToken());
         }
     }
 
diff --git a/src/java/org/apache/cassandra/db/Directories.java b/src/java/org/apache/cassandra/db/Directories.java
index 7ee7579..8c121de 100644
--- a/src/java/org/apache/cassandra/db/Directories.java
+++ b/src/java/org/apache/cassandra/db/Directories.java
@@ -17,29 +17,21 @@
  */
 package org.apache.cassandra.db;
 
-import static com.google.common.collect.Sets.newHashSet;
-
 import java.io.File;
 import java.io.FileFilter;
 import java.io.IOError;
 import java.io.IOException;
-import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
 import java.util.*;
 import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.BiFunction;
-import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSet.Builder;
 import com.google.common.collect.Iterables;
 
 import org.apache.commons.lang3.StringUtils;
@@ -53,6 +45,7 @@
 import org.apache.cassandra.io.FSWriteError;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.io.sstable.*;
+import org.apache.cassandra.utils.DirectorySizeCalculator;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.JVMStabilityInspector;
@@ -156,7 +149,8 @@
         {
             boolean privilege = false;
 
-            switch (action) {
+            switch (action)
+            {
                 case X:
                     privilege = file.canExecute();
                     break;
@@ -186,11 +180,18 @@
     private final CFMetaData metadata;
     private final DataDirectory[] paths;
     private final File[] dataPaths;
+    private final ImmutableMap<Path, DataDirectory> canonicalPathToDD;
 
     public Directories(final CFMetaData metadata)
     {
         this(metadata, dataDirectories);
     }
+
+    public Directories(final CFMetaData metadata, Collection<DataDirectory> paths)
+    {
+        this(metadata, paths.toArray(new DataDirectory[paths.size()]));
+    }
+
     /**
      * Create Directories of given ColumnFamily.
      * SSTable directories are created under data_directories defined in cassandra.yaml if not exist at this time.
@@ -202,6 +203,8 @@
         this.metadata = metadata;
         this.paths = paths;
 
+        ImmutableMap.Builder<Path, DataDirectory> canonicalPathsBuilder = ImmutableMap.builder();
+
         String cfId = ByteBufferUtil.bytesToHex(ByteBufferUtil.bytes(metadata.cfId));
         int idx = metadata.cfName.indexOf(SECONDARY_INDEX_NAME_SEPARATOR);
         String cfName = idx >= 0 ? metadata.cfName.substring(0, idx) : metadata.cfName;
@@ -213,27 +216,33 @@
         for (int i = 0; i < paths.length; ++i)
         {
             // check if old SSTable directory exists
-            dataPaths[i] = new File(paths[i].location, oldSSTableRelativePath);
+            File dataPath = new File(paths[i].location, oldSSTableRelativePath);
+            dataPaths[i] = dataPath;
+            canonicalPathsBuilder.put(Paths.get(FileUtils.getCanonicalPath(dataPath)), paths[i]);
         }
-        boolean olderDirectoryExists = Iterables.any(Arrays.asList(dataPaths), new Predicate<File>()
-        {
-            public boolean apply(File file)
-            {
-                return file.exists();
-            }
-        });
+        boolean olderDirectoryExists = Iterables.any(Arrays.asList(dataPaths), File::exists);
         if (!olderDirectoryExists)
         {
+            canonicalPathsBuilder = ImmutableMap.builder();
             // use 2.1+ style
             String newSSTableRelativePath = join(metadata.ksName, cfName + '-' + cfId);
             for (int i = 0; i < paths.length; ++i)
-                dataPaths[i] = new File(paths[i].location, newSSTableRelativePath);
+            {
+                File dataPath = new File(paths[i].location, newSSTableRelativePath);;
+                dataPaths[i] = dataPath;
+                canonicalPathsBuilder.put(Paths.get(FileUtils.getCanonicalPath(dataPath)), paths[i]);
+            }
         }
         // if index, then move to its own directory
         if (indexNameWithDot != null)
         {
+            canonicalPathsBuilder = ImmutableMap.builder();
             for (int i = 0; i < paths.length; ++i)
-                dataPaths[i] = new File(dataPaths[i], indexNameWithDot);
+            {
+                File dataPath = new File(dataPaths[i], indexNameWithDot);
+                dataPaths[i] = dataPath;
+                canonicalPathsBuilder.put(Paths.get(FileUtils.getCanonicalPath(dataPath)), paths[i]);
+            }
         }
 
         for (File dir : dataPaths)
@@ -277,6 +286,7 @@
                 }
             }
         }
+        canonicalPathToDD = canonicalPathsBuilder.build();
     }
 
     /**
@@ -299,6 +309,13 @@
         return null;
     }
 
+    public DataDirectory getDataDirectoryForFile(Descriptor descriptor)
+    {
+        if (descriptor != null)
+            return canonicalPathToDD.get(descriptor.directory.toPath());
+        return null;
+    }
+
     public Descriptor find(String filename)
     {
         for (File dir : dataPaths)
@@ -314,7 +331,7 @@
      * which may return any allowed directory - even a data directory that has no usable space.
      * Do not use this method in production code.
      *
-     * @throws IOError if all directories are blocked.
+     * @throws FSWriteError if all directories are disallowed.
      */
     public File getDirectoryForNewSSTables()
     {
@@ -324,11 +341,14 @@
     /**
      * Returns an allowed directory that _currently_ has {@code writeSize} bytes as usable space.
      *
-     * @throws IOError if all directories are disallowed.
+     * @throws FSWriteError if all directories are disallowed.
      */
     public File getWriteableLocationAsFile(long writeSize)
     {
-        return getLocationForDisk(getWriteableLocation(writeSize));
+        File location = getLocationForDisk(getWriteableLocation(writeSize));
+        if (location == null)
+            throw new FSWriteError(new IOException("No configured data directory contains enough space to write " + writeSize + " bytes"), "");
+        return location;
     }
 
     /**
@@ -362,7 +382,7 @@
     /**
      * Returns an allowed data directory that _currently_ has {@code writeSize} bytes as usable space.
      *
-     * @throws IOError if all directories are disallowed.
+     * @throws FSWriteError if all directories are disallowed.
      */
     public DataDirectory getWriteableLocation(long writeSize)
     {
@@ -451,6 +471,26 @@
         return totalAvailable > expectedTotalWriteSize;
     }
 
+    public DataDirectory[] getWriteableLocations()
+    {
+        List<DataDirectory> allowedDirs = new ArrayList<>();
+        for (DataDirectory dir : paths)
+        {
+            if (!DisallowedDirectories.isUnwritable(dir.location))
+                allowedDirs.add(dir);
+        }
+
+        Collections.sort(allowedDirs, new Comparator<DataDirectory>()
+        {
+            @Override
+            public int compare(DataDirectory o1, DataDirectory o2)
+            {
+                return o1.location.compareTo(o2.location);
+            }
+        });
+        return allowedDirs.toArray(new DataDirectory[allowedDirs.size()]);
+    }
+
     public static File getSnapshotDirectory(Descriptor desc, String snapshotName)
     {
         return getSnapshotDirectory(desc.directory, snapshotName);
@@ -550,6 +590,13 @@
         {
             return location.hashCode();
         }
+
+        public String toString()
+        {
+            return "DataDirectory{" +
+                   "location=" + location +
+                   '}';
+        }
     }
 
     static final class DataDirectoryCandidate implements Comparable<DataDirectoryCandidate>
@@ -802,7 +849,6 @@
         return snapshotSpaceMap;
     }
 
-
     public List<String> listEphemeralSnapshots()
     {
         final List<String> ephemeralSnapshots = new LinkedList<>();
@@ -874,7 +920,7 @@
                 }
                 catch (FSWriteError e)
                 {
-                    if (FBUtilities.isWindows())
+                    if (FBUtilities.isWindows)
                         SnapshotDeletingTask.addFailedSnapshot(snapshotDir);
                     else
                         throw e;
@@ -911,12 +957,25 @@
         return result;
     }
 
+    /**
+     * @return Raw size on disk for all directories
+     */
+    public long getRawDiretoriesSize()
+    {
+        long totalAllocatedSize = 0L;
+
+        for (File path : dataPaths)
+            totalAllocatedSize += FileUtils.folderSize(path);
+
+        return totalAllocatedSize;
+    }
+
     public long getTrueAllocatedSizeIn(File input)
     {
         if (!input.isDirectory())
             return 0;
 
-        TrueFilesSizeVisitor visitor = new TrueFilesSizeVisitor();
+        SSTableSizeSummer visitor = new SSTableSizeSummer(input, sstableLister(Directories.OnTxnErr.THROW).listFiles());
         try
         {
             Files.walkFileTree(input.toPath(), visitor);
@@ -1004,53 +1063,25 @@
         for (int i = 0; i < locations.length; ++i)
             dataDirectories[i] = new DataDirectory(new File(locations[i]));
     }
-    
-    private class TrueFilesSizeVisitor extends SimpleFileVisitor<Path>
-    {
-        private final AtomicLong size = new AtomicLong(0);
-        private final Set<String> visited = newHashSet(); //count each file only once
-        private final Set<String> alive;
 
-        TrueFilesSizeVisitor()
+    private class SSTableSizeSummer extends DirectorySizeCalculator
+    {
+        private final Set<String> toSkip;
+        SSTableSizeSummer(File path, List<File> files)
         {
-            super();
-            Builder<String> builder = ImmutableSet.builder();
-            for (File file : sstableLister(Directories.OnTxnErr.THROW).listFiles())
-                builder.add(file.getName());
-            alive = builder.build();
+            super(path);
+            toSkip = files.stream().map(f -> f.getName()).collect(Collectors.toSet());
         }
 
-        private boolean isAcceptable(Path file)
+        @Override
+        public boolean isAcceptable(Path path)
         {
-            String fileName = file.toFile().getName();
-            Pair<Descriptor, Component> pair = SSTable.tryComponentFromFilename(file.getParent().toFile(), fileName);
+            File file = path.toFile();
+            Pair<Descriptor, Component> pair = SSTable.tryComponentFromFilename(path.getParent().toFile(), file.getName());
             return pair != null
                     && pair.left.ksname.equals(metadata.ksName)
                     && pair.left.cfname.equals(metadata.cfName)
-                    && !visited.contains(fileName)
-                    && !alive.contains(fileName);
-        }
-
-        @Override
-        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
-        {
-            if (isAcceptable(file))
-            {
-                size.addAndGet(attrs.size());
-                visited.add(file.toFile().getName());
-            }
-            return FileVisitResult.CONTINUE;
-        }
-
-        @Override
-        public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException 
-        {
-            return FileVisitResult.CONTINUE;
-        }
-        
-        public long getAllocatedSize()
-        {
-            return size.get();
+                    && !toSkip.contains(file.getName());
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/db/DisallowedDirectories.java b/src/java/org/apache/cassandra/db/DisallowedDirectories.java
index 75b5e79..f030253 100644
--- a/src/java/org/apache/cassandra/db/DisallowedDirectories.java
+++ b/src/java/org/apache/cassandra/db/DisallowedDirectories.java
@@ -24,6 +24,7 @@
 import java.util.Collections;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -39,6 +40,8 @@
     private final Set<File> unreadableDirectories = new CopyOnWriteArraySet<File>();
     private final Set<File> unwritableDirectories = new CopyOnWriteArraySet<File>();
 
+    private static final AtomicInteger directoriesVersion = new AtomicInteger();
+
     private DisallowedDirectories()
     {
         // Register this instance with JMX
@@ -56,6 +59,16 @@
         return Collections.unmodifiableSet(unwritableDirectories);
     }
 
+    public void markUnreadable(String path)
+    {
+        maybeMarkUnreadable(new File(path));
+    }
+
+    public void markUnwritable(String path)
+    {
+        maybeMarkUnwritable(new File(path));
+    }
+
     /**
      * Adds parent directory of the file (or the file itself, if it is a directory)
      * to the set of unreadable directories.
@@ -67,6 +80,7 @@
         File directory = getDirectory(path);
         if (instance.unreadableDirectories.add(directory))
         {
+            directoriesVersion.incrementAndGet();
             logger.warn("Disallowing {} for reads", directory);
             return directory;
         }
@@ -84,12 +98,18 @@
         File directory = getDirectory(path);
         if (instance.unwritableDirectories.add(directory))
         {
+            directoriesVersion.incrementAndGet();
             logger.warn("Disallowing {} for writes", directory);
             return directory;
         }
         return null;
     }
 
+    public static int getDirectoriesVersion()
+    {
+        return directoriesVersion.get();
+    }
+
     /**
      * Testing only!
      * Clear the set of unwritable directories.
diff --git a/src/java/org/apache/cassandra/db/DisallowedDirectoriesMBean.java b/src/java/org/apache/cassandra/db/DisallowedDirectoriesMBean.java
index 8e825dd..64f15e5 100644
--- a/src/java/org/apache/cassandra/db/DisallowedDirectoriesMBean.java
+++ b/src/java/org/apache/cassandra/db/DisallowedDirectoriesMBean.java
@@ -22,9 +22,11 @@
 
 public interface DisallowedDirectoriesMBean
 {
-
     public Set<File> getUnreadableDirectories();
     
     public Set<File> getUnwritableDirectories();
-    
+
+    public void markUnreadable(String path);
+
+    public void markUnwritable(String path);
 }
diff --git a/src/java/org/apache/cassandra/db/DiskBoundaries.java b/src/java/org/apache/cassandra/db/DiskBoundaries.java
new file mode 100644
index 0000000..cb046eb
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/DiskBoundaries.java
@@ -0,0 +1,134 @@
+/*
+ * 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.db;
+
+import java.util.Collections;
+import java.util.List;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.service.StorageService;
+
+public class DiskBoundaries
+{
+    public final List<Directories.DataDirectory> directories;
+    public final ImmutableList<PartitionPosition> positions;
+    final long ringVersion;
+    final int directoriesVersion;
+    private final ColumnFamilyStore cfs;
+    private volatile boolean isInvalid = false;
+
+    public DiskBoundaries(ColumnFamilyStore cfs, Directories.DataDirectory[] directories, int diskVersion)
+    {
+        this(cfs, directories, null, -1, diskVersion);
+    }
+
+    @VisibleForTesting
+    public DiskBoundaries(ColumnFamilyStore cfs, Directories.DataDirectory[] directories, List<PartitionPosition> positions, long ringVersion, int diskVersion)
+    {
+        this.directories = directories == null ? null : ImmutableList.copyOf(directories);
+        this.positions = positions == null ? null : ImmutableList.copyOf(positions);
+        this.ringVersion = ringVersion;
+        this.directoriesVersion = diskVersion;
+        this.cfs = cfs;
+    }
+
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        DiskBoundaries that = (DiskBoundaries) o;
+
+        if (ringVersion != that.ringVersion) return false;
+        if (directoriesVersion != that.directoriesVersion) return false;
+        if (!directories.equals(that.directories)) return false;
+        return positions != null ? positions.equals(that.positions) : that.positions == null;
+    }
+
+    public int hashCode()
+    {
+        int result = directories != null ? directories.hashCode() : 0;
+        result = 31 * result + (positions != null ? positions.hashCode() : 0);
+        result = 31 * result + (int) (ringVersion ^ (ringVersion >>> 32));
+        result = 31 * result + directoriesVersion;
+        return result;
+    }
+
+    public String toString()
+    {
+        return "DiskBoundaries{" +
+               "directories=" + directories +
+               ", positions=" + positions +
+               ", ringVersion=" + ringVersion +
+               ", directoriesVersion=" + directoriesVersion +
+               '}';
+    }
+
+    /**
+     * check if the given disk boundaries are out of date due not being set or to having too old diskVersion/ringVersion
+     */
+    public boolean isOutOfDate()
+    {
+        if (isInvalid)
+            return true;
+        int currentDiskVersion = DisallowedDirectories.getDirectoriesVersion();
+        long currentRingVersion = StorageService.instance.getTokenMetadata().getRingVersion();
+        return currentDiskVersion != directoriesVersion || (ringVersion != -1 && currentRingVersion != ringVersion);
+    }
+
+    public void invalidate()
+    {
+        this.isInvalid = true;
+    }
+
+    public int getDiskIndex(SSTableReader sstable)
+    {
+        if (positions == null)
+        {
+            return getBoundariesFromSSTableDirectory(sstable);
+        }
+
+        int pos = Collections.binarySearch(positions, sstable.first);
+        assert pos < 0; // boundaries are .minkeybound and .maxkeybound so they should never be equal
+        return -pos - 1;
+    }
+
+    /**
+     * Try to figure out location based on sstable directory
+     */
+    private int getBoundariesFromSSTableDirectory(SSTableReader sstable)
+    {
+        Directories.DataDirectory actualDirectory = cfs.getDirectories().getDataDirectoryForFile(sstable.descriptor);
+        for (int i = 0; i < directories.size(); i++)
+        {
+            Directories.DataDirectory directory = directories.get(i);
+            if (actualDirectory != null && actualDirectory.equals(directory))
+                return i;
+        }
+        return 0;
+    }
+
+    public Directories.DataDirectory getCorrectDiskForSSTable(SSTableReader sstable)
+    {
+        return directories.get(getDiskIndex(sstable));
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/db/DiskBoundaryManager.java b/src/java/org/apache/cassandra/db/DiskBoundaryManager.java
new file mode 100644
index 0000000..51343ad
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/DiskBoundaryManager.java
@@ -0,0 +1,140 @@
+/*
+ * 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.db;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.dht.Splitter;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.locator.TokenMetadata;
+import org.apache.cassandra.service.PendingRangeCalculatorService;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.utils.FBUtilities;
+
+public class DiskBoundaryManager
+{
+    private static final Logger logger = LoggerFactory.getLogger(DiskBoundaryManager.class);
+    private volatile DiskBoundaries diskBoundaries;
+
+    public DiskBoundaries getDiskBoundaries(ColumnFamilyStore cfs)
+    {
+        if (!cfs.getPartitioner().splitter().isPresent())
+            return new DiskBoundaries(cfs, cfs.getDirectories().getWriteableLocations(), DisallowedDirectories.getDirectoriesVersion());
+        if (diskBoundaries == null || diskBoundaries.isOutOfDate())
+        {
+            synchronized (this)
+            {
+                if (diskBoundaries == null || diskBoundaries.isOutOfDate())
+                {
+                    logger.debug("Refreshing disk boundary cache for {}.{}", cfs.keyspace.getName(), cfs.getTableName());
+                    DiskBoundaries oldBoundaries = diskBoundaries;
+                    diskBoundaries = getDiskBoundaryValue(cfs);
+                    logger.debug("Updating boundaries from {} to {} for {}.{}", oldBoundaries, diskBoundaries, cfs.keyspace.getName(), cfs.getTableName());
+                }
+            }
+        }
+        return diskBoundaries;
+    }
+
+    public void invalidate()
+    {
+       if (diskBoundaries != null)
+           diskBoundaries.invalidate();
+    }
+
+    private static DiskBoundaries getDiskBoundaryValue(ColumnFamilyStore cfs)
+    {
+        Collection<Range<Token>> localRanges;
+
+        long ringVersion;
+        TokenMetadata tmd;
+        do
+        {
+            tmd = StorageService.instance.getTokenMetadata();
+            ringVersion = tmd.getRingVersion();
+            if (StorageService.instance.isBootstrapMode()
+                && !StorageService.isReplacingSameAddress()) // When replacing same address, the node marks itself as UN locally
+            {
+                PendingRangeCalculatorService.instance.blockUntilFinished();
+                localRanges = tmd.getPendingRanges(cfs.keyspace.getName(), FBUtilities.getBroadcastAddress());
+            }
+            else
+            {
+                // Reason we use use the future settled TMD is that if we decommission a node, we want to stream
+                // from that node to the correct location on disk, if we didn't, we would put new files in the wrong places.
+                // We do this to minimize the amount of data we need to move in rebalancedisks once everything settled
+                localRanges = cfs.keyspace.getReplicationStrategy().getAddressRanges(tmd.cloneAfterAllSettled()).get(FBUtilities.getBroadcastAddress());
+            }
+            logger.debug("Got local ranges {} (ringVersion = {})", localRanges, ringVersion);
+        }
+        while (ringVersion != tmd.getRingVersion()); // if ringVersion is different here it means that
+                                                     // it might have changed before we calculated localRanges - recalculate
+
+        int directoriesVersion;
+        Directories.DataDirectory[] dirs;
+        do
+        {
+            directoriesVersion = DisallowedDirectories.getDirectoriesVersion();
+            dirs = cfs.getDirectories().getWriteableLocations();
+        }
+        while (directoriesVersion != DisallowedDirectories.getDirectoriesVersion()); // if directoriesVersion has changed we need to recalculate
+
+        if (localRanges == null || localRanges.isEmpty())
+            return new DiskBoundaries(cfs, dirs, null, ringVersion, directoriesVersion);
+
+        List<Range<Token>> sortedLocalRanges = Range.sort(localRanges);
+
+        List<PartitionPosition> positions = getDiskBoundaries(sortedLocalRanges, cfs.getPartitioner(), dirs);
+        return new DiskBoundaries(cfs, dirs, positions, ringVersion, directoriesVersion);
+    }
+
+    /**
+     * Returns a list of disk boundaries, the result will differ depending on whether vnodes are enabled or not.
+     *
+     * What is returned are upper bounds for the disks, meaning everything from partitioner.minToken up to
+     * getDiskBoundaries(..).get(0) should be on the first disk, everything between 0 to 1 should be on the second disk
+     * etc.
+     *
+     * The final entry in the returned list will always be the partitioner maximum tokens upper key bound
+     */
+    private static List<PartitionPosition> getDiskBoundaries(List<Range<Token>> sortedLocalRanges, IPartitioner partitioner, Directories.DataDirectory[] dataDirectories)
+    {
+        assert partitioner.splitter().isPresent();
+        Splitter splitter = partitioner.splitter().get();
+        boolean dontSplitRanges = DatabaseDescriptor.getNumTokens() > 1;
+        List<Token> boundaries = splitter.splitOwnedRanges(dataDirectories.length, sortedLocalRanges, dontSplitRanges);
+        // If we can't split by ranges, split evenly to ensure utilisation of all disks
+        if (dontSplitRanges && boundaries.size() < dataDirectories.length)
+            boundaries = splitter.splitOwnedRanges(dataDirectories.length, sortedLocalRanges, false);
+
+        List<PartitionPosition> diskBoundaries = new ArrayList<>();
+        for (int i = 0; i < boundaries.size() - 1; i++)
+            diskBoundaries.add(boundaries.get(i).maxKeyBound());
+        diskBoundaries.add(partitioner.getMaximumToken().maxKeyBound());
+        return diskBoundaries;
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/IMutation.java b/src/java/org/apache/cassandra/db/IMutation.java
index aad35c3..c734e16 100644
--- a/src/java/org/apache/cassandra/db/IMutation.java
+++ b/src/java/org/apache/cassandra/db/IMutation.java
@@ -17,7 +17,6 @@
  */
 package org.apache.cassandra.db;
 
-import java.nio.ByteBuffer;
 import java.util.Collection;
 import java.util.UUID;
 
@@ -25,6 +24,7 @@
 
 public interface IMutation
 {
+    public void apply();
     public String getKeyspaceName();
     public Collection<UUID> getColumnFamilyIds();
     public DecoratedKey key();
diff --git a/src/java/org/apache/cassandra/db/Keyspace.java b/src/java/org/apache/cassandra/db/Keyspace.java
index 3d3e037..eb3de5a 100644
--- a/src/java/org/apache/cassandra/db/Keyspace.java
+++ b/src/java/org/apache/cassandra/db/Keyspace.java
@@ -33,7 +33,7 @@
 import org.apache.cassandra.concurrent.StageManager;
 import org.apache.cassandra.config.*;
 import org.apache.cassandra.db.commitlog.CommitLog;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.apache.cassandra.db.compaction.CompactionManager;
 import org.apache.cassandra.db.lifecycle.SSTableSet;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
@@ -46,11 +46,10 @@
 import org.apache.cassandra.locator.AbstractReplicationStrategy;
 import org.apache.cassandra.metrics.KeyspaceMetrics;
 import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.ReplicationParams;
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.tracing.Tracing;
-import org.apache.cassandra.utils.ByteBufferUtil;
-import org.apache.cassandra.utils.FBUtilities;
-import org.apache.cassandra.utils.JVMStabilityInspector;
+import org.apache.cassandra.utils.*;
 import org.apache.cassandra.utils.concurrent.OpOrder;
 
 /**
@@ -70,7 +69,7 @@
     // proper directories here as well as in CassandraDaemon.
     static
     {
-        if (!Config.isClientMode())
+        if (DatabaseDescriptor.isDaemonInitialized() || DatabaseDescriptor.isToolInitialized())
             DatabaseDescriptor.createAllDirectories();
     }
 
@@ -84,6 +83,7 @@
     private final ConcurrentMap<UUID, ColumnFamilyStore> columnFamilyStores = new ConcurrentHashMap<>();
     private volatile AbstractReplicationStrategy replicationStrategy;
     public final ViewManager viewManager;
+    private volatile ReplicationParams replicationParams;
 
     public static final Function<String,Keyspace> keyspaceTransformer = new Function<String, Keyspace>()
     {
@@ -102,7 +102,7 @@
 
     public static Keyspace open(String keyspaceName)
     {
-        assert initialized || Schema.isLocalSystemKeyspace(keyspaceName);
+        assert initialized || SchemaConstants.isLocalSystemKeyspace(keyspaceName);
         return open(keyspaceName, Schema.instance, true);
     }
 
@@ -218,9 +218,10 @@
      *
      * @param snapshotName     the tag associated with the name of the snapshot.  This value may not be null
      * @param columnFamilyName the column family to snapshot or all on null
+     * @param skipFlush Skip blocking flush of memtable
      * @throws IOException if the column family doesn't exist
      */
-    public void snapshot(String snapshotName, String columnFamilyName) throws IOException
+    public void snapshot(String snapshotName, String columnFamilyName, boolean skipFlush) throws IOException
     {
         assert snapshotName != null;
         boolean tookSnapShot = false;
@@ -229,7 +230,7 @@
             if (columnFamilyName == null || cfStore.name.equals(columnFamilyName))
             {
                 tookSnapShot = true;
-                cfStore.snapshot(snapshotName);
+                cfStore.snapshot(snapshotName, skipFlush);
             }
         }
 
@@ -238,6 +239,19 @@
     }
 
     /**
+     * Take a snapshot of the specific column family, or the entire set of column families
+     * if columnFamily is null with a given timestamp
+     *
+     * @param snapshotName     the tag associated with the name of the snapshot.  This value may not be null
+     * @param columnFamilyName the column family to snapshot or all on null
+     * @throws IOException if the column family doesn't exist
+     */
+    public void snapshot(String snapshotName, String columnFamilyName) throws IOException
+    {
+        snapshot(snapshotName, columnFamilyName, false);
+    }
+
+    /**
      * @param clientSuppliedName may be null.
      * @return the name of the snapshot
      */
@@ -251,6 +265,11 @@
         return snapshotName;
     }
 
+    public static String getTimestampedSnapshotNameWithPrefix(String clientSuppliedName, String prefix)
+    {
+        return prefix + "-" + getTimestampedSnapshotName(clientSuppliedName);
+    }
+
     /**
      * Check whether snapshots already exists for a given name.
      *
@@ -327,6 +346,12 @@
                                                                                     StorageService.instance.getTokenMetadata(),
                                                                                     DatabaseDescriptor.getEndpointSnitch(),
                                                                                     ksm.params.replication.options);
+        if (!ksm.params.replication.equals(replicationParams))
+        {
+            logger.debug("New replication settings for keyspace {} - invalidating disk boundary caches", ksm.name);
+            columnFamilyStores.values().forEach(ColumnFamilyStore::invalidateDiskBoundaries);
+        }
+        replicationParams = ksm.params.replication;
     }
 
     // best invoked on the compaction mananger.
@@ -354,6 +379,30 @@
     }
 
     /**
+     * Registers a custom cf instance with this keyspace.
+     * This is required for offline tools what use non-standard directories.
+     */
+    public void initCfCustom(ColumnFamilyStore newCfs)
+    {
+        ColumnFamilyStore cfs = columnFamilyStores.get(newCfs.metadata.cfId);
+
+        if (cfs == null)
+        {
+            // CFS being created for the first time, either on server startup or new CF being added.
+            // We don't worry about races here; startup is safe, and adding multiple idential CFs
+            // simultaneously is a "don't do that" scenario.
+            ColumnFamilyStore oldCfs = columnFamilyStores.putIfAbsent(newCfs.metadata.cfId, newCfs);
+            // CFS mbean instantiation will error out before we hit this, but in case that changes...
+            if (oldCfs != null)
+                throw new IllegalStateException("added multiple mappings for cf id " + newCfs.metadata.cfId);
+        }
+        else
+        {
+            throw new IllegalStateException("CFS is already initialized: " + cfs.name);
+        }
+    }
+
+    /**
      * adds a cf to internal structures, ends up creating disk files).
      */
     public void initCf(CFMetaData metadata, boolean loadSSTables)
@@ -403,7 +452,7 @@
 
     /**
      * If apply is blocking, apply must not be deferred
-     * Otherwise there is a race condition where ALL mutation workers are beeing blocked ending
+     * Otherwise there is a race condition where ALL mutation workers are being blocked ending
      * in a complete deadlock of the mutation stage. See CASSANDRA-12689.
      *
      * @param mutation       the row to write.  Must not be modified after calling apply, since commitlog append
@@ -422,21 +471,6 @@
     }
 
     /**
-     * Compatibility method that keeps <bold>isClReplay</bold> flag.
-     * @deprecated Use {@link this#applyFuture(Mutation, boolean, boolean, boolean, boolean)} instead
-     */
-    @Deprecated
-    public CompletableFuture<?> apply(final Mutation mutation,
-                                       final boolean writeCommitLog,
-                                       boolean updateIndexes,
-                                       boolean isClReplay,
-                                       boolean isDeferrable,
-                                       CompletableFuture<?> future)
-    {
-        return applyInternal(mutation, writeCommitLog, updateIndexes, !isClReplay, isDeferrable, future != null? future : new CompletableFuture<>());
-    }
-
-    /**
      * This method appends a row to the global CommitLog, then updates memtables and indexes.
      *
      * @param mutation       the row to write.  Must not be modified after calling apply, since commitlog append
@@ -456,88 +490,107 @@
         if (TEST_FAIL_WRITES && metadata.name.equals(TEST_FAIL_WRITES_KS))
             throw new RuntimeException("Testing write failures");
 
+        Lock[] locks = null;
+
         boolean requiresViewUpdate = updateIndexes && viewManager.updatesAffectView(Collections.singleton(mutation), false);
 
-        Lock lock = null;
         if (requiresViewUpdate)
         {
             mutation.viewLockAcquireStart.compareAndSet(0L, System.currentTimeMillis());
-            while (true)
-            {
-                if (TEST_FAIL_MV_LOCKS_COUNT == 0)
-                    lock = ViewManager.acquireLockFor(mutation.key().getKey());
-                else
-                    TEST_FAIL_MV_LOCKS_COUNT--;
 
-                if (lock == null)
+            // the order of lock acquisition doesn't matter (from a deadlock perspective) because we only use tryLock()
+            Collection<UUID> columnFamilyIds = mutation.getColumnFamilyIds();
+            Iterator<UUID> idIterator = columnFamilyIds.iterator();
+
+            locks = new Lock[columnFamilyIds.size()];
+            for (int i = 0; i < columnFamilyIds.size(); i++)
+            {
+                UUID cfid = idIterator.next();
+                int lockKey = Objects.hash(mutation.key().getKey(), cfid);
+                while (true)
                 {
-                    //throw WTE only if request is droppable
-                    if (isDroppable && (System.currentTimeMillis() - mutation.createdAt) > DatabaseDescriptor.getWriteRpcTimeout())
+                    Lock lock = null;
+
+                    if (TEST_FAIL_MV_LOCKS_COUNT == 0)
+                        lock = ViewManager.acquireLockFor(lockKey);
+                    else
+                        TEST_FAIL_MV_LOCKS_COUNT--;
+
+                    if (lock == null)
                     {
-                        logger.trace("Could not acquire lock for {}", ByteBufferUtil.bytesToHex(mutation.key().getKey()));
-                        Tracing.trace("Could not acquire MV lock");
-                        if (future != null)
+                        //throw WTE only if request is droppable
+                        if (isDroppable && (System.currentTimeMillis() - mutation.createdAt) > DatabaseDescriptor.getWriteRpcTimeout())
                         {
-                            future.completeExceptionally(new WriteTimeoutException(WriteType.VIEW, ConsistencyLevel.LOCAL_ONE, 0, 1));
+                            for (int j = 0; j < i; j++)
+                                locks[j].unlock();
+
+                            logger.trace("Could not acquire lock for {} and table {}", ByteBufferUtil.bytesToHex(mutation.key().getKey()), columnFamilyStores.get(cfid).name);
+                            Tracing.trace("Could not acquire MV lock");
+                            if (future != null)
+                            {
+                                future.completeExceptionally(new WriteTimeoutException(WriteType.VIEW, ConsistencyLevel.LOCAL_ONE, 0, 1));
+                                return future;
+                            }
+                            else
+                                throw new WriteTimeoutException(WriteType.VIEW, ConsistencyLevel.LOCAL_ONE, 0, 1);
+                        }
+                        else if (isDeferrable)
+                        {
+                            for (int j = 0; j < i; j++)
+                                locks[j].unlock();
+
+                            // This view update can't happen right now. so rather than keep this thread busy
+                            // we will re-apply ourself to the queue and try again later
+                            final CompletableFuture<?> mark = future;
+                            StageManager.getStage(Stage.MUTATION).execute(() ->
+                                                                          applyInternal(mutation, writeCommitLog, true, isDroppable, true, mark)
+                            );
                             return future;
                         }
                         else
                         {
-                            throw new WriteTimeoutException(WriteType.VIEW, ConsistencyLevel.LOCAL_ONE, 0, 1);
+                            // Retry lock on same thread, if mutation is not deferrable.
+                            // Mutation is not deferrable, if applied from MutationStage and caller is waiting for future to finish
+                            // If blocking caller defers future, this may lead to deadlock situation with all MutationStage workers
+                            // being blocked by waiting for futures which will never be processed as all workers are blocked
+                            try
+                            {
+                                // Wait a little bit before retrying to lock
+                                Thread.sleep(10);
+                            }
+                            catch (InterruptedException e)
+                            {
+                                // Just continue
+                            }
+                            continue;
                         }
                     }
-                    else if (isDeferrable)
-                    {
-                        //This view update can't happen right now. so rather than keep this thread busy
-                        // we will re-apply ourself to the queue and try again later
-                        final CompletableFuture<?> mark = future;
-                        StageManager.getStage(Stage.MUTATION).execute(() ->
-                                applyInternal(mutation, writeCommitLog, true, isDroppable, true, mark)
-                        );
-
-                        return future;
-                    }
                     else
                     {
-                        // Retry lock on same thread, if mutation is not deferrable.
-                        // Mutation is not deferrable, if applied from MutationStage and caller is waiting for future to finish
-                        // If blocking caller defers future, this may lead to deadlock situation with all MutationStage workers
-                        // being blocked by waiting for futures which will never be processed as all workers are blocked
-                        try
-                        {
-                            // Wait a little bit before retrying to lock
-                            Thread.sleep(10);
-                        }
-                        catch (InterruptedException e)
-                        {
-                            // Just continue
-                        }
-                        // continue in while loop
-                    }
-                }
-                else
-                {
-                    long acquireTime = System.currentTimeMillis() - mutation.viewLockAcquireStart.get();
-                    // Metrics are only collected for droppable write operations
-                    // Bulk non-droppable operations (e.g. commitlog replay, hint delivery) are not measured
-                    if (isDroppable)
-                    {
-                        for (UUID cfid : mutation.getColumnFamilyIds())
-                            columnFamilyStores.get(cfid).metric.viewLockAcquireTime.update(acquireTime, TimeUnit.MILLISECONDS);
+                        locks[i] = lock;
                     }
                     break;
                 }
             }
+
+            long acquireTime = System.currentTimeMillis() - mutation.viewLockAcquireStart.get();
+            // Metrics are only collected for droppable write operations
+            // Bulk non-droppable operations (e.g. commitlog replay, hint delivery) are not measured
+            if (isDroppable)
+            {
+                for(UUID cfid : columnFamilyIds)
+                    columnFamilyStores.get(cfid).metric.viewLockAcquireTime.update(acquireTime, TimeUnit.MILLISECONDS);
+            }
         }
         int nowInSec = FBUtilities.nowInSeconds();
         try (OpOrder.Group opGroup = writeOrder.start())
         {
             // write the mutation to the commitlog and memtables
-            ReplayPosition replayPosition = null;
+            CommitLogPosition commitLogPosition = null;
             if (writeCommitLog)
             {
                 Tracing.trace("Appending to commitlog");
-                replayPosition = CommitLog.instance.add(mutation);
+                commitLogPosition = CommitLog.instance.add(mutation);
             }
 
             for (PartitionUpdate upd : mutation.getPartitionUpdates())
@@ -570,7 +623,7 @@
                 UpdateTransaction indexTransaction = updateIndexes
                                                      ? cfs.indexManager.newUpdateTransaction(upd, opGroup, nowInSec)
                                                      : UpdateTransaction.NO_OP;
-                cfs.apply(upd, indexTransaction, opGroup, replayPosition);
+                cfs.apply(upd, indexTransaction, opGroup, commitLogPosition);
                 if (requiresViewUpdate)
                     baseComplete.set(System.currentTimeMillis());
             }
@@ -582,8 +635,12 @@
         }
         finally
         {
-            if (lock != null)
-                lock.unlock();
+            if (locks != null)
+            {
+                for (Lock lock : locks)
+                    if (lock != null)
+                        lock.unlock();
+            }
         }
     }
 
@@ -681,7 +738,7 @@
 
     public static Iterable<Keyspace> system()
     {
-        return Iterables.transform(Schema.LOCAL_SYSTEM_KEYSPACE_NAMES, keyspaceTransformer);
+        return Iterables.transform(SchemaConstants.LOCAL_SYSTEM_KEYSPACE_NAMES, keyspaceTransformer);
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/db/LegacyLayout.java b/src/java/org/apache/cassandra/db/LegacyLayout.java
index ff930c4..af00925 100644
--- a/src/java/org/apache/cassandra/db/LegacyLayout.java
+++ b/src/java/org/apache/cassandra/db/LegacyLayout.java
@@ -28,6 +28,7 @@
 
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.cql3.SuperColumnCompatibility;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.utils.AbstractIterator;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -133,7 +134,7 @@
         if (metadata.isSuper())
         {
             assert superColumnName != null;
-            return decodeForSuperColumn(metadata, new Clustering(superColumnName), cellname);
+            return decodeForSuperColumn(metadata, Clustering.make(superColumnName), cellname);
         }
 
         assert superColumnName == null;
@@ -142,10 +143,10 @@
 
     private static LegacyCellName decodeForSuperColumn(CFMetaData metadata, Clustering clustering, ByteBuffer subcol)
     {
-        ColumnDefinition def = metadata.getColumnDefinition(subcol);
+        ColumnDefinition def = getStaticallyDefinedSubColumn(metadata, subcol);
         if (def != null)
         {
-            // it's a statically defined subcolumn
+            // it's a statically defined subcolumn from column_metadata
             return new LegacyCellName(clustering, def, null);
         }
 
@@ -154,6 +155,17 @@
         return new LegacyCellName(clustering, def, subcol);
     }
 
+    private static ColumnDefinition getStaticallyDefinedSubColumn(CFMetaData metadata, ByteBuffer subcol) {
+        if (metadata.isDense())
+            return null;
+
+        ColumnDefinition def = metadata.getColumnDefinition(subcol);
+        if (def == null || !def.isRegular() || SuperColumnCompatibility.isSuperColumnMapColumn(def))
+            return null;
+
+        return def;
+    }
+
     public static LegacyCellName decodeCellName(CFMetaData metadata, ByteBuffer cellname) throws UnknownColumnException
     {
         return decodeCellName(metadata, cellname, false);
@@ -192,7 +204,7 @@
         {
             if (def == null || def.isPrimaryKeyColumn())
                 // If it's a compact table, it means the column is in fact a "dynamic" one
-                return new LegacyCellName(new Clustering(column), metadata.compactValueColumn(), null);
+                return new LegacyCellName(Clustering.make(column), metadata.compactValueColumn(), null);
         }
         else if (def == null)
         {
@@ -230,7 +242,7 @@
             // The non compound case is a lot easier, in that there is no EOC nor collection to worry about, so dealing
             // with that first.
             metadata.comparator.subtype(0).validateIfFixedSize(bound);
-            return new LegacyBound(isStart ? Slice.Bound.inclusiveStartOf(bound) : Slice.Bound.inclusiveEndOf(bound), false, null);
+            return new LegacyBound(isStart ? ClusteringBound.inclusiveStartOf(bound) : ClusteringBound.inclusiveEndOf(bound), false, null);
         }
 
         int clusteringSize = metadata.comparator.size();
@@ -308,9 +320,9 @@
             }
         }
 
-        Slice.Bound.Kind boundKind = Slice.Bound.boundKind(isStart, isInclusive);
-        Slice.Bound sb = Slice.Bound.create(boundKind, components.toArray(new ByteBuffer[components.size()]));
-        return new LegacyBound(sb, isStatic, collectionName);
+        ClusteringPrefix.Kind boundKind = ClusteringBound.boundKind(isStart, isInclusive);
+        ClusteringBound cb = ClusteringBound.create(boundKind, components.toArray(new ByteBuffer[components.size()]));
+        return new LegacyBound(cb, isStatic, collectionName);
     }
 
     // finds the simple column definition associated with components.get(clusteringSize)
@@ -355,9 +367,9 @@
         return buffers.stream().map(ByteBufferUtil::bytesToHex).collect(Collectors.joining());
     }
 
-    public static ByteBuffer encodeBound(CFMetaData metadata, Slice.Bound bound, boolean isStart)
+    public static ByteBuffer encodeBound(CFMetaData metadata, ClusteringBound bound, boolean isStart)
     {
-        if (bound == Slice.Bound.BOTTOM || bound == Slice.Bound.TOP || metadata.comparator.size() == 0)
+        if (bound == ClusteringBound.BOTTOM || bound == ClusteringBound.TOP || metadata.comparator.size() == 0)
             return ByteBufferUtil.EMPTY_BYTE_BUFFER;
 
         ClusteringPrefix clustering = bound.clustering();
@@ -454,7 +466,7 @@
             AbstractType<?> type = metadata.comparator.subtype(i);
             type.validateIfFixedSize(components.get(i));
         }
-        return new Clustering(components.subList(0, Math.min(csize, components.size())).toArray(new ByteBuffer[csize]));
+        return Clustering.make(components.subList(0, Math.min(csize, components.size())).toArray(new ByteBuffer[csize]));
     }
 
     public static ByteBuffer encodeClustering(CFMetaData metadata, ClusteringPrefix clustering)
@@ -921,8 +933,8 @@
         if (!row.deletion().isLive())
         {
             Clustering clustering = row.clustering();
-            Slice.Bound startBound = Slice.Bound.inclusiveStartOf(clustering);
-            Slice.Bound endBound = Slice.Bound.inclusiveEndOf(clustering);
+            ClusteringBound startBound = ClusteringBound.inclusiveStartOf(clustering);
+            ClusteringBound endBound = ClusteringBound.inclusiveEndOf(clustering);
 
             LegacyBound start = new LegacyLayout.LegacyBound(startBound, false, null);
             LegacyBound end = new LegacyLayout.LegacyBound(endBound, false, null);
@@ -943,12 +955,12 @@
                 boolean isStatic = clustering == Clustering.STATIC_CLUSTERING;
                 assert isStatic == col.isStatic();
 
-                Slice.Bound startBound = isStatic
+                ClusteringBound startBound = isStatic
                         ? LegacyDeletionInfo.staticBound(metadata, true)
-                        : Slice.Bound.inclusiveStartOf(clustering);
-                Slice.Bound endBound = isStatic
+                        : ClusteringBound.inclusiveStartOf(clustering);
+                ClusteringBound endBound = isStatic
                         ? LegacyDeletionInfo.staticBound(metadata, false)
-                        : Slice.Bound.inclusiveEndOf(clustering);
+                        : ClusteringBound.inclusiveEndOf(clustering);
 
                 LegacyLayout.LegacyBound start = new LegacyLayout.LegacyBound(startBound, isStatic, col);
                 LegacyLayout.LegacyBound end = new LegacyLayout.LegacyBound(endBound, isStatic, col);
@@ -1123,9 +1135,9 @@
             // we can have collection deletion and we want those to sort properly just before the column they
             // delete, not before the whole row.
             // We also want to special case static so they sort before any non-static. Note in particular that
-            // this special casing is important in the case of one of the Atom being Slice.Bound.BOTTOM: we want
+            // this special casing is important in the case of one of the Atom being Bound.BOTTOM: we want
             // it to sort after the static as we deal with static first in toUnfilteredAtomIterator and having
-            // Slice.Bound.BOTTOM first would mess that up (note that static deletion is handled through a specific
+            // Bound.BOTTOM first would mess that up (note that static deletion is handled through a specific
             // static tombstone, see LegacyDeletionInfo.add()).
             if (o1.isStatic() != o2.isStatic())
                 return o1.isStatic() ? -1 : 1;
@@ -1213,11 +1225,11 @@
             // that case. Note that if we are in a legit case of an unknown column, we want to simply skip that cell,
             // but we don't do this here and re-throw the exception because the calling code sometimes has to know
             // about this happening. This does mean code calling this method should handle this case properly.
-            if (!metadata.ksName.equals(SystemKeyspace.NAME) && metadata.getDroppedColumnDefinition(e.columnName) == null)
-                throw new IllegalStateException(String.format("Got cell for unknown column %s in sstable of %s.%s: " +
-                                                              "This suggest a problem with the schema which doesn't list " +
-                                                              "this column. Even if that column was dropped, it should have " +
-                                                              "been listed as such", UTF8Type.instance.compose(e.columnName), metadata.ksName, metadata.cfName), e);
+            if (!metadata.ksName.equals(SchemaConstants.SYSTEM_KEYSPACE_NAME) && metadata.getDroppedColumnDefinition(e.columnName) == null)
+                logger.warn(String.format("Got cell for unknown column %s in sstable of %s.%s: " +
+                                          "This suggest a problem with the schema which doesn't list " +
+                                          "this column. Even if that column was dropped, it should have " +
+                                          "been listed as such", UTF8Type.instance.compose(e.columnName), metadata.ksName, metadata.cfName), e);
 
             throw e;
         }
@@ -1298,7 +1310,7 @@
                     // then simply ignore the cell is fine. But also not that we ignore if it's the
                     // system keyspace because for those table we actually remove columns without registering
                     // them in the dropped columns
-                    if (metadata.ksName.equals(SystemKeyspace.NAME) || metadata.getDroppedColumnDefinition(e.columnName) != null)
+                    if (metadata.ksName.equals(SchemaConstants.SYSTEM_KEYSPACE_NAME) || metadata.getDroppedColumnDefinition(e.columnName) != null)
                         return computeNext();
                     else
                         throw new IOError(e);
@@ -1418,13 +1430,14 @@
             {
                 // It's the row marker
                 assert !cell.value.hasRemaining();
+
                 // In 2.1, the row marker expired cell might have been converted into a deleted one by compaction.
                 // If we do not set the primary key liveness info for this row and it does not contains any regular columns
                 // the row will be empty. To avoid that, we reuse the localDeletionTime but use a fake TTL.
                 // The only time in 2.x that we actually delete a row marker is in 2i tables, so in that case we do
                 // want to actually propagate the row deletion. (CASSANDRA-13320)
                 if (!cell.isTombstone())
-                    builder.addPrimaryKeyLivenessInfo(LivenessInfo.create(cell.timestamp, cell.ttl, cell.localDeletionTime));
+                    builder.addPrimaryKeyLivenessInfo(LivenessInfo.withExpirationTime(cell.timestamp, cell.ttl, cell.localDeletionTime));
                 else if (metadata.isIndex())
                     builder.addRowDeletion(Row.Deletion.regular(new DeletionTime(cell.timestamp, cell.localDeletionTime)));
                 else
@@ -1700,14 +1713,14 @@
 
     public static class LegacyBound
     {
-        public static final LegacyBound BOTTOM = new LegacyBound(Slice.Bound.BOTTOM, false, null);
-        public static final LegacyBound TOP = new LegacyBound(Slice.Bound.TOP, false, null);
+        public static final LegacyBound BOTTOM = new LegacyBound(ClusteringBound.BOTTOM, false, null);
+        public static final LegacyBound TOP = new LegacyBound(ClusteringBound.TOP, false, null);
 
-        public final Slice.Bound bound;
+        public final ClusteringBound bound;
         public final boolean isStatic;
         public final ColumnDefinition collectionName;
 
-        public LegacyBound(Slice.Bound bound, boolean isStatic, ColumnDefinition collectionName)
+        public LegacyBound(ClusteringBound bound, boolean isStatic, ColumnDefinition collectionName)
         {
             this.bound = bound;
             this.isStatic = isStatic;
@@ -1723,7 +1736,7 @@
             ByteBuffer[] values = new ByteBuffer[bound.size()];
             for (int i = 0; i < bound.size(); i++)
                 values[i] = bound.get(i);
-            return new Clustering(values);
+            return Clustering.make(values);
         }
 
         @Override
@@ -1936,9 +1949,9 @@
             if ((start.collectionName == null) != (stop.collectionName == null))
             {
                 if (start.collectionName == null)
-                    stop = new LegacyBound(Slice.Bound.inclusiveEndOf(stop.bound.values), stop.isStatic, null);
+                    stop = new LegacyBound(ClusteringBound.inclusiveEndOf(stop.bound.values), stop.isStatic, null);
                 else
-                    start = new LegacyBound(Slice.Bound.inclusiveStartOf(start.bound.values), start.isStatic, null);
+                    start = new LegacyBound(ClusteringBound.inclusiveStartOf(start.bound.values), start.isStatic, null);
             }
             else if (!Objects.equals(start.collectionName, stop.collectionName))
             {
@@ -1965,7 +1978,7 @@
             return new LegacyRangeTombstone(newStart, stop, deletionTime);
         }
 
-        public LegacyRangeTombstone withNewStart(Slice.Bound newStart)
+        public LegacyRangeTombstone withNewStart(ClusteringBound newStart)
         {
             return withNewStart(new LegacyBound(newStart, start.isStatic, null));
         }
@@ -1975,7 +1988,7 @@
             return new LegacyRangeTombstone(start, newStop, deletionTime);
         }
 
-        public LegacyRangeTombstone withNewEnd(Slice.Bound newEnd)
+        public LegacyRangeTombstone withNewEnd(ClusteringBound newEnd)
         {
             return withNewEnd(new LegacyBound(newEnd, stop.isStatic, null));
         }
@@ -2052,7 +2065,7 @@
             deletionInfo.add(topLevel);
         }
 
-        private static Slice.Bound staticBound(CFMetaData metadata, boolean isStart)
+        private static ClusteringBound staticBound(CFMetaData metadata, boolean isStart)
         {
             // In pre-3.0 nodes, static row started by a clustering with all empty values so we
             // preserve that here. Note that in practice, it doesn't really matter since the rest
@@ -2061,8 +2074,8 @@
             for (int i = 0; i < values.length; i++)
                 values[i] = ByteBufferUtil.EMPTY_BYTE_BUFFER;
             return isStart
-                 ? Slice.Bound.inclusiveStartOf(values)
-                 : Slice.Bound.inclusiveEndOf(values);
+                 ? ClusteringBound.inclusiveStartOf(values)
+                 : ClusteringBound.inclusiveEndOf(values);
         }
 
         public void add(CFMetaData metadata, LegacyRangeTombstone tombstone)
@@ -2209,7 +2222,7 @@
 
     /**
      * Almost an entire copy of RangeTombstoneList from C* 2.1.  The main difference is that LegacyBoundComparator
-     * is used in place of Comparator<Composite> (because Composite doesn't exist any more).
+     * is used in place of {@code Comparator<Composite>} (because Composite doesn't exist any more).
      *
      * This class is needed to allow us to convert single-row deletions and complex deletions into range tombstones
      * and properly merge them into the normal set of range tombstones.
diff --git a/src/java/org/apache/cassandra/db/LivenessInfo.java b/src/java/org/apache/cassandra/db/LivenessInfo.java
index f6c9b62..c2a2291 100644
--- a/src/java/org/apache/cassandra/db/LivenessInfo.java
+++ b/src/java/org/apache/cassandra/db/LivenessInfo.java
@@ -60,12 +60,8 @@
         this.timestamp = timestamp;
     }
 
-    public static LivenessInfo create(CFMetaData metadata, long timestamp, int nowInSec)
+    public static LivenessInfo create(long timestamp, int nowInSec)
     {
-        int defaultTTL = metadata.params.defaultTimeToLive;
-        if (defaultTTL != NO_TTL)
-            return expiring(timestamp, defaultTTL, nowInSec);
-
         return new LivenessInfo(timestamp);
     }
 
@@ -75,16 +71,16 @@
         return new ExpiringLivenessInfo(timestamp, ttl, ExpirationDateOverflowHandling.computeLocalExpirationTime(nowInSec, ttl));
     }
 
-    public static LivenessInfo create(CFMetaData metadata, long timestamp, int ttl, int nowInSec)
+    public static LivenessInfo create(long timestamp, int ttl, int nowInSec)
     {
         return ttl == NO_TTL
-             ? create(metadata, timestamp, nowInSec)
+             ? create(timestamp, nowInSec)
              : expiring(timestamp, ttl, nowInSec);
     }
 
-    // Note that this ctor ignores the default table ttl and takes the expiration time, not the current time.
+    // Note that this ctor takes the expiration time, not the current time.
     // Use when you know that's what you want.
-    public static LivenessInfo create(long timestamp, int ttl, int localExpirationTime)
+    public static LivenessInfo withExpirationTime(long timestamp, int ttl, int localExpirationTime)
     {
         if (ttl == EXPIRED_LIVENESS_TTL)
             return new ExpiredLivenessInfo(timestamp, ttl, localExpirationTime);
diff --git a/src/java/org/apache/cassandra/db/Memtable.java b/src/java/org/apache/cassandra/db/Memtable.java
index 139663e..9cfcb2f 100644
--- a/src/java/org/apache/cassandra/db/Memtable.java
+++ b/src/java/org/apache/cassandra/db/Memtable.java
@@ -17,7 +17,6 @@
  */
 package org.apache.cassandra.db;
 
-import java.io.File;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -25,6 +24,7 @@
 import java.util.concurrent.atomic.AtomicReference;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -32,10 +32,10 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.db.commitlog.CommitLog;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.apache.cassandra.db.commitlog.IntervalSet;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
-import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.db.filter.ClusteringIndexFilter;
 import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
@@ -46,23 +46,53 @@
 import org.apache.cassandra.dht.Murmur3Partitioner.LongToken;
 import org.apache.cassandra.index.transactions.UpdateTransaction;
 import org.apache.cassandra.io.sstable.Descriptor;
-import org.apache.cassandra.io.sstable.SSTableTxnWriter;
-import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.sstable.SSTableMultiWriter;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
+import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.service.ActiveRepairService;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.ObjectSizes;
 import org.apache.cassandra.utils.concurrent.OpOrder;
+import org.apache.cassandra.utils.memory.HeapPool;
 import org.apache.cassandra.utils.memory.MemtableAllocator;
+import org.apache.cassandra.utils.memory.MemtableCleaner;
 import org.apache.cassandra.utils.memory.MemtablePool;
+import org.apache.cassandra.utils.memory.NativePool;
+import org.apache.cassandra.utils.memory.SlabPool;
 
 public class Memtable implements Comparable<Memtable>
 {
     private static final Logger logger = LoggerFactory.getLogger(Memtable.class);
 
-    @VisibleForTesting
-    public static final MemtablePool MEMORY_POOL = DatabaseDescriptor.getMemtableAllocatorPool();
+    public static final MemtablePool MEMORY_POOL = createMemtableAllocatorPool();
+    public static final long NO_MIN_TIMESTAMP = -1;
+
+    private static MemtablePool createMemtableAllocatorPool()
+    {
+        long heapLimit = DatabaseDescriptor.getMemtableHeapSpaceInMb() << 20;
+        long offHeapLimit = DatabaseDescriptor.getMemtableOffheapSpaceInMb() << 20;
+        final float cleaningThreshold = DatabaseDescriptor.getMemtableCleanupThreshold();
+        final MemtableCleaner cleaner = ColumnFamilyStore::flushLargestMemtable;
+        switch (DatabaseDescriptor.getMemtableAllocationType())
+        {
+            case unslabbed_heap_buffers:
+                return new HeapPool(heapLimit, cleaningThreshold, cleaner);
+            case heap_buffers:
+                return new SlabPool(heapLimit, 0, cleaningThreshold, cleaner);
+            case offheap_buffers:
+                if (!FileUtils.isCleanerAvailable)
+                {
+                    throw new IllegalStateException("Could not free direct byte buffer: offheap_buffers is not a safe memtable_allocation_type without this ability, please adjust your config. This feature is only guaranteed to work on an Oracle JVM. Refusing to start.");
+                }
+                return new SlabPool(heapLimit, offHeapLimit, cleaningThreshold, cleaner);
+            case offheap_objects:
+                return new NativePool(heapLimit, offHeapLimit, cleaningThreshold, cleaner);
+            default:
+                throw new AssertionError();
+        }
+    }
+
     private static final int ROW_OVERHEAD_HEAP_SIZE = estimateRowOverhead(Integer.parseInt(System.getProperty("cassandra.memtable_row_overhead_computation_step", "100000")));
 
     private final MemtableAllocator allocator;
@@ -71,23 +101,25 @@
 
     // the write barrier for directing writes to this memtable or the next during a switch
     private volatile OpOrder.Barrier writeBarrier;
-    // the precise upper bound of ReplayPosition owned by this memtable
-    private volatile AtomicReference<ReplayPosition> commitLogUpperBound;
-    // the precise lower bound of ReplayPosition owned by this memtable; equal to its predecessor's commitLogUpperBound
-    private AtomicReference<ReplayPosition> commitLogLowerBound;
-    // the approximate lower bound by this memtable; must be <= commitLogLowerBound once our predecessor
+    // the precise upper bound of CommitLogPosition owned by this memtable
+    private volatile AtomicReference<CommitLogPosition> commitLogUpperBound;
+    // the precise lower bound of CommitLogPosition owned by this memtable; equal to its predecessor's commitLogUpperBound
+    private AtomicReference<CommitLogPosition> commitLogLowerBound;
+
+    // The approximate lower bound by this memtable; must be <= commitLogLowerBound once our predecessor
     // has been finalised, and this is enforced in the ColumnFamilyStore.setCommitLogUpperBound
-    private final ReplayPosition approximateCommitLogLowerBound = CommitLog.instance.getContext();
+    private final CommitLogPosition approximateCommitLogLowerBound = CommitLog.instance.getCurrentPosition();
 
     public int compareTo(Memtable that)
     {
         return this.approximateCommitLogLowerBound.compareTo(that.approximateCommitLogLowerBound);
     }
 
-    public static final class LastReplayPosition extends ReplayPosition
+    public static final class LastCommitLogPosition extends CommitLogPosition
     {
-        public LastReplayPosition(ReplayPosition copy) {
-            super(copy.segment, copy.position);
+        public LastCommitLogPosition(CommitLogPosition copy)
+        {
+            super(copy.segmentId, copy.position);
         }
     }
 
@@ -110,7 +142,7 @@
     private final StatsCollector statsCollector = new StatsCollector();
 
     // only to be used by init(), to setup the very first memtable for the cfs
-    public Memtable(AtomicReference<ReplayPosition> commitLogLowerBound, ColumnFamilyStore cfs)
+    public Memtable(AtomicReference<CommitLogPosition> commitLogLowerBound, ColumnFamilyStore cfs)
     {
         this.cfs = cfs;
         this.commitLogLowerBound = commitLogLowerBound;
@@ -130,6 +162,16 @@
         this.columnsCollector = new ColumnsCollector(metadata.partitionColumns());
     }
 
+    @VisibleForTesting
+    public Memtable(CFMetaData metadata, long minTimestamp)
+    {
+        this.initialComparator = metadata.comparator;
+        this.cfs = null;
+        this.allocator = null;
+        this.columnsCollector = new ColumnsCollector(metadata.partitionColumns());
+        this.minTimestamp = minTimestamp;
+    }
+
     public MemtableAllocator getAllocator()
     {
         return allocator;
@@ -146,10 +188,10 @@
     }
 
     @VisibleForTesting
-    public void setDiscarding(OpOrder.Barrier writeBarrier, AtomicReference<ReplayPosition> lastReplayPosition)
+    public void setDiscarding(OpOrder.Barrier writeBarrier, AtomicReference<CommitLogPosition> commitLogUpperBound)
     {
         assert this.writeBarrier == null;
-        this.commitLogUpperBound = lastReplayPosition;
+        this.commitLogUpperBound = commitLogUpperBound;
         this.writeBarrier = writeBarrier;
         allocator.setDiscarding();
     }
@@ -160,7 +202,7 @@
     }
 
     // decide if this memtable should take the write, or if it should go to the next memtable
-    public boolean accepts(OpOrder.Group opGroup, ReplayPosition replayPosition)
+    public boolean accepts(OpOrder.Group opGroup, CommitLogPosition commitLogPosition)
     {
         // if the barrier hasn't been set yet, then this memtable is still taking ALL writes
         OpOrder.Barrier barrier = this.writeBarrier;
@@ -170,7 +212,7 @@
         if (!barrier.isAfter(opGroup))
             return false;
         // if we aren't durable we are directed only by the barrier
-        if (replayPosition == null)
+        if (commitLogPosition == null)
             return true;
         while (true)
         {
@@ -179,22 +221,22 @@
             // its current value and ours; if it HAS been finalised, we simply accept its judgement
             // this permits us to coordinate a safe boundary, as the boundary choice is made
             // atomically wrt our max() maintenance, so an operation cannot sneak into the past
-            ReplayPosition currentLast = commitLogUpperBound.get();
-            if (currentLast instanceof LastReplayPosition)
-                return currentLast.compareTo(replayPosition) >= 0;
-            if (currentLast != null && currentLast.compareTo(replayPosition) >= 0)
+            CommitLogPosition currentLast = commitLogUpperBound.get();
+            if (currentLast instanceof LastCommitLogPosition)
+                return currentLast.compareTo(commitLogPosition) >= 0;
+            if (currentLast != null && currentLast.compareTo(commitLogPosition) >= 0)
                 return true;
-            if (commitLogUpperBound.compareAndSet(currentLast, replayPosition))
+            if (commitLogUpperBound.compareAndSet(currentLast, commitLogPosition))
                 return true;
         }
     }
 
-    public ReplayPosition getCommitLogLowerBound()
+    public CommitLogPosition getCommitLogLowerBound()
     {
         return commitLogLowerBound.get();
     }
 
-    public ReplayPosition getCommitLogUpperBound()
+    public CommitLogPosition getCommitLogUpperBound()
     {
         return commitLogUpperBound.get();
     }
@@ -209,7 +251,7 @@
         return partitions.isEmpty();
     }
 
-    public boolean mayContainDataBefore(ReplayPosition position)
+    public boolean mayContainDataBefore(CommitLogPosition position)
     {
         return approximateCommitLogLowerBound.compareTo(position) < 0;
     }
@@ -227,7 +269,7 @@
      * Should only be called by ColumnFamilyStore.apply via Keyspace.apply, which supplies the appropriate
      * OpOrdering.
      *
-     * replayPosition should only be null if this is a secondary index, in which case it is *expected* to be null
+     * commitLogSegmentPosition should only be null if this is a secondary index, in which case it is *expected* to be null
      */
     long put(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup)
     {
@@ -265,6 +307,45 @@
         return partitions.size();
     }
 
+    public List<FlushRunnable> flushRunnables(LifecycleTransaction txn)
+    {
+        return createFlushRunnables(txn);
+    }
+
+    private List<FlushRunnable> createFlushRunnables(LifecycleTransaction txn)
+    {
+        DiskBoundaries diskBoundaries = cfs.getDiskBoundaries();
+        List<PartitionPosition> boundaries = diskBoundaries.positions;
+        List<Directories.DataDirectory> locations = diskBoundaries.directories;
+        if (boundaries == null)
+            return Collections.singletonList(new FlushRunnable(txn));
+
+        List<FlushRunnable> runnables = new ArrayList<>(boundaries.size());
+        PartitionPosition rangeStart = cfs.getPartitioner().getMinimumToken().minKeyBound();
+        try
+        {
+            for (int i = 0; i < boundaries.size(); i++)
+            {
+                PartitionPosition t = boundaries.get(i);
+                runnables.add(new FlushRunnable(rangeStart, t, locations.get(i), txn));
+                rangeStart = t;
+            }
+            return runnables;
+        }
+        catch (Throwable e)
+        {
+            throw Throwables.propagate(abortRunnables(runnables, e));
+        }
+    }
+
+    public Throwable abortRunnables(List<FlushRunnable> runnables, Throwable t)
+    {
+        if (runnables != null)
+            for (FlushRunnable runnable : runnables)
+                t = runnable.writer.abort(t);
+        return t;
+    }
+
     public String toString()
     {
         return String.format("Memtable-%s@%s(%s serialized bytes, %s ops, %.0f%%/%.0f%% of on/off-heap limit)",
@@ -317,20 +398,16 @@
         return partitions.get(key);
     }
 
-    public Collection<SSTableReader> flush()
-    {
-        long estimatedSize = estimatedSize();
-        Directories.DataDirectory dataDirectory = cfs.getDirectories().getWriteableLocation(estimatedSize);
-        if (dataDirectory == null)
-            throw new RuntimeException("Insufficient disk space to write " + estimatedSize + " bytes");
-        File sstableDirectory = cfs.getDirectories().getLocationForDisk(dataDirectory);
-        assert sstableDirectory != null : "Flush task is not bound to any disk";
-        return writeSortedContents(sstableDirectory);
-    }
-
+    /**
+     * Returns the minTS if one available, otherwise NO_MIN_TIMESTAMP.
+     *
+     * EncodingStats uses a synthetic epoch TS at 2015. We don't want to leak that (CASSANDRA-18118) so we return NO_MIN_TIMESTAMP instead.
+     *
+     * @return The minTS or NO_MIN_TIMESTAMP if none available
+     */
     public long getMinTimestamp()
     {
-        return minTimestamp;
+        return minTimestamp != EncodingStats.NO_STATS.minTimestamp ? minTimestamp : NO_MIN_TIMESTAMP;
     }
 
     /**
@@ -342,35 +419,68 @@
         liveDataSize.addAndGet((long) 1024 * 1024 * 1024 * 1024 * 1024);
     }
 
-    private long estimatedSize()
+    class FlushRunnable implements Callable<SSTableMultiWriter>
     {
-        long keySize = 0;
-        for (PartitionPosition key : partitions.keySet())
+        private final long estimatedSize;
+        private final ConcurrentNavigableMap<PartitionPosition, AtomicBTreePartition> toFlush;
+
+        private final boolean isBatchLogTable;
+        private final SSTableMultiWriter writer;
+
+        // keeping these to be able to log what we are actually flushing
+        private final PartitionPosition from;
+        private final PartitionPosition to;
+
+        FlushRunnable(PartitionPosition from, PartitionPosition to, Directories.DataDirectory flushLocation, LifecycleTransaction txn)
         {
-            //  make sure we don't write non-sensical keys
-            assert key instanceof DecoratedKey;
-            keySize += ((DecoratedKey)key).getKey().remaining();
+            this(partitions.subMap(from, to), flushLocation, from, to, txn);
         }
-        return (long) ((keySize // index entries
-                        + keySize // keys in data file
-                        + liveDataSize.get()) // data
-                       * 1.2); // bloom filter and row index overhead
-    }
 
-    private Collection<SSTableReader> writeSortedContents(File sstableDirectory)
-    {
-        boolean isBatchLogTable = cfs.name.equals(SystemKeyspace.BATCHES) && cfs.keyspace.getName().equals(SystemKeyspace.NAME);
-
-        logger.debug("Writing {}", Memtable.this.toString());
-
-        Collection<SSTableReader> ssTables;
-        try (SSTableTxnWriter writer = createFlushWriter(cfs.getSSTablePath(sstableDirectory), columnsCollector.get(), statsCollector.get()))
+        FlushRunnable(LifecycleTransaction txn)
         {
+            this(partitions, null, null, null, txn);
+        }
+
+        FlushRunnable(ConcurrentNavigableMap<PartitionPosition, AtomicBTreePartition> toFlush, Directories.DataDirectory flushLocation, PartitionPosition from, PartitionPosition to, LifecycleTransaction txn)
+        {
+            this.toFlush = toFlush;
+            this.from = from;
+            this.to = to;
+            long keySize = 0;
+            for (PartitionPosition key : toFlush.keySet())
+            {
+                //  make sure we don't write non-sensical keys
+                assert key instanceof DecoratedKey;
+                keySize += ((DecoratedKey) key).getKey().remaining();
+            }
+            estimatedSize = (long) ((keySize // index entries
+                                    + keySize // keys in data file
+                                    + liveDataSize.get()) // data
+                                    * 1.2); // bloom filter and row index overhead
+
+            this.isBatchLogTable = cfs.name.equals(SystemKeyspace.BATCHES) && cfs.keyspace.getName().equals(SchemaConstants.SYSTEM_KEYSPACE_NAME);
+
+            if (flushLocation == null)
+                writer = createFlushWriter(txn, cfs.getSSTablePath(getDirectories().getWriteableLocationAsFile(estimatedSize)), columnsCollector.get(), statsCollector.get());
+            else
+                writer = createFlushWriter(txn, cfs.getSSTablePath(getDirectories().getLocationForDisk(flushLocation)), columnsCollector.get(), statsCollector.get());
+
+        }
+
+        protected Directories getDirectories()
+        {
+            return cfs.getDirectories();
+        }
+
+        private void writeSortedContents()
+        {
+            logger.debug("Writing {}, flushed range = ({}, {}]", Memtable.this.toString(), from, to);
+
             boolean trackContention = logger.isTraceEnabled();
             int heavilyContendedRowCount = 0;
             // (we can't clear out the map as-we-go to free up memory,
             //  since the memtable is being used for queries in the "pending flush" category)
-            for (AtomicBTreePartition partition : partitions.values())
+            for (AtomicBTreePartition partition : toFlush.values())
             {
                 // Each batchlog partition is a separate entry in the log. And for an entry, we only do 2
                 // operations: 1) we insert the entry and 2) we delete it. Further, BL data is strictly local,
@@ -392,58 +502,38 @@
                 }
             }
 
-            if (writer.getFilePointer() > 0)
-            {
-                logger.debug(String.format("Completed flushing %s (%s) for commitlog position %s",
-                                           writer.getFilename(),
-                                           FBUtilities.prettyPrintMemory(writer.getFilePointer()),
-                                           commitLogUpperBound));
-
-                // sstables should contain non-repaired data.
-                ssTables = writer.finish(true);
-            }
-            else
-            {
-                logger.debug("Completed flushing {}; nothing needed to be retained.  Commitlog position was {}",
-                             writer.getFilename(), commitLogUpperBound);
-                writer.abort();
-                ssTables = Collections.emptyList();
-            }
+            long bytesFlushed = writer.getFilePointer();
+            logger.debug("Completed flushing {} ({}) for commitlog position {}",
+                                                                              writer.getFilename(),
+                                                                              FBUtilities.prettyPrintMemory(bytesFlushed),
+                                                                              commitLogUpperBound);
+            // Update the metrics
+            cfs.metric.bytesFlushed.inc(bytesFlushed);
 
             if (heavilyContendedRowCount > 0)
-                logger.trace(String.format("High update contention in %d/%d partitions of %s ", heavilyContendedRowCount, partitions.size(), Memtable.this.toString()));
-
-            return ssTables;
+                logger.trace("High update contention in {}/{} partitions of {} ", heavilyContendedRowCount, toFlush.size(), Memtable.this);
         }
-    }
 
-    @SuppressWarnings("resource") // log and writer closed by SSTableTxnWriter
-    public SSTableTxnWriter createFlushWriter(String filename,
-                                              PartitionColumns columns,
-                                              EncodingStats stats)
-    {
-        // we operate "offline" here, as we expose the resulting reader consciously when done
-        // (although we may want to modify this behaviour in future, to encapsulate full flush behaviour in LifecycleTransaction)
-        LifecycleTransaction txn = null;
-        try
+        public SSTableMultiWriter createFlushWriter(LifecycleTransaction txn,
+                                                  String filename,
+                                                  PartitionColumns columns,
+                                                  EncodingStats stats)
         {
-            txn = LifecycleTransaction.offline(OperationType.FLUSH);
             MetadataCollector sstableMetadataCollector = new MetadataCollector(cfs.metadata.comparator)
-                    .commitLogIntervals(new IntervalSet(commitLogLowerBound.get(), commitLogUpperBound.get()));
+                    .commitLogIntervals(new IntervalSet<>(commitLogLowerBound.get(), commitLogUpperBound.get()));
 
-            return new SSTableTxnWriter(txn,
-                                        cfs.createSSTableMultiWriter(Descriptor.fromFilename(filename),
-                                                                     partitions.size(),
-                                                                     ActiveRepairService.UNREPAIRED_SSTABLE,
-                                                                     sstableMetadataCollector,
-                                                                     new SerializationHeader(true, cfs.metadata, columns, stats),
-                                                                     txn));
+            return cfs.createSSTableMultiWriter(Descriptor.fromFilename(filename),
+                                                toFlush.size(),
+                                                ActiveRepairService.UNREPAIRED_SSTABLE,
+                                                sstableMetadataCollector,
+                                                new SerializationHeader(true, cfs.metadata, columns, stats), txn);
         }
-        catch (Throwable t)
+
+        @Override
+        public SSTableMultiWriter call()
         {
-            if (txn != null)
-                txn.close();
-            throw t;
+            writeSortedContents();
+            return writer;
         }
     }
 
@@ -514,6 +604,7 @@
             assert entry.getKey() instanceof DecoratedKey;
             DecoratedKey key = (DecoratedKey)entry.getKey();
             ClusteringIndexFilter filter = dataRange.clusteringIndexFilter(key);
+
             return filter.getUnfilteredRowIterator(columnFilter, entry.getValue());
         }
     }
diff --git a/src/java/org/apache/cassandra/db/MultiCBuilder.java b/src/java/org/apache/cassandra/db/MultiCBuilder.java
index 7c77ab0..ae8c26c 100644
--- a/src/java/org/apache/cassandra/db/MultiCBuilder.java
+++ b/src/java/org/apache/cassandra/db/MultiCBuilder.java
@@ -19,6 +19,7 @@
 
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.NavigableSet;
 
@@ -27,46 +28,41 @@
 import org.apache.cassandra.utils.btree.BTreeSet;
 
 /**
- * Builder that allow to build multiple Clustering/Slice.Bound at the same time.
+ * Builder that allow to build multiple Clustering/ClusteringBound at the same time.
  */
-public class MultiCBuilder
+public abstract class MultiCBuilder
 {
     /**
      * The table comparator.
      */
-    private final ClusteringComparator comparator;
+    protected final ClusteringComparator comparator;
 
     /**
-     * The elements of the clusterings
+     * The number of clustering elements that have been added.
      */
-    private final List<List<ByteBuffer>> elementsList = new ArrayList<>();
-
-    /**
-     * The number of elements that have been added.
-     */
-    private int size;
+    protected int size;
 
     /**
      * <code>true</code> if the clusterings have been build, <code>false</code> otherwise.
      */
-    private boolean built;
+    protected boolean built;
 
     /**
      * <code>true</code> if the clusterings contains some <code>null</code> elements.
      */
-    private boolean containsNull;
+    protected boolean containsNull;
 
     /**
      * <code>true</code> if the composites contains some <code>unset</code> elements.
      */
-    private boolean containsUnset;
+    protected boolean containsUnset;
 
     /**
      * <code>true</code> if some empty collection have been added.
      */
-    private boolean hasMissingElements;
+    protected boolean hasMissingElements;
 
-    private MultiCBuilder(ClusteringComparator comparator)
+    protected MultiCBuilder(ClusteringComparator comparator)
     {
         this.comparator = comparator;
     }
@@ -74,19 +70,11 @@
     /**
      * Creates a new empty {@code MultiCBuilder}.
      */
-    public static MultiCBuilder create(ClusteringComparator comparator)
+    public static MultiCBuilder create(ClusteringComparator comparator, boolean forMultipleValues)
     {
-        return new MultiCBuilder(comparator);
-    }
-
-    /**
-     * Checks if this builder is empty.
-     *
-     * @return <code>true</code> if this builder is empty, <code>false</code> otherwise.
-     */
-    private boolean isEmpty()
-    {
-        return elementsList.isEmpty();
+        return forMultipleValues
+             ? new MultiClusteringBuilder(comparator)
+             : new OneClusteringBuilder(comparator);
     }
 
     /**
@@ -99,25 +87,7 @@
      * @param value the value of the next element
      * @return this <code>MulitCBuilder</code>
      */
-    public MultiCBuilder addElementToAll(ByteBuffer value)
-    {
-        checkUpdateable();
-
-        if (isEmpty())
-            elementsList.add(new ArrayList<ByteBuffer>());
-
-        for (int i = 0, m = elementsList.size(); i < m; i++)
-        {
-            if (value == null)
-                containsNull = true;
-            if (value == ByteBufferUtil.UNSET_BYTE_BUFFER)
-                containsUnset = true;
-
-            elementsList.get(i).add(value);
-        }
-        size++;
-        return this;
-    }
+    public abstract MultiCBuilder addElementToAll(ByteBuffer value);
 
     /**
      * Adds individually each of the specified elements to the end of all of the existing clusterings.
@@ -129,42 +99,7 @@
      * @param values the elements to add
      * @return this <code>CompositeBuilder</code>
      */
-    public MultiCBuilder addEachElementToAll(List<ByteBuffer> values)
-    {
-        checkUpdateable();
-
-        if (isEmpty())
-            elementsList.add(new ArrayList<ByteBuffer>());
-
-        if (values.isEmpty())
-        {
-            hasMissingElements = true;
-        }
-        else
-        {
-            for (int i = 0, m = elementsList.size(); i < m; i++)
-            {
-                List<ByteBuffer> oldComposite = elementsList.remove(0);
-
-                for (int j = 0, n = values.size(); j < n; j++)
-                {
-                    List<ByteBuffer> newComposite = new ArrayList<>(oldComposite);
-                    elementsList.add(newComposite);
-
-                    ByteBuffer value = values.get(j);
-
-                    if (value == null)
-                        containsNull = true;
-                    if (value == ByteBufferUtil.UNSET_BYTE_BUFFER)
-                        containsUnset = true;
-
-                    newComposite.add(values.get(j));
-                }
-            }
-        }
-        size++;
-        return this;
-    }
+    public abstract MultiCBuilder addEachElementToAll(List<ByteBuffer> values);
 
     /**
      * Adds individually each of the specified list of elements to the end of all of the existing composites.
@@ -176,41 +111,12 @@
      * @param values the elements to add
      * @return this <code>CompositeBuilder</code>
      */
-    public MultiCBuilder addAllElementsToAll(List<List<ByteBuffer>> values)
+    public abstract MultiCBuilder addAllElementsToAll(List<List<ByteBuffer>> values);
+
+    protected void checkUpdateable()
     {
-        checkUpdateable();
-
-        if (isEmpty())
-            elementsList.add(new ArrayList<ByteBuffer>());
-
-        if (values.isEmpty())
-        {
-            hasMissingElements = true;
-        }
-        else
-        {
-            for (int i = 0, m = elementsList.size(); i < m; i++)
-            {
-                List<ByteBuffer> oldComposite = elementsList.remove(0);
-
-                for (int j = 0, n = values.size(); j < n; j++)
-                {
-                    List<ByteBuffer> newComposite = new ArrayList<>(oldComposite);
-                    elementsList.add(newComposite);
-
-                    List<ByteBuffer> value = values.get(j);
-
-                    if (value.contains(null))
-                        containsNull = true;
-                    if (value.contains(ByteBufferUtil.UNSET_BYTE_BUFFER))
-                        containsUnset = true;
-
-                    newComposite.addAll(value);
-                }
-            }
-            size += values.get(0).size();
-        }
-        return this;
+        if (!hasRemaining() || built)
+            throw new IllegalStateException("this builder cannot be updated anymore");
     }
 
     /**
@@ -257,109 +163,30 @@
      *
      * @return the clusterings
      */
-    public NavigableSet<Clustering> build()
-    {
-        built = true;
-
-        if (hasMissingElements)
-            return BTreeSet.empty(comparator);
-
-        CBuilder builder = CBuilder.create(comparator);
-
-        if (elementsList.isEmpty())
-            return BTreeSet.of(builder.comparator(), builder.build());
-
-        BTreeSet.Builder<Clustering> set = BTreeSet.builder(builder.comparator());
-        for (int i = 0, m = elementsList.size(); i < m; i++)
-        {
-            List<ByteBuffer> elements = elementsList.get(i);
-            set.add(builder.buildWith(elements));
-        }
-        return set.build();
-    }
+    public abstract NavigableSet<Clustering> build();
 
     /**
-     * Builds the <code>Slice.Bound</code>s for slice restrictions.
+     * Builds the <code>ClusteringBound</code>s for slice restrictions.
      *
      * @param isStart specify if the bound is a start one
      * @param isInclusive specify if the bound is inclusive or not
      * @param isOtherBoundInclusive specify if the other bound is inclusive or not
      * @param columnDefs the columns of the slice restriction
-     * @return the <code>Slice.Bound</code>s
+     * @return the <code>ClusteringBound</code>s
      */
-    public NavigableSet<Slice.Bound> buildBoundForSlice(boolean isStart,
-                                                        boolean isInclusive,
-                                                        boolean isOtherBoundInclusive,
-                                                        List<ColumnDefinition> columnDefs)
-    {
-        built = true;
+    public abstract NavigableSet<ClusteringBound> buildBoundForSlice(boolean isStart,
+                                                                 boolean isInclusive,
+                                                                 boolean isOtherBoundInclusive,
+                                                                 List<ColumnDefinition> columnDefs);
 
-        if (hasMissingElements)
-            return BTreeSet.empty(comparator);
-
-        CBuilder builder = CBuilder.create(comparator);
-
-        if (elementsList.isEmpty())
-            return BTreeSet.of(comparator, builder.buildBound(isStart, isInclusive));
-
-        // Use a TreeSet to sort and eliminate duplicates
-        BTreeSet.Builder<Slice.Bound> set = BTreeSet.builder(comparator);
-
-        // The first column of the slice might not be the first clustering column (e.g. clustering_0 = ? AND (clustering_1, clustering_2) >= (?, ?)
-        int offset = columnDefs.get(0).position();
-
-        for (int i = 0, m = elementsList.size(); i < m; i++)
-        {
-            List<ByteBuffer> elements = elementsList.get(i);
-
-            // Handle the no bound case
-            if (elements.size() == offset)
-            {
-                set.add(builder.buildBoundWith(elements, isStart, true));
-                continue;
-            }
-
-            // In the case of mixed order columns, we will have some extra slices where the columns change directions.
-            // For example: if we have clustering_0 DESC and clustering_1 ASC a slice like (clustering_0, clustering_1) > (1, 2)
-            // will produce 2 slices: [BOTTOM, 1) and (1.2, 1]
-            // So, the END bound will return 2 bounds with the same values 1
-            ColumnDefinition lastColumn = columnDefs.get(columnDefs.size() - 1);
-            if (elements.size() <= lastColumn.position() && i < m - 1 && elements.equals(elementsList.get(i + 1)))
-            {
-                set.add(builder.buildBoundWith(elements, isStart, false));
-                set.add(builder.buildBoundWith(elementsList.get(i++), isStart, true));
-                continue;
-            }
-
-            // Handle the normal bounds
-            ColumnDefinition column = columnDefs.get(elements.size() - 1 - offset);
-            set.add(builder.buildBoundWith(elements, isStart, column.isReversedType() ? isOtherBoundInclusive : isInclusive));
-        }
-        return set.build();
-    }
-
-    public NavigableSet<Slice.Bound> buildBound(boolean isStart, boolean isInclusive)
-    {
-        built = true;
-
-        if (hasMissingElements)
-            return BTreeSet.empty(comparator);
-
-        CBuilder builder = CBuilder.create(comparator);
-
-        if (elementsList.isEmpty())
-            return BTreeSet.of(comparator, builder.buildBound(isStart, isInclusive));
-
-        // Use a TreeSet to sort and eliminate duplicates
-        BTreeSet.Builder<Slice.Bound> set = BTreeSet.builder(comparator);
-
-        for (int i = 0, m = elementsList.size(); i < m; i++)
-        {
-            List<ByteBuffer> elements = elementsList.get(i);
-            set.add(builder.buildBoundWith(elements, isStart, isInclusive));
-        }
-        return set.build();
-    }
+    /**
+     * Builds the <code>ClusteringBound</code>s
+     *
+     * @param isStart specify if the bound is a start one
+     * @param isInclusive specify if the bound is inclusive or not
+     * @return the <code>ClusteringBound</code>s
+     */
+    public abstract NavigableSet<ClusteringBound> buildBound(boolean isStart, boolean isInclusive);
 
     /**
      * Checks if some elements can still be added to the clusterings.
@@ -371,9 +198,298 @@
         return remainingCount() > 0;
     }
 
-    private void checkUpdateable()
+    /**
+     * Specialization of MultiCBuilder when we know only one clustering/bound is created.
+     */
+    private static class OneClusteringBuilder extends MultiCBuilder
     {
-        if (!hasRemaining() || built)
-            throw new IllegalStateException("this builder cannot be updated anymore");
+        /**
+         * The elements of the clusterings
+         */
+        private final ByteBuffer[] elements;
+
+        public OneClusteringBuilder(ClusteringComparator comparator)
+        {
+            super(comparator);
+            this.elements = new ByteBuffer[comparator.size()];
+        }
+
+        public MultiCBuilder addElementToAll(ByteBuffer value)
+        {
+            checkUpdateable();
+
+            if (value == null)
+                containsNull = true;
+            if (value == ByteBufferUtil.UNSET_BYTE_BUFFER)
+                containsUnset = true;
+
+            elements[size++] = value;
+            return this;
+        }
+
+        public MultiCBuilder addEachElementToAll(List<ByteBuffer> values)
+        {
+            if (values.isEmpty())
+            {
+                hasMissingElements = true;
+                return this;
+            }
+
+            assert values.size() == 1;
+
+            return addElementToAll(values.get(0));
+        }
+
+        public MultiCBuilder addAllElementsToAll(List<List<ByteBuffer>> values)
+        {
+            if (values.isEmpty())
+            {
+                hasMissingElements = true;
+                return this;
+            }
+
+            assert values.size() == 1;
+            return addEachElementToAll(values.get(0));
+        }
+
+        public NavigableSet<Clustering> build()
+        {
+            built = true;
+
+            if (hasMissingElements)
+                return BTreeSet.empty(comparator);
+
+            return BTreeSet.of(comparator, size == 0 ? Clustering.EMPTY : Clustering.make(elements));
+        }
+
+        @Override
+        public NavigableSet<ClusteringBound> buildBoundForSlice(boolean isStart,
+                                                                boolean isInclusive,
+                                                                boolean isOtherBoundInclusive,
+                                                                List<ColumnDefinition> columnDefs)
+        {
+            return buildBound(isStart, columnDefs.get(0).isReversedType() ? isOtherBoundInclusive : isInclusive);
+        }
+
+        public NavigableSet<ClusteringBound> buildBound(boolean isStart, boolean isInclusive)
+        {
+            built = true;
+
+            if (hasMissingElements)
+                return BTreeSet.empty(comparator);
+
+            if (size == 0)
+                return BTreeSet.of(comparator, isStart ? ClusteringBound.BOTTOM : ClusteringBound.TOP);
+
+            ByteBuffer[] newValues = size == elements.length
+                                   ? elements
+                                   : Arrays.copyOf(elements, size);
+
+            return BTreeSet.of(comparator, ClusteringBound.create(ClusteringBound.boundKind(isStart, isInclusive), newValues));
+        }
+    }
+
+    /**
+     * MultiCBuilder implementation actually supporting the creation of multiple clustering/bound.
+     */
+    private static class MultiClusteringBuilder extends MultiCBuilder
+    {
+        /**
+         * The elements of the clusterings
+         */
+        private final List<List<ByteBuffer>> elementsList = new ArrayList<>();
+
+        public MultiClusteringBuilder(ClusteringComparator comparator)
+        {
+            super(comparator);
+        }
+
+        public MultiCBuilder addElementToAll(ByteBuffer value)
+        {
+            checkUpdateable();
+
+            if (elementsList.isEmpty())
+                elementsList.add(new ArrayList<ByteBuffer>());
+
+            if (value == null)
+                containsNull = true;
+            else if (value == ByteBufferUtil.UNSET_BYTE_BUFFER)
+                containsUnset = true;
+
+            for (int i = 0, m = elementsList.size(); i < m; i++)
+                elementsList.get(i).add(value);
+
+            size++;
+            return this;
+        }
+
+        public MultiCBuilder addEachElementToAll(List<ByteBuffer> values)
+        {
+            checkUpdateable();
+
+            if (elementsList.isEmpty())
+                elementsList.add(new ArrayList<ByteBuffer>());
+
+            if (values.isEmpty())
+            {
+                hasMissingElements = true;
+            }
+            else
+            {
+                for (int i = 0, m = elementsList.size(); i < m; i++)
+                {
+                    List<ByteBuffer> oldComposite = elementsList.remove(0);
+
+                    for (int j = 0, n = values.size(); j < n; j++)
+                    {
+                        List<ByteBuffer> newComposite = new ArrayList<>(oldComposite);
+                        elementsList.add(newComposite);
+
+                        ByteBuffer value = values.get(j);
+
+                        if (value == null)
+                            containsNull = true;
+                        if (value == ByteBufferUtil.UNSET_BYTE_BUFFER)
+                            containsUnset = true;
+
+                        newComposite.add(values.get(j));
+                    }
+                }
+            }
+            size++;
+            return this;
+        }
+
+        public MultiCBuilder addAllElementsToAll(List<List<ByteBuffer>> values)
+        {
+            checkUpdateable();
+
+            if (elementsList.isEmpty())
+                elementsList.add(new ArrayList<ByteBuffer>());
+
+            if (values.isEmpty())
+            {
+                hasMissingElements = true;
+            }
+            else
+            {
+                for (int i = 0, m = elementsList.size(); i < m; i++)
+                {
+                    List<ByteBuffer> oldComposite = elementsList.remove(0);
+
+                    for (int j = 0, n = values.size(); j < n; j++)
+                    {
+                        List<ByteBuffer> newComposite = new ArrayList<>(oldComposite);
+                        elementsList.add(newComposite);
+
+                        List<ByteBuffer> value = values.get(j);
+
+                        if (value.contains(null))
+                            containsNull = true;
+                        if (value.contains(ByteBufferUtil.UNSET_BYTE_BUFFER))
+                            containsUnset = true;
+
+                        newComposite.addAll(value);
+                    }
+                }
+                size += values.get(0).size();
+            }
+            return this;
+        }
+
+        public NavigableSet<Clustering> build()
+        {
+            built = true;
+
+            if (hasMissingElements)
+                return BTreeSet.empty(comparator);
+
+            CBuilder builder = CBuilder.create(comparator);
+
+            if (elementsList.isEmpty())
+                return BTreeSet.of(builder.comparator(), builder.build());
+
+            BTreeSet.Builder<Clustering> set = BTreeSet.builder(builder.comparator());
+            for (int i = 0, m = elementsList.size(); i < m; i++)
+            {
+                List<ByteBuffer> elements = elementsList.get(i);
+                set.add(builder.buildWith(elements));
+            }
+            return set.build();
+        }
+
+        public NavigableSet<ClusteringBound> buildBoundForSlice(boolean isStart,
+                                                            boolean isInclusive,
+                                                            boolean isOtherBoundInclusive,
+                                                            List<ColumnDefinition> columnDefs)
+        {
+            built = true;
+
+            if (hasMissingElements)
+                return BTreeSet.empty(comparator);
+
+            CBuilder builder = CBuilder.create(comparator);
+
+            if (elementsList.isEmpty())
+                return BTreeSet.of(comparator, builder.buildBound(isStart, isInclusive));
+
+            // Use a TreeSet to sort and eliminate duplicates
+            BTreeSet.Builder<ClusteringBound> set = BTreeSet.builder(comparator);
+
+            // The first column of the slice might not be the first clustering column (e.g. clustering_0 = ? AND (clustering_1, clustering_2) >= (?, ?)
+            int offset = columnDefs.get(0).position();
+
+            for (int i = 0, m = elementsList.size(); i < m; i++)
+            {
+                List<ByteBuffer> elements = elementsList.get(i);
+
+                // Handle the no bound case
+                if (elements.size() == offset)
+                {
+                    set.add(builder.buildBoundWith(elements, isStart, true));
+                    continue;
+                }
+
+                // In the case of mixed order columns, we will have some extra slices where the columns change directions.
+                // For example: if we have clustering_0 DESC and clustering_1 ASC a slice like (clustering_0, clustering_1) > (1, 2)
+                // will produce 2 slices: [BOTTOM, 1) and (1.2, 1]
+                // So, the END bound will return 2 bounds with the same values 1
+                ColumnDefinition lastColumn = columnDefs.get(columnDefs.size() - 1);
+                if (elements.size() <= lastColumn.position() && i < m - 1 && elements.equals(elementsList.get(i + 1)))
+                {
+                    set.add(builder.buildBoundWith(elements, isStart, false));
+                    set.add(builder.buildBoundWith(elementsList.get(i++), isStart, true));
+                    continue;
+                }
+
+                // Handle the normal bounds
+                ColumnDefinition column = columnDefs.get(elements.size() - 1 - offset);
+                set.add(builder.buildBoundWith(elements, isStart, column.isReversedType() ? isOtherBoundInclusive : isInclusive));
+            }
+            return set.build();
+        }
+
+        public NavigableSet<ClusteringBound> buildBound(boolean isStart, boolean isInclusive)
+        {
+            built = true;
+
+            if (hasMissingElements)
+                return BTreeSet.empty(comparator);
+
+            CBuilder builder = CBuilder.create(comparator);
+
+            if (elementsList.isEmpty())
+                return BTreeSet.of(comparator, builder.buildBound(isStart, isInclusive));
+
+            // Use a TreeSet to sort and eliminate duplicates
+            BTreeSet.Builder<ClusteringBound> set = BTreeSet.builder(comparator);
+
+            for (int i = 0, m = elementsList.size(); i < m; i++)
+            {
+                List<ByteBuffer> elements = elementsList.get(i);
+                set.add(builder.buildBoundWith(elements, isStart, isInclusive));
+            }
+            return set.build();
+        }
     }
 }
diff --git a/src/java/org/apache/cassandra/db/MutableDeletionInfo.java b/src/java/org/apache/cassandra/db/MutableDeletionInfo.java
index d01b1d1..39728c5 100644
--- a/src/java/org/apache/cassandra/db/MutableDeletionInfo.java
+++ b/src/java/org/apache/cassandra/db/MutableDeletionInfo.java
@@ -17,10 +17,10 @@
  */
 package org.apache.cassandra.db;
 
+import java.util.Collections;
 import java.util.Iterator;
 
 import com.google.common.base.Objects;
-import com.google.common.collect.Iterators;
 
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.rows.EncodingStats;
@@ -151,12 +151,12 @@
     // Use sparingly, not the most efficient thing
     public Iterator<RangeTombstone> rangeIterator(boolean reversed)
     {
-        return ranges == null ? Iterators.<RangeTombstone>emptyIterator() : ranges.iterator(reversed);
+        return ranges == null ? Collections.emptyIterator() : ranges.iterator(reversed);
     }
 
     public Iterator<RangeTombstone> rangeIterator(Slice slice, boolean reversed)
     {
-        return ranges == null ? Iterators.<RangeTombstone>emptyIterator() : ranges.iterator(slice, reversed);
+        return ranges == null ? Collections.emptyIterator() : ranges.iterator(slice, reversed);
     }
 
     public RangeTombstone rangeCovering(Clustering name)
@@ -290,8 +290,8 @@
                 DeletionTime openDeletion = openMarker.openDeletionTime(reversed);
                 assert marker.closeDeletionTime(reversed).equals(openDeletion);
 
-                Slice.Bound open = openMarker.openBound(reversed);
-                Slice.Bound close = marker.closeBound(reversed);
+                ClusteringBound open = openMarker.openBound(reversed);
+                ClusteringBound close = marker.closeBound(reversed);
 
                 Slice slice = reversed ? Slice.make(close, open) : Slice.make(open, close);
                 deletion.add(new RangeTombstone(slice, openDeletion), comparator);
diff --git a/src/java/org/apache/cassandra/db/Mutation.java b/src/java/org/apache/cassandra/db/Mutation.java
index 7ed69c0..7f19073 100644
--- a/src/java/org/apache/cassandra/db/Mutation.java
+++ b/src/java/org/apache/cassandra/db/Mutation.java
@@ -38,15 +38,12 @@
 import org.apache.cassandra.net.MessageOut;
 import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.utils.ByteBufferUtil;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 // TODO convert this to a Builder pattern instead of encouraging M.add directly,
 // which is less-efficient since we have to keep a mutable HashMap around
 public class Mutation implements IMutation
 {
     public static final MutationSerializer serializer = new MutationSerializer();
-    private static final Logger logger = LoggerFactory.getLogger(Mutation.class);
 
     public static final String FORWARD_TO = "FWD_TO";
     public static final String FORWARD_FROM = "FWD_FRM";
@@ -60,15 +57,22 @@
     private final Map<UUID, PartitionUpdate> modifications;
 
     // Time at which this mutation was instantiated
-    public final long createdAt = System.currentTimeMillis();
+    public final long createdAt;
     // keep track of when mutation has started waiting for a MV partition lock
     public final AtomicLong viewLockAcquireStart = new AtomicLong(0);
 
+    private boolean cdcEnabled = false;
+
     public Mutation(String keyspaceName, DecoratedKey key)
     {
         this(keyspaceName, key, new HashMap<>());
     }
 
+    public Mutation(String keyspaceName, DecoratedKey key, long createdAt, boolean cdcEnabled)
+    {
+        this(keyspaceName, key, new HashMap<>(), createdAt, cdcEnabled);
+    }
+
     public Mutation(PartitionUpdate update)
     {
         this(update.metadata().ksName, update.partitionKey(), Collections.singletonMap(update.metadata().cfId, update));
@@ -76,9 +80,29 @@
 
     protected Mutation(String keyspaceName, DecoratedKey key, Map<UUID, PartitionUpdate> modifications)
     {
+        this(keyspaceName, key, modifications, System.currentTimeMillis());
+    }
+
+    private Mutation(String keyspaceName, DecoratedKey key, Map<UUID, PartitionUpdate> modifications, long createdAt)
+    {
+        this(keyspaceName, key, modifications, createdAt, cdcEnabled(modifications));
+    }
+
+    private Mutation(String keyspaceName, DecoratedKey key, Map<UUID, PartitionUpdate> modifications, long createdAt, boolean cdcEnabled)
+    {
         this.keyspaceName = keyspaceName;
         this.key = key;
         this.modifications = modifications;
+        this.cdcEnabled = cdcEnabled;
+        this.createdAt = createdAt;
+    }
+
+    private static boolean cdcEnabled(Map<UUID, PartitionUpdate> modifications)
+    {
+        boolean cdcEnabled = false;
+        for (PartitionUpdate pu : modifications.values())
+            cdcEnabled |= pu.metadata().params.cdc;
+        return cdcEnabled;
     }
 
     public Mutation copy()
@@ -93,6 +117,11 @@
 
         Mutation copy = copy();
         copy.modifications.keySet().removeAll(cfIds);
+
+        copy.cdcEnabled = false;
+        for (PartitionUpdate pu : modifications.values())
+            copy.cdcEnabled |= pu.metadata().params.cdc;
+
         return copy;
     }
 
@@ -126,10 +155,21 @@
         return modifications.get(cfId);
     }
 
+    /**
+     * Adds PartitionUpdate to the local set of modifications.
+     * Assumes no updates for the Table this PartitionUpdate impacts.
+     *
+     * @param update PartitionUpdate to append to Modifications list
+     * @return Mutation this mutation
+     * @throws IllegalArgumentException If PartitionUpdate for duplicate table is passed as argument
+     */
     public Mutation add(PartitionUpdate update)
     {
         assert update != null;
         assert update.partitionKey().getPartitioner() == key.getPartitioner();
+
+        cdcEnabled |= update.metadata().params.cdc;
+
         PartitionUpdate prev = modifications.put(update.metadata().cfId, update);
         if (prev != null)
             // developer error
@@ -252,6 +292,11 @@
         return gcgs;
     }
 
+    public boolean trackedByCDC()
+    {
+        return cdcEnabled;
+    }
+
     public String toString()
     {
         return toString(false);
@@ -265,7 +310,7 @@
         buff.append(", modifications=[");
         if (shallow)
         {
-            List<String> cfnames = new ArrayList<String>(modifications.size());
+            List<String> cfnames = new ArrayList<>(modifications.size());
             for (UUID cfid : modifications.keySet())
             {
                 CFMetaData cfm = Schema.instance.getCFMetaData(cfid);
@@ -275,11 +320,77 @@
         }
         else
         {
-            buff.append("\n  ").append(StringUtils.join(modifications.values(), "\n  ")).append("\n");
+            buff.append("\n  ").append(StringUtils.join(modifications.values(), "\n  ")).append('\n');
         }
         return buff.append("])").toString();
     }
 
+    /**
+     * Creates a new simple mutuation builder.
+     *
+     * @param keyspaceName the name of the keyspace this is a mutation for.
+     * @param partitionKey the key of partition this if a mutation for.
+     * @return a newly created builder.
+     */
+    public static SimpleBuilder simpleBuilder(String keyspaceName, DecoratedKey partitionKey)
+    {
+        return new SimpleBuilders.MutationBuilder(keyspaceName, partitionKey);
+    }
+
+    /**
+     * Interface for building mutations geared towards human.
+     * <p>
+     * This should generally not be used when performance matters too much, but provides a more convenient interface to
+     * build a mutation than using the class constructor when performance is not of the utmost importance.
+     */
+    public interface SimpleBuilder
+    {
+        /**
+         * Sets the timestamp to use for the following additions to this builder or any derived (update or row) builder.
+         *
+         * @param timestamp the timestamp to use for following additions. If that timestamp hasn't been set, the current
+         * time in microseconds will be used.
+         * @return this builder.
+         */
+        public SimpleBuilder timestamp(long timestamp);
+
+        /**
+         * Sets the ttl to use for the following additions to this builder or any derived (update or row) builder.
+         * <p>
+         * Note that the for non-compact tables, this method must be called before any column addition for this
+         * ttl to be used for the row {@code LivenessInfo}.
+         *
+         * @param ttl the ttl to use for following additions. If that ttl hasn't been set, no ttl will be used.
+         * @return this builder.
+         */
+        public SimpleBuilder ttl(int ttl);
+
+        /**
+         * Adds an update for table identified by the provided metadata and return a builder for that partition.
+         *
+         * @param metadata the metadata of the table for which to add an update.
+         * @return a builder for the partition identified by {@code metadata} (and the partition key for which this is a
+         * mutation of).
+         */
+        public PartitionUpdate.SimpleBuilder update(CFMetaData metadata);
+
+        /**
+         * Adds an update for table identified by the provided name and return a builder for that partition.
+         *
+         * @param tableName the name of the table for which to add an update.
+         * @return a builder for the partition identified by {@code metadata} (and the partition key for which this is a
+         * mutation of).
+         */
+        public PartitionUpdate.SimpleBuilder update(String tableName);
+
+        /**
+         * Build the mutation represented by this builder.
+         *
+         * @return the built mutation.
+         */
+        public Mutation build();
+    }
+
     public static class MutationSerializer implements IVersionedSerializer<Mutation>
     {
         public void serialize(Mutation mutation, DataOutputPlus out, int version) throws IOException
diff --git a/src/java/org/apache/cassandra/db/NativeClustering.java b/src/java/org/apache/cassandra/db/NativeClustering.java
new file mode 100644
index 0000000..e96435b
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/NativeClustering.java
@@ -0,0 +1,130 @@
+/*
+* 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.db;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.concurrent.OpOrder;
+import org.apache.cassandra.utils.memory.MemoryUtil;
+import org.apache.cassandra.utils.memory.NativeAllocator;
+
+public class NativeClustering extends AbstractClusteringPrefix implements Clustering
+{
+    private static final long EMPTY_SIZE = ObjectSizes.measure(new NativeClustering());
+
+    private final long peer;
+
+    private NativeClustering() { peer = 0; }
+
+    public NativeClustering(NativeAllocator allocator, OpOrder.Group writeOp, Clustering clustering)
+    {
+        int count = clustering.size();
+        int metadataSize = (count * 2) + 4;
+        int dataSize = clustering.dataSize();
+        int bitmapSize = ((count + 7) >>> 3);
+
+        assert count < 64 << 10;
+        assert dataSize < 64 << 10;
+
+        peer = allocator.allocate(metadataSize + dataSize + bitmapSize, writeOp);
+        long bitmapStart = peer + metadataSize;
+        MemoryUtil.setShort(peer, (short) count);
+        MemoryUtil.setShort(peer + (metadataSize - 2), (short) dataSize); // goes at the end of the other offsets
+
+        MemoryUtil.setByte(bitmapStart, bitmapSize, (byte) 0);
+        long dataStart = peer + metadataSize + bitmapSize;
+        int dataOffset = 0;
+        for (int i = 0 ; i < count ; i++)
+        {
+            MemoryUtil.setShort(peer + 2 + i * 2, (short) dataOffset);
+
+            ByteBuffer value = clustering.get(i);
+            if (value == null)
+            {
+                long boffset = bitmapStart + (i >>> 3);
+                int b = MemoryUtil.getByte(boffset);
+                b |= 1 << (i & 7);
+                MemoryUtil.setByte(boffset, (byte) b);
+                continue;
+            }
+
+            assert value.order() == ByteOrder.BIG_ENDIAN;
+
+            int size = value.remaining();
+            MemoryUtil.setBytes(dataStart + dataOffset, value);
+            dataOffset += size;
+        }
+    }
+
+    public Kind kind()
+    {
+        return Kind.CLUSTERING;
+    }
+
+    public int size()
+    {
+        return MemoryUtil.getShort(peer);
+    }
+
+    public ByteBuffer get(int i)
+    {
+        // offset at which we store the dataOffset
+        int size = size();
+        if (i >= size)
+            throw new IndexOutOfBoundsException();
+
+        int metadataSize = (size * 2) + 4;
+        int bitmapSize = ((size + 7) >>> 3);
+        long bitmapStart = peer + metadataSize;
+        int b = MemoryUtil.getByte(bitmapStart + (i >>> 3));
+        if ((b & (1 << (i & 7))) != 0)
+            return null;
+
+        int startOffset = MemoryUtil.getShort(peer + 2 + i * 2);
+        int endOffset = MemoryUtil.getShort(peer + 4 + i * 2);
+        return MemoryUtil.getByteBuffer(bitmapStart + bitmapSize + startOffset,
+                                        endOffset - startOffset,
+                                        ByteOrder.BIG_ENDIAN);
+    }
+
+    public ByteBuffer[] getRawValues()
+    {
+        ByteBuffer[] values = new ByteBuffer[size()];
+        for (int i = 0 ; i < values.length ; i++)
+            values[i] = get(i);
+        return values;
+    }
+
+    public long unsharedHeapSize()
+    {
+        return EMPTY_SIZE;
+    }
+
+    public long unsharedHeapSizeExcludingData()
+    {
+        return EMPTY_SIZE;
+    }
+
+    public ClusteringPrefix minimize()
+    {
+        return this;
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/NativeDecoratedKey.java b/src/java/org/apache/cassandra/db/NativeDecoratedKey.java
index ca874c3..add5218 100644
--- a/src/java/org/apache/cassandra/db/NativeDecoratedKey.java
+++ b/src/java/org/apache/cassandra/db/NativeDecoratedKey.java
@@ -18,6 +18,7 @@
 package org.apache.cassandra.db;
 
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.utils.concurrent.OpOrder;
@@ -32,6 +33,8 @@
     {
         super(token);
         assert key != null;
+        assert key.order() == ByteOrder.BIG_ENDIAN;
+
         int size = key.remaining();
         this.peer = allocator.allocate(4 + size, writeOp);
         MemoryUtil.setInt(peer, size);
@@ -40,6 +43,6 @@
 
     public ByteBuffer getKey()
     {
-        return MemoryUtil.getByteBuffer(peer + 4, MemoryUtil.getInt(peer));
+        return MemoryUtil.getByteBuffer(peer + 4, MemoryUtil.getInt(peer), ByteOrder.BIG_ENDIAN);
     }
 }
diff --git a/src/java/org/apache/cassandra/db/PartitionPosition.java b/src/java/org/apache/cassandra/db/PartitionPosition.java
index ac5258d..3b45c6c 100644
--- a/src/java/org/apache/cassandra/db/PartitionPosition.java
+++ b/src/java/org/apache/cassandra/db/PartitionPosition.java
@@ -23,7 +23,6 @@
 
 import org.apache.cassandra.dht.*;
 import org.apache.cassandra.io.util.DataOutputPlus;
-import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public interface PartitionPosition extends RingPosition<PartitionPosition>
diff --git a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java
index 1da66c1..30c3034 100644
--- a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java
+++ b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java
@@ -49,6 +49,7 @@
 import org.apache.cassandra.service.pager.*;
 import org.apache.cassandra.thrift.ThriftResultsMerger;
 import org.apache.cassandra.tracing.Tracing;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.FBUtilities;
 
 /**
@@ -131,8 +132,28 @@
         return dataRange.isNamesQuery();
     }
 
-    public PartitionRangeReadCommand forSubRange(AbstractBounds<PartitionPosition> range)
+    /**
+     * Returns an equivalent command but that only queries data within the provided range.
+     *
+     * @param range the sub-range to restrict the command to. This method <b>assumes</b> that this is a proper sub-range
+     * of the command this is applied to.
+     * @param isRangeContinuation whether {@code range} is a direct continuation of whatever previous range we have
+     * queried. This matters for the {@code DataLimits} that may contain states when we do paging and in the context of
+     * parallel queries: that state only make sense if the range queried is indeed the follow-up of whatever range we've
+     * previously query (that yield said state). In practice this means that ranges for which {@code isRangeContinuation}
+     * is false may have to be slightly pessimistic when counting data and may include a little bit than necessary, and
+     * this should be dealt with post-query (in the case of {@code StorageProxy.getRangeSlice()}, which uses this method
+     * for replica queries, this is dealt with by re-counting results on the coordinator). Note that if this is the
+     * first range we queried, then the {@code DataLimits} will have not state and the value of this parameter doesn't
+     * matter.
+     */
+    public PartitionRangeReadCommand forSubRange(AbstractBounds<PartitionPosition> range, boolean isRangeContinuation)
     {
+        // If we're not a continuation of whatever range we've previously queried, we should ignore the states of the
+        // DataLimits as it's either useless, or misleading. This is particularly important for GROUP BY queries, where
+        // DataLimits.CQLGroupByLimits.GroupByAwareCounter assumes that if GroupingState.hasClustering(), then we're in
+        // the middle of a group, but we can't make that assumption if we query and range "in advance" of where we are
+        // on the ring.
         return new PartitionRangeReadCommand(isDigestQuery(),
                                              digestVersion(),
                                              isForThrift(),
@@ -140,7 +161,7 @@
                                              nowInSec(),
                                              columnFilter(),
                                              rowFilter(),
-                                             limits(),
+                                             isRangeContinuation ? limits() : limits().withoutState(),
                                              dataRange().forSubRange(range),
                                              indexMetadata());
     }
@@ -173,6 +194,20 @@
                                              indexMetadata());
     }
 
+    public ReadCommand withUpdatedLimit(DataLimits newLimits)
+    {
+        return new PartitionRangeReadCommand(isDigestQuery(),
+                                             digestVersion(),
+                                             isForThrift(),
+                                             metadata(),
+                                             nowInSec(),
+                                             columnFilter(),
+                                             rowFilter(),
+                                             newLimits,
+                                             dataRange(),
+                                             indexMetadata());
+    }
+
     public PartitionRangeReadCommand withUpdatedDataRange(DataRange newDataRange)
     {
         return new PartitionRangeReadCommand(isDigestQuery(),
@@ -229,12 +264,12 @@
         return rowFilter().clusteringKeyRestrictionsAreSatisfiedBy(clustering);
     }
 
-    public PartitionIterator execute(ConsistencyLevel consistency, ClientState clientState) throws RequestExecutionException
+    public PartitionIterator execute(ConsistencyLevel consistency, ClientState clientState, long queryStartNanoTime) throws RequestExecutionException
     {
-        return StorageProxy.getRangeSlice(this, consistency);
+        return StorageProxy.getRangeSlice(this, consistency, queryStartNanoTime);
     }
 
-    public QueryPager getPager(PagingState pagingState, int protocolVersion)
+    public QueryPager getPager(PagingState pagingState, ProtocolVersion protocolVersion)
     {
             return new PartitionRangeQueryPager(this, pagingState, protocolVersion);
     }
@@ -245,7 +280,7 @@
     }
 
     @VisibleForTesting
-    public UnfilteredPartitionIterator queryStorage(final ColumnFamilyStore cfs, ReadOrderGroup orderGroup)
+    public UnfilteredPartitionIterator queryStorage(final ColumnFamilyStore cfs, ReadExecutionController executionController)
     {
         ColumnFamilyStore.ViewFragment view = cfs.select(View.selectLive(dataRange().keyRange()));
         Tracing.trace("Executing seq scan across {} sstables for {}", view.sstables.size(), dataRange().keyRange().getString(metadata().getKeyValidator()));
@@ -281,6 +316,7 @@
                 if (!sstable.isRepaired())
                     oldestUnrepairedTombstone = Math.min(oldestUnrepairedTombstone, sstable.getMinLocalDeletionTime());
             }
+            // iterators can be empty for offline tools
             return iterators.isEmpty() ? EmptyIterators.unfilteredPartition(metadata(), isForThrift())
                                        : checkCacheFilter(UnfilteredPartitionIterators.mergeLazily(iterators, nowInSec()), cfs);
         }
diff --git a/src/java/org/apache/cassandra/db/RangeTombstone.java b/src/java/org/apache/cassandra/db/RangeTombstone.java
index 4a26581..eb9725a 100644
--- a/src/java/org/apache/cassandra/db/RangeTombstone.java
+++ b/src/java/org/apache/cassandra/db/RangeTombstone.java
@@ -17,18 +17,9 @@
  */
 package org.apache.cassandra.db;
 
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.List;
 import java.util.Objects;
 
-import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.rows.RangeTombstoneMarker;
-import org.apache.cassandra.io.util.DataInputPlus;
-import org.apache.cassandra.io.util.DataOutputPlus;
-import org.apache.cassandra.utils.ByteBufferUtil;
-import org.apache.cassandra.utils.memory.AbstractAllocator;
-
 
 /**
  * A range tombstone is a tombstone that covers a slice/range of rows.
@@ -90,136 +81,4 @@
     {
         return Objects.hash(deletedSlice(), deletionTime());
     }
-
-    /**
-     * The bound of a range tombstone.
-     * <p>
-     * This is the same than for a slice but it includes "boundaries" between ranges. A boundary simply condensed
-     * a close and an opening "bound" into a single object. There is 2 main reasons for these "shortcut" boundaries:
-     *   1) When merging multiple iterators having range tombstones (that are represented by their start and end markers),
-     *      we need to know when a range is close on an iterator, if it is reopened right away. Otherwise, we cannot
-     *      easily produce the markers on the merged iterators within risking to fail the sorting guarantees of an
-     *      iterator. See this comment for more details: https://goo.gl/yyB5mR.
-     *   2) This saves some storage space.
-     */
-    public static class Bound extends Slice.Bound
-    {
-        public static final Serializer serializer = new Serializer();
-
-        /** The smallest start bound, i.e. the one that starts before any row. */
-        public static final Bound BOTTOM = new Bound(Kind.INCL_START_BOUND, EMPTY_VALUES_ARRAY);
-        /** The biggest end bound, i.e. the one that ends after any row. */
-        public static final Bound TOP = new Bound(Kind.INCL_END_BOUND, EMPTY_VALUES_ARRAY);
-
-        public Bound(Kind kind, ByteBuffer[] values)
-        {
-            super(kind, values);
-            assert values.length > 0 || !kind.isBoundary();
-        }
-
-        public boolean isBoundary()
-        {
-            return kind.isBoundary();
-        }
-
-        public boolean isOpen(boolean reversed)
-        {
-            return kind.isOpen(reversed);
-        }
-
-        public boolean isClose(boolean reversed)
-        {
-            return kind.isClose(reversed);
-        }
-
-        public static RangeTombstone.Bound inclusiveOpen(boolean reversed, ByteBuffer[] boundValues)
-        {
-            return new Bound(reversed ? Kind.INCL_END_BOUND : Kind.INCL_START_BOUND, boundValues);
-        }
-
-        public static RangeTombstone.Bound exclusiveOpen(boolean reversed, ByteBuffer[] boundValues)
-        {
-            return new Bound(reversed ? Kind.EXCL_END_BOUND : Kind.EXCL_START_BOUND, boundValues);
-        }
-
-        public static RangeTombstone.Bound inclusiveClose(boolean reversed, ByteBuffer[] boundValues)
-        {
-            return new Bound(reversed ? Kind.INCL_START_BOUND : Kind.INCL_END_BOUND, boundValues);
-        }
-
-        public static RangeTombstone.Bound exclusiveClose(boolean reversed, ByteBuffer[] boundValues)
-        {
-            return new Bound(reversed ? Kind.EXCL_START_BOUND : Kind.EXCL_END_BOUND, boundValues);
-        }
-
-        public static RangeTombstone.Bound inclusiveCloseExclusiveOpen(boolean reversed, ByteBuffer[] boundValues)
-        {
-            return new Bound(reversed ? Kind.EXCL_END_INCL_START_BOUNDARY : Kind.INCL_END_EXCL_START_BOUNDARY, boundValues);
-        }
-
-        public static RangeTombstone.Bound exclusiveCloseInclusiveOpen(boolean reversed, ByteBuffer[] boundValues)
-        {
-            return new Bound(reversed ? Kind.INCL_END_EXCL_START_BOUNDARY : Kind.EXCL_END_INCL_START_BOUNDARY, boundValues);
-        }
-
-        public static RangeTombstone.Bound fromSliceBound(Slice.Bound sliceBound)
-        {
-            return new RangeTombstone.Bound(sliceBound.kind(), sliceBound.getRawValues());
-        }
-
-        public RangeTombstone.Bound copy(AbstractAllocator allocator)
-        {
-            ByteBuffer[] newValues = new ByteBuffer[size()];
-            for (int i = 0; i < size(); i++)
-                newValues[i] = allocator.clone(get(i));
-            return new Bound(kind(), newValues);
-        }
-
-        public ClusteringPrefix minimize()
-        {
-            if (!ByteBufferUtil.canMinimize(values))
-                return this;
-            return new Bound(kind, ByteBufferUtil.minimizeBuffers(values));
-        }
-
-        @Override
-        public Bound withNewKind(Kind kind)
-        {
-            return new Bound(kind, values);
-        }
-
-        public static class Serializer
-        {
-            public void serialize(RangeTombstone.Bound bound, DataOutputPlus out, int version, List<AbstractType<?>> types) throws IOException
-            {
-                out.writeByte(bound.kind().ordinal());
-                out.writeShort(bound.size());
-                ClusteringPrefix.serializer.serializeValuesWithoutSize(bound, out, version, types);
-            }
-
-            public long serializedSize(RangeTombstone.Bound bound, int version, List<AbstractType<?>> types)
-            {
-                return 1 // kind ordinal
-                     + TypeSizes.sizeof((short)bound.size())
-                     + ClusteringPrefix.serializer.valuesWithoutSizeSerializedSize(bound, version, types);
-            }
-
-            public RangeTombstone.Bound deserialize(DataInputPlus in, int version, List<AbstractType<?>> types) throws IOException
-            {
-                Kind kind = Kind.values()[in.readByte()];
-                return deserializeValues(in, kind, version, types);
-            }
-
-            public RangeTombstone.Bound deserializeValues(DataInputPlus in, Kind kind, int version,
-                    List<AbstractType<?>> types) throws IOException
-            {
-                int size = in.readUnsignedShort();
-                if (size == 0)
-                    return kind.isStart() ? BOTTOM : TOP;
-
-                ByteBuffer[] values = ClusteringPrefix.serializer.deserializeValuesWithoutSize(in, size, version, types);
-                return new RangeTombstone.Bound(kind, values);
-            }
-        }
-    }
 }
diff --git a/src/java/org/apache/cassandra/db/RangeTombstoneList.java b/src/java/org/apache/cassandra/db/RangeTombstoneList.java
index ad91e72..1aa20c1 100644
--- a/src/java/org/apache/cassandra/db/RangeTombstoneList.java
+++ b/src/java/org/apache/cassandra/db/RangeTombstoneList.java
@@ -38,7 +38,7 @@
  * A range tombstone has 4 elements: the start and end of the range covered,
  * and the deletion infos (markedAt timestamp and local deletion time). The
  * markedAt timestamp is what define the priority of 2 overlapping tombstones.
- * That is, given 2 tombstones [0, 10]@t1 and [5, 15]@t2, then if t2 > t1 (and
+ * That is, given 2 tombstones {@code [0, 10]@t1 and [5, 15]@t2, then if t2 > t1} (and
  * are the tombstones markedAt values), the 2nd tombstone take precedence over
  * the first one on [5, 10]. If such tombstones are added to a RangeTombstoneList,
  * the range tombstone list will store them as [[0, 5]@t1, [5, 15]@t2].
@@ -54,15 +54,15 @@
 
     // Note: we don't want to use a List for the markedAts and delTimes to avoid boxing. We could
     // use a List for starts and ends, but having arrays everywhere is almost simpler.
-    private Slice.Bound[] starts;
-    private Slice.Bound[] ends;
+    private ClusteringBound[] starts;
+    private ClusteringBound[] ends;
     private long[] markedAts;
     private int[] delTimes;
 
     private long boundaryHeapSize;
     private int size;
 
-    private RangeTombstoneList(ClusteringComparator comparator, Slice.Bound[] starts, Slice.Bound[] ends, long[] markedAts, int[] delTimes, long boundaryHeapSize, int size)
+    private RangeTombstoneList(ClusteringComparator comparator, ClusteringBound[] starts, ClusteringBound[] ends, long[] markedAts, int[] delTimes, long boundaryHeapSize, int size)
     {
         assert starts.length == ends.length && starts.length == markedAts.length && starts.length == delTimes.length;
         this.comparator = comparator;
@@ -76,7 +76,7 @@
 
     public RangeTombstoneList(ClusteringComparator comparator, int capacity)
     {
-        this(comparator, new Slice.Bound[capacity], new Slice.Bound[capacity], new long[capacity], new int[capacity], 0, 0);
+        this(comparator, new ClusteringBound[capacity], new ClusteringBound[capacity], new long[capacity], new int[capacity], 0, 0);
     }
 
     public boolean isEmpty()
@@ -107,8 +107,8 @@
     public RangeTombstoneList copy(AbstractAllocator allocator)
     {
         RangeTombstoneList copy =  new RangeTombstoneList(comparator,
-                                                          new Slice.Bound[size],
-                                                          new Slice.Bound[size],
+                                                          new ClusteringBound[size],
+                                                          new ClusteringBound[size],
                                                           Arrays.copyOf(markedAts, size),
                                                           Arrays.copyOf(delTimes, size),
                                                           boundaryHeapSize, size);
@@ -123,12 +123,12 @@
         return copy;
     }
 
-    private static Slice.Bound clone(Slice.Bound bound, AbstractAllocator allocator)
+    private static ClusteringBound clone(ClusteringBound bound, AbstractAllocator allocator)
     {
         ByteBuffer[] values = new ByteBuffer[bound.size()];
         for (int i = 0; i < values.length; i++)
             values[i] = allocator.clone(bound.get(i));
-        return new Slice.Bound(bound.kind(), values);
+        return new ClusteringBound(bound.kind(), values);
     }
 
     public void add(RangeTombstone tombstone)
@@ -145,7 +145,7 @@
      * This method will be faster if the new tombstone sort after all the currently existing ones (this is a common use case),
      * but it doesn't assume it.
      */
-    public void add(Slice.Bound start, Slice.Bound end, long markedAt, int delTime)
+    public void add(ClusteringBound start, ClusteringBound end, long markedAt, int delTime)
     {
         if (isEmpty())
         {
@@ -324,17 +324,17 @@
         return new RangeTombstone(Slice.make(starts[idx], ends[idx]), new DeletionTime(markedAts[idx], delTimes[idx]));
     }
 
-    private RangeTombstone rangeTombstoneWithNewStart(int idx, Slice.Bound newStart)
+    private RangeTombstone rangeTombstoneWithNewStart(int idx, ClusteringBound newStart)
     {
         return new RangeTombstone(Slice.make(newStart, ends[idx]), new DeletionTime(markedAts[idx], delTimes[idx]));
     }
 
-    private RangeTombstone rangeTombstoneWithNewEnd(int idx, Slice.Bound newEnd)
+    private RangeTombstone rangeTombstoneWithNewEnd(int idx, ClusteringBound newEnd)
     {
         return new RangeTombstone(Slice.make(starts[idx], newEnd), new DeletionTime(markedAts[idx], delTimes[idx]));
     }
 
-    private RangeTombstone rangeTombstoneWithNewBounds(int idx, Slice.Bound newStart, Slice.Bound newEnd)
+    private RangeTombstone rangeTombstoneWithNewBounds(int idx, ClusteringBound newStart, ClusteringBound newEnd)
     {
         return new RangeTombstone(Slice.make(newStart, newEnd), new DeletionTime(markedAts[idx], delTimes[idx]));
     }
@@ -380,13 +380,13 @@
 
     private Iterator<RangeTombstone> forwardIterator(final Slice slice)
     {
-        int startIdx = slice.start() == Slice.Bound.BOTTOM ? 0 : searchInternal(slice.start(), 0, size);
+        int startIdx = slice.start() == ClusteringBound.BOTTOM ? 0 : searchInternal(slice.start(), 0, size);
         final int start = startIdx < 0 ? -startIdx-1 : startIdx;
 
         if (start >= size)
             return Collections.emptyIterator();
 
-        int finishIdx = slice.end() == Slice.Bound.TOP ? size - 1 : searchInternal(slice.end(), start, size);
+        int finishIdx = slice.end() == ClusteringBound.TOP ? size - 1 : searchInternal(slice.end(), start, size);
         // if stopIdx is the first range after 'slice.end()' we care only until the previous range
         final int finish = finishIdx < 0 ? -finishIdx-2 : finishIdx;
 
@@ -397,8 +397,8 @@
         {
             // We want to make sure the range are stricly included within the queried slice as this
             // make it easier to combine things when iterating over successive slices.
-            Slice.Bound s = comparator.compare(starts[start], slice.start()) < 0 ? slice.start() : starts[start];
-            Slice.Bound e = comparator.compare(slice.end(), ends[start]) < 0 ? slice.end() : ends[start];
+            ClusteringBound s = comparator.compare(starts[start], slice.start()) < 0 ? slice.start() : starts[start];
+            ClusteringBound e = comparator.compare(slice.end(), ends[start]) < 0 ? slice.end() : ends[start];
             if (Slice.isEmpty(comparator, s, e))
                 return Collections.emptyIterator();
             return Iterators.<RangeTombstone>singletonIterator(rangeTombstoneWithNewBounds(start, s, e));
@@ -427,14 +427,14 @@
 
     private Iterator<RangeTombstone> reverseIterator(final Slice slice)
     {
-        int startIdx = slice.end() == Slice.Bound.TOP ? size - 1 : searchInternal(slice.end(), 0, size);
+        int startIdx = slice.end() == ClusteringBound.TOP ? size - 1 : searchInternal(slice.end(), 0, size);
         // if startIdx is the first range after 'slice.end()' we care only until the previous range
         final int start = startIdx < 0 ? -startIdx-2 : startIdx;
 
         if (start < 0)
             return Collections.emptyIterator();
 
-        int finishIdx = slice.start() == Slice.Bound.BOTTOM ? 0 : searchInternal(slice.start(), 0, start + 1);  // include same as finish
+        int finishIdx = slice.start() == ClusteringBound.BOTTOM ? 0 : searchInternal(slice.start(), 0, start + 1);  // include same as finish
         // if stopIdx is the first range after 'slice.end()' we care only until the previous range
         final int finish = finishIdx < 0 ? -finishIdx-1 : finishIdx;
 
@@ -445,8 +445,8 @@
         {
             // We want to make sure the range are stricly included within the queried slice as this
             // make it easier to combine things when iterator over successive slices.
-            Slice.Bound s = comparator.compare(starts[start], slice.start()) < 0 ? slice.start() : starts[start];
-            Slice.Bound e = comparator.compare(slice.end(), ends[start]) < 0 ? slice.end() : ends[start];
+            ClusteringBound s = comparator.compare(starts[start], slice.start()) < 0 ? slice.start() : starts[start];
+            ClusteringBound e = comparator.compare(slice.end(), ends[start]) < 0 ? slice.end() : ends[start];
             if (Slice.isEmpty(comparator, s, e))
                 return Collections.emptyIterator();
             return Iterators.<RangeTombstone>singletonIterator(rangeTombstoneWithNewBounds(start, s, e));
@@ -460,7 +460,6 @@
             {
                 if (idx < 0 || idx < finish)
                     return endOfData();
-
                 // We want to make sure the range are stricly included within the queried slice as this
                 // make it easier to combine things when iterator over successive slices. This means that
                 // for the first and last range we might have to "cut" the range returned.
@@ -532,7 +531,7 @@
      *   - e_i <= s_i+1
      * Basically, range are non overlapping and in order.
      */
-    private void insertFrom(int i, Slice.Bound start, Slice.Bound end, long markedAt, int delTime)
+    private void insertFrom(int i, ClusteringBound start, ClusteringBound end, long markedAt, int delTime)
     {
         while (i < size)
         {
@@ -551,10 +550,10 @@
                 // First deal with what might come before the newly added one.
                 if (comparator.compare(starts[i], start) < 0)
                 {
-                    Slice.Bound newEnd = start.invert();
+                    ClusteringBound newEnd = start.invert();
                     if (!Slice.isEmpty(comparator, starts[i], newEnd))
                     {
-                        addInternal(i, starts[i], start.invert(), markedAts[i], delTimes[i]);
+                        addInternal(i, starts[i], newEnd, markedAts[i], delTimes[i]);
                         i++;
                         setInternal(i, start, ends[i], markedAts[i], delTimes[i]);
                     }
@@ -599,7 +598,7 @@
                     // one to reflect the not overwritten parts. We're then done.
                     addInternal(i, start, end, markedAt, delTime);
                     i++;
-                    Slice.Bound newStart = end.invert();
+                    ClusteringBound newStart = end.invert();
                     if (!Slice.isEmpty(comparator, newStart, ends[i]))
                     {
                         setInternal(i, newStart, ends[i], markedAts[i], delTimes[i]);
@@ -621,7 +620,7 @@
                         addInternal(i, start, end, markedAt, delTime);
                         return;
                     }
-                    Slice.Bound newEnd = starts[i].invert();
+                    ClusteringBound newEnd = starts[i].invert();
                     if (!Slice.isEmpty(comparator, start, newEnd))
                     {
                         addInternal(i, start, newEnd, markedAt, delTime);
@@ -653,7 +652,7 @@
     /*
      * Adds the new tombstone at index i, growing and/or moving elements to make room for it.
      */
-    private void addInternal(int i, Slice.Bound start, Slice.Bound end, long markedAt, int delTime)
+    private void addInternal(int i, ClusteringBound start, ClusteringBound end, long markedAt, int delTime)
     {
         assert i >= 0;
 
@@ -692,12 +691,12 @@
         delTimes = grow(delTimes, size, newLength, i);
     }
 
-    private static Slice.Bound[] grow(Slice.Bound[] a, int size, int newLength, int i)
+    private static ClusteringBound[] grow(ClusteringBound[] a, int size, int newLength, int i)
     {
         if (i < 0 || i >= size)
             return Arrays.copyOf(a, newLength);
 
-        Slice.Bound[] newA = new Slice.Bound[newLength];
+        ClusteringBound[] newA = new ClusteringBound[newLength];
         System.arraycopy(a, 0, newA, 0, i);
         System.arraycopy(a, i, newA, i+1, size - i);
         return newA;
@@ -742,7 +741,7 @@
         starts[i] = null;
     }
 
-    private void setInternal(int i, Slice.Bound start, Slice.Bound end, long markedAt, int delTime)
+    private void setInternal(int i, ClusteringBound start, ClusteringBound end, long markedAt, int delTime)
     {
         if (starts[i] != null)
             boundaryHeapSize -= starts[i].unsharedHeapSize() + ends[i].unsharedHeapSize();
diff --git a/src/java/org/apache/cassandra/db/ReadCommand.java b/src/java/org/apache/cassandra/db/ReadCommand.java
index 4fd4b5f..548b0c7 100644
--- a/src/java/org/apache/cassandra/db/ReadCommand.java
+++ b/src/java/org/apache/cassandra/db/ReadCommand.java
@@ -31,11 +31,14 @@
 import org.apache.cassandra.config.*;
 import org.apache.cassandra.cql3.Operator;
 import org.apache.cassandra.db.filter.*;
+import org.apache.cassandra.db.monitoring.ApproximateTime;
+import org.apache.cassandra.db.monitoring.MonitorableImpl;
 import org.apache.cassandra.db.partitions.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.transform.RTBoundCloser;
 import org.apache.cassandra.db.transform.RTBoundValidator;
 import org.apache.cassandra.db.transform.RTBoundValidator.Stage;
+import org.apache.cassandra.db.transform.StoppingTransformation;
 import org.apache.cassandra.db.transform.Transformation;
 import org.apache.cassandra.dht.AbstractBounds;
 import org.apache.cassandra.index.Index;
@@ -52,6 +55,7 @@
 import org.apache.cassandra.service.ClientWarn;
 import org.apache.cassandra.tracing.Tracing;
 import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.Pair;
 
 /**
@@ -60,8 +64,9 @@
  * <p>
  * This contains all the informations needed to do a local read.
  */
-public abstract class ReadCommand implements ReadQuery
+public abstract class ReadCommand extends MonitorableImpl implements ReadQuery
 {
+    private static final int TEST_ITERATION_DELAY_MILLIS = Integer.parseInt(System.getProperty("cassandra.test.read_iteration_delay_ms", "0"));
     protected static final Logger logger = LoggerFactory.getLogger(ReadCommand.class);
     public static final IVersionedSerializer<ReadCommand> serializer = new Serializer();
 
@@ -175,6 +180,14 @@
     public abstract boolean isLimitedToOnePartition();
 
     /**
+     * Creates a new <code>ReadCommand</code> instance with new limits.
+     *
+     * @param newLimits the new limits
+     * @return a new <code>ReadCommand</code> with the updated limits
+     */
+    public abstract ReadCommand withUpdatedLimit(DataLimits newLimits);
+
+    /**
      * The metadata for the table queried.
      *
      * @return the metadata for the table queried.
@@ -325,7 +338,7 @@
      */
     public abstract ReadCommand copyAsDigestQuery();
 
-    protected abstract UnfilteredPartitionIterator queryStorage(ColumnFamilyStore cfs, ReadOrderGroup orderGroup);
+    protected abstract UnfilteredPartitionIterator queryStorage(ColumnFamilyStore cfs, ReadExecutionController executionController);
 
     protected abstract int oldestUnrepairedTombstone();
 
@@ -391,13 +404,13 @@
     /**
      * Executes this command on the local host.
      *
-     * @param orderGroup the operation group spanning this command
+     * @param executionController the execution controller spanning this command
      *
      * @return an iterator over the result of executing this command locally.
      */
     @SuppressWarnings("resource") // The result iterator is closed upon exceptions (we know it's fine to potentially not close the intermediary
                                   // iterators created inside the try as long as we do close the original resultIterator), or by closing the result.
-    public UnfilteredPartitionIterator executeLocally(ReadOrderGroup orderGroup)
+    public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController)
     {
         long startTimeNanos = System.nanoTime();
 
@@ -414,13 +427,13 @@
             Tracing.trace("Executing read on {}.{} using index {}", cfs.metadata.ksName, cfs.metadata.cfName, index.getIndexMetadata().name);
         }
 
-        UnfilteredPartitionIterator iterator = (null == searcher) ? queryStorage(cfs, orderGroup) : searcher.search(orderGroup);
+        UnfilteredPartitionIterator iterator = (null == searcher) ? queryStorage(cfs, executionController) : searcher.search(executionController);
         iterator = RTBoundValidator.validate(iterator, Stage.MERGED, false);
 
         try
         {
+            iterator = withStateTracking(iterator);
             iterator = RTBoundValidator.validate(withoutPurgeableTombstones(iterator, cfs), Stage.PURGED, false);
-
             iterator = withMetricsRecording(iterator, cfs.metric, startTimeNanos);
 
             // If we've used a 2ndary index, we know the result already satisfy the primary expression used, so
@@ -451,14 +464,14 @@
 
     protected abstract void recordLatency(TableMetrics metric, long latencyNanos);
 
-    public PartitionIterator executeInternal(ReadOrderGroup orderGroup)
+    public PartitionIterator executeInternal(ReadExecutionController controller)
     {
-        return UnfilteredPartitionIterators.filter(executeLocally(orderGroup), nowInSec());
+        return UnfilteredPartitionIterators.filter(executeLocally(controller), nowInSec());
     }
 
-    public ReadOrderGroup startOrderGroup()
+    public ReadExecutionController executionController()
     {
-        return ReadOrderGroup.forCommand(this);
+        return ReadExecutionController.forCommand(this);
     }
 
     /**
@@ -472,7 +485,7 @@
             private final int failureThreshold = DatabaseDescriptor.getTombstoneFailureThreshold();
             private final int warningThreshold = DatabaseDescriptor.getTombstoneWarnThreshold();
 
-            private final boolean respectTombstoneThresholds = !Schema.isLocalSystemKeyspace(ReadCommand.this.metadata().ksName);
+            private final boolean respectTombstoneThresholds = !SchemaConstants.isLocalSystemKeyspace(ReadCommand.this.metadata().ksName);
             private final boolean enforceStrictLiveness = metadata.enforceStrictLiveness();
 
             private int liveRows = 0;
@@ -493,17 +506,37 @@
                 return applyToRow(row);
             }
 
+            /**
+             * Count the number of live rows returned by the read command and the number of tombstones.
+             *
+             * Tombstones come in two forms on rows :
+             * - cells that aren't live anymore (either expired through TTL or deleted) : 1 tombstone per cell
+             * - Rows that aren't live and have no cell (DELETEs performed on the primary key) : 1 tombstone per row 
+             * We avoid counting rows as tombstones if they contain nothing but expired cells.
+             */
             @Override
             public Row applyToRow(Row row)
             {
-                if (row.hasLiveData(ReadCommand.this.nowInSec(), enforceStrictLiveness))
-                    ++liveRows;
-
+                boolean hasTombstones = false;
                 for (Cell cell : row.cells())
                 {
                     if (!cell.isLive(ReadCommand.this.nowInSec()))
+                    {
                         countTombstone(row.clustering());
+                        hasTombstones = true; // allows to avoid counting an extra tombstone if the whole row expired
+                    }
                 }
+
+                if (row.hasLiveData(ReadCommand.this.nowInSec(), enforceStrictLiveness))
+                    ++liveRows;
+                else if (!row.primaryKeyLivenessInfo().isLive(ReadCommand.this.nowInSec())
+                        && row.hasDeletion(ReadCommand.this.nowInSec())
+                        && !hasTombstones)
+                {
+                    // We're counting primary key deletions only here.
+                    countTombstone(row.clustering());
+                }
+
                 return row;
             }
 
@@ -543,13 +576,71 @@
                     logger.warn(msg);
                 }
 
-                Tracing.trace("Read {} live and {} tombstone cells{}", liveRows, tombstones, (warnTombstones ? " (see tombstone_warn_threshold)" : ""));
+                Tracing.trace("Read {} live rows and {} tombstone cells{}",
+                        liveRows, tombstones,
+                        (warnTombstones ? " (see tombstone_warn_threshold)" : ""));
             }
         }
 
         return Transformation.apply(iter, new MetricRecording());
     }
 
+    protected class CheckForAbort extends StoppingTransformation<UnfilteredRowIterator>
+    {
+        long lastChecked = 0;
+
+        protected UnfilteredRowIterator applyToPartition(UnfilteredRowIterator partition)
+        {
+            if (maybeAbort())
+            {
+                partition.close();
+                return null;
+            }
+
+            return Transformation.apply(partition, this);
+        }
+
+        protected Row applyToRow(Row row)
+        {
+            if (TEST_ITERATION_DELAY_MILLIS > 0)
+                maybeDelayForTesting();
+
+            return maybeAbort() ? null : row;
+        }
+
+        private boolean maybeAbort()
+        {
+            /**
+             * The value returned by ApproximateTime.currentTimeMillis() is updated only every
+             * {@link ApproximateTime.CHECK_INTERVAL_MS}, by default 10 millis. Since MonitorableImpl
+             * relies on ApproximateTime, we don't need to check unless the approximate time has elapsed.
+             */
+            if (lastChecked == ApproximateTime.currentTimeMillis())
+                return false;
+
+            lastChecked = ApproximateTime.currentTimeMillis();
+
+            if (isAborted())
+            {
+                stop();
+                return true;
+            }
+
+            return false;
+        }
+
+        private void maybeDelayForTesting()
+        {
+            if (!metadata.ksName.startsWith("system"))
+                FBUtilities.sleepQuietly(TEST_ITERATION_DELAY_MILLIS);
+        }
+    }
+
+    protected UnfilteredPartitionIterator withStateTracking(UnfilteredPartitionIterator iter)
+    {
+        return Transformation.apply(iter, new CheckForAbort());
+    }
+
     /**
      * Creates a message for this command.
      */
@@ -604,6 +695,12 @@
         return sb.toString();
     }
 
+    // Monitorable interface
+    public String name()
+    {
+        return toCQLString();
+    }
+
     private static class Serializer implements IVersionedSerializer<ReadCommand>
     {
         private static int digestFlag(boolean isDigest)
@@ -648,7 +745,7 @@
             out.writeInt(command.nowInSec());
             ColumnFilter.serializer.serialize(command.columnFilter(), out, version);
             RowFilter.serializer.serialize(command.rowFilter(), out, version);
-            DataLimits.serializer.serialize(command.limits(), out, version);
+            DataLimits.serializer.serialize(command.limits(), out, version, command.metadata.comparator);
             if (null != command.index)
                 IndexMetadata.serializer.serialize(command.index, out, version);
 
@@ -669,7 +766,7 @@
             int nowInSec = in.readInt();
             ColumnFilter columnFilter = ColumnFilter.serializer.deserialize(in, version, metadata);
             RowFilter rowFilter = RowFilter.serializer.deserialize(in, version, metadata);
-            DataLimits limits = DataLimits.serializer.deserialize(in, version);
+            DataLimits limits = DataLimits.serializer.deserialize(in, version,  metadata.comparator);
             IndexMetadata index = hasIndex ? deserializeIndexMetadata(in, version, metadata) : null;
 
             return kind.selectionDeserializer.deserialize(in, version, isDigest, digestVersion, isForThrift, metadata, nowInSec, columnFilter, rowFilter, limits, index);
@@ -683,12 +780,11 @@
             }
             catch (UnknownIndexException e)
             {
-                String message = String.format("Couldn't find a defined index on %s.%s with the id %s. " +
-                                               "If an index was just created, this is likely due to the schema not " +
-                                               "being fully propagated. Local read will proceed without using the " +
-                                               "index. Please wait for schema agreement after index creation.",
-                                               cfm.ksName, cfm.cfName, e.indexId.toString());
-                logger.info(message);
+                logger.info("Couldn't find a defined index on {}.{} with the id {}. " +
+                            "If an index was just created, this is likely due to the schema not " +
+                            "being fully propagated. Local read will proceed without using the " +
+                            "index. Please wait for schema agreement after index creation.",
+                            cfm.ksName, cfm.cfName, e.indexId);
                 return null;
             }
         }
@@ -703,7 +799,7 @@
                  + TypeSizes.sizeof(command.nowInSec())
                  + ColumnFilter.serializer.serializedSize(command.columnFilter(), version)
                  + RowFilter.serializer.serializedSize(command.rowFilter(), version)
-                 + DataLimits.serializer.serializedSize(command.limits(), version)
+                 + DataLimits.serializer.serializedSize(command.limits(), version, command.metadata.comparator)
                  + command.selectionSerializedSize(version)
                  + command.indexSerializedSize(version);
         }
@@ -1055,7 +1151,7 @@
             // slice filter's stop.
             DataRange.Paging pagingRange = (DataRange.Paging) rangeCommand.dataRange();
             Clustering lastReturned = pagingRange.getLastReturned();
-            Slice.Bound newStart = Slice.Bound.inclusiveStartOf(lastReturned);
+            ClusteringBound newStart = ClusteringBound.inclusiveStartOf(lastReturned);
             Slice lastSlice = filter.requestedSlices().get(filter.requestedSlices().size() - 1);
             ByteBufferUtil.writeWithShortLength(LegacyLayout.encodeBound(metadata, newStart, true), out);
             ByteBufferUtil.writeWithShortLength(LegacyLayout.encodeClustering(metadata, lastSlice.end().clustering()), out);
@@ -1367,7 +1463,7 @@
                 // See CASSANDRA-11087.
                 if (metadata.isStaticCompactTable() && cellName.clustering.equals(Clustering.STATIC_CLUSTERING))
                 {
-                    clusterings.add(new Clustering(cellName.column.name.bytes));
+                    clusterings.add(Clustering.make(cellName.column.name.bytes));
                     selectionBuilder.add(metadata.compactValueColumn());
                 }
                 else
@@ -1518,7 +1614,7 @@
         static long serializedStaticSliceSize(CFMetaData metadata)
         {
             // unlike serializeStaticSlice(), but we don't care about reversal for size calculations
-            ByteBuffer sliceStart = LegacyLayout.encodeBound(metadata, Slice.Bound.BOTTOM, false);
+            ByteBuffer sliceStart = LegacyLayout.encodeBound(metadata, ClusteringBound.BOTTOM, false);
             long size = ByteBufferUtil.serializedSizeWithShortLength(sliceStart);
 
             size += TypeSizes.sizeof((short) (metadata.comparator.size() * 3 + 2));
@@ -1546,7 +1642,7 @@
             // slice finish after we've written the static slice start
             if (!isReversed)
             {
-                ByteBuffer sliceStart = LegacyLayout.encodeBound(metadata, Slice.Bound.BOTTOM, false);
+                ByteBuffer sliceStart = LegacyLayout.encodeBound(metadata, ClusteringBound.BOTTOM, false);
                 ByteBufferUtil.writeWithShortLength(sliceStart, out);
             }
 
@@ -1562,7 +1658,7 @@
 
             if (isReversed)
             {
-                ByteBuffer sliceStart = LegacyLayout.encodeBound(metadata, Slice.Bound.BOTTOM, false);
+                ByteBuffer sliceStart = LegacyLayout.encodeBound(metadata, ClusteringBound.BOTTOM, false);
                 ByteBufferUtil.writeWithShortLength(sliceStart, out);
             }
         }
@@ -1683,7 +1779,7 @@
             {
                 Slices.Builder slicesBuilder = new Slices.Builder(metadata.comparator);
                 for (Clustering clustering : requestedRows)
-                    slicesBuilder.add(Slice.Bound.inclusiveStartOf(clustering), Slice.Bound.inclusiveEndOf(clustering));
+                    slicesBuilder.add(ClusteringBound.inclusiveStartOf(clustering), ClusteringBound.inclusiveEndOf(clustering));
                 slices = slicesBuilder.build();
             }
 
diff --git a/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java b/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java
index 9cde8dc..a71e92d 100644
--- a/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java
+++ b/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java
@@ -41,15 +41,24 @@
         }
 
         ReadCommand command = message.payload;
+        command.setMonitoringTime(message.constructionTime, message.isCrossNode(), message.getTimeout(), message.getSlowQueryTimeout());
+
         ReadResponse response;
-        try (ReadOrderGroup opGroup = command.startOrderGroup(); UnfilteredPartitionIterator iterator = command.executeLocally(opGroup))
+        try (ReadExecutionController executionController = command.executionController();
+             UnfilteredPartitionIterator iterator = command.executeLocally(executionController))
         {
             response = command.createResponse(iterator);
         }
 
-        MessageOut<ReadResponse> reply = new MessageOut<>(MessagingService.Verb.REQUEST_RESPONSE, response, serializer());
+        if (!command.complete())
+        {
+            Tracing.trace("Discarding partial response to {} (timed out)", message.from);
+            MessagingService.instance().incrementDroppedMessages(message, message.getLifetimeInMS());
+            return;
+        }
 
         Tracing.trace("Enqueuing response to {}", message.from);
+        MessageOut<ReadResponse> reply = new MessageOut<>(MessagingService.Verb.REQUEST_RESPONSE, response, serializer());
         MessagingService.instance().sendReply(reply, id, message.from);
     }
 }
diff --git a/src/java/org/apache/cassandra/db/ReadExecutionController.java b/src/java/org/apache/cassandra/db/ReadExecutionController.java
new file mode 100644
index 0000000..56bb0d3
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/ReadExecutionController.java
@@ -0,0 +1,150 @@
+/*
+ * 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.db;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.utils.concurrent.OpOrder;
+
+public class ReadExecutionController implements AutoCloseable
+{
+    // For every reads
+    private final OpOrder.Group baseOp;
+    private final CFMetaData baseMetadata; // kept to sanity check that we have take the op order on the right table
+
+    // For index reads
+    private final ReadExecutionController indexController;
+    private final OpOrder.Group writeOp;
+
+    private ReadExecutionController(OpOrder.Group baseOp, CFMetaData baseMetadata, ReadExecutionController indexController, OpOrder.Group writeOp)
+    {
+        // We can have baseOp == null, but only when empty() is called, in which case the controller will never really be used
+        // (which validForReadOn should ensure). But if it's not null, we should have the proper metadata too.
+        assert (baseOp == null) == (baseMetadata == null);
+        this.baseOp = baseOp;
+        this.baseMetadata = baseMetadata;
+        this.indexController = indexController;
+        this.writeOp = writeOp;
+    }
+
+    public ReadExecutionController indexReadController()
+    {
+        return indexController;
+    }
+
+    public OpOrder.Group writeOpOrderGroup()
+    {
+        return writeOp;
+    }
+
+    public boolean validForReadOn(ColumnFamilyStore cfs)
+    {
+        return baseOp != null && cfs.metadata.cfId.equals(baseMetadata.cfId);
+    }
+
+    public static ReadExecutionController empty()
+    {
+        return new ReadExecutionController(null, null, null, null);
+    }
+
+    /**
+     * Creates an execution controller for the provided command.
+     * <p>
+     * Note: no code should use this method outside of {@link ReadCommand#executionController} (for
+     * consistency sake) and you should use that latter method if you need an execution controller.
+     *
+     * @param command the command for which to create a controller.
+     * @return the created execution controller, which must always be closed.
+     */
+    @SuppressWarnings("resource") // ops closed during controller close
+    static ReadExecutionController forCommand(ReadCommand command)
+    {
+        ColumnFamilyStore baseCfs = Keyspace.openAndGetStore(command.metadata());
+        ColumnFamilyStore indexCfs = maybeGetIndexCfs(baseCfs, command);
+
+        if (indexCfs == null)
+        {
+            return new ReadExecutionController(baseCfs.readOrdering.start(), baseCfs.metadata, null, null);
+        }
+        else
+        {
+            OpOrder.Group baseOp = null, writeOp = null;
+            ReadExecutionController indexController = null;
+            // OpOrder.start() shouldn't fail, but better safe than sorry.
+            try
+            {
+                baseOp = baseCfs.readOrdering.start();
+                indexController = new ReadExecutionController(indexCfs.readOrdering.start(), indexCfs.metadata, null, null);
+                // TODO: this should perhaps not open and maintain a writeOp for the full duration, but instead only *try* to delete stale entries, without blocking if there's no room
+                // as it stands, we open a writeOp and keep it open for the duration to ensure that should this CF get flushed to make room we don't block the reclamation of any room being made
+                writeOp = Keyspace.writeOrder.start();
+                return new ReadExecutionController(baseOp, baseCfs.metadata, indexController, writeOp);
+            }
+            catch (RuntimeException e)
+            {
+                // Note that must have writeOp == null since ReadOrderGroup ctor can't fail
+                assert writeOp == null;
+                try
+                {
+                    if (baseOp != null)
+                        baseOp.close();
+                }
+                finally
+                {
+                    if (indexController != null)
+                        indexController.close();
+                }
+                throw e;
+            }
+        }
+    }
+
+    private static ColumnFamilyStore maybeGetIndexCfs(ColumnFamilyStore baseCfs, ReadCommand command)
+    {
+        Index index = command.getIndex(baseCfs);
+        return index == null ? null : index.getBackingTable().orElse(null);
+    }
+
+    public CFMetaData metaData()
+    {
+        return baseMetadata;
+    }
+
+    public void close()
+    {
+        try
+        {
+            if (baseOp != null)
+                baseOp.close();
+        }
+        finally
+        {
+            if (indexController != null)
+            {
+                try
+                {
+                    indexController.close();
+                }
+                finally
+                {
+                    writeOp.close();
+                }
+            }
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/ReadOrderGroup.java b/src/java/org/apache/cassandra/db/ReadOrderGroup.java
deleted file mode 100644
index 416a2b8..0000000
--- a/src/java/org/apache/cassandra/db/ReadOrderGroup.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * 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.db;
-
-import org.apache.cassandra.index.Index;
-import org.apache.cassandra.utils.concurrent.OpOrder;
-
-public class ReadOrderGroup implements AutoCloseable
-{
-    // For every reads
-    private final OpOrder.Group baseOp;
-
-    // For index reads
-    private final OpOrder.Group indexOp;
-    private final OpOrder.Group writeOp;
-
-    private ReadOrderGroup(OpOrder.Group baseOp, OpOrder.Group indexOp, OpOrder.Group writeOp)
-    {
-        this.baseOp = baseOp;
-        this.indexOp = indexOp;
-        this.writeOp = writeOp;
-    }
-
-    public OpOrder.Group baseReadOpOrderGroup()
-    {
-        return baseOp;
-    }
-
-    public OpOrder.Group indexReadOpOrderGroup()
-    {
-        return indexOp;
-    }
-
-    public OpOrder.Group writeOpOrderGroup()
-    {
-        return writeOp;
-    }
-
-    public static ReadOrderGroup emptyGroup()
-    {
-        return new ReadOrderGroup(null, null, null);
-    }
-
-    @SuppressWarnings("resource") // ops closed during group close
-    public static ReadOrderGroup forCommand(ReadCommand command)
-    {
-        ColumnFamilyStore baseCfs = Keyspace.openAndGetStore(command.metadata());
-        ColumnFamilyStore indexCfs = maybeGetIndexCfs(baseCfs, command);
-
-        if (indexCfs == null)
-        {
-            return new ReadOrderGroup(baseCfs.readOrdering.start(), null, null);
-        }
-        else
-        {
-            OpOrder.Group baseOp = null, indexOp = null, writeOp = null;
-            // OpOrder.start() shouldn't fail, but better safe than sorry.
-            try
-            {
-                baseOp = baseCfs.readOrdering.start();
-                indexOp = indexCfs.readOrdering.start();
-                // TODO: this should perhaps not open and maintain a writeOp for the full duration, but instead only *try* to delete stale entries, without blocking if there's no room
-                // as it stands, we open a writeOp and keep it open for the duration to ensure that should this CF get flushed to make room we don't block the reclamation of any room being made
-                writeOp = Keyspace.writeOrder.start();
-                return new ReadOrderGroup(baseOp, indexOp, writeOp);
-            }
-            catch (RuntimeException e)
-            {
-                // Note that must have writeOp == null since ReadOrderGroup ctor can't fail
-                assert writeOp == null;
-                try
-                {
-                    if (baseOp != null)
-                        baseOp.close();
-                }
-                finally
-                {
-                    if (indexOp != null)
-                        indexOp.close();
-                }
-                throw e;
-            }
-        }
-    }
-
-    private static ColumnFamilyStore maybeGetIndexCfs(ColumnFamilyStore baseCfs, ReadCommand command)
-    {
-        Index index = command.getIndex(baseCfs);
-        return index == null ? null : index.getBackingTable().orElse(null);
-    }
-
-    public void close()
-    {
-        try
-        {
-            if (baseOp != null)
-                baseOp.close();
-        }
-        finally
-        {
-            if (indexOp != null)
-            {
-                try
-                {
-                    indexOp.close();
-                }
-                finally
-                {
-                    writeOp.close();
-                }
-            }
-        }
-    }
-}
diff --git a/src/java/org/apache/cassandra/db/ReadQuery.java b/src/java/org/apache/cassandra/db/ReadQuery.java
index 75ba8f5..64aeb2a 100644
--- a/src/java/org/apache/cassandra/db/ReadQuery.java
+++ b/src/java/org/apache/cassandra/db/ReadQuery.java
@@ -23,6 +23,7 @@
 import org.apache.cassandra.service.ClientState;
 import org.apache.cassandra.service.pager.QueryPager;
 import org.apache.cassandra.service.pager.PagingState;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Generic abstraction for read queries.
@@ -35,21 +36,26 @@
 {
     ReadQuery EMPTY = new ReadQuery()
     {
-        public ReadOrderGroup startOrderGroup()
+        public ReadExecutionController executionController()
         {
-            return ReadOrderGroup.emptyGroup();
+            return ReadExecutionController.empty();
         }
 
-        public PartitionIterator execute(ConsistencyLevel consistency, ClientState clientState) throws RequestExecutionException
+        public PartitionIterator execute(ConsistencyLevel consistency, ClientState clientState, long queryStartNanoTime) throws RequestExecutionException
         {
             return EmptyIterators.partition();
         }
 
-        public PartitionIterator executeInternal(ReadOrderGroup orderGroup)
+        public PartitionIterator executeInternal(ReadExecutionController controller)
         {
             return EmptyIterators.partition();
         }
 
+        public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController)
+        {
+            return EmptyIterators.unfilteredPartition(executionController.metaData(), false);
+        }
+
         public DataLimits limits()
         {
             // What we return here doesn't matter much in practice. However, returning DataLimits.NONE means
@@ -58,12 +64,7 @@
             return DataLimits.cqlLimits(0);
         }
 
-        public QueryPager getPager(PagingState state, int protocolVersion)
-        {
-            return QueryPager.EMPTY;
-        }
-
-        public QueryPager getLocalPager()
+        public QueryPager getPager(PagingState state, ProtocolVersion protocolVersion)
         {
             return QueryPager.EMPTY;
         }
@@ -92,9 +93,9 @@
      * The returned object <b>must</b> be closed on all path and it is thus strongly advised to
      * use it in a try-with-ressource construction.
      *
-     * @return a newly started order group for this {@code ReadQuery}.
+     * @return a newly started execution controller for this {@code ReadQuery}.
      */
-    public ReadOrderGroup startOrderGroup();
+    public ReadExecutionController executionController();
 
     /**
      * Executes the query at the provided consistency level.
@@ -105,15 +106,24 @@
      *
      * @return the result of the query.
      */
-    public PartitionIterator execute(ConsistencyLevel consistency, ClientState clientState) throws RequestExecutionException;
+    public PartitionIterator execute(ConsistencyLevel consistency, ClientState clientState, long queryStartNanoTime) throws RequestExecutionException;
 
     /**
      * Execute the query for internal queries (that is, it basically executes the query locally).
      *
-     * @param orderGroup the {@code ReadOrderGroup} protecting the read.
+     * @param controller the {@code ReadExecutionController} protecting the read.
      * @return the result of the query.
      */
-    public PartitionIterator executeInternal(ReadOrderGroup orderGroup);
+    public PartitionIterator executeInternal(ReadExecutionController controller);
+
+    /**
+     * Execute the query locally. This is similar to {@link ReadQuery#executeInternal(ReadExecutionController)}
+     * but it returns an unfiltered partition iterator that can be merged later on.
+     *
+     * @param controller the {@code ReadExecutionController} protecting the read.
+     * @return the result of the read query.
+     */
+    public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController);
 
     /**
      * Returns a pager for the query.
@@ -124,7 +134,7 @@
      *
      * @return a pager for the query.
      */
-    public QueryPager getPager(PagingState pagingState, int protocolVersion);
+    public QueryPager getPager(PagingState pagingState, ProtocolVersion protocolVersion);
 
     /**
      * The limits for the query.
diff --git a/src/java/org/apache/cassandra/db/ReadResponse.java b/src/java/org/apache/cassandra/db/ReadResponse.java
index bc44a1b..7aa915e 100644
--- a/src/java/org/apache/cassandra/db/ReadResponse.java
+++ b/src/java/org/apache/cassandra/db/ReadResponse.java
@@ -322,10 +322,9 @@
 
                     ClusteringIndexFilter filter = command.clusteringIndexFilter(partition.partitionKey());
 
-                    // Pre-3.0 we would always request one more row than we actually needed and the command-level "start" would
-                    // be the last-returned cell name, so the response would always include it. By consequence, we need to filter
-                    // the results here.
-                    UnfilteredRowIterator iterator = filter.filter(partition.sliceableUnfilteredIterator(command.columnFilter(), filter.isReversed()));
+                    // Pre-3.0, we would always request one more row than we actually needed and the command-level "start" would
+                    // be the last-returned cell name, so the response would always include it.
+                    UnfilteredRowIterator iterator = partition.unfilteredIterator(command.columnFilter(), filter.getSlices(command.metadata()), filter.isReversed());
 
                     // Wrap results with a ThriftResultMerger only if they're intended for the thrift command.
                     if (command.isForThrift())
diff --git a/src/java/org/apache/cassandra/db/RowIndexEntry.java b/src/java/org/apache/cassandra/db/RowIndexEntry.java
index 4e2f063..e620dc0 100644
--- a/src/java/org/apache/cassandra/db/RowIndexEntry.java
+++ b/src/java/org/apache/cassandra/db/RowIndexEntry.java
@@ -18,26 +18,135 @@
 package org.apache.cassandra.db;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
+import java.nio.ByteBuffer;
 import java.util.List;
 
-import com.google.common.primitives.Ints;
-
+import com.codahale.metrics.Histogram;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.cache.IMeasurableMemory;
-import org.apache.cassandra.io.sstable.IndexHelper;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.io.ISerializer;
+import org.apache.cassandra.io.sstable.IndexInfo;
 import org.apache.cassandra.io.sstable.format.Version;
 import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.io.util.DataOutputPlus;
-import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.FileDataInput;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.TrackedDataInputPlus;
+import org.apache.cassandra.metrics.DefaultNameFactory;
+import org.apache.cassandra.metrics.MetricNameFactory;
 import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.vint.VIntCoding;
+import org.github.jamm.Unmetered;
 
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+
+/**
+ * Binary format of {@code RowIndexEntry} is defined as follows:
+ * {@code
+ * (long) position (64 bit long, vint encoded)
+ *  (int) serialized size of data that follows (32 bit int, vint encoded)
+ * -- following for indexed entries only (so serialized size > 0)
+ *  (int) DeletionTime.localDeletionTime
+ * (long) DeletionTime.markedForDeletionAt
+ *  (int) number of IndexInfo objects (32 bit int, vint encoded)
+ *    (*) serialized IndexInfo objects, see below
+ *    (*) offsets of serialized IndexInfo objects, since version "ma" (3.0)
+ *        Each IndexInfo object's offset is relative to the first IndexInfo object.
+ * }
+ * <p>
+ * See {@link IndexInfo} for a description of the serialized format.
+ * </p>
+ *
+ * <p>
+ * For each partition, the layout of the index file looks like this:
+ * </p>
+ * <ol>
+ *     <li>partition key - prefixed with {@code short} length</li>
+ *     <li>serialized {@code RowIndexEntry} objects</li>
+ * </ol>
+ *
+ * <p>
+ *     Generally, we distinguish between index entries that have <i>index
+ *     samples</i> (list of {@link IndexInfo} objects) and those who don't.
+ *     For each <i>portion</i> of data for a single partition in the data file,
+ *     an index sample is created. The size of that <i>portion</i> is defined
+ *     by {@link org.apache.cassandra.config.Config#column_index_size_in_kb}.
+ * </p>
+ * <p>
+ *     Index entries with less than 2 index samples, will just store the
+ *     position in the data file.
+ * </p>
+ * <p>
+ *     Note: legacy sstables for index entries are those sstable formats that
+ *     do <i>not</i> have an offsets table to index samples ({@link IndexInfo}
+ *     objects). These are those sstables created on Cassandra versions
+ *     earlier than 3.0.
+ * </p>
+ * <p>
+ *     For index entries with index samples we store the index samples
+ *     ({@link IndexInfo} objects). The bigger the partition, the more
+ *     index samples are created. Since a huge amount of index samples
+ *     will "pollute" the heap and cause huge GC pressure, Cassandra 3.6
+ *     (CASSANDRA-11206) distinguishes between index entries with an
+ *     "acceptable" amount of index samples per partition and those
+ *     with an "enormous" amount of index samples. The barrier
+ *     is controlled by the configuration parameter
+ *     {@link org.apache.cassandra.config.Config#column_index_cache_size_in_kb}.
+ *     Index entries with a total serialized size of index samples up to
+ *     {@code column_index_cache_size_in_kb} will be held in an array.
+ *     Index entries exceeding that value will always be accessed from
+ *     disk.
+ * </p>
+ * <p>
+ *     This results in these classes:
+ * </p>
+ * <ul>
+ *     <li>{@link RowIndexEntry} just stores the offset in the data file.</li>
+ *     <li>{@link IndexedEntry} is for index entries with index samples
+ *     and used for both current and legacy sstables, which do not exceed
+ *     {@link org.apache.cassandra.config.Config#column_index_cache_size_in_kb}.</li>
+ *     <li>{@link ShallowIndexedEntry} is for index entries with index samples
+ *     that exceed {@link org.apache.cassandra.config.Config#column_index_cache_size_in_kb}
+ *     for sstables with an offset table to the index samples.</li>
+ *     <li>{@link LegacyShallowIndexedEntry} is for index entries with index samples
+ *     that exceed {@link org.apache.cassandra.config.Config#column_index_cache_size_in_kb}
+ *     but for legacy sstables.</li>
+ * </ul>
+ * <p>
+ *     Since access to index samples on disk (obviously) requires some file
+ *     reader, that functionality is encapsulated in implementations of
+ *     {@link IndexInfoRetriever}. There is an implementation to access
+ *     index samples of legacy sstables (without the offsets table),
+ *     an implementation of access sstables with an offsets table.
+ * </p>
+ * <p>
+ *     Until now (Cassandra 3.x), we still support reading from <i>legacy</i> sstables -
+ *     i.e. sstables created by Cassandra &lt; 3.0 (see {@link org.apache.cassandra.io.sstable.format.big.BigFormat}.
+ * </p>
+ *
+ */
 public class RowIndexEntry<T> implements IMeasurableMemory
 {
     private static final long EMPTY_SIZE = ObjectSizes.measure(new RowIndexEntry(0));
 
+    // constants for type of row-index-entry as serialized for saved-cache
+    static final int CACHE_NOT_INDEXED = 0;
+    static final int CACHE_INDEXED = 1;
+    static final int CACHE_INDEXED_SHALLOW = 2;
+
+    static final Histogram indexEntrySizeHistogram;
+    static final Histogram indexInfoCountHistogram;
+    static final Histogram indexInfoGetsHistogram;
+    static 
+    {
+        MetricNameFactory factory = new DefaultNameFactory("Index", "RowIndexEntry");
+        indexEntrySizeHistogram = Metrics.histogram(factory.createMetricName("IndexedEntrySize"), false);
+        indexInfoCountHistogram = Metrics.histogram(factory.createMetricName("IndexInfoCount"), false);
+        indexInfoGetsHistogram = Metrics.histogram(factory.createMetricName("IndexInfoGets"), false);
+    }
+
     public final long position;
 
     public RowIndexEntry(long position)
@@ -45,32 +154,18 @@
         this.position = position;
     }
 
-    protected int promotedSize(IndexHelper.IndexInfo.Serializer idxSerializer)
-    {
-        return 0;
-    }
-
-    public static RowIndexEntry<IndexHelper.IndexInfo> create(long position, DeletionTime deletionTime, ColumnIndex index)
-    {
-        assert index != null;
-        assert deletionTime != null;
-
-        // we only consider the columns summary when determining whether to create an IndexedEntry,
-        // since if there are insufficient columns to be worth indexing we're going to seek to
-        // the beginning of the row anyway, so we might as well read the tombstone there as well.
-        if (index.columnsIndex.size() > 1)
-            return new IndexedEntry(position, deletionTime, index.partitionHeaderLength, index.columnsIndex);
-        else
-            return new RowIndexEntry<>(position);
-    }
-
     /**
      * @return true if this index entry contains the row-level tombstone and column summary.  Otherwise,
      * caller should fetch these from the row header.
      */
     public boolean isIndexed()
     {
-        return !columnsIndex().isEmpty();
+        return columnsIndexCount() > 1;
+    }
+
+    public boolean indexOnHeap()
+    {
+        return false;
     }
 
     public DeletionTime deletionTime()
@@ -79,15 +174,6 @@
     }
 
     /**
-     * @return the offset to the start of the header information for this row.
-     * For some formats this may not be the start of the row.
-     */
-    public long headerOffset()
-    {
-        return 0;
-    }
-
-    /**
      * The length of the row header (partition key, partition deletion and static row).
      * This value is only provided for indexed entries and this method will throw
      * {@code UnsupportedOperationException} if {@code !isIndexed()}.
@@ -97,9 +183,9 @@
         throw new UnsupportedOperationException();
     }
 
-    public List<T> columnsIndex()
+    public int columnsIndexCount()
     {
-        return Collections.emptyList();
+        return 0;
     }
 
     public long unsharedHeapSize()
@@ -107,129 +193,178 @@
         return EMPTY_SIZE;
     }
 
-    public interface IndexSerializer<T>
+    /**
+     * @param dataFilePosition  position of the partition in the {@link org.apache.cassandra.io.sstable.Component.Type#DATA} file
+     * @param indexFilePosition position in the {@link org.apache.cassandra.io.sstable.Component.Type#PRIMARY_INDEX} of the {@link RowIndexEntry}
+     * @param deletionTime      deletion time of {@link RowIndexEntry}
+     * @param headerLength      deletion time of {@link RowIndexEntry}
+     * @param columnIndexCount  number of {@link IndexInfo} entries in the {@link RowIndexEntry}
+     * @param indexedPartSize   serialized size of all serialized {@link IndexInfo} objects and their offsets
+     * @param indexSamples      list with IndexInfo offsets (if total serialized size is less than {@link org.apache.cassandra.config.Config#column_index_cache_size_in_kb}
+     * @param offsets           offsets of IndexInfo offsets
+     * @param idxInfoSerializer the {@link IndexInfo} serializer
+     */
+    public static RowIndexEntry<IndexInfo> create(long dataFilePosition, long indexFilePosition,
+                                       DeletionTime deletionTime, long headerLength, int columnIndexCount,
+                                       int indexedPartSize,
+                                       List<IndexInfo> indexSamples, int[] offsets,
+                                       ISerializer<IndexInfo> idxInfoSerializer)
     {
-        void serialize(RowIndexEntry<T> rie, DataOutputPlus out) throws IOException;
-        RowIndexEntry<T> deserialize(DataInputPlus in) throws IOException;
-        int serializedSize(RowIndexEntry<T> rie);
+        // If the "partition building code" in BigTableWriter.append() via ColumnIndex returns a list
+        // of IndexInfo objects, which is the case if the serialized size is less than
+        // Config.column_index_cache_size_in_kb, AND we have more than one IndexInfo object, we
+        // construct an IndexedEntry object. (note: indexSamples.size() and columnIndexCount have the same meaning)
+        if (indexSamples != null && indexSamples.size() > 1)
+            return new IndexedEntry(dataFilePosition, deletionTime, headerLength,
+                                    indexSamples.toArray(new IndexInfo[indexSamples.size()]), offsets,
+                                    indexedPartSize, idxInfoSerializer);
+        // Here we have to decide whether we have serialized IndexInfo objects that exceeds
+        // Config.column_index_cache_size_in_kb (not exceeding case covered above).
+        // Such a "big" indexed-entry is represented as a shallow one.
+        if (columnIndexCount > 1)
+            return new ShallowIndexedEntry(dataFilePosition, indexFilePosition,
+                                           deletionTime, headerLength, columnIndexCount,
+                                           indexedPartSize, idxInfoSerializer);
+        // Last case is that there are no index samples.
+        return new RowIndexEntry<>(dataFilePosition);
     }
 
-    public static class Serializer implements IndexSerializer<IndexHelper.IndexInfo>
+    public IndexInfoRetriever openWithIndex(FileHandle indexFile)
     {
-        private final IndexHelper.IndexInfo.Serializer idxSerializer;
+        return null;
+    }
+
+    public interface IndexSerializer<T>
+    {
+        void serialize(RowIndexEntry<T> rie, DataOutputPlus out, ByteBuffer indexInfo) throws IOException;
+        RowIndexEntry<T> deserialize(DataInputPlus in, long indexFilePosition) throws IOException;
+        void serializeForCache(RowIndexEntry<T> rie, DataOutputPlus out) throws IOException;
+        RowIndexEntry<T> deserializeForCache(DataInputPlus in) throws IOException;
+
+        long deserializePositionAndSkip(DataInputPlus in) throws IOException;
+
+        ISerializer<T> indexInfoSerializer();
+    }
+
+    public static final class Serializer implements IndexSerializer<IndexInfo>
+    {
+        private final IndexInfo.Serializer idxInfoSerializer;
         private final Version version;
 
         public Serializer(CFMetaData metadata, Version version, SerializationHeader header)
         {
-            this.idxSerializer = new IndexHelper.IndexInfo.Serializer(metadata, version, header);
+            this.idxInfoSerializer = metadata.serializers().indexInfoSerializer(version, header);
             this.version = version;
         }
 
-        public void serialize(RowIndexEntry<IndexHelper.IndexInfo> rie, DataOutputPlus out) throws IOException
+        public IndexInfo.Serializer indexInfoSerializer()
+        {
+            return idxInfoSerializer;
+        }
+
+        public void serialize(RowIndexEntry<IndexInfo> rie, DataOutputPlus out, ByteBuffer indexInfo) throws IOException
         {
             assert version.storeRows() : "We read old index files but we should never write them";
 
-            out.writeUnsignedVInt(rie.position);
-            out.writeUnsignedVInt(rie.promotedSize(idxSerializer));
+            rie.serialize(out, idxInfoSerializer, indexInfo);
+        }
 
-            if (rie.isIndexed())
+        public void serializeForCache(RowIndexEntry<IndexInfo> rie, DataOutputPlus out) throws IOException
+        {
+            assert version.storeRows();
+
+            rie.serializeForCache(out);
+        }
+
+        public RowIndexEntry<IndexInfo> deserializeForCache(DataInputPlus in) throws IOException
+        {
+            assert version.storeRows();
+
+            long position = in.readUnsignedVInt();
+
+            switch (in.readByte())
             {
-                out.writeUnsignedVInt(rie.headerLength());
-                DeletionTime.serializer.serialize(rie.deletionTime(), out);
-                out.writeUnsignedVInt(rie.columnsIndex().size());
-
-                // Calculate and write the offsets to the IndexInfo objects.
-
-                int[] offsets = new int[rie.columnsIndex().size()];
-
-                if (out.hasPosition())
-                {
-                    // Out is usually a SequentialWriter, so using the file-pointer is fine to generate the offsets.
-                    // A DataOutputBuffer also works.
-                    long start = out.position();
-                    int i = 0;
-                    for (IndexHelper.IndexInfo info : rie.columnsIndex())
-                    {
-                        offsets[i] = i == 0 ? 0 : (int)(out.position() - start);
-                        i++;
-                        idxSerializer.serialize(info, out);
-                    }
-                }
-                else
-                {
-                    // Not sure this branch will ever be needed, but if it is called, it has to calculate the
-                    // serialized sizes instead of simply using the file-pointer.
-                    int i = 0;
-                    int offset = 0;
-                    for (IndexHelper.IndexInfo info : rie.columnsIndex())
-                    {
-                        offsets[i++] = offset;
-                        idxSerializer.serialize(info, out);
-                        offset += idxSerializer.serializedSize(info);
-                    }
-                }
-
-                for (int off : offsets)
-                    out.writeInt(off);
+                case CACHE_NOT_INDEXED:
+                    return new RowIndexEntry<>(position);
+                case CACHE_INDEXED:
+                    return new IndexedEntry(position, in, idxInfoSerializer, version);
+                case CACHE_INDEXED_SHALLOW:
+                    return new ShallowIndexedEntry(position, in, idxInfoSerializer);
+                default:
+                    throw new AssertionError();
             }
         }
 
-        public RowIndexEntry<IndexHelper.IndexInfo> deserialize(DataInputPlus in) throws IOException
+        public static void skipForCache(DataInputPlus in, Version version) throws IOException
+        {
+            assert version.storeRows();
+
+            /* long position = */in.readUnsignedVInt();
+            switch (in.readByte())
+            {
+                case CACHE_NOT_INDEXED:
+                    break;
+                case CACHE_INDEXED:
+                    IndexedEntry.skipForCache(in);
+                    break;
+                case CACHE_INDEXED_SHALLOW:
+                    ShallowIndexedEntry.skipForCache(in);
+                    break;
+                default:
+                    assert false;
+            }
+        }
+
+        public RowIndexEntry<IndexInfo> deserialize(DataInputPlus in, long indexFilePosition) throws IOException
         {
             if (!version.storeRows())
-            {
-                long position = in.readLong();
-
-                int size = in.readInt();
-                if (size > 0)
-                {
-                    DeletionTime deletionTime = DeletionTime.serializer.deserialize(in);
-
-                    int entries = in.readInt();
-                    List<IndexHelper.IndexInfo> columnsIndex = new ArrayList<>(entries);
-
-                    long headerLength = 0L;
-                    for (int i = 0; i < entries; i++)
-                    {
-                        IndexHelper.IndexInfo info = idxSerializer.deserialize(in);
-                        columnsIndex.add(info);
-                        if (i == 0)
-                            headerLength = info.offset;
-                    }
-
-                    return new IndexedEntry(position, deletionTime, headerLength, columnsIndex);
-                }
-                else
-                {
-                    return new RowIndexEntry<>(position);
-                }
-            }
+                return LegacyShallowIndexedEntry.deserialize(in, indexFilePosition, idxInfoSerializer);
 
             long position = in.readUnsignedVInt();
 
             int size = (int)in.readUnsignedVInt();
-            if (size > 0)
-            {
-                long headerLength = in.readUnsignedVInt();
-                DeletionTime deletionTime = DeletionTime.serializer.deserialize(in);
-                int entries = (int)in.readUnsignedVInt();
-                List<IndexHelper.IndexInfo> columnsIndex = new ArrayList<>(entries);
-                for (int i = 0; i < entries; i++)
-                    columnsIndex.add(idxSerializer.deserialize(in));
-
-                in.skipBytesFully(entries * TypeSizes.sizeof(0));
-
-                return new IndexedEntry(position, deletionTime, headerLength, columnsIndex);
-            }
-            else
+            if (size == 0)
             {
                 return new RowIndexEntry<>(position);
             }
+            else
+            {
+                long headerLength = in.readUnsignedVInt();
+                DeletionTime deletionTime = DeletionTime.serializer.deserialize(in);
+                int columnsIndexCount = (int) in.readUnsignedVInt();
+
+                int indexedPartSize = size - serializedSize(deletionTime, headerLength, columnsIndexCount);
+
+                if (size <= DatabaseDescriptor.getColumnIndexCacheSize())
+                {
+                    return new IndexedEntry(position, in, deletionTime, headerLength, columnsIndexCount,
+                                            idxInfoSerializer, version, indexedPartSize);
+                }
+                else
+                {
+                    in.skipBytes(indexedPartSize);
+
+                    return new ShallowIndexedEntry(position,
+                                                   indexFilePosition,
+                                                   deletionTime, headerLength, columnsIndexCount,
+                                                   indexedPartSize, idxInfoSerializer);
+                }
+            }
         }
 
-        // Reads only the data 'position' of the index entry and returns it. Note that this left 'in' in the middle
-        // of reading an entry, so this is only useful if you know what you are doing and in most case 'deserialize'
-        // should be used instead.
+        public long deserializePositionAndSkip(DataInputPlus in) throws IOException
+        {
+            if (!version.storeRows())
+                return LegacyShallowIndexedEntry.deserializePositionAndSkip(in);
+
+            return ShallowIndexedEntry.deserializePositionAndSkip(in);
+        }
+
+        /**
+         * Reads only the data 'position' of the index entry and returns it. Note that this left 'in' in the middle
+         * of reading an entry, so this is only useful if you know what you are doing and in most case 'deserialize'
+         * should be used instead.
+         */
         public static long readPosition(DataInputPlus in, Version version) throws IOException
         {
             return version.storeRows() ? in.readUnsignedVInt() : in.readLong();
@@ -250,51 +385,62 @@
             in.skipBytesFully(size);
         }
 
-        public int serializedSize(RowIndexEntry<IndexHelper.IndexInfo> rie)
+        public static void serializeOffsets(DataOutputBuffer out, int[] indexOffsets, int columnIndexCount) throws IOException
         {
-            assert version.storeRows() : "We read old index files but we should never write them";
-
-            int indexedSize = 0;
-            if (rie.isIndexed())
-            {
-                List<IndexHelper.IndexInfo> index = rie.columnsIndex();
-
-                indexedSize += TypeSizes.sizeofUnsignedVInt(rie.headerLength());
-                indexedSize += DeletionTime.serializer.serializedSize(rie.deletionTime());
-                indexedSize += TypeSizes.sizeofUnsignedVInt(index.size());
-
-                for (IndexHelper.IndexInfo info : index)
-                    indexedSize += idxSerializer.serializedSize(info);
-
-                indexedSize += index.size() * TypeSizes.sizeof(0);
-            }
-
-            return TypeSizes.sizeofUnsignedVInt(rie.position) + TypeSizes.sizeofUnsignedVInt(indexedSize) + indexedSize;
+            for (int i = 0; i < columnIndexCount; i++)
+                out.writeInt(indexOffsets[i]);
         }
     }
 
-    /**
-     * An entry in the row index for a row whose columns are indexed.
-     */
-    private static class IndexedEntry extends RowIndexEntry<IndexHelper.IndexInfo>
+    private static int serializedSize(DeletionTime deletionTime, long headerLength, int columnIndexCount)
     {
-        private final DeletionTime deletionTime;
+        return TypeSizes.sizeofUnsignedVInt(headerLength)
+               + (int) DeletionTime.serializer.serializedSize(deletionTime)
+               + TypeSizes.sizeofUnsignedVInt(columnIndexCount);
+    }
 
-        // The offset in the file when the index entry end
-        private final long headerLength;
-        private final List<IndexHelper.IndexInfo> columnsIndex;
-        private static final long BASE_SIZE =
-                ObjectSizes.measure(new IndexedEntry(0, DeletionTime.LIVE, 0, Arrays.<IndexHelper.IndexInfo>asList(null, null)))
-              + ObjectSizes.measure(new ArrayList<>(1));
+    public void serialize(DataOutputPlus out, IndexInfo.Serializer idxInfoSerializer, ByteBuffer indexInfo) throws IOException
+    {
+        out.writeUnsignedVInt(position);
 
-        private IndexedEntry(long position, DeletionTime deletionTime, long headerLength, List<IndexHelper.IndexInfo> columnsIndex)
+        out.writeUnsignedVInt(0);
+    }
+
+    public void serializeForCache(DataOutputPlus out) throws IOException
+    {
+        out.writeUnsignedVInt(position);
+
+        out.writeByte(CACHE_NOT_INDEXED);
+    }
+
+    private static final class LegacyShallowIndexedEntry extends RowIndexEntry<IndexInfo>
+    {
+        private static final long BASE_SIZE;
+        static
         {
-            super(position);
-            assert deletionTime != null;
-            assert columnsIndex != null && columnsIndex.size() > 1;
+            BASE_SIZE = ObjectSizes.measure(new LegacyShallowIndexedEntry(0, 0, DeletionTime.LIVE, 0, new int[0], null, 0));
+        }
+
+        private final long indexFilePosition;
+        private final int[] offsets;
+        @Unmetered
+        private final IndexInfo.Serializer idxInfoSerializer;
+        private final DeletionTime deletionTime;
+        private final long headerLength;
+        private final int serializedSize;
+
+        private LegacyShallowIndexedEntry(long dataFilePosition, long indexFilePosition,
+                                          DeletionTime deletionTime, long headerLength,
+                                          int[] offsets, IndexInfo.Serializer idxInfoSerializer,
+                                          int serializedSize)
+        {
+            super(dataFilePosition);
             this.deletionTime = deletionTime;
             this.headerLength = headerLength;
-            this.columnsIndex = columnsIndex;
+            this.indexFilePosition = indexFilePosition;
+            this.offsets = offsets;
+            this.idxInfoSerializer = idxInfoSerializer;
+            this.serializedSize = serializedSize;
         }
 
         @Override
@@ -310,36 +456,574 @@
         }
 
         @Override
-        public List<IndexHelper.IndexInfo> columnsIndex()
+        public long unsharedHeapSize()
         {
-            return columnsIndex;
+            return BASE_SIZE + offsets.length * TypeSizes.sizeof(0);
         }
 
         @Override
-        protected int promotedSize(IndexHelper.IndexInfo.Serializer idxSerializer)
+        public int columnsIndexCount()
         {
-            long size = TypeSizes.sizeofUnsignedVInt(headerLength)
-                      + DeletionTime.serializer.serializedSize(deletionTime)
-                      + TypeSizes.sizeofUnsignedVInt(columnsIndex.size()); // number of entries
-            for (IndexHelper.IndexInfo info : columnsIndex)
-                size += idxSerializer.serializedSize(info);
+            return offsets.length;
+        }
 
-            size += columnsIndex.size() * TypeSizes.sizeof(0);
+        @Override
+        public void serialize(DataOutputPlus out, IndexInfo.Serializer idxInfoSerializer, ByteBuffer indexInfo)
+        {
+            throw new UnsupportedOperationException("serializing legacy index entries is not supported");
+        }
 
-            return Ints.checkedCast(size);
+        @Override
+        public void serializeForCache(DataOutputPlus out)
+        {
+            throw new UnsupportedOperationException("serializing legacy index entries is not supported");
+        }
+
+        @Override
+        public IndexInfoRetriever openWithIndex(FileHandle indexFile)
+        {
+            int fieldsSize = (int) DeletionTime.serializer.serializedSize(deletionTime)
+                             + TypeSizes.sizeof(0); // columnIndexCount
+            indexEntrySizeHistogram.update(serializedSize);
+            indexInfoCountHistogram.update(offsets.length);
+            return new LegacyIndexInfoRetriever(indexFilePosition +
+                                                TypeSizes.sizeof(0L) + // position
+                                                TypeSizes.sizeof(0) + // indexInfoSize
+                                                fieldsSize,
+                                                offsets, indexFile.createReader(), idxInfoSerializer);
+        }
+
+        public static RowIndexEntry<IndexInfo> deserialize(DataInputPlus in, long indexFilePosition,
+                                                IndexInfo.Serializer idxInfoSerializer) throws IOException
+        {
+            long dataFilePosition = in.readLong();
+
+            int size = in.readInt();
+            if (size == 0)
+            {
+                return new RowIndexEntry<>(dataFilePosition);
+            }
+            else if (size <= DatabaseDescriptor.getColumnIndexCacheSize())
+            {
+                return new IndexedEntry(dataFilePosition, in, idxInfoSerializer);
+            }
+            else
+            {
+                DeletionTime deletionTime = DeletionTime.serializer.deserialize(in);
+
+                // For legacy sstables (i.e. sstables pre-"ma", pre-3.0) we have to scan all serialized IndexInfo
+                // objects to calculate the offsets array. However, it might be possible to deserialize all
+                // IndexInfo objects here - but to just skip feels more gentle to the heap/GC.
+
+                int entries = in.readInt();
+                int[] offsets = new int[entries];
+
+                TrackedDataInputPlus tracked = new TrackedDataInputPlus(in);
+                long start = tracked.getBytesRead();
+                long headerLength = 0L;
+                for (int i = 0; i < entries; i++)
+                {
+                    offsets[i] = (int) (tracked.getBytesRead() - start);
+                    if (i == 0)
+                    {
+                        IndexInfo info = idxInfoSerializer.deserialize(tracked);
+                        headerLength = info.offset;
+                    }
+                    else
+                        idxInfoSerializer.skip(tracked);
+                }
+
+                return new LegacyShallowIndexedEntry(dataFilePosition, indexFilePosition, deletionTime, headerLength, offsets, idxInfoSerializer, size);
+            }
+        }
+
+        static long deserializePositionAndSkip(DataInputPlus in) throws IOException
+        {
+            long position = in.readLong();
+
+            int size = in.readInt();
+            if (size > 0)
+                in.skipBytesFully(size);
+
+            return position;
+        }
+    }
+
+    private static final class LegacyIndexInfoRetriever extends FileIndexInfoRetriever
+    {
+        private final int[] offsets;
+
+        private LegacyIndexInfoRetriever(long indexFilePosition, int[] offsets, FileDataInput reader, IndexInfo.Serializer idxInfoSerializer)
+        {
+            super(indexFilePosition, reader, idxInfoSerializer);
+            this.offsets = offsets;
+        }
+
+        IndexInfo fetchIndex(int index) throws IOException
+        {
+            retrievals++;
+
+            // seek to posision of IndexInfo
+            indexReader.seek(indexInfoFilePosition + offsets[index]);
+
+            // deserialize IndexInfo
+            return idxInfoSerializer.deserialize(indexReader);
+        }
+    }
+
+    /**
+     * An entry in the row index for a row whose columns are indexed - used for both legacy and current formats.
+     */
+    private static final class IndexedEntry extends RowIndexEntry<IndexInfo>
+    {
+        private static final long BASE_SIZE;
+
+        static
+        {
+            BASE_SIZE = ObjectSizes.measure(new IndexedEntry(0, DeletionTime.LIVE, 0, null, null, 0, null));
+        }
+
+        private final DeletionTime deletionTime;
+        private final long headerLength;
+
+        private final IndexInfo[] columnsIndex;
+        private final int[] offsets;
+        private final int indexedPartSize;
+        @Unmetered
+        private final ISerializer<IndexInfo> idxInfoSerializer;
+
+        private IndexedEntry(long dataFilePosition, DeletionTime deletionTime, long headerLength,
+                             IndexInfo[] columnsIndex, int[] offsets,
+                             int indexedPartSize, ISerializer<IndexInfo> idxInfoSerializer)
+        {
+            super(dataFilePosition);
+
+            this.headerLength = headerLength;
+            this.deletionTime = deletionTime;
+
+            this.columnsIndex = columnsIndex;
+            this.offsets = offsets;
+            this.indexedPartSize = indexedPartSize;
+            this.idxInfoSerializer = idxInfoSerializer;
+        }
+
+        private IndexedEntry(long dataFilePosition, DataInputPlus in,
+                             DeletionTime deletionTime, long headerLength, int columnIndexCount,
+                             IndexInfo.Serializer idxInfoSerializer,
+                             Version version, int indexedPartSize) throws IOException
+        {
+            super(dataFilePosition);
+
+            this.headerLength = headerLength;
+            this.deletionTime = deletionTime;
+            int columnsIndexCount = columnIndexCount;
+
+            this.columnsIndex = new IndexInfo[columnsIndexCount];
+            for (int i = 0; i < columnsIndexCount; i++)
+                this.columnsIndex[i] = idxInfoSerializer.deserialize(in);
+
+            int[] offsets = null;
+            if (version.storeRows())
+            {
+                offsets = new int[this.columnsIndex.length];
+                for (int i = 0; i < offsets.length; i++)
+                    offsets[i] = in.readInt();
+            }
+            this.offsets = offsets;
+
+            this.indexedPartSize = indexedPartSize;
+
+            this.idxInfoSerializer = idxInfoSerializer;
+        }
+
+        /**
+         * Constructor called from {@link Serializer#deserializeForCache(org.apache.cassandra.io.util.DataInputPlus)}.
+         */
+        private IndexedEntry(long dataFilePosition, DataInputPlus in, IndexInfo.Serializer idxInfoSerializer, Version version) throws IOException
+        {
+            super(dataFilePosition);
+
+            this.headerLength = in.readUnsignedVInt();
+            this.deletionTime = DeletionTime.serializer.deserialize(in);
+            int columnsIndexCount = (int) in.readUnsignedVInt();
+
+            TrackedDataInputPlus trackedIn = new TrackedDataInputPlus(in);
+
+            this.columnsIndex = new IndexInfo[columnsIndexCount];
+            for (int i = 0; i < columnsIndexCount; i++)
+                this.columnsIndex[i] = idxInfoSerializer.deserialize(trackedIn);
+
+            this.offsets = null;
+
+            this.indexedPartSize = (int) trackedIn.getBytesRead();
+
+            this.idxInfoSerializer = idxInfoSerializer;
+        }
+
+        /**
+         * Constructor called from {@link LegacyShallowIndexedEntry#deserialize(org.apache.cassandra.io.util.DataInputPlus, long, org.apache.cassandra.io.sstable.IndexInfo.Serializer)}.
+         * Only for legacy sstables.
+         */
+        private IndexedEntry(long dataFilePosition, DataInputPlus in, IndexInfo.Serializer idxInfoSerializer) throws IOException
+        {
+            super(dataFilePosition);
+
+            long headerLength = 0;
+            this.deletionTime = DeletionTime.serializer.deserialize(in);
+            int columnsIndexCount = in.readInt();
+
+            TrackedDataInputPlus trackedIn = new TrackedDataInputPlus(in);
+
+            this.columnsIndex = new IndexInfo[columnsIndexCount];
+            for (int i = 0; i < columnsIndexCount; i++)
+            {
+                this.columnsIndex[i] = idxInfoSerializer.deserialize(trackedIn);
+                if (i == 0)
+                    headerLength = this.columnsIndex[i].offset;
+            }
+            this.headerLength = headerLength;
+
+            this.offsets = null;
+
+            this.indexedPartSize = (int) trackedIn.getBytesRead();
+
+            this.idxInfoSerializer = idxInfoSerializer;
+        }
+
+        @Override
+        public boolean indexOnHeap()
+        {
+            return true;
+        }
+
+        @Override
+        public int columnsIndexCount()
+        {
+            return columnsIndex.length;
+        }
+
+        @Override
+        public DeletionTime deletionTime()
+        {
+            return deletionTime;
+        }
+
+        @Override
+        public long headerLength()
+        {
+            return headerLength;
+        }
+
+        @Override
+        public IndexInfoRetriever openWithIndex(FileHandle indexFile)
+        {
+            indexEntrySizeHistogram.update(serializedSize(deletionTime, headerLength, columnsIndex.length) + indexedPartSize);
+            indexInfoCountHistogram.update(columnsIndex.length);
+            return new IndexInfoRetriever()
+            {
+                private int retrievals;
+
+                @Override
+                public IndexInfo columnsIndex(int index)
+                {
+                    retrievals++;
+                    return columnsIndex[index];
+                }
+
+                public void close()
+                {
+                    indexInfoGetsHistogram.update(retrievals);
+                }
+            };
         }
 
         @Override
         public long unsharedHeapSize()
         {
             long entrySize = 0;
-            for (IndexHelper.IndexInfo idx : columnsIndex)
+            for (IndexInfo idx : columnsIndex)
                 entrySize += idx.unsharedHeapSize();
-
             return BASE_SIZE
-                   + entrySize
-                   + deletionTime.unsharedHeapSize()
-                   + ObjectSizes.sizeOfReferenceArray(columnsIndex.size());
+                + entrySize
+                + ObjectSizes.sizeOfReferenceArray(columnsIndex.length);
+        }
+
+        @Override
+        public void serialize(DataOutputPlus out, IndexInfo.Serializer idxInfoSerializer, ByteBuffer indexInfo) throws IOException
+        {
+            assert indexedPartSize != Integer.MIN_VALUE;
+
+            out.writeUnsignedVInt(position);
+
+            out.writeUnsignedVInt(serializedSize(deletionTime, headerLength, columnsIndex.length) + indexedPartSize);
+
+            out.writeUnsignedVInt(headerLength);
+            DeletionTime.serializer.serialize(deletionTime, out);
+            out.writeUnsignedVInt(columnsIndex.length);
+            for (IndexInfo info : columnsIndex)
+                idxInfoSerializer.serialize(info, out);
+            for (int offset : offsets)
+                out.writeInt(offset);
+        }
+
+        @Override
+        public void serializeForCache(DataOutputPlus out) throws IOException
+        {
+            out.writeUnsignedVInt(position);
+            out.writeByte(CACHE_INDEXED);
+
+            out.writeUnsignedVInt(headerLength);
+            DeletionTime.serializer.serialize(deletionTime, out);
+            out.writeUnsignedVInt(columnsIndexCount());
+
+            for (IndexInfo indexInfo : columnsIndex)
+                idxInfoSerializer.serialize(indexInfo, out);
+        }
+
+        static void skipForCache(DataInputPlus in) throws IOException
+        {
+            /*long headerLength =*/in.readUnsignedVInt();
+            /*DeletionTime deletionTime = */DeletionTime.serializer.skip(in);
+            /*int columnsIndexCount = (int)*/in.readUnsignedVInt();
+
+            /*int indexedPartSize = (int)*/in.readUnsignedVInt();
+        }
+    }
+
+    /**
+     * An entry in the row index for a row whose columns are indexed and the {@link IndexInfo} objects
+     * are not read into the key cache.
+     */
+    private static final class ShallowIndexedEntry extends RowIndexEntry<IndexInfo>
+    {
+        private static final long BASE_SIZE;
+
+        static
+        {
+            BASE_SIZE = ObjectSizes.measure(new ShallowIndexedEntry(0, 0, DeletionTime.LIVE, 0, 10, 0, null));
+        }
+
+        private final long indexFilePosition;
+
+        private final DeletionTime deletionTime;
+        private final long headerLength;
+        private final int columnsIndexCount;
+
+        private final int indexedPartSize;
+        private final int offsetsOffset;
+        @Unmetered
+        private final ISerializer<IndexInfo> idxInfoSerializer;
+        private final int fieldsSerializedSize;
+
+        /**
+         * See {@link #create(long, long, DeletionTime, long, int, int, List, int[], ISerializer)} for a description
+         * of the parameters.
+         */
+        private ShallowIndexedEntry(long dataFilePosition, long indexFilePosition,
+                                    DeletionTime deletionTime, long headerLength, int columnIndexCount,
+                                    int indexedPartSize, ISerializer<IndexInfo> idxInfoSerializer)
+        {
+            super(dataFilePosition);
+
+            assert columnIndexCount > 1;
+
+            this.indexFilePosition = indexFilePosition;
+            this.headerLength = headerLength;
+            this.deletionTime = deletionTime;
+            this.columnsIndexCount = columnIndexCount;
+
+            this.indexedPartSize = indexedPartSize;
+            this.idxInfoSerializer = idxInfoSerializer;
+
+            this.fieldsSerializedSize = serializedSize(deletionTime, headerLength, columnIndexCount);
+            this.offsetsOffset = indexedPartSize + fieldsSerializedSize - columnsIndexCount * TypeSizes.sizeof(0);
+        }
+
+        /**
+         * Constructor for key-cache deserialization
+         */
+        private ShallowIndexedEntry(long dataFilePosition, DataInputPlus in, IndexInfo.Serializer idxInfoSerializer) throws IOException
+        {
+            super(dataFilePosition);
+
+            this.indexFilePosition = in.readUnsignedVInt();
+
+            this.headerLength = in.readUnsignedVInt();
+            this.deletionTime = DeletionTime.serializer.deserialize(in);
+            this.columnsIndexCount = (int) in.readUnsignedVInt();
+
+            this.indexedPartSize = (int) in.readUnsignedVInt();
+
+            this.idxInfoSerializer = idxInfoSerializer;
+
+            this.fieldsSerializedSize = serializedSize(deletionTime, headerLength, columnsIndexCount);
+            this.offsetsOffset = indexedPartSize + fieldsSerializedSize - columnsIndexCount * TypeSizes.sizeof(0);
+        }
+
+        @Override
+        public int columnsIndexCount()
+        {
+            return columnsIndexCount;
+        }
+
+        @Override
+        public DeletionTime deletionTime()
+        {
+            return deletionTime;
+        }
+
+        @Override
+        public long headerLength()
+        {
+            return headerLength;
+        }
+
+        @Override
+        public IndexInfoRetriever openWithIndex(FileHandle indexFile)
+        {
+            indexEntrySizeHistogram.update(indexedPartSize + fieldsSerializedSize);
+            indexInfoCountHistogram.update(columnsIndexCount);
+            return new ShallowInfoRetriever(indexFilePosition +
+                                            VIntCoding.computeUnsignedVIntSize(position) +
+                                            VIntCoding.computeUnsignedVIntSize(indexedPartSize + fieldsSerializedSize) +
+                                            fieldsSerializedSize,
+                                            offsetsOffset - fieldsSerializedSize,
+                                            indexFile.createReader(), idxInfoSerializer);
+        }
+
+        @Override
+        public long unsharedHeapSize()
+        {
+            return BASE_SIZE;
+        }
+
+        @Override
+        public void serialize(DataOutputPlus out, IndexInfo.Serializer idxInfoSerializer, ByteBuffer indexInfo) throws IOException
+        {
+            out.writeUnsignedVInt(position);
+
+            out.writeUnsignedVInt(fieldsSerializedSize + indexInfo.limit());
+
+            out.writeUnsignedVInt(headerLength);
+            DeletionTime.serializer.serialize(deletionTime, out);
+            out.writeUnsignedVInt(columnsIndexCount);
+
+            out.write(indexInfo);
+        }
+
+        static long deserializePositionAndSkip(DataInputPlus in) throws IOException
+        {
+            long position = in.readUnsignedVInt();
+
+            int size = (int) in.readUnsignedVInt();
+            if (size > 0)
+                in.skipBytesFully(size);
+
+            return position;
+        }
+
+        @Override
+        public void serializeForCache(DataOutputPlus out) throws IOException
+        {
+            out.writeUnsignedVInt(position);
+            out.writeByte(CACHE_INDEXED_SHALLOW);
+
+            out.writeUnsignedVInt(indexFilePosition);
+
+            out.writeUnsignedVInt(headerLength);
+            DeletionTime.serializer.serialize(deletionTime, out);
+            out.writeUnsignedVInt(columnsIndexCount);
+
+            out.writeUnsignedVInt(indexedPartSize);
+        }
+
+        static void skipForCache(DataInputPlus in) throws IOException
+        {
+            /*long indexFilePosition =*/in.readUnsignedVInt();
+
+            /*long headerLength =*/in.readUnsignedVInt();
+            /*DeletionTime deletionTime = */DeletionTime.serializer.skip(in);
+            /*int columnsIndexCount = (int)*/in.readUnsignedVInt();
+
+            /*int indexedPartSize = (int)*/in.readUnsignedVInt();
+        }
+    }
+
+    private static final class ShallowInfoRetriever extends FileIndexInfoRetriever
+    {
+        private final int offsetsOffset;
+
+        private ShallowInfoRetriever(long indexInfoFilePosition, int offsetsOffset,
+                                     FileDataInput indexReader, ISerializer<IndexInfo> idxInfoSerializer)
+        {
+            super(indexInfoFilePosition, indexReader, idxInfoSerializer);
+            this.offsetsOffset = offsetsOffset;
+        }
+
+        IndexInfo fetchIndex(int index) throws IOException
+        {
+            retrievals++;
+
+            // seek to position in "offsets to IndexInfo" table
+            indexReader.seek(indexInfoFilePosition + offsetsOffset + index * TypeSizes.sizeof(0));
+
+            // read offset of IndexInfo
+            int indexInfoPos = indexReader.readInt();
+
+            // seek to posision of IndexInfo
+            indexReader.seek(indexInfoFilePosition + indexInfoPos);
+
+            // finally, deserialize IndexInfo
+            return idxInfoSerializer.deserialize(indexReader);
+        }
+    }
+
+    /**
+     * Base class to access {@link IndexInfo} objects.
+     */
+    public interface IndexInfoRetriever extends AutoCloseable
+    {
+        IndexInfo columnsIndex(int index) throws IOException;
+
+        void close() throws IOException;
+    }
+
+    /**
+     * Base class to access {@link IndexInfo} objects on disk that keeps already
+     * read {@link IndexInfo} on heap.
+     */
+    private abstract static class FileIndexInfoRetriever implements IndexInfoRetriever
+    {
+        final long indexInfoFilePosition;
+        final ISerializer<IndexInfo> idxInfoSerializer;
+        final FileDataInput indexReader;
+        int retrievals;
+
+        /**
+         *
+         * @param indexInfoFilePosition offset of first serialized {@link IndexInfo} object
+         * @param indexReader file data input to access the index file, closed by this instance
+         * @param idxInfoSerializer the index serializer to deserialize {@link IndexInfo} objects
+         */
+        FileIndexInfoRetriever(long indexInfoFilePosition, FileDataInput indexReader, ISerializer<IndexInfo> idxInfoSerializer)
+        {
+            this.indexInfoFilePosition = indexInfoFilePosition;
+            this.idxInfoSerializer = idxInfoSerializer;
+            this.indexReader = indexReader;
+        }
+
+        public final IndexInfo columnsIndex(int index) throws IOException
+        {
+            return fetchIndex(index);
+        }
+
+        abstract IndexInfo fetchIndex(int index) throws IOException;
+
+        public void close() throws IOException
+        {
+            indexReader.close();
+
+            indexInfoGetsHistogram.update(retrievals);
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/db/RowUpdateBuilder.java b/src/java/org/apache/cassandra/db/RowUpdateBuilder.java
deleted file mode 100644
index c4b4c75..0000000
--- a/src/java/org/apache/cassandra/db/RowUpdateBuilder.java
+++ /dev/null
@@ -1,402 +0,0 @@
-/*
- * 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.db;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import org.apache.cassandra.cql3.ColumnIdentifier;
-import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.db.marshal.SetType;
-import org.apache.cassandra.db.marshal.UTF8Type;
-import org.apache.cassandra.db.rows.*;
-import org.apache.cassandra.db.context.CounterContext;
-import org.apache.cassandra.db.partitions.*;
-import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.db.marshal.ListType;
-import org.apache.cassandra.db.marshal.MapType;
-import org.apache.cassandra.utils.*;
-
-/**
- * Convenience object to create single row updates.
- *
- * This is meant for system table update, when performance is not of the utmost importance.
- */
-public class RowUpdateBuilder
-{
-    private final PartitionUpdate update;
-
-    private final long timestamp;
-    private final int ttl;
-    private final int localDeletionTime;
-
-    private final DeletionTime deletionTime;
-
-    private final Mutation mutation;
-
-    private Row.Builder regularBuilder;
-    private Row.Builder staticBuilder;
-
-    private boolean useRowMarker = true;
-
-    private RowUpdateBuilder(PartitionUpdate update, long timestamp, int ttl, int localDeletionTime, Mutation mutation)
-    {
-        this.update = update;
-
-        this.timestamp = timestamp;
-        this.ttl = ttl;
-        this.localDeletionTime = localDeletionTime;
-        this.deletionTime = new DeletionTime(timestamp, localDeletionTime);
-
-        // note that the created mutation may get further update later on, so we don't use the ctor that create a singletonMap
-        // underneath (this class if for convenience, not performance)
-        this.mutation = mutation == null ? new Mutation(update.metadata().ksName, update.partitionKey()).add(update) : mutation;
-    }
-
-    private RowUpdateBuilder(PartitionUpdate update, long timestamp, int ttl, Mutation mutation)
-    {
-        this(update, timestamp, ttl, FBUtilities.nowInSeconds(), mutation);
-    }
-
-    private void startRow(Clustering clustering)
-    {
-        assert staticBuilder == null : "Cannot update both static and non-static columns with the same RowUpdateBuilder object";
-        assert regularBuilder == null : "Cannot add the clustering twice to the same row";
-
-        regularBuilder = BTreeRow.unsortedBuilder(FBUtilities.nowInSeconds());
-        regularBuilder.newRow(clustering);
-
-        // If a CQL table, add the "row marker"
-        if (update.metadata().isCQLTable() && useRowMarker)
-            regularBuilder.addPrimaryKeyLivenessInfo(LivenessInfo.create(update.metadata(), timestamp, ttl, localDeletionTime));
-    }
-
-    private Row.Builder builder()
-    {
-        assert staticBuilder == null : "Cannot update both static and non-static columns with the same RowUpdateBuilder object";
-        if (regularBuilder == null)
-        {
-            // we don't force people to call clustering() if the table has no clustering, so call it ourselves
-            assert update.metadata().comparator.size() == 0 : "Missing call to clustering()";
-            startRow(Clustering.EMPTY);
-        }
-        return regularBuilder;
-    }
-
-    private Row.Builder staticBuilder()
-    {
-        assert regularBuilder == null : "Cannot update both static and non-static columns with the same RowUpdateBuilder object";
-        if (staticBuilder == null)
-        {
-            staticBuilder = BTreeRow.unsortedBuilder(FBUtilities.nowInSeconds());
-            staticBuilder.newRow(Clustering.STATIC_CLUSTERING);
-        }
-        return staticBuilder;
-    }
-
-    private Row.Builder builder(ColumnDefinition c)
-    {
-        return c.isStatic() ? staticBuilder() : builder();
-    }
-
-    public RowUpdateBuilder(CFMetaData metadata, long timestamp, Object partitionKey)
-    {
-        this(metadata, FBUtilities.nowInSeconds(), timestamp, partitionKey);
-    }
-
-    public RowUpdateBuilder(CFMetaData metadata, int localDeletionTime, long timestamp, Object partitionKey)
-    {
-        this(metadata, localDeletionTime, timestamp, metadata.params.defaultTimeToLive, partitionKey);
-    }
-
-    public RowUpdateBuilder(CFMetaData metadata, long timestamp, int ttl, Object partitionKey)
-    {
-        this(metadata, FBUtilities.nowInSeconds(), timestamp, ttl, partitionKey);
-    }
-
-    public RowUpdateBuilder(CFMetaData metadata, int localDeletionTime, long timestamp, int ttl, Object partitionKey)
-    {
-        this(new PartitionUpdate(metadata, makeKey(metadata, partitionKey), metadata.partitionColumns(), 1), timestamp, ttl, localDeletionTime, null);
-    }
-
-    public RowUpdateBuilder(CFMetaData metadata, long timestamp, Mutation mutation)
-    {
-        this(metadata, timestamp, LivenessInfo.NO_TTL, mutation);
-    }
-
-    public RowUpdateBuilder(CFMetaData metadata, long timestamp, int ttl, Mutation mutation)
-    {
-        this(getOrAdd(metadata, mutation), timestamp, ttl, mutation);
-    }
-
-    public RowUpdateBuilder(PartitionUpdate update, long timestamp, int ttl)
-    {
-        this(update, timestamp, ttl, null);
-    }
-
-    // This must be called before any addition or deletion if used.
-    public RowUpdateBuilder noRowMarker()
-    {
-        this.useRowMarker = false;
-        return this;
-    }
-
-    public RowUpdateBuilder clustering(Object... clusteringValues)
-    {
-        assert clusteringValues.length == update.metadata().comparator.size()
-             : "Invalid clustering values length. Expected: " + update.metadata().comparator.size() + " got: " + clusteringValues.length;
-
-        startRow(clusteringValues.length == 0 ? Clustering.EMPTY : update.metadata().comparator.make(clusteringValues));
-        return this;
-    }
-
-    public Mutation build()
-    {
-        Row.Builder builder = regularBuilder == null ? staticBuilder : regularBuilder;
-        if (builder != null)
-            update.add(builder.build());
-        return mutation;
-    }
-
-    public PartitionUpdate buildUpdate()
-    {
-        build();
-        return update;
-    }
-
-    private static void deleteRow(PartitionUpdate update, long timestamp, int localDeletionTime, Object... clusteringValues)
-    {
-        assert clusteringValues.length == update.metadata().comparator.size() || (clusteringValues.length == 0 && !update.columns().statics.isEmpty());
-
-        boolean isStatic = clusteringValues.length != update.metadata().comparator.size();
-        Row.Builder builder = BTreeRow.sortedBuilder();
-
-        if (isStatic)
-            builder.newRow(Clustering.STATIC_CLUSTERING);
-        else
-            builder.newRow(clusteringValues.length == 0 ? Clustering.EMPTY : update.metadata().comparator.make(clusteringValues));
-        builder.addRowDeletion(Row.Deletion.regular(new DeletionTime(timestamp, localDeletionTime)));
-
-        update.add(builder.build());
-    }
-
-    public static Mutation deleteRow(CFMetaData metadata, long timestamp, Mutation mutation, Object... clusteringValues)
-    {
-        deleteRow(getOrAdd(metadata, mutation), timestamp, FBUtilities.nowInSeconds(), clusteringValues);
-        return mutation;
-    }
-
-    public static Mutation deleteRow(CFMetaData metadata, long timestamp, Object key, Object... clusteringValues)
-    {
-        return deleteRowAt(metadata, timestamp, FBUtilities.nowInSeconds(), key, clusteringValues);
-    }
-
-    public static Mutation deleteRowAt(CFMetaData metadata, long timestamp, int localDeletionTime, Object key, Object... clusteringValues)
-    {
-        PartitionUpdate update = new PartitionUpdate(metadata, makeKey(metadata, key), metadata.partitionColumns(), 0);
-        deleteRow(update, timestamp, localDeletionTime, clusteringValues);
-        // note that the created mutation may get further update later on, so we don't use the ctor that create a singletonMap
-        // underneath (this class if for convenience, not performance)
-        return new Mutation(update.metadata().ksName, update.partitionKey()).add(update);
-    }
-
-    private static DecoratedKey makeKey(CFMetaData metadata, Object... partitionKey)
-    {
-        if (partitionKey.length == 1 && partitionKey[0] instanceof DecoratedKey)
-            return (DecoratedKey)partitionKey[0];
-
-        ByteBuffer key = CFMetaData.serializePartitionKey(metadata.getKeyValidatorAsClusteringComparator().make(partitionKey));
-        return metadata.decorateKey(key);
-    }
-
-    private static PartitionUpdate getOrAdd(CFMetaData metadata, Mutation mutation)
-    {
-        PartitionUpdate upd = mutation.get(metadata);
-        if (upd == null)
-        {
-            upd = new PartitionUpdate(metadata, mutation.key(), metadata.partitionColumns(), 1);
-            mutation.add(upd);
-        }
-        return upd;
-    }
-
-    public RowUpdateBuilder resetCollection(String columnName)
-    {
-        ColumnDefinition c = getDefinition(columnName);
-        assert c != null : "Cannot find column " + columnName;
-        assert c.isStatic() || update.metadata().comparator.size() == 0 || regularBuilder != null : "Cannot set non static column " + c + " since no clustering has been provided";
-        assert c.type.isCollection() && c.type.isMultiCell();
-        builder(c).addComplexDeletion(c, new DeletionTime(timestamp - 1, localDeletionTime));
-        return this;
-    }
-
-    public RowUpdateBuilder addRangeTombstone(RangeTombstone rt)
-    {
-        update.add(rt);
-        return this;
-    }
-
-    public RowUpdateBuilder addRangeTombstone(Slice slice)
-    {
-        return addRangeTombstone(new RangeTombstone(slice, deletionTime));
-    }
-
-    public RowUpdateBuilder addRangeTombstone(Object start, Object end)
-    {
-        ClusteringComparator cmp = update.metadata().comparator;
-        Slice slice = Slice.make(cmp.make(start), cmp.make(end));
-        return addRangeTombstone(slice);
-    }
-
-    public RowUpdateBuilder add(String columnName, Object value)
-    {
-        ColumnDefinition c = getDefinition(columnName);
-        assert c != null : "Cannot find column " + columnName;
-        return add(c, value);
-    }
-
-    private Cell makeCell(ColumnDefinition c, ByteBuffer value, CellPath path)
-    {
-        return value == null
-             ? BufferCell.tombstone(c, timestamp, localDeletionTime)
-             : (ttl == LivenessInfo.NO_TTL ? BufferCell.live(update.metadata(), c, timestamp, value, path) : BufferCell.expiring(c, timestamp, ttl, localDeletionTime, value, path));
-    }
-
-    public RowUpdateBuilder add(ColumnDefinition columnDefinition, Object value)
-    {
-        assert columnDefinition.isStatic() || update.metadata().comparator.size() == 0 || regularBuilder != null : "Cannot set non static column " + columnDefinition + " since no clustering hasn't been provided";
-        builder(columnDefinition).addCell(makeCell(columnDefinition, bb(value, columnDefinition.type), null));
-        return this;
-    }
-
-    public RowUpdateBuilder delete(String columnName)
-    {
-        ColumnDefinition c = getDefinition(columnName);
-        assert c != null : "Cannot find column " + columnName;
-        return delete(c);
-    }
-
-    public RowUpdateBuilder delete(ColumnDefinition columnDefinition)
-    {
-        return add(columnDefinition, null);
-    }
-
-    private static ByteBuffer bb(Object value, AbstractType<?> type)
-    {
-        if (value == null)
-            return null;
-
-        if (value instanceof ByteBuffer)
-            return (ByteBuffer)value;
-
-        if (type.isCounter())
-        {
-            // See UpdateParameters.addCounter()
-            assert value instanceof Long : "Attempted to adjust Counter cell with non-long value.";
-            return CounterContext.instance().createGlobal(CounterId.getLocalId(), 1, (Long)value);
-        }
-        return ((AbstractType)type).decompose(value);
-    }
-
-    public RowUpdateBuilder map(String columnName, Map<?, ?> map)
-    {
-        resetCollection(columnName);
-        for (Map.Entry<?, ?> entry : map.entrySet())
-            addMapEntry(columnName, entry.getKey(), entry.getValue());
-        return this;
-    }
-
-    public RowUpdateBuilder set(String columnName, Set<?> set)
-    {
-        resetCollection(columnName);
-        for (Object element : set)
-            addSetEntry(columnName, element);
-        return this;
-    }
-
-    public RowUpdateBuilder frozenList(String columnName, List<?> list)
-    {
-        ColumnDefinition c = getDefinition(columnName);
-        assert c.isStatic() || regularBuilder != null : "Cannot set non static column " + c + " since no clustering has been provided";
-        assert c.type instanceof ListType && !c.type.isMultiCell() : "Column " + c + " is not a frozen list";
-        builder(c).addCell(makeCell(c, bb(((AbstractType)c.type).decompose(list), c.type), null));
-        return this;
-    }
-
-    public RowUpdateBuilder frozenSet(String columnName, Set<?> set)
-    {
-        ColumnDefinition c = getDefinition(columnName);
-        assert c.isStatic() || regularBuilder != null : "Cannot set non static column " + c + " since no clustering has been provided";
-        assert c.type instanceof SetType && !c.type.isMultiCell() : "Column " + c + " is not a frozen set";
-        builder(c).addCell(makeCell(c, bb(((AbstractType)c.type).decompose(set), c.type), null));
-        return this;
-    }
-
-    public RowUpdateBuilder frozenMap(String columnName, Map<?, ?> map)
-    {
-        ColumnDefinition c = getDefinition(columnName);
-        assert c.isStatic() || regularBuilder != null : "Cannot set non static column " + c + " since no clustering has been provided";
-        assert c.type instanceof MapType && !c.type.isMultiCell() : "Column " + c + " is not a frozen map";
-        builder(c).addCell(makeCell(c, bb(((AbstractType)c.type).decompose(map), c.type), null));
-        return this;
-    }
-
-    public RowUpdateBuilder addMapEntry(String columnName, Object key, Object value)
-    {
-        ColumnDefinition c = getDefinition(columnName);
-        assert c.isStatic() || update.metadata().comparator.size() == 0 || regularBuilder != null : "Cannot set non static column " + c + " since no clustering has been provided";
-        assert c.type instanceof MapType && c.type.isMultiCell() : "Column " + c + " is not a non-frozen map";
-        MapType mt = (MapType)c.type;
-        builder(c).addCell(makeCell(c, bb(value, mt.getValuesType()), CellPath.create(bb(key, mt.getKeysType()))));
-        return this;
-    }
-
-    public RowUpdateBuilder addListEntry(String columnName, Object value)
-    {
-        ColumnDefinition c = getDefinition(columnName);
-        assert c.isStatic() || regularBuilder != null : "Cannot set non static column " + c + " since no clustering has been provided";
-        assert c.type instanceof ListType && c.type.isMultiCell() : "Column " + c + " is not a non-frozen list";
-        ListType lt = (ListType)c.type;
-        builder(c).addCell(makeCell(c, bb(value, lt.getElementsType()), CellPath.create(ByteBuffer.wrap(UUIDGen.getTimeUUIDBytes()))));
-        return this;
-    }
-
-    public RowUpdateBuilder addSetEntry(String columnName, Object value)
-    {
-        ColumnDefinition c = getDefinition(columnName);
-        assert c.isStatic() || regularBuilder != null : "Cannot set non static column " + c + " since no clustering has been provided";
-        assert c.type instanceof SetType && c.type.isMultiCell() : "Column " + c + " is not a non-frozen set";
-        SetType st = (SetType)c.type;
-        builder(c).addCell(makeCell(c, ByteBufferUtil.EMPTY_BYTE_BUFFER, CellPath.create(bb(value, st.getElementsType()))));
-        return this;
-    }
-
-    private ColumnDefinition getDefinition(String name)
-    {
-        return update.metadata().getColumnDefinitionForCQL(new ColumnIdentifier(name, true));
-    }
-
-    public UnfilteredRowIterator unfilteredIterator()
-    {
-        return update.unfilteredIterator();
-    }
-}
diff --git a/src/java/org/apache/cassandra/db/SchemaCheckVerbHandler.java b/src/java/org/apache/cassandra/db/SchemaCheckVerbHandler.java
index 4270a24..be501de 100644
--- a/src/java/org/apache/cassandra/db/SchemaCheckVerbHandler.java
+++ b/src/java/org/apache/cassandra/db/SchemaCheckVerbHandler.java
@@ -36,7 +36,14 @@
     public void doVerb(MessageIn message, int id)
     {
         logger.trace("Received schema check request.");
-        MessageOut<UUID> response = new MessageOut<UUID>(MessagingService.Verb.INTERNAL_RESPONSE, Schema.instance.getVersion(), UUIDSerializer.serializer);
+
+        /*
+        3.11 is special here: We return the 3.0 compatible version, if the requesting node
+        is running 3.0. Otherwise the "real" schema version.
+        */
+        MessageOut<UUID> response = new MessageOut<>(MessagingService.Verb.INTERNAL_RESPONSE,
+                                                     Schema.instance.getVersion(),
+                                                     UUIDSerializer.serializer);
         MessagingService.instance().sendReply(response, id, message.from);
     }
 }
diff --git a/src/java/org/apache/cassandra/db/SerializationHeader.java b/src/java/org/apache/cassandra/db/SerializationHeader.java
index 5c4f518..5b62b0a 100644
--- a/src/java/org/apache/cassandra/db/SerializationHeader.java
+++ b/src/java/org/apache/cassandra/db/SerializationHeader.java
@@ -22,14 +22,11 @@
 import java.util.*;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.db.marshal.BytesType;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.db.marshal.TypeParser;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
@@ -75,25 +72,6 @@
         return new SerializationHeader(true, metadata, metadata.partitionColumns(), EncodingStats.NO_STATS);
     }
 
-    public static SerializationHeader forKeyCache(CFMetaData metadata)
-    {
-        // We don't save type information in the key cache (we could change
-        // that but it's easier right now), so instead we simply use BytesType
-        // for both serialization and deserialization. Note that we also only
-        // serializer clustering prefixes in the key cache, so only the clusteringTypes
-        // really matter.
-        int size = metadata.clusteringColumns().size();
-        List<AbstractType<?>> clusteringTypes = new ArrayList<>(size);
-        for (int i = 0; i < size; i++)
-            clusteringTypes.add(BytesType.instance);
-        return new SerializationHeader(false,
-                                       BytesType.instance,
-                                       clusteringTypes,
-                                       PartitionColumns.NONE,
-                                       EncodingStats.NO_STATS,
-                                       Collections.<ByteBuffer, AbstractType<?>>emptyMap());
-    }
-
     public static SerializationHeader make(CFMetaData metadata, Collection<SSTableReader> sstables)
     {
         // The serialization header has to be computed before the start of compaction (since it's used to write)
@@ -139,17 +117,12 @@
     {
         this(isForSSTable,
              metadata.getKeyValidator(),
-             typesOf(metadata.clusteringColumns()),
+             metadata.comparator.subtypes(),
              columns,
              stats,
              null);
     }
 
-    private static List<AbstractType<?>> typesOf(List<ColumnDefinition> columns)
-    {
-        return ImmutableList.copyOf(Lists.transform(columns, column -> column.type));
-    }
-
     public PartitionColumns columns()
     {
         return columns;
@@ -317,6 +290,18 @@
             this.stats = stats;
         }
 
+        /**
+         * <em>Only</em> exposed for {@link org.apache.cassandra.io.sstable.SSTableHeaderFix}.
+         */
+        public static Component buildComponentForTools(AbstractType<?> keyType,
+                                                       List<AbstractType<?>> clusteringTypes,
+                                                       Map<ByteBuffer, AbstractType<?>> staticColumns,
+                                                       Map<ByteBuffer, AbstractType<?>> regularColumns,
+                                                       EncodingStats stats)
+        {
+            return new Component(keyType, clusteringTypes, staticColumns, regularColumns, stats);
+        }
+
         public MetadataType getType()
         {
             return MetadataType.HEADER;
@@ -437,7 +422,7 @@
             EncodingStats stats = EncodingStats.serializer.deserialize(in);
 
             AbstractType<?> keyType = metadata.getKeyValidator();
-            List<AbstractType<?>> clusteringTypes = typesOf(metadata.clusteringColumns());
+            List<AbstractType<?>> clusteringTypes = metadata.comparator.subtypes();
 
             Columns statics, regulars;
             if (selection == null)
diff --git a/src/java/org/apache/cassandra/db/Serializers.java b/src/java/org/apache/cassandra/db/Serializers.java
index bf340e7..02c9995 100644
--- a/src/java/org/apache/cassandra/db/Serializers.java
+++ b/src/java/org/apache/cassandra/db/Serializers.java
@@ -20,10 +20,15 @@
 import java.io.*;
 import java.nio.ByteBuffer;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.CompositeType;
 import org.apache.cassandra.io.ISerializer;
+import org.apache.cassandra.io.sstable.IndexInfo;
+import org.apache.cassandra.io.sstable.format.big.BigFormat;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.io.sstable.format.Version;
@@ -36,36 +41,66 @@
 {
     private final CFMetaData metadata;
 
+    private Map<Version, IndexInfo.Serializer> otherVersionClusteringSerializers;
+
+    private final IndexInfo.Serializer latestVersionIndexSerializer;
+
     public Serializers(CFMetaData metadata)
     {
         this.metadata = metadata;
+        this.latestVersionIndexSerializer = new IndexInfo.Serializer(BigFormat.latestVersion,
+                                                                     indexEntryClusteringPrefixSerializer(BigFormat.latestVersion, SerializationHeader.makeWithoutStats(metadata)));
+    }
+
+    IndexInfo.Serializer indexInfoSerializer(Version version, SerializationHeader header)
+    {
+        // null header indicates streaming from pre-3.0 sstables
+        if (version.equals(BigFormat.latestVersion) && header != null)
+            return latestVersionIndexSerializer;
+
+        if (otherVersionClusteringSerializers == null)
+            otherVersionClusteringSerializers = new ConcurrentHashMap<>();
+        IndexInfo.Serializer serializer = otherVersionClusteringSerializers.get(version);
+        if (serializer == null)
+        {
+            serializer = new IndexInfo.Serializer(version,
+                                                  indexEntryClusteringPrefixSerializer(version, header));
+            otherVersionClusteringSerializers.put(version, serializer);
+        }
+        return serializer;
     }
 
     // TODO: Once we drop support for old (pre-3.0) sstables, we can drop this method and inline the calls to
-    // ClusteringPrefix.serializer in IndexHelper directly. At which point this whole class probably becomes
+    // ClusteringPrefix.serializer directly. At which point this whole class probably becomes
     // unecessary (since IndexInfo.Serializer won't depend on the metadata either).
-    public ISerializer<ClusteringPrefix> indexEntryClusteringPrefixSerializer(final Version version, final SerializationHeader header)
+    private ISerializer<ClusteringPrefix> indexEntryClusteringPrefixSerializer(Version version, SerializationHeader header)
     {
         if (!version.storeRows() || header ==  null) //null header indicates streaming from pre-3.0 sstables
         {
             return oldFormatSerializer(version);
         }
 
-        return newFormatSerializer(version, header);
+        return new NewFormatSerializer(version, header.clusteringTypes());
     }
 
-    private ISerializer<ClusteringPrefix> oldFormatSerializer(final Version version)
+    private ISerializer<ClusteringPrefix> oldFormatSerializer(Version version)
     {
         return new ISerializer<ClusteringPrefix>()
         {
-            SerializationHeader newHeader = SerializationHeader.makeWithoutStats(metadata);
+            List<AbstractType<?>> clusteringTypes = SerializationHeader.makeWithoutStats(metadata).clusteringTypes();
 
             public void serialize(ClusteringPrefix clustering, DataOutputPlus out) throws IOException
             {
                 //we deserialize in the old format and serialize in the new format
                 ClusteringPrefix.serializer.serialize(clustering, out,
                                                       version.correspondingMessagingVersion(),
-                                                      newHeader.clusteringTypes());
+                                                      clusteringTypes);
+            }
+
+            @Override
+            public void skip(DataInputPlus in) throws IOException
+            {
+                ByteBufferUtil.skipShortLength(in);
             }
 
             public ClusteringPrefix deserialize(DataInputPlus in) throws IOException
@@ -83,7 +118,7 @@
                     return Clustering.STATIC_CLUSTERING;
 
                 if (!metadata.isCompound())
-                    return new Clustering(bb);
+                    return Clustering.make(bb);
 
                 List<ByteBuffer> components = CompositeType.splitName(bb);
                 byte eoc = CompositeType.lastEOC(bb);
@@ -94,48 +129,58 @@
                     if (components.size() > clusteringSize)
                         components = components.subList(0, clusteringSize);
 
-                    return new Clustering(components.toArray(new ByteBuffer[clusteringSize]));
+                    return Clustering.make(components.toArray(new ByteBuffer[clusteringSize]));
                 }
                 else
                 {
                     // It's a range tombstone bound. It is a start since that's the only part we've ever included
                     // in the index entries.
-                    Slice.Bound.Kind boundKind = eoc > 0
-                                                 ? Slice.Bound.Kind.EXCL_START_BOUND
-                                                 : Slice.Bound.Kind.INCL_START_BOUND;
+                    ClusteringPrefix.Kind boundKind = eoc > 0
+                                                 ? ClusteringPrefix.Kind.EXCL_START_BOUND
+                                                 : ClusteringPrefix.Kind.INCL_START_BOUND;
 
-                    return Slice.Bound.create(boundKind, components.toArray(new ByteBuffer[components.size()]));
+                    return ClusteringBound.create(boundKind, components.toArray(new ByteBuffer[components.size()]));
                 }
             }
 
             public long serializedSize(ClusteringPrefix clustering)
             {
                 return ClusteringPrefix.serializer.serializedSize(clustering, version.correspondingMessagingVersion(),
-                                                                  newHeader.clusteringTypes());
+                                                                  clusteringTypes);
             }
         };
     }
 
-
-    private ISerializer<ClusteringPrefix> newFormatSerializer(final Version version, final SerializationHeader header)
+    private static class NewFormatSerializer implements ISerializer<ClusteringPrefix>
     {
-        return new ISerializer<ClusteringPrefix>() //Reading and writing from/to the new sstable format
+        private final Version version;
+        private final List<AbstractType<?>> clusteringTypes;
+
+        NewFormatSerializer(Version version, List<AbstractType<?>> clusteringTypes)
         {
-            public void serialize(ClusteringPrefix clustering, DataOutputPlus out) throws IOException
-            {
-                ClusteringPrefix.serializer.serialize(clustering, out, version.correspondingMessagingVersion(), header.clusteringTypes());
-            }
+            this.version = version;
+            this.clusteringTypes = clusteringTypes;
+        }
 
-            public ClusteringPrefix deserialize(DataInputPlus in) throws IOException
-            {
-                return ClusteringPrefix.serializer.deserialize(in, version.correspondingMessagingVersion(), header.clusteringTypes());
-            }
+        public void serialize(ClusteringPrefix clustering, DataOutputPlus out) throws IOException
+        {
+            ClusteringPrefix.serializer.serialize(clustering, out, version.correspondingMessagingVersion(), clusteringTypes);
+        }
 
-            public long serializedSize(ClusteringPrefix clustering)
-            {
-                return ClusteringPrefix.serializer.serializedSize(clustering, version.correspondingMessagingVersion(), header.clusteringTypes());
-            }
-        };
+        @Override
+        public void skip(DataInputPlus in) throws IOException
+        {
+            ClusteringPrefix.serializer.skip(in, version.correspondingMessagingVersion(), clusteringTypes);
+        }
+
+        public ClusteringPrefix deserialize(DataInputPlus in) throws IOException
+        {
+            return ClusteringPrefix.serializer.deserialize(in, version.correspondingMessagingVersion(), clusteringTypes);
+        }
+
+        public long serializedSize(ClusteringPrefix clustering)
+        {
+            return ClusteringPrefix.serializer.serializedSize(clustering, version.correspondingMessagingVersion(), clusteringTypes);
+        }
     }
-
 }
diff --git a/src/java/org/apache/cassandra/db/SimpleBuilders.java b/src/java/org/apache/cassandra/db/SimpleBuilders.java
new file mode 100644
index 0000000..6e65743
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/SimpleBuilders.java
@@ -0,0 +1,461 @@
+/*
+ * 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.db;
+
+import java.nio.ByteBuffer;
+import java.util.*;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.db.context.CounterContext;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.BTreeRow;
+import org.apache.cassandra.db.rows.BufferCell;
+import org.apache.cassandra.db.rows.Cell;
+import org.apache.cassandra.db.rows.CellPath;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.CounterId;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.UUIDGen;
+
+public abstract class SimpleBuilders
+{
+    private SimpleBuilders()
+    {
+    }
+
+    private static DecoratedKey makePartitonKey(CFMetaData metadata, Object... partitionKey)
+    {
+        if (partitionKey.length == 1 && partitionKey[0] instanceof DecoratedKey)
+            return (DecoratedKey)partitionKey[0];
+
+        ByteBuffer key = CFMetaData.serializePartitionKey(metadata.getKeyValidatorAsClusteringComparator().make(partitionKey));
+        return metadata.decorateKey(key);
+    }
+
+    private static Clustering makeClustering(CFMetaData metadata, Object... clusteringColumns)
+    {
+        if (clusteringColumns.length == 1 && clusteringColumns[0] instanceof Clustering)
+            return (Clustering)clusteringColumns[0];
+
+        if (clusteringColumns.length == 0)
+        {
+            // If the table has clustering columns, passing no values is for updating the static values, so check we
+            // do have some static columns defined.
+            assert metadata.comparator.size() == 0 || !metadata.partitionColumns().statics.isEmpty();
+            return metadata.comparator.size() == 0 ? Clustering.EMPTY : Clustering.STATIC_CLUSTERING;
+        }
+        else
+        {
+            return metadata.comparator.make(clusteringColumns);
+        }
+    }
+
+    private static class AbstractBuilder<T>
+    {
+        protected long timestamp = FBUtilities.timestampMicros();
+        protected int ttl = 0;
+        protected int nowInSec = FBUtilities.nowInSeconds();
+
+        protected void copyParams(AbstractBuilder<?> other)
+        {
+            other.timestamp = timestamp;
+            other.ttl = ttl;
+            other.nowInSec = nowInSec;
+        }
+
+        public T timestamp(long timestamp)
+        {
+            this.timestamp = timestamp;
+            return (T)this;
+        }
+
+        public T ttl(int ttl)
+        {
+            this.ttl = ttl;
+            return (T)this;
+        }
+
+        public T nowInSec(int nowInSec)
+        {
+            this.nowInSec = nowInSec;
+            return (T)this;
+        }
+    }
+
+    public static class MutationBuilder extends AbstractBuilder<Mutation.SimpleBuilder> implements Mutation.SimpleBuilder
+    {
+        private final String keyspaceName;
+        private final DecoratedKey key;
+
+        private final Map<UUID, PartitionUpdateBuilder> updateBuilders = new HashMap<>();
+
+        public MutationBuilder(String keyspaceName, DecoratedKey key)
+        {
+            this.keyspaceName = keyspaceName;
+            this.key = key;
+        }
+
+        public PartitionUpdate.SimpleBuilder update(CFMetaData metadata)
+        {
+            assert metadata.ksName.equals(keyspaceName);
+
+            PartitionUpdateBuilder builder = updateBuilders.get(metadata.cfId);
+            if (builder == null)
+            {
+                builder = new PartitionUpdateBuilder(metadata, key);
+                updateBuilders.put(metadata.cfId, builder);
+            }
+
+            copyParams(builder);
+
+            return builder;
+        }
+
+        public PartitionUpdate.SimpleBuilder update(String tableName)
+        {
+            CFMetaData metadata = Schema.instance.getCFMetaData(keyspaceName, tableName);
+            assert metadata != null : "Unknown table " + tableName + " in keyspace " + keyspaceName;
+            return update(metadata);
+        }
+
+        public Mutation build()
+        {
+            assert !updateBuilders.isEmpty() : "Cannot create empty mutation";
+
+            if (updateBuilders.size() == 1)
+                return new Mutation(updateBuilders.values().iterator().next().build());
+
+            Mutation mutation = new Mutation(keyspaceName, key);
+            for (PartitionUpdateBuilder builder : updateBuilders.values())
+                mutation.add(builder.build());
+            return mutation;
+        }
+    }
+
+    public static class PartitionUpdateBuilder extends AbstractBuilder<PartitionUpdate.SimpleBuilder> implements PartitionUpdate.SimpleBuilder
+    {
+        private final CFMetaData metadata;
+        private final DecoratedKey key;
+        private final Map<Clustering, RowBuilder> rowBuilders = new HashMap<>();
+        private List<RTBuilder> rangeBuilders = null; // We use that rarely, so create lazily
+
+        private DeletionTime partitionDeletion = DeletionTime.LIVE;
+
+        public PartitionUpdateBuilder(CFMetaData metadata, Object... partitionKeyValues)
+        {
+            this.metadata = metadata;
+            this.key = makePartitonKey(metadata, partitionKeyValues);
+        }
+
+        public CFMetaData metadata()
+        {
+            return metadata;
+        }
+
+        public Row.SimpleBuilder row(Object... clusteringValues)
+        {
+            Clustering clustering = makeClustering(metadata, clusteringValues);
+            RowBuilder builder = rowBuilders.get(clustering);
+            if (builder == null)
+            {
+                builder = new RowBuilder(metadata, clustering);
+                rowBuilders.put(clustering, builder);
+            }
+
+            copyParams(builder);
+
+            return builder;
+        }
+
+        public PartitionUpdate.SimpleBuilder delete()
+        {
+            this.partitionDeletion = new DeletionTime(timestamp, nowInSec);
+            return this;
+        }
+
+        public RangeTombstoneBuilder addRangeTombstone()
+        {
+            if (rangeBuilders == null)
+                rangeBuilders = new ArrayList<>();
+
+            RTBuilder builder = new RTBuilder(metadata.comparator, new DeletionTime(timestamp, nowInSec));
+            rangeBuilders.add(builder);
+            return builder;
+        }
+
+        public PartitionUpdate build()
+        {
+            // Collect all updated columns
+            PartitionColumns.Builder columns = PartitionColumns.builder();
+            for (RowBuilder builder : rowBuilders.values())
+                columns.addAll(builder.columns());
+
+            // Note that rowBuilders.size() could include the static column so could be 1 off the really need capacity
+            // of the final PartitionUpdate, but as that's just a sizing hint, we'll live.
+            PartitionUpdate update = new PartitionUpdate(metadata, key, columns.build(), rowBuilders.size());
+
+            update.addPartitionDeletion(partitionDeletion);
+            if (rangeBuilders != null)
+            {
+                for (RTBuilder builder : rangeBuilders)
+                    update.add(builder.build());
+            }
+
+            for (RowBuilder builder : rowBuilders.values())
+                update.add(builder.build());
+
+            return update;
+        }
+
+        public Mutation buildAsMutation()
+        {
+            return new Mutation(build());
+        }
+
+        private static class RTBuilder implements RangeTombstoneBuilder
+        {
+            private final ClusteringComparator comparator;
+            private final DeletionTime deletionTime;
+
+            private Object[] start;
+            private Object[] end;
+
+            private boolean startInclusive = true;
+            private boolean endInclusive = true;
+
+            private RTBuilder(ClusteringComparator comparator, DeletionTime deletionTime)
+            {
+                this.comparator = comparator;
+                this.deletionTime = deletionTime;
+            }
+
+            public RangeTombstoneBuilder start(Object... values)
+            {
+                this.start = values;
+                return this;
+            }
+
+            public RangeTombstoneBuilder end(Object... values)
+            {
+                this.end = values;
+                return this;
+            }
+
+            public RangeTombstoneBuilder inclStart()
+            {
+                this.startInclusive = true;
+                return this;
+            }
+
+            public RangeTombstoneBuilder exclStart()
+            {
+                this.startInclusive = false;
+                return this;
+            }
+
+            public RangeTombstoneBuilder inclEnd()
+            {
+                this.endInclusive = true;
+                return this;
+            }
+
+            public RangeTombstoneBuilder exclEnd()
+            {
+                this.endInclusive = false;
+                return this;
+            }
+
+            private RangeTombstone build()
+            {
+                ClusteringBound startBound = ClusteringBound.create(comparator, true, startInclusive, start);
+                ClusteringBound endBound = ClusteringBound.create(comparator, false, endInclusive, end);
+                return new RangeTombstone(Slice.make(startBound, endBound), deletionTime);
+            }
+        }
+    }
+
+    public static class RowBuilder extends AbstractBuilder<Row.SimpleBuilder> implements Row.SimpleBuilder
+    {
+        private final CFMetaData metadata;
+
+        private final Set<ColumnDefinition> columns = new HashSet<>();
+        private final Row.Builder builder;
+
+        private boolean initiated;
+        private boolean noPrimaryKeyLivenessInfo;
+
+        public RowBuilder(CFMetaData metadata, Object... clusteringColumns)
+        {
+            this.metadata = metadata;
+            this.builder = BTreeRow.unsortedBuilder(FBUtilities.nowInSeconds());
+
+            this.builder.newRow(makeClustering(metadata, clusteringColumns));
+        }
+
+        Set<ColumnDefinition> columns()
+        {
+            return columns;
+        }
+
+        private void maybeInit()
+        {
+            // We're working around the fact that Row.Builder requires that addPrimaryKeyLivenessInfo() and
+            // addRowDeletion() are called before any cell addition (which is done so the builder can more easily skip
+            // shadowed cells).
+            if (initiated)
+                return;
+
+            // If a CQL table, add the "row marker"
+            if (metadata.isCQLTable() && !noPrimaryKeyLivenessInfo)
+                builder.addPrimaryKeyLivenessInfo(LivenessInfo.create(timestamp, ttl, nowInSec));
+
+            initiated = true;
+        }
+
+        public Row.SimpleBuilder add(String columnName, Object value)
+        {
+            return add(columnName, value, true);
+        }
+
+        public Row.SimpleBuilder appendAll(String columnName, Object value)
+        {
+            return add(columnName, value, false);
+        }
+
+        private Row.SimpleBuilder add(String columnName, Object value, boolean overwriteForCollection)
+        {
+            maybeInit();
+            ColumnDefinition column = getColumn(columnName);
+
+            if (!overwriteForCollection && !(column.type.isMultiCell() && column.type.isCollection()))
+                throw new IllegalArgumentException("appendAll() can only be called on non-frozen colletions");
+
+            columns.add(column);
+
+            if (!column.type.isMultiCell())
+            {
+                builder.addCell(cell(column, toByteBuffer(value, column.type), null));
+                return this;
+            }
+
+            assert column.type instanceof CollectionType : "Collection are the only multi-cell types supported so far";
+
+            if (value == null)
+            {
+                builder.addComplexDeletion(column, new DeletionTime(timestamp, nowInSec));
+                return this;
+            }
+
+            // Erase previous entry if any.
+            if (overwriteForCollection)
+                builder.addComplexDeletion(column, new DeletionTime(timestamp - 1, nowInSec));
+            switch (((CollectionType)column.type).kind)
+            {
+                case LIST:
+                    ListType lt = (ListType)column.type;
+                    assert value instanceof List;
+                    for (Object elt : (List)value)
+                        builder.addCell(cell(column, toByteBuffer(elt, lt.getElementsType()), CellPath.create(ByteBuffer.wrap(UUIDGen.getTimeUUIDBytes()))));
+                    break;
+                case SET:
+                    SetType st = (SetType)column.type;
+                    assert value instanceof Set;
+                    for (Object elt : (Set)value)
+                        builder.addCell(cell(column, ByteBufferUtil.EMPTY_BYTE_BUFFER, CellPath.create(toByteBuffer(elt, st.getElementsType()))));
+                    break;
+                case MAP:
+                    MapType mt = (MapType)column.type;
+                    assert value instanceof Map;
+                    for (Map.Entry entry : ((Map<?, ?>)value).entrySet())
+                        builder.addCell(cell(column,
+                                             toByteBuffer(entry.getValue(), mt.getValuesType()),
+                                             CellPath.create(toByteBuffer(entry.getKey(), mt.getKeysType()))));
+                    break;
+                default:
+                    throw new AssertionError();
+            }
+            return this;
+        }
+
+        public Row.SimpleBuilder delete()
+        {
+            assert !initiated : "If called, delete() should be called before any other column value addition";
+            builder.addRowDeletion(Row.Deletion.regular(new DeletionTime(timestamp, nowInSec)));
+            return this;
+        }
+
+        public Row.SimpleBuilder delete(String columnName)
+        {
+            return add(columnName, null);
+        }
+
+        public Row.SimpleBuilder noPrimaryKeyLivenessInfo()
+        {
+            this.noPrimaryKeyLivenessInfo = true;
+            return this;
+        }
+
+        public Row build()
+        {
+            maybeInit();
+            return builder.build();
+        }
+
+        private ColumnDefinition getColumn(String columnName)
+        {
+            ColumnDefinition column = metadata.getColumnDefinition(new ColumnIdentifier(columnName, true));
+            assert column != null : "Cannot find column " + columnName;
+            assert !column.isPrimaryKeyColumn();
+            assert !column.isStatic() || builder.clustering() == Clustering.STATIC_CLUSTERING : "Cannot add non-static column to static-row";
+            return column;
+        }
+
+        private Cell cell(ColumnDefinition column, ByteBuffer value, CellPath path)
+        {
+            if (value == null)
+                return BufferCell.tombstone(column, timestamp, nowInSec, path);
+
+            return ttl == LivenessInfo.NO_TTL
+                 ? BufferCell.live(column, timestamp, value, path)
+                 : BufferCell.expiring(column, timestamp, ttl, nowInSec, value, path);
+        }
+
+        private ByteBuffer toByteBuffer(Object value, AbstractType<?> type)
+        {
+            if (value == null)
+                return null;
+
+            if (value instanceof ByteBuffer)
+                return (ByteBuffer)value;
+
+            if (type.isCounter())
+            {
+                // See UpdateParameters.addCounter()
+                assert value instanceof Long : "Attempted to adjust Counter cell with non-long value.";
+                return CounterContext.instance().createGlobal(CounterId.getLocalId(), 1, (Long)value);
+            }
+
+            return ((AbstractType)type).decompose(value);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
index dc9abc9..dfdcc03 100644
--- a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
+++ b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
@@ -20,10 +20,13 @@
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.*;
+import java.util.stream.Collectors;
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 
+import org.apache.commons.lang3.tuple.Pair;
+
 import org.apache.cassandra.cache.IRowCacheEntry;
 import org.apache.cassandra.cache.RowCacheKey;
 import org.apache.cassandra.cache.RowCacheSentinel;
@@ -35,6 +38,7 @@
 import org.apache.cassandra.db.partitions.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.transform.RTBoundValidator;
+import org.apache.cassandra.db.transform.Transformation;
 import org.apache.cassandra.exceptions.RequestExecutionException;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.sstable.format.SSTableReadsListener;
@@ -50,11 +54,10 @@
 import org.apache.cassandra.service.pager.*;
 import org.apache.cassandra.thrift.ThriftResultsMerger;
 import org.apache.cassandra.tracing.Tracing;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.SearchIterator;
 import org.apache.cassandra.utils.btree.BTreeSet;
-import org.apache.cassandra.utils.concurrent.OpOrder;
-import org.apache.cassandra.utils.memory.HeapAllocator;
 
 
 /**
@@ -343,6 +346,21 @@
                                               indexMetadata());
     }
 
+    public SinglePartitionReadCommand withUpdatedLimit(DataLimits newLimits)
+    {
+        return new SinglePartitionReadCommand(isDigestQuery(),
+                                              digestVersion(),
+                                              isForThrift(),
+                                              metadata(),
+                                              nowInSec(),
+                                              columnFilter(),
+                                              rowFilter(),
+                                              newLimits,
+                                              partitionKey(),
+                                              clusteringIndexFilter(),
+                                              indexMetadata());
+    }
+
     public SinglePartitionReadCommand withUpdatedClusteringIndexFilter(ClusteringIndexFilter filter)
     {
         return new SinglePartitionReadCommand(isDigestQuery(),
@@ -443,11 +461,11 @@
      * @param lastReturned the last row returned by the previous page. The newly created command
      * will only query row that comes after this (in query order). This can be {@code null} if this
      * is the first page.
-     * @param pageSize the size to use for the page to query.
+     * @param limits the limits to use for the page to query.
      *
      * @return the newly create command.
      */
-    public SinglePartitionReadCommand forPaging(Clustering lastReturned, int pageSize)
+    public SinglePartitionReadCommand forPaging(Clustering lastReturned, DataLimits limits)
     {
         // We shouldn't have set digest yet when reaching that point
         assert !isDigestQuery();
@@ -456,22 +474,22 @@
                       nowInSec(),
                       columnFilter(),
                       rowFilter(),
-                      limits().forPaging(pageSize),
+                      limits,
                       partitionKey(),
                       lastReturned == null ? clusteringIndexFilter() : clusteringIndexFilter.forPaging(metadata().comparator, lastReturned, false));
     }
 
-    public PartitionIterator execute(ConsistencyLevel consistency, ClientState clientState) throws RequestExecutionException
+    public PartitionIterator execute(ConsistencyLevel consistency, ClientState clientState, long queryStartNanoTime) throws RequestExecutionException
     {
-        return StorageProxy.read(Group.one(this), consistency, clientState);
+        return StorageProxy.read(Group.one(this), consistency, clientState, queryStartNanoTime);
     }
 
-    public SinglePartitionPager getPager(PagingState pagingState, int protocolVersion)
+    public SinglePartitionPager getPager(PagingState pagingState, ProtocolVersion protocolVersion)
     {
         return getPager(this, pagingState, protocolVersion);
     }
 
-    private static SinglePartitionPager getPager(SinglePartitionReadCommand command, PagingState pagingState, int protocolVersion)
+    private static SinglePartitionPager getPager(SinglePartitionReadCommand command, PagingState pagingState, ProtocolVersion protocolVersion)
     {
         return new SinglePartitionPager(command, pagingState, protocolVersion);
     }
@@ -482,11 +500,11 @@
     }
 
     @SuppressWarnings("resource") // we close the created iterator through closing the result of this method (and SingletonUnfilteredPartitionIterator ctor cannot fail)
-    protected UnfilteredPartitionIterator queryStorage(final ColumnFamilyStore cfs, ReadOrderGroup orderGroup)
+    protected UnfilteredPartitionIterator queryStorage(final ColumnFamilyStore cfs, ReadExecutionController executionController)
     {
         UnfilteredRowIterator partition = cfs.isRowCacheEnabled()
-                                        ? getThroughCache(cfs, orderGroup.baseReadOpOrderGroup())
-                                        : queryMemtableAndDisk(cfs, orderGroup.baseReadOpOrderGroup());
+                                        ? getThroughCache(cfs, executionController)
+                                        : queryMemtableAndDisk(cfs, executionController);
         return new SingletonUnfilteredPartitionIterator(partition, isForThrift());
     }
 
@@ -499,7 +517,7 @@
      * If the partition is is not cached, we figure out what filter is "biggest", read
      * that from disk, then filter the result and either cache that or return it.
      */
-    private UnfilteredRowIterator getThroughCache(ColumnFamilyStore cfs, OpOrder.Group readOp)
+    private UnfilteredRowIterator getThroughCache(ColumnFamilyStore cfs, ReadExecutionController executionController)
     {
         assert !cfs.isIndex(); // CASSANDRA-5732
         assert cfs.isRowCacheEnabled() : String.format("Row cache is not enabled on table [%s]", cfs.name);
@@ -517,7 +535,7 @@
                 // Some other read is trying to cache the value, just do a normal non-caching read
                 Tracing.trace("Row cache miss (race)");
                 cfs.metric.rowCacheMiss.inc();
-                return queryMemtableAndDisk(cfs, readOp);
+                return queryMemtableAndDisk(cfs, executionController);
             }
 
             CachedPartition cachedPartition = (CachedPartition)cached;
@@ -532,7 +550,7 @@
 
             cfs.metric.rowCacheHitOutOfRange.inc();
             Tracing.trace("Ignoring row cache as cached value could not satisfy query");
-            return queryMemtableAndDisk(cfs, readOp);
+            return queryMemtableAndDisk(cfs, executionController);
         }
 
         cfs.metric.rowCacheMiss.inc();
@@ -563,7 +581,7 @@
                 final boolean enforceStrictLiveness = metadata().enforceStrictLiveness();
 
                 @SuppressWarnings("resource") // we close on exception or upon closing the result of this method
-                UnfilteredRowIterator iter = fullPartitionRead(metadata(), nowInSec(), partitionKey()).queryMemtableAndDisk(cfs, readOp);
+                UnfilteredRowIterator iter = fullPartitionRead(metadata(), nowInSec(), partitionKey()).queryMemtableAndDisk(cfs, executionController);
                 try
                 {
                     // Use a custom iterator instead of DataLimits to avoid stopping the original iterator
@@ -593,6 +611,7 @@
 
                     // We want to cache only rowsToCache rows
                     CachedPartition toCache = CachedBTreePartition.create(toCacheIterator, nowInSec());
+
                     if (sentinelSuccess && !toCache.isEmpty())
                     {
                         Tracing.trace("Caching {} rows", toCache.rowCount());
@@ -628,7 +647,7 @@
         }
 
         Tracing.trace("Fetching data but not populating cache as query does not query from the start of the partition");
-        return queryMemtableAndDisk(cfs, readOp);
+        return queryMemtableAndDisk(cfs, executionController);
     }
 
     /**
@@ -643,15 +662,15 @@
      * It is publicly exposed because there is a few places where that is exactly what we want,
      * but it should be used only where you know you don't need thoses things.
      * <p>
-     * Also note that one must have "started" a {@code OpOrder.Group} on the queried table, and that is
-     * to enforce that that it is required as parameter, even though it's not explicitlly used by the method.
+     * Also note that one must have created a {@code ReadExecutionController} on the queried table and we require it as
+     * a parameter to enforce that fact, even though it's not explicitlly used by the method.
      */
-    public UnfilteredRowIterator queryMemtableAndDisk(ColumnFamilyStore cfs, OpOrder.Group readOp)
+    public UnfilteredRowIterator queryMemtableAndDisk(ColumnFamilyStore cfs, ReadExecutionController executionController)
     {
+        assert executionController != null && executionController.validForReadOn(cfs);
         Tracing.trace("Executing single-partition query on {}", cfs.name);
 
-        boolean copyOnHeap = Memtable.MEMORY_POOL.needToCopyOnHeap();
-        return queryMemtableAndDiskInternal(cfs, copyOnHeap);
+        return queryMemtableAndDiskInternal(cfs);
     }
 
     @Override
@@ -660,7 +679,7 @@
         return oldestUnrepairedTombstone;
     }
 
-    private UnfilteredRowIterator queryMemtableAndDiskInternal(ColumnFamilyStore cfs, boolean copyOnHeap)
+    private UnfilteredRowIterator queryMemtableAndDiskInternal(ColumnFamilyStore cfs)
     {
         /*
          * We have 2 main strategies:
@@ -669,18 +688,22 @@
          *   2) If we have a name filter (so we query specific rows), we can make a bet: that all column for all queried row
          *      will have data in the most recent sstable(s), thus saving us from reading older ones. This does imply we
          *      have a way to guarantee we have all the data for what is queried, which is only possible for name queries
-         *      and if we have neither non-frozen collections/UDTs nor counters (indeed, for a non-frozen collection or UDT,
-         *      we can't guarantee an older sstable won't have some elements that weren't in the most recent sstables,
-         *      and counters are intrinsically a collection of shards and so have the same problem).
+         *      and if we have neither non-frozen collections/UDTs nor counters.
+         *      If a non-frozen collection or UDT is queried we can't guarantee that an older sstable won't have some
+         *      elements that weren't in the most recent sstables.
+         *      Counters are intrinsically a collection of shards and so have the same problem.
+         *      Counter tables are also special in the sense that their rows do not have primary key liveness
+         *      as INSERT statements are not supported on counter tables. Due to that even if only the primary key
+         *      columns where queried, querying SSTables in timestamp order will always be less efficient for counter tables.
          */
-        if (clusteringIndexFilter() instanceof ClusteringIndexNamesFilter && !queriesMulticellType())
-            return queryMemtableAndSSTablesInTimestampOrder(cfs, copyOnHeap, (ClusteringIndexNamesFilter)clusteringIndexFilter());
+        if (clusteringIndexFilter() instanceof ClusteringIndexNamesFilter && !metadata().isCounter() && !queriesMulticellType())
+            return queryMemtableAndSSTablesInTimestampOrder(cfs, (ClusteringIndexNamesFilter)clusteringIndexFilter());
 
         Tracing.trace("Acquiring sstable references");
         ColumnFamilyStore.ViewFragment view = cfs.select(View.select(SSTableSet.LIVE, partitionKey()));
-
         List<UnfilteredRowIterator> iterators = new ArrayList<>(Iterables.size(view.memtables) + view.sstables.size());
         ClusteringIndexFilter filter = clusteringIndexFilter();
+        long minTimestamp = Long.MAX_VALUE;
 
         try
         {
@@ -690,20 +713,17 @@
                 if (partition == null)
                     continue;
 
-                // 'iter' is added to iterators which is closed on exception, or through the closing of the final merged iterator
-                @SuppressWarnings("resource")
+                if (memtable.getMinTimestamp() != Memtable.NO_MIN_TIMESTAMP)
+                    minTimestamp = Math.min(minTimestamp, memtable.getMinTimestamp());
+
+                @SuppressWarnings("resource") // 'iter' is added to iterators which is closed on exception, or through the closing of the final merged iterator
                 UnfilteredRowIterator iter = filter.getUnfilteredRowIterator(columnFilter(), partition);
-
-                if (copyOnHeap)
-                    iter = UnfilteredRowIterators.cloningIterator(iter, HeapAllocator.instance);
-
-                oldestUnrepairedTombstone = Math.min(oldestUnrepairedTombstone, partition.stats().minLocalDeletionTime);
-
                 if (isForThrift())
                     iter = ThriftResultsMerger.maybeWrap(iter, nowInSec());
-
+                oldestUnrepairedTombstone = Math.min(oldestUnrepairedTombstone, partition.stats().minLocalDeletionTime);
                 iterators.add(RTBoundValidator.validate(iter, RTBoundValidator.Stage.MEMTABLE, false));
             }
+
             /*
              * We can't eliminate full sstables based on the timestamp of what we've already read like
              * in collectTimeOrderedData, but we still want to eliminate sstable whose maxTimestamp < mostRecentTombstone
@@ -715,11 +735,12 @@
              *   timestamp(tombstone) <= maxTimestamp_s1
              * In other words, iterating in descending maxTimestamp order allow to do our mostRecentPartitionTombstone
              * elimination in one pass, and minimize the number of sstables for which we read a partition tombstone.
-             */
-            Collections.sort(view.sstables, SSTableReader.maxTimestampComparator);
+            */
+            Collections.sort(view.sstables, SSTableReader.maxTimestampDescending);
             long mostRecentPartitionTombstone = Long.MIN_VALUE;
             int nonIntersectingSSTables = 0;
             int includedDueToTombstones = 0;
+
             SSTableReadMetricsCollector metricsCollector = new SSTableReadMetricsCollector();
 
             for (SSTableReader sstable : view.sstables)
@@ -736,16 +757,8 @@
 
                     // 'iter' is added to iterators which is closed on exception, or through the closing of the final merged iterator
                     @SuppressWarnings("resource")
-                    UnfilteredRowIterator iter = filter.filter(sstable.iterator(partitionKey(),
-                                                                                columnFilter(),
-                                                                                filter.isReversed(),
-                                                                                isForThrift(),
-                                                                                metricsCollector));
-
-                    if (isForThrift())
-                        iter = ThriftResultsMerger.maybeWrap(iter, nowInSec());
-
-                    iterators.add(RTBoundValidator.validate(iter, RTBoundValidator.Stage.SSTABLE, false));
+                    UnfilteredRowIterator iter = makeIterator(cfs, sstable, true, metricsCollector);
+                    iterators.add(iter);
                     mostRecentPartitionTombstone = Math.max(mostRecentPartitionTombstone,
                                                             iter.partitionLevelDeletion().markedForDeleteAt());
                 }
@@ -754,23 +767,19 @@
 
                     nonIntersectingSSTables++;
                     // sstable contains no tombstone if maxLocalDeletionTime == Integer.MAX_VALUE, so we can safely skip those entirely
-                    if (sstable.hasTombstones())
+                    if (sstable.mayHaveTombstones())
                     {
                         // 'iter' is added to iterators which is closed on exception, or through the closing of the final merged iterator
                         @SuppressWarnings("resource")
-                        UnfilteredRowIterator iter = filter.filter(sstable.iterator(partitionKey(),
-                                                                                    columnFilter(),
-                                                                                    filter.isReversed(),
-                                                                                    isForThrift(),
-                                                                                    metricsCollector));
+                        UnfilteredRowIterator iter = makeIterator(cfs, sstable, true, metricsCollector);
                         // if the sstable contains a partition delete, then we must include it regardless of whether it
                         // shadows any other data seen locally as we can't guarantee that other replicas have seen it
                         if (!iter.partitionLevelDeletion().isLive())
                         {
-                            includedDueToTombstones++;
-                            iterators.add(RTBoundValidator.validate(iter, RTBoundValidator.Stage.SSTABLE, false));
                             if (!sstable.isRepaired())
                                 oldestUnrepairedTombstone = Math.min(oldestUnrepairedTombstone, sstable.getMinLocalDeletionTime());
+                            iterators.add(iter);
+                            includedDueToTombstones++;
                             mostRecentPartitionTombstone = Math.max(mostRecentPartitionTombstone,
                                                                     iter.partitionLevelDeletion().markedForDeleteAt());
                         }
@@ -778,31 +787,19 @@
                         {
                             iter.close();
                         }
-
                     }
                 }
             }
 
             if (Tracing.isTracing())
                 Tracing.trace("Skipped {}/{} non-slice-intersecting sstables, included {} due to tombstones",
-                              nonIntersectingSSTables, view.sstables.size(), includedDueToTombstones);
-
-            cfs.metric.updateSSTableIterated(metricsCollector.getMergedSSTables());
+                               nonIntersectingSSTables, view.sstables.size(), includedDueToTombstones);
 
             if (iterators.isEmpty())
                 return EmptyIterators.unfilteredRow(cfs.metadata, partitionKey(), filter.isReversed());
 
-            Tracing.trace("Merging data from memtables and {} sstables", metricsCollector.getMergedSSTables());
-
-            @SuppressWarnings("resource") //  Closed through the closing of the result of that method.
-            UnfilteredRowIterator merged = UnfilteredRowIterators.merge(iterators, nowInSec());
-            if (!merged.isEmpty())
-            {
-                DecoratedKey key = merged.partitionKey();
-                cfs.metric.samplers.get(TableMetrics.Sampler.READS).addSample(key.getKey(), key.hashCode(), 1);
-            }
-
-            return merged;
+            StorageHook.instance.reportRead(cfs.metadata.cfId, partitionKey());
+            return withSSTablesIterated(iterators, cfs.metric, metricsCollector);
         }
         catch (RuntimeException | Error e)
         {
@@ -829,11 +826,58 @@
         return clusteringIndexFilter().shouldInclude(sstable);
     }
 
+    private UnfilteredRowIteratorWithLowerBound makeIterator(ColumnFamilyStore cfs,
+                                                             final SSTableReader sstable,
+                                                             boolean applyThriftTransformation,
+                                                             SSTableReadsListener listener)
+    {
+        return StorageHook.instance.makeRowIteratorWithLowerBound(cfs,
+                                                                  partitionKey(),
+                                                                  sstable,
+                                                                  clusteringIndexFilter(),
+                                                                  columnFilter(),
+                                                                  isForThrift(),
+                                                                  nowInSec(),
+                                                                  applyThriftTransformation,
+                                                                  listener);
+
+    }
+
+    /**
+     * Return a wrapped iterator that when closed will update the sstables iterated and READ sample metrics.
+     * Note that we cannot use the Transformations framework because they greedily get the static row, which
+     * would cause all iterators to be initialized and hence all sstables to be accessed.
+     */
+    private UnfilteredRowIterator withSSTablesIterated(List<UnfilteredRowIterator> iterators,
+                                                       TableMetrics metrics,
+                                                       SSTableReadMetricsCollector metricsCollector)
+    {
+        @SuppressWarnings("resource") //  Closed through the closing of the result of the caller method.
+        UnfilteredRowIterator merged = UnfilteredRowIterators.merge(iterators, nowInSec());
+
+        if (!merged.isEmpty())
+        {
+            DecoratedKey key = merged.partitionKey();
+            metrics.samplers.get(TableMetrics.Sampler.READS).addSample(key.getKey(), key.hashCode(), 1);
+        }
+
+        class UpdateSstablesIterated extends Transformation
+        {
+           public void onPartitionClose()
+           {
+               int mergedSSTablesIterated = metricsCollector.getMergedSSTables();
+               metrics.updateSSTableIterated(mergedSSTablesIterated);
+               Tracing.trace("Merged data from memtables and {} sstables", mergedSSTablesIterated);
+           }
+        };
+        return Transformation.apply(merged, new UpdateSstablesIterated());
+    }
+
     private boolean queriesMulticellType()
     {
-        for (ColumnDefinition column : columnFilter().fetchedColumns())
+        for (ColumnDefinition column : columnFilter().queriedColumns())
         {
-            if (column.type.isMultiCell() || column.type.isCounter())
+            if (column.type.isMultiCell())
                 return true;
         }
         return false;
@@ -848,7 +892,7 @@
      * no collection or counters are included).
      * This method assumes the filter is a {@code ClusteringIndexNamesFilter}.
      */
-    private UnfilteredRowIterator queryMemtableAndSSTablesInTimestampOrder(ColumnFamilyStore cfs, boolean copyOnHeap, ClusteringIndexNamesFilter filter)
+    private UnfilteredRowIterator queryMemtableAndSSTablesInTimestampOrder(ColumnFamilyStore cfs, ClusteringIndexNamesFilter filter)
     {
         Tracing.trace("Acquiring sstable references");
         ColumnFamilyStore.ViewFragment view = cfs.select(View.select(SSTableSet.LIVE, partitionKey()));
@@ -867,11 +911,8 @@
                 if (iter.isEmpty())
                     continue;
 
-                UnfilteredRowIterator clonedIter = copyOnHeap
-                                                   ? UnfilteredRowIterators.cloningIterator(iter, HeapAllocator.instance)
-                                                   : iter;
                 result = add(
-                    RTBoundValidator.validate(isForThrift() ? ThriftResultsMerger.maybeWrap(clonedIter, nowInSec()) : clonedIter, RTBoundValidator.Stage.MEMTABLE, false),
+                    RTBoundValidator.validate(isForThrift() ? ThriftResultsMerger.maybeWrap(iter, nowInSec()) : iter, RTBoundValidator.Stage.MEMTABLE, false),
                     result,
                     filter,
                     false
@@ -880,7 +921,7 @@
         }
 
         /* add the SSTables on disk */
-        Collections.sort(view.sstables, SSTableReader.maxTimestampComparator);
+        Collections.sort(view.sstables, SSTableReader.maxTimestampDescending);
         // read sorted sstables
         SSTableReadMetricsCollector metricsCollector = new SSTableReadMetricsCollector();
         for (SSTableReader sstable : view.sstables)
@@ -903,15 +944,18 @@
                 // however: if it is set, it impacts everything and must be included. Getting that top-level partition deletion costs us
                 // some seek in general however (unless the partition is indexed and is in the key cache), so we first check if the sstable
                 // has any tombstone at all as a shortcut.
-                if (!sstable.hasTombstones())
-                    continue; // Means no tombstone at all, we can skip that sstable
+                if (!sstable.mayHaveTombstones())
+                    continue; // no tombstone at all, we can skip that sstable
 
                 // We need to get the partition deletion and include it if it's live. In any case though, we're done with that sstable.
-                try (UnfilteredRowIterator iter = filter.filter(sstable.iterator(partitionKey(),
-                                                                                 columnFilter(),
-                                                                                 filter.isReversed(),
-                                                                                 isForThrift(),
-                                                                                 metricsCollector)))
+                try (UnfilteredRowIterator iter = StorageHook.instance.makeRowIterator(cfs,
+                                                                                       sstable,
+                                                                                       partitionKey(),
+                                                                                       filter.getSlices(metadata()),
+                                                                                       columnFilter(),
+                                                                                       filter.isReversed(),
+                                                                                       isForThrift(),
+                                                                                       metricsCollector))
                 {
                     if (!iter.partitionLevelDeletion().isLive())
                     {
@@ -940,12 +984,14 @@
                 continue;
             }
 
-            Tracing.trace("Merging data from sstable {}", sstable.descriptor.generation);
-            try (UnfilteredRowIterator iter = filter.filter(sstable.iterator(partitionKey(),
-                                                                             columnFilter(),
-                                                                             filter.isReversed(),
-                                                                             isForThrift(),
-                                                                             metricsCollector)))
+            try (UnfilteredRowIterator iter = StorageHook.instance.makeRowIterator(cfs,
+                                                                                   sstable,
+                                                                                   partitionKey(),
+                                                                                   filter.getSlices(metadata()),
+                                                                                   columnFilter(),
+                                                                                   filter.isReversed(),
+                                                                                   isForThrift(),
+                                                                                   metricsCollector))
             {
                 if (iter.isEmpty())
                     continue;
@@ -966,6 +1012,7 @@
 
         DecoratedKey key = result.partitionKey();
         cfs.metric.samplers.get(TableMetrics.Sampler.READS).addSample(key.getKey(), key.hashCode(), 1);
+        StorageHook.instance.reportRead(cfs.metadata.cfId, partitionKey());
 
         return result.unfilteredIterator(columnFilter(), Slices.ALL, clusteringIndexFilter().isReversed());
     }
@@ -992,7 +1039,15 @@
 
         SearchIterator<Clustering, Row> searchIter = result.searchIterator(columnFilter(), false);
 
-        PartitionColumns columns = columnFilter().fetchedColumns();
+        // According to the CQL semantics a row exists if at least one of its columns is not null (including the primary key columns).
+        // Having the queried columns not null is unfortunately not enough to prove that a row exists as some column deletion
+        // for the queried columns can exist on another node.
+        // For CQL tables it is enough to have the primary key liveness and the queried columns as the primary key liveness prove that
+        // the row exists even if all the other columns are deleted.
+        // COMPACT tables do not have primary key liveness and by consequence we are forced to get  all the fetched columns to ensure that
+        // we can return the correct result if the queried columns are deleted on another node but one of the non-queried columns is not.
+        PartitionColumns columns = metadata().isCompactTable() ? columnFilter().fetchedColumns() : columnFilter().queriedColumns();
+
         NavigableSet<Clustering> clusterings = filter.requestedRows();
 
         // We want to remove rows for which we have values for all requested columns. We have to deal with both static and regular rows.
@@ -1158,12 +1213,12 @@
 
         public static Group one(SinglePartitionReadCommand command)
         {
-            return new Group(Collections.<SinglePartitionReadCommand>singletonList(command), command.limits());
+            return new Group(Collections.singletonList(command), command.limits());
         }
 
-        public PartitionIterator execute(ConsistencyLevel consistency, ClientState clientState) throws RequestExecutionException
+        public PartitionIterator execute(ConsistencyLevel consistency, ClientState clientState, long queryStartNanoTime) throws RequestExecutionException
         {
-            return StorageProxy.read(this, consistency, clientState);
+            return StorageProxy.read(this, consistency, clientState, queryStartNanoTime);
         }
 
         public int nowInSec()
@@ -1187,30 +1242,53 @@
             return selectsFullPartitions;
         }
 
-        public ReadOrderGroup startOrderGroup()
+        public ReadExecutionController executionController()
         {
             // Note that the only difference between the command in a group must be the partition key on which
             // they applied. So as far as ReadOrderGroup is concerned, we can use any of the commands to start one.
-            return commands.get(0).startOrderGroup();
+            return commands.get(0).executionController();
         }
 
-        public PartitionIterator executeInternal(ReadOrderGroup orderGroup)
+        public PartitionIterator executeInternal(ReadExecutionController controller)
         {
-            List<PartitionIterator> partitions = new ArrayList<>(commands.size());
-            for (SinglePartitionReadCommand cmd : commands)
-                partitions.add(cmd.executeInternal(orderGroup));
-
             // Note that the only difference between the command in a group must be the partition key on which
             // they applied.
             boolean enforceStrictLiveness = commands.get(0).metadata().enforceStrictLiveness();
-            // Because we only have enforce the limit per command, we need to enforce it globally.
-            return limits.filter(PartitionIterators.concat(partitions),
+            return limits.filter(UnfilteredPartitionIterators.filter(executeLocally(controller, false), nowInSec),
                                  nowInSec,
                                  selectsFullPartitions,
                                  enforceStrictLiveness);
         }
 
-        public QueryPager getPager(PagingState pagingState, int protocolVersion)
+        public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController)
+        {
+            return executeLocally(executionController, true);
+        }
+
+        /**
+         * Implementation of {@link ReadQuery#executeLocally(ReadExecutionController)}.
+         *
+         * @param executionController - the {@code ReadExecutionController} protecting the read.
+         * @param sort - whether to sort the inner commands by partition key, required for merging the iterator
+         *               later on. This will be false when called by {@link ReadQuery#executeInternal(ReadExecutionController)}
+         *               because in this case it is safe to do so as there is no merging involved and we don't want to
+         *               change the old behavior which was to not sort by partition.
+         *
+         * @return - the iterator that can be used to retrieve the query result.
+         */
+        private UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController, boolean sort)
+        {
+            List<Pair<DecoratedKey, UnfilteredPartitionIterator>> partitions = new ArrayList<>(commands.size());
+            for (SinglePartitionReadCommand cmd : commands)
+                partitions.add(Pair.of(cmd.partitionKey, cmd.executeLocally(executionController)));
+
+            if (sort)
+                Collections.sort(partitions, (p1, p2) -> p1.getLeft().compareTo(p2.getLeft()));
+
+            return UnfilteredPartitionIterators.concat(partitions.stream().map(p -> p.getRight()).collect(Collectors.toList()));
+        }
+
+        public QueryPager getPager(PagingState pagingState, ProtocolVersion protocolVersion)
         {
             if (commands.size() == 1)
                 return SinglePartitionReadCommand.getPager(commands.get(0), pagingState, protocolVersion);
diff --git a/src/java/org/apache/cassandra/db/Slice.java b/src/java/org/apache/cassandra/db/Slice.java
index fb75b8e..5f58e3b 100644
--- a/src/java/org/apache/cassandra/db/Slice.java
+++ b/src/java/org/apache/cassandra/db/Slice.java
@@ -39,10 +39,10 @@
     public static final Serializer serializer = new Serializer();
 
     /** The slice selecting all rows (of a given partition) */
-    public static final Slice ALL = new Slice(Bound.BOTTOM, Bound.TOP)
+    public static final Slice ALL = new Slice(ClusteringBound.BOTTOM, ClusteringBound.TOP)
     {
         @Override
-        public boolean selects(ClusteringComparator comparator, Clustering clustering)
+        public boolean includes(ClusteringComparator comparator, ClusteringPrefix clustering)
         {
             return true;
         }
@@ -60,19 +60,19 @@
         }
     };
 
-    private final Bound start;
-    private final Bound end;
+    private final ClusteringBound start;
+    private final ClusteringBound end;
 
-    private Slice(Bound start, Bound end)
+    private Slice(ClusteringBound start, ClusteringBound end)
     {
         assert start.isStart() && end.isEnd();
         this.start = start;
         this.end = end;
     }
 
-    public static Slice make(Bound start, Bound end)
+    public static Slice make(ClusteringBound start, ClusteringBound end)
     {
-        if (start == Bound.BOTTOM && end == Bound.TOP)
+        if (start == ClusteringBound.BOTTOM && end == ClusteringBound.TOP)
             return ALL;
 
         return new Slice(start, end);
@@ -96,7 +96,7 @@
         // This doesn't give us what we want with the clustering prefix
         assert clustering != Clustering.STATIC_CLUSTERING;
         ByteBuffer[] values = extractValues(clustering);
-        return new Slice(Bound.inclusiveStartOf(values), Bound.inclusiveEndOf(values));
+        return new Slice(ClusteringBound.inclusiveStartOf(values), ClusteringBound.inclusiveEndOf(values));
     }
 
     public static Slice make(Clustering start, Clustering end)
@@ -107,7 +107,7 @@
         ByteBuffer[] startValues = extractValues(start);
         ByteBuffer[] endValues = extractValues(end);
 
-        return new Slice(Bound.inclusiveStartOf(startValues), Bound.inclusiveEndOf(endValues));
+        return new Slice(ClusteringBound.inclusiveStartOf(startValues), ClusteringBound.inclusiveEndOf(endValues));
     }
 
     private static ByteBuffer[] extractValues(ClusteringPrefix clustering)
@@ -118,22 +118,22 @@
         return values;
     }
 
-    public Bound start()
+    public ClusteringBound start()
     {
         return start;
     }
 
-    public Bound end()
+    public ClusteringBound end()
     {
         return end;
     }
 
-    public Bound open(boolean reversed)
+    public ClusteringBound open(boolean reversed)
     {
         return reversed ? end : start;
     }
 
-    public Bound close(boolean reversed)
+    public ClusteringBound close(boolean reversed)
     {
         return reversed ? start : end;
     }
@@ -158,34 +158,21 @@
      * @return whether the slice formed by {@code start} and {@code end} is
      * empty or not.
      */
-    public static boolean isEmpty(ClusteringComparator comparator, Slice.Bound start, Slice.Bound end)
+    public static boolean isEmpty(ClusteringComparator comparator, ClusteringBound start, ClusteringBound end)
     {
         assert start.isStart() && end.isEnd();
         return comparator.compare(end, start) <= 0;
     }
 
     /**
-     * Returns whether a given clustering is selected by this slice.
-     *
-     * @param comparator the comparator for the table this is a slice of.
-     * @param clustering the clustering to test inclusion of.
-     *
-     * @return whether {@code clustering} is selected by this slice.
-     */
-    public boolean selects(ClusteringComparator comparator, Clustering clustering)
-    {
-        return comparator.compare(start, clustering) <= 0 && comparator.compare(clustering, end) <= 0;
-    }
-
-    /**
-     * Returns whether a given bound is included in this slice.
+     * Returns whether a given clustering or bound is included in this slice.
      *
      * @param comparator the comparator for the table this is a slice of.
      * @param bound the bound to test inclusion of.
      *
      * @return whether {@code bound} is within the bounds of this slice.
      */
-    public boolean includes(ClusteringComparator comparator, Bound bound)
+    public boolean includes(ClusteringComparator comparator, ClusteringPrefix bound)
     {
         return comparator.compare(start, bound) <= 0 && comparator.compare(bound, end) <= 0;
     }
@@ -219,7 +206,7 @@
                 return this;
 
             ByteBuffer[] values = extractValues(lastReturned);
-            return new Slice(start, inclusive ? Bound.inclusiveEndOf(values) : Bound.exclusiveEndOf(values));
+            return new Slice(start, inclusive ? ClusteringBound.inclusiveEndOf(values) : ClusteringBound.exclusiveEndOf(values));
         }
         else
         {
@@ -232,7 +219,7 @@
                 return this;
 
             ByteBuffer[] values = extractValues(lastReturned);
-            return new Slice(inclusive ? Bound.inclusiveStartOf(values) : Bound.exclusiveStartOf(values), end);
+            return new Slice(inclusive ? ClusteringBound.inclusiveStartOf(values) : ClusteringBound.exclusiveStartOf(values), end);
         }
     }
 
@@ -300,245 +287,21 @@
     {
         public void serialize(Slice slice, DataOutputPlus out, int version, List<AbstractType<?>> types) throws IOException
         {
-            Bound.serializer.serialize(slice.start, out, version, types);
-            Bound.serializer.serialize(slice.end, out, version, types);
+            ClusteringBound.serializer.serialize(slice.start, out, version, types);
+            ClusteringBound.serializer.serialize(slice.end, out, version, types);
         }
 
         public long serializedSize(Slice slice, int version, List<AbstractType<?>> types)
         {
-            return Bound.serializer.serializedSize(slice.start, version, types)
-                 + Bound.serializer.serializedSize(slice.end, version, types);
+            return ClusteringBound.serializer.serializedSize(slice.start, version, types)
+                 + ClusteringBound.serializer.serializedSize(slice.end, version, types);
         }
 
         public Slice deserialize(DataInputPlus in, int version, List<AbstractType<?>> types) throws IOException
         {
-            Bound start = Bound.serializer.deserialize(in, version, types);
-            Bound end = Bound.serializer.deserialize(in, version, types);
+            ClusteringBound start = (ClusteringBound) ClusteringBound.serializer.deserialize(in, version, types);
+            ClusteringBound end = (ClusteringBound) ClusteringBound.serializer.deserialize(in, version, types);
             return new Slice(start, end);
         }
     }
-
-    /**
-     * The bound of a slice.
-     * <p>
-     * This can be either a start or an end bound, and this can be either inclusive or exclusive.
-     */
-    public static class Bound extends AbstractClusteringPrefix
-    {
-        public static final Serializer serializer = new Serializer();
-
-        /**
-         * The smallest and biggest bound. Note that as range tomstone bounds are (special case) of slice bounds,
-         * we want the BOTTOM and TOP to be the same object, but we alias them here because it's cleaner when dealing
-         * with slices to refer to Slice.Bound.BOTTOM and Slice.Bound.TOP.
-         */
-        public static final Bound BOTTOM = RangeTombstone.Bound.BOTTOM;
-        public static final Bound TOP = RangeTombstone.Bound.TOP;
-
-        protected Bound(Kind kind, ByteBuffer[] values)
-        {
-            super(kind, values);
-        }
-
-        public static Bound create(Kind kind, ByteBuffer[] values)
-        {
-            assert !kind.isBoundary();
-            return new Bound(kind, values);
-        }
-
-        public static Kind boundKind(boolean isStart, boolean isInclusive)
-        {
-            return isStart
-                 ? (isInclusive ? Kind.INCL_START_BOUND : Kind.EXCL_START_BOUND)
-                 : (isInclusive ? Kind.INCL_END_BOUND : Kind.EXCL_END_BOUND);
-        }
-
-        public static Bound inclusiveStartOf(ByteBuffer... values)
-        {
-            return create(Kind.INCL_START_BOUND, values);
-        }
-
-        public static Bound inclusiveEndOf(ByteBuffer... values)
-        {
-            return create(Kind.INCL_END_BOUND, values);
-        }
-
-        public static Bound exclusiveStartOf(ByteBuffer... values)
-        {
-            return create(Kind.EXCL_START_BOUND, values);
-        }
-
-        public static Bound exclusiveEndOf(ByteBuffer... values)
-        {
-            return create(Kind.EXCL_END_BOUND, values);
-        }
-
-        public static Bound inclusiveStartOf(ClusteringPrefix prefix)
-        {
-            ByteBuffer[] values = new ByteBuffer[prefix.size()];
-            for (int i = 0; i < prefix.size(); i++)
-                values[i] = prefix.get(i);
-            return inclusiveStartOf(values);
-        }
-
-        public static Bound exclusiveStartOf(ClusteringPrefix prefix)
-        {
-            ByteBuffer[] values = new ByteBuffer[prefix.size()];
-            for (int i = 0; i < prefix.size(); i++)
-                values[i] = prefix.get(i);
-            return exclusiveStartOf(values);
-        }
-
-        public static Bound inclusiveEndOf(ClusteringPrefix prefix)
-        {
-            ByteBuffer[] values = new ByteBuffer[prefix.size()];
-            for (int i = 0; i < prefix.size(); i++)
-                values[i] = prefix.get(i);
-            return inclusiveEndOf(values);
-        }
-
-        public static Bound create(ClusteringComparator comparator, boolean isStart, boolean isInclusive, Object... values)
-        {
-            CBuilder builder = CBuilder.create(comparator);
-            for (Object val : values)
-            {
-                if (val instanceof ByteBuffer)
-                    builder.add((ByteBuffer) val);
-                else
-                    builder.add(val);
-            }
-            return builder.buildBound(isStart, isInclusive);
-        }
-
-        public Bound withNewKind(Kind kind)
-        {
-            assert !kind.isBoundary();
-            return new Bound(kind, values);
-        }
-
-        public boolean isStart()
-        {
-            return kind().isStart();
-        }
-
-        public boolean isEnd()
-        {
-            return !isStart();
-        }
-
-        public boolean isInclusive()
-        {
-            return kind == Kind.INCL_START_BOUND || kind == Kind.INCL_END_BOUND;
-        }
-
-        public boolean isExclusive()
-        {
-            return kind == Kind.EXCL_START_BOUND || kind == Kind.EXCL_END_BOUND;
-        }
-
-        /**
-         * Returns the inverse of the current bound.
-         * <p>
-         * This invert both start into end (and vice-versa) and inclusive into exclusive (and vice-versa).
-         *
-         * @return the invert of this bound. For instance, if this bound is an exlusive start, this return
-         * an inclusive end with the same values.
-         */
-        public Slice.Bound invert()
-        {
-            return withNewKind(kind().invert());
-        }
-
-        // For use by intersects, it's called with the sstable bound opposite to the slice bound
-        // (so if the slice bound is a start, it's call with the max sstable bound)
-        private int compareTo(ClusteringComparator comparator, List<ByteBuffer> sstableBound)
-        {
-            for (int i = 0; i < sstableBound.size(); i++)
-            {
-                // Say the slice bound is a start. It means we're in the case where the max
-                // sstable bound is say (1:5) while the slice start is (1). So the start
-                // does start before the sstable end bound (and intersect it). It's the exact
-                // inverse with a end slice bound.
-                if (i >= size())
-                    return isStart() ? -1 : 1;
-
-                int cmp = comparator.compareComponent(i, get(i), sstableBound.get(i));
-                if (cmp != 0)
-                    return cmp;
-            }
-
-            // Say the slice bound is a start. I means we're in the case where the max
-            // sstable bound is say (1), while the slice start is (1:5). This again means
-            // that the slice start before the end bound.
-            if (size() > sstableBound.size())
-                return isStart() ? -1 : 1;
-
-            // The slice bound is equal to the sstable bound. Results depends on whether the slice is inclusive or not
-            return isInclusive() ? 0 : (isStart() ? 1 : -1);
-        }
-
-        public String toString(CFMetaData metadata)
-        {
-            return toString(metadata.comparator);
-        }
-
-        public String toString(ClusteringComparator comparator)
-        {
-            StringBuilder sb = new StringBuilder();
-            sb.append(kind()).append('(');
-            for (int i = 0; i < size(); i++)
-            {
-                if (i > 0)
-                    sb.append(", ");
-                sb.append(comparator.subtype(i).getString(get(i)));
-            }
-            return sb.append(')').toString();
-        }
-
-        public ClusteringPrefix minimize()
-        {
-            if (!ByteBufferUtil.canMinimize(values))
-                return this;
-            return new Bound(kind, ByteBufferUtil.minimizeBuffers(values));
-        }
-
-        /**
-         * Serializer for slice bounds.
-         * <p>
-         * Contrarily to {@code Clustering}, a slice bound can be a true prefix of the full clustering, so we actually record
-         * its size.
-         */
-        public static class Serializer
-        {
-            public void serialize(Slice.Bound bound, DataOutputPlus out, int version, List<AbstractType<?>> types) throws IOException
-            {
-                out.writeByte(bound.kind().ordinal());
-                out.writeShort(bound.size());
-                ClusteringPrefix.serializer.serializeValuesWithoutSize(bound, out, version, types);
-            }
-
-            public long serializedSize(Slice.Bound bound, int version, List<AbstractType<?>> types)
-            {
-                return 1 // kind ordinal
-                     + TypeSizes.sizeof((short)bound.size())
-                     + ClusteringPrefix.serializer.valuesWithoutSizeSerializedSize(bound, version, types);
-            }
-
-            public Slice.Bound deserialize(DataInputPlus in, int version, List<AbstractType<?>> types) throws IOException
-            {
-                Kind kind = Kind.values()[in.readByte()];
-                return deserializeValues(in, kind, version, types);
-            }
-
-            public Slice.Bound deserializeValues(DataInputPlus in, Kind kind, int version, List<AbstractType<?>> types) throws IOException
-            {
-                int size = in.readUnsignedShort();
-                if (size == 0)
-                    return kind.isStart() ? BOTTOM : TOP;
-
-                ByteBuffer[] values = ClusteringPrefix.serializer.deserializeValuesWithoutSize(in, size, version, types);
-                return Slice.Bound.create(kind, values);
-            }
-        }
-    }
 }
diff --git a/src/java/org/apache/cassandra/db/Slices.java b/src/java/org/apache/cassandra/db/Slices.java
index 269386e..93dcab9 100644
--- a/src/java/org/apache/cassandra/db/Slices.java
+++ b/src/java/org/apache/cassandra/db/Slices.java
@@ -25,7 +25,6 @@
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
@@ -59,7 +58,7 @@
      */
     public static Slices with(ClusteringComparator comparator, Slice slice)
     {
-        if (slice.start() == Slice.Bound.BOTTOM && slice.end() == Slice.Bound.TOP)
+        if (slice.start() == ClusteringBound.BOTTOM && slice.end() == ClusteringBound.TOP)
             return Slices.ALL;
 
         assert comparator.compare(slice.start(), slice.end()) <= 0;
@@ -141,16 +140,6 @@
      */
     public abstract boolean intersects(List<ByteBuffer> minClusteringValues, List<ByteBuffer> maxClusteringValues);
 
-    /**
-     * Given a sliceable row iterator, returns a row iterator that only return rows selected by the slice of
-     * this {@code Slices} object.
-     *
-     * @param iter the sliceable iterator to filter.
-     *
-     * @return an iterator that only returns the rows (or rather Unfiltered) of {@code iter} that are selected by those slices.
-     */
-    public abstract UnfilteredRowIterator makeSliceIterator(SliceableUnfilteredRowIterator iter);
-
     public abstract String toCQLString(CFMetaData metadata);
 
     /**
@@ -196,7 +185,7 @@
             this.slices = new ArrayList<>(initialSize);
         }
 
-        public Builder add(Slice.Bound start, Slice.Bound end)
+        public Builder add(ClusteringBound start, ClusteringBound end)
         {
             return add(Slice.make(start, end));
         }
@@ -345,7 +334,7 @@
             for (int i = 0; i < size; i++)
                 slices[i] = Slice.serializer.deserialize(in, version, metadata.comparator.subtypes());
 
-            if (size == 1 && slices[0].start() == Slice.Bound.BOTTOM && slices[0].end() == Slice.Bound.TOP)
+            if (size == 1 && slices[0].start() == ClusteringBound.BOTTOM && slices[0].end() == ClusteringBound.TOP)
                 return ALL;
 
             return new ArrayBackedSlices(metadata.comparator, slices);
@@ -459,65 +448,6 @@
             return false;
         }
 
-        public UnfilteredRowIterator makeSliceIterator(final SliceableUnfilteredRowIterator iter)
-        {
-            return new WrappingUnfilteredRowIterator(iter)
-            {
-                private int nextSlice = iter.isReverseOrder() ? slices.length - 1 : 0;
-                private Iterator<Unfiltered> currentSliceIterator = Collections.emptyIterator();
-
-                private Unfiltered next;
-
-                @Override
-                public boolean hasNext()
-                {
-                    prepareNext();
-                    return next != null;
-                }
-
-                @Override
-                public Unfiltered next()
-                {
-                    prepareNext();
-                    Unfiltered toReturn = next;
-                    next = null;
-                    return toReturn;
-                }
-
-                private boolean hasMoreSlice()
-                {
-                    return isReverseOrder()
-                         ? nextSlice >= 0
-                         : nextSlice < slices.length;
-                }
-
-                private Slice popNextSlice()
-                {
-                    return slices[isReverseOrder() ? nextSlice-- : nextSlice++];
-                }
-
-                private void prepareNext()
-                {
-                    if (next != null)
-                        return;
-
-                    while (true)
-                    {
-                        if (currentSliceIterator.hasNext())
-                        {
-                            next = currentSliceIterator.next();
-                            return;
-                        }
-
-                        if (!hasMoreSlice())
-                            return;
-
-                        currentSliceIterator = iter.slice(popNextSlice());
-                    }
-                }
-            };
-        }
-
         public Iterator<Slice> iterator()
         {
             return Iterators.forArray(slices);
@@ -683,6 +613,8 @@
                 }
                 else
                 {
+                    boolean isReversed = column.isReversedType();
+
                     // As said above, we assume (without checking) that this means all ComponentOfSlice for this column
                     // are the same, so we only bother about the first.
                     if (first.startValue != null)
@@ -690,14 +622,24 @@
                         if (needAnd)
                             sb.append(" AND ");
                         needAnd = true;
-                        sb.append(column.name).append(first.startInclusive ? " >= " : " > ").append(column.type.getString(first.startValue));
+                        sb.append(column.name);
+                        if (isReversed)
+                            sb.append(first.startInclusive ? " <= " : " < ");
+                        else
+                            sb.append(first.startInclusive ? " >= " : " > ");
+                        sb.append(column.type.getString(first.startValue));
                     }
                     if (first.endValue != null)
                     {
                         if (needAnd)
                             sb.append(" AND ");
                         needAnd = true;
-                        sb.append(column.name).append(first.endInclusive ? " <= " : " < ").append(column.type.getString(first.endValue));
+                        sb.append(column.name);
+                        if (isReversed)
+                            sb.append(first.endInclusive ? " >= " : " > ");
+                        else
+                            sb.append(first.endInclusive ? " <= " : " < ");
+                        sb.append(column.type.getString(first.endValue));
                     }
                 }
             }
@@ -722,8 +664,8 @@
 
             public static ComponentOfSlice fromSlice(int component, Slice slice)
             {
-                Slice.Bound start = slice.start();
-                Slice.Bound end = slice.end();
+                ClusteringBound start = slice.start();
+                ClusteringBound end = slice.end();
 
                 if (component >= start.size() && component >= end.size())
                     return null;
@@ -810,11 +752,6 @@
             return true;
         }
 
-        public UnfilteredRowIterator makeSliceIterator(SliceableUnfilteredRowIterator iter)
-        {
-            return iter;
-        }
-
         public Iterator<Slice> iterator()
         {
             return Iterators.singletonIterator(Slice.ALL);
@@ -890,15 +827,9 @@
             return false;
         }
 
-        public UnfilteredRowIterator makeSliceIterator(SliceableUnfilteredRowIterator iter)
-        {
-            return UnfilteredRowIterators.noRowsIterator(iter.metadata(), iter.partitionKey(), iter.staticRow(),
-                                                         iter.partitionLevelDeletion(), iter.isReverseOrder());
-        }
-
         public Iterator<Slice> iterator()
         {
-            return Iterators.emptyIterator();
+            return Collections.emptyIterator();
         }
 
         @Override
diff --git a/src/java/org/apache/cassandra/db/StorageHook.java b/src/java/org/apache/cassandra/db/StorageHook.java
new file mode 100644
index 0000000..48d7ede
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/StorageHook.java
@@ -0,0 +1,104 @@
+/*
+ * 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.db;
+
+import java.util.UUID;
+
+import org.apache.cassandra.db.filter.ClusteringIndexFilter;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.db.rows.UnfilteredRowIteratorWithLowerBound;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.sstable.format.SSTableReadsListener;
+import org.apache.cassandra.utils.FBUtilities;
+
+public interface StorageHook
+{
+    public static final StorageHook instance = createHook();
+
+    public void reportWrite(UUID cfid, PartitionUpdate partitionUpdate);
+    public void reportRead(UUID cfid, DecoratedKey key);
+    public UnfilteredRowIteratorWithLowerBound makeRowIteratorWithLowerBound(ColumnFamilyStore cfs,
+                                                                      DecoratedKey partitionKey,
+                                                                      SSTableReader sstable,
+                                                                      ClusteringIndexFilter filter,
+                                                                      ColumnFilter selectedColumns,
+                                                                      boolean isForThrift,
+                                                                      int nowInSec,
+                                                                      boolean applyThriftTransformation,
+                                                                      SSTableReadsListener listener);
+
+    public UnfilteredRowIterator makeRowIterator(ColumnFamilyStore cfs,
+                                                 SSTableReader sstable,
+                                                 DecoratedKey key,
+                                                 Slices slices,
+                                                 ColumnFilter selectedColumns,
+                                                 boolean reversed,
+                                                 boolean isForThrift,
+                                                 SSTableReadsListener listener);
+
+    static StorageHook createHook()
+    {
+        String className =  System.getProperty("cassandra.storage_hook");
+        if (className != null)
+        {
+            return FBUtilities.construct(className, StorageHook.class.getSimpleName());
+        }
+
+        return new StorageHook()
+        {
+            public void reportWrite(UUID cfid, PartitionUpdate partitionUpdate) {}
+
+            public void reportRead(UUID cfid, DecoratedKey key) {}
+
+            public UnfilteredRowIteratorWithLowerBound makeRowIteratorWithLowerBound(ColumnFamilyStore cfs,
+                                                                                     DecoratedKey partitionKey,
+                                                                                     SSTableReader sstable,
+                                                                                     ClusteringIndexFilter filter,
+                                                                                     ColumnFilter selectedColumns,
+                                                                                     boolean isForThrift,
+                                                                                     int nowInSec,
+                                                                                     boolean applyThriftTransformation,
+                                                                                     SSTableReadsListener listener)
+            {
+                return new UnfilteredRowIteratorWithLowerBound(partitionKey,
+                                                               sstable,
+                                                               filter,
+                                                               selectedColumns,
+                                                               isForThrift,
+                                                               nowInSec,
+                                                               applyThriftTransformation,
+                                                               listener);
+            }
+
+            public UnfilteredRowIterator makeRowIterator(ColumnFamilyStore cfs,
+                                                         SSTableReader sstable,
+                                                         DecoratedKey key,
+                                                         Slices slices,
+                                                         ColumnFilter selectedColumns,
+                                                         boolean reversed,
+                                                         boolean isForThrift,
+                                                         SSTableReadsListener listener)
+            {
+                return sstable.iterator(key, slices, selectedColumns, reversed, isForThrift, listener);
+            }
+        };
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/SystemKeyspace.java b/src/java/org/apache/cassandra/db/SystemKeyspace.java
index c0bf317..c902fa0 100644
--- a/src/java/org/apache/cassandra/db/SystemKeyspace.java
+++ b/src/java/org/apache/cassandra/db/SystemKeyspace.java
@@ -43,8 +43,10 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
 import com.google.common.io.ByteStreams;
 import com.google.common.util.concurrent.Futures;
 import org.slf4j.Logger;
@@ -52,20 +54,23 @@
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.cql3.functions.AggregateFcts;
 import org.apache.cassandra.cql3.functions.BytesConversionFcts;
+import org.apache.cassandra.cql3.functions.CastFcts;
 import org.apache.cassandra.cql3.functions.TimeFcts;
 import org.apache.cassandra.cql3.functions.UuidFcts;
 import org.apache.cassandra.db.commitlog.CommitLog;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.apache.cassandra.db.compaction.CompactionHistoryTabularData;
 import org.apache.cassandra.db.marshal.BytesType;
 import org.apache.cassandra.db.marshal.TimeUUIDType;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.db.marshal.UUIDType;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.Rows;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.LocalPartitioner;
 import org.apache.cassandra.dht.Range;
@@ -91,10 +96,11 @@
 import org.apache.cassandra.service.paxos.Commit;
 import org.apache.cassandra.service.paxos.PaxosState;
 import org.apache.cassandra.thrift.cassandraConstants;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.CassandraVersion;
 import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.MD5Digest;
 import org.apache.cassandra.utils.Pair;
 import org.apache.cassandra.utils.UUIDGen;
 
@@ -121,8 +127,6 @@
     // Cassandra was not previously installed and we're in the process of starting a fresh node.
     public static final CassandraVersion NULL_VERSION = new CassandraVersion("0.0.0-absent");
 
-    public static final String NAME = "system";
-
     public static final String BATCHES = "batches";
     public static final String PAXOS = "paxos";
     public static final String BUILT_INDEXES = "IndexInfo";
@@ -134,8 +138,10 @@
     public static final String SSTABLE_ACTIVITY = "sstable_activity";
     public static final String SIZE_ESTIMATES = "size_estimates";
     public static final String AVAILABLE_RANGES = "available_ranges";
+    public static final String TRANSFERRED_RANGES = "transferred_ranges";
     public static final String VIEWS_BUILDS_IN_PROGRESS = "views_builds_in_progress";
     public static final String BUILT_VIEWS = "built_views";
+    public static final String PREPARED_STATEMENTS = "prepared_statements";
 
     @Deprecated public static final String LEGACY_HINTS = "hints";
     @Deprecated public static final String LEGACY_BATCHLOG = "batchlog";
@@ -285,6 +291,16 @@
                 + "ranges set<blob>,"
                 + "PRIMARY KEY ((keyspace_name)))");
 
+    private static final CFMetaData TransferredRanges =
+        compile(TRANSFERRED_RANGES,
+                "record of transferred ranges for streaming operation",
+                "CREATE TABLE %s ("
+                + "operation text,"
+                + "peer inet,"
+                + "keyspace_name text,"
+                + "ranges set<blob>,"
+                + "PRIMARY KEY ((operation, keyspace_name), peer))");
+
     private static final CFMetaData ViewsBuildsInProgress =
         compile(VIEWS_BUILDS_IN_PROGRESS,
                 "views builds current progress",
@@ -301,8 +317,18 @@
                 "CREATE TABLE %s ("
                 + "keyspace_name text,"
                 + "view_name text,"
+                + "status_replicated boolean,"
                 + "PRIMARY KEY ((keyspace_name), view_name))");
 
+    private static final CFMetaData PreparedStatements =
+        compile(PREPARED_STATEMENTS,
+                "prepared statements",
+                "CREATE TABLE %s ("
+                + "prepared_id blob,"
+                + "logged_keyspace text,"
+                + "query_string text,"
+                + "PRIMARY KEY ((prepared_id)))");
+
     @Deprecated
     public static final CFMetaData LegacyHints =
         compile(LEGACY_HINTS,
@@ -447,13 +473,13 @@
 
     private static CFMetaData compile(String name, String description, String schema)
     {
-        return CFMetaData.compile(String.format(schema, name), NAME)
+        return CFMetaData.compile(String.format(schema, name), SchemaConstants.SYSTEM_KEYSPACE_NAME)
                          .comment(description);
     }
 
     public static KeyspaceMetadata metadata()
     {
-        return KeyspaceMetadata.create(NAME, KeyspaceParams.local(), tables(), Views.none(), Types.none(), functions());
+        return KeyspaceMetadata.create(SchemaConstants.SYSTEM_KEYSPACE_NAME, KeyspaceParams.local(), tables(), Views.none(), Types.none(), functions());
     }
 
     private static Tables tables()
@@ -469,10 +495,12 @@
                          SSTableActivity,
                          SizeEstimates,
                          AvailableRanges,
+                         TransferredRanges,
                          ViewsBuildsInProgress,
                          BuiltViews,
                          LegacyHints,
                          LegacyBatchlog,
+                         PreparedStatements,
                          LegacyKeyspaces,
                          LegacyColumnfamilies,
                          LegacyColumns,
@@ -489,10 +517,11 @@
                         .add(TimeFcts.all())
                         .add(BytesConversionFcts.all())
                         .add(AggregateFcts.all())
+                        .add(CastFcts.all())
                         .build();
     }
 
-    private static volatile Map<UUID, Pair<ReplayPosition, Long>> truncationRecords;
+    private static volatile Map<UUID, Pair<CommitLogPosition, Long>> truncationRecords;
 
     public enum BootstrapState
     {
@@ -536,7 +565,7 @@
                             FBUtilities.getReleaseVersionString(),
                             QueryProcessor.CQL_VERSION.toString(),
                             cassandraConstants.VERSION,
-                            String.valueOf(Server.CURRENT_VERSION),
+                            String.valueOf(ProtocolVersion.CURRENT.asInt()),
                             snitch.getDatacenter(FBUtilities.getBroadcastAddress()),
                             snitch.getRack(FBUtilities.getBroadcastAddress()),
                             DatabaseDescriptor.getPartitioner().getClass().getName(),
@@ -582,26 +611,36 @@
     public static boolean isViewBuilt(String keyspaceName, String viewName)
     {
         String req = "SELECT view_name FROM %s.\"%s\" WHERE keyspace_name=? AND view_name=?";
-        UntypedResultSet result = executeInternal(String.format(req, NAME, BUILT_VIEWS), keyspaceName, viewName);
+        UntypedResultSet result = executeInternal(String.format(req, SchemaConstants.SYSTEM_KEYSPACE_NAME, BUILT_VIEWS), keyspaceName, viewName);
         return !result.isEmpty();
     }
 
-    public static void setViewBuilt(String keyspaceName, String viewName)
+    public static boolean isViewStatusReplicated(String keyspaceName, String viewName)
     {
-        String req = "INSERT INTO %s.\"%s\" (keyspace_name, view_name) VALUES (?, ?)";
-        executeInternal(String.format(req, NAME, BUILT_VIEWS), keyspaceName, viewName);
-        forceBlockingFlush(BUILT_VIEWS);
+        String req = "SELECT status_replicated FROM %s.\"%s\" WHERE keyspace_name=? AND view_name=?";
+        UntypedResultSet result = executeInternal(String.format(req, SchemaConstants.SYSTEM_KEYSPACE_NAME, BUILT_VIEWS), keyspaceName, viewName);
+
+        if (result.isEmpty())
+            return false;
+        UntypedResultSet.Row row = result.one();
+        return row.has("status_replicated") && row.getBoolean("status_replicated");
     }
 
+    public static void setViewBuilt(String keyspaceName, String viewName, boolean replicated)
+    {
+        String req = "INSERT INTO %s.\"%s\" (keyspace_name, view_name, status_replicated) VALUES (?, ?, ?)";
+        executeInternal(String.format(req, SchemaConstants.SYSTEM_KEYSPACE_NAME, BUILT_VIEWS), keyspaceName, viewName, replicated);
+        forceBlockingFlush(BUILT_VIEWS);
+    }
 
     public static void setViewRemoved(String keyspaceName, String viewName)
     {
         String buildReq = "DELETE FROM %S.%s WHERE keyspace_name = ? AND view_name = ?";
-        executeInternal(String.format(buildReq, NAME, VIEWS_BUILDS_IN_PROGRESS), keyspaceName, viewName);
+        executeInternal(String.format(buildReq, SchemaConstants.SYSTEM_KEYSPACE_NAME, VIEWS_BUILDS_IN_PROGRESS), keyspaceName, viewName);
         forceBlockingFlush(VIEWS_BUILDS_IN_PROGRESS);
 
         String builtReq = "DELETE FROM %s.\"%s\" WHERE keyspace_name = ? AND view_name = ?";
-        executeInternal(String.format(builtReq, NAME, BUILT_VIEWS), keyspaceName, viewName);
+        executeInternal(String.format(builtReq, SchemaConstants.SYSTEM_KEYSPACE_NAME, BUILT_VIEWS), keyspaceName, viewName);
         forceBlockingFlush(BUILT_VIEWS);
     }
 
@@ -618,14 +657,18 @@
         // We flush the view built first, because if we fail now, we'll restart at the last place we checkpointed
         // view build.
         // If we flush the delete first, we'll have to restart from the beginning.
-        // Also, if the build succeeded, but the view build failed, we will be able to skip the view build check
-        // next boot.
-        setViewBuilt(ksname, viewName);
-        forceBlockingFlush(BUILT_VIEWS);
+        // Also, if writing to the built_view succeeds, but the view_builds_in_progress deletion fails, we will be able
+        // to skip the view build next boot.
+        setViewBuilt(ksname, viewName, false);
         executeInternal(String.format("DELETE FROM system.%s WHERE keyspace_name = ? AND view_name = ?", VIEWS_BUILDS_IN_PROGRESS), ksname, viewName);
         forceBlockingFlush(VIEWS_BUILDS_IN_PROGRESS);
     }
 
+    public static void setViewBuiltReplicated(String ksname, String viewName)
+    {
+        setViewBuilt(ksname, viewName, true);
+    }
+
     public static void updateViewBuildStatus(String ksname, String viewName, Token token)
     {
         String req = "INSERT INTO system.%s (keyspace_name, view_name, last_token) VALUES (?, ?, ?)";
@@ -655,7 +698,7 @@
         return Pair.create(generation, lastKey);
     }
 
-    public static synchronized void saveTruncationRecord(ColumnFamilyStore cfs, long truncatedAt, ReplayPosition position)
+    public static synchronized void saveTruncationRecord(ColumnFamilyStore cfs, long truncatedAt, CommitLogPosition position)
     {
         String req = "UPDATE system.%s SET truncated_at = truncated_at + ? WHERE key = '%s'";
         executeInternal(String.format(req, LOCAL, LOCAL), truncationAsMapEntry(cfs, truncatedAt, position));
@@ -674,13 +717,13 @@
         forceBlockingFlush(LOCAL);
     }
 
-    private static Map<UUID, ByteBuffer> truncationAsMapEntry(ColumnFamilyStore cfs, long truncatedAt, ReplayPosition position)
+    private static Map<UUID, ByteBuffer> truncationAsMapEntry(ColumnFamilyStore cfs, long truncatedAt, CommitLogPosition position)
     {
-        try (DataOutputBuffer out = new DataOutputBuffer())
+        try (DataOutputBuffer out = DataOutputBuffer.scratchBuffer.get())
         {
-            ReplayPosition.serializer.serialize(position, out);
+            CommitLogPosition.serializer.serialize(position, out);
             out.writeLong(truncatedAt);
-            return singletonMap(cfs.metadata.cfId, ByteBuffer.wrap(out.getData(), 0, out.getLength()));
+            return singletonMap(cfs.metadata.cfId, out.asNewBuffer());
         }
         catch (IOException e)
         {
@@ -688,30 +731,30 @@
         }
     }
 
-    public static ReplayPosition getTruncatedPosition(UUID cfId)
+    public static CommitLogPosition getTruncatedPosition(UUID cfId)
     {
-        Pair<ReplayPosition, Long> record = getTruncationRecord(cfId);
+        Pair<CommitLogPosition, Long> record = getTruncationRecord(cfId);
         return record == null ? null : record.left;
     }
 
     public static long getTruncatedAt(UUID cfId)
     {
-        Pair<ReplayPosition, Long> record = getTruncationRecord(cfId);
+        Pair<CommitLogPosition, Long> record = getTruncationRecord(cfId);
         return record == null ? Long.MIN_VALUE : record.right;
     }
 
-    private static synchronized Pair<ReplayPosition, Long> getTruncationRecord(UUID cfId)
+    private static synchronized Pair<CommitLogPosition, Long> getTruncationRecord(UUID cfId)
     {
         if (truncationRecords == null)
             truncationRecords = readTruncationRecords();
         return truncationRecords.get(cfId);
     }
 
-    private static Map<UUID, Pair<ReplayPosition, Long>> readTruncationRecords()
+    private static Map<UUID, Pair<CommitLogPosition, Long>> readTruncationRecords()
     {
         UntypedResultSet rows = executeInternal(String.format("SELECT truncated_at FROM system.%s WHERE key = '%s'", LOCAL, LOCAL));
 
-        Map<UUID, Pair<ReplayPosition, Long>> records = new HashMap<>();
+        Map<UUID, Pair<CommitLogPosition, Long>> records = new HashMap<>();
 
         if (!rows.isEmpty() && rows.one().has("truncated_at"))
         {
@@ -723,11 +766,11 @@
         return records;
     }
 
-    private static Pair<ReplayPosition, Long> truncationRecordFromBlob(ByteBuffer bytes)
+    private static Pair<CommitLogPosition, Long> truncationRecordFromBlob(ByteBuffer bytes)
     {
         try (RebufferingInputStream in = new DataInputBuffer(bytes, true))
         {
-            return Pair.create(ReplayPosition.serializer.deserialize(in), in.available() > 0 ? in.readLong() : Long.MIN_VALUE);
+            return Pair.create(CommitLogPosition.serializer.deserialize(in), in.available() > 0 ? in.readLong() : Long.MIN_VALUE);
         }
         catch (IOException e)
         {
@@ -768,9 +811,9 @@
         if (ep.equals(FBUtilities.getBroadcastAddress()))
             return;
 
-        String req = "INSERT INTO system.%s (peer, %s) VALUES (?, ?)";
+        String req = "INSERT INTO system.%s (peer, release_version) VALUES (?, ?)";
         executorService.execute(() -> {
-            executeInternal(String.format(req, PEERS, "release_version"), ep, value);
+            executeInternal(String.format(req, PEERS), ep, value);
             postUpdateTask.run();
         });
     }
@@ -831,8 +874,8 @@
 
     public static void forceBlockingFlush(String cfname)
     {
-        if (!Boolean.getBoolean("cassandra.unsafesystem"))
-            FBUtilities.waitOnFuture(Keyspace.open(NAME).getColumnFamilyStore(cfname).forceFlush());
+        if (!DatabaseDescriptor.isUnsafeSystem())
+            FBUtilities.waitOnFuture(Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(cfname).forceFlush());
     }
 
     /**
@@ -979,7 +1022,7 @@
         Keyspace keyspace;
         try
         {
-            keyspace = Keyspace.open(NAME);
+            keyspace = Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME);
         }
         catch (AssertionError err)
         {
@@ -1090,21 +1133,21 @@
     public static boolean isIndexBuilt(String keyspaceName, String indexName)
     {
         String req = "SELECT index_name FROM %s.\"%s\" WHERE table_name=? AND index_name=?";
-        UntypedResultSet result = executeInternal(String.format(req, NAME, BUILT_INDEXES), keyspaceName, indexName);
+        UntypedResultSet result = executeInternal(String.format(req, SchemaConstants.SYSTEM_KEYSPACE_NAME, BUILT_INDEXES), keyspaceName, indexName);
         return !result.isEmpty();
     }
 
     public static void setIndexBuilt(String keyspaceName, String indexName)
     {
         String req = "INSERT INTO %s.\"%s\" (table_name, index_name) VALUES (?, ?)";
-        executeInternal(String.format(req, NAME, BUILT_INDEXES), keyspaceName, indexName);
+        executeInternal(String.format(req, SchemaConstants.SYSTEM_KEYSPACE_NAME, BUILT_INDEXES), keyspaceName, indexName);
         forceBlockingFlush(BUILT_INDEXES);
     }
 
     public static void setIndexRemoved(String keyspaceName, String indexName)
     {
         String req = "DELETE FROM %s.\"%s\" WHERE table_name = ? AND index_name = ?";
-        executeInternal(String.format(req, NAME, BUILT_INDEXES), keyspaceName, indexName);
+        executeInternal(String.format(req, SchemaConstants.SYSTEM_KEYSPACE_NAME, BUILT_INDEXES), keyspaceName, indexName);
         forceBlockingFlush(BUILT_INDEXES);
     }
 
@@ -1112,7 +1155,7 @@
     {
         List<String> names = new ArrayList<>(indexNames);
         String req = "SELECT index_name from %s.\"%s\" WHERE table_name=? AND index_name IN ?";
-        UntypedResultSet results = executeInternal(String.format(req, NAME, BUILT_INDEXES), keyspaceName, names);
+        UntypedResultSet results = executeInternal(String.format(req, SchemaConstants.SYSTEM_KEYSPACE_NAME, BUILT_INDEXES), keyspaceName, names);
         return StreamSupport.stream(results.spliterator(), false)
                             .map(r -> r.getString("index_name"))
                             .collect(Collectors.toList());
@@ -1198,7 +1241,7 @@
     public static PaxosState loadPaxosState(DecoratedKey key, CFMetaData metadata, int nowInSec)
     {
         String req = "SELECT * FROM system.%s WHERE row_key = ? AND cf_id = ?";
-        UntypedResultSet results = QueryProcessor.executeInternalWithNow(nowInSec, String.format(req, PAXOS), key.getKey(), metadata.cfId);
+        UntypedResultSet results = QueryProcessor.executeInternalWithNow(nowInSec, System.nanoTime(), String.format(req, PAXOS), key.getKey(), metadata.cfId);
         if (results.isEmpty())
             return new PaxosState(key, metadata);
         UntypedResultSet.Row row = results.one();
@@ -1325,11 +1368,11 @@
         {
             Range<Token> range = entry.getKey();
             Pair<Long, Long> values = entry.getValue();
-            new RowUpdateBuilder(SizeEstimates, timestamp, mutation)
-                .clustering(table, range.left.toString(), range.right.toString())
-                .add("partitions_count", values.left)
-                .add("mean_partition_size", values.right)
-                .build();
+            update.add(Rows.simpleBuilder(SizeEstimates, table, range.left.toString(), range.right.toString())
+                           .timestamp(timestamp)
+                           .add("partitions_count", values.left)
+                           .add("mean_partition_size", values.right)
+                           .build());
         }
 
         mutation.apply();
@@ -1340,7 +1383,7 @@
      */
     public static void clearSizeEstimates(String keyspace, String table)
     {
-        String cql = String.format("DELETE FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", NAME, SIZE_ESTIMATES);
+        String cql = String.format("DELETE FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", SchemaConstants.SYSTEM_KEYSPACE_NAME, SIZE_ESTIMATES);
         executeInternal(cql, keyspace, table);
     }
 
@@ -1349,7 +1392,7 @@
      */
     public static void clearSizeEstimates(String keyspace)
     {
-        String cql = String.format("DELETE FROM %s.%s WHERE keyspace_name = ?", NAME, SIZE_ESTIMATES);
+        String cql = String.format("DELETE FROM %s.%s WHERE keyspace_name = ?", SchemaConstants.SYSTEM_KEYSPACE_NAME, SIZE_ESTIMATES);
         executeInternal(cql, keyspace);
     }
 
@@ -1360,7 +1403,7 @@
     public static synchronized SetMultimap<String, String> getTablesWithSizeEstimates()
     {
         SetMultimap<String, String> keyspaceTableMap = HashMultimap.create();
-        String cql = String.format("SELECT keyspace_name, table_name FROM %s.%s", NAME, SIZE_ESTIMATES);
+        String cql = String.format("SELECT keyspace_name, table_name FROM %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, SIZE_ESTIMATES);
         UntypedResultSet rs = executeInternal(cql);
         for (UntypedResultSet.Row row : rs)
         {
@@ -1399,10 +1442,43 @@
 
     public static void resetAvailableRanges()
     {
-        ColumnFamilyStore availableRanges = Keyspace.open(NAME).getColumnFamilyStore(AVAILABLE_RANGES);
+        ColumnFamilyStore availableRanges = Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(AVAILABLE_RANGES);
         availableRanges.truncateBlockingWithoutSnapshot();
     }
 
+    public static synchronized void updateTransferredRanges(String description,
+                                                         InetAddress peer,
+                                                         String keyspace,
+                                                         Collection<Range<Token>> streamedRanges)
+    {
+        String cql = "UPDATE system.%s SET ranges = ranges + ? WHERE operation = ? AND peer = ? AND keyspace_name = ?";
+        Set<ByteBuffer> rangesToUpdate = new HashSet<>(streamedRanges.size());
+        for (Range<Token> range : streamedRanges)
+        {
+            rangesToUpdate.add(rangeToBytes(range));
+        }
+        executeInternal(String.format(cql, TRANSFERRED_RANGES), rangesToUpdate, description, peer, keyspace);
+    }
+
+    public static synchronized Map<InetAddress, Set<Range<Token>>> getTransferredRanges(String description, String keyspace, IPartitioner partitioner)
+    {
+        Map<InetAddress, Set<Range<Token>>> result = new HashMap<>();
+        String query = "SELECT * FROM system.%s WHERE operation = ? AND keyspace_name = ?";
+        UntypedResultSet rs = executeInternal(String.format(query, TRANSFERRED_RANGES), description, keyspace);
+        for (UntypedResultSet.Row row : rs)
+        {
+            InetAddress peer = row.getInetAddress("peer");
+            Set<ByteBuffer> rawRanges = row.getSet("ranges", BytesType.instance);
+            Set<Range<Token>> ranges = Sets.newHashSetWithExpectedSize(rawRanges.size());
+            for (ByteBuffer rawRange : rawRanges)
+            {
+                ranges.add(byteBufferToRange(rawRange, partitioner));
+            }
+            result.put(peer, ranges);
+        }
+        return ImmutableMap.copyOf(result);
+    }
+
     /**
      * Compare the release version in the system.local table with the one included in the distro.
      * If they don't match, snapshot all tables in the system keyspace. This is intended to be
@@ -1415,6 +1491,8 @@
         String previous = getPreviousVersionString();
         String next = FBUtilities.getReleaseVersionString();
 
+        FBUtilities.setPreviousReleaseVersionString(previous);
+
         // if we're restarting after an upgrade, snapshot the system keyspace
         if (!previous.equals(NULL_VERSION.toString()) && !previous.equals(next))
 
@@ -1423,7 +1501,7 @@
             String snapshotName = Keyspace.getTimestampedSnapshotName(String.format("upgrade-%s-%s",
                                                                                     previous,
                                                                                     next));
-            Keyspace systemKs = Keyspace.open(SystemKeyspace.NAME);
+            Keyspace systemKs = Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME);
             systemKs.snapshot(snapshotName, null);
             return true;
         }
@@ -1452,7 +1530,7 @@
             // the current version is. If we couldn't read a previous version from system.local we check for
             // the existence of the legacy system.Versions table. We don't actually attempt to read a version
             // from there, but it informs us that this isn't a completely new node.
-            for (File dataDirectory : Directories.getKSChildDirectories(SystemKeyspace.NAME))
+            for (File dataDirectory : Directories.getKSChildDirectories(SchemaConstants.SYSTEM_KEYSPACE_NAME))
             {
                 if (dataDirectory.getName().equals("Versions") && dataDirectory.listFiles().length > 0)
                 {
@@ -1535,4 +1613,61 @@
         }
     }
 
+    public static void writePreparedStatement(String loggedKeyspace, MD5Digest key, String cql)
+    {
+        executeInternal(String.format("INSERT INTO %s.%s"
+                                      + " (logged_keyspace, prepared_id, query_string) VALUES (?, ?, ?)",
+                                      SchemaConstants.SYSTEM_KEYSPACE_NAME, PREPARED_STATEMENTS),
+                        loggedKeyspace, key.byteBuffer(), cql);
+        logger.debug("stored prepared statement for logged keyspace '{}': '{}'", loggedKeyspace, cql);
+    }
+
+    public static void removePreparedStatement(MD5Digest key)
+    {
+        executeInternal(String.format("DELETE FROM %s.%s"
+                                      + " WHERE prepared_id = ?",
+                                      SchemaConstants.SYSTEM_KEYSPACE_NAME, PREPARED_STATEMENTS),
+                        key.byteBuffer());
+    }
+
+    public static void resetPreparedStatements()
+    {
+        ColumnFamilyStore preparedStatements = Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(PREPARED_STATEMENTS);
+        preparedStatements.truncateBlockingWithoutSnapshot();
+    }
+
+    public static int loadPreparedStatements(TriFunction<MD5Digest, String, String, Boolean> onLoaded)
+    {
+        String query = String.format("SELECT prepared_id, logged_keyspace, query_string FROM %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, PREPARED_STATEMENTS);
+        UntypedResultSet resultSet = executeOnceInternal(query);
+        int counter = 0;
+        for (UntypedResultSet.Row row : resultSet)
+        {
+            if (onLoaded.accept(MD5Digest.wrap(row.getByteArray("prepared_id")),
+                                row.getString("query_string"),
+                                row.has("logged_keyspace") ? row.getString("logged_keyspace") : null))
+                counter++;
+        }
+        return counter;
+    }
+
+    public static int loadPreparedStatement(MD5Digest digest, TriFunction<MD5Digest, String, String, Boolean> onLoaded)
+    {
+        String query = String.format("SELECT prepared_id, logged_keyspace, query_string FROM %s.%s WHERE prepared_id = ?", SchemaConstants.SYSTEM_KEYSPACE_NAME, PREPARED_STATEMENTS);
+        UntypedResultSet resultSet = executeOnceInternal(query, digest.byteBuffer());
+        int counter = 0;
+        for (UntypedResultSet.Row row : resultSet)
+        {
+            if (onLoaded.accept(MD5Digest.wrap(row.getByteArray("prepared_id")),
+                                row.getString("query_string"),
+                                row.has("logged_keyspace") ? row.getString("logged_keyspace") : null))
+                counter++;
+        }
+        return counter;
+    }
+
+    public static interface TriFunction<A, B, C, D> {
+        D accept(A var1, B var2, C var3);
+    }
+
 }
diff --git a/src/java/org/apache/cassandra/db/UnfilteredDeserializer.java b/src/java/org/apache/cassandra/db/UnfilteredDeserializer.java
index 2d270bc..262b333 100644
--- a/src/java/org/apache/cassandra/db/UnfilteredDeserializer.java
+++ b/src/java/org/apache/cassandra/db/UnfilteredDeserializer.java
@@ -79,7 +79,7 @@
      * comparison. Whenever we know what to do with this atom (read it or skip it),
      * readNext or skipNext should be called.
      */
-    public abstract int compareNextTo(Slice.Bound bound) throws IOException;
+    public abstract int compareNextTo(ClusteringBound bound) throws IOException;
 
     /**
      * Returns whether the next atom is a row or not.
@@ -171,7 +171,7 @@
             isReady = true;
         }
 
-        public int compareNextTo(Slice.Bound bound) throws IOException
+        public int compareNextTo(ClusteringBound bound) throws IOException
         {
             if (!isReady)
                 prepareNext();
@@ -200,7 +200,7 @@
             isReady = false;
             if (UnfilteredSerializer.kind(nextFlags) == Unfiltered.Kind.RANGE_TOMBSTONE_MARKER)
             {
-                RangeTombstone.Bound bound = clusteringDeserializer.deserializeNextBound();
+                ClusteringBoundOrBoundary bound = clusteringDeserializer.deserializeNextBound();
                 return UnfilteredSerializer.serializer.deserializeMarkerBody(in, header, bound);
             }
             else
@@ -384,7 +384,7 @@
             }
         }
 
-        public int compareNextTo(Slice.Bound bound) throws IOException
+        public int compareNextTo(ClusteringBound bound) throws IOException
         {
             if (!hasNext())
                 throw new IllegalStateException();
@@ -483,6 +483,7 @@
                 this.atoms = new AtomIterator(atomReader, metadata);
             }
 
+
             public boolean hasNext()
             {
                 // Note that we loop on next == null because TombstoneTracker.openNew() could return null below or the atom might be shadowed.
@@ -521,7 +522,7 @@
                         return false;
                     }
                 }
-                return next != null;
+                return true;
             }
 
             private Unfiltered readRow(LegacyLayout.LegacyAtom first)
@@ -574,10 +575,10 @@
                         // the iterator for later processing.
                         Clustering currentRow = grouper.currentRowClustering();
                         atoms.next(); // consume since we had only just peeked it so far and we're using it
-                        atoms.pushOutOfOrder(rt.withNewStart(Slice.Bound.exclusiveStartOf(currentRow)));
+                        atoms.pushOutOfOrder(rt.withNewStart(ClusteringBound.exclusiveStartOf(currentRow)));
                         // Note: in theory the withNewStart is a no-op here, but not taking any risk
-                        grouper.addAtom(rt.withNewStart(Slice.Bound.inclusiveStartOf(currentRow))
-                                          .withNewEnd(Slice.Bound.inclusiveEndOf(currentRow)));
+                        grouper.addAtom(rt.withNewStart(ClusteringBound.inclusiveStartOf(currentRow))
+                                          .withNewEnd(ClusteringBound.inclusiveEndOf(currentRow)));
                     }
                 }
 
diff --git a/src/java/org/apache/cassandra/db/WindowsFailedSnapshotTracker.java b/src/java/org/apache/cassandra/db/WindowsFailedSnapshotTracker.java
index 7cc7893..134fb11 100644
--- a/src/java/org/apache/cassandra/db/WindowsFailedSnapshotTracker.java
+++ b/src/java/org/apache/cassandra/db/WindowsFailedSnapshotTracker.java
@@ -85,8 +85,7 @@
             }
             catch (IOException e)
             {
-                logger.warn("Failed to open {}. Obsolete snapshots from previous runs will not be deleted.", TODELETEFILE);
-                logger.warn("Exception: " + e);
+                logger.warn("Failed to open {}. Obsolete snapshots from previous runs will not be deleted.", TODELETEFILE, e);
             }
         }
 
diff --git a/src/java/org/apache/cassandra/db/WriteType.java b/src/java/org/apache/cassandra/db/WriteType.java
index fdbe97d..11909e7 100644
--- a/src/java/org/apache/cassandra/db/WriteType.java
+++ b/src/java/org/apache/cassandra/db/WriteType.java
@@ -25,5 +25,6 @@
     COUNTER,
     BATCH_LOG,
     CAS,
-    VIEW;
+    VIEW,
+    CDC;
 }
diff --git a/src/java/org/apache/cassandra/db/aggregation/AggregationSpecification.java b/src/java/org/apache/cassandra/db/aggregation/AggregationSpecification.java
new file mode 100644
index 0000000..7324dfd
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/aggregation/AggregationSpecification.java
@@ -0,0 +1,182 @@
+/*
+ * 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.db.aggregation;
+
+import java.io.IOException;
+
+import org.apache.cassandra.db.ClusteringComparator;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+
+/**
+ * Defines how rows should be grouped for creating aggregates.
+ */
+public abstract class AggregationSpecification
+{
+    public static final Serializer serializer = new Serializer();
+
+    /**
+     * <code>AggregationSpecification</code> that group all the row together.
+     */
+    public static final AggregationSpecification AGGREGATE_EVERYTHING = new AggregationSpecification(Kind.AGGREGATE_EVERYTHING)
+    {
+        @Override
+        public GroupMaker newGroupMaker(GroupingState state)
+        {
+            return GroupMaker.GROUP_EVERYTHING;
+        }
+    };
+
+    /**
+     * The <code>AggregationSpecification</code> kind.
+     */
+    private final Kind kind;
+
+    /**
+     * The <code>AggregationSpecification</code> kinds.
+     */
+    public static enum Kind
+    {
+        AGGREGATE_EVERYTHING, AGGREGATE_BY_PK_PREFIX
+    }
+
+    /**
+     * Returns the <code>AggregationSpecification</code> kind.
+     * @return the <code>AggregationSpecification</code> kind
+     */
+    public Kind kind()
+    {
+        return kind;
+    }
+
+    private AggregationSpecification(Kind kind)
+    {
+        this.kind = kind;
+    }
+
+    /**
+     * Creates a new <code>GroupMaker</code> instance.
+     *
+     * @return a new <code>GroupMaker</code> instance
+     */
+    public final GroupMaker newGroupMaker()
+    {
+        return newGroupMaker(GroupingState.EMPTY_STATE);
+    }
+
+    /**
+     * Creates a new <code>GroupMaker</code> instance.
+     *
+     * @param state <code>GroupMaker</code> state
+     * @return a new <code>GroupMaker</code> instance
+     */
+    public abstract GroupMaker newGroupMaker(GroupingState state);
+
+    /**
+     * Creates a new <code>AggregationSpecification</code> instance that will build aggregates based on primary key
+     * columns.
+     *
+     * @param comparator the comparator used to compare the clustering prefixes
+     * @param clusteringPrefixSize the number of clustering columns used to create the aggregates
+     * @return a new <code>AggregationSpecification</code> instance that will build aggregates based on primary key
+     * columns
+     */
+    public static AggregationSpecification aggregatePkPrefix(ClusteringComparator comparator, int clusteringPrefixSize)
+    {
+        return new AggregateByPkPrefix(comparator, clusteringPrefixSize);
+    }
+
+    /**
+     * <code>AggregationSpecification</code> that build aggregates based on primary key columns
+     */
+    private static final class AggregateByPkPrefix extends AggregationSpecification
+    {
+        /**
+         * The number of clustering component to compare.
+         */
+        private final int clusteringPrefixSize;
+
+        /**
+         * The comparator used to compare the clustering prefixes.
+         */
+        private final ClusteringComparator comparator;
+
+        public AggregateByPkPrefix(ClusteringComparator comparator, int clusteringPrefixSize)
+        {
+            super(Kind.AGGREGATE_BY_PK_PREFIX);
+            this.comparator = comparator;
+            this.clusteringPrefixSize = clusteringPrefixSize;
+        }
+
+        @Override
+        public GroupMaker newGroupMaker(GroupingState state)
+        {
+            return GroupMaker.newInstance(comparator, clusteringPrefixSize, state);
+        }
+    }
+
+    public static class Serializer
+    {
+        public void serialize(AggregationSpecification aggregationSpec, DataOutputPlus out, int version) throws IOException
+        {
+            out.writeByte(aggregationSpec.kind().ordinal());
+            switch (aggregationSpec.kind())
+            {
+                case AGGREGATE_EVERYTHING:
+                    break;
+                case AGGREGATE_BY_PK_PREFIX:
+                    out.writeUnsignedVInt(((AggregateByPkPrefix) aggregationSpec).clusteringPrefixSize);
+                    break;
+                default:
+                    throw new AssertionError();
+            }
+        }
+
+        public AggregationSpecification deserialize(DataInputPlus in, int version, ClusteringComparator comparator) throws IOException
+        {
+            Kind kind = Kind.values()[in.readUnsignedByte()];
+            switch (kind)
+            {
+                case AGGREGATE_EVERYTHING:
+                    return AggregationSpecification.AGGREGATE_EVERYTHING;
+                case AGGREGATE_BY_PK_PREFIX:
+                    int clusteringPrefixSize = (int) in.readUnsignedVInt();
+                    return AggregationSpecification.aggregatePkPrefix(comparator, clusteringPrefixSize);
+                default:
+                    throw new AssertionError();
+            }
+        }
+
+        public long serializedSize(AggregationSpecification aggregationSpec, int version)
+        {
+            long size = TypeSizes.sizeof((byte) aggregationSpec.kind().ordinal());
+            switch (aggregationSpec.kind())
+            {
+                case AGGREGATE_EVERYTHING:
+                    break;
+                case AGGREGATE_BY_PK_PREFIX:
+                    size += TypeSizes.sizeofUnsignedVInt(((AggregateByPkPrefix) aggregationSpec).clusteringPrefixSize);
+                    break;
+                default:
+                    throw new AssertionError();
+            }
+            return size;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/aggregation/GroupMaker.java b/src/java/org/apache/cassandra/db/aggregation/GroupMaker.java
new file mode 100644
index 0000000..f6ad433
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/aggregation/GroupMaker.java
@@ -0,0 +1,142 @@
+/*
+ * 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.db.aggregation;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.ClusteringComparator;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+/**
+ * A <code>GroupMaker</code> can be used to determine if some sorted rows belongs to the same group or not.
+ */
+public abstract class GroupMaker
+{
+    /**
+     * <code>GroupMaker</code> that groups all the rows together.
+     */
+    public static final GroupMaker GROUP_EVERYTHING = new GroupMaker()
+    {
+        public boolean isNewGroup(DecoratedKey partitionKey, Clustering clustering)
+        {
+            return false;
+        }
+
+        public boolean returnAtLeastOneRow()
+        {
+            return true;
+        }
+    };
+
+    public static GroupMaker newInstance(ClusteringComparator comparator, int clusteringPrefixSize, GroupingState state)
+    {
+        return new PkPrefixGroupMaker(comparator, clusteringPrefixSize, state);
+    }
+
+    public static GroupMaker newInstance(ClusteringComparator comparator, int clusteringPrefixSize)
+    {
+        return new PkPrefixGroupMaker(comparator, clusteringPrefixSize);
+    }
+
+    /**
+     * Checks if a given row belongs to the same group that the previous row or not.
+     *
+     * @param partitionKey the partition key.
+     * @param clustering the row clustering key
+     * @return <code>true</code> if the row belongs to the same group that the previous one, <code>false</code>
+     * otherwise.
+     */
+    public abstract boolean isNewGroup(DecoratedKey partitionKey, Clustering clustering);
+
+    /**
+     * Specify if at least one row must be returned. If the selection is performing some aggregations on all the rows,
+     * one row should be returned even if no records were processed.
+     *
+     * @return <code>true</code> if at least one row must be returned, <code>false</code> otherwise.
+     */
+    public boolean returnAtLeastOneRow()
+    {
+        return false;
+    }
+
+    private static final class PkPrefixGroupMaker extends GroupMaker
+    {
+        /**
+         * The size of the clustering prefix used to make the groups
+         */
+        private final int clusteringPrefixSize;
+
+        /**
+         * The comparator used to compare the clustering prefixes.
+         */
+        private final ClusteringComparator comparator;
+
+        /**
+         * The last partition key seen
+         */
+        private ByteBuffer lastPartitionKey;
+
+        /**
+         * The last clustering seen
+         */
+        private Clustering lastClustering;
+
+        public PkPrefixGroupMaker(ClusteringComparator comparator, int clusteringPrefixSize, GroupingState state)
+        {
+            this(comparator, clusteringPrefixSize);
+            this.lastPartitionKey = state.partitionKey();
+            this.lastClustering = state.clustering;
+        }
+
+        public PkPrefixGroupMaker(ClusteringComparator comparator, int clusteringPrefixSize)
+        {
+            this.comparator = comparator;
+            this.clusteringPrefixSize = clusteringPrefixSize;
+        }
+
+        @Override
+        public boolean isNewGroup(DecoratedKey partitionKey, Clustering clustering)
+        {
+            boolean isNew = false;
+
+            // We are entering a new group if:
+            // - the partition key is a new one
+            // - the last clustering was not null and does not have the same prefix as the new clustering one
+            if (!partitionKey.getKey().equals(lastPartitionKey))
+            {
+                lastPartitionKey = partitionKey.getKey();
+                isNew = true;
+                if (Clustering.STATIC_CLUSTERING == clustering)
+                {
+                    lastClustering = null;
+                    return true;
+                }
+            }
+            else if (lastClustering != null && comparator.compare(lastClustering, clustering, clusteringPrefixSize) != 0)
+            {
+                isNew = true;
+            }
+
+            lastClustering = clustering;
+            return isNew;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/db/aggregation/GroupingState.java b/src/java/org/apache/cassandra/db/aggregation/GroupingState.java
new file mode 100644
index 0000000..ba5ae28
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/aggregation/GroupingState.java
@@ -0,0 +1,142 @@
+/*
+ * 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.db.aggregation;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.ClusteringComparator;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+/**
+ * {@code GroupMaker} state.
+ *
+ * <p>The state contains the primary key of the last row that has been processed by the previous
+ * {@code GroupMaker} used. It can be passed to a {@code GroupMaker} to allow to resuming the grouping.
+ * </p>
+ * <p>
+ * {@code GroupingState} is only used for internal paging. When a new page is requested by a client the initial state
+ * will always be empty.</p>
+ * <p>
+ * If the state has a partition key but no clustering, it means that the previous group ended at the end of the
+ * previous partition. If the clustering is not null it means that we are in the middle of a group.
+ * </p>
+ */
+public final class GroupingState
+{
+    public static final GroupingState.Serializer serializer = new Serializer();
+
+    public static final GroupingState EMPTY_STATE = new GroupingState(null, null);
+
+    /**
+     * The last row partition key.
+     */
+    private final ByteBuffer partitionKey;
+
+    /**
+     * The last row clustering
+     */
+    final Clustering clustering;
+
+    public GroupingState(ByteBuffer partitionKey, Clustering clustering)
+    {
+        this.partitionKey = partitionKey;
+        this.clustering = clustering;
+    }
+
+    /**
+     * Returns the last row partition key or <code>null</code> if no rows has been processed yet.
+     * @return the last row partition key or <code>null</code> if no rows has been processed yet
+     */
+    public ByteBuffer partitionKey()
+    {
+        return partitionKey;
+    }
+
+    /**
+     * Returns the last row clustering or <code>null</code> if either no rows has been processed yet or the last
+     * row was a static row.
+     * @return he last row clustering or <code>null</code> if either no rows has been processed yet or the last
+     * row was a static row
+     */
+    public Clustering clustering()
+    {
+        return clustering;
+    }
+
+    /**
+     * Checks if the state contains a Clustering for the last row that has been processed.
+     * @return <code>true</code> if the state contains a Clustering for the last row that has been processed,
+     * <code>false</code> otherwise.
+     */
+    public boolean hasClustering()
+    {
+        return clustering != null;
+    }
+
+    public static class Serializer
+    {
+        public void serialize(GroupingState state, DataOutputPlus out, int version, ClusteringComparator comparator) throws IOException
+        {
+            boolean hasPartitionKey = state.partitionKey != null;
+            out.writeBoolean(hasPartitionKey);
+            if (hasPartitionKey)
+            {
+                ByteBufferUtil.writeWithVIntLength(state.partitionKey, out);
+                boolean hasClustering = state.hasClustering();
+                out.writeBoolean(hasClustering);
+                if (hasClustering)
+                    Clustering.serializer.serialize(state.clustering, out, version, comparator.subtypes());
+            }
+        }
+
+        public GroupingState deserialize(DataInputPlus in, int version, ClusteringComparator comparator) throws IOException
+        {
+            if (!in.readBoolean())
+                return GroupingState.EMPTY_STATE;
+
+            ByteBuffer partitionKey = ByteBufferUtil.readWithVIntLength(in);
+            Clustering clustering = null;
+            if (in.readBoolean())
+                clustering = Clustering.serializer.deserialize(in, version, comparator.subtypes());
+
+            return new GroupingState(partitionKey, clustering);
+        }
+
+        public long serializedSize(GroupingState state, int version, ClusteringComparator comparator)
+        {
+            boolean hasPartitionKey = state.partitionKey != null;
+            long size = TypeSizes.sizeof(hasPartitionKey);
+            if (hasPartitionKey)
+            {
+                size += ByteBufferUtil.serializedSizeWithVIntLength(state.partitionKey);
+                boolean hasClustering = state.hasClustering();
+                size += TypeSizes.sizeof(hasClustering);
+                if (hasClustering)
+                {
+                    size += Clustering.serializer.serializedSize(state.clustering, version, comparator.subtypes());
+                }
+            }
+            return size;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/columniterator/AbstractSSTableIterator.java b/src/java/org/apache/cassandra/db/columniterator/AbstractSSTableIterator.java
index 386b2c8..4eaf8f6 100644
--- a/src/java/org/apache/cassandra/db/columniterator/AbstractSSTableIterator.java
+++ b/src/java/org/apache/cassandra/db/columniterator/AbstractSSTableIterator.java
@@ -18,23 +18,24 @@
 package org.apache.cassandra.db.columniterator;
 
 import java.io.IOException;
-import java.util.Collections;
+import java.util.Comparator;
 import java.util.Iterator;
-import java.util.List;
+import java.util.NoSuchElementException;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.io.sstable.IndexInfo;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.sstable.CorruptSSTableException;
-import org.apache.cassandra.io.sstable.IndexHelper;
 import org.apache.cassandra.io.sstable.format.Version;
 import org.apache.cassandra.io.util.FileDataInput;
 import org.apache.cassandra.io.util.DataPosition;
+import org.apache.cassandra.io.util.FileHandle;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
-abstract class AbstractSSTableIterator implements SliceableUnfilteredRowIterator
+public abstract class AbstractSSTableIterator implements UnfilteredRowIterator
 {
     protected final SSTableReader sstable;
     protected final DecoratedKey key;
@@ -47,20 +48,28 @@
 
     private final boolean isForThrift;
 
+    protected final FileHandle ifile;
+
     private boolean isClosed;
 
+    protected final Slices slices;
+
     @SuppressWarnings("resource") // We need this because the analysis is not able to determine that we do close
                                   // file on every path where we created it.
     protected AbstractSSTableIterator(SSTableReader sstable,
                                       FileDataInput file,
                                       DecoratedKey key,
                                       RowIndexEntry indexEntry,
+                                      Slices slices,
                                       ColumnFilter columnFilter,
-                                      boolean isForThrift)
+                                      boolean isForThrift,
+                                      FileHandle ifile)
     {
         this.sstable = sstable;
+        this.ifile = ifile;
         this.key = key;
         this.columns = columnFilter;
+        this.slices = slices;
         this.helper = new SerializationHelper(sstable.metadata, sstable.descriptor.version.correspondingMessagingVersion(), SerializationHelper.Flag.LOCAL, columnFilter);
         this.isForThrift = isForThrift;
 
@@ -110,6 +119,9 @@
                     this.reader = needsReader ? createReader(indexEntry, file, shouldCloseFile) : null;
                 }
 
+                if (reader != null && !slices.isEmpty())
+                    reader.setForSlice(nextSlice());
+
                 if (reader == null && file != null && shouldCloseFile)
                     file.close();
             }
@@ -133,6 +145,23 @@
         }
     }
 
+    private Slice nextSlice()
+    {
+        return slices.get(nextSliceIndex());
+    }
+
+    /**
+     * Returns the index of the next slice to process.
+     * @return the index of the next slice to process
+     */
+    protected abstract int nextSliceIndex();
+
+    /**
+     * Checks if there are more slice to process.
+     * @return {@code true} if there are more slice to process, {@code false} otherwise.
+     */
+    protected abstract boolean hasMoreSlices();
+
     private static Row readStaticRow(SSTableReader sstable,
                                      FileDataInput file,
                                      SerializationHelper helper,
@@ -181,7 +210,13 @@
         }
     }
 
-    protected abstract Reader createReader(RowIndexEntry indexEntry, FileDataInput file, boolean shouldCloseFile);
+    protected abstract Reader createReaderInternal(RowIndexEntry indexEntry, FileDataInput file, boolean shouldCloseFile);
+
+    private Reader createReader(RowIndexEntry indexEntry, FileDataInput file, boolean shouldCloseFile)
+    {
+        return slices.isEmpty() ? new NoRowsReader(file, shouldCloseFile)
+                                : createReaderInternal(indexEntry, file, shouldCloseFile);
+    };
 
     public CFMetaData metadata()
     {
@@ -210,14 +245,24 @@
 
     public EncodingStats stats()
     {
-        // We could return sstable.header.stats(), but this may not be as accurate than the actual sstable stats (see
-        // SerializationHeader.make() for details) so we use the latter instead.
-        return new EncodingStats(sstable.getMinTimestamp(), sstable.getMinLocalDeletionTime(), sstable.getMinTTL());
+        return sstable.stats();
     }
 
     public boolean hasNext()
     {
-        return reader != null && reader.hasNext();
+        while (true)
+        {
+            if (reader == null)
+                return false;
+
+            if (reader.hasNext())
+                return true;
+
+            if (!hasMoreSlices())
+                return false;
+
+            slice(nextSlice());
+        }
     }
 
     public Unfiltered next()
@@ -226,15 +271,12 @@
         return reader.next();
     }
 
-    public Iterator<Unfiltered> slice(Slice slice)
+    private void slice(Slice slice)
     {
         try
         {
-            if (reader == null)
-                return Collections.emptyIterator();
-
-            reader.setForSlice(slice);
-            return reader;
+            if (reader != null)
+                reader.setForSlice(slice);
         }
         catch (IOException e)
         {
@@ -329,7 +371,7 @@
             openMarker = marker.isOpen(false) ? marker.openDeletionTime(false) : null;
         }
 
-        public boolean hasNext() 
+        public boolean hasNext()
         {
             try
             {
@@ -384,14 +426,38 @@
         }
     }
 
+    // Reader for when we have Slices.NONE but need to read static row or partition level deletion
+    private class NoRowsReader extends AbstractSSTableIterator.Reader
+    {
+        private NoRowsReader(FileDataInput file, boolean shouldCloseFile)
+        {
+            super(file, shouldCloseFile);
+        }
+
+        public void setForSlice(Slice slice) throws IOException
+        {
+            return;
+        }
+
+        protected boolean hasNextInternal() throws IOException
+        {
+            return false;
+        }
+
+        protected Unfiltered nextInternal() throws IOException
+        {
+            throw new NoSuchElementException();
+        }
+    }
+
     // Used by indexed readers to store where they are of the index.
-    protected static class IndexState
+    public static class IndexState implements AutoCloseable
     {
         private final Reader reader;
         private final ClusteringComparator comparator;
 
         private final RowIndexEntry indexEntry;
-        private final List<IndexHelper.IndexInfo> indexes;
+        private final RowIndexEntry.IndexInfoRetriever indexInfoRetriever;
         private final boolean reversed;
 
         private int currentIndexIdx;
@@ -399,25 +465,25 @@
         // Marks the beginning of the block corresponding to currentIndexIdx.
         private DataPosition mark;
 
-        public IndexState(Reader reader, ClusteringComparator comparator, RowIndexEntry indexEntry, boolean reversed)
+        public IndexState(Reader reader, ClusteringComparator comparator, RowIndexEntry indexEntry, boolean reversed, FileHandle indexFile)
         {
             this.reader = reader;
             this.comparator = comparator;
             this.indexEntry = indexEntry;
-            this.indexes = indexEntry.columnsIndex();
+            this.indexInfoRetriever = indexEntry.openWithIndex(indexFile);
             this.reversed = reversed;
-            this.currentIndexIdx = reversed ? indexEntry.columnsIndex().size() : -1;
+            this.currentIndexIdx = reversed ? indexEntry.columnsIndexCount() : -1;
         }
 
         public boolean isDone()
         {
-            return reversed ? currentIndexIdx < 0 : currentIndexIdx >= indexes.size();
+            return reversed ? currentIndexIdx < 0 : currentIndexIdx >= indexEntry.columnsIndexCount();
         }
 
         // Sets the reader to the beginning of blockIdx.
         public void setToBlock(int blockIdx) throws IOException
         {
-            if (blockIdx >= 0 && blockIdx < indexes.size())
+            if (blockIdx >= 0 && blockIdx < indexEntry.columnsIndexCount())
             {
                 reader.seekToPosition(columnOffset(blockIdx));
                 mark = reader.file.mark();
@@ -425,7 +491,7 @@
             }
 
             currentIndexIdx = blockIdx;
-            reader.openMarker = blockIdx > 0 ? indexes.get(blockIdx - 1).endOpenMarker : null;
+            reader.openMarker = blockIdx > 0 ? index(blockIdx - 1).endOpenMarker : null;
 
             // If we're reading an old format file and we move to the first block in the index (i.e. the
             // head of the partition), we skip the static row as it's already been read when we first opened
@@ -441,14 +507,14 @@
             }
         }
 
-        private long columnOffset(int i)
+        private long columnOffset(int i) throws IOException
         {
-            return indexEntry.position + indexes.get(i).offset;
+            return indexEntry.position + index(i).offset;
         }
 
         public int blocksCount()
         {
-            return indexes.size();
+            return indexEntry.columnsIndexCount();
         }
 
         // Update the block idx based on the current reader position if we're past the current block.
@@ -467,7 +533,7 @@
                 return;
             }
 
-            while (currentIndexIdx + 1 < indexes.size() && isPastCurrentBlock())
+            while (currentIndexIdx + 1 < indexEntry.columnsIndexCount() && isPastCurrentBlock())
             {
                 reader.openMarker = currentIndex().endOpenMarker;
                 ++currentIndexIdx;
@@ -490,7 +556,7 @@
         }
 
         // Check if we've crossed an index boundary (based on the mark on the beginning of the index block).
-        public boolean isPastCurrentBlock()
+        public boolean isPastCurrentBlock() throws IOException
         {
             assert reader.deserializer != null;
             long correction = reader.deserializer.bytesReadForUnconsumedData();
@@ -502,32 +568,94 @@
             return currentIndexIdx;
         }
 
-        public IndexHelper.IndexInfo currentIndex()
+        public IndexInfo currentIndex() throws IOException
         {
             return index(currentIndexIdx);
         }
 
-        public IndexHelper.IndexInfo index(int i)
+        public IndexInfo index(int i) throws IOException
         {
-            return indexes.get(i);
+            return indexInfoRetriever.columnsIndex(i);
         }
 
         // Finds the index of the first block containing the provided bound, starting at the provided index.
         // Will be -1 if the bound is before any block, and blocksCount() if it is after every block.
-        public int findBlockIndex(Slice.Bound bound, int fromIdx)
+        public int findBlockIndex(ClusteringBound bound, int fromIdx) throws IOException
         {
-            if (bound == Slice.Bound.BOTTOM)
+            if (bound == ClusteringBound.BOTTOM)
                 return -1;
-            if (bound == Slice.Bound.TOP)
+            if (bound == ClusteringBound.TOP)
                 return blocksCount();
 
-            return IndexHelper.indexFor(bound, indexes, comparator, reversed, fromIdx);
+            return indexFor(bound, fromIdx);
+        }
+
+        public int indexFor(ClusteringPrefix name, int lastIndex) throws IOException
+        {
+            IndexInfo target = new IndexInfo(name, name, 0, 0, null);
+            /*
+            Take the example from the unit test, and say your index looks like this:
+            [0..5][10..15][20..25]
+            and you look for the slice [13..17].
+
+            When doing forward slice, we are doing a binary search comparing 13 (the start of the query)
+            to the lastName part of the index slot. You'll end up with the "first" slot, going from left to right,
+            that may contain the start.
+
+            When doing a reverse slice, we do the same thing, only using as a start column the end of the query,
+            i.e. 17 in this example, compared to the firstName part of the index slots.  bsearch will give us the
+            first slot where firstName > start ([20..25] here), so we subtract an extra one to get the slot just before.
+            */
+            int startIdx = 0;
+            int endIdx = indexEntry.columnsIndexCount() - 1;
+
+            if (reversed)
+            {
+                if (lastIndex < endIdx)
+                {
+                    endIdx = lastIndex;
+                }
+            }
+            else
+            {
+                if (lastIndex > 0)
+                {
+                    startIdx = lastIndex;
+                }
+            }
+
+            int index = binarySearch(target, comparator.indexComparator(reversed), startIdx, endIdx);
+            return (index < 0 ? -index - (reversed ? 2 : 1) : index);
+        }
+
+        private int binarySearch(IndexInfo key, Comparator<IndexInfo> c, int low, int high) throws IOException
+        {
+            while (low <= high)
+            {
+                int mid = (low + high) >>> 1;
+                IndexInfo midVal = index(mid);
+                int cmp = c.compare(midVal, key);
+
+                if (cmp < 0)
+                    low = mid + 1;
+                else if (cmp > 0)
+                    high = mid - 1;
+                else
+                    return mid;
+            }
+            return -(low + 1);
         }
 
         @Override
         public String toString()
         {
-            return String.format("IndexState(indexSize=%d, currentBlock=%d, reversed=%b)", indexes.size(), currentIndexIdx, reversed);
+            return String.format("IndexState(indexSize=%d, currentBlock=%d, reversed=%b)", indexEntry.columnsIndexCount(), currentIndexIdx, reversed);
+        }
+
+        @Override
+        public void close() throws IOException
+        {
+            indexInfoRetriever.close();
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/db/columniterator/SSTableIterator.java b/src/java/org/apache/cassandra/db/columniterator/SSTableIterator.java
index 47f85ac..e33c748 100644
--- a/src/java/org/apache/cassandra/db/columniterator/SSTableIterator.java
+++ b/src/java/org/apache/cassandra/db/columniterator/SSTableIterator.java
@@ -24,40 +24,50 @@
 import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
-import org.apache.cassandra.io.sstable.format.SSTableReadsListener;
 import org.apache.cassandra.io.util.FileDataInput;
+import org.apache.cassandra.io.util.FileHandle;
 
 /**
  *  A Cell Iterator over SSTable
  */
 public class SSTableIterator extends AbstractSSTableIterator
 {
-    public SSTableIterator(SSTableReader sstable,
-                           DecoratedKey key,
-                           ColumnFilter columns,
-                           boolean isForThrift,
-                           SSTableReadsListener listener)
-    {
-        this(sstable, null, key, sstable.getPosition(key, SSTableReader.Operator.EQ, listener), columns, isForThrift);
-    }
+    /**
+     * The index of the slice being processed.
+     */
+    private int slice;
 
     public SSTableIterator(SSTableReader sstable,
                            FileDataInput file,
                            DecoratedKey key,
                            RowIndexEntry indexEntry,
+                           Slices slices,
                            ColumnFilter columns,
-                           boolean isForThrift)
+                           boolean isForThrift,
+                           FileHandle ifile)
     {
-        super(sstable, file, key, indexEntry, columns, isForThrift);
+        super(sstable, file, key, indexEntry, slices, columns, isForThrift, ifile);
     }
 
-    protected Reader createReader(RowIndexEntry indexEntry, FileDataInput file, boolean shouldCloseFile)
+    protected Reader createReaderInternal(RowIndexEntry indexEntry, FileDataInput file, boolean shouldCloseFile)
     {
         return indexEntry.isIndexed()
              ? new ForwardIndexedReader(indexEntry, file, shouldCloseFile)
              : new ForwardReader(file, shouldCloseFile);
     }
 
+    protected int nextSliceIndex()
+    {
+        int next = slice;
+        slice++;
+        return next;
+    }
+
+    protected boolean hasMoreSlices()
+    {
+        return slice < slices.size();
+    }
+
     public boolean isReverseOrder()
     {
         return false;
@@ -66,9 +76,9 @@
     private class ForwardReader extends Reader
     {
         // The start of the current slice. This will be null as soon as we know we've passed that bound.
-        protected Slice.Bound start;
+        protected ClusteringBound start;
         // The end of the current slice. Will never be null.
-        protected Slice.Bound end = Slice.Bound.TOP;
+        protected ClusteringBound end = ClusteringBound.TOP;
 
         protected Unfiltered next; // the next element to return: this is computed by hasNextInternal().
 
@@ -82,7 +92,7 @@
 
         public void setForSlice(Slice slice) throws IOException
         {
-            start = slice.start() == Slice.Bound.BOTTOM ? null : slice.start();
+            start = slice.start() == ClusteringBound.BOTTOM ? null : slice.start();
             end = slice.end();
 
             sliceDone = false;
@@ -110,7 +120,7 @@
                     updateOpenMarker((RangeTombstoneMarker)deserializer.readNext());
             }
 
-            Slice.Bound sliceStart = start;
+            ClusteringBound sliceStart = start;
             start = null;
 
             // We've reached the beginning of our queried slice. If we have an open marker
@@ -204,11 +214,18 @@
         private ForwardIndexedReader(RowIndexEntry indexEntry, FileDataInput file, boolean shouldCloseFile)
         {
             super(file, shouldCloseFile);
-            this.indexState = new IndexState(this, sstable.metadata.comparator, indexEntry, false);
+            this.indexState = new IndexState(this, sstable.metadata.comparator, indexEntry, false, ifile);
             this.lastBlockIdx = indexState.blocksCount(); // if we never call setForSlice, that's where we want to stop
         }
 
         @Override
+        public void close() throws IOException
+        {
+            super.close();
+            this.indexState.close();
+        }
+
+        @Override
         public void setForSlice(Slice slice) throws IOException
         {
             super.setForSlice(slice);
diff --git a/src/java/org/apache/cassandra/db/columniterator/SSTableReversedIterator.java b/src/java/org/apache/cassandra/db/columniterator/SSTableReversedIterator.java
index 8d3f4f3..23835ee 100644
--- a/src/java/org/apache/cassandra/db/columniterator/SSTableReversedIterator.java
+++ b/src/java/org/apache/cassandra/db/columniterator/SSTableReversedIterator.java
@@ -28,8 +28,8 @@
 import org.apache.cassandra.db.partitions.ImmutableBTreePartition;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
-import org.apache.cassandra.io.sstable.format.SSTableReadsListener;
 import org.apache.cassandra.io.util.FileDataInput;
+import org.apache.cassandra.io.util.FileHandle;
 import org.apache.cassandra.utils.AbstractIterator;
 import org.apache.cassandra.utils.btree.BTree;
 
@@ -38,26 +38,24 @@
  */
 public class SSTableReversedIterator extends AbstractSSTableIterator
 {
-    public SSTableReversedIterator(SSTableReader sstable,
-                                   DecoratedKey key,
-                                   ColumnFilter columns,
-                                   boolean isForThrift,
-                                   SSTableReadsListener listener)
-    {
-        this(sstable, null, key, sstable.getPosition(key, SSTableReader.Operator.EQ, listener), columns, isForThrift);
-    }
+    /**
+     * The index of the slice being processed.
+     */
+    private int slice;
 
     public SSTableReversedIterator(SSTableReader sstable,
                                    FileDataInput file,
                                    DecoratedKey key,
                                    RowIndexEntry indexEntry,
+                                   Slices slices,
                                    ColumnFilter columns,
-                                   boolean isForThrift)
+                                   boolean isForThrift,
+                                   FileHandle ifile)
     {
-        super(sstable, file, key, indexEntry, columns, isForThrift);
+        super(sstable, file, key, indexEntry, slices, columns, isForThrift, ifile);
     }
 
-    protected Reader createReader(RowIndexEntry indexEntry, FileDataInput file, boolean shouldCloseFile)
+    protected Reader createReaderInternal(RowIndexEntry indexEntry, FileDataInput file, boolean shouldCloseFile)
     {
         return indexEntry.isIndexed()
              ? new ReverseIndexedReader(indexEntry, file, shouldCloseFile)
@@ -69,6 +67,18 @@
         return true;
     }
 
+    protected int nextSliceIndex()
+    {
+        int next = slice;
+        slice++;
+        return slices.size() - (next + 1);
+    }
+
+    protected boolean hasMoreSlices()
+    {
+        return slice < slices.size();
+    }
+
     private class ReverseReader extends Reader
     {
         protected ReusablePartitionData buffer;
@@ -162,7 +172,7 @@
             return next;
         }
 
-        protected boolean stopReadingDisk()
+        protected boolean stopReadingDisk() throws IOException
         {
             return false;
         }
@@ -175,8 +185,8 @@
 
         // Reads the unfiltered from disk and load them into the reader buffer. It stops reading when either the partition
         // is fully read, or when stopReadingDisk() returns true.
-        protected void loadFromDisk(Slice.Bound start,
-                                    Slice.Bound end,
+        protected void loadFromDisk(ClusteringBound start,
+                                    ClusteringBound end,
                                     boolean hasPreviousBlock,
                                     boolean hasNextBlock,
                                     ClusteringPrefix currentFirstName,
@@ -217,7 +227,7 @@
                 // want to "return" it just yet, we'll wait until we reach it in the next blocks. That's why we trigger
                 // skipLastIteratedItem in that case (this is first item of the block, but we're iterating in reverse order
                 // so it will be last returned by the iterator).
-                RangeTombstone.Bound markerStart = start == null ? RangeTombstone.Bound.BOTTOM : RangeTombstone.Bound.fromSliceBound(start);
+                ClusteringBound markerStart = start == null ? ClusteringBound.BOTTOM : start;
                 buffer.add(new RangeTombstoneBoundMarker(markerStart, openMarker));
                 if (hasNextBlock)
                     skipLastIteratedItem = true;
@@ -318,7 +328,7 @@
                 // not breaking ImmutableBTreePartition, we should skip it when returning from the iterator, hence the
                 // skipFirstIteratedItem (this is the last item of the block, but we're iterating in reverse order so it will
                 // be the first returned by the iterator).
-                RangeTombstone.Bound markerEnd = end == null ? RangeTombstone.Bound.TOP : RangeTombstone.Bound.fromSliceBound(end);
+                ClusteringBound markerEnd = end == null ? ClusteringBound.TOP : end;
                 buffer.add(new RangeTombstoneBoundMarker(markerEnd, openMarker));
                 if (hasPreviousBlock)
                     skipFirstIteratedItem = true;
@@ -340,7 +350,14 @@
         private ReverseIndexedReader(RowIndexEntry indexEntry, FileDataInput file, boolean shouldCloseFile)
         {
             super(file, shouldCloseFile);
-            this.indexState = new IndexState(this, sstable.metadata.comparator, indexEntry, true);
+            this.indexState = new IndexState(this, sstable.metadata.comparator, indexEntry, true, ifile);
+        }
+
+        @Override
+        public void close() throws IOException
+        {
+            super.close();
+            this.indexState.close();
         }
 
         @Override
@@ -457,7 +474,7 @@
         }
 
         @Override
-        protected boolean stopReadingDisk()
+        protected boolean stopReadingDisk() throws IOException
         {
             return indexState.isPastCurrentBlock();
         }
diff --git a/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogSegmentManager.java b/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogSegmentManager.java
new file mode 100755
index 0000000..18b4374
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogSegmentManager.java
@@ -0,0 +1,565 @@
+/*
+ * 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.db.commitlog;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BooleanSupplier;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.nicoulaj.compilecommand.annotations.DontInline;
+import org.apache.cassandra.concurrent.NamedThreadFactory;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.io.util.SimpleCachedBufferPool;
+import org.apache.cassandra.utils.*;
+import org.apache.cassandra.utils.concurrent.WaitQueue;
+
+import static org.apache.cassandra.db.commitlog.CommitLogSegment.Allocation;
+
+/**
+ * Performs eager-creation of commit log segments in a background thread. All the
+ * public methods are thread safe.
+ */
+public abstract class AbstractCommitLogSegmentManager
+{
+    static final Logger logger = LoggerFactory.getLogger(AbstractCommitLogSegmentManager.class);
+
+    /**
+     * Segment that is ready to be used. The management thread fills this and blocks until consumed.
+     *
+     * A single management thread produces this, and consumers are already synchronizing to make sure other work is
+     * performed atomically with consuming this. Volatile to make sure writes by the management thread become
+     * visible (ordered/lazySet would suffice). Consumers (advanceAllocatingFrom and discardAvailableSegment) must
+     * synchronize on 'this'.
+     */
+    private volatile CommitLogSegment availableSegment = null;
+
+    private final WaitQueue segmentPrepared = new WaitQueue();
+
+    /** Active segments, containing unflushed data. The tail of this queue is the one we allocate writes to */
+    private final ConcurrentLinkedQueue<CommitLogSegment> activeSegments = new ConcurrentLinkedQueue<>();
+
+    /**
+     * The segment we are currently allocating commit log records to.
+     *
+     * Written by advanceAllocatingFrom which synchronizes on 'this'. Volatile to ensure reads get current value.
+     */
+    private volatile CommitLogSegment allocatingFrom = null;
+
+    final String storageDirectory;
+
+    /**
+     * Tracks commitlog size, in multiples of the segment size.  We need to do this so we can "promise" size
+     * adjustments ahead of actually adding/freeing segments on disk, so that the "evict oldest segment" logic
+     * can see the effect of recycling segments immediately (even though they're really happening asynchronously
+     * on the manager thread, which will take a ms or two).
+     */
+    private final AtomicLong size = new AtomicLong();
+
+    private Thread managerThread;
+    protected final CommitLog commitLog;
+    private volatile boolean shutdown;
+    private final BooleanSupplier managerThreadWaitCondition = () -> (availableSegment == null && !atSegmentBufferLimit()) || shutdown;
+    private final WaitQueue managerThreadWaitQueue = new WaitQueue();
+
+    private volatile SimpleCachedBufferPool bufferPool;
+
+    AbstractCommitLogSegmentManager(final CommitLog commitLog, String storageDirectory)
+    {
+        this.commitLog = commitLog;
+        this.storageDirectory = storageDirectory;
+    }
+
+    void start()
+    {
+        // The run loop for the manager thread
+        Runnable runnable = new WrappedRunnable()
+        {
+            public void runMayThrow() throws Exception
+            {
+                while (!shutdown)
+                {
+                    try
+                    {
+                        assert availableSegment == null;
+                        logger.trace("No segments in reserve; creating a fresh one");
+                        availableSegment = createSegment();
+                        if (shutdown)
+                        {
+                            // If shutdown() started and finished during segment creation, we are now left with a
+                            // segment that no one will consume. Discard it.
+                            discardAvailableSegment();
+                            return;
+                        }
+
+                        segmentPrepared.signalAll();
+                        Thread.yield();
+
+                        if (availableSegment == null && !atSegmentBufferLimit())
+                            // Writing threads need another segment now.
+                            continue;
+
+                        // Writing threads are not waiting for new segments, we can spend time on other tasks.
+                        // flush old Cfs if we're full
+                        maybeFlushToReclaim();
+                    }
+                    catch (Throwable t)
+                    {
+                        if (!CommitLog.handleCommitError("Failed managing commit log segments", t))
+                            return;
+                        // sleep some arbitrary period to avoid spamming CL
+                        Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
+
+                        // If we offered a segment, wait for it to be taken before reentering the loop.
+                        // There could be a new segment in next not offered, but only on failure to discard it while
+                        // shutting down-- nothing more can or needs to be done in that case.
+                    }
+
+                    WaitQueue.waitOnCondition(managerThreadWaitCondition, managerThreadWaitQueue);
+                }
+            }
+        };
+
+        // For encrypted segments we want to keep the compression buffers on-heap as we need those bytes for encryption,
+        // and we want to avoid copying from off-heap (compression buffer) to on-heap encryption APIs
+        BufferType bufferType = commitLog.configuration.useEncryption() || !commitLog.configuration.useCompression()
+                              ? BufferType.ON_HEAP
+                              : commitLog.configuration.getCompressor().preferredBufferType();
+
+        this.bufferPool = new SimpleCachedBufferPool(DatabaseDescriptor.getCommitLogMaxCompressionBuffersInPool(),
+                                                     DatabaseDescriptor.getCommitLogSegmentSize(),
+                                                     bufferType);
+
+        shutdown = false;
+        managerThread = NamedThreadFactory.createThread(runnable, "COMMIT-LOG-ALLOCATOR");
+        managerThread.start();
+
+        // for simplicity, ensure the first segment is allocated before continuing
+        advanceAllocatingFrom(null);
+    }
+
+    private boolean atSegmentBufferLimit()
+    {
+        return CommitLogSegment.usesBufferPool(commitLog) && bufferPool.atLimit();
+    }
+
+    private void maybeFlushToReclaim()
+    {
+        long unused = unusedCapacity();
+        if (unused < 0)
+        {
+            long flushingSize = 0;
+            List<CommitLogSegment> segmentsToRecycle = new ArrayList<>();
+            for (CommitLogSegment segment : activeSegments)
+            {
+                if (segment == allocatingFrom)
+                    break;
+                flushingSize += segment.onDiskSize();
+                segmentsToRecycle.add(segment);
+                if (flushingSize + unused >= 0)
+                    break;
+            }
+            flushDataFrom(segmentsToRecycle, false);
+        }
+    }
+
+
+    /**
+     * Allocate a segment within this CLSM. Should either succeed or throw.
+     */
+    public abstract Allocation allocate(Mutation mutation, int size);
+
+    /**
+     * The recovery and replay process replays mutations into memtables and flushes them to disk. Individual CLSM
+     * decide what to do with those segments on disk after they've been replayed.
+     */
+    abstract void handleReplayedSegment(final File file);
+
+    /**
+     * Hook to allow segment managers to track state surrounding creation of new segments. Onl perform as task submit
+     * to segment manager so it's performed on segment management thread.
+     */
+    abstract CommitLogSegment createSegment();
+
+    /**
+     * Indicates that a segment file has been flushed and is no longer needed. Only perform as task submit to segment
+     * manager so it's performend on segment management thread, or perform while segment management thread is shutdown
+     * during testing resets.
+     *
+     * @param segment segment to be discarded
+     * @param delete  whether or not the segment is safe to be deleted.
+     */
+    abstract void discard(CommitLogSegment segment, boolean delete);
+
+    /**
+     * Advances the allocatingFrom pointer to the next prepared segment, but only if it is currently the segment provided.
+     *
+     * WARNING: Assumes segment management thread always succeeds in allocating a new segment or kills the JVM.
+     */
+    @DontInline
+    void advanceAllocatingFrom(CommitLogSegment old)
+    {
+        while (true)
+        {
+            synchronized (this)
+            {
+                // do this in a critical section so we can maintain the order of segment construction when moving to allocatingFrom/activeSegments
+                if (allocatingFrom != old)
+                    return;
+
+                // If a segment is ready, take it now, otherwise wait for the management thread to construct it.
+                if (availableSegment != null)
+                {
+                    // Success! Change allocatingFrom and activeSegments (which must be kept in order) before leaving
+                    // the critical section.
+                    activeSegments.add(allocatingFrom = availableSegment);
+                    availableSegment = null;
+                    break;
+                }
+            }
+
+            awaitAvailableSegment(old);
+        }
+
+        // Signal the management thread to prepare a new segment.
+        wakeManager();
+
+        if (old != null)
+        {
+            // Now we can run the user defined command just after switching to the new commit log.
+            // (Do this here instead of in the recycle call so we can get a head start on the archive.)
+            commitLog.archiver.maybeArchive(old);
+
+            // ensure we don't continue to use the old file; not strictly necessary, but cleaner to enforce it
+            old.discardUnusedTail();
+        }
+
+        // request that the CL be synced out-of-band, as we've finished a segment
+        commitLog.requestExtraSync();
+    }
+
+    void awaitAvailableSegment(CommitLogSegment currentAllocatingFrom)
+    {
+        do
+        {
+            WaitQueue.Signal prepared = segmentPrepared.register(commitLog.metrics.waitingOnSegmentAllocation.time());
+            if (availableSegment == null && allocatingFrom == currentAllocatingFrom)
+                prepared.awaitUninterruptibly();
+            else
+                prepared.cancel();
+        }
+        while (availableSegment == null && allocatingFrom == currentAllocatingFrom);
+    }
+
+    /**
+     * Switch to a new segment, regardless of how much is left in the current one.
+     *
+     * Flushes any dirty CFs for this segment and any older segments, and then discards the segments.
+     * This is necessary to avoid resurrecting data during replay if a user creates a new table with
+     * the same name and ID. See CASSANDRA-16986 for more details.
+     */
+    void forceRecycleAll(Iterable<UUID> droppedCfs)
+    {
+        List<CommitLogSegment> segmentsToRecycle = new ArrayList<>(activeSegments);
+        CommitLogSegment last = segmentsToRecycle.get(segmentsToRecycle.size() - 1);
+        advanceAllocatingFrom(last);
+
+        // wait for the commit log modifications
+        last.waitForModifications();
+
+        // make sure the writes have materialized inside of the memtables by waiting for all outstanding writes
+        // to complete
+        Keyspace.writeOrder.awaitNewBarrier();
+
+        // flush and wait for all CFs that are dirty in segments up-to and including 'last'
+        Future<?> future = flushDataFrom(segmentsToRecycle, true);
+        try
+        {
+            future.get();
+
+            for (CommitLogSegment segment : activeSegments)
+                for (UUID cfId : droppedCfs)
+                    segment.markClean(cfId, CommitLogPosition.NONE, segment.getCurrentCommitLogPosition());
+
+            // now recycle segments that are unused, as we may not have triggered a discardCompletedSegments()
+            // if the previous active segment was the only one to recycle (since an active segment isn't
+            // necessarily dirty, and we only call dCS after a flush).
+            for (CommitLogSegment segment : activeSegments)
+            {
+                if (segment.isUnused())
+                    archiveAndDiscard(segment);
+            }
+
+            CommitLogSegment first;
+            if ((first = activeSegments.peek()) != null && first.id <= last.id)
+                logger.error("Failed to force-recycle all segments; at least one segment is still in use with dirty CFs.");
+        }
+        catch (Throwable t)
+        {
+            // for now just log the error
+            logger.error("Failed waiting for a forced recycle of in-use commit log segments", t);
+        }
+    }
+
+    /**
+     * Indicates that a segment is no longer in use and that it should be discarded.
+     *
+     * @param segment segment that is no longer in use
+     */
+    void archiveAndDiscard(final CommitLogSegment segment)
+    {
+        boolean archiveSuccess = commitLog.archiver.maybeWaitForArchiving(segment.getName());
+        if (!activeSegments.remove(segment))
+            return; // already discarded
+        // if archiving (command) was not successful then leave the file alone. don't delete or recycle.
+        logger.debug("Segment {} is no longer active and will be deleted {}", segment, archiveSuccess ? "now" : "by the archive script");
+        discard(segment, archiveSuccess);
+    }
+
+    /**
+     * Adjust the tracked on-disk size. Called by individual segments to reflect writes, allocations and discards.
+     * @param addedSize
+     */
+    void addSize(long addedSize)
+    {
+        size.addAndGet(addedSize);
+    }
+
+    /**
+     * @return the space (in bytes) used by all segment files.
+     */
+    public long onDiskSize()
+    {
+        return size.get();
+    }
+
+    private long unusedCapacity()
+    {
+        long total = DatabaseDescriptor.getTotalCommitlogSpaceInMB() * 1024 * 1024;
+        long currentSize = size.get();
+        logger.trace("Total active commitlog segment space used is {} out of {}", currentSize, total);
+        return total - currentSize;
+    }
+
+    /**
+     * Force a flush on all CFs that are still dirty in @param segments.
+     *
+     * @return a Future that will finish when all the flushes are complete.
+     */
+    private Future<?> flushDataFrom(List<CommitLogSegment> segments, boolean force)
+    {
+        if (segments.isEmpty())
+            return Futures.immediateFuture(null);
+        final CommitLogPosition maxCommitLogPosition = segments.get(segments.size() - 1).getCurrentCommitLogPosition();
+
+        // a map of CfId -> forceFlush() to ensure we only queue one flush per cf
+        final Map<UUID, ListenableFuture<?>> flushes = new LinkedHashMap<>();
+
+        for (CommitLogSegment segment : segments)
+        {
+            for (UUID dirtyCFId : segment.getDirtyCFIDs())
+            {
+                Pair<String,String> pair = Schema.instance.getCF(dirtyCFId);
+                if (pair == null)
+                {
+                    // even though we remove the schema entry before a final flush when dropping a CF,
+                    // it's still possible for a writer to race and finish his append after the flush.
+                    logger.trace("Marking clean CF {} that doesn't exist anymore", dirtyCFId);
+                    segment.markClean(dirtyCFId, CommitLogPosition.NONE, segment.getCurrentCommitLogPosition());
+                }
+                else if (!flushes.containsKey(dirtyCFId))
+                {
+                    String keyspace = pair.left;
+                    final ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(dirtyCFId);
+                    // can safely call forceFlush here as we will only ever block (briefly) for other attempts to flush,
+                    // no deadlock possibility since switchLock removal
+                    flushes.put(dirtyCFId, force ? cfs.forceFlush() : cfs.forceFlush(maxCommitLogPosition));
+                }
+            }
+        }
+
+        return Futures.allAsList(flushes.values());
+    }
+
+    /**
+     * Stops CL, for testing purposes. DO NOT USE THIS OUTSIDE OF TESTS.
+     * Only call this after the AbstractCommitLogService is shut down.
+     */
+    public void stopUnsafe(boolean deleteSegments)
+    {
+        logger.debug("CLSM closing and clearing existing commit log segments...");
+
+        shutdown();
+        try
+        {
+            awaitTermination();
+        }
+        catch (InterruptedException e)
+        {
+            throw new RuntimeException(e);
+        }
+
+        for (CommitLogSegment segment : activeSegments)
+            closeAndDeleteSegmentUnsafe(segment, deleteSegments);
+        activeSegments.clear();
+
+        size.set(0L);
+
+        logger.trace("CLSM done with closing and clearing existing commit log segments.");
+    }
+
+    /**
+     * To be used by tests only. Not safe if mutation slots are being allocated concurrently.
+     */
+    void awaitManagementTasksCompletion()
+    {
+        if (availableSegment == null && !atSegmentBufferLimit())
+        {
+            awaitAvailableSegment(allocatingFrom);
+        }
+    }
+
+    /**
+     * Explicitly for use only during resets in unit testing.
+     */
+    private void closeAndDeleteSegmentUnsafe(CommitLogSegment segment, boolean delete)
+    {
+        try
+        {
+            discard(segment, delete);
+        }
+        catch (AssertionError ignored)
+        {
+            // segment file does not exist
+        }
+    }
+
+    /**
+     * Initiates the shutdown process for the management thread.
+     */
+    public void shutdown()
+    {
+        assert !shutdown;
+        shutdown = true;
+
+        // Release the management thread and delete prepared segment.
+        // Do not block as another thread may claim the segment (this can happen during unit test initialization).
+        discardAvailableSegment();
+        wakeManager();
+    }
+
+    private void discardAvailableSegment()
+    {
+        CommitLogSegment next = null;
+        synchronized (this)
+        {
+            next = availableSegment;
+            availableSegment = null;
+        }
+        if (next != null)
+            next.discard(true);
+    }
+
+    /**
+     * Returns when the management thread terminates.
+     */
+    public void awaitTermination() throws InterruptedException
+    {
+        managerThread.join();
+        managerThread = null;
+
+        for (CommitLogSegment segment : activeSegments)
+            segment.close();
+
+        if (bufferPool != null)
+            bufferPool.emptyBufferPool();
+    }
+
+    /**
+     * @return a read-only collection of the active commit log segments
+     */
+    @VisibleForTesting
+    public Collection<CommitLogSegment> getActiveSegments()
+    {
+        return Collections.unmodifiableCollection(activeSegments);
+    }
+
+    /**
+     * @return the current CommitLogPosition of the active segment we're allocating from
+     */
+    CommitLogPosition getCurrentPosition()
+    {
+        return allocatingFrom.getCurrentCommitLogPosition();
+    }
+
+    /**
+     * Requests commit log files sync themselves, if needed. This may or may not involve flushing to disk.
+     *
+     * @param flush Request that the sync operation flush the file to disk.
+     */
+    public void sync(boolean flush) throws IOException
+    {
+        CommitLogSegment current = allocatingFrom;
+        for (CommitLogSegment segment : getActiveSegments())
+        {
+            // Do not sync segments that became active after sync started.
+            if (segment.id > current.id)
+                return;
+            segment.sync(flush);
+        }
+    }
+
+    /**
+     * Used by compressed and encrypted segments to share a buffer pool across the CLSM.
+     */
+    SimpleCachedBufferPool getBufferPool()
+    {
+        return bufferPool;
+    }
+
+    void wakeManager()
+    {
+        managerThreadWaitQueue.signalAll();
+    }
+
+    /**
+     * Called by commit log segments when a buffer is freed to wake the management thread, which may be waiting for
+     * a buffer to become available.
+     */
+    void notifyBufferFreed()
+    {
+        wakeManager();
+    }
+
+    /** Read-only access to current segment for subclasses. */
+    CommitLogSegment allocatingFrom()
+    {
+        return allocatingFrom;
+    }
+}
+
diff --git a/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogService.java b/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogService.java
index 0845bd5..b7ab705 100644
--- a/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogService.java
+++ b/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogService.java
@@ -17,19 +17,22 @@
  */
 package org.apache.cassandra.db.commitlog;
 
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.LockSupport;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.codahale.metrics.Timer.Context;
+
 import org.apache.cassandra.concurrent.NamedThreadFactory;
 import org.apache.cassandra.config.Config;
 import org.apache.cassandra.db.commitlog.CommitLogSegment.Allocation;
 import org.apache.cassandra.utils.Clock;
 import org.apache.cassandra.utils.NoSpamLogger;
 import org.apache.cassandra.utils.concurrent.WaitQueue;
-import org.slf4j.*;
-
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
-
-import com.google.common.annotations.VisibleForTesting;
 
 public abstract class AbstractCommitLogService
 {
@@ -51,7 +54,6 @@
 
     // signal that writers can wait on to be notified of a completed sync
     protected final WaitQueue syncComplete = new WaitQueue();
-    protected final Semaphore haveWork = new Semaphore(1);
 
     final CommitLog commitLog;
     private final String name;
@@ -59,13 +61,13 @@
     /**
      * The duration between syncs to disk.
      */
-    final long syncIntervalMillis;
+    final long syncIntervalNanos;
 
     /**
      * The duration between updating the chained markers in the the commit log file. This value should be
-     * 0 < {@link #markerIntervalMillis} <= {@link #syncIntervalMillis}.
+     * 0 < {@link #markerIntervalNanos} <= {@link #syncIntervalNanos}.
      */
-    final long markerIntervalMillis;
+    final long markerIntervalNanos;
 
     /**
      * A flag that callers outside of the sync thread can use to signal they want the commitlog segments
@@ -100,6 +102,7 @@
         this.commitLog = commitLog;
         this.name = name;
 
+        final long markerIntervalMillis;
         if (markHeadersFaster && syncIntervalMillis > DEFAULT_MARKER_INTERVAL_MILLIS)
         {
             markerIntervalMillis = DEFAULT_MARKER_INTERVAL_MILLIS;
@@ -118,31 +121,31 @@
         {
             markerIntervalMillis = syncIntervalMillis;
         }
-
         assert syncIntervalMillis % markerIntervalMillis == 0;
-        this.syncIntervalMillis = syncIntervalMillis;
+        this.markerIntervalNanos = TimeUnit.NANOSECONDS.convert(markerIntervalMillis, TimeUnit.MILLISECONDS);
+        this.syncIntervalNanos = TimeUnit.NANOSECONDS.convert(syncIntervalMillis, TimeUnit.MILLISECONDS);
     }
 
     // Separated into individual method to ensure relevant objects are constructed before this is started.
     void start()
     {
-        if (syncIntervalMillis < 1)
-            throw new IllegalArgumentException(String.format("Commit log flush interval must be positive: %dms",
-                                                             syncIntervalMillis));
+        if (syncIntervalNanos < 1)
+            throw new IllegalArgumentException(String.format("Commit log flush interval must be positive: %fms",
+                                                             syncIntervalNanos * 1e-6));
         shutdown = false;
         Runnable runnable = new SyncRunnable(new Clock());
-        thread = new Thread(NamedThreadFactory.threadLocalDeallocator(runnable), name);
+        thread = NamedThreadFactory.createThread(runnable, name);
         thread.start();
     }
 
     class SyncRunnable implements Runnable
     {
-        final Clock clock;
-        long firstLagAt = 0;
-        long totalSyncDuration = 0; // total time spent syncing since firstLagAt
-        long syncExceededIntervalBy = 0; // time that syncs exceeded pollInterval since firstLagAt
-        int lagCount = 0;
-        int syncCount = 0;
+        private final Clock clock;
+        private long firstLagAt = 0;
+        private long totalSyncDuration = 0; // total time spent syncing since firstLagAt
+        private long syncExceededIntervalBy = 0; // time that syncs exceeded pollInterval since firstLagAt
+        private int lagCount = 0;
+        private int syncCount = 0;
 
         SyncRunnable(Clock clock)
         {
@@ -160,19 +163,19 @@
 
         boolean sync()
         {
+            // always run once after shutdown signalled
+            boolean shutdownRequested = shutdown;
+
             try
             {
-                // always run once after shutdown signalled
-                boolean run = !shutdown;
-
                 // sync and signal
-                long pollStarted = clock.currentTimeMillis();
-                boolean flushToDisk = lastSyncedAt + syncIntervalMillis <= pollStarted || shutdown || syncRequested;
+                long pollStarted = clock.nanoTime();
+                boolean flushToDisk = lastSyncedAt + syncIntervalNanos <= pollStarted || shutdownRequested || syncRequested;
                 if (flushToDisk)
                 {
                     // in this branch, we want to flush the commit log to disk
                     syncRequested = false;
-                    commitLog.sync(shutdown, true);
+                    commitLog.sync(true);
                     lastSyncedAt = pollStarted;
                     syncComplete.signalAll();
                     syncCount++;
@@ -180,30 +183,19 @@
                 else
                 {
                     // in this branch, just update the commit log sync headers
-                    commitLog.sync(false, false);
+                    commitLog.sync(false);
                 }
 
-                long now = clock.currentTimeMillis();
+                long now = clock.nanoTime();
                 if (flushToDisk)
                     maybeLogFlushLag(pollStarted, now);
 
-                if (!run)
+                if (shutdownRequested)
                     return false;
 
-                // if we have lagged this round, we probably have work to do already so we don't sleep
-                long sleep = pollStarted + markerIntervalMillis - now;
-                if (sleep < 0)
-                    return true;
-
-                try
-                {
-                    haveWork.tryAcquire(sleep, TimeUnit.MILLISECONDS);
-                    haveWork.drainPermits();
-                }
-                catch (InterruptedException e)
-                {
-                    throw new AssertionError();
-                }
+                long wakeUpAt = pollStarted + markerIntervalNanos;
+                if (wakeUpAt > now)
+                    LockSupport.parkNanos(wakeUpAt - now);
             }
             catch (Throwable t)
             {
@@ -211,20 +203,14 @@
                     return false;
 
                 // sleep for full poll-interval after an error, so we don't spam the log file
-                try
-                {
-                    haveWork.tryAcquire(markerIntervalMillis, TimeUnit.MILLISECONDS);
-                }
-                catch (InterruptedException e)
-                {
-                    throw new AssertionError();
-                }
+                LockSupport.parkNanos(markerIntervalNanos);
             }
+
             return true;
         }
 
         /**
-         * Add a log entry whenever the time to flush the commit log to disk exceeds {@link #syncIntervalMillis}.
+         * Add a log entry whenever the time to flush the commit log to disk exceeds {@link #syncIntervalNanos}.
          */
         @VisibleForTesting
         boolean maybeLogFlushLag(long pollStarted, long now)
@@ -233,7 +219,7 @@
             totalSyncDuration += flushDuration;
 
             // this is the timestamp by which we should have completed the flush
-            long maxFlushTimestamp = pollStarted + syncIntervalMillis;
+            long maxFlushTimestamp = pollStarted + syncIntervalNanos;
             if (maxFlushTimestamp > now)
                 return false;
 
@@ -251,13 +237,16 @@
             if (firstLagAt > 0)
             {
                 //Only reset the lag tracking if it actually logged this time
-                boolean logged = NoSpamLogger.log(
-                logger,
-                NoSpamLogger.Level.WARN,
-                5,
-                TimeUnit.MINUTES,
-                "Out of {} commit log syncs over the past {}s with average duration of {}ms, {} have exceeded the configured commit interval by an average of {}ms",
-                syncCount, (now - firstLagAt) / 1000, String.format("%.2f", (double) totalSyncDuration / syncCount), lagCount, String.format("%.2f", (double) syncExceededIntervalBy / lagCount));
+                boolean logged = NoSpamLogger.log(logger,
+                                                  NoSpamLogger.Level.WARN,
+                                                  5,
+                                                  TimeUnit.MINUTES,
+                                                  "Out of {} commit log syncs over the past {}s with average duration of {}ms, {} have exceeded the configured commit interval by an average of {}ms",
+                                                  syncCount,
+                                                  String.format("%.2f", (now - firstLagAt) * 1e-9d),
+                                                  String.format("%.2f", totalSyncDuration * 1e-6d / syncCount),
+                                                  lagCount,
+                                                  String.format("%.2f", syncExceededIntervalBy * 1e-6d / lagCount));
                 if (logged)
                     firstLagAt = 0;
             }
@@ -271,7 +260,6 @@
         }
     }
 
-
     /**
      * Block for @param alloc to be sync'd as necessary, and handle bookkeeping
      */
@@ -284,48 +272,44 @@
     protected abstract void maybeWaitForSync(Allocation alloc);
 
     /**
-     * Sync immediately, but don't block for the sync to cmplete
+     * Request an additional sync cycle without blocking.
      */
-    public WaitQueue.Signal requestExtraSync()
-    {
-        WaitQueue.Signal signal = syncComplete.register();
-        requestSync();
-        return signal;
-    }
-
-    protected void requestSync()
+    void requestExtraSync()
     {
         syncRequested = true;
-        haveWork.release(1);
+        LockSupport.unpark(thread);
     }
 
     public void shutdown()
     {
         shutdown = true;
-        haveWork.release(1);
+        requestExtraSync();
     }
 
     /**
-     * FOR TESTING ONLY
+     * Request sync and wait until the current state is synced.
+     *
+     * Note: If a sync is in progress at the time of this request, the call will return after both it and a cycle
+     * initiated immediately afterwards complete.
      */
-    public void restartUnsafe()
+    public void syncBlocking()
     {
-        while (haveWork.availablePermits() < 1)
-            haveWork.release();
+        long requestTime = System.nanoTime();
+        requestExtraSync();
+        awaitSyncAt(requestTime, null);
+    }
 
-        while (haveWork.availablePermits() > 1)
+    void awaitSyncAt(long syncTime, Context context)
+    {
+        do
         {
-            try
-            {
-                haveWork.acquire();
-            }
-            catch (InterruptedException e)
-            {
-                throw new RuntimeException(e);
-            }
+            WaitQueue.Signal signal = context != null ? syncComplete.register(context) : syncComplete.register();
+            if (lastSyncedAt < syncTime)
+                signal.awaitUninterruptibly();
+            else
+                signal.cancel();
         }
-        shutdown = false;
-        start();
+        while (lastSyncedAt < syncTime);
     }
 
     public void awaitTermination() throws InterruptedException
diff --git a/src/java/org/apache/cassandra/db/commitlog/BatchCommitLogService.java b/src/java/org/apache/cassandra/db/commitlog/BatchCommitLogService.java
index c0e6afc..4edfa34 100644
--- a/src/java/org/apache/cassandra/db/commitlog/BatchCommitLogService.java
+++ b/src/java/org/apache/cassandra/db/commitlog/BatchCommitLogService.java
@@ -30,7 +30,7 @@
     {
         // wait until record has been safely persisted to disk
         pending.incrementAndGet();
-        requestSync();
+        requestExtraSync();
         alloc.awaitDiskSync(commitLog.metrics.waitingOnCommit);
         pending.decrementAndGet();
     }
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLog.java b/src/java/org/apache/cassandra/db/commitlog/CommitLog.java
index cac2d0f..47ef85c 100644
--- a/src/java/org/apache/cassandra/db/commitlog/CommitLog.java
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLog.java
@@ -40,15 +40,19 @@
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.ParameterizedClass;
 import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.exceptions.WriteTimeoutException;
 import org.apache.cassandra.io.FSWriteError;
 import org.apache.cassandra.io.compress.ICompressor;
 import org.apache.cassandra.io.util.BufferedDataOutputStreamPlus;
+import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.io.util.DataOutputBufferFixed;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.metrics.CommitLogMetrics;
 import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.schema.CompressionParams;
+import org.apache.cassandra.security.EncryptionContext;
 import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.MBeanWrapper;
 
@@ -68,36 +72,33 @@
 
     public static final CommitLog instance = CommitLog.construct();
 
-    // we used to try to avoid instantiating commitlog (thus creating an empty segment ready for writes)
-    // until after recover was finished.  this turns out to be fragile; it is less error-prone to go
-    // ahead and allow writes before recover(), and just skip active segments when we do.
     private static final FilenameFilter unmanagedFilesFilter = (dir, name) -> CommitLogDescriptor.isValid(name) && CommitLogSegment.shouldReplay(name);
 
     // we only permit records HALF the size of a commit log, to ensure we don't spin allocating many mostly
     // empty segments when writing large records
-    private final long MAX_MUTATION_SIZE = DatabaseDescriptor.getMaxMutationSize();
+    final long MAX_MUTATION_SIZE = DatabaseDescriptor.getMaxMutationSize();
 
-    public final CommitLogSegmentManager allocator;
+    final public AbstractCommitLogSegmentManager segmentManager;
+
     public final CommitLogArchiver archiver;
     final CommitLogMetrics metrics;
     final AbstractCommitLogService executor;
 
     volatile Configuration configuration;
-    final public String location;
 
     private static CommitLog construct()
     {
-        CommitLog log = new CommitLog(DatabaseDescriptor.getCommitLogLocation(), CommitLogArchiver.construct());
+        CommitLog log = new CommitLog(CommitLogArchiver.construct());
 
         MBeanWrapper.instance.registerMBean(log, "org.apache.cassandra.db:type=Commitlog");
         return log.start();
     }
 
     @VisibleForTesting
-    CommitLog(String location, CommitLogArchiver archiver)
+    CommitLog(CommitLogArchiver archiver)
     {
-        this.location = location;
-        this.configuration = new Configuration(DatabaseDescriptor.getCommitLogCompression());
+        this.configuration = new Configuration(DatabaseDescriptor.getCommitLogCompression(),
+                                               DatabaseDescriptor.getEncryptionContext());
         DatabaseDescriptor.createAllDirectories();
 
         this.archiver = archiver;
@@ -107,16 +108,18 @@
                 ? new BatchCommitLogService(this)
                 : new PeriodicCommitLogService(this);
 
-        allocator = new CommitLogSegmentManager(this);
+        segmentManager = DatabaseDescriptor.isCDCEnabled()
+                         ? new CommitLogSegmentManagerCDC(this, DatabaseDescriptor.getCommitLogLocation())
+                         : new CommitLogSegmentManagerStandard(this, DatabaseDescriptor.getCommitLogLocation());
 
         // register metrics
-        metrics.attach(executor, allocator);
+        metrics.attach(executor, segmentManager);
     }
 
     CommitLog start()
     {
+        segmentManager.start();
         executor.start();
-        allocator.start();
         return this;
     }
 
@@ -127,7 +130,7 @@
 
     private File[] getUnmanagedFiles()
     {
-        File[] files = new File(DatabaseDescriptor.getCommitLogLocation()).listFiles(unmanagedFilesFilter);
+        File[] files = new File(segmentManager.storageDirectory).listFiles(unmanagedFilesFilter);
         if (files == null)
             return new File[0];
         return files;
@@ -137,14 +140,13 @@
      * Perform recovery on commit logs located in the directory specified by the config file.
      *
      * @return the number of mutations replayed
+     * @throws IOException
      */
-    public int recover() throws IOException
+    public int recoverSegmentsOnDisk() throws IOException
     {
-        // If createReserveSegments is already flipped, the CLSM is running and recovery has already taken place.
-        if (allocator.createReserveSegments)
-            return 0;
-
-        // submit all existing files in the commit log dir for archiving prior to recovery - CASSANDRA-6904
+        // submit all files for this segment manager for archiving prior to recovery - CASSANDRA-6904
+        // The files may have already been archived by normal CommitLog operation. This may cause errors in this
+        // archiving pass, which we should not treat as serious.
         for (File file : getUnmanagedFiles())
         {
             archiver.maybeArchive(file.getPath(), file.getName());
@@ -154,9 +156,9 @@
         assert archiver.archivePending.isEmpty() : "Not all commit log archive tasks were completed before restore";
         archiver.maybeRestoreArchive();
 
+        // List the files again as archiver may have added segments.
         File[] files = getUnmanagedFiles();
         int replayed = 0;
-        allocator.enableReserveSegmentCreation();
         if (files.length == 0)
         {
             logger.info("No commitlog files found; skipping replay");
@@ -165,11 +167,11 @@
         {
             Arrays.sort(files, new CommitLogSegmentFileComparator());
             logger.info("Replaying {}", StringUtils.join(files, ", "));
-            replayed = recover(files);
+            replayed = recoverFiles(files);
             logger.info("Log replay complete, {} replayed mutations", replayed);
 
             for (File f : files)
-                allocator.recycleSegment(f);
+                segmentManager.handleReplayedSegment(f);
         }
 
         return replayed;
@@ -181,11 +183,18 @@
      * @param clogs   the list of commit log files to replay
      * @return the number of mutations replayed
      */
-    public int recover(File... clogs) throws IOException
+    public int recoverFiles(File... clogs) throws IOException
     {
-        CommitLogReplayer recovery = CommitLogReplayer.construct(this, getLocalHostId());
-        recovery.recover(clogs);
-        return recovery.blockForWrites();
+        CommitLogReplayer replayer = CommitLogReplayer.construct(this, getLocalHostId());
+        replayer.replayFiles(clogs);
+        return replayer.blockForWrites();
+    }
+
+    public void recoverPath(String path) throws IOException
+    {
+        CommitLogReplayer replayer = CommitLogReplayer.construct(this, getLocalHostId());
+        replayer.replayPath(new File(path), false);
+        replayer.blockForWrites();
     }
 
     private static UUID getLocalHostId()
@@ -194,22 +203,20 @@
     }
 
     /**
-     * Perform recovery on a single commit log.
+     * Perform recovery on a single commit log. Kept w/sub-optimal name due to coupling w/MBean / JMX
      */
     public void recover(String path) throws IOException
     {
-        CommitLogReplayer recovery = CommitLogReplayer.construct(this, getLocalHostId());
-        recovery.recover(new File(path), false);
-        recovery.blockForWrites();
+        recoverPath(path);
     }
 
     /**
-     * @return a ReplayPosition which, if >= one returned from add(), implies add() was started
+     * @return a CommitLogPosition which, if {@code >= one} returned from add(), implies add() was started
      * (but not necessarily finished) prior to this call
      */
-    public ReplayPosition getContext()
+    public CommitLogPosition getCurrentPosition()
     {
-        return allocator.allocatingFrom().getContext();
+        return segmentManager.getCurrentPosition();
     }
 
     /**
@@ -217,7 +224,7 @@
      */
     public void forceRecycleAllSegments(Iterable<UUID> droppedCfs)
     {
-        allocator.forceRecycleAll(droppedCfs);
+        segmentManager.forceRecycleAll(droppedCfs);
     }
 
     /**
@@ -225,21 +232,15 @@
      */
     public void forceRecycleAllSegments()
     {
-        allocator.forceRecycleAll(Collections.<UUID>emptyList());
+        segmentManager.forceRecycleAll(Collections.<UUID>emptyList());
     }
 
     /**
      * Forces a disk flush on the commit log files that need it.  Blocking.
      */
-    public void sync(boolean syncAllSegments, boolean flush)
+    public void sync(boolean flush) throws IOException
     {
-        CommitLogSegment current = allocator.allocatingFrom();
-        for (CommitLogSegment segment : allocator.getActiveSegments())
-        {
-            if (!syncAllSegments && segment.id > current.id)
-                return;
-            segment.sync(flush);
-        }
+        segmentManager.sync(flush);
     }
 
     /**
@@ -251,49 +252,60 @@
     }
 
     /**
-     * Add a Mutation to the commit log.
+     * Add a Mutation to the commit log. If CDC is enabled, this can fail.
      *
      * @param mutation the Mutation to add to the log
+     * @throws WriteTimeoutException
      */
-    public ReplayPosition add(Mutation mutation)
+    public CommitLogPosition add(Mutation mutation) throws WriteTimeoutException
     {
         assert mutation != null;
 
-        int size = (int) Mutation.serializer.serializedSize(mutation, MessagingService.current_version);
-
-        int totalSize = size + ENTRY_OVERHEAD_SIZE;
-        if (totalSize > MAX_MUTATION_SIZE)
+        try (DataOutputBuffer dob = DataOutputBuffer.scratchBuffer.get())
         {
-            throw new IllegalArgumentException(String.format("Mutation of %s bytes is too large for the maximum size of %s",
-                                                             totalSize, MAX_MUTATION_SIZE));
-        }
+            Mutation.serializer.serialize(mutation, dob, MessagingService.current_version);
+            int size = dob.getLength();
 
-        Allocation alloc = allocator.allocate(mutation, (int) totalSize);
-        CRC32 checksum = new CRC32();
-        final ByteBuffer buffer = alloc.getBuffer();
-        try (BufferedDataOutputStreamPlus dos = new DataOutputBufferFixed(buffer))
-        {
-            // checksummed length
-            dos.writeInt(size);
-            updateChecksumInt(checksum, size);
-            buffer.putInt((int) checksum.getValue());
+            int totalSize = size + ENTRY_OVERHEAD_SIZE;
+            if (totalSize > MAX_MUTATION_SIZE)
+            {
+                throw new IllegalArgumentException(String.format("Mutation of %s is too large for the maximum size of %s",
+                                                                 FBUtilities.prettyPrintMemory(totalSize),
+                                                                 FBUtilities.prettyPrintMemory(MAX_MUTATION_SIZE)));
+            }
 
-            // checksummed mutation
-            Mutation.serializer.serialize(mutation, dos, MessagingService.current_version);
-            updateChecksum(checksum, buffer, buffer.position() - size, size);
-            buffer.putInt((int) checksum.getValue());
+            Allocation alloc = segmentManager.allocate(mutation, totalSize);
+
+            CRC32 checksum = new CRC32();
+            final ByteBuffer buffer = alloc.getBuffer();
+            try (BufferedDataOutputStreamPlus dos = new DataOutputBufferFixed(buffer))
+            {
+                // checksummed length
+                dos.writeInt(size);
+                updateChecksumInt(checksum, size);
+                buffer.putInt((int) checksum.getValue());
+
+                // checksummed mutation
+                dos.write(dob.getData(), 0, size);
+                updateChecksum(checksum, buffer, buffer.position() - size, size);
+                buffer.putInt((int) checksum.getValue());
+            }
+            catch (IOException e)
+            {
+                throw new FSWriteError(e, alloc.getSegment().getPath());
+            }
+            finally
+            {
+                alloc.markWritten();
+            }
+
+            executor.finishWriteFor(alloc);
+            return alloc.getCommitLogPosition();
         }
         catch (IOException e)
         {
-            throw new FSWriteError(e, alloc.getSegment().getPath());
+            throw new FSWriteError(e, segmentManager.allocatingFrom().getPath());
         }
-        finally
-        {
-            alloc.markWritten();
-        }
-
-        executor.finishWriteFor(alloc);
-        return alloc.getReplayPosition();
     }
 
     /**
@@ -304,23 +316,23 @@
      * @param lowerBound the lowest covered replay position of the flush
      * @param lowerBound the highest covered replay position of the flush
      */
-    public void discardCompletedSegments(final UUID cfId, final ReplayPosition lowerBound, final ReplayPosition upperBound)
+    public void discardCompletedSegments(final UUID cfId, final CommitLogPosition lowerBound, final CommitLogPosition upperBound)
     {
         logger.trace("discard completed log segments for {}-{}, table {}", lowerBound, upperBound, cfId);
 
         // Go thru the active segment files, which are ordered oldest to newest, marking the
-        // flushed CF as clean, until we reach the segment file containing the ReplayPosition passed
+        // flushed CF as clean, until we reach the segment file containing the CommitLogPosition passed
         // in the arguments. Any segments that become unused after they are marked clean will be
         // recycled or discarded.
-        for (Iterator<CommitLogSegment> iter = allocator.getActiveSegments().iterator(); iter.hasNext();)
+        for (Iterator<CommitLogSegment> iter = segmentManager.getActiveSegments().iterator(); iter.hasNext();)
         {
             CommitLogSegment segment = iter.next();
             segment.markClean(cfId, lowerBound, upperBound);
 
             if (segment.isUnused())
             {
-                logger.trace("Commit log segment {} is unused", segment);
-                allocator.recycleSegment(segment);
+                logger.debug("Commit log segment {} is unused", segment);
+                segmentManager.archiveAndDiscard(segment);
             }
             else
             {
@@ -369,8 +381,8 @@
     public List<String> getActiveSegmentNames()
     {
         List<String> segmentNames = new ArrayList<>();
-        for (CommitLogSegment segment : allocator.getActiveSegments())
-            segmentNames.add(segment.getName());
+        for (CommitLogSegment seg : segmentManager.getActiveSegments())
+            segmentNames.add(seg.getName());
         return segmentNames;
     }
 
@@ -383,23 +395,23 @@
     public long getActiveContentSize()
     {
         long size = 0;
-        for (CommitLogSegment segment : allocator.getActiveSegments())
-            size += segment.contentSize();
+        for (CommitLogSegment seg : segmentManager.getActiveSegments())
+            size += seg.contentSize();
         return size;
     }
 
     @Override
     public long getActiveOnDiskSize()
     {
-        return allocator.onDiskSize();
+        return segmentManager.onDiskSize();
     }
 
     @Override
     public Map<String, Double> getActiveSegmentCompressionRatios()
     {
         Map<String, Double> segmentRatios = new TreeMap<>();
-        for (CommitLogSegment segment : allocator.getActiveSegments())
-            segmentRatios.put(segment.getName(), 1.0 * segment.onDiskSize() / segment.contentSize());
+        for (CommitLogSegment seg : segmentManager.getActiveSegments())
+            segmentRatios.put(seg.getName(), 1.0 * seg.onDiskSize() / seg.contentSize());
         return segmentRatios;
     }
 
@@ -411,12 +423,12 @@
     {
         executor.shutdown();
         executor.awaitTermination();
-        allocator.shutdown();
-        allocator.awaitTermination();
+        segmentManager.shutdown();
+        segmentManager.awaitTermination();
     }
 
     /**
-     * FOR TESTING PURPOSES. See CommitLogAllocator.
+     * FOR TESTING PURPOSES
      * @return the number of files recovered
      */
     public int resetUnsafe(boolean deleteSegments) throws IOException
@@ -427,7 +439,15 @@
     }
 
     /**
-     * FOR TESTING PURPOSES. See CommitLogAllocator.
+     * FOR TESTING PURPOSES.
+     */
+    public void resetConfiguration()
+    {
+        configuration = new Configuration(DatabaseDescriptor.getCommitLogCompression(),
+                                          DatabaseDescriptor.getEncryptionContext());
+    }
+
+    /**
      */
     public void stopUnsafe(boolean deleteSegments)
     {
@@ -440,50 +460,20 @@
         {
             throw new RuntimeException(e);
         }
-        allocator.stopUnsafe(deleteSegments);
+        segmentManager.stopUnsafe(deleteSegments);
         CommitLogSegment.resetReplayLimit();
+        if (DatabaseDescriptor.isCDCEnabled() && deleteSegments)
+            for (File f : new File(DatabaseDescriptor.getCDCLogLocation()).listFiles())
+                FileUtils.deleteWithConfirm(f);
+
     }
 
     /**
-     * FOR TESTING PURPOSES.
-     */
-    public void resetConfiguration()
-    {
-        configuration = new Configuration(DatabaseDescriptor.getCommitLogCompression());
-    }
-
-    /**
-     * FOR TESTING PURPOSES.  See CommitLogAllocator
+     * FOR TESTING PURPOSES
      */
     public int restartUnsafe() throws IOException
     {
-        allocator.start();
-        executor.restartUnsafe();
-        try
-        {
-            return recover();
-        }
-        catch (FSWriteError e)
-        {
-            // Workaround for a class of races that keeps showing up on Windows tests.
-            // stop/start/reset path on Windows with segment deletion is very touchy/brittle
-            // and the timing keeps getting screwed up. Rather than chasing our tail further
-            // or rewriting the CLSM, just report that we didn't recover anything back up
-            // the chain. This will silence most intermittent test failures on Windows
-            // and appropriately fail tests that expected segments to be recovered that
-            // were not.
-            return 0;
-        }
-    }
-
-    /**
-     * Used by tests.
-     *
-     * @return the number of active segments (segments with unflushed data in them)
-     */
-    public int activeSegments()
-    {
-        return allocator.getActiveSegments().size();
+        return start().recoverSegmentsOnDisk();
     }
 
     public static long freeDiskSpace()
@@ -545,10 +535,16 @@
          */
         private final ICompressor compressor;
 
-        public Configuration(ParameterizedClass compressorClass)
+        /**
+         * The encryption context used to encrypt the segments.
+         */
+        private EncryptionContext encryptionContext;
+
+        public Configuration(ParameterizedClass compressorClass, EncryptionContext encryptionContext)
         {
             this.compressorClass = compressorClass;
             this.compressor = compressorClass != null ? CompressionParams.createCompressor(compressorClass) : null;
+            this.encryptionContext = encryptionContext;
         }
 
         /**
@@ -561,6 +557,15 @@
         }
 
         /**
+         * Checks if the segments must be encrypted.
+         * @return <code>true</code> if the segments must be encrypted, <code>false</code> otherwise.
+         */
+        public boolean useEncryption()
+        {
+            return encryptionContext.isEnabled();
+        }
+
+        /**
          * Returns the compressor used to compress the segments.
          * @return the compressor used to compress the segments
          */
@@ -586,5 +591,14 @@
         {
             return useCompression() ? compressor.getClass().getSimpleName() : "none";
         }
+
+        /**
+         * Returns the encryption context used to encrypt the segments.
+         * @return the encryption context used to encrypt the segments
+         */
+        public EncryptionContext getEncryptionContext()
+        {
+            return encryptionContext;
+        }
     }
 }
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogArchiver.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogArchiver.java
index 5547d0e..a30ca0e 100644
--- a/src/java/org/apache/cassandra/db/commitlog/CommitLogArchiver.java
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogArchiver.java
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  * 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
@@ -7,16 +7,16 @@
  * 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.db.commitlog;
 
@@ -29,6 +29,8 @@
 import java.util.Properties;
 import java.util.TimeZone;
 import java.util.concurrent.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.apache.cassandra.concurrent.JMXEnabledThreadPoolExecutor;
 import org.apache.cassandra.config.DatabaseDescriptor;
@@ -40,12 +42,17 @@
 import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
 
 public class CommitLogArchiver
 {
     private static final Logger logger = LoggerFactory.getLogger(CommitLogArchiver.class);
     public static final SimpleDateFormat format = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
     private static final String DELIMITER = ",";
+    private static final Pattern NAME = Pattern.compile("%name");
+    private static final Pattern PATH = Pattern.compile("%path");
+    private static final Pattern FROM = Pattern.compile("%from");
+    private static final Pattern TO = Pattern.compile("%to");
     static
     {
         format.setTimeZone(TimeZone.getTimeZone("GMT"));
@@ -136,8 +143,8 @@
             protected void runMayThrow() throws IOException
             {
                 segment.waitForFinalSync();
-                String command = archiveCommand.replace("%name", segment.getName());
-                command = command.replace("%path", segment.getPath());
+                String command = NAME.matcher(archiveCommand).replaceAll(Matcher.quoteReplacement(segment.getName()));
+                command = PATH.matcher(command).replaceAll(Matcher.quoteReplacement(segment.getPath()));
                 exec(command);
             }
         }));
@@ -145,22 +152,32 @@
 
     /**
      * Differs from the above because it can be used on any file, rather than only
-     * managed commit log segments (and thus cannot call waitForFinalSync).
+     * managed commit log segments (and thus cannot call waitForFinalSync), and in
+     * the treatment of failures.
      *
-     * Used to archive files present in the commit log directory at startup (CASSANDRA-6904)
+     * Used to archive files present in the commit log directory at startup (CASSANDRA-6904).
+     * Since the files being already archived by normal operation could cause subsequent
+     * hard-linking or other operations to fail, we should not throw errors on failure
      */
     public void maybeArchive(final String path, final String name)
     {
         if (Strings.isNullOrEmpty(archiveCommand))
             return;
 
-        archivePending.put(name, executor.submit(new WrappedRunnable()
+        archivePending.put(name, executor.submit(new Runnable()
         {
-            protected void runMayThrow() throws IOException
+            public void run()
             {
-                String command = archiveCommand.replace("%name", name);
-                command = command.replace("%path", path);
-                exec(command);
+                try
+                {
+                    String command = NAME.matcher(archiveCommand).replaceAll(Matcher.quoteReplacement(name));
+                    command = PATH.matcher(command).replaceAll(Matcher.quoteReplacement(path));
+                    exec(command);
+                }
+                catch (IOException e)
+                {
+                    logger.warn("Archiving file {} failed, file may have already been archived.", name, e);
+                }
             }
         }));
     }
@@ -209,7 +226,7 @@
             }
             for (File fromFile : files)
             {
-                CommitLogDescriptor fromHeader = CommitLogDescriptor.fromHeader(fromFile);
+                CommitLogDescriptor fromHeader = CommitLogDescriptor.fromHeader(fromFile, DatabaseDescriptor.getEncryptionContext());
                 CommitLogDescriptor fromName = CommitLogDescriptor.isValid(fromFile.getName()) ? CommitLogDescriptor.fromFileName(fromFile.getName()) : null;
                 CommitLogDescriptor descriptor;
                 if (fromHeader == null && fromName == null)
@@ -225,7 +242,8 @@
                 if (descriptor.version > CommitLogDescriptor.current_version)
                     throw new IllegalStateException("Unsupported commit log version: " + descriptor.version);
 
-                if (descriptor.compression != null) {
+                if (descriptor.compression != null)
+                {
                     try
                     {
                         CompressionParams.createCompressor(descriptor.compression);
@@ -244,8 +262,8 @@
                     continue;
                 }
 
-                String command = restoreCommand.replace("%from", fromFile.getPath());
-                command = command.replace("%to", toFile.getPath());
+                String command = FROM.matcher(restoreCommand).replaceAll(Matcher.quoteReplacement(fromFile.getPath()));
+                command = TO.matcher(command).replaceAll(Matcher.quoteReplacement(toFile.getPath()));
                 try
                 {
                     exec(command);
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogDescriptor.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogDescriptor.java
index 0df20ce..a74bfe7 100644
--- a/src/java/org/apache/cassandra/db/commitlog/CommitLogDescriptor.java
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogDescriptor.java
@@ -27,6 +27,7 @@
 import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
+import java.util.Collections;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.regex.Matcher;
@@ -40,6 +41,7 @@
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.FSReadError;
 import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.security.EncryptionContext;
 import org.json.simple.JSONValue;
 
 import static org.apache.cassandra.utils.FBUtilities.updateChecksumInt;
@@ -51,14 +53,16 @@
     private static final String FILENAME_EXTENSION = ".log";
     // match both legacy and new version of commitlogs Ex: CommitLog-12345.log and CommitLog-4-12345.log.
     private static final Pattern COMMIT_LOG_FILE_PATTERN = Pattern.compile(FILENAME_PREFIX + "((\\d+)(" + SEPARATOR + "\\d+)?)" + FILENAME_EXTENSION);
-    private static final String COMPRESSION_PARAMETERS_KEY = "compressionParameters";
-    private static final String COMPRESSION_CLASS_KEY = "compressionClass";
+
+    static final String COMPRESSION_PARAMETERS_KEY = "compressionParameters";
+    static final String COMPRESSION_CLASS_KEY = "compressionClass";
 
     public static final int VERSION_12 = 2;
     public static final int VERSION_20 = 3;
     public static final int VERSION_21 = 4;
     public static final int VERSION_22 = 5;
     public static final int VERSION_30 = 6;
+
     /**
      * Increment this number if there is a changes in the commit log disc layout or MessagingVersion changes.
      * Note: make sure to handle {@link #getMessagingVersion()}
@@ -69,29 +73,40 @@
     final int version;
     public final long id;
     public final ParameterizedClass compression;
+    private final EncryptionContext encryptionContext;
 
-    public CommitLogDescriptor(int version, long id, ParameterizedClass compression)
+    public CommitLogDescriptor(int version, long id, ParameterizedClass compression, EncryptionContext encryptionContext)
     {
         this.version = version;
         this.id = id;
         this.compression = compression;
+        this.encryptionContext = encryptionContext;
     }
 
-    public CommitLogDescriptor(long id, ParameterizedClass compression)
+    public CommitLogDescriptor(long id, ParameterizedClass compression, EncryptionContext encryptionContext)
     {
-        this(current_version, id, compression);
+        this(current_version, id, compression, encryptionContext);
     }
 
     public static void writeHeader(ByteBuffer out, CommitLogDescriptor descriptor)
     {
+        writeHeader(out, descriptor, Collections.<String, String>emptyMap());
+    }
+
+    /**
+     * @param additionalHeaders Allow segments to pass custom header data
+     */
+    public static void writeHeader(ByteBuffer out, CommitLogDescriptor descriptor, Map<String, String> additionalHeaders)
+    {
         CRC32 crc = new CRC32();
         out.putInt(descriptor.version);
         updateChecksumInt(crc, descriptor.version);
         out.putLong(descriptor.id);
         updateChecksumInt(crc, (int) (descriptor.id & 0xFFFFFFFFL));
         updateChecksumInt(crc, (int) (descriptor.id >>> 32));
-        if (descriptor.version >= VERSION_22) {
-            String parametersString = constructParametersString(descriptor);
+        if (descriptor.version >= VERSION_22)
+        {
+            String parametersString = constructParametersString(descriptor.compression, descriptor.encryptionContext, additionalHeaders);
             byte[] parametersBytes = parametersString.getBytes(StandardCharsets.UTF_8);
             if (parametersBytes.length != (((short) parametersBytes.length) & 0xFFFF))
                 throw new ConfigurationException(String.format("Compression parameters too long, length %d cannot be above 65535.",
@@ -100,29 +115,33 @@
             updateChecksumInt(crc, parametersBytes.length);
             out.put(parametersBytes);
             crc.update(parametersBytes, 0, parametersBytes.length);
-        } else
+        }
+        else
             assert descriptor.compression == null;
         out.putInt((int) crc.getValue());
     }
 
-    private static String constructParametersString(CommitLogDescriptor descriptor)
+    @VisibleForTesting
+    static String constructParametersString(ParameterizedClass compression, EncryptionContext encryptionContext, Map<String, String> additionalHeaders)
     {
-        Map<String, Object> params = new TreeMap<String, Object>();
-        ParameterizedClass compression = descriptor.compression;
+        Map<String, Object> params = new TreeMap<>();
         if (compression != null)
         {
             params.put(COMPRESSION_PARAMETERS_KEY, compression.parameters);
             params.put(COMPRESSION_CLASS_KEY, compression.class_name);
         }
+        if (encryptionContext != null)
+            params.putAll(encryptionContext.toHeaderParameters());
+        params.putAll(additionalHeaders);
         return JSONValue.toJSONString(params);
     }
 
-    public static CommitLogDescriptor fromHeader(File file)
+    public static CommitLogDescriptor fromHeader(File file, EncryptionContext encryptionContext)
     {
         try (RandomAccessFile raf = new RandomAccessFile(file, "r"))
         {
             assert raf.getFilePointer() == 0;
-            return readHeader(raf);
+            return readHeader(raf, encryptionContext);
         }
         catch (EOFException e)
         {
@@ -134,7 +153,7 @@
         }
     }
 
-    public static CommitLogDescriptor readHeader(DataInput input) throws IOException
+    public static CommitLogDescriptor readHeader(DataInput input, EncryptionContext encryptionContext) throws IOException
     {
         CRC32 checkcrc = new CRC32();
         int version = input.readInt();
@@ -143,7 +162,8 @@
         updateChecksumInt(checkcrc, (int) (id & 0xFFFFFFFFL));
         updateChecksumInt(checkcrc, (int) (id >>> 32));
         int parametersLength = 0;
-        if (version >= VERSION_22) {
+        if (version >= VERSION_22)
+        {
             parametersLength = input.readShort() & 0xFFFF;
             updateChecksumInt(checkcrc, parametersLength);
         }
@@ -153,16 +173,20 @@
         input.readFully(parametersBytes);
         checkcrc.update(parametersBytes, 0, parametersBytes.length);
         int crc = input.readInt();
+
         if (crc == (int) checkcrc.getValue())
-            return new CommitLogDescriptor(version, id,
-                    parseCompression((Map<?, ?>) JSONValue.parse(new String(parametersBytes, StandardCharsets.UTF_8))));
+        {
+            Map<?, ?> map = (Map<?, ?>) JSONValue.parse(new String(parametersBytes, StandardCharsets.UTF_8));
+            return new CommitLogDescriptor(version, id, parseCompression(map), EncryptionContext.createFromMap(map, encryptionContext));
+        }
         return null;
     }
 
     @SuppressWarnings("unchecked")
-    private static ParameterizedClass parseCompression(Map<?, ?> params)
+    @VisibleForTesting
+    static ParameterizedClass parseCompression(Map<?, ?> params)
     {
-        if (params == null)
+        if (params == null || params.isEmpty())
             return null;
         String className = (String) params.get(COMPRESSION_CLASS_KEY);
         if (className == null)
@@ -182,7 +206,7 @@
             throw new UnsupportedOperationException("Commitlog segment is too old to open; upgrade to 1.2.5+ first");
 
         long id = Long.parseLong(matcher.group(3).split(SEPARATOR)[1]);
-        return new CommitLogDescriptor(Integer.parseInt(matcher.group(2)), id, null);
+        return new CommitLogDescriptor(Integer.parseInt(matcher.group(2)), id, null, new EncryptionContext());
     }
 
     public int getMessagingVersion()
@@ -218,6 +242,11 @@
         return COMMIT_LOG_FILE_PATTERN.matcher(filename).matches();
     }
 
+    public EncryptionContext getEncryptionContext()
+    {
+        return encryptionContext;
+    }
+
     public String toString()
     {
         return "(" + version + "," + id + (compression != null ? "," + compression : "") + ")";
@@ -235,7 +264,7 @@
 
     public boolean equals(CommitLogDescriptor that)
     {
-        return equalsIgnoringCompression(that) && Objects.equal(this.compression, that.compression);
+        return equalsIgnoringCompression(that) && Objects.equal(this.compression, that.compression)
+                && Objects.equal(encryptionContext, that.encryptionContext);
     }
-
 }
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogPosition.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogPosition.java
new file mode 100644
index 0000000..84054a4
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogPosition.java
@@ -0,0 +1,121 @@
+/*
+ * 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.db.commitlog;
+
+import java.io.IOException;
+import java.util.Comparator;
+
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.ISerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+
+/**
+ * Contains a segment id and a position for CommitLogSegment identification.
+ * Used for both replay and general CommitLog file reading.
+ */
+public class CommitLogPosition implements Comparable<CommitLogPosition>
+{
+    public static final CommitLogPositionSerializer serializer = new CommitLogPositionSerializer();
+
+    // NONE is used for SSTables that are streamed from other nodes and thus have no relationship
+    // with our local commitlog. The values satisfy the criteria that
+    //  - no real commitlog segment will have the given id
+    //  - it will sort before any real CommitLogPosition, so it will be effectively ignored by getCommitLogPosition
+    public static final CommitLogPosition NONE = new CommitLogPosition(-1, 0);
+
+    public final long segmentId;
+    public final int position;
+
+    public static final Comparator<CommitLogPosition> comparator = new Comparator<CommitLogPosition>()
+    {
+        public int compare(CommitLogPosition o1, CommitLogPosition o2)
+        {
+            if (o1.segmentId != o2.segmentId)
+            	return Long.compare(o1.segmentId,  o2.segmentId);
+
+            return Integer.compare(o1.position, o2.position);
+        }
+    };
+
+    public CommitLogPosition(long segmentId, int position)
+    {
+        this.segmentId = segmentId;
+        assert position >= 0;
+        this.position = position;
+    }
+
+    public int compareTo(CommitLogPosition other)
+    {
+        return comparator.compare(this, other);
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        CommitLogPosition that = (CommitLogPosition) o;
+
+        if (position != that.position) return false;
+        return segmentId == that.segmentId;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int result = (int) (segmentId ^ (segmentId >>> 32));
+        result = 31 * result + position;
+        return result;
+    }
+
+    @Override
+    public String toString()
+    {
+        return "CommitLogPosition(" +
+               "segmentId=" + segmentId +
+               ", position=" + position +
+               ')';
+    }
+
+    public CommitLogPosition clone()
+    {
+        return new CommitLogPosition(segmentId, position);
+    }
+
+
+    public static class CommitLogPositionSerializer implements ISerializer<CommitLogPosition>
+    {
+        public void serialize(CommitLogPosition clsp, DataOutputPlus out) throws IOException
+        {
+            out.writeLong(clsp.segmentId);
+            out.writeInt(clsp.position);
+        }
+
+        public CommitLogPosition deserialize(DataInputPlus in) throws IOException
+        {
+            return new CommitLogPosition(in.readLong(), in.readInt());
+        }
+
+        public long serializedSize(CommitLogPosition clsp)
+        {
+            return TypeSizes.sizeof(clsp.segmentId) + TypeSizes.sizeof(clsp.position);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogReadHandler.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogReadHandler.java
new file mode 100644
index 0000000..0602147
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogReadHandler.java
@@ -0,0 +1,76 @@
+/*
+ * 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.db.commitlog;
+
+import java.io.IOException;
+
+import org.apache.cassandra.db.Mutation;
+
+public interface CommitLogReadHandler
+{
+    enum CommitLogReadErrorReason
+    {
+        RECOVERABLE_DESCRIPTOR_ERROR,
+        UNRECOVERABLE_DESCRIPTOR_ERROR,
+        MUTATION_ERROR,
+        UNRECOVERABLE_UNKNOWN_ERROR,
+        EOF
+    }
+
+    class CommitLogReadException extends IOException
+    {
+        public final CommitLogReadErrorReason reason;
+        public final boolean permissible;
+
+        CommitLogReadException(String message, CommitLogReadErrorReason reason, boolean permissible)
+        {
+            super(message);
+            this.reason = reason;
+            this.permissible = permissible;
+        }
+    }
+
+    /**
+     * Handle an error during segment read, signaling whether or not you want the reader to skip the remainder of the
+     * current segment on error.
+     *
+     * @param exception CommitLogReadException w/details on exception state
+     * @return boolean indicating whether to stop reading
+     * @throws IOException In the event the handler wants forceful termination of all processing, throw IOException.
+     */
+    boolean shouldSkipSegmentOnError(CommitLogReadException exception) throws IOException;
+
+    /**
+     * In instances where we cannot recover from a specific error and don't care what the reader thinks
+     *
+     * @param exception CommitLogReadException w/details on exception state
+     * @throws IOException
+     */
+    void handleUnrecoverableError(CommitLogReadException exception) throws IOException;
+
+    /**
+     * Process a deserialized mutation
+     *
+     * @param m deserialized mutation
+     * @param size serialized size of the mutation
+     * @param entryLocation filePointer offset inside the CommitLogSegment for the record
+     * @param desc CommitLogDescriptor for mutation being processed
+     */
+    void handleMutation(Mutation m, int size, int entryLocation, CommitLogDescriptor desc);
+}
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogReader.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogReader.java
new file mode 100644
index 0000000..4d74557
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogReader.java
@@ -0,0 +1,564 @@
+/*
+ * 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.db.commitlog;
+
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.CRC32;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.UnknownColumnFamilyException;
+import org.apache.cassandra.db.commitlog.CommitLogReadHandler.CommitLogReadErrorReason;
+import org.apache.cassandra.db.commitlog.CommitLogReadHandler.CommitLogReadException;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.SerializationHelper;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.io.util.ChannelProxy;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.FileDataInput;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.io.util.RebufferingInputStream;
+import org.apache.cassandra.utils.JVMStabilityInspector;
+
+import static org.apache.cassandra.utils.FBUtilities.updateChecksumInt;
+
+public class CommitLogReader
+{
+    private static final Logger logger = LoggerFactory.getLogger(CommitLogReader.class);
+
+    private static final int LEGACY_END_OF_SEGMENT_MARKER = 0;
+
+    @VisibleForTesting
+    public static final int ALL_MUTATIONS = -1;
+    private final CRC32 checksum;
+    private final Map<UUID, AtomicInteger> invalidMutations;
+
+    private byte[] buffer;
+
+    public CommitLogReader()
+    {
+        checksum = new CRC32();
+        invalidMutations = new HashMap<>();
+        buffer = new byte[4096];
+    }
+
+    public Set<Map.Entry<UUID, AtomicInteger>> getInvalidMutations()
+    {
+        return invalidMutations.entrySet();
+    }
+
+    /**
+     * Reads all passed in files with no minimum, no start, and no mutation limit.
+     */
+    public void readAllFiles(CommitLogReadHandler handler, File[] files) throws IOException
+    {
+        readAllFiles(handler, files, CommitLogPosition.NONE);
+    }
+
+    private static boolean shouldSkip(File file) throws IOException, ConfigurationException
+    {
+        CommitLogDescriptor desc = CommitLogDescriptor.fromFileName(file.getName());
+        if (desc.version < CommitLogDescriptor.VERSION_21)
+        {
+            return false;
+        }
+        try(RandomAccessReader reader = RandomAccessReader.open(file))
+        {
+            CommitLogDescriptor.readHeader(reader, DatabaseDescriptor.getEncryptionContext());
+            int end = reader.readInt();
+            long filecrc = reader.readInt() & 0xffffffffL;
+            return end == 0 && filecrc == 0;
+        }
+    }
+
+    private static List<File> filterCommitLogFiles(File[] toFilter)
+    {
+        List<File> filtered = new ArrayList<>(toFilter.length);
+        for (File file: toFilter)
+        {
+            try
+            {
+                if (shouldSkip(file))
+                {
+                    logger.info("Skipping playback of empty log: {}", file.getName());
+                }
+                else
+                {
+                    filtered.add(file);
+                }
+            }
+            catch (Exception e)
+            {
+                // let recover deal with it
+                filtered.add(file);
+            }
+        }
+
+        return filtered;
+    }
+
+    /**
+     * Reads all passed in files with minPosition, no start, and no mutation limit.
+     */
+    public void readAllFiles(CommitLogReadHandler handler, File[] files, CommitLogPosition minPosition) throws IOException
+    {
+        List<File> filteredLogs = filterCommitLogFiles(files);
+        int i = 0;
+        for (File file: filteredLogs)
+        {
+            i++;
+            readCommitLogSegment(handler, file, minPosition, ALL_MUTATIONS, i == filteredLogs.size());
+        }
+    }
+
+    /**
+     * Reads passed in file fully
+     */
+    public void readCommitLogSegment(CommitLogReadHandler handler, File file, boolean tolerateTruncation) throws IOException
+    {
+        readCommitLogSegment(handler, file, CommitLogPosition.NONE, ALL_MUTATIONS, tolerateTruncation);
+    }
+
+    /**
+     * Reads passed in file fully, up to mutationLimit count
+     */
+    @VisibleForTesting
+    public void readCommitLogSegment(CommitLogReadHandler handler, File file, int mutationLimit, boolean tolerateTruncation) throws IOException
+    {
+        readCommitLogSegment(handler, file, CommitLogPosition.NONE, mutationLimit, tolerateTruncation);
+    }
+
+    /**
+     * Reads mutations from file, handing them off to handler
+     * @param handler Handler that will take action based on deserialized Mutations
+     * @param file CommitLogSegment file to read
+     * @param minPosition Optional minimum CommitLogPosition - all segments with id > or matching w/greater position will be read
+     * @param mutationLimit Optional limit on # of mutations to replay. Local ALL_MUTATIONS serves as marker to play all.
+     * @param tolerateTruncation Whether or not we should allow truncation of this file or throw if EOF found
+     *
+     * @throws IOException
+     */
+    public void readCommitLogSegment(CommitLogReadHandler handler,
+                                     File file,
+                                     CommitLogPosition minPosition,
+                                     int mutationLimit,
+                                     boolean tolerateTruncation) throws IOException
+    {
+        // just transform from the file name (no reading of headers) to determine version
+        CommitLogDescriptor desc = CommitLogDescriptor.fromFileName(file.getName());
+
+        try(RandomAccessReader reader = RandomAccessReader.open(file))
+        {
+            if (desc.version < CommitLogDescriptor.VERSION_21)
+            {
+                if (!shouldSkipSegmentId(file, desc, minPosition))
+                {
+                    if (minPosition.segmentId == desc.id)
+                        reader.seek(minPosition.position);
+                    ReadStatusTracker statusTracker = new ReadStatusTracker(mutationLimit, tolerateTruncation);
+                    statusTracker.errorContext = desc.fileName();
+                    readSection(handler, reader, minPosition, (int) reader.length(), statusTracker, desc);
+                }
+                return;
+            }
+
+            final long segmentIdFromFilename = desc.id;
+            try
+            {
+                // The following call can either throw or legitimately return null. For either case, we need to check
+                // desc outside this block and set it to null in the exception case.
+                desc = CommitLogDescriptor.readHeader(reader, DatabaseDescriptor.getEncryptionContext());
+            }
+            catch (Exception e)
+            {
+                desc = null;
+            }
+            if (desc == null)
+            {
+                // don't care about whether or not the handler thinks we can continue. We can't w/out descriptor.
+                // whether or not we continue with startup will depend on whether this is the last segment
+                handler.handleUnrecoverableError(new CommitLogReadException(
+                    String.format("Could not read commit log descriptor in file %s", file),
+                    CommitLogReadErrorReason.UNRECOVERABLE_DESCRIPTOR_ERROR,
+                    tolerateTruncation));
+                return;
+            }
+
+            if (segmentIdFromFilename != desc.id)
+            {
+                if (handler.shouldSkipSegmentOnError(new CommitLogReadException(String.format(
+                    "Segment id mismatch (filename %d, descriptor %d) in file %s", segmentIdFromFilename, desc.id, file),
+                                                                                CommitLogReadErrorReason.RECOVERABLE_DESCRIPTOR_ERROR,
+                                                                                false)))
+                {
+                    return;
+                }
+            }
+
+            if (shouldSkipSegmentId(file, desc, minPosition))
+                return;
+
+            CommitLogSegmentReader segmentReader;
+            try
+            {
+                segmentReader = new CommitLogSegmentReader(handler, desc, reader, tolerateTruncation);
+            }
+            catch(Exception e)
+            {
+                handler.handleUnrecoverableError(new CommitLogReadException(
+                    String.format("Unable to create segment reader for commit log file: %s", e),
+                    CommitLogReadErrorReason.UNRECOVERABLE_UNKNOWN_ERROR,
+                    tolerateTruncation));
+                return;
+            }
+
+            try
+            {
+                ReadStatusTracker statusTracker = new ReadStatusTracker(mutationLimit, tolerateTruncation);
+                for (CommitLogSegmentReader.SyncSegment syncSegment : segmentReader)
+                {
+                    // Only tolerate truncation if we allow in both global and segment
+                    statusTracker.tolerateErrorsInSection = tolerateTruncation & syncSegment.toleratesErrorsInSection;
+
+                    // Skip segments that are completely behind the desired minPosition
+                    if (desc.id == minPosition.segmentId && syncSegment.endPosition < minPosition.position)
+                        continue;
+
+                    statusTracker.errorContext = String.format("Next section at %d in %s", syncSegment.fileStartPosition, desc.fileName());
+
+                    readSection(handler, syncSegment.input, minPosition, syncSegment.endPosition, statusTracker, desc);
+                    if (!statusTracker.shouldContinue())
+                        break;
+                }
+            }
+            // Unfortunately AbstractIterator cannot throw a checked exception, so we check to see if a RuntimeException
+            // is wrapping an IOException.
+            catch (RuntimeException re)
+            {
+                if (re.getCause() instanceof IOException)
+                    throw (IOException) re.getCause();
+                throw re;
+            }
+            logger.debug("Finished reading {}", file);
+        }
+    }
+
+    /**
+     * Any segment with id >= minPosition.segmentId is a candidate for read.
+     */
+    private boolean shouldSkipSegmentId(File file, CommitLogDescriptor desc, CommitLogPosition minPosition)
+    {
+        logger.debug("Reading {} (CL version {}, messaging version {}, compression {})",
+            file.getPath(),
+            desc.version,
+            desc.getMessagingVersion(),
+            desc.compression);
+
+        if (minPosition.segmentId > desc.id)
+        {
+            logger.trace("Skipping read of fully-flushed {}", file);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Reads a section of a file containing mutations
+     *
+     * @param handler Handler that will take action based on deserialized Mutations
+     * @param reader FileDataInput / logical buffer containing commitlog mutations
+     * @param minPosition CommitLogPosition indicating when we should start actively replaying mutations
+     * @param end logical numeric end of the segment being read
+     * @param statusTracker ReadStatusTracker with current state of mutation count, error state, etc
+     * @param desc Descriptor for CommitLog serialization
+     */
+    private void readSection(CommitLogReadHandler handler,
+                             FileDataInput reader,
+                             CommitLogPosition minPosition,
+                             int end,
+                             ReadStatusTracker statusTracker,
+                             CommitLogDescriptor desc) throws IOException
+    {
+        // seek rather than deserializing mutation-by-mutation to reach the desired minPosition in this SyncSegment
+        if (desc.id == minPosition.segmentId && reader.getFilePointer() < minPosition.position)
+            reader.seek(minPosition.position);
+
+        while (statusTracker.shouldContinue() && reader.getFilePointer() < end && !reader.isEOF())
+        {
+            long mutationStart = reader.getFilePointer();
+            if (logger.isTraceEnabled())
+                logger.trace("Reading mutation at {}", mutationStart);
+
+            long claimedCRC32;
+            int serializedSize;
+            try
+            {
+                // We rely on reading serialized size == 0 (LEGACY_END_OF_SEGMENT_MARKER) to identify the end
+                // of a segment, which happens naturally due to the 0 padding of the empty segment on creation.
+                // However, it's possible with 2.1 era commitlogs that the last mutation ended less than 4 bytes
+                // from the end of the file, which means that we'll be unable to read an a full int and instead
+                // read an EOF here
+                if(end - reader.getFilePointer() < 4)
+                {
+                    logger.trace("Not enough bytes left for another mutation in this CommitLog segment, continuing");
+                    statusTracker.requestTermination();
+                    return;
+                }
+
+                // any of the reads may hit EOF
+                serializedSize = reader.readInt();
+                if (serializedSize == LEGACY_END_OF_SEGMENT_MARKER)
+                {
+                    logger.trace("Encountered end of segment marker at {}", reader.getFilePointer());
+                    statusTracker.requestTermination();
+                    return;
+                }
+
+                // Mutation must be at LEAST 10 bytes:
+                //    3 for a non-empty Keyspace
+                //    3 for a Key (including the 2-byte length from writeUTF/writeWithShortLength)
+                //    4 bytes for column count.
+                // This prevents CRC by being fooled by special-case garbage in the file; see CASSANDRA-2128
+                if (serializedSize < 10)
+                {
+                    if (handler.shouldSkipSegmentOnError(new CommitLogReadException(
+                                                    String.format("Invalid mutation size %d at %d in %s", serializedSize, mutationStart, statusTracker.errorContext),
+                                                    CommitLogReadErrorReason.MUTATION_ERROR,
+                                                    statusTracker.tolerateErrorsInSection)))
+                    {
+                        statusTracker.requestTermination();
+                    }
+                    return;
+                }
+
+                long claimedSizeChecksum = CommitLogFormat.calculateClaimedChecksum(reader, desc.version);
+                checksum.reset();
+                CommitLogFormat.updateChecksum(checksum, serializedSize, desc.version);
+
+                if (checksum.getValue() != claimedSizeChecksum)
+                {
+                    if (handler.shouldSkipSegmentOnError(new CommitLogReadException(
+                                                    String.format("Mutation size checksum failure at %d in %s", mutationStart, statusTracker.errorContext),
+                                                    CommitLogReadErrorReason.MUTATION_ERROR,
+                                                    statusTracker.tolerateErrorsInSection)))
+                    {
+                        statusTracker.requestTermination();
+                    }
+                    return;
+                }
+
+                if (serializedSize > buffer.length)
+                    buffer = new byte[(int) (1.2 * serializedSize)];
+                reader.readFully(buffer, 0, serializedSize);
+
+                claimedCRC32 = CommitLogFormat.calculateClaimedCRC32(reader, desc.version);
+            }
+            catch (EOFException eof)
+            {
+                if (handler.shouldSkipSegmentOnError(new CommitLogReadException(
+                                                String.format("Unexpected end of segment at %d in %s", mutationStart, statusTracker.errorContext),
+                                                CommitLogReadErrorReason.EOF,
+                                                statusTracker.tolerateErrorsInSection)))
+                {
+                    statusTracker.requestTermination();
+                }
+                return;
+            }
+
+            checksum.update(buffer, 0, serializedSize);
+            if (claimedCRC32 != checksum.getValue())
+            {
+                if (handler.shouldSkipSegmentOnError(new CommitLogReadException(
+                                                String.format("Mutation checksum failure at %d in %s", mutationStart, statusTracker.errorContext),
+                                                CommitLogReadErrorReason.MUTATION_ERROR,
+                                                statusTracker.tolerateErrorsInSection)))
+                {
+                    statusTracker.requestTermination();
+                }
+                continue;
+            }
+
+            long mutationPosition = reader.getFilePointer();
+            readMutation(handler, buffer, serializedSize, minPosition, (int)mutationPosition, desc);
+
+            // Only count this as a processed mutation if it is after our min as we suppress reading of mutations that
+            // are before this mark.
+            if (mutationPosition >= minPosition.position)
+                statusTracker.addProcessedMutation();
+        }
+    }
+
+    /**
+     * Deserializes and passes a Mutation to the ICommitLogReadHandler requested
+     *
+     * @param handler Handler that will take action based on deserialized Mutations
+     * @param inputBuffer raw byte array w/Mutation data
+     * @param size deserialized size of mutation
+     * @param minPosition We need to suppress replay of mutations that are before the required minPosition
+     * @param entryLocation filePointer offset of mutation within CommitLogSegment
+     * @param desc CommitLogDescriptor being worked on
+     */
+    @VisibleForTesting
+    protected void readMutation(CommitLogReadHandler handler,
+                                byte[] inputBuffer,
+                                int size,
+                                CommitLogPosition minPosition,
+                                final int entryLocation,
+                                final CommitLogDescriptor desc) throws IOException
+    {
+        // For now, we need to go through the motions of deserializing the mutation to determine its size and move
+        // the file pointer forward accordingly, even if we're behind the requested minPosition within this SyncSegment.
+        boolean shouldReplay = entryLocation > minPosition.position;
+
+        final Mutation mutation;
+        try (RebufferingInputStream bufIn = new DataInputBuffer(inputBuffer, 0, size))
+        {
+            mutation = Mutation.serializer.deserialize(bufIn,
+                                                       desc.getMessagingVersion(),
+                                                       SerializationHelper.Flag.LOCAL);
+            // doublecheck that what we read is still] valid for the current schema
+            for (PartitionUpdate upd : mutation.getPartitionUpdates())
+                upd.validate();
+        }
+        catch (UnknownColumnFamilyException ex)
+        {
+            if (ex.cfId == null)
+                return;
+            AtomicInteger i = invalidMutations.get(ex.cfId);
+            if (i == null)
+            {
+                i = new AtomicInteger(1);
+                invalidMutations.put(ex.cfId, i);
+            }
+            else
+                i.incrementAndGet();
+            return;
+        }
+        catch (Throwable t)
+        {
+            JVMStabilityInspector.inspectThrowable(t);
+            File f = File.createTempFile("mutation", "dat");
+
+            try (DataOutputStream out = new DataOutputStream(new FileOutputStream(f)))
+            {
+                out.write(inputBuffer, 0, size);
+            }
+
+            // Checksum passed so this error can't be permissible.
+            handler.handleUnrecoverableError(new CommitLogReadException(
+                String.format(
+                    "Unexpected error deserializing mutation; saved to %s.  " +
+                    "This may be caused by replaying a mutation against a table with the same name but incompatible schema.  " +
+                    "Exception follows: %s", f.getAbsolutePath(), t),
+                CommitLogReadErrorReason.MUTATION_ERROR,
+                false));
+            return;
+        }
+
+        if (logger.isTraceEnabled())
+            logger.trace("Read mutation for {}.{}: {}", mutation.getKeyspaceName(), mutation.key(),
+                         "{" + StringUtils.join(mutation.getPartitionUpdates().iterator(), ", ") + "}");
+
+        if (shouldReplay)
+            handler.handleMutation(mutation, size, entryLocation, desc);
+    }
+
+    /**
+     * Helper methods to deal with changing formats of internals of the CommitLog without polluting deserialization code.
+     */
+    private static class CommitLogFormat
+    {
+        public static long calculateClaimedChecksum(FileDataInput input, int commitLogVersion) throws IOException
+        {
+            switch (commitLogVersion)
+            {
+                case CommitLogDescriptor.VERSION_12:
+                case CommitLogDescriptor.VERSION_20:
+                    return input.readLong();
+                // Changed format in 2.1
+                default:
+                    return input.readInt() & 0xffffffffL;
+            }
+        }
+
+        public static void updateChecksum(CRC32 checksum, int serializedSize, int commitLogVersion)
+        {
+            switch (commitLogVersion)
+            {
+                case CommitLogDescriptor.VERSION_12:
+                    checksum.update(serializedSize);
+                    break;
+                // Changed format in 2.0
+                default:
+                    updateChecksumInt(checksum, serializedSize);
+                    break;
+            }
+        }
+
+        public static long calculateClaimedCRC32(FileDataInput input, int commitLogVersion) throws IOException
+        {
+            switch (commitLogVersion)
+            {
+                case CommitLogDescriptor.VERSION_12:
+                case CommitLogDescriptor.VERSION_20:
+                    return input.readLong();
+                // Changed format in 2.1
+                default:
+                    return input.readInt() & 0xffffffffL;
+            }
+        }
+    }
+
+    private static class ReadStatusTracker
+    {
+        private int mutationsLeft;
+        public String errorContext = "";
+        public boolean tolerateErrorsInSection;
+        private boolean error;
+
+        public ReadStatusTracker(int mutationLimit, boolean tolerateErrorsInSection)
+        {
+            this.mutationsLeft = mutationLimit;
+            this.tolerateErrorsInSection = tolerateErrorsInSection;
+        }
+
+        public void addProcessedMutation()
+        {
+            if (mutationsLeft == ALL_MUTATIONS)
+                return;
+            --mutationsLeft;
+        }
+
+        public boolean shouldContinue()
+        {
+            return !error && (mutationsLeft != 0 || mutationsLeft == ALL_MUTATIONS);
+        }
+
+        public void requestTermination()
+        {
+            error = true;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java
index 0f07dae..73e5d2f 100644
--- a/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java
@@ -18,101 +18,92 @@
  */
 package org.apache.cassandra.db.commitlog;
 
-import java.io.DataOutputStream;
-import java.io.EOFException;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.nio.ByteBuffer;
 import java.util.*;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.zip.CRC32;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Predicate;
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Ordering;
-
+import com.google.common.collect.*;
 import org.apache.commons.lang3.StringUtils;
+import org.cliffc.high_scale_lib.NonBlockingHashSet;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+
 import org.apache.cassandra.concurrent.Stage;
 import org.apache.cassandra.concurrent.StageManager;
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.Schema;
-import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.rows.SerializationHelper;
+import org.apache.cassandra.config.SchemaConstants;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
-import org.apache.cassandra.exceptions.ConfigurationException;
-import org.apache.cassandra.io.util.FileSegmentInputStream;
-import org.apache.cassandra.io.util.RebufferingInputStream;
-import org.apache.cassandra.schema.CompressionParams;
-import org.apache.cassandra.io.compress.ICompressor;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
-import org.apache.cassandra.io.util.ChannelProxy;
-import org.apache.cassandra.io.util.DataInputBuffer;
-import org.apache.cassandra.io.util.FileDataInput;
-import org.apache.cassandra.io.util.RandomAccessReader;
-import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.utils.FBUtilities;
-import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.WrappedRunnable;
-import org.cliffc.high_scale_lib.NonBlockingHashSet;
 
-import static org.apache.cassandra.utils.FBUtilities.updateChecksumInt;
-
-public class CommitLogReplayer
+public class CommitLogReplayer implements CommitLogReadHandler
 {
-    static final String IGNORE_REPLAY_ERRORS_PROPERTY = "cassandra.commitlog.ignorereplayerrors";
+    @VisibleForTesting
+    public static long MAX_OUTSTANDING_REPLAY_BYTES = Long.getLong("cassandra.commitlog_max_outstanding_replay_bytes", 1024 * 1024 * 64);
+    @VisibleForTesting
+    public static MutationInitiator mutationInitiator = new MutationInitiator();
+    static final String IGNORE_REPLAY_ERRORS_PROPERTY = Config.PROPERTY_PREFIX + "commitlog.ignorereplayerrors";
     private static final Logger logger = LoggerFactory.getLogger(CommitLogReplayer.class);
-    private static final int MAX_OUTSTANDING_REPLAY_COUNT = Integer.getInteger("cassandra.commitlog_max_outstanding_replay_count", 1024);
-    private static final int LEGACY_END_OF_SEGMENT_MARKER = 0;
+    private static final int MAX_OUTSTANDING_REPLAY_COUNT = Integer.getInteger(Config.PROPERTY_PREFIX + "commitlog_max_outstanding_replay_count", 1024);
 
-    private final Set<Keyspace> keyspacesRecovered;
-    private final List<Future<?>> futures;
-    private final Map<UUID, AtomicInteger> invalidMutations;
+    private final Set<Keyspace> keyspacesReplayed;
+    private final Queue<Future<Integer>> futures;
+
     private final AtomicInteger replayedCount;
-    private final Map<UUID, IntervalSet<ReplayPosition>> cfPersisted;
-    private final ReplayPosition globalPosition;
-    private final CRC32 checksum;
-    private byte[] buffer;
-    private byte[] uncompressedBuffer;
+    private final Map<UUID, IntervalSet<CommitLogPosition>> cfPersisted;
+    private final CommitLogPosition globalPosition;
+
+    // Used to throttle speed of replay of mutations if we pass the max outstanding count
+    private long pendingMutationBytes = 0;
 
     private final ReplayFilter replayFilter;
     private final CommitLogArchiver archiver;
 
-    CommitLogReplayer(CommitLog commitLog, ReplayPosition globalPosition, Map<UUID, IntervalSet<ReplayPosition>> cfPersisted, ReplayFilter replayFilter)
+    @VisibleForTesting
+    protected CommitLogReader commitLogReader;
+
+    CommitLogReplayer(CommitLog commitLog,
+                      CommitLogPosition globalPosition,
+                      Map<UUID, IntervalSet<CommitLogPosition>> cfPersisted,
+                      ReplayFilter replayFilter)
     {
-        this.keyspacesRecovered = new NonBlockingHashSet<Keyspace>();
-        this.futures = new ArrayList<Future<?>>();
-        this.buffer = new byte[4096];
-        this.uncompressedBuffer = new byte[4096];
-        this.invalidMutations = new HashMap<UUID, AtomicInteger>();
+        this.keyspacesReplayed = new NonBlockingHashSet<Keyspace>();
+        this.futures = new ArrayDeque<Future<Integer>>();
         // count the number of replayed mutation. We don't really care about atomicity, but we need it to be a reference.
         this.replayedCount = new AtomicInteger();
-        this.checksum = new CRC32();
         this.cfPersisted = cfPersisted;
         this.globalPosition = globalPosition;
         this.replayFilter = replayFilter;
         this.archiver = commitLog.archiver;
+        this.commitLogReader = new CommitLogReader();
     }
 
     public static CommitLogReplayer construct(CommitLog commitLog, UUID localHostId)
     {
         // compute per-CF and global replay intervals
-        Map<UUID, IntervalSet<ReplayPosition>> cfPersisted = new HashMap<>();
+        Map<UUID, IntervalSet<CommitLogPosition>> cfPersisted = new HashMap<>();
         ReplayFilter replayFilter = ReplayFilter.create();
+
         for (ColumnFamilyStore cfs : ColumnFamilyStore.all())
         {
             // but, if we've truncated the cf in question, then we need to need to start replay after the truncation
-            ReplayPosition truncatedAt = SystemKeyspace.getTruncatedPosition(cfs.metadata.cfId);
+            CommitLogPosition truncatedAt = SystemKeyspace.getTruncatedPosition(cfs.metadata.cfId);
             if (truncatedAt != null)
             {
-                // Point in time restore is taken to mean that the tables need to be recovered even if they were
+                // Point in time restore is taken to mean that the tables need to be replayed even if they were
                 // deleted at a later point in time. Any truncation record after that point must thus be cleared prior
-                // to recovery (CASSANDRA-9195).
+                // to replay (CASSANDRA-9195).
                 long restoreTime = commitLog.archiver.restorePointInTime;
                 long truncatedTime = SystemKeyspace.getTruncatedAt(cfs.metadata.cfId);
                 if (truncatedTime > restoreTime)
@@ -128,66 +119,114 @@
                 }
             }
 
-            IntervalSet<ReplayPosition> filter = persistedIntervals(cfs.getLiveSSTables(), truncatedAt, localHostId);
+            IntervalSet<CommitLogPosition> filter = persistedIntervals(cfs.getLiveSSTables(), truncatedAt, localHostId);
             cfPersisted.put(cfs.metadata.cfId, filter);
         }
-        ReplayPosition globalPosition = firstNotCovered(cfPersisted.values());
+        CommitLogPosition globalPosition = firstNotCovered(cfPersisted.values());
         logger.debug("Global replay position is {} from columnfamilies {}", globalPosition, FBUtilities.toString(cfPersisted));
         return new CommitLogReplayer(commitLog, globalPosition, cfPersisted, replayFilter);
     }
 
-    private static boolean shouldSkip(File file) throws IOException, ConfigurationException
+    public void replayPath(File file, boolean tolerateTruncation) throws IOException
     {
-        CommitLogDescriptor desc = CommitLogDescriptor.fromFileName(file.getName());
-        if (desc.version < CommitLogDescriptor.VERSION_21)
-        {
-            return false;
-        }
-        try(ChannelProxy channel = new ChannelProxy(file);
-            RandomAccessReader reader = RandomAccessReader.open(channel))
-        {
-            CommitLogDescriptor.readHeader(reader);
-            int end = reader.readInt();
-            long filecrc = reader.readInt() & 0xffffffffL;
-            return end == 0 && filecrc == 0;
-        }
+        commitLogReader.readCommitLogSegment(this, file, globalPosition, CommitLogReader.ALL_MUTATIONS, tolerateTruncation);
     }
 
-    private static List<File> filterCommitLogFiles(File[] toFilter)
+    public void replayFiles(File[] clogs) throws IOException
     {
-        List<File> filtered = new ArrayList<>(toFilter.length);
-        for (File file: toFilter)
-        {
-            try
-            {
-                if (shouldSkip(file))
-                {
-                    logger.info("Skipping playback of empty log: {}", file.getName());
-                }
-                else
-                {
-                    filtered.add(file);
-                }
-            }
-            catch (Exception e)
-            {
-                // let recover deal with it
-                filtered.add(file);
-            }
-        }
-
-        return filtered;
+        commitLogReader.readAllFiles(this, clogs, globalPosition);
     }
 
-    public void recover(File[] clogs) throws IOException
+    /**
+     * Flushes all keyspaces associated with this replayer in parallel, blocking until their flushes are complete.
+     * @return the number of mutations replayed
+     */
+    public int blockForWrites()
     {
-        List<File> filteredLogs = filterCommitLogFiles(clogs);
+        for (Map.Entry<UUID, AtomicInteger> entry : commitLogReader.getInvalidMutations())
+            logger.warn("Skipped {} mutations from unknown (probably removed) CF with id {}", entry.getValue(), entry.getKey());
 
-        int i = 0;
-        for (File clog: filteredLogs)
+        // wait for all the writes to finish on the mutation stage
+        FBUtilities.waitOnFutures(futures);
+        logger.trace("Finished waiting on mutations from recovery");
+
+        // flush replayed keyspaces
+        futures.clear();
+        boolean flushingSystem = false;
+
+        List<Future<?>> futures = new ArrayList<Future<?>>();
+        for (Keyspace keyspace : keyspacesReplayed)
         {
-            i++;
-            recover(clog, i == filteredLogs.size());
+            if (keyspace.getName().equals(SchemaConstants.SYSTEM_KEYSPACE_NAME))
+                flushingSystem = true;
+
+            futures.addAll(keyspace.flush());
+        }
+
+        // also flush batchlog incase of any MV updates
+        if (!flushingSystem)
+            futures.add(Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).forceFlush());
+
+        FBUtilities.waitOnFutures(futures);
+
+        return replayedCount.get();
+    }
+
+    /*
+     * Wrapper around initiating mutations read from the log to make it possible
+     * to spy on initiated mutations for test
+     */
+    @VisibleForTesting
+    public static class MutationInitiator
+    {
+        protected Future<Integer> initiateMutation(final Mutation mutation,
+                                                   final long segmentId,
+                                                   final int serializedSize,
+                                                   final int entryLocation,
+                                                   final CommitLogReplayer commitLogReplayer)
+        {
+            Runnable runnable = new WrappedRunnable()
+            {
+                public void runMayThrow()
+                {
+                    if (Schema.instance.getKSMetaData(mutation.getKeyspaceName()) == null)
+                        return;
+                    if (commitLogReplayer.pointInTimeExceeded(mutation))
+                        return;
+
+                    final Keyspace keyspace = Keyspace.open(mutation.getKeyspaceName());
+
+                    // Rebuild the mutation, omitting column families that
+                    //    a) the user has requested that we ignore,
+                    //    b) have already been flushed,
+                    // or c) are part of a cf that was dropped.
+                    // Keep in mind that the cf.name() is suspect. do every thing based on the cfid instead.
+                    Mutation newMutation = null;
+                    for (PartitionUpdate update : commitLogReplayer.replayFilter.filter(mutation))
+                    {
+                        if (Schema.instance.getCF(update.metadata().cfId) == null)
+                            continue; // dropped
+
+                        // replay if current segment is newer than last flushed one or,
+                        // if it is the last known segment, if we are after the commit log segment position
+                        if (commitLogReplayer.shouldReplay(update.metadata().cfId, new CommitLogPosition(segmentId, entryLocation)))
+                        {
+                            if (newMutation == null)
+                                newMutation = new Mutation(mutation.getKeyspaceName(), mutation.key());
+                            newMutation.add(update);
+                            commitLogReplayer.replayedCount.incrementAndGet();
+                        }
+                    }
+                    if (newMutation != null)
+                    {
+                        assert !newMutation.isEmpty();
+
+                        Keyspace.open(newMutation.getKeyspaceName()).apply(newMutation, false, true, false);
+                        commitLogReplayer.keyspacesReplayed.add(keyspace);
+                    }
+                }
+            };
+            return StageManager.getStage(Stage.MUTATION).submit(runnable, serializedSize);
         }
     }
 
@@ -195,11 +234,11 @@
      * A set of known safe-to-discard commit log replay positions, based on
      * the range covered by on disk sstables and those prior to the most recent truncation record
      */
-    public static IntervalSet<ReplayPosition> persistedIntervals(Iterable<SSTableReader> onDisk,
-                                                                    ReplayPosition truncatedAt,
+    public static IntervalSet<CommitLogPosition> persistedIntervals(Iterable<SSTableReader> onDisk,
+                                                                    CommitLogPosition truncatedAt,
                                                                     UUID localhostId)
     {
-        IntervalSet.Builder<ReplayPosition> builder = new IntervalSet.Builder<>();
+        IntervalSet.Builder<CommitLogPosition> builder = new IntervalSet.Builder<>();
         List<String> skippedSSTables = new ArrayList<>();
         for (SSTableReader reader : onDisk)
         {
@@ -216,7 +255,7 @@
         }
 
         if (truncatedAt != null)
-            builder.add(ReplayPosition.NONE, truncatedAt);
+            builder.add(CommitLogPosition.NONE, truncatedAt);
         return builder.build();
     }
 
@@ -232,77 +271,14 @@
      *   succeeded. The chances of this happening are at most very low, and if the assumption does prove to be
      *   incorrect during replay there is little chance that the affected deployment is in production.
      */
-    public static ReplayPosition firstNotCovered(Collection<IntervalSet<ReplayPosition>> ranges)
+    public static CommitLogPosition firstNotCovered(Collection<IntervalSet<CommitLogPosition>> ranges)
     {
         return ranges.stream()
-                .map(intervals -> Iterables.getFirst(intervals.ends(), ReplayPosition.NONE))
+                .map(intervals -> Iterables.getFirst(intervals.ends(), CommitLogPosition.NONE))
                 .min(Ordering.natural())
                 .get(); // iteration is per known-CF, there must be at least one.
     }
 
-    public int blockForWrites()
-    {
-        for (Map.Entry<UUID, AtomicInteger> entry : invalidMutations.entrySet())
-            logger.warn(String.format("Skipped %d mutations from unknown (probably removed) CF with id %s", entry.getValue().intValue(), entry.getKey()));
-
-        // wait for all the writes to finish on the mutation stage
-        FBUtilities.waitOnFutures(futures);
-        logger.trace("Finished waiting on mutations from recovery");
-
-        // flush replayed keyspaces
-        futures.clear();
-        boolean flushingSystem = false;
-        for (Keyspace keyspace : keyspacesRecovered)
-        {
-            if (keyspace.getName().equals(SystemKeyspace.NAME))
-                flushingSystem = true;
-
-            futures.addAll(keyspace.flush());
-        }
-
-        // also flush batchlog incase of any MV updates
-        if (!flushingSystem)
-            futures.add(Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).forceFlush());
-
-        FBUtilities.waitOnFutures(futures);
-        return replayedCount.get();
-    }
-
-    private int readSyncMarker(CommitLogDescriptor descriptor, int offset, RandomAccessReader reader, boolean tolerateTruncation) throws IOException
-    {
-        if (offset > reader.length() - CommitLogSegment.SYNC_MARKER_SIZE)
-        {
-            // There was no room in the segment to write a final header. No data could be present here.
-            return -1;
-        }
-        reader.seek(offset);
-        CRC32 crc = new CRC32();
-        updateChecksumInt(crc, (int) (descriptor.id & 0xFFFFFFFFL));
-        updateChecksumInt(crc, (int) (descriptor.id >>> 32));
-        updateChecksumInt(crc, (int) reader.getPosition());
-        int end = reader.readInt();
-        long filecrc = reader.readInt() & 0xffffffffL;
-        if (crc.getValue() != filecrc)
-        {
-            if (end != 0 || filecrc != 0)
-            {
-                handleReplayError(false, null,
-                                  "Encountered bad header at position %d of commit log %s, with invalid CRC. " +
-                                  "The end of segment marker should be zero.",
-                                  offset, reader.getPath());
-            }
-            return -1;
-        }
-        else if (end < offset || end > reader.length())
-        {
-            handleReplayError(tolerateTruncation, null,
-                            "Encountered bad header at position %d of commit log %s, with bad position but valid CRC",
-                              offset, reader.getPath());
-            return -1;
-        }
-        return end;
-    }
-
     abstract static class ReplayFilter
     {
         public abstract Iterable<PartitionUpdate> filter(Mutation mutation);
@@ -318,7 +294,7 @@
             Multimap<String, String> toReplay = HashMultimap.create();
             for (String rawPair : System.getProperty("cassandra.replayList").split(","))
             {
-                String[] pair = rawPair.trim().split("\\.");
+                String[] pair = StringUtils.split(rawPair.trim(), '.');
                 if (pair.length != 2)
                     throw new IllegalArgumentException("Each table to be replayed must be fully qualified with keyspace name, e.g., 'system.peers'");
 
@@ -384,350 +360,11 @@
      *
      * @return true iff replay is necessary
      */
-    private boolean shouldReplay(UUID cfId, ReplayPosition position)
+    private boolean shouldReplay(UUID cfId, CommitLogPosition position)
     {
         return !cfPersisted.get(cfId).contains(position);
     }
 
-    @SuppressWarnings("resource")
-    public void recover(File file, boolean tolerateTruncation) throws IOException
-    {
-        CommitLogDescriptor desc = CommitLogDescriptor.fromFileName(file.getName());
-        try(ChannelProxy channel = new ChannelProxy(file);
-            RandomAccessReader reader = RandomAccessReader.open(channel))
-        {
-            if (desc.version < CommitLogDescriptor.VERSION_21)
-            {
-                if (logAndCheckIfShouldSkip(file, desc))
-                    return;
-                if (globalPosition.segment == desc.id)
-                    reader.seek(globalPosition.position);
-                replaySyncSection(reader, (int) reader.length(), desc, desc.fileName(), tolerateTruncation);
-                return;
-            }
-
-            final long segmentId = desc.id;
-            try
-            {
-                desc = CommitLogDescriptor.readHeader(reader);
-            }
-            catch (IOException e)
-            {
-                desc = null;
-            }
-            if (desc == null) {
-                // Presumably a failed CRC or other IO error occurred, which may be ok if it's the last segment
-                // where we tolerate (and expect) truncation
-                handleReplayError(tolerateTruncation, null, "Could not read commit log descriptor in file %s", file);
-                return;
-            }
-            if (segmentId != desc.id)
-            {
-                handleReplayError(false, null,"Segment id mismatch (filename %d, descriptor %d) in file %s", segmentId, desc.id, file);
-                // continue processing if ignored.
-            }
-
-            if (logAndCheckIfShouldSkip(file, desc))
-                return;
-
-            ICompressor compressor = null;
-            if (desc.compression != null)
-            {
-                try
-                {
-                    compressor = CompressionParams.createCompressor(desc.compression);
-                }
-                catch (ConfigurationException e)
-                {
-                    handleReplayError(false, null, "Unknown compression: %s", e.getMessage());
-                    return;
-                }
-            }
-
-            assert reader.length() <= Integer.MAX_VALUE;
-            int end = (int) reader.getFilePointer();
-            int replayEnd = end;
-
-            while ((end = readSyncMarker(desc, end, reader, tolerateTruncation)) >= 0)
-            {
-                int replayPos = replayEnd + CommitLogSegment.SYNC_MARKER_SIZE;
-
-                if (logger.isTraceEnabled())
-                    logger.trace("Replaying {} between {} and {}", file, reader.getFilePointer(), end);
-                if (compressor != null)
-                {
-                    int uncompressedLength = reader.readInt();
-                    replayEnd = replayPos + uncompressedLength;
-                }
-                else
-                {
-                    replayEnd = end;
-                }
-
-                if (segmentId == globalPosition.segment && replayEnd < globalPosition.position)
-                    // Skip over flushed section.
-                    continue;
-
-                FileDataInput sectionReader = reader;
-                String errorContext = desc.fileName();
-                // In the uncompressed case the last non-fully-flushed section can be anywhere in the file.
-                boolean tolerateErrorsInSection = tolerateTruncation;
-                if (compressor != null)
-                {
-                    // In the compressed case we know if this is the last section.
-                    tolerateErrorsInSection &= end == reader.length() || end < 0;
-
-                    int start = (int) reader.getFilePointer();
-                    try
-                    {
-                        int compressedLength = end - start;
-                        if (logger.isTraceEnabled())
-                            logger.trace("Decompressing {} between replay positions {} and {}",
-                                         file,
-                                         replayPos,
-                                         replayEnd);
-                        if (compressedLength > buffer.length)
-                            buffer = new byte[(int) (1.2 * compressedLength)];
-                        reader.readFully(buffer, 0, compressedLength);
-                        int uncompressedLength = replayEnd - replayPos;
-                        if (uncompressedLength > uncompressedBuffer.length)
-                            uncompressedBuffer = new byte[(int) (1.2 * uncompressedLength)];
-                        compressedLength = compressor.uncompress(buffer, 0, compressedLength, uncompressedBuffer, 0);
-                        sectionReader = new FileSegmentInputStream(ByteBuffer.wrap(uncompressedBuffer), reader.getPath(), replayPos);
-                        errorContext = "compressed section at " + start + " in " + errorContext;
-                    }
-                    catch (IOException | ArrayIndexOutOfBoundsException e)
-                    {
-                        handleReplayError(tolerateErrorsInSection, e,
-                                          "Unexpected exception decompressing section at %d",
-                                          start);
-                        continue;
-                    }
-                }
-
-                if (!replaySyncSection(sectionReader, replayEnd, desc, errorContext, tolerateErrorsInSection))
-                    break;
-            }
-            logger.debug("Finished reading {}", file);
-        }
-    }
-
-    public boolean logAndCheckIfShouldSkip(File file, CommitLogDescriptor desc)
-    {
-        logger.debug("Replaying {} (CL version {}, messaging version {}, compression {})",
-                    file.getPath(),
-                    desc.version,
-                    desc.getMessagingVersion(),
-                    desc.compression);
-
-        if (globalPosition.segment > desc.id)
-        {
-            logger.trace("skipping replay of fully-flushed {}", file);
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Replays a sync section containing a list of mutations.
-     *
-     * @return Whether replay should continue with the next section.
-     */
-    private boolean replaySyncSection(FileDataInput reader, int end, CommitLogDescriptor desc, String errorContext, boolean tolerateErrors) throws IOException
-    {
-         /* read the logs populate Mutation and apply */
-        while (reader.getFilePointer() < end && !reader.isEOF())
-        {
-            long mutationStart = reader.getFilePointer();
-            if (logger.isTraceEnabled())
-                logger.trace("Reading mutation at {}", mutationStart);
-
-            long claimedCRC32;
-            int serializedSize;
-            try
-            {
-                // We rely on reading serialized size == 0 (LEGACY_END_OF_SEGMENT_MARKER) to identify the end
-                // of a segment, which happens naturally due to the 0 padding of the empty segment on creation.
-                // However, it's possible with 2.1 era commitlogs that the last mutation ended less than 4 bytes
-                // from the end of the file, which means that we'll be unable to read an a full int and instead
-                // read an EOF here
-                if(end - reader.getFilePointer() < 4)
-                {
-                    logger.trace("Not enough bytes left for another mutation in this CommitLog segment, continuing");
-                    return false;
-                }
-
-                // any of the reads may hit EOF
-                serializedSize = reader.readInt();
-                if (serializedSize == LEGACY_END_OF_SEGMENT_MARKER)
-                {
-                    logger.trace("Encountered end of segment marker at {}", reader.getFilePointer());
-                    return false;
-                }
-
-                // Mutation must be at LEAST 10 bytes:
-                // 3 each for a non-empty Keyspace and Key (including the
-                // 2-byte length from writeUTF/writeWithShortLength) and 4 bytes for column count.
-                // This prevents CRC by being fooled by special-case garbage in the file; see CASSANDRA-2128
-                if (serializedSize < 10)
-                {
-                    handleReplayError(tolerateErrors, null,
-                                      "Invalid mutation size %d at %d in %s",
-                                      serializedSize, mutationStart, errorContext);
-                    return false;
-                }
-
-                long claimedSizeChecksum;
-                if (desc.version < CommitLogDescriptor.VERSION_21)
-                    claimedSizeChecksum = reader.readLong();
-                else
-                    claimedSizeChecksum = reader.readInt() & 0xffffffffL;
-                checksum.reset();
-                if (desc.version < CommitLogDescriptor.VERSION_20)
-                    checksum.update(serializedSize);
-                else
-                    updateChecksumInt(checksum, serializedSize);
-
-                if (checksum.getValue() != claimedSizeChecksum)
-                {
-                    handleReplayError(tolerateErrors, null,
-                                      "Mutation size checksum failure at %d in %s",
-                                      mutationStart, errorContext);
-                    return false;
-                }
-                // ok.
-
-                if (serializedSize > buffer.length)
-                    buffer = new byte[(int) (1.2 * serializedSize)];
-                reader.readFully(buffer, 0, serializedSize);
-                if (desc.version < CommitLogDescriptor.VERSION_21)
-                    claimedCRC32 = reader.readLong();
-                else
-                    claimedCRC32 = reader.readInt() & 0xffffffffL;
-            }
-            catch (EOFException eof)
-            {
-                handleReplayError(tolerateErrors, eof,
-                                  "Unexpected end of segment",
-                                  mutationStart, errorContext);
-                return false; // last CL entry didn't get completely written. that's ok.
-            }
-
-            checksum.update(buffer, 0, serializedSize);
-            if (claimedCRC32 != checksum.getValue())
-            {
-                handleReplayError(tolerateErrors, null,
-                                  "Mutation checksum failure at %d in %s",
-                                  mutationStart, errorContext);
-                continue;
-            }
-            replayMutation(buffer, serializedSize, (int) reader.getFilePointer(), desc);
-        }
-        return true;
-    }
-
-    /**
-     * Deserializes and replays a commit log entry.
-     */
-    void replayMutation(byte[] inputBuffer, int size,
-            final int entryLocation, final CommitLogDescriptor desc) throws IOException
-    {
-
-        final Mutation mutation;
-        try (RebufferingInputStream bufIn = new DataInputBuffer(inputBuffer, 0, size))
-        {
-            mutation = Mutation.serializer.deserialize(bufIn,
-                                                       desc.getMessagingVersion(),
-                                                       SerializationHelper.Flag.LOCAL);
-            // doublecheck that what we read is [still] valid for the current schema
-            for (PartitionUpdate upd : mutation.getPartitionUpdates())
-                upd.validate();
-        }
-        catch (UnknownColumnFamilyException ex)
-        {
-            if (ex.cfId == null)
-                return;
-            AtomicInteger i = invalidMutations.get(ex.cfId);
-            if (i == null)
-            {
-                i = new AtomicInteger(1);
-                invalidMutations.put(ex.cfId, i);
-            }
-            else
-                i.incrementAndGet();
-            return;
-        }
-        catch (Throwable t)
-        {
-            JVMStabilityInspector.inspectThrowable(t);
-            File f = File.createTempFile("mutation", "dat");
-
-            try (DataOutputStream out = new DataOutputStream(new FileOutputStream(f)))
-            {
-                out.write(inputBuffer, 0, size);
-            }
-
-            // Checksum passed so this error can't be permissible.
-            handleReplayError(false, t,
-                              "Unexpected error deserializing mutation; saved to %s.  " +
-                              "This may be caused by replaying a mutation against a table with the same name but incompatible schema.",
-                              f.getAbsolutePath(),
-                              t);
-            return;
-        }
-
-        if (logger.isTraceEnabled())
-            logger.trace("replaying mutation for {}.{}: {}", mutation.getKeyspaceName(), mutation.key(), "{" + StringUtils.join(mutation.getPartitionUpdates().iterator(), ", ") + "}");
-
-        Runnable runnable = new WrappedRunnable()
-        {
-            public void runMayThrow()
-            {
-                if (Schema.instance.getKSMetaData(mutation.getKeyspaceName()) == null)
-                    return;
-                if (pointInTimeExceeded(mutation))
-                    return;
-
-                final Keyspace keyspace = Keyspace.open(mutation.getKeyspaceName());
-
-                // Rebuild the mutation, omitting column families that
-                //    a) the user has requested that we ignore,
-                //    b) have already been flushed,
-                // or c) are part of a cf that was dropped.
-                // Keep in mind that the cf.name() is suspect. do every thing based on the cfid instead.
-                Mutation newMutation = null;
-                for (PartitionUpdate update : replayFilter.filter(mutation))
-                {
-                    if (Schema.instance.getCF(update.metadata().cfId) == null)
-                        continue; // dropped
-
-                    // replay if current segment is newer than last flushed one or,
-                    // if it is the last known segment, if we are after the replay position
-                    if (shouldReplay(update.metadata().cfId, new ReplayPosition(desc.id, entryLocation)))
-                    {
-                        if (newMutation == null)
-                            newMutation = new Mutation(mutation.getKeyspaceName(), mutation.key());
-                        newMutation.add(update);
-                        replayedCount.incrementAndGet();
-                    }
-                }
-                if (newMutation != null)
-                {
-                    assert !newMutation.isEmpty();
-
-                    Keyspace.open(newMutation.getKeyspaceName()).apply(newMutation, false, true, false);
-                    keyspacesRecovered.add(keyspace);
-                }
-            }
-        };
-        futures.add(StageManager.getStage(Stage.MUTATION).submit(runnable));
-        if (futures.size() > MAX_OUTSTANDING_REPLAY_COUNT)
-        {
-            FBUtilities.waitOnFutures(futures);
-            futures.clear();
-        }
-    }
-
     protected boolean pointInTimeExceeded(Mutation fm)
     {
         long restoreTarget = archiver.restorePointInTime;
@@ -740,21 +377,47 @@
         return false;
     }
 
-    static void handleReplayError(boolean permissible, Throwable t, String message, Object... messageArgs) throws IOException
+    public void handleMutation(Mutation m, int size, int entryLocation, CommitLogDescriptor desc)
     {
-        String msg = String.format(message, messageArgs);
-        IOException e = new CommitLogReplayException(msg, t);
-        if (permissible)
-            logger.error("Ignoring commit log replay error likely due to incomplete flush to disk", e);
+        pendingMutationBytes += size;
+        futures.offer(mutationInitiator.initiateMutation(m,
+                                                         desc.id,
+                                                         size,
+                                                         entryLocation,
+                                                         this));
+        // If there are finished mutations, or too many outstanding bytes/mutations
+        // drain the futures in the queue
+        while (futures.size() > MAX_OUTSTANDING_REPLAY_COUNT
+               || pendingMutationBytes > MAX_OUTSTANDING_REPLAY_BYTES
+               || (!futures.isEmpty() && futures.peek().isDone()))
+        {
+            pendingMutationBytes -= FBUtilities.waitOnFuture(futures.poll());
+        }
+    }
+
+    public boolean shouldSkipSegmentOnError(CommitLogReadException exception) throws IOException
+    {
+        if (exception.permissible)
+            logger.error("Ignoring commit log replay error likely due to incomplete flush to disk", exception);
         else if (Boolean.getBoolean(IGNORE_REPLAY_ERRORS_PROPERTY))
-            logger.error("Ignoring commit log replay error", e);
-        else if (!CommitLog.handleCommitError("Failed commit log replay", e))
+            logger.error("Ignoring commit log replay error", exception);
+        else if (!CommitLog.handleCommitError("Failed commit log replay", exception))
         {
             logger.error("Replay stopped. If you wish to override this error and continue starting the node ignoring " +
                          "commit log replay problems, specify -D" + IGNORE_REPLAY_ERRORS_PROPERTY + "=true " +
                          "on the command line");
-            throw e;
+            throw new CommitLogReplayException(exception.getMessage(), exception);
         }
+        return false;
+    }
+
+    /**
+     * The logic for whether or not we throw on an error is identical for the replayer between recoverable or non.
+     */
+    public void handleUnrecoverableError(CommitLogReadException exception) throws IOException
+    {
+        // Don't care about return value, use this simply to throw exception as appropriate.
+        shouldSkipSegmentOnError(exception);
     }
 
     @SuppressWarnings("serial")
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java
index b803d88..af25b4a 100644
--- a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java
@@ -22,29 +22,19 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.file.StandardOpenOption;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.LockSupport;
 import java.util.zip.CRC32;
 
-import com.codahale.metrics.Timer;
-
 import org.cliffc.high_scale_lib.NonBlockingHashMap;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.config.Schema;
+import com.codahale.metrics.Timer;
+import org.apache.cassandra.config.*;
 import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.commitlog.CommitLog.Configuration;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.io.FSWriteError;
 import org.apache.cassandra.io.util.FileUtils;
@@ -62,9 +52,17 @@
  */
 public abstract class CommitLogSegment
 {
-    private static final Logger logger = LoggerFactory.getLogger(CommitLogSegment.class);
-
     private final static long idBase;
+
+    private CDCState cdcState = CDCState.PERMITTED;
+    public enum CDCState
+    {
+        PERMITTED,
+        FORBIDDEN,
+        CONTAINS
+    }
+    Object cdcStateLock = new Object();
+
     private final static AtomicInteger nextId = new AtomicInteger(1);
     private static long replayLimitId;
     static
@@ -120,15 +118,21 @@
     final FileChannel channel;
     final int fd;
 
-    ByteBuffer buffer;
+    protected final AbstractCommitLogSegmentManager manager;
 
-    final CommitLog commitLog;
+    ByteBuffer buffer;
+    private volatile boolean headerWritten;
+
     public final CommitLogDescriptor descriptor;
 
-    static CommitLogSegment createSegment(CommitLog commitLog, Runnable onClose)
+    static CommitLogSegment createSegment(CommitLog commitLog, AbstractCommitLogSegmentManager manager)
     {
-        return commitLog.configuration.useCompression() ? new CompressedSegment(commitLog, onClose)
-                                                        : new MemoryMappedSegment(commitLog);
+        Configuration config = commitLog.configuration;
+        CommitLogSegment segment = config.useEncryption() ? new EncryptedSegment(commitLog, manager)
+                                                          : config.useCompression() ? new CompressedSegment(commitLog, manager)
+                                                                                    : new MemoryMappedSegment(commitLog, manager);
+        segment.writeLogHeader();
+        return segment;
     }
 
     /**
@@ -139,7 +143,8 @@
      */
     static boolean usesBufferPool(CommitLog commitLog)
     {
-        return commitLog.configuration.useCompression();
+        Configuration config = commitLog.configuration;
+        return config.useEncryption() || config.useCompression();
     }
 
     static long getNextId()
@@ -149,15 +154,16 @@
 
     /**
      * Constructs a new segment file.
-     *
-     * @param filePath  if not null, recycles the existing file by renaming it and truncating it to CommitLog.SEGMENT_SIZE.
      */
-    CommitLogSegment(CommitLog commitLog)
+    CommitLogSegment(CommitLog commitLog, AbstractCommitLogSegmentManager manager)
     {
-        this.commitLog = commitLog;
+        this.manager = manager;
+
         id = getNextId();
-        descriptor = new CommitLogDescriptor(id, commitLog.configuration.getCompressorClass());
-        logFile = new File(commitLog.location, descriptor.fileName());
+        descriptor = new CommitLogDescriptor(id,
+                                             commitLog.configuration.getCompressorClass(),
+                                             commitLog.configuration.getEncryptionContext());
+        logFile = new File(manager.storageDirectory, descriptor.fileName());
 
         try
         {
@@ -170,12 +176,27 @@
         }
 
         buffer = createBuffer(commitLog);
-        // write the header
-        CommitLogDescriptor.writeHeader(buffer, descriptor);
+    }
+
+    /**
+     * Deferred writing of the commit log header until subclasses have had a chance to initialize
+     */
+    void writeLogHeader()
+    {
+        CommitLogDescriptor.writeHeader(buffer, descriptor, additionalHeaderParameters());
         endOfBuffer = buffer.capacity();
 
         lastSyncedOffset = lastMarkerOffset = buffer.position();
         allocatePosition.set(lastSyncedOffset + SYNC_MARKER_SIZE);
+        headerWritten = true;
+    }
+
+    /**
+     * Provide any additional header data that should be stored in the {@link CommitLogDescriptor}.
+     */
+    protected Map<String, String> additionalHeaderParameters()
+    {
+        return Collections.<String, String>emptyMap();
     }
 
     abstract ByteBuffer createBuffer(CommitLog commitLog);
@@ -233,6 +254,7 @@
                 assert buffer != null;
                 return prev;
             }
+            LockSupport.parkNanos(1); // ConstantBackoffCAS Algorithm from https://arxiv.org/pdf/1305.5800.pdf
         }
     }
 
@@ -284,6 +306,8 @@
      */
     synchronized void sync(boolean flush)
     {
+        if (!headerWritten)
+            throw new IllegalStateException("commit log header has not been written");
         assert lastMarkerOffset >= lastSyncedOffset : String.format("commit log segment positions are incorrect: last marked = %d, last synced = %d",
                                                                     lastMarkerOffset, lastSyncedOffset);
         // check we have more work to do
@@ -345,8 +369,20 @@
         }
     }
 
+    /**
+     * Create a sync marker to delineate sections of the commit log, typically created on each sync of the file.
+     * The sync marker consists of a file pointer to where the next sync marker should be (effectively declaring the length
+     * of this section), as well as a CRC value.
+     *
+     * @param buffer buffer in which to write out the sync marker.
+     * @param offset Offset into the {@code buffer} at which to write the sync marker.
+     * @param filePos The current position in the target file where the sync marker will be written (most likely different from the buffer position).
+     * @param nextMarker The file position of where the next sync marker should be.
+     */
     protected static void writeSyncMarker(long id, ByteBuffer buffer, int offset, int filePos, int nextMarker)
     {
+        if (filePos > nextMarker)
+            throw new IllegalArgumentException(String.format("commit log sync marker's current file position %d is greater than next file position %d", filePos, nextMarker));
         CRC32 crc = new CRC32();
         updateChecksumInt(crc, (int) (id & 0xFFFFFFFFL));
         updateChecksumInt(crc, (int) (id >>> 32));
@@ -365,22 +401,23 @@
     }
 
     /**
-     * Completely discards a segment file by deleting it. (Potentially blocking operation)
+     * Discards a segment file when the log no longer requires it. The file may be left on disk if the archive script
+     * requires it. (Potentially blocking operation)
      */
     void discard(boolean deleteFile)
     {
         close();
         if (deleteFile)
             FileUtils.deleteWithConfirm(logFile);
-        commitLog.allocator.addSize(-onDiskSize());
+        manager.addSize(-onDiskSize());
     }
 
     /**
-     * @return the current ReplayPosition for this log segment
+     * @return the current CommitLogPosition for this log segment
      */
-    public ReplayPosition getContext()
+    public CommitLogPosition getCurrentCommitLogPosition()
     {
-        return new ReplayPosition(id, allocatePosition.get());
+        return new CommitLogPosition(id, allocatePosition.get());
     }
 
     /**
@@ -480,17 +517,18 @@
      * given context argument is contained in this file, it will only mark the CF as
      * clean if no newer writes have taken place.
      *
-     * @param cfId    the column family ID that is now clean
-     * @param context the optional clean offset
+     * @param cfId           the column family ID that is now clean
+     * @param startPosition  the start of the range that is clean
+     * @param endPosition    the end of the range that is clean
      */
-    public synchronized void markClean(UUID cfId, ReplayPosition startPosition, ReplayPosition endPosition)
+    public synchronized void markClean(UUID cfId, CommitLogPosition startPosition, CommitLogPosition endPosition)
     {
-        if (startPosition.segment > id || endPosition.segment < id)
+        if (startPosition.segmentId > id || endPosition.segmentId < id)
             return;
         if (!cfDirty.containsKey(cfId))
             return;
-        int start = startPosition.segment == id ? startPosition.position : 0;
-        int end = endPosition.segment == id ? endPosition.position : Integer.MAX_VALUE;
+        int start = startPosition.segmentId == id ? startPosition.position : 0;
+        int end = endPosition.segmentId == id ? endPosition.position : Integer.MAX_VALUE;
         cfClean.computeIfAbsent(cfId, k -> new IntegerInterval.Set()).add(start, end);
         removeCleanFromDirty();
     }
@@ -551,14 +589,14 @@
     }
 
     /**
-     * Check to see if a certain ReplayPosition is contained by this segment file.
+     * Check to see if a certain CommitLogPosition is contained by this segment file.
      *
-     * @param   context the replay position to be checked
-     * @return  true if the replay position is contained by this segment file.
+     * @param   context the commit log segment position to be checked
+     * @return  true if the commit log segment position is contained by this segment file.
      */
-    public boolean contains(ReplayPosition context)
+    public boolean contains(CommitLogPosition context)
     {
-        return context.segment == id;
+        return context.segmentId == id;
     }
 
     // For debugging, not fast
@@ -599,14 +637,38 @@
         }
     }
 
+    public CDCState getCDCState()
+    {
+        return cdcState;
+    }
+
+    /**
+     * Change the current cdcState on this CommitLogSegment. There are some restrictions on state transitions and this
+     * method is idempotent.
+     */
+    public void setCDCState(CDCState newState)
+    {
+        if (newState == cdcState)
+            return;
+
+        // Also synchronized in CDCSizeTracker.processNewSegment and .processDiscardedSegment
+        synchronized(cdcStateLock)
+        {
+            if (cdcState == CDCState.CONTAINS && newState != CDCState.CONTAINS)
+                throw new IllegalArgumentException("Cannot transition from CONTAINS to any other state.");
+
+            if (cdcState == CDCState.FORBIDDEN && newState != CDCState.PERMITTED)
+                throw new IllegalArgumentException("Only transition from FORBIDDEN to PERMITTED is allowed.");
+
+            cdcState = newState;
+        }
+    }
+
     /**
      * A simple class for tracking information about the portion of a segment that has been allocated to a log write.
-     * The constructor leaves the fields uninitialized for population by CommitlogManager, so that it can be
-     * stack-allocated by escape analysis in CommitLog.add.
      */
-    static class Allocation
+    protected static class Allocation
     {
-
         private final CommitLogSegment segment;
         private final OpOrder.Group appendOp;
         private final int position;
@@ -642,10 +704,9 @@
             segment.waitForSync(position, waitingOnCommit);
         }
 
-        public ReplayPosition getReplayPosition()
+        public CommitLogPosition getCommitLogPosition()
         {
-            return new ReplayPosition(segment.id, buffer.limit());
+            return new CommitLogPosition(segment.id, buffer.limit());
         }
-
     }
 }
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManager.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManager.java
deleted file mode 100644
index 291616d..0000000
--- a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManager.java
+++ /dev/null
@@ -1,574 +0,0 @@
-/*
- * 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.db.commitlog;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
-
-import org.apache.cassandra.concurrent.NamedThreadFactory;
-import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.config.Schema;
-import org.apache.cassandra.db.ColumnFamilyStore;
-import org.apache.cassandra.db.Keyspace;
-import org.apache.cassandra.db.Mutation;
-import org.apache.cassandra.db.commitlog.CommitLogSegment.Allocation;
-import org.apache.cassandra.io.util.FileUtils;
-import org.apache.cassandra.utils.JVMStabilityInspector;
-import org.apache.cassandra.utils.Pair;
-import org.apache.cassandra.utils.WrappedRunnable;
-import org.apache.cassandra.utils.concurrent.WaitQueue;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Iterables;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.Runnables;
-import com.google.common.util.concurrent.Uninterruptibles;
-
-/**
- * Performs eager-creation of commit log segments in a background thread. All the
- * public methods are thread safe.
- */
-public class CommitLogSegmentManager
-{
-    static final Logger logger = LoggerFactory.getLogger(CommitLogSegmentManager.class);
-
-    /**
-     * Queue of work to be done by the manager thread, also used to wake the thread to perform segment allocation.
-     */
-    private final BlockingQueue<Runnable> segmentManagementTasks = new LinkedBlockingQueue<>();
-
-    /** Segments that are ready to be used. Head of the queue is the one we allocate writes to */
-    private final ConcurrentLinkedQueue<CommitLogSegment> availableSegments = new ConcurrentLinkedQueue<>();
-
-    /** Active segments, containing unflushed data */
-    private final ConcurrentLinkedQueue<CommitLogSegment> activeSegments = new ConcurrentLinkedQueue<>();
-
-    /** The segment we are currently allocating commit log records to */
-    private volatile CommitLogSegment allocatingFrom = null;
-
-    private final WaitQueue hasAvailableSegments = new WaitQueue();
-
-    /**
-     * Tracks commitlog size, in multiples of the segment size.  We need to do this so we can "promise" size
-     * adjustments ahead of actually adding/freeing segments on disk, so that the "evict oldest segment" logic
-     * can see the effect of recycling segments immediately (even though they're really happening asynchronously
-     * on the manager thread, which will take a ms or two).
-     */
-    private final AtomicLong size = new AtomicLong();
-
-    /**
-     * New segment creation is initially disabled because we'll typically get some "free" segments
-     * recycled after log replay.
-     */
-    volatile boolean createReserveSegments = false;
-
-    private Thread managerThread;
-    private volatile boolean run = true;
-    private final CommitLog commitLog;
-
-    CommitLogSegmentManager(final CommitLog commitLog)
-    {
-        this.commitLog = commitLog;
-    }
-
-    void start()
-    {
-        // The run loop for the manager thread
-        Runnable runnable = new WrappedRunnable()
-        {
-            public void runMayThrow() throws Exception
-            {
-                while (true)
-                {
-                    try
-                    {
-                        Runnable task = segmentManagementTasks.poll();
-                        if (task == null)
-                        {
-                            // if we have no more work to do, check if we were requested to exit before starting background tasks
-                            if (!run)
-                                return;
-
-                            // check if we should create a new segment
-                            if (!atSegmentLimit() && availableSegments.isEmpty() && (activeSegments.isEmpty() || createReserveSegments))
-                            {
-                                logger.trace("No segments in reserve; creating a fresh one");
-                                // TODO : some error handling in case we fail to create a new segment
-                                availableSegments.add(CommitLogSegment.createSegment(commitLog, () -> wakeManager()));
-                                hasAvailableSegments.signalAll();
-                            }
-
-                            // flush old Cfs if we're full
-                            long unused = unusedCapacity();
-                            if (unused < 0)
-                            {
-                                List<CommitLogSegment> segmentsToRecycle = new ArrayList<>();
-                                long spaceToReclaim = 0;
-                                for (CommitLogSegment segment : activeSegments)
-                                {
-                                    if (segment == allocatingFrom)
-                                        break;
-                                    segmentsToRecycle.add(segment);
-                                    spaceToReclaim += DatabaseDescriptor.getCommitLogSegmentSize();
-                                    if (spaceToReclaim + unused >= 0)
-                                        break;
-                                }
-                                flushDataFrom(segmentsToRecycle, false);
-                            }
-
-                            try
-                            {
-                                // wait for new work to be provided
-                                task = segmentManagementTasks.take();
-                            }
-                            catch (InterruptedException e)
-                            {
-                                throw new AssertionError();
-                            }
-                        }
-
-                        task.run();
-                    }
-                    catch (Throwable t)
-                    {
-                        if (!CommitLog.handleCommitError("Failed managing commit log segments", t))
-                            return;
-                        // sleep some arbitrary period to avoid spamming CL
-                        Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
-                    }
-                }
-            }
-
-            private boolean atSegmentLimit()
-            {
-                return CommitLogSegment.usesBufferPool(commitLog) && CompressedSegment.hasReachedPoolLimit();
-            }
-
-        };
-
-        run = true;
-
-        managerThread = new Thread(NamedThreadFactory.threadLocalDeallocator(runnable), "COMMIT-LOG-ALLOCATOR");
-        managerThread.start();
-    }
-
-    /**
-     * Reserve space in the current segment for the provided mutation or, if there isn't space available,
-     * create a new segment.
-     *
-     * @return the provided Allocation object
-     */
-    public Allocation allocate(Mutation mutation, int size)
-    {
-        CommitLogSegment segment = allocatingFrom();
-
-        Allocation alloc;
-        while ( null == (alloc = segment.allocate(mutation, size)) )
-        {
-            // failed to allocate, so move to a new segment with enough room
-            advanceAllocatingFrom(segment);
-            segment = allocatingFrom;
-        }
-
-        return alloc;
-    }
-
-    // simple wrapper to ensure non-null value for allocatingFrom; only necessary on first call
-    CommitLogSegment allocatingFrom()
-    {
-        CommitLogSegment r = allocatingFrom;
-        if (r == null)
-        {
-            advanceAllocatingFrom(null);
-            r = allocatingFrom;
-        }
-        return r;
-    }
-
-    /**
-     * Fetches a new segment from the queue, creating a new one if necessary, and activates it
-     */
-    private void advanceAllocatingFrom(CommitLogSegment old)
-    {
-        while (true)
-        {
-            CommitLogSegment next;
-            synchronized (this)
-            {
-                // do this in a critical section so we can atomically remove from availableSegments and add to allocatingFrom/activeSegments
-                // see https://issues.apache.org/jira/browse/CASSANDRA-6557?focusedCommentId=13874432&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-13874432
-                if (allocatingFrom != old)
-                    return;
-                next = availableSegments.poll();
-                if (next != null)
-                {
-                    allocatingFrom = next;
-                    activeSegments.add(next);
-                }
-            }
-
-            if (next != null)
-            {
-                if (old != null)
-                {
-                    // Now we can run the user defined command just after switching to the new commit log.
-                    // (Do this here instead of in the recycle call so we can get a head start on the archive.)
-                    commitLog.archiver.maybeArchive(old);
-
-                    // ensure we don't continue to use the old file; not strictly necessary, but cleaner to enforce it
-                    old.discardUnusedTail();
-                }
-
-                // request that the CL be synced out-of-band, as we've finished a segment
-                commitLog.requestExtraSync();
-                return;
-            }
-
-            // no more segments, so register to receive a signal when not empty
-            WaitQueue.Signal signal = hasAvailableSegments.register(commitLog.metrics.waitingOnSegmentAllocation.time());
-
-            // trigger the management thread; this must occur after registering
-            // the signal to ensure we are woken by any new segment creation
-            wakeManager();
-
-            // check if the queue has already been added to before waiting on the signal, to catch modifications
-            // that happened prior to registering the signal; *then* check to see if we've been beaten to making the change
-            if (!availableSegments.isEmpty() || allocatingFrom != old)
-            {
-                signal.cancel();
-                // if we've been beaten, just stop immediately
-                if (allocatingFrom != old)
-                    return;
-                // otherwise try again, as there should be an available segment
-                continue;
-            }
-
-            // can only reach here if the queue hasn't been inserted into
-            // before we registered the signal, as we only remove items from the queue
-            // after updating allocatingFrom. Can safely block until we are signalled
-            // by the allocator that new segments have been published
-            signal.awaitUninterruptibly();
-        }
-    }
-
-    private void wakeManager()
-    {
-        // put a NO-OP on the queue, to trigger management thread (and create a new segment if necessary)
-        segmentManagementTasks.add(Runnables.doNothing());
-    }
-
-    /**
-     * Switch to a new segment, regardless of how much is left in the current one.
-     *
-     * Flushes any dirty CFs for this segment and any older segments, and then recycles
-     * the segments. This is necessary to avoid resurrecting data during replay if a user
-     * creates a new table with the same name and ID. See CASSANDRA-16986 for more details.
-     */
-    void forceRecycleAll(Iterable<UUID> droppedCfs)
-    {
-        List<CommitLogSegment> segmentsToRecycle = new ArrayList<>(activeSegments);
-        CommitLogSegment last = segmentsToRecycle.get(segmentsToRecycle.size() - 1);
-        advanceAllocatingFrom(last);
-
-        // wait for the commit log modifications
-        last.waitForModifications();
-
-        // make sure the writes have materialized inside of the memtables by waiting for all outstanding writes
-        // on the relevant keyspaces to complete
-        Keyspace.writeOrder.awaitNewBarrier();
-
-        // flush and wait for all CFs that are dirty in segments up-to and including 'last'
-        Future<?> future = flushDataFrom(segmentsToRecycle, true);
-        try
-        {
-            future.get();
-
-            for (CommitLogSegment segment : activeSegments)
-                for (UUID cfId : droppedCfs)
-                    segment.markClean(cfId, ReplayPosition.NONE, segment.getContext());
-
-            // now recycle segments that are unused, as we may not have triggered a discardCompletedSegments()
-            // if the previous active segment was the only one to recycle (since an active segment isn't
-            // necessarily dirty, and we only call dCS after a flush).
-            for (CommitLogSegment segment : activeSegments)
-                if (segment.isUnused())
-                    recycleSegment(segment);
-
-            CommitLogSegment first;
-            if ((first = activeSegments.peek()) != null && first.id <= last.id)
-                logger.error("Failed to force-recycle all segments; at least one segment is still in use with dirty CFs.");
-        }
-        catch (Throwable t)
-        {
-            // for now just log the error and return false, indicating that we failed
-            logger.error("Failed waiting for a forced recycle of in-use commit log segments", t);
-        }
-    }
-
-    /**
-     * Indicates that a segment is no longer in use and that it should be recycled.
-     *
-     * @param segment segment that is no longer in use
-     */
-    void recycleSegment(final CommitLogSegment segment)
-    {
-        boolean archiveSuccess = commitLog.archiver.maybeWaitForArchiving(segment.getName());
-        if (activeSegments.remove(segment))
-        {
-            // if archiving (command) was not successful then leave the file alone. don't delete or recycle.
-            discardSegment(segment, archiveSuccess);
-        }
-        else
-        {
-            logger.warn("segment {} not found in activeSegments queue", segment);
-        }
-    }
-
-    /**
-     * Differs from the above because it can work on any file instead of just existing
-     * commit log segments managed by this manager.
-     *
-     * @param file segment file that is no longer in use.
-     */
-    void recycleSegment(final File file)
-    {
-        // (don't decrease managed size, since this was never a "live" segment)
-        logger.trace("(Unopened) segment {} is no longer needed and will be deleted now", file);
-        FileUtils.deleteWithConfirm(file);
-    }
-
-    /**
-     * Indicates that a segment file should be deleted.
-     *
-     * @param segment segment to be discarded
-     */
-    private void discardSegment(final CommitLogSegment segment, final boolean deleteFile)
-    {
-        logger.trace("Segment {} is no longer active and will be deleted {}", segment, deleteFile ? "now" : "by the archive script");
-
-        segmentManagementTasks.add(new Runnable()
-        {
-            public void run()
-            {
-                segment.discard(deleteFile);
-            }
-        });
-    }
-
-    /**
-     * Adjust the tracked on-disk size. Called by individual segments to reflect writes, allocations and discards.
-     * @param addedSize
-     */
-    void addSize(long addedSize)
-    {
-        size.addAndGet(addedSize);
-    }
-
-    /**
-     * @return the space (in bytes) used by all segment files.
-     */
-    public long onDiskSize()
-    {
-        return size.get();
-    }
-
-    private long unusedCapacity()
-    {
-        long total = DatabaseDescriptor.getTotalCommitlogSpaceInMB() * 1024 * 1024;
-        long currentSize = size.get();
-        logger.trace("Total active commitlog segment space used is {} out of {}", currentSize, total);
-        return total - currentSize;
-    }
-
-    /**
-     * @param name the filename to check
-     * @return true if file is managed by this manager.
-     */
-    public boolean manages(String name)
-    {
-        for (CommitLogSegment segment : Iterables.concat(activeSegments, availableSegments))
-            if (segment.getName().equals(name))
-                return true;
-        return false;
-    }
-
-    /**
-     * Throws a flag that enables the behavior of keeping at least one spare segment
-     * available at all times.
-     */
-    void enableReserveSegmentCreation()
-    {
-        createReserveSegments = true;
-        wakeManager();
-    }
-
-    /**
-     * Force a flush on all CFs that are still dirty in @param segments.
-     *
-     * @return a Future that will finish when all the flushes are complete.
-     */
-    private Future<?> flushDataFrom(List<CommitLogSegment> segments, boolean force)
-    {
-        if (segments.isEmpty())
-            return Futures.immediateFuture(null);
-        final ReplayPosition maxReplayPosition = segments.get(segments.size() - 1).getContext();
-
-        // a map of CfId -> forceFlush() to ensure we only queue one flush per cf
-        final Map<UUID, ListenableFuture<?>> flushes = new LinkedHashMap<>();
-
-        for (CommitLogSegment segment : segments)
-        {
-            for (UUID dirtyCFId : segment.getDirtyCFIDs())
-            {
-                Pair<String,String> pair = Schema.instance.getCF(dirtyCFId);
-                if (pair == null)
-                {
-                    // even though we remove the schema entry before a final flush when dropping a CF,
-                    // it's still possible for a writer to race and finish his append after the flush.
-                    logger.trace("Marking clean CF {} that doesn't exist anymore", dirtyCFId);
-                    segment.markClean(dirtyCFId, ReplayPosition.NONE, segment.getContext());
-                }
-                else if (!flushes.containsKey(dirtyCFId))
-                {
-                    String keyspace = pair.left;
-                    final ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(dirtyCFId);
-                    // can safely call forceFlush here as we will only ever block (briefly) for other attempts to flush,
-                    // no deadlock possibility since switchLock removal
-                    flushes.put(dirtyCFId, force ? cfs.forceFlush() : cfs.forceFlush(maxReplayPosition));
-                }
-            }
-        }
-
-        return Futures.allAsList(flushes.values());
-    }
-
-    /**
-     * Stops CL, for testing purposes. DO NOT USE THIS OUTSIDE OF TESTS.
-     * Only call this after the AbstractCommitLogService is shut down.
-     */
-    public void stopUnsafe(boolean deleteSegments)
-    {
-        logger.trace("CLSM closing and clearing existing commit log segments...");
-        createReserveSegments = false;
-
-        awaitManagementTasksCompletion();
-
-        shutdown();
-        try
-        {
-            awaitTermination();
-        }
-        catch (InterruptedException e)
-        {
-            throw new RuntimeException(e);
-        }
-
-        synchronized (this)
-        {
-            for (CommitLogSegment segment : activeSegments)
-                closeAndDeleteSegmentUnsafe(segment, deleteSegments);
-            activeSegments.clear();
-
-            for (CommitLogSegment segment : availableSegments)
-                closeAndDeleteSegmentUnsafe(segment, deleteSegments);
-            availableSegments.clear();
-        }
-
-        allocatingFrom = null;
-
-        segmentManagementTasks.clear();
-
-        size.set(0L);
-
-        logger.trace("CLSM done with closing and clearing existing commit log segments.");
-    }
-
-    // Used by tests only.
-    void awaitManagementTasksCompletion()
-    {
-        while (!segmentManagementTasks.isEmpty())
-            Thread.yield();
-        // The last management task is not yet complete. Wait a while for it.
-        Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
-        // TODO: If this functionality is required by anything other than tests, signalling must be used to ensure
-        // waiting completes correctly.
-    }
-
-    private static void closeAndDeleteSegmentUnsafe(CommitLogSegment segment, boolean delete)
-    {
-        try
-        {
-            segment.discard(delete);
-        }
-        catch (AssertionError ignored)
-        {
-            // segment file does not exist
-        }
-    }
-
-    /**
-     * Initiates the shutdown process for the management thread.
-     */
-    public void shutdown()
-    {
-        run = false;
-        wakeManager();
-    }
-
-    /**
-     * Returns when the management thread terminates.
-     */
-    public void awaitTermination() throws InterruptedException
-    {
-        managerThread.join();
-
-        for (CommitLogSegment segment : activeSegments)
-            segment.close();
-
-        for (CommitLogSegment segment : availableSegments)
-            segment.close();
-
-        CompressedSegment.shutdown();
-    }
-
-    /**
-     * @return a read-only collection of the active commit log segments
-     */
-    @VisibleForTesting
-    public Collection<CommitLogSegment> getActiveSegments()
-    {
-        return Collections.unmodifiableCollection(activeSegments);
-    }
-
-}
-
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDC.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDC.java
new file mode 100644
index 0000000..a91384f
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDC.java
@@ -0,0 +1,314 @@
+/*
+ * 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.db.commitlog;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.concurrent.*;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.RateLimiter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.commitlog.CommitLogSegment.CDCState;
+import org.apache.cassandra.exceptions.WriteTimeoutException;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.DirectorySizeCalculator;
+import org.apache.cassandra.utils.NoSpamLogger;
+
+public class CommitLogSegmentManagerCDC extends AbstractCommitLogSegmentManager
+{
+    static final Logger logger = LoggerFactory.getLogger(CommitLogSegmentManagerCDC.class);
+    private final CDCSizeTracker cdcSizeTracker;
+
+    public CommitLogSegmentManagerCDC(final CommitLog commitLog, String storageDirectory)
+    {
+        super(commitLog, storageDirectory);
+        cdcSizeTracker = new CDCSizeTracker(this, new File(DatabaseDescriptor.getCDCLogLocation()));
+    }
+
+    @Override
+    void start()
+    {
+        cdcSizeTracker.start();
+        super.start();
+    }
+
+    public void discard(CommitLogSegment segment, boolean delete)
+    {
+        segment.close();
+        addSize(-segment.onDiskSize());
+
+        cdcSizeTracker.processDiscardedSegment(segment);
+
+        if (segment.getCDCState() == CDCState.CONTAINS)
+            FileUtils.renameWithConfirm(segment.logFile.getAbsolutePath(), DatabaseDescriptor.getCDCLogLocation() + File.separator + segment.logFile.getName());
+        else
+        {
+            if (delete)
+                FileUtils.deleteWithConfirm(segment.logFile);
+        }
+    }
+
+    /**
+     * Initiates the shutdown process for the management thread. Also stops the cdc on-disk size calculator executor.
+     */
+    public void shutdown()
+    {
+        cdcSizeTracker.shutdown();
+        super.shutdown();
+    }
+
+    /**
+     * Reserve space in the current segment for the provided mutation or, if there isn't space available,
+     * create a new segment. For CDC mutations, allocation is expected to throw WTE if the segment disallows CDC mutations.
+     *
+     * @param mutation Mutation to allocate in segment manager
+     * @param size total size (overhead + serialized) of mutation
+     * @return the created Allocation object
+     * @throws WriteTimeoutException If segment disallows CDC mutations, we throw WTE
+     */
+    @Override
+    public CommitLogSegment.Allocation allocate(Mutation mutation, int size) throws WriteTimeoutException
+    {
+        CommitLogSegment segment = allocatingFrom();
+        CommitLogSegment.Allocation alloc;
+
+        throwIfForbidden(mutation, segment);
+        while ( null == (alloc = segment.allocate(mutation, size)) )
+        {
+            // Failed to allocate, so move to a new segment with enough room if possible.
+            advanceAllocatingFrom(segment);
+            segment = allocatingFrom();
+
+            throwIfForbidden(mutation, segment);
+        }
+
+        if (mutation.trackedByCDC())
+            segment.setCDCState(CDCState.CONTAINS);
+
+        return alloc;
+    }
+
+    private void throwIfForbidden(Mutation mutation, CommitLogSegment segment) throws WriteTimeoutException
+    {
+        if (mutation.trackedByCDC() && segment.getCDCState() == CDCState.FORBIDDEN)
+        {
+            cdcSizeTracker.submitOverflowSizeRecalculation();
+            NoSpamLogger.log(logger,
+                             NoSpamLogger.Level.WARN,
+                             10,
+                             TimeUnit.SECONDS,
+                             "Rejecting Mutation containing CDC-enabled table. Free up space in {}.",
+                             DatabaseDescriptor.getCDCLogLocation());
+            throw new WriteTimeoutException(WriteType.CDC, ConsistencyLevel.LOCAL_ONE, 0, 1);
+        }
+    }
+
+    /**
+     * Move files to cdc_raw after replay, since recovery will flush to SSTable and these mutations won't be available
+     * in the CL subsystem otherwise.
+     */
+    void handleReplayedSegment(final File file)
+    {
+        logger.trace("Moving (Unopened) segment {} to cdc_raw directory after replay", file);
+        FileUtils.renameWithConfirm(file.getAbsolutePath(), DatabaseDescriptor.getCDCLogLocation() + File.separator + file.getName());
+        cdcSizeTracker.addFlushedSize(file.length());
+    }
+
+    /**
+     * On segment creation, flag whether the segment should accept CDC mutations or not based on the total currently
+     * allocated unflushed CDC segments and the contents of cdc_raw
+     */
+    public CommitLogSegment createSegment()
+    {
+        CommitLogSegment segment = CommitLogSegment.createSegment(commitLog, this);
+        cdcSizeTracker.processNewSegment(segment);
+        return segment;
+    }
+
+    /**
+     * Tracks total disk usage of CDC subsystem, defined by the summation of all unflushed CommitLogSegments with CDC
+     * data in them and all segments archived into cdc_raw.
+     *
+     * Allows atomic increment/decrement of unflushed size, however only allows increment on flushed and requires a full
+     * directory walk to determine any potential deletions by CDC consumer.
+     */
+    private static class CDCSizeTracker extends DirectorySizeCalculator
+    {
+        private final RateLimiter rateLimiter = RateLimiter.create(1000.0 / DatabaseDescriptor.getCDCDiskCheckInterval());
+        private ExecutorService cdcSizeCalculationExecutor;
+        private CommitLogSegmentManagerCDC segmentManager;
+        private volatile long unflushedCDCSize;
+
+        // Used instead of size during walk to remove chance of over-allocation
+        private volatile long sizeInProgress = 0;
+
+        CDCSizeTracker(CommitLogSegmentManagerCDC segmentManager, File path)
+        {
+            super(path);
+            this.segmentManager = segmentManager;
+        }
+
+        /**
+         * Needed for stop/restart during unit tests
+         */
+        public void start()
+        {
+            size = 0;
+            unflushedCDCSize = 0;
+            cdcSizeCalculationExecutor = new ThreadPoolExecutor(1, 1, 1000, TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadPoolExecutor.DiscardPolicy());
+        }
+
+        /**
+         * Synchronous size recalculation on each segment creation/deletion call could lead to very long delays in new
+         * segment allocation, thus long delays in thread signaling to wake waiting allocation / writer threads.
+         *
+         * This can be reached either from the segment management thread in ABstractCommitLogSegmentManager or from the
+         * size recalculation executor, so we synchronize on this object to reduce the race overlap window available for
+         * size to get off.
+         *
+         * Reference DirectorySizerBench for more information about performance of the directory size recalc.
+         */
+        void processNewSegment(CommitLogSegment segment)
+        {
+            // See synchronization in CommitLogSegment.setCDCState
+            synchronized(segment.cdcStateLock)
+            {
+                segment.setCDCState(defaultSegmentSize() + totalCDCSizeOnDisk() > allowableCDCBytes()
+                                    ? CDCState.FORBIDDEN
+                                    : CDCState.PERMITTED);
+                if (segment.getCDCState() == CDCState.PERMITTED)
+                    unflushedCDCSize += defaultSegmentSize();
+            }
+
+            // Take this opportunity to kick off a recalc to pick up any consumer file deletion.
+            submitOverflowSizeRecalculation();
+        }
+
+        void processDiscardedSegment(CommitLogSegment segment)
+        {
+            // See synchronization in CommitLogSegment.setCDCState
+            synchronized(segment.cdcStateLock)
+            {
+                // Add to flushed size before decrementing unflushed so we don't have a window of false generosity
+                if (segment.getCDCState() == CDCState.CONTAINS)
+                    size += segment.onDiskSize();
+                if (segment.getCDCState() != CDCState.FORBIDDEN)
+                    unflushedCDCSize -= defaultSegmentSize();
+            }
+
+            // Take this opportunity to kick off a recalc to pick up any consumer file deletion.
+            submitOverflowSizeRecalculation();
+        }
+
+        private long allowableCDCBytes()
+        {
+            return (long)DatabaseDescriptor.getCDCSpaceInMB() * 1024 * 1024;
+        }
+
+        public void submitOverflowSizeRecalculation()
+        {
+            try
+            {
+                cdcSizeCalculationExecutor.submit(() -> recalculateOverflowSize());
+            }
+            catch (RejectedExecutionException e)
+            {
+                // Do nothing. Means we have one in flight so this req. should be satisfied when it completes.
+            }
+        }
+
+        private void recalculateOverflowSize()
+        {
+            rateLimiter.acquire();
+            calculateSize();
+            CommitLogSegment allocatingFrom = segmentManager.allocatingFrom();
+            if (allocatingFrom.getCDCState() == CDCState.FORBIDDEN)
+                processNewSegment(allocatingFrom);
+        }
+
+        private int defaultSegmentSize()
+        {
+            return DatabaseDescriptor.getCommitLogSegmentSize();
+        }
+
+        private void calculateSize()
+        {
+            try
+            {
+                // The Arrays.stream approach is considerably slower on Windows than linux
+                sizeInProgress = 0;
+                Files.walkFileTree(path.toPath(), this);
+                size = sizeInProgress;
+            }
+            catch (IOException ie)
+            {
+                CommitLog.instance.handleCommitError("Failed CDC Size Calculation", ie);
+            }
+        }
+
+        @Override
+        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
+        {
+            sizeInProgress += attrs.size();
+            return FileVisitResult.CONTINUE;
+        }
+
+        private void addFlushedSize(long toAdd)
+        {
+            size += toAdd;
+        }
+
+        private long totalCDCSizeOnDisk()
+        {
+            return unflushedCDCSize + size;
+        }
+
+        public void shutdown()
+        {
+            cdcSizeCalculationExecutor.shutdown();
+        }
+    }
+
+    /**
+     * Only use for testing / validation that size tracker is working. Not for production use.
+     */
+    @VisibleForTesting
+    public long updateCDCTotalSize()
+    {
+        cdcSizeTracker.submitOverflowSizeRecalculation();
+
+        // Give the update time to run
+        try
+        {
+            Thread.sleep(DatabaseDescriptor.getCDCDiskCheckInterval() + 10);
+        }
+        catch (InterruptedException e) {}
+
+        return cdcSizeTracker.totalCDCSizeOnDisk();
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerStandard.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerStandard.java
new file mode 100644
index 0000000..86e886b
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerStandard.java
@@ -0,0 +1,80 @@
+/*
+ * 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.db.commitlog;
+
+import java.io.File;
+
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.io.util.FileUtils;
+
+public class CommitLogSegmentManagerStandard extends AbstractCommitLogSegmentManager
+{
+    public CommitLogSegmentManagerStandard(final CommitLog commitLog, String storageDirectory)
+    {
+        super(commitLog, storageDirectory);
+    }
+
+    public void discard(CommitLogSegment segment, boolean delete)
+    {
+        segment.close();
+        if (delete)
+            FileUtils.deleteWithConfirm(segment.logFile);
+        addSize(-segment.onDiskSize());
+    }
+
+    /**
+     * Reserve space in the current segment for the provided mutation or, if there isn't space available,
+     * create a new segment. allocate() is blocking until allocation succeeds as it waits on a signal in advanceAllocatingFrom
+     *
+     * @param mutation mutation to allocate space for
+     * @param size total size of mutation (overhead + serialized size)
+     * @return the provided Allocation object
+     */
+    public CommitLogSegment.Allocation allocate(Mutation mutation, int size)
+    {
+        CommitLogSegment segment = allocatingFrom();
+
+        CommitLogSegment.Allocation alloc;
+        while ( null == (alloc = segment.allocate(mutation, size)) )
+        {
+            // failed to allocate, so move to a new segment with enough room
+            advanceAllocatingFrom(segment);
+            segment = allocatingFrom();
+        }
+
+        return alloc;
+    }
+
+    /**
+     * Simply delete untracked segment files w/standard, as it'll be flushed to sstables during recovery
+     *
+     * @param file segment file that is no longer in use.
+     */
+    void handleReplayedSegment(final File file)
+    {
+        // (don't decrease managed size, since this was never a "live" segment)
+        logger.trace("(Unopened) segment {} is no longer needed and will be deleted now", file);
+        FileUtils.deleteWithConfirm(file);
+    }
+
+    public CommitLogSegment createSegment()
+    {
+        return CommitLogSegment.createSegment(commitLog, this);
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentReader.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentReader.java
new file mode 100644
index 0000000..e23a915
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentReader.java
@@ -0,0 +1,368 @@
+/*
+ * 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.db.commitlog;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+import java.util.zip.CRC32;
+import javax.crypto.Cipher;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.AbstractIterator;
+
+import org.apache.cassandra.db.commitlog.EncryptedFileSegmentInputStream.ChunkProvider;
+import org.apache.cassandra.db.commitlog.CommitLogReadHandler.*;
+import org.apache.cassandra.io.FSReadError;
+import org.apache.cassandra.io.compress.ICompressor;
+import org.apache.cassandra.io.util.FileDataInput;
+import org.apache.cassandra.io.util.FileSegmentInputStream;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.schema.CompressionParams;
+import org.apache.cassandra.security.EncryptionUtils;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+import static org.apache.cassandra.db.commitlog.CommitLogSegment.SYNC_MARKER_SIZE;
+import static org.apache.cassandra.utils.FBUtilities.updateChecksumInt;
+
+/**
+ * Read each sync section of a commit log, iteratively.
+ */
+public class CommitLogSegmentReader implements Iterable<CommitLogSegmentReader.SyncSegment>
+{
+    private final CommitLogReadHandler handler;
+    private final CommitLogDescriptor descriptor;
+    private final RandomAccessReader reader;
+    private final Segmenter segmenter;
+    private final boolean tolerateTruncation;
+
+    /**
+     * ending position of the current sync section.
+     */
+    protected int end;
+
+    protected CommitLogSegmentReader(CommitLogReadHandler handler,
+                                     CommitLogDescriptor descriptor,
+                                     RandomAccessReader reader,
+                                     boolean tolerateTruncation)
+    {
+        this.handler = handler;
+        this.descriptor = descriptor;
+        this.reader = reader;
+        this.tolerateTruncation = tolerateTruncation;
+
+        end = (int) reader.getFilePointer();
+        if (descriptor.getEncryptionContext().isEnabled())
+            segmenter = new EncryptedSegmenter(descriptor, reader);
+        else if (descriptor.compression != null)
+            segmenter = new CompressedSegmenter(descriptor, reader);
+        else
+            segmenter = new NoOpSegmenter(reader);
+    }
+
+    public Iterator<SyncSegment> iterator()
+    {
+        return new SegmentIterator();
+    }
+
+    protected class SegmentIterator extends AbstractIterator<CommitLogSegmentReader.SyncSegment>
+    {
+        protected SyncSegment computeNext()
+        {
+            while (true)
+            {
+                try
+                {
+                    final int currentStart = end;
+                    end = readSyncMarker(descriptor, currentStart, reader);
+                    if (end == -1)
+                    {
+                        return endOfData();
+                    }
+                    if (end > reader.length())
+                    {
+                        // the CRC was good (meaning it was good when it was written and still looks legit), but the file is truncated now.
+                        // try to grab and use as much of the file as possible, which might be nothing if the end of the file truly is corrupt
+                        end = (int) reader.length();
+                    }
+                    return segmenter.nextSegment(currentStart + SYNC_MARKER_SIZE, end);
+                }
+                catch(CommitLogSegmentReader.SegmentReadException e)
+                {
+                    try
+                    {
+                        handler.handleUnrecoverableError(new CommitLogReadException(
+                                                    e.getMessage(),
+                                                    CommitLogReadErrorReason.UNRECOVERABLE_DESCRIPTOR_ERROR,
+                                                    !e.invalidCrc && tolerateTruncation));
+                    }
+                    catch (IOException ioe)
+                    {
+                        throw new RuntimeException(ioe);
+                    }
+                }
+                catch (IOException e)
+                {
+                    try
+                    {
+                        boolean tolerateErrorsInSection = tolerateTruncation & segmenter.tolerateSegmentErrors(end, reader.length());
+                        // if no exception is thrown, the while loop will continue
+                        handler.handleUnrecoverableError(new CommitLogReadException(
+                                                    e.getMessage(),
+                                                    CommitLogReadErrorReason.UNRECOVERABLE_DESCRIPTOR_ERROR,
+                                                    tolerateErrorsInSection));
+                    }
+                    catch (IOException ioe)
+                    {
+                        throw new RuntimeException(ioe);
+                    }
+                }
+            }
+        }
+    }
+
+    private int readSyncMarker(CommitLogDescriptor descriptor, int offset, RandomAccessReader reader) throws IOException
+    {
+        if (offset > reader.length() - SYNC_MARKER_SIZE)
+        {
+            // There was no room in the segment to write a final header. No data could be present here.
+            return -1;
+        }
+        reader.seek(offset);
+        CRC32 crc = new CRC32();
+        updateChecksumInt(crc, (int) (descriptor.id & 0xFFFFFFFFL));
+        updateChecksumInt(crc, (int) (descriptor.id >>> 32));
+        updateChecksumInt(crc, (int) reader.getPosition());
+        final int end = reader.readInt();
+        long filecrc = reader.readInt() & 0xffffffffL;
+        if (crc.getValue() != filecrc)
+        {
+            if (end != 0 || filecrc != 0)
+            {
+                String msg = String.format("Encountered bad header at position %d of commit log %s, with invalid CRC. " +
+                             "The end of segment marker should be zero.", offset, reader.getPath());
+                throw new SegmentReadException(msg, true);
+            }
+            return -1;
+        }
+        else if (end < offset || end > reader.length())
+        {
+            String msg = String.format("Encountered bad header at position %d of commit log %s, with bad position but valid CRC", offset, reader.getPath());
+            throw new SegmentReadException(msg, false);
+        }
+        return end;
+    }
+
+    public static class SegmentReadException extends IOException
+    {
+        public final boolean invalidCrc;
+
+        public SegmentReadException(String msg, boolean invalidCrc)
+        {
+            super(msg);
+            this.invalidCrc = invalidCrc;
+        }
+    }
+
+    public static class SyncSegment
+    {
+        /** the 'buffer' to replay commit log data from */
+        public final FileDataInput input;
+
+        /** offset in file where this section begins. */
+        public final int fileStartPosition;
+
+        /** offset in file where this section ends. */
+        public final int fileEndPosition;
+
+        /** the logical ending position of the buffer */
+        public final int endPosition;
+
+        public final boolean toleratesErrorsInSection;
+
+        public SyncSegment(FileDataInput input, int fileStartPosition, int fileEndPosition, int endPosition, boolean toleratesErrorsInSection)
+        {
+            this.input = input;
+            this.fileStartPosition = fileStartPosition;
+            this.fileEndPosition = fileEndPosition;
+            this.endPosition = endPosition;
+            this.toleratesErrorsInSection = toleratesErrorsInSection;
+        }
+    }
+
+    /**
+     * Derives the next section of the commit log to be replayed. Section boundaries are derived from the commit log sync markers.
+     */
+    interface Segmenter
+    {
+        /**
+         * Get the next section of the commit log to replay.
+         *
+         * @param startPosition the position in the file to begin reading at
+         * @param nextSectionStartPosition the file position of the beginning of the next section
+         * @return the buffer and it's logical end position
+         * @throws IOException
+         */
+        SyncSegment nextSegment(int startPosition, int nextSectionStartPosition) throws IOException;
+
+        /**
+         * Determine if we tolerate errors in the current segment.
+         */
+        default boolean tolerateSegmentErrors(int segmentEndPosition, long fileLength)
+        {
+            return segmentEndPosition >= fileLength || segmentEndPosition < 0;
+        }
+    }
+
+    static class NoOpSegmenter implements Segmenter
+    {
+        private final RandomAccessReader reader;
+
+        public NoOpSegmenter(RandomAccessReader reader)
+        {
+            this.reader = reader;
+        }
+
+        public SyncSegment nextSegment(int startPosition, int nextSectionStartPosition)
+        {
+            reader.seek(startPosition);
+            return new SyncSegment(reader, startPosition, nextSectionStartPosition, nextSectionStartPosition, true);
+        }
+
+        public boolean tolerateSegmentErrors(int end, long length)
+        {
+            return true;
+        }
+    }
+
+    static class CompressedSegmenter implements Segmenter
+    {
+        private final ICompressor compressor;
+        private final RandomAccessReader reader;
+        private byte[] compressedBuffer;
+        private byte[] uncompressedBuffer;
+        private long nextLogicalStart;
+
+        public CompressedSegmenter(CommitLogDescriptor desc, RandomAccessReader reader)
+        {
+            this(CompressionParams.createCompressor(desc.compression), reader);
+        }
+
+        public CompressedSegmenter(ICompressor compressor, RandomAccessReader reader)
+        {
+            this.compressor = compressor;
+            this.reader = reader;
+            compressedBuffer = new byte[0];
+            uncompressedBuffer = new byte[0];
+            nextLogicalStart = reader.getFilePointer();
+        }
+
+        @SuppressWarnings("resource")
+        public SyncSegment nextSegment(final int startPosition, final int nextSectionStartPosition) throws IOException
+        {
+            reader.seek(startPosition);
+            int uncompressedLength = reader.readInt();
+
+            int compressedLength = nextSectionStartPosition - (int)reader.getPosition();
+            if (compressedLength > compressedBuffer.length)
+                compressedBuffer = new byte[(int) (1.2 * compressedLength)];
+            reader.readFully(compressedBuffer, 0, compressedLength);
+
+            if (uncompressedLength > uncompressedBuffer.length)
+               uncompressedBuffer = new byte[(int) (1.2 * uncompressedLength)];
+            int count = compressor.uncompress(compressedBuffer, 0, compressedLength, uncompressedBuffer, 0);
+            nextLogicalStart += SYNC_MARKER_SIZE;
+            FileDataInput input = new FileSegmentInputStream(ByteBuffer.wrap(uncompressedBuffer, 0, count), reader.getPath(), nextLogicalStart);
+            nextLogicalStart += uncompressedLength;
+            return new SyncSegment(input, startPosition, nextSectionStartPosition, (int)nextLogicalStart, tolerateSegmentErrors(nextSectionStartPosition, reader.length()));
+        }
+    }
+
+    static class EncryptedSegmenter implements Segmenter
+    {
+        private final RandomAccessReader reader;
+        private final ICompressor compressor;
+        private final Cipher cipher;
+
+        /**
+         * the result of the decryption is written into this buffer.
+         */
+        private ByteBuffer decryptedBuffer;
+
+        /**
+         * the result of the decryption is written into this buffer.
+         */
+        private ByteBuffer uncompressedBuffer;
+
+        private final ChunkProvider chunkProvider;
+
+        private long currentSegmentEndPosition;
+        private long nextLogicalStart;
+
+        public EncryptedSegmenter(CommitLogDescriptor descriptor, RandomAccessReader reader)
+        {
+            this(reader, descriptor.getEncryptionContext());
+        }
+
+        @VisibleForTesting
+        EncryptedSegmenter(final RandomAccessReader reader, EncryptionContext encryptionContext)
+        {
+            this.reader = reader;
+            decryptedBuffer = ByteBuffer.allocate(0);
+            compressor = encryptionContext.getCompressor();
+            nextLogicalStart = reader.getFilePointer();
+
+            try
+            {
+                cipher = encryptionContext.getDecryptor();
+            }
+            catch (IOException ioe)
+            {
+                throw new FSReadError(ioe, reader.getPath());
+            }
+
+            chunkProvider = () -> {
+                if (reader.getFilePointer() >= currentSegmentEndPosition)
+                    return ByteBufferUtil.EMPTY_BYTE_BUFFER;
+                try
+                {
+                    decryptedBuffer = EncryptionUtils.decrypt(reader, decryptedBuffer, true, cipher);
+                    uncompressedBuffer = EncryptionUtils.uncompress(decryptedBuffer, uncompressedBuffer, true, compressor);
+                    return uncompressedBuffer;
+                }
+                catch (IOException e)
+                {
+                    throw new FSReadError(e, reader.getPath());
+                }
+            };
+        }
+
+        @SuppressWarnings("resource")
+        public SyncSegment nextSegment(int startPosition, int nextSectionStartPosition) throws IOException
+        {
+            int totalPlainTextLength = reader.readInt();
+            currentSegmentEndPosition = nextSectionStartPosition - 1;
+
+            nextLogicalStart += SYNC_MARKER_SIZE;
+            FileDataInput input = new EncryptedFileSegmentInputStream(reader.getPath(), nextLogicalStart, 0, totalPlainTextLength, chunkProvider);
+            nextLogicalStart += totalPlainTextLength;
+            return new SyncSegment(input, startPosition, nextSectionStartPosition, (int)nextLogicalStart, tolerateSegmentErrors(nextSectionStartPosition, reader.length()));
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/commitlog/CompressedSegment.java b/src/java/org/apache/cassandra/db/commitlog/CompressedSegment.java
index 8e05112..1d65fbe 100644
--- a/src/java/org/apache/cassandra/db/commitlog/CompressedSegment.java
+++ b/src/java/org/apache/cassandra/db/commitlog/CompressedSegment.java
@@ -17,93 +17,41 @@
  */
 package org.apache.cassandra.db.commitlog;
 
-import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.Queue;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.atomic.AtomicInteger;
 
-import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.io.FSWriteError;
-import org.apache.cassandra.io.compress.BufferType;
 import org.apache.cassandra.io.compress.ICompressor;
-import org.apache.cassandra.io.util.FileUtils;
-import org.apache.cassandra.utils.SyncUtil;
 
-/*
+/**
  * Compressed commit log segment. Provides an in-memory buffer for the mutation threads. On sync compresses the written
  * section of the buffer and writes it to the destination channel.
+ *
+ * The format of the compressed commit log is as follows:
+ * - standard commit log header (as written by {@link CommitLogDescriptor#writeHeader(ByteBuffer, CommitLogDescriptor)})
+ * - a series of 'sync segments' that are written every time the commit log is sync()'ed
+ * -- a sync section header, see {@link CommitLogSegment#writeSyncMarker(long, ByteBuffer, int, int, int)}
+ * -- total plain text length for this section
+ * -- a block of compressed data
  */
-public class CompressedSegment extends CommitLogSegment
+public class CompressedSegment extends FileDirectSegment
 {
-    private static final ThreadLocal<ByteBuffer> compressedBufferHolder = new ThreadLocal<ByteBuffer>() {
-        protected ByteBuffer initialValue()
-        {
-            return ByteBuffer.allocate(0);
-        }
-    };
-    static Queue<ByteBuffer> bufferPool = new ConcurrentLinkedQueue<>();
-
-    /**
-     * The number of buffers in use
-     */
-    private static AtomicInteger usedBuffers = new AtomicInteger(0);
-
-
-    /**
-     * Maximum number of buffers in the compression pool. The default value is 3, it should not be set lower than that
-     * (one segment in compression, one written to, one in reserve); delays in compression may cause the log to use
-     * more, depending on how soon the sync policy stops all writing threads.
-     */
-    static final int MAX_BUFFERPOOL_SIZE = DatabaseDescriptor.getCommitLogMaxCompressionBuffersInPool();
-
     static final int COMPRESSED_MARKER_SIZE = SYNC_MARKER_SIZE + 4;
     final ICompressor compressor;
-    final Runnable onClose;
-
-    volatile long lastWrittenPos = 0;
 
     /**
      * Constructs a new segment file.
      */
-    CompressedSegment(CommitLog commitLog, Runnable onClose)
+    CompressedSegment(CommitLog commitLog, AbstractCommitLogSegmentManager manager)
     {
-        super(commitLog);
+        super(commitLog, manager);
         this.compressor = commitLog.configuration.getCompressor();
-        this.onClose = onClose;
-        try
-        {
-            channel.write((ByteBuffer) buffer.duplicate().flip());
-            commitLog.allocator.addSize(lastWrittenPos = buffer.position());
-        }
-        catch (IOException e)
-        {
-            throw new FSWriteError(e, getPath());
-        }
-    }
-
-    ByteBuffer allocate(int size)
-    {
-        return compressor.preferredBufferType().allocate(size);
     }
 
     ByteBuffer createBuffer(CommitLog commitLog)
     {
-        usedBuffers.incrementAndGet();
-        ByteBuffer buf = bufferPool.poll();
-        if (buf == null)
-        {
-            // this.compressor is not yet set, so we must use the commitLog's one.
-            buf = commitLog.configuration.getCompressor()
-                                         .preferredBufferType()
-                                         .allocate(DatabaseDescriptor.getCommitLogSegmentSize());
-        } else
-            buf.clear();
-        return buf;
+        return manager.getBufferPool().createBuffer();
     }
 
-    static long startMillis = System.currentTimeMillis();
-
     @Override
     void write(int startMarker, int nextMarker)
     {
@@ -115,14 +63,7 @@
         try
         {
             int neededBufferSize = compressor.initialCompressedBufferLength(length) + COMPRESSED_MARKER_SIZE;
-            ByteBuffer compressedBuffer = compressedBufferHolder.get();
-            if (compressor.preferredBufferType() != BufferType.typeOf(compressedBuffer) ||
-                compressedBuffer.capacity() < neededBufferSize)
-            {
-                FileUtils.clean(compressedBuffer);
-                compressedBuffer = allocate(neededBufferSize);
-                compressedBufferHolder.set(compressedBuffer);
-            }
+            ByteBuffer compressedBuffer = manager.getBufferPool().getThreadLocalReusableBuffer(neededBufferSize);
 
             ByteBuffer inputBuffer = buffer.duplicate();
             inputBuffer.limit(contentStart + length).position(contentStart);
@@ -135,7 +76,7 @@
             // Only one thread can be here at a given time.
             // Protected by synchronization on CommitLogSegment.sync().
             writeSyncMarker(id, compressedBuffer, 0, (int) channel.position(), (int) channel.position() + compressedBuffer.remaining());
-            commitLog.allocator.addSize(compressedBuffer.limit());
+            manager.addSize(compressedBuffer.limit());
             channel.write(compressedBuffer);
             assert channel.position() - lastWrittenPos == compressedBuffer.limit();
             lastWrittenPos = channel.position();
@@ -147,52 +88,6 @@
     }
 
     @Override
-    protected void flush(int startMarker, int nextMarker)
-    {
-        try
-        {
-            SyncUtil.force(channel, true);
-        }
-        catch (Exception e)
-        {
-            throw new FSWriteError(e, getPath());
-        }
-    }
-
-    @Override
-    protected void internalClose()
-    {
-        usedBuffers.decrementAndGet();
-        try {
-            if (bufferPool.size() < MAX_BUFFERPOOL_SIZE)
-                bufferPool.add(buffer);
-            else
-                FileUtils.clean(buffer);
-            super.internalClose();
-        }
-        finally
-        {
-            onClose.run();
-        }
-    }
-
-    /**
-     * Checks if the number of buffers in use is greater or equals to the maximum number of buffers allowed in the pool.
-     *
-     * @return <code>true</code> if the number of buffers in use is greater or equals to the maximum number of buffers
-     * allowed in the pool, <code>false</code> otherwise.
-     */
-    static boolean hasReachedPoolLimit()
-    {
-        return usedBuffers.get() >= MAX_BUFFERPOOL_SIZE;
-    }
-
-    static void shutdown()
-    {
-        bufferPool.clear();
-    }
-
-    @Override
     public long onDiskSize()
     {
         return lastWrittenPos;
diff --git a/src/java/org/apache/cassandra/db/commitlog/EncryptedFileSegmentInputStream.java b/src/java/org/apache/cassandra/db/commitlog/EncryptedFileSegmentInputStream.java
new file mode 100644
index 0000000..9da3d50
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/commitlog/EncryptedFileSegmentInputStream.java
@@ -0,0 +1,108 @@
+/*
+ *
+ * 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.db.commitlog;
+
+import java.io.DataInput;
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.io.util.DataPosition;
+import org.apache.cassandra.io.util.FileDataInput;
+import org.apache.cassandra.io.util.FileSegmentInputStream;
+
+/**
+ * Each segment of an encrypted file may contain many encrypted chunks, and each chunk needs to be individually decrypted
+ * to reconstruct the full segment.
+ */
+public class EncryptedFileSegmentInputStream extends FileSegmentInputStream implements FileDataInput, DataInput
+{
+    private final long segmentOffset;
+    private final int expectedLength;
+    private final ChunkProvider chunkProvider;
+
+    /**
+     * Offset representing the decrypted chunks already processed in this segment.
+     */
+    private int totalChunkOffset;
+
+    public EncryptedFileSegmentInputStream(String filePath, long segmentOffset, int position, int expectedLength, ChunkProvider chunkProvider)
+    {
+        super(chunkProvider.nextChunk(), filePath, position);
+        this.segmentOffset = segmentOffset;
+        this.expectedLength = expectedLength;
+        this.chunkProvider = chunkProvider;
+    }
+
+    public interface ChunkProvider
+    {
+        /**
+         * Get the next chunk from the backing provider, if any chunks remain.
+         * @return Next chunk, else null if no more chunks remain.
+         */
+        ByteBuffer nextChunk();
+    }
+
+    public long getFilePointer()
+    {
+        return segmentOffset + totalChunkOffset + buffer.position();
+    }
+
+    public boolean isEOF()
+    {
+        return totalChunkOffset + buffer.position() >= expectedLength;
+    }
+
+    public long bytesRemaining()
+    {
+        return expectedLength - (totalChunkOffset + buffer.position());
+    }
+
+    public void seek(long position)
+    {
+        long bufferPos = position - totalChunkOffset - segmentOffset;
+        while (buffer != null && bufferPos > buffer.capacity())
+        {
+            // rebuffer repeatedly until we have reached desired position
+            buffer.position(buffer.limit());
+
+            // increases totalChunkOffset
+            reBuffer();
+            bufferPos = position - totalChunkOffset - segmentOffset;
+        }
+        if (buffer == null || bufferPos < 0 || bufferPos > buffer.capacity())
+            throw new IllegalArgumentException(
+                    String.format("Unable to seek to position %d in %s (%d bytes) in partial mode",
+                            position,
+                            getPath(),
+                            segmentOffset + expectedLength));
+        buffer.position((int) bufferPos);
+    }
+
+    public long bytesPastMark(DataPosition mark)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public void reBuffer()
+    {
+        totalChunkOffset += buffer.position();
+        buffer = chunkProvider.nextChunk();
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/commitlog/EncryptedSegment.java b/src/java/org/apache/cassandra/db/commitlog/EncryptedSegment.java
new file mode 100644
index 0000000..a13f615
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/commitlog/EncryptedSegment.java
@@ -0,0 +1,154 @@
+/*
+ * 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.db.commitlog;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import javax.crypto.Cipher;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.io.FSWriteError;
+import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.io.compress.ICompressor;
+import org.apache.cassandra.security.EncryptionUtils;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.utils.Hex;
+
+import static org.apache.cassandra.security.EncryptionUtils.ENCRYPTED_BLOCK_HEADER_SIZE;
+
+/**
+ * Writes encrypted segments to disk. Data is compressed before encrypting to (hopefully) reduce the size of the data into
+ * the encryption algorithms.
+ *
+ * The format of the encrypted commit log is as follows:
+ * - standard commit log header (as written by {@link CommitLogDescriptor#writeHeader(ByteBuffer, CommitLogDescriptor)})
+ * - a series of 'sync segments' that are written every time the commit log is sync()'ed
+ * -- a sync section header, see {@link CommitLogSegment#writeSyncMarker(long, ByteBuffer, int, int, int)}
+ * -- total plain text length for this section
+ * -- a series of encrypted data blocks, each of which contains:
+ * --- the length of the encrypted block (cipher text)
+ * --- the length of the unencrypted data (compressed text)
+ * --- the encrypted block, which contains:
+ * ---- the length of the plain text (raw) data
+ * ---- block of compressed data
+ *
+ * Notes:
+ * - "length of the unencrypted data" is different from the length of resulting decrypted buffer as encryption adds padding
+ * to the output buffer, and we need to ignore that padding when processing.
+ */
+public class EncryptedSegment extends FileDirectSegment
+{
+    private static final Logger logger = LoggerFactory.getLogger(EncryptedSegment.class);
+
+    private static final int ENCRYPTED_SECTION_HEADER_SIZE = SYNC_MARKER_SIZE + 4;
+
+    private final EncryptionContext encryptionContext;
+    private final Cipher cipher;
+
+    public EncryptedSegment(CommitLog commitLog, AbstractCommitLogSegmentManager manager)
+    {
+        super(commitLog, manager);
+        this.encryptionContext = commitLog.configuration.getEncryptionContext();
+
+        try
+        {
+            cipher = encryptionContext.getEncryptor();
+        }
+        catch (IOException e)
+        {
+            throw new FSWriteError(e, logFile);
+        }
+        logger.debug("created a new encrypted commit log segment: {}", logFile);
+    }
+
+    protected Map<String, String> additionalHeaderParameters()
+    {
+        Map<String, String> map = encryptionContext.toHeaderParameters();
+        map.put(EncryptionContext.ENCRYPTION_IV, Hex.bytesToHex(cipher.getIV()));
+        return map;
+    }
+
+    ByteBuffer createBuffer(CommitLog commitLog)
+    {
+        // Note: we want to keep the compression buffers on-heap as we need those bytes for encryption,
+        // and we want to avoid copying from off-heap (compression buffer) to on-heap encryption APIs
+        return manager.getBufferPool().createBuffer();
+    }
+
+    void write(int startMarker, int nextMarker)
+    {
+        int contentStart = startMarker + SYNC_MARKER_SIZE;
+        final int length = nextMarker - contentStart;
+        // The length may be 0 when the segment is being closed.
+        assert length > 0 || length == 0 && !isStillAllocating();
+
+        final ICompressor compressor = encryptionContext.getCompressor();
+        final int blockSize = encryptionContext.getChunkLength();
+        try
+        {
+            ByteBuffer inputBuffer = buffer.duplicate();
+            inputBuffer.limit(contentStart + length).position(contentStart);
+            ByteBuffer buffer = manager.getBufferPool().getThreadLocalReusableBuffer(DatabaseDescriptor.getCommitLogSegmentSize());
+
+            // save space for the sync marker at the beginning of this section
+            final long syncMarkerPosition = lastWrittenPos;
+            channel.position(syncMarkerPosition + ENCRYPTED_SECTION_HEADER_SIZE);
+
+            // loop over the segment data in encryption buffer sized chunks
+            while (contentStart < nextMarker)
+            {
+                int nextBlockSize = nextMarker - blockSize > contentStart ? blockSize : nextMarker - contentStart;
+                ByteBuffer slice = inputBuffer.duplicate();
+                slice.limit(contentStart + nextBlockSize).position(contentStart);
+
+                buffer = EncryptionUtils.compress(slice, buffer, true, compressor);
+
+                // reuse the same buffer for the input and output of the encryption operation
+                buffer = EncryptionUtils.encryptAndWrite(buffer, channel, true, cipher);
+
+                contentStart += nextBlockSize;
+                manager.addSize(buffer.limit() + ENCRYPTED_BLOCK_HEADER_SIZE);
+            }
+
+            lastWrittenPos = channel.position();
+
+            // rewind to the beginning of the section and write out the sync marker
+            buffer.position(0).limit(ENCRYPTED_SECTION_HEADER_SIZE);
+            writeSyncMarker(id, buffer, 0, (int) syncMarkerPosition, (int) lastWrittenPos);
+            buffer.putInt(SYNC_MARKER_SIZE, length);
+            buffer.rewind();
+            manager.addSize(buffer.limit());
+
+            channel.position(syncMarkerPosition);
+            channel.write(buffer);
+        }
+        catch (Exception e)
+        {
+            throw new FSWriteError(e, getPath());
+        }
+    }
+
+    public long onDiskSize()
+    {
+        return lastWrittenPos;
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/commitlog/FileDirectSegment.java b/src/java/org/apache/cassandra/db/commitlog/FileDirectSegment.java
new file mode 100644
index 0000000..d5431f8
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/commitlog/FileDirectSegment.java
@@ -0,0 +1,80 @@
+/*
+ * 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.db.commitlog;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.io.FSWriteError;
+import org.apache.cassandra.utils.SyncUtil;
+
+/**
+ * Writes to the backing commit log file only on sync, allowing transformations of the mutations,
+ * such as compression or encryption, before writing out to disk.
+ */
+public abstract class FileDirectSegment extends CommitLogSegment
+{
+    volatile long lastWrittenPos = 0;
+
+    FileDirectSegment(CommitLog commitLog, AbstractCommitLogSegmentManager manager)
+    {
+        super(commitLog, manager);
+    }
+
+    @Override
+    void writeLogHeader()
+    {
+        super.writeLogHeader();
+        try
+        {
+            channel.write((ByteBuffer) buffer.duplicate().flip());
+            manager.addSize(lastWrittenPos = buffer.position());
+        }
+        catch (IOException e)
+        {
+            throw new FSWriteError(e, getPath());
+        }
+    }
+
+    @Override
+    protected void internalClose()
+    {
+        try
+        {
+            manager.getBufferPool().releaseBuffer(buffer);
+            super.internalClose();
+        }
+        finally
+        {
+            manager.notifyBufferFreed();
+        }
+    }
+
+    @Override
+    protected void flush(int startMarker, int nextMarker)
+    {
+        try
+        {
+            SyncUtil.force(channel, true);
+        }
+        catch (Exception e)
+        {
+            throw new FSWriteError(e, getPath());
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/commitlog/IntervalSet.java b/src/java/org/apache/cassandra/db/commitlog/IntervalSet.java
index a25dbfd..45db2f6 100644
--- a/src/java/org/apache/cassandra/db/commitlog/IntervalSet.java
+++ b/src/java/org/apache/cassandra/db/commitlog/IntervalSet.java
@@ -33,7 +33,7 @@
  * to a single interval covering both).
  *
  * The set is stored as a sorted map from interval starts to the corresponding end. The map satisfies
- *   curr().getKey() <= curr().getValue() < next().getKey()
+ *   {@code curr().getKey() <= curr().getValue() < next().getKey()}
  */
 public class IntervalSet<T extends Comparable<T>>
 {
@@ -58,7 +58,7 @@
     @SuppressWarnings("unchecked")
     public static <T extends Comparable<T>> IntervalSet<T> empty()
     {
-        return (IntervalSet<T>) EMPTY;
+        return EMPTY;
     }
 
     public boolean contains(T position)
@@ -123,7 +123,7 @@
                     pointSerializer.serialize(en.getValue(), out);
                 }
             }
-    
+
             public IntervalSet<T> deserialize(DataInputPlus in) throws IOException
             {
                 int count = in.readInt();
@@ -132,7 +132,7 @@
                     ranges.put(pointSerializer.deserialize(in), pointSerializer.deserialize(in));
                 return new IntervalSet<T>(ImmutableSortedMap.copyOfSorted(ranges));
             }
-    
+
             public long serializedSize(IntervalSet<T> intervals)
             {
                 long size = TypeSizes.sizeof(intervals.ranges.size());
@@ -150,7 +150,7 @@
      * Builder of interval sets, applying the necessary normalization while adding ranges.
      *
      * Data is stored as above, as a sorted map from interval starts to the corresponding end, which satisfies
-     *   curr().getKey() <= curr().getValue() < next().getKey()
+     *   {@code curr().getKey() <= curr().getValue() < next().getKey()}
      */
     static public class Builder<T extends Comparable<T>>
     {
diff --git a/src/java/org/apache/cassandra/db/commitlog/MemoryMappedSegment.java b/src/java/org/apache/cassandra/db/commitlog/MemoryMappedSegment.java
index 8259f04..e79278d 100644
--- a/src/java/org/apache/cassandra/db/commitlog/MemoryMappedSegment.java
+++ b/src/java/org/apache/cassandra/db/commitlog/MemoryMappedSegment.java
@@ -38,12 +38,11 @@
     /**
      * Constructs a new segment file.
      *
-     * @param filePath  if not null, recycles the existing file by renaming it and truncating it to CommitLog.SEGMENT_SIZE.
      * @param commitLog the commit log it will be used with.
      */
-    MemoryMappedSegment(CommitLog commitLog)
+    MemoryMappedSegment(CommitLog commitLog, AbstractCommitLogSegmentManager manager)
     {
-        super(commitLog);
+        super(commitLog, manager);
         // mark the initial sync marker as uninitialised
         int firstSync = buffer.position();
         buffer.putInt(firstSync + 0, 0);
@@ -55,7 +54,7 @@
         try
         {
             MappedByteBuffer mappedFile = channel.map(FileChannel.MapMode.READ_WRITE, 0, DatabaseDescriptor.getCommitLogSegmentSize());
-            commitLog.allocator.addSize(DatabaseDescriptor.getCommitLogSegmentSize());
+            manager.addSize(DatabaseDescriptor.getCommitLogSegmentSize());
             return mappedFile;
         }
         catch (IOException e)
@@ -103,7 +102,7 @@
     @Override
     protected void internalClose()
     {
-        if (FileUtils.isCleanerAvailable())
+        if (FileUtils.isCleanerAvailable)
             FileUtils.clean(buffer);
         super.internalClose();
     }
diff --git a/src/java/org/apache/cassandra/db/commitlog/PeriodicCommitLogService.java b/src/java/org/apache/cassandra/db/commitlog/PeriodicCommitLogService.java
index 7a09de0..efd3394 100644
--- a/src/java/org/apache/cassandra/db/commitlog/PeriodicCommitLogService.java
+++ b/src/java/org/apache/cassandra/db/commitlog/PeriodicCommitLogService.java
@@ -18,42 +18,25 @@
 package org.apache.cassandra.db.commitlog;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.utils.concurrent.WaitQueue;
 
 class PeriodicCommitLogService extends AbstractCommitLogService
 {
-    private static final int blockWhenSyncLagsMillis = (int) (DatabaseDescriptor.getCommitLogSyncPeriod() * 1.5);
+    private static final long blockWhenSyncLagsNanos = (long) (DatabaseDescriptor.getCommitLogSyncPeriod() * 1.5e6);
 
     public PeriodicCommitLogService(final CommitLog commitLog)
     {
         super(commitLog, "PERIODIC-COMMIT-LOG-SYNCER", DatabaseDescriptor.getCommitLogSyncPeriod(),
-              !commitLog.configuration.useCompression());
+              !(commitLog.configuration.useCompression() || commitLog.configuration.useEncryption()));
     }
 
     protected void maybeWaitForSync(CommitLogSegment.Allocation alloc)
     {
-        if (waitForSyncToCatchUp(Long.MAX_VALUE))
+        long expectedSyncTime = System.nanoTime() - blockWhenSyncLagsNanos;
+        if (lastSyncedAt < expectedSyncTime)
         {
-            // wait until periodic sync() catches up with its schedule
-            long started = System.currentTimeMillis();
             pending.incrementAndGet();
-            while (waitForSyncToCatchUp(started))
-            {
-                WaitQueue.Signal signal = syncComplete.register(commitLog.metrics.waitingOnCommit.time());
-                if (waitForSyncToCatchUp(started))
-                    signal.awaitUninterruptibly();
-                else
-                    signal.cancel();
-            }
+            awaitSyncAt(expectedSyncTime, commitLog.metrics.waitingOnCommit.time());
             pending.decrementAndGet();
         }
     }
-
-    /**
-     * @return true if sync is currently lagging behind inserts
-     */
-    private boolean waitForSyncToCatchUp(long started)
-    {
-        return started > lastSyncedAt + blockWhenSyncLagsMillis;
-    }
 }
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/db/commitlog/ReplayPosition.java b/src/java/org/apache/cassandra/db/commitlog/ReplayPosition.java
deleted file mode 100644
index b0214b8..0000000
--- a/src/java/org/apache/cassandra/db/commitlog/ReplayPosition.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * 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.db.commitlog;
-
-import java.io.IOException;
-
-import org.apache.cassandra.db.TypeSizes;
-import org.apache.cassandra.io.ISerializer;
-import org.apache.cassandra.io.util.DataInputPlus;
-import org.apache.cassandra.io.util.DataOutputPlus;
-
-public class ReplayPosition implements Comparable<ReplayPosition>
-{
-    public static final ReplayPositionSerializer serializer = new ReplayPositionSerializer();
-
-    // NONE is used for SSTables that are streamed from other nodes and thus have no relationship
-    // with our local commitlog. The values satisfy the criteria that
-    //  - no real commitlog segment will have the given id
-    //  - it will sort before any real replayposition, so it will be effectively ignored by getReplayPosition
-    public static final ReplayPosition NONE = new ReplayPosition(-1, 0);
-
-    public final long segment;
-    public final int position;
-
-    public ReplayPosition(long segment, int position)
-    {
-        this.segment = segment;
-        assert position >= 0;
-        this.position = position;
-    }
-
-    public int compareTo(ReplayPosition that)
-    {
-        if (this.segment != that.segment)
-            return Long.compare(this.segment, that.segment);
-
-        return Integer.compare(this.position, that.position);
-    }
-
-    @Override
-    public boolean equals(Object o)
-    {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-
-        ReplayPosition that = (ReplayPosition) o;
-
-        if (position != that.position) return false;
-        return segment == that.segment;
-    }
-
-    @Override
-    public int hashCode()
-    {
-        int result = (int) (segment ^ (segment >>> 32));
-        result = 31 * result + position;
-        return result;
-    }
-
-    @Override
-    public String toString()
-    {
-        return "ReplayPosition(" +
-               "segmentId=" + segment +
-               ", position=" + position +
-               ')';
-    }
-
-    public ReplayPosition clone()
-    {
-        return new ReplayPosition(segment, position);
-    }
-
-    public static class ReplayPositionSerializer implements ISerializer<ReplayPosition>
-    {
-        public void serialize(ReplayPosition rp, DataOutputPlus out) throws IOException
-        {
-            out.writeLong(rp.segment);
-            out.writeInt(rp.position);
-        }
-
-        public ReplayPosition deserialize(DataInputPlus in) throws IOException
-        {
-            return new ReplayPosition(in.readLong(), in.readInt());
-        }
-
-        public long serializedSize(ReplayPosition rp)
-        {
-            return TypeSizes.sizeof(rp.segment) + TypeSizes.sizeof(rp.position);
-        }
-    }
-}
diff --git a/src/java/org/apache/cassandra/db/compaction/AbstractCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/AbstractCompactionStrategy.java
index 3ceda93..4298be8 100644
--- a/src/java/org/apache/cassandra/db/compaction/AbstractCompactionStrategy.java
+++ b/src/java/org/apache/cassandra/db/compaction/AbstractCompactionStrategy.java
@@ -23,10 +23,10 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
-import com.google.common.util.concurrent.RateLimiter;
 
 import org.apache.cassandra.db.Directories;
 import org.apache.cassandra.db.SerializationHeader;
+import org.apache.cassandra.index.Index;
 import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.SSTableMultiWriter;
@@ -44,6 +44,7 @@
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.ISSTableScanner;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
+import org.apache.cassandra.schema.CompactionParams;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 
 /**
@@ -62,11 +63,13 @@
     // minimum interval needed to perform tombstone removal compaction in seconds, default 86400 or 1 day.
     protected static final long DEFAULT_TOMBSTONE_COMPACTION_INTERVAL = 86400;
     protected static final boolean DEFAULT_UNCHECKED_TOMBSTONE_COMPACTION_OPTION = false;
+    protected static final boolean DEFAULT_LOG_ALL_OPTION = false;
 
     protected static final String TOMBSTONE_THRESHOLD_OPTION = "tombstone_threshold";
     protected static final String TOMBSTONE_COMPACTION_INTERVAL_OPTION = "tombstone_compaction_interval";
     // disable range overlap check when deciding if an SSTable is candidate for tombstone compaction (CASSANDRA-6563)
     protected static final String UNCHECKED_TOMBSTONE_COMPACTION_OPTION = "unchecked_tombstone_compaction";
+    protected static final String LOG_ALL_OPTION = "log_all";
     protected static final String COMPACTION_ENABLED = "enabled";
     public static final String ONLY_PURGE_REPAIRED_TOMBSTONES = "only_purge_repaired_tombstones";
 
@@ -77,6 +80,7 @@
     protected long tombstoneCompactionInterval;
     protected boolean uncheckedTombstoneCompaction;
     protected boolean disableTombstoneCompactions = false;
+    protected boolean logAll = true;
 
     private final Directories directories;
 
@@ -109,6 +113,8 @@
             tombstoneCompactionInterval = optionValue == null ? DEFAULT_TOMBSTONE_COMPACTION_INTERVAL : Long.parseLong(optionValue);
             optionValue = options.get(UNCHECKED_TOMBSTONE_COMPACTION_OPTION);
             uncheckedTombstoneCompaction = optionValue == null ? DEFAULT_UNCHECKED_TOMBSTONE_COMPACTION_OPTION : Boolean.parseBoolean(optionValue);
+            optionValue = options.get(LOG_ALL_OPTION);
+            logAll = optionValue == null ? DEFAULT_LOG_ALL_OPTION : Boolean.parseBoolean(optionValue);
             if (!shouldBeEnabled())
                 this.disable();
         }
@@ -120,7 +126,7 @@
             uncheckedTombstoneCompaction = DEFAULT_UNCHECKED_TOMBSTONE_COMPACTION_OPTION;
         }
 
-        directories = new Directories(cfs.metadata, Directories.dataDirectories);
+        directories = cfs.getDirectories();
     }
 
     public Directories getDirectories()
@@ -277,12 +283,11 @@
     @SuppressWarnings("resource")
     public ScannerList getScanners(Collection<SSTableReader> sstables, Collection<Range<Token>> ranges)
     {
-        RateLimiter limiter = CompactionManager.instance.getRateLimiter();
         ArrayList<ISSTableScanner> scanners = new ArrayList<ISSTableScanner>();
         try
         {
             for (SSTableReader sstable : sstables)
-                scanners.add(sstable.getScanner(ranges, limiter));
+                scanners.add(sstable.getScanner(ranges, null));
         }
         catch (Throwable t)
         {
@@ -304,16 +309,36 @@
         return getClass().getSimpleName();
     }
 
+    /**
+     * Replaces sstables in the compaction strategy
+     *
+     * Note that implementations must be able to handle duplicate notifications here (that removed are already gone and
+     * added have already been added)
+     * */
     public synchronized void replaceSSTables(Collection<SSTableReader> removed, Collection<SSTableReader> added)
     {
         for (SSTableReader remove : removed)
             removeSSTable(remove);
-        for (SSTableReader add : added)
-            addSSTable(add);
+        addSSTables(added);
     }
 
+    /**
+     * Adds sstable, note that implementations must handle duplicate notifications here (added already being in the compaction strategy)
+     */
     public abstract void addSSTable(SSTableReader added);
 
+    /**
+     * Adds sstables, note that implementations must handle duplicate notifications here (added already being in the compaction strategy)
+     */
+    public synchronized void addSSTables(Iterable<SSTableReader> added)
+    {
+        for (SSTableReader sstable : added)
+            addSSTable(sstable);
+    }
+
+    /**
+     * Removes sstable from the strategy, implementations must be able to handle the sstable having already been removed.
+     */
     public abstract void removeSSTable(SSTableReader sstable);
 
     public static class ScannerList implements AutoCloseable
@@ -324,6 +349,41 @@
             this.scanners = scanners;
         }
 
+        public long getTotalBytesScanned()
+        {
+            long bytesScanned = 0L;
+            for (ISSTableScanner scanner : scanners)
+                bytesScanned += scanner.getBytesScanned();
+
+            return bytesScanned;
+        }
+
+        public long getTotalCompressedSize()
+        {
+            long compressedSize = 0;
+            for (ISSTableScanner scanner : scanners)
+                compressedSize += scanner.getCompressedLengthInBytes();
+
+            return compressedSize;
+        }
+
+        public double getCompressionRatio()
+        {
+            double compressed = 0.0;
+            double uncompressed = 0.0;
+
+            for (ISSTableScanner scanner : scanners)
+            {
+                compressed += scanner.getCompressedLengthInBytes();
+                uncompressed += scanner.getLengthInBytes();
+            }
+
+            if (compressed == uncompressed || uncompressed == 0)
+                return MetadataCollector.NO_COMPRESSION_RATIO;
+
+            return compressed / uncompressed;
+        }
+
         public void close()
         {
             Throwable t = null;
@@ -451,7 +511,16 @@
         if (unchecked != null)
         {
             if (!unchecked.equalsIgnoreCase("true") && !unchecked.equalsIgnoreCase("false"))
-                throw new ConfigurationException(String.format("'%s' should be either 'true' or 'false', not '%s'",UNCHECKED_TOMBSTONE_COMPACTION_OPTION, unchecked));
+                throw new ConfigurationException(String.format("'%s' should be either 'true' or 'false', not '%s'", UNCHECKED_TOMBSTONE_COMPACTION_OPTION, unchecked));
+        }
+
+        String logAll = options.get(LOG_ALL_OPTION);
+        if (logAll != null)
+        {
+            if (!logAll.equalsIgnoreCase("true") && !logAll.equalsIgnoreCase("false"))
+            {
+                throw new ConfigurationException(String.format("'%s' should either be 'true' or 'false', not %s", LOG_ALL_OPTION, logAll));
+            }
         }
 
         String compactionEnabled = options.get(COMPACTION_ENABLED);
@@ -462,12 +531,15 @@
                 throw new ConfigurationException(String.format("enabled should either be 'true' or 'false', not %s", compactionEnabled));
             }
         }
+
         Map<String, String> uncheckedOptions = new HashMap<String, String>(options);
         uncheckedOptions.remove(TOMBSTONE_THRESHOLD_OPTION);
         uncheckedOptions.remove(TOMBSTONE_COMPACTION_INTERVAL_OPTION);
         uncheckedOptions.remove(UNCHECKED_TOMBSTONE_COMPACTION_OPTION);
+        uncheckedOptions.remove(LOG_ALL_OPTION);
         uncheckedOptions.remove(COMPACTION_ENABLED);
         uncheckedOptions.remove(ONLY_PURGE_REPAIRED_TOMBSTONES);
+        uncheckedOptions.remove(CompactionParams.Option.PROVIDE_OVERLAPPING_TOMBSTONES.toString());
         return uncheckedOptions;
     }
 
@@ -509,9 +581,20 @@
         return groupedSSTables;
     }
 
-    public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, long keyCount, long repairedAt, MetadataCollector meta, SerializationHeader header, LifecycleNewTracker lifecycleNewTracker)
+    public CompactionLogger.Strategy strategyLogger()
     {
-        return SimpleSSTableMultiWriter.create(descriptor, keyCount, repairedAt, cfs.metadata, meta, header, lifecycleNewTracker);
+        return CompactionLogger.Strategy.none;
+    }
+
+    public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor,
+                                                       long keyCount,
+                                                       long repairedAt,
+                                                       MetadataCollector meta,
+                                                       SerializationHeader header,
+                                                       Collection<Index> indexes,
+                                                       LifecycleNewTracker lifecycleNewTracker)
+    {
+        return SimpleSSTableMultiWriter.create(descriptor, keyCount, repairedAt, cfs.metadata, meta, header, indexes, lifecycleNewTracker);
     }
 
     public boolean supportsEarlyOpen()
diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionController.java b/src/java/org/apache/cassandra/db/compaction/CompactionController.java
index 34d093e..19318ff 100644
--- a/src/java/org/apache/cassandra/db/compaction/CompactionController.java
+++ b/src/java/org/apache/cassandra/db/compaction/CompactionController.java
@@ -20,19 +20,25 @@
 import java.util.*;
 import java.util.function.Predicate;
 
+import org.apache.cassandra.config.Config;
 import org.apache.cassandra.db.Memtable;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+
+import com.google.common.base.Predicates;
 import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.RateLimiter;
 
 import org.apache.cassandra.db.partitions.Partition;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.util.FileDataInput;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.schema.CompactionParams.TombstoneOption;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.db.ColumnFamilyStore;
-import org.apache.cassandra.db.DecoratedKey;
-import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.*;
 import org.apache.cassandra.utils.AlwaysPresentFilter;
-
 import org.apache.cassandra.utils.OverlapIterator;
 import org.apache.cassandra.utils.concurrent.Refs;
 
@@ -44,7 +50,8 @@
 public class CompactionController implements AutoCloseable
 {
     private static final Logger logger = LoggerFactory.getLogger(CompactionController.class);
-    static final boolean NEVER_PURGE_TOMBSTONES = Boolean.getBoolean("cassandra.never_purge_tombstones");
+    private static final String NEVER_PURGE_TOMBSTONES_PROPERTY = Config.PROPERTY_PREFIX + "never_purge_tombstones";
+    static final boolean NEVER_PURGE_TOMBSTONES = Boolean.getBoolean(NEVER_PURGE_TOMBSTONES_PROPERTY);
 
     public final ColumnFamilyStore cfs;
     private final boolean compactingRepaired;
@@ -54,6 +61,10 @@
     private Refs<SSTableReader> overlappingSSTables;
     private OverlapIterator<PartitionPosition, SSTableReader> overlapIterator;
     private final Iterable<SSTableReader> compacting;
+    private final RateLimiter limiter;
+    private final long minTimestamp;
+    final TombstoneOption tombstoneOption;
+    final Map<SSTableReader, FileDataInput> openDataFiles = new HashMap<>();
 
     public final int gcBefore;
 
@@ -64,11 +75,22 @@
 
     public CompactionController(ColumnFamilyStore cfs, Set<SSTableReader> compacting, int gcBefore)
     {
+        this(cfs, compacting, gcBefore, null,
+             cfs.getCompactionStrategyManager().getCompactionParams().tombstoneOption());
+    }
+
+    public CompactionController(ColumnFamilyStore cfs, Set<SSTableReader> compacting, int gcBefore, RateLimiter limiter, TombstoneOption tombstoneOption)
+    {
         assert cfs != null;
         this.cfs = cfs;
         this.gcBefore = gcBefore;
         this.compacting = compacting;
+        this.limiter = limiter;
         compactingRepaired = compacting != null && compacting.stream().allMatch(SSTableReader::isRepaired);
+        this.tombstoneOption = tombstoneOption;
+        this.minTimestamp = compacting != null && !compacting.isEmpty()       // check needed for test
+                          ? compacting.stream().mapToLong(SSTableReader::getMinTimestamp).min().getAsLong()
+                          : 0;
         refreshOverlaps();
         if (NEVER_PURGE_TOMBSTONES)
             logger.warn("You are running with -Dcassandra.never_purge_tombstones=true, this is dangerous!");
@@ -78,7 +100,14 @@
     {
         if (NEVER_PURGE_TOMBSTONES)
         {
-            logger.debug("not refreshing overlaps - running with -Dcassandra.never_purge_tombstones=true");
+            logger.debug("not refreshing overlaps - running with -D{}=true",
+                    NEVER_PURGE_TOMBSTONES_PROPERTY);
+            return;
+        }
+
+        if (ignoreOverlaps())
+        {
+            logger.debug("not refreshing overlaps - running with ignoreOverlaps activated");
             return;
         }
 
@@ -98,9 +127,9 @@
             return;
 
         if (this.overlappingSSTables != null)
-            overlappingSSTables.release();
+            close();
 
-        if (compacting == null)
+        if (compacting == null || ignoreOverlaps())
             overlappingSSTables = Refs.tryRef(Collections.<SSTableReader>emptyList());
         else
             overlappingSSTables = cfs.getAndReferenceOverlappingLiveSSTables(compacting);
@@ -109,7 +138,7 @@
 
     public Set<SSTableReader> getFullyExpiredSSTables()
     {
-        return getFullyExpiredSSTables(cfs, compacting, overlappingSSTables, gcBefore);
+        return getFullyExpiredSSTables(cfs, compacting, overlappingSSTables, gcBefore, ignoreOverlaps());
     }
 
     /**
@@ -118,7 +147,7 @@
      * works something like this;
      * 1. find "global" minTimestamp of overlapping sstables, compacting sstables and memtables containing any non-expired data
      * 2. build a list of fully expired candidates
-     * 3. check if the candidates to be dropped actually can be dropped (maxTimestamp < global minTimestamp)
+     * 3. check if the candidates to be dropped actually can be dropped {@code (maxTimestamp < global minTimestamp)}
      *    - if not droppable, remove from candidates
      * 4. return candidates.
      *
@@ -126,20 +155,39 @@
      * @param compacting we take the drop-candidates from this set, it is usually the sstables included in the compaction
      * @param overlapping the sstables that overlap the ones in compacting.
      * @param gcBefore
+     * @param ignoreOverlaps don't check if data shadows/overlaps any data in other sstables
      * @return
      */
-    public static Set<SSTableReader> getFullyExpiredSSTables(ColumnFamilyStore cfStore, Iterable<SSTableReader> compacting, Iterable<SSTableReader> overlapping, int gcBefore)
+    public static Set<SSTableReader> getFullyExpiredSSTables(ColumnFamilyStore cfStore,
+                                                             Iterable<SSTableReader> compacting,
+                                                             Iterable<SSTableReader> overlapping,
+                                                             int gcBefore,
+                                                             boolean ignoreOverlaps)
     {
         logger.trace("Checking droppable sstables in {}", cfStore);
 
-        if (compacting == null || NEVER_PURGE_TOMBSTONES)
-            return Collections.<SSTableReader>emptySet();
+        if (NEVER_PURGE_TOMBSTONES || compacting == null)
+            return Collections.emptySet();
 
         if (cfStore.getCompactionStrategyManager().onlyPurgeRepairedTombstones() && !Iterables.all(compacting, SSTableReader::isRepaired))
             return Collections.emptySet();
 
-        List<SSTableReader> candidates = new ArrayList<>();
+        if (ignoreOverlaps)
+        {
+            Set<SSTableReader> fullyExpired = new HashSet<>();
+            for (SSTableReader candidate : compacting)
+            {
+                if (candidate.getSSTableMetadata().maxLocalDeletionTime < gcBefore)
+                {
+                    fullyExpired.add(candidate);
+                    logger.trace("Dropping overlap ignored expired SSTable {} (maxLocalDeletionTime={}, gcBefore={})",
+                                 candidate, candidate.getSSTableMetadata().maxLocalDeletionTime, gcBefore);
+                }
+            }
+            return fullyExpired;
+        }
 
+        List<SSTableReader> candidates = new ArrayList<>();
         long minTimestamp = Long.MAX_VALUE;
 
         for (SSTableReader sstable : overlapping)
@@ -159,7 +207,10 @@
         }
 
         for (Memtable memtable : cfStore.getTracker().getView().getAllMemtables())
-            minTimestamp = Math.min(minTimestamp, memtable.getMinTimestamp());
+        {
+            if (memtable.getMinTimestamp() != Memtable.NO_MIN_TIMESTAMP)
+                minTimestamp = Math.min(minTimestamp, memtable.getMinTimestamp());
+        }
 
         // At this point, minTimestamp denotes the lowest timestamp of any relevant
         // SSTable or Memtable that contains a constructive value. candidates contains all the
@@ -183,6 +234,14 @@
         return new HashSet<>(candidates);
     }
 
+    public static Set<SSTableReader> getFullyExpiredSSTables(ColumnFamilyStore cfStore,
+                                                             Iterable<SSTableReader> compacting,
+                                                             Iterable<SSTableReader> overlapping,
+                                                             int gcBefore)
+    {
+        return getFullyExpiredSSTables(cfStore, compacting, overlapping, gcBefore, false);
+    }
+
     public String getKeyspace()
     {
         return cfs.keyspace.getName();
@@ -202,7 +261,7 @@
      */
     public Predicate<Long> getPurgeEvaluator(DecoratedKey key)
     {
-        if (!compactingRepaired() || NEVER_PURGE_TOMBSTONES)
+        if (NEVER_PURGE_TOMBSTONES || !compactingRepaired())
             return time -> false;
 
         overlapIterator.update(key);
@@ -211,7 +270,7 @@
         long minTimestampSeen = Long.MAX_VALUE;
         boolean hasTimestamp = false;
 
-        for (SSTableReader sstable : filteredSSTables)
+        for (SSTableReader sstable: filteredSSTables)
         {
             // if we don't have bloom filter(bf_fp_chance=1.0 or filter file is missing),
             // we check index file instead.
@@ -225,11 +284,14 @@
 
         for (Memtable memtable : memtables)
         {
-            Partition partition = memtable.getPartition(key);
-            if (partition != null)
+            if (memtable.getMinTimestamp() != Memtable.NO_MIN_TIMESTAMP)
             {
-                minTimestampSeen = Math.min(minTimestampSeen, partition.stats().minTimestamp);
-                hasTimestamp = true;
+                Partition partition = memtable.getPartition(key);
+                if (partition != null)
+                {
+                    minTimestampSeen = Math.min(minTimestampSeen, partition.stats().minTimestamp);
+                    hasTimestamp = true;
+                }
             }
         }
 
@@ -246,6 +308,9 @@
     {
         if (overlappingSSTables != null)
             overlappingSSTables.release();
+
+        FileUtils.closeQuietly(openDataFiles.values());
+        openDataFiles.clear();
     }
 
     public boolean compactingRepaired()
@@ -253,4 +318,55 @@
         return !cfs.getCompactionStrategyManager().onlyPurgeRepairedTombstones() || compactingRepaired;
     }
 
+    boolean provideTombstoneSources()
+    {
+        return tombstoneOption != TombstoneOption.NONE;
+    }
+
+    // caller must close iterators
+    public Iterable<UnfilteredRowIterator> shadowSources(DecoratedKey key, boolean tombstoneOnly)
+    {
+        if (!provideTombstoneSources() || !compactingRepaired() || NEVER_PURGE_TOMBSTONES)
+            return null;
+        overlapIterator.update(key);
+        return Iterables.filter(Iterables.transform(overlapIterator.overlaps(),
+                                                    reader -> getShadowIterator(reader, key, tombstoneOnly)),
+                                Predicates.notNull());
+    }
+
+    @SuppressWarnings("resource") // caller to close
+    private UnfilteredRowIterator getShadowIterator(SSTableReader reader, DecoratedKey key, boolean tombstoneOnly)
+    {
+        if (reader.isMarkedSuspect() ||
+            reader.getMaxTimestamp() <= minTimestamp ||
+            tombstoneOnly && !reader.mayHaveTombstones())
+            return null;
+        RowIndexEntry<?> position = reader.getPosition(key, SSTableReader.Operator.EQ);
+        if (position == null)
+            return null;
+        FileDataInput dfile = openDataFiles.computeIfAbsent(reader, this::openDataFile);
+        return reader.simpleIterator(dfile, key, position, tombstoneOnly);
+    }
+
+    /**
+     * Is overlapped sstables ignored
+     *
+     * Control whether or not we are taking into account overlapping sstables when looking for fully expired sstables.
+     * In order to reduce the amount of work needed, we look for sstables that can be dropped instead of compacted.
+     * As a safeguard mechanism, for each time range of data in a sstable, we are checking globally to see if all data
+     * of this time range is fully expired before considering to drop the sstable.
+     * This strategy can retain for a long time a lot of sstables on disk (see CASSANDRA-13418) so this option
+     * control whether or not this check should be ignored.
+     *
+     * @return false by default
+     */
+    protected boolean ignoreOverlaps()
+    {
+        return false;
+    }
+
+    private FileDataInput openDataFile(SSTableReader reader)
+    {
+        return limiter != null ? reader.openDataReader(limiter) : reader.openDataReader();
+    }
 }
diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionHistoryTabularData.java b/src/java/org/apache/cassandra/db/compaction/CompactionHistoryTabularData.java
index be64d44..485f1a0 100644
--- a/src/java/org/apache/cassandra/db/compaction/CompactionHistoryTabularData.java
+++ b/src/java/org/apache/cassandra/db/compaction/CompactionHistoryTabularData.java
@@ -46,7 +46,8 @@
 
     private static final TabularType TABULAR_TYPE;
 
-    static {
+    static 
+    {
         try
         {
             ITEM_TYPES = new OpenType[]{ SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, SimpleType.LONG,
diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionInfo.java b/src/java/org/apache/cassandra/db/compaction/CompactionInfo.java
index 2bae5f8..a6dbd9d 100644
--- a/src/java/org/apache/cassandra/db/compaction/CompactionInfo.java
+++ b/src/java/org/apache/cassandra/db/compaction/CompactionInfo.java
@@ -23,8 +23,6 @@
 import java.util.UUID;
 
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.metrics.StorageMetrics;
-import org.apache.cassandra.service.StorageService;
 
 /** Implements serializable to allow structured info to be returned via JMX. */
 public final class CompactionInfo implements Serializable
@@ -161,8 +159,6 @@
     {
         private volatile boolean stopRequested = false;
         public abstract CompactionInfo getCompactionInfo();
-        double load = StorageMetrics.load.getCount();
-        double reportedSeverity = 0d;
 
         public void stop()
         {
@@ -179,24 +175,5 @@
         {
             return stopRequested || (isGlobal() && CompactionManager.instance.isGlobalCompactionPaused());
         }
-
-        /**
-         * report event on the size of the compaction.
-         */
-        public void started()
-        {
-            reportedSeverity = getCompactionInfo().getTotal() / load;
-            StorageService.instance.reportSeverity(reportedSeverity);
-        }
-
-        /**
-         * remove the event complete
-         */
-        public void finished()
-        {
-            if (reportedSeverity != 0d)
-                StorageService.instance.reportSeverity(-(reportedSeverity));
-            reportedSeverity = 0d;
-        }
     }
 }
diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionIterator.java b/src/java/org/apache/cassandra/db/compaction/CompactionIterator.java
index 8c4732b..4b35e9d 100644
--- a/src/java/org/apache/cassandra/db/compaction/CompactionIterator.java
+++ b/src/java/org/apache/cassandra/db/compaction/CompactionIterator.java
@@ -17,17 +17,16 @@
  */
 package org.apache.cassandra.db.compaction;
 
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
 import java.util.function.Predicate;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import com.google.common.collect.Ordering;
 
 import org.apache.cassandra.config.CFMetaData;
 
 import org.apache.cassandra.db.transform.DuplicateRowChecker;
 import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.partitions.PurgeFunction;
 import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
 import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
@@ -36,6 +35,7 @@
 import org.apache.cassandra.index.transactions.CompactionTransaction;
 import org.apache.cassandra.io.sstable.ISSTableScanner;
 import org.apache.cassandra.metrics.CompactionMetrics;
+import org.apache.cassandra.schema.CompactionParams.TombstoneOption;
 
 /**
  * Merge multiple iterators over the content of sstable into a "compacted" iterator.
@@ -55,7 +55,6 @@
  */
 public class CompactionIterator extends CompactionInfo.Holder implements UnfilteredPartitionIterator
 {
-    private static final Logger logger = LoggerFactory.getLogger(CompactionIterator.class);
     private static final long UNFILTERED_TO_UPDATE_PROGRESS = 100;
 
     private final OperationType type;
@@ -66,6 +65,7 @@
 
     private final long totalBytes;
     private long bytesRead;
+    private long totalSourceCQLRows;
 
     /*
      * counters for merged rows.
@@ -106,6 +106,7 @@
                                              ? EmptyIterators.unfilteredPartition(controller.cfs.metadata, false)
                                              : UnfilteredPartitionIterators.merge(scanners, nowInSec, listener());
         boolean isForThrift = merged.isForThrift(); // to stop capture of iterator in Purger, which is confusing for debug
+        merged = Transformation.apply(merged, new GarbageSkipper(controller, nowInSec));
         merged = Transformation.apply(merged, new Purger(isForThrift, controller, nowInSec));
         this.compacted = DuplicateRowChecker.duringCompaction(merged, type);
     }
@@ -145,6 +146,11 @@
         return mergeCounters;
     }
 
+    public long getTotalSourceCQLRows()
+    {
+        return totalSourceCQLRows;
+    }
+
     private UnfilteredPartitionIterators.MergeListener listener()
     {
         return new UnfilteredPartitionIterators.MergeListener()
@@ -301,6 +307,7 @@
         @Override
         protected void updateProgress()
         {
+            totalSourceCQLRows++;
             if ((++compactedUnfiltered) % UNFILTERED_TO_UPDATE_PROGRESS == 0)
                 updateBytesRead();
         }
@@ -320,4 +327,237 @@
             return purgeEvaluator;
         }
     }
+
+    /**
+     * Unfiltered row iterator that removes deleted data as provided by a "tombstone source" for the partition.
+     * The result produced by this iterator is such that when merged with tombSource it produces the same output
+     * as the merge of dataSource and tombSource.
+     */
+    private static class GarbageSkippingUnfilteredRowIterator extends WrappingUnfilteredRowIterator
+    {
+        final UnfilteredRowIterator tombSource;
+        final DeletionTime partitionLevelDeletion;
+        final Row staticRow;
+        final ColumnFilter cf;
+        final int nowInSec;
+        final CFMetaData metadata;
+        final boolean cellLevelGC;
+
+        DeletionTime tombOpenDeletionTime = DeletionTime.LIVE;
+        DeletionTime dataOpenDeletionTime = DeletionTime.LIVE;
+        DeletionTime openDeletionTime = DeletionTime.LIVE;
+        DeletionTime partitionDeletionTime;
+        DeletionTime activeDeletionTime;
+        Unfiltered tombNext = null;
+        Unfiltered dataNext = null;
+        Unfiltered next = null;
+
+        /**
+         * Construct an iterator that filters out data shadowed by the provided "tombstone source".
+         *
+         * @param dataSource The input row. The result is a filtered version of this.
+         * @param tombSource Tombstone source, i.e. iterator used to identify deleted data in the input row.
+         * @param nowInSec Current time, used in choosing the winner when cell expiration is involved.
+         * @param cellLevelGC If false, the iterator will only look at row-level deletion times and tombstones.
+         *                    If true, deleted or overwritten cells within a surviving row will also be removed.
+         */
+        protected GarbageSkippingUnfilteredRowIterator(UnfilteredRowIterator dataSource, UnfilteredRowIterator tombSource, int nowInSec, boolean cellLevelGC)
+        {
+            super(dataSource);
+            this.tombSource = tombSource;
+            this.nowInSec = nowInSec;
+            this.cellLevelGC = cellLevelGC;
+            metadata = dataSource.metadata();
+            cf = ColumnFilter.all(metadata);
+
+            activeDeletionTime = partitionDeletionTime = tombSource.partitionLevelDeletion();
+
+            // Only preserve partition level deletion if not shadowed. (Note: Shadowing deletion must not be copied.)
+            this.partitionLevelDeletion = dataSource.partitionLevelDeletion().supersedes(tombSource.partitionLevelDeletion()) ?
+                    dataSource.partitionLevelDeletion() :
+                    DeletionTime.LIVE;
+
+            Row dataStaticRow = garbageFilterRow(dataSource.staticRow(), tombSource.staticRow());
+            this.staticRow = dataStaticRow != null ? dataStaticRow : Rows.EMPTY_STATIC_ROW;
+
+            tombNext = advance(tombSource);
+            dataNext = advance(dataSource);
+        }
+
+        private static Unfiltered advance(UnfilteredRowIterator source)
+        {
+            return source.hasNext() ? source.next() : null;
+        }
+
+        @Override
+        public DeletionTime partitionLevelDeletion()
+        {
+            return partitionLevelDeletion;
+        }
+
+        public void close()
+        {
+            super.close();
+            tombSource.close();
+        }
+
+        @Override
+        public Row staticRow()
+        {
+            return staticRow;
+        }
+
+        @Override
+        public boolean hasNext()
+        {
+            // Produce the next element. This may consume multiple elements from both inputs until we find something
+            // from dataSource that is still live. We track the currently open deletion in both sources, as well as the
+            // one we have last issued to the output. The tombOpenDeletionTime is used to filter out content; the others
+            // to decide whether or not a tombstone is superseded, and to be able to surface (the rest of) a deletion
+            // range from the input when a suppressing deletion ends.
+            while (next == null && dataNext != null)
+            {
+                int cmp = tombNext == null ? -1 : metadata.comparator.compare(dataNext, tombNext);
+                if (cmp < 0)
+                {
+                    if (dataNext.isRow())
+                        next = ((Row) dataNext).filter(cf, activeDeletionTime, false, metadata);
+                    else
+                        next = processDataMarker();
+                }
+                else if (cmp == 0)
+                {
+                    if (dataNext.isRow())
+                    {
+                        next = garbageFilterRow((Row) dataNext, (Row) tombNext);
+                    }
+                    else
+                    {
+                        tombOpenDeletionTime = updateOpenDeletionTime(tombOpenDeletionTime, tombNext);
+                        activeDeletionTime = Ordering.natural().max(partitionDeletionTime,
+                                                                    tombOpenDeletionTime);
+                        next = processDataMarker();
+                    }
+                }
+                else // (cmp > 0)
+                {
+                    if (tombNext.isRangeTombstoneMarker())
+                    {
+                        tombOpenDeletionTime = updateOpenDeletionTime(tombOpenDeletionTime, tombNext);
+                        activeDeletionTime = Ordering.natural().max(partitionDeletionTime,
+                                                                    tombOpenDeletionTime);
+                        boolean supersededBefore = openDeletionTime.isLive();
+                        boolean supersededAfter = !dataOpenDeletionTime.supersedes(activeDeletionTime);
+                        // If a range open was not issued because it was superseded and the deletion isn't superseded any more, we need to open it now.
+                        if (supersededBefore && !supersededAfter)
+                            next = new RangeTombstoneBoundMarker(((RangeTombstoneMarker) tombNext).closeBound(false).invert(), dataOpenDeletionTime);
+                        // If the deletion begins to be superseded, we don't close the range yet. This can save us a close/open pair if it ends after the superseding range.
+                    }
+                }
+
+                if (next instanceof RangeTombstoneMarker)
+                    openDeletionTime = updateOpenDeletionTime(openDeletionTime, next);
+
+                if (cmp <= 0)
+                    dataNext = advance(wrapped);
+                if (cmp >= 0)
+                    tombNext = advance(tombSource);
+            }
+            return next != null;
+        }
+
+        protected Row garbageFilterRow(Row dataRow, Row tombRow)
+        {
+            if (cellLevelGC)
+            {
+                return Rows.removeShadowedCells(dataRow, tombRow, activeDeletionTime, nowInSec);
+            }
+            else
+            {
+                DeletionTime deletion = Ordering.natural().max(tombRow.deletion().time(),
+                                                               activeDeletionTime);
+                return dataRow.filter(cf, deletion, false, metadata);
+            }
+        }
+
+        /**
+         * Decide how to act on a tombstone marker from the input iterator. We can decide what to issue depending on
+         * whether or not the ranges before and after the marker are superseded/live -- if none are, we can reuse the
+         * marker; if both are, the marker can be ignored; otherwise we issue a corresponding start/end marker.
+         */
+        private RangeTombstoneMarker processDataMarker()
+        {
+            dataOpenDeletionTime = updateOpenDeletionTime(dataOpenDeletionTime, dataNext);
+            boolean supersededBefore = openDeletionTime.isLive();
+            boolean supersededAfter = !dataOpenDeletionTime.supersedes(activeDeletionTime);
+            RangeTombstoneMarker marker = (RangeTombstoneMarker) dataNext;
+            if (!supersededBefore)
+                if (!supersededAfter)
+                    return marker;
+                else
+                    return new RangeTombstoneBoundMarker(marker.closeBound(false), marker.closeDeletionTime(false));
+            else
+                if (!supersededAfter)
+                    return new RangeTombstoneBoundMarker(marker.openBound(false), marker.openDeletionTime(false));
+                else
+                    return null;
+        }
+
+        @Override
+        public Unfiltered next()
+        {
+            if (!hasNext())
+                throw new IllegalStateException();
+
+            Unfiltered v = next;
+            next = null;
+            return v;
+        }
+
+        private DeletionTime updateOpenDeletionTime(DeletionTime openDeletionTime, Unfiltered next)
+        {
+            RangeTombstoneMarker marker = (RangeTombstoneMarker) next;
+            assert openDeletionTime.isLive() == !marker.isClose(false);
+            assert openDeletionTime.isLive() || openDeletionTime.equals(marker.closeDeletionTime(false));
+            return marker.isOpen(false) ? marker.openDeletionTime(false) : DeletionTime.LIVE;
+        }
+    }
+
+    /**
+     * Partition transformation applying GarbageSkippingUnfilteredRowIterator, obtaining tombstone sources for each
+     * partition using the controller's shadowSources method.
+     */
+    private static class GarbageSkipper extends Transformation<UnfilteredRowIterator>
+    {
+        final int nowInSec;
+        final CompactionController controller;
+        final boolean cellLevelGC;
+
+        private GarbageSkipper(CompactionController controller, int nowInSec)
+        {
+            this.controller = controller;
+            this.nowInSec = nowInSec;
+            cellLevelGC = controller.tombstoneOption == TombstoneOption.CELL;
+        }
+
+        @Override
+        protected UnfilteredRowIterator applyToPartition(UnfilteredRowIterator partition)
+        {
+            Iterable<UnfilteredRowIterator> sources = controller.shadowSources(partition.partitionKey(), !cellLevelGC);
+            if (sources == null)
+                return partition;
+            List<UnfilteredRowIterator> iters = new ArrayList<>();
+            for (UnfilteredRowIterator iter : sources)
+            {
+                if (!iter.isEmpty())
+                    iters.add(iter);
+                else
+                    iter.close();
+            }
+            if (iters.isEmpty())
+                return partition;
+
+            return new GarbageSkippingUnfilteredRowIterator(partition, UnfilteredRowIterators.merge(iters, nowInSec), nowInSec, cellLevelGC);
+        }
+    }
 }
diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionLogger.java b/src/java/org/apache/cassandra/db/compaction/CompactionLogger.java
new file mode 100644
index 0000000..1a4abfe
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/compaction/CompactionLogger.java
@@ -0,0 +1,360 @@
+/*
+ * 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.db.compaction;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.lang.ref.WeakReference;
+import java.nio.file.*;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import com.google.common.collect.MapMaker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.utils.NoSpamLogger;
+
+public class CompactionLogger
+{
+    public interface Strategy
+    {
+        JsonNode sstable(SSTableReader sstable);
+
+        JsonNode options();
+
+        static Strategy none = new Strategy()
+        {
+            public JsonNode sstable(SSTableReader sstable)
+            {
+                return null;
+            }
+
+            public JsonNode options()
+            {
+                return null;
+            }
+        };
+    }
+
+    /**
+     * This will produce the compaction strategy's starting information.
+     */
+    public interface StrategySummary
+    {
+        JsonNode getSummary();
+    }
+
+    /**
+     * This is an interface to allow writing to a different interface.
+     */
+    public interface Writer
+    {
+        /**
+         * This is used when we are already trying to write out the start of a
+         * @param statement This should be written out to the medium capturing the logs
+         * @param tag       This is an identifier for a strategy; each strategy should have a distinct Object
+         */
+        void writeStart(JsonNode statement, Object tag);
+
+        /**
+         * @param statement This should be written out to the medium capturing the logs
+         * @param summary   This can be used when a tag is not recognized by this writer; this can be because the file
+         *                  has been rolled, or otherwise the writer had to start over
+         * @param tag       This is an identifier for a strategy; each strategy should have a distinct Object
+         */
+        void write(JsonNode statement, StrategySummary summary, Object tag);
+    }
+
+    private interface CompactionStrategyAndTableFunction
+    {
+        JsonNode apply(AbstractCompactionStrategy strategy, SSTableReader sstable);
+    }
+
+    private static final JsonNodeFactory json = JsonNodeFactory.instance;
+    private static final Logger logger = LoggerFactory.getLogger(CompactionLogger.class);
+    private static final Writer serializer = new CompactionLogSerializer();
+    private final WeakReference<ColumnFamilyStore> cfsRef;
+    private final WeakReference<CompactionStrategyManager> csmRef;
+    private final AtomicInteger identifier = new AtomicInteger(0);
+    private final Map<AbstractCompactionStrategy, String> compactionStrategyMapping = new MapMaker().weakKeys().makeMap();
+    private final AtomicBoolean enabled = new AtomicBoolean(false);
+
+    public CompactionLogger(ColumnFamilyStore cfs, CompactionStrategyManager csm)
+    {
+        csmRef = new WeakReference<>(csm);
+        cfsRef = new WeakReference<>(cfs);
+    }
+
+    private void forEach(Consumer<AbstractCompactionStrategy> consumer)
+    {
+        CompactionStrategyManager csm = csmRef.get();
+        if (csm == null)
+            return;
+        csm.getStrategies()
+           .forEach(l -> l.forEach(consumer));
+    }
+
+    private ArrayNode compactionStrategyMap(Function<AbstractCompactionStrategy, JsonNode> select)
+    {
+        ArrayNode node = json.arrayNode();
+        forEach(acs -> node.add(select.apply(acs)));
+        return node;
+    }
+
+    private ArrayNode sstableMap(Collection<SSTableReader> sstables, CompactionStrategyAndTableFunction csatf)
+    {
+        CompactionStrategyManager csm = csmRef.get();
+        ArrayNode node = json.arrayNode();
+        if (csm == null)
+            return node;
+        sstables.forEach(t -> node.add(csatf.apply(csm.getCompactionStrategyFor(t), t)));
+        return node;
+    }
+
+    private String getId(AbstractCompactionStrategy strategy)
+    {
+        return compactionStrategyMapping.computeIfAbsent(strategy, s -> String.valueOf(identifier.getAndIncrement()));
+    }
+
+    private JsonNode formatSSTables(AbstractCompactionStrategy strategy)
+    {
+        ArrayNode node = json.arrayNode();
+        CompactionStrategyManager csm = csmRef.get();
+        ColumnFamilyStore cfs = cfsRef.get();
+        if (csm == null || cfs == null)
+            return node;
+        for (SSTableReader sstable : cfs.getLiveSSTables())
+        {
+            if (csm.getCompactionStrategyFor(sstable) == strategy)
+                node.add(formatSSTable(strategy, sstable));
+        }
+        return node;
+    }
+
+    private JsonNode formatSSTable(AbstractCompactionStrategy strategy, SSTableReader sstable)
+    {
+        ObjectNode node = json.objectNode();
+        node.put("generation", sstable.descriptor.generation);
+        node.put("version", sstable.descriptor.version.getVersion());
+        node.put("size", sstable.onDiskLength());
+        JsonNode logResult = strategy.strategyLogger().sstable(sstable);
+        if (logResult != null)
+            node.put("details", logResult);
+        return node;
+    }
+
+    private JsonNode startStrategy(AbstractCompactionStrategy strategy)
+    {
+        ObjectNode node = json.objectNode();
+        CompactionStrategyManager csm = csmRef.get();
+        if (csm == null)
+            return node;
+        node.put("strategyId", getId(strategy));
+        node.put("type", strategy.getName());
+        node.put("tables", formatSSTables(strategy));
+        node.put("repaired", csm.isRepaired(strategy));
+        List<String> folders = csm.getStrategyFolders(strategy);
+        ArrayNode folderNode = json.arrayNode();
+        for (String folder : folders)
+        {
+            folderNode.add(folder);
+        }
+        node.put("folders", folderNode);
+
+        JsonNode logResult = strategy.strategyLogger().options();
+        if (logResult != null)
+            node.put("options", logResult);
+        return node;
+    }
+
+    private JsonNode shutdownStrategy(AbstractCompactionStrategy strategy)
+    {
+        ObjectNode node = json.objectNode();
+        node.put("strategyId", getId(strategy));
+        return node;
+    }
+
+    private JsonNode describeSSTable(AbstractCompactionStrategy strategy, SSTableReader sstable)
+    {
+        ObjectNode node = json.objectNode();
+        node.put("strategyId", getId(strategy));
+        node.put("table", formatSSTable(strategy, sstable));
+        return node;
+    }
+
+    private void describeStrategy(ObjectNode node)
+    {
+        ColumnFamilyStore cfs = cfsRef.get();
+        if (cfs == null)
+            return;
+        node.put("keyspace", cfs.keyspace.getName());
+        node.put("table", cfs.getTableName());
+        node.put("time", System.currentTimeMillis());
+    }
+
+    private JsonNode startStrategies()
+    {
+        ObjectNode node = json.objectNode();
+        node.put("type", "enable");
+        describeStrategy(node);
+        node.put("strategies", compactionStrategyMap(this::startStrategy));
+        return node;
+    }
+
+    public void enable()
+    {
+        if (enabled.compareAndSet(false, true))
+        {
+            serializer.writeStart(startStrategies(), this);
+        }
+    }
+
+    public void disable()
+    {
+        if (enabled.compareAndSet(true, false))
+        {
+            ObjectNode node = json.objectNode();
+            node.put("type", "disable");
+            describeStrategy(node);
+            node.put("strategies", compactionStrategyMap(this::shutdownStrategy));
+            serializer.write(node, this::startStrategies, this);
+        }
+    }
+
+    public void flush(Collection<SSTableReader> sstables)
+    {
+        if (enabled.get())
+        {
+            ObjectNode node = json.objectNode();
+            node.put("type", "flush");
+            describeStrategy(node);
+            node.put("tables", sstableMap(sstables, this::describeSSTable));
+            serializer.write(node, this::startStrategies, this);
+        }
+    }
+
+    public void compaction(long startTime, Collection<SSTableReader> input, long endTime, Collection<SSTableReader> output)
+    {
+        if (enabled.get())
+        {
+            ObjectNode node = json.objectNode();
+            node.put("type", "compaction");
+            describeStrategy(node);
+            node.put("start", String.valueOf(startTime));
+            node.put("end", String.valueOf(endTime));
+            node.put("input", sstableMap(input, this::describeSSTable));
+            node.put("output", sstableMap(output, this::describeSSTable));
+            serializer.write(node, this::startStrategies, this);
+        }
+    }
+
+    public void pending(AbstractCompactionStrategy strategy, int remaining)
+    {
+        if (remaining != 0 && enabled.get())
+        {
+            ObjectNode node = json.objectNode();
+            node.put("type", "pending");
+            describeStrategy(node);
+            node.put("strategyId", getId(strategy));
+            node.put("pending", remaining);
+            serializer.write(node, this::startStrategies, this);
+        }
+    }
+
+    private static class CompactionLogSerializer implements Writer
+    {
+        private static final String logDirectory = System.getProperty("cassandra.logdir", ".");
+        private final ExecutorService loggerService = Executors.newFixedThreadPool(1);
+        // This is only accessed on the logger service thread, so it does not need to be thread safe
+        private final Set<Object> rolled = new HashSet<>();
+        private OutputStreamWriter stream;
+
+        private static OutputStreamWriter createStream() throws IOException
+        {
+            int count = 0;
+            Path compactionLog = Paths.get(logDirectory, "compaction.log");
+            if (Files.exists(compactionLog))
+            {
+                Path tryPath = compactionLog;
+                while (Files.exists(tryPath))
+                {
+                    tryPath = Paths.get(logDirectory, String.format("compaction-%d.log", count++));
+                }
+                Files.move(compactionLog, tryPath);
+            }
+
+            return new OutputStreamWriter(Files.newOutputStream(compactionLog, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE));
+        }
+
+        private void writeLocal(String toWrite)
+        {
+            try
+            {
+                if (stream == null)
+                    stream = createStream();
+                stream.write(toWrite);
+                stream.flush();
+            }
+            catch (IOException ioe)
+            {
+                // We'll drop the change and log the error to the logger.
+                NoSpamLogger.log(logger, NoSpamLogger.Level.ERROR, 1, TimeUnit.MINUTES,
+                                 "Could not write to the log file: {}", ioe);
+            }
+
+        }
+
+        public void writeStart(JsonNode statement, Object tag)
+        {
+            final String toWrite = statement.toString() + System.lineSeparator();
+            loggerService.execute(() -> {
+                rolled.add(tag);
+                writeLocal(toWrite);
+            });
+        }
+
+        public void write(JsonNode statement, StrategySummary summary, Object tag)
+        {
+            final String toWrite = statement.toString() + System.lineSeparator();
+            loggerService.execute(() -> {
+                if (!rolled.contains(tag))
+                {
+                    writeLocal(summary.getSummary().toString() + System.lineSeparator());
+                    rolled.add(tag);
+                }
+                writeLocal(toWrite);
+            });
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionManager.java b/src/java/org/apache/cassandra/db/compaction/CompactionManager.java
index 2b9ee50..82e0bf2 100644
--- a/src/java/org/apache/cassandra/db/compaction/CompactionManager.java
+++ b/src/java/org/apache/cassandra/db/compaction/CompactionManager.java
@@ -23,6 +23,7 @@
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 import javax.management.openmbean.OpenDataException;
 import javax.management.openmbean.TabularData;
 
@@ -33,6 +34,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import io.netty.util.concurrent.FastThreadLocal;
 import org.apache.cassandra.cache.AutoSavingCache;
 import org.apache.cassandra.concurrent.DebuggableThreadPoolExecutor;
 import org.apache.cassandra.concurrent.JMXEnabledThreadPoolExecutor;
@@ -44,6 +46,7 @@
 import org.apache.cassandra.db.compaction.CompactionInfo.Holder;
 import org.apache.cassandra.db.lifecycle.ILifecycleTransaction;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
+import org.apache.cassandra.db.lifecycle.SSTableIntervalTree;
 import org.apache.cassandra.db.lifecycle.SSTableSet;
 import org.apache.cassandra.db.lifecycle.View;
 import org.apache.cassandra.db.lifecycle.WrappedLifecycleTransaction;
@@ -64,6 +67,7 @@
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.metrics.CompactionMetrics;
 import org.apache.cassandra.repair.Validator;
+import org.apache.cassandra.schema.CompactionParams.TombstoneOption;
 import org.apache.cassandra.service.ActiveRepairService;
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.utils.*;
@@ -90,7 +94,7 @@
 
     // A thread local that tells us if the current thread is owned by the compaction manager. Used
     // by CounterContext to figure out if it should log a warning for invalid counter shards.
-    public static final ThreadLocal<Boolean> isCompactionManager = new ThreadLocal<Boolean>()
+    public static final FastThreadLocal<Boolean> isCompactionManager = new FastThreadLocal<Boolean>()
     {
         @Override
         protected Boolean initialValue()
@@ -299,7 +303,7 @@
             Iterable<SSTableReader> sstables = Lists.newArrayList(operation.filterSSTables(compacting));
             if (Iterables.isEmpty(sstables))
             {
-                logger.info("No sstables for {}.{}", cfs.keyspace.getName(), cfs.name);
+                logger.info("No sstables to {} for {}.{}", operationType.name(), cfs.keyspace.getName(), cfs.name);
                 return AllSSTableOpStatus.SUCCESSFUL;
             }
 
@@ -439,7 +443,7 @@
             }
 
             @Override
-            public void execute(LifecycleTransaction txn) throws IOException
+            public void execute(LifecycleTransaction txn)
             {
                 AbstractCompactionTask task = cfs.getCompactionStrategyManager().getCompactionTask(txn, NO_GC, Long.MAX_VALUE);
                 task.setUserDefined(true);
@@ -486,7 +490,7 @@
                 }
                 logger.info("Skipping cleanup for {}/{} sstables for {}.{} since they are fully contained in owned ranges ({})",
                             skippedSStables, totalSSTables, cfStore.keyspace.getName(), cfStore.getTableName(), ranges);
-                sortedSSTables.sort(new SSTableReader.SizeComparator());
+                sortedSSTables.sort(SSTableReader.sizeComparator);
                 return sortedSSTables;
             }
 
@@ -499,6 +503,121 @@
         }, jobs, OperationType.CLEANUP);
     }
 
+    public AllSSTableOpStatus performGarbageCollection(final ColumnFamilyStore cfStore, TombstoneOption tombstoneOption, int jobs) throws InterruptedException, ExecutionException
+    {
+        assert !cfStore.isIndex();
+
+        return parallelAllSSTableOperation(cfStore, new OneSSTableOperation()
+        {
+            @Override
+            public Iterable<SSTableReader> filterSSTables(LifecycleTransaction transaction)
+            {
+                Iterable<SSTableReader> originals = transaction.originals();
+                if (cfStore.getCompactionStrategyManager().onlyPurgeRepairedTombstones())
+                    originals = Iterables.filter(originals, SSTableReader::isRepaired);
+                List<SSTableReader> sortedSSTables = Lists.newArrayList(originals);
+                Collections.sort(sortedSSTables, SSTableReader.maxTimestampAscending);
+                return sortedSSTables;
+            }
+
+            @Override
+            public void execute(LifecycleTransaction txn) throws IOException
+            {
+                logger.debug("Garbage collecting {}", txn.originals());
+                CompactionTask task = new CompactionTask(cfStore, txn, getDefaultGcBefore(cfStore, FBUtilities.nowInSeconds()))
+                {
+                    @Override
+                    protected CompactionController getCompactionController(Set<SSTableReader> toCompact)
+                    {
+                        return new CompactionController(cfStore, toCompact, gcBefore, null, tombstoneOption);
+                    }
+
+                    @Override
+                    protected int getLevel()
+                    {
+                        return txn.onlyOne().getSSTableLevel();
+                    }
+                };
+                task.setUserDefined(true);
+                task.setCompactionType(OperationType.GARBAGE_COLLECT);
+                task.execute(metrics);
+            }
+        }, jobs, OperationType.GARBAGE_COLLECT);
+    }
+
+    public AllSSTableOpStatus relocateSSTables(final ColumnFamilyStore cfs, int jobs) throws ExecutionException, InterruptedException
+    {
+        if (!cfs.getPartitioner().splitter().isPresent())
+        {
+            logger.info("Partitioner does not support splitting");
+            return AllSSTableOpStatus.ABORTED;
+        }
+        final Collection<Range<Token>> r = StorageService.instance.getLocalRanges(cfs.keyspace.getName());
+
+        if (r.isEmpty())
+        {
+            logger.info("Relocate cannot run before a node has joined the ring");
+            return AllSSTableOpStatus.ABORTED;
+        }
+
+        final DiskBoundaries diskBoundaries = cfs.getDiskBoundaries();
+
+        return parallelAllSSTableOperation(cfs, new OneSSTableOperation()
+        {
+            @Override
+            public Iterable<SSTableReader> filterSSTables(LifecycleTransaction transaction)
+            {
+                Set<SSTableReader> originals = Sets.newHashSet(transaction.originals());
+                Set<SSTableReader> needsRelocation = originals.stream().filter(s -> !inCorrectLocation(s)).collect(Collectors.toSet());
+                transaction.cancel(Sets.difference(originals, needsRelocation));
+
+                Map<Integer, List<SSTableReader>> groupedByDisk = groupByDiskIndex(needsRelocation);
+
+                int maxSize = 0;
+                for (List<SSTableReader> diskSSTables : groupedByDisk.values())
+                    maxSize = Math.max(maxSize, diskSSTables.size());
+
+                List<SSTableReader> mixedSSTables = new ArrayList<>();
+
+                for (int i = 0; i < maxSize; i++)
+                    for (List<SSTableReader> diskSSTables : groupedByDisk.values())
+                        if (i < diskSSTables.size())
+                            mixedSSTables.add(diskSSTables.get(i));
+
+                return mixedSSTables;
+            }
+
+            public Map<Integer, List<SSTableReader>> groupByDiskIndex(Set<SSTableReader> needsRelocation)
+            {
+                return needsRelocation.stream().collect(Collectors.groupingBy((s) -> diskBoundaries.getDiskIndex(s)));
+            }
+
+            private boolean inCorrectLocation(SSTableReader sstable)
+            {
+                if (!cfs.getPartitioner().splitter().isPresent())
+                    return true;
+
+                int diskIndex = diskBoundaries.getDiskIndex(sstable);
+                PartitionPosition diskLast = diskBoundaries.positions.get(diskIndex);
+
+                // the location we get from directoryIndex is based on the first key in the sstable
+                // now we need to make sure the last key is less than the boundary as well:
+                Directories.DataDirectory dataDirectory = cfs.getDirectories().getDataDirectoryForFile(sstable.descriptor);
+                return diskBoundaries.directories.get(diskIndex).equals(dataDirectory) && sstable.last.compareTo(diskLast) <= 0;
+            }
+
+            @Override
+            public void execute(LifecycleTransaction txn)
+            {
+                logger.debug("Relocating {}", txn.originals());
+                AbstractCompactionTask task = cfs.getCompactionStrategyManager().getCompactionTask(txn, NO_GC, Long.MAX_VALUE);
+                task.setUserDefined(true);
+                task.setCompactionType(OperationType.RELOCATE);
+                task.execute(metrics);
+            }
+        }, jobs, OperationType.RELOCATE);
+    }
+
     /**
      * Submit anti-compactions for a collection of SSTables over a set of repaired ranges and marks corresponding SSTables
      * as repaired.
@@ -516,7 +635,8 @@
                                           final long repairedAt,
                                           final UUID parentRepairSession)
     {
-        Runnable runnable = new WrappedRunnable() {
+        Runnable runnable = new WrappedRunnable()
+        {
             @Override
             @SuppressWarnings("resource")
             public void runMayThrow() throws Exception
@@ -666,14 +786,16 @@
             return Collections.emptyList();
 
         List<Future<?>> futures = new ArrayList<>();
+
         int nonEmptyTasks = 0;
         for (final AbstractCompactionTask task : tasks)
         {
             if (task.transaction.originals().size() > 0)
                 nonEmptyTasks++;
+
             Runnable runnable = new WrappedRunnable()
             {
-                protected void runMayThrow() throws IOException
+                protected void runMayThrow()
                 {
                     task.execute(metrics);
                 }
@@ -684,10 +806,78 @@
                 futures.add(fut);
         }
         if (nonEmptyTasks > 1)
-            logger.info("Cannot perform a full major compaction as repaired and unrepaired sstables cannot be compacted together. These two set of sstables will be compacted separately.");
+            logger.info("Major compaction will not result in a single sstable - repaired and unrepaired data is kept separate and compaction runs per data_file_directory.");
+
+
         return futures;
     }
 
+    /**
+     * Forces a major compaction of specified token ranges of the specified column family.
+     * <p>
+     * The token ranges will be interpreted as closed intervals to match the closed interval defined by the first and
+     * last keys of a sstable, even though the {@link Range} class is suppossed to be half-open by definition.
+     *
+     * @param cfStore The column family store to be compacted.
+     * @param ranges The token ranges to be compacted, interpreted as closed intervals.
+     */
+    public void forceCompactionForTokenRange(ColumnFamilyStore cfStore, Collection<Range<Token>> ranges)
+    {
+        Callable<Collection<AbstractCompactionTask>> taskCreator = () -> {
+            Collection<SSTableReader> sstables = sstablesInBounds(cfStore, ranges);
+            if (sstables == null || sstables.isEmpty())
+            {
+                logger.debug("No sstables found for the provided token range");
+                return null;
+            }
+            return cfStore.getCompactionStrategyManager().getUserDefinedTasks(sstables, getDefaultGcBefore(cfStore, FBUtilities.nowInSeconds()));
+        };
+
+        final Collection<AbstractCompactionTask> tasks = cfStore.runWithCompactionsDisabled(taskCreator, false, false);
+
+        if (tasks == null)
+            return;
+
+        Runnable runnable = new WrappedRunnable()
+        {
+            protected void runMayThrow() throws Exception
+            {
+                try
+                {
+                    for (AbstractCompactionTask task : tasks)
+                        if (task != null)
+                            task.execute(metrics);
+                }
+                finally
+                {
+                    FBUtilities.closeAll(tasks.stream().map(task -> task.transaction).collect(Collectors.toList()));
+                }
+            }
+        };
+
+        FBUtilities.waitOnFuture(executor.submitIfRunning(runnable, "force compaction for token range"));
+    }
+
+    /**
+     * Returns the sstables of the specified column family store that intersect with the specified token ranges.
+     * <p>
+     * The token ranges will be interpreted as closed intervals to match the closed interval defined by the first and
+     * last keys of a sstable, even though the {@link Range} class is suppossed to be half-open by definition.
+     */
+    private static Collection<SSTableReader> sstablesInBounds(ColumnFamilyStore cfs, Collection<Range<Token>> tokenRangeCollection)
+    {
+        final Set<SSTableReader> sstables = new HashSet<>();
+        Iterable<SSTableReader> liveTables = cfs.getTracker().getView().select(SSTableSet.LIVE);
+        SSTableIntervalTree tree = SSTableIntervalTree.build(liveTables);
+
+        for (Range<Token> tokenRange : tokenRangeCollection)
+        {
+            Iterable<SSTableReader> ssTableReaders = View.sstablesInBounds(tokenRange.left.minKeyBound(), tokenRange.right.maxKeyBound(), tree);
+            Iterables.addAll(sstables, ssTableReaders);
+        }
+        return sstables;
+    }
+
     public void forceUserDefinedCompaction(String dataFiles)
     {
         String[] filenames = dataFiles.split(",");
@@ -714,11 +904,66 @@
         FBUtilities.waitOnFutures(futures);
     }
 
+    public void forceUserDefinedCleanup(String dataFiles)
+    {
+        String[] filenames = dataFiles.split(",");
+        HashMap<ColumnFamilyStore, Descriptor> descriptors = Maps.newHashMap();
+
+        for (String filename : filenames)
+        {
+            // extract keyspace and columnfamily name from filename
+            Descriptor desc = Descriptor.fromFilename(filename.trim());
+            if (Schema.instance.getCFMetaData(desc) == null)
+            {
+                logger.warn("Schema does not exist for file {}. Skipping.", filename);
+                continue;
+            }
+            // group by keyspace/columnfamily
+            ColumnFamilyStore cfs = Keyspace.open(desc.ksname).getColumnFamilyStore(desc.cfname);
+            desc = cfs.getDirectories().find(new File(filename.trim()).getName());
+            if (desc != null)
+                descriptors.put(cfs, desc);
+        }
+
+        if (!StorageService.instance.isJoined())
+        {
+            logger.error("Cleanup cannot run before a node has joined the ring");
+            return;
+        }
+
+        for (Map.Entry<ColumnFamilyStore,Descriptor> entry : descriptors.entrySet())
+        {
+            ColumnFamilyStore cfs = entry.getKey();
+            Keyspace keyspace = cfs.keyspace;
+            Collection<Range<Token>> ranges = StorageService.instance.getLocalRanges(keyspace.getName());
+            boolean hasIndexes = cfs.indexManager.hasIndexes();
+            SSTableReader sstable = lookupSSTable(cfs, entry.getValue());
+
+            if (sstable == null)
+            {
+                logger.warn("Will not clean {}, it is not an active sstable", entry.getValue());
+            }
+            else
+            {
+                CleanupStrategy cleanupStrategy = CleanupStrategy.get(cfs, ranges, FBUtilities.nowInSeconds());
+                try (LifecycleTransaction txn = cfs.getTracker().tryModify(sstable, OperationType.CLEANUP))
+                {
+                    doCleanupOne(cfs, txn, cleanupStrategy, ranges, hasIndexes);
+                }
+                catch (IOException e)
+                {
+                    logger.error("forceUserDefinedCleanup failed: {}", e.getLocalizedMessage());
+                }
+            }
+        }
+    }
+
+
     public Future<?> submitUserDefined(final ColumnFamilyStore cfs, final Collection<Descriptor> dataFiles, final int gcBefore)
     {
         Runnable runnable = new WrappedRunnable()
         {
-            protected void runMayThrow() throws IOException
+            protected void runMayThrow() throws Exception
             {
                 // look up the sstables now that we're on the compaction executor, so we don't try to re-compact
                 // something that was already being compacted earlier.
@@ -743,9 +988,19 @@
                 }
                 else
                 {
-                    AbstractCompactionTask task = cfs.getCompactionStrategyManager().getUserDefinedTask(sstables, gcBefore);
-                    if (task != null)
-                        task.execute(metrics);
+                    List<AbstractCompactionTask> tasks = cfs.getCompactionStrategyManager().getUserDefinedTasks(sstables, gcBefore);
+                    try
+                    {
+                        for (AbstractCompactionTask task : tasks)
+                        {
+                            if (task != null)
+                                task.execute(metrics);
+                        }
+                    }
+                    finally
+                    {
+                        FBUtilities.closeAll(tasks.stream().map(task -> task.transaction).collect(Collectors.toList()));
+                    }
                 }
             }
         };
@@ -924,19 +1179,24 @@
 
         logger.info("Cleaning up {}", sstable);
 
-        File compactionFileLocation = cfs.getDirectories().getWriteableLocationAsFile(cfs.getExpectedCompactedFileSize(txn.originals(), OperationType.CLEANUP));
-        if (compactionFileLocation == null)
-            throw new IOException("disk full");
+        File compactionFileLocation = sstable.descriptor.directory;
+        RateLimiter limiter = getRateLimiter();
+        double compressionRatio = sstable.getCompressionRatio();
+        if (compressionRatio == MetadataCollector.NO_COMPRESSION_RATIO)
+            compressionRatio = 1.0;
 
         List<SSTableReader> finished;
+
         int nowInSec = FBUtilities.nowInSeconds();
-        try (SSTableRewriter writer = SSTableRewriter.construct(cfs, txn, false, sstable.maxDataAge, false);
-             ISSTableScanner scanner = cleanupStrategy.getScanner(sstable, getRateLimiter());
+        try (SSTableRewriter writer = SSTableRewriter.construct(cfs, txn, false, sstable.maxDataAge);
+             ISSTableScanner scanner = cleanupStrategy.getScanner(sstable, null);
              CompactionController controller = new CompactionController(cfs, txn.originals(), getDefaultGcBefore(cfs, nowInSec));
              Refs<SSTableReader> refs = Refs.ref(Collections.singleton(sstable));
              CompactionIterator ci = new CompactionIterator(OperationType.CLEANUP, Collections.singletonList(scanner), controller, nowInSec, UUIDGen.getTimeUUID(), metrics))
         {
             writer.switchWriter(createWriter(cfs, compactionFileLocation, expectedBloomFilterSize, sstable.getSSTableMetadata().repairedAt, sstable, txn));
+            long lastBytesScanned = 0;
+
 
             while (ci.hasNext())
             {
@@ -951,6 +1211,12 @@
 
                     if (writer.append(notCleaned) != null)
                         totalkeysWritten++;
+
+                    long bytesScanned = scanner.getBytesScanned();
+
+                    compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio);
+
+                    lastBytesScanned = bytesScanned;
                 }
             }
 
@@ -962,18 +1228,33 @@
 
         if (!finished.isEmpty())
         {
-            String format = "Cleaned up to %s.  %,d to %,d (~%d%% of original) bytes for %,d keys.  Time: %,dms.";
+            String format = "Cleaned up to %s.  %s to %s (~%d%% of original) for %,d keys.  Time: %,dms.";
             long dTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
             long startsize = sstable.onDiskLength();
             long endsize = 0;
             for (SSTableReader newSstable : finished)
                 endsize += newSstable.onDiskLength();
             double ratio = (double) endsize / (double) startsize;
-            logger.info(String.format(format, finished.get(0).getFilename(), startsize, endsize, (int) (ratio * 100), totalkeysWritten, dTime));
+            logger.info(String.format(format, finished.get(0).getFilename(), FBUtilities.prettyPrintMemory(startsize),
+                                      FBUtilities.prettyPrintMemory(endsize), (int) (ratio * 100), totalkeysWritten, dTime));
         }
 
     }
 
+    static void compactionRateLimiterAcquire(RateLimiter limiter, long bytesScanned, long lastBytesScanned, double compressionRatio)
+    {
+        long lengthRead = (long) ((bytesScanned - lastBytesScanned) * compressionRatio) + 1;
+        while (lengthRead >= Integer.MAX_VALUE)
+        {
+            limiter.acquire(Integer.MAX_VALUE);
+            lengthRead -= Integer.MAX_VALUE;
+        }
+        if (lengthRead > 0)
+        {
+            limiter.acquire((int) lengthRead);
+        }
+    }
+
     private static abstract class CleanupStrategy
     {
         protected final Collection<Range<Token>> ranges;
@@ -1071,6 +1352,7 @@
                                     repairedAt,
                                     sstable.getSSTableLevel(),
                                     header,
+                                    cfs.indexManager.listIndexes(),
                                     txn);
     }
 
@@ -1103,6 +1385,7 @@
                                     cfs.metadata,
                                     new MetadataCollector(sstables, cfs.metadata.comparator, minLevel),
                                     SerializationHeader.make(cfs.metadata, sstables),
+                                    cfs.indexManager.listIndexes(),
                                     txn);
     }
 
@@ -1157,7 +1440,7 @@
                 StorageService.instance.forceKeyspaceFlush(cfs.keyspace.getName(), cfs.name);
                 sstables = getSSTablesToValidate(cfs, validator);
                 if (sstables == null)
-                    return; // this means the parent repair session was removed - the repair session failed on another node and we removed i
+                    return; // this means the parent repair session was removed - the repair session failed on another node and we removed it
                 if (validator.gcBefore > 0)
                     gcBefore = validator.gcBefore;
                 else
@@ -1167,11 +1450,18 @@
             // Create Merkle trees suitable to hold estimated partitions for the given ranges.
             // We blindly assume that a partition is evenly distributed on all sstables for now.
             MerkleTrees tree = createMerkleTrees(sstables, validator.desc.ranges, cfs);
+            RateLimiter limiter = getRateLimiter();
             long start = System.nanoTime();
             try (AbstractCompactionStrategy.ScannerList scanners = cfs.getCompactionStrategyManager().getScanners(sstables, validator.desc.ranges);
                  ValidationCompactionController controller = new ValidationCompactionController(cfs, gcBefore);
                  CompactionIterator ci = new ValidationCompactionIterator(scanners.scanners, controller, nowInSec, metrics))
             {
+                double compressionRatio = scanners.getCompressionRatio();
+                if (compressionRatio == MetadataCollector.NO_COMPRESSION_RATIO)
+                    compressionRatio = 1.0;
+
+                long lastBytesScanned = 0;
+
                 // validate the CF as we iterate over it
                 validator.prepare(cfs, tree);
                 while (ci.hasNext())
@@ -1182,6 +1472,11 @@
                     {
                         validator.add(partition);
                     }
+
+                    long bytesScanned = scanners.getTotalBytesScanned();
+                    compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio);
+                    lastBytesScanned = bytesScanned;
+
                 }
                 validator.complete();
             }
@@ -1214,7 +1509,7 @@
     {
         MerkleTrees tree = new MerkleTrees(cfs.getPartitioner());
         long allPartitions = 0;
-        Map<Range<Token>, Long> rangePartitionCounts = new HashMap<>();
+        Map<Range<Token>, Long> rangePartitionCounts = Maps.newHashMapWithExpectedSize(ranges.size());
         for (Range<Token> range : ranges)
         {
             long numPartitions = 0;
@@ -1376,8 +1671,8 @@
 
         CompactionStrategyManager strategy = cfs.getCompactionStrategyManager();
         try (SharedTxn sharedTxn = new SharedTxn(anticompactionGroup);
-             SSTableRewriter repairedSSTableWriter = new SSTableRewriter(sharedTxn, groupMaxDataAge, false, false);
-             SSTableRewriter unRepairedSSTableWriter = new SSTableRewriter(sharedTxn, groupMaxDataAge, false, false);
+             SSTableRewriter repairedSSTableWriter = SSTableRewriter.constructWithoutEarlyOpening(sharedTxn, false, groupMaxDataAge);
+             SSTableRewriter unRepairedSSTableWriter = SSTableRewriter.constructWithoutEarlyOpening(sharedTxn, false, groupMaxDataAge);
              AbstractCompactionStrategy.ScannerList scanners = strategy.getScanners(anticompactionGroup.originals());
              CompactionController controller = new CompactionController(cfs, sstableAsSet, getDefaultGcBefore(cfs, nowInSec));
              CompactionIterator ci = new CompactionIterator(OperationType.ANTICOMPACTION, scanners.scanners, controller, nowInSec, UUIDGen.getTimeUUID(), metrics))
@@ -1610,7 +1905,7 @@
         public void afterExecute(Runnable r, Throwable t)
         {
             DebuggableThreadPoolExecutor.maybeResetTraceSessionWrapper(r);
-    
+
             if (t == null)
                 t = DebuggableThreadPoolExecutor.extractThrowable(r);
 
@@ -1769,6 +2064,22 @@
         }
     }
 
+    public void setConcurrentCompactors(int value)
+    {
+        if (value > executor.getCorePoolSize())
+        {
+            // we are increasing the value
+            executor.setMaximumPoolSize(value);
+            executor.setCorePoolSize(value);
+        }
+        else if (value < executor.getCorePoolSize())
+        {
+            // we are reducing the value
+            executor.setCorePoolSize(value);
+            executor.setMaximumPoolSize(value);
+        }
+    }
+
     public int getCoreCompactorThreads()
     {
         return executor.getCorePoolSize();
diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionManagerMBean.java b/src/java/org/apache/cassandra/db/compaction/CompactionManagerMBean.java
index d5da0fe..8785b41 100644
--- a/src/java/org/apache/cassandra/db/compaction/CompactionManagerMBean.java
+++ b/src/java/org/apache/cassandra/db/compaction/CompactionManagerMBean.java
@@ -19,7 +19,6 @@
 
 import java.util.List;
 import java.util.Map;
-import java.util.UUID;
 import javax.management.openmbean.TabularData;
 
 public interface CompactionManagerMBean
@@ -45,6 +44,17 @@
     public void forceUserDefinedCompaction(String dataFiles);
 
     /**
+     * Triggers the cleanup of user specified sstables.
+     * You can specify files from various keyspaces and columnfamilies.
+     * If you do so, cleanup is performed each file individually
+     *
+     * @param dataFiles a comma separated list of sstable file to cleanup.
+     *                  must contain keyspace and columnfamily name in path(for 2.1+) or file name itself.
+     */
+    public void forceUserDefinedCleanup(String dataFiles);
+
+
+    /**
      * Stop all running compaction-like tasks having the provided {@code type}.
      * @param type the type of compaction to stop. Can be one of:
      *   - COMPACTION
diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java
index 5d4a254..7169245 100644
--- a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java
+++ b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java
@@ -20,6 +20,14 @@
 
 import java.util.*;
 import java.util.concurrent.Callable;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Iterables;
+import com.google.common.primitives.Ints;
 
 import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
 import org.slf4j.Logger;
@@ -28,12 +36,14 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.db.DiskBoundaries;
 import org.apache.cassandra.db.Memtable;
 import org.apache.cassandra.db.SerializationHeader;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
 import org.apache.cassandra.db.lifecycle.SSTableSet;
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.index.Index;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.SSTableMultiWriter;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
@@ -46,35 +56,77 @@
 /**
  * Manages the compaction strategies.
  *
- * Currently has two instances of actual compaction strategies - one for repaired data and one for
+ * Currently has two instances of actual compaction strategies per data directory - one for repaired data and one for
  * unrepaired data. This is done to be able to totally separate the different sets of sstables.
+ *
+ * Operations on this class are guarded by a {@link ReentrantReadWriteLock}. This lock performs mutual exclusion on
+ * reads and writes to the following variables: {@link this#repaired}, {@link this#unrepaired}, {@link this#isActive},
+ * {@link this#params}, {@link this#currentBoundaries}. Whenever performing reads on these variables,
+ * the {@link this#readLock} should be acquired. Likewise, updates to these variables should be guarded by
+ * {@link this#writeLock}.
+ *
+ * Whenever the {@link DiskBoundaries} change, the compaction strategies must be reloaded, so in order to ensure
+ * the compaction strategy placement reflect most up-to-date disk boundaries, call {@link this#maybeReloadDiskBoundaries()}
+ * before acquiring the read lock to acess the strategies.
+ *
  */
 public class CompactionStrategyManager implements INotificationConsumer
 {
     private static final Logger logger = LoggerFactory.getLogger(CompactionStrategyManager.class);
+    public final CompactionLogger compactionLogger;
     private final ColumnFamilyStore cfs;
-    private volatile AbstractCompactionStrategy repaired;
-    private volatile AbstractCompactionStrategy unrepaired;
-    private volatile boolean enabled = true;
-    public boolean isActive = true;
+    private final boolean partitionSSTablesByTokenRange;
+    private final Supplier<DiskBoundaries> boundariesSupplier;
+
+    /**
+     * Performs mutual exclusion on the variables below
+     */
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+    private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
+    private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
+
+    /**
+     * Variables guarded by read and write lock above
+     */
+    //TODO check possibility of getting rid of these locks by encapsulating these in an immutable atomic object
+    private final List<AbstractCompactionStrategy> repaired = new ArrayList<>();
+    private final List<AbstractCompactionStrategy> unrepaired = new ArrayList<>();
     private volatile CompactionParams params;
-    /*
+    private DiskBoundaries currentBoundaries;
+    private volatile boolean enabled = true;
+    private volatile boolean isActive = true;
+
+    /**
         We keep a copy of the schema compaction parameters here to be able to decide if we
-        should update the compaction strategy in maybeReloadCompactionStrategy() due to an ALTER.
+        should update the compaction strategy in {@link this#maybeReload(CFMetaData)} due to an ALTER.
 
         If a user changes the local compaction strategy and then later ALTERs a compaction parameter,
         we will use the new compaction parameters.
-     */
-    private CompactionParams schemaCompactionParams;
+     **/
+    private volatile CompactionParams schemaCompactionParams;
+    private volatile boolean supportsEarlyOpen;
+    private volatile int fanout;
+    private volatile long maxSSTableSizeBytes;
+    private volatile String name;
 
     public CompactionStrategyManager(ColumnFamilyStore cfs)
     {
+        this(cfs, cfs::getDiskBoundaries, cfs.getPartitioner().splitter().isPresent());
+    }
+
+    @VisibleForTesting
+    public CompactionStrategyManager(ColumnFamilyStore cfs, Supplier<DiskBoundaries> boundariesSupplier,
+                                     boolean partitionSSTablesByTokenRange)
+    {
         cfs.getTracker().subscribe(this);
         logger.trace("{} subscribed to the data tracker.", this);
         this.cfs = cfs;
-        reload(cfs.metadata);
+        this.compactionLogger = new CompactionLogger(cfs, this);
+        this.boundariesSupplier = boundariesSupplier;
+        this.partitionSSTablesByTokenRange = partitionSSTablesByTokenRange;
         params = cfs.metadata.params.compaction;
         enabled = params.isEnabled();
+        reload(cfs.metadata.params.compaction);
     }
 
     /**
@@ -83,27 +135,32 @@
      * Returns a task for the compaction strategy that needs it the most (most estimated remaining tasks)
      *
      */
-    public synchronized AbstractCompactionTask getNextBackgroundTask(int gcBefore)
+    public AbstractCompactionTask getNextBackgroundTask(int gcBefore)
     {
-        if (!isEnabled())
-            return null;
-
-        maybeReload(cfs.metadata);
-
-        if (repaired.getEstimatedRemainingTasks() > unrepaired.getEstimatedRemainingTasks())
+        maybeReloadDiskBoundaries();
+        readLock.lock();
+        try
         {
-            AbstractCompactionTask repairedTask = repaired.getNextBackgroundTask(gcBefore);
-            if (repairedTask != null)
-                return repairedTask;
-            return unrepaired.getNextBackgroundTask(gcBefore);
+            if (!isEnabled())
+                return null;
+
+            List<AbstractCompactionStrategy> strategies = new ArrayList<>();
+
+            strategies.addAll(repaired);
+            strategies.addAll(unrepaired);
+            Collections.sort(strategies, (o1, o2) -> Ints.compare(o2.getEstimatedRemainingTasks(), o1.getEstimatedRemainingTasks()));
+            for (AbstractCompactionStrategy strategy : strategies)
+            {
+                AbstractCompactionTask task = strategy.getNextBackgroundTask(gcBefore);
+                if (task != null)
+                    return task;
+            }
         }
-        else
+        finally
         {
-            AbstractCompactionTask unrepairedTask = unrepaired.getNextBackgroundTask(gcBefore);
-            if (unrepairedTask != null)
-                return unrepairedTask;
-            return repaired.getNextBackgroundTask(gcBefore);
+            readLock.unlock();
         }
+        return null;
     }
 
     public boolean isEnabled()
@@ -111,9 +168,22 @@
         return enabled && isActive;
     }
 
-    public synchronized void resume()
+    public boolean isActive()
     {
-        isActive = true;
+        return isActive;
+    }
+
+    public void resume()
+    {
+        writeLock.lock();
+        try
+        {
+            isActive = true;
+        }
+        finally
+        {
+            writeLock.unlock();
+        }
     }
 
     /**
@@ -121,66 +191,202 @@
      *
      * Separate call from enable/disable to not have to save the enabled-state externally
       */
-    public synchronized void pause()
+    public void pause()
     {
-        isActive = false;
-    }
+        writeLock.lock();
+        try
+        {
+            isActive = false;
+        }
+        finally
+        {
+            writeLock.unlock();
+        }
 
+    }
 
     private void startup()
     {
-        for (SSTableReader sstable : cfs.getSSTables(SSTableSet.CANONICAL))
+        writeLock.lock();
+        try
         {
-            if (sstable.openReason != SSTableReader.OpenReason.EARLY)
-                getCompactionStrategyFor(sstable).addSSTable(sstable);
+            for (SSTableReader sstable : cfs.getSSTables(SSTableSet.CANONICAL))
+            {
+                if (sstable.openReason != SSTableReader.OpenReason.EARLY)
+                    compactionStrategyFor(sstable).addSSTable(sstable);
+            }
+            repaired.forEach(AbstractCompactionStrategy::startup);
+            unrepaired.forEach(AbstractCompactionStrategy::startup);
+            supportsEarlyOpen = repaired.get(0).supportsEarlyOpen();
+            fanout = (repaired.get(0) instanceof LeveledCompactionStrategy) ? ((LeveledCompactionStrategy) repaired.get(0)).getLevelFanoutSize() : LeveledCompactionStrategy.DEFAULT_LEVEL_FANOUT_SIZE;
+            name = repaired.get(0).getName();
+            maxSSTableSizeBytes = repaired.get(0).getMaxSSTableBytes();
         }
-        repaired.startup();
-        unrepaired.startup();
+        finally
+        {
+            writeLock.unlock();
+        }
+        repaired.forEach(AbstractCompactionStrategy::startup);
+        unrepaired.forEach(AbstractCompactionStrategy::startup);
+        if (Stream.concat(repaired.stream(), unrepaired.stream()).anyMatch(cs -> cs.logAll))
+            compactionLogger.enable();
     }
 
     /**
      * return the compaction strategy for the given sstable
      *
-     * returns differently based on the repaired status
+     * returns differently based on the repaired status and which vnode the compaction strategy belongs to
      * @param sstable
      * @return
      */
-    private AbstractCompactionStrategy getCompactionStrategyFor(SSTableReader sstable)
+    protected AbstractCompactionStrategy getCompactionStrategyFor(SSTableReader sstable)
     {
-        if (sstable.isRepaired())
-            return repaired;
-        else
-            return unrepaired;
+        maybeReloadDiskBoundaries();
+        return compactionStrategyFor(sstable);
+    }
+
+    @VisibleForTesting
+    protected AbstractCompactionStrategy compactionStrategyFor(SSTableReader sstable)
+    {
+        // should not call maybeReloadDiskBoundaries because it may be called from within lock
+        readLock.lock();
+        try
+        {
+            int index = compactionStrategyIndexFor(sstable);
+            if (sstable.isRepaired())
+                return repaired.get(index);
+            else
+                return unrepaired.get(index);
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+    }
+
+    /**
+     * Get the correct compaction strategy for the given sstable. If the first token starts within a disk boundary, we
+     * will add it to that compaction strategy.
+     *
+     * In the case we are upgrading, the first compaction strategy will get most files - we do not care about which disk
+     * the sstable is on currently (unless we don't know the local tokens yet). Once we start compacting we will write out
+     * sstables in the correct locations and give them to the correct compaction strategy instance.
+     *
+     * @param sstable
+     * @return
+     */
+    int compactionStrategyIndexFor(SSTableReader sstable)
+    {
+        // should not call maybeReload because it may be called from within lock
+        readLock.lock();
+        try
+        {
+            //We only have a single compaction strategy when sstables are not
+            //partitioned by token range
+            if (!partitionSSTablesByTokenRange)
+                return 0;
+
+            return currentBoundaries.getDiskIndex(sstable);
+        }
+        finally
+        {
+            readLock.unlock();
+        }
     }
 
     public void shutdown()
     {
-        isActive = false;
-        repaired.shutdown();
-        unrepaired.shutdown();
+        writeLock.lock();
+        try
+        {
+            isActive = false;
+            repaired.forEach(AbstractCompactionStrategy::shutdown);
+            unrepaired.forEach(AbstractCompactionStrategy::shutdown);
+            compactionLogger.disable();
+        }
+        finally
+        {
+            writeLock.unlock();
+        }
     }
 
-    public synchronized void maybeReload(CFMetaData metadata)
+    public void maybeReload(CFMetaData metadata)
     {
         // compare the old schema configuration to the new one, ignore any locally set changes.
         if (metadata.params.compaction.equals(schemaCompactionParams))
             return;
-        reload(metadata);
+
+        writeLock.lock();
+        try
+        {
+            // compare the old schema configuration to the new one, ignore any locally set changes.
+            if (metadata.params.compaction.equals(schemaCompactionParams))
+                return;
+            reload(metadata.params.compaction);
+        }
+        finally
+        {
+            writeLock.unlock();
+        }
+    }
+
+    /**
+     * Checks if the disk boundaries changed and reloads the compaction strategies
+     * to reflect the most up-to-date disk boundaries.
+     *
+     * This is typically called before acquiring the {@link this#readLock} to ensure the most up-to-date
+     * disk locations and boundaries are used.
+     *
+     * This should *never* be called inside by a thread holding the {@link this#readLock}, since it
+     * will potentially acquire the {@link this#writeLock} to update the compaction strategies
+     * what can cause a deadlock.
+     */
+    //TODO improve this to reload after receiving a notification rather than trying to reload on every operation
+    @VisibleForTesting
+    protected void maybeReloadDiskBoundaries()
+    {
+        if (!currentBoundaries.isOutOfDate())
+            return;
+
+        writeLock.lock();
+        try
+        {
+            if (!currentBoundaries.isOutOfDate())
+                return;
+            reload(params);
+        }
+        finally
+        {
+            writeLock.unlock();
+        }
     }
 
     /**
      * Reload the compaction strategies
      *
      * Called after changing configuration and at startup.
-     * @param metadata
+     * @param newCompactionParams
      */
-    public synchronized void reload(CFMetaData metadata)
+    private void reload(CompactionParams newCompactionParams)
     {
+        boolean enabledWithJMX = enabled && !shouldBeEnabled();
         boolean disabledWithJMX = !enabled && shouldBeEnabled();
-        setStrategy(metadata.params.compaction);
-        schemaCompactionParams = metadata.params.compaction;
 
-        if (disabledWithJMX || !shouldBeEnabled())
+        if (currentBoundaries != null)
+        {
+            if (!newCompactionParams.equals(schemaCompactionParams))
+                logger.debug("Recreating compaction strategy - compaction parameters changed for {}.{}", cfs.keyspace.getName(), cfs.getTableName());
+            else if (currentBoundaries.isOutOfDate())
+                logger.debug("Recreating compaction strategy - disk boundaries are out of date for {}.{}.", cfs.keyspace.getName(), cfs.getTableName());
+        }
+
+        if (currentBoundaries == null || currentBoundaries.isOutOfDate())
+            currentBoundaries = boundariesSupplier.get();
+
+        setStrategy(newCompactionParams);
+        schemaCompactionParams = cfs.metadata.params.compaction;
+
+        if (disabledWithJMX || !shouldBeEnabled() && !enabledWithJMX)
             disable();
         else
             enable();
@@ -196,26 +402,57 @@
 
     public int getUnleveledSSTables()
     {
-        if (repaired instanceof LeveledCompactionStrategy && unrepaired instanceof LeveledCompactionStrategy)
+        maybeReloadDiskBoundaries();
+        readLock.lock();
+        try
         {
-            int count = 0;
-            count += ((LeveledCompactionStrategy)repaired).getLevelSize(0);
-            count += ((LeveledCompactionStrategy)unrepaired).getLevelSize(0);
-            return count;
+            if (repaired.get(0) instanceof LeveledCompactionStrategy && unrepaired.get(0) instanceof LeveledCompactionStrategy)
+            {
+                int count = 0;
+                for (AbstractCompactionStrategy strategy : repaired)
+                    count += ((LeveledCompactionStrategy) strategy).getLevelSize(0);
+                for (AbstractCompactionStrategy strategy : unrepaired)
+                    count += ((LeveledCompactionStrategy) strategy).getLevelSize(0);
+                return count;
+            }
+        }
+        finally
+        {
+            readLock.unlock();
         }
         return 0;
     }
 
-    public synchronized int[] getSSTableCountPerLevel()
+    public int getLevelFanoutSize()
     {
-        if (repaired instanceof LeveledCompactionStrategy && unrepaired instanceof LeveledCompactionStrategy)
+        return fanout;
+    }
+
+    public int[] getSSTableCountPerLevel()
+    {
+        maybeReloadDiskBoundaries();
+        readLock.lock();
+        try
         {
-            int [] res = new int[LeveledManifest.MAX_LEVEL_COUNT];
-            int[] repairedCountPerLevel = ((LeveledCompactionStrategy) repaired).getAllLevelSize();
-            res = sumArrays(res, repairedCountPerLevel);
-            int[] unrepairedCountPerLevel = ((LeveledCompactionStrategy) unrepaired).getAllLevelSize();
-            res = sumArrays(res, unrepairedCountPerLevel);
-            return res;
+            if (repaired.get(0) instanceof LeveledCompactionStrategy && unrepaired.get(0) instanceof LeveledCompactionStrategy)
+            {
+                int[] res = new int[LeveledGenerations.MAX_LEVEL_COUNT];
+                for (AbstractCompactionStrategy strategy : repaired)
+                {
+                    int[] repairedCountPerLevel = ((LeveledCompactionStrategy) strategy).getAllLevelSize();
+                    res = sumArrays(res, repairedCountPerLevel);
+                }
+                for (AbstractCompactionStrategy strategy : unrepaired)
+                {
+                    int[] unrepairedCountPerLevel = ((LeveledCompactionStrategy) strategy).getAllLevelSize();
+                    res = sumArrays(res, unrepairedCountPerLevel);
+                }
+                return res;
+            }
+        }
+        finally
+        {
+            readLock.unlock();
         }
         return null;
     }
@@ -237,113 +474,175 @@
 
     public Directories getDirectories()
     {
-        assert repaired.getClass().equals(unrepaired.getClass());
-        return repaired.getDirectories();
+        maybeReloadDiskBoundaries();
+        readLock.lock();
+        try
+        {
+            assert repaired.get(0).getClass().equals(unrepaired.get(0).getClass());
+            return repaired.get(0).getDirectories();
+        }
+        finally
+        {
+            readLock.unlock();
+        }
     }
 
-    public synchronized void handleNotification(INotification notification, Object sender)
+    /**
+     * Should only be called holding the readLock
+     */
+    private void handleFlushNotification(Iterable<SSTableReader> added)
     {
-        if (notification instanceof SSTableAddedNotification)
-        {
-            SSTableAddedNotification flushedNotification = (SSTableAddedNotification) notification;
-            for (SSTableReader sstable : flushedNotification.added)
-            {
-                if (sstable.isRepaired())
-                    repaired.addSSTable(sstable);
-                else
-                    unrepaired.addSSTable(sstable);
-            }
-        }
-        else if (notification instanceof SSTableListChangedNotification)
-        {
-            SSTableListChangedNotification listChangedNotification = (SSTableListChangedNotification) notification;
-            Set<SSTableReader> repairedRemoved = new HashSet<>();
-            Set<SSTableReader> repairedAdded = new HashSet<>();
-            Set<SSTableReader> unrepairedRemoved = new HashSet<>();
-            Set<SSTableReader> unrepairedAdded = new HashSet<>();
+        for (SSTableReader sstable : added)
+            compactionStrategyFor(sstable).addSSTable(sstable);
+    }
 
-            for (SSTableReader sstable : listChangedNotification.removed)
-            {
-                if (sstable.isRepaired())
-                    repairedRemoved.add(sstable);
-                else
-                    unrepairedRemoved.add(sstable);
-            }
-            for (SSTableReader sstable : listChangedNotification.added)
-            {
-                if (sstable.isRepaired())
-                    repairedAdded.add(sstable);
-                else
-                    unrepairedAdded.add(sstable);
-            }
-            if (!repairedRemoved.isEmpty())
-            {
-                repaired.replaceSSTables(repairedRemoved, repairedAdded);
-            }
-            else
-            {
-                for (SSTableReader sstable : repairedAdded)
-                    repaired.addSSTable(sstable);
-            }
+    /**
+     * Should only be called holding the readLock
+     */
+    private void handleListChangedNotification(Iterable<SSTableReader> added, Iterable<SSTableReader> removed)
+    {
+        // a bit of gymnastics to be able to replace sstables in compaction strategies
+        // we use this to know that a compaction finished and where to start the next compaction in LCS
+        int locationSize = partitionSSTablesByTokenRange? currentBoundaries.directories.size() : 1;
 
-            if (!unrepairedRemoved.isEmpty())
-            {
-                unrepaired.replaceSSTables(unrepairedRemoved, unrepairedAdded);
-            }
-            else
-            {
-                for (SSTableReader sstable : unrepairedAdded)
-                    unrepaired.addSSTable(sstable);
-            }
-        }
-        else if (notification instanceof SSTableRepairStatusChanged)
+        List<Set<SSTableReader>> repairedRemoved = new ArrayList<>(locationSize);
+        List<Set<SSTableReader>> repairedAdded = new ArrayList<>(locationSize);
+        List<Set<SSTableReader>> unrepairedRemoved = new ArrayList<>(locationSize);
+        List<Set<SSTableReader>> unrepairedAdded = new ArrayList<>(locationSize);
+
+        for (int i = 0; i < locationSize; i++)
         {
-            for (SSTableReader sstable : ((SSTableRepairStatusChanged) notification).sstable)
-            {
-                if (sstable.isRepaired())
-                {
-                    unrepaired.removeSSTable(sstable);
-                    repaired.addSSTable(sstable);
-                }
-                else
-                {
-                    repaired.removeSSTable(sstable);
-                    unrepaired.addSSTable(sstable);
-                }
-            }
+            repairedRemoved.add(new HashSet<>());
+            repairedAdded.add(new HashSet<>());
+            unrepairedRemoved.add(new HashSet<>());
+            unrepairedAdded.add(new HashSet<>());
         }
-        else if (notification instanceof SSTableDeletingNotification)
+
+        for (SSTableReader sstable : removed)
         {
-            SSTableReader sstable = ((SSTableDeletingNotification)notification).deleting;
+            int i = compactionStrategyIndexFor(sstable);
             if (sstable.isRepaired())
-                repaired.removeSSTable(sstable);
+                repairedRemoved.get(i).add(sstable);
             else
-                unrepaired.removeSSTable(sstable);
+                unrepairedRemoved.get(i).add(sstable);
+        }
+        for (SSTableReader sstable : added)
+        {
+            int i = compactionStrategyIndexFor(sstable);
+            if (sstable.isRepaired())
+                repairedAdded.get(i).add(sstable);
+            else
+                unrepairedAdded.get(i).add(sstable);
+        }
+        for (int i = 0; i < locationSize; i++)
+        {
+            if (!repairedRemoved.get(i).isEmpty())
+                repaired.get(i).replaceSSTables(repairedRemoved.get(i), repairedAdded.get(i));
+            else
+                repaired.get(i).addSSTables(repairedAdded.get(i));
+
+            if (!unrepairedRemoved.get(i).isEmpty())
+                unrepaired.get(i).replaceSSTables(unrepairedRemoved.get(i), unrepairedAdded.get(i));
+            else
+                unrepaired.get(i).addSSTables(unrepairedAdded.get(i));
+        }
+    }
+
+    private void handleRepairStatusChangedNotification(Iterable<SSTableReader> sstables)
+    {
+        for (SSTableReader sstable : sstables)
+        {
+            int index = compactionStrategyIndexFor(sstable);
+            if (sstable.isRepaired())
+            {
+                unrepaired.get(index).removeSSTable(sstable);
+                repaired.get(index).addSSTable(sstable);
+            }
+            else
+            {
+                repaired.get(index).removeSSTable(sstable);
+                unrepaired.get(index).addSSTable(sstable);
+            }
+        }
+
+    }
+
+    private void handleDeletingNotification(SSTableReader deleted)
+    {
+        compactionStrategyFor(deleted).removeSSTable(deleted);
+    }
+
+    public void handleNotification(INotification notification, Object sender)
+    {
+        // we might race with reload adding/removing the sstables, this means that compaction strategies
+        // must handle double notifications.
+        maybeReloadDiskBoundaries();
+        readLock.lock();
+        try
+        {
+            if (notification instanceof SSTableAddedNotification)
+            {
+                SSTableAddedNotification flushedNotification = (SSTableAddedNotification) notification;
+                handleFlushNotification(flushedNotification.added);
+            }
+            else if (notification instanceof SSTableListChangedNotification)
+            {
+                SSTableListChangedNotification listChangedNotification = (SSTableListChangedNotification) notification;
+                handleListChangedNotification(listChangedNotification.added, listChangedNotification.removed);
+            }
+            else if (notification instanceof SSTableRepairStatusChanged)
+            {
+                handleRepairStatusChangedNotification(((SSTableRepairStatusChanged) notification).sstables);
+            }
+            else if (notification instanceof SSTableDeletingNotification)
+            {
+                handleDeletingNotification(((SSTableDeletingNotification) notification).deleting);
+            }
+        }
+        finally
+        {
+            readLock.unlock();
         }
     }
 
     public void enable()
     {
-        if (repaired != null)
-            repaired.enable();
-        if (unrepaired != null)
-            unrepaired.enable();
-        // enable this last to make sure the strategies are ready to get calls.
-        enabled = true;
+        writeLock.lock();
+        try
+        {
+            if (repaired != null)
+                repaired.forEach(AbstractCompactionStrategy::enable);
+            if (unrepaired != null)
+                unrepaired.forEach(AbstractCompactionStrategy::enable);
+            // enable this last to make sure the strategies are ready to get calls.
+            enabled = true;
+        }
+        finally
+        {
+            writeLock.unlock();
+        }
     }
 
     public void disable()
     {
-        // disable this first avoid asking disabled strategies for compaction tasks
-        enabled = false;
-        if (repaired != null)
-            repaired.disable();
-        if (unrepaired != null)
-            unrepaired.disable();
+        writeLock.lock();
+        try
+        {
+            // disable this first avoid asking disabled strategies for compaction tasks
+            enabled = false;
+            if (repaired != null)
+                repaired.forEach(AbstractCompactionStrategy::disable);
+            if (unrepaired != null)
+                unrepaired.forEach(AbstractCompactionStrategy::disable);
+        }
+        finally
+        {
+            writeLock.unlock();
+        }
     }
 
     /**
-     * Create ISSTableScanner from the given sstables
+     * Create ISSTableScanners from the given sstables
      *
      * Delegates the call to the compaction strategies to allow LCS to create a scanner
      * @param sstables
@@ -351,89 +650,233 @@
      * @return
      */
     @SuppressWarnings("resource")
-    public synchronized AbstractCompactionStrategy.ScannerList getScanners(Collection<SSTableReader> sstables,  Collection<Range<Token>> ranges)
+    public AbstractCompactionStrategy.ScannerList getScanners(Collection<SSTableReader> sstables,  Collection<Range<Token>> ranges)
     {
-        List<SSTableReader> repairedSSTables = new ArrayList<>();
-        List<SSTableReader> unrepairedSSTables = new ArrayList<>();
-        for (SSTableReader sstable : sstables)
+        maybeReloadDiskBoundaries();
+        readLock.lock();
+        try
         {
-            if (sstable.isRepaired())
-                repairedSSTables.add(sstable);
-            else
-                unrepairedSSTables.add(sstable);
-        }
+            assert repaired.size() == unrepaired.size();
+            List<Set<SSTableReader>> repairedSSTables = new ArrayList<>();
+            List<Set<SSTableReader>> unrepairedSSTables = new ArrayList<>();
 
-        Set<ISSTableScanner> scanners = new HashSet<>(sstables.size());
-        AbstractCompactionStrategy.ScannerList repairedScanners = repaired.getScanners(repairedSSTables, ranges);
-        AbstractCompactionStrategy.ScannerList unrepairedScanners = unrepaired.getScanners(unrepairedSSTables, ranges);
-        scanners.addAll(repairedScanners.scanners);
-        scanners.addAll(unrepairedScanners.scanners);
-        return new AbstractCompactionStrategy.ScannerList(new ArrayList<>(scanners));
+            for (int i = 0; i < repaired.size(); i++)
+            {
+                repairedSSTables.add(new HashSet<>());
+                unrepairedSSTables.add(new HashSet<>());
+            }
+
+            for (SSTableReader sstable : sstables)
+            {
+                if (sstable.isRepaired())
+                    repairedSSTables.get(compactionStrategyIndexFor(sstable)).add(sstable);
+                else
+                    unrepairedSSTables.get(compactionStrategyIndexFor(sstable)).add(sstable);
+            }
+
+            List<ISSTableScanner> scanners = new ArrayList<>(sstables.size());
+            for (int i = 0; i < repairedSSTables.size(); i++)
+            {
+                if (!repairedSSTables.get(i).isEmpty())
+                    scanners.addAll(repaired.get(i).getScanners(repairedSSTables.get(i), ranges).scanners);
+            }
+            for (int i = 0; i < unrepairedSSTables.size(); i++)
+            {
+                if (!unrepairedSSTables.get(i).isEmpty())
+                    scanners.addAll(unrepaired.get(i).getScanners(unrepairedSSTables.get(i), ranges).scanners);
+            }
+
+            return new AbstractCompactionStrategy.ScannerList(scanners);
+        }
+        finally
+        {
+            readLock.unlock();
+        }
     }
 
-    public synchronized AbstractCompactionStrategy.ScannerList getScanners(Collection<SSTableReader> sstables)
+    public AbstractCompactionStrategy.ScannerList getScanners(Collection<SSTableReader> sstables)
     {
         return getScanners(sstables, null);
     }
 
     public Collection<Collection<SSTableReader>> groupSSTablesForAntiCompaction(Collection<SSTableReader> sstablesToGroup)
     {
-        return unrepaired.groupSSTablesForAntiCompaction(sstablesToGroup);
+        maybeReloadDiskBoundaries();
+        readLock.lock();
+        try
+        {
+            Map<Integer, List<SSTableReader>> groups = sstablesToGroup.stream().collect(Collectors.groupingBy((s) -> compactionStrategyIndexFor(s)));
+            Collection<Collection<SSTableReader>> anticompactionGroups = new ArrayList<>();
+
+            for (Map.Entry<Integer, List<SSTableReader>> group : groups.entrySet())
+                anticompactionGroups.addAll(unrepaired.get(group.getKey()).groupSSTablesForAntiCompaction(group.getValue()));
+            return anticompactionGroups;
+        }
+        finally
+        {
+            readLock.unlock();
+        }
     }
 
     public long getMaxSSTableBytes()
     {
-        return unrepaired.getMaxSSTableBytes();
+        return maxSSTableSizeBytes;
     }
 
     public AbstractCompactionTask getCompactionTask(LifecycleTransaction txn, int gcBefore, long maxSSTableBytes)
     {
-        return getCompactionStrategyFor(txn.originals().iterator().next()).getCompactionTask(txn, gcBefore, maxSSTableBytes);
+        maybeReloadDiskBoundaries();
+        readLock.lock();
+        try
+        {
+            validateForCompaction(txn.originals());
+            return compactionStrategyFor(txn.originals().iterator().next()).getCompactionTask(txn, gcBefore, maxSSTableBytes);
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+
+    }
+
+    private void validateForCompaction(Iterable<SSTableReader> input)
+    {
+        readLock.lock();
+        try
+        {
+            SSTableReader firstSSTable = Iterables.getFirst(input, null);
+            assert firstSSTable != null;
+            boolean repaired = firstSSTable.isRepaired();
+            int firstIndex = compactionStrategyIndexFor(firstSSTable);
+            for (SSTableReader sstable : input)
+            {
+                if (sstable.isRepaired() != repaired)
+                    throw new UnsupportedOperationException("You can't mix repaired and unrepaired data in a compaction");
+                if (firstIndex != compactionStrategyIndexFor(sstable))
+                    throw new UnsupportedOperationException("You can't mix sstables from different directories in a compaction");
+            }
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+
     }
 
     public Collection<AbstractCompactionTask> getMaximalTasks(final int gcBefore, final boolean splitOutput)
     {
+        maybeReloadDiskBoundaries();
         // runWithCompactionsDisabled cancels active compactions and disables them, then we are able
         // to make the repaired/unrepaired strategies mark their own sstables as compacting. Once the
         // sstables are marked the compactions are re-enabled
         return cfs.runWithCompactionsDisabled(new Callable<Collection<AbstractCompactionTask>>()
         {
             @Override
-            public Collection<AbstractCompactionTask> call() throws Exception
+            public Collection<AbstractCompactionTask> call()
             {
-                synchronized (CompactionStrategyManager.this)
+                List<AbstractCompactionTask> tasks = new ArrayList<>();
+                readLock.lock();
+                try
                 {
-                    Collection<AbstractCompactionTask> repairedTasks = repaired.getMaximalTask(gcBefore, splitOutput);
-                    Collection<AbstractCompactionTask> unrepairedTasks = unrepaired.getMaximalTask(gcBefore, splitOutput);
-
-                    if (repairedTasks == null && unrepairedTasks == null)
-                        return null;
-
-                    if (repairedTasks == null)
-                        return unrepairedTasks;
-                    if (unrepairedTasks == null)
-                        return repairedTasks;
-
-                    List<AbstractCompactionTask> tasks = new ArrayList<>();
-                    tasks.addAll(repairedTasks);
-                    tasks.addAll(unrepairedTasks);
-                    return tasks;
+                    for (AbstractCompactionStrategy strategy : repaired)
+                    {
+                        Collection<AbstractCompactionTask> task = strategy.getMaximalTask(gcBefore, splitOutput);
+                        if (task != null)
+                            tasks.addAll(task);
+                    }
+                    for (AbstractCompactionStrategy strategy : unrepaired)
+                    {
+                        Collection<AbstractCompactionTask> task = strategy.getMaximalTask(gcBefore, splitOutput);
+                        if (task != null)
+                            tasks.addAll(task);
+                    }
                 }
+                finally
+                {
+                    readLock.unlock();
+                }
+                if (tasks.isEmpty())
+                    return null;
+                return tasks;
             }
         }, false, false);
     }
 
+    /**
+     * Return a list of compaction tasks corresponding to the sstables requested. Split the sstables according
+     * to whether they are repaired or not, and by disk location. Return a task per disk location and repair status
+     * group.
+     *
+     * @param sstables the sstables to compact
+     * @param gcBefore gc grace period, throw away tombstones older than this
+     * @return a list of compaction tasks corresponding to the sstables requested
+     */
+    public List<AbstractCompactionTask> getUserDefinedTasks(Collection<SSTableReader> sstables, int gcBefore)
+    {
+        return getUserDefinedTasks(sstables, gcBefore, false);
+    }
+
+    public List<AbstractCompactionTask> getUserDefinedTasks(Collection<SSTableReader> sstables, int gcBefore, boolean validateForCompaction)
+    {
+        maybeReloadDiskBoundaries();
+        List<AbstractCompactionTask> ret = new ArrayList<>();
+        readLock.lock();
+        try
+        {
+            if (validateForCompaction)
+                validateForCompaction(sstables);
+
+            Map<Integer, List<SSTableReader>> repairedSSTables = sstables.stream()
+                                                                         .filter(s -> !s.isMarkedSuspect() && s.isRepaired())
+                                                                         .collect(Collectors.groupingBy((s) -> compactionStrategyIndexFor(s)));
+
+            Map<Integer, List<SSTableReader>> unrepairedSSTables = sstables.stream()
+                                                                           .filter(s -> !s.isMarkedSuspect() && !s.isRepaired())
+                                                                           .collect(Collectors.groupingBy((s) -> compactionStrategyIndexFor(s)));
+
+
+            for (Map.Entry<Integer, List<SSTableReader>> group : repairedSSTables.entrySet())
+                ret.add(repaired.get(group.getKey()).getUserDefinedTask(group.getValue(), gcBefore));
+
+            for (Map.Entry<Integer, List<SSTableReader>> group : unrepairedSSTables.entrySet())
+                ret.add(unrepaired.get(group.getKey()).getUserDefinedTask(group.getValue(), gcBefore));
+
+            return ret;
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+    }
+
+    /**
+     * @deprecated use {@link #getUserDefinedTasks(Collection, int)} instead.
+     */
+    @Deprecated()
     public AbstractCompactionTask getUserDefinedTask(Collection<SSTableReader> sstables, int gcBefore)
     {
-        return getCompactionStrategyFor(sstables.iterator().next()).getUserDefinedTask(sstables, gcBefore);
+        List<AbstractCompactionTask> tasks = getUserDefinedTasks(sstables, gcBefore, true);
+        assert tasks.size() == 1;
+        return tasks.get(0);
     }
 
     public int getEstimatedRemainingTasks()
     {
+        maybeReloadDiskBoundaries();
         int tasks = 0;
-        tasks += repaired.getEstimatedRemainingTasks();
-        tasks += unrepaired.getEstimatedRemainingTasks();
+        readLock.lock();
+        try
+        {
 
+            for (AbstractCompactionStrategy strategy : repaired)
+                tasks += strategy.getEstimatedRemainingTasks();
+            for (AbstractCompactionStrategy strategy : unrepaired)
+                tasks += strategy.getEstimatedRemainingTasks();
+        }
+        finally
+        {
+            readLock.unlock();
+        }
         return tasks;
     }
 
@@ -444,33 +887,62 @@
 
     public String getName()
     {
-        return unrepaired.getName();
+        return name;
     }
 
-    public List<AbstractCompactionStrategy> getStrategies()
+    public List<List<AbstractCompactionStrategy>> getStrategies()
     {
-        return Arrays.asList(repaired, unrepaired);
+        maybeReloadDiskBoundaries();
+        readLock.lock();
+        try
+        {
+            return Arrays.asList(repaired, unrepaired);
+        }
+        finally
+        {
+            readLock.unlock();
+        }
     }
 
-    public synchronized void setNewLocalCompactionStrategy(CompactionParams params)
+    public void setNewLocalCompactionStrategy(CompactionParams params)
     {
         logger.info("Switching local compaction strategy from {} to {}}", this.params, params);
-        setStrategy(params);
-        if (shouldBeEnabled())
-            enable();
-        else
-            disable();
-        startup();
+        writeLock.lock();
+        try
+        {
+            setStrategy(params);
+            if (shouldBeEnabled())
+                enable();
+            else
+                disable();
+            startup();
+        }
+        finally
+        {
+            writeLock.unlock();
+        }
     }
 
     private void setStrategy(CompactionParams params)
     {
-        if (repaired != null)
-            repaired.shutdown();
-        if (unrepaired != null)
-            unrepaired.shutdown();
-        repaired = CFMetaData.createCompactionStrategyInstance(cfs, params);
-        unrepaired = CFMetaData.createCompactionStrategyInstance(cfs, params);
+        repaired.forEach(AbstractCompactionStrategy::shutdown);
+        unrepaired.forEach(AbstractCompactionStrategy::shutdown);
+        repaired.clear();
+        unrepaired.clear();
+
+        if (partitionSSTablesByTokenRange)
+        {
+            for (int i = 0; i < currentBoundaries.directories.size(); i++)
+            {
+                repaired.add(CFMetaData.createCompactionStrategyInstance(cfs, params));
+                unrepaired.add(CFMetaData.createCompactionStrategyInstance(cfs, params));
+            }
+        }
+        else
+        {
+            repaired.add(CFMetaData.createCompactionStrategyInstance(cfs, params));
+            unrepaired.add(CFMetaData.createCompactionStrategyInstance(cfs, params));
+        }
         this.params = params;
     }
 
@@ -484,20 +956,81 @@
         return Boolean.parseBoolean(params.options().get(AbstractCompactionStrategy.ONLY_PURGE_REPAIRED_TOMBSTONES));
     }
 
-    public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor, long keyCount, long repairedAt, MetadataCollector collector, SerializationHeader header, LifecycleNewTracker lifecycleNewTracker)
+    public SSTableMultiWriter createSSTableMultiWriter(Descriptor descriptor,
+                                                       long keyCount,
+                                                       long repairedAt,
+                                                       MetadataCollector collector,
+                                                       SerializationHeader header,
+                                                       Collection<Index> indexes,
+                                                       LifecycleNewTracker lifecycleNewTracker)
     {
-        if (repairedAt == ActiveRepairService.UNREPAIRED_SSTABLE)
+        maybeReloadDiskBoundaries();
+        readLock.lock();
+        try
         {
-            return unrepaired.createSSTableMultiWriter(descriptor, keyCount, repairedAt, collector, header, lifecycleNewTracker);
+            if (repairedAt == ActiveRepairService.UNREPAIRED_SSTABLE)
+            {
+                return unrepaired.get(0).createSSTableMultiWriter(descriptor, keyCount, repairedAt, collector, header, indexes, lifecycleNewTracker);
+            }
+            else
+            {
+                return repaired.get(0).createSSTableMultiWriter(descriptor, keyCount, repairedAt, collector, header, indexes, lifecycleNewTracker);
+            }
         }
-        else
+        finally
         {
-            return repaired.createSSTableMultiWriter(descriptor, keyCount, repairedAt, collector, header, lifecycleNewTracker);
+            readLock.unlock();
         }
     }
 
+    public boolean isRepaired(AbstractCompactionStrategy strategy)
+    {
+        readLock.lock();
+        try
+        {
+            return repaired.contains(strategy);
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+    }
+
+    public List<String> getStrategyFolders(AbstractCompactionStrategy strategy)
+    {
+        readLock.lock();
+        try
+        {
+            List<Directories.DataDirectory> locations = currentBoundaries.directories;
+            if (partitionSSTablesByTokenRange)
+            {
+                int unrepairedIndex = unrepaired.indexOf(strategy);
+                if (unrepairedIndex > 0)
+                {
+                    return Collections.singletonList(locations.get(unrepairedIndex).location.getAbsolutePath());
+                }
+                int repairedIndex = repaired.indexOf(strategy);
+                if (repairedIndex > 0)
+                {
+                    return Collections.singletonList(locations.get(repairedIndex).location.getAbsolutePath());
+                }
+            }
+            List<String> folders = new ArrayList<>(locations.size());
+            for (Directories.DataDirectory location : locations)
+            {
+                folders.add(location.location.getAbsolutePath());
+            }
+            return folders;
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+
+    }
+
     public boolean supportsEarlyOpen()
     {
-        return repaired.supportsEarlyOpen();
+        return supportsEarlyOpen;
     }
 }
diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionTask.java b/src/java/org/apache/cassandra/db/compaction/CompactionTask.java
index d023cef..34c249c 100644
--- a/src/java/org/apache/cassandra/db/compaction/CompactionTask.java
+++ b/src/java/org/apache/cassandra/db/compaction/CompactionTask.java
@@ -27,6 +27,8 @@
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
+import com.google.common.primitives.Ints;
+import com.google.common.util.concurrent.RateLimiter;
 
 import org.apache.cassandra.db.Directories;
 import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter;
@@ -41,40 +43,34 @@
 import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.db.compaction.CompactionManager.CompactionExecutorStatsCollector;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
+import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
 import org.apache.cassandra.service.ActiveRepairService;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.concurrent.Refs;
 
 public class CompactionTask extends AbstractCompactionTask
 {
-    private static class Summary
-    {
-        final String partitionMerge;
-        final long totalSourceRows;
-
-        public Summary(String partitionMerge, long totalSourceRows)
-        {
-            this.partitionMerge = partitionMerge;
-            this.totalSourceRows = totalSourceRows;
-        }
-    }
     protected static final Logger logger = LoggerFactory.getLogger(CompactionTask.class);
     protected final int gcBefore;
-    protected final boolean offline;
     protected final boolean keepOriginals;
     protected static long totalBytesCompacted = 0;
     private CompactionExecutorStatsCollector collector;
 
     public CompactionTask(ColumnFamilyStore cfs, LifecycleTransaction txn, int gcBefore)
     {
-        this(cfs, txn, gcBefore, false, false);
+        this(cfs, txn, gcBefore, false);
     }
 
+    @Deprecated
     public CompactionTask(ColumnFamilyStore cfs, LifecycleTransaction txn, int gcBefore, boolean offline, boolean keepOriginals)
     {
+        this(cfs, txn, gcBefore, keepOriginals);
+    }
+
+    public CompactionTask(ColumnFamilyStore cfs, LifecycleTransaction txn, int gcBefore, boolean keepOriginals)
+    {
         super(cfs, txn);
         this.gcBefore = gcBefore;
-        this.offline = offline;
         this.keepOriginals = keepOriginals;
     }
 
@@ -158,16 +154,19 @@
 
         logger.debug("Compacting ({}) {}", taskId, ssTableLoggerMsg);
 
+        RateLimiter limiter = CompactionManager.instance.getRateLimiter();
         long start = System.nanoTime();
+        long startTime = System.currentTimeMillis();
         long totalKeysWritten = 0;
         long estimatedKeys = 0;
+        long inputSizeBytes;
         try (CompactionController controller = getCompactionController(transaction.originals()))
         {
             Set<SSTableReader> actuallyCompact = Sets.difference(transaction.originals(), controller.getFullyExpiredSSTables());
-
             Collection<SSTableReader> newSStables;
 
             long[] mergedRowCounts;
+            long totalSourceCQLRows;
 
             // SSTableScanners need to be closed before markCompactedSSTablesReplaced call as scanners contain references
             // to both ifile and dfile and SSTR will throw deletion errors on Windows if it tries to delete before scanner is closed.
@@ -178,6 +177,12 @@
                  CompactionIterator ci = new CompactionIterator(compactionType, scanners.scanners, controller, nowInSec, taskId))
             {
                 long lastCheckObsoletion = start;
+                inputSizeBytes = scanners.getTotalCompressedSize();
+                double compressionRatio = scanners.getCompressionRatio();
+                if (compressionRatio == MetadataCollector.NO_COMPRESSION_RATIO)
+                    compressionRatio = 1.0;
+
+                long lastBytesScanned = 0;
 
                 if (collector != null)
                     collector.beginCompaction(ci);
@@ -188,7 +193,7 @@
                     // where the compaction does not exist in activeCompactions but the CSM gets paused.
                     // We already have the sstables marked compacting here so CompactionManager#waitForCessation will
                     // block until the below exception is thrown and the transaction is cancelled.
-                    if (!controller.cfs.getCompactionStrategyManager().isActive)
+                    if (!controller.cfs.getCompactionStrategyManager().isActive())
                         throw new CompactionInterruptedException(ci.getCompactionInfo());
                     estimatedKeys = writer.estimatedKeys();
                     while (ci.hasNext())
@@ -199,6 +204,14 @@
                         if (writer.append(ci.next()))
                             totalKeysWritten++;
 
+
+                        long bytesScanned = scanners.getTotalBytesScanned();
+
+                        //Rate limit the scanners, and account for compression
+                        CompactionManager.compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio);
+
+                        lastBytesScanned = bytesScanned;
+
                         if (System.nanoTime() - lastCheckObsoletion > TimeUnit.MINUTES.toNanos(1L))
                         {
                             controller.maybeRefreshOverlaps();
@@ -215,28 +228,50 @@
                         collector.finishCompaction(ci);
 
                     mergedRowCounts = ci.getMergedRowCounts();
+
+                    totalSourceCQLRows = ci.getTotalSourceCQLRows();
                 }
             }
 
+            if (transaction.isOffline())
+                return;
+
             // log a bunch of statistics about the result and save to system table compaction_history
-            long dTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
-            long startsize = SSTableReader.getTotalBytes(transaction.originals());
+            long durationInNano = System.nanoTime() - start;
+            long dTime = TimeUnit.NANOSECONDS.toMillis(durationInNano);
+            long startsize = inputSizeBytes;
             long endsize = SSTableReader.getTotalBytes(newSStables);
             double ratio = (double) endsize / (double) startsize;
 
             StringBuilder newSSTableNames = new StringBuilder();
             for (SSTableReader reader : newSStables)
                 newSSTableNames.append(reader.descriptor.baseFilename()).append(",");
+            long totalSourceRows = 0;
+            for (int i = 0; i < mergedRowCounts.length; i++)
+                totalSourceRows += mergedRowCounts[i] * (i + 1);
 
-            if (offline)
-                return;
-
-            double mbps = dTime > 0 ? (double) endsize / (1024 * 1024) / ((double) dTime / 1000) : 0;
-            Summary mergeSummary = updateCompactionHistory(cfs.keyspace.getName(), cfs.getColumnFamilyName(), mergedRowCounts, startsize, endsize);
-            logger.debug(String.format("Compacted (%s) %d sstables to [%s] to level=%d.  %,d bytes to %,d (~%d%% of original) in %,dms = %fMB/s.  %,d total partitions merged to %,d.  Partition merge counts were {%s}",
-                                       taskId, transaction.originals().size(), newSSTableNames.toString(), getLevel(), startsize, endsize, (int) (ratio * 100), dTime, mbps, mergeSummary.totalSourceRows, totalKeysWritten, mergeSummary.partitionMerge));
-            logger.trace(String.format("CF Total Bytes Compacted: %,d", CompactionTask.addToTotalBytesCompacted(endsize)));
+            String mergeSummary = updateCompactionHistory(cfs.keyspace.getName(), cfs.getTableName(), mergedRowCounts, startsize, endsize);
+            logger.debug(String.format("Compacted (%s) %d sstables to [%s] to level=%d.  %s to %s (~%d%% of original) in %,dms.  Read Throughput = %s, Write Throughput = %s, Row Throughput = ~%,d/s.  %,d total partitions merged to %,d.  Partition merge counts were {%s}",
+                                       taskId,
+                                       transaction.originals().size(),
+                                       newSSTableNames.toString(),
+                                       getLevel(),
+                                       FBUtilities.prettyPrintMemory(startsize),
+                                       FBUtilities.prettyPrintMemory(endsize),
+                                       (int) (ratio * 100),
+                                       dTime,
+                                       FBUtilities.prettyPrintMemoryPerSecond(startsize, durationInNano),
+                                       FBUtilities.prettyPrintMemoryPerSecond(endsize, durationInNano),
+                                       (int) totalSourceCQLRows / (TimeUnit.NANOSECONDS.toSeconds(durationInNano) + 1),
+                                       totalSourceRows,
+                                       totalKeysWritten,
+                                       mergeSummary));
+            logger.trace("CF Total Bytes Compacted: {}", FBUtilities.prettyPrintMemory(CompactionTask.addToTotalBytesCompacted(endsize)));
             logger.trace("Actual #keys: {}, Estimated #keys:{}, Err%: {}", totalKeysWritten, estimatedKeys, ((double)(totalKeysWritten - estimatedKeys)/totalKeysWritten));
+            cfs.getCompactionStrategyManager().compactionLogger.compaction(startTime, transaction.originals(), System.currentTimeMillis(), newSStables);
+
+            // update the metrics
+            cfs.metric.compactionBytesWritten.inc(endsize);
         }
     }
 
@@ -246,14 +281,13 @@
                                                           LifecycleTransaction transaction,
                                                           Set<SSTableReader> nonExpiredSSTables)
     {
-        return new DefaultCompactionWriter(cfs, directories, transaction, nonExpiredSSTables, offline, keepOriginals);
+        return new DefaultCompactionWriter(cfs, directories, transaction, nonExpiredSSTables, keepOriginals, getLevel());
     }
 
-    public static Summary updateCompactionHistory(String keyspaceName, String columnFamilyName, long[] mergedRowCounts, long startSize, long endSize)
+    public static String updateCompactionHistory(String keyspaceName, String columnFamilyName, long[] mergedRowCounts, long startSize, long endSize)
     {
         StringBuilder mergeSummary = new StringBuilder(mergedRowCounts.length * 10);
         Map<Integer, Long> mergedRows = new HashMap<>();
-        long totalSourceRows = 0;
         for (int i = 0; i < mergedRowCounts.length; i++)
         {
             long count = mergedRowCounts[i];
@@ -261,12 +295,11 @@
                 continue;
 
             int rows = i + 1;
-            totalSourceRows += rows * count;
             mergeSummary.append(String.format("%d:%d, ", rows, count));
             mergedRows.put(rows, count);
         }
         SystemKeyspace.updateCompactionHistory(keyspaceName, columnFamilyName, System.currentTimeMillis(), startSize, endSize, mergedRows);
-        return new Summary(mergeSummary.toString(), totalSourceRows);
+        return mergeSummary.toString();
     }
 
     protected Directories getDirectories()
diff --git a/src/java/org/apache/cassandra/db/compaction/DateTieredCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/DateTieredCompactionStrategy.java
index 7c38fa8..ed3b172 100644
--- a/src/java/org/apache/cassandra/db/compaction/DateTieredCompactionStrategy.java
+++ b/src/java/org/apache/cassandra/db/compaction/DateTieredCompactionStrategy.java
@@ -18,6 +18,7 @@
 package org.apache.cassandra.db.compaction;
 
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Predicate;
@@ -32,6 +33,9 @@
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.schema.CompactionParams;
 import org.apache.cassandra.utils.Pair;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 
 import static com.google.common.collect.Iterables.filter;
 
@@ -67,8 +71,9 @@
 
     @Override
     @SuppressWarnings("resource")
-    public synchronized AbstractCompactionTask getNextBackgroundTask(int gcBefore)
+    public AbstractCompactionTask getNextBackgroundTask(int gcBefore)
     {
+        List<SSTableReader> previousCandidate = null;
         while (true)
         {
             List<SSTableReader> latestBucket = getNextBackgroundSSTables(gcBefore);
@@ -76,9 +81,20 @@
             if (latestBucket.isEmpty())
                 return null;
 
+            // Already tried acquiring references without success. It means there is a race with
+            // the tracker but candidate SSTables were not yet replaced in the compaction strategy manager
+            if (latestBucket.equals(previousCandidate))
+            {
+                logger.warn("Could not acquire references for compacting SSTables {} which is not a problem per se," +
+                            "unless it happens frequently, in which case it must be reported. Will retry later.",
+                            latestBucket);
+                return null;
+            }
+
             LifecycleTransaction modifier = cfs.getTracker().tryModify(latestBucket, OperationType.COMPACTION);
             if (modifier != null)
                 return new CompactionTask(cfs, modifier, gcBefore);
+            previousCandidate = latestBucket;
         }
     }
 
@@ -89,10 +105,14 @@
      */
     private synchronized List<SSTableReader> getNextBackgroundSSTables(final int gcBefore)
     {
-        if (Iterables.isEmpty(cfs.getSSTables(SSTableSet.LIVE)))
-            return Collections.emptyList();
+        Set<SSTableReader> uncompacting;
+        synchronized (sstables)
+        {
+            if (sstables.isEmpty())
+                return Collections.emptyList();
 
-        Set<SSTableReader> uncompacting = ImmutableSet.copyOf(filter(cfs.getUncompactingSSTables(), sstables::contains));
+            uncompacting = ImmutableSet.copyOf(filter(cfs.getUncompactingSSTables(), sstables::contains));
+        }
 
         Set<SSTableReader> expired = Collections.emptySet();
         // we only check for expired sstables every 10 minutes (by default) due to it being an expensive operation
@@ -134,7 +154,7 @@
         if (sstablesWithTombstones.isEmpty())
             return Collections.emptyList();
 
-        return Collections.singletonList(Collections.min(sstablesWithTombstones, new SSTableReader.SizeComparator()));
+        return Collections.singletonList(Collections.min(sstablesWithTombstones, SSTableReader.sizeComparator));
     }
 
     private List<SSTableReader> getCompactionCandidates(Iterable<SSTableReader> candidateSSTables, long now, int base)
@@ -166,6 +186,8 @@
         // no need to convert to collection if had an Iterables.max(), but not present in standard toolkit, and not worth adding
         List<SSTableReader> list = new ArrayList<>();
         Iterables.addAll(list, cfs.getSSTables(SSTableSet.LIVE));
+        if (list.isEmpty())
+            return 0;
         return Collections.max(list, (o1, o2) -> Long.compare(o1.getMaxTimestamp(), o2.getMaxTimestamp()))
                           .getMaxTimestamp();
     }
@@ -212,6 +234,7 @@
     {
         sstables.remove(sstable);
     }
+
     /**
      * A target time span used for bucketing SSTables based on timestamps.
      */
@@ -341,6 +364,7 @@
                     n += Math.ceil((double)stcsBucket.size() / cfs.getMaximumCompactionThreshold());
         }
         estimatedRemainingTasks = n;
+        cfs.getCompactionStrategyManager().compactionLogger.pending(this, n);
     }
 
 
@@ -452,6 +476,33 @@
         return uncheckedOptions;
     }
 
+    public CompactionLogger.Strategy strategyLogger()
+    {
+        return new CompactionLogger.Strategy()
+        {
+            public JsonNode sstable(SSTableReader sstable)
+            {
+                ObjectNode node = JsonNodeFactory.instance.objectNode();
+                node.put("min_timestamp", sstable.getMinTimestamp());
+                node.put("max_timestamp", sstable.getMaxTimestamp());
+                return node;
+            }
+
+            public JsonNode options()
+            {
+                ObjectNode node = JsonNodeFactory.instance.objectNode();
+                TimeUnit resolution = DateTieredCompactionStrategy.this.options.timestampResolution;
+                node.put(DateTieredCompactionStrategyOptions.TIMESTAMP_RESOLUTION_KEY,
+                         resolution.toString());
+                node.put(DateTieredCompactionStrategyOptions.BASE_TIME_KEY,
+                         resolution.toSeconds(DateTieredCompactionStrategy.this.options.baseTime));
+                node.put(DateTieredCompactionStrategyOptions.MAX_WINDOW_SIZE_KEY,
+                         resolution.toSeconds(DateTieredCompactionStrategy.this.options.maxWindowSize));
+                return node;
+            }
+        };
+    }
+
     public String toString()
     {
         return String.format("DateTieredCompactionStrategy[%s/%s]",
diff --git a/src/java/org/apache/cassandra/db/compaction/DateTieredCompactionStrategyOptions.java b/src/java/org/apache/cassandra/db/compaction/DateTieredCompactionStrategyOptions.java
index 78a0cab..9362bde 100644
--- a/src/java/org/apache/cassandra/db/compaction/DateTieredCompactionStrategyOptions.java
+++ b/src/java/org/apache/cassandra/db/compaction/DateTieredCompactionStrategyOptions.java
@@ -27,7 +27,7 @@
 
 public final class DateTieredCompactionStrategyOptions
 {
-    private static final Logger logger = LoggerFactory.getLogger(DateTieredCompactionStrategy.class);
+    private static final Logger logger = LoggerFactory.getLogger(DateTieredCompactionStrategyOptions.class);
     protected static final TimeUnit DEFAULT_TIMESTAMP_RESOLUTION = TimeUnit.MICROSECONDS;
     @Deprecated
     protected static final double DEFAULT_MAX_SSTABLE_AGE_DAYS = 365*1000;
@@ -44,6 +44,7 @@
 
     @Deprecated
     protected final long maxSSTableAge;
+    protected final TimeUnit timestampResolution;
     protected final long baseTime;
     protected final long expiredSSTableCheckFrequency;
     protected final long maxWindowSize;
@@ -51,7 +52,7 @@
     public DateTieredCompactionStrategyOptions(Map<String, String> options)
     {
         String optionValue = options.get(TIMESTAMP_RESOLUTION_KEY);
-        TimeUnit timestampResolution = optionValue == null ? DEFAULT_TIMESTAMP_RESOLUTION : TimeUnit.valueOf(optionValue);
+        timestampResolution = optionValue == null ? DEFAULT_TIMESTAMP_RESOLUTION : TimeUnit.valueOf(optionValue);
         if (timestampResolution != DEFAULT_TIMESTAMP_RESOLUTION)
             logger.warn("Using a non-default timestamp_resolution {} - are you really doing inserts with USING TIMESTAMP <non_microsecond_timestamp> (or driver equivalent)?", timestampResolution.toString());
         optionValue = options.get(MAX_SSTABLE_AGE_KEY);
@@ -68,9 +69,10 @@
     public DateTieredCompactionStrategyOptions()
     {
         maxSSTableAge = Math.round(DEFAULT_MAX_SSTABLE_AGE_DAYS * DEFAULT_TIMESTAMP_RESOLUTION.convert((long) DEFAULT_MAX_SSTABLE_AGE_DAYS, TimeUnit.DAYS));
-        baseTime = DEFAULT_TIMESTAMP_RESOLUTION.convert(DEFAULT_BASE_TIME_SECONDS, TimeUnit.SECONDS);
+        timestampResolution = DEFAULT_TIMESTAMP_RESOLUTION;
+        baseTime = timestampResolution.convert(DEFAULT_BASE_TIME_SECONDS, TimeUnit.SECONDS);
         expiredSSTableCheckFrequency = TimeUnit.MILLISECONDS.convert(DEFAULT_EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS, TimeUnit.SECONDS);
-        maxWindowSize = DEFAULT_TIMESTAMP_RESOLUTION.convert(1, TimeUnit.DAYS);
+        maxWindowSize = timestampResolution.convert(1, TimeUnit.DAYS);
     }
 
     public static Map<String, String> validateOptions(Map<String, String> options, Map<String, String> uncheckedOptions) throws  ConfigurationException
diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStrategy.java
index 65fca27..77cb223 100644
--- a/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStrategy.java
+++ b/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStrategy.java
@@ -29,6 +29,7 @@
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.Config;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
 import org.apache.cassandra.db.rows.UnfilteredRowIterator;
@@ -37,41 +38,54 @@
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.sstable.ISSTableScanner;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
-import org.apache.cassandra.utils.FBUtilities;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 
 public class LeveledCompactionStrategy extends AbstractCompactionStrategy
 {
     private static final Logger logger = LoggerFactory.getLogger(LeveledCompactionStrategy.class);
     private static final String SSTABLE_SIZE_OPTION = "sstable_size_in_mb";
+    private static final boolean tolerateSstableSize = Boolean.getBoolean(Config.PROPERTY_PREFIX + "tolerate_sstable_size");
+    private static final String LEVEL_FANOUT_SIZE_OPTION = "fanout_size";
+    public static final int DEFAULT_LEVEL_FANOUT_SIZE = 10;
 
     @VisibleForTesting
     final LeveledManifest manifest;
     private final int maxSSTableSizeInMB;
+    private final int levelFanoutSize;
 
     public LeveledCompactionStrategy(ColumnFamilyStore cfs, Map<String, String> options)
     {
         super(cfs, options);
         int configuredMaxSSTableSize = 160;
+        int configuredLevelFanoutSize = DEFAULT_LEVEL_FANOUT_SIZE;
         SizeTieredCompactionStrategyOptions localOptions = new SizeTieredCompactionStrategyOptions(options);
         if (options != null)
-        {             
-            if (options.containsKey(SSTABLE_SIZE_OPTION))             
-            {                 
-                configuredMaxSSTableSize = Integer.parseInt(options.get(SSTABLE_SIZE_OPTION));                 
-                if (!Boolean.getBoolean("cassandra.tolerate_sstable_size"))                 
-                {                     
+        {
+            if (options.containsKey(SSTABLE_SIZE_OPTION))
+            {
+                configuredMaxSSTableSize = Integer.parseInt(options.get(SSTABLE_SIZE_OPTION));
+                if (!tolerateSstableSize)
+                {
                     if (configuredMaxSSTableSize >= 1000)
                         logger.warn("Max sstable size of {}MB is configured for {}.{}; having a unit of compaction this large is probably a bad idea",
                                 configuredMaxSSTableSize, cfs.name, cfs.getColumnFamilyName());
-                    if (configuredMaxSSTableSize < 50)  
+                    if (configuredMaxSSTableSize < 50)
                         logger.warn("Max sstable size of {}MB is configured for {}.{}.  Testing done for CASSANDRA-5727 indicates that performance improves up to 160MB",
                                 configuredMaxSSTableSize, cfs.name, cfs.getColumnFamilyName());
                 }
             }
+
+            if (options.containsKey(LEVEL_FANOUT_SIZE_OPTION))
+            {
+                configuredLevelFanoutSize = Integer.parseInt(options.get(LEVEL_FANOUT_SIZE_OPTION));
+            }
         }
         maxSSTableSizeInMB = configuredMaxSSTableSize;
+        levelFanoutSize = configuredLevelFanoutSize;
 
-        manifest = new LeveledManifest(cfs, this.maxSSTableSizeInMB, localOptions);
+        manifest = new LeveledManifest(cfs, this.maxSSTableSizeInMB, this.levelFanoutSize, localOptions);
         logger.trace("Created {}", manifest);
     }
 
@@ -96,9 +110,10 @@
      * the only difference between background and maximal in LCS is that maximal is still allowed
      * (by explicit user request) even when compaction is disabled.
      */
-    @SuppressWarnings("resource")
-    public synchronized AbstractCompactionTask getNextBackgroundTask(int gcBefore)
+    @SuppressWarnings("resource") // transaction is closed by AbstractCompactionTask::execute
+    public AbstractCompactionTask getNextBackgroundTask(int gcBefore)
     {
+        Collection<SSTableReader> previousCandidate = null;
         while (true)
         {
             OperationType op;
@@ -122,6 +137,16 @@
                 op = OperationType.COMPACTION;
             }
 
+            // Already tried acquiring references without success. It means there is a race with
+            // the tracker but candidate SSTables were not yet replaced in the compaction strategy manager
+            if (candidate.sstables.equals(previousCandidate))
+            {
+                logger.warn("Could not acquire references for compacting SSTables {} which is not a problem per se," +
+                            "unless it happens frequently, in which case it must be reported. Will retry later.",
+                            candidate.sstables);
+                return null;
+            }
+
             LifecycleTransaction txn = cfs.getTracker().tryModify(candidate.sstables, OperationType.COMPACTION);
             if (txn != null)
             {
@@ -129,13 +154,14 @@
                 newTask.setCompactionType(op);
                 return newTask;
             }
+            previousCandidate = candidate.sstables;
         }
     }
 
-    @SuppressWarnings("resource")
+    @SuppressWarnings("resource") // transaction is closed by AbstractCompactionTask::execute
     public synchronized Collection<AbstractCompactionTask> getMaximalTask(int gcBefore, boolean splitOutput)
     {
-        Iterable<SSTableReader> sstables = manifest.getAllSSTables();
+        Iterable<SSTableReader> sstables = manifest.getSSTables();
 
         Iterable<SSTableReader> filteredSSTables = filterSuspectSSTables(sstables);
         if (Iterables.isEmpty(sstables))
@@ -148,9 +174,21 @@
     }
 
     @Override
+    @SuppressWarnings("resource") // transaction is closed by AbstractCompactionTask::execute
     public AbstractCompactionTask getUserDefinedTask(Collection<SSTableReader> sstables, int gcBefore)
     {
-        throw new UnsupportedOperationException("LevelDB compaction strategy does not allow user-specified compactions");
+
+        if (sstables.isEmpty())
+            return null;
+
+        LifecycleTransaction transaction = cfs.getTracker().tryModify(sstables, OperationType.COMPACTION);
+        if (transaction == null)
+        {
+            logger.trace("Unable to mark {} for compaction; probably a background compaction got to it first.  You can disable background compactions temporarily if this is a problem", sstables);
+            return null;
+        }
+        int level = sstables.size() > 1 ? 0 : sstables.iterator().next().getSSTableLevel();
+        return new LeveledCompactionTask(cfs, transaction, level, gcBefore, level == 0 ? Long.MAX_VALUE : getMaxSSTableBytes(), false);
     }
 
     @Override
@@ -184,11 +222,13 @@
         for (SSTableReader sstable : ssTablesToGroup)
         {
             Integer level = sstable.getSSTableLevel();
-            if (!sstablesByLevel.containsKey(level))
+            Collection<SSTableReader> sstablesForLevel = sstablesByLevel.get(level);
+            if (sstablesForLevel == null)
             {
-                sstablesByLevel.put(level, new ArrayList<SSTableReader>());
+                sstablesForLevel = new ArrayList<SSTableReader>();
+                sstablesByLevel.put(level, sstablesForLevel);
             }
-            sstablesByLevel.get(level).add(sstable);
+            sstablesForLevel.add(sstable);
         }
 
         Collection<Collection<SSTableReader>> groupedSSTables = new ArrayList<>();
@@ -215,7 +255,9 @@
 
     public int getEstimatedRemainingTasks()
     {
-        return manifest.getEstimatedTasks();
+        int n = manifest.getEstimatedTasks();
+        cfs.getCompactionStrategyManager().compactionLogger.pending(this, n);
+        return n;
     }
 
     public long getMaxSSTableBytes()
@@ -223,6 +265,11 @@
         return maxSSTableSizeInMB * 1024L * 1024L;
     }
 
+    public int getLevelFanoutSize()
+    {
+        return levelFanoutSize;
+    }
+
     public ScannerList getScanners(Collection<SSTableReader> sstables, Collection<Range<Token>> ranges)
     {
         Set<SSTableReader>[] sstablesPerLevel = manifest.getSStablesPerLevelSnapshot();
@@ -255,7 +302,7 @@
                 {
                     // L0 makes no guarantees about overlapping-ness.  Just create a direct scanner for each
                     for (SSTableReader sstable : byLevel.get(level))
-                        scanners.add(sstable.getScanner(ranges, CompactionManager.instance.getRateLimiter()));
+                        scanners.add(sstable.getScanner(ranges, null));
                 }
                 else
                 {
@@ -293,9 +340,15 @@
     }
 
     @Override
+    public void addSSTables(Iterable<SSTableReader> sstables)
+    {
+        manifest.addSSTables(sstables);
+    }
+
+    @Override
     public void addSSTable(SSTableReader added)
     {
-        manifest.add(added);
+        manifest.addSSTables(Collections.singleton(added));
     }
 
     @Override
@@ -312,9 +365,11 @@
         private final List<SSTableReader> sstables;
         private final Iterator<SSTableReader> sstableIterator;
         private final long totalLength;
+        private final long compressedLength;
 
         private ISSTableScanner currentScanner;
         private long positionOffset;
+        private long totalBytesScanned = 0;
 
         public LeveledScanner(Collection<SSTableReader> sstables, Collection<Range<Token>> ranges)
         {
@@ -323,6 +378,7 @@
             // add only sstables that intersect our range, and estimate how much data that involves
             this.sstables = new ArrayList<>(sstables.size());
             long length = 0;
+            long cLength = 0;
             for (SSTableReader sstable : sstables)
             {
                 this.sstables.add(sstable);
@@ -333,13 +389,17 @@
                     estKeysInRangeRatio = ((double) sstable.estimatedKeysForRanges(ranges)) / estimatedKeys;
 
                 length += sstable.uncompressedLength() * estKeysInRangeRatio;
+                cLength += sstable.onDiskLength() * estKeysInRangeRatio;
             }
 
             totalLength = length;
+            compressedLength = cLength;
             Collections.sort(this.sstables, SSTableReader.sstableComparator);
             sstableIterator = this.sstables.iterator();
             assert sstableIterator.hasNext(); // caller should check intersecting first
-            currentScanner = sstableIterator.next().getScanner(ranges, CompactionManager.instance.getRateLimiter());
+            SSTableReader currentSSTable = sstableIterator.next();
+            currentScanner = currentSSTable.getScanner(ranges, null);
+
         }
 
         public static Collection<SSTableReader> intersecting(Collection<SSTableReader> sstables, Collection<Range<Token>> ranges)
@@ -382,6 +442,8 @@
                     return currentScanner.next();
 
                 positionOffset += currentScanner.getLengthInBytes();
+                totalBytesScanned += currentScanner.getBytesScanned();
+
                 currentScanner.close();
                 if (!sstableIterator.hasNext())
                 {
@@ -389,7 +451,8 @@
                     currentScanner = null;
                     return endOfData();
                 }
-                currentScanner = sstableIterator.next().getScanner(ranges, CompactionManager.instance.getRateLimiter());
+                SSTableReader currentSSTable = sstableIterator.next();
+                currentScanner = currentSSTable.getScanner(ranges, null);
             }
         }
 
@@ -409,6 +472,16 @@
             return positionOffset + (currentScanner == null ? 0L : currentScanner.getCurrentPosition());
         }
 
+        public long getCompressedLengthInBytes()
+        {
+            return compressedLength;
+        }
+
+        public long getBytesScanned()
+        {
+            return currentScanner == null ? totalBytesScanned : totalBytesScanned + currentScanner.getBytesScanned();
+        }
+
         public String getBackingFiles()
         {
             return Joiner.on(", ").join(sstables);
@@ -426,21 +499,17 @@
         level:
         for (int i = manifest.getLevelCount(); i >= 0; i--)
         {
-            // sort sstables by droppable ratio in descending order
-            SortedSet<SSTableReader> sstables = manifest.getLevelSorted(i, new Comparator<SSTableReader>()
-            {
-                public int compare(SSTableReader o1, SSTableReader o2)
-                {
-                    double r1 = o1.getEstimatedDroppableTombstoneRatio(gcBefore);
-                    double r2 = o2.getEstimatedDroppableTombstoneRatio(gcBefore);
-                    return -1 * Doubles.compare(r1, r2);
-                }
-            });
-            if (sstables.isEmpty())
+            if (manifest.getLevelSize(i) == 0)
                 continue;
+            // sort sstables by droppable ratio in descending order
+            List<SSTableReader> tombstoneSortedSSTables = manifest.getLevelSorted(i, (o1, o2) -> {
+                double r1 = o1.getEstimatedDroppableTombstoneRatio(gcBefore);
+                double r2 = o2.getEstimatedDroppableTombstoneRatio(gcBefore);
+                return -1 * Doubles.compare(r1, r2);
+            });
 
             Set<SSTableReader> compacting = cfs.getTracker().getCompacting();
-            for (SSTableReader sstable : sstables)
+            for (SSTableReader sstable : tombstoneSortedSSTables)
             {
                 if (sstable.getEstimatedDroppableTombstoneRatio(gcBefore) <= tombstoneThreshold)
                     continue level;
@@ -451,6 +520,26 @@
         return null;
     }
 
+    public CompactionLogger.Strategy strategyLogger()
+    {
+        return new CompactionLogger.Strategy()
+        {
+            public JsonNode sstable(SSTableReader sstable)
+            {
+                ObjectNode node = JsonNodeFactory.instance.objectNode();
+                node.put("level", sstable.getSSTableLevel());
+                node.put("min_token", sstable.first.getToken().toString());
+                node.put("max_token", sstable.last.getToken().toString());
+                return node;
+            }
+
+            public JsonNode options()
+            {
+                return null;
+            }
+        };
+    }
+
     public static Map<String, String> validateOptions(Map<String, String> options) throws ConfigurationException
     {
         Map<String, String> uncheckedOptions = AbstractCompactionStrategy.validateOptions(options);
@@ -471,6 +560,23 @@
 
         uncheckedOptions.remove(SSTABLE_SIZE_OPTION);
 
+        // Validate the fanout_size option
+        String levelFanoutSize = options.containsKey(LEVEL_FANOUT_SIZE_OPTION) ? options.get(LEVEL_FANOUT_SIZE_OPTION) : String.valueOf(DEFAULT_LEVEL_FANOUT_SIZE);
+        try
+        {
+            int fanoutSize = Integer.parseInt(levelFanoutSize);
+            if (fanoutSize < 1)
+            {
+                throw new ConfigurationException(String.format("%s must be larger than 0, but was %s", LEVEL_FANOUT_SIZE_OPTION, fanoutSize));
+            }
+        }
+        catch (NumberFormatException ex)
+        {
+            throw new ConfigurationException(String.format("%s is not a parsable int (base10) for %s", size, LEVEL_FANOUT_SIZE_OPTION), ex);
+        }
+
+        uncheckedOptions.remove(LEVEL_FANOUT_SIZE_OPTION);
+
         uncheckedOptions = SizeTieredCompactionStrategyOptions.validateOptions(options, uncheckedOptions);
 
         return uncheckedOptions;
diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledCompactionTask.java b/src/java/org/apache/cassandra/db/compaction/LeveledCompactionTask.java
index 20ff21c..c40582c 100644
--- a/src/java/org/apache/cassandra/db/compaction/LeveledCompactionTask.java
+++ b/src/java/org/apache/cassandra/db/compaction/LeveledCompactionTask.java
@@ -49,8 +49,8 @@
                                                           Set<SSTableReader> nonExpiredSSTables)
     {
         if (majorCompaction)
-            return new MajorLeveledCompactionWriter(cfs, directories, txn, nonExpiredSSTables, maxSSTableBytes, false, false);
-        return new MaxSSTableSizeWriter(cfs, directories, txn, nonExpiredSSTables, maxSSTableBytes, getLevel(), false, false);
+            return new MajorLeveledCompactionWriter(cfs, directories, txn, nonExpiredSSTables, maxSSTableBytes, false);
+        return new MaxSSTableSizeWriter(cfs, directories, txn, nonExpiredSSTables, maxSSTableBytes, getLevel(), false);
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java b/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java
new file mode 100644
index 0000000..4af5087
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java
@@ -0,0 +1,330 @@
+/*
+ * 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.db.compaction;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.PeekingIterator;
+import com.google.common.primitives.Ints;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.utils.FBUtilities;
+
+/**
+ * Handles the leveled manifest generations
+ *
+ * Not thread safe, all access should be synchronized in LeveledManifest
+ */
+class LeveledGenerations
+{
+    private static final Logger logger = LoggerFactory.getLogger(LeveledGenerations.class);
+    private final boolean strictLCSChecksTest = Boolean.getBoolean(Config.PROPERTY_PREFIX + "test.strict_lcs_checks");
+    // It includes L0, i.e. we support [L0 - L8] levels
+    static final int MAX_LEVEL_COUNT = 9;
+
+    /**
+     * This map is used to track the original NORMAL instances of sstables
+     *
+     * When aborting a compaction we can get notified that a MOVED_STARTS sstable is replaced with a NORMAL instance
+     * of the same sstable but since we use sorted sets (on the first token) in L1+ we won't find it and won't remove it.
+     * Then, when we add the NORMAL instance we have to replace the *instance* of the sstable to be able to later mark
+     * it compacting again.
+     *
+     * In this map we rely on the fact that hashCode and equals do not care about first token, so when we
+     * do allSSTables.get(instance_with_moved_starts) we will get the NORMAL sstable back, which we can then remove
+     * from the TreeSet.
+     */
+    private final Map<SSTableReader, SSTableReader> allSSTables = new HashMap<>();
+    private final Set<SSTableReader> l0 = new HashSet<>();
+    private static long lastOverlapCheck = System.nanoTime();
+    // note that since l0 is broken out, levels[0] represents L1:
+    private final TreeSet<SSTableReader> [] levels = new TreeSet[MAX_LEVEL_COUNT - 1];
+
+    private static final Comparator<SSTableReader> nonL0Comparator = (o1, o2) -> {
+        int cmp = SSTableReader.sstableComparator.compare(o1, o2);
+        if (cmp == 0)
+            cmp = Ints.compare(o1.descriptor.generation, o2.descriptor.generation);
+        return cmp;
+    };
+
+    LeveledGenerations()
+    {
+        for (int i = 0; i < MAX_LEVEL_COUNT - 1; i++)
+            levels[i] = new TreeSet<>(nonL0Comparator);
+    }
+
+    Set<SSTableReader> get(int level)
+    {
+        if (level > levelCount() - 1 || level < 0)
+            throw new ArrayIndexOutOfBoundsException("Invalid generation " + level + " - maximum is " + (levelCount() - 1));
+        if (level == 0)
+            return l0;
+        return levels[level - 1];
+    }
+
+    int levelCount()
+    {
+        return levels.length + 1;
+    }
+
+    /**
+     * Adds readers to the correct level
+     *
+     * If adding an sstable would cause an overlap in the level (if level > 1) we send it to L0. This can happen
+     * for example when moving sstables from unrepaired to repaired.
+     *
+     * If the sstable is already in the manifest we replace the instance.
+     *
+     * If the sstable exists in the manifest but has the wrong level, it is removed from the wrong level and added to the correct one
+     *
+     * todo: group sstables per level, add all if level is currently empty, improve startup speed
+     */
+    void addAll(Iterable<SSTableReader> readers)
+    {
+        logDistribution();
+        for (SSTableReader sstable : readers)
+        {
+            assert sstable.getSSTableLevel() < levelCount() : "Invalid level " + sstable.getSSTableLevel() + " out of " + (levelCount() - 1);
+            int existingLevel = getLevelIfExists(sstable);
+            if (existingLevel != -1)
+            {
+                if (sstable.getSSTableLevel() != existingLevel)
+                {
+                    logger.error("SSTable {} on the wrong level in the manifest - {} instead of {} as recorded in the sstable metadata, removing from level {}", sstable, existingLevel, sstable.getSSTableLevel(), existingLevel);
+                    if (strictLCSChecksTest)
+                        throw new AssertionError("SSTable not in matching level in manifest: "+sstable + ": "+existingLevel+" != " + sstable.getSSTableLevel());
+                }
+                else
+                {
+                    logger.info("Manifest already contains {} in level {} - replacing instance", sstable, existingLevel);
+                }
+                get(existingLevel).remove(sstable);
+                allSSTables.remove(sstable);
+            }
+
+            allSSTables.put(sstable, sstable);
+            if (sstable.getSSTableLevel() == 0)
+            {
+                l0.add(sstable);
+                continue;
+            }
+
+            TreeSet<SSTableReader> level = levels[sstable.getSSTableLevel() - 1];
+            /*
+            current level: |-----||----||----|        |---||---|
+              new sstable:                      |--|
+                                          ^ before
+                                                        ^ after
+                overlap if before.last >= newsstable.first or after.first <= newsstable.last
+             */
+            SSTableReader after = level.ceiling(sstable);
+            SSTableReader before = level.floor(sstable);
+
+            if (before != null && before.last.compareTo(sstable.first) >= 0 ||
+                after != null && after.first.compareTo(sstable.last) <= 0)
+            {
+                if (strictLCSChecksTest) // we can only assert this in tests since this is normal when for example moving sstables from unrepaired to repaired
+                    throw new AssertionError("Got unexpected overlap in level "+sstable.getSSTableLevel());
+                sendToL0(sstable);
+            }
+            else
+            {
+                level.add(sstable);
+            }
+        }
+        maybeVerifyLevels();
+    }
+
+    /**
+     * Sends sstable to L0 by mutating its level in the sstable metadata.
+     *
+     * SSTable should not exist in the manifest
+     */
+    private void sendToL0(SSTableReader sstable)
+    {
+        try
+        {
+            sstable.descriptor.getMetadataSerializer().mutateLevel(sstable.descriptor, 0);
+            sstable.reloadSSTableMetadata();
+        }
+        catch (IOException e)
+        {
+            // Adding it to L0 and marking suspect is probably the best we can do here - it won't create overlap
+            // and we won't pick it for later compactions.
+            logger.error("Failed mutating sstable metadata for {} - adding it to L0 to avoid overlap. Marking suspect", sstable, e);
+            sstable.markSuspect();
+        }
+        l0.add(sstable);
+    }
+
+    /**
+     * Tries to find the sstable in the levels without using the sstable-recorded level
+     *
+     * Used to make sure we don't try to re-add an existing sstable
+     */
+    private int getLevelIfExists(SSTableReader sstable)
+    {
+        for (int i = 0; i < levelCount(); i++)
+        {
+            if (get(i).contains(sstable))
+                return i;
+        }
+        return -1;
+    }
+
+    int remove(Collection<SSTableReader> readers)
+    {
+        int minLevel = Integer.MAX_VALUE;
+        for (SSTableReader sstable : readers)
+        {
+            int level = sstable.getSSTableLevel();
+            minLevel = Math.min(minLevel, level);
+            SSTableReader versionInManifest = allSSTables.get(sstable);
+            if (versionInManifest != null)
+            {
+                get(level).remove(versionInManifest);
+                allSSTables.remove(versionInManifest);
+            }
+        }
+        return minLevel;
+    }
+
+    int[] getAllLevelSize()
+    {
+        int[] counts = new int[levelCount()];
+        for (int i = 0; i < levelCount(); i++)
+            counts[i] = get(i).size();
+        return counts;
+    }
+
+    Set<SSTableReader> allSSTables()
+    {
+        ImmutableSet.Builder<SSTableReader> builder = ImmutableSet.builder();
+        builder.addAll(l0);
+        for (Set<SSTableReader> sstables : levels)
+            builder.addAll(sstables);
+        return builder.build();
+    }
+
+    /**
+     * given a level with sstables with first tokens [0, 10, 20, 30] and a lastCompactedSSTable with last = 15, we will
+     * return an Iterator over [20, 30, 0, 10].
+     */
+    Iterator<SSTableReader> wrappingIterator(int lvl, SSTableReader lastCompactedSSTable)
+    {
+        assert lvl > 0; // only makes sense in L1+
+        TreeSet<SSTableReader> level = levels[lvl - 1];
+        if (level.isEmpty())
+            return Collections.emptyIterator();
+        if (lastCompactedSSTable == null)
+            return level.iterator();
+
+        PeekingIterator<SSTableReader> tail = Iterators.peekingIterator(level.tailSet(lastCompactedSSTable).iterator());
+        SSTableReader pivot = null;
+        // then we need to make sure that the first token of the pivot is greater than the last token of the lastCompactedSSTable
+        while (tail.hasNext())
+        {
+            SSTableReader potentialPivot = tail.peek();
+            if (potentialPivot.first.compareTo(lastCompactedSSTable.last) > 0)
+            {
+                pivot = potentialPivot;
+                break;
+            }
+            tail.next();
+        }
+
+        if (pivot == null)
+            return level.iterator();
+
+        return Iterators.concat(tail, level.headSet(pivot, false).iterator());
+    }
+
+    void logDistribution()
+    {
+        if (logger.isTraceEnabled())
+        {
+            for (int i = 0; i < levelCount(); i++)
+            {
+                Set<SSTableReader> level = get(i);
+                if (!level.isEmpty())
+                {
+                    logger.trace("L{} contains {} SSTables ({}) in {}",
+                                 i,
+                                 level.size(),
+                                 FBUtilities.prettyPrintMemory(SSTableReader.getTotalBytes(level)),
+                                 this);
+                }
+            }
+        }
+    }
+
+    Set<SSTableReader>[] snapshot()
+    {
+        Set<SSTableReader> [] levelsCopy = new Set[levelCount()];
+        for (int i = 0; i < levelCount(); i++)
+            levelsCopy[i] = ImmutableSet.copyOf(get(i));
+        return levelsCopy;
+    }
+
+    /**
+     * do extra verification of the sstables in the generations
+     *
+     * only used during tests
+     */
+    private void maybeVerifyLevels()
+    {
+        if (!strictLCSChecksTest || System.nanoTime() - lastOverlapCheck <= TimeUnit.NANOSECONDS.convert(5, TimeUnit.SECONDS))
+            return;
+        logger.info("LCS verifying levels");
+        lastOverlapCheck = System.nanoTime();
+        for (int i = 1; i < levelCount(); i++)
+        {
+            SSTableReader prev = null;
+            for (SSTableReader sstable : get(i))
+            {
+                // no overlap:
+                assert prev == null || prev.last.compareTo(sstable.first) < 0;
+                prev = sstable;
+                // make sure it does not exist in any other level:
+                for (int j = 0; j < levelCount(); j++)
+                {
+                    if (i == j)
+                        continue;
+                    assert !get(j).contains(sstable);
+                }
+            }
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java
index bf543e5..f5615dd 100644
--- a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java
+++ b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java
@@ -17,20 +17,19 @@
  */
 package org.apache.cassandra.db.compaction;
 
-import java.io.IOException;
 import java.util.*;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
-import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Ints;
 
 import org.apache.cassandra.db.PartitionPosition;
 import org.apache.cassandra.io.sstable.Component;
+import org.apache.cassandra.io.sstable.SSTable;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -43,6 +42,8 @@
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.utils.Pair;
 
+import static org.apache.cassandra.db.compaction.LeveledGenerations.MAX_LEVEL_COUNT;
+
 public class LeveledManifest
 {
     private static final Logger logger = LoggerFactory.getLogger(LeveledManifest.class);
@@ -59,52 +60,40 @@
      * that level into lower level compactions
      */
     private static final int NO_COMPACTION_LIMIT = 25;
-    // allocate enough generations for a PB of data, with a 1-MB sstable size.  (Note that if maxSSTableSize is
-    // updated, we will still have sstables of the older, potentially smaller size.  So don't make this
-    // dependent on maxSSTableSize.)
-    public static final int MAX_LEVEL_COUNT = (int) Math.log10(1000 * 1000 * 1000);
+
     private final ColumnFamilyStore cfs;
-    @VisibleForTesting
-    protected final List<SSTableReader>[] generations;
-    private final PartitionPosition[] lastCompactedKeys;
+
+    private final LeveledGenerations generations;
+
+    private final SSTableReader[] lastCompactedSSTables;
     private final long maxSSTableSizeInBytes;
     private final SizeTieredCompactionStrategyOptions options;
     private final int [] compactionCounter;
+    private final int levelFanoutSize;
 
-    LeveledManifest(ColumnFamilyStore cfs, int maxSSTableSizeInMB, SizeTieredCompactionStrategyOptions options)
+    LeveledManifest(ColumnFamilyStore cfs, int maxSSTableSizeInMB, int fanoutSize, SizeTieredCompactionStrategyOptions options)
     {
         this.cfs = cfs;
         this.maxSSTableSizeInBytes = maxSSTableSizeInMB * 1024L * 1024L;
         this.options = options;
+        this.levelFanoutSize = fanoutSize;
 
-        generations = new List[MAX_LEVEL_COUNT];
-        lastCompactedKeys = new PartitionPosition[MAX_LEVEL_COUNT];
-        for (int i = 0; i < generations.length; i++)
-        {
-            generations[i] = new ArrayList<>();
-            lastCompactedKeys[i] = cfs.getPartitioner().getMinimumToken().minKeyBound();
-        }
+        lastCompactedSSTables = new SSTableReader[MAX_LEVEL_COUNT];
+        generations = new LeveledGenerations();
         compactionCounter = new int[MAX_LEVEL_COUNT];
     }
 
-    public static LeveledManifest create(ColumnFamilyStore cfs, int maxSSTableSize, List<SSTableReader> sstables)
+    public static LeveledManifest create(ColumnFamilyStore cfs, int maxSSTableSize, int fanoutSize, List<SSTableReader> sstables)
     {
-        return create(cfs, maxSSTableSize, sstables, new SizeTieredCompactionStrategyOptions());
+        return create(cfs, maxSSTableSize, fanoutSize, sstables, new SizeTieredCompactionStrategyOptions());
     }
 
-    public static LeveledManifest create(ColumnFamilyStore cfs, int maxSSTableSize, Iterable<SSTableReader> sstables, SizeTieredCompactionStrategyOptions options)
+    public static LeveledManifest create(ColumnFamilyStore cfs, int maxSSTableSize, int fanoutSize, Iterable<SSTableReader> sstables, SizeTieredCompactionStrategyOptions options)
     {
-        LeveledManifest manifest = new LeveledManifest(cfs, maxSSTableSize, options);
+        LeveledManifest manifest = new LeveledManifest(cfs, maxSSTableSize, fanoutSize, options);
 
         // ensure all SSTables are in the manifest
-        for (SSTableReader ssTableReader : sstables)
-        {
-            manifest.add(ssTableReader);
-        }
-        for (int i = 1; i < manifest.getAllLevelSize().length; i++)
-        {
-            manifest.repairOverlappingSSTables(i);
-        }
+        manifest.addSSTables(sstables);
         manifest.calculateLastCompactedKeys();
         return manifest;
     }
@@ -113,17 +102,18 @@
      * If we want to start compaction in level n, find the newest (by modification time) file in level n+1
      * and use its last token for last compacted key in level n;
      */
-    public void calculateLastCompactedKeys()
+    void calculateLastCompactedKeys()
     {
-        for (int i = 0; i < generations.length - 1; i++)
+        for (int i = 0; i < generations.levelCount() - 1; i++)
         {
+            Set<SSTableReader> level = generations.get(i + 1);
             // this level is empty
-            if (generations[i + 1].isEmpty())
+            if (level.isEmpty())
                 continue;
 
             SSTableReader sstableWithMaxModificationTime = null;
             long maxModificationTime = Long.MIN_VALUE;
-            for (SSTableReader ssTableReader : generations[i + 1])
+            for (SSTableReader ssTableReader : level)
             {
                 long modificationTime = ssTableReader.getCreationTimeFor(Component.DATA);
                 if (modificationTime >= maxModificationTime)
@@ -133,60 +123,27 @@
                 }
             }
 
-            lastCompactedKeys[i] = sstableWithMaxModificationTime.last;
+            lastCompactedSSTables[i] = sstableWithMaxModificationTime;
         }
     }
 
-    public synchronized void add(SSTableReader reader)
+    public synchronized void addSSTables(Iterable<SSTableReader> readers)
     {
-        int level = reader.getSSTableLevel();
-
-        assert level < generations.length : "Invalid level " + level + " out of " + (generations.length - 1);
-        logDistribution();
-        if (canAddSSTable(reader))
-        {
-            // adding the sstable does not cause overlap in the level
-            logger.trace("Adding {} to L{}", reader, level);
-            generations[level].add(reader);
-        }
-        else
-        {
-            // this can happen if:
-            // * a compaction has promoted an overlapping sstable to the given level, or
-            //   was also supposed to add an sstable at the given level.
-            // * we are moving sstables from unrepaired to repaired and the sstable
-            //   would cause overlap
-            //
-            // The add(..):ed sstable will be sent to level 0
-            try
-            {
-                reader.descriptor.getMetadataSerializer().mutateLevel(reader.descriptor, 0);
-                reader.reloadSSTableMetadata();
-            }
-            catch (IOException e)
-            {
-                logger.error("Could not change sstable level - adding it at level 0 anyway, we will find it at restart.", e);
-            }
-            generations[0].add(reader);
-        }
+        generations.addAll(readers);
     }
 
     public synchronized void replace(Collection<SSTableReader> removed, Collection<SSTableReader> added)
     {
         assert !removed.isEmpty(); // use add() instead of promote when adding new sstables
-        logDistribution();
         if (logger.isTraceEnabled())
+        {
+            generations.logDistribution();
             logger.trace("Replacing [{}]", toString(removed));
+        }
 
         // the level for the added sstables is the max of the removed ones,
         // plus one if the removed were all on the same level
-        int minLevel = Integer.MAX_VALUE;
-
-        for (SSTableReader sstable : removed)
-        {
-            int thisLevel = remove(sstable);
-            minLevel = Math.min(minLevel, thisLevel);
-        }
+        int minLevel = generations.remove(removed);
 
         // it's valid to do a remove w/o an add (e.g. on truncate)
         if (added.isEmpty())
@@ -195,76 +152,8 @@
         if (logger.isTraceEnabled())
             logger.trace("Adding [{}]", toString(added));
 
-        for (SSTableReader ssTableReader : added)
-            add(ssTableReader);
-        lastCompactedKeys[minLevel] = SSTableReader.sstableOrdering.max(added).last;
-    }
-
-    public synchronized void repairOverlappingSSTables(int level)
-    {
-        SSTableReader previous = null;
-        Collections.sort(generations[level], SSTableReader.sstableComparator);
-        List<SSTableReader> outOfOrderSSTables = new ArrayList<>();
-        for (SSTableReader current : generations[level])
-        {
-            if (previous != null && current.first.compareTo(previous.last) <= 0)
-            {
-                logger.warn(String.format("At level %d, %s [%s, %s] overlaps %s [%s, %s].  This could be caused by a bug in Cassandra 1.1.0 .. 1.1.3 or due to the fact that you have dropped sstables from another node into the data directory. " +
-                                          "Sending back to L0.  If you didn't drop in sstables, and have not yet run scrub, you should do so since you may also have rows out-of-order within an sstable",
-                                          level, previous, previous.first, previous.last, current, current.first, current.last));
-                outOfOrderSSTables.add(current);
-            }
-            else
-            {
-                previous = current;
-            }
-        }
-
-        if (!outOfOrderSSTables.isEmpty())
-        {
-            for (SSTableReader sstable : outOfOrderSSTables)
-                sendBackToL0(sstable);
-        }
-    }
-
-    /**
-     * Checks if adding the sstable creates an overlap in the level
-     * @param sstable the sstable to add
-     * @return true if it is safe to add the sstable in the level.
-     */
-    private boolean canAddSSTable(SSTableReader sstable)
-    {
-        int level = sstable.getSSTableLevel();
-        if (level == 0)
-            return true;
-
-        List<SSTableReader> copyLevel = new ArrayList<>(generations[level]);
-        copyLevel.add(sstable);
-        Collections.sort(copyLevel, SSTableReader.sstableComparator);
-
-        SSTableReader previous = null;
-        for (SSTableReader current : copyLevel)
-        {
-            if (previous != null && current.first.compareTo(previous.last) <= 0)
-                return false;
-            previous = current;
-        }
-        return true;
-    }
-
-    private synchronized void sendBackToL0(SSTableReader sstable)
-    {
-        remove(sstable);
-        try
-        {
-            sstable.descriptor.getMetadataSerializer().mutateLevel(sstable.descriptor, 0);
-            sstable.reloadSSTableMetadata();
-            add(sstable);
-        }
-        catch (IOException e)
-        {
-            throw new RuntimeException("Could not reload sstable meta data", e);
-        }
+        generations.addAll(added);
+        lastCompactedSSTables[minLevel] = SSTableReader.sstableOrdering.max(added);
     }
 
     private String toString(Collection<SSTableReader> sstables)
@@ -282,11 +171,16 @@
         return builder.toString();
     }
 
-    public static long maxBytesForLevel(int level, long maxSSTableSizeInBytes)
+    public long maxBytesForLevel(int level, long maxSSTableSizeInBytes)
+    {
+        return maxBytesForLevel(level, levelFanoutSize, maxSSTableSizeInBytes);
+    }
+
+    public static long maxBytesForLevel(int level, int levelFanoutSize, long maxSSTableSizeInBytes)
     {
         if (level == 0)
             return 4L * maxSSTableSizeInBytes;
-        double bytes = Math.pow(10, level) * maxSSTableSizeInBytes;
+        double bytes = Math.pow(levelFanoutSize, level) * maxSSTableSizeInBytes;
         if (bytes > Long.MAX_VALUE)
             throw new RuntimeException("At most " + Long.MAX_VALUE + " bytes may be in a compaction level; your maxSSTableSize must be absurdly high to compute " + bytes);
         return (long) bytes;
@@ -302,7 +196,7 @@
         // the streamed files can be placed in their original levels
         if (StorageService.instance.isBootstrapMode())
         {
-            List<SSTableReader> mostInteresting = getSSTablesForSTCS(getLevel(0));
+            List<SSTableReader> mostInteresting = getSSTablesForSTCS(generations.get(0));
             if (!mostInteresting.isEmpty())
             {
                 logger.info("Bootstrapping - doing STCS in L0");
@@ -337,19 +231,30 @@
         // This isn't a magic wand -- if you are consistently writing too fast for LCS to keep
         // up, you're still screwed.  But if instead you have intermittent bursts of activity,
         // it can help a lot.
-        for (int i = generations.length - 1; i > 0; i--)
+        for (int i = generations.levelCount() - 1; i > 0; i--)
         {
-            List<SSTableReader> sstables = getLevel(i);
+            Set<SSTableReader> sstables = generations.get(i);
             if (sstables.isEmpty())
                 continue; // mostly this just avoids polluting the debug log with zero scores
             // we want to calculate score excluding compacting ones
             Set<SSTableReader> sstablesInLevel = Sets.newHashSet(sstables);
             Set<SSTableReader> remaining = Sets.difference(sstablesInLevel, cfs.getTracker().getCompacting());
-            double score = (double) SSTableReader.getTotalBytes(remaining) / (double)maxBytesForLevel(i, maxSSTableSizeInBytes);
+            long remainingBytesForLevel = SSTableReader.getTotalBytes(remaining);
+            long maxBytesForLevel = maxBytesForLevel(i, maxSSTableSizeInBytes);
+            double score = (double) remainingBytesForLevel / (double) maxBytesForLevel;
             logger.trace("Compaction score for level {} is {}", i, score);
 
             if (score > 1.001)
             {
+                // the highest level should not ever exceed its maximum size under normal curcumstaces,
+                // but if it happens we warn about it
+                if (i == generations.levelCount() - 1)
+                {
+                    logger.warn("L" + i + " (maximum supported level) has " + remainingBytesForLevel + " bytes while "
+                            + "its maximum size is supposed to be " + maxBytesForLevel + " bytes");
+                    continue;
+                }
+
                 // before proceeding with a higher level, let's see if L0 is far enough behind to warrant STCS
                 CompactionCandidate l0Compaction = getSTCSInL0CompactionCandidate();
                 if (l0Compaction != null)
@@ -373,7 +278,7 @@
         }
 
         // Higher levels are happy, time for a standard, non-STCS L0 compaction
-        if (getLevel(0).isEmpty())
+        if (generations.get(0).isEmpty())
             return null;
         Collection<SSTableReader> candidates = getCandidatesFor(0);
         if (candidates.isEmpty())
@@ -383,14 +288,14 @@
             // small in L0.
             return getSTCSInL0CompactionCandidate();
         }
-        return new CompactionCandidate(candidates, getNextLevel(candidates), cfs.getCompactionStrategyManager().getMaxSSTableBytes());
+        return new CompactionCandidate(candidates, getNextLevel(candidates), maxSSTableSizeInBytes);
     }
 
     private CompactionCandidate getSTCSInL0CompactionCandidate()
     {
-        if (!DatabaseDescriptor.getDisableSTCSInL0() && getLevel(0).size() > MAX_COMPACTING_L0)
+        if (!DatabaseDescriptor.getDisableSTCSInL0() && generations.get(0).size() > MAX_COMPACTING_L0)
         {
-            List<SSTableReader> mostInteresting = getSSTablesForSTCS(getLevel(0));
+            List<SSTableReader> mostInteresting = getSSTablesForSTCS(generations.get(0));
             if (!mostInteresting.isEmpty())
             {
                 logger.debug("L0 is too far behind, performing size-tiering there first");
@@ -427,7 +332,7 @@
     {
         Set<SSTableReader> withStarvedCandidate = new HashSet<>(candidates);
 
-        for (int i = generations.length - 1; i > 0; i--)
+        for (int i = generations.levelCount() - 1; i > 0; i--)
             compactionCounter[i]++;
         compactionCounter[targetLevel] = 0;
         if (logger.isTraceEnabled())
@@ -436,7 +341,7 @@
                 logger.trace("CompactionCounter: {}: {}", j, compactionCounter[j]);
         }
 
-        for (int i = generations.length - 1; i > 0; i--)
+        for (int i = generations.levelCount() - 1; i > 0; i--)
         {
             if (getLevelSize(i) > 0)
             {
@@ -459,9 +364,9 @@
                         return candidates;
                     Set<SSTableReader> compacting = cfs.getTracker().getCompacting();
                     Range<PartitionPosition> boundaries = new Range<>(min, max);
-                    for (SSTableReader sstable : getLevel(i))
+                    for (SSTableReader sstable : generations.get(i))
                     {
-                        Range<PartitionPosition> r = new Range<PartitionPosition>(sstable.first, sstable.last);
+                        Range<PartitionPosition> r = new Range<>(sstable.first, sstable.last);
                         if (boundaries.contains(r) && !compacting.contains(sstable))
                         {
                             logger.info("Adding high-level (L{}) {} to candidates", sstable.getSSTableLevel(), sstable);
@@ -479,32 +384,12 @@
 
     public synchronized int getLevelSize(int i)
     {
-        if (i >= generations.length)
-            throw new ArrayIndexOutOfBoundsException("Maximum valid generation is " + (generations.length - 1));
-        return getLevel(i).size();
+        return generations.get(i).size();
     }
 
     public synchronized int[] getAllLevelSize()
     {
-        int[] counts = new int[generations.length];
-        for (int i = 0; i < counts.length; i++)
-            counts[i] = getLevel(i).size();
-        return counts;
-    }
-
-    private void logDistribution()
-    {
-        if (logger.isTraceEnabled())
-        {
-            for (int i = 0; i < generations.length; i++)
-            {
-                if (!getLevel(i).isEmpty())
-                {
-                    logger.trace("L{} contains {} SSTables ({} bytes) in {}",
-                                 i, getLevel(i).size(), SSTableReader.getTotalBytes(getLevel(i)), this);
-                }
-            }
-        }
+        return generations.getAllLevelSize();
     }
 
     @VisibleForTesting
@@ -512,10 +397,15 @@
     {
         int level = reader.getSSTableLevel();
         assert level >= 0 : reader + " not present in manifest: "+level;
-        generations[level].remove(reader);
+        generations.remove(Collections.singleton(reader));
         return level;
     }
 
+    public synchronized Set<SSTableReader> getSSTables()
+    {
+        return generations.allSSTables();
+    }
+
     private static Set<SSTableReader> overlapping(Collection<SSTableReader> candidates, Iterable<SSTableReader> others)
     {
         assert !candidates.isEmpty();
@@ -543,36 +433,43 @@
         return overlapping(first, last, others);
     }
 
-    @VisibleForTesting
-    static Set<SSTableReader> overlapping(SSTableReader sstable, Iterable<SSTableReader> others)
+    private static Set<SSTableReader> overlappingWithBounds(SSTableReader sstable, Map<SSTableReader, Bounds<Token>> others)
     {
-        return overlapping(sstable.first.getToken(), sstable.last.getToken(), others);
+        return overlappingWithBounds(sstable.first.getToken(), sstable.last.getToken(), others);
     }
 
     /**
      * @return sstables from @param sstables that contain keys between @param start and @param end, inclusive.
      */
-    private static Set<SSTableReader> overlapping(Token start, Token end, Iterable<SSTableReader> sstables)
+    @VisibleForTesting
+    static Set<SSTableReader> overlapping(Token start, Token end, Iterable<SSTableReader> sstables)
+    {
+        return overlappingWithBounds(start, end, genBounds(sstables));
+    }
+
+    private static Set<SSTableReader> overlappingWithBounds(Token start, Token end, Map<SSTableReader, Bounds<Token>> sstables)
     {
         assert start.compareTo(end) <= 0;
         Set<SSTableReader> overlapped = new HashSet<>();
-        Bounds<Token> promotedBounds = new Bounds<Token>(start, end);
-        for (SSTableReader candidate : sstables)
+        Bounds<Token> promotedBounds = new Bounds<>(start, end);
+
+        for (Map.Entry<SSTableReader, Bounds<Token>> pair : sstables.entrySet())
         {
-            Bounds<Token> candidateBounds = new Bounds<Token>(candidate.first.getToken(), candidate.last.getToken());
-            if (candidateBounds.intersects(promotedBounds))
-                overlapped.add(candidate);
+            if (pair.getValue().intersects(promotedBounds))
+                overlapped.add(pair.getKey());
         }
         return overlapped;
     }
 
-    private static final Predicate<SSTableReader> suspectP = new Predicate<SSTableReader>()
+    private static Map<SSTableReader, Bounds<Token>> genBounds(Iterable<SSTableReader> ssTableReaders)
     {
-        public boolean apply(SSTableReader candidate)
+        Map<SSTableReader, Bounds<Token>> boundsMap = new HashMap<>();
+        for (SSTableReader sstable : ssTableReaders)
         {
-            return candidate.isMarkedSuspect();
+            boundsMap.put(sstable, new Bounds<>(sstable.first.getToken(), sstable.last.getToken()));
         }
-    };
+        return boundsMap;
+    }
 
     /**
      * @return highest-priority sstables to compact for the given level.
@@ -581,14 +478,14 @@
      */
     private Collection<SSTableReader> getCandidatesFor(int level)
     {
-        assert !getLevel(level).isEmpty();
+        assert !generations.get(level).isEmpty();
         logger.trace("Choosing candidates for L{}", level);
 
         final Set<SSTableReader> compacting = cfs.getTracker().getCompacting();
 
         if (level == 0)
         {
-            Set<SSTableReader> compactingL0 = getCompacting(0);
+            Set<SSTableReader> compactingL0 = getCompactingL0();
 
             PartitionPosition lastCompactingKey = null;
             PartitionPosition firstCompactingKey = null;
@@ -614,20 +511,20 @@
             // basically screwed, since we expect all or most L0 sstables to overlap with each L1 sstable.
             // So if an L1 sstable is suspect we can't do much besides try anyway and hope for the best.
             Set<SSTableReader> candidates = new HashSet<>();
-            Set<SSTableReader> remaining = new HashSet<>();
-            Iterables.addAll(remaining, Iterables.filter(getLevel(0), Predicates.not(suspectP)));
-            for (SSTableReader sstable : ageSortedSSTables(remaining))
+            Map<SSTableReader, Bounds<Token>> remaining = genBounds(Iterables.filter(generations.get(0), Predicates.not(SSTableReader::isMarkedSuspect)));
+
+            for (SSTableReader sstable : ageSortedSSTables(remaining.keySet()))
             {
                 if (candidates.contains(sstable))
                     continue;
 
-                Sets.SetView<SSTableReader> overlappedL0 = Sets.union(Collections.singleton(sstable), overlapping(sstable, remaining));
+                Sets.SetView<SSTableReader> overlappedL0 = Sets.union(Collections.singleton(sstable), overlappingWithBounds(sstable, remaining));
                 if (!Sets.intersection(overlappedL0, compactingL0).isEmpty())
                     continue;
 
                 for (SSTableReader newCandidate : overlappedL0)
                 {
-                    if (firstCompactingKey == null || lastCompactingKey == null || overlapping(firstCompactingKey.getToken(), lastCompactingKey.getToken(), Arrays.asList(newCandidate)).size() == 0)
+                    if (firstCompactingKey == null || lastCompactingKey == null || overlapping(firstCompactingKey.getToken(), lastCompactingKey.getToken(), Collections.singleton(newCandidate)).size() == 0)
                         candidates.add(newCandidate);
                     remaining.remove(newCandidate);
                 }
@@ -646,7 +543,7 @@
                 // add sstables from L1 that overlap candidates
                 // if the overlapping ones are already busy in a compaction, leave it out.
                 // TODO try to find a set of L0 sstables that only overlaps with non-busy L1 sstables
-                Set<SSTableReader> l1overlapping = overlapping(candidates, getLevel(1));
+                Set<SSTableReader> l1overlapping = overlapping(candidates, generations.get(1));
                 if (Sets.intersection(l1overlapping, compacting).size() > 0)
                     return Collections.emptyList();
                 if (!overlapping(candidates, compactingL0).isEmpty())
@@ -659,26 +556,16 @@
                 return candidates;
         }
 
-        // for non-L0 compactions, pick up where we left off last time
-        Collections.sort(getLevel(level), SSTableReader.sstableComparator);
-        int start = 0; // handles case where the prior compaction touched the very last range
-        for (int i = 0; i < getLevel(level).size(); i++)
-        {
-            SSTableReader sstable = getLevel(level).get(i);
-            if (sstable.first.compareTo(lastCompactedKeys[level]) > 0)
-            {
-                start = i;
-                break;
-            }
-        }
-
         // look for a non-suspect keyspace to compact with, starting with where we left off last time,
         // and wrapping back to the beginning of the generation if necessary
-        for (int i = 0; i < getLevel(level).size(); i++)
+        Map<SSTableReader, Bounds<Token>> sstablesNextLevel = genBounds(generations.get(level + 1));
+        Iterator<SSTableReader> levelIterator = generations.wrappingIterator(level, lastCompactedSSTables[level]);
+        while (levelIterator.hasNext())
         {
-            SSTableReader sstable = getLevel(level).get((start + i) % getLevel(level).size());
-            Set<SSTableReader> candidates = Sets.union(Collections.singleton(sstable), overlapping(sstable, getLevel(level + 1)));
-            if (Iterables.any(candidates, suspectP))
+            SSTableReader sstable = levelIterator.next();
+            Set<SSTableReader> candidates = Sets.union(Collections.singleton(sstable), overlappingWithBounds(sstable, sstablesNextLevel));
+
+            if (Iterables.any(candidates, SSTableReader::isMarkedSuspect))
                 continue;
             if (Sets.intersection(candidates, compacting).isEmpty())
                 return candidates;
@@ -688,10 +575,10 @@
         return Collections.emptyList();
     }
 
-    private Set<SSTableReader> getCompacting(int level)
+    private Set<SSTableReader> getCompactingL0()
     {
         Set<SSTableReader> sstables = new HashSet<>();
-        Set<SSTableReader> levelSSTables = new HashSet<>(getLevel(level));
+        Set<SSTableReader> levelSSTables = new HashSet<>(generations.get(0));
         for (SSTableReader sstable : cfs.getTracker().getCompacting())
         {
             if (levelSSTables.contains(sstable))
@@ -700,21 +587,17 @@
         return sstables;
     }
 
-    private List<SSTableReader> ageSortedSSTables(Collection<SSTableReader> candidates)
+    @VisibleForTesting
+    List<SSTableReader> ageSortedSSTables(Collection<SSTableReader> candidates)
     {
-        List<SSTableReader> ageSortedCandidates = new ArrayList<>(candidates);
-        Collections.sort(ageSortedCandidates, SSTableReader.maxTimestampComparator);
-        return ageSortedCandidates;
+        List<SSTableReader> copy = new ArrayList<>(candidates);
+        copy.sort(SSTableReader.maxTimestampAscending);
+        return ImmutableList.copyOf(copy);
     }
 
     public synchronized Set<SSTableReader>[] getSStablesPerLevelSnapshot()
     {
-        Set<SSTableReader>[] sstablesPerLevel = new Set[generations.length];
-        for (int i = 0; i < generations.length; i++)
-        {
-            sstablesPerLevel[i] = new HashSet<>(generations[i]);
-        }
-        return sstablesPerLevel;
+        return generations.snapshot();
     }
 
     @Override
@@ -723,34 +606,24 @@
         return "Manifest@" + hashCode();
     }
 
-    public int getLevelCount()
+    public synchronized int getLevelCount()
     {
-        for (int i = generations.length - 1; i >= 0; i--)
+        for (int i = generations.levelCount() - 1; i >= 0; i--)
         {
-            if (getLevel(i).size() > 0)
+            if (generations.get(i).size() > 0)
                 return i;
         }
         return 0;
     }
 
-    public synchronized SortedSet<SSTableReader> getLevelSorted(int level, Comparator<SSTableReader> comparator)
-    {
-        return ImmutableSortedSet.copyOf(comparator, getLevel(level));
-    }
-
-    public List<SSTableReader> getLevel(int i)
-    {
-        return generations[i];
-    }
-
     public synchronized int getEstimatedTasks()
     {
         long tasks = 0;
-        long[] estimated = new long[generations.length];
+        long[] estimated = new long[generations.levelCount()];
 
-        for (int i = generations.length - 1; i >= 0; i--)
+        for (int i = generations.levelCount() - 1; i >= 0; i--)
         {
-            List<SSTableReader> sstables = getLevel(i);
+            Set<SSTableReader> sstables = generations.get(i);
             // If there is 1 byte over TBL - (MBL * 1.001), there is still a task left, so we need to round up.
             estimated[i] = (long)Math.ceil((double)Math.max(0L, SSTableReader.getTotalBytes(sstables) - (long)(maxBytesForLevel(i, maxSSTableSizeInBytes) * 1.001)) / (double)maxSSTableSizeInBytes);
             tasks += estimated[i];
@@ -782,17 +655,18 @@
             assert newLevel > 0;
         }
         return newLevel;
-
     }
 
-    public Iterable<SSTableReader> getAllSSTables()
+    synchronized Set<SSTableReader> getLevel(int level)
     {
-        Set<SSTableReader> sstables = new HashSet<>();
-        for (List<SSTableReader> generation : generations)
-        {
-            sstables.addAll(generation);
-        }
-        return sstables;
+        return ImmutableSet.copyOf(generations.get(level));
+    }
+
+    synchronized List<SSTableReader> getLevelSorted(int level, Comparator<SSTableReader> comparator)
+    {
+        List<SSTableReader> copy = new ArrayList<>(generations.get(level));
+        copy.sort(comparator);
+        return ImmutableList.copyOf(copy);
     }
 
     public static class CompactionCandidate
diff --git a/src/java/org/apache/cassandra/db/compaction/OperationType.java b/src/java/org/apache/cassandra/db/compaction/OperationType.java
index 20e6df2..e957e42 100644
--- a/src/java/org/apache/cassandra/db/compaction/OperationType.java
+++ b/src/java/org/apache/cassandra/db/compaction/OperationType.java
@@ -19,6 +19,7 @@
 
 public enum OperationType
 {
+    /** Each modification here should be also applied to {@link org.apache.cassandra.tools.nodetool.Stop#compactionType} */
     COMPACTION("Compaction"),
     VALIDATION("Validation"),
     KEY_CACHE_SAVE("Key cache save"),
@@ -37,7 +38,9 @@
     STREAM("Stream"),
     WRITE("Write"),
     VIEW_BUILD("View build"),
-    INDEX_SUMMARY("Index summary redistribution");
+    INDEX_SUMMARY("Index summary redistribution"),
+    RELOCATE("Relocate sstables to correct disk"),
+    GARBAGE_COLLECT("Remove deleted data");
 
     public final String type;
     public final String fileName;
diff --git a/src/java/org/apache/cassandra/db/compaction/SSTableSplitter.java b/src/java/org/apache/cassandra/db/compaction/SSTableSplitter.java
index fce8c2e..924e29c 100644
--- a/src/java/org/apache/cassandra/db/compaction/SSTableSplitter.java
+++ b/src/java/org/apache/cassandra/db/compaction/SSTableSplitter.java
@@ -26,8 +26,8 @@
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
 
-public class SSTableSplitter {
-
+public class SSTableSplitter 
+{
     private final SplittingCompactionTask task;
 
     private CompactionInfo.Holder info;
@@ -61,7 +61,7 @@
 
         public SplittingCompactionTask(ColumnFamilyStore cfs, LifecycleTransaction transaction, int sstableSizeInMB)
         {
-            super(cfs, transaction, CompactionManager.NO_GC, true, false);
+            super(cfs, transaction, CompactionManager.NO_GC, false);
             this.sstableSizeInMB = sstableSizeInMB;
 
             if (sstableSizeInMB <= 0)
@@ -80,7 +80,7 @@
                                                               LifecycleTransaction txn,
                                                               Set<SSTableReader> nonExpiredSSTables)
         {
-            return new MaxSSTableSizeWriter(cfs, directories, txn, nonExpiredSSTables, sstableSizeInMB * 1024L * 1024L, 0, true, false);
+            return new MaxSSTableSizeWriter(cfs, directories, txn, nonExpiredSSTables, sstableSizeInMB * 1024L * 1024L, 0, false);
         }
 
         @Override
diff --git a/src/java/org/apache/cassandra/db/compaction/Scrubber.java b/src/java/org/apache/cassandra/db/compaction/Scrubber.java
index b42093f..f43e3ce 100644
--- a/src/java/org/apache/cassandra/db/compaction/Scrubber.java
+++ b/src/java/org/apache/cassandra/db/compaction/Scrubber.java
@@ -58,26 +58,20 @@
     private final ScrubInfo scrubInfo;
     private final RowIndexEntry.IndexSerializer rowIndexEntrySerializer;
 
-    private int goodRows;
-    private int badRows;
-    private int emptyRows;
+    private int goodPartitions;
+    private int badPartitions;
+    private int emptyPartitions;
 
     private ByteBuffer currentIndexKey;
     private ByteBuffer nextIndexKey;
-    long currentRowPositionFromIndex;
-    long nextRowPositionFromIndex;
+    private long currentPartitionPositionFromIndex;
+    private long nextPartitionPositionFromIndex;
 
     private NegativeLocalDeletionInfoMetrics negativeLocalDeletionInfoMetrics = new NegativeLocalDeletionInfoMetrics();
 
     private final OutputHandler outputHandler;
 
-    private static final Comparator<Partition> partitionComparator = new Comparator<Partition>()
-    {
-         public int compare(Partition r1, Partition r2)
-         {
-             return r1.partitionKey().compareTo(r2.partitionKey());
-         }
-    };
+    private static final Comparator<Partition> partitionComparator = Comparator.comparing(Partition::partitionKey);
     private final SortedSet<Partition> outOfOrder = new TreeSet<>(partitionComparator);
 
     public Scrubber(ColumnFamilyStore cfs, LifecycleTransaction transaction, boolean skipCorrupted, boolean checkData) throws IOException
@@ -108,43 +102,41 @@
         this.rowIndexEntrySerializer = sstable.descriptor.version.getSSTableFormat().getIndexSerializer(sstable.metadata,
                                                                                                         sstable.descriptor.version,
                                                                                                         sstable.header);
+
         List<SSTableReader> toScrub = Collections.singletonList(sstable);
 
-        // Calculate the expected compacted filesize
-        this.destination = cfs.getDirectories().getWriteableLocationAsFile(cfs.getExpectedCompactedFileSize(toScrub, OperationType.SCRUB));
-        if (destination == null)
-            throw new IOException("disk full");
 
+        this.destination = cfs.getDirectories().getLocationForDisk(cfs.getDiskBoundaries().getCorrectDiskForSSTable(sstable));
         this.isCommutative = cfs.metadata.isCounter();
 
         boolean hasIndexFile = (new File(sstable.descriptor.filenameFor(Component.PRIMARY_INDEX))).exists();
         this.isIndex = cfs.isIndex();
         if (!hasIndexFile)
         {
-            // if there's any corruption in the -Data.db then rows can't be skipped over. but it's worth a shot.
+            // if there's any corruption in the -Data.db then partitions can't be skipped over. but it's worth a shot.
             outputHandler.warn("Missing component: " + sstable.descriptor.filenameFor(Component.PRIMARY_INDEX));
         }
         this.checkData = checkData && !this.isIndex; //LocalByPartitionerType does not support validation
         this.expectedBloomFilterSize = Math.max(
-            cfs.metadata.params.minIndexInterval,
-            hasIndexFile ? SSTableReader.getApproximateKeyCount(toScrub) : 0);
+        cfs.metadata.params.minIndexInterval,
+        hasIndexFile ? SSTableReader.getApproximateKeyCount(toScrub) : 0);
 
-        // loop through each row, deserializing to check for damage.
+        // loop through each partition, deserializing to check for damage.
         // we'll also loop through the index at the same time, using the position from the index to recover if the
-        // row header (key or data size) is corrupt. (This means our position in the index file will be one row
-        // "ahead" of the data file.)
+        // partition header (key or data size) is corrupt. (This means our position in the index file will be one
+        // partition "ahead" of the data file.)
         this.dataFile = transaction.isOffline()
                         ? sstable.openDataReader()
                         : sstable.openDataReader(CompactionManager.instance.getRateLimiter());
 
         this.indexFile = hasIndexFile
-                ? RandomAccessReader.open(new File(sstable.descriptor.filenameFor(Component.PRIMARY_INDEX)))
-                : null;
+                         ? RandomAccessReader.open(new File(sstable.descriptor.filenameFor(Component.PRIMARY_INDEX)))
+                         : null;
 
         this.scrubInfo = new ScrubInfo(dataFile, sstable);
 
-        this.currentRowPositionFromIndex = 0;
-        this.nextRowPositionFromIndex = 0;
+        this.currentPartitionPositionFromIndex = 0;
+        this.nextPartitionPositionFromIndex = 0;
 
         if (reinsertOverflowedTTLRows)
             outputHandler.output("Starting scrub with reinsert overflowed TTL option");
@@ -155,19 +147,30 @@
         return checkData ? UnfilteredRowIterators.withValidation(iter, filename) : iter;
     }
 
+    private String keyString(DecoratedKey key)
+    {
+        try
+        {
+            return cfs.metadata.getKeyValidator().getString(key.getKey());
+        }
+        catch (Exception e)
+        {
+            return String.format("(corrupted; hex value: %s)", ByteBufferUtil.bytesToHex(key.getKey()));
+        }
+    }
+
     public void scrub()
     {
         List<SSTableReader> finished = new ArrayList<>();
-        boolean completed = false;
-        outputHandler.output(String.format("Scrubbing %s (%s bytes)", sstable, dataFile.length()));
-        try (SSTableRewriter writer = SSTableRewriter.construct(cfs, transaction, false, sstable.maxDataAge, transaction.isOffline());
+        outputHandler.output(String.format("Scrubbing %s (%s)", sstable, FBUtilities.prettyPrintMemory(dataFile.length())));
+        try (SSTableRewriter writer = SSTableRewriter.construct(cfs, transaction, false, sstable.maxDataAge);
              Refs<SSTableReader> refs = Refs.ref(Collections.singleton(sstable)))
         {
             nextIndexKey = indexAvailable() ? ByteBufferUtil.readWithShortLength(indexFile) : null;
             if (indexAvailable())
             {
                 // throw away variable so we don't have a side effect in the assert
-                long firstRowPositionFromIndex = rowIndexEntrySerializer.deserialize(indexFile).position;
+                long firstRowPositionFromIndex = rowIndexEntrySerializer.deserializePositionAndSkip(indexFile);
                 assert firstRowPositionFromIndex == 0 : firstRowPositionFromIndex;
             }
 
@@ -186,7 +189,10 @@
                 DecoratedKey key = null;
                 try
                 {
-                    key = sstable.decorateKey(ByteBufferUtil.readWithShortLength(dataFile));
+                    ByteBuffer raw = ByteBufferUtil.readWithShortLength(dataFile);
+                    if (!cfs.metadata.isIndex())
+                        cfs.metadata.getKeyValidator().validate(raw);
+                    key = sstable.decorateKey(raw);
                 }
                 catch (Throwable th)
                 {
@@ -202,33 +208,31 @@
                 long dataSizeFromIndex = -1;
                 if (currentIndexKey != null)
                 {
-                    dataStartFromIndex = currentRowPositionFromIndex + 2 + currentIndexKey.remaining();
-                    dataSizeFromIndex = nextRowPositionFromIndex - dataStartFromIndex;
+                    dataStartFromIndex = currentPartitionPositionFromIndex + 2 + currentIndexKey.remaining();
+                    dataSizeFromIndex = nextPartitionPositionFromIndex - dataStartFromIndex;
                 }
 
-                // avoid an NPE if key is null
-                String keyName = key == null ? "(unreadable key)" : ByteBufferUtil.bytesToHex(key.getKey());
-                outputHandler.debug(String.format("row %s is %s bytes", keyName, dataSizeFromIndex));
-
+                String keyName = key == null ? "(unreadable key)" : keyString(key);
+                outputHandler.debug(String.format("partition %s is %s", keyName, FBUtilities.prettyPrintMemory(dataSizeFromIndex)));
                 assert currentIndexKey != null || !indexAvailable();
 
                 try
                 {
                     if (key == null)
-                        throw new IOError(new IOException("Unable to read row key from data file"));
+                        throw new IOError(new IOException("Unable to read partition key from data file"));
 
                     if (currentIndexKey != null && !key.getKey().equals(currentIndexKey))
                     {
                         throw new IOError(new IOException(String.format("Key from data file (%s) does not match key from index file (%s)",
-                                //ByteBufferUtil.bytesToHex(key.getKey()), ByteBufferUtil.bytesToHex(currentIndexKey))));
-                                "_too big_", ByteBufferUtil.bytesToHex(currentIndexKey))));
+                                                                        //ByteBufferUtil.bytesToHex(key.getKey()), ByteBufferUtil.bytesToHex(currentIndexKey))));
+                                                                        "_too big_", ByteBufferUtil.bytesToHex(currentIndexKey))));
                     }
 
                     if (indexFile != null && dataSizeFromIndex > dataFile.length())
-                        throw new IOError(new IOException("Impossible row size (greater than file length): " + dataSizeFromIndex));
+                        throw new IOError(new IOException("Impossible partition size (greater than file length): " + dataSizeFromIndex));
 
                     if (indexFile != null && dataStart != dataStartFromIndex)
-                        outputHandler.warn(String.format("Data file row position %d differs from index file row position %d", dataStart, dataStartFromIndex));
+                        outputHandler.warn(String.format("Data file partition position %d differs from index file partition position %d", dataStart, dataStartFromIndex));
 
                     if (tryAppend(prevKey, key, writer))
                         prevKey = key;
@@ -236,16 +240,18 @@
                 catch (Throwable th)
                 {
                     throwIfFatal(th);
-                    outputHandler.warn("Error reading row (stacktrace follows):", th);
+                    outputHandler.warn(String.format("Error reading partition %s (stacktrace follows):", keyName), th);
 
                     if (currentIndexKey != null
                         && (key == null || !key.getKey().equals(currentIndexKey) || dataStart != dataStartFromIndex))
                     {
-                        outputHandler.output(String.format("Retrying from row index; data is %s bytes starting at %s",
-                                                  dataSizeFromIndex, dataStartFromIndex));
+                        outputHandler.output(String.format("Retrying from index; data is %s bytes starting at %s",
+                                                           dataSizeFromIndex, dataStartFromIndex));
                         key = sstable.decorateKey(currentIndexKey);
                         try
                         {
+                            if (!cfs.metadata.isIndex())
+                                cfs.metadata.getKeyValidator().validate(key.getKey());
                             dataFile.seek(dataStartFromIndex);
 
                             if (tryAppend(prevKey, key, writer))
@@ -256,9 +262,9 @@
                             throwIfFatal(th2);
                             throwIfCannotContinue(key, th2);
 
-                            outputHandler.warn("Retry failed too. Skipping to next row (retry's stacktrace follows)", th2);
-                            badRows++;
-                            if (!seekToNextRow())
+                            outputHandler.warn("Retry failed too. Skipping to next partition (retry's stacktrace follows)", th2);
+                            badPartitions++;
+                            if (!seekToNextPartition())
                                 break;
                         }
                     }
@@ -266,10 +272,10 @@
                     {
                         throwIfCannotContinue(key, th);
 
-                        outputHandler.warn("Row starting at position " + dataStart + " is unreadable; skipping to next");
-                        badRows++;
+                        outputHandler.warn("Partition starting at position " + dataStart + " is unreadable; skipping to next");
+                        badPartitions++;
                         if (currentIndexKey != null)
-                            if (!seekToNextRow())
+                            if (!seekToNextPartition())
                                 break;
                     }
                 }
@@ -277,8 +283,8 @@
 
             if (!outOfOrder.isEmpty())
             {
-                // out of order rows, but no bad rows found - we can keep our repairedAt time
-                long repairedAt = badRows > 0 ? ActiveRepairService.UNREPAIRED_SSTABLE : sstable.getSSTableMetadata().repairedAt;
+                // out of order partitions/rows, but no bad partition found - we can keep our repairedAt time
+                long repairedAt = badPartitions > 0 ? ActiveRepairService.UNREPAIRED_SSTABLE : sstable.getSSTableMetadata().repairedAt;
                 SSTableReader newInOrderSstable;
                 try (SSTableWriter inOrderWriter = CompactionManager.createWriter(cfs, destination, expectedBloomFilterSize, repairedAt, sstable, transaction))
                 {
@@ -288,12 +294,12 @@
                 }
                 transaction.update(newInOrderSstable, false);
                 finished.add(newInOrderSstable);
-                outputHandler.warn(String.format("%d out of order rows found while scrubbing %s; Those have been written (in order) to a new sstable (%s)", outOfOrder.size(), sstable, newInOrderSstable));
+                outputHandler.warn(String.format("%d out of order partition (or partitions with out of order rows) found while scrubbing %s; " +
+                                                 "Those have been written (in order) to a new sstable (%s)", outOfOrder.size(), sstable, newInOrderSstable));
             }
 
             // finish obsoletes the old sstable
-            finished.addAll(writer.setRepairedAt(badRows > 0 ? ActiveRepairService.UNREPAIRED_SSTABLE : sstable.getSSTableMetadata().repairedAt).finish());
-            completed = true;
+            finished.addAll(writer.setRepairedAt(badPartitions > 0 ? ActiveRepairService.UNREPAIRED_SSTABLE : sstable.getSSTableMetadata().repairedAt).finish());
         }
         catch (IOException e)
         {
@@ -305,20 +311,20 @@
                 finished.forEach(sstable -> sstable.selfRef().release());
         }
 
-        if (completed)
+        if (!finished.isEmpty())
         {
-            outputHandler.output("Scrub of " + sstable + " complete: " + goodRows + " rows in new sstable and " + emptyRows + " empty (tombstoned) rows dropped");
+            outputHandler.output("Scrub of " + sstable + " complete: " + goodPartitions + " partitionss in new sstable and " + emptyPartitions + " empty (tombstoned) partitions dropped");
             if (negativeLocalDeletionInfoMetrics.fixedRows > 0)
                 outputHandler.output("Fixed " + negativeLocalDeletionInfoMetrics.fixedRows + " rows with overflowed local deletion time.");
-            if (badRows > 0)
-                outputHandler.warn("Unable to recover " + badRows + " rows that were skipped.  You can attempt manual recovery from the pre-scrub snapshot.  You can also run nodetool repair to transfer the data from a healthy replica, if any");
+            if (badPartitions > 0)
+                outputHandler.warn("Unable to recover " + badPartitions + " partitions that were skipped.  You can attempt manual recovery from the pre-scrub snapshot.  You can also run nodetool repair to transfer the data from a healthy replica, if any");
         }
         else
         {
-            if (badRows > 0)
-                outputHandler.warn("No valid rows found while scrubbing " + sstable + "; it is marked for deletion now. If you want to attempt manual recovery, you can find a copy in the pre-scrub snapshot");
+            if (badPartitions > 0)
+                outputHandler.warn("No valid partitions found while scrubbing " + sstable + "; it is marked for deletion now. If you want to attempt manual recovery, you can find a copy in the pre-scrub snapshot");
             else
-                outputHandler.output("Scrub of " + sstable + " complete; looks like all " + emptyRows + " rows were tombstoned");
+                outputHandler.output("Scrub of " + sstable + " complete; looks like all " + emptyPartitions + " partitions were tombstoned");
         }
     }
 
@@ -335,19 +341,19 @@
         {
             if (prevKey != null && prevKey.compareTo(key) > 0)
             {
-                saveOutOfOrderRow(prevKey, key, iterator);
+                saveOutOfOrderPartition(prevKey, key, iterator);
                 return false;
             }
 
             if (writer.tryAppend(iterator) == null)
-                emptyRows++;
+                emptyPartitions++;
             else
-                goodRows++;
+                goodPartitions++;
         }
 
         if (sstableIterator.hasRowsOutOfOrder())
         {
-            outputHandler.warn(String.format("Out of order rows found in partition: %s", key));
+            outputHandler.warn(String.format("Out of order rows found in partition: %s", keyString(key)));
             outOfOrder.add(sstableIterator.getRowsOutOfOrder());
         }
 
@@ -360,7 +366,7 @@
      */
     private UnfilteredRowIterator getIterator(DecoratedKey key)
     {
-        RowMergingSSTableIterator rowMergingIterator = new RowMergingSSTableIterator(sstable, dataFile, key);
+        RowMergingSSTableIterator rowMergingIterator = new RowMergingSSTableIterator(SSTableIdentityIterator.create(sstable, dataFile, key));
         return reinsertOverflowedTTLRows ? new FixNegativeLocalDeletionTimeIterator(rowMergingIterator,
                                                                                     outputHandler,
                                                                                     negativeLocalDeletionInfoMetrics) : rowMergingIterator;
@@ -369,21 +375,21 @@
     private void updateIndexKey()
     {
         currentIndexKey = nextIndexKey;
-        currentRowPositionFromIndex = nextRowPositionFromIndex;
+        currentPartitionPositionFromIndex = nextPartitionPositionFromIndex;
         try
         {
             nextIndexKey = !indexAvailable() ? null : ByteBufferUtil.readWithShortLength(indexFile);
 
-            nextRowPositionFromIndex = !indexAvailable()
-                    ? dataFile.length()
-                    : rowIndexEntrySerializer.deserialize(indexFile).position;
+            nextPartitionPositionFromIndex = !indexAvailable()
+                                       ? dataFile.length()
+                                       : rowIndexEntrySerializer.deserializePositionAndSkip(indexFile);
         }
         catch (Throwable th)
         {
             JVMStabilityInspector.inspectThrowable(th);
             outputHandler.warn("Error reading index file", th);
             nextIndexKey = null;
-            nextRowPositionFromIndex = dataFile.length();
+            nextPartitionPositionFromIndex = dataFile.length();
         }
     }
 
@@ -392,20 +398,20 @@
         return indexFile != null && !indexFile.isEOF();
     }
 
-    private boolean seekToNextRow()
+    private boolean seekToNextPartition()
     {
-        while(nextRowPositionFromIndex < dataFile.length())
+        while(nextPartitionPositionFromIndex < dataFile.length())
         {
             try
             {
-                dataFile.seek(nextRowPositionFromIndex);
+                dataFile.seek(nextPartitionPositionFromIndex);
                 return true;
             }
             catch (Throwable th)
             {
                 throwIfFatal(th);
-                outputHandler.warn(String.format("Failed to seek to next row position %d", nextRowPositionFromIndex), th);
-                badRows++;
+                outputHandler.warn(String.format("Failed to seek to next partition position %d", nextPartitionPositionFromIndex), th);
+                badPartitions++;
             }
 
             updateIndexKey();
@@ -414,10 +420,11 @@
         return false;
     }
 
-    private void saveOutOfOrderRow(DecoratedKey prevKey, DecoratedKey key, UnfilteredRowIterator iterator)
+    private void saveOutOfOrderPartition(DecoratedKey prevKey, DecoratedKey key, UnfilteredRowIterator iterator)
     {
         // TODO bitch if the row is too large?  if it is there's not much we can do ...
-        outputHandler.warn(String.format("Out of order row detected (%s found after %s)", key, prevKey));
+        outputHandler.warn(String.format("Out of order partition detected (%s found after %s)",
+                                         keyString(key), keyString(prevKey)));
         outOfOrder.add(ImmutableBTreePartition.create(iterator));
     }
 
@@ -431,18 +438,19 @@
     {
         if (isIndex)
         {
-            outputHandler.warn(String.format("An error occurred while scrubbing the row with key '%s' for an index table. " +
-                                             "Scrubbing will abort for this table and the index will be rebuilt.", key));
+            outputHandler.warn(String.format("An error occurred while scrubbing the partition with key '%s' for an index table. " +
+                                             "Scrubbing will abort for this table and the index will be rebuilt.", keyString(key)));
             throw new IOError(th);
         }
 
         if (isCommutative && !skipCorrupted)
         {
-            outputHandler.warn(String.format("An error occurred while scrubbing the row with key '%s'.  Skipping corrupt " +
-                                             "rows in counter tables will result in undercounts for the affected " +
+            outputHandler.warn(String.format("An error occurred while scrubbing the partition with key '%s'.  Skipping corrupt " +
+                                             "data in counter tables will result in undercounts for the affected " +
                                              "counters (see CASSANDRA-2759 for more details), so by default the scrub will " +
-                                             "stop at this point.  If you would like to skip the row anyway and continue " +
-                                             "scrubbing, re-run the scrub with the --skip-corrupted option.", key));
+                                             "stop at this point.  If you would like to skip the partition anyway and continue " +
+                                             "scrubbing, re-run the scrub with the --skip-corrupted option.",
+                                             keyString(key)));
             throw new IOError(th);
         }
     }
@@ -483,7 +491,7 @@
             }
             catch (Exception e)
             {
-                throw new RuntimeException();
+                throw new RuntimeException(e);
             }
         }
 
@@ -502,15 +510,15 @@
 
     public static final class ScrubResult
     {
-        public final int goodRows;
-        public final int badRows;
-        public final int emptyRows;
+        public final int goodPartitions;
+        public final int badPartitions;
+        public final int emptyPartitions;
 
         public ScrubResult(Scrubber scrubber)
         {
-            this.goodRows = scrubber.goodRows;
-            this.badRows = scrubber.badRows;
-            this.emptyRows = scrubber.emptyRows;
+            this.goodPartitions = scrubber.goodPartitions;
+            this.badPartitions = scrubber.badPartitions;
+            this.emptyPartitions = scrubber.emptyPartitions;
         }
     }
 
@@ -525,41 +533,45 @@
      *
      * For more details, refer to CASSANDRA-12144.
      */
-    private static class RowMergingSSTableIterator extends SSTableIdentityIterator
+    private static class RowMergingSSTableIterator extends WrappingUnfilteredRowIterator
     {
-        RowMergingSSTableIterator(SSTableReader sstable, RandomAccessReader file, DecoratedKey key)
+        Unfiltered nextToOffer = null;
+
+        RowMergingSSTableIterator(UnfilteredRowIterator source)
         {
-            super(sstable, file, key);
+            super(source);
         }
 
         @Override
-        protected Unfiltered doCompute()
+        public boolean hasNext()
         {
-            if (!iterator.hasNext())
-                return endOfData();
+            return nextToOffer != null || wrapped.hasNext();
+        }
 
-            Unfiltered next = iterator.next();
-            if (!next.isRow())
-                return next;
+        @Override
+        public Unfiltered next()
+        {
+            Unfiltered next = nextToOffer != null ? nextToOffer : wrapped.next();
 
-            while (iterator.hasNext())
+            if (next.isRow())
             {
-                Unfiltered peek = iterator.peek();
-                // If there was a duplicate row, merge it.
-                if (next.clustering().equals(peek.clustering()) && peek.isRow())
+                while (wrapped.hasNext())
                 {
-                    iterator.next(); // Make sure that the peeked item was consumed.
+                    Unfiltered peek = wrapped.next();
+                    if (!peek.isRow() || !next.clustering().equals(peek.clustering()))
+                    {
+                        nextToOffer = peek; // Offer peek in next call
+                        return next;
+                    }
+
+                    // Duplicate row, merge it.
                     next = Rows.merge((Row) next, (Row) peek, FBUtilities.nowInSeconds());
                 }
-                else
-                {
-                    break;
-                }
             }
 
+            nextToOffer = null;
             return next;
         }
-
     }
 
     /**
@@ -809,4 +821,5 @@
             return builder.build();
         }
     }
+
 }
diff --git a/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategy.java
index dae3a78..048803a 100644
--- a/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategy.java
+++ b/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategy.java
@@ -65,7 +65,8 @@
 
     protected SizeTieredCompactionStrategyOptions sizeTieredOptions;
     protected volatile int estimatedRemainingTasks;
-    private final Set<SSTableReader> sstables = new HashSet<>();
+    @VisibleForTesting
+    protected final Set<SSTableReader> sstables = new HashSet<>();
 
     public SizeTieredCompactionStrategy(ColumnFamilyStore cfs, Map<String, String> options)
     {
@@ -84,7 +85,8 @@
 
         List<List<SSTableReader>> buckets = getBuckets(createSSTableAndLengthPairs(candidates), sizeTieredOptions.bucketHigh, sizeTieredOptions.bucketLow, sizeTieredOptions.minSSTableSize);
         logger.trace("Compaction buckets are {}", buckets);
-        updateEstimatedCompactionsByTasks(buckets);
+        estimatedRemainingTasks = getEstimatedCompactionsByTasks(cfs, buckets);
+        cfs.getCompactionStrategyManager().compactionLogger.pending(this, estimatedRemainingTasks);
         List<SSTableReader> mostInteresting = mostInterestingBucket(buckets, minThreshold, maxThreshold);
         if (!mostInteresting.isEmpty())
             return mostInteresting;
@@ -100,8 +102,7 @@
         if (sstablesWithTombstones.isEmpty())
             return Collections.emptyList();
 
-        Collections.sort(sstablesWithTombstones, new SSTableReader.SizeComparator());
-        return Collections.singletonList(sstablesWithTombstones.get(0));
+        return Collections.singletonList(Collections.max(sstablesWithTombstones, SSTableReader.sizeComparator));
     }
 
 
@@ -174,8 +175,9 @@
     }
 
     @SuppressWarnings("resource")
-    public synchronized AbstractCompactionTask getNextBackgroundTask(int gcBefore)
+    public AbstractCompactionTask getNextBackgroundTask(int gcBefore)
     {
+        List<SSTableReader> previousCandidate = null;
         while (true)
         {
             List<SSTableReader> hottestBucket = getNextBackgroundSSTables(gcBefore);
@@ -183,9 +185,20 @@
             if (hottestBucket.isEmpty())
                 return null;
 
+            // Already tried acquiring references without success. It means there is a race with
+            // the tracker but candidate SSTables were not yet replaced in the compaction strategy manager
+            if (hottestBucket.equals(previousCandidate))
+            {
+                logger.warn("Could not acquire references for compacting SSTables {} which is not a problem per se," +
+                            "unless it happens frequently, in which case it must be reported. Will retry later.",
+                            hottestBucket);
+                return null;
+            }
+
             LifecycleTransaction transaction = cfs.getTracker().tryModify(hottestBucket, OperationType.COMPACTION);
             if (transaction != null)
                 return new CompactionTask(cfs, transaction, gcBefore);
+            previousCandidate = hottestBucket;
         }
     }
 
@@ -282,15 +295,15 @@
         return new ArrayList<List<T>>(buckets.values());
     }
 
-    private void updateEstimatedCompactionsByTasks(List<List<SSTableReader>> tasks)
+    public static int getEstimatedCompactionsByTasks(ColumnFamilyStore cfs, List<List<SSTableReader>> tasks)
     {
         int n = 0;
-        for (List<SSTableReader> bucket: tasks)
+        for (List<SSTableReader> bucket : tasks)
         {
             if (bucket.size() >= cfs.getMinimumCompactionThreshold())
                 n += Math.ceil((double)bucket.size() / cfs.getMaximumCompactionThreshold());
         }
-        estimatedRemainingTasks = n;
+        return n;
     }
 
     public long getMaxSSTableBytes()
diff --git a/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategyOptions.java b/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategyOptions.java
index 911bb9f..288af2b 100644
--- a/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategyOptions.java
+++ b/src/java/org/apache/cassandra/db/compaction/SizeTieredCompactionStrategyOptions.java
@@ -94,4 +94,11 @@
 
         return uncheckedOptions;
     }
+
+    @Override
+    public String toString()
+    {
+        return String.format("Min sstable size: %d, bucket low: %f, bucket high: %f", minSSTableSize, bucketLow, bucketHigh);
+    }
+
 }
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionController.java b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionController.java
new file mode 100644
index 0000000..cf9e0e6
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionController.java
@@ -0,0 +1,49 @@
+/*
+ * 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.db.compaction;
+
+
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+
+public class TimeWindowCompactionController extends CompactionController
+{
+    private static final Logger logger = LoggerFactory.getLogger(TimeWindowCompactionController.class);
+
+    private final boolean ignoreOverlaps;
+
+    public TimeWindowCompactionController(ColumnFamilyStore cfs, Set<SSTableReader> compacting, int gcBefore, boolean ignoreOverlaps)
+    {
+        super(cfs, compacting, gcBefore);
+        this.ignoreOverlaps = ignoreOverlaps;
+        if (ignoreOverlaps)
+            logger.warn("You are running with sstables overlapping checks disabled, it can result in loss of data");
+    }
+
+    @Override
+    protected boolean ignoreOverlaps()
+    {
+        return ignoreOverlaps;
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategy.java
index 5ae1cc7..bbc9cdf 100644
--- a/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategy.java
+++ b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategy.java
@@ -23,6 +23,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
+import java.util.Objects;
 import java.util.TreeSet;
 import java.util.concurrent.TimeUnit;
 import java.util.HashSet;
@@ -60,20 +61,23 @@
         super(cfs, options);
         this.estimatedRemainingTasks = 0;
         this.options = new TimeWindowCompactionStrategyOptions(options);
-        if (!options.containsKey(AbstractCompactionStrategy.TOMBSTONE_COMPACTION_INTERVAL_OPTION) && !options.containsKey(AbstractCompactionStrategy.TOMBSTONE_THRESHOLD_OPTION))
+        String[] tsOpts = { UNCHECKED_TOMBSTONE_COMPACTION_OPTION, TOMBSTONE_COMPACTION_INTERVAL_OPTION, TOMBSTONE_THRESHOLD_OPTION };
+        if (Arrays.stream(tsOpts).map(o -> options.get(o)).filter(Objects::nonNull).anyMatch(v -> !v.equals("false")))
         {
-            disableTombstoneCompactions = true;
-            logger.debug("Disabling tombstone compactions for TWCS");
+            logger.debug("Enabling tombstone compactions for TWCS");
         }
         else
-            logger.debug("Enabling tombstone compactions for TWCS");
-
+        {
+            logger.debug("Disabling tombstone compactions for TWCS");
+            disableTombstoneCompactions = true;
+        }
     }
 
     @Override
     @SuppressWarnings("resource") // transaction is closed by AbstractCompactionTask::execute
     public AbstractCompactionTask getNextBackgroundTask(int gcBefore)
     {
+        List<SSTableReader> previousCandidate = null;
         while (true)
         {
             List<SSTableReader> latestBucket = getNextBackgroundSSTables(gcBefore);
@@ -81,9 +85,20 @@
             if (latestBucket.isEmpty())
                 return null;
 
+            // Already tried acquiring references without success. It means there is a race with
+            // the tracker but candidate SSTables were not yet replaced in the compaction strategy manager
+            if (latestBucket.equals(previousCandidate))
+            {
+                logger.warn("Could not acquire references for compacting SSTables {} which is not a problem per se," +
+                            "unless it happens frequently, in which case it must be reported. Will retry later.",
+                            latestBucket);
+                return null;
+            }
+
             LifecycleTransaction modifier = cfs.getTracker().tryModify(latestBucket, OperationType.COMPACTION);
             if (modifier != null)
-                return new CompactionTask(cfs, modifier, gcBefore);
+                return new TimeWindowCompactionTask(cfs, modifier, gcBefore, options.ignoreOverlaps);
+            previousCandidate = latestBucket;
         }
     }
 
@@ -105,7 +120,8 @@
         if (System.currentTimeMillis() - lastExpiredCheck > options.expiredSSTableCheckFrequency)
         {
             logger.debug("TWCS expired check sufficiently far in the past, checking for fully expired SSTables");
-            expired = CompactionController.getFullyExpiredSSTables(cfs, uncompacting, cfs.getOverlappingLiveSSTables(uncompacting), gcBefore);
+            expired = CompactionController.getFullyExpiredSSTables(cfs, uncompacting, options.ignoreOverlaps ? Collections.emptySet() : cfs.getOverlappingLiveSSTables(uncompacting),
+                                                                   gcBefore, options.ignoreOverlaps);
             lastExpiredCheck = System.currentTimeMillis();
         }
         else
@@ -145,7 +161,7 @@
         if (sstablesWithTombstones.isEmpty())
             return Collections.emptyList();
 
-        return Collections.singletonList(Collections.min(sstablesWithTombstones, new SSTableReader.SizeComparator()));
+        return Collections.singletonList(Collections.min(sstablesWithTombstones, SSTableReader.sizeComparator));
     }
 
     private List<SSTableReader> getCompactionCandidates(Iterable<SSTableReader> candidateSSTables)
@@ -155,16 +171,15 @@
         if(buckets.right > this.highestWindowSeen)
             this.highestWindowSeen = buckets.right;
 
-        updateEstimatedCompactionsByTasks(buckets.left);
-        List<SSTableReader> mostInteresting = newestBucket(buckets.left,
-                                                           cfs.getMinimumCompactionThreshold(),
-                                                           cfs.getMaximumCompactionThreshold(),
-                                                           options.sstableWindowUnit,
-                                                           options.sstableWindowSize,
-                                                           options.stcsOptions,
-                                                           this.highestWindowSeen);
-        if (!mostInteresting.isEmpty())
-            return mostInteresting;
+        NewestBucket mostInteresting = newestBucket(buckets.left,
+                cfs.getMinimumCompactionThreshold(),
+                cfs.getMaximumCompactionThreshold(),
+                options.stcsOptions,
+                this.highestWindowSeen);
+
+        this.estimatedRemainingTasks = mostInteresting.estimatedRemainingTasks;
+        if (!mostInteresting.sstables.isEmpty())
+            return mostInteresting.sstables;
         return null;
     }
 
@@ -193,16 +208,16 @@
         switch(windowTimeUnit)
         {
             case MINUTES:
-                lowerTimestamp = timestampInSeconds - ((timestampInSeconds) % (60 * windowTimeSize));
+                lowerTimestamp = timestampInSeconds - ((timestampInSeconds) % (60L * windowTimeSize));
                 upperTimestamp = (lowerTimestamp + (60L * (windowTimeSize - 1L))) + 59L;
                 break;
             case HOURS:
-                lowerTimestamp = timestampInSeconds - ((timestampInSeconds) % (3600 * windowTimeSize));
+                lowerTimestamp = timestampInSeconds - ((timestampInSeconds) % (3600L * windowTimeSize));
                 upperTimestamp = (lowerTimestamp + (3600L * (windowTimeSize - 1L))) + 3599L;
                 break;
             case DAYS:
             default:
-                lowerTimestamp = timestampInSeconds - ((timestampInSeconds) % (86400 * windowTimeSize));
+                lowerTimestamp = timestampInSeconds - ((timestampInSeconds) % (86400L * windowTimeSize));
                 upperTimestamp = (lowerTimestamp + (86400L * (windowTimeSize - 1L))) + 86399L;
                 break;
         }
@@ -240,24 +255,29 @@
                 maxTimestamp = bounds.left;
         }
 
-        logger.trace("buckets {}, max timestamp", buckets, maxTimestamp);
+        logger.trace("buckets {}, max timestamp {}", buckets, maxTimestamp);
         return Pair.create(buckets, maxTimestamp);
     }
 
-    private void updateEstimatedCompactionsByTasks(HashMultimap<Long, SSTableReader> tasks)
+    static final class NewestBucket
     {
-        int n = 0;
-        long now = this.highestWindowSeen;
+        /** The sstables that should be compacted next */
+        final List<SSTableReader> sstables;
 
-        for(Long key : tasks.keySet())
+        /** The number of tasks estimated */
+        final int estimatedRemainingTasks;
+
+        NewestBucket(List<SSTableReader> sstables, int estimatedRemainingTasks)
         {
-            // For current window, make sure it's compactable
-            if (key.compareTo(now) >= 0 && tasks.get(key).size() >= cfs.getMinimumCompactionThreshold())
-                n++;
-            else if (key.compareTo(now) < 0 && tasks.get(key).size() >= 2)
-                n++;
+            this.sstables = sstables;
+            this.estimatedRemainingTasks = estimatedRemainingTasks;
         }
-        this.estimatedRemainingTasks = n;
+
+        @Override
+        public String toString()
+        {
+            return String.format("sstables: %s, estimated remaining tasks: %d", sstables, estimatedRemainingTasks);
+        }
     }
 
 
@@ -268,12 +288,15 @@
      * @return a bucket (list) of sstables to compact.
      */
     @VisibleForTesting
-    static List<SSTableReader> newestBucket(HashMultimap<Long, SSTableReader> buckets, int minThreshold, int maxThreshold, TimeUnit sstableWindowUnit, int sstableWindowSize, SizeTieredCompactionStrategyOptions stcsOptions, long now)
+    static NewestBucket newestBucket(HashMultimap<Long, SSTableReader> buckets, int minThreshold, int maxThreshold, SizeTieredCompactionStrategyOptions stcsOptions, long now)
     {
         // If the current bucket has at least minThreshold SSTables, choose that one.
         // For any other bucket, at least 2 SSTables is enough.
         // In any case, limit to maxThreshold SSTables.
 
+        List<SSTableReader> sstables = Collections.emptyList();
+        int estimatedRemainingTasks = 0;
+
         TreeSet<Long> allKeys = new TreeSet<>(buckets.keySet());
 
         Iterator<Long> it = allKeys.descendingIterator();
@@ -287,24 +310,44 @@
                 // If we're in the newest bucket, we'll use STCS to prioritize sstables
                 List<Pair<SSTableReader,Long>> pairs = SizeTieredCompactionStrategy.createSSTableAndLengthPairs(bucket);
                 List<List<SSTableReader>> stcsBuckets = SizeTieredCompactionStrategy.getBuckets(pairs, stcsOptions.bucketHigh, stcsOptions.bucketLow, stcsOptions.minSSTableSize);
-                logger.debug("Using STCS compaction for first window of bucket: data files {} , options {}", pairs, stcsOptions);
                 List<SSTableReader> stcsInterestingBucket = SizeTieredCompactionStrategy.mostInterestingBucket(stcsBuckets, minThreshold, maxThreshold);
 
                 // If the tables in the current bucket aren't eligible in the STCS strategy, we'll skip it and look for other buckets
                 if (!stcsInterestingBucket.isEmpty())
-                    return stcsInterestingBucket;
+                {
+                    double remaining = bucket.size() - maxThreshold;
+                    estimatedRemainingTasks +=  1 + (remaining > minThreshold ? Math.ceil(remaining / maxThreshold) : 0);
+                    if (sstables.isEmpty())
+                    {
+                        logger.debug("Using STCS compaction for first window of bucket: data files {} , options {}", pairs, stcsOptions);
+                        sstables = stcsInterestingBucket;
+                    }
+                    else
+                    {
+                        logger.trace("First window of bucket is eligible but not selected: data files {} , options {}", pairs, stcsOptions);
+                    }
+                }
             }
             else if (bucket.size() >= 2 && key < now)
             {
-                logger.debug("bucket size {} >= 2 and not in current bucket, compacting what's here: {}", bucket.size(), bucket);
-                return trimToThreshold(bucket, maxThreshold);
+                double remaining = bucket.size() - maxThreshold;
+                estimatedRemainingTasks +=  1 + (remaining > minThreshold ? Math.ceil(remaining / maxThreshold) : 0);
+                if (sstables.isEmpty())
+                {
+                    logger.debug("bucket size {} >= 2 and not in current bucket, compacting what's here: {}", bucket.size(), bucket);
+                    sstables = trimToThreshold(bucket, maxThreshold);
+                }
+                else
+                {
+                    logger.trace("bucket size {} >= 2 and not in current bucket, eligible but not selected: {}", bucket.size(), bucket);
+                }
             }
             else
             {
                 logger.trace("No compaction necessary for bucket size {} , key {}, now {}", bucket.size(), key, now);
             }
         }
-        return Collections.<SSTableReader>emptyList();
+        return new NewestBucket(sstables, estimatedRemainingTasks);
     }
 
     /**
@@ -318,7 +361,7 @@
         List<SSTableReader> ssTableReaders = new ArrayList<>(bucket);
 
         // Trim the largest sstables off the end to meet the maxThreshold
-        Collections.sort(ssTableReaders, new SSTableReader.SizeComparator());
+        Collections.sort(ssTableReaders, SSTableReader.sizeComparator);
 
         return ImmutableList.copyOf(Iterables.limit(ssTableReaders, maxThreshold));
     }
@@ -333,7 +376,7 @@
         LifecycleTransaction txn = cfs.getTracker().tryModify(filteredSSTables, OperationType.COMPACTION);
         if (txn == null)
             return null;
-        return Collections.singleton(new CompactionTask(cfs, txn, gcBefore));
+        return Collections.singleton(new TimeWindowCompactionTask(cfs, txn, gcBefore, options.ignoreOverlaps));
     }
 
     /**
@@ -363,7 +406,7 @@
             return null;
         }
 
-        return new CompactionTask(cfs, modifier, gcBefore).setUserDefined(true);
+        return new TimeWindowCompactionTask(cfs, modifier, gcBefore, options.ignoreOverlaps).setUserDefined(true);
     }
 
     public int getEstimatedRemainingTasks()
diff --git a/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategyOptions.java b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategyOptions.java
index bcbdab6..24b4fe0 100644
--- a/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategyOptions.java
+++ b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategyOptions.java
@@ -26,6 +26,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.config.Config;
 import org.apache.cassandra.exceptions.ConfigurationException;
 
 public final class TimeWindowCompactionStrategyOptions
@@ -36,16 +37,21 @@
     protected static final TimeUnit DEFAULT_COMPACTION_WINDOW_UNIT = TimeUnit.DAYS;
     protected static final int DEFAULT_COMPACTION_WINDOW_SIZE = 1;
     protected static final int DEFAULT_EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS = 60 * 10;
+    protected static final Boolean DEFAULT_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION = false;
 
     protected static final String TIMESTAMP_RESOLUTION_KEY = "timestamp_resolution";
     protected static final String COMPACTION_WINDOW_UNIT_KEY = "compaction_window_unit";
     protected static final String COMPACTION_WINDOW_SIZE_KEY = "compaction_window_size";
     protected static final String EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_KEY = "expired_sstable_check_frequency_seconds";
+    protected static final String UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_KEY = "unsafe_aggressive_sstable_expiration";
+
+    static final String UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_PROPERTY = Config.PROPERTY_PREFIX + "allow_unsafe_aggressive_sstable_expiration";
 
     protected final int sstableWindowSize;
     protected final TimeUnit sstableWindowUnit;
     protected final TimeUnit timestampResolution;
     protected final long expiredSSTableCheckFrequency;
+    protected final boolean ignoreOverlaps;
 
     SizeTieredCompactionStrategyOptions stcsOptions;
 
@@ -68,6 +74,9 @@
         optionValue = options.get(EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_KEY);
         expiredSSTableCheckFrequency = TimeUnit.MILLISECONDS.convert(optionValue == null ? DEFAULT_EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS : Long.parseLong(optionValue), TimeUnit.SECONDS);
 
+        optionValue = options.get(UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_KEY);
+        ignoreOverlaps = optionValue == null ? DEFAULT_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION : (Boolean.getBoolean(UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_PROPERTY) && Boolean.parseBoolean(optionValue));
+
         stcsOptions = new SizeTieredCompactionStrategyOptions(options);
     }
 
@@ -77,6 +86,7 @@
         timestampResolution = DEFAULT_TIMESTAMP_RESOLUTION;
         sstableWindowSize = DEFAULT_COMPACTION_WINDOW_SIZE;
         expiredSSTableCheckFrequency = TimeUnit.MILLISECONDS.convert(DEFAULT_EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS, TimeUnit.SECONDS);
+        ignoreOverlaps = DEFAULT_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION;
         stcsOptions = new SizeTieredCompactionStrategyOptions();
     }
 
@@ -114,12 +124,12 @@
             int sstableWindowSize = optionValue == null ? DEFAULT_COMPACTION_WINDOW_SIZE : Integer.parseInt(optionValue);
             if (sstableWindowSize < 1)
             {
-                throw new ConfigurationException(String.format("%s must be greater than 1", DEFAULT_COMPACTION_WINDOW_SIZE, sstableWindowSize));
+                throw new ConfigurationException(String.format("%d must be greater than 1 for %s", sstableWindowSize, COMPACTION_WINDOW_SIZE_KEY));
             }
         }
         catch (NumberFormatException e)
         {
-            throw new ConfigurationException(String.format("%s is not a parsable int (base10) for %s", optionValue, DEFAULT_COMPACTION_WINDOW_SIZE), e);
+            throw new ConfigurationException(String.format("%s is not a parsable int (base10) for %s", optionValue, COMPACTION_WINDOW_SIZE_KEY), e);
         }
 
         optionValue = options.get(EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_KEY);
@@ -136,10 +146,22 @@
             throw new ConfigurationException(String.format("%s is not a parsable int (base10) for %s", optionValue, EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_KEY), e);
         }
 
+
+        optionValue = options.get(UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_KEY);
+        if (optionValue != null)
+        {
+            if (!(optionValue.equalsIgnoreCase("true") || optionValue.equalsIgnoreCase("false")))
+                throw new ConfigurationException(String.format("%s is not 'true' or 'false' (%s)", UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_KEY, optionValue));
+
+            if(optionValue.equalsIgnoreCase("true") && !Boolean.getBoolean(UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_PROPERTY))
+                throw new ConfigurationException(String.format("%s is requested but not allowed, restart cassandra with -D%s=true to allow it", UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_KEY, UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_PROPERTY));
+        }
+
         uncheckedOptions.remove(COMPACTION_WINDOW_SIZE_KEY);
         uncheckedOptions.remove(COMPACTION_WINDOW_UNIT_KEY);
         uncheckedOptions.remove(TIMESTAMP_RESOLUTION_KEY);
         uncheckedOptions.remove(EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_KEY);
+        uncheckedOptions.remove(UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_KEY);
 
         uncheckedOptions = SizeTieredCompactionStrategyOptions.validateOptions(options, uncheckedOptions);
 
diff --git a/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionTask.java b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionTask.java
new file mode 100644
index 0000000..4f1fe6a
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/compaction/TimeWindowCompactionTask.java
@@ -0,0 +1,42 @@
+/*
+ * 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.db.compaction;
+
+import java.util.Set;
+
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+
+public class TimeWindowCompactionTask extends CompactionTask
+{
+    private final boolean ignoreOverlaps;
+
+    public TimeWindowCompactionTask(ColumnFamilyStore cfs, LifecycleTransaction txn, int gcBefore, boolean ignoreOverlaps)
+    {
+        super(cfs, txn, gcBefore);
+        this.ignoreOverlaps = ignoreOverlaps;
+    }
+
+    @Override
+    public CompactionController getCompactionController(Set<SSTableReader> toCompact)
+    {
+        return new TimeWindowCompactionController(cfs, toCompact, gcBefore, ignoreOverlaps);
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/compaction/Upgrader.java b/src/java/org/apache/cassandra/db/compaction/Upgrader.java
index 77831a7..7a5b719 100644
--- a/src/java/org/apache/cassandra/db/compaction/Upgrader.java
+++ b/src/java/org/apache/cassandra/db/compaction/Upgrader.java
@@ -76,6 +76,7 @@
                                     cfs.metadata,
                                     sstableMetadataCollector,
                                     SerializationHeader.make(cfs.metadata, Sets.newHashSet(sstable)),
+                                    cfs.indexManager.listIndexes(),
                                     transaction);
     }
 
@@ -83,7 +84,7 @@
     {
         outputHandler.output("Upgrading " + sstable);
         int nowInSec = FBUtilities.nowInSeconds();
-        try (SSTableRewriter writer = SSTableRewriter.construct(cfs, transaction, keepOriginals, CompactionTask.getMaxDataAge(transaction.originals()), true);
+        try (SSTableRewriter writer = SSTableRewriter.construct(cfs, transaction, keepOriginals, CompactionTask.getMaxDataAge(transaction.originals()));
              AbstractCompactionStrategy.ScannerList scanners = strategyManager.getScanners(transaction.originals());
              CompactionIterator iter = new CompactionIterator(transaction.opType(), scanners.scanners, controller, nowInSec, UUIDGen.getTimeUUID()))
         {
diff --git a/src/java/org/apache/cassandra/db/compaction/Verifier.java b/src/java/org/apache/cassandra/db/compaction/Verifier.java
index 82eabd0..8c5e8bb 100644
--- a/src/java/org/apache/cassandra/db/compaction/Verifier.java
+++ b/src/java/org/apache/cassandra/db/compaction/Verifier.java
@@ -35,6 +35,7 @@
 import org.apache.cassandra.io.util.RandomAccessReader;
 import org.apache.cassandra.service.ActiveRepairService;
 import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.OutputHandler;
 import org.apache.cassandra.utils.UUIDGen;
 
@@ -64,12 +65,12 @@
     private final OutputHandler outputHandler;
     private FileDigestValidator validator;
 
-    public Verifier(ColumnFamilyStore cfs, SSTableReader sstable, boolean isOffline) throws IOException
+    public Verifier(ColumnFamilyStore cfs, SSTableReader sstable, boolean isOffline)
     {
         this(cfs, sstable, new OutputHandler.LogOutput(), isOffline);
     }
 
-    public Verifier(ColumnFamilyStore cfs, SSTableReader sstable, OutputHandler outputHandler, boolean isOffline) throws IOException
+    public Verifier(ColumnFamilyStore cfs, SSTableReader sstable, OutputHandler outputHandler, boolean isOffline)
     {
         this.cfs = cfs;
         this.sstable = sstable;
@@ -89,7 +90,7 @@
     {
         long rowStart = 0;
 
-        outputHandler.output(String.format("Verifying %s (%s bytes)", sstable, dataFile.length()));
+        outputHandler.output(String.format("Verifying %s (%s)", sstable, FBUtilities.prettyPrintMemory(dataFile.length())));
         outputHandler.output(String.format("Deserializing sstable metadata for %s ", sstable));
         try
         {
@@ -144,7 +145,7 @@
         {
             ByteBuffer nextIndexKey = ByteBufferUtil.readWithShortLength(indexFile);
             {
-                long firstRowPositionFromIndex = rowIndexEntrySerializer.deserialize(indexFile).position;
+                long firstRowPositionFromIndex = rowIndexEntrySerializer.deserializePositionAndSkip(indexFile);
                 if (firstRowPositionFromIndex != 0)
                     markAndThrow();
             }
@@ -178,7 +179,7 @@
                     nextIndexKey = indexFile.isEOF() ? null : ByteBufferUtil.readWithShortLength(indexFile);
                     nextRowPositionFromIndex = indexFile.isEOF()
                                              ? dataFile.length()
-                                             : rowIndexEntrySerializer.deserialize(indexFile).position;
+                                             : rowIndexEntrySerializer.deserializePositionAndSkip(indexFile);
                 }
                 catch (Throwable th)
                 {
@@ -193,7 +194,7 @@
                 long dataSize = nextRowPositionFromIndex - dataStartFromIndex;
                 // avoid an NPE if key is null
                 String keyName = key == null ? "(unreadable key)" : ByteBufferUtil.bytesToHex(key.getKey());
-                outputHandler.debug(String.format("row %s is %s bytes", keyName, dataSize));
+                outputHandler.debug(String.format("row %s is %s", keyName, FBUtilities.prettyPrintMemory(dataSize)));
 
                 assert currentIndexKey != null || indexFile.isEOF();
 
@@ -203,7 +204,7 @@
                         markAndThrow();
 
                     //mimic the scrub read path, intentionally unused
-                    try (UnfilteredRowIterator iterator = new SSTableIdentityIterator(sstable, dataFile, key))
+                    try (UnfilteredRowIterator iterator = SSTableIdentityIterator.create(sstable, dataFile, key))
                     {
                     }
 
diff --git a/src/java/org/apache/cassandra/db/compaction/writers/CompactionAwareWriter.java b/src/java/org/apache/cassandra/db/compaction/writers/CompactionAwareWriter.java
index 1ceed1c..7ffe33a 100644
--- a/src/java/org/apache/cassandra/db/compaction/writers/CompactionAwareWriter.java
+++ b/src/java/org/apache/cassandra/db/compaction/writers/CompactionAwareWriter.java
@@ -18,18 +18,28 @@
 
 package org.apache.cassandra.db.compaction.writers;
 
+import java.io.File;
 import java.util.Collection;
+import java.util.List;
 import java.util.Set;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.db.DiskBoundaries;
+import org.apache.cassandra.db.PartitionPosition;
 import org.apache.cassandra.db.rows.UnfilteredRowIterator;
 import org.apache.cassandra.db.compaction.CompactionTask;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
+import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.SSTableRewriter;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.concurrent.Transactional;
+import org.apache.cassandra.db.compaction.OperationType;
 
 
 /**
@@ -38,6 +48,8 @@
  */
 public abstract class CompactionAwareWriter extends Transactional.AbstractTransactional implements Transactional
 {
+    protected static final Logger logger = LoggerFactory.getLogger(CompactionAwareWriter.class);
+
     protected final ColumnFamilyStore cfs;
     protected final Directories directories;
     protected final Set<SSTableReader> nonExpiredSSTables;
@@ -45,10 +57,13 @@
     protected final long maxAge;
     protected final long minRepairedAt;
 
-    protected final LifecycleTransaction txn;
     protected final SSTableRewriter sstableWriter;
-    private boolean isInitialized = false;
+    protected final LifecycleTransaction txn;
+    private final List<Directories.DataDirectory> locations;
+    private final List<PartitionPosition> diskBoundaries;
+    private int locationIndex;
 
+    @Deprecated
     public CompactionAwareWriter(ColumnFamilyStore cfs,
                                  Directories directories,
                                  LifecycleTransaction txn,
@@ -56,14 +71,28 @@
                                  boolean offline,
                                  boolean keepOriginals)
     {
+        this(cfs, directories, txn, nonExpiredSSTables, keepOriginals);
+    }
+
+    public CompactionAwareWriter(ColumnFamilyStore cfs,
+                                 Directories directories,
+                                 LifecycleTransaction txn,
+                                 Set<SSTableReader> nonExpiredSSTables,
+                                 boolean keepOriginals)
+    {
         this.cfs = cfs;
         this.directories = directories;
         this.nonExpiredSSTables = nonExpiredSSTables;
-        this.estimatedTotalKeys = SSTableReader.getApproximateKeyCount(nonExpiredSSTables);
-        this.maxAge = CompactionTask.getMaxDataAge(nonExpiredSSTables);
-        this.minRepairedAt = CompactionTask.getMinRepairedAt(nonExpiredSSTables);
         this.txn = txn;
-        this.sstableWriter = SSTableRewriter.construct(cfs, txn, keepOriginals, maxAge, offline);
+
+        estimatedTotalKeys = SSTableReader.getApproximateKeyCount(nonExpiredSSTables);
+        maxAge = CompactionTask.getMaxDataAge(nonExpiredSSTables);
+        sstableWriter = SSTableRewriter.construct(cfs, txn, keepOriginals, maxAge);
+        minRepairedAt = CompactionTask.getMinRepairedAt(nonExpiredSSTables);
+        DiskBoundaries db = cfs.getDiskBoundaries();
+        diskBoundaries = db.positions;
+        locations = db.directories;
+        locationIndex = -1;
     }
 
     @Override
@@ -103,6 +132,11 @@
         return estimatedTotalKeys;
     }
 
+    /**
+     * Writes a partition in an implementation specific way
+     * @param partition the partition to append
+     * @return true if the partition was written, false otherwise
+     */
     public final boolean append(UnfilteredRowIterator partition)
     {
         maybeSwitchWriter(partition.partitionKey());
@@ -124,9 +158,26 @@
      */
     protected void maybeSwitchWriter(DecoratedKey key)
     {
-        if (!isInitialized)
-            switchCompactionLocation(getDirectories().getWriteableLocation(getExpectedWriteSize()));
-        isInitialized = true;
+        if (diskBoundaries == null)
+        {
+            if (locationIndex < 0)
+            {
+                Directories.DataDirectory defaultLocation = getWriteDirectory(nonExpiredSSTables, getExpectedWriteSize());
+                switchCompactionLocation(defaultLocation);
+                locationIndex = 0;
+            }
+            return;
+        }
+
+        if (locationIndex > -1 && key.compareTo(diskBoundaries.get(locationIndex)) < 0)
+            return;
+
+        int prevIdx = locationIndex;
+        while (locationIndex == -1 || key.compareTo(diskBoundaries.get(locationIndex)) > 0)
+            locationIndex++;
+        if (prevIdx >= 0)
+            logger.debug("Switching write location from {} to {}", locations.get(prevIdx), locations.get(locationIndex));
+        switchCompactionLocation(locations.get(locationIndex));
     }
 
     /**
@@ -147,14 +198,46 @@
 
     /**
      * Return a directory where we can expect expectedWriteSize to fit.
+     *
+     * @param sstables the sstables to compact
+     * @return
      */
-    public Directories.DataDirectory getWriteDirectory(long expectedWriteSize)
+    public Directories.DataDirectory getWriteDirectory(Iterable<SSTableReader> sstables, long estimatedWriteSize)
     {
-        Directories.DataDirectory directory = getDirectories().getWriteableLocation(expectedWriteSize);
-        if (directory == null)
-            throw new RuntimeException("Insufficient disk space to write " + expectedWriteSize + " bytes");
+        Descriptor descriptor = null;
+        for (SSTableReader sstable : sstables)
+        {
+            if (descriptor == null)
+                descriptor = sstable.descriptor;
+            if (!descriptor.directory.equals(sstable.descriptor.directory))
+            {
+                logger.trace("All sstables not from the same disk - putting results in {}", descriptor.directory);
+                break;
+            }
+        }
+        Directories.DataDirectory d = getDirectories().getDataDirectoryForFile(descriptor);
+        if (d != null)
+        {
+            long availableSpace = d.getAvailableSpace();
+            if (availableSpace < estimatedWriteSize)
+                throw new RuntimeException(String.format("Not enough space to write %s to %s (%s available)",
+                                                         FBUtilities.prettyPrintMemory(estimatedWriteSize),
+                                                         d.location,
+                                                         FBUtilities.prettyPrintMemory(availableSpace)));
+            logger.trace("putting compaction results in {}", descriptor.directory);
+            return d;
+        }
+        d = getDirectories().getWriteableLocation(estimatedWriteSize);
+        if (d == null)
+            throw new RuntimeException(String.format("Not enough disk space to store %s",
+                                                     FBUtilities.prettyPrintMemory(estimatedWriteSize)));
+        return d;
+    }
 
-        return directory;
+    public CompactionAwareWriter setRepairedAt(long repairedAt)
+    {
+        this.sstableWriter.setRepairedAt(repairedAt);
+        return this;
     }
 
     protected long getExpectedWriteSize()
diff --git a/src/java/org/apache/cassandra/db/compaction/writers/DefaultCompactionWriter.java b/src/java/org/apache/cassandra/db/compaction/writers/DefaultCompactionWriter.java
index 8b90224..f8ecd87 100644
--- a/src/java/org/apache/cassandra/db/compaction/writers/DefaultCompactionWriter.java
+++ b/src/java/org/apache/cassandra/db/compaction/writers/DefaultCompactionWriter.java
@@ -39,16 +39,24 @@
 public class DefaultCompactionWriter extends CompactionAwareWriter
 {
     protected static final Logger logger = LoggerFactory.getLogger(DefaultCompactionWriter.class);
+    private final int sstableLevel;
 
     public DefaultCompactionWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set<SSTableReader> nonExpiredSSTables)
     {
-        this(cfs, directories, txn, nonExpiredSSTables, false, false);
+        this(cfs, directories, txn, nonExpiredSSTables, false, 0);
+    }
+
+    @Deprecated
+    public DefaultCompactionWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set<SSTableReader> nonExpiredSSTables, boolean offline, boolean keepOriginals, int sstableLevel)
+    {
+        this(cfs, directories, txn, nonExpiredSSTables, keepOriginals, sstableLevel);
     }
 
     @SuppressWarnings("resource")
-    public DefaultCompactionWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set<SSTableReader> nonExpiredSSTables, boolean offline, boolean keepOriginals)
+    public DefaultCompactionWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set<SSTableReader> nonExpiredSSTables, boolean keepOriginals, int sstableLevel)
     {
-        super(cfs, directories, txn, nonExpiredSSTables, offline, keepOriginals);
+        super(cfs, directories, txn, nonExpiredSSTables, keepOriginals);
+        this.sstableLevel = sstableLevel;
     }
 
     @Override
@@ -58,15 +66,16 @@
     }
 
     @Override
-    protected void switchCompactionLocation(Directories.DataDirectory directory)
+    public void switchCompactionLocation(Directories.DataDirectory directory)
     {
         @SuppressWarnings("resource")
         SSTableWriter writer = SSTableWriter.create(Descriptor.fromFilename(cfs.getSSTablePath(getDirectories().getLocationForDisk(directory))),
                                                     estimatedTotalKeys,
                                                     minRepairedAt,
                                                     cfs.metadata,
-                                                    new MetadataCollector(txn.originals(), cfs.metadata.comparator, 0),
+                                                    new MetadataCollector(txn.originals(), cfs.metadata.comparator, sstableLevel),
                                                     SerializationHeader.make(cfs.metadata, nonExpiredSSTables),
+                                                    cfs.indexManager.listIndexes(),
                                                     txn);
         sstableWriter.switchWriter(writer);
     }
diff --git a/src/java/org/apache/cassandra/db/compaction/writers/MajorLeveledCompactionWriter.java b/src/java/org/apache/cassandra/db/compaction/writers/MajorLeveledCompactionWriter.java
index 3eee398..f1326e9 100644
--- a/src/java/org/apache/cassandra/db/compaction/writers/MajorLeveledCompactionWriter.java
+++ b/src/java/org/apache/cassandra/db/compaction/writers/MajorLeveledCompactionWriter.java
@@ -17,12 +17,8 @@
  */
 package org.apache.cassandra.db.compaction.writers;
 
-import java.io.File;
 import java.util.Set;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Directories;
 import org.apache.cassandra.db.RowIndexEntry;
@@ -37,15 +33,15 @@
 
 public class MajorLeveledCompactionWriter extends CompactionAwareWriter
 {
-    private static final Logger logger = LoggerFactory.getLogger(MajorLeveledCompactionWriter.class);
     private final long maxSSTableSize;
-    private final long expectedWriteSize;
-    private final Set<SSTableReader> allSSTables;
     private int currentLevel = 1;
     private long averageEstimatedKeysPerSSTable;
     private long partitionsWritten = 0;
     private long totalWrittenInLevel = 0;
     private int sstablesWritten = 0;
+    private final long keysPerSSTable;
+    private Directories.DataDirectory sstableDirectory;
+    private final int levelFanoutSize;
 
     public MajorLeveledCompactionWriter(ColumnFamilyStore cfs,
                                         Directories directories,
@@ -53,10 +49,10 @@
                                         Set<SSTableReader> nonExpiredSSTables,
                                         long maxSSTableSize)
     {
-        this(cfs, directories, txn, nonExpiredSSTables, maxSSTableSize, false, false);
+        this(cfs, directories, txn, nonExpiredSSTables, maxSSTableSize, false);
     }
 
-    @SuppressWarnings("resource")
+    @Deprecated
     public MajorLeveledCompactionWriter(ColumnFamilyStore cfs,
                                         Directories directories,
                                         LifecycleTransaction txn,
@@ -65,54 +61,66 @@
                                         boolean offline,
                                         boolean keepOriginals)
     {
-        super(cfs, directories, txn, nonExpiredSSTables, offline, keepOriginals);
+        this(cfs, directories, txn, nonExpiredSSTables, maxSSTableSize, keepOriginals);
+    }
+
+    @SuppressWarnings("resource")
+    public MajorLeveledCompactionWriter(ColumnFamilyStore cfs,
+                                        Directories directories,
+                                        LifecycleTransaction txn,
+                                        Set<SSTableReader> nonExpiredSSTables,
+                                        long maxSSTableSize,
+                                        boolean keepOriginals)
+    {
+        super(cfs, directories, txn, nonExpiredSSTables, keepOriginals);
         this.maxSSTableSize = maxSSTableSize;
-        this.allSSTables = txn.originals();
-        expectedWriteSize = Math.min(maxSSTableSize, cfs.getExpectedCompactedFileSize(nonExpiredSSTables, txn.opType()));
+        this.levelFanoutSize = cfs.getLevelFanoutSize();
+        long estimatedSSTables = Math.max(1, SSTableReader.getTotalBytes(nonExpiredSSTables) / maxSSTableSize);
+        keysPerSSTable = estimatedTotalKeys / estimatedSSTables;
     }
 
     @Override
     @SuppressWarnings("resource")
     public boolean realAppend(UnfilteredRowIterator partition)
     {
-        long posBefore = sstableWriter.currentWriter().getOnDiskFilePointer();
         RowIndexEntry rie = sstableWriter.append(partition);
-        totalWrittenInLevel += sstableWriter.currentWriter().getOnDiskFilePointer() - posBefore;
         partitionsWritten++;
-        if (sstableWriter.currentWriter().getOnDiskFilePointer() > maxSSTableSize)
+        long totalWrittenInCurrentWriter = sstableWriter.currentWriter().getEstimatedOnDiskBytesWritten();
+        if (totalWrittenInCurrentWriter > maxSSTableSize)
         {
-            if (totalWrittenInLevel > LeveledManifest.maxBytesForLevel(currentLevel, maxSSTableSize))
+            totalWrittenInLevel += totalWrittenInCurrentWriter;
+            if (totalWrittenInLevel > LeveledManifest.maxBytesForLevel(currentLevel, levelFanoutSize, maxSSTableSize))
             {
                 totalWrittenInLevel = 0;
                 currentLevel++;
             }
-
-            averageEstimatedKeysPerSSTable = Math.round(((double) averageEstimatedKeysPerSSTable * sstablesWritten + partitionsWritten) / (sstablesWritten + 1));
-            switchCompactionLocation(getWriteDirectory(getExpectedWriteSize()));
-            partitionsWritten = 0;
-            sstablesWritten++;
+            switchCompactionLocation(sstableDirectory);
         }
         return rie != null;
 
     }
 
-    public void switchCompactionLocation(Directories.DataDirectory directory)
+    @Override
+    public void switchCompactionLocation(Directories.DataDirectory location)
     {
-        File sstableDirectory = getDirectories().getLocationForDisk(directory);
-        @SuppressWarnings("resource")
-        SSTableWriter writer = SSTableWriter.create(Descriptor.fromFilename(cfs.getSSTablePath(sstableDirectory)),
-                                                    averageEstimatedKeysPerSSTable,
-                                                    minRepairedAt,
-                                                    cfs.metadata,
-                                                    new MetadataCollector(allSSTables, cfs.metadata.comparator, currentLevel),
-                                                    SerializationHeader.make(cfs.metadata, nonExpiredSSTables),
-                                                    txn);
-        sstableWriter.switchWriter(writer);
+        this.sstableDirectory = location;
+        averageEstimatedKeysPerSSTable = Math.round(((double) averageEstimatedKeysPerSSTable * sstablesWritten + partitionsWritten) / (sstablesWritten + 1));
+        sstableWriter.switchWriter(SSTableWriter.create(Descriptor.fromFilename(cfs.getSSTablePath(getDirectories().getLocationForDisk(sstableDirectory))),
+                keysPerSSTable,
+                minRepairedAt,
+                cfs.metadata,
+                new MetadataCollector(txn.originals(), cfs.metadata.comparator, currentLevel),
+                SerializationHeader.make(cfs.metadata, txn.originals()),
+                cfs.indexManager.listIndexes(),
+                txn));
+        partitionsWritten = 0;
+        sstablesWritten = 0;
+
     }
 
     @Override
     protected long getExpectedWriteSize()
     {
-        return expectedWriteSize;
+        return Math.min(maxSSTableSize, super.getExpectedWriteSize());
     }
 }
diff --git a/src/java/org/apache/cassandra/db/compaction/writers/MaxSSTableSizeWriter.java b/src/java/org/apache/cassandra/db/compaction/writers/MaxSSTableSizeWriter.java
index d76381a..36c69b0 100644
--- a/src/java/org/apache/cassandra/db/compaction/writers/MaxSSTableSizeWriter.java
+++ b/src/java/org/apache/cassandra/db/compaction/writers/MaxSSTableSizeWriter.java
@@ -33,11 +33,11 @@
 
 public class MaxSSTableSizeWriter extends CompactionAwareWriter
 {
-    private final long expectedWriteSize;
     private final long maxSSTableSize;
     private final int level;
     private final long estimatedSSTables;
     private final Set<SSTableReader> allSSTables;
+    private Directories.DataDirectory sstableDirectory;
 
     public MaxSSTableSizeWriter(ColumnFamilyStore cfs,
                                 Directories directories,
@@ -46,10 +46,10 @@
                                 long maxSSTableSize,
                                 int level)
     {
-        this(cfs, directories, txn, nonExpiredSSTables, maxSSTableSize, level, false, false);
+        this(cfs, directories, txn, nonExpiredSSTables, maxSSTableSize, level, false);
     }
 
-    @SuppressWarnings("resource")
+    @Deprecated
     public MaxSSTableSizeWriter(ColumnFamilyStore cfs,
                                 Directories directories,
                                 LifecycleTransaction txn,
@@ -59,13 +59,23 @@
                                 boolean offline,
                                 boolean keepOriginals)
     {
-        super(cfs, directories, txn, nonExpiredSSTables, offline, keepOriginals);
+        this(cfs, directories, txn, nonExpiredSSTables, maxSSTableSize, level, keepOriginals);
+    }
+
+    public MaxSSTableSizeWriter(ColumnFamilyStore cfs,
+                                Directories directories,
+                                LifecycleTransaction txn,
+                                Set<SSTableReader> nonExpiredSSTables,
+                                long maxSSTableSize,
+                                int level,
+                                boolean keepOriginals)
+    {
+        super(cfs, directories, txn, nonExpiredSSTables, keepOriginals);
         this.allSSTables = txn.originals();
         this.level = level;
         this.maxSSTableSize = maxSSTableSize;
 
         long totalSize = getTotalWriteSize(nonExpiredSSTables, estimatedTotalKeys, cfs, txn.opType());
-        expectedWriteSize = Math.min(maxSSTableSize, totalSize);
         estimatedSSTables = Math.max(1, totalSize / maxSSTableSize);
     }
 
@@ -79,36 +89,40 @@
             estimatedKeysBeforeCompaction += sstable.estimatedKeys();
         estimatedKeysBeforeCompaction = Math.max(1, estimatedKeysBeforeCompaction);
         double estimatedCompactionRatio = (double) estimatedTotalKeys / estimatedKeysBeforeCompaction;
+
         return Math.round(estimatedCompactionRatio * cfs.getExpectedCompactedFileSize(nonExpiredSSTables, compactionType));
     }
 
-    @Override
-    public boolean realAppend(UnfilteredRowIterator partition)
+    protected boolean realAppend(UnfilteredRowIterator partition)
     {
         RowIndexEntry rie = sstableWriter.append(partition);
-        if (sstableWriter.currentWriter().getOnDiskFilePointer() > maxSSTableSize)
-            switchCompactionLocation(getWriteDirectory(getExpectedWriteSize()));
+        if (sstableWriter.currentWriter().getEstimatedOnDiskBytesWritten() > maxSSTableSize)
+        {
+            switchCompactionLocation(sstableDirectory);
+        }
         return rie != null;
     }
 
+    @Override
     public void switchCompactionLocation(Directories.DataDirectory location)
     {
+        sstableDirectory = location;
         @SuppressWarnings("resource")
-        SSTableWriter writer = SSTableWriter.create(Descriptor.fromFilename(cfs.getSSTablePath(getDirectories().getLocationForDisk(location))),
+        SSTableWriter writer = SSTableWriter.create(Descriptor.fromFilename(cfs.getSSTablePath(getDirectories().getLocationForDisk(sstableDirectory))),
                                                     estimatedTotalKeys / estimatedSSTables,
                                                     minRepairedAt,
                                                     cfs.metadata,
                                                     new MetadataCollector(allSSTables, cfs.metadata.comparator, level),
                                                     SerializationHeader.make(cfs.metadata, nonExpiredSSTables),
+                                                    cfs.indexManager.listIndexes(),
                                                     txn);
 
         sstableWriter.switchWriter(writer);
-
     }
 
     @Override
     protected long getExpectedWriteSize()
     {
-        return expectedWriteSize;
+        return Math.min(maxSSTableSize, super.getExpectedWriteSize());
     }
 }
diff --git a/src/java/org/apache/cassandra/db/compaction/writers/SplittingSizeTieredCompactionWriter.java b/src/java/org/apache/cassandra/db/compaction/writers/SplittingSizeTieredCompactionWriter.java
index 77672d8..b4d7097 100644
--- a/src/java/org/apache/cassandra/db/compaction/writers/SplittingSizeTieredCompactionWriter.java
+++ b/src/java/org/apache/cassandra/db/compaction/writers/SplittingSizeTieredCompactionWriter.java
@@ -17,7 +17,6 @@
  */
 package org.apache.cassandra.db.compaction.writers;
 
-import java.io.File;
 import java.util.Arrays;
 import java.util.Set;
 
@@ -51,13 +50,13 @@
     private final Set<SSTableReader> allSSTables;
     private long currentBytesToWrite;
     private int currentRatioIndex = 0;
+    private Directories.DataDirectory location;
 
     public SplittingSizeTieredCompactionWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set<SSTableReader> nonExpiredSSTables)
     {
         this(cfs, directories, txn, nonExpiredSSTables, DEFAULT_SMALLEST_SSTABLE_BYTES);
     }
 
-    @SuppressWarnings("resource")
     public SplittingSizeTieredCompactionWriter(ColumnFamilyStore cfs, Directories directories, LifecycleTransaction txn, Set<SSTableReader> nonExpiredSSTables, long smallestSSTable)
     {
         super(cfs, directories, txn, nonExpiredSSTables, false, false);
@@ -82,27 +81,27 @@
             }
         }
         ratios = Arrays.copyOfRange(potentialRatios, 0, noPointIndex);
-        long currentPartitionsToWrite = Math.round(estimatedTotalKeys * ratios[currentRatioIndex]);
         currentBytesToWrite = Math.round(totalSize * ratios[currentRatioIndex]);
-        switchCompactionLocation(getWriteDirectory(currentBytesToWrite));
-        logger.trace("Ratios={}, expectedKeys = {}, totalSize = {}, currentPartitionsToWrite = {}, currentBytesToWrite = {}", ratios, estimatedTotalKeys, totalSize, currentPartitionsToWrite, currentBytesToWrite);
     }
 
     @Override
     public boolean realAppend(UnfilteredRowIterator partition)
     {
         RowIndexEntry rie = sstableWriter.append(partition);
-        if (sstableWriter.currentWriter().getOnDiskFilePointer() > currentBytesToWrite && currentRatioIndex < ratios.length - 1) // if we underestimate how many keys we have, the last sstable might get more than we expect
+        if (sstableWriter.currentWriter().getEstimatedOnDiskBytesWritten() > currentBytesToWrite && currentRatioIndex < ratios.length - 1) // if we underestimate how many keys we have, the last sstable might get more than we expect
         {
             currentRatioIndex++;
             currentBytesToWrite = getExpectedWriteSize();
-            switchCompactionLocation(getWriteDirectory(currentBytesToWrite));
+            switchCompactionLocation(location);
+            logger.debug("Switching writer, currentBytesToWrite = {}", currentBytesToWrite);
         }
         return rie != null;
     }
 
+    @Override
     public void switchCompactionLocation(Directories.DataDirectory location)
     {
+        this.location = location;
         long currentPartitionsToWrite = Math.round(ratios[currentRatioIndex] * estimatedTotalKeys);
         @SuppressWarnings("resource")
         SSTableWriter writer = SSTableWriter.create(Descriptor.fromFilename(cfs.getSSTablePath(getDirectories().getLocationForDisk(location))),
@@ -111,12 +110,13 @@
                                                     cfs.metadata,
                                                     new MetadataCollector(allSSTables, cfs.metadata.comparator, 0),
                                                     SerializationHeader.make(cfs.metadata, nonExpiredSSTables),
+                                                    cfs.indexManager.listIndexes(),
                                                     txn);
         logger.trace("Switching writer, currentPartitionsToWrite = {}", currentPartitionsToWrite);
         sstableWriter.switchWriter(writer);
-
     }
 
+    @Override
     protected long getExpectedWriteSize()
     {
         return Math.round(totalSize * ratios[currentRatioIndex]);
diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java
index e3f824f..f184035 100644
--- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java
@@ -114,11 +114,10 @@
     /**
      * Returns an iterator that only returns the rows of the provided iterator that this filter selects.
      * <p>
-     * This method is the "dumb" counterpart to {@link #filter(SliceableUnfilteredRowIterator)} in that it has no way to quickly get
+     * This method is the "dumb" counterpart to {@link #getSlices(CFMetaData)} in that it has no way to quickly get
      * to what is actually selected, so it simply iterate over it all and filters out what shouldn't be returned. This should
-     * be avoided in general, we should make sure to have {@code SliceableUnfilteredRowIterator} when we have filtering to do, but this
-     * currently only used in {@link SinglePartitionReadCommand#getThroughCache} when we know this won't be a performance problem.
-     * Another difference with {@link #filter(SliceableUnfilteredRowIterator)} is that this method also filter the queried
+     * be avoided in general.
+     * Another difference with {@link #getSlices(CFMetaData)} is that this method also filter the queried
      * columns in the returned result, while the former assumes that the provided iterator has already done it.
      *
      * @param columnFilter the columns to include in the rows of the result iterator.
@@ -128,14 +127,7 @@
      */
     public UnfilteredRowIterator filterNotIndexed(ColumnFilter columnFilter, UnfilteredRowIterator iterator);
 
-    /**
-     * Returns an iterator that only returns the rows of the provided sliceable iterator that this filter selects.
-     *
-     * @param iterator the sliceable iterator for which we should filter rows.
-     *
-     * @return an iterator that only returns the rows (or rather unfiltered) from {@code iterator} that are selected by this filter.
-     */
-    public UnfilteredRowIterator filter(SliceableUnfilteredRowIterator iterator);
+    public Slices getSlices(CFMetaData metadata);
 
     /**
      * Given a partition, returns a row iterator for the rows of this partition that are selected by this filter.
@@ -145,8 +137,6 @@
      *
      * @return a unfiltered row iterator returning those rows (or rather Unfiltered) from {@code partition} that are selected by this filter.
      */
-    // TODO: we could get rid of that if Partition was exposing a SliceableUnfilteredRowIterator (instead of the two searchIterator() and
-    // unfilteredIterator() methods). However, for AtomicBtreePartition this would require changes to Btree so we'll leave that for later.
     public UnfilteredRowIterator getUnfilteredRowIterator(ColumnFilter columnFilter, Partition partition);
 
     /**
diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
index f4859cd..6c7e14b 100644
--- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
@@ -128,52 +128,12 @@
         return Transformation.apply(iterator, new FilterNotIndexed());
     }
 
-    public UnfilteredRowIterator filter(final SliceableUnfilteredRowIterator iter)
+    public Slices getSlices(CFMetaData metadata)
     {
-        // Please note that this method assumes that rows from 'iter' already have their columns filtered, i.e. that
-        // they only include columns that we select.
-        return new WrappingUnfilteredRowIterator(iter)
-        {
-            private final Iterator<Clustering> clusteringIter = clusteringsInQueryOrder.iterator();
-            private Iterator<Unfiltered> currentClustering;
-            private Unfiltered next;
-
-            @Override
-            public boolean hasNext()
-            {
-                if (next != null)
-                    return true;
-
-                if (currentClustering != null && currentClustering.hasNext())
-                {
-                    next = currentClustering.next();
-                    return true;
-                }
-
-                while (clusteringIter.hasNext())
-                {
-                    Clustering nextClustering = clusteringIter.next();
-                    currentClustering = iter.slice(Slice.make(nextClustering));
-                    if (currentClustering.hasNext())
-                    {
-                        next = currentClustering.next();
-                        return true;
-                    }
-                }
-                return false;
-            }
-
-            @Override
-            public Unfiltered next()
-            {
-                if (next == null && !hasNext())
-                    throw new NoSuchElementException();
-
-                Unfiltered toReturn = next;
-                next = null;
-                return toReturn;
-            }
-        };
+        Slices.Builder builder = new Slices.Builder(metadata.comparator, clusteringsInQueryOrder.size());
+        for (Clustering clustering : clusteringsInQueryOrder)
+            builder.add(Slice.make(clustering));
+        return builder.build();
     }
 
     public UnfilteredRowIterator getUnfilteredRowIterator(final ColumnFilter columnFilter, final Partition partition)
@@ -231,7 +191,7 @@
 
     public String toCQLString(CFMetaData metadata)
     {
-        if (clusterings.isEmpty())
+        if (metadata.clusteringColumns().isEmpty() || clusterings.isEmpty())
             return "";
 
         StringBuilder sb = new StringBuilder();
@@ -239,7 +199,7 @@
         sb.append(clusterings.size() == 1 ? " = " : " IN (");
         int i = 0;
         for (Clustering clustering : clusterings)
-            sb.append(i++ == 0 ? "" : ", ").append("(").append(clustering.toCQLString(metadata)).append(")");
+            sb.append(i++ == 0 ? "" : ", ").append('(').append(clustering.toCQLString(metadata)).append(')');
         sb.append(clusterings.size() == 1 ? "" : ")");
 
         appendOrderByToCQLString(metadata, sb);
diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java
index 7a174ee..02a44d7 100644
--- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java
@@ -94,11 +94,6 @@
         // the range extend) and it's harmless to leave them.
         class FilterNotIndexed extends Transformation
         {
-            public boolean isDoneForPartition()
-            {
-                return tester.isDone();
-            }
-
             @Override
             public Row applyToRow(Row row)
             {
@@ -114,11 +109,9 @@
         return Transformation.apply(iterator, new FilterNotIndexed());
     }
 
-    public UnfilteredRowIterator filter(SliceableUnfilteredRowIterator iterator)
+    public Slices getSlices(CFMetaData metadata)
     {
-        // Please note that this method assumes that rows from 'iter' already have their columns filtered, i.e. that
-        // they only include columns that we select.
-        return slices.makeSliceIterator(iterator);
+        return slices;
     }
 
     public UnfilteredRowIterator getUnfilteredRowIterator(ColumnFilter columnFilter, Partition partition)
diff --git a/src/java/org/apache/cassandra/db/filter/ColumnFilter.java b/src/java/org/apache/cassandra/db/filter/ColumnFilter.java
index c658c12..d7b4c81 100644
--- a/src/java/org/apache/cassandra/db/filter/ColumnFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ColumnFilter.java
@@ -23,11 +23,15 @@
 import com.google.common.collect.SortedSetMultimap;
 import com.google.common.collect.TreeMultimap;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.CellPath;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.gms.Gossiper;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.net.MessagingService;
@@ -36,33 +40,43 @@
  * Represents which (non-PK) columns (and optionally which sub-part of a column for complex columns) are selected
  * by a query.
  *
- * In practice, this class cover 2 main cases:
- *   1) most user queries have to internally query all columns, because the CQL semantic requires us to know if
- *      a row is live or not even if it has no values for the columns requested by the user (see #6588for more
- *      details). However, while we need to know for columns if it has live values, we can actually save from
- *      sending the values for those columns that will not be returned to the user.
- *   2) for some internal queries (and for queries using #6588 if we introduce it), we're actually fine only
- *      actually querying some of the columns.
+ * We distinguish 2 sets of columns in practice: the _fetched_ columns, which are the columns that we (may, see
+ * below) need to fetch internally, and the _queried_ columns, which are the columns that the user has selected
+ * in its request.
  *
- * For complex columns, this class allows to be more fine grained than the column by only selection some of the
- * cells of the complex column (either individual cell by path name, or some slice).
+ * The reason for distinguishing those 2 sets is that due to the CQL semantic (see #6588 for more details), we
+ * often need to internally fetch all columns for the queried table, but can still do some optimizations for those
+ * columns that are not directly queried by the user (see #10657 for more details).
+ *
+ * Note that in practice:
+ *   - the _queried_ columns set is always included in the _fetched_ one.
+ *   - whenever those sets are different, we know the _fetched_ set contains all columns for the table, so we
+ *     don't have to record this set, we just keep a pointer to the table metadata. The only set we concretely
+ *     store is thus the _queried_ one.
+ *   - in the special case of a {@code SELECT *} query, we want to query all columns, and _fetched_ == _queried.
+ *     As this is a common case, we special case it by keeping the _queried_ set {@code null} (and we retrieve
+ *     the columns through the metadata pointer).
+ *
+ * For complex columns, this class optionally allows to specify a subset of the cells to query for each column.
+ * We can either select individual cells by path name, or a slice of them. Note that this is a sub-selection of
+ * _queried_ cells, so if _fetched_ != _queried_, then the cell selected by this sub-selection are considered
+ * queried and the other ones are considered fetched (and if a column has some sub-selection, it must be a queried
+ * column, which is actually enforced by the Builder below).
  */
 public class ColumnFilter
 {
+    private final static Logger logger = LoggerFactory.getLogger(ColumnFilter.class);
+
     public static final Serializer serializer = new Serializer();
 
-    // Distinguish between the 2 cases described above: if 'isFetchAll' is true, then all columns will be retrieved
-    // by the query, but the values for column/cells not selected by 'queried' and 'subSelections' will be skipped.
-    // Otherwise, only the column/cells returned by 'queried' and 'subSelections' will be returned at all.
+    // True if _fetched_ is all the columns, in which case metadata must not be null. If false,
+    // then _fetched_ == _queried_ and we only store _queried_.
     private final boolean isFetchAll;
 
-    private final PartitionColumns queried; // can be null if isFetchAll and we don't want to skip any value
     private final PartitionColumns fetched;
+    private final PartitionColumns queried; // can be null if isFetchAll and _fetched_ == _queried_
     private final SortedSetMultimap<ColumnIdentifier, ColumnSubselection> subSelections; // can be null
 
-    /**
-     * Used on replica for deserialisation
-     */
     private ColumnFilter(boolean isFetchAll,
                          PartitionColumns fetched,
                          PartitionColumns queried,
@@ -77,7 +91,7 @@
     }
 
     /**
-     * A selection that includes all columns (and their values).
+     * A filter that includes all columns for the provided table.
      */
     public static ColumnFilter all(CFMetaData metadata)
     {
@@ -85,7 +99,7 @@
     }
 
     /**
-     * A selection that only fetch the provided columns.
+     * A filter that only fetches/queries the provided columns.
      * <p>
      * Note that this shouldn't be used for CQL queries in general as all columns should be queried to
      * preserve CQL semantic (see class javadoc). This is ok for some internal queries however (and
@@ -102,48 +116,113 @@
      */
     public static ColumnFilter selection(CFMetaData metadata, PartitionColumns queried)
     {
-        return new ColumnFilter(true, metadata.partitionColumns(), queried, null);
+        // When fetchAll is enabled on pre CASSANDRA-10657 (3.4-), queried columns are not considered at all, and it
+        // is assumed that all columns are queried. CASSANDRA-10657 (3.4+) brings back skipping values of columns
+        // which are not in queried set when fetchAll is enabled. That makes exactly the same filter being
+        // interpreted in a different way on 3.4- and 3.4+.
+        //
+        // Moreover, there is no way to convert the filter with fetchAll and queried != null so that it is
+        // interpreted the same way on 3.4- because that Cassandra version does not support such filtering.
+        //
+        // In order to avoid inconsitencies in data read by 3.4- and 3.4+ we need to avoid creation of incompatible
+        // filters when the cluster contains 3.4- nodes. We do that by forcibly setting queried to null.
+        //
+        // see CASSANDRA-10657, CASSANDRA-15833, CASSANDRA-16415
+        return new ColumnFilter(true, metadata.partitionColumns(), Gossiper.instance.isAnyNodeOn30() ? null : queried, null);
     }
 
     /**
-     * The columns that needs to be fetched internally for this selection.
-     * <p>
-     * This is the columns that must be present in the internal rows returned by queries using this selection,
-     * not the columns that are actually queried by the user (see the class javadoc for details).
+     * The columns that needs to be fetched internally for this filter.
      *
-     * @return the column to fetch for this selection.
+     * @return the columns to fetch for this filter.
      */
     public PartitionColumns fetchedColumns()
     {
         return fetched;
     }
 
-    public boolean includesAllColumns()
+    /**
+     * The columns actually queried by the user.
+     * <p>
+     * Note that this is in general not all the columns that are fetched internally (see {@link #fetchedColumns}).
+     */
+    public PartitionColumns queriedColumns()
+    {
+        return queried == null ? fetched : queried;
+    }
+
+    public boolean fetchesAllColumns()
     {
         return isFetchAll;
     }
 
     /**
-     * Whether the provided column is selected by this selection.
+     * Whether _fetched_ == _queried_ for this filter, and so if the {@code isQueried()} methods
+     * can return {@code false} for some column/cell.
      */
-    public boolean includes(ColumnDefinition column)
+    public boolean allFetchedColumnsAreQueried()
+    {
+        return !isFetchAll || (queried == null && subSelections == null);
+    }
+
+    /**
+     * Whether the provided column is fetched by this filter.
+     */
+    public boolean fetches(ColumnDefinition column)
     {
         return isFetchAll || queried.contains(column);
     }
 
     /**
-     * Whether we can skip the value for the provided selected column.
+     * Whether the provided column, which is assumed to be _fetched_ by this filter (so the caller must guarantee
+     * that {@code fetches(column) == true}, is also _queried_ by the user.
+     *
+     * !WARNING! please be sure to understand the difference between _fetched_ and _queried_
+     * columns that this class made before using this method. If unsure, you probably want
+     * to use the {@link #fetches} method.
      */
-    @SuppressWarnings("unused")
-    public boolean canSkipValue(ColumnDefinition column)
+    public boolean fetchedColumnIsQueried(ColumnDefinition column)
     {
-        // We don't use that currently, see #10655 for more details.
+        return !isFetchAll || queried == null || queried.contains(column);
+    }
+
+    /**
+     * Whether the provided complex cell (identified by its column and path), which is assumed to be _fetched_ by
+     * this filter, is also _queried_ by the user.
+     *
+     * !WARNING! please be sure to understand the difference between _fetched_ and _queried_
+     * columns that this class made before using this method. If unsure, you probably want
+     * to use the {@link #fetches} method.
+     */
+    public boolean fetchedCellIsQueried(ColumnDefinition column, CellPath path)
+    {
+        assert path != null;
+
+        // first verify that the column to which the cell belongs is queried
+        if (!fetchedColumnIsQueried(column))
+            return false;
+
+        if (subSelections == null)
+            return true;
+
+        SortedSet<ColumnSubselection> s = subSelections.get(column.name);
+        // No subsection for this column means everything is queried
+        if (s.isEmpty())
+            return true;
+
+        for (ColumnSubselection subSel : s)
+            if (subSel.compareInclusionOf(path) == 0)
+                return true;
+
         return false;
     }
 
     /**
      * Creates a new {@code Tester} to efficiently test the inclusion of cells of complex column
      * {@code column}.
+     *
+     * @return the created tester or {@code null} if all the cells from the provided column
+     * are queried.
      */
     public Tester newTester(ColumnDefinition column)
     {
@@ -158,8 +237,8 @@
     }
 
     /**
-     * Returns a {@code ColumnFilter}} builder that includes all columns (so the selections
-     * added to the builder are the columns/cells for which we shouldn't skip the values).
+     * Returns a {@code ColumnFilter}} builder that fetches all columns (and queries the columns
+     * added to the builder, or everything if no column is added).
      */
     public static Builder allColumnsBuilder(CFMetaData metadata)
     {
@@ -167,8 +246,7 @@
     }
 
     /**
-     * Returns a {@code ColumnFilter}} builder that includes only the columns/cells
-     * added to the builder.
+     * Returns a {@code ColumnFilter} builder that only fetches the columns/cells added to the builder.
      */
     public static Builder selectionBuilder()
     {
@@ -187,17 +265,20 @@
             this.iterator = iterator;
         }
 
-        public boolean includes(CellPath path)
+        public boolean fetches(CellPath path)
         {
-            return isFetchAll || includedBySubselection(path);
+            return isFetchAll || hasSubselection(path);
         }
 
-        public boolean canSkipValue(CellPath path)
+        /**
+         * Must only be called if {@code fetches(path) == true}.
+         */
+        public boolean fetchedCellIsQueried(CellPath path)
         {
-            return isFetchAll && !includedBySubselection(path);
+            return !isFetchAll || hasSubselection(path);
         }
 
-        private boolean includedBySubselection(CellPath path)
+        private boolean hasSubselection(CellPath path)
         {
             while (current != null || iterator.hasNext())
             {
@@ -217,10 +298,22 @@
         }
     }
 
+    /**
+     * A builder for a {@code ColumnFilter} object.
+     *
+     * Note that the columns added to this build are the _queried_ column. Whether or not all columns
+     * are _fetched_ depends on which constructor you've used to obtained this builder, allColumnsBuilder (all
+     * columns are fetched) or selectionBuilder (only the queried columns are fetched).
+     *
+     * Note that for a allColumnsBuilder, if no queried columns are added, this is interpreted as querying
+     * all columns, not querying none (but if you know you want to query all columns, prefer
+     * {@link ColumnFilter#all(CFMetaData)}. For selectionBuilder, adding no queried columns means no column will be
+     * fetched (so the builder will return {@code PartitionColumns.NONE}).
+     */
     public static class Builder
     {
-        private final CFMetaData metadata;
-        private PartitionColumns.Builder selection;
+        private final CFMetaData metadata; // null if we don't fetch all columns
+        private PartitionColumns.Builder queriedBuilder;
         private List<ColumnSubselection> subSelections;
 
         private Builder(CFMetaData metadata)
@@ -230,17 +323,17 @@
 
         public Builder add(ColumnDefinition c)
         {
-            if (selection == null)
-                selection = PartitionColumns.builder();
-            selection.add(c);
+            if (queriedBuilder == null)
+                queriedBuilder = PartitionColumns.builder();
+            queriedBuilder.add(c);
             return this;
         }
 
         public Builder addAll(Iterable<ColumnDefinition> columns)
         {
-            if (selection == null)
-                selection = PartitionColumns.builder();
-            selection.addAll(columns);
+            if (queriedBuilder == null)
+                queriedBuilder = PartitionColumns.builder();
+            queriedBuilder.addAll(columns);
             return this;
         }
 
@@ -267,11 +360,11 @@
         {
             boolean isFetchAll = metadata != null;
 
-            PartitionColumns selectedColumns = selection == null ? null : selection.build();
-            // It's only ok to have queried == null in ColumnFilter if isFetchAll. So deal with the case of a "selection" builder
+            PartitionColumns queried = queriedBuilder == null ? null : queriedBuilder.build();
+            // It's only ok to have queried == null in ColumnFilter if isFetchAll. So deal with the case of a selectionBuilder
             // with nothing selected (we can at least happen on some backward compatible queries - CASSANDRA-10471).
-            if (!isFetchAll && selectedColumns == null)
-                selectedColumns = PartitionColumns.NONE;
+            if (!isFetchAll && queried == null)
+                queried = PartitionColumns.NONE;
 
             SortedSetMultimap<ColumnIdentifier, ColumnSubselection> s = null;
             if (subSelections != null)
@@ -281,7 +374,14 @@
                     s.put(subSelection.column().name, subSelection);
             }
 
-            return new ColumnFilter(isFetchAll, isFetchAll ? metadata.partitionColumns() : selectedColumns, selectedColumns, s);
+            // See the comment in {@link ColumnFilter#selection(CFMetaData, PartitionColumns)}
+            if (isFetchAll && queried != null && Gossiper.instance.isAnyNodeOn30())
+            {
+                logger.trace("Column filter will be automatically converted to query all columns because 3.0 nodes are present in the cluster");
+                queried = null;
+            }
+
+            return new ColumnFilter(isFetchAll, isFetchAll ? metadata.partitionColumns() : null, queried, s);
         }
     }
 
@@ -300,19 +400,22 @@
                Objects.equals(otherCf.fetched, this.fetched) &&
                Objects.equals(otherCf.queried, this.queried) &&
                Objects.equals(otherCf.subSelections, this.subSelections);
-
     }
 
     @Override
     public String toString()
     {
-        if (isFetchAll)
+        if (isFetchAll && queried == null)
             return "*/*";
 
-        if (queried.isEmpty())
-            return "[]";
+        String prefix = "";
+        if (isFetchAll)
+            prefix = "*/";
 
-        return toString(false);
+        if (queried.isEmpty())
+            return prefix + "[]";
+
+        return prefix + toString(false);
     }
 
     public String toCQLString()
@@ -348,13 +451,13 @@
     public static class Serializer
     {
         private static final int IS_FETCH_ALL_MASK       = 0x01;
-        private static final int HAS_SELECTION_MASK      = 0x02;
+        private static final int HAS_QUERIED_MASK      = 0x02;
         private static final int HAS_SUB_SELECTIONS_MASK = 0x04;
 
         private static int makeHeaderByte(ColumnFilter selection)
         {
             return (selection.isFetchAll ? IS_FETCH_ALL_MASK : 0)
-                 | (selection.queried != null ? HAS_SELECTION_MASK : 0)
+                 | (selection.queried != null ? HAS_QUERIED_MASK : 0)
                  | (selection.subSelections != null ? HAS_SUB_SELECTIONS_MASK : 0);
         }
 
@@ -386,11 +489,11 @@
         {
             int header = in.readUnsignedByte();
             boolean isFetchAll = (header & IS_FETCH_ALL_MASK) != 0;
-            boolean hasSelection = (header & HAS_SELECTION_MASK) != 0;
+            boolean hasQueried = (header & HAS_QUERIED_MASK) != 0;
             boolean hasSubSelections = (header & HAS_SUB_SELECTIONS_MASK) != 0;
 
             PartitionColumns fetched = null;
-            PartitionColumns selection = null;
+            PartitionColumns queried = null;
 
             if (isFetchAll)
             {
@@ -406,11 +509,11 @@
                 }
             }
 
-            if (hasSelection)
+            if (hasQueried)
             {
                 Columns statics = Columns.serializer.deserialize(in, metadata);
                 Columns regulars = Columns.serializer.deserialize(in, metadata);
-                selection = new PartitionColumns(statics, regulars);
+                queried = new PartitionColumns(statics, regulars);
             }
 
             SortedSetMultimap<ColumnIdentifier, ColumnSubselection> subSelections = null;
@@ -425,7 +528,18 @@
                 }
             }
 
-            return new ColumnFilter(isFetchAll, fetched, selection, subSelections);
+            // Since nodes with and without CASSANDRA-10657 are not distinguishable by messaging version, we need to
+            // check whether there are any nodes running pre CASSANDRA-10657 Cassandra and apply conversion to the
+            // column filter so that it is interpreted in the same way as on the nodes without CASSANDRA-10657.
+            //
+            // See the comment in {@link ColumnFilter#selection(CFMetaData, PartitionColumns)}
+            if (isFetchAll && queried != null && Gossiper.instance.isAnyNodeOn30())
+            {
+                logger.trace("Deserialized column filter will be automatically converted to query all columns because 3.0 nodes are present in the cluster");
+                queried = null;
+            }
+
+            return new ColumnFilter(isFetchAll, fetched, queried, subSelections);
         }
 
         public long serializedSize(ColumnFilter selection, int version)
diff --git a/src/java/org/apache/cassandra/db/filter/DataLimits.java b/src/java/org/apache/cassandra/db/filter/DataLimits.java
index fa9d47a..c4118cd 100644
--- a/src/java/org/apache/cassandra/db/filter/DataLimits.java
+++ b/src/java/org/apache/cassandra/db/filter/DataLimits.java
@@ -22,6 +22,9 @@
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.aggregation.GroupMaker;
+import org.apache.cassandra.db.aggregation.GroupingState;
+import org.apache.cassandra.db.aggregation.AggregationSpecification;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.partitions.*;
 import org.apache.cassandra.db.transform.BasePartitions;
@@ -79,11 +82,11 @@
     // partition (see SelectStatement.makeFilter). So an "unbounded" distinct is still actually doing some filtering.
     public static final DataLimits DISTINCT_NONE = new CQLLimits(NO_LIMIT, 1, true);
 
-    public enum Kind { CQL_LIMIT, CQL_PAGING_LIMIT, THRIFT_LIMIT, SUPER_COLUMN_COUNTING_LIMIT }
+    public enum Kind { CQL_LIMIT, CQL_PAGING_LIMIT, THRIFT_LIMIT, SUPER_COLUMN_COUNTING_LIMIT, CQL_GROUP_BY_LIMIT, CQL_GROUP_BY_PAGING_LIMIT }
 
     public static DataLimits cqlLimits(int cqlRowLimit)
     {
-        return new CQLLimits(cqlRowLimit);
+        return cqlRowLimit == NO_LIMIT ? NONE : new CQLLimits(cqlRowLimit);
     }
 
     // mixed mode partition range scans on compact storage tables without clustering columns coordinated by 2.x are
@@ -107,7 +110,24 @@
 
     public static DataLimits cqlLimits(int cqlRowLimit, int perPartitionLimit)
     {
-        return new CQLLimits(cqlRowLimit, perPartitionLimit);
+        return cqlRowLimit == NO_LIMIT && perPartitionLimit == NO_LIMIT
+             ? NONE
+             : new CQLLimits(cqlRowLimit, perPartitionLimit);
+    }
+
+    private static DataLimits cqlLimits(int cqlRowLimit, int perPartitionLimit, boolean isDistinct)
+    {
+        return cqlRowLimit == NO_LIMIT && perPartitionLimit == NO_LIMIT && !isDistinct
+             ? NONE
+             : new CQLLimits(cqlRowLimit, perPartitionLimit, isDistinct);
+    }
+
+    public static DataLimits groupByLimits(int groupLimit,
+                                           int groupPerPartitionLimit,
+                                           int rowLimit,
+                                           AggregationSpecification groupBySpec)
+    {
+        return new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, rowLimit, groupBySpec);
     }
 
     public static DataLimits distinctLimits(int cqlRowLimit)
@@ -130,11 +150,32 @@
     public abstract boolean isUnlimited();
     public abstract boolean isDistinct();
 
+    public boolean isGroupByLimit()
+    {
+        return false;
+    }
+
+    public boolean isExhausted(Counter counter)
+    {
+        return counter.counted() < count();
+    }
+
     public abstract DataLimits forPaging(int pageSize);
     public abstract DataLimits forPaging(int pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining);
 
     public abstract DataLimits forShortReadRetry(int toFetch);
 
+    /**
+     * Creates a <code>DataLimits</code> instance to be used for paginating internally GROUP BY queries.
+     *
+     * @param state the <code>GroupMaker</code> state
+     * @return a <code>DataLimits</code> instance to be used for paginating internally GROUP BY queries
+     */
+    public DataLimits forGroupByInternalPaging(GroupingState state)
+    {
+        throw new UnsupportedOperationException();
+    }
+
     public abstract boolean hasEnoughLiveData(CachedPartition cached,
                                               int nowInSec,
                                               boolean countPartitionsWithOnlyStaticData,
@@ -170,6 +211,12 @@
 
     public abstract int perPartitionCount();
 
+    /**
+     * Returns equivalent limits but where any internal state kept to track where we are of paging and/or grouping is
+     * discarded.
+     */
+    public abstract DataLimits withoutState();
+
     public UnfilteredPartitionIterator filter(UnfilteredPartitionIterator iter,
                                               int nowInSec,
                                               boolean countPartitionsWithOnlyStaticData)
@@ -205,8 +252,19 @@
 
     public static abstract class Counter extends StoppingTransformation<BaseRowIterator<?>>
     {
+        protected final int nowInSec;
+        protected final boolean assumeLiveData;
+        private final boolean enforceStrictLiveness;
+
         // false means we do not propagate our stop signals onto the iterator, we only count
-        private boolean enforceLimits = true;
+        protected boolean enforceLimits = true;
+
+        protected Counter(int nowInSec, boolean assumeLiveData, boolean enforceStrictLiveness)
+        {
+            this.nowInSec = nowInSec;
+            this.assumeLiveData = assumeLiveData;
+            this.enforceStrictLiveness = enforceStrictLiveness;
+        }
 
         public Counter onlyCount()
         {
@@ -242,11 +300,31 @@
          * @return the number of results counted.
          */
         public abstract int counted();
+
         public abstract int countedInCurrentPartition();
 
+        /**
+         * The number of rows counted.
+         *
+         * @return the number of rows counted.
+         */
+        public abstract int rowsCounted();
+
+        /**
+         * The number of rows counted in the current partition.
+         *
+         * @return the number of rows counted in the current partition.
+         */
+        public abstract int rowsCountedInCurrentPartition();
+
         public abstract boolean isDone();
         public abstract boolean isDoneForPartition();
 
+        protected boolean isLive(Row row)
+        {
+            return assumeLiveData || row.hasLiveData(nowInSec, enforceStrictLiveness);
+        }
+
         @Override
         protected BaseRowIterator<?> applyToPartition(BaseRowIterator<?> partition)
         {
@@ -275,6 +353,12 @@
             if (isDoneForPartition())
                 stopInPartition();
         }
+
+        @Override
+        public void onClose()
+        {
+            super.onClose();
+        }
     }
 
     /**
@@ -384,6 +468,11 @@
             return perPartitionLimit;
         }
 
+        public DataLimits withoutState()
+        {
+            return this;
+        }
+
         public float estimateTotalResults(ColumnFamilyStore cfs)
         {
             // TODO: we should start storing stats on the number of rows (instead of the number of cells, which
@@ -394,38 +483,32 @@
 
         protected class CQLCounter extends Counter
         {
-            protected final int nowInSec;
-            protected final boolean assumeLiveData;
+            protected int rowsCounted;
+            protected int rowsInCurrentPartition;
             protected final boolean countPartitionsWithOnlyStaticData;
 
-            protected int rowCounted;
-            protected int rowInCurrentPartition;
-
             protected boolean hasLiveStaticRow;
-            private final boolean enforceStrictLiveness;
 
             public CQLCounter(int nowInSec,
                               boolean assumeLiveData,
                               boolean countPartitionsWithOnlyStaticData,
                               boolean enforceStrictLiveness)
             {
-                this.nowInSec = nowInSec;
-                this.assumeLiveData = assumeLiveData;
+                super(nowInSec, assumeLiveData, enforceStrictLiveness);
                 this.countPartitionsWithOnlyStaticData = countPartitionsWithOnlyStaticData;
-                this.enforceStrictLiveness = enforceStrictLiveness;
             }
 
             @Override
             public void applyToPartition(DecoratedKey partitionKey, Row staticRow)
             {
-                rowInCurrentPartition = 0;
-                hasLiveStaticRow = !staticRow.isEmpty() && (assumeLiveData || staticRow.hasLiveData(nowInSec, enforceStrictLiveness));
+                rowsInCurrentPartition = 0;
+                hasLiveStaticRow = !staticRow.isEmpty() && isLive(staticRow);
             }
 
             @Override
             public Row applyToRow(Row row)
             {
-                if (assumeLiveData || row.hasLiveData(nowInSec, enforceStrictLiveness))
+                if (isLive(row))
                     incrementRowCount();
                 return row;
             }
@@ -436,37 +519,47 @@
                 // Normally, we don't count static rows as from a CQL point of view, it will be merge with other
                 // rows in the partition. However, if we only have the static row, it will be returned as one row
                 // so count it.
-                if (countPartitionsWithOnlyStaticData && hasLiveStaticRow && rowInCurrentPartition == 0)
+                if (countPartitionsWithOnlyStaticData && hasLiveStaticRow && rowsInCurrentPartition == 0)
                     incrementRowCount();
                 super.onPartitionClose();
             }
 
-            private void incrementRowCount()
+            protected void incrementRowCount()
             {
-                if (++rowCounted >= rowLimit)
+                if (++rowsCounted >= rowLimit)
                     stop();
-                if (++rowInCurrentPartition >= perPartitionLimit)
+                if (++rowsInCurrentPartition >= perPartitionLimit)
                     stopInPartition();
             }
 
             public int counted()
             {
-                return rowCounted;
+                return rowsCounted;
             }
 
             public int countedInCurrentPartition()
             {
-                return rowInCurrentPartition;
+                return rowsInCurrentPartition;
+            }
+
+            public int rowsCounted()
+            {
+                return rowsCounted;
+            }
+
+            public int rowsCountedInCurrentPartition()
+            {
+                return rowsInCurrentPartition;
             }
 
             public boolean isDone()
             {
-                return rowCounted >= rowLimit;
+                return rowsCounted >= rowLimit;
             }
 
             public boolean isDoneForPartition()
             {
-                return isDone() || rowInCurrentPartition >= perPartitionLimit;
+                return isDone() || rowsInCurrentPartition >= perPartitionLimit;
             }
         }
 
@@ -520,6 +613,12 @@
         }
 
         @Override
+        public DataLimits withoutState()
+        {
+            return new CQLLimits(rowLimit, perPartitionLimit, isDistinct);
+        }
+
+        @Override
         public Counter newCounter(int nowInSec, boolean assumeLiveData, boolean countPartitionsWithOnlyStaticData, boolean enforceStrictLiveness)
         {
             return new PagingAwareCounter(nowInSec, assumeLiveData, countPartitionsWithOnlyStaticData, enforceStrictLiveness);
@@ -540,7 +639,7 @@
             {
                 if (partitionKey.getKey().equals(lastReturnedKey))
                 {
-                    rowInCurrentPartition = perPartitionLimit - lastReturnedKeyRemaining;
+                    rowsInCurrentPartition = perPartitionLimit - lastReturnedKeyRemaining;
                     // lastReturnedKey is the last key for which we're returned rows in the first page.
                     // So, since we know we have returned rows, we know we have accounted for the static row
                     // if any already, so force hasLiveStaticRow to false so we make sure to not count it
@@ -556,6 +655,508 @@
     }
 
     /**
+     * <code>CQLLimits</code> used for GROUP BY queries or queries with aggregates.
+     * <p>Internally, GROUP BY queries are always paginated by number of rows to avoid OOMExceptions. By consequence,
+     * the limits keep track of the number of rows as well as the number of groups.</p>
+     * <p>A group can only be counted if the next group or the end of the data is reached.</p>
+     */
+    private static class CQLGroupByLimits extends CQLLimits
+    {
+        /**
+         * The <code>GroupMaker</code> state
+         */
+        protected final GroupingState state;
+
+        /**
+         * The GROUP BY specification
+         */
+        protected final AggregationSpecification groupBySpec;
+
+        /**
+         * The limit on the number of groups
+         */
+        protected final int groupLimit;
+
+        /**
+         * The limit on the number of groups per partition
+         */
+        protected final int groupPerPartitionLimit;
+
+        public CQLGroupByLimits(int groupLimit,
+                                int groupPerPartitionLimit,
+                                int rowLimit,
+                                AggregationSpecification groupBySpec)
+        {
+            this(groupLimit, groupPerPartitionLimit, rowLimit, groupBySpec, GroupingState.EMPTY_STATE);
+        }
+
+        private CQLGroupByLimits(int groupLimit,
+                                 int groupPerPartitionLimit,
+                                 int rowLimit,
+                                 AggregationSpecification groupBySpec,
+                                 GroupingState state)
+        {
+            super(rowLimit, NO_LIMIT, false);
+            this.groupLimit = groupLimit;
+            this.groupPerPartitionLimit = groupPerPartitionLimit;
+            this.groupBySpec = groupBySpec;
+            this.state = state;
+        }
+
+        @Override
+        public Kind kind()
+        {
+            return Kind.CQL_GROUP_BY_LIMIT;
+        }
+
+        @Override
+        public boolean isGroupByLimit()
+        {
+            return true;
+        }
+
+        public boolean isUnlimited()
+        {
+            return groupLimit == NO_LIMIT && groupPerPartitionLimit == NO_LIMIT && rowLimit == NO_LIMIT;
+        }
+
+        public DataLimits forShortReadRetry(int toFetch)
+        {
+            return new CQLLimits(toFetch);
+        }
+
+        @Override
+        public float estimateTotalResults(ColumnFamilyStore cfs)
+        {
+            // For the moment, we return the estimated number of rows as we have no good way of estimating 
+            // the number of groups that will be returned. Hopefully, we should be able to fix
+            // that problem at some point.
+            return super.estimateTotalResults(cfs);
+        }
+
+        @Override
+        public DataLimits forPaging(int pageSize)
+        {
+            return new CQLGroupByLimits(pageSize,
+                                        groupPerPartitionLimit,
+                                        rowLimit,
+                                        groupBySpec,
+                                        state);
+        }
+
+        @Override
+        public DataLimits forPaging(int pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining)
+        {
+            return new CQLGroupByPagingLimits(pageSize,
+                                              groupPerPartitionLimit,
+                                              rowLimit,
+                                              groupBySpec,
+                                              state,
+                                              lastReturnedKey,
+                                              lastReturnedKeyRemaining);
+        }
+
+        @Override
+        public DataLimits forGroupByInternalPaging(GroupingState state)
+        {
+            return new CQLGroupByLimits(rowLimit,
+                                        groupPerPartitionLimit,
+                                        rowLimit,
+                                        groupBySpec,
+                                        state);
+        }
+
+        @Override
+        public Counter newCounter(int nowInSec,
+                                  boolean assumeLiveData,
+                                  boolean countPartitionsWithOnlyStaticData,
+                                  boolean enforceStrictLiveness)
+        {
+            return new GroupByAwareCounter(nowInSec, assumeLiveData, countPartitionsWithOnlyStaticData, enforceStrictLiveness);
+        }
+
+        @Override
+        public int count()
+        {
+            return groupLimit;
+        }
+
+        @Override
+        public int perPartitionCount()
+        {
+            return groupPerPartitionLimit;
+        }
+
+        @Override
+        public DataLimits withoutState()
+        {
+            return state == GroupingState.EMPTY_STATE
+                 ? this
+                 : new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, rowLimit, groupBySpec);
+        }
+
+        @Override
+        public String toString()
+        {
+            StringBuilder sb = new StringBuilder();
+
+            if (groupLimit != NO_LIMIT)
+            {
+                sb.append("GROUP LIMIT ").append(groupLimit);
+                if (groupPerPartitionLimit != NO_LIMIT || rowLimit != NO_LIMIT)
+                    sb.append(' ');
+            }
+
+            if (groupPerPartitionLimit != NO_LIMIT)
+            {
+                sb.append("GROUP PER PARTITION LIMIT ").append(groupPerPartitionLimit);
+                if (rowLimit != NO_LIMIT)
+                    sb.append(' ');
+            }
+
+            if (rowLimit != NO_LIMIT)
+            {
+                sb.append("LIMIT ").append(rowLimit);
+            }
+
+            return sb.toString();
+        }
+
+        @Override
+        public boolean isExhausted(Counter counter)
+        {
+            return ((GroupByAwareCounter) counter).rowsCounted < rowLimit
+                    && counter.counted() < groupLimit;
+        }
+
+        protected class GroupByAwareCounter extends Counter
+        {
+            private final GroupMaker groupMaker;
+
+            protected final boolean countPartitionsWithOnlyStaticData;
+
+            /**
+             * The key of the partition being processed.
+             */
+            protected DecoratedKey currentPartitionKey;
+
+            /**
+             * The number of rows counted so far.
+             */
+            protected int rowsCounted;
+
+            /**
+             * The number of rows counted so far in the current partition.
+             */
+            protected int rowsCountedInCurrentPartition;
+
+            /**
+             * The number of groups counted so far. A group is counted only once it is complete
+             * (e.g the next one has been reached).
+             */
+            protected int groupCounted;
+
+            /**
+             * The number of groups in the current partition.
+             */
+            protected int groupInCurrentPartition;
+
+            protected boolean hasUnfinishedGroup;
+
+            protected boolean hasLiveStaticRow;
+
+            protected boolean hasReturnedRowsFromCurrentPartition;
+
+            private GroupByAwareCounter(int nowInSec,
+                                        boolean assumeLiveData,
+                                        boolean countPartitionsWithOnlyStaticData,
+                                        boolean enforceStrictLiveness)
+            {
+                super(nowInSec, assumeLiveData, enforceStrictLiveness);
+                this.groupMaker = groupBySpec.newGroupMaker(state);
+                this.countPartitionsWithOnlyStaticData = countPartitionsWithOnlyStaticData;
+
+                // If the end of the partition was reached at the same time than the row limit, the last group might
+                // not have been counted yet. Due to that we need to guess, based on the state, if the previous group
+                // is still open.
+                hasUnfinishedGroup = state.hasClustering();
+            }
+
+            @Override
+            public void applyToPartition(DecoratedKey partitionKey, Row staticRow)
+            {
+                if (partitionKey.getKey().equals(state.partitionKey()))
+                {
+                    // The only case were we could have state.partitionKey() equals to the partition key
+                    // is if some of the partition rows have been returned in the previous page but the
+                    // partition was not exhausted (as the state partition key has not been updated yet).
+                    // Since we know we have returned rows, we know we have accounted for
+                    // the static row if any already, so force hasLiveStaticRow to false so we make sure to not count it
+                    // once more.
+                    hasLiveStaticRow = false;
+                    hasReturnedRowsFromCurrentPartition = true;
+                    hasUnfinishedGroup = true;
+                }
+                else
+                {
+                    // We need to increment our count of groups if we have reached a new one and unless we had no new
+                    // content added since we closed our last group (that is, if hasUnfinishedGroup). Note that we may get
+                    // here with hasUnfinishedGroup == false in the following cases:
+                    // * the partition limit was reached for the previous partition
+                    // * the previous partition was containing only one static row
+                    // * the rows of the last group of the previous partition were all marked as deleted
+                    if (hasUnfinishedGroup && groupMaker.isNewGroup(partitionKey, Clustering.STATIC_CLUSTERING))
+                    {
+                        incrementGroupCount();
+                        // If we detect, before starting the new partition, that we are done, we need to increase
+                        // the per partition group count of the previous partition as the next page will start from
+                        // there.
+                        if (isDone())
+                            incrementGroupInCurrentPartitionCount();
+                        hasUnfinishedGroup = false;
+                    }
+                    hasReturnedRowsFromCurrentPartition = false;
+                    hasLiveStaticRow = !staticRow.isEmpty() && isLive(staticRow);
+                }
+                currentPartitionKey = partitionKey;
+                // If we are done we need to preserve the groupInCurrentPartition and rowsCountedInCurrentPartition
+                // because the pager need to retrieve the count associated to the last value it has returned.
+                if (!isDone())
+                {
+                    groupInCurrentPartition = 0;
+                    rowsCountedInCurrentPartition = 0;
+                }
+            }
+
+            @Override
+            protected Row applyToStatic(Row row)
+            {
+                // It's possible that we're "done" if the partition we just started bumped the number of groups (in
+                // applyToPartition() above), in which case Transformation will still call this method. In that case, we
+                // want to ignore the static row, it should (and will) be returned with the next page/group if needs be.
+                if (enforceLimits && isDone())
+                {
+                    hasLiveStaticRow = false; // The row has not been returned
+                    return Rows.EMPTY_STATIC_ROW;
+                }
+                return row;
+            }
+
+            @Override
+            public Row applyToRow(Row row)
+            {
+                // We want to check if the row belongs to a new group even if it has been deleted. The goal being
+                // to minimize the chances of having to go through the same data twice if we detect on the next
+                // non deleted row that we have reached the limit.
+                if (groupMaker.isNewGroup(currentPartitionKey, row.clustering()))
+                {
+                    if (hasUnfinishedGroup)
+                    {
+                        incrementGroupCount();
+                        incrementGroupInCurrentPartitionCount();
+                    }
+                    hasUnfinishedGroup = false;
+                }
+
+                // That row may have made us increment the group count, which may mean we're done for this partition, in
+                // which case we shouldn't count this row (it won't be returned).
+                if (enforceLimits && isDoneForPartition())
+                {
+                    hasUnfinishedGroup = false;
+                    return null;
+                }
+
+                if (isLive(row))
+                {
+                    hasUnfinishedGroup = true;
+                    incrementRowCount();
+                    hasReturnedRowsFromCurrentPartition = true;
+                }
+
+                return row;
+            }
+
+            @Override
+            public int counted()
+            {
+                return groupCounted;
+            }
+
+            @Override
+            public int countedInCurrentPartition()
+            {
+                return groupInCurrentPartition;
+            }
+
+            @Override
+            public int rowsCounted()
+            {
+                return rowsCounted;
+            }
+
+            @Override
+            public int rowsCountedInCurrentPartition()
+            {
+                return rowsCountedInCurrentPartition;
+            }
+
+            protected void incrementRowCount()
+            {
+                rowsCountedInCurrentPartition++;
+                if (++rowsCounted >= rowLimit)
+                    stop();
+            }
+
+            private void incrementGroupCount()
+            {
+                groupCounted++;
+                if (groupCounted >= groupLimit)
+                    stop();
+            }
+
+            private void incrementGroupInCurrentPartitionCount()
+            {
+                groupInCurrentPartition++;
+                if (groupInCurrentPartition >= groupPerPartitionLimit)
+                    stopInPartition();
+            }
+
+            @Override
+            public boolean isDoneForPartition()
+            {
+                return isDone() || groupInCurrentPartition >= groupPerPartitionLimit;
+            }
+
+            @Override
+            public boolean isDone()
+            {
+                return groupCounted >= groupLimit;
+            }
+
+            @Override
+            public void onPartitionClose()
+            {
+                // Normally, we don't count static rows as from a CQL point of view, it will be merge with other
+                // rows in the partition. However, if we only have the static row, it will be returned as one group
+                // so count it.
+                if (countPartitionsWithOnlyStaticData && hasLiveStaticRow && !hasReturnedRowsFromCurrentPartition)
+                {
+                    incrementRowCount();
+                    incrementGroupCount();
+                    incrementGroupInCurrentPartitionCount();
+                    hasUnfinishedGroup = false;
+                }
+                super.onPartitionClose();
+            }
+
+            @Override
+            public void onClose()
+            {
+                // Groups are only counted when the end of the group is reached.
+                // The end of a group is detected by 2 ways:
+                // 1) a new group is reached
+                // 2) the end of the data is reached
+                // We know that the end of the data is reached if the group limit has not been reached
+                // and the number of rows counted is smaller than the internal page size.
+                if (hasUnfinishedGroup && groupCounted < groupLimit && rowsCounted < rowLimit)
+                {
+                    incrementGroupCount();
+                    incrementGroupInCurrentPartitionCount();
+                }
+
+                super.onClose();
+            }
+        }
+    }
+
+    private static class CQLGroupByPagingLimits extends CQLGroupByLimits
+    {
+        private final ByteBuffer lastReturnedKey;
+
+        private final int lastReturnedKeyRemaining;
+
+        public CQLGroupByPagingLimits(int groupLimit,
+                                      int groupPerPartitionLimit,
+                                      int rowLimit,
+                                      AggregationSpecification groupBySpec,
+                                      GroupingState state,
+                                      ByteBuffer lastReturnedKey,
+                                      int lastReturnedKeyRemaining)
+        {
+            super(groupLimit,
+                  groupPerPartitionLimit,
+                  rowLimit,
+                  groupBySpec,
+                  state);
+
+            this.lastReturnedKey = lastReturnedKey;
+            this.lastReturnedKeyRemaining = lastReturnedKeyRemaining;
+        }
+
+        @Override
+        public Kind kind()
+        {
+            return Kind.CQL_GROUP_BY_PAGING_LIMIT;
+        }
+
+        @Override
+        public DataLimits forPaging(int pageSize)
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public DataLimits forPaging(int pageSize, ByteBuffer lastReturnedKey, int lastReturnedKeyRemaining)
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public DataLimits forGroupByInternalPaging(GroupingState state)
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Counter newCounter(int nowInSec, boolean assumeLiveData, boolean countPartitionsWithOnlyStaticData, boolean enforceStrictLiveness)
+        {
+            assert state == GroupingState.EMPTY_STATE || lastReturnedKey.equals(state.partitionKey());
+            return new PagingGroupByAwareCounter(nowInSec, assumeLiveData, countPartitionsWithOnlyStaticData, enforceStrictLiveness);
+        }
+
+        @Override
+        public DataLimits withoutState()
+        {
+            return new CQLGroupByLimits(groupLimit, groupPerPartitionLimit, rowLimit, groupBySpec);
+        }
+
+        private class PagingGroupByAwareCounter extends GroupByAwareCounter
+        {
+            private PagingGroupByAwareCounter(int nowInSec, boolean assumeLiveData, boolean countPartitionsWithOnlyStaticData, boolean enforceStrictLiveness)
+            {
+                super(nowInSec, assumeLiveData, countPartitionsWithOnlyStaticData, enforceStrictLiveness);
+            }
+
+            @Override
+            public void applyToPartition(DecoratedKey partitionKey, Row staticRow)
+            {
+                if (partitionKey.getKey().equals(lastReturnedKey))
+                {
+                    currentPartitionKey = partitionKey;
+                    groupInCurrentPartition = groupPerPartitionLimit - lastReturnedKeyRemaining;
+                    hasReturnedRowsFromCurrentPartition = true;
+                    hasLiveStaticRow = false;
+                    hasUnfinishedGroup = state.hasClustering();
+                }
+                else
+                {
+                    super.applyToPartition(partitionKey, staticRow);
+                }
+            }
+        }
+    }
+
+    /**
      * Limits used by thrift; this count partition and cells.
      */
     private static class ThriftLimits extends DataLimits
@@ -633,7 +1234,7 @@
 
         public Counter newCounter(int nowInSec, boolean assumeLiveData, boolean countPartitionsWithOnlyStaticData, boolean enforceStrictLiveness)
         {
-            return new ThriftCounter(nowInSec, assumeLiveData);
+            return new ThriftCounter(nowInSec, assumeLiveData, enforceStrictLiveness);
         }
 
         public int count()
@@ -646,6 +1247,11 @@
             return cellPerPartitionLimit;
         }
 
+        public DataLimits withoutState()
+        {
+            return this;
+        }
+
         public float estimateTotalResults(ColumnFamilyStore cfs)
         {
             // remember that getMeansColumns returns a number of cells: we should clean nomenclature
@@ -655,17 +1261,13 @@
 
         protected class ThriftCounter extends Counter
         {
-            protected final int nowInSec;
-            protected final boolean assumeLiveData;
-
             protected int partitionsCounted;
             protected int cellsCounted;
             protected int cellsInCurrentPartition;
 
-            public ThriftCounter(int nowInSec, boolean assumeLiveData)
+            public ThriftCounter(int nowInSec, boolean assumeLiveData, boolean enforceStrictLiveness)
             {
-                this.nowInSec = nowInSec;
-                this.assumeLiveData = assumeLiveData;
+                super(nowInSec, assumeLiveData, enforceStrictLiveness);
             }
 
             @Override
@@ -709,6 +1311,16 @@
                 return cellsInCurrentPartition;
             }
 
+            public int rowsCounted()
+            {
+                throw new UnsupportedOperationException();
+            }
+
+            public int rowsCountedInCurrentPartition()
+            {
+                throw new UnsupportedOperationException();
+            }
+
             public boolean isDone()
             {
                 return partitionsCounted >= partitionLimit;
@@ -772,7 +1384,7 @@
 
             public SuperColumnCountingCounter(int nowInSec, boolean assumeLiveData, boolean enforceStrictLiveness)
             {
-                super(nowInSec, assumeLiveData);
+                super(nowInSec, assumeLiveData, enforceStrictLiveness);
                 this.enforceStrictLiveness = enforceStrictLiveness;
             }
 
@@ -780,7 +1392,7 @@
             public Row applyToRow(Row row)
             {
                 // In the internal format, a row == a super column, so that's what we want to count.
-                if (assumeLiveData || row.hasLiveData(nowInSec, enforceStrictLiveness))
+                if (isLive(row))
                 {
                     ++cellsCounted;
                     if (++cellsInCurrentPartition >= cellPerPartitionLimit)
@@ -793,7 +1405,7 @@
 
     public static class Serializer
     {
-        public void serialize(DataLimits limits, DataOutputPlus out, int version) throws IOException
+        public void serialize(DataLimits limits, DataOutputPlus out, int version, ClusteringComparator comparator) throws IOException
         {
             out.writeByte(limits.kind().ordinal());
             switch (limits.kind())
@@ -811,6 +1423,25 @@
                         out.writeUnsignedVInt(pagingLimits.lastReturnedKeyRemaining);
                     }
                     break;
+                case CQL_GROUP_BY_LIMIT:
+                case CQL_GROUP_BY_PAGING_LIMIT:
+                    CQLGroupByLimits groupByLimits = (CQLGroupByLimits) limits;
+                    out.writeUnsignedVInt(groupByLimits.groupLimit);
+                    out.writeUnsignedVInt(groupByLimits.groupPerPartitionLimit);
+                    out.writeUnsignedVInt(groupByLimits.rowLimit);
+
+                    AggregationSpecification groupBySpec = groupByLimits.groupBySpec;
+                    AggregationSpecification.serializer.serialize(groupBySpec, out, version);
+
+                    GroupingState.serializer.serialize(groupByLimits.state, out, version, comparator);
+
+                    if (limits.kind() == Kind.CQL_GROUP_BY_PAGING_LIMIT)
+                    {
+                        CQLGroupByPagingLimits pagingLimits = (CQLGroupByPagingLimits) groupByLimits;
+                        ByteBufferUtil.writeWithVIntLength(pagingLimits.lastReturnedKey, out);
+                        out.writeUnsignedVInt(pagingLimits.lastReturnedKeyRemaining);
+                     }
+                     break;
                 case THRIFT_LIMIT:
                 case SUPER_COLUMN_COUNTING_LIMIT:
                     ThriftLimits thriftLimits = (ThriftLimits)limits;
@@ -820,54 +1451,102 @@
             }
         }
 
-        public DataLimits deserialize(DataInputPlus in, int version) throws IOException
+        public DataLimits deserialize(DataInputPlus in, int version, ClusteringComparator comparator) throws IOException
         {
             Kind kind = Kind.values()[in.readUnsignedByte()];
             switch (kind)
             {
                 case CQL_LIMIT:
                 case CQL_PAGING_LIMIT:
-                    int rowLimit = (int)in.readUnsignedVInt();
-                    int perPartitionLimit = (int)in.readUnsignedVInt();
+                {
+                    int rowLimit = (int) in.readUnsignedVInt();
+                    int perPartitionLimit = (int) in.readUnsignedVInt();
                     boolean isDistinct = in.readBoolean();
                     if (kind == Kind.CQL_LIMIT)
-                        return new CQLLimits(rowLimit, perPartitionLimit, isDistinct);
+                        return cqlLimits(rowLimit, perPartitionLimit, isDistinct);
+                    ByteBuffer lastKey = ByteBufferUtil.readWithVIntLength(in);
+                    int lastRemaining = (int) in.readUnsignedVInt();
+                    return new CQLPagingLimits(rowLimit, perPartitionLimit, isDistinct, lastKey, lastRemaining);
+                }
+                case CQL_GROUP_BY_LIMIT:
+                case CQL_GROUP_BY_PAGING_LIMIT:
+                {
+                    int groupLimit = (int) in.readUnsignedVInt();
+                    int groupPerPartitionLimit = (int) in.readUnsignedVInt();
+                    int rowLimit = (int) in.readUnsignedVInt();
+
+                    AggregationSpecification groupBySpec = AggregationSpecification.serializer.deserialize(in, version, comparator);
+
+                    GroupingState state = GroupingState.serializer.deserialize(in, version, comparator);
+
+                    if (kind == Kind.CQL_GROUP_BY_LIMIT)
+                        return new CQLGroupByLimits(groupLimit,
+                                                    groupPerPartitionLimit,
+                                                    rowLimit,
+                                                    groupBySpec,
+                                                    state);
 
                     ByteBuffer lastKey = ByteBufferUtil.readWithVIntLength(in);
-                    int lastRemaining = (int)in.readUnsignedVInt();
-                    return new CQLPagingLimits(rowLimit, perPartitionLimit, isDistinct, lastKey, lastRemaining);
+                    int lastRemaining = (int) in.readUnsignedVInt();
+                    return new CQLGroupByPagingLimits(groupLimit,
+                                                      groupPerPartitionLimit,
+                                                      rowLimit,
+                                                      groupBySpec,
+                                                      state,
+                                                      lastKey,
+                                                      lastRemaining);
+                }
                 case THRIFT_LIMIT:
                 case SUPER_COLUMN_COUNTING_LIMIT:
-                    int partitionLimit = (int)in.readUnsignedVInt();
-                    int cellPerPartitionLimit = (int)in.readUnsignedVInt();
+                    int partitionLimit = (int) in.readUnsignedVInt();
+                    int cellPerPartitionLimit = (int) in.readUnsignedVInt();
                     return kind == Kind.THRIFT_LIMIT
-                         ? new ThriftLimits(partitionLimit, cellPerPartitionLimit)
-                         : new SuperColumnCountingLimits(partitionLimit, cellPerPartitionLimit);
+                            ? new ThriftLimits(partitionLimit, cellPerPartitionLimit)
+                            : new SuperColumnCountingLimits(partitionLimit, cellPerPartitionLimit);
             }
             throw new AssertionError();
         }
 
-        public long serializedSize(DataLimits limits, int version)
+        public long serializedSize(DataLimits limits, int version, ClusteringComparator comparator)
         {
-            long size = TypeSizes.sizeof((byte)limits.kind().ordinal());
+            long size = TypeSizes.sizeof((byte) limits.kind().ordinal());
             switch (limits.kind())
             {
                 case CQL_LIMIT:
                 case CQL_PAGING_LIMIT:
-                    CQLLimits cqlLimits = (CQLLimits)limits;
+                    CQLLimits cqlLimits = (CQLLimits) limits;
                     size += TypeSizes.sizeofUnsignedVInt(cqlLimits.rowLimit);
                     size += TypeSizes.sizeofUnsignedVInt(cqlLimits.perPartitionLimit);
                     size += TypeSizes.sizeof(cqlLimits.isDistinct);
                     if (limits.kind() == Kind.CQL_PAGING_LIMIT)
                     {
-                        CQLPagingLimits pagingLimits = (CQLPagingLimits)cqlLimits;
+                        CQLPagingLimits pagingLimits = (CQLPagingLimits) cqlLimits;
+                        size += ByteBufferUtil.serializedSizeWithVIntLength(pagingLimits.lastReturnedKey);
+                        size += TypeSizes.sizeofUnsignedVInt(pagingLimits.lastReturnedKeyRemaining);
+                    }
+                    break;
+                case CQL_GROUP_BY_LIMIT:
+                case CQL_GROUP_BY_PAGING_LIMIT:
+                    CQLGroupByLimits groupByLimits = (CQLGroupByLimits) limits;
+                    size += TypeSizes.sizeofUnsignedVInt(groupByLimits.groupLimit);
+                    size += TypeSizes.sizeofUnsignedVInt(groupByLimits.groupPerPartitionLimit);
+                    size += TypeSizes.sizeofUnsignedVInt(groupByLimits.rowLimit);
+
+                    AggregationSpecification groupBySpec = groupByLimits.groupBySpec;
+                    size += AggregationSpecification.serializer.serializedSize(groupBySpec, version);
+
+                    size += GroupingState.serializer.serializedSize(groupByLimits.state, version, comparator);
+
+                    if (limits.kind() == Kind.CQL_GROUP_BY_PAGING_LIMIT)
+                    {
+                        CQLGroupByPagingLimits pagingLimits = (CQLGroupByPagingLimits) groupByLimits;
                         size += ByteBufferUtil.serializedSizeWithVIntLength(pagingLimits.lastReturnedKey);
                         size += TypeSizes.sizeofUnsignedVInt(pagingLimits.lastReturnedKeyRemaining);
                     }
                     break;
                 case THRIFT_LIMIT:
                 case SUPER_COLUMN_COUNTING_LIMIT:
-                    ThriftLimits thriftLimits = (ThriftLimits)limits;
+                    ThriftLimits thriftLimits = (ThriftLimits) limits;
                     size += TypeSizes.sizeofUnsignedVInt(thriftLimits.partitionLimit);
                     size += TypeSizes.sizeofUnsignedVInt(thriftLimits.cellPerPartitionLimit);
                     break;
diff --git a/src/java/org/apache/cassandra/db/filter/RowFilter.java b/src/java/org/apache/cassandra/db/filter/RowFilter.java
index 774e4d3..b4f8a7f 100644
--- a/src/java/org/apache/cassandra/db/filter/RowFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/RowFilter.java
@@ -20,8 +20,13 @@
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import com.google.common.base.Objects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
@@ -29,7 +34,10 @@
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.context.*;
 import org.apache.cassandra.db.marshal.*;
-import org.apache.cassandra.db.partitions.*;
+import org.apache.cassandra.db.partitions.FilteredPartition;
+import org.apache.cassandra.db.partitions.ImmutableBTreePartition;
+import org.apache.cassandra.db.partitions.PartitionIterator;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.transform.Transformation;
 import org.apache.cassandra.exceptions.InvalidRequestException;
@@ -54,6 +62,8 @@
  */
 public abstract class RowFilter implements Iterable<RowFilter.Expression>
 {
+    private static final Logger logger = LoggerFactory.getLogger(RowFilter.class);
+
     public static final Serializer serializer = new Serializer();
     public static final RowFilter NONE = new CQLFilter(Collections.emptyList());
 
@@ -79,9 +89,11 @@
         return new ThriftFilter(new ArrayList<>(capacity));
     }
 
-    public void add(ColumnDefinition def, Operator op, ByteBuffer value)
+    public SimpleExpression add(ColumnDefinition def, Operator op, ByteBuffer value)
     {
-        add(new SimpleExpression(def, op, value));
+        SimpleExpression expression = new SimpleExpression(def, op, value);
+        add(expression);
+        return expression;
     }
 
     public void addMapEquality(ColumnDefinition def, ByteBuffer key, Operator op, ByteBuffer value)
@@ -106,6 +118,11 @@
         expressions.add(expression);
     }
 
+    public void addUserExpression(UserExpression e)
+    {
+        expressions.add(e);
+    }
+
     public List<Expression> getExpressions()
     {
         return expressions;
@@ -235,6 +252,11 @@
         return withNewExpressions(newExpressions);
     }
 
+    public RowFilter withoutExpressions()
+    {
+        return withNewExpressions(Collections.emptyList());
+    }
+
     protected abstract RowFilter withNewExpressions(List<Expression> expressions);
 
     public boolean isEmpty()
@@ -253,11 +275,11 @@
         if (metadata.isCompound())
         {
             List<ByteBuffer> values = CompositeType.splitName(name);
-            return new Clustering(values.toArray(new ByteBuffer[metadata.comparator.size()]));
+            return Clustering.make(values.toArray(new ByteBuffer[metadata.comparator.size()]));
         }
         else
         {
-            return new Clustering(name);
+            return Clustering.make(name);
         }
     }
 
@@ -283,23 +305,34 @@
 
         protected Transformation<BaseRowIterator<?>> filter(CFMetaData metadata, int nowInSec)
         {
-            long numberOfStaticColumnExpressions = expressions.stream().filter(e -> e.column.isStatic()).count();
-            final boolean filterStaticColumns = numberOfStaticColumnExpressions != 0;
-            final boolean filterNonStaticColumns = (expressions.size() - numberOfStaticColumnExpressions) > 0;
+            List<Expression> partitionLevelExpressions = new ArrayList<>();
+            List<Expression> rowLevelExpressions = new ArrayList<>();
+            for (Expression e: expressions)
+            {
+                if (e.column.isStatic() || e.column.isPartitionKey())
+                    partitionLevelExpressions.add(e);
+                else
+                    rowLevelExpressions.add(e);
+            }
+
+            long numberOfRegularColumnExpressions = rowLevelExpressions.size();
+            final boolean filterNonStaticColumns = numberOfRegularColumnExpressions > 0;
 
             return new Transformation<BaseRowIterator<?>>()
             {
                 DecoratedKey pk;
                 protected BaseRowIterator<?> applyToPartition(BaseRowIterator<?> partition)
                 {
-                    // The filter might be on static columns, so need to check static row first.
-                    if (filterStaticColumns && applyToRow(partition.staticRow()) == null)
-                    {
-                        partition.close();
-                        return null;
-                    }
-
                     pk = partition.partitionKey();
+
+                    // Short-circuit all partitions that won't match based on static and partition keys
+                    for (Expression e : partitionLevelExpressions)
+                        if (!e.isSatisfiedBy(metadata, partition.partitionKey(), partition.staticRow()))
+                        {
+                            partition.close();
+                            return null;
+                        }
+
                     BaseRowIterator<?> iterator = partition instanceof UnfilteredRowIterator
                                                   ? Transformation.apply((UnfilteredRowIterator) partition, this)
                                                   : Transformation.apply((RowIterator) partition, this);
@@ -319,9 +352,10 @@
                     if (purged == null)
                         return null;
 
-                    for (Expression e : expressions)
+                    for (Expression e : rowLevelExpressions)
                         if (!e.isSatisfiedBy(metadata, pk, purged))
                             return null;
+
                     return row;
                 }
             };
@@ -394,9 +428,9 @@
         private static final Serializer serializer = new Serializer();
 
         // Note: the order of this enum matter, it's used for serialization
-        protected enum Kind { SIMPLE, MAP_EQUALITY, THRIFT_DYN_EXPR, CUSTOM }
+        protected enum Kind { SIMPLE, MAP_EQUALITY, THRIFT_DYN_EXPR, CUSTOM, USER }
 
-        abstract Kind kind();
+        protected abstract Kind kind();
         protected final ColumnDefinition column;
         protected final Operator operator;
         protected final ByteBuffer value;
@@ -413,6 +447,11 @@
             return kind() == Kind.CUSTOM;
         }
 
+        public boolean isUserDefined()
+        {
+            return kind() == Kind.USER;
+        }
+
         public ColumnDefinition column()
         {
             return column;
@@ -535,6 +574,13 @@
                     return;
                 }
 
+                if (expression.kind() == Kind.USER)
+                {
+                    assert version >= MessagingService.VERSION_30;
+                    UserExpression.serialize((UserExpression)expression, out, version);
+                    return;
+                }
+
                 ByteBufferUtil.writeWithShortLength(expression.column.name.bytes, out);
                 expression.operator.writeTo(out);
 
@@ -578,6 +624,11 @@
                                                     IndexMetadata.serializer.deserialize(in, version, metadata),
                                                     ByteBufferUtil.readWithShortLength(in));
                     }
+
+                    if (kind == Kind.USER)
+                    {
+                        return UserExpression.deserialize(in, version, metadata);
+                    }
                 }
 
                 name = ByteBufferUtil.readWithShortLength(in);
@@ -627,8 +678,11 @@
                 // version 3.0+ includes a byte for Kind
                 long size = version >= MessagingService.VERSION_30 ? 1 : 0;
 
-                // custom expressions don't include a column or operator, all other expressions do
-                if (expression.kind() != Kind.CUSTOM)
+                // Custom expressions include neither a column or operator, but all
+                // other expressions do. Also, custom expressions are 3.0+ only, so
+                // the column & operator will always be the first things written for
+                // any pre-3.0 version
+                if (expression.kind() != Kind.CUSTOM && expression.kind() != Kind.USER)
                     size += ByteBufferUtil.serializedSizeWithShortLength(expression.column().name.bytes)
                             + expression.operator.serializedSize();
 
@@ -651,8 +705,11 @@
                     case CUSTOM:
                         if (version >= MessagingService.VERSION_30)
                             size += IndexMetadata.serializer.serializedSize(((CustomExpression)expression).targetIndex, version)
-                                  + ByteBufferUtil.serializedSizeWithShortLength(expression.value);
+                                   + ByteBufferUtil.serializedSizeWithShortLength(expression.value);
                         break;
+                    case USER:
+                        if (version >= MessagingService.VERSION_30)
+                            size += UserExpression.serializedSize((UserExpression)expression, version);
                 }
                 return size;
             }
@@ -662,9 +719,9 @@
     /**
      * An expression of the form 'column' 'op' 'value'.
      */
-    private static class SimpleExpression extends Expression
+    public static class SimpleExpression extends Expression
     {
-        public SimpleExpression(ColumnDefinition column, Operator operator, ByteBuffer value)
+        SimpleExpression(ColumnDefinition column, Operator operator, ByteBuffer value)
         {
             super(column, operator, value);
         }
@@ -675,9 +732,6 @@
             // TODO: we should try to merge both code someday.
             assert value != null;
 
-            if (row.isStatic() != column.isStatic())
-                return true;
-
             switch (operator)
             {
                 case EQ:
@@ -707,6 +761,10 @@
                         }
                     }
                 case NEQ:
+                case LIKE_PREFIX:
+                case LIKE_SUFFIX:
+                case LIKE_CONTAINS:
+                case LIKE_MATCHES:
                     {
                         assert !column.isComplex() : "Only CONTAINS and CONTAINS_KEY are supported for 'complex' types";
                         ByteBuffer foundValue = getValue(metadata, partitionKey, row);
@@ -803,7 +861,7 @@
         }
 
         @Override
-        Kind kind()
+        protected Kind kind()
         {
             return Kind.SIMPLE;
         }
@@ -897,7 +955,7 @@
         }
 
         @Override
-        Kind kind()
+        protected Kind kind()
         {
             return Kind.MAP_EQUALITY;
         }
@@ -945,7 +1003,7 @@
         }
 
         @Override
-        Kind kind()
+        protected Kind kind()
         {
             return Kind.THRIFT_DYN_EXPR;
         }
@@ -995,7 +1053,7 @@
                                          .customExpressionValueType());
         }
 
-        Kind kind()
+        protected Kind kind()
         {
             return Kind.CUSTOM;
         }
@@ -1007,6 +1065,100 @@
         }
     }
 
+    /**
+     * A user defined filtering expression. These may be added to RowFilter programmatically by a
+     * QueryHandler implementation. No concrete implementations are provided and adding custom impls
+     * to the classpath is a task for operators (needless to say, this is something of a power
+     * user feature). Care must also be taken to register implementations, via the static register
+     * method during system startup. An implementation and its corresponding Deserializer must be
+     * registered before sending or receiving any messages containing expressions of that type.
+     * Use of custom filtering expressions in a mixed version cluster should be handled with caution
+     * as the order in which types are registered is significant: if continuity of use during upgrades
+     * is important, new types should registered last and obsoleted types should still be registered (
+     * or dummy implementations registered in their place) to preserve consistent identifiers across
+     * the cluster).
+     *
+     * During serialization, the identifier for the Deserializer implementation is prepended to the
+     * implementation specific payload. To deserialize, the identifier is read first to obtain the
+     * Deserializer, which then provides the concrete expression instance.
+     */
+    public static abstract class UserExpression extends Expression
+    {
+        private static final DeserializerRegistry deserializers = new DeserializerRegistry();
+        private static final class DeserializerRegistry
+        {
+            private final AtomicInteger counter = new AtomicInteger(0);
+            private final ConcurrentMap<Integer, Deserializer> deserializers = new ConcurrentHashMap<>();
+            private final ConcurrentMap<Class<? extends UserExpression>, Integer> registeredClasses = new ConcurrentHashMap<>();
+
+            public void registerUserExpressionClass(Class<? extends UserExpression> expressionClass,
+                                                    UserExpression.Deserializer deserializer)
+            {
+                int id = registeredClasses.computeIfAbsent(expressionClass, (cls) -> counter.getAndIncrement());
+                deserializers.put(id, deserializer);
+
+                logger.debug("Registered user defined expression type {} and serializer {} with identifier {}",
+                             expressionClass.getName(), deserializer.getClass().getName(), id);
+            }
+
+            public Integer getId(UserExpression expression)
+            {
+                return registeredClasses.get(expression.getClass());
+            }
+
+            public Deserializer getDeserializer(int id)
+            {
+                return deserializers.get(id);
+            }
+        }
+
+        protected static abstract class Deserializer
+        {
+            protected abstract UserExpression deserialize(DataInputPlus in,
+                                                          int version,
+                                                          CFMetaData metadata) throws IOException;
+        }
+
+        public static void register(Class<? extends UserExpression> expressionClass, Deserializer deserializer)
+        {
+            deserializers.registerUserExpressionClass(expressionClass, deserializer);
+        }
+
+        private static UserExpression deserialize(DataInputPlus in, int version, CFMetaData metadata) throws IOException
+        {
+            int id = in.readInt();
+            Deserializer deserializer = deserializers.getDeserializer(id);
+            assert deserializer != null : "No user defined expression type registered with id " + id;
+            return deserializer.deserialize(in, version, metadata);
+        }
+
+        private static void serialize(UserExpression expression, DataOutputPlus out, int version) throws IOException
+        {
+            Integer id = deserializers.getId(expression);
+            assert id != null : "User defined expression type " + expression.getClass().getName() + " is not registered";
+            out.writeInt(id);
+            expression.serialize(out, version);
+        }
+
+        private static long serializedSize(UserExpression expression, int version)
+        {   // 4 bytes for the expression type id
+            return 4 + expression.serializedSize(version);
+        }
+
+        protected UserExpression(ColumnDefinition column, Operator operator, ByteBuffer value)
+        {
+            super(column, operator, value);
+        }
+
+        protected Kind kind()
+        {
+            return Kind.USER;
+        }
+
+        protected abstract void serialize(DataOutputPlus out, int version) throws IOException;
+        protected abstract long serializedSize(int version);
+    }
+
     public static class Serializer
     {
         public void serialize(RowFilter filter, DataOutputPlus out, int version) throws IOException
diff --git a/src/java/org/apache/cassandra/db/lifecycle/ILifecycleTransaction.java b/src/java/org/apache/cassandra/db/lifecycle/ILifecycleTransaction.java
index d694a86..3de0a35 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/ILifecycleTransaction.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/ILifecycleTransaction.java
@@ -35,4 +35,5 @@
     void obsoleteOriginals();
     Set<SSTableReader> originals();
     boolean isObsolete(SSTableReader reader);
+    boolean isOffline();
 }
diff --git a/src/java/org/apache/cassandra/db/lifecycle/LifecycleTransaction.java b/src/java/org/apache/cassandra/db/lifecycle/LifecycleTransaction.java
index 4abce33..5994707 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/LifecycleTransaction.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/LifecycleTransaction.java
@@ -30,6 +30,7 @@
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Directories;
 import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.io.sstable.SSTable;
@@ -577,9 +578,14 @@
         log.untrackNew(table);
     }
 
-    public static void removeUnfinishedLeftovers(CFMetaData metadata)
+    public static boolean removeUnfinishedLeftovers(ColumnFamilyStore cfs)
     {
-        LogTransaction.removeUnfinishedLeftovers(metadata);
+        return LogTransaction.removeUnfinishedLeftovers(cfs.getDirectories().getCFDirectories());
+    }
+
+    public static boolean removeUnfinishedLeftovers(CFMetaData cfMetaData)
+    {
+        return LogTransaction.removeUnfinishedLeftovers(cfMetaData);
     }
 
     /**
diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogAwareFileLister.java b/src/java/org/apache/cassandra/db/lifecycle/LogAwareFileLister.java
index 7728f9c..212076d 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/LogAwareFileLister.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/LogAwareFileLister.java
@@ -171,13 +171,12 @@
 
         logger.error("Failed to classify files in {}\n" +
                      "Some old files are missing but the txn log is still there and not completed\n" +
-                     "Files in folder:\n{}\nTxn: {}\n{}",
+                     "Files in folder:\n{}\nTxn: {}",
                      folder,
                      files.isEmpty()
                         ? "\t-"
                         : String.join("\n", files.keySet().stream().map(f -> String.format("\t%s", f)).collect(Collectors.toList())),
-                     txnFile.toString(),
-                     String.join("\n", txnFile.getRecords().stream().map(r -> String.format("\t%s", r)).collect(Collectors.toList())));
+                     txnFile.toString(true));
 
         // some old files are missing and yet the txn is still there and not completed
         // something must be wrong (see comment at the top of LogTransaction requiring txn to be
diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogFile.java b/src/java/org/apache/cassandra/db/lifecycle/LogFile.java
index 42d81ca..3550d66 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/LogFile.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/LogFile.java
@@ -98,9 +98,9 @@
         return new LogFile(operationType, id, logReplicas);
     }
 
-    Throwable syncFolder(Throwable accumulate)
+    Throwable syncDirectory(Throwable accumulate)
     {
-        return replicas.syncFolder(accumulate);
+        return replicas.syncDirectory(accumulate);
     }
 
     OperationType type()
@@ -117,11 +117,17 @@
     {
         try
         {
+            // we sync the parent directories before content deletion to ensure
+            // any previously deleted files (see SSTableTider) are not
+            // incorrectly picked up by record.getExistingFiles() in
+            // deleteRecordFiles(), see CASSANDRA-12261
+            Throwables.maybeFail(syncDirectory(accumulate));
+
             deleteFilesForRecordsOfType(committed() ? Type.REMOVE : Type.ADD);
 
-            // we sync the parent folders between contents and log deletion
+            // we sync the parent directories between contents and log deletion
             // to ensure there is a happens before edge between them
-            Throwables.maybeFail(syncFolder(accumulate));
+            Throwables.maybeFail(syncDirectory(accumulate));
 
             accumulate = replicas.delete(accumulate);
         }
@@ -155,7 +161,7 @@
         records.clear();
         if (!replicas.readRecords(records))
         {
-            logger.error("Failed to read records from {}", replicas);
+            logger.error("Failed to read records for transaction log {}", this);
             return false;
         }
 
@@ -182,7 +188,7 @@
         LogRecord failedOn = firstInvalid.get();
         if (getLastRecord() != failedOn)
         {
-            logError(failedOn);
+            setErrorInReplicas(failedOn);
             return false;
         }
 
@@ -190,25 +196,24 @@
         if (records.stream()
                    .filter((r) -> r != failedOn)
                    .filter(LogRecord::isInvalid)
-                   .map(LogFile::logError)
+                   .map(this::setErrorInReplicas)
                    .findFirst().isPresent())
         {
-            logError(failedOn);
+            setErrorInReplicas(failedOn);
             return false;
         }
 
         // if only the last record is corrupt and all other records have matching files on disk, @see verifyRecord,
         // then we simply exited whilst serializing the last record and we carry on
-        logger.warn(String.format("Last record of transaction %s is corrupt or incomplete [%s], " +
-                                  "but all previous records match state on disk; continuing",
-                                  id,
-                                  failedOn.error()));
+        logger.warn("Last record of transaction {} is corrupt or incomplete [{}], " +
+                    "but all previous records match state on disk; continuing",
+                    id, failedOn.error());
         return true;
     }
 
-    static LogRecord logError(LogRecord record)
+    LogRecord setErrorInReplicas(LogRecord record)
     {
-        logger.error("{}", record.error());
+        replicas.setErrorInReplicas(record);
         return record;
     }
 
@@ -216,9 +221,8 @@
     {
         if (record.checksum != record.computeChecksum())
         {
-            record.setError(String.format("Invalid checksum for sstable [%s], record [%s]: [%d] should have been [%d]",
+            record.setError(String.format("Invalid checksum for sstable [%s]: [%d] should have been [%d]",
                                           record.fileName(),
-                                          record,
                                           record.checksum,
                                           record.computeChecksum()));
             return;
@@ -238,9 +242,8 @@
         if (truncateMillis(record.updateTime) != truncateMillis(record.status.onDiskRecord.updateTime) && record.status.onDiskRecord.updateTime > 0)
         {
             record.setError(String.format("Unexpected files detected for sstable [%s]: " +
-                                          "record [%s]: last update time [%tc] (%d) should have been [%tc] (%d)",
+                                          "last update time [%tc] (%d) should have been [%tc] (%d)",
                                           record.fileName(),
-                                          record,
                                           record.status.onDiskRecord.updateTime,
                                           record.status.onDiskRecord.updateTime,
                                           record.updateTime,
@@ -264,11 +267,9 @@
         if (record.type == Type.REMOVE && record.status.onDiskRecord.numFiles < record.numFiles)
         { // if we found a corruption in the last record, then we continue only
           // if the number of files matches exactly for all previous records.
-            record.setError(String.format("Incomplete fileset detected for sstable [%s], record [%s]: " +
-                                          "number of files [%d] should have been [%d]. Treating as unrecoverable " +
-                                          "due to corruption of the final record.",
+            record.setError(String.format("Incomplete fileset detected for sstable [%s]: " +
+                                          "number of files [%d] should have been [%d].",
                                           record.fileName(),
-                                          record.raw,
                                           record.status.onDiskRecord.numFiles,
                                           record.numFiles));
         }
@@ -347,9 +348,9 @@
 
     private void maybeCreateReplica(SSTable sstable)
     {
-        File folder = sstable.descriptor.directory;
-        String fileName = getFileName(folder);
-        replicas.maybeCreateReplica(folder, fileName, onDiskRecords);
+        File directory = sstable.descriptor.directory;
+        String fileName = StringUtils.join(directory, File.separator, getFileName());
+        replicas.maybeCreateReplica(directory, fileName, onDiskRecords);
     }
 
     void addRecord(LogRecord record)
@@ -460,7 +461,25 @@
     @Override
     public String toString()
     {
-        return replicas.toString();
+        return toString(false);
+    }
+
+    public String toString(boolean showContents)
+    {
+        StringBuilder str = new StringBuilder();
+        str.append('[');
+        str.append(getFileName());
+        str.append(" in ");
+        str.append(replicas.getDirectories());
+        str.append(']');
+        if (showContents)
+        {
+            str.append(System.lineSeparator());
+            str.append("Files and contents follow:");
+            str.append(System.lineSeparator());
+            replicas.printContentsWithAnyErrors(str);
+        }
+        return str.toString();
     }
 
     @VisibleForTesting
@@ -475,22 +494,16 @@
         return replicas.getFilePaths();
     }
 
-    private String getFileName(File folder)
+    private String getFileName()
     {
-        String fileName = StringUtils.join(BigFormat.latestVersion,
-                                           LogFile.SEP,
-                                           "txn",
-                                           LogFile.SEP,
-                                           type.fileName,
-                                           LogFile.SEP,
-                                           id.toString(),
-                                           LogFile.EXT);
-        return StringUtils.join(folder, File.separator, fileName);
-    }
-
-    Collection<LogRecord> getRecords()
-    {
-        return records;
+        return StringUtils.join(BigFormat.latestVersion,
+                                LogFile.SEP,
+                                "txn",
+                                LogFile.SEP,
+                                type.fileName,
+                                LogFile.SEP,
+                                id.toString(),
+                                LogFile.EXT);
     }
 
     public boolean isEmpty()
diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogRecord.java b/src/java/org/apache/cassandra/db/lifecycle/LogRecord.java
index 69b4920..a9b7433 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/LogRecord.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/LogRecord.java
@@ -127,13 +127,15 @@
             Type type = Type.fromPrefix(matcher.group(1));
             return new LogRecord(type,
                                  matcher.group(2) + Component.separator, // see comment on CASSANDRA-13294 below
-                                 Long.valueOf(matcher.group(3)),
-                                 Integer.valueOf(matcher.group(4)),
-                                 Long.valueOf(matcher.group(5)), line);
+                                 Long.parseLong(matcher.group(3)),
+                                 Integer.parseInt(matcher.group(4)),
+                                 Long.parseLong(matcher.group(5)),
+                                 line);
         }
-        catch (Throwable t)
+        catch (IllegalArgumentException e)
         {
-            return new LogRecord(Type.UNKNOWN, null, 0, 0, 0, line).setError(t);
+            return new LogRecord(Type.UNKNOWN, null, 0, 0, 0, line)
+                   .setError(String.format("Failed to parse line: %s", e.getMessage()));
         }
     }
 
@@ -237,11 +239,6 @@
         }
     }
 
-    LogRecord setError(Throwable t)
-    {
-        return setError(t.getMessage());
-    }
-
     LogRecord setError(String error)
     {
         status.setError(error);
diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogReplica.java b/src/java/org/apache/cassandra/db/lifecycle/LogReplica.java
index 0378046..cdc4c35 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/LogReplica.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/LogReplica.java
@@ -19,6 +19,9 @@
 package org.apache.cassandra.db.lifecycle;
 
 import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.io.IOException;
 
 import org.slf4j.Logger;
@@ -33,7 +36,7 @@
 /**
  * Because a column family may have sstables on different disks and disks can
  * be removed, we duplicate log files into many replicas so as to have a file
- * in each folder where sstables exist.
+ * in each directory where sstables exist.
  *
  * Each replica contains the exact same content but we do allow for final
  * partial records in case we crashed after writing to one replica but
@@ -46,13 +49,14 @@
     private static final Logger logger = LoggerFactory.getLogger(LogReplica.class);
 
     private final File file;
-    private int folderDescriptor;
+    private int directoryDescriptor;
+    private final Map<String, String> errors = new HashMap<>();
 
-    static LogReplica create(File folder, String fileName)
+    static LogReplica create(File directory, String fileName)
     {
-        int folderFD = NativeLibrary.tryOpenDirectory(folder.getPath());
-        if (folderFD == -1 && !FBUtilities.isWindows())
-            throw new FSReadError(new IOException(String.format("Invalid folder descriptor trying to create log replica %s", folder.getPath())), folder.getPath());
+        int folderFD = NativeLibrary.tryOpenDirectory(directory.getPath());
+        if (folderFD == -1 && !FBUtilities.isWindows)
+            throw new FSReadError(new IOException(String.format("Invalid folder descriptor trying to create log replica %s", directory.getPath())), directory.getPath());
 
         return new LogReplica(new File(fileName), folderFD);
     }
@@ -60,16 +64,16 @@
     static LogReplica open(File file)
     {
         int folderFD = NativeLibrary.tryOpenDirectory(file.getParentFile().getPath());
-        if (folderFD == -1 && !FBUtilities.isWindows())
+        if (folderFD == -1 && !FBUtilities.isWindows)
             throw new FSReadError(new IOException(String.format("Invalid folder descriptor trying to create log replica %s", file.getParentFile().getPath())), file.getParentFile().getPath());
 
         return new LogReplica(file, folderFD);
     }
 
-    LogReplica(File file, int folderDescriptor)
+    LogReplica(File file, int directoryDescriptor)
     {
         this.file = file;
-        this.folderDescriptor = folderDescriptor;
+        this.directoryDescriptor = directoryDescriptor;
     }
 
     File file()
@@ -77,6 +81,21 @@
         return file;
     }
 
+    List<String> readLines()
+    {
+        return FileUtils.readLines(file);
+    }
+
+    String getFileName()
+    {
+        return file.getName();
+    }
+
+    String getDirectory()
+    {
+        return file.getParent();
+    }
+
     void append(LogRecord record)
     {
         boolean existed = exists();
@@ -91,21 +110,21 @@
         }
 
         // If the file did not exist before appending the first
-        // line, then sync the folder as well since now it must exist
+        // line, then sync the directory as well since now it must exist
         if (!existed)
-            syncFolder();
+            syncDirectory();
     }
 
-    void syncFolder()
+    void syncDirectory()
     {
         try
         {
-            if (folderDescriptor >= 0)
-                NativeLibrary.trySync(folderDescriptor);
+            if (directoryDescriptor >= 0)
+                NativeLibrary.trySync(directoryDescriptor);
         }
         catch (FSError e)
         {
-            logger.error("Failed to sync directory descriptor {}", folderDescriptor, e);
+            logger.error("Failed to sync directory descriptor {}", directoryDescriptor, e);
             FileUtils.handleFSErrorAndPropagate(e);
         }
     }
@@ -113,7 +132,7 @@
     void delete()
     {
         LogTransaction.delete(file);
-        syncFolder();
+        syncDirectory();
     }
 
     boolean exists()
@@ -123,10 +142,10 @@
 
     public void close()
     {
-        if (folderDescriptor >= 0)
+        if (directoryDescriptor >= 0)
         {
-            NativeLibrary.tryCloseFD(folderDescriptor);
-            folderDescriptor = -1;
+            NativeLibrary.tryCloseFD(directoryDescriptor);
+            directoryDescriptor = -1;
         }
     }
 
@@ -135,4 +154,31 @@
     {
         return String.format("[%s] ", file);
     }
+
+    void setError(String line, String error)
+    {
+        errors.put(line, error);
+    }
+
+    void printContentsWithAnyErrors(StringBuilder str)
+    {
+        str.append(file.getPath());
+        str.append(System.lineSeparator());
+        FileUtils.readLines(file).forEach(line -> printLineWithAnyError(str, line));
+    }
+
+    private void printLineWithAnyError(StringBuilder str, String line)
+    {
+        str.append('\t');
+        str.append(line);
+        str.append(System.lineSeparator());
+
+        String error = errors.get(line);
+        if (error != null)
+        {
+            str.append("\t\t***");
+            str.append(error);
+            str.append(System.lineSeparator());
+        }
+    }
 }
diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogReplicaSet.java b/src/java/org/apache/cassandra/db/lifecycle/LogReplicaSet.java
index 0bf20e5..67d9dfd 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/LogReplicaSet.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/LogReplicaSet.java
@@ -39,7 +39,8 @@
  * A set of log replicas. This class mostly iterates over replicas when writing or reading,
  * ensuring consistency among them and hiding replication details from LogFile.
  *
- * @see LogReplica, LogFile
+ * @see LogReplica
+ * @see LogFile
  */
 public class LogReplicaSet implements AutoCloseable
 {
@@ -59,11 +60,11 @@
 
     void addReplica(File file)
     {
-        File folder = file.getParentFile();
-        assert !replicasByFile.containsKey(folder);
+        File directory = file.getParentFile();
+        assert !replicasByFile.containsKey(directory);
         try
         {
-            replicasByFile.put(folder, LogReplica.open(file));
+            replicasByFile.put(directory, LogReplica.open(file));
         }
         catch(FSError e)
         {
@@ -71,35 +72,33 @@
             FileUtils.handleFSErrorAndPropagate(e);
         }
 
-        if (logger.isTraceEnabled())
-            logger.trace("Added log file replica {} ", file);
+        logger.trace("Added log file replica {} ", file);
     }
 
-    void maybeCreateReplica(File folder, String fileName, Set<LogRecord> records)
+    void maybeCreateReplica(File directory, String fileName, Set<LogRecord> records)
     {
-        if (replicasByFile.containsKey(folder))
+        if (replicasByFile.containsKey(directory))
             return;
 
         try
         {
             @SuppressWarnings("resource")  // LogReplicas are closed in LogReplicaSet::close
-            final LogReplica replica = LogReplica.create(folder, fileName);
+            final LogReplica replica = LogReplica.create(directory, fileName);
             records.forEach(replica::append);
-            replicasByFile.put(folder, replica);
+            replicasByFile.put(directory, replica);
 
-            if (logger.isTraceEnabled())
-                logger.trace("Created new file replica {}", replica);
+            logger.trace("Created new file replica {}", replica);
         }
         catch(FSError e)
         {
-            logger.error("Failed to create log replica {}/{}", folder,  fileName, e);
+            logger.error("Failed to create log replica {}/{}", directory,  fileName, e);
             FileUtils.handleFSErrorAndPropagate(e);
         }
     }
 
-    Throwable syncFolder(Throwable accumulate)
+    Throwable syncDirectory(Throwable accumulate)
     {
-        return Throwables.perform(accumulate, replicas().stream().map(s -> s::syncFolder));
+        return Throwables.perform(accumulate, replicas().stream().map(s -> s::syncDirectory));
     }
 
     Throwable delete(Throwable accumulate)
@@ -116,15 +115,18 @@
 
     boolean readRecords(Set<LogRecord> records)
     {
-        Map<File, List<String>> linesByReplica = replicas().stream()
-                                                           .map(LogReplica::file)
-                                                           .collect(Collectors.toMap(Function.<File>identity(), FileUtils::readLines));
+        Map<LogReplica, List<String>> linesByReplica = replicas().stream()
+                                                                 .collect(Collectors.toMap(Function.<LogReplica>identity(),
+                                                                                           LogReplica::readLines,
+                                                                                           (k, v) -> {throw new IllegalStateException("Duplicated key: " + k);},
+                                                                                           LinkedHashMap::new));
+
         int maxNumLines = linesByReplica.values().stream().map(List::size).reduce(0, Integer::max);
         for (int i = 0; i < maxNumLines; i++)
         {
             String firstLine = null;
             boolean partial = false;
-            for (Map.Entry<File, List<String>> entry : linesByReplica.entrySet())
+            for (Map.Entry<LogReplica, List<String>> entry : linesByReplica.entrySet())
             {
                 List<String> currentLines = entry.getValue();
                 if (i >= currentLines.size())
@@ -140,9 +142,10 @@
                 if (!isPrefixMatch(firstLine, currentLine))
                 { // not a prefix match
                     logger.error("Mismatched line in file {}: got '{}' expected '{}', giving up",
-                                 entry.getKey().getName(),
+                                 entry.getKey().getFileName(),
                                  currentLine,
                                  firstLine);
+                    entry.getKey().setError(currentLine, String.format("Does not match <%s> in first replica file", firstLine));
                     return false;
                 }
 
@@ -151,7 +154,7 @@
                     if (i == currentLines.size() - 1)
                     { // last record, just set record as invalid and move on
                         logger.warn("Mismatched last line in file {}: '{}' not the same as '{}'",
-                                    entry.getKey().getName(),
+                                    entry.getKey().getFileName(),
                                     currentLine,
                                     firstLine);
 
@@ -163,9 +166,10 @@
                     else
                     {   // mismatched entry file has more lines, giving up
                         logger.error("Mismatched line in file {}: got '{}' expected '{}', giving up",
-                                     entry.getKey().getName(),
+                                     entry.getKey().getFileName(),
                                      currentLine,
                                      firstLine);
+                        entry.getKey().setError(currentLine, String.format("Does not match <%s> in first replica file", firstLine));
                         return false;
                     }
                 }
@@ -175,6 +179,7 @@
             if (records.contains(record))
             { // duplicate records
                 logger.error("Found duplicate record {} for {}, giving up", record, record.fileName());
+                setError(record, "Duplicated record");
                 return false;
             }
 
@@ -186,6 +191,7 @@
             if (record.isFinal() && i != (maxNumLines - 1))
             { // too many final records
                 logger.error("Found too many lines for {}, giving up", record.fileName());
+                setError(record, "This record should have been the last one in all replicas");
                 return false;
             }
         }
@@ -193,6 +199,22 @@
         return true;
     }
 
+    void setError(LogRecord record, String error)
+    {
+        record.setError(error);
+        setErrorInReplicas(record);
+    }
+
+    void setErrorInReplicas(LogRecord record)
+    {
+        replicas().forEach(r -> r.setError(record.raw, record.error()));
+    }
+
+    void printContentsWithAnyErrors(StringBuilder str)
+    {
+        replicas().forEach(r -> r.printContentsWithAnyErrors(str));
+    }
+
     /**
      *  Add the record to all the replicas: if it is a final record then we throw only if we fail to write it
      *  to all, otherwise we throw if we fail to write it to any file, see CASSANDRA-10421 for details
@@ -231,6 +253,11 @@
                : "[-]";
     }
 
+    String getDirectories()
+    {
+        return String.join(", ", replicas().stream().map(LogReplica::getDirectory).collect(Collectors.toList()));
+    }
+
     @VisibleForTesting
     List<File> getFiles()
     {
diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogTransaction.java b/src/java/org/apache/cassandra/db/lifecycle/LogTransaction.java
index da92fa2..014b06f 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/LogTransaction.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/LogTransaction.java
@@ -17,13 +17,16 @@
  */
 package org.apache.cassandra.db.lifecycle;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
 import java.util.*;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.util.concurrent.Runnables;
@@ -45,6 +48,7 @@
 import org.apache.cassandra.io.sstable.SnapshotDeletingTask;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.tools.StandaloneSSTableUtil;
 import org.apache.cassandra.utils.*;
 import org.apache.cassandra.utils.concurrent.Ref;
 import org.apache.cassandra.utils.concurrent.RefCounted;
@@ -59,7 +63,7 @@
  * IMPORTANT: The transaction must complete (commit or abort) before any temporary files are deleted, even though the
  * txn log file itself will not be deleted until all tracked files are deleted. This is required by FileLister to ensure
  * a consistent disk state. LifecycleTransaction ensures this requirement, so this class should really never be used
- * outside of LT. @see FileLister.classifyFiles(TransactionData txn)
+ * outside of LT. @see LogAwareFileLister.classifyFiles()
  *
  * A class that tracks sstable files involved in a transaction across sstables:
  * if the transaction succeeds the old files should be deleted and the new ones kept; vice-versa if it fails.
@@ -71,8 +75,7 @@
  *
  * where sstable-2 is a new sstable to be retained if the transaction succeeds and sstable-1 is an old sstable to be
  * removed. CRC is an incremental CRC of the file content up to this point. For old sstable files we also log the
- * last update time of all files for the sstable descriptor and a checksum of vital properties such as update times
- * and file sizes.
+ * last update time of all files for the sstable descriptor and the number of sstable files.
  *
  * Upon commit we add a final line to the log file:
  *
@@ -241,7 +244,16 @@
         }
         catch (NoSuchFileException e)
         {
-            logger.error("Unable to delete {} as it does not exist", file);
+            logger.error("Unable to delete {} as it does not exist, see debug log file for stack trace", file);
+            if (logger.isDebugEnabled())
+            {
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                try (PrintStream ps = new PrintStream(baos))
+                {
+                    e.printStackTrace(ps);
+                }
+                logger.debug("Unable to delete {} as it does not exist, stack trace:\n {}", file, baos.toString());
+            }
         }
         catch (IOException e)
         {
@@ -339,6 +351,7 @@
         private final boolean wasNew;
         private final Object lock;
         private final Ref<LogTransaction> parentRef;
+        private final UUID txnId;
 
         public SSTableTidier(SSTableReader referent, boolean wasNew, LogTransaction parent)
         {
@@ -348,6 +361,7 @@
             this.wasNew = wasNew;
             this.lock = parent.lock;
             this.parentRef = parent.selfRef.tryRef();
+            this.txnId = parent.id();
 
             if (this.parentRef == null)
                 throw new IllegalStateException("Transaction already completed");
@@ -368,7 +382,11 @@
                     // If we can't successfully delete the DATA component, set the task to be retried later: see TransactionTidier
                     File datafile = new File(desc.filenameFor(Component.DATA));
 
-                    delete(datafile);
+                    if (datafile.exists())
+                        delete(datafile);
+                    else if (!wasNew)
+                        logger.error("SSTableTidier ran with no existing data file for an sstable that was not new");
+
                     // let the remainder be cleaned up by delete
                     SSTable.delete(desc, SSTable.discoverComponentsFor(desc));
                 }
@@ -426,7 +444,7 @@
         }
         catch (Throwable t)
         {
-            logger.error("Failed to complete file transaction {}", id(), t);
+            logger.error("Failed to complete file transaction id {}", id(), t);
             return Throwables.merge(accumulate, t);
         }
     }
@@ -450,31 +468,43 @@
     protected void doPrepare() { }
 
     /**
-     * Called on startup to scan existing folders for any unfinished leftovers of
-     * operations that were ongoing when the process exited. Also called by the standalone
-     * sstableutil tool when the cleanup option is specified, @see StandaloneSSTableUtil.
+     * Removes any leftovers from unifinished transactions as indicated by any transaction log files that
+     * are found in the table directories. This means that any old sstable files for transactions that were committed,
+     * or any new sstable files for transactions that were aborted or still in progress, should be removed *if
+     * it is safe to do so*. Refer to the checks in LogFile.verify for further details on the safety checks
+     * before removing transaction leftovers and refer to the comments at the beginning of this file or in NEWS.txt
+     * for further details on transaction logs.
+     *
+     * This method is called on startup and by the standalone sstableutil tool when the cleanup option is specified,
+     * @see StandaloneSSTableUtil
+     *
+     * @return true if the leftovers of all transaction logs found were removed, false otherwise.
      *
      */
-    static void removeUnfinishedLeftovers(CFMetaData metadata)
+    static boolean removeUnfinishedLeftovers(CFMetaData metadata)
     {
-        removeUnfinishedLeftovers(new Directories(metadata, ColumnFamilyStore.getInitialDirectories()).getCFDirectories());
+        return removeUnfinishedLeftovers(new Directories(metadata, ColumnFamilyStore.getInitialDirectories()).getCFDirectories());
     }
 
     @VisibleForTesting
-    static void removeUnfinishedLeftovers(List<File> folders)
+    static boolean removeUnfinishedLeftovers(List<File> directories)
     {
         LogFilesByName logFiles = new LogFilesByName();
-        folders.forEach(logFiles::list);
-        logFiles.removeUnfinishedLeftovers();
+        directories.forEach(logFiles::list);
+        return logFiles.removeUnfinishedLeftovers();
     }
 
     private static final class LogFilesByName
     {
+        // This maps a transaction log file name to a list of physical files. Each sstable
+        // can have multiple directories and a transaction is trakced by identical transaction log
+        // files, one per directory. So for each transaction file name we can have multiple
+        // physical files.
         Map<String, List<File>> files = new HashMap<>();
 
-        void list(File folder)
+        void list(File directory)
         {
-            Arrays.stream(folder.listFiles(LogFile::isLogFile)).forEach(this::add);
+            Arrays.stream(directory.listFiles(LogFile::isLogFile)).forEach(this::add);
         }
 
         void add(File file)
@@ -489,25 +519,34 @@
             filesByName.add(file);
         }
 
-        void removeUnfinishedLeftovers()
+        boolean removeUnfinishedLeftovers()
         {
-            files.forEach(LogFilesByName::removeUnfinishedLeftovers);
+            return files.entrySet()
+                        .stream()
+                        .map(LogFilesByName::removeUnfinishedLeftovers)
+                        .allMatch(Predicate.isEqual(true));
         }
 
-        static void removeUnfinishedLeftovers(String name, List<File> logFiles)
+        static boolean removeUnfinishedLeftovers(Map.Entry<String, List<File>> entry)
         {
-
-            try(LogFile txn = LogFile.make(name, logFiles))
+            try(LogFile txn = LogFile.make(entry.getKey(), entry.getValue()))
             {
                 if (txn.verify())
                 {
                     Throwable failure = txn.removeUnfinishedLeftovers(null);
                     if (failure != null)
-                        logger.error("Failed to remove unfinished transaction leftovers for txn {}", txn, failure);
+                    {
+                        logger.error("Failed to remove unfinished transaction leftovers for transaction log {}",
+                                     txn.toString(true), failure);
+                        return false;
+                    }
+
+                    return true;
                 }
                 else
                 {
-                    logger.error("Unexpected disk state: failed to read transaction txn {}", txn);
+                    logger.error("Unexpected disk state: failed to read transaction log {}", txn.toString(true));
+                    return false;
                 }
             }
         }
diff --git a/src/java/org/apache/cassandra/db/lifecycle/Tracker.java b/src/java/org/apache/cassandra/db/lifecycle/Tracker.java
index 6c78bdf..5d5714a 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/Tracker.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/Tracker.java
@@ -31,7 +31,7 @@
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Directories;
 import org.apache.cassandra.db.Memtable;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -314,7 +314,7 @@
     /**
      * get the Memtable that the ordered writeOp should be directed to
      */
-    public Memtable getMemtableFor(OpOrder.Group opGroup, ReplayPosition replayPosition)
+    public Memtable getMemtableFor(OpOrder.Group opGroup, CommitLogPosition commitLogPosition)
     {
         // since any new memtables appended to the list after we fetch it will be for operations started
         // after us, we can safely assume that we will always find the memtable that 'accepts' us;
@@ -325,7 +325,7 @@
         // assign operations to a memtable that was retired/queued before we started)
         for (Memtable memtable : view.get().liveMemtables)
         {
-            if (memtable.accepts(opGroup, replayPosition))
+            if (memtable.accepts(opGroup, commitLogPosition))
                 return memtable;
         }
         throw new AssertionError(view.get().liveMemtables.toString());
@@ -344,6 +344,8 @@
         Pair<View, View> result = apply(View.switchMemtable(newMemtable));
         if (truncating)
             notifyRenewed(newMemtable);
+        else
+            notifySwitched(result.left.getCurrentMemtable());
 
         return result.left.getCurrentMemtable();
     }
@@ -353,10 +355,10 @@
         apply(View.markFlushing(memtable));
     }
 
-    public void replaceFlushed(Memtable memtable, Collection<SSTableReader> sstables)
+    public void replaceFlushed(Memtable memtable, Iterable<SSTableReader> sstables)
     {
         assert !isDummy();
-        if (sstables.isEmpty())
+        if (Iterables.isEmpty(sstables))
         {
             // sstable may be null if we flushed batchlog and nothing needed to be retained
             // if it's null, we don't care what state the cfstore is in, we just replace it and continue
@@ -372,6 +374,9 @@
 
         Throwable fail;
         fail = updateSizeTracking(emptySet(), sstables, null);
+
+        notifyDiscarded(memtable);
+
         // TODO: if we're invalidated, should we notifyadded AND removed, or just skip both?
         fail = notifyAdded(sstables, false, fail);
 
@@ -472,16 +477,30 @@
             subscriber.handleNotification(notification, this);
     }
 
-    public void notifyRenewed(Memtable renewed)
+    public void notifyTruncated(long truncatedAt)
     {
-        INotification notification = new MemtableRenewedNotification(renewed);
+        INotification notification = new TruncationNotification(truncatedAt);
         for (INotificationConsumer subscriber : subscribers)
             subscriber.handleNotification(notification, this);
     }
 
-    public void notifyTruncated(long truncatedAt)
+    public void notifyRenewed(Memtable renewed)
     {
-        INotification notification = new TruncationNotification(truncatedAt);
+        notify(new MemtableRenewedNotification(renewed));
+    }
+
+    public void notifySwitched(Memtable previous)
+    {
+        notify(new MemtableSwitchedNotification(previous));
+    }
+
+    public void notifyDiscarded(Memtable discarded)
+    {
+        notify(new MemtableDiscardedNotification(discarded));
+    }
+
+    private void notify(INotification notification)
+    {
         for (INotificationConsumer subscriber : subscribers)
             subscriber.handleNotification(notification, this);
     }
diff --git a/src/java/org/apache/cassandra/db/lifecycle/View.java b/src/java/org/apache/cassandra/db/lifecycle/View.java
index c33f305..203f2fa 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/View.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/View.java
@@ -39,7 +39,6 @@
 import static com.google.common.collect.Iterables.all;
 import static com.google.common.collect.Iterables.concat;
 import static com.google.common.collect.Iterables.filter;
-import static com.google.common.collect.Iterables.transform;
 import static org.apache.cassandra.db.lifecycle.Helpers.emptySet;
 import static org.apache.cassandra.db.lifecycle.Helpers.filterOut;
 import static org.apache.cassandra.db.lifecycle.Helpers.replace;
@@ -324,7 +323,7 @@
     }
 
     // called after flush: removes memtable from flushingMemtables, and inserts flushed into the live sstable set
-    static Function<View, View> replaceFlushed(final Memtable memtable, final Collection<SSTableReader> flushed)
+    static Function<View, View> replaceFlushed(final Memtable memtable, final Iterable<SSTableReader> flushed)
     {
         return new Function<View, View>()
         {
@@ -333,7 +332,7 @@
                 List<Memtable> flushingMemtables = copyOf(filter(view.flushingMemtables, not(equalTo(memtable))));
                 assert flushingMemtables.size() == view.flushingMemtables.size() - 1;
 
-                if (flushed == null || flushed.isEmpty())
+                if (flushed == null || Iterables.isEmpty(flushed))
                     return new View(view.liveMemtables, flushingMemtables, view.sstablesMap,
                                     view.compactingMap, view.intervalTree);
 
diff --git a/src/java/org/apache/cassandra/db/lifecycle/WrappedLifecycleTransaction.java b/src/java/org/apache/cassandra/db/lifecycle/WrappedLifecycleTransaction.java
index ff84208..12c46a9 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/WrappedLifecycleTransaction.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/WrappedLifecycleTransaction.java
@@ -109,4 +109,8 @@
         return delegate.opType();
     }
 
+    public boolean isOffline()
+    {
+        return delegate.isOffline();
+    }
 }
diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java b/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java
index 3d74c99..0c9a2c5 100644
--- a/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java
+++ b/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java
@@ -21,11 +21,13 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.regex.Pattern;
 
 import org.apache.cassandra.cql3.Term;
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.BytesSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 /**
@@ -107,6 +109,10 @@
         }
         return l.toArray(new ByteBuffer[l.size()]);
     }
+    private static final String COLON = ":";
+    private static final Pattern COLON_PAT = Pattern.compile(COLON);
+    private static final String ESCAPED_COLON = "\\\\:";
+    private static final Pattern ESCAPED_COLON_PAT = Pattern.compile(ESCAPED_COLON);
 
 
     /*
@@ -118,7 +124,7 @@
         if (input.isEmpty())
             return input;
 
-        String res = input.replaceAll(":", "\\\\:");
+        String res = COLON_PAT.matcher(input).replaceAll(ESCAPED_COLON);
         char last = res.charAt(res.length() - 1);
         return last == '\\' || last == '!' ? res + '!' : res;
     }
@@ -132,7 +138,7 @@
         if (input.isEmpty())
             return input;
 
-        String res = input.replaceAll("\\\\:", ":");
+        String res = ESCAPED_COLON_PAT.matcher(input).replaceAll(COLON);
         char last = res.charAt(res.length() - 1);
         return last == '!' ? res.substring(0, res.length() - 1) : res;
     }
@@ -246,7 +252,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         throw new UnsupportedOperationException();
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java
index b14af70..8bef154 100644
--- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java
+++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java
@@ -31,13 +31,16 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.cql3.AssignmentTestable;
 import org.apache.cassandra.cql3.CQL3Type;
+import org.apache.cassandra.cql3.ColumnSpecification;
 import org.apache.cassandra.cql3.Term;
 import org.apache.cassandra.db.TypeSizes;
 import org.apache.cassandra.exceptions.SyntaxException;
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.MarshalException;
 
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.FastByteOperations;
 import org.github.jamm.Unmetered;
 import org.apache.cassandra.io.util.DataOutputPlus;
@@ -55,7 +58,7 @@
  * represent a valid ByteBuffer for the type being compared.
  */
 @Unmetered
-public abstract class AbstractType<T> implements Comparator<ByteBuffer>
+public abstract class AbstractType<T> implements Comparator<ByteBuffer>, AssignmentTestable
 {
     private static final Logger logger = LoggerFactory.getLogger(AbstractType.class);
 
@@ -149,7 +152,7 @@
      * @param protocolVersion the protocol version to use for the conversion
      * @return a JSON string representing the specified value
      */
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return '"' + Objects.toString(getSerializer().deserialize(buffer), "") + '"';
     }
@@ -321,7 +324,7 @@
         return false;
     }
 
-    public boolean isMultiCell()
+    public boolean isUDT()
     {
         return false;
     }
@@ -331,7 +334,12 @@
         return false;
     }
 
-    public boolean isUDT()
+    public boolean isMultiCell()
+    {
+        return false;
+    }
+
+    public boolean isFreezable()
     {
         return false;
     }
@@ -342,6 +350,19 @@
     }
 
     /**
+     * Returns an AbstractType instance that is equivalent to this one, but with all nested UDTs and collections
+     * explicitly frozen.
+     *
+     * This is only necessary for {@code 2.x -> 3.x} schema migrations, and can be removed in Cassandra 4.0.
+     *
+     * See CASSANDRA-11609 and CASSANDRA-11613.
+     */
+    public AbstractType<?> freezeNestedMulticellTypes()
+    {
+        return this;
+    }
+
+    /**
      * Returns {@code true} for types where empty should be handled like {@code null} like {@link Int32Type}.
      */
     public boolean isEmptyValueMeaningless()
@@ -457,6 +478,11 @@
         return false;
     }
 
+    public boolean referencesDuration()
+    {
+        return false;
+    }
+
     /**
      * This must be overriden by subclasses if necessary so that for any
      * AbstractType, this == TypeParser.parse(toString()).
@@ -470,6 +496,17 @@
         return getClass().getName();
     }
 
+    /**
+     * Checks to see if two types are equal when ignoring or not ignoring differences in being frozen, depending on
+     * the value of the ignoreFreezing parameter.
+     * @param other type to compare
+     * @param ignoreFreezing if true, differences in the types being frozen will be ignored
+     */
+    public boolean equals(Object other, boolean ignoreFreezing)
+    {
+        return this.equals(other);
+    }
+
     public void checkComparable()
     {
         switch (comparisonType)
@@ -478,4 +515,24 @@
                 throw new IllegalArgumentException(this + " cannot be used in comparisons, so cannot be used as a clustering column");
         }
     }
+
+    public final AssignmentTestable.TestResult testAssignment(String keyspace, ColumnSpecification receiver)
+    {
+        // We should ignore the fact that the output type is frozen in our comparison as functions do not support
+        // frozen types for arguments
+        AbstractType<?> receiverType = receiver.type;
+        if (isFreezable() && !isMultiCell())
+            receiverType = receiverType.freeze();
+
+        if (isReversed())
+            receiverType = ReversedType.getInstance(receiverType);
+
+        if (equals(receiverType))
+            return AssignmentTestable.TestResult.EXACT_MATCH;
+
+        if (receiverType.isValueCompatibleWith(this))
+            return AssignmentTestable.TestResult.WEAKLY_ASSIGNABLE;
+
+        return AssignmentTestable.TestResult.NOT_ASSIGNABLE;
+    }
 }
diff --git a/src/java/org/apache/cassandra/db/marshal/AsciiType.java b/src/java/org/apache/cassandra/db/marshal/AsciiType.java
index 69b2b01..3cd45de 100644
--- a/src/java/org/apache/cassandra/db/marshal/AsciiType.java
+++ b/src/java/org/apache/cassandra/db/marshal/AsciiType.java
@@ -23,6 +23,7 @@
 import java.nio.charset.CharsetEncoder;
 import java.nio.charset.CharacterCodingException;
 
+import io.netty.util.concurrent.FastThreadLocal;
 import org.apache.cassandra.cql3.Constants;
 import org.apache.cassandra.cql3.Json;
 
@@ -31,6 +32,7 @@
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.AsciiSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class AsciiType extends AbstractType<String>
@@ -39,7 +41,7 @@
 
     AsciiType() {super(ComparisonType.BYTE_ORDER);} // singleton
 
-    private final ThreadLocal<CharsetEncoder> encoder = new ThreadLocal<CharsetEncoder>()
+    private final FastThreadLocal<CharsetEncoder> encoder = new FastThreadLocal<CharsetEncoder>()
     {
         @Override
         protected CharsetEncoder initialValue()
@@ -79,7 +81,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         try
         {
diff --git a/src/java/org/apache/cassandra/db/marshal/BooleanType.java b/src/java/org/apache/cassandra/db/marshal/BooleanType.java
index 24d0632..1dbd1af 100644
--- a/src/java/org/apache/cassandra/db/marshal/BooleanType.java
+++ b/src/java/org/apache/cassandra/db/marshal/BooleanType.java
@@ -25,6 +25,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.BooleanSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -80,7 +81,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return getSerializer().deserialize(buffer).toString();
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/ByteType.java b/src/java/org/apache/cassandra/db/marshal/ByteType.java
index 6bcf7cb..55aea8f 100644
--- a/src/java/org/apache/cassandra/db/marshal/ByteType.java
+++ b/src/java/org/apache/cassandra/db/marshal/ByteType.java
@@ -25,6 +25,7 @@
 import org.apache.cassandra.serializers.ByteSerializer;
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.serializers.TypeSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class ByteType extends AbstractType<Byte>
@@ -71,7 +72,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return getSerializer().deserialize(buffer).toString();
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/BytesType.java b/src/java/org/apache/cassandra/db/marshal/BytesType.java
index cec20f4..cabd007 100644
--- a/src/java/org/apache/cassandra/db/marshal/BytesType.java
+++ b/src/java/org/apache/cassandra/db/marshal/BytesType.java
@@ -25,6 +25,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.BytesSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.Hex;
 
@@ -64,7 +65,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return "\"0x" + ByteBufferUtil.bytesToHex(buffer) + '"';
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/CollectionType.java b/src/java/org/apache/cassandra/db/marshal/CollectionType.java
index d65e3a6..6e7d5d7 100644
--- a/src/java/org/apache/cassandra/db/marshal/CollectionType.java
+++ b/src/java/org/apache/cassandra/db/marshal/CollectionType.java
@@ -22,7 +22,6 @@
 import java.util.List;
 import java.util.Iterator;
 
-import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.CQL3Type;
 import org.apache.cassandra.cql3.ColumnSpecification;
 import org.apache.cassandra.cql3.Lists;
@@ -34,6 +33,7 @@
 import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.serializers.CollectionSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 /**
@@ -133,13 +133,19 @@
         return kind == Kind.MAP;
     }
 
+    @Override
+    public boolean isFreezable()
+    {
+        return true;
+    }
+
     // Overrided by maps
     protected int collectionSize(List<ByteBuffer> values)
     {
         return values.size();
     }
 
-    public ByteBuffer serializeForNativeProtocol(ColumnDefinition def, Iterator<Cell> cells, int version)
+    public ByteBuffer serializeForNativeProtocol(Iterator<Cell> cells, ProtocolVersion version)
     {
         assert isMultiCell();
         List<ByteBuffer> values = serializedValues(cells);
@@ -204,6 +210,27 @@
     }
 
     @Override
+    public boolean equals(Object o, boolean ignoreFreezing)
+    {
+        if (this == o)
+            return true;
+
+        if (!(o instanceof CollectionType))
+            return false;
+
+        CollectionType other = (CollectionType)o;
+
+        if (kind != other.kind)
+            return false;
+
+        if (!ignoreFreezing && isMultiCell() != other.isMultiCell())
+            return false;
+
+        return nameComparator().equals(other.nameComparator(), ignoreFreezing) &&
+               valueComparator().equals(other.valueComparator(), ignoreFreezing);
+    }
+
+    @Override
     public String toString()
     {
         return this.toString(false);
diff --git a/src/java/org/apache/cassandra/db/marshal/ColumnToCollectionType.java b/src/java/org/apache/cassandra/db/marshal/ColumnToCollectionType.java
index 96efa24..de50446 100644
--- a/src/java/org/apache/cassandra/db/marshal/ColumnToCollectionType.java
+++ b/src/java/org/apache/cassandra/db/marshal/ColumnToCollectionType.java
@@ -29,6 +29,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.BytesSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 /*
@@ -103,7 +104,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         throw new UnsupportedOperationException();
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/CompositeType.java b/src/java/org/apache/cassandra/db/marshal/CompositeType.java
index d4ddfc0..6358d71 100644
--- a/src/java/org/apache/cassandra/db/marshal/CompositeType.java
+++ b/src/java/org/apache/cassandra/db/marshal/CompositeType.java
@@ -363,7 +363,9 @@
         for (ByteBuffer bb : buffers)
         {
             ByteBufferUtil.writeShortLength(out, bb.remaining());
-            out.put(bb.duplicate());
+            int toCopy = bb.remaining();
+            ByteBufferUtil.arrayCopy(bb, bb.position(), out, out.position(), toCopy);
+            out.position(out.position() + toCopy);
             out.put((byte) 0);
         }
         out.flip();
diff --git a/src/java/org/apache/cassandra/db/marshal/CounterColumnType.java b/src/java/org/apache/cassandra/db/marshal/CounterColumnType.java
index 18ff256..8bb1a25 100644
--- a/src/java/org/apache/cassandra/db/marshal/CounterColumnType.java
+++ b/src/java/org/apache/cassandra/db/marshal/CounterColumnType.java
@@ -25,6 +25,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.CounterSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class CounterColumnType extends AbstractType<Long>
@@ -78,7 +79,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return CounterSerializer.instance.deserialize(buffer).toString();
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/DateType.java b/src/java/org/apache/cassandra/db/marshal/DateType.java
index dee800e..87b2cad 100644
--- a/src/java/org/apache/cassandra/db/marshal/DateType.java
+++ b/src/java/org/apache/cassandra/db/marshal/DateType.java
@@ -29,6 +29,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.TimestampSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 /**
@@ -77,7 +78,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return '"' + TimestampSerializer.getJsonDateFormatter().format(TimestampSerializer.instance.deserialize(buffer)) + '"';
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/DecimalType.java b/src/java/org/apache/cassandra/db/marshal/DecimalType.java
index 43b2711..05ac2e2 100644
--- a/src/java/org/apache/cassandra/db/marshal/DecimalType.java
+++ b/src/java/org/apache/cassandra/db/marshal/DecimalType.java
@@ -27,6 +27,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.DecimalSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class DecimalType extends AbstractType<BigDecimal>
@@ -81,7 +82,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return Objects.toString(getSerializer().deserialize(buffer), "\"\"");
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/DoubleType.java b/src/java/org/apache/cassandra/db/marshal/DoubleType.java
index 17d9cf5..f761ce3 100644
--- a/src/java/org/apache/cassandra/db/marshal/DoubleType.java
+++ b/src/java/org/apache/cassandra/db/marshal/DoubleType.java
@@ -25,6 +25,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.DoubleSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class DoubleType extends AbstractType<Double>
@@ -83,7 +84,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         Double value = getSerializer().deserialize(buffer);
         if (value == null)
diff --git a/src/java/org/apache/cassandra/db/marshal/DurationType.java b/src/java/org/apache/cassandra/db/marshal/DurationType.java
new file mode 100644
index 0000000..134a6f8
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/marshal/DurationType.java
@@ -0,0 +1,90 @@
+/*
+ * 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.db.marshal;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.cql3.CQL3Type;
+import org.apache.cassandra.cql3.Constants;
+import org.apache.cassandra.cql3.Duration;
+import org.apache.cassandra.cql3.Term;
+import org.apache.cassandra.serializers.DurationSerializer;
+import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.serializers.TypeSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+/**
+ * Represents a duration. The duration is stored as  months, days, and nanoseconds. This is done
+ * <p>Internally he duration is stored as months (unsigned integer), days (unsigned integer), and nanoseconds.</p>
+ */
+public class DurationType extends AbstractType<Duration>
+{
+    public static final DurationType instance = new DurationType();
+
+    DurationType()
+    {
+        super(ComparisonType.BYTE_ORDER);
+    } // singleton
+
+    public ByteBuffer fromString(String source) throws MarshalException
+    {
+        // Return an empty ByteBuffer for an empty string.
+        if (source.isEmpty())
+            return ByteBufferUtil.EMPTY_BYTE_BUFFER;
+
+        return decompose(Duration.from(source));
+    }
+
+    @Override
+    public boolean isValueCompatibleWithInternal(AbstractType<?> otherType)
+    {
+        return this == otherType;
+    }
+
+    public Term fromJSONObject(Object parsed) throws MarshalException
+    {
+        try
+        {
+            return new Constants.Value(fromString((String) parsed));
+        }
+        catch (ClassCastException exc)
+        {
+            throw new MarshalException(String.format("Expected a string representation of a duration, but got a %s: %s",
+                                                     parsed.getClass().getSimpleName(), parsed));
+        }
+    }
+
+    @Override
+    public TypeSerializer<Duration> getSerializer()
+    {
+        return DurationSerializer.instance;
+    }
+
+    @Override
+    public CQL3Type asCQL3Type()
+    {
+        return CQL3Type.Native.DURATION;
+    }
+
+    @Override
+    public boolean referencesDuration()
+    {
+        return true;
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/marshal/DynamicCompositeType.java b/src/java/org/apache/cassandra/db/marshal/DynamicCompositeType.java
index 657f126..d314bd9 100644
--- a/src/java/org/apache/cassandra/db/marshal/DynamicCompositeType.java
+++ b/src/java/org/apache/cassandra/db/marshal/DynamicCompositeType.java
@@ -30,6 +30,7 @@
 import org.apache.cassandra.exceptions.SyntaxException;
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 /*
@@ -123,7 +124,8 @@
          * If both types are ReversedType(Type), we need to compare on the wrapped type (which may differ between the two types) to avoid
          * incompatible comparisons being made.
          */
-        if ((comp1 instanceof ReversedType) && (comp2 instanceof ReversedType)) {
+        if ((comp1 instanceof ReversedType) && (comp2 instanceof ReversedType)) 
+        {
             comp1 = ((ReversedType<?>) comp1).baseType;
             comp2 = ((ReversedType<?>) comp2).baseType;
         }
@@ -381,7 +383,7 @@
         }
 
         @Override
-        public String toJSONString(ByteBuffer buffer, int protocolVersion)
+        public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
         {
             throw new UnsupportedOperationException();
         }
diff --git a/src/java/org/apache/cassandra/db/marshal/EmptyType.java b/src/java/org/apache/cassandra/db/marshal/EmptyType.java
index de087f5..00c919d 100644
--- a/src/java/org/apache/cassandra/db/marshal/EmptyType.java
+++ b/src/java/org/apache/cassandra/db/marshal/EmptyType.java
@@ -29,6 +29,7 @@
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.serializers.TypeSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.serializers.EmptySerializer;
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.utils.ByteBufferUtil;
@@ -102,7 +103,8 @@
         return CQL3Type.Native.EMPTY;
     }
 
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    @Override
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return "\"\"";
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/FloatType.java b/src/java/org/apache/cassandra/db/marshal/FloatType.java
index 0ff02a3..7d8959a 100644
--- a/src/java/org/apache/cassandra/db/marshal/FloatType.java
+++ b/src/java/org/apache/cassandra/db/marshal/FloatType.java
@@ -25,6 +25,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.FloatSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 
@@ -82,7 +83,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         Float value = getSerializer().deserialize(buffer);
         if (value == null)
diff --git a/src/java/org/apache/cassandra/db/marshal/FrozenType.java b/src/java/org/apache/cassandra/db/marshal/FrozenType.java
index 261e789..6cabb53 100644
--- a/src/java/org/apache/cassandra/db/marshal/FrozenType.java
+++ b/src/java/org/apache/cassandra/db/marshal/FrozenType.java
@@ -25,6 +25,7 @@
 import org.apache.cassandra.exceptions.SyntaxException;
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * A fake type that is only used for parsing type strings that include frozen types.
@@ -61,7 +62,7 @@
         throw new UnsupportedOperationException();
     }
 
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         throw new UnsupportedOperationException();
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/InetAddressType.java b/src/java/org/apache/cassandra/db/marshal/InetAddressType.java
index 393f09e..6838c83 100644
--- a/src/java/org/apache/cassandra/db/marshal/InetAddressType.java
+++ b/src/java/org/apache/cassandra/db/marshal/InetAddressType.java
@@ -26,6 +26,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.InetAddressSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class InetAddressType extends AbstractType<InetAddress>
@@ -79,7 +80,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return '"' + toString(getSerializer().deserialize(buffer)) + '"';
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/Int32Type.java b/src/java/org/apache/cassandra/db/marshal/Int32Type.java
index 9ac7d5b..eb75475 100644
--- a/src/java/org/apache/cassandra/db/marshal/Int32Type.java
+++ b/src/java/org/apache/cassandra/db/marshal/Int32Type.java
@@ -26,6 +26,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.Int32Serializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class Int32Type extends AbstractType<Integer>
@@ -96,7 +97,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return Objects.toString(getSerializer().deserialize(buffer), "\"\"");
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/IntegerType.java b/src/java/org/apache/cassandra/db/marshal/IntegerType.java
index f65856e..beb6759 100644
--- a/src/java/org/apache/cassandra/db/marshal/IntegerType.java
+++ b/src/java/org/apache/cassandra/db/marshal/IntegerType.java
@@ -27,6 +27,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.IntegerSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public final class IntegerType extends AbstractType<BigInteger>
@@ -164,7 +165,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return Objects.toString(getSerializer().deserialize(buffer), "\"\"");
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/ListType.java b/src/java/org/apache/cassandra/db/marshal/ListType.java
index 29e75bd..c03f866 100644
--- a/src/java/org/apache/cassandra/db/marshal/ListType.java
+++ b/src/java/org/apache/cassandra/db/marshal/ListType.java
@@ -29,6 +29,7 @@
 import org.apache.cassandra.serializers.CollectionSerializer;
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.serializers.ListSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -80,6 +81,12 @@
         return getElementsType().referencesUserType(userTypeName);
     }
 
+    @Override
+    public boolean referencesDuration()
+    {
+        return getElementsType().referencesDuration();
+    }
+
     public AbstractType<T> getElementsType()
     {
         return elements;
@@ -110,6 +117,18 @@
     }
 
     @Override
+    public AbstractType<?> freezeNestedMulticellTypes()
+    {
+        if (!isMultiCell())
+            return this;
+
+        if (elements.isFreezable() && elements.isMultiCell())
+            return getInstance(elements.freeze(), isMultiCell);
+
+        return getInstance(elements.freezeNestedMulticellTypes(), isMultiCell);
+    }
+
+    @Override
     public boolean isMultiCell()
     {
         return isMultiCell;
@@ -144,13 +163,13 @@
         ByteBuffer bb1 = o1.duplicate();
         ByteBuffer bb2 = o2.duplicate();
 
-        int size1 = CollectionSerializer.readCollectionSize(bb1, 3);
-        int size2 = CollectionSerializer.readCollectionSize(bb2, 3);
+        int size1 = CollectionSerializer.readCollectionSize(bb1, ProtocolVersion.V3);
+        int size2 = CollectionSerializer.readCollectionSize(bb2, ProtocolVersion.V3);
 
         for (int i = 0; i < Math.min(size1, size2); i++)
         {
-            ByteBuffer v1 = CollectionSerializer.readValue(bb1, 3);
-            ByteBuffer v2 = CollectionSerializer.readValue(bb2, 3);
+            ByteBuffer v1 = CollectionSerializer.readValue(bb1, ProtocolVersion.V3);
+            ByteBuffer v2 = CollectionSerializer.readValue(bb2, ProtocolVersion.V3);
             int cmp = elementsComparator.compare(v1, v2);
             if (cmp != 0)
                 return cmp;
@@ -205,7 +224,7 @@
         return new Lists.DelayedValue(terms);
     }
 
-    public static String setOrListToJsonString(ByteBuffer buffer, AbstractType elementsType, int protocolVersion)
+    public static String setOrListToJsonString(ByteBuffer buffer, AbstractType elementsType, ProtocolVersion protocolVersion)
     {
         ByteBuffer value = buffer.duplicate();
         StringBuilder sb = new StringBuilder("[");
@@ -220,7 +239,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return setOrListToJsonString(buffer, elements, protocolVersion);
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/LongType.java b/src/java/org/apache/cassandra/db/marshal/LongType.java
index d63229f..a491ee9 100644
--- a/src/java/org/apache/cassandra/db/marshal/LongType.java
+++ b/src/java/org/apache/cassandra/db/marshal/LongType.java
@@ -26,6 +26,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.LongSerializer;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class LongType extends AbstractType<Long>
@@ -98,7 +99,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return Objects.toString(getSerializer().deserialize(buffer), "\"\"");
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/MapType.java b/src/java/org/apache/cassandra/db/marshal/MapType.java
index 9c9cda9..1817f31 100644
--- a/src/java/org/apache/cassandra/db/marshal/MapType.java
+++ b/src/java/org/apache/cassandra/db/marshal/MapType.java
@@ -29,7 +29,7 @@
 import org.apache.cassandra.serializers.CollectionSerializer;
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.serializers.MapSerializer;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.Pair;
 
 public class MapType<K, V> extends CollectionType<Map<K, V>>
@@ -81,6 +81,13 @@
                getValuesType().referencesUserType(userTypeName);
     }
 
+    @Override
+    public boolean referencesDuration()
+    {
+        // Maps cannot be created with duration as keys
+        return getValuesType().referencesDuration();
+    }
+
     public AbstractType<K> getKeysType()
     {
         return keys;
@@ -117,6 +124,23 @@
     }
 
     @Override
+    public AbstractType<?> freezeNestedMulticellTypes()
+    {
+        if (!isMultiCell())
+            return this;
+
+        AbstractType<?> keyType = (keys.isFreezable() && keys.isMultiCell())
+                                ? keys.freeze()
+                                : keys.freezeNestedMulticellTypes();
+
+        AbstractType<?> valueType = (values.isFreezable() && values.isMultiCell())
+                                  ? values.freeze()
+                                  : values.freezeNestedMulticellTypes();
+
+        return getInstance(keyType, valueType, isMultiCell);
+    }
+
+    @Override
     public boolean isCompatibleWithFrozen(CollectionType<?> previous)
     {
         assert !isMultiCell;
@@ -146,7 +170,7 @@
         ByteBuffer bb1 = o1.duplicate();
         ByteBuffer bb2 = o2.duplicate();
 
-        int protocolVersion = Server.VERSION_3;
+        ProtocolVersion protocolVersion = ProtocolVersion.V3;
         int size1 = CollectionSerializer.readCollectionSize(bb1, protocolVersion);
         int size2 = CollectionSerializer.readCollectionSize(bb2, protocolVersion);
 
@@ -232,7 +256,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         ByteBuffer value = buffer.duplicate();
         StringBuilder sb = new StringBuilder("{");
diff --git a/src/java/org/apache/cassandra/db/marshal/PartitionerDefinedOrder.java b/src/java/org/apache/cassandra/db/marshal/PartitionerDefinedOrder.java
index 02f01ae..397c3be 100644
--- a/src/java/org/apache/cassandra/db/marshal/PartitionerDefinedOrder.java
+++ b/src/java/org/apache/cassandra/db/marshal/PartitionerDefinedOrder.java
@@ -26,6 +26,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
 
@@ -82,7 +83,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         throw new UnsupportedOperationException();
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/ReversedType.java b/src/java/org/apache/cassandra/db/marshal/ReversedType.java
index 82a1895..0eb0046 100644
--- a/src/java/org/apache/cassandra/db/marshal/ReversedType.java
+++ b/src/java/org/apache/cassandra/db/marshal/ReversedType.java
@@ -28,6 +28,7 @@
 import org.apache.cassandra.exceptions.SyntaxException;
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.serializers.TypeSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 public class ReversedType<T> extends AbstractType<T>
 {
@@ -94,7 +95,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return baseType.toJSONString(buffer, protocolVersion);
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/SetType.java b/src/java/org/apache/cassandra/db/marshal/SetType.java
index 22577b3..fdd29ec 100644
--- a/src/java/org/apache/cassandra/db/marshal/SetType.java
+++ b/src/java/org/apache/cassandra/db/marshal/SetType.java
@@ -28,6 +28,7 @@
 import org.apache.cassandra.exceptions.SyntaxException;
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.serializers.SetSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 public class SetType<T> extends CollectionType<Set<T>>
 {
@@ -105,6 +106,18 @@
     }
 
     @Override
+    public AbstractType<?> freezeNestedMulticellTypes()
+    {
+        if (!isMultiCell())
+            return this;
+
+        if (elements.isFreezable() && elements.isMultiCell())
+            return getInstance(elements.freeze(), isMultiCell);
+
+        return getInstance(elements.freezeNestedMulticellTypes(), isMultiCell);
+    }
+
+    @Override
     public boolean isCompatibleWithFrozen(CollectionType<?> previous)
     {
         assert !isMultiCell;
@@ -175,7 +188,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return ListType.setOrListToJsonString(buffer, elements, protocolVersion);
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/ShortType.java b/src/java/org/apache/cassandra/db/marshal/ShortType.java
index 22f134a..fe706d4 100644
--- a/src/java/org/apache/cassandra/db/marshal/ShortType.java
+++ b/src/java/org/apache/cassandra/db/marshal/ShortType.java
@@ -26,6 +26,7 @@
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.serializers.ShortSerializer;
 import org.apache.cassandra.serializers.TypeSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class ShortType extends AbstractType<Short>
@@ -76,7 +77,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return Objects.toString(getSerializer().deserialize(buffer), "\"\"");
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/SimpleDateType.java b/src/java/org/apache/cassandra/db/marshal/SimpleDateType.java
index 92b2dbd..9db5e36 100644
--- a/src/java/org/apache/cassandra/db/marshal/SimpleDateType.java
+++ b/src/java/org/apache/cassandra/db/marshal/SimpleDateType.java
@@ -25,6 +25,7 @@
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.serializers.SimpleDateSerializer;
 import org.apache.cassandra.serializers.TypeSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class SimpleDateType extends AbstractType<Integer>
@@ -69,7 +70,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return '"' + SimpleDateSerializer.instance.toString(SimpleDateSerializer.instance.deserialize(buffer)) + '"';
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/TimeType.java b/src/java/org/apache/cassandra/db/marshal/TimeType.java
index 8cd221e..99f4f67 100644
--- a/src/java/org/apache/cassandra/db/marshal/TimeType.java
+++ b/src/java/org/apache/cassandra/db/marshal/TimeType.java
@@ -25,7 +25,7 @@
 import org.apache.cassandra.cql3.CQL3Type;
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.MarshalException;
-import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Nanosecond resolution time values
@@ -60,7 +60,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return '"' + TimeSerializer.instance.toString(TimeSerializer.instance.deserialize(buffer)) + '"';
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/TimestampType.java b/src/java/org/apache/cassandra/db/marshal/TimestampType.java
index a2d92ba..08cab92 100644
--- a/src/java/org/apache/cassandra/db/marshal/TimestampType.java
+++ b/src/java/org/apache/cassandra/db/marshal/TimestampType.java
@@ -28,6 +28,7 @@
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.serializers.TimestampSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 /**
@@ -93,7 +94,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         return '"' + toString(TimestampSerializer.instance.deserialize(buffer)) + '"';
     }
diff --git a/src/java/org/apache/cassandra/db/marshal/TupleType.java b/src/java/org/apache/cassandra/db/marshal/TupleType.java
index 7ab6ffc..b87964e 100644
--- a/src/java/org/apache/cassandra/db/marshal/TupleType.java
+++ b/src/java/org/apache/cassandra/db/marshal/TupleType.java
@@ -22,13 +22,17 @@
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import com.google.common.base.Objects;
 
 import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.exceptions.SyntaxException;
 import org.apache.cassandra.serializers.*;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 /**
@@ -37,16 +41,31 @@
  */
 public class TupleType extends AbstractType<ByteBuffer>
 {
+    private static final String COLON = ":";
+    private static final Pattern COLON_PAT = Pattern.compile(COLON);
+    private static final String ESCAPED_COLON = "\\\\:";
+    private static final Pattern ESCAPED_COLON_PAT = Pattern.compile(ESCAPED_COLON);
+    private static final String AT = "@";
+    private static final Pattern AT_PAT = Pattern.compile(AT);
+    private static final String ESCAPED_AT = "\\\\@";
+    private static final Pattern ESCAPED_AT_PAT = Pattern.compile(ESCAPED_AT);
+    
     protected final List<AbstractType<?>> types;
 
     private final TupleSerializer serializer;
 
     public TupleType(List<AbstractType<?>> types)
     {
+        this(types, true);
+    }
+
+    protected TupleType(List<AbstractType<?>> types, boolean freezeInner)
+    {
         super(ComparisonType.CUSTOM);
-        for (int i = 0; i < types.size(); i++)
-            types.set(i, types.get(i).freeze());
-        this.types = types;
+        if (freezeInner)
+            this.types = types.stream().map(AbstractType::freeze).collect(Collectors.toList());
+        else
+            this.types = types;
         this.serializer = new TupleSerializer(fieldSerializers(types));
     }
 
@@ -73,6 +92,12 @@
         return allTypes().stream().anyMatch(f -> f.referencesUserType(name));
     }
 
+    @Override
+    public boolean referencesDuration()
+    {
+        return allTypes().stream().anyMatch(f -> f.referencesDuration());
+    }
+
     public AbstractType<?> type(int i)
     {
         return types.get(i);
@@ -155,8 +180,22 @@
                 return Arrays.copyOfRange(components, 0, i);
 
             int size = input.getInt();
+
+            if (input.remaining() < size)
+                throw new MarshalException(String.format("Not enough bytes to read %dth component", i));
+
+            // size < 0 means null value
             components[i] = size < 0 ? null : ByteBufferUtil.readBytes(input, size);
         }
+
+        // error out if we got more values in the tuple/UDT than we expected
+        if (input.hasRemaining())
+        {
+            throw new InvalidRequestException(String.format(
+                    "Expected %s %s for %s column, but got more",
+                    size(), size() == 1 ? "value" : "values", this.asCQL3Type()));
+        }
+
         return components;
     }
 
@@ -186,6 +225,9 @@
     @Override
     public String getString(ByteBuffer value)
     {
+        if (value == null)
+            return "null";
+
         StringBuilder sb = new StringBuilder();
         ByteBuffer input = value.duplicate();
         for (int i = 0; i < size(); i++)
@@ -206,7 +248,9 @@
 
             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("@", "\\\\@"));
+            String fld = COLON_PAT.matcher(type.getString(field)).replaceAll(ESCAPED_COLON);
+            fld = AT_PAT.matcher(fld).replaceAll(ESCAPED_AT);
+            sb.append(fld);
         }
         return sb.toString();
     }
@@ -229,7 +273,9 @@
                 continue;
 
             AbstractType<?> type = type(i);
-            fields[i] = type.fromString(fieldString.replaceAll("\\\\:", ":").replaceAll("\\\\@", "@"));
+            fieldString = ESCAPED_COLON_PAT.matcher(fieldString).replaceAll(COLON);
+            fieldString = ESCAPED_AT_PAT.matcher(fieldString).replaceAll(AT);
+            fields[i] = type.fromString(fieldString);
         }
         return buildValue(fields);
     }
@@ -270,7 +316,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         ByteBuffer duplicated = buffer.duplicate();
         StringBuilder sb = new StringBuilder("[");
@@ -352,6 +398,12 @@
     }
 
     @Override
+    public boolean isTuple()
+    {
+        return true;
+    }
+
+    @Override
     public CQL3Type asCQL3Type()
     {
         return CQL3Type.Tuple.create(this);
@@ -362,9 +414,4 @@
     {
         return getClass().getName() + TypeParser.stringifyTypeParameters(types, true);
     }
-
-    public boolean isTuple()
-    {
-        return true;
-    }
 }
diff --git a/src/java/org/apache/cassandra/db/marshal/TypeParser.java b/src/java/org/apache/cassandra/db/marshal/TypeParser.java
index 590eea3..7416d49 100644
--- a/src/java/org/apache/cassandra/db/marshal/TypeParser.java
+++ b/src/java/org/apache/cassandra/db/marshal/TypeParser.java
@@ -26,6 +26,7 @@
 import com.google.common.base.Verify;
 import com.google.common.collect.ImmutableMap;
 
+import org.apache.cassandra.cql3.FieldIdentifier;
 import org.apache.cassandra.exceptions.*;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
@@ -560,17 +561,17 @@
         return sb.toString();
     }
 
-    public static String stringifyUserTypeParameters(String keysace, ByteBuffer typeName, List<ByteBuffer> columnNames, List<AbstractType<?>> columnTypes)
+    public static String stringifyUserTypeParameters(String keysace, ByteBuffer typeName, List<FieldIdentifier> fields,
+                                                     List<AbstractType<?>> columnTypes, boolean ignoreFreezing)
     {
         StringBuilder sb = new StringBuilder();
         sb.append('(').append(keysace).append(",").append(ByteBufferUtil.bytesToHex(typeName));
 
-        for (int i = 0; i < columnNames.size(); i++)
+        for (int i = 0; i < fields.size(); i++)
         {
             sb.append(',');
-            sb.append(ByteBufferUtil.bytesToHex(columnNames.get(i))).append(":");
-            // omit FrozenType(...) from fields because it is currently implicit
-            sb.append(columnTypes.get(i).toString(true));
+            sb.append(ByteBufferUtil.bytesToHex(fields.get(i).bytes)).append(":");
+            sb.append(columnTypes.get(i).toString(ignoreFreezing));
         }
         sb.append(')');
         return sb.toString();
diff --git a/src/java/org/apache/cassandra/db/marshal/UTF8Type.java b/src/java/org/apache/cassandra/db/marshal/UTF8Type.java
index 7c18ce5..1da6629 100644
--- a/src/java/org/apache/cassandra/db/marshal/UTF8Type.java
+++ b/src/java/org/apache/cassandra/db/marshal/UTF8Type.java
@@ -29,6 +29,7 @@
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.UTF8Serializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class UTF8Type extends AbstractType<String>
@@ -58,7 +59,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         try
         {
diff --git a/src/java/org/apache/cassandra/db/marshal/UUIDType.java b/src/java/org/apache/cassandra/db/marshal/UUIDType.java
index acaf27c..9722a52 100644
--- a/src/java/org/apache/cassandra/db/marshal/UUIDType.java
+++ b/src/java/org/apache/cassandra/db/marshal/UUIDType.java
@@ -140,7 +140,7 @@
         {
             try
             {
-                return ByteBuffer.wrap(UUIDGen.decompose(UUID.fromString(source)));
+                return UUIDGen.toByteBuffer(UUID.fromString(source));
             }
             catch (IllegalArgumentException e)
             {
diff --git a/src/java/org/apache/cassandra/db/marshal/UserType.java b/src/java/org/apache/cassandra/db/marshal/UserType.java
index 03545ca..febd91c 100644
--- a/src/java/org/apache/cassandra/db/marshal/UserType.java
+++ b/src/java/org/apache/cassandra/db/marshal/UserType.java
@@ -18,20 +18,24 @@
 package org.apache.cassandra.db.marshal;
 
 import java.nio.ByteBuffer;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.StandardCharsets;
 import java.util.*;
+import java.util.stream.Collectors;
 
 import com.google.common.base.Objects;
 
 import org.apache.cassandra.cql3.*;
+import org.apache.cassandra.db.rows.Cell;
+import org.apache.cassandra.db.rows.CellPath;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.SyntaxException;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.serializers.UserTypeSerializer;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.Pair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * A user defined type.
@@ -40,34 +44,31 @@
  */
 public class UserType extends TupleType
 {
+    private static final Logger logger = LoggerFactory.getLogger(UserType.class);
+
     public final String keyspace;
     public final ByteBuffer name;
-    private final List<ByteBuffer> fieldNames;
+    private final List<FieldIdentifier> fieldNames;
     private final List<String> stringFieldNames;
+    private final boolean isMultiCell;
     private final UserTypeSerializer serializer;
 
-    public UserType(String keyspace, ByteBuffer name, List<ByteBuffer> fieldNames, List<AbstractType<?>> fieldTypes)
+    public UserType(String keyspace, ByteBuffer name, List<FieldIdentifier> fieldNames, List<AbstractType<?>> fieldTypes, boolean isMultiCell)
     {
-        super(fieldTypes);
+        super(fieldTypes, false);
         assert fieldNames.size() == fieldTypes.size();
         this.keyspace = keyspace;
         this.name = name;
         this.fieldNames = fieldNames;
         this.stringFieldNames = new ArrayList<>(fieldNames.size());
+        this.isMultiCell = isMultiCell;
+
         LinkedHashMap<String , TypeSerializer<?>> fieldSerializers = new LinkedHashMap<>(fieldTypes.size());
         for (int i = 0, m = fieldNames.size(); i < m; i++)
         {
-            ByteBuffer fieldName = fieldNames.get(i);
-            try
-            {
-                String stringFieldName = ByteBufferUtil.string(fieldName, StandardCharsets.UTF_8);
-                stringFieldNames.add(stringFieldName);
-                fieldSerializers.put(stringFieldName, fieldTypes.get(i).getSerializer());
-            }
-            catch (CharacterCodingException ex)
-            {
-                throw new AssertionError("Got non-UTF8 field name for user-defined type: " + ByteBufferUtil.bytesToHex(fieldName), ex);
-            }
+            String stringFieldName = fieldNames.get(i).toString();
+            stringFieldNames.add(stringFieldName);
+            fieldSerializers.put(stringFieldName, fieldTypes.get(i).getSerializer());
         }
         this.serializer = new UserTypeSerializer(fieldSerializers);
     }
@@ -77,14 +78,33 @@
         Pair<Pair<String, ByteBuffer>, List<Pair<ByteBuffer, AbstractType>>> params = parser.getUserTypeParameters();
         String keyspace = params.left.left;
         ByteBuffer name = params.left.right;
-        List<ByteBuffer> columnNames = new ArrayList<>(params.right.size());
+        List<FieldIdentifier> columnNames = new ArrayList<>(params.right.size());
         List<AbstractType<?>> columnTypes = new ArrayList<>(params.right.size());
         for (Pair<ByteBuffer, AbstractType> p : params.right)
         {
-            columnNames.add(p.left);
-            columnTypes.add(p.right.freeze());
+            columnNames.add(new FieldIdentifier(p.left));
+            columnTypes.add(p.right);
         }
-        return new UserType(keyspace, name, columnNames, columnTypes);
+
+        return new UserType(keyspace, name, columnNames, columnTypes, true);
+    }
+
+    @Override
+    public boolean isUDT()
+    {
+        return true;
+    }
+
+    @Override
+    public boolean isMultiCell()
+    {
+        return isMultiCell;
+    }
+
+    @Override
+    public boolean isFreezable()
+    {
+        return true;
     }
 
     public AbstractType<?> fieldType(int i)
@@ -97,7 +117,7 @@
         return types;
     }
 
-    public ByteBuffer fieldName(int i)
+    public FieldIdentifier fieldName(int i)
     {
         return fieldNames.get(i);
     }
@@ -107,7 +127,7 @@
         return stringFieldNames.get(i);
     }
 
-    public List<ByteBuffer> fieldNames()
+    public List<FieldIdentifier> fieldNames()
     {
         return fieldNames;
     }
@@ -117,6 +137,62 @@
         return UTF8Type.instance.compose(name);
     }
 
+    public int fieldPosition(FieldIdentifier fieldName)
+    {
+        return fieldNames.indexOf(fieldName);
+    }
+
+    public CellPath cellPathForField(FieldIdentifier fieldName)
+    {
+        // we use the field position instead of the field name to allow for field renaming in ALTER TYPE statements
+        return CellPath.create(ByteBufferUtil.bytes((short)fieldPosition(fieldName)));
+    }
+
+    public ShortType nameComparator()
+    {
+        return ShortType.instance;
+    }
+
+    public ByteBuffer serializeForNativeProtocol(Iterator<Cell> cells, ProtocolVersion protocolVersion)
+    {
+        assert isMultiCell;
+
+        ByteBuffer[] components = new ByteBuffer[size()];
+        short fieldPosition = 0;
+        while (cells.hasNext())
+        {
+            Cell cell = cells.next();
+
+            // handle null fields that aren't at the end
+            short fieldPositionOfCell = ByteBufferUtil.toShort(cell.path().get(0));
+            while (fieldPosition < fieldPositionOfCell)
+                components[fieldPosition++] = null;
+
+            components[fieldPosition++] = cell.value();
+        }
+
+        // append trailing nulls for missing cells
+        while (fieldPosition < size())
+            components[fieldPosition++] = null;
+
+        return TupleType.buildValue(components);
+    }
+
+    public void validateCell(Cell cell) throws MarshalException
+    {
+        if (isMultiCell)
+        {
+            ByteBuffer path = cell.path().get(0);
+            nameComparator().validate(path);
+            Short fieldPosition = nameComparator().getSerializer().deserialize(path);
+            fieldType(fieldPosition).validate(cell.value());
+        }
+        else
+        {
+            validate(cell.value());
+        }
+    }
+
     @Override
     public Term fromJSONObject(Object parsed) throws MarshalException
     {
@@ -166,7 +242,7 @@
     }
 
     @Override
-    public String toJSONString(ByteBuffer buffer, int protocolVersion)
+    public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
         ByteBuffer[] buffers = split(buffer);
         StringBuilder sb = new StringBuilder("{");
@@ -193,19 +269,93 @@
     }
 
     @Override
+    public UserType freeze()
+    {
+        if (isMultiCell)
+            return new UserType(keyspace, name, fieldNames, fieldTypes(), false);
+        else
+            return this;
+    }
+
+    @Override
+    public AbstractType<?> freezeNestedMulticellTypes()
+    {
+        if (!isMultiCell())
+            return this;
+
+        // the behavior here doesn't exactly match the method name: we want to freeze everything inside of UDTs
+        List<AbstractType<?>> newTypes = fieldTypes().stream()
+                .map(subtype -> (subtype.isFreezable() && subtype.isMultiCell() ? subtype.freeze() : subtype))
+                .collect(Collectors.toList());
+
+        return new UserType(keyspace, name, fieldNames, newTypes, isMultiCell);
+    }
+
+    @Override
     public int hashCode()
     {
-        return Objects.hashCode(keyspace, name, fieldNames, types);
+        return Objects.hashCode(keyspace, name, fieldNames, types, isMultiCell);
+    }
+
+    @Override
+    public boolean isValueCompatibleWith(AbstractType<?> previous)
+    {
+        if (this == previous)
+            return true;
+
+        if (!(previous instanceof UserType))
+            return false;
+
+        UserType other = (UserType) previous;
+        if (isMultiCell != other.isMultiCell())
+            return false;
+
+        if (!keyspace.equals(other.keyspace))
+            return false;
+
+        Iterator<AbstractType<?>> thisTypeIter = types.iterator();
+        Iterator<AbstractType<?>> previousTypeIter = other.types.iterator();
+        while (thisTypeIter.hasNext() && previousTypeIter.hasNext())
+        {
+            if (!thisTypeIter.next().isCompatibleWith(previousTypeIter.next()))
+                return false;
+        }
+
+        // it's okay for the new type to have additional fields, but not for the old type to have additional fields
+        return !previousTypeIter.hasNext();
     }
 
     @Override
     public boolean equals(Object o)
     {
+        return o instanceof UserType && equals(o, false);
+    }
+
+    @Override
+    public boolean equals(Object o, boolean ignoreFreezing)
+    {
         if(!(o instanceof UserType))
             return false;
 
         UserType that = (UserType)o;
-        return keyspace.equals(that.keyspace) && name.equals(that.name) && fieldNames.equals(that.fieldNames) && types.equals(that.types);
+
+        if (!keyspace.equals(that.keyspace) || !name.equals(that.name) || !fieldNames.equals(that.fieldNames))
+            return false;
+
+        if (!ignoreFreezing && isMultiCell != that.isMultiCell)
+            return false;
+
+        if (this.types.size() != that.types.size())
+            return false;
+
+        Iterator<AbstractType<?>> otherTypeIter = that.types.iterator();
+        for (AbstractType<?> type : types)
+        {
+            if (!type.equals(otherTypeIter.next(), ignoreFreezing))
+                return false;
+        }
+
+        return true;
     }
 
     @Override
@@ -222,9 +372,36 @@
     }
 
     @Override
+    public boolean referencesDuration()
+    {
+        return fieldTypes().stream().anyMatch(f -> f.referencesDuration());
+    }
+
+    @Override
     public String toString()
     {
-        return getClass().getName() + TypeParser.stringifyUserTypeParameters(keyspace, name, fieldNames, types);
+        return this.toString(false);
+    }
+
+    @Override
+    public boolean isTuple()
+    {
+        return false;
+    }
+
+    @Override
+    public String toString(boolean ignoreFreezing)
+    {
+        boolean includeFrozenType = !ignoreFreezing && !isMultiCell();
+
+        StringBuilder sb = new StringBuilder();
+        if (includeFrozenType)
+            sb.append(FrozenType.class.getName()).append("(");
+        sb.append(getClass().getName());
+        sb.append(TypeParser.stringifyUserTypeParameters(keyspace, name, fieldNames, types, ignoreFreezing || !isMultiCell));
+        if (includeFrozenType)
+            sb.append(")");
+        return sb.toString();
     }
 
     @Override
@@ -232,14 +409,4 @@
     {
         return serializer;
     }
-
-    public boolean isTuple()
-    {
-        return false;
-    }
-
-    public boolean isUDT()
-    {
-        return true;
-    }
 }
diff --git a/src/java/org/apache/cassandra/db/monitoring/ApproximateTime.java b/src/java/org/apache/cassandra/db/monitoring/ApproximateTime.java
new file mode 100644
index 0000000..cc4b410
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/monitoring/ApproximateTime.java
@@ -0,0 +1,61 @@
+/*
+ * 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.db.monitoring;
+
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.config.Config;
+
+/**
+ * This is an approximation of System.currentTimeInMillis(). It updates its
+ * time value at periodic intervals of CHECK_INTERVAL_MS milliseconds
+ * (currently 10 milliseconds by default). It can be used as a faster alternative
+ * to System.currentTimeInMillis() every time an imprecision of a few milliseconds
+ * can be accepted.
+ */
+public class ApproximateTime
+{
+    private static final Logger logger = LoggerFactory.getLogger(ApproximateTime.class);
+    private static final int CHECK_INTERVAL_MS = Math.max(5, Integer.parseInt(System.getProperty(Config.PROPERTY_PREFIX + "approximate_time_precision_ms", "10")));
+
+    private static volatile long time = System.currentTimeMillis();
+    static
+    {
+        logger.info("Scheduling approximate time-check task with a precision of {} milliseconds", CHECK_INTERVAL_MS);
+        ScheduledExecutors.scheduledFastTasks.scheduleWithFixedDelay(() -> time = System.currentTimeMillis(),
+                                                                     CHECK_INTERVAL_MS,
+                                                                     CHECK_INTERVAL_MS,
+                                                                     TimeUnit.MILLISECONDS);
+    }
+
+    public static long currentTimeMillis()
+    {
+        return time;
+    }
+
+    public static long precision()
+    {
+        return 2 * CHECK_INTERVAL_MS;
+    }
+
+}
diff --git a/src/java/org/apache/cassandra/db/monitoring/Monitorable.java b/src/java/org/apache/cassandra/db/monitoring/Monitorable.java
new file mode 100644
index 0000000..c9bf94e
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/monitoring/Monitorable.java
@@ -0,0 +1,36 @@
+/*
+ * 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.db.monitoring;
+
+public interface Monitorable
+{
+    String name();
+    long constructionTime();
+    long timeout();
+    long slowTimeout();
+
+    boolean isInProgress();
+    boolean isAborted();
+    boolean isCompleted();
+    boolean isSlow();
+    boolean isCrossNode();
+
+    boolean abort();
+    boolean complete();
+}
diff --git a/src/java/org/apache/cassandra/db/monitoring/MonitorableImpl.java b/src/java/org/apache/cassandra/db/monitoring/MonitorableImpl.java
new file mode 100644
index 0000000..48c8152
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/monitoring/MonitorableImpl.java
@@ -0,0 +1,135 @@
+/*
+ * 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.db.monitoring;
+
+public abstract class MonitorableImpl implements Monitorable
+{
+    private MonitoringState state;
+    private boolean isSlow;
+    private long constructionTime = -1;
+    private long timeout;
+    private long slowTimeout;
+    private boolean isCrossNode;
+
+    protected MonitorableImpl()
+    {
+        this.state = MonitoringState.IN_PROGRESS;
+        this.isSlow = false;
+    }
+
+    /**
+     * This setter is ugly but the construction chain to ReadCommand
+     * is too complex, it would require passing new parameters to all serializers
+     * or specializing the serializers to accept these message properties.
+     */
+    public void setMonitoringTime(long constructionTime, boolean isCrossNode, long timeout, long slowTimeout)
+    {
+        assert constructionTime >= 0;
+        this.constructionTime = constructionTime;
+        this.isCrossNode = isCrossNode;
+        this.timeout = timeout;
+        this.slowTimeout = slowTimeout;
+    }
+
+    public long constructionTime()
+    {
+        return constructionTime;
+    }
+
+    public long timeout()
+    {
+        return timeout;
+    }
+
+    public boolean isCrossNode()
+    {
+        return isCrossNode;
+    }
+
+    public long slowTimeout()
+    {
+        return slowTimeout;
+    }
+
+    public boolean isInProgress()
+    {
+        check();
+        return state == MonitoringState.IN_PROGRESS;
+    }
+
+    public boolean isAborted()
+    {
+        check();
+        return state == MonitoringState.ABORTED;
+    }
+
+    public boolean isCompleted()
+    {
+        check();
+        return state == MonitoringState.COMPLETED;
+    }
+
+    public boolean isSlow()
+    {
+        check();
+        return isSlow;
+    }
+
+    public boolean abort()
+    {
+        if (state == MonitoringState.IN_PROGRESS)
+        {
+            if (constructionTime >= 0)
+                MonitoringTask.addFailedOperation(this, ApproximateTime.currentTimeMillis());
+
+            state = MonitoringState.ABORTED;
+            return true;
+        }
+
+        return state == MonitoringState.ABORTED;
+    }
+
+    public boolean complete()
+    {
+        if (state == MonitoringState.IN_PROGRESS)
+        {
+            if (isSlow && slowTimeout > 0 && constructionTime >= 0)
+                MonitoringTask.addSlowOperation(this, ApproximateTime.currentTimeMillis());
+
+            state = MonitoringState.COMPLETED;
+            return true;
+        }
+
+        return state == MonitoringState.COMPLETED;
+    }
+
+    private void check()
+    {
+        if (constructionTime < 0 || state != MonitoringState.IN_PROGRESS)
+            return;
+
+        long elapsed = ApproximateTime.currentTimeMillis() - constructionTime;
+
+        if (elapsed >= slowTimeout && !isSlow)
+            isSlow = true;
+
+        if (elapsed >= timeout)
+            abort();
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/monitoring/MonitoringState.java b/src/java/org/apache/cassandra/db/monitoring/MonitoringState.java
new file mode 100644
index 0000000..4fe3cf8
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/monitoring/MonitoringState.java
@@ -0,0 +1,26 @@
+/*
+ * 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.db.monitoring;
+
+public enum MonitoringState
+{
+    IN_PROGRESS,
+    ABORTED,
+    COMPLETED
+}
diff --git a/src/java/org/apache/cassandra/db/monitoring/MonitoringTask.java b/src/java/org/apache/cassandra/db/monitoring/MonitoringTask.java
new file mode 100644
index 0000000..9426042
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/monitoring/MonitoringTask.java
@@ -0,0 +1,415 @@
+/*
+ * 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.db.monitoring;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.utils.NoSpamLogger;
+
+import static java.lang.System.getProperty;
+
+/**
+ * A task for monitoring in progress operations, currently only read queries, and aborting them if they time out.
+ * We also log timed out operations, see CASSANDRA-7392.
+ * Since CASSANDRA-12403 we also log queries that were slow.
+ */
+class MonitoringTask
+{
+    private static final String LINE_SEPARATOR = getProperty("line.separator");
+    private static final Logger logger = LoggerFactory.getLogger(MonitoringTask.class);
+    private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 5L, TimeUnit.MINUTES);
+
+    /**
+     * Defines the interval for reporting any operations that have timed out.
+     */
+    private static final int REPORT_INTERVAL_MS = Math.max(0, Integer.parseInt(System.getProperty(Config.PROPERTY_PREFIX + "monitoring_report_interval_ms", "5000")));
+
+    /**
+     * Defines the maximum number of unique timed out queries that will be reported in the logs.
+     * Use a negative number to remove any limit.
+     */
+    private static final int MAX_OPERATIONS = Integer.parseInt(System.getProperty(Config.PROPERTY_PREFIX + "monitoring_max_operations", "50"));
+
+    @VisibleForTesting
+    static MonitoringTask instance = make(REPORT_INTERVAL_MS, MAX_OPERATIONS);
+
+    private final ScheduledFuture<?> reportingTask;
+    private final OperationsQueue failedOperationsQueue;
+    private final OperationsQueue slowOperationsQueue;
+    private long lastLogTime;
+
+
+    @VisibleForTesting
+    static MonitoringTask make(int reportIntervalMillis, int maxTimedoutOperations)
+    {
+        if (instance != null)
+        {
+            instance.cancel();
+            instance = null;
+        }
+
+        return new MonitoringTask(reportIntervalMillis, maxTimedoutOperations);
+    }
+
+    private MonitoringTask(int reportIntervalMillis, int maxOperations)
+    {
+        this.failedOperationsQueue = new OperationsQueue(maxOperations);
+        this.slowOperationsQueue = new OperationsQueue(maxOperations);
+
+        this.lastLogTime = ApproximateTime.currentTimeMillis();
+
+        logger.info("Scheduling monitoring task with report interval of {} ms, max operations {}", reportIntervalMillis, maxOperations);
+        this.reportingTask = ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(() -> logOperations(ApproximateTime.currentTimeMillis()),
+                                                                                     reportIntervalMillis,
+                                                                                     reportIntervalMillis,
+                                                                                     TimeUnit.MILLISECONDS);
+    }
+
+    public void cancel()
+    {
+        reportingTask.cancel(false);
+    }
+
+    static void addFailedOperation(Monitorable operation, long now)
+    {
+        instance.failedOperationsQueue.offer(new FailedOperation(operation, now));
+    }
+
+    static void addSlowOperation(Monitorable operation, long now)
+    {
+        instance.slowOperationsQueue.offer(new SlowOperation(operation, now));
+    }
+
+    @VisibleForTesting
+    List<String> getFailedOperations()
+    {
+        return getLogMessages(failedOperationsQueue.popOperations());
+    }
+
+    @VisibleForTesting
+    List<String> getSlowOperations()
+    {
+        return getLogMessages(slowOperationsQueue.popOperations());
+    }
+
+    private List<String> getLogMessages(AggregatedOperations operations)
+    {
+        String ret = operations.getLogMessage();
+        return ret.isEmpty() ? Collections.emptyList() : Arrays.asList(ret.split("\n"));
+    }
+
+    @VisibleForTesting
+    private void logOperations(long now)
+    {
+        logSlowOperations(now);
+        logFailedOperations(now);
+
+        lastLogTime = now;
+    }
+
+    @VisibleForTesting
+    boolean logFailedOperations(long now)
+    {
+        AggregatedOperations failedOperations = failedOperationsQueue.popOperations();
+        if (!failedOperations.isEmpty())
+        {
+            long elapsed = now - lastLogTime;
+            noSpamLogger.warn("Some operations timed out, details available at debug level (debug.log)");
+
+            if (logger.isDebugEnabled())
+                logger.debug("{} operations timed out in the last {} msecs:{}{}",
+                            failedOperations.num(),
+                            elapsed,
+                            LINE_SEPARATOR,
+                            failedOperations.getLogMessage());
+            return true;
+        }
+
+        return false;
+    }
+
+    @VisibleForTesting
+    boolean logSlowOperations(long now)
+    {
+        AggregatedOperations slowOperations = slowOperationsQueue.popOperations();
+        if (!slowOperations.isEmpty())
+        {
+            long elapsed = now - lastLogTime;
+            noSpamLogger.info("Some operations were slow, details available at debug level (debug.log)");
+
+            if (logger.isDebugEnabled())
+                logger.debug("{} operations were slow in the last {} msecs:{}{}",
+                             slowOperations.num(),
+                             elapsed,
+                             LINE_SEPARATOR,
+                             slowOperations.getLogMessage());
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * A wrapper for a queue that can be either bounded, in which case
+     * we increment a counter if we exceed the queue size, or unbounded.
+     */
+    private static final class OperationsQueue
+    {
+        /** The max operations on the queue. If this value is zero then logging is disabled
+         * and the queue will always be empty. If this value is negative then the queue is unbounded.
+         */
+        private final int maxOperations;
+
+        /**
+         * The operations queue, it can be either bounded or unbounded depending on the value of maxOperations.
+         */
+        private final BlockingQueue<Operation> queue;
+
+        /**
+         * If we fail to add an operation to the queue then we increment this value. We reset this value
+         * when the queue is emptied.
+         */
+        private final AtomicLong numDroppedOperations;
+
+        OperationsQueue(int maxOperations)
+        {
+            this.maxOperations = maxOperations;
+            this.queue = maxOperations > 0 ? new ArrayBlockingQueue<>(maxOperations) : new LinkedBlockingQueue<>();
+            this.numDroppedOperations = new AtomicLong();
+        }
+
+        /**
+         * Add an operation to the queue, if possible, or increment the dropped counter.
+         *
+         * @param operation - the operations to add
+         */
+        private void offer(Operation operation)
+        {
+            if (maxOperations == 0)
+                return; // logging of operations is disabled
+
+            if (!queue.offer(operation))
+                numDroppedOperations.incrementAndGet();
+        }
+
+
+        /**
+         * Return all operations in the queue, aggregated by name, and reset
+         * the counter for dropped operations.
+         *
+         * @return - the aggregated operations
+         */
+        private AggregatedOperations popOperations()
+        {
+            Map<String, Operation> operations = new HashMap<>();
+
+            Operation operation;
+            while((operation = queue.poll()) != null)
+            {
+                Operation existing = operations.get(operation.name());
+                if (existing != null)
+                    existing.add(operation);
+                else
+                    operations.put(operation.name(), operation);
+            }
+            return new AggregatedOperations(operations, numDroppedOperations.getAndSet(0L));
+        }
+    }
+
+    /**
+     * Convert a map of aggregated operations into a log message that
+     * includes the information of whether some operations were dropped.
+     */
+    private static final class AggregatedOperations
+    {
+        private final Map<String, Operation> operations;
+        private final long numDropped;
+
+        AggregatedOperations(Map<String, Operation> operations, long numDropped)
+        {
+            this.operations = operations;
+            this.numDropped = numDropped;
+        }
+
+        public boolean isEmpty()
+        {
+            return operations.isEmpty() && numDropped == 0;
+        }
+
+        public long num()
+        {
+            return operations.size() + numDropped;
+        }
+
+        String getLogMessage()
+        {
+            if (isEmpty())
+                return "";
+
+            final StringBuilder ret = new StringBuilder();
+            operations.values().forEach(o -> addOperation(ret, o));
+
+            if (numDropped > 0)
+                ret.append(LINE_SEPARATOR)
+                   .append("... (")
+                   .append(numDropped)
+                   .append(" were dropped)");
+
+            return ret.toString();
+        }
+
+        private static void addOperation(StringBuilder ret, Operation operation)
+        {
+            if (ret.length() > 0)
+                ret.append(LINE_SEPARATOR);
+
+            ret.append(operation.getLogMessage());
+        }
+    }
+
+    /**
+     * A wrapper class for an operation that either failed (timed-out) or
+     * was reported as slow. Because the same operation (query) may execute
+     * multiple times, we aggregate the number of times an operation with the
+     * same name (CQL query text) is reported and store the average, min and max
+     * times.
+     */
+    protected abstract static class Operation
+    {
+        /** The operation that was reported as slow or timed out */
+        final Monitorable operation;
+
+        /** The number of times the operation was reported */
+        int numTimesReported;
+
+        /** The total time spent by this operation */
+        long totalTime;
+
+        /** The maximum time spent by this operation */
+        long maxTime;
+
+        /** The minimum time spent by this operation */
+        long minTime;
+
+        /** The name of the operation, i.e. the SELECT query CQL,
+         * this is set lazily as it takes time to build the query CQL */
+        private String name;
+
+        Operation(Monitorable operation, long failedAt)
+        {
+            this.operation = operation;
+            numTimesReported = 1;
+            totalTime = failedAt - operation.constructionTime();
+            minTime = totalTime;
+            maxTime = totalTime;
+        }
+
+        public String name()
+        {
+            if (name == null)
+                name = operation.name();
+            return name;
+        }
+
+        void add(Operation operation)
+        {
+            numTimesReported++;
+            totalTime += operation.totalTime;
+            maxTime = Math.max(maxTime, operation.maxTime);
+            minTime = Math.min(minTime, operation.minTime);
+        }
+
+        public abstract String getLogMessage();
+    }
+
+    /**
+     * An operation (query) that timed out.
+     */
+    private final static class FailedOperation extends Operation
+    {
+        FailedOperation(Monitorable operation, long failedAt)
+        {
+            super(operation, failedAt);
+        }
+
+        public String getLogMessage()
+        {
+            if (numTimesReported == 1)
+                return String.format("<%s>, total time %d msec, timeout %d %s",
+                                     name(),
+                                     totalTime,
+                                     operation.timeout(),
+                                     operation.isCrossNode() ? "msec/cross-node" : "msec");
+            else
+                return String.format("<%s> timed out %d times, avg/min/max %d/%d/%d msec, timeout %d %s",
+                                     name(),
+                                     numTimesReported,
+                                     totalTime / numTimesReported,
+                                     minTime,
+                                     maxTime,
+                                     operation.timeout(),
+                                     operation.isCrossNode() ? "msec/cross-node" : "msec");
+        }
+    }
+
+    /**
+     * An operation (query) that was reported as slow.
+     */
+    private final static class SlowOperation extends Operation
+    {
+        SlowOperation(Monitorable operation, long failedAt)
+        {
+            super(operation, failedAt);
+        }
+
+        public String getLogMessage()
+        {
+            if (numTimesReported == 1)
+                return String.format("<%s>, time %d msec - slow timeout %d %s",
+                                     name(),
+                                     totalTime,
+                                     operation.slowTimeout(),
+                                     operation.isCrossNode() ? "msec/cross-node" : "msec");
+            else
+                return String.format("<%s>, was slow %d times: avg/min/max %d/%d/%d msec - slow timeout %d %s",
+                                     name(),
+                                     numTimesReported,
+                                     totalTime / numTimesReported,
+                                     minTime,
+                                     maxTime,
+                                     operation.slowTimeout(),
+                                     operation.isCrossNode() ? "msec/cross-node" : "msec");
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/partitions/AbstractBTreePartition.java b/src/java/org/apache/cassandra/db/partitions/AbstractBTreePartition.java
index 2cd9e97..d9ad3fe 100644
--- a/src/java/org/apache/cassandra/db/partitions/AbstractBTreePartition.java
+++ b/src/java/org/apache/cassandra/db/partitions/AbstractBTreePartition.java
@@ -171,7 +171,7 @@
 
     public UnfilteredRowIterator unfilteredIterator()
     {
-        return unfilteredIterator(ColumnFilter.all(metadata()), Slices.ALL, false);
+        return unfilteredIterator(ColumnFilter.selection(columns()), Slices.ALL, false);
     }
 
     public UnfilteredRowIterator unfilteredIterator(ColumnFilter selection, Slices slices, boolean reversed)
@@ -185,7 +185,7 @@
         if (slices.size() == 0)
         {
             DeletionTime partitionDeletion = current.deletionInfo.getPartitionDeletion();
-            return UnfilteredRowIterators.noRowsIterator(metadata, partitionKey, staticRow, partitionDeletion, reversed);
+            return UnfilteredRowIterators.noRowsIterator(metadata, partitionKey(), staticRow, partitionDeletion, reversed);
         }
 
         return slices.size() == 1
@@ -195,18 +195,17 @@
 
     private UnfilteredRowIterator sliceIterator(ColumnFilter selection, Slice slice, boolean reversed, Holder current, Row staticRow)
     {
-        Slice.Bound start = slice.start() == Slice.Bound.BOTTOM ? null : slice.start();
-        Slice.Bound end = slice.end() == Slice.Bound.TOP ? null : slice.end();
+        ClusteringBound start = slice.start() == ClusteringBound.BOTTOM ? null : slice.start();
+        ClusteringBound end = slice.end() == ClusteringBound.TOP ? null : slice.end();
         Iterator<Row> rowIter = BTree.slice(current.tree, metadata.comparator, start, true, end, true, desc(reversed));
         Iterator<RangeTombstone> deleteIter = current.deletionInfo.rangeIterator(slice, reversed);
-
         return merge(rowIter, deleteIter, selection, reversed, current, staticRow);
     }
 
     private RowAndDeletionMergeIterator merge(Iterator<Row> rowIter, Iterator<RangeTombstone> deleteIter,
-                                                     ColumnFilter selection, boolean reversed, Holder current, Row staticRow)
+                                              ColumnFilter selection, boolean reversed, Holder current, Row staticRow)
     {
-        return new RowAndDeletionMergeIterator(metadata, partitionKey, current.deletionInfo.getPartitionDeletion(),
+        return new RowAndDeletionMergeIterator(metadata, partitionKey(), current.deletionInfo.getPartitionDeletion(),
                                                selection, staticRow, reversed, current.stats,
                                                rowIter, deleteIter,
                                                canHaveShadowedData());
@@ -217,22 +216,10 @@
         final Holder current;
         final ColumnFilter selection;
 
-        private AbstractIterator(ColumnFilter selection, boolean isReversed)
-        {
-            this(AbstractBTreePartition.this.holder(), selection, isReversed);
-        }
-
-        private AbstractIterator(Holder current, ColumnFilter selection, boolean isReversed)
-        {
-            this(current,
-                 AbstractBTreePartition.this.staticRow(current, selection, false),
-                 selection, isReversed);
-        }
-
         private AbstractIterator(Holder current, Row staticRow, ColumnFilter selection, boolean isReversed)
         {
             super(AbstractBTreePartition.this.metadata,
-                  AbstractBTreePartition.this.partitionKey,
+                  AbstractBTreePartition.this.partitionKey(),
                   current.deletionInfo.getPartitionDeletion(),
                   selection.fetchedColumns(), // non-selected columns will be filtered in subclasses by RowAndDeletionMergeIterator
                                               // it would also be more precise to return the intersection of the selection and current.columns,
@@ -284,40 +271,6 @@
         }
     }
 
-    public class SliceableIterator extends AbstractIterator implements SliceableUnfilteredRowIterator
-    {
-        private Iterator<Unfiltered> iterator;
-
-        protected SliceableIterator(ColumnFilter selection, boolean isReversed)
-        {
-            super(selection, isReversed);
-        }
-
-        protected Unfiltered computeNext()
-        {
-            if (iterator == null)
-                iterator = unfilteredIterator(selection, Slices.ALL, isReverseOrder);
-            if (!iterator.hasNext())
-                return endOfData();
-            return iterator.next();
-        }
-
-        public Iterator<Unfiltered> slice(Slice slice)
-        {
-            return sliceIterator(selection, slice, isReverseOrder, current, staticRow);
-        }
-    }
-
-    public SliceableUnfilteredRowIterator sliceableUnfilteredIterator(ColumnFilter columns, boolean reversed)
-    {
-        return new SliceableIterator(columns, reversed);
-    }
-
-    protected SliceableUnfilteredRowIterator sliceableUnfilteredIterator()
-    {
-        return sliceableUnfilteredIterator(ColumnFilter.all(metadata), false);
-    }
-
     protected static Holder build(UnfilteredRowIterator iterator, int initialRowCapacity)
     {
         return build(iterator, initialRowCapacity, true, null);
@@ -325,8 +278,12 @@
 
     protected static Holder build(UnfilteredRowIterator iterator, int initialRowCapacity, boolean ordered, BTree.Builder.QuickResolver<Row> quickResolver)
     {
+        return build(iterator, initialRowCapacity, ordered, quickResolver, iterator.columns());
+    }
+
+    protected static Holder build(UnfilteredRowIterator iterator, int initialRowCapacity, boolean ordered, BTree.Builder.QuickResolver<Row> quickResolver, PartitionColumns columns)
+    {
         CFMetaData metadata = iterator.metadata();
-        PartitionColumns columns = iterator.columns();
         boolean reversed = iterator.isReverseOrder();
 
         BTree.Builder<Row> builder = BTree.builder(metadata.comparator, initialRowCapacity);
@@ -360,10 +317,7 @@
         BTree.Builder<Row> builder = BTree.builder(metadata.comparator, initialRowCapacity);
         builder.auto(false);
         while (rows.hasNext())
-        {
-            Row row = rows.next();
-            builder.add(row);
-        }
+            builder.add(rows.next());
 
         if (reversed)
             builder.reverse();
diff --git a/src/java/org/apache/cassandra/db/partitions/AtomicBTreePartition.java b/src/java/org/apache/cassandra/db/partitions/AtomicBTreePartition.java
index 1543fd3..a588303 100644
--- a/src/java/org/apache/cassandra/db/partitions/AtomicBTreePartition.java
+++ b/src/java/org/apache/cassandra/db/partitions/AtomicBTreePartition.java
@@ -19,6 +19,7 @@
 
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
@@ -26,12 +27,12 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.rows.EncodingStats;
-import org.apache.cassandra.db.rows.Row;
-import org.apache.cassandra.db.rows.Rows;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.index.transactions.UpdateTransaction;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.SearchIterator;
 import org.apache.cassandra.utils.btree.BTree;
 import org.apache.cassandra.utils.btree.UpdateFunction;
 import org.apache.cassandra.utils.concurrent.Locks;
@@ -169,6 +170,66 @@
         }
     }
 
+    @Override
+    public DeletionInfo deletionInfo()
+    {
+        return allocator.ensureOnHeap().applyToDeletionInfo(super.deletionInfo());
+    }
+
+    @Override
+    public Row staticRow()
+    {
+        return allocator.ensureOnHeap().applyToStatic(super.staticRow());
+    }
+
+    @Override
+    public DecoratedKey partitionKey()
+    {
+        return allocator.ensureOnHeap().applyToPartitionKey(super.partitionKey());
+    }
+
+    @Override
+    public Row getRow(Clustering clustering)
+    {
+        return allocator.ensureOnHeap().applyToRow(super.getRow(clustering));
+    }
+
+    @Override
+    public Row lastRow()
+    {
+        return allocator.ensureOnHeap().applyToRow(super.lastRow());
+    }
+
+    @Override
+    public SearchIterator<Clustering, Row> searchIterator(ColumnFilter columns, boolean reversed)
+    {
+        return allocator.ensureOnHeap().applyToPartition(super.searchIterator(columns, reversed));
+    }
+
+    @Override
+    public UnfilteredRowIterator unfilteredIterator(ColumnFilter selection, Slices slices, boolean reversed)
+    {
+        return allocator.ensureOnHeap().applyToPartition(super.unfilteredIterator(selection, slices, reversed));
+    }
+
+    @Override
+    public UnfilteredRowIterator unfilteredIterator()
+    {
+        return allocator.ensureOnHeap().applyToPartition(super.unfilteredIterator());
+    }
+
+    @Override
+    public UnfilteredRowIterator unfilteredIterator(Holder current, ColumnFilter selection, Slices slices, boolean reversed)
+    {
+        return allocator.ensureOnHeap().applyToPartition(super.unfilteredIterator(current, selection, slices, reversed));
+    }
+
+    @Override
+    public Iterator<Row> iterator()
+    {
+        return allocator.ensureOnHeap().applyToPartition(super.iterator());
+    }
+
     private boolean maybeLock(OpOrder.Group writeOp)
     {
         if (!useLock())
@@ -319,7 +380,6 @@
             if (inserted != null)
                 inserted.clear();
         }
-
         public boolean abortEarly()
         {
             return updating.ref != ref;
diff --git a/src/java/org/apache/cassandra/db/partitions/CachedPartition.java b/src/java/org/apache/cassandra/db/partitions/CachedPartition.java
index 33e6ecc..0cbaba0 100644
--- a/src/java/org/apache/cassandra/db/partitions/CachedPartition.java
+++ b/src/java/org/apache/cassandra/db/partitions/CachedPartition.java
@@ -45,7 +45,7 @@
     /**
      * The number of rows that were live at the time the partition was cached.
      *
-     * See {@link ColumnFamilyStore#isFilterFullyCoveredBy} to see why we need this.
+     * See {@link org.apache.cassandra.db.ColumnFamilyStore#isFilterFullyCoveredBy} to see why we need this.
      *
      * @return the number of rows in this partition that were live at the time the
      * partition was cached (this can be different from the number of live rows now
@@ -58,7 +58,7 @@
      * non-deleted cell.
      *
      * Note that this is generally not a very meaningful number, but this is used by
-     * {@link DataLimits#hasEnoughLiveData} as an optimization.
+     * {@link org.apache.cassandra.db.filter.DataLimits#hasEnoughLiveData} as an optimization.
      *
      * @return the number of row that have at least one non-expiring non-deleted cell.
      */
@@ -86,7 +86,7 @@
      * The number of cells in this cached partition that are neither tombstone nor expiring.
      *
      * Note that this is generally not a very meaningful number, but this is used by
-     * {@link DataLimits#hasEnoughLiveData} as an optimization.
+     * {@link org.apache.cassandra.db.filter.DataLimits#hasEnoughLiveData} as an optimization.
      *
      * @return the number of cells that are neither tombstones nor expiring.
      */
diff --git a/src/java/org/apache/cassandra/db/partitions/FilteredPartition.java b/src/java/org/apache/cassandra/db/partitions/FilteredPartition.java
index 26a947b..70a4678 100644
--- a/src/java/org/apache/cassandra/db/partitions/FilteredPartition.java
+++ b/src/java/org/apache/cassandra/db/partitions/FilteredPartition.java
@@ -65,7 +65,7 @@
 
             public DecoratedKey partitionKey()
             {
-                return partitionKey;
+                return FilteredPartition.this.partitionKey();
             }
 
             public Row staticRow()
diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionIterator.java b/src/java/org/apache/cassandra/db/partitions/PartitionIterator.java
index 529a9e2..e4b926a 100644
--- a/src/java/org/apache/cassandra/db/partitions/PartitionIterator.java
+++ b/src/java/org/apache/cassandra/db/partitions/PartitionIterator.java
@@ -17,8 +17,6 @@
  */
 package org.apache.cassandra.db.partitions;
 
-import java.util.Iterator;
-
 import org.apache.cassandra.db.rows.*;
 
 /**
diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java
index 3560e90..828ee95 100644
--- a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java
+++ b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java
@@ -61,7 +61,7 @@
 
     public static final PartitionUpdateSerializer serializer = new PartitionUpdateSerializer();
 
-    private final int createdAtInSec = FBUtilities.nowInSeconds();
+    private final int createdAtInSec;
 
     // Records whether this update is "built", i.e. if the build() method has been called, which
     // happens when the update is read. Further writing is then rejected though a manual call
@@ -81,26 +81,30 @@
                             PartitionColumns columns,
                             MutableDeletionInfo deletionInfo,
                             int initialRowCapacity,
-                            boolean canHaveShadowedData)
+                            boolean canHaveShadowedData,
+                            int createdAtInSec)
     {
         super(metadata, key);
         this.deletionInfo = deletionInfo;
         this.holder = new Holder(columns, BTree.empty(), deletionInfo, Rows.EMPTY_STATIC_ROW, EncodingStats.NO_STATS);
         this.canHaveShadowedData = canHaveShadowedData;
         rowBuilder = builder(initialRowCapacity);
+        this.createdAtInSec = createdAtInSec;
     }
 
     private PartitionUpdate(CFMetaData metadata,
                             DecoratedKey key,
                             Holder holder,
                             MutableDeletionInfo deletionInfo,
-                            boolean canHaveShadowedData)
+                            boolean canHaveShadowedData,
+                            int createdAtInSec)
     {
         super(metadata, key);
         this.holder = holder;
         this.deletionInfo = deletionInfo;
         this.isBuilt = true;
         this.canHaveShadowedData = canHaveShadowedData;
+        this.createdAtInSec = createdAtInSec;
     }
 
     public PartitionUpdate(CFMetaData metadata,
@@ -108,7 +112,16 @@
                            PartitionColumns columns,
                            int initialRowCapacity)
     {
-        this(metadata, key, columns, MutableDeletionInfo.live(), initialRowCapacity, true);
+        this(metadata, key, columns, MutableDeletionInfo.live(), initialRowCapacity, true, FBUtilities.nowInSeconds());
+    }
+
+    public PartitionUpdate(CFMetaData metadata,
+                           DecoratedKey key,
+                           PartitionColumns columns,
+                           int initialRowCapacity,
+                           int createdAtInSec)
+    {
+        this(metadata, key, columns, MutableDeletionInfo.live(), initialRowCapacity, true, createdAtInSec);
     }
 
     public PartitionUpdate(CFMetaData metadata,
@@ -134,7 +147,7 @@
     {
         MutableDeletionInfo deletionInfo = MutableDeletionInfo.live();
         Holder holder = new Holder(PartitionColumns.NONE, BTree.empty(), deletionInfo, Rows.EMPTY_STATIC_ROW, EncodingStats.NO_STATS);
-        return new PartitionUpdate(metadata, key, holder, deletionInfo, false);
+        return new PartitionUpdate(metadata, key, holder, deletionInfo, false, FBUtilities.nowInSeconds());
     }
 
     /**
@@ -151,7 +164,7 @@
     {
         MutableDeletionInfo deletionInfo = new MutableDeletionInfo(timestamp, nowInSec);
         Holder holder = new Holder(PartitionColumns.NONE, BTree.empty(), deletionInfo, Rows.EMPTY_STATIC_ROW, EncodingStats.NO_STATS);
-        return new PartitionUpdate(metadata, key, holder, deletionInfo, false);
+        return new PartitionUpdate(metadata, key, holder, deletionInfo, false, FBUtilities.nowInSeconds());
     }
 
     /**
@@ -177,7 +190,7 @@
             staticRow == null ? Rows.EMPTY_STATIC_ROW : staticRow,
             EncodingStats.NO_STATS
         );
-        return new PartitionUpdate(metadata, key, holder, deletionInfo, false);
+        return new PartitionUpdate(metadata, key, holder, deletionInfo, false, FBUtilities.nowInSeconds());
     }
 
     /**
@@ -211,21 +224,35 @@
     /**
      * Turns the given iterator into an update.
      *
+     * @param iterator the iterator to turn into updates.
+     * @param filter the column filter used when querying {@code iterator}. This is used to make
+     * sure we don't include data for which the value has been skipped while reading (as we would
+     * then be writing something incorrect).
+     *
      * Warning: this method does not close the provided iterator, it is up to
      * the caller to close it.
      */
-    public static PartitionUpdate fromIterator(UnfilteredRowIterator iterator)
+    public static PartitionUpdate fromIterator(UnfilteredRowIterator iterator, ColumnFilter filter)
     {
-        return fromIterator(iterator, true,  null);
+
+        return fromIterator(iterator, filter, true,  null);
+    }
+
+    public static PartitionUpdate fromIteratorExplicitColumns(UnfilteredRowIterator iterator, ColumnFilter filter)
+    {
+        iterator = UnfilteredRowIterators.withOnlyQueriedData(iterator, filter);
+        Holder holder = build(iterator, 16, true, null, filter.fetchedColumns());
+        MutableDeletionInfo deletionInfo = (MutableDeletionInfo) holder.deletionInfo;
+        return new PartitionUpdate(iterator.metadata(), iterator.partitionKey(), holder, deletionInfo, false, FBUtilities.nowInSeconds());
     }
 
     private static final NoSpamLogger rowMergingLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES);
     /**
      * Removes duplicate rows from incoming iterator, to be used when we can't trust the underlying iterator (like when reading legacy sstables)
      */
-    public static PartitionUpdate fromPre30Iterator(UnfilteredRowIterator iterator)
+    public static PartitionUpdate fromPre30Iterator(UnfilteredRowIterator iterator, ColumnFilter filter)
     {
-        return fromIterator(iterator, false, (a, b) -> {
+        return fromIterator(iterator, filter, false, (a, b) -> {
             CFMetaData cfm = iterator.metadata();
             rowMergingLogger.warn(String.format("Merging rows from pre 3.0 iterator for partition key: %s",
                                                 cfm.getKeyValidator().getString(iterator.partitionKey().getKey())));
@@ -233,18 +260,31 @@
         });
     }
 
-    private static PartitionUpdate fromIterator(UnfilteredRowIterator iterator, boolean ordered, BTree.Builder.QuickResolver<Row> quickResolver)
+    private static PartitionUpdate fromIterator(UnfilteredRowIterator iterator, ColumnFilter filter, boolean ordered, BTree.Builder.QuickResolver<Row> quickResolver)
     {
+        iterator = UnfilteredRowIterators.withOnlyQueriedData(iterator, filter);
         Holder holder = build(iterator, 16, ordered, quickResolver);
         MutableDeletionInfo deletionInfo = (MutableDeletionInfo) holder.deletionInfo;
-        return new PartitionUpdate(iterator.metadata(), iterator.partitionKey(), holder, deletionInfo, false);
+        return new PartitionUpdate(iterator.metadata(), iterator.partitionKey(), holder, deletionInfo, false, FBUtilities.nowInSeconds());
     }
 
-    public static PartitionUpdate fromIterator(RowIterator iterator)
+    /**
+     * Turns the given iterator into an update.
+     *
+     * @param iterator the iterator to turn into updates.
+     * @param filter the column filter used when querying {@code iterator}. This is used to make
+     * sure we don't include data for which the value has been skipped while reading (as we would
+     * then be writing something incorrect).
+     *
+     * Warning: this method does not close the provided iterator, it is up to
+     * the caller to close it.
+     */
+    public static PartitionUpdate fromIterator(RowIterator iterator, ColumnFilter filter)
     {
+        iterator = RowIterators.withOnlyQueriedData(iterator, filter);
         MutableDeletionInfo deletionInfo = MutableDeletionInfo.live();
         Holder holder = build(iterator, deletionInfo, true, 16);
-        return new PartitionUpdate(iterator.metadata(), iterator.partitionKey(), holder, deletionInfo, false);
+        return new PartitionUpdate(iterator.metadata(), iterator.partitionKey(), holder, deletionInfo, false, FBUtilities.nowInSeconds());
     }
 
     protected boolean canHaveShadowedData()
@@ -293,7 +333,7 @@
         try (DataOutputBuffer out = new DataOutputBuffer())
         {
             serializer.serialize(update, out, version);
-            return ByteBuffer.wrap(out.getData(), 0, out.getLength());
+            return out.buffer();
         }
         catch (IOException e)
         {
@@ -333,7 +373,7 @@
 
         int nowInSecs = FBUtilities.nowInSeconds();
         List<UnfilteredRowIterator> asIterators = Lists.transform(updates, AbstractBTreePartition::unfilteredIterator);
-        return fromIterator(UnfilteredRowIterators.merge(asIterators, nowInSecs));
+        return fromIterator(UnfilteredRowIterators.merge(asIterators, nowInSecs), ColumnFilter.all(updates.get(0).metadata()));
     }
 
     // We override this, because the version in the super-class calls holder(), which build the update preventing
@@ -475,13 +515,6 @@
         return super.iterator();
     }
 
-    @Override
-    public SliceableUnfilteredRowIterator sliceableUnfilteredIterator(ColumnFilter columns, boolean reversed)
-    {
-        maybeBuild();
-        return super.sliceableUnfilteredIterator(columns, reversed);
-    }
-
     /**
      * Validates the data contained in this update.
      *
@@ -680,11 +713,165 @@
         return sb.toString();
     }
 
+    /**
+     * Creates a new simple partition update builder.
+     *
+     * @param metadata the metadata for the table this is a partition of.
+     * @param partitionKeyValues the values for partition key columns identifying this partition. The values for each
+     * partition key column can be passed either directly as {@code ByteBuffer} or using a "native" value (int for
+     * Int32Type, string for UTF8Type, ...). It is also allowed to pass a single {@code DecoratedKey} value directly.
+     * @return a newly created builder.
+     */
+    public static SimpleBuilder simpleBuilder(CFMetaData metadata, Object... partitionKeyValues)
+    {
+        return new SimpleBuilders.PartitionUpdateBuilder(metadata, partitionKeyValues);
+    }
+
+    /**
+     * Interface for building partition updates geared towards human.
+     * <p>
+     * This should generally not be used when performance matters too much, but provides a more convenient interface to
+     * build an update than using the class constructor when performance is not of the utmost importance.
+     */
+    public interface SimpleBuilder
+    {
+        /**
+         * The metadata of the table this is a builder on.
+         */
+        public CFMetaData metadata();
+
+        /**
+         * Sets the timestamp to use for the following additions to this builder or any derived (row) builder.
+         *
+         * @param timestamp the timestamp to use for following additions. If that timestamp hasn't been set, the current
+         * time in microseconds will be used.
+         * @return this builder.
+         */
+        public SimpleBuilder timestamp(long timestamp);
+
+        /**
+         * Sets the ttl to use for the following additions to this builder or any derived (row) builder.
+         *
+         * @param ttl the ttl to use for following additions. If that ttl hasn't been set, no ttl will be used.
+         * @return this builder.
+         */
+        public SimpleBuilder ttl(int ttl);
+
+        /**
+         * Sets the current time to use for the following additions to this builder or any derived (row) builder.
+         *
+         * @param nowInSec the current time to use for following additions. If the current time hasn't been set, the current
+         * time in seconds will be used.
+         * @return this builder.
+         */
+        public SimpleBuilder nowInSec(int nowInSec);
+
+        /**
+         * Adds the row identifier by the provided clustering and return a builder for that row.
+         *
+         * @param clusteringValues the value for the clustering columns of the row to add to this build. There may be no
+         * values if either the table has no clustering column, or if you want to edit the static row. Note that as a
+         * shortcut it is also allowed to pass a {@code Clustering} object directly, in which case that should be the
+         * only argument.
+         * @return a builder for the row identified by {@code clusteringValues}.
+         */
+        public Row.SimpleBuilder row(Object... clusteringValues);
+
+        /**
+         * Deletes the partition identified by this builder (using a partition level deletion).
+         *
+         * @return this builder.
+         */
+        public SimpleBuilder delete();
+
+        /**
+         * Adds a new range tombstone to this update, returning a builder for that range.
+         *
+         * @return the range tombstone builder for the newly added range.
+         */
+        public RangeTombstoneBuilder addRangeTombstone();
+
+        /**
+         * Build the update represented by this builder.
+         *
+         * @return the built update.
+         */
+        public PartitionUpdate build();
+
+        /**
+         * As shortcut for {@code new Mutation(build())}.
+         *
+         * @return the built update, wrapped in a {@code Mutation}.
+         */
+        public Mutation buildAsMutation();
+
+        /**
+         * Interface to build range tombstone.
+         *
+         * By default, if no other methods are called, the represented range is inclusive of both start and end and
+         * includes everything (its start is {@code BOTTOM} and it's end is {@code TOP}).
+         */
+        public interface RangeTombstoneBuilder
+        {
+            /**
+             * Sets the start for the built range using the provided values.
+             *
+             * @param values the value for the start of the range. They act like the {@code clusteringValues} argument
+             * of the {@link SimpleBuilder#row(Object...)} method, except that it doesn't have to be a full
+             * clustering, it can only be a prefix.
+             * @return this builder.
+             */
+            public RangeTombstoneBuilder start(Object... values);
+
+            /**
+             * Sets the end for the built range using the provided values.
+             *
+             * @param values the value for the end of the range. They act like the {@code clusteringValues} argument
+             * of the {@link SimpleBuilder#row(Object...)} method, except that it doesn't have to be a full
+             * clustering, it can only be a prefix.
+             * @return this builder.
+             */
+            public RangeTombstoneBuilder end(Object... values);
+
+            /**
+             * Sets the start of this range as inclusive.
+             * <p>
+             * This is the default and don't need to be called, but can for explicitness.
+             *
+             * @return this builder.
+             */
+            public RangeTombstoneBuilder inclStart();
+
+            /**
+             * Sets the start of this range as exclusive.
+             *
+             * @return this builder.
+             */
+            public RangeTombstoneBuilder exclStart();
+
+            /**
+             * Sets the end of this range as inclusive.
+             * <p>
+             * This is the default and don't need to be called, but can for explicitness.
+             *
+             * @return this builder.
+             */
+            public RangeTombstoneBuilder inclEnd();
+
+            /**
+             * Sets the end of this range as exclusive.
+             *
+             * @return this builder.
+             */
+            public RangeTombstoneBuilder exclEnd();
+        }
+    }
+
     public static class PartitionUpdateSerializer
     {
         public void serialize(PartitionUpdate update, DataOutputPlus out, int version) throws IOException
         {
-            try (UnfilteredRowIterator iter = update.sliceableUnfilteredIterator())
+            try (UnfilteredRowIterator iter = update.unfilteredIterator())
             {
                 assert !iter.isReverseOrder();
 
@@ -759,7 +946,8 @@
                                        header.key,
                                        new Holder(header.sHeader.columns(), rows.build(), deletionInfo, header.staticRow, header.sHeader.stats()),
                                        deletionInfo,
-                                       false);
+                                       false,
+                                       FBUtilities.nowInSeconds());
         }
 
         private static PartitionUpdate deserializePre30(DataInputPlus in, int version, SerializationHelper.Flag flag, ByteBuffer key) throws IOException
@@ -767,13 +955,13 @@
             try (UnfilteredRowIterator iterator = LegacyLayout.deserializeLegacyPartition(in, version, flag, key))
             {
                 assert iterator != null; // This is only used in mutation, and mutation have never allowed "null" column families
-                return PartitionUpdate.fromPre30Iterator(iterator);
+                return PartitionUpdate.fromPre30Iterator(iterator, ColumnFilter.all(iterator.metadata()));
             }
         }
 
         public long serializedSize(PartitionUpdate update, int version)
         {
-            try (UnfilteredRowIterator iter = update.sliceableUnfilteredIterator())
+            try (UnfilteredRowIterator iter = update.unfilteredIterator())
             {
                 if (version < MessagingService.VERSION_30)
                     return LegacyLayout.serializedSizeAsLegacyPartition(null, iter, version);
diff --git a/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java b/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java
index 4af53e2..245ea35 100644
--- a/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java
+++ b/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java
@@ -27,6 +27,7 @@
 import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.transform.FilteredPartitions;
+import org.apache.cassandra.db.transform.MorePartitions;
 import org.apache.cassandra.db.transform.Transformation;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
@@ -77,6 +78,24 @@
         return Transformation.apply(toReturn, new Close());
     }
 
+    public static UnfilteredPartitionIterator concat(final List<UnfilteredPartitionIterator> iterators)
+    {
+        if (iterators.size() == 1)
+            return iterators.get(0);
+
+        class Extend implements MorePartitions<UnfilteredPartitionIterator>
+        {
+            int i = 1;
+            public UnfilteredPartitionIterator moreContents()
+            {
+                if (i >= iterators.size())
+                    return null;
+                return iterators.get(i++);
+            }
+        }
+        return MorePartitions.extend(iterators.get(0), new Extend());
+    }
+
     public static PartitionIterator filter(final UnfilteredPartitionIterator iterator, final int nowInSec)
     {
         return FilteredPartitions.filter(iterator, nowInSec);
@@ -175,12 +194,6 @@
         {
             private final List<UnfilteredRowIterator> toMerge = new ArrayList<>(iterators.size());
 
-            @Override
-            public boolean trivialReduceIsTrivial()
-            {
-                return false;
-            }
-
             public void reduce(int idx, UnfilteredRowIterator current)
             {
                 toMerge.add(current);
diff --git a/src/java/org/apache/cassandra/db/rows/AbstractCell.java b/src/java/org/apache/cassandra/db/rows/AbstractCell.java
index 576351e..a993f36 100644
--- a/src/java/org/apache/cassandra/db/rows/AbstractCell.java
+++ b/src/java/org/apache/cassandra/db/rows/AbstractCell.java
@@ -17,15 +17,20 @@
  */
 package org.apache.cassandra.db.rows;
 
+import java.nio.ByteBuffer;
 import java.security.MessageDigest;
 import java.util.Objects;
 
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.db.DeletionPurger;
+import org.apache.cassandra.db.TypeSizes;
 import org.apache.cassandra.db.context.CounterContext;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.CollectionType;
 import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.memory.AbstractAllocator;
 
 /**
  * Base abstract class for {@code Cell} implementations.
@@ -40,6 +45,81 @@
         super(column);
     }
 
+    public boolean isCounterCell()
+    {
+        return !isTombstone() && column.isCounterColumn();
+    }
+
+    public boolean isLive(int nowInSec)
+    {
+        return localDeletionTime() == NO_DELETION_TIME || (ttl() != NO_TTL && nowInSec < localDeletionTime());
+    }
+
+    public boolean isTombstone()
+    {
+        return localDeletionTime() != NO_DELETION_TIME && ttl() == NO_TTL;
+    }
+
+    public boolean isExpiring()
+    {
+        return ttl() != NO_TTL;
+    }
+
+    public Cell markCounterLocalToBeCleared()
+    {
+        if (!isCounterCell())
+            return this;
+
+        ByteBuffer value = value();
+        ByteBuffer marked = CounterContext.instance().markLocalToBeCleared(value);
+        return marked == value ? this : new BufferCell(column, timestamp(), ttl(), localDeletionTime(), marked, path());
+    }
+
+    public Cell purge(DeletionPurger purger, int nowInSec)
+    {
+        if (!isLive(nowInSec))
+        {
+            if (purger.shouldPurge(timestamp(), localDeletionTime()))
+                return null;
+
+            // We slightly hijack purging to convert expired but not purgeable columns to tombstones. The reason we do that is
+            // that once a column has expired it is equivalent to a tombstone but actually using a tombstone is more compact since
+            // we don't keep the column value. The reason we do it here is that 1) it's somewhat related to dealing with tombstones
+            // so hopefully not too surprising and 2) we want to this and purging at the same places, so it's simpler/more efficient
+            // to do both here.
+            if (isExpiring())
+            {
+                // Note that as long as the expiring column and the tombstone put together live longer than GC grace seconds,
+                // we'll fulfil our responsibility to repair. See discussion at
+                // http://cassandra-user-incubator-apache-org.3065146.n2.nabble.com/repair-compaction-and-tombstone-rows-td7583481.html
+                return BufferCell.tombstone(column, timestamp(), localDeletionTime() - ttl(), path()).purge(purger, nowInSec);
+            }
+        }
+        return this;
+    }
+
+    public Cell copy(AbstractAllocator allocator)
+    {
+        CellPath path = path();
+        return new BufferCell(column, timestamp(), ttl(), localDeletionTime(), allocator.clone(value()), path == null ? null : path.copy(allocator));
+    }
+
+    // note: while the cell returned may be different, the value is the same, so if the value is offheap it must be referenced inside a guarded context (or copied)
+    public Cell updateAllTimestamp(long newTimestamp)
+    {
+        return new BufferCell(column, isTombstone() ? newTimestamp - 1 : newTimestamp, ttl(), localDeletionTime(), value(), path());
+    }
+
+    public int dataSize()
+    {
+        CellPath path = path();
+        return TypeSizes.sizeof(timestamp())
+               + TypeSizes.sizeof(ttl())
+               + TypeSizes.sizeof(localDeletionTime())
+               + value().remaining()
+               + (path == null ? 0 : path.dataSize());
+    }
+
     public void digest(MessageDigest digest)
     {
         if (isCounterCell())
@@ -67,19 +147,11 @@
         if (isExpiring() && localDeletionTime() == NO_DELETION_TIME)
             throw new MarshalException("Shoud not have a TTL without an associated local deletion time");
 
-        if (isTombstone())
-        {
-            // If cell is a tombstone, it shouldn't have a value.
-            if (value().hasRemaining())
-                throw new MarshalException("A tombstone should not have a value");
-        }
-        else
-        {
-            column().validateCellValue(value());
-        }
-
-        if (path() != null)
-            column().validateCellPath(path());
+        // non-frozen UDTs require both the cell path & value to validate,
+        // so that logic is pushed down into ColumnDefinition. Tombstone
+        // validation is done there too as it also involves the cell path
+        // for complex columns
+        column().validateCell(this);
     }
 
     public long maxTimestamp()
@@ -124,14 +196,26 @@
             CollectionType ct = (CollectionType)type;
             return String.format("[%s[%s]=%s %s]",
                                  column().name,
-                                 ct.nameComparator().getString(path().get(0)),
-                                 ct.valueComparator().getString(value()),
+                                 safeToString(ct.nameComparator(), path().get(0)),
+                                 safeToString(ct.valueComparator(), value()),
                                  livenessInfoString());
         }
         if (isTombstone())
             return String.format("[%s=<tombstone> %s]", column().name, livenessInfoString());
         else
-            return String.format("[%s=%s %s]", column().name, type.getString(value()), livenessInfoString());
+            return String.format("[%s=%s %s]", column().name, safeToString(type, value()), livenessInfoString());
+    }
+
+    private static String safeToString(AbstractType<?> type, ByteBuffer data)
+    {
+        try
+        {
+            return type.getString(data);
+        }
+        catch (Exception e)
+        {
+            return "0x" + ByteBufferUtil.bytesToHex(data);
+        }
     }
 
     private String livenessInfoString()
diff --git a/src/java/org/apache/cassandra/db/rows/AbstractRangeTombstoneMarker.java b/src/java/org/apache/cassandra/db/rows/AbstractRangeTombstoneMarker.java
index b1ee7ec..153243c 100644
--- a/src/java/org/apache/cassandra/db/rows/AbstractRangeTombstoneMarker.java
+++ b/src/java/org/apache/cassandra/db/rows/AbstractRangeTombstoneMarker.java
@@ -20,18 +20,18 @@
 import java.nio.ByteBuffer;
 
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.ClusteringBoundOrBoundary;
 
-public abstract class AbstractRangeTombstoneMarker implements RangeTombstoneMarker
+public abstract class AbstractRangeTombstoneMarker<B extends ClusteringBoundOrBoundary> implements RangeTombstoneMarker
 {
-    protected final RangeTombstone.Bound bound;
+    protected final B bound;
 
-    protected AbstractRangeTombstoneMarker(RangeTombstone.Bound bound)
+    protected AbstractRangeTombstoneMarker(B bound)
     {
         this.bound = bound;
     }
 
-    public RangeTombstone.Bound clustering()
+    public B clustering()
     {
         return bound;
     }
@@ -58,7 +58,7 @@
 
     public void validateData(CFMetaData metadata)
     {
-        Slice.Bound bound = clustering();
+        ClusteringBoundOrBoundary bound = clustering();
         for (int i = 0; i < bound.size(); i++)
         {
             ByteBuffer value = bound.get(i);
diff --git a/src/java/org/apache/cassandra/db/rows/AbstractRow.java b/src/java/org/apache/cassandra/db/rows/AbstractRow.java
index f91126b..7cc864d 100644
--- a/src/java/org/apache/cassandra/db/rows/AbstractRow.java
+++ b/src/java/org/apache/cassandra/db/rows/AbstractRow.java
@@ -19,14 +19,19 @@
 import java.nio.ByteBuffer;
 import java.security.MessageDigest;
 import java.util.AbstractCollection;
-import java.util.Collection;
+import java.util.Collections;
 import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 
 import com.google.common.collect.Iterables;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.marshal.CollectionType;
+import org.apache.cassandra.db.marshal.UserType;
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.utils.FBUtilities;
 
@@ -60,6 +65,11 @@
 
     public void digest(MessageDigest digest)
     {
+        digest(digest, Collections.emptySet());
+    }
+
+    public void digest(MessageDigest digest, Set<ByteBuffer> columnsToExclude)
+    {
         FBUtilities.updateWithByte(digest, kind().ordinal());
         clustering().digest(digest);
 
@@ -67,7 +77,8 @@
         primaryKeyLivenessInfo().digest(digest);
 
         for (ColumnData cd : this)
-            cd.digest(digest);
+            if (!columnsToExclude.contains(cd.column.name.bytes))
+                cd.digest(digest);
     }
 
     public void validateData(CFMetaData metadata)
@@ -77,15 +88,31 @@
         {
             ByteBuffer value = clustering.get(i);
             if (value != null)
-                metadata.comparator.subtype(i).validate(value);
+            {
+                try
+                {
+                    metadata.comparator.subtype(i).validate(value);
+                }
+                catch (Exception e)
+                {
+                    throw new MarshalException("comparator #" + i + " '" + metadata.comparator.subtype(i) + "' in '" + metadata + "' didn't validate", e);
+                }
+            }
         }
 
         primaryKeyLivenessInfo().validate();
         if (deletion().time().localDeletionTime() < 0)
-            throw new MarshalException("A local deletion time should not be negative");
+            throw new MarshalException("A local deletion time should not be negative in '" + metadata + "'");
 
         for (ColumnData cd : this)
-            cd.validate();
+            try
+            {
+                cd.validate();
+            }
+            catch (Exception e)
+            {
+                throw new MarshalException("data for '" + cd.column.debugString() + "', " + cd + " in '" + metadata + "' didn't validate", e);
+            }
     }
 
     public String toString()
@@ -152,16 +179,31 @@
                 }
                 else
                 {
-                    ComplexColumnData complexData = (ComplexColumnData)cd;
-                    CollectionType ct = (CollectionType)cd.column().type;
-                    sb.append(cd.column().name).append("={");
-                    int i = 0;
-                    for (Cell cell : complexData)
+                    sb.append(cd.column().name).append('=');
+                    ComplexColumnData complexData = (ComplexColumnData) cd;
+                    Function<Cell, String> transform = null;
+                    if (cd.column().type.isCollection())
                     {
-                        sb.append(i++ == 0 ? "" : ", ");
-                        sb.append(ct.nameComparator().getString(cell.path().get(0))).append("->").append(ct.valueComparator().getString(cell.value()));
+                        CollectionType ct = (CollectionType) cd.column().type;
+                        transform = cell -> String.format("%s -> %s",
+                                                  ct.nameComparator().getString(cell.path().get(0)),
+                                                  ct.valueComparator().getString(cell.value()));
+
                     }
-                    sb.append('}');
+                    else if (cd.column().type.isUDT())
+                    {
+                        UserType ut = (UserType)cd.column().type;
+                        transform = cell -> {
+                            Short fId = ut.nameComparator().getSerializer().deserialize(cell.path().get(0));
+                            return String.format("%s -> %s",
+                                                 ut.fieldNameAsString(fId),
+                                                 ut.fieldType(fId).getString(cell.value()));
+                        };
+                    }
+                    transform = transform != null ? transform : cell -> "";
+                    sb.append(StreamSupport.stream(complexData.spliterator(), false)
+                                           .map(transform)
+                                           .collect(Collectors.joining(", ", "{", "}")));
                 }
             }
         }
diff --git a/src/java/org/apache/cassandra/db/rows/AbstractTypeVersionComparator.java b/src/java/org/apache/cassandra/db/rows/AbstractTypeVersionComparator.java
deleted file mode 100644
index e47f681..0000000
--- a/src/java/org/apache/cassandra/db/rows/AbstractTypeVersionComparator.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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.db.rows;
-
-import java.util.Comparator;
-import java.util.List;
-
-import org.apache.cassandra.db.marshal.*;
-
-/**
- * A {@code Comparator} use to determine which version of a type should be used.
- * <p>In the case of UDTs it is possible to have 2 versions or more of the same type, if some fields has been added to
- * the type. To avoid problems the latest type need to be used.</p>
- */
-final class AbstractTypeVersionComparator implements Comparator<AbstractType<?>>
-{
-    public static final Comparator<AbstractType<?>> INSTANCE = new AbstractTypeVersionComparator();
-
-    private AbstractTypeVersionComparator()
-    {
-    }
-
-    @Override
-    public int compare(AbstractType<?> type, AbstractType<?> otherType)
-    {
-        if (!type.getClass().equals(otherType.getClass()))
-            throw new IllegalArgumentException(String.format("Trying to compare 2 different types: %s and %s",
-                                                             type,
-                                                             otherType));
-
-        if (type.equals(otherType))
-            return 0;
-
-        // The only case where 2 types can differ is if they contains some UDTs and one of them has more
-        // fields (due to an ALTER type ADD) than in the other type. In this case we need to pick the type with
-        // the bigger amount of fields.
-        if (type.isUDT())
-            return compareUserType((UserType) type, (UserType) otherType);
-
-        if (type.isTuple())
-            return compareTuple((TupleType) type, (TupleType) otherType);
-
-        if (type.isCollection())
-            return compareCollectionTypes(type, otherType);
-
-        if (type instanceof CompositeType)
-            return compareCompositeTypes((CompositeType) type, (CompositeType) otherType);
-
-        // In theory we should never reach that point but to be on the safe side we allow it.
-        return 0;
-    }
-
-    private int compareCompositeTypes(CompositeType type, CompositeType otherType)
-    {
-        List<AbstractType<?>> types = type.getComponents();
-        List<AbstractType<?>> otherTypes = otherType.getComponents();
-
-        if (types.size() != otherTypes.size())
-            return Integer.compare(types.size(), otherTypes.size());
-
-        for (int i = 0, m = type.componentsCount(); i < m ; i++)
-        {
-            int test = compare(types.get(i), otherTypes.get(i));
-            if (test != 0);
-                return test;
-        }
-        return 0;
-    }
-
-    private int compareCollectionTypes(AbstractType<?> type, AbstractType<?> otherType)
-    {
-        if (type instanceof MapType)
-            return compareMapType((MapType<?, ?>) type, (MapType<?, ?>) otherType);
-
-        if (type instanceof SetType)
-            return compare(((SetType<?>) type).getElementsType(), ((SetType<?>) otherType).getElementsType());
-
-        return compare(((ListType<?>) type).getElementsType(), ((ListType<?>) otherType).getElementsType());
-    }
-
-    private int compareMapType(MapType<?, ?> type, MapType<?, ?> otherType)
-    {
-        int test = compare(type.getKeysType(), otherType.getKeysType());
-        return test != 0 ? test : compare(type.getValuesType(), otherType.getValuesType());
-    }
-
-    private int compareUserType(UserType type, UserType otherType)
-    {
-        return compareTuple(type, otherType);
-    }
-
-    private int compareTuple(TupleType type, TupleType otherType)
-    {
-        if (type.size() != otherType.size())
-            return Integer.compare(type.size(), otherType.size());
-
-        int test = 0;
-        int i = 0;
-        while (test == 0 && i < type.size())
-        {
-            test = compare(type.type(i), otherType.type(i));
-            i++;
-        }
-        return test;
-    }
-}
diff --git a/src/java/org/apache/cassandra/db/rows/BTreeRow.java b/src/java/org/apache/cassandra/db/rows/BTreeRow.java
index e46d0cc..7540c11 100644
--- a/src/java/org/apache/cassandra/db/rows/BTreeRow.java
+++ b/src/java/org/apache/cassandra/db/rows/BTreeRow.java
@@ -19,6 +19,7 @@
 
 import java.nio.ByteBuffer;
 import java.util.*;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 import com.google.common.base.Function;
@@ -32,9 +33,7 @@
 import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
-import org.apache.cassandra.utils.AbstractIterator;
-import org.apache.cassandra.utils.ByteBufferUtil;
-import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.*;
 import org.apache.cassandra.utils.btree.BTree;
 import org.apache.cassandra.utils.btree.BTreeSearchIterator;
 import org.apache.cassandra.utils.btree.UpdateFunction;
@@ -62,7 +61,11 @@
     // no expiring cells, this will be Integer.MAX_VALUE;
     private final int minLocalDeletionTime;
 
-    private BTreeRow(Clustering clustering, LivenessInfo primaryKeyLivenessInfo, Deletion deletion, Object[] btree, int minLocalDeletionTime)
+    private BTreeRow(Clustering clustering,
+                     LivenessInfo primaryKeyLivenessInfo,
+                     Deletion deletion,
+                     Object[] btree,
+                     int minLocalDeletionTime)
     {
         assert !deletion.isShadowedBy(primaryKeyLivenessInfo);
         this.clustering = clustering;
@@ -78,7 +81,10 @@
     }
 
     // Note that it's often easier/safer to use the sortedBuilder/unsortedBuilder or one of the static creation method below. Only directly useful in a small amount of cases.
-    public static BTreeRow create(Clustering clustering, LivenessInfo primaryKeyLivenessInfo, Deletion deletion, Object[] btree)
+    public static BTreeRow create(Clustering clustering,
+                                  LivenessInfo primaryKeyLivenessInfo,
+                                  Deletion deletion,
+                                  Object[] btree)
     {
         int minDeletionTime = Math.min(minDeletionTime(primaryKeyLivenessInfo), minDeletionTime(deletion.time()));
         if (minDeletionTime != Integer.MIN_VALUE)
@@ -87,6 +93,15 @@
                 minDeletionTime = Math.min(minDeletionTime, minDeletionTime(cd));
         }
 
+        return create(clustering, primaryKeyLivenessInfo, deletion, btree, minDeletionTime);
+    }
+
+    public static BTreeRow create(Clustering clustering,
+                                  LivenessInfo primaryKeyLivenessInfo,
+                                  Deletion deletion,
+                                  Object[] btree,
+                                  int minDeletionTime)
+    {
         return new BTreeRow(clustering, primaryKeyLivenessInfo, deletion, btree, minDeletionTime);
     }
 
@@ -113,7 +128,11 @@
     public static BTreeRow noCellLiveRow(Clustering clustering, LivenessInfo primaryKeyLivenessInfo)
     {
         assert !primaryKeyLivenessInfo.isEmpty();
-        return new BTreeRow(clustering, primaryKeyLivenessInfo, Deletion.LIVE, BTree.empty(), minDeletionTime(primaryKeyLivenessInfo));
+        return new BTreeRow(clustering,
+                            primaryKeyLivenessInfo,
+                            Deletion.LIVE,
+                            BTree.empty(),
+                            minDeletionTime(primaryKeyLivenessInfo));
     }
 
     private static int minDeletionTime(Cell cell)
@@ -148,16 +167,23 @@
         return cd.column().isSimple() ? minDeletionTime((Cell) cd) : minDeletionTime((ComplexColumnData)cd);
     }
 
+    public void apply(Consumer<ColumnData> function, boolean reversed)
+    {
+        BTree.apply(btree, function, reversed);
+    }
+
+    public void apply(Consumer<ColumnData> funtion, com.google.common.base.Predicate<ColumnData> stopCondition, boolean reversed)
+    {
+        BTree.apply(btree, funtion, stopCondition, reversed);
+    }
+
     private static int minDeletionTime(Object[] btree, LivenessInfo info, DeletionTime rowDeletion)
     {
-        int min = Math.min(minDeletionTime(info), minDeletionTime(rowDeletion));
-        for (ColumnData cd : BTree.<ColumnData>iterable(btree))
-        {
-            min = Math.min(min, minDeletionTime(cd));
-            if (min == Integer.MIN_VALUE)
-                break;
-        }
-        return min;
+        //we have to wrap this for the lambda
+        final WrappedInt min = new WrappedInt(Math.min(minDeletionTime(info), minDeletionTime(rowDeletion)));
+
+        BTree.<ColumnData>apply(btree, cd -> min.set( Math.min(min.get(), minDeletionTime(cd)) ), cd -> min.get() == Integer.MIN_VALUE, false);
+        return min.get();
     }
 
     public Clustering clustering()
@@ -247,10 +273,12 @@
     {
         Map<ByteBuffer, CFMetaData.DroppedColumn> droppedColumns = metadata.getDroppedColumns();
 
-        if (filter.includesAllColumns() && (activeDeletion.isLive() || deletion.supersedes(activeDeletion)) && droppedColumns.isEmpty())
+        boolean mayFilterColumns = !filter.fetchesAllColumns() || !filter.allFetchedColumnsAreQueried();
+        boolean mayHaveShadowed = activeDeletion.supersedes(deletion.time());
+
+        if (!mayFilterColumns && !mayHaveShadowed && droppedColumns.isEmpty())
             return this;
 
-        boolean mayHaveShadowed = activeDeletion.supersedes(deletion.time());
 
         LivenessInfo newInfo = primaryKeyLivenessInfo;
         Deletion newDeletion = deletion;
@@ -265,6 +293,8 @@
 
         Columns columns = filter.fetchedColumns().columns(isStatic());
         Predicate<ColumnDefinition> inclusionTester = columns.inOrderInclusionTester();
+        Predicate<ColumnDefinition> queriedByUserTester = filter.queriedColumns().columns(isStatic()).inOrderInclusionTester();
+        final LivenessInfo rowLiveness = newInfo;
         return transformAndFilter(newInfo, newDeletion, (cd) -> {
 
             ColumnDefinition column = cd.column();
@@ -273,11 +303,37 @@
 
             CFMetaData.DroppedColumn dropped = droppedColumns.get(column.name.bytes);
             if (column.isComplex())
-                return ((ComplexColumnData) cd).filter(filter, mayHaveShadowed ? activeDeletion : DeletionTime.LIVE, dropped);
+                return ((ComplexColumnData) cd).filter(filter, mayHaveShadowed ? activeDeletion : DeletionTime.LIVE, dropped, rowLiveness);
 
             Cell cell = (Cell) cd;
-            return (dropped == null || cell.timestamp() > dropped.droppedTime) && !(mayHaveShadowed && activeDeletion.deletes(cell))
-                   ? cell : null;
+            // We include the cell unless it is 1) shadowed, 2) for a dropped column or 3) skippable.
+            // And a cell is skippable if it is for a column that is not queried by the user and its timestamp
+            // is lower than the row timestamp (see #10657 or SerializationHelper.includes() for details).
+            boolean isForDropped = dropped != null && cell.timestamp() <= dropped.droppedTime;
+            boolean isShadowed = mayHaveShadowed && activeDeletion.deletes(cell);
+            boolean isSkippable = !queriedByUserTester.test(column);
+
+            if (isForDropped || isShadowed || (isSkippable && cell.timestamp() < rowLiveness.timestamp()))
+                return null;
+
+            // We should apply the same "optimization" as in Cell.deserialize to avoid discrepances
+            // between sstables and memtables data, i.e resulting in a digest mismatch.
+            return isSkippable ? cell.withSkippedValue() : cell;
+        });
+    }
+
+    public Row withOnlyQueriedData(ColumnFilter filter)
+    {
+        if (filter.allFetchedColumnsAreQueried())
+            return this;
+
+        return transformAndFilter(primaryKeyLivenessInfo, deletion, (cd) -> {
+
+            ColumnDefinition column = cd.column();
+            if (column.isComplex())
+                return ((ComplexColumnData)cd).withOnlyQueriedData(filter);
+
+            return filter.fetchedColumnIsQueried(column) ? cd : null;
         });
     }
 
@@ -290,16 +346,26 @@
 
     public boolean hasComplexDeletion()
     {
-        // We start by the end cause we know complex columns sort before simple ones
-        for (ColumnData cd : BTree.<ColumnData>iterable(btree, BTree.Dir.DESC))
-        {
-            if (cd.column().isSimple())
-                return false;
+        final WrappedBoolean result = new WrappedBoolean(false);
 
-            if (!((ComplexColumnData)cd).complexDeletion().isLive())
+        // We start by the end cause we know complex columns sort before simple ones
+        apply(c -> {}, cd -> {
+            if (cd.column.isSimple())
+            {
+                result.set(false);
                 return true;
-        }
-        return false;
+            }
+
+            if (!((ComplexColumnData) cd).complexDeletion().isLive())
+            {
+                result.set(true);
+                return true;
+            }
+
+            return false;
+        }, true);
+
+        return result.get();
     }
 
     public Row markCounterLocalToBeCleared()
@@ -370,7 +436,7 @@
             return null;
 
         int minDeletionTime = minDeletionTime(transformed, info, deletion.time());
-        return new BTreeRow(clustering, info, deletion, transformed, minDeletionTime);
+        return BTreeRow.create(clustering, info, deletion, transformed, minDeletionTime);
     }
 
     public int dataSize()
@@ -616,13 +682,13 @@
                 return new ComplexColumnData(column, btree, deletion);
             }
 
-        };
+        }
         protected Clustering clustering;
         protected LivenessInfo primaryKeyLivenessInfo = LivenessInfo.EMPTY;
         protected Deletion deletion = Deletion.LIVE;
 
         private final boolean isSorted;
-        private final BTree.Builder<Cell> cells;
+        private BTree.Builder<Cell> cells_;
         private final CellResolver resolver;
         private boolean hasComplex = false;
 
@@ -635,10 +701,19 @@
 
         protected Builder(boolean isSorted, int nowInSecs)
         {
-            this.cells = BTree.builder(ColumnData.comparator);
+            cells_ = null;
             resolver = new CellResolver(nowInSecs);
             this.isSorted = isSorted;
-            this.cells.auto(false);
+        }
+
+        private BTree.Builder<Cell> getCells()
+        {
+            if (cells_ == null)
+            {
+                cells_ = BTree.builder(ColumnData.comparator);
+                cells_.auto(false);
+            }
+            return cells_;
         }
 
         protected Builder(Builder builder)
@@ -646,7 +721,7 @@
             clustering = builder.clustering;
             primaryKeyLivenessInfo = builder.primaryKeyLivenessInfo;
             deletion = builder.deletion;
-            cells = builder.cells.copy();
+            cells_ = builder.cells_ == null ? null : builder.cells_.copy();
             resolver = builder.resolver;
             isSorted = builder.isSorted;
             hasComplex = builder.hasComplex;
@@ -679,7 +754,7 @@
             this.clustering = null;
             this.primaryKeyLivenessInfo = LivenessInfo.EMPTY;
             this.deletion = Deletion.LIVE;
-            this.cells.reuse();
+            this.cells_.reuse();
             this.hasComplex = false;
         }
 
@@ -701,38 +776,38 @@
         public void addCell(Cell cell)
         {
             assert cell.column().isStatic() == (clustering == Clustering.STATIC_CLUSTERING) : "Column is " + cell.column() + ", clustering = " + clustering;
+
             // In practice, only unsorted builder have to deal with shadowed cells, but it doesn't cost us much to deal with it unconditionally in this case
             if (deletion.deletes(cell))
                 return;
 
-            cells.add(cell);
+            getCells().add(cell);
             hasComplex |= cell.column.isComplex();
         }
 
         public void addComplexDeletion(ColumnDefinition column, DeletionTime complexDeletion)
         {
-            cells.add(new ComplexColumnDeletion(column, complexDeletion));
+            getCells().add(new ComplexColumnDeletion(column, complexDeletion));
             hasComplex = true;
         }
 
         public Row build()
         {
             if (!isSorted)
-                cells.sort();
+                getCells().sort();
             // we can avoid resolving if we're sorted and have no complex values
             // (because we'll only have unique simple cells, which are already in their final condition)
             if (!isSorted | hasComplex)
-                cells.resolve(resolver);
-            Object[] btree = cells.build();
+                getCells().resolve(resolver);
+            Object[] btree = getCells().build();
 
             if (deletion.isShadowedBy(primaryKeyLivenessInfo))
                 deletion = Deletion.LIVE;
 
             int minDeletionTime = minDeletionTime(btree, primaryKeyLivenessInfo, deletion.time());
-            Row row = new BTreeRow(clustering, primaryKeyLivenessInfo, deletion, btree, minDeletionTime);
+            Row row = BTreeRow.create(clustering, primaryKeyLivenessInfo, deletion, btree, minDeletionTime);
             reset();
             return row;
         }
-
     }
 }
diff --git a/src/java/org/apache/cassandra/db/rows/BufferCell.java b/src/java/org/apache/cassandra/db/rows/BufferCell.java
index df2619c..e445049 100644
--- a/src/java/org/apache/cassandra/db/rows/BufferCell.java
+++ b/src/java/org/apache/cassandra/db/rows/BufferCell.java
@@ -17,18 +17,13 @@
  */
 package org.apache.cassandra.db.rows;
 
-import java.io.IOException;
 import java.nio.ByteBuffer;
 
-import org.apache.cassandra.config.*;
-import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.context.CounterContext;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.db.ExpirationDateOverflowHandling;
 import org.apache.cassandra.db.marshal.ByteType;
-import org.apache.cassandra.io.util.DataInputPlus;
-import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.ObjectSizes;
-import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.memory.AbstractAllocator;
 
 public class BufferCell extends AbstractCell
@@ -45,6 +40,7 @@
     public BufferCell(ColumnDefinition column, long timestamp, int ttl, int localDeletionTime, ByteBuffer value, CellPath path)
     {
         super(column);
+        assert !column.isPrimaryKeyColumn();
         assert column.isComplex() == (path != null);
         this.timestamp = timestamp;
         this.ttl = ttl;
@@ -53,16 +49,13 @@
         this.path = path;
     }
 
-    public static BufferCell live(CFMetaData metadata, ColumnDefinition column, long timestamp, ByteBuffer value)
+    public static BufferCell live(ColumnDefinition column, long timestamp, ByteBuffer value)
     {
-        return live(metadata, column, timestamp, value, null);
+        return live(column, timestamp, value, null);
     }
 
-    public static BufferCell live(CFMetaData metadata, ColumnDefinition column, long timestamp, ByteBuffer value, CellPath path)
+    public static BufferCell live(ColumnDefinition column, long timestamp, ByteBuffer value, CellPath path)
     {
-        if (metadata.params.defaultTimeToLive != NO_TTL)
-            return expiring(column, timestamp, metadata.params.defaultTimeToLive, FBUtilities.nowInSeconds(), value, path);
-
         return new BufferCell(column, timestamp, NO_TTL, NO_DELETION_TIME, value, path);
     }
 
@@ -87,26 +80,6 @@
         return new BufferCell(column, timestamp, NO_TTL, nowInSec, ByteBufferUtil.EMPTY_BYTE_BUFFER, path);
     }
 
-    public boolean isCounterCell()
-    {
-        return !isTombstone() && column.isCounterColumn();
-    }
-
-    public boolean isLive(int nowInSec)
-    {
-        return localDeletionTime == NO_DELETION_TIME || (ttl != NO_TTL && nowInSec < localDeletionTime);
-    }
-
-    public boolean isTombstone()
-    {
-        return localDeletionTime != NO_DELETION_TIME && ttl == NO_TTL;
-    }
-
-    public boolean isExpiring()
-    {
-        return ttl != NO_TTL;
-    }
-
     public long timestamp()
     {
         return timestamp;
@@ -147,6 +120,11 @@
         return new BufferCell(column, newTimestamp, ttl, newLocalDeletionTime, value, path);
     }
 
+    public Cell withSkippedValue()
+    {
+        return withUpdatedValue(ByteBufferUtil.EMPTY_BYTE_BUFFER);
+    }
+
     public Cell copy(AbstractAllocator allocator)
     {
         if (!value.hasRemaining())
@@ -155,216 +133,8 @@
         return new BufferCell(column, timestamp, ttl, localDeletionTime, allocator.clone(value), path == null ? null : path.copy(allocator));
     }
 
-    public Cell markCounterLocalToBeCleared()
-    {
-        if (!isCounterCell())
-            return this;
-
-        ByteBuffer marked = CounterContext.instance().markLocalToBeCleared(value());
-        return marked == value() ? this : new BufferCell(column, timestamp, ttl, localDeletionTime, marked, path);
-    }
-
-    public Cell purge(DeletionPurger purger, int nowInSec)
-    {
-        if (!isLive(nowInSec))
-        {
-            if (purger.shouldPurge(timestamp, localDeletionTime))
-                return null;
-
-            // We slightly hijack purging to convert expired but not purgeable columns to tombstones. The reason we do that is
-            // that once a column has expired it is equivalent to a tombstone but actually using a tombstone is more compact since
-            // we don't keep the column value. The reason we do it here is that 1) it's somewhat related to dealing with tombstones
-            // so hopefully not too surprising and 2) we want to this and purging at the same places, so it's simpler/more efficient
-            // to do both here.
-            if (isExpiring())
-            {
-                // Note that as long as the expiring column and the tombstone put together live longer than GC grace seconds,
-                // we'll fulfil our responsibility to repair. See discussion at
-                // http://cassandra-user-incubator-apache-org.3065146.n2.nabble.com/repair-compaction-and-tombstone-rows-td7583481.html
-                return BufferCell.tombstone(column, timestamp, localDeletionTime - ttl, path).purge(purger, nowInSec);
-            }
-        }
-        return this;
-    }
-
-    public Cell updateAllTimestamp(long newTimestamp)
-    {
-        return new BufferCell(column, isTombstone() ? newTimestamp - 1 : newTimestamp, ttl, localDeletionTime, value, path);
-    }
-
-    public int dataSize()
-    {
-        return TypeSizes.sizeof(timestamp)
-             + TypeSizes.sizeof(ttl)
-             + TypeSizes.sizeof(localDeletionTime)
-             + value.remaining()
-             + (path == null ? 0 : path.dataSize());
-    }
-
     public long unsharedHeapSizeExcludingData()
     {
         return EMPTY_SIZE + ObjectSizes.sizeOnHeapExcludingData(value) + (path == null ? 0 : path.unsharedHeapSizeExcludingData());
     }
-
-    /**
-     * The serialization format for cell is:
-     *     [ flags ][ timestamp ][ deletion time ][    ttl    ][ path size ][ path ][ value size ][ value ]
-     *     [   1b  ][ 8b (vint) ][   4b (vint)   ][ 4b (vint) ][ 4b (vint) ][  arb ][  4b (vint) ][  arb  ]
-     *
-     * where not all field are always present (in fact, only the [ flags ] are guaranteed to be present). The fields have the following
-     * meaning:
-     *   - [ flags ] is the cell flags. It is a byte for which each bit represents a flag whose meaning is explained below (*_MASK constants)
-     *   - [ timestamp ] is the cell timestamp. Present unless the cell has the USE_TIMESTAMP_MASK.
-     *   - [ deletion time]: the local deletion time for the cell. Present if either the cell is deleted (IS_DELETED_MASK)
-     *       or it is expiring (IS_EXPIRING_MASK) but doesn't have the USE_ROW_TTL_MASK.
-     *   - [ ttl ]: the ttl for the cell. Present if the row is expiring (IS_EXPIRING_MASK) but doesn't have the
-     *       USE_ROW_TTL_MASK.
-     *   - [ value size ] is the size of the [ value ] field. It's present unless either the cell has the HAS_EMPTY_VALUE_MASK, or the value
-     *       for columns of this type have a fixed length.
-     *   - [ path size ] is the size of the [ path ] field. Present iff this is the cell of a complex column.
-     *   - [ value ]: the cell value, unless it has the HAS_EMPTY_VALUE_MASK.
-     *   - [ path ]: the cell path if the column this is a cell of is complex.
-     */
-    static class Serializer implements Cell.Serializer
-    {
-        private final static int IS_DELETED_MASK             = 0x01; // Whether the cell is a tombstone or not.
-        private final static int IS_EXPIRING_MASK            = 0x02; // Whether the cell is expiring.
-        private final static int HAS_EMPTY_VALUE_MASK        = 0x04; // Wether the cell has an empty value. This will be the case for tombstone in particular.
-        private final static int USE_ROW_TIMESTAMP_MASK      = 0x08; // Wether the cell has the same timestamp than the row this is a cell of.
-        private final static int USE_ROW_TTL_MASK            = 0x10; // Wether the cell has the same ttl than the row this is a cell of.
-
-        public void serialize(Cell cell, ColumnDefinition column, DataOutputPlus out, LivenessInfo rowLiveness, SerializationHeader header) throws IOException
-        {
-            assert cell != null;
-            boolean hasValue = cell.value().hasRemaining();
-            boolean isDeleted = cell.isTombstone();
-            boolean isExpiring = cell.isExpiring();
-            boolean useRowTimestamp = !rowLiveness.isEmpty() && cell.timestamp() == rowLiveness.timestamp();
-            boolean useRowTTL = isExpiring && rowLiveness.isExpiring() && cell.ttl() == rowLiveness.ttl() && cell.localDeletionTime() == rowLiveness.localExpirationTime();
-            int flags = 0;
-            if (!hasValue)
-                flags |= HAS_EMPTY_VALUE_MASK;
-
-            if (isDeleted)
-                flags |= IS_DELETED_MASK;
-            else if (isExpiring)
-                flags |= IS_EXPIRING_MASK;
-
-            if (useRowTimestamp)
-                flags |= USE_ROW_TIMESTAMP_MASK;
-            if (useRowTTL)
-                flags |= USE_ROW_TTL_MASK;
-
-            out.writeByte((byte)flags);
-
-            if (!useRowTimestamp)
-                header.writeTimestamp(cell.timestamp(), out);
-
-            if ((isDeleted || isExpiring) && !useRowTTL)
-                header.writeLocalDeletionTime(cell.localDeletionTime(), out);
-            if (isExpiring && !useRowTTL)
-                header.writeTTL(cell.ttl(), out);
-
-            if (column.isComplex())
-                column.cellPathSerializer().serialize(cell.path(), out);
-
-            if (hasValue)
-                header.getType(column).writeValue(cell.value(), out);
-        }
-
-        public Cell deserialize(DataInputPlus in, LivenessInfo rowLiveness, ColumnDefinition column, SerializationHeader header, SerializationHelper helper) throws IOException
-        {
-            int flags = in.readUnsignedByte();
-            boolean hasValue = (flags & HAS_EMPTY_VALUE_MASK) == 0;
-            boolean isDeleted = (flags & IS_DELETED_MASK) != 0;
-            boolean isExpiring = (flags & IS_EXPIRING_MASK) != 0;
-            boolean useRowTimestamp = (flags & USE_ROW_TIMESTAMP_MASK) != 0;
-            boolean useRowTTL = (flags & USE_ROW_TTL_MASK) != 0;
-
-            long timestamp = useRowTimestamp ? rowLiveness.timestamp() : header.readTimestamp(in);
-
-            int localDeletionTime = useRowTTL
-                                  ? rowLiveness.localExpirationTime()
-                                  : (isDeleted || isExpiring ? header.readLocalDeletionTime(in) : NO_DELETION_TIME);
-
-            int ttl = useRowTTL ? rowLiveness.ttl() : (isExpiring ? header.readTTL(in) : NO_TTL);
-
-            CellPath path = column.isComplex()
-                          ? column.cellPathSerializer().deserialize(in)
-                          : null;
-
-            boolean isCounter = localDeletionTime == NO_DELETION_TIME && column.type.isCounter();
-
-            ByteBuffer value = ByteBufferUtil.EMPTY_BYTE_BUFFER;
-            if (hasValue)
-            {
-                if (helper.canSkipValue(column) || (path != null && helper.canSkipValue(path)))
-                {
-                    header.getType(column).skipValue(in);
-                }
-                else
-                {
-                    value = header.getType(column).readValue(in, DatabaseDescriptor.getMaxValueSize());
-                    if (isCounter)
-                        value = helper.maybeClearCounterValue(value);
-                }
-            }
-
-            return new BufferCell(column, timestamp, ttl, localDeletionTime, value, path);
-        }
-
-        public long serializedSize(Cell cell, ColumnDefinition column, LivenessInfo rowLiveness, SerializationHeader header)
-        {
-            long size = 1; // flags
-            boolean hasValue = cell.value().hasRemaining();
-            boolean isDeleted = cell.isTombstone();
-            boolean isExpiring = cell.isExpiring();
-            boolean useRowTimestamp = !rowLiveness.isEmpty() && cell.timestamp() == rowLiveness.timestamp();
-            boolean useRowTTL = isExpiring && rowLiveness.isExpiring() && cell.ttl() == rowLiveness.ttl() && cell.localDeletionTime() == rowLiveness.localExpirationTime();
-
-            if (!useRowTimestamp)
-                size += header.timestampSerializedSize(cell.timestamp());
-
-            if ((isDeleted || isExpiring) && !useRowTTL)
-                size += header.localDeletionTimeSerializedSize(cell.localDeletionTime());
-            if (isExpiring && !useRowTTL)
-                size += header.ttlSerializedSize(cell.ttl());
-
-            if (column.isComplex())
-                size += column.cellPathSerializer().serializedSize(cell.path());
-
-            if (hasValue)
-                size += header.getType(column).writtenLength(cell.value());
-
-            return size;
-        }
-
-        // Returns if the skipped cell was an actual cell (i.e. it had its presence flag).
-        public boolean skip(DataInputPlus in, ColumnDefinition column, SerializationHeader header) throws IOException
-        {
-            int flags = in.readUnsignedByte();
-            boolean hasValue = (flags & HAS_EMPTY_VALUE_MASK) == 0;
-            boolean isDeleted = (flags & IS_DELETED_MASK) != 0;
-            boolean isExpiring = (flags & IS_EXPIRING_MASK) != 0;
-            boolean useRowTimestamp = (flags & USE_ROW_TIMESTAMP_MASK) != 0;
-            boolean useRowTTL = (flags & USE_ROW_TTL_MASK) != 0;
-
-            if (!useRowTimestamp)
-                header.skipTimestamp(in);
-
-            if (!useRowTTL && (isDeleted || isExpiring))
-                header.skipLocalDeletionTime(in);
-
-            if (!useRowTTL && isExpiring)
-                header.skipTTL(in);
-
-            if (column.isComplex())
-                column.cellPathSerializer().skip(in);
-
-            if (hasValue)
-                header.getType(column).skipValue(in);
-
-            return true;
-        }
-    }
 }
diff --git a/src/java/org/apache/cassandra/db/rows/Cell.java b/src/java/org/apache/cassandra/db/rows/Cell.java
index c69e11f..9e59246 100644
--- a/src/java/org/apache/cassandra/db/rows/Cell.java
+++ b/src/java/org/apache/cassandra/db/rows/Cell.java
@@ -28,9 +28,11 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.Attributes;
+import org.apache.cassandra.config.*;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.memory.AbstractAllocator;
 
 /**
@@ -142,6 +144,13 @@
 
     public abstract Cell withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime);
 
+    /**
+     * Used to apply the same optimization as in {@link Cell.Serializer#deserialize} when
+     * the column is not queried but eventhough it's used for digest calculation.
+     * @return a cell with an empty buffer as value
+     */
+    public abstract Cell withSkippedValue();
+
     public abstract Cell copy(AbstractAllocator allocator);
 
     @Override
@@ -152,15 +161,165 @@
     // Overrides super type to provide a more precise return type.
     public abstract Cell purge(DeletionPurger purger, int nowInSec);
 
-    public interface Serializer
+    /**
+     * The serialization format for cell is:
+     *     [ flags ][ timestamp ][ deletion time ][    ttl    ][ path size ][ path ][ value size ][ value ]
+     *     [   1b  ][ 8b (vint) ][   4b (vint)   ][ 4b (vint) ][ 4b (vint) ][  arb ][  4b (vint) ][  arb  ]
+     *
+     * where not all field are always present (in fact, only the [ flags ] are guaranteed to be present). The fields have the following
+     * meaning:
+     *   - [ flags ] is the cell flags. It is a byte for which each bit represents a flag whose meaning is explained below (*_MASK constants)
+     *   - [ timestamp ] is the cell timestamp. Present unless the cell has the USE_TIMESTAMP_MASK.
+     *   - [ deletion time]: the local deletion time for the cell. Present if either the cell is deleted (IS_DELETED_MASK)
+     *       or it is expiring (IS_EXPIRING_MASK) but doesn't have the USE_ROW_TTL_MASK.
+     *   - [ ttl ]: the ttl for the cell. Present if the row is expiring (IS_EXPIRING_MASK) but doesn't have the
+     *       USE_ROW_TTL_MASK.
+     *   - [ value size ] is the size of the [ value ] field. It's present unless either the cell has the HAS_EMPTY_VALUE_MASK, or the value
+     *       for columns of this type have a fixed length.
+     *   - [ path size ] is the size of the [ path ] field. Present iff this is the cell of a complex column.
+     *   - [ value ]: the cell value, unless it has the HAS_EMPTY_VALUE_MASK.
+     *   - [ path ]: the cell path if the column this is a cell of is complex.
+     */
+    static class Serializer
     {
-        public void serialize(Cell cell, ColumnDefinition column, DataOutputPlus out, LivenessInfo rowLiveness, SerializationHeader header) throws IOException;
+        private final static int IS_DELETED_MASK             = 0x01; // Whether the cell is a tombstone or not.
+        private final static int IS_EXPIRING_MASK            = 0x02; // Whether the cell is expiring.
+        private final static int HAS_EMPTY_VALUE_MASK        = 0x04; // Wether the cell has an empty value. This will be the case for tombstone in particular.
+        private final static int USE_ROW_TIMESTAMP_MASK      = 0x08; // Wether the cell has the same timestamp than the row this is a cell of.
+        private final static int USE_ROW_TTL_MASK            = 0x10; // Wether the cell has the same ttl than the row this is a cell of.
 
-        public Cell deserialize(DataInputPlus in, LivenessInfo rowLiveness, ColumnDefinition column, SerializationHeader header, SerializationHelper helper) throws IOException;
+        public void serialize(Cell cell, ColumnDefinition column, DataOutputPlus out, LivenessInfo rowLiveness, SerializationHeader header) throws IOException
+        {
+            assert cell != null;
+            boolean hasValue = cell.value().hasRemaining();
+            boolean isDeleted = cell.isTombstone();
+            boolean isExpiring = cell.isExpiring();
+            boolean useRowTimestamp = !rowLiveness.isEmpty() && cell.timestamp() == rowLiveness.timestamp();
+            boolean useRowTTL = isExpiring && rowLiveness.isExpiring() && cell.ttl() == rowLiveness.ttl() && cell.localDeletionTime() == rowLiveness.localExpirationTime();
+            int flags = 0;
+            if (!hasValue)
+                flags |= HAS_EMPTY_VALUE_MASK;
 
-        public long serializedSize(Cell cell, ColumnDefinition column, LivenessInfo rowLiveness, SerializationHeader header);
+            if (isDeleted)
+                flags |= IS_DELETED_MASK;
+            else if (isExpiring)
+                flags |= IS_EXPIRING_MASK;
+
+            if (useRowTimestamp)
+                flags |= USE_ROW_TIMESTAMP_MASK;
+            if (useRowTTL)
+                flags |= USE_ROW_TTL_MASK;
+
+            out.writeByte((byte)flags);
+
+            if (!useRowTimestamp)
+                header.writeTimestamp(cell.timestamp(), out);
+
+            if ((isDeleted || isExpiring) && !useRowTTL)
+                header.writeLocalDeletionTime(cell.localDeletionTime(), out);
+            if (isExpiring && !useRowTTL)
+                header.writeTTL(cell.ttl(), out);
+
+            if (column.isComplex())
+                column.cellPathSerializer().serialize(cell.path(), out);
+
+            if (hasValue)
+                header.getType(column).writeValue(cell.value(), out);
+        }
+
+        public Cell deserialize(DataInputPlus in, LivenessInfo rowLiveness, ColumnDefinition column, SerializationHeader header, SerializationHelper helper) throws IOException
+        {
+            int flags = in.readUnsignedByte();
+            boolean hasValue = (flags & HAS_EMPTY_VALUE_MASK) == 0;
+            boolean isDeleted = (flags & IS_DELETED_MASK) != 0;
+            boolean isExpiring = (flags & IS_EXPIRING_MASK) != 0;
+            boolean useRowTimestamp = (flags & USE_ROW_TIMESTAMP_MASK) != 0;
+            boolean useRowTTL = (flags & USE_ROW_TTL_MASK) != 0;
+
+            long timestamp = useRowTimestamp ? rowLiveness.timestamp() : header.readTimestamp(in);
+
+            int localDeletionTime = useRowTTL
+                                    ? rowLiveness.localExpirationTime()
+                                    : (isDeleted || isExpiring ? header.readLocalDeletionTime(in) : NO_DELETION_TIME);
+
+            int ttl = useRowTTL ? rowLiveness.ttl() : (isExpiring ? header.readTTL(in) : NO_TTL);
+
+            CellPath path = column.isComplex()
+                            ? column.cellPathSerializer().deserialize(in)
+                            : null;
+
+            ByteBuffer value = ByteBufferUtil.EMPTY_BYTE_BUFFER;
+            if (hasValue)
+            {
+                if (helper.canSkipValue(column) || (path != null && helper.canSkipValue(path)))
+                {
+                    header.getType(column).skipValue(in);
+                }
+                else
+                {
+                    boolean isCounter = localDeletionTime == NO_DELETION_TIME && column.type.isCounter();
+
+                    value = header.getType(column).readValue(in, DatabaseDescriptor.getMaxValueSize());
+                    if (isCounter)
+                        value = helper.maybeClearCounterValue(value);
+                }
+            }
+
+            return new BufferCell(column, timestamp, ttl, localDeletionTime, value, path);
+        }
+
+        public long serializedSize(Cell cell, ColumnDefinition column, LivenessInfo rowLiveness, SerializationHeader header)
+        {
+            long size = 1; // flags
+            boolean hasValue = cell.value().hasRemaining();
+            boolean isDeleted = cell.isTombstone();
+            boolean isExpiring = cell.isExpiring();
+            boolean useRowTimestamp = !rowLiveness.isEmpty() && cell.timestamp() == rowLiveness.timestamp();
+            boolean useRowTTL = isExpiring && rowLiveness.isExpiring() && cell.ttl() == rowLiveness.ttl() && cell.localDeletionTime() == rowLiveness.localExpirationTime();
+
+            if (!useRowTimestamp)
+                size += header.timestampSerializedSize(cell.timestamp());
+
+            if ((isDeleted || isExpiring) && !useRowTTL)
+                size += header.localDeletionTimeSerializedSize(cell.localDeletionTime());
+            if (isExpiring && !useRowTTL)
+                size += header.ttlSerializedSize(cell.ttl());
+
+            if (column.isComplex())
+                size += column.cellPathSerializer().serializedSize(cell.path());
+
+            if (hasValue)
+                size += header.getType(column).writtenLength(cell.value());
+
+            return size;
+        }
 
         // Returns if the skipped cell was an actual cell (i.e. it had its presence flag).
-        public boolean skip(DataInputPlus in, ColumnDefinition column, SerializationHeader header) throws IOException;
+        public boolean skip(DataInputPlus in, ColumnDefinition column, SerializationHeader header) throws IOException
+        {
+            int flags = in.readUnsignedByte();
+            boolean hasValue = (flags & HAS_EMPTY_VALUE_MASK) == 0;
+            boolean isDeleted = (flags & IS_DELETED_MASK) != 0;
+            boolean isExpiring = (flags & IS_EXPIRING_MASK) != 0;
+            boolean useRowTimestamp = (flags & USE_ROW_TIMESTAMP_MASK) != 0;
+            boolean useRowTTL = (flags & USE_ROW_TTL_MASK) != 0;
+
+            if (!useRowTimestamp)
+                header.skipTimestamp(in);
+
+            if (!useRowTTL && (isDeleted || isExpiring))
+                header.skipLocalDeletionTime(in);
+
+            if (!useRowTTL && isExpiring)
+                header.skipTTL(in);
+
+            if (column.isComplex())
+                column.cellPathSerializer().skip(in);
+
+            if (hasValue)
+                header.getType(column).skipValue(in);
+
+            return true;
+        }
     }
 }
diff --git a/src/java/org/apache/cassandra/db/rows/CellPath.java b/src/java/org/apache/cassandra/db/rows/CellPath.java
index 68e3c2b..91a5217 100644
--- a/src/java/org/apache/cassandra/db/rows/CellPath.java
+++ b/src/java/org/apache/cassandra/db/rows/CellPath.java
@@ -39,11 +39,11 @@
     public abstract int size();
     public abstract ByteBuffer get(int i);
 
-    // The only complex we currently have are collections that have only one value.
+    // The only complex paths we currently have are collections and UDTs, which both have a depth of one
     public static CellPath create(ByteBuffer value)
     {
         assert value != null;
-        return new CollectionCellPath(value);
+        return new SingleItemCellPath(value);
     }
 
     public int dataSize()
@@ -98,13 +98,13 @@
         public void skip(DataInputPlus in) throws IOException;
     }
 
-    private static class CollectionCellPath extends CellPath
+    private static class SingleItemCellPath extends CellPath
     {
-        private static final long EMPTY_SIZE = ObjectSizes.measure(new CollectionCellPath(ByteBufferUtil.EMPTY_BYTE_BUFFER));
+        private static final long EMPTY_SIZE = ObjectSizes.measure(new SingleItemCellPath(ByteBufferUtil.EMPTY_BYTE_BUFFER));
 
         protected final ByteBuffer value;
 
-        private CollectionCellPath(ByteBuffer value)
+        private SingleItemCellPath(ByteBuffer value)
         {
             this.value = value;
         }
@@ -122,7 +122,7 @@
 
         public CellPath copy(AbstractAllocator allocator)
         {
-            return new CollectionCellPath(allocator.clone(value));
+            return new SingleItemCellPath(allocator.clone(value));
         }
 
         public long unsharedHeapSizeExcludingData()
diff --git a/src/java/org/apache/cassandra/db/rows/Cells.java b/src/java/org/apache/cassandra/db/rows/Cells.java
index 54df26e..38bde16 100644
--- a/src/java/org/apache/cassandra/db/rows/Cells.java
+++ b/src/java/org/apache/cassandra/db/rows/Cells.java
@@ -233,6 +233,86 @@
         return timeDelta;
     }
 
+    /**
+     * Adds to the builder a representation of the given existing cell that, when merged/reconciled with the given
+     * update cell, produces the same result as merging the original with the update.
+     * <p>
+     * For simple cells that is either the original cell (if still live), or nothing (if shadowed).
+     *
+     * @param existing the pre-existing cell, the one that is updated.
+     * @param update the newly added cell, the update. This can be {@code null} out
+     * of convenience, in which case this function simply copy {@code existing} to
+     * {@code writer}.
+     * @param deletion the deletion time that applies to the cells being considered.
+     * This deletion time may delete both {@code existing} or {@code update}.
+     * @param builder the row builder to which the result of the filtering is written.
+     * @param nowInSec the current time in seconds (which plays a role during reconciliation
+     * because deleted cells always have precedence on timestamp equality and deciding if a
+     * cell is a live or not depends on the current time due to expiring cells).
+     */
+    public static void addNonShadowed(Cell existing,
+                                      Cell update,
+                                      DeletionTime deletion,
+                                      Row.Builder builder,
+                                      int nowInSec)
+    {
+        if (deletion.deletes(existing))
+            return;
+
+        Cell reconciled = reconcile(existing, update, nowInSec);
+        if (reconciled != update)
+            builder.addCell(existing);
+    }
+
+    /**
+     * Adds to the builder a representation of the given existing cell that, when merged/reconciled with the given
+     * update cell, produces the same result as merging the original with the update.
+     * <p>
+     * For simple cells that is either the original cell (if still live), or nothing (if shadowed).
+     *
+     * @param column the complex column the cells are for.
+     * @param existing the pre-existing cells, the ones that are updated.
+     * @param update the newly added cells, the update. This can be {@code null} out
+     * of convenience, in which case this function simply copy the cells from
+     * {@code existing} to {@code writer}.
+     * @param deletion the deletion time that applies to the cells being considered.
+     * This deletion time may delete both {@code existing} or {@code update}.
+     * @param builder the row builder to which the result of the filtering is written.
+     * @param nowInSec the current time in seconds (which plays a role during reconciliation
+     * because deleted cells always have precedence on timestamp equality and deciding if a
+     * cell is a live or not depends on the current time due to expiring cells).
+     */
+    public static void addNonShadowedComplex(ColumnDefinition column,
+                                             Iterator<Cell> existing,
+                                             Iterator<Cell> update,
+                                             DeletionTime deletion,
+                                             Row.Builder builder,
+                                             int nowInSec)
+    {
+        Comparator<CellPath> comparator = column.cellPathComparator();
+        Cell nextExisting = getNext(existing);
+        Cell nextUpdate = getNext(update);
+        while (nextExisting != null)
+        {
+            int cmp = nextUpdate == null ? -1 : comparator.compare(nextExisting.path(), nextUpdate.path());
+            if (cmp < 0)
+            {
+                addNonShadowed(nextExisting, null, deletion, builder, nowInSec);
+                nextExisting = getNext(existing);
+            }
+            else if (cmp == 0)
+            {
+                addNonShadowed(nextExisting, nextUpdate, deletion, builder, nowInSec);
+                nextExisting = getNext(existing);
+                nextUpdate = getNext(update);
+            }
+            else
+            {
+                nextUpdate = getNext(update);
+            }
+        }
+    }
+
     private static Cell getNext(Iterator<Cell> iterator)
     {
         return iterator == null || !iterator.hasNext() ? null : iterator.next();
diff --git a/src/java/org/apache/cassandra/db/rows/ColumnDefinitionVersionComparator.java b/src/java/org/apache/cassandra/db/rows/ColumnDefinitionVersionComparator.java
new file mode 100644
index 0000000..9be24c8
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/rows/ColumnDefinitionVersionComparator.java
@@ -0,0 +1,85 @@
+/*
+ * 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.db.rows;
+
+import java.util.Comparator;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.db.marshal.*;
+
+/**
+ * A {@code Comparator} use to determine which version of a {@link ColumnDefinition} should be used.
+ * <p>
+ * We can sometimes get 2 different versions of the definition of a give column due to differing types. This can happen
+ * in at least 2 cases:
+ * <ul>
+ *     <li>for UDT, where new fields can be added (see CASSANDRA-13776).</li>
+ *     <li>pre-CASSANDRA-12443, when we allowed type altering. And while we don't allow it anymore, it is possible
+ *     to still have sstables with metadata mentioning an old pre-altering type (such old version of pre-altering
+ *     types will be eventually eliminated from the system by compaction and thanks to this comparator, but we
+ *     cannot guarantee when that's fully done).</li>
+ * </ul>
+ */
+final class ColumnDefinitionVersionComparator implements Comparator<ColumnDefinition>
+{
+    public static final Comparator<ColumnDefinition> INSTANCE = new ColumnDefinitionVersionComparator();
+
+    private ColumnDefinitionVersionComparator()
+    {
+    }
+
+    @Override
+    public int compare(ColumnDefinition v1, ColumnDefinition v2)
+    {
+        assert v1.ksName.equals(v2.ksName)
+               && v1.cfName.equals(v2.cfName)
+               && v1.name.equals(v2.name) : v1.debugString() + " != " + v2.debugString();
+
+        AbstractType<?> v1Type = v1.type;
+        AbstractType<?> v2Type = v2.type;
+
+        // In most cases, this is used on equal types, and on most types, equality is cheap (most are singleton classes
+        // and just use reference equality), so evacuating that case first.
+        if (v1Type.equals(v2Type))
+            return 0;
+
+        // If those aren't the same type, one must be "more general" than the other, that is accept strictly more values.
+        if (v1Type.isValueCompatibleWith(v2Type))
+        {
+            // Note: if both accept the same values, there is really no good way to prefer one over the other and so we
+            // consider them equal here. In practice, this mean we have 2 types that accepts the same values but are
+            // not equal. For internal types, TimestampType/DataType/LongType is, afaik, the only example, but as user
+            // can write custom types, who knows when this can happen. But excluding any user custom type weirdness
+            // (that would really be a bug of their type), such types should only differ in the way they sort, and as
+            // this method is only used for regular/static columns in practice, where sorting has no impact whatsoever,
+            // it shouldn't matter too much what we return here.
+            return v2Type.isValueCompatibleWith(v1Type) ? 0 : 1;
+        }
+        else if (v2Type.isValueCompatibleWith(v1Type))
+        {
+            return -1;
+        }
+        else
+        {
+            // Neither is a super type of the other: something is pretty wrong and we probably shouldn't ignore it.
+            throw new IllegalArgumentException(String.format("Found 2 incompatible versions of column %s in %s.%s: one " +
+                                                             "of type %s and one of type %s (but both types are incompatible)",
+                                                             v1.name, v1.ksName, v1.cfName, v1Type, v2Type));
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java
index e768769..2dea162 100644
--- a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java
+++ b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java
@@ -19,22 +19,21 @@
 
 import java.nio.ByteBuffer;
 import java.security.MessageDigest;
-import java.util.*;
-import java.util.function.BiFunction;
+import java.util.Iterator;
+import java.util.Objects;
 
 import com.google.common.base.Function;
-import com.google.common.collect.Iterables;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.db.DeletionTime;
 import org.apache.cassandra.db.DeletionPurger;
+import org.apache.cassandra.db.DeletionTime;
+import org.apache.cassandra.db.LivenessInfo;
 import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.marshal.ByteType;
 import org.apache.cassandra.db.marshal.SetType;
 import org.apache.cassandra.utils.ObjectSizes;
 import org.apache.cassandra.utils.btree.BTree;
-import org.apache.cassandra.utils.btree.UpdateFunction;
 
 /**
  * The data for a complex column, that is it's cells and potential complex
@@ -144,19 +143,28 @@
         return transformAndFilter(complexDeletion, Cell::markCounterLocalToBeCleared);
     }
 
-    public ComplexColumnData filter(ColumnFilter filter, DeletionTime activeDeletion, CFMetaData.DroppedColumn dropped)
+    public ComplexColumnData filter(ColumnFilter filter, DeletionTime activeDeletion, CFMetaData.DroppedColumn dropped, LivenessInfo rowLiveness)
     {
         ColumnFilter.Tester cellTester = filter.newTester(column);
-        if (cellTester == null && activeDeletion.isLive() && dropped == null)
+        boolean isQueriedColumn = filter.fetchedColumnIsQueried(column);
+        if (cellTester == null && activeDeletion.isLive() && dropped == null && isQueriedColumn)
             return this;
 
         DeletionTime newDeletion = activeDeletion.supersedes(complexDeletion) ? DeletionTime.LIVE : complexDeletion;
-        return transformAndFilter(newDeletion,
-                                  (cell) ->
-                                           (cellTester == null || cellTester.includes(cell.path()))
-                                        && !activeDeletion.deletes(cell)
-                                        && (dropped == null || cell.timestamp() > dropped.droppedTime)
-                                           ? cell : null);
+        return transformAndFilter(newDeletion, (cell) ->
+        {
+            CellPath path = cell.path();
+            boolean isForDropped = dropped != null && cell.timestamp() <= dropped.droppedTime;
+            boolean isShadowed = activeDeletion.deletes(cell);
+            boolean isFetchedCell = cellTester == null || cellTester.fetches(path);
+            boolean isQueriedCell = isQueriedColumn && isFetchedCell && (cellTester == null || cellTester.fetchedCellIsQueried(path));
+            boolean isSkippableCell = !isFetchedCell || (!isQueriedCell && cell.timestamp() < rowLiveness.timestamp());
+            if (isForDropped || isShadowed || isSkippableCell)
+                return null;
+            // We should apply the same "optimization" as in Cell.deserialize to avoid discrepances
+            // between sstables and memtables data, i.e resulting in a digest mismatch.
+            return isQueriedCell ? cell : cell.withSkippedValue();
+        });
     }
 
     public ComplexColumnData purge(DeletionPurger purger, int nowInSec)
@@ -165,6 +173,11 @@
         return transformAndFilter(newDeletion, (cell) -> cell.purge(purger, nowInSec));
     }
 
+    public ComplexColumnData withOnlyQueriedData(ColumnFilter filter)
+    {
+        return transformAndFilter(complexDeletion, (cell) -> filter.fetchedCellIsQueried(column, cell.path()) ? null : cell);
+    }
+
     private ComplexColumnData transformAndFilter(DeletionTime newDeletion, Function<? super Cell, ? extends Cell> function)
     {
         Object[] transformed = BTree.transformAndFilter(cells, function);
@@ -228,10 +241,6 @@
 
     public static class Builder
     {
-        private static BiFunction<Cell, Cell, Cell> noResolve = (a, b) -> {
-            throw new IllegalStateException();
-        };
-
         private DeletionTime complexDeletion;
         private ColumnDefinition column;
         private BTree.Builder<Cell> builder;
@@ -240,8 +249,10 @@
         {
             this.column = column;
             this.complexDeletion = DeletionTime.LIVE; // default if writeComplexDeletion is not called
-            if (builder == null) builder = BTree.builder(column.cellComparator());
-            else builder.reuse(column.cellComparator());
+            if (builder == null)
+                builder = BTree.builder(column.cellComparator());
+            else
+                builder.reuse(column.cellComparator());
         }
 
         public void addComplexDeletion(DeletionTime complexDeletion)
diff --git a/src/java/org/apache/cassandra/db/rows/LazilyInitializedUnfilteredRowIterator.java b/src/java/org/apache/cassandra/db/rows/LazilyInitializedUnfilteredRowIterator.java
index 8ba4394..504d60e 100644
--- a/src/java/org/apache/cassandra/db/rows/LazilyInitializedUnfilteredRowIterator.java
+++ b/src/java/org/apache/cassandra/db/rows/LazilyInitializedUnfilteredRowIterator.java
@@ -27,7 +27,7 @@
  *
  * This is used during partition range queries when we know the partition key but want
  * to defer the initialization of the rest of the UnfilteredRowIterator until we need those informations.
- * See {@link BigTableScanner#KeyScanningIterator} for instance.
+ * See {@link org.apache.cassandra.io.sstable.format.big.BigTableScanner.KeyScanningIterator} for instance.
  */
 public abstract class LazilyInitializedUnfilteredRowIterator extends AbstractIterator<Unfiltered> implements UnfilteredRowIterator
 {
@@ -42,12 +42,17 @@
 
     protected abstract UnfilteredRowIterator initializeIterator();
 
-    private void maybeInit()
+    protected void maybeInit()
     {
         if (iterator == null)
             iterator = initializeIterator();
     }
 
+    public boolean initialized()
+    {
+        return iterator != null;
+    }
+
     public CFMetaData metadata()
     {
         maybeInit();
diff --git a/src/java/org/apache/cassandra/db/rows/NativeCell.java b/src/java/org/apache/cassandra/db/rows/NativeCell.java
new file mode 100644
index 0000000..1f8c258
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/rows/NativeCell.java
@@ -0,0 +1,167 @@
+/*
+ * 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.db.rows;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.concurrent.OpOrder;
+import org.apache.cassandra.utils.memory.MemoryUtil;
+import org.apache.cassandra.utils.memory.NativeAllocator;
+
+public class NativeCell extends AbstractCell
+{
+    private static final long EMPTY_SIZE = ObjectSizes.measure(new NativeCell());
+
+    private static final long HAS_CELLPATH = 0;
+    private static final long TIMESTAMP = 1;
+    private static final long TTL = 9;
+    private static final long DELETION = 13;
+    private static final long LENGTH = 17;
+    private static final long VALUE = 21;
+
+    private final long peer;
+
+    private NativeCell()
+    {
+        super(null);
+        this.peer = 0;
+    }
+
+    public NativeCell(NativeAllocator allocator,
+                      OpOrder.Group writeOp,
+                      Cell cell)
+    {
+        this(allocator,
+             writeOp,
+             cell.column(),
+             cell.timestamp(),
+             cell.ttl(),
+             cell.localDeletionTime(),
+             cell.value(),
+             cell.path());
+    }
+
+    public NativeCell(NativeAllocator allocator,
+                      OpOrder.Group writeOp,
+                      ColumnDefinition column,
+                      long timestamp,
+                      int ttl,
+                      int localDeletionTime,
+                      ByteBuffer value,
+                      CellPath path)
+    {
+        super(column);
+        long size = simpleSize(value.remaining());
+
+        assert value.order() == ByteOrder.BIG_ENDIAN;
+        assert column.isComplex() == (path != null);
+        if (path != null)
+        {
+            assert path.size() == 1;
+            size += 4 + path.get(0).remaining();
+        }
+
+        if (size > Integer.MAX_VALUE)
+            throw new IllegalStateException();
+
+        // cellpath? : timestamp : ttl : localDeletionTime : length : <data> : [cell path length] : [<cell path data>]
+        peer = allocator.allocate((int) size, writeOp);
+        MemoryUtil.setByte(peer + HAS_CELLPATH, (byte)(path == null ? 0 : 1));
+        MemoryUtil.setLong(peer + TIMESTAMP, timestamp);
+        MemoryUtil.setInt(peer + TTL, ttl);
+        MemoryUtil.setInt(peer + DELETION, localDeletionTime);
+        MemoryUtil.setInt(peer + LENGTH, value.remaining());
+        MemoryUtil.setBytes(peer + VALUE, value);
+
+        if (path != null)
+        {
+            ByteBuffer pathbuffer = path.get(0);
+            assert pathbuffer.order() == ByteOrder.BIG_ENDIAN;
+
+            long offset = peer + VALUE + value.remaining();
+            MemoryUtil.setInt(offset, pathbuffer.remaining());
+            MemoryUtil.setBytes(offset + 4, pathbuffer);
+        }
+    }
+
+    private static long simpleSize(int length)
+    {
+        return VALUE + length;
+    }
+
+    public long timestamp()
+    {
+        return MemoryUtil.getLong(peer + TIMESTAMP);
+    }
+
+    public int ttl()
+    {
+        return MemoryUtil.getInt(peer + TTL);
+    }
+
+    public int localDeletionTime()
+    {
+        return MemoryUtil.getInt(peer + DELETION);
+    }
+
+    public ByteBuffer value()
+    {
+        int length = MemoryUtil.getInt(peer + LENGTH);
+        return MemoryUtil.getByteBuffer(peer + VALUE, length, ByteOrder.BIG_ENDIAN);
+    }
+
+    public CellPath path()
+    {
+        if (MemoryUtil.getByte(peer+ HAS_CELLPATH) == 0)
+            return null;
+
+        long offset = peer + VALUE + MemoryUtil.getInt(peer + LENGTH);
+        int size = MemoryUtil.getInt(offset);
+        return CellPath.create(MemoryUtil.getByteBuffer(offset + 4, size, ByteOrder.BIG_ENDIAN));
+    }
+
+    public Cell withUpdatedValue(ByteBuffer newValue)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public Cell withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
+    {
+        return new BufferCell(column, newTimestamp, ttl(), newLocalDeletionTime, value(), path());
+    }
+
+    public Cell withUpdatedColumn(ColumnDefinition column)
+    {
+        return new BufferCell(column, timestamp(), ttl(), localDeletionTime(), value(), path());
+    }
+
+    public Cell withSkippedValue()
+    {
+        return new BufferCell(column, timestamp(), ttl(), localDeletionTime(), ByteBufferUtil.EMPTY_BYTE_BUFFER, path());
+    }
+
+    public long unsharedHeapSizeExcludingData()
+    {
+        return EMPTY_SIZE;
+    }
+
+}
diff --git a/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundMarker.java b/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundMarker.java
index 0079114..f6ba149 100644
--- a/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundMarker.java
+++ b/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundMarker.java
@@ -20,6 +20,7 @@
 import java.nio.ByteBuffer;
 import java.security.MessageDigest;
 import java.util.Objects;
+import java.util.Set;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
@@ -28,43 +29,37 @@
 /**
  * A range tombstone marker that indicates the bound of a range tombstone (start or end).
  */
-public class RangeTombstoneBoundMarker extends AbstractRangeTombstoneMarker
+public class RangeTombstoneBoundMarker extends AbstractRangeTombstoneMarker<ClusteringBound>
 {
     private final DeletionTime deletion;
 
-    public RangeTombstoneBoundMarker(RangeTombstone.Bound bound, DeletionTime deletion)
+    public RangeTombstoneBoundMarker(ClusteringBound bound, DeletionTime deletion)
     {
         super(bound);
-        assert !bound.isBoundary();
         this.deletion = deletion;
     }
 
-    public RangeTombstoneBoundMarker(Slice.Bound bound, DeletionTime deletion)
-    {
-        this(new RangeTombstone.Bound(bound.kind(), bound.getRawValues()), deletion);
-    }
-
     public static RangeTombstoneBoundMarker inclusiveOpen(boolean reversed, ByteBuffer[] boundValues, DeletionTime deletion)
     {
-        RangeTombstone.Bound bound = RangeTombstone.Bound.inclusiveOpen(reversed, boundValues);
+        ClusteringBound bound = ClusteringBound.inclusiveOpen(reversed, boundValues);
         return new RangeTombstoneBoundMarker(bound, deletion);
     }
 
     public static RangeTombstoneBoundMarker exclusiveOpen(boolean reversed, ByteBuffer[] boundValues, DeletionTime deletion)
     {
-        RangeTombstone.Bound bound = RangeTombstone.Bound.exclusiveOpen(reversed, boundValues);
+        ClusteringBound bound = ClusteringBound.exclusiveOpen(reversed, boundValues);
         return new RangeTombstoneBoundMarker(bound, deletion);
     }
 
     public static RangeTombstoneBoundMarker inclusiveClose(boolean reversed, ByteBuffer[] boundValues, DeletionTime deletion)
     {
-        RangeTombstone.Bound bound = RangeTombstone.Bound.inclusiveClose(reversed, boundValues);
+        ClusteringBound bound = ClusteringBound.inclusiveClose(reversed, boundValues);
         return new RangeTombstoneBoundMarker(bound, deletion);
     }
 
     public static RangeTombstoneBoundMarker exclusiveClose(boolean reversed, ByteBuffer[] boundValues, DeletionTime deletion)
     {
-        RangeTombstone.Bound bound = RangeTombstone.Bound.exclusiveClose(reversed, boundValues);
+        ClusteringBound bound = ClusteringBound.exclusiveClose(reversed, boundValues);
         return new RangeTombstoneBoundMarker(bound, deletion);
     }
 
@@ -109,12 +104,12 @@
         return bound.isInclusive();
     }
 
-    public RangeTombstone.Bound openBound(boolean reversed)
+    public ClusteringBound openBound(boolean reversed)
     {
         return isOpen(reversed) ? clustering() : null;
     }
 
-    public RangeTombstone.Bound closeBound(boolean reversed)
+    public ClusteringBound closeBound(boolean reversed)
     {
         return isClose(reversed) ? clustering() : null;
     }
@@ -138,6 +133,12 @@
         deletion.digest(digest);
     }
 
+    @Override
+    public void digest(MessageDigest digest, Set<ByteBuffer> columnsToExclude)
+    {
+        digest(digest);
+    }
+
     public String toString(CFMetaData metadata)
     {
         return String.format("Marker %s@%d/%d", bound.toString(metadata), deletion.markedForDeleteAt(), deletion.localDeletionTime());
diff --git a/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundaryMarker.java b/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundaryMarker.java
index f0f5421..ad71784 100644
--- a/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundaryMarker.java
+++ b/src/java/org/apache/cassandra/db/rows/RangeTombstoneBoundaryMarker.java
@@ -20,6 +20,7 @@
 import java.nio.ByteBuffer;
 import java.security.MessageDigest;
 import java.util.Objects;
+import java.util.Set;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
@@ -28,12 +29,12 @@
 /**
  * A range tombstone marker that represents a boundary between 2 range tombstones (i.e. it closes one range and open another).
  */
-public class RangeTombstoneBoundaryMarker extends AbstractRangeTombstoneMarker
+public class RangeTombstoneBoundaryMarker extends AbstractRangeTombstoneMarker<ClusteringBoundary>
 {
     private final DeletionTime endDeletion;
     private final DeletionTime startDeletion;
 
-    public RangeTombstoneBoundaryMarker(RangeTombstone.Bound bound, DeletionTime endDeletion, DeletionTime startDeletion)
+    public RangeTombstoneBoundaryMarker(ClusteringBoundary bound, DeletionTime endDeletion, DeletionTime startDeletion)
     {
         super(bound);
         assert bound.isBoundary();
@@ -43,7 +44,7 @@
 
     public static RangeTombstoneBoundaryMarker exclusiveCloseInclusiveOpen(boolean reversed, ByteBuffer[] boundValues, DeletionTime closeDeletion, DeletionTime openDeletion)
     {
-        RangeTombstone.Bound bound = RangeTombstone.Bound.exclusiveCloseInclusiveOpen(reversed, boundValues);
+        ClusteringBoundary bound = ClusteringBoundary.exclusiveCloseInclusiveOpen(reversed, boundValues);
         DeletionTime endDeletion = reversed ? openDeletion : closeDeletion;
         DeletionTime startDeletion = reversed ? closeDeletion : openDeletion;
         return new RangeTombstoneBoundaryMarker(bound, endDeletion, startDeletion);
@@ -51,7 +52,7 @@
 
     public static RangeTombstoneBoundaryMarker inclusiveCloseExclusiveOpen(boolean reversed, ByteBuffer[] boundValues, DeletionTime closeDeletion, DeletionTime openDeletion)
     {
-        RangeTombstone.Bound bound = RangeTombstone.Bound.inclusiveCloseExclusiveOpen(reversed, boundValues);
+        ClusteringBoundary bound = ClusteringBoundary.inclusiveCloseExclusiveOpen(reversed, boundValues);
         DeletionTime endDeletion = reversed ? openDeletion : closeDeletion;
         DeletionTime startDeletion = reversed ? closeDeletion : openDeletion;
         return new RangeTombstoneBoundaryMarker(bound, endDeletion, startDeletion);
@@ -88,14 +89,14 @@
         return (bound.kind() == ClusteringPrefix.Kind.EXCL_END_INCL_START_BOUNDARY) ^ reversed;
     }
 
-    public RangeTombstone.Bound openBound(boolean reversed)
+    public ClusteringBound openBound(boolean reversed)
     {
-        return bound.withNewKind(bound.kind().openBoundOfBoundary(reversed));
+        return bound.openBound(reversed);
     }
 
-    public RangeTombstone.Bound closeBound(boolean reversed)
+    public ClusteringBound closeBound(boolean reversed)
     {
-        return bound.withNewKind(bound.kind().closeBoundOfBoundary(reversed));
+        return bound.closeBound(reversed);
     }
 
     public boolean closeIsInclusive(boolean reversed)
@@ -125,9 +126,9 @@
         return new RangeTombstoneBoundaryMarker(clustering(), reversed ? newDeletionTime : endDeletion, reversed ? startDeletion : newDeletionTime);
     }
 
-    public static RangeTombstoneBoundaryMarker makeBoundary(boolean reversed, Slice.Bound close, Slice.Bound open, DeletionTime closeDeletion, DeletionTime openDeletion)
+    public static RangeTombstoneBoundaryMarker makeBoundary(boolean reversed, ClusteringBound close, ClusteringBound open, DeletionTime closeDeletion, DeletionTime openDeletion)
     {
-        assert RangeTombstone.Bound.Kind.compare(close.kind(), open.kind()) == 0 : "Both bound don't form a boundary";
+        assert ClusteringPrefix.Kind.compare(close.kind(), open.kind()) == 0 : "Both bound don't form a boundary";
         boolean isExclusiveClose = close.isExclusive() || (close.isInclusive() && open.isInclusive() && openDeletion.supersedes(closeDeletion));
         return isExclusiveClose
              ? exclusiveCloseInclusiveOpen(reversed, close.getRawValues(), closeDeletion, openDeletion)
@@ -151,6 +152,12 @@
         startDeletion.digest(digest);
     }
 
+    @Override
+    public void digest(MessageDigest digest, Set<ByteBuffer> columnsToExclude)
+    {
+        digest(digest);
+    }
+
     public String toString(CFMetaData metadata)
     {
         return String.format("Marker %s@%d/%d-%d/%d",
diff --git a/src/java/org/apache/cassandra/db/rows/RangeTombstoneMarker.java b/src/java/org/apache/cassandra/db/rows/RangeTombstoneMarker.java
index dee7231..f4bcdf5 100644
--- a/src/java/org/apache/cassandra/db/rows/RangeTombstoneMarker.java
+++ b/src/java/org/apache/cassandra/db/rows/RangeTombstoneMarker.java
@@ -20,7 +20,6 @@
 import java.nio.ByteBuffer;
 import java.util.*;
 
-import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.utils.memory.AbstractAllocator;
 
@@ -32,7 +31,7 @@
 public interface RangeTombstoneMarker extends Unfiltered
 {
     @Override
-    public RangeTombstone.Bound clustering();
+    public ClusteringBoundOrBoundary clustering();
 
     public boolean isBoundary();
 
@@ -44,8 +43,8 @@
     public boolean openIsInclusive(boolean reversed);
     public boolean closeIsInclusive(boolean reversed);
 
-    public RangeTombstone.Bound openBound(boolean reversed);
-    public RangeTombstone.Bound closeBound(boolean reversed);
+    public ClusteringBound openBound(boolean reversed);
+    public ClusteringBound closeBound(boolean reversed);
 
     public RangeTombstoneMarker copy(AbstractAllocator allocator);
 
@@ -75,7 +74,7 @@
         private final DeletionTime partitionDeletion;
         private final boolean reversed;
 
-        private RangeTombstone.Bound bound;
+        private ClusteringBoundOrBoundary bound;
         private final RangeTombstoneMarker[] markers;
 
         // For each iterator, what is the currently open marker deletion time (or null if there is no open marker on that iterator)
diff --git a/src/java/org/apache/cassandra/db/rows/Row.java b/src/java/org/apache/cassandra/db/rows/Row.java
index dcb78f3..dd8e303 100644
--- a/src/java/org/apache/cassandra/db/rows/Row.java
+++ b/src/java/org/apache/cassandra/db/rows/Row.java
@@ -19,6 +19,9 @@
 
 import java.util.*;
 import java.security.MessageDigest;
+import java.util.function.Consumer;
+
+import com.google.common.base.Predicate;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
@@ -236,6 +239,16 @@
     public Row purge(DeletionPurger purger, int nowInSec, boolean enforceStrictLiveness);
 
     /**
+     * Returns a copy of this row which only include the data queried by {@code filter}, excluding anything _fetched_ for
+     * internal reasons but not queried by the user (see {@link ColumnFilter} for details).
+     *
+     * @param filter the {@code ColumnFilter} to use when deciding what is user queried. This should be the filter
+     * that was used when querying the row on which this method is called.
+     * @return the row but with all data that wasn't queried by the user skipped.
+     */
+    public Row withOnlyQueriedData(ColumnFilter filter);
+
+    /**
      * Returns a copy of this row where all counter cells have they "local" shard marked for clearing.
      */
     public Row markCounterLocalToBeCleared();
@@ -245,8 +258,6 @@
      * timestamp by {@code newTimestamp - 1}.
      *
      * @param newTimestamp the timestamp to use for all live data in the returned row.
-     * @return a copy of this row with timestamp updated using {@code newTimestamp}. This can return {@code null} in the
-     * rare where the row only as a shadowable row deletion and the new timestamp supersedes it.
      *
      * @see Commit for why we need this.
      */
@@ -271,19 +282,29 @@
     public String toString(CFMetaData metadata, boolean fullDetails);
 
     /**
+     * Apply a function to every column in a row
+     */
+    public void apply(Consumer<ColumnData> function, boolean reverse);
+
+    /**
+     * Apply a funtion to every column in a row until a stop condition is reached
+     */
+    public void apply(Consumer<ColumnData> function, Predicate<ColumnData> stopCondition, boolean reverse);
+
+    /**
      * A row deletion/tombstone.
      * <p>
      * A row deletion mostly consists of the time of said deletion, but there is 2 variants: shadowable
      * and regular row deletion.
      * <p>
      * A shadowable row deletion only exists if the row has no timestamp. In other words, the deletion is only
-     * valid as long as no newer insert is done (thus setting a row timestap; note that if the row timestamp set
+     * valid as long as no newer insert is done (thus setting a row timestamp; note that if the row timestamp set
      * is lower than the deletion, it is shadowed (and thus ignored) as usual).
      * <p>
-     * That is, if a row has a shadowable deletion with timestamp A and an update is madeto that row with a
-     * timestamp B such that B > A (and that update sets the row timestamp), then the shadowable deletion is 'shadowed'
+     * That is, if a row has a shadowable deletion with timestamp A and an update is made to that row with a
+     * timestamp B such that {@code B > A} (and that update sets the row timestamp), then the shadowable deletion is 'shadowed'
      * by that update. A concrete consequence is that if said update has cells with timestamp lower than A, then those
-     * cells are preserved(since the deletion is removed), and this contrarily to a normal (regular) deletion where the
+     * cells are preserved(since the deletion is removed), and this is contrary to a normal (regular) deletion where the
      * deletion is preserved and such cells are removed.
      * <p>
      * Currently, the only use of shadowable row deletions is Materialized Views, see CASSANDRA-10261.
@@ -461,11 +482,11 @@
         public Clustering clustering();
 
         /**
-         * Adds the liveness information for the primary key columns of this row.
+         * Adds the liveness information for the partition key columns of this row.
          *
          * This call is optional (skipping it is equivalent to calling {@code addPartitionKeyLivenessInfo(LivenessInfo.NONE)}).
          *
-         * @param info the liveness information for the primary key columns of the built row.
+         * @param info the liveness information for the partition key columns of the built row.
          */
         public void addPrimaryKeyLivenessInfo(LivenessInfo info);
 
@@ -502,6 +523,105 @@
     }
 
     /**
+     * Row builder interface geared towards human.
+     * <p>
+     * Where the {@link Builder} deals with building rows efficiently from internal objects ({@code Cell}, {@code
+     * LivenessInfo}, ...), the {@code SimpleBuilder} is geared towards building rows from string column name and
+     * 'native' values (string for text, ints for numbers, et...). In particular, it is meant to be convenient, not
+     * efficient, and should be used only in place where performance is not of the utmost importance (it is used to
+     * build schema mutation for instance).
+     * <p>
+     * Also note that contrarily to {@link Builder}, the {@code SimpleBuilder} API has no {@code newRow()} method: it is
+     * expected that the clustering of the row built is provided by the constructor of the builder.
+     */
+    public interface SimpleBuilder
+    {
+        /**
+         * Sets the timestamp to use for the following additions.
+         * <p>
+         * Note that the for non-compact tables, this method must be called before any column addition for this
+         * timestamp to be used for the row {@code LivenessInfo}.
+         *
+         * @param timestamp the timestamp to use for following additions. If that timestamp hasn't been set, the current
+         * time in microseconds will be used.
+         * @return this builder.
+         */
+        public SimpleBuilder timestamp(long timestamp);
+
+        /**
+         * Sets the ttl to use for the following additions.
+         * <p>
+         * Note that the for non-compact tables, this method must be called before any column addition for this
+         * ttl to be used for the row {@code LivenessInfo}.
+         *
+         * @param ttl the ttl to use for following additions. If that ttl hasn't been set, no ttl will be used.
+         * @return this builder.
+         */
+        public SimpleBuilder ttl(int ttl);
+
+        /**
+         * Adds a value to a given column.
+         *
+         * @param columnName the name of the column for which to add a new value.
+         * @param value the value to add, which must be of the proper type for {@code columnName}. This can be {@code
+         * null} in which case the this is equivalent to {@code delete(columnName)}.
+         * @return this builder.
+         */
+        public SimpleBuilder add(String columnName, Object value);
+
+        /**
+         * Appends new values to a given non-frozen collection column.
+         * <p>
+         * This method is similar to {@code add()} but the collection elements added through this method are "appended"
+         * to any pre-exising elements. In other words, this is like {@code add()} except that it doesn't delete the
+         * previous value of the collection. This can only be called on non-frozen collection columns.
+         * <p>
+         * Note that this method can be used in replacement of {@code add()} if you know that there can't be any
+         * pre-existing value for that column, in which case this is slightly less expensive as it avoid the collection
+         * tombstone inherent to {@code add()}.
+         *
+         * @param columnName the name of the column for which to add a new value, which must be a non-frozen collection.
+         * @param value the value to add, which must be of the proper type for {@code columnName} (in other words, it
+         * <b>must</b> be a collection).
+         * @return this builder.
+         *
+         * @throws IllegalArgumentException if columnName is not a non-frozen collection column.
+         */
+        public SimpleBuilder appendAll(String columnName, Object value);
+
+        /**
+         * Deletes the whole row.
+         * <p>
+         * If called, this is generally the only method called on the builder (outside of {@code timestamp()}.
+         *
+         * @return this builder.
+         */
+        public SimpleBuilder delete();
+
+        /**
+         * Removes the value for a given column (creating a tombstone).
+         *
+         * @param columnName the name of the column to delete.
+         * @return this builder.
+         */
+        public SimpleBuilder delete(String columnName);
+
+        /**
+         * Don't include any primary key {@code LivenessInfo} in the built row.
+         *
+         * @return this builder.
+         */
+        public SimpleBuilder noPrimaryKeyLivenessInfo();
+
+        /**
+         * Returns the built row.
+         *
+         * @return the built row.
+         */
+        public Row build();
+    }
+
+    /**
      * Utility class to help merging rows from multiple inputs (UnfilteredRowIterators).
      */
     public static class Merger
@@ -648,7 +768,7 @@
                 if (column == null)
                     return true;
 
-                return AbstractTypeVersionComparator.INSTANCE.compare(column.type, dataColumn.type) < 0;
+                return ColumnDefinitionVersionComparator.INSTANCE.compare(column, dataColumn) < 0;
             }
 
             protected ColumnData getReduced()
diff --git a/src/java/org/apache/cassandra/db/rows/RowAndDeletionMergeIterator.java b/src/java/org/apache/cassandra/db/rows/RowAndDeletionMergeIterator.java
index d47bd8c..5af6c4b 100644
--- a/src/java/org/apache/cassandra/db/rows/RowAndDeletionMergeIterator.java
+++ b/src/java/org/apache/cassandra/db/rows/RowAndDeletionMergeIterator.java
@@ -192,12 +192,12 @@
         return range;
     }
 
-    private Slice.Bound openBound(RangeTombstone range)
+    private ClusteringBound openBound(RangeTombstone range)
     {
         return range.deletedSlice().open(isReverseOrder());
     }
 
-    private Slice.Bound closeBound(RangeTombstone range)
+    private ClusteringBound closeBound(RangeTombstone range)
     {
         return range.deletedSlice().close(isReverseOrder());
     }
diff --git a/src/java/org/apache/cassandra/db/rows/RowDiffListener.java b/src/java/org/apache/cassandra/db/rows/RowDiffListener.java
index ec848a0..0c7e32b 100644
--- a/src/java/org/apache/cassandra/db/rows/RowDiffListener.java
+++ b/src/java/org/apache/cassandra/db/rows/RowDiffListener.java
@@ -23,7 +23,7 @@
 /**
  * Interface that allows to act on the result of merging multiple rows.
  *
- * More precisely, given N rows and the result of merging them, one can call {@link Rows#diff()}
+ * More precisely, given N rows and the result of merging them, one can call {@link Rows#diff(RowDiffListener, Row, Row...)}
  * with a {@code RowDiffListener} and that listener will be informed for each input row of the diff between
  * that input and merge row.
  */
diff --git a/src/java/org/apache/cassandra/db/rows/RowIterator.java b/src/java/org/apache/cassandra/db/rows/RowIterator.java
index f0b4499..0cc4a3c 100644
--- a/src/java/org/apache/cassandra/db/rows/RowIterator.java
+++ b/src/java/org/apache/cassandra/db/rows/RowIterator.java
@@ -17,11 +17,6 @@
  */
 package org.apache.cassandra.db.rows;
 
-import java.util.Iterator;
-
-import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.db.*;
-
 /**
  * An iterator over rows belonging to a partition.
  *
diff --git a/src/java/org/apache/cassandra/db/rows/RowIterators.java b/src/java/org/apache/cassandra/db/rows/RowIterators.java
index ae051c0..1463bf5 100644
--- a/src/java/org/apache/cassandra/db/rows/RowIterators.java
+++ b/src/java/org/apache/cassandra/db/rows/RowIterators.java
@@ -17,12 +17,15 @@
  */
 package org.apache.cassandra.db.rows;
 
+import java.nio.ByteBuffer;
 import java.security.MessageDigest;
+import java.util.Set;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.transform.Transformation;
 import org.apache.cassandra.utils.FBUtilities;
 
@@ -35,7 +38,7 @@
 
     private RowIterators() {}
 
-    public static void digest(RowIterator iterator, MessageDigest digest)
+    public static void digest(RowIterator iterator, MessageDigest digest, MessageDigest altDigest, Set<ByteBuffer> columnsToExclude)
     {
         // TODO: we're not computing digest the same way that old nodes. This is
         // currently ok as this is only used for schema digest and the is no exchange
@@ -47,8 +50,39 @@
         FBUtilities.updateWithBoolean(digest, iterator.isReverseOrder());
         iterator.staticRow().digest(digest);
 
+        if (altDigest != null)
+        {
+            // Compute the "alternative digest" here.
+            altDigest.update(iterator.partitionKey().getKey().duplicate());
+            iterator.columns().regulars.digest(altDigest, columnsToExclude);
+            iterator.columns().statics.digest(altDigest, columnsToExclude);
+            FBUtilities.updateWithBoolean(altDigest, iterator.isReverseOrder());
+            iterator.staticRow().digest(altDigest, columnsToExclude);
+        }
+
         while (iterator.hasNext())
-            iterator.next().digest(digest);
+        {
+            Row row = iterator.next();
+            row.digest(digest);
+            if (altDigest != null)
+                row.digest(altDigest, columnsToExclude);
+        }
+    }
+
+    /**
+     * Filter the provided iterator to only include cells that are selected by the user.
+     *
+     * @param iterator the iterator to filter.
+     * @param filter the {@code ColumnFilter} to use when deciding which cells are queried by the user. This should be the filter
+     * that was used when querying {@code iterator}.
+     * @return the filtered iterator..
+     */
+    public static RowIterator withOnlyQueriedData(RowIterator iterator, ColumnFilter filter)
+    {
+        if (filter.allFetchedColumnsAreQueried())
+            return iterator;
+
+        return Transformation.apply(iterator, new WithOnlyQueriedData(filter));
     }
 
     /**
diff --git a/src/java/org/apache/cassandra/db/rows/Rows.java b/src/java/org/apache/cassandra/db/rows/Rows.java
index 09213a4..2701682 100644
--- a/src/java/org/apache/cassandra/db/rows/Rows.java
+++ b/src/java/org/apache/cassandra/db/rows/Rows.java
@@ -22,10 +22,12 @@
 import com.google.common.collect.Iterators;
 import com.google.common.collect.PeekingIterator;
 
+import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.partitions.PartitionStatisticsCollector;
 import org.apache.cassandra.utils.MergeIterator;
+import org.apache.cassandra.utils.WrappedInt;
 
 /**
  * Static utilities to work on Row objects.
@@ -59,6 +61,21 @@
     }
 
     /**
+     * Creates a new simple row builder.
+     *
+     * @param metadata the metadata of the table this is a row of.
+     * @param clusteringValues the value for the clustering columns of the row to add to this build. There may be no
+     * values if either the table has no clustering column, or if you want to edit the static row. Note that as a
+     * shortcut it is also allowed to pass a {@code Clustering} object directly, in which case that should be the
+     * only argument.
+     * @return a newly created builder.
+     */
+    public static Row.SimpleBuilder simpleBuilder(CFMetaData metadata, Object... clusteringValues)
+    {
+        return new SimpleBuilders.RowBuilder(metadata, clusteringValues);
+    }
+
+    /**
      * Collect statistics on a given row.
      *
      * @param row the row for which to collect stats.
@@ -72,14 +89,15 @@
         collector.update(row.primaryKeyLivenessInfo());
         collector.update(row.deletion().time());
 
-        int columnCount = 0;
-        int cellCount = 0;
-        for (ColumnData cd : row)
-        {
+        //we have to wrap these for the lambda
+        final WrappedInt columnCount = new WrappedInt(0);
+        final WrappedInt cellCount = new WrappedInt(0);
+
+        row.apply(cd -> {
             if (cd.column().isSimple())
             {
-                ++columnCount;
-                ++cellCount;
+                columnCount.increment();
+                cellCount.increment();
                 Cells.collectStats((Cell) cd, collector);
             }
             else
@@ -88,18 +106,18 @@
                 collector.update(complexData.complexDeletion());
                 if (complexData.hasCells())
                 {
-                    ++columnCount;
+                    columnCount.increment();
                     for (Cell cell : complexData)
                     {
-                        ++cellCount;
+                        cellCount.increment();
                         Cells.collectStats(cell, collector);
                     }
                 }
             }
+        }, false);
 
-        }
-        collector.updateColumnSetPerRow(columnCount);
-        return cellCount;
+        collector.updateColumnSetPerRow(columnCount.get());
+        return cellCount.get();
     }
 
     /**
@@ -312,6 +330,82 @@
     }
 
     /**
+     * Returns a row that is obtained from the given existing row by removing everything that is shadowed by data in
+     * the update row. In other words, produces the smallest result row such that
+     * {@code merge(result, update, nowInSec) == merge(existing, update, nowInSec)} after filtering by rangeDeletion.
+     *
+     * @param existing source row
+     * @param update shadowing row
+     * @param rangeDeletion extra {@code DeletionTime} from covering tombstone
+     * @param nowInSec the current time in seconds (which plays a role during reconciliation
+     * because deleted cells always have precedence on timestamp equality and deciding if a
+     * cell is a live or not depends on the current time due to expiring cells).
+     */
+    public static Row removeShadowedCells(Row existing, Row update, DeletionTime rangeDeletion, int nowInSec)
+    {
+        Row.Builder builder = BTreeRow.sortedBuilder();
+        Clustering clustering = existing.clustering();
+        builder.newRow(clustering);
+
+        DeletionTime deletion = update.deletion().time();
+        if (rangeDeletion.supersedes(deletion))
+            deletion = rangeDeletion;
+
+        LivenessInfo existingInfo = existing.primaryKeyLivenessInfo();
+        if (!deletion.deletes(existingInfo))
+            builder.addPrimaryKeyLivenessInfo(existingInfo);
+        Row.Deletion rowDeletion = existing.deletion();
+        if (!deletion.supersedes(rowDeletion.time()))
+            builder.addRowDeletion(rowDeletion);
+
+        Iterator<ColumnData> a = existing.iterator();
+        Iterator<ColumnData> b = update.iterator();
+        ColumnData nexta = a.hasNext() ? a.next() : null, nextb = b.hasNext() ? b.next() : null;
+        while (nexta != null)
+        {
+            int comparison = nextb == null ? -1 : nexta.column.compareTo(nextb.column);
+            if (comparison <= 0)
+            {
+                ColumnData cura = nexta;
+                ColumnDefinition column = cura.column;
+                ColumnData curb = comparison == 0 ? nextb : null;
+                if (column.isSimple())
+                {
+                    Cells.addNonShadowed((Cell) cura, (Cell) curb, deletion, builder, nowInSec);
+                }
+                else
+                {
+                    ComplexColumnData existingData = (ComplexColumnData) cura;
+                    ComplexColumnData updateData = (ComplexColumnData) curb;
+
+                    DeletionTime existingDt = existingData.complexDeletion();
+                    DeletionTime updateDt = updateData == null ? DeletionTime.LIVE : updateData.complexDeletion();
+
+                    DeletionTime maxDt = updateDt.supersedes(deletion) ? updateDt : deletion;
+                    if (existingDt.supersedes(maxDt))
+                    {
+                        builder.addComplexDeletion(column, existingDt);
+                        maxDt = existingDt;
+                    }
+
+                    Iterator<Cell> existingCells = existingData.iterator();
+                    Iterator<Cell> updateCells = updateData == null ? null : updateData.iterator();
+                    Cells.addNonShadowedComplex(column, existingCells, updateCells, maxDt, builder, nowInSec);
+                }
+                nexta = a.hasNext() ? a.next() : null;
+                if (curb != null)
+                    nextb = b.hasNext() ? b.next() : null;
+            }
+            else
+            {
+                nextb = b.hasNext() ? b.next() : null;
+            }
+        }
+        Row row = builder.build();
+        return row != null && !row.isEmpty() ? row : null;
+    }
+
+    /**
      * Returns the {@code ColumnDefinition} to use for merging the columns.
      * If the 2 column definitions are different the latest one will be returned.
      */
@@ -323,7 +417,7 @@
         if (curb == null)
             return cura.column;
 
-        if (AbstractTypeVersionComparator.INSTANCE.compare(cura.column.type, curb.column.type) >= 0)
+        if (ColumnDefinitionVersionComparator.INSTANCE.compare(cura.column, curb.column) >= 0)
             return cura.column;
 
         return curb.column;
diff --git a/src/java/org/apache/cassandra/db/rows/SerializationHelper.java b/src/java/org/apache/cassandra/db/rows/SerializationHelper.java
index 6b4bc2e..e40a1e1 100644
--- a/src/java/org/apache/cassandra/db/rows/SerializationHelper.java
+++ b/src/java/org/apache/cassandra/db/rows/SerializationHelper.java
@@ -67,34 +67,49 @@
         this(metadata, version, flag, null);
     }
 
-    public Columns fetchedStaticColumns(SerializationHeader header)
-    {
-        return columnsToFetch == null ? header.columns().statics : columnsToFetch.fetchedColumns().statics;
-    }
-
-    public Columns fetchedRegularColumns(SerializationHeader header)
-    {
-        return columnsToFetch == null ? header.columns().regulars : columnsToFetch.fetchedColumns().regulars;
-    }
-
     public boolean includes(ColumnDefinition column)
     {
-        return columnsToFetch == null || columnsToFetch.includes(column);
+        return columnsToFetch == null || columnsToFetch.fetches(column);
+    }
+
+    public boolean includes(Cell cell, LivenessInfo rowLiveness)
+    {
+        if (columnsToFetch == null)
+            return true;
+
+        // During queries, some columns are included even though they are not queried by the user because
+        // we always need to distinguish between having a row (with potentially only null values) and not
+        // having a row at all (see #CASSANDRA-7085 for background). In the case where the column is not
+        // actually requested by the user however (canSkipValue), we can skip the full cell if the cell
+        // timestamp is lower than the row one, because in that case, the row timestamp is enough proof
+        // of the liveness of the row. Otherwise, we'll only be able to skip the values of those cells.
+        ColumnDefinition column = cell.column();
+        if (column.isComplex())
+        {
+            if (!includes(cell.path()))
+                return false;
+
+            return !canSkipValue(cell.path()) || cell.timestamp() >= rowLiveness.timestamp();
+        }
+        else
+        {
+            return columnsToFetch.fetchedColumnIsQueried(column) || cell.timestamp() >= rowLiveness.timestamp();
+        }
     }
 
     public boolean includes(CellPath path)
     {
-        return path == null || tester == null || tester.includes(path);
+        return path == null || tester == null || tester.fetches(path);
     }
 
     public boolean canSkipValue(ColumnDefinition column)
     {
-        return columnsToFetch != null && columnsToFetch.canSkipValue(column);
+        return columnsToFetch != null && !columnsToFetch.fetchedColumnIsQueried(column);
     }
 
     public boolean canSkipValue(CellPath path)
     {
-        return path != null && tester != null && tester.canSkipValue(path);
+        return path != null && tester != null && !tester.fetchedCellIsQueried(path);
     }
 
     public void startOfComplexColumn(ColumnDefinition column)
diff --git a/src/java/org/apache/cassandra/db/rows/SliceableUnfilteredRowIterator.java b/src/java/org/apache/cassandra/db/rows/SliceableUnfilteredRowIterator.java
deleted file mode 100644
index 2250ee9..0000000
--- a/src/java/org/apache/cassandra/db/rows/SliceableUnfilteredRowIterator.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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.db.rows;
-
-import java.util.Iterator;
-
-import org.apache.cassandra.db.Slice;
-
-public interface SliceableUnfilteredRowIterator extends UnfilteredRowIterator
-{
-    /**
-     * Move forward (resp. backward if isReverseOrder() is true for the iterator) in
-     * the iterator and return an iterator over the Unfiltered selected by the provided
-     * {@code slice}.
-     * <p>
-     * Please note that successive calls to {@code slice} are allowed provided the
-     * slice are non overlapping and are passed in clustering (resp. reverse clustering) order.
-     * However, {@code slice} is allowed to leave the iterator in an unknown state and there
-     * is no guarantee over what a call to {@code hasNext} or {@code next} will yield after
-     * a call to {@code slice}. In other words, for a given iterator, you should either use
-     * {@code slice} or {@code hasNext/next} but not both.
-     */
-    public Iterator<Unfiltered> slice(Slice slice);
-}
diff --git a/src/java/org/apache/cassandra/db/rows/Unfiltered.java b/src/java/org/apache/cassandra/db/rows/Unfiltered.java
index 9511eeb..3d8a9b1 100644
--- a/src/java/org/apache/cassandra/db/rows/Unfiltered.java
+++ b/src/java/org/apache/cassandra/db/rows/Unfiltered.java
@@ -17,7 +17,9 @@
  */
 package org.apache.cassandra.db.rows;
 
+import java.nio.ByteBuffer;
 import java.security.MessageDigest;
+import java.util.Set;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.Clusterable;
@@ -46,10 +48,20 @@
     public void digest(MessageDigest digest);
 
     /**
+     * Digest the atom using the provided {@code MessageDigest}.
+     * This method only exists in 3.11.
+     * Same like {@link #digest(MessageDigest)}, but excludes the given columns from digest calculation.
+     */
+    public default void digest(MessageDigest digest, Set<ByteBuffer> columnsToExclude)
+    {
+        throw new UnsupportedOperationException("no no no - don't use this one - use digest(MessageDigest) instead");
+    }
+
+    /**
      * Validate the data of this atom.
      *
      * @param metadata the metadata for the table this atom is part of.
-     * @throws MarshalException if some of the data in this atom is
+     * @throws org.apache.cassandra.serializers.MarshalException if some of the data in this atom is
      * invalid (some value is invalid for its column type, or some field
      * is nonsensical).
      */
diff --git a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterator.java b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterator.java
index a969858..bbef11e 100644
--- a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterator.java
+++ b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterator.java
@@ -17,9 +17,6 @@
  */
 package org.apache.cassandra.db.rows;
 
-import java.util.Iterator;
-
-import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
 
 /**
diff --git a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIteratorSerializer.java b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIteratorSerializer.java
index 932ca4c..45c026f 100644
--- a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIteratorSerializer.java
+++ b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIteratorSerializer.java
@@ -36,6 +36,7 @@
  * The serialization is composed of a header, follows by the rows and range tombstones of the iterator serialized
  * until we read the end of the partition (see UnfilteredSerializer for details). The header itself
  * is:
+ * {@code
  *     <cfid><key><flags><s_header>[<partition_deletion>][<static_row>][<row_estimate>]
  * where:
  *     <cfid> is the table cfid.
@@ -54,6 +55,7 @@
  *     <static_row> is the static row for this partition as serialized by UnfilteredSerializer.
  *     <row_estimate> is the (potentially estimated) number of rows serialized. This is only used for
  *         the purpose of sizing on the receiving end and should not be relied upon too strongly.
+ * }
  *
  * Please note that the format described above is the on-wire format. On-disk, the format is basically the
  * same, but the header is written once per sstable, not once per-partition. Further, the actual row and
@@ -78,12 +80,15 @@
     }
 
     // Should only be used for the on-wire format.
+
     public void serialize(UnfilteredRowIterator iterator, ColumnFilter selection, DataOutputPlus out, int version, int rowEstimate) throws IOException
     {
+
         SerializationHeader header = new SerializationHeader(false,
                                                              iterator.metadata(),
                                                              iterator.columns(),
                                                              iterator.stats());
+
         serialize(iterator, header, selection, out, version, rowEstimate);
     }
 
diff --git a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIteratorWithLowerBound.java b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIteratorWithLowerBound.java
new file mode 100644
index 0000000..e381436
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIteratorWithLowerBound.java
@@ -0,0 +1,266 @@
+/*
+ *
+ * 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.db.rows;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.filter.ClusteringIndexFilter;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.transform.RTBoundValidator;
+import org.apache.cassandra.io.sstable.IndexInfo;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.sstable.format.SSTableReadsListener;
+import org.apache.cassandra.io.sstable.metadata.StatsMetadata;
+import org.apache.cassandra.thrift.ThriftResultsMerger;
+import org.apache.cassandra.utils.IteratorWithLowerBound;
+
+/**
+ * An unfiltered row iterator with a lower bound retrieved from either the global
+ * sstable statistics or the row index lower bounds (if available in the cache).
+ * Before initializing the sstable unfiltered row iterator, we return an empty row
+ * with the clustering set to the lower bound. The empty row will be filtered out and
+ * the result is that if we don't need to access this sstable, i.e. due to the LIMIT conditon,
+ * then we will not. See CASSANDRA-8180 for examples of why this is useful.
+ */
+public class UnfilteredRowIteratorWithLowerBound extends LazilyInitializedUnfilteredRowIterator implements IteratorWithLowerBound<Unfiltered>
+{
+    private final SSTableReader sstable;
+    private final ClusteringIndexFilter filter;
+    private final ColumnFilter selectedColumns;
+    private final boolean isForThrift;
+    private final int nowInSec;
+    private final boolean applyThriftTransformation;
+    private final SSTableReadsListener listener;
+    private ClusteringBound lowerBound;
+    private boolean firstItemRetrieved;
+
+    public UnfilteredRowIteratorWithLowerBound(DecoratedKey partitionKey,
+                                               SSTableReader sstable,
+                                               ClusteringIndexFilter filter,
+                                               ColumnFilter selectedColumns,
+                                               boolean isForThrift,
+                                               int nowInSec,
+                                               boolean applyThriftTransformation,
+                                               SSTableReadsListener listener)
+    {
+        super(partitionKey);
+        this.sstable = sstable;
+        this.filter = filter;
+        this.selectedColumns = selectedColumns;
+        this.isForThrift = isForThrift;
+        this.nowInSec = nowInSec;
+        this.applyThriftTransformation = applyThriftTransformation;
+        this.listener = listener;
+        this.lowerBound = null;
+        this.firstItemRetrieved = false;
+    }
+
+    public Unfiltered lowerBound()
+    {
+        if (lowerBound != null)
+            return makeBound(lowerBound);
+
+        // The partition index lower bound is more accurate than the sstable metadata lower bound but it is only
+        // present if the iterator has already been initialized, which we only do when there are tombstones since in
+        // this case we cannot use the sstable metadata clustering values
+        ClusteringBound ret = getPartitionIndexLowerBound();
+        return ret != null ? makeBound(ret) : makeBound(getMetadataLowerBound());
+    }
+
+    private Unfiltered makeBound(ClusteringBound bound)
+    {
+        if (bound == null)
+            return null;
+
+        if (lowerBound != bound)
+            lowerBound = bound;
+
+        return new RangeTombstoneBoundMarker(lowerBound, DeletionTime.LIVE);
+    }
+
+    @Override
+    protected UnfilteredRowIterator initializeIterator()
+    {
+        @SuppressWarnings("resource") // 'iter' is added to iterators which is closed on exception, or through the closing of the final merged iterator
+        UnfilteredRowIterator iter = sstable.iterator(partitionKey(), filter.getSlices(metadata()), selectedColumns, filter.isReversed(), isForThrift, listener);
+
+        if (isForThrift && applyThriftTransformation)
+            iter = ThriftResultsMerger.maybeWrap(iter, nowInSec);
+
+        return RTBoundValidator.validate(iter, RTBoundValidator.Stage.SSTABLE, false);
+    }
+
+    @Override
+    protected Unfiltered computeNext()
+    {
+        Unfiltered ret = super.computeNext();
+        if (firstItemRetrieved)
+            return ret;
+
+        // Check that the lower bound is not bigger than the first item retrieved
+        firstItemRetrieved = true;
+        if (lowerBound != null && ret != null)
+            assert comparator().compare(lowerBound, ret.clustering()) <= 0
+                : String.format("Lower bound [%s ]is bigger than first returned value [%s] for sstable %s",
+                                lowerBound.toString(sstable.metadata),
+                                ret.toString(sstable.metadata),
+                                sstable.getFilename());
+
+        return ret;
+    }
+
+    private Comparator<Clusterable> comparator()
+    {
+        return filter.isReversed() ? sstable.metadata.comparator.reversed() : sstable.metadata.comparator;
+    }
+
+    @Override
+    public CFMetaData metadata()
+    {
+        return sstable.metadata;
+    }
+
+    @Override
+    public boolean isReverseOrder()
+    {
+        return filter.isReversed();
+    }
+
+    @Override
+    public PartitionColumns columns()
+    {
+        return selectedColumns.fetchedColumns();
+    }
+
+    @Override
+    public EncodingStats stats()
+    {
+        return sstable.stats();
+    }
+
+    @Override
+    public DeletionTime partitionLevelDeletion()
+    {
+        if (!sstable.mayHaveTombstones())
+            return DeletionTime.LIVE;
+
+        return super.partitionLevelDeletion();
+    }
+
+    @Override
+    public Row staticRow()
+    {
+        if (columns().statics.isEmpty())
+            return Rows.EMPTY_STATIC_ROW;
+
+        return super.staticRow();
+    }
+
+    /**
+     * @return the lower bound stored on the index entry for this partition, if available.
+     */
+    private ClusteringBound getPartitionIndexLowerBound()
+    {
+        // NOTE: CASSANDRA-11206 removed the lookup against the key-cache as the IndexInfo objects are no longer
+        // in memory for not heap backed IndexInfo objects (so, these are on disk).
+        // CASSANDRA-11369 is there to fix this afterwards.
+
+        // Creating the iterator ensures that rowIndexEntry is loaded if available (partitions bigger than
+        // DatabaseDescriptor.column_index_size_in_kb)
+        if (!canUseMetadataLowerBound())
+            maybeInit();
+
+        RowIndexEntry rowIndexEntry = sstable.getCachedPosition(partitionKey(), false);
+        if (rowIndexEntry == null || !rowIndexEntry.indexOnHeap())
+            return null;
+
+        try (RowIndexEntry.IndexInfoRetriever onHeapRetriever = rowIndexEntry.openWithIndex(null))
+        {
+            IndexInfo column = onHeapRetriever.columnsIndex(filter.isReversed() ? rowIndexEntry.columnsIndexCount() - 1 : 0);
+            ClusteringPrefix lowerBoundPrefix = filter.isReversed() ? column.lastName : column.firstName;
+            assert lowerBoundPrefix.getRawValues().length <= sstable.metadata.comparator.size() :
+            String.format("Unexpected number of clustering values %d, expected %d or fewer for %s",
+                          lowerBoundPrefix.getRawValues().length,
+                          sstable.metadata.comparator.size(),
+                          sstable.getFilename());
+            return ClusteringBound.inclusiveOpen(filter.isReversed(), lowerBoundPrefix.getRawValues());
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException("should never occur", e);
+        }
+    }
+
+    /**
+     * Whether we can use the clustering values in the stats of the sstable to build the lower bound.
+     * <p>
+     * Currently, the clustering values of the stats file records for each clustering component the min and max
+     * value seen, null excluded. In other words, having a non-null value for a component in those min/max clustering
+     * values does _not_ guarantee that there isn't an unfiltered in the sstable whose clustering has either no value for
+     * that component (it's a prefix) or a null value.
+     * <p>
+     * This is problematic as this means we can't in general build a lower bound from those values since the "min"
+     * values doesn't actually guarantee minimality.
+     * <p>
+     * However, we can use those values if we can guarantee that no clustering in the sstable 1) is a true prefix and
+     * 2) uses null values. Nat having true prefixes means having no range tombstone markers since rows use
+     * {@link Clustering} which is always "full" (all components are always present). As for null values, we happen to
+     * only allow those in compact tables (for backward compatibility), so we can simply exclude those tables.
+     * <p>
+     * Note that the information we currently have at our disposal make this condition less precise that it could be.
+     * In particular, {@link SSTableReader#mayHaveTombstones} could return {@code true} (making us not use the stats)
+     * because of cell tombstone or even expiring cells even if the sstable has no range tombstone markers, even though
+     * it's really only markers we want to exclude here (more precisely, as said above, we want to exclude anything
+     * whose clustering is not "full", but that's only markers). It wouldn't be very hard to collect whether a sstable
+     * has any range tombstone marker however so it's a possible improvement.
+     */
+    private boolean canUseMetadataLowerBound()
+    {
+        // Side-note: pre-2.1 sstable stat file had clustering value arrays whose size may not match the comparator size
+        // and that would break getMetadataLowerBound. We don't support upgrade from 2.0 to 3.0 directly however so it's
+        // not a true concern. Besides, !sstable.mayHaveTombstones already ensure this is a 3.0 sstable anyway.
+        return !sstable.mayHaveTombstones() && !sstable.metadata.isCompactTable();
+    }
+
+    /**
+     * @return a global lower bound made from the clustering values stored in the sstable metadata, note that
+     * this currently does not correctly compare tombstone bounds, especially ranges.
+     */
+    private ClusteringBound getMetadataLowerBound()
+    {
+        if (!canUseMetadataLowerBound())
+            return null;
+
+        final StatsMetadata m = sstable.getSSTableMetadata();
+        List<ByteBuffer> vals = filter.isReversed() ? m.maxClusteringValues : m.minClusteringValues;
+        assert vals.size() <= sstable.metadata.comparator.size() :
+        String.format("Unexpected number of clustering values %d, expected %d or fewer for %s",
+                      vals.size(),
+                      sstable.metadata.comparator.size(),
+                      sstable.getFilename());
+        return  ClusteringBound.inclusiveOpen(filter.isReversed(), vals.toArray(new ByteBuffer[vals.size()]));
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterators.java b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterators.java
index b6dbf82..1fda52a 100644
--- a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterators.java
+++ b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterators.java
@@ -25,6 +25,7 @@
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.transform.FilteredRows;
 import org.apache.cassandra.db.transform.MoreRows;
 import org.apache.cassandra.db.transform.Transformation;
@@ -35,7 +36,6 @@
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.IMergeIterator;
 import org.apache.cassandra.utils.MergeIterator;
-import org.apache.cassandra.utils.memory.AbstractAllocator;
 
 /**
  * Static methods to work with atom iterators.
@@ -214,6 +214,23 @@
     }
 
     /**
+     * Filter the provided iterator to exclude cells that have been fetched but are not queried by the user
+     * (see ColumnFilter for detailes).
+     *
+     * @param iterator the iterator to filter.
+     * @param filter the {@code ColumnFilter} to use when deciding which columns are the one queried by the
+     * user. This should be the filter that was used when querying {@code iterator}.
+     * @return the filtered iterator..
+     */
+    public static UnfilteredRowIterator withOnlyQueriedData(UnfilteredRowIterator iterator, ColumnFilter filter)
+    {
+        if (filter.allFetchedColumnsAreQueried())
+            return iterator;
+
+        return Transformation.apply(iterator, new WithOnlyQueriedData(filter));
+    }
+
+    /**
      * Returns an iterator that concatenate two atom iterators.
      * This method assumes that both iterator are from the same partition and that the atom from
      * {@code iter2} come after the ones of {@code iter1} (that is, that concatenating the iterator
@@ -270,32 +287,6 @@
         };
     }
 
-    public static UnfilteredRowIterator cloningIterator(UnfilteredRowIterator iterator, final AbstractAllocator allocator)
-    {
-        class Cloner extends Transformation
-        {
-            private final Row.Builder builder = allocator.cloningBTreeRowBuilder();
-
-            public Row applyToStatic(Row row)
-            {
-                return Rows.copy(row, builder).build();
-            }
-
-            @Override
-            public Row applyToRow(Row row)
-            {
-                return Rows.copy(row, builder).build();
-            }
-
-            @Override
-            public RangeTombstoneMarker applyToMarker(RangeTombstoneMarker marker)
-            {
-                return marker.copy(allocator);
-            }
-        }
-        return Transformation.apply(iterator, new Cloner());
-    }
-
     /**
      * Validate that the data of the provided iterator is valid, that is that the values
      * it contains are valid for the type they represent, and more generally that the
diff --git a/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java b/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java
index 0342e39..c81ac9d 100644
--- a/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java
+++ b/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java
@@ -19,54 +19,76 @@
 
 import java.io.IOException;
 
-import com.google.common.collect.Collections2;
 
+import net.nicoulaj.compilecommand.annotations.Inline;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.rows.Row.Deletion;
 import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.io.util.FileDataInput;
 import org.apache.cassandra.utils.SearchIterator;
+import org.apache.cassandra.utils.WrappedException;
 
 /**
  * Serialize/deserialize a single Unfiltered (both on-wire and on-disk).
+ * <p>
  *
- * The encoded format for an unfiltered is <flags>(<row>|<marker>) where:
- *
- *   <flags> is a byte (or two) whose bits are flags used by the rest of the serialization. Each
- *       flag is defined/explained below as the "Unfiltered flags" constants. One of those flags
- *       is an extension flag, and if present, trigger the rid of another byte that contains more
- *       flags. If the extension is not set, defaults are assumed for the flags of that 2nd byte.
- *   <row> is <clustering><size>[<timestamp>][<ttl>][<deletion>]<sc1>...<sci><cc1>...<ccj> where
- *       <clustering> is the row clustering as serialized by {@code Clustering.serializer} (note
- *       that static row are an exception and don't have this).
- *       <size> is the size of the whole unfiltered on disk (it's only used for sstables and is
- *       used to efficiently skip rows).
- *       <timestamp>, <ttl> and <deletion> are the row timestamp, ttl and deletion
- *       whose presence is determined by the flags. <sci> is the simple columns of the row and <ccj> the
- *       complex ones.
- *       The columns for the row are then serialized if they differ from those in the header,
- *       and each cell then follows:
- *         * Each simple column <sci> will simply be a <cell>
- *           (which might have no value, see below),
- *         * Each <ccj> will be [<delTime>]<n><cell1>...<celln> where <delTime>
- *           is the deletion for this complex column (if flags indicates it present), <n>
- *           is the vint encoded value of n, i.e. <celln>'s 1-based index, <celli>
- *           are the <cell> for this complex column
- *   <marker> is <bound><deletion> where <bound> is the marker bound as serialized
- *       by {@code Slice.Bound.serializer} and <deletion> is the marker deletion
- *       time.
- *
- *   <cell> A cell start with a 1 byte <flag>. The 2nd and third flag bits indicate if
- *       it's a deleted or expiring cell. The 4th flag indicates if the value
- *       is empty or not. The 5th and 6th indicates if the timestamp and ttl/
- *       localDeletionTime for the cell are the same than the row one (if that
- *       is the case, those are not repeated for the cell).Follows the <value>
- *       (unless it's marked empty in the flag) and a delta-encoded long <timestamp>
- *       (unless the flag tells to use the row level one).
- *       Then if it's a deleted or expiring cell a delta-encoded int <localDelTime>
- *       and if it's expiring a delta-encoded int <ttl> (unless it's an expiring cell
- *       and the ttl and localDeletionTime are indicated by the flags to be the same
- *       than the row ones, in which case none of those appears).
+ * The encoded format for an unfiltered is {@code <flags>(<row>|<marker>)} where:
+ * <ul>
+ *   <li>
+ *     {@code <flags>} is a byte (or two) whose bits are flags used by the rest
+ *     of the serialization. Each flag is defined/explained below as the
+ *     "Unfiltered flags" constants. One of those flags is an extension flag,
+ *     and if present, indicates the presence of a 2ndbyte that contains more
+ *     flags. If the extension is not set, defaults are assumed for the flags
+ *     of that 2nd byte.
+ *   </li>
+ *   <li>
+ *     {@code <row>} is
+ *        {@code <clustering><sizes>[<pkliveness>][<deletion>][<columns>]<columns_data>}
+ *     where:
+ *     <ul>
+ *       <li>{@code <clustering>} is the row clustering as serialized by
+ *           {@link org.apache.cassandra.db.Clustering.Serializer} (note that static row are an
+ *           exception and don't have this). </li>
+ *       <li>{@code <sizes>} are the sizes of the whole unfiltered on disk and
+ *           of the previous unfiltered. This is only present for sstables and
+ *           is used to efficiently skip rows (both forward and backward).</li>
+ *       <li>{@code <pkliveness>} is the row primary key liveness infos, and it
+ *           contains the timestamp, ttl and local deletion time of that info,
+ *           though some/all of those can be absent based on the flags. </li>
+ *       <li>{@code deletion} is the row deletion. It's presence is determined
+ *           by the flags and if present, it conists of both the deletion
+ *           timestamp and local deletion time.</li>
+ *       <li>{@code <columns>} are the columns present in the row  encoded by
+ *           {@link org.apache.cassandra.db.Columns.Serializer#serializeSubset}. It is absent if the row
+ *           contains all the columns of the {@code SerializationHeader} (which
+ *           is then indicated by a flag). </li>
+ *       <li>{@code <columns_data>} is the data for each of the column present
+ *           in the row. The encoding of each data depends on whether the data
+ *           is for a simple or complex column:
+ *           <ul>
+ *              <li>Simple columns are simply encoded as one {@code <cell>}</li>
+ *              <li>Complex columns are encoded as {@code [<delTime>]<n><cell1>...<celln>}
+ *                  where {@code <delTime>} is the deletion for this complex
+ *                  column (if flags indicates its presence), {@code <n>} is the
+ *                  vint encoded value of n, i.e. {@code <celln>}'s 1-based
+ *                  inde and {@code <celli>} are the {@code <cell>} for this
+ *                  complex column</li>
+ *           </ul>
+ *       </li>
+ *     </ul>
+ *   </li>
+ *   <li>
+ *     {@code <marker>} is {@code <bound><deletion>} where {@code <bound>} is
+ *     the marker bound as serialized by {@link org.apache.cassandra.db.ClusteringBoundOrBoundary.Serializer}
+ *     and {@code <deletion>} is the marker deletion time.
+ *   </li>
+ * </ul>
+ * <p>
+ * The serialization of a {@code <cell>} is defined by {@link Cell.Serializer}.
  */
 public class UnfilteredSerializer
 {
@@ -133,7 +155,7 @@
         LivenessInfo pkLiveness = row.primaryKeyLivenessInfo();
         Row.Deletion deletion = row.deletion();
         boolean hasComplexDeletion = row.hasComplexDeletion();
-        boolean hasAllColumns = (row.columnCount() == headerColumns.size());
+        boolean hasAllColumns = row.columnCount() == headerColumns.size();
         boolean hasExtendedFlags = hasExtendedFlags(row);
 
         if (isStatic)
@@ -166,9 +188,32 @@
 
         if (header.isForSSTable())
         {
-            out.writeUnsignedVInt(serializedRowBodySize(row, header, previousUnfilteredSize, version));
-            out.writeUnsignedVInt(previousUnfilteredSize);
+            try (DataOutputBuffer dob = DataOutputBuffer.scratchBuffer.get())
+            {
+                serializeRowBody(row, flags, header, dob);
+
+                out.writeUnsignedVInt(dob.position() + TypeSizes.sizeofUnsignedVInt(previousUnfilteredSize));
+                // We write the size of the previous unfiltered to make reverse queries more efficient (and simpler).
+                // This is currently not used however and using it is tbd.
+                out.writeUnsignedVInt(previousUnfilteredSize);
+                out.write(dob.getData(), 0, dob.getLength());
+            }
         }
+        else
+        {
+            serializeRowBody(row, flags, header, out);
+        }
+    }
+
+    @Inline
+    private void serializeRowBody(Row row, int flags, SerializationHeader header, DataOutputPlus out)
+    throws IOException
+    {
+        boolean isStatic = row.isStatic();
+
+        Columns headerColumns = header.columns(isStatic);
+        LivenessInfo pkLiveness = row.primaryKeyLivenessInfo();
+        Row.Deletion deletion = row.deletion();
 
         if ((flags & HAS_TIMESTAMP) != 0)
             header.writeTimestamp(pkLiveness.timestamp(), out);
@@ -180,24 +225,41 @@
         if ((flags & HAS_DELETION) != 0)
             header.writeDeletionTime(deletion.time(), out);
 
-        if (!hasAllColumns)
+        if ((flags & HAS_ALL_COLUMNS) == 0)
             Columns.serializer.serializeSubset(row.columns(), headerColumns, out);
 
         SearchIterator<ColumnDefinition, ColumnDefinition> si = headerColumns.iterator();
-        for (ColumnData data : row)
-        {
-            // We can obtain the column for data directly from data.column(). However, if the cell/complex data
-            // originates from a sstable, the column we'll get will have the type used when the sstable was serialized,
-            // and if that type have been recently altered, that may not be the type we want to serialize the column
-            // with. So we use the ColumnDefinition from the "header" which is "current". Also see #11810 for what
-            // happens if we don't do that.
-            ColumnDefinition column = si.next(data.column());
-            assert column != null;
 
-            if (data.column.isSimple())
-                Cell.serializer.serialize((Cell) data, column, out, pkLiveness, header);
-            else
-                writeComplexColumn((ComplexColumnData) data, column, hasComplexDeletion, pkLiveness, header, out);
+        try
+        {
+            row.apply(cd -> {
+                // We can obtain the column for data directly from data.column(). However, if the cell/complex data
+                // originates from a sstable, the column we'll get will have the type used when the sstable was serialized,
+                // and if that type have been recently altered, that may not be the type we want to serialize the column
+                // with. So we use the ColumnDefinition from the "header" which is "current". Also see #11810 for what
+                // happens if we don't do that.
+                ColumnDefinition column = si.next(cd.column());
+                assert column != null;
+
+                try
+                {
+                    if (cd.column.isSimple())
+                        Cell.serializer.serialize((Cell) cd, column, out, pkLiveness, header);
+                    else
+                        writeComplexColumn((ComplexColumnData) cd, column, (flags & HAS_COMPLEX_DELETION) != 0, pkLiveness, header, out);
+                }
+                catch (IOException e)
+                {
+                    throw new WrappedException(e);
+                }
+            }, false);
+        }
+        catch (WrappedException e)
+        {
+            if (e.getCause() instanceof IOException)
+                throw (IOException) e.getCause();
+
+            throw e;
         }
     }
 
@@ -216,7 +278,7 @@
     throws IOException
     {
         out.writeByte((byte)IS_MARKER);
-        RangeTombstone.Bound.serializer.serialize(marker.clustering(), out, version, header.clusteringTypes());
+        ClusteringBoundOrBoundary.serializer.serialize(marker.clustering(), out, version, header.clusteringTypes());
 
         if (header.isForSSTable())
         {
@@ -274,7 +336,7 @@
         LivenessInfo pkLiveness = row.primaryKeyLivenessInfo();
         Row.Deletion deletion = row.deletion();
         boolean hasComplexDeletion = row.hasComplexDeletion();
-        boolean hasAllColumns = (row.columnCount() == headerColumns.size());
+        boolean hasAllColumns = row.columnCount() == headerColumns.size();
 
         if (!pkLiveness.isEmpty())
             size += header.timestampSerializedSize(pkLiveness.timestamp());
@@ -293,7 +355,8 @@
         for (ColumnData data : row)
         {
             ColumnDefinition column = si.next(data.column());
-            assert column != null;
+            if (column == null)
+                continue;
 
             if (data.column.isSimple())
                 size += Cell.serializer.serializedSize((Cell) data, column, pkLiveness, header);
@@ -322,7 +385,7 @@
     {
         assert !header.isForSSTable();
         return 1 // flags
-             + RangeTombstone.Bound.serializer.serializedSize(marker.clustering(), version, header.clusteringTypes())
+             + ClusteringBoundOrBoundary.serializer.serializedSize(marker.clustering(), version, header.clusteringTypes())
              + serializedMarkerBodySize(marker, header, previousUnfilteredSize, version);
     }
 
@@ -404,7 +467,7 @@
 
         if (kind(flags) == Unfiltered.Kind.RANGE_TOMBSTONE_MARKER)
         {
-            RangeTombstone.Bound bound = RangeTombstone.Bound.serializer.deserialize(in, helper.version, header.clusteringTypes());
+            ClusteringBoundOrBoundary bound = ClusteringBoundOrBoundary.serializer.deserialize(in, helper.version, header.clusteringTypes());
             return deserializeMarkerBody(in, header, bound);
         }
         else
@@ -418,6 +481,58 @@
         }
     }
 
+    public Unfiltered deserializeTombstonesOnly(FileDataInput in, SerializationHeader header, SerializationHelper helper)
+    throws IOException
+    {
+        while (true)
+        {
+            int flags = in.readUnsignedByte();
+            if (isEndOfPartition(flags))
+                return null;
+
+            int extendedFlags = readExtendedFlags(in, flags);
+
+            if (kind(flags) == Unfiltered.Kind.RANGE_TOMBSTONE_MARKER)
+            {
+                ClusteringBoundOrBoundary bound = ClusteringBoundOrBoundary.serializer.deserialize(in, helper.version, header.clusteringTypes());
+                return deserializeMarkerBody(in, header, bound);
+            }
+            else
+            {
+                assert !isStatic(extendedFlags); // deserializeStaticRow should be used for that.
+                if ((flags & HAS_DELETION) != 0)
+                {
+                    assert header.isForSSTable();
+                    boolean hasTimestamp = (flags & HAS_TIMESTAMP) != 0;
+                    boolean hasTTL = (flags & HAS_TTL) != 0;
+                    boolean deletionIsShadowable = (extendedFlags & HAS_SHADOWABLE_DELETION) != 0;
+                    Clustering clustering = Clustering.serializer.deserialize(in, helper.version, header.clusteringTypes());
+                    long nextPosition = in.readUnsignedVInt() + in.getFilePointer();
+                    in.readUnsignedVInt(); // skip previous unfiltered size
+                    if (hasTimestamp)
+                    {
+                        header.readTimestamp(in);
+                        if (hasTTL)
+                        {
+                            header.readTTL(in);
+                            header.readLocalDeletionTime(in);
+                        }
+                    }
+
+                    Deletion deletion = new Row.Deletion(header.readDeletionTime(in), deletionIsShadowable);
+                    in.seek(nextPosition);
+                    return BTreeRow.emptyDeletedRow(clustering, deletion);
+                }
+                else
+                {
+                    Clustering.serializer.skip(in, helper.version, header.clusteringTypes());
+                    skipRowBody(in);
+                    // Continue with next item.
+                }
+            }
+        }
+    }
+
     public Row deserializeStaticRow(DataInputPlus in, SerializationHeader header, SerializationHelper helper)
     throws IOException
     {
@@ -429,7 +544,7 @@
         return deserializeRowBody(in, header, helper, flags, extendedFlags, builder);
     }
 
-    public RangeTombstoneMarker deserializeMarkerBody(DataInputPlus in, SerializationHeader header, RangeTombstone.Bound bound)
+    public RangeTombstoneMarker deserializeMarkerBody(DataInputPlus in, SerializationHeader header, ClusteringBoundOrBoundary bound)
     throws IOException
     {
         if (header.isForSSTable())
@@ -439,9 +554,9 @@
         }
 
         if (bound.isBoundary())
-            return new RangeTombstoneBoundaryMarker(bound, header.readDeletionTime(in), header.readDeletionTime(in));
+            return new RangeTombstoneBoundaryMarker((ClusteringBoundary) bound, header.readDeletionTime(in), header.readDeletionTime(in));
         else
-            return new RangeTombstoneBoundMarker(bound, header.readDeletionTime(in));
+            return new RangeTombstoneBoundMarker((ClusteringBound) bound, header.readDeletionTime(in));
     }
 
     public Row deserializeRowBody(DataInputPlus in,
@@ -475,19 +590,38 @@
                 long timestamp = header.readTimestamp(in);
                 int ttl = hasTTL ? header.readTTL(in) : LivenessInfo.NO_TTL;
                 int localDeletionTime = hasTTL ? header.readLocalDeletionTime(in) : LivenessInfo.NO_EXPIRATION_TIME;
-                rowLiveness = LivenessInfo.create(timestamp, ttl, localDeletionTime);
+                rowLiveness = LivenessInfo.withExpirationTime(timestamp, ttl, localDeletionTime);
             }
 
             builder.addPrimaryKeyLivenessInfo(rowLiveness);
             builder.addRowDeletion(hasDeletion ? new Row.Deletion(header.readDeletionTime(in), deletionIsShadowable) : Row.Deletion.LIVE);
 
             Columns columns = hasAllColumns ? headerColumns : Columns.serializer.deserializeSubset(headerColumns, in);
-            for (ColumnDefinition column : columns)
+
+            final LivenessInfo livenessInfo = rowLiveness;
+
+            try
             {
-                if (column.isSimple())
-                    readSimpleColumn(column, in, header, helper, builder, rowLiveness);
-                else
-                    readComplexColumn(column, in, header, helper, hasComplexDeletion, builder, rowLiveness);
+                columns.apply(column -> {
+                    try
+                    {
+                        if (column.isSimple())
+                            readSimpleColumn(column, in, header, helper, builder, livenessInfo);
+                        else
+                            readComplexColumn(column, in, header, helper, hasComplexDeletion, builder, livenessInfo);
+                    }
+                    catch (IOException e)
+                    {
+                        throw new WrappedException(e);
+                    }
+                }, false);
+            }
+            catch (WrappedException e)
+            {
+                if (e.getCause() instanceof IOException)
+                    throw (IOException) e.getCause();
+
+                throw e;
             }
 
             return builder.build();
@@ -508,7 +642,7 @@
         if (helper.includes(column))
         {
             Cell cell = Cell.serializer.deserialize(in, rowLiveness, column, header, helper);
-            if (!helper.isDropped(cell, false))
+            if (helper.includes(cell, rowLiveness) && !helper.isDropped(cell, false))
                 builder.addCell(cell);
         }
         else
@@ -534,7 +668,7 @@
             while (--count >= 0)
             {
                 Cell cell = Cell.serializer.deserialize(in, rowLiveness, column, header, helper);
-                if (helper.includes(cell.path()) && !helper.isDropped(cell, true))
+                if (helper.includes(cell, rowLiveness) && !helper.isDropped(cell, true))
                     builder.addCell(cell);
             }
 
diff --git a/src/java/org/apache/cassandra/db/rows/WithOnlyQueriedData.java b/src/java/org/apache/cassandra/db/rows/WithOnlyQueriedData.java
new file mode 100644
index 0000000..dcf0891
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/rows/WithOnlyQueriedData.java
@@ -0,0 +1,55 @@
+/*
+ * 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.db.rows;
+
+import org.apache.cassandra.db.PartitionColumns;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.transform.Transformation;
+
+/**
+ * Function to skip cells (from an iterator) that are not part of those queried by the user
+ * according to the provided {@code ColumnFilter}. See {@link UnfilteredRowIterators#withOnlyQueriedData}
+ * for more details.
+ */
+public class WithOnlyQueriedData<I extends BaseRowIterator<?>> extends Transformation<I>
+{
+    private final ColumnFilter filter;
+
+    public WithOnlyQueriedData(ColumnFilter filter)
+    {
+        this.filter = filter;
+    }
+
+    @Override
+    protected PartitionColumns applyToPartitionColumns(PartitionColumns columns)
+    {
+        return filter.queriedColumns();
+    }
+
+    @Override
+    protected Row applyToStatic(Row row)
+    {
+        return row.withOnlyQueriedData(filter);
+    }
+
+    @Override
+    protected Row applyToRow(Row row)
+    {
+        return row.withOnlyQueriedData(filter);
+    }
+};
diff --git a/src/java/org/apache/cassandra/db/rows/WrappingUnfilteredRowIterator.java b/src/java/org/apache/cassandra/db/rows/WrappingUnfilteredRowIterator.java
index 8b18554..411950e 100644
--- a/src/java/org/apache/cassandra/db/rows/WrappingUnfilteredRowIterator.java
+++ b/src/java/org/apache/cassandra/db/rows/WrappingUnfilteredRowIterator.java
@@ -29,7 +29,7 @@
  * some of the methods.
  * <p>
  * Note that if most of what you want to do is modifying/filtering the returned
- * {@code Unfiltered}, {@link org.apache.cassandra.db.transform.Transformation.apply} can be a simpler option.
+ * {@code Unfiltered}, {@link org.apache.cassandra.db.transform.Transformation#apply(UnfilteredRowIterator,Transformation)} can be a simpler option.
  */
 public abstract class WrappingUnfilteredRowIterator extends UnmodifiableIterator<Unfiltered>  implements UnfilteredRowIterator
 {
diff --git a/src/java/org/apache/cassandra/db/transform/BaseRows.java b/src/java/org/apache/cassandra/db/transform/BaseRows.java
index e6ce1da..42d272c 100644
--- a/src/java/org/apache/cassandra/db/transform/BaseRows.java
+++ b/src/java/org/apache/cassandra/db/transform/BaseRows.java
@@ -17,7 +17,7 @@
  * specific language governing permissions and limitations
  * under the License.
  *
-*/
+ */
 package org.apache.cassandra.db.transform;
 
 import org.apache.cassandra.config.CFMetaData;
@@ -33,11 +33,13 @@
 {
 
     private Row staticRow;
+    private DecoratedKey partitionKey;
 
     public BaseRows(I input)
     {
         super(input);
         staticRow = input.staticRow();
+        partitionKey = input.partitionKey();
     }
 
     // swap parameter order to avoid casting errors
@@ -45,6 +47,7 @@
     {
         super(copyFrom);
         staticRow = copyFrom.staticRow;
+        partitionKey = copyFrom.partitionKey();
     }
 
     public CFMetaData metadata()
@@ -105,6 +108,7 @@
         if (staticRow != null)
             staticRow = transformation.applyToStatic(staticRow);
         next = applyOne(next, transformation);
+        partitionKey = transformation.applyToPartitionKey(partitionKey);
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/db/transform/DuplicateRowChecker.java b/src/java/org/apache/cassandra/db/transform/DuplicateRowChecker.java
index 621821b..57aa0ae 100644
--- a/src/java/org/apache/cassandra/db/transform/DuplicateRowChecker.java
+++ b/src/java/org/apache/cassandra/db/transform/DuplicateRowChecker.java
@@ -98,6 +98,7 @@
                         metadata.getKeyValidator().getString(key.getKey()),
                         stage,
                         hadNonEqualDuplicates ? " Some duplicates had different byte representation." : "");
+
             if (snapshotOnDuplicate)
                 DiagnosticSnapshotService.duplicateRows(metadata, replicas);
         }
diff --git a/src/java/org/apache/cassandra/db/transform/RTBoundCloser.java b/src/java/org/apache/cassandra/db/transform/RTBoundCloser.java
index ee5401d..192b2fe 100644
--- a/src/java/org/apache/cassandra/db/transform/RTBoundCloser.java
+++ b/src/java/org/apache/cassandra/db/transform/RTBoundCloser.java
@@ -20,14 +20,14 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.Clustering;
 import org.apache.cassandra.db.DeletionTime;
-import org.apache.cassandra.db.ReadOrderGroup;
+import org.apache.cassandra.db.ReadExecutionController;
 import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
 import org.apache.cassandra.db.rows.*;
 
 /**
  * A transformation that appends an RT bound marker to row iterators in case they don't have one.
  *
- * This used to happen, for example, in {@link org.apache.cassandra.db.ReadCommand#executeLocally(ReadOrderGroup)},
+ * This used to happen, for example, in {@link org.apache.cassandra.db.ReadCommand#executeLocally(ReadExecutionController)}
  * if {@link org.apache.cassandra.db.filter.DataLimits} stopped the iterator on a live row that was enclosed in an
  * older RT.
  *
diff --git a/src/java/org/apache/cassandra/db/transform/Stack.java b/src/java/org/apache/cassandra/db/transform/Stack.java
index f680ec9..b15dd55 100644
--- a/src/java/org/apache/cassandra/db/transform/Stack.java
+++ b/src/java/org/apache/cassandra/db/transform/Stack.java
@@ -24,6 +24,8 @@
 
 class Stack
 {
+    public static final Transformation[] EMPTY_TRANSFORMATIONS = new Transformation[0];
+    public static final MoreContentsHolder[] EMPTY_MORE_CONTENTS_HOLDERS = new MoreContentsHolder[0];
     static final Stack EMPTY = new Stack();
 
     Transformation[] stack;
@@ -44,8 +46,8 @@
 
     Stack()
     {
-        stack = new Transformation[0];
-        moreContents = new MoreContentsHolder[0];
+        stack = EMPTY_TRANSFORMATIONS;
+        moreContents = EMPTY_MORE_CONTENTS_HOLDERS;
     }
 
     Stack(Stack copy)
diff --git a/src/java/org/apache/cassandra/db/transform/Transformation.java b/src/java/org/apache/cassandra/db/transform/Transformation.java
index 06dd057..77f91e4 100644
--- a/src/java/org/apache/cassandra/db/transform/Transformation.java
+++ b/src/java/org/apache/cassandra/db/transform/Transformation.java
@@ -20,6 +20,7 @@
  */
 package org.apache.cassandra.db.transform;
 
+import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.DeletionTime;
 import org.apache.cassandra.db.PartitionColumns;
 import org.apache.cassandra.db.partitions.PartitionIterator;
@@ -83,6 +84,11 @@
     }
 
     /**
+     * Applied to the partition key of any rows/unfiltered iterator we are applied to
+     */
+    protected DecoratedKey applyToPartitionKey(DecoratedKey key) { return key; }
+
+    /**
      * Applied to the static row of any rows iterator.
      *
      * NOTE that this is only applied to the first iterator in any sequence of iterators filled by a MoreContents;
@@ -104,6 +110,17 @@
         return deletionTime;
     }
 
+    /**
+     * Applied to the {@code PartitionColumns} of any rows iterator.
+     *
+     * NOTE: same remark than for applyToDeletion: it is only applied to the first iterator in a sequence of iterators
+     * filled by MoreContents.
+     */
+    protected PartitionColumns applyToPartitionColumns(PartitionColumns columns)
+    {
+        return columns;
+    }
+
 
     //******************************************************
     //          Static Application Methods
diff --git a/src/java/org/apache/cassandra/db/transform/UnfilteredRows.java b/src/java/org/apache/cassandra/db/transform/UnfilteredRows.java
index c631f2e..2dccad7 100644
--- a/src/java/org/apache/cassandra/db/transform/UnfilteredRows.java
+++ b/src/java/org/apache/cassandra/db/transform/UnfilteredRows.java
@@ -28,7 +28,7 @@
 
 final class UnfilteredRows extends BaseRows<Unfiltered, UnfilteredRowIterator> implements UnfilteredRowIterator
 {
-    private PartitionColumns columns;
+    private PartitionColumns partitionColumns;
     private DeletionTime partitionLevelDeletion;
 
     public UnfilteredRows(UnfilteredRowIterator input)
@@ -39,22 +39,25 @@
     public UnfilteredRows(UnfilteredRowIterator input, PartitionColumns columns)
     {
         super(input);
-        this.columns = columns;
+        partitionColumns = columns;
         partitionLevelDeletion = input.partitionLevelDeletion();
     }
 
-    public PartitionColumns columns()
-    {
-        return columns;
-    }
-
     @Override
     void add(Transformation add)
     {
         super.add(add);
+        partitionColumns = add.applyToPartitionColumns(partitionColumns);
         partitionLevelDeletion = add.applyToDeletion(partitionLevelDeletion);
     }
 
+    @Override
+    public PartitionColumns columns()
+    {
+        return partitionColumns;
+    }
+
+    @Override
     public DeletionTime partitionLevelDeletion()
     {
         return partitionLevelDeletion;
diff --git a/src/java/org/apache/cassandra/db/view/TableViews.java b/src/java/org/apache/cassandra/db/view/TableViews.java
index 34d112f..5636192 100644
--- a/src/java/org/apache/cassandra/db/view/TableViews.java
+++ b/src/java/org/apache/cassandra/db/view/TableViews.java
@@ -28,7 +28,7 @@
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.apache.cassandra.db.filter.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.partitions.*;
@@ -105,7 +105,7 @@
             viewCfs.dumpMemtable();
     }
 
-    public void truncateBlocking(ReplayPosition replayAfter, long truncatedAt)
+    public void truncateBlocking(CommitLogPosition replayAfter, long truncatedAt)
     {
         for (ColumnFamilyStore viewCfs : allViewsCfs())
         {
@@ -137,6 +137,7 @@
 
         // Read modified rows
         int nowInSec = FBUtilities.nowInSeconds();
+        long queryStartNanoTime = System.nanoTime();
         SinglePartitionReadCommand command = readExistingRowsCommand(update, views, nowInSec);
         if (command == null)
             return;
@@ -144,7 +145,7 @@
         ColumnFamilyStore cfs = Keyspace.openAndGetStore(update.metadata());
         long start = System.nanoTime();
         Collection<Mutation> mutations;
-        try (ReadOrderGroup orderGroup = command.startOrderGroup();
+        try (ReadExecutionController orderGroup = command.executionController();
              UnfilteredRowIterator existings = UnfilteredPartitionIterators.getOnlyElement(command.executeLocally(orderGroup), command);
              UnfilteredRowIterator updates = update.unfilteredIterator())
         {
@@ -153,7 +154,7 @@
         Keyspace.openAndGetStore(update.metadata()).metric.viewReadTime.update(System.nanoTime() - start, TimeUnit.NANOSECONDS);
 
         if (!mutations.isEmpty())
-            StorageProxy.mutateMV(update.partitionKey().getKey(), mutations, writeCommitLog, baseComplete);
+            StorageProxy.mutateMV(update.partitionKey().getKey(), mutations, writeCommitLog, baseComplete, queryStartNanoTime);
     }
 
 
diff --git a/src/java/org/apache/cassandra/db/view/View.java b/src/java/org/apache/cassandra/db/view/View.java
index b5cd387..6653836 100644
--- a/src/java/org/apache/cassandra/db/view/View.java
+++ b/src/java/org/apache/cassandra/db/view/View.java
@@ -33,7 +33,6 @@
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.config.ViewDefinition;
-import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.cql3.MultiColumnRelation;
 import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.Relation;
@@ -57,6 +56,8 @@
  */
 public class View
 {
+    public final static String USAGE_WARNING = "Materialized views are experimental and are not recommended for production use.";
+
     private static final Logger logger = LoggerFactory.getLogger(View.class);
 
     public final String name;
@@ -92,13 +93,11 @@
 
     /**
      * This updates the columns stored which are dependent on the base CFMetaData.
-     *
-     * @return true if the view contains only columns which are part of the base's primary key; false if there is at
-     *         least one column which is not.
      */
     public void updateDefinition(ViewDefinition definition)
     {
         this.definition = definition;
+
         List<ColumnDefinition> nonPKDefPartOfViewPK = new ArrayList<>();
         for (ColumnDefinition baseColumn : baseCfs.metadata.allColumns())
         {
@@ -260,12 +259,12 @@
             if (rel.isMultiColumn())
             {
                 sb.append(((MultiColumnRelation) rel).getEntities().stream()
-                        .map(ColumnIdentifier.Raw::toCQLString)
+                        .map(ColumnDefinition.Raw::toString)
                         .collect(Collectors.joining(", ", "(", ")")));
             }
             else
             {
-                sb.append(((SingleColumnRelation) rel).getEntity().toCQLString());
+                sb.append(((SingleColumnRelation) rel).getEntity());
             }
 
             sb.append(" ").append(rel.operator()).append(" ");
diff --git a/src/java/org/apache/cassandra/db/view/ViewBuilder.java b/src/java/org/apache/cassandra/db/view/ViewBuilder.java
index ca60da3..412c83c 100644
--- a/src/java/org/apache/cassandra/db/view/ViewBuilder.java
+++ b/src/java/org/apache/cassandra/db/view/ViewBuilder.java
@@ -43,6 +43,7 @@
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.io.sstable.ReducingKeyIterator;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.repair.SystemDistributedKeyspace;
 import org.apache.cassandra.service.StorageProxy;
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.utils.FBUtilities;
@@ -69,7 +70,6 @@
 
     private void buildKey(DecoratedKey key)
     {
-        AtomicLong noBase = new AtomicLong(Long.MAX_VALUE);
         ReadQuery selectQuery = view.getReadQuery();
 
         if (!selectQuery.selectsKey(key))
@@ -85,28 +85,35 @@
         // and pretend that there is nothing pre-existing.
         UnfilteredRowIterator empty = UnfilteredRowIterators.noRowsIterator(baseCfs.metadata, key, Rows.EMPTY_STATIC_ROW, DeletionTime.LIVE, false);
 
-        try (ReadOrderGroup orderGroup = command.startOrderGroup();
+        try (ReadExecutionController orderGroup = command.executionController();
              UnfilteredRowIterator data = UnfilteredPartitionIterators.getOnlyElement(command.executeLocally(orderGroup), command))
         {
             Iterator<Collection<Mutation>> mutations = baseCfs.keyspace.viewManager
                                                       .forTable(baseCfs.metadata)
                                                       .generateViewUpdates(Collections.singleton(view), data, empty, nowInSec, true);
 
-            mutations.forEachRemaining(m -> StorageProxy.mutateMV(key.getKey(), m, true, noBase));
+            AtomicLong noBase = new AtomicLong(Long.MAX_VALUE);
+            mutations.forEachRemaining(m -> StorageProxy.mutateMV(key.getKey(), m, true, noBase, System.nanoTime()));
         }
     }
 
     public void run()
     {
         logger.debug("Starting view builder for {}.{}", baseCfs.metadata.ksName, view.name);
+        logger.trace("Running view builder for {}.{}", baseCfs.metadata.ksName, view.name);
+        UUID localHostId = SystemKeyspace.getOrInitializeLocalHostId();
         String ksname = baseCfs.metadata.ksName, viewName = view.name;
 
         if (SystemKeyspace.isViewBuilt(ksname, viewName))
         {
             logger.debug("View already marked built for {}.{}", baseCfs.metadata.ksName, view.name);
+            if (!SystemKeyspace.isViewStatusReplicated(ksname, viewName))
+                updateDistributed(ksname, viewName, localHostId);
+
             completed.countDown();
             return;
         }
+
         Iterable<Range<Token>> ranges = StorageService.instance.getLocalRanges(baseCfs.metadata.ksName);
 
         final Pair<Integer, Token> buildStatus = SystemKeyspace.getViewBuildStatus(ksname, viewName);
@@ -137,6 +144,7 @@
         try (Refs<SSTableReader> sstables = baseCfs.selectAndReference(function).refs;
              ReducingKeyIterator iter = new ReducingKeyIterator(sstables))
         {
+            SystemDistributedKeyspace.startViewBuild(ksname, viewName, localHostId);
             while (!isStopRequested() && iter.hasNext())
             {
                 DecoratedKey key = iter.next();
@@ -166,6 +174,7 @@
             {
                 logger.debug("Marking view({}.{}) as built covered {} keys ", ksname, viewName, keysBuilt);
                 SystemKeyspace.finishViewBuildStatus(ksname, viewName);
+                updateDistributed(ksname, viewName, localHostId);
             }
             else
             {
@@ -174,8 +183,7 @@
         }
         catch (Exception e)
         {
-            final ViewBuilder builder = new ViewBuilder(baseCfs, view);
-            ScheduledExecutors.nonPeriodicTasks.schedule(() -> CompactionManager.instance.submitViewBuilder(builder),
+            ScheduledExecutors.nonPeriodicTasks.schedule(() -> CompactionManager.instance.submitViewBuilder(this),
                                                          5,
                                                          TimeUnit.MINUTES);
             logger.warn("Materialized View failed to complete, sleeping 5 minutes before restarting", e);
@@ -183,6 +191,22 @@
         completed.countDown();
     }
 
+    private void updateDistributed(String ksname, String viewName, UUID localHostId)
+    {
+        try
+        {
+            SystemDistributedKeyspace.successfulViewBuild(ksname, viewName, localHostId);
+            SystemKeyspace.setViewBuiltReplicated(ksname, viewName);
+        }
+        catch (Exception e)
+        {
+            ScheduledExecutors.nonPeriodicTasks.schedule(() -> CompactionManager.instance.submitViewBuilder(this),
+                                                         5,
+                                                         TimeUnit.MINUTES);
+            logger.warn("Failed to updated the distributed status of view, sleeping 5 minutes before retrying", e);
+        }
+    }
+
     public CompactionInfo getCompactionInfo()
     {
         long rangesCompleted = 0, rangesTotal = 0;
diff --git a/src/java/org/apache/cassandra/db/view/ViewManager.java b/src/java/org/apache/cassandra/db/view/ViewManager.java
index d1cfd9e..cb1e02b 100644
--- a/src/java/org/apache/cassandra/db/view/ViewManager.java
+++ b/src/java/org/apache/cassandra/db/view/ViewManager.java
@@ -17,7 +17,6 @@
  */
 package org.apache.cassandra.db.view;
 
-import java.nio.ByteBuffer;
 import java.util.*;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ConcurrentHashMap;
@@ -31,17 +30,17 @@
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.ViewDefinition;
 import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.partitions.*;
-
+import org.apache.cassandra.repair.SystemDistributedKeyspace;
+import org.apache.cassandra.service.StorageService;
 
 /**
  * Manages {@link View}'s for a single {@link ColumnFamilyStore}. All of the views for that table are created when this
  * manager is initialized.
  *
  * The main purposes of the manager are to provide a single location for updates to be vetted to see whether they update
- * any views {@link ViewManager#updatesAffectView(Collection, boolean)}, provide locks to prevent multiple
- * updates from creating incoherent updates in the view {@link ViewManager#acquireLockFor(ByteBuffer)}, and
+ * any views {@link #updatesAffectView(Collection, boolean)}, provide locks to prevent multiple
+ * updates from creating incoherent updates in the view {@link #acquireLockFor(int)}, and
  * to affect change on the view.
  *
  * TODO: I think we can get rid of that class. For addition/removal of view by names, we could move it Keyspace. And we
@@ -69,7 +68,7 @@
 
     public boolean updatesAffectView(Collection<? extends IMutation> mutations, boolean coordinatorBatchlog)
     {
-        if (coordinatorBatchlog && !enableCoordinatorBatchlog)
+        if (!enableCoordinatorBatchlog && coordinatorBatchlog)
             return false;
 
         for (IMutation mutation : mutations)
@@ -126,6 +125,20 @@
                 addView(entry.getValue());
         }
 
+        // Building views involves updating view build status in the system_distributed
+        // keyspace and therefore it requires ring information. This check prevents builds
+        // being submitted when Keyspaces are initialized during CassandraDaemon::setup as
+        // that happens before StorageService & gossip are initialized. After SS has been
+        // init'd we schedule builds for *all* views anyway, so this doesn't have any effect
+        // on startup. It does mean however, that builds will not be triggered if gossip is
+        // disabled via JMX or nodetool as that sets SS to an uninitialized state.
+        if (!StorageService.instance.isInitialized())
+        {
+            logger.info("Not submitting build tasks for views in keyspace {} as " +
+                        "storage service is not initialized", keyspace.getName());
+            return;
+        }
+
         for (View view : allViews())
         {
             view.build();
@@ -159,6 +172,7 @@
 
         forTable(view.getDefinition().baseTableMetadata()).removeByName(name);
         SystemKeyspace.setViewRemoved(keyspace.getName(), view.name);
+        SystemDistributedKeyspace.setViewRemoved(keyspace.getName(), view.name);
     }
 
     public View getByName(String name)
@@ -186,9 +200,9 @@
         return views;
     }
 
-    public static Lock acquireLockFor(ByteBuffer key)
+    public static Lock acquireLockFor(int keyAndCfidHash)
     {
-        Lock lock = LOCKS.get(key);
+        Lock lock = LOCKS.get(keyAndCfidHash);
 
         if (lock.tryLock())
             return lock;
diff --git a/src/java/org/apache/cassandra/db/view/ViewUpdateGenerator.java b/src/java/org/apache/cassandra/db/view/ViewUpdateGenerator.java
index 74d3e52..7937e05 100644
--- a/src/java/org/apache/cassandra/db/view/ViewUpdateGenerator.java
+++ b/src/java/org/apache/cassandra/db/view/ViewUpdateGenerator.java
@@ -136,7 +136,7 @@
 
     /**
      * Returns the updates that needs to be done to the view given the base table updates
-     * passed to {@link #generateViewMutations}.
+     * passed to {@link #addBaseTableUpdate}.
      *
      * @return the updates to do to the view.
      */
@@ -409,7 +409,7 @@
               * TODO This is a dirty overload of LivenessInfo and we should modify
               * the storage engine to properly support this on CASSANDRA-13826.
               */
-            LivenessInfo info = LivenessInfo.create(timestamp, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSec);
+            LivenessInfo info = LivenessInfo.withExpirationTime(timestamp, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSec);
             currentViewEntryBuilder.addPrimaryKeyLivenessInfo(info);
         }
         currentViewEntryBuilder.addRowDeletion(mergedBaseRow.deletion());
@@ -438,7 +438,7 @@
                 clusteringValues[viewColumn.position()] = value;
         }
 
-        currentViewEntryBuilder.newRow(new Clustering(clusteringValues));
+        currentViewEntryBuilder.newRow(Clustering.make(clusteringValues));
     }
 
     private LivenessInfo computeLivenessInfoForEntry(Row baseRow)
@@ -487,14 +487,14 @@
                 }
             }
             if (baseLiveness.isLive(nowInSec) && !baseLiveness.isExpiring())
-                return LivenessInfo.create(viewMetadata, timestamp, nowInSec);
+                return LivenessInfo.create(timestamp, nowInSec);
             if (hasNonExpiringLiveCell)
-                return LivenessInfo.create(viewMetadata, timestamp, nowInSec);
+                return LivenessInfo.create(timestamp, nowInSec);
             if (biggestExpirationCell == null)
                 return baseLiveness;
             if (biggestExpirationCell.localDeletionTime() > baseLiveness.localExpirationTime()
                     || !baseLiveness.isLive(nowInSec))
-                return LivenessInfo.create(timestamp,
+                return LivenessInfo.withExpirationTime(timestamp,
                                                        biggestExpirationCell.ttl(),
                                                        biggestExpirationCell.localDeletionTime());
             return baseLiveness;
@@ -503,7 +503,7 @@
         Cell cell = baseRow.getCell(view.baseNonPKColumnsInViewPK.get(0));
         assert isLive(cell) : "We shouldn't have got there if the base row had no associated entry";
 
-        return LivenessInfo.create(cell.timestamp(), cell.ttl(), cell.localDeletionTime());
+        return LivenessInfo.withExpirationTime(cell.timestamp(), cell.ttl(), cell.localDeletionTime());
     }
 
     private long computeTimestampForEntryDeletion(Row existingBaseRow, Row mergedBaseRow)
diff --git a/src/java/org/apache/cassandra/db/view/ViewUtils.java b/src/java/org/apache/cassandra/db/view/ViewUtils.java
index 4d9517f..4dc1766 100644
--- a/src/java/org/apache/cassandra/db/view/ViewUtils.java
+++ b/src/java/org/apache/cassandra/db/view/ViewUtils.java
@@ -45,7 +45,7 @@
      * nodes in the local datacenter when calculating cardinality.
      *
      * For example, if we have the following ring:
-     *   A, T1 -> B, T2 -> C, T3 -> A
+     *   {@code A, T1 -> B, T2 -> C, T3 -> A}
      *
      * For the token T1, at RF=1, A would be included, so A's cardinality for T1 is 1. For the token T1, at RF=2, B would
      * be included, so B's cardinality for token T1 is 2. For token T3, at RF = 2, A would be included, so A's cardinality
diff --git a/src/java/org/apache/cassandra/dht/BootStrapper.java b/src/java/org/apache/cassandra/dht/BootStrapper.java
index 1c40482..1e00f48 100644
--- a/src/java/org/apache/cassandra/dht/BootStrapper.java
+++ b/src/java/org/apache/cassandra/dht/BootStrapper.java
@@ -33,12 +33,15 @@
 import org.apache.cassandra.dht.tokenallocator.TokenAllocation;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.gms.FailureDetector;
+import org.apache.cassandra.gms.Gossiper;
 import org.apache.cassandra.io.IVersionedSerializer;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.locator.AbstractReplicationStrategy;
 import org.apache.cassandra.locator.TokenMetadata;
+import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.streaming.*;
+import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.progress.ProgressEvent;
 import org.apache.cassandra.utils.progress.ProgressEventNotifierSupport;
 import org.apache.cassandra.utils.progress.ProgressEventType;
@@ -73,7 +76,8 @@
                                                    "Bootstrap",
                                                    useStrictConsistency,
                                                    DatabaseDescriptor.getEndpointSnitch(),
-                                                   stateStore);
+                                                   stateStore,
+                                                   true);
         streamer.addSourceFilter(new RangeStreamer.FailureDetectorSourceFilter(FailureDetector.instance));
         streamer.addSourceFilter(new RangeStreamer.ExcludeLocalNodeFilter());
 
@@ -154,7 +158,7 @@
      * otherwise, if allocationKeyspace is specified use the token allocation algorithm to generate suitable tokens
      * else choose num_tokens tokens at random
      */
-    public static Collection<Token> getBootstrapTokens(final TokenMetadata metadata, InetAddress address) throws ConfigurationException
+    public static Collection<Token> getBootstrapTokens(final TokenMetadata metadata, InetAddress address, int schemaWaitDelay) throws ConfigurationException
     {
         String allocationKeyspace = DatabaseDescriptor.getAllocateTokensForKeyspace();
         Collection<String> initialTokens = DatabaseDescriptor.getInitialTokens();
@@ -170,7 +174,7 @@
             throw new ConfigurationException("num_tokens must be >= 1");
 
         if (allocationKeyspace != null)
-            return allocateTokens(metadata, address, allocationKeyspace, numTokens);
+            return allocateTokens(metadata, address, allocationKeyspace, numTokens, schemaWaitDelay);
 
         if (numTokens == 1)
             logger.warn("Picking random token for a single vnode.  You should probably add more vnodes and/or use the automatic token allocation mechanism.");
@@ -181,7 +185,7 @@
     private static Collection<Token> getSpecifiedTokens(final TokenMetadata metadata,
                                                         Collection<String> initialTokens)
     {
-        logger.trace("tokens manually specified as {}",  initialTokens);
+        logger.info("tokens manually specified as {}",  initialTokens);
         List<Token> tokens = new ArrayList<>(initialTokens.size());
         for (String tokenString : initialTokens)
         {
@@ -196,8 +200,13 @@
     static Collection<Token> allocateTokens(final TokenMetadata metadata,
                                             InetAddress address,
                                             String allocationKeyspace,
-                                            int numTokens)
+                                            int numTokens,
+                                            int schemaWaitDelay)
     {
+        StorageService.instance.waitForSchema(schemaWaitDelay);
+        if (!FBUtilities.getBroadcastAddress().equals(InetAddress.getLoopbackAddress()))
+            Gossiper.waitToSettle();
+
         Keyspace ks = Keyspace.open(allocationKeyspace);
         if (ks == null)
             throw new ConfigurationException("Problem opening token allocation keyspace " + allocationKeyspace);
@@ -215,6 +224,8 @@
             if (metadata.getEndpoint(token) == null)
                 tokens.add(token);
         }
+
+        logger.info("Generated random tokens. tokens are {}", tokens);
         return tokens;
     }
 
diff --git a/src/java/org/apache/cassandra/dht/Bounds.java b/src/java/org/apache/cassandra/dht/Bounds.java
index a125168..626eaa6 100644
--- a/src/java/org/apache/cassandra/dht/Bounds.java
+++ b/src/java/org/apache/cassandra/dht/Bounds.java
@@ -18,7 +18,6 @@
 package org.apache.cassandra.dht;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
diff --git a/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java b/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java
index bbf6fd6..1271a5a 100644
--- a/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java
@@ -41,6 +41,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
 
 public class ByteOrderedPartitioner implements IPartitioner
 {
@@ -157,6 +158,11 @@
         return new BytesToken(bytesForBig(midpair.left, sigbytes, midpair.right));
     }
 
+    public Token split(Token left, Token right, double ratioToLeft)
+    {
+        throw new UnsupportedOperationException();
+    }
+
     /**
      * Convert a byte array containing the most significant of 'sigbytes' bytes
      * representing a big-endian magnitude into a BigInteger.
@@ -203,13 +209,18 @@
 
     public BytesToken getRandomToken()
     {
-        Random r = new Random();
+       return getRandomToken(ThreadLocalRandom.current());
+    }
+
+    public BytesToken getRandomToken(Random random)
+    {
         byte[] buffer = new byte[16];
-        r.nextBytes(buffer);
+        random.nextBytes(buffer);
         return new BytesToken(buffer);
     }
 
-    private final Token.TokenFactory tokenFactory = new Token.TokenFactory() {
+    private final Token.TokenFactory tokenFactory = new Token.TokenFactory() 
+    {
         public ByteBuffer toByteArray(Token token)
         {
             BytesToken bytesToken = (BytesToken) token;
diff --git a/src/java/org/apache/cassandra/dht/ExcludingBounds.java b/src/java/org/apache/cassandra/dht/ExcludingBounds.java
index 8fbde28..ed6c2fc 100644
--- a/src/java/org/apache/cassandra/dht/ExcludingBounds.java
+++ b/src/java/org/apache/cassandra/dht/ExcludingBounds.java
@@ -23,7 +23,7 @@
 import org.apache.cassandra.utils.Pair;
 
 /**
- * AbstractBounds containing neither of its endpoints: (left, right).  Used by CQL key > X AND key < Y range scans.
+ * AbstractBounds containing neither of its endpoints: (left, right).  Used by {@code CQL key > X AND key < Y} range scans.
  */
 public class ExcludingBounds<T extends RingPosition<T>> extends AbstractBounds<T>
 {
diff --git a/src/java/org/apache/cassandra/dht/IPartitioner.java b/src/java/org/apache/cassandra/dht/IPartitioner.java
index e0a08dc..e342bd0 100644
--- a/src/java/org/apache/cassandra/dht/IPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/IPartitioner.java
@@ -20,6 +20,8 @@
 import java.nio.ByteBuffer;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.Random;
 
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.marshal.AbstractType;
@@ -43,12 +45,28 @@
     public Token midpoint(Token left, Token right);
 
     /**
+     * Calculate a Token which take approximate 0 <= ratioToLeft <= 1 ownership of the given range.
+     */
+    public Token split(Token left, Token right, double ratioToLeft);
+
+    /**
      * @return A Token smaller than all others in the range that is being partitioned.
      * Not legal to assign to a node or key.  (But legal to use in range scans.)
      */
     public Token getMinimumToken();
 
     /**
+     * The biggest token for this partitioner, unlike getMinimumToken, this token is actually used and users wanting to
+     * include all tokens need to do getMaximumToken().maxKeyBound()
+     *
+     * Not implemented for the ordered partitioners
+     */
+    default Token getMaximumToken()
+    {
+        throw new UnsupportedOperationException("If you are using a splitting partitioner, getMaximumToken has to be implemented");
+    }
+
+    /**
      * @return a Token that can be used to route a given key
      * (This is NOT a method to create a Token from its string representation;
      * for that, use TokenFactory.fromString.)
@@ -60,6 +78,13 @@
      */
     public Token getRandomToken();
 
+    /**
+     * @param random instance of Random to use when generating the token
+     *
+     * @return a randomly generated token
+     */
+    public Token getRandomToken(Random random);
+
     public Token.TokenFactory getTokenFactory();
 
     /**
@@ -84,4 +109,9 @@
      * Used by secondary indices.
      */
     public AbstractType<?> partitionOrdering();
+
+    default Optional<Splitter> splitter()
+    {
+        return Optional.empty();
+    }
 }
diff --git a/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java b/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java
index 19c098e..ac5185f 100644
--- a/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java
+++ b/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java
@@ -23,7 +23,7 @@
 import org.apache.cassandra.utils.Pair;
 
 /**
- * AbstractBounds containing only its left endpoint: [left, right).  Used by CQL key >= X AND key < Y range scans.
+ * AbstractBounds containing only its left endpoint: [left, right).  Used by {@code CQL key >= X AND key < Y} range scans.
  */
 public class IncludingExcludingBounds<T extends RingPosition<T>> extends AbstractBounds<T>
 {
diff --git a/src/java/org/apache/cassandra/dht/LocalPartitioner.java b/src/java/org/apache/cassandra/dht/LocalPartitioner.java
index aaf2569..168601c 100644
--- a/src/java/org/apache/cassandra/dht/LocalPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/LocalPartitioner.java
@@ -21,6 +21,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Random;
 
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.CachedHashDecoratedKey;
@@ -50,6 +51,11 @@
         throw new UnsupportedOperationException();
     }
 
+    public Token split(Token left, Token right, double ratioToLeft)
+    {
+        throw new UnsupportedOperationException();
+    }
+
     public LocalToken getMinimumToken()
     {
         return new LocalToken(ByteBufferUtil.EMPTY_BYTE_BUFFER);
@@ -65,6 +71,11 @@
         throw new UnsupportedOperationException();
     }
 
+    public LocalToken getRandomToken(Random random)
+    {
+        throw new UnsupportedOperationException();
+    }
+
     public Token.TokenFactory getTokenFactory()
     {
         return tokenFactory;
diff --git a/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java b/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java
index d68be3f..0f922e3 100644
--- a/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java
+++ b/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java
@@ -48,6 +48,19 @@
     public static final Murmur3Partitioner instance = new Murmur3Partitioner();
     public static final AbstractType<?> partitionOrdering = new PartitionerDefinedOrder(instance);
 
+    private final Splitter splitter = new Splitter(this)
+    {
+        public Token tokenForValue(BigInteger value)
+        {
+            return new LongToken(value.longValue());
+        }
+
+        public BigInteger valueForToken(Token token)
+        {
+            return BigInteger.valueOf(((LongToken) token).token);
+        }
+    };
+
     public DecoratedKey decorateKey(ByteBuffer key)
     {
         long[] hash = getHash(key);
@@ -81,6 +94,42 @@
         return new LongToken(midpoint.longValue());
     }
 
+    public Token split(Token lToken, Token rToken, double ratioToLeft)
+    {
+        BigDecimal l = BigDecimal.valueOf(((LongToken) lToken).token),
+                   r = BigDecimal.valueOf(((LongToken) rToken).token),
+                   ratio = BigDecimal.valueOf(ratioToLeft);
+        long newToken;
+
+        if (l.compareTo(r) < 0)
+        {
+            newToken = r.subtract(l).multiply(ratio).add(l).toBigInteger().longValue();
+        }
+        else
+        {
+            // wrapping case
+            // L + ((R - min) + (max - L)) * pct
+            BigDecimal max = BigDecimal.valueOf(MAXIMUM);
+            BigDecimal min = BigDecimal.valueOf(MINIMUM.token);
+
+            BigInteger token = max.subtract(min).add(r).subtract(l).multiply(ratio).add(l).toBigInteger();
+
+            BigInteger maxToken = BigInteger.valueOf(MAXIMUM);
+
+            if (token.compareTo(maxToken) <= 0)
+            {
+                newToken = token.longValue();
+            }
+            else
+            {
+                // if the value is above maximum
+                BigInteger minToken = BigInteger.valueOf(MINIMUM.token);
+                newToken = minToken.add(token.subtract(maxToken)).longValue();
+            }
+        }
+        return new LongToken(newToken);
+    }
+
     public LongToken getMinimumToken()
     {
         return MINIMUM;
@@ -265,7 +314,7 @@
         {
             try
             {
-                Long.valueOf(token);
+                fromString(token);
             }
             catch (NumberFormatException e)
             {
@@ -291,8 +340,18 @@
         return LongType.instance;
     }
 
+    public Token getMaximumToken()
+    {
+        return new LongToken(Long.MAX_VALUE);
+    }
+
     public AbstractType<?> partitionOrdering()
     {
         return partitionOrdering;
     }
+
+    public Optional<Splitter> splitter()
+    {
+        return Optional.of(splitter);
+    }
 }
diff --git a/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java b/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java
index 96b4ca0..954b0af 100644
--- a/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java
@@ -21,6 +21,7 @@
 import java.nio.ByteBuffer;
 import java.nio.charset.CharacterCodingException;
 import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
 
 import org.apache.cassandra.config.*;
 import org.apache.cassandra.db.DecoratedKey;
@@ -37,6 +38,8 @@
 
 public class OrderPreservingPartitioner implements IPartitioner
 {
+    private static final String rndchars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+
     public static final StringToken MINIMUM = new StringToken("");
 
     public static final BigInteger CHAR_MASK = new BigInteger("65535");
@@ -60,6 +63,11 @@
         return new StringToken(stringForBig(midpair.left, sigchars, midpair.right));
     }
 
+    public Token split(Token left, Token right, double ratioToLeft)
+    {
+        throw new UnsupportedOperationException();
+    }
+
     /**
      * Copies the characters of the given string into a BigInteger.
      *
@@ -106,12 +114,14 @@
 
     public StringToken getRandomToken()
     {
-        String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
-        Random r = new Random();
+        return getRandomToken(ThreadLocalRandom.current());
+    }
+
+    public StringToken getRandomToken(Random random)
+    {
         StringBuilder buffer = new StringBuilder();
-        for (int j = 0; j < 16; j++) {
-            buffer.append(chars.charAt(r.nextInt(chars.length())));
-        }
+        for (int j = 0; j < 16; j++)
+            buffer.append(rndchars.charAt(random.nextInt(rndchars.length())));
         return new StringToken(buffer.toString());
     }
 
diff --git a/src/java/org/apache/cassandra/dht/RandomPartitioner.java b/src/java/org/apache/cassandra/dht/RandomPartitioner.java
index c7837c9..bdf8b85 100644
--- a/src/java/org/apache/cassandra/dht/RandomPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/RandomPartitioner.java
@@ -74,6 +74,18 @@
     public static final RandomPartitioner instance = new RandomPartitioner();
     public static final AbstractType<?> partitionOrdering = new PartitionerDefinedOrder(instance);
 
+    private final Splitter splitter = new Splitter(this)
+    {
+        public Token tokenForValue(BigInteger value)
+        {
+            return new BigIntegerToken(value);
+        }
+
+        public BigInteger valueForToken(Token token)
+        {
+            return ((BigIntegerToken)token).getTokenValue();
+        }
+    };
 
     public DecoratedKey decorateKey(ByteBuffer key)
     {
@@ -90,6 +102,32 @@
         return new BigIntegerToken(midpair.left);
     }
 
+    public Token split(Token ltoken, Token rtoken, double ratioToLeft)
+    {
+        BigDecimal left = ltoken.equals(MINIMUM) ? BigDecimal.ZERO : new BigDecimal(((BigIntegerToken)ltoken).token),
+                   right = rtoken.equals(MINIMUM) ? BigDecimal.ZERO : new BigDecimal(((BigIntegerToken)rtoken).token),
+                   ratio = BigDecimal.valueOf(ratioToLeft);
+
+        BigInteger newToken;
+
+        if (left.compareTo(right) < 0)
+        {
+            newToken = right.subtract(left).multiply(ratio).add(left).toBigInteger();
+        }
+        else
+        {
+            // wrapping case
+            // L + ((R - min) + (max - L)) * ratio
+            BigDecimal max = new BigDecimal(MAXIMUM);
+
+            newToken = max.add(right).subtract(left).multiply(ratio).add(left).toBigInteger().mod(MAXIMUM);
+        }
+
+        assert isValidToken(newToken) : "Invalid tokens from split";
+
+        return new BigIntegerToken(newToken);
+    }
+
     public BigIntegerToken getMinimumToken()
     {
         return MINIMUM;
@@ -103,7 +141,20 @@
         return new BigIntegerToken(token);
     }
 
-    private final Token.TokenFactory tokenFactory = new Token.TokenFactory() {
+    public BigIntegerToken getRandomToken(Random random)
+    {
+        BigInteger token = hashToBigInteger(GuidGenerator.guidAsBytes(random, "host/127.0.0.1", 0));
+        if ( token.signum() == -1 )
+            token = token.multiply(BigInteger.valueOf(-1L));
+        return new BigIntegerToken(token);
+    }
+
+    private boolean isValidToken(BigInteger token) {
+        return token.compareTo(ZERO) >= 0 && token.compareTo(MAXIMUM) <= 0;
+    }
+
+    private final Token.TokenFactory tokenFactory = new Token.TokenFactory()
+    {
         public ByteBuffer toByteArray(Token token)
         {
             BigIntegerToken bigIntegerToken = (BigIntegerToken) token;
@@ -125,11 +176,8 @@
         {
             try
             {
-                BigInteger i = new BigInteger(token);
-                if (i.compareTo(ZERO) < 0)
-                    throw new ConfigurationException("Token must be >= 0");
-                if (i.compareTo(MAXIMUM) > 0)
-                    throw new ConfigurationException("Token must be <= 2**127");
+                if(!isValidToken(new BigInteger(token)))
+                    throw new ConfigurationException("Token must be >= 0 and <= 2**127");
             }
             catch (NumberFormatException e)
             {
@@ -164,7 +212,8 @@
 
         // convenience method for testing
         @VisibleForTesting
-        public BigIntegerToken(String token) {
+        public BigIntegerToken(String token)
+        {
             this(new BigInteger(token));
         }
 
@@ -179,6 +228,19 @@
         {
             return HEAP_SIZE;
         }
+
+        public Token increaseSlightly()
+        {
+            return new BigIntegerToken(token.add(BigInteger.ONE));
+        }
+
+        public double size(Token next)
+        {
+            BigIntegerToken n = (BigIntegerToken) next;
+            BigInteger v = n.token.subtract(token);  // Overflow acceptable and desired.
+            double d = Math.scalb(v.doubleValue(), -127); // Scale so that the full range is 1.
+            return d > 0.0 ? d : (d + 1.0); // Adjust for signed long, also making sure t.size(t) == 1.
+        }
     }
 
     public BigIntegerToken getToken(ByteBuffer key)
@@ -197,17 +259,20 @@
         // 0-case
         if (!i.hasNext()) { throw new RuntimeException("No nodes present in the cluster. Has this node finished starting up?"); }
         // 1-case
-        if (sortedTokens.size() == 1) {
+        if (sortedTokens.size() == 1)
+        {
             ownerships.put(i.next(), new Float(1.0));
         }
         // n-case
-        else {
+        else
+        {
             // NOTE: All divisions must take place in BigDecimals, and all modulo operators must take place in BigIntegers.
             final BigInteger ri = MAXIMUM;                                                  //  (used for addition later)
             final BigDecimal r  = new BigDecimal(ri);                                       // The entire range, 2**127
             Token start = i.next(); BigInteger ti = ((BigIntegerToken)start).token;  // The first token and its value
             Token t; BigInteger tim1 = ti;                                                  // The last token and its value (after loop)
-            while (i.hasNext()) {
+            while (i.hasNext())
+            {
                 t = i.next(); ti = ((BigIntegerToken)t).token;                                      // The next token and its value
                 float x = new BigDecimal(ti.subtract(tim1).add(ri).mod(ri)).divide(r).floatValue(); // %age = ((T(i) - T(i-1) + R) % R) / R
                 ownerships.put(t, x);                                                               // save (T(i) -> %age)
@@ -220,6 +285,11 @@
         return ownerships;
     }
 
+    public Token getMaximumToken()
+    {
+        return new BigIntegerToken(MAXIMUM);
+    }
+
     public AbstractType<?> getTokenValidator()
     {
         return IntegerType.instance;
@@ -230,6 +300,11 @@
         return partitionOrdering;
     }
 
+    public Optional<Splitter> splitter()
+    {
+        return Optional.of(splitter);
+    }
+
     private static BigInteger hashToBigInteger(ByteBuffer data)
     {
         MessageDigest messageDigest = localMD5Digest.get();
diff --git a/src/java/org/apache/cassandra/dht/Range.java b/src/java/org/apache/cassandra/dht/Range.java
index 3cf292a..72c61c7 100644
--- a/src/java/org/apache/cassandra/dht/Range.java
+++ b/src/java/org/apache/cassandra/dht/Range.java
@@ -166,7 +166,7 @@
         boolean thatwraps = isWrapAround(that.left, that.right);
         if (!thiswraps && !thatwraps)
         {
-            // neither wraps.  the straightforward case.
+            // neither wraps:  the straightforward case.
             if (!(left.compareTo(that.right) < 0 && that.left.compareTo(right) < 0))
                 return Collections.emptySet();
             return rangeSet(new Range<T>(ObjectUtils.max(this.left, that.left),
@@ -174,7 +174,7 @@
         }
         if (thiswraps && thatwraps)
         {
-            // if the starts are the same, one contains the other, which we have already ruled out.
+            //both wrap: if the starts are the same, one contains the other, which we have already ruled out.
             assert !this.left.equals(that.left);
             // two wrapping ranges always intersect.
             // since we have already determined that neither this nor that contains the other, we have 2 cases,
@@ -188,9 +188,9 @@
                    ? intersectionBothWrapping(this, that)
                    : intersectionBothWrapping(that, this);
         }
-        if (thiswraps && !thatwraps)
+        if (thiswraps) // this wraps, that does not wrap
             return intersectionOneWrapping(this, that);
-        assert (!thiswraps && thatwraps);
+        // the last case: this does not wrap, that wraps
         return intersectionOneWrapping(that, this);
     }
 
@@ -515,6 +515,23 @@
         return new Range<T>(left, newRight);
     }
 
+    public static <T extends RingPosition<T>> List<Range<T>> sort(Collection<Range<T>> ranges)
+    {
+        List<Range<T>> output = new ArrayList<>(ranges.size());
+        for (Range<T> r : ranges)
+            output.addAll(r.unwrap());
+        // sort by left
+        Collections.sort(output, new Comparator<Range<T>>()
+        {
+            public int compare(Range<T> b1, Range<T> b2)
+            {
+                return b1.left.compareTo(b2.left);
+            }
+        });
+        return output;
+    }
+
+
     /**
      * Compute a range of keys corresponding to a given range of token.
      */
diff --git a/src/java/org/apache/cassandra/dht/RangeStreamer.java b/src/java/org/apache/cassandra/dht/RangeStreamer.java
index 32e084f..a3cc996 100644
--- a/src/java/org/apache/cassandra/dht/RangeStreamer.java
+++ b/src/java/org/apache/cassandra/dht/RangeStreamer.java
@@ -122,19 +122,38 @@
         }
     }
 
+    /**
+     * Source filter which only includes endpoints contained within a provided set.
+     */
+    public static class AllowedSourcesFilter implements ISourceFilter
+    {
+        private final Set<InetAddress> allowedSources;
+
+        public AllowedSourcesFilter(Set<InetAddress> allowedSources)
+        {
+            this.allowedSources = allowedSources;
+        }
+
+        public boolean shouldInclude(InetAddress endpoint)
+        {
+            return allowedSources.contains(endpoint);
+        }
+    }
+
     public RangeStreamer(TokenMetadata metadata,
                          Collection<Token> tokens,
                          InetAddress address,
                          String description,
                          boolean useStrictConsistency,
                          IEndpointSnitch snitch,
-                         StreamStateStore stateStore)
+                         StreamStateStore stateStore,
+                         boolean connectSequentially)
     {
         this.metadata = metadata;
         this.tokens = tokens;
         this.address = address;
         this.description = description;
-        this.streamPlan = new StreamPlan(description, true);
+        this.streamPlan = new StreamPlan(description, true, connectSequentially);
         this.useStrictConsistency = useStrictConsistency;
         this.snitch = snitch;
         this.stateStore = stateStore;
@@ -160,7 +179,7 @@
         if (logger.isTraceEnabled())
         {
             for (Map.Entry<Range<Token>, InetAddress> entry : rangesForKeyspace.entries())
-                logger.trace(String.format("%s: range %s exists on %s", description, entry.getKey(), entry.getValue()));
+                logger.trace("{}: range {} exists on {}", description, entry.getKey(), entry.getValue());
         }
 
         for (Map.Entry<InetAddress, Collection<Range<Token>>> entry : getRangeFetchMap(rangesForKeyspace, sourceFilters, keyspaceName, useStrictConsistency).asMap().entrySet())
@@ -168,7 +187,7 @@
             if (logger.isTraceEnabled())
             {
                 for (Range<Token> r : entry.getValue())
-                    logger.trace(String.format("%s: range %s from source %s for keyspace %s", description, r, entry.getKey(), keyspaceName));
+                    logger.trace("{}: range {} from source {} for keyspace {}", description, r, entry.getKey(), keyspaceName);
             }
             toFetch.put(keyspaceName, entry);
         }
@@ -325,8 +344,8 @@
                         throw new IllegalStateException("Unable to find sufficient sources for streaming range " + range + " in keyspace " + keyspace + " with RF=1. " +
                                                         "Ensure this keyspace contains replicas in the source datacenter.");
                     else
-                        logger.warn("Unable to find sufficient sources for streaming range " + range + " in keyspace " + keyspace + " with RF=1. " +
-                                    "Keyspace might be missing data.");
+                        logger.warn("Unable to find sufficient sources for streaming range {} in keyspace {} with RF=1. " +
+                                    "Keyspace might be missing data.", range, keyspace);
                 }
                 else
                     throw new IllegalStateException("Unable to find sufficient sources for streaming range " + range + " in keyspace " + keyspace);
diff --git a/src/java/org/apache/cassandra/dht/Splitter.java b/src/java/org/apache/cassandra/dht/Splitter.java
new file mode 100644
index 0000000..610ac3e
--- /dev/null
+++ b/src/java/org/apache/cassandra/dht/Splitter.java
@@ -0,0 +1,137 @@
+/*
+ * 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.dht;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Partition splitter.
+ */
+public abstract class Splitter
+{
+    private final IPartitioner partitioner;
+
+    protected Splitter(IPartitioner partitioner)
+    {
+        this.partitioner = partitioner;
+    }
+
+    protected abstract Token tokenForValue(BigInteger value);
+
+    protected abstract BigInteger valueForToken(Token token);
+
+    public List<Token> splitOwnedRanges(int parts, List<Range<Token>> localRanges, boolean dontSplitRanges)
+    {
+        if (localRanges.isEmpty() || parts == 1)
+            return Collections.singletonList(partitioner.getMaximumToken());
+
+        BigInteger totalTokens = BigInteger.ZERO;
+        for (Range<Token> r : localRanges)
+        {
+            BigInteger right = valueForToken(token(r.right));
+            totalTokens = totalTokens.add(right.subtract(valueForToken(r.left)));
+        }
+        BigInteger perPart = totalTokens.divide(BigInteger.valueOf(parts));
+        // the range owned is so tiny we can't split it:
+        if (perPart.equals(BigInteger.ZERO))
+            return Collections.singletonList(partitioner.getMaximumToken());
+
+        if (dontSplitRanges)
+            return splitOwnedRangesNoPartialRanges(localRanges, perPart, parts);
+
+        List<Token> boundaries = new ArrayList<>();
+        BigInteger sum = BigInteger.ZERO;
+        BigInteger tokensLeft = totalTokens;
+        for (Range<Token> r : localRanges)
+        {
+            Token right = token(r.right);
+            BigInteger currentRangeWidth = valueForToken(right).subtract(valueForToken(r.left)).abs();
+            BigInteger left = valueForToken(r.left);
+            while (sum.add(currentRangeWidth).compareTo(perPart) >= 0)
+            {
+                BigInteger withinRangeBoundary = perPart.subtract(sum);
+                left = left.add(withinRangeBoundary);
+                boundaries.add(tokenForValue(left));
+                tokensLeft = tokensLeft.subtract(perPart);
+                currentRangeWidth = currentRangeWidth.subtract(withinRangeBoundary);
+                sum = BigInteger.ZERO;
+                int partsLeft = parts - boundaries.size();
+                if (partsLeft == 0)
+                    break;
+                else if (partsLeft == 1)
+                    perPart = tokensLeft;
+            }
+            sum = sum.add(currentRangeWidth);
+        }
+        boundaries.set(boundaries.size() - 1, partitioner.getMaximumToken());
+
+        assert boundaries.size() == parts : boundaries.size() + "!=" + parts + " " + boundaries + ":" + localRanges;
+        return boundaries;
+    }
+
+    private List<Token> splitOwnedRangesNoPartialRanges(List<Range<Token>> localRanges, BigInteger perPart, int parts)
+    {
+        List<Token> boundaries = new ArrayList<>(parts);
+        BigInteger sum = BigInteger.ZERO;
+
+        int i = 0;
+        final int rangesCount = localRanges.size();
+        while (boundaries.size() < parts - 1 && i < rangesCount - 1)
+        {
+            Range<Token> r = localRanges.get(i);
+            Range<Token> nextRange = localRanges.get(i + 1);
+            Token right = token(r.right);
+            Token nextRight = token(nextRange.right);
+
+            BigInteger currentRangeWidth = valueForToken(right).subtract(valueForToken(r.left));
+            BigInteger nextRangeWidth = valueForToken(nextRight).subtract(valueForToken(nextRange.left));
+            sum = sum.add(currentRangeWidth);
+
+            // does this or next range take us beyond the per part limit?
+            if (sum.compareTo(perPart) > 0 || sum.add(nextRangeWidth).compareTo(perPart) > 0)
+            {
+                // Either this or the next range will take us beyond the perPart limit. Will stopping now or
+                // adding the next range create the smallest difference to perPart?
+                BigInteger diffCurrent = sum.subtract(perPart).abs();
+                BigInteger diffNext = sum.add(nextRangeWidth).subtract(perPart).abs();
+                if (diffNext.compareTo(diffCurrent) >= 0)
+                {
+                    sum = BigInteger.ZERO;
+                    boundaries.add(right);
+                }
+            }
+            i++;
+        }
+        boundaries.add(partitioner.getMaximumToken());
+        return boundaries;
+    }
+
+    /**
+     * We avoid calculating for wrap around ranges, instead we use the actual max token, and then, when translating
+     * to PartitionPositions, we include tokens from .minKeyBound to .maxKeyBound to make sure we include all tokens.
+     */
+    private Token token(Token t)
+    {
+        return t.equals(partitioner.getMinimumToken()) ? partitioner.getMaximumToken() : t;
+    }
+
+}
diff --git a/src/java/org/apache/cassandra/dht/StreamStateStore.java b/src/java/org/apache/cassandra/dht/StreamStateStore.java
index f6046aa..47b3072 100644
--- a/src/java/org/apache/cassandra/dht/StreamStateStore.java
+++ b/src/java/org/apache/cassandra/dht/StreamStateStore.java
@@ -66,6 +66,11 @@
             StreamEvent.SessionCompleteEvent se = (StreamEvent.SessionCompleteEvent) event;
             if (se.success)
             {
+                Set<String> keyspaces = se.transferredRangesPerKeyspace.keySet();
+                for (String keyspace : keyspaces)
+                {
+                    SystemKeyspace.updateTransferredRanges(se.description, se.peer, keyspace, se.transferredRangesPerKeyspace.get(keyspace));
+                }
                 for (StreamRequest request : se.requests)
                 {
                     SystemKeyspace.updateAvailableRanges(request.keyspace, request.ranges);
diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocator.java b/src/java/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocator.java
new file mode 100644
index 0000000..54d80dc
--- /dev/null
+++ b/src/java/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocator.java
@@ -0,0 +1,266 @@
+/*
+ * 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.dht.tokenallocator;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.PriorityQueue;
+import java.util.Queue;
+import java.util.Set;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Queues;
+
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Token;
+
+public class NoReplicationTokenAllocator<Unit> extends TokenAllocatorBase<Unit>
+{
+    PriorityQueue<Weighted<UnitInfo>> sortedUnits = Queues.newPriorityQueue();
+    Map<Unit, PriorityQueue<Weighted<TokenInfo>>> tokensInUnits = Maps.newHashMap();
+
+    private static final double MAX_TAKEOVER_RATIO = 0.90;
+    private static final double MIN_TAKEOVER_RATIO = 1.0 - MAX_TAKEOVER_RATIO;
+
+    public NoReplicationTokenAllocator(NavigableMap<Token, Unit> sortedTokens,
+                                       ReplicationStrategy<Unit> strategy,
+                                       IPartitioner partitioner)
+    {
+        super(sortedTokens, strategy, partitioner);
+    }
+
+    /**
+     * Construct the token ring as a CircularList of TokenInfo,
+     * and populate the ownership of the UnitInfo's provided
+     */
+    private TokenInfo<Unit> createTokenInfos(Map<Unit, UnitInfo<Unit>> units)
+    {
+        if (units.isEmpty())
+            return null;
+
+        // build the circular list
+        TokenInfo<Unit> prev = null;
+        TokenInfo<Unit> first = null;
+        for (Map.Entry<Token, Unit> en : sortedTokens.entrySet())
+        {
+            Token t = en.getKey();
+            UnitInfo<Unit> ni = units.get(en.getValue());
+            TokenInfo<Unit> ti = new TokenInfo<>(t, ni);
+            first = ti.insertAfter(first, prev);
+            prev = ti;
+        }
+
+        TokenInfo<Unit> curr = first;
+        tokensInUnits.clear();
+        sortedUnits.clear();
+        do
+        {
+            populateTokenInfoAndAdjustUnit(curr);
+            curr = curr.next;
+        } while (curr != first);
+
+        for (UnitInfo<Unit> unitInfo : units.values())
+        {
+            sortedUnits.add(new Weighted<UnitInfo>(unitInfo.ownership, unitInfo));
+        }
+
+        return first;
+    }
+
+    /**
+     * Used in tests.
+     */
+    protected void createTokenInfos()
+    {
+        createTokenInfos(createUnitInfos(Maps.newHashMap()));
+    }
+
+    private void populateTokenInfoAndAdjustUnit(TokenInfo<Unit> token)
+    {
+        token.replicationStart = token.prevInRing().token;
+        token.replicationThreshold = token.token;
+        token.replicatedOwnership = token.replicationStart.size(token.token);
+        token.owningUnit.ownership += token.replicatedOwnership;
+
+        PriorityQueue<Weighted<TokenInfo>> unitTokens = tokensInUnits.get(token.owningUnit.unit);
+        if (unitTokens == null)
+        {
+            unitTokens = Queues.newPriorityQueue();
+            tokensInUnits.put(token.owningUnit.unit, unitTokens);
+        }
+        unitTokens.add(new Weighted<TokenInfo>(token.replicatedOwnership, token));
+    }
+
+    private Collection<Token> generateRandomTokens(UnitInfo<Unit> newUnit, int numTokens, Map<Unit, UnitInfo<Unit>> unitInfos)
+    {
+        Set<Token> tokens = new HashSet<>(numTokens);
+        while (tokens.size() < numTokens)
+        {
+            Token token = partitioner.getRandomToken();
+            if (!sortedTokens.containsKey(token))
+            {
+                tokens.add(token);
+                sortedTokens.put(token, newUnit.unit);
+            }
+        }
+        unitInfos.put(newUnit.unit, newUnit);
+        createTokenInfos(unitInfos);
+        return tokens;
+    }
+
+    public Collection<Token> addUnit(Unit newUnit, int numTokens)
+    {
+        assert !tokensInUnits.containsKey(newUnit);
+
+        Map<Object, GroupInfo> groups = Maps.newHashMap();
+        UnitInfo<Unit> newUnitInfo = new UnitInfo<>(newUnit, 0, groups, strategy);
+        Map<Unit, UnitInfo<Unit>> unitInfos = createUnitInfos(groups);
+
+        if (unitInfos.isEmpty())
+            return generateRandomTokens(newUnitInfo, numTokens, unitInfos);
+
+        if (numTokens > sortedTokens.size())
+            return generateRandomTokens(newUnitInfo, numTokens, unitInfos);
+
+        TokenInfo<Unit> head = createTokenInfos(unitInfos);
+
+        // Select the nodes we will work with, extract them from sortedUnits and calculate targetAverage
+        double targetAverage = 0.0;
+        double sum = 0.0;
+        List<Weighted<UnitInfo>> unitsToChange = new ArrayList<>();
+
+        for (int i = 0; i < numTokens; i++)
+        {
+            Weighted<UnitInfo> unit = sortedUnits.peek();
+
+            if (unit == null)
+                break;
+
+            sum += unit.weight;
+            double average = sum / (unitsToChange.size() + 2); // unit and newUnit must be counted
+            if (unit.weight <= average)
+                // No point to include later nodes, target can only decrease from here.
+                break;
+
+            sortedUnits.remove();
+            unitsToChange.add(unit);
+            targetAverage = average;
+        }
+
+        List<Token> newTokens = Lists.newArrayListWithCapacity(numTokens);
+
+        int nr = 0;
+        // calculate the tokens
+        for (Weighted<UnitInfo> unit : unitsToChange)
+        {
+            // TODO: Any better ways to assign how many tokens to change in each node?
+            int tokensToChange = numTokens / unitsToChange.size() + (nr < numTokens % unitsToChange.size() ? 1 : 0);
+
+            Queue<Weighted<TokenInfo>> unitTokens = tokensInUnits.get(unit.value.unit);
+            List<Weighted<TokenInfo>> tokens = Lists.newArrayListWithCapacity(tokensToChange);
+
+            double workWeight = 0;
+            // Extract biggest vnodes and calculate how much weight we can work with.
+            for (int i = 0; i < tokensToChange; i++)
+            {
+                Weighted<TokenInfo> wt = unitTokens.remove();
+                tokens.add(wt);
+                workWeight += wt.weight;
+                unit.value.ownership -= wt.weight;
+            }
+
+            double toTakeOver = unit.weight - targetAverage;
+            // Split toTakeOver proportionally between the vnodes.
+            for (Weighted<TokenInfo> wt : tokens)
+            {
+                double slice;
+                Token token;
+
+                if (toTakeOver < workWeight)
+                {
+                    // Spread decrease.
+                    slice = toTakeOver / workWeight;
+
+                    if (slice < MIN_TAKEOVER_RATIO)
+                        slice = MIN_TAKEOVER_RATIO;
+                    if (slice > MAX_TAKEOVER_RATIO)
+                        slice = MAX_TAKEOVER_RATIO;
+                }
+                else
+                {
+                    slice = MAX_TAKEOVER_RATIO;
+                }
+                token = partitioner.split(wt.value.prevInRing().token, wt.value.token, slice);
+
+                //Token selected, now change all data
+                sortedTokens.put(token, newUnit);
+
+                TokenInfo<Unit> ti = new TokenInfo<>(token, newUnitInfo);
+
+                ti.insertAfter(head, wt.value.prevInRing());
+
+                populateTokenInfoAndAdjustUnit(ti);
+                populateTokenInfoAndAdjustUnit(wt.value);
+                newTokens.add(token);
+            }
+
+            // adjust the weight for current unit
+            sortedUnits.add(new Weighted<>(unit.value.ownership, unit.value));
+            ++nr;
+        }
+        sortedUnits.add(new Weighted<>(newUnitInfo.ownership, newUnitInfo));
+
+        return newTokens;
+    }
+
+    /**
+     * For testing, remove the given unit preserving correct state of the allocator.
+     */
+    void removeUnit(Unit n)
+    {
+        Iterator<Weighted<UnitInfo>> it = sortedUnits.iterator();
+        while (it.hasNext())
+        {
+            if (it.next().value.unit.equals(n))
+            {
+                it.remove();
+                break;
+            }
+        }
+
+        PriorityQueue<Weighted<TokenInfo>> tokenInfos = tokensInUnits.remove(n);
+        Collection<Token> tokens = Lists.newArrayListWithCapacity(tokenInfos.size());
+        for (Weighted<TokenInfo> tokenInfo : tokenInfos)
+        {
+            tokens.add(tokenInfo.value.token);
+        }
+        sortedTokens.keySet().removeAll(tokens);
+    }
+
+    public int getReplicas()
+    {
+        return 1;
+    }
+}
diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/ReplicationAwareTokenAllocator.java b/src/java/org/apache/cassandra/dht/tokenallocator/ReplicationAwareTokenAllocator.java
index 054a90e..87dba59 100644
--- a/src/java/org/apache/cassandra/dht/tokenallocator/ReplicationAwareTokenAllocator.java
+++ b/src/java/org/apache/cassandra/dht/tokenallocator/ReplicationAwareTokenAllocator.java
@@ -36,23 +36,23 @@
  * ownership needs to be evenly distributed. At the moment only nodes as a whole are treated as units, but that
  * will change with the introduction of token ranges per disk.
  */
-class ReplicationAwareTokenAllocator<Unit> implements TokenAllocator<Unit>
+class ReplicationAwareTokenAllocator<Unit> extends TokenAllocatorBase<Unit>
 {
-    final NavigableMap<Token, Unit> sortedTokens;
     final Multimap<Unit, Token> unitToTokens;
-    final ReplicationStrategy<Unit> strategy;
-    final IPartitioner partitioner;
     final int replicas;
 
     ReplicationAwareTokenAllocator(NavigableMap<Token, Unit> sortedTokens, ReplicationStrategy<Unit> strategy, IPartitioner partitioner)
     {
-        this.sortedTokens = sortedTokens;
+        super(sortedTokens, strategy, partitioner);
         unitToTokens = HashMultimap.create();
         for (Map.Entry<Token, Unit> en : sortedTokens.entrySet())
             unitToTokens.put(en.getValue(), en.getKey());
-        this.strategy = strategy;
         this.replicas = strategy.replicas();
-        this.partitioner = partitioner;
+    }
+
+    public int getReplicas()
+    {
+        return replicas;
     }
 
     public Collection<Token> addUnit(Unit newUnit, int numTokens)
@@ -151,19 +151,6 @@
         return tokens;
     }
 
-    private Map<Unit, UnitInfo<Unit>> createUnitInfos(Map<Object, GroupInfo> groups)
-    {
-        Map<Unit, UnitInfo<Unit>> map = Maps.newHashMap();
-        for (Unit n : sortedTokens.values())
-        {
-            UnitInfo<Unit> ni = map.get(n);
-            if (ni == null)
-                map.put(n, ni = new UnitInfo<>(n, 0, groups, strategy));
-            ni.tokenCount++;
-        }
-        return map;
-    }
-
     /**
      * Construct the token ring as a CircularList of TokenInfo,
      * and populate the ownership of the UnitInfo's provided
@@ -506,19 +493,6 @@
         }
     }
 
-    private Map.Entry<Token, Unit> mapEntryFor(Token t)
-    {
-        Map.Entry<Token, Unit> en = sortedTokens.floorEntry(t);
-        if (en == null)
-            en = sortedTokens.lastEntry();
-        return en;
-    }
-
-    Unit unitFor(Token t)
-    {
-        return mapEntryFor(t).getValue();
-    }
-
     private double optimalTokenOwnership(int tokensToAdd)
     {
         return 1.0 * replicas / (sortedTokens.size() + tokensToAdd);
@@ -554,7 +528,7 @@
         sortedTokens.keySet().removeAll(tokens);
     }
 
-    int unitCount()
+    public int unitCount()
     {
         return unitToTokens.asMap().size();
     }
@@ -564,189 +538,6 @@
         return getClass().getSimpleName();
     }
 
-    // get or initialise the shared GroupInfo associated with the unit
-    private static <Unit> GroupInfo getGroup(Unit unit, Map<Object, GroupInfo> groupMap, ReplicationStrategy<Unit> strategy)
-    {
-        Object groupClass = strategy.getGroup(unit);
-        GroupInfo group = groupMap.get(groupClass);
-        if (group == null)
-            groupMap.put(groupClass, group = new GroupInfo(groupClass));
-        return group;
-    }
-
-    /**
-     * Unique group object that one or more UnitInfo objects link to.
-     */
-    private static class GroupInfo
-    {
-        /**
-         * Group identifier given by ReplicationStrategy.getGroup(Unit).
-         */
-        final Object group;
-
-        /**
-         * Seen marker. When non-null, the group is already seen in replication walks.
-         * Also points to previous seen group to enable walking the seen groups and clearing the seen markers.
-         */
-        GroupInfo prevSeen = null;
-        /**
-         * Same marker/chain used by populateTokenInfo.
-         */
-        GroupInfo prevPopulate = null;
-
-        /**
-         * Value used as terminator for seen chains.
-         */
-        static GroupInfo TERMINATOR = new GroupInfo(null);
-
-        public GroupInfo(Object group)
-        {
-            this.group = group;
-        }
-
-        public String toString()
-        {
-            return group.toString() + (prevSeen != null ? "*" : "");
-        }
-    }
-
-    /**
-     * Unit information created and used by ReplicationAwareTokenDistributor. Contained vnodes all point to the same
-     * instance.
-     */
-    static class UnitInfo<Unit>
-    {
-        final Unit unit;
-        final GroupInfo group;
-        double ownership;
-        int tokenCount;
-
-        /**
-         * During evaluateImprovement this is used to form a chain of units affected by the candidate insertion.
-         */
-        UnitInfo<Unit> prevUsed;
-        /**
-         * During evaluateImprovement this holds the ownership after the candidate insertion.
-         */
-        double adjustedOwnership;
-
-        private UnitInfo(Unit unit, GroupInfo group)
-        {
-            this.unit = unit;
-            this.group = group;
-            this.tokenCount = 0;
-        }
-
-        public UnitInfo(Unit unit, double ownership, Map<Object, GroupInfo> groupMap, ReplicationStrategy<Unit> strategy)
-        {
-            this(unit, getGroup(unit, groupMap, strategy));
-            this.ownership = ownership;
-        }
-
-        public String toString()
-        {
-            return String.format("%s%s(%.2e)%s",
-                    unit, unit == group.group ? (group.prevSeen != null ? "*" : "") : ":" + group.toString(),
-                    ownership, prevUsed != null ? (prevUsed == this ? "#" : "->" + prevUsed.toString()) : "");
-        }
-    }
-
-    private static class CircularList<T extends CircularList<T>>
-    {
-        T prev;
-        T next;
-
-        /**
-         * Inserts this after unit in the circular list which starts at head. Returns the new head of the list, which
-         * only changes if head was null.
-         */
-        @SuppressWarnings("unchecked")
-        T insertAfter(T head, T unit)
-        {
-            if (head == null)
-            {
-                return prev = next = (T) this;
-            }
-            assert unit != null;
-            assert unit.next != null;
-            prev = unit;
-            next = unit.next;
-            prev.next = (T) this;
-            next.prev = (T) this;
-            return head;
-        }
-
-        /**
-         * Removes this from the list that starts at head. Returns the new head of the list, which only changes if the
-         * head was removed.
-         */
-        T removeFrom(T head)
-        {
-            next.prev = prev;
-            prev.next = next;
-            return this == head ? (this == next ? null : next) : head;
-        }
-    }
-
-    private static class BaseTokenInfo<Unit, T extends BaseTokenInfo<Unit, T>> extends CircularList<T>
-    {
-        final Token token;
-        final UnitInfo<Unit> owningUnit;
-
-        /**
-         * Start of the replication span for the vnode, i.e. the first token of the RF'th group seen before the token.
-         * The replicated ownership of the unit is the range between {@code replicationStart} and {@code token}.
-         */
-        Token replicationStart;
-        /**
-         * The closest position that the new candidate can take to become the new replication start. If candidate is
-         * closer, the start moves to this position. Used to determine replicationStart after insertion of new token.
-         *
-         * Usually the RF minus one boundary, i.e. the first token of the RF-1'th group seen before the token.
-         */
-        Token replicationThreshold;
-        /**
-         * Current replicated ownership. This number is reflected in the owning unit's ownership.
-         */
-        double replicatedOwnership = 0;
-
-        public BaseTokenInfo(Token token, UnitInfo<Unit> owningUnit)
-        {
-            this.token = token;
-            this.owningUnit = owningUnit;
-        }
-
-        public String toString()
-        {
-            return String.format("%s(%s)", token, owningUnit);
-        }
-
-        /**
-         * Previous unit in the token ring. For existing tokens this is prev,
-         * for candidates it's "split".
-         */
-        TokenInfo<Unit> prevInRing()
-        {
-            return null;
-        }
-    }
-
-    /**
-     * TokenInfo about existing tokens/vnodes.
-     */
-    private static class TokenInfo<Unit> extends BaseTokenInfo<Unit, TokenInfo<Unit>>
-    {
-        public TokenInfo(Token token, UnitInfo<Unit> owningUnit)
-        {
-            super(token, owningUnit);
-        }
-
-        TokenInfo<Unit> prevInRing()
-        {
-            return prev;
-        }
-    }
-
     /**
      * TokenInfo about candidate new tokens/vnodes.
      */
@@ -772,34 +563,9 @@
         BaseTokenInfo<?, ?> token = tokens;
         do
         {
-            System.out.format("%s%s: rs %s rt %s size %.2e\n", lead, token, token.replicationStart, token.replicationThreshold, token.replicatedOwnership);
+            System.out.format("%s%s: rs %s rt %s size %.2e%n", lead, token, token.replicationStart, token.replicationThreshold, token.replicatedOwnership);
             token = token.next;
         } while (token != null && token != tokens);
     }
-
-    static class Weighted<T> implements Comparable<Weighted<T>>
-    {
-        final double weight;
-        final T value;
-
-        public Weighted(double weight, T value)
-        {
-            this.weight = weight;
-            this.value = value;
-        }
-
-        @Override
-        public int compareTo(Weighted<T> o)
-        {
-            int cmp = Double.compare(o.weight, this.weight);
-            return cmp;
-        }
-
-        @Override
-        public String toString()
-        {
-            return String.format("%s<%s>", value, weight);
-        }
-    }
 }
 
diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java
index 5501378..8a3ede7 100644
--- a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java
+++ b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java
@@ -35,12 +35,14 @@
 
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.gms.Gossiper;
 import org.apache.cassandra.locator.AbstractReplicationStrategy;
 import org.apache.cassandra.locator.IEndpointSnitch;
 import org.apache.cassandra.locator.NetworkTopologyStrategy;
 import org.apache.cassandra.locator.SimpleStrategy;
 import org.apache.cassandra.locator.TokenMetadata;
 import org.apache.cassandra.locator.TokenMetadata.Topology;
+import org.apache.cassandra.utils.FBUtilities;
 
 public class TokenAllocation
 {
@@ -62,8 +64,8 @@
             SummaryStatistics os = replicatedOwnershipStats(tokenMetadataCopy, rs, endpoint);
             tokenMetadataCopy.updateNormalTokens(tokens, endpoint);
             SummaryStatistics ns = replicatedOwnershipStats(tokenMetadataCopy, rs, endpoint);
-            logger.warn("Replicated node load in datacentre before allocation " + statToString(os));
-            logger.warn("Replicated node load in datacentre after allocation " + statToString(ns));
+            logger.warn("Replicated node load in datacentre before allocation {}", statToString(os));
+            logger.warn("Replicated node load in datacentre after allocation {}", statToString(ns));
 
             // TODO: Is it worth doing the replicated ownership calculation always to be able to raise this alarm?
             if (ns.getStandardDeviation() > os.getStandardDeviation())
@@ -147,7 +149,7 @@
             if (strategy.inAllocationRing(en.getValue()))
                 sortedTokens.put(en.getKey(), en.getValue());
         }
-        return new ReplicationAwareTokenAllocator<>(sortedTokens, strategy, tokenMetadata.partitioner);
+        return TokenAllocatorFactory.createTokenAllocator(sortedTokens, strategy, tokenMetadata.partitioner);
     }
 
     interface StrategyAdapter extends ReplicationStrategy<InetAddress>
@@ -198,6 +200,31 @@
         final String dc = snitch.getDatacenter(endpoint);
         final int replicas = rs.getReplicationFactor(dc);
 
+        if (replicas == 0 || replicas == 1)
+        {
+            // No replication, each node is treated as separate.
+            return new StrategyAdapter()
+            {
+                @Override
+                public int replicas()
+                {
+                    return 1;
+                }
+
+                @Override
+                public Object getGroup(InetAddress unit)
+                {
+                    return unit;
+                }
+
+                @Override
+                public boolean inAllocationRing(InetAddress other)
+                {
+                    return dc.equals(snitch.getDatacenter(other));
+                }
+            };
+        }
+
         Topology topology = tokenMetadata.getTopology();
 
         // if topology hasn't been setup yet for this endpoint+rack then treat it as a separate unit
diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocator.java b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocator.java
index 580f2ec..2eb9a4c 100644
--- a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocator.java
+++ b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocator.java
@@ -23,5 +23,5 @@
 
 public interface TokenAllocator<Unit>
 {
-    public Collection<Token> addUnit(Unit newUnit, int numTokens);
+    Collection<Token> addUnit(Unit newUnit, int numTokens);
 }
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorBase.java b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorBase.java
new file mode 100644
index 0000000..f59bfd4
--- /dev/null
+++ b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorBase.java
@@ -0,0 +1,279 @@
+/*
+ * 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.dht.tokenallocator;
+
+import java.util.Map;
+import java.util.NavigableMap;
+
+import com.google.common.collect.Maps;
+
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Token;
+
+public abstract class TokenAllocatorBase<Unit> implements TokenAllocator<Unit>
+{
+    final NavigableMap<Token, Unit> sortedTokens;
+    final ReplicationStrategy<Unit> strategy;
+    final IPartitioner partitioner;
+
+    protected TokenAllocatorBase(NavigableMap<Token, Unit> sortedTokens,
+                             ReplicationStrategy<Unit> strategy,
+                             IPartitioner partitioner)
+    {
+        this.sortedTokens = sortedTokens;
+        this.strategy = strategy;
+        this.partitioner = partitioner;
+    }
+
+    public abstract int getReplicas();
+
+    protected Map<Unit, UnitInfo<Unit>> createUnitInfos(Map<Object, GroupInfo> groups)
+    {
+        Map<Unit, UnitInfo<Unit>> map = Maps.newHashMap();
+        for (Unit n : sortedTokens.values())
+        {
+            UnitInfo<Unit> ni = map.get(n);
+            if (ni == null)
+                map.put(n, ni = new UnitInfo<>(n, 0, groups, strategy));
+            ni.tokenCount++;
+        }
+        return map;
+    }
+
+    private Map.Entry<Token, Unit> mapEntryFor(Token t)
+    {
+        Map.Entry<Token, Unit> en = sortedTokens.floorEntry(t);
+        if (en == null)
+            en = sortedTokens.lastEntry();
+        return en;
+    }
+
+    Unit unitFor(Token t)
+    {
+        return mapEntryFor(t).getValue();
+    }
+
+    // get or initialise the shared GroupInfo associated with the unit
+    private static <Unit> GroupInfo getGroup(Unit unit, Map<Object, GroupInfo> groupMap, ReplicationStrategy<Unit> strategy)
+    {
+        Object groupClass = strategy.getGroup(unit);
+        GroupInfo group = groupMap.get(groupClass);
+        if (group == null)
+            groupMap.put(groupClass, group = new GroupInfo(groupClass));
+        return group;
+    }
+
+    /**
+     * Unique group object that one or more UnitInfo objects link to.
+     */
+    static class GroupInfo
+    {
+        /**
+         * Group identifier given by ReplicationStrategy.getGroup(Unit).
+         */
+        final Object group;
+
+        /**
+         * Seen marker. When non-null, the group is already seen in replication walks.
+         * Also points to previous seen group to enable walking the seen groups and clearing the seen markers.
+         */
+        GroupInfo prevSeen = null;
+        /**
+         * Same marker/chain used by populateTokenInfo.
+         */
+        GroupInfo prevPopulate = null;
+
+        /**
+         * Value used as terminator for seen chains.
+         */
+        static GroupInfo TERMINATOR = new GroupInfo(null);
+
+        public GroupInfo(Object group)
+        {
+            this.group = group;
+        }
+
+        public String toString()
+        {
+            return group.toString() + (prevSeen != null ? "*" : "");
+        }
+    }
+
+    /**
+     * Unit information created and used by ReplicationAwareTokenDistributor. Contained vnodes all point to the same
+     * instance.
+     */
+    static class UnitInfo<Unit>
+    {
+        final Unit unit;
+        final GroupInfo group;
+        double ownership;
+        int tokenCount;
+
+        /**
+         * During evaluateImprovement this is used to form a chain of units affected by the candidate insertion.
+         */
+        UnitInfo<Unit> prevUsed;
+        /**
+         * During evaluateImprovement this holds the ownership after the candidate insertion.
+         */
+        double adjustedOwnership;
+
+        private UnitInfo(Unit unit, GroupInfo group)
+        {
+            this.unit = unit;
+            this.group = group;
+            this.tokenCount = 0;
+        }
+
+        public UnitInfo(Unit unit, double ownership, Map<Object, GroupInfo> groupMap, ReplicationStrategy<Unit> strategy)
+        {
+            this(unit, getGroup(unit, groupMap, strategy));
+            this.ownership = ownership;
+        }
+
+        public String toString()
+        {
+            return String.format("%s%s(%.2e)%s",
+                                 unit, unit == group.group ? (group.prevSeen != null ? "*" : "") : ":" + group.toString(),
+                                 ownership, prevUsed != null ? (prevUsed == this ? "#" : "->" + prevUsed.toString()) : "");
+        }
+    }
+
+    private static class CircularList<T extends CircularList<T>>
+    {
+        T prev;
+        T next;
+
+        /**
+         * Inserts this after unit in the circular list which starts at head. Returns the new head of the list, which
+         * only changes if head was null.
+         */
+        @SuppressWarnings("unchecked")
+        T insertAfter(T head, T unit)
+        {
+            if (head == null)
+            {
+                return prev = next = (T) this;
+            }
+            assert unit != null;
+            assert unit.next != null;
+            prev = unit;
+            next = unit.next;
+            prev.next = (T) this;
+            next.prev = (T) this;
+            return head;
+        }
+
+        /**
+         * Removes this from the list that starts at head. Returns the new head of the list, which only changes if the
+         * head was removed.
+         */
+        T removeFrom(T head)
+        {
+            next.prev = prev;
+            prev.next = next;
+            return this == head ? (this == next ? null : next) : head;
+        }
+    }
+
+    static class BaseTokenInfo<Unit, T extends BaseTokenInfo<Unit, T>> extends CircularList<T>
+    {
+        final Token token;
+        final UnitInfo<Unit> owningUnit;
+
+        /**
+         * Start of the replication span for the vnode, i.e. the first token of the RF'th group seen before the token.
+         * The replicated ownership of the unit is the range between {@code replicationStart} and {@code token}.
+         */
+        Token replicationStart;
+        /**
+         * The closest position that the new candidate can take to become the new replication start. If candidate is
+         * closer, the start moves to this position. Used to determine replicationStart after insertion of new token.
+         *
+         * Usually the RF minus one boundary, i.e. the first token of the RF-1'th group seen before the token.
+         */
+        Token replicationThreshold;
+        /**
+         * Current replicated ownership. This number is reflected in the owning unit's ownership.
+         */
+        double replicatedOwnership = 0;
+
+        public BaseTokenInfo(Token token, UnitInfo<Unit> owningUnit)
+        {
+            this.token = token;
+            this.owningUnit = owningUnit;
+        }
+
+        public String toString()
+        {
+            return String.format("%s(%s)", token, owningUnit);
+        }
+
+        /**
+         * Previous unit in the token ring. For existing tokens this is prev,
+         * for candidates it's "split".
+         */
+        TokenInfo<Unit> prevInRing()
+        {
+            return null;
+        }
+    }
+
+    /**
+     * TokenInfo about existing tokens/vnodes.
+     */
+    static class TokenInfo<Unit> extends BaseTokenInfo<Unit, TokenInfo<Unit>>
+    {
+        public TokenInfo(Token token, UnitInfo<Unit> owningUnit)
+        {
+            super(token, owningUnit);
+        }
+
+        TokenInfo<Unit> prevInRing()
+        {
+            return prev;
+        }
+    }
+
+    static class Weighted<T> implements Comparable<Weighted<T>>
+    {
+        final double weight;
+        final T value;
+
+        public Weighted(double weight, T value)
+        {
+            this.weight = weight;
+            this.value = value;
+        }
+
+        @Override
+        public int compareTo(Weighted<T> o)
+        {
+            int cmp = Double.compare(o.weight, this.weight);
+            return cmp;
+        }
+
+        @Override
+        public String toString()
+        {
+            return String.format("%s<%s>", value, weight);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorFactory.java b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorFactory.java
new file mode 100644
index 0000000..58acb56
--- /dev/null
+++ b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorFactory.java
@@ -0,0 +1,45 @@
+/*
+ * 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.dht.tokenallocator;
+
+import java.net.InetAddress;
+import java.util.NavigableMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Token;
+
+public class TokenAllocatorFactory
+{
+    private static final Logger logger = LoggerFactory.getLogger(TokenAllocatorFactory.class);
+    public static TokenAllocator<InetAddress> createTokenAllocator(NavigableMap<Token, InetAddress> sortedTokens,
+                                                     ReplicationStrategy<InetAddress> strategy,
+                                                     IPartitioner partitioner)
+    {
+        if(strategy.replicas() == 1)
+        {
+            logger.info("Using NoReplicationTokenAllocator.");
+            return new NoReplicationTokenAllocator<>(sortedTokens, strategy, partitioner);
+        }
+        logger.info("Using ReplicationAwareTokenAllocator.");
+        return new ReplicationAwareTokenAllocator<>(sortedTokens, strategy, partitioner);
+    }
+}
diff --git a/src/java/org/apache/cassandra/exceptions/ReadFailureException.java b/src/java/org/apache/cassandra/exceptions/ReadFailureException.java
index 91cf580..82885e3 100644
--- a/src/java/org/apache/cassandra/exceptions/ReadFailureException.java
+++ b/src/java/org/apache/cassandra/exceptions/ReadFailureException.java
@@ -17,15 +17,18 @@
  */
 package org.apache.cassandra.exceptions;
 
+import java.net.InetAddress;
+import java.util.Map;
+
 import org.apache.cassandra.db.ConsistencyLevel;
 
 public class ReadFailureException extends RequestFailureException
 {
     public final boolean dataPresent;
 
-    public ReadFailureException(ConsistencyLevel consistency, int received, int failures, int blockFor, boolean dataPresent)
+    public ReadFailureException(ConsistencyLevel consistency, int received, int blockFor, boolean dataPresent, Map<InetAddress, RequestFailureReason> failureReasonByEndpoint)
     {
-        super(ExceptionCode.READ_FAILURE, consistency, received, failures, blockFor);
+        super(ExceptionCode.READ_FAILURE, consistency, received, blockFor, failureReasonByEndpoint);
         this.dataPresent = dataPresent;
     }
 }
diff --git a/src/java/org/apache/cassandra/exceptions/RequestFailureException.java b/src/java/org/apache/cassandra/exceptions/RequestFailureException.java
index 6b8b40f..1a5289c 100644
--- a/src/java/org/apache/cassandra/exceptions/RequestFailureException.java
+++ b/src/java/org/apache/cassandra/exceptions/RequestFailureException.java
@@ -17,21 +17,32 @@
  */
 package org.apache.cassandra.exceptions;
 
+import java.net.InetAddress;
+import java.util.HashMap;
+import java.util.Map;
+
 import org.apache.cassandra.db.ConsistencyLevel;
 
 public class RequestFailureException extends RequestExecutionException
 {
     public final ConsistencyLevel consistency;
     public final int received;
-    public final int failures;
     public final int blockFor;
+    public final Map<InetAddress, RequestFailureReason> failureReasonByEndpoint;
 
-    protected RequestFailureException(ExceptionCode code, ConsistencyLevel consistency, int received, int failures, int blockFor)
+    protected RequestFailureException(ExceptionCode code, ConsistencyLevel consistency, int received, int blockFor, Map<InetAddress, RequestFailureReason> failureReasonByEndpoint)
     {
-        super(code, String.format("Operation failed - received %d responses and %d failures", received, failures));
+        super(code, String.format("Operation failed - received %d responses and %d failures", received, failureReasonByEndpoint.size()));
         this.consistency = consistency;
         this.received = received;
-        this.failures = failures;
         this.blockFor = blockFor;
+
+        // It is possible for the passed in failureReasonByEndpoint map
+        // to have new entries added after this exception is constructed
+        // (e.g. a delayed failure response from a replica). So to be safe
+        // we make a copy of the map at this point to ensure it will not be
+        // modified any further. Otherwise, there could be implications when
+        // we encode this map for transport.
+        this.failureReasonByEndpoint = new HashMap<>(failureReasonByEndpoint);
     }
 }
diff --git a/src/java/org/apache/cassandra/exceptions/RequestFailureReason.java b/src/java/org/apache/cassandra/exceptions/RequestFailureReason.java
new file mode 100644
index 0000000..96ab7b5
--- /dev/null
+++ b/src/java/org/apache/cassandra/exceptions/RequestFailureReason.java
@@ -0,0 +1,51 @@
+/*
+ * 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.exceptions;
+
+public enum RequestFailureReason
+{
+    /**
+     * The reason for the failure was none of the below reasons or was not recorded by the data node.
+     */
+    UNKNOWN                  (0x0000),
+
+    /**
+     * The data node read too many tombstones when attempting to execute a read query (see tombstone_failure_threshold).
+     */
+    READ_TOO_MANY_TOMBSTONES (0x0001);
+
+    /** The code to be serialized as an unsigned 16 bit integer */
+    public final int code;
+    public static final RequestFailureReason[] VALUES = values();
+
+    RequestFailureReason(final int code)
+    {
+        this.code = code;
+    }
+
+    public static RequestFailureReason fromCode(final int code)
+    {
+        for (RequestFailureReason reasonCode : VALUES)
+        {
+            if (reasonCode.code == code)
+                return reasonCode;
+        }
+        throw new IllegalArgumentException("Unknown request failure reason error code: " + code);
+    }
+}
diff --git a/src/java/org/apache/cassandra/exceptions/StartupException.java b/src/java/org/apache/cassandra/exceptions/StartupException.java
index ec4890f..1513cf9 100644
--- a/src/java/org/apache/cassandra/exceptions/StartupException.java
+++ b/src/java/org/apache/cassandra/exceptions/StartupException.java
@@ -23,6 +23,10 @@
  */
 public class StartupException extends Exception
 {
+    public final static int ERR_WRONG_MACHINE_STATE = 1;
+    public final static int ERR_WRONG_DISK_STATE = 3;
+    public final static int ERR_WRONG_CONFIG = 100;
+
     public final int returnCode;
 
     public StartupException(int returnCode, String message)
diff --git a/src/java/org/apache/cassandra/exceptions/UnrecognizedEntityException.java b/src/java/org/apache/cassandra/exceptions/UnrecognizedEntityException.java
deleted file mode 100644
index e8392e9..0000000
--- a/src/java/org/apache/cassandra/exceptions/UnrecognizedEntityException.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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.exceptions;
-
-import org.apache.cassandra.cql3.ColumnIdentifier;
-import org.apache.cassandra.cql3.Relation;
-
-/**
- * Exception thrown when an entity is not recognized within a relation.
- */
-public final class UnrecognizedEntityException extends InvalidRequestException
-{
-    /**
-     * The unrecognized entity.
-     */
-    public final ColumnIdentifier entity;
-
-    /**
-     * The entity relation.
-     */
-    public final Relation relation;
-
-    /**
-     * Creates a new <code>UnrecognizedEntityException</code>.
-     * @param entity the unrecognized entity
-     * @param relation the entity relation
-     */
-    public UnrecognizedEntityException(ColumnIdentifier entity, Relation relation)
-    {
-        super(String.format("Undefined name %s in where clause ('%s')", entity, relation));
-        this.entity = entity;
-        this.relation = relation;
-    }
-}
diff --git a/src/java/org/apache/cassandra/exceptions/WriteFailureException.java b/src/java/org/apache/cassandra/exceptions/WriteFailureException.java
index 24de9b1..1a857fe 100644
--- a/src/java/org/apache/cassandra/exceptions/WriteFailureException.java
+++ b/src/java/org/apache/cassandra/exceptions/WriteFailureException.java
@@ -17,6 +17,9 @@
  */
 package org.apache.cassandra.exceptions;
 
+import java.net.InetAddress;
+import java.util.Map;
+
 import org.apache.cassandra.db.ConsistencyLevel;
 import org.apache.cassandra.db.WriteType;
 
@@ -24,9 +27,9 @@
 {
     public final WriteType writeType;
 
-    public WriteFailureException(ConsistencyLevel consistency, int received, int failures, int blockFor, WriteType writeType)
+    public WriteFailureException(ConsistencyLevel consistency, int received, int blockFor, WriteType writeType, Map<InetAddress, RequestFailureReason> failureReasonByEndpoint)
     {
-        super(ExceptionCode.WRITE_FAILURE, consistency, received, failures, blockFor);
+        super(ExceptionCode.WRITE_FAILURE, consistency, received, blockFor, failureReasonByEndpoint);
         this.writeType = writeType;
     }
 }
diff --git a/src/java/org/apache/cassandra/gms/EchoMessage.java b/src/java/org/apache/cassandra/gms/EchoMessage.java
index 339750d..2fee889 100644
--- a/src/java/org/apache/cassandra/gms/EchoMessage.java
+++ b/src/java/org/apache/cassandra/gms/EchoMessage.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.gms;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
@@ -30,13 +30,13 @@
 public final class EchoMessage
 {
 	public static final EchoMessage instance = new EchoMessage();
-	
+
     public static final IVersionedSerializer<EchoMessage> serializer = new EchoMessageSerializer();
 
 	private EchoMessage()
 	{
 	}
-	
+
     public static class EchoMessageSerializer implements IVersionedSerializer<EchoMessage>
     {
         public void serialize(EchoMessage t, DataOutputPlus out, int version) throws IOException
diff --git a/src/java/org/apache/cassandra/gms/EndpointState.java b/src/java/org/apache/cassandra/gms/EndpointState.java
index 70f2a68..b587635 100644
--- a/src/java/org/apache/cassandra/gms/EndpointState.java
+++ b/src/java/org/apache/cassandra/gms/EndpointState.java
@@ -22,14 +22,19 @@
 import java.util.EnumMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.atomic.AtomicReference;
 
+import javax.annotation.Nullable;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.apache.cassandra.db.TypeSizes;
 import org.apache.cassandra.io.IVersionedSerializer;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.utils.CassandraVersion;
+
 /**
  * This abstraction represents both the HeartBeatState and the ApplicationState in an EndpointState
  * instance. Any state for a given endpoint can be retrieved from this instance.
@@ -78,6 +83,11 @@
         return applicationState.get().get(key);
     }
 
+    public boolean containsApplicationState(ApplicationState key)
+    {
+        return applicationState.get().containsKey(key);
+    }
+
     public Set<Map.Entry<ApplicationState, VersionedValue>> states()
     {
         return applicationState.get().entrySet();
@@ -154,6 +164,24 @@
         return pieces[0];
     }
 
+    @Nullable
+    public UUID getSchemaVersion()
+    {
+        VersionedValue applicationState = getApplicationState(ApplicationState.SCHEMA);
+        return applicationState != null
+               ? UUID.fromString(applicationState.value)
+               : null;
+    }
+
+    @Nullable
+    public CassandraVersion getReleaseVersion()
+    {
+        VersionedValue applicationState = getApplicationState(ApplicationState.RELEASE_VERSION);
+        return applicationState != null
+               ? new CassandraVersion(applicationState.value)
+               : null;
+    }
+
     public String toString()
     {
         return "EndpointState: HeartBeatState = " + hbState + ", AppStateMap = " + applicationState.get();
diff --git a/src/java/org/apache/cassandra/gms/FailureDetector.java b/src/java/org/apache/cassandra/gms/FailureDetector.java
index 69888a6..d8e1324 100644
--- a/src/java/org/apache/cassandra/gms/FailureDetector.java
+++ b/src/java/org/apache/cassandra/gms/FailureDetector.java
@@ -237,7 +237,7 @@
         // it's worth being defensive here so minor bugs don't cause disproportionate
         // badness.  (See CASSANDRA-1463 for an example).
         if (epState == null)
-            logger.error("unknown endpoint {}", ep);
+            logger.error("Unknown endpoint: " + ep, new IllegalArgumentException(""));
         return epState != null && epState.isAlive();
     }
 
@@ -341,7 +341,7 @@
         for (InetAddress ep : eps)
         {
             ArrivalWindow hWnd = arrivalSamples.get(ep);
-            sb.append(ep + " : ");
+            sb.append(ep).append(" : ");
             sb.append(hWnd);
             sb.append(System.getProperty("line.separator"));
         }
diff --git a/src/java/org/apache/cassandra/gms/GossipDigestAckVerbHandler.java b/src/java/org/apache/cassandra/gms/GossipDigestAckVerbHandler.java
index 59060f8..d6d9dfb 100644
--- a/src/java/org/apache/cassandra/gms/GossipDigestAckVerbHandler.java
+++ b/src/java/org/apache/cassandra/gms/GossipDigestAckVerbHandler.java
@@ -54,8 +54,10 @@
         if (Gossiper.instance.isInShadowRound())
         {
             if (logger.isDebugEnabled())
-                logger.debug("Finishing shadow round with {}", from);
-            Gossiper.instance.finishShadowRound(epStateMap);
+                logger.debug("Received an ack from {}, which may trigger exit from shadow round", from);
+
+            // if the ack is completely empty, then we can infer that the respondent is also in a shadow round
+            Gossiper.instance.maybeFinishShadowRound(from, gDigestList.isEmpty() && epStateMap.isEmpty(), epStateMap);
             return; // don't bother doing anything else, we have what we came for
         }
 
diff --git a/src/java/org/apache/cassandra/gms/GossipDigestSynVerbHandler.java b/src/java/org/apache/cassandra/gms/GossipDigestSynVerbHandler.java
index 1c67570..6d0afa2 100644
--- a/src/java/org/apache/cassandra/gms/GossipDigestSynVerbHandler.java
+++ b/src/java/org/apache/cassandra/gms/GossipDigestSynVerbHandler.java
@@ -38,7 +38,7 @@
         InetAddress from = message.from;
         if (logger.isTraceEnabled())
             logger.trace("Received a GossipDigestSynMessage from {}", from);
-        if (!Gossiper.instance.isEnabled())
+        if (!Gossiper.instance.isEnabled() && !Gossiper.instance.isInShadowRound())
         {
             if (logger.isTraceEnabled())
                 logger.trace("Ignoring GossipDigestSynMessage because gossip is disabled");
@@ -60,6 +60,32 @@
         }
 
         List<GossipDigest> gDigestList = gDigestMessage.getGossipDigests();
+
+        // if the syn comes from a peer performing a shadow round and this node is
+        // also currently in a shadow round, send back a minimal ack. This node must
+        // be in the sender's seed list and doing this allows the sender to
+        // differentiate between seeds from which it is partitioned and those which
+        // are in their shadow round
+        if (!Gossiper.instance.isEnabled() && Gossiper.instance.isInShadowRound())
+        {
+            // a genuine syn (as opposed to one from a node currently
+            // doing a shadow round) will always contain > 0 digests
+            if (gDigestList.size() > 0)
+            {
+                logger.debug("Ignoring non-empty GossipDigestSynMessage because currently in gossip shadow round");
+                return;
+            }
+
+            logger.debug("Received a shadow round syn from {}. Gossip is disabled but " +
+                         "currently also in shadow round, responding with a minimal ack", from);
+            MessagingService.instance()
+                            .sendOneWay(new MessageOut<>(MessagingService.Verb.GOSSIP_DIGEST_ACK,
+                                                         new GossipDigestAck(new ArrayList<>(), new HashMap<>()),
+                                                         GossipDigestAck.serializer),
+                                        from);
+            return;
+        }
+
         if (logger.isTraceEnabled())
         {
             StringBuilder sb = new StringBuilder();
diff --git a/src/java/org/apache/cassandra/gms/Gossiper.java b/src/java/org/apache/cassandra/gms/Gossiper.java
index 3cfb1b5..613b566 100644
--- a/src/java/org/apache/cassandra/gms/Gossiper.java
+++ b/src/java/org/apache/cassandra/gms/Gossiper.java
@@ -23,6 +23,7 @@
 import java.util.Map.Entry;
 import java.util.concurrent.*;
 import java.util.concurrent.locks.ReentrantLock;
+import javax.annotation.Nullable;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -38,8 +39,6 @@
 import com.google.common.util.concurrent.Uninterruptibles;
 
 import io.netty.util.concurrent.FastThreadLocal;
-import org.apache.cassandra.db.SystemKeyspace;
-import org.apache.cassandra.utils.CassandraVersion;
 import org.apache.cassandra.utils.ExecutorUtils;
 import org.apache.cassandra.utils.MBeanWrapper;
 import org.apache.cassandra.utils.NoSpamLogger;
@@ -52,12 +51,15 @@
 import org.apache.cassandra.concurrent.Stage;
 import org.apache.cassandra.concurrent.StageManager;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.net.IAsyncCallback;
 import org.apache.cassandra.net.MessageIn;
 import org.apache.cassandra.net.MessageOut;
 import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.utils.CassandraVersion;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.RecomputingSupplier;
@@ -94,7 +96,9 @@
     static final List<String> DEAD_STATES = Arrays.asList(VersionedValue.REMOVING_TOKEN, VersionedValue.REMOVED_TOKEN,
                                                           VersionedValue.STATUS_LEFT, VersionedValue.HIBERNATE);
     static ArrayList<String> SILENT_SHUTDOWN_STATES = new ArrayList<>();
-    static {
+
+    static
+    {
         SILENT_SHUTDOWN_STATES.addAll(DEAD_STATES);
         SILENT_SHUTDOWN_STATES.add(VersionedValue.STATUS_BOOTSTRAPPING);
         SILENT_SHUTDOWN_STATES.add(VersionedValue.STATUS_BOOTSTRAPPING_REPLACE);
@@ -135,7 +139,8 @@
     private final Map<InetAddress, Long> unreachableEndpoints = new ConcurrentHashMap<InetAddress, Long>();
 
     /* initial seeds for joining the cluster */
-    private final Set<InetAddress> seeds = new ConcurrentSkipListSet<InetAddress>(inetcomparator);
+    @VisibleForTesting
+    final Set<InetAddress> seeds = new ConcurrentSkipListSet<InetAddress>(inetcomparator);
 
     /* map where key is the endpoint and value is the state associated with the endpoint */
     final ConcurrentMap<InetAddress, EndpointState> endpointStateMap = new ConcurrentHashMap<InetAddress, EndpointState>();
@@ -148,7 +153,10 @@
 
     private final Map<InetAddress, Long> expireTimeEndpointMap = new ConcurrentHashMap<InetAddress, Long>();
 
+    private volatile boolean anyNodeOn30 = false; // we assume the regular case here - all nodes are on 3.11
     private volatile boolean inShadowRound = false;
+    // seeds gathered during shadow round that indicated to be in the shadow round phase as well
+    private final Set<InetAddress> seedsInShadowRound = new ConcurrentSkipListSet<>(inetcomparator);
     // endpoint states as gathered during shadow round
     private final Map<InetAddress, EndpointState> endpointShadowStateMap = new ConcurrentHashMap<>();
 
@@ -684,7 +692,6 @@
         InetAddress endpoint = InetAddress.getByName(address);
         runInGossipStageBlocking(() -> {
             EndpointState epState = endpointStateMap.get(endpoint);
-            Collection<Token> tokens = null;
             logger.warn("Assassinating {} via gossip", endpoint);
 
             if (epState == null)
@@ -709,6 +716,7 @@
                 epState.getHeartBeatState().forceNewerGenerationUnsafe();
             }
 
+            Collection<Token> tokens = null;
             try
             {
                 tokens = StorageService.instance.getTokenMetadata().getTokens(endpoint);
@@ -716,8 +724,10 @@
             catch (Throwable th)
             {
                 JVMStabilityInspector.inspectThrowable(th);
-                // TODO this is broken
-                logger.warn("Unable to calculate tokens for {}.  Will use a random one", address);
+            }
+            if (tokens == null || tokens.isEmpty())
+            {
+                logger.warn("Trying to assassinate an endpoint {} that does not have any tokens assigned. This should not have happened, trying to continue with a random token.", address);
                 tokens = Collections.singletonList(StorageService.instance.getTokenMetadata().partitioner.getRandomToken());
             }
 
@@ -748,7 +758,7 @@
     private boolean sendGossip(MessageOut<GossipDigestSyn> message, Set<InetAddress> epSet)
     {
         List<InetAddress> endpoints = ImmutableList.copyOf(epSet);
-        
+
         int size = endpoints.size();
         if (size < 1)
             return false;
@@ -825,16 +835,24 @@
     }
 
     /**
-     * Check if this endpoint can safely bootstrap into the cluster.
+     * Check if this node can safely be started and join the ring.
+     * If the node is bootstrapping, examines gossip state for any previous status to decide whether
+     * it's safe to allow this node to start and bootstrap. If not bootstrapping, compares the host ID
+     * that the node itself has (obtained by reading from system.local or generated if not present)
+     * with the host ID obtained from gossip for the endpoint address (if any). This latter case
+     * prevents a non-bootstrapping, new node from being started with the same address of a
+     * previously started, but currently down predecessor.
      *
      * @param endpoint - the endpoint to check
+     * @param localHostUUID - the host id to check
+     * @param isBootstrapping - whether the node intends to bootstrap when joining
      * @param epStates - endpoint states in the cluster
-     * @return true if the endpoint can join the cluster
+     * @return true if it is safe to start the node, false otherwise
      */
-    public boolean isSafeForBootstrap(InetAddress endpoint, Map<InetAddress, EndpointState> epStates)
+    public boolean isSafeForStartup(InetAddress endpoint, UUID localHostUUID, boolean isBootstrapping,
+                                    Map<InetAddress, EndpointState> epStates)
     {
         EndpointState epState = epStates.get(endpoint);
-
         // if there's no previous state, we're good
         if (epState == null)
             return true;
@@ -848,15 +866,29 @@
             return false;
         }
 
+        //the node was previously removed from the cluster
         if (isDeadState(epState))
             return true;
 
-        // these states are not allowed to join the cluster as it would not be safe
-        final List<String> unsafeStatuses = new ArrayList<String>() {{
-            add(""); // failed bootstrap but we did start gossiping
-            add(VersionedValue.STATUS_NORMAL); // node is legit in the cluster or it was stopped with kill -9
-            add(VersionedValue.SHUTDOWN); }}; // node was shutdown
-        return !unsafeStatuses.contains(status);
+        if (isBootstrapping)
+        {
+            // these states are not allowed to join the cluster as it would not be safe
+            final List<String> unsafeStatuses = new ArrayList<String>()
+            {{
+                add("");                           // failed bootstrap but we did start gossiping
+                add(VersionedValue.STATUS_NORMAL); // node is legit in the cluster or it was stopped with kill -9
+                add(VersionedValue.SHUTDOWN);      // node was shutdown
+            }};
+            return !unsafeStatuses.contains(status);
+        }
+        else
+        {
+            // if the previous UUID matches what we currently have (i.e. what was read from
+            // system.local at startup), then we're good to start up. Otherwise, something
+            // is amiss and we need to replace the previous node
+            VersionedValue previous = epState.getApplicationState(ApplicationState.HOST_ID);
+            return UUID.fromString(previous.value).equals(localHostUUID);
+        }
     }
 
     @VisibleForTesting
@@ -945,20 +977,6 @@
         return endpointStateMap.get(ep);
     }
 
-    public boolean valuesEqual(InetAddress ep1, InetAddress ep2, ApplicationState as)
-    {
-        EndpointState state1 = getEndpointStateForEndpoint(ep1);
-        EndpointState state2 = getEndpointStateForEndpoint(ep2);
-
-        if (state1 == null || state2 == null)
-            return false;
-
-        VersionedValue value1 = state1.getApplicationState(as);
-        VersionedValue value2 = state2.getApplicationState(as);
-
-        return !(value1 == null || value2 == null) && value1.value.equals(value2.value);
-    }
-
     public Set<Entry<InetAddress, EndpointState>> getEndpointStates()
     {
         return endpointStateMap.entrySet();
@@ -1313,6 +1331,26 @@
                 handleMajorStateChange(ep, remoteState);
             }
         }
+
+        boolean any30 = anyEndpointOn30();
+        if (any30 != anyNodeOn30)
+        {
+            logger.info(any30
+                        ? "There is at least one 3.0 node in the cluster - will store and announce compatible schema version"
+                        : "There are no 3.0 nodes in the cluster - will store and announce real schema version");
+
+            anyNodeOn30 = any30;
+            executor.submit(Schema.instance::updateVersionAndAnnounce);
+        }
+    }
+
+    private boolean anyEndpointOn30()
+    {
+        return endpointStateMap.values()
+                               .stream()
+                               .map(EndpointState::getReleaseVersion)
+                               .filter(Objects::nonNull)
+                               .anyMatch(CassandraVersion::is30);
     }
 
     private void applyNewStates(InetAddress addr, EndpointState localState, EndpointState remoteState)
@@ -1345,7 +1383,7 @@
         for (Entry<ApplicationState, VersionedValue> updatedEntry : updatedStates)
             doOnChangeNotifications(addr, updatedEntry.getKey(), updatedEntry.getValue());
     }
-    
+
     // notify that a local application state is going to change (doesn't get triggered for remote changes)
     private void doBeforeChangeNotifications(InetAddress addr, EndpointState epState, ApplicationState apState, VersionedValue newValue)
     {
@@ -1483,27 +1521,42 @@
                                                               TimeUnit.MILLISECONDS);
     }
 
+    public synchronized Map<InetAddress, EndpointState> doShadowRound()
+    {
+        return doShadowRound(Collections.EMPTY_SET);
+    }
+
     /**
      * Do a single 'shadow' round of gossip by retrieving endpoint states that will be stored exclusively in the
      * map return value, instead of endpointStateMap.
      *
-     * Used when preparing to join the ring:
      * <ul>
      *     <li>when replacing a node, to get and assume its tokens</li>
      *     <li>when joining, to check that the local host id matches any previous id for the endpoint address</li>
      * </ul>
      *
      * Method is synchronized, as we use an in-progress flag to indicate that shadow round must be cleared
-     * again by calling {@link Gossiper#finishShadowRound(Map)}. This will update
+     * again by calling {@link Gossiper#maybeFinishShadowRound(InetAddress, boolean, Map)}. This will update
      * {@link Gossiper#endpointShadowStateMap} with received values, in order to return an immutable copy to the
      * caller of {@link Gossiper#doShadowRound()}. Therefor only a single shadow round execution is permitted at
      * the same time.
      *
+     * @param peers Additional peers to try gossiping with.
      * @return endpoint states gathered during shadow round or empty map
      */
-    public synchronized Map<InetAddress, EndpointState> doShadowRound()
+    public synchronized Map<InetAddress, EndpointState> doShadowRound(Set<InetAddress> peers)
     {
         buildSeedsList();
+        // it may be that the local address is the only entry in the seed + peers
+        // list in which case, attempting a shadow round is pointless
+        if (seeds.isEmpty() && peers.isEmpty())
+            return endpointShadowStateMap;
+
+        boolean isSeed = DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddress());
+        // We double RING_DELAY if we're not a seed to increase chance of successful startup during a full cluster bounce,
+        // giving the seeds a chance to startup before we fail the shadow round
+        int shadowRoundDelay =  isSeed ? StorageService.RING_DELAY : StorageService.RING_DELAY * 2;
+        seedsInShadowRound.clear();
         endpointShadowStateMap.clear();
         // send a completely empty syn
         List<GossipDigest> gDigests = new ArrayList<GossipDigest>();
@@ -1515,6 +1568,7 @@
                 GossipDigestSyn.serializer);
 
         inShadowRound = true;
+        boolean includePeers = false;
         int slept = 0;
         try
         {
@@ -1523,8 +1577,18 @@
                 if (slept % 5000 == 0)
                 { // CASSANDRA-8072, retry at the beginning and every 5 seconds
                     logger.trace("Sending shadow round GOSSIP DIGEST SYN to seeds {}", seeds);
+
                     for (InetAddress seed : seeds)
                         MessagingService.instance().sendOneWay(message, seed);
+
+                    // Send to any peers we already know about, but only if a seed didn't respond.
+                    if (includePeers)
+                    {
+                        logger.trace("Sending shadow round GOSSIP DIGEST SYN to known peers {}", peers);
+                        for (InetAddress peer : peers)
+                            MessagingService.instance().sendOneWay(message, peer);
+                    }
+                    includePeers = true;
                 }
 
                 Thread.sleep(1000);
@@ -1532,8 +1596,15 @@
                     break;
 
                 slept += 1000;
-                if (slept > StorageService.RING_DELAY)
-                    throw new RuntimeException("Unable to gossip with any seeds");
+                if (slept > shadowRoundDelay)
+                {
+                    // if we got here no peers could be gossiped to. If we're a seed that's OK, but otherwise we stop. See CASSANDRA-13851
+                    if (!isSeed)
+                        throw new RuntimeException("Unable to gossip with any peers");
+
+                    inShadowRound = false;
+                    break;
+                }
             }
         }
         catch (InterruptedException wtf)
@@ -1544,7 +1615,8 @@
         return ImmutableMap.copyOf(endpointShadowStateMap);
     }
 
-    private void buildSeedsList()
+    @VisibleForTesting
+    void buildSeedsList()
     {
         for (InetAddress seed : DatabaseDescriptor.getSeeds())
         {
@@ -1664,12 +1736,56 @@
         return (scheduledGossipTask != null) && (!scheduledGossipTask.isCancelled());
     }
 
-    protected void finishShadowRound(Map<InetAddress, EndpointState> epStateMap)
+    public boolean isAnyNodeOn30()
+    {
+        return anyNodeOn30;
+    }
+
+    public boolean sufficientForStartupSafetyCheck(Map<InetAddress, EndpointState> epStateMap)
+    {
+        // it is possible for a previously queued ack to be sent to us when we come back up in shadow
+        EndpointState localState = epStateMap.get(FBUtilities.getBroadcastAddress());
+        // return false if response doesn't contain state necessary for safety check
+        return localState == null || isDeadState(localState) || localState.containsApplicationState(ApplicationState.HOST_ID);
+    }
+
+    protected void maybeFinishShadowRound(InetAddress respondent, boolean isInShadowRound, Map<InetAddress, EndpointState> epStateMap)
     {
         if (inShadowRound)
         {
-            endpointShadowStateMap.putAll(epStateMap);
-            inShadowRound = false;
+            if (!isInShadowRound)
+            {
+                if (!sufficientForStartupSafetyCheck(epStateMap))
+                {
+                    logger.debug("Not exiting shadow round because received ACK with insufficient states {} -> {}",
+                                 FBUtilities.getBroadcastAddress(), epStateMap.get(FBUtilities.getBroadcastAddress()));
+                    return;
+                }
+
+                if (!seeds.contains(respondent))
+                    logger.warn("Received an ack from {}, who isn't a seed. Ensure your seed list includes a live node. Exiting shadow round",
+                                respondent);
+                logger.debug("Received a regular ack from {}, can now exit shadow round", respondent);
+                // respondent sent back a full ack, so we can exit our shadow round
+                endpointShadowStateMap.putAll(epStateMap);
+                inShadowRound = false;
+                seedsInShadowRound.clear();
+            }
+            else
+            {
+                // respondent indicates it too is in a shadow round, if all seeds
+                // are in this state then we can exit our shadow round. Otherwise,
+                // we keep retrying the SR until one responds with a full ACK or
+                // we learn that all seeds are in SR.
+                logger.debug("Received an ack from {} indicating it is also in shadow round", respondent);
+                seedsInShadowRound.add(respondent);
+                if (seedsInShadowRound.containsAll(seeds))
+                {
+                    logger.debug("All seeds are in a shadow round, clearing this node to exit its own");
+                    inShadowRound = false;
+                    seedsInShadowRound.clear();
+                }
+            }
         }
     }
 
@@ -1725,6 +1841,65 @@
         return System.currentTimeMillis() + Gossiper.aVeryLongTime;
     }
 
+    @Nullable
+    public CassandraVersion getReleaseVersion(InetAddress ep)
+    {
+        EndpointState state = getEndpointStateForEndpoint(ep);
+        return state != null ? state.getReleaseVersion() : null;
+    }
+
+    @Nullable
+    public UUID getSchemaVersion(InetAddress ep)
+    {
+        EndpointState state = getEndpointStateForEndpoint(ep);
+        return state != null ? state.getSchemaVersion() : null;
+    }
+
+    public static void waitToSettle()
+    {
+        int forceAfter = Integer.getInteger("cassandra.skip_wait_for_gossip_to_settle", -1);
+        if (forceAfter == 0)
+        {
+            return;
+        }
+        final int GOSSIP_SETTLE_MIN_WAIT_MS = 5000;
+        final int GOSSIP_SETTLE_POLL_INTERVAL_MS = 1000;
+        final int GOSSIP_SETTLE_POLL_SUCCESSES_REQUIRED = 3;
+
+        logger.info("Waiting for gossip to settle...");
+        Uninterruptibles.sleepUninterruptibly(GOSSIP_SETTLE_MIN_WAIT_MS, TimeUnit.MILLISECONDS);
+        int totalPolls = 0;
+        int numOkay = 0;
+        int epSize = Gossiper.instance.getEndpointStates().size();
+        while (numOkay < GOSSIP_SETTLE_POLL_SUCCESSES_REQUIRED)
+        {
+            Uninterruptibles.sleepUninterruptibly(GOSSIP_SETTLE_POLL_INTERVAL_MS, TimeUnit.MILLISECONDS);
+            int currentSize = Gossiper.instance.getEndpointStates().size();
+            totalPolls++;
+            if (currentSize == epSize)
+            {
+                logger.debug("Gossip looks settled.");
+                numOkay++;
+            }
+            else
+            {
+                logger.info("Gossip not settled after {} polls.", totalPolls);
+                numOkay = 0;
+            }
+            epSize = currentSize;
+            if (forceAfter > 0 && totalPolls > forceAfter)
+            {
+                logger.warn("Gossip not settled but startup forced by cassandra.skip_wait_for_gossip_to_settle. Gossip total polls: {}",
+                            totalPolls);
+                break;
+            }
+        }
+        if (totalPolls > GOSSIP_SETTLE_POLL_SUCCESSES_REQUIRED)
+            logger.info("Gossip settled after {} extra polls; proceeding", totalPolls - GOSSIP_SETTLE_POLL_SUCCESSES_REQUIRED);
+        else
+            logger.info("No gossip backlog; proceeding");
+    }
+
     @VisibleForTesting
     public void stopShutdownAndWait(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
     {
@@ -1797,4 +1972,10 @@
 
         return minVersion;
     }
+
+    @VisibleForTesting
+    public void setAnyNodeOn30(boolean anyNodeOn30)
+    {
+        this.anyNodeOn30 = anyNodeOn30;
+    }
 }
diff --git a/src/java/org/apache/cassandra/gms/TokenSerializer.java b/src/java/org/apache/cassandra/gms/TokenSerializer.java
index 1404258..41bd821 100644
--- a/src/java/org/apache/cassandra/gms/TokenSerializer.java
+++ b/src/java/org/apache/cassandra/gms/TokenSerializer.java
@@ -19,6 +19,8 @@
 
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.FBUtilities;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -37,9 +39,9 @@
     {
         for (Token token : tokens)
         {
-            byte[] bintoken = partitioner.getTokenFactory().toByteArray(token).array();
-            out.writeInt(bintoken.length);
-            out.write(bintoken);
+            ByteBuffer tokenBuffer = partitioner.getTokenFactory().toByteArray(token);
+            assert tokenBuffer.arrayOffset() == 0;
+            ByteBufferUtil.writeWithLength(tokenBuffer.array(), out);
         }
         out.writeInt(0);
     }
@@ -52,7 +54,7 @@
             int size = in.readInt();
             if (size < 1)
                 break;
-            logger.trace("Reading token of {} bytes", size);
+            logger.trace("Reading token of {}", FBUtilities.prettyPrintMemory(size));
             byte[] bintoken = new byte[size];
             in.readFully(bintoken);
             tokens.add(partitioner.getTokenFactory().fromByteArray(ByteBuffer.wrap(bintoken)));
diff --git a/src/java/org/apache/cassandra/gms/VersionedValue.java b/src/java/org/apache/cassandra/gms/VersionedValue.java
index 4323e0f..e48db95 100644
--- a/src/java/org/apache/cassandra/gms/VersionedValue.java
+++ b/src/java/org/apache/cassandra/gms/VersionedValue.java
@@ -261,7 +261,13 @@
 
         public VersionedValue releaseVersion()
         {
-            return new VersionedValue(FBUtilities.getReleaseVersionString());
+            return releaseVersion(FBUtilities.getReleaseVersionString());
+        }
+
+        @VisibleForTesting
+        public VersionedValue releaseVersion(String version)
+        {
+            return new VersionedValue(version);
         }
 
         public VersionedValue networkVersion()
diff --git a/src/java/org/apache/cassandra/hadoop/HadoopCompat.java b/src/java/org/apache/cassandra/hadoop/HadoopCompat.java
index bcfb952..479f948 100644
--- a/src/java/org/apache/cassandra/hadoop/HadoopCompat.java
+++ b/src/java/org/apache/cassandra/hadoop/HadoopCompat.java
@@ -45,7 +45,8 @@
  * Utility methods to allow applications to deal with inconsistencies between
  * MapReduce Context Objects API between Hadoop 1.x and 2.x.
  */
-public class HadoopCompat {
+public class HadoopCompat
+{
 
     private static final boolean useV21;
 
@@ -64,12 +65,15 @@
     private static final Method GET_TASK_ATTEMPT_ID;
     private static final Method PROGRESS_METHOD;
 
-    static {
+    static
+    {
         boolean v21 = true;
         final String PACKAGE = "org.apache.hadoop.mapreduce";
-        try {
+        try
+        {
             Class.forName(PACKAGE + ".task.JobContextImpl");
-        } catch (ClassNotFoundException cnfe) {
+        } catch (ClassNotFoundException cnfe)
+        {
             v21 = false;
         }
         useV21 = v21;
@@ -78,8 +82,10 @@
         Class<?> taskIOContextCls;
         Class<?> mapContextCls;
         Class<?> genericCounterCls;
-        try {
-            if (v21) {
+        try
+        {
+            if (v21)
+            {
                 jobContextCls =
                         Class.forName(PACKAGE+".task.JobContextImpl");
                 taskContextCls =
@@ -88,7 +94,9 @@
                         Class.forName(PACKAGE+".task.TaskInputOutputContextImpl");
                 mapContextCls = Class.forName(PACKAGE + ".task.MapContextImpl");
                 genericCounterCls = Class.forName(PACKAGE+".counters.GenericCounter");
-            } else {
+            }
+            else
+            {
                 jobContextCls =
                         Class.forName(PACKAGE+".JobContext");
                 taskContextCls =
@@ -100,10 +108,12 @@
                         Class.forName("org.apache.hadoop.mapred.Counters$Counter");
 
             }
-        } catch (ClassNotFoundException e) {
+        } catch (ClassNotFoundException e)
+        {
             throw new IllegalArgumentException("Can't find class", e);
         }
-        try {
+        try
+        {
             JOB_CONTEXT_CONSTRUCTOR =
                     jobContextCls.getConstructor(Configuration.class, JobID.class);
             JOB_CONTEXT_CONSTRUCTOR.setAccessible(true);
@@ -117,7 +127,8 @@
                             Long.TYPE);
             GENERIC_COUNTER_CONSTRUCTOR.setAccessible(true);
 
-            if (useV21) {
+            if (useV21)
+            {
                 MAP_CONTEXT_CONSTRUCTOR =
                         mapContextCls.getDeclaredConstructor(Configuration.class,
                                 TaskAttemptID.class,
@@ -127,15 +138,20 @@
                                 StatusReporter.class,
                                 InputSplit.class);
                 Method get_counter;
-                try {
+                try
+                {
                     get_counter = Class.forName(PACKAGE + ".TaskAttemptContext").getMethod("getCounter", String.class,
                             String.class);
-                } catch (Exception e) {
+                }
+                catch (Exception e)
+                {
                     get_counter = Class.forName(PACKAGE + ".TaskInputOutputContext").getMethod("getCounter",
                             String.class, String.class);
                 }
                 GET_COUNTER_METHOD = get_counter;
-            } else {
+            }
+            else
+            {
                 MAP_CONTEXT_CONSTRUCTOR =
                         mapContextCls.getConstructor(Configuration.class,
                                 TaskAttemptID.class,
@@ -163,13 +179,21 @@
             PROGRESS_METHOD = Class.forName(PACKAGE+".TaskAttemptContext")
                     .getMethod("progress");
 
-        } catch (SecurityException e) {
+        }
+        catch (SecurityException e)
+        {
             throw new IllegalArgumentException("Can't run constructor ", e);
-        } catch (NoSuchMethodException e) {
+        }
+        catch (NoSuchMethodException e)
+        {
             throw new IllegalArgumentException("Can't find constructor ", e);
-        } catch (NoSuchFieldException e) {
+        }
+        catch (NoSuchFieldException e)
+        {
             throw new IllegalArgumentException("Can't find field ", e);
-        } catch (ClassNotFoundException e) {
+        }
+        catch (ClassNotFoundException e)
+        {
             throw new IllegalArgumentException("Can't find class", e);
         }
     }
@@ -177,18 +201,27 @@
     /**
      * True if runtime Hadoop version is 2.x, false otherwise.
      */
-    public static boolean isVersion2x() {
+    public static boolean isVersion2x()
+    {
         return useV21;
     }
 
-    private static Object newInstance(Constructor<?> constructor, Object...args) {
-        try {
+    private static Object newInstance(Constructor<?> constructor, Object...args)
+    {
+        try
+        {
             return constructor.newInstance(args);
-        } catch (InstantiationException e) {
+        }
+        catch (InstantiationException e)
+        {
             throw new IllegalArgumentException("Can't instantiate " + constructor, e);
-        } catch (IllegalAccessException e) {
+        }
+        catch (IllegalAccessException e)
+        {
             throw new IllegalArgumentException("Can't instantiate " + constructor, e);
-        } catch (InvocationTargetException e) {
+        }
+        catch (InvocationTargetException e)
+        {
             throw new IllegalArgumentException("Can't instantiate " + constructor, e);
         }
     }
@@ -230,11 +263,15 @@
      * @return with Hadoop 2 : <code>new GenericCounter(args)</code>,<br>
      *         with Hadoop 1 : <code>new Counter(args)</code>
      */
-    public static Counter newGenericCounter(String name, String displayName, long value) {
-        try {
+    public static Counter newGenericCounter(String name, String displayName, long value)
+    {
+        try
+        {
             return (Counter)
                     GENERIC_COUNTER_CONSTRUCTOR.newInstance(name, displayName, value);
-        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
+        }
+        catch (InstantiationException | IllegalAccessException | InvocationTargetException e)
+        {
             throw new IllegalArgumentException("Can't instantiate Counter", e);
         }
     }
@@ -242,10 +279,14 @@
     /**
      * Invokes a method and rethrows any exception as runtime excetpions.
      */
-    private static Object invoke(Method method, Object obj, Object... args) {
-        try {
+    private static Object invoke(Method method, Object obj, Object... args)
+    {
+        try
+        {
             return method.invoke(obj, args);
-        } catch (IllegalAccessException | InvocationTargetException e) {
+        }
+        catch (IllegalAccessException | InvocationTargetException e)
+        {
             throw new IllegalArgumentException("Can't invoke method " + method.getName(), e);
         }
     }
@@ -254,7 +295,8 @@
      * Invoke getConfiguration() on JobContext. Works with both
      * Hadoop 1 and 2.
      */
-    public static Configuration getConfiguration(JobContext context) {
+    public static Configuration getConfiguration(JobContext context)
+    {
         return (Configuration) invoke(GET_CONFIGURATION_METHOD, context);
     }
 
@@ -262,7 +304,8 @@
      * Invoke setStatus() on TaskAttemptContext. Works with both
      * Hadoop 1 and 2.
      */
-    public static void setStatus(TaskAttemptContext context, String status) {
+    public static void setStatus(TaskAttemptContext context, String status)
+    {
         invoke(SET_STATUS_METHOD, context, status);
     }
 
@@ -270,7 +313,8 @@
      * returns TaskAttemptContext.getTaskAttemptID(). Works with both
      * Hadoop 1 and 2.
      */
-    public static TaskAttemptID getTaskAttemptID(TaskAttemptContext taskContext) {
+    public static TaskAttemptID getTaskAttemptID(TaskAttemptContext taskContext)
+    {
         return (TaskAttemptID) invoke(GET_TASK_ATTEMPT_ID, taskContext);
     }
 
@@ -279,7 +323,8 @@
      * Hadoop 1 and 2.
      */
     public static Counter getCounter(TaskInputOutputContext context,
-                                     String groupName, String counterName) {
+                                     String groupName, String counterName)
+    {
         return (Counter) invoke(GET_COUNTER_METHOD, context, groupName, counterName);
     }
 
@@ -287,14 +332,16 @@
      * Invoke TaskAttemptContext.progress(). Works with both
      * Hadoop 1 and 2.
      */
-    public static void progress(TaskAttemptContext context) {
+    public static void progress(TaskAttemptContext context)
+    {
         invoke(PROGRESS_METHOD, context);
     }
 
     /**
      * Increment the counter. Works with both Hadoop 1 and 2
      */
-    public static void incrementCounter(Counter counter, long increment) {
+    public static void incrementCounter(Counter counter, long increment)
+    {
         // incrementing a count might be called often. Might be affected by
         // cost of invoke(). might be good candidate to handle in a shim.
         // (TODO Raghu) figure out how achieve such a build with maven
diff --git a/src/java/org/apache/cassandra/hadoop/ReporterWrapper.java b/src/java/org/apache/cassandra/hadoop/ReporterWrapper.java
index 00023d8..d2cc769 100644
--- a/src/java/org/apache/cassandra/hadoop/ReporterWrapper.java
+++ b/src/java/org/apache/cassandra/hadoop/ReporterWrapper.java
@@ -26,50 +26,60 @@
 /**
  * A reporter that works with both mapred and mapreduce APIs.
  */
-public class ReporterWrapper extends StatusReporter implements Reporter {
+public class ReporterWrapper extends StatusReporter implements Reporter
+{
     private Reporter wrappedReporter;
 
-    public ReporterWrapper(Reporter reporter) {
+    public ReporterWrapper(Reporter reporter)
+    {
         wrappedReporter = reporter;
     }
 
     @Override
-    public Counters.Counter getCounter(Enum<?> anEnum) {
+    public Counters.Counter getCounter(Enum<?> anEnum)
+    {
         return wrappedReporter.getCounter(anEnum);
     }
 
     @Override
-    public Counters.Counter getCounter(String s, String s1) {
+    public Counters.Counter getCounter(String s, String s1)
+    {
         return wrappedReporter.getCounter(s, s1);
     }
 
     @Override
-    public void incrCounter(Enum<?> anEnum, long l) {
+    public void incrCounter(Enum<?> anEnum, long l)
+    {
         wrappedReporter.incrCounter(anEnum, l);
     }
 
     @Override
-    public void incrCounter(String s, String s1, long l) {
+    public void incrCounter(String s, String s1, long l)
+    {
         wrappedReporter.incrCounter(s, s1, l);
     }
 
     @Override
-    public InputSplit getInputSplit() throws UnsupportedOperationException {
+    public InputSplit getInputSplit() throws UnsupportedOperationException
+    {
         return wrappedReporter.getInputSplit();
     }
 
     @Override
-    public void progress() {
+    public void progress()
+    {
         wrappedReporter.progress();
     }
 
     // @Override
-    public float getProgress() {
+    public float getProgress()
+    {
         throw new UnsupportedOperationException();
     }
 
     @Override
-    public void setStatus(String s) {
+    public void setStatus(String s)
+    {
         wrappedReporter.setStatus(s);
     }
 }
diff --git a/src/java/org/apache/cassandra/hadoop/cql3/CqlBulkOutputFormat.java b/src/java/org/apache/cassandra/hadoop/cql3/CqlBulkOutputFormat.java
index 051447c..dfdf855 100644
--- a/src/java/org/apache/cassandra/hadoop/cql3/CqlBulkOutputFormat.java
+++ b/src/java/org/apache/cassandra/hadoop/cql3/CqlBulkOutputFormat.java
@@ -45,10 +45,10 @@
  * As is the case with the {@link org.apache.cassandra.hadoop.cql3.CqlOutputFormat}, 
  * you need to set the prepared statement in your
  * Hadoop job Configuration. The {@link CqlConfigHelper} class, through its
- * {@link ConfigHelper#setOutputPreparedStatement} method, is provided to make this
+ * {@link org.apache.cassandra.hadoop.ConfigHelper#setOutputPreparedStatement} method, is provided to make this
  * simple.
  * you need to set the Keyspace. The {@link ConfigHelper} class, through its
- * {@link ConfigHelper#setOutputColumnFamily} method, is provided to make this
+ * {@link org.apache.cassandra.hadoop.ConfigHelper#setOutputColumnFamily} method, is provided to make this
  * simple.
  * </p>
  */
diff --git a/src/java/org/apache/cassandra/hadoop/cql3/CqlBulkRecordWriter.java b/src/java/org/apache/cassandra/hadoop/cql3/CqlBulkRecordWriter.java
index 2ed37ee..fd9ed00 100644
--- a/src/java/org/apache/cassandra/hadoop/cql3/CqlBulkRecordWriter.java
+++ b/src/java/org/apache/cassandra/hadoop/cql3/CqlBulkRecordWriter.java
@@ -105,7 +105,6 @@
 
     CqlBulkRecordWriter(Configuration conf) throws IOException
     {
-        Config.setOutboundBindAny(true);
         this.conf = conf;
         DatabaseDescriptor.setStreamThroughputOutboundMegabitsPerSec(Integer.parseInt(conf.get(STREAM_THROTTLE_MBITS, "0")));
         maxFailures = Integer.parseInt(conf.get(MAX_FAILED_HOSTS, "0"));
diff --git a/src/java/org/apache/cassandra/hadoop/cql3/CqlConfigHelper.java b/src/java/org/apache/cassandra/hadoop/cql3/CqlConfigHelper.java
index 757be65..4c71273 100644
--- a/src/java/org/apache/cassandra/hadoop/cql3/CqlConfigHelper.java
+++ b/src/java/org/apache/cassandra/hadoop/cql3/CqlConfigHelper.java
@@ -283,7 +283,8 @@
         return conf.get(OUTPUT_CQL);
     }
 
-    private static Optional<Integer> getProtocolVersion(Configuration conf) {
+    private static Optional<Integer> getProtocolVersion(Configuration conf) 
+    {
         return getIntSetting(INPUT_NATIVE_PROTOCOL_VERSION, conf);
     }
 
@@ -330,7 +331,8 @@
         if (sslOptions.isPresent())
             builder.withSSL(sslOptions.get());
 
-        if (protocolVersion.isPresent()) {
+        if (protocolVersion.isPresent()) 
+        {
             builder.withProtocolVersion(ProtocolVersion.fromInt(protocolVersion.get()));
         }
         builder.withLoadBalancingPolicy(loadBalancingPolicy)
@@ -529,13 +531,14 @@
     public static Optional<SSLOptions> getSSLOptions(Configuration conf)
     {
         Optional<String> truststorePath = getInputNativeSSLTruststorePath(conf);
-        Optional<String> keystorePath = getInputNativeSSLKeystorePath(conf);
-        Optional<String> truststorePassword = getInputNativeSSLTruststorePassword(conf);
-        Optional<String> keystorePassword = getInputNativeSSLKeystorePassword(conf);
-        Optional<String> cipherSuites = getInputNativeSSLCipherSuites(conf);
 
         if (truststorePath.isPresent())
         {
+            Optional<String> keystorePath = getInputNativeSSLKeystorePath(conf);
+            Optional<String> truststorePassword = getInputNativeSSLTruststorePassword(conf);
+            Optional<String> keystorePassword = getInputNativeSSLKeystorePassword(conf);
+            Optional<String> cipherSuites = getInputNativeSSLCipherSuites(conf);
+
             SSLContext context;
             try
             {
diff --git a/src/java/org/apache/cassandra/hadoop/cql3/CqlInputFormat.java b/src/java/org/apache/cassandra/hadoop/cql3/CqlInputFormat.java
index a426532..5e47ed5 100644
--- a/src/java/org/apache/cassandra/hadoop/cql3/CqlInputFormat.java
+++ b/src/java/org/apache/cassandra/hadoop/cql3/CqlInputFormat.java
@@ -29,6 +29,7 @@
 import com.datastax.driver.core.Session;
 import com.datastax.driver.core.TokenRange;
 
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.mapred.InputSplit;
 import org.apache.hadoop.mapred.JobConf;
@@ -213,7 +214,7 @@
                 metadata.newToken(partitioner.getTokenFactory().toString(range.right)));
     }
 
-    private Map<TokenRange, Long> getSubSplits(String keyspace, String cfName, TokenRange range, Configuration conf, Session session) throws IOException
+    private Map<TokenRange, Long> getSubSplits(String keyspace, String cfName, TokenRange range, Configuration conf, Session session)
     {
         int splitSize = ConfigHelper.getInputSplitSize(conf);
         int splitSizeMb = ConfigHelper.getInputSplitSizeInMb(conf);
@@ -239,7 +240,7 @@
         String query = String.format("SELECT mean_partition_size, partitions_count " +
                                      "FROM %s.%s " +
                                      "WHERE keyspace_name = ? AND table_name = ? AND range_start = ? AND range_end = ?",
-                                     SystemKeyspace.NAME,
+                                     SchemaConstants.SYSTEM_KEYSPACE_NAME,
                                      SystemKeyspace.SIZE_ESTIMATES);
 
         ResultSet resultSet = session.execute(query, keyspace, table, tokenRange.getStart().toString(), tokenRange.getEnd().toString());
@@ -324,9 +325,9 @@
 
             boolean partitionerIsOpp = partitioner instanceof OrderPreservingPartitioner || partitioner instanceof ByteOrderedPartitioner;
 
-            for (TokenRange subSplit : subSplits.keySet())
+            for (Map.Entry<TokenRange, Long> subSplitEntry : subSplits.entrySet())
             {
-                List<TokenRange> ranges = subSplit.unwrap();
+                List<TokenRange> ranges = subSplitEntry.getKey().unwrap();
                 for (TokenRange subrange : ranges)
                 {
                     ColumnFamilySplit split =
@@ -335,7 +336,7 @@
                                             subrange.getStart().toString().substring(2) : subrange.getStart().toString(),
                                     partitionerIsOpp ?
                                             subrange.getEnd().toString().substring(2) : subrange.getEnd().toString(),
-                                    subSplits.get(subSplit),
+                                    subSplitEntry.getValue(),
                                     endpoints);
 
                     logger.trace("adding {}", split);
diff --git a/src/java/org/apache/cassandra/hadoop/cql3/CqlRecordReader.java b/src/java/org/apache/cassandra/hadoop/cql3/CqlRecordReader.java
index 8b04df3..d9aad19 100644
--- a/src/java/org/apache/cassandra/hadoop/cql3/CqlRecordReader.java
+++ b/src/java/org/apache/cassandra/hadoop/cql3/CqlRecordReader.java
@@ -248,7 +248,8 @@
      * Return native version protocol of the cluster connection
      * @return serialization protocol version.
      */
-    public int getNativeProtocolVersion() {
+    public int getNativeProtocolVersion() 
+    {
         return nativeProtocolVersion;
     }
 
diff --git a/src/java/org/apache/cassandra/hints/ChecksummedDataInput.java b/src/java/org/apache/cassandra/hints/ChecksummedDataInput.java
index a78256b..6ebc830 100644
--- a/src/java/org/apache/cassandra/hints/ChecksummedDataInput.java
+++ b/src/java/org/apache/cassandra/hints/ChecksummedDataInput.java
@@ -22,10 +22,12 @@
 import java.nio.ByteBuffer;
 import java.util.zip.CRC32;
 
-import org.apache.cassandra.io.util.ChannelProxy;
-import org.apache.cassandra.io.util.DataPosition;
-import org.apache.cassandra.io.util.RandomAccessReader;
+import com.google.common.base.Preconditions;
+
+import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.io.util.*;
 import org.apache.cassandra.utils.NativeLibrary;
+import org.apache.cassandra.utils.memory.BufferPool;
 
 /**
  * A {@link RandomAccessReader} wrapper that calculates the CRC in place.
@@ -38,30 +40,46 @@
  * corrupted sequence by reading a huge corrupted length of bytes via
  * {@link org.apache.cassandra.utils.ByteBufferUtil#readWithLength(java.io.DataInput)}.
  */
-public class ChecksummedDataInput extends RandomAccessReader.RandomAccessReaderWithOwnChannel
+public class ChecksummedDataInput extends RebufferingInputStream
 {
     private final CRC32 crc;
     private int crcPosition;
     private boolean crcUpdateDisabled;
 
     private long limit;
-    private DataPosition limitMark;
+    private long limitMark;
 
-    protected ChecksummedDataInput(Builder builder)
+    protected long bufferOffset;
+    protected final ChannelProxy channel;
+
+    ChecksummedDataInput(ChannelProxy channel, BufferType bufferType)
     {
-        super(builder);
+        super(BufferPool.get(RandomAccessReader.DEFAULT_BUFFER_SIZE, bufferType));
 
         crc = new CRC32();
         crcPosition = 0;
         crcUpdateDisabled = false;
+        this.channel = channel;
+        bufferOffset = 0;
+        buffer.limit(0);
 
         resetLimit();
     }
 
-    @SuppressWarnings("resource")   // channel owned by RandomAccessReaderWithOwnChannel
+    ChecksummedDataInput(ChannelProxy channel)
+    {
+        this(channel, BufferType.OFF_HEAP);
+    }
+
+    @SuppressWarnings("resource")
     public static ChecksummedDataInput open(File file)
     {
-        return new Builder(new ChannelProxy(file)).build();
+        return new ChecksummedDataInput(new ChannelProxy(file));
+    }
+
+    public boolean isEOF()
+    {
+        return getPosition() == channel.size();
     }
 
     static class Position implements InputPosition
@@ -105,8 +123,16 @@
 
     public void limit(long newLimit)
     {
-        limit = newLimit;
-        limitMark = mark();
+        limitMark = getPosition();
+        limit = limitMark + newLimit;
+    }
+
+    /**
+     * Returns the exact position in the uncompressed view of the file.
+     */
+    protected long getPosition()
+    {
+        return bufferOffset + buffer.position();
     }
 
     /**
@@ -121,22 +147,19 @@
     public void resetLimit()
     {
         limit = Long.MAX_VALUE;
-        limitMark = null;
+        limitMark = -1;
     }
 
     public void checkLimit(int length) throws IOException
     {
-        if (limitMark == null)
-            return;
-
-        if ((bytesPastLimit() + length) > limit)
+        if (getPosition() + length > limit)
             throw new IOException("Digest mismatch exception");
     }
 
     public long bytesPastLimit()
     {
-        assert limitMark != null;
-        return bytesPastMark(limitMark);
+        assert limitMark != -1;
+        return getPosition() - limitMark;
     }
 
     public boolean checkCrc() throws IOException
@@ -172,13 +195,24 @@
     }
 
     @Override
-    public void reBuffer()
+    protected void reBuffer()
     {
+        Preconditions.checkState(buffer.remaining() == 0);
         updateCrc();
-        super.reBuffer();
+        bufferOffset += buffer.limit();
+
+        readBuffer();
+
         crcPosition = buffer.position();
     }
 
+    protected void readBuffer()
+    {
+        buffer.clear();
+        while ((channel.read(buffer, bufferOffset)) == 0) {}
+        buffer.flip();
+    }
+
     public void tryUncacheRead()
     {
         NativeLibrary.trySkipCache(getChannel().getFileDescriptor(), 0, getSourcePosition(), getPath());
@@ -198,16 +232,20 @@
         crc.update(unprocessed);
     }
 
-    public static class Builder extends RandomAccessReader.Builder
+    @Override
+    public void close()
     {
-        public Builder(ChannelProxy channel)
-        {
-            super(channel);
-        }
+        BufferPool.put(buffer);
+        channel.close();
+    }
 
-        public ChecksummedDataInput build()
-        {
-            return new ChecksummedDataInput(this);
-        }
+    protected String getPath()
+    {
+        return channel.filePath();
+    }
+
+    public ChannelProxy getChannel()
+    {
+        return channel;
     }
 }
diff --git a/src/java/org/apache/cassandra/hints/CompressedChecksummedDataInput.java b/src/java/org/apache/cassandra/hints/CompressedChecksummedDataInput.java
index c0de1cf..0766fa5 100644
--- a/src/java/org/apache/cassandra/hints/CompressedChecksummedDataInput.java
+++ b/src/java/org/apache/cassandra/hints/CompressedChecksummedDataInput.java
@@ -21,6 +21,9 @@
 import java.io.IOException;
 import java.nio.ByteBuffer;
 
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.cassandra.hints.ChecksummedDataInput.Position;
 import org.apache.cassandra.io.FSReadError;
 import org.apache.cassandra.io.compress.ICompressor;
 import org.apache.cassandra.io.util.ChannelProxy;
@@ -34,13 +37,11 @@
     private volatile ByteBuffer compressedBuffer = null;
     private final ByteBuffer metadataBuffer = ByteBuffer.allocate(CompressedHintsWriter.METADATA_SIZE);
 
-    public CompressedChecksummedDataInput(Builder builder)
+    public CompressedChecksummedDataInput(ChannelProxy channel, ICompressor compressor, long filePosition)
     {
-        super(builder);
-        assert regions == null;  //mmapped regions are not supported
-
-        compressor = builder.compressor;
-        sourcePosition =  filePosition = builder.position;
+        super(channel, compressor.preferredBufferType());
+        this.compressor = compressor;
+        this.sourcePosition = this.filePosition = filePosition;
     }
 
     /**
@@ -96,7 +97,8 @@
         assert buffer.position() == pos.bufferPosition;
     }
 
-    protected void reBufferStandard()
+    @Override
+    protected void readBuffer()
     {
         sourcePosition = filePosition;
         if (isEOF())
@@ -117,7 +119,7 @@
             {
                 BufferPool.put(compressedBuffer);
             }
-            compressedBuffer = allocateBuffer(bufferSize, compressor.preferredBufferType());
+            compressedBuffer = BufferPool.get(bufferSize, compressor.preferredBufferType());
         }
 
         compressedBuffer.clear();
@@ -126,12 +128,11 @@
         compressedBuffer.rewind();
         filePosition += compressedSize;
 
-        bufferOffset += buffer.position();
         if (buffer.capacity() < uncompressedSize)
         {
             int bufferSize = uncompressedSize + (uncompressedSize / 20);
             BufferPool.put(buffer);
-            buffer = allocateBuffer(bufferSize, compressor.preferredBufferType());
+            buffer = BufferPool.get(bufferSize, compressor.preferredBufferType());
         }
 
         buffer.clear();
@@ -147,63 +148,25 @@
         }
     }
 
-    protected void releaseBuffer()
+    @Override
+    public void close()
     {
-        super.releaseBuffer();
-        if (compressedBuffer != null)
-        {
-            BufferPool.put(compressedBuffer);
-            compressedBuffer = null;
-        }
+        BufferPool.put(compressedBuffer);
+        super.close();
     }
 
-    protected void reBufferMmap()
-    {
-        throw new UnsupportedOperationException();
-    }
-
-    public static final class Builder extends ChecksummedDataInput.Builder
-    {
-        private long position;
-        private ICompressor compressor;
-
-        public Builder(ChannelProxy channel)
-        {
-            super(channel);
-            bufferType = null;
-        }
-
-        public CompressedChecksummedDataInput build()
-        {
-            assert position >= 0;
-            assert compressor != null;
-            return new CompressedChecksummedDataInput(this);
-        }
-
-        public Builder withCompressor(ICompressor compressor)
-        {
-            this.compressor = compressor;
-            bufferType = compressor.preferredBufferType();
-            return this;
-        }
-
-        public Builder withPosition(long position)
-        {
-            this.position = position;
-            return this;
-        }
-    }
-
-    // Closing the CompressedChecksummedDataInput will close the underlying channel.
-    @SuppressWarnings("resource")
-    public static final CompressedChecksummedDataInput upgradeInput(ChecksummedDataInput input, ICompressor compressor)
+    @SuppressWarnings("resource") // Closing the ChecksummedDataInput will close the underlying channel.
+    public static ChecksummedDataInput upgradeInput(ChecksummedDataInput input, ICompressor compressor)
     {
         long position = input.getPosition();
         input.close();
 
-        Builder builder = new Builder(new ChannelProxy(input.getPath()));
-        builder.withPosition(position);
-        builder.withCompressor(compressor);
-        return builder.build();
+        return new CompressedChecksummedDataInput(new ChannelProxy(input.getPath()), compressor, position);
+    }
+
+    @VisibleForTesting
+    ICompressor getCompressor()
+    {
+        return compressor;
     }
 }
diff --git a/src/java/org/apache/cassandra/hints/CompressedHintsWriter.java b/src/java/org/apache/cassandra/hints/CompressedHintsWriter.java
index 491dceb..8792e32 100644
--- a/src/java/org/apache/cassandra/hints/CompressedHintsWriter.java
+++ b/src/java/org/apache/cassandra/hints/CompressedHintsWriter.java
@@ -24,6 +24,8 @@
 import java.nio.channels.FileChannel;
 import java.util.zip.CRC32;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import org.apache.cassandra.io.compress.ICompressor;
 
 public class CompressedHintsWriter extends HintsWriter
@@ -64,4 +66,10 @@
         compressionBuffer.limit(compressedSize + METADATA_SIZE);
         super.writeBuffer(compressionBuffer);
     }
+
+    @VisibleForTesting
+    ICompressor getCompressor()
+    {
+        return compressor;
+    }
 }
diff --git a/src/java/org/apache/cassandra/hints/EncryptedChecksummedDataInput.java b/src/java/org/apache/cassandra/hints/EncryptedChecksummedDataInput.java
new file mode 100644
index 0000000..b01161d
--- /dev/null
+++ b/src/java/org/apache/cassandra/hints/EncryptedChecksummedDataInput.java
@@ -0,0 +1,155 @@
+/*
+ * 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.hints;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import javax.crypto.Cipher;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.security.EncryptionUtils;
+import org.apache.cassandra.hints.CompressedChecksummedDataInput.Position;
+import org.apache.cassandra.io.FSReadError;
+import org.apache.cassandra.io.compress.ICompressor;
+import org.apache.cassandra.io.util.ChannelProxy;
+
+public class EncryptedChecksummedDataInput extends ChecksummedDataInput
+{
+    private static final FastThreadLocal<ByteBuffer> reusableBuffers = new FastThreadLocal<ByteBuffer>()
+    {
+        protected ByteBuffer initialValue()
+        {
+            return ByteBuffer.allocate(0);
+        }
+    };
+
+    private final Cipher cipher;
+    private final ICompressor compressor;
+
+    private final EncryptionUtils.ChannelProxyReadChannel readChannel;
+    private long sourcePosition;
+
+    protected EncryptedChecksummedDataInput(ChannelProxy channel, Cipher cipher, ICompressor compressor, long filePosition)
+    {
+        super(channel);
+        this.cipher = cipher;
+        this.compressor = compressor;
+        readChannel = new EncryptionUtils.ChannelProxyReadChannel(channel, filePosition);
+        this.sourcePosition = filePosition;
+        assert cipher != null;
+        assert compressor != null;
+    }
+
+    /**
+     * Since an entire block of compressed data is read off of disk, not just a hint at a time,
+     * we don't report EOF until the decompressed data has also been read completely
+     */
+    public boolean isEOF()
+    {
+        return readChannel.getCurrentPosition() == channel.size() && buffer.remaining() == 0;
+    }
+
+    public long getSourcePosition()
+    {
+        return sourcePosition;
+    }
+
+    static class Position extends ChecksummedDataInput.Position
+    {
+        final long bufferStart;
+        final int bufferPosition;
+
+        public Position(long sourcePosition, long bufferStart, int bufferPosition)
+        {
+            super(sourcePosition);
+            this.bufferStart = bufferStart;
+            this.bufferPosition = bufferPosition;
+        }
+
+        @Override
+        public long subtract(InputPosition o)
+        {
+            Position other = (Position) o;
+            return bufferStart - other.bufferStart + bufferPosition - other.bufferPosition;
+        }
+    }
+
+    public InputPosition getSeekPosition()
+    {
+        return new Position(sourcePosition, bufferOffset, buffer.position());
+    }
+
+    public void seek(InputPosition p)
+    {
+        Position pos = (Position) p;
+        bufferOffset = pos.bufferStart;
+        readChannel.setPosition(pos.sourcePosition);
+        buffer.position(0).limit(0);
+        resetCrc();
+        reBuffer();
+        buffer.position(pos.bufferPosition);
+        assert sourcePosition == pos.sourcePosition;
+        assert bufferOffset == pos.bufferStart;
+        assert buffer.position() == pos.bufferPosition;
+    }
+
+    @Override
+    protected void readBuffer()
+    {
+        this.sourcePosition = readChannel.getCurrentPosition();
+        if (isEOF())
+            return;
+
+        try
+        {
+            ByteBuffer byteBuffer = reusableBuffers.get();
+            ByteBuffer decrypted = EncryptionUtils.decrypt(readChannel, byteBuffer, true, cipher);
+            buffer = EncryptionUtils.uncompress(decrypted, buffer, true, compressor);
+
+            if (decrypted.capacity() > byteBuffer.capacity())
+                reusableBuffers.set(decrypted);
+        }
+        catch (IOException ioe)
+        {
+            throw new FSReadError(ioe, getPath());
+        }
+    }
+
+    @SuppressWarnings("resource")
+    public static ChecksummedDataInput upgradeInput(ChecksummedDataInput input, Cipher cipher, ICompressor compressor)
+    {
+        long position = input.getPosition();
+        input.close();
+
+        return new EncryptedChecksummedDataInput(new ChannelProxy(input.getPath()), cipher, compressor, position);
+    }
+
+    @VisibleForTesting
+    Cipher getCipher()
+    {
+        return cipher;
+    }
+
+    @VisibleForTesting
+    ICompressor getCompressor()
+    {
+        return compressor;
+    }
+}
diff --git a/src/java/org/apache/cassandra/hints/EncryptedHintsWriter.java b/src/java/org/apache/cassandra/hints/EncryptedHintsWriter.java
new file mode 100644
index 0000000..4786d9c
--- /dev/null
+++ b/src/java/org/apache/cassandra/hints/EncryptedHintsWriter.java
@@ -0,0 +1,65 @@
+/*
+ * 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.hints;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.zip.CRC32;
+import javax.crypto.Cipher;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.cassandra.security.EncryptionUtils;
+import org.apache.cassandra.io.compress.ICompressor;
+
+import static org.apache.cassandra.utils.FBUtilities.updateChecksum;
+
+public class EncryptedHintsWriter extends HintsWriter
+{
+    private final Cipher cipher;
+    private final ICompressor compressor;
+    private volatile ByteBuffer byteBuffer;
+
+    protected EncryptedHintsWriter(File directory, HintsDescriptor descriptor, File file, FileChannel channel, int fd, CRC32 globalCRC)
+    {
+        super(directory, descriptor, file, channel, fd, globalCRC);
+        cipher = descriptor.getCipher();
+        compressor = descriptor.createCompressor();
+    }
+
+    protected void writeBuffer(ByteBuffer input) throws IOException
+    {
+        byteBuffer = EncryptionUtils.compress(input, byteBuffer, true, compressor);
+        ByteBuffer output = EncryptionUtils.encryptAndWrite(byteBuffer, channel, true, cipher);
+        updateChecksum(globalCRC, output);
+    }
+
+    @VisibleForTesting
+    Cipher getCipher()
+    {
+        return cipher;
+    }
+
+    @VisibleForTesting
+    ICompressor getCompressor()
+    {
+        return compressor;
+    }
+}
diff --git a/src/java/org/apache/cassandra/hints/HintsBuffer.java b/src/java/org/apache/cassandra/hints/HintsBuffer.java
index e86dede..d944b4d 100644
--- a/src/java/org/apache/cassandra/hints/HintsBuffer.java
+++ b/src/java/org/apache/cassandra/hints/HintsBuffer.java
@@ -23,12 +23,11 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.zip.CRC32;
 
 import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.io.util.DataOutputBufferFixed;
-import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.utils.AbstractIterator;
@@ -53,10 +52,9 @@
 {
     // hint entry overhead in bytes (int length, int length checksum, int body checksum)
     static final int ENTRY_OVERHEAD_SIZE = 12;
-    static final int CLOSED = -1;
 
     private final ByteBuffer slab; // the underlying backing ByteBuffer for all the serialized hints
-    private final AtomicInteger position; // the position in the slab that we currently allocate from
+    private final AtomicLong position; // the position in the slab that we currently allocate from
 
     private final ConcurrentMap<UUID, Queue<Integer>> offsets;
     private final OpOrder appendOrder;
@@ -65,7 +63,7 @@
     {
         this.slab = slab;
 
-        position = new AtomicInteger();
+        position = new AtomicLong();
         offsets = new ConcurrentHashMap<>();
         appendOrder = new OpOrder();
     }
@@ -77,7 +75,7 @@
 
     boolean isClosed()
     {
-        return position.get() == CLOSED;
+        return position.get() < 0;
     }
 
     int capacity()
@@ -87,8 +85,8 @@
 
     int remaining()
     {
-        int pos = position.get();
-        return pos == CLOSED ? 0 : capacity() - pos;
+        long pos = position.get();
+        return (int) (pos < 0 ? 0 : Math.max(0, capacity() - pos));
     }
 
     HintsBuffer recycle()
@@ -178,25 +176,21 @@
         return new Allocation(offset, totalSize, opGroup);
     }
 
+    // allocate bytes in the slab, or return negative if not enough space
     private int allocateBytes(int totalSize)
     {
-        while (true)
+        long prev = position.getAndAdd(totalSize);
+
+        if (prev < 0) // the slab has been 'closed'
+            return -1;
+
+        if ((prev + totalSize) > slab.capacity())
         {
-            int prev = position.get();
-            int next = prev + totalSize;
-
-            if (prev == CLOSED) // the slab has been 'closed'
-                return CLOSED;
-
-            if (next > slab.capacity())
-            {
-                position.set(CLOSED); // mark the slab as no longer allocating if we've exceeded its capacity
-                return CLOSED;
-            }
-
-            if (position.compareAndSet(prev, next))
-                return prev;
+            position.set(Long.MIN_VALUE); // mark the slab as no longer allocating if we've exceeded its capacity
+            return -1;
         }
+
+        return (int)prev;
     }
 
     private void put(UUID hostId, int offset)
diff --git a/src/java/org/apache/cassandra/hints/HintsCatalog.java b/src/java/org/apache/cassandra/hints/HintsCatalog.java
index 48bbc08..81ec98e 100644
--- a/src/java/org/apache/cassandra/hints/HintsCatalog.java
+++ b/src/java/org/apache/cassandra/hints/HintsCatalog.java
@@ -162,7 +162,7 @@
                 FileUtils.handleFSErrorAndPropagate(e);
             }
         }
-        else if (!FBUtilities.isWindows())
+        else if (!FBUtilities.isWindows)
         {
             logger.error("Unable to open directory {}", hintsDirectory.getAbsolutePath());
             FileUtils.handleFSErrorAndPropagate(new FSWriteError(new IOException(String.format("Unable to open hint directory %s", hintsDirectory.getAbsolutePath())), hintsDirectory.getAbsolutePath()));
diff --git a/src/java/org/apache/cassandra/hints/HintsDescriptor.java b/src/java/org/apache/cassandra/hints/HintsDescriptor.java
index d1f1116..429d4d4 100644
--- a/src/java/org/apache/cassandra/hints/HintsDescriptor.java
+++ b/src/java/org/apache/cassandra/hints/HintsDescriptor.java
@@ -23,11 +23,13 @@
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
 import java.util.UUID;
 import java.util.regex.Pattern;
 import java.util.zip.CRC32;
+import javax.crypto.Cipher;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
@@ -36,6 +38,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.ParameterizedClass;
 import org.apache.cassandra.db.TypeSizes;
 import org.apache.cassandra.io.FSReadError;
@@ -43,6 +46,8 @@
 import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.schema.CompressionParams;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.utils.Hex;
 import org.json.simple.JSONValue;
 
 import static org.apache.cassandra.utils.FBUtilities.updateChecksumInt;
@@ -61,6 +66,7 @@
     static final int CURRENT_VERSION = VERSION_30;
 
     static final String COMPRESSION = "compression";
+    static final String ENCRYPTION = "encryption";
 
     static final Pattern pattern =
         Pattern.compile("^[a-fA-F0-9]{8}\\-[a-fA-F0-9]{4}\\-[a-fA-F0-9]{4}\\-[a-fA-F0-9]{4}\\-[a-fA-F0-9]{12}\\-(\\d+)\\-(\\d+)\\.hints$");
@@ -69,17 +75,35 @@
     final int version;
     final long timestamp;
 
-    // implemented for future compression support - see CASSANDRA-9428
     final ImmutableMap<String, Object> parameters;
     final ParameterizedClass compressionConfig;
 
+    private final Cipher cipher;
+    private final ICompressor compressor;
+
     HintsDescriptor(UUID hostId, int version, long timestamp, ImmutableMap<String, Object> parameters)
     {
         this.hostId = hostId;
         this.version = version;
         this.timestamp = timestamp;
-        this.parameters = parameters;
         compressionConfig = createCompressionConfig(parameters);
+
+        EncryptionData encryption = createEncryption(parameters);
+        if (encryption == null)
+        {
+            cipher = null;
+            compressor = null;
+        }
+        else
+        {
+            if (compressionConfig != null)
+                throw new IllegalStateException("a hints file cannot be configured for both compression and encryption");
+            cipher = encryption.cipher;
+            compressor = encryption.compressor;
+            parameters = encryption.params;
+        }
+
+        this.parameters = parameters;
     }
 
     HintsDescriptor(UUID hostId, long timestamp, ImmutableMap<String, Object> parameters)
@@ -107,6 +131,71 @@
         }
     }
 
+    /**
+     * Create, if necessary, the required encryption components (for either decrpyt or encrypt operations).
+     * Note that in the case of encyption (this is, when writing out a new hints file), we need to write
+     * the cipher's IV out to the header so it can be used when decrypting. Thus, we need to add an additional
+     * entry to the {@code params} map.
+     *
+     * @param params the base parameters into the descriptor.
+     * @return null if not using encryption; else, the initialized {@link Cipher} and a possibly updated version
+     * of the {@code params} map.
+     */
+    @SuppressWarnings("unchecked")
+    static EncryptionData createEncryption(ImmutableMap<String, Object> params)
+    {
+        if (params.containsKey(ENCRYPTION))
+        {
+            Map<?, ?> encryptionConfig = (Map<?, ?>) params.get(ENCRYPTION);
+            EncryptionContext encryptionContext = EncryptionContext.createFromMap(encryptionConfig, DatabaseDescriptor.getEncryptionContext());
+
+            try
+            {
+                Cipher cipher;
+                if (encryptionConfig.containsKey(EncryptionContext.ENCRYPTION_IV))
+                {
+                    cipher = encryptionContext.getDecryptor();
+                }
+                else
+                {
+                    cipher = encryptionContext.getEncryptor();
+                    ImmutableMap<String, Object> encParams = ImmutableMap.<String, Object>builder()
+                                                                 .putAll(encryptionContext.toHeaderParameters())
+                                                                 .put(EncryptionContext.ENCRYPTION_IV, Hex.bytesToHex(cipher.getIV()))
+                                                                 .build();
+
+                    Map<String, Object> map = new HashMap<>(params);
+                    map.put(ENCRYPTION, encParams);
+                    params = ImmutableMap.<String, Object>builder().putAll(map).build();
+                }
+                return new EncryptionData(cipher, encryptionContext.getCompressor(), params);
+            }
+            catch (IOException ioe)
+            {
+                logger.warn("failed to create encyption context for hints file. ignoring encryption for hints.", ioe);
+                return null;
+            }
+        }
+        else
+        {
+            return null;
+        }
+    }
+
+    private static final class EncryptionData
+    {
+        final Cipher cipher;
+        final ICompressor compressor;
+        final ImmutableMap<String, Object> params;
+
+        private EncryptionData(Cipher cipher, ICompressor compressor, ImmutableMap<String, Object> params)
+        {
+            this.cipher = cipher;
+            this.compressor = compressor;
+            this.params = params;
+        }
+    }
+
     String fileName()
     {
         return String.format("%s-%s-%s.hints", hostId, timestamp, version);
@@ -196,9 +285,23 @@
         return compressionConfig != null;
     }
 
+    public boolean isEncrypted()
+    {
+        return cipher != null;
+    }
+
     public ICompressor createCompressor()
     {
-        return isCompressed() ? CompressionParams.createCompressor(compressionConfig) : null;
+        if (isCompressed())
+            return CompressionParams.createCompressor(compressionConfig);
+        if (isEncrypted())
+            return compressor;
+        return null;
+    }
+
+    public Cipher getCipher()
+    {
+        return isEncrypted() ? cipher : null;
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/hints/HintsDispatchTrigger.java b/src/java/org/apache/cassandra/hints/HintsDispatchTrigger.java
index 5fe0e27..cc1c221 100644
--- a/src/java/org/apache/cassandra/hints/HintsDispatchTrigger.java
+++ b/src/java/org/apache/cassandra/hints/HintsDispatchTrigger.java
@@ -19,8 +19,8 @@
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.gms.ApplicationState;
-import org.apache.cassandra.gms.FailureDetector;
 import org.apache.cassandra.gms.Gossiper;
 
 import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddress;
@@ -65,7 +65,7 @@
                .filter(store -> !isScheduled(store))
                .filter(HintsStore::isLive)
                .filter(store -> store.isWriting() || store.hasFiles())
-               .filter(store -> Gossiper.instance.valuesEqual(getBroadcastAddress(), store.address(), ApplicationState.SCHEMA))
+               .filter(store -> Schema.instance.isSameVersion(Gossiper.instance.getSchemaVersion(store.address())))
                .forEach(this::schedule);
     }
 
diff --git a/src/java/org/apache/cassandra/hints/HintsDispatcher.java b/src/java/org/apache/cassandra/hints/HintsDispatcher.java
index db5f42f..c432553 100644
--- a/src/java/org/apache/cassandra/hints/HintsDispatcher.java
+++ b/src/java/org/apache/cassandra/hints/HintsDispatcher.java
@@ -29,7 +29,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.exceptions.RequestFailureReason;
+import org.apache.cassandra.metrics.HintsServiceMetrics;
 import org.apache.cassandra.net.IAsyncCallbackWithFailure;
 import org.apache.cassandra.net.MessageIn;
 import org.apache.cassandra.net.MessagingService;
@@ -130,11 +131,33 @@
         if (action == Action.ABORT)
             return action;
 
+        boolean hadFailures = false;
         for (Callback cb : callbacks)
-            if (cb.await() != Callback.Outcome.SUCCESS)
-                return Action.ABORT;
+        {
+            Callback.Outcome outcome = cb.await();
+            updateMetrics(outcome);
 
-        return Action.CONTINUE;
+            if (outcome != Callback.Outcome.SUCCESS)
+                hadFailures = true;
+        }
+
+        return hadFailures ? Action.ABORT : Action.CONTINUE;
+    }
+
+    private void updateMetrics(Callback.Outcome outcome)
+    {
+        switch (outcome)
+        {
+            case SUCCESS:
+                HintsServiceMetrics.hintsSucceeded.mark();
+                break;
+            case FAILURE:
+                HintsServiceMetrics.hintsFailed.mark();
+                break;
+            case TIMEOUT:
+                HintsServiceMetrics.hintsTimedOut.mark();
+                break;
+        }
     }
 
     /*
@@ -182,7 +205,7 @@
 
         Outcome await()
         {
-            long timeout = TimeUnit.MILLISECONDS.toNanos(DatabaseDescriptor.getTimeout(MessagingService.Verb.HINT)) - (System.nanoTime() - start);
+            long timeout = TimeUnit.MILLISECONDS.toNanos(MessagingService.Verb.HINT.getTimeout()) - (System.nanoTime() - start);
             boolean timedOut;
 
             try
@@ -198,7 +221,7 @@
             return timedOut ? Outcome.TIMEOUT : outcome;
         }
 
-        public void onFailure(InetAddress from)
+        public void onFailure(InetAddress from, RequestFailureReason failureReason)
         {
             outcome = Outcome.FAILURE;
             condition.signalAll();
@@ -214,5 +237,11 @@
         {
             return false;
         }
+
+        @Override
+        public boolean supportsBackPressure()
+        {
+            return true;
+        }
     }
 }
diff --git a/src/java/org/apache/cassandra/hints/HintsReader.java b/src/java/org/apache/cassandra/hints/HintsReader.java
index 34113ec..8550613 100644
--- a/src/java/org/apache/cassandra/hints/HintsReader.java
+++ b/src/java/org/apache/cassandra/hints/HintsReader.java
@@ -84,6 +84,8 @@
                 // The compressed input is instantiated with the uncompressed input's position
                 reader = CompressedChecksummedDataInput.upgradeInput(reader, descriptor.createCompressor());
             }
+            else if (descriptor.isEncrypted())
+                reader = EncryptedChecksummedDataInput.upgradeInput(reader, descriptor.getCipher(), descriptor.createCompressor());
             return new HintsReader(descriptor, file, reader, rateLimiter);
         }
         catch (IOException e)
diff --git a/src/java/org/apache/cassandra/hints/HintsService.java b/src/java/org/apache/cassandra/hints/HintsService.java
index 6546836..49e824f 100644
--- a/src/java/org/apache/cassandra/hints/HintsService.java
+++ b/src/java/org/apache/cassandra/hints/HintsService.java
@@ -149,8 +149,7 @@
         // we have to make sure that the HintsStore instances get properly initialized - otherwise dispatch will not trigger
         catalog.maybeLoadStores(hostIds);
 
-        if (hint.isLive())
-            bufferPool.write(hostIds, hint);
+        bufferPool.write(hostIds, hint);
 
         StorageMetrics.totalHints.inc(size(hostIds));
     }
diff --git a/src/java/org/apache/cassandra/hints/HintsWriter.java b/src/java/org/apache/cassandra/hints/HintsWriter.java
index 31a440d..48b8c7c 100644
--- a/src/java/org/apache/cassandra/hints/HintsWriter.java
+++ b/src/java/org/apache/cassandra/hints/HintsWriter.java
@@ -33,7 +33,6 @@
 import org.apache.cassandra.io.FSWriteError;
 import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.io.util.DataOutputBufferFixed;
-import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.utils.NativeLibrary;
 import org.apache.cassandra.utils.SyncUtil;
 import org.apache.cassandra.utils.Throwables;
@@ -49,9 +48,9 @@
     private final File directory;
     private final HintsDescriptor descriptor;
     private final File file;
-    private final FileChannel channel;
+    protected final FileChannel channel;
     private final int fd;
-    private final CRC32 globalCRC;
+    protected final CRC32 globalCRC;
 
     private volatile long lastSyncPosition = 0L;
 
@@ -75,7 +74,7 @@
 
         CRC32 crc = new CRC32();
 
-        try (DataOutputBuffer dob = new DataOutputBuffer())
+        try (DataOutputBuffer dob = DataOutputBuffer.scratchBuffer.get())
         {
             // write the descriptor
             descriptor.serialize(dob);
@@ -89,14 +88,11 @@
             throw e;
         }
 
+        if (descriptor.isEncrypted())
+            return new EncryptedHintsWriter(directory, descriptor, file, channel, fd, crc);
         if (descriptor.isCompressed())
-        {
             return new CompressedHintsWriter(directory, descriptor, file, channel, fd, crc);
-        }
-        else
-        {
-            return new HintsWriter(directory, descriptor, file, channel, fd, crc);
-        }
+        return new HintsWriter(directory, descriptor, file, channel, fd, crc);
     }
 
     HintsDescriptor descriptor()
diff --git a/src/java/org/apache/cassandra/hints/InputPosition.java b/src/java/org/apache/cassandra/hints/InputPosition.java
index 0b8953c..28146c1 100644
--- a/src/java/org/apache/cassandra/hints/InputPosition.java
+++ b/src/java/org/apache/cassandra/hints/InputPosition.java
@@ -15,6 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.cassandra.hints;
 
 /**
diff --git a/src/java/org/apache/cassandra/hints/LegacyHintsMigrator.java b/src/java/org/apache/cassandra/hints/LegacyHintsMigrator.java
index 30e5fe0..50d8b6e 100644
--- a/src/java/org/apache/cassandra/hints/LegacyHintsMigrator.java
+++ b/src/java/org/apache/cassandra/hints/LegacyHintsMigrator.java
@@ -27,6 +27,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.db.*;
@@ -59,7 +60,7 @@
         this.hintsDirectory = hintsDirectory;
         this.maxHintsFileSize = maxHintsFileSize;
 
-        legacyHintsTable = Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.LEGACY_HINTS);
+        legacyHintsTable = Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.LEGACY_HINTS);
         pageSize = calculatePageSize(legacyHintsTable);
     }
 
@@ -88,7 +89,7 @@
         logger.info("Migrating legacy hints to new storage");
 
         // major-compact all of the existing sstables to get rid of the tombstones + expired hints
-        logger.info("Forcing a major compaction of {}.{} table", SystemKeyspace.NAME, SystemKeyspace.LEGACY_HINTS);
+        logger.info("Forcing a major compaction of {}.{} table", SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.LEGACY_HINTS);
         compactLegacyHints();
 
         // paginate over legacy hints and write them to the new storage
@@ -96,7 +97,7 @@
         migrateLegacyHints();
 
         // truncate the legacy hints table
-        logger.info("Truncating {}.{} table", SystemKeyspace.NAME, SystemKeyspace.LEGACY_HINTS);
+        logger.info("Truncating {}.{} table", SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.LEGACY_HINTS);
         legacyHintsTable.truncateBlocking();
     }
 
@@ -123,7 +124,7 @@
     private void migrateLegacyHints()
     {
         ByteBuffer buffer = ByteBuffer.allocateDirect(256 * 1024);
-        String query = String.format("SELECT DISTINCT target_id FROM %s.%s", SystemKeyspace.NAME, SystemKeyspace.LEGACY_HINTS);
+        String query = String.format("SELECT DISTINCT target_id FROM %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.LEGACY_HINTS);
         //noinspection ConstantConditions
         QueryProcessor.executeInternal(query).forEach(row -> migrateLegacyHints(row.getUUID("target_id"), buffer));
         FileUtils.clean(buffer);
@@ -134,7 +135,7 @@
         String query = String.format("SELECT target_id, hint_id, message_version, mutation, ttl(mutation) AS ttl, writeTime(mutation) AS write_time " +
                                      "FROM %s.%s " +
                                      "WHERE target_id = ?",
-                                     SystemKeyspace.NAME,
+                                     SchemaConstants.SYSTEM_KEYSPACE_NAME,
                                      SystemKeyspace.LEGACY_HINTS);
 
         // read all the old hints (paged iterator), write them in the new format
@@ -213,18 +214,18 @@
         }
         catch (IOException e)
         {
-            logger.error("Failed to migrate a hint for {} from legacy {}.{} table: {}",
+            logger.error("Failed to migrate a hint for {} from legacy {}.{} table",
                          row.getUUID("target_id"),
-                         SystemKeyspace.NAME,
+                         SchemaConstants.SYSTEM_KEYSPACE_NAME,
                          SystemKeyspace.LEGACY_HINTS,
                          e);
             return null;
         }
         catch (MarshalException e)
         {
-            logger.warn("Failed to validate a hint for {} (table id {}) from legacy {}.{} table - skipping: {})",
+            logger.warn("Failed to validate a hint for {} from legacy {}.{} table - skipping",
                         row.getUUID("target_id"),
-                        SystemKeyspace.NAME,
+                        SchemaConstants.SYSTEM_KEYSPACE_NAME,
                         SystemKeyspace.LEGACY_HINTS,
                         e);
             return null;
diff --git a/src/java/org/apache/cassandra/index/Index.java b/src/java/org/apache/cassandra/index/Index.java
index 843009b..f3c6df5 100644
--- a/src/java/org/apache/cassandra/index/Index.java
+++ b/src/java/org/apache/cassandra/index/Index.java
@@ -20,13 +20,16 @@
  */
 package org.apache.cassandra.index;
 
+import java.util.Collection;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.function.BiFunction;
 
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.Operator;
 import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.db.filter.RowFilter;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.partitions.PartitionIterator;
@@ -34,7 +37,12 @@
 import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
 import org.apache.cassandra.db.rows.Row;
 import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.index.internal.CollatedViewIndexBuilder;
 import org.apache.cassandra.index.transactions.IndexTransaction;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.ReducingKeyIterator;
+import org.apache.cassandra.io.sstable.format.SSTableFlushObserver;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.schema.IndexMetadata;
 import org.apache.cassandra.utils.concurrent.OpOrder;
 
@@ -62,12 +70,12 @@
  * SecondaryIndexManager. It includes methods for registering and un-registering an index, performing maintenance
  * tasks such as (re)building an index from SSTable data, flushing, invalidating and so forth, as well as some to
  * retrieve general metadata about the index (index name, any internal tables used for persistence etc).
- * Several of these maintenance functions have a return type of Callable<?>; the expectation for these methods is
+ * Several of these maintenance functions have a return type of {@code Callable<?>}; the expectation for these methods is
  * that any work required to be performed by the method be done inside the Callable so that the responsibility for
  * scheduling its execution can rest with SecondaryIndexManager. For instance, a task like reloading index metadata
  * following potential updates caused by modifications to the base table may be performed in a blocking way. In
  * contrast, adding a new index may require it to be built from existing SSTable data, a potentially expensive task
- * which should be performed asyncronously.
+ * which should be performed asynchronously.
  *
  * Index Selection:
  * There are two facets to index selection, write time and read time selection. The former is concerned with
@@ -100,10 +108,10 @@
  * whether any of them are supported by a registered Index. supportsExpression is used to filter out Indexes which
  * cannot support a given Expression. After filtering, the set of candidate indexes are ranked according to the result
  * of getEstimatedResultRows and the most selective (i.e. the one expected to return the smallest number of results) is
- * chosen. A Searcher instance is then obtained from the searcherFor method & used to perform the actual Index lookup.
+ * chosen. A Searcher instance is then obtained from the searcherFor method and used to perform the actual Index lookup.
  * Finally, Indexes can define a post processing step to be performed on the coordinator, after results (partitions from
  * the primary table) have been received from replicas and reconciled. This post processing is defined as a
- * java.util.functions.BiFunction<PartitionIterator, RowFilter, PartitionIterator>, that is a function which takes as
+ * {@code java.util.functions.BiFunction<PartitionIterator, RowFilter, PartitionIterator>}, that is a function which takes as
  * arguments a PartitionIterator (containing the reconciled result rows) and a RowFilter (from the ReadCommand being
  * executed) and returns another iterator of partitions, possibly having transformed the initial results in some way.
  * The post processing function is obtained from the Index's postProcessorFor method; the built-in indexes which ship
@@ -111,7 +119,7 @@
  *
  * An optional static method may be provided to validate custom index options (two variants are supported):
  *
- * <pre>{@code public static Map<String, String> validateOptions(Map<String, String> options);</pre>
+ * <pre>{@code public static Map<String, String> validateOptions(Map<String, String> options);}</pre>
  *
  * The input is the map of index options supplied in the WITH clause of a CREATE INDEX statement.
  *
@@ -130,10 +138,56 @@
 {
 
     /*
+     * Helpers for building indexes from SSTable data
+     */
+
+    /**
+     * Provider of {@code SecondaryIndexBuilder} instances. See {@code getBuildTaskSupport} and
+     * {@code SecondaryIndexManager.buildIndexesBlocking} for more detail.
+     */
+    interface IndexBuildingSupport
+    {
+        SecondaryIndexBuilder getIndexBuildTask(ColumnFamilyStore cfs, Set<Index> indexes, Collection<SSTableReader> sstables);
+    }
+
+    /**
+     * Default implementation of {@code IndexBuildingSupport} which uses a {@code ReducingKeyIterator} to obtain a
+     * collated view of the data in the SSTables.
+     */
+    public static class CollatedViewIndexBuildingSupport implements IndexBuildingSupport
+    {
+        public SecondaryIndexBuilder getIndexBuildTask(ColumnFamilyStore cfs, Set<Index> indexes, Collection<SSTableReader> sstables)
+        {
+            return new CollatedViewIndexBuilder(cfs, indexes, new ReducingKeyIterator(sstables));
+        }
+    }
+
+    /**
+     * Singleton instance of {@code CollatedViewIndexBuildingSupport}, which may be used by any {@code Index}
+     * implementation.
+     */
+    public static final CollatedViewIndexBuildingSupport INDEX_BUILDER_SUPPORT = new CollatedViewIndexBuildingSupport();
+
+    /*
      * Management functions
      */
 
     /**
+     * Get an instance of a helper to provide tasks for building the index from a set of SSTable data.
+     * When processing a number of indexes to be rebuilt, {@code SecondaryIndexManager.buildIndexesBlocking} groups
+     * those with the same {@code IndexBuildingSupport} instance, allowing multiple indexes to be built with a
+     * single pass through the data. The singleton instance returned from the default method implementation builds
+     * indexes using a {@code ReducingKeyIterator} to provide a collated view of the SSTable data.
+     *
+     * @return an instance of the index build taski helper. Index implementations which return <b>the same instance</b>
+     * will be built using a single task.
+     */
+    default IndexBuildingSupport getBuildTaskSupport()
+    {
+        return INDEX_BUILDER_SUPPORT;
+    }
+
+    /**
      * Return a task to perform any initialization work when a new index instance is created.
      * This may involve costly operations such as (re)building the index, and is performed asynchronously
      * by SecondaryIndexManager
@@ -199,17 +253,40 @@
     public Callable<?> getTruncateTask(long truncatedAt);
 
     /**
+     * Return a task to be executed before the node enters NORMAL state and finally joins the ring.
+     *
+     * @param hadBootstrap If the node had bootstrap before joining.
+     * @return task to be executed by the index manager before joining the ring.
+     */
+    default public Callable<?> getPreJoinTask(boolean hadBootstrap)
+    {
+        return null;
+    }
+
+    /**
      * Return true if this index can be built or rebuilt when the index manager determines it is necessary. Returning
      * false enables the index implementation (or some other component) to control if and when SSTable data is
      * incorporated into the index.
      *
-     * This is called by SecondaryIndexManager in buildIndexBlocking, buildAllIndexesBlocking & rebuildIndexesBlocking
+     * This is called by SecondaryIndexManager in buildIndexBlocking, buildAllIndexesBlocking and rebuildIndexesBlocking
      * where a return value of false causes the index to be exluded from the set of those which will process the
      * SSTable data.
      * @return if the index should be included in the set which processes SSTable data, false otherwise.
      */
     public boolean shouldBuildBlocking();
 
+    /**
+     * Get flush observer to observe partition/cell events generated by flushing SSTable (memtable flush or compaction).
+     *
+     * @param descriptor The descriptor of the sstable observer is requested for.
+     * @param opType The type of the operation which requests observer e.g. memtable flush or compaction.
+     *
+     * @return SSTable flush observer.
+     */
+    default SSTableFlushObserver getFlushObserver(Descriptor descriptor, OperationType opType)
+    {
+        return null;
+    }
 
     /*
      * Index selection
@@ -368,7 +445,7 @@
          * Notification of a modification to a row in the base table's Memtable.
          * This is allow an Index implementation to clean up entries for base data which is
          * never flushed to disk (and so will not be purged during compaction).
-         * It's important to note that the old & new rows supplied here may not represent
+         * It's important to note that the old and new rows supplied here may not represent
          * the totality of the data for the Row with this particular Clustering. There may be
          * additional column data in SSTables which is not present in either the old or new row,
          * so implementations should be aware of that.
@@ -460,7 +537,7 @@
      * See CASSANDRA-8717 for further discussion.
      *
      * The function takes a PartitionIterator of the results from the replicas which has already been collated
-     * & reconciled, along with the command being executed. It returns another PartitionIterator containing the results
+     * and reconciled, along with the command being executed. It returns another PartitionIterator containing the results
      * of the transformation (which may be the same as the input if the transformation is a no-op).
      */
     public BiFunction<PartitionIterator, ReadCommand, PartitionIterator> postProcessorFor(ReadCommand command);
@@ -481,9 +558,9 @@
     public interface Searcher
     {
         /**
-         * @param orderGroup the collection of OpOrder.Groups which the ReadCommand is being performed under.
+         * @param executionController the collection of OpOrder.Groups which the ReadCommand is being performed under.
          * @return partitions from the base table matching the criteria of the search.
          */
-        public UnfilteredPartitionIterator search(ReadOrderGroup orderGroup);
+        public UnfilteredPartitionIterator search(ReadExecutionController executionController);
     }
 }
diff --git a/src/java/org/apache/cassandra/index/IndexNotAvailableException.java b/src/java/org/apache/cassandra/index/IndexNotAvailableException.java
index 5440e2a..5e5a753 100644
--- a/src/java/org/apache/cassandra/index/IndexNotAvailableException.java
+++ b/src/java/org/apache/cassandra/index/IndexNotAvailableException.java
@@ -25,7 +25,7 @@
 {
     /**
      * Creates a new <code>IndexNotAvailableException</code> for the specified index.
-     * @param name the index name
+     * @param index the index
      */
     public IndexNotAvailableException(Index index)
     {
diff --git a/src/java/org/apache/cassandra/index/SecondaryIndexBuilder.java b/src/java/org/apache/cassandra/index/SecondaryIndexBuilder.java
index 907f65f..8276626 100644
--- a/src/java/org/apache/cassandra/index/SecondaryIndexBuilder.java
+++ b/src/java/org/apache/cassandra/index/SecondaryIndexBuilder.java
@@ -17,63 +17,14 @@
  */
 package org.apache.cassandra.index;
 
-import java.util.Set;
-import java.util.UUID;
-
-import org.apache.cassandra.db.ColumnFamilyStore;
-import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.compaction.CompactionInfo;
-import org.apache.cassandra.db.compaction.CompactionInterruptedException;
-import org.apache.cassandra.db.compaction.OperationType;
-import org.apache.cassandra.io.sstable.ReducingKeyIterator;
-import org.apache.cassandra.utils.UUIDGen;
 
 /**
  * Manages building an entire index from column family data. Runs on to compaction manager.
  */
-public class SecondaryIndexBuilder extends CompactionInfo.Holder
+public abstract class SecondaryIndexBuilder extends CompactionInfo.Holder
 {
-    private final ColumnFamilyStore cfs;
-    private final Set<Index> indexers;
-    private final ReducingKeyIterator iter;
-    private final UUID compactionId;
-
-    public SecondaryIndexBuilder(ColumnFamilyStore cfs, Set<Index> indexers, ReducingKeyIterator iter)
-    {
-        this.cfs = cfs;
-        this.indexers = indexers;
-        this.iter = iter;
-        this.compactionId = UUIDGen.getTimeUUID();
-    }
-
-    public CompactionInfo getCompactionInfo()
-    {
-        return new CompactionInfo(cfs.metadata,
-                                  OperationType.INDEX_BUILD,
-                                  iter.getBytesRead(),
-                                  iter.getTotalBytes(),
-                                  compactionId);
-    }
-
-    public void build()
-    {
-        try
-        {
-            int pageSize = cfs.indexManager.calculateIndexingPageSize();
-            while (iter.hasNext())
-            {
-                if (isStopRequested())
-                    throw new CompactionInterruptedException(getCompactionInfo());
-                DecoratedKey key = iter.next();
-                cfs.indexManager.indexPartition(key, indexers, pageSize);
-            }
-        }
-        finally
-        {
-            iter.close();
-        }
-    }
-
+    public abstract void build();
     public boolean isGlobal()
     {
         return false;
diff --git a/src/java/org/apache/cassandra/index/SecondaryIndexManager.java b/src/java/org/apache/cassandra/index/SecondaryIndexManager.java
index d66a18b..c603404 100644
--- a/src/java/org/apache/cassandra/index/SecondaryIndexManager.java
+++ b/src/java/org/apache/cassandra/index/SecondaryIndexManager.java
@@ -54,12 +54,12 @@
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.index.internal.CassandraIndex;
 import org.apache.cassandra.index.transactions.*;
-import org.apache.cassandra.io.sstable.ReducingKeyIterator;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.schema.IndexMetadata;
 import org.apache.cassandra.schema.Indexes;
 import org.apache.cassandra.service.pager.SinglePartitionPager;
 import org.apache.cassandra.tracing.Tracing;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.transport.Server;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.concurrent.OpOrder;
@@ -67,18 +67,17 @@
 
 import static org.apache.cassandra.utils.ExecutorUtils.awaitTermination;
 import static org.apache.cassandra.utils.ExecutorUtils.shutdown;
-import static org.apache.cassandra.utils.ExecutorUtils.shutdownNow;
 
 /**
  * Handles the core maintenance functionality associated with indexes: adding/removing them to or from
  * a table, (re)building during bootstrap or other streaming operations, flushing, reloading metadata
  * and so on.
  *
- * The Index interface defines a number of methods which return Callable<?>. These are primarily the
+ * The Index interface defines a number of methods which return {@code Callable<?>}. These are primarily the
  * management tasks for an index implementation. Most of them are currently executed in a blocking
  * fashion via submission to SIM's blockingExecutor. This provides the desired behaviour in pretty
  * much all cases, as tasks like flushing an index needs to be executed synchronously to avoid potentially
- * deadlocking on the FlushWriter or PostFlusher. Several of these Callable<?> returning methods on Index could
+ * deadlocking on the FlushWriter or PostFlusher. Several of these {@code Callable<?>} returning methods on Index could
  * then be defined with as void and called directly from SIM (rather than being run via the executor service).
  * Separating the task defintion from execution gives us greater flexibility though, so that in future, for example,
  * if the flush process allows it we leave open the possibility of executing more of these tasks asynchronously.
@@ -371,11 +370,20 @@
                     indexes.stream().map(i -> i.getIndexMetadata().name).collect(Collectors.joining(",")),
                     sstables.stream().map(SSTableReader::toString).collect(Collectors.joining(",")));
 
-        SecondaryIndexBuilder builder = new SecondaryIndexBuilder(baseCfs,
-                                                                  indexes,
-                                                                  new ReducingKeyIterator(sstables));
-        Future<?> future = CompactionManager.instance.submitIndexBuild(builder);
-        FBUtilities.waitOnFuture(future);
+        Map<Index.IndexBuildingSupport, Set<Index>> byType = new HashMap<>();
+        for (Index index : indexes)
+        {
+            Set<Index> stored = byType.computeIfAbsent(index.getBuildTaskSupport(), i -> new HashSet<>());
+            stored.add(index);
+        }
+
+        List<Future<?>> futures = byType.entrySet()
+                                        .stream()
+                                        .map((e) -> e.getKey().getIndexBuildTask(baseCfs, e.getValue(), sstables))
+                                        .map(CompactionManager.instance::submitIndexBuild)
+                                        .collect(Collectors.toList());
+
+        FBUtilities.waitOnFutures(futures);
 
         flushIndexesBlocking(indexes);
         logger.info("Index build of {} complete",
@@ -421,7 +429,7 @@
             {
                 Class<? extends Index> indexClass = FBUtilities.classForName(className, "Index");
                 Constructor<? extends Index> ctor = indexClass.getConstructor(ColumnFamilyStore.class, IndexMetadata.class);
-                newIndex = (Index)ctor.newInstance(baseCfs, indexDef);
+                newIndex = ctor.newInstance(baseCfs, indexDef);
             }
             catch (Exception e)
             {
@@ -497,6 +505,17 @@
     }
 
     /**
+     * Performs a blocking execution of pre-join tasks of all indexes
+     */
+    public void executePreJoinTasksBlocking(boolean hadBootstrap)
+    {
+        logger.info("Executing pre-join{} tasks for: {}", hadBootstrap ? " post-bootstrap" : "", this.baseCfs);
+        executeAllBlocking(indexes.values().stream(), (index) -> {
+            return index.getPreJoinTask(hadBootstrap);
+        });
+    }
+
+    /**
      * @return all indexes which are marked as built and ready to use
      */
     public List<String> getBuiltIndexNames()
@@ -542,12 +561,12 @@
             int nowInSec = cmd.nowInSec();
             boolean readStatic = false;
 
-            SinglePartitionPager pager = new SinglePartitionPager(cmd, null, Server.CURRENT_VERSION);
+            SinglePartitionPager pager = new SinglePartitionPager(cmd, null, ProtocolVersion.CURRENT);
             while (!pager.isExhausted())
             {
-                try (ReadOrderGroup readGroup = cmd.startOrderGroup();
+                try (ReadExecutionController controller = cmd.executionController();
                      OpOrder.Group writeGroup = Keyspace.writeOrder.start();
-                     UnfilteredPartitionIterator page = pager.fetchPageUnfiltered(baseCfs.metadata, pageSize, readGroup))
+                     UnfilteredPartitionIterator page = pager.fetchPageUnfiltered(baseCfs.metadata, pageSize, controller))
                 {
                     if (!page.hasNext())
                         break;
@@ -655,7 +674,7 @@
      * Delete all data from all indexes for this partition.
      * For when cleanup rips a partition out entirely.
      *
-     * TODO : improve cleanup transaction to batch updates & perform them async
+     * TODO : improve cleanup transaction to batch updates and perform them async
      */
     public void deletePartition(UnfilteredRowIterator partition, int nowInSec)
     {
@@ -726,7 +745,7 @@
                 Tracing.trace("Command contains a custom index expression, using target index {}", customExpression.getTargetIndex().name);
                 return indexes.get(customExpression.getTargetIndex().name);
             }
-            else
+            else if (!expression.isUserDefined())
             {
                 indexes.values().stream()
                        .filter(index -> index.supportsExpression(expression.column(), expression.operator()))
@@ -759,6 +778,11 @@
         return selected;
     }
 
+    public Optional<Index> getBestIndexFor(RowFilter.Expression expression)
+    {
+        return indexes.values().stream().filter((i) -> i.supportsExpression(expression.column(), expression.operator())).findFirst();
+    }
+
     /**
      * Called at write time to ensure that values present in the update
      * are valid according to the rules of all registered indexes which
@@ -1139,6 +1163,12 @@
 
     private static void executeAllBlocking(Stream<Index> indexers, Function<Index, Callable<?>> function)
     {
+        if (function == null)
+        {
+            logger.error("failed to flush indexes: {} because flush task is missing.", indexers);
+            return;
+        }
+
         List<Future<?>> waitFor = new ArrayList<>();
         indexers.forEach(indexer -> {
             Callable<?> task = function.apply(indexer);
diff --git a/src/java/org/apache/cassandra/index/TargetParser.java b/src/java/org/apache/cassandra/index/TargetParser.java
new file mode 100644
index 0000000..96d03af
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/TargetParser.java
@@ -0,0 +1,93 @@
+/*
+ * 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.index;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.statements.IndexTarget;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.utils.Pair;
+
+public class TargetParser
+{
+    private static final Pattern TARGET_REGEX = Pattern.compile("^(keys|entries|values|full)\\((.+)\\)$");
+    private static final Pattern TWO_QUOTES = Pattern.compile("\"\"");
+    private static final String QUOTE = "\"";
+
+    public static Pair<ColumnDefinition, IndexTarget.Type> parse(CFMetaData cfm, IndexMetadata indexDef)
+    {
+        String target = indexDef.options.get("target");
+        assert target != null : String.format("No target definition found for index %s", indexDef.name);
+        Pair<ColumnDefinition, IndexTarget.Type> result = parse(cfm, target);
+        if (result == null)
+            throw new ConfigurationException(String.format("Unable to parse targets for index %s (%s)", indexDef.name, target));
+        return result;
+    }
+
+    public static Pair<ColumnDefinition, IndexTarget.Type> parse(CFMetaData cfm, String target)
+    {
+        // if the regex matches then the target is in the form "keys(foo)", "entries(bar)" etc
+        // if not, then it must be a simple column name and implictly its type is VALUES
+        Matcher matcher = TARGET_REGEX.matcher(target);
+        String columnName;
+        IndexTarget.Type targetType;
+        if (matcher.matches())
+        {
+            targetType = IndexTarget.Type.fromString(matcher.group(1));
+            columnName = matcher.group(2);
+        }
+        else
+        {
+            columnName = target;
+            targetType = IndexTarget.Type.VALUES;
+        }
+
+        // in the case of a quoted column name the name in the target string
+        // will be enclosed in quotes, which we need to unwrap. It may also
+        // include quote characters internally, escaped like so:
+        //      abc"def -> abc""def.
+        // Because the target string is stored in a CQL compatible form, we
+        // need to un-escape any such quotes to get the actual column name
+        if (columnName.startsWith(QUOTE))
+        {
+            columnName = StringUtils.substring(StringUtils.substring(columnName, 1), 0, -1);
+            columnName = TWO_QUOTES.matcher(columnName).replaceAll(QUOTE);
+        }
+
+        // if it's not a CQL table, we can't assume that the column name is utf8, so
+        // in that case we have to do a linear scan of the cfm's columns to get the matching one.
+        // After dropping compact storage (see CASSANDRA-10857), we can't distinguish between the
+        // former compact/thrift table, so we have to fall back to linear scan in both cases.
+        ColumnDefinition cd = cfm.getColumnDefinition(new ColumnIdentifier(columnName, true));
+        if (cd != null)
+            return Pair.create(cd, targetType);
+
+        for (ColumnDefinition column : cfm.allColumns())
+            if (column.name.toString().equals(columnName))
+                return Pair.create(column, targetType);
+
+        return null;
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/internal/CassandraIndex.java b/src/java/org/apache/cassandra/index/internal/CassandraIndex.java
index de3a974..27b60e5 100644
--- a/src/java/org/apache/cassandra/index/internal/CassandraIndex.java
+++ b/src/java/org/apache/cassandra/index/internal/CassandraIndex.java
@@ -25,19 +25,16 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.Future;
 import java.util.function.BiFunction;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
 
 import com.google.common.collect.ImmutableSet;
-import org.apache.commons.lang3.StringUtils;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.cql3.Operator;
 import org.apache.cassandra.cql3.statements.IndexTarget;
 import org.apache.cassandra.db.*;
@@ -52,11 +49,8 @@
 import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.dht.LocalPartitioner;
-import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.index.Index;
-import org.apache.cassandra.index.IndexRegistry;
-import org.apache.cassandra.index.SecondaryIndexBuilder;
+import org.apache.cassandra.index.*;
 import org.apache.cassandra.index.internal.composites.CompositesSearcher;
 import org.apache.cassandra.index.internal.keys.KeysSearcher;
 import org.apache.cassandra.index.transactions.IndexTransaction;
@@ -79,8 +73,6 @@
 {
     private static final Logger logger = LoggerFactory.getLogger(CassandraIndex.class);
 
-    public static final Pattern TARGET_REGEX = Pattern.compile("^(keys|entries|values|full)\\((.+)\\)$");
-
     public final ColumnFamilyStore baseCfs;
     protected IndexMetadata metadata;
     protected ColumnFamilyStore indexCfs;
@@ -231,7 +223,7 @@
     private void setMetadata(IndexMetadata indexDef)
     {
         metadata = indexDef;
-        Pair<ColumnDefinition, IndexTarget.Type> target = parseTarget(baseCfs.metadata, indexDef);
+        Pair<ColumnDefinition, IndexTarget.Type> target = TargetParser.parse(baseCfs.metadata, indexDef);
         functions = getFunctions(indexDef, target);
         CFMetaData cfm = indexCfsMetadata(baseCfs.metadata, indexDef);
         indexCfs = ColumnFamilyStore.createColumnFamilyStore(baseCfs.keyspace,
@@ -325,7 +317,6 @@
 
         if (target.isPresent())
         {
-            target.get().validateForIndexing();
             switch (getIndexMetadata().kind)
             {
                 case COMPOSITES:
@@ -472,7 +463,7 @@
                 insert(key.getKey(),
                        clustering,
                        cell,
-                       LivenessInfo.create(cell.timestamp(), cell.ttl(), cell.localDeletionTime()),
+                       LivenessInfo.withExpirationTime(cell.timestamp(), cell.ttl(), cell.localDeletionTime()),
                        opGroup);
             }
 
@@ -517,7 +508,7 @@
                             timestamp = cellTimestamp;
                     }
                 }
-                return LivenessInfo.create(baseCfs.metadata, timestamp, ttl, nowInSec);
+                return LivenessInfo.create(timestamp, ttl, nowInSec);
             }
         };
     }
@@ -733,9 +724,9 @@
                         metadata.name,
                         getSSTableNames(sstables));
 
-            SecondaryIndexBuilder builder = new SecondaryIndexBuilder(baseCfs,
-                                                                      Collections.singleton(this),
-                                                                      new ReducingKeyIterator(sstables));
+            SecondaryIndexBuilder builder = new CollatedViewIndexBuilder(baseCfs,
+                                                                         Collections.singleton(this),
+                                                                         new ReducingKeyIterator(sstables));
             Future<?> future = CompactionManager.instance.submitIndexBuild(builder);
             FBUtilities.waitOnFuture(future);
             indexCfs.forceBlockingFlush();
@@ -760,7 +751,7 @@
      */
     public static final CFMetaData indexCfsMetadata(CFMetaData baseCfsMetadata, IndexMetadata indexMetadata)
     {
-        Pair<ColumnDefinition, IndexTarget.Type> target = parseTarget(baseCfsMetadata, indexMetadata);
+        Pair<ColumnDefinition, IndexTarget.Type> target = TargetParser.parse(baseCfsMetadata, indexMetadata);
         CassandraIndexFunctions utils = getFunctions(indexMetadata, target);
         ColumnDefinition indexedColumn = target.left;
         AbstractType<?> indexedValueType = utils.getIndexedValueType(indexedColumn);
@@ -804,64 +795,7 @@
      */
     public static CassandraIndex newIndex(ColumnFamilyStore baseCfs, IndexMetadata indexMetadata)
     {
-        return getFunctions(indexMetadata, parseTarget(baseCfs.metadata, indexMetadata)).newIndexInstance(baseCfs, indexMetadata);
-    }
-
-    public static Pair<ColumnDefinition, IndexTarget.Type> parseTarget(CFMetaData cfm, IndexMetadata indexDef)
-    {
-        String target = indexDef.options.get("target");
-        assert target != null : String.format("No target definition found for index %s", indexDef.name);
-        Pair<ColumnDefinition, IndexTarget.Type> result = parseTarget(cfm, target);
-        if (result == null)
-            throw new ConfigurationException(String.format("Unable to parse targets for index %s (%s)", indexDef.name, target));
-        return result;
-    }
-
-    // Public because it's also used to convert index metadata into a thrift-compatible format
-    public static Pair<ColumnDefinition, IndexTarget.Type> parseTarget(CFMetaData cfm,
-                                                                       String target)
-    {
-        // if the regex matches then the target is in the form "keys(foo)", "entries(bar)" etc
-        // if not, then it must be a simple column name and implictly its type is VALUES
-        Matcher matcher = TARGET_REGEX.matcher(target);
-        String columnName;
-        IndexTarget.Type targetType;
-        if (matcher.matches())
-        {
-            targetType = IndexTarget.Type.fromString(matcher.group(1));
-            columnName = matcher.group(2);
-        }
-        else
-        {
-            columnName = target;
-            targetType = IndexTarget.Type.VALUES;
-        }
-
-        // in the case of a quoted column name the name in the target string
-        // will be enclosed in quotes, which we need to unwrap. It may also
-        // include quote characters internally, escaped like so:
-        //      abc"def -> abc""def.
-        // Because the target string is stored in a CQL compatible form, we
-        // need to un-escape any such quotes to get the actual column name
-        if (columnName.startsWith("\""))
-        {
-            columnName = StringUtils.substring(StringUtils.substring(columnName, 1), 0, -1);
-            columnName = columnName.replaceAll("\"\"", "\"");
-        }
-
-        // if it's not a CQL table, we can't assume that the column name is utf8, so
-        // in that case we have to do a linear scan of the cfm's columns to get the matching one.
-        // After dropping compact storage (see CASSANDRA-10857), we can't distinguish between the
-        // former compact/thrift table, so we have to fall back to linear scan in both cases.
-        ColumnDefinition cd = cfm.getColumnDefinition(new ColumnIdentifier(columnName, true));
-        if (cd != null)
-            return Pair.create(cd, targetType);
-
-        for (ColumnDefinition column : cfm.allColumns())
-            if (column.name.toString().equals(columnName))
-                return Pair.create(column, targetType);
-
-        return null;
+        return getFunctions(indexMetadata, TargetParser.parse(baseCfs.metadata, indexMetadata)).newIndexInstance(baseCfs, indexMetadata);
     }
 
     static CassandraIndexFunctions getFunctions(IndexMetadata indexDef,
@@ -898,6 +832,7 @@
             case CLUSTERING:
                 return CassandraIndexFunctions.CLUSTERING_COLUMN_INDEX_FUNCTIONS;
             case REGULAR:
+            case STATIC:
                 return CassandraIndexFunctions.REGULAR_COLUMN_INDEX_FUNCTIONS;
             case PARTITION_KEY:
                 return CassandraIndexFunctions.PARTITION_KEY_INDEX_FUNCTIONS;
diff --git a/src/java/org/apache/cassandra/index/internal/CassandraIndexFunctions.java b/src/java/org/apache/cassandra/index/internal/CassandraIndexFunctions.java
index b7cb3a2..89eebdf 100644
--- a/src/java/org/apache/cassandra/index/internal/CassandraIndexFunctions.java
+++ b/src/java/org/apache/cassandra/index/internal/CassandraIndexFunctions.java
@@ -54,7 +54,7 @@
     /**
      * Add the clustering columns for a specific type of index table to the a CFMetaData.Builder (which is being
      * used to construct the index table's CFMetadata. In the default implementation, the clustering columns of the
-     * index table hold the partition key & clustering columns of the base table. This is overridden in several cases:
+     * index table hold the partition key and clustering columns of the base table. This is overridden in several cases:
      * * When the indexed value is itself a clustering column, in which case, we only need store the base table's
      *   *other* clustering values in the index - the indexed value being the index table's partition key
      * * When the indexed value is a collection value, in which case we also need to capture the cell path from the base
diff --git a/src/java/org/apache/cassandra/index/internal/CassandraIndexSearcher.java b/src/java/org/apache/cassandra/index/internal/CassandraIndexSearcher.java
index d6b39e6..7c23345 100644
--- a/src/java/org/apache/cassandra/index/internal/CassandraIndexSearcher.java
+++ b/src/java/org/apache/cassandra/index/internal/CassandraIndexSearcher.java
@@ -56,14 +56,14 @@
 
     @SuppressWarnings("resource") // Both the OpOrder and 'indexIter' are closed on exception, or through the closing of the result
     // of this method.
-    public UnfilteredPartitionIterator search(ReadOrderGroup orderGroup)
+    public UnfilteredPartitionIterator search(ReadExecutionController executionController)
     {
         // the value of the index expression is the partition key in the index table
         DecoratedKey indexKey = index.getBackingTable().get().decorateKey(expression.getIndexValue());
-        UnfilteredRowIterator indexIter = queryIndex(indexKey, command, orderGroup);
+        UnfilteredRowIterator indexIter = queryIndex(indexKey, command, executionController);
         try
         {
-            return queryDataFromIndex(indexKey, UnfilteredRowIterators.filter(indexIter, command.nowInSec()), command, orderGroup);
+            return queryDataFromIndex(indexKey, UnfilteredRowIterators.filter(indexIter, command.nowInSec()), command, executionController);
         }
         catch (RuntimeException | Error e)
         {
@@ -72,13 +72,13 @@
         }
     }
 
-    private UnfilteredRowIterator queryIndex(DecoratedKey indexKey, ReadCommand command, ReadOrderGroup orderGroup)
+    private UnfilteredRowIterator queryIndex(DecoratedKey indexKey, ReadCommand command, ReadExecutionController executionController)
     {
         ClusteringIndexFilter filter = makeIndexFilter(command);
         ColumnFamilyStore indexCfs = index.getBackingTable().get();
         CFMetaData indexCfm = indexCfs.metadata;
         return SinglePartitionReadCommand.create(indexCfm, command.nowInSec(), indexKey, ColumnFilter.all(indexCfm), filter)
-                                         .queryMemtableAndDisk(indexCfs, orderGroup.indexReadOpOrderGroup());
+                                         .queryMemtableAndDisk(indexCfs, executionController.indexReadController());
     }
 
     private ClusteringIndexFilter makeIndexFilter(ReadCommand command)
@@ -132,14 +132,15 @@
                     DecoratedKey startKey = (DecoratedKey) range.left;
                     DecoratedKey endKey = (DecoratedKey) range.right;
 
-                    Slice.Bound start = Slice.Bound.BOTTOM;
-                    Slice.Bound end = Slice.Bound.TOP;
+                    ClusteringBound start = ClusteringBound.BOTTOM;
+                    ClusteringBound end = ClusteringBound.TOP;
 
                     /*
-                     * For index queries over a range, we can't do a whole lot better than querying everything for the key range, though for
-                     * slice queries where we can slightly restrict the beginning and end.
+                     * For index queries over a range, we can't do a whole lot better than querying everything for the
+                     * key range, though for slice queries where we can slightly restrict the beginning and end. We can
+                     * not do this optimisation for static column indexes.
                      */
-                    if (!dataRange.isNamesQuery())
+                    if (!dataRange.isNamesQuery() && !index.indexedColumn.isStatic())
                     {
                         ClusteringIndexSliceFilter startSliceFilter = ((ClusteringIndexSliceFilter) dataRange.clusteringIndexFilter(
                                                                                                                                    startKey));
@@ -166,15 +167,15 @@
                 else
                 {
                     // otherwise, just start the index slice from the key we do have
-                    slice = Slice.make(makeIndexBound(((DecoratedKey)range.left).getKey(), Slice.Bound.BOTTOM),
-                                       Slice.Bound.TOP);
+                    slice = Slice.make(makeIndexBound(((DecoratedKey)range.left).getKey(), ClusteringBound.BOTTOM),
+                                       ClusteringBound.TOP);
                 }
             }
             return new ClusteringIndexSliceFilter(Slices.with(index.getIndexComparator(), slice), false);
         }
     }
 
-    private Slice.Bound makeIndexBound(ByteBuffer rowKey, Slice.Bound bound)
+    private ClusteringBound makeIndexBound(ByteBuffer rowKey, ClusteringBound bound)
     {
         return index.buildIndexClusteringPrefix(rowKey, bound, null)
                                  .buildBound(bound.isStart(), bound.isInclusive());
@@ -188,5 +189,5 @@
     protected abstract UnfilteredPartitionIterator queryDataFromIndex(DecoratedKey indexKey,
                                                                       RowIterator indexHits,
                                                                       ReadCommand command,
-                                                                      ReadOrderGroup orderGroup);
+                                                                      ReadExecutionController executionController);
 }
diff --git a/src/java/org/apache/cassandra/index/internal/CollatedViewIndexBuilder.java b/src/java/org/apache/cassandra/index/internal/CollatedViewIndexBuilder.java
new file mode 100644
index 0000000..811d857
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/internal/CollatedViewIndexBuilder.java
@@ -0,0 +1,79 @@
+/*
+ * 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.index.internal;
+
+import java.util.Set;
+import java.util.UUID;
+
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.compaction.CompactionInfo;
+import org.apache.cassandra.db.compaction.CompactionInterruptedException;
+import org.apache.cassandra.db.compaction.OperationType;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.index.SecondaryIndexBuilder;
+import org.apache.cassandra.io.sstable.ReducingKeyIterator;
+import org.apache.cassandra.utils.UUIDGen;
+
+/**
+ * Manages building an entire index from column family data. Runs on to compaction manager.
+ */
+public class CollatedViewIndexBuilder extends SecondaryIndexBuilder
+{
+    private final ColumnFamilyStore cfs;
+    private final Set<Index> indexers;
+    private final ReducingKeyIterator iter;
+    private final UUID compactionId;
+
+    public CollatedViewIndexBuilder(ColumnFamilyStore cfs, Set<Index> indexers, ReducingKeyIterator iter)
+    {
+        this.cfs = cfs;
+        this.indexers = indexers;
+        this.iter = iter;
+        this.compactionId = UUIDGen.getTimeUUID();
+    }
+
+    public CompactionInfo getCompactionInfo()
+    {
+        return new CompactionInfo(cfs.metadata,
+                OperationType.INDEX_BUILD,
+                iter.getBytesRead(),
+                iter.getTotalBytes(),
+                compactionId);
+    }
+
+    public void build()
+    {
+        try
+        {
+            int pageSize = cfs.indexManager.calculateIndexingPageSize();
+            while (iter.hasNext())
+            {
+                if (isStopRequested())
+                    throw new CompactionInterruptedException(getCompactionInfo());
+                DecoratedKey key = iter.next();
+                cfs.indexManager.indexPartition(key, indexers, pageSize);
+            }
+        }
+        finally
+        {
+            iter.close();
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/internal/composites/ClusteringColumnIndex.java b/src/java/org/apache/cassandra/index/internal/composites/ClusteringColumnIndex.java
index cace6de..d1917f9 100644
--- a/src/java/org/apache/cassandra/index/internal/composites/ClusteringColumnIndex.java
+++ b/src/java/org/apache/cassandra/index/internal/composites/ClusteringColumnIndex.java
@@ -36,12 +36,14 @@
  * has no impact) and v the cell value.
  *
  * Such a cell is always indexed by this index (or rather, it is indexed if
+ * {@code 
  * n >= columnDef.componentIndex, which will always be the case in practice)
  * and it will generate (makeIndexColumnName()) an index entry whose:
  *   - row key will be ck_i (getIndexedValue()) where i == columnDef.componentIndex.
  *   - cell name will
  *       rk ck_0 ... ck_{i-1} ck_{i+1} ck_n
  *     where rk is the row key of the initial cell and i == columnDef.componentIndex.
+ * }
  */
 public class ClusteringColumnIndex extends CassandraIndex
 {
diff --git a/src/java/org/apache/cassandra/index/internal/composites/CollectionKeyIndexBase.java b/src/java/org/apache/cassandra/index/internal/composites/CollectionKeyIndexBase.java
index fe77c96..ef76870 100644
--- a/src/java/org/apache/cassandra/index/internal/composites/CollectionKeyIndexBase.java
+++ b/src/java/org/apache/cassandra/index/internal/composites/CollectionKeyIndexBase.java
@@ -54,6 +54,9 @@
     {
         CBuilder builder = CBuilder.create(getIndexComparator());
         builder.add(partitionKey);
+
+        // When indexing a static column, prefix will be empty but only the
+        // partition key is needed at query time.
         for (int i = 0; i < prefix.size(); i++)
             builder.add(prefix.get(i));
 
@@ -63,16 +66,24 @@
     public IndexEntry decodeEntry(DecoratedKey indexedValue,
                                   Row indexEntry)
     {
-        int count = 1 + baseCfs.metadata.clusteringColumns().size();
         Clustering clustering = indexEntry.clustering();
-        CBuilder builder = CBuilder.create(baseCfs.getComparator());
-        for (int i = 0; i < count - 1; i++)
-            builder.add(clustering.get(i + 1));
+
+        Clustering indexedEntryClustering = null;
+        if (getIndexedColumn().isStatic())
+            indexedEntryClustering = Clustering.STATIC_CLUSTERING;
+        else
+        {
+            int count = 1 + baseCfs.metadata.clusteringColumns().size();
+            CBuilder builder = CBuilder.create(baseCfs.getComparator());
+            for (int i = 0; i < count - 1; i++)
+                builder.add(clustering.get(i + 1));
+            indexedEntryClustering = builder.build();
+        }
 
         return new IndexEntry(indexedValue,
                               clustering,
                               indexEntry.primaryKeyLivenessInfo().timestamp(),
                               clustering.get(0),
-                              builder.build());
+                              indexedEntryClustering);
     }
 }
diff --git a/src/java/org/apache/cassandra/index/internal/composites/CollectionValueIndex.java b/src/java/org/apache/cassandra/index/internal/composites/CollectionValueIndex.java
index 95bd7e1..5929e69 100644
--- a/src/java/org/apache/cassandra/index/internal/composites/CollectionValueIndex.java
+++ b/src/java/org/apache/cassandra/index/internal/composites/CollectionValueIndex.java
@@ -63,7 +63,10 @@
         for (int i = 0; i < prefix.size(); i++)
             builder.add(prefix.get(i));
 
-        // When indexing, cell will be present, but when searching, it won't  (CASSANDRA-7525)
+        // When indexing a static column, prefix will be empty but only the
+        // partition key is needed at query time.
+        // In the non-static case, cell will be present during indexing but
+        // not when searching (CASSANDRA-7525).
         if (prefix.size() == baseCfs.metadata.clusteringColumns().size() && path != null)
             builder.add(path.get(0));
 
@@ -73,15 +76,22 @@
     public IndexEntry decodeEntry(DecoratedKey indexedValue, Row indexEntry)
     {
         Clustering clustering = indexEntry.clustering();
-        CBuilder builder = CBuilder.create(baseCfs.getComparator());
-        for (int i = 0; i < baseCfs.getComparator().size(); i++)
-            builder.add(clustering.get(i + 1));
+        Clustering indexedEntryClustering = null;
+        if (getIndexedColumn().isStatic())
+            indexedEntryClustering = Clustering.STATIC_CLUSTERING;
+        else
+        {
+            CBuilder builder = CBuilder.create(baseCfs.getComparator());
+            for (int i = 0; i < baseCfs.getComparator().size(); i++)
+                builder.add(clustering.get(i + 1));
+            indexedEntryClustering = builder.build();
+        }
 
         return new IndexEntry(indexedValue,
                                 clustering,
                                 indexEntry.primaryKeyLivenessInfo().timestamp(),
                                 clustering.get(0),
-                                builder.build());
+                                indexedEntryClustering);
     }
 
     public boolean supportsOperator(ColumnDefinition indexedColumn, Operator operator)
diff --git a/src/java/org/apache/cassandra/index/internal/composites/CompositesSearcher.java b/src/java/org/apache/cassandra/index/internal/composites/CompositesSearcher.java
index 6bb9869..4429027 100644
--- a/src/java/org/apache/cassandra/index/internal/composites/CompositesSearcher.java
+++ b/src/java/org/apache/cassandra/index/internal/composites/CompositesSearcher.java
@@ -21,12 +21,10 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter;
+import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter;
 import org.apache.cassandra.db.filter.DataLimits;
 import org.apache.cassandra.db.filter.RowFilter;
 import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
@@ -41,8 +39,6 @@
 
 public class CompositesSearcher extends CassandraIndexSearcher
 {
-    private static final Logger logger = LoggerFactory.getLogger(CompositesSearcher.class);
-
     public CompositesSearcher(ReadCommand command,
                               RowFilter.Expression expression,
                               CassandraIndex index)
@@ -55,10 +51,15 @@
         return command.selectsKey(partitionKey) && command.selectsClustering(partitionKey, entry.indexedEntryClustering);
     }
 
+    private boolean isStaticColumn()
+    {
+        return index.getIndexedColumn().isStatic();
+    }
+
     protected UnfilteredPartitionIterator queryDataFromIndex(final DecoratedKey indexKey,
                                                              final RowIterator indexHits,
                                                              final ReadCommand command,
-                                                             final ReadOrderGroup orderGroup)
+                                                             final ReadExecutionController executionController)
     {
         assert indexHits.staticRow() == Rows.EMPTY_STATIC_ROW;
 
@@ -108,51 +109,74 @@
                         nextEntry = index.decodeEntry(indexKey, indexHits.next());
                     }
 
-                    // Gather all index hits belonging to the same partition and query the data for those hits.
-                    // TODO: it's much more efficient to do 1 read for all hits to the same partition than doing
-                    // 1 read per index hit. However, this basically mean materializing all hits for a partition
-                    // in memory so we should consider adding some paging mechanism. However, index hits should
-                    // be relatively small so it's much better than the previous code that was materializing all
-                    // *data* for a given partition.
-                    BTreeSet.Builder<Clustering> clusterings = BTreeSet.builder(index.baseCfs.getComparator());
-                    List<IndexEntry> entries = new ArrayList<>();
+                    SinglePartitionReadCommand dataCmd;
                     DecoratedKey partitionKey = index.baseCfs.decorateKey(nextEntry.indexedKey);
-
-                    while (nextEntry != null && partitionKey.getKey().equals(nextEntry.indexedKey))
+                    List<IndexEntry> entries = new ArrayList<>();
+                    if (isStaticColumn())
                     {
-                        // We're queried a slice of the index, but some hits may not match some of the clustering column constraints
-                        if (isMatchingEntry(partitionKey, nextEntry, command))
-                        {
-                            clusterings.add(nextEntry.indexedEntryClustering);
-                            entries.add(nextEntry);
+                        // The index hit may not match the commad key constraint
+                        if (!isMatchingEntry(partitionKey, nextEntry, command)) {
+                            nextEntry = indexHits.hasNext() ? index.decodeEntry(indexKey, indexHits.next()) : null;
+                            continue;
                         }
 
+                        // If the index is on a static column, we just need to do a full read on the partition.
+                        // Note that we want to re-use the command.columnFilter() in case of future change.
+                        dataCmd = SinglePartitionReadCommand.create(index.baseCfs.metadata,
+                                                                    command.nowInSec(),
+                                                                    command.columnFilter(),
+                                                                    RowFilter.NONE,
+                                                                    DataLimits.NONE,
+                                                                    partitionKey,
+                                                                    command.clusteringIndexFilter(partitionKey));
+                        entries.add(nextEntry);
                         nextEntry = indexHits.hasNext() ? index.decodeEntry(indexKey, indexHits.next()) : null;
                     }
+                    else
+                    {
+                        // Gather all index hits belonging to the same partition and query the data for those hits.
+                        // TODO: it's much more efficient to do 1 read for all hits to the same partition than doing
+                        // 1 read per index hit. However, this basically mean materializing all hits for a partition
+                        // in memory so we should consider adding some paging mechanism. However, index hits should
+                        // be relatively small so it's much better than the previous code that was materializing all
+                        // *data* for a given partition.
+                        BTreeSet.Builder<Clustering> clusterings = BTreeSet.builder(index.baseCfs.getComparator());
+                        while (nextEntry != null && partitionKey.getKey().equals(nextEntry.indexedKey))
+                        {
+                            // We're queried a slice of the index, but some hits may not match some of the clustering column constraints
+                            if (isMatchingEntry(partitionKey, nextEntry, command))
+                            {
+                                clusterings.add(nextEntry.indexedEntryClustering);
+                                entries.add(nextEntry);
+                            }
 
-                    // Because we've eliminated entries that don't match the clustering columns, it's possible we added nothing
-                    if (clusterings.isEmpty())
-                        continue;
+                            nextEntry = indexHits.hasNext() ? index.decodeEntry(indexKey, indexHits.next()) : null;
+                        }
 
-                    // Query the gathered index hits. We still need to filter stale hits from the resulting query.
-                    ClusteringIndexNamesFilter filter = new ClusteringIndexNamesFilter(clusterings.build(), false);
-                    SinglePartitionReadCommand dataCmd = SinglePartitionReadCommand.create(isForThrift(),
-                                                                                           index.baseCfs.metadata,
-                                                                                           command.nowInSec(),
-                                                                                           command.columnFilter(),
-                                                                                           command.rowFilter(),
-                                                                                           DataLimits.NONE,
-                                                                                           partitionKey,
-                                                                                           filter,
-                                                                                           null);
+                        // Because we've eliminated entries that don't match the clustering columns, it's possible we added nothing
+                        if (clusterings.isEmpty())
+                            continue;
+
+                        // Query the gathered index hits. We still need to filter stale hits from the resulting query.
+                        ClusteringIndexNamesFilter filter = new ClusteringIndexNamesFilter(clusterings.build(), false);
+                        dataCmd = SinglePartitionReadCommand.create(isForThrift(),
+                                                                    index.baseCfs.metadata,
+                                                                    command.nowInSec(),
+                                                                    command.columnFilter(),
+                                                                    command.rowFilter(),
+                                                                    DataLimits.NONE,
+                                                                    partitionKey,
+                                                                    filter,
+                                                                    null);
+                    }
+
                     @SuppressWarnings("resource") // We close right away if empty, and if it's assign to next it will be called either
                     // by the next caller of next, or through closing this iterator is this come before.
                     UnfilteredRowIterator dataIter =
-                        filterStaleEntries(dataCmd.queryMemtableAndDisk(index.baseCfs,
-                                                                        orderGroup.baseReadOpOrderGroup()),
+                        filterStaleEntries(dataCmd.queryMemtableAndDisk(index.baseCfs, executionController),
                                            indexKey.getKey(),
                                            entries,
-                                           orderGroup.writeOpOrderGroup(),
+                                           executionController.writeOpOrderGroup(),
                                            command.nowInSec());
 
                     if (dataIter.isEmpty())
@@ -184,11 +208,12 @@
     {
         entries.forEach(entry ->
             index.deleteStaleEntry(entry.indexValue,
-                                     entry.indexClustering,
-                                     new DeletionTime(entry.timestamp, nowInSec),
-                                     writeOp));
+                                   entry.indexClustering,
+                                   new DeletionTime(entry.timestamp, nowInSec),
+                                   writeOp));
     }
 
+    // We assume all rows in dataIter belong to the same partition.
     private UnfilteredRowIterator filterStaleEntries(UnfilteredRowIterator dataIter,
                                                      final ByteBuffer indexValue,
                                                      final List<IndexEntry> entries,
@@ -209,39 +234,62 @@
             });
         }
 
-        ClusteringComparator comparator = dataIter.metadata().comparator;
-        class Transform extends Transformation
+        UnfilteredRowIterator iteratorToReturn = null;
+        if (isStaticColumn())
         {
-            private int entriesIdx;
+            if (entries.size() != 1)
+                throw new AssertionError("A partition should have at most one index within a static column index");
 
-            @Override
-            public Row applyToRow(Row row)
+            iteratorToReturn = dataIter;
+            if (index.isStale(dataIter.staticRow(), indexValue, nowInSec))
             {
-                IndexEntry entry = findEntry(row.clustering());
-                if (!index.isStale(row, indexValue, nowInSec))
-                    return row;
-
-                staleEntries.add(entry);
-                return null;
+                // The entry is staled, we return no rows in this partition.
+                staleEntries.addAll(entries);
+                iteratorToReturn = UnfilteredRowIterators.noRowsIterator(dataIter.metadata(),
+                                                                         dataIter.partitionKey(),
+                                                                         Rows.EMPTY_STATIC_ROW,
+                                                                         dataIter.partitionLevelDeletion(),
+                                                                         dataIter.isReverseOrder());
             }
+            deleteAllEntries(staleEntries, writeOp, nowInSec);
+        }
+        else
+        {
+            ClusteringComparator comparator = dataIter.metadata().comparator;
 
-            private IndexEntry findEntry(Clustering clustering)
+            class Transform extends Transformation
             {
-                assert entriesIdx < entries.size();
-                while (entriesIdx < entries.size())
+                private int entriesIdx;
+
+                @Override
+                public Row applyToRow(Row row)
                 {
-                    IndexEntry entry = entries.get(entriesIdx++);
-                    Clustering indexedEntryClustering = entry.indexedEntryClustering;
-                    // The entries are in clustering order. So that the requested entry should be the
-                    // next entry, the one at 'entriesIdx'. However, we can have stale entries, entries
-                    // that have no corresponding row in the base table typically because of a range
-                    // tombstone or partition level deletion. Delete such stale entries.
-                    int cmp = comparator.compare(indexedEntryClustering, clustering);
-                    assert cmp <= 0; // this would means entries are not in clustering order, which shouldn't happen
-                    if (cmp == 0)
-                        return entry;
-                    else
+                    IndexEntry entry = findEntry(row.clustering());
+                    if (!index.isStale(row, indexValue, nowInSec))
+                        return row;
+
+                    staleEntries.add(entry);
+                    return null;
+                }
+
+                private IndexEntry findEntry(Clustering clustering)
+                {
+                    assert entriesIdx < entries.size();
+                    while (entriesIdx < entries.size())
                     {
+                        IndexEntry entry = entries.get(entriesIdx++);
+                        Clustering indexedEntryClustering = entry.indexedEntryClustering;
+                        // The entries are in clustering order. So that the requested entry should be the
+                        // next entry, the one at 'entriesIdx'. However, we can have stale entries, entries
+                        // that have no corresponding row in the base table typically because of a range
+                        // tombstone or partition level deletion. Delete such stale entries.
+                        // For static column, we only need to compare the partition key, otherwise we compare
+                        // the whole clustering.
+                        int cmp = comparator.compare(indexedEntryClustering, clustering);
+                        assert cmp <= 0; // this would means entries are not in clustering order, which shouldn't happen
+                        if (cmp == 0)
+                            return entry;
+
                         // COMPACT COMPOSITE tables support null values in there clustering key but
                         // those tables do not support static columns. By consequence if a table
                         // has some static columns and all its clustering key elements are null
@@ -249,25 +297,26 @@
                        if (!dataIter.metadata().hasStaticColumns() || !containsOnlyNullValues(indexedEntryClustering))
                            staleEntries.add(entry);
                     }
+                    // entries correspond to the rows we've queried, so we shouldn't have a row that has no corresponding entry.
+                    throw new AssertionError();
                 }
-                // entries correspond to the rows we've queried, so we shouldn't have a row that has no corresponding entry.
-                throw new AssertionError();
-            }
 
-            private boolean containsOnlyNullValues(Clustering indexedEntryClustering)
-            {
-                int i = 0;
-                for (; i < indexedEntryClustering.size() && indexedEntryClustering.get(i) == null; i++);
-                return i == indexedEntryClustering.size();
-            }
+                private boolean containsOnlyNullValues(Clustering indexedEntryClustering)
+                {
+                    int i = 0;
+                    for (; i < indexedEntryClustering.size() && indexedEntryClustering.get(i) == null; i++);
+                    return i == indexedEntryClustering.size();
+                }
 
-            @Override
-            public void onPartitionClose()
-            {
-                deleteAllEntries(staleEntries, writeOp, nowInSec);
+                @Override
+                public void onPartitionClose()
+                {
+                    deleteAllEntries(staleEntries, writeOp, nowInSec);
+                }
             }
+            iteratorToReturn = Transformation.apply(dataIter, new Transform());
         }
 
-        return Transformation.apply(dataIter, new Transform());
+        return iteratorToReturn;
     }
 }
diff --git a/src/java/org/apache/cassandra/index/internal/composites/RegularColumnIndex.java b/src/java/org/apache/cassandra/index/internal/composites/RegularColumnIndex.java
index f1dc3af..9cbfe03 100644
--- a/src/java/org/apache/cassandra/index/internal/composites/RegularColumnIndex.java
+++ b/src/java/org/apache/cassandra/index/internal/composites/RegularColumnIndex.java
@@ -68,22 +68,34 @@
         for (int i = 0; i < prefix.size(); i++)
             builder.add(prefix.get(i));
 
+        // Note: if indexing a static column, prefix will be Clustering.STATIC_CLUSTERING
+        // so the Clustering obtained from builder::build will contain a value for only
+        // the partition key. At query time though, this is all that's needed as the entire
+        // base table partition should be returned for any mathching index entry.
         return builder;
     }
 
     public IndexEntry decodeEntry(DecoratedKey indexedValue, Row indexEntry)
     {
         Clustering clustering = indexEntry.clustering();
-        ClusteringComparator baseComparator = baseCfs.getComparator();
-        CBuilder builder = CBuilder.create(baseComparator);
-        for (int i = 0; i < baseComparator.size(); i++)
-            builder.add(clustering.get(i + 1));
+
+        Clustering indexedEntryClustering = null;
+        if (getIndexedColumn().isStatic())
+            indexedEntryClustering = Clustering.STATIC_CLUSTERING;
+        else
+        {
+            ClusteringComparator baseComparator = baseCfs.getComparator();
+            CBuilder builder = CBuilder.create(baseComparator);
+            for (int i = 0; i < baseComparator.size(); i++)
+                builder.add(clustering.get(i + 1));
+            indexedEntryClustering = builder.build();
+        }
 
         return new IndexEntry(indexedValue,
                                 clustering,
                                 indexEntry.primaryKeyLivenessInfo().timestamp(),
                                 clustering.get(0),
-                                builder.build());
+                                indexedEntryClustering);
     }
 
     public boolean isStale(Row data, ByteBuffer indexValue, int nowInSec)
diff --git a/src/java/org/apache/cassandra/index/internal/keys/KeysSearcher.java b/src/java/org/apache/cassandra/index/internal/keys/KeysSearcher.java
index 7cf4c51..fa05420 100644
--- a/src/java/org/apache/cassandra/index/internal/keys/KeysSearcher.java
+++ b/src/java/org/apache/cassandra/index/internal/keys/KeysSearcher.java
@@ -49,7 +49,7 @@
     protected UnfilteredPartitionIterator queryDataFromIndex(final DecoratedKey indexKey,
                                                              final RowIterator indexHits,
                                                              final ReadCommand command,
-                                                             final ReadOrderGroup orderGroup)
+                                                             final ReadExecutionController executionController)
     {
         assert indexHits.staticRow() == Rows.EMPTY_STATIC_ROW;
 
@@ -105,11 +105,10 @@
                     @SuppressWarnings("resource") // filterIfStale closes it's iterator if either it materialize it or if it returns null.
                                                   // Otherwise, we close right away if empty, and if it's assigned to next it will be called either
                                                   // by the next caller of next, or through closing this iterator is this come before.
-                    UnfilteredRowIterator dataIter = filterIfStale(dataCmd.queryMemtableAndDisk(index.baseCfs,
-                                                                                                orderGroup.baseReadOpOrderGroup()),
+                    UnfilteredRowIterator dataIter = filterIfStale(dataCmd.queryMemtableAndDisk(index.baseCfs, executionController),
                                                                    hit,
                                                                    indexKey.getKey(),
-                                                                   orderGroup.writeOpOrderGroup(),
+                                                                   executionController.writeOpOrderGroup(),
                                                                    isForThrift(),
                                                                    command.nowInSec());
 
@@ -140,7 +139,7 @@
 
     private ColumnFilter getExtendedFilter(ColumnFilter initialFilter)
     {
-        if (command.columnFilter().includes(index.getIndexedColumn()))
+        if (command.columnFilter().fetches(index.getIndexedColumn()))
             return initialFilter;
 
         ColumnFilter.Builder builder = ColumnFilter.selectionBuilder();
@@ -162,7 +161,7 @@
             // is the indexed name and so we need to materialize the partition.
             ImmutableBTreePartition result = ImmutableBTreePartition.create(iterator);
             iterator.close();
-            Row data = result.getRow(new Clustering(index.getIndexedColumn().name.bytes));
+            Row data = result.getRow(Clustering.make(index.getIndexedColumn().name.bytes));
             if (data == null)
                 return null;
 
@@ -174,14 +173,14 @@
             {
                 // Index is stale, remove the index entry and ignore
                 index.deleteStaleEntry(index.getIndexCfs().decorateKey(indexedValue),
-                                         new Clustering(index.getIndexedColumn().name.bytes),
+                                         Clustering.make(index.getIndexedColumn().name.bytes),
                                          new DeletionTime(indexHit.primaryKeyLivenessInfo().timestamp(), nowInSec),
                                          writeOp);
                 return null;
             }
             else
             {
-                if (command.columnFilter().includes(index.getIndexedColumn()))
+                if (command.columnFilter().fetches(index.getIndexedColumn()))
                     return result.unfilteredIterator();
 
                 // The query on the base table used an extended column filter to ensure that the
diff --git a/src/java/org/apache/cassandra/index/sasi/SASIIndex.java b/src/java/org/apache/cassandra/index/sasi/SASIIndex.java
new file mode 100644
index 0000000..5ea7cec
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/SASIIndex.java
@@ -0,0 +1,358 @@
+/*
+ * 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.index.sasi;
+
+import java.util.*;
+import java.util.concurrent.Callable;
+import java.util.function.BiFunction;
+
+import com.googlecode.concurrenttrees.common.Iterables;
+
+import org.apache.cassandra.config.*;
+import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.cql3.statements.IndexTarget;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.compaction.CompactionManager;
+import org.apache.cassandra.db.compaction.OperationType;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.db.lifecycle.Tracker;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.partitions.PartitionIterator;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.index.IndexRegistry;
+import org.apache.cassandra.index.SecondaryIndexBuilder;
+import org.apache.cassandra.index.TargetParser;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.conf.IndexMode;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.Mode;
+import org.apache.cassandra.index.sasi.disk.PerSSTableIndexWriter;
+import org.apache.cassandra.index.sasi.plan.QueryPlan;
+import org.apache.cassandra.index.transactions.IndexTransaction;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.format.SSTableFlushObserver;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.notifications.*;
+import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.Pair;
+import org.apache.cassandra.utils.concurrent.OpOrder;
+
+public class SASIIndex implements Index, INotificationConsumer
+{
+    public final static String USAGE_WARNING = "SASI indexes are experimental and are not recommended for production use.";
+
+    private static class SASIIndexBuildingSupport implements IndexBuildingSupport
+    {
+        public SecondaryIndexBuilder getIndexBuildTask(ColumnFamilyStore cfs,
+                                                       Set<Index> indexes,
+                                                       Collection<SSTableReader> sstablesToRebuild)
+        {
+            NavigableMap<SSTableReader, Map<ColumnDefinition, ColumnIndex>> sstables = new TreeMap<>((a, b) -> {
+                return Integer.compare(a.descriptor.generation, b.descriptor.generation);
+            });
+
+            indexes.stream()
+                   .filter((i) -> i instanceof SASIIndex)
+                   .forEach((i) -> {
+                       SASIIndex sasi = (SASIIndex) i;
+                       sasi.index.dropData(sstablesToRebuild);
+                       sstablesToRebuild.stream()
+                                        .filter((sstable) -> !sasi.index.hasSSTable(sstable))
+                                        .forEach((sstable) -> {
+                                            Map<ColumnDefinition, ColumnIndex> toBuild = sstables.get(sstable);
+                                            if (toBuild == null)
+                                                sstables.put(sstable, (toBuild = new HashMap<>()));
+
+                                            toBuild.put(sasi.index.getDefinition(), sasi.index);
+                                        });
+                   });
+
+            return new SASIIndexBuilder(cfs, sstables);
+        }
+    }
+
+    private static final SASIIndexBuildingSupport INDEX_BUILDER_SUPPORT = new SASIIndexBuildingSupport();
+
+    private final ColumnFamilyStore baseCfs;
+    private final IndexMetadata config;
+    private final ColumnIndex index;
+
+    public SASIIndex(ColumnFamilyStore baseCfs, IndexMetadata config)
+    {
+        this.baseCfs = baseCfs;
+        this.config = config;
+
+        ColumnDefinition column = TargetParser.parse(baseCfs.metadata, config).left;
+        this.index = new ColumnIndex(baseCfs.metadata.getKeyValidator(), column, config);
+
+        Tracker tracker = baseCfs.getTracker();
+        tracker.subscribe(this);
+
+        SortedMap<SSTableReader, Map<ColumnDefinition, ColumnIndex>> toRebuild = new TreeMap<>((a, b)
+                                                -> Integer.compare(a.descriptor.generation, b.descriptor.generation));
+
+        for (SSTableReader sstable : index.init(tracker.getView().liveSSTables()))
+        {
+            Map<ColumnDefinition, ColumnIndex> perSSTable = toRebuild.get(sstable);
+            if (perSSTable == null)
+                toRebuild.put(sstable, (perSSTable = new HashMap<>()));
+
+            perSSTable.put(index.getDefinition(), index);
+        }
+
+        CompactionManager.instance.submitIndexBuild(new SASIIndexBuilder(baseCfs, toRebuild));
+    }
+
+    /**
+     * Called via reflection at {@link IndexMetadata#validateCustomIndexOptions}
+     */
+    public static Map<String, String> validateOptions(Map<String, String> options, CFMetaData cfm)
+    {
+        if (!(cfm.partitioner instanceof Murmur3Partitioner))
+            throw new ConfigurationException("SASI only supports Murmur3Partitioner.");
+
+        String targetColumn = options.get("target");
+        if (targetColumn == null)
+            throw new ConfigurationException("unknown target column");
+
+        Pair<ColumnDefinition, IndexTarget.Type> target = TargetParser.parse(cfm, targetColumn);
+        if (target == null)
+            throw new ConfigurationException("failed to retrieve target column for: " + targetColumn);
+
+        if (target.left.isComplex())
+            throw new ConfigurationException("complex columns are not yet supported by SASI");
+
+        if (target.left.isPartitionKey())
+            throw new ConfigurationException("partition key columns are not yet supported by SASI");
+
+        IndexMode.validateAnalyzer(options, target.left);
+
+        IndexMode mode = IndexMode.getMode(target.left, options);
+        if (mode.mode == Mode.SPARSE)
+        {
+            if (mode.isLiteral)
+                throw new ConfigurationException("SPARSE mode is only supported on non-literal columns.");
+
+            if (mode.isAnalyzed)
+                throw new ConfigurationException("SPARSE mode doesn't support analyzers.");
+        }
+
+        return Collections.emptyMap();
+    }
+
+    public void register(IndexRegistry registry)
+    {
+        registry.registerIndex(this);
+    }
+
+    public IndexMetadata getIndexMetadata()
+    {
+        return config;
+    }
+
+    public Callable<?> getInitializationTask()
+    {
+        return null;
+    }
+
+    public Callable<?> getMetadataReloadTask(IndexMetadata indexMetadata)
+    {
+        return null;
+    }
+
+    public Callable<?> getBlockingFlushTask()
+    {
+        return null; // SASI indexes are flushed along side memtable
+    }
+
+    public Callable<?> getInvalidateTask()
+    {
+        return getTruncateTask(FBUtilities.timestampMicros());
+    }
+
+    public Callable<?> getTruncateTask(long truncatedAt)
+    {
+        return () -> {
+            index.dropData(truncatedAt);
+            return null;
+        };
+    }
+
+    public boolean shouldBuildBlocking()
+    {
+        return true;
+    }
+
+    public Optional<ColumnFamilyStore> getBackingTable()
+    {
+        return Optional.empty();
+    }
+
+    public boolean indexes(PartitionColumns columns)
+    {
+        return columns.contains(index.getDefinition());
+    }
+
+    public boolean dependsOn(ColumnDefinition column)
+    {
+        return index.getDefinition().compareTo(column) == 0;
+    }
+
+    public boolean supportsExpression(ColumnDefinition column, Operator operator)
+    {
+        return dependsOn(column) && index.supports(operator);
+    }
+
+    public AbstractType<?> customExpressionValueType()
+    {
+        return null;
+    }
+
+    public RowFilter getPostIndexQueryFilter(RowFilter filter)
+    {
+        return filter.withoutExpressions();
+    }
+
+    public long getEstimatedResultRows()
+    {
+        // this is temporary (until proper QueryPlan is integrated into Cassandra)
+        // and allows us to priority SASI indexes if any in the query since they
+        // are going to be more efficient, to query and intersect, than built-in indexes.
+        return Long.MIN_VALUE;
+    }
+
+    public void validate(PartitionUpdate update) throws InvalidRequestException
+    {}
+
+    @Override
+    public boolean supportsReplicaFilteringProtection(RowFilter rowFilter)
+    {
+        return false;
+    }
+
+    public Indexer indexerFor(DecoratedKey key, PartitionColumns columns, int nowInSec, OpOrder.Group opGroup, IndexTransaction.Type transactionType)
+    {
+        return new Indexer()
+        {
+            public void begin()
+            {}
+
+            public void partitionDelete(DeletionTime deletionTime)
+            {}
+
+            public void rangeTombstone(RangeTombstone tombstone)
+            {}
+
+            public void insertRow(Row row)
+            {
+                if (isNewData())
+                    adjustMemtableSize(index.index(key, row), opGroup);
+            }
+
+            public void updateRow(Row oldRow, Row newRow)
+            {
+                insertRow(newRow);
+            }
+
+            public void removeRow(Row row)
+            {}
+
+            public void finish()
+            {}
+
+            // we are only interested in the data from Memtable
+            // everything else is going to be handled by SSTableWriter observers
+            private boolean isNewData()
+            {
+                return transactionType == IndexTransaction.Type.UPDATE;
+            }
+
+            public void adjustMemtableSize(long additionalSpace, OpOrder.Group opGroup)
+            {
+                baseCfs.getTracker().getView().getCurrentMemtable().getAllocator().onHeap().allocate(additionalSpace, opGroup);
+            }
+        };
+    }
+
+    public Searcher searcherFor(ReadCommand command) throws InvalidRequestException
+    {
+        CFMetaData config = command.metadata();
+        ColumnFamilyStore cfs = Schema.instance.getColumnFamilyStoreInstance(config.cfId);
+        return controller -> new QueryPlan(cfs, command, DatabaseDescriptor.getRangeRpcTimeout()).execute(controller);
+    }
+
+    public SSTableFlushObserver getFlushObserver(Descriptor descriptor, OperationType opType)
+    {
+        return newWriter(baseCfs.metadata.getKeyValidator(), descriptor, Collections.singletonMap(index.getDefinition(), index), opType);
+    }
+
+    public BiFunction<PartitionIterator, ReadCommand, PartitionIterator> postProcessorFor(ReadCommand command)
+    {
+        return (partitionIterator, readCommand) -> partitionIterator;
+    }
+
+    public IndexBuildingSupport getBuildTaskSupport()
+    {
+        return INDEX_BUILDER_SUPPORT;
+    }
+
+    public void handleNotification(INotification notification, Object sender)
+    {
+        // unfortunately, we can only check the type of notification via instanceof :(
+        if (notification instanceof SSTableAddedNotification)
+        {
+            SSTableAddedNotification notice = (SSTableAddedNotification) notification;
+            index.update(Collections.<SSTableReader>emptyList(), Iterables.toList(notice.added));
+        }
+        else if (notification instanceof SSTableListChangedNotification)
+        {
+            SSTableListChangedNotification notice = (SSTableListChangedNotification) notification;
+            index.update(notice.removed, notice.added);
+        }
+        else if (notification instanceof MemtableRenewedNotification)
+        {
+            index.switchMemtable();
+        }
+        else if (notification instanceof MemtableSwitchedNotification)
+        {
+            index.switchMemtable(((MemtableSwitchedNotification) notification).memtable);
+        }
+        else if (notification instanceof MemtableDiscardedNotification)
+        {
+            index.discardMemtable(((MemtableDiscardedNotification) notification).memtable);
+        }
+    }
+
+    public ColumnIndex getIndex()
+    {
+        return index;
+    }
+
+    protected static PerSSTableIndexWriter newWriter(AbstractType<?> keyValidator,
+                                                     Descriptor descriptor,
+                                                     Map<ColumnDefinition, ColumnIndex> indexes,
+                                                     OperationType opType)
+    {
+        return new PerSSTableIndexWriter(keyValidator, descriptor, opType, indexes);
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/SASIIndexBuilder.java b/src/java/org/apache/cassandra/index/sasi/SASIIndexBuilder.java
new file mode 100644
index 0000000..d50875a
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/SASIIndexBuilder.java
@@ -0,0 +1,152 @@
+/*
+ *
+ * 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.index.sasi;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.RowIndexEntry;
+import org.apache.cassandra.db.compaction.CompactionInfo;
+import org.apache.cassandra.db.compaction.CompactionInterruptedException;
+import org.apache.cassandra.db.compaction.OperationType;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.SecondaryIndexBuilder;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.disk.PerSSTableIndexWriter;
+import org.apache.cassandra.io.FSReadError;
+import org.apache.cassandra.io.sstable.KeyIterator;
+import org.apache.cassandra.io.sstable.SSTable;
+import org.apache.cassandra.io.sstable.SSTableIdentityIterator;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.UUIDGen;
+
+class SASIIndexBuilder extends SecondaryIndexBuilder
+{
+    private final ColumnFamilyStore cfs;
+    private final UUID compactionId = UUIDGen.getTimeUUID();
+
+    private final SortedMap<SSTableReader, Map<ColumnDefinition, ColumnIndex>> sstables;
+
+    private long bytesProcessed = 0;
+    private final long totalSizeInBytes;
+
+    public SASIIndexBuilder(ColumnFamilyStore cfs, SortedMap<SSTableReader, Map<ColumnDefinition, ColumnIndex>> sstables)
+    {
+        long totalIndexBytes = 0;
+        for (SSTableReader sstable : sstables.keySet())
+            totalIndexBytes += getPrimaryIndexLength(sstable);
+
+        this.cfs = cfs;
+        this.sstables = sstables;
+        this.totalSizeInBytes = totalIndexBytes;
+    }
+
+    public void build()
+    {
+        AbstractType<?> keyValidator = cfs.metadata.getKeyValidator();
+        for (Map.Entry<SSTableReader, Map<ColumnDefinition, ColumnIndex>> e : sstables.entrySet())
+        {
+            SSTableReader sstable = e.getKey();
+            Map<ColumnDefinition, ColumnIndex> indexes = e.getValue();
+
+            try (RandomAccessReader dataFile = sstable.openDataReader())
+            {
+                PerSSTableIndexWriter indexWriter = SASIIndex.newWriter(keyValidator, sstable.descriptor, indexes, OperationType.COMPACTION);
+
+                long previousKeyPosition = 0;
+                try (KeyIterator keys = new KeyIterator(sstable.descriptor, cfs.metadata))
+                {
+                    while (keys.hasNext())
+                    {
+                        if (isStopRequested())
+                            throw new CompactionInterruptedException(getCompactionInfo());
+
+                        final DecoratedKey key = keys.next();
+                        final long keyPosition = keys.getKeyPosition();
+
+                        indexWriter.startPartition(key, keyPosition);
+
+                        try
+                        {
+                            RowIndexEntry indexEntry = sstable.getPosition(key, SSTableReader.Operator.EQ);
+                            dataFile.seek(indexEntry.position);
+                            ByteBufferUtil.readWithShortLength(dataFile); // key
+
+                            try (SSTableIdentityIterator partition = SSTableIdentityIterator.create(sstable, dataFile, key))
+                            {
+                                // if the row has statics attached, it has to be indexed separately
+                                if (cfs.metadata.hasStaticColumns())
+                                    indexWriter.nextUnfilteredCluster(partition.staticRow());
+
+                                while (partition.hasNext())
+                                    indexWriter.nextUnfilteredCluster(partition.next());
+                            }
+                        }
+                        catch (IOException ex)
+                        {
+                            throw new FSReadError(ex, sstable.getFilename());
+                        }
+
+                        bytesProcessed += keyPosition - previousKeyPosition;
+                        previousKeyPosition = keyPosition;
+                    }
+
+                    completeSSTable(indexWriter, sstable, indexes.values());
+                }
+            }
+        }
+    }
+
+    public CompactionInfo getCompactionInfo()
+    {
+        return new CompactionInfo(cfs.metadata,
+                                  OperationType.INDEX_BUILD,
+                                  bytesProcessed,
+                                  totalSizeInBytes,
+                                  compactionId);
+    }
+
+    private long getPrimaryIndexLength(SSTable sstable)
+    {
+        File primaryIndex = new File(sstable.getIndexFilename());
+        return primaryIndex.exists() ? primaryIndex.length() : 0;
+    }
+
+    private void completeSSTable(PerSSTableIndexWriter indexWriter, SSTableReader sstable, Collection<ColumnIndex> indexes)
+    {
+        indexWriter.complete();
+
+        for (ColumnIndex index : indexes)
+        {
+            File tmpIndex = new File(sstable.descriptor.filenameFor(index.getComponent()));
+            if (!tmpIndex.exists()) // no data was inserted into the index for given sstable
+                continue;
+
+            index.update(Collections.<SSTableReader>emptyList(), Collections.singletonList(sstable));
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/SSTableIndex.java b/src/java/org/apache/cassandra/index/sasi/SSTableIndex.java
new file mode 100644
index 0000000..c67c39c
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/SSTableIndex.java
@@ -0,0 +1,198 @@
+/*
+ * 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.index.sasi;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndex;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.io.FSReadError;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.concurrent.Ref;
+
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+import com.google.common.base.Function;
+
+public class SSTableIndex
+{
+    private final ColumnIndex columnIndex;
+    private final Ref<SSTableReader> sstableRef;
+    private final SSTableReader sstable;
+    private final OnDiskIndex index;
+    private final AtomicInteger references = new AtomicInteger(1);
+    private final AtomicBoolean obsolete = new AtomicBoolean(false);
+
+    public SSTableIndex(ColumnIndex index, File indexFile, SSTableReader referent)
+    {
+        this.columnIndex = index;
+        this.sstableRef = referent.tryRef();
+        this.sstable = sstableRef.get();
+
+        if (sstable == null)
+            throw new IllegalStateException("Couldn't acquire reference to the sstable: " + referent);
+
+        AbstractType<?> validator = columnIndex.getValidator();
+
+        assert validator != null;
+        assert indexFile.exists() : String.format("SSTable %s should have index %s.",
+                sstable.getFilename(),
+                columnIndex.getIndexName());
+
+        this.index = new OnDiskIndex(indexFile, validator, new DecoratedKeyFetcher(sstable));
+    }
+
+    public OnDiskIndexBuilder.Mode mode()
+    {
+        return index.mode();
+    }
+
+    public boolean hasMarkedPartials()
+    {
+        return index.hasMarkedPartials();
+    }
+
+    public ByteBuffer minTerm()
+    {
+        return index.minTerm();
+    }
+
+    public ByteBuffer maxTerm()
+    {
+        return index.maxTerm();
+    }
+
+    public ByteBuffer minKey()
+    {
+        return index.minKey();
+    }
+
+    public ByteBuffer maxKey()
+    {
+        return index.maxKey();
+    }
+
+    public RangeIterator<Long, Token> search(Expression expression)
+    {
+        return index.search(expression);
+    }
+
+    public SSTableReader getSSTable()
+    {
+        return sstable;
+    }
+
+    public String getPath()
+    {
+        return index.getIndexPath();
+    }
+
+    public boolean reference()
+    {
+        while (true)
+        {
+            int n = references.get();
+            if (n <= 0)
+                return false;
+            if (references.compareAndSet(n, n + 1))
+                return true;
+        }
+    }
+
+    public void release()
+    {
+        int n = references.decrementAndGet();
+        if (n == 0)
+        {
+            FileUtils.closeQuietly(index);
+            sstableRef.release();
+            if (obsolete.get() || sstableRef.globalCount() == 0)
+                FileUtils.delete(index.getIndexPath());
+        }
+    }
+
+    public void markObsolete()
+    {
+        obsolete.getAndSet(true);
+        release();
+    }
+
+    public boolean isObsolete()
+    {
+        return obsolete.get();
+    }
+
+    public boolean equals(Object o)
+    {
+        return o instanceof SSTableIndex && index.getIndexPath().equals(((SSTableIndex) o).index.getIndexPath());
+    }
+
+    public int hashCode()
+    {
+        return new HashCodeBuilder().append(index.getIndexPath()).build();
+    }
+
+    public String toString()
+    {
+        return String.format("SSTableIndex(column: %s, SSTable: %s)", columnIndex.getColumnName(), sstable.descriptor);
+    }
+
+    private static class DecoratedKeyFetcher implements Function<Long, DecoratedKey>
+    {
+        private final SSTableReader sstable;
+
+        DecoratedKeyFetcher(SSTableReader reader)
+        {
+            sstable = reader;
+        }
+
+        public DecoratedKey apply(Long offset)
+        {
+            try
+            {
+                return sstable.keyAt(offset);
+            }
+            catch (IOException e)
+            {
+                throw new FSReadError(new IOException("Failed to read key from " + sstable.descriptor, e), sstable.getFilename());
+            }
+        }
+
+        public int hashCode()
+        {
+            return sstable.descriptor.hashCode();
+        }
+
+        public boolean equals(Object other)
+        {
+            return other instanceof DecoratedKeyFetcher
+                    && sstable.descriptor.equals(((DecoratedKeyFetcher) other).sstable.descriptor);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/Term.java b/src/java/org/apache/cassandra/index/sasi/Term.java
new file mode 100644
index 0000000..8f42d58
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/Term.java
@@ -0,0 +1,81 @@
+/*
+ * 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.index.sasi;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.TermSize;
+import org.apache.cassandra.index.sasi.utils.MappedBuffer;
+import org.apache.cassandra.db.marshal.AbstractType;
+
+import static org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.IS_PARTIAL_BIT;
+
+public class Term
+{
+    protected final MappedBuffer content;
+    protected final TermSize termSize;
+
+    private final boolean hasMarkedPartials;
+
+    public Term(MappedBuffer content, TermSize size, boolean hasMarkedPartials)
+    {
+        this.content = content;
+        this.termSize = size;
+        this.hasMarkedPartials = hasMarkedPartials;
+    }
+
+    public ByteBuffer getTerm()
+    {
+        long offset = termSize.isConstant() ? content.position() : content.position() + 2;
+        int  length = termSize.isConstant() ? termSize.size : readLength(content.position());
+
+        return content.getPageRegion(offset, length);
+    }
+
+    public boolean isPartial()
+    {
+        return !termSize.isConstant()
+               && hasMarkedPartials
+               && (content.getShort(content.position()) & (1 << IS_PARTIAL_BIT)) != 0;
+    }
+
+    public long getDataOffset()
+    {
+        long position = content.position();
+        return position + (termSize.isConstant() ? termSize.size : 2 + readLength(position));
+    }
+
+    public int compareTo(AbstractType<?> comparator, ByteBuffer query)
+    {
+        return compareTo(comparator, query, true);
+    }
+
+    public int compareTo(AbstractType<?> comparator, ByteBuffer query, boolean checkFully)
+    {
+        long position = content.position();
+        int padding = termSize.isConstant() ? 0 : 2;
+        int len = termSize.isConstant() ? termSize.size : readLength(position);
+
+        return content.comparePageTo(position + padding, checkFully ? len : Math.min(len, query.remaining()), comparator, query);
+    }
+
+    private short readLength(long position)
+    {
+        return (short) (content.getShort(position) & ~(1 << IS_PARTIAL_BIT));
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/TermIterator.java b/src/java/org/apache/cassandra/index/sasi/TermIterator.java
new file mode 100644
index 0000000..85f81b0
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/TermIterator.java
@@ -0,0 +1,220 @@
+/*
+ * 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.index.sasi;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.NamedThreadFactory;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.io.util.FileUtils;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.Uninterruptibles;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TermIterator extends RangeIterator<Long, Token>
+{
+    private static final Logger logger = LoggerFactory.getLogger(TermIterator.class);
+
+    private static final FastThreadLocal<ExecutorService> SEARCH_EXECUTOR = new FastThreadLocal<ExecutorService>()
+    {
+        public ExecutorService initialValue()
+        {
+            final String currentThread = Thread.currentThread().getName();
+            final int concurrencyFactor = DatabaseDescriptor.searchConcurrencyFactor();
+
+            logger.info("Search Concurrency Factor is set to {} for {}", concurrencyFactor, currentThread);
+
+            return (concurrencyFactor <= 1)
+                    ? MoreExecutors.newDirectExecutorService()
+                    : Executors.newFixedThreadPool(concurrencyFactor, new ThreadFactory()
+            {
+                public final AtomicInteger count = new AtomicInteger();
+
+                public Thread newThread(Runnable task)
+                {
+                    return NamedThreadFactory.createThread(task, currentThread + "-SEARCH-" + count.incrementAndGet(), true);
+                }
+            });
+        }
+    };
+
+    private final Expression expression;
+
+    private final RangeIterator<Long, Token> union;
+    private final Set<SSTableIndex> referencedIndexes;
+
+    private TermIterator(Expression e,
+                         RangeIterator<Long, Token> union,
+                         Set<SSTableIndex> referencedIndexes)
+    {
+        super(union.getMinimum(), union.getMaximum(), union.getCount());
+
+        this.expression = e;
+        this.union = union;
+        this.referencedIndexes = referencedIndexes;
+    }
+
+    @SuppressWarnings("resource")
+    public static TermIterator build(final Expression e, Set<SSTableIndex> perSSTableIndexes)
+    {
+        final List<RangeIterator<Long, Token>> tokens = new CopyOnWriteArrayList<>();
+        final AtomicLong tokenCount = new AtomicLong(0);
+
+        RangeIterator<Long, Token> memtableIterator = e.index.searchMemtable(e);
+        if (memtableIterator != null)
+        {
+            tokens.add(memtableIterator);
+            tokenCount.addAndGet(memtableIterator.getCount());
+        }
+
+        final Set<SSTableIndex> referencedIndexes = new CopyOnWriteArraySet<>();
+
+        try
+        {
+            final CountDownLatch latch = new CountDownLatch(perSSTableIndexes.size());
+            final ExecutorService searchExecutor = SEARCH_EXECUTOR.get();
+
+            for (final SSTableIndex index : perSSTableIndexes)
+            {
+                if (e.getOp() == Expression.Op.PREFIX &&
+                    index.mode() == OnDiskIndexBuilder.Mode.CONTAINS && !index.hasMarkedPartials())
+                    throw new UnsupportedOperationException(String.format("The index %s has not yet been upgraded " +
+                                                                          "to support prefix queries in CONTAINS mode. " +
+                                                                          "Wait for compaction or rebuild the index.",
+                                                                          index.getPath()));
+
+
+                if (!index.reference())
+                {
+                    latch.countDown();
+                    continue;
+                }
+
+                // add to referenced right after the reference was acquired,
+                // that helps to release index if something goes bad inside of the search
+                referencedIndexes.add(index);
+
+                searchExecutor.submit((Runnable) () -> {
+                    try
+                    {
+                        e.checkpoint();
+
+                        RangeIterator<Long, Token> keyIterator = index.search(e);
+                        if (keyIterator == null)
+                        {
+                            releaseIndex(referencedIndexes, index);
+                            return;
+                        }
+
+                        tokens.add(keyIterator);
+                        tokenCount.getAndAdd(keyIterator.getCount());
+                    }
+                    catch (Throwable e1)
+                    {
+                        releaseIndex(referencedIndexes, index);
+
+                        if (logger.isDebugEnabled())
+                            logger.debug(String.format("Failed search an index %s, skipping.", index.getPath()), e1);
+                    }
+                    finally
+                    {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            Uninterruptibles.awaitUninterruptibly(latch);
+
+            // checkpoint right away after all indexes complete search because we might have crossed the quota
+            e.checkpoint();
+
+            RangeIterator<Long, Token> ranges = RangeUnionIterator.build(tokens);
+            return new TermIterator(e, ranges, referencedIndexes);
+        }
+        catch (Throwable ex)
+        {
+            // if execution quota was exceeded while opening indexes or something else happened
+            // local (yet to be tracked) indexes should be released first before re-throwing exception
+            referencedIndexes.forEach(TermIterator::releaseQuietly);
+
+            throw ex;
+        }
+    }
+
+    protected Token computeNext()
+    {
+        try
+        {
+            return union.hasNext() ? union.next() : endOfData();
+        }
+        finally
+        {
+            expression.checkpoint();
+        }
+    }
+
+    protected void performSkipTo(Long nextToken)
+    {
+        try
+        {
+            union.skipTo(nextToken);
+        }
+        finally
+        {
+            expression.checkpoint();
+        }
+    }
+
+    public void close()
+    {
+        FileUtils.closeQuietly(union);
+        referencedIndexes.forEach(TermIterator::releaseQuietly);
+        referencedIndexes.clear();
+    }
+
+    private static void releaseIndex(Set<SSTableIndex> indexes, SSTableIndex index)
+    {
+        indexes.remove(index);
+        releaseQuietly(index);
+    }
+
+    private static void releaseQuietly(SSTableIndex index)
+    {
+        try
+        {
+            index.release();
+        }
+        catch (Throwable e)
+        {
+            logger.error(String.format("Failed to release index %s", index.getPath()), e);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/AbstractAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/analyzer/AbstractAnalyzer.java
new file mode 100644
index 0000000..8630c67
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/AbstractAnalyzer.java
@@ -0,0 +1,77 @@
+/*
+ * 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.index.sasi.analyzer;
+
+import java.nio.ByteBuffer;
+import java.text.Normalizer;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.exceptions.ConfigurationException;
+
+public abstract class AbstractAnalyzer implements Iterator<ByteBuffer>
+{
+    protected ByteBuffer next = null;
+
+    public ByteBuffer next()
+    {
+        return next;
+    }
+
+    public void remove()
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public void validate(Map<String, String> options, ColumnDefinition cd) throws ConfigurationException
+    {
+        if (!isCompatibleWith(cd.type))
+            throw new ConfigurationException(String.format("%s does not support type %s",
+                                                           this.getClass().getSimpleName(),
+                                                           cd.type.asCQL3Type()));
+    }
+
+    public abstract void init(Map<String, String> options, AbstractType<?> validator);
+
+    public abstract void reset(ByteBuffer input);
+
+    /**
+     * Test whether the given validator is compatible with the underlying analyzer.
+     *
+     * @param validator the validator to test the compatibility with
+     * @return true if the give validator is compatible, false otherwise
+     */
+    protected abstract boolean isCompatibleWith(AbstractType<?> validator);
+
+    /**
+     * @return true if current analyzer provides text tokenization, false otherwise.
+     */
+    public boolean isTokenizing()
+    {
+        return false;
+    }
+
+    public static String normalize(String original)
+    {
+        return Normalizer.isNormalized(original, Normalizer.Form.NFC)
+                ? original
+                : Normalizer.normalize(original, Normalizer.Form.NFC);
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/DelimiterAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/analyzer/DelimiterAnalyzer.java
new file mode 100644
index 0000000..05dfedc
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/DelimiterAnalyzer.java
@@ -0,0 +1,111 @@
+/*
+ * 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.index.sasi.analyzer;
+
+import java.nio.CharBuffer;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.AsciiType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.utils.AbstractIterator;
+
+@Beta
+public class DelimiterAnalyzer extends AbstractAnalyzer
+{
+
+    private static final Map<AbstractType<?>, Charset> VALID_ANALYZABLE_TYPES = new HashMap<AbstractType<?>, Charset>()
+    {{
+        put(UTF8Type.instance, StandardCharsets.UTF_8);
+        put(AsciiType.instance, StandardCharsets.US_ASCII);
+    }};
+
+    private char delimiter;
+    private Charset charset;
+    private Iterator<ByteBuffer> iter;
+
+    public DelimiterAnalyzer()
+    {
+    }
+
+    @Override
+    public ByteBuffer next()
+    {
+        return iter.next();
+    }
+
+    public void init(Map<String, String> options, AbstractType<?> validator)
+    {
+        DelimiterTokenizingOptions tokenizingOptions = DelimiterTokenizingOptions.buildFromMap(options);
+        delimiter = tokenizingOptions.getDelimiter();
+        charset = VALID_ANALYZABLE_TYPES.get(validator);
+    }
+
+    public boolean hasNext()
+    {
+        return iter.hasNext();
+    }
+
+    public void reset(ByteBuffer input)
+    {
+        Preconditions.checkNotNull(input);
+        final CharBuffer cb = charset.decode(input);
+
+        this.iter = new AbstractIterator<ByteBuffer>() {
+            protected ByteBuffer computeNext() {
+
+                if (!cb.hasRemaining())
+                    return endOfData();
+
+                CharBuffer readahead = cb.duplicate();
+                // loop until we see the next delimiter character, or reach end of data
+                boolean readaheadRemaining;
+                while ((readaheadRemaining = readahead.hasRemaining()) && readahead.get() != delimiter);
+
+                char[] chars = new char[readahead.position() - cb.position() - (readaheadRemaining ? 1 : 0)];
+                cb.get(chars);
+                Preconditions.checkState(!cb.hasRemaining() || cb.get() == delimiter);
+
+                return 0 < chars.length
+                        ? charset.encode(CharBuffer.wrap(chars))
+                        // blank partition keys not permitted, ref ConcurrentRadixTree.putIfAbsent(..)
+                        : computeNext();
+            }
+        };
+    }
+
+    @Override
+    public boolean isTokenizing()
+    {
+        return true;
+    }
+
+    @Override
+    public boolean isCompatibleWith(AbstractType<?> validator)
+    {
+        return VALID_ANALYZABLE_TYPES.containsKey(validator);
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/DelimiterTokenizingOptions.java b/src/java/org/apache/cassandra/index/sasi/analyzer/DelimiterTokenizingOptions.java
new file mode 100644
index 0000000..c2c8ef7
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/DelimiterTokenizingOptions.java
@@ -0,0 +1,71 @@
+/*
+ * 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.index.sasi.analyzer;
+
+import java.util.Map;
+
+/** Simple tokenizer based on a specified delimiter (rather than whitespace).
+ */
+public class DelimiterTokenizingOptions
+{
+    public static final String DELIMITER = "delimiter";
+
+    private final char delimiter;
+
+    private DelimiterTokenizingOptions(char delimiter)
+    {
+        this.delimiter = delimiter;
+    }
+
+    char getDelimiter()
+    {
+        return delimiter;
+    }
+
+    private static class OptionsBuilder
+    {
+        private char delimiter = ',';
+
+        public DelimiterTokenizingOptions build()
+        {
+            return new DelimiterTokenizingOptions(delimiter);
+        }
+    }
+
+    static DelimiterTokenizingOptions buildFromMap(Map<String, String> optionsMap)
+    {
+        OptionsBuilder optionsBuilder = new OptionsBuilder();
+
+        for (Map.Entry<String, String> entry : optionsMap.entrySet())
+        {
+            switch (entry.getKey())
+            {
+                case DELIMITER:
+                {
+                    String value = entry.getValue();
+                    if (1 != value.length())
+                        throw new IllegalArgumentException(String.format("Only single character delimiters supported, was %s", value));
+
+                    optionsBuilder.delimiter = entry.getValue().charAt(0);
+                    break;
+                }
+            }
+        }
+        return optionsBuilder.build();
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/NoOpAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/analyzer/NoOpAnalyzer.java
new file mode 100644
index 0000000..1a42789
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/NoOpAnalyzer.java
@@ -0,0 +1,60 @@
+/*
+ * 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.index.sasi.analyzer;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+
+/**
+ * Default noOp tokenizer. The iterator will iterate only once
+ * returning the unmodified input
+ */
+public class NoOpAnalyzer extends AbstractAnalyzer
+{
+    private ByteBuffer input;
+    private boolean hasNext = false;
+
+    public void init(Map<String, String> options, AbstractType<?> validator)
+    {}
+
+    public boolean hasNext()
+    {
+        if (hasNext)
+        {
+            this.next = input;
+            this.hasNext = false;
+            return true;
+        }
+        return false;
+    }
+
+    public void reset(ByteBuffer input)
+    {
+        this.next = null;
+        this.input = input;
+        this.hasNext = true;
+    }
+
+    @Override
+    public boolean isCompatibleWith(AbstractType<?> validator)
+    {
+        return true;
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/NonTokenizingAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/analyzer/NonTokenizingAnalyzer.java
new file mode 100644
index 0000000..30c8506
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/NonTokenizingAnalyzer.java
@@ -0,0 +1,145 @@
+/*
+ * 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.index.sasi.analyzer;
+
+import java.nio.ByteBuffer;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.index.sasi.analyzer.filter.BasicResultFilters;
+import org.apache.cassandra.index.sasi.analyzer.filter.FilterPipelineBuilder;
+import org.apache.cassandra.index.sasi.analyzer.filter.FilterPipelineExecutor;
+import org.apache.cassandra.index.sasi.analyzer.filter.FilterPipelineTask;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.AsciiType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Analyzer that does *not* tokenize the input. Optionally will
+ * apply filters for the input output as defined in analyzers options
+ */
+public class NonTokenizingAnalyzer extends AbstractAnalyzer
+{
+    private static final Logger logger = LoggerFactory.getLogger(NonTokenizingAnalyzer.class);
+
+    private static final Set<AbstractType<?>> VALID_ANALYZABLE_TYPES = new HashSet<AbstractType<?>>()
+    {{
+            add(UTF8Type.instance);
+            add(AsciiType.instance);
+    }};
+
+    private AbstractType<?> validator;
+    private NonTokenizingOptions options;
+    private FilterPipelineTask filterPipeline;
+
+    private ByteBuffer input;
+    private boolean hasNext = false;
+
+    @Override
+    public void validate(Map<String, String> options, ColumnDefinition cd) throws ConfigurationException
+    {
+        super.validate(options, cd);
+        if (options.containsKey(NonTokenizingOptions.CASE_SENSITIVE) &&
+            (options.containsKey(NonTokenizingOptions.NORMALIZE_LOWERCASE)
+             || options.containsKey(NonTokenizingOptions.NORMALIZE_UPPERCASE)))
+            throw new ConfigurationException("case_sensitive option cannot be specified together " +
+                                               "with either normalize_lowercase or normalize_uppercase");
+    }
+
+    public void init(Map<String, String> options, AbstractType<?> validator)
+    {
+        init(NonTokenizingOptions.buildFromMap(options), validator);
+    }
+
+    public void init(NonTokenizingOptions tokenizerOptions, AbstractType<?> validator)
+    {
+        this.validator = validator;
+        this.options = tokenizerOptions;
+        this.filterPipeline = getFilterPipeline();
+    }
+
+    public boolean hasNext()
+    {
+        // check that we know how to handle the input, otherwise bail
+        if (!VALID_ANALYZABLE_TYPES.contains(validator))
+            return false;
+
+        if (hasNext)
+        {
+            String inputStr;
+
+            try
+            {
+                inputStr = validator.getString(input);
+                if (inputStr == null)
+                    throw new MarshalException(String.format("'null' deserialized value for %s with %s", ByteBufferUtil.bytesToHex(input), validator));
+
+                Object pipelineRes = FilterPipelineExecutor.execute(filterPipeline, inputStr);
+                if (pipelineRes == null)
+                    return false;
+
+                next = validator.fromString(normalize((String) pipelineRes));
+                return true;
+            }
+            catch (MarshalException e)
+            {
+                logger.error("Failed to deserialize value with " + validator, e);
+                return false;
+            }
+            finally
+            {
+                hasNext = false;
+            }
+        }
+
+        return false;
+    }
+
+    public void reset(ByteBuffer input)
+    {
+        this.next = null;
+        this.input = input;
+        this.hasNext = true;
+    }
+
+    private FilterPipelineTask getFilterPipeline()
+    {
+        FilterPipelineBuilder builder = new FilterPipelineBuilder(new BasicResultFilters.NoOperation());
+        if (options.isCaseSensitive() && options.shouldLowerCaseOutput())
+            builder = builder.add("to_lower", new BasicResultFilters.LowerCase());
+        if (options.isCaseSensitive() && options.shouldUpperCaseOutput())
+            builder = builder.add("to_upper", new BasicResultFilters.UpperCase());
+        if (!options.isCaseSensitive())
+            builder = builder.add("to_lower", new BasicResultFilters.LowerCase());
+        return builder.build();
+    }
+
+    @Override
+    public boolean isCompatibleWith(AbstractType<?> validator)
+    {
+        return VALID_ANALYZABLE_TYPES.contains(validator);
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/NonTokenizingOptions.java b/src/java/org/apache/cassandra/index/sasi/analyzer/NonTokenizingOptions.java
new file mode 100644
index 0000000..d783028
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/NonTokenizingOptions.java
@@ -0,0 +1,142 @@
+/*
+ * 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.index.sasi.analyzer;
+
+import java.util.Map;
+
+public class NonTokenizingOptions
+{
+    public static final String NORMALIZE_LOWERCASE = "normalize_lowercase";
+    public static final String NORMALIZE_UPPERCASE = "normalize_uppercase";
+    public static final String CASE_SENSITIVE = "case_sensitive";
+
+    private boolean caseSensitive;
+    private boolean upperCaseOutput;
+    private boolean lowerCaseOutput;
+
+    public boolean isCaseSensitive()
+    {
+        return caseSensitive;
+    }
+
+    public void setCaseSensitive(boolean caseSensitive)
+    {
+        this.caseSensitive = caseSensitive;
+    }
+
+    public boolean shouldUpperCaseOutput()
+    {
+        return upperCaseOutput;
+    }
+
+    public void setUpperCaseOutput(boolean upperCaseOutput)
+    {
+        this.upperCaseOutput = upperCaseOutput;
+    }
+
+    public boolean shouldLowerCaseOutput()
+    {
+        return lowerCaseOutput;
+    }
+
+    public void setLowerCaseOutput(boolean lowerCaseOutput)
+    {
+        this.lowerCaseOutput = lowerCaseOutput;
+    }
+
+    public static class OptionsBuilder
+    {
+        private boolean caseSensitive = true;
+        private boolean upperCaseOutput = false;
+        private boolean lowerCaseOutput = false;
+
+        public OptionsBuilder()
+        {
+        }
+
+        public OptionsBuilder caseSensitive(boolean caseSensitive)
+        {
+            this.caseSensitive = caseSensitive;
+            return this;
+        }
+
+        public OptionsBuilder upperCaseOutput(boolean upperCaseOutput)
+        {
+            this.upperCaseOutput = upperCaseOutput;
+            return this;
+        }
+
+        public OptionsBuilder lowerCaseOutput(boolean lowerCaseOutput)
+        {
+            this.lowerCaseOutput = lowerCaseOutput;
+            return this;
+        }
+
+        public NonTokenizingOptions build()
+        {
+            if (lowerCaseOutput && upperCaseOutput)
+                throw new IllegalArgumentException("Options to normalize terms cannot be " +
+                        "both uppercase and lowercase at the same time");
+
+            NonTokenizingOptions options = new NonTokenizingOptions();
+            options.setCaseSensitive(caseSensitive);
+            options.setUpperCaseOutput(upperCaseOutput);
+            options.setLowerCaseOutput(lowerCaseOutput);
+            return options;
+        }
+    }
+
+    public static NonTokenizingOptions buildFromMap(Map<String, String> optionsMap)
+    {
+        OptionsBuilder optionsBuilder = new OptionsBuilder();
+
+        for (Map.Entry<String, String> entry : optionsMap.entrySet())
+        {
+            switch (entry.getKey())
+            {
+                case NORMALIZE_LOWERCASE:
+                {
+                    boolean bool = Boolean.parseBoolean(entry.getValue());
+                    optionsBuilder = optionsBuilder.lowerCaseOutput(bool);
+                    break;
+                }
+                case NORMALIZE_UPPERCASE:
+                {
+                    boolean bool = Boolean.parseBoolean(entry.getValue());
+                    optionsBuilder = optionsBuilder.upperCaseOutput(bool);
+                    break;
+                }
+                case CASE_SENSITIVE:
+                {
+                    boolean bool = Boolean.parseBoolean(entry.getValue());
+                    optionsBuilder = optionsBuilder.caseSensitive(bool);
+                    break;
+                }
+            }
+        }
+        return optionsBuilder.build();
+    }
+
+    public static NonTokenizingOptions getDefaultOptions()
+    {
+        return new OptionsBuilder()
+                .caseSensitive(true).lowerCaseOutput(false)
+                .upperCaseOutput(false)
+                .build();
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/SUPPLEMENTARY.jflex-macro b/src/java/org/apache/cassandra/index/sasi/analyzer/SUPPLEMENTARY.jflex-macro
new file mode 100644
index 0000000..f5bf68e
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/SUPPLEMENTARY.jflex-macro
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+// Generated using ICU4J 52.1.0.0
+// by org.apache.lucene.analysis.icu.GenerateJFlexSupplementaryMacros
+
+
+ALetterSupp = (
+	  ([\ud83b][\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB])
+	| ([\ud81a][\uDC00-\uDE38])
+	| ([\ud81b][\uDF00-\uDF44\uDF50\uDF93-\uDF9F])
+	| ([\ud835][\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB])
+	| ([\ud80d][\uDC00-\uDC2E])
+	| ([\ud80c][\uDC00-\uDFFF])
+	| ([\ud809][\uDC00-\uDC62])
+	| ([\ud808][\uDC00-\uDF6E])
+	| ([\ud805][\uDE80-\uDEAA])
+	| ([\ud804][\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD83-\uDDB2\uDDC1-\uDDC4])
+	| ([\ud801][\uDC00-\uDC9D])
+	| ([\ud800][\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1E\uDF30-\uDF4A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5])
+	| ([\ud803][\uDC00-\uDC48])
+	| ([\ud802][\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72])
+)
+FormatSupp = (
+	  ([\ud804][\uDCBD])
+	| ([\ud834][\uDD73-\uDD7A])
+	| ([\udb40][\uDC01\uDC20-\uDC7F])
+)
+NumericSupp = (
+	  ([\ud805][\uDEC0-\uDEC9])
+	| ([\ud804][\uDC66-\uDC6F\uDCF0-\uDCF9\uDD36-\uDD3F\uDDD0-\uDDD9])
+	| ([\ud835][\uDFCE-\uDFFF])
+	| ([\ud801][\uDCA0-\uDCA9])
+)
+ExtendSupp = (
+	  ([\ud81b][\uDF51-\uDF7E\uDF8F-\uDF92])
+	| ([\ud805][\uDEAB-\uDEB7])
+	| ([\ud804][\uDC00-\uDC02\uDC38-\uDC46\uDC80-\uDC82\uDCB0-\uDCBA\uDD00-\uDD02\uDD27-\uDD34\uDD80-\uDD82\uDDB3-\uDDC0])
+	| ([\ud834][\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44])
+	| ([\ud800][\uDDFD])
+	| ([\udb40][\uDD00-\uDDEF])
+	| ([\ud802][\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F])
+)
+KatakanaSupp = (
+	  ([\ud82c][\uDC00])
+)
+MidLetterSupp = (
+	  []
+)
+MidNumSupp = (
+	  []
+)
+MidNumLetSupp = (
+	  []
+)
+ExtendNumLetSupp = (
+	  []
+)
+ExtendNumLetSupp = (
+	  []
+)
+ComplexContextSupp = (
+	  []
+)
+HanSupp = (
+	  ([\ud87e][\uDC00-\uDE1D])
+	| ([\ud86b][\uDC00-\uDFFF])
+	| ([\ud86a][\uDC00-\uDFFF])
+	| ([\ud869][\uDC00-\uDED6\uDF00-\uDFFF])
+	| ([\ud868][\uDC00-\uDFFF])
+	| ([\ud86e][\uDC00-\uDC1D])
+	| ([\ud86d][\uDC00-\uDF34\uDF40-\uDFFF])
+	| ([\ud86c][\uDC00-\uDFFF])
+	| ([\ud863][\uDC00-\uDFFF])
+	| ([\ud862][\uDC00-\uDFFF])
+	| ([\ud861][\uDC00-\uDFFF])
+	| ([\ud860][\uDC00-\uDFFF])
+	| ([\ud867][\uDC00-\uDFFF])
+	| ([\ud866][\uDC00-\uDFFF])
+	| ([\ud865][\uDC00-\uDFFF])
+	| ([\ud864][\uDC00-\uDFFF])
+	| ([\ud858][\uDC00-\uDFFF])
+	| ([\ud859][\uDC00-\uDFFF])
+	| ([\ud85a][\uDC00-\uDFFF])
+	| ([\ud85b][\uDC00-\uDFFF])
+	| ([\ud85c][\uDC00-\uDFFF])
+	| ([\ud85d][\uDC00-\uDFFF])
+	| ([\ud85e][\uDC00-\uDFFF])
+	| ([\ud85f][\uDC00-\uDFFF])
+	| ([\ud850][\uDC00-\uDFFF])
+	| ([\ud851][\uDC00-\uDFFF])
+	| ([\ud852][\uDC00-\uDFFF])
+	| ([\ud853][\uDC00-\uDFFF])
+	| ([\ud854][\uDC00-\uDFFF])
+	| ([\ud855][\uDC00-\uDFFF])
+	| ([\ud856][\uDC00-\uDFFF])
+	| ([\ud857][\uDC00-\uDFFF])
+	| ([\ud849][\uDC00-\uDFFF])
+	| ([\ud848][\uDC00-\uDFFF])
+	| ([\ud84b][\uDC00-\uDFFF])
+	| ([\ud84a][\uDC00-\uDFFF])
+	| ([\ud84d][\uDC00-\uDFFF])
+	| ([\ud84c][\uDC00-\uDFFF])
+	| ([\ud84f][\uDC00-\uDFFF])
+	| ([\ud84e][\uDC00-\uDFFF])
+	| ([\ud841][\uDC00-\uDFFF])
+	| ([\ud840][\uDC00-\uDFFF])
+	| ([\ud843][\uDC00-\uDFFF])
+	| ([\ud842][\uDC00-\uDFFF])
+	| ([\ud845][\uDC00-\uDFFF])
+	| ([\ud844][\uDC00-\uDFFF])
+	| ([\ud847][\uDC00-\uDFFF])
+	| ([\ud846][\uDC00-\uDFFF])
+)
+HiraganaSupp = (
+	  ([\ud83c][\uDE00])
+	| ([\ud82c][\uDC01])
+)
+SingleQuoteSupp = (
+	  []
+)
+DoubleQuoteSupp = (
+	  []
+)
+HebrewLetterSupp = (
+	  []
+)
+RegionalIndicatorSupp = (
+	  ([\ud83c][\uDDE6-\uDDFF])
+)
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java
new file mode 100644
index 0000000..3156156
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java
@@ -0,0 +1,220 @@
+/*
+ * 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.index.sasi.analyzer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.cassandra.index.sasi.analyzer.filter.*;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.AsciiType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.carrotsearch.hppc.IntObjectMap;
+import com.carrotsearch.hppc.IntObjectOpenHashMap;
+
+public class StandardAnalyzer extends AbstractAnalyzer
+{
+
+    private static final Set<AbstractType<?>> VALID_ANALYZABLE_TYPES = new HashSet<AbstractType<?>>()
+    {
+        {
+            add(UTF8Type.instance);
+            add(AsciiType.instance);
+        }
+    };
+
+    public enum TokenType
+    {
+        EOF(-1),
+        ALPHANUM(0),
+        NUM(6),
+        SOUTHEAST_ASIAN(9),
+        IDEOGRAPHIC(10),
+        HIRAGANA(11),
+        KATAKANA(12),
+        HANGUL(13);
+
+        private static final IntObjectMap<TokenType> TOKENS = new IntObjectOpenHashMap<>();
+
+        static
+        {
+            for (TokenType type : TokenType.values())
+                TOKENS.put(type.value, type);
+        }
+
+        public final int value;
+
+        TokenType(int value)
+        {
+            this.value = value;
+        }
+
+        public int getValue()
+        {
+            return value;
+        }
+
+        public static TokenType fromValue(int val)
+        {
+            return TOKENS.get(val);
+        }
+    }
+
+    private AbstractType<?> validator;
+
+    private StandardTokenizerInterface scanner;
+    private StandardTokenizerOptions options;
+    private FilterPipelineTask filterPipeline;
+
+    protected Reader inputReader = null;
+
+    public String getToken()
+    {
+        return scanner.getText();
+    }
+
+    public final boolean incrementToken() throws IOException
+    {
+        while(true)
+        {
+            TokenType currentTokenType = TokenType.fromValue(scanner.getNextToken());
+            if (currentTokenType == TokenType.EOF)
+                return false;
+            if (scanner.yylength() <= options.getMaxTokenLength()
+                    && scanner.yylength() >= options.getMinTokenLength())
+                return true;
+        }
+    }
+
+    protected String getFilteredCurrentToken() throws IOException
+    {
+        String token = getToken();
+        Object pipelineRes;
+
+        while (true)
+        {
+            pipelineRes = FilterPipelineExecutor.execute(filterPipeline, token);
+            if (pipelineRes != null)
+                break;
+
+            boolean reachedEOF = incrementToken();
+            if (!reachedEOF)
+                break;
+
+            token = getToken();
+        }
+
+        return (String) pipelineRes;
+    }
+
+    private FilterPipelineTask getFilterPipeline()
+    {
+        FilterPipelineBuilder builder = new FilterPipelineBuilder(new BasicResultFilters.NoOperation());
+        if (!options.isCaseSensitive() && options.shouldLowerCaseTerms())
+            builder = builder.add("to_lower", new BasicResultFilters.LowerCase());
+        if (!options.isCaseSensitive() && options.shouldUpperCaseTerms())
+            builder = builder.add("to_upper", new BasicResultFilters.UpperCase());
+        if (options.shouldIgnoreStopTerms())
+            builder = builder.add("skip_stop_words", new StopWordFilters.DefaultStopWordFilter(options.getLocale()));
+        if (options.shouldStemTerms())
+            builder = builder.add("term_stemming", new StemmingFilters.DefaultStemmingFilter(options.getLocale()));
+        return builder.build();
+    }
+
+    public void init(Map<String, String> options, AbstractType<?> validator)
+    {
+        init(StandardTokenizerOptions.buildFromMap(options), validator);
+    }
+
+    @VisibleForTesting
+    protected void init(StandardTokenizerOptions options)
+    {
+        init(options, UTF8Type.instance);
+    }
+
+    public void init(StandardTokenizerOptions tokenizerOptions, AbstractType<?> validator)
+    {
+        this.validator = validator;
+        this.options = tokenizerOptions;
+        this.filterPipeline = getFilterPipeline();
+
+        Reader reader = new InputStreamReader(new DataInputBuffer(ByteBufferUtil.EMPTY_BYTE_BUFFER, false), StandardCharsets.UTF_8);
+        this.scanner = new StandardTokenizerImpl(reader);
+        this.inputReader = reader;
+    }
+
+    public boolean hasNext()
+    {
+        try
+        {
+            if (incrementToken())
+            {
+                if (getFilteredCurrentToken() != null)
+                {
+                    this.next = validator.fromString(normalize(getFilteredCurrentToken()));
+                    return true;
+                }
+            }
+        }
+        catch (IOException e)
+        {}
+
+        return false;
+    }
+
+    public void reset(ByteBuffer input)
+    {
+        this.next = null;
+        Reader reader = new InputStreamReader(new DataInputBuffer(input, false), StandardCharsets.UTF_8);
+        scanner.yyreset(reader);
+        this.inputReader = reader;
+    }
+
+    @VisibleForTesting
+    public void reset(InputStream input)
+    {
+        this.next = null;
+        Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8);
+        scanner.yyreset(reader);
+        this.inputReader = reader;
+    }
+
+    @Override
+    public boolean isTokenizing()
+    {
+        return true;
+    }
+
+    @Override
+    public boolean isCompatibleWith(AbstractType<?> validator)
+    {
+        return VALID_ANALYZABLE_TYPES.contains(validator);
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerImpl.jflex b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerImpl.jflex
new file mode 100644
index 0000000..bdc35eb
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerImpl.jflex
@@ -0,0 +1,220 @@
+package org.apache.cassandra.index.sasi.analyzer;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+
+/**
+ * This class implements Word Break rules from the Unicode Text Segmentation 
+ * algorithm, as specified in 
+ * <a href="http://unicode.org/reports/tr29/">Unicode Standard Annex #29</a>. 
+ * <p/>
+ * Tokens produced are of the following types:
+ * <ul>
+ *   <li>&lt;ALPHANUM&gt;: A sequence of alphabetic and numeric characters</li>
+ *   <li>&lt;NUM&gt;: A number</li>
+ *   <li>&lt;SOUTHEAST_ASIAN&gt;: A sequence of characters from South and Southeast
+ *       Asian languages, including Thai, Lao, Myanmar, and Khmer</li>
+ *   <li>&lt;IDEOGRAPHIC&gt;: A single CJKV ideographic character</li>
+ *   <li>&lt;HIRAGANA&gt;: A single hiragana character</li>
+ *   <li>&lt;KATAKANA&gt;: A sequence of katakana characters</li>
+ *   <li>&lt;HANGUL&gt;: A sequence of Hangul characters</li>
+ * </ul>
+ */
+%%
+
+%unicode 6.3
+%integer
+%final
+%public
+%class StandardTokenizerImpl
+%implements StandardTokenizerInterface
+%function getNextToken
+%char
+%buffer 4096
+
+%include SUPPLEMENTARY.jflex-macro
+ALetter           = (\p{WB:ALetter}                                     | {ALetterSupp})
+Format            = (\p{WB:Format}                                      | {FormatSupp})
+Numeric           = ([\p{WB:Numeric}[\p{Blk:HalfAndFullForms}&&\p{Nd}]] | {NumericSupp})
+Extend            = (\p{WB:Extend}                                      | {ExtendSupp})
+Katakana          = (\p{WB:Katakana}                                    | {KatakanaSupp})
+MidLetter         = (\p{WB:MidLetter}                                   | {MidLetterSupp})
+MidNum            = (\p{WB:MidNum}                                      | {MidNumSupp})
+MidNumLet         = (\p{WB:MidNumLet}                                   | {MidNumLetSupp})
+ExtendNumLet      = (\p{WB:ExtendNumLet}                                | {ExtendNumLetSupp})
+ComplexContext    = (\p{LB:Complex_Context}                             | {ComplexContextSupp})
+Han               = (\p{Script:Han}                                     | {HanSupp})
+Hiragana          = (\p{Script:Hiragana}                                | {HiraganaSupp})
+SingleQuote       = (\p{WB:Single_Quote}                                | {SingleQuoteSupp})
+DoubleQuote       = (\p{WB:Double_Quote}                                | {DoubleQuoteSupp})
+HebrewLetter      = (\p{WB:Hebrew_Letter}                               | {HebrewLetterSupp})
+RegionalIndicator = (\p{WB:Regional_Indicator}                          | {RegionalIndicatorSupp})
+HebrewOrALetter   = ({HebrewLetter} | {ALetter})
+
+// UAX#29 WB4. X (Extend | Format)* --> X
+//
+HangulEx            = [\p{Script:Hangul}&&[\p{WB:ALetter}\p{WB:Hebrew_Letter}]] ({Format} | {Extend})*
+HebrewOrALetterEx   = {HebrewOrALetter}                                         ({Format} | {Extend})*
+NumericEx           = {Numeric}                                                 ({Format} | {Extend})*
+KatakanaEx          = {Katakana}                                                ({Format} | {Extend})* 
+MidLetterEx         = ({MidLetter} | {MidNumLet} | {SingleQuote})               ({Format} | {Extend})* 
+MidNumericEx        = ({MidNum} | {MidNumLet} | {SingleQuote})                  ({Format} | {Extend})*
+ExtendNumLetEx      = {ExtendNumLet}                                            ({Format} | {Extend})*
+HanEx               = {Han}                                                     ({Format} | {Extend})*
+HiraganaEx          = {Hiragana}                                                ({Format} | {Extend})*
+SingleQuoteEx       = {SingleQuote}                                             ({Format} | {Extend})*                                            
+DoubleQuoteEx       = {DoubleQuote}                                             ({Format} | {Extend})*
+HebrewLetterEx      = {HebrewLetter}                                            ({Format} | {Extend})*
+RegionalIndicatorEx = {RegionalIndicator}                                       ({Format} | {Extend})*
+
+
+%{
+  /** Alphanumeric sequences */
+  public static final int WORD_TYPE = StandardAnalyzer.TokenType.ALPHANUM.value;
+  
+  /** Numbers */
+  public static final int NUMERIC_TYPE = StandardAnalyzer.TokenType.NUM.value;
+  
+  /**
+   * Chars in class \p{Line_Break = Complex_Context} are from South East Asian
+   * scripts (Thai, Lao, Myanmar, Khmer, etc.).  Sequences of these are kept 
+   * together as as a single token rather than broken up, because the logic
+   * required to break them at word boundaries is too complex for UAX#29.
+   * <p>
+   * See Unicode Line Breaking Algorithm: http://www.unicode.org/reports/tr14/#SA
+   */
+  public static final int SOUTH_EAST_ASIAN_TYPE = StandardAnalyzer.TokenType.SOUTHEAST_ASIAN.value;
+  
+  public static final int IDEOGRAPHIC_TYPE = StandardAnalyzer.TokenType.IDEOGRAPHIC.value;
+  
+  public static final int HIRAGANA_TYPE = StandardAnalyzer.TokenType.HIRAGANA.value;
+  
+  public static final int KATAKANA_TYPE = StandardAnalyzer.TokenType.KATAKANA.value;
+  
+  public static final int HANGUL_TYPE = StandardAnalyzer.TokenType.HANGUL.value;
+
+  public final int yychar()
+  {
+    return yychar;
+  }
+
+  public String getText()
+  {
+    return String.valueOf(zzBuffer, zzStartRead, zzMarkedPos-zzStartRead);
+  }
+
+  public char[] getArray()
+  {
+    return Arrays.copyOfRange(zzBuffer, zzStartRead, zzMarkedPos);
+  }
+
+  public byte[] getBytes()
+  {
+    return getText().getBytes();
+  }
+
+%}
+
+%%
+
+// UAX#29 WB1.   sot   ÷
+//        WB2.     ÷   eot
+//
+<<EOF>> { return StandardAnalyzer.TokenType.EOF.value; }
+
+// UAX#29 WB8.   Numeric × Numeric
+//        WB11.  Numeric (MidNum | MidNumLet | Single_Quote) × Numeric
+//        WB12.  Numeric × (MidNum | MidNumLet | Single_Quote) Numeric
+//        WB13a. (ALetter | Hebrew_Letter | Numeric | Katakana | ExtendNumLet) × ExtendNumLet
+//        WB13b. ExtendNumLet × (ALetter | Hebrew_Letter | Numeric | Katakana) 
+//
+{ExtendNumLetEx}* {NumericEx} ( ( {ExtendNumLetEx}* | {MidNumericEx} ) {NumericEx} )* {ExtendNumLetEx}* 
+  { return NUMERIC_TYPE; }
+
+// subset of the below for typing purposes only!
+{HangulEx}+
+  { return HANGUL_TYPE; }
+  
+{KatakanaEx}+
+  { return KATAKANA_TYPE; }
+
+// UAX#29 WB5.   (ALetter | Hebrew_Letter) × (ALetter | Hebrew_Letter)
+//        WB6.   (ALetter | Hebrew_Letter) × (MidLetter | MidNumLet | Single_Quote) (ALetter | Hebrew_Letter)
+//        WB7.   (ALetter | Hebrew_Letter) (MidLetter | MidNumLet | Single_Quote) × (ALetter | Hebrew_Letter)
+//        WB7a.  Hebrew_Letter × Single_Quote
+//        WB7b.  Hebrew_Letter × Double_Quote Hebrew_Letter
+//        WB7c.  Hebrew_Letter Double_Quote × Hebrew_Letter
+//        WB9.   (ALetter | Hebrew_Letter) × Numeric
+//        WB10.  Numeric × (ALetter | Hebrew_Letter)
+//        WB13.  Katakana × Katakana
+//        WB13a. (ALetter | Hebrew_Letter | Numeric | Katakana | ExtendNumLet) × ExtendNumLet
+//        WB13b. ExtendNumLet × (ALetter | Hebrew_Letter | Numeric | Katakana) 
+//
+{ExtendNumLetEx}*  ( {KatakanaEx}          ( {ExtendNumLetEx}*   {KatakanaEx}                           )*
+                   | ( {HebrewLetterEx}    ( {SingleQuoteEx}     | {DoubleQuoteEx}  {HebrewLetterEx}    )
+                     | {NumericEx}         ( ( {ExtendNumLetEx}* | {MidNumericEx} ) {NumericEx}         )*
+                     | {HebrewOrALetterEx} ( ( {ExtendNumLetEx}* | {MidLetterEx}  ) {HebrewOrALetterEx} )*
+                     )+
+                   )
+({ExtendNumLetEx}+ ( {KatakanaEx}          ( {ExtendNumLetEx}*   {KatakanaEx}                           )*
+                   | ( {HebrewLetterEx}    ( {SingleQuoteEx}     | {DoubleQuoteEx}  {HebrewLetterEx}    )
+                     | {NumericEx}         ( ( {ExtendNumLetEx}* | {MidNumericEx} ) {NumericEx}         )*
+                     | {HebrewOrALetterEx} ( ( {ExtendNumLetEx}* | {MidLetterEx}  ) {HebrewOrALetterEx} )*
+                     )+
+                   )
+)*
+{ExtendNumLetEx}* 
+  { return WORD_TYPE; }
+
+
+// From UAX #29:
+//
+//    [C]haracters with the Line_Break property values of Contingent_Break (CB), 
+//    Complex_Context (SA/South East Asian), and XX (Unknown) are assigned word 
+//    boundary property values based on criteria outside of the scope of this
+//    annex.  That means that satisfactory treatment of languages like Chinese
+//    or Thai requires special handling.
+// 
+// In Unicode 6.3, only one character has the \p{Line_Break = Contingent_Break}
+// property: U+FFFC (  ) OBJECT REPLACEMENT CHARACTER.
+//
+// In the ICU implementation of UAX#29, \p{Line_Break = Complex_Context}
+// character sequences (from South East Asian scripts like Thai, Myanmar, Khmer,
+// Lao, etc.) are kept together.  This grammar does the same below.
+//
+// See also the Unicode Line Breaking Algorithm:
+//
+//    http://www.unicode.org/reports/tr14/#SA
+//
+{ComplexContext}+ { return SOUTH_EAST_ASIAN_TYPE; }
+
+// UAX#29 WB14.  Any ÷ Any
+//
+{HanEx} { return IDEOGRAPHIC_TYPE; }
+{HiraganaEx} { return HIRAGANA_TYPE; }
+
+
+// UAX#29 WB3.   CR × LF
+//        WB3a.  (Newline | CR | LF) ÷
+//        WB3b.  ÷ (Newline | CR | LF)
+//        WB13c. Regional_Indicator × Regional_Indicator
+//        WB14.  Any ÷ Any
+//
+{RegionalIndicatorEx} {RegionalIndicatorEx}+ | [^]
+  { /* Break so we don't hit fall-through warning: */ break; /* Not numeric, word, ideographic, hiragana, or SE Asian -- ignore it. */ }
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerInterface.java b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerInterface.java
new file mode 100644
index 0000000..57e35d7
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerInterface.java
@@ -0,0 +1,65 @@
+/*
+ * 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.index.sasi.analyzer;
+
+import java.io.IOException;
+import java.io.Reader;
+
+/**
+ * Internal interface for supporting versioned grammars.
+ */
+public interface StandardTokenizerInterface
+{
+
+    String getText();
+
+    char[] getArray();
+
+    byte[] getBytes();
+
+    /**
+     * Returns the current position.
+     */
+    int yychar();
+
+    /**
+     * Returns the length of the matched text region.
+     */
+    int yylength();
+
+    /**
+     * Resumes scanning until the next regular expression is matched,
+     * the end of input is encountered or an I/O-Error occurs.
+     *
+     * @return      the next token, {@link #YYEOF} on end of stream
+     * @exception   java.io.IOException  if any I/O-Error occurs
+     */
+    int getNextToken() throws IOException;
+
+    /**
+     * Resets the scanner to read from a new input stream.
+     * Does not close the old reader.
+     *
+     * All internal variables are reset, the old input stream
+     * <b>cannot</b> be reused (internal buffer is discarded and lost).
+     * Lexical state is set to <tt>ZZ_INITIAL</tt>.
+     *
+     * @param reader   the new input stream
+     */
+    void yyreset(Reader reader);
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerOptions.java b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerOptions.java
new file mode 100644
index 0000000..da44f0a
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerOptions.java
@@ -0,0 +1,273 @@
+/*
+ * 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.index.sasi.analyzer;
+
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Various options for controlling tokenization and enabling
+ * or disabling features
+ */
+public class StandardTokenizerOptions
+{
+    public static final String TOKENIZATION_ENABLE_STEMMING = "tokenization_enable_stemming";
+    public static final String TOKENIZATION_SKIP_STOP_WORDS = "tokenization_skip_stop_words";
+    public static final String TOKENIZATION_LOCALE = "tokenization_locale";
+    public static final String TOKENIZATION_NORMALIZE_LOWERCASE = "tokenization_normalize_lowercase";
+    public static final String TOKENIZATION_NORMALIZE_UPPERCASE = "tokenization_normalize_uppercase";
+
+    public static final int DEFAULT_MAX_TOKEN_LENGTH = 255;
+    public static final int DEFAULT_MIN_TOKEN_LENGTH = 0;
+
+    private boolean stemTerms;
+    private boolean ignoreStopTerms;
+    private Locale locale;
+    private boolean caseSensitive;
+    private boolean allTermsToUpperCase;
+    private boolean allTermsToLowerCase;
+    private int minTokenLength;
+    private int maxTokenLength;
+
+    public boolean shouldStemTerms()
+    {
+        return stemTerms;
+    }
+
+    public void setStemTerms(boolean stemTerms)
+    {
+        this.stemTerms = stemTerms;
+    }
+
+    public boolean shouldIgnoreStopTerms()
+    {
+        return ignoreStopTerms;
+    }
+
+    public void setIgnoreStopTerms(boolean ignoreStopTerms)
+    {
+        this.ignoreStopTerms = ignoreStopTerms;
+    }
+
+    public Locale getLocale()
+    {
+        return locale;
+    }
+
+    public void setLocale(Locale locale)
+    {
+        this.locale = locale;
+    }
+
+    public boolean isCaseSensitive()
+    {
+        return caseSensitive;
+    }
+
+    public void setCaseSensitive(boolean caseSensitive)
+    {
+        this.caseSensitive = caseSensitive;
+    }
+
+    public boolean shouldUpperCaseTerms()
+    {
+        return allTermsToUpperCase;
+    }
+
+    public void setAllTermsToUpperCase(boolean allTermsToUpperCase)
+    {
+        this.allTermsToUpperCase = allTermsToUpperCase;
+    }
+
+    public boolean shouldLowerCaseTerms()
+    {
+        return allTermsToLowerCase;
+    }
+
+    public void setAllTermsToLowerCase(boolean allTermsToLowerCase)
+    {
+        this.allTermsToLowerCase = allTermsToLowerCase;
+    }
+
+    public int getMinTokenLength()
+    {
+        return minTokenLength;
+    }
+
+    public void setMinTokenLength(int minTokenLength)
+    {
+        this.minTokenLength = minTokenLength;
+    }
+
+    public int getMaxTokenLength()
+    {
+        return maxTokenLength;
+    }
+
+    public void setMaxTokenLength(int maxTokenLength)
+    {
+        this.maxTokenLength = maxTokenLength;
+    }
+
+    public static class OptionsBuilder 
+    {
+        private boolean stemTerms;
+        private boolean ignoreStopTerms;
+        private Locale locale;
+        private boolean caseSensitive;
+        private boolean allTermsToUpperCase;
+        private boolean allTermsToLowerCase;
+        private int minTokenLength = DEFAULT_MIN_TOKEN_LENGTH;
+        private int maxTokenLength = DEFAULT_MAX_TOKEN_LENGTH;
+
+        public OptionsBuilder()
+        {
+        }
+
+        public OptionsBuilder stemTerms(boolean stemTerms)
+        {
+            this.stemTerms = stemTerms;
+            return this;
+        }
+
+        public OptionsBuilder ignoreStopTerms(boolean ignoreStopTerms)
+        {
+            this.ignoreStopTerms = ignoreStopTerms;
+            return this;
+        }
+
+        public OptionsBuilder useLocale(Locale locale)
+        {
+            this.locale = locale;
+            return this;
+        }
+
+        public OptionsBuilder caseSensitive(boolean caseSensitive)
+        {
+            this.caseSensitive = caseSensitive;
+            return this;
+        }
+
+        public OptionsBuilder alwaysUpperCaseTerms(boolean allTermsToUpperCase)
+        {
+            this.allTermsToUpperCase = allTermsToUpperCase;
+            return this;
+        }
+
+        public OptionsBuilder alwaysLowerCaseTerms(boolean allTermsToLowerCase)
+        {
+            this.allTermsToLowerCase = allTermsToLowerCase;
+            return this;
+        }
+
+        /**
+         * Set the min allowed token length.  Any token shorter
+         * than this is skipped.
+         */
+        public OptionsBuilder minTokenLength(int minTokenLength)
+        {
+            if (minTokenLength < 1)
+                throw new IllegalArgumentException("minTokenLength must be greater than zero");
+            this.minTokenLength = minTokenLength;
+            return this;
+        }
+
+        /**
+         * Set the max allowed token length.  Any token longer
+         * than this is skipped.
+         */
+        public OptionsBuilder maxTokenLength(int maxTokenLength)
+        {
+            if (maxTokenLength < 1)
+                throw new IllegalArgumentException("maxTokenLength must be greater than zero");
+            this.maxTokenLength = maxTokenLength;
+            return this;
+        }
+
+        public StandardTokenizerOptions build()
+        {
+            if(allTermsToLowerCase && allTermsToUpperCase)
+                throw new IllegalArgumentException("Options to normalize terms cannot be " +
+                        "both uppercase and lowercase at the same time");
+
+            StandardTokenizerOptions options = new StandardTokenizerOptions();
+            options.setIgnoreStopTerms(ignoreStopTerms);
+            options.setStemTerms(stemTerms);
+            options.setLocale(locale);
+            options.setCaseSensitive(caseSensitive);
+            options.setAllTermsToLowerCase(allTermsToLowerCase);
+            options.setAllTermsToUpperCase(allTermsToUpperCase);
+            options.setMinTokenLength(minTokenLength);
+            options.setMaxTokenLength(maxTokenLength);
+            return options;
+        }
+    }
+
+    public static StandardTokenizerOptions buildFromMap(Map<String, String> optionsMap)
+    {
+        OptionsBuilder optionsBuilder = new OptionsBuilder();
+
+        for (Map.Entry<String, String> entry : optionsMap.entrySet())
+        {
+            switch(entry.getKey())
+            {
+                case TOKENIZATION_ENABLE_STEMMING:
+                {
+                    boolean bool = Boolean.parseBoolean(entry.getValue());
+                    optionsBuilder = optionsBuilder.stemTerms(bool);
+                    break;
+                }
+                case TOKENIZATION_SKIP_STOP_WORDS:
+                {
+                    boolean bool = Boolean.parseBoolean(entry.getValue());
+                    optionsBuilder = optionsBuilder.ignoreStopTerms(bool);
+                    break;
+                }
+                case TOKENIZATION_LOCALE:
+                {
+                    Locale locale = new Locale(entry.getValue());
+                    optionsBuilder = optionsBuilder.useLocale(locale);
+                    break;
+                }
+                case TOKENIZATION_NORMALIZE_UPPERCASE:
+                {
+                    boolean bool = Boolean.parseBoolean(entry.getValue());
+                    optionsBuilder = optionsBuilder.alwaysUpperCaseTerms(bool);
+                    break;
+                }
+                case TOKENIZATION_NORMALIZE_LOWERCASE:
+                {
+                    boolean bool = Boolean.parseBoolean(entry.getValue());
+                    optionsBuilder = optionsBuilder.alwaysLowerCaseTerms(bool);
+                    break;
+                }
+                default:
+                {
+                }
+            }
+        }
+        return optionsBuilder.build();
+    }
+
+    public static StandardTokenizerOptions getDefaultOptions()
+    {
+        return new OptionsBuilder()
+                .ignoreStopTerms(true).alwaysLowerCaseTerms(true)
+                .stemTerms(false).useLocale(Locale.ENGLISH).build();
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/BasicResultFilters.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/BasicResultFilters.java
new file mode 100644
index 0000000..2b949b8
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/BasicResultFilters.java
@@ -0,0 +1,76 @@
+/*
+ * 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.index.sasi.analyzer.filter;
+
+import java.util.Locale;
+
+/**
+ * Basic/General Token Filters
+ */
+public class BasicResultFilters
+{
+    private static final Locale DEFAULT_LOCALE = Locale.getDefault();
+
+    public static class LowerCase extends FilterPipelineTask<String, String>
+    {
+        private Locale locale;
+
+        public LowerCase(Locale locale)
+        {
+            this.locale = locale;
+        }
+
+        public LowerCase()
+        {
+            this.locale = DEFAULT_LOCALE;
+        }
+
+        public String process(String input) throws Exception
+        {
+            return input.toLowerCase(locale);
+        }
+    }
+
+    public static class UpperCase extends FilterPipelineTask<String, String>
+    {
+        private Locale locale;
+
+        public UpperCase(Locale locale)
+        {
+            this.locale = locale;
+        }
+
+        public UpperCase()
+        {
+            this.locale = DEFAULT_LOCALE;
+        }
+
+        public String process(String input) throws Exception
+        {
+            return input.toUpperCase(locale);
+        }
+    }
+
+    public static class NoOperation extends FilterPipelineTask<Object, Object>
+    {
+        public Object process(Object input) throws Exception
+        {
+            return input;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineBuilder.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineBuilder.java
new file mode 100644
index 0000000..e9d262d
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineBuilder.java
@@ -0,0 +1,51 @@
+/*
+ * 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.index.sasi.analyzer.filter;
+
+/**
+ * Creates a Pipeline object for applying n pieces of logic
+ * from the provided methods to the builder in a guaranteed order
+ */
+public class FilterPipelineBuilder
+{
+    private final FilterPipelineTask<?,?> parent;
+    private FilterPipelineTask<?,?> current;
+
+    public FilterPipelineBuilder(FilterPipelineTask<?, ?> first)
+    {
+        this(first, first);
+    }
+
+    private FilterPipelineBuilder(FilterPipelineTask<?, ?> first, FilterPipelineTask<?, ?> current)
+    {
+        this.parent = first;
+        this.current = current;
+    }
+
+    public FilterPipelineBuilder add(String name, FilterPipelineTask<?,?> nextTask)
+    {
+        this.current.setLast(name, nextTask);
+        this.current = nextTask;
+        return this;
+    }
+
+    public FilterPipelineTask<?,?> build()
+    {
+        return this.parent;
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineExecutor.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineExecutor.java
new file mode 100644
index 0000000..68c055e
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineExecutor.java
@@ -0,0 +1,53 @@
+/*
+ * 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.index.sasi.analyzer.filter;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Executes all linked Pipeline Tasks serially and returns
+ * output (if exists) from the executed logic
+ */
+public class FilterPipelineExecutor
+{
+    private static final Logger logger = LoggerFactory.getLogger(FilterPipelineExecutor.class);
+
+    public static <F,T> T execute(FilterPipelineTask<F, T> task, T initialInput)
+    {
+        FilterPipelineTask<?, ?> taskPtr = task;
+        T result = initialInput;
+        try
+        {
+            while (true)
+            {
+                FilterPipelineTask<F,T> taskGeneric = (FilterPipelineTask<F,T>) taskPtr;
+                result = taskGeneric.process((F) result);
+                taskPtr = taskPtr.next;
+                if(taskPtr == null)
+                    return result;
+            }
+        }
+        catch (Exception e)
+        {
+            logger.info("An unhandled exception to occurred while processing " +
+                    "pipeline [{}]", task.getName(), e);
+        }
+        return null;
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineTask.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineTask.java
new file mode 100644
index 0000000..13e2a17
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/FilterPipelineTask.java
@@ -0,0 +1,52 @@
+/*
+ * 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.index.sasi.analyzer.filter;
+
+/**
+ * A single task or set of work to process an input
+ * and return a single output. Maintains a link to the
+ * next task to be executed after itself
+ */
+public abstract class FilterPipelineTask<F, T>
+{
+    private String name;
+    public FilterPipelineTask<?, ?> next;
+
+    protected <K, V> void setLast(String name, FilterPipelineTask<K, V> last)
+    {
+        if (last == this)
+            throw new IllegalArgumentException("provided last task [" + last.name + "] cannot be set to itself");
+
+        if (this.next == null)
+        {
+            this.next = last;
+            this.name = name;
+        }
+        else
+        {
+            this.next.setLast(name, last);
+        }
+    }
+
+    public abstract T process(F input) throws Exception;
+
+    public String getName()
+    {
+        return name;
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StemmerFactory.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StemmerFactory.java
new file mode 100644
index 0000000..ae232db
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StemmerFactory.java
@@ -0,0 +1,102 @@
+/*
+ * 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.index.sasi.analyzer.filter;
+
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.tartarus.snowball.SnowballStemmer;
+import org.tartarus.snowball.ext.*;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Returns a SnowballStemmer instance appropriate for
+ * a given language
+ */
+public class StemmerFactory
+{
+    private static final Logger logger = LoggerFactory.getLogger(StemmerFactory.class);
+    private static final LoadingCache<Class, Constructor<?>> STEMMER_CONSTRUCTOR_CACHE = CacheBuilder.newBuilder()
+            .build(new CacheLoader<Class, Constructor<?>>()
+            {
+                public Constructor<?> load(Class aClass) throws Exception
+                {
+                    try
+                    {
+                        return aClass.getConstructor();
+                    }
+                    catch (Exception e) 
+                    {
+                        logger.error("Failed to get stemmer constructor", e);
+                    }
+                    return null;
+                }
+            });
+
+    private static final Map<String, Class> SUPPORTED_LANGUAGES;
+
+    static
+    {
+        SUPPORTED_LANGUAGES = new HashMap<>();
+        SUPPORTED_LANGUAGES.put("de", germanStemmer.class);
+        SUPPORTED_LANGUAGES.put("da", danishStemmer.class);
+        SUPPORTED_LANGUAGES.put("es", spanishStemmer.class);
+        SUPPORTED_LANGUAGES.put("en", englishStemmer.class);
+        SUPPORTED_LANGUAGES.put("fl", finnishStemmer.class);
+        SUPPORTED_LANGUAGES.put("fr", frenchStemmer.class);
+        SUPPORTED_LANGUAGES.put("hu", hungarianStemmer.class);
+        SUPPORTED_LANGUAGES.put("it", italianStemmer.class);
+        SUPPORTED_LANGUAGES.put("nl", dutchStemmer.class);
+        SUPPORTED_LANGUAGES.put("no", norwegianStemmer.class);
+        SUPPORTED_LANGUAGES.put("pt", portugueseStemmer.class);
+        SUPPORTED_LANGUAGES.put("ro", romanianStemmer.class);
+        SUPPORTED_LANGUAGES.put("ru", russianStemmer.class);
+        SUPPORTED_LANGUAGES.put("sv", swedishStemmer.class);
+        SUPPORTED_LANGUAGES.put("tr", turkishStemmer.class);
+    }
+
+    public static SnowballStemmer getStemmer(Locale locale)
+    {
+        if (locale == null)
+            return null;
+
+        String rootLang = locale.getLanguage().substring(0, 2);
+        try
+        {
+            Class clazz = SUPPORTED_LANGUAGES.get(rootLang);
+            if(clazz == null)
+                return null;
+            Constructor<?> ctor = STEMMER_CONSTRUCTOR_CACHE.get(clazz);
+            return (SnowballStemmer) ctor.newInstance();
+        }
+        catch (Exception e)
+        {
+            logger.debug("Failed to create new SnowballStemmer instance " +
+                    "for language [{}]", locale.getLanguage(), e);
+        }
+        return null;
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StemmingFilters.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StemmingFilters.java
new file mode 100644
index 0000000..cb840a8
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StemmingFilters.java
@@ -0,0 +1,46 @@
+/*
+ * 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.index.sasi.analyzer.filter;
+
+import java.util.Locale;
+
+import org.tartarus.snowball.SnowballStemmer;
+
+/**
+ * Filters for performing Stemming on tokens
+ */
+public class StemmingFilters
+{
+    public static class DefaultStemmingFilter extends FilterPipelineTask<String, String>
+    {
+        private SnowballStemmer stemmer;
+
+        public DefaultStemmingFilter(Locale locale)
+        {
+            stemmer = StemmerFactory.getStemmer(locale);
+        }
+
+        public String process(String input) throws Exception
+        {
+            if (input == null || stemmer == null)
+                return input;
+            stemmer.setCurrent(input);
+            return (stemmer.stem()) ? stemmer.getCurrent() : input;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StopWordFactory.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StopWordFactory.java
new file mode 100644
index 0000000..8ec02e0
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StopWordFactory.java
@@ -0,0 +1,100 @@
+/*
+ * 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.index.sasi.analyzer.filter;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides a list of Stop Words for a given language
+ */
+public class StopWordFactory
+{
+    private static final Logger logger = LoggerFactory.getLogger(StopWordFactory.class);
+
+    private static final String DEFAULT_RESOURCE_EXT = "_ST.txt";
+    private static final String DEFAULT_RESOURCE_PREFIX = StopWordFactory.class.getPackage()
+            .getName().replace(".", File.separator);
+    private static final Set<String> SUPPORTED_LANGUAGES = new HashSet<>(
+            Arrays.asList("ar","bg","cs","de","en","es","fi","fr","hi","hu","it",
+            "pl","pt","ro","ru","sv"));
+
+    private static final LoadingCache<String, Set<String>> STOP_WORDS_CACHE = CacheBuilder.newBuilder()
+            .build(new CacheLoader<String, Set<String>>()
+            {
+                public Set<String> load(String s)
+                {
+                    return getStopWordsFromResource(s);
+                }
+            });
+
+    public static Set<String> getStopWordsForLanguage(Locale locale)
+    {
+        if (locale == null)
+            return null;
+
+        String rootLang = locale.getLanguage().substring(0, 2);
+        try
+        {
+            return (!SUPPORTED_LANGUAGES.contains(rootLang)) ? null : STOP_WORDS_CACHE.get(rootLang);
+        }
+        catch (ExecutionException e)
+        {
+            logger.error("Failed to populate Stop Words Cache for language [{}]", locale.getLanguage(), e);
+            return null;
+        }
+    }
+
+    private static Set<String> getStopWordsFromResource(String language)
+    {
+        Set<String> stopWords = new HashSet<>();
+        String resourceName = DEFAULT_RESOURCE_PREFIX + File.separator + language + DEFAULT_RESOURCE_EXT;
+        try (InputStream is = StopWordFactory.class.getClassLoader().getResourceAsStream(resourceName);
+             BufferedReader r = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)))
+        {
+                String line;
+                while ((line = r.readLine()) != null)
+                {
+                    //skip comments (lines starting with # char)
+                    if(line.charAt(0) == '#')
+                        continue;
+                    stopWords.add(line.trim());
+                }
+        }
+        catch (Exception e)
+        {
+            logger.error("Failed to retrieve Stop Terms resource for language [{}]", language, e);
+        }
+        return stopWords;
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StopWordFilters.java b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StopWordFilters.java
new file mode 100644
index 0000000..4ae849c
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/filter/StopWordFilters.java
@@ -0,0 +1,42 @@
+/*
+ * 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.index.sasi.analyzer.filter;
+
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Filter implementations for input matching Stop Words
+ */
+public class StopWordFilters
+{
+    public static class DefaultStopWordFilter extends FilterPipelineTask<String, String>
+    {
+        private Set<String> stopWords = null;
+
+        public DefaultStopWordFilter(Locale locale)
+        {
+            this.stopWords = StopWordFactory.getStopWordsForLanguage(locale);
+        }
+
+        public String process(String input) throws Exception
+        {
+            return (stopWords != null && stopWords.contains(input)) ? null : input;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/conf/ColumnIndex.java b/src/java/org/apache/cassandra/index/sasi/conf/ColumnIndex.java
new file mode 100644
index 0000000..0958113
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/conf/ColumnIndex.java
@@ -0,0 +1,259 @@
+/*
+ * 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.index.sasi.conf;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Memtable;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.AsciiType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.rows.Cell;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
+import org.apache.cassandra.index.sasi.conf.view.View;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.memory.IndexMemtable;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.index.sasi.plan.Expression.Op;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
+import org.apache.cassandra.io.sstable.Component;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.utils.FBUtilities;
+
+public class ColumnIndex
+{
+    private static final String FILE_NAME_FORMAT = "SI_%s.db";
+
+    private final AbstractType<?> keyValidator;
+
+    private final ColumnDefinition column;
+    private final Optional<IndexMetadata> config;
+
+    private final AtomicReference<IndexMemtable> memtable;
+    private final ConcurrentMap<Memtable, IndexMemtable> pendingFlush = new ConcurrentHashMap<>();
+
+    private final IndexMode mode;
+
+    private final Component component;
+    private final DataTracker tracker;
+
+    private final boolean isTokenized;
+
+    public ColumnIndex(AbstractType<?> keyValidator, ColumnDefinition column, IndexMetadata metadata)
+    {
+        this.keyValidator = keyValidator;
+        this.column = column;
+        this.config = metadata == null ? Optional.empty() : Optional.of(metadata);
+        this.mode = IndexMode.getMode(column, config);
+        this.memtable = new AtomicReference<>(new IndexMemtable(this));
+        this.tracker = new DataTracker(keyValidator, this);
+        this.component = new Component(Component.Type.SECONDARY_INDEX, String.format(FILE_NAME_FORMAT, getIndexName()));
+        this.isTokenized = getAnalyzer().isTokenizing();
+    }
+
+    /**
+     * Initialize this column index with specific set of SSTables.
+     *
+     * @param sstables The sstables to be used by index initially.
+     *
+     * @return A collection of sstables which don't have this specific index attached to them.
+     */
+    public Iterable<SSTableReader> init(Set<SSTableReader> sstables)
+    {
+        return tracker.update(Collections.emptySet(), sstables);
+    }
+
+    public AbstractType<?> keyValidator()
+    {
+        return keyValidator;
+    }
+
+    public long index(DecoratedKey key, Row row)
+    {
+        return getCurrentMemtable().index(key, getValueOf(column, row, FBUtilities.nowInSeconds()));
+    }
+
+    public void switchMemtable()
+    {
+        // discard current memtable with all of it's data, useful on truncate
+        memtable.set(new IndexMemtable(this));
+    }
+
+    public void switchMemtable(Memtable parent)
+    {
+        pendingFlush.putIfAbsent(parent, memtable.getAndSet(new IndexMemtable(this)));
+    }
+
+    public void discardMemtable(Memtable parent)
+    {
+        pendingFlush.remove(parent);
+    }
+
+    @VisibleForTesting
+    public IndexMemtable getCurrentMemtable()
+    {
+        return memtable.get();
+    }
+
+    @VisibleForTesting
+    public Collection<IndexMemtable> getPendingMemtables()
+    {
+        return pendingFlush.values();
+    }
+
+    public RangeIterator<Long, Token> searchMemtable(Expression e)
+    {
+        RangeIterator.Builder<Long, Token> builder = new RangeUnionIterator.Builder<>();
+        builder.add(getCurrentMemtable().search(e));
+        for (IndexMemtable memtable : getPendingMemtables())
+            builder.add(memtable.search(e));
+
+        return builder.build();
+    }
+
+    public void update(Collection<SSTableReader> oldSSTables, Collection<SSTableReader> newSSTables)
+    {
+        tracker.update(oldSSTables, newSSTables);
+    }
+
+    public ColumnDefinition getDefinition()
+    {
+        return column;
+    }
+
+    public AbstractType<?> getValidator()
+    {
+        return column.cellValueType();
+    }
+
+    public Component getComponent()
+    {
+        return component;
+    }
+
+    public IndexMode getMode()
+    {
+        return mode;
+    }
+
+    public String getColumnName()
+    {
+        return column.name.toString();
+    }
+
+    public String getIndexName()
+    {
+        return config.isPresent() ? config.get().name : "undefined";
+    }
+
+    public AbstractAnalyzer getAnalyzer()
+    {
+        AbstractAnalyzer analyzer = mode.getAnalyzer(getValidator());
+        analyzer.init(config.isPresent() ? config.get().options : Collections.emptyMap(), column.cellValueType());
+        return analyzer;
+    }
+
+    public View getView()
+    {
+        return tracker.getView();
+    }
+
+    public boolean hasSSTable(SSTableReader sstable)
+    {
+        return tracker.hasSSTable(sstable);
+    }
+
+    public void dropData(Collection<SSTableReader> sstablesToRebuild)
+    {
+        tracker.dropData(sstablesToRebuild);
+    }
+
+    public void dropData(long truncateUntil)
+    {
+        switchMemtable();
+        tracker.dropData(truncateUntil);
+    }
+
+    public boolean isIndexed()
+    {
+        return mode != IndexMode.NOT_INDEXED;
+    }
+
+    public boolean isLiteral()
+    {
+        AbstractType<?> validator = getValidator();
+        return isIndexed() ? mode.isLiteral : (validator instanceof UTF8Type || validator instanceof AsciiType);
+    }
+
+    public boolean supports(Operator op)
+    {
+        if (op == Operator.LIKE)
+            return isLiteral();
+
+        Op operator = Op.valueOf(op);
+        return !(isTokenized && operator == Op.EQ) // EQ is only applicable to non-tokenized indexes
+               && !(isTokenized && mode.mode == OnDiskIndexBuilder.Mode.CONTAINS && operator == Op.PREFIX) // PREFIX not supported on tokenized CONTAINS mode indexes
+               && !(isLiteral() && operator == Op.RANGE) // RANGE only applicable to indexes non-literal indexes
+               && mode.supports(operator); // for all other cases let's refer to index itself
+
+    }
+
+    public static ByteBuffer getValueOf(ColumnDefinition column, Row row, int nowInSecs)
+    {
+        if (row == null)
+            return null;
+
+        switch (column.kind)
+        {
+            case CLUSTERING:
+                // skip indexing of static clustering when regular column is indexed
+                if (row.isStatic())
+                    return null;
+
+                return row.clustering().get(column.position());
+
+            // treat static cell retrieval the same was as regular
+            // only if row kind is STATIC otherwise return null
+            case STATIC:
+                if (!row.isStatic())
+                    return null;
+            case REGULAR:
+                Cell cell = row.getCell(column);
+                return cell == null || !cell.isLive(nowInSecs) ? null : cell.value();
+
+            default:
+                return null;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/conf/DataTracker.java b/src/java/org/apache/cassandra/index/sasi/conf/DataTracker.java
new file mode 100644
index 0000000..99516b8
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/conf/DataTracker.java
@@ -0,0 +1,192 @@
+/*
+ * 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.index.sasi.conf;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sasi.SSTableIndex;
+import org.apache.cassandra.index.sasi.conf.view.View;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.utils.Pair;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** a pared-down version of DataTracker and DT.View. need one for each index of each column family */
+public class DataTracker
+{
+    private static final Logger logger = LoggerFactory.getLogger(DataTracker.class);
+
+    private final AbstractType<?> keyValidator;
+    private final ColumnIndex columnIndex;
+    private final AtomicReference<View> view = new AtomicReference<>();
+
+    public DataTracker(AbstractType<?> keyValidator, ColumnIndex index)
+    {
+        this.keyValidator = keyValidator;
+        this.columnIndex = index;
+        this.view.set(new View(index, Collections.<SSTableIndex>emptySet()));
+    }
+
+    public View getView()
+    {
+        return view.get();
+    }
+
+    /**
+     * Replaces old SSTables with new by creating new immutable tracker.
+     *
+     * @param oldSSTables A set of SSTables to remove.
+     * @param newSSTables A set of SSTables to add to tracker.
+     *
+     * @return A collection of SSTables which don't have component attached for current index.
+     */
+    public Iterable<SSTableReader> update(Collection<SSTableReader> oldSSTables, Collection<SSTableReader> newSSTables)
+    {
+        final Pair<Set<SSTableIndex>, Set<SSTableReader>> built = getBuiltIndexes(newSSTables);
+        final Set<SSTableIndex> newIndexes = built.left;
+        final Set<SSTableReader> indexedSSTables = built.right;
+
+        View currentView, newView;
+        do
+        {
+            currentView = view.get();
+            newView = new View(columnIndex, currentView.getIndexes(), oldSSTables, newIndexes);
+        }
+        while (!view.compareAndSet(currentView, newView));
+
+        for (SSTableReader sstable : indexedSSTables)
+        {
+                sstable.addComponents(Collections.singleton(columnIndex.getComponent()));
+        }
+
+        return newSSTables.stream().filter(sstable -> !indexedSSTables.contains(sstable)).collect(Collectors.toList());
+    }
+
+    public boolean hasSSTable(SSTableReader sstable)
+    {
+        View currentView = view.get();
+        for (SSTableIndex index : currentView)
+        {
+            if (index.getSSTable().equals(sstable))
+                return true;
+        }
+
+        return false;
+    }
+
+    public void dropData(Collection<SSTableReader> sstablesToRebuild)
+    {
+        View currentView = view.get();
+        if (currentView == null)
+            return;
+
+        Set<SSTableReader> toRemove = new HashSet<>(sstablesToRebuild);
+        for (SSTableIndex index : currentView)
+        {
+            SSTableReader sstable = index.getSSTable();
+            if (!sstablesToRebuild.contains(sstable))
+                continue;
+
+            index.markObsolete();
+        }
+
+        update(toRemove, Collections.<SSTableReader>emptyList());
+    }
+
+    public void dropData(long truncateUntil)
+    {
+        View currentView = view.get();
+        if (currentView == null)
+            return;
+
+        Set<SSTableReader> toRemove = new HashSet<>();
+        for (SSTableIndex index : currentView)
+        {
+            SSTableReader sstable = index.getSSTable();
+            if (sstable.getMaxTimestamp() > truncateUntil)
+                continue;
+
+            index.markObsolete();
+            toRemove.add(sstable);
+        }
+
+        update(toRemove, Collections.<SSTableReader>emptyList());
+    }
+
+    private Pair<Set<SSTableIndex>, Set<SSTableReader>> getBuiltIndexes(Collection<SSTableReader> sstables)
+    {
+        Set<SSTableIndex> indexes = new HashSet<>(sstables.size());
+        Set<SSTableReader> builtSSTables = new HashSet<>(sstables.size());
+        for (SSTableReader sstable : sstables)
+        {
+            if (sstable.isMarkedCompacted())
+                continue;
+
+            File indexFile = new File(sstable.descriptor.filenameFor(columnIndex.getComponent()));
+            if (!indexFile.exists())
+                continue;
+
+            // if the index file is empty, we have to ignore it to avoid re-building, but it doesn't take
+            // a part in query process
+            if (indexFile.length() == 0)
+            {
+                builtSSTables.add(sstable);
+                continue;
+            }
+
+            SSTableIndex index = null;
+
+            try
+            {
+                index = new SSTableIndex(columnIndex, indexFile, sstable);
+
+                logger.info("SSTableIndex.open(column: {}, minTerm: {}, maxTerm: {}, minKey: {}, maxKey: {}, sstable: {})",
+                            columnIndex.getColumnName(),
+                            columnIndex.getValidator().getString(index.minTerm()),
+                            columnIndex.getValidator().getString(index.maxTerm()),
+                            keyValidator.getString(index.minKey()),
+                            keyValidator.getString(index.maxKey()),
+                            index.getSSTable());
+
+                // Try to add new index to the set, if set already has such index, we'll simply release and move on.
+                // This covers situation when sstable collection has the same sstable multiple
+                // times because we don't know what kind of collection it actually is.
+                if (indexes.add(index))
+                    builtSSTables.add(sstable);
+                else
+                    index.release();
+            }
+            catch (Throwable t)
+            {
+                logger.error("Can't open index file at " + indexFile.getAbsolutePath() + ", skipping.", t);
+                if (index != null)
+                    index.release();
+            }
+        }
+
+        return Pair.create(indexes, builtSSTables);
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java b/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java
new file mode 100644
index 0000000..e9ee703
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java
@@ -0,0 +1,203 @@
+/*
+ * 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.index.sasi.conf;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
+import org.apache.cassandra.index.sasi.analyzer.NoOpAnalyzer;
+import org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer;
+import org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.Mode;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.AsciiType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.index.sasi.plan.Expression.Op;
+import org.apache.cassandra.schema.IndexMetadata;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class IndexMode
+{
+    private static final Logger logger = LoggerFactory.getLogger(IndexMode.class);
+
+    public static final IndexMode NOT_INDEXED = new IndexMode(Mode.PREFIX, true, false, NonTokenizingAnalyzer.class, 0);
+
+    private static final Set<AbstractType<?>> TOKENIZABLE_TYPES = new HashSet<AbstractType<?>>()
+    {{
+        add(UTF8Type.instance);
+        add(AsciiType.instance);
+    }};
+
+    private static final String INDEX_MODE_OPTION = "mode";
+    private static final String INDEX_ANALYZED_OPTION = "analyzed";
+    private static final String INDEX_ANALYZER_CLASS_OPTION = "analyzer_class";
+    private static final String INDEX_IS_LITERAL_OPTION = "is_literal";
+    private static final String INDEX_MAX_FLUSH_MEMORY_OPTION = "max_compaction_flush_memory_in_mb";
+    private static final double INDEX_MAX_FLUSH_DEFAULT_MULTIPLIER = 0.15;
+    private static final long DEFAULT_MAX_MEM_BYTES = (long) (1073741824 * INDEX_MAX_FLUSH_DEFAULT_MULTIPLIER); // 1G default for memtable
+
+    public final Mode mode;
+    public final boolean isAnalyzed, isLiteral;
+    public final Class analyzerClass;
+    public final long maxCompactionFlushMemoryInBytes;
+
+    private IndexMode(Mode mode, boolean isLiteral, boolean isAnalyzed, Class analyzerClass, long maxMemBytes)
+    {
+        this.mode = mode;
+        this.isLiteral = isLiteral;
+        this.isAnalyzed = isAnalyzed;
+        this.analyzerClass = analyzerClass;
+        this.maxCompactionFlushMemoryInBytes = maxMemBytes;
+    }
+
+    public AbstractAnalyzer getAnalyzer(AbstractType<?> validator)
+    {
+        AbstractAnalyzer analyzer = new NoOpAnalyzer();
+
+        try
+        {
+            if (isAnalyzed)
+            {
+                if (analyzerClass != null)
+                    analyzer = (AbstractAnalyzer) analyzerClass.newInstance();
+                else if (TOKENIZABLE_TYPES.contains(validator))
+                    analyzer = new StandardAnalyzer();
+            }
+        }
+        catch (InstantiationException | IllegalAccessException e)
+        {
+            logger.error("Failed to create new instance of analyzer with class [{}]", analyzerClass.getName(), e);
+        }
+
+        return analyzer;
+    }
+
+    public static void validateAnalyzer(Map<String, String> indexOptions, ColumnDefinition cd) throws ConfigurationException
+    {
+        // validate that a valid analyzer class was provided if specified
+        if (indexOptions.containsKey(INDEX_ANALYZER_CLASS_OPTION))
+        {
+            Class<?> analyzerClass;
+            try
+            {
+                analyzerClass = Class.forName(indexOptions.get(INDEX_ANALYZER_CLASS_OPTION));
+            }
+            catch (ClassNotFoundException e)
+            {
+                throw new ConfigurationException(String.format("Invalid analyzer class option specified [%s]",
+                                                               indexOptions.get(INDEX_ANALYZER_CLASS_OPTION)));
+            }
+
+            AbstractAnalyzer analyzer;
+            try
+            {
+                analyzer = (AbstractAnalyzer) analyzerClass.newInstance();
+                analyzer.validate(indexOptions, cd);
+            }
+            catch (InstantiationException | IllegalAccessException e)
+            {
+                throw new ConfigurationException(String.format("Unable to initialize analyzer class option specified [%s]",
+                                                               analyzerClass.getSimpleName()));
+            }
+        }
+    }
+
+    public static IndexMode getMode(ColumnDefinition column, Optional<IndexMetadata> config) throws ConfigurationException
+    {
+        return getMode(column, config.isPresent() ? config.get().options : null);
+    }
+
+    public static IndexMode getMode(ColumnDefinition column, Map<String, String> indexOptions) throws ConfigurationException
+    {
+        if (indexOptions == null || indexOptions.isEmpty())
+            return IndexMode.NOT_INDEXED;
+
+        Mode mode;
+
+        try
+        {
+            mode = indexOptions.get(INDEX_MODE_OPTION) == null
+                            ? Mode.PREFIX
+                            : Mode.mode(indexOptions.get(INDEX_MODE_OPTION));
+        }
+        catch (IllegalArgumentException e)
+        {
+            throw new ConfigurationException("Incorrect index mode: " + indexOptions.get(INDEX_MODE_OPTION));
+        }
+
+        boolean isAnalyzed = false;
+        Class analyzerClass = null;
+        try
+        {
+            if (indexOptions.get(INDEX_ANALYZER_CLASS_OPTION) != null)
+            {
+                analyzerClass = Class.forName(indexOptions.get(INDEX_ANALYZER_CLASS_OPTION));
+                isAnalyzed = indexOptions.get(INDEX_ANALYZED_OPTION) == null
+                              ? true : Boolean.parseBoolean(indexOptions.get(INDEX_ANALYZED_OPTION));
+            }
+            else if (indexOptions.get(INDEX_ANALYZED_OPTION) != null)
+            {
+                isAnalyzed = Boolean.parseBoolean(indexOptions.get(INDEX_ANALYZED_OPTION));
+            }
+        }
+        catch (ClassNotFoundException e)
+        {
+            // should not happen as we already validated we could instantiate an instance in validateAnalyzer()
+            logger.error("Failed to find specified analyzer class [{}]. Falling back to default analyzer",
+                         indexOptions.get(INDEX_ANALYZER_CLASS_OPTION));
+        }
+
+        boolean isLiteral = false;
+        try
+        {
+            String literalOption = indexOptions.get(INDEX_IS_LITERAL_OPTION);
+            AbstractType<?> validator = column.cellValueType();
+
+            isLiteral = literalOption == null
+                            ? (validator instanceof UTF8Type || validator instanceof AsciiType)
+                            : Boolean.parseBoolean(literalOption);
+        }
+        catch (Exception e)
+        {
+            logger.error("failed to parse {} option, defaulting to 'false'.", INDEX_IS_LITERAL_OPTION);
+        }
+
+        long maxMemBytes = indexOptions.get(INDEX_MAX_FLUSH_MEMORY_OPTION) == null
+                ? DEFAULT_MAX_MEM_BYTES
+                : 1048576L * Long.parseLong(indexOptions.get(INDEX_MAX_FLUSH_MEMORY_OPTION));
+
+        if (maxMemBytes > 100L * 1073741824)
+        {
+            logger.error("{} configured as {} is above 100GB, reverting to default 1GB", INDEX_MAX_FLUSH_MEMORY_OPTION, maxMemBytes);
+            maxMemBytes = DEFAULT_MAX_MEM_BYTES;
+        }
+        return new IndexMode(mode, isLiteral, isAnalyzed, analyzerClass, maxMemBytes);
+    }
+
+    public boolean supports(Op operator)
+    {
+        return mode.supports(operator);
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/conf/view/PrefixTermTree.java b/src/java/org/apache/cassandra/index/sasi/conf/view/PrefixTermTree.java
new file mode 100644
index 0000000..f7cd942
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/conf/view/PrefixTermTree.java
@@ -0,0 +1,195 @@
+/*
+ * 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.index.sasi.conf.view;
+
+import java.nio.ByteBuffer;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.cassandra.index.sasi.SSTableIndex;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.index.sasi.utils.trie.KeyAnalyzer;
+import org.apache.cassandra.index.sasi.utils.trie.PatriciaTrie;
+import org.apache.cassandra.index.sasi.utils.trie.Trie;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.utils.Interval;
+import org.apache.cassandra.utils.IntervalTree;
+
+import com.google.common.collect.Sets;
+
+/**
+ * This class is an extension over RangeTermTree for string terms,
+ * it is required because interval tree can't handle matching if search is on the
+ * prefix of min/max of the range, so for ascii/utf8 fields we build an additional
+ * prefix trie (including both min/max terms of the index) and do union of the results
+ * of the prefix tree search and results from the interval tree lookup.
+ */
+public class PrefixTermTree extends RangeTermTree
+{
+    private final OnDiskIndexBuilder.Mode mode;
+    private final Trie<ByteBuffer, Set<SSTableIndex>> trie;
+
+    public PrefixTermTree(ByteBuffer min, ByteBuffer max,
+                          Trie<ByteBuffer, Set<SSTableIndex>> trie,
+                          IntervalTree<Term, SSTableIndex, Interval<Term, SSTableIndex>> ranges,
+                          OnDiskIndexBuilder.Mode mode,
+                          AbstractType<?> comparator)
+    {
+        super(min, max, ranges, comparator);
+
+        this.mode = mode;
+        this.trie = trie;
+    }
+
+    public Set<SSTableIndex> search(Expression e)
+    {
+        Map<ByteBuffer, Set<SSTableIndex>> indexes = (e == null || e.lower == null || mode == OnDiskIndexBuilder.Mode.CONTAINS)
+                                                        ? trie : trie.prefixMap(e.lower.value);
+
+        Set<SSTableIndex> view = new HashSet<>(indexes.size());
+        indexes.values().forEach(view::addAll);
+        return Sets.union(view, super.search(e));
+    }
+
+    public static class Builder extends RangeTermTree.Builder
+    {
+        private final PatriciaTrie<ByteBuffer, Set<SSTableIndex>> trie;
+
+        protected Builder(OnDiskIndexBuilder.Mode mode, final AbstractType<?> comparator)
+        {
+            super(mode, comparator);
+            trie = new PatriciaTrie<>(new ByteBufferKeyAnalyzer(comparator));
+
+        }
+
+        public void addIndex(SSTableIndex index)
+        {
+            super.addIndex(index);
+            addTerm(index.minTerm(), index);
+            addTerm(index.maxTerm(), index);
+        }
+
+        public TermTree build()
+        {
+            return new PrefixTermTree(min, max, trie, IntervalTree.build(intervals), mode, comparator);
+        }
+
+        private void addTerm(ByteBuffer term, SSTableIndex index)
+        {
+            Set<SSTableIndex> indexes = trie.get(term);
+            if (indexes == null)
+                trie.put(term, (indexes = new HashSet<>()));
+
+            indexes.add(index);
+        }
+    }
+
+    private static class ByteBufferKeyAnalyzer implements KeyAnalyzer<ByteBuffer>
+    {
+        private final AbstractType<?> comparator;
+
+        public ByteBufferKeyAnalyzer(AbstractType<?> comparator)
+        {
+            this.comparator = comparator;
+        }
+
+        /**
+         * A bit mask where the first bit is 1 and the others are zero
+         */
+        private static final int MSB = 1 << Byte.SIZE-1;
+
+        public int compare(ByteBuffer a, ByteBuffer b)
+        {
+            return comparator.compare(a, b);
+        }
+
+        public int lengthInBits(ByteBuffer o)
+        {
+            return o.remaining() * Byte.SIZE;
+        }
+
+        public boolean isBitSet(ByteBuffer key, int bitIndex)
+        {
+            if (bitIndex >= lengthInBits(key))
+                return false;
+
+            int index = bitIndex / Byte.SIZE;
+            int bit = bitIndex % Byte.SIZE;
+            return (key.get(index) & mask(bit)) != 0;
+        }
+
+        public int bitIndex(ByteBuffer key, ByteBuffer otherKey)
+        {
+            int length = Math.max(key.remaining(), otherKey.remaining());
+
+            boolean allNull = true;
+            for (int i = 0; i < length; i++)
+            {
+                byte b1 = valueAt(key, i);
+                byte b2 = valueAt(otherKey, i);
+
+                if (b1 != b2)
+                {
+                    int xor = b1 ^ b2;
+                    for (int j = 0; j < Byte.SIZE; j++)
+                    {
+                        if ((xor & mask(j)) != 0)
+                            return (i * Byte.SIZE) + j;
+                    }
+                }
+
+                if (b1 != 0)
+                    allNull = false;
+            }
+
+            return allNull ? KeyAnalyzer.NULL_BIT_KEY : KeyAnalyzer.EQUAL_BIT_KEY;
+        }
+
+        public boolean isPrefix(ByteBuffer key, ByteBuffer prefix)
+        {
+            if (key.remaining() < prefix.remaining())
+                return false;
+
+            for (int i = 0; i < prefix.remaining(); i++)
+            {
+                if (key.get(i) != prefix.get(i))
+                    return false;
+            }
+
+            return true;
+        }
+
+        /**
+         * Returns the {@code byte} value at the given index.
+         */
+        private byte valueAt(ByteBuffer value, int index)
+        {
+            return index >= 0 && index < value.remaining() ? value.get(index) : 0;
+        }
+
+        /**
+         * Returns a bit mask where the given bit is set
+         */
+        private int mask(int bit)
+        {
+            return MSB >>> bit;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/conf/view/RangeTermTree.java b/src/java/org/apache/cassandra/index/sasi/conf/view/RangeTermTree.java
new file mode 100644
index 0000000..d6b4551
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/conf/view/RangeTermTree.java
@@ -0,0 +1,105 @@
+/*
+ * 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.index.sasi.conf.view;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.cassandra.index.sasi.SSTableIndex;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.utils.Interval;
+import org.apache.cassandra.utils.IntervalTree;
+
+public class RangeTermTree implements TermTree
+{
+    protected final ByteBuffer min, max;
+    protected final IntervalTree<Term, SSTableIndex, Interval<Term, SSTableIndex>> rangeTree;
+    protected final AbstractType<?> comparator;
+
+    public RangeTermTree(ByteBuffer min, ByteBuffer max, IntervalTree<Term, SSTableIndex, Interval<Term, SSTableIndex>> rangeTree, AbstractType<?> comparator)
+    {
+        this.min = min;
+        this.max = max;
+        this.rangeTree = rangeTree;
+        this.comparator = comparator;
+    }
+
+    public Set<SSTableIndex> search(Expression e)
+    {
+        ByteBuffer minTerm = e.lower == null ? min : e.lower.value;
+        ByteBuffer maxTerm = e.upper == null ? max : e.upper.value;
+
+        return new HashSet<>(rangeTree.search(Interval.create(new Term(minTerm, comparator),
+                                                              new Term(maxTerm, comparator),
+                                                              (SSTableIndex) null)));
+    }
+
+    public int intervalCount()
+    {
+        return rangeTree.intervalCount();
+    }
+
+    static class Builder extends TermTree.Builder
+    {
+        protected final List<Interval<Term, SSTableIndex>> intervals = new ArrayList<>();
+
+        protected Builder(OnDiskIndexBuilder.Mode mode, AbstractType<?> comparator)
+        {
+            super(mode, comparator);
+        }
+
+        public void addIndex(SSTableIndex index)
+        {
+            intervals.add(Interval.create(new Term(index.minTerm(), comparator),
+                                          new Term(index.maxTerm(), comparator), index));
+        }
+
+
+        public TermTree build()
+        {
+            return new RangeTermTree(min, max, IntervalTree.build(intervals), comparator);
+        }
+    }
+
+
+    /**
+     * This is required since IntervalTree doesn't support custom Comparator
+     * implementations and relied on items to be comparable which "raw" terms are not.
+     */
+    protected static class Term implements Comparable<Term>
+    {
+        private final ByteBuffer term;
+        private final AbstractType<?> comparator;
+
+        public Term(ByteBuffer term, AbstractType<?> comparator)
+        {
+            this.term = term;
+            this.comparator = comparator;
+        }
+
+        public int compareTo(Term o)
+        {
+            return comparator.compare(term, o.term);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/conf/view/TermTree.java b/src/java/org/apache/cassandra/index/sasi/conf/view/TermTree.java
new file mode 100644
index 0000000..a175e22
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/conf/view/TermTree.java
@@ -0,0 +1,58 @@
+/*
+ * 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.index.sasi.conf.view;
+
+import java.nio.ByteBuffer;
+import java.util.Set;
+
+import org.apache.cassandra.index.sasi.SSTableIndex;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public interface TermTree
+{
+    Set<SSTableIndex> search(Expression e);
+
+    int intervalCount();
+
+    abstract class Builder
+    {
+        protected final OnDiskIndexBuilder.Mode mode;
+        protected final AbstractType<?> comparator;
+        protected ByteBuffer min, max;
+
+        protected Builder(OnDiskIndexBuilder.Mode mode, AbstractType<?> comparator)
+        {
+            this.mode = mode;
+            this.comparator = comparator;
+        }
+
+        public final void add(SSTableIndex index)
+        {
+            addIndex(index);
+
+            min = min == null || comparator.compare(min, index.minTerm()) > 0 ? index.minTerm() : min;
+            max = max == null || comparator.compare(max, index.maxTerm()) < 0 ? index.maxTerm() : max;
+        }
+
+        protected abstract void addIndex(SSTableIndex index);
+
+        public abstract TermTree build();
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/conf/view/View.java b/src/java/org/apache/cassandra/index/sasi/conf/view/View.java
new file mode 100644
index 0000000..b0afc5b
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/conf/view/View.java
@@ -0,0 +1,134 @@
+/*
+ * 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.index.sasi.conf.view;
+
+import java.nio.ByteBuffer;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import org.apache.cassandra.index.sasi.SSTableIndex;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.AsciiType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.utils.Interval;
+import org.apache.cassandra.utils.IntervalTree;
+
+import com.google.common.collect.Iterables;
+
+public class View implements Iterable<SSTableIndex>
+{
+    private final Map<Descriptor, SSTableIndex> view;
+
+    private final TermTree termTree;
+    private final AbstractType<?> keyValidator;
+    private final IntervalTree<Key, SSTableIndex, Interval<Key, SSTableIndex>> keyIntervalTree;
+
+    public View(ColumnIndex index, Set<SSTableIndex> indexes)
+    {
+        this(index, Collections.<SSTableIndex>emptyList(), Collections.<SSTableReader>emptyList(), indexes);
+    }
+
+    public View(ColumnIndex index,
+                Collection<SSTableIndex> currentView,
+                Collection<SSTableReader> oldSSTables,
+                Set<SSTableIndex> newIndexes)
+    {
+        Map<Descriptor, SSTableIndex> newView = new HashMap<>();
+
+        AbstractType<?> validator = index.getValidator();
+        TermTree.Builder termTreeBuilder = (validator instanceof AsciiType || validator instanceof UTF8Type)
+                                            ? new PrefixTermTree.Builder(index.getMode().mode, validator)
+                                            : new RangeTermTree.Builder(index.getMode().mode, validator);
+
+        List<Interval<Key, SSTableIndex>> keyIntervals = new ArrayList<>();
+        // Ensure oldSSTables and newIndexes are disjoint (in index redistribution case the intersection can be non-empty).
+        // also favor newIndexes over currentView in case an SSTable has been re-opened (also occurs during redistribution)
+        // See CASSANDRA-14055
+        Collection<SSTableReader> toRemove = new HashSet<>(oldSSTables);
+        toRemove.removeAll(newIndexes.stream().map(SSTableIndex::getSSTable).collect(Collectors.toSet()));
+        for (SSTableIndex sstableIndex : Iterables.concat(newIndexes, currentView))
+        {
+            SSTableReader sstable = sstableIndex.getSSTable();
+            if (toRemove.contains(sstable) || sstable.isMarkedCompacted() || newView.containsKey(sstable.descriptor))
+            {
+                sstableIndex.release();
+                continue;
+            }
+
+            newView.put(sstable.descriptor, sstableIndex);
+
+            termTreeBuilder.add(sstableIndex);
+            keyIntervals.add(Interval.create(new Key(sstableIndex.minKey(), index.keyValidator()),
+                                             new Key(sstableIndex.maxKey(), index.keyValidator()),
+                                             sstableIndex));
+        }
+
+        this.view = newView;
+        this.termTree = termTreeBuilder.build();
+        this.keyValidator = index.keyValidator();
+        this.keyIntervalTree = IntervalTree.build(keyIntervals);
+
+        if (keyIntervalTree.intervalCount() != termTree.intervalCount())
+            throw new IllegalStateException(String.format("mismatched sizes for intervals tree for keys vs terms: %d != %d", keyIntervalTree.intervalCount(), termTree.intervalCount()));
+    }
+
+    public Set<SSTableIndex> match(Expression expression)
+    {
+        return termTree.search(expression);
+    }
+
+    public List<SSTableIndex> match(ByteBuffer minKey, ByteBuffer maxKey)
+    {
+        return keyIntervalTree.search(Interval.create(new Key(minKey, keyValidator), new Key(maxKey, keyValidator), (SSTableIndex) null));
+    }
+
+    public Iterator<SSTableIndex> iterator()
+    {
+        return view.values().iterator();
+    }
+
+    public Collection<SSTableIndex> getIndexes()
+    {
+        return view.values();
+    }
+
+    /**
+     * This is required since IntervalTree doesn't support custom Comparator
+     * implementations and relied on items to be comparable which "raw" keys are not.
+     */
+    private static class Key implements Comparable<Key>
+    {
+        private final ByteBuffer key;
+        private final AbstractType<?> comparator;
+
+        public Key(ByteBuffer key, AbstractType<?> comparator)
+        {
+            this.key = key;
+            this.comparator = comparator;
+        }
+
+        public int compareTo(Key o)
+        {
+            return comparator.compare(key, o.key);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/disk/AbstractTokenTreeBuilder.java b/src/java/org/apache/cassandra/index/sasi/disk/AbstractTokenTreeBuilder.java
new file mode 100644
index 0000000..ae1024f
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/disk/AbstractTokenTreeBuilder.java
@@ -0,0 +1,681 @@
+/*
+ * 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.index.sasi.disk;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.utils.AbstractIterator;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.Pair;
+
+import com.carrotsearch.hppc.LongArrayList;
+import com.carrotsearch.hppc.LongSet;
+import com.carrotsearch.hppc.cursors.LongCursor;
+
+public abstract class AbstractTokenTreeBuilder implements TokenTreeBuilder
+{
+    protected int numBlocks;
+    protected Node root;
+    protected InteriorNode rightmostParent;
+    protected Leaf leftmostLeaf;
+    protected Leaf rightmostLeaf;
+    protected long tokenCount = 0;
+    protected long treeMinToken;
+    protected long treeMaxToken;
+
+    public void add(TokenTreeBuilder other)
+    {
+        add(other.iterator());
+    }
+
+    public TokenTreeBuilder finish()
+    {
+        if (root == null)
+            constructTree();
+
+        return this;
+    }
+
+    public long getTokenCount()
+    {
+        return tokenCount;
+    }
+
+    public int serializedSize()
+    {
+        if (numBlocks == 1)
+            return BLOCK_HEADER_BYTES +
+                   ((int) tokenCount * BLOCK_ENTRY_BYTES) +
+                   (((Leaf) root).overflowCollisionCount() * OVERFLOW_ENTRY_BYTES);
+        else
+            return numBlocks * BLOCK_BYTES;
+    }
+
+    public void write(DataOutputPlus out) throws IOException
+    {
+        ByteBuffer blockBuffer = ByteBuffer.allocate(BLOCK_BYTES);
+        Iterator<Node> levelIterator = root.levelIterator();
+        long childBlockIndex = 1;
+
+        while (levelIterator != null)
+        {
+            Node firstChild = null;
+            while (levelIterator.hasNext())
+            {
+                Node block = levelIterator.next();
+
+                if (firstChild == null && !block.isLeaf())
+                    firstChild = ((InteriorNode) block).children.get(0);
+
+                if (block.isSerializable())
+                {
+                    block.serialize(childBlockIndex, blockBuffer);
+                    flushBuffer(blockBuffer, out, numBlocks != 1);
+                }
+
+                childBlockIndex += block.childCount();
+            }
+
+            levelIterator = (firstChild == null) ? null : firstChild.levelIterator();
+        }
+    }
+
+    protected abstract void constructTree();
+
+    protected void flushBuffer(ByteBuffer buffer, DataOutputPlus o, boolean align) throws IOException
+    {
+        // seek to end of last block before flushing
+        if (align)
+            alignBuffer(buffer, BLOCK_BYTES);
+
+        buffer.flip();
+        o.write(buffer);
+        buffer.clear();
+    }
+
+    protected abstract class Node
+    {
+        protected InteriorNode parent;
+        protected Node next;
+        protected Long nodeMinToken, nodeMaxToken;
+
+        public Node(Long minToken, Long maxToken)
+        {
+            nodeMinToken = minToken;
+            nodeMaxToken = maxToken;
+        }
+
+        public abstract boolean isSerializable();
+        public abstract void serialize(long childBlockIndex, ByteBuffer buf);
+        public abstract int childCount();
+        public abstract int tokenCount();
+
+        public Long smallestToken()
+        {
+            return nodeMinToken;
+        }
+
+        public Long largestToken()
+        {
+            return nodeMaxToken;
+        }
+
+        public Iterator<Node> levelIterator()
+        {
+            return new LevelIterator(this);
+        }
+
+        public boolean isLeaf()
+        {
+            return (this instanceof Leaf);
+        }
+
+        protected boolean isLastLeaf()
+        {
+            return this == rightmostLeaf;
+        }
+
+        protected boolean isRoot()
+        {
+            return this == root;
+        }
+
+        protected void updateTokenRange(long token)
+        {
+            nodeMinToken = nodeMinToken == null ? token : Math.min(nodeMinToken, token);
+            nodeMaxToken = nodeMaxToken == null ? token : Math.max(nodeMaxToken, token);
+        }
+
+        protected void serializeHeader(ByteBuffer buf)
+        {
+            Header header;
+            if (isRoot())
+                header = new RootHeader();
+            else if (!isLeaf())
+                header = new InteriorNodeHeader();
+            else
+                header = new LeafHeader();
+
+            header.serialize(buf);
+            alignBuffer(buf, BLOCK_HEADER_BYTES);
+        }
+
+        private abstract class Header
+        {
+            public void serialize(ByteBuffer buf)
+            {
+                buf.put(infoByte())
+                   .putShort((short) (tokenCount()))
+                   .putLong(nodeMinToken)
+                   .putLong(nodeMaxToken);
+            }
+
+            protected abstract byte infoByte();
+        }
+
+        private class RootHeader extends Header
+        {
+            public void serialize(ByteBuffer buf)
+            {
+                super.serialize(buf);
+                writeMagic(buf);
+                buf.putLong(tokenCount)
+                   .putLong(treeMinToken)
+                   .putLong(treeMaxToken);
+            }
+
+            protected byte infoByte()
+            {
+                // if leaf, set leaf indicator and last leaf indicator (bits 0 & 1)
+                // if not leaf, clear both bits
+                return (byte) ((isLeaf()) ? 3 : 0);
+            }
+
+            protected void writeMagic(ByteBuffer buf)
+            {
+                switch (Descriptor.CURRENT_VERSION)
+                {
+                    case Descriptor.VERSION_AB:
+                        buf.putShort(AB_MAGIC);
+                        break;
+
+                    default:
+                        break;
+                }
+
+            }
+        }
+
+        private class InteriorNodeHeader extends Header
+        {
+            // bit 0 (leaf indicator) & bit 1 (last leaf indicator) cleared
+            protected byte infoByte()
+            {
+                return 0;
+            }
+        }
+
+        private class LeafHeader extends Header
+        {
+            // bit 0 set as leaf indicator
+            // bit 1 set if this is last leaf of data
+            protected byte infoByte()
+            {
+                byte infoByte = 1;
+                infoByte |= (isLastLeaf()) ? (1 << LAST_LEAF_SHIFT) : 0;
+
+                return infoByte;
+            }
+        }
+
+    }
+
+    protected abstract class Leaf extends Node
+    {
+        protected LongArrayList overflowCollisions;
+
+        public Leaf(Long minToken, Long maxToken)
+        {
+            super(minToken, maxToken);
+        }
+
+        public int childCount()
+        {
+            return 0;
+        }
+
+        public int overflowCollisionCount() {
+            return overflowCollisions == null ? 0 : overflowCollisions.size();
+        }
+
+        protected void serializeOverflowCollisions(ByteBuffer buf)
+        {
+            if (overflowCollisions != null)
+                for (LongCursor offset : overflowCollisions)
+                    buf.putLong(offset.value);
+        }
+
+        public void serialize(long childBlockIndex, ByteBuffer buf)
+        {
+            serializeHeader(buf);
+            serializeData(buf);
+            serializeOverflowCollisions(buf);
+        }
+
+        protected abstract void serializeData(ByteBuffer buf);
+
+        protected LeafEntry createEntry(final long tok, final LongSet offsets)
+        {
+            int offsetCount = offsets.size();
+            switch (offsetCount)
+            {
+                case 0:
+                    throw new AssertionError("no offsets for token " + tok);
+                case 1:
+                    long offset = offsets.toArray()[0];
+                    if (offset > MAX_OFFSET)
+                        throw new AssertionError("offset " + offset + " cannot be greater than " + MAX_OFFSET);
+                    else if (offset <= Integer.MAX_VALUE)
+                        return new SimpleLeafEntry(tok, offset);
+                    else
+                        return new FactoredOffsetLeafEntry(tok, offset);
+                case 2:
+                    long[] rawOffsets = offsets.toArray();
+                    if (rawOffsets[0] <= Integer.MAX_VALUE && rawOffsets[1] <= Integer.MAX_VALUE &&
+                        (rawOffsets[0] <= Short.MAX_VALUE || rawOffsets[1] <= Short.MAX_VALUE))
+                        return new PackedCollisionLeafEntry(tok, rawOffsets);
+                    else
+                        return createOverflowEntry(tok, offsetCount, offsets);
+                default:
+                    return createOverflowEntry(tok, offsetCount, offsets);
+            }
+        }
+
+        private LeafEntry createOverflowEntry(final long tok, final int offsetCount, final LongSet offsets)
+        {
+            if (overflowCollisions == null)
+                overflowCollisions = new LongArrayList();
+
+            LeafEntry entry = new OverflowCollisionLeafEntry(tok, (short) overflowCollisions.size(), (short) offsetCount);
+            for (LongCursor o : offsets)
+            {
+                if (overflowCollisions.size() == OVERFLOW_TRAILER_CAPACITY)
+                    throw new AssertionError("cannot have more than " + OVERFLOW_TRAILER_CAPACITY + " overflow collisions per leaf");
+                else
+                    overflowCollisions.add(o.value);
+            }
+            return entry;
+        }
+
+        protected abstract class LeafEntry
+        {
+            protected final long token;
+
+            abstract public EntryType type();
+            abstract public int offsetData();
+            abstract public short offsetExtra();
+
+            public LeafEntry(final long tok)
+            {
+                token = tok;
+            }
+
+            public void serialize(ByteBuffer buf)
+            {
+                buf.putShort((short) type().ordinal())
+                   .putShort(offsetExtra())
+                   .putLong(token)
+                   .putInt(offsetData());
+            }
+
+        }
+
+
+        // assumes there is a single offset and the offset is <= Integer.MAX_VALUE
+        protected class SimpleLeafEntry extends LeafEntry
+        {
+            private final long offset;
+
+            public SimpleLeafEntry(final long tok, final long off)
+            {
+                super(tok);
+                offset = off;
+            }
+
+            public EntryType type()
+            {
+                return EntryType.SIMPLE;
+            }
+
+            public int offsetData()
+            {
+                return (int) offset;
+            }
+
+            public short offsetExtra()
+            {
+                return 0;
+            }
+        }
+
+        // assumes there is a single offset and Integer.MAX_VALUE < offset <= MAX_OFFSET
+        // take the middle 32 bits of offset (or the top 32 when considering offset is max 48 bits)
+        // and store where offset is normally stored. take bottom 16 bits of offset and store in entry header
+        private class FactoredOffsetLeafEntry extends LeafEntry
+        {
+            private final long offset;
+
+            public FactoredOffsetLeafEntry(final long tok, final long off)
+            {
+                super(tok);
+                offset = off;
+            }
+
+            public EntryType type()
+            {
+                return EntryType.FACTORED;
+            }
+
+            public int offsetData()
+            {
+                return (int) (offset >>> Short.SIZE);
+            }
+
+            public short offsetExtra()
+            {
+                // exta offset is supposed to be an unsigned 16-bit integer
+                return (short) offset;
+            }
+        }
+
+        // holds an entry with two offsets that can be packed in an int & a short
+        // the int offset is stored where offset is normally stored. short offset is
+        // stored in entry header
+        private class PackedCollisionLeafEntry extends LeafEntry
+        {
+            private short smallerOffset;
+            private int largerOffset;
+
+            public PackedCollisionLeafEntry(final long tok, final long[] offs)
+            {
+                super(tok);
+
+                smallerOffset = (short) Math.min(offs[0], offs[1]);
+                largerOffset = (int) Math.max(offs[0], offs[1]);
+            }
+
+            public EntryType type()
+            {
+                return EntryType.PACKED;
+            }
+
+            public int offsetData()
+            {
+                return largerOffset;
+            }
+
+            public short offsetExtra()
+            {
+                return smallerOffset;
+            }
+        }
+
+        // holds an entry with three or more offsets, or two offsets that cannot
+        // be packed into an int & a short. the index into the overflow list
+        // is stored where the offset is normally stored. the number of overflowed offsets
+        // for the entry is stored in the entry header
+        private class OverflowCollisionLeafEntry extends LeafEntry
+        {
+            private final short startIndex;
+            private final short count;
+
+            public OverflowCollisionLeafEntry(final long tok, final short collisionStartIndex, final short collisionCount)
+            {
+                super(tok);
+                startIndex = collisionStartIndex;
+                count = collisionCount;
+            }
+
+            public EntryType type()
+            {
+                return EntryType.OVERFLOW;
+            }
+
+            public int offsetData()
+            {
+                return startIndex;
+            }
+
+            public short offsetExtra()
+            {
+                return count;
+            }
+
+        }
+
+    }
+
+    protected class InteriorNode extends Node
+    {
+        protected List<Long> tokens = new ArrayList<>(TOKENS_PER_BLOCK);
+        protected List<Node> children = new ArrayList<>(TOKENS_PER_BLOCK + 1);
+        protected int position = 0;
+
+        public InteriorNode()
+        {
+            super(null, null);
+        }
+
+        public boolean isSerializable()
+        {
+            return true;
+        }
+
+        public void serialize(long childBlockIndex, ByteBuffer buf)
+        {
+            serializeHeader(buf);
+            serializeTokens(buf);
+            serializeChildOffsets(childBlockIndex, buf);
+        }
+
+        public int childCount()
+        {
+            return children.size();
+        }
+
+        public int tokenCount()
+        {
+            return tokens.size();
+        }
+
+        public Long smallestToken()
+        {
+            return tokens.get(0);
+        }
+
+        protected void add(Long token, InteriorNode leftChild, InteriorNode rightChild)
+        {
+            int pos = tokens.size();
+            if (pos == TOKENS_PER_BLOCK)
+            {
+                InteriorNode sibling = split();
+                sibling.add(token, leftChild, rightChild);
+
+            }
+            else
+            {
+                if (leftChild != null)
+                    children.add(pos, leftChild);
+
+                if (rightChild != null)
+                {
+                    children.add(pos + 1, rightChild);
+                    rightChild.parent = this;
+                }
+
+                updateTokenRange(token);
+                tokens.add(pos, token);
+            }
+        }
+
+        protected void add(Leaf node)
+        {
+
+            if (position == (TOKENS_PER_BLOCK + 1))
+            {
+                rightmostParent = split();
+                rightmostParent.add(node);
+            }
+            else
+            {
+
+                node.parent = this;
+                children.add(position, node);
+                position++;
+
+                // the first child is referenced only during bulk load. we don't take a value
+                // to store into the tree, one is subtracted since position has already been incremented
+                // for the next node to be added
+                if (position - 1 == 0)
+                    return;
+
+
+                // tokens are inserted one behind the current position, but 2 is subtracted because
+                // position has already been incremented for the next add
+                Long smallestToken = node.smallestToken();
+                updateTokenRange(smallestToken);
+                tokens.add(position - 2, smallestToken);
+            }
+
+        }
+
+        protected InteriorNode split()
+        {
+            Pair<Long, InteriorNode> splitResult = splitBlock();
+            Long middleValue = splitResult.left;
+            InteriorNode sibling = splitResult.right;
+            InteriorNode leftChild = null;
+
+            // create a new root if necessary
+            if (parent == null)
+            {
+                parent = new InteriorNode();
+                root = parent;
+                sibling.parent = parent;
+                leftChild = this;
+                numBlocks++;
+            }
+
+            parent.add(middleValue, leftChild, sibling);
+
+            return sibling;
+        }
+
+        protected Pair<Long, InteriorNode> splitBlock()
+        {
+            final int splitPosition = TOKENS_PER_BLOCK - 2;
+            InteriorNode sibling = new InteriorNode();
+            sibling.parent = parent;
+            next = sibling;
+
+            Long middleValue = tokens.get(splitPosition);
+
+            for (int i = splitPosition; i < TOKENS_PER_BLOCK; i++)
+            {
+                if (i != TOKENS_PER_BLOCK && i != splitPosition)
+                {
+                    long token = tokens.get(i);
+                    sibling.updateTokenRange(token);
+                    sibling.tokens.add(token);
+                }
+
+                Node child = children.get(i + 1);
+                child.parent = sibling;
+                sibling.children.add(child);
+                sibling.position++;
+            }
+
+            for (int i = TOKENS_PER_BLOCK; i >= splitPosition; i--)
+            {
+                if (i != TOKENS_PER_BLOCK)
+                    tokens.remove(i);
+
+                if (i != splitPosition)
+                    children.remove(i);
+            }
+
+            nodeMinToken = smallestToken();
+            nodeMaxToken = tokens.get(tokens.size() - 1);
+            numBlocks++;
+
+            return Pair.create(middleValue, sibling);
+        }
+
+        protected boolean isFull()
+        {
+            return (position >= TOKENS_PER_BLOCK + 1);
+        }
+
+        private void serializeTokens(ByteBuffer buf)
+        {
+            tokens.forEach(buf::putLong);
+        }
+
+        private void serializeChildOffsets(long childBlockIndex, ByteBuffer buf)
+        {
+            for (int i = 0; i < children.size(); i++)
+                buf.putLong((childBlockIndex + i) * BLOCK_BYTES);
+        }
+    }
+
+    public static class LevelIterator extends AbstractIterator<Node>
+    {
+        private Node currentNode;
+
+        LevelIterator(Node first)
+        {
+            currentNode = first;
+        }
+
+        public Node computeNext()
+        {
+            if (currentNode == null)
+                return endOfData();
+
+            Node returnNode = currentNode;
+            currentNode = returnNode.next;
+
+            return returnNode;
+        }
+    }
+
+
+    protected static void alignBuffer(ByteBuffer buffer, int blockSize)
+    {
+        long curPos = buffer.position();
+        if ((curPos & (blockSize - 1)) != 0) // align on the block boundary if needed
+            buffer.position((int) FBUtilities.align(curPos, blockSize));
+    }
+
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/disk/Descriptor.java b/src/java/org/apache/cassandra/index/sasi/disk/Descriptor.java
new file mode 100644
index 0000000..3aa6f14
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/disk/Descriptor.java
@@ -0,0 +1,51 @@
+/*
+ * 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.index.sasi.disk;
+
+/**
+ * Object descriptor for SASIIndex files. Similar to, and based upon, the sstable descriptor.
+ */
+public class Descriptor
+{
+    public static final String VERSION_AA = "aa";
+    public static final String VERSION_AB = "ab";
+    public static final String CURRENT_VERSION = VERSION_AB;
+    public static final Descriptor CURRENT = new Descriptor(CURRENT_VERSION);
+
+    public static class Version
+    {
+        public final String version;
+
+        public Version(String version)
+        {
+            this.version = version;
+        }
+
+        public String toString()
+        {
+            return version;
+        }
+    }
+
+    public final Version version;
+
+    public Descriptor(String v)
+    {
+        this.version = new Version(v);
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/disk/DynamicTokenTreeBuilder.java b/src/java/org/apache/cassandra/index/sasi/disk/DynamicTokenTreeBuilder.java
new file mode 100644
index 0000000..2ddfd89
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/disk/DynamicTokenTreeBuilder.java
@@ -0,0 +1,189 @@
+/*
+ * 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.index.sasi.disk;
+
+import java.nio.ByteBuffer;
+import java.util.*;
+
+import org.apache.cassandra.utils.AbstractIterator;
+import org.apache.cassandra.utils.Pair;
+
+import com.carrotsearch.hppc.LongOpenHashSet;
+import com.carrotsearch.hppc.LongSet;
+import com.carrotsearch.hppc.cursors.LongCursor;
+
+public class DynamicTokenTreeBuilder extends AbstractTokenTreeBuilder
+{
+    private final SortedMap<Long, LongSet> tokens = new TreeMap<>();
+
+
+    public DynamicTokenTreeBuilder()
+    {}
+
+    public DynamicTokenTreeBuilder(TokenTreeBuilder data)
+    {
+        add(data);
+    }
+
+    public DynamicTokenTreeBuilder(SortedMap<Long, LongSet> data)
+    {
+        add(data);
+    }
+
+    public void add(Long token, long keyPosition)
+    {
+        LongSet found = tokens.get(token);
+        if (found == null)
+            tokens.put(token, (found = new LongOpenHashSet(2)));
+
+        found.add(keyPosition);
+    }
+
+    public void add(Iterator<Pair<Long, LongSet>> data)
+    {
+        while (data.hasNext())
+        {
+            Pair<Long, LongSet> entry = data.next();
+            for (LongCursor l : entry.right)
+                add(entry.left, l.value);
+        }
+    }
+
+    public void add(SortedMap<Long, LongSet> data)
+    {
+        for (Map.Entry<Long, LongSet> newEntry : data.entrySet())
+        {
+            LongSet found = tokens.get(newEntry.getKey());
+            if (found == null)
+                tokens.put(newEntry.getKey(), (found = new LongOpenHashSet(4)));
+
+            for (LongCursor offset : newEntry.getValue())
+                found.add(offset.value);
+        }
+    }
+
+    public Iterator<Pair<Long, LongSet>> iterator()
+    {
+        final Iterator<Map.Entry<Long, LongSet>> iterator = tokens.entrySet().iterator();
+        return new AbstractIterator<Pair<Long, LongSet>>()
+        {
+            protected Pair<Long, LongSet> computeNext()
+            {
+                if (!iterator.hasNext())
+                    return endOfData();
+
+                Map.Entry<Long, LongSet> entry = iterator.next();
+                return Pair.create(entry.getKey(), entry.getValue());
+            }
+        };
+    }
+
+    public boolean isEmpty()
+    {
+        return tokens.size() == 0;
+    }
+
+    protected void constructTree()
+    {
+        tokenCount = tokens.size();
+        treeMinToken = tokens.firstKey();
+        treeMaxToken = tokens.lastKey();
+        numBlocks = 1;
+
+        // special case the tree that only has a single block in it (so we don't create a useless root)
+        if (tokenCount <= TOKENS_PER_BLOCK)
+        {
+            leftmostLeaf = new DynamicLeaf(tokens);
+            rightmostLeaf = leftmostLeaf;
+            root = leftmostLeaf;
+        }
+        else
+        {
+            root = new InteriorNode();
+            rightmostParent = (InteriorNode) root;
+
+            int i = 0;
+            Leaf lastLeaf = null;
+            Long firstToken = tokens.firstKey();
+            Long finalToken = tokens.lastKey();
+            Long lastToken;
+            for (Long token : tokens.keySet())
+            {
+                if (i == 0 || (i % TOKENS_PER_BLOCK != 0 && i != (tokenCount - 1)))
+                {
+                    i++;
+                    continue;
+                }
+
+                lastToken = token;
+                Leaf leaf = (i != (tokenCount - 1) || token.equals(finalToken)) ?
+                        new DynamicLeaf(tokens.subMap(firstToken, lastToken)) : new DynamicLeaf(tokens.tailMap(firstToken));
+
+                if (i == TOKENS_PER_BLOCK)
+                    leftmostLeaf = leaf;
+                else
+                    lastLeaf.next = leaf;
+
+                rightmostParent.add(leaf);
+                lastLeaf = leaf;
+                rightmostLeaf = leaf;
+                firstToken = lastToken;
+                i++;
+                numBlocks++;
+
+                if (token.equals(finalToken))
+                {
+                    Leaf finalLeaf = new DynamicLeaf(tokens.tailMap(token));
+                    lastLeaf.next = finalLeaf;
+                    rightmostParent.add(finalLeaf);
+                    rightmostLeaf = finalLeaf;
+                    numBlocks++;
+                }
+            }
+
+        }
+    }
+
+    private class DynamicLeaf extends Leaf
+    {
+        private final SortedMap<Long, LongSet> tokens;
+
+        DynamicLeaf(SortedMap<Long, LongSet> data)
+        {
+            super(data.firstKey(), data.lastKey());
+            tokens = data;
+        }
+
+        public int tokenCount()
+        {
+            return tokens.size();
+        }
+
+        public boolean isSerializable()
+        {
+            return true;
+        }
+
+        protected void serializeData(ByteBuffer buf)
+        {
+            for (Map.Entry<Long, LongSet> entry : tokens.entrySet())
+                createEntry(entry.getKey(), entry.getValue()).serialize(buf);
+        }
+
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/disk/OnDiskBlock.java b/src/java/org/apache/cassandra/index/sasi/disk/OnDiskBlock.java
new file mode 100644
index 0000000..e335b50
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/disk/OnDiskBlock.java
@@ -0,0 +1,143 @@
+/*
+ * 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.index.sasi.disk;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.index.sasi.Term;
+import org.apache.cassandra.index.sasi.utils.MappedBuffer;
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public abstract class OnDiskBlock<T extends Term>
+{
+    public enum BlockType
+    {
+        POINTER, DATA
+    }
+
+    // this contains offsets of the terms and term data
+    protected final MappedBuffer blockIndex;
+    protected final int blockIndexSize;
+
+    protected final boolean hasCombinedIndex;
+    protected final TokenTree combinedIndex;
+
+    public OnDiskBlock(Descriptor descriptor, MappedBuffer block, BlockType blockType)
+    {
+        blockIndex = block;
+
+        if (blockType == BlockType.POINTER)
+        {
+            hasCombinedIndex = false;
+            combinedIndex = null;
+            blockIndexSize = block.getInt() << 1; // num terms * sizeof(short)
+            return;
+        }
+
+        long blockOffset = block.position();
+        int combinedIndexOffset = block.getInt(blockOffset + OnDiskIndexBuilder.BLOCK_SIZE);
+
+        hasCombinedIndex = (combinedIndexOffset >= 0);
+        long blockIndexOffset = blockOffset + OnDiskIndexBuilder.BLOCK_SIZE + 4 + combinedIndexOffset;
+
+        combinedIndex = hasCombinedIndex ? new TokenTree(descriptor, blockIndex.duplicate().position(blockIndexOffset)) : null;
+        blockIndexSize = block.getInt() * 2;
+    }
+
+    public SearchResult<T> search(AbstractType<?> comparator, ByteBuffer query)
+    {
+        int cmp = -1, start = 0, end = termCount() - 1, middle = 0;
+
+        T element = null;
+        while (start <= end)
+        {
+            middle = start + ((end - start) >> 1);
+            element = getTerm(middle);
+
+            cmp = element.compareTo(comparator, query);
+            if (cmp == 0)
+                return new SearchResult<>(element, cmp, middle);
+            else if (cmp < 0)
+                start = middle + 1;
+            else
+                end = middle - 1;
+        }
+
+        return new SearchResult<>(element, cmp, middle);
+    }
+
+    @SuppressWarnings("resource")
+    protected T getTerm(int index)
+    {
+        MappedBuffer dup = blockIndex.duplicate();
+        long startsAt = getTermPosition(index);
+        if (termCount() - 1 == index) // last element
+            dup.position(startsAt);
+        else
+            dup.position(startsAt).limit(getTermPosition(index + 1));
+
+        return cast(dup);
+    }
+
+    protected long getTermPosition(int idx)
+    {
+        return getTermPosition(blockIndex, idx, blockIndexSize);
+    }
+
+    protected int termCount()
+    {
+        return blockIndexSize >> 1;
+    }
+
+    protected abstract T cast(MappedBuffer data);
+
+    static long getTermPosition(MappedBuffer data, int idx, int indexSize)
+    {
+        idx <<= 1;
+        assert idx < indexSize;
+        return data.position() + indexSize + data.getShort(data.position() + idx);
+    }
+
+    public TokenTree getBlockIndex()
+    {
+        return combinedIndex;
+    }
+
+    public int minOffset(OnDiskIndex.IteratorOrder order)
+    {
+        return order == OnDiskIndex.IteratorOrder.DESC ? 0 : termCount() - 1;
+    }
+
+    public int maxOffset(OnDiskIndex.IteratorOrder order)
+    {
+        return minOffset(order) == 0 ? termCount() - 1 : 0;
+    }
+
+    public static class SearchResult<T>
+    {
+        public final T result;
+        public final int index, cmp;
+
+        public SearchResult(T result, int cmp, int index)
+        {
+            this.result = result;
+            this.index = index;
+            this.cmp = cmp;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java b/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java
new file mode 100644
index 0000000..4d43cd9
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java
@@ -0,0 +1,813 @@
+/*
+ * 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.index.sasi.disk;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.index.sasi.Term;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.index.sasi.plan.Expression.Op;
+import org.apache.cassandra.index.sasi.utils.MappedBuffer;
+import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
+import org.apache.cassandra.index.sasi.utils.AbstractIterator;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.io.FSReadError;
+import org.apache.cassandra.io.util.ChannelProxy;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.FBUtilities;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.PeekingIterator;
+
+import static org.apache.cassandra.index.sasi.disk.OnDiskBlock.SearchResult;
+
+public class OnDiskIndex implements Iterable<OnDiskIndex.DataTerm>, Closeable
+{
+    public enum IteratorOrder
+    {
+        DESC(1), ASC(-1);
+
+        public final int step;
+
+        IteratorOrder(int step)
+        {
+            this.step = step;
+        }
+
+        public int startAt(OnDiskBlock<DataTerm> block, Expression e)
+        {
+            switch (this)
+            {
+                case DESC:
+                    return e.lower == null
+                            ? 0
+                            : startAt(block.search(e.validator, e.lower.value), e.lower.inclusive);
+
+                case ASC:
+                    return e.upper == null
+                            ? block.termCount() - 1
+                            : startAt(block.search(e.validator, e.upper.value), e.upper.inclusive);
+
+                default:
+                    throw new IllegalArgumentException("Unknown order: " + this);
+            }
+        }
+
+        public int startAt(SearchResult<DataTerm> found, boolean inclusive)
+        {
+            switch (this)
+            {
+                case DESC:
+                    if (found.cmp < 0)
+                        return found.index + 1;
+
+                    return inclusive || found.cmp != 0 ? found.index : found.index + 1;
+
+                case ASC:
+                    if (found.cmp < 0) // search term was bigger then whole data set
+                        return found.index;
+                    return inclusive && (found.cmp == 0 || found.cmp < 0) ? found.index : found.index - 1;
+
+                default:
+                    throw new IllegalArgumentException("Unknown order: " + this);
+            }
+        }
+    }
+
+    public final Descriptor descriptor;
+    protected final OnDiskIndexBuilder.Mode mode;
+    protected final OnDiskIndexBuilder.TermSize termSize;
+
+    protected final AbstractType<?> comparator;
+    protected final MappedBuffer indexFile;
+    protected final long indexSize;
+    protected final boolean hasMarkedPartials;
+
+    protected final Function<Long, DecoratedKey> keyFetcher;
+
+    protected final String indexPath;
+
+    protected final PointerLevel[] levels;
+    protected final DataLevel dataLevel;
+
+    protected final ByteBuffer minTerm, maxTerm, minKey, maxKey;
+
+    @SuppressWarnings("resource")
+    public OnDiskIndex(File index, AbstractType<?> cmp, Function<Long, DecoratedKey> keyReader)
+    {
+        keyFetcher = keyReader;
+
+        comparator = cmp;
+        indexPath = index.getAbsolutePath();
+
+        RandomAccessFile backingFile = null;
+        try
+        {
+            backingFile = new RandomAccessFile(index, "r");
+
+            descriptor = new Descriptor(backingFile.readUTF());
+
+            termSize = OnDiskIndexBuilder.TermSize.of(backingFile.readShort());
+
+            minTerm = ByteBufferUtil.readWithShortLength(backingFile);
+            maxTerm = ByteBufferUtil.readWithShortLength(backingFile);
+
+            minKey = ByteBufferUtil.readWithShortLength(backingFile);
+            maxKey = ByteBufferUtil.readWithShortLength(backingFile);
+
+            mode = OnDiskIndexBuilder.Mode.mode(backingFile.readUTF());
+            hasMarkedPartials = backingFile.readBoolean();
+
+            indexSize = backingFile.length();
+            indexFile = new MappedBuffer(new ChannelProxy(indexPath, backingFile.getChannel()));
+
+            // start of the levels
+            indexFile.position(indexFile.getLong(indexSize - 8));
+
+            int numLevels = indexFile.getInt();
+            levels = new PointerLevel[numLevels];
+            for (int i = 0; i < levels.length; i++)
+            {
+                int blockCount = indexFile.getInt();
+                levels[i] = new PointerLevel(indexFile.position(), blockCount);
+                indexFile.position(indexFile.position() + blockCount * 8);
+            }
+
+            int blockCount = indexFile.getInt();
+            dataLevel = new DataLevel(indexFile.position(), blockCount);
+        }
+        catch (IOException e)
+        {
+            throw new FSReadError(e, index);
+        }
+        finally
+        {
+            FileUtils.closeQuietly(backingFile);
+        }
+    }
+
+    public boolean hasMarkedPartials()
+    {
+        return hasMarkedPartials;
+    }
+
+    public OnDiskIndexBuilder.Mode mode()
+    {
+        return mode;
+    }
+
+    public ByteBuffer minTerm()
+    {
+        return minTerm;
+    }
+
+    public ByteBuffer maxTerm()
+    {
+        return maxTerm;
+    }
+
+    public ByteBuffer minKey()
+    {
+        return minKey;
+    }
+
+    public ByteBuffer maxKey()
+    {
+        return maxKey;
+    }
+
+    public DataTerm min()
+    {
+        return dataLevel.getBlock(0).getTerm(0);
+    }
+
+    public DataTerm max()
+    {
+        DataBlock block = dataLevel.getBlock(dataLevel.blockCount - 1);
+        return block.getTerm(block.termCount() - 1);
+    }
+
+    /**
+     * Search for rows which match all of the terms inside the given expression in the index file.
+     *
+     * @param exp The expression to use for the query.
+     *
+     * @return Iterator which contains rows for all of the terms from the given range.
+     */
+    public RangeIterator<Long, Token> search(Expression exp)
+    {
+        assert mode.supports(exp.getOp());
+
+        if (exp.getOp() == Expression.Op.PREFIX && mode == OnDiskIndexBuilder.Mode.CONTAINS && !hasMarkedPartials)
+            throw new UnsupportedOperationException("prefix queries in CONTAINS mode are not supported by this index");
+
+        // optimization in case single term is requested from index
+        // we don't really need to build additional union iterator
+        if (exp.getOp() == Op.EQ)
+        {
+            DataTerm term = getTerm(exp.lower.value);
+            return term == null ? null : term.getTokens();
+        }
+
+        // convert single NOT_EQ to range with exclusion
+        final Expression expression = (exp.getOp() != Op.NOT_EQ)
+                                        ? exp
+                                        : new Expression(exp).setOp(Op.RANGE)
+                                                .setLower(new Expression.Bound(minTerm, true))
+                                                .setUpper(new Expression.Bound(maxTerm, true))
+                                                .addExclusion(exp.lower.value);
+
+        List<ByteBuffer> exclusions = new ArrayList<>(expression.exclusions.size());
+
+        Iterables.addAll(exclusions, expression.exclusions.stream().filter(exclusion -> {
+            // accept only exclusions which are in the bounds of lower/upper
+            return !(expression.lower != null && comparator.compare(exclusion, expression.lower.value) < 0)
+                && !(expression.upper != null && comparator.compare(exclusion, expression.upper.value) > 0);
+        }).collect(Collectors.toList()));
+
+        Collections.sort(exclusions, comparator);
+
+        if (exclusions.size() == 0)
+            return searchRange(expression);
+
+        List<Expression> ranges = new ArrayList<>(exclusions.size());
+
+        // calculate range splits based on the sorted exclusions
+        Iterator<ByteBuffer> exclusionsIterator = exclusions.iterator();
+
+        Expression.Bound min = expression.lower, max = null;
+        while (exclusionsIterator.hasNext())
+        {
+            max = new Expression.Bound(exclusionsIterator.next(), false);
+            ranges.add(new Expression(expression).setOp(Op.RANGE).setLower(min).setUpper(max));
+            min = max;
+        }
+
+        assert max != null;
+        ranges.add(new Expression(expression).setOp(Op.RANGE).setLower(max).setUpper(expression.upper));
+
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+        for (Expression e : ranges)
+        {
+            @SuppressWarnings("resource")
+            RangeIterator<Long, Token> range = searchRange(e);
+            if (range != null)
+                builder.add(range);
+        }
+
+        return builder.build();
+    }
+
+    private RangeIterator<Long, Token> searchRange(Expression range)
+    {
+        Expression.Bound lower = range.lower;
+        Expression.Bound upper = range.upper;
+
+        int lowerBlock = lower == null ? 0 : getDataBlock(lower.value);
+        int upperBlock = upper == null
+                ? dataLevel.blockCount - 1
+                // optimization so we don't have to fetch upperBlock when query has lower == upper
+                : (lower != null && comparator.compare(lower.value, upper.value) == 0) ? lowerBlock : getDataBlock(upper.value);
+
+        return (mode != OnDiskIndexBuilder.Mode.SPARSE || lowerBlock == upperBlock || upperBlock - lowerBlock <= 1)
+                ? searchPoint(lowerBlock, range)
+                : searchRange(lowerBlock, lower, upperBlock, upper);
+    }
+
+    private RangeIterator<Long, Token> searchRange(int lowerBlock, Expression.Bound lower, int upperBlock, Expression.Bound upper)
+    {
+        // if lower is at the beginning of the block that means we can just do a single iterator per block
+        SearchResult<DataTerm> lowerPosition = (lower == null) ? null : searchIndex(lower.value, lowerBlock);
+        SearchResult<DataTerm> upperPosition = (upper == null) ? null : searchIndex(upper.value, upperBlock);
+
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+
+        // optimistically assume that first and last blocks are full block reads, saves at least 3 'else' conditions
+        int firstFullBlockIdx = lowerBlock, lastFullBlockIdx = upperBlock;
+
+        // 'lower' doesn't cover the whole block so we need to do a partial iteration
+        // Two reasons why that can happen:
+        //   - 'lower' is not the first element of the block
+        //   - 'lower' is first element but it's not inclusive in the query
+        if (lowerPosition != null && (lowerPosition.index > 0 || !lower.inclusive))
+        {
+            DataBlock block = dataLevel.getBlock(lowerBlock);
+            int start = (lower.inclusive || lowerPosition.cmp != 0) ? lowerPosition.index : lowerPosition.index + 1;
+
+            builder.add(block.getRange(start, block.termCount()));
+            firstFullBlockIdx = lowerBlock + 1;
+        }
+
+        if (upperPosition != null)
+        {
+            DataBlock block = dataLevel.getBlock(upperBlock);
+            int lastIndex = block.termCount() - 1;
+
+            // The save as with 'lower' but here we need to check if the upper is the last element of the block,
+            // which means that we only have to get individual results if:
+            //  - if it *is not* the last element, or
+            //  - it *is* but shouldn't be included (dictated by upperInclusive)
+            if (upperPosition.index != lastIndex || !upper.inclusive)
+            {
+                int end = (upperPosition.cmp < 0 || (upperPosition.cmp == 0 && upper.inclusive))
+                                ? upperPosition.index + 1 : upperPosition.index;
+
+                builder.add(block.getRange(0, end));
+                lastFullBlockIdx = upperBlock - 1;
+            }
+        }
+
+        int totalSuperBlocks = (lastFullBlockIdx - firstFullBlockIdx) / OnDiskIndexBuilder.SUPER_BLOCK_SIZE;
+
+        // if there are no super-blocks, we can simply read all of the block iterators in sequence
+        if (totalSuperBlocks == 0)
+        {
+            for (int i = firstFullBlockIdx; i <= lastFullBlockIdx; i++)
+                builder.add(dataLevel.getBlock(i).getBlockIndex().iterator(keyFetcher));
+
+            return builder.build();
+        }
+
+        // first get all of the blocks which are aligned before the first super-block in the sequence,
+        // e.g. if the block range was (1, 9) and super-block-size = 4, we need to read 1, 2, 3, 4 - 7 is covered by
+        // super-block, 8, 9 is a remainder.
+
+        int superBlockAlignedStart = firstFullBlockIdx == 0 ? 0 : (int) FBUtilities.align(firstFullBlockIdx, OnDiskIndexBuilder.SUPER_BLOCK_SIZE);
+        for (int blockIdx = firstFullBlockIdx; blockIdx < Math.min(superBlockAlignedStart, lastFullBlockIdx); blockIdx++)
+            builder.add(getBlockIterator(blockIdx));
+
+        // now read all of the super-blocks matched by the request, from the previous comment
+        // it's a block with index 1 (which covers everything from 4 to 7)
+
+        int superBlockIdx = superBlockAlignedStart / OnDiskIndexBuilder.SUPER_BLOCK_SIZE;
+        for (int offset = 0; offset < totalSuperBlocks - 1; offset++)
+            builder.add(dataLevel.getSuperBlock(superBlockIdx++).iterator());
+
+        // now it's time for a remainder read, again from the previous example it's 8, 9 because
+        // we have over-shot previous block but didn't request enough to cover next super-block.
+
+        int lastCoveredBlock = superBlockIdx * OnDiskIndexBuilder.SUPER_BLOCK_SIZE;
+        for (int offset = 0; offset <= (lastFullBlockIdx - lastCoveredBlock); offset++)
+            builder.add(getBlockIterator(lastCoveredBlock + offset));
+
+        return builder.build();
+    }
+
+    private RangeIterator<Long, Token> searchPoint(int lowerBlock, Expression expression)
+    {
+        Iterator<DataTerm> terms = new TermIterator(lowerBlock, expression, IteratorOrder.DESC);
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+
+        while (terms.hasNext())
+        {
+            try
+            {
+                builder.add(terms.next().getTokens());
+            }
+            finally
+            {
+                expression.checkpoint();
+            }
+        }
+
+        return builder.build();
+    }
+
+    private RangeIterator<Long, Token> getBlockIterator(int blockIdx)
+    {
+        DataBlock block = dataLevel.getBlock(blockIdx);
+        return (block.hasCombinedIndex)
+                ? block.getBlockIndex().iterator(keyFetcher)
+                : block.getRange(0, block.termCount());
+    }
+
+    public Iterator<DataTerm> iteratorAt(ByteBuffer query, IteratorOrder order, boolean inclusive)
+    {
+        Expression e = new Expression("", comparator);
+        Expression.Bound bound = new Expression.Bound(query, inclusive);
+
+        switch (order)
+        {
+            case DESC:
+                e.setLower(bound);
+                break;
+
+            case ASC:
+                e.setUpper(bound);
+                break;
+
+            default:
+                throw new IllegalArgumentException("Unknown order: " + order);
+        }
+
+        return new TermIterator(levels.length == 0 ? 0 : getBlockIdx(findPointer(query), query), e, order);
+    }
+
+    private int getDataBlock(ByteBuffer query)
+    {
+        return levels.length == 0 ? 0 : getBlockIdx(findPointer(query), query);
+    }
+
+    public Iterator<DataTerm> iterator()
+    {
+        return new TermIterator(0, new Expression("", comparator), IteratorOrder.DESC);
+    }
+
+    public void close() throws IOException
+    {
+        FileUtils.closeQuietly(indexFile);
+    }
+
+    private PointerTerm findPointer(ByteBuffer query)
+    {
+        PointerTerm ptr = null;
+        for (PointerLevel level : levels)
+        {
+            if ((ptr = level.getPointer(ptr, query)) == null)
+                return null;
+        }
+
+        return ptr;
+    }
+
+    private DataTerm getTerm(ByteBuffer query)
+    {
+        SearchResult<DataTerm> term = searchIndex(query, getDataBlock(query));
+        return term.cmp == 0 ? term.result : null;
+    }
+
+    private SearchResult<DataTerm> searchIndex(ByteBuffer query, int blockIdx)
+    {
+        return dataLevel.getBlock(blockIdx).search(comparator, query);
+    }
+
+    private int getBlockIdx(PointerTerm ptr, ByteBuffer query)
+    {
+        int blockIdx = 0;
+        if (ptr != null)
+        {
+            int cmp = ptr.compareTo(comparator, query);
+            blockIdx = (cmp == 0 || cmp > 0) ? ptr.getBlock() : ptr.getBlock() + 1;
+        }
+
+        return blockIdx;
+    }
+
+    protected class PointerLevel extends Level<PointerBlock>
+    {
+        public PointerLevel(long offset, int count)
+        {
+            super(offset, count);
+        }
+
+        public PointerTerm getPointer(PointerTerm parent, ByteBuffer query)
+        {
+            return getBlock(getBlockIdx(parent, query)).search(comparator, query).result;
+        }
+
+        protected PointerBlock cast(MappedBuffer block)
+        {
+            return new PointerBlock(block);
+        }
+    }
+
+    protected class DataLevel extends Level<DataBlock>
+    {
+        protected final int superBlockCnt;
+        protected final long superBlocksOffset;
+
+        public DataLevel(long offset, int count)
+        {
+            super(offset, count);
+            long baseOffset = blockOffsets + blockCount * 8;
+            superBlockCnt = indexFile.getInt(baseOffset);
+            superBlocksOffset = baseOffset + 4;
+        }
+
+        protected DataBlock cast(MappedBuffer block)
+        {
+            return new DataBlock(block);
+        }
+
+        public OnDiskSuperBlock getSuperBlock(int idx)
+        {
+            assert idx < superBlockCnt : String.format("requested index %d is greater than super block count %d", idx, superBlockCnt);
+            long blockOffset = indexFile.getLong(superBlocksOffset + idx * 8);
+            return new OnDiskSuperBlock(indexFile.duplicate().position(blockOffset));
+        }
+    }
+
+    protected class OnDiskSuperBlock
+    {
+        private final TokenTree tokenTree;
+
+        public OnDiskSuperBlock(MappedBuffer buffer)
+        {
+            tokenTree = new TokenTree(descriptor, buffer);
+        }
+
+        public RangeIterator<Long, Token> iterator()
+        {
+            return tokenTree.iterator(keyFetcher);
+        }
+    }
+
+    protected abstract class Level<T extends OnDiskBlock>
+    {
+        protected final long blockOffsets;
+        protected final int blockCount;
+
+        public Level(long offsets, int count)
+        {
+            this.blockOffsets = offsets;
+            this.blockCount = count;
+        }
+
+        public T getBlock(int idx) throws FSReadError
+        {
+            assert idx >= 0 && idx < blockCount;
+
+            // calculate block offset and move there
+            // (long is intentional, we'll just need mmap implementation which supports long positions)
+            long blockOffset = indexFile.getLong(blockOffsets + idx * 8);
+            return cast(indexFile.duplicate().position(blockOffset));
+        }
+
+        protected abstract T cast(MappedBuffer block);
+    }
+
+    protected class DataBlock extends OnDiskBlock<DataTerm>
+    {
+        public DataBlock(MappedBuffer data)
+        {
+            super(descriptor, data, BlockType.DATA);
+        }
+
+        protected DataTerm cast(MappedBuffer data)
+        {
+            return new DataTerm(data, termSize, getBlockIndex());
+        }
+
+        public RangeIterator<Long, Token> getRange(int start, int end)
+        {
+            RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+            NavigableMap<Long, Token> sparse = new TreeMap<>();
+
+            for (int i = start; i < end; i++)
+            {
+                DataTerm term = getTerm(i);
+
+                if (term.isSparse())
+                {
+                    NavigableMap<Long, Token> tokens = term.getSparseTokens();
+                    for (Map.Entry<Long, Token> t : tokens.entrySet())
+                    {
+                        Token token = sparse.get(t.getKey());
+                        if (token == null)
+                            sparse.put(t.getKey(), t.getValue());
+                        else
+                            token.merge(t.getValue());
+                    }
+                }
+                else
+                {
+                    builder.add(term.getTokens());
+                }
+            }
+
+            PrefetchedTokensIterator prefetched = sparse.isEmpty() ? null : new PrefetchedTokensIterator(sparse);
+
+            if (builder.rangeCount() == 0)
+                return prefetched;
+
+            builder.add(prefetched);
+            return builder.build();
+        }
+    }
+
+    protected class PointerBlock extends OnDiskBlock<PointerTerm>
+    {
+        public PointerBlock(MappedBuffer block)
+        {
+            super(descriptor, block, BlockType.POINTER);
+        }
+
+        protected PointerTerm cast(MappedBuffer data)
+        {
+            return new PointerTerm(data, termSize, hasMarkedPartials);
+        }
+    }
+
+    public class DataTerm extends Term implements Comparable<DataTerm>
+    {
+        private final TokenTree perBlockIndex;
+
+        protected DataTerm(MappedBuffer content, OnDiskIndexBuilder.TermSize size, TokenTree perBlockIndex)
+        {
+            super(content, size, hasMarkedPartials);
+            this.perBlockIndex = perBlockIndex;
+        }
+
+        public RangeIterator<Long, Token> getTokens()
+        {
+            final long blockEnd = FBUtilities.align(content.position(), OnDiskIndexBuilder.BLOCK_SIZE);
+
+            if (isSparse())
+                return new PrefetchedTokensIterator(getSparseTokens());
+
+            long offset = blockEnd + 4 + content.getInt(getDataOffset() + 1);
+            return new TokenTree(descriptor, indexFile.duplicate().position(offset)).iterator(keyFetcher);
+        }
+
+        public boolean isSparse()
+        {
+            return content.get(getDataOffset()) > 0;
+        }
+
+        public NavigableMap<Long, Token> getSparseTokens()
+        {
+            long ptrOffset = getDataOffset();
+
+            byte size = content.get(ptrOffset);
+
+            assert size > 0;
+
+            NavigableMap<Long, Token> individualTokens = new TreeMap<>();
+            for (int i = 0; i < size; i++)
+            {
+                Token token = perBlockIndex.get(content.getLong(ptrOffset + 1 + (8 * i)), keyFetcher);
+
+                assert token != null;
+                individualTokens.put(token.get(), token);
+            }
+
+            return individualTokens;
+        }
+
+        public int compareTo(DataTerm other)
+        {
+            return other == null ? 1 : compareTo(comparator, other.getTerm());
+        }
+    }
+
+    protected static class PointerTerm extends Term
+    {
+        public PointerTerm(MappedBuffer content, OnDiskIndexBuilder.TermSize size, boolean hasMarkedPartials)
+        {
+            super(content, size, hasMarkedPartials);
+        }
+
+        public int getBlock()
+        {
+            return content.getInt(getDataOffset());
+        }
+    }
+
+    private static class PrefetchedTokensIterator extends RangeIterator<Long, Token>
+    {
+        private final NavigableMap<Long, Token> tokens;
+        private PeekingIterator<Token> currentIterator;
+
+        public PrefetchedTokensIterator(NavigableMap<Long, Token> tokens)
+        {
+            super(tokens.firstKey(), tokens.lastKey(), tokens.size());
+            this.tokens = tokens;
+            this.currentIterator = Iterators.peekingIterator(tokens.values().iterator());
+        }
+
+        protected Token computeNext()
+        {
+            return currentIterator != null && currentIterator.hasNext()
+                    ? currentIterator.next()
+                    : endOfData();
+        }
+
+        protected void performSkipTo(Long nextToken)
+        {
+            currentIterator = Iterators.peekingIterator(tokens.tailMap(nextToken, true).values().iterator());
+        }
+
+        public void close() throws IOException
+        {
+            endOfData();
+        }
+    }
+
+    public AbstractType<?> getComparator()
+    {
+        return comparator;
+    }
+
+    public String getIndexPath()
+    {
+        return indexPath;
+    }
+
+    private class TermIterator extends AbstractIterator<DataTerm>
+    {
+        private final Expression e;
+        private final IteratorOrder order;
+
+        protected OnDiskBlock<DataTerm> currentBlock;
+        protected int blockIndex, offset;
+
+        private boolean checkLower = true, checkUpper = true;
+
+        public TermIterator(int startBlock, Expression expression, IteratorOrder order)
+        {
+            this.e = expression;
+            this.order = order;
+            this.blockIndex = startBlock;
+
+            nextBlock();
+        }
+
+        protected DataTerm computeNext()
+        {
+            for (;;)
+            {
+                if (currentBlock == null)
+                    return endOfData();
+
+                if (offset >= 0 && offset < currentBlock.termCount())
+                {
+                    DataTerm currentTerm = currentBlock.getTerm(nextOffset());
+
+                    // we need to step over all of the partial terms, in PREFIX mode,
+                    // encountered by the query until upper-bound tells us to stop
+                    if (e.getOp() == Op.PREFIX && currentTerm.isPartial())
+                        continue;
+
+                    // haven't reached the start of the query range yet, let's
+                    // keep skip the current term until lower bound is satisfied
+                    if (checkLower && !e.isLowerSatisfiedBy(currentTerm))
+                        continue;
+
+                    // flip the flag right on the first bounds match
+                    // to avoid expensive comparisons
+                    checkLower = false;
+
+                    if (checkUpper && !e.isUpperSatisfiedBy(currentTerm))
+                        return endOfData();
+
+                    return currentTerm;
+                }
+
+                nextBlock();
+            }
+        }
+
+        protected void nextBlock()
+        {
+            currentBlock = null;
+
+            if (blockIndex < 0 || blockIndex >= dataLevel.blockCount)
+                return;
+
+            currentBlock = dataLevel.getBlock(nextBlockIndex());
+            offset = checkLower ? order.startAt(currentBlock, e) : currentBlock.minOffset(order);
+
+            // let's check the last term of the new block right away
+            // if expression's upper bound is satisfied by it such means that we can avoid
+            // doing any expensive upper bound checks for that block.
+            checkUpper = e.hasUpper() && !e.isUpperSatisfiedBy(currentBlock.getTerm(currentBlock.maxOffset(order)));
+        }
+
+        protected int nextBlockIndex()
+        {
+            int current = blockIndex;
+            blockIndex += order.step;
+            return current;
+        }
+
+        protected int nextOffset()
+        {
+            int current = offset;
+            offset += order.step;
+            return current;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java b/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java
new file mode 100644
index 0000000..0298539
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java
@@ -0,0 +1,681 @@
+/*
+ * 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.index.sasi.disk;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.*;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.index.sasi.plan.Expression.Op;
+import org.apache.cassandra.index.sasi.sa.IndexedTerm;
+import org.apache.cassandra.index.sasi.sa.IntegralSA;
+import org.apache.cassandra.index.sasi.sa.SA;
+import org.apache.cassandra.index.sasi.sa.TermIterator;
+import org.apache.cassandra.index.sasi.sa.SuffixSA;
+import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.io.FSWriteError;
+import org.apache.cassandra.io.util.*;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.Pair;
+
+import com.carrotsearch.hppc.LongArrayList;
+import com.carrotsearch.hppc.LongSet;
+import com.carrotsearch.hppc.ShortArrayList;
+import com.google.common.annotations.VisibleForTesting;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class OnDiskIndexBuilder
+{
+    private static final Logger logger = LoggerFactory.getLogger(OnDiskIndexBuilder.class);
+
+    public enum Mode
+    {
+        PREFIX(EnumSet.of(Op.EQ, Op.MATCH, Op.PREFIX, Op.NOT_EQ, Op.RANGE)),
+        CONTAINS(EnumSet.of(Op.EQ, Op.MATCH, Op.CONTAINS, Op.PREFIX, Op.SUFFIX, Op.NOT_EQ)),
+        SPARSE(EnumSet.of(Op.EQ, Op.NOT_EQ, Op.RANGE));
+
+        Set<Op> supportedOps;
+
+        Mode(Set<Op> ops)
+        {
+            supportedOps = ops;
+        }
+
+        public static Mode mode(String mode)
+        {
+            return Mode.valueOf(mode.toUpperCase());
+        }
+
+        public boolean supports(Op op)
+        {
+            return supportedOps.contains(op);
+        }
+    }
+
+    public enum TermSize
+    {
+        INT(4), LONG(8), UUID(16), VARIABLE(-1);
+
+        public final int size;
+
+        TermSize(int size)
+        {
+            this.size = size;
+        }
+
+        public boolean isConstant()
+        {
+            return this != VARIABLE;
+        }
+
+        public static TermSize of(int size)
+        {
+            switch (size)
+            {
+                case -1:
+                    return VARIABLE;
+
+                case 4:
+                    return INT;
+
+                case 8:
+                    return LONG;
+
+                case 16:
+                    return UUID;
+
+                default:
+                    throw new IllegalStateException("unknown state: " + size);
+            }
+        }
+
+        public static TermSize sizeOf(AbstractType<?> comparator)
+        {
+            if (comparator instanceof Int32Type || comparator instanceof FloatType)
+                return INT;
+
+            if (comparator instanceof LongType || comparator instanceof DoubleType
+                    || comparator instanceof TimestampType || comparator instanceof DateType)
+                return LONG;
+
+            if (comparator instanceof TimeUUIDType || comparator instanceof UUIDType)
+                return UUID;
+
+            return VARIABLE;
+        }
+    }
+
+    public static final int BLOCK_SIZE = 4096;
+    public static final int MAX_TERM_SIZE = 1024;
+    public static final int SUPER_BLOCK_SIZE = 64;
+    public static final int IS_PARTIAL_BIT = 15;
+
+    private static final SequentialWriterOption WRITER_OPTION = SequentialWriterOption.newBuilder()
+                                                                                      .bufferSize(BLOCK_SIZE)
+                                                                                      .build();
+
+    private final List<MutableLevel<InMemoryPointerTerm>> levels = new ArrayList<>();
+    private MutableLevel<InMemoryDataTerm> dataLevel;
+
+    private final TermSize termSize;
+
+    private final AbstractType<?> keyComparator, termComparator;
+
+    private final Map<ByteBuffer, TokenTreeBuilder> terms;
+    private final Mode mode;
+    private final boolean marksPartials;
+
+    private ByteBuffer minKey, maxKey;
+    private long estimatedBytes;
+
+    public OnDiskIndexBuilder(AbstractType<?> keyComparator, AbstractType<?> comparator, Mode mode)
+    {
+        this(keyComparator, comparator, mode, true);
+    }
+
+    public OnDiskIndexBuilder(AbstractType<?> keyComparator, AbstractType<?> comparator, Mode mode, boolean marksPartials)
+    {
+        this.keyComparator = keyComparator;
+        this.termComparator = comparator;
+        this.terms = new HashMap<>();
+        this.termSize = TermSize.sizeOf(comparator);
+        this.mode = mode;
+        this.marksPartials = marksPartials;
+    }
+
+    public OnDiskIndexBuilder add(ByteBuffer term, DecoratedKey key, long keyPosition)
+    {
+        if (term.remaining() >= MAX_TERM_SIZE)
+        {
+            logger.error("Rejecting value (value size {}, maximum size {}).",
+                         FBUtilities.prettyPrintMemory(term.remaining()),
+                         FBUtilities.prettyPrintMemory(Short.MAX_VALUE));
+            return this;
+        }
+
+        TokenTreeBuilder tokens = terms.get(term);
+        if (tokens == null)
+        {
+            terms.put(term, (tokens = new DynamicTokenTreeBuilder()));
+
+            // on-heap size estimates from jol
+            // 64 bytes for TTB + 48 bytes for TreeMap in TTB + size bytes for the term (map key)
+            estimatedBytes += 64 + 48 + term.remaining();
+        }
+
+        tokens.add((Long) key.getToken().getTokenValue(), keyPosition);
+
+        // calculate key range (based on actual key values) for current index
+        minKey = (minKey == null || keyComparator.compare(minKey, key.getKey()) > 0) ? key.getKey() : minKey;
+        maxKey = (maxKey == null || keyComparator.compare(maxKey, key.getKey()) < 0) ? key.getKey() : maxKey;
+
+        // 60 ((boolean(1)*4) + (long(8)*4) + 24) bytes for the LongOpenHashSet created when the keyPosition was added
+        // + 40 bytes for the TreeMap.Entry + 8 bytes for the token (key).
+        // in the case of hash collision for the token we may overestimate but this is extremely rare
+        estimatedBytes += 60 + 40 + 8;
+
+        return this;
+    }
+
+    public long estimatedMemoryUse()
+    {
+        return estimatedBytes;
+    }
+
+    private void addTerm(InMemoryDataTerm term, SequentialWriter out) throws IOException
+    {
+        InMemoryPointerTerm ptr = dataLevel.add(term);
+        if (ptr == null)
+            return;
+
+        int levelIdx = 0;
+        for (;;)
+        {
+            MutableLevel<InMemoryPointerTerm> level = getIndexLevel(levelIdx++, out);
+            if ((ptr = level.add(ptr)) == null)
+                break;
+        }
+    }
+
+    public boolean isEmpty()
+    {
+        return terms.isEmpty();
+    }
+
+    public void finish(Pair<ByteBuffer, ByteBuffer> range, File file, TermIterator terms)
+    {
+        finish(Descriptor.CURRENT, range, file, terms);
+    }
+
+    /**
+     * Finishes up index building process by creating/populating index file.
+     *
+     * @param indexFile The file to write index contents to.
+     *
+     * @return true if index was written successfully, false otherwise (e.g. if index was empty).
+     *
+     * @throws FSWriteError on I/O error.
+     */
+    public boolean finish(File indexFile) throws FSWriteError
+    {
+        return finish(Descriptor.CURRENT, indexFile);
+    }
+
+    @VisibleForTesting
+    protected boolean finish(Descriptor descriptor, File file) throws FSWriteError
+    {
+        // no terms means there is nothing to build
+        if (terms.isEmpty())
+        {
+            try
+            {
+                file.createNewFile();
+            }
+            catch (IOException e)
+            {
+                throw new FSWriteError(e, file);
+            }
+
+            return false;
+        }
+
+        // split terms into suffixes only if it's text, otherwise (even if CONTAINS is set) use terms in original form
+        SA sa = ((termComparator instanceof UTF8Type || termComparator instanceof AsciiType) && mode == Mode.CONTAINS)
+                    ? new SuffixSA(termComparator, mode) : new IntegralSA(termComparator, mode);
+
+        for (Map.Entry<ByteBuffer, TokenTreeBuilder> term : terms.entrySet())
+            sa.add(term.getKey(), term.getValue());
+
+        finish(descriptor, Pair.create(minKey, maxKey), file, sa.finish());
+        return true;
+    }
+
+    @SuppressWarnings("resource")
+    protected void finish(Descriptor descriptor, Pair<ByteBuffer, ByteBuffer> range, File file, TermIterator terms)
+    {
+        SequentialWriter out = null;
+
+        try
+        {
+            out = new SequentialWriter(file, WRITER_OPTION);
+
+            out.writeUTF(descriptor.version.toString());
+
+            out.writeShort(termSize.size);
+
+            // min, max term (useful to find initial scan range from search expressions)
+            ByteBufferUtil.writeWithShortLength(terms.minTerm(), out);
+            ByteBufferUtil.writeWithShortLength(terms.maxTerm(), out);
+
+            // min, max keys covered by index (useful when searching across multiple indexes)
+            ByteBufferUtil.writeWithShortLength(range.left, out);
+            ByteBufferUtil.writeWithShortLength(range.right, out);
+
+            out.writeUTF(mode.toString());
+            out.writeBoolean(marksPartials);
+
+            out.skipBytes((int) (BLOCK_SIZE - out.position()));
+
+            dataLevel = mode == Mode.SPARSE ? new DataBuilderLevel(out, new MutableDataBlock(termComparator, mode))
+                                            : new MutableLevel<>(out, new MutableDataBlock(termComparator, mode));
+            while (terms.hasNext())
+            {
+                Pair<IndexedTerm, TokenTreeBuilder> term = terms.next();
+                addTerm(new InMemoryDataTerm(term.left, term.right), out);
+            }
+
+            dataLevel.finalFlush();
+            for (MutableLevel l : levels)
+                l.flush(); // flush all of the buffers
+
+            // and finally write levels index
+            final long levelIndexPosition = out.position();
+
+            out.writeInt(levels.size());
+            for (int i = levels.size() - 1; i >= 0; i--)
+                levels.get(i).flushMetadata();
+
+            dataLevel.flushMetadata();
+
+            out.writeLong(levelIndexPosition);
+
+            // sync contents of the output and disk,
+            // since it's not done implicitly on close
+            out.sync();
+        }
+        catch (IOException e)
+        {
+            throw new FSWriteError(e, file);
+        }
+        finally
+        {
+            FileUtils.closeQuietly(out);
+        }
+    }
+
+    private MutableLevel<InMemoryPointerTerm> getIndexLevel(int idx, SequentialWriter out)
+    {
+        if (levels.size() == 0)
+            levels.add(new MutableLevel<>(out, new MutableBlock<>()));
+
+        if (levels.size() - 1 < idx)
+        {
+            int toAdd = idx - (levels.size() - 1);
+            for (int i = 0; i < toAdd; i++)
+                levels.add(new MutableLevel<>(out, new MutableBlock<>()));
+        }
+
+        return levels.get(idx);
+    }
+
+    protected static void alignToBlock(SequentialWriter out) throws IOException
+    {
+        long endOfBlock = out.position();
+        if ((endOfBlock & (BLOCK_SIZE - 1)) != 0) // align on the block boundary if needed
+            out.skipBytes((int) (FBUtilities.align(endOfBlock, BLOCK_SIZE) - endOfBlock));
+    }
+
+    private class InMemoryTerm
+    {
+        protected final IndexedTerm term;
+
+        public InMemoryTerm(IndexedTerm term)
+        {
+            this.term = term;
+        }
+
+        public int serializedSize()
+        {
+            return (termSize.isConstant() ? 0 : 2) + term.getBytes().remaining();
+        }
+
+        public void serialize(DataOutputPlus out) throws IOException
+        {
+            if (termSize.isConstant())
+            {
+                out.write(term.getBytes());
+            }
+            else
+            {
+                out.writeShort(term.getBytes().remaining() | ((marksPartials && term.isPartial() ? 1 : 0) << IS_PARTIAL_BIT));
+                out.write(term.getBytes());
+            }
+
+        }
+    }
+
+    private class InMemoryPointerTerm extends InMemoryTerm
+    {
+        protected final int blockCnt;
+
+        public InMemoryPointerTerm(IndexedTerm term, int blockCnt)
+        {
+            super(term);
+            this.blockCnt = blockCnt;
+        }
+
+        public int serializedSize()
+        {
+            return super.serializedSize() + 4;
+        }
+
+        public void serialize(DataOutputPlus out) throws IOException
+        {
+            super.serialize(out);
+            out.writeInt(blockCnt);
+        }
+    }
+
+    private class InMemoryDataTerm extends InMemoryTerm
+    {
+        private final TokenTreeBuilder keys;
+
+        public InMemoryDataTerm(IndexedTerm term, TokenTreeBuilder keys)
+        {
+            super(term);
+            this.keys = keys;
+        }
+    }
+
+    private class MutableLevel<T extends InMemoryTerm>
+    {
+        private final LongArrayList blockOffsets = new LongArrayList();
+
+        protected final SequentialWriter out;
+
+        private final MutableBlock<T> inProcessBlock;
+        private InMemoryPointerTerm lastTerm;
+
+        public MutableLevel(SequentialWriter out, MutableBlock<T> block)
+        {
+            this.out = out;
+            this.inProcessBlock = block;
+        }
+
+        /**
+         * @return If we flushed a block, return the last term of that block; else, null.
+         */
+        public InMemoryPointerTerm add(T term) throws IOException
+        {
+            InMemoryPointerTerm toPromote = null;
+
+            if (!inProcessBlock.hasSpaceFor(term))
+            {
+                flush();
+                toPromote = lastTerm;
+            }
+
+            inProcessBlock.add(term);
+
+            lastTerm = new InMemoryPointerTerm(term.term, blockOffsets.size());
+            return toPromote;
+        }
+
+        public void flush() throws IOException
+        {
+            blockOffsets.add(out.position());
+            inProcessBlock.flushAndClear(out);
+        }
+
+        public void finalFlush() throws IOException
+        {
+            flush();
+        }
+
+        public void flushMetadata() throws IOException
+        {
+            flushMetadata(blockOffsets);
+        }
+
+        protected void flushMetadata(LongArrayList longArrayList) throws IOException
+        {
+            out.writeInt(longArrayList.size());
+            for (int i = 0; i < longArrayList.size(); i++)
+                out.writeLong(longArrayList.get(i));
+        }
+    }
+
+    /** builds standard data blocks and super blocks, as well */
+    private class DataBuilderLevel extends MutableLevel<InMemoryDataTerm>
+    {
+        private final LongArrayList superBlockOffsets = new LongArrayList();
+
+        /** count of regular data blocks written since current super block was init'd */
+        private int dataBlocksCnt;
+        private TokenTreeBuilder superBlockTree;
+
+        public DataBuilderLevel(SequentialWriter out, MutableBlock<InMemoryDataTerm> block)
+        {
+            super(out, block);
+            superBlockTree = new DynamicTokenTreeBuilder();
+        }
+
+        public InMemoryPointerTerm add(InMemoryDataTerm term) throws IOException
+        {
+            InMemoryPointerTerm ptr = super.add(term);
+            if (ptr != null)
+            {
+                dataBlocksCnt++;
+                flushSuperBlock(false);
+            }
+            superBlockTree.add(term.keys);
+            return ptr;
+        }
+
+        public void flushSuperBlock(boolean force) throws IOException
+        {
+            if (dataBlocksCnt == SUPER_BLOCK_SIZE || (force && !superBlockTree.isEmpty()))
+            {
+                superBlockOffsets.add(out.position());
+                superBlockTree.finish().write(out);
+                alignToBlock(out);
+
+                dataBlocksCnt = 0;
+                superBlockTree = new DynamicTokenTreeBuilder();
+            }
+        }
+
+        public void finalFlush() throws IOException
+        {
+            super.flush();
+            flushSuperBlock(true);
+        }
+
+        public void flushMetadata() throws IOException
+        {
+            super.flushMetadata();
+            flushMetadata(superBlockOffsets);
+        }
+    }
+
+    private static class MutableBlock<T extends InMemoryTerm>
+    {
+        protected final DataOutputBufferFixed buffer;
+        protected final ShortArrayList offsets;
+
+        public MutableBlock()
+        {
+            buffer = new DataOutputBufferFixed(BLOCK_SIZE);
+            offsets = new ShortArrayList();
+        }
+
+        public final void add(T term) throws IOException
+        {
+            offsets.add((short) buffer.position());
+            addInternal(term);
+        }
+
+        protected void addInternal(T term) throws IOException
+        {
+            term.serialize(buffer);
+        }
+
+        public boolean hasSpaceFor(T element)
+        {
+            return sizeAfter(element) < BLOCK_SIZE;
+        }
+
+        protected int sizeAfter(T element)
+        {
+            return getWatermark() + 4 + element.serializedSize();
+        }
+
+        protected int getWatermark()
+        {
+            return 4 + offsets.size() * 2 + (int) buffer.position();
+        }
+
+        public void flushAndClear(SequentialWriter out) throws IOException
+        {
+            out.writeInt(offsets.size());
+            for (int i = 0; i < offsets.size(); i++)
+                out.writeShort(offsets.get(i));
+
+            out.write(buffer.buffer());
+
+            alignToBlock(out);
+
+            offsets.clear();
+            buffer.clear();
+        }
+    }
+
+    private static class MutableDataBlock extends MutableBlock<InMemoryDataTerm>
+    {
+        private static final int MAX_KEYS_SPARSE = 5;
+
+        private final AbstractType<?> comparator;
+        private final Mode mode;
+
+        private int offset = 0;
+
+        private final List<TokenTreeBuilder> containers = new ArrayList<>();
+        private TokenTreeBuilder combinedIndex;
+
+        public MutableDataBlock(AbstractType<?> comparator, Mode mode)
+        {
+            this.comparator = comparator;
+            this.mode = mode;
+            this.combinedIndex = initCombinedIndex();
+        }
+
+        protected void addInternal(InMemoryDataTerm term) throws IOException
+        {
+            TokenTreeBuilder keys = term.keys;
+
+            if (mode == Mode.SPARSE)
+            {
+                if (keys.getTokenCount() > MAX_KEYS_SPARSE)
+                    throw new IOException(String.format("Term - '%s' belongs to more than %d keys in %s mode, which is not allowed.",
+                                                        comparator.getString(term.term.getBytes()), MAX_KEYS_SPARSE, mode.name()));
+
+                writeTerm(term, keys);
+            }
+            else
+            {
+                writeTerm(term, offset);
+
+                offset += keys.serializedSize();
+                containers.add(keys);
+            }
+
+            if (mode == Mode.SPARSE)
+                combinedIndex.add(keys);
+        }
+
+        protected int sizeAfter(InMemoryDataTerm element)
+        {
+            return super.sizeAfter(element) + ptrLength(element);
+        }
+
+        public void flushAndClear(SequentialWriter out) throws IOException
+        {
+            super.flushAndClear(out);
+
+            out.writeInt(mode == Mode.SPARSE ? offset : -1);
+
+            if (containers.size() > 0)
+            {
+                for (TokenTreeBuilder tokens : containers)
+                    tokens.write(out);
+            }
+
+            if (mode == Mode.SPARSE && combinedIndex != null)
+                combinedIndex.finish().write(out);
+
+            alignToBlock(out);
+
+            containers.clear();
+            combinedIndex = initCombinedIndex();
+
+            offset = 0;
+        }
+
+        private int ptrLength(InMemoryDataTerm term)
+        {
+            return (term.keys.getTokenCount() > 5)
+                    ? 5 // 1 byte type + 4 byte offset to the tree
+                    : 1 + (8 * (int) term.keys.getTokenCount()); // 1 byte size + n 8 byte tokens
+        }
+
+        private void writeTerm(InMemoryTerm term, TokenTreeBuilder keys) throws IOException
+        {
+            term.serialize(buffer);
+            buffer.writeByte((byte) keys.getTokenCount());
+            for (Pair<Long, LongSet> key : keys)
+                buffer.writeLong(key.left);
+        }
+
+        private void writeTerm(InMemoryTerm term, int offset) throws IOException
+        {
+            term.serialize(buffer);
+            buffer.writeByte(0x0);
+            buffer.writeInt(offset);
+        }
+
+        private TokenTreeBuilder initCombinedIndex()
+        {
+            return mode == Mode.SPARSE ? new DynamicTokenTreeBuilder() : null;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriter.java b/src/java/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriter.java
new file mode 100644
index 0000000..527deab
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriter.java
@@ -0,0 +1,375 @@
+/*
+ * 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.index.sasi.disk;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.*;
+
+import org.apache.cassandra.concurrent.JMXEnabledThreadPoolExecutor;
+import org.apache.cassandra.concurrent.NamedThreadFactory;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.compaction.OperationType;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.Unfiltered;
+import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.utils.CombinedTermIterator;
+import org.apache.cassandra.index.sasi.utils.TypeUtil;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.io.FSError;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.format.SSTableFlushObserver;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.Pair;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.Uninterruptibles;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PerSSTableIndexWriter implements SSTableFlushObserver
+{
+    private static final Logger logger = LoggerFactory.getLogger(PerSSTableIndexWriter.class);
+
+    private static final int POOL_SIZE = 8;
+    private static final ThreadPoolExecutor INDEX_FLUSHER_MEMTABLE;
+    private static final ThreadPoolExecutor INDEX_FLUSHER_GENERAL;
+
+    static
+    {
+        INDEX_FLUSHER_GENERAL = new JMXEnabledThreadPoolExecutor(POOL_SIZE, POOL_SIZE, 1, TimeUnit.MINUTES,
+                                                                 new LinkedBlockingQueue<>(),
+                                                                 new NamedThreadFactory("SASI-General"),
+                                                                 "internal");
+        INDEX_FLUSHER_GENERAL.allowCoreThreadTimeOut(true);
+
+        INDEX_FLUSHER_MEMTABLE = new JMXEnabledThreadPoolExecutor(POOL_SIZE, POOL_SIZE, 1, TimeUnit.MINUTES,
+                                                                  new LinkedBlockingQueue<>(),
+                                                                  new NamedThreadFactory("SASI-Memtable"),
+                                                                  "internal");
+        INDEX_FLUSHER_MEMTABLE.allowCoreThreadTimeOut(true);
+    }
+
+    private final int nowInSec = FBUtilities.nowInSeconds();
+
+    private final Descriptor descriptor;
+    private final OperationType source;
+
+    private final AbstractType<?> keyValidator;
+
+    @VisibleForTesting
+    protected final Map<ColumnDefinition, Index> indexes;
+
+    private DecoratedKey currentKey;
+    private long currentKeyPosition;
+    private boolean isComplete;
+
+    public PerSSTableIndexWriter(AbstractType<?> keyValidator,
+                                 Descriptor descriptor,
+                                 OperationType source,
+                                 Map<ColumnDefinition, ColumnIndex> supportedIndexes)
+    {
+        this.keyValidator = keyValidator;
+        this.descriptor = descriptor;
+        this.source = source;
+        this.indexes = new HashMap<>();
+        for (Map.Entry<ColumnDefinition, ColumnIndex> entry : supportedIndexes.entrySet())
+            indexes.put(entry.getKey(), newIndex(entry.getValue()));
+    }
+
+    public void begin()
+    {}
+
+    public void startPartition(DecoratedKey key, long curPosition)
+    {
+        currentKey = key;
+        currentKeyPosition = curPosition;
+    }
+
+    public void nextUnfilteredCluster(Unfiltered unfiltered)
+    {
+        if (!unfiltered.isRow())
+            return;
+
+        Row row = (Row) unfiltered;
+
+        indexes.forEach((column, index) -> {
+            ByteBuffer value = ColumnIndex.getValueOf(column, row, nowInSec);
+            if (value == null)
+                return;
+
+            if (index == null)
+                throw new IllegalArgumentException("No index exists for column " + column.name.toString());
+
+            index.add(value.duplicate(), currentKey, currentKeyPosition);
+        });
+    }
+
+    public void complete()
+    {
+        if (isComplete)
+            return;
+
+        currentKey = null;
+
+        try
+        {
+            CountDownLatch latch = new CountDownLatch(indexes.size());
+            for (Index index : indexes.values())
+                index.complete(latch);
+
+            Uninterruptibles.awaitUninterruptibly(latch);
+        }
+        finally
+        {
+            indexes.clear();
+            isComplete = true;
+        }
+    }
+
+    public Index getIndex(ColumnDefinition columnDef)
+    {
+        return indexes.get(columnDef);
+    }
+
+    public Descriptor getDescriptor()
+    {
+        return descriptor;
+    }
+
+    @VisibleForTesting
+    protected Index newIndex(ColumnIndex columnIndex)
+    {
+        return new Index(columnIndex);
+    }
+
+    @VisibleForTesting
+    protected class Index
+    {
+        @VisibleForTesting
+        protected final String outputFile;
+
+        private final ColumnIndex columnIndex;
+        private final AbstractAnalyzer analyzer;
+        private final long maxMemorySize;
+
+        @VisibleForTesting
+        protected final Set<Future<OnDiskIndex>> segments;
+        private int segmentNumber = 0;
+
+        private OnDiskIndexBuilder currentBuilder;
+
+        public Index(ColumnIndex columnIndex)
+        {
+            this.columnIndex = columnIndex;
+            this.outputFile = descriptor.filenameFor(columnIndex.getComponent());
+            this.analyzer = columnIndex.getAnalyzer();
+            this.segments = new HashSet<>();
+            this.maxMemorySize = maxMemorySize(columnIndex);
+            this.currentBuilder = newIndexBuilder();
+        }
+
+        public void add(ByteBuffer term, DecoratedKey key, long keyPosition)
+        {
+            if (term.remaining() == 0)
+                return;
+
+            boolean isAdded = false;
+
+            analyzer.reset(term);
+            while (analyzer.hasNext())
+            {
+                ByteBuffer token = analyzer.next();
+                int size = token.remaining();
+
+                if (token.remaining() >= OnDiskIndexBuilder.MAX_TERM_SIZE)
+                {
+                    logger.info("Rejecting value (size {}, maximum {}) for column {} (analyzed {}) at {} SSTable.",
+                            FBUtilities.prettyPrintMemory(term.remaining()),
+                            FBUtilities.prettyPrintMemory(OnDiskIndexBuilder.MAX_TERM_SIZE),
+                            columnIndex.getColumnName(),
+                            columnIndex.getMode().isAnalyzed,
+                            descriptor);
+                    continue;
+                }
+
+                if (!TypeUtil.isValid(token, columnIndex.getValidator()))
+                {
+                    if ((token = TypeUtil.tryUpcast(token, columnIndex.getValidator())) == null)
+                    {
+                        logger.info("({}) Failed to add {} to index for key: {}, value size was {}, validator is {}.",
+                                    outputFile,
+                                    columnIndex.getColumnName(),
+                                    keyValidator.getString(key.getKey()),
+                                    FBUtilities.prettyPrintMemory(size),
+                                    columnIndex.getValidator());
+                        continue;
+                    }
+                }
+
+                currentBuilder.add(token, key, keyPosition);
+                isAdded = true;
+            }
+
+            if (!isAdded || currentBuilder.estimatedMemoryUse() < maxMemorySize)
+                return; // non of the generated tokens were added to the index or memory size wasn't reached
+
+            segments.add(getExecutor().submit(scheduleSegmentFlush(false)));
+        }
+
+        @VisibleForTesting
+        protected Callable<OnDiskIndex> scheduleSegmentFlush(final boolean isFinal)
+        {
+            final OnDiskIndexBuilder builder = currentBuilder;
+            currentBuilder = newIndexBuilder();
+
+            final String segmentFile = filename(isFinal);
+
+            return () -> {
+                long start = System.nanoTime();
+
+                try
+                {
+                    File index = new File(segmentFile);
+                    return builder.finish(index) ? new OnDiskIndex(index, columnIndex.getValidator(), null) : null;
+                }
+                catch (Exception | FSError e)
+                {
+                    logger.error("Failed to build index segment {}", segmentFile, e);
+                    return null;
+                }
+                finally
+                {
+                    if (!isFinal)
+                        logger.info("Flushed index segment {}, took {} ms.", segmentFile, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
+                }
+            };
+        }
+
+        public void complete(final CountDownLatch latch)
+        {
+            logger.info("Scheduling index flush to {}", outputFile);
+
+            getExecutor().submit((Runnable) () -> {
+                long start1 = System.nanoTime();
+
+                OnDiskIndex[] parts = new OnDiskIndex[segments.size() + 1];
+
+                try
+                {
+                    // no parts present, build entire index from memory
+                    if (segments.isEmpty())
+                    {
+                        scheduleSegmentFlush(true).call();
+                        return;
+                    }
+
+                    // parts are present but there is something still in memory, let's flush that inline
+                    if (!currentBuilder.isEmpty())
+                    {
+                        @SuppressWarnings("resource")
+                        OnDiskIndex last = scheduleSegmentFlush(false).call();
+                        segments.add(Futures.immediateFuture(last));
+                    }
+
+                    int index = 0;
+                    ByteBuffer combinedMin = null, combinedMax = null;
+
+                    for (Future<OnDiskIndex> f : segments)
+                    {
+                        @SuppressWarnings("resource")
+                        OnDiskIndex part = f.get();
+                        if (part == null)
+                            continue;
+
+                        parts[index++] = part;
+                        combinedMin = (combinedMin == null || keyValidator.compare(combinedMin, part.minKey()) > 0) ? part.minKey() : combinedMin;
+                        combinedMax = (combinedMax == null || keyValidator.compare(combinedMax, part.maxKey()) < 0) ? part.maxKey() : combinedMax;
+                    }
+
+                    OnDiskIndexBuilder builder = newIndexBuilder();
+                    builder.finish(Pair.create(combinedMin, combinedMax),
+                                   new File(outputFile),
+                                   new CombinedTermIterator(parts));
+                }
+                catch (Exception | FSError e)
+                {
+                    logger.error("Failed to flush index {}.", outputFile, e);
+                    FileUtils.delete(outputFile);
+                }
+                finally
+                {
+                    logger.info("Index flush to {} took {} ms.", outputFile, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start1));
+
+                    for (int segment = 0; segment < segmentNumber; segment++)
+                    {
+                        @SuppressWarnings("resource")
+                        OnDiskIndex part = parts[segment];
+
+                        if (part != null)
+                            FileUtils.closeQuietly(part);
+
+                        FileUtils.delete(outputFile + "_" + segment);
+                    }
+
+                    latch.countDown();
+                }
+            });
+        }
+
+        private ExecutorService getExecutor()
+        {
+            return source == OperationType.FLUSH ? INDEX_FLUSHER_MEMTABLE : INDEX_FLUSHER_GENERAL;
+        }
+
+        private OnDiskIndexBuilder newIndexBuilder()
+        {
+            return new OnDiskIndexBuilder(keyValidator, columnIndex.getValidator(), columnIndex.getMode().mode);
+        }
+
+        public String filename(boolean isFinal)
+        {
+            return outputFile + (isFinal ? "" : "_" + segmentNumber++);
+        }
+    }
+
+    protected long maxMemorySize(ColumnIndex columnIndex)
+    {
+        // 1G for memtable and configuration for compaction
+        return source == OperationType.FLUSH ? 1073741824L : columnIndex.getMode().maxCompactionFlushMemoryInBytes;
+    }
+
+    public int hashCode()
+    {
+        return descriptor.hashCode();
+    }
+
+    public boolean equals(Object o)
+    {
+        return !(o == null || !(o instanceof PerSSTableIndexWriter)) && descriptor.equals(((PerSSTableIndexWriter) o).descriptor);
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/disk/StaticTokenTreeBuilder.java b/src/java/org/apache/cassandra/index/sasi/disk/StaticTokenTreeBuilder.java
new file mode 100644
index 0000000..6e64c56
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/disk/StaticTokenTreeBuilder.java
@@ -0,0 +1,253 @@
+/*
+ * 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.index.sasi.disk;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+import java.util.SortedMap;
+
+import org.apache.cassandra.index.sasi.utils.CombinedTerm;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.utils.AbstractIterator;
+import org.apache.cassandra.utils.Pair;
+
+import com.carrotsearch.hppc.LongSet;
+import com.google.common.collect.Iterators;
+
+/**
+ * Intended usage of this class is to be used in place of {@link DynamicTokenTreeBuilder}
+ * when multiple index segments produced by {@link PerSSTableIndexWriter} are stitched together
+ * by {@link PerSSTableIndexWriter#complete()}.
+ *
+ * This class uses the RangeIterator, now provided by
+ * {@link CombinedTerm#getTokenIterator()}, to iterate the data twice.
+ * The first iteration builds the tree with leaves that contain only enough
+ * information to build the upper layers -- these leaves do not store more
+ * than their minimum and maximum tokens plus their total size, which makes them
+ * un-serializable.
+ *
+ * When the tree is written to disk the final layer is not
+ * written. Its at this point the data is iterated once again to write
+ * the leaves to disk. This (logarithmically) reduces copying of the
+ * token values while building and writing upper layers of the tree,
+ * removes the use of SortedMap when combining SAs, and relies on the
+ * memory mapped SAs otherwise, greatly improving performance and no
+ * longer causing OOMs when TokenTree sizes are big.
+ *
+ * See https://issues.apache.org/jira/browse/CASSANDRA-11383 for more details.
+ */
+@SuppressWarnings("resource")
+public class StaticTokenTreeBuilder extends AbstractTokenTreeBuilder
+{
+    private final CombinedTerm combinedTerm;
+
+    public StaticTokenTreeBuilder(CombinedTerm term)
+    {
+        combinedTerm = term;
+    }
+
+    public void add(Long token, long keyPosition)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public void add(SortedMap<Long, LongSet> data)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public void add(Iterator<Pair<Long, LongSet>> data)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean isEmpty()
+    {
+        return tokenCount == 0;
+    }
+
+    public Iterator<Pair<Long, LongSet>> iterator()
+    {
+        Iterator<Token> iterator = combinedTerm.getTokenIterator();
+        return new AbstractIterator<Pair<Long, LongSet>>()
+        {
+            protected Pair<Long, LongSet> computeNext()
+            {
+                if (!iterator.hasNext())
+                    return endOfData();
+
+                Token token = iterator.next();
+                return Pair.create(token.get(), token.getOffsets());
+            }
+        };
+    }
+
+    public long getTokenCount()
+    {
+        return tokenCount;
+    }
+
+    @Override
+    public void write(DataOutputPlus out) throws IOException
+    {
+        // if the root is not a leaf then none of the leaves have been written (all are PartialLeaf)
+        // so write out the last layer of the tree by converting PartialLeaf to StaticLeaf and
+        // iterating the data once more
+        super.write(out);
+        if (root.isLeaf())
+            return;
+
+        RangeIterator<Long, Token> tokens = combinedTerm.getTokenIterator();
+        ByteBuffer blockBuffer = ByteBuffer.allocate(BLOCK_BYTES);
+        Iterator<Node> leafIterator = leftmostLeaf.levelIterator();
+        while (leafIterator.hasNext())
+        {
+            Leaf leaf = (Leaf) leafIterator.next();
+            Leaf writeableLeaf = new StaticLeaf(Iterators.limit(tokens, leaf.tokenCount()), leaf);
+            writeableLeaf.serialize(-1, blockBuffer);
+            flushBuffer(blockBuffer, out, true);
+        }
+
+    }
+
+    protected void constructTree()
+    {
+        RangeIterator<Long, Token> tokens = combinedTerm.getTokenIterator();
+
+        tokenCount = 0;
+        treeMinToken = tokens.getMinimum();
+        treeMaxToken = tokens.getMaximum();
+        numBlocks = 1;
+
+        root = new InteriorNode();
+        rightmostParent = (InteriorNode) root;
+        Leaf lastLeaf = null;
+        Long lastToken, firstToken = null;
+        int leafSize = 0;
+        while (tokens.hasNext())
+        {
+            Long token = tokens.next().get();
+            if (firstToken == null)
+                firstToken = token;
+
+            tokenCount++;
+            leafSize++;
+
+            // skip until the last token in the leaf
+            if (tokenCount % TOKENS_PER_BLOCK != 0 && token != treeMaxToken)
+                continue;
+
+            lastToken = token;
+            Leaf leaf = new PartialLeaf(firstToken, lastToken, leafSize);
+            if (lastLeaf == null) // first leaf created
+                leftmostLeaf = leaf;
+            else
+                lastLeaf.next = leaf;
+
+
+            rightmostParent.add(leaf);
+            lastLeaf = rightmostLeaf = leaf;
+            firstToken = null;
+            numBlocks++;
+            leafSize = 0;
+        }
+
+        // if the tree is really a single leaf the empty root interior
+        // node must be discarded
+        if (root.tokenCount() == 0)
+        {
+            numBlocks = 1;
+            root = new StaticLeaf(combinedTerm.getTokenIterator(), treeMinToken, treeMaxToken, tokenCount, true);
+        }
+    }
+
+    // This denotes the leaf which only has min/max and token counts
+    // but doesn't have any associated data yet, so it can't be serialized.
+    private class PartialLeaf extends Leaf
+    {
+        private final int size;
+        public PartialLeaf(Long min, Long max, int count)
+        {
+            super(min, max);
+            size = count;
+        }
+
+        public int tokenCount()
+        {
+            return size;
+        }
+
+        public void serializeData(ByteBuffer buf)
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        public boolean isSerializable()
+        {
+            return false;
+        }
+    }
+
+    // This denotes the leaf which has been filled with data and is ready to be serialized
+    private class StaticLeaf extends Leaf
+    {
+        private final Iterator<Token> tokens;
+        private final int count;
+        private final boolean isLast;
+
+        public StaticLeaf(Iterator<Token> tokens, Leaf leaf)
+        {
+            this(tokens, leaf.smallestToken(), leaf.largestToken(), leaf.tokenCount(), leaf.isLastLeaf());
+        }
+
+        public StaticLeaf(Iterator<Token> tokens, Long min, Long max, long count, boolean isLastLeaf)
+        {
+            super(min, max);
+
+            this.count = (int) count; // downcast is safe since leaf size is always < Integer.MAX_VALUE
+            this.tokens = tokens;
+            this.isLast = isLastLeaf;
+        }
+
+        public boolean isLastLeaf()
+        {
+            return isLast;
+        }
+
+        public int tokenCount()
+        {
+            return count;
+        }
+
+        public void serializeData(ByteBuffer buf)
+        {
+            while (tokens.hasNext())
+            {
+                Token entry = tokens.next();
+                createEntry(entry.get(), entry.getOffsets()).serialize(buf);
+            }
+        }
+
+        public boolean isSerializable()
+        {
+            return true;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/disk/Token.java b/src/java/org/apache/cassandra/index/sasi/disk/Token.java
new file mode 100644
index 0000000..4cd1ea3
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/disk/Token.java
@@ -0,0 +1,47 @@
+/*
+ * 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.index.sasi.disk;
+
+import com.google.common.primitives.Longs;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.index.sasi.utils.CombinedValue;
+
+import com.carrotsearch.hppc.LongSet;
+
+public abstract class Token implements CombinedValue<Long>, Iterable<DecoratedKey>
+{
+    protected final long token;
+
+    public Token(long token)
+    {
+        this.token = token;
+    }
+
+    public Long get()
+    {
+        return token;
+    }
+
+    public abstract LongSet getOffsets();
+
+    public int compareTo(CombinedValue<Long> o)
+    {
+        return Longs.compare(token, ((Token) o).token);
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java b/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java
new file mode 100644
index 0000000..c69ce00
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java
@@ -0,0 +1,523 @@
+/*
+ * 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.index.sasi.disk;
+
+import java.io.IOException;
+import java.util.*;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.index.sasi.utils.AbstractIterator;
+import org.apache.cassandra.index.sasi.utils.CombinedValue;
+import org.apache.cassandra.index.sasi.utils.MappedBuffer;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.utils.MergeIterator;
+
+import com.carrotsearch.hppc.LongOpenHashSet;
+import com.carrotsearch.hppc.LongSet;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.Iterators;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+import static org.apache.cassandra.index.sasi.disk.TokenTreeBuilder.EntryType;
+
+// Note: all of the seek-able offsets contained in TokenTree should be sizeof(long)
+// even if currently only lower int portion of them if used, because that makes
+// it possible to switch to mmap implementation which supports long positions
+// without any on-disk format changes and/or re-indexing if one day we'll have a need to.
+public class TokenTree
+{
+    private static final int LONG_BYTES = Long.SIZE / 8;
+    private static final int SHORT_BYTES = Short.SIZE / 8;
+
+    private final Descriptor descriptor;
+    private final MappedBuffer file;
+    private final long startPos;
+    private final long treeMinToken;
+    private final long treeMaxToken;
+    private final long tokenCount;
+
+    @VisibleForTesting
+    protected TokenTree(MappedBuffer tokenTree)
+    {
+        this(Descriptor.CURRENT, tokenTree);
+    }
+
+    public TokenTree(Descriptor d, MappedBuffer tokenTree)
+    {
+        descriptor = d;
+        file = tokenTree;
+        startPos = file.position();
+
+        file.position(startPos + TokenTreeBuilder.SHARED_HEADER_BYTES);
+
+        if (!validateMagic())
+            throw new IllegalArgumentException("invalid token tree");
+
+        tokenCount = file.getLong();
+        treeMinToken = file.getLong();
+        treeMaxToken = file.getLong();
+    }
+
+    public long getCount()
+    {
+        return tokenCount;
+    }
+
+    public RangeIterator<Long, Token> iterator(Function<Long, DecoratedKey> keyFetcher)
+    {
+        return new TokenTreeIterator(file.duplicate(), keyFetcher);
+    }
+
+    public OnDiskToken get(final long searchToken, Function<Long, DecoratedKey> keyFetcher)
+    {
+        seekToLeaf(searchToken, file);
+        long leafStart = file.position();
+        short leafSize = file.getShort(leafStart + 1); // skip the info byte
+
+        file.position(leafStart + TokenTreeBuilder.BLOCK_HEADER_BYTES); // skip to tokens
+        short tokenIndex = searchLeaf(searchToken, leafSize);
+
+        file.position(leafStart + TokenTreeBuilder.BLOCK_HEADER_BYTES);
+
+        OnDiskToken token = OnDiskToken.getTokenAt(file, tokenIndex, leafSize, keyFetcher);
+        return token.get().equals(searchToken) ? token : null;
+    }
+
+    private boolean validateMagic()
+    {
+        switch (descriptor.version.toString())
+        {
+            case Descriptor.VERSION_AA:
+                return true;
+            case Descriptor.VERSION_AB:
+                return TokenTreeBuilder.AB_MAGIC == file.getShort();
+            default:
+                return false;
+        }
+    }
+
+    // finds leaf that *could* contain token
+    private void seekToLeaf(long token, MappedBuffer file)
+    {
+        // this loop always seeks forward except for the first iteration
+        // where it may seek back to the root
+        long blockStart = startPos;
+        while (true)
+        {
+            file.position(blockStart);
+
+            byte info = file.get();
+            boolean isLeaf = (info & 1) == 1;
+
+            if (isLeaf)
+            {
+                file.position(blockStart);
+                break;
+            }
+
+            short tokenCount = file.getShort();
+
+            long minToken = file.getLong();
+            long maxToken = file.getLong();
+
+            long seekBase = blockStart + TokenTreeBuilder.BLOCK_HEADER_BYTES;
+            if (minToken > token)
+            {
+                // seek to beginning of child offsets to locate first child
+                file.position(seekBase + tokenCount * LONG_BYTES);
+                blockStart = (startPos + (int) file.getLong());
+            }
+            else if (maxToken < token)
+            {
+                // seek to end of child offsets to locate last child
+                file.position(seekBase + (2 * tokenCount) * LONG_BYTES);
+                blockStart = (startPos + (int) file.getLong());
+            }
+            else
+            {
+                // skip to end of block header/start of interior block tokens
+                file.position(seekBase);
+
+                short offsetIndex = searchBlock(token, tokenCount, file);
+
+                // file pointer is now at beginning of offsets
+                if (offsetIndex == tokenCount)
+                    file.position(file.position() + (offsetIndex * LONG_BYTES));
+                else
+                    file.position(file.position() + ((tokenCount - offsetIndex - 1) + offsetIndex) * LONG_BYTES);
+
+                blockStart = (startPos + (int) file.getLong());
+            }
+        }
+    }
+
+    private short searchBlock(long searchToken, short tokenCount, MappedBuffer file)
+    {
+        short offsetIndex = 0;
+        for (int i = 0; i < tokenCount; i++)
+        {
+            long readToken = file.getLong();
+            if (searchToken < readToken)
+                break;
+
+            offsetIndex++;
+        }
+
+        return offsetIndex;
+    }
+
+    private short searchLeaf(long searchToken, short tokenCount)
+    {
+        long base = file.position();
+
+        int start = 0;
+        int end = tokenCount;
+        int middle = 0;
+
+        while (start <= end)
+        {
+            middle = start + ((end - start) >> 1);
+
+            // each entry is 16 bytes wide, token is in bytes 4-11
+            long token = file.getLong(base + (middle * (2 * LONG_BYTES) + 4));
+
+            if (token == searchToken)
+                break;
+
+            if (token < searchToken)
+                start = middle + 1;
+            else
+                end = middle - 1;
+        }
+
+        return (short) middle;
+    }
+
+    public class TokenTreeIterator extends RangeIterator<Long, Token>
+    {
+        private final Function<Long, DecoratedKey> keyFetcher;
+        private final MappedBuffer file;
+
+        private long currentLeafStart;
+        private int currentTokenIndex;
+
+        private long leafMinToken;
+        private long leafMaxToken;
+        private short leafSize;
+
+        protected boolean firstIteration = true;
+        private boolean lastLeaf;
+
+        TokenTreeIterator(MappedBuffer file, Function<Long, DecoratedKey> keyFetcher)
+        {
+            super(treeMinToken, treeMaxToken, tokenCount);
+
+            this.file = file;
+            this.keyFetcher = keyFetcher;
+        }
+
+        protected Token computeNext()
+        {
+            maybeFirstIteration();
+
+            if (currentTokenIndex >= leafSize && lastLeaf)
+                return endOfData();
+
+            if (currentTokenIndex < leafSize) // tokens remaining in this leaf
+            {
+                return getTokenAt(currentTokenIndex++);
+            }
+            else // no more tokens remaining in this leaf
+            {
+                assert !lastLeaf;
+
+                seekToNextLeaf();
+                setupBlock();
+                return computeNext();
+            }
+        }
+
+        protected void performSkipTo(Long nextToken)
+        {
+            maybeFirstIteration();
+
+            if (nextToken <= leafMaxToken) // next is in this leaf block
+            {
+                searchLeaf(nextToken);
+            }
+            else // next is in a leaf block that needs to be found
+            {
+                seekToLeaf(nextToken, file);
+                setupBlock();
+                findNearest(nextToken);
+            }
+        }
+
+        private void setupBlock()
+        {
+            currentLeafStart = file.position();
+            currentTokenIndex = 0;
+
+            lastLeaf = (file.get() & (1 << TokenTreeBuilder.LAST_LEAF_SHIFT)) > 0;
+            leafSize = file.getShort();
+
+            leafMinToken = file.getLong();
+            leafMaxToken = file.getLong();
+
+            // seek to end of leaf header/start of data
+            file.position(currentLeafStart + TokenTreeBuilder.BLOCK_HEADER_BYTES);
+        }
+
+        private void findNearest(Long next)
+        {
+            if (next > leafMaxToken && !lastLeaf)
+            {
+                seekToNextLeaf();
+                setupBlock();
+                findNearest(next);
+            }
+            else if (next > leafMinToken)
+                searchLeaf(next);
+        }
+
+        private void searchLeaf(long next)
+        {
+            for (int i = currentTokenIndex; i < leafSize; i++)
+            {
+                if (compareTokenAt(currentTokenIndex, next) >= 0)
+                    break;
+
+                currentTokenIndex++;
+            }
+        }
+
+        private int compareTokenAt(int idx, long toToken)
+        {
+            return Long.compare(file.getLong(getTokenPosition(idx)), toToken);
+        }
+
+        private Token getTokenAt(int idx)
+        {
+            return OnDiskToken.getTokenAt(file, idx, leafSize, keyFetcher);
+        }
+
+        private long getTokenPosition(int idx)
+        {
+            // skip 4 byte entry header to get position pointing directly at the entry's token
+            return OnDiskToken.getEntryPosition(idx, file) + (2 * SHORT_BYTES);
+        }
+
+        private void seekToNextLeaf()
+        {
+            file.position(currentLeafStart + TokenTreeBuilder.BLOCK_BYTES);
+        }
+
+        public void close() throws IOException
+        {
+            // nothing to do here
+        }
+
+        private void maybeFirstIteration()
+        {
+            // seek to the first token only when requested for the first time,
+            // highly predictable branch and saves us a lot by not traversing the tree
+            // on creation time because it's not at all required.
+            if (!firstIteration)
+                return;
+
+            seekToLeaf(treeMinToken, file);
+            setupBlock();
+            firstIteration = false;
+        }
+    }
+
+    public static class OnDiskToken extends Token
+    {
+        private final Set<TokenInfo> info = new HashSet<>(2);
+        private final Set<DecoratedKey> loadedKeys = new TreeSet<>(DecoratedKey.comparator);
+
+        public OnDiskToken(MappedBuffer buffer, long position, short leafSize, Function<Long, DecoratedKey> keyFetcher)
+        {
+            super(buffer.getLong(position + (2 * SHORT_BYTES)));
+            info.add(new TokenInfo(buffer, position, leafSize, keyFetcher));
+        }
+
+        public void merge(CombinedValue<Long> other)
+        {
+            if (!(other instanceof Token))
+                return;
+
+            Token o = (Token) other;
+            if (token != o.token)
+                throw new IllegalArgumentException(String.format("%s != %s", token, o.token));
+
+            if (o instanceof OnDiskToken)
+            {
+                info.addAll(((OnDiskToken) other).info);
+            }
+            else
+            {
+                Iterators.addAll(loadedKeys, o.iterator());
+            }
+        }
+
+        public Iterator<DecoratedKey> iterator()
+        {
+            List<Iterator<DecoratedKey>> keys = new ArrayList<>(info.size());
+
+            for (TokenInfo i : info)
+                keys.add(i.iterator());
+
+            if (!loadedKeys.isEmpty())
+                keys.add(loadedKeys.iterator());
+
+            return MergeIterator.get(keys, DecoratedKey.comparator, new MergeIterator.Reducer<DecoratedKey, DecoratedKey>()
+            {
+                DecoratedKey reduced = null;
+
+                public boolean trivialReduceIsTrivial()
+                {
+                    return true;
+                }
+
+                public void reduce(int idx, DecoratedKey current)
+                {
+                    reduced = current;
+                }
+
+                protected DecoratedKey getReduced()
+                {
+                    return reduced;
+                }
+            });
+        }
+
+        public LongSet getOffsets()
+        {
+            LongSet offsets = new LongOpenHashSet(4);
+            for (TokenInfo i : info)
+            {
+                for (long offset : i.fetchOffsets())
+                    offsets.add(offset);
+            }
+
+            return offsets;
+        }
+
+        public static OnDiskToken getTokenAt(MappedBuffer buffer, int idx, short leafSize, Function<Long, DecoratedKey> keyFetcher)
+        {
+            return new OnDiskToken(buffer, getEntryPosition(idx, buffer), leafSize, keyFetcher);
+        }
+
+        private static long getEntryPosition(int idx, MappedBuffer file)
+        {
+            // info (4 bytes) + token (8 bytes) + offset (4 bytes) = 16 bytes
+            return file.position() + (idx * (2 * LONG_BYTES));
+        }
+    }
+
+    private static class TokenInfo
+    {
+        private final MappedBuffer buffer;
+        private final Function<Long, DecoratedKey> keyFetcher;
+
+        private final long position;
+        private final short leafSize;
+
+        public TokenInfo(MappedBuffer buffer, long position, short leafSize, Function<Long, DecoratedKey> keyFetcher)
+        {
+            this.keyFetcher = keyFetcher;
+            this.buffer = buffer;
+            this.position = position;
+            this.leafSize = leafSize;
+        }
+
+        public Iterator<DecoratedKey> iterator()
+        {
+            return new KeyIterator(keyFetcher, fetchOffsets());
+        }
+
+        public int hashCode()
+        {
+            return new HashCodeBuilder().append(keyFetcher).append(position).append(leafSize).build();
+        }
+
+        public boolean equals(Object other)
+        {
+            if (!(other instanceof TokenInfo))
+                return false;
+
+            TokenInfo o = (TokenInfo) other;
+            return keyFetcher == o.keyFetcher && position == o.position;
+        }
+
+        private long[] fetchOffsets()
+        {
+            short info = buffer.getShort(position);
+            // offset extra is unsigned short (right-most 16 bits of 48 bits allowed for an offset)
+            int offsetExtra = buffer.getShort(position + SHORT_BYTES) & 0xFFFF;
+            // is the it left-most (32-bit) base of the actual offset in the index file
+            int offsetData = buffer.getInt(position + (2 * SHORT_BYTES) + LONG_BYTES);
+
+            EntryType type = EntryType.of(info & TokenTreeBuilder.ENTRY_TYPE_MASK);
+
+            switch (type)
+            {
+                case SIMPLE:
+                    return new long[] { offsetData };
+
+                case OVERFLOW:
+                    long[] offsets = new long[offsetExtra]; // offsetShort contains count of tokens
+                    long offsetPos = (buffer.position() + (2 * (leafSize * LONG_BYTES)) + (offsetData * LONG_BYTES));
+
+                    for (int i = 0; i < offsetExtra; i++)
+                        offsets[i] = buffer.getLong(offsetPos + (i * LONG_BYTES));
+
+                    return offsets;
+
+                case FACTORED:
+                    return new long[] { (((long) offsetData) << Short.SIZE) + offsetExtra };
+
+                case PACKED:
+                    return new long[] { offsetExtra, offsetData };
+
+                default:
+                    throw new IllegalStateException("Unknown entry type: " + type);
+            }
+        }
+    }
+
+    private static class KeyIterator extends AbstractIterator<DecoratedKey>
+    {
+        private final Function<Long, DecoratedKey> keyFetcher;
+        private final long[] offsets;
+        private int index = 0;
+
+        public KeyIterator(Function<Long, DecoratedKey> keyFetcher, long[] offsets)
+        {
+            this.keyFetcher = keyFetcher;
+            this.offsets = offsets;
+        }
+
+        public DecoratedKey computeNext()
+        {
+            return index < offsets.length ? keyFetcher.apply(offsets[index++]) : endOfData();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java b/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java
new file mode 100644
index 0000000..29cecc8
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java
@@ -0,0 +1,78 @@
+/*
+ * 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.index.sasi.disk;
+
+import java.io.IOException;
+import java.util.*;
+
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.utils.Pair;
+
+import com.carrotsearch.hppc.LongSet;
+
+public interface TokenTreeBuilder extends Iterable<Pair<Long, LongSet>>
+{
+    int BLOCK_BYTES = 4096;
+    int BLOCK_HEADER_BYTES = 64;
+    int BLOCK_ENTRY_BYTES = 2 * Long.BYTES;
+    int OVERFLOW_TRAILER_BYTES = 64;
+    int OVERFLOW_ENTRY_BYTES = Long.BYTES;
+    int OVERFLOW_TRAILER_CAPACITY = OVERFLOW_TRAILER_BYTES / OVERFLOW_ENTRY_BYTES;
+    int TOKENS_PER_BLOCK = (BLOCK_BYTES - BLOCK_HEADER_BYTES - OVERFLOW_TRAILER_BYTES) / BLOCK_ENTRY_BYTES;
+    long MAX_OFFSET = (1L << 47) - 1; // 48 bits for (signed) offset
+    byte LAST_LEAF_SHIFT = 1;
+    byte SHARED_HEADER_BYTES = 19;
+    byte ENTRY_TYPE_MASK = 0x03;
+    short AB_MAGIC = 0x5A51;
+
+    // note: ordinal positions are used here, do not change order
+    enum EntryType
+    {
+        SIMPLE, FACTORED, PACKED, OVERFLOW;
+
+        public static EntryType of(int ordinal)
+        {
+            if (ordinal == SIMPLE.ordinal())
+                return SIMPLE;
+
+            if (ordinal == FACTORED.ordinal())
+                return FACTORED;
+
+            if (ordinal == PACKED.ordinal())
+                return PACKED;
+
+            if (ordinal == OVERFLOW.ordinal())
+                return OVERFLOW;
+
+            throw new IllegalArgumentException("Unknown ordinal: " + ordinal);
+        }
+    }
+
+    void add(Long token, long keyPosition);
+    void add(SortedMap<Long, LongSet> data);
+    void add(Iterator<Pair<Long, LongSet>> data);
+    void add(TokenTreeBuilder ttb);
+
+    boolean isEmpty();
+    long getTokenCount();
+
+    TokenTreeBuilder finish();
+
+    int serializedSize();
+    void write(DataOutputPlus out) throws IOException;
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/index/sasi/exceptions/TimeQuotaExceededException.java b/src/java/org/apache/cassandra/index/sasi/exceptions/TimeQuotaExceededException.java
new file mode 100644
index 0000000..af577dc
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/exceptions/TimeQuotaExceededException.java
@@ -0,0 +1,21 @@
+/*
+ * 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.index.sasi.exceptions;
+
+public class TimeQuotaExceededException extends RuntimeException
+{}
diff --git a/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java b/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java
new file mode 100644
index 0000000..e55a806
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java
@@ -0,0 +1,72 @@
+/*
+ * 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.index.sasi.memory;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.index.sasi.utils.TypeUtil;
+import org.apache.cassandra.utils.FBUtilities;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class IndexMemtable
+{
+    private static final Logger logger = LoggerFactory.getLogger(IndexMemtable.class);
+
+    private final MemIndex index;
+
+    public IndexMemtable(ColumnIndex columnIndex)
+    {
+        this.index = MemIndex.forColumn(columnIndex.keyValidator(), columnIndex);
+    }
+
+    public long index(DecoratedKey key, ByteBuffer value)
+    {
+        if (value == null || value.remaining() == 0)
+            return 0;
+
+        AbstractType<?> validator = index.columnIndex.getValidator();
+        if (!TypeUtil.isValid(value, validator))
+        {
+            int size = value.remaining();
+            if ((value = TypeUtil.tryUpcast(value, validator)) == null)
+            {
+                logger.error("Can't add column {} to index for key: {}, value size {}, validator: {}.",
+                             index.columnIndex.getColumnName(),
+                             index.columnIndex.keyValidator().getString(key.getKey()),
+                             FBUtilities.prettyPrintMemory(size),
+                             validator);
+                return 0;
+            }
+        }
+
+        return index.add(key, value);
+    }
+
+    public RangeIterator<Long, Token> search(Expression expression)
+    {
+        return index == null ? null : index.search(expression);
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/memory/KeyRangeIterator.java b/src/java/org/apache/cassandra/index/sasi/memory/KeyRangeIterator.java
new file mode 100644
index 0000000..a2f2c0e
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/memory/KeyRangeIterator.java
@@ -0,0 +1,129 @@
+/*
+ * 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.index.sasi.memory;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.utils.AbstractIterator;
+import org.apache.cassandra.index.sasi.utils.CombinedValue;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+
+import com.carrotsearch.hppc.LongOpenHashSet;
+import com.carrotsearch.hppc.LongSet;
+import com.google.common.collect.PeekingIterator;
+
+public class KeyRangeIterator extends RangeIterator<Long, Token>
+{
+    private final DKIterator iterator;
+
+    public KeyRangeIterator(ConcurrentSkipListSet<DecoratedKey> keys)
+    {
+        super((Long) keys.first().getToken().getTokenValue(), (Long) keys.last().getToken().getTokenValue(), keys.size());
+        this.iterator = new DKIterator(keys.iterator());
+    }
+
+    protected Token computeNext()
+    {
+        return iterator.hasNext() ? new DKToken(iterator.next()) : endOfData();
+    }
+
+    protected void performSkipTo(Long nextToken)
+    {
+        while (iterator.hasNext())
+        {
+            DecoratedKey key = iterator.peek();
+            if (Long.compare((long) key.getToken().getTokenValue(), nextToken) >= 0)
+                break;
+
+            // consume smaller key
+            iterator.next();
+        }
+    }
+
+    public void close() throws IOException
+    {}
+
+    private static class DKIterator extends AbstractIterator<DecoratedKey> implements PeekingIterator<DecoratedKey>
+    {
+        private final Iterator<DecoratedKey> keys;
+
+        public DKIterator(Iterator<DecoratedKey> keys)
+        {
+            this.keys = keys;
+        }
+
+        protected DecoratedKey computeNext()
+        {
+            return keys.hasNext() ? keys.next() : endOfData();
+        }
+    }
+
+    private static class DKToken extends Token
+    {
+        private final SortedSet<DecoratedKey> keys;
+
+        public DKToken(final DecoratedKey key)
+        {
+            super((long) key.getToken().getTokenValue());
+
+            keys = new TreeSet<DecoratedKey>(DecoratedKey.comparator)
+            {{
+                add(key);
+            }};
+        }
+
+        public LongSet getOffsets()
+        {
+            LongSet offsets = new LongOpenHashSet(4);
+            for (DecoratedKey key : keys)
+                offsets.add((long) key.getToken().getTokenValue());
+
+            return offsets;
+        }
+
+        public void merge(CombinedValue<Long> other)
+        {
+            if (!(other instanceof Token))
+                return;
+
+            Token o = (Token) other;
+            assert o.get().equals(token);
+
+            if (o instanceof DKToken)
+            {
+                keys.addAll(((DKToken) o).keys);
+            }
+            else
+            {
+                for (DecoratedKey key : o)
+                    keys.add(key);
+            }
+        }
+
+        public Iterator<DecoratedKey> iterator()
+        {
+            return keys.iterator();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/index/sasi/memory/MemIndex.java b/src/java/org/apache/cassandra/index/sasi/memory/MemIndex.java
new file mode 100644
index 0000000..cc1eb3f
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/memory/MemIndex.java
@@ -0,0 +1,49 @@
+/*
+ * 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.index.sasi.memory;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public abstract class MemIndex
+{
+    protected final AbstractType<?> keyValidator;
+    protected final ColumnIndex columnIndex;
+
+    protected MemIndex(AbstractType<?> keyValidator, ColumnIndex columnIndex)
+    {
+        this.keyValidator = keyValidator;
+        this.columnIndex = columnIndex;
+    }
+
+    public abstract long add(DecoratedKey key, ByteBuffer value);
+    public abstract RangeIterator<Long, Token> search(Expression expression);
+
+    public static MemIndex forColumn(AbstractType<?> keyValidator, ColumnIndex columnIndex)
+    {
+        return columnIndex.isLiteral()
+                ? new TrieMemIndex(keyValidator, columnIndex)
+                : new SkipListMemIndex(keyValidator, columnIndex);
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java b/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java
new file mode 100644
index 0000000..69b57d0
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java
@@ -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.index.sasi.memory;
+
+import java.nio.ByteBuffer;
+import java.util.*;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public class SkipListMemIndex extends MemIndex
+{
+    public static final int CSLM_OVERHEAD = 128; // average overhead of CSLM
+
+    private final ConcurrentSkipListMap<ByteBuffer, ConcurrentSkipListSet<DecoratedKey>> index;
+
+    public SkipListMemIndex(AbstractType<?> keyValidator, ColumnIndex columnIndex)
+    {
+        super(keyValidator, columnIndex);
+        index = new ConcurrentSkipListMap<>(columnIndex.getValidator());
+    }
+
+    public long add(DecoratedKey key, ByteBuffer value)
+    {
+        long overhead = CSLM_OVERHEAD; // DKs are shared
+        ConcurrentSkipListSet<DecoratedKey> keys = index.get(value);
+
+        if (keys == null)
+        {
+            ConcurrentSkipListSet<DecoratedKey> newKeys = new ConcurrentSkipListSet<>(DecoratedKey.comparator);
+            keys = index.putIfAbsent(value, newKeys);
+            if (keys == null)
+            {
+                overhead += CSLM_OVERHEAD + value.remaining();
+                keys = newKeys;
+            }
+        }
+
+        keys.add(key);
+
+        return overhead;
+    }
+
+    public RangeIterator<Long, Token> search(Expression expression)
+    {
+        ByteBuffer min = expression.lower == null ? null : expression.lower.value;
+        ByteBuffer max = expression.upper == null ? null : expression.upper.value;
+
+        SortedMap<ByteBuffer, ConcurrentSkipListSet<DecoratedKey>> search;
+
+        if (min == null && max == null)
+        {
+            throw new IllegalArgumentException();
+        }
+        if (min != null && max != null)
+        {
+            search = index.subMap(min, expression.lower.inclusive, max, expression.upper.inclusive);
+        }
+        else if (min == null)
+        {
+            search = index.headMap(max, expression.upper.inclusive);
+        }
+        else
+        {
+            search = index.tailMap(min, expression.lower.inclusive);
+        }
+
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+        search.values().stream()
+                       .filter(keys -> !keys.isEmpty())
+                       .forEach(keys -> builder.add(new KeyRangeIterator(keys)));
+
+        return builder.build();
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java b/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java
new file mode 100644
index 0000000..ca60ac5
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java
@@ -0,0 +1,283 @@
+/*
+ * 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.index.sasi.memory;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.index.sasi.plan.Expression.Op;
+import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
+import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.db.marshal.AbstractType;
+
+import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree;
+import com.googlecode.concurrenttrees.suffix.ConcurrentSuffixTree;
+import com.googlecode.concurrenttrees.radix.node.concrete.SmartArrayBasedNodeFactory;
+import com.googlecode.concurrenttrees.radix.node.Node;
+import org.apache.cassandra.utils.FBUtilities;
+
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.cassandra.index.sasi.memory.SkipListMemIndex.CSLM_OVERHEAD;
+
+public class TrieMemIndex extends MemIndex
+{
+    private static final Logger logger = LoggerFactory.getLogger(TrieMemIndex.class);
+
+    private final ConcurrentTrie index;
+
+    public TrieMemIndex(AbstractType<?> keyValidator, ColumnIndex columnIndex)
+    {
+        super(keyValidator, columnIndex);
+
+        switch (columnIndex.getMode().mode)
+        {
+            case CONTAINS:
+                index = new ConcurrentSuffixTrie(columnIndex.getDefinition());
+                break;
+
+            case PREFIX:
+                index = new ConcurrentPrefixTrie(columnIndex.getDefinition());
+                break;
+
+            default:
+                throw new IllegalStateException("Unsupported mode: " + columnIndex.getMode().mode);
+        }
+    }
+
+    public long add(DecoratedKey key, ByteBuffer value)
+    {
+        AbstractAnalyzer analyzer = columnIndex.getAnalyzer();
+        analyzer.reset(value.duplicate());
+
+        long size = 0;
+        while (analyzer.hasNext())
+        {
+            ByteBuffer term = analyzer.next();
+
+            if (term.remaining() >= OnDiskIndexBuilder.MAX_TERM_SIZE)
+            {
+                logger.info("Can't add term of column {} to index for key: {}, term size {}, max allowed size {}, use analyzed = true (if not yet set) for that column.",
+                            columnIndex.getColumnName(),
+                            keyValidator.getString(key.getKey()),
+                            FBUtilities.prettyPrintMemory(term.remaining()),
+                            FBUtilities.prettyPrintMemory(OnDiskIndexBuilder.MAX_TERM_SIZE));
+                continue;
+            }
+
+            size += index.add(columnIndex.getValidator().getString(term), key);
+        }
+
+        return size;
+    }
+
+    public RangeIterator<Long, Token> search(Expression expression)
+    {
+        return index.search(expression);
+    }
+
+    private static abstract class ConcurrentTrie
+    {
+        public static final SizeEstimatingNodeFactory NODE_FACTORY = new SizeEstimatingNodeFactory();
+
+        protected final ColumnDefinition definition;
+
+        public ConcurrentTrie(ColumnDefinition column)
+        {
+            definition = column;
+        }
+
+        public long add(String value, DecoratedKey key)
+        {
+            long overhead = CSLM_OVERHEAD;
+            ConcurrentSkipListSet<DecoratedKey> keys = get(value);
+            if (keys == null)
+            {
+                ConcurrentSkipListSet<DecoratedKey> newKeys = new ConcurrentSkipListSet<>(DecoratedKey.comparator);
+                keys = putIfAbsent(value, newKeys);
+                if (keys == null)
+                {
+                    overhead += CSLM_OVERHEAD + value.length();
+                    keys = newKeys;
+                }
+            }
+
+            keys.add(key);
+
+            // get and reset new memory size allocated by current thread
+            overhead += NODE_FACTORY.currentUpdateSize();
+            NODE_FACTORY.reset();
+
+            return overhead;
+        }
+
+        public RangeIterator<Long, Token> search(Expression expression)
+        {
+            ByteBuffer prefix = expression.lower == null ? null : expression.lower.value;
+
+            Iterable<ConcurrentSkipListSet<DecoratedKey>> search = search(expression.getOp(), definition.cellValueType().getString(prefix));
+
+            RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+            for (ConcurrentSkipListSet<DecoratedKey> keys : search)
+            {
+                if (!keys.isEmpty())
+                    builder.add(new KeyRangeIterator(keys));
+            }
+
+            return builder.build();
+        }
+
+        protected abstract ConcurrentSkipListSet<DecoratedKey> get(String value);
+        protected abstract Iterable<ConcurrentSkipListSet<DecoratedKey>> search(Op operator, String value);
+        protected abstract ConcurrentSkipListSet<DecoratedKey> putIfAbsent(String value, ConcurrentSkipListSet<DecoratedKey> key);
+    }
+
+    protected static class ConcurrentPrefixTrie extends ConcurrentTrie
+    {
+        private final ConcurrentRadixTree<ConcurrentSkipListSet<DecoratedKey>> trie;
+
+        private ConcurrentPrefixTrie(ColumnDefinition column)
+        {
+            super(column);
+            trie = new ConcurrentRadixTree<>(NODE_FACTORY);
+        }
+
+        public ConcurrentSkipListSet<DecoratedKey> get(String value)
+        {
+            return trie.getValueForExactKey(value);
+        }
+
+        public ConcurrentSkipListSet<DecoratedKey> putIfAbsent(String value, ConcurrentSkipListSet<DecoratedKey> newKeys)
+        {
+            return trie.putIfAbsent(value, newKeys);
+        }
+
+        public Iterable<ConcurrentSkipListSet<DecoratedKey>> search(Op operator, String value)
+        {
+            switch (operator)
+            {
+                case EQ:
+                case MATCH:
+                    ConcurrentSkipListSet<DecoratedKey> keys = trie.getValueForExactKey(value);
+                    return keys == null ? Collections.emptyList() : Collections.singletonList(keys);
+
+                case PREFIX:
+                    return trie.getValuesForKeysStartingWith(value);
+
+                default:
+                    throw new UnsupportedOperationException(String.format("operation %s is not supported.", operator));
+            }
+        }
+    }
+
+    protected static class ConcurrentSuffixTrie extends ConcurrentTrie
+    {
+        private final ConcurrentSuffixTree<ConcurrentSkipListSet<DecoratedKey>> trie;
+
+        private ConcurrentSuffixTrie(ColumnDefinition column)
+        {
+            super(column);
+            trie = new ConcurrentSuffixTree<>(NODE_FACTORY);
+        }
+
+        public ConcurrentSkipListSet<DecoratedKey> get(String value)
+        {
+            return trie.getValueForExactKey(value);
+        }
+
+        public ConcurrentSkipListSet<DecoratedKey> putIfAbsent(String value, ConcurrentSkipListSet<DecoratedKey> newKeys)
+        {
+            return trie.putIfAbsent(value, newKeys);
+        }
+
+        public Iterable<ConcurrentSkipListSet<DecoratedKey>> search(Op operator, String value)
+        {
+            switch (operator)
+            {
+                case EQ:
+                case MATCH:
+                    ConcurrentSkipListSet<DecoratedKey> keys = trie.getValueForExactKey(value);
+                    return keys == null ? Collections.emptyList() : Collections.singletonList(keys);
+
+                case SUFFIX:
+                    return trie.getValuesForKeysEndingWith(value);
+
+                case PREFIX:
+                case CONTAINS:
+                    return trie.getValuesForKeysContaining(value);
+
+                default:
+                    throw new UnsupportedOperationException(String.format("operation %s is not supported.", operator));
+            }
+        }
+    }
+
+    // This relies on the fact that all of the tree updates are done under exclusive write lock,
+    // method would overestimate in certain circumstances e.g. when nodes are replaced in place,
+    // but it's still better comparing to underestimate since it gives more breathing room for other memory users.
+    private static class SizeEstimatingNodeFactory extends SmartArrayBasedNodeFactory
+    {
+        private final ThreadLocal<Long> updateSize = ThreadLocal.withInitial(() -> 0L);
+
+        public Node createNode(CharSequence edgeCharacters, Object value, List<Node> childNodes, boolean isRoot)
+        {
+            Node node = super.createNode(edgeCharacters, value, childNodes, isRoot);
+            updateSize.set(updateSize.get() + measure(node));
+            return node;
+        }
+
+        public long currentUpdateSize()
+        {
+            return updateSize.get();
+        }
+
+        public void reset()
+        {
+            updateSize.set(0L);
+        }
+
+        private long measure(Node node)
+        {
+            // node with max overhead is CharArrayNodeLeafWithValue = 24B
+            long overhead = 24;
+
+            // array of chars (2 bytes) + CharSequence overhead
+            overhead += 24 + node.getIncomingEdge().length() * 2;
+
+            if (node.getOutgoingEdges() != null)
+            {
+                // 16 bytes for AtomicReferenceArray
+                overhead += 16;
+                overhead += 24 * node.getOutgoingEdges().size();
+            }
+
+            return overhead;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/plan/Expression.java b/src/java/org/apache/cassandra/index/sasi/plan/Expression.java
new file mode 100644
index 0000000..fba7f34
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/plan/Expression.java
@@ -0,0 +1,415 @@
+/*
+ * 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.index.sasi.plan;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndex;
+import org.apache.cassandra.index.sasi.utils.TypeUtil;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.FBUtilities;
+
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Iterators;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Expression
+{
+    private static final Logger logger = LoggerFactory.getLogger(Expression.class);
+
+    public enum Op
+    {
+        EQ, MATCH, PREFIX, SUFFIX, CONTAINS, NOT_EQ, RANGE;
+
+        public static Op valueOf(Operator operator)
+        {
+            switch (operator)
+            {
+                case EQ:
+                    return EQ;
+
+                case NEQ:
+                    return NOT_EQ;
+
+                case LT:
+                case GT:
+                case LTE:
+                case GTE:
+                    return RANGE;
+
+                case LIKE_PREFIX:
+                    return PREFIX;
+
+                case LIKE_SUFFIX:
+                    return SUFFIX;
+
+                case LIKE_CONTAINS:
+                    return CONTAINS;
+
+                case LIKE_MATCHES:
+                    return MATCH;
+
+                default:
+                    throw new IllegalArgumentException("unknown operator: " + operator);
+            }
+        }
+    }
+
+    private final QueryController controller;
+
+    public final AbstractAnalyzer analyzer;
+
+    public final ColumnIndex index;
+    public final AbstractType<?> validator;
+    public final boolean isLiteral;
+
+    @VisibleForTesting
+    protected Op operation;
+
+    public Bound lower, upper;
+    public List<ByteBuffer> exclusions = new ArrayList<>();
+
+    public Expression(Expression other)
+    {
+        this(other.controller, other.index);
+        operation = other.operation;
+    }
+
+    public Expression(QueryController controller, ColumnIndex columnIndex)
+    {
+        this.controller = controller;
+        this.index = columnIndex;
+        this.analyzer = columnIndex.getAnalyzer();
+        this.validator = columnIndex.getValidator();
+        this.isLiteral = columnIndex.isLiteral();
+    }
+
+    @VisibleForTesting
+    public Expression(String name, AbstractType<?> validator)
+    {
+        this(null, new ColumnIndex(UTF8Type.instance, ColumnDefinition.regularDef("sasi", "internal", name, validator), null));
+    }
+
+    public Expression setLower(Bound newLower)
+    {
+        lower = newLower == null ? null : new Bound(newLower.value, newLower.inclusive);
+        return this;
+    }
+
+    public Expression setUpper(Bound newUpper)
+    {
+        upper = newUpper == null ? null : new Bound(newUpper.value, newUpper.inclusive);
+        return this;
+    }
+
+    public Expression setOp(Op op)
+    {
+        this.operation = op;
+        return this;
+    }
+
+    public Expression add(Operator op, ByteBuffer value)
+    {
+        boolean lowerInclusive = false, upperInclusive = false;
+        switch (op)
+        {
+            case LIKE_PREFIX:
+            case LIKE_SUFFIX:
+            case LIKE_CONTAINS:
+            case LIKE_MATCHES:
+            case EQ:
+                lower = new Bound(value, true);
+                upper = lower;
+                operation = Op.valueOf(op);
+                break;
+
+            case NEQ:
+                // index expressions are priority sorted
+                // and NOT_EQ is the lowest priority, which means that operation type
+                // is always going to be set before reaching it in case of RANGE or EQ.
+                if (operation == null)
+                {
+                    operation = Op.NOT_EQ;
+                    lower = new Bound(value, true);
+                    upper = lower;
+                }
+                else
+                    exclusions.add(value);
+                break;
+
+            case LTE:
+                if (index.getDefinition().isReversedType())
+                    lowerInclusive = true;
+                else
+                    upperInclusive = true;
+            case LT:
+                operation = Op.RANGE;
+                if (index.getDefinition().isReversedType())
+                    lower = new Bound(value, lowerInclusive);
+                else
+                    upper = new Bound(value, upperInclusive);
+                break;
+
+            case GTE:
+                if (index.getDefinition().isReversedType())
+                    upperInclusive = true;
+                else
+                    lowerInclusive = true;
+            case GT:
+                operation = Op.RANGE;
+                if (index.getDefinition().isReversedType())
+                    upper = new Bound(value, upperInclusive);
+                else
+                    lower = new Bound(value, lowerInclusive);
+
+                break;
+        }
+
+        return this;
+    }
+
+    public Expression addExclusion(ByteBuffer value)
+    {
+        exclusions.add(value);
+        return this;
+    }
+
+    public boolean isSatisfiedBy(ByteBuffer value)
+    {
+        if (!TypeUtil.isValid(value, validator))
+        {
+            int size = value.remaining();
+            if ((value = TypeUtil.tryUpcast(value, validator)) == null)
+            {
+                logger.error("Can't cast value for {} to size accepted by {}, value size is {}.",
+                             index.getColumnName(),
+                             validator,
+                             FBUtilities.prettyPrintMemory(size));
+                return false;
+            }
+        }
+
+        if (lower != null)
+        {
+            // suffix check
+            if (isLiteral)
+            {
+                if (!validateStringValue(value, lower.value))
+                    return false;
+            }
+            else
+            {
+                // range or (not-)equals - (mainly) for numeric values
+                int cmp = validator.compare(lower.value, value);
+
+                // in case of (NOT_)EQ lower == upper
+                if (operation == Op.EQ || operation == Op.NOT_EQ)
+                    return cmp == 0;
+
+                if (cmp > 0 || (cmp == 0 && !lower.inclusive))
+                    return false;
+            }
+        }
+
+        if (upper != null && lower != upper)
+        {
+            // string (prefix or suffix) check
+            if (isLiteral)
+            {
+                if (!validateStringValue(value, upper.value))
+                    return false;
+            }
+            else
+            {
+                // range - mainly for numeric values
+                int cmp = validator.compare(upper.value, value);
+                if (cmp < 0 || (cmp == 0 && !upper.inclusive))
+                    return false;
+            }
+        }
+
+        // as a last step let's check exclusions for the given field,
+        // this covers EQ/RANGE with exclusions.
+        for (ByteBuffer term : exclusions)
+        {
+            if (isLiteral && validateStringValue(value, term))
+                return false;
+            else if (validator.compare(term, value) == 0)
+                return false;
+        }
+
+        return true;
+    }
+
+    private boolean validateStringValue(ByteBuffer columnValue, ByteBuffer requestedValue)
+    {
+        analyzer.reset(columnValue.duplicate());
+        while (analyzer.hasNext())
+        {
+            ByteBuffer term = analyzer.next();
+
+            boolean isMatch = false;
+            switch (operation)
+            {
+                case EQ:
+                case MATCH:
+                // Operation.isSatisfiedBy handles conclusion on !=,
+                // here we just need to make sure that term matched it
+                case NOT_EQ:
+                    isMatch = validator.compare(term, requestedValue) == 0;
+                    break;
+
+                case PREFIX:
+                    isMatch = ByteBufferUtil.startsWith(term, requestedValue);
+                    break;
+
+                case SUFFIX:
+                    isMatch = ByteBufferUtil.endsWith(term, requestedValue);
+                    break;
+
+                case CONTAINS:
+                    isMatch = ByteBufferUtil.contains(term, requestedValue);
+                    break;
+            }
+
+            if (isMatch)
+                return true;
+        }
+
+        return false;
+    }
+
+    public Op getOp()
+    {
+        return operation;
+    }
+
+    public void checkpoint()
+    {
+        if (controller == null)
+            return;
+
+        controller.checkpoint();
+    }
+
+    public boolean hasLower()
+    {
+        return lower != null;
+    }
+
+    public boolean hasUpper()
+    {
+        return upper != null;
+    }
+
+    public boolean isLowerSatisfiedBy(OnDiskIndex.DataTerm term)
+    {
+        if (!hasLower())
+            return true;
+
+        int cmp = term.compareTo(validator, lower.value, operation == Op.RANGE && !isLiteral);
+        return cmp > 0 || cmp == 0 && lower.inclusive;
+    }
+
+    public boolean isUpperSatisfiedBy(OnDiskIndex.DataTerm term)
+    {
+        if (!hasUpper())
+            return true;
+
+        int cmp = term.compareTo(validator, upper.value, operation == Op.RANGE && !isLiteral);
+        return cmp < 0 || cmp == 0 && upper.inclusive;
+    }
+
+    public boolean isIndexed()
+    {
+        return index.isIndexed();
+    }
+
+    public String toString()
+    {
+        return String.format("Expression{name: %s, op: %s, lower: (%s, %s), upper: (%s, %s), exclusions: %s}",
+                             index.getColumnName(),
+                             operation,
+                             lower == null ? "null" : validator.getString(lower.value),
+                             lower != null && lower.inclusive,
+                             upper == null ? "null" : validator.getString(upper.value),
+                             upper != null && upper.inclusive,
+                             Iterators.toString(Iterators.transform(exclusions.iterator(), validator::getString)));
+    }
+
+    public int hashCode()
+    {
+        return new HashCodeBuilder().append(index.getColumnName())
+                                    .append(operation)
+                                    .append(validator)
+                                    .append(lower).append(upper)
+                                    .append(exclusions).build();
+    }
+
+    public boolean equals(Object other)
+    {
+        if (!(other instanceof Expression))
+            return false;
+
+        if (this == other)
+            return true;
+
+        Expression o = (Expression) other;
+
+        return Objects.equals(index.getColumnName(), o.index.getColumnName())
+                && validator.equals(o.validator)
+                && operation == o.operation
+                && Objects.equals(lower, o.lower)
+                && Objects.equals(upper, o.upper)
+                && exclusions.equals(o.exclusions);
+    }
+
+    public static class Bound
+    {
+        public final ByteBuffer value;
+        public final boolean inclusive;
+
+        public Bound(ByteBuffer value, boolean inclusive)
+        {
+            this.value = value;
+            this.inclusive = inclusive;
+        }
+
+        public boolean equals(Object other)
+        {
+            if (!(other instanceof Bound))
+                return false;
+
+            Bound o = (Bound) other;
+            return value.equals(o.value) && inclusive == o.inclusive;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/plan/Operation.java b/src/java/org/apache/cassandra/index/sasi/plan/Operation.java
new file mode 100644
index 0000000..aaa3068
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/plan/Operation.java
@@ -0,0 +1,505 @@
+/*
+ * 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.index.sasi.plan;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.*;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.ColumnDefinition.Kind;
+import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.Unfiltered;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.plan.Expression.Op;
+import org.apache.cassandra.index.sasi.utils.RangeIntersectionIterator;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.*;
+import org.apache.cassandra.utils.FBUtilities;
+
+@SuppressWarnings("resource")
+public class Operation extends RangeIterator<Long, Token>
+{
+    public enum OperationType
+    {
+        AND, OR;
+
+        public boolean apply(boolean a, boolean b)
+        {
+            switch (this)
+            {
+                case OR:
+                    return a | b;
+
+                case AND:
+                    return a & b;
+
+                default:
+                    throw new AssertionError();
+            }
+        }
+    }
+
+    private final QueryController controller;
+
+    protected final OperationType op;
+    protected final ListMultimap<ColumnDefinition, Expression> expressions;
+    protected final RangeIterator<Long, Token> range;
+
+    protected Operation left, right;
+
+    private Operation(OperationType operation,
+                      QueryController controller,
+                      ListMultimap<ColumnDefinition, Expression> expressions,
+                      RangeIterator<Long, Token> range,
+                      Operation left, Operation right)
+    {
+        super(range);
+
+        this.op = operation;
+        this.controller = controller;
+        this.expressions = expressions;
+        this.range = range;
+
+        this.left = left;
+        this.right = right;
+    }
+
+    /**
+     * Recursive "satisfies" checks based on operation
+     * and data from the lower level members using depth-first search
+     * and bubbling the results back to the top level caller.
+     *
+     * Most of the work here is done by {@link #localSatisfiedBy(Unfiltered, Row, boolean)}
+     * see it's comment for details, if there are no local expressions
+     * assigned to Operation it will call satisfiedBy(Row) on it's children.
+     *
+     * Query: first_name = X AND (last_name = Y OR address = XYZ AND street = IL AND city = C) OR (state = 'CA' AND country = 'US')
+     * Row: key1: (first_name: X, last_name: Z, address: XYZ, street: IL, city: C, state: NY, country:US)
+     *
+     * #1                       OR
+     *                        /    \
+     * #2       (first_name) AND   AND (state, country)
+     *                          \
+     * #3            (last_name) OR
+     *                             \
+     * #4                          AND (address, street, city)
+     *
+     *
+     * Evaluation of the key1 is top-down depth-first search:
+     *
+     * --- going down ---
+     * Level #1 is evaluated, OR expression has to pull results from it's children which are at level #2 and OR them together,
+     * Level #2 AND (state, country) could be be evaluated right away, AND (first_name) refers to it's "right" child from level #3
+     * Level #3 OR (last_name) requests results from level #4
+     * Level #4 AND (address, street, city) does logical AND between it's 3 fields, returns result back to level #3.
+     * --- bubbling up ---
+     * Level #3 computes OR between AND (address, street, city) result and it's "last_name" expression
+     * Level #2 computes AND between "first_name" and result of level #3, AND (state, country) which is already computed
+     * Level #1 does OR between results of AND (first_name) and AND (state, country) and returns final result.
+     *
+     * @param currentCluster The row cluster to check.
+     * @param staticRow The static row associated with current cluster.
+     * @param allowMissingColumns allow columns value to be null.
+     * @return true if give Row satisfied all of the expressions in the tree,
+     *         false otherwise.
+     */
+    public boolean satisfiedBy(Unfiltered currentCluster, Row staticRow, boolean allowMissingColumns)
+    {
+        boolean sideL, sideR;
+
+        if (expressions == null || expressions.isEmpty())
+        {
+            sideL =  left != null &&  left.satisfiedBy(currentCluster, staticRow, allowMissingColumns);
+            sideR = right != null && right.satisfiedBy(currentCluster, staticRow, allowMissingColumns);
+
+            // one of the expressions was skipped
+            // because it had no indexes attached
+            if (left == null)
+                return sideR;
+        }
+        else
+        {
+            sideL = localSatisfiedBy(currentCluster, staticRow, allowMissingColumns);
+
+            // if there is no right it means that this expression
+            // is last in the sequence, we can just return result from local expressions
+            if (right == null)
+                return sideL;
+
+            sideR = right.satisfiedBy(currentCluster, staticRow, allowMissingColumns);
+        }
+
+
+        return op.apply(sideL, sideR);
+    }
+
+    /**
+     * Check every expression in the analyzed list to figure out if the
+     * columns in the give row match all of the based on the operation
+     * set to the current operation node.
+     *
+     * The algorithm is as follows: for every given expression from analyzed
+     * list get corresponding column from the Row:
+     *   - apply {@link Expression#isSatisfiedBy(ByteBuffer)}
+     *     method to figure out if it's satisfied;
+     *   - apply logical operation between boolean accumulator and current boolean result;
+     *   - if result == false and node's operation is AND return right away;
+     *
+     * After all of the expressions have been evaluated return resulting accumulator variable.
+     *
+     * Example:
+     *
+     * Operation = (op: AND, columns: [first_name = p, 5 < age < 7, last_name: y])
+     * Row = (first_name: pavel, last_name: y, age: 6, timestamp: 15)
+     *
+     * #1 get "first_name" = p (expressions)
+     *      - row-get "first_name"                      => "pavel"
+     *      - compare "pavel" against "p"               => true (current)
+     *      - set accumulator current                   => true (because this is expression #1)
+     *
+     * #2 get "last_name" = y (expressions)
+     *      - row-get "last_name"                       => "y"
+     *      - compare "y" against "y"                   => true (current)
+     *      - set accumulator to accumulator & current  => true
+     *
+     * #3 get 5 < "age" < 7 (expressions)
+     *      - row-get "age"                             => "6"
+     *      - compare 5 < 6 < 7                         => true (current)
+     *      - set accumulator to accumulator & current  => true
+     *
+     * #4 return accumulator => true (row satisfied all of the conditions)
+     *
+     * @param currentCluster The row cluster to check.
+     * @param staticRow The static row associated with current cluster.
+     * @param allowMissingColumns allow columns value to be null.
+     * @return true if give Row satisfied all of the analyzed expressions,
+     *         false otherwise.
+     */
+    private boolean localSatisfiedBy(Unfiltered currentCluster, Row staticRow, boolean allowMissingColumns)
+    {
+        if (currentCluster == null || !currentCluster.isRow())
+            return false;
+
+        final int now = FBUtilities.nowInSeconds();
+        boolean result = false;
+        int idx = 0;
+
+        for (ColumnDefinition column : expressions.keySet())
+        {
+            if (column.kind == Kind.PARTITION_KEY)
+                continue;
+
+            ByteBuffer value = ColumnIndex.getValueOf(column, column.kind == Kind.STATIC ? staticRow : (Row) currentCluster, now);
+            boolean isMissingColumn = value == null;
+
+            if (!allowMissingColumns && isMissingColumn)
+                throw new IllegalStateException("All indexed columns should be included into the column slice, missing: " + column);
+
+            boolean isMatch = false;
+            // If there is a column with multiple expressions that effectively means an OR
+            // e.g. comment = 'x y z' could be split into 'comment' EQ 'x', 'comment' EQ 'y', 'comment' EQ 'z'
+            // by analyzer, in situation like that we only need to check if at least one of expressions matches,
+            // and there is no hit on the NOT_EQ (if any) which are always at the end of the filter list.
+            // Loop always starts from the end of the list, which makes it possible to break after the last
+            // NOT_EQ condition on first EQ/RANGE condition satisfied, instead of checking every
+            // single expression in the column filter list.
+            List<Expression> filters = expressions.get(column);
+            for (int i = filters.size() - 1; i >= 0; i--)
+            {
+                Expression expression = filters.get(i);
+                isMatch = !isMissingColumn && expression.isSatisfiedBy(value);
+                if (expression.getOp() == Op.NOT_EQ)
+                {
+                    // since this is NOT_EQ operation we have to
+                    // inverse match flag (to check against other expressions),
+                    // and break in case of negative inverse because that means
+                    // that it's a positive hit on the not-eq clause.
+                    isMatch = !isMatch;
+                    if (!isMatch)
+                        break;
+                } // if it was a match on EQ/RANGE or column is missing
+                else if (isMatch || isMissingColumn)
+                    break;
+            }
+
+            if (idx++ == 0)
+            {
+                result = isMatch;
+                continue;
+            }
+
+            result = op.apply(result, isMatch);
+
+            // exit early because we already got a single false
+            if (op == OperationType.AND && !result)
+                return false;
+        }
+
+        return idx == 0 || result;
+    }
+
+    @VisibleForTesting
+    protected static ListMultimap<ColumnDefinition, Expression> analyzeGroup(QueryController controller,
+                                                                             OperationType op,
+                                                                             List<RowFilter.Expression> expressions)
+    {
+        ListMultimap<ColumnDefinition, Expression> analyzed = ArrayListMultimap.create();
+
+        // sort all of the expressions in the operation by name and priority of the logical operator
+        // this gives us an efficient way to handle inequality and combining into ranges without extra processing
+        // and converting expressions from one type to another.
+        Collections.sort(expressions, (a, b) -> {
+            int cmp = a.column().compareTo(b.column());
+            return cmp == 0 ? -Integer.compare(getPriority(a.operator()), getPriority(b.operator())) : cmp;
+        });
+
+        for (final RowFilter.Expression e : expressions)
+        {
+            ColumnIndex columnIndex = controller.getIndex(e);
+            List<Expression> perColumn = analyzed.get(e.column());
+
+            if (columnIndex == null)
+                columnIndex = new ColumnIndex(controller.getKeyValidator(), e.column(), null);
+
+            AbstractAnalyzer analyzer = columnIndex.getAnalyzer();
+            analyzer.reset(e.getIndexValue().duplicate());
+
+            // EQ/LIKE_*/NOT_EQ can have multiple expressions e.g. text = "Hello World",
+            // becomes text = "Hello" OR text = "World" because "space" is always interpreted as a split point (by analyzer),
+            // NOT_EQ is made an independent expression only in case of pre-existing multiple EQ expressions, or
+            // if there is no EQ operations and NOT_EQ is met or a single NOT_EQ expression present,
+            // in such case we know exactly that there would be no more EQ/RANGE expressions for given column
+            // since NOT_EQ has the lowest priority.
+            boolean isMultiExpression = false;
+            switch (e.operator())
+            {
+                case EQ:
+                    isMultiExpression = false;
+                    break;
+
+                case LIKE_PREFIX:
+                case LIKE_SUFFIX:
+                case LIKE_CONTAINS:
+                case LIKE_MATCHES:
+                    isMultiExpression = true;
+                    break;
+
+                case NEQ:
+                    isMultiExpression = (perColumn.size() == 0 || perColumn.size() > 1
+                                     || (perColumn.size() == 1 && perColumn.get(0).getOp() == Op.NOT_EQ));
+                    break;
+            }
+
+            if (isMultiExpression)
+            {
+                while (analyzer.hasNext())
+                {
+                    final ByteBuffer token = analyzer.next();
+                    perColumn.add(new Expression(controller, columnIndex).add(e.operator(), token));
+                }
+            }
+            else
+            // "range" or not-equals operator, combines both bounds together into the single expression,
+            // iff operation of the group is AND, otherwise we are forced to create separate expressions,
+            // not-equals is combined with the range iff operator is AND.
+            {
+                Expression range;
+                if (perColumn.size() == 0 || op != OperationType.AND)
+                    perColumn.add((range = new Expression(controller, columnIndex)));
+                else
+                    range = Iterables.getLast(perColumn);
+
+                while (analyzer.hasNext())
+                    range.add(e.operator(), analyzer.next());
+            }
+        }
+
+        return analyzed;
+    }
+
+    private static int getPriority(Operator op)
+    {
+        switch (op)
+        {
+            case EQ:
+                return 5;
+
+            case LIKE_PREFIX:
+            case LIKE_SUFFIX:
+            case LIKE_CONTAINS:
+            case LIKE_MATCHES:
+                return 4;
+
+            case GTE:
+            case GT:
+                return 3;
+
+            case LTE:
+            case LT:
+                return 2;
+
+            case NEQ:
+                return 1;
+
+            default:
+                return 0;
+        }
+    }
+
+    protected Token computeNext()
+    {
+        return range != null && range.hasNext() ? range.next() : endOfData();
+    }
+
+    protected void performSkipTo(Long nextToken)
+    {
+        if (range != null)
+            range.skipTo(nextToken);
+    }
+
+    public void close() throws IOException
+    {
+        controller.releaseIndexes(this);
+    }
+
+    public static class Builder
+    {
+        private final QueryController controller;
+
+        protected final OperationType op;
+        protected final List<RowFilter.Expression> expressions;
+
+        protected Builder left, right;
+
+        public Builder(OperationType operation, QueryController controller, RowFilter.Expression... columns)
+        {
+            this.op = operation;
+            this.controller = controller;
+            this.expressions = new ArrayList<>();
+            Collections.addAll(expressions, columns);
+        }
+
+        public Builder setRight(Builder operation)
+        {
+            this.right = operation;
+            return this;
+        }
+
+        public Builder setLeft(Builder operation)
+        {
+            this.left = operation;
+            return this;
+        }
+
+        public void add(RowFilter.Expression e)
+        {
+            expressions.add(e);
+        }
+
+        public void add(Collection<RowFilter.Expression> newExpressions)
+        {
+            if (expressions != null)
+                expressions.addAll(newExpressions);
+        }
+
+        public Operation complete()
+        {
+            if (!expressions.isEmpty())
+            {
+                ListMultimap<ColumnDefinition, Expression> analyzedExpressions = analyzeGroup(controller, op, expressions);
+                RangeIterator.Builder<Long, Token> range = controller.getIndexes(op, analyzedExpressions.values());
+
+                Operation rightOp = null;
+                if (right != null)
+                {
+                    rightOp = right.complete();
+                    range.add(rightOp);
+                }
+
+                return new Operation(op, controller, analyzedExpressions, range.build(), null, rightOp);
+            }
+            else
+            {
+                Operation leftOp = null, rightOp = null;
+                boolean leftIndexes = false, rightIndexes = false;
+
+                if (left != null)
+                {
+                    leftOp = left.complete();
+                    leftIndexes = leftOp != null && leftOp.range != null;
+                }
+
+                if (right != null)
+                {
+                    rightOp = right.complete();
+                    rightIndexes = rightOp != null && rightOp.range != null;
+                }
+
+                RangeIterator<Long, Token> join;
+                /**
+                 * Operation should allow one of it's sub-trees to wrap no indexes, that is related  to the fact that we
+                 * have to accept defined-but-not-indexed columns as well as key range as IndexExpressions.
+                 *
+                 * Two cases are possible:
+                 *
+                 * only left child produced indexed iterators, that could happen when there are two columns
+                 * or key range on the right:
+                 *
+                 *                AND
+                 *              /     \
+                 *            OR       \
+                 *           /   \     AND
+                 *          a     b   /   \
+                 *                  key   key
+                 *
+                 * only right child produced indexed iterators:
+                 *
+                 *               AND
+                 *              /    \
+                 *            AND     a
+                 *           /   \
+                 *         key  key
+                 */
+                if (leftIndexes && !rightIndexes)
+                    join = leftOp;
+                else if (!leftIndexes && rightIndexes)
+                    join = rightOp;
+                else if (leftIndexes)
+                {
+                    RangeIterator.Builder<Long, Token> builder = op == OperationType.OR
+                                                ? RangeUnionIterator.<Long, Token>builder()
+                                                : RangeIntersectionIterator.<Long, Token>builder();
+
+                    join = builder.add(leftOp).add(rightOp).build();
+                }
+                else
+                    throw new AssertionError("both sub-trees have 0 indexes.");
+
+                return new Operation(op, controller, null, join, leftOp, rightOp);
+            }
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java b/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java
new file mode 100644
index 0000000..22fca68
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java
@@ -0,0 +1,253 @@
+/*
+ * 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.index.sasi.plan;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.collect.Sets;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.filter.DataLimits;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.index.sasi.SASIIndex;
+import org.apache.cassandra.index.sasi.SSTableIndex;
+import org.apache.cassandra.index.sasi.TermIterator;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.conf.view.View;
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.exceptions.TimeQuotaExceededException;
+import org.apache.cassandra.index.sasi.plan.Operation.OperationType;
+import org.apache.cassandra.index.sasi.utils.RangeIntersectionIterator;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Pair;
+
+public class QueryController
+{
+    private final long executionQuota;
+    private final long executionStart;
+
+    private final ColumnFamilyStore cfs;
+    private final PartitionRangeReadCommand command;
+    private final DataRange range;
+    private final Map<Collection<Expression>, List<RangeIterator<Long, Token>>> resources = new HashMap<>();
+
+    public QueryController(ColumnFamilyStore cfs, PartitionRangeReadCommand command, long timeQuotaMs)
+    {
+        this.cfs = cfs;
+        this.command = command;
+        this.range = command.dataRange();
+        this.executionQuota = TimeUnit.MILLISECONDS.toNanos(timeQuotaMs);
+        this.executionStart = System.nanoTime();
+    }
+
+    public boolean isForThrift()
+    {
+        return command.isForThrift();
+    }
+
+    public CFMetaData metadata()
+    {
+        return command.metadata();
+    }
+
+    public Collection<RowFilter.Expression> getExpressions()
+    {
+        return command.rowFilter().getExpressions();
+    }
+
+    public DataRange dataRange()
+    {
+        return command.dataRange();
+    }
+
+    public AbstractType<?> getKeyValidator()
+    {
+        return cfs.metadata.getKeyValidator();
+    }
+
+    public ColumnIndex getIndex(RowFilter.Expression expression)
+    {
+        Optional<Index> index = cfs.indexManager.getBestIndexFor(expression);
+        return index.isPresent() ? ((SASIIndex) index.get()).getIndex() : null;
+    }
+
+
+    public UnfilteredRowIterator getPartition(DecoratedKey key, ReadExecutionController executionController)
+    {
+        if (key == null)
+            throw new NullPointerException();
+        try
+        {
+            SinglePartitionReadCommand partition = SinglePartitionReadCommand.create(command.isForThrift(),
+                                                                                     cfs.metadata,
+                                                                                     command.nowInSec(),
+                                                                                     command.columnFilter(),
+                                                                                     command.rowFilter().withoutExpressions(),
+                                                                                     DataLimits.NONE,
+                                                                                     key,
+                                                                                     command.clusteringIndexFilter(key));
+
+            return partition.queryMemtableAndDisk(cfs, executionController);
+        }
+        finally
+        {
+            checkpoint();
+        }
+    }
+
+    /**
+     * Build a range iterator from the given list of expressions by applying given operation (OR/AND).
+     * Building of such iterator involves index search, results of which are persisted in the internal resources list
+     * and can be released later via {@link QueryController#releaseIndexes(Operation)}.
+     *
+     * @param op The operation type to coalesce expressions with.
+     * @param expressions The expressions to build range iterator from (expressions with not results are ignored).
+     *
+     * @return The range builder based on given expressions and operation type.
+     */
+    public RangeIterator.Builder<Long, Token> getIndexes(OperationType op, Collection<Expression> expressions)
+    {
+        if (resources.containsKey(expressions))
+            throw new IllegalArgumentException("Can't process the same expressions multiple times.");
+
+        RangeIterator.Builder<Long, Token> builder = op == OperationType.OR
+                                                ? RangeUnionIterator.<Long, Token>builder()
+                                                : RangeIntersectionIterator.<Long, Token>builder();
+
+        List<RangeIterator<Long, Token>> perIndexUnions = new ArrayList<>();
+
+        for (Map.Entry<Expression, Set<SSTableIndex>> e : getView(op, expressions).entrySet())
+        {
+            @SuppressWarnings("resource") // RangeIterators are closed by releaseIndexes
+            RangeIterator<Long, Token> index = TermIterator.build(e.getKey(), e.getValue());
+
+            builder.add(index);
+            perIndexUnions.add(index);
+        }
+
+        resources.put(expressions, perIndexUnions);
+        return builder;
+    }
+
+    public void checkpoint()
+    {
+        if ((System.nanoTime() - executionStart) >= executionQuota)
+            throw new TimeQuotaExceededException();
+    }
+
+    public void releaseIndexes(Operation operation)
+    {
+        if (operation.expressions != null)
+            releaseIndexes(resources.remove(operation.expressions.values()));
+    }
+
+    private void releaseIndexes(List<RangeIterator<Long, Token>> indexes)
+    {
+        if (indexes == null)
+            return;
+
+        indexes.forEach(FileUtils::closeQuietly);
+    }
+
+    public void finish()
+    {
+        resources.values().forEach(this::releaseIndexes);
+    }
+
+    private Map<Expression, Set<SSTableIndex>> getView(OperationType op, Collection<Expression> expressions)
+    {
+        // first let's determine the primary expression if op is AND
+        Pair<Expression, Set<SSTableIndex>> primary = (op == OperationType.AND) ? calculatePrimary(expressions) : null;
+
+        Map<Expression, Set<SSTableIndex>> indexes = new HashMap<>();
+        for (Expression e : expressions)
+        {
+            // NO_EQ and non-index column query should only act as FILTER BY for satisfiedBy(Row) method
+            // because otherwise it likely to go through the whole index.
+            if (!e.isIndexed() || e.getOp() == Expression.Op.NOT_EQ)
+                continue;
+
+            // primary expression, we'll have to add as is
+            if (primary != null && e.equals(primary.left))
+            {
+                indexes.put(primary.left, primary.right);
+                continue;
+            }
+
+            View view = e.index.getView();
+            if (view == null)
+                continue;
+
+            Set<SSTableIndex> readers = new HashSet<>();
+            if (primary != null && primary.right.size() > 0)
+            {
+                for (SSTableIndex index : primary.right)
+                    readers.addAll(view.match(index.minKey(), index.maxKey()));
+            }
+            else
+            {
+                readers.addAll(applyScope(view.match(e)));
+            }
+
+            indexes.put(e, readers);
+        }
+
+        return indexes;
+    }
+
+    private Pair<Expression, Set<SSTableIndex>> calculatePrimary(Collection<Expression> expressions)
+    {
+        Expression expression = null;
+        Set<SSTableIndex> primaryIndexes = Collections.emptySet();
+
+        for (Expression e : expressions)
+        {
+            if (!e.isIndexed())
+                continue;
+
+            View view = e.index.getView();
+            if (view == null)
+                continue;
+
+            Set<SSTableIndex> indexes = applyScope(view.match(e));
+            if (expression == null || primaryIndexes.size() > indexes.size())
+            {
+                primaryIndexes = indexes;
+                expression = e;
+            }
+        }
+
+        return expression == null ? null : Pair.create(expression, primaryIndexes);
+    }
+
+    private Set<SSTableIndex> applyScope(Set<SSTableIndex> indexes)
+    {
+        return Sets.filter(indexes, index -> {
+            SSTableReader sstable = index.getSSTable();
+            return range.startKey().compareTo(sstable.last) <= 0 && (range.stopKey().isMinimum() || sstable.first.compareTo(range.stopKey()) <= 0);
+        });
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java b/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java
new file mode 100644
index 0000000..326ea0d
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java
@@ -0,0 +1,175 @@
+/*
+ * 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.index.sasi.plan;
+
+import java.util.*;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.plan.Operation.OperationType;
+import org.apache.cassandra.exceptions.RequestTimeoutException;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.AbstractIterator;
+
+public class QueryPlan
+{
+    private final QueryController controller;
+
+    public QueryPlan(ColumnFamilyStore cfs, ReadCommand command, long executionQuotaMs)
+    {
+        this.controller = new QueryController(cfs, (PartitionRangeReadCommand) command, executionQuotaMs);
+    }
+
+    /**
+     * Converts expressions into operation tree (which is currently just a single AND).
+     *
+     * Operation tree allows us to do a couple of important optimizations
+     * namely, group flattening for AND operations (query rewrite), expression bounds checks,
+     * "satisfies by" checks for resulting rows with an early exit.
+     *
+     * @return root of the operations tree.
+     */
+    private Operation analyze()
+    {
+        try
+        {
+            Operation.Builder and = new Operation.Builder(OperationType.AND, controller);
+            controller.getExpressions().forEach(and::add);
+            return and.complete();
+        }
+        catch (Exception | Error e)
+        {
+            controller.finish();
+            throw e;
+        }
+    }
+
+    public UnfilteredPartitionIterator execute(ReadExecutionController executionController) throws RequestTimeoutException
+    {
+        return new ResultIterator(analyze(), controller, executionController);
+    }
+
+    private static class ResultIterator extends AbstractIterator<UnfilteredRowIterator> implements UnfilteredPartitionIterator
+    {
+        private final AbstractBounds<PartitionPosition> keyRange;
+        private final Operation operationTree;
+        private final QueryController controller;
+        private final ReadExecutionController executionController;
+
+        private Iterator<DecoratedKey> currentKeys = null;
+
+        public ResultIterator(Operation operationTree, QueryController controller, ReadExecutionController executionController)
+        {
+            this.keyRange = controller.dataRange().keyRange();
+            this.operationTree = operationTree;
+            this.controller = controller;
+            this.executionController = executionController;
+            if (operationTree != null)
+                operationTree.skipTo((Long) keyRange.left.getToken().getTokenValue());
+        }
+
+        protected UnfilteredRowIterator computeNext()
+        {
+            if (operationTree == null)
+                return endOfData();
+
+            for (;;)
+            {
+                if (currentKeys == null || !currentKeys.hasNext())
+                {
+                    if (!operationTree.hasNext())
+                         return endOfData();
+
+                    Token token = operationTree.next();
+                    currentKeys = token.iterator();
+                }
+
+                while (currentKeys.hasNext())
+                {
+                    DecoratedKey key = currentKeys.next();
+
+                    if (!keyRange.right.isMinimum() && keyRange.right.compareTo(key) < 0)
+                        return endOfData();
+
+                    if (!keyRange.inclusiveLeft() && key.compareTo(keyRange.left) == 0)
+                        continue;
+
+                    try (UnfilteredRowIterator partition = controller.getPartition(key, executionController))
+                    {
+                        Row staticRow = partition.staticRow();
+                        List<Unfiltered> clusters = new ArrayList<>();
+
+                        while (partition.hasNext())
+                        {
+                            Unfiltered row = partition.next();
+                            if (operationTree.satisfiedBy(row, staticRow, true))
+                                clusters.add(row);
+                        }
+
+                        if (!clusters.isEmpty())
+                            return new PartitionIterator(partition, clusters);
+                    }
+                }
+            }
+        }
+
+        private static class PartitionIterator extends AbstractUnfilteredRowIterator
+        {
+            private final Iterator<Unfiltered> rows;
+
+            public PartitionIterator(UnfilteredRowIterator partition, Collection<Unfiltered> content)
+            {
+                super(partition.metadata(),
+                      partition.partitionKey(),
+                      partition.partitionLevelDeletion(),
+                      partition.columns(),
+                      partition.staticRow(),
+                      partition.isReverseOrder(),
+                      partition.stats());
+
+                rows = content.iterator();
+            }
+
+            @Override
+            protected Unfiltered computeNext()
+            {
+                return rows.hasNext() ? rows.next() : endOfData();
+            }
+        }
+
+        public boolean isForThrift()
+        {
+            return controller.isForThrift();
+        }
+
+        public CFMetaData metadata()
+        {
+            return controller.metadata();
+        }
+
+        public void close()
+        {
+            FileUtils.closeQuietly(operationTree);
+            controller.finish();
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/sa/ByteTerm.java b/src/java/org/apache/cassandra/index/sasi/sa/ByteTerm.java
new file mode 100644
index 0000000..c7bbab7
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/sa/ByteTerm.java
@@ -0,0 +1,51 @@
+/*
+ * 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.index.sasi.sa;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public class ByteTerm extends Term<ByteBuffer>
+{
+    public ByteTerm(int position, ByteBuffer value, TokenTreeBuilder tokens)
+    {
+        super(position, value, tokens);
+    }
+
+    public ByteBuffer getTerm()
+    {
+        return value.duplicate();
+    }
+
+    public ByteBuffer getSuffix(int start)
+    {
+        return (ByteBuffer) value.duplicate().position(value.position() + start);
+    }
+
+    public int compareTo(AbstractType<?> comparator, Term other)
+    {
+        return comparator.compare(value, (ByteBuffer) other.value);
+    }
+
+    public int length()
+    {
+        return value.remaining();
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/sa/CharTerm.java b/src/java/org/apache/cassandra/index/sasi/sa/CharTerm.java
new file mode 100644
index 0000000..533b566
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/sa/CharTerm.java
@@ -0,0 +1,54 @@
+/*
+ * 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.index.sasi.sa;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+
+import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
+import org.apache.cassandra.db.marshal.AbstractType;
+
+import com.google.common.base.Charsets;
+
+public class CharTerm extends Term<CharBuffer>
+{
+    public CharTerm(int position, CharBuffer value, TokenTreeBuilder tokens)
+    {
+        super(position, value, tokens);
+    }
+
+    public ByteBuffer getTerm()
+    {
+        return Charsets.UTF_8.encode(value.duplicate());
+    }
+
+    public ByteBuffer getSuffix(int start)
+    {
+        return Charsets.UTF_8.encode(value.subSequence(value.position() + start, value.remaining()));
+    }
+
+    public int compareTo(AbstractType<?> comparator, Term other)
+    {
+        return value.compareTo((CharBuffer) other.value);
+    }
+
+    public int length()
+    {
+        return value.length();
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/sa/IndexedTerm.java b/src/java/org/apache/cassandra/index/sasi/sa/IndexedTerm.java
new file mode 100644
index 0000000..8e27134
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/sa/IndexedTerm.java
@@ -0,0 +1,43 @@
+/*
+ * 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.index.sasi.sa;
+
+import java.nio.ByteBuffer;
+
+public class IndexedTerm
+{
+    private final ByteBuffer term;
+    private final boolean isPartial;
+
+    public IndexedTerm(ByteBuffer term, boolean isPartial)
+    {
+        this.term = term;
+        this.isPartial = isPartial;
+    }
+
+    public ByteBuffer getBytes()
+    {
+        return term;
+    }
+
+    public boolean isPartial()
+    {
+        return isPartial;
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/sa/IntegralSA.java b/src/java/org/apache/cassandra/index/sasi/sa/IntegralSA.java
new file mode 100644
index 0000000..5f04876
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/sa/IntegralSA.java
@@ -0,0 +1,84 @@
+/*
+ * 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.index.sasi.sa;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
+import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.utils.Pair;
+
+public class IntegralSA extends SA<ByteBuffer>
+{
+    public IntegralSA(AbstractType<?> comparator, OnDiskIndexBuilder.Mode mode)
+    {
+        super(comparator, mode);
+    }
+
+    public Term<ByteBuffer> getTerm(ByteBuffer termValue, TokenTreeBuilder tokens)
+    {
+        return new ByteTerm(charCount, termValue, tokens);
+    }
+
+    public TermIterator finish()
+    {
+        return new IntegralSuffixIterator();
+    }
+
+
+    private class IntegralSuffixIterator extends TermIterator
+    {
+        private final Iterator<Term<ByteBuffer>> termIterator;
+
+        public IntegralSuffixIterator()
+        {
+            Collections.sort(terms, new Comparator<Term<?>>()
+            {
+                public int compare(Term<?> a, Term<?> b)
+                {
+                    return a.compareTo(comparator, b);
+                }
+            });
+
+            termIterator = terms.iterator();
+        }
+
+        public ByteBuffer minTerm()
+        {
+            return terms.get(0).getTerm();
+        }
+
+        public ByteBuffer maxTerm()
+        {
+            return terms.get(terms.size() - 1).getTerm();
+        }
+
+        protected Pair<IndexedTerm, TokenTreeBuilder> computeNext()
+        {
+            if (!termIterator.hasNext())
+                return endOfData();
+
+            Term<ByteBuffer> term = termIterator.next();
+            return Pair.create(new IndexedTerm(term.getTerm(), false), term.getTokens().finish());
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/sa/SA.java b/src/java/org/apache/cassandra/index/sasi/sa/SA.java
new file mode 100644
index 0000000..75f9f92
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/sa/SA.java
@@ -0,0 +1,58 @@
+/*
+ * 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.index.sasi.sa;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.Mode;
+import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public abstract class SA<T extends Buffer>
+{
+    protected final AbstractType<?> comparator;
+    protected final Mode mode;
+
+    protected final List<Term<T>> terms = new ArrayList<>();
+    protected int charCount = 0;
+
+    public SA(AbstractType<?> comparator, Mode mode)
+    {
+        this.comparator = comparator;
+        this.mode = mode;
+    }
+
+    public Mode getMode()
+    {
+        return mode;
+    }
+
+    public void add(ByteBuffer termValue, TokenTreeBuilder tokens)
+    {
+        Term<T> term = getTerm(termValue, tokens);
+        terms.add(term);
+        charCount += term.length();
+    }
+
+    public abstract TermIterator finish();
+
+    protected abstract Term<T> getTerm(ByteBuffer termValue, TokenTreeBuilder tokens);
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/sa/SuffixSA.java b/src/java/org/apache/cassandra/index/sasi/sa/SuffixSA.java
new file mode 100644
index 0000000..9e1c76a
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/sa/SuffixSA.java
@@ -0,0 +1,159 @@
+/*
+ * 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.index.sasi.sa;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+
+import org.apache.cassandra.index.sasi.disk.DynamicTokenTreeBuilder;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
+import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.utils.LongTimSort;
+import org.apache.cassandra.utils.Pair;
+
+import com.google.common.base.Charsets;
+
+public class SuffixSA extends SA<CharBuffer>
+{
+    public SuffixSA(AbstractType<?> comparator, OnDiskIndexBuilder.Mode mode)
+    {
+        super(comparator, mode);
+    }
+
+    protected Term<CharBuffer> getTerm(ByteBuffer termValue, TokenTreeBuilder tokens)
+    {
+        return new CharTerm(charCount, Charsets.UTF_8.decode(termValue.duplicate()), tokens);
+    }
+
+    public TermIterator finish()
+    {
+        return new SASuffixIterator();
+    }
+
+    private class SASuffixIterator extends TermIterator
+    {
+
+        private static final int COMPLETE_BIT = 31;
+
+        private final long[] suffixes;
+
+        private int current = 0;
+        private IndexedTerm lastProcessedSuffix;
+        private TokenTreeBuilder container;
+
+        public SASuffixIterator()
+        {
+            // each element has term index and char position encoded as two 32-bit integers
+            // to avoid binary search per suffix while sorting suffix array.
+            suffixes = new long[charCount];
+
+            long termIndex = -1, currentTermLength = -1;
+            boolean isComplete = false;
+            for (int i = 0; i < charCount; i++)
+            {
+                if (i >= currentTermLength || currentTermLength == -1)
+                {
+                    Term currentTerm = terms.get((int) ++termIndex);
+                    currentTermLength = currentTerm.getPosition() + currentTerm.length();
+                    isComplete = true;
+                }
+
+                suffixes[i] = (termIndex << 32) | i;
+                if (isComplete)
+                    suffixes[i] |= (1L << COMPLETE_BIT);
+
+                isComplete = false;
+            }
+
+            LongTimSort.sort(suffixes, (a, b) -> {
+                Term aTerm = terms.get((int) (a >>> 32));
+                Term bTerm = terms.get((int) (b >>> 32));
+                return comparator.compare(aTerm.getSuffix(clearCompleteBit(a) - aTerm.getPosition()),
+                                          bTerm.getSuffix(clearCompleteBit(b) - bTerm.getPosition()));
+            });
+        }
+
+        private int clearCompleteBit(long value)
+        {
+            return (int) (value & ~(1L << COMPLETE_BIT));
+        }
+
+        private Pair<IndexedTerm, TokenTreeBuilder> suffixAt(int position)
+        {
+            long index = suffixes[position];
+            Term term = terms.get((int) (index >>> 32));
+            boolean isPartitial = (index & ((long) 1 << 31)) == 0;
+            return Pair.create(new IndexedTerm(term.getSuffix(clearCompleteBit(index) - term.getPosition()), isPartitial), term.getTokens());
+        }
+
+        public ByteBuffer minTerm()
+        {
+            return suffixAt(0).left.getBytes();
+        }
+
+        public ByteBuffer maxTerm()
+        {
+            return suffixAt(suffixes.length - 1).left.getBytes();
+        }
+
+        protected Pair<IndexedTerm, TokenTreeBuilder> computeNext()
+        {
+            while (true)
+            {
+                if (current >= suffixes.length)
+                {
+                    if (lastProcessedSuffix == null)
+                        return endOfData();
+
+                    Pair<IndexedTerm, TokenTreeBuilder> result = finishSuffix();
+
+                    lastProcessedSuffix = null;
+                    return result;
+                }
+
+                Pair<IndexedTerm, TokenTreeBuilder> suffix = suffixAt(current++);
+
+                if (lastProcessedSuffix == null)
+                {
+                    lastProcessedSuffix = suffix.left;
+                    container = new DynamicTokenTreeBuilder(suffix.right);
+                }
+                else if (comparator.compare(lastProcessedSuffix.getBytes(), suffix.left.getBytes()) == 0)
+                {
+                    lastProcessedSuffix = suffix.left;
+                    container.add(suffix.right);
+                }
+                else
+                {
+                    Pair<IndexedTerm, TokenTreeBuilder> result = finishSuffix();
+
+                    lastProcessedSuffix = suffix.left;
+                    container = new DynamicTokenTreeBuilder(suffix.right);
+
+                    return result;
+                }
+            }
+        }
+
+        private Pair<IndexedTerm, TokenTreeBuilder> finishSuffix()
+        {
+            return Pair.create(lastProcessedSuffix, container.finish());
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/sa/Term.java b/src/java/org/apache/cassandra/index/sasi/sa/Term.java
new file mode 100644
index 0000000..fe6eca8
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/sa/Term.java
@@ -0,0 +1,58 @@
+/*
+ * 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.index.sasi.sa;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public abstract class Term<T extends Buffer>
+{
+    protected final int position;
+    protected final T value;
+    protected TokenTreeBuilder tokens;
+
+
+    public Term(int position, T value, TokenTreeBuilder tokens)
+    {
+        this.position = position;
+        this.value = value;
+        this.tokens = tokens;
+    }
+
+    public int getPosition()
+    {
+        return position;
+    }
+
+    public abstract ByteBuffer getTerm();
+    public abstract ByteBuffer getSuffix(int start);
+
+    public TokenTreeBuilder getTokens()
+    {
+        return tokens;
+    }
+
+    public abstract int compareTo(AbstractType<?> comparator, Term other);
+
+    public abstract int length();
+
+}
+
diff --git a/src/java/org/apache/cassandra/index/sasi/sa/TermIterator.java b/src/java/org/apache/cassandra/index/sasi/sa/TermIterator.java
new file mode 100644
index 0000000..c8572a9
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/sa/TermIterator.java
@@ -0,0 +1,31 @@
+/*
+ * 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.index.sasi.sa;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
+import org.apache.cassandra.utils.Pair;
+
+import com.google.common.collect.AbstractIterator;
+
+public abstract class TermIterator extends AbstractIterator<Pair<IndexedTerm, TokenTreeBuilder>>
+{
+    public abstract ByteBuffer minTerm();
+    public abstract ByteBuffer maxTerm();
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/AbstractIterator.java b/src/java/org/apache/cassandra/index/sasi/utils/AbstractIterator.java
new file mode 100644
index 0000000..cf918c1
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/AbstractIterator.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2007 The Guava Authors
+ *
+ * Licensed 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.index.sasi.utils;
+
+import java.util.NoSuchElementException;
+
+import com.google.common.collect.PeekingIterator;
+
+import static com.google.common.base.Preconditions.checkState;
+
+// This is fork of the Guava AbstractIterator, the only difference
+// is that state & next variables are now protected, this was required
+// for SkippableIterator.skipTo(..) to void all previous state.
+public abstract class AbstractIterator<T> implements PeekingIterator<T>
+{
+    protected State state = State.NOT_READY;
+
+    /** Constructor for use by subclasses. */
+    protected AbstractIterator() {}
+
+    protected enum State
+    {
+        /** We have computed the next element and haven't returned it yet. */
+        READY,
+
+        /** We haven't yet computed or have already returned the element. */
+        NOT_READY,
+
+        /** We have reached the end of the data and are finished. */
+        DONE,
+
+        /** We've suffered an exception and are kaput. */
+        FAILED,
+    }
+
+    protected T next;
+
+    /**
+     * Returns the next element. <b>Note:</b> the implementation must call {@link
+     * #endOfData()} when there are no elements left in the iteration. Failure to
+     * do so could result in an infinite loop.
+     *
+     * <p>The initial invocation of {@link #hasNext()} or {@link #next()} calls
+     * this method, as does the first invocation of {@code hasNext} or {@code
+     * next} following each successful call to {@code next}. Once the
+     * implementation either invokes {@code endOfData} or throws an exception,
+     * {@code computeNext} is guaranteed to never be called again.
+     *
+     * <p>If this method throws an exception, it will propagate outward to the
+     * {@code hasNext} or {@code next} invocation that invoked this method. Any
+     * further attempts to use the iterator will result in an {@link
+     * IllegalStateException}.
+     *
+     * <p>The implementation of this method may not invoke the {@code hasNext},
+     * {@code next}, or {@link #peek()} methods on this instance; if it does, an
+     * {@code IllegalStateException} will result.
+     *
+     * @return the next element if there was one. If {@code endOfData} was called
+     *     during execution, the return value will be ignored.
+     * @throws RuntimeException if any unrecoverable error happens. This exception
+     *     will propagate outward to the {@code hasNext()}, {@code next()}, or
+     *     {@code peek()} invocation that invoked this method. Any further
+     *     attempts to use the iterator will result in an
+     *     {@link IllegalStateException}.
+     */
+    protected abstract T computeNext();
+
+    /**
+     * Implementations of {@link #computeNext} <b>must</b> invoke this method when
+     * there are no elements left in the iteration.
+     *
+     * @return {@code null}; a convenience so your {@code computeNext}
+     *     implementation can use the simple statement {@code return endOfData();}
+     */
+    protected final T endOfData()
+    {
+        state = State.DONE;
+        return null;
+    }
+
+    public final boolean hasNext()
+    {
+        checkState(state != State.FAILED);
+
+        switch (state)
+        {
+            case DONE:
+                return false;
+
+            case READY:
+                return true;
+
+            default:
+        }
+
+        return tryToComputeNext();
+    }
+
+    protected boolean tryToComputeNext()
+    {
+        state = State.FAILED; // temporary pessimism
+        next = computeNext();
+
+        if (state != State.DONE)
+        {
+            state = State.READY;
+            return true;
+        }
+
+        return false;
+    }
+
+    public final T next()
+    {
+        if (!hasNext())
+            throw new NoSuchElementException();
+
+        state = State.NOT_READY;
+        return next;
+    }
+
+    public void remove()
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Returns the next element in the iteration without advancing the iteration,
+     * according to the contract of {@link PeekingIterator#peek()}.
+     *
+     * <p>Implementations of {@code AbstractIterator} that wish to expose this
+     * functionality should implement {@code PeekingIterator}.
+     */
+    public final T peek()
+    {
+        if (!hasNext())
+            throw new NoSuchElementException();
+
+        return next;
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/CombinedTerm.java b/src/java/org/apache/cassandra/index/sasi/utils/CombinedTerm.java
new file mode 100644
index 0000000..81e535d
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/CombinedTerm.java
@@ -0,0 +1,84 @@
+/*
+ * 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.index.sasi.utils;
+
+import java.nio.ByteBuffer;
+import java.util.*;
+
+import org.apache.cassandra.index.sasi.disk.*;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndex.DataTerm;
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public class CombinedTerm implements CombinedValue<DataTerm>
+{
+    private final AbstractType<?> comparator;
+    private final DataTerm term;
+    private final List<DataTerm> mergedTerms = new ArrayList<>();
+
+    public CombinedTerm(AbstractType<?> comparator, DataTerm term)
+    {
+        this.comparator = comparator;
+        this.term = term;
+    }
+
+    public ByteBuffer getTerm()
+    {
+        return term.getTerm();
+    }
+
+    public boolean isPartial()
+    {
+        return term.isPartial();
+    }
+
+    public RangeIterator<Long, Token> getTokenIterator()
+    {
+        RangeIterator.Builder<Long, Token> union = RangeUnionIterator.builder();
+        union.add(term.getTokens());
+        mergedTerms.stream().map(OnDiskIndex.DataTerm::getTokens).forEach(union::add);
+
+        return union.build();
+    }
+
+    public TokenTreeBuilder getTokenTreeBuilder()
+    {
+        return new StaticTokenTreeBuilder(this).finish();
+    }
+
+    public void merge(CombinedValue<DataTerm> other)
+    {
+        if (!(other instanceof CombinedTerm))
+            return;
+
+        CombinedTerm o = (CombinedTerm) other;
+
+        assert comparator == o.comparator;
+
+        mergedTerms.add(o.term);
+    }
+
+    public DataTerm get()
+    {
+        return term;
+    }
+
+    public int compareTo(CombinedValue<DataTerm> o)
+    {
+        return term.compareTo(comparator, o.get().getTerm());
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/CombinedTermIterator.java b/src/java/org/apache/cassandra/index/sasi/utils/CombinedTermIterator.java
new file mode 100644
index 0000000..4b004e0
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/CombinedTermIterator.java
@@ -0,0 +1,89 @@
+/*
+ * 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.index.sasi.utils;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.index.sasi.disk.Descriptor;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndex;
+import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
+import org.apache.cassandra.index.sasi.sa.IndexedTerm;
+import org.apache.cassandra.index.sasi.sa.TermIterator;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.utils.Pair;
+
+@SuppressWarnings("resource")
+public class CombinedTermIterator extends TermIterator
+{
+    final Descriptor descriptor;
+    final RangeIterator<OnDiskIndex.DataTerm, CombinedTerm> union;
+    final ByteBuffer min;
+    final ByteBuffer max;
+
+    public CombinedTermIterator(OnDiskIndex... sas)
+    {
+        this(Descriptor.CURRENT, sas);
+    }
+
+    public CombinedTermIterator(Descriptor d, OnDiskIndex... parts)
+    {
+        descriptor = d;
+        union = OnDiskIndexIterator.union(parts);
+
+        AbstractType<?> comparator = parts[0].getComparator(); // assumes all SAs have same comparator
+        ByteBuffer minimum = parts[0].minTerm();
+        ByteBuffer maximum = parts[0].maxTerm();
+
+        for (int i = 1; i < parts.length; i++)
+        {
+            OnDiskIndex part = parts[i];
+            if (part == null)
+                continue;
+
+            minimum = comparator.compare(minimum, part.minTerm()) > 0 ? part.minTerm() : minimum;
+            maximum = comparator.compare(maximum, part.maxTerm()) < 0 ? part.maxTerm() : maximum;
+        }
+
+        min = minimum;
+        max = maximum;
+    }
+
+    public ByteBuffer minTerm()
+    {
+        return min;
+    }
+
+    public ByteBuffer maxTerm()
+    {
+        return max;
+    }
+
+    protected Pair<IndexedTerm, TokenTreeBuilder> computeNext()
+    {
+        if (!union.hasNext())
+        {
+            return endOfData();
+        }
+        else
+        {
+            CombinedTerm term = union.next();
+            return Pair.create(new IndexedTerm(term.getTerm(), term.isPartial()), term.getTokenTreeBuilder());
+        }
+
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/CombinedValue.java b/src/java/org/apache/cassandra/index/sasi/utils/CombinedValue.java
new file mode 100644
index 0000000..ca5f9be
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/CombinedValue.java
@@ -0,0 +1,25 @@
+/*
+ * 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.index.sasi.utils;
+
+public interface CombinedValue<V> extends Comparable<CombinedValue<V>>
+{
+    void merge(CombinedValue<V> other);
+
+    V get();
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/MappedBuffer.java b/src/java/org/apache/cassandra/index/sasi/utils/MappedBuffer.java
new file mode 100644
index 0000000..efabe7b
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/MappedBuffer.java
@@ -0,0 +1,253 @@
+/*
+ * 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.index.sasi.utils;
+
+import java.io.Closeable;
+import java.nio.ByteBuffer;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel.MapMode;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.io.util.ChannelProxy;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+
+import com.google.common.annotations.VisibleForTesting;
+
+public class MappedBuffer implements Closeable
+{
+    private final MappedByteBuffer[] pages;
+
+    private long position, limit;
+    private final long capacity;
+    private final int pageSize, sizeBits;
+
+    private MappedBuffer(MappedBuffer other)
+    {
+        this.sizeBits = other.sizeBits;
+        this.pageSize = other.pageSize;
+        this.position = other.position;
+        this.limit = other.limit;
+        this.capacity = other.capacity;
+        this.pages = other.pages;
+    }
+
+    public MappedBuffer(RandomAccessReader file)
+    {
+        this(file.getChannel(), 30);
+    }
+
+    public MappedBuffer(ChannelProxy file)
+    {
+        this(file, 30);
+    }
+
+    @VisibleForTesting
+    protected MappedBuffer(ChannelProxy file, int numPageBits)
+    {
+        if (numPageBits > Integer.SIZE - 1)
+            throw new IllegalArgumentException("page size can't be bigger than 1G");
+
+        sizeBits = numPageBits;
+        pageSize = 1 << sizeBits;
+        position = 0;
+        limit = capacity = file.size();
+        pages = new MappedByteBuffer[(int) (file.size() / pageSize) + 1];
+
+        try
+        {
+            long offset = 0;
+            for (int i = 0; i < pages.length; i++)
+            {
+                long pageSize = Math.min(this.pageSize, (capacity - offset));
+                pages[i] = file.map(MapMode.READ_ONLY, offset, pageSize);
+                offset += pageSize;
+            }
+        }
+        finally
+        {
+            file.close();
+        }
+    }
+
+    public int comparePageTo(long offset, int length, AbstractType<?> comparator, ByteBuffer other)
+    {
+        return comparator.compare(getPageRegion(offset, length), other);
+    }
+
+    public long capacity()
+    {
+        return capacity;
+    }
+
+    public long position()
+    {
+        return position;
+    }
+
+    public MappedBuffer position(long newPosition)
+    {
+        if (newPosition < 0 || newPosition > limit)
+            throw new IllegalArgumentException("position: " + newPosition + ", limit: " + limit);
+
+        position = newPosition;
+        return this;
+    }
+
+    public long limit()
+    {
+        return limit;
+    }
+
+    public MappedBuffer limit(long newLimit)
+    {
+        if (newLimit < position || newLimit > capacity)
+            throw new IllegalArgumentException();
+
+        limit = newLimit;
+        return this;
+    }
+
+    public long remaining()
+    {
+        return limit - position;
+    }
+
+    public boolean hasRemaining()
+    {
+        return remaining() > 0;
+    }
+
+    public byte get()
+    {
+        return get(position++);
+    }
+
+    public byte get(long pos)
+    {
+        return pages[getPage(pos)].get(getPageOffset(pos));
+    }
+
+    public short getShort()
+    {
+        short value = getShort(position);
+        position += 2;
+        return value;
+    }
+
+    public short getShort(long pos)
+    {
+        if (isPageAligned(pos, 2))
+            return pages[getPage(pos)].getShort(getPageOffset(pos));
+
+        int ch1 = get(pos)     & 0xff;
+        int ch2 = get(pos + 1) & 0xff;
+        return (short) ((ch1 << 8) + ch2);
+    }
+
+    public int getInt()
+    {
+        int value = getInt(position);
+        position += 4;
+        return value;
+    }
+
+    public int getInt(long pos)
+    {
+        if (isPageAligned(pos, 4))
+            return pages[getPage(pos)].getInt(getPageOffset(pos));
+
+        int ch1 = get(pos)     & 0xff;
+        int ch2 = get(pos + 1) & 0xff;
+        int ch3 = get(pos + 2) & 0xff;
+        int ch4 = get(pos + 3) & 0xff;
+
+        return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
+    }
+
+    public long getLong()
+    {
+        long value = getLong(position);
+        position += 8;
+        return value;
+    }
+
+
+    public long getLong(long pos)
+    {
+        // fast path if the long could be retrieved from a single page
+        // that would avoid multiple expensive look-ups into page array.
+        return (isPageAligned(pos, 8))
+                ? pages[getPage(pos)].getLong(getPageOffset(pos))
+                : ((long) (getInt(pos)) << 32) + (getInt(pos + 4) & 0xFFFFFFFFL);
+    }
+
+    public ByteBuffer getPageRegion(long position, int length)
+    {
+        if (!isPageAligned(position, length))
+            throw new IllegalArgumentException(String.format("range: %s-%s wraps more than one page", position, length));
+
+        ByteBuffer slice = pages[getPage(position)].duplicate();
+
+        int pageOffset = getPageOffset(position);
+        slice.position(pageOffset).limit(pageOffset + length);
+
+        return slice;
+    }
+
+    public MappedBuffer duplicate()
+    {
+        return new MappedBuffer(this);
+    }
+
+    public void close()
+    {
+        if (!FileUtils.isCleanerAvailable)
+            return;
+
+        /*
+         * Try forcing the unmapping of pages using undocumented unsafe sun APIs.
+         * If this fails (non Sun JVM), we'll have to wait for the GC to finalize the mapping.
+         * If this works and a thread tries to access any page, hell will unleash on earth.
+         */
+        try
+        {
+            for (MappedByteBuffer segment : pages)
+                FileUtils.clean(segment);
+        }
+        catch (Exception e)
+        {
+            // This is not supposed to happen
+        }
+    }
+
+    private int getPage(long position)
+    {
+        return (int) (position >> sizeBits);
+    }
+
+    private int getPageOffset(long position)
+    {
+        return (int) (position & pageSize - 1);
+    }
+
+    private boolean isPageAligned(long position, int length)
+    {
+        return pageSize - (getPageOffset(position) + length) > 0;
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/OnDiskIndexIterator.java b/src/java/org/apache/cassandra/index/sasi/utils/OnDiskIndexIterator.java
new file mode 100644
index 0000000..ae97cab
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/OnDiskIndexIterator.java
@@ -0,0 +1,64 @@
+/*
+ * 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.index.sasi.utils;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import org.apache.cassandra.index.sasi.disk.OnDiskIndex;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndex.DataTerm;
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public class OnDiskIndexIterator extends RangeIterator<DataTerm, CombinedTerm>
+{
+    private final AbstractType<?> comparator;
+    private final Iterator<DataTerm> terms;
+
+    public OnDiskIndexIterator(OnDiskIndex index)
+    {
+        super(index.min(), index.max(), Long.MAX_VALUE);
+
+        this.comparator = index.getComparator();
+        this.terms = index.iterator();
+    }
+
+    public static RangeIterator<DataTerm, CombinedTerm> union(OnDiskIndex... union)
+    {
+        RangeUnionIterator.Builder<DataTerm, CombinedTerm> builder = RangeUnionIterator.builder();
+        for (OnDiskIndex e : union)
+        {
+            if (e != null)
+                builder.add(new OnDiskIndexIterator(e));
+        }
+
+        return builder.build();
+    }
+
+    protected CombinedTerm computeNext()
+    {
+        return terms.hasNext() ? new CombinedTerm(comparator, terms.next()) : endOfData();
+    }
+
+    protected void performSkipTo(DataTerm nextToken)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public void close() throws IOException
+    {}
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java b/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java
new file mode 100644
index 0000000..bd8c725
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java
@@ -0,0 +1,285 @@
+/*
+ * 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.index.sasi.utils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.PriorityQueue;
+
+import com.google.common.collect.Iterators;
+import org.apache.cassandra.io.util.FileUtils;
+
+import com.google.common.annotations.VisibleForTesting;
+
+@SuppressWarnings("resource")
+public class RangeIntersectionIterator
+{
+    protected enum Strategy
+    {
+        BOUNCE, LOOKUP, ADAPTIVE
+    }
+
+    public static <K extends Comparable<K>, D extends CombinedValue<K>> Builder<K, D> builder()
+    {
+        return builder(Strategy.ADAPTIVE);
+    }
+
+    @VisibleForTesting
+    protected static <K extends Comparable<K>, D extends CombinedValue<K>> Builder<K, D> builder(Strategy strategy)
+    {
+        return new Builder<>(strategy);
+    }
+
+    public static class Builder<K extends Comparable<K>, D extends CombinedValue<K>> extends RangeIterator.Builder<K, D>
+    {
+        private final Strategy strategy;
+
+        public Builder(Strategy strategy)
+        {
+            super(IteratorType.INTERSECTION);
+            this.strategy = strategy;
+        }
+
+        protected RangeIterator<K, D> buildIterator()
+        {
+            // if the range is disjoint or we have an intersection with an empty set,
+            // we can simply return an empty iterator, because it's not going to produce any results.
+            if (statistics.isDisjoint())
+                return new EmptyRangeIterator<>();
+
+            if (rangeCount() == 1)
+                return ranges.poll();
+
+            switch (strategy)
+            {
+                case LOOKUP:
+                    return new LookupIntersectionIterator<>(statistics, ranges);
+
+                case BOUNCE:
+                    return new BounceIntersectionIterator<>(statistics, ranges);
+
+                case ADAPTIVE:
+                    return statistics.sizeRatio() <= 0.01d
+                            ? new LookupIntersectionIterator<>(statistics, ranges)
+                            : new BounceIntersectionIterator<>(statistics, ranges);
+
+                default:
+                    throw new IllegalStateException("Unknown strategy: " + strategy);
+            }
+        }
+    }
+
+    private static abstract class AbstractIntersectionIterator<K extends Comparable<K>, D extends CombinedValue<K>> extends RangeIterator<K, D>
+    {
+        protected final PriorityQueue<RangeIterator<K, D>> ranges;
+
+        private AbstractIntersectionIterator(Builder.Statistics<K, D> statistics, PriorityQueue<RangeIterator<K, D>> ranges)
+        {
+            super(statistics);
+            this.ranges = ranges;
+        }
+
+        public void close() throws IOException
+        {
+            for (RangeIterator<K, D> range : ranges)
+                FileUtils.closeQuietly(range);
+        }
+    }
+
+    /**
+     * Iterator which performs intersection of multiple ranges by using bouncing (merge-join) technique to identify
+     * common elements in the given ranges. Aforementioned "bounce" works as follows: range queue is poll'ed for the
+     * range with the smallest current token (main loop), that token is used to {@link RangeIterator#skipTo(Comparable)}
+     * other ranges, if token produced by {@link RangeIterator#skipTo(Comparable)} is equal to current "candidate" token,
+     * both get merged together and the same operation is repeated for next range from the queue, if returned token
+     * is not equal than candidate, candidate's range gets put back into the queue and the main loop gets repeated until
+     * next intersection token is found or at least one iterator runs out of tokens.
+     *
+     * This technique is every efficient to jump over gaps in the ranges.
+     *
+     * @param <K> The type used to sort ranges.
+     * @param <D> The container type which is going to be returned by {@link Iterator#next()}.
+     */
+    @VisibleForTesting
+    protected static class BounceIntersectionIterator<K extends Comparable<K>, D extends CombinedValue<K>> extends AbstractIntersectionIterator<K, D>
+    {
+        private BounceIntersectionIterator(Builder.Statistics<K, D> statistics, PriorityQueue<RangeIterator<K, D>> ranges)
+        {
+            super(statistics, ranges);
+        }
+
+        protected D computeNext()
+        {
+            while (!ranges.isEmpty())
+            {
+                RangeIterator<K, D> head = ranges.poll();
+
+                // jump right to the beginning of the intersection or return next element
+                if (head.getCurrent().compareTo(getMinimum()) < 0)
+                    head.skipTo(getMinimum());
+
+                D candidate = head.hasNext() ? head.next() : null;
+                if (candidate == null || candidate.get().compareTo(getMaximum()) > 0)
+                {
+                    ranges.add(head);
+                    return endOfData();
+                }
+
+                List<RangeIterator<K, D>> processed = new ArrayList<>();
+
+                boolean intersectsAll = true, exhausted = false;
+                while (!ranges.isEmpty())
+                {
+                    RangeIterator<K, D> range = ranges.poll();
+
+                    // found a range which doesn't overlap with one (or possibly more) other range(s)
+                    if (!isOverlapping(head, range))
+                    {
+                        exhausted = true;
+                        intersectsAll = false;
+                        break;
+                    }
+
+                    D point = range.skipTo(candidate.get());
+
+                    if (point == null) // other range is exhausted
+                    {
+                        exhausted = true;
+                        intersectsAll = false;
+                        break;
+                    }
+
+                    processed.add(range);
+
+                    if (candidate.get().equals(point.get()))
+                    {
+                        candidate.merge(point);
+                        // advance skipped range to the next element if any
+                        Iterators.getNext(range, null);
+                    }
+                    else
+                    {
+                        intersectsAll = false;
+                        break;
+                    }
+                }
+
+                ranges.add(head);
+
+                for (RangeIterator<K, D> range : processed)
+                    ranges.add(range);
+
+                if (exhausted)
+                    return endOfData();
+
+                if (intersectsAll)
+                    return candidate;
+            }
+
+            return endOfData();
+        }
+
+        protected void performSkipTo(K nextToken)
+        {
+            List<RangeIterator<K, D>> skipped = new ArrayList<>();
+
+            while (!ranges.isEmpty())
+            {
+                RangeIterator<K, D> range = ranges.poll();
+                range.skipTo(nextToken);
+                skipped.add(range);
+            }
+
+            for (RangeIterator<K, D> range : skipped)
+                ranges.add(range);
+        }
+    }
+
+    /**
+     * Iterator which performs a linear scan over a primary range (the smallest of the ranges)
+     * and O(log(n)) lookup into secondary ranges using values from the primary iterator.
+     * This technique is efficient when one of the intersection ranges is smaller than others
+     * e.g. ratio 0.01d (default), in such situation scan + lookup is more efficient comparing
+     * to "bounce" merge because "bounce" distance is never going to be big.
+     *
+     * @param <K> The type used to sort ranges.
+     * @param <D> The container type which is going to be returned by {@link Iterator#next()}.
+     */
+    @VisibleForTesting
+    protected static class LookupIntersectionIterator<K extends Comparable<K>, D extends CombinedValue<K>> extends AbstractIntersectionIterator<K, D>
+    {
+        private final RangeIterator<K, D> smallestIterator;
+
+        private LookupIntersectionIterator(Builder.Statistics<K, D> statistics, PriorityQueue<RangeIterator<K, D>> ranges)
+        {
+            super(statistics, ranges);
+
+            smallestIterator = statistics.minRange;
+
+            if (smallestIterator.getCurrent().compareTo(getMinimum()) < 0)
+                smallestIterator.skipTo(getMinimum());
+        }
+
+        protected D computeNext()
+        {
+            while (smallestIterator.hasNext())
+            {
+                D candidate = smallestIterator.next();
+                K token = candidate.get();
+
+                boolean intersectsAll = true;
+                for (RangeIterator<K, D> range : ranges)
+                {
+                    // avoid checking against self, much cheaper than changing queue comparator
+                    // to compare based on the size and re-populating such queue.
+                    if (range.equals(smallestIterator))
+                        continue;
+
+                    // found a range which doesn't overlap with one (or possibly more) other range(s)
+                    if (!isOverlapping(smallestIterator, range))
+                        return endOfData();
+
+                    D point = range.skipTo(token);
+
+                    if (point == null) // one of the iterators is exhausted
+                        return endOfData();
+
+                    if (!point.get().equals(token))
+                    {
+                        intersectsAll = false;
+                        break;
+                    }
+
+                    candidate.merge(point);
+                }
+
+                if (intersectsAll)
+                    return candidate;
+            }
+
+            return endOfData();
+        }
+
+        protected void performSkipTo(K nextToken)
+        {
+            smallestIterator.skipTo(nextToken);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/RangeIterator.java b/src/java/org/apache/cassandra/index/sasi/utils/RangeIterator.java
new file mode 100644
index 0000000..06c5a6a
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/RangeIterator.java
@@ -0,0 +1,320 @@
+/*
+ * 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.index.sasi.utils;
+
+import java.io.Closeable;
+import java.util.Comparator;
+import java.util.List;
+import java.util.PriorityQueue;
+
+import com.google.common.annotations.VisibleForTesting;
+
+public abstract class RangeIterator<K extends Comparable<K>, T extends CombinedValue<K>> extends AbstractIterator<T> implements Closeable
+{
+    private final K min, max;
+    private final long count;
+    private K current;
+
+    protected RangeIterator(Builder.Statistics<K, T> statistics)
+    {
+        this(statistics.min, statistics.max, statistics.tokenCount);
+    }
+
+    public RangeIterator(RangeIterator<K, T> range)
+    {
+        this(range == null ? null : range.min, range == null ? null : range.max, range == null ? -1 : range.count);
+    }
+
+    public RangeIterator(K min, K max, long count)
+    {
+        if (min == null || max == null || count == 0)
+            assert min == null && max == null && (count == 0 || count == -1);
+
+        this.min = min;
+        this.current = min;
+        this.max = max;
+        this.count = count;
+    }
+
+    public final K getMinimum()
+    {
+        return min;
+    }
+
+    public final K getCurrent()
+    {
+        return current;
+    }
+
+    public final K getMaximum()
+    {
+        return max;
+    }
+
+    public final long getCount()
+    {
+        return count;
+    }
+
+    /**
+     * When called, this iterators current position should
+     * be skipped forwards until finding either:
+     *   1) an element equal to or bigger than next
+     *   2) the end of the iterator
+     *
+     * @param nextToken value to skip the iterator forward until matching
+     *
+     * @return The next current token after the skip was performed
+     */
+    public final T skipTo(K nextToken)
+    {
+        if (min == null || max == null)
+            return endOfData();
+
+        if (current.compareTo(nextToken) >= 0)
+            return next == null ? recomputeNext() : next;
+
+        if (max.compareTo(nextToken) < 0)
+            return endOfData();
+
+        performSkipTo(nextToken);
+        return recomputeNext();
+    }
+
+    protected abstract void performSkipTo(K nextToken);
+
+    protected T recomputeNext()
+    {
+        return tryToComputeNext() ? peek() : endOfData();
+    }
+
+    protected boolean tryToComputeNext()
+    {
+        boolean hasNext = super.tryToComputeNext();
+        current = hasNext ? next.get() : getMaximum();
+        return hasNext;
+    }
+
+    public static abstract class Builder<K extends Comparable<K>, D extends CombinedValue<K>>
+    {
+        public enum IteratorType
+        {
+            UNION, INTERSECTION
+        }
+
+        @VisibleForTesting
+        protected final Statistics<K, D> statistics;
+
+        @VisibleForTesting
+        protected final PriorityQueue<RangeIterator<K, D>> ranges;
+
+        public Builder(IteratorType type)
+        {
+            statistics = new Statistics<>(type);
+            ranges = new PriorityQueue<>(16, (Comparator<RangeIterator<K, D>>) (a, b) -> a.getCurrent().compareTo(b.getCurrent()));
+        }
+
+        public K getMinimum()
+        {
+            return statistics.min;
+        }
+
+        public K getMaximum()
+        {
+            return statistics.max;
+        }
+
+        public long getTokenCount()
+        {
+            return statistics.tokenCount;
+        }
+
+        public int rangeCount()
+        {
+            return ranges.size();
+        }
+
+        public Builder<K, D> add(RangeIterator<K, D> range)
+        {
+            if (range == null)
+                return this;
+
+            if (range.getCount() > 0)
+                ranges.add(range);
+            statistics.update(range);
+
+            return this;
+        }
+
+        public Builder<K, D> add(List<RangeIterator<K, D>> ranges)
+        {
+            if (ranges == null || ranges.isEmpty())
+                return this;
+
+            ranges.forEach(this::add);
+            return this;
+        }
+
+        public final RangeIterator<K, D> build()
+        {
+            if (rangeCount() == 0)
+                return new EmptyRangeIterator<>();
+            else
+                return buildIterator();
+        }
+
+        public static class EmptyRangeIterator<K extends Comparable<K>, D extends CombinedValue<K>> extends RangeIterator<K, D>
+        {
+            EmptyRangeIterator() { super(null, null, 0); }
+            public D computeNext() { return endOfData(); }
+            protected void performSkipTo(K nextToken) { }
+            public void close() { }
+        }
+
+        protected abstract RangeIterator<K, D> buildIterator();
+
+        public static class Statistics<K extends Comparable<K>, D extends CombinedValue<K>>
+        {
+            protected final IteratorType iteratorType;
+
+            protected K min, max;
+            protected long tokenCount;
+
+            // iterator with the least number of items
+            protected RangeIterator<K, D> minRange;
+            // iterator with the most number of items
+            protected RangeIterator<K, D> maxRange;
+
+            // tracks if all of the added ranges overlap, which is useful in case of intersection,
+            // as it gives direct answer as to such iterator is going to produce any results.
+            private boolean isOverlapping = true;
+
+            public Statistics(IteratorType iteratorType)
+            {
+                this.iteratorType = iteratorType;
+            }
+
+            /**
+             * Update statistics information with the given range.
+             *
+             * Updates min/max of the combined range, token count and
+             * tracks range with the least/most number of tokens.
+             *
+             * @param range The range to update statistics with.
+             */
+            public void update(RangeIterator<K, D> range)
+            {
+                switch (iteratorType)
+                {
+                    case UNION:
+                        min = nullSafeMin(min, range.getMinimum());
+                        max = nullSafeMax(max, range.getMaximum());
+                        break;
+
+                    case INTERSECTION:
+                        // minimum of the intersection is the biggest minimum of individual iterators
+                        min = nullSafeMax(min, range.getMinimum());
+                        // maximum of the intersection is the smallest maximum of individual iterators
+                        max = nullSafeMin(max, range.getMaximum());
+                        break;
+
+                    default:
+                        throw new IllegalStateException("Unknown iterator type: " + iteratorType);
+                }
+
+                // check if new range is disjoint with already added ranges, which means that this intersection
+                // is not going to produce any results, so we can cleanup range storage and never added anything to it.
+                isOverlapping &= isOverlapping(min, max, range);
+
+                minRange = minRange == null ? range : min(minRange, range);
+                maxRange = maxRange == null ? range : max(maxRange, range);
+
+                tokenCount += range.getCount();
+            }
+
+            private RangeIterator<K, D> min(RangeIterator<K, D> a, RangeIterator<K, D> b)
+            {
+                return a.getCount() > b.getCount() ? b : a;
+            }
+
+            private RangeIterator<K, D> max(RangeIterator<K, D> a, RangeIterator<K, D> b)
+            {
+                return a.getCount() > b.getCount() ? a : b;
+            }
+
+            public boolean isDisjoint()
+            {
+                return !isOverlapping;
+            }
+
+            public double sizeRatio()
+            {
+                return minRange.getCount() * 1d / maxRange.getCount();
+            }
+        }
+    }
+
+    @VisibleForTesting
+    protected static <K extends Comparable<K>, D extends CombinedValue<K>> boolean isOverlapping(RangeIterator<K, D> a, RangeIterator<K, D> b)
+    {
+        return isOverlapping(a.getCurrent(), a.getMaximum(), b);
+    }
+
+    /**
+     * Ranges are overlapping the following cases:
+     *
+     *   * When they have a common subrange:
+     *
+     *   min       b.current      max          b.max
+     *   +---------|--------------+------------|
+     *
+     *   b.current      min       max          b.max
+     *   |--------------+---------+------------|
+     *
+     *   min        b.current     b.max        max
+     *   +----------|-------------|------------+
+     *
+     *
+     *  If either range is empty, they're disjoint.
+     */
+    @VisibleForTesting
+    protected static <K extends Comparable<K>, D extends CombinedValue<K>> boolean isOverlapping(K min, K max, RangeIterator<K, D> b)
+    {
+        return (min != null && max != null) &&
+               b.getCount() != 0 &&
+               (min.compareTo(b.getMaximum()) <= 0 && b.getCurrent().compareTo(max) <= 0);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T extends Comparable> T nullSafeMin(T a, T b)
+    {
+        if (a == null) return b;
+        if (b == null) return a;
+
+        return a.compareTo(b) > 0 ? b : a;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T extends Comparable> T nullSafeMax(T a, T b)
+    {
+        if (a == null) return b;
+        if (b == null) return a;
+
+        return a.compareTo(b) > 0 ? a : b;
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java b/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java
new file mode 100644
index 0000000..599de07
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java
@@ -0,0 +1,166 @@
+/*
+ * 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.index.sasi.utils;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import org.apache.cassandra.io.util.FileUtils;
+
+/**
+ * Range Union Iterator is used to return sorted stream of elements from multiple RangeIterator instances.
+ *
+ * PriorityQueue is used as a sorting mechanism for the ranges, where each computeNext() operation would poll
+ * from the queue (and push when done), which returns range that contains the smallest element, because
+ * sorting is done on the moving window of range iteration {@link RangeIterator#getCurrent()}. Once retrieved
+ * the smallest element (return candidate) is attempted to be merged with other ranges, because there could
+ * be equal elements in adjacent ranges, such ranges are poll'ed only if their {@link RangeIterator#getCurrent()}
+ * equals to the return candidate.
+ *
+ * @param <K> The type used to sort ranges.
+ * @param <D> The container type which is going to be returned by {@link Iterator#next()}.
+ */
+@SuppressWarnings("resource")
+public class RangeUnionIterator<K extends Comparable<K>, D extends CombinedValue<K>> extends RangeIterator<K, D>
+{
+    private final PriorityQueue<RangeIterator<K, D>> ranges;
+
+    private RangeUnionIterator(Builder.Statistics<K, D> statistics, PriorityQueue<RangeIterator<K, D>> ranges)
+    {
+        super(statistics);
+        this.ranges = ranges;
+    }
+
+    public D computeNext()
+    {
+        RangeIterator<K, D> head = null;
+
+        while (!ranges.isEmpty())
+        {
+            head = ranges.poll();
+            if (head.hasNext())
+                break;
+
+            FileUtils.closeQuietly(head);
+        }
+
+        if (head == null || !head.hasNext())
+            return endOfData();
+
+        D candidate = head.next();
+
+        List<RangeIterator<K, D>> processedRanges = new ArrayList<>();
+
+        if (head.hasNext())
+            processedRanges.add(head);
+        else
+            FileUtils.closeQuietly(head);
+
+        while (!ranges.isEmpty())
+        {
+            // peek here instead of poll is an optimization
+            // so we can re-insert less ranges back if candidate
+            // is less than head of the current range.
+            RangeIterator<K, D> range = ranges.peek();
+
+            int cmp = candidate.get().compareTo(range.getCurrent());
+
+            assert cmp <= 0;
+
+            if (cmp < 0)
+            {
+                break; // candidate is smaller than next token, return immediately
+            }
+            else if (cmp == 0)
+            {
+                candidate.merge(range.next()); // consume and merge
+
+                range = ranges.poll();
+                // re-prioritize changed range
+
+                if (range.hasNext())
+                    processedRanges.add(range);
+                else
+                    FileUtils.closeQuietly(range);
+            }
+        }
+
+        ranges.addAll(processedRanges);
+        return candidate;
+    }
+
+    protected void performSkipTo(K nextToken)
+    {
+        List<RangeIterator<K, D>> changedRanges = new ArrayList<>();
+
+        while (!ranges.isEmpty())
+        {
+            if (ranges.peek().getCurrent().compareTo(nextToken) >= 0)
+                break;
+
+            RangeIterator<K, D> head = ranges.poll();
+
+            if (head.getMaximum().compareTo(nextToken) >= 0)
+            {
+                head.skipTo(nextToken);
+                changedRanges.add(head);
+                continue;
+            }
+
+            FileUtils.closeQuietly(head);
+        }
+
+        ranges.addAll(changedRanges.stream().collect(Collectors.toList()));
+    }
+
+    public void close() throws IOException
+    {
+        ranges.forEach(FileUtils::closeQuietly);
+    }
+
+    public static <K extends Comparable<K>, D extends CombinedValue<K>> Builder<K, D> builder()
+    {
+        return new Builder<>();
+    }
+
+    public static <K extends Comparable<K>, D extends CombinedValue<K>> RangeIterator<K, D> build(List<RangeIterator<K, D>> tokens)
+    {
+        return new Builder<K, D>().add(tokens).build();
+    }
+
+    public static class Builder<K extends Comparable<K>, D extends CombinedValue<K>> extends RangeIterator.Builder<K, D>
+    {
+        public Builder()
+        {
+            super(IteratorType.UNION);
+        }
+
+        protected RangeIterator<K, D> buildIterator()
+        {
+            switch (rangeCount())
+            {
+                case 1:
+                    return ranges.poll();
+
+                default:
+                    return new RangeUnionIterator<>(statistics, ranges);
+            }
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/TypeUtil.java b/src/java/org/apache/cassandra/index/sasi/utils/TypeUtil.java
new file mode 100644
index 0000000..9815233
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/TypeUtil.java
@@ -0,0 +1,84 @@
+/*
+ * 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.index.sasi.utils;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.serializers.MarshalException;
+
+public class TypeUtil
+{
+    public static boolean isValid(ByteBuffer term, AbstractType<?> validator)
+    {
+        try
+        {
+            validator.validate(term);
+            return true;
+        }
+        catch (MarshalException e)
+        {
+            return false;
+        }
+    }
+
+    public static ByteBuffer tryUpcast(ByteBuffer term, AbstractType<?> validator)
+    {
+        if (term.remaining() == 0)
+            return null;
+
+        try
+        {
+            if (validator instanceof Int32Type && term.remaining() == 2)
+            {
+                return Int32Type.instance.decompose((int) term.getShort(term.position()));
+            }
+            else if (validator instanceof LongType)
+            {
+                long upcastToken;
+
+                switch (term.remaining())
+                {
+                    case 2:
+                        upcastToken = (long) term.getShort(term.position());
+                        break;
+
+                    case 4:
+                        upcastToken = (long) Int32Type.instance.compose(term);
+                        break;
+
+                    default:
+                        upcastToken = Long.parseLong(UTF8Type.instance.getString(term));
+                }
+
+                return LongType.instance.decompose(upcastToken);
+            }
+            else if (validator instanceof DoubleType && term.remaining() == 4)
+            {
+                return DoubleType.instance.decompose((double) FloatType.instance.compose(term));
+            }
+
+            // maybe it was a string after all
+            return validator.fromString(UTF8Type.instance.getString(term));
+        }
+        catch (Exception e)
+        {
+            return null;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/AbstractPatriciaTrie.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/AbstractPatriciaTrie.java
new file mode 100644
index 0000000..8067ccc
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/trie/AbstractPatriciaTrie.java
@@ -0,0 +1,1152 @@
+/*
+ * Copyright 2005-2010 Roger Kapsi, Sam Berlin
+ *
+ *   Licensed 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.
+ */
+
+/**
+ * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified
+ * to correspond to Cassandra code style, as the only Patricia Trie implementation,
+ * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based
+ * on rkapsi/patricia-trie project) only supports String keys)
+ * but unfortunately is not deployed to the maven central as a downloadable artifact.
+ */
+
+package org.apache.cassandra.index.sasi.utils.trie;
+
+import java.util.AbstractCollection;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import org.apache.cassandra.index.sasi.utils.trie.Cursor.Decision;
+
+/**
+ * This class implements the base PATRICIA algorithm and everything that
+ * is related to the {@link Map} interface.
+ */
+abstract class AbstractPatriciaTrie<K, V> extends AbstractTrie<K, V>
+{
+    private static final long serialVersionUID = -2303909182832019043L;
+
+    /**
+     * The root node of the {@link Trie}.
+     */
+    final TrieEntry<K, V> root = new TrieEntry<>(null, null, -1);
+
+    /**
+     * Each of these fields are initialized to contain an instance of the
+     * appropriate view the first time this view is requested. The views are
+     * stateless, so there's no reason to create more than one of each.
+     */
+    private transient volatile Set<K> keySet;
+    private transient volatile Collection<V> values;
+    private transient volatile Set<Map.Entry<K,V>> entrySet;
+
+    /**
+     * The current size of the {@link Trie}
+     */
+    private int size = 0;
+
+    /**
+     * The number of times this {@link Trie} has been modified.
+     * It's used to detect concurrent modifications and fail-fast
+     * the {@link Iterator}s.
+     */
+    transient int modCount = 0;
+
+    public AbstractPatriciaTrie(KeyAnalyzer<? super K> keyAnalyzer)
+    {
+        super(keyAnalyzer);
+    }
+
+    public AbstractPatriciaTrie(KeyAnalyzer<? super K> keyAnalyzer, Map<? extends K, ? extends V> m)
+    {
+        super(keyAnalyzer);
+        putAll(m);
+    }
+
+    @Override
+    public void clear()
+    {
+        root.key = null;
+        root.bitIndex = -1;
+        root.value = null;
+
+        root.parent = null;
+        root.left = root;
+        root.right = null;
+        root.predecessor = root;
+
+        size = 0;
+        incrementModCount();
+    }
+
+    @Override
+    public int size()
+    {
+        return size;
+    }
+
+    /**
+     * A helper method to increment the {@link Trie} size
+     * and the modification counter.
+     */
+    void incrementSize()
+    {
+        size++;
+        incrementModCount();
+    }
+
+    /**
+     * A helper method to decrement the {@link Trie} size
+     * and increment the modification counter.
+     */
+    void decrementSize()
+    {
+        size--;
+        incrementModCount();
+    }
+
+    /**
+     * A helper method to increment the modification counter.
+     */
+    private void incrementModCount()
+    {
+        ++modCount;
+    }
+
+    @Override
+    public V put(K key, V value)
+    {
+        if (key == null)
+            throw new NullPointerException("Key cannot be null");
+
+        int lengthInBits = lengthInBits(key);
+
+        // The only place to store a key with a length
+        // of zero bits is the root node
+        if (lengthInBits == 0)
+        {
+            if (root.isEmpty())
+                incrementSize();
+            else
+                incrementModCount();
+
+            return root.setKeyValue(key, value);
+        }
+
+        TrieEntry<K, V> found = getNearestEntryForKey(key);
+        if (compareKeys(key, found.key))
+        {
+            if (found.isEmpty()) // <- must be the root
+                incrementSize();
+            else
+                incrementModCount();
+
+            return found.setKeyValue(key, value);
+        }
+
+        int bitIndex = bitIndex(key, found.key);
+        if (!Tries.isOutOfBoundsIndex(bitIndex))
+        {
+            if (Tries.isValidBitIndex(bitIndex)) // in 99.999...9% the case
+            {
+                /* NEW KEY+VALUE TUPLE */
+                TrieEntry<K, V> t = new TrieEntry<>(key, value, bitIndex);
+                addEntry(t);
+                incrementSize();
+                return null;
+            }
+            else if (Tries.isNullBitKey(bitIndex))
+            {
+                // A bits of the Key are zero. The only place to
+                // store such a Key is the root Node!
+
+                /* NULL BIT KEY */
+                if (root.isEmpty())
+                    incrementSize();
+                else
+                    incrementModCount();
+
+                return root.setKeyValue(key, value);
+
+            }
+            else if (Tries.isEqualBitKey(bitIndex))
+            {
+                // This is a very special and rare case.
+
+                /* REPLACE OLD KEY+VALUE */
+                if (found != root)
+                {
+                    incrementModCount();
+                    return found.setKeyValue(key, value);
+                }
+            }
+        }
+
+        throw new IndexOutOfBoundsException("Failed to put: "
+                + key + " -> " + value + ", " + bitIndex);
+    }
+
+    /**
+     * Adds the given {@link TrieEntry} to the {@link Trie}
+     */
+    TrieEntry<K, V> addEntry(TrieEntry<K, V> entry)
+    {
+        TrieEntry<K, V> current = root.left;
+        TrieEntry<K, V> path = root;
+
+        while(true)
+        {
+            if (current.bitIndex >= entry.bitIndex || current.bitIndex <= path.bitIndex)
+            {
+                entry.predecessor = entry;
+
+                if (!isBitSet(entry.key, entry.bitIndex))
+                {
+                    entry.left = entry;
+                    entry.right = current;
+                }
+                else
+                {
+                    entry.left = current;
+                    entry.right = entry;
+                }
+
+                entry.parent = path;
+                if (current.bitIndex >= entry.bitIndex)
+                    current.parent = entry;
+
+                // if we inserted an uplink, set the predecessor on it
+                if (current.bitIndex <= path.bitIndex)
+                    current.predecessor = entry;
+
+                if (path == root || !isBitSet(entry.key, path.bitIndex))
+                    path.left = entry;
+                else
+                    path.right = entry;
+
+                return entry;
+            }
+
+            path = current;
+
+            current = !isBitSet(entry.key, current.bitIndex)
+                       ? current.left : current.right;
+        }
+    }
+
+    @Override
+    public V get(Object k)
+    {
+        TrieEntry<K, V> entry = getEntry(k);
+        return entry != null ? entry.getValue() : null;
+    }
+
+    /**
+     * Returns the entry associated with the specified key in the
+     * AbstractPatriciaTrie.  Returns null if the map contains no mapping
+     * for this key.
+     *
+     * This may throw ClassCastException if the object is not of type K.
+     */
+    TrieEntry<K,V> getEntry(Object k)
+    {
+        K key = Tries.cast(k);
+        if (key == null)
+            return null;
+
+        TrieEntry<K,V> entry = getNearestEntryForKey(key);
+        return !entry.isEmpty() && compareKeys(key, entry.key) ? entry : null;
+    }
+
+    @Override
+    public Map.Entry<K, V> select(K key)
+    {
+        Reference<Map.Entry<K, V>> reference = new Reference<>();
+        return !selectR(root.left, -1, key, reference) ? reference.get() : null;
+    }
+
+    @Override
+    public Map.Entry<K,V> select(K key, Cursor<? super K, ? super V> cursor)
+    {
+        Reference<Map.Entry<K, V>> reference = new Reference<>();
+        selectR(root.left, -1, key, cursor, reference);
+        return reference.get();
+    }
+
+    /**
+     * This is equivalent to the other {@link #selectR(TrieEntry, int,
+     * K, Cursor, Reference)} method but without its overhead
+     * because we're selecting only one best matching Entry from the
+     * {@link Trie}.
+     */
+    private boolean selectR(TrieEntry<K, V> h, int bitIndex, final K key, final Reference<Map.Entry<K, V>> reference)
+    {
+        if (h.bitIndex <= bitIndex)
+        {
+            // If we hit the root Node and it is empty
+            // we have to look for an alternative best
+            // matching node.
+            if (!h.isEmpty())
+            {
+                reference.set(h);
+                return false;
+            }
+            return true;
+        }
+
+        if (!isBitSet(key, h.bitIndex))
+        {
+            if (selectR(h.left, h.bitIndex, key, reference))
+            {
+                return selectR(h.right, h.bitIndex, key, reference);
+            }
+        }
+        else
+        {
+            if (selectR(h.right, h.bitIndex, key, reference))
+            {
+                return selectR(h.left, h.bitIndex, key, reference);
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     *
+     */
+    private boolean selectR(TrieEntry<K,V> h, int bitIndex,
+                            final K key, final Cursor<? super K, ? super V> cursor,
+                            final Reference<Map.Entry<K, V>> reference)
+    {
+        if (h.bitIndex <= bitIndex)
+        {
+            if (!h.isEmpty())
+            {
+                Decision decision = cursor.select(h);
+                switch(decision)
+                {
+                    case REMOVE:
+                        throw new UnsupportedOperationException("Cannot remove during select");
+
+                    case EXIT:
+                        reference.set(h);
+                        return false; // exit
+
+                    case REMOVE_AND_EXIT:
+                        TrieEntry<K, V> entry = new TrieEntry<>(h.getKey(), h.getValue(), -1);
+                        reference.set(entry);
+                        removeEntry(h);
+                        return false;
+
+                    case CONTINUE:
+                        // fall through.
+                }
+            }
+
+            return true; // continue
+        }
+
+        if (!isBitSet(key, h.bitIndex))
+        {
+            if (selectR(h.left, h.bitIndex, key, cursor, reference))
+            {
+                return selectR(h.right, h.bitIndex, key, cursor, reference);
+            }
+        }
+        else
+        {
+            if (selectR(h.right, h.bitIndex, key, cursor, reference))
+            {
+                return selectR(h.left, h.bitIndex, key, cursor, reference);
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public Map.Entry<K, V> traverse(Cursor<? super K, ? super V> cursor)
+    {
+        TrieEntry<K, V> entry = nextEntry(null);
+        while (entry != null)
+        {
+            TrieEntry<K, V> current = entry;
+
+            Decision decision = cursor.select(current);
+            entry = nextEntry(current);
+
+            switch(decision)
+            {
+                case EXIT:
+                    return current;
+
+                case REMOVE:
+                    removeEntry(current);
+                    break; // out of switch, stay in while loop
+
+                case REMOVE_AND_EXIT:
+                    Map.Entry<K, V> value = new TrieEntry<>(current.getKey(), current.getValue(), -1);
+                    removeEntry(current);
+                    return value;
+
+                case CONTINUE: // do nothing.
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public boolean containsKey(Object k)
+    {
+        if (k == null)
+            return false;
+
+        K key = Tries.cast(k);
+        TrieEntry<K, V> entry = getNearestEntryForKey(key);
+        return !entry.isEmpty() && compareKeys(key, entry.key);
+    }
+
+    @Override
+    public Set<Map.Entry<K,V>> entrySet()
+    {
+        if (entrySet == null)
+            entrySet = new EntrySet();
+
+        return entrySet;
+    }
+
+    @Override
+    public Set<K> keySet()
+    {
+        if (keySet == null)
+            keySet = new KeySet();
+        return keySet;
+    }
+
+    @Override
+    public Collection<V> values()
+    {
+        if (values == null)
+            values = new Values();
+        return values;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws ClassCastException if provided key is of an incompatible type
+     */
+    @Override
+    public V remove(Object k)
+    {
+        if (k == null)
+            return null;
+
+        K key = Tries.cast(k);
+        TrieEntry<K, V> current = root.left;
+        TrieEntry<K, V> path = root;
+        while (true)
+        {
+            if (current.bitIndex <= path.bitIndex)
+            {
+                if (!current.isEmpty() && compareKeys(key, current.key))
+                {
+                    return removeEntry(current);
+                }
+                else
+                {
+                    return null;
+                }
+            }
+
+            path = current;
+            current = !isBitSet(key, current.bitIndex) ? current.left : current.right;
+        }
+    }
+
+    /**
+     * Returns the nearest entry for a given key.  This is useful
+     * for finding knowing if a given key exists (and finding the value
+     * for it), or for inserting the key.
+     *
+     * The actual get implementation. This is very similar to
+     * selectR but with the exception that it might return the
+     * root Entry even if it's empty.
+     */
+    TrieEntry<K, V> getNearestEntryForKey(K key)
+    {
+        TrieEntry<K, V> current = root.left;
+        TrieEntry<K, V> path = root;
+
+        while(true)
+        {
+            if (current.bitIndex <= path.bitIndex)
+                return current;
+
+            path = current;
+            current = !isBitSet(key, current.bitIndex) ? current.left : current.right;
+        }
+    }
+
+    /**
+     * Removes a single entry from the {@link Trie}.
+     *
+     * If we found a Key (Entry h) then figure out if it's
+     * an internal (hard to remove) or external Entry (easy
+     * to remove)
+     */
+    V removeEntry(TrieEntry<K, V> h)
+    {
+        if (h != root)
+        {
+            if (h.isInternalNode())
+            {
+                removeInternalEntry(h);
+            }
+            else
+            {
+                removeExternalEntry(h);
+            }
+        }
+
+        decrementSize();
+        return h.setKeyValue(null, null);
+    }
+
+    /**
+     * Removes an external entry from the {@link Trie}.
+     *
+     * If it's an external Entry then just remove it.
+     * This is very easy and straight forward.
+     */
+    private void removeExternalEntry(TrieEntry<K, V> h)
+    {
+        if (h == root)
+        {
+            throw new IllegalArgumentException("Cannot delete root Entry!");
+        }
+        else if (!h.isExternalNode())
+        {
+            throw new IllegalArgumentException(h + " is not an external Entry!");
+        }
+
+        TrieEntry<K, V> parent = h.parent;
+        TrieEntry<K, V> child = (h.left == h) ? h.right : h.left;
+
+        if (parent.left == h)
+        {
+            parent.left = child;
+        }
+        else
+        {
+            parent.right = child;
+        }
+
+        // either the parent is changing, or the predecessor is changing.
+        if (child.bitIndex > parent.bitIndex)
+        {
+            child.parent = parent;
+        }
+        else
+        {
+            child.predecessor = parent;
+        }
+
+    }
+
+    /**
+     * Removes an internal entry from the {@link Trie}.
+     *
+     * If it's an internal Entry then "good luck" with understanding
+     * this code. The Idea is essentially that Entry p takes Entry h's
+     * place in the trie which requires some re-wiring.
+     */
+    private void removeInternalEntry(TrieEntry<K, V> h)
+    {
+        if (h == root)
+        {
+            throw new IllegalArgumentException("Cannot delete root Entry!");
+        }
+        else if (!h.isInternalNode())
+        {
+            throw new IllegalArgumentException(h + " is not an internal Entry!");
+        }
+
+        TrieEntry<K, V> p = h.predecessor;
+
+        // Set P's bitIndex
+        p.bitIndex = h.bitIndex;
+
+        // Fix P's parent, predecessor and child Nodes
+        {
+            TrieEntry<K, V> parent = p.parent;
+            TrieEntry<K, V> child = (p.left == h) ? p.right : p.left;
+
+            // if it was looping to itself previously,
+            // it will now be pointed from it's parent
+            // (if we aren't removing it's parent --
+            //  in that case, it remains looping to itself).
+            // otherwise, it will continue to have the same
+            // predecessor.
+            if (p.predecessor == p && p.parent != h)
+                p.predecessor = p.parent;
+
+            if (parent.left == p)
+            {
+                parent.left = child;
+            }
+            else
+            {
+                parent.right = child;
+            }
+
+            if (child.bitIndex > parent.bitIndex)
+            {
+                child.parent = parent;
+            }
+        }
+
+        // Fix H's parent and child Nodes
+        {
+            // If H is a parent of its left and right child
+            // then change them to P
+            if (h.left.parent == h)
+                h.left.parent = p;
+
+            if (h.right.parent == h)
+                h.right.parent = p;
+
+            // Change H's parent
+            if (h.parent.left == h)
+            {
+                h.parent.left = p;
+            }
+            else
+            {
+                h.parent.right = p;
+            }
+        }
+
+        // Copy the remaining fields from H to P
+        //p.bitIndex = h.bitIndex;
+        p.parent = h.parent;
+        p.left = h.left;
+        p.right = h.right;
+
+        // Make sure that if h was pointing to any uplinks,
+        // p now points to them.
+        if (isValidUplink(p.left, p))
+            p.left.predecessor = p;
+
+        if (isValidUplink(p.right, p))
+            p.right.predecessor = p;
+    }
+
+    /**
+     * Returns the entry lexicographically after the given entry.
+     * If the given entry is null, returns the first node.
+     */
+    TrieEntry<K, V> nextEntry(TrieEntry<K, V> node)
+    {
+        return (node == null) ? firstEntry() : nextEntryImpl(node.predecessor, node, null);
+    }
+
+    /**
+     * Scans for the next node, starting at the specified point, and using 'previous'
+     * as a hint that the last node we returned was 'previous' (so we know not to return
+     * it again).  If 'tree' is non-null, this will limit the search to the given tree.
+     *
+     * The basic premise is that each iteration can follow the following steps:
+     *
+     * 1) Scan all the way to the left.
+     *   a) If we already started from this node last time, proceed to Step 2.
+     *   b) If a valid uplink is found, use it.
+     *   c) If the result is an empty node (root not set), break the scan.
+     *   d) If we already returned the left node, break the scan.
+     *
+     * 2) Check the right.
+     *   a) If we already returned the right node, proceed to Step 3.
+     *   b) If it is a valid uplink, use it.
+     *   c) Do Step 1 from the right node.
+     *
+     * 3) Back up through the parents until we encounter find a parent
+     *    that we're not the right child of.
+     *
+     * 4) If there's no right child of that parent, the iteration is finished.
+     *    Otherwise continue to Step 5.
+     *
+     * 5) Check to see if the right child is a valid uplink.
+     *    a) If we already returned that child, proceed to Step 6.
+     *       Otherwise, use it.
+     *
+     * 6) If the right child of the parent is the parent itself, we've
+     *    already found & returned the end of the Trie, so exit.
+     *
+     * 7) Do Step 1 on the parent's right child.
+     */
+    TrieEntry<K, V> nextEntryImpl(TrieEntry<K, V> start, TrieEntry<K, V> previous, TrieEntry<K, V> tree)
+    {
+        TrieEntry<K, V> current = start;
+
+        // Only look at the left if this was a recursive or
+        // the first check, otherwise we know we've already looked
+        // at the left.
+        if (previous == null || start != previous.predecessor)
+        {
+            while (!current.left.isEmpty())
+            {
+                // stop traversing if we've already
+                // returned the left of this node.
+                if (previous == current.left)
+                    break;
+
+                if (isValidUplink(current.left, current))
+                    return current.left;
+
+                current = current.left;
+            }
+        }
+
+        // If there's no data at all, exit.
+        if (current.isEmpty())
+            return null;
+
+        // If we've already returned the left,
+        // and the immediate right is null,
+        // there's only one entry in the Trie
+        // which is stored at the root.
+        //
+        //  / ("")   <-- root
+        //  \_/  \
+        //       null <-- 'current'
+        //
+        if (current.right == null)
+            return null;
+
+        // If nothing valid on the left, try the right.
+        if (previous != current.right)
+        {
+            // See if it immediately is valid.
+            if (isValidUplink(current.right, current))
+                return current.right;
+
+            // Must search on the right's side if it wasn't initially valid.
+            return nextEntryImpl(current.right, previous, tree);
+        }
+
+        // Neither left nor right are valid, find the first parent
+        // whose child did not come from the right & traverse it.
+        while (current == current.parent.right)
+        {
+            // If we're going to traverse to above the subtree, stop.
+            if (current == tree)
+                return null;
+
+            current = current.parent;
+        }
+
+        // If we're on the top of the subtree, we can't go any higher.
+        if (current == tree)
+            return null;
+
+        // If there's no right, the parent must be root, so we're done.
+        if (current.parent.right == null)
+            return null;
+
+        // If the parent's right points to itself, we've found one.
+        if (previous != current.parent.right && isValidUplink(current.parent.right, current.parent))
+            return current.parent.right;
+
+        // If the parent's right is itself, there can't be any more nodes.
+        if (current.parent.right == current.parent)
+            return null;
+
+        // We need to traverse down the parent's right's path.
+        return nextEntryImpl(current.parent.right, previous, tree);
+    }
+
+    /**
+     * Returns the first entry the {@link Trie} is storing.
+     *
+     * This is implemented by going always to the left until
+     * we encounter a valid uplink. That uplink is the first key.
+     */
+    TrieEntry<K, V> firstEntry()
+    {
+        // if Trie is empty, no first node.
+        return isEmpty() ? null : followLeft(root);
+    }
+
+    /**
+     * Goes left through the tree until it finds a valid node.
+     */
+    TrieEntry<K, V> followLeft(TrieEntry<K, V> node)
+    {
+        while(true)
+        {
+            TrieEntry<K, V> child = node.left;
+            // if we hit root and it didn't have a node, go right instead.
+            if (child.isEmpty())
+                child = node.right;
+
+            if (child.bitIndex <= node.bitIndex)
+                return child;
+
+            node = child;
+        }
+    }
+
+    /**
+     * Returns true if 'next' is a valid uplink coming from 'from'.
+     */
+    static boolean isValidUplink(TrieEntry<?, ?> next, TrieEntry<?, ?> from)
+    {
+        return next != null && next.bitIndex <= from.bitIndex && !next.isEmpty();
+    }
+
+    /**
+     * A {@link Reference} allows us to return something through a Method's
+     * argument list. An alternative would be to an Array with a length of
+     * one (1) but that leads to compiler warnings. Computationally and memory
+     * wise there's no difference (except for the need to load the
+     * {@link Reference} Class but that happens only once).
+     */
+    private static class Reference<E>
+    {
+
+        private E item;
+
+        public void set(E item)
+        {
+            this.item = item;
+        }
+
+        public E get()
+        {
+            return item;
+        }
+    }
+
+    /**
+     *  A {@link Trie} is a set of {@link TrieEntry} nodes
+     */
+    static class TrieEntry<K,V> extends BasicEntry<K, V>
+    {
+
+        private static final long serialVersionUID = 4596023148184140013L;
+
+        /** The index this entry is comparing. */
+        protected int bitIndex;
+
+        /** The parent of this entry. */
+        protected TrieEntry<K,V> parent;
+
+        /** The left child of this entry. */
+        protected TrieEntry<K,V> left;
+
+        /** The right child of this entry. */
+        protected TrieEntry<K,V> right;
+
+        /** The entry who uplinks to this entry. */
+        protected TrieEntry<K,V> predecessor;
+
+        public TrieEntry(K key, V value, int bitIndex)
+        {
+            super(key, value);
+
+            this.bitIndex = bitIndex;
+
+            this.parent = null;
+            this.left = this;
+            this.right = null;
+            this.predecessor = this;
+        }
+
+        /**
+         * Whether or not the entry is storing a key.
+         * Only the root can potentially be empty, all other
+         * nodes must have a key.
+         */
+        public boolean isEmpty()
+        {
+            return key == null;
+        }
+
+        /**
+         * Neither the left nor right child is a loopback
+         */
+        public boolean isInternalNode()
+        {
+            return left != this && right != this;
+        }
+
+        /**
+         * Either the left or right child is a loopback
+         */
+        public boolean isExternalNode()
+        {
+            return !isInternalNode();
+        }
+    }
+
+
+    /**
+     * This is a entry set view of the {@link Trie} as returned
+     * by {@link Map#entrySet()}
+     */
+    private class EntrySet extends AbstractSet<Map.Entry<K,V>>
+    {
+        @Override
+        public Iterator<Map.Entry<K,V>> iterator()
+        {
+            return new EntryIterator();
+        }
+
+        @Override
+        public boolean contains(Object o)
+        {
+            if (!(o instanceof Map.Entry))
+                return false;
+
+            TrieEntry<K,V> candidate = getEntry(((Map.Entry<?, ?>)o).getKey());
+            return candidate != null && candidate.equals(o);
+        }
+
+        @Override
+        public boolean remove(Object o)
+        {
+            int size = size();
+            AbstractPatriciaTrie.this.remove(o);
+            return size != size();
+        }
+
+        @Override
+        public int size()
+        {
+            return AbstractPatriciaTrie.this.size();
+        }
+
+        @Override
+        public void clear()
+        {
+            AbstractPatriciaTrie.this.clear();
+        }
+
+        /**
+         * An {@link Iterator} that returns {@link Entry} Objects
+         */
+        private class EntryIterator extends TrieIterator<Map.Entry<K,V>>
+        {
+            @Override
+            public Map.Entry<K,V> next()
+            {
+                return nextEntry();
+            }
+        }
+    }
+
+    /**
+     * This is a key set view of the {@link Trie} as returned
+     * by {@link Map#keySet()}
+     */
+    private class KeySet extends AbstractSet<K>
+    {
+        @Override
+        public Iterator<K> iterator()
+        {
+            return new KeyIterator();
+        }
+
+        @Override
+        public int size()
+        {
+            return AbstractPatriciaTrie.this.size();
+        }
+
+        @Override
+        public boolean contains(Object o)
+        {
+            return containsKey(o);
+        }
+
+        @Override
+        public boolean remove(Object o)
+        {
+            int size = size();
+            AbstractPatriciaTrie.this.remove(o);
+            return size != size();
+        }
+
+        @Override
+        public void clear()
+        {
+            AbstractPatriciaTrie.this.clear();
+        }
+
+        /**
+         * An {@link Iterator} that returns Key Objects
+         */
+        private class KeyIterator extends TrieIterator<K>
+        {
+            @Override
+            public K next()
+            {
+                return nextEntry().getKey();
+            }
+        }
+    }
+
+    /**
+     * This is a value view of the {@link Trie} as returned
+     * by {@link Map#values()}
+     */
+    private class Values extends AbstractCollection<V>
+    {
+        @Override
+        public Iterator<V> iterator()
+        {
+            return new ValueIterator();
+        }
+
+        @Override
+        public int size()
+        {
+            return AbstractPatriciaTrie.this.size();
+        }
+
+        @Override
+        public boolean contains(Object o)
+        {
+            return containsValue(o);
+        }
+
+        @Override
+        public void clear()
+        {
+            AbstractPatriciaTrie.this.clear();
+        }
+
+        @Override
+        public boolean remove(Object o)
+        {
+            for (Iterator<V> it = iterator(); it.hasNext(); )
+            {
+                V value = it.next();
+                if (Tries.areEqual(value, o))
+                {
+                    it.remove();
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * An {@link Iterator} that returns Value Objects
+         */
+        private class ValueIterator extends TrieIterator<V>
+        {
+            @Override
+            public V next()
+            {
+                return nextEntry().getValue();
+            }
+        }
+    }
+
+    /**
+     * An iterator for the entries.
+     */
+    abstract class TrieIterator<E> implements Iterator<E>
+    {
+        /**
+         * For fast-fail
+         */
+        protected int expectedModCount = AbstractPatriciaTrie.this.modCount;
+
+        protected TrieEntry<K, V> next; // the next node to return
+        protected TrieEntry<K, V> current; // the current entry we're on
+
+        /**
+         * Starts iteration from the root
+         */
+        protected TrieIterator()
+        {
+            next = AbstractPatriciaTrie.this.nextEntry(null);
+        }
+
+        /**
+         * Starts iteration at the given entry
+         */
+        protected TrieIterator(TrieEntry<K, V> firstEntry)
+        {
+            next = firstEntry;
+        }
+
+        /**
+         * Returns the next {@link TrieEntry}
+         */
+        protected TrieEntry<K,V> nextEntry()
+        {
+            if (expectedModCount != AbstractPatriciaTrie.this.modCount)
+                throw new ConcurrentModificationException();
+
+            TrieEntry<K,V> e = next;
+            if (e == null)
+                throw new NoSuchElementException();
+
+            next = findNext(e);
+            current = e;
+            return e;
+        }
+
+        /**
+         * @see PatriciaTrie#nextEntry(TrieEntry)
+         */
+        protected TrieEntry<K, V> findNext(TrieEntry<K, V> prior)
+        {
+            return AbstractPatriciaTrie.this.nextEntry(prior);
+        }
+
+        @Override
+        public boolean hasNext()
+        {
+            return next != null;
+        }
+
+        @Override
+        public void remove()
+        {
+            if (current == null)
+                throw new IllegalStateException();
+
+            if (expectedModCount != AbstractPatriciaTrie.this.modCount)
+                throw new ConcurrentModificationException();
+
+            TrieEntry<K, V> node = current;
+            current = null;
+            AbstractPatriciaTrie.this.removeEntry(node);
+
+            expectedModCount = AbstractPatriciaTrie.this.modCount;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/AbstractTrie.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/AbstractTrie.java
new file mode 100644
index 0000000..f24ec14
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/trie/AbstractTrie.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2005-2010 Roger Kapsi, Sam Berlin
+ *
+ *   Licensed 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.index.sasi.utils.trie;
+
+import java.io.Serializable;
+import java.util.AbstractMap;
+import java.util.Map;
+
+/**
+ * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified
+ * to correspond to Cassandra code style, as the only Patricia Trie implementation,
+ * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based
+ * on rkapsi/patricia-trie project) only supports String keys)
+ * but unfortunately is not deployed to the maven central as a downloadable artifact.
+ */
+
+/**
+ * This class provides some basic {@link Trie} functionality and
+ * utility methods for actual {@link Trie} implementations.
+ */
+abstract class AbstractTrie<K, V> extends AbstractMap<K, V> implements Serializable, Trie<K, V>
+{
+    private static final long serialVersionUID = -6358111100045408883L;
+
+    /**
+     * The {@link KeyAnalyzer} that's being used to build the
+     * PATRICIA {@link Trie}
+     */
+    protected final KeyAnalyzer<? super K> keyAnalyzer;
+
+    /**
+     * Constructs a new {@link Trie} using the given {@link KeyAnalyzer}
+     */
+    public AbstractTrie(KeyAnalyzer<? super K> keyAnalyzer)
+    {
+        this.keyAnalyzer = Tries.notNull(keyAnalyzer, "keyAnalyzer");
+    }
+
+    @Override
+    public K selectKey(K key)
+    {
+        Map.Entry<K, V> entry = select(key);
+        return entry != null ? entry.getKey() : null;
+    }
+
+    @Override
+    public V selectValue(K key)
+    {
+        Map.Entry<K, V> entry = select(key);
+        return entry != null ? entry.getValue() : null;
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder();
+        buffer.append("Trie[").append(size()).append("]={\n");
+        for (Map.Entry<K, V> entry : entrySet())
+        {
+            buffer.append("  ").append(entry).append("\n");
+        }
+        buffer.append("}\n");
+        return buffer.toString();
+    }
+
+    /**
+     * Returns the length of the given key in bits
+     *
+     * @see KeyAnalyzer#lengthInBits(Object)
+     */
+    final int lengthInBits(K key)
+    {
+        return key == null ? 0 : keyAnalyzer.lengthInBits(key);
+    }
+
+    /**
+     * Returns whether or not the given bit on the
+     * key is set or false if the key is null.
+     *
+     * @see KeyAnalyzer#isBitSet(Object, int)
+     */
+    final boolean isBitSet(K key, int bitIndex)
+    {
+        return key != null && keyAnalyzer.isBitSet(key, bitIndex);
+    }
+
+    /**
+     * Utility method for calling {@link KeyAnalyzer#bitIndex(Object, Object)}
+     */
+    final int bitIndex(K key, K otherKey)
+    {
+        if (key != null && otherKey != null)
+        {
+            return keyAnalyzer.bitIndex(key, otherKey);
+        }
+        else if (key != null)
+        {
+            return bitIndex(key);
+        }
+        else if (otherKey != null)
+        {
+            return bitIndex(otherKey);
+        }
+
+        return KeyAnalyzer.NULL_BIT_KEY;
+    }
+
+    private int bitIndex(K key)
+    {
+        int lengthInBits = lengthInBits(key);
+        for (int i = 0; i < lengthInBits; i++)
+        {
+            if (isBitSet(key, i))
+                return i;
+        }
+
+        return KeyAnalyzer.NULL_BIT_KEY;
+    }
+
+    /**
+     * An utility method for calling {@link KeyAnalyzer#compare(Object, Object)}
+     */
+    final boolean compareKeys(K key, K other)
+    {
+        if (key == null)
+        {
+            return (other == null);
+        }
+        else if (other == null)
+        {
+            return false;
+        }
+
+        return keyAnalyzer.compare(key, other) == 0;
+    }
+
+    /**
+     * A basic implementation of {@link Entry}
+     */
+    abstract static class BasicEntry<K, V> implements Map.Entry<K, V>, Serializable
+    {
+        private static final long serialVersionUID = -944364551314110330L;
+
+        protected K key;
+
+        protected V value;
+
+        private transient int hashCode = 0;
+
+        public BasicEntry(K key, V value)
+        {
+            this.key = key;
+            this.value = value;
+        }
+
+        /**
+         * Replaces the current key and value with the provided
+         * key &amp; value
+         */
+        public V setKeyValue(K key, V value)
+        {
+            this.key = key;
+            this.hashCode = 0;
+            return setValue(value);
+        }
+
+        @Override
+        public K getKey()
+        {
+            return key;
+        }
+
+        @Override
+        public V getValue()
+        {
+            return value;
+        }
+
+        @Override
+        public V setValue(V value)
+        {
+            V previous = this.value;
+            this.value = value;
+            return previous;
+        }
+
+        @Override
+        public int hashCode()
+        {
+            if (hashCode == 0)
+                hashCode = (key != null ? key.hashCode() : 0);
+            return hashCode;
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (o == this)
+            {
+                return true;
+            }
+            else if (!(o instanceof Map.Entry<?, ?>))
+            {
+                return false;
+            }
+
+            Map.Entry<?, ?> other = (Map.Entry<?, ?>)o;
+            return Tries.areEqual(key, other.getKey()) && Tries.areEqual(value, other.getValue());
+        }
+
+        @Override
+        public String toString()
+        {
+            return key + "=" + value;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/Cursor.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/Cursor.java
new file mode 100644
index 0000000..dde3c8a
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/trie/Cursor.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2005-2010 Roger Kapsi, Sam Berlin
+ *
+ *   Licensed 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.index.sasi.utils.trie;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified
+ * to correspond to Cassandra code style, as the only Patricia Trie implementation,
+ * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based
+ * on rkapsi/patricia-trie project) only supports String keys)
+ * but unfortunately is not deployed to the maven central as a downloadable artifact.
+ */
+
+/**
+ * A {@link Cursor} can be used to traverse a {@link Trie}, visit each node
+ * step by step and make {@link Decision}s on each step how to continue with
+ * traversing the {@link Trie}.
+ */
+public interface Cursor<K, V>
+{
+
+    /**
+     * The {@link Decision} tells the {@link Cursor} what to do on each step
+     * while traversing the {@link Trie}.
+     *
+     * NOTE: Not all operations that work with a {@link Cursor} support all
+     * {@link Decision} types
+     */
+    enum Decision
+    {
+
+        /**
+         * Exit the traverse operation
+         */
+        EXIT,
+
+        /**
+         * Continue with the traverse operation
+         */
+        CONTINUE,
+
+        /**
+         * Remove the previously returned element
+         * from the {@link Trie} and continue
+         */
+        REMOVE,
+
+        /**
+         * Remove the previously returned element
+         * from the {@link Trie} and exit from the
+         * traverse operation
+         */
+        REMOVE_AND_EXIT
+    }
+
+    /**
+     * Called for each {@link Entry} in the {@link Trie}. Return
+     * {@link Decision#EXIT} to finish the {@link Trie} operation,
+     * {@link Decision#CONTINUE} to go to the next {@link Entry},
+     * {@link Decision#REMOVE} to remove the {@link Entry} and
+     * continue iterating or {@link Decision#REMOVE_AND_EXIT} to
+     * remove the {@link Entry} and stop iterating.
+     *
+     * Note: Not all operations support {@link Decision#REMOVE}.
+     */
+    Decision select(Map.Entry<? extends K, ? extends V> entry);
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/KeyAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/KeyAnalyzer.java
new file mode 100644
index 0000000..c6ef665
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/trie/KeyAnalyzer.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010 Roger Kapsi
+ *
+ *   Licensed 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.index.sasi.utils.trie;
+
+import java.util.Comparator;
+
+/**
+ * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified
+ * to correspond to Cassandra code style, as the only Patricia Trie implementation,
+ * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based
+ * on rkapsi/patricia-trie project) only supports String keys)
+ * but unfortunately is not deployed to the maven central as a downloadable artifact.
+ */
+
+/**
+ * The {@link KeyAnalyzer} provides bit-level access to keys
+ * for the {@link PatriciaTrie}.
+ */
+public interface KeyAnalyzer<K> extends Comparator<K>
+{
+    /**
+     * Returned by {@link #bitIndex(Object, Object)} if a key's
+     * bits were all zero (0).
+     */
+    int NULL_BIT_KEY = -1;
+
+    /**
+     * Returned by {@link #bitIndex(Object, Object)} if a the
+     * bits of two keys were all equal.
+     */
+    int EQUAL_BIT_KEY = -2;
+
+    /**
+     * Returned by {@link #bitIndex(Object, Object)} if a keys
+     * indices are out of bounds.
+     */
+    int OUT_OF_BOUNDS_BIT_KEY = -3;
+
+    /**
+     * Returns the key's length in bits.
+     */
+    int lengthInBits(K key);
+
+    /**
+     * Returns {@code true} if a key's bit it set at the given index.
+     */
+    boolean isBitSet(K key, int bitIndex);
+
+    /**
+     * Returns the index of the first bit that is different in the two keys.
+     */
+    int bitIndex(K key, K otherKey);
+
+    /**
+     * Returns {@code true} if the second argument is a
+     * prefix of the first argument.
+     */
+    boolean isPrefix(K key, K prefix);
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/PatriciaTrie.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/PatriciaTrie.java
new file mode 100644
index 0000000..9187894
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/trie/PatriciaTrie.java
@@ -0,0 +1,1261 @@
+/*
+ * Copyright 2005-2010 Roger Kapsi, Sam Berlin
+ *
+ *   Licensed 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.index.sasi.utils.trie;
+
+import java.io.Serializable;
+import java.util.*;
+
+/**
+ * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified
+ * to correspond to Cassandra code style, as the only Patricia Trie implementation,
+ * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based
+ * on rkapsi/patricia-trie project) only supports String keys)
+ * but unfortunately is not deployed to the maven central as a downloadable artifact.
+ */
+
+/**
+ * <h3>PATRICIA {@link Trie}</h3>
+ *
+ * <i>Practical Algorithm to Retrieve Information Coded in Alphanumeric</i>
+ *
+ * <p>A PATRICIA {@link Trie} is a compressed {@link Trie}. Instead of storing
+ * all data at the edges of the {@link Trie} (and having empty internal nodes),
+ * PATRICIA stores data in every node. This allows for very efficient traversal,
+ * insert, delete, predecessor, successor, prefix, range, and {@link #select(Object)}
+ * operations. All operations are performed at worst in O(K) time, where K
+ * is the number of bits in the largest item in the tree. In practice,
+ * operations actually take O(A(K)) time, where A(K) is the average number of
+ * bits of all items in the tree.
+ *
+ * <p>Most importantly, PATRICIA requires very few comparisons to keys while
+ * doing any operation. While performing a lookup, each comparison (at most
+ * K of them, described above) will perform a single bit comparison against
+ * the given key, instead of comparing the entire key to another key.
+ *
+ * <p>The {@link Trie} can return operations in lexicographical order using the
+ * {@link #traverse(Cursor)}, 'prefix', 'submap', or 'iterator' methods. The
+ * {@link Trie} can also scan for items that are 'bitwise' (using an XOR
+ * metric) by the 'select' method. Bitwise closeness is determined by the
+ * {@link KeyAnalyzer} returning true or false for a bit being set or not in
+ * a given key.
+ *
+ * <p>Any methods here that take an {@link Object} argument may throw a
+ * {@link ClassCastException} if the method is expecting an instance of K
+ * and it isn't K.
+ *
+ * @see <a href="http://en.wikipedia.org/wiki/Radix_tree">Radix Tree</a>
+ * @see <a href="http://www.csse.monash.edu.au/~lloyd/tildeAlgDS/Tree/PATRICIA">PATRICIA</a>
+ * @see <a href="http://www.imperialviolet.org/binary/critbit.pdf">Crit-Bit Tree</a>
+ *
+ * @author Roger Kapsi
+ * @author Sam Berlin
+ */
+public class PatriciaTrie<K, V> extends AbstractPatriciaTrie<K, V> implements Serializable
+{
+    private static final long serialVersionUID = -2246014692353432660L;
+
+    public PatriciaTrie(KeyAnalyzer<? super K> keyAnalyzer)
+    {
+        super(keyAnalyzer);
+    }
+
+    public PatriciaTrie(KeyAnalyzer<? super K> keyAnalyzer, Map<? extends K, ? extends V> m)
+    {
+        super(keyAnalyzer, m);
+    }
+
+    @Override
+    public Comparator<? super K> comparator()
+    {
+        return keyAnalyzer;
+    }
+
+    @Override
+    public SortedMap<K, V> prefixMap(K prefix)
+    {
+        return lengthInBits(prefix) == 0 ? this : new PrefixRangeMap(prefix);
+    }
+
+    @Override
+    public K firstKey()
+    {
+        return firstEntry().getKey();
+    }
+
+    @Override
+    public K lastKey()
+    {
+        TrieEntry<K, V> entry = lastEntry();
+        return entry != null ? entry.getKey() : null;
+    }
+
+    @Override
+    public SortedMap<K, V> headMap(K toKey)
+    {
+        return new RangeEntryMap(null, toKey);
+    }
+
+    @Override
+    public SortedMap<K, V> subMap(K fromKey, K toKey)
+    {
+        return new RangeEntryMap(fromKey, toKey);
+    }
+
+    @Override
+    public SortedMap<K, V> tailMap(K fromKey)
+    {
+        return new RangeEntryMap(fromKey, null);
+    }
+
+    /**
+     * Returns an entry strictly higher than the given key,
+     * or null if no such entry exists.
+     */
+    private TrieEntry<K,V> higherEntry(K key)
+    {
+        // TODO: Cleanup so that we don't actually have to add/remove from the
+        //       tree.  (We do it here because there are other well-defined
+        //       functions to perform the search.)
+        int lengthInBits = lengthInBits(key);
+
+        if (lengthInBits == 0)
+        {
+            if (!root.isEmpty())
+            {
+                // If data in root, and more after -- return it.
+                return size() > 1 ? nextEntry(root) : null;
+            }
+            else
+            {
+                // Root is empty & we want something after empty, return first.
+                return firstEntry();
+            }
+        }
+
+        TrieEntry<K, V> found = getNearestEntryForKey(key);
+        if (compareKeys(key, found.key))
+            return nextEntry(found);
+
+        int bitIndex = bitIndex(key, found.key);
+        if (Tries.isValidBitIndex(bitIndex))
+        {
+            return replaceCeil(key, bitIndex);
+        }
+        else if (Tries.isNullBitKey(bitIndex))
+        {
+            if (!root.isEmpty())
+            {
+                return firstEntry();
+            }
+            else if (size() > 1)
+            {
+                return nextEntry(firstEntry());
+            }
+            else
+            {
+                return null;
+            }
+        }
+        else if (Tries.isEqualBitKey(bitIndex))
+        {
+            return nextEntry(found);
+        }
+
+        // we should have exited above.
+        throw new IllegalStateException("invalid lookup: " + key);
+    }
+
+    /**
+     * Returns a key-value mapping associated with the least key greater
+     * than or equal to the given key, or null if there is no such key.
+     */
+    TrieEntry<K,V> ceilingEntry(K key)
+    {
+        // Basically:
+        // Follow the steps of adding an entry, but instead...
+        //
+        // - If we ever encounter a situation where we found an equal
+        //   key, we return it immediately.
+        //
+        // - If we hit an empty root, return the first iterable item.
+        //
+        // - If we have to add a new item, we temporarily add it,
+        //   find the successor to it, then remove the added item.
+        //
+        // These steps ensure that the returned value is either the
+        // entry for the key itself, or the first entry directly after
+        // the key.
+
+        // TODO: Cleanup so that we don't actually have to add/remove from the
+        //       tree.  (We do it here because there are other well-defined
+        //       functions to perform the search.)
+        int lengthInBits = lengthInBits(key);
+
+        if (lengthInBits == 0)
+        {
+            if (!root.isEmpty())
+            {
+                return root;
+            }
+            else
+            {
+                return firstEntry();
+            }
+        }
+
+        TrieEntry<K, V> found = getNearestEntryForKey(key);
+        if (compareKeys(key, found.key))
+            return found;
+
+        int bitIndex = bitIndex(key, found.key);
+        if (Tries.isValidBitIndex(bitIndex))
+        {
+            return replaceCeil(key, bitIndex);
+        }
+        else if (Tries.isNullBitKey(bitIndex))
+        {
+            if (!root.isEmpty())
+            {
+                return root;
+            }
+            else
+            {
+                return firstEntry();
+            }
+        }
+        else if (Tries.isEqualBitKey(bitIndex))
+        {
+            return found;
+        }
+
+        // we should have exited above.
+        throw new IllegalStateException("invalid lookup: " + key);
+    }
+
+    private TrieEntry<K, V> replaceCeil(K key, int bitIndex)
+    {
+        TrieEntry<K, V> added = new TrieEntry<>(key, null, bitIndex);
+        addEntry(added);
+        incrementSize(); // must increment because remove will decrement
+        TrieEntry<K, V> ceil = nextEntry(added);
+        removeEntry(added);
+        modCount -= 2; // we didn't really modify it.
+        return ceil;
+    }
+
+    private TrieEntry<K, V> replaceLower(K key, int bitIndex)
+    {
+        TrieEntry<K, V> added = new TrieEntry<>(key, null, bitIndex);
+        addEntry(added);
+        incrementSize(); // must increment because remove will decrement
+        TrieEntry<K, V> prior = previousEntry(added);
+        removeEntry(added);
+        modCount -= 2; // we didn't really modify it.
+        return prior;
+    }
+
+    /**
+     * Returns a key-value mapping associated with the greatest key
+     * strictly less than the given key, or null if there is no such key.
+     */
+    TrieEntry<K,V> lowerEntry(K key)
+    {
+        // Basically:
+        // Follow the steps of adding an entry, but instead...
+        //
+        // - If we ever encounter a situation where we found an equal
+        //   key, we return it's previousEntry immediately.
+        //
+        // - If we hit root (empty or not), return null.
+        //
+        // - If we have to add a new item, we temporarily add it,
+        //   find the previousEntry to it, then remove the added item.
+        //
+        // These steps ensure that the returned value is always just before
+        // the key or null (if there was nothing before it).
+
+        // TODO: Cleanup so that we don't actually have to add/remove from the
+        //       tree.  (We do it here because there are other well-defined
+        //       functions to perform the search.)
+        int lengthInBits = lengthInBits(key);
+
+        if (lengthInBits == 0)
+            return null; // there can never be anything before root.
+
+        TrieEntry<K, V> found = getNearestEntryForKey(key);
+        if (compareKeys(key, found.key))
+            return previousEntry(found);
+
+        int bitIndex = bitIndex(key, found.key);
+        if (Tries.isValidBitIndex(bitIndex))
+        {
+            return replaceLower(key, bitIndex);
+        }
+        else if (Tries.isNullBitKey(bitIndex))
+        {
+            return null;
+        }
+        else if (Tries.isEqualBitKey(bitIndex))
+        {
+            return previousEntry(found);
+        }
+
+        // we should have exited above.
+        throw new IllegalStateException("invalid lookup: " + key);
+    }
+
+    /**
+     * Returns a key-value mapping associated with the greatest key
+     * less than or equal to the given key, or null if there is no such key.
+     */
+    TrieEntry<K,V> floorEntry(K key) {
+        // TODO: Cleanup so that we don't actually have to add/remove from the
+        //       tree.  (We do it here because there are other well-defined
+        //       functions to perform the search.)
+        int lengthInBits = lengthInBits(key);
+
+        if (lengthInBits == 0)
+        {
+            return !root.isEmpty() ? root : null;
+        }
+
+        TrieEntry<K, V> found = getNearestEntryForKey(key);
+        if (compareKeys(key, found.key))
+            return found;
+
+        int bitIndex = bitIndex(key, found.key);
+        if (Tries.isValidBitIndex(bitIndex))
+        {
+            return replaceLower(key, bitIndex);
+        }
+        else if (Tries.isNullBitKey(bitIndex))
+        {
+            if (!root.isEmpty())
+            {
+                return root;
+            }
+            else
+            {
+                return null;
+            }
+        }
+        else if (Tries.isEqualBitKey(bitIndex))
+        {
+            return found;
+        }
+
+        // we should have exited above.
+        throw new IllegalStateException("invalid lookup: " + key);
+    }
+
+    /**
+     * Finds the subtree that contains the prefix.
+     *
+     * This is very similar to getR but with the difference that
+     * we stop the lookup if h.bitIndex > lengthInBits.
+     */
+    private TrieEntry<K, V> subtree(K prefix)
+    {
+        int lengthInBits = lengthInBits(prefix);
+
+        TrieEntry<K, V> current = root.left;
+        TrieEntry<K, V> path = root;
+        while(true)
+        {
+            if (current.bitIndex <= path.bitIndex || lengthInBits < current.bitIndex)
+                break;
+
+            path = current;
+            current = !isBitSet(prefix, current.bitIndex)
+                    ? current.left : current.right;
+        }
+
+        // Make sure the entry is valid for a subtree.
+        TrieEntry<K, V> entry = current.isEmpty() ? path : current;
+
+        // If entry is root, it can't be empty.
+        if (entry.isEmpty())
+            return null;
+
+        // if root && length of root is less than length of lookup,
+        // there's nothing.
+        // (this prevents returning the whole subtree if root has an empty
+        //  string and we want to lookup things with "\0")
+        if (entry == root && lengthInBits(entry.getKey()) < lengthInBits)
+            return null;
+
+        // Found key's length-th bit differs from our key
+        // which means it cannot be the prefix...
+        if (isBitSet(prefix, lengthInBits) != isBitSet(entry.key, lengthInBits))
+            return null;
+
+        // ... or there are less than 'length' equal bits
+        int bitIndex = bitIndex(prefix, entry.key);
+        return (bitIndex >= 0 && bitIndex < lengthInBits) ? null : entry;
+    }
+
+    /**
+     * Returns the last entry the {@link Trie} is storing.
+     *
+     * <p>This is implemented by going always to the right until
+     * we encounter a valid uplink. That uplink is the last key.
+     */
+    private TrieEntry<K, V> lastEntry()
+    {
+        return followRight(root.left);
+    }
+
+    /**
+     * Traverses down the right path until it finds an uplink.
+     */
+    private TrieEntry<K, V> followRight(TrieEntry<K, V> node)
+    {
+        // if Trie is empty, no last entry.
+        if (node.right == null)
+            return null;
+
+        // Go as far right as possible, until we encounter an uplink.
+        while (node.right.bitIndex > node.bitIndex)
+        {
+            node = node.right;
+        }
+
+        return node.right;
+    }
+
+    /**
+     * Returns the node lexicographically before the given node (or null if none).
+     *
+     * This follows four simple branches:
+     *  - If the uplink that returned us was a right uplink:
+     *      - If predecessor's left is a valid uplink from predecessor, return it.
+     *      - Else, follow the right path from the predecessor's left.
+     *  - If the uplink that returned us was a left uplink:
+     *      - Loop back through parents until we encounter a node where
+     *        node != node.parent.left.
+     *          - If node.parent.left is uplink from node.parent:
+     *              - If node.parent.left is not root, return it.
+     *              - If it is root & root isEmpty, return null.
+     *              - If it is root & root !isEmpty, return root.
+     *          - If node.parent.left is not uplink from node.parent:
+     *              - Follow right path for first right child from node.parent.left
+     *
+     * @param start the start entry
+     */
+    private TrieEntry<K, V> previousEntry(TrieEntry<K, V> start)
+    {
+        if (start.predecessor == null)
+            throw new IllegalArgumentException("must have come from somewhere!");
+
+        if (start.predecessor.right == start)
+        {
+            return isValidUplink(start.predecessor.left, start.predecessor)
+                    ? start.predecessor.left
+                    : followRight(start.predecessor.left);
+        }
+
+        TrieEntry<K, V> node = start.predecessor;
+        while (node.parent != null && node == node.parent.left)
+        {
+            node = node.parent;
+        }
+
+        if (node.parent == null) // can be null if we're looking up root.
+            return null;
+
+        if (isValidUplink(node.parent.left, node.parent))
+        {
+            if (node.parent.left == root)
+            {
+                return root.isEmpty() ? null : root;
+            }
+            else
+            {
+                return node.parent.left;
+            }
+        }
+        else
+        {
+            return followRight(node.parent.left);
+        }
+    }
+
+    /**
+     * Returns the entry lexicographically after the given entry.
+     * If the given entry is null, returns the first node.
+     *
+     * This will traverse only within the subtree.  If the given node
+     * is not within the subtree, this will have undefined results.
+     */
+    private TrieEntry<K, V> nextEntryInSubtree(TrieEntry<K, V> node, TrieEntry<K, V> parentOfSubtree)
+    {
+        return (node == null) ? firstEntry() : nextEntryImpl(node.predecessor, node, parentOfSubtree);
+    }
+
+    private boolean isPrefix(K key, K prefix)
+    {
+        return keyAnalyzer.isPrefix(key, prefix);
+    }
+
+    /**
+     * A range view of the {@link Trie}
+     */
+    private abstract class RangeMap extends AbstractMap<K, V> implements SortedMap<K, V>
+    {
+        /**
+         * The {@link #entrySet()} view
+         */
+        private transient volatile Set<Map.Entry<K, V>> entrySet;
+
+        /**
+         * Creates and returns an {@link #entrySet()}
+         * view of the {@link RangeMap}
+         */
+        protected abstract Set<Map.Entry<K, V>> createEntrySet();
+
+        /**
+         * Returns the FROM Key
+         */
+        protected abstract K getFromKey();
+
+        /**
+         * Whether or not the {@link #getFromKey()} is in the range
+         */
+        protected abstract boolean isFromInclusive();
+
+        /**
+         * Returns the TO Key
+         */
+        protected abstract K getToKey();
+
+        /**
+         * Whether or not the {@link #getToKey()} is in the range
+         */
+        protected abstract boolean isToInclusive();
+
+
+        @Override
+        public Comparator<? super K> comparator()
+        {
+            return PatriciaTrie.this.comparator();
+        }
+
+        @Override
+        public boolean containsKey(Object key)
+        {
+            return inRange(Tries.<K>cast(key)) && PatriciaTrie.this.containsKey(key);
+        }
+
+        @Override
+        public V remove(Object key)
+        {
+            return (!inRange(Tries.<K>cast(key))) ? null : PatriciaTrie.this.remove(key);
+        }
+
+        @Override
+        public V get(Object key)
+        {
+            return (!inRange(Tries.<K>cast(key))) ? null : PatriciaTrie.this.get(key);
+        }
+
+        @Override
+        public V put(K key, V value)
+        {
+            if (!inRange(key))
+                throw new IllegalArgumentException("Key is out of range: " + key);
+
+            return PatriciaTrie.this.put(key, value);
+        }
+
+        @Override
+        public Set<Map.Entry<K, V>> entrySet()
+        {
+            if (entrySet == null)
+                entrySet = createEntrySet();
+            return entrySet;
+        }
+
+        @Override
+        public SortedMap<K, V> subMap(K fromKey, K toKey)
+        {
+            if (!inRange2(fromKey))
+                throw new IllegalArgumentException("FromKey is out of range: " + fromKey);
+
+            if (!inRange2(toKey))
+                throw new IllegalArgumentException("ToKey is out of range: " + toKey);
+
+            return createRangeMap(fromKey, isFromInclusive(), toKey, isToInclusive());
+        }
+
+        @Override
+        public SortedMap<K, V> headMap(K toKey)
+        {
+            if (!inRange2(toKey))
+                throw new IllegalArgumentException("ToKey is out of range: " + toKey);
+
+            return createRangeMap(getFromKey(), isFromInclusive(), toKey, isToInclusive());
+        }
+
+        @Override
+        public SortedMap<K, V> tailMap(K fromKey)
+        {
+            if (!inRange2(fromKey))
+                throw new IllegalArgumentException("FromKey is out of range: " + fromKey);
+
+            return createRangeMap(fromKey, isFromInclusive(), getToKey(), isToInclusive());
+        }
+
+        /**
+         * Returns true if the provided key is greater than TO and
+         * less than FROM
+         */
+        protected boolean inRange(K key)
+        {
+            K fromKey = getFromKey();
+            K toKey = getToKey();
+
+            return (fromKey == null || inFromRange(key, false))
+                    && (toKey == null || inToRange(key, false));
+        }
+
+        /**
+         * This form allows the high endpoint (as well as all legit keys)
+         */
+        protected boolean inRange2(K key)
+        {
+            K fromKey = getFromKey();
+            K toKey = getToKey();
+
+            return (fromKey == null || inFromRange(key, false))
+                    && (toKey == null || inToRange(key, true));
+        }
+
+        /**
+         * Returns true if the provided key is in the FROM range
+         * of the {@link RangeMap}
+         */
+        protected boolean inFromRange(K key, boolean forceInclusive)
+        {
+            K fromKey = getFromKey();
+            boolean fromInclusive = isFromInclusive();
+
+            int ret = keyAnalyzer.compare(key, fromKey);
+            return (fromInclusive || forceInclusive) ? ret >= 0 : ret > 0;
+        }
+
+        /**
+         * Returns true if the provided key is in the TO range
+         * of the {@link RangeMap}
+         */
+        protected boolean inToRange(K key, boolean forceInclusive)
+        {
+            K toKey = getToKey();
+            boolean toInclusive = isToInclusive();
+
+            int ret = keyAnalyzer.compare(key, toKey);
+            return (toInclusive || forceInclusive) ? ret <= 0 : ret < 0;
+        }
+
+        /**
+         * Creates and returns a sub-range view of the current {@link RangeMap}
+         */
+        protected abstract SortedMap<K, V> createRangeMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive);
+    }
+
+   /**
+    * A {@link RangeMap} that deals with {@link Entry}s
+    */
+   private class RangeEntryMap extends RangeMap
+   {
+       /**
+        * The key to start from, null if the beginning.
+        */
+       protected final K fromKey;
+
+       /**
+        * The key to end at, null if till the end.
+        */
+       protected final K toKey;
+
+       /**
+        * Whether or not the 'from' is inclusive.
+        */
+       protected final boolean fromInclusive;
+
+       /**
+        * Whether or not the 'to' is inclusive.
+        */
+       protected final boolean toInclusive;
+
+       /**
+        * Creates a {@link RangeEntryMap} with the fromKey included and
+        * the toKey excluded from the range
+        */
+       protected RangeEntryMap(K fromKey, K toKey)
+       {
+           this(fromKey, true, toKey, false);
+       }
+
+       /**
+        * Creates a {@link RangeEntryMap}
+        */
+       protected RangeEntryMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive)
+       {
+           if (fromKey == null && toKey == null)
+               throw new IllegalArgumentException("must have a from or to!");
+
+           if (fromKey != null && toKey != null && keyAnalyzer.compare(fromKey, toKey) > 0)
+               throw new IllegalArgumentException("fromKey > toKey");
+
+           this.fromKey = fromKey;
+           this.fromInclusive = fromInclusive;
+           this.toKey = toKey;
+           this.toInclusive = toInclusive;
+       }
+
+
+       @Override
+       public K firstKey()
+       {
+           Map.Entry<K,V> e  = fromKey == null
+                ? firstEntry()
+                : fromInclusive ? ceilingEntry(fromKey) : higherEntry(fromKey);
+
+           K first = e != null ? e.getKey() : null;
+           if (e == null || toKey != null && !inToRange(first, false))
+               throw new NoSuchElementException();
+
+           return first;
+       }
+
+
+       @Override
+       public K lastKey()
+       {
+           Map.Entry<K,V> e = toKey == null
+                ? lastEntry()
+                : toInclusive ? floorEntry(toKey) : lowerEntry(toKey);
+
+           K last = e != null ? e.getKey() : null;
+           if (e == null || fromKey != null && !inFromRange(last, false))
+               throw new NoSuchElementException();
+
+           return last;
+       }
+
+       @Override
+       protected Set<Entry<K, V>> createEntrySet()
+       {
+           return new RangeEntrySet(this);
+       }
+
+       @Override
+       public K getFromKey()
+       {
+           return fromKey;
+       }
+
+       @Override
+       public K getToKey()
+       {
+           return toKey;
+       }
+
+       @Override
+       public boolean isFromInclusive()
+       {
+           return fromInclusive;
+       }
+
+       @Override
+       public boolean isToInclusive()
+       {
+           return toInclusive;
+       }
+
+       @Override
+       protected SortedMap<K, V> createRangeMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive)
+       {
+           return new RangeEntryMap(fromKey, fromInclusive, toKey, toInclusive);
+       }
+   }
+
+    /**
+     * A {@link Set} view of a {@link RangeMap}
+     */
+    private class RangeEntrySet extends AbstractSet<Map.Entry<K, V>>
+    {
+
+        private final RangeMap delegate;
+
+        private int size = -1;
+
+        private int expectedModCount = -1;
+
+        /**
+         * Creates a {@link RangeEntrySet}
+         */
+        public RangeEntrySet(RangeMap delegate)
+        {
+            if (delegate == null)
+                throw new NullPointerException("delegate");
+
+            this.delegate = delegate;
+        }
+
+        @Override
+        public Iterator<Map.Entry<K, V>> iterator()
+        {
+            K fromKey = delegate.getFromKey();
+            K toKey = delegate.getToKey();
+
+            TrieEntry<K, V> first = fromKey == null ? firstEntry() : ceilingEntry(fromKey);
+            TrieEntry<K, V> last = null;
+            if (toKey != null)
+                last = ceilingEntry(toKey);
+
+            return new EntryIterator(first, last);
+        }
+
+        @Override
+        public int size()
+        {
+            if (size == -1 || expectedModCount != PatriciaTrie.this.modCount)
+            {
+                size = 0;
+
+                for (Iterator<?> it = iterator(); it.hasNext(); it.next())
+                {
+                    ++size;
+                }
+
+                expectedModCount = PatriciaTrie.this.modCount;
+            }
+
+            return size;
+        }
+
+        @Override
+        public boolean isEmpty()
+        {
+            return !iterator().hasNext();
+        }
+
+        @Override
+        public boolean contains(Object o)
+        {
+            if (!(o instanceof Map.Entry<?, ?>))
+                return false;
+
+            @SuppressWarnings("unchecked")
+            Map.Entry<K, V> entry = (Map.Entry<K, V>) o;
+            K key = entry.getKey();
+            if (!delegate.inRange(key))
+                return false;
+
+            TrieEntry<K, V> node = getEntry(key);
+            return node != null && Tries.areEqual(node.getValue(), entry.getValue());
+        }
+
+        @Override
+        public boolean remove(Object o)
+        {
+            if (!(o instanceof Map.Entry<?, ?>))
+                return false;
+
+            @SuppressWarnings("unchecked")
+            Map.Entry<K, V> entry = (Map.Entry<K, V>) o;
+            K key = entry.getKey();
+            if (!delegate.inRange(key))
+                return false;
+
+            TrieEntry<K, V> node = getEntry(key);
+            if (node != null && Tries.areEqual(node.getValue(), entry.getValue()))
+            {
+                removeEntry(node);
+                return true;
+            }
+
+            return false;
+        }
+
+        /**
+         * An {@link Iterator} for {@link RangeEntrySet}s.
+         */
+        private final class EntryIterator extends TrieIterator<Map.Entry<K,V>>
+        {
+            private final K excludedKey;
+
+            /**
+             * Creates a {@link EntryIterator}
+             */
+            private EntryIterator(TrieEntry<K,V> first, TrieEntry<K,V> last)
+            {
+                super(first);
+                this.excludedKey = (last != null ? last.getKey() : null);
+            }
+
+            @Override
+            public boolean hasNext()
+            {
+                return next != null && !Tries.areEqual(next.key, excludedKey);
+            }
+
+            @Override
+            public Map.Entry<K,V> next()
+            {
+                if (next == null || Tries.areEqual(next.key, excludedKey))
+                    throw new NoSuchElementException();
+
+                return nextEntry();
+            }
+        }
+    }
+
+    /**
+     * A submap used for prefix views over the {@link Trie}.
+     */
+    private class PrefixRangeMap extends RangeMap
+    {
+
+        private final K prefix;
+
+        private K fromKey = null;
+
+        private K toKey = null;
+
+        private int expectedModCount = -1;
+
+        private int size = -1;
+
+        /**
+         * Creates a {@link PrefixRangeMap}
+         */
+        private PrefixRangeMap(K prefix)
+        {
+            this.prefix = prefix;
+        }
+
+        /**
+         * This method does two things. It determinates the FROM
+         * and TO range of the {@link PrefixRangeMap} and the number
+         * of elements in the range. This method must be called every
+         * time the {@link Trie} has changed.
+         */
+        private int fixup()
+        {
+            // The trie has changed since we last
+            // found our toKey / fromKey
+            if (size == - 1 || PatriciaTrie.this.modCount != expectedModCount)
+            {
+                Iterator<Map.Entry<K, V>> it = entrySet().iterator();
+                size = 0;
+
+                Map.Entry<K, V> entry = null;
+                if (it.hasNext())
+                {
+                    entry = it.next();
+                    size = 1;
+                }
+
+                fromKey = entry == null ? null : entry.getKey();
+                if (fromKey != null)
+                {
+                    TrieEntry<K, V> prior = previousEntry((TrieEntry<K, V>)entry);
+                    fromKey = prior == null ? null : prior.getKey();
+                }
+
+                toKey = fromKey;
+
+                while (it.hasNext())
+                {
+                    ++size;
+                    entry = it.next();
+                }
+
+                toKey = entry == null ? null : entry.getKey();
+
+                if (toKey != null)
+                {
+                    entry = nextEntry((TrieEntry<K, V>)entry);
+                    toKey = entry == null ? null : entry.getKey();
+                }
+
+                expectedModCount = PatriciaTrie.this.modCount;
+            }
+
+            return size;
+        }
+
+        @Override
+        public K firstKey()
+        {
+            fixup();
+
+            Map.Entry<K,V> e = fromKey == null ? firstEntry() : higherEntry(fromKey);
+            K first = e != null ? e.getKey() : null;
+            if (e == null || !isPrefix(first, prefix))
+                throw new NoSuchElementException();
+
+            return first;
+        }
+
+        @Override
+        public K lastKey()
+        {
+            fixup();
+
+            Map.Entry<K,V> e = toKey == null ? lastEntry() : lowerEntry(toKey);
+            K last = e != null ? e.getKey() : null;
+            if (e == null || !isPrefix(last, prefix))
+                throw new NoSuchElementException();
+
+            return last;
+        }
+
+        /**
+         * Returns true if this {@link PrefixRangeMap}'s key is a prefix
+         * of the provided key.
+         */
+        @Override
+        protected boolean inRange(K key)
+        {
+            return isPrefix(key, prefix);
+        }
+
+        /**
+         * Same as {@link #inRange(Object)}
+         */
+        @Override
+        protected boolean inRange2(K key)
+        {
+            return inRange(key);
+        }
+
+        /**
+         * Returns true if the provided Key is in the FROM range
+         * of the {@link PrefixRangeMap}
+         */
+        @Override
+        protected boolean inFromRange(K key, boolean forceInclusive)
+        {
+            return isPrefix(key, prefix);
+        }
+
+        /**
+         * Returns true if the provided Key is in the TO range
+         * of the {@link PrefixRangeMap}
+         */
+        @Override
+        protected boolean inToRange(K key, boolean forceInclusive)
+        {
+            return isPrefix(key, prefix);
+        }
+
+        @Override
+        protected Set<Map.Entry<K, V>> createEntrySet()
+        {
+            return new PrefixRangeEntrySet(this);
+        }
+
+        @Override
+        public K getFromKey()
+        {
+            return fromKey;
+        }
+
+        @Override
+        public K getToKey()
+        {
+            return toKey;
+        }
+
+        @Override
+        public boolean isFromInclusive()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isToInclusive()
+        {
+            return false;
+        }
+
+        @Override
+        protected SortedMap<K, V> createRangeMap(K fromKey, boolean fromInclusive,
+                                                 K toKey, boolean toInclusive)
+        {
+            return new RangeEntryMap(fromKey, fromInclusive, toKey, toInclusive);
+        }
+    }
+
+    /**
+     * A prefix {@link RangeEntrySet} view of the {@link Trie}
+     */
+    private final class PrefixRangeEntrySet extends RangeEntrySet
+    {
+        private final PrefixRangeMap delegate;
+
+        private TrieEntry<K, V> prefixStart;
+
+        private int expectedModCount = -1;
+
+        /**
+         * Creates a {@link PrefixRangeEntrySet}
+         */
+        public PrefixRangeEntrySet(PrefixRangeMap delegate)
+        {
+            super(delegate);
+            this.delegate = delegate;
+        }
+
+        @Override
+        public int size()
+        {
+            return delegate.fixup();
+        }
+
+        @Override
+        public Iterator<Map.Entry<K,V>> iterator()
+        {
+            if (PatriciaTrie.this.modCount != expectedModCount)
+            {
+                prefixStart = subtree(delegate.prefix);
+                expectedModCount = PatriciaTrie.this.modCount;
+            }
+
+            if (prefixStart == null)
+            {
+                Set<Map.Entry<K,V>> empty = Collections.emptySet();
+                return empty.iterator();
+            }
+            else if (lengthInBits(delegate.prefix) >= prefixStart.bitIndex)
+            {
+                return new SingletonIterator(prefixStart);
+            }
+            else
+            {
+                return new EntryIterator(prefixStart, delegate.prefix);
+            }
+        }
+
+        /**
+         * An {@link Iterator} that holds a single {@link TrieEntry}.
+         */
+        private final class SingletonIterator implements Iterator<Map.Entry<K, V>>
+        {
+            private final TrieEntry<K, V> entry;
+
+            private int hit = 0;
+
+            public SingletonIterator(TrieEntry<K, V> entry)
+            {
+                this.entry = entry;
+            }
+
+            @Override
+            public boolean hasNext()
+            {
+                return hit == 0;
+            }
+
+            @Override
+            public Map.Entry<K, V> next()
+            {
+                if (hit != 0)
+                    throw new NoSuchElementException();
+
+                ++hit;
+                return entry;
+            }
+
+
+            @Override
+            public void remove()
+            {
+                if (hit != 1)
+                    throw new IllegalStateException();
+
+                ++hit;
+                PatriciaTrie.this.removeEntry(entry);
+            }
+        }
+
+        /**
+         * An {@link Iterator} for iterating over a prefix search.
+         */
+        private final class EntryIterator extends TrieIterator<Map.Entry<K, V>>
+        {
+            // values to reset the subtree if we remove it.
+            protected final K prefix;
+            protected boolean lastOne;
+
+            protected TrieEntry<K, V> subtree; // the subtree to search within
+
+            /**
+             * Starts iteration at the given entry & search only
+             * within the given subtree.
+             */
+            EntryIterator(TrieEntry<K, V> startScan, K prefix)
+            {
+                subtree = startScan;
+                next = PatriciaTrie.this.followLeft(startScan);
+                this.prefix = prefix;
+            }
+
+            @Override
+            public Map.Entry<K,V> next()
+            {
+                Map.Entry<K, V> entry = nextEntry();
+                if (lastOne)
+                    next = null;
+                return entry;
+            }
+
+            @Override
+            protected TrieEntry<K, V> findNext(TrieEntry<K, V> prior)
+            {
+                return PatriciaTrie.this.nextEntryInSubtree(prior, subtree);
+            }
+
+            @Override
+            public void remove()
+            {
+                // If the current entry we're removing is the subtree
+                // then we need to find a new subtree parent.
+                boolean needsFixing = false;
+                int bitIdx = subtree.bitIndex;
+                if (current == subtree)
+                    needsFixing = true;
+
+                super.remove();
+
+                // If the subtree changed its bitIndex or we
+                // removed the old subtree, get a new one.
+                if (bitIdx != subtree.bitIndex || needsFixing)
+                    subtree = subtree(prefix);
+
+                // If the subtree's bitIndex is less than the
+                // length of our prefix, it's the last item
+                // in the prefix tree.
+                if (lengthInBits(prefix) >= subtree.bitIndex)
+                    lastOne = true;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/Trie.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/Trie.java
new file mode 100644
index 0000000..1866fec
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/trie/Trie.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2005-2010 Roger Kapsi, Sam Berlin
+ *
+ *   Licensed 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.index.sasi.utils.trie;
+
+import java.util.Map;
+import java.util.SortedMap;
+
+import org.apache.cassandra.index.sasi.utils.trie.Cursor.Decision;
+
+/**
+ * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified
+ * to correspond to Cassandra code style, as the only Patricia Trie implementation,
+ * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based
+ * on rkapsi/patricia-trie project) only supports String keys)
+ * but unfortunately is not deployed to the maven central as a downloadable artifact.
+ */
+
+/**
+ * Defines the interface for a prefix tree, an ordered tree data structure. For
+ * more information, see <a href="http://en.wikipedia.org/wiki/Trie">Tries</a>.
+ *
+ * @author Roger Kapsi
+ * @author Sam Berlin
+ */
+public interface Trie<K, V> extends SortedMap<K, V>
+{
+    /**
+     * Returns the {@link Map.Entry} whose key is closest in a bitwise XOR
+     * metric to the given key. This is NOT lexicographic closeness.
+     * For example, given the keys:
+     *
+     * <ol>
+     * <li>D = 1000100
+     * <li>H = 1001000
+     * <li>L = 1001100
+     * </ol>
+     *
+     * If the {@link Trie} contained 'H' and 'L', a lookup of 'D' would
+     * return 'L', because the XOR distance between D &amp; L is smaller
+     * than the XOR distance between D &amp; H.
+     *
+     * @return The {@link Map.Entry} whose key is closest in a bitwise XOR metric
+     * to the provided key.
+     */
+    Map.Entry<K, V> select(K key);
+
+    /**
+     * Returns the key that is closest in a bitwise XOR metric to the
+     * provided key. This is NOT lexicographic closeness!
+     *
+     * For example, given the keys:
+     *
+     * <ol>
+     * <li>D = 1000100
+     * <li>H = 1001000
+     * <li>L = 1001100
+     * </ol>
+     *
+     * If the {@link Trie} contained 'H' and 'L', a lookup of 'D' would
+     * return 'L', because the XOR distance between D &amp; L is smaller
+     * than the XOR distance between D &amp; H.
+     *
+     * @return The key that is closest in a bitwise XOR metric to the provided key.
+     */
+    @SuppressWarnings("unused")
+    K selectKey(K key);
+
+    /**
+     * Returns the value whose key is closest in a bitwise XOR metric to
+     * the provided key. This is NOT lexicographic closeness!
+     *
+     * For example, given the keys:
+     *
+     * <ol>
+     * <li>D = 1000100
+     * <li>H = 1001000
+     * <li>L = 1001100
+     * </ol>
+     *
+     * If the {@link Trie} contained 'H' and 'L', a lookup of 'D' would
+     * return 'L', because the XOR distance between D &amp; L is smaller
+     * than the XOR distance between D &amp; H.
+     *
+     * @return The value whose key is closest in a bitwise XOR metric
+     * to the provided key.
+     */
+    @SuppressWarnings("unused")
+    V selectValue(K key);
+
+    /**
+     * Iterates through the {@link Trie}, starting with the entry whose bitwise
+     * value is closest in an XOR metric to the given key. After the closest
+     * entry is found, the {@link Trie} will call select on that entry and continue
+     * calling select for each entry (traversing in order of XOR closeness,
+     * NOT lexicographically) until the cursor returns {@link Decision#EXIT}.
+     *
+     * <p>The cursor can return {@link Decision#CONTINUE} to continue traversing.
+     *
+     * <p>{@link Decision#REMOVE_AND_EXIT} is used to remove the current element
+     * and stop traversing.
+     *
+     * <p>Note: The {@link Decision#REMOVE} operation is not supported.
+     *
+     * @return The entry the cursor returned {@link Decision#EXIT} on, or null
+     * if it continued till the end.
+     */
+    Map.Entry<K,V> select(K key, Cursor<? super K, ? super V> cursor);
+
+    /**
+     * Traverses the {@link Trie} in lexicographical order.
+     * {@link Cursor#select(java.util.Map.Entry)} will be called on each entry.
+     *
+     * <p>The traversal will stop when the cursor returns {@link Decision#EXIT},
+     * {@link Decision#CONTINUE} is used to continue traversing and
+     * {@link Decision#REMOVE} is used to remove the element that was selected
+     * and continue traversing.
+     *
+     * <p>{@link Decision#REMOVE_AND_EXIT} is used to remove the current element
+     * and stop traversing.
+     *
+     * @return The entry the cursor returned {@link Decision#EXIT} on, or null
+     * if it continued till the end.
+     */
+    Map.Entry<K,V> traverse(Cursor<? super K, ? super V> cursor);
+
+    /**
+     * Returns a view of this {@link Trie} of all elements that are prefixed
+     * by the given key.
+     *
+     * <p>In a {@link Trie} with fixed size keys, this is essentially a
+     * {@link #get(Object)} operation.
+     *
+     * <p>For example, if the {@link Trie} contains 'Anna', 'Anael',
+     * 'Analu', 'Andreas', 'Andrea', 'Andres', and 'Anatole', then
+     * a lookup of 'And' would return 'Andreas', 'Andrea', and 'Andres'.
+     */
+    SortedMap<K, V> prefixMap(K prefix);
+}
diff --git a/src/java/org/apache/cassandra/index/sasi/utils/trie/Tries.java b/src/java/org/apache/cassandra/index/sasi/utils/trie/Tries.java
new file mode 100644
index 0000000..84080c2
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sasi/utils/trie/Tries.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2005-2010 Roger Kapsi
+ *
+ *   Licensed 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.
+ */
+
+/**
+ * This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified
+ * to correspond to Cassandra code style, as the only Patricia Trie implementation,
+ * which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based
+ * on rkapsi/patricia-trie project) only supports String keys)
+ * but unfortunately is not deployed to the maven central as a downloadable artifact.
+ */
+
+package org.apache.cassandra.index.sasi.utils.trie;
+
+/**
+ * A collection of {@link Trie} utilities
+ */
+public class Tries
+{
+    /**
+     * Returns true if bitIndex is a {@link KeyAnalyzer#OUT_OF_BOUNDS_BIT_KEY}
+     */
+    static boolean isOutOfBoundsIndex(int bitIndex)
+    {
+        return bitIndex == KeyAnalyzer.OUT_OF_BOUNDS_BIT_KEY;
+    }
+
+    /**
+     * Returns true if bitIndex is a {@link KeyAnalyzer#EQUAL_BIT_KEY}
+     */
+    static boolean isEqualBitKey(int bitIndex)
+    {
+        return bitIndex == KeyAnalyzer.EQUAL_BIT_KEY;
+    }
+
+    /**
+     * Returns true if bitIndex is a {@link KeyAnalyzer#NULL_BIT_KEY}
+     */
+    static boolean isNullBitKey(int bitIndex)
+    {
+        return bitIndex == KeyAnalyzer.NULL_BIT_KEY;
+    }
+
+    /**
+     * Returns true if the given bitIndex is valid. Indices
+     * are considered valid if they're between 0 and
+     * {@link Integer#MAX_VALUE}
+     */
+    static boolean isValidBitIndex(int bitIndex)
+    {
+        return 0 <= bitIndex;
+    }
+
+    /**
+     * Returns true if both values are either null or equal
+     */
+    static boolean areEqual(Object a, Object b)
+    {
+        return (a == null ? b == null : a.equals(b));
+    }
+
+    /**
+     * Throws a {@link NullPointerException} with the given message if
+     * the argument is null.
+     */
+    static <T> T notNull(T o, String message)
+    {
+        if (o == null)
+            throw new NullPointerException(message);
+
+        return o;
+    }
+
+    /**
+     * A utility method to cast keys. It actually doesn't
+     * cast anything. It's just fooling the compiler!
+     */
+    @SuppressWarnings("unchecked")
+    static <K> K cast(Object key)
+    {
+        return (K)key;
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/transactions/CleanupTransaction.java b/src/java/org/apache/cassandra/index/transactions/CleanupTransaction.java
index 1d6ba56..29ee14c 100644
--- a/src/java/org/apache/cassandra/index/transactions/CleanupTransaction.java
+++ b/src/java/org/apache/cassandra/index/transactions/CleanupTransaction.java
@@ -26,7 +26,7 @@
  *
  * Notifies registered indexers of each partition being removed and
  *
- * Compaction & Cleanup are somewhat simpler than dealing with incoming writes,
+ * Compaction and Cleanup are somewhat simpler than dealing with incoming writes,
  * being only concerned with cleaning up stale index entries.
  *
  * When multiple versions of a row are compacted, the CleanupTransaction is
diff --git a/src/java/org/apache/cassandra/index/transactions/IndexTransaction.java b/src/java/org/apache/cassandra/index/transactions/IndexTransaction.java
index 3fb8235..3d4b7e2 100644
--- a/src/java/org/apache/cassandra/index/transactions/IndexTransaction.java
+++ b/src/java/org/apache/cassandra/index/transactions/IndexTransaction.java
@@ -26,7 +26,7 @@
  *   Used on the regular write path and when indexing newly acquired SSTables from streaming or sideloading. This type
  *   of transaction may include both row inserts and updates to rows previously existing in the base Memtable. Instances
  *   are scoped to a single partition update and are obtained from the factory method
- *   @{code SecondaryIndexManager#newUpdateTransaction}
+ *   {@code SecondaryIndexManager#newUpdateTransaction}
  *
  * * {@code CompactionTransaction}
  *   Used during compaction when stale entries which have been superceded are cleaned up from the index. As rows in a
diff --git a/src/java/org/apache/cassandra/index/transactions/UpdateTransaction.java b/src/java/org/apache/cassandra/index/transactions/UpdateTransaction.java
index c78304a..51533c2 100644
--- a/src/java/org/apache/cassandra/index/transactions/UpdateTransaction.java
+++ b/src/java/org/apache/cassandra/index/transactions/UpdateTransaction.java
@@ -53,7 +53,7 @@
  * onInserted(row)*              -- called for each Row not already present in the Memtable
  * onUpdated(existing, updated)* -- called for any Row in the update for where a version was already present
  *                                  in the Memtable. It's important to note here that existing is the previous
- *                                  row from the Memtable & updated is the final version replacing it. It is
+ *                                  row from the Memtable and updated is the final version replacing it. It is
  *                                  *not* the incoming row, but the result of merging the incoming and existing
  *                                  rows.
  * commit()                      -- finally, finish is called when the new Partition is swapped into the Memtable
diff --git a/src/java/org/apache/cassandra/io/ISerializer.java b/src/java/org/apache/cassandra/io/ISerializer.java
index 562d226..637a1c7 100644
--- a/src/java/org/apache/cassandra/io/ISerializer.java
+++ b/src/java/org/apache/cassandra/io/ISerializer.java
@@ -43,4 +43,9 @@
     public T deserialize(DataInputPlus in) throws IOException;
 
     public long serializedSize(T t);
+
+    public default void skip(DataInputPlus in) throws IOException
+    {
+        deserialize(in);
+    }
 }
diff --git a/src/java/org/apache/cassandra/io/compress/CompressedRandomAccessReader.java b/src/java/org/apache/cassandra/io/compress/CompressedRandomAccessReader.java
deleted file mode 100644
index daaffd3..0000000
--- a/src/java/org/apache/cassandra/io/compress/CompressedRandomAccessReader.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * 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.io.compress;
-
-import java.io.*;
-import java.nio.ByteBuffer;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.zip.Checksum;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.primitives.Ints;
-
-import org.apache.cassandra.io.FSReadError;
-import org.apache.cassandra.io.sstable.CorruptSSTableException;
-import org.apache.cassandra.io.util.*;
-import org.apache.cassandra.utils.memory.BufferPool;
-
-/**
- * CRAR extends RAR to transparently uncompress blocks from the file into RAR.buffer.  Most of the RAR
- * "read bytes from the buffer, rebuffering when necessary" machinery works unchanged after that.
- */
-public class CompressedRandomAccessReader extends RandomAccessReader
-{
-    private final CompressionMetadata metadata;
-
-    // we read the raw compressed bytes into this buffer, then move the uncompressed ones into super.buffer.
-    private ByteBuffer compressed;
-
-    // re-use single crc object
-    private final Checksum checksum;
-
-    // raw checksum bytes
-    private ByteBuffer checksumBytes;
-
-    @VisibleForTesting
-    public double getCrcCheckChance()
-    {
-        return metadata.parameters.getCrcCheckChance();
-    }
-
-    protected CompressedRandomAccessReader(Builder builder)
-    {
-        super(builder);
-        this.metadata = builder.metadata;
-        this.checksum = metadata.checksumType.newInstance();
-
-        if (regions == null)
-        {
-            compressed = allocateBuffer(metadata.compressor().initialCompressedBufferLength(metadata.chunkLength()), bufferType);
-            checksumBytes = ByteBuffer.wrap(new byte[4]);
-        }
-    }
-
-    @Override
-    protected void releaseBuffer()
-    {
-        try
-        {
-            if (buffer != null)
-            {
-                BufferPool.put(buffer);
-                buffer = null;
-            }
-        }
-        finally
-        {
-            // this will always be null if using mmap access mode (unlike in parent, where buffer is set to a region)
-            if (compressed != null)
-            {
-                BufferPool.put(compressed);
-                compressed = null;
-            }
-        }
-    }
-
-    @Override
-    protected void reBufferStandard()
-    {
-        try
-        {
-            long position = current();
-            assert position < metadata.dataLength;
-
-            CompressionMetadata.Chunk chunk = metadata.chunkFor(position);
-
-            if (compressed.capacity() < chunk.length)
-            {
-                BufferPool.put(compressed);
-                compressed = allocateBuffer(chunk.length, bufferType);
-            }
-            else
-            {
-                compressed.clear();
-            }
-
-            compressed.limit(chunk.length);
-            if (channel.read(compressed, chunk.offset) != chunk.length)
-                throw new CorruptBlockException(getPath(), chunk);
-
-            compressed.flip();
-            buffer.clear();
-
-            if (getCrcCheckChance() >= 1d ||
-                    getCrcCheckChance() > ThreadLocalRandom.current().nextDouble())
-            {
-                if (checksum(chunk) != (int) metadata.checksumType.of(compressed))
-                    throw new CorruptBlockException(getPath(), chunk);
-
-                compressed.rewind();
-            }
-
-            try
-            {
-                metadata.compressor().uncompress(compressed, buffer);
-            }
-            catch (IOException e)
-            {
-                throw new CorruptBlockException(getPath(), chunk, e);
-            }
-            finally
-            {
-                buffer.flip();
-            }
-
-            // buffer offset is always aligned
-            bufferOffset = position & ~(buffer.capacity() - 1);
-            buffer.position((int) (position - bufferOffset));
-            // the length() can be provided at construction time, to override the true (uncompressed) length of the file;
-            // this is permitted to occur within a compressed segment, so we truncate validBufferBytes if we cross the imposed length
-            if (bufferOffset + buffer.limit() > length())
-                buffer.limit((int)(length() - bufferOffset));
-        }
-        catch (CorruptBlockException e)
-        {
-            // Make sure reader does not see stale data.
-            buffer.position(0).limit(0);
-            throw new CorruptSSTableException(e, getPath());
-        }
-        catch (IOException e)
-        {
-            throw new FSReadError(e, getPath());
-        }
-    }
-
-    @Override
-    protected void reBufferMmap()
-    {
-        try
-        {
-            long position = current();
-            assert position < metadata.dataLength;
-
-            CompressionMetadata.Chunk chunk = metadata.chunkFor(position);
-
-            MmappedRegions.Region region = regions.floor(chunk.offset);
-            long segmentOffset = region.bottom();
-            int chunkOffset = Ints.checkedCast(chunk.offset - segmentOffset);
-            ByteBuffer compressedChunk = region.buffer.duplicate(); // TODO: change to slice(chunkOffset) when we upgrade LZ4-java
-
-            compressedChunk.position(chunkOffset).limit(chunkOffset + chunk.length);
-
-            buffer.clear();
-
-            if (getCrcCheckChance() >= 1d ||
-                getCrcCheckChance() > ThreadLocalRandom.current().nextDouble())
-            {
-                int checksum = (int) metadata.checksumType.of(compressedChunk);
-                compressedChunk.limit(compressedChunk.capacity());
-                if (compressedChunk.getInt() != checksum)
-                    throw new CorruptBlockException(getPath(), chunk);
-
-                compressedChunk.position(chunkOffset).limit(chunkOffset + chunk.length);
-            }
-
-            try
-            {
-                metadata.compressor().uncompress(compressedChunk, buffer);
-            }
-            catch (IOException e)
-            {
-                throw new CorruptBlockException(getPath(), chunk, e);
-            }
-            finally
-            {
-                buffer.flip();
-            }
-
-            // buffer offset is always aligned
-            bufferOffset = position & ~(buffer.capacity() - 1);
-            buffer.position((int) (position - bufferOffset));
-            // the length() can be provided at construction time, to override the true (uncompressed) length of the file;
-            // this is permitted to occur within a compressed segment, so we truncate validBufferBytes if we cross the imposed length
-            if (bufferOffset + buffer.limit() > length())
-                buffer.limit((int)(length() - bufferOffset));
-        }
-        catch (CorruptBlockException e)
-        {
-            // Make sure reader does not see stale data.
-            buffer.position(0).limit(0);
-            throw new CorruptSSTableException(e, getPath());
-        }
-
-    }
-
-    private int checksum(CompressionMetadata.Chunk chunk) throws IOException
-    {
-        long position = chunk.offset + chunk.length;
-        checksumBytes.clear();
-        if (channel.read(checksumBytes, position) != checksumBytes.capacity())
-            throw new CorruptBlockException(getPath(), chunk);
-        return checksumBytes.getInt(0);
-    }
-
-    @Override
-    public long length()
-    {
-        return metadata.dataLength;
-    }
-
-    @Override
-    public String toString()
-    {
-        return String.format("%s - chunk length %d, data length %d.", getPath(), metadata.chunkLength(), metadata.dataLength);
-    }
-
-    public final static class Builder extends RandomAccessReader.Builder
-    {
-        private final CompressionMetadata metadata;
-
-        public Builder(ICompressedFile file)
-        {
-            super(file.channel());
-            this.metadata = applyMetadata(file.getMetadata());
-            this.regions = file.regions();
-        }
-
-        public Builder(ChannelProxy channel, CompressionMetadata metadata)
-        {
-            super(channel);
-            this.metadata = applyMetadata(metadata);
-        }
-
-        private CompressionMetadata applyMetadata(CompressionMetadata metadata)
-        {
-            this.overrideLength = metadata.compressedFileLength;
-            this.bufferSize = metadata.chunkLength();
-            this.bufferType = metadata.compressor().preferredBufferType();
-
-            assert Integer.bitCount(this.bufferSize) == 1; //must be a power of two
-
-            return metadata;
-        }
-
-        @Override
-        protected ByteBuffer createBuffer()
-        {
-            buffer = allocateBuffer(bufferSize, bufferType);
-            buffer.limit(0);
-            return buffer;
-        }
-
-        @Override
-        public RandomAccessReader build()
-        {
-            return new CompressedRandomAccessReader(this);
-        }
-    }
-}
diff --git a/src/java/org/apache/cassandra/io/compress/CompressedSequentialWriter.java b/src/java/org/apache/cassandra/io/compress/CompressedSequentialWriter.java
index 9c47513..4068be7 100644
--- a/src/java/org/apache/cassandra/io/compress/CompressedSequentialWriter.java
+++ b/src/java/org/apache/cassandra/io/compress/CompressedSequentialWriter.java
@@ -17,29 +17,27 @@
  */
 package org.apache.cassandra.io.compress;
 
-import static org.apache.cassandra.utils.Throwables.merge;
-
 import java.io.DataOutputStream;
 import java.io.EOFException;
 import java.io.File;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.Channels;
+import java.util.Optional;
 import java.util.zip.CRC32;
 
 import org.apache.cassandra.io.FSReadError;
 import org.apache.cassandra.io.FSWriteError;
 import org.apache.cassandra.io.sstable.CorruptSSTableException;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
-import org.apache.cassandra.io.util.DataIntegrityMetadata;
-import org.apache.cassandra.io.util.DataPosition;
-import org.apache.cassandra.io.util.FileUtils;
-import org.apache.cassandra.io.util.SequentialWriter;
+import org.apache.cassandra.io.util.*;
 import org.apache.cassandra.schema.CompressionParams;
 
+import static org.apache.cassandra.utils.Throwables.merge;
+
 public class CompressedSequentialWriter extends SequentialWriter
 {
-    private final DataIntegrityMetadata.ChecksumWriter crcMetadata;
+    private final ChecksumWriter crcMetadata;
 
     // holds offset in the file where current chunk should be written
     // changed only by flush() method where data buffer gets compressed and stored to the file
@@ -60,14 +58,34 @@
     private final MetadataCollector sstableMetadataCollector;
 
     private final ByteBuffer crcCheckBuffer = ByteBuffer.allocate(4);
+    private final Optional<File> digestFile;
 
+    /**
+     * Create CompressedSequentialWriter without digest file.
+     *
+     * @param file File to write
+     * @param offsetsPath File name to write compression metadata
+     * @param digestFile File to write digest
+     * @param option Write option (buffer size and type will be set the same as compression params)
+     * @param parameters Compression mparameters
+     * @param sstableMetadataCollector Metadata collector
+     */
     public CompressedSequentialWriter(File file,
                                       String offsetsPath,
+                                      File digestFile,
+                                      SequentialWriterOption option,
                                       CompressionParams parameters,
                                       MetadataCollector sstableMetadataCollector)
     {
-        super(file, parameters.chunkLength(), parameters.getSstableCompressor().preferredBufferType());
+        super(file, SequentialWriterOption.newBuilder()
+                            .bufferSize(option.bufferSize())
+                            .bufferType(option.bufferType())
+                            .bufferSize(parameters.chunkLength())
+                            .bufferType(parameters.getSstableCompressor().preferredBufferType())
+                            .finishOnClose(option.finishOnClose())
+                            .build());
         this.compressor = parameters.getSstableCompressor();
+        this.digestFile = Optional.ofNullable(digestFile);
 
         // buffer for compression should be the same size as buffer itself
         compressed = compressor.preferredBufferType().allocate(compressor.initialCompressedBufferLength(buffer.capacity()));
@@ -76,7 +94,7 @@
         metadataWriter = CompressionMetadata.Writer.open(parameters, offsetsPath);
 
         this.sstableMetadataCollector = sstableMetadataCollector;
-        crcMetadata = new DataIntegrityMetadata.ChecksumWriter(new DataOutputStream(Channels.newOutputStream(channel)));
+        crcMetadata = new ChecksumWriter(new DataOutputStream(Channels.newOutputStream(channel)));
     }
 
     @Override
@@ -92,6 +110,17 @@
         }
     }
 
+    /**
+     * Get a quick estimation on how many bytes have been written to disk
+     *
+     * It should for the most part be exactly the same as getOnDiskFilePointer()
+     */
+    @Override
+    public long getEstimatedOnDiskBytesWritten()
+    {
+        return chunkOffset;
+    }
+
     @Override
     public void flush()
     {
@@ -292,8 +321,7 @@
         protected void doPrepare()
         {
             syncInternal();
-            if (descriptor != null)
-                crcMetadata.writeFullChecksum(descriptor);
+            digestFile.ifPresent(crcMetadata::writeFullChecksum);
             sstableMetadataCollector.addCompressionRatio(compressedSize, uncompressedSize);
             metadataWriter.finalizeLength(current(), chunkCount).prepareToCommit();
         }
diff --git a/src/java/org/apache/cassandra/io/compress/CompressionMetadata.java b/src/java/org/apache/cassandra/io/compress/CompressionMetadata.java
index 10d1ae9..90ba749a 100644
--- a/src/java/org/apache/cassandra/io/compress/CompressionMetadata.java
+++ b/src/java/org/apache/cassandra/io/compress/CompressionMetadata.java
@@ -49,7 +49,6 @@
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
-import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.io.util.Memory;
 import org.apache.cassandra.io.util.SafeMemory;
 import org.apache.cassandra.schema.CompressionParams;
diff --git a/src/java/org/apache/cassandra/io/compress/DeflateCompressor.java b/src/java/org/apache/cassandra/io/compress/DeflateCompressor.java
index f2ccd64..8557f5f 100644
--- a/src/java/org/apache/cassandra/io/compress/DeflateCompressor.java
+++ b/src/java/org/apache/cassandra/io/compress/DeflateCompressor.java
@@ -17,7 +17,8 @@
  */
 package org.apache.cassandra.io.compress;
 
-import org.apache.cassandra.utils.FBUtilities;
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.schema.CompressionParams;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -32,8 +33,22 @@
 {
     public static final DeflateCompressor instance = new DeflateCompressor();
 
-    private final ThreadLocal<Deflater> deflater;
-    private final ThreadLocal<Inflater> inflater;
+    private static final FastThreadLocal<byte[]> threadLocalScratchBuffer = new FastThreadLocal<byte[]>()
+    {
+        @Override
+        protected byte[] initialValue()
+        {
+            return new byte[CompressionParams.DEFAULT_CHUNK_LENGTH];
+        }
+    };
+
+    public static byte[] getThreadLocalScratchBuffer()
+    {
+        return threadLocalScratchBuffer.get();
+    }
+
+    private final FastThreadLocal<Deflater> deflater;
+    private final FastThreadLocal<Inflater> inflater;
 
     public static DeflateCompressor create(Map<String, String> compressionOptions)
     {
@@ -43,7 +58,7 @@
 
     private DeflateCompressor()
     {
-        deflater = new ThreadLocal<Deflater>()
+        deflater = new FastThreadLocal<Deflater>()
         {
             @Override
             protected Deflater initialValue()
@@ -51,7 +66,7 @@
                 return new Deflater();
             }
         };
-        inflater = new ThreadLocal<Inflater>()
+        inflater = new FastThreadLocal<Inflater>()
         {
             @Override
             protected Inflater initialValue()
@@ -104,7 +119,7 @@
         Deflater def = deflater.get();
         def.reset();
 
-        byte[] buffer = FBUtilities.getThreadLocalScratchBuffer();
+        byte[] buffer = getThreadLocalScratchBuffer();
         // Use half the buffer for input, half for output.
         int chunkLen = buffer.length / 2;
         while (input.remaining() > chunkLen)
@@ -149,7 +164,7 @@
             Inflater inf = inflater.get();
             inf.reset();
 
-            byte[] buffer = FBUtilities.getThreadLocalScratchBuffer();
+            byte[] buffer = getThreadLocalScratchBuffer();
             // Use half the buffer for input, half for output.
             int chunkLen = buffer.length / 2;
             while (input.remaining() > chunkLen)
diff --git a/src/java/org/apache/cassandra/io/compress/ICompressor.java b/src/java/org/apache/cassandra/io/compress/ICompressor.java
index 5719834..40dc7c2 100644
--- a/src/java/org/apache/cassandra/io/compress/ICompressor.java
+++ b/src/java/org/apache/cassandra/io/compress/ICompressor.java
@@ -49,7 +49,7 @@
     public BufferType preferredBufferType();
 
     /**
-     * Checks if the given buffer would be supported by the compressor. If a type is supported the compressor must be
+     * Checks if the given buffer would be supported by the compressor. If a type is supported, the compressor must be
      * able to use it in combination with all other supported types.
      *
      * Direct and memory-mapped buffers must be supported by all compressors.
diff --git a/src/java/org/apache/cassandra/io/compress/LZ4Compressor.java b/src/java/org/apache/cassandra/io/compress/LZ4Compressor.java
index 3a3b024..1b3844d 100644
--- a/src/java/org/apache/cassandra/io/compress/LZ4Compressor.java
+++ b/src/java/org/apache/cassandra/io/compress/LZ4Compressor.java
@@ -23,31 +23,80 @@
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import net.jpountz.lz4.LZ4Exception;
 import net.jpountz.lz4.LZ4Factory;
-import org.apache.cassandra.schema.CompressionParams;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.utils.Pair;
 
 public class LZ4Compressor implements ICompressor
 {
+    private static final Logger logger = LoggerFactory.getLogger(LZ4Compressor.class);
+
+    public static final String LZ4_FAST_COMPRESSOR = "fast";
+    public static final String LZ4_HIGH_COMPRESSOR = "high";
+    private static final Set<String> VALID_COMPRESSOR_TYPES = new HashSet<>(Arrays.asList(LZ4_FAST_COMPRESSOR, LZ4_HIGH_COMPRESSOR));
+
+    private static final int DEFAULT_HIGH_COMPRESSION_LEVEL = 9;
+    private static final String DEFAULT_LZ4_COMPRESSOR_TYPE = LZ4_FAST_COMPRESSOR;
+
+    public static final String LZ4_HIGH_COMPRESSION_LEVEL = "lz4_high_compressor_level";
+    public static final String LZ4_COMPRESSOR_TYPE = "lz4_compressor_type";
+
     private static final int INTEGER_BYTES = 4;
 
-    @VisibleForTesting
-    public static final LZ4Compressor instance = new LZ4Compressor();
+    private static final ConcurrentHashMap<Pair<String, Integer>, LZ4Compressor> instances = new ConcurrentHashMap<>();
 
-    public static LZ4Compressor create(Map<String, String> args)
+    public static LZ4Compressor create(Map<String, String> args) throws ConfigurationException
     {
+        String compressorType = validateCompressorType(args.get(LZ4_COMPRESSOR_TYPE));
+        Integer compressionLevel = validateCompressionLevel(args.get(LZ4_HIGH_COMPRESSION_LEVEL));
+
+        Pair<String, Integer> compressorTypeAndLevel = Pair.create(compressorType, compressionLevel);
+        LZ4Compressor instance = instances.get(compressorTypeAndLevel);
+        if (instance == null)
+        {
+            if (compressorType.equals(LZ4_FAST_COMPRESSOR) && args.get(LZ4_HIGH_COMPRESSION_LEVEL) != null)
+                logger.warn("'{}' parameter is ignored when '{}' is '{}'", LZ4_HIGH_COMPRESSION_LEVEL, LZ4_COMPRESSOR_TYPE, LZ4_FAST_COMPRESSOR);
+            instance = new LZ4Compressor(compressorType, compressionLevel);
+            LZ4Compressor instanceFromMap = instances.putIfAbsent(compressorTypeAndLevel, instance);
+            if(instanceFromMap != null)
+                instance = instanceFromMap;
+        }
         return instance;
     }
 
     private final net.jpountz.lz4.LZ4Compressor compressor;
     private final net.jpountz.lz4.LZ4FastDecompressor decompressor;
+    @VisibleForTesting
+    final String compressorType;
+    @VisibleForTesting
+    final Integer compressionLevel;
 
-    private LZ4Compressor()
+    private LZ4Compressor(String type, Integer compressionLevel)
     {
+        this.compressorType = type;
+        this.compressionLevel = compressionLevel;
         final LZ4Factory lz4Factory = LZ4Factory.fastestInstance();
-        compressor = lz4Factory.fastCompressor();
+        switch (type)
+        {
+            case LZ4_HIGH_COMPRESSOR:
+            {
+                compressor = lz4Factory.highCompressor(compressionLevel);
+                break;
+            }
+            case LZ4_FAST_COMPRESSOR:
+            default:
+            {
+                compressor = lz4Factory.fastCompressor();
+            }
+        }
+
         decompressor = lz4Factory.fastDecompressor();
     }
 
@@ -127,7 +176,50 @@
 
     public Set<String> supportedOptions()
     {
-        return new HashSet<>();
+        return new HashSet<>(Arrays.asList(LZ4_HIGH_COMPRESSION_LEVEL, LZ4_COMPRESSOR_TYPE));
+    }
+
+    public static String validateCompressorType(String compressorType) throws ConfigurationException
+    {
+        if (compressorType == null)
+            return DEFAULT_LZ4_COMPRESSOR_TYPE;
+
+        if (!VALID_COMPRESSOR_TYPES.contains(compressorType))
+        {
+            throw new ConfigurationException(String.format("Invalid compressor type '%s' specified for LZ4 parameter '%s'. "
+                                                           + "Valid options are %s.", compressorType, LZ4_COMPRESSOR_TYPE,
+                                                           VALID_COMPRESSOR_TYPES.toString()));
+        }
+        else
+        {
+            return compressorType;
+        }
+    }
+
+    public static Integer validateCompressionLevel(String compressionLevel) throws ConfigurationException
+    {
+        if (compressionLevel == null)
+            return DEFAULT_HIGH_COMPRESSION_LEVEL;
+
+        ConfigurationException ex = new ConfigurationException("Invalid value [" + compressionLevel + "] for parameter '"
+                                                                 + LZ4_HIGH_COMPRESSION_LEVEL + "'. Value must be between 1 and 17.");
+
+        Integer level;
+        try
+        {
+            level = Integer.valueOf(compressionLevel);
+        }
+        catch (NumberFormatException e)
+        {
+            throw ex;
+        }
+
+        if (level < 1 || level > 17)
+        {
+            throw ex;
+        }
+
+        return level;
     }
 
     public BufferType preferredBufferType()
diff --git a/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java b/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java
index 62348ec..9a8f968 100644
--- a/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java
+++ b/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java
@@ -22,12 +22,12 @@
 import java.io.IOException;
 import java.io.Closeable;
 import java.nio.ByteBuffer;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.EncodingStats;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
@@ -43,8 +43,9 @@
     protected final File directory;
     protected final CFMetaData metadata;
     protected final PartitionColumns columns;
-    protected SSTableFormat.Type formatType = DatabaseDescriptor.getSSTableFormat();
+    protected SSTableFormat.Type formatType = SSTableFormat.Type.current();
     protected static AtomicInteger generation = new AtomicInteger(0);
+    protected boolean makeRangeAware = false;
 
     protected AbstractSSTableSimpleWriter(File directory, CFMetaData metadata, PartitionColumns columns)
     {
@@ -58,14 +59,26 @@
         this.formatType = type;
     }
 
+    protected void setRangeAwareWriting(boolean makeRangeAware)
+    {
+        this.makeRangeAware = makeRangeAware;
+    }
+
+
     protected SSTableTxnWriter createWriter()
     {
+        SerializationHeader header = new SerializationHeader(true, metadata, columns, EncodingStats.NO_STATS);
+
+        if (makeRangeAware)
+            return SSTableTxnWriter.createRangeAware(metadata, 0,  ActiveRepairService.UNREPAIRED_SSTABLE, formatType, 0, header);
+
         return SSTableTxnWriter.create(metadata,
                                        createDescriptor(directory, metadata.ksName, metadata.cfName, formatType),
                                        0,
                                        ActiveRepairService.UNREPAIRED_SSTABLE,
                                        0,
-                                       new SerializationHeader(true, metadata, columns, EncodingStats.NO_STATS));
+                                       header,
+                                       Collections.emptySet());
     }
 
     private static Descriptor createDescriptor(File directory, final String keyspace, final String columnFamily, final SSTableFormat.Type fmt)
diff --git a/src/java/org/apache/cassandra/io/sstable/CQLSSTableWriter.java b/src/java/org/apache/cassandra/io/sstable/CQLSSTableWriter.java
index 39f7339..694fe37 100644
--- a/src/java/org/apache/cassandra/io/sstable/CQLSSTableWriter.java
+++ b/src/java/org/apache/cassandra/io/sstable/CQLSSTableWriter.java
@@ -21,30 +21,49 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.stream.Collectors;
 
-import org.apache.cassandra.config.*;
-import org.apache.cassandra.cql3.*;
-import org.apache.cassandra.cql3.statements.CFStatement;
+import com.datastax.driver.core.ProtocolVersion;
+import com.datastax.driver.core.TypeCodec;
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
+import org.apache.cassandra.cql3.ColumnSpecification;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.UpdateParameters;
+import org.apache.cassandra.cql3.functions.UDHelper;
 import org.apache.cassandra.cql3.statements.CreateTableStatement;
+import org.apache.cassandra.cql3.statements.CreateTypeStatement;
+import org.apache.cassandra.cql3.statements.ModificationStatement;
 import org.apache.cassandra.cql3.statements.ParsedStatement;
 import org.apache.cassandra.cql3.statements.UpdateStatement;
 import org.apache.cassandra.db.Clustering;
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.SystemKeyspace;
-import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.UserType;
 import org.apache.cassandra.db.partitions.Partition;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.Murmur3Partitioner;
 import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.exceptions.RequestValidationException;
+import org.apache.cassandra.exceptions.SyntaxException;
 import org.apache.cassandra.io.sstable.format.SSTableFormat;
+import org.apache.cassandra.schema.Functions;
 import org.apache.cassandra.schema.KeyspaceMetadata;
 import org.apache.cassandra.schema.KeyspaceParams;
 import org.apache.cassandra.schema.SchemaKeyspace;
 import org.apache.cassandra.schema.Tables;
 import org.apache.cassandra.schema.Types;
+import org.apache.cassandra.schema.Views;
 import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.Pair;
 
 /**
@@ -52,12 +71,14 @@
  * <p>
  * Typical usage looks like:
  * <pre>
+ *   String type = CREATE TYPE myKs.myType (a int, b int)";
  *   String schema = "CREATE TABLE myKs.myTable ("
  *                 + "  k int PRIMARY KEY,"
  *                 + "  v1 text,"
- *                 + "  v2 int"
+ *                 + "  v2 int,"
+ *                 + "  v3 myType,"
  *                 + ")";
- *   String insert = "INSERT INTO myKs.myTable (k, v1, v2) VALUES (?, ?, ?)";
+ *   String insert = "INSERT INTO myKs.myTable (k, v1, v2, v3) VALUES (?, ?, ?, ?)";
  *
  *   // Creates a new writer. You need to provide at least the directory where to write the created sstable,
  *   // the schema for the sstable to write and a (prepared) insert statement to use. If you do not use the
@@ -65,13 +86,15 @@
  *   // CQLSSTableWriter.Builder for more details on the available options.
  *   CQLSSTableWriter writer = CQLSSTableWriter.builder()
  *                                             .inDirectory("path/to/directory")
+ *                                             .withType(type)
  *                                             .forTable(schema)
  *                                             .using(insert).build();
  *
+ *   UserType myType = writer.getUDType("myType");
  *   // Adds a nember of rows to the resulting sstable
- *   writer.addRow(0, "test1", 24);
- *   writer.addRow(1, "test2", null);
- *   writer.addRow(2, "test3", 42);
+ *   writer.addRow(0, "test1", 24, myType.newValue().setInt("a", 10).setInt("b", 20));
+ *   writer.addRow(1, "test2", null, null);
+ *   writer.addRow(2, "test3", 42, myType.newValue().setInt("a", 30).setInt("b", 40));
  *
  *   // Close the writer, finalizing the sstable
  *   writer.close();
@@ -83,9 +106,11 @@
  */
 public class CQLSSTableWriter implements Closeable
 {
+    public static final ByteBuffer UNSET_VALUE = ByteBufferUtil.UNSET_BYTE_BUFFER;
+
     static
     {
-        Config.setClientMode(true);
+        DatabaseDescriptor.clientInitialization(false);
         // Partitioner is not set in client mode.
         if (DatabaseDescriptor.getPartitioner() == null)
             DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
@@ -94,12 +119,15 @@
     private final AbstractSSTableSimpleWriter writer;
     private final UpdateStatement insert;
     private final List<ColumnSpecification> boundNames;
+    private final List<TypeCodec> typeCodecs;
 
     private CQLSSTableWriter(AbstractSSTableSimpleWriter writer, UpdateStatement insert, List<ColumnSpecification> boundNames)
     {
         this.writer = writer;
         this.insert = insert;
         this.boundNames = boundNames;
+        this.typeCodecs = boundNames.stream().map(bn ->  UDHelper.codecFor(UDHelper.driverType(bn.type)))
+                                             .collect(Collectors.toList());
     }
 
     /**
@@ -147,8 +175,13 @@
     {
         int size = Math.min(values.size(), boundNames.size());
         List<ByteBuffer> rawValues = new ArrayList<>(size);
+
         for (int i = 0; i < size; i++)
-            rawValues.add(values.get(i) == null ? null : ((AbstractType)boundNames.get(i).type).decompose(values.get(i)));
+        {
+            Object value = values.get(i);
+            rawValues.add(serialize(value, typeCodecs.get(i)));
+        }
+
         return rawAddRow(rawValues);
     }
 
@@ -177,10 +210,11 @@
     {
         int size = boundNames.size();
         List<ByteBuffer> rawValues = new ArrayList<>(size);
-        for (int i = 0; i < size; i++) {
+        for (int i = 0; i < size; i++)
+        {
             ColumnSpecification spec = boundNames.get(i);
             Object value = values.get(spec.name.toString());
-            rawValues.add(value == null ? null : ((AbstractType)spec.type).decompose(value));
+            rawValues.add(serialize(value, typeCodecs.get(i)));
         }
         return rawAddRow(rawValues);
     }
@@ -264,7 +298,8 @@
     {
         int size = Math.min(values.size(), boundNames.size());
         List<ByteBuffer> rawValues = new ArrayList<>(size);
-        for (int i = 0; i < size; i++) {
+        for (int i = 0; i < size; i++) 
+        {
             ColumnSpecification spec = boundNames.get(i);
             rawValues.add(values.get(spec.name.toString()));
         }
@@ -272,6 +307,20 @@
     }
 
     /**
+     * Returns the User Defined type, used in this SSTable Writer, that can
+     * be used to create UDTValue instances.
+     *
+     * @param dataType name of the User Defined type
+     * @return user defined type
+     */
+    public com.datastax.driver.core.UserType getUDType(String dataType)
+    {
+        KeyspaceMetadata ksm = Schema.instance.getKSMetaData(insert.keyspace());
+        UserType userType = ksm.types.getNullable(ByteBufferUtil.bytes(dataType));
+        return (com.datastax.driver.core.UserType) UDHelper.driverType(userType);
+    }
+
+    /**
      * Close this writer.
      * <p>
      * This method should be called, otherwise the produced sstables are not
@@ -282,6 +331,13 @@
         writer.close();
     }
 
+    private ByteBuffer serialize(Object value, TypeCodec codec)
+    {
+        if (value == null || value == UNSET_VALUE)
+            return (ByteBuffer) value;
+
+        return codec.serialize(value, ProtocolVersion.NEWEST_SUPPORTED);
+    }
     /**
      * A Builder for a CQLSSTableWriter object.
      */
@@ -291,14 +347,17 @@
 
         protected SSTableFormat.Type formatType = null;
 
-        private CFMetaData schema;
-        private UpdateStatement insert;
-        private List<ColumnSpecification> boundNames;
+        private CreateTableStatement.RawStatement schemaStatement;
+        private final List<CreateTypeStatement> typeStatements;
+        private ModificationStatement.Parsed insertStatement;
+        private IPartitioner partitioner;
 
         private boolean sorted = false;
         private long bufferSizeInMB = 128;
 
-        protected Builder() {}
+        protected Builder() {
+            this.typeStatements = new ArrayList<>();
+        }
 
         /**
          * The directory where to write the sstables.
@@ -336,6 +395,12 @@
             return this;
         }
 
+        public Builder withType(String typeDefinition) throws SyntaxException
+        {
+            typeStatements.add(QueryProcessor.parseStatement(typeDefinition, CreateTypeStatement.class, "CREATE TYPE"));
+            return this;
+        }
+
         /**
          * The schema (CREATE TABLE statement) for the table for which sstable are to be created.
          * <p>
@@ -352,57 +417,8 @@
          */
         public Builder forTable(String schema)
         {
-            try
-            {
-                synchronized (CQLSSTableWriter.class)
-                {
-                    if (Schema.instance.getKSMetaData(SchemaKeyspace.NAME) == null)
-                        Schema.instance.load(SchemaKeyspace.metadata());
-                    if (Schema.instance.getKSMetaData(SystemKeyspace.NAME) == null)
-                        Schema.instance.load(SystemKeyspace.metadata());
-
-                    this.schema = getTableMetadata(schema);
-
-                    // We need to register the keyspace/table metadata through Schema, otherwise we won't be able to properly
-                    // build the insert statement in using().
-                    KeyspaceMetadata ksm = Schema.instance.getKSMetaData(this.schema.ksName);
-                    if (ksm == null)
-                    {
-                        createKeyspaceWithTable(this.schema);
-                    }
-                    else if (Schema.instance.getCFMetaData(this.schema.ksName, this.schema.cfName) == null)
-                    {
-                        addTableToKeyspace(ksm, this.schema);
-                    }
-                    return this;
-                }
-            }
-            catch (RequestValidationException e)
-            {
-                throw new IllegalArgumentException(e.getMessage(), e);
-            }
-        }
-
-        /**
-         * Creates the keyspace with the specified table.
-         *
-         * @param table the table that must be created.
-         */
-        private static void createKeyspaceWithTable(CFMetaData table)
-        {
-            Schema.instance.load(KeyspaceMetadata.create(table.ksName, KeyspaceParams.simple(1), Tables.of(table)));
-        }
-
-        /**
-         * Adds the table to the to the specified keyspace.
-         *
-         * @param keyspace the keyspace to add to
-         * @param table the table to add
-         */
-        private static void addTableToKeyspace(KeyspaceMetadata keyspace, CFMetaData table)
-        {
-            Schema.instance.load(table);
-            Schema.instance.setKeyspaceMetadata(keyspace.withSwapped(keyspace.tables.with(table)));
+            this.schemaStatement = QueryProcessor.parseStatement(schema, CreateTableStatement.RawStatement.class, "CREATE TABLE");
+            return this;
         }
 
         /**
@@ -417,41 +433,29 @@
          */
         public Builder withPartitioner(IPartitioner partitioner)
         {
-            this.schema = schema.copy(partitioner);
+            this.partitioner = partitioner;
             return this;
         }
 
         /**
-         * The INSERT statement defining the order of the values to add for a given CQL row.
+         * The INSERT or UPDATE statement defining the order of the values to add for a given CQL row.
          * <p>
          * Please note that the provided INSERT statement <b>must</b> use a fully-qualified
-         * table name, one that include the keyspace name. Morewover, said statement must use
-         * bind variables since it is those bind variables that will be bound to values by the
-         * resulting writer.
+         * table name, one that include the keyspace name. Moreover, said statement must use
+         * bind variables since these variables will be bound to values by the resulting writer.
          * <p>
-         * This is a mandatory option, and this needs to be called after foTable().
+         * This is a mandatory option.
          *
-         * @param insertStatement an insertion statement that defines the order
+         * @param insert an insertion statement that defines the order
          * of column values to use.
          * @return this builder.
          *
          * @throws IllegalArgumentException if {@code insertStatement} is not a valid insertion
          * statement, does not have a fully-qualified table name or have no bind variables.
          */
-        public Builder using(String insertStatement)
+        public Builder using(String insert)
         {
-            if (schema == null)
-                throw new IllegalStateException("You need to define the schema by calling forTable() prior to this call.");
-
-            Pair<UpdateStatement, List<ColumnSpecification>> p = getStatement(insertStatement, UpdateStatement.class, "INSERT");
-            this.insert = p.left;
-            this.boundNames = p.right;
-            if (this.insert.hasConditions())
-                throw new IllegalArgumentException("Conditional statements are not supported");
-            if (this.insert.isCounter())
-                throw new IllegalArgumentException("Counter update statements are not supported");
-            if (this.boundNames.isEmpty())
-                throw new IllegalArgumentException("Provided insert statement has no bind variables");
+            this.insertStatement = QueryProcessor.parseStatement(insert, ModificationStatement.Parsed.class, "INSERT/UPDATE");
             return this;
         }
 
@@ -497,54 +501,105 @@
             return this;
         }
 
-        private static CFMetaData getTableMetadata(String schema)
-        {
-            CFStatement parsed = (CFStatement)QueryProcessor.parseStatement(schema);
-            // tables with UDTs are currently not supported by CQLSSTableWrite, so we just use Types.none(), for now
-            // see CASSANDRA-10624 for more details
-            CreateTableStatement statement = (CreateTableStatement) ((CreateTableStatement.RawStatement) parsed).prepare(Types.none()).statement;
-            statement.validate(ClientState.forInternalCalls());
-            return statement.getCFMetaData();
-        }
-
-        private static <T extends CQLStatement> Pair<T, List<ColumnSpecification>> getStatement(String query, Class<T> klass, String type)
-        {
-            try
-            {
-                ClientState state = ClientState.forInternalCalls();
-                ParsedStatement.Prepared prepared = QueryProcessor.getStatement(query, state);
-                CQLStatement stmt = prepared.statement;
-                stmt.validate(state);
-
-                if (!stmt.getClass().equals(klass))
-                    throw new IllegalArgumentException("Invalid query, must be a " + type + " statement");
-
-                return Pair.create(klass.cast(stmt), prepared.boundNames);
-            }
-            catch (RequestValidationException e)
-            {
-                throw new IllegalArgumentException(e.getMessage(), e);
-            }
-        }
-
         @SuppressWarnings("resource")
         public CQLSSTableWriter build()
         {
             if (directory == null)
                 throw new IllegalStateException("No ouptut directory specified, you should provide a directory with inDirectory()");
-            if (schema == null)
+            if (schemaStatement == null)
                 throw new IllegalStateException("Missing schema, you should provide the schema for the SSTable to create with forTable()");
-            if (insert == null)
+            if (insertStatement == null)
                 throw new IllegalStateException("No insert statement specified, you should provide an insert statement through using()");
 
-            AbstractSSTableSimpleWriter writer = sorted
-                                               ? new SSTableSimpleWriter(directory, schema, insert.updatedColumns())
-                                               : new SSTableSimpleUnsortedWriter(directory, schema, insert.updatedColumns(), bufferSizeInMB);
+            synchronized (CQLSSTableWriter.class)
+            {
+                if (Schema.instance.getKSMetaData(SchemaConstants.SCHEMA_KEYSPACE_NAME) == null)
+                    Schema.instance.load(SchemaKeyspace.metadata());
+                if (Schema.instance.getKSMetaData(SchemaConstants.SYSTEM_KEYSPACE_NAME) == null)
+                    Schema.instance.load(SystemKeyspace.metadata());
 
-            if (formatType != null)
-                writer.setSSTableFormatType(formatType);
+                String keyspace = schemaStatement.keyspace();
 
-            return new CQLSSTableWriter(writer, insert, boundNames);
+                if (Schema.instance.getKSMetaData(keyspace) == null)
+                {
+                    Schema.instance.load(KeyspaceMetadata.create(keyspace,
+                                                                 KeyspaceParams.simple(1),
+                                                                 Tables.none(),
+                                                                 Views.none(),
+                                                                 Types.none(),
+                                                                 Functions.none()));
+                }
+
+
+                KeyspaceMetadata ksm = Schema.instance.getKSMetaData(keyspace);
+                CFMetaData cfMetaData = ksm.tables.getNullable(schemaStatement.columnFamily());
+                if (cfMetaData == null)
+                {
+                    Types types = createTypes(keyspace);
+                    cfMetaData = createTable(types);
+
+                    Schema.instance.load(cfMetaData);
+                    Schema.instance.setKeyspaceMetadata(ksm.withSwapped(ksm.tables.with(cfMetaData)).withSwapped(types));
+                }
+
+                Pair<UpdateStatement, List<ColumnSpecification>> preparedInsert = prepareInsert();
+
+                AbstractSSTableSimpleWriter writer = sorted
+                                                     ? new SSTableSimpleWriter(directory, cfMetaData, preparedInsert.left.updatedColumns())
+                                                     : new SSTableSimpleUnsortedWriter(directory, cfMetaData, preparedInsert.left.updatedColumns(), bufferSizeInMB);
+
+                if (formatType != null)
+                    writer.setSSTableFormatType(formatType);
+
+                return new CQLSSTableWriter(writer, preparedInsert.left, preparedInsert.right);
+            }
+        }
+
+        private Types createTypes(String keyspace)
+        {
+            Types.RawBuilder builder = Types.rawBuilder(keyspace);
+            for (CreateTypeStatement st : typeStatements)
+                st.addToRawBuilder(builder);
+            return builder.build();
+        }
+
+        /**
+         * Creates the table according to schema statement
+         *
+         * @param types types this table should be created with
+         */
+        private CFMetaData createTable(Types types)
+        {
+            CreateTableStatement statement = (CreateTableStatement) schemaStatement.prepare(types).statement;
+            statement.validate(ClientState.forInternalCalls());
+
+            CFMetaData cfMetaData = statement.getCFMetaData();
+
+            if (partitioner != null)
+                return cfMetaData.copy(partitioner);
+            else
+                return cfMetaData;
+        }
+
+        /**
+         * Prepares insert statement for writing data to SSTable
+         *
+         * @return prepared Insert statement and it's bound names
+         */
+        private Pair<UpdateStatement, List<ColumnSpecification>> prepareInsert()
+        {
+            ParsedStatement.Prepared cqlStatement = insertStatement.prepare(ClientState.forInternalCalls());
+            UpdateStatement insert = (UpdateStatement) cqlStatement.statement;
+            insert.validate(ClientState.forInternalCalls());
+
+            if (insert.hasConditions())
+                throw new IllegalArgumentException("Conditional statements are not supported");
+            if (insert.isCounter())
+                throw new IllegalArgumentException("Counter update statements are not supported");
+            if (cqlStatement.boundNames.isEmpty())
+                throw new IllegalArgumentException("Provided insert statement has no bind variables");
+
+            return Pair.create(insert, cqlStatement.boundNames);
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/io/sstable/Component.java b/src/java/org/apache/cassandra/io/sstable/Component.java
index 9454882..d0dd185 100644
--- a/src/java/org/apache/cassandra/io/sstable/Component.java
+++ b/src/java/org/apache/cassandra/io/sstable/Component.java
@@ -19,7 +19,9 @@
 
 import java.io.File;
 import java.util.EnumSet;
+import java.util.regex.Pattern;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Objects;
 
 import org.apache.cassandra.utils.ChecksumType;
@@ -57,6 +59,8 @@
         SUMMARY("Summary.db"),
         // table of contents, stores the list of all components for the sstable
         TOC("TOC.txt"),
+        // built-in secondary index (may be multiple per sstable)
+        SECONDARY_INDEX("SI_.*.db"),
         // custom component, used by e.g. custom compaction strategy
         CUSTOM(new String[] { null });
         
@@ -71,12 +75,16 @@
             this.repr = repr;
         }
 
-        static Type fromRepresentation(String repr)
+        @VisibleForTesting
+        public static Type fromRepresentation(String repr)
         {
             for (Type type : TYPES)
-                for (String representation : type.repr)
-                    if (repr.equals(representation))
-                        return type;
+            {
+                if (type.repr == null || type.repr.length == 0 || type.repr[0] == null)
+                    continue;
+                if (Pattern.matches(type.repr[0], repr))
+                    return type;
+            }
             return CUSTOM;
         }
     }
@@ -169,6 +177,7 @@
             case CRC:               component = Component.CRC;                          break;
             case SUMMARY:           component = Component.SUMMARY;                      break;
             case TOC:               component = Component.TOC;                          break;
+            case SECONDARY_INDEX:   component = new Component(Type.SECONDARY_INDEX, path.right); break;
             case CUSTOM:            component = new Component(Type.CUSTOM, path.right); break;
             default:
                  throw new IllegalStateException();
diff --git a/src/java/org/apache/cassandra/io/sstable/Descriptor.java b/src/java/org/apache/cassandra/io/sstable/Descriptor.java
index 3828a15..7950e7b 100644
--- a/src/java/org/apache/cassandra/io/sstable/Descriptor.java
+++ b/src/java/org/apache/cassandra/io/sstable/Descriptor.java
@@ -27,11 +27,9 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Objects;
 
-import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.Directories;
 import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.Version;
-import org.apache.cassandra.io.sstable.format.big.BigFormat;
 import org.apache.cassandra.io.sstable.metadata.IMetadataSerializer;
 import org.apache.cassandra.io.sstable.metadata.LegacyMetadataSerializer;
 import org.apache.cassandra.io.sstable.metadata.MetadataSerializer;
@@ -68,7 +66,7 @@
     @VisibleForTesting
     public Descriptor(File directory, String ksname, String cfname, int generation)
     {
-        this(DatabaseDescriptor.getSSTableFormat().info.getLatestVersion(), directory, ksname, cfname, generation, DatabaseDescriptor.getSSTableFormat(), null);
+        this(SSTableFormat.Type.current().info.getLatestVersion(), directory, ksname, cfname, generation, SSTableFormat.Type.current(), null);
     }
 
     /**
@@ -76,13 +74,13 @@
      */
     public Descriptor(File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType)
     {
-        this(formatType.info.getLatestVersion(), directory, ksname, cfname, generation, formatType, Component.digestFor(BigFormat.latestVersion.uncompressedChecksumType()));
+        this(formatType.info.getLatestVersion(), directory, ksname, cfname, generation, formatType, Component.digestFor(formatType.info.getLatestVersion().uncompressedChecksumType()));
     }
 
     @VisibleForTesting
     public Descriptor(String version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType)
     {
-        this(formatType.info.getVersion(version), directory, ksname, cfname, generation, formatType, Component.digestFor(BigFormat.latestVersion.uncompressedChecksumType()));
+        this(formatType.info.getVersion(version), directory, ksname, cfname, generation, formatType, Component.digestFor(formatType.info.getLatestVersion().uncompressedChecksumType()));
     }
 
     public Descriptor(Version version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType, Component digestComponent)
@@ -283,11 +281,12 @@
 
         // version
         nexttok = tokenStack.pop();
-        Version version = fmt.info.getVersion(nexttok);
 
-        if (!version.validate(nexttok))
+        if (!Version.validate(nexttok))
             throw new UnsupportedOperationException("SSTable " + name + " is too old to open.  Upgrade to 2.0 first, and run upgradesstables");
 
+        Version version = fmt.info.getVersion(nexttok);
+
         // ks/cf names
         String ksname, cfname;
         if (version.hasNewFileName())
diff --git a/src/java/org/apache/cassandra/io/sstable/ISSTableScanner.java b/src/java/org/apache/cassandra/io/sstable/ISSTableScanner.java
index 7063057..2dff34e 100644
--- a/src/java/org/apache/cassandra/io/sstable/ISSTableScanner.java
+++ b/src/java/org/apache/cassandra/io/sstable/ISSTableScanner.java
@@ -28,6 +28,8 @@
 public interface ISSTableScanner extends UnfilteredPartitionIterator
 {
     public long getLengthInBytes();
+    public long getCompressedLengthInBytes();
     public long getCurrentPosition();
+    public long getBytesScanned();
     public String getBackingFiles();
 }
diff --git a/src/java/org/apache/cassandra/io/sstable/IndexHelper.java b/src/java/org/apache/cassandra/io/sstable/IndexHelper.java
deleted file mode 100644
index 74a0fc5..0000000
--- a/src/java/org/apache/cassandra/io/sstable/IndexHelper.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * 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.io.sstable;
-
-import java.io.*;
-import java.util.Collections;
-import java.util.List;
-
-import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.db.*;
-import org.apache.cassandra.io.ISerializer;
-import org.apache.cassandra.io.sstable.format.Version;
-import org.apache.cassandra.io.util.DataInputPlus;
-import org.apache.cassandra.io.util.DataOutputPlus;
-import org.apache.cassandra.utils.*;
-
-/**
- * Provides helper to serialize, deserialize and use column indexes.
- */
-public final class IndexHelper
-{
-    private IndexHelper()
-    {
-    }
-
-    /**
-     * The index of the IndexInfo in which a scan starting with @name should begin.
-     *
-     * @param name name to search for
-     * @param indexList list of the indexInfo objects
-     * @param comparator the comparator to use
-     * @param reversed whether or not the search is reversed, i.e. we scan forward or backward from name
-     * @param lastIndex where to start the search from in indexList
-     *
-     * @return int index
-     */
-    public static int indexFor(ClusteringPrefix name, List<IndexInfo> indexList, ClusteringComparator comparator, boolean reversed, int lastIndex)
-    {
-        IndexInfo target = new IndexInfo(name, name, 0, 0, null);
-        /*
-        Take the example from the unit test, and say your index looks like this:
-        [0..5][10..15][20..25]
-        and you look for the slice [13..17].
-
-        When doing forward slice, we are doing a binary search comparing 13 (the start of the query)
-        to the lastName part of the index slot. You'll end up with the "first" slot, going from left to right,
-        that may contain the start.
-
-        When doing a reverse slice, we do the same thing, only using as a start column the end of the query,
-        i.e. 17 in this example, compared to the firstName part of the index slots.  bsearch will give us the
-        first slot where firstName > start ([20..25] here), so we subtract an extra one to get the slot just before.
-        */
-        int startIdx = 0;
-        List<IndexInfo> toSearch = indexList;
-        if (reversed)
-        {
-            if (lastIndex < indexList.size() - 1)
-            {
-                toSearch = indexList.subList(0, lastIndex + 1);
-            }
-        }
-        else
-        {
-            if (lastIndex > 0)
-            {
-                startIdx = lastIndex;
-                toSearch = indexList.subList(lastIndex, indexList.size());
-            }
-        }
-        int index = Collections.binarySearch(toSearch, target, comparator.indexComparator(reversed));
-        return startIdx + (index < 0 ? -index - (reversed ? 2 : 1) : index);
-    }
-
-    public static class IndexInfo
-    {
-        private static final long EMPTY_SIZE = ObjectSizes.measure(new IndexInfo(null, null, 0, 0, null));
-
-        public final long offset;
-        public final long width;
-        public final ClusteringPrefix firstName;
-        public final ClusteringPrefix lastName;
-
-        // If at the end of the index block there is an open range tombstone marker, this marker
-        // deletion infos. null otherwise.
-        public final DeletionTime endOpenMarker;
-
-        public IndexInfo(ClusteringPrefix firstName,
-                         ClusteringPrefix lastName,
-                         long offset,
-                         long width,
-                         DeletionTime endOpenMarker)
-        {
-            this.firstName = firstName;
-            this.lastName = lastName;
-            this.offset = offset;
-            this.width = width;
-            this.endOpenMarker = endOpenMarker;
-        }
-
-        public static class Serializer
-        {
-            // This is the default index size that we use to delta-encode width when serializing so we get better vint-encoding.
-            // This is imperfect as user can change the index size and ideally we would save the index size used with each index file
-            // to use as base. However, that's a bit more involved a change that we want for now and very seldom do use change the index
-            // size so using the default is almost surely better than using no base at all.
-            public static final long WIDTH_BASE = 64 * 1024;
-
-            private final ISerializer<ClusteringPrefix> clusteringSerializer;
-            private final Version version;
-
-            public Serializer(CFMetaData metadata, Version version, SerializationHeader header)
-            {
-                this.clusteringSerializer = metadata.serializers().indexEntryClusteringPrefixSerializer(version, header);
-                this.version = version;
-            }
-
-            public void serialize(IndexInfo info, DataOutputPlus out) throws IOException
-            {
-                assert version.storeRows() : "We read old index files but we should never write them";
-
-                clusteringSerializer.serialize(info.firstName, out);
-                clusteringSerializer.serialize(info.lastName, out);
-                out.writeUnsignedVInt(info.offset);
-                out.writeVInt(info.width - WIDTH_BASE);
-
-                out.writeBoolean(info.endOpenMarker != null);
-                if (info.endOpenMarker != null)
-                    DeletionTime.serializer.serialize(info.endOpenMarker, out);
-            }
-
-            public IndexInfo deserialize(DataInputPlus in) throws IOException
-            {
-                ClusteringPrefix firstName = clusteringSerializer.deserialize(in);
-                ClusteringPrefix lastName = clusteringSerializer.deserialize(in);
-                long offset;
-                long width;
-                DeletionTime endOpenMarker = null;
-                if (version.storeRows())
-                {
-                    offset = in.readUnsignedVInt();
-                    width = in.readVInt() + WIDTH_BASE;
-                    if (in.readBoolean())
-                        endOpenMarker = DeletionTime.serializer.deserialize(in);
-                }
-                else
-                {
-                    offset = in.readLong();
-                    width = in.readLong();
-                }
-                return new IndexInfo(firstName, lastName, offset, width, endOpenMarker);
-            }
-
-            public long serializedSize(IndexInfo info)
-            {
-                assert version.storeRows() : "We read old index files but we should never write them";
-
-                long size = clusteringSerializer.serializedSize(info.firstName)
-                          + clusteringSerializer.serializedSize(info.lastName)
-                          + TypeSizes.sizeofUnsignedVInt(info.offset)
-                          + TypeSizes.sizeofVInt(info.width - WIDTH_BASE)
-                          + TypeSizes.sizeof(info.endOpenMarker != null);
-
-                if (info.endOpenMarker != null)
-                    size += DeletionTime.serializer.serializedSize(info.endOpenMarker);
-                return size;
-            }
-        }
-
-        public long unsharedHeapSize()
-        {
-            return EMPTY_SIZE
-                 + firstName.unsharedHeapSize()
-                 + lastName.unsharedHeapSize()
-                 + (endOpenMarker == null ? 0 : endOpenMarker.unsharedHeapSize());
-        }
-    }
-}
diff --git a/src/java/org/apache/cassandra/io/sstable/IndexInfo.java b/src/java/org/apache/cassandra/io/sstable/IndexInfo.java
new file mode 100644
index 0000000..9ee1996
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/sstable/IndexInfo.java
@@ -0,0 +1,176 @@
+/*
+ * 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.io.sstable;
+
+import java.io.IOException;
+
+import org.apache.cassandra.db.ClusteringPrefix;
+import org.apache.cassandra.db.DeletionTime;
+import org.apache.cassandra.db.RowIndexEntry;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.ISerializer;
+import org.apache.cassandra.io.sstable.format.Version;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.utils.ObjectSizes;
+
+/**
+ * {@code IndexInfo} is embedded in the indexed version of {@link RowIndexEntry}.
+ * Each instance roughly covers a range of {@link org.apache.cassandra.config.Config#column_index_size_in_kb column_index_size_in_kb} kB
+ * and contains the first and last clustering value (or slice bound), its offset in the data file and width in the data file.
+ * <p>
+ * Each {@code IndexInfo} object is serialized as follows.
+ * </p>
+ * <p>
+ * Serialization format changed in 3.0; the {@link #endOpenMarker} has been introduced and integer fields are
+ * stored using varint encoding.
+ * </p>
+ * <p>
+ * {@code
+ *    (*) IndexInfo.firstName (ClusteringPrefix serializer, either Clustering.serializer.serialize or Slice.Bound.serializer.serialize)
+ *    (*) IndexInfo.lastName (ClusteringPrefix serializer, either Clustering.serializer.serialize or Slice.Bound.serializer.serialize)
+ * (long) IndexInfo.offset
+ * (long) IndexInfo.width
+ * (bool) IndexInfo.endOpenMarker != null              (if 3.0)
+ *  (int) IndexInfo.endOpenMarker.localDeletionTime    (if 3.0 && IndexInfo.endOpenMarker != null)
+ * (long) IndexInfo.endOpenMarker.markedForDeletionAt  (if 3.0 && IndexInfo.endOpenMarker != null)
+ * }
+ * </p>
+ */
+public class IndexInfo
+{
+    private static final long EMPTY_SIZE = ObjectSizes.measure(new IndexInfo(null, null, 0, 0, null));
+
+    public final long offset;
+    public final long width;
+    public final ClusteringPrefix firstName;
+    public final ClusteringPrefix lastName;
+
+    // If at the end of the index block there is an open range tombstone marker, this marker
+    // deletion infos. null otherwise.
+    public final DeletionTime endOpenMarker;
+
+    public IndexInfo(ClusteringPrefix firstName,
+                     ClusteringPrefix lastName,
+                     long offset,
+                     long width,
+                     DeletionTime endOpenMarker)
+    {
+        this.firstName = firstName;
+        this.lastName = lastName;
+        this.offset = offset;
+        this.width = width;
+        this.endOpenMarker = endOpenMarker;
+    }
+
+    public static class Serializer implements ISerializer<IndexInfo>
+    {
+        // This is the default index size that we use to delta-encode width when serializing so we get better vint-encoding.
+        // This is imperfect as user can change the index size and ideally we would save the index size used with each index file
+        // to use as base. However, that's a bit more involved a change that we want for now and very seldom do use change the index
+        // size so using the default is almost surely better than using no base at all.
+        public static final long WIDTH_BASE = 64 * 1024;
+
+        private final ISerializer<ClusteringPrefix> clusteringSerializer;
+        private final Version version;
+
+        public Serializer(Version version, ISerializer<ClusteringPrefix> clusteringSerializer)
+        {
+            this.clusteringSerializer = clusteringSerializer;
+            this.version = version;
+        }
+
+        public void serialize(IndexInfo info, DataOutputPlus out) throws IOException
+        {
+            assert version.storeRows() : "We read old index files but we should never write them";
+
+            clusteringSerializer.serialize(info.firstName, out);
+            clusteringSerializer.serialize(info.lastName, out);
+            out.writeUnsignedVInt(info.offset);
+            out.writeVInt(info.width - WIDTH_BASE);
+
+            out.writeBoolean(info.endOpenMarker != null);
+            if (info.endOpenMarker != null)
+                DeletionTime.serializer.serialize(info.endOpenMarker, out);
+        }
+
+        public void skip(DataInputPlus in) throws IOException
+        {
+            clusteringSerializer.skip(in);
+            clusteringSerializer.skip(in);
+            if (version.storeRows())
+            {
+                in.readUnsignedVInt();
+                in.readVInt();
+                if (in.readBoolean())
+                    DeletionTime.serializer.skip(in);
+            }
+            else
+            {
+                in.skipBytes(TypeSizes.sizeof(0L));
+                in.skipBytes(TypeSizes.sizeof(0L));
+            }
+        }
+
+        public IndexInfo deserialize(DataInputPlus in) throws IOException
+        {
+            ClusteringPrefix firstName = clusteringSerializer.deserialize(in);
+            ClusteringPrefix lastName = clusteringSerializer.deserialize(in);
+            long offset;
+            long width;
+            DeletionTime endOpenMarker = null;
+            if (version.storeRows())
+            {
+                offset = in.readUnsignedVInt();
+                width = in.readVInt() + WIDTH_BASE;
+                if (in.readBoolean())
+                    endOpenMarker = DeletionTime.serializer.deserialize(in);
+            }
+            else
+            {
+                offset = in.readLong();
+                width = in.readLong();
+            }
+            return new IndexInfo(firstName, lastName, offset, width, endOpenMarker);
+        }
+
+        public long serializedSize(IndexInfo info)
+        {
+            assert version.storeRows() : "We read old index files but we should never write them";
+
+            long size = clusteringSerializer.serializedSize(info.firstName)
+                        + clusteringSerializer.serializedSize(info.lastName)
+                        + TypeSizes.sizeofUnsignedVInt(info.offset)
+                        + TypeSizes.sizeofVInt(info.width - WIDTH_BASE)
+                        + TypeSizes.sizeof(info.endOpenMarker != null);
+
+            if (info.endOpenMarker != null)
+                size += DeletionTime.serializer.serializedSize(info.endOpenMarker);
+            return size;
+        }
+    }
+
+    public long unsharedHeapSize()
+    {
+        return EMPTY_SIZE
+             + firstName.unsharedHeapSize()
+             + lastName.unsharedHeapSize()
+             + (endOpenMarker == null ? 0 : endOpenMarker.unsharedHeapSize());
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/sstable/IndexSummary.java b/src/java/org/apache/cassandra/io/sstable/IndexSummary.java
index 371a243..6de3478 100644
--- a/src/java/org/apache/cassandra/io/sstable/IndexSummary.java
+++ b/src/java/org/apache/cassandra/io/sstable/IndexSummary.java
@@ -29,7 +29,9 @@
 import org.apache.cassandra.db.PartitionPosition;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.io.util.*;
+import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.Pair;
 import org.apache.cassandra.utils.concurrent.Ref;
 import org.apache.cassandra.utils.concurrent.WrappedSharedCloseable;
 import org.apache.cassandra.utils.memory.MemoryUtil;
@@ -347,5 +349,26 @@
                 offsets.setInt(i, (int) (offsets.getInt(i) - offsets.size()));
             return new IndexSummary(partitioner, offsets, offsetCount, entries, entries.size(), fullSamplingSummarySize, minIndexInterval, samplingLevel);
         }
+
+        /**
+         * Deserializes the first and last key stored in the summary
+         *
+         * Only for use by offline tools like SSTableMetadataViewer, otherwise SSTable.first/last should be used.
+         */
+        public Pair<DecoratedKey, DecoratedKey> deserializeFirstLastKey(DataInputStream in, IPartitioner partitioner, boolean haveSamplingLevel) throws IOException
+        {
+            in.skipBytes(4); // minIndexInterval
+            int offsetCount = in.readInt();
+            long offheapSize = in.readLong();
+            if (haveSamplingLevel)
+                in.skipBytes(8); // samplingLevel, fullSamplingSummarySize
+
+            in.skip(offsetCount * 4);
+            in.skip(offheapSize - offsetCount * 4);
+
+            DecoratedKey first = partitioner.decorateKey(ByteBufferUtil.readWithLength(in));
+            DecoratedKey last = partitioner.decorateKey(ByteBufferUtil.readWithLength(in));
+            return Pair.create(first, last);
+        }
     }
 }
diff --git a/src/java/org/apache/cassandra/io/sstable/IndexSummaryRedistribution.java b/src/java/org/apache/cassandra/io/sstable/IndexSummaryRedistribution.java
index 45bd7eb..b914963 100644
--- a/src/java/org/apache/cassandra/io/sstable/IndexSummaryRedistribution.java
+++ b/src/java/org/apache/cassandra/io/sstable/IndexSummaryRedistribution.java
@@ -42,6 +42,7 @@
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.metrics.StorageMetrics;
+import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.Pair;
 import org.apache.cassandra.utils.concurrent.Refs;
 
@@ -137,8 +138,8 @@
         total = 0;
         for (SSTableReader sstable : Iterables.concat(compacting, oldFormatSSTables, newSSTables))
             total += sstable.getIndexSummaryOffHeapSize();
-        logger.trace("Completed resizing of index summaries; current approximate memory used: {} MB",
-                     total / 1024.0 / 1024.0);
+        logger.trace("Completed resizing of index summaries; current approximate memory used: {}",
+                     FBUtilities.prettyPrintMemory(total));
 
         return newSSTables;
     }
@@ -189,11 +190,12 @@
             int numEntriesAtNewSamplingLevel = IndexSummaryBuilder.entriesAtSamplingLevel(newSamplingLevel, maxSummarySize);
             double effectiveIndexInterval = sstable.getEffectiveIndexInterval();
 
-            logger.trace("{} has {} reads/sec; ideal space for index summary: {} bytes ({} entries); considering moving " +
-                    "from level {} ({} entries, {} bytes) to level {} ({} entries, {} bytes)",
-                    sstable.getFilename(), readsPerSec, idealSpace, targetNumEntries, currentSamplingLevel, currentNumEntries,
-                    currentNumEntries * avgEntrySize, newSamplingLevel, numEntriesAtNewSamplingLevel,
-                    numEntriesAtNewSamplingLevel * avgEntrySize);
+            logger.trace("{} has {} reads/sec; ideal space for index summary: {} ({} entries); considering moving " +
+                    "from level {} ({} entries, {}) " +
+                    "to level {} ({} entries, {})",
+                    sstable.getFilename(), readsPerSec, FBUtilities.prettyPrintMemory(idealSpace), targetNumEntries,
+                    currentSamplingLevel, currentNumEntries, FBUtilities.prettyPrintMemory((long) (currentNumEntries * avgEntrySize)),
+                    newSamplingLevel, numEntriesAtNewSamplingLevel, FBUtilities.prettyPrintMemory((long) (numEntriesAtNewSamplingLevel * avgEntrySize)));
 
             if (effectiveIndexInterval < minIndexInterval)
             {
diff --git a/src/java/org/apache/cassandra/io/sstable/KeyIterator.java b/src/java/org/apache/cassandra/io/sstable/KeyIterator.java
index f02b9d1..d51e97b 100644
--- a/src/java/org/apache/cassandra/io/sstable/KeyIterator.java
+++ b/src/java/org/apache/cassandra/io/sstable/KeyIterator.java
@@ -84,6 +84,7 @@
     private final In in;
     private final IPartitioner partitioner;
 
+    private long keyPosition;
 
     public KeyIterator(Descriptor desc, CFMetaData metadata)
     {
@@ -99,6 +100,7 @@
             if (in.isEOF())
                 return endOfData();
 
+            keyPosition = in.getFilePointer();
             DecoratedKey key = partitioner.decorateKey(ByteBufferUtil.readWithShortLength(in.get()));
             RowIndexEntry.Serializer.skip(in.get(), desc.version); // skip remainder of the entry
             return key;
@@ -123,4 +125,9 @@
     {
         return in.length();
     }
+
+    public long getKeyPosition()
+    {
+        return keyPosition;
+    }
 }
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTable.java b/src/java/org/apache/cassandra/io/sstable/SSTable.java
index 6bfb0bf..a64d086 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTable.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTable.java
@@ -36,6 +36,7 @@
 import org.apache.cassandra.db.RowIndexEntry;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.io.FSWriteError;
+import org.apache.cassandra.io.util.DiskOptimizationStrategy;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.io.util.RandomAccessReader;
 import org.apache.cassandra.utils.ByteBufferUtil;
@@ -58,7 +59,6 @@
 {
     static final Logger logger = LoggerFactory.getLogger(SSTable.class);
 
-
     public static final int TOMBSTONE_HISTOGRAM_BIN_SIZE = 100;
     public static final int TOMBSTONE_HISTOGRAM_SPOOL_SIZE = 100000;
     public static final int TOMBSTONE_HISTOGRAM_TTL_ROUND_SECONDS = Integer.valueOf(System.getProperty("cassandra.streaminghistogram.roundseconds", "60"));
@@ -71,12 +71,9 @@
     public DecoratedKey first;
     public DecoratedKey last;
 
-    protected SSTable(Descriptor descriptor, CFMetaData metadata)
-    {
-        this(descriptor, new HashSet<>(), metadata);
-    }
+    protected final DiskOptimizationStrategy optimizationStrategy;
 
-    protected SSTable(Descriptor descriptor, Set<Component> components, CFMetaData metadata)
+    protected SSTable(Descriptor descriptor, Set<Component> components, CFMetaData metadata, DiskOptimizationStrategy optimizationStrategy)
     {
         // In almost all cases, metadata shouldn't be null, but allowing null allows to create a mostly functional SSTable without
         // full schema definition. SSTableLoader use that ability
@@ -89,6 +86,7 @@
         this.compression = dataComponents.contains(Component.COMPRESSION_INFO);
         this.components = new CopyOnWriteArraySet<>(dataComponents);
         this.metadata = metadata;
+        this.optimizationStrategy = Objects.requireNonNull(optimizationStrategy);
     }
 
     /**
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableHeaderFix.java b/src/java/org/apache/cassandra/io/sstable/SSTableHeaderFix.java
new file mode 100644
index 0000000..5dad2b9
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/sstable/SSTableHeaderFix.java
@@ -0,0 +1,921 @@
+/*
+ * 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.io.sstable;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
+import org.apache.cassandra.cql3.CQL3Type;
+import org.apache.cassandra.cql3.statements.IndexTarget;
+import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.db.SerializationHeader;
+import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
+import org.apache.cassandra.db.marshal.AbstractCompositeType;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.CollectionType;
+import org.apache.cassandra.db.marshal.CompositeType;
+import org.apache.cassandra.db.marshal.DynamicCompositeType;
+import org.apache.cassandra.db.marshal.ListType;
+import org.apache.cassandra.db.marshal.MapType;
+import org.apache.cassandra.db.marshal.SetType;
+import org.apache.cassandra.db.marshal.TupleType;
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.io.sstable.metadata.MetadataComponent;
+import org.apache.cassandra.io.sstable.metadata.MetadataType;
+import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.CassandraVersion;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.Pair;
+
+/**
+ * Validates and fixes type issues in the serialization-header of sstables.
+ */
+public abstract class SSTableHeaderFix
+{
+    // C* 3.0 upgrade code
+
+    private static final String SKIPAUTOMATICUDTFIX = "cassandra.skipautomaticudtfix";
+    private static final boolean SKIP_AUTOMATIC_FIX_ON_UPGRADE = Boolean.getBoolean(SKIPAUTOMATICUDTFIX);
+
+    public static void fixNonFrozenUDTIfUpgradeFrom30()
+    {
+        String previousVersionString = FBUtilities.getPreviousReleaseVersionString();
+        if (previousVersionString == null)
+            return;
+        CassandraVersion previousVersion = new CassandraVersion(previousVersionString);
+        if (previousVersion.major != 3 || previousVersion.minor > 0)
+        {
+            // Not an upgrade from 3.0 to 3.x, nothing to do here
+            return;
+        }
+
+        if (SKIP_AUTOMATIC_FIX_ON_UPGRADE)
+        {
+            logger.warn("Detected upgrade from {} to {}, but -D{}=true, NOT fixing UDT type references in " +
+                        "sstable metadata serialization-headers",
+                        previousVersionString,
+                        FBUtilities.getReleaseVersionString(),
+                        SKIPAUTOMATICUDTFIX);
+            return;
+        }
+
+        logger.info("Detected upgrade from {} to {}, fixing UDT type references in sstable metadata serialization-headers",
+                    previousVersionString,
+                    FBUtilities.getReleaseVersionString());
+
+        SSTableHeaderFix instance = SSTableHeaderFix.builder()
+                                                    .schemaCallback(() -> Schema.instance::getCFMetaData)
+                                                    .build();
+        instance.execute();
+    }
+
+    // "regular" SSTableHeaderFix code, also used by StandaloneScrubber.
+
+    private static final Logger logger = LoggerFactory.getLogger(SSTableHeaderFix.class);
+
+    protected final Consumer<String> info;
+    protected final Consumer<String> warn;
+    protected final Consumer<String> error;
+    protected final boolean dryRun;
+    protected final Function<Descriptor, CFMetaData> schemaCallback;
+
+    private final List<Descriptor> descriptors;
+
+    private final List<Pair<Descriptor, Map<MetadataType, MetadataComponent>>> updates = new ArrayList<>();
+    private boolean hasErrors;
+
+    SSTableHeaderFix(Builder builder)
+    {
+        this.info = builder.info;
+        this.warn = builder.warn;
+        this.error = builder.error;
+        this.dryRun = builder.dryRun;
+        this.schemaCallback = builder.schemaCallback.get();
+        this.descriptors = new ArrayList<>(builder.descriptors);
+        Objects.requireNonNull(this.info, "info is null");
+        Objects.requireNonNull(this.warn, "warn is null");
+        Objects.requireNonNull(this.error, "error is null");
+        Objects.requireNonNull(this.schemaCallback, "schemaCallback is null");
+    }
+
+    public static Builder builder()
+    {
+        return new Builder();
+    }
+
+    /**
+     * Builder to configure and construct an instance of {@link SSTableHeaderFix}.
+     * Default settings:
+     * <ul>
+     *     <li>log via the slf4j logger of {@link SSTableHeaderFix}</li>
+     *     <li>no dry-run (i.e. validate and fix, if no serious errors are detected)</li>
+     *     <li>no schema callback</li>
+     * </ul>
+     * If neither {@link #withDescriptor(Descriptor)} nor {@link #withPath(Path)} are used,
+     * all "live" sstables in all data directories will be scanned.
+     */
+    public static class Builder
+    {
+        private final List<Path> paths = new ArrayList<>();
+        private final List<Descriptor> descriptors = new ArrayList<>();
+        private Consumer<String> info = (ln) -> logger.info("{}", ln);
+        private Consumer<String> warn = (ln) -> logger.warn("{}", ln);
+        private Consumer<String> error = (ln) -> logger.error("{}", ln);
+        private boolean dryRun;
+        private Supplier<Function<Descriptor, CFMetaData>> schemaCallback = () -> null;
+
+        private Builder()
+        {}
+
+        /**
+         * Only validate and prepare fix, but do not write updated (fixed) sstable serialization-headers.
+         */
+        public Builder dryRun()
+        {
+            dryRun = true;
+            return this;
+        }
+
+        public Builder info(Consumer<String> output)
+        {
+            this.info = output;
+            return this;
+        }
+
+        public Builder warn(Consumer<String> warn)
+        {
+            this.warn = warn;
+            return this;
+        }
+
+        public Builder error(Consumer<String> error)
+        {
+            this.error = error;
+            return this;
+        }
+
+        /**
+         * Manually provide an individual sstable or directory containing sstables.
+         *
+         * Implementation note: procesing "live" sstables in their data directories as well as sstables
+         * in snapshots and backups in the data directories works.
+         *
+         * But processing sstables that reside somewhere else (i.e. verifying sstables before import)
+         * requires the use of {@link #withDescriptor(Descriptor)}.
+         */
+        public Builder withPath(Path path)
+        {
+            this.paths.add(path);
+            return this;
+        }
+
+        public Builder withDescriptor(Descriptor descriptor)
+        {
+            this.descriptors.add(descriptor);
+            return this;
+        }
+
+        /**
+         * Schema callback to retrieve the schema of a table. Production code always delegates to the
+         * live schema ({@code Schema.instance}). Unit tests use this method to feed a custom schema.
+         */
+        public Builder schemaCallback(Supplier<Function<Descriptor, CFMetaData>> schemaCallback)
+        {
+            this.schemaCallback = schemaCallback;
+            return this;
+        }
+
+        public SSTableHeaderFix build()
+        {
+            if (paths.isEmpty() && descriptors.isEmpty())
+                return new AutomaticHeaderFix(this);
+
+            return new ManualHeaderFix(this);
+        }
+
+        public Builder logToList(List<String> output)
+        {
+            return info(ln -> output.add("INFO  " + ln))
+                   .warn(ln -> output.add("WARN  " + ln))
+                   .error(ln -> output.add("ERROR " + ln));
+        }
+    }
+
+    public final void execute()
+    {
+        prepare();
+
+        logger.debug("Processing {} sstables:{}",
+                     descriptors.size(),
+                     descriptors.stream().map(Descriptor::toString).collect(Collectors.joining("\n    ", "\n    ", "")));
+
+        descriptors.forEach(this::processSSTable);
+
+        if (updates.isEmpty())
+            return;
+
+        if (hasErrors)
+        {
+            info.accept("Stopping due to previous errors. Either fix the errors or specify the ignore-errors option.");
+            return;
+        }
+
+        if (dryRun)
+        {
+            info.accept("Not fixing identified and fixable serialization-header issues.");
+            return;
+        }
+
+        info.accept("Writing new metadata files");
+        updates.forEach(descAndMeta -> writeNewMetadata(descAndMeta.left, descAndMeta.right));
+        info.accept("Finished writing new metadata files");
+    }
+
+    /**
+     * Whether {@link #execute()} encountered an error.
+     */
+    public boolean hasError()
+    {
+        return hasErrors;
+    }
+
+    /**
+     * Whether {@link #execute()} found mismatches.
+     */
+    public boolean hasChanges()
+    {
+        return !updates.isEmpty();
+    }
+
+    abstract void prepare();
+
+    private void error(String format, Object... args)
+    {
+        hasErrors = true;
+        error.accept(String.format(format, args));
+    }
+
+    void processFileOrDirectory(Path path)
+    {
+        Stream.of(path)
+              .flatMap(SSTableHeaderFix::maybeExpandDirectory)
+              .filter(p -> {
+                  File f = p.toFile();
+                  return Component.fromFilename(f.getParentFile(), f.getName()).right.type == Component.Type.DATA;
+              })
+              .map(Path::toString)
+              .map(Descriptor::fromFilename)
+              .forEach(descriptors::add);
+    }
+
+    private static Stream<Path> maybeExpandDirectory(Path path)
+    {
+        if (Files.isRegularFile(path))
+            return Stream.of(path);
+        return LifecycleTransaction.getFiles(path, (file, fileType) -> fileType == Directories.FileType.FINAL, Directories.OnTxnErr.IGNORE)
+                                   .stream()
+                                   .map(File::toPath);
+    }
+
+    private void processSSTable(Descriptor desc)
+    {
+        if (desc.cfname.indexOf('.') != -1)
+        {
+            // secondary index not checked
+
+            // partition-key is the indexed column type
+            // clustering-key is org.apache.cassandra.db.marshal.PartitionerDefinedOrder
+            // no static columns, no regular columns
+            return;
+        }
+
+        CFMetaData tableMetadata = schemaCallback.apply(desc);
+        if (tableMetadata == null)
+        {
+            error("Table %s.%s not found in the schema - NOT checking sstable %s", desc.ksname, desc.cfname, desc);
+            return;
+        }
+
+        Set<Component> components = SSTable.discoverComponentsFor(desc);
+        if (components.stream().noneMatch(c -> c.type == Component.Type.STATS))
+        {
+            error("sstable %s has no -Statistics.db component.", desc);
+            return;
+        }
+
+        Map<MetadataType, MetadataComponent> metadata = readSSTableMetadata(desc);
+        if (metadata == null)
+            return;
+
+        MetadataComponent component = metadata.get(MetadataType.HEADER);
+        if (!(component instanceof SerializationHeader.Component))
+        {
+            error("sstable %s: Expected %s, but got %s from metadata.get(MetadataType.HEADER)",
+                  desc,
+                  SerializationHeader.Component.class.getName(),
+                  component != null ? component.getClass().getName() : "'null'");
+            return;
+        }
+        SerializationHeader.Component header = (SerializationHeader.Component) component;
+
+        // check partition key type
+        AbstractType<?> keyType = validatePartitionKey(desc, tableMetadata, header);
+
+        // check clustering columns
+        List<AbstractType<?>> clusteringTypes = validateClusteringColumns(desc, tableMetadata, header);
+
+        // check static and regular columns
+        Map<ByteBuffer, AbstractType<?>> staticColumns = validateColumns(desc, tableMetadata, header.getStaticColumns(), ColumnDefinition.Kind.STATIC);
+        Map<ByteBuffer, AbstractType<?>> regularColumns = validateColumns(desc, tableMetadata, header.getRegularColumns(), ColumnDefinition.Kind.REGULAR);
+
+        SerializationHeader.Component newHeader = SerializationHeader.Component.buildComponentForTools(keyType,
+                                                                                                       clusteringTypes,
+                                                                                                       staticColumns,
+                                                                                                       regularColumns,
+                                                                                                       header.getEncodingStats());
+
+        // SerializationHeader.Component has no equals(), but a "good" toString()
+        if (header.toString().equals(newHeader.toString()))
+            return;
+
+        Map<MetadataType, MetadataComponent> newMetadata = new LinkedHashMap<>(metadata);
+        newMetadata.put(MetadataType.HEADER, newHeader);
+
+        updates.add(Pair.create(desc, newMetadata));
+    }
+
+    private AbstractType<?> validatePartitionKey(Descriptor desc, CFMetaData tableMetadata, SerializationHeader.Component header)
+    {
+        boolean keyMismatch = false;
+        AbstractType<?> headerKeyType = header.getKeyType();
+        AbstractType<?> schemaKeyType = tableMetadata.getKeyValidator();
+        boolean headerKeyComposite = headerKeyType instanceof CompositeType;
+        boolean schemaKeyComposite = schemaKeyType instanceof CompositeType;
+        if (headerKeyComposite != schemaKeyComposite)
+        {
+            // one is a composite partition key, the other is not - very suspicious
+            keyMismatch = true;
+        }
+        else if (headerKeyComposite) // && schemaKeyComposite
+        {
+            // Note, the logic is similar as just calling 'fixType()' using the composite partition key,
+            // but the log messages should use the composite partition key column names.
+            List<AbstractType<?>> headerKeyComponents = ((CompositeType) headerKeyType).types;
+            List<AbstractType<?>> schemaKeyComponents = ((CompositeType) schemaKeyType).types;
+            if (headerKeyComponents.size() != schemaKeyComponents.size())
+            {
+                // different number of components in composite partition keys - very suspicious
+                keyMismatch = true;
+                // Just use the original type from the header. Since the number of partition key components
+                // don't match, there's nothing to meaningfully validate against.
+            }
+            else
+            {
+                // fix components in composite partition key, if necessary
+                List<AbstractType<?>> newComponents = new ArrayList<>(schemaKeyComponents.size());
+                for (int i = 0; i < schemaKeyComponents.size(); i++)
+                {
+                    AbstractType<?> headerKeyComponent = headerKeyComponents.get(i);
+                    AbstractType<?> schemaKeyComponent = schemaKeyComponents.get(i);
+                    AbstractType<?> fixedType = fixType(desc,
+                                                        tableMetadata.partitionKeyColumns().get(i).name.bytes,
+                                                        headerKeyComponent,
+                                                        schemaKeyComponent,
+                                                        false);
+                    if (fixedType == null)
+                        keyMismatch = true;
+                    else
+                        headerKeyComponent = fixedType;
+                    newComponents.add(fixType(desc,
+                                              tableMetadata.partitionKeyColumns().get(i).name.bytes,
+                                              headerKeyComponent,
+                                              schemaKeyComponent,
+                                              false));
+                }
+                headerKeyType = CompositeType.getInstance(newComponents);
+            }
+        }
+        else
+        {
+            // fix non-composite partition key, if necessary
+            AbstractType<?> fixedType = fixType(desc, tableMetadata.partitionKeyColumns().get(0).name.bytes, headerKeyType, schemaKeyType, false);
+            if (fixedType == null)
+                // non-composite partition key doesn't match and cannot be fixed
+                keyMismatch = true;
+            else
+                headerKeyType = fixedType;
+        }
+        if (keyMismatch)
+            error("sstable %s: Mismatch in partition key type between sstable serialization-header and schema (%s vs %s)",
+                  desc,
+                  headerKeyType.asCQL3Type(),
+                  schemaKeyType.asCQL3Type());
+        return headerKeyType;
+    }
+
+    private List<AbstractType<?>> validateClusteringColumns(Descriptor desc, CFMetaData tableMetadata, SerializationHeader.Component header)
+    {
+        List<AbstractType<?>> headerClusteringTypes = header.getClusteringTypes();
+        List<AbstractType<?>> clusteringTypes = new ArrayList<>();
+        boolean clusteringMismatch = false;
+        List<ColumnDefinition> schemaClustering = tableMetadata.clusteringColumns();
+        if (schemaClustering.size() != headerClusteringTypes.size())
+        {
+            clusteringMismatch = true;
+            // Just use the original types. Since the number of clustering columns don't match, there's nothing to
+            // meaningfully validate against.
+            clusteringTypes.addAll(headerClusteringTypes);
+        }
+        else
+        {
+            for (int i = 0; i < headerClusteringTypes.size(); i++)
+            {
+                AbstractType<?> headerType = headerClusteringTypes.get(i);
+                ColumnDefinition column = schemaClustering.get(i);
+                AbstractType<?> schemaType = column.type;
+                AbstractType<?> fixedType = fixType(desc, column.name.bytes, headerType, schemaType, false);
+                if (fixedType == null)
+                    clusteringMismatch = true;
+                else
+                    headerType = fixedType;
+                clusteringTypes.add(headerType);
+            }
+        }
+        if (clusteringMismatch)
+            error("sstable %s: mismatch in clustering columns between sstable serialization-header and schema (%s vs %s)",
+                  desc,
+                  headerClusteringTypes.stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(Collectors.joining(",")),
+                  schemaClustering.stream().map(cd -> cd.type.asCQL3Type().toString()).collect(Collectors.joining(",")));
+        return clusteringTypes;
+    }
+
+    private Map<ByteBuffer, AbstractType<?>> validateColumns(Descriptor desc, CFMetaData tableMetadata, Map<ByteBuffer, AbstractType<?>> columns, ColumnDefinition.Kind kind)
+    {
+        Map<ByteBuffer, AbstractType<?>> target = new LinkedHashMap<>();
+        for (Map.Entry<ByteBuffer, AbstractType<?>> nameAndType : columns.entrySet())
+        {
+            ByteBuffer name = nameAndType.getKey();
+            AbstractType<?> type = nameAndType.getValue();
+
+            AbstractType<?> fixedType = validateColumn(desc, tableMetadata, kind, name, type);
+            if (fixedType == null)
+            {
+                error("sstable %s: contains column '%s' of type '%s', which could not be validated",
+                      desc,
+                      type,
+                      logColumnName(name));
+                // don't use a "null" type instance
+                fixedType = type;
+            }
+
+            target.put(name, fixedType);
+        }
+        return target;
+    }
+
+    private AbstractType<?> validateColumn(Descriptor desc, CFMetaData tableMetadata, ColumnDefinition.Kind kind, ByteBuffer name, AbstractType<?> type)
+    {
+        ColumnDefinition cd = tableMetadata.getColumnDefinition(name);
+        if (cd == null)
+        {
+            // In case the column was dropped, there is not much that we can actually validate.
+            // The column could have been recreated using the same or a different kind or the same or
+            // a different type. Lottery...
+
+            cd = tableMetadata.getDroppedColumnDefinition(name, kind == ColumnDefinition.Kind.STATIC);
+            if (cd == null)
+            {
+                for (IndexMetadata indexMetadata : tableMetadata.getIndexes())
+                {
+                    String target = indexMetadata.options.get(IndexTarget.TARGET_OPTION_NAME);
+                    if (target != null && ByteBufferUtil.bytes(target).equals(name))
+                    {
+                        warn.accept(String.format("sstable %s: contains column '%s', which is not a column in the table '%s.%s', but a target for that table's index '%s'",
+                                                  desc,
+                                                  logColumnName(name),
+                                                  tableMetadata.ksName,
+                                                  tableMetadata.cfName,
+                                                  indexMetadata.name));
+                        return type;
+                    }
+                }
+
+                warn.accept(String.format("sstable %s: contains column '%s', which is not present in the schema",
+                                          desc,
+                                          logColumnName(name)));
+            }
+            else
+            {
+                // This is a best-effort approach to handle the case of a UDT column created *AND* dropped in
+                // C* 3.0.
+                if (type instanceof UserType && cd.type instanceof TupleType)
+                {
+                    // At this point, we know that the type belongs to a dropped column, recorded with the
+                    // dropped column type "TupleType" and using "UserType" in the sstable. So it is very
+                    // likely, that this belongs to a dropped UDT. Fix that information to tuple-type.
+                    return fixType(desc, name, type, cd.type, true);
+                }
+            }
+
+            return type;
+        }
+
+        // At this point, the column name is known to be a "non-dropped" column in the table.
+        if (cd.kind != kind)
+            error("sstable %s: contains column '%s' as a %s column, but is of kind %s in the schema",
+                  desc,
+                  logColumnName(name),
+                  kind.name().toLowerCase(),
+                  cd.kind.name().toLowerCase());
+        else
+            type = fixType(desc, name, type, cd.type, false);
+        return type;
+    }
+
+    private AbstractType<?> fixType(Descriptor desc, ByteBuffer name, AbstractType<?> typeInHeader, AbstractType<?> typeInSchema, boolean droppedColumnMode)
+    {
+        AbstractType<?> fixedType = fixTypeInner(typeInHeader, typeInSchema, droppedColumnMode);
+        if (fixedType != null)
+        {
+            if (fixedType != typeInHeader)
+                info.accept(String.format("sstable %s: Column '%s' needs to be updated from type '%s' to '%s'",
+                                          desc,
+                                          logColumnName(name),
+                                          typeInHeader.asCQL3Type(),
+                                          fixedType.asCQL3Type()));
+            return fixedType;
+        }
+
+        error("sstable %s: contains column '%s' as type '%s', but schema mentions '%s'",
+              desc,
+              logColumnName(name),
+              typeInHeader.asCQL3Type(),
+              typeInSchema.asCQL3Type());
+
+        return typeInHeader;
+    }
+
+    private AbstractType<?> fixTypeInner(AbstractType<?> typeInHeader, AbstractType<?> typeInSchema, boolean droppedColumnMode)
+    {
+        if (typeEquals(typeInHeader, typeInSchema))
+            return typeInHeader;
+
+        if (typeInHeader instanceof CollectionType)
+            return fixTypeInnerCollection(typeInHeader, typeInSchema, droppedColumnMode);
+
+        if (typeInHeader instanceof AbstractCompositeType)
+            return fixTypeInnerAbstractComposite(typeInHeader, typeInSchema, droppedColumnMode);
+
+        if (typeInHeader instanceof TupleType)
+            return fixTypeInnerAbstractTuple(typeInHeader, typeInSchema, droppedColumnMode);
+
+        // all types, beside CollectionType + AbstractCompositeType + TupleType, should be ok (no nested types) - just check for compatibility
+        if (typeInHeader.isCompatibleWith(typeInSchema))
+            return typeInHeader;
+
+        return null;
+    }
+
+    private AbstractType<?> fixTypeInnerAbstractTuple(AbstractType<?> typeInHeader, AbstractType<?> typeInSchema, boolean droppedColumnMode)
+    {
+        // This first 'if' handles the case when a UDT has been dropped, as a dropped UDT is recorded as a tuple
+        // in dropped_columns. If a UDT is to be replaced with a tuple, then also do that for the inner UDTs.
+        if (droppedColumnMode && typeInHeader.getClass() == UserType.class && typeInSchema instanceof TupleType)
+            return fixTypeInnerUserTypeDropped((UserType) typeInHeader, (TupleType) typeInSchema);
+
+        if (typeInHeader.getClass() != typeInSchema.getClass())
+            return null;
+
+        if (typeInHeader.getClass() == UserType.class)
+            return fixTypeInnerUserType((UserType) typeInHeader, (UserType) typeInSchema);
+
+        if (typeInHeader.getClass() == TupleType.class)
+            return fixTypeInnerTuple((TupleType) typeInHeader, (TupleType) typeInSchema, droppedColumnMode);
+
+        throw new IllegalArgumentException("Unknown tuple type class " + typeInHeader.getClass().getName());
+    }
+
+    private AbstractType<?> fixTypeInnerCollection(AbstractType<?> typeInHeader, AbstractType<?> typeInSchema, boolean droppedColumnMode)
+    {
+        if (typeInHeader.getClass() != typeInSchema.getClass())
+            return null;
+
+        if (typeInHeader.getClass() == ListType.class)
+            return fixTypeInnerList((ListType<?>) typeInHeader, (ListType<?>) typeInSchema, droppedColumnMode);
+
+        if (typeInHeader.getClass() == SetType.class)
+            return fixTypeInnerSet((SetType<?>) typeInHeader, (SetType<?>) typeInSchema, droppedColumnMode);
+
+        if (typeInHeader.getClass() == MapType.class)
+            return fixTypeInnerMap((MapType<?, ?>) typeInHeader, (MapType<?, ?>) typeInSchema, droppedColumnMode);
+
+        throw new IllegalArgumentException("Unknown collection type class " + typeInHeader.getClass().getName());
+    }
+
+    private AbstractType<?> fixTypeInnerAbstractComposite(AbstractType<?> typeInHeader, AbstractType<?> typeInSchema, boolean droppedColumnMode)
+    {
+        if (typeInHeader.getClass() != typeInSchema.getClass())
+            return null;
+
+        if (typeInHeader.getClass() == CompositeType.class)
+            return fixTypeInnerComposite((CompositeType) typeInHeader, (CompositeType) typeInSchema, droppedColumnMode);
+
+        if (typeInHeader.getClass() == DynamicCompositeType.class)
+        {
+            // Not sure if we should care about UDTs in DynamicCompositeType at all...
+            if (!typeInHeader.isCompatibleWith(typeInSchema))
+                return null;
+
+            return typeInHeader;
+        }
+
+        throw new IllegalArgumentException("Unknown composite type class " + typeInHeader.getClass().getName());
+    }
+
+    private AbstractType<?> fixTypeInnerUserType(UserType cHeader, UserType cSchema)
+    {
+        if (!cHeader.keyspace.equals(cSchema.keyspace) || !cHeader.name.equals(cSchema.name))
+            // different UDT - bummer...
+            return null;
+
+        if (cHeader.isMultiCell() != cSchema.isMultiCell())
+        {
+            if (cHeader.isMultiCell() && !cSchema.isMultiCell())
+            {
+                // C* 3.0 writes broken SerializationHeader.Component instances - i.e. broken UDT type
+                // definitions into the sstable -Stats.db file, because 3.0 does not enclose frozen UDTs
+                // (and all UDTs in 3.0 were frozen) with an '' bracket. Since CASSANDRA-7423 (support
+                // for non-frozen UDTs, committed to C* 3.6), that frozen-bracket is quite important.
+                // Non-frozen (= multi-cell) UDTs are serialized in a fundamentally different way than
+                // frozen UDTs in sstables - most importantly, the order of serialized columns depends on
+                // the type: fixed-width types first, then variable length types (like frozen types),
+                // multi-cell types last. If C* >= 3.6 reads an sstable with a UDT that's written by
+                // C* < 3.6, a variety of CorruptSSTableExceptions get logged and clients will encounter
+                // read errors.
+                // At this point, we know that the type belongs to a "live" (non-dropped) column, so it
+                // is safe to correct the information from the header.
+                return cSchema;
+            }
+
+            // In all other cases, there's not much we can do.
+            return null;
+        }
+
+        return cHeader;
+    }
+
+    private AbstractType<?> fixTypeInnerUserTypeDropped(UserType cHeader, TupleType cSchema)
+    {
+        // Do not mess around with the UserType in the serialization header, if the column has been dropped.
+        // Only fix the multi-cell status when the header contains it as a multicell (non-frozen) UserType,
+        // but the schema says "frozen".
+        if (cHeader.isMultiCell() && !cSchema.isMultiCell())
+        {
+            return new UserType(cHeader.keyspace, cHeader.name, cHeader.fieldNames(), cHeader.fieldTypes(), cSchema.isMultiCell());
+        }
+
+        return cHeader;
+    }
+
+    private AbstractType<?> fixTypeInnerTuple(TupleType cHeader, TupleType cSchema, boolean droppedColumnMode)
+    {
+        if (cHeader.size() != cSchema.size())
+            // different number of components - bummer...
+            return null;
+        List<AbstractType<?>> cHeaderFixed = new ArrayList<>(cHeader.size());
+        boolean anyChanged = false;
+        for (int i = 0; i < cHeader.size(); i++)
+        {
+            AbstractType<?> cHeaderComp = cHeader.type(i);
+            AbstractType<?> cHeaderCompFixed = fixTypeInner(cHeaderComp, cSchema.type(i), droppedColumnMode);
+            if (cHeaderCompFixed == null)
+                // incompatible, bummer...
+                return null;
+            cHeaderFixed.add(cHeaderCompFixed);
+            anyChanged |= cHeaderComp != cHeaderCompFixed;
+        }
+        if (anyChanged || cSchema.isMultiCell() != cHeader.isMultiCell())
+            // TODO this should create a non-frozen tuple type for the sake of handling a dropped, non-frozen UDT
+            return new TupleType(cHeaderFixed);
+        return cHeader;
+    }
+
+    private AbstractType<?> fixTypeInnerComposite(CompositeType cHeader, CompositeType cSchema, boolean droppedColumnMode)
+    {
+        if (cHeader.types.size() != cSchema.types.size())
+            // different number of components - bummer...
+            return null;
+        List<AbstractType<?>> cHeaderFixed = new ArrayList<>(cHeader.types.size());
+        boolean anyChanged = false;
+        for (int i = 0; i < cHeader.types.size(); i++)
+        {
+            AbstractType<?> cHeaderComp = cHeader.types.get(i);
+            AbstractType<?> cHeaderCompFixed = fixTypeInner(cHeaderComp, cSchema.types.get(i), droppedColumnMode);
+            if (cHeaderCompFixed == null)
+                // incompatible, bummer...
+                return null;
+            cHeaderFixed.add(cHeaderCompFixed);
+            anyChanged |= cHeaderComp != cHeaderCompFixed;
+        }
+        if (anyChanged)
+            return CompositeType.getInstance(cHeaderFixed);
+        return cHeader;
+    }
+
+    private AbstractType<?> fixTypeInnerList(ListType<?> cHeader, ListType<?> cSchema, boolean droppedColumnMode)
+    {
+        AbstractType<?> cHeaderElem = cHeader.getElementsType();
+        AbstractType<?> cHeaderElemFixed = fixTypeInner(cHeaderElem, cSchema.getElementsType(), droppedColumnMode);
+        if (cHeaderElemFixed == null)
+            // bummer...
+            return null;
+        if (cHeaderElem != cHeaderElemFixed)
+            // element type changed
+            return ListType.getInstance(cHeaderElemFixed, cHeader.isMultiCell());
+        return cHeader;
+    }
+
+    private AbstractType<?> fixTypeInnerSet(SetType<?> cHeader, SetType<?> cSchema, boolean droppedColumnMode)
+    {
+        AbstractType<?> cHeaderElem = cHeader.getElementsType();
+        AbstractType<?> cHeaderElemFixed = fixTypeInner(cHeaderElem, cSchema.getElementsType(), droppedColumnMode);
+        if (cHeaderElemFixed == null)
+            // bummer...
+            return null;
+        if (cHeaderElem != cHeaderElemFixed)
+            // element type changed
+            return SetType.getInstance(cHeaderElemFixed, cHeader.isMultiCell());
+        return cHeader;
+    }
+
+    private AbstractType<?> fixTypeInnerMap(MapType<?, ?> cHeader, MapType<?, ?> cSchema, boolean droppedColumnMode)
+    {
+        AbstractType<?> cHeaderKey = cHeader.getKeysType();
+        AbstractType<?> cHeaderVal = cHeader.getValuesType();
+        AbstractType<?> cHeaderKeyFixed = fixTypeInner(cHeaderKey, cSchema.getKeysType(), droppedColumnMode);
+        AbstractType<?> cHeaderValFixed = fixTypeInner(cHeaderVal, cSchema.getValuesType(), droppedColumnMode);
+        if (cHeaderKeyFixed == null || cHeaderValFixed == null)
+            // bummer...
+            return null;
+        if (cHeaderKey != cHeaderKeyFixed || cHeaderVal != cHeaderValFixed)
+            // element type changed
+            return MapType.getInstance(cHeaderKeyFixed, cHeaderValFixed, cHeader.isMultiCell());
+        return cHeader;
+    }
+
+    private boolean typeEquals(AbstractType<?> typeInHeader, AbstractType<?> typeInSchema)
+    {
+        // Quite annoying, but the implementations of equals() on some implementation of AbstractType seems to be
+        // wrong, but toString() seems to work in such cases.
+        return typeInHeader.equals(typeInSchema) || typeInHeader.toString().equals(typeInSchema.toString());
+    }
+
+    private static String logColumnName(ByteBuffer columnName)
+    {
+        try
+        {
+            return ByteBufferUtil.string(columnName);
+        }
+        catch (CharacterCodingException e)
+        {
+            return "?? " + e;
+        }
+    }
+
+    private Map<MetadataType, MetadataComponent> readSSTableMetadata(Descriptor desc)
+    {
+        Map<MetadataType, MetadataComponent> metadata;
+        try
+        {
+            metadata = desc.getMetadataSerializer().deserialize(desc, EnumSet.allOf(MetadataType.class));
+        }
+        catch (IOException e)
+        {
+            error("Failed to deserialize metadata for sstable %s: %s", desc, e.toString());
+            return null;
+        }
+        return metadata;
+    }
+
+    private void writeNewMetadata(Descriptor desc, Map<MetadataType, MetadataComponent> newMetadata)
+    {
+        String file = desc.filenameFor(Component.STATS);
+        info.accept(String.format("  Writing new metadata file %s", file));
+        try
+        {
+            desc.getMetadataSerializer().rewriteSSTableMetadata(desc, newMetadata);
+        }
+        catch (IOException e)
+        {
+            error("Failed to write metadata component for %s: %s", file, e.toString());
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Fix individually provided sstables or directories containing sstables.
+     */
+    static class ManualHeaderFix extends SSTableHeaderFix
+    {
+        private final List<Path> paths;
+
+        ManualHeaderFix(Builder builder)
+        {
+            super(builder);
+            this.paths = builder.paths;
+        }
+
+        public void prepare()
+        {
+            paths.forEach(this::processFileOrDirectory);
+        }
+    }
+
+    /**
+     * Fix all sstables in the configured data-directories.
+     */
+    static class AutomaticHeaderFix extends SSTableHeaderFix
+    {
+        AutomaticHeaderFix(Builder builder)
+        {
+            super(builder);
+        }
+
+        public void prepare()
+        {
+            info.accept("Scanning all data directories...");
+            for (Directories.DataDirectory dataDirectory : Directories.dataDirectories)
+                scanDataDirectory(dataDirectory);
+            info.accept("Finished scanning all data directories...");
+        }
+
+        private void scanDataDirectory(Directories.DataDirectory dataDirectory)
+        {
+            info.accept(String.format("Scanning data directory %s", dataDirectory.location));
+            File[] ksDirs = dataDirectory.location.listFiles();
+            if (ksDirs == null)
+                return;
+            for (File ksDir : ksDirs)
+            {
+                if (!ksDir.isDirectory() || !ksDir.canRead())
+                    continue;
+
+                String name = ksDir.getName();
+
+                // silently ignore all system keyspaces
+                if (SchemaConstants.isLocalSystemKeyspace(name) || SchemaConstants.isReplicatedSystemKeyspace(name))
+                    continue;
+
+                File[] tabDirs = ksDir.listFiles();
+                if (tabDirs == null)
+                    continue;
+                for (File tabDir : tabDirs)
+                {
+                    if (!tabDir.isDirectory() || !tabDir.canRead())
+                        continue;
+
+                    processFileOrDirectory(tabDir.toPath());
+                }
+            }
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableIdentityIterator.java b/src/java/org/apache/cassandra/io/sstable/SSTableIdentityIterator.java
index a5af334..2a79f88 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTableIdentityIterator.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTableIdentityIterator.java
@@ -19,15 +19,15 @@
 
 import java.io.*;
 
-import org.apache.cassandra.utils.AbstractIterator;
-
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.util.FileDataInput;
 import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ByteBufferUtil;
 
-public class SSTableIdentityIterator extends AbstractIterator<Unfiltered> implements Comparable<SSTableIdentityIterator>, UnfilteredRowIterator
+public class SSTableIdentityIterator implements Comparable<SSTableIdentityIterator>, UnfilteredRowIterator
 {
     private final SSTableReader sstable;
     private final DecoratedKey key;
@@ -37,29 +37,51 @@
     protected final SSTableSimpleIterator iterator;
     private final Row staticRow;
 
-    /**
-     * Used to iterate through the columns of a row.
-     * @param sstable SSTable we are reading ffrom.
-     * @param file Reading using this file.
-     * @param key Key of this row.
-     */
-    public SSTableIdentityIterator(SSTableReader sstable, RandomAccessReader file, DecoratedKey key)
+    public SSTableIdentityIterator(SSTableReader sstable, DecoratedKey key, DeletionTime partitionLevelDeletion,
+            String filename, SSTableSimpleIterator iterator) throws IOException
     {
+        super();
         this.sstable = sstable;
-        this.filename = file.getPath();
         this.key = key;
+        this.partitionLevelDeletion = partitionLevelDeletion;
+        this.filename = filename;
+        this.iterator = iterator;
+        this.staticRow = iterator.readStaticRow();
+    }
 
+    public static SSTableIdentityIterator create(SSTableReader sstable, RandomAccessReader file, DecoratedKey key)
+    {
         try
         {
-            this.partitionLevelDeletion = DeletionTime.serializer.deserialize(file);
+            DeletionTime partitionLevelDeletion = DeletionTime.serializer.deserialize(file);
             SerializationHelper helper = new SerializationHelper(sstable.metadata, sstable.descriptor.version.correspondingMessagingVersion(), SerializationHelper.Flag.LOCAL);
-            this.iterator = SSTableSimpleIterator.create(sstable.metadata, file, sstable.header, helper, partitionLevelDeletion);
-            this.staticRow = iterator.readStaticRow();
+            SSTableSimpleIterator iterator = SSTableSimpleIterator.create(sstable.metadata, file, sstable.header, helper, partitionLevelDeletion);
+            return new SSTableIdentityIterator(sstable, key, partitionLevelDeletion, file.getPath(), iterator);
         }
         catch (IOException e)
         {
             sstable.markSuspect();
-            throw new CorruptSSTableException(e, filename);
+            throw new CorruptSSTableException(e, file.getPath());
+        }
+    }
+
+    public static SSTableIdentityIterator create(SSTableReader sstable, FileDataInput dfile, RowIndexEntry<?> indexEntry, DecoratedKey key, boolean tombstoneOnly)
+    {
+        try
+        {
+            dfile.seek(indexEntry.position);
+            ByteBufferUtil.skipShortLength(dfile); // Skip partition key
+            DeletionTime partitionLevelDeletion = DeletionTime.serializer.deserialize(dfile);
+            SerializationHelper helper = new SerializationHelper(sstable.metadata, sstable.descriptor.version.correspondingMessagingVersion(), SerializationHelper.Flag.LOCAL);
+            SSTableSimpleIterator iterator = tombstoneOnly
+                    ? SSTableSimpleIterator.createTombstoneOnly(sstable.metadata, dfile, sstable.header, helper, partitionLevelDeletion)
+                    : SSTableSimpleIterator.create(sstable.metadata, dfile, sstable.header, helper, partitionLevelDeletion);
+            return new SSTableIdentityIterator(sstable, key, partitionLevelDeletion, dfile.getPath(), iterator);
+        }
+        catch (IOException e)
+        {
+            sstable.markSuspect();
+            throw new CorruptSSTableException(e, dfile.getPath());
         }
     }
 
@@ -93,7 +115,32 @@
         return staticRow;
     }
 
-    protected Unfiltered computeNext()
+    public boolean hasNext()
+    {
+        try
+        {
+            return iterator.hasNext();
+        }
+        catch (IndexOutOfBoundsException e)
+        {
+            sstable.markSuspect();
+            throw new CorruptSSTableException(e, filename);
+        }
+        catch (IOError e)
+        {
+            if (e.getCause() instanceof IOException)
+            {
+                sstable.markSuspect();
+                throw new CorruptSSTableException((Exception)e.getCause(), filename);
+            }
+            else
+            {
+                throw e;
+            }
+        }
+    }
+
+    public Unfiltered next()
     {
         try
         {
@@ -120,7 +167,7 @@
 
     protected Unfiltered doCompute()
     {
-        return iterator.hasNext() ? iterator.next() : endOfData();
+        return iterator.next();
     }
 
     public void close()
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableLoader.java b/src/java/org/apache/cassandra/io/sstable/SSTableLoader.java
index e597e54..334e0e0 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTableLoader.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTableLoader.java
@@ -159,7 +159,7 @@
         client.init(keyspace);
         outputHandler.output("Established connection to initial hosts");
 
-        StreamPlan plan = new StreamPlan("Bulk Load", 0, connectionsPerHost, false, false).connectionFactory(client.getConnectionFactory());
+        StreamPlan plan = new StreamPlan("Bulk Load", 0, connectionsPerHost, false, false, false).connectionFactory(client.getConnectionFactory());
 
         Map<InetAddress, Collection<Range<Token>>> endpointToRanges = client.getEndpointToRangesMap();
         openSSTables(endpointToRanges);
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableMultiWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableMultiWriter.java
index 0bb3721..b92bc78 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTableMultiWriter.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTableMultiWriter.java
@@ -19,7 +19,6 @@
 package org.apache.cassandra.io.sstable;
 
 import java.util.Collection;
-import java.util.List;
 import java.util.UUID;
 
 import org.apache.cassandra.db.rows.UnfilteredRowIterator;
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableRewriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableRewriter.java
index b2fbcb1..a71d1af 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTableRewriter.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTableRewriter.java
@@ -66,7 +66,6 @@
     private long currentlyOpenedEarlyAt; // the position (in MB) in the target file we last (re)opened at
 
     private final List<SSTableWriter> writers = new ArrayList<>();
-    private final boolean isOffline; // true for operations that are performed without Cassandra running (prevents updates of Tracker)
     private final boolean keepOriginals; // true if we do not want to obsolete the originals
 
     private SSTableWriter writer;
@@ -75,34 +74,45 @@
     // for testing (TODO: remove when have byteman setup)
     private boolean throwEarly, throwLate;
 
+    @Deprecated
     public SSTableRewriter(ILifecycleTransaction transaction, long maxAge, boolean isOffline)
     {
         this(transaction, maxAge, isOffline, true);
     }
-
+    @Deprecated
     public SSTableRewriter(ILifecycleTransaction transaction, long maxAge, boolean isOffline, boolean shouldOpenEarly)
     {
-        this(transaction, maxAge, isOffline, calculateOpenInterval(shouldOpenEarly), false);
+        this(transaction, maxAge, calculateOpenInterval(shouldOpenEarly), false);
     }
 
     @VisibleForTesting
-    public SSTableRewriter(ILifecycleTransaction transaction, long maxAge, boolean isOffline, long preemptiveOpenInterval, boolean keepOriginals)
+    public SSTableRewriter(ILifecycleTransaction transaction, long maxAge, long preemptiveOpenInterval, boolean keepOriginals)
     {
         this.transaction = transaction;
         this.maxAge = maxAge;
-        this.isOffline = isOffline;
         this.keepOriginals = keepOriginals;
         this.preemptiveOpenInterval = preemptiveOpenInterval;
     }
 
+    @Deprecated
     public static SSTableRewriter constructKeepingOriginals(ILifecycleTransaction transaction, boolean keepOriginals, long maxAge, boolean isOffline)
     {
-        return new SSTableRewriter(transaction, maxAge, isOffline, calculateOpenInterval(true), keepOriginals);
+        return constructKeepingOriginals(transaction, keepOriginals, maxAge);
     }
 
-    public static SSTableRewriter construct(ColumnFamilyStore cfs, ILifecycleTransaction transaction, boolean keepOriginals, long maxAge, boolean isOffline)
+    public static SSTableRewriter constructKeepingOriginals(ILifecycleTransaction transaction, boolean keepOriginals, long maxAge)
     {
-        return new SSTableRewriter(transaction, maxAge, isOffline, calculateOpenInterval(cfs.supportsEarlyOpen()), keepOriginals);
+        return new SSTableRewriter(transaction, maxAge, calculateOpenInterval(true), keepOriginals);
+    }
+
+    public static SSTableRewriter constructWithoutEarlyOpening(ILifecycleTransaction transaction, boolean keepOriginals, long maxAge)
+    {
+        return new SSTableRewriter(transaction, maxAge, calculateOpenInterval(false), keepOriginals);
+    }
+
+    public static SSTableRewriter construct(ColumnFamilyStore cfs, ILifecycleTransaction transaction, boolean keepOriginals, long maxAge)
+    {
+        return new SSTableRewriter(transaction, maxAge, calculateOpenInterval(cfs.supportsEarlyOpen()), keepOriginals);
     }
 
     private static long calculateOpenInterval(boolean shouldOpenEarly)
@@ -124,19 +134,16 @@
         DecoratedKey key = partition.partitionKey();
         maybeReopenEarly(key);
         RowIndexEntry index = writer.append(partition);
-        if (!isOffline && index != null)
+        if (!transaction.isOffline() && index != null)
         {
-            boolean save = false;
             for (SSTableReader reader : transaction.originals())
             {
                 if (reader.getCachedPosition(key, false) != null)
                 {
-                    save = true;
+                    cachedKeys.put(key, index);
                     break;
                 }
             }
-            if (save)
-                cachedKeys.put(key, index);
         }
         return index;
     }
@@ -160,7 +167,7 @@
     {
         if (writer.getFilePointer() - currentlyOpenedEarlyAt > preemptiveOpenInterval)
         {
-            if (isOffline)
+            if (transaction.isOffline())
             {
                 for (SSTableReader reader : transaction.originals())
                 {
@@ -216,18 +223,24 @@
      */
     private void moveStarts(SSTableReader newReader, DecoratedKey lowerbound)
     {
-        if (isOffline)
+        if (transaction.isOffline())
             return;
         if (preemptiveOpenInterval == Long.MAX_VALUE)
             return;
 
-        final List<DecoratedKey> invalidateKeys = new ArrayList<>();
-        invalidateKeys.addAll(cachedKeys.keySet());
         newReader.setupOnline();
-        for (Map.Entry<DecoratedKey, RowIndexEntry> cacheKey : cachedKeys.entrySet())
-            newReader.cacheKey(cacheKey.getKey(), cacheKey.getValue());
+        List<DecoratedKey> invalidateKeys = null;
+        if (!cachedKeys.isEmpty())
+        {
+            invalidateKeys = new ArrayList<>(cachedKeys.size());
+            for (Map.Entry<DecoratedKey, RowIndexEntry> cacheKey : cachedKeys.entrySet())
+            {
+                invalidateKeys.add(cacheKey.getKey());
+                newReader.cacheKey(cacheKey.getKey(), cacheKey.getValue());
+            }
+        }
 
-        cachedKeys = new HashMap<>();
+        cachedKeys.clear();
         for (SSTableReader sstable : transaction.originals())
         {
             // we call getCurrentReplacement() to support multiple rewriters operating over the same source readers at once.
@@ -238,12 +251,15 @@
             if (latest.first.compareTo(lowerbound) > 0)
                 continue;
 
-            Runnable runOnClose = new InvalidateKeys(latest, invalidateKeys);
+            Runnable runOnClose = invalidateKeys != null ? new InvalidateKeys(latest, invalidateKeys) : null;
             if (lowerbound.compareTo(latest.last) >= 0)
             {
                 if (!transaction.isObsolete(latest))
                 {
-                    latest.runOnClose(runOnClose);
+                    if (runOnClose != null)
+                    {
+                        latest.runOnClose(runOnClose);
+                    }
                     transaction.obsolete(latest);
                 }
                 continue;
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleIterator.java b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleIterator.java
index f82db4e..ce42126 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleIterator.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleIterator.java
@@ -29,8 +29,8 @@
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.io.util.DataInputPlus;
-import org.apache.cassandra.io.util.FileDataInput;
 import org.apache.cassandra.io.util.DataPosition;
+import org.apache.cassandra.io.util.FileDataInput;
 import org.apache.cassandra.net.MessagingService;
 
 /**
@@ -60,6 +60,14 @@
             return new CurrentFormatIterator(metadata, in, header, helper);
     }
 
+    public static SSTableSimpleIterator createTombstoneOnly(CFMetaData metadata, DataInputPlus in, SerializationHeader header, SerializationHelper helper, DeletionTime partitionDeletion)
+    {
+        if (helper.version < MessagingService.VERSION_30)
+            return new OldFormatTombstoneIterator(metadata, in, helper, partitionDeletion);
+        else
+            return new CurrentFormatTombstoneIterator(metadata, in, header, helper);
+    }
+
     public abstract Row readStaticRow() throws IOException;
 
     private static class CurrentFormatIterator extends SSTableSimpleIterator
@@ -94,6 +102,41 @@
         }
     }
 
+    private static class CurrentFormatTombstoneIterator extends SSTableSimpleIterator
+    {
+        private final SerializationHeader header;
+
+        private CurrentFormatTombstoneIterator(CFMetaData metadata, DataInputPlus in, SerializationHeader header, SerializationHelper helper)
+        {
+            super(metadata, in, helper);
+            this.header = header;
+        }
+
+        public Row readStaticRow() throws IOException
+        {
+            if (header.hasStatic())
+            {
+                Row staticRow = UnfilteredSerializer.serializer.deserializeStaticRow(in, header, helper);
+                if (!staticRow.deletion().isLive())
+                    return BTreeRow.emptyDeletedRow(staticRow.clustering(), staticRow.deletion());
+            }
+            return Rows.EMPTY_STATIC_ROW;
+        }
+
+        protected Unfiltered computeNext()
+        {
+            try
+            {
+                Unfiltered unfiltered = UnfilteredSerializer.serializer.deserializeTombstonesOnly((FileDataInput) in, header, helper);
+                return unfiltered == null ? endOfData() : unfiltered;
+            }
+            catch (IOException e)
+            {
+                throw new IOError(e);
+            }
+        }
+    }
+
     private static class OldFormatIterator extends SSTableSimpleIterator
     {
         private final UnfilteredDeserializer deserializer;
@@ -138,27 +181,61 @@
 
         protected Unfiltered computeNext()
         {
-            try
+            while (true)
             {
-                if (!deserializer.hasNext())
-                    return endOfData();
-
-                Unfiltered unfiltered = deserializer.readNext();
-                if (metadata.isStaticCompactTable() && unfiltered.kind() == Unfiltered.Kind.ROW)
+                try
                 {
-                    Row row = (Row) unfiltered;
-                    ColumnDefinition def = metadata.getColumnDefinition(LegacyLayout.encodeClustering(metadata, row.clustering()));
-                    if (def != null && def.isStatic())
-                        return computeNext();
+                    if (!deserializer.hasNext())
+                        return endOfData();
+
+                    Unfiltered unfiltered = deserializer.readNext();
+                    if (metadata.isStaticCompactTable() && unfiltered.kind() == Unfiltered.Kind.ROW)
+                    {
+                        Row row = (Row) unfiltered;
+                        ColumnDefinition def = metadata.getColumnDefinition(LegacyLayout.encodeClustering(metadata, row.clustering()));
+                        if (def != null && def.isStatic())
+                            continue;
+                    }
+                    return unfiltered;
                 }
-                return unfiltered;
-            }
-            catch (IOException e)
-            {
-                throw new IOError(e);
+                catch (IOException e)
+                {
+                    throw new IOError(e);
+                }
             }
         }
 
     }
 
+    private static class OldFormatTombstoneIterator extends OldFormatIterator
+    {
+        private OldFormatTombstoneIterator(CFMetaData metadata, DataInputPlus in, SerializationHelper helper, DeletionTime partitionDeletion)
+        {
+            super(metadata, in, helper, partitionDeletion);
+        }
+
+        public Row readStaticRow() throws IOException
+        {
+            Row row = super.readStaticRow();
+            if (!row.deletion().isLive())
+                return BTreeRow.emptyDeletedRow(row.clustering(), row.deletion());
+            return Rows.EMPTY_STATIC_ROW;
+        }
+
+        protected Unfiltered computeNext()
+        {
+            while (true)
+            {
+                Unfiltered unfiltered = super.computeNext();
+                if (unfiltered == null || unfiltered.isRangeTombstoneMarker())
+                    return unfiltered;
+
+                Row row = (Row) unfiltered;
+                if (!row.deletion().isLive())
+                    return BTreeRow.emptyDeletedRow(row.clustering(), row.deletion());
+                // Otherwise read next.
+            }
+        }
+
+    }
 }
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleUnsortedWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleUnsortedWriter.java
index 6d3a714..23e18b5 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleUnsortedWriter.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleUnsortedWriter.java
@@ -27,6 +27,7 @@
 
 import com.google.common.base.Throwables;
 
+import io.netty.util.concurrent.FastThreadLocalThread;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.Row;
@@ -189,7 +190,7 @@
     //// typedef
     static class Buffer extends TreeMap<DecoratedKey, PartitionUpdate> {}
 
-    private class DiskWriter extends Thread
+    private class DiskWriter extends FastThreadLocalThread
     {
         volatile Throwable exception = null;
 
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleWriter.java
index 45722cd..7fbd79d 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleWriter.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleWriter.java
@@ -25,7 +25,6 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
-import org.apache.cassandra.dht.IPartitioner;
 
 /**
  * A SSTable writer that assumes rows are in (partitioner) sorted order.
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableTxnWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableTxnWriter.java
index e889d85..015c5bb 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTableTxnWriter.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTableTxnWriter.java
@@ -18,6 +18,7 @@
 
 package org.apache.cassandra.io.sstable;
 
+import java.io.IOException;
 import java.util.Collection;
 
 import org.apache.cassandra.config.CFMetaData;
@@ -27,6 +28,9 @@
 import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
 import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.io.sstable.format.RangeAwareSSTableWriter;
+import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
 import org.apache.cassandra.utils.concurrent.Transactional;
@@ -93,7 +97,7 @@
         return writer.finished();
     }
 
-    @SuppressWarnings("resource") // log and writer closed during postCleanup
+    @SuppressWarnings("resource") // log and writer closed during doPostCleanup
     public static SSTableTxnWriter create(ColumnFamilyStore cfs, Descriptor descriptor, long keyCount, long repairedAt, int sstableLevel, SerializationHeader header)
     {
         LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.WRITE);
@@ -101,13 +105,46 @@
         return new SSTableTxnWriter(txn, writer);
     }
 
-    @SuppressWarnings("resource") // log and writer closed during postCleanup
-    public static SSTableTxnWriter create(CFMetaData cfm, Descriptor descriptor, long keyCount, long repairedAt, int sstableLevel, SerializationHeader header)
+
+    @SuppressWarnings("resource") // log and writer closed during doPostCleanup
+    public static SSTableTxnWriter createRangeAware(CFMetaData cfm,
+                                                    long keyCount,
+                                                    long repairedAt,
+                                                    SSTableFormat.Type type,
+                                                    int sstableLevel,
+                                                    SerializationHeader header)
+    {
+
+        ColumnFamilyStore cfs = Keyspace.open(cfm.ksName).getColumnFamilyStore(cfm.cfName);
+        LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.WRITE);
+        SSTableMultiWriter writer;
+        try
+        {
+            writer = new RangeAwareSSTableWriter(cfs, keyCount, repairedAt, type, sstableLevel, 0, txn, header);
+        }
+        catch (IOException e)
+        {
+            //We don't know the total size so this should never happen
+            //as we send in 0
+            throw new RuntimeException(e);
+        }
+
+        return new SSTableTxnWriter(txn, writer);
+    }
+
+    @SuppressWarnings("resource") // log and writer closed during doPostCleanup
+    public static SSTableTxnWriter create(CFMetaData cfm,
+                                          Descriptor descriptor,
+                                          long keyCount,
+                                          long repairedAt,
+                                          int sstableLevel,
+                                          SerializationHeader header,
+                                          Collection<Index> indexes)
     {
         // if the column family store does not exist, we create a new default SSTableMultiWriter to use:
         LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.WRITE);
         MetadataCollector collector = new MetadataCollector(cfm.comparator).sstableLevel(sstableLevel);
-        SSTableMultiWriter writer = SimpleSSTableMultiWriter.create(descriptor, keyCount, repairedAt, cfm, collector, header, txn);
+        SSTableMultiWriter writer = SimpleSSTableMultiWriter.create(descriptor, keyCount, repairedAt, cfm, collector, header, indexes, txn);
         return new SSTableTxnWriter(txn, writer);
     }
 
diff --git a/src/java/org/apache/cassandra/io/sstable/SimpleSSTableMultiWriter.java b/src/java/org/apache/cassandra/io/sstable/SimpleSSTableMultiWriter.java
index ded070e..76e4dbb 100644
--- a/src/java/org/apache/cassandra/io/sstable/SimpleSSTableMultiWriter.java
+++ b/src/java/org/apache/cassandra/io/sstable/SimpleSSTableMultiWriter.java
@@ -27,6 +27,7 @@
 import org.apache.cassandra.db.SerializationHeader;
 import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
 import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.index.Index;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.sstable.format.SSTableWriter;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
@@ -34,9 +35,11 @@
 public class SimpleSSTableMultiWriter implements SSTableMultiWriter
 {
     private final SSTableWriter writer;
+    private final LifecycleNewTracker lifecycleNewTracker;
 
-    protected SimpleSSTableMultiWriter(SSTableWriter writer)
+    protected SimpleSSTableMultiWriter(SSTableWriter writer, LifecycleNewTracker lifecycleNewTracker)
     {
+        this.lifecycleNewTracker = lifecycleNewTracker;
         this.writer = writer;
     }
 
@@ -89,6 +92,7 @@
 
     public Throwable abort(Throwable accumulate)
     {
+        lifecycleNewTracker.untrackNew(writer);
         return writer.abort(accumulate);
     }
 
@@ -109,9 +113,10 @@
                                             CFMetaData cfm,
                                             MetadataCollector metadataCollector,
                                             SerializationHeader header,
+                                            Collection<Index> indexes,
                                             LifecycleNewTracker lifecycleNewTracker)
     {
-        SSTableWriter writer = SSTableWriter.create(descriptor, keyCount, repairedAt, cfm, metadataCollector, header, lifecycleNewTracker);
-        return new SimpleSSTableMultiWriter(writer);
+        SSTableWriter writer = SSTableWriter.create(descriptor, keyCount, repairedAt, cfm, metadataCollector, header, indexes, lifecycleNewTracker);
+        return new SimpleSSTableMultiWriter(writer, lifecycleNewTracker);
     }
 }
diff --git a/src/java/org/apache/cassandra/io/sstable/format/RangeAwareSSTableWriter.java b/src/java/org/apache/cassandra/io/sstable/format/RangeAwareSSTableWriter.java
new file mode 100644
index 0000000..3358225
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/sstable/format/RangeAwareSSTableWriter.java
@@ -0,0 +1,208 @@
+/*
+ * 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.io.sstable.format;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.db.DiskBoundaries;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.SerializationHeader;
+import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.SSTableMultiWriter;
+import org.apache.cassandra.utils.FBUtilities;
+
+public class RangeAwareSSTableWriter implements SSTableMultiWriter
+{
+    private final List<PartitionPosition> boundaries;
+    private final List<Directories.DataDirectory> directories;
+    private final int sstableLevel;
+    private final long estimatedKeys;
+    private final long repairedAt;
+    private final SSTableFormat.Type format;
+    private final SerializationHeader header;
+    private final LifecycleNewTracker lifecycleNewTracker;
+    private int currentIndex = -1;
+    public final ColumnFamilyStore cfs;
+    private final List<SSTableMultiWriter> finishedWriters = new ArrayList<>();
+    private final List<SSTableReader> finishedReaders = new ArrayList<>();
+    private SSTableMultiWriter currentWriter = null;
+
+    public RangeAwareSSTableWriter(ColumnFamilyStore cfs, long estimatedKeys, long repairedAt, SSTableFormat.Type format, int sstableLevel, long totalSize, LifecycleNewTracker lifecycleNewTracker, SerializationHeader header) throws IOException
+    {
+        DiskBoundaries db = cfs.getDiskBoundaries();
+        directories = db.directories;
+        this.sstableLevel = sstableLevel;
+        this.cfs = cfs;
+        this.estimatedKeys = estimatedKeys / directories.size();
+        this.repairedAt = repairedAt;
+        this.format = format;
+        this.lifecycleNewTracker = lifecycleNewTracker;
+        this.header = header;
+        boundaries = db.positions;
+        if (boundaries == null)
+        {
+            Directories.DataDirectory localDir = cfs.getDirectories().getWriteableLocation(totalSize);
+            if (localDir == null)
+                throw new IOException(String.format("Insufficient disk space to store %s",
+                                                    FBUtilities.prettyPrintMemory(totalSize)));
+            Descriptor desc = Descriptor.fromFilename(cfs.getSSTablePath(cfs.getDirectories().getLocationForDisk(localDir), format));
+            currentWriter = cfs.createSSTableMultiWriter(desc, estimatedKeys, repairedAt, sstableLevel, header, lifecycleNewTracker);
+        }
+    }
+
+    private void maybeSwitchWriter(DecoratedKey key)
+    {
+        if (boundaries == null)
+            return;
+
+        boolean switched = false;
+        while (currentIndex < 0 || key.compareTo(boundaries.get(currentIndex)) > 0)
+        {
+            switched = true;
+            currentIndex++;
+        }
+
+        if (switched)
+        {
+            if (currentWriter != null)
+                finishedWriters.add(currentWriter);
+
+            Descriptor desc = Descriptor.fromFilename(cfs.getSSTablePath(cfs.getDirectories().getLocationForDisk(directories.get(currentIndex))), format);
+            currentWriter = cfs.createSSTableMultiWriter(desc, estimatedKeys, repairedAt, sstableLevel, header, lifecycleNewTracker);
+        }
+    }
+
+    public boolean append(UnfilteredRowIterator partition)
+    {
+        maybeSwitchWriter(partition.partitionKey());
+        return currentWriter.append(partition);
+    }
+
+    @Override
+    public Collection<SSTableReader> finish(long repairedAt, long maxDataAge, boolean openResult)
+    {
+        if (currentWriter != null)
+            finishedWriters.add(currentWriter);
+        currentWriter = null;
+        for (SSTableMultiWriter writer : finishedWriters)
+        {
+            if (writer.getFilePointer() > 0)
+                finishedReaders.addAll(writer.finish(repairedAt, maxDataAge, openResult));
+            else
+                SSTableMultiWriter.abortOrDie(writer);
+        }
+        return finishedReaders;
+    }
+
+    @Override
+    public Collection<SSTableReader> finish(boolean openResult)
+    {
+        if (currentWriter != null)
+            finishedWriters.add(currentWriter);
+        currentWriter = null;
+        for (SSTableMultiWriter writer : finishedWriters)
+        {
+            if (writer.getFilePointer() > 0)
+                finishedReaders.addAll(writer.finish(openResult));
+            else
+                SSTableMultiWriter.abortOrDie(writer);
+        }
+        return finishedReaders;
+    }
+
+    @Override
+    public Collection<SSTableReader> finished()
+    {
+        return finishedReaders;
+    }
+
+    @Override
+    public SSTableMultiWriter setOpenResult(boolean openResult)
+    {
+        finishedWriters.forEach((w) -> w.setOpenResult(openResult));
+        currentWriter.setOpenResult(openResult);
+        return this;
+    }
+
+    public String getFilename()
+    {
+        return String.join("/", cfs.keyspace.getName(), cfs.getTableName());
+    }
+
+    @Override
+    public long getFilePointer()
+    {
+        return currentWriter.getFilePointer();
+    }
+
+    @Override
+    public UUID getCfId()
+    {
+        return currentWriter.getCfId();
+    }
+
+    @Override
+    public Throwable commit(Throwable accumulate)
+    {
+        if (currentWriter != null)
+            finishedWriters.add(currentWriter);
+        currentWriter = null;
+        for (SSTableMultiWriter writer : finishedWriters)
+            accumulate = writer.commit(accumulate);
+        return accumulate;
+    }
+
+    @Override
+    public Throwable abort(Throwable accumulate)
+    {
+        if (currentWriter != null)
+            finishedWriters.add(currentWriter);
+        currentWriter = null;
+        for (SSTableMultiWriter finishedWriter : finishedWriters)
+            accumulate = finishedWriter.abort(accumulate);
+
+        return accumulate;
+    }
+
+    @Override
+    public void prepareToCommit()
+    {
+        if (currentWriter != null)
+            finishedWriters.add(currentWriter);
+        currentWriter = null;
+        finishedWriters.forEach(SSTableMultiWriter::prepareToCommit);
+    }
+
+    @Override
+    public void close()
+    {
+        if (currentWriter != null)
+            finishedWriters.add(currentWriter);
+        currentWriter = null;
+        finishedWriters.forEach(SSTableMultiWriter::close);
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableFlushObserver.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableFlushObserver.java
new file mode 100644
index 0000000..f0b6bac
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableFlushObserver.java
@@ -0,0 +1,55 @@
+/*
+ * 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.io.sstable.format;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.rows.Unfiltered;
+
+/**
+ * Observer for events in the lifecycle of writing out an sstable.
+ */
+public interface SSTableFlushObserver
+{
+    /**
+     * Called before writing any data to the sstable.
+     */
+    void begin();
+
+    /**
+     * Called when a new partition in being written to the sstable,
+     * but before any cells are processed (see {@link #nextUnfilteredCluster(Unfiltered)}).
+     *
+     * @param key The key being appended to SSTable.
+     * @param indexPosition The position of the key in the SSTable PRIMARY_INDEX file.
+     */
+    void startPartition(DecoratedKey key, long indexPosition);
+
+    /**
+     * Called after the unfiltered cluster is written to the sstable.
+     * Will be preceded by a call to {@code startPartition(DecoratedKey, long)},
+     * and the cluster should be assumed to belong to that partition.
+     *
+     * @param unfilteredCluster The unfiltered cluster being added to SSTable.
+     */
+    void nextUnfilteredCluster(Unfiltered unfilteredCluster);
+
+    /**
+     * Called when all data is written to the file and it's ready to be finished up.
+     */
+    void complete();
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java
index 1286f16..4391946 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java
@@ -18,23 +18,17 @@
 package org.apache.cassandra.io.sstable.format;
 
 import com.google.common.base.CharMatcher;
-import com.google.common.collect.ImmutableList;
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.db.LegacyLayout;
 import org.apache.cassandra.db.RowIndexEntry;
 import org.apache.cassandra.db.SerializationHeader;
-import org.apache.cassandra.db.compaction.CompactionController;
 import org.apache.cassandra.io.sstable.format.big.BigFormat;
-import org.apache.cassandra.io.util.FileDataInput;
-
-import java.util.Iterator;
 
 /**
  * Provides the accessors to data on disk.
  */
 public interface SSTableFormat
 {
-    static boolean enableSSTableDevelopmentTestMode = Boolean.valueOf(System.getProperty("cassandra.test.sstableformatdevelopment","false"));
+    static boolean enableSSTableDevelopmentTestMode = Boolean.getBoolean("cassandra.test.sstableformatdevelopment");
 
 
     Version getLatestVersion();
@@ -57,6 +51,11 @@
         public final SSTableFormat info;
         public final String name;
 
+        public static Type current()
+        {
+            return BIG;
+        }
+
         private Type(String name, SSTableFormat info)
         {
             //Since format comes right after generation
diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java
index 6bb17ba..bd40332 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java
@@ -37,16 +37,23 @@
 import com.clearspring.analytics.stream.cardinality.CardinalityMergeException;
 import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus;
 import com.clearspring.analytics.stream.cardinality.ICardinality;
+
+import org.apache.cassandra.cache.ChunkCache;
 import org.apache.cassandra.cache.InstrumentingCache;
 import org.apache.cassandra.cache.KeyCacheKey;
 import org.apache.cassandra.concurrent.DebuggableThreadPoolExecutor;
+import org.apache.cassandra.concurrent.NamedThreadFactory;
 import org.apache.cassandra.concurrent.ScheduledExecutors;
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.filter.ColumnFilter;
-import org.apache.cassandra.db.rows.SliceableUnfilteredRowIterator;
+import org.apache.cassandra.db.rows.Cell;
+import org.apache.cassandra.db.rows.EncodingStats;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
 import org.apache.cassandra.dht.AbstractBounds;
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
@@ -132,16 +139,23 @@
 {
     private static final Logger logger = LoggerFactory.getLogger(SSTableReader.class);
 
-    private static final ScheduledThreadPoolExecutor syncExecutor = new ScheduledThreadPoolExecutor(1);
-    static
+    private static final ScheduledThreadPoolExecutor syncExecutor = initSyncExecutor();
+    private static ScheduledThreadPoolExecutor initSyncExecutor()
     {
+        if (DatabaseDescriptor.isClientOrToolInitialized())
+            return null;
+
+        // Do NOT start this thread pool in client mode
+
+        ScheduledThreadPoolExecutor syncExecutor = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("read-hotness-tracker"));
         // Immediately remove readMeter sync task when cancelled.
         syncExecutor.setRemoveOnCancelPolicy(true);
+        return syncExecutor;
     }
     private static final RateLimiter meterSyncThrottle = RateLimiter.create(100.0);
 
-    // Descending order
-    public static final Comparator<SSTableReader> maxTimestampComparator = (o1, o2) -> Long.compare(o2.getMaxTimestamp(), o1.getMaxTimestamp());
+    public static final Comparator<SSTableReader> maxTimestampDescending = (o1, o2) -> Long.compare(o2.getMaxTimestamp(), o1.getMaxTimestamp());
+    public static final Comparator<SSTableReader> maxTimestampAscending = (o1, o2) -> Long.compare(o1.getMaxTimestamp(), o2.getMaxTimestamp());
 
     // it's just an object, which we use regular Object equality on; we introduce a special class just for easy recognition
     public static final class UniqueIdentifier {}
@@ -152,7 +166,13 @@
 
     public static final Ordering<SSTableReader> sstableOrdering = Ordering.from(sstableComparator);
 
-    public static final Comparator<SSTableReader> sizeComparator = (o1, o2) -> Longs.compare(o1.onDiskLength(), o2.onDiskLength());
+    public static final Comparator<SSTableReader> sizeComparator = new Comparator<SSTableReader>()
+    {
+        public int compare(SSTableReader o1, SSTableReader o2)
+        {
+            return Longs.compare(o1.onDiskLength(), o2.onDiskLength());
+        }
+    };
 
     /**
      * maxDataAge is a timestamp in local server time (e.g. System.currentTimeMilli) which represents an upper bound
@@ -180,8 +200,8 @@
     public final UniqueIdentifier instanceId = new UniqueIdentifier();
 
     // indexfile and datafile: might be null before a call to load()
-    protected SegmentedFile ifile;
-    protected SegmentedFile dfile;
+    protected FileHandle ifile;
+    protected FileHandle dfile;
     protected IndexSummary indexSummary;
     protected IFilter bf;
 
@@ -405,12 +425,13 @@
         String partitionerName = metadata.partitioner.getClass().getCanonicalName();
         if (validationMetadata != null && !partitionerName.equals(validationMetadata.partitioner))
         {
-            logger.error(String.format("Cannot open %s; partitioner %s does not match system partitioner %s.  Note that the default partitioner starting with Cassandra 1.2 is Murmur3Partitioner, so you will need to edit that to match your old partitioner if upgrading.",
-                    descriptor, validationMetadata.partitioner, partitionerName));
+            logger.error("Cannot open {}; partitioner {} does not match system partitioner {}.  Note that the default partitioner starting with Cassandra 1.2 is Murmur3Partitioner, so you will need to edit that to match your old partitioner if upgrading.",
+                         descriptor, validationMetadata.partitioner, partitionerName);
             System.exit(1);
         }
 
-        logger.debug("Opening {} ({} bytes)", descriptor, new File(descriptor.filenameFor(Component.DATA)).length());
+        long fileLength = new File(descriptor.filenameFor(Component.DATA)).length();
+        logger.debug("Opening {} ({})", descriptor, FBUtilities.prettyPrintMemory(fileLength));
         SSTableReader sstable = internalOpen(descriptor,
                                              components,
                                              metadata,
@@ -419,16 +440,20 @@
                                              OpenReason.NORMAL,
                                              header == null? null : header.toHeader(metadata));
 
-        // special implementation of load to use non-pooled SegmentedFile builders
-        try(SegmentedFile.Builder ibuilder = new BufferedSegmentedFile.Builder();
-            SegmentedFile.Builder dbuilder = sstable.compression
-                ? new CompressedSegmentedFile.Builder(null)
-                : new BufferedSegmentedFile.Builder())
+        try(FileHandle.Builder ibuilder = new FileHandle.Builder(sstable.descriptor.filenameFor(Component.PRIMARY_INDEX))
+                                                     .mmapped(DatabaseDescriptor.getIndexAccessMode() == Config.DiskAccessMode.mmap)
+                                                     .withChunkCache(ChunkCache.instance);
+            FileHandle.Builder dbuilder = new FileHandle.Builder(sstable.descriptor.filenameFor(Component.DATA)).compressed(sstable.compression)
+                                                     .mmapped(DatabaseDescriptor.getDiskAccessMode() == Config.DiskAccessMode.mmap)
+                                                     .withChunkCache(ChunkCache.instance))
         {
-            if (!sstable.loadSummary(ibuilder, dbuilder))
-                sstable.buildSummary(false, ibuilder, dbuilder, false, Downsampling.BASE_SAMPLING_LEVEL);
-            sstable.ifile = ibuilder.buildIndex(sstable.descriptor, sstable.indexSummary);
-            sstable.dfile = dbuilder.buildData(sstable.descriptor, statsMetadata);
+            if (!sstable.loadSummary())
+                sstable.buildSummary(false, false, Downsampling.BASE_SAMPLING_LEVEL);
+            long indexFileLength = new File(descriptor.filenameFor(Component.PRIMARY_INDEX)).length();
+            int dataBufferSize = sstable.optimizationStrategy.bufferSize(statsMetadata.estimatedPartitionSize.percentile(DatabaseDescriptor.getDiskOptimizationEstimatePercentile()));
+            int indexBufferSize = sstable.optimizationStrategy.bufferSize(indexFileLength / sstable.indexSummary.size());
+            sstable.ifile = ibuilder.bufferSize(indexBufferSize).complete();
+            sstable.dfile = dbuilder.bufferSize(dataBufferSize).complete();
             sstable.bf = FilterFactory.AlwaysPresent;
             sstable.setup(false);
             return sstable;
@@ -481,12 +506,13 @@
         String partitionerName = metadata.partitioner.getClass().getCanonicalName();
         if (validationMetadata != null && !partitionerName.equals(validationMetadata.partitioner))
         {
-            logger.error(String.format("Cannot open %s; partitioner %s does not match system partitioner %s.  Note that the default partitioner starting with Cassandra 1.2 is Murmur3Partitioner, so you will need to edit that to match your old partitioner if upgrading.",
-                    descriptor, validationMetadata.partitioner, partitionerName));
+            logger.error("Cannot open {}; partitioner {} does not match system partitioner {}.  Note that the default partitioner starting with Cassandra 1.2 is Murmur3Partitioner, so you will need to edit that to match your old partitioner if upgrading.",
+                         descriptor, validationMetadata.partitioner, partitionerName);
             System.exit(1);
         }
 
-        logger.debug("Opening {} ({} bytes)", descriptor, new File(descriptor.filenameFor(Component.DATA)).length());
+        long fileLength = new File(descriptor.filenameFor(Component.DATA)).length();
+        logger.debug("Opening {} ({})", descriptor, FBUtilities.prettyPrintMemory(fileLength));
         SSTableReader sstable = internalOpen(descriptor,
                                              components,
                                              metadata,
@@ -587,8 +613,8 @@
     public static SSTableReader internalOpen(Descriptor desc,
                                       Set<Component> components,
                                       CFMetaData metadata,
-                                      SegmentedFile ifile,
-                                      SegmentedFile dfile,
+                                      FileHandle ifile,
+                                      FileHandle dfile,
                                       IndexSummary isummary,
                                       IFilter bf,
                                       long maxDataAge,
@@ -631,7 +657,7 @@
                             OpenReason openReason,
                             SerializationHeader header)
     {
-        super(desc, components, metadata);
+        super(desc, components, metadata, DatabaseDescriptor.getDiskOptimizationStrategy());
         this.sstableMetadata = sstableMetadata;
         this.header = header;
         this.maxDataAge = maxDataAge;
@@ -743,23 +769,33 @@
      */
     private void load(boolean recreateBloomFilter, boolean saveSummaryIfCreated) throws IOException
     {
-        try(SegmentedFile.Builder ibuilder = SegmentedFile.getBuilder(DatabaseDescriptor.getIndexAccessMode(), false);
-            SegmentedFile.Builder dbuilder = SegmentedFile.getBuilder(DatabaseDescriptor.getDiskAccessMode(), compression))
+        try(FileHandle.Builder ibuilder = new FileHandle.Builder(descriptor.filenameFor(Component.PRIMARY_INDEX))
+                                                     .mmapped(DatabaseDescriptor.getIndexAccessMode() == Config.DiskAccessMode.mmap)
+                                                     .withChunkCache(ChunkCache.instance);
+            FileHandle.Builder dbuilder = new FileHandle.Builder(descriptor.filenameFor(Component.DATA)).compressed(compression)
+                                                     .mmapped(DatabaseDescriptor.getDiskAccessMode() == Config.DiskAccessMode.mmap)
+                                                     .withChunkCache(ChunkCache.instance))
         {
-            boolean summaryLoaded = loadSummary(ibuilder, dbuilder);
+            boolean summaryLoaded = loadSummary();
             boolean buildSummary = !summaryLoaded || recreateBloomFilter;
             if (buildSummary)
-                buildSummary(recreateBloomFilter, ibuilder, dbuilder, summaryLoaded, Downsampling.BASE_SAMPLING_LEVEL);
+                buildSummary(recreateBloomFilter, summaryLoaded, Downsampling.BASE_SAMPLING_LEVEL);
+
+            int dataBufferSize = optimizationStrategy.bufferSize(sstableMetadata.estimatedPartitionSize.percentile(DatabaseDescriptor.getDiskOptimizationEstimatePercentile()));
 
             if (components.contains(Component.PRIMARY_INDEX))
-                ifile = ibuilder.buildIndex(descriptor, indexSummary);
+            {
+                long indexFileLength = new File(descriptor.filenameFor(Component.PRIMARY_INDEX)).length();
+                int indexBufferSize = optimizationStrategy.bufferSize(indexFileLength / indexSummary.size());
+                ifile = ibuilder.bufferSize(indexBufferSize).complete();
+            }
 
-            dfile = dbuilder.buildData(descriptor, sstableMetadata);
+            dfile = dbuilder.bufferSize(dataBufferSize).complete();
 
             if (buildSummary)
             {
                 if (saveSummaryIfCreated)
-                    saveSummary(ibuilder, dbuilder);
+                    saveSummary();
                 if (recreateBloomFilter)
                     saveBloomFilter();
             }
@@ -792,12 +828,10 @@
      * Build index summary(and optionally bloom filter) by reading through Index.db file.
      *
      * @param recreateBloomFilter true if recreate bloom filter
-     * @param ibuilder
-     * @param dbuilder
      * @param summaryLoaded true if index summary is already loaded and not need to build again
      * @throws IOException
      */
-    private void buildSummary(boolean recreateBloomFilter, SegmentedFile.Builder ibuilder, SegmentedFile.Builder dbuilder, boolean summaryLoaded, int samplingLevel) throws IOException
+    private void buildSummary(boolean recreateBloomFilter, boolean summaryLoaded, int samplingLevel) throws IOException
     {
          if (!components.contains(Component.PRIMARY_INDEX))
              return;
@@ -817,12 +851,11 @@
             try (IndexSummaryBuilder summaryBuilder = summaryLoaded ? null : new IndexSummaryBuilder(estimatedKeys, metadata.params.minIndexInterval, samplingLevel))
             {
                 long indexPosition;
-                RowIndexEntry.IndexSerializer rowIndexSerializer = descriptor.getFormat().getIndexSerializer(metadata, descriptor.version, header);
 
                 while ((indexPosition = primaryIndex.getFilePointer()) != indexSize)
                 {
                     ByteBuffer key = ByteBufferUtil.readWithShortLength(primaryIndex);
-                    RowIndexEntry indexEntry = rowIndexSerializer.deserialize(primaryIndex);
+                    RowIndexEntry.Serializer.skip(primaryIndex, descriptor.version);
                     DecoratedKey decoratedKey = decorateKey(key);
                     if (first == null)
                         first = decoratedKey;
@@ -853,12 +886,10 @@
      * if loaded index summary has different index interval from current value stored in schema,
      * then Summary.db file will be deleted and this returns false to rebuild summary.
      *
-     * @param ibuilder
-     * @param dbuilder
      * @return true if index summary is loaded successfully from Summary.db file.
      */
     @SuppressWarnings("resource")
-    public boolean loadSummary(SegmentedFile.Builder ibuilder, SegmentedFile.Builder dbuilder)
+    public boolean loadSummary()
     {
         File summariesFile = new File(descriptor.filenameFor(Component.SUMMARY));
         if (!summariesFile.exists())
@@ -873,8 +904,6 @@
                     metadata.params.minIndexInterval, metadata.params.maxIndexInterval);
             first = decorateKey(ByteBufferUtil.readWithLength(iStream));
             last = decorateKey(ByteBufferUtil.readWithLength(iStream));
-            ibuilder.deserializeBounds(iStream, descriptor.version);
-            dbuilder.deserializeBounds(iStream, descriptor.version);
         }
         catch (IOException e)
         {
@@ -897,25 +926,22 @@
 
     /**
      * Save index summary to Summary.db file.
-     *
-     * @param ibuilder
-     * @param dbuilder
      */
 
-    public void saveSummary(SegmentedFile.Builder ibuilder, SegmentedFile.Builder dbuilder)
+    public void saveSummary()
     {
-        saveSummary(this.descriptor, this.first, this.last, ibuilder, dbuilder, indexSummary);
+        saveSummary(this.descriptor, this.first, this.last, indexSummary);
     }
 
-    private void saveSummary(SegmentedFile.Builder ibuilder, SegmentedFile.Builder dbuilder, IndexSummary newSummary)
+    private void saveSummary(IndexSummary newSummary)
     {
-        saveSummary(this.descriptor, this.first, this.last, ibuilder, dbuilder, newSummary);
+        saveSummary(this.descriptor, this.first, this.last, newSummary);
     }
+
     /**
      * Save index summary to Summary.db file.
      */
-    public static void saveSummary(Descriptor descriptor, DecoratedKey first, DecoratedKey last,
-                                   SegmentedFile.Builder ibuilder, SegmentedFile.Builder dbuilder, IndexSummary summary)
+    public static void saveSummary(Descriptor descriptor, DecoratedKey first, DecoratedKey last, IndexSummary summary)
     {
         File summariesFile = new File(descriptor.filenameFor(Component.SUMMARY));
         if (summariesFile.exists())
@@ -926,8 +952,6 @@
             IndexSummary.serializer.serialize(summary, oStream, descriptor.version.hasSamplingLevel());
             ByteBufferUtil.writeWithLength(first.getKey(), oStream);
             ByteBufferUtil.writeWithLength(last.getKey(), oStream);
-            ibuilder.serializeBounds(oStream, descriptor.version);
-            dbuilder.serializeBounds(oStream, descriptor.version);
         }
         catch (IOException e)
         {
@@ -1086,13 +1110,13 @@
 
     private static class DropPageCache implements Runnable
     {
-        final SegmentedFile dfile;
+        final FileHandle dfile;
         final long dfilePosition;
-        final SegmentedFile ifile;
+        final FileHandle ifile;
         final long ifilePosition;
         final Runnable andThen;
 
-        private DropPageCache(SegmentedFile dfile, long dfilePosition, SegmentedFile ifile, long ifilePosition, Runnable andThen)
+        private DropPageCache(FileHandle dfile, long dfilePosition, FileHandle ifile, long ifilePosition, Runnable andThen)
         {
             this.dfile = dfile;
             this.dfilePosition = dfilePosition;
@@ -1107,7 +1131,8 @@
 
             if (ifile != null)
                 ifile.dropPageCache(ifilePosition);
-            andThen.run();
+            if (andThen != null)
+                andThen.run();
         }
     }
 
@@ -1154,12 +1179,8 @@
                         "no adjustments to min/max_index_interval");
             }
 
-            //Always save the resampled index
-            try(SegmentedFile.Builder ibuilder = SegmentedFile.getBuilder(DatabaseDescriptor.getIndexAccessMode(), false);
-                SegmentedFile.Builder dbuilder = SegmentedFile.getBuilder(DatabaseDescriptor.getDiskAccessMode(), compression))
-            {
-                saveSummary(ibuilder, dbuilder, newSummary);
-            }
+            // Always save the resampled index
+            saveSummary(newSummary);
 
             return cloneAndReplace(first, OpenReason.METADATA_CHANGE, newSummary);
         }
@@ -1231,7 +1252,7 @@
 
     /**
      * Gets the position in the index file to start scanning to find the given key (at most indexInterval keys away,
-     * modulo downsampling of the index summary). Always returns a value >= 0
+     * modulo downsampling of the index summary). Always returns a {@code value >= 0}
      */
     public long getIndexScanPosition(PartitionPosition key)
     {
@@ -1276,7 +1297,7 @@
         if (!compression)
             throw new IllegalStateException(this + " is not compressed");
 
-        return ((ICompressedFile) dfile).getMetadata();
+        return dfile.compressionMetadata().get();
     }
 
     /**
@@ -1515,7 +1536,8 @@
 
     protected RowIndexEntry getCachedPosition(KeyCacheKey unifiedKey, boolean updateStats)
     {
-        if (keyCache != null && keyCache.getCapacity() > 0 && metadata.params.caching.cacheKeys()) {
+        if (keyCache != null && keyCache.getCapacity() > 0 && metadata.params.caching.cacheKeys())
+        {
             if (updateStats)
             {
                 RowIndexEntry cachedEntry = keyCache.get(unifiedKey);
@@ -1536,14 +1558,23 @@
     }
 
     /**
-     * Get position updating key cache and stats.
-     * @see #getPosition(PartitionPosition, SSTableReader.Operator, boolean)
+     * Retrieves the position while updating the key cache and the stats.
+     * @param key The key to apply as the rhs to the given Operator. A 'fake' key is allowed to
+     * allow key selection by token bounds but only if op != * EQ
+     * @param op The Operator defining matching keys: the nearest key to the target matching the operator wins.
      */
     public final RowIndexEntry getPosition(PartitionPosition key, Operator op)
     {
         return getPosition(key, op, SSTableReadsListener.NOOP_LISTENER);
     }
 
+    /**
+     * Retrieves the position while updating the key cache and the stats.
+     * @param key The key to apply as the rhs to the given Operator. A 'fake' key is allowed to
+     * allow key selection by token bounds but only if op != * EQ
+     * @param op The Operator defining matching keys: the nearest key to the target matching the operator wins.
+     * @param listener the {@code SSTableReaderListener} that must handle the notifications.
+     */
     public final RowIndexEntry getPosition(PartitionPosition key, Operator op, SSTableReadsListener listener)
     {
         return getPosition(key, op, true, false, listener);
@@ -1558,6 +1589,7 @@
      * allow key selection by token bounds but only if op != * EQ
      * @param op The Operator defining matching keys: the nearest key to the target matching the operator wins.
      * @param updateCacheAndStats true if updating stats and cache
+     * @param listener a listener used to handle internal events
      * @return The index entry corresponding to the key, or null if the key is not present
      */
     protected abstract RowIndexEntry getPosition(PartitionPosition key,
@@ -1566,18 +1598,16 @@
                                                  boolean permitMatchPastLast,
                                                  SSTableReadsListener listener);
 
-    public abstract SliceableUnfilteredRowIterator iterator(DecoratedKey key,
-                                                            ColumnFilter selectedColumns,
-                                                            boolean reversed,
-                                                            boolean isForThrift,
-                                                            SSTableReadsListener listener);
+    public abstract UnfilteredRowIterator iterator(DecoratedKey key,
+                                                   Slices slices,
+                                                   ColumnFilter selectedColumns,
+                                                   boolean reversed,
+                                                   boolean isForThrift,
+                                                   SSTableReadsListener listener);
 
-    public abstract SliceableUnfilteredRowIterator iterator(FileDataInput file,
-                                                            DecoratedKey key,
-                                                            RowIndexEntry indexEntry,
-                                                            ColumnFilter selectedColumns,
-                                                            boolean reversed,
-                                                            boolean isForThrift);
+    public abstract UnfilteredRowIterator iterator(FileDataInput file, DecoratedKey key, RowIndexEntry indexEntry, Slices slices, ColumnFilter selectedColumns, boolean reversed, boolean isForThrift);
+
+    public abstract UnfilteredRowIterator simpleIterator(FileDataInput file, DecoratedKey key, RowIndexEntry indexEntry, boolean tombstoneOnly);
 
     /**
      * Finds and returns the first key beyond a given token in this SSTable or null if no such key exists.
@@ -1622,7 +1652,7 @@
      */
     public long uncompressedLength()
     {
-        return dfile.length;
+        return dfile.dataLength();
     }
 
     /**
@@ -1650,9 +1680,7 @@
     public void setCrcCheckChance(double crcCheckChance)
     {
         this.crcCheckChance = crcCheckChance;
-        if (compression)
-            ((CompressedSegmentedFile)dfile).metadata.parameters.setCrcCheckChance(crcCheckChance);
-
+        dfile.compressionMetadata().ifPresent(metadata -> metadata.parameters.setCrcCheckChance(crcCheckChance));
     }
 
     /**
@@ -1661,7 +1689,6 @@
      * When calling this function, the caller must ensure that the SSTableReader is not referenced anywhere
      * except for threads holding a reference.
      *
-     * @return true if the this is the first time the file was marked obsolete.  Calling this
      * multiple times is usually buggy (see exceptions in Tracker.unmarkCompacting and removeOldSSTablesSize).
      */
     public void markObsolete(Runnable tidier)
@@ -1787,6 +1814,11 @@
 
     public void createLinks(String snapshotDirectoryPath)
     {
+        createLinks(descriptor, components, snapshotDirectoryPath);
+    }
+
+    public static void createLinks(Descriptor descriptor, Set<Component> components, String snapshotDirectoryPath)
+    {
         for (Component component : components)
         {
             File sourceFile = new File(descriptor.filenameFor(component));
@@ -1802,6 +1834,26 @@
         return sstableMetadata.repairedAt != ActiveRepairService.UNREPAIRED_SSTABLE;
     }
 
+    public DecoratedKey keyAt(long indexPosition) throws IOException
+    {
+        DecoratedKey key;
+        try (FileDataInput in = ifile.createReader(indexPosition))
+        {
+            if (in.isEOF())
+                return null;
+
+            key = decorateKey(ByteBufferUtil.readWithShortLength(in));
+
+            // hint read path about key location if caching is enabled
+            // this saves index summary lookup and index file iteration which whould be pretty costly
+            // especially in presence of promoted column indexes
+            if (isKeyCacheSetup())
+                cacheKey(key, rowIndexEntrySerializer.deserialize(in, in.getFilePointer()));
+        }
+
+        return key;
+    }
+
     /**
      * TODO: Move someplace reusable
      */
@@ -1913,12 +1965,20 @@
         return sstableMetadata.maxLocalDeletionTime;
     }
 
-    /** sstable contains no tombstones if minLocalDeletionTime == Integer.MAX_VALUE */
-    public boolean hasTombstones()
+    /**
+     * Whether the sstable may contain tombstones or if it is guaranteed to not contain any.
+     * <p>
+     * Note that having that method return {@code false} guarantees the sstable has no tombstones whatsoever (so no
+     * cell tombstone, no range tombstone maker and no expiring columns), but having it return {@code true} doesn't
+     * guarantee it contains any as 1) it may simply have non-expired cells and 2) old-format sstables didn't contain
+     * enough information to decide this and so always return {@code true}.
+     */
+    public boolean mayHaveTombstones()
     {
-        // sstable contains no tombstone if minLocalDeletionTime is still set to  the default value Integer.MAX_VALUE
-        // which is bigger than any valid deletion times
-        return getMinLocalDeletionTime() != Integer.MAX_VALUE;
+        // A sstable is guaranteed to have no tombstones if it properly tracked the minLocalDeletionTime (which we only
+        // do since 3.0 - see CASSANDRA-13366) and that value is still set to its default, Cell.NO_DELETION_TIME, which
+        // is bigger than any valid deletion times.
+        return !descriptor.version.storeRows() || getMinLocalDeletionTime() != Cell.NO_DELETION_TIME;
     }
 
     public int getMinTTL()
@@ -2000,6 +2060,11 @@
         return ifile.channel;
     }
 
+    public FileHandle getIndexFile()
+    {
+        return ifile;
+    }
+
     /**
      * @param component component to get timestamp.
      * @return last modified time for given component. 0 if given component does not exist or IO error occurs.
@@ -2035,40 +2100,11 @@
             readMeter.mark();
     }
 
-    /**
-     * Checks if this sstable can overlap with another one based on the min/man clustering values.
-     * If this methods return false, we're guarantee that {@code this} and {@code other} have no overlapping
-     * data, i.e. no cells to reconcile.
-     */
-    public boolean mayOverlapsWith(SSTableReader other)
+    public EncodingStats stats()
     {
-        StatsMetadata m1 = getSSTableMetadata();
-        StatsMetadata m2 = other.getSSTableMetadata();
-
-        if (m1.minClusteringValues.isEmpty() || m1.maxClusteringValues.isEmpty() || m2.minClusteringValues.isEmpty() || m2.maxClusteringValues.isEmpty())
-            return true;
-
-        return !(compare(m1.maxClusteringValues, m2.minClusteringValues) < 0 || compare(m1.minClusteringValues, m2.maxClusteringValues) > 0);
-    }
-
-    private int compare(List<ByteBuffer> values1, List<ByteBuffer> values2)
-    {
-        ClusteringComparator comparator = metadata.comparator;
-        for (int i = 0; i < Math.min(values1.size(), values2.size()); i++)
-        {
-            int cmp = comparator.subtype(i).compare(values1.get(i), values2.get(i));
-            if (cmp != 0)
-                return cmp;
-        }
-        return 0;
-    }
-
-    public static class SizeComparator implements Comparator<SSTableReader>
-    {
-        public int compare(SSTableReader o1, SSTableReader o2)
-        {
-            return Longs.compare(o1.onDiskLength(), o2.onDiskLength());
-        }
+        // We could return sstable.header.stats(), but this may not be as accurate than the actual sstable stats (see
+        // SerializationHeader.make() for details) so we use the latter instead.
+        return new EncodingStats(getMinTimestamp(), getMinLocalDeletionTime(), getMinTTL());
     }
 
     public Ref<SSTableReader> tryRef()
@@ -2125,8 +2161,8 @@
         private IFilter bf;
         private IndexSummary summary;
 
-        private SegmentedFile dfile;
-        private SegmentedFile ifile;
+        private FileHandle dfile;
+        private FileHandle ifile;
         private Runnable runOnClose;
         private boolean isReplaced = false;
 
@@ -2256,7 +2292,8 @@
 
             // Don't track read rates for tables in the system keyspace and don't bother trying to load or persist
             // the read meter when in client mode.
-            if (Schema.isLocalSystemKeyspace(desc.ksname) || !DatabaseDescriptor.isDaemonInitialized())
+            // Also, do not track read rates when running in client or tools mode (syncExecuter isn't available in these modes)
+            if (SchemaConstants.isLocalSystemKeyspace(desc.ksname) || DatabaseDescriptor.isClientOrToolInitialized())
             {
                 readMeter = null;
                 readMeterSyncFuture = NULL;
@@ -2345,6 +2382,7 @@
 
     public static void shutdownBlocking(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
     {
+
         ExecutorUtils.shutdownNowAndWait(timeout, unit, syncExecutor);
         resetTidying();
     }
diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableReadsListener.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableReadsListener.java
index 6d384bf..8f6e3c0 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/SSTableReadsListener.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableReadsListener.java
@@ -18,6 +18,7 @@
 package org.apache.cassandra.io.sstable.format;
 
 import org.apache.cassandra.db.RowIndexEntry;
+import org.apache.cassandra.io.sstable.format.SSTableReadsListener.SelectionReason;
 
 /**
  * Listener for receiving notifications associated with reading SSTables.
diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableWriter.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableWriter.java
index 998f2e7..83a07fd 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/SSTableWriter.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableWriter.java
@@ -18,20 +18,22 @@
 
 package org.apache.cassandra.io.sstable.format;
 
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.RowIndexEntry;
 import org.apache.cassandra.db.SerializationHeader;
+import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
 import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.io.FSWriteError;
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.SSTable;
@@ -58,6 +60,7 @@
     protected final RowIndexEntry.IndexSerializer rowIndexEntrySerializer;
     protected final SerializationHeader header;
     protected final TransactionalProxy txnProxy = txnProxy();
+    protected final Collection<SSTableFlushObserver> observers;
 
     protected abstract TransactionalProxy txnProxy();
 
@@ -69,19 +72,21 @@
         protected boolean openResult;
     }
 
-    protected SSTableWriter(Descriptor descriptor, 
-                            long keyCount, 
-                            long repairedAt, 
-                            CFMetaData metadata, 
-                            MetadataCollector metadataCollector, 
-                            SerializationHeader header)
+    protected SSTableWriter(Descriptor descriptor,
+                            long keyCount,
+                            long repairedAt,
+                            CFMetaData metadata,
+                            MetadataCollector metadataCollector,
+                            SerializationHeader header,
+                            Collection<SSTableFlushObserver> observers)
     {
-        super(descriptor, components(metadata), metadata);
+        super(descriptor, components(metadata), metadata, DatabaseDescriptor.getDiskOptimizationStrategy());
         this.keyCount = keyCount;
         this.repairedAt = repairedAt;
         this.metadataCollector = metadataCollector;
         this.header = header != null ? header : SerializationHeader.makeWithoutStats(metadata); //null header indicates streaming from pre-3.0 sstable
         this.rowIndexEntrySerializer = descriptor.version.getSSTableFormat().getIndexSerializer(metadata, descriptor.version, header);
+        this.observers = observers == null ? Collections.emptySet() : observers;
     }
 
     public static SSTableWriter create(Descriptor descriptor,
@@ -90,16 +95,23 @@
                                        CFMetaData metadata,
                                        MetadataCollector metadataCollector,
                                        SerializationHeader header,
+                                       Collection<Index> indexes,
                                        LifecycleNewTracker lifecycleNewTracker)
     {
         Factory writerFactory = descriptor.getFormat().getWriterFactory();
-        return writerFactory.open(descriptor, keyCount, repairedAt, metadata, metadataCollector, header, lifecycleNewTracker);
+        return writerFactory.open(descriptor, keyCount, repairedAt, metadata, metadataCollector, header, observers(descriptor, indexes, lifecycleNewTracker.opType()), lifecycleNewTracker);
     }
 
-    public static SSTableWriter create(Descriptor descriptor, long keyCount, long repairedAt, int sstableLevel, SerializationHeader header, LifecycleNewTracker lifecycleNewTracker)
+    public static SSTableWriter create(Descriptor descriptor,
+                                       long keyCount,
+                                       long repairedAt,
+                                       int sstableLevel,
+                                       SerializationHeader header,
+                                       Collection<Index> indexes,
+                                       LifecycleNewTracker lifecycleNewTracker)
     {
         CFMetaData metadata = Schema.instance.getCFMetaData(descriptor);
-        return create(metadata, descriptor, keyCount, repairedAt, sstableLevel, header, lifecycleNewTracker);
+        return create(metadata, descriptor, keyCount, repairedAt, sstableLevel, header, indexes, lifecycleNewTracker);
     }
 
     public static SSTableWriter create(CFMetaData metadata,
@@ -108,21 +120,34 @@
                                        long repairedAt,
                                        int sstableLevel,
                                        SerializationHeader header,
+                                       Collection<Index> indexes,
                                        LifecycleNewTracker lifecycleNewTracker)
     {
         MetadataCollector collector = new MetadataCollector(metadata.comparator).sstableLevel(sstableLevel);
-        return create(descriptor, keyCount, repairedAt, metadata, collector, header, lifecycleNewTracker);
+        return create(descriptor, keyCount, repairedAt, metadata, collector, header, indexes, lifecycleNewTracker);
     }
 
-    public static SSTableWriter create(String filename, long keyCount, long repairedAt, int sstableLevel, SerializationHeader header, LifecycleNewTracker lifecycleNewTracker)
+    public static SSTableWriter create(String filename,
+                                       long keyCount,
+                                       long repairedAt,
+                                       int sstableLevel,
+                                       SerializationHeader header,
+                                       Collection<Index> indexes,
+                                       LifecycleNewTracker lifecycleNewTracker)
     {
-        return create(Descriptor.fromFilename(filename), keyCount, repairedAt, sstableLevel, header, lifecycleNewTracker);
+        return create(Descriptor.fromFilename(filename), keyCount, repairedAt, sstableLevel, header, indexes, lifecycleNewTracker);
     }
 
     @VisibleForTesting
-    public static SSTableWriter create(String filename, long keyCount, long repairedAt, SerializationHeader header, LifecycleNewTracker lifecycleNewTracker)
+    public static SSTableWriter create(String filename,
+                                       long keyCount,
+                                       long repairedAt,
+                                       SerializationHeader header,
+                                       Collection<Index> indexes,
+                                       LifecycleNewTracker lifecycleNewTracker)
     {
-        return create(Descriptor.fromFilename(filename), keyCount, repairedAt, 0, header, lifecycleNewTracker);
+        Descriptor descriptor = Descriptor.fromFilename(filename);
+        return create(descriptor, keyCount, repairedAt, 0, header, indexes, lifecycleNewTracker);
     }
 
     private static Set<Component> components(CFMetaData metadata)
@@ -150,6 +175,27 @@
         return components;
     }
 
+    private static Collection<SSTableFlushObserver> observers(Descriptor descriptor,
+                                                              Collection<Index> indexes,
+                                                              OperationType operationType)
+    {
+        if (indexes == null)
+            return Collections.emptyList();
+
+        List<SSTableFlushObserver> observers = new ArrayList<>(indexes.size());
+        for (Index index : indexes)
+        {
+            SSTableFlushObserver observer = index.getFlushObserver(descriptor, operationType);
+            if (observer != null)
+            {
+                observer.begin();
+                observers.add(observer);
+            }
+        }
+
+        return ImmutableList.copyOf(observers);
+    }
+
     public abstract void mark();
 
     /**
@@ -167,6 +213,11 @@
 
     public abstract long getOnDiskFilePointer();
 
+    public long getEstimatedOnDiskBytesWritten()
+    {
+        return getOnDiskFilePointer();
+    }
+
     public abstract void resetAndTruncate();
 
     public SSTableWriter setRepairedAt(long repairedAt)
@@ -211,6 +262,7 @@
     {
         setOpenResult(openResult);
         txnProxy.finish();
+        observers.forEach(SSTableFlushObserver::complete);
         return finished();
     }
 
@@ -231,7 +283,14 @@
 
     public final Throwable commit(Throwable accumulate)
     {
-        return txnProxy.commit(accumulate);
+        try
+        {
+            return txnProxy.commit(accumulate);
+        }
+        finally
+        {
+            observers.forEach(SSTableFlushObserver::complete);
+        }
     }
 
     public final Throwable abort(Throwable accumulate)
@@ -285,6 +344,7 @@
                                            CFMetaData metadata,
                                            MetadataCollector metadataCollector,
                                            SerializationHeader header,
+                                           Collection<SSTableFlushObserver> observers,
                                            LifecycleNewTracker lifecycleNewTracker);
     }
 }
diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigFormat.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigFormat.java
index c26e468..de40e60 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/big/BigFormat.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigFormat.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.io.sstable.format.big;
 
+import java.util.Collection;
 import java.util.Set;
 
 import org.apache.cassandra.config.CFMetaData;
@@ -25,10 +26,7 @@
 import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
-import org.apache.cassandra.io.sstable.format.SSTableFormat;
-import org.apache.cassandra.io.sstable.format.SSTableReader;
-import org.apache.cassandra.io.sstable.format.SSTableWriter;
-import org.apache.cassandra.io.sstable.format.Version;
+import org.apache.cassandra.io.sstable.format.*;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
 import org.apache.cassandra.io.sstable.metadata.StatsMetadata;
 import org.apache.cassandra.net.MessagingService;
@@ -88,9 +86,10 @@
                                   CFMetaData metadata,
                                   MetadataCollector metadataCollector,
                                   SerializationHeader header,
+                                  Collection<SSTableFlushObserver> observers,
                                   LifecycleNewTracker lifecycleNewTracker)
         {
-            return new BigTableWriter(descriptor, keyCount, repairedAt, metadata, metadataCollector, header, lifecycleNewTracker);
+            return new BigTableWriter(descriptor, keyCount, repairedAt, metadata, metadataCollector, header, observers, lifecycleNewTracker);
         }
     }
 
diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableReader.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableReader.java
index 080cf04..db28bcd 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableReader.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableReader.java
@@ -21,17 +21,16 @@
 import org.apache.cassandra.cache.KeyCacheKey;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.rows.SliceableUnfilteredRowIterator;
 import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.columniterator.SSTableIterator;
 import org.apache.cassandra.db.columniterator.SSTableReversedIterator;
+import org.apache.cassandra.db.rows.Rows;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.db.rows.UnfilteredRowIterators;
 import org.apache.cassandra.dht.AbstractBounds;
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
-import org.apache.cassandra.io.sstable.Component;
-import org.apache.cassandra.io.sstable.CorruptSSTableException;
-import org.apache.cassandra.io.sstable.Descriptor;
-import org.apache.cassandra.io.sstable.ISSTableScanner;
+import org.apache.cassandra.io.sstable.*;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.sstable.format.SSTableReadsListener;
 import org.apache.cassandra.io.sstable.format.SSTableReadsListener.SkippingReason;
@@ -60,22 +59,19 @@
         super(desc, components, metadata, maxDataAge, sstableMetadata, openReason, header);
     }
 
-    public SliceableUnfilteredRowIterator iterator(DecoratedKey key,
-                                                   ColumnFilter selectedColumns,
-                                                   boolean reversed,
-                                                   boolean isForThrift,
-                                                   SSTableReadsListener listener)
+    public UnfilteredRowIterator iterator(DecoratedKey key, Slices slices, ColumnFilter selectedColumns, boolean reversed, boolean isForThrift, SSTableReadsListener listener)
     {
-        return reversed
-             ? new SSTableReversedIterator(this, key, selectedColumns, isForThrift, listener)
-             : new SSTableIterator(this, key, selectedColumns, isForThrift, listener);
+        RowIndexEntry rie = getPosition(key, SSTableReader.Operator.EQ, listener);
+        return iterator(null, key, rie, slices, selectedColumns, reversed, isForThrift);
     }
 
-    public SliceableUnfilteredRowIterator iterator(FileDataInput file, DecoratedKey key, RowIndexEntry indexEntry, ColumnFilter selectedColumns, boolean reversed, boolean isForThrift)
+    public UnfilteredRowIterator iterator(FileDataInput file, DecoratedKey key, RowIndexEntry indexEntry, Slices slices, ColumnFilter selectedColumns, boolean reversed, boolean isForThrift)
     {
+        if (indexEntry == null)
+            return UnfilteredRowIterators.noRowsIterator(metadata, key, Rows.EMPTY_STATIC_ROW, DeletionTime.LIVE, reversed);
         return reversed
-             ? new SSTableReversedIterator(this, file, key, indexEntry, selectedColumns, isForThrift)
-             : new SSTableIterator(this, file, key, indexEntry, selectedColumns, isForThrift);
+             ? new SSTableReversedIterator(this, file, key, indexEntry, slices, selectedColumns, isForThrift, ifile)
+             : new SSTableIterator(this, file, key, indexEntry, slices, selectedColumns, isForThrift, ifile);
     }
 
     @Override
@@ -124,14 +120,14 @@
     }
 
 
-    /**
-     * @param key The key to apply as the rhs to the given Operator. A 'fake' key is allowed to
-     * allow key selection by token bounds but only if op != * EQ
-     * @param op The Operator defining matching keys: the nearest key to the target matching the operator wins.
-     * @param updateCacheAndStats true if updating stats and cache
-     * @param listener a listener used to handle internal events
-     * @return The index entry corresponding to the key, or null if the key is not present
-     */
+    @SuppressWarnings("resource") // caller to close
+    @Override
+    public UnfilteredRowIterator simpleIterator(FileDataInput dfile, DecoratedKey key, RowIndexEntry position, boolean tombstoneOnly)
+    {
+        return SSTableIdentityIterator.create(this, dfile, position, key, tombstoneOnly);
+    }
+
+    @Override
     protected RowIndexEntry getPosition(PartitionPosition key,
                                         Operator op,
                                         boolean updateCacheAndStats,
@@ -252,7 +248,7 @@
                 if (opSatisfied)
                 {
                     // read data position from index entry
-                    RowIndexEntry indexEntry = rowIndexEntrySerializer.deserialize(in);
+                    RowIndexEntry indexEntry = rowIndexEntrySerializer.deserialize(in, in.getFilePointer());
                     if (exactMatch && updateCacheAndStats)
                     {
                         assert key instanceof DecoratedKey; // key can be == to the index key only if it's a true row key
@@ -275,7 +271,7 @@
                     if (op == Operator.EQ && updateCacheAndStats)
                         bloomFilterTracker.addTruePositive();
                     listener.onSSTableSelected(this, indexEntry, SelectionReason.INDEX_ENTRY_FOUND);
-                    Tracing.trace("Partition index with {} entries found for sstable {}", indexEntry.columnsIndex().size(), descriptor.generation);
+                    Tracing.trace("Partition index with {} entries found for sstable {}", indexEntry.columnsIndexCount(), descriptor.generation);
                     return indexEntry;
                 }
 
diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java
index 82d8211..f4bd1ea 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java
@@ -64,13 +64,18 @@
     private final RowIndexEntry.IndexSerializer rowIndexEntrySerializer;
     private final boolean isForThrift;
     private final SSTableReadsListener listener;
+    private long startScan = -1;
+    private long bytesScanned = 0;
 
     protected Iterator<UnfilteredRowIterator> iterator;
 
     // Full scan of the sstables
     public static ISSTableScanner getScanner(SSTableReader sstable, RateLimiter limiter)
     {
-        return new BigTableScanner(sstable, limiter, Iterators.singletonIterator(fullRange(sstable)));
+        return new BigTableScanner(sstable,
+                                   ColumnFilter.all(sstable.metadata),
+                                   limiter,
+                                   Iterators.singletonIterator(fullRange(sstable)));
     }
 
     public static ISSTableScanner getScanner(SSTableReader sstable,
@@ -80,13 +85,7 @@
                                              boolean isForThrift,
                                              SSTableReadsListener listener)
     {
-        return new BigTableScanner(sstable,
-                                   columns,
-                                   dataRange,
-                                   limiter,
-                                   isForThrift,
-                                   makeBounds(sstable, dataRange).iterator(),
-                                   listener);
+        return new BigTableScanner(sstable, columns, dataRange, limiter, isForThrift, makeBounds(sstable, dataRange).iterator(), listener);
     }
 
     public static ISSTableScanner getScanner(SSTableReader sstable, Collection<Range<Token>> tokenRanges, RateLimiter limiter)
@@ -96,19 +95,23 @@
         if (positions.isEmpty())
             return new EmptySSTableScanner(sstable);
 
-        return new BigTableScanner(sstable, limiter, makeBounds(sstable, tokenRanges).iterator());
+        return new BigTableScanner(sstable,
+                                   ColumnFilter.all(sstable.metadata),
+                                   limiter,
+                                   makeBounds(sstable, tokenRanges).iterator());
     }
 
     public static ISSTableScanner getScanner(SSTableReader sstable, Iterator<AbstractBounds<PartitionPosition>> rangeIterator)
     {
-        return new BigTableScanner(sstable, null, rangeIterator);
+        return new BigTableScanner(sstable, ColumnFilter.all(sstable.metadata), null, rangeIterator);
     }
 
     private BigTableScanner(SSTableReader sstable,
+                            ColumnFilter columns,
                             RateLimiter limiter,
                             Iterator<AbstractBounds<PartitionPosition>> rangeIterator)
     {
-        this(sstable, ColumnFilter.all(sstable.metadata), null, limiter, false, rangeIterator, SSTableReadsListener.NOOP_LISTENER);
+        this(sstable, columns, null, limiter, false, rangeIterator, SSTableReadsListener.NOOP_LISTENER);
     }
 
     private BigTableScanner(SSTableReader sstable,
@@ -250,6 +253,16 @@
         return dfile.getFilePointer();
     }
 
+    public long getBytesScanned()
+    {
+        return bytesScanned;
+    }
+
+    public long getCompressedLengthInBytes()
+    {
+        return sstable.onDiskLength();
+    }
+
     public String getBackingFiles()
     {
         return sstable.toString();
@@ -305,18 +318,22 @@
                 {
                     do
                     {
+                        if (startScan != -1)
+                            bytesScanned += dfile.getFilePointer() - startScan;
+
                         // we're starting the first range or we just passed the end of the previous range
                         if (!rangeIterator.hasNext())
                             return endOfData();
 
                         currentRange = rangeIterator.next();
                         seekToCurrentRangeStart();
+                        startScan = dfile.getFilePointer();
 
                         if (ifile.isEOF())
                             return endOfData();
 
                         currentKey = sstable.decorateKey(ByteBufferUtil.readWithShortLength(ifile));
-                        currentEntry = rowIndexEntrySerializer.deserialize(ifile);
+                        currentEntry = rowIndexEntrySerializer.deserialize(ifile, ifile.getFilePointer());
                     } while (!currentRange.contains(currentKey));
                 }
                 else
@@ -335,7 +352,7 @@
                 {
                     // we need the position of the start of the next key, regardless of whether it falls in the current range
                     nextKey = sstable.decorateKey(ByteBufferUtil.readWithShortLength(ifile));
-                    nextEntry = rowIndexEntrySerializer.deserialize(ifile);
+                    nextEntry = rowIndexEntrySerializer.deserialize(ifile, ifile.getFilePointer());
 
                     if (!currentRange.contains(nextKey))
                     {
@@ -353,17 +370,26 @@
                 {
                     protected UnfilteredRowIterator initializeIterator()
                     {
+
+                        if (startScan != -1)
+                            bytesScanned += dfile.getFilePointer() - startScan;
+
                         try
                         {
                             if (dataRange == null)
                             {
                                 dfile.seek(currentEntry.position);
+                                startScan = dfile.getFilePointer();
                                 ByteBufferUtil.skipShortLength(dfile); // key
-                                return new SSTableIdentityIterator(sstable, dfile, partitionKey());
+                                return SSTableIdentityIterator.create(sstable, dfile, partitionKey());
+                            }
+                            else
+                            {
+                                startScan = dfile.getFilePointer();
                             }
 
                             ClusteringIndexFilter filter = dataRange.clusteringIndexFilter(partitionKey());
-                            return filter.filter(sstable.iterator(dfile, partitionKey(), currentEntry, columns, filter.isReversed(), isForThrift));
+                            return sstable.iterator(dfile, partitionKey(), currentEntry, filter.getSlices(BigTableScanner.this.metadata()), columns, filter.isReversed(), isForThrift);
                         }
                         catch (CorruptSSTableException | IOException e)
                         {
@@ -410,6 +436,16 @@
             return 0;
         }
 
+        public long getBytesScanned()
+        {
+            return 0;
+        }
+
+        public long getCompressedLengthInBytes()
+        {
+            return 0;
+        }
+
         public String getBackingFiles()
         {
             return sstable.getFilename();
diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java
index 1c91092..b0d9fb1 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java
@@ -17,71 +17,91 @@
  */
 package org.apache.cassandra.io.sstable.format.big;
 
-import java.io.*;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
 import java.util.Map;
-
-import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.transform.Transformation;
-import org.apache.cassandra.io.sstable.*;
-import org.apache.cassandra.io.sstable.format.SSTableReader;
-import org.apache.cassandra.io.sstable.format.SSTableWriter;
+import java.util.Optional;
 
 import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.cache.ChunkCache;
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.db.transform.Transformation;
 import org.apache.cassandra.io.FSWriteError;
 import org.apache.cassandra.io.compress.CompressedSequentialWriter;
+import org.apache.cassandra.io.sstable.*;
+import org.apache.cassandra.io.sstable.format.SSTableFlushObserver;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.sstable.format.SSTableWriter;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
 import org.apache.cassandra.io.sstable.metadata.MetadataComponent;
 import org.apache.cassandra.io.sstable.metadata.MetadataType;
 import org.apache.cassandra.io.sstable.metadata.StatsMetadata;
 import org.apache.cassandra.io.util.*;
-import org.apache.cassandra.utils.ByteBufferUtil;
-import org.apache.cassandra.utils.FBUtilities;
-import org.apache.cassandra.utils.FilterFactory;
-import org.apache.cassandra.utils.IFilter;
+import org.apache.cassandra.utils.*;
 import org.apache.cassandra.utils.concurrent.Transactional;
 
-import org.apache.cassandra.utils.SyncUtil;
-
 public class BigTableWriter extends SSTableWriter
 {
     private static final Logger logger = LoggerFactory.getLogger(BigTableWriter.class);
 
+    private final ColumnIndex columnIndexWriter;
     private final IndexWriter iwriter;
-    private final SegmentedFile.Builder dbuilder;
+    private final FileHandle.Builder dbuilder;
     protected final SequentialWriter dataFile;
     private DecoratedKey lastWrittenKey;
     private DataPosition dataMark;
+    private long lastEarlyOpenLength = 0;
+    private final Optional<ChunkCache> chunkCache = Optional.ofNullable(ChunkCache.instance);
 
-    public BigTableWriter(Descriptor descriptor, 
-                          Long keyCount, 
-                          Long repairedAt, 
-                          CFMetaData metadata, 
+    private final SequentialWriterOption writerOption = SequentialWriterOption.newBuilder()
+                                                        .trickleFsync(DatabaseDescriptor.getTrickleFsync())
+                                                        .trickleFsyncByteInterval(DatabaseDescriptor.getTrickleFsyncIntervalInKb() * 1024)
+                                                        .build();
+
+    public BigTableWriter(Descriptor descriptor,
+                          long keyCount,
+                          long repairedAt,
+                          CFMetaData metadata,
                           MetadataCollector metadataCollector, 
                           SerializationHeader header,
+                          Collection<SSTableFlushObserver> observers,
                           LifecycleNewTracker lifecycleNewTracker)
     {
-        super(descriptor, keyCount, repairedAt, metadata, metadataCollector, header);
+        super(descriptor, keyCount, repairedAt, metadata, metadataCollector, header, observers);
         lifecycleNewTracker.trackNew(this); // must track before any files are created
 
         if (compression)
         {
-            dataFile = SequentialWriter.open(getFilename(),
+            dataFile = new CompressedSequentialWriter(new File(getFilename()),
                                              descriptor.filenameFor(Component.COMPRESSION_INFO),
+                                             new File(descriptor.filenameFor(descriptor.digestComponent)),
+                                             writerOption,
                                              metadata.compressionParams(),
                                              metadataCollector);
-            dbuilder = SegmentedFile.getCompressedBuilder((CompressedSequentialWriter) dataFile);
         }
         else
         {
-            dataFile = SequentialWriter.open(new File(getFilename()), new File(descriptor.filenameFor(Component.CRC)));
-            dbuilder = SegmentedFile.getBuilder(DatabaseDescriptor.getDiskAccessMode(), false);
+            dataFile = new ChecksummedSequentialWriter(new File(getFilename()),
+                    new File(descriptor.filenameFor(Component.CRC)),
+                    new File(descriptor.filenameFor(descriptor.digestComponent)),
+                    writerOption);
         }
-        iwriter = new IndexWriter(keyCount, dataFile);
+        dbuilder = new FileHandle.Builder(descriptor.filenameFor(Component.DATA)).compressed(compression)
+                                              .mmapped(DatabaseDescriptor.getDiskAccessMode() == Config.DiskAccessMode.mmap);
+        chunkCache.ifPresent(dbuilder::withChunkCache);
+        iwriter = new IndexWriter(keyCount);
+
+        columnIndexWriter = new ColumnIndex(this.header, dataFile, descriptor.version, this.observers, getRowIndexEntrySerializer().indexInfoSerializer());
     }
 
     public void mark()
@@ -107,7 +127,7 @@
         return (lastWrittenKey == null) ? 0 : dataFile.position();
     }
 
-    private void afterAppend(DecoratedKey decoratedKey, long dataEnd, RowIndexEntry index) throws IOException
+    private void afterAppend(DecoratedKey decoratedKey, long dataEnd, RowIndexEntry index, ByteBuffer indexInfo) throws IOException
     {
         metadataCollector.addKey(decoratedKey.getKey());
         lastWrittenKey = decoratedKey;
@@ -117,7 +137,7 @@
 
         if (logger.isTraceEnabled())
             logger.trace("wrote {} at {}", decoratedKey, dataEnd);
-        iwriter.append(decoratedKey, index, dataEnd);
+        iwriter.append(decoratedKey, index, dataEnd, indexInfo);
     }
 
     /**
@@ -143,18 +163,33 @@
             return null;
 
         long startPosition = beforeAppend(key);
+        observers.forEach((o) -> o.startPartition(key, iwriter.indexFile.position()));
+
+        //Reuse the writer for each row
+        columnIndexWriter.reset();
 
         try (UnfilteredRowIterator collecting = Transformation.apply(iterator, new StatsCollector(metadataCollector)))
         {
-            ColumnIndex index = ColumnIndex.writeAndBuildIndex(collecting, dataFile, header, descriptor.version);
+            columnIndexWriter.buildRowIndex(collecting);
 
-            RowIndexEntry entry = RowIndexEntry.create(startPosition, collecting.partitionLevelDeletion(), index);
+            // afterAppend() writes the partition key before the first RowIndexEntry - so we have to add it's
+            // serialized size to the index-writer position
+            long indexFilePosition = ByteBufferUtil.serializedSizeWithShortLength(key.getKey()) + iwriter.indexFile.position();
+
+            RowIndexEntry entry = RowIndexEntry.create(startPosition, indexFilePosition,
+                                                       collecting.partitionLevelDeletion(),
+                                                       columnIndexWriter.headerLength,
+                                                       columnIndexWriter.columnIndexCount,
+                                                       columnIndexWriter.indexInfoSerializedSize(),
+                                                       columnIndexWriter.indexSamples(),
+                                                       columnIndexWriter.offsets(),
+                                                       getRowIndexEntrySerializer().indexInfoSerializer());
 
             long endPosition = dataFile.position();
             long rowSize = endPosition - startPosition;
             maybeLogLargePartitionWarning(key, rowSize);
             metadataCollector.addPartitionSizeInBytes(rowSize);
-            afterAppend(key, endPosition, entry);
+            afterAppend(key, endPosition, entry, columnIndexWriter.buffer());
             return entry;
         }
         catch (IOException e)
@@ -163,12 +198,17 @@
         }
     }
 
+    private RowIndexEntry.IndexSerializer<IndexInfo> getRowIndexEntrySerializer()
+    {
+        return (RowIndexEntry.IndexSerializer<IndexInfo>) rowIndexEntrySerializer;
+    }
+
     private void maybeLogLargePartitionWarning(DecoratedKey key, long rowSize)
     {
         if (rowSize > DatabaseDescriptor.getCompactionLargePartitionWarningThreshold())
         {
             String keyString = metadata.getKeyValidator().getString(key.getKey());
-            logger.warn("Writing large partition {}/{}:{} ({} bytes to sstable {}) ", metadata.ksName, metadata.cfName, keyString, rowSize, getFilename());
+            logger.warn("Writing large partition {}/{}:{} ({}) to sstable {}", metadata.ksName, metadata.cfName, keyString, FBUtilities.prettyPrintMemory(rowSize), getFilename());
         }
     }
 
@@ -241,8 +281,14 @@
         assert boundary.indexLength > 0 && boundary.dataLength > 0;
         // open the reader early
         IndexSummary indexSummary = iwriter.summary.build(metadata.partitioner, boundary);
-        SegmentedFile ifile = iwriter.builder.buildIndex(descriptor, indexSummary, boundary);
-        SegmentedFile dfile = dbuilder.buildData(descriptor, stats, boundary);
+        long indexFileLength = new File(descriptor.filenameFor(Component.PRIMARY_INDEX)).length();
+        int indexBufferSize = optimizationStrategy.bufferSize(indexFileLength / indexSummary.size());
+        FileHandle ifile = iwriter.builder.bufferSize(indexBufferSize).complete(boundary.indexLength);
+        if (compression)
+            dbuilder.withCompressionMetadata(((CompressedSequentialWriter) dataFile).open(boundary.dataLength));
+        int dataBufferSize = optimizationStrategy.bufferSize(stats.estimatedPartitionSize.percentile(DatabaseDescriptor.getDiskOptimizationEstimatePercentile()));
+        FileHandle dfile = dbuilder.bufferSize(dataBufferSize).complete(boundary.dataLength);
+        invalidateCacheAtBoundary(dfile);
         SSTableReader sstable = SSTableReader.internalOpen(descriptor,
                                                            components, metadata,
                                                            ifile, dfile, indexSummary,
@@ -254,17 +300,26 @@
         return sstable;
     }
 
+    void invalidateCacheAtBoundary(FileHandle dfile)
+    {
+        chunkCache.ifPresent(cache -> {
+            if (lastEarlyOpenLength != 0 && dfile.dataLength() > lastEarlyOpenLength)
+                cache.invalidatePosition(dfile, lastEarlyOpenLength);
+        });
+        lastEarlyOpenLength = dfile.dataLength();
+    }
+
     public SSTableReader openFinalEarly()
     {
         // we must ensure the data is completely flushed to disk
         dataFile.sync();
         iwriter.indexFile.sync();
 
-        return openFinal(descriptor, SSTableReader.OpenReason.EARLY);
+        return openFinal(SSTableReader.OpenReason.EARLY);
     }
 
     @SuppressWarnings("resource")
-    private SSTableReader openFinal(Descriptor desc, SSTableReader.OpenReason openReason)
+    private SSTableReader openFinal(SSTableReader.OpenReason openReason)
     {
         if (maxDataAge < 0)
             maxDataAge = System.currentTimeMillis();
@@ -272,9 +327,15 @@
         StatsMetadata stats = statsMetadata();
         // finalize in-memory state for the reader
         IndexSummary indexSummary = iwriter.summary.build(this.metadata.partitioner);
-        SegmentedFile ifile = iwriter.builder.buildIndex(desc, indexSummary);
-        SegmentedFile dfile = dbuilder.buildData(desc, stats);
-        SSTableReader sstable = SSTableReader.internalOpen(desc,
+        long indexFileLength = new File(descriptor.filenameFor(Component.PRIMARY_INDEX)).length();
+        int dataBufferSize = optimizationStrategy.bufferSize(stats.estimatedPartitionSize.percentile(DatabaseDescriptor.getDiskOptimizationEstimatePercentile()));
+        int indexBufferSize = optimizationStrategy.bufferSize(indexFileLength / indexSummary.size());
+        FileHandle ifile = iwriter.builder.bufferSize(indexBufferSize).complete();
+        if (compression)
+            dbuilder.withCompressionMetadata(((CompressedSequentialWriter) dataFile).open(0));
+        FileHandle dfile = dbuilder.bufferSize(dataBufferSize).complete();
+        invalidateCacheAtBoundary(dfile);
+        SSTableReader sstable = SSTableReader.internalOpen(descriptor,
                                                            components,
                                                            this.metadata,
                                                            ifile,
@@ -303,14 +364,14 @@
             iwriter.prepareToCommit();
 
             // write sstable statistics
-            dataFile.setDescriptor(descriptor).prepareToCommit();
+            dataFile.prepareToCommit();
             writeMetadata(descriptor, finalizeMetadata());
 
             // save the table of components
             SSTable.appendTOC(descriptor, components);
 
             if (openResult)
-                finalReader = openFinal(descriptor, SSTableReader.OpenReason.NORMAL);
+                finalReader = openFinal(SSTableReader.OpenReason.NORMAL);
         }
 
         protected Throwable doCommit(Throwable accumulate)
@@ -335,13 +396,13 @@
         }
     }
 
-    private static void writeMetadata(Descriptor desc, Map<MetadataType, MetadataComponent> components)
+    private void writeMetadata(Descriptor desc, Map<MetadataType, MetadataComponent> components)
     {
         File file = new File(desc.filenameFor(Component.STATS));
-        try (SequentialWriter out = SequentialWriter.open(file))
+        try (SequentialWriter out = new SequentialWriter(file, writerOption))
         {
             desc.getMetadataSerializer().serialize(components, out, desc.version);
-            out.setDescriptor(desc).finish();
+            out.finish();
         }
         catch (IOException e)
         {
@@ -359,38 +420,32 @@
         return dataFile.getOnDiskFilePointer();
     }
 
+    public long getEstimatedOnDiskBytesWritten()
+    {
+        return dataFile.getEstimatedOnDiskBytesWritten();
+    }
+
     /**
      * Encapsulates writing the index and filter for an SSTable. The state of this object is not valid until it has been closed.
      */
     class IndexWriter extends AbstractTransactional implements Transactional
     {
         private final SequentialWriter indexFile;
-        public final SegmentedFile.Builder builder;
+        public final FileHandle.Builder builder;
         public final IndexSummaryBuilder summary;
         public final IFilter bf;
         private DataPosition mark;
 
-        IndexWriter(long keyCount, final SequentialWriter dataFile)
+        IndexWriter(long keyCount)
         {
-            indexFile = SequentialWriter.open(new File(descriptor.filenameFor(Component.PRIMARY_INDEX)));
-            builder = SegmentedFile.getBuilder(DatabaseDescriptor.getIndexAccessMode(), false);
+            indexFile = new SequentialWriter(new File(descriptor.filenameFor(Component.PRIMARY_INDEX)), writerOption);
+            builder = new FileHandle.Builder(descriptor.filenameFor(Component.PRIMARY_INDEX)).mmapped(DatabaseDescriptor.getIndexAccessMode() == Config.DiskAccessMode.mmap);
+            chunkCache.ifPresent(builder::withChunkCache);
             summary = new IndexSummaryBuilder(keyCount, metadata.params.minIndexInterval, Downsampling.BASE_SAMPLING_LEVEL);
             bf = FilterFactory.getFilter(keyCount, metadata.params.bloomFilterFpChance, true, descriptor.version.hasOldBfHashOrder());
             // register listeners to be alerted when the data files are flushed
-            indexFile.setPostFlushListener(new Runnable()
-            {
-                public void run()
-                {
-                    summary.markIndexSynced(indexFile.getLastFlushOffset());
-                }
-            });
-            dataFile.setPostFlushListener(new Runnable()
-            {
-                public void run()
-                {
-                    summary.markDataSynced(dataFile.getLastFlushOffset());
-                }
-            });
+            indexFile.setPostFlushListener(() -> summary.markIndexSynced(indexFile.getLastFlushOffset()));
+            dataFile.setPostFlushListener(() -> summary.markDataSynced(dataFile.getLastFlushOffset()));
         }
 
         // finds the last (-offset) decorated key that can be guaranteed to occur fully in the flushed portion of the index file
@@ -399,14 +454,14 @@
             return summary.getLastReadableBoundary();
         }
 
-        public void append(DecoratedKey key, RowIndexEntry indexEntry, long dataEnd) throws IOException
+        public void append(DecoratedKey key, RowIndexEntry indexEntry, long dataEnd, ByteBuffer indexInfo) throws IOException
         {
             bf.add(key);
             long indexStart = indexFile.position();
             try
             {
                 ByteBufferUtil.writeWithShortLength(key.getKey(), indexFile);
-                rowIndexEntrySerializer.serialize(indexEntry, indexFile);
+                rowIndexEntrySerializer.serialize(indexEntry, indexFile, indexInfo);
             }
             catch (IOException e)
             {
@@ -461,15 +516,15 @@
             flushBf();
 
             // truncate index file
-            long position = iwriter.indexFile.position();
-            iwriter.indexFile.setDescriptor(descriptor).prepareToCommit();
-            FileUtils.truncate(iwriter.indexFile.getPath(), position);
+            long position = indexFile.position();
+            indexFile.prepareToCommit();
+            FileUtils.truncate(indexFile.getPath(), position);
 
             // save summary
             summary.prepareToCommit();
-            try (IndexSummary summary = iwriter.summary.build(getPartitioner()))
+            try (IndexSummary indexSummary = summary.build(getPartitioner()))
             {
-                SSTableReader.saveSummary(descriptor, first, last, iwriter.builder, dbuilder, summary);
+                SSTableReader.saveSummary(descriptor, first, last, indexSummary);
             }
         }
 
diff --git a/src/java/org/apache/cassandra/io/sstable/metadata/IMetadataSerializer.java b/src/java/org/apache/cassandra/io/sstable/metadata/IMetadataSerializer.java
index 100cfdb..a86327d 100644
--- a/src/java/org/apache/cassandra/io/sstable/metadata/IMetadataSerializer.java
+++ b/src/java/org/apache/cassandra/io/sstable/metadata/IMetadataSerializer.java
@@ -73,4 +73,9 @@
      * Mutate repairedAt time
      */
     void mutateRepairedAt(Descriptor descriptor, long newRepairedAt) throws IOException;
+
+    /**
+     * Replace the sstable metadata file ({@code -Statistics.db}) with the given components.
+     */
+    void rewriteSSTableMetadata(Descriptor descriptor, Map<MetadataType, MetadataComponent> currentComponents) throws IOException;
 }
diff --git a/src/java/org/apache/cassandra/io/sstable/metadata/LegacyMetadataSerializer.java b/src/java/org/apache/cassandra/io/sstable/metadata/LegacyMetadataSerializer.java
index ac10762..e8be898 100644
--- a/src/java/org/apache/cassandra/io/sstable/metadata/LegacyMetadataSerializer.java
+++ b/src/java/org/apache/cassandra/io/sstable/metadata/LegacyMetadataSerializer.java
@@ -21,11 +21,9 @@
 import java.nio.ByteBuffer;
 import java.util.*;
 
-import com.google.common.collect.Maps;
-
 import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.apache.cassandra.db.commitlog.IntervalSet;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.format.Version;
@@ -37,7 +35,7 @@
 import org.apache.cassandra.utils.StreamingHistogram;
 import org.apache.cassandra.utils.UUIDSerializer;
 
-import static org.apache.cassandra.io.sstable.metadata.StatsMetadata.replayPositionSetSerializer;
+import static org.apache.cassandra.io.sstable.metadata.StatsMetadata.commitLogPositionSetSerializer;
 
 /**
  * Serializer for SSTable from legacy versions
@@ -59,7 +57,7 @@
 
         EstimatedHistogram.serializer.serialize(stats.estimatedPartitionSize, out);
         EstimatedHistogram.serializer.serialize(stats.estimatedColumnCount, out);
-        ReplayPosition.serializer.serialize(stats.commitLogIntervals.upperBound().orElse(ReplayPosition.NONE), out);
+        CommitLogPosition.serializer.serialize(stats.commitLogIntervals.upperBound().orElse(CommitLogPosition.NONE), out);
         out.writeLong(stats.minTimestamp);
         out.writeLong(stats.maxTimestamp);
         out.writeInt(stats.maxLocalDeletionTime);
@@ -76,9 +74,9 @@
         for (ByteBuffer value : stats.maxClusteringValues)
             ByteBufferUtil.writeWithShortLength(value, out);
         if (version.hasCommitLogLowerBound())
-            ReplayPosition.serializer.serialize(stats.commitLogIntervals.lowerBound().orElse(ReplayPosition.NONE), out);
+            CommitLogPosition.serializer.serialize(stats.commitLogIntervals.lowerBound().orElse(CommitLogPosition.NONE), out);
         if (version.hasCommitLogIntervals())
-            replayPositionSetSerializer.serialize(stats.commitLogIntervals, out);
+            commitLogPositionSetSerializer.serialize(stats.commitLogIntervals, out);
         if (version.hasOriginatingHostId())
         {
             if (stats.originatingHostId != null)
@@ -99,7 +97,7 @@
     @Override
     public Map<MetadataType, MetadataComponent> deserialize(Descriptor descriptor, EnumSet<MetadataType> types) throws IOException
     {
-        Map<MetadataType, MetadataComponent> components = Maps.newHashMap();
+        Map<MetadataType, MetadataComponent> components = new EnumMap<>(MetadataType.class);
 
         File statsFile = new File(descriptor.filenameFor(Component.STATS));
         if (!statsFile.exists() && types.contains(MetadataType.STATS))
@@ -112,8 +110,8 @@
             {
                 EstimatedHistogram partitionSizes = EstimatedHistogram.serializer.deserialize(in);
                 EstimatedHistogram columnCounts = EstimatedHistogram.serializer.deserialize(in);
-                ReplayPosition commitLogLowerBound = ReplayPosition.NONE;
-                ReplayPosition commitLogUpperBound = ReplayPosition.serializer.deserialize(in);
+                CommitLogPosition commitLogLowerBound = CommitLogPosition.NONE;
+                CommitLogPosition commitLogUpperBound = CommitLogPosition.serializer.deserialize(in);
                 long minTimestamp = in.readLong();
                 long maxTimestamp = in.readLong();
                 int maxLocalDeletionTime = in.readInt();
@@ -138,10 +136,10 @@
                     maxColumnNames.add(ByteBufferUtil.readWithShortLength(in));
 
                 if (descriptor.version.hasCommitLogLowerBound())
-                    commitLogLowerBound = ReplayPosition.serializer.deserialize(in);
-                IntervalSet<ReplayPosition> commitLogIntervals;
+                    commitLogLowerBound = CommitLogPosition.serializer.deserialize(in);
+                IntervalSet<CommitLogPosition> commitLogIntervals;
                 if (descriptor.version.hasCommitLogIntervals())
-                    commitLogIntervals = replayPositionSetSerializer.deserialize(in);
+                    commitLogIntervals = commitLogPositionSetSerializer.deserialize(in);
                 else
                     commitLogIntervals = new IntervalSet<>(commitLogLowerBound, commitLogUpperBound);
 
diff --git a/src/java/org/apache/cassandra/io/sstable/metadata/MetadataCollector.java b/src/java/org/apache/cassandra/io/sstable/metadata/MetadataCollector.java
index 533c511..a269dfc 100644
--- a/src/java/org/apache/cassandra/io/sstable/metadata/MetadataCollector.java
+++ b/src/java/org/apache/cassandra/io/sstable/metadata/MetadataCollector.java
@@ -20,18 +20,18 @@
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
 import com.google.common.base.Preconditions;
-import com.google.common.collect.Maps;
 
 import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus;
 import com.clearspring.analytics.stream.cardinality.ICardinality;
 import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.apache.cassandra.db.commitlog.IntervalSet;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
 import org.apache.cassandra.db.partitions.PartitionStatisticsCollector;
 import org.apache.cassandra.db.rows.Cell;
 import org.apache.cassandra.io.sstable.SSTable;
@@ -90,7 +90,7 @@
     protected EstimatedHistogram estimatedPartitionSize = defaultPartitionSizeHistogram();
     // TODO: cound the number of row per partition (either with the number of cells, or instead)
     protected EstimatedHistogram estimatedCellPerPartitionCount = defaultCellPerPartitionCountHistogram();
-    protected IntervalSet commitLogIntervals = IntervalSet.empty();
+    protected IntervalSet<CommitLogPosition> commitLogIntervals = IntervalSet.empty();
     protected final MinMaxLongTracker timestampTracker = new MinMaxLongTracker();
     protected final MinMaxIntTracker localDeletionTimeTracker = new MinMaxIntTracker(Cell.NO_DELETION_TIME, Cell.NO_DELETION_TIME);
     protected final MinMaxIntTracker ttlTracker = new MinMaxIntTracker(Cell.NO_TTL, Cell.NO_TTL);
@@ -129,7 +129,7 @@
     {
         this(comparator);
 
-        IntervalSet.Builder<ReplayPosition> intervals = new IntervalSet.Builder<>();
+        IntervalSet.Builder<CommitLogPosition> intervals = new IntervalSet.Builder<>();
         if (originatingHostId != null)
         {
             for (SSTableReader sstable : sstables)
@@ -226,7 +226,7 @@
         ttlTracker.update(newTTL);
     }
 
-    public MetadataCollector commitLogIntervals(IntervalSet commitLogIntervals)
+    public MetadataCollector commitLogIntervals(IntervalSet<CommitLogPosition> commitLogIntervals)
     {
         this.commitLogIntervals = commitLogIntervals;
         return this;
@@ -256,7 +256,7 @@
                                  || comparator.compare(maxClustering, minClustering) >= 0);
         ByteBuffer[] minValues = minClustering != null ? minClustering.getRawValues() : EMPTY_CLUSTERING;
         ByteBuffer[] maxValues = maxClustering != null ? maxClustering.getRawValues() : EMPTY_CLUSTERING;
-        Map<MetadataType, MetadataComponent> components = Maps.newHashMap();
+        Map<MetadataType, MetadataComponent> components = new EnumMap<>(MetadataType.class);
         components.put(MetadataType.VALIDATION, new ValidationMetadata(partitioner, bloomFilterFPChance));
         components.put(MetadataType.STATS, new StatsMetadata(estimatedPartitionSize,
                                                              estimatedCellPerPartitionCount,
diff --git a/src/java/org/apache/cassandra/io/sstable/metadata/MetadataSerializer.java b/src/java/org/apache/cassandra/io/sstable/metadata/MetadataSerializer.java
index 635adcd..779fc07 100644
--- a/src/java/org/apache/cassandra/io/sstable/metadata/MetadataSerializer.java
+++ b/src/java/org/apache/cassandra/io/sstable/metadata/MetadataSerializer.java
@@ -21,7 +21,6 @@
 import java.util.*;
 
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -37,7 +36,7 @@
 import org.apache.cassandra.utils.FBUtilities;
 
 /**
- * Metadata serializer for SSTables version >= 'k'.
+ * Metadata serializer for SSTables {@code version >= 'k'}.
  *
  * <pre>
  * File format := | number of components (4 bytes) | toc | component1 | component2 | ... |
@@ -84,7 +83,7 @@
         if (!statsFile.exists())
         {
             logger.trace("No sstable stats for {}", descriptor);
-            components = Maps.newHashMap();
+            components = new EnumMap<>(MetadataType.class);
             components.put(MetadataType.STATS, MetadataCollector.defaultStatsMetadata());
         }
         else
@@ -104,11 +103,11 @@
 
     public Map<MetadataType, MetadataComponent> deserialize(Descriptor descriptor, FileDataInput in, EnumSet<MetadataType> types) throws IOException
     {
-        Map<MetadataType, MetadataComponent> components = Maps.newHashMap();
+        Map<MetadataType, MetadataComponent> components = new EnumMap<>(MetadataType.class);
         // read number of components
         int numComponents = in.readInt();
         // read toc
-        Map<MetadataType, Integer> toc = new HashMap<>(numComponents);
+        Map<MetadataType, Integer> toc = new EnumMap<>(MetadataType.class);
         MetadataType[] values = MetadataType.values();
         for (int i = 0; i < numComponents; i++)
         {
@@ -147,7 +146,7 @@
         rewriteSSTableMetadata(descriptor, currentComponents);
     }
 
-    private void rewriteSSTableMetadata(Descriptor descriptor, Map<MetadataType, MetadataComponent> currentComponents) throws IOException
+    public void rewriteSSTableMetadata(Descriptor descriptor, Map<MetadataType, MetadataComponent> currentComponents) throws IOException
     {
         String filePath = descriptor.tmpFilenameFor(Component.STATS);
         try (DataOutputStreamPlus out = new BufferedDataOutputStreamPlus(new FileOutputStream(filePath)))
@@ -156,7 +155,7 @@
             out.flush();
         }
         // we cant move a file on top of another file in windows:
-        if (FBUtilities.isWindows())
+        if (FBUtilities.isWindows)
             FileUtils.delete(descriptor.filenameFor(Component.STATS));
         FileUtils.renameWithConfirm(filePath, descriptor.filenameFor(Component.STATS));
 
diff --git a/src/java/org/apache/cassandra/io/sstable/metadata/StatsMetadata.java b/src/java/org/apache/cassandra/io/sstable/metadata/StatsMetadata.java
index 50df500..2196ecf 100644
--- a/src/java/org/apache/cassandra/io/sstable/metadata/StatsMetadata.java
+++ b/src/java/org/apache/cassandra/io/sstable/metadata/StatsMetadata.java
@@ -31,8 +31,8 @@
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.apache.cassandra.db.commitlog.IntervalSet;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.net.MessagingService;
@@ -47,11 +47,11 @@
 public class StatsMetadata extends MetadataComponent
 {
     public static final IMetadataComponentSerializer serializer = new StatsMetadataSerializer();
-    public static final ISerializer<IntervalSet<ReplayPosition>> replayPositionSetSerializer = IntervalSet.serializer(ReplayPosition.serializer);
+    public static final ISerializer<IntervalSet<CommitLogPosition>> commitLogPositionSetSerializer = IntervalSet.serializer(CommitLogPosition.serializer);
 
     public final EstimatedHistogram estimatedPartitionSize;
     public final EstimatedHistogram estimatedColumnCount;
-    public final IntervalSet<ReplayPosition> commitLogIntervals;
+    public final IntervalSet<CommitLogPosition> commitLogIntervals;
     public final long minTimestamp;
     public final long maxTimestamp;
     public final int minLocalDeletionTime;
@@ -71,7 +71,7 @@
 
     public StatsMetadata(EstimatedHistogram estimatedPartitionSize,
                          EstimatedHistogram estimatedColumnCount,
-                         IntervalSet<ReplayPosition> commitLogIntervals,
+                         IntervalSet<CommitLogPosition> commitLogIntervals,
                          long minTimestamp,
                          long maxTimestamp,
                          int minLocalDeletionTime,
@@ -250,7 +250,7 @@
             int size = 0;
             size += EstimatedHistogram.serializer.serializedSize(component.estimatedPartitionSize);
             size += EstimatedHistogram.serializer.serializedSize(component.estimatedColumnCount);
-            size += ReplayPosition.serializer.serializedSize(component.commitLogIntervals.upperBound().orElse(ReplayPosition.NONE));
+            size += CommitLogPosition.serializer.serializedSize(component.commitLogIntervals.upperBound().orElse(CommitLogPosition.NONE));
             if (version.storeRows())
                 size += 8 + 8 + 4 + 4 + 4 + 4 + 8 + 8; // mix/max timestamp(long), min/maxLocalDeletionTime(int), min/max TTL, compressionRatio(double), repairedAt (long)
             else
@@ -269,9 +269,9 @@
             if (version.storeRows())
                 size += 8 + 8; // totalColumnsSet, totalRows
             if (version.hasCommitLogLowerBound())
-                size += ReplayPosition.serializer.serializedSize(component.commitLogIntervals.lowerBound().orElse(ReplayPosition.NONE));
+                size += CommitLogPosition.serializer.serializedSize(component.commitLogIntervals.lowerBound().orElse(CommitLogPosition.NONE));
             if (version.hasCommitLogIntervals())
-                size += replayPositionSetSerializer.serializedSize(component.commitLogIntervals);
+                size += commitLogPositionSetSerializer.serializedSize(component.commitLogIntervals);
             if (version.hasOriginatingHostId())
             {
                 size += 1; // boolean: is originatingHostId present
@@ -285,7 +285,7 @@
         {
             EstimatedHistogram.serializer.serialize(component.estimatedPartitionSize, out);
             EstimatedHistogram.serializer.serialize(component.estimatedColumnCount, out);
-            ReplayPosition.serializer.serialize(component.commitLogIntervals.upperBound().orElse(ReplayPosition.NONE), out);
+            CommitLogPosition.serializer.serialize(component.commitLogIntervals.upperBound().orElse(CommitLogPosition.NONE), out);
             out.writeLong(component.minTimestamp);
             out.writeLong(component.maxTimestamp);
             if (version.storeRows())
@@ -315,9 +315,9 @@
             }
 
             if (version.hasCommitLogLowerBound())
-                ReplayPosition.serializer.serialize(component.commitLogIntervals.lowerBound().orElse(ReplayPosition.NONE), out);
+                CommitLogPosition.serializer.serialize(component.commitLogIntervals.lowerBound().orElse(CommitLogPosition.NONE), out);
             if (version.hasCommitLogIntervals())
-                replayPositionSetSerializer.serialize(component.commitLogIntervals, out);
+                commitLogPositionSetSerializer.serialize(component.commitLogIntervals, out);
             if (version.hasOriginatingHostId())
             {
                 if (component.originatingHostId != null)
@@ -355,8 +355,8 @@
                 columnCounts.clearOverflow();
             }
 
-            ReplayPosition commitLogLowerBound = ReplayPosition.NONE, commitLogUpperBound;
-            commitLogUpperBound = ReplayPosition.serializer.deserialize(in);
+            CommitLogPosition commitLogLowerBound = CommitLogPosition.NONE, commitLogUpperBound;
+            commitLogUpperBound = CommitLogPosition.serializer.deserialize(in);
 
             long minTimestamp = in.readLong();
             long maxTimestamp = in.readLong();
@@ -400,12 +400,12 @@
             long totalRows = version.storeRows() ? in.readLong() : -1L;
 
             if (version.hasCommitLogLowerBound())
-                commitLogLowerBound = ReplayPosition.serializer.deserialize(in);
-            IntervalSet<ReplayPosition> commitLogIntervals;
+                commitLogLowerBound = CommitLogPosition.serializer.deserialize(in);
+            IntervalSet<CommitLogPosition> commitLogIntervals;
             if (version.hasCommitLogIntervals())
-                commitLogIntervals = replayPositionSetSerializer.deserialize(in);
+                commitLogIntervals = commitLogPositionSetSerializer.deserialize(in);
             else
-                commitLogIntervals = new IntervalSet<ReplayPosition>(commitLogLowerBound, commitLogUpperBound);
+                commitLogIntervals = new IntervalSet<CommitLogPosition>(commitLogLowerBound, commitLogUpperBound);
 
             UUID originatingHostId = null;
             if (version.hasOriginatingHostId() && in.readByte() != 0)
diff --git a/src/java/org/apache/cassandra/io/util/AbstractReaderFileProxy.java b/src/java/org/apache/cassandra/io/util/AbstractReaderFileProxy.java
new file mode 100644
index 0000000..7962c0f
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/AbstractReaderFileProxy.java
@@ -0,0 +1,61 @@
+/*
+ * 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.io.util;
+
+public abstract class AbstractReaderFileProxy implements ReaderFileProxy
+{
+    protected final ChannelProxy channel;
+    protected final long fileLength;
+
+    protected AbstractReaderFileProxy(ChannelProxy channel, long fileLength)
+    {
+        this.channel = channel;
+        this.fileLength = fileLength >= 0 ? fileLength : channel.size();
+    }
+
+    @Override
+    public ChannelProxy channel()
+    {
+        return channel;
+    }
+
+    @Override
+    public long fileLength()
+    {
+        return fileLength;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getClass().getSimpleName() + "(filePath='" + channel + "')";
+    }
+
+    @Override
+    public void close()
+    {
+        // nothing in base class
+    }
+
+    @Override
+    public double getCrcCheckChance()
+    {
+        return 0; // Only valid for compressed files.
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/io/util/BufferManagingRebufferer.java b/src/java/org/apache/cassandra/io/util/BufferManagingRebufferer.java
new file mode 100644
index 0000000..1648bcf
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/BufferManagingRebufferer.java
@@ -0,0 +1,141 @@
+/*
+ *
+ * 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.io.util;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import org.apache.cassandra.utils.memory.BufferPool;
+
+/**
+ * Buffer manager used for reading from a ChunkReader when cache is not in use. Instances of this class are
+ * reader-specific and thus do not need to be thread-safe since the reader itself isn't.
+ *
+ * The instances reuse themselves as the BufferHolder to avoid having to return a new object for each rebuffer call.
+ */
+public abstract class BufferManagingRebufferer implements Rebufferer, Rebufferer.BufferHolder
+{
+    protected final ChunkReader source;
+    protected final ByteBuffer buffer;
+    protected long offset = 0;
+
+    abstract long alignedPosition(long position);
+
+    protected BufferManagingRebufferer(ChunkReader wrapped)
+    {
+        this.source = wrapped;
+        buffer = BufferPool.get(wrapped.chunkSize(), wrapped.preferredBufferType()).order(ByteOrder.BIG_ENDIAN);
+        buffer.limit(0);
+    }
+
+    @Override
+    public void closeReader()
+    {
+        BufferPool.put(buffer);
+        offset = -1;
+    }
+
+    @Override
+    public void close()
+    {
+        assert offset == -1;    // reader must be closed at this point.
+        source.close();
+    }
+
+    @Override
+    public ChannelProxy channel()
+    {
+        return source.channel();
+    }
+
+    @Override
+    public long fileLength()
+    {
+        return source.fileLength();
+    }
+
+    @Override
+    public BufferHolder rebuffer(long position)
+    {
+        offset = alignedPosition(position);
+        source.readChunk(offset, buffer);
+        return this;
+    }
+
+    @Override
+    public double getCrcCheckChance()
+    {
+        return source.getCrcCheckChance();
+    }
+
+    @Override
+    public String toString()
+    {
+        return "BufferManagingRebufferer." + getClass().getSimpleName() + ":" + source.toString();
+    }
+
+    // BufferHolder methods
+
+    public ByteBuffer buffer()
+    {
+        return buffer;
+    }
+
+    public long offset()
+    {
+        return offset;
+    }
+
+    @Override
+    public void release()
+    {
+        // nothing to do, we don't delete buffers before we're closed.
+    }
+
+    public static class Unaligned extends BufferManagingRebufferer
+    {
+        public Unaligned(ChunkReader wrapped)
+        {
+            super(wrapped);
+        }
+
+        @Override
+        long alignedPosition(long position)
+        {
+            return position;
+        }
+    }
+
+    public static class Aligned extends BufferManagingRebufferer
+    {
+        public Aligned(ChunkReader wrapped)
+        {
+            super(wrapped);
+            assert Integer.bitCount(wrapped.chunkSize()) == 1;
+        }
+
+        @Override
+        long alignedPosition(long position)
+        {
+            return position & -buffer.capacity();
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/util/BufferedSegmentedFile.java b/src/java/org/apache/cassandra/io/util/BufferedSegmentedFile.java
deleted file mode 100644
index 090c5bd..0000000
--- a/src/java/org/apache/cassandra/io/util/BufferedSegmentedFile.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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.io.util;
-
-public class BufferedSegmentedFile extends SegmentedFile
-{
-    public BufferedSegmentedFile(ChannelProxy channel, int bufferSize, long length)
-    {
-        super(new Cleanup(channel), channel, bufferSize, length);
-    }
-
-    private BufferedSegmentedFile(BufferedSegmentedFile copy)
-    {
-        super(copy);
-    }
-
-    public static class Builder extends SegmentedFile.Builder
-    {
-        public SegmentedFile complete(ChannelProxy channel, int bufferSize, long overrideLength)
-        {
-            long length = overrideLength > 0 ? overrideLength : channel.size();
-            return new BufferedSegmentedFile(channel, bufferSize, length);
-        }
-    }
-
-    public BufferedSegmentedFile sharedCopy()
-    {
-        return new BufferedSegmentedFile(this);
-    }
-}
diff --git a/src/java/org/apache/cassandra/io/util/ChannelProxy.java b/src/java/org/apache/cassandra/io/util/ChannelProxy.java
index 1463fdd..91bb03b 100644
--- a/src/java/org/apache/cassandra/io/util/ChannelProxy.java
+++ b/src/java/org/apache/cassandra/io/util/ChannelProxy.java
@@ -125,6 +125,7 @@
     {
         try
         {
+            // FIXME: consider wrapping in a while loop
             return channel.read(buffer, position);
         }
         catch (IOException e)
diff --git a/src/java/org/apache/cassandra/io/util/ChecksumWriter.java b/src/java/org/apache/cassandra/io/util/ChecksumWriter.java
new file mode 100644
index 0000000..50aaccb
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/ChecksumWriter.java
@@ -0,0 +1,104 @@
+/*
+ * 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.io.util;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.CRC32;
+
+import javax.annotation.Nonnull;
+
+import org.apache.cassandra.io.FSWriteError;
+
+public class ChecksumWriter
+{
+    private final CRC32 incrementalChecksum = new CRC32();
+    private final DataOutput incrementalOut;
+    private final CRC32 fullChecksum = new CRC32();
+
+    public ChecksumWriter(DataOutput incrementalOut)
+    {
+        this.incrementalOut = incrementalOut;
+    }
+
+    public void writeChunkSize(int length)
+    {
+        try
+        {
+            incrementalOut.writeInt(length);
+        }
+        catch (IOException e)
+        {
+            throw new IOError(e);
+        }
+    }
+
+    // checksumIncrementalResult indicates if the checksum we compute for this buffer should itself be
+    // included in the full checksum, translating to if the partial checksum is serialized along with the
+    // data it checksums (in which case the file checksum as calculated by external tools would mismatch if
+    // we did not include it), or independently.
+
+    // CompressedSequentialWriters serialize the partial checksums inline with the compressed data chunks they
+    // corroborate, whereas ChecksummedSequentialWriters serialize them to a different file.
+    public void appendDirect(ByteBuffer bb, boolean checksumIncrementalResult)
+    {
+        try
+        {
+            ByteBuffer toAppend = bb.duplicate();
+            toAppend.mark();
+            incrementalChecksum.update(toAppend);
+            toAppend.reset();
+
+            int incrementalChecksumValue = (int) incrementalChecksum.getValue();
+            incrementalOut.writeInt(incrementalChecksumValue);
+
+            fullChecksum.update(toAppend);
+            if (checksumIncrementalResult)
+            {
+                ByteBuffer byteBuffer = ByteBuffer.allocate(4);
+                byteBuffer.putInt(incrementalChecksumValue);
+                assert byteBuffer.arrayOffset() == 0;
+                fullChecksum.update(byteBuffer.array(), 0, byteBuffer.array().length);
+            }
+            incrementalChecksum.reset();
+
+        }
+        catch (IOException e)
+        {
+            throw new IOError(e);
+        }
+    }
+
+    public void writeFullChecksum(@Nonnull File digestFile)
+    {
+        try (FileOutputStream fos = new FileOutputStream(digestFile);
+             DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fos)))
+        {
+            out.write(String.valueOf(fullChecksum.getValue()).getBytes(StandardCharsets.UTF_8));
+            out.flush();
+            fos.getFD().sync();
+        }
+        catch (IOException e)
+        {
+            throw new FSWriteError(e, digestFile);
+        }
+    }
+}
+
diff --git a/src/java/org/apache/cassandra/io/util/ChecksummedRandomAccessReader.java b/src/java/org/apache/cassandra/io/util/ChecksummedRandomAccessReader.java
index 30f1e0c..2e59e3b 100644
--- a/src/java/org/apache/cassandra/io/util/ChecksummedRandomAccessReader.java
+++ b/src/java/org/apache/cassandra/io/util/ChecksummedRandomAccessReader.java
@@ -19,109 +19,27 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.util.zip.CRC32;
 
-import org.apache.cassandra.io.compress.BufferType;
-import org.apache.cassandra.utils.ByteBufferUtil;
-import org.apache.cassandra.utils.Throwables;
+import org.apache.cassandra.utils.ChecksumType;
 
-public class ChecksummedRandomAccessReader extends RandomAccessReader
+public final class ChecksummedRandomAccessReader
 {
-    @SuppressWarnings("serial")
-    public static class CorruptFileException extends RuntimeException
+    @SuppressWarnings("resource") // The Rebufferer owns both the channel and the validator and handles closing both.
+    public static RandomAccessReader open(File file, File crcFile) throws IOException
     {
-        public final String filePath;
-
-        public CorruptFileException(Exception cause, String filePath)
-        {
-            super(cause);
-            this.filePath = filePath;
-        }
-    }
-
-    private final DataIntegrityMetadata.ChecksumValidator validator;
-
-    private ChecksummedRandomAccessReader(Builder builder)
-    {
-        super(builder);
-        this.validator = builder.validator;
-    }
-
-    @SuppressWarnings("resource")
-    @Override
-    protected void reBufferStandard()
-    {
-        long desiredPosition = current();
-        // align with buffer size, as checksums were computed in chunks of buffer size each.
-        bufferOffset = (desiredPosition / buffer.capacity()) * buffer.capacity();
-
-        buffer.clear();
-
-        long position = bufferOffset;
-        while (buffer.hasRemaining())
-        {
-            int n = channel.read(buffer, position);
-            if (n < 0)
-                break;
-            position += n;
-        }
-
-        buffer.flip();
-
+        ChannelProxy channel = new ChannelProxy(file);
         try
         {
-            validator.validate(ByteBufferUtil.getArray(buffer), 0, buffer.remaining());
+            DataIntegrityMetadata.ChecksumValidator validator = new DataIntegrityMetadata.ChecksumValidator(ChecksumType.CRC32,
+                                                                                                            RandomAccessReader.open(crcFile),
+                                                                                                            file.getPath());
+            Rebufferer rebufferer = new ChecksummedRebufferer(channel, validator);
+            return new RandomAccessReader.RandomAccessReaderWithOwnChannel(rebufferer);
         }
-        catch (IOException e)
+        catch (Throwable t)
         {
-            throw new CorruptFileException(e, channel.filePath());
-        }
-
-        buffer.position((int) (desiredPosition - bufferOffset));
-    }
-
-    @Override
-    protected void reBufferMmap()
-    {
-        throw new AssertionError("Unsupported operation");
-    }
-
-    @Override
-    public void seek(long newPosition)
-    {
-        validator.seek(newPosition);
-        super.seek(newPosition);
-    }
-
-    @Override
-    public void close()
-    {
-        Throwables.perform(channel.filePath(), Throwables.FileOpType.READ,
-                           super::close,
-                           validator::close,
-                           channel::close);
-    }
-
-    public static final class Builder extends RandomAccessReader.Builder
-    {
-        private final DataIntegrityMetadata.ChecksumValidator validator;
-
-        @SuppressWarnings("resource")
-        public Builder(File file, File crcFile) throws IOException
-        {
-            super(new ChannelProxy(file));
-            this.validator = new DataIntegrityMetadata.ChecksumValidator(new CRC32(),
-                                                                         RandomAccessReader.open(crcFile),
-                                                                         file.getPath());
-
-            super.bufferSize(validator.chunkSize)
-                 .bufferType(BufferType.ON_HEAP);
-        }
-
-        @Override
-        public RandomAccessReader build()
-        {
-            return new ChecksummedRandomAccessReader(this);
+            channel.close();
+            throw t;
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/io/util/ChecksummedRebufferer.java b/src/java/org/apache/cassandra/io/util/ChecksummedRebufferer.java
new file mode 100644
index 0000000..99091d9
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/ChecksummedRebufferer.java
@@ -0,0 +1,77 @@
+/*
+ * 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.io.util;
+
+import java.io.IOException;
+
+import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+class ChecksummedRebufferer extends BufferManagingRebufferer
+{
+    private final DataIntegrityMetadata.ChecksumValidator validator;
+
+    @SuppressWarnings("resource") // chunk reader is closed by super::close()
+    ChecksummedRebufferer(ChannelProxy channel, DataIntegrityMetadata.ChecksumValidator validator)
+    {
+        super(new SimpleChunkReader(channel, channel.size(), BufferType.ON_HEAP, validator.chunkSize));
+        this.validator = validator;
+    }
+
+    @Override
+    public BufferHolder rebuffer(long desiredPosition)
+    {
+        if (desiredPosition != offset + buffer.position())
+            validator.seek(desiredPosition);
+
+        // align with buffer size, as checksums were computed in chunks of buffer size each.
+        offset = alignedPosition(desiredPosition);
+        source.readChunk(offset, buffer);
+
+        try
+        {
+            validator.validate(ByteBufferUtil.getArray(buffer), 0, buffer.remaining());
+        }
+        catch (IOException e)
+        {
+            throw new CorruptFileException(e, channel().filePath());
+        }
+
+        return this;
+    }
+
+    @Override
+    public void close()
+    {
+        try
+        {
+            source.close();
+        }
+        finally
+        {
+            validator.close();
+        }
+    }
+
+    @Override
+    long alignedPosition(long desiredPosition)
+    {
+        return (desiredPosition / buffer.capacity()) * buffer.capacity();
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/util/ChecksummedSequentialWriter.java b/src/java/org/apache/cassandra/io/util/ChecksummedSequentialWriter.java
index fd88151..f89e7cc 100644
--- a/src/java/org/apache/cassandra/io/util/ChecksummedSequentialWriter.java
+++ b/src/java/org/apache/cassandra/io/util/ChecksummedSequentialWriter.java
@@ -19,20 +19,25 @@
 
 import java.io.File;
 import java.nio.ByteBuffer;
-
-import org.apache.cassandra.io.compress.BufferType;
+import java.util.Optional;
 
 public class ChecksummedSequentialWriter extends SequentialWriter
 {
-    private final SequentialWriter crcWriter;
-    private final DataIntegrityMetadata.ChecksumWriter crcMetadata;
+    private static final SequentialWriterOption CRC_WRITER_OPTION = SequentialWriterOption.newBuilder()
+                                                                                          .bufferSize(8 * 1024)
+                                                                                          .build();
 
-    public ChecksummedSequentialWriter(File file, int bufferSize, File crcPath)
+    private final SequentialWriter crcWriter;
+    private final ChecksumWriter crcMetadata;
+    private final Optional<File> digestFile;
+
+    public ChecksummedSequentialWriter(File file, File crcPath, File digestFile, SequentialWriterOption option)
     {
-        super(file, bufferSize, BufferType.ON_HEAP);
-        crcWriter = new SequentialWriter(crcPath, 8 * 1024, BufferType.ON_HEAP);
-        crcMetadata = new DataIntegrityMetadata.ChecksumWriter(crcWriter);
+        super(file, option);
+        crcWriter = new SequentialWriter(crcPath, CRC_WRITER_OPTION);
+        crcMetadata = new ChecksumWriter(crcWriter);
         crcMetadata.writeChunkSize(buffer.capacity());
+        this.digestFile = Optional.ofNullable(digestFile);
     }
 
     @Override
@@ -63,9 +68,8 @@
         protected void doPrepare()
         {
             syncInternal();
-            if (descriptor != null)
-                crcMetadata.writeFullChecksum(descriptor);
-            crcWriter.setDescriptor(descriptor).prepareToCommit();
+            digestFile.ifPresent(crcMetadata::writeFullChecksum);
+            crcWriter.prepareToCommit();
         }
     }
 
diff --git a/src/java/org/apache/cassandra/io/util/ChunkReader.java b/src/java/org/apache/cassandra/io/util/ChunkReader.java
new file mode 100644
index 0000000..1d3439e
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/ChunkReader.java
@@ -0,0 +1,51 @@
+/*
+ * 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.io.util;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.io.compress.BufferType;
+
+/**
+ * RandomFileReader component that reads data from a file into a provided buffer and may have requirements over the
+ * size and alignment of reads.
+ * A caching or buffer-managing rebufferer will reference one of these to do the actual reading.
+ * Note: Implementations of this interface must be thread-safe!
+ */
+public interface ChunkReader extends RebuffererFactory
+{
+    /**
+     * Read the chunk at the given position, attempting to fill the capacity of the given buffer.
+     * The filled buffer must be positioned at 0, with limit set at the size of the available data.
+     * The source may have requirements for the positioning and/or size of the buffer (e.g. chunk-aligned and
+     * chunk-sized). These must be satisfied by the caller. 
+     */
+    void readChunk(long position, ByteBuffer buffer);
+
+    /**
+     * Buffer size required for this rebufferer. Must be power of 2 if alignment is required.
+     */
+    int chunkSize();
+
+    /**
+     * Specifies type of buffer the caller should attempt to give.
+     * This is not guaranteed to be fulfilled.
+     */
+    BufferType preferredBufferType();
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/io/util/CompressedChunkReader.java b/src/java/org/apache/cassandra/io/util/CompressedChunkReader.java
new file mode 100644
index 0000000..7250198
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/CompressedChunkReader.java
@@ -0,0 +1,217 @@
+/*
+ * 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.io.util;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.ThreadLocalRandom;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.primitives.Ints;
+
+import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.io.compress.CompressionMetadata;
+import org.apache.cassandra.io.compress.CorruptBlockException;
+import org.apache.cassandra.io.sstable.CorruptSSTableException;
+
+public abstract class CompressedChunkReader extends AbstractReaderFileProxy implements ChunkReader
+{
+    final CompressionMetadata metadata;
+
+    protected CompressedChunkReader(ChannelProxy channel, CompressionMetadata metadata)
+    {
+        super(channel, metadata.dataLength);
+        this.metadata = metadata;
+        assert Integer.bitCount(metadata.chunkLength()) == 1; //must be a power of two
+    }
+
+    @VisibleForTesting
+    public double getCrcCheckChance()
+    {
+        return metadata.parameters.getCrcCheckChance();
+    }
+
+    protected final boolean shouldCheckCrc()
+    {
+        return getCrcCheckChance() >= 1d || getCrcCheckChance() > ThreadLocalRandom.current().nextDouble();
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.format("CompressedChunkReader.%s(%s - %s, chunk length %d, data length %d)",
+                             getClass().getSimpleName(),
+                             channel.filePath(),
+                             metadata.compressor().getClass().getSimpleName(),
+                             metadata.chunkLength(),
+                             metadata.dataLength);
+    }
+
+    @Override
+    public int chunkSize()
+    {
+        return metadata.chunkLength();
+    }
+
+    @Override
+    public BufferType preferredBufferType()
+    {
+        return metadata.compressor().preferredBufferType();
+    }
+
+    @Override
+    public Rebufferer instantiateRebufferer()
+    {
+        return new BufferManagingRebufferer.Aligned(this);
+    }
+
+    public static class Standard extends CompressedChunkReader
+    {
+        // we read the raw compressed bytes into this buffer, then uncompressed them into the provided one.
+        ThreadLocalByteBufferHolder bufferHolder;
+
+        public Standard(ChannelProxy channel, CompressionMetadata metadata)
+        {
+            super(channel, metadata);
+            bufferHolder = new ThreadLocalByteBufferHolder(metadata.compressor().preferredBufferType());
+        }
+
+        @Override
+        public void readChunk(long position, ByteBuffer uncompressed)
+        {
+            try
+            {
+                // accesses must always be aligned
+                assert (position & -uncompressed.capacity()) == position;
+                assert position <= fileLength;
+
+                CompressionMetadata.Chunk chunk = metadata.chunkFor(position);
+
+                boolean shouldCheckCrc = shouldCheckCrc();
+
+                int length = shouldCheckCrc ? chunk.length + Integer.BYTES : chunk.length;
+                ByteBuffer compressed = bufferHolder.getBuffer(length);
+
+                if (channel.read(compressed, chunk.offset) != length)
+                    throw new CorruptBlockException(channel.filePath(), chunk);
+
+                compressed.flip();
+                uncompressed.clear();
+
+                compressed.position(0).limit(chunk.length);
+
+                if (shouldCheckCrc)
+                {
+                    int checksum = (int) metadata.checksumType.of(compressed);
+
+                    compressed.limit(length);
+                    if (compressed.getInt() != checksum)
+                        throw new CorruptBlockException(channel.filePath(), chunk);
+
+                    compressed.position(0).limit(chunk.length);
+                }
+
+                try
+                {
+                    metadata.compressor().uncompress(compressed, uncompressed);
+                }
+                catch (IOException e)
+                {
+                    throw new CorruptBlockException(channel.filePath(), chunk, e);
+                }
+                finally
+                {
+                    uncompressed.flip();
+                }
+            }
+            catch (CorruptBlockException e)
+            {
+                throw new CorruptSSTableException(e, channel.filePath());
+            }
+        }
+    }
+
+    public static class Mmap extends CompressedChunkReader
+    {
+        protected final MmappedRegions regions;
+
+        public Mmap(ChannelProxy channel, CompressionMetadata metadata, MmappedRegions regions)
+        {
+            super(channel, metadata);
+            this.regions = regions;
+        }
+
+        @Override
+        public void readChunk(long position, ByteBuffer uncompressed)
+        {
+            try
+            {
+                // accesses must always be aligned
+                assert (position & -uncompressed.capacity()) == position;
+                assert position <= fileLength;
+
+                CompressionMetadata.Chunk chunk = metadata.chunkFor(position);
+
+                MmappedRegions.Region region = regions.floor(chunk.offset);
+                long segmentOffset = region.offset();
+                int chunkOffset = Ints.checkedCast(chunk.offset - segmentOffset);
+                ByteBuffer compressedChunk = region.buffer();
+
+                compressedChunk.position(chunkOffset).limit(chunkOffset + chunk.length);
+
+                uncompressed.clear();
+
+                if (shouldCheckCrc())
+                {
+                    int checksum = (int) metadata.checksumType.of(compressedChunk);
+
+                    compressedChunk.limit(compressedChunk.capacity());
+                    if (compressedChunk.getInt() != checksum)
+                        throw new CorruptBlockException(channel.filePath(), chunk);
+
+                    compressedChunk.position(chunkOffset).limit(chunkOffset + chunk.length);
+                }
+
+                try
+                {
+                    metadata.compressor().uncompress(compressedChunk, uncompressed);
+                }
+                catch (IOException e)
+                {
+                    throw new CorruptBlockException(channel.filePath(), chunk, e);
+                }
+                finally
+                {
+                    uncompressed.flip();
+                }
+            }
+            catch (CorruptBlockException e)
+            {
+                throw new CorruptSSTableException(e, channel.filePath());
+            }
+
+        }
+
+        public void close()
+        {
+            regions.closeQuietly();
+            super.close();
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/util/CompressedSegmentedFile.java b/src/java/org/apache/cassandra/io/util/CompressedSegmentedFile.java
deleted file mode 100644
index 16f791a..0000000
--- a/src/java/org/apache/cassandra/io/util/CompressedSegmentedFile.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * 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.io.util;
-
-import com.google.common.util.concurrent.RateLimiter;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.cassandra.config.Config;
-import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.io.compress.CompressedRandomAccessReader;
-import org.apache.cassandra.io.compress.CompressedSequentialWriter;
-import org.apache.cassandra.io.compress.CompressionMetadata;
-import org.apache.cassandra.utils.JVMStabilityInspector;
-import org.apache.cassandra.utils.concurrent.Ref;
-
-public class CompressedSegmentedFile extends SegmentedFile implements ICompressedFile
-{
-    private static final Logger logger = LoggerFactory.getLogger(CompressedSegmentedFile.class);
-    private static final boolean useMmap = DatabaseDescriptor.getDiskAccessMode() == Config.DiskAccessMode.mmap;
-
-    public final CompressionMetadata metadata;
-    private final MmappedRegions regions;
-
-    public CompressedSegmentedFile(ChannelProxy channel, int bufferSize, CompressionMetadata metadata)
-    {
-        this(channel,
-             bufferSize,
-             metadata,
-             useMmap
-             ? MmappedRegions.map(channel, metadata)
-             : null);
-    }
-
-    public CompressedSegmentedFile(ChannelProxy channel, int bufferSize, CompressionMetadata metadata, MmappedRegions regions)
-    {
-        super(new Cleanup(channel, metadata, regions), channel, bufferSize, metadata.dataLength, metadata.compressedFileLength);
-        this.metadata = metadata;
-        this.regions = regions;
-    }
-
-    private CompressedSegmentedFile(CompressedSegmentedFile copy)
-    {
-        super(copy);
-        this.metadata = copy.metadata;
-        this.regions = copy.regions;
-    }
-
-    public ChannelProxy channel()
-    {
-        return channel;
-    }
-
-    public MmappedRegions regions()
-    {
-        return regions;
-    }
-
-    private static final class Cleanup extends SegmentedFile.Cleanup
-    {
-        final CompressionMetadata metadata;
-        private final MmappedRegions regions;
-
-        protected Cleanup(ChannelProxy channel, CompressionMetadata metadata, MmappedRegions regions)
-        {
-            super(channel);
-            this.metadata = metadata;
-            this.regions = regions;
-        }
-        public void tidy()
-        {
-            Throwable err = regions == null ? null : regions.close(null);
-            if (err != null)
-            {
-                JVMStabilityInspector.inspectThrowable(err);
-
-                // This is not supposed to happen
-                logger.error("Error while closing mmapped regions", err);
-            }
-
-            metadata.close();
-
-            super.tidy();
-        }
-    }
-
-    public CompressedSegmentedFile sharedCopy()
-    {
-        return new CompressedSegmentedFile(this);
-    }
-
-    public void addTo(Ref.IdentityCollection identities)
-    {
-        super.addTo(identities);
-        metadata.addTo(identities);
-    }
-
-    public static class Builder extends SegmentedFile.Builder
-    {
-        final CompressedSequentialWriter writer;
-        public Builder(CompressedSequentialWriter writer)
-        {
-            this.writer = writer;
-        }
-
-        protected CompressionMetadata metadata(String path, long overrideLength)
-        {
-            if (writer == null)
-                return CompressionMetadata.create(path);
-
-            return writer.open(overrideLength);
-        }
-
-        public SegmentedFile complete(ChannelProxy channel, int bufferSize, long overrideLength)
-        {
-            return new CompressedSegmentedFile(channel, bufferSize, metadata(channel.filePath(), overrideLength));
-        }
-    }
-
-    public void dropPageCache(long before)
-    {
-        if (before >= metadata.dataLength)
-            super.dropPageCache(0);
-        super.dropPageCache(metadata.chunkFor(before).offset);
-    }
-
-    public RandomAccessReader createReader()
-    {
-        return new CompressedRandomAccessReader.Builder(this).build();
-    }
-
-    public RandomAccessReader createReader(RateLimiter limiter)
-    {
-        return new CompressedRandomAccessReader.Builder(this).limiter(limiter).build();
-    }
-
-    public CompressionMetadata getMetadata()
-    {
-        return metadata;
-    }
-}
diff --git a/src/java/org/apache/cassandra/io/util/CorruptFileException.java b/src/java/org/apache/cassandra/io/util/CorruptFileException.java
new file mode 100644
index 0000000..875d06f
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/CorruptFileException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.io.util;
+
+@SuppressWarnings("serial")
+public class CorruptFileException extends RuntimeException
+{
+    public final String filePath;
+
+    public CorruptFileException(Exception cause, String filePath)
+    {
+        super(cause);
+        this.filePath = filePath;
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java b/src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java
index 79dfe2f..9afd0c0 100644
--- a/src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java
+++ b/src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java
@@ -17,23 +17,15 @@
  */
 package org.apache.cassandra.io.util;
 
-import java.io.BufferedOutputStream;
 import java.io.Closeable;
-import java.io.DataOutput;
-import java.io.DataOutputStream;
 import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOError;
 import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.zip.CRC32;
 import java.util.zip.CheckedInputStream;
 import java.util.zip.Checksum;
 
-import org.apache.cassandra.io.FSWriteError;
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.utils.ChecksumType;
 import org.apache.cassandra.utils.Throwables;
 
 public class DataIntegrityMetadata
@@ -45,21 +37,21 @@
 
     public static class ChecksumValidator implements Closeable
     {
-        private final Checksum checksum;
+        private final ChecksumType checksumType;
         private final RandomAccessReader reader;
         public final int chunkSize;
         private final String dataFilename;
 
         public ChecksumValidator(Descriptor descriptor) throws IOException
         {
-            this(descriptor.version.uncompressedChecksumType().newInstance(),
+            this(descriptor.version.uncompressedChecksumType(),
                  RandomAccessReader.open(new File(descriptor.filenameFor(Component.CRC))),
                  descriptor.filenameFor(Component.DATA));
         }
 
-        public ChecksumValidator(Checksum checksum, RandomAccessReader reader, String dataFilename) throws IOException
+        public ChecksumValidator(ChecksumType checksumType, RandomAccessReader reader, String dataFilename) throws IOException
         {
-            this.checksum = checksum;
+            this.checksumType = checksumType;
             this.reader = reader;
             this.dataFilename = dataFilename;
             chunkSize = reader.readInt();
@@ -79,9 +71,7 @@
 
         public void validate(byte[] bytes, int start, int end) throws IOException
         {
-            checksum.update(bytes, start, end);
-            int current = (int) checksum.getValue();
-            checksum.reset();
+            int current = (int) checksumType.of(bytes, start, end);
             int actual = reader.readInt();
             if (current != actual)
                 throw new IOException("Corrupted File : " + dataFilename);
@@ -132,7 +122,8 @@
 
             while( checkedInputStream.read(chunk) > 0 ) { }
             long calculatedDigestValue = checkedInputStream.getChecksum().getValue();
-            if (storedDigestValue != calculatedDigestValue) {
+            if (storedDigestValue != calculatedDigestValue)
+            {
                 throw new IOException("Corrupted SSTable : " + descriptor.filenameFor(Component.DATA));
             }
         }
@@ -143,83 +134,4 @@
                                dataReader::close);
         }
     }
-
-
-    public static class ChecksumWriter
-    {
-        private final CRC32 incrementalChecksum = new CRC32();
-        private final DataOutput incrementalOut;
-        private final CRC32 fullChecksum = new CRC32();
-
-        public ChecksumWriter(DataOutput incrementalOut)
-        {
-            this.incrementalOut = incrementalOut;
-        }
-
-        public void writeChunkSize(int length)
-        {
-            try
-            {
-                incrementalOut.writeInt(length);
-            }
-            catch (IOException e)
-            {
-                throw new IOError(e);
-            }
-        }
-
-        // checksumIncrementalResult indicates if the checksum we compute for this buffer should itself be
-        // included in the full checksum, translating to if the partial checksum is serialized along with the
-        // data it checksums (in which case the file checksum as calculated by external tools would mismatch if
-        // we did not include it), or independently.
-
-        // CompressedSequentialWriters serialize the partial checksums inline with the compressed data chunks they
-        // corroborate, whereas ChecksummedSequentialWriters serialize them to a different file.
-        public void appendDirect(ByteBuffer bb, boolean checksumIncrementalResult)
-        {
-            try
-            {
-
-                ByteBuffer toAppend = bb.duplicate();
-                toAppend.mark();
-                incrementalChecksum.update(toAppend);
-                toAppend.reset();
-
-                int incrementalChecksumValue = (int) incrementalChecksum.getValue();
-                incrementalOut.writeInt(incrementalChecksumValue);
-
-                fullChecksum.update(toAppend);
-                if (checksumIncrementalResult)
-                {
-                    ByteBuffer byteBuffer = ByteBuffer.allocate(4);
-                    byteBuffer.putInt(incrementalChecksumValue);
-                    fullChecksum.update(byteBuffer.array(), 0, byteBuffer.array().length);
-                }
-                incrementalChecksum.reset();
-
-            }
-            catch (IOException e)
-            {
-                throw new IOError(e);
-            }
-        }
-
-        public void writeFullChecksum(Descriptor descriptor)
-        {
-            if (descriptor.digestComponent == null)
-                throw new NullPointerException("Null digest component for " + descriptor.ksname + '.' + descriptor.cfname + " file " + descriptor.baseFilename());
-            File outFile = new File(descriptor.filenameFor(descriptor.digestComponent));
-            try (FileOutputStream fos = new FileOutputStream(outFile);
-                 DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fos)))
-            {
-                out.write(String.valueOf(fullChecksum.getValue()).getBytes(StandardCharsets.UTF_8));
-                out.flush();
-                fos.getFD().sync();
-            }
-            catch (IOException e)
-            {
-                throw new FSWriteError(e, outFile);
-            }
-        }
-    }
 }
diff --git a/src/java/org/apache/cassandra/io/util/DataOutputBuffer.java b/src/java/org/apache/cassandra/io/util/DataOutputBuffer.java
index 195fdb4..a6c7086 100644
--- a/src/java/org/apache/cassandra/io/util/DataOutputBuffer.java
+++ b/src/java/org/apache/cassandra/io/util/DataOutputBuffer.java
@@ -21,11 +21,12 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.WritableByteChannel;
 
-import org.apache.cassandra.config.Config;
-
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.config.Config;
+
 /**
  * An implementation of the DataOutputStream interface using a FastByteArrayOutputStream and exposing
  * its buffer so copies can be avoided.
@@ -39,9 +40,41 @@
      */
     static final long DOUBLING_THRESHOLD = Long.getLong(Config.PROPERTY_PREFIX + "DOB_DOUBLING_THRESHOLD_MB", 64);
 
+    /*
+     * Only recycle OutputBuffers up to 1Mb. Larger buffers will be trimmed back to this size.
+     */
+    private static final int MAX_RECYCLE_BUFFER_SIZE = Integer.getInteger(Config.PROPERTY_PREFIX + "dob_max_recycle_bytes", 1024 * 1024);
+
+    private static final int DEFAULT_INITIAL_BUFFER_SIZE = 128;
+
+    /**
+     * Scratch buffers used mostly for serializing in memory. It's important to call #recycle() when finished
+     * to keep the memory overhead from being too large in the system.
+     */
+    public static final FastThreadLocal<DataOutputBuffer> scratchBuffer = new FastThreadLocal<DataOutputBuffer>()
+    {
+        protected DataOutputBuffer initialValue() throws Exception
+        {
+            return new DataOutputBuffer()
+            {
+                public void close()
+                {
+                    if (buffer.capacity() <= MAX_RECYCLE_BUFFER_SIZE)
+                    {
+                        buffer.clear();
+                    }
+                    else
+                    {
+                        buffer = ByteBuffer.allocate(DEFAULT_INITIAL_BUFFER_SIZE);
+                    }
+                }
+            };
+        }
+    };
+
     public DataOutputBuffer()
     {
-        this(128);
+        this(DEFAULT_INITIAL_BUFFER_SIZE);
     }
 
     public DataOutputBuffer(int size)
@@ -49,7 +82,7 @@
         super(ByteBuffer.allocate(size));
     }
 
-    protected DataOutputBuffer(ByteBuffer buffer)
+    public DataOutputBuffer(ByteBuffer buffer)
     {
         super(buffer);
     }
@@ -135,6 +168,11 @@
         return new GrowingChannel();
     }
 
+    public void clear()
+    {
+        buffer.clear();
+    }
+
     @VisibleForTesting
     final class GrowingChannel implements WritableByteChannel
     {
@@ -183,6 +221,7 @@
 
     public byte[] getData()
     {
+        assert buffer.arrayOffset() == 0;
         return buffer.array();
     }
 
@@ -201,6 +240,11 @@
         return getLength();
     }
 
+    public ByteBuffer asNewBuffer()
+    {
+        return ByteBuffer.wrap(toByteArray());
+    }
+
     public byte[] toByteArray()
     {
         ByteBuffer buffer = buffer();
diff --git a/src/java/org/apache/cassandra/io/util/DataOutputBufferFixed.java b/src/java/org/apache/cassandra/io/util/DataOutputBufferFixed.java
index c9767fc..a78bd49 100644
--- a/src/java/org/apache/cassandra/io/util/DataOutputBufferFixed.java
+++ b/src/java/org/apache/cassandra/io/util/DataOutputBufferFixed.java
@@ -38,7 +38,7 @@
 
     public DataOutputBufferFixed(int size)
     {
-        super(ByteBuffer.allocate(size));
+        super(size);
     }
 
     public DataOutputBufferFixed(ByteBuffer buffer)
@@ -62,4 +62,9 @@
     {
         throw new BufferOverflowException();
     }
+
+    public void clear()
+    {
+        buffer.clear();
+    }
 }
diff --git a/src/java/org/apache/cassandra/io/util/DataOutputStreamPlus.java b/src/java/org/apache/cassandra/io/util/DataOutputStreamPlus.java
index a846384..4adb6d2 100644
--- a/src/java/org/apache/cassandra/io/util/DataOutputStreamPlus.java
+++ b/src/java/org/apache/cassandra/io/util/DataOutputStreamPlus.java
@@ -22,6 +22,7 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.WritableByteChannel;
 
+import io.netty.util.concurrent.FastThreadLocal;
 import org.apache.cassandra.config.Config;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
@@ -64,7 +65,7 @@
         return bytes;
     }
 
-    private static final ThreadLocal<byte[]> tempBuffer = new ThreadLocal<byte[]>()
+    private static final FastThreadLocal<byte[]> tempBuffer = new FastThreadLocal<byte[]>()
     {
         @Override
         public byte[] initialValue()
@@ -86,7 +87,7 @@
             }
 
             @Override
-            public void close() throws IOException
+            public void close()
             {
             }
 
diff --git a/src/java/org/apache/cassandra/io/util/DiskAwareRunnable.java b/src/java/org/apache/cassandra/io/util/DiskAwareRunnable.java
deleted file mode 100644
index 1a15d6f..0000000
--- a/src/java/org/apache/cassandra/io/util/DiskAwareRunnable.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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.io.util;
-
-import java.io.IOException;
-
-import org.apache.cassandra.db.Directories;
-import org.apache.cassandra.io.FSWriteError;
-import org.apache.cassandra.utils.WrappedRunnable;
-
-public abstract class DiskAwareRunnable extends WrappedRunnable
-{
-    protected Directories.DataDirectory getWriteDirectory(long writeSize)
-    {
-        Directories.DataDirectory directory = getDirectories().getWriteableLocation(writeSize);
-        if (directory == null)
-            throw new FSWriteError(new IOException("Insufficient disk space to write " + writeSize + " bytes"), "");
-
-        return directory;
-    }
-
-    /**
-     * Get sstable directories for the CF.
-     * @return Directories instance for the CF.
-     */
-    protected abstract Directories getDirectories();
-}
diff --git a/src/java/org/apache/cassandra/io/util/DiskOptimizationStrategy.java b/src/java/org/apache/cassandra/io/util/DiskOptimizationStrategy.java
new file mode 100644
index 0000000..272291e
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/DiskOptimizationStrategy.java
@@ -0,0 +1,74 @@
+/*
+ * 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.io.util;
+
+public interface DiskOptimizationStrategy
+{
+    int MIN_BUFFER_SIZE = 1 << 12; // 4096, the typical size of a page in the OS cache
+    int MIN_BUFFER_SIZE_MASK = MIN_BUFFER_SIZE - 1;
+
+    // The maximum buffer size, we will never buffer more than this size. Further,
+    // when the limiter is not null, i.e. when throttling is enabled, we read exactly
+    // this size, since when throttling the intention is to eventually read everything,
+    // see CASSANDRA-8630
+    // NOTE: this size is chosen both for historical consistency, as a reasonable upper bound,
+    //       and because our BufferPool currently has a maximum allocation size of this.
+    int MAX_BUFFER_SIZE = 1 << 16; // 64k
+
+    /**
+     * @param recordSize record size
+     * @return the buffer size for a given record size.
+     */
+    int bufferSize(long recordSize);
+
+    /**
+     * Round up to the next multiple of 4k but no more than {@link #MAX_BUFFER_SIZE}.
+     */
+    default int roundBufferSize(long size)
+    {
+        if (size <= 0)
+            return MIN_BUFFER_SIZE;
+
+        size = (size + MIN_BUFFER_SIZE_MASK) & ~MIN_BUFFER_SIZE_MASK;
+        return (int)Math.min(size, MAX_BUFFER_SIZE);
+    }
+
+    /**
+     * Round either up or down to the next power of two, which is required by the
+     * {@link org.apache.cassandra.cache.ChunkCache.CachingRebufferer}, but capping between {@link #MIN_BUFFER_SIZE}
+     * and {@link #MAX_BUFFER_SIZE}.
+     *
+     * @param size - the size to round to a power of two, normally this is a buffer size that was previously
+     *             returned by a {@link #bufferSize(long)}.
+     * @param roundUp - whether to round up or down
+     *
+     * @return a value rounded to a power of two but never bigger than {@link #MAX_BUFFER_SIZE} or smaller than {@link #MIN_BUFFER_SIZE}.
+     */
+    static int roundForCaching(int size, boolean roundUp)
+    {
+        if (size <= MIN_BUFFER_SIZE)
+            return MIN_BUFFER_SIZE;
+
+        int ret = roundUp
+                  ? 1 << (32 - Integer.numberOfLeadingZeros(size - 1))
+                  : Integer.highestOneBit(size);
+
+        return Math.min(MAX_BUFFER_SIZE, ret);
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/util/FastByteArrayInputStream.java b/src/java/org/apache/cassandra/io/util/FastByteArrayInputStream.java
index 0e729b9..f61546c 100644
--- a/src/java/org/apache/cassandra/io/util/FastByteArrayInputStream.java
+++ b/src/java/org/apache/cassandra/io/util/FastByteArrayInputStream.java
@@ -36,7 +36,8 @@
  *
  * @see ByteArrayInputStream
  */
-public class FastByteArrayInputStream extends InputStream {
+public class FastByteArrayInputStream extends InputStream
+{
     /**
      * The {@code byte} array containing the bytes to stream over.
      */
@@ -66,7 +67,8 @@
      * @param buf
      *            the byte array to stream over.
      */
-    public FastByteArrayInputStream(byte buf[]) {
+    public FastByteArrayInputStream(byte buf[])
+    {
         this.mark = 0;
         this.buf = buf;
         this.count = buf.length;
@@ -84,7 +86,8 @@
      * @param length
      *            the number of bytes available for streaming.
      */
-    public FastByteArrayInputStream(byte buf[], int offset, int length) {
+    public FastByteArrayInputStream(byte buf[], int offset, int length)
+    {
         this.buf = buf;
         pos = offset;
         mark = offset;
@@ -99,7 +102,8 @@
      * @return the number of bytes available before blocking.
      */
     @Override
-    public int available() {
+    public int available()
+    {
         return count - pos;
     }
 
@@ -110,7 +114,8 @@
      *             if an I/O error occurs while closing this stream.
      */
     @Override
-    public void close() throws IOException {
+    public void close() throws IOException
+    {
         // Do nothing on close, this matches JDK behaviour.
     }
 
@@ -125,7 +130,8 @@
      * @see #reset()
      */
     @Override
-    public void mark(int readlimit) {
+    public void mark(int readlimit)
+    {
         mark = pos;
     }
 
@@ -139,7 +145,8 @@
      * @see #reset()
      */
     @Override
-    public boolean markSupported() {
+    public boolean markSupported()
+    {
         return true;
     }
 
@@ -151,7 +158,8 @@
      * @return the byte read or -1 if the end of this stream has been reached.
      */
     @Override
-    public int read() {
+    public int read()
+    {
         return pos < count ? buf[pos++] & 0xFF : -1;
     }
 
@@ -177,20 +185,24 @@
      *             if {@code b} is {@code null}.
      */
     @Override
-    public int read(byte b[], int offset, int length) {
+    public int read(byte b[], int offset, int length)
+    {
         if (b == null) {
             throw new NullPointerException();
         }
         // avoid int overflow
         if (offset < 0 || offset > b.length || length < 0
-                || length > b.length - offset) {
+                || length > b.length - offset)
+        {
             throw new IndexOutOfBoundsException();
         }
         // Are there any bytes available?
-        if (this.pos >= this.count) {
+        if (this.pos >= this.count)
+        {
             return -1;
         }
-        if (length == 0) {
+        if (length == 0)
+        {
             return 0;
         }
 
@@ -208,7 +220,8 @@
      * @see #mark(int)
      */
     @Override
-    public void reset() {
+    public void reset()
+    {
         pos = mark;
     }
 
@@ -223,8 +236,10 @@
      * @return the number of bytes actually skipped.
      */
     @Override
-    public long skip(long n) {
-        if (n <= 0) {
+    public long skip(long n)
+    {
+        if (n <= 0)
+        {
             return 0;
         }
         int temp = pos;
diff --git a/src/java/org/apache/cassandra/io/util/FileHandle.java b/src/java/org/apache/cassandra/io/util/FileHandle.java
new file mode 100644
index 0000000..a3afc2f
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/FileHandle.java
@@ -0,0 +1,442 @@
+/*
+ * 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.io.util;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import com.google.common.util.concurrent.RateLimiter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.cache.ChunkCache;
+import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.io.compress.CompressionMetadata;
+import org.apache.cassandra.utils.NativeLibrary;
+import org.apache.cassandra.utils.concurrent.Ref;
+import org.apache.cassandra.utils.concurrent.RefCounted;
+import org.apache.cassandra.utils.concurrent.SharedCloseableImpl;
+
+import static org.apache.cassandra.utils.Throwables.maybeFail;
+
+/**
+ * {@link FileHandle} provides access to a file for reading, including the ones written by various {@link SequentialWriter}
+ * instances, and it is typically used by {@link org.apache.cassandra.io.sstable.format.SSTableReader}.
+ *
+ * Use {@link FileHandle.Builder} to create an instance, and call {@link #createReader()} (and its variants) to
+ * access the readers for the underlying file.
+ *
+ * You can use {@link Builder#complete()} several times during its lifecycle with different {@code overrideLength}(i.e. early opening file).
+ * For that reason, the builder keeps a reference to the file channel and makes a copy for each {@link Builder#complete()} call.
+ * Therefore, it is important to close the {@link Builder} when it is no longer needed, as well as any {@link FileHandle}
+ * instances.
+ */
+public class FileHandle extends SharedCloseableImpl
+{
+    private static final Logger logger = LoggerFactory.getLogger(FileHandle.class);
+
+    public final ChannelProxy channel;
+
+    public final long onDiskLength;
+
+    /*
+     * Rebufferer factory to use when constructing RandomAccessReaders
+     */
+    private final RebuffererFactory rebuffererFactory;
+
+    /*
+     * Optional CompressionMetadata when dealing with compressed file
+     */
+    private final Optional<CompressionMetadata> compressionMetadata;
+
+    private FileHandle(Cleanup cleanup,
+                       ChannelProxy channel,
+                       RebuffererFactory rebuffererFactory,
+                       CompressionMetadata compressionMetadata,
+                       long onDiskLength)
+    {
+        super(cleanup);
+        this.rebuffererFactory = rebuffererFactory;
+        this.channel = channel;
+        this.compressionMetadata = Optional.ofNullable(compressionMetadata);
+        this.onDiskLength = onDiskLength;
+    }
+
+    private FileHandle(FileHandle copy)
+    {
+        super(copy);
+        channel = copy.channel;
+        rebuffererFactory = copy.rebuffererFactory;
+        compressionMetadata = copy.compressionMetadata;
+        onDiskLength = copy.onDiskLength;
+    }
+
+    /**
+     * @return Path to the file this factory is referencing
+     */
+    public String path()
+    {
+        return channel.filePath();
+    }
+
+    public long dataLength()
+    {
+        return compressionMetadata.map(c -> c.dataLength).orElseGet(rebuffererFactory::fileLength);
+    }
+
+    public RebuffererFactory rebuffererFactory()
+    {
+        return rebuffererFactory;
+    }
+
+    public Optional<CompressionMetadata> compressionMetadata()
+    {
+        return compressionMetadata;
+    }
+
+    @Override
+    public void addTo(Ref.IdentityCollection identities)
+    {
+        super.addTo(identities);
+        compressionMetadata.ifPresent(metadata -> metadata.addTo(identities));
+    }
+
+    @Override
+    public FileHandle sharedCopy()
+    {
+        return new FileHandle(this);
+    }
+
+    /**
+     * Create {@link RandomAccessReader} with configured method of reading content of the file.
+     *
+     * @return RandomAccessReader for the file
+     */
+    public RandomAccessReader createReader()
+    {
+        return createReader(null);
+    }
+
+    /**
+     * Create {@link RandomAccessReader} with configured method of reading content of the file.
+     * Reading from file will be rate limited by given {@link RateLimiter}.
+     *
+     * @param limiter RateLimiter to use for rate limiting read
+     * @return RandomAccessReader for the file
+     */
+    public RandomAccessReader createReader(RateLimiter limiter)
+    {
+        return new RandomAccessReader(instantiateRebufferer(limiter));
+    }
+
+    public FileDataInput createReader(long position)
+    {
+        RandomAccessReader reader = createReader();
+        reader.seek(position);
+        return reader;
+    }
+
+    /**
+     * Drop page cache from start to given {@code before}.
+     *
+     * @param before uncompressed position from start of the file to be dropped from cache. if 0, to end of file.
+     */
+    public void dropPageCache(long before)
+    {
+        long position = compressionMetadata.map(metadata -> {
+            if (before >= metadata.dataLength)
+                return 0L;
+            else
+                return metadata.chunkFor(before).offset;
+        }).orElse(before);
+        NativeLibrary.trySkipCache(channel.getFileDescriptor(), 0, position, path());
+    }
+
+    private Rebufferer instantiateRebufferer(RateLimiter limiter)
+    {
+        Rebufferer rebufferer = rebuffererFactory.instantiateRebufferer();
+
+        if (limiter != null)
+            rebufferer = new LimitingRebufferer(rebufferer, limiter, DiskOptimizationStrategy.MAX_BUFFER_SIZE);
+        return rebufferer;
+    }
+
+    /**
+     * Perform clean up of all resources held by {@link FileHandle}.
+     */
+    private static class Cleanup implements RefCounted.Tidy
+    {
+        final ChannelProxy channel;
+        final RebuffererFactory rebufferer;
+        final CompressionMetadata compressionMetadata;
+        final Optional<ChunkCache> chunkCache;
+
+        private Cleanup(ChannelProxy channel,
+                        RebuffererFactory rebufferer,
+                        CompressionMetadata compressionMetadata,
+                        ChunkCache chunkCache)
+        {
+            this.channel = channel;
+            this.rebufferer = rebufferer;
+            this.compressionMetadata = compressionMetadata;
+            this.chunkCache = Optional.ofNullable(chunkCache);
+        }
+
+        public String name()
+        {
+            return channel.filePath();
+        }
+
+        public void tidy()
+        {
+            chunkCache.ifPresent(cache -> cache.invalidateFile(name()));
+            try
+            {
+                if (compressionMetadata != null)
+                {
+                    compressionMetadata.close();
+                }
+            }
+            finally
+            {
+                try
+                {
+                    channel.close();
+                }
+                finally
+                {
+                    rebufferer.close();
+                }
+            }
+        }
+    }
+
+    /**
+     * Configures how the file will be read (compressed, mmapped, use cache etc.)
+     */
+    public static class Builder implements AutoCloseable
+    {
+        private final String path;
+
+        private ChannelProxy channel;
+        private CompressionMetadata compressionMetadata;
+        private MmappedRegions regions;
+        private ChunkCache chunkCache;
+        private int bufferSize = RandomAccessReader.DEFAULT_BUFFER_SIZE;
+        private BufferType bufferType = BufferType.OFF_HEAP;
+
+        private boolean mmapped = false;
+        private boolean compressed = false;
+
+        public Builder(String path)
+        {
+            this.path = path;
+        }
+
+        public Builder(ChannelProxy channel)
+        {
+            this.channel = channel;
+            this.path = channel.filePath();
+        }
+
+        public Builder compressed(boolean compressed)
+        {
+            this.compressed = compressed;
+            return this;
+        }
+
+        /**
+         * Set {@link ChunkCache} to use.
+         *
+         * @param chunkCache ChunkCache object to use for caching
+         * @return this object
+         */
+        public Builder withChunkCache(ChunkCache chunkCache)
+        {
+            this.chunkCache = chunkCache;
+            return this;
+        }
+
+        /**
+         * Provide {@link CompressionMetadata} to use when reading compressed file.
+         *
+         * @param metadata CompressionMetadata to use
+         * @return this object
+         */
+        public Builder withCompressionMetadata(CompressionMetadata metadata)
+        {
+            this.compressed = Objects.nonNull(metadata);
+            this.compressionMetadata = metadata;
+            return this;
+        }
+
+        /**
+         * Set whether to use mmap for reading
+         *
+         * @param mmapped true if using mmap
+         * @return this instance
+         */
+        public Builder mmapped(boolean mmapped)
+        {
+            this.mmapped = mmapped;
+            return this;
+        }
+
+        /**
+         * Set the buffer size to use (if appropriate).
+         *
+         * @param bufferSize Buffer size in bytes
+         * @return this instance
+         */
+        public Builder bufferSize(int bufferSize)
+        {
+            this.bufferSize = bufferSize;
+            return this;
+        }
+
+        /**
+         * Set the buffer type (on heap or off heap) to use (if appropriate).
+         *
+         * @param bufferType Buffer type to use
+         * @return this instance
+         */
+        public Builder bufferType(BufferType bufferType)
+        {
+            this.bufferType = bufferType;
+            return this;
+        }
+
+        /**
+         * Complete building {@link FileHandle} without overriding file length.
+         *
+         * @see #complete(long)
+         */
+        public FileHandle complete()
+        {
+            return complete(-1L);
+        }
+
+        /**
+         * Complete building {@link FileHandle} with the given length, which overrides the file length.
+         *
+         * @param overrideLength Override file length (in bytes) so that read cannot go further than this value.
+         *                       If the value is less than or equal to 0, then the value is ignored.
+         * @return Built file
+         */
+        @SuppressWarnings("resource")
+        public FileHandle complete(long overrideLength)
+        {
+            if (channel == null)
+            {
+                channel = new ChannelProxy(path);
+            }
+
+            ChannelProxy channelCopy = channel.sharedCopy();
+            try
+            {
+                if (compressed && compressionMetadata == null)
+                    compressionMetadata = CompressionMetadata.create(channelCopy.filePath());
+
+                long length = overrideLength > 0 ? overrideLength : compressed ? compressionMetadata.compressedFileLength : channelCopy.size();
+
+                RebuffererFactory rebuffererFactory;
+                if (mmapped)
+                {
+                    if (compressed)
+                    {
+                        regions = MmappedRegions.map(channelCopy, compressionMetadata);
+                        rebuffererFactory = maybeCached(new CompressedChunkReader.Mmap(channelCopy, compressionMetadata,
+                                                                                       regions));
+                    }
+                    else
+                    {
+                        updateRegions(channelCopy, length);
+                        rebuffererFactory = new MmapRebufferer(channelCopy, length, regions.sharedCopy());
+                    }
+                }
+                else
+                {
+                    regions = null;
+                    if (compressed)
+                    {
+                        rebuffererFactory = maybeCached(new CompressedChunkReader.Standard(channelCopy, compressionMetadata));
+                    }
+                    else
+                    {
+                        int chunkSize = DiskOptimizationStrategy.roundForCaching(bufferSize, ChunkCache.roundUp);
+                        rebuffererFactory = maybeCached(new SimpleChunkReader(channelCopy, length, bufferType, chunkSize));
+                    }
+                }
+                Cleanup cleanup = new Cleanup(channelCopy, rebuffererFactory, compressionMetadata, chunkCache);
+                return new FileHandle(cleanup, channelCopy, rebuffererFactory, compressionMetadata, length);
+            }
+            catch (Throwable t)
+            {
+                channelCopy.close();
+                throw t;
+            }
+        }
+
+        public Throwable close(Throwable accumulate)
+        {
+            if (!compressed && regions != null)
+                accumulate = regions.close(accumulate);
+            if (channel != null)
+                return channel.close(accumulate);
+
+            return accumulate;
+        }
+
+        public void close()
+        {
+            maybeFail(close(null));
+        }
+
+        private RebuffererFactory maybeCached(ChunkReader reader)
+        {
+            if (chunkCache != null && chunkCache.capacity() > 0)
+                return chunkCache.wrap(reader);
+            return reader;
+        }
+
+        private void updateRegions(ChannelProxy channel, long length)
+        {
+            if (regions != null && !regions.isValid(channel))
+            {
+                Throwable err = regions.close(null);
+                if (err != null)
+                    logger.error("Failed to close mapped regions", err);
+
+                regions = null;
+            }
+
+            if (regions == null)
+                regions = MmappedRegions.map(channel, length);
+            else
+                regions.extend(length);
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        return getClass().getSimpleName() + "(path='" + path() + '\'' +
+               ", length=" + rebuffererFactory.fileLength() +
+               ')';
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/util/FileUtils.java b/src/java/org/apache/cassandra/io/util/FileUtils.java
index 5a21729..e1fd380 100644
--- a/src/java/org/apache/cassandra/io/util/FileUtils.java
+++ b/src/java/org/apache/cassandra/io/util/FileUtils.java
@@ -23,12 +23,14 @@
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.*;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.nio.file.attribute.FileAttributeView;
 import java.nio.file.attribute.FileStoreAttributeView;
 import java.text.DecimalFormat;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
@@ -61,8 +63,8 @@
     public static final long ONE_TB = 1024 * ONE_GB;
 
     private static final DecimalFormat df = new DecimalFormat("#.##");
-    private static final boolean canCleanDirectBuffers;
-    private static final AtomicReference<FSErrorHandler> fsErrorHandler = new AtomicReference<>();
+    public static final boolean isCleanerAvailable;
+    private static final AtomicReference<Optional<FSErrorHandler>> fsErrorHandler = new AtomicReference<>(Optional.empty());
 
     static
     {
@@ -78,7 +80,7 @@
             logger.error("Cannot initialize un-mmaper.  (Are you using a non-Oracle JVM?)  Compacted data files will not be removed promptly.  Consider using an Oracle JVM or using standard disk access mode", t);
             JVMStabilityInspector.inspectThrowable(t);
         }
-        canCleanDirectBuffers = canClean;
+        isCleanerAvailable = canClean;
     }
 
     public static void createHardLink(String from, String to)
@@ -180,7 +182,7 @@
     {
         assert from.exists();
         if (logger.isTraceEnabled())
-            logger.trace((String.format("Renaming %s to %s", from.getPath(), to.getPath())));
+            logger.trace("Renaming {} to {}", from.getPath(), to.getPath());
         // this is not FSWE because usually when we see it it's because we didn't close the file before renaming it,
         // and Windows is picky about that.
         try
@@ -338,14 +340,11 @@
         }
     }
 
-    public static boolean isCleanerAvailable()
-    {
-        return canCleanDirectBuffers;
-    }
-
     public static void clean(ByteBuffer buffer)
     {
-        if (isCleanerAvailable() && buffer.isDirect())
+        if (buffer == null)
+            return;
+        if (isCleanerAvailable && buffer.isDirect())
         {
             DirectBuffer db = (DirectBuffer) buffer;
             if (db.cleaner() != null)
@@ -423,25 +422,25 @@
         {
             d = value / ONE_TB;
             String val = df.format(d);
-            return val + " TB";
+            return val + " TiB";
         }
         else if ( value >= ONE_GB )
         {
             d = value / ONE_GB;
             String val = df.format(d);
-            return val + " GB";
+            return val + " GiB";
         }
         else if ( value >= ONE_MB )
         {
             d = value / ONE_MB;
             String val = df.format(d);
-            return val + " MB";
+            return val + " MiB";
         }
         else if ( value >= ONE_KB )
         {
             d = value / ONE_KB;
             String val = df.format(d);
-            return val + " KB";
+            return val + " KiB";
         }
         else
         {
@@ -481,22 +480,18 @@
                 deleteRecursiveOnExit(new File(dir, child));
         }
 
-        logger.trace("Scheduling deferred deletion of file: " + dir);
+        logger.trace("Scheduling deferred deletion of file: {}", dir);
         dir.deleteOnExit();
     }
 
     public static void handleCorruptSSTable(CorruptSSTableException e)
     {
-        FSErrorHandler handler = fsErrorHandler.get();
-        if (handler != null)
-            handler.handleCorruptSSTable(e);
+        fsErrorHandler.get().ifPresent(handler -> handler.handleCorruptSSTable(e));
     }
 
     public static void handleFSError(FSError e)
     {
-        FSErrorHandler handler = fsErrorHandler.get();
-        if (handler != null)
-            handler.handleFSError(e);
+        fsErrorHandler.get().ifPresent(handler -> handler.handleFSError(e));
     }
 
     /**
@@ -515,20 +510,29 @@
 
     /**
      * Get the size of a directory in bytes
-     * @param directory The directory for which we need size.
+     * @param folder The directory for which we need size.
      * @return The size of the directory
      */
-    public static long folderSize(File directory)
+    public static long folderSize(File folder)
     {
-        long length = 0;
-        for (File file : directory.listFiles())
+        final long [] sizeArr = {0L};
+        try
         {
-            if (file.isFile())
-                length += file.length();
-            else
-                length += folderSize(file);
+            Files.walkFileTree(folder.toPath(), new SimpleFileVisitor<Path>()
+            {
+                @Override
+                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+                {
+                    sizeArr[0] += attrs.size();
+                    return FileVisitResult.CONTINUE;
+                }
+            });
         }
-        return length;
+        catch (IOException e)
+        {
+            logger.error("Error while getting {} folder size. {}", folder, e);
+        }
+        return sizeArr[0];
     }
 
     public static void copyTo(DataInput in, OutputStream out, int length) throws IOException
@@ -619,7 +623,7 @@
 
     public static void setFSErrorHandler(FSErrorHandler handler)
     {
-        fsErrorHandler.getAndSet(handler);
+        fsErrorHandler.getAndSet(Optional.ofNullable(handler));
     }
 
     /**
diff --git a/src/java/org/apache/cassandra/io/util/ICompressedFile.java b/src/java/org/apache/cassandra/io/util/ICompressedFile.java
deleted file mode 100644
index 43cef8c..0000000
--- a/src/java/org/apache/cassandra/io/util/ICompressedFile.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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.io.util;
-
-import org.apache.cassandra.io.compress.CompressionMetadata;
-
-public interface ICompressedFile
-{
-    ChannelProxy channel();
-    CompressionMetadata getMetadata();
-    MmappedRegions regions();
-
-}
diff --git a/src/java/org/apache/cassandra/io/util/LimitingRebufferer.java b/src/java/org/apache/cassandra/io/util/LimitingRebufferer.java
new file mode 100644
index 0000000..a1e9715
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/LimitingRebufferer.java
@@ -0,0 +1,126 @@
+/*
+ *
+ * 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.io.util;
+
+import java.nio.ByteBuffer;
+
+import com.google.common.primitives.Ints;
+import com.google.common.util.concurrent.RateLimiter;
+
+/**
+ * Rebufferer wrapper that applies rate limiting.
+ *
+ * Instantiated once per RandomAccessReader, thread-unsafe.
+ * The instances reuse themselves as the BufferHolder to avoid having to return a new object for each rebuffer call.
+ */
+public class LimitingRebufferer implements Rebufferer, Rebufferer.BufferHolder
+{
+    final private Rebufferer wrapped;
+    final private RateLimiter limiter;
+    final private int limitQuant;
+
+    private BufferHolder bufferHolder;
+    private ByteBuffer buffer;
+    private long offset;
+
+    public LimitingRebufferer(Rebufferer wrapped, RateLimiter limiter, int limitQuant)
+    {
+        this.wrapped = wrapped;
+        this.limiter = limiter;
+        this.limitQuant = limitQuant;
+    }
+
+    @Override
+    public BufferHolder rebuffer(long position)
+    {
+        bufferHolder = wrapped.rebuffer(position);
+        buffer = bufferHolder.buffer();
+        offset = bufferHolder.offset();
+        int posInBuffer = Ints.checkedCast(position - offset);
+        int remaining = buffer.limit() - posInBuffer;
+        if (remaining == 0)
+            return this;
+
+        if (remaining > limitQuant)
+        {
+            buffer.limit(posInBuffer + limitQuant); // certainly below current limit
+            remaining = limitQuant;
+        }
+        limiter.acquire(remaining);
+        return this;
+    }
+
+    @Override
+    public ChannelProxy channel()
+    {
+        return wrapped.channel();
+    }
+
+    @Override
+    public long fileLength()
+    {
+        return wrapped.fileLength();
+    }
+
+    @Override
+    public double getCrcCheckChance()
+    {
+        return wrapped.getCrcCheckChance();
+    }
+
+    @Override
+    public void close()
+    {
+        wrapped.close();
+    }
+
+    @Override
+    public void closeReader()
+    {
+        wrapped.closeReader();
+    }
+
+    @Override
+    public String toString()
+    {
+        return "LimitingRebufferer[" + limiter.toString() + "]:" + wrapped.toString();
+    }
+
+    // BufferHolder methods
+
+    @Override
+    public ByteBuffer buffer()
+    {
+        return buffer;
+    }
+
+    @Override
+    public long offset()
+    {
+        return offset;
+    }
+
+    @Override
+    public void release()
+    {
+        bufferHolder.release();
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/util/Memory.java b/src/java/org/apache/cassandra/io/util/Memory.java
index 78950ce..431f73d 100644
--- a/src/java/org/apache/cassandra/io/util/Memory.java
+++ b/src/java/org/apache/cassandra/io/util/Memory.java
@@ -22,6 +22,8 @@
 import java.nio.ByteOrder;
 
 import net.nicoulaj.compilecommand.annotations.Inline;
+
+import org.apache.cassandra.utils.Architecture;
 import org.apache.cassandra.utils.FastByteOperations;
 import org.apache.cassandra.utils.concurrent.Ref;
 import org.apache.cassandra.utils.memory.MemoryUtil;
@@ -51,17 +53,9 @@
     private static final long BYTE_ARRAY_BASE_OFFSET = unsafe.arrayBaseOffset(byte[].class);
 
     private static final boolean bigEndian = ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN);
-    private static final boolean unaligned;
 
     public static final ByteBuffer[] NO_BYTE_BUFFERS = new ByteBuffer[0];
 
-    static
-    {
-        String arch = System.getProperty("os.arch");
-        unaligned = arch.equals("i386") || arch.equals("x86")
-                    || arch.equals("amd64") || arch.equals("x86_64") || arch.equals("s390x");
-    }
-
     protected long peer;
     // size of the memory region
     protected final long size;
@@ -113,7 +107,7 @@
     public void setLong(long offset, long l)
     {
         checkBounds(offset, offset + 8);
-        if (unaligned)
+        if (Architecture.IS_UNALIGNED)
         {
             unsafe.putLong(peer + offset, l);
         }
@@ -152,7 +146,7 @@
     public void setInt(long offset, int l)
     {
         checkBounds(offset, offset + 4);
-        if (unaligned)
+        if (Architecture.IS_UNALIGNED)
         {
             unsafe.putInt(peer + offset, l);
         }
@@ -183,7 +177,7 @@
     public void setShort(long offset, short l)
     {
         checkBounds(offset, offset + 2);
-        if (unaligned)
+        if (Architecture.IS_UNALIGNED)
         {
             unsafe.putShort(peer + offset, l);
         }
@@ -258,15 +252,18 @@
     public long getLong(long offset)
     {
         checkBounds(offset, offset + 8);
-        if (unaligned) {
+        if (Architecture.IS_UNALIGNED)
+        {
             return unsafe.getLong(peer + offset);
         } else {
             return getLongByByte(peer + offset);
         }
     }
 
-    private long getLongByByte(long address) {
-        if (bigEndian) {
+    private long getLongByByte(long address)
+    {
+        if (bigEndian)
+        {
             return  (((long) unsafe.getByte(address    )       ) << 56) |
                     (((long) unsafe.getByte(address + 1) & 0xff) << 48) |
                     (((long) unsafe.getByte(address + 2) & 0xff) << 40) |
@@ -275,7 +272,9 @@
                     (((long) unsafe.getByte(address + 5) & 0xff) << 16) |
                     (((long) unsafe.getByte(address + 6) & 0xff) <<  8) |
                     (((long) unsafe.getByte(address + 7) & 0xff)      );
-        } else {
+        }
+        else
+        {
             return  (((long) unsafe.getByte(address + 7)       ) << 56) |
                     (((long) unsafe.getByte(address + 6) & 0xff) << 48) |
                     (((long) unsafe.getByte(address + 5) & 0xff) << 40) |
@@ -290,24 +289,31 @@
     public int getInt(long offset)
     {
         checkBounds(offset, offset + 4);
-        if (unaligned) {
+        if (Architecture.IS_UNALIGNED)
+        {
             return unsafe.getInt(peer + offset);
-        } else {
+        }
+        else
+        {
             return getIntByByte(peer + offset);
         }
     }
 
-    private int getIntByByte(long address) {
-        if (bigEndian) {
-            return  (((int) unsafe.getByte(address    )       ) << 24) |
-                    (((int) unsafe.getByte(address + 1) & 0xff) << 16) |
-                    (((int) unsafe.getByte(address + 2) & 0xff) << 8 ) |
-                    (((int) unsafe.getByte(address + 3) & 0xff)      );
-        } else {
-            return  (((int) unsafe.getByte(address + 3)       ) << 24) |
-                    (((int) unsafe.getByte(address + 2) & 0xff) << 16) |
-                    (((int) unsafe.getByte(address + 1) & 0xff) <<  8) |
-                    (((int) unsafe.getByte(address    ) & 0xff)      );
+    private int getIntByByte(long address)
+    {
+        if (bigEndian)
+        {
+            return  ((unsafe.getByte(address    )       ) << 24) |
+                    ((unsafe.getByte(address + 1) & 0xff) << 16) |
+                    ((unsafe.getByte(address + 2) & 0xff) << 8 ) |
+                    ((unsafe.getByte(address + 3) & 0xff)      );
+        }
+        else
+        {
+            return  ((unsafe.getByte(address + 3)       ) << 24) |
+                    ((unsafe.getByte(address + 2) & 0xff) << 16) |
+                    ((unsafe.getByte(address + 1) & 0xff) <<  8) |
+                    ((unsafe.getByte(address    ) & 0xff)      );
         }
     }
 
diff --git a/src/java/org/apache/cassandra/io/util/MemoryInputStream.java b/src/java/org/apache/cassandra/io/util/MemoryInputStream.java
index e009528..3daa4c4 100644
--- a/src/java/org/apache/cassandra/io/util/MemoryInputStream.java
+++ b/src/java/org/apache/cassandra/io/util/MemoryInputStream.java
@@ -71,6 +71,6 @@
 
     private static ByteBuffer getByteBuffer(long offset, int length)
     {
-        return MemoryUtil.getByteBuffer(offset, length).order(ByteOrder.BIG_ENDIAN);
+        return MemoryUtil.getByteBuffer(offset, length, ByteOrder.BIG_ENDIAN);
     }
 }
diff --git a/src/java/org/apache/cassandra/io/util/MmapRebufferer.java b/src/java/org/apache/cassandra/io/util/MmapRebufferer.java
new file mode 100644
index 0000000..8df6370
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/MmapRebufferer.java
@@ -0,0 +1,69 @@
+/*
+ *
+ * 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.io.util;
+
+/**
+ * Rebufferer for memory-mapped files. Thread-safe and shared among reader instances.
+ * This is simply a thin wrapper around MmappedRegions as the buffers there can be used directly after duplication.
+ */
+class MmapRebufferer extends AbstractReaderFileProxy implements Rebufferer, RebuffererFactory
+{
+    protected final MmappedRegions regions;
+
+    MmapRebufferer(ChannelProxy channel, long fileLength, MmappedRegions regions)
+    {
+        super(channel, fileLength);
+        this.regions = regions;
+    }
+
+    @Override
+    public BufferHolder rebuffer(long position)
+    {
+        return regions.floor(position);
+    }
+
+    @Override
+    public Rebufferer instantiateRebufferer()
+    {
+        return this;
+    }
+
+    @Override
+    public void close()
+    {
+        regions.closeQuietly();
+    }
+
+    @Override
+    public void closeReader()
+    {
+        // Instance is shared among readers. Nothing to release.
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.format("%s(%s - data length %d)",
+                             getClass().getSimpleName(),
+                             channel.filePath(),
+                             fileLength());
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/util/MmappedRegions.java b/src/java/org/apache/cassandra/io/util/MmappedRegions.java
index 8f6cd92..13b476a 100644
--- a/src/java/org/apache/cassandra/io/util/MmappedRegions.java
+++ b/src/java/org/apache/cassandra/io/util/MmappedRegions.java
@@ -22,8 +22,11 @@
 import java.nio.channels.FileChannel;
 import java.util.Arrays;
 
+import org.slf4j.LoggerFactory;
+
 import org.apache.cassandra.io.FSReadError;
 import org.apache.cassandra.io.compress.CompressionMetadata;
+import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.Throwables;
 import org.apache.cassandra.utils.concurrent.RefCounted;
 import org.apache.cassandra.utils.concurrent.SharedCloseableImpl;
@@ -88,6 +91,11 @@
         return new MmappedRegions(channel, null, 0);
     }
 
+    /**
+     * @param channel file to map. the MmappedRegions instance will hold shared copy of given channel.
+     * @param metadata
+     * @return new instance
+     */
     public static MmappedRegions map(ChannelProxy channel, CompressionMetadata metadata)
     {
         if (metadata == null)
@@ -190,8 +198,20 @@
         assert !isCleanedUp() : "Attempted to use closed region";
         return state.floor(position);
     }
+    
+    public void closeQuietly()
+    {
+        Throwable err = close(null);
+        if (err != null)
+        {
+            JVMStabilityInspector.inspectThrowable(err);
 
-    public static final class Region
+            // This is not supposed to happen
+            LoggerFactory.getLogger(getClass()).error("Error while closing mmapped regions", err);
+        }
+    }
+
+    public static final class Region implements Rebufferer.BufferHolder
     {
         public final long offset;
         public final ByteBuffer buffer;
@@ -202,15 +222,25 @@
             this.buffer = buffer;
         }
 
-        public long bottom()
+        public ByteBuffer buffer()
+        {
+            return buffer.duplicate();
+        }
+
+        public long offset()
         {
             return offset;
         }
 
-        public long top()
+        public long end()
         {
             return offset + buffer.capacity();
         }
+
+        public void release()
+        {
+            // only released after no readers are present
+        }
     }
 
     private static final class State
@@ -260,7 +290,7 @@
 
         private Region floor(long position)
         {
-            assert 0 <= position && position < length : String.format("%d >= %d", position, length);
+            assert 0 <= position && position <= length : String.format("%d > %d", position, length);
 
             int idx = Arrays.binarySearch(offsets, 0, last +1, position);
             assert idx != -1 : String.format("Bad position %d for regions %s, last %d in %s", position, Arrays.toString(offsets), last, channel);
@@ -300,7 +330,7 @@
              * If this fails (non Sun JVM), we'll have to wait for the GC to finalize the mapping.
              * If this works and a thread tries to access any segment, hell will unleash on earth.
              */
-            if (!FileUtils.isCleanerAvailable())
+            if (!FileUtils.isCleanerAvailable)
                 return accumulate;
 
             return perform(accumulate, channel.filePath(), Throwables.FileOpType.READ,
diff --git a/src/java/org/apache/cassandra/io/util/MmappedSegmentedFile.java b/src/java/org/apache/cassandra/io/util/MmappedSegmentedFile.java
deleted file mode 100644
index 5f56ff6..0000000
--- a/src/java/org/apache/cassandra/io/util/MmappedSegmentedFile.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * 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.io.util;
-
-import java.io.*;
-
-import com.google.common.util.concurrent.RateLimiter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.cassandra.db.TypeSizes;
-import org.apache.cassandra.io.sstable.format.Version;
-import org.apache.cassandra.utils.JVMStabilityInspector;
-
-public class MmappedSegmentedFile extends SegmentedFile
-{
-    private static final Logger logger = LoggerFactory.getLogger(MmappedSegmentedFile.class);
-
-    private final MmappedRegions regions;
-
-    public MmappedSegmentedFile(ChannelProxy channel, int bufferSize, long length, MmappedRegions regions)
-    {
-        super(new Cleanup(channel, regions), channel, bufferSize, length);
-        this.regions = regions;
-    }
-
-    private MmappedSegmentedFile(MmappedSegmentedFile copy)
-    {
-        super(copy);
-        this.regions = copy.regions;
-    }
-
-    public MmappedSegmentedFile sharedCopy()
-    {
-        return new MmappedSegmentedFile(this);
-    }
-
-    public RandomAccessReader createReader()
-    {
-        return new RandomAccessReader.Builder(channel)
-               .overrideLength(length)
-               .regions(regions)
-               .build();
-    }
-
-    public RandomAccessReader createReader(RateLimiter limiter)
-    {
-        return new RandomAccessReader.Builder(channel)
-               .overrideLength(length)
-               .bufferSize(bufferSize)
-               .regions(regions)
-               .limiter(limiter)
-               .build();
-    }
-
-    private static final class Cleanup extends SegmentedFile.Cleanup
-    {
-        private final MmappedRegions regions;
-
-        Cleanup(ChannelProxy channel, MmappedRegions regions)
-        {
-            super(channel);
-            this.regions = regions;
-        }
-
-        public void tidy()
-        {
-            Throwable err = regions.close(null);
-            if (err != null)
-            {
-                JVMStabilityInspector.inspectThrowable(err);
-
-                // This is not supposed to happen
-                logger.error("Error while closing mmapped regions", err);
-            }
-
-            super.tidy();
-        }
-    }
-
-    /**
-     * Overrides the default behaviour to create segments of a maximum size.
-     */
-    static class Builder extends SegmentedFile.Builder
-    {
-        private MmappedRegions regions;
-
-        Builder()
-        {
-            super();
-        }
-
-        public SegmentedFile complete(ChannelProxy channel, int bufferSize, long overrideLength)
-        {
-            long length = overrideLength > 0 ? overrideLength : channel.size();
-            updateRegions(channel, length);
-
-            return new MmappedSegmentedFile(channel, bufferSize, length, regions.sharedCopy());
-        }
-
-        private void updateRegions(ChannelProxy channel, long length)
-        {
-            if (regions != null && !regions.isValid(channel))
-            {
-                Throwable err = regions.close(null);
-                if (err != null)
-                    logger.error("Failed to close mapped regions", err);
-
-                regions = null;
-            }
-
-            if (regions == null)
-                regions = MmappedRegions.map(channel, length);
-            else
-                regions.extend(length);
-        }
-
-        @Override
-        public void serializeBounds(DataOutput out, Version version) throws IOException
-        {
-            if (!version.hasBoundaries())
-                return;
-
-            super.serializeBounds(out, version);
-            out.writeInt(0);
-        }
-
-        @Override
-        public void deserializeBounds(DataInput in, Version version) throws IOException
-        {
-            if (!version.hasBoundaries())
-                return;
-
-            super.deserializeBounds(in, version);
-            in.skipBytes(in.readInt() * TypeSizes.sizeof(0L));
-        }
-
-        @Override
-        public Throwable close(Throwable accumulate)
-        {
-            return super.close(regions == null
-                               ? accumulate
-                               : regions.close(accumulate));
-        }
-    }
-}
diff --git a/src/java/org/apache/cassandra/io/util/RandomAccessReader.java b/src/java/org/apache/cassandra/io/util/RandomAccessReader.java
index 1943773..b8c1818 100644
--- a/src/java/org/apache/cassandra/io/util/RandomAccessReader.java
+++ b/src/java/org/apache/cassandra/io/util/RandomAccessReader.java
@@ -17,84 +17,35 @@
  */
 package org.apache.cassandra.io.util;
 
-import java.io.*;
-import java.nio.ByteBuffer;
+import java.io.File;
+import java.io.IOException;
 import java.nio.ByteOrder;
 
 import com.google.common.primitives.Ints;
-import com.google.common.util.concurrent.RateLimiter;
 
-import org.apache.cassandra.io.FSReadError;
 import org.apache.cassandra.io.compress.BufferType;
-import org.apache.cassandra.utils.memory.BufferPool;
+import org.apache.cassandra.io.util.Rebufferer.BufferHolder;
 
 public class RandomAccessReader extends RebufferingInputStream implements FileDataInput
 {
     // The default buffer size when the client doesn't specify it
     public static final int DEFAULT_BUFFER_SIZE = 4096;
 
-    // The maximum buffer size, we will never buffer more than this size. Further,
-    // when the limiter is not null, i.e. when throttling is enabled, we read exactly
-    // this size, since when throttling the intention is to eventually read everything,
-    // see CASSANDRA-8630
-    // NOTE: this size is chosen both for historical consistency, as a reasonable upper bound,
-    //       and because our BufferPool currently has a maximum allocation size of this.
-    public static final int MAX_BUFFER_SIZE = 1 << 16; // 64k
-
-    // the IO channel to the file, we do not own a reference to this due to
-    // performance reasons (CASSANDRA-9379) so it's up to the owner of the RAR to
-    // ensure that the channel stays open and that it is closed afterwards
-    protected final ChannelProxy channel;
-
-    // optional memory mapped regions for the channel
-    protected final MmappedRegions regions;
-
-    // An optional limiter that will throttle the amount of data we read
-    protected final RateLimiter limiter;
-
-    // the file length, this can be overridden at construction to a value shorter
-    // than the true length of the file; if so, it acts as an imposed limit on reads,
-    // required when opening sstables early not to read past the mark
-    private final long fileLength;
-
-    // the buffer size for buffered readers
-    protected final int bufferSize;
-
-    // the buffer type for buffered readers
-    protected final BufferType bufferType;
-
-    // offset from the beginning of the file
-    protected long bufferOffset;
-
     // offset of the last file mark
-    protected long markedPointer;
+    private long markedPointer;
 
-    protected RandomAccessReader(Builder builder)
+    final Rebufferer rebufferer;
+    private BufferHolder bufferHolder = Rebufferer.EMPTY;
+
+    /**
+     * Only created through Builder
+     *
+     * @param rebufferer Rebufferer to use
+     */
+    RandomAccessReader(Rebufferer rebufferer)
     {
-        super(builder.createBuffer());
-
-        this.channel = builder.channel;
-        this.regions = builder.regions;
-        this.limiter = builder.limiter;
-        this.fileLength = builder.overrideLength <= 0 ? builder.channel.size() : builder.overrideLength;
-        this.bufferSize = builder.bufferSize;
-        this.bufferType = builder.bufferType;
-        this.buffer = builder.buffer;
-    }
-
-    protected static ByteBuffer allocateBuffer(int size, BufferType bufferType)
-    {
-        return BufferPool.get(size, bufferType).order(ByteOrder.BIG_ENDIAN);
-    }
-
-    protected void releaseBuffer()
-    {
-        if (buffer != null)
-        {
-            if (regions == null)
-                BufferPool.put(buffer);
-            buffer = null;
-        }
+        super(Rebufferer.EMPTY.buffer());
+        this.rebufferer = rebufferer;
     }
 
     /**
@@ -105,80 +56,40 @@
         if (isEOF())
             return;
 
-        if (regions == null)
-            reBufferStandard();
-        else
-            reBufferMmap();
+        reBufferAt(current());
+    }
 
-        if (limiter != null)
-            limiter.acquire(buffer.remaining());
+    private void reBufferAt(long position)
+    {
+        bufferHolder.release();
+        bufferHolder = rebufferer.rebuffer(position);
+        buffer = bufferHolder.buffer();
+        buffer.position(Ints.checkedCast(position - bufferHolder.offset()));
 
         assert buffer.order() == ByteOrder.BIG_ENDIAN : "Buffer must have BIG ENDIAN byte ordering";
     }
 
-    protected void reBufferStandard()
-    {
-        bufferOffset += buffer.position();
-        assert bufferOffset < fileLength;
-
-        buffer.clear();
-        long position = bufferOffset;
-        long limit = bufferOffset;
-
-        long pageAligedPos = position & ~4095;
-        // Because the buffer capacity is a multiple of the page size, we read less
-        // the first time and then we should read at page boundaries only,
-        // unless the user seeks elsewhere
-        long upperLimit = Math.min(fileLength, pageAligedPos + buffer.capacity());
-        buffer.limit((int)(upperLimit - position));
-        while (buffer.hasRemaining() && limit < upperLimit)
-        {
-            int n = channel.read(buffer, position);
-            if (n < 0)
-                throw new FSReadError(new IOException("Unexpected end of file"), channel.filePath());
-
-            position += n;
-            limit = bufferOffset + buffer.position();
-        }
-
-        buffer.flip();
-    }
-
-    protected void reBufferMmap()
-    {
-        long position = bufferOffset + buffer.position();
-        assert position < fileLength;
-
-        MmappedRegions.Region region = regions.floor(position);
-        bufferOffset = region.bottom();
-        buffer = region.buffer.duplicate();
-        buffer.position(Ints.checkedCast(position - bufferOffset));
-
-        if (limiter != null && bufferSize < buffer.remaining())
-        { // ensure accurate throttling
-            buffer.limit(buffer.position() + bufferSize);
-        }
-    }
-
     @Override
     public long getFilePointer()
     {
+        if (buffer == null)     // closed already
+            return rebufferer.fileLength();
         return current();
     }
 
     protected long current()
     {
-        return bufferOffset + (buffer == null ? 0 : buffer.position());
+        return bufferHolder.offset() + buffer.position();
     }
 
     public String getPath()
     {
-        return channel.filePath();
+        return getChannel().filePath();
     }
 
     public ChannelProxy getChannel()
     {
-        return channel;
+        return rebufferer.channel();
     }
 
     @Override
@@ -242,12 +153,14 @@
     @Override
     public void close()
     {
-	    //make idempotent
+        // close needs to be idempotent.
         if (buffer == null)
             return;
 
-        bufferOffset += buffer.position();
-        releaseBuffer();
+        bufferHolder.release();
+        rebufferer.closeReader();
+        buffer = null;
+        bufferHolder = null;
 
         //For performance reasons we don't keep a reference to the file
         //channel so we don't close it
@@ -256,17 +169,17 @@
     @Override
     public String toString()
     {
-        return getClass().getSimpleName() + "(filePath='" + channel + "')";
+        return getClass().getSimpleName() + ':' + rebufferer.toString();
     }
 
     /**
      * Class to hold a mark to the position of the file
      */
-    protected static class BufferedRandomAccessFileMark implements DataPosition
+    private static class BufferedRandomAccessFileMark implements DataPosition
     {
         final long pointer;
 
-        public BufferedRandomAccessFileMark(long pointer)
+        private BufferedRandomAccessFileMark(long pointer)
         {
             this.pointer = pointer;
         }
@@ -281,26 +194,29 @@
         if (buffer == null)
             throw new IllegalStateException("Attempted to seek in a closed RAR");
 
-        if (newPosition >= length()) // it is save to call length() in read-only mode
-        {
-            if (newPosition > length())
-                throw new IllegalArgumentException(String.format("Unable to seek to position %d in %s (%d bytes) in read-only mode",
-                                                             newPosition, getPath(), length()));
-            buffer.limit(0);
-            bufferOffset = newPosition;
-            return;
-        }
-
+        long bufferOffset = bufferHolder.offset();
         if (newPosition >= bufferOffset && newPosition < bufferOffset + buffer.limit())
         {
             buffer.position((int) (newPosition - bufferOffset));
             return;
         }
-        // Set current location to newPosition and clear buffer so reBuffer calculates from newPosition
-        bufferOffset = newPosition;
-        buffer.clear();
-        reBuffer();
-        assert current() == newPosition;
+
+        if (newPosition > length())
+            throw new IllegalArgumentException(String.format("Unable to seek to position %d in %s (%d bytes) in read-only mode",
+                                                         newPosition, getPath(), length()));
+        reBufferAt(newPosition);
+    }
+
+    @Override
+    public int skipBytes(int n) throws IOException
+    {
+        if (n < 0)
+            return 0;
+        long current = current();
+        long newPosition = Math.min(current + n, length());
+        n = (int)(newPosition - current);
+        seek(newPosition);
+        return n;
     }
 
     /**
@@ -308,10 +224,10 @@
      * represented by zero or more characters followed by {@code '\n'}, {@code
      * '\r'}, {@code "\r\n"} or the end of file marker. The string does not
      * include the line terminating sequence.
-     * <p/>
+     * <p>
      * Blocks until a line terminating sequence has been read, the end of the
      * file is reached or an exception is thrown.
-     *
+     * </p>
      * @return the contents of the line or {@code null} if no characters have
      * been read before the end of the file has been reached.
      * @throws IOException if this file is closed or another I/O error occurs.
@@ -353,7 +269,7 @@
 
     public long length()
     {
-        return fileLength;
+        return rebufferer.fileLength();
     }
 
     public long getPosition()
@@ -361,115 +277,9 @@
         return current();
     }
 
-    public static class Builder
+    public double getCrcCheckChance()
     {
-        // The NIO file channel or an empty channel
-        public final ChannelProxy channel;
-
-        // We override the file length when we open sstables early, so that we do not
-        // read past the early mark
-        public long overrideLength;
-
-        // The size of the buffer for buffered readers
-        public int bufferSize;
-
-        // The type of the buffer for buffered readers
-        public BufferType bufferType;
-
-        // The buffer
-        public ByteBuffer buffer;
-
-        // The mmap segments for mmap readers
-        public MmappedRegions regions;
-
-        // An optional limiter that will throttle the amount of data we read
-        public RateLimiter limiter;
-
-        public Builder(ChannelProxy channel)
-        {
-            this.channel = channel;
-            this.overrideLength = -1L;
-            this.bufferSize = DEFAULT_BUFFER_SIZE;
-            this.bufferType = BufferType.OFF_HEAP;
-            this.regions = null;
-            this.limiter = null;
-        }
-
-        /** The buffer size is typically already page aligned but if that is not the case
-         * make sure that it is a multiple of the page size, 4096. Also limit it to the maximum
-         * buffer size unless we are throttling, in which case we may as well read the maximum
-         * directly since the intention is to read the full file, see CASSANDRA-8630.
-         * */
-        private void setBufferSize()
-        {
-            if (limiter != null)
-            {
-                bufferSize = MAX_BUFFER_SIZE;
-                return;
-            }
-
-            if ((bufferSize & ~4095) != bufferSize)
-            { // should already be a page size multiple but if that's not case round it up
-                bufferSize = (bufferSize + 4095) & ~4095;
-            }
-
-            bufferSize = Math.min(MAX_BUFFER_SIZE, bufferSize);
-        }
-
-        protected ByteBuffer createBuffer()
-        {
-            setBufferSize();
-
-            buffer = regions == null
-                     ? allocateBuffer(bufferSize, bufferType)
-                     : regions.floor(0).buffer.duplicate();
-
-            buffer.limit(0);
-            return buffer;
-        }
-
-        public Builder overrideLength(long overrideLength)
-        {
-            this.overrideLength = overrideLength;
-            return this;
-        }
-
-        public Builder bufferSize(int bufferSize)
-        {
-            if (bufferSize <= 0)
-                throw new IllegalArgumentException("bufferSize must be positive");
-
-            this.bufferSize = bufferSize;
-            return this;
-        }
-
-        public Builder bufferType(BufferType bufferType)
-        {
-            this.bufferType = bufferType;
-            return this;
-        }
-
-        public Builder regions(MmappedRegions regions)
-        {
-            this.regions = regions;
-            return this;
-        }
-
-        public Builder limiter(RateLimiter limiter)
-        {
-            this.limiter = limiter;
-            return this;
-        }
-
-        public RandomAccessReader build()
-        {
-            return new RandomAccessReader(this);
-        }
-
-        public RandomAccessReader buildWithChannel()
-        {
-            return new RandomAccessReaderWithOwnChannel(this);
-        }
+        return rebufferer.getCrcCheckChance();
     }
 
     // A wrapper of the RandomAccessReader that closes the channel when done.
@@ -477,11 +287,11 @@
     // a channel but assumes the owner will keep it open and close it,
     // see CASSANDRA-9379, this thin class is just for those cases where we do
     // not have a shared channel.
-    public static class RandomAccessReaderWithOwnChannel extends RandomAccessReader
+    static class RandomAccessReaderWithOwnChannel extends RandomAccessReader
     {
-        protected RandomAccessReaderWithOwnChannel(Builder builder)
+        RandomAccessReaderWithOwnChannel(Rebufferer rebufferer)
         {
-            super(builder);
+            super(rebufferer);
         }
 
         @Override
@@ -493,19 +303,38 @@
             }
             finally
             {
-                channel.close();
+                try
+                {
+                    rebufferer.close();
+                }
+                finally
+                {
+                    getChannel().close();
+                }
             }
         }
     }
 
+    /**
+     * Open a RandomAccessReader (not compressed, not mmapped, no read throttling) that will own its channel.
+     *
+     * @param file File to open for reading
+     * @return new RandomAccessReader that owns the channel opened in this method.
+     */
     @SuppressWarnings("resource")
     public static RandomAccessReader open(File file)
     {
-        return new Builder(new ChannelProxy(file)).buildWithChannel();
-    }
-
-    public static RandomAccessReader open(ChannelProxy channel)
-    {
-        return new Builder(channel).build();
+        ChannelProxy channel = new ChannelProxy(file);
+        try
+        {
+            ChunkReader reader = new SimpleChunkReader(channel, -1, BufferType.OFF_HEAP, DEFAULT_BUFFER_SIZE);
+            Rebufferer rebufferer = reader.instantiateRebufferer();
+            return new RandomAccessReaderWithOwnChannel(rebufferer);
+        }
+        catch (Throwable t)
+        {
+            channel.close();
+            throw t;
+        }
     }
 }
diff --git a/src/java/org/apache/cassandra/io/util/ReaderFileProxy.java b/src/java/org/apache/cassandra/io/util/ReaderFileProxy.java
new file mode 100644
index 0000000..3ddb143
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/ReaderFileProxy.java
@@ -0,0 +1,36 @@
+/*
+ * 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.io.util;
+
+/**
+ * Base class for the RandomAccessReader components that implement reading.
+ */
+public interface ReaderFileProxy extends AutoCloseable
+{
+    void close();               // no checked exceptions
+
+    ChannelProxy channel();
+
+    long fileLength();
+
+    /**
+     * Needed for tests. Returns the table's CRC check chance, which is only set for compressed tables.
+     */
+    double getCrcCheckChance();
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/io/util/Rebufferer.java b/src/java/org/apache/cassandra/io/util/Rebufferer.java
new file mode 100644
index 0000000..2fc7ffa
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/Rebufferer.java
@@ -0,0 +1,84 @@
+/*
+ * 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.io.util;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Rebufferer for reading data by a RandomAccessReader.
+ */
+public interface Rebufferer extends ReaderFileProxy
+{
+    /**
+     * Rebuffer (move on or seek to) a given position, and return a buffer that can be used there.
+     * The only guarantee about the size of the returned data is that unless rebuffering at the end of the file,
+     * the buffer will not be empty and will contain the requested position, i.e.
+     * {@code offset <= position < offset + bh.buffer().limit()}, but the buffer will not be positioned there.
+     */
+    BufferHolder rebuffer(long position);
+
+    /**
+     * Called when a reader is closed. Should clean up reader-specific data.
+     */
+    void closeReader();
+
+    interface BufferHolder
+    {
+        /**
+         * Returns a useable buffer (i.e. one whose position and limit can be freely modified). Its limit will be set
+         * to the size of the available data in the buffer.
+         * The buffer must be treated as read-only.
+         */
+        ByteBuffer buffer();
+
+        /**
+         * Position in the file of the start of the buffer.
+         */
+        long offset();
+
+        /**
+         * To be called when this buffer is no longer in use. Must be called for all BufferHolders, or ChunkCache
+         * will not be able to free blocks.
+         */
+        void release();
+    }
+
+    BufferHolder EMPTY = new BufferHolder()
+    {
+        final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
+
+        @Override
+        public ByteBuffer buffer()
+        {
+            return EMPTY_BUFFER;
+        }
+
+        @Override
+        public long offset()
+        {
+            return 0;
+        }
+
+        @Override
+        public void release()
+        {
+            // nothing to do
+        }
+    };
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/io/util/RebuffererFactory.java b/src/java/org/apache/cassandra/io/util/RebuffererFactory.java
new file mode 100644
index 0000000..ec35f0b
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/RebuffererFactory.java
@@ -0,0 +1,32 @@
+/*
+ * 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.io.util;
+
+/**
+ * Interface for the classes that can be used to instantiate rebufferers over a given file.
+ *
+ * These are one of two types:
+ *  - chunk sources (e.g. SimpleReadRebufferer) which instantiate a buffer managing rebufferer referencing
+ *    themselves.
+ *  - thread-safe shared rebufferers (e.g. MmapRebufferer) which directly return themselves.
+ */
+public interface RebuffererFactory extends ReaderFileProxy
+{
+    Rebufferer instantiateRebufferer();
+}
diff --git a/src/java/org/apache/cassandra/io/util/RebufferingInputStream.java b/src/java/org/apache/cassandra/io/util/RebufferingInputStream.java
index 15d0975..094115a 100644
--- a/src/java/org/apache/cassandra/io/util/RebufferingInputStream.java
+++ b/src/java/org/apache/cassandra/io/util/RebufferingInputStream.java
@@ -69,8 +69,8 @@
     }
 
     @Override
-    public int read(byte[] b, int off, int len) throws IOException {
-
+    public int read(byte[] b, int off, int len) throws IOException
+    {
         // avoid int overflow
         if (off < 0 || off > b.length || len < 0 || len > b.length - off)
             throw new IndexOutOfBoundsException();
@@ -275,16 +275,4 @@
             return -1;
         }
     }
-
-    @Override
-    public void reset() throws IOException
-    {
-        throw new IOException("mark/reset not supported");
-    }
-
-    @Override
-    public boolean markSupported()
-    {
-        return false;
-    }
 }
diff --git a/src/java/org/apache/cassandra/io/util/RewindableDataInputStreamPlus.java b/src/java/org/apache/cassandra/io/util/RewindableDataInputStreamPlus.java
index 3a680f4..a1842bc 100644
--- a/src/java/org/apache/cassandra/io/util/RewindableDataInputStreamPlus.java
+++ b/src/java/org/apache/cassandra/io/util/RewindableDataInputStreamPlus.java
@@ -33,19 +33,19 @@
  * Adds mark/reset functionality to another input stream by caching read bytes to a memory buffer and
  * spilling to disk if necessary.
  *
- * When the stream is marked via {@link this#mark()} or {@link this#mark(int)}, up to
+ * When the stream is marked via {@link #mark()} or {@link #mark(int)}, up to
  * <code>maxMemBufferSize</code> will be cached in memory (heap). If more than
  * <code>maxMemBufferSize</code> bytes are read while the stream is marked, the
  * following bytes are cached on the <code>spillFile</code> for up to <code>maxDiskBufferSize</code>.
  *
- * Please note that successive calls to {@link this#mark()} and {@link this#reset()} will write
+ * Please note that successive calls to {@link #mark()} and {@link #reset()} will write
  * sequentially to the same <code>spillFile</code> until <code>maxDiskBufferSize</code> is reached.
  * At this point, if less than <code>maxDiskBufferSize</code> bytes are currently cached on the
  * <code>spillFile</code>, the remaining bytes are written to the beginning of the file,
  * treating the <code>spillFile</code> as a circular buffer.
  *
  * If more than <code>maxMemBufferSize + maxDiskBufferSize</code> are cached while the stream is marked,
- * the following {@link this#reset()} invocation will throw a {@link IllegalStateException}.
+ * the following {@link #reset()} invocation will throw a {@link IllegalStateException}.
  *
  */
 public class RewindableDataInputStreamPlus extends FilterInputStream implements RewindableDataInput, Closeable
@@ -83,7 +83,7 @@
     /* RewindableDataInput methods */
 
     /**
-     * Marks the current position of a stream to return to this position later via the {@link this#reset(DataPosition)} method.
+     * Marks the current position of a stream to return to this position later via the {@link #reset(DataPosition)} method.
      * @return An empty @link{DataPosition} object
      */
     public DataPosition mark()
@@ -93,7 +93,7 @@
     }
 
     /**
-     * Rewinds to the previously marked position via the {@link this#mark()} method.
+     * Rewinds to the previously marked position via the {@link #mark()} method.
      * @param mark it's not possible to return to a custom position, so this parameter is ignored.
      * @throws IOException if an error ocurs while resetting
      */
@@ -121,7 +121,7 @@
 
     /**
      * Marks the current position of a stream to return to this position
-     * later via the {@link this#reset()} method.
+     * later via the {@link #reset()} method.
      * @param readlimit the maximum amount of bytes to cache
      */
     public synchronized void mark(int readlimit)
@@ -277,7 +277,7 @@
             if (len > 0 && diskTailAvailable > 0)
             {
                 int readFromTail = diskTailAvailable < len? diskTailAvailable : len;
-                getIfNotClosed(spillBuffer).read(b, off, readFromTail);
+                readFromTail = getIfNotClosed(spillBuffer).read(b, off, readFromTail);
                 readBytes += readFromTail;
                 diskTailAvailable -= readFromTail;
                 off += readFromTail;
@@ -288,7 +288,7 @@
             if (len > 0 && diskHeadAvailable > 0)
             {
                 int readFromHead = diskHeadAvailable < len? diskHeadAvailable : len;
-                getIfNotClosed(spillBuffer).read(b, off, readFromHead);
+                readFromHead = getIfNotClosed(spillBuffer).read(b, off, readFromHead);
                 readBytes += readFromHead;
                 diskHeadAvailable -= readFromHead;
                 off += readFromHead;
@@ -389,7 +389,7 @@
     {
         int newSize = Math.min(2 * (pos + writeSize), maxMemBufferSize);
         byte newBuffer[] = new byte[newSize];
-        System.arraycopy(memBuffer, 0, newBuffer, 0, (int)pos);
+        System.arraycopy(memBuffer, 0, newBuffer, 0, pos);
         memBuffer = newBuffer;
     }
 
@@ -438,7 +438,8 @@
         return skipped;
     }
 
-    private <T> T getIfNotClosed(T in) throws IOException {
+    private <T> T getIfNotClosed(T in) throws IOException
+    {
         if (closed.get())
             throw new IOException("Stream closed");
         return in;
@@ -476,7 +477,8 @@
             {
                 fail = merge(fail, e);
             }
-            try {
+            try
+            {
                 if (spillFile.exists())
                 {
                     spillFile.delete();
diff --git a/src/java/org/apache/cassandra/io/util/SafeMemory.java b/src/java/org/apache/cassandra/io/util/SafeMemory.java
index e8cd54f..4482d96 100644
--- a/src/java/org/apache/cassandra/io/util/SafeMemory.java
+++ b/src/java/org/apache/cassandra/io/util/SafeMemory.java
@@ -84,7 +84,7 @@
             this.size = size;
         }
 
-        public void tidy() throws Exception
+        public void tidy()
         {
             /** see {@link Memory#Memory(long)} re: null pointers*/
             if (peer != 0)
diff --git a/src/java/org/apache/cassandra/io/util/SegmentedFile.java b/src/java/org/apache/cassandra/io/util/SegmentedFile.java
deleted file mode 100644
index 9df4c81..0000000
--- a/src/java/org/apache/cassandra/io/util/SegmentedFile.java
+++ /dev/null
@@ -1,314 +0,0 @@
-/*
- * 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.io.util;
-
-import java.io.DataInput;
-import java.io.DataOutput;
-import java.io.File;
-import java.io.IOException;
-import java.util.function.Supplier;
-
-import com.google.common.util.concurrent.RateLimiter;
-
-import org.apache.cassandra.config.Config;
-import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.io.compress.CompressedSequentialWriter;
-import org.apache.cassandra.io.sstable.Component;
-import org.apache.cassandra.io.sstable.Descriptor;
-import org.apache.cassandra.io.sstable.IndexSummary;
-import org.apache.cassandra.io.sstable.IndexSummaryBuilder;
-import org.apache.cassandra.io.sstable.format.Version;
-import org.apache.cassandra.io.sstable.metadata.StatsMetadata;
-import org.apache.cassandra.utils.NativeLibrary;
-import org.apache.cassandra.utils.concurrent.RefCounted;
-import org.apache.cassandra.utils.concurrent.SharedCloseableImpl;
-
-import static org.apache.cassandra.utils.Throwables.maybeFail;
-
-/**
- * Abstracts a read-only file that has been split into segments, each of which can be represented by an independent
- * FileDataInput. Allows for iteration over the FileDataInputs, or random access to the FileDataInput for a given
- * position.
- *
- * The JVM can only map up to 2GB at a time, so each segment is at most that size when using mmap i/o. If a segment
- * would need to be longer than 2GB, that segment will not be mmap'd, and a new RandomAccessFile will be created for
- * each access to that segment.
- */
-public abstract class SegmentedFile extends SharedCloseableImpl
-{
-    public final ChannelProxy channel;
-    public final int bufferSize;
-    public final long length;
-
-    // This differs from length for compressed files (but we still need length for
-    // SegmentIterator because offsets in the file are relative to the uncompressed size)
-    public final long onDiskLength;
-
-    /**
-     * Use getBuilder to get a Builder to construct a SegmentedFile.
-     */
-    SegmentedFile(Cleanup cleanup, ChannelProxy channel, int bufferSize, long length)
-    {
-        this(cleanup, channel, bufferSize, length, length);
-    }
-
-    protected SegmentedFile(Cleanup cleanup, ChannelProxy channel, int bufferSize, long length, long onDiskLength)
-    {
-        super(cleanup);
-        this.channel = channel;
-        this.bufferSize = bufferSize;
-        this.length = length;
-        this.onDiskLength = onDiskLength;
-    }
-
-    protected SegmentedFile(SegmentedFile copy)
-    {
-        super(copy);
-        channel = copy.channel;
-        bufferSize = copy.bufferSize;
-        length = copy.length;
-        onDiskLength = copy.onDiskLength;
-    }
-
-    public String path()
-    {
-        return channel.filePath();
-    }
-
-    protected static class Cleanup implements RefCounted.Tidy
-    {
-        final ChannelProxy channel;
-        protected Cleanup(ChannelProxy channel)
-        {
-            this.channel = channel;
-        }
-
-        public String name()
-        {
-            return channel.filePath();
-        }
-
-        public void tidy()
-        {
-            channel.close();
-        }
-    }
-
-    public abstract SegmentedFile sharedCopy();
-
-    public RandomAccessReader createReader()
-    {
-        return new RandomAccessReader.Builder(channel)
-               .overrideLength(length)
-               .bufferSize(bufferSize)
-               .build();
-    }
-
-    public RandomAccessReader createReader(RateLimiter limiter)
-    {
-        return new RandomAccessReader.Builder(channel)
-               .overrideLength(length)
-               .bufferSize(bufferSize)
-               .limiter(limiter)
-               .build();
-    }
-
-    public FileDataInput createReader(long position)
-    {
-        RandomAccessReader reader = createReader();
-        reader.seek(position);
-        return reader;
-    }
-
-    public void dropPageCache(long before)
-    {
-        NativeLibrary.trySkipCache(channel.getFileDescriptor(), 0, before, path());
-    }
-
-    /**
-     * @return A SegmentedFile.Builder.
-     */
-    public static Builder getBuilder(Config.DiskAccessMode mode, boolean compressed)
-    {
-        return compressed ? new CompressedSegmentedFile.Builder(null)
-                          : mode == Config.DiskAccessMode.mmap ? new MmappedSegmentedFile.Builder()
-                                                               : new BufferedSegmentedFile.Builder();
-    }
-
-    public static Builder getCompressedBuilder(CompressedSequentialWriter writer)
-    {
-        return new CompressedSegmentedFile.Builder(writer);
-    }
-
-    /**
-     * Collects potential segmentation points in an underlying file, and builds a SegmentedFile to represent it.
-     */
-    public static abstract class Builder implements AutoCloseable
-    {
-        private ChannelProxy channel;
-
-        /**
-         * Called after all potential boundaries have been added to apply this Builder to a concrete file on disk.
-         * @param channel The channel to the file on disk.
-         */
-        protected abstract SegmentedFile complete(ChannelProxy channel, int bufferSize, long overrideLength);
-
-        @SuppressWarnings("resource") // SegmentedFile owns channel
-        private SegmentedFile complete(String path, int bufferSize, long overrideLength)
-        {
-            ChannelProxy channelCopy = getChannel(path);
-            try
-            {
-                return complete(channelCopy, bufferSize, overrideLength);
-            }
-            catch (Throwable t)
-            {
-                channelCopy.close();
-                throw t;
-            }
-        }
-
-        public SegmentedFile buildData(Descriptor desc, StatsMetadata stats, IndexSummaryBuilder.ReadableBoundary boundary)
-        {
-            return complete(desc.filenameFor(Component.DATA), bufferSize(stats), boundary.dataLength);
-        }
-
-        public SegmentedFile buildData(Descriptor desc, StatsMetadata stats)
-        {
-            return complete(desc.filenameFor(Component.DATA), bufferSize(stats), -1L);
-        }
-
-        public SegmentedFile buildIndex(Descriptor desc, IndexSummary indexSummary, IndexSummaryBuilder.ReadableBoundary boundary)
-        {
-            return complete(desc.filenameFor(Component.PRIMARY_INDEX), bufferSize(desc, indexSummary), boundary.indexLength);
-        }
-
-        public SegmentedFile buildIndex(Descriptor desc, IndexSummary indexSummary)
-        {
-            return complete(desc.filenameFor(Component.PRIMARY_INDEX), bufferSize(desc, indexSummary), -1L);
-        }
-
-        private static int bufferSize(StatsMetadata stats)
-        {
-            return bufferSize(stats.estimatedPartitionSize.percentile(DatabaseDescriptor.getDiskOptimizationEstimatePercentile()));
-        }
-
-        private static int bufferSize(Descriptor desc, IndexSummary indexSummary)
-        {
-            File file = new File(desc.filenameFor(Component.PRIMARY_INDEX));
-            return bufferSize(file.length() / indexSummary.size());
-        }
-
-        /**
-            Return the buffer size for a given record size. For spinning disks always add one page.
-            For solid state disks only add one page if the chance of crossing to the next page is more
-            than a predifined value, @see Config.disk_optimization_page_cross_chance.
-         */
-        static int bufferSize(long recordSize)
-        {
-            Config.DiskOptimizationStrategy strategy = DatabaseDescriptor.getDiskOptimizationStrategy();
-            if (strategy == Config.DiskOptimizationStrategy.ssd)
-            {
-                // The crossing probability is calculated assuming a uniform distribution of record
-                // start position in a page, so it's the record size modulo the page size divided by
-                // the total page size.
-                double pageCrossProbability = (recordSize % 4096) / 4096.;
-                // if the page cross probability is equal or bigger than disk_optimization_page_cross_chance we add one page
-                if ((pageCrossProbability - DatabaseDescriptor.getDiskOptimizationPageCrossChance()) > -1e-16)
-                    recordSize += 4096;
-
-                return roundBufferSize(recordSize);
-            }
-            else if (strategy == Config.DiskOptimizationStrategy.spinning)
-            {
-                return roundBufferSize(recordSize + 4096);
-            }
-            else
-            {
-                throw new IllegalStateException("Unsupported disk optimization strategy: " + strategy);
-            }
-        }
-
-        /**
-           Round up to the next multiple of 4k but no more than 64k
-         */
-        static int roundBufferSize(long size)
-        {
-            if (size <= 0)
-                return 4096;
-
-            size = (size + 4095) & ~4095;
-            return (int)Math.min(size, 1 << 16);
-        }
-
-        public void serializeBounds(DataOutput out, Version version) throws IOException
-        {
-            if (!version.hasBoundaries())
-                return;
-
-            out.writeUTF(DatabaseDescriptor.getDiskAccessMode().name());
-        }
-
-        public void deserializeBounds(DataInput in, Version version) throws IOException
-        {
-            if (!version.hasBoundaries())
-                return;
-
-            if (!in.readUTF().equals(DatabaseDescriptor.getDiskAccessMode().name()))
-                throw new IOException("Cannot deserialize SSTable Summary component because the DiskAccessMode was changed!");
-        }
-
-        public Throwable close(Throwable accumulate)
-        {
-            if (channel != null)
-                return channel.close(accumulate);
-
-            return accumulate;
-        }
-
-        public void close()
-        {
-            maybeFail(close(null));
-        }
-
-        private ChannelProxy getChannel(String path)
-        {
-            if (channel != null)
-            {
-                // This is really fragile, both path and channel.filePath()
-                // must agree, i.e. they both must be absolute or both relative
-                // eventually we should really pass the filePath to the builder
-                // constructor and remove this
-                if (channel.filePath().equals(path))
-                    return channel.sharedCopy();
-                else
-                    channel.close();
-            }
-
-            channel = new ChannelProxy(path);
-            return channel.sharedCopy();
-        }
-    }
-
-    @Override
-    public String toString() {
-        return getClass().getSimpleName() + "(path='" + path() + '\'' +
-               ", length=" + length +
-               ')';
-}
-}
diff --git a/src/java/org/apache/cassandra/io/util/SequentialWriter.java b/src/java/org/apache/cassandra/io/util/SequentialWriter.java
index d17ac34..e71f2fa 100644
--- a/src/java/org/apache/cassandra/io/util/SequentialWriter.java
+++ b/src/java/org/apache/cassandra/io/util/SequentialWriter.java
@@ -17,32 +17,24 @@
  */
 package org.apache.cassandra.io.util;
 
-import java.io.*;
+import java.io.File;
+import java.io.IOException;
 import java.nio.channels.FileChannel;
 import java.nio.file.StandardOpenOption;
 
-import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.io.FSReadError;
 import org.apache.cassandra.io.FSWriteError;
-import org.apache.cassandra.io.compress.BufferType;
-import org.apache.cassandra.io.compress.CompressedSequentialWriter;
-import org.apache.cassandra.schema.CompressionParams;
-import org.apache.cassandra.io.sstable.Descriptor;
-import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
+import org.apache.cassandra.utils.SyncUtil;
 import org.apache.cassandra.utils.concurrent.Transactional;
 
 import static org.apache.cassandra.utils.Throwables.merge;
 
-import org.apache.cassandra.utils.SyncUtil;
-
 /**
  * Adds buffering, mark, and fsyncing to OutputStream.  We always fsync on close; we may also
  * fsync incrementally if Config.trickle_fsync is enabled.
  */
 public class SequentialWriter extends BufferedDataOutputStreamPlus implements Transactional
 {
-    private static final int DEFAULT_BUFFER_SIZE = 64 * 1024;
-
     // absolute path to the given file
     private final String filePath;
 
@@ -53,8 +45,7 @@
 
     // whether to do trickling fsync() to avoid sudden bursts of dirty buffer flushing by kernel causing read
     // latency spikes
-    private boolean trickleFsync;
-    private int trickleFsyncByteInterval;
+    private final SequentialWriterOption option;
     private int bytesSinceTrickleFsync = 0;
 
     protected long lastFlushOffset;
@@ -62,8 +53,6 @@
     protected Runnable runPostFlush;
 
     private final TransactionalProxy txnProxy = txnProxy();
-    private boolean finishOnClose;
-    protected Descriptor descriptor;
 
     // due to lack of multiple-inheritance, we proxy our transactional implementation
     protected class TransactionalProxy extends AbstractTransactional
@@ -102,7 +91,8 @@
     }
 
     // TODO: we should specify as a parameter if we permit an existing file or not
-    private static FileChannel openChannel(File file) {
+    private static FileChannel openChannel(File file)
+    {
         try
         {
             if (file.exists())
@@ -130,43 +120,38 @@
         }
     }
 
-    public SequentialWriter(File file, int bufferSize, BufferType bufferType)
+    /**
+     * Create heap-based, non-compressed SequenialWriter with default buffer size(64k).
+     *
+     * @param file File to write
+     */
+    public SequentialWriter(File file)
     {
-        super(openChannel(file), bufferType.allocate(bufferSize));
+       this(file, SequentialWriterOption.DEFAULT);
+    }
+
+    /**
+     * Create SequentialWriter for given file with specific writer option.
+     *
+     * @param file File to write
+     * @param option Writer option
+     */
+    public SequentialWriter(File file, SequentialWriterOption option)
+    {
+        super(openChannel(file), option.allocateBuffer());
         strictFlushing = true;
         fchannel = (FileChannel)channel;
 
         filePath = file.getAbsolutePath();
 
-        this.trickleFsync = DatabaseDescriptor.getTrickleFsync();
-        this.trickleFsyncByteInterval = DatabaseDescriptor.getTrickleFsyncIntervalInKb() * 1024;
+        this.option = option;
     }
 
-    /**
-     * Open a heap-based, non-compressed SequentialWriter
-     */
-    public static SequentialWriter open(File file)
+    public void skipBytes(int numBytes) throws IOException
     {
-        return new SequentialWriter(file, DEFAULT_BUFFER_SIZE, BufferType.ON_HEAP);
-    }
-
-    public static ChecksummedSequentialWriter open(File file, File crcPath)
-    {
-        return new ChecksummedSequentialWriter(file, DEFAULT_BUFFER_SIZE, crcPath);
-    }
-
-    public static CompressedSequentialWriter open(String dataFilePath,
-                                                  String offsetsPath,
-                                                  CompressionParams parameters,
-                                                  MetadataCollector sstableMetadataCollector)
-    {
-        return new CompressedSequentialWriter(new File(dataFilePath), offsetsPath, parameters, sstableMetadataCollector);
-    }
-
-    public SequentialWriter finishOnClose()
-    {
-        finishOnClose = true;
-        return this;
+        flush();
+        fchannel.position(fchannel.position() + numBytes);
+        bufferOffset = fchannel.position();
     }
 
     /**
@@ -205,10 +190,10 @@
     {
         flushData();
 
-        if (trickleFsync)
+        if (option.trickleFsync())
         {
             bytesSinceTrickleFsync += buffer.position();
-            if (bytesSinceTrickleFsync >= trickleFsyncByteInterval)
+            if (bytesSinceTrickleFsync >= option.trickleFsyncByteInterval())
             {
                 syncDataOnlyInternal();
                 bytesSinceTrickleFsync = 0;
@@ -269,6 +254,11 @@
         return position();
     }
 
+    public long getEstimatedOnDiskBytesWritten()
+    {
+        return getOnDiskFilePointer();
+    }
+
     public long length()
     {
         try
@@ -363,12 +353,6 @@
         return channel.isOpen();
     }
 
-    public SequentialWriter setDescriptor(Descriptor descriptor)
-    {
-        this.descriptor = descriptor;
-        return this;
-    }
-
     public final void prepareToCommit()
     {
         txnProxy.prepareToCommit();
@@ -387,7 +371,7 @@
     @Override
     public final void close()
     {
-        if (finishOnClose)
+        if (option.finishOnClose())
             txnProxy.finish();
         else
             txnProxy.close();
diff --git a/src/java/org/apache/cassandra/io/util/SequentialWriterOption.java b/src/java/org/apache/cassandra/io/util/SequentialWriterOption.java
new file mode 100644
index 0000000..61f375b
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/SequentialWriterOption.java
@@ -0,0 +1,154 @@
+/*
+ * 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.io.util;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+import org.apache.cassandra.io.compress.BufferType;
+
+/**
+ * SequentialWriter option
+ */
+public class SequentialWriterOption
+{
+    /**
+     * Default write option.
+     *
+     * <ul>
+     *   <li>buffer size: 64 KB
+     *   <li>buffer type: on heap
+     *   <li>trickle fsync: false
+     *   <li>trickle fsync byte interval: 10 MB
+     *   <li>finish on close: false
+     * </ul>
+     */
+    public static final SequentialWriterOption DEFAULT = SequentialWriterOption.newBuilder().build();
+
+    private final int bufferSize;
+    private final BufferType bufferType;
+    private final boolean trickleFsync;
+    private final int trickleFsyncByteInterval;
+    private final boolean finishOnClose;
+
+    private SequentialWriterOption(int bufferSize,
+                                   BufferType bufferType,
+                                   boolean trickleFsync,
+                                   int trickleFsyncByteInterval,
+                                   boolean finishOnClose)
+    {
+        this.bufferSize = bufferSize;
+        this.bufferType = bufferType;
+        this.trickleFsync = trickleFsync;
+        this.trickleFsyncByteInterval = trickleFsyncByteInterval;
+        this.finishOnClose = finishOnClose;
+    }
+
+    public static Builder newBuilder()
+    {
+        return new Builder();
+    }
+
+    public int bufferSize()
+    {
+        return bufferSize;
+    }
+
+    public BufferType bufferType()
+    {
+        return bufferType;
+    }
+
+    public boolean trickleFsync()
+    {
+        return trickleFsync;
+    }
+
+    public int trickleFsyncByteInterval()
+    {
+        return trickleFsyncByteInterval;
+    }
+
+    public boolean finishOnClose()
+    {
+        return finishOnClose;
+    }
+
+    /**
+     * Allocate buffer using set buffer type and buffer size.
+     *
+     * @return allocated ByteBuffer
+     */
+    public ByteBuffer allocateBuffer()
+    {
+        return bufferType.allocate(bufferSize);
+    }
+
+    public static class Builder
+    {
+        /* default buffer size: 64k */
+        private int bufferSize = 64 * 1024;
+        /* default buffer type: on heap */
+        private BufferType bufferType = BufferType.ON_HEAP;
+        /* default: no trickle fsync */
+        private boolean trickleFsync = false;
+        /* default tricle fsync byte interval: 10MB */
+        private int trickleFsyncByteInterval = 10 * 1024 * 1024;
+        private boolean finishOnClose = false;
+
+        /* construct throguh SequentialWriteOption.newBuilder */
+        private Builder() {}
+
+        public SequentialWriterOption build()
+        {
+            return new SequentialWriterOption(bufferSize, bufferType, trickleFsync,
+                                   trickleFsyncByteInterval, finishOnClose);
+        }
+
+        public Builder bufferSize(int bufferSize)
+        {
+            this.bufferSize = bufferSize;
+            return this;
+        }
+
+        public Builder bufferType(BufferType bufferType)
+        {
+            this.bufferType = Objects.requireNonNull(bufferType);
+            return this;
+        }
+
+        public Builder trickleFsync(boolean trickleFsync)
+        {
+            this.trickleFsync = trickleFsync;
+            return this;
+        }
+
+        public Builder trickleFsyncByteInterval(int trickleFsyncByteInterval)
+        {
+            this.trickleFsyncByteInterval = trickleFsyncByteInterval;
+            return this;
+        }
+
+        public Builder finishOnClose(boolean finishOnClose)
+        {
+            this.finishOnClose = finishOnClose;
+            return this;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/util/SimpleCachedBufferPool.java b/src/java/org/apache/cassandra/io/util/SimpleCachedBufferPool.java
new file mode 100644
index 0000000..b7550fa
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/SimpleCachedBufferPool.java
@@ -0,0 +1,132 @@
+/*
+ * 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.io.util;
+
+import java.nio.ByteBuffer;
+import java.util.Queue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.cassandra.io.compress.BufferType;
+import org.jctools.queues.MpmcArrayQueue;
+
+/**
+ * A very simple Bytebuffer pool with a fixed allocation size and a cached max allocation count. Will allow
+ * you to go past the "max", freeing all buffers allocated beyond the max buffer count on release.
+ *
+ * Has a reusable thread local ByteBuffer that users can make use of.
+ */
+public class SimpleCachedBufferPool
+{
+    private final ThreadLocalByteBufferHolder bufferHolder;
+
+    private final Queue<ByteBuffer> bufferPool;
+
+    /**
+     * The number of buffers currently used.
+     */
+    private AtomicInteger usedBuffers = new AtomicInteger(0);
+
+    /**
+     * Maximum number of buffers in the compression pool. Any buffers above this count that are allocated will be cleaned
+     * upon release rather than held and re-used.
+     */
+    private final int maxBufferPoolSize;
+
+    /**
+     * Size of individual buffer segments on allocation.
+     */
+    private final int bufferSize;
+
+    private final BufferType preferredReusableBufferType;
+
+    public SimpleCachedBufferPool(int maxBufferPoolSize, int bufferSize, BufferType preferredReusableBufferType)
+    {
+        // We want to use a bounded queue to ensure that we do not pool more buffers than maxBufferPoolSize
+        this.bufferPool = new MpmcArrayQueue<>(maxBufferPoolSize);
+        this.maxBufferPoolSize = maxBufferPoolSize;
+        this.bufferSize = bufferSize;
+        this.preferredReusableBufferType = preferredReusableBufferType;
+        this.bufferHolder = new ThreadLocalByteBufferHolder(preferredReusableBufferType);
+    }
+
+    public ByteBuffer createBuffer()
+    {
+        usedBuffers.incrementAndGet();
+        ByteBuffer buf = bufferPool.poll();
+        if (buf != null)
+        {
+            buf.clear();
+            return buf;
+        }
+        return preferredReusableBufferType.allocate(bufferSize);
+    }
+
+    public ByteBuffer getThreadLocalReusableBuffer(int size)
+    {
+        return bufferHolder.getBuffer(size);
+    }
+
+    public void releaseBuffer(ByteBuffer buffer)
+    {
+        assert buffer != null;
+        assert preferredReusableBufferType == BufferType.typeOf(buffer);
+
+        usedBuffers.decrementAndGet();
+
+        // We use a bounded queue. By consequence if we have reached the maximum size for the buffer pool
+        // offer will return false and we know that we can simply get rid of the buffer.
+        if (!bufferPool.offer(buffer))
+            FileUtils.clean(buffer);
+    }
+
+    /**
+     * Empties the buffer pool.
+     */
+    public void emptyBufferPool()
+    {
+        ByteBuffer buffer = bufferPool.poll();
+        while(buffer != null)
+        {
+            FileUtils.clean(buffer);
+            buffer = bufferPool.poll();
+        }
+    }
+
+    /**
+     * Checks if the number of used buffers has exceeded the maximum number of cached buffers.
+     *
+     * @return {@code true} if the number of used buffers has exceeded the maximum number of cached buffers,
+     * {@code false} otherwise.
+     */
+    public boolean atLimit()
+    {
+        return usedBuffers.get() >= maxBufferPoolSize;
+    }
+
+    @Override
+    public String toString()
+    {
+        return new StringBuilder()
+               .append("SimpleBufferPool:")
+               .append(" usedBuffers:").append(usedBuffers.get())
+               .append(", maxBufferPoolSize:").append(maxBufferPoolSize)
+               .append(", bufferSize:").append(bufferSize)
+               .toString();
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/util/SimpleChunkReader.java b/src/java/org/apache/cassandra/io/util/SimpleChunkReader.java
new file mode 100644
index 0000000..bc1a529
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/SimpleChunkReader.java
@@ -0,0 +1,72 @@
+/*
+ * 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.io.util;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.io.compress.BufferType;
+
+class SimpleChunkReader extends AbstractReaderFileProxy implements ChunkReader
+{
+    private final int bufferSize;
+    private final BufferType bufferType;
+
+    SimpleChunkReader(ChannelProxy channel, long fileLength, BufferType bufferType, int bufferSize)
+    {
+        super(channel, fileLength);
+        this.bufferSize = bufferSize;
+        this.bufferType = bufferType;
+    }
+
+    @Override
+    public void readChunk(long position, ByteBuffer buffer)
+    {
+        buffer.clear();
+        channel.read(buffer, position);
+        buffer.flip();
+    }
+
+    @Override
+    public int chunkSize()
+    {
+        return bufferSize;
+    }
+
+    @Override
+    public BufferType preferredBufferType()
+    {
+        return bufferType;
+    }
+
+    @Override
+    public Rebufferer instantiateRebufferer()
+    {
+        return new BufferManagingRebufferer.Unaligned(this);
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.format("%s(%s - chunk length %d, data length %d)",
+                             getClass().getSimpleName(),
+                             channel.filePath(),
+                             bufferSize,
+                             fileLength());
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/io/util/SpinningDiskOptimizationStrategy.java b/src/java/org/apache/cassandra/io/util/SpinningDiskOptimizationStrategy.java
new file mode 100644
index 0000000..5cec282
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/SpinningDiskOptimizationStrategy.java
@@ -0,0 +1,31 @@
+/*
+ * 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.io.util;
+
+public class SpinningDiskOptimizationStrategy implements DiskOptimizationStrategy
+{
+    /**
+     * For spinning disks always add one page.
+     */
+    @Override
+    public int bufferSize(long recordSize)
+    {
+        return roundBufferSize(recordSize + 4096);
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/util/SsdDiskOptimizationStrategy.java b/src/java/org/apache/cassandra/io/util/SsdDiskOptimizationStrategy.java
new file mode 100644
index 0000000..032ec2b
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/SsdDiskOptimizationStrategy.java
@@ -0,0 +1,49 @@
+/*
+ * 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.io.util;
+
+public class SsdDiskOptimizationStrategy implements DiskOptimizationStrategy
+{
+    private final double diskOptimizationPageCrossChance;
+
+    public SsdDiskOptimizationStrategy(double diskOptimizationPageCrossChance)
+    {
+        this.diskOptimizationPageCrossChance = diskOptimizationPageCrossChance;
+    }
+
+    /**
+     * For solid state disks only add one page if the chance of crossing to the next page is more
+     * than a predifined value.
+     *
+     * @see org.apache.cassandra.config.Config#disk_optimization_page_cross_chance
+     */
+    @Override
+    public int bufferSize(long recordSize)
+    {
+        // The crossing probability is calculated assuming a uniform distribution of record
+        // start position in a page, so it's the record size modulo the page size divided by
+        // the total page size.
+        double pageCrossProbability = (recordSize % 4096) / 4096.;
+        // if the page cross probability is equal or bigger than disk_optimization_page_cross_chance we add one page
+        if ((pageCrossProbability - diskOptimizationPageCrossChance) > -1e-16)
+            recordSize += 4096;
+
+        return roundBufferSize(recordSize);
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/util/ThreadLocalByteBufferHolder.java b/src/java/org/apache/cassandra/io/util/ThreadLocalByteBufferHolder.java
new file mode 100644
index 0000000..392edba
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/ThreadLocalByteBufferHolder.java
@@ -0,0 +1,83 @@
+/*
+ * 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.io.util;
+
+import java.nio.ByteBuffer;
+import java.util.EnumMap;
+
+import io.netty.util.concurrent.FastThreadLocal;
+
+import org.apache.cassandra.io.compress.BufferType;
+
+/**
+ * Utility class that allow buffers to be reused by storing them in a thread local instance.
+ */
+public final class ThreadLocalByteBufferHolder
+{
+    private static final EnumMap<BufferType, FastThreadLocal<ByteBuffer>> reusableBBHolder = new EnumMap<>(BufferType.class);
+    // Convenience variable holding a ref to the current resuableBB to avoid map lookups
+    private final FastThreadLocal<ByteBuffer> reusableBB;
+
+    static
+    {
+        for (BufferType bbType : BufferType.values())
+        {
+            reusableBBHolder.put(bbType, new FastThreadLocal<ByteBuffer>()
+            {
+                protected ByteBuffer initialValue()
+                {
+                    return ByteBuffer.allocate(0);
+                }
+            });
+        }
+    };
+
+    /**
+     * The type of buffer that will be returned
+     */
+    private final BufferType bufferType;
+
+    public ThreadLocalByteBufferHolder(BufferType bufferType)
+    {
+        this.bufferType = bufferType;
+        this.reusableBB = reusableBBHolder.get(bufferType);
+    }
+
+    /**
+     * Returns the buffer for the current thread.
+     *
+     * <p>If the buffer for the current thread does not have a capacity large enough. A new buffer with the requested
+     *  size will be instatiated an will replace the existing one.</p>
+     *
+     * @param size the buffer size
+     * @return the buffer for the current thread.
+     */
+    public ByteBuffer getBuffer(int size)
+    {
+        ByteBuffer buffer = reusableBB.get();
+        if (buffer.capacity() < size)
+        {
+            FileUtils.clean(buffer);
+            buffer = bufferType.allocate(size);
+            reusableBB.set(buffer);
+        }
+        buffer.clear().limit(size);
+        return buffer;
+    }
+}
diff --git a/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java b/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java
index 709de20..94f4b70 100644
--- a/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java
+++ b/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java
@@ -114,21 +114,22 @@
     public abstract List<InetAddress> calculateNaturalEndpoints(Token searchToken, TokenMetadata tokenMetadata);
 
     public <T> AbstractWriteResponseHandler<T> getWriteResponseHandler(Collection<InetAddress> naturalEndpoints,
-                                                                Collection<InetAddress> pendingEndpoints,
-                                                                ConsistencyLevel consistency_level,
-                                                                Runnable callback,
-                                                                WriteType writeType)
+                                                                       Collection<InetAddress> pendingEndpoints,
+                                                                       ConsistencyLevel consistency_level,
+                                                                       Runnable callback,
+                                                                       WriteType writeType,
+                                                                       long queryStartNanoTime)
     {
         if (consistency_level.isDatacenterLocal())
         {
             // block for in this context will be localnodes block.
-            return new DatacenterWriteResponseHandler<T>(naturalEndpoints, pendingEndpoints, consistency_level, getKeyspace(), callback, writeType);
+            return new DatacenterWriteResponseHandler<T>(naturalEndpoints, pendingEndpoints, consistency_level, getKeyspace(), callback, writeType, queryStartNanoTime);
         }
         else if (consistency_level == ConsistencyLevel.EACH_QUORUM && (this instanceof NetworkTopologyStrategy))
         {
-            return new DatacenterSyncWriteResponseHandler<T>(naturalEndpoints, pendingEndpoints, consistency_level, getKeyspace(), callback, writeType);
+            return new DatacenterSyncWriteResponseHandler<T>(naturalEndpoints, pendingEndpoints, consistency_level, getKeyspace(), callback, writeType, queryStartNanoTime);
         }
-        return new WriteResponseHandler<T>(naturalEndpoints, pendingEndpoints, consistency_level, getKeyspace(), callback, writeType);
+        return new WriteResponseHandler<T>(naturalEndpoints, pendingEndpoints, consistency_level, getKeyspace(), callback, writeType, queryStartNanoTime);
     }
 
     private Keyspace getKeyspace()
@@ -203,6 +204,8 @@
 
     public abstract void validateOptions() throws ConfigurationException;
 
+    public abstract void maybeWarnOnOptions();
+
     /*
      * The options recognized by the strategy.
      * The empty collection means that no options are accepted, but null means
@@ -271,6 +274,7 @@
         AbstractReplicationStrategy strategy = createInternal(keyspaceName, strategyClass, tokenMetadata, snitch, strategyOptions);
         strategy.validateExpectedOptions();
         strategy.validateOptions();
+        strategy.maybeWarnOnOptions();
     }
 
     public static Class<AbstractReplicationStrategy> getClass(String cls) throws ConfigurationException
diff --git a/src/java/org/apache/cassandra/locator/CloudstackSnitch.java b/src/java/org/apache/cassandra/locator/CloudstackSnitch.java
index 88c62e9..ec2e87e 100644
--- a/src/java/org/apache/cassandra/locator/CloudstackSnitch.java
+++ b/src/java/org/apache/cassandra/locator/CloudstackSnitch.java
@@ -60,7 +60,7 @@
 
     private static final String DEFAULT_DC = "UNKNOWN-DC";
     private static final String DEFAULT_RACK = "UNKNOWN-RACK";
-    private static final String[] LEASE_FILES = 
+    private static final String[] LEASE_FILES =
     {
         "file:///var/lib/dhcp/dhclient.eth0.leases",
         "file:///var/lib/dhclient/dhclient.eth0.leases"
@@ -75,7 +75,7 @@
         String zone = csQueryMetadata(endpoint + ZONE_NAME_QUERY_URI);
         String zone_parts[] = zone.split("-");
 
-        if (zone_parts.length != 3) 
+        if (zone_parts.length != 3)
         {
             throw new ConfigurationException("CloudstackSnitch cannot handle invalid zone format: " + zone);
         }
@@ -88,7 +88,7 @@
         if (endpoint.equals(FBUtilities.getBroadcastAddress()))
             return csZoneRack;
         EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint);
-        if (state == null || state.getApplicationState(ApplicationState.RACK) == null) 
+        if (state == null || state.getApplicationState(ApplicationState.RACK) == null)
         {
             if (savedEndpoints == null)
                 savedEndpoints = SystemKeyspace.loadDcRackInfo();
@@ -104,7 +104,7 @@
         if (endpoint.equals(FBUtilities.getBroadcastAddress()))
             return csZoneDc;
         EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint);
-        if (state == null || state.getApplicationState(ApplicationState.DC) == null) 
+        if (state == null || state.getApplicationState(ApplicationState.DC) == null)
         {
             if (savedEndpoints == null)
                 savedEndpoints = SystemKeyspace.loadDcRackInfo();
@@ -120,18 +120,18 @@
         HttpURLConnection conn = null;
         DataInputStream is = null;
 
-        try 
+        try
         {
             conn = (HttpURLConnection) new URL(url).openConnection();
-        } 
-        catch (Exception e) 
+        }
+        catch (Exception e)
         {
             throw new ConfigurationException("CloudstackSnitch cannot query wrong metadata URL: " + url);
         }
-        try 
+        try
         {
             conn.setRequestMethod("GET");
-            if (conn.getResponseCode() != 200) 
+            if (conn.getResponseCode() != 200)
             {
                 throw new ConfigurationException("CloudstackSnitch was unable to query metadata.");
             }
@@ -141,8 +141,8 @@
             is = new DataInputStream(new BufferedInputStream(conn.getInputStream()));
             is.readFully(b);
             return new String(b, StandardCharsets.UTF_8);
-        } 
-        finally 
+        }
+        finally
         {
             FileUtils.close(is);
             conn.disconnect();
@@ -151,17 +151,17 @@
 
     String csMetadataEndpoint() throws ConfigurationException
     {
-        for (String lease_uri: LEASE_FILES) 
+        for (String lease_uri: LEASE_FILES)
         {
-            try 
+            try
             {
                 File lease_file = new File(new URI(lease_uri));
-                if (lease_file.exists()) 
+                if (lease_file.exists())
                 {
                     return csEndpointFromLease(lease_file);
                 }
-            } 
-            catch (Exception e) 
+            }
+            catch (Exception e)
             {
                 JVMStabilityInspector.inspectThrowable(e);
                 continue;
@@ -180,24 +180,24 @@
 
         try (BufferedReader reader = new BufferedReader(new FileReader(lease)))
         {
-            
-            while ((line = reader.readLine()) != null) 
+
+            while ((line = reader.readLine()) != null)
             {
                 Matcher matcher = identifierPattern.matcher(line);
 
-                if (matcher.find()) 
+                if (matcher.find())
                 {
                     endpoint = matcher.group(1);
                     break;
                 }
             }
         }
-        catch (Exception e)  
+        catch (Exception e)
         {
             throw new ConfigurationException("CloudstackSnitch cannot access lease file.");
         }
 
-        if (endpoint == null) 
+        if (endpoint == null)
         {
             throw new ConfigurationException("No metadata server could be found in lease file.");
         }
diff --git a/src/java/org/apache/cassandra/locator/DynamicEndpointSnitch.java b/src/java/org/apache/cassandra/locator/DynamicEndpointSnitch.java
index 542677b..53e4d1d 100644
--- a/src/java/org/apache/cassandra/locator/DynamicEndpointSnitch.java
+++ b/src/java/org/apache/cassandra/locator/DynamicEndpointSnitch.java
@@ -1,4 +1,4 @@
-/**
+/*
  * 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
@@ -22,6 +22,7 @@
 import java.net.UnknownHostException;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
 import com.codahale.metrics.ExponentiallyDecayingReservoir;
@@ -29,6 +30,10 @@
 import com.codahale.metrics.Snapshot;
 import org.apache.cassandra.concurrent.ScheduledExecutors;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.gms.ApplicationState;
+import org.apache.cassandra.gms.EndpointState;
+import org.apache.cassandra.gms.Gossiper;
+import org.apache.cassandra.gms.VersionedValue;
 import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.utils.FBUtilities;
@@ -45,13 +50,13 @@
     private static final double ALPHA = 0.75; // set to 0.75 to make EDS more biased to towards the newer values
     private static final int WINDOW_SIZE = 100;
 
-    private final int UPDATE_INTERVAL_IN_MS = DatabaseDescriptor.getDynamicUpdateInterval();
-    private final int RESET_INTERVAL_IN_MS = DatabaseDescriptor.getDynamicResetInterval();
-    private final double BADNESS_THRESHOLD = DatabaseDescriptor.getDynamicBadnessThreshold();
+    private volatile int dynamicUpdateInterval = DatabaseDescriptor.getDynamicUpdateInterval();
+    private volatile int dynamicResetInterval = DatabaseDescriptor.getDynamicResetInterval();
+    private volatile double dynamicBadnessThreshold = DatabaseDescriptor.getDynamicBadnessThreshold();
 
     // the score for a merged set of endpoints must be this much worse than the score for separate endpoints to
     // warrant not merging two ranges into a single range
-    private double RANGE_MERGING_PREFERENCE = 1.5;
+    private static final double RANGE_MERGING_PREFERENCE = 1.5;
 
     private String mbeanName;
     private boolean registered = false;
@@ -61,24 +66,31 @@
 
     public final IEndpointSnitch subsnitch;
 
+    private volatile ScheduledFuture<?> updateSchedular;
+    private volatile ScheduledFuture<?> resetSchedular;
+
+    private final Runnable update;
+    private final Runnable reset;
+
     public DynamicEndpointSnitch(IEndpointSnitch snitch)
     {
         this(snitch, null);
     }
+
     public DynamicEndpointSnitch(IEndpointSnitch snitch, String instance)
     {
         mbeanName = "org.apache.cassandra.db:type=DynamicEndpointSnitch";
         if (instance != null)
             mbeanName += ",instance=" + instance;
         subsnitch = snitch;
-        Runnable update = new Runnable()
+        update = new Runnable()
         {
             public void run()
             {
                 updateScores();
             }
         };
-        Runnable reset = new Runnable()
+        reset = new Runnable()
         {
             public void run()
             {
@@ -87,18 +99,54 @@
                 reset();
             }
         };
-        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(update, UPDATE_INTERVAL_IN_MS, UPDATE_INTERVAL_IN_MS, TimeUnit.MILLISECONDS);
-        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(reset, RESET_INTERVAL_IN_MS, RESET_INTERVAL_IN_MS, TimeUnit.MILLISECONDS);
-        registerMBean();
-   }
+
+        if (DatabaseDescriptor.isDaemonInitialized())
+        {
+            updateSchedular = ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(update, dynamicUpdateInterval, dynamicUpdateInterval, TimeUnit.MILLISECONDS);
+            resetSchedular = ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(reset, dynamicResetInterval, dynamicResetInterval, TimeUnit.MILLISECONDS);
+            registerMBean();
+        }
+    }
+
+    /**
+     * Update configuration from {@link DatabaseDescriptor} and estart the update-scheduler and reset-scheduler tasks
+     * if the configured rates for these tasks have changed.
+     */
+    public void applyConfigChanges()
+    {
+        if (dynamicUpdateInterval != DatabaseDescriptor.getDynamicUpdateInterval())
+        {
+            dynamicUpdateInterval = DatabaseDescriptor.getDynamicUpdateInterval();
+            if (DatabaseDescriptor.isDaemonInitialized())
+            {
+                updateSchedular.cancel(false);
+                updateSchedular = ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(update, dynamicUpdateInterval, dynamicUpdateInterval, TimeUnit.MILLISECONDS);
+            }
+        }
+
+        if (dynamicResetInterval != DatabaseDescriptor.getDynamicResetInterval())
+        {
+            dynamicResetInterval = DatabaseDescriptor.getDynamicResetInterval();
+            if (DatabaseDescriptor.isDaemonInitialized())
+            {
+                resetSchedular.cancel(false);
+                resetSchedular = ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(reset, dynamicResetInterval, dynamicResetInterval, TimeUnit.MILLISECONDS);
+            }
+        }
+
+        dynamicBadnessThreshold = DatabaseDescriptor.getDynamicBadnessThreshold();
+    }
 
     private void registerMBean()
     {
         MBeanWrapper.instance.registerMBean(this, mbeanName);
     }
 
-    public void unregisterMBean()
+    public void close()
     {
+        updateSchedular.cancel(false);
+        resetSchedular.cancel(false);
+
         MBeanWrapper.instance.unregisterMBean(mbeanName);
     }
 
@@ -129,7 +177,7 @@
     public void sortByProximity(final InetAddress address, List<InetAddress> addresses)
     {
         assert address.equals(FBUtilities.getBroadcastAddress()); // we only know about ourself
-        if (BADNESS_THRESHOLD == 0)
+        if (dynamicBadnessThreshold == 0)
         {
             sortByProximityWithScore(address, addresses);
         }
@@ -173,7 +221,7 @@
         }
 
         // Sort the scores and then compare them (positionally) to the scores in the subsnitch order.
-        // If any of the subsnitch-ordered scores exceed the optimal/sorted score by BADNESS_THRESHOLD, use
+        // If any of the subsnitch-ordered scores exceed the optimal/sorted score by dynamicBadnessThreshold, use
         // the score-sorted ordering instead of the subsnitch ordering.
         ArrayList<Double> sortedScores = new ArrayList<>(subsnitchOrderedScores);
         Collections.sort(sortedScores);
@@ -181,7 +229,7 @@
         Iterator<Double> sortedScoreIterator = sortedScores.iterator();
         for (Double subsnitchScore : subsnitchOrderedScores)
         {
-            if (subsnitchScore > (sortedScoreIterator.next() * (1.0 + BADNESS_THRESHOLD)))
+            if (subsnitchScore > (sortedScoreIterator.next() * (1.0 + dynamicBadnessThreshold)))
             {
                 sortByProximityWithScore(address, addresses);
                 return;
@@ -236,7 +284,7 @@
 
     private void updateScores() // this is expensive
     {
-        if (!StorageService.instance.isInitialized()) 
+        if (!StorageService.instance.isGossipActive())
             return;
         if (!registered)
         {
@@ -271,7 +319,7 @@
             // finally, add the severity without any weighting, since hosts scale this relative to their own load and the size of the task causing the severity.
             // "Severity" is basically a measure of compaction activity (CASSANDRA-3722).
             if (USE_SEVERITY)
-                score += StorageService.instance.getSeverity(entry.getKey());
+                score += getSeverity(entry.getKey());
             // lowest score (least amount of badness) wins.
             newScores.put(entry.getKey(), score);
         }
@@ -290,15 +338,15 @@
 
     public int getUpdateInterval()
     {
-        return UPDATE_INTERVAL_IN_MS;
+        return dynamicUpdateInterval;
     }
     public int getResetInterval()
     {
-        return RESET_INTERVAL_IN_MS;
+        return dynamicResetInterval;
     }
     public double getBadnessThreshold()
     {
-        return BADNESS_THRESHOLD;
+        return dynamicBadnessThreshold;
     }
 
     public String getSubsnitchClassName()
@@ -321,12 +369,25 @@
 
     public void setSeverity(double severity)
     {
-        StorageService.instance.reportManualSeverity(severity);
+        Gossiper.instance.addLocalApplicationState(ApplicationState.SEVERITY, StorageService.instance.valueFactory.severity(severity));
+    }
+
+    private double getSeverity(InetAddress endpoint)
+    {
+        EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint);
+        if (state == null)
+            return 0.0;
+
+        VersionedValue event = state.getApplicationState(ApplicationState.SEVERITY);
+        if (event == null)
+            return 0.0;
+
+        return Double.parseDouble(event.value);
     }
 
     public double getSeverity()
     {
-        return StorageService.instance.getSeverity(FBUtilities.getBroadcastAddress());
+        return getSeverity(FBUtilities.getBroadcastAddress());
     }
 
     public boolean isWorthMergingForRangeQuery(List<InetAddress> merged, List<InetAddress> l1, List<InetAddress> l2)
diff --git a/src/java/org/apache/cassandra/locator/DynamicEndpointSnitchMBean.java b/src/java/org/apache/cassandra/locator/DynamicEndpointSnitchMBean.java
index a413bc5..bfafa75 100644
--- a/src/java/org/apache/cassandra/locator/DynamicEndpointSnitchMBean.java
+++ b/src/java/org/apache/cassandra/locator/DynamicEndpointSnitchMBean.java
@@ -22,18 +22,38 @@
 import java.util.Map;
 import java.util.List;
 
-public interface DynamicEndpointSnitchMBean {
+public interface DynamicEndpointSnitchMBean 
+{
     public Map<InetAddress, Double> getScores();
     public int getUpdateInterval();
     public int getResetInterval();
     public double getBadnessThreshold();
     public String getSubsnitchClassName();
     public List<Double> dumpTimings(String hostname) throws UnknownHostException;
+
     /**
-     * Use this if you want to specify a severity; it can be negative
-     * Example: Page cache is cold and you want data to be sent 
-     *          though it is not preferred one.
+     * Setting a Severity allows operators to inject preference information into the Dynamic Snitch
+     * replica selection.
+     *
+     * When choosing which replicas to participate in a read request, the DSnitch sorts replicas
+     * by response latency, and selects the fastest replicas.  Latencies are normalized to a score
+     * from 0 to 1,  with lower scores being faster.
+     *
+     * The Severity injected here will be added to the normalized score.
+     *
+     * Thus, adding a Severity greater than 1 will mean the replica will never be contacted
+     * (unless needed for ALL or if it is added later for rapid read protection).
+     *
+     * Conversely, adding a negative Severity means the replica will *always* be contacted.
+     *
+     * (The "Severity" term is historical and dates to when this was used to represent how
+     * badly background tasks like compaction were affecting a replica's performance.
+     * See CASSANDRA-3722 for when this was introduced and CASSANDRA-11738 for why it was removed.)
      */
     public void setSeverity(double severity);
+
+    /**
+     * @return the current manually injected Severity.
+     */
     public double getSeverity();
 }
diff --git a/src/java/org/apache/cassandra/locator/EndpointSnitchInfoMBean.java b/src/java/org/apache/cassandra/locator/EndpointSnitchInfoMBean.java
index 6de5022..d6a18ff 100644
--- a/src/java/org/apache/cassandra/locator/EndpointSnitchInfoMBean.java
+++ b/src/java/org/apache/cassandra/locator/EndpointSnitchInfoMBean.java
@@ -53,5 +53,4 @@
      * @return Snitch name
      */
     public String getSnitchName();
-
 }
diff --git a/src/java/org/apache/cassandra/locator/LocalStrategy.java b/src/java/org/apache/cassandra/locator/LocalStrategy.java
index ae58203..7aa9a48 100644
--- a/src/java/org/apache/cassandra/locator/LocalStrategy.java
+++ b/src/java/org/apache/cassandra/locator/LocalStrategy.java
@@ -63,6 +63,10 @@
     {
     }
 
+    public void maybeWarnOnOptions()
+    {
+    }
+
     public Collection<String> recognizedOptions()
     {
         // LocalStrategy doesn't expect any options.
diff --git a/src/java/org/apache/cassandra/locator/NetworkTopologyStrategy.java b/src/java/org/apache/cassandra/locator/NetworkTopologyStrategy.java
index 82183bb..3942311 100644
--- a/src/java/org/apache/cassandra/locator/NetworkTopologyStrategy.java
+++ b/src/java/org/apache/cassandra/locator/NetworkTopologyStrategy.java
@@ -24,13 +24,18 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.locator.TokenMetadata.Topology;
+import org.apache.cassandra.service.ClientWarn;
+import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.Pair;
 
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
 
 /**
  * <p>
@@ -49,14 +54,12 @@
  */
 public class NetworkTopologyStrategy extends AbstractReplicationStrategy
 {
-    private final IEndpointSnitch snitch;
     private final Map<String, Integer> datacenters;
     private static final Logger logger = LoggerFactory.getLogger(NetworkTopologyStrategy.class);
 
     public NetworkTopologyStrategy(String keyspaceName, TokenMetadata tokenMetadata, IEndpointSnitch snitch, Map<String, String> configOptions) throws ConfigurationException
     {
         super(keyspaceName, tokenMetadata, snitch, configOptions);
-        this.snitch = snitch;
 
         Map<String, Integer> newDatacenters = new HashMap<String, Integer>();
         if (configOptions != null)
@@ -76,17 +79,78 @@
     }
 
     /**
-     * calculate endpoints in one pass through the tokens by tracking our progress in each DC, rack etc.
+     * Endpoint adder applying the replication rules for a given DC.
      */
-    @SuppressWarnings("serial")
+    private static final class DatacenterEndpoints
+    {
+        /** List accepted endpoints get pushed into. */
+        Set<InetAddress> endpoints;
+        /**
+         * Racks encountered so far. Replicas are put into separate racks while possible.
+         * For efficiency the set is shared between the instances, using the location pair (dc, rack) to make sure
+         * clashing names aren't a problem.
+         */
+        Set<Pair<String, String>> racks;
+
+        /** Number of replicas left to fill from this DC. */
+        int rfLeft;
+        int acceptableRackRepeats;
+
+        DatacenterEndpoints(int rf, int rackCount, int nodeCount, Set<InetAddress> endpoints, Set<Pair<String, String>> racks)
+        {
+            this.endpoints = endpoints;
+            this.racks = racks;
+            // If there aren't enough nodes in this DC to fill the RF, the number of nodes is the effective RF.
+            this.rfLeft = Math.min(rf, nodeCount);
+            // If there aren't enough racks in this DC to fill the RF, we'll still use at least one node from each rack,
+            // and the difference is to be filled by the first encountered nodes.
+            acceptableRackRepeats = rf - rackCount;
+        }
+
+        /**
+         * Attempts to add an endpoint to the replicas for this datacenter, adding to the endpoints set if successful.
+         * Returns true if the endpoint was added, and this datacenter does not require further replicas.
+         */
+        boolean addEndpointAndCheckIfDone(InetAddress ep, Pair<String,String> location)
+        {
+            if (done())
+                return false;
+
+            if (racks.add(location))
+            {
+                // New rack.
+                --rfLeft;
+                boolean added = endpoints.add(ep);
+                assert added;
+                return done();
+            }
+            if (acceptableRackRepeats <= 0)
+                // There must be rfLeft distinct racks left, do not add any more rack repeats.
+                return false;
+            if (!endpoints.add(ep))
+                // Cannot repeat a node.
+                return false;
+            // Added a node that is from an already met rack to match RF when there aren't enough racks.
+            --acceptableRackRepeats;
+            --rfLeft;
+            return done();
+        }
+
+        boolean done()
+        {
+            assert rfLeft >= 0;
+            return rfLeft == 0;
+        }
+    }
+
+    /**
+     * calculate endpoints in one pass through the tokens by tracking our progress in each DC.
+     */
     public List<InetAddress> calculateNaturalEndpoints(Token searchToken, TokenMetadata tokenMetadata)
     {
         // we want to preserve insertion order so that the first added endpoint becomes primary
         Set<InetAddress> replicas = new LinkedHashSet<>();
-        // replicas we have found in each DC
-        Map<String, Set<InetAddress>> dcReplicas = new HashMap<>(datacenters.size());
-        for (Map.Entry<String, Integer> dc : datacenters.entrySet())
-            dcReplicas.put(dc.getKey(), new HashSet<InetAddress>(dc.getValue()));
+        Set<Pair<String, String>> seenRacks = new HashSet<>();
 
         Topology topology = tokenMetadata.getTopology();
         // all endpoints in each DC, so we can check when we have exhausted all the members of a DC
@@ -95,74 +159,45 @@
         Map<String, ImmutableMultimap<String, InetAddress>> racks = topology.getDatacenterRacks();
         assert !allEndpoints.isEmpty() && !racks.isEmpty() : "not aware of any cluster members";
 
-        // tracks the racks we have already placed replicas in
-        Map<String, Set<String>> seenRacks = new HashMap<>(datacenters.size());
-        for (Map.Entry<String, Integer> dc : datacenters.entrySet())
-            seenRacks.put(dc.getKey(), new HashSet<String>());
+        int dcsToFill = 0;
+        Map<String, DatacenterEndpoints> dcs = new HashMap<>(datacenters.size() * 2);
 
-        // tracks the endpoints that we skipped over while looking for unique racks
-        // when we relax the rack uniqueness we can append this to the current result so we don't have to wind back the iterator
-        Map<String, Set<InetAddress>> skippedDcEndpoints = new HashMap<>(datacenters.size());
-        for (Map.Entry<String, Integer> dc : datacenters.entrySet())
-            skippedDcEndpoints.put(dc.getKey(), new LinkedHashSet<InetAddress>());
+        // Create a DatacenterEndpoints object for each non-empty DC.
+        for (Map.Entry<String, Integer> en : datacenters.entrySet())
+        {
+            String dc = en.getKey();
+            int rf = en.getValue();
+            int nodeCount = sizeOrZero(allEndpoints.get(dc));
+
+            if (rf <= 0 || nodeCount <= 0)
+                continue;
+
+            DatacenterEndpoints dcEndpoints = new DatacenterEndpoints(rf, sizeOrZero(racks.get(dc)), nodeCount, replicas, seenRacks);
+            dcs.put(dc, dcEndpoints);
+            ++dcsToFill;
+        }
 
         Iterator<Token> tokenIter = TokenMetadata.ringIterator(tokenMetadata.sortedTokens(), searchToken, false);
-        while (tokenIter.hasNext() && !hasSufficientReplicas(dcReplicas, allEndpoints))
+        while (dcsToFill > 0 && tokenIter.hasNext())
         {
             Token next = tokenIter.next();
             InetAddress ep = tokenMetadata.getEndpoint(next);
-            String dc = snitch.getDatacenter(ep);
-            // have we already found all replicas for this dc?
-            if (!datacenters.containsKey(dc) || hasSufficientReplicas(dc, dcReplicas, allEndpoints))
-                continue;
-            // can we skip checking the rack?
-            if (seenRacks.get(dc).size() == racks.get(dc).keySet().size())
-            {
-                dcReplicas.get(dc).add(ep);
-                replicas.add(ep);
-            }
-            else
-            {
-                String rack = snitch.getRack(ep);
-                // is this a new rack?
-                if (seenRacks.get(dc).contains(rack))
-                {
-                    skippedDcEndpoints.get(dc).add(ep);
-                }
-                else
-                {
-                    dcReplicas.get(dc).add(ep);
-                    replicas.add(ep);
-                    seenRacks.get(dc).add(rack);
-                    // if we've run out of distinct racks, add the hosts we skipped past already (up to RF)
-                    if (seenRacks.get(dc).size() == racks.get(dc).keySet().size())
-                    {
-                        Iterator<InetAddress> skippedIt = skippedDcEndpoints.get(dc).iterator();
-                        while (skippedIt.hasNext() && !hasSufficientReplicas(dc, dcReplicas, allEndpoints))
-                        {
-                            InetAddress nextSkipped = skippedIt.next();
-                            dcReplicas.get(dc).add(nextSkipped);
-                            replicas.add(nextSkipped);
-                        }
-                    }
-                }
-            }
+            Pair<String, String> location = topology.getLocation(ep);
+            DatacenterEndpoints dcEndpoints = dcs.get(location.left);
+            if (dcEndpoints != null && dcEndpoints.addEndpointAndCheckIfDone(ep, location))
+                --dcsToFill;
         }
-
-        return new ArrayList<InetAddress>(replicas);
+        return new ArrayList<>(replicas);
     }
 
-    private boolean hasSufficientReplicas(String dc, Map<String, Set<InetAddress>> dcReplicas, Multimap<String, InetAddress> allEndpoints)
+    private int sizeOrZero(Multimap<?, ?> collection)
     {
-        return dcReplicas.get(dc).size() >= Math.min(allEndpoints.get(dc).size(), getReplicationFactor(dc));
+        return collection != null ? collection.asMap().size() : 0;
     }
 
-    private boolean hasSufficientReplicas(Map<String, Set<InetAddress>> dcReplicas, Multimap<String, InetAddress> allEndpoints)
+    private int sizeOrZero(Collection<?> collection)
     {
-        for (String dc : datacenters.keySet())
-            if (!hasSufficientReplicas(dc, dcReplicas, allEndpoints))
-                return false;
-        return true;
+        return collection != null ? collection.size() : 0;
     }
 
     public int getReplicationFactor()
@@ -194,10 +229,35 @@
         }
     }
 
-    public Collection<String> recognizedOptions()
+    public void maybeWarnOnOptions()
     {
-        // We explicitely allow all options
-        return null;
+        if (!SchemaConstants.isLocalSystemKeyspace(keyspaceName) && !SchemaConstants.isReplicatedSystemKeyspace(keyspaceName))
+        {
+            ImmutableMultimap<String, InetAddress> dcsNodes = Multimaps.index(StorageService.instance.getTokenMetadata().getAllMembers(), snitch::getDatacenter);
+
+            for (Entry<String, String> e : this.configOptions.entrySet())
+            {
+                String dc = e.getKey();
+                int rf = getReplicationFactor(dc);
+                int nodeCount = dcsNodes.get(dc).size();
+                // nodeCount==0 on many tests
+                if (rf > nodeCount && nodeCount != 0)
+                {
+                    String msg = "Your replication factor " + rf
+                                 + " for keyspace "
+                                 + keyspaceName
+                                 + " is higher than the number of nodes "
+                                 + nodeCount
+                                 + " for datacenter "
+                                 + dc;
+                    if (ClientWarn.instance.getWarnings() == null || !ClientWarn.instance.getWarnings().contains(msg))
+                    {
+                        ClientWarn.instance.warn(msg);
+                        logger.warn(msg);
+                    }
+                }
+            }
+        }
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/locator/OldNetworkTopologyStrategy.java b/src/java/org/apache/cassandra/locator/OldNetworkTopologyStrategy.java
index b9bd767..379547e 100644
--- a/src/java/org/apache/cassandra/locator/OldNetworkTopologyStrategy.java
+++ b/src/java/org/apache/cassandra/locator/OldNetworkTopologyStrategy.java
@@ -25,8 +25,14 @@
 import java.util.List;
 import java.util.Map;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.service.ClientWarn;
+import org.apache.cassandra.service.StorageService;
 
 /**
  * This Replication Strategy returns the nodes responsible for a given
@@ -37,6 +43,8 @@
  */
 public class OldNetworkTopologyStrategy extends AbstractReplicationStrategy
 {
+    private static final Logger logger = LoggerFactory.getLogger(OldNetworkTopologyStrategy.class);
+
     public OldNetworkTopologyStrategy(String keyspaceName, TokenMetadata tokenMetadata, IEndpointSnitch snitch, Map<String, String> configOptions)
     {
         super(keyspaceName, tokenMetadata, snitch, configOptions);
@@ -115,6 +123,29 @@
         validateReplicationFactor(configOptions.get("replication_factor"));
     }
 
+    public void maybeWarnOnOptions()
+    {
+        if (!SchemaConstants.isLocalSystemKeyspace(keyspaceName) && !SchemaConstants.isReplicatedSystemKeyspace(keyspaceName))
+        {
+            int nodeCount = StorageService.instance.getHostIdToEndpoint().size();
+            // nodeCount==0 on many tests
+            int rf = getReplicationFactor();
+            if (rf > nodeCount && nodeCount != 0)
+            {
+                String msg = "Your replication factor " + rf
+                             + " for keyspace "
+                             + keyspaceName
+                             + " is higher than the number of nodes "
+                             + nodeCount;
+                if (ClientWarn.instance.getWarnings() == null || !ClientWarn.instance.getWarnings().contains(msg))
+                {
+                    ClientWarn.instance.warn(msg);
+                    logger.warn(msg);
+                }
+            }
+        }
+    }
+
     public Collection<String> recognizedOptions()
     {
         return Collections.<String>singleton("replication_factor");
diff --git a/src/java/org/apache/cassandra/locator/ReconnectableSnitchHelper.java b/src/java/org/apache/cassandra/locator/ReconnectableSnitchHelper.java
index 3277af7..a6bec0c 100644
--- a/src/java/org/apache/cassandra/locator/ReconnectableSnitchHelper.java
+++ b/src/java/org/apache/cassandra/locator/ReconnectableSnitchHelper.java
@@ -63,10 +63,10 @@
                 && !MessagingService.instance().getConnectionPool(publicAddress).endPoint().equals(localAddress))
         {
             MessagingService.instance().getConnectionPool(publicAddress).reset(localAddress);
-            logger.debug(String.format("Intiated reconnect to an Internal IP %s for the %s", localAddress, publicAddress));
+            logger.debug("Initiated reconnect to an Internal IP {} for the {}", localAddress, publicAddress);
         }
     }
-    
+
     public void beforeChange(InetAddress endpoint, EndpointState currentState, ApplicationState newStateKey, VersionedValue newValue)
     {
         // no-op
@@ -80,7 +80,7 @@
 
     public void onChange(InetAddress endpoint, ApplicationState state, VersionedValue value)
     {
-        if (preferLocal && !Gossiper.instance.isDeadState(Gossiper.instance.getEndpointStateForEndpoint(endpoint)) && state == ApplicationState.INTERNAL_IP)
+        if (preferLocal && state == ApplicationState.INTERNAL_IP && !Gossiper.instance.isDeadState(Gossiper.instance.getEndpointStateForEndpoint(endpoint)))
             reconnect(endpoint, value);
     }
 
diff --git a/src/java/org/apache/cassandra/locator/SimpleStrategy.java b/src/java/org/apache/cassandra/locator/SimpleStrategy.java
index 9a5062b..860aa5a 100644
--- a/src/java/org/apache/cassandra/locator/SimpleStrategy.java
+++ b/src/java/org/apache/cassandra/locator/SimpleStrategy.java
@@ -25,8 +25,14 @@
 import java.util.List;
 import java.util.Map;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.service.ClientWarn;
+import org.apache.cassandra.service.StorageService;
 
 
 /**
@@ -37,6 +43,8 @@
  */
 public class SimpleStrategy extends AbstractReplicationStrategy
 {
+    private static final Logger logger = LoggerFactory.getLogger(SimpleStrategy.class);
+
     public SimpleStrategy(String keyspaceName, TokenMetadata tokenMetadata, IEndpointSnitch snitch, Map<String, String> configOptions)
     {
         super(keyspaceName, tokenMetadata, snitch, configOptions);
@@ -75,6 +83,29 @@
         validateReplicationFactor(rf);
     }
 
+    public void maybeWarnOnOptions()
+    {
+        if (!SchemaConstants.isLocalSystemKeyspace(keyspaceName) && !SchemaConstants.isReplicatedSystemKeyspace(keyspaceName))
+        {
+            int nodeCount = StorageService.instance.getHostIdToEndpoint().size();
+            // nodeCount==0 on many tests
+            int rf = getReplicationFactor();
+            if (rf > nodeCount && nodeCount != 0)
+            {
+                String msg = "Your replication factor " + rf
+                             + " for keyspace "
+                             + keyspaceName
+                             + " is higher than the number of nodes "
+                             + nodeCount;
+                if (ClientWarn.instance.getWarnings() == null || !ClientWarn.instance.getWarnings().contains(msg))
+                {
+                    ClientWarn.instance.warn(msg);
+                    logger.warn(msg);
+                }
+            }
+        }
+    }
+
     public Collection<String> recognizedOptions()
     {
         return Collections.<String>singleton("replication_factor");
diff --git a/src/java/org/apache/cassandra/locator/SnitchProperties.java b/src/java/org/apache/cassandra/locator/SnitchProperties.java
index 8fdda7a..158feef 100644
--- a/src/java/org/apache/cassandra/locator/SnitchProperties.java
+++ b/src/java/org/apache/cassandra/locator/SnitchProperties.java
@@ -42,9 +42,9 @@
             URL url;
             if (configURL == null)
                 url = SnitchProperties.class.getClassLoader().getResource(RACKDC_PROPERTY_FILENAME);
-            else 
+            else
             	url = new URL(configURL);
-            
+
             stream = url.openStream(); // catch block handles potential NPE
             properties.load(stream);
         }
diff --git a/src/java/org/apache/cassandra/locator/TokenMetadata.java b/src/java/org/apache/cassandra/locator/TokenMetadata.java
index 2d7afcd..3b20f14 100644
--- a/src/java/org/apache/cassandra/locator/TokenMetadata.java
+++ b/src/java/org/apache/cassandra/locator/TokenMetadata.java
@@ -25,6 +25,7 @@
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
 
 import javax.annotation.concurrent.GuardedBy;
 
@@ -139,7 +140,7 @@
     }
 
     /**
-     * To be used by tests only (via {@link StorageService#setPartitionerUnsafe}).
+     * To be used by tests only (via {@link org.apache.cassandra.service.StorageService#setPartitionerUnsafe}).
      */
     @VisibleForTesting
     public TokenMetadata cloneWithNewPartitioner(IPartitioner newPartitioner)
@@ -565,11 +566,11 @@
     public Collection<Token> getTokens(InetAddress endpoint)
     {
         assert endpoint != null;
-        assert isMember(endpoint); // don't want to return nulls
 
         lock.readLock().lock();
         try
         {
+            assert isMember(endpoint); // don't want to return nulls
             return new ArrayList<>(tokenToEndpointMap.inverse().get(endpoint));
         }
         finally
@@ -985,18 +986,18 @@
 
     public Token getPredecessor(Token token)
     {
-        List tokens = sortedTokens();
+        List<Token> tokens = sortedTokens();
         int index = Collections.binarySearch(tokens, token);
         assert index >= 0 : token + " not found in " + tokenToEndpointMapKeysAsStrings();
-        return (Token) (index == 0 ? tokens.get(tokens.size() - 1) : tokens.get(index - 1));
+        return index == 0 ? tokens.get(tokens.size() - 1) : tokens.get(index - 1);
     }
 
     public Token getSuccessor(Token token)
     {
-        List tokens = sortedTokens();
+        List<Token> tokens = sortedTokens();
         int index = Collections.binarySearch(tokens, token);
         assert index >= 0 : token + " not found in " + tokenToEndpointMapKeysAsStrings();
-        return (Token) ((index == (tokens.size() - 1)) ? tokens.get(0) : tokens.get(index + 1));
+        return (index == (tokens.size() - 1)) ? tokens.get(0) : tokens.get(index + 1);
     }
 
     private String tokenToEndpointMapKeysAsStrings()
@@ -1039,6 +1040,13 @@
         }
     }
 
+    public Set<InetAddress> getAllMembers()
+    {
+        return getAllEndpoints().stream()
+                                .filter(this::isMember)
+                                .collect(Collectors.toSet());
+    }
+
     /** caller should not modify leavingEndpoints */
     public Set<InetAddress> getLeavingEndpoints()
     {
@@ -1070,7 +1078,7 @@
         }
     }
 
-    public static int firstTokenIndex(final ArrayList ring, Token start, boolean insertMin)
+    public static int firstTokenIndex(final ArrayList<Token> ring, Token start, boolean insertMin)
     {
         assert ring.size() > 0;
         // insert the minimum token (at index == -1) if we were asked to include it and it isn't a member of the ring
@@ -1098,7 +1106,7 @@
     {
         if (ring.isEmpty())
             return includeMin ? Iterators.singletonIterator(start.getPartitioner().getMinimumToken())
-                              : Iterators.<Token>emptyIterator();
+                              : Collections.emptyIterator();
 
         final boolean insertMin = includeMin && !ring.get(0).isMinimum();
         final int startIndex = firstTokenIndex(ring, start, insertMin);
@@ -1158,7 +1166,8 @@
         lock.readLock().lock();
         try
         {
-            Set<InetAddress> eps = tokenToEndpointMap.inverse().keySet();
+            Multimap<InetAddress, Token> endpointToTokenMap = tokenToEndpointMap.inverse();
+            Set<InetAddress> eps = endpointToTokenMap.keySet();
 
             if (!eps.isEmpty())
             {
@@ -1168,7 +1177,7 @@
                 {
                     sb.append(ep);
                     sb.append(':');
-                    sb.append(tokenToEndpointMap.inverse().get(ep));
+                    sb.append(endpointToTokenMap.get(ep));
                     sb.append(System.getProperty("line.separator"));
                 }
             }
@@ -1368,6 +1377,14 @@
             return dcRacks;
         }
 
+        /**
+         * @return The DC and rack of the given endpoint.
+         */
+        public Pair<String, String> getLocation(InetAddress addr)
+        {
+            return currentLocations.get(addr);
+        }
+
         Builder unbuild()
         {
             return new Builder(this);
@@ -1498,6 +1515,5 @@
                 return new Topology(this);
             }
         }
-
     }
 }
diff --git a/src/java/org/apache/cassandra/metrics/AuthMetrics.java b/src/java/org/apache/cassandra/metrics/AuthMetrics.java
new file mode 100644
index 0000000..8f0f97e
--- /dev/null
+++ b/src/java/org/apache/cassandra/metrics/AuthMetrics.java
@@ -0,0 +1,58 @@
+/*
+ * 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.metrics;
+
+import com.codahale.metrics.Meter;
+
+/**
+ * Metrics about authentication
+ */
+public class AuthMetrics
+{
+
+    public static final AuthMetrics instance = new AuthMetrics();
+
+    public static void init()
+    {
+        // no-op, just used to force instance creation
+    }
+
+    /** Number and rate of successful logins */
+    protected final Meter success;
+
+    /** Number and rate of login failures */
+    protected final Meter failure;
+
+    private AuthMetrics()
+    {
+
+        success = ClientMetrics.instance.registerMeter("AuthSuccess");
+        failure = ClientMetrics.instance.registerMeter("AuthFailure");
+    }
+
+    public void markSuccess()
+    {
+        success.mark();
+    }
+
+    public void markFailure()
+    {
+        failure.mark();
+    }
+}
diff --git a/src/java/org/apache/cassandra/metrics/BufferPoolMetrics.java b/src/java/org/apache/cassandra/metrics/BufferPoolMetrics.java
index 107717d..c9c859a 100644
--- a/src/java/org/apache/cassandra/metrics/BufferPoolMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/BufferPoolMetrics.java
@@ -19,7 +19,6 @@
 
 import com.codahale.metrics.Gauge;
 import com.codahale.metrics.Meter;
-import com.codahale.metrics.RatioGauge;
 import org.apache.cassandra.utils.memory.BufferPool;
 
 import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
diff --git a/src/java/org/apache/cassandra/metrics/CASClientRequestMetrics.java b/src/java/org/apache/cassandra/metrics/CASClientRequestMetrics.java
index 4e64cff..f3f1f64 100644
--- a/src/java/org/apache/cassandra/metrics/CASClientRequestMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/CASClientRequestMetrics.java
@@ -32,7 +32,8 @@
 
     public final Counter unfinishedCommit;
 
-    public CASClientRequestMetrics(String scope) {
+    public CASClientRequestMetrics(String scope) 
+    {
         super(scope);
         contention = Metrics.histogram(factory.createMetricName("ContentionHistogram"), false);
         conditionNotMet =  Metrics.counter(factory.createMetricName("ConditionNotMet"));
diff --git a/src/java/org/apache/cassandra/metrics/CacheMetrics.java b/src/java/org/apache/cassandra/metrics/CacheMetrics.java
index 151268b..e623dcb 100644
--- a/src/java/org/apache/cassandra/metrics/CacheMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/CacheMetrics.java
@@ -17,8 +17,6 @@
  */
 package org.apache.cassandra.metrics;
 
-import java.util.concurrent.atomic.AtomicLong;
-
 import com.codahale.metrics.Gauge;
 import com.codahale.metrics.Meter;
 import com.codahale.metrics.RatioGauge;
@@ -56,7 +54,7 @@
      * @param type Type of Cache to identify metrics.
      * @param cache Cache to measure metrics
      */
-    public CacheMetrics(String type, final ICache cache)
+    public CacheMetrics(String type, final ICache<?, ?> cache)
     {
         MetricNameFactory factory = new DefaultNameFactory("Cache", type);
 
diff --git a/src/java/org/apache/cassandra/metrics/CacheMissMetrics.java b/src/java/org/apache/cassandra/metrics/CacheMissMetrics.java
new file mode 100644
index 0000000..19d61ef
--- /dev/null
+++ b/src/java/org/apache/cassandra/metrics/CacheMissMetrics.java
@@ -0,0 +1,114 @@
+/*
+ * 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.metrics;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.RatioGauge;
+import com.codahale.metrics.Timer;
+import org.apache.cassandra.cache.CacheSize;
+
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+
+/**
+ * Metrics for {@code ICache}.
+ */
+public class CacheMissMetrics
+{
+    /** Cache capacity in bytes */
+    public final Gauge<Long> capacity;
+    /** Total number of cache hits */
+    public final Meter misses;
+    /** Total number of cache requests */
+    public final Meter requests;
+    /** Latency of misses */
+    public final Timer missLatency;
+    /** all time cache hit rate */
+    public final Gauge<Double> hitRate;
+    /** 1m hit rate */
+    public final Gauge<Double> oneMinuteHitRate;
+    /** 5m hit rate */
+    public final Gauge<Double> fiveMinuteHitRate;
+    /** 15m hit rate */
+    public final Gauge<Double> fifteenMinuteHitRate;
+    /** Total size of cache, in bytes */
+    public final Gauge<Long> size;
+    /** Total number of cache entries */
+    public final Gauge<Integer> entries;
+
+    /**
+     * Create metrics for given cache.
+     *
+     * @param type Type of Cache to identify metrics.
+     * @param cache Cache to measure metrics
+     */
+    public CacheMissMetrics(String type, final CacheSize cache)
+    {
+        MetricNameFactory factory = new DefaultNameFactory("Cache", type);
+
+        capacity = Metrics.register(factory.createMetricName("Capacity"), (Gauge<Long>) cache::capacity);
+        misses = Metrics.meter(factory.createMetricName("Misses"));
+        requests = Metrics.meter(factory.createMetricName("Requests"));
+        missLatency = Metrics.timer(factory.createMetricName("MissLatency"));
+        hitRate = Metrics.register(factory.createMetricName("HitRate"), new RatioGauge()
+        {
+            @Override
+            public Ratio getRatio()
+            {
+                long req = requests.getCount();
+                long mis = misses.getCount();
+                return Ratio.of(req - mis, req);
+            }
+        });
+        oneMinuteHitRate = Metrics.register(factory.createMetricName("OneMinuteHitRate"), new RatioGauge()
+        {
+            protected Ratio getRatio()
+            {
+                double req = requests.getOneMinuteRate();
+                double mis = misses.getOneMinuteRate();
+                return Ratio.of(req - mis, req);
+            }
+        });
+        fiveMinuteHitRate = Metrics.register(factory.createMetricName("FiveMinuteHitRate"), new RatioGauge()
+        {
+            protected Ratio getRatio()
+            {
+                double req = requests.getFiveMinuteRate();
+                double mis = misses.getFiveMinuteRate();
+                return Ratio.of(req - mis, req);
+            }
+        });
+        fifteenMinuteHitRate = Metrics.register(factory.createMetricName("FifteenMinuteHitRate"), new RatioGauge()
+        {
+            protected Ratio getRatio()
+            {
+                double req = requests.getFifteenMinuteRate();
+                double mis = misses.getFifteenMinuteRate();
+                return Ratio.of(req - mis, req);
+            }
+        });
+        size = Metrics.register(factory.createMetricName("Size"), (Gauge<Long>) cache::weightedSize);
+        entries = Metrics.register(factory.createMetricName("Entries"), (Gauge<Integer>) cache::size);
+    }
+
+    public void reset()
+    {
+        requests.mark(-requests.getCount());
+        misses.mark(-misses.getCount());
+    }
+}
diff --git a/src/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java b/src/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java
index e455dc0..2181f5c 100644
--- a/src/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java
+++ b/src/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java
@@ -194,6 +194,18 @@
             MBeanWrapper.instance.unregisterMBean(name.getMBeanName());
         } catch (Exception ignored) {}
     }
+    
+    /**
+     * Strips a single final '$' from input
+     * 
+     * @param s String to strip
+     * @return a string with one less '$' at end
+     */
+    private static String withoutFinalDollar(String s)
+    {
+        int l = s.length();
+        return (l!=0 && '$' == s.charAt(l-1))?s.substring(0,l-1):s;
+    }
 
     public interface MetricMBean
     {
@@ -604,7 +616,7 @@
         public MetricName(Class<?> klass, String name, String scope)
         {
             this(klass.getPackage() == null ? "" : klass.getPackage().getName(),
-                    klass.getSimpleName().replaceAll("\\$$", ""),
+                    withoutFinalDollar(klass.getSimpleName()),
                     name,
                     scope);
         }
@@ -814,7 +826,7 @@
         {
             if (type == null || type.isEmpty())
             {
-                type = klass.getSimpleName().replaceAll("\\$$", "");
+                type = withoutFinalDollar(klass.getSimpleName());
             }
             return type;
         }
diff --git a/src/java/org/apache/cassandra/metrics/ClientMetrics.java b/src/java/org/apache/cassandra/metrics/ClientMetrics.java
index e034be8..38b84d0 100644
--- a/src/java/org/apache/cassandra/metrics/ClientMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/ClientMetrics.java
@@ -85,17 +85,13 @@
 
     public void addCounter(String name, final Callable<Integer> provider)
     {
-        Metrics.register(factory.createMetricName(name), new Gauge<Integer>()
-        {
-            public Integer getValue()
+        Metrics.register(factory.createMetricName(name), (Gauge<Integer>) () -> {
+            try
             {
-                try
-                {
-                    return provider.call();
-                } catch (Exception e)
-                {
-                    throw new RuntimeException(e);
-                }
+                return provider.call();
+            } catch (Exception e)
+            {
+                throw new RuntimeException(e);
             }
         });
     }
@@ -115,7 +111,7 @@
         return Metrics.register(factory.createMetricName(name), gauge);
     }
 
-    private Meter registerMeter(String name)
+    public Meter registerMeter(String name)
     {
         return Metrics.meter(factory.createMetricName(name));
     }
diff --git a/src/java/org/apache/cassandra/metrics/CommitLogMetrics.java b/src/java/org/apache/cassandra/metrics/CommitLogMetrics.java
index 1da6ed0..08c1c8e 100644
--- a/src/java/org/apache/cassandra/metrics/CommitLogMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/CommitLogMetrics.java
@@ -17,11 +17,10 @@
  */
 package org.apache.cassandra.metrics;
 
-
 import com.codahale.metrics.Gauge;
 import com.codahale.metrics.Timer;
 import org.apache.cassandra.db.commitlog.AbstractCommitLogService;
-import org.apache.cassandra.db.commitlog.CommitLogSegmentManager;
+import org.apache.cassandra.db.commitlog.AbstractCommitLogSegmentManager;
 
 import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
 
@@ -42,14 +41,14 @@
     public final Timer waitingOnSegmentAllocation;
     /** The time spent waiting on CL sync; for Periodic this is only occurs when the sync is lagging its sync interval */
     public final Timer waitingOnCommit;
-    
+
     public CommitLogMetrics()
     {
         waitingOnSegmentAllocation = Metrics.timer(factory.createMetricName("WaitingOnSegmentAllocation"));
         waitingOnCommit = Metrics.timer(factory.createMetricName("WaitingOnCommit"));
     }
 
-    public void attach(final AbstractCommitLogService service, final CommitLogSegmentManager allocator)
+    public void attach(final AbstractCommitLogService service, final AbstractCommitLogSegmentManager segmentManager)
     {
         completedTasks = Metrics.register(factory.createMetricName("CompletedTasks"), new Gauge<Long>()
         {
@@ -69,7 +68,7 @@
         {
             public Long getValue()
             {
-                return allocator.onDiskSize();
+                return segmentManager.onDiskSize();
             }
         });
     }
diff --git a/src/java/org/apache/cassandra/metrics/CompactionMetrics.java b/src/java/org/apache/cassandra/metrics/CompactionMetrics.java
index 19eadc8..9aef0f8 100644
--- a/src/java/org/apache/cassandra/metrics/CompactionMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/CompactionMetrics.java
@@ -24,6 +24,7 @@
 import com.codahale.metrics.Gauge;
 import com.codahale.metrics.Meter;
 
+import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
@@ -44,6 +45,9 @@
 
     /** Estimated number of compactions remaining to perform */
     public final Gauge<Integer> pendingTasks;
+    /** Estimated number of compactions remaining to perform, group by keyspace and then table name */
+    public final Gauge<Map<String, Map<String, Integer>>> pendingTasksByTableName;
+
     /** Number of completed compactions since server [re]start */
     public final Gauge<Long> completedTasks;
     /** Total number of compactions since server [re]start */
@@ -68,6 +72,59 @@
                 return n + compactions.size();
             }
         });
+
+        pendingTasksByTableName = Metrics.register(factory.createMetricName("PendingTasksByTableName"),
+            new Gauge<Map<String, Map<String, Integer>>>()
+        {
+            @Override
+            public Map<String, Map<String, Integer>> getValue() 
+            {
+                Map<String, Map<String, Integer>> resultMap = new HashMap<>();
+                // estimation of compactions need to be done
+                for (String keyspaceName : Schema.instance.getKeyspaces())
+                {
+                    for (ColumnFamilyStore cfs : Keyspace.open(keyspaceName).getColumnFamilyStores())
+                    {
+                        int taskNumber = cfs.getCompactionStrategyManager().getEstimatedRemainingTasks();
+                        if (taskNumber > 0)
+                        {
+                            if (!resultMap.containsKey(keyspaceName))
+                            {
+                                resultMap.put(keyspaceName, new HashMap<>());
+                            }
+                            resultMap.get(keyspaceName).put(cfs.getTableName(), taskNumber);
+                        }
+                    }
+                }
+
+                // currently running compactions
+                for (CompactionInfo.Holder compaction : compactions)
+                {
+                    CFMetaData metaData = compaction.getCompactionInfo().getCFMetaData();
+                    if (metaData == null)
+                    {
+                        continue;
+                    }
+                    if (!resultMap.containsKey(metaData.ksName))
+                    {
+                        resultMap.put(metaData.ksName, new HashMap<>());
+                    }
+
+                    Map<String, Integer> tableNameToCountMap = resultMap.get(metaData.ksName);
+                    if (tableNameToCountMap.containsKey(metaData.cfName))
+                    {
+                        tableNameToCountMap.put(metaData.cfName,
+                                                tableNameToCountMap.get(metaData.cfName) + 1);
+                    }
+                    else
+                    {
+                        tableNameToCountMap.put(metaData.cfName, 1);
+                    }
+                }
+                return resultMap;
+            }
+        });
+
         completedTasks = Metrics.register(factory.createMetricName("CompletedTasks"), new Gauge<Long>()
         {
             public Long getValue()
@@ -84,15 +141,11 @@
 
     public void beginCompaction(CompactionInfo.Holder ci)
     {
-        // notify
-        ci.started();
         compactions.add(ci);
     }
 
     public void finishCompaction(CompactionInfo.Holder ci)
     {
-        // notify
-        ci.finished();
         compactions.remove(ci);
         bytesCompacted.inc(ci.getCompactionInfo().getTotal());
         totalCompactionsCompleted.mark();
diff --git a/src/java/org/apache/cassandra/metrics/DecayingEstimatedHistogramReservoir.java b/src/java/org/apache/cassandra/metrics/DecayingEstimatedHistogramReservoir.java
index 2f8bdf8..1bf9dbc 100644
--- a/src/java/org/apache/cassandra/metrics/DecayingEstimatedHistogramReservoir.java
+++ b/src/java/org/apache/cassandra/metrics/DecayingEstimatedHistogramReservoir.java
@@ -24,7 +24,6 @@
 import java.nio.charset.Charset;
 import java.util.Arrays;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicLongArray;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
@@ -519,19 +518,23 @@
 
             final long count = count();
 
-            if(count <= 1) {
+            if(count <= 1)
+            {
                 return 0.0D;
-            } else {
+            }
+            else
+            {
                 double mean = this.getMean();
                 double sum = 0.0D;
 
-                for(int i = 0; i < lastBucket; ++i) {
+                for(int i = 0; i < lastBucket; ++i)
+                {
                     long value = bucketOffsets[i];
-                    double diff = (double)value - mean;
+                    double diff = value - mean;
                     sum += diff * diff * decayingBuckets[i];
                 }
 
-                return Math.sqrt(sum / (double)(count - 1));
+                return Math.sqrt(sum / (count - 1));
             }
         }
 
@@ -541,7 +544,8 @@
             {
                 int length = decayingBuckets.length;
 
-                for(int i = 0; i < length; ++i) {
+                for(int i = 0; i < length; ++i)
+                {
                     out.printf("%d%n", decayingBuckets[i]);
                 }
             }
diff --git a/src/java/org/apache/cassandra/metrics/DroppedMessageMetrics.java b/src/java/org/apache/cassandra/metrics/DroppedMessageMetrics.java
index 6d16f8b..794fa9c 100644
--- a/src/java/org/apache/cassandra/metrics/DroppedMessageMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/DroppedMessageMetrics.java
@@ -18,6 +18,8 @@
 package org.apache.cassandra.metrics;
 
 import com.codahale.metrics.Meter;
+import com.codahale.metrics.Timer;
+
 import org.apache.cassandra.net.MessagingService;
 
 import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
@@ -30,6 +32,12 @@
     /** Number of dropped messages */
     public final Meter dropped;
 
+    /** The dropped latency within node */
+    public final Timer internalDroppedLatency;
+
+    /** The cross node dropped latency */
+    public final Timer crossNodeDroppedLatency;
+
     public DroppedMessageMetrics(MessagingService.Verb verb)
     {
         this(new DefaultNameFactory("DroppedMessage", verb.toString()));
@@ -38,5 +46,7 @@
     public DroppedMessageMetrics(MetricNameFactory factory)
     {
         dropped = Metrics.meter(factory.createMetricName("Dropped"));
+        internalDroppedLatency = Metrics.timer(factory.createMetricName("InternalDroppedLatency"));
+        crossNodeDroppedLatency = Metrics.timer(factory.createMetricName("CrossNodeDroppedLatency"));
     }
 }
diff --git a/src/java/org/apache/cassandra/metrics/HintsServiceMetrics.java b/src/java/org/apache/cassandra/metrics/HintsServiceMetrics.java
index 062f67d..ad85281 100644
--- a/src/java/org/apache/cassandra/metrics/HintsServiceMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/HintsServiceMetrics.java
@@ -17,9 +17,18 @@
  */
 package org.apache.cassandra.metrics;
 
+import com.codahale.metrics.Meter;
+
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+
 /**
- * Metrics for {@link HintsService}.
+ * Metrics for {@link org.apache.cassandra.hints.HintsService}.
  */
 public final class HintsServiceMetrics
 {
+    private static final MetricNameFactory factory = new DefaultNameFactory("HintsService");
+
+    public static final Meter hintsSucceeded = Metrics.meter(factory.createMetricName("HintsSucceeded"));
+    public static final Meter hintsFailed    = Metrics.meter(factory.createMetricName("HintsFailed"));
+    public static final Meter hintsTimedOut  = Metrics.meter(factory.createMetricName("HintsTimedOut"));
 }
diff --git a/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java b/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java
index ef62034..2e1c384 100644
--- a/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java
@@ -94,10 +94,10 @@
 
     public final MetricNameFactory factory;
     private Keyspace keyspace;
-    
+
     /** set containing names of all the metrics stored here, for releasing later */
     private Set<String> allMetrics = Sets.newHashSet();
-    
+
     /**
      * Creates metrics for given {@link ColumnFamilyStore}.
      *
@@ -120,7 +120,7 @@
             {
                 return metric.memtableLiveDataSize.getValue();
             }
-        }); 
+        });
         memtableOnHeapDataSize = createKeyspaceGauge("MemtableOnHeapDataSize", new MetricValue()
         {
             public Long getValue(TableMetrics metric)
@@ -243,7 +243,7 @@
      */
     public void release()
     {
-        for(String name : allMetrics) 
+        for(String name : allMetrics)
         {
             Metrics.remove(factory.createMetricName(name));
         }
@@ -252,7 +252,7 @@
         writeLatency.release();
         rangeLatency.release();
     }
-    
+
     /**
      * Represents a column family metric value.
      */
diff --git a/src/java/org/apache/cassandra/metrics/MessagingMetrics.java b/src/java/org/apache/cassandra/metrics/MessagingMetrics.java
new file mode 100644
index 0000000..e126c93
--- /dev/null
+++ b/src/java/org/apache/cassandra/metrics/MessagingMetrics.java
@@ -0,0 +1,59 @@
+/*
+ * 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.metrics;
+
+import java.net.InetAddress;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.codahale.metrics.Timer;
+
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+
+/**
+ * Metrics for messages
+ */
+public class MessagingMetrics
+{
+    private static Logger logger = LoggerFactory.getLogger(MessagingMetrics.class);
+    private static final MetricNameFactory factory = new DefaultNameFactory("Messaging");
+    public final Timer crossNodeLatency;
+    public final ConcurrentHashMap<String, Timer> dcLatency;
+
+    public MessagingMetrics()
+    {
+        crossNodeLatency = Metrics.timer(factory.createMetricName("CrossNodeLatency"));
+        dcLatency = new ConcurrentHashMap<>();
+    }
+
+    public void addTimeTaken(InetAddress from, long timeTaken)
+    {
+        String dc = DatabaseDescriptor.getEndpointSnitch().getDatacenter(from);
+        Timer timer = dcLatency.get(dc);
+        if (timer == null)
+        {
+            timer = dcLatency.computeIfAbsent(dc, k -> Metrics.timer(factory.createMetricName(dc + "-Latency")));
+        }
+        timer.update(timeTaken, TimeUnit.MILLISECONDS);
+        crossNodeLatency.update(timeTaken, TimeUnit.MILLISECONDS);
+    }
+}
diff --git a/src/java/org/apache/cassandra/metrics/RestorableMeter.java b/src/java/org/apache/cassandra/metrics/RestorableMeter.java
index 9e2ed92..ea3fdde 100644
--- a/src/java/org/apache/cassandra/metrics/RestorableMeter.java
+++ b/src/java/org/apache/cassandra/metrics/RestorableMeter.java
@@ -49,7 +49,8 @@
     /**
      * Creates a new, uninitialized RestorableMeter.
      */
-    public RestorableMeter() {
+    public RestorableMeter()
+    {
         this.m15Rate = new RestorableEWMA(TimeUnit.MINUTES.toSeconds(15));
         this.m120Rate = new RestorableEWMA(TimeUnit.MINUTES.toSeconds(120));
         this.startTime = this.clock.getTick();
@@ -61,7 +62,8 @@
      * @param lastM15Rate the last-seen 15m rate, in terms of events per second
      * @param lastM120Rate the last seen 2h rate, in terms of events per second
      */
-    public RestorableMeter(double lastM15Rate, double lastM120Rate) {
+    public RestorableMeter(double lastM15Rate, double lastM120Rate)
+    {
         this.m15Rate = new RestorableEWMA(lastM15Rate, TimeUnit.MINUTES.toSeconds(15));
         this.m120Rate = new RestorableEWMA(lastM120Rate, TimeUnit.MINUTES.toSeconds(120));
         this.startTime = this.clock.getTick();
@@ -71,15 +73,19 @@
     /**
      * Updates the moving averages as needed.
      */
-    private void tickIfNecessary() {
+    private void tickIfNecessary()
+    {
         final long oldTick = lastTick.get();
         final long newTick = clock.getTick();
         final long age = newTick - oldTick;
-        if (age > TICK_INTERVAL) {
+        if (age > TICK_INTERVAL)
+        {
             final long newIntervalStartTick = newTick - age % TICK_INTERVAL;
-            if (lastTick.compareAndSet(oldTick, newIntervalStartTick)) {
+            if (lastTick.compareAndSet(oldTick, newIntervalStartTick))
+            {
                 final long requiredTicks = age / TICK_INTERVAL;
-                for (long i = 0; i < requiredTicks; i++) {
+                for (long i = 0; i < requiredTicks; i++)
+                {
                     m15Rate.tick();
                     m120Rate.tick();
                 }
@@ -90,7 +96,8 @@
     /**
      * Mark the occurrence of an event.
      */
-    public void mark() {
+    public void mark()
+    {
         mark(1);
     }
 
@@ -99,7 +106,8 @@
      *
      * @param n the number of events
      */
-    public void mark(long n) {
+    public void mark(long n)
+    {
         tickIfNecessary();
         count.addAndGet(n);
         m15Rate.update(n);
@@ -109,7 +117,8 @@
     /**
      * Returns the 15-minute rate in terms of events per second.  This carries the previous rate when restored.
      */
-    public double fifteenMinuteRate() {
+    public double fifteenMinuteRate()
+    {
         tickIfNecessary();
         return m15Rate.rate();
     }
@@ -117,7 +126,8 @@
     /**
      * Returns the two-hour rate in terms of events per second.  This carries the previous rate when restored.
      */
-    public double twoHourRate() {
+    public double twoHourRate()
+    {
         tickIfNecessary();
         return m120Rate.rate();
     }
@@ -126,7 +136,8 @@
      * The total number of events that have occurred since this object was created.  Note that the previous count
      * is *not* carried over when a RestorableMeter is restored.
      */
-    public long count() {
+    public long count()
+    {
         return count.get();
     }
 
@@ -135,8 +146,10 @@
      * does *not* carry over when a RestorableMeter is restored, so the mean rate is only a measure since
      * this object was created.
      */
-    public double meanRate() {
-        if (count() == 0) {
+    public double meanRate()
+    {
+        if (count() == 0)
+        {
             return 0.0;
         } else {
             final long elapsed = (clock.getTick() - startTime);
@@ -144,7 +157,8 @@
         }
     }
 
-    static class RestorableEWMA {
+    static class RestorableEWMA
+    {
         private volatile boolean initialized = false;
         private volatile double rate = 0.0; // average rate in terms of events per nanosecond
 
@@ -156,7 +170,8 @@
          *
          * @param windowInSeconds the window of time this EWMA should average over, expressed as a number of seconds
          */
-        public RestorableEWMA(long windowInSeconds) {
+        public RestorableEWMA(long windowInSeconds)
+        {
             this.alpha = 1 - exp((-TICK_INTERVAL / NANOS_PER_SECOND) / windowInSeconds);
             this.interval = TICK_INTERVAL;
         }
@@ -166,7 +181,8 @@
          *
          * @param intervalInSeconds the window of time this EWMA should average over, expressed as a number of seconds
          */
-        public RestorableEWMA(double lastRate, long intervalInSeconds) {
+        public RestorableEWMA(double lastRate, long intervalInSeconds)
+        {
             this(intervalInSeconds);
             this.rate = lastRate / NANOS_PER_SECOND;
             this.initialized = true;
@@ -175,19 +191,24 @@
         /**
          * Update the moving average with a new value.
          */
-        public void update(long n) {
+        public void update(long n)
+        {
             uncounted.addAndGet(n);
         }
 
         /**
          * Mark the passage of time and decay the current rate accordingly.
          */
-        public void tick() {
+        public void tick()
+        {
             final long count = uncounted.getAndSet(0);
             final double instantRate = count / interval;
-            if (initialized) {
+            if (initialized)
+            {
                 rate += (alpha * (instantRate - rate));
-            } else {
+            }
+            else
+            {
                 rate = instantRate;
                 initialized = true;
             }
@@ -196,7 +217,8 @@
         /**
          * Returns the rate in terms of events per second.
          */
-        public double rate() {
+        public double rate()
+        {
             return rate * NANOS_PER_SECOND;
         }
     }
diff --git a/src/java/org/apache/cassandra/metrics/TableMetrics.java b/src/java/org/apache/cassandra/metrics/TableMetrics.java
index aee0590..dcfffce 100644
--- a/src/java/org/apache/cassandra/metrics/TableMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/TableMetrics.java
@@ -19,22 +19,26 @@
 
 import java.nio.ByteBuffer;
 import java.util.*;
-import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeUnit;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Maps;
+import com.google.common.annotations.VisibleForTesting;
 
 import org.apache.commons.lang3.ArrayUtils;
 
 import com.codahale.metrics.*;
 import com.codahale.metrics.Timer;
 
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.Memtable;
 import org.apache.cassandra.db.lifecycle.SSTableSet;
+import org.apache.cassandra.index.SecondaryIndexManager;
+import org.apache.cassandra.io.compress.CompressionMetadata;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
 import org.apache.cassandra.utils.EstimatedHistogram;
@@ -47,7 +51,6 @@
  */
 public class TableMetrics
 {
-
     /** Total amount of data stored in the memtable that resides on-heap, including column related overhead and partitions overwritten. */
     public final Gauge<Long> memtableOnHeapSize;
     /** Total amount of data stored in the memtable that resides off-heap, including column related overhead and partitions overwritten. */
@@ -82,6 +85,10 @@
     public final LatencyMetrics writeLatency;
     /** Estimated number of tasks pending for this table */
     public final Counter pendingFlushes;
+    /** Total number of bytes flushed since server [re]start */
+    public final Counter bytesFlushed;
+    /** Total number of bytes written by compaction since server [re]start */
+    public final Counter compactionBytesWritten;
     /** Estimate of number of pending compactios for this table */
     public final Gauge<Integer> pendingCompactions;
     /** Number of SSTables on disk for this CF */
@@ -116,7 +123,7 @@
     public final Gauge<Double> keyCacheHitRate;
     /** Tombstones scanned in queries on this CF */
     public final TableHistogram tombstoneScannedHistogram;
-    /** Live cells scanned in queries on this CF */
+    /** Live rows scanned in queries on this CF */
     public final TableHistogram liveScannedHistogram;
     /** Column update time delta on this CF */
     public final TableHistogram colUpdateTimeDeltaHistogram;
@@ -138,6 +145,8 @@
     public final LatencyMetrics casPropose;
     /** CAS Commit metrics */
     public final LatencyMetrics casCommit;
+    /** percent of the data that is repaired */
+    public final Gauge<Double> percentRepaired;
 
     public final Timer coordinatorReadLatency;
     public final Timer coordinatorScanLatency;
@@ -145,6 +154,9 @@
     /** Time spent waiting for free memtable space, either on- or off-heap */
     public final Histogram waitingOnFreeMemtableSpace;
 
+    /** Dropped Mutations Count */
+    public final Counter droppedMutations;
+
     private final MetricNameFactory factory;
     private final MetricNameFactory aliasFactory;
     private static final MetricNameFactory globalFactory = new AllTableMetricNameFactory("Table");
@@ -156,6 +168,40 @@
     public final static LatencyMetrics globalWriteLatency = new LatencyMetrics(globalFactory, globalAliasFactory, "Write");
     public final static LatencyMetrics globalRangeLatency = new LatencyMetrics(globalFactory, globalAliasFactory, "Range");
 
+    public final static Gauge<Double> globalPercentRepaired = Metrics.register(globalFactory.createMetricName("PercentRepaired"),
+            new Gauge<Double>()
+    {
+        public Double getValue()
+        {
+            double repaired = 0;
+            double total = 0;
+            for (String keyspace : Schema.instance.getNonSystemKeyspaces())
+            {
+                Keyspace k = Schema.instance.getKeyspaceInstance(keyspace);
+                if (SchemaConstants.DISTRIBUTED_KEYSPACE_NAME.equals(k.getName()))
+                    continue;
+                if (k.getReplicationStrategy().getReplicationFactor() < 2)
+                    continue;
+
+                for (ColumnFamilyStore cf : k.getColumnFamilyStores())
+                {
+                    if (!SecondaryIndexManager.isIndexColumnFamily(cf.name))
+                    {
+                        for (SSTableReader sstable : cf.getSSTables(SSTableSet.CANONICAL))
+                        {
+                            if (sstable.isRepaired())
+                            {
+                                repaired += sstable.uncompressedLength();
+                            }
+                            total += sstable.uncompressedLength();
+                        }
+                    }
+                }
+            }
+            return total > 0 ? (repaired / total) * 100 : 100.0;
+        }
+    });
+
     public final Meter readRepairRequests;
     public final Meter shortReadProtectionRequests;
     
@@ -340,42 +386,40 @@
         {
             public Double getValue()
             {
-                double sum = 0;
-                int total = 0;
-                for (SSTableReader sstable : cfs.getSSTables(SSTableSet.CANONICAL))
-                {
-                    if (sstable.getCompressionRatio() != MetadataCollector.NO_COMPRESSION_RATIO)
-                    {
-                        sum += sstable.getCompressionRatio();
-                        total++;
-                    }
-                }
-                return total != 0 ? sum / total : 0;
+                return computeCompressionRatio(cfs.getSSTables(SSTableSet.CANONICAL));
             }
         }, new Gauge<Double>() // global gauge
         {
             public Double getValue()
             {
-                double sum = 0;
-                int total = 0;
-                for (Keyspace keyspace : Keyspace.all())
+                List<SSTableReader> sstables = new ArrayList<>();
+                Keyspace.all().forEach(ks -> sstables.addAll(ks.getAllSSTables(SSTableSet.CANONICAL)));
+                return computeCompressionRatio(sstables);
+            }
+        });
+        percentRepaired = createTableGauge("PercentRepaired", new Gauge<Double>()
+        {
+            public Double getValue()
+            {
+                double repaired = 0;
+                double total = 0;
+                for (SSTableReader sstable : cfs.getSSTables(SSTableSet.CANONICAL))
                 {
-                    for (SSTableReader sstable : keyspace.getAllSSTables(SSTableSet.CANONICAL))
+                    if (sstable.isRepaired())
                     {
-                        if (sstable.getCompressionRatio() != MetadataCollector.NO_COMPRESSION_RATIO)
-                        {
-                            sum += sstable.getCompressionRatio();
-                            total++;
-                        }
+                        repaired += sstable.uncompressedLength();
                     }
+                    total += sstable.uncompressedLength();
                 }
-                return total != 0 ? sum / total : 0;
+                return total > 0 ? (repaired / total) * 100 : 100.0;
             }
         });
         readLatency = new LatencyMetrics(factory, "Read", cfs.keyspace.metric.readLatency, globalReadLatency);
         writeLatency = new LatencyMetrics(factory, "Write", cfs.keyspace.metric.writeLatency, globalWriteLatency);
         rangeLatency = new LatencyMetrics(factory, "Range", cfs.keyspace.metric.rangeLatency, globalRangeLatency);
         pendingFlushes = createTableCounter("PendingFlushes");
+        bytesFlushed = createTableCounter("BytesFlushed");
+        compactionBytesWritten = createTableCounter("CompactionBytesWritten");
         pendingCompactions = createTableGauge("PendingCompactions", new Gauge<Integer>()
         {
             public Integer getValue()
@@ -665,6 +709,7 @@
         rowCacheHitOutOfRange = createTableCounter("RowCacheHitOutOfRange");
         rowCacheHit = createTableCounter("RowCacheHit");
         rowCacheMiss = createTableCounter("RowCacheMiss");
+        droppedMutations = createTableCounter("DroppedMutations");
 
         casPrepare = new LatencyMetrics(factory, "CasPrepare", cfs.keyspace.metric.casPrepare);
         casPropose = new LatencyMetrics(factory, "CasPropose", cfs.keyspace.metric.casPropose);
@@ -688,11 +733,11 @@
     {
         for(Map.Entry<String, String> entry : all.entrySet())
         {
-            final CassandraMetricsRegistry.MetricName name = factory.createMetricName(entry.getKey());
+            CassandraMetricsRegistry.MetricName name = factory.createMetricName(entry.getKey());
+            CassandraMetricsRegistry.MetricName alias = aliasFactory.createMetricName(entry.getValue());
             final Metric metric = Metrics.getMetrics().get(name.getMetricName());
             if (metric != null)
-            {  // Metric will be null if it's a view metric we are releasing. Views have null for ViewLockAcquireTime and ViewLockReadTime
-                final CassandraMetricsRegistry.MetricName alias = aliasFactory.createMetricName(entry.getValue());
+            {   // Metric will be null if it's a view metric we are releasing. Views have null for ViewLockAcquireTime and ViewLockReadTime
                 allTableMetrics.get(entry.getKey()).remove(metric);
                 Metrics.remove(name, alias);
             }
@@ -801,6 +846,32 @@
     }
 
     /**
+     * Computes the compression ratio for the specified SSTables
+     *
+     * @param sstables the SSTables
+     * @return the compression ratio for the specified SSTables
+     */
+    private static Double computeCompressionRatio(Iterable<SSTableReader> sstables)
+    {
+        double compressedLengthSum = 0;
+        double dataLengthSum = 0;
+        for (SSTableReader sstable : sstables)
+        {
+            if (sstable.compression)
+            {
+                // We should not have any sstable which are in an open early mode as the sstable were selected
+                // using SSTableSet.CANONICAL.
+                assert sstable.openReason != SSTableReader.OpenReason.EARLY;
+
+                CompressionMetadata compressionMetadata = sstable.getCompressionMetadata();
+                compressedLengthSum += compressionMetadata.compressedFileLength;
+                dataLengthSum += compressionMetadata.dataLength;
+            }
+        }
+        return dataLengthSum != 0 ? compressedLengthSum / dataLengthSum : MetadataCollector.NO_COMPRESSION_RATIO;
+    }
+
+    /**
      * Create a histogram-like interface that will register both a CF, keyspace and global level
      * histogram and forward any updates to both
      */
@@ -929,7 +1000,7 @@
             String groupName = TableMetrics.class.getPackage().getName();
             StringBuilder mbeanName = new StringBuilder();
             mbeanName.append(groupName).append(":");
-            mbeanName.append("type=" + type);
+            mbeanName.append("type=").append(type);
             mbeanName.append(",name=").append(metricName);
             return new CassandraMetricsRegistry.MetricName(groupName, type, metricName, "all", mbeanName.toString());
         }
diff --git a/src/java/org/apache/cassandra/metrics/ViewWriteMetrics.java b/src/java/org/apache/cassandra/metrics/ViewWriteMetrics.java
index df98865..98363d4 100644
--- a/src/java/org/apache/cassandra/metrics/ViewWriteMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/ViewWriteMetrics.java
@@ -31,7 +31,8 @@
     // time between when mutation is applied to local memtable to when CL.ONE is achieved on MV
     public final Timer viewWriteLatency;
 
-    public ViewWriteMetrics(String scope) {
+    public ViewWriteMetrics(String scope)
+    {
         super(scope);
         viewReplicasAttempted = Metrics.counter(factory.createMetricName("ViewReplicasAttempted"));
         viewReplicasSuccess = Metrics.counter(factory.createMetricName("ViewReplicasSuccess"));
diff --git a/src/java/org/apache/cassandra/net/BackPressureState.java b/src/java/org/apache/cassandra/net/BackPressureState.java
new file mode 100644
index 0000000..34fd0dd
--- /dev/null
+++ b/src/java/org/apache/cassandra/net/BackPressureState.java
@@ -0,0 +1,51 @@
+/*
+ * 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.net;
+
+import java.net.InetAddress;
+
+/**
+ * Interface meant to track the back-pressure state per replica host.
+ */
+public interface BackPressureState
+{
+    /**
+     * Called when a message is sent to a replica.
+     */
+    void onMessageSent(MessageOut<?> message);
+
+    /**
+     * Called when a response is received from a replica.
+     */
+    void onResponseReceived();
+
+    /**
+     * Called when no response is received from replica.
+     */
+    void onResponseTimeout();
+
+    /**
+     * Gets the current back-pressure rate limit.
+     */
+    double getBackPressureRateLimit();
+
+    /**
+     * Returns the host this state refers to.
+     */
+    InetAddress getHost();
+}
diff --git a/src/java/org/apache/cassandra/net/BackPressureStrategy.java b/src/java/org/apache/cassandra/net/BackPressureStrategy.java
new file mode 100644
index 0000000..b61a0a1
--- /dev/null
+++ b/src/java/org/apache/cassandra/net/BackPressureStrategy.java
@@ -0,0 +1,42 @@
+/*
+ * 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.net;
+
+import java.net.InetAddress;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Back-pressure algorithm interface.
+ * <br/>
+ * For experts usage only. Implementors must provide a constructor accepting a single {@code Map<String, Object>} argument,
+ * representing any parameters eventually required by the specific implementation.
+ */
+public interface BackPressureStrategy<S extends BackPressureState>
+{
+    /**
+     * Applies the back-pressure algorithm, based and acting on the given {@link BackPressureState}s, and up to the given
+     * timeout.
+     */
+    void apply(Set<S> states, long timeout, TimeUnit unit);
+
+    /**
+     * Creates a new {@link BackPressureState} initialized as needed by the specific implementation.
+     */
+    S newState(InetAddress host);
+}
diff --git a/src/java/org/apache/cassandra/net/IAsyncCallback.java b/src/java/org/apache/cassandra/net/IAsyncCallback.java
index a29260c..7835079 100644
--- a/src/java/org/apache/cassandra/net/IAsyncCallback.java
+++ b/src/java/org/apache/cassandra/net/IAsyncCallback.java
@@ -31,7 +31,7 @@
  */
 public interface IAsyncCallback<T>
 {
-    public static Predicate<InetAddress> isAlive = new Predicate<InetAddress>()
+    Predicate<InetAddress> isAlive = new Predicate<InetAddress>()
     {
         public boolean apply(InetAddress endpoint)
         {
@@ -42,11 +42,16 @@
     /**
      * @param msg response received.
      */
-    public void response(MessageIn<T> msg);
+    void response(MessageIn<T> msg);
 
     /**
      * @return true if this callback is on the read path and its latency should be
      * given as input to the dynamic snitch.
      */
     boolean isLatencyForSnitch();
+
+    default boolean supportsBackPressure()
+    {
+        return false;
+    }
 }
diff --git a/src/java/org/apache/cassandra/net/IAsyncCallbackWithFailure.java b/src/java/org/apache/cassandra/net/IAsyncCallbackWithFailure.java
index 744bb62..1cd27b6 100644
--- a/src/java/org/apache/cassandra/net/IAsyncCallbackWithFailure.java
+++ b/src/java/org/apache/cassandra/net/IAsyncCallbackWithFailure.java
@@ -19,11 +19,13 @@
 
 import java.net.InetAddress;
 
+import org.apache.cassandra.exceptions.RequestFailureReason;
+
 public interface IAsyncCallbackWithFailure<T> extends IAsyncCallback<T>
 {
 
     /**
      * Called when there is an exception on the remote node or timeout happens
      */
-    public void onFailure(InetAddress from);
+    void onFailure(InetAddress from, RequestFailureReason failureReason);
 }
diff --git a/src/java/org/apache/cassandra/net/IVerbHandler.java b/src/java/org/apache/cassandra/net/IVerbHandler.java
index 574f30f..0995a68 100644
--- a/src/java/org/apache/cassandra/net/IVerbHandler.java
+++ b/src/java/org/apache/cassandra/net/IVerbHandler.java
@@ -36,5 +36,5 @@
      * @param message - incoming message that needs handling.
      * @param id
      */
-    public void doVerb(MessageIn<T> message, int id) throws IOException;
+    void doVerb(MessageIn<T> message, int id) throws IOException;
 }
diff --git a/src/java/org/apache/cassandra/net/IncomingTcpConnection.java b/src/java/org/apache/cassandra/net/IncomingTcpConnection.java
index ae71e90..5f4b976 100644
--- a/src/java/org/apache/cassandra/net/IncomingTcpConnection.java
+++ b/src/java/org/apache/cassandra/net/IncomingTcpConnection.java
@@ -32,6 +32,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import io.netty.util.concurrent.FastThreadLocalThread;
 import net.jpountz.lz4.LZ4BlockInputStream;
 import net.jpountz.lz4.LZ4FastDecompressor;
 import net.jpountz.lz4.LZ4Factory;
@@ -42,11 +43,12 @@
 import org.xerial.snappy.SnappyInputStream;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.UnknownColumnFamilyException;
+import org.apache.cassandra.db.monitoring.ApproximateTime;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataInputPlus.DataInputStreamPlus;
 import org.apache.cassandra.io.util.NIODataInputStream;
 
-public class IncomingTcpConnection extends Thread implements Closeable
+public class IncomingTcpConnection extends FastThreadLocalThread implements Closeable
 {
     private static final Logger logger = LoggerFactory.getLogger(IncomingTcpConnection.class);
 
@@ -65,7 +67,7 @@
         this.compressed = compressed;
         this.socket = socket;
         this.group = group;
-        if (DatabaseDescriptor.getInternodeRecvBufferSize() != null)
+        if (DatabaseDescriptor.getInternodeRecvBufferSize() > 0)
         {
             try
             {
@@ -201,19 +203,8 @@
             id = Integer.parseInt(input.readUTF());
         else
             id = input.readInt();
-
-        long timestamp = System.currentTimeMillis();
-        boolean isCrossNodeTimestamp = false;
-        // make sure to readInt, even if cross_node_to is not enabled
-        int partial = input.readInt();
-        if (DatabaseDescriptor.hasCrossNodeTimeout())
-        {
-            long crossNodeTimestamp = (timestamp & 0xFFFFFFFF00000000L) | (((partial & 0xFFFFFFFFL) << 2) >> 2);
-            isCrossNodeTimestamp = (timestamp != crossNodeTimestamp);
-            timestamp = crossNodeTimestamp;
-        }
-
-        MessageIn message = MessageIn.read(input, version, id);
+        long currentTime = ApproximateTime.currentTimeMillis();
+        MessageIn message = MessageIn.read(input, version, id, MessageIn.readConstructionTime(from, input, currentTime));
         if (message == null)
         {
             // callback expired; nothing to do
@@ -221,7 +212,7 @@
         }
         if (version <= MessagingService.current_version)
         {
-            MessagingService.instance().receive(message, id, timestamp, isCrossNodeTimestamp);
+            MessagingService.instance().receive(message, id);
         }
         else
         {
diff --git a/src/java/org/apache/cassandra/net/MessageDeliveryTask.java b/src/java/org/apache/cassandra/net/MessageDeliveryTask.java
index 26e780f..3de84d8 100644
--- a/src/java/org/apache/cassandra/net/MessageDeliveryTask.java
+++ b/src/java/org/apache/cassandra/net/MessageDeliveryTask.java
@@ -27,8 +27,10 @@
 
 import org.apache.cassandra.db.Mutation;
 import org.apache.cassandra.db.filter.TombstoneOverwhelmingException;
+import org.apache.cassandra.exceptions.RequestFailureReason;
 import org.apache.cassandra.gms.Gossiper;
 import org.apache.cassandra.index.IndexNotAvailableException;
+import org.apache.cassandra.io.util.DataOutputBuffer;
 
 public class MessageDeliveryTask implements Runnable
 {
@@ -36,25 +38,22 @@
 
     private final MessageIn<?> message;
     private final int id;
-    private final long constructionTime;
-    private final boolean isCrossNodeTimestamp;
 
-    public MessageDeliveryTask(MessageIn<?> message, int id, long timestamp, boolean isCrossNodeTimestamp)
+    public MessageDeliveryTask(MessageIn<?> message, int id)
     {
         assert message != null;
         this.message = message;
         this.id = id;
-        this.constructionTime = timestamp;
-        this.isCrossNodeTimestamp = isCrossNodeTimestamp;
     }
 
     public void run()
     {
         MessagingService.Verb verb = message.verb;
+        long timeTaken = message.getLifetimeInMS();
         if (MessagingService.DROPPABLE_VERBS.contains(verb)
-            && System.currentTimeMillis() > constructionTime + message.getTimeout())
+            && timeTaken > message.getTimeout())
         {
-            MessagingService.instance().incrementDroppedMessages(verb, isCrossNodeTimestamp);
+            MessagingService.instance().incrementDroppedMessages(message, timeTaken);
             return;
         }
 
@@ -86,16 +85,29 @@
         }
 
         if (GOSSIP_VERBS.contains(message.verb))
-            Gossiper.instance.setLastProcessedMessageAt(constructionTime);
+            Gossiper.instance.setLastProcessedMessageAt(message.constructionTime);
     }
 
     private void handleFailure(Throwable t)
     {
         if (message.doCallbackOnFailure())
         {
-            MessageOut<?> response = new MessageOut<>(MessagingService.Verb.INTERNAL_RESPONSE)
+            MessageOut response = new MessageOut(MessagingService.Verb.INTERNAL_RESPONSE)
                                                 .withParameter(MessagingService.FAILURE_RESPONSE_PARAM, MessagingService.ONE_BYTE);
 
+            if (t instanceof TombstoneOverwhelmingException)
+            {
+                try (DataOutputBuffer out = new DataOutputBuffer())
+                {
+                    out.writeShort(RequestFailureReason.READ_TOO_MANY_TOMBSTONES.code);
+                    response = response.withParameter(MessagingService.FAILURE_REASON_PARAM, out.getData());
+                }
+                catch (IOException ex)
+                {
+                    throw new RuntimeException(ex);
+                }
+            }
+
             InetAddress from;
             byte[] fromBytes = message.parameters.get(Mutation.FORWARD_FROM);
             try
diff --git a/src/java/org/apache/cassandra/net/MessageIn.java b/src/java/org/apache/cassandra/net/MessageIn.java
index 64b8e81..d06d515 100644
--- a/src/java/org/apache/cassandra/net/MessageIn.java
+++ b/src/java/org/apache/cassandra/net/MessageIn.java
@@ -24,43 +24,67 @@
 
 import com.google.common.collect.ImmutableMap;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.apache.cassandra.concurrent.Stage;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.monitoring.ApproximateTime;
+import org.apache.cassandra.exceptions.RequestFailureReason;
 import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputBuffer;
 import org.apache.cassandra.io.util.DataInputPlus;
-import org.apache.cassandra.io.util.FileUtils;
 
 public class MessageIn<T>
 {
-    private static final Logger logger = LoggerFactory.getLogger(MessageIn.class);
-
     public final InetAddress from;
     public final T payload;
     public final Map<String, byte[]> parameters;
     public final MessagingService.Verb verb;
     public final int version;
+    public final long constructionTime;
 
-    private MessageIn(InetAddress from, T payload, Map<String, byte[]> parameters, MessagingService.Verb verb, int version)
+    private MessageIn(InetAddress from,
+                      T payload,
+                      Map<String, byte[]> parameters,
+                      MessagingService.Verb verb,
+                      int version,
+                      long constructionTime)
     {
         this.from = from;
         this.payload = payload;
         this.parameters = parameters;
         this.verb = verb;
         this.version = version;
+        this.constructionTime = constructionTime;
     }
 
-    public static <T> MessageIn<T> create(InetAddress from, T payload, Map<String, byte[]> parameters, MessagingService.Verb verb, int version)
+    public static <T> MessageIn<T> create(InetAddress from,
+                                          T payload,
+                                          Map<String, byte[]> parameters,
+                                          MessagingService.Verb verb,
+                                          int version,
+                                          long constructionTime)
     {
-        return new MessageIn<T>(from, payload, parameters, verb, version);
+        return new MessageIn<>(from, payload, parameters, verb, version, constructionTime);
+    }
+
+    public static <T> MessageIn<T> create(InetAddress from,
+                                          T payload,
+                                          Map<String, byte[]> parameters,
+                                          MessagingService.Verb verb,
+                                          int version)
+    {
+        return new MessageIn<>(from, payload, parameters, verb, version, ApproximateTime.currentTimeMillis());
     }
 
     public static <T2> MessageIn<T2> read(DataInputPlus in, int version, int id) throws IOException
     {
+        return read(in, version, id, ApproximateTime.currentTimeMillis());
+    }
+
+    public static <T2> MessageIn<T2> read(DataInputPlus in, int version, int id, long constructionTime) throws IOException
+    {
         InetAddress from = CompactEndpointSerializationHelper.deserialize(in);
 
-        MessagingService.Verb verb = MessagingService.Verb.values()[in.readInt()];
+        MessagingService.Verb verb = MessagingService.verbValues[in.readInt()];
         int parameterCount = in.readInt();
         Map<String, byte[]> parameters;
         if (parameterCount == 0)
@@ -81,7 +105,7 @@
         }
 
         int payloadSize = in.readInt();
-        IVersionedSerializer<T2> serializer = (IVersionedSerializer<T2>) MessagingService.verbSerializers.get(verb);
+        IVersionedSerializer<T2> serializer = (IVersionedSerializer<T2>) MessagingService.instance().verbSerializers.get(verb);
         if (serializer instanceof MessagingService.CallbackDeterminedSerializer)
         {
             CallbackInfo callback = MessagingService.instance().getRegisteredCallback(id);
@@ -94,9 +118,46 @@
             serializer = (IVersionedSerializer<T2>) callback.serializer;
         }
         if (payloadSize == 0 || serializer == null)
-            return create(from, null, parameters, verb, version);
+            return create(from, null, parameters, verb, version, constructionTime);
+
         T2 payload = serializer.deserialize(in, version);
-        return MessageIn.create(from, payload, parameters, verb, version);
+        return MessageIn.create(from, payload, parameters, verb, version, constructionTime);
+    }
+
+    public static long readConstructionTime(InetAddress from, DataInputPlus input, long currentTime) throws IOException
+    {
+        // Reconstruct the message construction time sent by the remote host (we sent only the lower 4 bytes, assuming the
+        // higher 4 bytes wouldn't change between the sender and receiver)
+        int partial = input.readInt(); // make sure to readInt, even if cross_node_to is not enabled
+        long sentConstructionTime = (currentTime & 0xFFFFFFFF00000000L) | (((partial & 0xFFFFFFFFL) << 2) >> 2);
+
+        // Because nodes may not have their clock perfectly in sync, it's actually possible the sentConstructionTime is
+        // later than the currentTime (the received time). If that's the case, as we definitively know there is a lack
+        // of proper synchronziation of the clock, we ignore sentConstructionTime. We also ignore that
+        // sentConstructionTime if we're told to.
+        long elapsed = currentTime - sentConstructionTime;
+        if (elapsed > 0)
+            MessagingService.instance().metrics.addTimeTaken(from, elapsed);
+
+        boolean useSentTime = DatabaseDescriptor.hasCrossNodeTimeout() && elapsed > 0;
+        return useSentTime ? sentConstructionTime : currentTime;
+    }
+
+    /**
+     * Since how long (in milliseconds) the message has lived.
+     */
+    public long getLifetimeInMS()
+    {
+        return ApproximateTime.currentTimeMillis() - constructionTime;
+    }
+
+    /**
+     * Whether the message has crossed the node boundary, that is whether it originated from another node.
+     *
+     */
+    public boolean isCrossNode()
+    {
+        return !from.equals(DatabaseDescriptor.getBroadcastAddress());
     }
 
     public Stage getMessageType()
@@ -114,9 +175,38 @@
         return parameters.containsKey(MessagingService.FAILURE_RESPONSE_PARAM);
     }
 
+    public boolean containsFailureReason()
+    {
+        return parameters.containsKey(MessagingService.FAILURE_REASON_PARAM);
+    }
+
+    public RequestFailureReason getFailureReason()
+    {
+        if (containsFailureReason())
+        {
+            try (DataInputBuffer in = new DataInputBuffer(parameters.get(MessagingService.FAILURE_REASON_PARAM)))
+            {
+                return RequestFailureReason.fromCode(in.readUnsignedShort());
+            }
+            catch (IOException ex)
+            {
+                throw new RuntimeException(ex);
+            }
+        }
+        else
+        {
+            return RequestFailureReason.UNKNOWN;
+        }
+    }
+
     public long getTimeout()
     {
-        return DatabaseDescriptor.getTimeout(verb);
+        return verb.getTimeout();
+    }
+
+    public long getSlowQueryTimeout()
+    {
+        return DatabaseDescriptor.getSlowQueryTimeout();
     }
 
     public String toString()
diff --git a/src/java/org/apache/cassandra/net/MessageOut.java b/src/java/org/apache/cassandra/net/MessageOut.java
index 09ff63b..1d1dd49 100644
--- a/src/java/org/apache/cassandra/net/MessageOut.java
+++ b/src/java/org/apache/cassandra/net/MessageOut.java
@@ -27,17 +27,12 @@
 import com.google.common.collect.ImmutableMap;
 
 import org.apache.cassandra.concurrent.Stage;
-import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.TypeSizes;
 import org.apache.cassandra.io.IVersionedSerializer;
 import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.tracing.Tracing;
 import org.apache.cassandra.utils.FBUtilities;
-import org.apache.cassandra.utils.UUIDGen;
-
-import static org.apache.cassandra.tracing.Tracing.TRACE_HEADER;
-import static org.apache.cassandra.tracing.Tracing.TRACE_TYPE;
 import static org.apache.cassandra.tracing.Tracing.isTracing;
 
 public class MessageOut<T>
@@ -62,8 +57,7 @@
              payload,
              serializer,
              isTracing()
-                 ? ImmutableMap.of(TRACE_HEADER, UUIDGen.decompose(Tracing.instance.getSessionId()),
-                                   TRACE_TYPE, new byte[] { Tracing.TraceType.serialize(Tracing.instance.getTraceType()) })
+                 ? Tracing.instance.getTraceHeaders()
                  : Collections.<String, byte[]>emptyMap());
     }
 
@@ -96,7 +90,7 @@
 
     public long getTimeout()
     {
-        return DatabaseDescriptor.getTimeout(verb);
+        return verb.getTimeout();
     }
 
     public String toString()
@@ -108,6 +102,28 @@
 
     public void serialize(DataOutputPlus out, int version) throws IOException
     {
+        serialize(out, from, verb, parameters, payload, serializer, version);
+    }
+
+    public static <T> void serialize(DataOutputPlus out,
+                                     InetAddress from,
+                                     MessagingService.Verb verb,
+                                     Map<String, byte[]> parameters,
+                                     T payload,
+                                     int version) throws IOException
+    {
+        IVersionedSerializer<T> serializer = (IVersionedSerializer<T>) MessagingService.instance().verbSerializers.get(verb);
+        serialize(out, from, verb, parameters, payload, serializer, version);
+    }
+
+    public static <T> void serialize(DataOutputPlus out,
+                                     InetAddress from,
+                                     MessagingService.Verb verb,
+                                     Map<String, byte[]> parameters,
+                                     T payload,
+                                     IVersionedSerializer<T> serializer,
+                                     int version) throws IOException
+    {
         CompactEndpointSerializationHelper.serialize(from, out);
 
         out.writeInt(MessagingService.Verb.convertForMessagingServiceVersion(verb, version).ordinal());
@@ -119,11 +135,21 @@
             out.write(entry.getValue());
         }
 
-        long longSize = payloadSize(version);
-        assert longSize <= Integer.MAX_VALUE; // larger values are supported in sstables but not messages
-        out.writeInt((int) longSize);
-        if (payload != null)
-            serializer.serialize(payload, out, version);
+        if (payload != null && serializer != MessagingService.CallbackDeterminedSerializer.instance)
+        {
+            try (DataOutputBuffer dob = DataOutputBuffer.scratchBuffer.get())
+            {
+                serializer.serialize(payload, dob, version);
+
+                int size = dob.getLength();
+                out.writeInt(size);
+                out.write(dob.getData(), 0, size);
+            }
+        }
+        else
+        {
+            out.writeInt(0);
+        }
     }
 
     public int serializedSize(int version)
diff --git a/src/java/org/apache/cassandra/net/MessagingService.java b/src/java/org/apache/cassandra/net/MessagingService.java
index b6f12a5..dfca087 100644
--- a/src/java/org/apache/cassandra/net/MessagingService.java
+++ b/src/java/org/apache/cassandra/net/MessagingService.java
@@ -27,6 +27,8 @@
 import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 
 import javax.net.ssl.SSLHandshakeException;
 
@@ -36,8 +38,10 @@
 import com.google.common.collect.Sets;
 
 import org.cliffc.high_scale_lib.NonBlockingHashMap;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+
 import org.apache.cassandra.concurrent.ExecutorLocals;
 import org.apache.cassandra.concurrent.ScheduledExecutors;
 import org.apache.cassandra.concurrent.Stage;
@@ -51,6 +55,7 @@
 import org.apache.cassandra.dht.BootStrapper;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.exceptions.RequestFailureReason;
 import org.apache.cassandra.gms.EchoMessage;
 import org.apache.cassandra.gms.GossipDigestAck;
 import org.apache.cassandra.gms.GossipDigestAck2;
@@ -65,6 +70,7 @@
 import org.apache.cassandra.metrics.CassandraMetricsRegistry;
 import org.apache.cassandra.metrics.ConnectionMetrics;
 import org.apache.cassandra.metrics.DroppedMessageMetrics;
+import org.apache.cassandra.metrics.MessagingMetrics;
 import org.apache.cassandra.repair.messages.RepairMessage;
 import org.apache.cassandra.security.SSLFactory;
 import org.apache.cassandra.service.*;
@@ -77,7 +83,7 @@
 
 public final class MessagingService implements MessagingServiceMBean
 {
-    // Required to allow schema migrations while upgrading within the minor 3.0.x versions to 3.0.14.
+    // Required to allow schema migrations while upgrading within the minor 3.0.x/3.x versions to 3.11+.
     // See CASSANDRA-13004 for details.
     public final static boolean FORCE_3_0_PROTOCOL_VERSION = Boolean.getBoolean("cassandra.force_3_0_protocol_version");
 
@@ -95,6 +101,7 @@
     public static final String FAILURE_CALLBACK_PARAM = "CAL_BAC";
     public static final byte[] ONE_BYTE = new byte[1];
     public static final String FAILURE_RESPONSE_PARAM = "FAIL";
+    public static final String FAILURE_REASON_PARAM = "FAIL_REASON";
 
     /**
      * we preface every message with this number so the recipient can validate the sender is sane
@@ -104,19 +111,63 @@
     private boolean allNodesAtLeast22 = true;
     private boolean allNodesAtLeast30 = true;
 
+    public final MessagingMetrics metrics = new MessagingMetrics();
+
     /* All verb handler identifiers */
     public enum Verb
     {
-        MUTATION,
-        HINT,
-        READ_REPAIR,
-        READ,
+        MUTATION
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getWriteRpcTimeout();
+            }
+        },
+        HINT
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getWriteRpcTimeout();
+            }
+        },
+        READ_REPAIR
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getWriteRpcTimeout();
+            }
+        },
+        READ
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getReadRpcTimeout();
+            }
+        },
         REQUEST_RESPONSE, // client-initiated reads and writes
-        BATCH_STORE,  // was @Deprecated STREAM_INITIATE,
-        BATCH_REMOVE, // was @Deprecated STREAM_INITIATE_DONE,
+        BATCH_STORE
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getWriteRpcTimeout();
+            }
+        },  // was @Deprecated STREAM_INITIATE,
+        BATCH_REMOVE
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getWriteRpcTimeout();
+            }
+        }, // was @Deprecated STREAM_INITIATE_DONE,
         @Deprecated STREAM_REPLY,
         @Deprecated STREAM_REQUEST,
-        RANGE_SLICE,
+        RANGE_SLICE
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getRangeRpcTimeout();
+            }
+        },
         @Deprecated BOOTSTRAP_TOKEN,
         @Deprecated TREE_REQUEST,
         @Deprecated TREE_RESPONSE,
@@ -126,12 +177,24 @@
         GOSSIP_DIGEST_ACK2,
         @Deprecated DEFINITIONS_ANNOUNCE,
         DEFINITIONS_UPDATE,
-        TRUNCATE,
+        TRUNCATE
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getTruncateRpcTimeout();
+            }
+        },
         SCHEMA_CHECK,
         @Deprecated INDEX_SCAN,
         REPLICATION_FINISHED,
         INTERNAL_RESPONSE, // responses to internal calls
-        COUNTER_MUTATION,
+        COUNTER_MUTATION
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getCounterWriteRpcTimeout();
+            }
+        },
         @Deprecated STREAMING_REPAIR_REQUEST,
         @Deprecated STREAMING_REPAIR_RESPONSE,
         SNAPSHOT, // Similar to nt snapshot
@@ -140,12 +203,35 @@
         _TRACE, // dummy verb so we can use MS.droppedMessagesMap
         ECHO,
         REPAIR_MESSAGE,
-        PAXOS_PREPARE,
-        PAXOS_PROPOSE,
-        PAXOS_COMMIT,
-        @Deprecated PAGED_RANGE,
+        PAXOS_PREPARE
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getWriteRpcTimeout();
+            }
+        },
+        PAXOS_PROPOSE
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getWriteRpcTimeout();
+            }
+        },
+        PAXOS_COMMIT
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getWriteRpcTimeout();
+            }
+        },
+        @Deprecated PAGED_RANGE
+        {
+            public long getTimeout()
+            {
+                return DatabaseDescriptor.getRangeRpcTimeout();
+            }
+        },
         PING,
-
         // UNUSED verbs were used as padding for backward/forward compatability before 4.0,
         // but it wasn't quite as bullet/future proof as needed. We still need to keep these entries
         // around, at least for a major rev or two (post-4.0). see CASSANDRA-13993 for a discussion.
@@ -168,8 +254,15 @@
 
             return verb;
         }
+
+        public long getTimeout()
+        {
+            return DatabaseDescriptor.getRpcTimeout();
+        }
     }
 
+    public static final Verb[] verbValues = Verb.values();
+
     public static final EnumMap<MessagingService.Verb, Stage> verbStages = new EnumMap<MessagingService.Verb, Stage>(MessagingService.Verb.class)
     {{
         put(Verb.MUTATION, Stage.MUTATION);
@@ -229,7 +322,7 @@
      * intermediary byte[] (See CASSANDRA-3716), we need to wire that up to the CallbackInfo object
      * (see below).
      */
-    public static final EnumMap<Verb, IVersionedSerializer<?>> verbSerializers = new EnumMap<Verb, IVersionedSerializer<?>>(Verb.class)
+    public final EnumMap<Verb, IVersionedSerializer<?>> verbSerializers = new EnumMap<Verb, IVersionedSerializer<?>>(Verb.class)
     {{
         put(Verb.REQUEST_RESPONSE, CallbackDeterminedSerializer.instance);
         put(Verb.INTERNAL_RESPONSE, CallbackDeterminedSerializer.instance);
@@ -293,7 +386,7 @@
      * a placeholder class that means "deserialize using the callback." We can't implement this without
      * special-case code in InboundTcpConnection because there is no way to pass the message id to IVersionedSerializer.
      */
-    public static class CallbackDeterminedSerializer implements IVersionedSerializer<Object>
+    static class CallbackDeterminedSerializer implements IVersionedSerializer<Object>
     {
         public static final CallbackDeterminedSerializer instance = new CallbackDeterminedSerializer();
 
@@ -341,12 +434,11 @@
                                                                    Verb.BATCH_STORE,
                                                                    Verb.BATCH_REMOVE);
 
-
     private static final class DroppedMessages
     {
         final DroppedMessageMetrics metrics;
-        final AtomicInteger droppedInternalTimeout;
-        final AtomicInteger droppedCrossNodeTimeout;
+        final AtomicInteger droppedInternal;
+        final AtomicInteger droppedCrossNode;
 
         DroppedMessages(Verb verb)
         {
@@ -356,8 +448,8 @@
         DroppedMessages(DroppedMessageMetrics metrics)
         {
             this.metrics = metrics;
-            this.droppedInternalTimeout = new AtomicInteger(0);
-            this.droppedCrossNodeTimeout = new AtomicInteger(0);
+            this.droppedInternal = new AtomicInteger(0);
+            this.droppedCrossNode = new AtomicInteger(0);
         }
     }
 
@@ -381,20 +473,8 @@
     // message sinks are a testing hook
     private final Set<IMessageSink> messageSinks = new CopyOnWriteArraySet<>();
 
-    public void addMessageSink(IMessageSink sink)
-    {
-        messageSinks.add(sink);
-    }
-
-    public void removeMessageSink(IMessageSink sink)
-    {
-        messageSinks.remove(sink);
-    }
-
-    public void clearMessageSinks()
-    {
-        messageSinks.clear();
-    }
+    // back-pressure implementation
+    private final BackPressureStrategy backPressure = DatabaseDescriptor.getBackPressureStrategy();
 
     private static class MSHandle
     {
@@ -440,15 +520,25 @@
             public Object apply(Pair<Integer, ExpiringMap.CacheableObject<CallbackInfo>> pair)
             {
                 final CallbackInfo expiredCallbackInfo = pair.right.value;
+
                 maybeAddLatency(expiredCallbackInfo.callback, expiredCallbackInfo.target, pair.right.timeout);
+
                 ConnectionMetrics.totalTimeouts.mark();
                 getConnectionPool(expiredCallbackInfo.target).incrementTimeout();
+
+                if (expiredCallbackInfo.callback.supportsBackPressure())
+                {
+                    updateBackPressureOnReceive(expiredCallbackInfo.target, expiredCallbackInfo.callback, true);
+                }
+
                 if (expiredCallbackInfo.isFailureCallback())
                 {
-                    StageManager.getStage(Stage.INTERNAL_RESPONSE).submit(new Runnable() {
+                    StageManager.getStage(Stage.INTERNAL_RESPONSE).submit(new Runnable()
+                    {
                         @Override
-                        public void run() {
-                            ((IAsyncCallbackWithFailure)expiredCallbackInfo.callback).onFailure(expiredCallbackInfo.target);
+                        public void run()
+                        {
+                            ((IAsyncCallbackWithFailure)expiredCallbackInfo.callback).onFailure(expiredCallbackInfo.target, RequestFailureReason.UNKNOWN);
                         }
                     });
                 }
@@ -471,6 +561,76 @@
         }
     }
 
+    public void addMessageSink(IMessageSink sink)
+    {
+        messageSinks.add(sink);
+    }
+
+    public void removeMessageSink(IMessageSink sink)
+    {
+        messageSinks.remove(sink);
+    }
+
+    public void clearMessageSinks()
+    {
+        messageSinks.clear();
+    }
+
+    /**
+     * Updates the back-pressure state on sending to the given host if enabled and the given message callback supports it.
+     *
+     * @param host The replica host the back-pressure state refers to.
+     * @param callback The message callback.
+     * @param message The actual message.
+     */
+    public void updateBackPressureOnSend(InetAddress host, IAsyncCallback callback, MessageOut<?> message)
+    {
+        if (DatabaseDescriptor.backPressureEnabled() && callback.supportsBackPressure())
+        {
+            BackPressureState backPressureState = getConnectionPool(host).getBackPressureState();
+            backPressureState.onMessageSent(message);
+        }
+    }
+
+    /**
+     * Updates the back-pressure state on reception from the given host if enabled and the given message callback supports it.
+     *
+     * @param host The replica host the back-pressure state refers to.
+     * @param callback The message callback.
+     * @param timeout True if updated following a timeout, false otherwise.
+     */
+    public void updateBackPressureOnReceive(InetAddress host, IAsyncCallback callback, boolean timeout)
+    {
+        if (DatabaseDescriptor.backPressureEnabled() && callback.supportsBackPressure())
+        {
+            BackPressureState backPressureState = getConnectionPool(host).getBackPressureState();
+            if (!timeout)
+                backPressureState.onResponseReceived();
+            else
+                backPressureState.onResponseTimeout();
+        }
+    }
+
+    /**
+     * Applies back-pressure for the given hosts, according to the configured strategy.
+     *
+     * If the local host is present, it is removed from the pool, as back-pressure is only applied
+     * to remote hosts.
+     *
+     * @param hosts The hosts to apply back-pressure to.
+     * @param timeoutInNanos The max back-pressure timeout.
+     */
+    public void applyBackPressure(Iterable<InetAddress> hosts, long timeoutInNanos)
+    {
+        if (DatabaseDescriptor.backPressureEnabled())
+        {
+            backPressure.apply(StreamSupport.stream(hosts.spliterator(), false)
+                    .filter(h -> !h.equals(FBUtilities.getBroadcastAddress()))
+                    .map(h -> getConnectionPool(h).getBackPressureState())
+                    .collect(Collectors.toSet()), timeoutInNanos, TimeUnit.NANOSECONDS);
+        }
+    }
+
     /**
      * Track latency information for the dynamic snitch
      *
@@ -625,7 +785,7 @@
         OutboundTcpConnectionPool cp = connectionManagers.get(to);
         if (cp == null)
         {
-            cp = new OutboundTcpConnectionPool(to);
+            cp = new OutboundTcpConnectionPool(to, backPressure.newState(to));
             OutboundTcpConnectionPool existingPool = connectionManagers.putIfAbsent(to, cp);
             if (existingPool != null)
                 cp = existingPool;
@@ -731,6 +891,7 @@
     public int sendRR(MessageOut message, InetAddress to, IAsyncCallback cb, long timeout, boolean failureCallback)
     {
         int id = addCallback(cb, message, to, timeout, failureCallback);
+        updateBackPressureOnSend(to, cb, message);
         sendOneWay(failureCallback ? message.withParameter(FAILURE_CALLBACK_PARAM, ONE_BYTE) : message, id, to);
         return id;
     }
@@ -753,6 +914,7 @@
                       boolean allowHints)
     {
         int id = addCallback(handler, message, to, message.getTimeout(), handler.consistencyLevel, allowHints);
+        updateBackPressureOnSend(to, handler, message);
         sendOneWay(message.withParameter(FAILURE_CALLBACK_PARAM, ONE_BYTE), id, to);
         return id;
     }
@@ -855,7 +1017,7 @@
         }
     }
 
-    public void receive(MessageIn message, int id, long timestamp, boolean isCrossNodeTimestamp)
+    public void receive(MessageIn message, int id)
     {
         TraceState state = Tracing.instance.initializeFromMessage(message);
         if (state != null)
@@ -866,7 +1028,7 @@
             if (!ms.allowIncomingMessage(message, id))
                 return;
 
-        Runnable runnable = new MessageDeliveryTask(message, id, timestamp, isCrossNodeTimestamp);
+        Runnable runnable = new MessageDeliveryTask(message, id);
         LocalAwareExecutorService stage = StageManager.getStage(message.getMessageType());
         assert stage != null : "No stage for message type " + message.verb;
 
@@ -1005,24 +1167,76 @@
         return versions.containsKey(endpoint);
     }
 
+    public void incrementDroppedMutations(Optional<IMutation> mutationOpt, long timeTaken)
+    {
+        if (mutationOpt.isPresent())
+        {
+            updateDroppedMutationCount(mutationOpt.get());
+        }
+        incrementDroppedMessages(Verb.MUTATION, timeTaken);
+    }
+
     public void incrementDroppedMessages(Verb verb)
     {
         incrementDroppedMessages(verb, false);
     }
 
-    public void incrementDroppedMessages(Verb verb, boolean isCrossNodeTimeout)
+    public void incrementDroppedMessages(Verb verb, long timeTaken)
     {
-        assert DROPPABLE_VERBS.contains(verb) : "Verb " + verb + " should not legally be dropped";
-        incrementDroppedMessages(droppedMessagesMap.get(verb), isCrossNodeTimeout);
+        incrementDroppedMessages(verb, timeTaken, false);
     }
 
-    private void incrementDroppedMessages(DroppedMessages droppedMessages, boolean isCrossNodeTimeout)
+    public void incrementDroppedMessages(MessageIn message, long timeTaken)
+    {
+        if (message.payload instanceof IMutation)
+        {
+            updateDroppedMutationCount((IMutation) message.payload);
+        }
+        incrementDroppedMessages(message.verb, timeTaken, message.isCrossNode());
+    }
+
+    public void incrementDroppedMessages(Verb verb, long timeTaken, boolean isCrossNode)
+    {
+        assert DROPPABLE_VERBS.contains(verb) : "Verb " + verb + " should not legally be dropped";
+        incrementDroppedMessages(droppedMessagesMap.get(verb), timeTaken, isCrossNode);
+    }
+
+    public void incrementDroppedMessages(Verb verb, boolean isCrossNode)
+    {
+        assert DROPPABLE_VERBS.contains(verb) : "Verb " + verb + " should not legally be dropped";
+        incrementDroppedMessages(droppedMessagesMap.get(verb), isCrossNode);
+    }
+
+    private void updateDroppedMutationCount(IMutation mutation)
+    {
+        assert mutation != null : "Mutation should not be null when updating dropped mutations count";
+
+        for (UUID columnFamilyId : mutation.getColumnFamilyIds())
+        {
+            ColumnFamilyStore cfs = Keyspace.open(mutation.getKeyspaceName()).getColumnFamilyStore(columnFamilyId);
+            if (cfs != null)
+            {
+                cfs.metric.droppedMutations.inc();
+            }
+        }
+    }
+
+    private void incrementDroppedMessages(DroppedMessages droppedMessages, long timeTaken, boolean isCrossNode)
+    {
+        if (isCrossNode)
+            droppedMessages.metrics.crossNodeDroppedLatency.update(timeTaken, TimeUnit.MILLISECONDS);
+        else
+            droppedMessages.metrics.internalDroppedLatency.update(timeTaken, TimeUnit.MILLISECONDS);
+        incrementDroppedMessages(droppedMessages, isCrossNode);
+    }
+
+    private void incrementDroppedMessages(DroppedMessages droppedMessages, boolean isCrossNode)
     {
         droppedMessages.metrics.dropped.mark();
-        if (isCrossNodeTimeout)
-            droppedMessages.droppedCrossNodeTimeout.incrementAndGet();
+        if (isCrossNode)
+            droppedMessages.droppedCrossNode.incrementAndGet();
         else
-            droppedMessages.droppedInternalTimeout.incrementAndGet();
+            droppedMessages.droppedInternal.incrementAndGet();
     }
 
     private void logDroppedMessages()
@@ -1044,15 +1258,18 @@
             Verb verb = entry.getKey();
             DroppedMessages droppedMessages = entry.getValue();
 
-            int droppedInternalTimeout = droppedMessages.droppedInternalTimeout.getAndSet(0);
-            int droppedCrossNodeTimeout = droppedMessages.droppedCrossNodeTimeout.getAndSet(0);
-            if (droppedInternalTimeout > 0 || droppedCrossNodeTimeout > 0)
+            int droppedInternal = droppedMessages.droppedInternal.getAndSet(0);
+            int droppedCrossNode = droppedMessages.droppedCrossNode.getAndSet(0);
+            if (droppedInternal > 0 || droppedCrossNode > 0)
             {
-                ret.add(String.format("%s messages were dropped in last %d ms: %d for internal timeout and %d for cross node timeout",
-                                      verb,
-                                      LOG_DROPPED_INTERVAL_IN_MS,
-                                      droppedInternalTimeout,
-                                      droppedCrossNodeTimeout));
+                ret.add(String.format("%s messages were dropped in last %d ms: %d internal and %d cross node."
+                                     + " Mean internal dropped latency: %d ms and Mean cross-node dropped latency: %d ms",
+                                     verb,
+                                     LOG_DROPPED_INTERVAL_IN_MS,
+                                     droppedInternal,
+                                     droppedCrossNode,
+                                     TimeUnit.NANOSECONDS.toMillis((long)droppedMessages.metrics.internalDroppedLatency.getSnapshot().getMean()),
+                                     TimeUnit.NANOSECONDS.toMillis((long)droppedMessages.metrics.crossNodeDroppedLatency.getSnapshot().getMean())));
             }
         }
         return ret;
@@ -1279,6 +1496,27 @@
         return result;
     }
 
+    public Map<String, Double> getBackPressurePerHost()
+    {
+        Map<String, Double> map = new HashMap<>(connectionManagers.size());
+        for (Map.Entry<InetAddress, OutboundTcpConnectionPool> entry : connectionManagers.entrySet())
+            map.put(entry.getKey().getHostAddress(), entry.getValue().getBackPressureState().getBackPressureRateLimit());
+
+        return map;
+    }
+
+    @Override
+    public void setBackPressureEnabled(boolean enabled)
+    {
+        DatabaseDescriptor.setBackPressureEnabled(enabled);
+    }
+
+    @Override
+    public boolean isBackPressureEnabled()
+    {
+        return DatabaseDescriptor.backPressureEnabled();
+    }
+
     public static IPartitioner globalPartitioner()
     {
         return StorageService.instance.getTokenMetadata().partitioner;
diff --git a/src/java/org/apache/cassandra/net/MessagingServiceMBean.java b/src/java/org/apache/cassandra/net/MessagingServiceMBean.java
index 3bcb0d5..b2e79e0 100644
--- a/src/java/org/apache/cassandra/net/MessagingServiceMBean.java
+++ b/src/java/org/apache/cassandra/net/MessagingServiceMBean.java
@@ -23,8 +23,7 @@
 import java.util.Map;
 
 /**
- * MBean exposing MessagingService metrics.
- * - OutboundConnectionPools - Command/Response - Pending/Completed Tasks
+ * MBean exposing MessagingService metrics plus allowing to enable/disable back-pressure.
  */
 public interface MessagingServiceMBean
 {
@@ -88,5 +87,20 @@
      */
     public Map<String, Long> getTimeoutsPerHost();
 
+    /**
+     * Back-pressure rate limiting per host
+     */
+    public Map<String, Double> getBackPressurePerHost();
+
+    /**
+     * Enable/Disable back-pressure
+     */
+    public void setBackPressureEnabled(boolean enabled);
+
+    /**
+     * Get back-pressure enabled state
+     */
+    public boolean isBackPressureEnabled();
+
     public int getVersion(String address) throws UnknownHostException;
 }
diff --git a/src/java/org/apache/cassandra/net/OutboundTcpConnection.java b/src/java/org/apache/cassandra/net/OutboundTcpConnection.java
index 8df10b1..4ae62c1 100644
--- a/src/java/org/apache/cassandra/net/OutboundTcpConnection.java
+++ b/src/java/org/apache/cassandra/net/OutboundTcpConnection.java
@@ -41,6 +41,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import io.netty.util.concurrent.FastThreadLocalThread;
 import net.jpountz.lz4.LZ4BlockOutputStream;
 import net.jpountz.lz4.LZ4Compressor;
 import net.jpountz.lz4.LZ4Factory;
@@ -66,7 +67,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.util.concurrent.Uninterruptibles;
 
-public class OutboundTcpConnection extends Thread
+public class OutboundTcpConnection extends FastThreadLocalThread
 {
     private static final Logger logger = LoggerFactory.getLogger(OutboundTcpConnection.class);
 
@@ -76,7 +77,7 @@
      * Enabled/disable TCP_NODELAY for intradc connections. Defaults to enabled.
      */
     private static final String INTRADC_TCP_NODELAY_PROPERTY = PREFIX + "otc_intradc_tcp_nodelay";
-    private static final boolean INTRADC_TCP_NODELAY = Boolean.valueOf(System.getProperty(INTRADC_TCP_NODELAY_PROPERTY, "true"));
+    private static final boolean INTRADC_TCP_NODELAY = Boolean.parseBoolean(System.getProperty(INTRADC_TCP_NODELAY_PROPERTY, "true"));
 
     /*
      * Size of buffer in output stream
@@ -117,15 +118,14 @@
 
         if (coalescingWindow < 0)
             throw new ExceptionInInitializerError(
-                    "Value provided for coalescing window must be greather than 0: " + coalescingWindow);
+                    "Value provided for coalescing window must be greater than 0: " + coalescingWindow);
 
         int otc_backlog_expiration_interval_in_ms = DatabaseDescriptor.getOtcBacklogExpirationInterval();
         if (otc_backlog_expiration_interval_in_ms != Config.otc_backlog_expiration_interval_ms_default)
             logger.info("OutboundTcpConnection backlog expiration interval set to to {}ms", otc_backlog_expiration_interval_in_ms);
-
     }
 
-    private static final MessageOut<?> CLOSE_SENTINEL = new MessageOut<MessagingService.Verb>(MessagingService.Verb.INTERNAL_RESPONSE);
+    private static final MessageOut<?> CLOSE_SENTINEL = new MessageOut(MessagingService.Verb.INTERNAL_RESPONSE);
     private volatile boolean isStopped = false;
 
     private static final int OPEN_RETRY_DELAY = 100; // ms between retries
@@ -151,9 +151,9 @@
     private volatile int currentMsgBufferCount = 0;
     private volatile int targetVersion;
 
-    public OutboundTcpConnection(OutboundTcpConnectionPool pool)
+    public OutboundTcpConnection(OutboundTcpConnectionPool pool, String name)
     {
-        super("MessagingService-Outgoing-" + pool.endPoint());
+        super("MessagingService-Outgoing-" + pool.endPoint() + "-" + name);
         this.poolReference = pool;
         cs = newCoalescingStrategy(pool.endPoint().getHostAddress());
 
@@ -202,6 +202,7 @@
 
     void closeSocket(boolean destroyThread)
     {
+        logger.debug("Enqueuing socket close for {}", poolReference.endPoint());
         isStopped = destroyThread; // Exit loop to stop the thread
         backlog.clear();
         // in the "destroyThread = true" case, enqueuing the sentinel is important mostly to unblock the backlog.take()
@@ -323,7 +324,7 @@
                 {
                     byte[] traceTypeBytes = qm.message.parameters.get(Tracing.TRACE_TYPE);
                     Tracing.TraceType traceType = traceTypeBytes == null ? Tracing.TraceType.QUERY : Tracing.TraceType.deserialize(traceTypeBytes[0]);
-                    TraceState.mutateWithTracing(ByteBuffer.wrap(sessionBytes), message, -1, traceType.getTTL());
+                    Tracing.instance.trace(ByteBuffer.wrap(sessionBytes), message, traceType.getTTL());
                 }
                 else
                 {
@@ -346,11 +347,10 @@
             disconnect();
             if (e instanceof IOException || e.getCause() instanceof IOException)
             {
-                if (logger.isTraceEnabled())
-                    logger.trace("error writing to {}", poolReference.endPoint(), e);
+                logger.debug("Error writing to {}", poolReference.endPoint(), e);
 
-                // if the message was important, such as a repair acknowledgement, put it back on the queue
-                // to retry after re-connecting.  See CASSANDRA-5393
+                // If we haven't retried this message yet, put it back on the queue to retry after re-connecting.
+                // See CASSANDRA-5393 and CASSANDRA-12192.
                 if (qm.shouldRetry())
                 {
                     try
@@ -408,13 +408,11 @@
             try
             {
                 socket.close();
-                if (logger.isTraceEnabled())
-                    logger.trace("Socket to {} closed", poolReference.endPoint());
+                logger.debug("Socket to {} closed", poolReference.endPoint());
             }
             catch (IOException e)
             {
-                if (logger.isTraceEnabled())
-                    logger.trace("exception closing connection to " + poolReference.endPoint(), e);
+                logger.debug("Exception closing connection to {}", poolReference.endPoint(), e);
             }
             out = null;
             socket = null;
@@ -424,8 +422,7 @@
     @SuppressWarnings("resource")
     private boolean connect()
     {
-        if (logger.isTraceEnabled())
-            logger.trace("attempting to connect to {}", poolReference.endPoint());
+        logger.debug("Attempting to connect to {}", poolReference.endPoint());
 
         long start = System.nanoTime();
         long timeout = TimeUnit.MILLISECONDS.toNanos(DatabaseDescriptor.getRpcTimeout());
@@ -444,7 +441,7 @@
                 {
                     socket.setTcpNoDelay(DatabaseDescriptor.getInterDCTcpNoDelay());
                 }
-                if (DatabaseDescriptor.getInternodeSendBufferSize() != null)
+                if (DatabaseDescriptor.getInternodeSendBufferSize() > 0)
                 {
                     try
                     {
@@ -515,7 +512,7 @@
                 if (shouldCompressConnection())
                 {
                     out.flush();
-                    logger.trace("Upgrading OutputStream to be compressed");
+                    logger.trace("Upgrading OutputStream to {} to be compressed", poolReference.endPoint());
                     if (targetVersion < MessagingService.VERSION_21)
                     {
                         // Snappy is buffered, so no need for extra buffering output stream
@@ -533,7 +530,7 @@
                                                                             true)); // no async flushing
                     }
                 }
-
+                logger.debug("Done connecting to {}", poolReference.endPoint());
                 return true;
             }
             catch (SSLHandshakeException e)
@@ -546,8 +543,7 @@
             catch (IOException e)
             {
                 disconnect();
-                if (logger.isTraceEnabled())
-                    logger.trace("unable to connect to " + poolReference.endPoint(), e);
+                logger.debug("Unable to connect to {}", poolReference.endPoint(), e);
                 Uninterruptibles.sleepUninterruptibly(OPEN_RETRY_DELAY, TimeUnit.MILLISECONDS);
             }
         }
@@ -558,7 +554,7 @@
     {
         final AtomicInteger version = new AtomicInteger(NO_VERSION);
         final CountDownLatch versionLatch = new CountDownLatch(1);
-        new Thread(NamedThreadFactory.threadLocalDeallocator(() ->
+        NamedThreadFactory.createThread(() ->
         {
             try
             {
@@ -578,7 +574,7 @@
                 //unblock the waiting thread on either success or fail
                 versionLatch.countDown();
             }
-        }),"HANDSHAKE-" + poolReference.endPoint()).start();
+        }, "HANDSHAKE-" + poolReference.endPoint()).start();
 
         try
         {
@@ -666,7 +662,8 @@
 
         boolean shouldRetry()
         {
-            return !droppable;
+            // retry all messages once
+            return true;
         }
 
         public long timestampNanos()
diff --git a/src/java/org/apache/cassandra/net/OutboundTcpConnectionPool.java b/src/java/org/apache/cassandra/net/OutboundTcpConnectionPool.java
index c9dfe9c..f47a598 100644
--- a/src/java/org/apache/cassandra/net/OutboundTcpConnectionPool.java
+++ b/src/java/org/apache/cassandra/net/OutboundTcpConnectionPool.java
@@ -49,15 +49,20 @@
     private InetAddress resetEndpoint;
     private ConnectionMetrics metrics;
 
-    OutboundTcpConnectionPool(InetAddress remoteEp)
+    // back-pressure state linked to this connection:
+    private final BackPressureState backPressureState;
+
+    OutboundTcpConnectionPool(InetAddress remoteEp, BackPressureState backPressureState)
     {
         id = remoteEp;
         resetEndpoint = SystemKeyspace.getPreferredIP(remoteEp);
         started = new CountDownLatch(1);
 
-        smallMessages = new OutboundTcpConnection(this);
-        largeMessages = new OutboundTcpConnection(this);
-        gossipMessages = new OutboundTcpConnection(this);
+        smallMessages = new OutboundTcpConnection(this, "Small");
+        largeMessages = new OutboundTcpConnection(this, "Large");
+        gossipMessages = new OutboundTcpConnection(this, "Gossip");
+
+        this.backPressureState = backPressureState;
     }
 
     /**
@@ -73,6 +78,11 @@
                : smallMessages;
     }
 
+    public BackPressureState getBackPressureState()
+    {
+        return backPressureState;
+    }
+
     void reset()
     {
         for (OutboundTcpConnection conn : new OutboundTcpConnection[] { smallMessages, largeMessages, gossipMessages })
@@ -121,23 +131,17 @@
         return newSocket(endPoint());
     }
 
-    // Closing the socket will close the underlying channel.
-    @SuppressWarnings("resource")
+    @SuppressWarnings("resource") // Closing the socket will close the underlying channel.
     public static Socket newSocket(InetAddress endpoint) throws IOException
     {
         // zero means 'bind on any available port.'
         if (DatabaseDescriptor.getServerEncryptionOptions().shouldEncrypt(endpoint))
         {
-            if (DatabaseDescriptor.getOutboundBindAny())
-                return SSLFactory.getSocket(DatabaseDescriptor.getServerEncryptionOptions(), endpoint, DatabaseDescriptor.getSSLStoragePort());
-            else
-                return SSLFactory.getSocket(DatabaseDescriptor.getServerEncryptionOptions(), endpoint, DatabaseDescriptor.getSSLStoragePort(), FBUtilities.getLocalAddress(), 0);
+            return SSLFactory.getSocket(DatabaseDescriptor.getServerEncryptionOptions(), endpoint, DatabaseDescriptor.getSSLStoragePort());
         }
         else
         {
             SocketChannel channel = SocketChannel.open();
-            if (!DatabaseDescriptor.getOutboundBindAny())
-                channel.bind(new InetSocketAddress(FBUtilities.getLocalAddress(), 0));
             channel.connect(new InetSocketAddress(endpoint, DatabaseDescriptor.getStoragePort()));
             return channel.socket();
         }
diff --git a/src/java/org/apache/cassandra/net/RateBasedBackPressure.java b/src/java/org/apache/cassandra/net/RateBasedBackPressure.java
new file mode 100644
index 0000000..565bf4c
--- /dev/null
+++ b/src/java/org/apache/cassandra/net/RateBasedBackPressure.java
@@ -0,0 +1,299 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.net;
+
+import java.net.InetAddress;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.RateLimiter;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.utils.NoSpamLogger;
+import org.apache.cassandra.utils.SystemTimeSource;
+import org.apache.cassandra.utils.TimeSource;
+import org.apache.cassandra.utils.concurrent.IntervalLock;
+
+/**
+ * Back-pressure algorithm based on rate limiting according to the ratio between incoming and outgoing rates, computed
+ * over a sliding time window with size equal to write RPC timeout.
+ */
+public class RateBasedBackPressure implements BackPressureStrategy<RateBasedBackPressureState>
+{
+    static final String HIGH_RATIO = "high_ratio";
+    static final String FACTOR = "factor";
+    static final String FLOW = "flow";
+    private static final String BACK_PRESSURE_HIGH_RATIO = "0.90";
+    private static final String BACK_PRESSURE_FACTOR = "5";
+    private static final String BACK_PRESSURE_FLOW = "FAST";
+
+    private static final Logger logger = LoggerFactory.getLogger(RateBasedBackPressure.class);
+    private static final NoSpamLogger tenSecsNoSpamLogger = NoSpamLogger.getLogger(logger, 10, TimeUnit.SECONDS);
+    private static final NoSpamLogger oneMinNoSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES);
+
+    protected final TimeSource timeSource;
+    protected final double highRatio;
+    protected final int factor;
+    protected final Flow flow;
+    protected final long windowSize;
+
+    private final Cache<Set<RateBasedBackPressureState>, IntervalRateLimiter> rateLimiters =
+            CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.HOURS).build();
+
+    enum Flow
+    {
+        FAST,
+        SLOW
+    }
+
+    public static ParameterizedClass withDefaultParams()
+    {
+        return new ParameterizedClass(RateBasedBackPressure.class.getName(),
+                                      ImmutableMap.of(HIGH_RATIO, BACK_PRESSURE_HIGH_RATIO,
+                                                      FACTOR, BACK_PRESSURE_FACTOR,
+                                                      FLOW, BACK_PRESSURE_FLOW));
+    }
+
+    public RateBasedBackPressure(Map<String, Object> args)
+    {
+        this(args, new SystemTimeSource(), DatabaseDescriptor.getWriteRpcTimeout());
+    }
+
+    @VisibleForTesting
+    public RateBasedBackPressure(Map<String, Object> args, TimeSource timeSource, long windowSize)
+    {
+        if (args.size() != 3)
+        {
+            throw new IllegalArgumentException(RateBasedBackPressure.class.getCanonicalName()
+                    + " requires 3 arguments: high ratio, back-pressure factor and flow type.");
+        }
+
+        try
+        {
+            highRatio = Double.parseDouble(args.getOrDefault(HIGH_RATIO, "").toString().trim());
+            factor = Integer.parseInt(args.getOrDefault(FACTOR, "").toString().trim());
+            flow = Flow.valueOf(args.getOrDefault(FLOW, "").toString().trim().toUpperCase());
+        }
+        catch (Exception ex)
+        {
+            throw new IllegalArgumentException(ex.getMessage(), ex);
+        }
+
+        if (highRatio <= 0 || highRatio > 1)
+        {
+            throw new IllegalArgumentException("Back-pressure high ratio must be > 0 and <= 1");
+        }
+        if (factor < 1)
+        {
+            throw new IllegalArgumentException("Back-pressure factor must be >= 1");
+        }
+        if (windowSize < 10)
+        {
+            throw new IllegalArgumentException("Back-pressure window size must be >= 10");
+        }
+
+        this.timeSource = timeSource;
+        this.windowSize = windowSize;
+
+        logger.info("Initialized back-pressure with high ratio: {}, factor: {}, flow: {}, window size: {}.",
+                    highRatio, factor, flow, windowSize);
+    }
+
+    @Override
+    public void apply(Set<RateBasedBackPressureState> states, long timeout, TimeUnit unit)
+    {
+        // Go through the back-pressure states, try updating each of them and collect min/max rates:
+        boolean isUpdated = false;
+        double minRateLimit = Double.POSITIVE_INFINITY;
+        double maxRateLimit = Double.NEGATIVE_INFINITY;
+        double minIncomingRate = Double.POSITIVE_INFINITY;
+        RateLimiter currentMin = null;
+        RateLimiter currentMax = null;
+        for (RateBasedBackPressureState backPressure : states)
+        {
+            // Get the incoming/outgoing rates:
+            double incomingRate = backPressure.incomingRate.get(TimeUnit.SECONDS);
+            double outgoingRate = backPressure.outgoingRate.get(TimeUnit.SECONDS);
+            // Compute the min incoming rate:
+            if (incomingRate < minIncomingRate)
+                minIncomingRate = incomingRate;
+
+            // Try acquiring the interval lock:
+            if (backPressure.tryIntervalLock(windowSize))
+            {
+                // If acquired, proceed updating thi back-pressure state rate limit:
+                isUpdated = true;
+                try
+                {
+                    RateLimiter limiter = backPressure.rateLimiter;
+
+                    // If we have sent any outgoing requests during this time window, go ahead with rate limiting
+                    // (this is safe against concurrent back-pressure state updates thanks to the rw-locking in
+                    // RateBasedBackPressureState):
+                    if (outgoingRate > 0)
+                    {
+                        // Compute the incoming/outgoing ratio:
+                        double actualRatio = incomingRate / outgoingRate;
+
+                        // If the ratio is above the high mark, try growing by the back-pressure factor:
+                        double limiterRate = limiter.getRate();
+                        if (actualRatio >= highRatio)
+                        {
+                            // Only if the outgoing rate is able to keep up with the rate increase:
+                            if (limiterRate <= outgoingRate)
+                            {
+                                double newRate = limiterRate + ((limiterRate * factor) / 100);
+                                if (newRate > 0 && newRate != Double.POSITIVE_INFINITY)
+                                {
+                                    limiter.setRate(newRate);
+                                }
+                            }
+                        }
+                        // If below, set the rate limiter at the incoming rate, decreased by factor:
+                        else
+                        {
+                            // Only if the new rate is actually less than the actual rate:
+                            double newRate = incomingRate - ((incomingRate * factor) / 100);
+                            if (newRate > 0 && newRate < limiterRate)
+                            {
+                                limiter.setRate(newRate);
+                            }
+                        }
+                        if (logger.isTraceEnabled())
+                        {
+                            logger.trace("Back-pressure state for {}: incoming rate {}, outgoing rate {}, ratio {}, rate limiting {}",
+                                         backPressure.getHost(), incomingRate, outgoingRate, actualRatio, limiter.getRate());
+                        }
+                    }
+                    // Otherwise reset the rate limiter:
+                    else
+                    {
+                        limiter.setRate(Double.POSITIVE_INFINITY);
+                    }
+
+                    // Housekeeping: pruning windows and resetting the last check timestamp!
+                    backPressure.incomingRate.prune();
+                    backPressure.outgoingRate.prune();
+                }
+                finally
+                {
+                    backPressure.releaseIntervalLock();
+                }
+            }
+            if (backPressure.rateLimiter.getRate() <= minRateLimit)
+            {
+                minRateLimit = backPressure.rateLimiter.getRate();
+                currentMin = backPressure.rateLimiter;
+            }
+            if (backPressure.rateLimiter.getRate() >= maxRateLimit)
+            {
+                maxRateLimit = backPressure.rateLimiter.getRate();
+                currentMax = backPressure.rateLimiter;
+            }
+        }
+
+        // Now find the rate limiter corresponding to the replica group represented by these back-pressure states:
+        if (!states.isEmpty())
+        {
+            try
+            {
+                // Get the rate limiter:
+                IntervalRateLimiter rateLimiter = rateLimiters.get(states, () -> new IntervalRateLimiter(timeSource));
+
+                // If the back-pressure was updated and we acquire the interval lock for the rate limiter of this group:
+                if (isUpdated && rateLimiter.tryIntervalLock(windowSize))
+                {
+                    try
+                    {
+                        // Update the rate limiter value based on the configured flow:
+                        if (flow.equals(Flow.FAST))
+                            rateLimiter.limiter = currentMax;
+                        else
+                            rateLimiter.limiter = currentMin;
+
+                        tenSecsNoSpamLogger.info("{} currently applied for remote replicas: {}", rateLimiter.limiter, states);
+                    }
+                    finally
+                    {
+                        rateLimiter.releaseIntervalLock();
+                    }
+                }
+                // Assigning a single rate limiter per replica group once per window size allows the back-pressure rate
+                // limiting to be stable within the group itself.
+
+                // Finally apply the rate limit with a max pause time equal to the provided timeout minus the
+                // response time computed from the incoming rate, to reduce the number of client timeouts by taking into
+                // account how long it could take to process responses after back-pressure:
+                long responseTimeInNanos = (long) (TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS) / minIncomingRate);
+                doRateLimit(rateLimiter.limiter, Math.max(0, TimeUnit.NANOSECONDS.convert(timeout, unit) - responseTimeInNanos));
+            }
+            catch (ExecutionException ex)
+            {
+                throw new IllegalStateException(ex);
+            }
+        }
+    }
+
+    @Override
+    public RateBasedBackPressureState newState(InetAddress host)
+    {
+        return new RateBasedBackPressureState(host, timeSource, windowSize);
+    }
+
+    @VisibleForTesting
+    RateLimiter getRateLimiterForReplicaGroup(Set<RateBasedBackPressureState> states)
+    {
+        IntervalRateLimiter rateLimiter = rateLimiters.getIfPresent(states);
+        return rateLimiter != null ? rateLimiter.limiter : RateLimiter.create(Double.POSITIVE_INFINITY);
+    }
+
+    @VisibleForTesting
+    boolean doRateLimit(RateLimiter rateLimiter, long timeoutInNanos)
+    {
+        if (!rateLimiter.tryAcquire(1, timeoutInNanos, TimeUnit.NANOSECONDS))
+        {
+            timeSource.sleepUninterruptibly(timeoutInNanos, TimeUnit.NANOSECONDS);
+            oneMinNoSpamLogger.info("Cannot apply {} due to exceeding write timeout, pausing {} nanoseconds instead.",
+                                    rateLimiter, timeoutInNanos);
+
+            return false;
+        }
+
+        return true;
+    }
+
+    private static class IntervalRateLimiter extends IntervalLock
+    {
+        public volatile RateLimiter limiter = RateLimiter.create(Double.POSITIVE_INFINITY);
+
+        IntervalRateLimiter(TimeSource timeSource)
+        {
+            super(timeSource);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/net/RateBasedBackPressureState.java b/src/java/org/apache/cassandra/net/RateBasedBackPressureState.java
new file mode 100644
index 0000000..c19f277
--- /dev/null
+++ b/src/java/org/apache/cassandra/net/RateBasedBackPressureState.java
@@ -0,0 +1,133 @@
+/*
+ * 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.net;
+
+import java.net.InetAddress;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.util.concurrent.RateLimiter;
+
+import org.apache.cassandra.utils.SlidingTimeRate;
+import org.apache.cassandra.utils.TimeSource;
+import org.apache.cassandra.utils.concurrent.IntervalLock;
+
+/**
+ * The rate-based back-pressure state, tracked per replica host.
+ * <br/><br/>
+ *
+ * This back-pressure state is made up of the following attributes:
+ * <ul>
+ * <li>windowSize: the length of the back-pressure window in milliseconds.</li>
+ * <li>incomingRate: the rate of back-pressure supporting incoming messages.</li>
+ * <li>outgoingRate: the rate of back-pressure supporting outgoing messages.</li>
+ * <li>rateLimiter: the rate limiter to eventually apply to outgoing messages.</li>
+ * </ul>
+ * <br/>
+ * The incomingRate and outgoingRate are updated together when a response is received to guarantee consistency between
+ * the two.
+ * <br/>
+ * It also provides methods to exclusively lock/release back-pressure windows at given intervals;
+ * this allows to apply back-pressure even under concurrent modifications. Please also note a read lock is acquired
+ * during response processing so that no concurrent rate updates can screw rate computations.
+ */
+class RateBasedBackPressureState extends IntervalLock implements BackPressureState
+{
+    private final InetAddress host;
+    private final long windowSize;
+    final SlidingTimeRate incomingRate;
+    final SlidingTimeRate outgoingRate;
+    final RateLimiter rateLimiter;
+
+    RateBasedBackPressureState(InetAddress host, TimeSource timeSource, long windowSize)
+    {
+        super(timeSource);
+        this.host = host;
+        this.windowSize = windowSize;
+        this.incomingRate = new SlidingTimeRate(timeSource, this.windowSize, this.windowSize / 10, TimeUnit.MILLISECONDS);
+        this.outgoingRate = new SlidingTimeRate(timeSource, this.windowSize, this.windowSize / 10, TimeUnit.MILLISECONDS);
+        this.rateLimiter = RateLimiter.create(Double.POSITIVE_INFINITY);
+    }
+
+    @Override
+    public void onMessageSent(MessageOut<?> message) {}
+
+    @Override
+    public void onResponseReceived()
+    {
+        readLock().lock();
+        try
+        {
+            incomingRate.update(1);
+            outgoingRate.update(1);
+        }
+        finally
+        {
+            readLock().unlock();
+        }
+    }
+
+    @Override
+    public void onResponseTimeout()
+    {
+        readLock().lock();
+        try
+        {
+            outgoingRate.update(1);
+        }
+        finally
+        {
+            readLock().unlock();
+        }
+    }
+
+    @Override
+    public double getBackPressureRateLimit()
+    {
+        return rateLimiter.getRate();
+    }
+
+    @Override
+    public InetAddress getHost()
+    {
+        return host;
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+        if (obj instanceof RateBasedBackPressureState)
+        {
+            RateBasedBackPressureState other = (RateBasedBackPressureState) obj;
+            return this.host.equals(other.host);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return this.host.hashCode();
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.format("[host: %s, incoming rate: %.3f, outgoing rate: %.3f, rate limit: %.3f]",
+                             host, incomingRate.get(TimeUnit.SECONDS), outgoingRate.get(TimeUnit.SECONDS), rateLimiter.getRate());
+    }
+}
diff --git a/src/java/org/apache/cassandra/net/ResponseVerbHandler.java b/src/java/org/apache/cassandra/net/ResponseVerbHandler.java
index 28ed365..fe22e42 100644
--- a/src/java/org/apache/cassandra/net/ResponseVerbHandler.java
+++ b/src/java/org/apache/cassandra/net/ResponseVerbHandler.java
@@ -44,7 +44,7 @@
         IAsyncCallback cb = callbackInfo.callback;
         if (message.isFailureResponse())
         {
-            ((IAsyncCallbackWithFailure) cb).onFailure(message.from);
+            ((IAsyncCallbackWithFailure) cb).onFailure(message.from, message.getFailureReason());
         }
         else
         {
@@ -52,5 +52,10 @@
             MessagingService.instance().maybeAddLatency(cb, message.from, latency);
             cb.response(message);
         }
+
+        if (callbackInfo.callback.supportsBackPressure())
+        {
+            MessagingService.instance().updateBackPressureOnReceive(message.from, cb, false);
+        }
     }
 }
diff --git a/src/java/org/apache/cassandra/notifications/MemtableDiscardedNotification.java b/src/java/org/apache/cassandra/notifications/MemtableDiscardedNotification.java
new file mode 100644
index 0000000..778cad0
--- /dev/null
+++ b/src/java/org/apache/cassandra/notifications/MemtableDiscardedNotification.java
@@ -0,0 +1,30 @@
+/*
+ * 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.notifications;
+
+import org.apache.cassandra.db.Memtable;
+
+public class MemtableDiscardedNotification implements INotification
+{
+    public final Memtable memtable;
+
+    public MemtableDiscardedNotification(Memtable discarded)
+    {
+        this.memtable = discarded;
+    }
+}
diff --git a/src/java/org/apache/cassandra/notifications/MemtableSwitchedNotification.java b/src/java/org/apache/cassandra/notifications/MemtableSwitchedNotification.java
new file mode 100644
index 0000000..946de4e
--- /dev/null
+++ b/src/java/org/apache/cassandra/notifications/MemtableSwitchedNotification.java
@@ -0,0 +1,30 @@
+/*
+ * 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.notifications;
+
+import org.apache.cassandra.db.Memtable;
+
+public class MemtableSwitchedNotification implements INotification
+{
+    public final Memtable memtable;
+
+    public MemtableSwitchedNotification(Memtable switched)
+    {
+        this.memtable = switched;
+    }
+}
diff --git a/src/java/org/apache/cassandra/notifications/SSTableAddedNotification.java b/src/java/org/apache/cassandra/notifications/SSTableAddedNotification.java
index 81e8cc3..56d6130 100644
--- a/src/java/org/apache/cassandra/notifications/SSTableAddedNotification.java
+++ b/src/java/org/apache/cassandra/notifications/SSTableAddedNotification.java
@@ -22,7 +22,6 @@
 public class SSTableAddedNotification implements INotification
 {
     public final Iterable<SSTableReader> added;
-
     public SSTableAddedNotification(Iterable<SSTableReader> added)
     {
         this.added = added;
diff --git a/src/java/org/apache/cassandra/notifications/SSTableRepairStatusChanged.java b/src/java/org/apache/cassandra/notifications/SSTableRepairStatusChanged.java
index d1398bc..8c48fa8 100644
--- a/src/java/org/apache/cassandra/notifications/SSTableRepairStatusChanged.java
+++ b/src/java/org/apache/cassandra/notifications/SSTableRepairStatusChanged.java
@@ -24,10 +24,10 @@
 
 public class SSTableRepairStatusChanged implements INotification
 {
-    public final Collection<SSTableReader> sstable;
+    public final Collection<SSTableReader> sstables;
 
     public SSTableRepairStatusChanged(Collection<SSTableReader> repairStatusChanged)
     {
-        this.sstable = repairStatusChanged;
+        this.sstables = repairStatusChanged;
     }
 }
diff --git a/src/java/org/apache/cassandra/repair/AnticompactionTask.java b/src/java/org/apache/cassandra/repair/AnticompactionTask.java
index 02e18a7..6e6bb65 100644
--- a/src/java/org/apache/cassandra/repair/AnticompactionTask.java
+++ b/src/java/org/apache/cassandra/repair/AnticompactionTask.java
@@ -33,6 +33,7 @@
 import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.exceptions.RequestFailureReason;
 import org.apache.cassandra.gms.ApplicationState;
 import org.apache.cassandra.gms.EndpointState;
 import org.apache.cassandra.gms.FailureDetector;
@@ -132,7 +133,7 @@
             return false;
         }
 
-        public void onFailure(InetAddress from)
+        public void onFailure(InetAddress from, RequestFailureReason failureReason)
         {
             maybeSetException(new RuntimeException("Anticompaction failed or timed out in " + from));
         }
diff --git a/src/java/org/apache/cassandra/repair/LocalSyncTask.java b/src/java/org/apache/cassandra/repair/LocalSyncTask.java
index 5d43868..57a3551 100644
--- a/src/java/org/apache/cassandra/repair/LocalSyncTask.java
+++ b/src/java/org/apache/cassandra/repair/LocalSyncTask.java
@@ -47,10 +47,13 @@
 
     private final long repairedAt;
 
-    public LocalSyncTask(RepairJobDesc desc, InetAddress firstEndpoint, InetAddress secondEndpoint, List<Range<Token>> rangesToSync, long repairedAt)
+    private final boolean pullRepair;
+
+    public LocalSyncTask(RepairJobDesc desc, InetAddress firstEndpoint, InetAddress secondEndpoint, List<Range<Token>> rangesToSync, long repairedAt, boolean pullRepair)
     {
         super(desc, firstEndpoint, secondEndpoint, rangesToSync);
         this.repairedAt = repairedAt;
+        this.pullRepair = pullRepair;
     }
 
     /**
@@ -73,13 +76,17 @@
             isIncremental = prs.isIncremental;
         }
         Tracing.traceRepair(message);
-        new StreamPlan("Repair", repairedAt, 1, false, isIncremental).listeners(this)
+        StreamPlan plan = new StreamPlan("Repair", repairedAt, 1, false, isIncremental, false).listeners(this)
                                             .flushBeforeTransfer(true)
                                             // request ranges from the remote node
-                                            .requestRanges(dst, preferred, desc.keyspace, differences, desc.columnFamily)
-                                            // send ranges to the remote node
-                                            .transferRanges(dst, preferred, desc.keyspace, differences, desc.columnFamily)
-                                            .execute();
+                                            .requestRanges(dst, preferred, desc.keyspace, differences, desc.columnFamily);
+        if (!pullRepair)
+        {
+            // send ranges to the remote node if we are not performing a pull repair
+            plan.transferRanges(dst, preferred, desc.keyspace, differences, desc.columnFamily);
+        }
+
+        plan.execute();
     }
 
     public void handleStreamEvent(StreamEvent event)
@@ -98,9 +105,9 @@
                 break;
             case FILE_PROGRESS:
                 ProgressInfo pi = ((StreamEvent.ProgressEvent) event).progress;
-                state.trace("{}/{} bytes ({}%) {} idx:{}{}",
-                            new Object[] { pi.currentBytes,
-                                           pi.totalBytes,
+                state.trace("{}/{} ({}%) {} idx:{}{}",
+                            new Object[] { FBUtilities.prettyPrintMemory(pi.currentBytes),
+                                           FBUtilities.prettyPrintMemory(pi.totalBytes),
                                            pi.currentBytes * 100 / pi.totalBytes,
                                            pi.direction == ProgressInfo.Direction.OUT ? "sent to" : "received from",
                                            pi.sessionIndex,
diff --git a/src/java/org/apache/cassandra/repair/RepairJob.java b/src/java/org/apache/cassandra/repair/RepairJob.java
index ef2380c..eb87465 100644
--- a/src/java/org/apache/cassandra/repair/RepairJob.java
+++ b/src/java/org/apache/cassandra/repair/RepairJob.java
@@ -103,7 +103,7 @@
             ListenableFuture<List<InetAddress>> allSnapshotTasks = Futures.allAsList(snapshotTasks);
             validations = Futures.transform(allSnapshotTasks, new AsyncFunction<List<InetAddress>, List<TreeResponse>>()
             {
-                public ListenableFuture<List<TreeResponse>> apply(List<InetAddress> endpoints) throws Exception
+                public ListenableFuture<List<TreeResponse>> apply(List<InetAddress> endpoints)
                 {
                     if (parallelismDegree == RepairParallelism.SEQUENTIAL)
                         return sendSequentialValidationRequest(endpoints);
@@ -132,7 +132,7 @@
         {
             public void onSuccess(List<SyncStat> stats)
             {
-                logger.info(String.format("[repair #%s] %s is fully synced", session.getId(), desc.columnFamily));
+                logger.info("[repair #{}] {} is fully synced", session.getId(), desc.columnFamily);
                 SystemDistributedKeyspace.successfulRepairJob(session.getId(), desc.keyspace, desc.columnFamily);
                 set(new RepairResult(desc, stats));
             }
@@ -142,7 +142,7 @@
              */
             public void onFailure(Throwable t)
             {
-                logger.warn(String.format("[repair #%s] %s sync failed", session.getId(), desc.columnFamily));
+                logger.warn("[repair #{}] {} sync failed", session.getId(), desc.columnFamily);
                 SystemDistributedKeyspace.failedRepairJob(session.getId(), desc.keyspace, desc.columnFamily, t);
                 setException(t);
             }
@@ -181,7 +181,7 @@
 
                 if (r1.endpoint.equals(local) || r2.endpoint.equals(local))
                 {
-                    task = new LocalSyncTask(desc, r1.endpoint, r2.endpoint, differences, repairedAt);
+                    task = new LocalSyncTask(desc, r1.endpoint, r2.endpoint, differences, repairedAt, session.pullRepair);
                 }
                 else
                 {
diff --git a/src/java/org/apache/cassandra/repair/RepairMessageVerbHandler.java b/src/java/org/apache/cassandra/repair/RepairMessageVerbHandler.java
index edcb4f9..52625bf 100644
--- a/src/java/org/apache/cassandra/repair/RepairMessageVerbHandler.java
+++ b/src/java/org/apache/cassandra/repair/RepairMessageVerbHandler.java
@@ -21,7 +21,6 @@
 import java.util.*;
 
 import com.google.common.base.Predicate;
-import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 import org.slf4j.Logger;
@@ -30,8 +29,6 @@
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.compaction.CompactionManager;
 import org.apache.cassandra.dht.Bounds;
-import org.apache.cassandra.dht.Range;
-import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.net.IVerbHandler;
 import org.apache.cassandra.net.MessageIn;
@@ -90,6 +87,7 @@
                                                                      desc.keyspace, desc.columnFamily), message.from, id);
                         return;
                     }
+
                     ActiveRepairService.ParentRepairSession prs = ActiveRepairService.instance.getParentRepairSession(desc.parentSessionId);
                     if (prs.isGlobal)
                     {
@@ -105,7 +103,7 @@
                                        !sstable.metadata.isIndex() && // exclude SSTables from 2i
                                        new Bounds<>(sstable.first.getToken(), sstable.last.getToken()).intersects(desc.ranges);
                             }
-                        }, true); //ephemeral snapshot, if repair fails, it will be cleaned next startup
+                        }, true, false); //ephemeral snapshot, if repair fails, it will be cleaned next startup
                     }
                     logger.debug("Enqueuing response to snapshot request {} to {}", desc.sessionId, message.from);
                     MessagingService.instance().sendReply(new MessageOut(MessagingService.Verb.INTERNAL_RESPONSE), id, message.from);
@@ -150,7 +148,7 @@
                         {
                             MessagingService.instance().sendReply(new MessageOut(MessagingService.Verb.INTERNAL_RESPONSE), id, message.from);
                         }
-                    }, MoreExecutors.sameThreadExecutor());
+                    }, MoreExecutors.directExecutor());
                     break;
 
                 case CLEANUP:
diff --git a/src/java/org/apache/cassandra/repair/RepairRunnable.java b/src/java/org/apache/cassandra/repair/RepairRunnable.java
index a7c17c9..7a9590b 100644
--- a/src/java/org/apache/cassandra/repair/RepairRunnable.java
+++ b/src/java/org/apache/cassandra/repair/RepairRunnable.java
@@ -35,6 +35,7 @@
 
 import org.apache.cassandra.concurrent.JMXConfigurableThreadPoolExecutor;
 import org.apache.cassandra.concurrent.NamedThreadFactory;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.UntypedResultSet;
@@ -73,6 +74,8 @@
 
     private final List<ProgressListener> listeners = new ArrayList<>();
 
+    private static final AtomicInteger threadCounter = new AtomicInteger(1);
+
     public RepairRunnable(StorageService storageService, int cmd, RepairOption options, String keyspace)
     {
         this.storageService = storageService;
@@ -110,7 +113,7 @@
     protected void runMayThrow() throws Exception
     {
         final TraceState traceState;
-
+        final UUID parentSession = UUIDGen.getTimeUUID();
         final String tag = "repair:" + cmd;
 
         final AtomicInteger progress = new AtomicInteger();
@@ -131,10 +134,9 @@
         }
 
         final long startTime = System.currentTimeMillis();
-        String message = String.format("Starting repair command #%d, repairing keyspace %s with %s", cmd, keyspace,
+        String message = String.format("Starting repair command #%d (%s), repairing keyspace %s with %s", cmd, parentSession, keyspace,
                                        options);
         logger.info(message);
-        fireProgressEvent(tag, new ProgressEvent(ProgressEventType.START, 0, 100, message));
         if (options.isTraced())
         {
             StringBuilder cfsb = new StringBuilder();
@@ -144,6 +146,8 @@
             UUID sessionId = Tracing.instance.newSession(Tracing.TraceType.REPAIR);
             traceState = Tracing.instance.begin("repair", ImmutableMap.of("keyspace", keyspace, "columnFamilies",
                                                                           cfsb.substring(2)));
+            message = message + " tracing with " + sessionId;
+            fireProgressEvent(tag, new ProgressEvent(ProgressEventType.START, 0, 100, message));
             Tracing.traceRepair(message);
             traceState.enableActivityNotification(tag);
             for (ProgressListener listener : listeners)
@@ -154,6 +158,7 @@
         }
         else
         {
+            fireProgressEvent(tag, new ProgressEvent(ProgressEventType.START, 0, 100, message));
             traceState = null;
         }
 
@@ -233,8 +238,7 @@
             cfnames[i] = columnFamilyStores.get(i).name;
         }
 
-        final UUID parentSession = UUIDGen.getTimeUUID();
-        SystemDistributedKeyspace.startParentRepair(parentSession, keyspace, cfnames, options.getRanges());
+        SystemDistributedKeyspace.startParentRepair(parentSession, keyspace, cfnames, options);
         long repairedAt;
         try
         {
@@ -266,6 +270,7 @@
                                                               options.getParallelism(),
                                                               p.left,
                                                               repairedAt,
+                                                              options.isPullRepair(),
                                                               executor,
                                                               cfnames);
             if (session == null)
@@ -316,7 +321,7 @@
         ListenableFuture anticompactionResult = Futures.transform(allSessions, new AsyncFunction<List<RepairSessionResult>, Object>()
         {
             @SuppressWarnings("unchecked")
-            public ListenableFuture apply(List<RepairSessionResult> results) throws Exception
+            public ListenableFuture apply(List<RepairSessionResult> results)
             {
                 // filter out null(=failed) results and get successful ranges
                 for (RepairSessionResult sessionResult : results)
@@ -403,7 +408,7 @@
 
     private Thread createQueryThread(final int cmd, final UUID sessionId)
     {
-        return new Thread(NamedThreadFactory.threadLocalDeallocator(new WrappedRunnable()
+        return NamedThreadFactory.createThread(new WrappedRunnable()
         {
             // Query events within a time interval that overlaps the last by one second. Ignore duplicates. Ignore local traces.
             // Wake up upon local trace activity. Query when notified of trace activity with a timeout that doubles every two timeouts.
@@ -414,7 +419,7 @@
                     throw new Exception("no tracestate");
 
                 String format = "select event_id, source, activity from %s.%s where session_id = ? and event_id > ? and event_id < ?;";
-                String query = String.format(format, TraceKeyspace.NAME, TraceKeyspace.EVENTS);
+                String query = String.format(format, SchemaConstants.TRACE_KEYSPACE_NAME, TraceKeyspace.EVENTS);
                 SelectStatement statement = (SelectStatement) QueryProcessor.parseStatement(query).prepare(ClientState.forInternalCalls()).statement;
 
                 ByteBuffer sessionIdBytes = ByteBufferUtil.bytes(sessionId);
@@ -449,7 +454,7 @@
                     QueryOptions options = QueryOptions.forInternalCalls(ConsistencyLevel.ONE, Lists.newArrayList(sessionIdBytes,
                                                                                                                   tminBytes,
                                                                                                                   tmaxBytes));
-                    ResultMessage.Rows rows = statement.execute(QueryState.forInternalCalls(), options);
+                    ResultMessage.Rows rows = statement.execute(QueryState.forInternalCalls(), options, System.nanoTime());
                     UntypedResultSet result = UntypedResultSet.create(rows.result);
 
                     for (UntypedResultSet.Row r : result)
@@ -470,6 +475,6 @@
                     seen[si].clear();
                 }
             }
-        }));
+        }, "Repair-Runnable-" + threadCounter.incrementAndGet());
     }
 }
diff --git a/src/java/org/apache/cassandra/repair/RepairSession.java b/src/java/org/apache/cassandra/repair/RepairSession.java
index 8cd4884..1207d36 100644
--- a/src/java/org/apache/cassandra/repair/RepairSession.java
+++ b/src/java/org/apache/cassandra/repair/RepairSession.java
@@ -87,6 +87,7 @@
     public final String keyspace;
     private final String[] cfnames;
     public final RepairParallelism parallelismDegree;
+    public final boolean pullRepair;
     /** Range to repair */
     public final Collection<Range<Token>> ranges;
     public final Set<InetAddress> endpoints;
@@ -114,6 +115,7 @@
      * @param parallelismDegree specifies the degree of parallelism when calculating the merkle trees
      * @param endpoints the data centers that should be part of the repair; null for all DCs
      * @param repairedAt when the repair occurred (millis)
+     * @param pullRepair true if the repair should be one way (from remote host to this host and only applicable between two hosts--see RepairOption)
      * @param cfnames names of columnfamilies
      */
     public RepairSession(UUID parentRepairSession,
@@ -123,6 +125,7 @@
                          RepairParallelism parallelismDegree,
                          Set<InetAddress> endpoints,
                          long repairedAt,
+                         boolean pullRepair,
                          String... cfnames)
     {
         assert cfnames.length > 0 : "Repairing no column families seems pointless, doesn't it";
@@ -135,6 +138,7 @@
         this.ranges = ranges;
         this.endpoints = endpoints;
         this.repairedAt = repairedAt;
+        this.pullRepair = pullRepair;
         this.taskExecutor = MoreExecutors.listeningDecorator(createExecutor());
     }
 
@@ -202,7 +206,7 @@
             return;
         }
 
-        logger.debug(String.format("[repair #%s] Repair completed between %s and %s on %s", getId(), nodes.endpoint1, nodes.endpoint2, desc.columnFamily));
+        logger.debug("[repair #{}] Repair completed between {} and {} on {}", getId(), nodes.endpoint1, nodes.endpoint2, desc.columnFamily);
         task.syncComplete(success);
     }
 
@@ -235,7 +239,7 @@
         if (terminated)
             return;
 
-        logger.info(String.format("[repair #%s] new session: will sync %s on range %s for %s.%s", getId(), repairedNodes(), ranges, keyspace, Arrays.toString(cfnames)));
+        logger.info("[repair #{}] new session: will sync {} on range {} for {}.{}", getId(), repairedNodes(), ranges, keyspace, Arrays.toString(cfnames));
         Tracing.traceRepair("Syncing range {}", ranges);
         SystemDistributedKeyspace.startRepairs(getId(), parentRepairSession, keyspace, cfnames, ranges, endpoints);
 
diff --git a/src/java/org/apache/cassandra/repair/SnapshotTask.java b/src/java/org/apache/cassandra/repair/SnapshotTask.java
index 94361d8..2b267a7 100644
--- a/src/java/org/apache/cassandra/repair/SnapshotTask.java
+++ b/src/java/org/apache/cassandra/repair/SnapshotTask.java
@@ -23,6 +23,7 @@
 
 import com.google.common.util.concurrent.AbstractFuture;
 
+import org.apache.cassandra.exceptions.RequestFailureReason;
 import org.apache.cassandra.net.IAsyncCallbackWithFailure;
 import org.apache.cassandra.net.MessageIn;
 import org.apache.cassandra.net.MessagingService;
@@ -73,7 +74,7 @@
 
         public boolean isLatencyForSnitch() { return false; }
 
-        public void onFailure(InetAddress from)
+        public void onFailure(InetAddress from, RequestFailureReason failureReason)
         {
             //listener.failedSnapshot();
             task.setException(new RuntimeException("Could not create snapshot at " + from));
diff --git a/src/java/org/apache/cassandra/repair/StreamingRepairTask.java b/src/java/org/apache/cassandra/repair/StreamingRepairTask.java
index 25ef06e..f5b2b1d 100644
--- a/src/java/org/apache/cassandra/repair/StreamingRepairTask.java
+++ b/src/java/org/apache/cassandra/repair/StreamingRepairTask.java
@@ -55,14 +55,14 @@
     {
         InetAddress dest = request.dst;
         InetAddress preferred = SystemKeyspace.getPreferredIP(dest);
-        logger.info(String.format("[streaming task #%s] Performing streaming repair of %d ranges with %s", desc.sessionId, request.ranges.size(), request.dst));
+        logger.info("[streaming task #{}] Performing streaming repair of {} ranges with {}", desc.sessionId, request.ranges.size(), request.dst);
         boolean isIncremental = false;
         if (desc.parentSessionId != null)
         {
             ActiveRepairService.ParentRepairSession prs = ActiveRepairService.instance.getParentRepairSession(desc.parentSessionId);
             isIncremental = prs.isIncremental;
         }
-        new StreamPlan("Repair", repairedAt, 1, false, isIncremental).listeners(this)
+        new StreamPlan("Repair", repairedAt, 1, false, isIncremental, false).listeners(this)
                                             .flushBeforeTransfer(true)
                                             // request ranges from the remote node
                                             .requestRanges(dest, preferred, desc.keyspace, request.ranges, desc.columnFamily)
@@ -82,7 +82,7 @@
      */
     public void onSuccess(StreamState state)
     {
-        logger.info(String.format("[repair #%s] streaming task succeed, returning response to %s", desc.sessionId, request.initiator));
+        logger.info("[repair #{}] streaming task succeed, returning response to {}", desc.sessionId, request.initiator);
         MessagingService.instance().sendOneWay(new SyncComplete(desc, request.src, request.dst, true).createMessage(), request.initiator);
     }
 
diff --git a/src/java/org/apache/cassandra/repair/SystemDistributedKeyspace.java b/src/java/org/apache/cassandra/repair/SystemDistributedKeyspace.java
index a922b28..eb2226b 100644
--- a/src/java/org/apache/cassandra/repair/SystemDistributedKeyspace.java
+++ b/src/java/org/apache/cassandra/repair/SystemDistributedKeyspace.java
@@ -23,27 +23,38 @@
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
 
 import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.repair.messages.RepairOption;
 import org.apache.cassandra.schema.KeyspaceMetadata;
 import org.apache.cassandra.schema.KeyspaceParams;
 import org.apache.cassandra.schema.Tables;
-import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
 
+import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
+
 public final class SystemDistributedKeyspace
 {
     private SystemDistributedKeyspace()
@@ -52,8 +63,6 @@
 
     private static final Logger logger = LoggerFactory.getLogger(SystemDistributedKeyspace.class);
 
-    public static final String NAME = "system_distributed";
-
     /**
      * Generation is used as a timestamp for automatic table creation on startup.
      * If you make any changes to the tables below, make sure to increment the
@@ -62,13 +71,16 @@
      * gen 0: original definition in 2.2
      * gen 1: (pre-)add options column to parent_repair_history in 3.0, 3.11
      * gen 2: (pre-)add coordinator_port and participants_v2 columns to repair_history in 3.0, 3.11, 4.0
+     * gen 3: gc_grace_seconds raised from 0 to 10 days in CASSANDRA-12954 in 3.11.0
      */
-    public static final long GENERATION = 2;
+    public static final long GENERATION = 3;
 
     public static final String REPAIR_HISTORY = "repair_history";
 
     public static final String PARENT_REPAIR_HISTORY = "parent_repair_history";
 
+    public static final String VIEW_BUILD_STATUS = "view_build_status";
+
     private static final CFMetaData RepairHistory =
         compile(REPAIR_HISTORY,
                 "Repair history",
@@ -106,26 +118,62 @@
                      + "options map<text, text>,"
                      + "PRIMARY KEY (parent_id))");
 
+    private static final CFMetaData ViewBuildStatus =
+    compile(VIEW_BUILD_STATUS,
+            "Materialized View build status",
+            "CREATE TABLE %s ("
+                     + "keyspace_name text,"
+                     + "view_name text,"
+                     + "host_id uuid,"
+                     + "status text,"
+                     + "PRIMARY KEY ((keyspace_name, view_name), host_id))");
+
     private static CFMetaData compile(String name, String description, String schema)
     {
-        return CFMetaData.compile(String.format(schema, name), NAME)
-                         .comment(description);
+        return CFMetaData.compile(String.format(schema, name), SchemaConstants.DISTRIBUTED_KEYSPACE_NAME)
+                         .comment(description)
+                         .gcGraceSeconds((int) TimeUnit.DAYS.toSeconds(10));
     }
 
     public static KeyspaceMetadata metadata()
     {
-        return KeyspaceMetadata.create(NAME, KeyspaceParams.simple(3), Tables.of(RepairHistory, ParentRepairHistory));
+        return KeyspaceMetadata.create(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, KeyspaceParams.simple(3), Tables.of(RepairHistory, ParentRepairHistory, ViewBuildStatus));
     }
 
-    public static void startParentRepair(UUID parent_id, String keyspaceName, String[] cfnames, Collection<Range<Token>> ranges)
+    public static void startParentRepair(UUID parent_id, String keyspaceName, String[] cfnames, RepairOption options)
     {
-
-        String query = "INSERT INTO %s.%s (parent_id, keyspace_name, columnfamily_names, requested_ranges, started_at)"+
-                                 " VALUES (%s,        '%s',          { '%s' },           { '%s' },          toTimestamp(now()))";
-        String fmtQry = String.format(query, NAME, PARENT_REPAIR_HISTORY, parent_id.toString(), keyspaceName, Joiner.on("','").join(cfnames), Joiner.on("','").join(ranges));
+        Collection<Range<Token>> ranges = options.getRanges();
+        String query = "INSERT INTO %s.%s (parent_id, keyspace_name, columnfamily_names, requested_ranges, started_at,          options)"+
+                                 " VALUES (%s,        '%s',          { '%s' },           { '%s' },          toTimestamp(now()), { %s })";
+        String fmtQry = String.format(query,
+                                      SchemaConstants.DISTRIBUTED_KEYSPACE_NAME,
+                                      PARENT_REPAIR_HISTORY,
+                                      parent_id.toString(),
+                                      keyspaceName,
+                                      Joiner.on("','").join(cfnames),
+                                      Joiner.on("','").join(ranges),
+                                      toCQLMap(options.asMap(), RepairOption.RANGES_KEY, RepairOption.COLUMNFAMILIES_KEY));
         processSilent(fmtQry);
     }
 
+    private static String toCQLMap(Map<String, String> options, String ... ignore)
+    {
+        Set<String> toIgnore = Sets.newHashSet(ignore);
+        StringBuilder map = new StringBuilder();
+        boolean first = true;
+        for (Map.Entry<String, String> entry : options.entrySet())
+        {
+            if (!toIgnore.contains(entry.getKey()))
+            {
+                if (!first)
+                    map.append(',');
+                first = false;
+                map.append(String.format("'%s': '%s'", entry.getKey(), entry.getValue()));
+            }
+        }
+        return map.toString();
+    }
+
     public static void failParentRepair(UUID parent_id, Throwable t)
     {
         String query = "UPDATE %s.%s SET finished_at = toTimestamp(now()), exception_message=?, exception_stacktrace=? WHERE parent_id=%s";
@@ -133,14 +181,14 @@
         StringWriter sw = new StringWriter();
         PrintWriter pw = new PrintWriter(sw);
         t.printStackTrace(pw);
-        String fmtQuery = String.format(query, NAME, PARENT_REPAIR_HISTORY, parent_id.toString());
+        String fmtQuery = String.format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, PARENT_REPAIR_HISTORY, parent_id.toString());
         processSilent(fmtQuery, t.getMessage(), sw.toString());
     }
 
     public static void successfulParentRepair(UUID parent_id, Collection<Range<Token>> successfulRanges)
     {
         String query = "UPDATE %s.%s SET finished_at = toTimestamp(now()), successful_ranges = {'%s'} WHERE parent_id=%s";
-        String fmtQuery = String.format(query, NAME, PARENT_REPAIR_HISTORY, Joiner.on("','").join(successfulRanges), parent_id.toString());
+        String fmtQuery = String.format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, PARENT_REPAIR_HISTORY, Joiner.on("','").join(successfulRanges), parent_id.toString());
         processSilent(fmtQuery);
     }
 
@@ -160,7 +208,7 @@
         {
             for (Range<Token> range : ranges)
             {
-                String fmtQry = String.format(query, NAME, REPAIR_HISTORY,
+                String fmtQry = String.format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, REPAIR_HISTORY,
                                               keyspaceName,
                                               cfname,
                                               id.toString(),
@@ -184,7 +232,7 @@
     public static void successfulRepairJob(UUID id, String keyspaceName, String cfname)
     {
         String query = "UPDATE %s.%s SET status = '%s', finished_at = toTimestamp(now()) WHERE keyspace_name = '%s' AND columnfamily_name = '%s' AND id = %s";
-        String fmtQuery = String.format(query, NAME, REPAIR_HISTORY,
+        String fmtQuery = String.format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, REPAIR_HISTORY,
                                         RepairState.SUCCESS.toString(),
                                         keyspaceName,
                                         cfname,
@@ -198,14 +246,68 @@
         StringWriter sw = new StringWriter();
         PrintWriter pw = new PrintWriter(sw);
         t.printStackTrace(pw);
-        String fmtQry = String.format(query, NAME, REPAIR_HISTORY,
-                RepairState.FAILED.toString(),
-                keyspaceName,
-                cfname,
-                id.toString());
+        String fmtQry = String.format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, REPAIR_HISTORY,
+                                      RepairState.FAILED.toString(),
+                                      keyspaceName,
+                                      cfname,
+                                      id.toString());
         processSilent(fmtQry, t.getMessage(), sw.toString());
     }
 
+    public static void startViewBuild(String keyspace, String view, UUID hostId)
+    {
+        String query = "INSERT INTO %s.%s (keyspace_name, view_name, host_id, status) VALUES (?, ?, ?, ?)";
+        QueryProcessor.process(String.format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, VIEW_BUILD_STATUS),
+                               ConsistencyLevel.ONE,
+                               Lists.newArrayList(bytes(keyspace),
+                                                  bytes(view),
+                                                  bytes(hostId),
+                                                  bytes(BuildStatus.STARTED.toString())));
+    }
+
+    public static void successfulViewBuild(String keyspace, String view, UUID hostId)
+    {
+        String query = "UPDATE %s.%s SET status = ? WHERE keyspace_name = ? AND view_name = ? AND host_id = ?";
+        QueryProcessor.process(String.format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, VIEW_BUILD_STATUS),
+                               ConsistencyLevel.ONE,
+                               Lists.newArrayList(bytes(BuildStatus.SUCCESS.toString()),
+                                                  bytes(keyspace),
+                                                  bytes(view),
+                                                  bytes(hostId)));
+    }
+
+    public static Map<UUID, String> viewStatus(String keyspace, String view)
+    {
+        String query = "SELECT host_id, status FROM %s.%s WHERE keyspace_name = ? AND view_name = ?";
+        UntypedResultSet results;
+        try
+        {
+            results = QueryProcessor.execute(String.format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, VIEW_BUILD_STATUS),
+                                             ConsistencyLevel.ONE,
+                                             keyspace,
+                                             view);
+        }
+        catch (Exception e)
+        {
+            return Collections.emptyMap();
+        }
+
+
+        Map<UUID, String> status = new HashMap<>();
+        for (UntypedResultSet.Row row : results)
+        {
+            status.put(row.getUUID("host_id"), row.getString("status"));
+        }
+        return status;
+    }
+
+    public static void setViewRemoved(String keyspaceName, String viewName)
+    {
+        String buildReq = "DELETE FROM %s.%s WHERE keyspace_name = ? AND view_name = ?";
+        QueryProcessor.executeInternal(String.format(buildReq, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, VIEW_BUILD_STATUS), keyspaceName, viewName);
+        forceBlockingFlush(VIEW_BUILD_STATUS);
+    }
+
     private static void processSilent(String fmtQry, String... values)
     {
         try
@@ -213,7 +315,7 @@
             List<ByteBuffer> valueList = new ArrayList<>();
             for (String v : values)
             {
-                valueList.add(ByteBufferUtil.bytes(v));
+                valueList.add(bytes(v));
             }
             QueryProcessor.process(fmtQry, ConsistencyLevel.ONE, valueList);
         }
@@ -223,9 +325,19 @@
         }
     }
 
+    public static void forceBlockingFlush(String table)
+    {
+        if (!DatabaseDescriptor.isUnsafeSystem())
+            FBUtilities.waitOnFuture(Keyspace.open(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME).getColumnFamilyStore(table).forceFlush());
+    }
 
     private enum RepairState
     {
         STARTED, SUCCESS, FAILED
     }
+
+    private enum BuildStatus
+    {
+        UNKNOWN, STARTED, SUCCESS
+    }
 }
diff --git a/src/java/org/apache/cassandra/repair/ValidationTask.java b/src/java/org/apache/cassandra/repair/ValidationTask.java
index bd866d2..177ad3e 100644
--- a/src/java/org/apache/cassandra/repair/ValidationTask.java
+++ b/src/java/org/apache/cassandra/repair/ValidationTask.java
@@ -18,16 +18,12 @@
 package org.apache.cassandra.repair;
 
 import java.net.InetAddress;
-import java.util.Map;
 
 import com.google.common.util.concurrent.AbstractFuture;
 
-import org.apache.cassandra.dht.Range;
-import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.exceptions.RepairException;
 import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.repair.messages.ValidationRequest;
-import org.apache.cassandra.utils.MerkleTree;
 import org.apache.cassandra.utils.MerkleTrees;
 
 /**
diff --git a/src/java/org/apache/cassandra/repair/Validator.java b/src/java/org/apache/cassandra/repair/Validator.java
index 9baa358..a2a2512 100644
--- a/src/java/org/apache/cassandra/repair/Validator.java
+++ b/src/java/org/apache/cassandra/repair/Validator.java
@@ -99,7 +99,7 @@
         {
             List<DecoratedKey> keys = new ArrayList<>();
             Random random = new Random();
-            
+
             for (Range<Token> range : tree.ranges())
             {
                 for (DecoratedKey sample : cfs.keySamples(range))
@@ -135,7 +135,7 @@
      * Called (in order) for every row present in the CF.
      * Hashes the row, and adds it to the tree being built.
      *
-     * @param row Row to add hash
+     * @param partition Partition to add hash
      */
     public void add(UnfilteredRowIterator partition)
     {
@@ -278,7 +278,7 @@
         // respond to the request that triggered this validation
         if (!initiator.equals(FBUtilities.getBroadcastAddress()))
         {
-            logger.info(String.format("[repair #%s] Sending completed merkle tree to %s for %s.%s", desc.sessionId, initiator, desc.keyspace, desc.columnFamily));
+            logger.info("[repair #{}] Sending completed merkle tree to {} for {}.{}", desc.sessionId, initiator, desc.keyspace, desc.columnFamily);
             Tracing.traceRepair("Sending completed merkle tree to {} for {}.{}", initiator, desc.keyspace, desc.columnFamily);
         }
         MessagingService.instance().sendOneWay(new ValidationComplete(desc, trees).createMessage(), initiator);
diff --git a/src/java/org/apache/cassandra/repair/messages/RepairOption.java b/src/java/org/apache/cassandra/repair/messages/RepairOption.java
index 5d56d3a..14fff97 100644
--- a/src/java/org/apache/cassandra/repair/messages/RepairOption.java
+++ b/src/java/org/apache/cassandra/repair/messages/RepairOption.java
@@ -19,6 +19,7 @@
 
 import java.util.*;
 
+import com.google.common.base.Joiner;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -28,7 +29,6 @@
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.repair.RepairParallelism;
-import org.apache.cassandra.tools.nodetool.Repair;
 import org.apache.cassandra.utils.FBUtilities;
 
 /**
@@ -45,6 +45,8 @@
     public static final String DATACENTERS_KEY = "dataCenters";
     public static final String HOSTS_KEY = "hosts";
     public static final String TRACE_KEY = "trace";
+    public static final String SUB_RANGE_REPAIR_KEY = "sub_range_repair";
+    public static final String PULL_REPAIR_KEY = "pullRepair";
     public static final String IGNORE_UNREPLICATED_KS = "ignoreUnreplicatedKeyspaces";
 
     // we don't want to push nodes too much for repair
@@ -116,6 +118,12 @@
      *             Multiple hosts can be given as comma separated values(e.g. cass1,cass2).</td>
      *             <td></td>
      *         </tr>
+     *         <tr>
+     *             <td>pullRepair</td>
+     *             <td>"true" if the repair should only stream data one way from a remote host to this host.
+     *             This is only allowed if exactly 2 hosts are specified along with a token range that they share.</td>
+     *             <td>false</td>
+     *         </tr>
      *     </tbody>
      * </table>
      *
@@ -130,6 +138,7 @@
         boolean primaryRange = Boolean.parseBoolean(options.get(PRIMARY_RANGE_KEY));
         boolean incremental = Boolean.parseBoolean(options.get(INCREMENTAL_KEY));
         boolean trace = Boolean.parseBoolean(options.get(TRACE_KEY));
+        boolean pullRepair = Boolean.parseBoolean(options.get(PULL_REPAIR_KEY));
         boolean ignoreUnreplicatedKeyspaces = Boolean.parseBoolean(options.get(IGNORE_UNREPLICATED_KS));
 
         int jobThreads = 1;
@@ -168,7 +177,7 @@
             }
         }
 
-        RepairOption option = new RepairOption(parallelism, primaryRange, incremental, trace, jobThreads, ranges, !ranges.isEmpty(), ignoreUnreplicatedKeyspaces);
+        RepairOption option = new RepairOption(parallelism, primaryRange, incremental, trace, jobThreads, ranges, !ranges.isEmpty(), pullRepair, ignoreUnreplicatedKeyspaces);
 
         // data centers
         String dataCentersStr = options.get(DATACENTERS_KEY);
@@ -214,10 +223,25 @@
         {
             throw new IllegalArgumentException("Too many job threads. Max is " + MAX_JOB_THREADS);
         }
+        if (!dataCenters.isEmpty() && !hosts.isEmpty())
+        {
+            throw new IllegalArgumentException("Cannot combine -dc and -hosts options.");
+        }
         if (primaryRange && ((!dataCenters.isEmpty() && !option.isInLocalDCOnly()) || !hosts.isEmpty()))
         {
             throw new IllegalArgumentException("You need to run primary range repair on all nodes in the cluster.");
         }
+        if (pullRepair)
+        {
+            if (hosts.size() != 2)
+            {
+                throw new IllegalArgumentException("Pull repair can only be performed between two hosts. Please specify two hosts, one of which must be this host.");
+            }
+            else if (ranges.isEmpty())
+            {
+                throw new IllegalArgumentException("Token ranges must be specified when performing pull repair. Please specify at least one token range which both hosts have in common.");
+            }
+        }
 
         return option;
     }
@@ -228,6 +252,7 @@
     private final boolean trace;
     private final int jobThreads;
     private final boolean isSubrangeRepair;
+    private final boolean pullRepair;
     private final boolean ignoreUnreplicatedKeyspaces;
 
     private final Collection<String> columnFamilies = new HashSet<>();
@@ -235,9 +260,9 @@
     private final Collection<String> hosts = new HashSet<>();
     private final Collection<Range<Token>> ranges = new HashSet<>();
 
-    public RepairOption(RepairParallelism parallelism, boolean primaryRange, boolean incremental, boolean trace, int jobThreads, Collection<Range<Token>> ranges, boolean isSubrangeRepair, boolean ignoreUnreplicatedKeyspaces)
+    public RepairOption(RepairParallelism parallelism, boolean primaryRange, boolean incremental, boolean trace, int jobThreads, Collection<Range<Token>> ranges, boolean isSubrangeRepair, boolean pullRepair, boolean ignoreUnreplicatedKeyspaces)
     {
-        if (FBUtilities.isWindows() &&
+        if (FBUtilities.isWindows &&
             (DatabaseDescriptor.getDiskAccessMode() != Config.DiskAccessMode.standard || DatabaseDescriptor.getIndexAccessMode() != Config.DiskAccessMode.standard) &&
             parallelism == RepairParallelism.SEQUENTIAL)
         {
@@ -253,6 +278,7 @@
         this.jobThreads = jobThreads;
         this.ranges.addAll(ranges);
         this.isSubrangeRepair = isSubrangeRepair;
+        this.pullRepair = pullRepair;
         this.ignoreUnreplicatedKeyspaces = ignoreUnreplicatedKeyspaces;
     }
 
@@ -276,6 +302,11 @@
         return trace;
     }
 
+    public boolean isPullRepair()
+    {
+        return pullRepair;
+    }
+
     public int getJobThreads()
     {
         return jobThreads;
@@ -331,7 +362,25 @@
                        ", dataCenters: " + dataCenters +
                        ", hosts: " + hosts +
                        ", # of ranges: " + ranges.size() +
+                       ", pull repair: " + pullRepair +
                        ", ignore unreplicated keyspaces: "+ ignoreUnreplicatedKeyspaces +
                        ')';
     }
+
+    public Map<String, String> asMap()
+    {
+        Map<String, String> options = new HashMap<>();
+        options.put(PARALLELISM_KEY, parallelism.toString());
+        options.put(PRIMARY_RANGE_KEY, Boolean.toString(primaryRange));
+        options.put(INCREMENTAL_KEY, Boolean.toString(incremental));
+        options.put(JOB_THREADS_KEY, Integer.toString(jobThreads));
+        options.put(COLUMNFAMILIES_KEY, Joiner.on(",").join(columnFamilies));
+        options.put(DATACENTERS_KEY, Joiner.on(",").join(dataCenters));
+        options.put(HOSTS_KEY, Joiner.on(",").join(hosts));
+        options.put(SUB_RANGE_REPAIR_KEY, Boolean.toString(isSubrangeRepair));
+        options.put(TRACE_KEY, Boolean.toString(trace));
+        options.put(RANGES_KEY, Joiner.on(",").join(ranges));
+        options.put(PULL_REPAIR_KEY, Boolean.toString(pullRepair));
+        return options;
+    }
 }
diff --git a/src/java/org/apache/cassandra/scheduler/RoundRobinScheduler.java b/src/java/org/apache/cassandra/scheduler/RoundRobinScheduler.java
index 61dfa50..fd967f3 100644
--- a/src/java/org/apache/cassandra/scheduler/RoundRobinScheduler.java
+++ b/src/java/org/apache/cassandra/scheduler/RoundRobinScheduler.java
@@ -60,17 +60,14 @@
         taskCount = new Semaphore(options.throttle_limit - 1);
 
         queues = new NonBlockingHashMap<String, WeightedQueue>();
-        Runnable runnable = new Runnable()
+        Runnable runnable = () ->
         {
-            public void run()
+            while (true)
             {
-                while (true)
-                {
-                    schedule();
-                }
+                schedule();
             }
         };
-        Thread scheduler = new Thread(NamedThreadFactory.threadLocalDeallocator(runnable), "REQUEST-SCHEDULER");
+        Thread scheduler = NamedThreadFactory.createThread(runnable, "REQUEST-SCHEDULER");
         scheduler.start();
         logger.info("Started the RoundRobin Request Scheduler");
     }
diff --git a/src/java/org/apache/cassandra/scheduler/WeightedQueue.java b/src/java/org/apache/cassandra/scheduler/WeightedQueue.java
index 298938d..76c7e9d 100644
--- a/src/java/org/apache/cassandra/scheduler/WeightedQueue.java
+++ b/src/java/org/apache/cassandra/scheduler/WeightedQueue.java
@@ -20,9 +20,6 @@
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.TimeUnit;
-import java.lang.management.ManagementFactory;
-import javax.management.MBeanServer;
-import javax.management.ObjectName;
 
 import org.apache.cassandra.metrics.LatencyMetrics;
 
diff --git a/src/java/org/apache/cassandra/schema/CompactionParams.java b/src/java/org/apache/cassandra/schema/CompactionParams.java
index 720efa3..c096b7c 100644
--- a/src/java/org/apache/cassandra/schema/CompactionParams.java
+++ b/src/java/org/apache/cassandra/schema/CompactionParams.java
@@ -18,9 +18,11 @@
 package org.apache.cassandra.schema;
 
 import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableMap;
@@ -45,7 +47,8 @@
         CLASS,
         ENABLED,
         MIN_THRESHOLD,
-        MAX_THRESHOLD;
+        MAX_THRESHOLD,
+        PROVIDE_OVERLAPPING_TOMBSTONES;
 
         @Override
         public String toString()
@@ -54,27 +57,45 @@
         }
     }
 
+    public enum TombstoneOption
+    {
+        NONE,
+        ROW,
+        CELL;
+
+        private static final TombstoneOption[] copyOfValues = values();
+
+        public static Optional<TombstoneOption> forName(String name)
+        {
+            return Arrays.stream(copyOfValues).filter(x -> x.name().equals(name)).findFirst();
+        }
+    }
+
     public static final int DEFAULT_MIN_THRESHOLD = 4;
     public static final int DEFAULT_MAX_THRESHOLD = 32;
 
     public static final boolean DEFAULT_ENABLED = true;
+    public static final TombstoneOption DEFAULT_PROVIDE_OVERLAPPING_TOMBSTONES =
+            TombstoneOption.valueOf(System.getProperty("default.provide.overlapping.tombstones", TombstoneOption.NONE.toString()).toUpperCase());
 
     public static final Map<String, String> DEFAULT_THRESHOLDS =
         ImmutableMap.of(Option.MIN_THRESHOLD.toString(), Integer.toString(DEFAULT_MIN_THRESHOLD),
                         Option.MAX_THRESHOLD.toString(), Integer.toString(DEFAULT_MAX_THRESHOLD));
 
     public static final CompactionParams DEFAULT =
-        new CompactionParams(SizeTieredCompactionStrategy.class, DEFAULT_THRESHOLDS, DEFAULT_ENABLED);
+        new CompactionParams(SizeTieredCompactionStrategy.class, DEFAULT_THRESHOLDS, DEFAULT_ENABLED, DEFAULT_PROVIDE_OVERLAPPING_TOMBSTONES);
 
     private final Class<? extends AbstractCompactionStrategy> klass;
     private final ImmutableMap<String, String> options;
     private final boolean isEnabled;
+    private final TombstoneOption tombstoneOption;
 
-    private CompactionParams(Class<? extends AbstractCompactionStrategy> klass, Map<String, String> options, boolean isEnabled)
+    private CompactionParams(Class<? extends AbstractCompactionStrategy> klass, Map<String, String> options, boolean isEnabled, TombstoneOption tombstoneOption)
     {
         this.klass = klass;
         this.options = ImmutableMap.copyOf(options);
         this.isEnabled = isEnabled;
+        this.tombstoneOption = tombstoneOption;
     }
 
     public static CompactionParams create(Class<? extends AbstractCompactionStrategy> klass, Map<String, String> options)
@@ -82,6 +103,16 @@
         boolean isEnabled = options.containsKey(Option.ENABLED.toString())
                           ? Boolean.parseBoolean(options.get(Option.ENABLED.toString()))
                           : DEFAULT_ENABLED;
+        String overlappingTombstoneParm = options.getOrDefault(Option.PROVIDE_OVERLAPPING_TOMBSTONES.toString(),
+                                                               DEFAULT_PROVIDE_OVERLAPPING_TOMBSTONES.toString()).toUpperCase();
+        Optional<TombstoneOption> tombstoneOptional = TombstoneOption.forName(overlappingTombstoneParm);
+        if (!tombstoneOptional.isPresent())
+        {
+            throw new ConfigurationException(format("Invalid value %s for 'provide_overlapping_tombstones' compaction sub-option - must be one of the following [%s].",
+                                                    overlappingTombstoneParm,
+                                                    StringUtils.join(TombstoneOption.values(), ", ")));
+        }
+        TombstoneOption tombstoneOption = tombstoneOptional.get();
 
         Map<String, String> allOptions = new HashMap<>(options);
         if (supportsThresholdParams(klass))
@@ -90,7 +121,7 @@
             allOptions.putIfAbsent(Option.MAX_THRESHOLD.toString(), Integer.toString(DEFAULT_MAX_THRESHOLD));
         }
 
-        return new CompactionParams(klass, allOptions, isEnabled);
+        return new CompactionParams(klass, allOptions, isEnabled, tombstoneOption);
     }
 
     public static CompactionParams scts(Map<String, String> options)
@@ -119,6 +150,11 @@
              : Integer.parseInt(threshold);
     }
 
+    public TombstoneOption tombstoneOption()
+    {
+        return tombstoneOption;
+    }
+
     public void validate()
     {
         try
diff --git a/src/java/org/apache/cassandra/schema/CompressionParams.java b/src/java/org/apache/cassandra/schema/CompressionParams.java
index cd1686f..f48a688 100644
--- a/src/java/org/apache/cassandra/schema/CompressionParams.java
+++ b/src/java/org/apache/cassandra/schema/CompressionParams.java
@@ -56,7 +56,7 @@
     public static final String CHUNK_LENGTH_IN_KB = "chunk_length_in_kb";
     public static final String ENABLED = "enabled";
 
-    public static final CompressionParams DEFAULT = new CompressionParams(LZ4Compressor.instance,
+    public static final CompressionParams DEFAULT = new CompressionParams(LZ4Compressor.create(Collections.<String, String>emptyMap()),
                                                                           DEFAULT_CHUNK_LENGTH,
                                                                           Collections.emptyMap());
 
@@ -139,7 +139,7 @@
 
     public static CompressionParams lz4(Integer chunkLength)
     {
-        return new CompressionParams(LZ4Compressor.instance, chunkLength, Collections.emptyMap());
+        return new CompressionParams(LZ4Compressor.create(Collections.emptyMap()), chunkLength, Collections.emptyMap());
     }
 
     public CompressionParams(String sstableCompressorClass, Integer chunkLength, Map<String, String> otherOptions) throws ConfigurationException
@@ -265,14 +265,15 @@
         }
     }
 
-    public static ICompressor createCompressor(ParameterizedClass compression) throws ConfigurationException {
+    public static ICompressor createCompressor(ParameterizedClass compression) throws ConfigurationException
+    {
         return createCompressor(parseCompressorClass(compression.class_name), copyOptions(compression.parameters));
     }
 
     private static Map<String, String> copyOptions(Map<? extends CharSequence, ? extends CharSequence> co)
     {
         if (co == null || co.isEmpty())
-            return Collections.<String, String>emptyMap();
+            return Collections.emptyMap();
 
         Map<String, String> compressionOptions = new HashMap<>();
         for (Map.Entry<? extends CharSequence, ? extends CharSequence> entry : co.entrySet())
@@ -282,7 +283,7 @@
 
     /**
      * Parse the chunk length (in KB) and returns it as bytes.
-     * 
+     *
      * @param chLengthKB the length of the chunk to parse
      * @return the chunk length in bytes
      * @throws ConfigurationException if the chunk size is too large
@@ -330,9 +331,9 @@
             if (!hasLoggedChunkLengthWarning)
             {
                 hasLoggedChunkLengthWarning = true;
-                logger.warn(format("The %s option has been deprecated. You should use %s instead",
+                logger.warn("The {} option has been deprecated. You should use {} instead",
                                    CHUNK_LENGTH_KB,
-                                   CHUNK_LENGTH_IN_KB));
+                                   CHUNK_LENGTH_IN_KB);
             }
 
             return parseChunkLength(options.remove(CHUNK_LENGTH_KB));
@@ -379,9 +380,9 @@
         if (options.containsKey(SSTABLE_COMPRESSION) && !hasLoggedSsTableCompressionWarning)
         {
             hasLoggedSsTableCompressionWarning = true;
-            logger.warn(format("The %s option has been deprecated. You should use %s instead",
+            logger.warn("The {} option has been deprecated. You should use {} instead",
                                SSTABLE_COMPRESSION,
-                               CLASS));
+                               CLASS);
         }
 
         return options.remove(SSTABLE_COMPRESSION);
diff --git a/src/java/org/apache/cassandra/schema/Functions.java b/src/java/org/apache/cassandra/schema/Functions.java
index c65f58d..a936d81 100644
--- a/src/java/org/apache/cassandra/schema/Functions.java
+++ b/src/java/org/apache/cassandra/schema/Functions.java
@@ -131,7 +131,7 @@
      */
     public static boolean typesMatch(AbstractType<?> t1, AbstractType<?> t2)
     {
-        return t1.asCQL3Type().toString().equals(t2.asCQL3Type().toString());
+        return t1.freeze().asCQL3Type().toString().equals(t2.freeze().asCQL3Type().toString());
     }
 
     public static boolean typesMatch(List<AbstractType<?>> t1, List<AbstractType<?>> t2)
diff --git a/src/java/org/apache/cassandra/schema/IndexMetadata.java b/src/java/org/apache/cassandra/schema/IndexMetadata.java
index 7c60a64..04e06ab 100644
--- a/src/java/org/apache/cassandra/schema/IndexMetadata.java
+++ b/src/java/org/apache/cassandra/schema/IndexMetadata.java
@@ -21,6 +21,7 @@
 import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.util.*;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import com.google.common.base.Objects;
@@ -46,6 +47,10 @@
 public final class IndexMetadata
 {
     private static final Logger logger = LoggerFactory.getLogger(IndexMetadata.class);
+    
+    private static final Pattern PATTERN_NON_WORD_CHAR = Pattern.compile("\\W");
+    private static final Pattern PATTERN_WORD_CHARS = Pattern.compile("\\w+");
+
 
     public static final Serializer serializer = new Serializer();
 
@@ -127,15 +132,15 @@
 
     public static boolean isNameValid(String name)
     {
-        return name != null && !name.isEmpty() && name.matches("\\w+");
+        return name != null && !name.isEmpty() && PATTERN_WORD_CHARS.matcher(name).matches();
     }
 
     public static String getDefaultIndexName(String cfName, String root)
     {
         if (root == null)
-            return (cfName + "_" + "idx").replaceAll("\\W", "");
+            return PATTERN_NON_WORD_CHAR.matcher(cfName + "_" + "idx").replaceAll("");
         else
-            return (cfName + "_" + root + "_idx").replaceAll("\\W", "");
+            return PATTERN_NON_WORD_CHAR.matcher(cfName + "_" + root + "_idx").replaceAll("");
     }
 
     public void validate(CFMetaData cfm)
diff --git a/src/java/org/apache/cassandra/schema/Indexes.java b/src/java/org/apache/cassandra/schema/Indexes.java
index 49a1d3b..eb49d39 100644
--- a/src/java/org/apache/cassandra/schema/Indexes.java
+++ b/src/java/org/apache/cassandra/schema/Indexes.java
@@ -95,7 +95,7 @@
     /**
      * Get the index with the specified id
      *
-     * @param name a UUID which identifies an index
+     * @param id a UUID which identifies an index
      * @return an empty {@link Optional} if no index with the specified id is found; a non-empty optional of
      *         {@link IndexMetadata} otherwise
      */
@@ -107,7 +107,7 @@
 
     /**
      * Answer true if contains an index with the specified id.
-     * @param name a UUID which identifies an index.
+     * @param id a UUID which identifies an index.
      * @return true if an index with the specified id is found; false otherwise
      */
     public boolean has(UUID id)
diff --git a/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java b/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java
index 76ba27d..4fefd44 100644
--- a/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java
+++ b/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java
@@ -23,11 +23,12 @@
 
 import javax.annotation.Nullable;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 import com.google.common.collect.Iterables;
 
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.config.ViewDefinition;
 import org.apache.cassandra.exceptions.ConfigurationException;
 
@@ -154,14 +155,14 @@
     @Override
     public String toString()
     {
-        return Objects.toStringHelper(this)
-                      .add("name", name)
-                      .add("params", params)
-                      .add("tables", tables)
-                      .add("views", views)
-                      .add("functions", functions)
-                      .add("types", types)
-                      .toString();
+        return MoreObjects.toStringHelper(this)
+                          .add("name", name)
+                          .add("params", params)
+                          .add("tables", tables)
+                          .add("views", views)
+                          .add("functions", functions)
+                          .add("types", types)
+                          .toString();
     }
 
     public void validate()
@@ -169,7 +170,7 @@
         if (!CFMetaData.isNameValid(name))
             throw new ConfigurationException(String.format("Keyspace name must not be empty, more than %s characters long, "
                                                            + "or contain non-alphanumeric-underscore characters (got \"%s\")",
-                                                           Schema.NAME_LENGTH,
+                                                           SchemaConstants.NAME_LENGTH,
                                                            name));
         params.validate(name);
         tablesAndViews().forEach(CFMetaData::validate);
diff --git a/src/java/org/apache/cassandra/schema/KeyspaceParams.java b/src/java/org/apache/cassandra/schema/KeyspaceParams.java
index 2ea18ca..1deaa29 100644
--- a/src/java/org/apache/cassandra/schema/KeyspaceParams.java
+++ b/src/java/org/apache/cassandra/schema/KeyspaceParams.java
@@ -20,6 +20,7 @@
 import java.util.Map;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 
 /**
@@ -30,9 +31,9 @@
     public static final boolean DEFAULT_DURABLE_WRITES = true;
 
     /**
-     * This determines durable writes for the {@link org.apache.cassandra.db.SystemKeyspace#NAME}
-     * and {@link SchemaKeyspace#NAME} keyspaces, the only reason it is not final is for commitlog
-     * unit tests. It should only be changed for testing purposes.
+     * This determines durable writes for the {@link org.apache.cassandra.config.SchemaConstants#SCHEMA_KEYSPACE_NAME}
+     * and {@link org.apache.cassandra.config.SchemaConstants#SYSTEM_KEYSPACE_NAME} keyspaces,
+     * the only reason it is not final is for commitlog unit tests. It should only be changed for testing purposes.
      */
     @VisibleForTesting
     public static boolean DEFAULT_LOCAL_DURABLE_WRITES = true;
@@ -111,9 +112,9 @@
     @Override
     public String toString()
     {
-        return Objects.toStringHelper(this)
-                      .add(Option.DURABLE_WRITES.toString(), durableWrites)
-                      .add(Option.REPLICATION.toString(), replication)
-                      .toString();
+        return MoreObjects.toStringHelper(this)
+                          .add(Option.DURABLE_WRITES.toString(), durableWrites)
+                          .add(Option.REPLICATION.toString(), replication)
+                          .toString();
     }
 }
diff --git a/src/java/org/apache/cassandra/schema/LegacySchemaMigrator.java b/src/java/org/apache/cassandra/schema/LegacySchemaMigrator.java
index b7f7e73..c320672 100644
--- a/src/java/org/apache/cassandra/schema/LegacySchemaMigrator.java
+++ b/src/java/org/apache/cassandra/schema/LegacySchemaMigrator.java
@@ -29,6 +29,7 @@
 
 import org.apache.cassandra.config.*;
 import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.FieldIdentifier;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.SuperColumnCompatibility;
 import org.apache.cassandra.cql3.UntypedResultSet;
@@ -41,9 +42,7 @@
 import org.apache.cassandra.db.rows.RowIterator;
 import org.apache.cassandra.db.rows.UnfilteredRowIterators;
 import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
-import org.apache.cassandra.utils.concurrent.OpOrder;
 
 import static java.lang.String.format;
 import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
@@ -88,7 +87,7 @@
         // write metadata to the new schema tables
         logger.info("Moving {} keyspaces from legacy schema tables to the new schema keyspace ({})",
                     keyspaces.size(),
-                    SchemaKeyspace.NAME);
+                    SchemaConstants.SCHEMA_KEYSPACE_NAME);
         keyspaces.forEach(LegacySchemaMigrator::storeKeyspaceInNewSchemaTables);
         keyspaces.forEach(LegacySchemaMigrator::migrateBuiltIndexesForKeyspace);
 
@@ -128,7 +127,7 @@
 
     static void unloadLegacySchemaTables()
     {
-        KeyspaceMetadata systemKeyspace = Schema.instance.getKSMetaData(SystemKeyspace.NAME);
+        KeyspaceMetadata systemKeyspace = Schema.instance.getKSMetaData(SchemaConstants.SYSTEM_KEYSPACE_NAME);
 
         Tables systemTables = systemKeyspace.tables;
         for (CFMetaData table : LegacySchemaTables)
@@ -149,20 +148,20 @@
     {
         logger.info("Migrating keyspace {}", keyspace);
 
-        Mutation mutation = SchemaKeyspace.makeCreateKeyspaceMutation(keyspace.name, keyspace.params, keyspace.timestamp);
+        Mutation.SimpleBuilder builder = SchemaKeyspace.makeCreateKeyspaceMutation(keyspace.name, keyspace.params, keyspace.timestamp);
         for (Table table : keyspace.tables)
-            SchemaKeyspace.addTableToSchemaMutation(table.metadata, table.timestamp, true, mutation);
+            SchemaKeyspace.addTableToSchemaMutation(table.metadata, true, builder.timestamp(table.timestamp));
 
         for (Type type : keyspace.types)
-            SchemaKeyspace.addTypeToSchemaMutation(type.metadata, type.timestamp, mutation);
+            SchemaKeyspace.addTypeToSchemaMutation(type.metadata, builder.timestamp(type.timestamp));
 
         for (Function function : keyspace.functions)
-            SchemaKeyspace.addFunctionToSchemaMutation(function.metadata, function.timestamp, mutation);
+            SchemaKeyspace.addFunctionToSchemaMutation(function.metadata, builder.timestamp(function.timestamp));
 
         for (Aggregate aggregate : keyspace.aggregates)
-            SchemaKeyspace.addAggregateToSchemaMutation(aggregate.metadata, aggregate.timestamp, mutation);
+            SchemaKeyspace.addAggregateToSchemaMutation(aggregate.metadata, builder.timestamp(aggregate.timestamp));
 
-        mutation.apply();
+        builder.build().apply();
     }
 
     /*
@@ -170,10 +169,10 @@
      */
     private static Collection<Keyspace> readSchema()
     {
-        String query = format("SELECT keyspace_name FROM %s.%s", SystemKeyspace.NAME, SystemKeyspace.LEGACY_KEYSPACES);
+        String query = format("SELECT keyspace_name FROM %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.LEGACY_KEYSPACES);
         Collection<String> keyspaceNames = new ArrayList<>();
         query(query).forEach(row -> keyspaceNames.add(row.getString("keyspace_name")));
-        keyspaceNames.removeAll(Schema.LOCAL_SYSTEM_KEYSPACE_NAMES);
+        keyspaceNames.removeAll(SchemaConstants.LOCAL_SYSTEM_KEYSPACE_NAMES);
 
         Collection<Keyspace> keyspaces = new ArrayList<>();
         keyspaceNames.forEach(name -> keyspaces.add(readKeyspace(name)));
@@ -202,7 +201,7 @@
     private static long readKeyspaceTimestamp(String keyspaceName)
     {
         String query = format("SELECT writeTime(durable_writes) AS timestamp FROM %s.%s WHERE keyspace_name = ?",
-                              SystemKeyspace.NAME,
+                              SchemaConstants.SYSTEM_KEYSPACE_NAME,
                               SystemKeyspace.LEGACY_KEYSPACES);
         return query(query, keyspaceName).one().getLong("timestamp");
     }
@@ -210,7 +209,7 @@
     private static KeyspaceParams readKeyspaceParams(String keyspaceName)
     {
         String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ?",
-                              SystemKeyspace.NAME,
+                              SchemaConstants.SYSTEM_KEYSPACE_NAME,
                               SystemKeyspace.LEGACY_KEYSPACES);
         UntypedResultSet.Row row = query(query, keyspaceName).one();
 
@@ -230,7 +229,7 @@
     private static Collection<Table> readTables(String keyspaceName)
     {
         String query = format("SELECT columnfamily_name FROM %s.%s WHERE keyspace_name = ?",
-                              SystemKeyspace.NAME,
+                              SchemaConstants.SYSTEM_KEYSPACE_NAME,
                               SystemKeyspace.LEGACY_COLUMNFAMILIES);
         Collection<String> tableNames = new ArrayList<>();
         query(query, keyspaceName).forEach(row -> tableNames.add(row.getString("columnfamily_name")));
@@ -250,7 +249,7 @@
     private static long readTableTimestamp(String keyspaceName, String tableName)
     {
         String query = format("SELECT writeTime(type) AS timestamp FROM %s.%s WHERE keyspace_name = ? AND columnfamily_name = ?",
-                              SystemKeyspace.NAME,
+                              SchemaConstants.SYSTEM_KEYSPACE_NAME,
                               SystemKeyspace.LEGACY_COLUMNFAMILIES);
         return query(query, keyspaceName, tableName).one().getLong("timestamp");
     }
@@ -258,17 +257,17 @@
     private static CFMetaData readTableMetadata(String keyspaceName, String tableName)
     {
         String tableQuery = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND columnfamily_name = ?",
-                                   SystemKeyspace.NAME,
+                                   SchemaConstants.SYSTEM_KEYSPACE_NAME,
                                    SystemKeyspace.LEGACY_COLUMNFAMILIES);
         UntypedResultSet.Row tableRow = query(tableQuery, keyspaceName, tableName).one();
 
         String columnsQuery = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND columnfamily_name = ?",
-                                     SystemKeyspace.NAME,
+                                     SchemaConstants.SYSTEM_KEYSPACE_NAME,
                                      SystemKeyspace.LEGACY_COLUMNS);
         UntypedResultSet columnRows = query(columnsQuery, keyspaceName, tableName);
 
         String triggersQuery = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND columnfamily_name = ?",
-                                      SystemKeyspace.NAME,
+                                      SchemaConstants.SYSTEM_KEYSPACE_NAME,
                                       SystemKeyspace.LEGACY_TRIGGERS);
         UntypedResultSet triggerRows = query(triggersQuery, keyspaceName, tableName);
 
@@ -286,7 +285,7 @@
         AbstractType<?> rawComparator = TypeParser.parse(tableRow.getString("comparator"));
         AbstractType<?> subComparator = tableRow.has("subcomparator") ? TypeParser.parse(tableRow.getString("subcomparator")) : null;
 
-        boolean isSuper = "super".equals(tableRow.getString("type").toLowerCase());
+        boolean isSuper = "super".equals(tableRow.getString("type").toLowerCase(Locale.ENGLISH));
         boolean isCompound = rawComparator instanceof CompositeType || isSuper;
 
         /*
@@ -505,14 +504,15 @@
         return params.build();
     }
 
+
     /**
      *
-     * 2.1 and newer use JSON'ified map of caching parameters, but older versions had valid Strings
-     * NONE, KEYS_ONLY, ROWS_ONLY, and ALL
-     *
-     * @param caching, the string representing the table's caching options
-     * @return CachingParams object corresponding to the input string
-     */
+      * 2.1 and newer use JSON'ified map of caching parameters, but older versions had valid Strings
+      * NONE, KEYS_ONLY, ROWS_ONLY, and ALL
+      *
+      * @param caching, the string representing the table's caching options
+      * @return CachingParams object corresponding to the input string
+      */
     @VisibleForTesting
     public static CachingParams cachingFromRow(String caching)
     {
@@ -745,6 +745,14 @@
 
         AbstractType<?> validator = parseType(row.getString("validator"));
 
+        // In the 2.x schema we didn't store UDT's with a FrozenType wrapper because they were implicitly frozen.  After
+        // CASSANDRA-7423 (non-frozen UDTs), this is no longer true, so we need to freeze UDTs and nested freezable
+        // types (UDTs and collections) to properly migrate the schema.  See CASSANDRA-11609 and CASSANDRA-11613.
+        if (validator.isUDT() && validator.isMultiCell())
+            validator = validator.freeze();
+        else
+            validator = validator.freezeNestedMulticellTypes();
+
         return new ColumnDefinition(keyspace, table, name, validator, componentIndex, kind);
     }
 
@@ -774,21 +782,26 @@
             if (row.has("index_options"))
                 indexOptions = fromJsonMap(row.getString("index_options"));
 
-            String indexName = null;
             if (row.has("index_name"))
-                indexName = row.getString("index_name");
+            {
+                String indexName = row.getString("index_name");
 
-            ColumnDefinition column = createColumnFromColumnRow(row,
-                                                                keyspace,
-                                                                table,
-                                                                rawComparator,
-                                                                rawSubComparator,
-                                                                isSuper,
-                                                                isCQLTable,
-                                                                isStaticCompactTable,
-                                                                needsUpgrade);
+                ColumnDefinition column = createColumnFromColumnRow(row,
+                                                                    keyspace,
+                                                                    table,
+                                                                    rawComparator,
+                                                                    rawSubComparator,
+                                                                    isSuper,
+                                                                    isCQLTable,
+                                                                    isStaticCompactTable,
+                                                                    needsUpgrade);
 
-            indexes.add(IndexMetadata.fromLegacyMetadata(cfm, column, indexName, kind, indexOptions));
+                indexes.add(IndexMetadata.fromLegacyMetadata(cfm, column, indexName, kind, indexOptions));
+            }
+            else
+            {
+                logger.error("Failed to find index name for legacy migration of index on {}.{}", keyspace, table);
+            }
         }
 
         return indexes.build();
@@ -826,7 +839,7 @@
     private static Collection<Type> readTypes(String keyspaceName)
     {
         String query = format("SELECT type_name FROM %s.%s WHERE keyspace_name = ?",
-                              SystemKeyspace.NAME,
+                              SchemaConstants.SYSTEM_KEYSPACE_NAME,
                               SystemKeyspace.LEGACY_USERTYPES);
         Collection<String> typeNames = new ArrayList<>();
         query(query, keyspaceName).forEach(row -> typeNames.add(row.getString("type_name")));
@@ -849,7 +862,7 @@
      */
     private static long readTypeTimestamp(String keyspaceName, String typeName)
     {
-        ColumnFamilyStore store = org.apache.cassandra.db.Keyspace.open(SystemKeyspace.NAME)
+        ColumnFamilyStore store = org.apache.cassandra.db.Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME)
                                                                   .getColumnFamilyStore(SystemKeyspace.LEGACY_USERTYPES);
 
         ClusteringComparator comparator = store.metadata.comparator;
@@ -858,8 +871,8 @@
         DecoratedKey key = store.metadata.decorateKey(AsciiType.instance.fromString(keyspaceName));
         SinglePartitionReadCommand command = SinglePartitionReadCommand.create(store.metadata, nowInSec, key, slices);
 
-        try (OpOrder.Group op = store.readOrdering.start();
-             RowIterator partition = UnfilteredRowIterators.filter(command.queryMemtableAndDisk(store, op), nowInSec))
+        try (ReadExecutionController controller = command.executionController();
+             RowIterator partition = UnfilteredRowIterators.filter(command.queryMemtableAndDisk(store, controller), nowInSec))
         {
             return partition.next().primaryKeyLivenessInfo().timestamp();
         }
@@ -868,14 +881,14 @@
     private static UserType readTypeMetadata(String keyspaceName, String typeName)
     {
         String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND type_name = ?",
-                              SystemKeyspace.NAME,
+                              SchemaConstants.SYSTEM_KEYSPACE_NAME,
                               SystemKeyspace.LEGACY_USERTYPES);
         UntypedResultSet.Row row = query(query, keyspaceName, typeName).one();
 
-        List<ByteBuffer> names =
+        List<FieldIdentifier> names =
             row.getList("field_names", UTF8Type.instance)
                .stream()
-               .map(ByteBufferUtil::bytes)
+               .map(t -> FieldIdentifier.forInternalString(t))
                .collect(Collectors.toList());
 
         List<AbstractType<?>> types =
@@ -884,7 +897,7 @@
                .map(LegacySchemaMigrator::parseType)
                .collect(Collectors.toList());
 
-        return new UserType(keyspaceName, bytes(typeName), names, types);
+        return new UserType(keyspaceName, bytes(typeName), names, types, true);
     }
 
     /*
@@ -894,7 +907,7 @@
     private static Collection<Function> readFunctions(String keyspaceName)
     {
         String query = format("SELECT function_name, signature FROM %s.%s WHERE keyspace_name = ?",
-                              SystemKeyspace.NAME,
+                              SchemaConstants.SYSTEM_KEYSPACE_NAME,
                               SystemKeyspace.LEGACY_FUNCTIONS);
         HashMultimap<String, List<String>> functionSignatures = HashMultimap.create();
         query(query, keyspaceName).forEach(row -> functionSignatures.put(row.getString("function_name"), row.getList("signature", UTF8Type.instance)));
@@ -916,7 +929,7 @@
         String query = format("SELECT writeTime(return_type) AS timestamp " +
                               "FROM %s.%s " +
                               "WHERE keyspace_name = ? AND function_name = ? AND signature = ?",
-                              SystemKeyspace.NAME,
+                              SchemaConstants.SYSTEM_KEYSPACE_NAME,
                               SystemKeyspace.LEGACY_FUNCTIONS);
         return query(query, keyspaceName, functionName, signature).one().getLong("timestamp");
     }
@@ -924,7 +937,7 @@
     private static UDFunction readFunctionMetadata(String keyspaceName, String functionName, List<String> signature)
     {
         String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND function_name = ? AND signature = ?",
-                              SystemKeyspace.NAME,
+                              SchemaConstants.SYSTEM_KEYSPACE_NAME,
                               SystemKeyspace.LEGACY_FUNCTIONS);
         UntypedResultSet.Row row = query(query, keyspaceName, functionName, signature).one();
 
@@ -963,7 +976,7 @@
     private static Collection<Aggregate> readAggregates(Functions functions, String keyspaceName)
     {
         String query = format("SELECT aggregate_name, signature FROM %s.%s WHERE keyspace_name = ?",
-                              SystemKeyspace.NAME,
+                              SchemaConstants.SYSTEM_KEYSPACE_NAME,
                               SystemKeyspace.LEGACY_AGGREGATES);
         HashMultimap<String, List<String>> aggregateSignatures = HashMultimap.create();
         query(query, keyspaceName).forEach(row -> aggregateSignatures.put(row.getString("aggregate_name"), row.getList("signature", UTF8Type.instance)));
@@ -985,7 +998,7 @@
         String query = format("SELECT writeTime(return_type) AS timestamp " +
                               "FROM %s.%s " +
                               "WHERE keyspace_name = ? AND aggregate_name = ? AND signature = ?",
-                              SystemKeyspace.NAME,
+                              SchemaConstants.SYSTEM_KEYSPACE_NAME,
                               SystemKeyspace.LEGACY_AGGREGATES);
         return query(query, keyspaceName, aggregateName, signature).one().getLong("timestamp");
     }
@@ -993,7 +1006,7 @@
     private static UDAggregate readAggregateMetadata(Functions functions, String keyspaceName, String functionName, List<String> signature)
     {
         String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND aggregate_name = ? AND signature = ?",
-                              SystemKeyspace.NAME,
+                              SchemaConstants.SYSTEM_KEYSPACE_NAME,
                               SystemKeyspace.LEGACY_AGGREGATES);
         UntypedResultSet.Row row = query(query, keyspaceName, functionName, signature).one();
 
diff --git a/src/java/org/apache/cassandra/schema/SchemaKeyspace.java b/src/java/org/apache/cassandra/schema/SchemaKeyspace.java
index 3c49327..6b0089f 100644
--- a/src/java/org/apache/cassandra/schema/SchemaKeyspace.java
+++ b/src/java/org/apache/cassandra/schema/SchemaKeyspace.java
@@ -21,38 +21,82 @@
 import java.nio.charset.CharacterCodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.*;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.MapDifference;
 import com.google.common.collect.Maps;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.config.*;
+import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.CFMetaData.DroppedColumn;
+import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.config.ColumnDefinition.ClusteringOrder;
-import org.apache.cassandra.cql3.*;
-import org.apache.cassandra.cql3.functions.*;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
+import org.apache.cassandra.config.ViewDefinition;
+import org.apache.cassandra.cql3.CQL3Type;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.FieldIdentifier;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.Terms;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.functions.AbstractFunction;
+import org.apache.cassandra.cql3.functions.FunctionName;
+import org.apache.cassandra.cql3.functions.UDAggregate;
+import org.apache.cassandra.cql3.functions.UDFunction;
 import org.apache.cassandra.cql3.statements.SelectStatement;
-import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.marshal.*;
-import org.apache.cassandra.db.partitions.*;
-import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.PartitionRangeReadCommand;
+import org.apache.cassandra.db.ReadCommand;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.CompositeType;
+import org.apache.cassandra.db.marshal.ListType;
+import org.apache.cassandra.db.marshal.MapType;
+import org.apache.cassandra.db.marshal.ReversedType;
+import org.apache.cassandra.db.marshal.SetType;
+import org.apache.cassandra.db.marshal.TupleType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.db.partitions.PartitionIterator;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.RowIterator;
+import org.apache.cassandra.db.rows.RowIterators;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
 import org.apache.cassandra.db.view.View;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.locator.LocalStrategy;
 import org.apache.cassandra.service.PendingRangeCalculatorService;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.Pair;
 
 import static java.lang.String.format;
-
 import static java.util.stream.Collectors.toList;
 import static org.apache.cassandra.cql3.QueryProcessor.executeInternal;
 import static org.apache.cassandra.cql3.QueryProcessor.executeOnceInternal;
@@ -60,7 +104,7 @@
 
 /**
  * system_schema.* tables and methods for manipulating them.
- */
+*/
 public final class SchemaKeyspace
 {
     private SchemaKeyspace()
@@ -69,47 +113,17 @@
 
     private static final Logger logger = LoggerFactory.getLogger(SchemaKeyspace.class);
 
-    private static final boolean FLUSH_SCHEMA_TABLES = Boolean.valueOf(System.getProperty("cassandra.test.flush_local_schema_changes", "true"));
-    private static final boolean IGNORE_CORRUPTED_SCHEMA_TABLES = Boolean.valueOf(System.getProperty("cassandra.ignore_corrupted_schema_tables", "false"));
-
-    public static final String NAME = "system_schema";
-
-    public static final String KEYSPACES = "keyspaces";
-    public static final String TABLES = "tables";
-    public static final String COLUMNS = "columns";
-    public static final String DROPPED_COLUMNS = "dropped_columns";
-    public static final String TRIGGERS = "triggers";
-    public static final String VIEWS = "views";
-    public static final String TYPES = "types";
-    public static final String FUNCTIONS = "functions";
-    public static final String AGGREGATES = "aggregates";
-    public static final String INDEXES = "indexes";
+    private static final boolean FLUSH_SCHEMA_TABLES = Boolean.parseBoolean(System.getProperty("cassandra.test.flush_local_schema_changes", "true"));
+    private static final boolean IGNORE_CORRUPTED_SCHEMA_TABLES = Boolean.parseBoolean(System.getProperty("cassandra.ignore_corrupted_schema_tables", "false"));
 
     /**
-     * The order in this list matters.
-     *
-     * When flushing schema tables, we want to flush them in a way that mitigates the effects of an abrupt shutdown whilst
-     * the tables are being flushed. On startup, we load the schema from disk before replaying the CL, so we need to
-     * try to avoid problems like reading a table without columns or types, for example. So columns and types should be
-     * flushed before tables, which should be flushed before keyspaces.
-     *
-     * When truncating, the order should be reversed. For immutable lists this is an efficient operation that simply
-     * iterates in reverse order.
-     *
-     * See CASSANDRA-12213 for more details.
+     * The tables to which we added the cdc column. This is used in {@link #makeUpdateForSchema} below to make sure we skip that
+     * column is cdc is disabled as the columns breaks pre-cdc to post-cdc upgrades (typically, 3.0 -> 3.X).
      */
-    public static final ImmutableList<String> ALL =
-        ImmutableList.of(COLUMNS, DROPPED_COLUMNS, TRIGGERS, TYPES, FUNCTIONS, AGGREGATES, INDEXES, TABLES, VIEWS, KEYSPACES);
-
-    /**
-     * Until we upgrade the messaging service version, that is version 4.0, we must preserve the old order (before CASSANDRA-12213)
-     * for digest calculations, otherwise the nodes will never agree on the schema during a rolling upgrade, see CASSANDRA-13559.
-     */
-    public static final ImmutableList<String> ALL_FOR_DIGEST =
-        ImmutableList.of(KEYSPACES, TABLES, COLUMNS, DROPPED_COLUMNS, TRIGGERS, VIEWS, TYPES, FUNCTIONS, AGGREGATES, INDEXES);
+    private static final Set<String> TABLES_WITH_CDC_ADDED = ImmutableSet.of(SchemaKeyspaceTables.TABLES, SchemaKeyspaceTables.VIEWS);
 
     private static final CFMetaData Keyspaces =
-        compile(KEYSPACES,
+        compile(SchemaKeyspaceTables.KEYSPACES,
                 "keyspace definitions",
                 "CREATE TABLE %s ("
                 + "keyspace_name text,"
@@ -118,7 +132,7 @@
                 + "PRIMARY KEY ((keyspace_name)))");
 
     private static final CFMetaData Tables =
-        compile(TABLES,
+        compile(SchemaKeyspaceTables.TABLES,
                 "table definitions",
                 "CREATE TABLE %s ("
                 + "keyspace_name text,"
@@ -140,10 +154,11 @@
                 + "min_index_interval int,"
                 + "read_repair_chance double,"
                 + "speculative_retry text,"
+                + "cdc boolean,"
                 + "PRIMARY KEY ((keyspace_name), table_name))");
 
     private static final CFMetaData Columns =
-        compile(COLUMNS,
+        compile(SchemaKeyspaceTables.COLUMNS,
                 "column definitions",
                 "CREATE TABLE %s ("
                 + "keyspace_name text,"
@@ -157,7 +172,7 @@
                 + "PRIMARY KEY ((keyspace_name), table_name, column_name))");
 
     private static final CFMetaData DroppedColumns =
-        compile(DROPPED_COLUMNS,
+        compile(SchemaKeyspaceTables.DROPPED_COLUMNS,
                 "dropped column registry",
                 "CREATE TABLE %s ("
                 + "keyspace_name text,"
@@ -169,7 +184,7 @@
                 + "PRIMARY KEY ((keyspace_name), table_name, column_name))");
 
     private static final CFMetaData Triggers =
-        compile(TRIGGERS,
+        compile(SchemaKeyspaceTables.TRIGGERS,
                 "trigger definitions",
                 "CREATE TABLE %s ("
                 + "keyspace_name text,"
@@ -179,7 +194,7 @@
                 + "PRIMARY KEY ((keyspace_name), table_name, trigger_name))");
 
     private static final CFMetaData Views =
-        compile(VIEWS,
+        compile(SchemaKeyspaceTables.VIEWS,
                 "view definitions",
                 "CREATE TABLE %s ("
                 + "keyspace_name text,"
@@ -204,10 +219,11 @@
                 + "min_index_interval int,"
                 + "read_repair_chance double,"
                 + "speculative_retry text,"
+                + "cdc boolean,"
                 + "PRIMARY KEY ((keyspace_name), view_name))");
 
     private static final CFMetaData Indexes =
-        compile(INDEXES,
+        compile(SchemaKeyspaceTables.INDEXES,
                 "secondary index definitions",
                 "CREATE TABLE %s ("
                 + "keyspace_name text,"
@@ -218,7 +234,7 @@
                 + "PRIMARY KEY ((keyspace_name), table_name, index_name))");
 
     private static final CFMetaData Types =
-        compile(TYPES,
+        compile(SchemaKeyspaceTables.TYPES,
                 "user defined type definitions",
                 "CREATE TABLE %s ("
                 + "keyspace_name text,"
@@ -228,7 +244,7 @@
                 + "PRIMARY KEY ((keyspace_name), type_name))");
 
     private static final CFMetaData Functions =
-        compile(FUNCTIONS,
+        compile(SchemaKeyspaceTables.FUNCTIONS,
                 "user defined function definitions",
                 "CREATE TABLE %s ("
                 + "keyspace_name text,"
@@ -242,7 +258,7 @@
                 + "PRIMARY KEY ((keyspace_name), function_name, argument_types))");
 
     private static final CFMetaData Aggregates =
-        compile(AGGREGATES,
+        compile(SchemaKeyspaceTables.AGGREGATES,
                 "user defined aggregate definitions",
                 "CREATE TABLE %s ("
                 + "keyspace_name text,"
@@ -260,88 +276,99 @@
 
     private static CFMetaData compile(String name, String description, String schema)
     {
-        return CFMetaData.compile(String.format(schema, name), NAME)
+        return CFMetaData.compile(String.format(schema, name), SchemaConstants.SCHEMA_KEYSPACE_NAME)
                          .comment(description)
                          .gcGraceSeconds((int) TimeUnit.DAYS.toSeconds(7));
     }
 
     public static KeyspaceMetadata metadata()
     {
-        return KeyspaceMetadata.create(NAME, KeyspaceParams.local(), org.apache.cassandra.schema.Tables.of(ALL_TABLE_METADATA));
+        return KeyspaceMetadata.create(SchemaConstants.SCHEMA_KEYSPACE_NAME, KeyspaceParams.local(), org.apache.cassandra.schema.Tables.of(ALL_TABLE_METADATA));
     }
 
     /**
      * Add entries to system_schema.* for the hardcoded system keyspaces
      */
-    public static void saveSystemKeyspacesSchema()
+    public static synchronized void saveSystemKeyspacesSchema()
     {
-        KeyspaceMetadata system = Schema.instance.getKSMetaData(SystemKeyspace.NAME);
-        KeyspaceMetadata schema = Schema.instance.getKSMetaData(NAME);
+        KeyspaceMetadata system = Schema.instance.getKSMetaData(SchemaConstants.SYSTEM_KEYSPACE_NAME);
+        KeyspaceMetadata schema = Schema.instance.getKSMetaData(SchemaConstants.SCHEMA_KEYSPACE_NAME);
 
         long timestamp = FBUtilities.timestampMicros();
 
         // delete old, possibly obsolete entries in schema tables
-        for (String schemaTable : ALL)
+        for (String schemaTable : SchemaKeyspaceTables.ALL)
         {
-            String query = String.format("DELETE FROM %s.%s USING TIMESTAMP ? WHERE keyspace_name = ?", NAME, schemaTable);
-            for (String systemKeyspace : Schema.LOCAL_SYSTEM_KEYSPACE_NAMES)
+            String query = String.format("DELETE FROM %s.%s USING TIMESTAMP ? WHERE keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, schemaTable);
+            for (String systemKeyspace : SchemaConstants.LOCAL_SYSTEM_KEYSPACE_NAMES)
                 executeOnceInternal(query, timestamp, systemKeyspace);
         }
 
         // (+1 to timestamp to make sure we don't get shadowed by the tombstones we just added)
-        makeCreateKeyspaceMutation(system, timestamp + 1).apply();
-        makeCreateKeyspaceMutation(schema, timestamp + 1).apply();
+        makeCreateKeyspaceMutation(system, timestamp + 1).build().apply();
+        makeCreateKeyspaceMutation(schema, timestamp + 1).build().apply();
     }
 
-    public static void truncate()
+    public static synchronized void truncate()
     {
-        ALL.reverse().forEach(table -> getSchemaCFS(table).truncateBlocking());
+        SchemaKeyspaceTables.ALL.reverse().forEach(table -> getSchemaCFS(table).truncateBlocking());
     }
 
     static void flush()
     {
-        if (!Boolean.getBoolean("cassandra.unsafesystem"))
-            ALL.forEach(table -> FBUtilities.waitOnFuture(getSchemaCFS(table).forceFlush()));
+        if (!DatabaseDescriptor.isUnsafeSystem())
+            SchemaKeyspaceTables.ALL.forEach(table -> FBUtilities.waitOnFuture(getSchemaCFS(table).forceFlush()));
     }
 
     /**
      * Read schema from system keyspace and calculate MD5 digest of every row, resulting digest
      * will be converted into UUID which would act as content-based version of the schema.
+     *
+     * This implementation is special cased for 3.11 as it returns the schema digests for 3.11
+     * <em>and</em> 3.0 - i.e. with and without the beloved {@code cdc} column.
      */
-    public static UUID calculateSchemaDigest()
+    public static Pair<UUID, UUID> calculateSchemaDigest()
+    {
+        Set<ByteBuffer> cdc = Collections.singleton(ByteBufferUtil.bytes("cdc"));
+
+        return calculateSchemaDigest(cdc);
+    }
+
+    @VisibleForTesting
+    static synchronized Pair<UUID, UUID> calculateSchemaDigest(Set<ByteBuffer> columnsToExclude)
     {
         MessageDigest digest;
+        MessageDigest digest30;
         try
         {
             digest = MessageDigest.getInstance("MD5");
+            digest30 = MessageDigest.getInstance("MD5");
         }
         catch (NoSuchAlgorithmException e)
         {
             throw new RuntimeException(e);
         }
 
-        for (String table : ALL_FOR_DIGEST)
+        for (String table : SchemaKeyspaceTables.ALL_FOR_DIGEST)
         {
-            // Due to CASSANDRA-11050 we want to exclude DROPPED_COLUMNS for schema digest computation. We can and
-            // should remove that in the next major release (so C* 4.0).
-            if (table.equals(DROPPED_COLUMNS))
-                continue;
-
             ReadCommand cmd = getReadCommandForTableSchema(table);
-            try (ReadOrderGroup orderGroup = cmd.startOrderGroup();
-                 PartitionIterator schema = cmd.executeInternal(orderGroup))
+            try (ReadExecutionController executionController = cmd.executionController();
+                 PartitionIterator schema = cmd.executeInternal(executionController))
             {
                 while (schema.hasNext())
                 {
                     try (RowIterator partition = schema.next())
                     {
                         if (!isSystemKeyspaceSchemaPartition(partition.partitionKey()))
-                            RowIterators.digest(partition, digest);
+                        {
+                            RowIterators.digest(partition, digest, digest30, columnsToExclude);
+                        }
                     }
                 }
             }
         }
-        return UUID.nameUUIDFromBytes(digest.digest());
+
+        return Pair.create(UUID.nameUUIDFromBytes(digest.digest()), UUID.nameUUIDFromBytes(digest30.digest()));
     }
 
     /**
@@ -350,7 +377,7 @@
      */
     private static ColumnFamilyStore getSchemaCFS(String schemaTableName)
     {
-        return Keyspace.open(NAME).getColumnFamilyStore(schemaTableName);
+        return Keyspace.open(SchemaConstants.SCHEMA_KEYSPACE_NAME).getColumnFamilyStore(schemaTableName);
     }
 
     /**
@@ -363,11 +390,11 @@
         return PartitionRangeReadCommand.allDataRead(cfs.metadata, FBUtilities.nowInSeconds());
     }
 
-    public static Collection<Mutation> convertSchemaToMutations()
+    public static synchronized Collection<Mutation> convertSchemaToMutations()
     {
         Map<DecoratedKey, Mutation> mutationMap = new HashMap<>();
 
-        for (String table : ALL)
+        for (String table : SchemaKeyspaceTables.ALL)
             convertSchemaToMutations(mutationMap, table);
 
         return mutationMap.values();
@@ -376,7 +403,8 @@
     private static void convertSchemaToMutations(Map<DecoratedKey, Mutation> mutationMap, String schemaTableName)
     {
         ReadCommand cmd = getReadCommandForTableSchema(schemaTableName);
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup(); UnfilteredPartitionIterator iter = cmd.executeLocally(orderGroup))
+        try (ReadExecutionController executionController = cmd.executionController();
+             UnfilteredPartitionIterator iter = cmd.executeLocally(executionController))
         {
             while (iter.hasNext())
             {
@@ -389,175 +417,196 @@
                     Mutation mutation = mutationMap.get(key);
                     if (mutation == null)
                     {
-                        mutation = new Mutation(NAME, key);
+                        mutation = new Mutation(SchemaConstants.SCHEMA_KEYSPACE_NAME, key);
                         mutationMap.put(key, mutation);
                     }
 
-                    mutation.add(PartitionUpdate.fromIterator(partition));
+                    mutation.add(makeUpdateForSchema(partition, cmd.columnFilter()));
                 }
             }
         }
     }
 
-    private static ByteBuffer getSchemaKSKey(String ksName)
+    /**
+     * Creates a PartitionUpdate from a partition containing some schema table content.
+     * This is mainly calling {@code PartitionUpdate.fromIterator} except if cdc is not enabled to handle the problem
+     * in #12236/#16770.
+     */
+    private static PartitionUpdate makeUpdateForSchema(UnfilteredRowIterator partition, ColumnFilter filter)
     {
-        return AsciiType.instance.fromString(ksName);
+        // This method is used during schema migration tasks, and if cdc is disabled, we want to force excluding the
+        // 'cdc' column from the TABLES/VIEWS schema table because it is problematic if received by older nodes (see #12236
+        // and #12697). Otherwise though, we just simply "buffer" the content of the partition into a PartitionUpdate.
+        if (DatabaseDescriptor.isCDCEnabled() || !TABLES_WITH_CDC_ADDED.contains(partition.metadata().cfName))
+            return PartitionUpdate.fromIterator(partition, filter);
+
+        // We want to skip the 'cdc' column. A simple solution for that is based on the fact that
+        // 'PartitionUpdate.fromIterator()' will ignore any columns that are marked as 'fetched' but not 'queried'.
+        ColumnFilter.Builder builder = ColumnFilter.selectionBuilder();
+        for (ColumnDefinition column : filter.fetchedColumns())
+        {
+            if (!column.name.toString().equals("cdc"))
+                builder.add(column);
+        }
+
+        return PartitionUpdate.fromIteratorExplicitColumns(partition, builder.build());
     }
 
     private static boolean isSystemKeyspaceSchemaPartition(DecoratedKey partitionKey)
     {
-        return Schema.isLocalSystemKeyspace(UTF8Type.instance.compose(partitionKey.getKey()));
+        return SchemaConstants.isLocalSystemKeyspace(UTF8Type.instance.compose(partitionKey.getKey()));
     }
 
     /*
      * Schema entities to mutations
      */
 
-    public static Mutation makeCreateKeyspaceMutation(String name, KeyspaceParams params, long timestamp)
+    private static DecoratedKey decorate(CFMetaData metadata, Object value)
     {
-        RowUpdateBuilder adder = new RowUpdateBuilder(Keyspaces, timestamp, name).clustering();
-        return adder.add(KeyspaceParams.Option.DURABLE_WRITES.toString(), params.durableWrites)
-                    .frozenMap(KeyspaceParams.Option.REPLICATION.toString(), params.replication.asMap())
-                    .build();
+        return metadata.decorateKey(((AbstractType)metadata.getKeyValidator()).decompose(value));
     }
 
-    public static Mutation makeCreateKeyspaceMutation(KeyspaceMetadata keyspace, long timestamp)
+    public static Mutation.SimpleBuilder makeCreateKeyspaceMutation(String name, KeyspaceParams params, long timestamp)
     {
-        Mutation mutation = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+        Mutation.SimpleBuilder builder = Mutation.simpleBuilder(Keyspaces.ksName, decorate(Keyspaces, name))
+                                                 .timestamp(timestamp);
 
-        keyspace.tables.forEach(table -> addTableToSchemaMutation(table, timestamp, true, mutation));
-        keyspace.views.forEach(view -> addViewToSchemaMutation(view, timestamp, true, mutation));
-        keyspace.types.forEach(type -> addTypeToSchemaMutation(type, timestamp, mutation));
-        keyspace.functions.udfs().forEach(udf -> addFunctionToSchemaMutation(udf, timestamp, mutation));
-        keyspace.functions.udas().forEach(uda -> addAggregateToSchemaMutation(uda, timestamp, mutation));
+        builder.update(Keyspaces)
+               .row()
+               .add(KeyspaceParams.Option.DURABLE_WRITES.toString(), params.durableWrites)
+               .add(KeyspaceParams.Option.REPLICATION.toString(), params.replication.asMap());
 
-        return mutation;
+        return builder;
     }
 
-    public static Mutation makeDropKeyspaceMutation(KeyspaceMetadata keyspace, long timestamp)
+    public static Mutation.SimpleBuilder makeCreateKeyspaceMutation(KeyspaceMetadata keyspace, long timestamp)
     {
-        int nowInSec = FBUtilities.nowInSeconds();
-        Mutation mutation = new Mutation(NAME, Keyspaces.decorateKey(getSchemaKSKey(keyspace.name)));
+        Mutation.SimpleBuilder builder = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+
+        keyspace.tables.forEach(table -> addTableToSchemaMutation(table, true, builder));
+        keyspace.views.forEach(view -> addViewToSchemaMutation(view, true, builder));
+        keyspace.types.forEach(type -> addTypeToSchemaMutation(type, builder));
+        keyspace.functions.udfs().forEach(udf -> addFunctionToSchemaMutation(udf, builder));
+        keyspace.functions.udas().forEach(uda -> addAggregateToSchemaMutation(uda, builder));
+
+        return builder;
+    }
+
+    public static Mutation.SimpleBuilder makeDropKeyspaceMutation(KeyspaceMetadata keyspace, long timestamp)
+    {
+        Mutation.SimpleBuilder builder = Mutation.simpleBuilder(SchemaConstants.SCHEMA_KEYSPACE_NAME, decorate(Keyspaces, keyspace.name))
+                                                 .timestamp(timestamp);
 
         for (CFMetaData schemaTable : ALL_TABLE_METADATA)
-            mutation.add(PartitionUpdate.fullPartitionDelete(schemaTable, mutation.key(), timestamp, nowInSec));
+            builder.update(schemaTable).delete();
 
-        return mutation;
+        return builder;
     }
 
-    public static Mutation makeCreateTypeMutation(KeyspaceMetadata keyspace, UserType type, long timestamp)
+    public static Mutation.SimpleBuilder makeCreateTypeMutation(KeyspaceMetadata keyspace, UserType type, long timestamp)
     {
         // Include the serialized keyspace in case the target node missed a CREATE KEYSPACE migration (see CASSANDRA-5631).
-        Mutation mutation = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
-        addTypeToSchemaMutation(type, timestamp, mutation);
-        return mutation;
+        Mutation.SimpleBuilder builder = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+        addTypeToSchemaMutation(type, builder);
+        return builder;
     }
 
-    static void addTypeToSchemaMutation(UserType type, long timestamp, Mutation mutation)
+    static void addTypeToSchemaMutation(UserType type, Mutation.SimpleBuilder mutation)
     {
-        RowUpdateBuilder adder = new RowUpdateBuilder(Types, timestamp, mutation)
-                                 .clustering(type.getNameAsString())
-                                 .frozenList("field_names", type.fieldNames().stream().map(SchemaKeyspace::bbToString).collect(toList()))
-                                 .frozenList("field_types", type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList()));
-
-        adder.build();
+        mutation.update(Types)
+                .row(type.getNameAsString())
+                .add("field_names", type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList()))
+                .add("field_types", type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList()));
     }
 
-    private static String bbToString(ByteBuffer bb)
-    {
-        try
-        {
-            return ByteBufferUtil.string(bb);
-        }
-        catch (CharacterCodingException e)
-        {
-            throw new RuntimeException(e);
-        }
-    }
-
-    public static Mutation dropTypeFromSchemaMutation(KeyspaceMetadata keyspace, UserType type, long timestamp)
+    public static Mutation.SimpleBuilder dropTypeFromSchemaMutation(KeyspaceMetadata keyspace, UserType type, long timestamp)
     {
         // Include the serialized keyspace in case the target node missed a CREATE KEYSPACE migration (see CASSANDRA-5631).
-        Mutation mutation = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
-        return RowUpdateBuilder.deleteRow(Types, timestamp, mutation, type.name);
+        Mutation.SimpleBuilder builder = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+        builder.update(Types).row(type.name).delete();
+        return builder;
     }
 
-    public static Mutation makeCreateTableMutation(KeyspaceMetadata keyspace, CFMetaData table, long timestamp)
+    public static Mutation.SimpleBuilder makeCreateTableMutation(KeyspaceMetadata keyspace, CFMetaData table, long timestamp)
     {
         // Include the serialized keyspace in case the target node missed a CREATE KEYSPACE migration (see CASSANDRA-5631).
-        Mutation mutation = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
-        addTableToSchemaMutation(table, timestamp, true, mutation);
-        return mutation;
+        Mutation.SimpleBuilder builder = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+        addTableToSchemaMutation(table, true, builder);
+        return builder;
     }
 
-    public static void addTableToSchemaMutation(CFMetaData table, long timestamp, boolean withColumnsAndTriggers, Mutation mutation)
+    public static void addTableToSchemaMutation(CFMetaData table, boolean withColumnsAndTriggers, Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder adder = new RowUpdateBuilder(Tables, timestamp, mutation).clustering(table.cfName);
+        Row.SimpleBuilder rowBuilder = builder.update(Tables)
+                                              .row(table.cfName)
+                                              .add("id", table.cfId)
+                                              .add("flags", CFMetaData.flagsToStrings(table.flags()));
 
-        addTableParamsToSchemaMutation(table.params, adder);
-
-        adder.add("id", table.cfId)
-             .frozenSet("flags", CFMetaData.flagsToStrings(table.flags()))
-             .build();
+        addTableParamsToRowBuilder(table.params, rowBuilder);
 
         if (withColumnsAndTriggers)
         {
             for (ColumnDefinition column : table.allColumns())
-                addColumnToSchemaMutation(table, column, timestamp, mutation);
+                addColumnToSchemaMutation(table, column, builder);
 
             for (CFMetaData.DroppedColumn column : table.getDroppedColumns().values())
-                addDroppedColumnToSchemaMutation(table, column, timestamp, mutation);
+                addDroppedColumnToSchemaMutation(table, column, builder);
 
             for (TriggerMetadata trigger : table.getTriggers())
-                addTriggerToSchemaMutation(table, trigger, timestamp, mutation);
+                addTriggerToSchemaMutation(table, trigger, builder);
 
             for (IndexMetadata index : table.getIndexes())
-                addIndexToSchemaMutation(table, index, timestamp, mutation);
+                addIndexToSchemaMutation(table, index, builder);
         }
     }
 
-    private static void addTableParamsToSchemaMutation(TableParams params, RowUpdateBuilder adder)
+    private static void addTableParamsToRowBuilder(TableParams params, Row.SimpleBuilder builder)
     {
-        adder.add("bloom_filter_fp_chance", params.bloomFilterFpChance)
-             .add("comment", params.comment)
-             .add("dclocal_read_repair_chance", params.dcLocalReadRepairChance)
-             .add("default_time_to_live", params.defaultTimeToLive)
-             .add("gc_grace_seconds", params.gcGraceSeconds)
-             .add("max_index_interval", params.maxIndexInterval)
-             .add("memtable_flush_period_in_ms", params.memtableFlushPeriodInMs)
-             .add("min_index_interval", params.minIndexInterval)
-             .add("read_repair_chance", params.readRepairChance)
-             .add("speculative_retry", params.speculativeRetry.toString())
-             .add("crc_check_chance", params.crcCheckChance)
-             .frozenMap("caching", params.caching.asMap())
-             .frozenMap("compaction", params.compaction.asMap())
-             .frozenMap("compression", params.compression.asMap())
-             .frozenMap("extensions", params.extensions);
+        builder.add("bloom_filter_fp_chance", params.bloomFilterFpChance)
+               .add("comment", params.comment)
+               .add("dclocal_read_repair_chance", params.dcLocalReadRepairChance)
+               .add("default_time_to_live", params.defaultTimeToLive)
+               .add("gc_grace_seconds", params.gcGraceSeconds)
+               .add("max_index_interval", params.maxIndexInterval)
+               .add("memtable_flush_period_in_ms", params.memtableFlushPeriodInMs)
+               .add("min_index_interval", params.minIndexInterval)
+               .add("read_repair_chance", params.readRepairChance)
+               .add("speculative_retry", params.speculativeRetry.toString())
+               .add("crc_check_chance", params.crcCheckChance)
+               .add("caching", params.caching.asMap())
+               .add("compaction", params.compaction.asMap())
+               .add("compression", params.compression.asMap())
+               .add("extensions", params.extensions);
+
+        // Only add CDC-enabled flag to schema if it's enabled on the node. This is to work around RTE's post-8099 if a 3.8+
+        // node sends table schema to a < 3.8 versioned node with an unknown column.
+        if (DatabaseDescriptor.isCDCEnabled())
+            builder.add("cdc", params.cdc);
     }
 
-    public static Mutation makeUpdateTableMutation(KeyspaceMetadata keyspace,
-                                                   CFMetaData oldTable,
-                                                   CFMetaData newTable,
-                                                   long timestamp)
+    public static Mutation.SimpleBuilder makeUpdateTableMutation(KeyspaceMetadata keyspace,
+                                                                 CFMetaData oldTable,
+                                                                 CFMetaData newTable,
+                                                                 long timestamp)
     {
-        Mutation mutation = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+        Mutation.SimpleBuilder builder = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
 
-        addTableToSchemaMutation(newTable, timestamp, false, mutation);
+        addTableToSchemaMutation(newTable, false, builder);
 
         MapDifference<ByteBuffer, ColumnDefinition> columnDiff = Maps.difference(oldTable.getColumnMetadata(),
                                                                                  newTable.getColumnMetadata());
 
         // columns that are no longer needed
         for (ColumnDefinition column : columnDiff.entriesOnlyOnLeft().values())
-            dropColumnFromSchemaMutation(oldTable, column, timestamp, mutation);
+            dropColumnFromSchemaMutation(oldTable, column, builder);
 
         // newly added columns
         for (ColumnDefinition column : columnDiff.entriesOnlyOnRight().values())
-            addColumnToSchemaMutation(newTable, column, timestamp, mutation);
+            addColumnToSchemaMutation(newTable, column, builder);
 
         // old columns with updated attributes
         for (ByteBuffer name : columnDiff.entriesDiffering().keySet())
-            addColumnToSchemaMutation(newTable, newTable.getColumnDefinition(name), timestamp, mutation);
+            addColumnToSchemaMutation(newTable, newTable.getColumnDefinition(name), builder);
 
         // dropped columns
         MapDifference<ByteBuffer, CFMetaData.DroppedColumn> droppedColumnDiff =
@@ -565,38 +614,38 @@
 
         // newly dropped columns
         for (CFMetaData.DroppedColumn column : droppedColumnDiff.entriesOnlyOnRight().values())
-            addDroppedColumnToSchemaMutation(newTable, column, timestamp, mutation);
+            addDroppedColumnToSchemaMutation(newTable, column, builder);
 
         // columns added then dropped again
         for (ByteBuffer name : droppedColumnDiff.entriesDiffering().keySet())
-            addDroppedColumnToSchemaMutation(newTable, newTable.getDroppedColumns().get(name), timestamp, mutation);
+            addDroppedColumnToSchemaMutation(newTable, newTable.getDroppedColumns().get(name), builder);
 
         MapDifference<String, TriggerMetadata> triggerDiff = triggersDiff(oldTable.getTriggers(), newTable.getTriggers());
 
         // dropped triggers
         for (TriggerMetadata trigger : triggerDiff.entriesOnlyOnLeft().values())
-            dropTriggerFromSchemaMutation(oldTable, trigger, timestamp, mutation);
+            dropTriggerFromSchemaMutation(oldTable, trigger, builder);
 
         // newly created triggers
         for (TriggerMetadata trigger : triggerDiff.entriesOnlyOnRight().values())
-            addTriggerToSchemaMutation(newTable, trigger, timestamp, mutation);
+            addTriggerToSchemaMutation(newTable, trigger, builder);
 
         MapDifference<String, IndexMetadata> indexesDiff = indexesDiff(oldTable.getIndexes(),
                                                                        newTable.getIndexes());
 
         // dropped indexes
         for (IndexMetadata index : indexesDiff.entriesOnlyOnLeft().values())
-            dropIndexFromSchemaMutation(oldTable, index, timestamp, mutation);
+            dropIndexFromSchemaMutation(oldTable, index, builder);
 
         // newly created indexes
         for (IndexMetadata index : indexesDiff.entriesOnlyOnRight().values())
-            addIndexToSchemaMutation(newTable, index, timestamp, mutation);
+            addIndexToSchemaMutation(newTable, index, builder);
 
         // updated indexes need to be updated
         for (MapDifference.ValueDifference<IndexMetadata> diff : indexesDiff.entriesDiffering().values())
-            addUpdatedIndexToSchemaMutation(newTable, diff.rightValue(), timestamp, mutation);
+            addUpdatedIndexToSchemaMutation(newTable, diff.rightValue(), builder);
 
-        return mutation;
+        return builder;
     }
 
     private static MapDifference<String, IndexMetadata> indexesDiff(Indexes before, Indexes after)
@@ -621,151 +670,143 @@
         return Maps.difference(beforeMap, afterMap);
     }
 
-    public static Mutation makeDropTableMutation(KeyspaceMetadata keyspace, CFMetaData table, long timestamp)
+    public static Mutation.SimpleBuilder makeDropTableMutation(KeyspaceMetadata keyspace, CFMetaData table, long timestamp)
     {
         // Include the serialized keyspace in case the target node missed a CREATE KEYSPACE migration (see CASSANDRA-5631).
-        Mutation mutation = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+        Mutation.SimpleBuilder builder = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
 
-        RowUpdateBuilder.deleteRow(Tables, timestamp, mutation, table.cfName);
+        builder.update(Tables).row(table.cfName).delete();
 
         for (ColumnDefinition column : table.allColumns())
-            dropColumnFromSchemaMutation(table, column, timestamp, mutation);
+            dropColumnFromSchemaMutation(table, column, builder);
 
         for (CFMetaData.DroppedColumn column : table.getDroppedColumns().values())
-            dropDroppedColumnFromSchemaMutation(table, column, timestamp, mutation);
+            dropDroppedColumnFromSchemaMutation(table, column, timestamp, builder);
 
         for (TriggerMetadata trigger : table.getTriggers())
-            dropTriggerFromSchemaMutation(table, trigger, timestamp, mutation);
+            dropTriggerFromSchemaMutation(table, trigger, builder);
 
         for (IndexMetadata index : table.getIndexes())
-            dropIndexFromSchemaMutation(table, index, timestamp, mutation);
+            dropIndexFromSchemaMutation(table, index, builder);
 
-        return mutation;
+        return builder;
     }
 
-    private static void addColumnToSchemaMutation(CFMetaData table, ColumnDefinition column, long timestamp, Mutation mutation)
+    private static void addColumnToSchemaMutation(CFMetaData table, ColumnDefinition column, Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder adder = new RowUpdateBuilder(Columns, timestamp, mutation).clustering(table.cfName, column.name.toString());
-
         AbstractType<?> type = column.type;
         if (type instanceof ReversedType)
             type = ((ReversedType) type).baseType;
 
-        adder.add("column_name_bytes", column.name.bytes)
-             .add("kind", column.kind.toString().toLowerCase())
-             .add("position", column.position())
-             .add("clustering_order", column.clusteringOrder().toString().toLowerCase())
-             .add("type", type.asCQL3Type().toString())
-             .build();
+        builder.update(Columns)
+               .row(table.cfName, column.name.toString())
+               .add("column_name_bytes", column.name.bytes)
+               .add("kind", column.kind.toString().toLowerCase())
+               .add("position", column.position())
+               .add("clustering_order", column.clusteringOrder().toString().toLowerCase())
+               .add("type", type.asCQL3Type().toString());
     }
 
-    private static void dropColumnFromSchemaMutation(CFMetaData table, ColumnDefinition column, long timestamp, Mutation mutation)
+    private static void dropColumnFromSchemaMutation(CFMetaData table, ColumnDefinition column, Mutation.SimpleBuilder builder)
     {
         // Note: we do want to use name.toString(), not name.bytes directly for backward compatibility (For CQL3, this won't make a difference).
-        RowUpdateBuilder.deleteRow(Columns, timestamp, mutation, table.cfName, column.name.toString());
+        builder.update(Columns).row(table.cfName, column.name.toString()).delete();
     }
 
-    private static void dropDroppedColumnFromSchemaMutation(CFMetaData table, DroppedColumn column, long timestamp, Mutation mutation)
+    private static void addDroppedColumnToSchemaMutation(CFMetaData table, CFMetaData.DroppedColumn column, Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder.deleteRow(DroppedColumns, timestamp, mutation, table.cfName, column.name);
+        builder.update(DroppedColumns)
+               .row(table.cfName, column.name)
+               .add("dropped_time", new Date(TimeUnit.MICROSECONDS.toMillis(column.droppedTime)))
+               .add("kind", null != column.kind ? column.kind.toString().toLowerCase() : null)
+               .add("type", expandUserTypes(column.type).asCQL3Type().toString());
     }
 
-    private static void addDroppedColumnToSchemaMutation(CFMetaData table, CFMetaData.DroppedColumn column, long timestamp, Mutation mutation)
+    private static void dropDroppedColumnFromSchemaMutation(CFMetaData table, DroppedColumn column, long timestamp, Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder adder = new RowUpdateBuilder(DroppedColumns, timestamp, mutation).clustering(table.cfName, column.name);
-
-        adder.add("dropped_time", new Date(TimeUnit.MICROSECONDS.toMillis(column.droppedTime)))
-             .add("kind", null != column.kind ? column.kind.toString().toLowerCase() : null)
-             .add("type", expandUserTypes(column.type).asCQL3Type().toString())
-             .build();
+        builder.update(DroppedColumns).row(table.cfName, column.name).delete();
     }
 
-    private static void addTriggerToSchemaMutation(CFMetaData table, TriggerMetadata trigger, long timestamp, Mutation mutation)
+    private static void addTriggerToSchemaMutation(CFMetaData table, TriggerMetadata trigger, Mutation.SimpleBuilder builder)
     {
-        new RowUpdateBuilder(Triggers, timestamp, mutation)
-            .clustering(table.cfName, trigger.name)
-            .frozenMap("options", Collections.singletonMap("class", trigger.classOption))
-            .build();
+        builder.update(Triggers)
+               .row(table.cfName, trigger.name)
+               .add("options", Collections.singletonMap("class", trigger.classOption));
     }
 
-    private static void dropTriggerFromSchemaMutation(CFMetaData table, TriggerMetadata trigger, long timestamp, Mutation mutation)
+    private static void dropTriggerFromSchemaMutation(CFMetaData table, TriggerMetadata trigger, Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder.deleteRow(Triggers, timestamp, mutation, table.cfName, trigger.name);
+        builder.update(Triggers).row(table.cfName, trigger.name).delete();
     }
 
-    public static Mutation makeCreateViewMutation(KeyspaceMetadata keyspace, ViewDefinition view, long timestamp)
+    public static Mutation.SimpleBuilder makeCreateViewMutation(KeyspaceMetadata keyspace, ViewDefinition view, long timestamp)
     {
         // Include the serialized keyspace in case the target node missed a CREATE KEYSPACE migration (see CASSANDRA-5631).
-        Mutation mutation = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
-        addViewToSchemaMutation(view, timestamp, true, mutation);
-        return mutation;
+        Mutation.SimpleBuilder builder = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+        addViewToSchemaMutation(view, true, builder);
+        return builder;
     }
 
-    private static void addViewToSchemaMutation(ViewDefinition view, long timestamp, boolean includeColumns, Mutation mutation)
+    private static void addViewToSchemaMutation(ViewDefinition view, boolean includeColumns, Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder builder = new RowUpdateBuilder(Views, timestamp, mutation)
-            .clustering(view.viewName);
-
         CFMetaData table = view.metadata;
+        Row.SimpleBuilder rowBuilder = builder.update(Views)
+                                              .row(view.viewName)
+                                              .add("include_all_columns", view.includeAllColumns)
+                                              .add("base_table_id", view.baseTableId)
+                                              .add("base_table_name", view.baseTableMetadata().cfName)
+                                              .add("where_clause", view.whereClause)
+                                              .add("id", table.cfId);
 
-        builder.add("include_all_columns", view.includeAllColumns)
-               .add("base_table_id", view.baseTableId)
-               .add("base_table_name", view.baseTableMetadata().cfName)
-               .add("where_clause", view.whereClause)
-               .add("id", table.cfId);
-
-        addTableParamsToSchemaMutation(table.params, builder);
+        addTableParamsToRowBuilder(table.params, rowBuilder);
 
         if (includeColumns)
         {
             for (ColumnDefinition column : table.allColumns())
-                addColumnToSchemaMutation(table, column, timestamp, mutation);
+                addColumnToSchemaMutation(table, column, builder);
 
             for (CFMetaData.DroppedColumn column : table.getDroppedColumns().values())
-                addDroppedColumnToSchemaMutation(table, column, timestamp, mutation);
+                addDroppedColumnToSchemaMutation(table, column, builder);
         }
-
-        builder.build();
     }
 
-    public static Mutation makeDropViewMutation(KeyspaceMetadata keyspace, ViewDefinition view, long timestamp)
+    public static Mutation.SimpleBuilder makeDropViewMutation(KeyspaceMetadata keyspace, ViewDefinition view, long timestamp)
     {
         // Include the serialized keyspace in case the target node missed a CREATE KEYSPACE migration (see CASSANDRA-5631).
-        Mutation mutation = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+        Mutation.SimpleBuilder builder = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
 
-        RowUpdateBuilder.deleteRow(Views, timestamp, mutation, view.viewName);
+        builder.update(Views).row(view.viewName).delete();
 
         CFMetaData table = view.metadata;
         for (ColumnDefinition column : table.allColumns())
-            dropColumnFromSchemaMutation(table, column, timestamp, mutation);
+            dropColumnFromSchemaMutation(table, column, builder);
 
         for (IndexMetadata index : table.getIndexes())
-            dropIndexFromSchemaMutation(table, index, timestamp, mutation);
+            dropIndexFromSchemaMutation(table, index, builder);
 
-        return mutation;
+        return builder;
     }
 
-    public static Mutation makeUpdateViewMutation(Mutation mutation,
-                                                  ViewDefinition oldView,
-                                                  ViewDefinition newView,
-                                                  long timestamp)
+    public static Mutation.SimpleBuilder makeUpdateViewMutation(Mutation.SimpleBuilder builder,
+                                                                ViewDefinition oldView,
+                                                                ViewDefinition newView)
     {
-        addViewToSchemaMutation(newView, timestamp, false, mutation);
+        addViewToSchemaMutation(newView, false, builder);
 
         MapDifference<ByteBuffer, ColumnDefinition> columnDiff = Maps.difference(oldView.metadata.getColumnMetadata(),
                                                                                  newView.metadata.getColumnMetadata());
 
         // columns that are no longer needed
         for (ColumnDefinition column : columnDiff.entriesOnlyOnLeft().values())
-            dropColumnFromSchemaMutation(oldView.metadata, column, timestamp, mutation);
+            dropColumnFromSchemaMutation(oldView.metadata, column, builder);
 
         // newly added columns
         for (ColumnDefinition column : columnDiff.entriesOnlyOnRight().values())
-            addColumnToSchemaMutation(newView.metadata, column, timestamp, mutation);
+            addColumnToSchemaMutation(newView.metadata, column, builder);
 
         // old columns with updated attributes
         for (ByteBuffer name : columnDiff.entriesDiffering().keySet())
-            addColumnToSchemaMutation(newView.metadata, newView.metadata.getColumnDefinition(name), timestamp, mutation);
+            addColumnToSchemaMutation(newView.metadata, newView.metadata.getColumnDefinition(name), builder);
 
         // dropped columns
         MapDifference<ByteBuffer, CFMetaData.DroppedColumn> droppedColumnDiff =
@@ -773,63 +814,68 @@
 
         // newly dropped columns
         for (CFMetaData.DroppedColumn column : droppedColumnDiff.entriesOnlyOnRight().values())
-            addDroppedColumnToSchemaMutation(oldView.metadata, column, timestamp, mutation);
+            addDroppedColumnToSchemaMutation(oldView.metadata, column, builder);
 
         // columns added then dropped again
         for (ByteBuffer name : droppedColumnDiff.entriesDiffering().keySet())
-            addDroppedColumnToSchemaMutation(newView.metadata, newView.metadata.getDroppedColumns().get(name), timestamp, mutation);
+            addDroppedColumnToSchemaMutation(newView.metadata, newView.metadata.getDroppedColumns().get(name), builder);
 
-        return mutation;
+        return builder;
     }
 
     private static void addIndexToSchemaMutation(CFMetaData table,
                                                  IndexMetadata index,
-                                                 long timestamp,
-                                                 Mutation mutation)
+                                                 Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder builder = new RowUpdateBuilder(Indexes, timestamp, mutation).clustering(table.cfName, index.name);
-
-        builder.add("kind", index.kind.toString());
-        builder.frozenMap("options", index.options);
-        builder.build();
+        builder.update(Indexes)
+               .row(table.cfName, index.name)
+               .add("kind", index.kind.toString())
+               .add("options", index.options);
     }
 
     private static void dropIndexFromSchemaMutation(CFMetaData table,
                                                     IndexMetadata index,
-                                                    long timestamp,
-                                                    Mutation mutation)
+                                                    Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder.deleteRow(Indexes, timestamp, mutation, table.cfName, index.name);
+        builder.update(Indexes).row(table.cfName, index.name).delete();
     }
 
     private static void addUpdatedIndexToSchemaMutation(CFMetaData table,
                                                         IndexMetadata index,
-                                                        long timestamp,
-                                                        Mutation mutation)
+                                                        Mutation.SimpleBuilder builder)
     {
-        addIndexToSchemaMutation(table, index, timestamp, mutation);
+        addIndexToSchemaMutation(table, index, builder);
     }
 
-    public static Mutation makeCreateFunctionMutation(KeyspaceMetadata keyspace, UDFunction function, long timestamp)
+    public static Mutation.SimpleBuilder makeCreateFunctionMutation(KeyspaceMetadata keyspace, UDFunction function, long timestamp)
     {
         // Include the serialized keyspace in case the target node missed a CREATE KEYSPACE migration (see CASSANDRA-5631).
-        Mutation mutation = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
-        addFunctionToSchemaMutation(function, timestamp, mutation);
-        return mutation;
+        Mutation.SimpleBuilder builder = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+        addFunctionToSchemaMutation(function, builder);
+        return builder;
     }
 
-    static void addFunctionToSchemaMutation(UDFunction function, long timestamp, Mutation mutation)
+    static void addFunctionToSchemaMutation(UDFunction function, Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder adder =
-            new RowUpdateBuilder(Functions, timestamp, mutation).clustering(function.name().name, functionArgumentsList(function));
+        builder.update(Functions)
+               .row(function.name().name, functionArgumentsList(function))
+               .add("body", function.body())
+               .add("language", function.language())
+               .add("return_type", function.returnType().asCQL3Type().toString())
+               .add("called_on_null_input", function.isCalledOnNullInput())
+               .add("argument_names", function.argNames().stream().map((c) -> bbToString(c.bytes)).collect(toList()));
+    }
 
-        adder.add("body", function.body())
-             .add("language", function.language())
-             .add("return_type", function.returnType().asCQL3Type().toString())
-             .add("called_on_null_input", function.isCalledOnNullInput())
-             .frozenList("argument_names", function.argNames().stream().map((c) -> bbToString(c.bytes)).collect(toList()));
-
-        adder.build();
+    private static String bbToString(ByteBuffer bb)
+    {
+        try
+        {
+            return ByteBufferUtil.string(bb);
+        }
+        catch (CharacterCodingException e)
+        {
+            throw new RuntimeException(e);
+        }
     }
 
     private static List<String> functionArgumentsList(AbstractFunction fun)
@@ -841,42 +887,42 @@
                   .collect(toList());
     }
 
-    public static Mutation makeDropFunctionMutation(KeyspaceMetadata keyspace, UDFunction function, long timestamp)
+    public static Mutation.SimpleBuilder makeDropFunctionMutation(KeyspaceMetadata keyspace, UDFunction function, long timestamp)
     {
         // Include the serialized keyspace in case the target node missed a CREATE KEYSPACE migration (see CASSANDRA-5631).
-        Mutation mutation = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
-        return RowUpdateBuilder.deleteRow(Functions, timestamp, mutation, function.name().name, functionArgumentsList(function));
+        Mutation.SimpleBuilder builder = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+        builder.update(Functions).row(function.name().name, functionArgumentsList(function)).delete();
+        return builder;
     }
 
-    public static Mutation makeCreateAggregateMutation(KeyspaceMetadata keyspace, UDAggregate aggregate, long timestamp)
+    public static Mutation.SimpleBuilder makeCreateAggregateMutation(KeyspaceMetadata keyspace, UDAggregate aggregate, long timestamp)
     {
         // Include the serialized keyspace in case the target node missed a CREATE KEYSPACE migration (see CASSANDRA-5631).
-        Mutation mutation = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
-        addAggregateToSchemaMutation(aggregate, timestamp, mutation);
-        return mutation;
+        Mutation.SimpleBuilder builder = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+        addAggregateToSchemaMutation(aggregate, builder);
+        return builder;
     }
 
-    static void addAggregateToSchemaMutation(UDAggregate aggregate, long timestamp, Mutation mutation)
+    static void addAggregateToSchemaMutation(UDAggregate aggregate, Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder adder =
-            new RowUpdateBuilder(Aggregates, timestamp, mutation) .clustering(aggregate.name().name, functionArgumentsList(aggregate));
-
-        adder.add("return_type", aggregate.returnType().asCQL3Type().toString())
-             .add("state_func", aggregate.stateFunction().name().name)
-             .add("state_type", aggregate.stateType().asCQL3Type().toString())
-             .add("final_func", aggregate.finalFunction() != null ? aggregate.finalFunction().name().name : null)
-             .add("initcond", aggregate.initialCondition() != null
-                              // must use the frozen state type here, as 'null' for unfrozen collections may mean 'empty'
-                              ? aggregate.stateType().freeze().asCQL3Type().toCQLLiteral(aggregate.initialCondition(), Server.CURRENT_VERSION)
-                              : null)
-             .build();
+        builder.update(Aggregates)
+               .row(aggregate.name().name, functionArgumentsList(aggregate))
+               .add("return_type", aggregate.returnType().asCQL3Type().toString())
+               .add("state_func", aggregate.stateFunction().name().name)
+               .add("state_type", aggregate.stateType().asCQL3Type().toString())
+               .add("final_func", aggregate.finalFunction() != null ? aggregate.finalFunction().name().name : null)
+               .add("initcond", aggregate.initialCondition() != null
+                                // must use the frozen state type here, as 'null' for unfrozen collections may mean 'empty'
+                                ? aggregate.stateType().freeze().asCQL3Type().toCQLLiteral(aggregate.initialCondition(), ProtocolVersion.CURRENT)
+                                : null);
     }
 
-    public static Mutation makeDropAggregateMutation(KeyspaceMetadata keyspace, UDAggregate aggregate, long timestamp)
+    public static Mutation.SimpleBuilder makeDropAggregateMutation(KeyspaceMetadata keyspace, UDAggregate aggregate, long timestamp)
     {
         // Include the serialized keyspace in case the target node missed a CREATE KEYSPACE migration (see CASSANDRA-5631).
-        Mutation mutation = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
-        return RowUpdateBuilder.deleteRow(Aggregates, timestamp, mutation, aggregate.name().name, functionArgumentsList(aggregate));
+        Mutation.SimpleBuilder builder = makeCreateKeyspaceMutation(keyspace.name, keyspace.params, timestamp);
+        builder.update(Aggregates).row(aggregate.name().name, functionArgumentsList(aggregate)).delete();
+        return builder;
     }
 
     /*
@@ -885,12 +931,12 @@
 
     public static Keyspaces fetchNonSystemKeyspaces()
     {
-        return fetchKeyspacesWithout(Schema.LOCAL_SYSTEM_KEYSPACE_NAMES);
+        return fetchKeyspacesWithout(SchemaConstants.LOCAL_SYSTEM_KEYSPACE_NAMES);
     }
 
     private static Keyspaces fetchKeyspacesWithout(Set<String> excludedKeyspaceNames)
     {
-        String query = format("SELECT keyspace_name FROM %s.%s", NAME, KEYSPACES);
+        String query = format("SELECT keyspace_name FROM %s.%s", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.KEYSPACES);
 
         Keyspaces.Builder keyspaces = org.apache.cassandra.schema.Keyspaces.builder();
         for (UntypedResultSet.Row row : query(query))
@@ -908,7 +954,7 @@
          * We know the keyspace names we are going to query, but we still want to run the SELECT IN
          * query, to filter out the keyspaces that had been dropped by the applied mutation set.
          */
-        String query = format("SELECT keyspace_name FROM %s.%s WHERE keyspace_name IN ?", NAME, KEYSPACES);
+        String query = format("SELECT keyspace_name FROM %s.%s WHERE keyspace_name IN ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.KEYSPACES);
 
         Keyspaces.Builder keyspaces = org.apache.cassandra.schema.Keyspaces.builder();
         for (UntypedResultSet.Row row : query(query, new ArrayList<>(includedKeyspaceNames)))
@@ -928,7 +974,7 @@
 
     private static KeyspaceParams fetchKeyspaceParams(String keyspaceName)
     {
-        String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ?", NAME, KEYSPACES);
+        String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.KEYSPACES);
 
         UntypedResultSet.Row row = query(query, keyspaceName).one();
         boolean durableWrites = row.getBoolean(KeyspaceParams.Option.DURABLE_WRITES.toString());
@@ -938,7 +984,7 @@
 
     private static Types fetchTypes(String keyspaceName)
     {
-        String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ?", NAME, TYPES);
+        String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TYPES);
 
         Types.RawBuilder types = org.apache.cassandra.schema.Types.rawBuilder(keyspaceName);
         for (UntypedResultSet.Row row : query(query, keyspaceName))
@@ -953,7 +999,7 @@
 
     private static Tables fetchTables(String keyspaceName, Types types)
     {
-        String query = format("SELECT table_name FROM %s.%s WHERE keyspace_name = ?", NAME, TABLES);
+        String query = format("SELECT table_name FROM %s.%s WHERE keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TABLES);
 
         Tables.Builder tables = org.apache.cassandra.schema.Tables.builder();
         for (UntypedResultSet.Row row : query(query, keyspaceName))
@@ -971,10 +1017,10 @@
                                                 "\"DELETE FROM %s.%s WHERE keyspace_name = '%s' AND table_name = '%s'; " +
                                                 "DELETE FROM %s.%s WHERE keyspace_name = '%s' AND table_name = '%s';\" " +
                                                 "If the table is not supposed to be dropped, restore %s.%s sstables from backups.",
-                                                keyspaceName, tableName, NAME, COLUMNS,
-                                                NAME, TABLES, keyspaceName, tableName,
-                                                NAME, COLUMNS, keyspaceName, tableName,
-                                                NAME, COLUMNS);
+                                                keyspaceName, tableName, SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.COLUMNS,
+                                                SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TABLES, keyspaceName, tableName,
+                                                SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.COLUMNS, keyspaceName, tableName,
+                                                SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.COLUMNS);
 
                 if (IGNORE_CORRUPTED_SCHEMA_TABLES)
                 {
@@ -992,7 +1038,7 @@
 
     private static CFMetaData fetchTable(String keyspaceName, String tableName, Types types)
     {
-        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", NAME, TABLES);
+        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TABLES);
         UntypedResultSet rows = query(query, keyspaceName, tableName);
         if (rows.isEmpty())
             throw new RuntimeException(String.format("%s:%s not found in the schema definitions keyspace.", keyspaceName, tableName));
@@ -1008,6 +1054,12 @@
         boolean isCompound = flags.contains(CFMetaData.Flag.COMPOUND);
 
         List<ColumnDefinition> columns = fetchColumns(keyspaceName, tableName, types);
+        if (!columns.stream().anyMatch(ColumnDefinition::isPartitionKey))
+        {
+            String msg = String.format("Table %s.%s did not have any partition key columns in the schema tables", keyspaceName, tableName);
+            throw new AssertionError(msg);
+        }
+
         Map<ByteBuffer, CFMetaData.DroppedColumn> droppedColumns = fetchDroppedColumns(keyspaceName, tableName);
         Indexes indexes = fetchIndexes(keyspaceName, tableName);
         Triggers triggers = fetchTriggers(keyspaceName, tableName);
@@ -1046,12 +1098,13 @@
                           .readRepairChance(row.getDouble("read_repair_chance"))
                           .crcCheckChance(row.getDouble("crc_check_chance"))
                           .speculativeRetry(SpeculativeRetryParam.fromString(row.getString("speculative_retry")))
+                          .cdc(row.has("cdc") ? row.getBoolean("cdc") : false)
                           .build();
     }
 
     private static List<ColumnDefinition> fetchColumns(String keyspace, String table, Types types)
     {
-        String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", NAME, COLUMNS);
+        String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.COLUMNS);
         UntypedResultSet columnRows = query(query, keyspace, table);
         if (columnRows.isEmpty())
             throw new MissingColumns("Columns not found in schema table for " + keyspace + "." + table);
@@ -1086,7 +1139,7 @@
 
     private static Map<ByteBuffer, CFMetaData.DroppedColumn> fetchDroppedColumns(String keyspace, String table)
     {
-        String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", NAME, DROPPED_COLUMNS);
+        String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.DROPPED_COLUMNS);
         Map<ByteBuffer, CFMetaData.DroppedColumn> columns = new HashMap<>();
         for (UntypedResultSet.Row row : query(query, keyspace, table))
         {
@@ -1116,7 +1169,7 @@
 
     private static Indexes fetchIndexes(String keyspace, String table)
     {
-        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", NAME, INDEXES);
+        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.INDEXES);
         Indexes.Builder indexes = org.apache.cassandra.schema.Indexes.builder();
         query(query, keyspace, table).forEach(row -> indexes.add(createIndexMetadataFromRow(row)));
         return indexes.build();
@@ -1132,7 +1185,7 @@
 
     private static Triggers fetchTriggers(String keyspace, String table)
     {
-        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", NAME, TRIGGERS);
+        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TRIGGERS);
         Triggers.Builder triggers = org.apache.cassandra.schema.Triggers.builder();
         query(query, keyspace, table).forEach(row -> triggers.add(createTriggerFromRow(row)));
         return triggers.build();
@@ -1147,7 +1200,7 @@
 
     private static Views fetchViews(String keyspaceName, Types types)
     {
-        String query = format("SELECT view_name FROM %s.%s WHERE keyspace_name = ?", NAME, VIEWS);
+        String query = format("SELECT view_name FROM %s.%s WHERE keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.VIEWS);
 
         Views.Builder views = org.apache.cassandra.schema.Views.builder();
         for (UntypedResultSet.Row row : query(query, keyspaceName))
@@ -1157,7 +1210,7 @@
 
     private static ViewDefinition fetchView(String keyspaceName, String viewName, Types types)
     {
-        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND view_name = ?", NAME, VIEWS);
+        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND view_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.VIEWS);
         UntypedResultSet rows = query(query, keyspaceName, viewName);
         if (rows.isEmpty())
             throw new RuntimeException(String.format("%s:%s not found in the schema definitions keyspace.", keyspaceName, viewName));
@@ -1205,7 +1258,7 @@
 
     private static Functions fetchUDFs(String keyspaceName, Types types)
     {
-        String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ?", NAME, FUNCTIONS);
+        String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.FUNCTIONS);
 
         Functions.Builder functions = org.apache.cassandra.schema.Functions.builder();
         for (UntypedResultSet.Row row : query(query, keyspaceName))
@@ -1266,7 +1319,7 @@
 
     private static Functions fetchUDAs(String keyspaceName, Functions udfs, Types types)
     {
-        String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ?", NAME, AGGREGATES);
+        String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.AGGREGATES);
 
         Functions.Builder aggregates = org.apache.cassandra.schema.Functions.builder();
         for (UntypedResultSet.Row row : query(query, keyspaceName))
@@ -1457,7 +1510,8 @@
      * We do it for dropped_columns, to allow safely dropping unused user types without retaining any references
      * in dropped_columns.
      */
-    private static AbstractType<?> expandUserTypes(AbstractType<?> original)
+    @VisibleForTesting
+    public static AbstractType<?> expandUserTypes(AbstractType<?> original)
     {
         if (original instanceof UserType)
             return new TupleType(expandUserTypes(((UserType) original).fieldTypes()));
diff --git a/src/java/org/apache/cassandra/schema/SchemaKeyspaceTables.java b/src/java/org/apache/cassandra/schema/SchemaKeyspaceTables.java
new file mode 100644
index 0000000..a1ae445
--- /dev/null
+++ b/src/java/org/apache/cassandra/schema/SchemaKeyspaceTables.java
@@ -0,0 +1,77 @@
+/*
+ * 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.schema;
+
+import com.google.common.collect.ImmutableList;
+
+public class SchemaKeyspaceTables
+{
+    public static final String KEYSPACES = "keyspaces";
+    public static final String TABLES = "tables";
+    public static final String COLUMNS = "columns";
+    public static final String DROPPED_COLUMNS = "dropped_columns";
+    public static final String TRIGGERS = "triggers";
+    public static final String VIEWS = "views";
+    public static final String TYPES = "types";
+    public static final String FUNCTIONS = "functions";
+    public static final String AGGREGATES = "aggregates";
+    public static final String INDEXES = "indexes";
+
+    /**
+     * The order in this list matters.
+     *
+     * When flushing schema tables, we want to flush them in a way that mitigates the effects of an abrupt shutdown
+     * whilst the tables are being flushed. On startup, we load the schema from disk before replaying the CL, so we need
+     * to try to avoid problems like reading a table without columns or types, for example. So columns and types should
+     * be flushed before tables, which should be flushed before keyspaces.
+     *
+     * When truncating, the order should be reversed. For immutable lists this is an efficient operation that simply
+     * iterates in reverse order.
+     *
+     * See CASSANDRA-12213 for more details.
+     */
+    public static final ImmutableList<String> ALL = ImmutableList.of(COLUMNS,
+                                                                     DROPPED_COLUMNS,
+                                                                     TRIGGERS,
+                                                                     TYPES,
+                                                                     FUNCTIONS,
+                                                                     AGGREGATES,
+                                                                     INDEXES,
+                                                                     TABLES,
+                                                                     VIEWS,
+                                                                     KEYSPACES);
+
+    /**
+     * Until we upgrade the messaging service version, that is version 4.0, we must preserve the old order (before
+     * CASSANDRA-12213) for digest calculations, otherwise the nodes will never agree on the schema during a rolling
+     * upgrade, see CASSANDRA-13559.
+     */
+    public static final ImmutableList<String> ALL_FOR_DIGEST = ImmutableList.of(KEYSPACES,
+                                                                                TABLES,
+                                                                                COLUMNS,
+                                                                                TRIGGERS,
+                                                                                VIEWS,
+                                                                                TYPES,
+                                                                                FUNCTIONS,
+                                                                                AGGREGATES,
+                                                                                INDEXES);
+
+    private SchemaKeyspaceTables()
+    {
+    }
+}
diff --git a/src/java/org/apache/cassandra/schema/TableParams.java b/src/java/org/apache/cassandra/schema/TableParams.java
index dfa8603..3750aa1 100644
--- a/src/java/org/apache/cassandra/schema/TableParams.java
+++ b/src/java/org/apache/cassandra/schema/TableParams.java
@@ -27,6 +27,7 @@
 import org.apache.cassandra.cql3.Attributes;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.utils.BloomCalculations;
+
 import static java.lang.String.format;
 
 public final class TableParams
@@ -49,7 +50,8 @@
         MIN_INDEX_INTERVAL,
         READ_REPAIR_CHANCE,
         SPECULATIVE_RETRY,
-        CRC_CHECK_CHANCE;
+        CRC_CHECK_CHANCE,
+        CDC;
 
         @Override
         public String toString()
@@ -83,6 +85,7 @@
     public final CompactionParams compaction;
     public final CompressionParams compression;
     public final ImmutableMap<String, ByteBuffer> extensions;
+    public final boolean cdc;
 
     private TableParams(Builder builder)
     {
@@ -103,6 +106,7 @@
         compaction = builder.compaction;
         compression = builder.compression;
         extensions = builder.extensions;
+        cdc = builder.cdc;
     }
 
     public static Builder builder()
@@ -126,7 +130,8 @@
                             .minIndexInterval(params.minIndexInterval)
                             .readRepairChance(params.readRepairChance)
                             .speculativeRetry(params.speculativeRetry)
-                            .extensions(params.extensions);
+                            .extensions(params.extensions)
+                            .cdc(params.cdc);
     }
 
     public void validate()
@@ -219,7 +224,8 @@
             && caching.equals(p.caching)
             && compaction.equals(p.compaction)
             && compression.equals(p.compression)
-            && extensions.equals(p.extensions);
+            && extensions.equals(p.extensions)
+            && cdc == p.cdc;
     }
 
     @Override
@@ -239,7 +245,8 @@
                                 caching,
                                 compaction,
                                 compression,
-                                extensions);
+                                extensions,
+                                cdc);
     }
 
     @Override
@@ -261,6 +268,7 @@
                           .add(Option.COMPACTION.toString(), compaction)
                           .add(Option.COMPRESSION.toString(), compression)
                           .add(Option.EXTENSIONS.toString(), extensions)
+                          .add(Option.CDC.toString(), cdc)
                           .toString();
     }
 
@@ -281,6 +289,7 @@
         private CompactionParams compaction = CompactionParams.DEFAULT;
         private CompressionParams compression = CompressionParams.DEFAULT;
         private ImmutableMap<String, ByteBuffer> extensions = ImmutableMap.of();
+        private boolean cdc;
 
         public Builder()
         {
@@ -375,6 +384,12 @@
             return this;
         }
 
+        public Builder cdc(boolean val)
+        {
+            cdc = val;
+            return this;
+        }
+
         public Builder extensions(Map<String, ByteBuffer> val)
         {
             extensions = ImmutableMap.copyOf(val);
diff --git a/src/java/org/apache/cassandra/schema/TriggerMetadata.java b/src/java/org/apache/cassandra/schema/TriggerMetadata.java
index 2e0d547..d985081 100644
--- a/src/java/org/apache/cassandra/schema/TriggerMetadata.java
+++ b/src/java/org/apache/cassandra/schema/TriggerMetadata.java
@@ -18,6 +18,7 @@
  */
 package org.apache.cassandra.schema;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 
 public final class TriggerMetadata
@@ -64,9 +65,9 @@
     @Override
     public String toString()
     {
-        return Objects.toStringHelper(this)
-                      .add("name", name)
-                      .add("class", classOption)
-                      .toString();
+        return MoreObjects.toStringHelper(this)
+                          .add("name", name)
+                          .add("class", classOption)
+                          .toString();
     }
 }
diff --git a/src/java/org/apache/cassandra/schema/Types.java b/src/java/org/apache/cassandra/schema/Types.java
index 1b71364..c2d8aac 100644
--- a/src/java/org/apache/cassandra/schema/Types.java
+++ b/src/java/org/apache/cassandra/schema/Types.java
@@ -22,17 +22,13 @@
 
 import javax.annotation.Nullable;
 
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.MapDifference;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
+import com.google.common.collect.*;
 
+import org.apache.cassandra.cql3.FieldIdentifier;
 import org.apache.cassandra.cql3.CQL3Type;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.UserType;
 import org.apache.cassandra.exceptions.ConfigurationException;
-import org.apache.cassandra.utils.ByteBufferUtil;
 
 import static java.lang.String.format;
 import static com.google.common.collect.Iterables.filter;
@@ -139,7 +135,30 @@
     @Override
     public boolean equals(Object o)
     {
-        return this == o || (o instanceof Types && types.equals(((Types) o).types));
+        if (this == o)
+            return true;
+
+        if (!(o instanceof Types))
+            return false;
+
+        Types other = (Types) o;
+
+        if (types.size() != other.types.size())
+            return false;
+
+        Iterator<Map.Entry<ByteBuffer, UserType>> thisIter = this.types.entrySet().iterator();
+        Iterator<Map.Entry<ByteBuffer, UserType>> otherIter = other.types.entrySet().iterator();
+        while (thisIter.hasNext())
+        {
+            Map.Entry<ByteBuffer, UserType> thisNext = thisIter.next();
+            Map.Entry<ByteBuffer, UserType> otherNext = otherIter.next();
+            if (!thisNext.getKey().equals(otherNext.getKey()))
+                return false;
+
+            if (!thisNext.getValue().equals(otherNext.getValue(), true))  // ignore freezing
+                return false;
+        }
+        return true;
     }
 
     @Override
@@ -156,7 +175,7 @@
 
     public static final class Builder
     {
-        final ImmutableMap.Builder<ByteBuffer, UserType> types = ImmutableMap.builder();
+        final ImmutableSortedMap.Builder<ByteBuffer, UserType> types = ImmutableSortedMap.naturalOrder();
 
         private Builder()
         {
@@ -169,6 +188,7 @@
 
         public Builder add(UserType type)
         {
+            assert type.isMultiCell();
             types.put(type.name, type);
             return this;
         }
@@ -283,9 +303,9 @@
 
             UserType prepare(String keyspace, Types types)
             {
-                List<ByteBuffer> preparedFieldNames =
+                List<FieldIdentifier> preparedFieldNames =
                     fieldNames.stream()
-                              .map(ByteBufferUtil::bytes)
+                              .map(t -> FieldIdentifier.forInternalString(t))
                               .collect(toList());
 
                 List<AbstractType<?>> preparedFieldTypes =
@@ -293,7 +313,7 @@
                               .map(t -> t.prepareInternal(keyspace, types).getType())
                               .collect(toList());
 
-                return new UserType(keyspace, bytes(name), preparedFieldNames, preparedFieldTypes);
+                return new UserType(keyspace, bytes(name), preparedFieldNames, preparedFieldTypes, true);
             }
 
             @Override
diff --git a/src/java/org/apache/cassandra/security/CipherFactory.java b/src/java/org/apache/cassandra/security/CipherFactory.java
new file mode 100644
index 0000000..3f5c5f3
--- /dev/null
+++ b/src/java/org/apache/cassandra/security/CipherFactory.java
@@ -0,0 +1,176 @@
+/*
+ * 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.security;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.concurrent.ExecutionException;
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.RemovalListener;
+import com.google.common.cache.RemovalNotification;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.config.TransparentDataEncryptionOptions;
+
+/**
+ * A factory for loading encryption keys from {@link KeyProvider} instances.
+ * Maintains a cache of loaded keys to avoid invoking the key provider on every call.
+ */
+public class CipherFactory
+{
+    private final Logger logger = LoggerFactory.getLogger(CipherFactory.class);
+
+    /**
+     * Keep around thread local instances of Cipher as they are quite expensive to instantiate (@code Cipher#getInstance).
+     * Bonus points if you can avoid calling (@code Cipher#init); hence, the point of the supporting struct
+     * for caching Cipher instances.
+     */
+    private static final FastThreadLocal<CachedCipher> cipherThreadLocal = new FastThreadLocal<>();
+
+    private final SecureRandom secureRandom;
+    private final LoadingCache<String, Key> cache;
+    private final int ivLength;
+    private final KeyProvider keyProvider;
+
+    public CipherFactory(TransparentDataEncryptionOptions options)
+    {
+        logger.info("initializing CipherFactory");
+        ivLength = options.iv_length;
+
+        try
+        {
+            secureRandom = SecureRandom.getInstance("SHA1PRNG");
+            Class<KeyProvider> keyProviderClass = (Class<KeyProvider>)Class.forName(options.key_provider.class_name);
+            Constructor ctor = keyProviderClass.getConstructor(TransparentDataEncryptionOptions.class);
+            keyProvider = (KeyProvider)ctor.newInstance(options);
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException("couldn't load cipher factory", e);
+        }
+
+        cache = CacheBuilder.newBuilder() // by default cache is unbounded
+                .maximumSize(64) // a value large enough that we should never even get close (so nothing gets evicted)
+                .concurrencyLevel(Runtime.getRuntime().availableProcessors())
+                .removalListener(new RemovalListener<String, Key>()
+                {
+                    public void onRemoval(RemovalNotification<String, Key> notice)
+                    {
+                        // maybe reload the key? (to avoid the reload being on the user's dime)
+                        logger.info("key {} removed from cipher key cache", notice.getKey());
+                    }
+                })
+                .build(new CacheLoader<String, Key>()
+                {
+                    @Override
+                    public Key load(String alias) throws Exception
+                    {
+                        logger.info("loading secret key for alias {}", alias);
+                        return keyProvider.getSecretKey(alias);
+                    }
+                });
+    }
+
+    public Cipher getEncryptor(String transformation, String keyAlias) throws IOException
+    {
+        byte[] iv = new byte[ivLength];
+        secureRandom.nextBytes(iv);
+        return buildCipher(transformation, keyAlias, iv, Cipher.ENCRYPT_MODE);
+    }
+
+    public Cipher getDecryptor(String transformation, String keyAlias, byte[] iv) throws IOException
+    {
+        assert iv != null && iv.length > 0 : "trying to decrypt, but the initialization vector is empty";
+        return buildCipher(transformation, keyAlias, iv, Cipher.DECRYPT_MODE);
+    }
+
+    @VisibleForTesting
+    Cipher buildCipher(String transformation, String keyAlias, byte[] iv, int cipherMode) throws IOException
+    {
+        try
+        {
+            CachedCipher cachedCipher = cipherThreadLocal.get();
+            if (cachedCipher != null)
+            {
+                Cipher cipher = cachedCipher.cipher;
+                // rigorous checks to make sure we've absolutely got the correct instance (with correct alg/key/iv/...)
+                if (cachedCipher.mode == cipherMode && cipher.getAlgorithm().equals(transformation)
+                    && cachedCipher.keyAlias.equals(keyAlias) && Arrays.equals(cipher.getIV(), iv))
+                    return cipher;
+            }
+
+            Key key = retrieveKey(keyAlias);
+            Cipher cipher = Cipher.getInstance(transformation);
+            cipher.init(cipherMode, key, new IvParameterSpec(iv));
+            cipherThreadLocal.set(new CachedCipher(cipherMode, keyAlias, cipher));
+            return cipher;
+        }
+        catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException e)
+        {
+            logger.error("could not build cipher", e);
+            throw new IOException("cannot load cipher", e);
+        }
+    }
+
+    private Key retrieveKey(String keyAlias) throws IOException
+    {
+        try
+        {
+            return cache.get(keyAlias);
+        }
+        catch (ExecutionException e)
+        {
+            if (e.getCause() instanceof IOException)
+                throw (IOException)e.getCause();
+            throw new IOException("failed to load key from cache: " + keyAlias, e);
+        }
+    }
+
+    /**
+     * A simple struct to use with the thread local caching of Cipher as we can't get the mode (encrypt/decrypt) nor
+     * key_alias (or key!) from the Cipher itself to use for comparisons
+     */
+    private static class CachedCipher
+    {
+        public final int mode;
+        public final String keyAlias;
+        public final Cipher cipher;
+
+        private CachedCipher(int mode, String keyAlias, Cipher cipher)
+        {
+            this.mode = mode;
+            this.keyAlias = keyAlias;
+            this.cipher = cipher;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/security/EncryptionContext.java b/src/java/org/apache/cassandra/security/EncryptionContext.java
new file mode 100644
index 0000000..8176d60
--- /dev/null
+++ b/src/java/org/apache/cassandra/security/EncryptionContext.java
@@ -0,0 +1,172 @@
+/*
+ * 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.security;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.crypto.Cipher;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+
+import org.apache.cassandra.config.TransparentDataEncryptionOptions;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.io.compress.ICompressor;
+import org.apache.cassandra.io.compress.LZ4Compressor;
+import org.apache.cassandra.utils.Hex;
+
+/**
+ * A (largely) immutable wrapper for the application-wide file-level encryption settings.
+ */
+public class EncryptionContext
+{
+    public static final String ENCRYPTION_CIPHER = "encCipher";
+    public static final String ENCRYPTION_KEY_ALIAS = "encKeyAlias";
+    public static final String ENCRYPTION_IV = "encIV";
+
+    private final TransparentDataEncryptionOptions tdeOptions;
+    private final ICompressor compressor;
+    private final CipherFactory cipherFactory;
+
+    private final byte[] iv;
+    private final int chunkLength;
+
+    public EncryptionContext()
+    {
+        this(new TransparentDataEncryptionOptions());
+    }
+
+    public EncryptionContext(TransparentDataEncryptionOptions tdeOptions)
+    {
+        this(tdeOptions, null, true);
+    }
+
+    @VisibleForTesting
+    public EncryptionContext(TransparentDataEncryptionOptions tdeOptions, byte[] iv, boolean init)
+    {
+        this.tdeOptions = tdeOptions;
+        compressor = LZ4Compressor.create(Collections.<String, String>emptyMap());
+        chunkLength = tdeOptions.chunk_length_kb * 1024;
+        this.iv = iv;
+
+        // always attempt to load the cipher factory, as we could be in the situation where the user has disabled encryption,
+        // but has existing commitlogs and sstables on disk that are still encrypted (and still need to be read)
+        CipherFactory factory = null;
+
+        if (tdeOptions.enabled && init)
+        {
+            try
+            {
+                factory = new CipherFactory(tdeOptions);
+            }
+            catch (Exception e)
+            {
+                throw new ConfigurationException("failed to load key provider for transparent data encryption", e);
+            }
+        }
+
+        cipherFactory = factory;
+    }
+
+    public ICompressor getCompressor()
+    {
+        return compressor;
+    }
+
+    public Cipher getEncryptor() throws IOException
+    {
+        return cipherFactory.getEncryptor(tdeOptions.cipher, tdeOptions.key_alias);
+    }
+
+    public Cipher getDecryptor() throws IOException
+    {
+        if (iv == null || iv.length == 0)
+            throw new IllegalStateException("no initialization vector (IV) found in this context");
+        return cipherFactory.getDecryptor(tdeOptions.cipher, tdeOptions.key_alias, iv);
+    }
+
+    public boolean isEnabled()
+    {
+        return tdeOptions.enabled;
+    }
+
+    public int getChunkLength()
+    {
+        return chunkLength;
+    }
+
+    public byte[] getIV()
+    {
+        return iv;
+    }
+
+    public TransparentDataEncryptionOptions getTransparentDataEncryptionOptions()
+    {
+        return tdeOptions;
+    }
+
+    public boolean equals(Object o)
+    {
+        return o instanceof EncryptionContext && equals((EncryptionContext) o);
+    }
+
+    public boolean equals(EncryptionContext other)
+    {
+        return Objects.equal(tdeOptions, other.tdeOptions)
+               && Objects.equal(compressor, other.compressor)
+               && Arrays.equals(iv, other.iv);
+    }
+
+    public Map<String, String> toHeaderParameters()
+    {
+        Map<String, String> map = new HashMap<>(3);
+        // add compression options, someday ...
+        if (tdeOptions.enabled)
+        {
+            map.put(ENCRYPTION_CIPHER, tdeOptions.cipher);
+            map.put(ENCRYPTION_KEY_ALIAS, tdeOptions.key_alias);
+
+            if (iv != null && iv.length > 0)
+                map.put(ENCRYPTION_IV, Hex.bytesToHex(iv));
+        }
+        return map;
+    }
+
+    /**
+     * If encryption headers are found in the {@code parameters},
+     * those headers are merged with the application-wide {@code encryptionContext}.
+     */
+    public static EncryptionContext createFromMap(Map<?, ?> parameters, EncryptionContext encryptionContext)
+    {
+        if (parameters == null || parameters.isEmpty())
+            return new EncryptionContext(new TransparentDataEncryptionOptions(false));
+
+        String keyAlias = (String)parameters.get(ENCRYPTION_KEY_ALIAS);
+        String cipher = (String)parameters.get(ENCRYPTION_CIPHER);
+        String ivString = (String)parameters.get(ENCRYPTION_IV);
+        if (keyAlias == null || cipher == null)
+            return new EncryptionContext(new TransparentDataEncryptionOptions(false));
+
+        TransparentDataEncryptionOptions tdeOptions = new TransparentDataEncryptionOptions(cipher, keyAlias, encryptionContext.getTransparentDataEncryptionOptions().key_provider);
+        byte[] iv = ivString != null ? Hex.hexToBytes(ivString) : null;
+        return new EncryptionContext(tdeOptions, iv, true);
+    }
+}
diff --git a/src/java/org/apache/cassandra/security/EncryptionUtils.java b/src/java/org/apache/cassandra/security/EncryptionUtils.java
new file mode 100644
index 0000000..855e2a9
--- /dev/null
+++ b/src/java/org/apache/cassandra/security/EncryptionUtils.java
@@ -0,0 +1,321 @@
+/*
+ * 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.security;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.ShortBufferException;
+
+import com.google.common.base.Preconditions;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.db.commitlog.EncryptedSegment;
+import org.apache.cassandra.io.compress.ICompressor;
+import org.apache.cassandra.io.util.ChannelProxy;
+import org.apache.cassandra.io.util.FileDataInput;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+/**
+ * Encryption and decryption functions specific to the commit log.
+ * See comments in {@link EncryptedSegment} for details on the binary format.
+ * The normal, and expected, invocation pattern is to compress then encrypt the data on the encryption pass,
+ * then decrypt and uncompress the data on the decrypt pass.
+ */
+public class EncryptionUtils
+{
+    public static final int COMPRESSED_BLOCK_HEADER_SIZE = 4;
+    public static final int ENCRYPTED_BLOCK_HEADER_SIZE = 8;
+
+    private static final FastThreadLocal<ByteBuffer> reusableBuffers = new FastThreadLocal<ByteBuffer>()
+    {
+        protected ByteBuffer initialValue()
+        {
+            return ByteBuffer.allocate(ENCRYPTED_BLOCK_HEADER_SIZE);
+        }
+    };
+
+    /**
+     * Compress the raw data, as well as manage sizing of the {@code outputBuffer}; if the buffer is not big enough,
+     * deallocate current, and allocate a large enough buffer.
+     * Write the two header lengths (plain text length, compressed length) to the beginning of the buffer as we want those
+     * values encapsulated in the encrypted block, as well.
+     *
+     * @return the byte buffer that was actaully written to; it may be the {@code outputBuffer} if it had enough capacity,
+     * or it may be a new, larger instance. Callers should capture the return buffer (if calling multiple times).
+     */
+    public static ByteBuffer compress(ByteBuffer inputBuffer, ByteBuffer outputBuffer, boolean allowBufferResize, ICompressor compressor) throws IOException
+    {
+        int inputLength = inputBuffer.remaining();
+        final int compressedLength = compressor.initialCompressedBufferLength(inputLength);
+        outputBuffer = ByteBufferUtil.ensureCapacity(outputBuffer, compressedLength + COMPRESSED_BLOCK_HEADER_SIZE, allowBufferResize);
+
+        outputBuffer.putInt(inputLength);
+        compressor.compress(inputBuffer, outputBuffer);
+        outputBuffer.flip();
+
+        return outputBuffer;
+    }
+
+    /**
+     * Encrypt the input data, and writes out to the same input buffer; if the buffer is not big enough,
+     * deallocate current, and allocate a large enough buffer.
+     * Writes the cipher text and headers out to the channel, as well.
+     *
+     * Note: channel is a parameter as we cannot write header info to the output buffer as we assume the input and output
+     * buffers can be the same buffer (and writing the headers to a shared buffer will corrupt any input data). Hence,
+     * we write out the headers directly to the channel, and then the cipher text (once encrypted).
+     */
+    public static ByteBuffer encryptAndWrite(ByteBuffer inputBuffer, WritableByteChannel channel, boolean allowBufferResize, Cipher cipher) throws IOException
+    {
+        final int plainTextLength = inputBuffer.remaining();
+        final int encryptLength = cipher.getOutputSize(plainTextLength);
+        ByteBuffer outputBuffer = inputBuffer.duplicate();
+        outputBuffer = ByteBufferUtil.ensureCapacity(outputBuffer, encryptLength, allowBufferResize);
+
+        // it's unfortunate that we need to allocate a small buffer here just for the headers, but if we reuse the input buffer
+        // for the output, then we would overwrite the first n bytes of the real data with the header data.
+        ByteBuffer intBuf = ByteBuffer.allocate(ENCRYPTED_BLOCK_HEADER_SIZE);
+        intBuf.putInt(0, encryptLength);
+        intBuf.putInt(4, plainTextLength);
+        channel.write(intBuf);
+
+        try
+        {
+            cipher.doFinal(inputBuffer, outputBuffer);
+        }
+        catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e)
+        {
+            throw new IOException("failed to encrypt commit log block", e);
+        }
+
+        outputBuffer.position(0).limit(encryptLength);
+        channel.write(outputBuffer);
+        outputBuffer.position(0).limit(encryptLength);
+
+        return outputBuffer;
+    }
+
+    @SuppressWarnings("resource")
+    public static ByteBuffer encrypt(ByteBuffer inputBuffer, ByteBuffer outputBuffer, boolean allowBufferResize, Cipher cipher) throws IOException
+    {
+        Preconditions.checkNotNull(outputBuffer, "output buffer may not be null");
+        return encryptAndWrite(inputBuffer, new ChannelAdapter(outputBuffer), allowBufferResize, cipher);
+    }
+
+    /**
+     * Decrypt the input data, as well as manage sizing of the {@code outputBuffer}; if the buffer is not big enough,
+     * deallocate current, and allocate a large enough buffer.
+     *
+     * @return the byte buffer that was actaully written to; it may be the {@code outputBuffer} if it had enough capacity,
+     * or it may be a new, larger instance. Callers should capture the return buffer (if calling multiple times).
+     */
+    public static ByteBuffer decrypt(ReadableByteChannel channel, ByteBuffer outputBuffer, boolean allowBufferResize, Cipher cipher) throws IOException
+    {
+        ByteBuffer metadataBuffer = reusableBuffers.get();
+        if (metadataBuffer.capacity() < ENCRYPTED_BLOCK_HEADER_SIZE)
+        {
+            metadataBuffer = ByteBufferUtil.ensureCapacity(metadataBuffer, ENCRYPTED_BLOCK_HEADER_SIZE, true);
+            reusableBuffers.set(metadataBuffer);
+        }
+
+        metadataBuffer.position(0).limit(ENCRYPTED_BLOCK_HEADER_SIZE);
+        channel.read(metadataBuffer);
+        if (metadataBuffer.remaining() < ENCRYPTED_BLOCK_HEADER_SIZE)
+            throw new IllegalStateException("could not read encrypted blocked metadata header");
+        int encryptedLength = metadataBuffer.getInt();
+        // this is the length of the compressed data
+        int plainTextLength = metadataBuffer.getInt();
+
+        outputBuffer = ByteBufferUtil.ensureCapacity(outputBuffer, Math.max(plainTextLength, encryptedLength), allowBufferResize);
+        outputBuffer.position(0).limit(encryptedLength);
+        channel.read(outputBuffer);
+
+        ByteBuffer dupe = outputBuffer.duplicate();
+        dupe.clear();
+
+        try
+        {
+            cipher.doFinal(outputBuffer, dupe);
+        }
+        catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e)
+        {
+            throw new IOException("failed to decrypt commit log block", e);
+        }
+
+        dupe.position(0).limit(plainTextLength);
+        return dupe;
+    }
+
+    // path used when decrypting commit log files
+    @SuppressWarnings("resource")
+    public static ByteBuffer decrypt(FileDataInput fileDataInput, ByteBuffer outputBuffer, boolean allowBufferResize, Cipher cipher) throws IOException
+    {
+        return decrypt(new DataInputReadChannel(fileDataInput), outputBuffer, allowBufferResize, cipher);
+    }
+
+    /**
+     * Uncompress the input data, as well as manage sizing of the {@code outputBuffer}; if the buffer is not big enough,
+     * deallocate current, and allocate a large enough buffer.
+     *
+     * @return the byte buffer that was actaully written to; it may be the {@code outputBuffer} if it had enough capacity,
+     * or it may be a new, larger instance. Callers should capture the return buffer (if calling multiple times).
+     */
+    public static ByteBuffer uncompress(ByteBuffer inputBuffer, ByteBuffer outputBuffer, boolean allowBufferResize, ICompressor compressor) throws IOException
+    {
+        int outputLength = inputBuffer.getInt();
+        outputBuffer = ByteBufferUtil.ensureCapacity(outputBuffer, outputLength, allowBufferResize);
+        compressor.uncompress(inputBuffer, outputBuffer);
+        outputBuffer.position(0).limit(outputLength);
+
+        return outputBuffer;
+    }
+
+    public static int uncompress(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset, ICompressor compressor) throws IOException
+    {
+        int outputLength = readInt(input, inputOffset);
+        inputOffset += 4;
+        inputLength -= 4;
+
+        if (output.length - outputOffset < outputLength)
+        {
+            String msg = String.format("buffer to uncompress into is not large enough; buf size = %d, buf offset = %d, target size = %s",
+                                       output.length, outputOffset, outputLength);
+            throw new IllegalStateException(msg);
+        }
+
+        return compressor.uncompress(input, inputOffset, inputLength, output, outputOffset);
+    }
+
+    private static int readInt(byte[] input, int inputOffset)
+    {
+        return  (input[inputOffset + 3] & 0xFF)
+                | ((input[inputOffset + 2] & 0xFF) << 8)
+                | ((input[inputOffset + 1] & 0xFF) << 16)
+                | ((input[inputOffset] & 0xFF) << 24);
+    }
+
+    /**
+     * A simple {@link java.nio.channels.Channel} adapter for ByteBuffers.
+     */
+    private static final class ChannelAdapter implements WritableByteChannel
+    {
+        private final ByteBuffer buffer;
+
+        private ChannelAdapter(ByteBuffer buffer)
+        {
+            this.buffer = buffer;
+        }
+
+        public int write(ByteBuffer src)
+        {
+            int count = src.remaining();
+            buffer.put(src);
+            return count;
+        }
+
+        public boolean isOpen()
+        {
+            return true;
+        }
+
+        public void close()
+        {
+            // nop
+        }
+    }
+
+    private static class DataInputReadChannel implements ReadableByteChannel
+    {
+        private final FileDataInput fileDataInput;
+
+        private DataInputReadChannel(FileDataInput dataInput)
+        {
+            this.fileDataInput = dataInput;
+        }
+
+        public int read(ByteBuffer dst) throws IOException
+        {
+            int readLength = dst.remaining();
+            // we should only be performing encrypt/decrypt operations with on-heap buffers, so calling BB.array() should be legit here
+            fileDataInput.readFully(dst.array(), dst.position(), readLength);
+            return readLength;
+        }
+
+        public boolean isOpen()
+        {
+            try
+            {
+                return fileDataInput.isEOF();
+            }
+            catch (IOException e)
+            {
+                return true;
+            }
+        }
+
+        public void close()
+        {
+            // nop
+        }
+    }
+
+    public static class ChannelProxyReadChannel implements ReadableByteChannel
+    {
+        private final ChannelProxy channelProxy;
+        private volatile long currentPosition;
+
+        public ChannelProxyReadChannel(ChannelProxy channelProxy, long currentPosition)
+        {
+            this.channelProxy = channelProxy;
+            this.currentPosition = currentPosition;
+        }
+
+        public int read(ByteBuffer dst) throws IOException
+        {
+            int bytesRead = channelProxy.read(dst, currentPosition);
+            dst.flip();
+            currentPosition += bytesRead;
+            return bytesRead;
+        }
+
+        public long getCurrentPosition()
+        {
+            return currentPosition;
+        }
+
+        public boolean isOpen()
+        {
+            return channelProxy.isCleanedUp();
+        }
+
+        public void close()
+        {
+            // nop
+        }
+
+        public void setPosition(long sourcePosition)
+        {
+            this.currentPosition = sourcePosition;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/security/JKSKeyProvider.java b/src/java/org/apache/cassandra/security/JKSKeyProvider.java
new file mode 100644
index 0000000..db7a2b9
--- /dev/null
+++ b/src/java/org/apache/cassandra/security/JKSKeyProvider.java
@@ -0,0 +1,83 @@
+/*
+ * 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.security;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.Key;
+import java.security.KeyStore;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.TransparentDataEncryptionOptions;
+
+/**
+ * A {@code KeyProvider} that retrieves keys from a java keystore.
+ */
+public class JKSKeyProvider implements KeyProvider
+{
+    private static final Logger logger = LoggerFactory.getLogger(JKSKeyProvider.class);
+    static final String PROP_KEYSTORE = "keystore";
+    static final String PROP_KEYSTORE_PW = "keystore_password";
+    static final String PROP_KEYSTORE_TYPE = "store_type";
+    static final String PROP_KEY_PW = "key_password";
+
+    private final KeyStore store;
+    private final boolean isJceks;
+    private final TransparentDataEncryptionOptions options;
+
+    public JKSKeyProvider(TransparentDataEncryptionOptions options)
+    {
+        this.options = options;
+        logger.info("initializing keystore from file {}", options.get(PROP_KEYSTORE));
+        try (FileInputStream inputStream = new FileInputStream(options.get(PROP_KEYSTORE)))
+        {
+            store = KeyStore.getInstance(options.get(PROP_KEYSTORE_TYPE));
+            store.load(inputStream, options.get(PROP_KEYSTORE_PW).toCharArray());
+            isJceks = store.getType().equalsIgnoreCase("jceks");
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException("couldn't load keystore", e);
+        }
+    }
+
+    public Key getSecretKey(String keyAlias) throws IOException
+    {
+        // there's a lovely behavior with jceks files that all aliases are lower-cased
+        if (isJceks)
+            keyAlias = keyAlias.toLowerCase();
+
+        Key key;
+        try
+        {
+            String password = options.get(PROP_KEY_PW);
+            if (password == null || password.isEmpty())
+                password = options.get(PROP_KEYSTORE_PW);
+            key = store.getKey(keyAlias, password.toCharArray());
+        }
+        catch (Exception e)
+        {
+            throw new IOException("unable to load key from keystore");
+        }
+        if (key == null)
+            throw new IOException(String.format("key %s was not found in keystore", keyAlias));
+        return key;
+    }
+}
diff --git a/src/java/org/apache/cassandra/security/KeyProvider.java b/src/java/org/apache/cassandra/security/KeyProvider.java
new file mode 100644
index 0000000..f380aed
--- /dev/null
+++ b/src/java/org/apache/cassandra/security/KeyProvider.java
@@ -0,0 +1,33 @@
+/*
+ * 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.security;
+
+import java.io.IOException;
+import java.security.Key;
+
+/**
+ * Customizable key retrieval mechanism. Implementations should expect that retrieved keys will be cached.
+ * Further, each key will be requested non-concurrently (that is, no stampeding herds for the same key), although
+ * unique keys may be requested concurrently (unless you mark {@code getSecretKey} synchronized).
+ *
+ * Implementations must provide a constructor that accepts {@code TransparentDataEncryptionOptions} as the sole parameter.
+ */
+public interface KeyProvider
+{
+    Key getSecretKey(String alias) throws IOException;
+}
diff --git a/src/java/org/apache/cassandra/security/SSLFactory.java b/src/java/org/apache/cassandra/security/SSLFactory.java
index 56a3a3f..7216e2c 100644
--- a/src/java/org/apache/cassandra/security/SSLFactory.java
+++ b/src/java/org/apache/cassandra/security/SSLFactory.java
@@ -31,6 +31,7 @@
 
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
 import javax.net.ssl.SSLServerSocket;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.TrustManager;
@@ -53,20 +54,16 @@
 public final class SSLFactory
 {
     private static final Logger logger = LoggerFactory.getLogger(SSLFactory.class);
-    public static final String[] ACCEPTED_PROTOCOLS = new String[] {"SSLv2Hello", "TLSv1", "TLSv1.1", "TLSv1.2"};
     private static boolean checkedExpiry = false;
 
     public static SSLServerSocket getServerSocket(EncryptionOptions options, InetAddress address, int port) throws IOException
     {
         SSLContext ctx = createSSLContext(options, true);
-        SSLServerSocket serverSocket = (SSLServerSocket) ctx.getServerSocketFactory().createServerSocket();
+        SSLServerSocket serverSocket = (SSLServerSocket)ctx.getServerSocketFactory().createServerSocket();
         try
         {
             serverSocket.setReuseAddress(true);
-            String[] suites = filterCipherSuites(serverSocket.getSupportedCipherSuites(), options.cipher_suites);
-            serverSocket.setEnabledCipherSuites(suites);
-            serverSocket.setNeedClientAuth(options.require_client_auth);
-            serverSocket.setEnabledProtocols(ACCEPTED_PROTOCOLS);
+            prepareSocket(serverSocket, options);
             serverSocket.bind(new InetSocketAddress(address, port), 500);
             return serverSocket;
         }
@@ -84,9 +81,7 @@
         SSLSocket socket = (SSLSocket) ctx.getSocketFactory().createSocket(address, port, localAddress, localPort);
         try
         {
-            String[] suites = filterCipherSuites(socket.getSupportedCipherSuites(), options.cipher_suites);
-            socket.setEnabledCipherSuites(suites);
-            socket.setEnabledProtocols(ACCEPTED_PROTOCOLS);
+            prepareSocket(socket, options);
             return socket;
         }
         catch (IllegalArgumentException e)
@@ -103,9 +98,7 @@
         SSLSocket socket = (SSLSocket) ctx.getSocketFactory().createSocket(address, port);
         try
         {
-            String[] suites = filterCipherSuites(socket.getSupportedCipherSuites(), options.cipher_suites);
-            socket.setEnabledCipherSuites(suites);
-            socket.setEnabledProtocols(ACCEPTED_PROTOCOLS);
+            prepareSocket(socket, options);
             return socket;
         }
         catch (IllegalArgumentException e)
@@ -122,9 +115,7 @@
         SSLSocket socket = (SSLSocket) ctx.getSocketFactory().createSocket();
         try
         {
-            String[] suites = filterCipherSuites(socket.getSupportedCipherSuites(), options.cipher_suites);
-            socket.setEnabledCipherSuites(suites);
-            socket.setEnabledProtocols(ACCEPTED_PROTOCOLS);
+            prepareSocket(socket, options);
             return socket;
         }
         catch (IllegalArgumentException e)
@@ -134,6 +125,33 @@
         }
     }
 
+    /** Sets relevant socket options specified in encryption settings */
+    private static void prepareSocket(SSLServerSocket serverSocket, EncryptionOptions options)
+    {
+        String[] suites = filterCipherSuites(serverSocket.getSupportedCipherSuites(), options.cipher_suites);
+        if(options.require_endpoint_verification)
+        {
+            SSLParameters sslParameters = serverSocket.getSSLParameters();
+            sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+            serverSocket.setSSLParameters(sslParameters);
+        }
+        serverSocket.setEnabledCipherSuites(suites);
+        serverSocket.setNeedClientAuth(options.require_client_auth);
+    }
+
+    /** Sets relevant socket options specified in encryption settings */
+    private static void prepareSocket(SSLSocket socket, EncryptionOptions options)
+    {
+        String[] suites = filterCipherSuites(socket.getSupportedCipherSuites(), options.cipher_suites);
+        if(options.require_endpoint_verification)
+        {
+            SSLParameters sslParameters = socket.getSSLParameters();
+            sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+            socket.setSSLParameters(sslParameters);
+        }
+        socket.setEnabledCipherSuites(suites);
+    }
+
     @SuppressWarnings("resource")
     public static SSLContext createSSLContext(EncryptionOptions options, boolean buildTruststore) throws IOException
     {
diff --git a/src/java/org/apache/cassandra/security/SecurityThreadGroup.java b/src/java/org/apache/cassandra/security/SecurityThreadGroup.java
new file mode 100644
index 0000000..c57a7b7
--- /dev/null
+++ b/src/java/org/apache/cassandra/security/SecurityThreadGroup.java
@@ -0,0 +1,53 @@
+/*
+ * 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.security;
+
+import java.util.Set;
+
+/**
+ * Used by {@link ThreadAwareSecurityManager} to determine whether access-control checks needs to be performed.
+ */
+public final class SecurityThreadGroup extends ThreadGroup
+{
+    private final Set<String> allowedPackages;
+    private final ThreadInitializer threadInitializer;
+
+    public SecurityThreadGroup(String name, Set<String> allowedPackages, ThreadInitializer threadInitializer)
+    {
+        super(name);
+        this.allowedPackages = allowedPackages;
+        this.threadInitializer = threadInitializer;
+    }
+
+    public void initializeThread()
+    {
+        threadInitializer.initializeThread();
+    }
+
+    public boolean isPackageAllowed(String pkg)
+    {
+        return allowedPackages == null || allowedPackages.contains(pkg);
+    }
+
+    @FunctionalInterface
+    public interface ThreadInitializer
+    {
+        void initializeThread();
+    }
+}
diff --git a/src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java b/src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java
new file mode 100644
index 0000000..3d72559
--- /dev/null
+++ b/src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java
@@ -0,0 +1,219 @@
+/*
+ * 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.security;
+
+import java.security.AccessControlException;
+import java.security.AllPermission;
+import java.security.CodeSource;
+import java.security.Permission;
+import java.security.PermissionCollection;
+import java.security.Permissions;
+import java.security.Policy;
+import java.security.ProtectionDomain;
+import java.util.Collections;
+import java.util.Enumeration;
+
+import io.netty.util.concurrent.FastThreadLocal;
+
+import org.apache.cassandra.utils.logging.LoggingSupportFactory;
+import org.apache.cassandra.config.DatabaseDescriptor;
+
+/**
+ * Custom {@link SecurityManager} and {@link Policy} implementation that only performs access checks
+ * if explicitly enabled.
+ * <p>
+ * This implementation gives no measurable performance penalty
+ * (see <a href="http://cstar.datastax.com/tests/id/1d461628-12ba-11e5-918f-42010af0688f">see cstar test</a>).
+ * This is better than the penalty of 1 to 3 percent using a standard {@code SecurityManager} with an <i>allow all</i> policy.
+ * </p>
+ */
+public final class ThreadAwareSecurityManager extends SecurityManager
+{
+    public static final PermissionCollection noPermissions = new PermissionCollection()
+    {
+        public void add(Permission permission)
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        public boolean implies(Permission permission)
+        {
+            return false;
+        }
+
+        public Enumeration<Permission> elements()
+        {
+            return Collections.emptyEnumeration();
+        }
+    };
+
+    private static final RuntimePermission CHECK_MEMBER_ACCESS_PERMISSION = new RuntimePermission("accessDeclaredMembers");
+    private static final RuntimePermission MODIFY_THREAD_PERMISSION = new RuntimePermission("modifyThread");
+    private static final RuntimePermission MODIFY_THREADGROUP_PERMISSION = new RuntimePermission("modifyThreadGroup");
+    private static final RuntimePermission SET_SECURITY_MANAGER_PERMISSION = new RuntimePermission("setSecurityManager");
+
+    private static volatile boolean installed;
+
+    public static void install()
+    {
+        if (installed)
+            return;
+        System.setSecurityManager(new ThreadAwareSecurityManager());
+        LoggingSupportFactory.getLoggingSupport().onStartup();
+        installed = true;
+    }
+
+    static
+    {
+        //
+        // Use own security policy to be easier (and faster) since the C* has no fine grained permissions.
+        // Either code has access to everything or code has access to nothing (UDFs).
+        // This also removes the burden to maintain and configure policy files for production, unit tests etc.
+        //
+        // Note: a permission is only granted, if there is no objector. This means that
+        // AccessController/AccessControlContext collect all applicable ProtectionDomains - only if none of these
+        // applicable ProtectionDomains denies access, the permission is granted.
+        // A ProtectionDomain can have its origin at an oridinary code-source or provided via a
+        // AccessController.doPrivileded() call.
+        //
+        Policy.setPolicy(new Policy()
+        {
+            public PermissionCollection getPermissions(CodeSource codesource)
+            {
+                // contract of getPermissions() methods is to return a _mutable_ PermissionCollection
+
+                Permissions perms = new Permissions();
+
+                if (codesource == null || codesource.getLocation() == null)
+                    return perms;
+
+                switch (codesource.getLocation().getProtocol())
+                {
+                    case "file":
+                        // All JARs and class files reside on the file system - we can safely
+                        // assume that these classes are "good".
+                        perms.add(new AllPermission());
+                        return perms;
+                }
+
+                return perms;
+            }
+
+            public PermissionCollection getPermissions(ProtectionDomain domain)
+            {
+                return getPermissions(domain.getCodeSource());
+            }
+
+            public boolean implies(ProtectionDomain domain, Permission permission)
+            {
+                CodeSource codesource = domain.getCodeSource();
+                if (codesource == null || codesource.getLocation() == null)
+                    return false;
+
+                switch (codesource.getLocation().getProtocol())
+                {
+                    case "file":
+                        // All JARs and class files reside on the file system - we can safely
+                        // assume that these classes are "good".
+                        return true;
+                }
+
+                return false;
+            }
+        });
+    }
+
+    private static final FastThreadLocal<Boolean> initializedThread = new FastThreadLocal<>();
+
+    private ThreadAwareSecurityManager()
+    {
+    }
+
+    public static boolean isSecuredThread()
+    {
+        ThreadGroup tg = Thread.currentThread().getThreadGroup();
+        if (!(tg instanceof SecurityThreadGroup))
+            return false;
+        Boolean threadInitialized = initializedThread.get();
+        if (threadInitialized == null)
+        {
+            initializedThread.set(false);
+            ((SecurityThreadGroup) tg).initializeThread();
+            initializedThread.set(true);
+            threadInitialized = true;
+        }
+        return threadInitialized;
+    }
+
+    public void checkAccess(Thread t)
+    {
+        // need to override since the default implementation only checks the permission if the current thread's
+        // in the root-thread-group
+
+        if (isSecuredThread())
+            throw new AccessControlException("access denied: " + MODIFY_THREAD_PERMISSION, MODIFY_THREAD_PERMISSION);
+        super.checkAccess(t);
+    }
+
+    public void checkAccess(ThreadGroup g)
+    {
+        // need to override since the default implementation only checks the permission if the current thread's
+        // in the root-thread-group
+
+        if (isSecuredThread())
+            throw new AccessControlException("access denied: " + MODIFY_THREADGROUP_PERMISSION, MODIFY_THREADGROUP_PERMISSION);
+        super.checkAccess(g);
+    }
+
+    public void checkPermission(Permission perm)
+    {
+        if (!DatabaseDescriptor.enableUserDefinedFunctionsThreads() && !DatabaseDescriptor.allowExtraInsecureUDFs() && SET_SECURITY_MANAGER_PERMISSION.equals(perm))
+            throw new AccessControlException("Access denied");
+
+        if (!isSecuredThread())
+            return;
+
+        // required by JavaDriver 2.2.0-rc3 and 3.0.0-a2 or newer
+        // code in com.datastax.driver.core.CodecUtils uses Guava stuff, which in turns requires this permission
+        if (CHECK_MEMBER_ACCESS_PERMISSION.equals(perm))
+            return;
+
+        super.checkPermission(perm);
+    }
+
+    public void checkPermission(Permission perm, Object context)
+    {
+        if (isSecuredThread())
+            super.checkPermission(perm, context);
+    }
+
+    public void checkPackageAccess(String pkg)
+    {
+        if (!isSecuredThread())
+            return;
+
+        if (!((SecurityThreadGroup) Thread.currentThread().getThreadGroup()).isPackageAllowed(pkg))
+        {
+            RuntimePermission perm = new RuntimePermission("accessClassInPackage." + pkg);
+            throw new AccessControlException("access denied: " + perm, perm);
+        }
+
+        super.checkPackageAccess(pkg);
+    }
+}
diff --git a/src/java/org/apache/cassandra/serializers/AsciiSerializer.java b/src/java/org/apache/cassandra/serializers/AsciiSerializer.java
index b013b23..e265cb2 100644
--- a/src/java/org/apache/cassandra/serializers/AsciiSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/AsciiSerializer.java
@@ -35,7 +35,7 @@
         for (int i = bytes.position(); i < bytes.limit(); i++)
         {
             byte b = bytes.get(i);
-            if (b < 0 || b > 127)
+            if (b < 0)
                 throw new MarshalException("Invalid byte for ascii: " + Byte.toString(b));
         }
     }
diff --git a/src/java/org/apache/cassandra/serializers/ByteSerializer.java b/src/java/org/apache/cassandra/serializers/ByteSerializer.java
index 8c736cb..9d34fbc 100644
--- a/src/java/org/apache/cassandra/serializers/ByteSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/ByteSerializer.java
@@ -28,7 +28,7 @@
 
     public Byte deserialize(ByteBuffer bytes)
     {
-        return bytes.remaining() == 0 ? null : bytes.get(bytes.position());
+        return bytes == null || bytes.remaining() == 0 ? null : bytes.get(bytes.position());
     }
 
     public ByteBuffer serialize(Byte value)
diff --git a/src/java/org/apache/cassandra/serializers/CollectionSerializer.java b/src/java/org/apache/cassandra/serializers/CollectionSerializer.java
index 3d6be67..95a0388 100644
--- a/src/java/org/apache/cassandra/serializers/CollectionSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/CollectionSerializer.java
@@ -22,7 +22,7 @@
 import java.util.Collection;
 import java.util.List;
 
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public abstract class CollectionSerializer<T> implements TypeSerializer<T>
@@ -30,14 +30,14 @@
     protected abstract List<ByteBuffer> serializeValues(T value);
     protected abstract int getElementCount(T value);
 
-    public abstract T deserializeForNativeProtocol(ByteBuffer buffer, int version);
-    public abstract void validateForNativeProtocol(ByteBuffer buffer, int version);
+    public abstract T deserializeForNativeProtocol(ByteBuffer buffer, ProtocolVersion version);
+    public abstract void validateForNativeProtocol(ByteBuffer buffer, ProtocolVersion version);
 
     public ByteBuffer serialize(T value)
     {
         List<ByteBuffer> values = serializeValues(value);
         // See deserialize() for why using the protocol v3 variant is the right thing to do.
-        return pack(values, getElementCount(value), Server.VERSION_3);
+        return pack(values, getElementCount(value), ProtocolVersion.V3);
     }
 
     public T deserialize(ByteBuffer bytes)
@@ -47,16 +47,16 @@
         //  1) when collections are frozen
         //  2) for internal calls.
         // In both case, using the protocol 3 version variant is the right thing to do.
-        return deserializeForNativeProtocol(bytes, Server.VERSION_3);
+        return deserializeForNativeProtocol(bytes, ProtocolVersion.V3);
     }
 
     public void validate(ByteBuffer bytes) throws MarshalException
     {
         // Same thing as above
-        validateForNativeProtocol(bytes, Server.VERSION_3);
+        validateForNativeProtocol(bytes, ProtocolVersion.V3);
     }
 
-    public static ByteBuffer pack(Collection<ByteBuffer> buffers, int elements, int version)
+    public static ByteBuffer pack(Collection<ByteBuffer> buffers, int elements, ProtocolVersion version)
     {
         int size = 0;
         for (ByteBuffer bb : buffers)
@@ -69,22 +69,22 @@
         return (ByteBuffer)result.flip();
     }
 
-    protected static void writeCollectionSize(ByteBuffer output, int elements, int version)
+    protected static void writeCollectionSize(ByteBuffer output, int elements, ProtocolVersion version)
     {
             output.putInt(elements);
     }
 
-    public static int readCollectionSize(ByteBuffer input, int version)
+    public static int readCollectionSize(ByteBuffer input, ProtocolVersion version)
     {
         return input.getInt();
     }
 
-    protected static int sizeOfCollectionSize(int elements, int version)
+    protected static int sizeOfCollectionSize(int elements, ProtocolVersion version)
     {
         return 4;
     }
 
-    public static void writeValue(ByteBuffer output, ByteBuffer value, int version)
+    public static void writeValue(ByteBuffer output, ByteBuffer value, ProtocolVersion version)
     {
         if (value == null)
         {
@@ -96,7 +96,7 @@
         output.put(value.duplicate());
     }
 
-    public static ByteBuffer readValue(ByteBuffer input, int version)
+    public static ByteBuffer readValue(ByteBuffer input, ProtocolVersion version)
     {
         int size = input.getInt();
         if (size < 0)
@@ -105,7 +105,7 @@
         return ByteBufferUtil.readBytes(input, size);
     }
 
-    public static int sizeOfValue(ByteBuffer value, int version)
+    public static int sizeOfValue(ByteBuffer value, ProtocolVersion version)
     {
         return value == null ? 4 : 4 + value.remaining();
     }
diff --git a/src/java/org/apache/cassandra/serializers/DurationSerializer.java b/src/java/org/apache/cassandra/serializers/DurationSerializer.java
new file mode 100644
index 0000000..c752c40
--- /dev/null
+++ b/src/java/org/apache/cassandra/serializers/DurationSerializer.java
@@ -0,0 +1,131 @@
+/*
+ * 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.serializers;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.cql3.Duration;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataOutputBufferFixed;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.vint.VIntCoding;
+
+public final class DurationSerializer implements TypeSerializer<Duration>
+{
+    public static final DurationSerializer instance = new DurationSerializer();
+
+    public ByteBuffer serialize(Duration duration)
+    {
+        if (duration == null)
+            return ByteBufferUtil.EMPTY_BYTE_BUFFER;
+
+        long months = duration.getMonths();
+        long days = duration.getDays();
+        long nanoseconds = duration.getNanoseconds();
+
+        int size = VIntCoding.computeVIntSize(months)
+                + VIntCoding.computeVIntSize(days)
+                + VIntCoding.computeVIntSize(nanoseconds);
+
+        try (DataOutputBufferFixed output = new DataOutputBufferFixed(size))
+        {
+            output.writeVInt(months);
+            output.writeVInt(days);
+            output.writeVInt(nanoseconds);
+            return output.buffer();
+        }
+        catch (IOException e)
+        {
+            // this should never happen with a DataOutputBufferFixed
+            throw new AssertionError("Unexpected error", e);
+        }
+    }
+
+    public Duration deserialize(ByteBuffer bytes)
+    {
+        if (bytes.remaining() == 0)
+            return null;
+
+        try (DataInputBuffer in = new DataInputBuffer(bytes, true))
+        {
+            int months = (int) in.readVInt();
+            int days = (int) in.readVInt();
+            long nanoseconds = in.readVInt();
+            return Duration.newInstance(months, days, nanoseconds);
+        }
+        catch (IOException e)
+        {
+            // this should never happen with a DataInputBuffer
+            throw new AssertionError("Unexpected error", e);
+        }
+    }
+
+    public void validate(ByteBuffer bytes) throws MarshalException
+    {
+        if (bytes.remaining() < 3)
+            throw new MarshalException(String.format("Expected at least 3 bytes for a duration (%d)", bytes.remaining()));
+
+        try (DataInputBuffer in = new DataInputBuffer(bytes, true))
+        {
+            long monthsAsLong = in.readVInt();
+            long daysAsLong = in.readVInt();
+            long nanoseconds = in.readVInt();
+
+            if (!canBeCastToInt(monthsAsLong))
+                throw new MarshalException(String.format("The duration months must be a 32 bits integer but was: %d",
+                                                         monthsAsLong));
+            if (!canBeCastToInt(daysAsLong))
+                throw new MarshalException(String.format("The duration days must be a 32 bits integer but was: %d",
+                                                         daysAsLong));
+            int months = (int) monthsAsLong;
+            int days = (int) daysAsLong;
+
+            if (!((months >= 0 && days >= 0 && nanoseconds >= 0) || (months <= 0 && days <=0 && nanoseconds <=0)))
+                throw new MarshalException(String.format("The duration months, days and nanoseconds must be all of the same sign (%d, %d, %d)",
+                                                         months, days, nanoseconds));
+        }
+        catch (IOException e)
+        {
+            // this should never happen with a DataInputBuffer
+            throw new AssertionError("Unexpected error", e);
+        }
+    }
+
+    /**
+     * Checks that the specified {@code long} can be cast to an {@code int} without information lost.
+     *
+     * @param l the {@code long} to check
+     * @return {@code true} if the specified {@code long} can be cast to an {@code int} without information lost,
+     * {@code false} otherwise.
+     */
+    private boolean canBeCastToInt(long l)
+    {
+        return ((int) l) == l;
+    }
+
+    public String toString(Duration duration)
+    {
+        return duration == null ? "" : duration.toString();
+    }
+
+    public Class<Duration> getType()
+    {
+        return Duration.class;
+    }
+}
diff --git a/src/java/org/apache/cassandra/serializers/ListSerializer.java b/src/java/org/apache/cassandra/serializers/ListSerializer.java
index 3fd0803..44c33a6 100644
--- a/src/java/org/apache/cassandra/serializers/ListSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/ListSerializer.java
@@ -18,7 +18,7 @@
 
 package org.apache.cassandra.serializers;
 
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
@@ -60,7 +60,7 @@
         return value.size();
     }
 
-    public void validateForNativeProtocol(ByteBuffer bytes, int version)
+    public void validateForNativeProtocol(ByteBuffer bytes, ProtocolVersion version)
     {
         try
         {
@@ -78,7 +78,7 @@
         }
     }
 
-    public List<T> deserializeForNativeProtocol(ByteBuffer bytes, int version)
+    public List<T> deserializeForNativeProtocol(ByteBuffer bytes, ProtocolVersion version)
     {
         try
         {
@@ -130,7 +130,7 @@
         try
         {
             ByteBuffer input = serializedList.duplicate();
-            int n = readCollectionSize(input, Server.VERSION_3);
+            int n = readCollectionSize(input, ProtocolVersion.V3);
             if (n <= index)
                 return null;
 
@@ -139,7 +139,7 @@
                 int length = input.getInt();
                 input.position(input.position() + length);
             }
-            return readValue(input, Server.VERSION_3);
+            return readValue(input, ProtocolVersion.V3);
         }
         catch (BufferUnderflowException e)
         {
diff --git a/src/java/org/apache/cassandra/serializers/MapSerializer.java b/src/java/org/apache/cassandra/serializers/MapSerializer.java
index fa8432a..232a1b0 100644
--- a/src/java/org/apache/cassandra/serializers/MapSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/MapSerializer.java
@@ -23,7 +23,7 @@
 import java.util.*;
 
 import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.Pair;
 
 public class MapSerializer<K, V> extends CollectionSerializer<Map<K, V>>
@@ -74,7 +74,7 @@
         return value.size();
     }
 
-    public void validateForNativeProtocol(ByteBuffer bytes, int version)
+    public void validateForNativeProtocol(ByteBuffer bytes, ProtocolVersion version)
     {
         try
         {
@@ -98,7 +98,7 @@
         }
     }
 
-    public Map<K, V> deserializeForNativeProtocol(ByteBuffer bytes, int version)
+    public Map<K, V> deserializeForNativeProtocol(ByteBuffer bytes, ProtocolVersion version)
     {
         try
         {
@@ -145,11 +145,11 @@
         try
         {
             ByteBuffer input = serializedMap.duplicate();
-            int n = readCollectionSize(input, Server.VERSION_3);
+            int n = readCollectionSize(input, ProtocolVersion.V3);
             for (int i = 0; i < n; i++)
             {
-                ByteBuffer kbb = readValue(input, Server.VERSION_3);
-                ByteBuffer vbb = readValue(input, Server.VERSION_3);
+                ByteBuffer kbb = readValue(input, ProtocolVersion.V3);
+                ByteBuffer vbb = readValue(input, ProtocolVersion.V3);
                 int comparison = keyType.compare(kbb, serializedKey);
                 if (comparison == 0)
                     return vbb;
diff --git a/src/java/org/apache/cassandra/serializers/SetSerializer.java b/src/java/org/apache/cassandra/serializers/SetSerializer.java
index 14fde3b..52286b0 100644
--- a/src/java/org/apache/cassandra/serializers/SetSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/SetSerializer.java
@@ -22,6 +22,8 @@
 import java.nio.ByteBuffer;
 import java.util.*;
 
+import org.apache.cassandra.transport.ProtocolVersion;
+
 public class SetSerializer<T> extends CollectionSerializer<Set<T>>
 {
     // interning instances
@@ -61,7 +63,7 @@
         return value.size();
     }
 
-    public void validateForNativeProtocol(ByteBuffer bytes, int version)
+    public void validateForNativeProtocol(ByteBuffer bytes, ProtocolVersion version)
     {
         try
         {
@@ -82,7 +84,7 @@
         }
     }
 
-    public Set<T> deserializeForNativeProtocol(ByteBuffer bytes, int version)
+    public Set<T> deserializeForNativeProtocol(ByteBuffer bytes, ProtocolVersion version)
     {
         try
         {
diff --git a/src/java/org/apache/cassandra/serializers/TimestampSerializer.java b/src/java/org/apache/cassandra/serializers/TimestampSerializer.java
index 9bd9a8d..ac75d4b 100644
--- a/src/java/org/apache/cassandra/serializers/TimestampSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/TimestampSerializer.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.serializers;
 
+import io.netty.util.concurrent.FastThreadLocal;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 import java.nio.ByteBuffer;
@@ -33,7 +34,8 @@
 
     //NOTE: This list is used below and if you change the order
     //      you need to update the default format and json formats in the code below.
-    private static final String[] dateStringPatterns = new String[] {
+    private static final String[] dateStringPatterns = new String[]
+    {
             "yyyy-MM-dd HH:mm",
             "yyyy-MM-dd HH:mm:ss",
             "yyyy-MM-dd HH:mm z",
@@ -89,7 +91,7 @@
     private static final String DEFAULT_FORMAT = dateStringPatterns[6];
     private static final Pattern timestampPattern = Pattern.compile("^-?\\d+$");
 
-    private static final ThreadLocal<SimpleDateFormat> FORMATTER = new ThreadLocal<SimpleDateFormat>()
+    private static final FastThreadLocal<SimpleDateFormat> FORMATTER = new FastThreadLocal<SimpleDateFormat>()
     {
         protected SimpleDateFormat initialValue()
         {
@@ -98,7 +100,7 @@
     };
 
     private static final String UTC_FORMAT = dateStringPatterns[40];
-    private static final ThreadLocal<SimpleDateFormat> FORMATTER_UTC = new ThreadLocal<SimpleDateFormat>()
+    private static final FastThreadLocal<SimpleDateFormat> FORMATTER_UTC = new FastThreadLocal<SimpleDateFormat>()
     {
         protected SimpleDateFormat initialValue()
         {
@@ -107,9 +109,9 @@
             return sdf;
         }
     };
-    
+
     private static final String TO_JSON_FORMAT = dateStringPatterns[19];
-    private static final ThreadLocal<SimpleDateFormat> FORMATTER_TO_JSON = new ThreadLocal<SimpleDateFormat>()
+    private static final FastThreadLocal<SimpleDateFormat> FORMATTER_TO_JSON = new FastThreadLocal<SimpleDateFormat>()
     {
         protected SimpleDateFormat initialValue()
         {
@@ -120,7 +122,7 @@
     };
 
 
-    
+
     public static final TimestampSerializer instance = new TimestampSerializer();
 
     public Date deserialize(ByteBuffer bytes)
@@ -161,8 +163,8 @@
             throw new MarshalException(String.format("Unable to coerce '%s' to a formatted date (long)", source), e1);
         }
     }
-    
-    public static SimpleDateFormat getJsonDateFormatter() 
+
+    public static SimpleDateFormat getJsonDateFormatter()
     {
     	return FORMATTER_TO_JSON.get();
     }
diff --git a/src/java/org/apache/cassandra/serializers/TupleSerializer.java b/src/java/org/apache/cassandra/serializers/TupleSerializer.java
index 7cf71c6..65e6654 100644
--- a/src/java/org/apache/cassandra/serializers/TupleSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/TupleSerializer.java
@@ -18,9 +18,7 @@
 package org.apache.cassandra.serializers;
 
 import java.nio.ByteBuffer;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 import org.apache.cassandra.utils.ByteBufferUtil;
 
@@ -43,7 +41,7 @@
             if (!input.hasRemaining())
                 return;
 
-            if (input.remaining() < 4)
+            if (input.remaining() < Integer.BYTES)
                 throw new MarshalException(String.format("Not enough bytes to read size of %dth component", i));
 
             int size = input.getInt();
diff --git a/src/java/org/apache/cassandra/serializers/TypeSerializer.java b/src/java/org/apache/cassandra/serializers/TypeSerializer.java
index e66c36d..bf197cb 100644
--- a/src/java/org/apache/cassandra/serializers/TypeSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/TypeSerializer.java
@@ -23,11 +23,17 @@
 public interface TypeSerializer<T>
 {
     public ByteBuffer serialize(T value);
+
+    /*
+     * Does not modify the position or limit of the buffer even temporarily.
+     */
     public T deserialize(ByteBuffer bytes);
 
     /*
      * Validate that the byte array is a valid sequence for the type this represents.
      * This guarantees deserialize() can be called without errors.
+     *
+     * Does not modify the position or limit of the buffer even temporarily
      */
     public void validate(ByteBuffer bytes) throws MarshalException;
 
diff --git a/src/java/org/apache/cassandra/serializers/UTF8Serializer.java b/src/java/org/apache/cassandra/serializers/UTF8Serializer.java
index 7c41b94..444c06c 100644
--- a/src/java/org/apache/cassandra/serializers/UTF8Serializer.java
+++ b/src/java/org/apache/cassandra/serializers/UTF8Serializer.java
@@ -37,7 +37,8 @@
 
     static class UTF8Validator
     {
-        enum State {
+        enum State
+        {
             START,
             TWO,
             TWO_80,
@@ -97,10 +98,8 @@
                             if (b == (byte)0xf0)
                                 // 0xf0, 0x90-0xbf, 0x80-0xbf, 0x80-0xbf
                                 state = State.FOUR_90bf;
-                            else if (b == (byte)0xf4)
-                                // 0xf4, 0x80-0xbf, 0x80-0xbf, 0x80-0xbf
-                                state = State.FOUR_80bf_3;
                             else
+                                // 0xf4, 0x80-0xbf, 0x80-0xbf, 0x80-0xbf
                                 // 0xf1-0xf3, 0x80-0xbf, 0x80-0xbf, 0x80-0xbf
                                 state = State.FOUR_80bf_3;
                             break;
diff --git a/src/java/org/apache/cassandra/serializers/UUIDSerializer.java b/src/java/org/apache/cassandra/serializers/UUIDSerializer.java
index f8e2582..4501f34 100644
--- a/src/java/org/apache/cassandra/serializers/UUIDSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/UUIDSerializer.java
@@ -34,7 +34,7 @@
 
     public ByteBuffer serialize(UUID value)
     {
-        return value == null ? ByteBufferUtil.EMPTY_BYTE_BUFFER : ByteBuffer.wrap(UUIDGen.decompose(value));
+        return value == null ? ByteBufferUtil.EMPTY_BYTE_BUFFER : UUIDGen.toByteBuffer(value);
     }
 
     public void validate(ByteBuffer bytes) throws MarshalException
diff --git a/src/java/org/apache/cassandra/service/AbstractReadExecutor.java b/src/java/org/apache/cassandra/service/AbstractReadExecutor.java
index 177fdb2..572ae6e 100644
--- a/src/java/org/apache/cassandra/service/AbstractReadExecutor.java
+++ b/src/java/org/apache/cassandra/service/AbstractReadExecutor.java
@@ -63,11 +63,11 @@
     protected final ReadCallback handler;
     protected final TraceState traceState;
 
-    AbstractReadExecutor(Keyspace keyspace, ReadCommand command, ConsistencyLevel consistencyLevel, List<InetAddress> targetReplicas)
+    AbstractReadExecutor(Keyspace keyspace, ReadCommand command, ConsistencyLevel consistencyLevel, List<InetAddress> targetReplicas, long queryStartNanoTime)
     {
         this.command = command;
         this.targetReplicas = targetReplicas;
-        this.handler = new ReadCallback(new DigestResolver(keyspace, command, consistencyLevel, targetReplicas.size()), consistencyLevel, command, targetReplicas);
+        this.handler = new ReadCallback(new DigestResolver(keyspace, command, consistencyLevel, targetReplicas.size()), consistencyLevel, command, targetReplicas, queryStartNanoTime);
         this.traceState = Tracing.instance.get();
 
         // Set the digest version (if we request some digests). This is the smallest version amongst all our target replicas since new nodes
@@ -148,7 +148,7 @@
     /**
      * @return an executor appropriate for the configured speculative read policy
      */
-    public static AbstractReadExecutor getReadExecutor(SinglePartitionReadCommand command, ConsistencyLevel consistencyLevel) throws UnavailableException
+    public static AbstractReadExecutor getReadExecutor(SinglePartitionReadCommand command, ConsistencyLevel consistencyLevel, long queryStartNanoTime) throws UnavailableException
     {
         Keyspace keyspace = Keyspace.open(command.metadata().ksName);
         List<InetAddress> allReplicas = StorageProxy.getLiveSortedEndpoints(keyspace, command.partitionKey());
@@ -175,14 +175,14 @@
         if (retry.equals(SpeculativeRetryParam.NONE)
             || consistencyLevel == ConsistencyLevel.EACH_QUORUM
             || consistencyLevel.blockFor(keyspace) == allReplicas.size())
-            return new NeverSpeculatingReadExecutor(keyspace, command, consistencyLevel, targetReplicas);
+            return new NeverSpeculatingReadExecutor(keyspace, command, consistencyLevel, targetReplicas, queryStartNanoTime);
 
         if (targetReplicas.size() == allReplicas.size())
         {
             // CL.ALL, RRD.GLOBAL or RRD.DC_LOCAL and a single-DC.
             // We are going to contact every node anyway, so ask for 2 full data requests instead of 1, for redundancy
             // (same amount of requests in total, but we turn 1 digest request into a full blown data request).
-            return new AlwaysSpeculatingReadExecutor(keyspace, cfs, command, consistencyLevel, targetReplicas);
+            return new AlwaysSpeculatingReadExecutor(keyspace, cfs, command, consistencyLevel, targetReplicas, queryStartNanoTime);
         }
 
         // RRD.NONE or RRD.DC_LOCAL w/ multiple DCs.
@@ -203,16 +203,16 @@
         targetReplicas.add(extraReplica);
 
         if (retry.equals(SpeculativeRetryParam.ALWAYS))
-            return new AlwaysSpeculatingReadExecutor(keyspace, cfs, command, consistencyLevel, targetReplicas);
+            return new AlwaysSpeculatingReadExecutor(keyspace, cfs, command, consistencyLevel, targetReplicas, queryStartNanoTime);
         else // PERCENTILE or CUSTOM.
-            return new SpeculatingReadExecutor(keyspace, cfs, command, consistencyLevel, targetReplicas);
+            return new SpeculatingReadExecutor(keyspace, cfs, command, consistencyLevel, targetReplicas, queryStartNanoTime);
     }
 
     public static class NeverSpeculatingReadExecutor extends AbstractReadExecutor
     {
-        public NeverSpeculatingReadExecutor(Keyspace keyspace, ReadCommand command, ConsistencyLevel consistencyLevel, List<InetAddress> targetReplicas)
+        public NeverSpeculatingReadExecutor(Keyspace keyspace, ReadCommand command, ConsistencyLevel consistencyLevel, List<InetAddress> targetReplicas, long queryStartNanoTime)
         {
-            super(keyspace, command, consistencyLevel, targetReplicas);
+            super(keyspace, command, consistencyLevel, targetReplicas, queryStartNanoTime);
         }
 
         public void executeAsync()
@@ -242,9 +242,10 @@
                                        ColumnFamilyStore cfs,
                                        ReadCommand command,
                                        ConsistencyLevel consistencyLevel,
-                                       List<InetAddress> targetReplicas)
+                                       List<InetAddress> targetReplicas,
+                                       long queryStartNanoTime)
         {
-            super(keyspace, command, consistencyLevel, targetReplicas);
+            super(keyspace, command, consistencyLevel, targetReplicas, queryStartNanoTime);
             this.cfs = cfs;
         }
 
@@ -314,9 +315,10 @@
                                              ColumnFamilyStore cfs,
                                              ReadCommand command,
                                              ConsistencyLevel consistencyLevel,
-                                             List<InetAddress> targetReplicas)
+                                             List<InetAddress> targetReplicas,
+                                             long queryStartNanoTime)
         {
-            super(keyspace, command, consistencyLevel, targetReplicas);
+            super(keyspace, command, consistencyLevel, targetReplicas, queryStartNanoTime);
             this.cfs = cfs;
         }
 
diff --git a/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java b/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java
index bf099e4..dc58701 100644
--- a/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java
+++ b/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java
@@ -19,10 +19,13 @@
 
 import java.net.InetAddress;
 import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 
 import com.google.common.collect.Iterables;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -31,7 +34,6 @@
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.WriteType;
 import org.apache.cassandra.exceptions.*;
-import org.apache.cassandra.net.IAsyncCallback;
 import org.apache.cassandra.net.IAsyncCallbackWithFailure;
 import org.apache.cassandra.net.MessageIn;
 import org.apache.cassandra.utils.concurrent.SimpleCondition;
@@ -42,7 +44,6 @@
 
     private final SimpleCondition condition = new SimpleCondition();
     protected final Keyspace keyspace;
-    protected final long start;
     protected final Collection<InetAddress> naturalEndpoints;
     public final ConsistencyLevel consistencyLevel;
     protected final Runnable callback;
@@ -51,33 +52,35 @@
     private static final AtomicIntegerFieldUpdater<AbstractWriteResponseHandler> failuresUpdater
         = AtomicIntegerFieldUpdater.newUpdater(AbstractWriteResponseHandler.class, "failures");
     private volatile int failures = 0;
+    private final Map<InetAddress, RequestFailureReason> failureReasonByEndpoint;
+    private final long queryStartNanoTime;
+    private volatile boolean supportsBackPressure = true;
 
     /**
      * @param callback A callback to be called when the write is successful.
+     * @param queryStartNanoTime
      */
     protected AbstractWriteResponseHandler(Keyspace keyspace,
                                            Collection<InetAddress> naturalEndpoints,
                                            Collection<InetAddress> pendingEndpoints,
                                            ConsistencyLevel consistencyLevel,
                                            Runnable callback,
-                                           WriteType writeType)
+                                           WriteType writeType,
+                                           long queryStartNanoTime)
     {
         this.keyspace = keyspace;
         this.pendingEndpoints = pendingEndpoints;
-        this.start = System.nanoTime();
         this.consistencyLevel = consistencyLevel;
         this.naturalEndpoints = naturalEndpoints;
         this.callback = callback;
         this.writeType = writeType;
+        this.failureReasonByEndpoint = new ConcurrentHashMap<>();
+        this.queryStartNanoTime = queryStartNanoTime;
     }
 
     public void get() throws WriteTimeoutException, WriteFailureException
     {
-        long requestTimeout = writeType == WriteType.COUNTER
-                            ? DatabaseDescriptor.getCounterWriteRpcTimeout()
-                            : DatabaseDescriptor.getWriteRpcTimeout();
-
-        long timeout = TimeUnit.MILLISECONDS.toNanos(requestTimeout) - (System.nanoTime() - start);
+        long timeout = currentTimeout();
 
         boolean success;
         try
@@ -103,12 +106,20 @@
 
         if (totalBlockFor() + failures > totalEndpoints())
         {
-            throw new WriteFailureException(consistencyLevel, ackCount(), failures, totalBlockFor(), writeType);
+            throw new WriteFailureException(consistencyLevel, ackCount(), totalBlockFor(), writeType, failureReasonByEndpoint);
         }
     }
 
-    /** 
-     * @return the minimum number of endpoints that must reply. 
+    public final long currentTimeout()
+    {
+        long requestTimeout = writeType == WriteType.COUNTER
+                              ? DatabaseDescriptor.getCounterWriteRpcTimeout()
+                              : DatabaseDescriptor.getWriteRpcTimeout();
+        return TimeUnit.MILLISECONDS.toNanos(requestTimeout) - (System.nanoTime() - queryStartNanoTime);
+    }
+
+    /**
+     * @return the minimum number of endpoints that must reply.
      */
     protected int totalBlockFor()
     {
@@ -117,8 +128,8 @@
         return consistencyLevel.blockFor(keyspace) + pendingEndpoints.size();
     }
 
-    /** 
-     * @return the total number of endpoints the request has been sent to. 
+    /**
+     * @return the total number of endpoints the request has been sent to.
      */
     protected int totalEndpoints()
     {
@@ -157,7 +168,7 @@
     }
 
     @Override
-    public void onFailure(InetAddress from)
+    public void onFailure(InetAddress from, RequestFailureReason failureReason)
     {
         logger.trace("Got failure from {}", from);
 
@@ -165,7 +176,20 @@
               ? failuresUpdater.incrementAndGet(this)
               : failures;
 
+        failureReasonByEndpoint.put(from, failureReason);
+
         if (totalBlockFor() + n > totalEndpoints())
             signal();
     }
+
+    @Override
+    public boolean supportsBackPressure()
+    {
+        return supportsBackPressure;
+    }
+
+    public void setSupportsBackPressure(boolean supportsBackPressure)
+    {
+        this.supportsBackPressure = supportsBackPressure;
+    }
 }
diff --git a/src/java/org/apache/cassandra/service/ActiveRepairService.java b/src/java/org/apache/cassandra/service/ActiveRepairService.java
index f63cb86..626aa91 100644
--- a/src/java/org/apache/cassandra/service/ActiveRepairService.java
+++ b/src/java/org/apache/cassandra/service/ActiveRepairService.java
@@ -17,7 +17,6 @@
  */
 package org.apache.cassandra.service;
 
-import java.io.File;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -45,6 +44,7 @@
 import org.apache.cassandra.dht.Bounds;
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.exceptions.RequestFailureReason;
 import org.apache.cassandra.gms.ApplicationState;
 import org.apache.cassandra.gms.EndpointState;
 import org.apache.cassandra.gms.FailureDetector;
@@ -134,6 +134,7 @@
                                              RepairParallelism parallelismDegree,
                                              Set<InetAddress> endpoints,
                                              long repairedAt,
+                                             boolean pullRepair,
                                              ListeningExecutorService executor,
                                              String... cfnames)
     {
@@ -143,7 +144,7 @@
         if (cfnames.length == 0)
             return null;
 
-        final RepairSession session = new RepairSession(parentRepairSession, UUIDGen.getTimeUUID(), range, keyspace, parallelismDegree, endpoints, repairedAt, cfnames);
+        final RepairSession session = new RepairSession(parentRepairSession, UUIDGen.getTimeUUID(), range, keyspace, parallelismDegree, endpoints, repairedAt, pullRepair, cfnames);
 
         sessions.put(session.getId(), session);
         // register listeners
@@ -159,7 +160,7 @@
             {
                 sessions.remove(session.getId());
             }
-        }, MoreExecutors.sameThreadExecutor());
+        }, MoreExecutors.directExecutor());
         session.start(executor);
         return session;
     }
@@ -268,9 +269,10 @@
 
             if (specifiedHost.size() <= 1)
             {
-                String msg = "Repair requires at least two endpoints that are neighbours before it can continue, the endpoint used for this repair is %s, " +
-                             "other available neighbours are %s but these neighbours were not part of the supplied list of hosts to use during the repair (%s).";
-                throw new IllegalArgumentException(String.format(msg, specifiedHost, neighbors, hosts));
+                String msg = "Specified hosts %s do not share range %s needed for repair. Either restrict repair ranges " +
+                             "with -st/-et options, or specify one of the neighbors that share this range with " +
+                             "this node: %s.";
+                throw new IllegalArgumentException(String.format(msg, hosts, toRepair, neighbors));
             }
 
             specifiedHost.remove(FBUtilities.getBroadcastAddress());
@@ -300,7 +302,7 @@
                 return false;
             }
 
-            public void onFailure(InetAddress from)
+            public void onFailure(InetAddress from, RequestFailureReason failureReason)
             {
                 status.set(false);
                 failedNodes.add(from.getHostAddress());
@@ -422,10 +424,11 @@
      */
     public synchronized ParentRepairSession removeParentRepairSession(UUID parentSessionId)
     {
+        String snapshotName = parentSessionId.toString();
         for (ColumnFamilyStore cfs : getParentRepairSession(parentSessionId).columnFamilyStores.values())
         {
-            if (cfs.snapshotExists(parentSessionId.toString()))
-                cfs.clearSnapshot(parentSessionId.toString());
+            if (cfs.snapshotExists(snapshotName))
+                cfs.clearSnapshot(snapshotName);
         }
         return parentRepairSessions.remove(parentSessionId);
     }
@@ -472,7 +475,7 @@
             {
                 removeParentRepairSession(parentRepairSession);
             }
-        }, MoreExecutors.sameThreadExecutor());
+        }, MoreExecutors.directExecutor());
 
         return allAntiCompactionResults;
     }
@@ -644,7 +647,7 @@
                                !(sstable.metadata.isIndex()) && // exclude SSTables from 2i
                                new Bounds<>(sstable.first.getToken(), sstable.last.getToken()).intersects(ranges);
                     }
-                }, true);
+                }, true, false);
 
                 if (isAlreadyRepairing(cfId, parentSessionId, snapshottedSSTables))
                 {
diff --git a/src/java/org/apache/cassandra/service/BatchlogResponseHandler.java b/src/java/org/apache/cassandra/service/BatchlogResponseHandler.java
index 0eb26aa..241c6e3 100644
--- a/src/java/org/apache/cassandra/service/BatchlogResponseHandler.java
+++ b/src/java/org/apache/cassandra/service/BatchlogResponseHandler.java
@@ -21,9 +21,11 @@
 import java.net.InetAddress;
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 
+import org.apache.cassandra.exceptions.RequestFailureReason;
 import org.apache.cassandra.exceptions.WriteFailureException;
 import org.apache.cassandra.exceptions.WriteTimeoutException;
 import org.apache.cassandra.net.MessageIn;
+import org.apache.cassandra.net.MessagingService;
 
 public class BatchlogResponseHandler<T> extends AbstractWriteResponseHandler<T>
 {
@@ -33,9 +35,9 @@
     private static final AtomicIntegerFieldUpdater<BatchlogResponseHandler> requiredBeforeFinishUpdater
             = AtomicIntegerFieldUpdater.newUpdater(BatchlogResponseHandler.class, "requiredBeforeFinish");
 
-    public BatchlogResponseHandler(AbstractWriteResponseHandler<T> wrapped, int requiredBeforeFinish, BatchlogCleanup cleanup)
+    public BatchlogResponseHandler(AbstractWriteResponseHandler<T> wrapped, int requiredBeforeFinish, BatchlogCleanup cleanup, long queryStartNanoTime)
     {
-        super(wrapped.keyspace, wrapped.naturalEndpoints, wrapped.pendingEndpoints, wrapped.consistencyLevel, wrapped.callback, wrapped.writeType);
+        super(wrapped.keyspace, wrapped.naturalEndpoints, wrapped.pendingEndpoints, wrapped.consistencyLevel, wrapped.callback, wrapped.writeType, queryStartNanoTime);
         this.wrapped = wrapped;
         this.requiredBeforeFinish = requiredBeforeFinish;
         this.cleanup = cleanup;
@@ -58,9 +60,9 @@
         return wrapped.isLatencyForSnitch();
     }
 
-    public void onFailure(InetAddress from)
+    public void onFailure(InetAddress from, RequestFailureReason failureReason)
     {
-        wrapped.onFailure(from);
+        wrapped.onFailure(from, failureReason);
     }
 
     public void assureSufficientLiveNodes()
diff --git a/src/java/org/apache/cassandra/service/CacheService.java b/src/java/org/apache/cassandra/service/CacheService.java
index 873a319..434db54 100644
--- a/src/java/org/apache/cassandra/service/CacheService.java
+++ b/src/java/org/apache/cassandra/service/CacheService.java
@@ -39,10 +39,8 @@
 import org.apache.cassandra.cache.AutoSavingCache.CacheSerializer;
 import org.apache.cassandra.concurrent.Stage;
 import org.apache.cassandra.concurrent.StageManager;
-import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.filter.*;
@@ -56,7 +54,6 @@
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.MBeanWrapper;
 import org.apache.cassandra.utils.Pair;
-import org.apache.cassandra.utils.concurrent.OpOrder;
 
 public class CacheService implements CacheServiceMBean
 {
@@ -64,7 +61,7 @@
 
     public static final String MBEAN_NAME = "org.apache.cassandra.db:type=Caches";
 
-    public static enum CacheType
+    public enum CacheType
     {
         KEY_CACHE("KeyCache"),
         ROW_CACHE("RowCache"),
@@ -72,7 +69,7 @@
 
         private final String name;
 
-        private CacheType(String typeName)
+        CacheType(String typeName)
         {
             name = typeName;
         }
@@ -382,7 +379,8 @@
 
                     ClusteringIndexFilter filter = new ClusteringIndexNamesFilter(FBUtilities.<Clustering>singleton(name.clustering, cfs.metadata.comparator), false);
                     SinglePartitionReadCommand cmd = SinglePartitionReadCommand.create(cfs.metadata, nowInSec, key, builder.build(), filter);
-                    try (OpOrder.Group op = cfs.readOrdering.start(); RowIterator iter = UnfilteredRowIterators.filter(cmd.queryMemtableAndDisk(cfs, op), nowInSec))
+                    try (ReadExecutionController controller = cmd.executionController();
+                         RowIterator iter = UnfilteredRowIterators.filter(cmd.queryMemtableAndDisk(cfs, controller), nowInSec))
                     {
                         Cell cell;
                         if (column.isStatic())
@@ -421,9 +419,9 @@
             //Keyspace and CF name are deserialized by AutoSaving cache and used to fetch the CFS provided as a
             //parameter so they aren't deserialized here, even though they are serialized by this serializer
             final ByteBuffer buffer = ByteBufferUtil.readWithLength(in);
-            final int rowsToCache = cfs.metadata.params.caching.rowsPerPartitionToCache();
             if (cfs == null  || !cfs.isRowCacheEnabled())
                 return null;
+            final int rowsToCache = cfs.metadata.params.caching.rowsPerPartitionToCache();
             assert(!cfs.isIndex());//Shouldn't have row cache entries for indexes
 
             return StageManager.getStage(Stage.READ).submit(new Callable<Pair<RowCacheKey, IRowCacheEntry>>()
@@ -432,7 +430,8 @@
                 {
                     DecoratedKey key = cfs.decorateKey(buffer);
                     int nowInSec = FBUtilities.nowInSeconds();
-                    try (OpOrder.Group op = cfs.readOrdering.start(); UnfilteredRowIterator iter = SinglePartitionReadCommand.fullPartitionRead(cfs.metadata, nowInSec, key).queryMemtableAndDisk(cfs, op))
+                    SinglePartitionReadCommand cmd = SinglePartitionReadCommand.fullPartitionRead(cfs.metadata, nowInSec, key);
+                    try (ReadExecutionController controller = cmd.executionController(); UnfilteredRowIterator iter = cmd.queryMemtableAndDisk(cfs, controller))
                     {
                         CachedPartition toCache = CachedBTreePartition.create(DataLimits.cqlLimits(rowsToCache).filter(iter, nowInSec, true), nowInSec);
                         return Pair.create(new RowCacheKey(cfs.metadata.ksAndCFName, key), (IRowCacheEntry)toCache);
@@ -463,7 +462,9 @@
             ByteBufferUtil.writeWithLength(key.key, out);
             out.writeInt(key.desc.generation);
             out.writeBoolean(true);
-            key.desc.getFormat().getIndexSerializer(cfs.metadata, key.desc.version, SerializationHeader.forKeyCache(cfs.metadata)).serialize(entry, out);
+
+            SerializationHeader header = new SerializationHeader(false, cfs.metadata, cfs.metadata.partitionColumns(), EncodingStats.NO_STATS);
+            key.desc.getFormat().getIndexSerializer(cfs.metadata, key.desc.version, header).serializeForCache(entry, out);
         }
 
         public Future<Pair<KeyCacheKey, RowIndexEntry>> deserialize(DataInputPlus input, ColumnFamilyStore cfs) throws IOException
@@ -505,13 +506,13 @@
                 // wrong is during upgrade, in which case we fail at deserialization. This is not a huge deal however since 1) this is unlikely enough that
                 // this won't affect many users (if any) and only once, 2) this doesn't prevent the node from starting and 3) CASSANDRA-10219 shows that this
                 // part of the code has been broken for a while without anyone noticing (it is, btw, still broken until CASSANDRA-10219 is fixed).
-                RowIndexEntry.Serializer.skip(input, BigFormat.instance.getLatestVersion());
+                RowIndexEntry.Serializer.skipForCache(input, BigFormat.instance.getLatestVersion());
                 return null;
             }
             RowIndexEntry.IndexSerializer<?> indexSerializer = reader.descriptor.getFormat().getIndexSerializer(reader.metadata,
                                                                                                                 reader.descriptor.version,
-                                                                                                                SerializationHeader.forKeyCache(cfs.metadata));
-            RowIndexEntry entry = indexSerializer.deserialize(input);
+                                                                                                                reader.header);
+            RowIndexEntry<?> entry = indexSerializer.deserializeForCache(input);
             return Futures.immediateFuture(Pair.create(new KeyCacheKey(cfs.metadata.ksAndCFName, reader.descriptor, key), entry));
         }
 
diff --git a/src/java/org/apache/cassandra/service/CassandraDaemon.java b/src/java/org/apache/cassandra/service/CassandraDaemon.java
index 1df6161..a4edca7 100644
--- a/src/java/org/apache/cassandra/service/CassandraDaemon.java
+++ b/src/java/org/apache/cassandra/service/CassandraDaemon.java
@@ -21,34 +21,18 @@
 import java.io.IOException;
 import java.lang.management.ManagementFactory;
 import java.lang.management.MemoryPoolMXBean;
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.Proxy;
 import java.net.InetAddress;
+import java.net.URL;
 import java.net.UnknownHostException;
-import java.rmi.AccessException;
-import java.rmi.AlreadyBoundException;
-import java.rmi.NotBoundException;
-import java.rmi.Remote;
-import java.rmi.RemoteException;
-import java.rmi.registry.Registry;
-import java.rmi.server.RMIClientSocketFactory;
-import java.rmi.server.RMIServerSocketFactory;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import javax.management.ObjectName;
 import javax.management.StandardMBean;
 import javax.management.remote.JMXConnectorServer;
-import javax.management.remote.JMXServiceURL;
-import javax.management.remote.MBeanServerForwarder;
-import javax.management.remote.rmi.RMIConnectorServer;
-import javax.management.remote.rmi.RMIJRMPServerImpl;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.Uninterruptibles;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -65,7 +49,8 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
-import org.apache.cassandra.cql3.functions.ThreadAwareSecurityManager;
+import org.apache.cassandra.config.SchemaConstants;
+import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.SizeEstimatesRecorder;
@@ -78,19 +63,21 @@
 import org.apache.cassandra.hints.LegacyHintsMigrator;
 import org.apache.cassandra.io.FSError;
 import org.apache.cassandra.io.sstable.CorruptSSTableException;
+import org.apache.cassandra.io.sstable.SSTableHeaderFix;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.metrics.CassandraMetricsRegistry;
 import org.apache.cassandra.metrics.DefaultNameFactory;
 import org.apache.cassandra.metrics.StorageMetrics;
 import org.apache.cassandra.schema.LegacySchemaMigrator;
+import org.apache.cassandra.security.ThreadAwareSecurityManager;
 import org.apache.cassandra.thrift.ThriftServer;
 import org.apache.cassandra.tracing.Tracing;
 import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.JMXServerUtils;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.MBeanWrapper;
 import org.apache.cassandra.utils.Mx4jTool;
 import org.apache.cassandra.utils.NativeLibrary;
-import org.apache.cassandra.utils.RMIServerSocketFactoryImpl;
 import org.apache.cassandra.utils.WindowsTimer;
 
 /**
@@ -102,8 +89,6 @@
 public class CassandraDaemon
 {
     public static final String MBEAN_NAME = "org.apache.cassandra.db:type=NativeAccess";
-    private static JMXConnectorServer jmxServer = null;
-    private static MBeanServerForwarder authzProxy = null;
 
     private static final Logger logger;
 
@@ -134,37 +119,46 @@
 
     private void maybeInitJmx()
     {
+        // If the standard com.sun.management.jmxremote.port property has been set
+        // then the JVM agent will have already started up a default JMX connector
+        // server. This behaviour is deprecated, but some clients may be relying
+        // on it, so log a warning and skip setting up the server with the settings
+        // as configured in cassandra-env.(sh|ps1)
+        // See: CASSANDRA-11540 & CASSANDRA-11725
         if (System.getProperty("com.sun.management.jmxremote.port") != null)
+        {
+            logger.warn("JMX settings in cassandra-env.sh have been bypassed as the JMX connector server is " +
+                        "already initialized. Please refer to cassandra-env.(sh|ps1) for JMX configuration info");
             return;
+        }
 
-        String jmxPort = System.getProperty("cassandra.jmx.local.port");
+        System.setProperty("java.rmi.server.randomIDs", "true");
+
+        // If a remote port has been specified then use that to set up a JMX
+        // connector server which can be accessed remotely. Otherwise, look
+        // for the local port property and create a server which is bound
+        // only to the loopback address. Auth options are applied to both
+        // remote and local-only servers, but currently SSL is only
+        // available for remote.
+        // If neither is remote nor local port is set in cassandra-env.(sh|ps)
+        // then JMX is effectively  disabled.
+        boolean localOnly = false;
+        String jmxPort = System.getProperty("cassandra.jmx.remote.port");
+
+        if (jmxPort == null)
+        {
+            localOnly = true;
+            jmxPort = System.getProperty("cassandra.jmx.local.port");
+        }
+
         if (jmxPort == null)
             return;
 
-        MBeanServerForwarder authzProxy = createAuthzProxy();
-
-        System.setProperty("java.rmi.server.hostname", InetAddress.getLoopbackAddress().getHostAddress());
-        RMIServerSocketFactory serverFactory = new RMIServerSocketFactoryImpl();
-        Map<String, Object> env = new HashMap<>();
-        env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, serverFactory);
-        env.put("jmx.remote.rmi.server.credential.types",
-            new String[] { String[].class.getName(), String.class.getName() });
         try
         {
-            Registry registry = new JmxRegistry(Integer.valueOf(jmxPort), null, serverFactory, "jmxrmi");
-            JMXServiceURL url = new JMXServiceURL(String.format("service:jmx:rmi://localhost/jndi/rmi://localhost:%s/jmxrmi", jmxPort));
-            @SuppressWarnings("resource")
-            RMIJRMPServerImpl server = new RMIJRMPServerImpl(Integer.valueOf(jmxPort),
-                                                             null,
-                                                             (RMIServerSocketFactory) env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE),
-                                                             env);
-            jmxServer = new RMIConnectorServer(url, env, server, ManagementFactory.getPlatformMBeanServer());
-
-            if (authzProxy != null)
-                jmxServer.setMBeanServerForwarder(authzProxy);
-
-            jmxServer.start();
-            ((JmxRegistry)registry).setRemoteServerStub(server.toStub());
+            jmxServer = JMXServerUtils.createJMXServer(Integer.parseInt(jmxPort), localOnly);
+            if (jmxServer == null)
+                return;
         }
         catch (IOException e)
         {
@@ -172,38 +166,23 @@
         }
     }
 
-    private MBeanServerForwarder createAuthzProxy()
-    {
-        // If a custom authz proxy is supplied (Cassandra ships with AuthorizationProxy, which
-        // delegates to its own role based IAuthorizer), then instantiate and return one which
-        // can be set as the JMXConnectorServer's MBeanServerForwarder.
-        // If no custom proxy is supplied, check system properties for the location of the
-        // standard access file & stash it in env
-        String authzProxyClass = System.getProperty("cassandra.jmx.authorizer");
-        if (authzProxyClass == null)
-            return null;
-
-        final InvocationHandler handler = FBUtilities.construct(authzProxyClass, "JMX authz proxy");
-        final Class[] interfaces = { MBeanServerForwarder.class };
-
-        Object proxy = Proxy.newProxyInstance(MBeanServerForwarder.class.getClassLoader(), interfaces, handler);
-        return MBeanServerForwarder.class.cast(proxy);
-    }
-
-    private static final CassandraDaemon instance = new CassandraDaemon();
+    static final CassandraDaemon instance = new CassandraDaemon();
 
     private volatile Server thriftServer;
     private volatile NativeTransportService nativeTransportService;
+    private JMXConnectorServer jmxServer;
 
     private final boolean runManaged;
     protected final StartupChecks startupChecks;
     private boolean setupCompleted;
 
-    public CassandraDaemon() {
+    public CassandraDaemon()
+    {
         this(false);
     }
 
-    public CassandraDaemon(boolean runManaged) {
+    public CassandraDaemon(boolean runManaged)
+    {
         this.runManaged = runManaged;
         this.startupChecks = new StartupChecks().withDefaultTests();
         this.setupCompleted = false;
@@ -219,7 +198,7 @@
         FileUtils.setFSErrorHandler(new DefaultFSErrorHandler());
 
         // Delete any failed snapshot deletions on Windows - see CASSANDRA-9658
-        if (FBUtilities.isWindows())
+        if (FBUtilities.isWindows)
             WindowsFailedSnapshotTracker.deleteOldSnapshots();
 
         maybeInitJmx();
@@ -287,15 +266,26 @@
         // load schema from disk
         Schema.instance.loadFromDisk();
 
+        SSTableHeaderFix.fixNonFrozenUDTIfUpgradeFrom30();
+
         // clean up debris in the rest of the keyspaces
         for (String keyspaceName : Schema.instance.getKeyspaces())
         {
             // Skip system as we've already cleaned it
-            if (keyspaceName.equals(SystemKeyspace.NAME))
+            if (keyspaceName.equals(SchemaConstants.SYSTEM_KEYSPACE_NAME))
                 continue;
 
             for (CFMetaData cfm : Schema.instance.getTablesAndViews(keyspaceName))
-                ColumnFamilyStore.scrubDataDirectories(cfm);
+            {
+                try
+                {
+                    ColumnFamilyStore.scrubDataDirectories(cfm);
+                }
+                catch (StartupException e)
+                {
+                    exitOrFail(e.returnCode, e.getMessage(), e.getCause());
+                }
+            }
         }
 
         Keyspace.setInitialized();
@@ -305,7 +295,7 @@
         {
             if (logger.isDebugEnabled())
                 logger.debug("opening keyspace {}", keyspaceName);
-            // disable auto compaction until commit log replay ends
+            // disable auto compaction until gossip settles since disk boundaries may be affected by ring layout
             for (ColumnFamilyStore cfs : Keyspace.open(keyspaceName).getColumnFamilyStores())
             {
                 for (ColumnFamilyStore store : cfs.concatWithIndexes())
@@ -315,7 +305,6 @@
             }
         }
 
-
         try
         {
             loadRowAndKeyCacheAsync().get();
@@ -336,10 +325,10 @@
             logger.warn("Unable to start GCInspector (currently only supported on the Sun JVM)");
         }
 
-        // replay the log if necessary
+        // Replay any CommitLogSegments found on disk
         try
         {
-            CommitLog.instance.recover();
+            CommitLog.instance.recoverSegmentsOnDisk();
         }
         catch (IOException e)
         {
@@ -355,34 +344,6 @@
         // migrate any legacy (pre-3.0) batch entries from system.batchlog to system.batches (new table format)
         LegacyBatchlogMigrator.migrate();
 
-        // enable auto compaction
-        for (Keyspace keyspace : Keyspace.all())
-        {
-            for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores())
-            {
-                for (final ColumnFamilyStore store : cfs.concatWithIndexes())
-                {
-                    if (store.getCompactionStrategyManager().shouldBeEnabled())
-                        store.enableAutoCompaction();
-                }
-            }
-        }
-
-        Runnable viewRebuild = new Runnable()
-        {
-            @Override
-            public void run()
-            {
-                for (Keyspace keyspace : Keyspace.all())
-                {
-                    keyspace.viewManager.buildAllViews();
-                }
-                logger.debug("Completed submission of build tasks for any materialized views defined at startup");
-            }
-        };
-
-        ScheduledExecutors.optionalTasks.schedule(viewRebuild, StorageService.RING_DELAY, TimeUnit.MILLISECONDS);
-
         SystemKeyspace.finishStartup();
 
         // Clean up system.size_estimates entries left lying around from missed keyspace drops (CASSANDRA-14905)
@@ -394,17 +355,8 @@
         if (sizeRecorderInterval > 0)
             ScheduledExecutors.optionalTasks.scheduleWithFixedDelay(SizeEstimatesRecorder.instance, 30, sizeRecorderInterval, TimeUnit.SECONDS);
 
-        // start server internals
-        StorageService.instance.registerDaemon(this);
-        try
-        {
-            StorageService.instance.initServer();
-        }
-        catch (ConfigurationException e)
-        {
-            System.err.println(e.getMessage() + "\nFatal configuration error; unable to start server.  See log for stacktrace.");
-            exitOrFail(1, "Fatal configuration error", e);
-        }
+        // Prepared statements
+        QueryProcessor.instance.preloadPreparedStatements();
 
         // Metrics
         String metricsReporterConfigFile = System.getProperty("cassandra.metricsReporterConfigFile");
@@ -419,8 +371,16 @@
                 CassandraMetricsRegistry.Metrics.register("jvm.memory", new MemoryUsageGaugeSet());
                 CassandraMetricsRegistry.Metrics.register("jvm.fd.usage", new FileDescriptorRatioGauge());
                 // initialize metrics-reporter-config from yaml file
-                String reportFileLocation = CassandraDaemon.class.getClassLoader().getResource(metricsReporterConfigFile).getFile();
-                ReporterConfig.loadFromFile(reportFileLocation).enableAll(CassandraMetricsRegistry.Metrics);
+                URL resource = CassandraDaemon.class.getClassLoader().getResource(metricsReporterConfigFile);
+                if (resource == null)
+                {
+                    logger.warn("Failed to load metrics-reporter-config, file does not exist: {}", metricsReporterConfigFile);
+                }
+                else
+                {
+                    String reportFileLocation = resource.getFile();
+                    ReporterConfig.loadFromFile(reportFileLocation).enableAll(CassandraMetricsRegistry.Metrics);
+                }
             }
             catch (Exception e)
             {
@@ -428,8 +388,48 @@
             }
         }
 
+        // start server internals
+        StorageService.instance.registerDaemon(this);
+        try
+        {
+            StorageService.instance.initServer();
+        }
+        catch (ConfigurationException e)
+        {
+            System.err.println(e.getMessage() + "\nFatal configuration error; unable to start server.  See log for stacktrace.");
+            exitOrFail(1, "Fatal configuration error", e);
+        }
+
+        // Because we are writing to the system_distributed keyspace, this should happen after that is created, which
+        // happens in StorageService.instance.initServer()
+        Runnable viewRebuild = () -> {
+            for (Keyspace keyspace : Keyspace.all())
+            {
+                keyspace.viewManager.buildAllViews();
+            }
+            logger.debug("Completed submission of build tasks for any materialized views defined at startup");
+        };
+
+        ScheduledExecutors.optionalTasks.schedule(viewRebuild, StorageService.RING_DELAY, TimeUnit.MILLISECONDS);
+
         if (!FBUtilities.getBroadcastAddress().equals(InetAddress.getLoopbackAddress()))
-            waitForGossipToSettle();
+            Gossiper.waitToSettle();
+
+        // re-enable auto-compaction after gossip is settled, so correct disk boundaries are used
+        for (Keyspace keyspace : Keyspace.all())
+        {
+            for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores())
+            {
+                for (final ColumnFamilyStore store : cfs.concatWithIndexes())
+                {
+                    store.reload(); //reload CFs in case there was a change of disk boundaries
+                    if (store.getCompactionStrategyManager().shouldBeEnabled())
+                    {
+                        store.enableAutoCompaction();
+                    }
+                }
+            }
+        }
 
         // schedule periodic background compaction task submission. this is simply a backstop against compactions stalling
         // due to scheduling errors or race conditions
@@ -495,7 +495,9 @@
 	        }
 
 	        logger.info("JVM vendor/version: {}/{}", System.getProperty("java.vm.name"), System.getProperty("java.version"));
-	        logger.info("Heap size: {}/{}", Runtime.getRuntime().totalMemory(), Runtime.getRuntime().maxMemory());
+	        logger.info("Heap size: {}/{}",
+                        FBUtilities.prettyPrintMemory(Runtime.getRuntime().totalMemory()),
+                        FBUtilities.prettyPrintMemory(Runtime.getRuntime().maxMemory()));
 
 	        for(MemoryPoolMXBean pool: ManagementFactory.getMemoryPoolMXBeans())
 	            logger.info("{} {}: {}", pool.getName(), pool.getType(), pool.getPeakUsage());
@@ -537,7 +539,7 @@
         catch (IllegalStateException isx)
         {
             // If there are any errors, we just log and return in this case
-            logger.warn(isx.getMessage());
+            logger.info(isx.getMessage());
             return;
         }
 
@@ -577,7 +579,7 @@
 
         // On windows, we need to stop the entire system as prunsrv doesn't have the jsvc hooks
         // We rely on the shutdown hook to drain the node
-        if (FBUtilities.isWindows())
+        if (FBUtilities.isWindows)
             System.exit(0);
 
         if (jmxServer != null)
@@ -617,19 +619,11 @@
         // Do not put any references to DatabaseDescriptor above the forceStaticInitialization call.
         try
         {
-            try
-            {
-                DatabaseDescriptor.forceStaticInitialization();
-                DatabaseDescriptor.setDaemonInitialized();
-            }
-            catch (ExceptionInInitializerError e)
-            {
-                throw e.getCause();
-            }
+            applyConfig();
 
             MBeanWrapper.instance.registerMBean(new StandardMBean(new NativeAccess(), NativeAccessMBean.class), MBEAN_NAME, MBeanWrapper.OnException.LOG);
 
-            if (FBUtilities.isWindows())
+            if (FBUtilities.isWindows)
             {
                 // We need to adjust the system timer on windows from the default 15ms down to the minimum of 1ms as this
                 // impacts timer intervals, thread scheduling, driver interrupts, etc.
@@ -658,7 +652,7 @@
         catch (Throwable e)
         {
             boolean logStackTrace =
-                    e instanceof ConfigurationException ? ((ConfigurationException)e).logStackTrace : true;
+            e instanceof ConfigurationException ? ((ConfigurationException)e).logStackTrace : true;
 
             System.out.println("Exception (" + e.getClass().getName() + ") encountered during startup: " + e.getMessage());
 
@@ -681,6 +675,11 @@
         }
     }
 
+    public void applyConfig()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     public void validateTransportsCanStart()
     {
         // We only start transports if bootstrap has completed and we're not in survey mode, OR if we are in
@@ -771,57 +770,12 @@
         stop();
         destroy();
         // completely shut down cassandra
-        if(!runManaged) {
+        if(!runManaged)
+        {
             System.exit(0);
         }
     }
 
-    @VisibleForTesting
-    public static void waitForGossipToSettle()
-    {
-        int forceAfter = Integer.getInteger("cassandra.skip_wait_for_gossip_to_settle", -1);
-        if (forceAfter == 0)
-        {
-            return;
-        }
-        final int GOSSIP_SETTLE_MIN_WAIT_MS = 5000;
-        final int GOSSIP_SETTLE_POLL_INTERVAL_MS = 1000;
-        final int GOSSIP_SETTLE_POLL_SUCCESSES_REQUIRED = 3;
-
-        logger.info("Waiting for gossip to settle before accepting client requests...");
-        Uninterruptibles.sleepUninterruptibly(GOSSIP_SETTLE_MIN_WAIT_MS, TimeUnit.MILLISECONDS);
-        int totalPolls = 0;
-        int numOkay = 0;
-        int epSize = Gossiper.instance.getEndpointStates().size();
-        while (numOkay < GOSSIP_SETTLE_POLL_SUCCESSES_REQUIRED)
-        {
-            Uninterruptibles.sleepUninterruptibly(GOSSIP_SETTLE_POLL_INTERVAL_MS, TimeUnit.MILLISECONDS);
-            int currentSize = Gossiper.instance.getEndpointStates().size();
-            totalPolls++;
-            if (currentSize == epSize)
-            {
-                logger.debug("Gossip looks settled.");
-                numOkay++;
-            }
-            else
-            {
-                logger.info("Gossip not settled after {} polls.", totalPolls);
-                numOkay = 0;
-            }
-            epSize = currentSize;
-            if (forceAfter > 0 && totalPolls > forceAfter)
-            {
-                logger.warn("Gossip not settled but startup forced by cassandra.skip_wait_for_gossip_to_settle. Gossip total polls: {}",
-                            totalPolls);
-                break;
-            }
-        }
-        if (totalPolls > GOSSIP_SETTLE_POLL_SUCCESSES_REQUIRED)
-            logger.info("Gossip settled after {} extra polls; proceeding", totalPolls - GOSSIP_SETTLE_POLL_SUCCESSES_REQUIRED);
-        else
-            logger.info("No gossip backlog; proceeding");
-    }
-
     public static void stop(String[] args)
     {
         instance.deactivate();
@@ -832,21 +786,24 @@
         instance.activate();
     }
 
-    private void exitOrFail(int code, String message) {
+    private void exitOrFail(int code, String message)
+    {
         exitOrFail(code, message, null);
     }
 
-    private void exitOrFail(int code, String message, Throwable cause) {
-            if(runManaged) {
-                RuntimeException t = cause!=null ? new RuntimeException(message, cause) : new RuntimeException(message);
-                throw t;
-            }
-            else {
-                logger.error(message, cause);
-                System.exit(code);
-            }
-
+    private void exitOrFail(int code, String message, Throwable cause)
+    {
+        if (runManaged)
+        {
+            RuntimeException t = cause!=null ? new RuntimeException(message, cause) : new RuntimeException(message);
+            throw t;
         }
+        else
+        {
+            logger.error(message, cause);
+            System.exit(code);
+        }
+    }
 
     static class NativeAccess implements NativeAccessMBean
     {
@@ -882,48 +839,4 @@
          */
         public boolean isRunning();
     }
-
-
-    @SuppressWarnings("restriction")
-    private static class JmxRegistry extends sun.rmi.registry.RegistryImpl {
-        private final String lookupName;
-        private Remote remoteServerStub;
-
-        JmxRegistry(final int port,
-                    final RMIClientSocketFactory csf,
-                    RMIServerSocketFactory ssf,
-                    final String lookupName) throws RemoteException
-        {
-            super(port, csf, ssf);
-            this.lookupName = lookupName;
-        }
-
-        @Override
-        public Remote lookup(String s) throws RemoteException, NotBoundException
-        {
-            return lookupName.equals(s) ? remoteServerStub : null;
-        }
-
-        @Override
-        public void bind(String s, Remote remote) throws RemoteException, AlreadyBoundException, AccessException
-        {
-        }
-
-        @Override
-        public void unbind(String s) throws RemoteException, NotBoundException, AccessException {
-        }
-
-        @Override
-        public void rebind(String s, Remote remote) throws RemoteException, AccessException {
-        }
-
-        @Override
-        public String[] list() throws RemoteException {
-            return new String[] {lookupName};
-        }
-
-        public void setRemoteServerStub(Remote remoteServerStub) {
-            this.remoteServerStub = remoteServerStub;
-        }
-    }
 }
diff --git a/src/java/org/apache/cassandra/service/ClientState.java b/src/java/org/apache/cassandra/service/ClientState.java
index 1cbbfd4..52ab793 100644
--- a/src/java/org/apache/cassandra/service/ClientState.java
+++ b/src/java/org/apache/cassandra/service/ClientState.java
@@ -27,10 +27,19 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.auth.*;
-import org.apache.cassandra.config.Config;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.auth.CassandraAuthorizer;
+import org.apache.cassandra.auth.CassandraRoleManager;
+import org.apache.cassandra.auth.DataResource;
+import org.apache.cassandra.auth.FunctionResource;
+import org.apache.cassandra.auth.IResource;
+import org.apache.cassandra.auth.PasswordAuthenticator;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.auth.Resources;
+import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.QueryHandler;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.functions.Function;
@@ -39,8 +48,9 @@
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.exceptions.RequestExecutionException;
 import org.apache.cassandra.exceptions.UnauthorizedException;
-import org.apache.cassandra.schema.SchemaKeyspace;
+import org.apache.cassandra.schema.SchemaKeyspaceTables;
 import org.apache.cassandra.thrift.ThriftValidation;
+import org.apache.cassandra.utils.CassandraVersion;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.CassandraVersion;
@@ -62,21 +72,21 @@
         // We want these system cfs to be always readable to authenticated users since many tools rely on them
         // (nodetool, cqlsh, bulkloader, etc.)
         for (String cf : Arrays.asList(SystemKeyspace.LOCAL, SystemKeyspace.PEERS))
-            READABLE_SYSTEM_RESOURCES.add(DataResource.table(SystemKeyspace.NAME, cf));
+            READABLE_SYSTEM_RESOURCES.add(DataResource.table(SchemaConstants.SYSTEM_KEYSPACE_NAME, cf));
 
-        SchemaKeyspace.ALL.forEach(table -> READABLE_SYSTEM_RESOURCES.add(DataResource.table(SchemaKeyspace.NAME, table)));
+        SchemaKeyspaceTables.ALL.forEach(table -> READABLE_SYSTEM_RESOURCES.add(DataResource.table(SchemaConstants.SCHEMA_KEYSPACE_NAME, table)));
 
-        if (!Config.isClientMode())
+        // neither clients nor tools need authentication/authorization
+        if (DatabaseDescriptor.isDaemonInitialized())
         {
             PROTECTED_AUTH_RESOURCES.addAll(DatabaseDescriptor.getAuthenticator().protectedResources());
             PROTECTED_AUTH_RESOURCES.addAll(DatabaseDescriptor.getAuthorizer().protectedResources());
             PROTECTED_AUTH_RESOURCES.addAll(DatabaseDescriptor.getRoleManager().protectedResources());
         }
 
-        // allow users with sufficient privileges to drop legacy tables (users, credentials, permissions) from AUTH_KS
-        DROPPABLE_SYSTEM_AUTH_TABLES.add(DataResource.table(AuthKeyspace.NAME, PasswordAuthenticator.LEGACY_CREDENTIALS_TABLE));
-        DROPPABLE_SYSTEM_AUTH_TABLES.add(DataResource.table(AuthKeyspace.NAME, CassandraRoleManager.LEGACY_USERS_TABLE));
-        DROPPABLE_SYSTEM_AUTH_TABLES.add(DataResource.table(AuthKeyspace.NAME, CassandraAuthorizer.USER_PERMISSIONS));
+        DROPPABLE_SYSTEM_AUTH_TABLES.add(DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, PasswordAuthenticator.LEGACY_CREDENTIALS_TABLE));
+        DROPPABLE_SYSTEM_AUTH_TABLES.add(DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, CassandraRoleManager.LEGACY_USERS_TABLE));
+        DROPPABLE_SYSTEM_AUTH_TABLES.add(DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, CassandraAuthorizer.USER_PERMISSIONS));
     }
 
     // Current user for the session
@@ -316,6 +326,12 @@
         hasAccess(keyspace, perm, DataResource.table(keyspace, columnFamily));
     }
 
+    public void hasColumnFamilyAccess(CFMetaData cfm, Permission perm)
+    throws UnauthorizedException, InvalidRequestException
+    {
+        hasAccess(cfm.ksName, perm, cfm.resource);
+    }
+
     private void hasAccess(String keyspace, Permission perm, DataResource resource)
     throws UnauthorizedException, InvalidRequestException
     {
@@ -340,12 +356,12 @@
 
     public void ensureHasPermission(Permission perm, IResource resource) throws UnauthorizedException
     {
-        if (DatabaseDescriptor.getAuthorizer() instanceof AllowAllAuthorizer)
+        if (!DatabaseDescriptor.getAuthorizer().requireAuthorization())
             return;
 
         // Access to built in functions is unrestricted
         if(resource instanceof FunctionResource && resource.hasParent())
-            if (((FunctionResource)resource).getKeyspace().equals(SystemKeyspace.NAME))
+            if (((FunctionResource)resource).getKeyspace().equals(SchemaConstants.SYSTEM_KEYSPACE_NAME))
                 return;
 
         checkPermissionOnResourceChain(perm, resource);
@@ -356,7 +372,7 @@
     public void ensureHasPermission(Permission permission, Function function)
     {
         // Save creating a FunctionResource is we don't need to
-        if (DatabaseDescriptor.getAuthorizer() instanceof AllowAllAuthorizer)
+        if (!DatabaseDescriptor.getAuthorizer().requireAuthorization())
             return;
 
         // built in functions are always available to all
@@ -387,10 +403,10 @@
             return;
 
         // prevent ALL local system keyspace modification
-        if (Schema.isLocalSystemKeyspace(keyspace))
+        if (SchemaConstants.isLocalSystemKeyspace(keyspace))
             throw new UnauthorizedException(keyspace + " keyspace is not user-modifiable.");
 
-        if (Schema.isReplicatedSystemKeyspace(keyspace))
+        if (SchemaConstants.isReplicatedSystemKeyspace(keyspace))
         {
             // allow users with sufficient privileges to alter replication params of replicated system keyspaces
             if (perm == Permission.ALTER && resource.isKeyspaceLevel())
diff --git a/src/java/org/apache/cassandra/service/ClientWarn.java b/src/java/org/apache/cassandra/service/ClientWarn.java
index ddad197..5a6a878 100644
--- a/src/java/org/apache/cassandra/service/ClientWarn.java
+++ b/src/java/org/apache/cassandra/service/ClientWarn.java
@@ -20,24 +20,27 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import io.netty.util.concurrent.FastThreadLocal;
 import org.apache.cassandra.concurrent.ExecutorLocal;
 import org.apache.cassandra.utils.FBUtilities;
 
 public class ClientWarn implements ExecutorLocal<ClientWarn.State>
 {
     private static final String TRUNCATED = " [truncated]";
-    private static final ThreadLocal<ClientWarn.State> warnLocal = new ThreadLocal<>();
+    private static final FastThreadLocal<State> warnLocal = new FastThreadLocal<>();
     public static ClientWarn instance = new ClientWarn();
 
     private ClientWarn()
     {
     }
 
-    public State get() {
+    public State get()
+    {
         return warnLocal.get();
     }
 
-    public void set(State value) {
+    public void set(State value)
+    {
         warnLocal.set(value);
     }
 
diff --git a/src/java/org/apache/cassandra/service/DataResolver.java b/src/java/org/apache/cassandra/service/DataResolver.java
index cc17267..bab63f2 100644
--- a/src/java/org/apache/cassandra/service/DataResolver.java
+++ b/src/java/org/apache/cassandra/service/DataResolver.java
@@ -30,8 +30,8 @@
 import org.apache.cassandra.concurrent.StageManager;
 import org.apache.cassandra.config.*;
 import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.filter.ClusteringIndexFilter;
-import org.apache.cassandra.db.filter.DataLimits;
+import org.apache.cassandra.db.filter.*;
+import org.apache.cassandra.db.filter.DataLimits.Counter;
 import org.apache.cassandra.db.partitions.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.transform.*;
@@ -52,12 +52,13 @@
 
     @VisibleForTesting
     final List<AsyncOneResponse<?>> repairResults = Collections.synchronizedList(new ArrayList<>());
-
+    private final long queryStartNanoTime;
     private final boolean enforceStrictLiveness;
 
-    DataResolver(Keyspace keyspace, ReadCommand command, ConsistencyLevel consistency, int maxResponseCount)
+    DataResolver(Keyspace keyspace, ReadCommand command, ConsistencyLevel consistency, int maxResponseCount, long queryStartNanoTime)
     {
         super(keyspace, command, consistency, maxResponseCount);
+        this.queryStartNanoTime = queryStartNanoTime;
         this.enforceStrictLiveness = command.metadata().enforceStrictLiveness();
     }
 
@@ -195,6 +196,7 @@
         ReplicaFilteringProtection rfp = new ReplicaFilteringProtection(keyspace,
                                                                         command,
                                                                         consistency,
+                                                                        queryStartNanoTime,
                                                                         firstPhaseContext.sources,
                                                                         DatabaseDescriptor.getCachedReplicaRowsWarnThreshold(),
                                                                         DatabaseDescriptor.getCachedReplicaRowsFailThreshold());
@@ -327,7 +329,7 @@
             // For each source, the time of the current deletion as known by the source.
             private final DeletionTime[] sourceDeletionTime = new DeletionTime[sources.length];
             // For each source, record if there is an open range to send as repair, and from where.
-            private final Slice.Bound[] markerToRepair = new Slice.Bound[sources.length];
+            private final ClusteringBound[] markerToRepair = new ClusteringBound[sources.length];
 
             private MergeListener(DecoratedKey partitionKey, PartitionColumns columns, boolean isReversed)
             {
@@ -357,10 +359,22 @@
 
                     public void onCell(int i, Clustering clustering, Cell merged, Cell original)
                     {
-                        if (merged != null && !merged.equals(original))
+                        if (merged != null && !merged.equals(original) && isQueried(merged))
                             currentRow(i, clustering).addCell(merged);
                     }
 
+                    private boolean isQueried(Cell cell)
+                    {
+                        // When we read, we may have some cell that have been fetched but are not selected by the user. Those cells may
+                        // have empty values as optimization (see CASSANDRA-10655) and hence they should not be included in the read-repair.
+                        // This is fine since those columns are not actually requested by the user and are only present for the sake of CQL
+                        // semantic (making sure we can always distinguish between a row that doesn't exist from one that do exist but has
+                        /// no value for the column requested by the user) and so it won't be unexpected by the user that those columns are
+                        // not repaired.
+                        ColumnDefinition column = cell.column();
+                        ColumnFilter filter = command.columnFilter();
+                        return column.isComplex() ? filter.fetchedCellIsQueried(column, cell.path()) : filter.fetchedColumnIsQueried(column);
+                    }
                 };
             }
 
@@ -535,8 +549,8 @@
 
                         if (merged.isClose(isReversed))
                         {
-                            // We're closing the merged range. If we've marked the source as needing to be repaired for
-                            // that range, close and add it to the repair to be sent.
+                            // We're closing the merged range. If we're recorded that this should be repaird for the
+                            // source, close and add said range to the repair to send.
                             if (markerToRepair[i] != null)
                                 closeOpenMarker(i, merged.closeBound(isReversed));
 
@@ -559,9 +573,9 @@
                     mergedDeletionTime = merged.isOpen(isReversed) ? merged.openDeletionTime(isReversed) : null;
             }
 
-            private void closeOpenMarker(int i, Slice.Bound close)
+            private void closeOpenMarker(int i, ClusteringBound close)
             {
-                Slice.Bound open = markerToRepair[i];
+                ClusteringBound open = markerToRepair[i];
                 update(i).add(new RangeTombstone(Slice.make(isReversed ? close : open, isReversed ? open : close), currentDeletion()));
                 markerToRepair[i] = null;
             }
@@ -628,7 +642,8 @@
         ShortReadPartitionsProtection protection = new ShortReadPartitionsProtection(context.sources[i],
                                                                                      () -> responses.clearUnsafe(i),
                                                                                      singleResultCounter,
-                                                                                     context.mergedResultCounter);
+                                                                                     context.mergedResultCounter,
+                                                                                     queryStartNanoTime);
 
         /*
          * The order of extention and transformations is important here. Extending with more partitions has to happen
@@ -674,15 +689,19 @@
 
         private boolean partitionsFetched; // whether we've seen any new partitions since iteration start or last moreContents() call
 
+        private final long queryStartNanoTime;
+
         private ShortReadPartitionsProtection(InetAddress source,
                                               Runnable preFetchCallback,
                                               DataLimits.Counter singleResultCounter,
-                                              DataLimits.Counter mergedResultCounter)
+                                              DataLimits.Counter mergedResultCounter,
+                                              long queryStartNanoTime)
         {
             this.source = source;
             this.preFetchCallback = preFetchCallback;
             this.singleResultCounter = singleResultCounter;
             this.mergedResultCounter = mergedResultCounter;
+            this.queryStartNanoTime = queryStartNanoTime;
         }
 
         @Override
@@ -727,7 +746,7 @@
              * Can only take the short cut if there is no per partition limit set. Otherwise it's possible to hit false
              * positives due to some rows being uncounted for in certain scenarios (see CASSANDRA-13911).
              */
-            if (!singleResultCounter.isDone() && command.limits().perPartitionCount() == DataLimits.NO_LIMIT)
+            if (command.limits().isExhausted(singleResultCounter) && command.limits().perPartitionCount() == DataLimits.NO_LIMIT)
                 return null;
 
             /*
@@ -745,7 +764,7 @@
              * then future ShortReadRowsProtection.moreContents() calls will fetch the missing ones.
              */
             int toQuery = command.limits().count() != DataLimits.NO_LIMIT
-                        ? command.limits().count() - mergedResultCounter.counted()
+                        ? command.limits().count() - counted(mergedResultCounter)
                         : command.limits().perPartitionCount();
 
             ColumnFamilyStore.metricsFor(command.metadata().cfId).shortReadProtectionRequests.mark();
@@ -758,6 +777,15 @@
             return executeReadCommand(cmd);
         }
 
+        /** Returns the number of results counted by the counter */
+        private int counted(Counter counter)
+        {
+            // We are interested by the number of rows but for GROUP BY queries 'counted' returns the number of groups.
+            return command.limits().isGroupByLimit()
+                 ? counter.rowsCounted()
+                 : counter.counted();
+        }
+
         private PartitionRangeReadCommand makeFetchAdditionalPartitionReadCommand(int toQuery)
         {
             PartitionRangeReadCommand cmd = (PartitionRangeReadCommand) command;
@@ -816,7 +844,7 @@
                  * Can only take the short cut if there is no per partition limit set. Otherwise it's possible to hit false
                  * positives due to some rows being uncounted for in certain scenarios (see CASSANDRA-13911).
                  */
-                if (!singleResultCounter.isDoneForPartition() && command.limits().perPartitionCount() == DataLimits.NO_LIMIT)
+                if (command.limits().isExhausted(singleResultCounter) && command.limits().perPartitionCount() == DataLimits.NO_LIMIT)
                     return null;
 
                 /*
@@ -835,7 +863,7 @@
                  * rows. In that scenario the counter will remain at 0 until the partition is closed - which happens after
                  * the moreContents() call.
                  */
-                if (singleResultCounter.countedInCurrentPartition() == 0)
+                if (countedInCurrentPartition(singleResultCounter) == 0)
                     return null;
 
                 /*
@@ -845,8 +873,8 @@
                 if (Clustering.EMPTY == lastClustering)
                     return null;
 
-                lastFetched = singleResultCounter.countedInCurrentPartition() - lastCounted;
-                lastCounted = singleResultCounter.countedInCurrentPartition();
+                lastFetched = countedInCurrentPartition(singleResultCounter) - lastCounted;
+                lastCounted = countedInCurrentPartition(singleResultCounter);
 
                 // getting back fewer rows than we asked for means the partition on the replica has been fully consumed
                 if (lastQueried > 0 && lastFetched < lastQueried)
@@ -895,6 +923,16 @@
                 return UnfilteredPartitionIterators.getOnlyElement(executeReadCommand(cmd), cmd);
             }
 
+            /** Returns the number of results counted in the partition by the counter */
+            private int countedInCurrentPartition(Counter counter)
+            {
+                // We are interested by the number of rows but for GROUP BY queries 'countedInCurrentPartition' returns
+                // the number of groups in the current partition.
+                return command.limits().isGroupByLimit()
+                     ? counter.rowsCountedInCurrentPartition()
+                     : counter.countedInCurrentPartition();
+            }
+
             private SinglePartitionReadCommand makeFetchAdditionalRowsReadCommand(int toQuery)
             {
                 ClusteringIndexFilter filter = command.clusteringIndexFilter(partitionKey);
@@ -915,8 +953,8 @@
 
         private UnfilteredPartitionIterator executeReadCommand(ReadCommand cmd)
         {
-            DataResolver resolver = new DataResolver(keyspace, cmd, ConsistencyLevel.ONE, 1);
-            ReadCallback handler = new ReadCallback(resolver, ConsistencyLevel.ONE, cmd, Collections.singletonList(source));
+            DataResolver resolver = new DataResolver(keyspace, cmd, ConsistencyLevel.ONE, 1, queryStartNanoTime);
+            ReadCallback handler = new ReadCallback(resolver, ConsistencyLevel.ONE, cmd, Collections.singletonList(source), queryStartNanoTime);
 
             if (StorageProxy.canDoLocalRequest(source))
                 StageManager.getStage(Stage.READ).maybeExecuteImmediately(new StorageProxy.LocalReadRunnable(cmd, handler));
diff --git a/src/java/org/apache/cassandra/service/DatacenterSyncWriteResponseHandler.java b/src/java/org/apache/cassandra/service/DatacenterSyncWriteResponseHandler.java
index b095c7f..9584611 100644
--- a/src/java/org/apache/cassandra/service/DatacenterSyncWriteResponseHandler.java
+++ b/src/java/org/apache/cassandra/service/DatacenterSyncWriteResponseHandler.java
@@ -46,10 +46,11 @@
                                               ConsistencyLevel consistencyLevel,
                                               Keyspace keyspace,
                                               Runnable callback,
-                                              WriteType writeType)
+                                              WriteType writeType,
+                                              long queryStartNanoTime)
     {
         // Response is been managed by the map so make it 1 for the superclass.
-        super(keyspace, naturalEndpoints, pendingEndpoints, consistencyLevel, callback, writeType);
+        super(keyspace, naturalEndpoints, pendingEndpoints, consistencyLevel, callback, writeType, queryStartNanoTime);
         assert consistencyLevel == ConsistencyLevel.EACH_QUORUM;
 
         NetworkTopologyStrategy strategy = (NetworkTopologyStrategy) keyspace.getReplicationStrategy();
diff --git a/src/java/org/apache/cassandra/service/DatacenterWriteResponseHandler.java b/src/java/org/apache/cassandra/service/DatacenterWriteResponseHandler.java
index b1b7b10..2309e87 100644
--- a/src/java/org/apache/cassandra/service/DatacenterWriteResponseHandler.java
+++ b/src/java/org/apache/cassandra/service/DatacenterWriteResponseHandler.java
@@ -35,9 +35,10 @@
                                           ConsistencyLevel consistencyLevel,
                                           Keyspace keyspace,
                                           Runnable callback,
-                                          WriteType writeType)
+                                          WriteType writeType,
+                                          long queryStartNanoTime)
     {
-        super(naturalEndpoints, pendingEndpoints, consistencyLevel, keyspace, callback, writeType);
+        super(naturalEndpoints, pendingEndpoints, consistencyLevel, keyspace, callback, writeType, queryStartNanoTime);
         assert consistencyLevel.isDatacenterLocal();
     }
 
diff --git a/src/java/org/apache/cassandra/service/DefaultFSErrorHandler.java b/src/java/org/apache/cassandra/service/DefaultFSErrorHandler.java
index 2cf75d2..7e7539e 100644
--- a/src/java/org/apache/cassandra/service/DefaultFSErrorHandler.java
+++ b/src/java/org/apache/cassandra/service/DefaultFSErrorHandler.java
@@ -43,7 +43,7 @@
     @Override
     public void handleCorruptSSTable(CorruptSSTableException e)
     {
-        if (!StorageService.instance.isSetupCompleted())
+        if (!StorageService.instance.isDaemonSetupCompleted())
             handleStartupFSError(e);
 
         switch (DatabaseDescriptor.getDiskFailurePolicy())
@@ -60,7 +60,7 @@
     @Override
     public void handleFSError(FSError e)
     {
-        if (!StorageService.instance.isSetupCompleted())
+        if (!StorageService.instance.isDaemonSetupCompleted())
             handleStartupFSError(e);
 
         switch (DatabaseDescriptor.getDiskFailurePolicy())
diff --git a/src/java/org/apache/cassandra/service/EchoVerbHandler.java b/src/java/org/apache/cassandra/service/EchoVerbHandler.java
index 16a5a70..5410837 100644
--- a/src/java/org/apache/cassandra/service/EchoVerbHandler.java
+++ b/src/java/org/apache/cassandra/service/EchoVerbHandler.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.service;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 import org.apache.cassandra.gms.EchoMessage;
diff --git a/src/java/org/apache/cassandra/service/EmbeddedCassandraService.java b/src/java/org/apache/cassandra/service/EmbeddedCassandraService.java
index 6c154cd..2515259 100644
--- a/src/java/org/apache/cassandra/service/EmbeddedCassandraService.java
+++ b/src/java/org/apache/cassandra/service/EmbeddedCassandraService.java
@@ -19,9 +19,6 @@
 
 import java.io.IOException;
 
-import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.service.CassandraDaemon;
-
 /**
  * An embedded, in-memory cassandra storage service that listens
  * on the thrift interface as configured in cassandra.yaml
@@ -49,8 +46,8 @@
 
     public void start() throws IOException
     {
-        cassandraDaemon = new CassandraDaemon();
-        DatabaseDescriptor.setDaemonInitialized();
+        cassandraDaemon = CassandraDaemon.instance;
+        cassandraDaemon.applyConfig();
         cassandraDaemon.init(null);
         cassandraDaemon.start();
     }
diff --git a/src/java/org/apache/cassandra/service/GCInspector.java b/src/java/org/apache/cassandra/service/GCInspector.java
index 787d79a..0c0f9e4 100644
--- a/src/java/org/apache/cassandra/service/GCInspector.java
+++ b/src/java/org/apache/cassandra/service/GCInspector.java
@@ -137,18 +137,17 @@
 
     public GCInspector()
     {
-        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
-
         try
         {
             ObjectName gcName = new ObjectName(ManagementFactory.GARBAGE_COLLECTOR_MXBEAN_DOMAIN_TYPE + ",*");
-            for (ObjectName name : mbs.queryNames(gcName, null))
+            for (ObjectName name : MBeanWrapper.instance.queryNames(gcName, null))
             {
-                GarbageCollectorMXBean gc = ManagementFactory.newPlatformMXBeanProxy(mbs, name.getCanonicalName(), GarbageCollectorMXBean.class);
+                GarbageCollectorMXBean gc = ManagementFactory.newPlatformMXBeanProxy(MBeanWrapper.instance.getMBeanServer(), name.getCanonicalName(), GarbageCollectorMXBean.class);
                 gcStates.put(gc.getName(), new GCState(gc, assumeGCIsPartiallyConcurrent(gc), assumeGCIsOldGen(gc)));
             }
-
-            MBeanWrapper.instance.registerMBean(this, new ObjectName(MBEAN_NAME));
+            ObjectName me = new ObjectName(MBEAN_NAME);
+            if (!MBeanWrapper.instance.isRegistered(me))
+                MBeanWrapper.instance.registerMBean(this, me);
         }
         catch (Exception e)
         {
diff --git a/src/java/org/apache/cassandra/service/MigrationCoordinator.java b/src/java/org/apache/cassandra/service/MigrationCoordinator.java
index 83d60f2..b30e645 100644
--- a/src/java/org/apache/cassandra/service/MigrationCoordinator.java
+++ b/src/java/org/apache/cassandra/service/MigrationCoordinator.java
@@ -51,10 +51,12 @@
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.exceptions.RequestFailureReason;
 import org.apache.cassandra.gms.ApplicationState;
 import org.apache.cassandra.gms.EndpointState;
 import org.apache.cassandra.gms.FailureDetector;
 import org.apache.cassandra.gms.Gossiper;
+import org.apache.cassandra.gms.IEndpointStateChangeSubscriber;
 import org.apache.cassandra.gms.VersionedValue;
 import org.apache.cassandra.net.IAsyncCallbackWithFailure;
 import org.apache.cassandra.net.MessageIn;
@@ -254,12 +256,14 @@
             return false;
         }
 
-        if (Schema.instance.getVersion().equals(version))
+        if (Schema.instance.isSameVersion(version))
         {
             logger.debug("Not pulling schema for version {}, because schema versions match: " +
-                         "local={}, remote={}",
-                         version, Schema.instance.getVersion(),
-                         version);
+                         "local/real={}, local/compatible={}, remote={}",
+                         version,
+                         Schema.schemaVersionToString(Schema.instance.getRealVersion()),
+                         Schema.schemaVersionToString(Schema.instance.getAltVersion()),
+                         Schema.schemaVersionToString(version));
             return false;
         }
         return true;
@@ -319,10 +323,11 @@
         {
             // If we think we may be bootstrapping or have recently started, submit MigrationTask immediately
             logger.debug("Immediately submitting migration task for {}, " +
-                         "schema versions: local={}, remote={}",
+                         "schema versions: local/real={}, local/compatible={}, remote={}",
                          endpoint,
-                         Schema.instance.getVersion(),
-                         version);
+                         Schema.schemaVersionToString(Schema.instance.getRealVersion()),
+                         Schema.schemaVersionToString(Schema.instance.getAltVersion()),
+                         Schema.schemaVersionToString(version));
             return true;
         }
         return false;
@@ -331,7 +336,7 @@
     @VisibleForTesting
     protected boolean isLocalVersion(UUID version)
     {
-        return Schema.instance.getVersion().equals(version);
+        return Schema.instance.isSameVersion(version);
     }
 
     /**
@@ -374,12 +379,12 @@
         if (state == null)
             return FINISHED_FUTURE;
 
-        VersionedValue version = state.getApplicationState(ApplicationState.SCHEMA);
+        UUID version = state.getSchemaVersion();
 
         if (version == null)
             return FINISHED_FUTURE;
 
-        return reportEndpointVersion(endpoint, UUID.fromString(version.value));
+        return reportEndpointVersion(endpoint, version);
     }
 
     private synchronized void removeEndpointFromVersion(InetAddress endpoint, UUID version)
@@ -455,7 +460,7 @@
             this.info = info;
         }
 
-        public void onFailure(InetAddress from)
+        public void onFailure(InetAddress from, RequestFailureReason failureReason)
         {
             fail();
         }
@@ -478,6 +483,7 @@
                 {
                     try
                     {
+                        logger.debug("Pulled schema from endpoint {};", endpoint);
                         mergeSchemaFrom(endpoint, mutations);
                     }
                     catch (Exception e)
@@ -544,7 +550,7 @@
     public boolean awaitSchemaRequests(long waitMillis)
     {
         if (!FBUtilities.getBroadcastAddress().equals(InetAddress.getLoopbackAddress()))
-            CassandraDaemon.waitForGossipToSettle();
+            Gossiper.waitToSettle();
 
         WaitQueue.Signal signal = null;
         try
diff --git a/src/java/org/apache/cassandra/service/MigrationManager.java b/src/java/org/apache/cassandra/service/MigrationManager.java
index fba3a22..21daef2 100644
--- a/src/java/org/apache/cassandra/service/MigrationManager.java
+++ b/src/java/org/apache/cassandra/service/MigrationManager.java
@@ -202,7 +202,7 @@
         if (Schema.instance.getKSMetaData(ksm.name) != null)
             throw new AlreadyExistsException(ksm.name);
 
-        logger.info(String.format("Create new Keyspace: %s", ksm));
+        logger.info("Create new Keyspace: {}", ksm);
         announce(SchemaKeyspace.makeCreateKeyspaceMutation(ksm, timestamp), announceLocally);
     }
 
@@ -232,7 +232,7 @@
         else if (throwOnDuplicate && ksm.getTableOrViewNullable(cfm.cfName) != null)
             throw new AlreadyExistsException(cfm.ksName, cfm.cfName);
 
-        logger.info(String.format("Create new table: %s", cfm));
+        logger.info("Create new table: {}", cfm);
         announce(SchemaKeyspace.makeCreateTableMutation(ksm, cfm, timestamp), announceLocally);
     }
 
@@ -246,7 +246,7 @@
         else if (ksm.getTableOrViewNullable(view.viewName) != null)
             throw new AlreadyExistsException(view.ksName, view.viewName);
 
-        logger.info(String.format("Create new view: %s", view));
+        logger.info("Create new view: {}", view);
         announce(SchemaKeyspace.makeCreateViewMutation(ksm, view, FBUtilities.timestampMicros()), announceLocally);
     }
 
@@ -258,14 +258,14 @@
 
     public static void announceNewFunction(UDFunction udf, boolean announceLocally)
     {
-        logger.info(String.format("Create scalar function '%s'", udf.name()));
+        logger.info("Create scalar function '{}'", udf.name());
         KeyspaceMetadata ksm = Schema.instance.getKSMetaData(udf.name().keyspace);
         announce(SchemaKeyspace.makeCreateFunctionMutation(ksm, udf, FBUtilities.timestampMicros()), announceLocally);
     }
 
     public static void announceNewAggregate(UDAggregate udf, boolean announceLocally)
     {
-        logger.info(String.format("Create aggregate function '%s'", udf.name()));
+        logger.info("Create aggregate function '{}'", udf.name());
         KeyspaceMetadata ksm = Schema.instance.getKSMetaData(udf.name().keyspace);
         announce(SchemaKeyspace.makeCreateAggregateMutation(ksm, udf, FBUtilities.timestampMicros()), announceLocally);
     }
@@ -283,7 +283,7 @@
         if (oldKsm == null)
             throw new ConfigurationException(String.format("Cannot update non existing keyspace '%s'.", ksm.name));
 
-        logger.info(String.format("Update Keyspace '%s' From %s To %s", ksm.name, oldKsm, ksm));
+        logger.info("Update Keyspace '{}' From {} To {}", ksm.name, oldKsm, ksm);
         announce(SchemaKeyspace.makeCreateKeyspaceMutation(ksm.name, ksm.params, FBUtilities.timestampMicros()), announceLocally);
     }
 
@@ -310,25 +310,25 @@
 
         long timestamp = FBUtilities.timestampMicros();
 
-        logger.info(String.format("Update table '%s/%s' From %s To %s", cfm.ksName, cfm.cfName, oldCfm, cfm));
-        Mutation mutation = SchemaKeyspace.makeUpdateTableMutation(ksm, oldCfm, cfm, timestamp);
+        logger.info("Update table '{}/{}' From {} To {}", cfm.ksName, cfm.cfName, oldCfm, cfm);
+        Mutation.SimpleBuilder builder = SchemaKeyspace.makeUpdateTableMutation(ksm, oldCfm, cfm, timestamp);
 
         if (views != null)
-            views.forEach(view -> addViewUpdateToMutation(view, mutation, timestamp));
+            views.forEach(view -> addViewUpdateToMutationBuilder(view, builder));
 
-        announce(mutation, announceLocally);
+        announce(builder, announceLocally);
     }
 
     public static void announceViewUpdate(ViewDefinition view, boolean announceLocally) throws ConfigurationException
     {
         KeyspaceMetadata ksm = Schema.instance.getKSMetaData(view.ksName);
         long timestamp = FBUtilities.timestampMicros();
-        Mutation mutation = SchemaKeyspace.makeCreateKeyspaceMutation(ksm.name, ksm.params, timestamp);
-        addViewUpdateToMutation(view, mutation, timestamp);
-        announce(mutation, announceLocally);
+        Mutation.SimpleBuilder builder = SchemaKeyspace.makeCreateKeyspaceMutation(ksm.name, ksm.params, timestamp);
+        addViewUpdateToMutationBuilder(view, builder);
+        announce(builder, announceLocally);
     }
 
-    private static void addViewUpdateToMutation(ViewDefinition view, Mutation mutation, long timestamp)
+    private static void addViewUpdateToMutationBuilder(ViewDefinition view, Mutation.SimpleBuilder builder)
     {
         view.metadata.validate();
 
@@ -338,12 +338,13 @@
 
         oldView.metadata.validateCompatibility(view.metadata);
 
-        logger.info(String.format("Update view '%s/%s' From %s To %s", view.ksName, view.viewName, oldView, view));
-        SchemaKeyspace.makeUpdateViewMutation(mutation, oldView, view, timestamp);
+        logger.info("Update view '{}/{}' From {} To {}", view.ksName, view.viewName, oldView, view);
+        SchemaKeyspace.makeUpdateViewMutation(builder, oldView, view);
     }
 
     public static void announceTypeUpdate(UserType updatedType, boolean announceLocally)
     {
+        logger.info("Update type '{}.{}' to {}", updatedType.keyspace, updatedType.getNameAsString(), updatedType);
         announceNewType(updatedType, announceLocally);
     }
 
@@ -358,7 +359,7 @@
         if (oldKsm == null)
             throw new ConfigurationException(String.format("Cannot drop non existing keyspace '%s'.", ksName));
 
-        logger.info(String.format("Drop Keyspace '%s'", oldKsm.name));
+        logger.info("Drop Keyspace '{}'", oldKsm.name);
         announce(SchemaKeyspace.makeDropKeyspaceMutation(oldKsm, FBUtilities.timestampMicros()), announceLocally);
     }
 
@@ -374,7 +375,7 @@
             throw new ConfigurationException(String.format("Cannot drop non existing table '%s' in keyspace '%s'.", cfName, ksName));
         KeyspaceMetadata ksm = Schema.instance.getKSMetaData(ksName);
 
-        logger.info(String.format("Drop table '%s/%s'", oldCfm.ksName, oldCfm.cfName));
+        logger.info("Drop table '{}/{}'", oldCfm.ksName, oldCfm.cfName);
         announce(SchemaKeyspace.makeDropTableMutation(ksm, oldCfm, FBUtilities.timestampMicros()), announceLocally);
     }
 
@@ -385,7 +386,7 @@
             throw new ConfigurationException(String.format("Cannot drop non existing materialized view '%s' in keyspace '%s'.", viewName, ksName));
         KeyspaceMetadata ksm = Schema.instance.getKSMetaData(ksName);
 
-        logger.info(String.format("Drop table '%s/%s'", view.ksName, view.viewName));
+        logger.info("Drop table '{}/{}'", view.ksName, view.viewName);
         announce(SchemaKeyspace.makeDropViewMutation(ksm, view, FBUtilities.timestampMicros()), announceLocally);
     }
 
@@ -402,14 +403,14 @@
 
     public static void announceFunctionDrop(UDFunction udf, boolean announceLocally)
     {
-        logger.info(String.format("Drop scalar function overload '%s' args '%s'", udf.name(), udf.argTypes()));
+        logger.info("Drop scalar function overload '{}' args '{}'", udf.name(), udf.argTypes());
         KeyspaceMetadata ksm = Schema.instance.getKSMetaData(udf.name().keyspace);
         announce(SchemaKeyspace.makeDropFunctionMutation(ksm, udf, FBUtilities.timestampMicros()), announceLocally);
     }
 
     public static void announceAggregateDrop(UDAggregate udf, boolean announceLocally)
     {
-        logger.info(String.format("Drop aggregate function overload '%s' args '%s'", udf.name(), udf.argTypes()));
+        logger.info("Drop aggregate function overload '{}' args '{}'", udf.name(), udf.argTypes());
         KeyspaceMetadata ksm = Schema.instance.getKSMetaData(udf.name().keyspace);
         announce(SchemaKeyspace.makeDropAggregateMutation(ksm, udf, FBUtilities.timestampMicros()), announceLocally);
     }
@@ -418,21 +419,19 @@
      * actively announce a new version to active hosts via rpc
      * @param schema The schema mutation to be applied
      */
-    static void announce(Mutation schema, boolean announceLocally)
+    private static void announce(Mutation.SimpleBuilder schema, boolean announceLocally)
     {
-        announce(Collections.singletonList(schema), announceLocally);
-    }
+        List<Mutation> mutations = Collections.singletonList(schema.build());
 
-    static void announce(Collection<Mutation> schema, boolean announceLocally)
-    {
         if (announceLocally)
-            SchemaKeyspace.mergeSchema(schema);
+            SchemaKeyspace.mergeSchema(mutations);
         else
-            FBUtilities.waitOnFuture(announce(schema));
+            FBUtilities.waitOnFuture(announce(mutations));
     }
 
     private static void pushSchemaMutation(InetAddress endpoint, Collection<Mutation> schema)
     {
+        logger.debug("Pushing schema to endpoint {}", endpoint);
         MessageOut<Collection<Mutation>> msg = new MessageOut<>(MessagingService.Verb.DEFINITIONS_UPDATE,
                                                                 schema,
                                                                 MigrationsSerializer.instance);
@@ -472,11 +471,14 @@
      * Used to notify nodes as they arrive in the cluster.
      *
      * @param version The schema version to announce
+     * @param compatible flag whether {@code version} is a 3.0 compatible version
      */
-    public static void passiveAnnounce(UUID version)
+    public static void passiveAnnounce(UUID version, boolean compatible)
     {
         Gossiper.instance.addLocalApplicationState(ApplicationState.SCHEMA, StorageService.instance.valueFactory.schema(version));
-        logger.debug("Gossiping my schema version {}", version);
+        logger.debug("Gossiping my {} schema version {}",
+                     compatible ? "3.0 compatible" : "3.11",
+                     Schema.schemaVersionToString(version));
     }
 
     /**
@@ -528,7 +530,7 @@
      */
     static Optional<Mutation> evolveSystemKeyspace(KeyspaceMetadata keyspace, long generation)
     {
-        Mutation mutation = null;
+        Mutation.SimpleBuilder builder = null;
 
         KeyspaceMetadata definedKeyspace = Schema.instance.getKSMetaData(keyspace.name);
         Tables definedTables = null == definedKeyspace ? Tables.none() : definedKeyspace.tables;
@@ -538,20 +540,23 @@
             if (table.equals(definedTables.getNullable(table.cfName)))
                 continue;
 
-            if (null == mutation)
+            if (null == builder)
             {
                 // for the keyspace definition itself (name, replication, durability) always use generation 0;
                 // this ensures that any changes made to replication by the user will never be overwritten.
-                mutation = SchemaKeyspace.makeCreateKeyspaceMutation(keyspace.name, keyspace.params, 0);
+                builder = SchemaKeyspace.makeCreateKeyspaceMutation(keyspace.name, keyspace.params, 0);
+
+                // now set the timestamp to generation, so the tables have the expected timestamp
+                builder.timestamp(generation);
             }
 
             // for table definitions always use the provided generation; these tables, unlike their containing
             // keyspaces, are *NOT* meant to be altered by the user; if their definitions need to change,
             // the schema must be updated in code, and the appropriate generation must be bumped.
-            SchemaKeyspace.addTableToSchemaMutation(table, generation, true, mutation);
+            SchemaKeyspace.addTableToSchemaMutation(table, true, builder);
         }
 
-        return Optional.ofNullable(mutation);
+        return builder == null ? Optional.empty() : Optional.of(builder.build());
     }
 
     public static class MigrationsSerializer implements IVersionedSerializer<Collection<Mutation>>
diff --git a/src/java/org/apache/cassandra/service/NativeAccessMBean.java b/src/java/org/apache/cassandra/service/NativeAccessMBean.java
index 0128369..b0c408c 100644
--- a/src/java/org/apache/cassandra/service/NativeAccessMBean.java
+++ b/src/java/org/apache/cassandra/service/NativeAccessMBean.java
@@ -19,15 +19,7 @@
 
 public interface NativeAccessMBean 
 {
-    /**
-     * Checks if the native library has been successfully linked.
-     * @return {@code true} if the library has been successfully linked, {@code false} otherwise.
-     */
     boolean isAvailable();
 
-    /**
-     * Checks if the native library is able to lock memory.
-     * @return {@code true} if the native library is able to lock memory, {@code false} otherwise.
-     */
     boolean isMemoryLockable();
 }
diff --git a/src/java/org/apache/cassandra/service/NativeTransportService.java b/src/java/org/apache/cassandra/service/NativeTransportService.java
index 587f781..c58bb5e 100644
--- a/src/java/org/apache/cassandra/service/NativeTransportService.java
+++ b/src/java/org/apache/cassandra/service/NativeTransportService.java
@@ -31,7 +31,9 @@
 import io.netty.channel.epoll.Epoll;
 import io.netty.channel.epoll.EpollEventLoopGroup;
 import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.util.concurrent.EventExecutor;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.metrics.AuthMetrics;
 import org.apache.cassandra.metrics.ClientMetrics;
 import org.apache.cassandra.transport.ConfiguredLimit;
 import org.apache.cassandra.transport.Message;
@@ -108,6 +110,8 @@
         // register metrics
         ClientMetrics.instance.init(servers);
 
+        AuthMetrics.init();
+
         initialized = true;
     }
 
@@ -144,7 +148,7 @@
 
     public int getMaxProtocolVersion()
     {
-        return protocolVersionLimit.getMaxVersion();
+        return protocolVersionLimit.getMaxVersion().asInt();
     }
 
     public void refreshMaxNegotiableProtocolVersion()
@@ -161,7 +165,7 @@
      */
     public static boolean useEpoll()
     {
-        final boolean enableEpoll = Boolean.valueOf(System.getProperty("cassandra.native.epoll.enabled", "true"));
+        final boolean enableEpoll = Boolean.parseBoolean(System.getProperty("cassandra.native.epoll.enabled", "true"));
         return enableEpoll && Epoll.isAvailable();
     }
 
diff --git a/src/java/org/apache/cassandra/service/QueryState.java b/src/java/org/apache/cassandra/service/QueryState.java
index ddbc959..f0ae3b2 100644
--- a/src/java/org/apache/cassandra/service/QueryState.java
+++ b/src/java/org/apache/cassandra/service/QueryState.java
@@ -18,6 +18,8 @@
 package org.apache.cassandra.service;
 
 import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ThreadLocalRandom;
 
@@ -74,16 +76,16 @@
         this.preparedTracingSession = sessionId;
     }
 
-    public void createTracingSession()
+    public void createTracingSession(Map<String,ByteBuffer> customPayload)
     {
         UUID session = this.preparedTracingSession;
         if (session == null)
         {
-            Tracing.instance.newSession();
+            Tracing.instance.newSession(customPayload);
         }
         else
         {
-            Tracing.instance.newSession(session);
+            Tracing.instance.newSession(session, customPayload);
             this.preparedTracingSession = null;
         }
     }
diff --git a/src/java/org/apache/cassandra/service/ReadCallback.java b/src/java/org/apache/cassandra/service/ReadCallback.java
index 9a632cf..de546ba 100644
--- a/src/java/org/apache/cassandra/service/ReadCallback.java
+++ b/src/java/org/apache/cassandra/service/ReadCallback.java
@@ -20,6 +20,8 @@
 import java.net.InetAddress;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 
@@ -32,10 +34,8 @@
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.partitions.PartitionIterator;
-import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
-import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.exceptions.RequestFailureReason;
 import org.apache.cassandra.db.transform.DuplicateRowChecker;
-import org.apache.cassandra.db.transform.Transformation;
 import org.apache.cassandra.exceptions.ReadFailureException;
 import org.apache.cassandra.exceptions.ReadTimeoutException;
 import org.apache.cassandra.exceptions.UnavailableException;
@@ -56,7 +56,7 @@
 
     public final ResponseResolver resolver;
     private final SimpleCondition condition = new SimpleCondition();
-    private final long start;
+    private final long queryStartNanoTime;
     final int blockfor;
     final List<InetAddress> endpoints;
     private final ReadCommand command;
@@ -67,41 +67,44 @@
     private static final AtomicIntegerFieldUpdater<ReadCallback> failuresUpdater
             = AtomicIntegerFieldUpdater.newUpdater(ReadCallback.class, "failures");
     private volatile int failures = 0;
+    private final Map<InetAddress, RequestFailureReason> failureReasonByEndpoint;
 
     private final Keyspace keyspace; // TODO push this into ConsistencyLevel?
 
     /**
      * Constructor when response count has to be calculated and blocked for.
      */
-    public ReadCallback(ResponseResolver resolver, ConsistencyLevel consistencyLevel, ReadCommand command, List<InetAddress> filteredEndpoints)
+    public ReadCallback(ResponseResolver resolver, ConsistencyLevel consistencyLevel, ReadCommand command, List<InetAddress> filteredEndpoints, long queryStartNanoTime)
     {
         this(resolver,
              consistencyLevel,
              consistencyLevel.blockFor(Keyspace.open(command.metadata().ksName)),
              command,
              Keyspace.open(command.metadata().ksName),
-             filteredEndpoints);
+             filteredEndpoints,
+             queryStartNanoTime);
     }
 
-    public ReadCallback(ResponseResolver resolver, ConsistencyLevel consistencyLevel, int blockfor, ReadCommand command, Keyspace keyspace, List<InetAddress> endpoints)
+    public ReadCallback(ResponseResolver resolver, ConsistencyLevel consistencyLevel, int blockfor, ReadCommand command, Keyspace keyspace, List<InetAddress> endpoints, long queryStartNanoTime)
     {
         this.command = command;
         this.keyspace = keyspace;
         this.blockfor = blockfor;
         this.consistencyLevel = consistencyLevel;
         this.resolver = resolver;
-        this.start = System.nanoTime();
+        this.queryStartNanoTime = queryStartNanoTime;
         this.endpoints = endpoints;
+        this.failureReasonByEndpoint = new ConcurrentHashMap<>();
         // we don't support read repair (or rapid read protection) for range scans yet (CASSANDRA-6897)
         assert !(command instanceof PartitionRangeReadCommand) || blockfor >= endpoints.size();
 
         if (logger.isTraceEnabled())
-            logger.trace(String.format("Blockfor is %s; setting up requests to %s", blockfor, StringUtils.join(this.endpoints, ",")));
+            logger.trace("Blockfor is {}; setting up requests to {}", blockfor, StringUtils.join(this.endpoints, ","));
     }
 
     public boolean await(long timePastStart, TimeUnit unit)
     {
-        long time = unit.toNanos(timePastStart) - (System.nanoTime() - start);
+        long time = unit.toNanos(timePastStart) - (System.nanoTime() - queryStartNanoTime);
         try
         {
             return condition.await(time, TimeUnit.NANOSECONDS);
@@ -132,7 +135,7 @@
 
         // Same as for writes, see AbstractWriteResponseHandler
         throw failed
-            ? new ReadFailureException(consistencyLevel, received, failures, blockfor, resolver.isDataPresent())
+            ? new ReadFailureException(consistencyLevel, received, blockfor, resolver.isDataPresent(), failureReasonByEndpoint)
             : new ReadTimeoutException(consistencyLevel, received, blockfor, resolver.isDataPresent());
     }
 
@@ -143,7 +146,7 @@
 
         PartitionIterator result = blockfor == 1 ? resolver.getData() : resolver.resolve();
         if (logger.isTraceEnabled())
-            logger.trace("Read: {} ms.", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
+            logger.trace("Read: {} ms.", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - queryStartNanoTime));
         return DuplicateRowChecker.duringRead(result, endpoints);
     }
 
@@ -175,7 +178,7 @@
                 TraceState traceState = Tracing.instance.get();
                 if (traceState != null)
                     traceState.trace("Initiating read-repair");
-                StageManager.getStage(Stage.READ_REPAIR).execute(new AsyncRepairRunner(traceState));
+                StageManager.getStage(Stage.READ_REPAIR).execute(new AsyncRepairRunner(traceState, queryStartNanoTime));
             }
         }
     }
@@ -219,10 +222,12 @@
     private class AsyncRepairRunner implements Runnable
     {
         private final TraceState traceState;
+        private final long queryStartNanoTime;
 
-        public AsyncRepairRunner(TraceState traceState)
+        public AsyncRepairRunner(TraceState traceState, long queryStartNanoTime)
         {
             this.traceState = traceState;
+            this.queryStartNanoTime = queryStartNanoTime;
         }
 
         public void run()
@@ -242,8 +247,8 @@
                     traceState.trace("Digest mismatch: {}", e.toString());
 
                 ReadRepairMetrics.repairedBackground.mark();
-                
-                final DataResolver repairResolver = new DataResolver(keyspace, command, consistencyLevel, endpoints.size());
+
+                final DataResolver repairResolver = new DataResolver(keyspace, command, consistencyLevel, endpoints.size(), queryStartNanoTime);
                 AsyncRepairCallback repairHandler = new AsyncRepairCallback(repairResolver, endpoints.size());
 
                 for (InetAddress endpoint : endpoints)
@@ -256,12 +261,14 @@
     }
 
     @Override
-    public void onFailure(InetAddress from)
+    public void onFailure(InetAddress from, RequestFailureReason failureReason)
     {
         int n = waitingFor(from)
               ? failuresUpdater.incrementAndGet(this)
               : failures;
 
+        failureReasonByEndpoint.put(from, failureReason);
+
         if (blockfor + n > endpoints.size())
             condition.signalAll();
     }
diff --git a/src/java/org/apache/cassandra/service/ReplicaFilteringProtection.java b/src/java/org/apache/cassandra/service/ReplicaFilteringProtection.java
index 0a57e66..f764439 100644
--- a/src/java/org/apache/cassandra/service/ReplicaFilteringProtection.java
+++ b/src/java/org/apache/cassandra/service/ReplicaFilteringProtection.java
@@ -91,12 +91,13 @@
     private final Keyspace keyspace;
     private final ReadCommand command;
     private final ConsistencyLevel consistency;
+    private final long queryStartNanoTime;
     private final InetAddress[] sources;
     private final TableMetrics tableMetrics;
 
     private final int cachedRowsWarnThreshold;
     private final int cachedRowsFailThreshold;
-    
+
     /** Tracks whether or not we've already hit the warning threshold while evaluating a partition. */
     private boolean hitWarningThreshold = false;
 
@@ -111,6 +112,7 @@
     ReplicaFilteringProtection(Keyspace keyspace,
                                ReadCommand command,
                                ConsistencyLevel consistency,
+                               long queryStartNanoTime,
                                InetAddress[] sources,
                                int cachedRowsWarnThreshold,
                                int cachedRowsFailThreshold)
@@ -118,6 +120,7 @@
         this.keyspace = keyspace;
         this.command = command;
         this.consistency = consistency;
+        this.queryStartNanoTime = queryStartNanoTime;
         this.sources = sources;
         this.originalPartitions = new ArrayList<>(sources.length);
 
@@ -134,8 +137,8 @@
 
     private UnfilteredPartitionIterator executeReadCommand(ReadCommand cmd, InetAddress source)
     {
-        DataResolver resolver = new DataResolver(keyspace, cmd, ConsistencyLevel.ONE, 1);
-        ReadCallback handler = new ReadCallback(resolver, ConsistencyLevel.ONE, cmd, Collections.singletonList(source));
+        DataResolver resolver = new DataResolver(keyspace, cmd, ConsistencyLevel.ONE, 1, queryStartNanoTime);
+        ReadCallback handler = new ReadCallback(resolver, ConsistencyLevel.ONE, cmd, Collections.singletonList(source), queryStartNanoTime);
 
         if (StorageProxy.canDoLocalRequest(source))
             StageManager.getStage(Stage.READ).maybeExecuteImmediately(new StorageProxy.LocalReadRunnable(cmd, handler));
@@ -176,7 +179,7 @@
                 PartitionBuilder[] builders = new PartitionBuilder[sources.length];
                 PartitionColumns columns = columns(versions);
                 EncodingStats stats = EncodingStats.merge(versions, NULL_TO_NO_STATS);
-                
+
                 for (int i = 0; i < sources.length; i++)
                     builders[i] = new PartitionBuilder(partitionKey, sources[i], columns, stats);
 
@@ -243,7 +246,7 @@
     private void incrementCachedRows()
     {
         currentRowsCached++;
-        
+
         if (currentRowsCached == cachedRowsFailThreshold + 1)
         {
             String message = String.format("Replica filtering protection has cached over %d rows during query %s. " +
@@ -257,7 +260,7 @@
         else if (currentRowsCached == cachedRowsWarnThreshold + 1 && !hitWarningThreshold)
         {
             hitWarningThreshold = true;
-            
+
             String message = String.format("Replica filtering protection has cached over %d rows during query %s. " +
                                            "(See 'cached_replica_rows_warn_threshold' in cassandra.yaml.)",
                                            cachedRowsWarnThreshold, command.toCQLString());
@@ -329,7 +332,7 @@
                 {
                     PartitionIterators.consumeNext(merged);
                 }
-                
+
                 return !partitions.isEmpty();
             }
 
@@ -497,11 +500,11 @@
 
             NavigableSet<Clustering> clusterings = toFetch.build();
             tableMetrics.replicaFilteringProtectionRequests.mark();
-            
+
             if (logger.isTraceEnabled())
                 logger.trace("Requesting rows {} in partition {} from {} for replica filtering protection",
                              clusterings, key, source);
-            
+
             Tracing.trace("Requesting {} rows in partition {} from {} for replica filtering protection",
                           clusterings.size(), key, source);
 
diff --git a/src/java/org/apache/cassandra/service/StartupChecks.java b/src/java/org/apache/cassandra/service/StartupChecks.java
index d1982f7..cb10ab4 100644
--- a/src/java/org/apache/cassandra/service/StartupChecks.java
+++ b/src/java/org/apache/cassandra/service/StartupChecks.java
@@ -35,20 +35,24 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.auth.AuthKeyspace;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.UntypedResultSet;
-import org.apache.cassandra.db.*;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.StartupException;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.util.FileUtils;
-import org.apache.cassandra.schema.SchemaKeyspace;
-import org.apache.cassandra.utils.*;
+import org.apache.cassandra.utils.NativeLibrary;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.JavaUtils;
+import org.apache.cassandra.utils.SigarLibrary;
 
 /**
  * Verifies that the system and environment is in a fit state to be started.
@@ -82,6 +86,7 @@
     private final List<StartupCheck> DEFAULT_TESTS = ImmutableList.of(checkJemalloc,
                                                                       checkValidLaunchDate,
                                                                       checkJMXPorts,
+                                                                      checkJMXProperties,
                                                                       inspectJvmOptions,
                                                                       checkNativeLibraryInitialization,
                                                                       initSigarLibrary,
@@ -122,9 +127,9 @@
 
     public static final StartupCheck checkJemalloc = new StartupCheck()
     {
-        public void execute() throws StartupException
+        public void execute()
         {
-            if (FBUtilities.isWindows())
+            if (FBUtilities.isWindows)
                 return;
             String jemalloc = System.getProperty("cassandra.libjemalloc");
             if (jemalloc == null)
@@ -148,8 +153,9 @@
         {
             long now = System.currentTimeMillis();
             if (now < EARLIEST_LAUNCH_DATE)
-                throw new StartupException(1, String.format("current machine time is %s, but that is seemingly incorrect. exiting now.",
-                                                            new Date(now).toString()));
+                throw new StartupException(StartupException.ERR_WRONG_MACHINE_STATE,
+                                           String.format("current machine time is %s, but that is seemingly incorrect. exiting now.",
+                                                         new Date(now).toString()));
         }
     };
 
@@ -157,7 +163,7 @@
     {
         public void execute()
         {
-            String jmxPort = System.getProperty("com.sun.management.jmxremote.port");
+            String jmxPort = System.getProperty("cassandra.jmx.remote.port");
             if (jmxPort == null)
             {
                 logger.warn("JMX is not enabled to receive remote connections. Please see cassandra-env.sh for more info.");
@@ -167,7 +173,19 @@
             }
             else
             {
-                logger.info("JMX is enabled to receive remote connections on port: " + jmxPort);
+                logger.info("JMX is enabled to receive remote connections on port: {}", jmxPort);
+            }
+        }
+    };
+
+    public static final StartupCheck checkJMXProperties = new StartupCheck()
+    {
+        public void execute()
+        {
+            if (System.getProperty("com.sun.management.jmxremote.port") != null)
+            {
+                logger.warn("Use of com.sun.management.jmxremote.port at startup is deprecated. " +
+                            "Please use cassandra.jmx.remote.port instead.");
             }
         }
     };
@@ -237,7 +255,7 @@
         {
             // Fail-fast if the native library could not be linked.
             if (!NativeLibrary.isAvailable())
-                throw new StartupException(3, "The native library could not be initialized properly. ");
+                throw new StartupException(StartupException.ERR_WRONG_MACHINE_STATE, "The native library could not be initialized properly. ");
         }
     };
 
@@ -281,7 +299,7 @@
 
         public void execute()
         {
-            if (!FBUtilities.hasProcFS())
+            if (!FBUtilities.isLinux)
                 return;
 
             if (DatabaseDescriptor.getDiskAccessMode() == Config.DiskAccessMode.standard &&
@@ -314,12 +332,14 @@
                 logger.warn("Directory {} doesn't exist", dataDir);
                 // if they don't, failing their creation, stop cassandra.
                 if (!dir.mkdirs())
-                    throw new StartupException(3, "Has no permission to create directory "+ dataDir);
+                    throw new StartupException(StartupException.ERR_WRONG_DISK_STATE,
+                                               "Has no permission to create directory "+ dataDir);
             }
 
             // if directories exist verify their permissions
             if (!Directories.verifyFullPermissions(dir, dataDir))
-                throw new StartupException(3, "Insufficient permissions on directory " + dataDir);
+                throw new StartupException(StartupException.ERR_WRONG_DISK_STATE,
+                                           "Insufficient permissions on directory " + dataDir);
         }
     };
 
@@ -335,7 +355,7 @@
 
             FileVisitor<Path> sstableVisitor = new SimpleFileVisitor<Path>()
             {
-                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
+                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                 {
                     if (!Descriptor.isValidFile(file.getFileName().toString()))
                         return FileVisitResult.CONTINUE;
@@ -376,11 +396,12 @@
             }
 
             if (!invalid.isEmpty())
-                throw new StartupException(3, String.format("Detected unreadable sstables %s, please check " +
-                                                            "NEWS.txt and ensure that you have upgraded through " +
-                                                            "all required intermediate versions, running " +
-                                                            "upgradesstables",
-                                                            Joiner.on(",").join(invalid)));
+                throw new StartupException(StartupException.ERR_WRONG_DISK_STATE,
+                                           String.format("Detected unreadable sstables %s, please check " +
+                                                         "NEWS.txt and ensure that you have upgraded through " +
+                                                         "all required intermediate versions, running " +
+                                                         "upgradesstables",
+                                                         Joiner.on(",").join(invalid)));
 
         }
     };
@@ -393,7 +414,7 @@
             // we do a one-off scrub of the system keyspace first; we can't load the list of the rest of the keyspaces,
             // until system keyspace is opened.
 
-            for (CFMetaData cfm : Schema.instance.getTablesAndViews(SystemKeyspace.NAME))
+            for (CFMetaData cfm : Schema.instance.getTablesAndViews(SchemaConstants.SYSTEM_KEYSPACE_NAME))
                 ColumnFamilyStore.scrubDataDirectories(cfm);
 
             try
@@ -422,7 +443,7 @@
                         String formatMessage = "Cannot start node if snitch's data center (%s) differs from previous data center (%s). " +
                                                "Please fix the snitch configuration, decommission and rebootstrap this node or use the flag -Dcassandra.ignore_dc=true.";
 
-                        throw new StartupException(100, String.format(formatMessage, currentDc, storedDc));
+                        throw new StartupException(StartupException.ERR_WRONG_CONFIG, String.format(formatMessage, currentDc, storedDc));
                     }
                 }
             }
@@ -444,7 +465,7 @@
                         String formatMessage = "Cannot start node if snitch's rack (%s) differs from previous rack (%s). " +
                                                "Please fix the snitch configuration, decommission and rebootstrap this node or use the flag -Dcassandra.ignore_rack=true.";
 
-                        throw new StartupException(100, String.format(formatMessage, currentRack, storedRack));
+                        throw new StartupException(StartupException.ERR_WRONG_CONFIG, String.format(formatMessage, currentRack, storedRack));
                     }
                 }
             }
@@ -461,16 +482,16 @@
         List<String> existing = new ArrayList<>(LEGACY_AUTH_TABLES).stream().filter((legacyAuthTable) ->
             {
                 UntypedResultSet result = QueryProcessor.executeOnceInternal(String.format("SELECT table_name FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s'",
-                                                                                           SchemaKeyspace.NAME,
+                                                                                           SchemaConstants.SCHEMA_KEYSPACE_NAME,
                                                                                            "tables",
-                                                                                           AuthKeyspace.NAME,
+                                                                                           SchemaConstants.AUTH_KEYSPACE_NAME,
                                                                                            legacyAuthTable));
                 return result != null && !result.isEmpty();
             }).collect(Collectors.toList());
 
         if (!existing.isEmpty())
             return Optional.of(String.format("Legacy auth tables %s in keyspace %s still exist and have not been properly migrated.",
-                        Joiner.on(", ").join(existing), AuthKeyspace.NAME));
+                        Joiner.on(", ").join(existing), SchemaConstants.AUTH_KEYSPACE_NAME));
         else
             return Optional.empty();
     };
diff --git a/src/java/org/apache/cassandra/service/StorageProxy.java b/src/java/org/apache/cassandra/service/StorageProxy.java
index 89f93f6..bac46eb 100644
--- a/src/java/org/apache/cassandra/service/StorageProxy.java
+++ b/src/java/org/apache/cassandra/service/StorageProxy.java
@@ -32,7 +32,9 @@
 import com.google.common.collect.*;
 import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.Uninterruptibles;
+
 import org.apache.commons.lang3.StringUtils;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -44,6 +46,7 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.filter.DataLimits;
 import org.apache.cassandra.db.filter.TombstoneOverwhelmingException;
@@ -98,6 +101,8 @@
     private static final CASClientRequestMetrics casWriteMetrics = new CASClientRequestMetrics("CASWrite");
     private static final CASClientRequestMetrics casReadMetrics = new CASClientRequestMetrics("CASRead");
     private static final ViewWriteMetrics viewWriteMetrics = new ViewWriteMetrics("ViewWrite");
+    private static final Map<ConsistencyLevel, ClientRequestMetrics> readMetricsMap = new EnumMap<>(ConsistencyLevel.class);
+    private static final Map<ConsistencyLevel, ClientRequestMetrics> writeMetricsMap = new EnumMap<>(ConsistencyLevel.class);
 
     private static final double CONCURRENT_SUBREQUESTS_MARGIN = 0.10;
 
@@ -169,6 +174,12 @@
             }
         };
 
+        for(ConsistencyLevel level : ConsistencyLevel.values())
+        {
+            readMetricsMap.put(level, new ClientRequestMetrics("Read-" + level.name()));
+            writeMetricsMap.put(level, new ClientRequestMetrics("Write-" + level.name()));
+        }
+
         if (disableSerialReadLinearizability)
         {
             logger.warn("This node was started with -D{}. SERIAL (and LOCAL_SERIAL) reads coordinated by this node " +
@@ -227,10 +238,11 @@
                                   CASRequest request,
                                   ConsistencyLevel consistencyForPaxos,
                                   ConsistencyLevel consistencyForCommit,
-                                  ClientState state)
+                                  ClientState state,
+                                  long queryStartNanoTime)
     throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, InvalidRequestException
     {
-        final long start = System.nanoTime();
+        final long startTimeForMetrics = System.nanoTime();
         try
         {
             CFMetaData metadata = Schema.instance.getCFMetaData(keyspaceName, cfName);
@@ -243,7 +255,7 @@
                 ConsistencyLevel readConsistency = consistencyForPaxos == ConsistencyLevel.LOCAL_SERIAL ? ConsistencyLevel.LOCAL_QUORUM : ConsistencyLevel.QUORUM;
 
                 FilteredPartition current;
-                try (RowIterator rowIter = readOne(readCommand, readConsistency))
+                try (RowIterator rowIter = readOne(readCommand, readConsistency, queryStartNanoTime))
                 {
                     current = FilteredPartition.create(rowIter);
                 }
@@ -276,7 +288,7 @@
                            consistencyForCommit,
                            consistencyForCommit,
                            state,
-                           start,
+                           queryStartNanoTime,
                            casWriteMetrics,
                            updateProposer);
 
@@ -284,24 +296,35 @@
         catch (WriteTimeoutException | ReadTimeoutException e)
         {
             casWriteMetrics.timeouts.mark();
+            writeMetricsMap.get(consistencyForPaxos).timeouts.mark();
             throw e;
         }
         catch (WriteFailureException | ReadFailureException e)
         {
             casWriteMetrics.failures.mark();
+            writeMetricsMap.get(consistencyForPaxos).failures.mark();
             throw e;
         }
         catch (UnavailableException e)
         {
             casWriteMetrics.unavailables.mark();
+            writeMetricsMap.get(consistencyForPaxos).unavailables.mark();
             throw e;
         }
         finally
         {
-            casWriteMetrics.addNano(System.nanoTime() - start);
+            final long latency = System.nanoTime() - startTimeForMetrics;
+            casWriteMetrics.addNano(latency);
+            writeMetricsMap.get(consistencyForPaxos).addNano(latency);
         }
     }
 
+    private static void recordCasContention(CASClientRequestMetrics casMetrics, int contentions)
+    {
+        if(contentions > 0)
+            casMetrics.contention.update(contentions);
+    }
+
     /**
      * Performs the Paxos rounds for a given proposal, retrying when preempted until the timeout.
      *
@@ -317,7 +340,8 @@
      * @param consistencyForReplayCommits the consistency for the commit phase of "replayed" in-progress operations.
      * @param consistencyForCommit the consistency for the commit phase of _this_ operation update.
      * @param state the client state.
-     * @param queryStartNanoTime the nano time for the start of the query this is part of.
+     * @param queryStartNanoTime the nano time for the start of the query this is part of. This is the base time for
+     *     timeouts.
      * @param casMetrics the metrics to update for this operation.
      * @param createUpdateProposal method called after a successful 'prepare' phase to obtain 1) the actual update of
      *     this operation and 2) the result that the whole method should return. This can return {@code null} in the
@@ -371,14 +395,14 @@
 
                 Commit proposal = Commit.newProposal(ballot, proposalPair.left);
                 Tracing.trace("CAS precondition is met; proposing client-requested updates for {}", ballot);
-                if (proposePaxos(proposal, liveEndpoints, requiredParticipants, true, consistencyForPaxos))
+                if (proposePaxos(proposal, liveEndpoints, requiredParticipants, true, consistencyForPaxos, queryStartNanoTime))
                 {
                     // We skip committing accepted updates when they are empty. This is an optimization which works
                     // because we also skip replaying those same empty update in beginAndRepairPaxos (see the longer
                     // comment there). As empty update are somewhat common (serial reads and non-applying CAS propose
                     // them), this is worth bothering.
                     if (!proposal.update.isEmpty())
-                        commitPaxos(proposal, consistencyForCommit, true);
+                        commitPaxos(proposal, consistencyForCommit, true, queryStartNanoTime);
                     RowIterator result = proposalPair.right;
                     if (result != null)
                         Tracing.trace("CAS did not apply");
@@ -451,7 +475,7 @@
      * @return the Paxos ballot promised by the replicas if no in-progress requests were seen and a quorum of
      * nodes have seen the mostRecentCommit.  Otherwise, return null.
      */
-    private static Pair<UUID, Integer> beginAndRepairPaxos(long start,
+    private static Pair<UUID, Integer> beginAndRepairPaxos(long queryStartNanoTime,
                                                            DecoratedKey key,
                                                            CFMetaData metadata,
                                                            List<InetAddress> liveEndpoints,
@@ -466,7 +490,7 @@
 
         PrepareCallback summary = null;
         int contentions = 0;
-        while (System.nanoTime() - start < timeout)
+        while (System.nanoTime() - queryStartNanoTime < timeout)
         {
             // We want a timestamp that is guaranteed to be unique for that node (so that the ballot is globally unique), but if we've got a prepare rejected
             // already we also want to make sure we pick a timestamp that has a chance to be promised, i.e. one that is greater that the most recently known
@@ -481,7 +505,7 @@
             // prepare
             Tracing.trace("Preparing {}", ballot);
             Commit toPrepare = Commit.newPrepare(key, metadata, ballot);
-            summary = preparePaxos(toPrepare, liveEndpoints, requiredParticipants, consistencyForPaxos);
+            summary = preparePaxos(toPrepare, liveEndpoints, requiredParticipants, consistencyForPaxos, queryStartNanoTime);
             if (!summary.promised)
             {
                 Tracing.trace("Some replicas have already promised a higher ballot than ours; aborting");
@@ -517,14 +541,15 @@
                 Tracing.trace("Finishing incomplete paxos round {}", inProgress);
                 casMetrics.unfinishedCommit.inc();
                 Commit refreshedInProgress = Commit.newProposal(ballot, inProgress.update);
-                if (proposePaxos(refreshedInProgress, liveEndpoints, requiredParticipants, false, consistencyForPaxos))
+                if (proposePaxos(refreshedInProgress, liveEndpoints, requiredParticipants, false, consistencyForPaxos, queryStartNanoTime))
                 {
                     try
                     {
-                        commitPaxos(refreshedInProgress, consistencyForCommit, false);
+                        commitPaxos(refreshedInProgress, consistencyForCommit, false, queryStartNanoTime);
                     }
                     catch (WriteTimeoutException e)
                     {
+                        recordCasContention(casMetrics, contentions);
                         // We're still doing preparation for the paxos rounds, so we want to use the CAS (see CASSANDRA-8672)
                         throw new WriteTimeoutException(WriteType.CAS, e.consistency, e.received, e.blockFor);
                     }
@@ -559,6 +584,7 @@
             return Pair.create(ballot, contentions);
         }
 
+        recordCasContention(casMetrics, contentions);
         throw new WriteTimeoutException(WriteType.CAS, consistencyForPaxos, 0, consistencyForPaxos.blockFor(Keyspace.open(metadata.ksName)));
     }
 
@@ -572,10 +598,10 @@
             MessagingService.instance().sendOneWay(message, target);
     }
 
-    private static PrepareCallback preparePaxos(Commit toPrepare, List<InetAddress> endpoints, int requiredParticipants, ConsistencyLevel consistencyForPaxos)
+    private static PrepareCallback preparePaxos(Commit toPrepare, List<InetAddress> endpoints, int requiredParticipants, ConsistencyLevel consistencyForPaxos, long queryStartNanoTime)
     throws WriteTimeoutException
     {
-        PrepareCallback callback = new PrepareCallback(toPrepare.update.partitionKey(), toPrepare.update.metadata(), requiredParticipants, consistencyForPaxos);
+        PrepareCallback callback = new PrepareCallback(toPrepare.update.partitionKey(), toPrepare.update.metadata(), requiredParticipants, consistencyForPaxos, queryStartNanoTime);
         MessageOut<Commit> message = new MessageOut<Commit>(MessagingService.Verb.PAXOS_PREPARE, toPrepare, Commit.serializer);
         for (InetAddress target : endpoints)
             MessagingService.instance().sendRR(message, target, callback);
@@ -583,10 +609,10 @@
         return callback;
     }
 
-    private static boolean proposePaxos(Commit proposal, List<InetAddress> endpoints, int requiredParticipants, boolean timeoutIfPartial, ConsistencyLevel consistencyLevel)
+    private static boolean proposePaxos(Commit proposal, List<InetAddress> endpoints, int requiredParticipants, boolean timeoutIfPartial, ConsistencyLevel consistencyLevel, long queryStartNanoTime)
     throws WriteTimeoutException
     {
-        ProposeCallback callback = new ProposeCallback(endpoints.size(), requiredParticipants, !timeoutIfPartial, consistencyLevel);
+        ProposeCallback callback = new ProposeCallback(endpoints.size(), requiredParticipants, !timeoutIfPartial, consistencyLevel, queryStartNanoTime);
         MessageOut<Commit> message = new MessageOut<Commit>(MessagingService.Verb.PAXOS_PROPOSE, proposal, Commit.serializer);
         for (InetAddress target : endpoints)
             MessagingService.instance().sendRR(message, target, callback);
@@ -602,7 +628,7 @@
         return false;
     }
 
-    private static void commitPaxos(Commit proposal, ConsistencyLevel consistencyLevel, boolean allowHints) throws WriteTimeoutException
+    private static void commitPaxos(Commit proposal, ConsistencyLevel consistencyLevel, boolean allowHints, long queryStartNanoTime) throws WriteTimeoutException
     {
         boolean shouldBlock = consistencyLevel != ConsistencyLevel.ANY;
         Keyspace keyspace = Keyspace.open(proposal.update.metadata().ksName);
@@ -615,12 +641,15 @@
         if (shouldBlock)
         {
             AbstractReplicationStrategy rs = keyspace.getReplicationStrategy();
-            responseHandler = rs.getWriteResponseHandler(naturalEndpoints, pendingEndpoints, consistencyLevel, null, WriteType.SIMPLE);
+            responseHandler = rs.getWriteResponseHandler(naturalEndpoints, pendingEndpoints, consistencyLevel, null, WriteType.SIMPLE, queryStartNanoTime);
+            responseHandler.setSupportsBackPressure(false);
         }
 
         MessageOut<Commit> message = new MessageOut<Commit>(MessagingService.Verb.PAXOS_COMMIT, proposal, Commit.serializer);
         for (InetAddress destination : Iterables.concat(naturalEndpoints, pendingEndpoints))
         {
+            checkHintOverload(destination);
+
             if (FailureDetector.instance.isAlive(destination))
             {
                 if (shouldBlock)
@@ -666,7 +695,7 @@
                 {
                     if (!(ex instanceof WriteTimeoutException))
                         logger.error("Failed to apply paxos commit locally : {}", ex);
-                    responseHandler.onFailure(FBUtilities.getBroadcastAddress());
+                    responseHandler.onFailure(FBUtilities.getBroadcastAddress(), RequestFailureReason.UNKNOWN);
                 }
             }
 
@@ -686,8 +715,9 @@
      *
      * @param mutations the mutations to be applied across the replicas
      * @param consistency_level the consistency level for the operation
+     * @param queryStartNanoTime the value of System.nanoTime() when the query started to be processed
      */
-    public static void mutate(Collection<? extends IMutation> mutations, ConsistencyLevel consistency_level)
+    public static void mutate(Collection<? extends IMutation> mutations, ConsistencyLevel consistency_level, long queryStartNanoTime)
     throws UnavailableException, OverloadedException, WriteTimeoutException, WriteFailureException
     {
         Tracing.trace("Determining replicas for mutation");
@@ -702,12 +732,12 @@
             {
                 if (mutation instanceof CounterMutation)
                 {
-                    responseHandlers.add(mutateCounter((CounterMutation)mutation, localDataCenter));
+                    responseHandlers.add(mutateCounter((CounterMutation)mutation, localDataCenter, queryStartNanoTime));
                 }
                 else
                 {
                     WriteType wt = mutations.size() <= 1 ? WriteType.SIMPLE : WriteType.UNLOGGED_BATCH;
-                    responseHandlers.add(performWrite(mutation, consistency_level, localDataCenter, standardWritePerformer, null, wt));
+                    responseHandlers.add(performWrite(mutation, consistency_level, localDataCenter, standardWritePerformer, null, wt, queryStartNanoTime));
                 }
             }
 
@@ -728,13 +758,15 @@
                 if (ex instanceof WriteFailureException)
                 {
                     writeMetrics.failures.mark();
+                    writeMetricsMap.get(consistency_level).failures.mark();
                     WriteFailureException fe = (WriteFailureException)ex;
                     Tracing.trace("Write failure; received {} of {} required replies, failed {} requests",
-                                  fe.received, fe.blockFor, fe.failures);
+                                  fe.received, fe.blockFor, fe.failureReasonByEndpoint.size());
                 }
                 else
                 {
                     writeMetrics.timeouts.mark();
+                    writeMetricsMap.get(consistency_level).timeouts.mark();
                     WriteTimeoutException te = (WriteTimeoutException)ex;
                     Tracing.trace("Write timeout; received {} of {} required replies", te.received, te.blockFor);
                 }
@@ -744,18 +776,22 @@
         catch (UnavailableException e)
         {
             writeMetrics.unavailables.mark();
+            writeMetricsMap.get(consistency_level).unavailables.mark();
             Tracing.trace("Unavailable");
             throw e;
         }
         catch (OverloadedException e)
         {
             writeMetrics.unavailables.mark();
+            writeMetricsMap.get(consistency_level).unavailables.mark();
             Tracing.trace("Overloaded");
             throw e;
         }
         finally
         {
-            writeMetrics.addNano(System.nanoTime() - startTime);
+            long latency = System.nanoTime() - startTime;
+            writeMetrics.addNano(latency);
+            writeMetricsMap.get(consistency_level).addNano(latency);
         }
     }
 
@@ -811,8 +847,9 @@
      * @param mutations the mutations to be applied across the replicas
      * @param writeCommitLog if commitlog should be written
      * @param baseComplete time from epoch in ms that the local base mutation was(or will be) completed
+     * @param queryStartNanoTime the value of System.nanoTime() when the query started to be processed
      */
-    public static void mutateMV(ByteBuffer dataKey, Collection<Mutation> mutations, boolean writeCommitLog, AtomicLong baseComplete)
+    public static void mutateMV(ByteBuffer dataKey, Collection<Mutation> mutations, boolean writeCommitLog, AtomicLong baseComplete, long queryStartNanoTime)
     throws UnavailableException, OverloadedException, WriteTimeoutException
     {
         Tracing.trace("Determining replicas for mutation");
@@ -869,6 +906,7 @@
                     // write so the view mutation is sent to the pending endpoint
                     if (pairedEndpoint.get().equals(FBUtilities.getBroadcastAddress()) && StorageService.instance.isJoined()
                         && pendingEndpoints.isEmpty())
+                    {
                         try
                         {
                             mutation.apply(writeCommitLog);
@@ -882,6 +920,7 @@
                                          mutation.getKeyspaceName(), mutation.getColumnFamilyIds(), mutation.key());
                             throw exc;
                         }
+                    }
                     else
                     {
                         wrappers.add(wrapViewBatchResponseHandler(mutation,
@@ -890,7 +929,8 @@
                                                                   Collections.singletonList(pairedEndpoint.get()),
                                                                   baseComplete,
                                                                   WriteType.BATCH,
-                                                                  cleanup));
+                                                                  cleanup,
+                                                                  queryStartNanoTime));
                     }
                 }
 
@@ -912,7 +952,8 @@
     @SuppressWarnings("unchecked")
     public static void mutateWithTriggers(Collection<? extends IMutation> mutations,
                                           ConsistencyLevel consistencyLevel,
-                                          boolean mutateAtomically)
+                                          boolean mutateAtomically,
+                                          long queryStartNanoTime)
     throws WriteTimeoutException, WriteFailureException, UnavailableException, OverloadedException, InvalidRequestException
     {
         Collection<Mutation> augmented = TriggerExecutor.instance.execute(mutations);
@@ -922,13 +963,13 @@
                               .updatesAffectView(mutations, true);
 
         if (augmented != null)
-            mutateAtomically(augmented, consistencyLevel, updatesView);
+            mutateAtomically(augmented, consistencyLevel, updatesView, queryStartNanoTime);
         else
         {
             if (mutateAtomically || updatesView)
-                mutateAtomically((Collection<Mutation>) mutations, consistencyLevel, updatesView);
+                mutateAtomically((Collection<Mutation>) mutations, consistencyLevel, updatesView, queryStartNanoTime);
             else
-                mutate(mutations, consistencyLevel);
+                mutate(mutations, consistencyLevel, queryStartNanoTime);
         }
     }
 
@@ -941,10 +982,12 @@
      * @param mutations the Mutations to be applied across the replicas
      * @param consistency_level the consistency level for the operation
      * @param requireQuorumForRemove at least a quorum of nodes will see update before deleting batchlog
+     * @param queryStartNanoTime the value of System.nanoTime() when the query started to be processed
      */
     public static void mutateAtomically(Collection<Mutation> mutations,
                                         ConsistencyLevel consistency_level,
-                                        boolean requireQuorumForRemove)
+                                        boolean requireQuorumForRemove,
+                                        long queryStartNanoTime)
     throws UnavailableException, OverloadedException, WriteTimeoutException
     {
         Tracing.trace("Determining replicas for atomic batch");
@@ -972,7 +1015,7 @@
             final BatchlogEndpoints batchlogEndpoints = getBatchlogEndpoints(localDataCenter, batchConsistencyLevel);
             final UUID batchUUID = UUIDGen.getTimeUUID();
             BatchlogResponseHandler.BatchlogCleanup cleanup = new BatchlogResponseHandler.BatchlogCleanup(mutations.size(),
-                                                                                                          () -> asyncRemoveFromBatchlog(batchlogEndpoints, batchUUID));
+                                                                                                          () -> asyncRemoveFromBatchlog(batchlogEndpoints, batchUUID, queryStartNanoTime));
 
             // add a handler for each mutation - includes checking availability, but doesn't initiate any writes, yet
             for (Mutation mutation : mutations)
@@ -981,14 +1024,15 @@
                                                                                consistency_level,
                                                                                batchConsistencyLevel,
                                                                                WriteType.BATCH,
-                                                                               cleanup);
+                                                                               cleanup,
+                                                                               queryStartNanoTime);
                 // exit early if we can't fulfill the CL at this time.
                 wrapper.handler.assureSufficientLiveNodes();
                 wrappers.add(wrapper);
             }
 
             // write to the batchlog
-            syncWriteToBatchlog(mutations, batchlogEndpoints, batchUUID);
+            syncWriteToBatchlog(mutations, batchlogEndpoints, batchUUID, queryStartNanoTime);
 
             // now actually perform the writes and wait for them to complete
             syncWriteBatchedMutations(wrappers, localDataCenter, Stage.MUTATION);
@@ -996,24 +1040,30 @@
         catch (UnavailableException e)
         {
             writeMetrics.unavailables.mark();
+            writeMetricsMap.get(consistency_level).unavailables.mark();
             Tracing.trace("Unavailable");
             throw e;
         }
         catch (WriteTimeoutException e)
         {
             writeMetrics.timeouts.mark();
+            writeMetricsMap.get(consistency_level).timeouts.mark();
             Tracing.trace("Write timeout; received {} of {} required replies", e.received, e.blockFor);
             throw e;
         }
         catch (WriteFailureException e)
         {
             writeMetrics.failures.mark();
+            writeMetricsMap.get(consistency_level).failures.mark();
             Tracing.trace("Write failure; received {} of {} required replies", e.received, e.blockFor);
             throw e;
         }
         finally
         {
-            writeMetrics.addNano(System.nanoTime() - startTime);
+            long latency = System.nanoTime() - startTime;
+            writeMetrics.addNano(latency);
+            writeMetricsMap.get(consistency_level).addNano(latency);
+
         }
     }
 
@@ -1022,15 +1072,16 @@
         return replica.equals(FBUtilities.getBroadcastAddress());
     }
 
-    private static void syncWriteToBatchlog(Collection<Mutation> mutations, BatchlogEndpoints endpoints, UUID uuid)
+    private static void syncWriteToBatchlog(Collection<Mutation> mutations, BatchlogEndpoints endpoints, UUID uuid, long queryStartNanoTime)
     throws WriteTimeoutException, WriteFailureException
     {
         WriteResponseHandler<?> handler = new WriteResponseHandler<>(endpoints.all,
                                                                      Collections.<InetAddress>emptyList(),
                                                                      endpoints.all.size() == 1 ? ConsistencyLevel.ONE : ConsistencyLevel.TWO,
-                                                                     Keyspace.open(SystemKeyspace.NAME),
+                                                                     Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME),
                                                                      null,
-                                                                     WriteType.BATCH_LOG);
+                                                                     WriteType.BATCH_LOG,
+                                                                     queryStartNanoTime);
 
         Batch batch = Batch.createLocal(uuid, FBUtilities.timestampMicros(), mutations);
 
@@ -1053,19 +1104,19 @@
             logger.trace("Sending batchlog store request {} to {} for {} mutations", batch.id, target, batch.size());
 
             if (canDoLocalRequest(target))
-                performLocally(Stage.MUTATION, () -> BatchlogManager.store(batch), handler);
+                performLocally(Stage.MUTATION, Optional.empty(), () -> BatchlogManager.store(batch), handler);
             else
                 MessagingService.instance().sendRR(message, target, handler);
         }
     }
 
-    private static void asyncRemoveFromBatchlog(BatchlogEndpoints endpoints, UUID uuid)
+    private static void asyncRemoveFromBatchlog(BatchlogEndpoints endpoints, UUID uuid, long queryStartNanoTime)
     {
         if (!endpoints.current.isEmpty())
             asyncRemoveFromBatchlog(endpoints.current, uuid);
 
         if (!endpoints.legacy.isEmpty())
-            LegacyBatchlogMigrator.asyncRemoveFromBatchlog(endpoints.legacy, uuid);
+            LegacyBatchlogMigrator.asyncRemoveFromBatchlog(endpoints.legacy, uuid, queryStartNanoTime);
     }
 
     private static void asyncRemoveFromBatchlog(Collection<InetAddress> endpoints, UUID uuid)
@@ -1095,7 +1146,7 @@
             }
             catch (OverloadedException | WriteTimeoutException e)
             {
-                wrapper.handler.onFailure(FBUtilities.getBroadcastAddress());
+                wrapper.handler.onFailure(FBUtilities.getBroadcastAddress(), RequestFailureReason.UNKNOWN);
             }
         }
     }
@@ -1126,14 +1177,15 @@
      * given the list of write endpoints (either standardWritePerformer for
      * standard writes or counterWritePerformer for counter writes).
      * @param callback an optional callback to be run if and when the write is
-     * successful.
+     * @param queryStartNanoTime the value of System.nanoTime() when the query started to be processed
      */
     public static AbstractWriteResponseHandler<IMutation> performWrite(IMutation mutation,
-                                                            ConsistencyLevel consistency_level,
-                                                            String localDataCenter,
-                                                            WritePerformer performer,
-                                                            Runnable callback,
-                                                            WriteType writeType)
+                                                                       ConsistencyLevel consistency_level,
+                                                                       String localDataCenter,
+                                                                       WritePerformer performer,
+                                                                       Runnable callback,
+                                                                       WriteType writeType,
+                                                                       long queryStartNanoTime)
     throws UnavailableException, OverloadedException
     {
         String keyspaceName = mutation.getKeyspaceName();
@@ -1143,7 +1195,7 @@
         List<InetAddress> naturalEndpoints = StorageService.instance.getNaturalEndpoints(keyspaceName, tk);
         Collection<InetAddress> pendingEndpoints = StorageService.instance.getTokenMetadata().pendingEndpointsFor(tk, keyspaceName);
 
-        AbstractWriteResponseHandler<IMutation> responseHandler = rs.getWriteResponseHandler(naturalEndpoints, pendingEndpoints, consistency_level, callback, writeType);
+        AbstractWriteResponseHandler<IMutation> responseHandler = rs.getWriteResponseHandler(naturalEndpoints, pendingEndpoints, consistency_level, callback, writeType, queryStartNanoTime);
 
         // exit early if we can't fulfill the CL at this time
         responseHandler.assureSufficientLiveNodes();
@@ -1157,7 +1209,8 @@
                                                                         ConsistencyLevel consistency_level,
                                                                         ConsistencyLevel batchConsistencyLevel,
                                                                         WriteType writeType,
-                                                                        BatchlogResponseHandler.BatchlogCleanup cleanup)
+                                                                        BatchlogResponseHandler.BatchlogCleanup cleanup,
+                                                                        long queryStartNanoTime)
     {
         Keyspace keyspace = Keyspace.open(mutation.getKeyspaceName());
         AbstractReplicationStrategy rs = keyspace.getReplicationStrategy();
@@ -1165,8 +1218,8 @@
         Token tk = mutation.key().getToken();
         List<InetAddress> naturalEndpoints = StorageService.instance.getNaturalEndpoints(keyspaceName, tk);
         Collection<InetAddress> pendingEndpoints = StorageService.instance.getTokenMetadata().pendingEndpointsFor(tk, keyspaceName);
-        AbstractWriteResponseHandler<IMutation> writeHandler = rs.getWriteResponseHandler(naturalEndpoints, pendingEndpoints, consistency_level, null, writeType);
-        BatchlogResponseHandler<IMutation> batchHandler = new BatchlogResponseHandler<>(writeHandler, batchConsistencyLevel.blockFor(keyspace), cleanup);
+        AbstractWriteResponseHandler<IMutation> writeHandler = rs.getWriteResponseHandler(naturalEndpoints, pendingEndpoints, consistency_level, null, writeType, queryStartNanoTime);
+        BatchlogResponseHandler<IMutation> batchHandler = new BatchlogResponseHandler<>(writeHandler, batchConsistencyLevel.blockFor(keyspace), cleanup, queryStartNanoTime);
         return new WriteResponseHandlerWrapper(batchHandler, mutation);
     }
 
@@ -1180,7 +1233,8 @@
                                                                             List<InetAddress> naturalEndpoints,
                                                                             AtomicLong baseComplete,
                                                                             WriteType writeType,
-                                                                            BatchlogResponseHandler.BatchlogCleanup cleanup)
+                                                                            BatchlogResponseHandler.BatchlogCleanup cleanup,
+                                                                            long queryStartNanoTime)
     {
         Keyspace keyspace = Keyspace.open(mutation.getKeyspaceName());
         AbstractReplicationStrategy rs = keyspace.getReplicationStrategy();
@@ -1190,8 +1244,8 @@
         AbstractWriteResponseHandler<IMutation> writeHandler = rs.getWriteResponseHandler(naturalEndpoints, pendingEndpoints, consistency_level, () -> {
             long delay = Math.max(0, System.currentTimeMillis() - baseComplete.get());
             viewWriteMetrics.viewWriteLatency.update(delay, TimeUnit.MILLISECONDS);
-        }, writeType);
-        BatchlogResponseHandler<IMutation> batchHandler = new ViewWriteMetricsWrapped(writeHandler, batchConsistencyLevel.blockFor(keyspace), cleanup);
+        }, writeType, queryStartNanoTime);
+        BatchlogResponseHandler<IMutation> batchHandler = new ViewWriteMetricsWrapped(writeHandler, batchConsistencyLevel.blockFor(keyspace), cleanup, queryStartNanoTime);
         return new WriteResponseHandlerWrapper(batchHandler, mutation);
     }
 
@@ -1283,6 +1337,10 @@
                                              Stage stage)
     throws OverloadedException
     {
+        int targetsSize = Iterables.size(targets);
+
+        // this dc replicas:
+        Collection<InetAddress> localDc = null;
         // extra-datacenter replicas, grouped by dc
         Map<String, Collection<InetAddress>> dcGroups = null;
         // only need to create a Message for non-local writes
@@ -1291,6 +1349,8 @@
         boolean insertLocal = false;
         ArrayList<InetAddress> endpointsToHint = null;
 
+        List<InetAddress> backPressureHosts = null;
+
         for (InetAddress destination : targets)
         {
             checkHintOverload(destination);
@@ -1306,12 +1366,17 @@
                     // belongs on a different server
                     if (message == null)
                         message = mutation.createMessage();
+
                     String dc = DatabaseDescriptor.getEndpointSnitch().getDatacenter(destination);
+
                     // direct writes to local DC or old Cassandra versions
                     // (1.1 knows how to forward old-style String message IDs; updated to int in 2.0)
                     if (localDataCenter.equals(dc))
                     {
-                        MessagingService.instance().sendRR(message, destination, responseHandler, true);
+                        if (localDc == null)
+                            localDc = new ArrayList<>(targetsSize);
+
+                        localDc.add(destination);
                     }
                     else
                     {
@@ -1323,8 +1388,14 @@
                                 dcGroups = new HashMap<>();
                             dcGroups.put(dc, messages);
                         }
+
                         messages.add(destination);
                     }
+
+                    if (backPressureHosts == null)
+                        backPressureHosts = new ArrayList<>(targetsSize);
+
+                    backPressureHosts.add(destination);
                 }
             }
             else
@@ -1332,24 +1403,30 @@
                 if (shouldHint(destination))
                 {
                     if (endpointsToHint == null)
-                        endpointsToHint = new ArrayList<>(Iterables.size(targets));
+                        endpointsToHint = new ArrayList<>(targetsSize);
+
                     endpointsToHint.add(destination);
                 }
             }
         }
 
+        if (backPressureHosts != null)
+            MessagingService.instance().applyBackPressure(backPressureHosts, responseHandler.currentTimeout());
+
         if (endpointsToHint != null)
             submitHint(mutation, endpointsToHint, responseHandler);
 
         if (insertLocal)
-            performLocally(stage, mutation::apply, responseHandler);
+            performLocally(stage, Optional.of(mutation), mutation::apply, responseHandler);
 
+        if (localDc != null)
+        {
+            for (InetAddress destination : localDc)
+                MessagingService.instance().sendRR(message, destination, responseHandler, true);
+        }
         if (dcGroups != null)
         {
             // for each datacenter, send the message to one node to relay the write to other replicas
-            if (message == null)
-                message = mutation.createMessage();
-
             for (Collection<InetAddress> dcTargets : dcGroups.values())
                 sendMessagesToNonlocalDC(message, dcTargets, responseHandler);
         }
@@ -1379,7 +1456,7 @@
         InetAddress target = iter.next();
 
         // Add the other destinations of the same message as a FORWARD_HEADER entry
-        try (DataOutputBuffer out = new DataOutputBuffer())
+        try(DataOutputBuffer out = new DataOutputBuffer())
         {
             out.writeInt(targets.size() - 1);
             while (iter.hasNext())
@@ -1431,9 +1508,9 @@
         });
     }
 
-    private static void performLocally(Stage stage, final Runnable runnable, final IAsyncCallbackWithFailure<?> handler)
+    private static void performLocally(Stage stage, Optional<IMutation> mutation, final Runnable runnable, final IAsyncCallbackWithFailure<?> handler)
     {
-        StageManager.getStage(stage).maybeExecuteImmediately(new LocalMutationRunnable()
+        StageManager.getStage(stage).maybeExecuteImmediately(new LocalMutationRunnable(mutation)
         {
             public void runMayThrow()
             {
@@ -1446,7 +1523,7 @@
                 {
                     if (!(ex instanceof WriteTimeoutException))
                         logger.error("Failed to apply mutation locally : ", ex);
-                    handler.onFailure(FBUtilities.getBroadcastAddress());
+                    handler.onFailure(FBUtilities.getBroadcastAddress(), RequestFailureReason.UNKNOWN);
                 }
             }
 
@@ -1472,13 +1549,13 @@
      * quicker response and because the WriteResponseHandlers don't make it easy to send back an error. We also always gather
      * the write latencies at the coordinator node to make gathering point similar to the case of standard writes.
      */
-    public static AbstractWriteResponseHandler<IMutation> mutateCounter(CounterMutation cm, String localDataCenter) throws UnavailableException, OverloadedException
+    public static AbstractWriteResponseHandler<IMutation> mutateCounter(CounterMutation cm, String localDataCenter, long queryStartNanoTime) throws UnavailableException, OverloadedException
     {
         InetAddress endpoint = findSuitableEndpoint(cm.getKeyspaceName(), cm.key(), localDataCenter, cm.consistency());
 
         if (endpoint.equals(FBUtilities.getBroadcastAddress()))
         {
-            return applyCounterMutationOnCoordinator(cm, localDataCenter);
+            return applyCounterMutationOnCoordinator(cm, localDataCenter, queryStartNanoTime);
         }
         else
         {
@@ -1489,10 +1566,10 @@
             List<InetAddress> naturalEndpoints = StorageService.instance.getNaturalEndpoints(keyspaceName, tk);
             Collection<InetAddress> pendingEndpoints = StorageService.instance.getTokenMetadata().pendingEndpointsFor(tk, keyspaceName);
 
-            rs.getWriteResponseHandler(naturalEndpoints, pendingEndpoints, cm.consistency(), null, WriteType.COUNTER).assureSufficientLiveNodes();
+            rs.getWriteResponseHandler(naturalEndpoints, pendingEndpoints, cm.consistency(), null, WriteType.COUNTER, queryStartNanoTime).assureSufficientLiveNodes();
 
             // Forward the actual update to the chosen leader replica
-            AbstractWriteResponseHandler<IMutation> responseHandler = new WriteResponseHandler<>(endpoint, WriteType.COUNTER);
+            AbstractWriteResponseHandler<IMutation> responseHandler = new WriteResponseHandler<>(endpoint, WriteType.COUNTER, queryStartNanoTime);
 
             Tracing.trace("Enqueuing counter update to {}", endpoint);
             MessagingService.instance().sendRR(cm.makeMutationMessage(), endpoint, responseHandler, false);
@@ -1546,18 +1623,18 @@
 
     // Must be called on a replica of the mutation. This replica becomes the
     // leader of this mutation.
-    public static AbstractWriteResponseHandler<IMutation> applyCounterMutationOnLeader(CounterMutation cm, String localDataCenter, Runnable callback)
+    public static AbstractWriteResponseHandler<IMutation> applyCounterMutationOnLeader(CounterMutation cm, String localDataCenter, Runnable callback, long queryStartNanoTime)
     throws UnavailableException, OverloadedException
     {
-        return performWrite(cm, cm.consistency(), localDataCenter, counterWritePerformer, callback, WriteType.COUNTER);
+        return performWrite(cm, cm.consistency(), localDataCenter, counterWritePerformer, callback, WriteType.COUNTER, queryStartNanoTime);
     }
 
     // Same as applyCounterMutationOnLeader but must with the difference that it use the MUTATION stage to execute the write (while
     // applyCounterMutationOnLeader assumes it is on the MUTATION stage already)
-    public static AbstractWriteResponseHandler<IMutation> applyCounterMutationOnCoordinator(CounterMutation cm, String localDataCenter)
+    public static AbstractWriteResponseHandler<IMutation> applyCounterMutationOnCoordinator(CounterMutation cm, String localDataCenter, long queryStartNanoTime)
     throws UnavailableException, OverloadedException
     {
-        return performWrite(cm, cm.consistency(), localDataCenter, counterWriteOnCoordinatorPerformer, null, WriteType.COUNTER);
+        return performWrite(cm, cm.consistency(), localDataCenter, counterWriteOnCoordinatorPerformer, null, WriteType.COUNTER, queryStartNanoTime);
     }
 
     private static Runnable counterWriteTask(final IMutation mutation,
@@ -1572,7 +1649,7 @@
             {
                 assert mutation instanceof CounterMutation;
 
-                Mutation result = ((CounterMutation) mutation).apply();
+                Mutation result = ((CounterMutation) mutation).applyCounterMutation();
                 responseHandler.response(null);
 
                 Set<InetAddress> remotes = Sets.difference(ImmutableSet.copyOf(targets),
@@ -1586,50 +1663,51 @@
     private static boolean systemKeyspaceQuery(List<? extends ReadCommand> cmds)
     {
         for (ReadCommand cmd : cmds)
-            if (!Schema.isLocalSystemKeyspace(cmd.metadata().ksName))
+            if (!SchemaConstants.isLocalSystemKeyspace(cmd.metadata().ksName))
                 return false;
         return true;
     }
 
-    public static RowIterator readOne(SinglePartitionReadCommand command, ConsistencyLevel consistencyLevel)
+    public static RowIterator readOne(SinglePartitionReadCommand command, ConsistencyLevel consistencyLevel, long queryStartNanoTime)
     throws UnavailableException, IsBootstrappingException, ReadFailureException, ReadTimeoutException, InvalidRequestException
     {
-        return readOne(command, consistencyLevel, null);
+        return readOne(command, consistencyLevel, null, queryStartNanoTime);
     }
 
-    public static RowIterator readOne(SinglePartitionReadCommand command, ConsistencyLevel consistencyLevel, ClientState state)
+    public static RowIterator readOne(SinglePartitionReadCommand command, ConsistencyLevel consistencyLevel, ClientState state, long queryStartNanoTime)
     throws UnavailableException, IsBootstrappingException, ReadFailureException, ReadTimeoutException, InvalidRequestException
     {
-        return PartitionIterators.getOnlyElement(read(SinglePartitionReadCommand.Group.one(command), consistencyLevel, state), command);
+        return PartitionIterators.getOnlyElement(read(SinglePartitionReadCommand.Group.one(command), consistencyLevel, state, queryStartNanoTime), command);
     }
 
-    public static PartitionIterator read(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel)
+    public static PartitionIterator read(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, long queryStartNanoTime)
     throws UnavailableException, IsBootstrappingException, ReadFailureException, ReadTimeoutException, InvalidRequestException
     {
         // When using serial CL, the ClientState should be provided
         assert !consistencyLevel.isSerialConsistency();
-        return read(group, consistencyLevel, null);
+        return read(group, consistencyLevel, null, queryStartNanoTime);
     }
 
     /**
      * Performs the actual reading of a row out of the StorageService, fetching
      * a specific set of column names from a given column family.
      */
-    public static PartitionIterator read(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, ClientState state)
+    public static PartitionIterator read(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, ClientState state, long queryStartNanoTime)
     throws UnavailableException, IsBootstrappingException, ReadFailureException, ReadTimeoutException, InvalidRequestException
     {
         if (StorageService.instance.isBootstrapMode() && !systemKeyspaceQuery(group.commands))
         {
             readMetrics.unavailables.mark();
+            readMetricsMap.get(consistencyLevel).unavailables.mark();
             throw new IsBootstrappingException();
         }
 
         return consistencyLevel.isSerialConsistency()
-             ? readWithPaxos(group, consistencyLevel, state)
-             : readRegular(group, consistencyLevel);
+             ? readWithPaxos(group, consistencyLevel, state, queryStartNanoTime)
+             : readRegular(group, consistencyLevel, queryStartNanoTime);
     }
 
-    private static PartitionIterator readWithPaxos(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, ClientState state)
+    private static PartitionIterator readWithPaxos(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, ClientState state, long queryStartNanoTime)
     throws InvalidRequestException, UnavailableException, ReadFailureException, ReadTimeoutException
     {
         assert state != null;
@@ -1644,9 +1722,9 @@
         PartitionIterator result = null;
         try
         {
-            final ConsistencyLevel consistencyForReplayCommitOrFetch = consistencyLevel == ConsistencyLevel.LOCAL_SERIAL
-                                                                       ? ConsistencyLevel.LOCAL_QUORUM
-                                                                       : ConsistencyLevel.QUORUM;
+            final ConsistencyLevel consistencyForReplayCommitsOrFetch = consistencyLevel == ConsistencyLevel.LOCAL_SERIAL
+                                                                        ? ConsistencyLevel.LOCAL_QUORUM
+                                                                        : ConsistencyLevel.QUORUM;
 
             try
             {
@@ -1663,7 +1741,7 @@
                 doPaxos(metadata,
                         key,
                         consistencyLevel,
-                        consistencyForReplayCommitOrFetch,
+                        consistencyForReplayCommitsOrFetch,
                         ConsistencyLevel.ANY,
                         state,
                         start,
@@ -1676,27 +1754,30 @@
             }
             catch (WriteFailureException e)
             {
-                throw new ReadFailureException(consistencyLevel, e.received, e.failures, e.blockFor, false);
+                throw new ReadFailureException(consistencyLevel, e.received, e.blockFor, false, e.failureReasonByEndpoint);
             }
 
-            result = fetchRows(group.commands, consistencyForReplayCommitOrFetch);
+            result = fetchRows(group.commands, consistencyForReplayCommitsOrFetch, queryStartNanoTime);
         }
         catch (UnavailableException e)
         {
             readMetrics.unavailables.mark();
             casReadMetrics.unavailables.mark();
+            readMetricsMap.get(consistencyLevel).unavailables.mark();
             throw e;
         }
         catch (ReadTimeoutException e)
         {
             readMetrics.timeouts.mark();
             casReadMetrics.timeouts.mark();
+            readMetricsMap.get(consistencyLevel).timeouts.mark();
             throw e;
         }
         catch (ReadFailureException e)
         {
             readMetrics.failures.mark();
             casReadMetrics.failures.mark();
+            readMetricsMap.get(consistencyLevel).failures.mark();
             throw e;
         }
         finally
@@ -1704,6 +1785,7 @@
             long latency = System.nanoTime() - start;
             readMetrics.addNano(latency);
             casReadMetrics.addNano(latency);
+            readMetricsMap.get(consistencyLevel).addNano(latency);
             Keyspace.open(metadata.ksName).getColumnFamilyStore(metadata.cfName).metric.coordinatorReadLatency.update(latency, TimeUnit.NANOSECONDS);
         }
 
@@ -1711,13 +1793,13 @@
     }
 
     @SuppressWarnings("resource")
-    private static PartitionIterator readRegular(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel)
+    private static PartitionIterator readRegular(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, long queryStartNanoTime)
     throws UnavailableException, ReadFailureException, ReadTimeoutException
     {
         long start = System.nanoTime();
         try
         {
-            PartitionIterator result = fetchRows(group.commands, consistencyLevel);
+            PartitionIterator result = fetchRows(group.commands, consistencyLevel, queryStartNanoTime);
             // Note that the only difference between the command in a group must be the partition key on which
             // they applied.
             boolean enforceStrictLiveness = group.commands.get(0).metadata().enforceStrictLiveness();
@@ -1730,22 +1812,26 @@
         catch (UnavailableException e)
         {
             readMetrics.unavailables.mark();
+            readMetricsMap.get(consistencyLevel).unavailables.mark();
             throw e;
         }
         catch (ReadTimeoutException e)
         {
             readMetrics.timeouts.mark();
+            readMetricsMap.get(consistencyLevel).timeouts.mark();
             throw e;
         }
         catch (ReadFailureException e)
         {
             readMetrics.failures.mark();
+            readMetricsMap.get(consistencyLevel).failures.mark();
             throw e;
         }
         finally
         {
             long latency = System.nanoTime() - start;
             readMetrics.addNano(latency);
+            readMetricsMap.get(consistencyLevel).addNano(latency);
             // TODO avoid giving every command the same latency number.  Can fix this in CASSADRA-5329
             for (ReadCommand command : group.commands)
                 Keyspace.openAndGetStore(command.metadata()).metric.coordinatorReadLatency.update(latency, TimeUnit.NANOSECONDS);
@@ -1763,14 +1849,14 @@
      * 4. If the digests (if any) match the data return the data
      * 5. else carry out read repair by getting data from all the nodes.
      */
-    private static PartitionIterator fetchRows(List<SinglePartitionReadCommand> commands, ConsistencyLevel consistencyLevel)
+    private static PartitionIterator fetchRows(List<SinglePartitionReadCommand> commands, ConsistencyLevel consistencyLevel, long queryStartNanoTime)
     throws UnavailableException, ReadFailureException, ReadTimeoutException
     {
         int cmdCount = commands.size();
 
         SinglePartitionReadLifecycle[] reads = new SinglePartitionReadLifecycle[cmdCount];
         for (int i = 0; i < cmdCount; i++)
-            reads[i] = new SinglePartitionReadLifecycle(commands.get(i), consistencyLevel);
+            reads[i] = new SinglePartitionReadLifecycle(commands.get(i), consistencyLevel, queryStartNanoTime);
 
         for (int i = 0; i < cmdCount; i++)
             reads[i].doInitialQueries();
@@ -1800,15 +1886,17 @@
         private final SinglePartitionReadCommand command;
         private final AbstractReadExecutor executor;
         private final ConsistencyLevel consistency;
+        private final long queryStartNanoTime;
 
         private PartitionIterator result;
         private ReadCallback repairHandler;
 
-        SinglePartitionReadLifecycle(SinglePartitionReadCommand command, ConsistencyLevel consistency)
+        SinglePartitionReadLifecycle(SinglePartitionReadCommand command, ConsistencyLevel consistency, long queryStartNanoTime)
         {
             this.command = command;
-            this.executor = AbstractReadExecutor.getReadExecutor(command, consistency);
+            this.executor = AbstractReadExecutor.getReadExecutor(command, consistency, queryStartNanoTime);
             this.consistency = consistency;
+            this.queryStartNanoTime = queryStartNanoTime;
         }
 
         boolean isDone()
@@ -1834,19 +1922,20 @@
             }
             catch (DigestMismatchException ex)
             {
-                Tracing.trace("Digest mismatch: {}", ex);
+                Tracing.trace("Digest mismatch: {}", ex.toString());
 
                 ReadRepairMetrics.repairedBlocking.mark();
 
                 // Do a full data read to resolve the correct response (and repair node that need be)
                 Keyspace keyspace = Keyspace.open(command.metadata().ksName);
-                DataResolver resolver = new DataResolver(keyspace, command, ConsistencyLevel.ALL, executor.handler.endpoints.size());
+                DataResolver resolver = new DataResolver(keyspace, command, ConsistencyLevel.ALL, executor.handler.endpoints.size(), queryStartNanoTime);
                 repairHandler = new ReadCallback(resolver,
                                                  ConsistencyLevel.ALL,
                                                  executor.getContactedReplicas().size(),
                                                  command,
                                                  keyspace,
-                                                 executor.handler.endpoints);
+                                                 executor.handler.endpoints,
+                                                 queryStartNanoTime);
 
                 for (InetAddress endpoint : executor.getContactedReplicas())
                 {
@@ -1909,19 +1998,39 @@
         {
             try
             {
-                try (ReadOrderGroup orderGroup = command.startOrderGroup(); UnfilteredPartitionIterator iterator = command.executeLocally(orderGroup))
+                command.setMonitoringTime(constructionTime, false, verb.getTimeout(), DatabaseDescriptor.getSlowQueryTimeout());
+
+                ReadResponse response;
+                try (ReadExecutionController executionController = command.executionController();
+                     UnfilteredPartitionIterator iterator = command.executeLocally(executionController))
                 {
-                    handler.response(command.createResponse(iterator));
+                    response = command.createResponse(iterator);
                 }
+
+                if (command.complete())
+                {
+                    handler.response(response);
+                }
+                else
+                {
+                    MessagingService.instance().incrementDroppedMessages(verb, System.currentTimeMillis() - constructionTime);
+                    handler.onFailure(FBUtilities.getBroadcastAddress(), RequestFailureReason.UNKNOWN);
+                }
+
                 MessagingService.instance().addLatency(FBUtilities.getBroadcastAddress(), TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
             }
             catch (Throwable t)
             {
-                handler.onFailure(FBUtilities.getBroadcastAddress());
                 if (t instanceof TombstoneOverwhelmingException)
+                {
+                    handler.onFailure(FBUtilities.getBroadcastAddress(), RequestFailureReason.READ_TOO_MANY_TOMBSTONES);
                     logger.error(t.getMessage());
+                }
                 else
+                {
+                    handler.onFailure(FBUtilities.getBroadcastAddress(), RequestFailureReason.UNKNOWN);
                     throw t;
+                }
             }
         }
     }
@@ -2136,6 +2245,7 @@
         private final boolean enforceStrictLiveness;
 
         private final long startTime;
+        private final long queryStartNanoTime;
         private DataLimits.Counter counter;
         private PartitionIterator sentQueryIterator;
 
@@ -2153,7 +2263,8 @@
                                     int maxConcurrencyFactor,
                                     int totalRangeCount,
                                     Keyspace keyspace,
-                                    ConsistencyLevel consistency)
+                                    ConsistencyLevel consistency,
+                                    long queryStartNanoTime)
         {
             this.command = command;
             this.concurrencyFactor = concurrencyFactor;
@@ -2163,6 +2274,7 @@
             this.totalRangeCount = totalRangeCount;
             this.consistency = consistency;
             this.keyspace = keyspace;
+            this.queryStartNanoTime = queryStartNanoTime;
             this.enforceStrictLiveness = command.metadata().enforceStrictLiveness();
         }
 
@@ -2232,20 +2344,28 @@
             int concurrencyFactor = Math.max(1, Math.min(maxConcurrencyFactor, Math.round(remainingRows / rowsPerRange)));
             logger.trace("Didn't get enough response rows; actual rows per range: {}; remaining rows: {}, new concurrent requests: {}",
                          rowsPerRange, remainingRows, concurrencyFactor);
-
             return concurrencyFactor;
         }
 
-        private SingleRangeResponse query(RangeForQuery toQuery)
+        /**
+         * Queries the provided sub-range.
+         *
+         * @param toQuery the subRange to query.
+         * @param isFirst in the case where multiple queries are sent in parallel, whether that's the first query on
+         * that batch or not. The reason it matters is that whe paging queries, the command (more specifically the
+         * {@code DataLimits}) may have "state" information and that state may only be valid for the first query (in
+         * that it's the query that "continues" whatever we're previously queried).
+         */
+        private SingleRangeResponse query(RangeForQuery toQuery, boolean isFirst)
         {
-            PartitionRangeReadCommand rangeCommand = command.forSubRange(toQuery.range);
+            PartitionRangeReadCommand rangeCommand = command.forSubRange(toQuery.range, isFirst);
 
-            DataResolver resolver = new DataResolver(keyspace, rangeCommand, consistency, toQuery.filteredEndpoints.size());
+            DataResolver resolver = new DataResolver(keyspace, rangeCommand, consistency, toQuery.filteredEndpoints.size(), queryStartNanoTime);
 
             int blockFor = consistency.blockFor(keyspace);
             int minResponses = Math.min(toQuery.filteredEndpoints.size(), blockFor);
             List<InetAddress> minimalEndpoints = toQuery.filteredEndpoints.subList(0, minResponses);
-            ReadCallback handler = new ReadCallback(resolver, consistency, rangeCommand, minimalEndpoints);
+            ReadCallback handler = new ReadCallback(resolver, consistency, rangeCommand, minimalEndpoints, queryStartNanoTime);
 
             handler.assureSufficientLiveNodes();
 
@@ -2272,7 +2392,7 @@
             for (int i = 0; i < concurrencyFactor && ranges.hasNext();)
             {
                 RangeForQuery range = ranges.next();
-                concurrentQueries.add(query(range));
+                concurrentQueries.add(query(range, i == 0));
                 rangesQueried += range.vnodeCount();
                 i += range.vnodeCount();
             }
@@ -2314,7 +2434,7 @@
     }
 
     @SuppressWarnings("resource")
-    public static PartitionIterator getRangeSlice(PartitionRangeReadCommand command, ConsistencyLevel consistencyLevel)
+    public static PartitionIterator getRangeSlice(PartitionRangeReadCommand command, ConsistencyLevel consistencyLevel, long queryStartNanoTime)
     {
         Tracing.trace("Computing ranges to query");
 
@@ -2342,7 +2462,8 @@
                                                                              maxConcurrencyFactor,
                                                                              ranges.rangeCount(),
                                                                              keyspace,
-                                                                             consistencyLevel);
+                                                                             consistencyLevel,
+                                                                             queryStartNanoTime);
         return command.limits().filter(command.postReconciliationProcessing(rangeCommandIterator),
                                        command.nowInSec(),
                                        command.selectsFullPartition(),
@@ -2551,9 +2672,8 @@
      * @param cfname
      * @throws UnavailableException If some of the hosts in the ring are down.
      * @throws TimeoutException
-     * @throws IOException
      */
-    public static void truncateBlocking(String keyspace, String cfname) throws UnavailableException, TimeoutException, IOException
+    public static void truncateBlocking(String keyspace, String cfname) throws UnavailableException, TimeoutException
     {
         logger.debug("Starting a blocking truncate operation on keyspace {}, CF {}", keyspace, cfname);
         if (isAnyStorageHostDown())
@@ -2613,9 +2733,9 @@
      */
     private static class ViewWriteMetricsWrapped extends BatchlogResponseHandler<IMutation>
     {
-        public ViewWriteMetricsWrapped(AbstractWriteResponseHandler<IMutation> writeHandler, int i, BatchlogCleanup cleanup)
+        public ViewWriteMetricsWrapped(AbstractWriteResponseHandler<IMutation> writeHandler, int i, BatchlogCleanup cleanup, long queryStartNanoTime)
         {
-            super(writeHandler, i, cleanup);
+            super(writeHandler, i, cleanup, queryStartNanoTime);
             viewWriteMetrics.viewReplicasAttempted.inc(totalEndpoints());
         }
 
@@ -2631,20 +2751,21 @@
      */
     private static abstract class DroppableRunnable implements Runnable
     {
-        private final long constructionTime = System.nanoTime();
-        private final MessagingService.Verb verb;
+        final long constructionTime;
+        final MessagingService.Verb verb;
 
         public DroppableRunnable(MessagingService.Verb verb)
         {
+            this.constructionTime = System.currentTimeMillis();
             this.verb = verb;
         }
 
         public final void run()
         {
-
-            if (TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - constructionTime) > DatabaseDescriptor.getTimeout(verb))
+            long timeTaken = System.currentTimeMillis() - constructionTime;
+            if (timeTaken > verb.getTimeout())
             {
-                MessagingService.instance().incrementDroppedMessages(verb);
+                MessagingService.instance().incrementDroppedMessages(verb, timeTaken);
                 return;
             }
             try
@@ -2657,11 +2778,6 @@
             }
         }
 
-        protected MessagingService.Verb verb()
-        {
-            return verb;
-        }
-
         abstract protected void runMayThrow() throws Exception;
     }
 
@@ -2673,13 +2789,27 @@
     {
         private final long constructionTime = System.currentTimeMillis();
 
+        private final Optional<IMutation> mutationOpt;
+
+        public LocalMutationRunnable(Optional<IMutation> mutationOpt)
+        {
+            this.mutationOpt = mutationOpt;
+        }
+
+        public LocalMutationRunnable()
+        {
+            this.mutationOpt = Optional.empty();
+        }
+
         public final void run()
         {
             final MessagingService.Verb verb = verb();
-            if (System.currentTimeMillis() > constructionTime + DatabaseDescriptor.getTimeout(verb))
+            long mutationTimeout = verb.getTimeout();
+            long timeTaken = System.currentTimeMillis() - constructionTime;
+            if (timeTaken > mutationTimeout)
             {
-                if (MessagingService.DROPPABLE_VERBS.contains(verb()))
-                    MessagingService.instance().incrementDroppedMessages(verb);
+                if (MessagingService.DROPPABLE_VERBS.contains(verb))
+                    MessagingService.instance().incrementDroppedMutations(mutationOpt, timeTaken);
                 HintRunnable runnable = new HintRunnable(Collections.singleton(FBUtilities.getBroadcastAddress()))
                 {
                     protected void runMayThrow() throws Exception
@@ -2852,18 +2982,26 @@
 
     public void reloadTriggerClasses() { TriggerExecutor.instance.reloadClasses(); }
 
-    public long getReadRepairAttempted() {
+    public long getReadRepairAttempted()
+    {
         return ReadRepairMetrics.attempted.getCount();
     }
 
-    public long getReadRepairRepairedBlocking() {
+    public long getReadRepairRepairedBlocking()
+    {
         return ReadRepairMetrics.repairedBlocking.getCount();
     }
 
-    public long getReadRepairRepairedBackground() {
+    public long getReadRepairRepairedBackground()
+    {
         return ReadRepairMetrics.repairedBackground.getCount();
     }
 
+    public int getNumberOfTables()
+    {
+        return Schema.instance.getNumberOfTables();
+    }
+
     public int getOtcBacklogExpirationInterval() {
         return DatabaseDescriptor.getOtcBacklogExpirationInterval();
     }
diff --git a/src/java/org/apache/cassandra/service/StorageProxyMBean.java b/src/java/org/apache/cassandra/service/StorageProxyMBean.java
index 9ec8e64..f4367d3 100644
--- a/src/java/org/apache/cassandra/service/StorageProxyMBean.java
+++ b/src/java/org/apache/cassandra/service/StorageProxyMBean.java
@@ -65,6 +65,8 @@
     /** Returns each live node's schema version */
     public Map<String, List<String>> getSchemaVersions();
 
+    public int getNumberOfTables();
+
     void enableSnapshotOnDuplicateRowDetection();
     void disableSnapshotOnDuplicateRowDetection();
     boolean getSnapshotOnDuplicateRowDetectionEnabled();
diff --git a/src/java/org/apache/cassandra/service/StorageService.java b/src/java/org/apache/cassandra/service/StorageService.java
index 9a37593..ba5ebc0 100644
--- a/src/java/org/apache/cassandra/service/StorageService.java
+++ b/src/java/org/apache/cassandra/service/StorageService.java
@@ -22,7 +22,6 @@
 import java.io.File;
 import java.io.IOError;
 import java.io.IOException;
-import java.lang.management.ManagementFactory;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
@@ -40,6 +39,7 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
+import java.util.Scanner;
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
@@ -55,11 +55,12 @@
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.MatchResult;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 import javax.annotation.Nullable;
-import javax.management.JMX;
 import javax.management.NotificationBroadcasterSupport;
-import javax.management.ObjectName;
 import javax.management.openmbean.TabularData;
 import javax.management.openmbean.TabularDataSupport;
 
@@ -84,11 +85,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.jmx.JMXConfiguratorMBean;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.Appender;
-import ch.qos.logback.core.hook.DelayingShutdownHook;
 import org.apache.cassandra.auth.AuthKeyspace;
 import org.apache.cassandra.auth.AuthMigrationListener;
 import org.apache.cassandra.batchlog.BatchRemoveVerbHandler;
@@ -102,6 +98,7 @@
 import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.config.ViewDefinition;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.CounterMutationVerbHandler;
@@ -148,6 +145,7 @@
 import org.apache.cassandra.hints.HintVerbHandler;
 import org.apache.cassandra.hints.HintsService;
 import org.apache.cassandra.io.sstable.SSTableLoader;
+import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.VersionAndType;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.locator.AbstractReplicationStrategy;
@@ -165,7 +163,9 @@
 import org.apache.cassandra.repair.RepairRunnable;
 import org.apache.cassandra.repair.SystemDistributedKeyspace;
 import org.apache.cassandra.repair.messages.RepairOption;
+import org.apache.cassandra.schema.CompactionParams.TombstoneOption;
 import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.ReplicationParams;
 import org.apache.cassandra.schema.SchemaKeyspace;
 import org.apache.cassandra.service.paxos.CommitVerbHandler;
 import org.apache.cassandra.service.paxos.PrepareVerbHandler;
@@ -179,14 +179,16 @@
 import org.apache.cassandra.thrift.TokenRange;
 import org.apache.cassandra.thrift.cassandraConstants;
 import org.apache.cassandra.tracing.TraceKeyspace;
-import org.apache.cassandra.utils.BackgroundActivityMonitor;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.MBeanWrapper;
 import org.apache.cassandra.utils.OutputHandler;
 import org.apache.cassandra.utils.Pair;
+import org.apache.cassandra.utils.Throwables;
 import org.apache.cassandra.utils.WindowsTimer;
 import org.apache.cassandra.utils.WrappedRunnable;
+import org.apache.cassandra.utils.logging.LoggingSupportFactory;
 import org.apache.cassandra.utils.progress.ProgressEvent;
 import org.apache.cassandra.utils.progress.ProgressEventType;
 import org.apache.cassandra.utils.progress.jmx.JMXProgressSupport;
@@ -223,6 +225,8 @@
     @Deprecated
     private final LegacyJMXProgressSupport legacyProgressSupport;
 
+    private static final AtomicInteger threadCounter = new AtomicInteger(1);
+
     private static int getRingDelay()
     {
         String newdelay = System.getProperty("cassandra.ring_delay_ms");
@@ -240,7 +244,7 @@
         String newdelay = System.getProperty("cassandra.schema_delay_ms");
         if (newdelay != null)
         {
-            logger.info("Overriding SCHEMA_DELAY to {}ms", newdelay);
+            logger.info("Overriding SCHEMA_DELAY_MILLIS to {}ms", newdelay);
             return Integer.parseInt(newdelay);
         }
         else
@@ -256,6 +260,8 @@
 
     private Thread drainOnShutdown = null;
     private volatile boolean isShutdown = false;
+    private final List<Runnable> preShutdownHooks = new ArrayList<>();
+    private final List<Runnable> postShutdownHooks = new ArrayList<>();
 
     public static final StorageService instance = new StorageService();
 
@@ -303,15 +309,16 @@
     private volatile boolean isBootstrapMode;
 
     /* we bootstrap but do NOT join the ring unless told to do so */
-    private boolean isSurveyMode = Boolean.parseBoolean(System.getProperty
-            ("cassandra.write_survey", "false"));
-
+    private boolean isSurveyMode = Boolean.parseBoolean(System.getProperty("cassandra.write_survey", "false"));
     /* true if node is rebuilding and receiving data */
     private final AtomicBoolean isRebuilding = new AtomicBoolean();
+    private final AtomicBoolean isDecommissioning = new AtomicBoolean();
 
-    private boolean initialized;
+    private volatile boolean initialized = false;
     private volatile boolean joined = false;
+    private volatile boolean gossipActive = false;
     private final AtomicBoolean authSetupCalled = new AtomicBoolean(false);
+    private volatile boolean authSetupComplete = false;
 
     /* the probability for tracing any particular request, 0 disables tracing and 1 enables for all */
     private double traceProbability = 0.0;
@@ -326,18 +333,15 @@
 
     private final List<IEndpointLifecycleSubscriber> lifecycleSubscribers = new CopyOnWriteArrayList<>();
 
-    private static final BackgroundActivityMonitor bgMonitor = new BackgroundActivityMonitor();
-
     private final String jmxObjectName;
 
     private Collection<Token> bootstrapTokens = null;
 
     // true when keeping strict consistency while bootstrapping
-    private boolean useStrictConsistency = Boolean.parseBoolean(System.getProperty("cassandra.consistent.rangemovement", "true"));
-    private static final boolean allowSimultaneousMoves = Boolean.valueOf(System.getProperty("cassandra.consistent.simultaneousmoves.allow","false"));
+    private static final boolean useStrictConsistency = Boolean.parseBoolean(System.getProperty("cassandra.consistent.rangemovement", "true"));
+    private static final boolean allowSimultaneousMoves = Boolean.parseBoolean(System.getProperty("cassandra.consistent.simultaneousmoves.allow","false"));
     private static final boolean joinRing = Boolean.parseBoolean(System.getProperty("cassandra.join_ring", "true"));
     private boolean replacing;
-    private UUID replacingId;
 
     private final StreamStateStore streamStateStore = new StreamStateStore();
 
@@ -419,7 +423,7 @@
         MessagingService.instance().registerVerbHandlers(MessagingService.Verb.BATCH_STORE, new BatchStoreVerbHandler());
         MessagingService.instance().registerVerbHandlers(MessagingService.Verb.BATCH_REMOVE, new BatchRemoveVerbHandler());
 
-        sstablesTracker = new SSTablesGlobalTracker(DatabaseDescriptor.getSSTableFormat());
+        sstablesTracker = new SSTablesGlobalTracker(SSTableFormat.Type.current());
     }
 
     public void registerDaemon(CassandraDaemon daemon)
@@ -440,7 +444,7 @@
     // should only be called via JMX
     public void stopGossiping()
     {
-        if (initialized)
+        if (gossipActive)
         {
             if (!isNormal() && joinRing)
                 throw new IllegalStateException("Unable to stop gossip because the node is not in the normal state. Try to stop the node instead.");
@@ -453,14 +457,14 @@
             }
 
             Gossiper.instance.stop();
-            initialized = false;
+            gossipActive = false;
         }
     }
 
     // should only be called via JMX
     public synchronized void startGossiping()
     {
-        if (!initialized)
+        if (!gossipActive)
         {
             checkServiceAllowedToStart("gossip");
 
@@ -478,7 +482,7 @@
 
             Gossiper.instance.forceNewerGeneration();
             Gossiper.instance.start((int) (System.currentTimeMillis() / 1000));
-            initialized = true;
+            gossipActive = true;
         }
     }
 
@@ -501,23 +505,17 @@
         // We only start transports if bootstrap has completed and we're not in survey mode, OR if we are in
         // survey mode and streaming has completed but we're not using auth.
         // OR if we have not joined the ring yet.
-        if (StorageService.instance.hasJoined())
+        if (StorageService.instance.hasJoined() &&
+                ((!StorageService.instance.isSurveyMode() && !SystemKeyspace.bootstrapComplete()) ||
+                (StorageService.instance.isSurveyMode() && StorageService.instance.isBootstrapMode())))
         {
-            if (StorageService.instance.isSurveyMode())
-            {
-                if (StorageService.instance.isBootstrapMode() || DatabaseDescriptor.getAuthenticator().requireAuthentication())
-                {
-                    throw new IllegalStateException("Not starting RPC server in write_survey mode as it's bootstrapping or " +
-                            "auth is enabled");
-                }
-            }
-            else
-            {
-                if (!SystemKeyspace.bootstrapComplete())
-                {
-                    throw new IllegalStateException("Node is not yet bootstrapped completely. Use nodetool to check bootstrap state and resume. For more, see `nodetool help bootstrap`");
-                }
-            }
+            throw new IllegalStateException("Node is not yet bootstrapped completely. Use nodetool to check bootstrap state and resume. For more, see `nodetool help bootstrap`");
+        }
+        else if (StorageService.instance.hasJoined() && StorageService.instance.isSurveyMode() &&
+                DatabaseDescriptor.getAuthenticator().requireAuthentication())
+        {
+            // Auth isn't initialised until we join the ring, so if we're in survey mode auth will always fail.
+            throw new IllegalStateException("Not starting RPC server as write_survey mode and authentication is enabled");
         }
 
         daemon.startThriftServer();
@@ -607,7 +605,7 @@
             logger.error("Stopping native transport");
             stopNativeTransport();
         }
-        if (isInitialized())
+        if (isGossipActive())
         {
             logger.error("Stopping gossiper");
             stopGossiping();
@@ -646,7 +644,12 @@
         return initialized;
     }
 
-    public boolean isSetupCompleted()
+    public boolean isGossipActive()
+    {
+        return gossipActive;
+    }
+
+    public boolean isDaemonSetupCompleted()
     {
         return daemon == null
                ? false
@@ -660,11 +663,13 @@
         daemon.deactivate();
     }
 
-    public synchronized Collection<Token> prepareReplacementInfo() throws ConfigurationException
+    private synchronized UUID prepareForReplacement() throws ConfigurationException
     {
-        logger.info("Gathering node replacement information for {}", DatabaseDescriptor.getReplaceAddress());
-        if (!MessagingService.instance().isListening())
-            MessagingService.instance().listen();
+        if (SystemKeyspace.bootstrapComplete())
+            throw new RuntimeException("Cannot replace address with a node that is already bootstrapped");
+
+        if (!joinRing)
+            throw new ConfigurationException("Cannot set both join_ring=false and attempt to replace a node");
 
         if (!shouldBootstrap() && !Boolean.getBoolean("cassandra.allow_unsafe_replace"))
             throw new RuntimeException("Replacing a node without bootstrapping risks invalidating consistency " +
@@ -672,44 +677,63 @@
                                        "To perform this operation, please restart with " +
                                        "-Dcassandra.allow_unsafe_replace=true");
 
-        // make magic happen
+        InetAddress replaceAddress = DatabaseDescriptor.getReplaceAddress();
+        logger.info("Gathering node replacement information for {}", replaceAddress);
         Map<InetAddress, EndpointState> epStates = Gossiper.instance.doShadowRound();
-        // now that we've gossiped at least once, we should be able to find the node we're replacing
-        if (epStates.get(DatabaseDescriptor.getReplaceAddress())== null)
-            throw new RuntimeException("Cannot replace_address " + DatabaseDescriptor.getReplaceAddress() + " because it doesn't exist in gossip");
-        replacingId = Gossiper.instance.getHostId(DatabaseDescriptor.getReplaceAddress(), epStates);
+        // as we've completed the shadow round of gossip, we should be able to find the node we're replacing
+        if (epStates.get(replaceAddress) == null)
+            throw new RuntimeException(String.format("Cannot replace_address %s because it doesn't exist in gossip", replaceAddress));
+
         try
         {
-            VersionedValue tokensVersionedValue = epStates.get(DatabaseDescriptor.getReplaceAddress()).getApplicationState(ApplicationState.TOKENS);
+            VersionedValue tokensVersionedValue = epStates.get(replaceAddress).getApplicationState(ApplicationState.TOKENS);
             if (tokensVersionedValue == null)
-                throw new RuntimeException("Could not find tokens for " + DatabaseDescriptor.getReplaceAddress() + " to replace");
-            Collection<Token> tokens = TokenSerializer.deserialize(tokenMetadata.partitioner, new DataInputStream(new ByteArrayInputStream(tokensVersionedValue.toBytes())));
+                throw new RuntimeException(String.format("Could not find tokens for %s to replace", replaceAddress));
 
-            if (isReplacingSameAddress())
-            {
-                SystemKeyspace.setLocalHostId(replacingId); // use the replacee's host Id as our own so we receive hints, etc
-            }
-            return tokens;
+            bootstrapTokens = TokenSerializer.deserialize(tokenMetadata.partitioner, new DataInputStream(new ByteArrayInputStream(tokensVersionedValue.toBytes())));
         }
         catch (IOException e)
         {
             throw new RuntimeException(e);
         }
+
+        UUID localHostId = SystemKeyspace.getOrInitializeLocalHostId();
+
+        if (isReplacingSameAddress())
+        {
+            localHostId = Gossiper.instance.getHostId(replaceAddress, epStates);
+            SystemKeyspace.setLocalHostId(localHostId); // use the replacee's host Id as our own so we receive hints, etc
+        }
+
+        return localHostId;
     }
 
-    public synchronized void checkForEndpointCollision() throws ConfigurationException
+    public synchronized void checkForEndpointCollision(UUID localHostId, Set<InetAddress> peers) throws ConfigurationException
     {
+        if (Boolean.getBoolean("cassandra.allow_unsafe_join"))
+        {
+            logger.warn("Skipping endpoint collision check as cassandra.allow_unsafe_join=true");
+            return;
+        }
+
         logger.debug("Starting shadow gossip round to check for endpoint collision");
-        if (!MessagingService.instance().isListening())
-            MessagingService.instance().listen();
-        Map<InetAddress, EndpointState> epStates = Gossiper.instance.doShadowRound();
-        if (!Gossiper.instance.isSafeForBootstrap(FBUtilities.getBroadcastAddress(), epStates))
+        Map<InetAddress, EndpointState> epStates = Gossiper.instance.doShadowRound(peers);
+
+        if (epStates.isEmpty() && DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddress()))
+            logger.info("Unable to gossip with any peers but continuing anyway since node is in its own seed list");
+
+        // If bootstrapping, check whether any previously known status for the endpoint makes it unsafe to do so.
+        // If not bootstrapping, compare the host id for this endpoint learned from gossip (if any) with the local
+        // one, which was either read from system.local or generated at startup. If a learned id is present &
+        // doesn't match the local, then the node needs replacing
+        if (!Gossiper.instance.isSafeForStartup(FBUtilities.getBroadcastAddress(), localHostId, shouldBootstrap(), epStates))
         {
             throw new RuntimeException(String.format("A node with address %s already exists, cancelling join. " +
                                                      "Use cassandra.replace_address if you want to replace this node.",
                                                      FBUtilities.getBroadcastAddress()));
         }
-        if (useStrictConsistency && !allowSimultaneousMoves())
+
+        if (shouldBootstrap() && useStrictConsistency && !allowSimultaneousMoves())
         {
             for (Map.Entry<InetAddress, EndpointState> entry : epStates.entrySet())
             {
@@ -734,6 +758,7 @@
     public void unsafeInitialize() throws ConfigurationException
     {
         initialized = true;
+        gossipActive = true;
         Gossiper.instance.register(this);
         Gossiper.instance.start((int) (System.currentTimeMillis() / 1000)); // needed for node-ring gathering.
         Gossiper.instance.addLocalApplicationState(ApplicationState.NET_VERSION, valueFactory.networkVersion());
@@ -766,9 +791,9 @@
         logger.info("Cassandra version: {}", FBUtilities.getReleaseVersionString());
         logger.info("Thrift API version: {}", cassandraConstants.VERSION);
         logger.info("CQL supported versions: {} (default: {})",
-                StringUtils.join(ClientState.getCQLSupportedVersion(), ","), ClientState.DEFAULT_CQL_VERSION);
-
-        initialized = true;
+                StringUtils.join(ClientState.getCQLSupportedVersion(), ", "), ClientState.DEFAULT_CQL_VERSION);
+        logger.info("Native protocol supported versions: {} (default: {})",
+                    StringUtils.join(ProtocolVersion.supportedVersions(), ", "), ProtocolVersion.CURRENT);
 
         try
         {
@@ -782,56 +807,30 @@
             throw new AssertionError(e);
         }
 
-        if (Boolean.parseBoolean(System.getProperty("cassandra.load_ring_state", "true")))
-        {
-            logger.info("Loading persisted ring state");
-            Multimap<InetAddress, Token> loadedTokens = SystemKeyspace.loadTokens();
-            Map<InetAddress, UUID> loadedHostIds = SystemKeyspace.loadHostIds();
-            for (InetAddress ep : loadedTokens.keySet())
-            {
-                if (ep.equals(FBUtilities.getBroadcastAddress()))
-                {
-                    // entry has been mistakenly added, delete it
-                    SystemKeyspace.removeEndpoint(ep);
-                }
-                else
-                {
-                    if (loadedHostIds.containsKey(ep))
-                        tokenMetadata.updateHostId(loadedHostIds.get(ep), ep);
-                    Gossiper.runInGossipStageBlocking(() -> Gossiper.instance.addSavedEndpoint(ep));
-                }
-            }
-        }
-
         // daemon threads, like our executors', continue to run while shutdown hooks are invoked
-        drainOnShutdown = new Thread(NamedThreadFactory.threadLocalDeallocator(new WrappedRunnable()
+        drainOnShutdown = NamedThreadFactory.createThread(new WrappedRunnable()
         {
             @Override
             public void runMayThrow() throws InterruptedException, ExecutionException, IOException
             {
                 drain(true);
 
-                if (FBUtilities.isWindows())
+                if (FBUtilities.isWindows)
                     WindowsTimer.endTimerPeriod(DatabaseDescriptor.getWindowsTimerInterval());
 
-                // Cleanup logback
-                DelayingShutdownHook logbackHook = new DelayingShutdownHook();
-                logbackHook.setContext((LoggerContext)LoggerFactory.getILoggerFactory());
-                logbackHook.run();
-
-                // wait for miscellaneous tasks like sstable and commitlog segment deletion
-                ScheduledExecutors.nonPeriodicTasks.shutdown();
-                if (!ScheduledExecutors.nonPeriodicTasks.awaitTermination(1, MINUTES))
-                    logger.warn("Miscellaneous task executor still busy after one minute; proceeding with shutdown");
+                LoggingSupportFactory.getLoggingSupport().onShutdown();
             }
-        }), "StorageServiceShutdownHook");
+        }, "StorageServiceShutdownHook");
         Runtime.getRuntime().addShutdownHook(drainOnShutdown);
 
-        replacing = DatabaseDescriptor.isReplacing();
+        replacing = isReplacing();
 
         if (!Boolean.parseBoolean(System.getProperty("cassandra.start_gossip", "true")))
         {
             logger.info("Not starting gossip as requested.");
+            // load ring state in preparation for starting gossip later
+            loadRingState();
+            initialized = true;
             return;
         }
 
@@ -867,6 +866,42 @@
             doAuthSetup(true);
             logger.info("Not joining ring as requested. Use JMX (StorageService->joinRing()) to initiate ring joining");
         }
+
+        initialized = true;
+    }
+
+    private void loadRingState()
+    {
+        if (Boolean.parseBoolean(System.getProperty("cassandra.load_ring_state", "true")))
+        {
+            logger.info("Loading persisted ring state");
+            Multimap<InetAddress, Token> loadedTokens = SystemKeyspace.loadTokens();
+            Map<InetAddress, UUID> loadedHostIds = SystemKeyspace.loadHostIds();
+            for (InetAddress ep : loadedTokens.keySet())
+            {
+                if (ep.equals(FBUtilities.getBroadcastAddress()))
+                {
+                    // entry has been mistakenly added, delete it
+                    SystemKeyspace.removeEndpoint(ep);
+                }
+                else
+                {
+                    if (loadedHostIds.containsKey(ep))
+                        tokenMetadata.updateHostId(loadedHostIds.get(ep), ep);
+                    Gossiper.runInGossipStageBlocking(() -> Gossiper.instance.addSavedEndpoint(ep));
+                }
+            }
+        }
+    }
+
+    private boolean isReplacing()
+    {
+        if (System.getProperty("cassandra.replace_address_first_boot", null) != null && SystemKeyspace.bootstrapComplete())
+        {
+            logger.info("Replace address on first boot requested; this node is already bootstrapped");
+            return false;
+        }
+        return DatabaseDescriptor.getReplaceAddress() != null;
     }
 
     /**
@@ -877,13 +912,18 @@
         if (drainOnShutdown != null)
             Runtime.getRuntime().removeShutdownHook(drainOnShutdown);
 
-        if (FBUtilities.isWindows())
+        if (FBUtilities.isWindows)
             WindowsTimer.endTimerPeriod(DatabaseDescriptor.getWindowsTimerInterval());
     }
 
     private boolean shouldBootstrap()
     {
-        return DatabaseDescriptor.isAutoBootstrap() && !SystemKeyspace.bootstrapComplete() && !DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddress());
+        return DatabaseDescriptor.isAutoBootstrap() && !SystemKeyspace.bootstrapComplete() && !isSeed();
+    }
+
+    public static boolean isSeed()
+    {
+        return DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddress());
     }
 
     @VisibleForTesting
@@ -904,51 +944,55 @@
                 else
                     throw new ConfigurationException("This node was decommissioned and will not rejoin the ring unless cassandra.override_decommission=true has been set, or all existing data is removed and the node is bootstrapped again");
             }
-            if (replacing && !joinRing)
-                throw new ConfigurationException("Cannot set both join_ring=false and attempt to replace a node");
+
             if (DatabaseDescriptor.getReplaceTokens().size() > 0 || DatabaseDescriptor.getReplaceNode() != null)
                 throw new RuntimeException("Replace method removed; use cassandra.replace_address instead");
+
+            if (!MessagingService.instance().isListening())
+                MessagingService.instance().listen();
+
+            UUID localHostId = SystemKeyspace.getOrInitializeLocalHostId();
+
             if (replacing)
             {
-                if (SystemKeyspace.bootstrapComplete())
-                    throw new RuntimeException("Cannot replace address with a node that is already bootstrapped");
-                bootstrapTokens = prepareReplacementInfo();
+                localHostId = prepareForReplacement();
+                appStates.put(ApplicationState.TOKENS, valueFactory.tokens(bootstrapTokens));
+
                 if (!shouldBootstrap())
                 {
                     // Will not do replace procedure, persist the tokens we're taking over locally
                     // so that they don't get clobbered with auto generated ones in joinTokenRing
                     SystemKeyspace.updateTokens(bootstrapTokens);
                 }
-                if (isReplacingSameAddress())
+                else if (isReplacingSameAddress())
                 {
+                    //only go into hibernate state if replacing the same address (CASSANDRA-8523)
                     logger.warn("Writes will not be forwarded to this node during replacement because it has the same address as " +
                                 "the node to be replaced ({}). If the previous node has been down for longer than max_hint_window_in_ms, " +
                                 "repair must be run after the replacement process in order to make this node consistent.",
                                 DatabaseDescriptor.getReplaceAddress());
-                    appStates.put(ApplicationState.TOKENS, valueFactory.tokens(bootstrapTokens));
                     appStates.put(ApplicationState.STATUS, valueFactory.hibernate(true));
                 }
                 MigrationCoordinator.instance.removeAndIgnoreEndpoint(DatabaseDescriptor.getReplaceAddress());
             }
-            else if (shouldBootstrap())
+            else
             {
-                checkForEndpointCollision();
-            }
-            else if (SystemKeyspace.bootstrapComplete())
-            {
-                Preconditions.checkState(!Config.isClientMode());
-                // tokens are only ever saved to system.local after bootstrap has completed and we're joining the ring,
-                // or when token update operations (move, decom) are completed
-                Collection<Token> savedTokens = SystemKeyspace.getSavedTokens();
-                if (!savedTokens.isEmpty())
-                    appStates.put(ApplicationState.TOKENS, valueFactory.tokens(savedTokens));
+                checkForEndpointCollision(localHostId, SystemKeyspace.loadHostIds().keySet());
+                if (SystemKeyspace.bootstrapComplete())
+                {
+                    Preconditions.checkState(!Config.isClientMode());
+                    // tokens are only ever saved to system.local after bootstrap has completed and we're joining the ring,
+                    // or when token update operations (move, decom) are completed
+                    Collection<Token> savedTokens = SystemKeyspace.getSavedTokens();
+                    if (!savedTokens.isEmpty())
+                        appStates.put(ApplicationState.TOKENS, valueFactory.tokens(savedTokens));
+                }
             }
 
             // have to start the gossip service before we can see any info on other nodes.  this is necessary
             // for bootstrap to get the load info it needs.
             // (we won't be part of the storage ring though until we add a counterId to our state, below.)
             // Seed the host ID-to-endpoint map with our own ID.
-            UUID localHostId = SystemKeyspace.getOrInitializeLocalHostId();
             getTokenMetadata().updateHostId(localHostId, FBUtilities.getBroadcastAddress());
             appStates.put(ApplicationState.NET_VERSION, valueFactory.networkVersion());
             appStates.put(ApplicationState.HOST_ID, valueFactory.hostId(localHostId));
@@ -956,9 +1000,15 @@
             appStates.put(ApplicationState.RELEASE_VERSION, valueFactory.releaseVersion());
             appStates.put(ApplicationState.SSTABLE_VERSIONS, valueFactory.sstableVersions(sstablesTracker.versionsInUse()));
 
+            // load the persisted ring state. This used to be done earlier in the init process,
+            // but now we always perform a shadow round when preparing to join and we have to
+            // clear endpoint states after doing that.
+            loadRingState();
+
             logger.info("Starting up server gossip");
             Gossiper.instance.register(this);
             Gossiper.instance.start(SystemKeyspace.incrementAndGetGeneration(), appStates); // needed for node-ring gathering.
+            gossipActive = true;
 
             sstablesTracker.register((notification, o) -> {
                 if (!(notification instanceof SSTablesVersionsInUseChangeNotification))
@@ -975,16 +1025,13 @@
             gossipSnitchInfo();
             // gossip Schema.emptyVersion forcing immediate check for schema updates (see MigrationManager#maybeScheduleSchemaPull)
             Schema.instance.updateVersionAndAnnounce(); // Ensure we know our own actual Schema UUID in preparation for updates
-
-            if (!MessagingService.instance().isListening())
-                MessagingService.instance().listen();
             LoadBroadcaster.instance.startBroadcasting();
-
             HintsService.instance.startDispatch();
             BatchlogManager.instance.start();
         }
     }
 
+
     public void waitForSchema(int delay)
     {
         // first sleep the delay to make sure we see all our peers
@@ -1017,7 +1064,7 @@
     }
 
     @VisibleForTesting
-    public void joinTokenRing(int delay) throws ConfigurationException
+    private void joinTokenRing(int delay) throws ConfigurationException
     {
         joined = true;
 
@@ -1045,24 +1092,14 @@
         }
 
         boolean dataAvailable = true; // make this to false when bootstrap streaming failed
-        if (shouldBootstrap())
+        boolean bootstrap = shouldBootstrap();
+        if (bootstrap)
         {
             if (SystemKeyspace.bootstrapInProgress())
                 logger.warn("Detected previous bootstrap failure; retrying");
             else
                 SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.IN_PROGRESS);
             setMode(Mode.JOINING, "waiting for ring information", true);
-            // first sleep the delay to make sure we see all our peers
-            for (int i = 0; i < delay; i += 1000)
-            {
-                // if we see schema, we can proceed to the next check directly
-                if (!Schema.instance.getVersion().equals(Schema.emptyVersion))
-                {
-                    logger.debug("got schema: {}", Schema.instance.getVersion());
-                    break;
-                }
-                Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
-            }
             waitForSchema(delay);
             setMode(Mode.JOINING, "schema complete, ready to bootstrap", true);
             setMode(Mode.JOINING, "waiting for pending range calculation", true);
@@ -1078,7 +1115,10 @@
                         tokenMetadata.getMovingEndpoints().size() > 0
                     ))
             {
-                throw new UnsupportedOperationException("Other bootstrapping/leaving/moving nodes detected, cannot bootstrap while cassandra.consistent.rangemovement is true");
+                String bootstrapTokens = StringUtils.join(tokenMetadata.getBootstrapTokens().valueSet(), ',');
+                String leavingTokens = StringUtils.join(tokenMetadata.getLeavingEndpoints(), ',');
+                String movingTokens = StringUtils.join(tokenMetadata.getMovingEndpoints().stream().map(e -> e.right).toArray(), ',');
+                throw new UnsupportedOperationException(String.format("Other bootstrapping/leaving/moving nodes detected, cannot bootstrap while cassandra.consistent.rangemovement is true. Nodes detected, bootstrapping: %s; leaving: %s; moving: %s;", bootstrapTokens, leavingTokens, movingTokens));
             }
 
             // get bootstrap tokens
@@ -1090,7 +1130,7 @@
                     throw new UnsupportedOperationException(s);
                 }
                 setMode(Mode.JOINING, "getting bootstrap token", true);
-                bootstrapTokens = BootStrapper.getBootstrapTokens(tokenMetadata, FBUtilities.getBroadcastAddress());
+                bootstrapTokens = BootStrapper.getBootstrapTokens(tokenMetadata, FBUtilities.getBroadcastAddress(), delay);
             }
             else
             {
@@ -1146,22 +1186,7 @@
             bootstrapTokens = SystemKeyspace.getSavedTokens();
             if (bootstrapTokens.isEmpty())
             {
-                Collection<String> initialTokens = DatabaseDescriptor.getInitialTokens();
-                if (initialTokens.size() < 1)
-                {
-                    bootstrapTokens = BootStrapper.getRandomTokens(tokenMetadata, DatabaseDescriptor.getNumTokens());
-                    if (DatabaseDescriptor.getNumTokens() == 1)
-                        logger.warn("Generated random token {}. Random tokens will result in an unbalanced ring; see http://wiki.apache.org/cassandra/Operations", bootstrapTokens);
-                    else
-                        logger.info("Generated random tokens. tokens are {}", bootstrapTokens);
-                }
-                else
-                {
-                    bootstrapTokens = new ArrayList<>(initialTokens.size());
-                    for (String token : initialTokens)
-                        bootstrapTokens.add(getTokenFactory().fromString(token));
-                    logger.info("Saved tokens not found. Using configuration value: {}", bootstrapTokens);
-                }
+                bootstrapTokens = BootStrapper.getBootstrapTokens(tokenMetadata, FBUtilities.getBroadcastAddress(), delay);
             }
             else
             {
@@ -1178,8 +1203,7 @@
         {
             if (dataAvailable)
             {
-                finishJoiningRing(bootstrapTokens);
-
+                finishJoiningRing(bootstrap, bootstrapTokens);
                 // remove the existing info about the replaced node.
                 if (!current.isEmpty())
                 {
@@ -1212,7 +1236,8 @@
 
     public static boolean isReplacingSameAddress()
     {
-        return DatabaseDescriptor.getReplaceAddress().equals(FBUtilities.getBroadcastAddress());
+        InetAddress replaceAddress = DatabaseDescriptor.getReplaceAddress();
+        return replaceAddress != null && replaceAddress.equals(FBUtilities.getBroadcastAddress());
     }
 
     public void gossipSnitchInfo()
@@ -1224,7 +1249,13 @@
         Gossiper.instance.addLocalApplicationState(ApplicationState.RACK, StorageService.instance.valueFactory.rack(rack));
     }
 
-    public synchronized void joinRing() throws IOException
+    public void joinRing() throws IOException
+    {
+        SystemKeyspace.BootstrapState state = SystemKeyspace.getBootstrapState();
+        joinRing(state.equals(SystemKeyspace.BootstrapState.IN_PROGRESS));
+    }
+
+    private synchronized void joinRing(boolean resumedBootstrap) throws IOException
     {
         if (!joined)
         {
@@ -1246,7 +1277,7 @@
             {
                 isSurveyMode = false;
                 logger.info("Leaving write survey mode and joining ring at operator request");
-                finishJoiningRing(SystemKeyspace.getSavedTokens());
+                finishJoiningRing(resumedBootstrap, SystemKeyspace.getSavedTokens());
                 daemon.start();
             }
             else
@@ -1261,10 +1292,19 @@
         }
     }
 
-    private void finishJoiningRing(Collection<Token> tokens)
+    private void executePreJoinTasks(boolean bootstrap)
+    {
+        StreamSupport.stream(ColumnFamilyStore.all().spliterator(), false)
+                .filter(cfs -> Schema.instance.getUserKeyspaces().contains(cfs.keyspace.getName()))
+                .forEach(cfs -> cfs.indexManager.executePreJoinTasksBlocking(bootstrap));
+    }
+
+    private void finishJoiningRing(boolean didBootstrap, Collection<Token> tokens)
     {
         // start participating in the ring.
+        setMode(Mode.JOINING, "Finish joining ring", true);
         SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.COMPLETED);
+        executePreJoinTasks(didBootstrap);
         setTokens(tokens);
 
         assert tokenMetadata.sortedTokens().size() > 0;
@@ -1284,11 +1324,16 @@
             DatabaseDescriptor.getRoleManager().setup();
             DatabaseDescriptor.getAuthenticator().setup();
             DatabaseDescriptor.getAuthorizer().setup();
-
             MigrationManager.instance.register(new AuthMigrationListener());
+            authSetupComplete = true;
         }
     }
 
+    public boolean isAuthSetupComplete()
+    {
+        return authSetupComplete;
+    }
+
     private void setUpDistributedSystemKeyspaces()
     {
         Collection<Mutation> changes = new ArrayList<>(3);
@@ -1308,6 +1353,11 @@
 
     public void rebuild(String sourceDc)
     {
+        rebuild(sourceDc, null, null, null);
+    }
+
+    public void rebuild(String sourceDc, String keyspace, String tokens, String specificSources)
+    {
         try
         {
             // check ongoing rebuild
@@ -1326,6 +1376,12 @@
                                                                      sourceDc, String.join(",", availableDCs)));
                 }
             }
+
+            // check the arguments
+            if (keyspace == null && tokens != null)
+            {
+                throw new IllegalArgumentException("Cannot specify tokens without keyspace.");
+            }
         }
         catch (Throwable ex)
         {
@@ -1333,7 +1389,9 @@
             throw ex;
         }
 
-        logger.info("rebuild from dc: {}", sourceDc == null ? "(any dc)" : sourceDc);
+        logger.info("rebuild from dc: {}, {}, {}", sourceDc == null ? "(any dc)" : sourceDc,
+                    keyspace == null ? "(All keyspaces)" : keyspace,
+                    tokens == null ? "(All tokens)" : tokens);
 
         try
         {
@@ -1341,15 +1399,86 @@
                                                        null,
                                                        FBUtilities.getBroadcastAddress(),
                                                        "Rebuild",
-                                                       !replacing && useStrictConsistency,
+                                                       useStrictConsistency && !replacing,
                                                        DatabaseDescriptor.getEndpointSnitch(),
-                                                       streamStateStore);
+                                                       streamStateStore,
+                                                       false);
             streamer.addSourceFilter(new RangeStreamer.FailureDetectorSourceFilter(FailureDetector.instance));
             if (sourceDc != null)
                 streamer.addSourceFilter(new RangeStreamer.SingleDatacenterFilter(DatabaseDescriptor.getEndpointSnitch(), sourceDc));
 
-            for (String keyspaceName : Schema.instance.getNonLocalStrategyKeyspaces())
-                streamer.addRanges(keyspaceName, getLocalRanges(keyspaceName));
+            if (keyspace == null)
+            {
+                for (String keyspaceName : Schema.instance.getNonLocalStrategyKeyspaces())
+                    streamer.addRanges(keyspaceName, getLocalRanges(keyspaceName));
+            }
+            else if (tokens == null)
+            {
+                streamer.addRanges(keyspace, getLocalRanges(keyspace));
+            }
+            else
+            {
+                Token.TokenFactory factory = getTokenFactory();
+                List<Range<Token>> ranges = new ArrayList<>();
+                Pattern rangePattern = Pattern.compile("\\(\\s*(-?\\w+)\\s*,\\s*(-?\\w+)\\s*\\]");
+                try (Scanner tokenScanner = new Scanner(tokens))
+                {
+                    while (tokenScanner.findInLine(rangePattern) != null)
+                    {
+                        MatchResult range = tokenScanner.match();
+                        Token startToken = factory.fromString(range.group(1));
+                        Token endToken = factory.fromString(range.group(2));
+                        logger.info("adding range: ({},{}]", startToken, endToken);
+                        ranges.add(new Range<>(startToken, endToken));
+                    }
+                    if (tokenScanner.hasNext())
+                        throw new IllegalArgumentException("Unexpected string: " + tokenScanner.next());
+                }
+
+                // Ensure all specified ranges are actually ranges owned by this host
+                Collection<Range<Token>> localRanges = getLocalRanges(keyspace);
+                for (Range<Token> specifiedRange : ranges)
+                {
+                    boolean foundParentRange = false;
+                    for (Range<Token> localRange : localRanges)
+                    {
+                        if (localRange.contains(specifiedRange))
+                        {
+                            foundParentRange = true;
+                            break;
+                        }
+                    }
+                    if (!foundParentRange)
+                    {
+                        throw new IllegalArgumentException(String.format("The specified range %s is not a range that is owned by this node. Please ensure that all token ranges specified to be rebuilt belong to this node.", specifiedRange.toString()));
+                    }
+                }
+
+                if (specificSources != null)
+                {
+                    String[] stringHosts = specificSources.split(",");
+                    Set<InetAddress> sources = new HashSet<>(stringHosts.length);
+                    for (String stringHost : stringHosts)
+                    {
+                        try
+                        {
+                            InetAddress endpoint = InetAddress.getByName(stringHost);
+                            if (FBUtilities.getBroadcastAddress().equals(endpoint))
+                            {
+                                throw new IllegalArgumentException("This host was specified as a source for rebuilding. Sources for a rebuild can only be other nodes in the cluster.");
+                            }
+                            sources.add(endpoint);
+                        }
+                        catch (UnknownHostException ex)
+                        {
+                            throw new IllegalArgumentException("Unknown host specified " + stringHost, ex);
+                        }
+                    }
+                    streamer.addSourceFilter(new RangeStreamer.AllowedSourcesFilter(sources));
+                }
+
+                streamer.addRanges(keyspace, ranges);
+            }
 
             StreamResultFuture resultFuture = streamer.fetchAsync();
             // wait for result
@@ -1372,6 +1501,94 @@
         }
     }
 
+    public void setRpcTimeout(long value)
+    {
+        DatabaseDescriptor.setRpcTimeout(value);
+        logger.info("set rpc timeout to {} ms", value);
+    }
+
+    public long getRpcTimeout()
+    {
+        return DatabaseDescriptor.getRpcTimeout();
+    }
+
+    public void setReadRpcTimeout(long value)
+    {
+        DatabaseDescriptor.setReadRpcTimeout(value);
+        logger.info("set read rpc timeout to {} ms", value);
+    }
+
+    public long getReadRpcTimeout()
+    {
+        return DatabaseDescriptor.getReadRpcTimeout();
+    }
+
+    public void setRangeRpcTimeout(long value)
+    {
+        DatabaseDescriptor.setRangeRpcTimeout(value);
+        logger.info("set range rpc timeout to {} ms", value);
+    }
+
+    public long getRangeRpcTimeout()
+    {
+        return DatabaseDescriptor.getRangeRpcTimeout();
+    }
+
+    public void setWriteRpcTimeout(long value)
+    {
+        DatabaseDescriptor.setWriteRpcTimeout(value);
+        logger.info("set write rpc timeout to {} ms", value);
+    }
+
+    public long getWriteRpcTimeout()
+    {
+        return DatabaseDescriptor.getWriteRpcTimeout();
+    }
+
+    public void setCounterWriteRpcTimeout(long value)
+    {
+        DatabaseDescriptor.setCounterWriteRpcTimeout(value);
+        logger.info("set counter write rpc timeout to {} ms", value);
+    }
+
+    public long getCounterWriteRpcTimeout()
+    {
+        return DatabaseDescriptor.getCounterWriteRpcTimeout();
+    }
+
+    public void setCasContentionTimeout(long value)
+    {
+        DatabaseDescriptor.setCasContentionTimeout(value);
+        logger.info("set cas contention rpc timeout to {} ms", value);
+    }
+
+    public long getCasContentionTimeout()
+    {
+        return DatabaseDescriptor.getCasContentionTimeout();
+    }
+
+    public void setTruncateRpcTimeout(long value)
+    {
+        DatabaseDescriptor.setTruncateRpcTimeout(value);
+        logger.info("set truncate rpc timeout to {} ms", value);
+    }
+
+    public long getTruncateRpcTimeout()
+    {
+        return DatabaseDescriptor.getTruncateRpcTimeout();
+    }
+
+    public void setStreamingSocketTimeout(int value)
+    {
+        DatabaseDescriptor.setStreamingSocketTimeout(value);
+        logger.info("set streaming socket timeout to {} ms", value);
+    }
+
+    public int getStreamingSocketTimeout()
+    {
+        return DatabaseDescriptor.getStreamingSocketTimeout();
+    }
+
     public void setStreamThroughputMbPerSec(int value)
     {
         int oldValue = DatabaseDescriptor.getStreamThroughputOutboundMegabitsPerSec();
@@ -1398,10 +1615,6 @@
         return DatabaseDescriptor.getInterDCStreamThroughputOutboundMegabitsPerSec();
     }
 
-    public int getConcurrentCompactors()
-    {
-        return DatabaseDescriptor.getConcurrentCompactors();
-    }
 
     public int getCompactionThroughputMbPerSec()
     {
@@ -1414,6 +1627,19 @@
         CompactionManager.instance.setRate(value);
     }
 
+    public int getConcurrentCompactors()
+    {
+        return DatabaseDescriptor.getConcurrentCompactors();
+    }
+
+    public void setConcurrentCompactors(int value)
+    {
+        if (value <= 0)
+            throw new IllegalArgumentException("Number of concurrent compactors should be greater than 0.");
+        DatabaseDescriptor.setConcurrentCompactors(value);
+        CompactionManager.instance.setConcurrentCompactors(value);
+    }
+
     public boolean isIncrementalBackupsEnabled()
     {
         return DatabaseDescriptor.isIncrementalBackupsEnabled();
@@ -1496,10 +1722,13 @@
             SystemKeyspace.resetAvailableRanges();
         }
 
+        // Force disk boundary invalidation now that local tokens are set
+        invalidateDiskBoundaries();
+
         setMode(Mode.JOINING, "Starting to bootstrap...", true);
         BootStrapper bootstrapper = new BootStrapper(FBUtilities.getBroadcastAddress(), tokens, tokenMetadata);
         bootstrapper.addProgressListener(progressSupport);
-        ListenableFuture<StreamState> bootstrapStream = bootstrapper.bootstrap(streamStateStore, !replacing && useStrictConsistency); // handles token update
+        ListenableFuture<StreamState> bootstrapStream = bootstrapper.bootstrap(streamStateStore, useStrictConsistency && !replacing); // handles token update
         try
         {
             bootstrapStream.get();
@@ -1514,6 +1743,20 @@
         }
     }
 
+    private void invalidateDiskBoundaries()
+    {
+        for (Keyspace keyspace : Keyspace.all())
+        {
+            for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores())
+            {
+                for (final ColumnFamilyStore store : cfs.concatWithIndexes())
+                {
+                    store.invalidateDiskBoundaries();
+                }
+            }
+        }
+    }
+
     /**
      * All MVs have been created during bootstrap, so mark them as built
      */
@@ -1544,7 +1787,7 @@
             // already bootstrapped ranges are filtered during bootstrap
             BootStrapper bootstrapper = new BootStrapper(FBUtilities.getBroadcastAddress(), tokens, tokenMetadata);
             bootstrapper.addProgressListener(progressSupport);
-            ListenableFuture<StreamState> bootstrapStream = bootstrapper.bootstrap(streamStateStore, !replacing && useStrictConsistency); // handles token update
+            ListenableFuture<StreamState> bootstrapStream = bootstrapper.bootstrap(streamStateStore, useStrictConsistency && !replacing); // handles token update
             Futures.addCallback(bootstrapStream, new FutureCallback<StreamState>()
             {
                 @Override
@@ -1561,7 +1804,7 @@
                         {
                             isSurveyMode = false;
                             progressSupport.progress("bootstrap", ProgressEvent.createNotification("Joining ring..."));
-                            finishJoiningRing(bootstrapTokens);
+                            finishJoiningRing(true, bootstrapTokens);
                         }
                         progressSupport.progress("bootstrap", new ProgressEvent(ProgressEventType.COMPLETE, 1, 1, "Resume bootstrap complete"));
                         if (!isNativeTransportRunning())
@@ -1613,29 +1856,6 @@
     }
 
     /**
-     * Increment about the known Compaction severity of the events in this node
-     */
-    public void reportSeverity(double incr)
-    {
-        bgMonitor.incrCompactionSeverity(incr);
-    }
-
-    public void reportManualSeverity(double incr)
-    {
-        bgMonitor.incrManualSeverity(incr);
-    }
-
-    public double getSeverity(InetAddress endpoint)
-    {
-        return bgMonitor.getSeverity(endpoint);
-    }
-
-    public void shutdownBGMonitorAndWait(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException
-    {
-        bgMonitor.shutdownAndWait(timeout, unit);
-    }
-
-    /**
      * for a keyspace, return the ranges and corresponding listen addresses.
      * @param keyspace
      * @return the endpoint map
@@ -1882,7 +2102,7 @@
             return id;
         // this condition is to prevent accessing the tables when the node is not started yet, and in particular,
         // when it is not going to be started at all (e.g. when running some unit tests or client tools).
-        else if (DatabaseDescriptor.isDaemonInitialized())
+        else if (DatabaseDescriptor.isDaemonInitialized() || DatabaseDescriptor.isToolInitialized())
             return SystemKeyspace.getLocalHostId();
 
         return null;
@@ -2062,7 +2282,7 @@
     {
         try
         {
-            MessagingService.instance().setVersion(endpoint, Integer.valueOf(value.value));
+            MessagingService.instance().setVersion(endpoint, Integer.parseInt(value.value));
         }
         catch (NumberFormatException e)
         {
@@ -2851,7 +3071,7 @@
     }
 
     // TODO
-    public final void deliverHints(String host) throws UnknownHostException
+    public final void deliverHints(String host)
     {
         throw new UnsupportedOperationException();
     }
@@ -2905,6 +3125,15 @@
         return Schema.instance.getVersion().toString();
     }
 
+    public String getKeyspaceReplicationInfo(String keyspaceName)
+    {
+        Keyspace keyspaceInstance = Schema.instance.getKeyspaceInstance(keyspaceName);
+        if (keyspaceInstance == null)
+            throw new IllegalArgumentException(); // ideally should never happen
+        ReplicationParams replicationParams = keyspaceInstance.getMetadata().params.replication;
+        return replicationParams.klass.getSimpleName() + " " + replicationParams.options.toString();
+    }
+
     public List<String> getLeavingNodes()
     {
         return stringify(tokenMetadata.getLeavingEndpoints());
@@ -3001,7 +3230,7 @@
 
     public int forceKeyspaceCleanup(int jobs, String keyspaceName, String... tables) throws IOException, ExecutionException, InterruptedException
     {
-        if (Schema.isLocalSystemKeyspace(keyspaceName))
+        if (SchemaConstants.isLocalSystemKeyspace(keyspaceName))
             throw new RuntimeException("Cleanup of the system keyspace is neither necessary nor wise");
 
         CompactionManager.AllSSTableOpStatus status = CompactionManager.AllSSTableOpStatus.SUCCESSFUL;
@@ -3078,6 +3307,88 @@
         }
     }
 
+    public int relocateSSTables(String keyspaceName, String ... columnFamilies) throws IOException, ExecutionException, InterruptedException
+    {
+        return relocateSSTables(0, keyspaceName, columnFamilies);
+    }
+
+    public int relocateSSTables(int jobs, String keyspaceName, String ... columnFamilies) throws IOException, ExecutionException, InterruptedException
+    {
+        CompactionManager.AllSSTableOpStatus status = CompactionManager.AllSSTableOpStatus.SUCCESSFUL;
+        for (ColumnFamilyStore cfs : getValidColumnFamilies(false, false, keyspaceName, columnFamilies))
+        {
+            CompactionManager.AllSSTableOpStatus oneStatus = cfs.relocateSSTables(jobs);
+            if (oneStatus != CompactionManager.AllSSTableOpStatus.SUCCESSFUL)
+                status = oneStatus;
+        }
+        return status.statusCode;
+    }
+
+    public int garbageCollect(String tombstoneOptionString, int jobs, String keyspaceName, String ... columnFamilies) throws IOException, ExecutionException, InterruptedException
+    {
+        TombstoneOption tombstoneOption = TombstoneOption.valueOf(tombstoneOptionString);
+        CompactionManager.AllSSTableOpStatus status = CompactionManager.AllSSTableOpStatus.SUCCESSFUL;
+        for (ColumnFamilyStore cfs : getValidColumnFamilies(false, false, keyspaceName, columnFamilies))
+        {
+            CompactionManager.AllSSTableOpStatus oneStatus = cfs.garbageCollect(tombstoneOption, jobs);
+            if (oneStatus != CompactionManager.AllSSTableOpStatus.SUCCESSFUL)
+                status = oneStatus;
+        }
+        return status.statusCode;
+    }
+
+    /**
+     * Takes the snapshot of a multiple column family from different keyspaces. A snapshot name must be specified.
+     *
+     * @param tag
+     *            the tag given to the snapshot; may not be null or empty
+     * @param options
+     *            Map of options (skipFlush is the only supported option for now)
+     * @param entities
+     *            list of keyspaces / tables in the form of empty | ks1 ks2 ... | ks1.cf1,ks2.cf2,...
+     */
+    @Override
+    public void takeSnapshot(String tag, Map<String, String> options, String... entities) throws IOException
+    {
+        boolean skipFlush = Boolean.parseBoolean(options.getOrDefault("skipFlush", "false"));
+
+        if (entities != null && entities.length > 0 && entities[0].contains("."))
+        {
+            takeMultipleTableSnapshot(tag, skipFlush, entities);
+        }
+        else
+        {
+            takeSnapshot(tag, skipFlush, entities);
+        }
+    }
+
+    /**
+     * Takes the snapshot of a specific table. A snapshot name must be
+     * specified.
+     *
+     * @param keyspaceName
+     *            the keyspace which holds the specified table
+     * @param tableName
+     *            the table to snapshot
+     * @param tag
+     *            the tag given to the snapshot; may not be null or empty
+     */
+    public void takeTableSnapshot(String keyspaceName, String tableName, String tag)
+            throws IOException
+    {
+        takeMultipleTableSnapshot(tag, false, keyspaceName + "." + tableName);
+    }
+
+    public void forceKeyspaceCompactionForTokenRange(String keyspaceName, String startToken, String endToken, String... tableNames) throws IOException, ExecutionException, InterruptedException
+    {
+        Collection<Range<Token>> tokenRanges = createRepairRangeFrom(startToken, endToken);
+
+        for (ColumnFamilyStore cfStore : getValidColumnFamilies(true, false, keyspaceName, tableNames))
+        {
+            cfStore.forceCompactionForTokenRange(tokenRanges);
+        }
+    }
+
     /**
      * Takes the snapshot for the given keyspaces. A snapshot name must be specified.
      *
@@ -3086,6 +3397,32 @@
      */
     public void takeSnapshot(String tag, String... keyspaceNames) throws IOException
     {
+        takeSnapshot(tag, false, keyspaceNames);
+    }
+
+    /**
+     * Takes the snapshot of a multiple column family from different keyspaces. A snapshot name must be specified.
+     *
+     * @param tag
+     *            the tag given to the snapshot; may not be null or empty
+     * @param tableList
+     *            list of tables from different keyspace in the form of ks1.cf1 ks2.cf2
+     */
+    public void takeMultipleTableSnapshot(String tag, String... tableList)
+            throws IOException
+    {
+        takeMultipleTableSnapshot(tag, false, tableList);
+    }
+
+    /**
+     * Takes the snapshot for the given keyspaces. A snapshot name must be specified.
+     *
+     * @param tag the tag given to the snapshot; may not be null or empty
+     * @param skipFlush Skip blocking flush of memtable
+     * @param keyspaceNames the names of the keyspaces to snapshot; empty means "all."
+     */
+    private void takeSnapshot(String tag, boolean skipFlush, String... keyspaceNames) throws IOException
+    {
         if (operationMode == Mode.JOINING)
             throw new IOException("Cannot snapshot until bootstrap completes");
         if (tag == null || tag.equals(""))
@@ -3111,37 +3448,7 @@
 
 
         for (Keyspace keyspace : keyspaces)
-            keyspace.snapshot(tag, null);
-    }
-
-    /**
-     * Takes the snapshot of a specific table. A snapshot name must be specified.
-     *
-     * @param keyspaceName the keyspace which holds the specified table
-     * @param tableName the table to snapshot
-     * @param tag the tag given to the snapshot; may not be null or empty
-     */
-    public void takeTableSnapshot(String keyspaceName, String tableName, String tag) throws IOException
-    {
-        if (keyspaceName == null)
-            throw new IOException("You must supply a keyspace name");
-        if (operationMode == Mode.JOINING)
-            throw new IOException("Cannot snapshot until bootstrap completes");
-
-        if (tableName == null)
-            throw new IOException("You must supply a table name");
-        if (tableName.contains("."))
-            throw new IllegalArgumentException("Cannot take a snapshot of a secondary index by itself. Run snapshot on the table that owns the index.");
-
-        if (tag == null || tag.equals(""))
-            throw new IOException("You must supply a snapshot name.");
-
-        Keyspace keyspace = getValidKeyspace(keyspaceName);
-        ColumnFamilyStore columnFamilyStore = keyspace.getColumnFamilyStore(tableName);
-        if (columnFamilyStore.snapshotExists(tag))
-            throw new IOException("Snapshot " + tag + " already exists.");
-
-        columnFamilyStore.snapshot(tag);
+            keyspace.snapshot(tag, null, skipFlush);
     }
 
     /**
@@ -3150,17 +3457,18 @@
      *
      * @param tag
      *            the tag given to the snapshot; may not be null or empty
+     * @param skipFlush
+     *            Skip blocking flush of memtable
      * @param tableList
      *            list of tables from different keyspace in the form of ks1.cf1 ks2.cf2
      */
-    @Override
-    public void takeMultipleTableSnapshot(String tag, String... tableList)
+    private void takeMultipleTableSnapshot(String tag, boolean skipFlush, String... tableList)
             throws IOException
     {
         Map<Keyspace, List<String>> keyspaceColumnfamily = new HashMap<Keyspace, List<String>>();
         for (String table : tableList)
         {
-            String splittedString[] = table.split("\\.");
+            String splittedString[] = StringUtils.split(table, '.');
             if (splittedString.length == 2)
             {
                 String keyspaceName = splittedString[0];
@@ -3203,7 +3511,7 @@
         for (Entry<Keyspace, List<String>> entry : keyspaceColumnfamily.entrySet())
         {
             for (String table : entry.getValue())
-                entry.getKey().snapshot(tag, table);
+                entry.getKey().snapshot(tag, table, skipFlush);
         }
 
     }
@@ -3250,7 +3558,7 @@
         Map<String, TabularData> snapshotMap = new HashMap<>();
         for (Keyspace keyspace : Keyspace.all())
         {
-            if (Schema.isLocalSystemKeyspace(keyspace.getName()))
+            if (SchemaConstants.isLocalSystemKeyspace(keyspace.getName()))
                 continue;
 
             for (ColumnFamilyStore cfStore : keyspace.getColumnFamilyStores())
@@ -3276,7 +3584,7 @@
         long total = 0;
         for (Keyspace keyspace : Keyspace.all())
         {
-            if (Schema.isLocalSystemKeyspace(keyspace.getName()))
+            if (SchemaConstants.isLocalSystemKeyspace(keyspace.getName()))
                 continue;
 
             for (ColumnFamilyStore cfStore : keyspace.getColumnFamilyStores())
@@ -3395,13 +3703,13 @@
             throw new IllegalArgumentException("Invalid parallelism degree specified: " + parallelismDegree);
         }
         RepairParallelism parallelism = RepairParallelism.values()[parallelismDegree];
-        if (FBUtilities.isWindows() && parallelism != RepairParallelism.PARALLEL)
+        if (FBUtilities.isWindows && parallelism != RepairParallelism.PARALLEL)
         {
             logger.warn("Snapshot-based repair is not yet supported on Windows.  Reverting to parallel repair.");
             parallelism = RepairParallelism.PARALLEL;
         }
 
-        RepairOption options = new RepairOption(parallelism, primaryRange, !fullRepair, false, 1, Collections.<Range<Token>>emptyList(), false, false);
+        RepairOption options = new RepairOption(parallelism, primaryRange, !fullRepair, false, 1, Collections.<Range<Token>>emptyList(), false, false, false);
         if (dataCenters != null)
         {
             options.getDataCenters().addAll(dataCenters);
@@ -3481,7 +3789,7 @@
             throw new IllegalArgumentException("Invalid parallelism degree specified: " + parallelismDegree);
         }
         RepairParallelism parallelism = RepairParallelism.values()[parallelismDegree];
-        if (FBUtilities.isWindows() && parallelism != RepairParallelism.PARALLEL)
+        if (FBUtilities.isWindows && parallelism != RepairParallelism.PARALLEL)
         {
             logger.warn("Snapshot-based repair is not yet supported on Windows.  Reverting to parallel repair.");
             parallelism = RepairParallelism.PARALLEL;
@@ -3493,7 +3801,7 @@
                         "The repair will occur but without anti-compaction.");
         Collection<Range<Token>> repairingRange = createRepairRangeFrom(beginToken, endToken);
 
-        RepairOption options = new RepairOption(parallelism, false, !fullRepair, false, 1, repairingRange, true, false);
+        RepairOption options = new RepairOption(parallelism, false, !fullRepair, false, 1, repairingRange, true, false, false);
         if (dataCenters != null)
         {
             options.getDataCenters().addAll(dataCenters);
@@ -3581,7 +3889,7 @@
             return 0;
 
         int cmd = nextRepairCommand.incrementAndGet();
-        new Thread(NamedThreadFactory.threadLocalDeallocator(createRepairTask(cmd, keyspace, options, legacy))).start();
+        NamedThreadFactory.createThread(createRepairTask(cmd, keyspace, options, legacy), "Repair-Task-" + threadCounter.incrementAndGet()).start();
         return cmd;
     }
 
@@ -3591,6 +3899,13 @@
         {
             throw new IllegalArgumentException("the local data center must be part of the repair");
         }
+        Set<String> existingDatacenters = tokenMetadata.cloneOnlyTokenMap().getTopology().getDatacenterEndpoints().keys().elementSet();
+        List<String> datacenters = new ArrayList<>(options.getDataCenters());
+        if (!existingDatacenters.containsAll(datacenters))
+        {
+            datacenters.removeAll(existingDatacenters);
+            throw new IllegalArgumentException("data center(s) " + datacenters.toString() + " not found");
+        }
 
         RepairRunnable task = new RepairRunnable(this, cmd, options, keyspace);
         task.addProgressListener(progressSupport);
@@ -3599,7 +3914,8 @@
         return new FutureTask<>(task, null);
     }
 
-    public void forceTerminateAllRepairSessions() {
+    public void forceTerminateAllRepairSessions()
+    {
         ActiveRepairService.instance.terminateSessions();
     }
 
@@ -3799,48 +4115,16 @@
 
     public void setLoggingLevel(String classQualifier, String rawLevel) throws Exception
     {
-        ch.qos.logback.classic.Logger logBackLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(classQualifier);
-
-        // if both classQualifer and rawLevel are empty, reload from configuration
-        if (StringUtils.isBlank(classQualifier) && StringUtils.isBlank(rawLevel) )
-        {
-            JMXConfiguratorMBean jmxConfiguratorMBean = JMX.newMBeanProxy(ManagementFactory.getPlatformMBeanServer(),
-                    new ObjectName("ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator"),
-                    JMXConfiguratorMBean.class);
-            jmxConfiguratorMBean.reloadDefaultConfiguration();
-            return;
-        }
-        // classQualifer is set, but blank level given
-        else if (StringUtils.isNotBlank(classQualifier) && StringUtils.isBlank(rawLevel) )
-        {
-            if (logBackLogger.getLevel() != null || hasAppenders(logBackLogger))
-                logBackLogger.setLevel(null);
-            return;
-        }
-
-        ch.qos.logback.classic.Level level = ch.qos.logback.classic.Level.toLevel(rawLevel);
-        logBackLogger.setLevel(level);
-        logger.info("set log level to {} for classes under '{}' (if the level doesn't look like '{}' then the logger couldn't parse '{}')", level, classQualifier, rawLevel, rawLevel);
+        LoggingSupportFactory.getLoggingSupport().setLoggingLevel(classQualifier, rawLevel);
     }
 
     /**
      * @return the runtime logging levels for all the configured loggers
      */
     @Override
-    public Map<String,String>getLoggingLevels() {
-        Map<String, String> logLevelMaps = Maps.newLinkedHashMap();
-        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
-        for (ch.qos.logback.classic.Logger logger : lc.getLoggerList())
-        {
-            if(logger.getLevel() != null || hasAppenders(logger))
-                logLevelMaps.put(logger.getName(), logger.getLevel().toString());
-        }
-        return logLevelMaps;
-    }
-
-    private boolean hasAppenders(ch.qos.logback.classic.Logger logger) {
-        Iterator<Appender<ILoggingEvent>> it = logger.iteratorForAppenders();
-        return it.hasNext();
+    public Map<String,String> getLoggingLevels()
+    {
+        return LoggingSupportFactory.getLoggingSupport().getLoggingLevels();
     }
 
     /**
@@ -3916,41 +4200,63 @@
             throw new UnsupportedOperationException("local node is not a member of the token ring yet");
         if (tokenMetadata.cloneAfterAllLeft().sortedTokens().size() < 2)
             throw new UnsupportedOperationException("no other normal nodes in the ring; decommission would be pointless");
-        if (!isNormal())
+        if (operationMode != Mode.LEAVING && operationMode != Mode.NORMAL)
             throw new UnsupportedOperationException("Node in " + operationMode + " state; wait for status to become normal or restart");
-
-        PendingRangeCalculatorService.instance.blockUntilFinished();
-        for (String keyspaceName : Schema.instance.getNonLocalStrategyKeyspaces())
-        {
-            if (tokenMetadata.getPendingRanges(keyspaceName, FBUtilities.getBroadcastAddress()).size() > 0)
-                throw new UnsupportedOperationException("data is currently moving to this node; unable to leave the ring");
-        }
+        if (isDecommissioning.compareAndSet(true, true))
+            throw new IllegalStateException("Node is still decommissioning. Check nodetool netstats.");
 
         if (logger.isDebugEnabled())
             logger.debug("DECOMMISSIONING");
-        startLeaving();
-        long timeout = Math.max(RING_DELAY, BatchlogManager.instance.getBatchlogTimeout());
-        setMode(Mode.LEAVING, "sleeping " + timeout + " ms for batch processing and pending range setup", true);
-        Thread.sleep(timeout);
 
-        Runnable finishLeaving = new Runnable()
+        try
         {
-            public void run()
+            PendingRangeCalculatorService.instance.blockUntilFinished();
+            for (String keyspaceName : Schema.instance.getNonLocalStrategyKeyspaces())
             {
-                shutdownClientServers();
-                Gossiper.instance.stop();
-                try {
-                    MessagingService.instance().shutdown();
-                } catch (IOError ioe) {
-                    logger.info("failed to shutdown message service: {}", ioe);
-                }
-                StageManager.shutdownNow();
-                SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.DECOMMISSIONED);
-                setMode(Mode.DECOMMISSIONED, true);
-                // let op be responsible for killing the process
+                if (tokenMetadata.getPendingRanges(keyspaceName, FBUtilities.getBroadcastAddress()).size() > 0)
+                    throw new UnsupportedOperationException("data is currently moving to this node; unable to leave the ring");
             }
-        };
-        unbootstrap(finishLeaving);
+
+            startLeaving();
+            long timeout = Math.max(RING_DELAY, BatchlogManager.instance.getBatchlogTimeout());
+            setMode(Mode.LEAVING, "sleeping " + timeout + " ms for batch processing and pending range setup", true);
+            Thread.sleep(timeout);
+
+            Runnable finishLeaving = new Runnable()
+            {
+                public void run()
+                {
+                    shutdownClientServers();
+                    Gossiper.instance.stop();
+                    try
+                    {
+                        MessagingService.instance().shutdown();
+                    }
+                    catch (IOError ioe)
+                    {
+                        logger.info("failed to shutdown message service: {}", ioe);
+                    }
+                    StageManager.shutdownNow();
+                    SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.DECOMMISSIONED);
+                    setMode(Mode.DECOMMISSIONED, true);
+                    // let op be responsible for killing the process
+                }
+            };
+            unbootstrap(finishLeaving);
+        }
+        catch (InterruptedException e)
+        {
+            throw new RuntimeException("Node interrupted while decommissioning");
+        }
+        catch (ExecutionException e)
+        {
+            logger.error("Error while decommissioning node ", e.getCause());
+            throw new RuntimeException("Error while decommissioning node: " + e.getCause().getMessage());
+        }
+        finally
+        {
+            isDecommissioning.set(false);
+        }
     }
 
     private void leaveRing()
@@ -3965,7 +4271,7 @@
         Uninterruptibles.sleepUninterruptibly(delay, TimeUnit.MILLISECONDS);
     }
 
-    private void unbootstrap(Runnable onFinish)
+    private void unbootstrap(Runnable onFinish) throws ExecutionException, InterruptedException
     {
         Map<String, Multimap<Range<Token>, InetAddress>> rangesToStream = new HashMap<>();
 
@@ -3987,14 +4293,7 @@
 
         // Wait for batch log to complete before streaming hints.
         logger.debug("waiting for batch log processing.");
-        try
-        {
-            batchlogReplay.get();
-        }
-        catch (ExecutionException | InterruptedException e)
-        {
-            throw new RuntimeException(e);
-        }
+        batchlogReplay.get();
 
         setMode(Mode.LEAVING, "streaming hints to other nodes", true);
 
@@ -4002,15 +4301,8 @@
 
         // wait for the transfer runnables to signal the latch.
         logger.debug("waiting for stream acks.");
-        try
-        {
-            streamSuccess.get();
-            hintsSuccess.get();
-        }
-        catch (ExecutionException | InterruptedException e)
-        {
-            throw new RuntimeException(e);
-        }
+        streamSuccess.get();
+        hintsSuccess.get();
         logger.debug("stream acks all received.");
         leaveRing();
         onFinish.run();
@@ -4277,7 +4569,8 @@
      */
     public String getRemovalStatus()
     {
-        if (removingNode == null) {
+        if (removingNode == null)
+        {
             return "No token removals in process.";
         }
         return String.format("Removing token (%s). Waiting for replication confirmation from [%s].",
@@ -4287,7 +4580,7 @@
 
     /**
      * Force a remove operation to complete. This may be necessary if a remove operation
-     * blocks forever due to node/stream failure. removeToken() must be called
+     * blocks forever due to node/stream failure. removeNode() must be called
      * first, this is a last resort measure.  No further attempt will be made to restore replicas.
      */
     public void forceRemoveCompletion()
@@ -4306,18 +4599,18 @@
         }
         else
         {
-            logger.warn("No tokens to force removal on, call 'removenode' first");
+            logger.warn("No nodes to force removal on, call 'removenode' first");
         }
     }
 
     /**
      * Remove a node that has died, attempting to restore the replica count.
      * If the node is alive, decommission should be attempted.  If decommission
-     * fails, then removeToken should be called.  If we fail while trying to
+     * fails, then removeNode should be called.  If we fail while trying to
      * restore the replica count, finally forceRemoveCompleteion should be
      * called to forcibly remove the node without regard to replica count.
      *
-     * @param hostIdString token for the node
+     * @param hostIdString Host ID for the node
      */
     public void removeNode(String hostIdString)
     {
@@ -4329,7 +4622,8 @@
         if (endpoint == null)
             throw new UnsupportedOperationException("Host ID not found.");
 
-        Collection<Token> tokens = tokenMetadata.getTokens(endpoint);
+        if (!tokenMetadata.isMember(endpoint))
+            throw new UnsupportedOperationException("Node to be removed is not a member of the token ring");
 
         if (endpoint.equals(myAddress))
              throw new UnsupportedOperationException("Cannot remove self");
@@ -4344,6 +4638,8 @@
         if (!replicatingNodes.isEmpty())
             throw new UnsupportedOperationException("This node is already processing a removal. Wait for it to complete, or use 'removenode force' if this has failed.");
 
+        Collection<Token> tokens = tokenMetadata.getTokens(endpoint);
+
         // Find the endpoints that are going to become responsible for data
         for (String keyspaceName : Schema.instance.getNonLocalStrategyKeyspaces())
         {
@@ -4473,6 +4769,10 @@
         assert !isShutdown;
         isShutdown = true;
 
+        Throwable preShutdownHookThrowable = Throwables.perform(null, preShutdownHooks.stream().map(h -> h::run));
+        if (preShutdownHookThrowable != null)
+            logger.error("Attempting to continue draining after pre-shutdown hooks returned exception", preShutdownHookThrowable);
+
         try
         {
             setMode(Mode.DRAINING, "starting drain process", !isFinalShutdown);
@@ -4588,6 +4888,59 @@
         {
             logger.error("Caught an exception while draining ", t);
         }
+        finally
+        {
+            Throwable postShutdownHookThrowable = Throwables.perform(null, postShutdownHooks.stream().map(h -> h::run));
+            if (postShutdownHookThrowable != null)
+                logger.error("Post-shutdown hooks returned exception", postShutdownHookThrowable);
+        }
+    }
+
+    /**
+     * Add a runnable which will be called before shut down or drain. This is useful for other
+     * applications running in the same JVM which may want to shut down first rather than time
+     * out attempting to use Cassandra calls which will no longer work.
+     * @param hook: the code to run
+     * @return true on success, false if Cassandra is already shutting down, in which case the runnable
+     * has NOT been added.
+     */
+    public synchronized boolean addPreShutdownHook(Runnable hook)
+    {
+        if (!isDraining() && !isDrained())
+            return preShutdownHooks.add(hook);
+
+        return false;
+    }
+
+    /**
+     * Remove a preshutdown hook
+     */
+    public synchronized boolean removePreShutdownHook(Runnable hook)
+    {
+        return preShutdownHooks.remove(hook);
+    }
+
+    /**
+     * Add a runnable which will be called after shutdown or drain. This is useful for other applications
+     * running in the same JVM that Cassandra needs to work and should shut down later.
+     * @param hook: the code to run
+     * @return true on success, false if Cassandra is already shutting down, in which case the runnable has NOT been
+     * added.
+     */
+    public synchronized boolean addPostShutdownHook(Runnable hook)
+    {
+        if (!isDraining() && !isDrained())
+            return postShutdownHooks.add(hook);
+
+        return false;
+    }
+
+    /**
+     * Remove a postshutdownhook
+     */
+    public synchronized boolean removePostShutdownHook(Runnable hook)
+    {
+        return postShutdownHooks.remove(hook);
     }
 
     @VisibleForTesting
@@ -4750,7 +5103,8 @@
 
     public List<String> getNonSystemKeyspaces()
     {
-        return Collections.unmodifiableList(Schema.instance.getNonSystemKeyspaces());
+        List<String> nonKeyspaceNamesList = new ArrayList<>(Schema.instance.getNonSystemKeyspaces());
+        return Collections.unmodifiableList(nonKeyspaceNamesList);
     }
 
     public List<String> getNonLocalStrategyKeyspaces()
@@ -4758,37 +5112,107 @@
         return Collections.unmodifiableList(Schema.instance.getNonLocalStrategyKeyspaces());
     }
 
+    public Map<String, String> getViewBuildStatuses(String keyspace, String view)
+    {
+        Map<UUID, String> coreViewStatus = SystemDistributedKeyspace.viewStatus(keyspace, view);
+        Map<InetAddress, UUID> hostIdToEndpoint = tokenMetadata.getEndpointToHostIdMapForReading();
+        Map<String, String> result = new HashMap<>();
+
+        for (Map.Entry<InetAddress, UUID> entry : hostIdToEndpoint.entrySet())
+        {
+            UUID hostId = entry.getValue();
+            InetAddress endpoint = entry.getKey();
+            result.put(endpoint.toString(),
+                       coreViewStatus.containsKey(hostId)
+                       ? coreViewStatus.get(hostId)
+                       : "UNKNOWN");
+        }
+
+        return Collections.unmodifiableMap(result);
+    }
+
+    public void setDynamicUpdateInterval(int dynamicUpdateInterval)
+    {
+        if (DatabaseDescriptor.getEndpointSnitch() instanceof DynamicEndpointSnitch)
+        {
+
+            try
+            {
+                updateSnitch(null, true, dynamicUpdateInterval, null, null);
+            }
+            catch (ClassNotFoundException e)
+            {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    public int getDynamicUpdateInterval()
+    {
+        return DatabaseDescriptor.getDynamicUpdateInterval();
+    }
+
     public void updateSnitch(String epSnitchClassName, Boolean dynamic, Integer dynamicUpdateInterval, Integer dynamicResetInterval, Double dynamicBadnessThreshold) throws ClassNotFoundException
     {
+        // apply dynamic snitch configuration
+        if (dynamicUpdateInterval != null)
+            DatabaseDescriptor.setDynamicUpdateInterval(dynamicUpdateInterval);
+        if (dynamicResetInterval != null)
+            DatabaseDescriptor.setDynamicResetInterval(dynamicResetInterval);
+        if (dynamicBadnessThreshold != null)
+            DatabaseDescriptor.setDynamicBadnessThreshold(dynamicBadnessThreshold);
+
         IEndpointSnitch oldSnitch = DatabaseDescriptor.getEndpointSnitch();
 
         // new snitch registers mbean during construction
-        IEndpointSnitch newSnitch;
-        try
+        if(epSnitchClassName != null)
         {
-            newSnitch = FBUtilities.construct(epSnitchClassName, "snitch");
-        }
-        catch (ConfigurationException e)
-        {
-            throw new ClassNotFoundException(e.getMessage());
-        }
-        if (dynamic)
-        {
-            DatabaseDescriptor.setDynamicUpdateInterval(dynamicUpdateInterval);
-            DatabaseDescriptor.setDynamicResetInterval(dynamicResetInterval);
-            DatabaseDescriptor.setDynamicBadnessThreshold(dynamicBadnessThreshold);
-            newSnitch = new DynamicEndpointSnitch(newSnitch);
-        }
 
-        // point snitch references to the new instance
-        DatabaseDescriptor.setEndpointSnitch(newSnitch);
-        for (String ks : Schema.instance.getKeyspaces())
-        {
-            Keyspace.open(ks).getReplicationStrategy().snitch = newSnitch;
-        }
+            // need to unregister the mbean _before_ the new dynamic snitch is instantiated (and implicitly initialized
+            // and its mbean registered)
+            if (oldSnitch instanceof DynamicEndpointSnitch)
+                ((DynamicEndpointSnitch)oldSnitch).close();
 
-        if (oldSnitch instanceof DynamicEndpointSnitch)
-            ((DynamicEndpointSnitch)oldSnitch).unregisterMBean();
+            IEndpointSnitch newSnitch;
+            try
+            {
+                newSnitch = DatabaseDescriptor.createEndpointSnitch(dynamic != null && dynamic, epSnitchClassName);
+            }
+            catch (ConfigurationException e)
+            {
+                throw new ClassNotFoundException(e.getMessage());
+            }
+
+            if (newSnitch instanceof DynamicEndpointSnitch)
+            {
+                logger.info("Created new dynamic snitch {} with update-interval={}, reset-interval={}, badness-threshold={}",
+                            ((DynamicEndpointSnitch)newSnitch).subsnitch.getClass().getName(), DatabaseDescriptor.getDynamicUpdateInterval(),
+                            DatabaseDescriptor.getDynamicResetInterval(), DatabaseDescriptor.getDynamicBadnessThreshold());
+            }
+            else
+            {
+                logger.info("Created new non-dynamic snitch {}", newSnitch.getClass().getName());
+            }
+
+            // point snitch references to the new instance
+            DatabaseDescriptor.setEndpointSnitch(newSnitch);
+            for (String ks : Schema.instance.getKeyspaces())
+            {
+                Keyspace.open(ks).getReplicationStrategy().snitch = newSnitch;
+            }
+        }
+        else
+        {
+            if (oldSnitch instanceof DynamicEndpointSnitch)
+            {
+                logger.info("Applying config change to dynamic snitch {} with update-interval={}, reset-interval={}, badness-threshold={}",
+                            ((DynamicEndpointSnitch)oldSnitch).subsnitch.getClass().getName(), DatabaseDescriptor.getDynamicUpdateInterval(),
+                            DatabaseDescriptor.getDynamicResetInterval(), DatabaseDescriptor.getDynamicBadnessThreshold());
+
+                DynamicEndpointSnitch snitch = (DynamicEndpointSnitch)oldSnitch;
+                snitch.applyConfigChanges();
+            }
+        }
 
         updateTopology();
     }
@@ -4803,6 +5227,7 @@
     {
         // First, we build a list of ranges to stream to each host, per table
         Map<String, Map<InetAddress, List<Range<Token>>>> sessionsToStreamByKeyspace = new HashMap<>();
+
         for (Map.Entry<String, Multimap<Range<Token>, InetAddress>> entry : rangesToStreamByKeyspace.entrySet())
         {
             String keyspace = entry.getKey();
@@ -4811,12 +5236,22 @@
             if (rangesWithEndpoints.isEmpty())
                 continue;
 
+            Map<InetAddress, Set<Range<Token>>> transferredRangePerKeyspace = SystemKeyspace.getTransferredRanges("Unbootstrap",
+                                                                                                                  keyspace,
+                                                                                                                  StorageService.instance.getTokenMetadata().partitioner);
             Map<InetAddress, List<Range<Token>>> rangesPerEndpoint = new HashMap<>();
             for (Map.Entry<Range<Token>, InetAddress> endPointEntry : rangesWithEndpoints.entries())
             {
                 Range<Token> range = endPointEntry.getKey();
                 InetAddress endpoint = endPointEntry.getValue();
 
+                Set<Range<Token>> transferredRanges = transferredRangePerKeyspace.get(endpoint);
+                if (transferredRanges != null && transferredRanges.contains(range))
+                {
+                    logger.debug("Skipping transferred range {} of keyspace {}, endpoint {}", range, keyspace, endpoint);
+                    continue;
+                }
+
                 List<Range<Token>> curRanges = rangesPerEndpoint.get(endpoint);
                 if (curRanges == null)
                 {
@@ -4830,6 +5265,10 @@
         }
 
         StreamPlan streamPlan = new StreamPlan("Unbootstrap");
+
+        // Vinculate StreamStateStore to current StreamPlan to update transferred ranges per StreamSession
+        streamPlan.listeners(streamStateStore);
+
         for (Map.Entry<String, Map<InetAddress, List<Range<Token>>>> entry : sessionsToStreamByKeyspace.entrySet())
         {
             String keyspaceName = entry.getKey();
diff --git a/src/java/org/apache/cassandra/service/StorageServiceMBean.java b/src/java/org/apache/cassandra/service/StorageServiceMBean.java
index d66b47a..f456d4e 100644
--- a/src/java/org/apache/cassandra/service/StorageServiceMBean.java
+++ b/src/java/org/apache/cassandra/service/StorageServiceMBean.java
@@ -97,6 +97,11 @@
      */
     public String getSchemaVersion();
 
+    /**
+     * Fetch the replication factor for a given keyspace.
+     * @return string representation of replication information
+     */
+    public String getKeyspaceReplicationInfo(String keyspaceName);
 
     /**
      * Get the list of all data file locations from conf
@@ -195,31 +200,34 @@
     public List<InetAddress> getNaturalEndpoints(String keyspaceName, ByteBuffer key);
 
     /**
-     * Takes the snapshot for the given keyspaces. A snapshot name must be specified.
-     *
-     * @param tag the tag given to the snapshot; may not be null or empty
-     * @param keyspaceNames the name of the keyspaces to snapshot; empty means "all."
+     * @deprecated use {@link #takeSnapshot(String tag, Map options, String... entities)} instead.
      */
+    @Deprecated
     public void takeSnapshot(String tag, String... keyspaceNames) throws IOException;
 
     /**
-     * Takes the snapshot of a specific column family. A snapshot name must be specified.
-     *
-     * @param keyspaceName the keyspace which holds the specified column family
-     * @param tableName the table to snapshot
-     * @param tag the tag given to the snapshot; may not be null or empty
+     * @deprecated use {@link #takeSnapshot(String tag, Map options, String... entities)} instead.
      */
+    @Deprecated
     public void takeTableSnapshot(String keyspaceName, String tableName, String tag) throws IOException;
 
     /**
+     * @deprecated use {@link #takeSnapshot(String tag, Map options, String... entities)} instead.
+     */
+    @Deprecated
+    public void takeMultipleTableSnapshot(String tag, String... tableList) throws IOException;
+
+    /**
      * Takes the snapshot of a multiple column family from different keyspaces. A snapshot name must be specified.
-     * 
+     *
      * @param tag
      *            the tag given to the snapshot; may not be null or empty
-     * @param tableList
-     *            list of tables from different keyspace in the form of ks1.cf1 ks2.cf2
+     * @param options
+     *            Map of options (skipFlush is the only supported option for now)
+     * @param entities
+     *            list of keyspaces / tables in the form of empty | ks1 ks2 ... | ks1.cf1,ks2.cf2,...
      */
-    public void takeMultipleTableSnapshot(String tag, String... tableList) throws IOException;
+    public void takeSnapshot(String tag, Map<String, String> options, String... entities) throws IOException;
 
     /**
      * Remove the snapshot with the given name from the given keyspaces.
@@ -254,6 +262,20 @@
      */
     public void forceKeyspaceCompaction(boolean splitOutput, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException;
 
+    @Deprecated
+    public int relocateSSTables(String keyspace, String ... cfnames) throws IOException, ExecutionException, InterruptedException;
+    public int relocateSSTables(int jobs, String keyspace, String ... cfnames) throws IOException, ExecutionException, InterruptedException;
+
+    /**
+     * Forces major compaction of specified token range in a single keyspace.
+     *
+     * @param keyspaceName the name of the keyspace to be compacted
+     * @param startToken the token at which the compaction range starts (inclusive)
+     * @param endToken the token at which compaction range ends (inclusive)
+     * @param tableNames the names of the tables to be compacted
+     */
+    public void forceKeyspaceCompactionForTokenRange(String keyspaceName, String startToken, String endToken, String... tableNames) throws IOException, ExecutionException, InterruptedException;
+
     /**
      * Trigger a cleanup of keys on a single keyspace
      */
@@ -293,6 +315,12 @@
     public int upgradeSSTables(String keyspaceName, boolean excludeCurrentVersion, int jobs, String... tableNames) throws IOException, ExecutionException, InterruptedException;
 
     /**
+     * Rewrites all sstables from the given tables to remove deleted data.
+     * The tombstone option defines the granularity of the procedure: ROW removes deleted partitions and rows, CELL also removes overwritten or deleted cells.
+     */
+    public int garbageCollect(String tombstoneOption, int jobs, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException;
+
+    /**
      * Flush all memtables for the given column families, or all columnfamilies for the given keyspace
      * if none are explicitly listed.
      * @param keyspaceName
@@ -403,12 +431,12 @@
      * If classQualifer is not empty but level is empty/null, it will set the level to null for the defined classQualifer<br>
      * If level cannot be parsed, then the level will be defaulted to DEBUG<br>
      * <br>
-     * The logback configuration should have < jmxConfigurator /> set
-     * 
+     * The logback configuration should have {@code < jmxConfigurator />} set
+     *
      * @param classQualifier The logger's classQualifer
      * @param level The log level
-     * @throws Exception 
-     * 
+     * @throws Exception
+     *
      *  @see ch.qos.logback.classic.Level#toLevel(String)
      */
     public void setLoggingLevel(String classQualifier, String level) throws Exception;
@@ -442,7 +470,7 @@
 
     /**
      * given a list of tokens (representing the nodes in the cluster), returns
-     *   a mapping from "token -> %age of cluster owned by that token"
+     *   a mapping from {@code "token -> %age of cluster owned by that token"}
      */
     public Map<InetAddress, Float> getOwnership();
 
@@ -461,16 +489,36 @@
 
     public List<String> getNonLocalStrategyKeyspaces();
 
+    public Map<String, String> getViewBuildStatuses(String keyspace, String view);
+
     /**
-     * Change endpointsnitch class and dynamic-ness (and dynamic attributes) at runtime
+     * Change endpointsnitch class and dynamic-ness (and dynamic attributes) at runtime.
+     *
+     * This method is used to change the snitch implementation and/or dynamic snitch parameters.
+     * If {@code epSnitchClassName} is specified, it will configure a new snitch instance and make it a
+     * 'dynamic snitch' if {@code dynamic} is specified and {@code true}.
+     *
+     * The parameters {@code dynamicUpdateInterval}, {@code dynamicResetInterval} and {@code dynamicBadnessThreshold}
+     * can be specified individually to update the parameters of the dynamic snitch during runtime.
+     *
      * @param epSnitchClassName        the canonical path name for a class implementing IEndpointSnitch
-     * @param dynamic                  boolean that decides whether dynamicsnitch is used or not
-     * @param dynamicUpdateInterval    integer, in ms (default 100)
-     * @param dynamicResetInterval     integer, in ms (default 600,000)
-     * @param dynamicBadnessThreshold  double, (default 0.0)
+     * @param dynamic                  boolean that decides whether dynamicsnitch is used or not - only valid, if {@code epSnitchClassName} is specified
+     * @param dynamicUpdateInterval    integer, in ms (defaults to the value configured in cassandra.yaml, which defaults to 100)
+     * @param dynamicResetInterval     integer, in ms (defaults to the value configured in cassandra.yaml, which defaults to 600,000)
+     * @param dynamicBadnessThreshold  double, (defaults to the value configured in cassandra.yaml, which defaults to 0.0)
      */
     public void updateSnitch(String epSnitchClassName, Boolean dynamic, Integer dynamicUpdateInterval, Integer dynamicResetInterval, Double dynamicBadnessThreshold) throws ClassNotFoundException;
 
+    /*
+      Update dynamic_snitch_update_interval_in_ms
+     */
+    public void setDynamicUpdateInterval(int dynamicUpdateInterval);
+
+    /*
+      Get dynamic_snitch_update_interval_in_ms
+     */
+    public int getDynamicUpdateInterval();
+
     // allows a user to forcibly 'kill' a sick node
     public void stopGossiping();
 
@@ -483,7 +531,7 @@
     // allows a user to forcibly completely stop cassandra
     public void stopDaemon();
 
-    // to determine if gossip is disabled
+    // to determine if initialization has completed
     public boolean isInitialized();
 
     // allows a user to disable thrift
@@ -512,17 +560,42 @@
      */
     public boolean isBootstrapMode();
 
+    public void setRpcTimeout(long value);
+    public long getRpcTimeout();
+
+    public void setReadRpcTimeout(long value);
+    public long getReadRpcTimeout();
+
+    public void setRangeRpcTimeout(long value);
+    public long getRangeRpcTimeout();
+
+    public void setWriteRpcTimeout(long value);
+    public long getWriteRpcTimeout();
+
+    public void setCounterWriteRpcTimeout(long value);
+    public long getCounterWriteRpcTimeout();
+
+    public void setCasContentionTimeout(long value);
+    public long getCasContentionTimeout();
+
+    public void setTruncateRpcTimeout(long value);
+    public long getTruncateRpcTimeout();
+
+    public void setStreamingSocketTimeout(int value);
+    public int getStreamingSocketTimeout();
+
     public void setStreamThroughputMbPerSec(int value);
     public int getStreamThroughputMbPerSec();
 
     public void setInterDCStreamThroughputMbPerSec(int value);
     public int getInterDCStreamThroughputMbPerSec();
 
-    public int getConcurrentCompactors();
-
     public int getCompactionThroughputMbPerSec();
     public void setCompactionThroughputMbPerSec(int value);
 
+    public int getConcurrentCompactors();
+    public void setConcurrentCompactors(int value);
+
     public boolean isIncrementalBackupsEnabled();
     public void setIncrementalBackupsEnabled(boolean value);
 
@@ -535,6 +608,16 @@
      */
     public void rebuild(String sourceDc);
 
+    /**
+     * Same as {@link #rebuild(String)}, but only for specified keyspace and ranges.
+     *
+     * @param sourceDc Name of DC from which to select sources for streaming or null to pick any node
+     * @param keyspace Name of the keyspace which to rebuild or null to rebuild all keyspaces.
+     * @param tokens Range of tokens to rebuild or null to rebuild all token ranges. In the format of:
+     *               "(start_token_1,end_token_1],(start_token_2,end_token_2],...(start_token_n,end_token_n]"
+     */
+    public void rebuild(String sourceDc, String keyspace, String tokens, String specificSources);
+
     /** Starts a bulk load and blocks until it completes. */
     public void bulkLoad(String directory);
 
@@ -550,7 +633,7 @@
      * Load new SSTables to the given keyspace/table
      *
      * @param ksName The parent keyspace name
-     * @param cfName The ColumnFamily name where SSTables belong
+     * @param tableName The ColumnFamily name where SSTables belong
      */
     public void loadNewSSTables(String ksName, String tableName);
 
diff --git a/src/java/org/apache/cassandra/service/WriteResponseHandler.java b/src/java/org/apache/cassandra/service/WriteResponseHandler.java
index 1dc03e0..46e4e93 100644
--- a/src/java/org/apache/cassandra/service/WriteResponseHandler.java
+++ b/src/java/org/apache/cassandra/service/WriteResponseHandler.java
@@ -47,20 +47,21 @@
                                 ConsistencyLevel consistencyLevel,
                                 Keyspace keyspace,
                                 Runnable callback,
-                                WriteType writeType)
+                                WriteType writeType,
+                                long queryStartNanoTime)
     {
-        super(keyspace, writeEndpoints, pendingEndpoints, consistencyLevel, callback, writeType);
+        super(keyspace, writeEndpoints, pendingEndpoints, consistencyLevel, callback, writeType, queryStartNanoTime);
         responses = totalBlockFor();
     }
 
-    public WriteResponseHandler(InetAddress endpoint, WriteType writeType, Runnable callback)
+    public WriteResponseHandler(InetAddress endpoint, WriteType writeType, Runnable callback, long queryStartNanoTime)
     {
-        this(Arrays.asList(endpoint), Collections.<InetAddress>emptyList(), ConsistencyLevel.ONE, null, callback, writeType);
+        this(Arrays.asList(endpoint), Collections.<InetAddress>emptyList(), ConsistencyLevel.ONE, null, callback, writeType, queryStartNanoTime);
     }
 
-    public WriteResponseHandler(InetAddress endpoint, WriteType writeType)
+    public WriteResponseHandler(InetAddress endpoint, WriteType writeType, long queryStartNanoTime)
     {
-        this(endpoint, writeType, null);
+        this(endpoint, writeType, null, queryStartNanoTime);
     }
 
     public void response(MessageIn<T> m)
diff --git a/src/java/org/apache/cassandra/service/pager/AbstractQueryPager.java b/src/java/org/apache/cassandra/service/pager/AbstractQueryPager.java
index 2eecfee..fa3f262 100644
--- a/src/java/org/apache/cassandra/service/pager/AbstractQueryPager.java
+++ b/src/java/org/apache/cassandra/service/pager/AbstractQueryPager.java
@@ -23,15 +23,14 @@
 import org.apache.cassandra.db.partitions.*;
 import org.apache.cassandra.db.filter.DataLimits;
 import org.apache.cassandra.db.transform.Transformation;
-import org.apache.cassandra.exceptions.RequestExecutionException;
-import org.apache.cassandra.exceptions.RequestValidationException;
 import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 abstract class AbstractQueryPager implements QueryPager
 {
     protected final ReadCommand command;
     protected final DataLimits limits;
-    protected final int protocolVersion;
+    protected final ProtocolVersion protocolVersion;
     private final boolean enforceStrictLiveness;
 
     private int remaining;
@@ -44,7 +43,7 @@
 
     private boolean exhausted;
 
-    protected AbstractQueryPager(ReadCommand command, int protocolVersion)
+    protected AbstractQueryPager(ReadCommand command, ProtocolVersion protocolVersion)
     {
         this.command = command;
         this.protocolVersion = protocolVersion;
@@ -55,12 +54,12 @@
         this.remainingInPartition = limits.perPartitionCount();
     }
 
-    public ReadOrderGroup startOrderGroup()
+    public ReadExecutionController executionController()
     {
-        return command.startOrderGroup();
+        return command.executionController();
     }
 
-    public PartitionIterator fetchPage(int pageSize, ConsistencyLevel consistency, ClientState clientState) throws RequestValidationException, RequestExecutionException
+    public PartitionIterator fetchPage(int pageSize, ConsistencyLevel consistency, ClientState clientState, long queryStartNanoTime)
     {
         if (isExhausted())
             return EmptyIterators.partition();
@@ -73,10 +72,10 @@
             exhausted = true;
             return EmptyIterators.partition();
         }
-        return Transformation.apply(readCommand.execute(consistency, clientState), pager);
+        return Transformation.apply(readCommand.execute(consistency, clientState, queryStartNanoTime), pager);
     }
 
-    public PartitionIterator fetchPageInternal(int pageSize, ReadOrderGroup orderGroup) throws RequestValidationException, RequestExecutionException
+    public PartitionIterator fetchPageInternal(int pageSize, ReadExecutionController executionController)
     {
         if (isExhausted())
             return EmptyIterators.partition();
@@ -89,24 +88,23 @@
             exhausted = true;
             return EmptyIterators.partition();
         }
-        return Transformation.apply(readCommand.executeInternal(orderGroup), pager);
+        return Transformation.apply(readCommand.executeInternal(executionController), pager);
     }
 
-    public UnfilteredPartitionIterator fetchPageUnfiltered(CFMetaData cfm, int pageSize, ReadOrderGroup orderGroup)
+    public UnfilteredPartitionIterator fetchPageUnfiltered(CFMetaData cfm, int pageSize, ReadExecutionController executionController)
     {
         if (isExhausted())
             return EmptyIterators.unfilteredPartition(cfm, false);
 
         pageSize = Math.min(pageSize, remaining);
-
+        UnfilteredPager pager = new UnfilteredPager(limits.forPaging(pageSize), command.nowInSec());
         ReadCommand readCommand = nextPageReadCommand(pageSize);
         if (readCommand == null)
         {
             exhausted = true;
             return EmptyIterators.unfilteredPartition(cfm, false);
         }
-        UnfilteredPager pager = new UnfilteredPager(limits.forPaging(pageSize), command.nowInSec());
-        return Transformation.apply(readCommand.executeLocally(orderGroup), pager);
+        return Transformation.apply(readCommand.executeLocally(executionController), pager);
     }
 
     private class UnfilteredPager extends Pager<Unfiltered>
@@ -141,6 +139,7 @@
     {
         private final DataLimits pageLimits;
         protected final DataLimits.Counter counter;
+        private DecoratedKey currentKey;
         private Row lastRow;
         private boolean isFirstPartition = true;
 
@@ -153,10 +152,7 @@
         @Override
         public BaseRowIterator<T> applyToPartition(BaseRowIterator<T> partition)
         {
-            DecoratedKey key = partition.partitionKey();
-            if (lastKey == null || !lastKey.equals(key))
-                remainingInPartition = limits.perPartitionCount();
-            lastKey = key;
+            currentKey = partition.partitionKey();
 
             // If this is the first partition of this page, this could be the continuation of a partition we've started
             // on the previous page. In which case, we could have the problem that the partition has no more "regular"
@@ -166,7 +162,7 @@
             if (isFirstPartition)
             {
                 isFirstPartition = false;
-                if (isPreviouslyReturnedPartition(key) && !partition.hasNext())
+                if (isPreviouslyReturnedPartition(currentKey) && !partition.hasNext())
                 {
                     partition.close();
                     return null;
@@ -181,10 +177,12 @@
         @Override
         public void onClose()
         {
+            // In some case like GROUP BY a counter need to know when the processing is completed.
+            counter.onClose();
+
             recordLast(lastKey, lastRow);
 
-            int counted = counter.counted();
-            remaining -= counted;
+            remaining -= counter.counted();
             // If the clustering of the last row returned is a static one, it means that the partition was only
             // containing data within the static columns. If the clustering of the last row returned is empty
             // it means that there is only one row per partition. Therefore, in both cases there are no data remaining
@@ -198,19 +196,29 @@
             {
                 remainingInPartition -= counter.countedInCurrentPartition();
             }
-            exhausted = counted < pageLimits.count();
+            exhausted = pageLimits.isExhausted(counter);
         }
 
         public Row applyToStatic(Row row)
         {
             if (!row.isEmpty())
+            {
+                if (!currentKey.equals(lastKey))
+                    remainingInPartition = limits.perPartitionCount();
+                lastKey = currentKey;
                 lastRow = row;
+            }
             return row;
         }
 
         @Override
         public Row applyToRow(Row row)
         {
+            if (!currentKey.equals(lastKey))
+            {
+                remainingInPartition = limits.perPartitionCount();
+                lastKey = currentKey;
+            }
             lastRow = row;
             return row;
         }
diff --git a/src/java/org/apache/cassandra/service/pager/AggregationQueryPager.java b/src/java/org/apache/cassandra/service/pager/AggregationQueryPager.java
new file mode 100644
index 0000000..f9a8cda
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/pager/AggregationQueryPager.java
@@ -0,0 +1,437 @@
+/*
+ * 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.service.pager;
+
+import java.nio.ByteBuffer;
+import java.util.NoSuchElementException;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.aggregation.GroupingState;
+import org.apache.cassandra.db.filter.DataLimits;
+import org.apache.cassandra.db.partitions.PartitionIterator;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.RowIterator;
+import org.apache.cassandra.service.ClientState;
+
+/**
+ * {@code QueryPager} that takes care of fetching the pages for aggregation queries.
+ * <p>
+ * For aggregation/group by queries, the user page size is in number of groups. But each group could be composed of very
+ * many rows so to avoid running into OOMs, this pager will page internal queries into sub-pages. So each call to
+ * {@link fetchPage} may (transparently) yield multiple internal queries (sub-pages).
+ */
+public final class AggregationQueryPager implements QueryPager
+{
+    private final DataLimits limits;
+
+    // The sub-pager, used to retrieve the next sub-page.
+    private QueryPager subPager;
+
+    public AggregationQueryPager(QueryPager subPager, DataLimits limits)
+    {
+        this.subPager = subPager;
+        this.limits = limits;
+    }
+
+    @Override
+    public PartitionIterator fetchPage(int pageSize,
+                                       ConsistencyLevel consistency,
+                                       ClientState clientState,
+                                       long queryStartNanoTime)
+    {
+        if (limits.isGroupByLimit())
+            return new GroupByPartitionIterator(pageSize, consistency, clientState, queryStartNanoTime);
+
+        return new AggregationPartitionIterator(pageSize, consistency, clientState, queryStartNanoTime);
+    }
+
+    @Override
+    public ReadExecutionController executionController()
+    {
+        return subPager.executionController();
+    }
+
+    @Override
+    public PartitionIterator fetchPageInternal(int pageSize, ReadExecutionController executionController)
+    {
+        if (limits.isGroupByLimit())
+            return new GroupByPartitionIterator(pageSize, executionController, System.nanoTime());
+
+        return new AggregationPartitionIterator(pageSize, executionController, System.nanoTime());
+    }
+
+    @Override
+    public boolean isExhausted()
+    {
+        return subPager.isExhausted();
+    }
+
+    @Override
+    public int maxRemaining()
+    {
+        return subPager.maxRemaining();
+    }
+
+    @Override
+    public PagingState state()
+    {
+        return subPager.state();
+    }
+
+    @Override
+    public QueryPager withUpdatedLimit(DataLimits newLimits)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * <code>PartitionIterator</code> that automatically fetch a new sub-page of data if needed when the current iterator is
+     * exhausted.
+     */
+    public class GroupByPartitionIterator implements PartitionIterator
+    {
+        /**
+         * The top-level page size in number of groups.
+         */
+        private final int pageSize;
+
+        // For "normal" queries
+        private final ConsistencyLevel consistency;
+        private final ClientState clientState;
+
+        // For internal queries
+        private final ReadExecutionController executionController;
+
+        /**
+         * The <code>PartitionIterator</code> over the last page retrieved.
+         */
+        private PartitionIterator partitionIterator;
+
+        /**
+         * The next <code>RowIterator</code> to be returned.
+         */
+        private RowIterator next;
+
+        /**
+         * Specify if all the data have been returned.
+         */
+        private boolean endOfData;
+
+        /**
+         * Keeps track if the partitionIterator has been closed or not.
+         */
+        private boolean closed;
+
+        /**
+         * The key of the last partition processed.
+         */
+        private ByteBuffer lastPartitionKey;
+
+        /**
+         * The clustering of the last row processed
+         */
+        private Clustering lastClustering;
+
+        /**
+         * The initial amount of row remaining
+         */
+        private int initialMaxRemaining;
+
+        private long queryStartNanoTime;
+
+        public GroupByPartitionIterator(int pageSize,
+                                         ConsistencyLevel consistency,
+                                         ClientState clientState,
+                                        long queryStartNanoTime)
+        {
+            this(pageSize, consistency, clientState, null, queryStartNanoTime);
+        }
+
+        public GroupByPartitionIterator(int pageSize,
+                                        ReadExecutionController executionController,
+                                        long queryStartNanoTime)
+       {
+           this(pageSize, null, null, executionController, queryStartNanoTime);
+       }
+
+        private GroupByPartitionIterator(int pageSize,
+                                         ConsistencyLevel consistency,
+                                         ClientState clientState,
+                                         ReadExecutionController executionController,
+                                         long queryStartNanoTime)
+        {
+            this.pageSize = handlePagingOff(pageSize);
+            this.consistency = consistency;
+            this.clientState = clientState;
+            this.executionController = executionController;
+            this.queryStartNanoTime = queryStartNanoTime;
+        }
+
+        private int handlePagingOff(int pageSize)
+        {
+            // If the paging is off, the pageSize will be <= 0. So we need to replace
+            // it by DataLimits.NO_LIMIT
+            return pageSize <= 0 ? DataLimits.NO_LIMIT : pageSize;
+        }
+
+        public final void close()
+        {
+            if (!closed)
+            {
+                closed = true;
+                partitionIterator.close();
+            }
+        }
+
+        public final boolean hasNext()
+        {
+            if (endOfData)
+                return false;
+
+            if (next != null)
+                return true;
+
+            fetchNextRowIterator();
+
+            return next != null;
+        }
+
+        /**
+         * Loads the next <code>RowIterator</code> to be returned.
+         */
+        private void fetchNextRowIterator()
+        {
+            if (partitionIterator == null)
+            {
+                initialMaxRemaining = subPager.maxRemaining();
+                partitionIterator = fetchSubPage(pageSize);
+            }
+
+            while (!partitionIterator.hasNext())
+            {
+                partitionIterator.close();
+
+                int counted = initialMaxRemaining - subPager.maxRemaining();
+
+                if (isDone(pageSize, counted) || subPager.isExhausted())
+                {
+                    endOfData = true;
+                    closed = true;
+                    return;
+                }
+
+                subPager = updatePagerLimit(subPager, limits, lastPartitionKey, lastClustering);
+                partitionIterator = fetchSubPage(computeSubPageSize(pageSize, counted));
+            }
+
+            next = partitionIterator.next();
+        }
+
+        protected boolean isDone(int pageSize, int counted)
+        {
+            return counted == pageSize;
+        }
+
+        /**
+         * Updates the pager with the new limits if needed.
+         *
+         * @param pager the pager previoulsy used
+         * @param limits the DataLimits
+         * @param lastPartitionKey the partition key of the last row returned
+         * @param lastClustering the clustering of the last row returned
+         * @return the pager to use to query the next page of data
+         */
+        protected QueryPager updatePagerLimit(QueryPager pager,
+                                              DataLimits limits,
+                                              ByteBuffer lastPartitionKey,
+                                              Clustering lastClustering)
+        {
+            GroupingState state = new GroupingState(lastPartitionKey, lastClustering);
+            DataLimits newLimits = limits.forGroupByInternalPaging(state);
+            return pager.withUpdatedLimit(newLimits);
+        }
+
+        /**
+         * Computes the size of the next sub-page to retrieve.
+         *
+         * @param pageSize the top-level page size
+         * @param counted the number of result returned so far by the previous sub-pages
+         * @return the size of the next sub-page to retrieve
+         */
+        protected int computeSubPageSize(int pageSize, int counted)
+        {
+            return pageSize - counted;
+        }
+
+        /**
+         * Fetchs the next sub-page.
+         *
+         * @param subPageSize the sub-page size in number of groups
+         * @return the next sub-page
+         */
+        private final PartitionIterator fetchSubPage(int subPageSize)
+        {
+            return consistency != null ? subPager.fetchPage(subPageSize, consistency, clientState, queryStartNanoTime)
+                                       : subPager.fetchPageInternal(subPageSize, executionController);
+        }
+
+        public final RowIterator next()
+        {
+            if (!hasNext())
+                throw new NoSuchElementException();
+
+            RowIterator iterator = new GroupByRowIterator(next);
+            lastPartitionKey = iterator.partitionKey().getKey();
+            next = null;
+            return iterator;
+        }
+
+        private class GroupByRowIterator implements RowIterator
+        {
+            /**
+             * The decorated <code>RowIterator</code>.
+             */
+            private RowIterator rowIterator;
+
+            /**
+             * Keeps track if the decorated iterator has been closed or not.
+             */
+            private boolean closed;
+
+            public GroupByRowIterator(RowIterator delegate)
+            {
+                this.rowIterator = delegate;
+            }
+
+            public CFMetaData metadata()
+            {
+                return rowIterator.metadata();
+            }
+
+            public boolean isReverseOrder()
+            {
+                return rowIterator.isReverseOrder();
+            }
+
+            public PartitionColumns columns()
+            {
+                return rowIterator.columns();
+            }
+
+            public DecoratedKey partitionKey()
+            {
+                return rowIterator.partitionKey();
+            }
+
+            public Row staticRow()
+            {
+                Row row = rowIterator.staticRow();
+                lastClustering = null;
+                return row;
+            }
+
+            public boolean isEmpty()
+            {
+                return this.rowIterator.isEmpty() && !hasNext();
+            }
+
+            public void close()
+            {
+                if (!closed)
+                    rowIterator.close();
+            }
+
+            public boolean hasNext()
+            {
+                if (rowIterator.hasNext())
+                    return true;
+
+                DecoratedKey partitionKey = rowIterator.partitionKey();
+
+                rowIterator.close();
+
+                // Fetch the next RowIterator
+                GroupByPartitionIterator.this.hasNext();
+
+                // if the previous page was ending within the partition the
+                // next RowIterator is the continuation of this one
+                if (next != null && partitionKey.equals(next.partitionKey()))
+                {
+                    rowIterator = next;
+                    next = null;
+                    return rowIterator.hasNext();
+                }
+
+                closed = true;
+                return false;
+            }
+
+            public Row next()
+            {
+                Row row = this.rowIterator.next();
+                lastClustering = row.clustering();
+                return row;
+            }
+        }
+    }
+
+    /**
+     * <code>PartitionIterator</code> for queries without Group By but with aggregates.
+     * <p>For maintaining backward compatibility we are forced to use the {@link org.apache.cassandra.db.filter.DataLimits.CQLLimits} instead of the
+     * {@link org.apache.cassandra.db.filter.DataLimits.CQLGroupByLimits}. Due to that pages need to be fetched in a different way.</p>
+     */
+    public final class AggregationPartitionIterator extends GroupByPartitionIterator
+    {
+        public AggregationPartitionIterator(int pageSize,
+                                            ConsistencyLevel consistency,
+                                            ClientState clientState,
+                                            long queryStartNanoTime)
+        {
+            super(pageSize, consistency, clientState, queryStartNanoTime);
+        }
+
+        public AggregationPartitionIterator(int pageSize,
+                                            ReadExecutionController executionController,
+                                            long queryStartNanoTime)
+        {
+            super(pageSize, executionController, queryStartNanoTime);
+        }
+
+        @Override
+        protected QueryPager updatePagerLimit(QueryPager pager,
+                                              DataLimits limits,
+                                              ByteBuffer lastPartitionKey,
+                                              Clustering lastClustering)
+        {
+            return pager;
+        }
+
+        @Override
+        protected boolean isDone(int pageSize, int counted)
+        {
+            return false;
+        }
+
+        @Override
+        protected int computeSubPageSize(int pageSize, int counted)
+        {
+            return pageSize;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/pager/MultiPartitionPager.java b/src/java/org/apache/cassandra/service/pager/MultiPartitionPager.java
index aa268ab..9dae11c 100644
--- a/src/java/org/apache/cassandra/service/pager/MultiPartitionPager.java
+++ b/src/java/org/apache/cassandra/service/pager/MultiPartitionPager.java
@@ -17,8 +17,11 @@
  */
 package org.apache.cassandra.service.pager;
 
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.AbstractIterator;
 
+import java.util.Arrays;
+
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.filter.DataLimits;
@@ -45,18 +48,16 @@
 {
     private final SinglePartitionPager[] pagers;
     private final DataLimits limit;
-    private final boolean selectsFullPartitions;
 
     private final int nowInSec;
 
     private int remaining;
     private int current;
 
-    public MultiPartitionPager(SinglePartitionReadCommand.Group group, PagingState state, int protocolVersion)
+    public MultiPartitionPager(SinglePartitionReadCommand.Group group, PagingState state, ProtocolVersion protocolVersion)
     {
         this.limit = group.limits();
         this.nowInSec = group.nowInSec();
-        this.selectsFullPartitions = group.selectsFullPartition();
 
         int i = 0;
         // If it's not the beginning (state != null), we need to find where we were and skip previous commands
@@ -84,16 +85,39 @@
         remaining = state == null ? limit.count() : state.remaining;
     }
 
+    private MultiPartitionPager(SinglePartitionPager[] pagers,
+                                DataLimits limit,
+                                int nowInSec,
+                                int remaining,
+                                int current)
+    {
+        this.pagers = pagers;
+        this.limit = limit;
+        this.nowInSec = nowInSec;
+        this.remaining = remaining;
+        this.current = current;
+    }
+
+    public QueryPager withUpdatedLimit(DataLimits newLimits)
+    {
+        SinglePartitionPager[] newPagers = Arrays.copyOf(pagers, pagers.length);
+        newPagers[current] = newPagers[current].withUpdatedLimit(newLimits);
+
+        return new MultiPartitionPager(newPagers,
+                                       newLimits,
+                                       nowInSec,
+                                       remaining,
+                                       current);
+    }
+
     public PagingState state()
     {
         // Sets current to the first non-exhausted pager
         if (isExhausted())
             return null;
 
-        SinglePartitionPager pager = pagers[current];
-        PagingState pagerState = pager.state();
-        // Multi-partition paging state represents a _current_ position.
-        return new PagingState(pager.key(), pagerState == null ? null : pagerState.rowMark, remaining, pager.remainingInPartition());
+        PagingState state = pagers[current].state();
+        return new PagingState(pagers[current].key(), state == null ? null : state.rowMark, remaining, pagers[current].remainingInPartition());
     }
 
     public boolean isExhausted()
@@ -111,70 +135,56 @@
         return true;
     }
 
-    public ReadOrderGroup startOrderGroup()
+    public ReadExecutionController executionController()
     {
         // Note that for all pagers, the only difference is the partition key to which it applies, so in practice we
         // can use any of the sub-pager ReadOrderGroup group to protect the whole pager
         for (int i = current; i < pagers.length; i++)
         {
             if (pagers[i] != null)
-                return pagers[i].startOrderGroup();
+                return pagers[i].executionController();
         }
         throw new AssertionError("Shouldn't be called on an exhausted pager");
     }
 
     @SuppressWarnings("resource") // iter closed via countingIter
-    public PartitionIterator fetchPage(int pageSize, ConsistencyLevel consistency, ClientState clientState) throws RequestValidationException, RequestExecutionException
+    public PartitionIterator fetchPage(int pageSize, ConsistencyLevel consistency, ClientState clientState, long queryStartNanoTime) throws RequestValidationException, RequestExecutionException
     {
         int toQuery = Math.min(remaining, pageSize);
-        PagersIterator iter = new PagersIterator(toQuery, consistency, clientState, null);
-        /**
-         * It's safe to set it as false since all PartitionIterators have been filtered by each SPRC.
-         */
-        boolean enforceStrictLiveness = false;
-        DataLimits.Counter counter = limit.forPaging(toQuery).newCounter(nowInSec, true, selectsFullPartitions, enforceStrictLiveness);
-        iter.setCounter(counter);
-        return counter.applyTo(iter);
+        return new PagersIterator(toQuery, consistency, clientState, null, queryStartNanoTime);
     }
 
     @SuppressWarnings("resource") // iter closed via countingIter
-    public PartitionIterator fetchPageInternal(int pageSize, ReadOrderGroup orderGroup) throws RequestValidationException, RequestExecutionException
+    public PartitionIterator fetchPageInternal(int pageSize, ReadExecutionController executionController) throws RequestValidationException, RequestExecutionException
     {
         int toQuery = Math.min(remaining, pageSize);
-        PagersIterator iter = new PagersIterator(toQuery, null, null, orderGroup);
-        /**
-         * It's safe to set it as false since all PartitionIterators have been filtered by each SPRC.
-         */
-        boolean enforceStrictLiveness = false;
-        DataLimits.Counter counter = limit.forPaging(toQuery).newCounter(nowInSec, true, selectsFullPartitions, enforceStrictLiveness);
-        iter.setCounter(counter);
-        return counter.applyTo(iter);
+        return new PagersIterator(toQuery, null, null, executionController, System.nanoTime());
     }
 
     private class PagersIterator extends AbstractIterator<RowIterator> implements PartitionIterator
     {
         private final int pageSize;
         private PartitionIterator result;
-        private DataLimits.Counter counter;
+        private boolean closed;
+        private final long queryStartNanoTime;
 
         // For "normal" queries
         private final ConsistencyLevel consistency;
         private final ClientState clientState;
 
         // For internal queries
-        private final ReadOrderGroup orderGroup;
+        private final ReadExecutionController executionController;
 
-        public PagersIterator(int pageSize, ConsistencyLevel consistency, ClientState clientState, ReadOrderGroup orderGroup)
+        private int pagerMaxRemaining;
+        private int counted;
+
+        public PagersIterator(int pageSize, ConsistencyLevel consistency, ClientState clientState, ReadExecutionController executionController, long queryStartNanoTime)
         {
             this.pageSize = pageSize;
             this.consistency = consistency;
             this.clientState = clientState;
-            this.orderGroup = orderGroup;
-        }
-
-        public void setCounter(DataLimits.Counter counter)
-        {
-            this.counter = counter;
+            this.executionController = executionController;
+            this.queryStartNanoTime = queryStartNanoTime;
         }
 
         protected RowIterator computeNext()
@@ -182,24 +192,36 @@
             while (result == null || !result.hasNext())
             {
                 if (result != null)
+                {
                     result.close();
+                    counted += pagerMaxRemaining - pagers[current].maxRemaining();
+                }
 
-                // This sets us on the first non-exhausted pager
-                if (isExhausted())
+                // We are done if we have reached the page size or in the case of GROUP BY if the current pager
+                // is not exhausted.
+                boolean isDone = counted >= pageSize
+                        || (result != null && limit.isGroupByLimit() && !pagers[current].isExhausted());
+
+                // isExhausted() will sets us on the first non-exhausted pager
+                if (isDone || isExhausted())
+                {
+                    closed = true;
                     return endOfData();
+                }
 
-                int toQuery = pageSize - counter.counted();
+                pagerMaxRemaining = pagers[current].maxRemaining();
+                int toQuery = pageSize - counted;
                 result = consistency == null
-                       ? pagers[current].fetchPageInternal(toQuery, orderGroup)
-                       : pagers[current].fetchPage(toQuery, consistency, clientState);
+                       ? pagers[current].fetchPageInternal(toQuery, executionController)
+                       : pagers[current].fetchPage(toQuery, consistency, clientState, queryStartNanoTime);
             }
             return result.next();
         }
 
         public void close()
         {
-            remaining -= counter.counted();
-            if (result != null)
+            remaining -= counted;
+            if (result != null && !closed)
                 result.close();
         }
     }
@@ -208,4 +230,4 @@
     {
         return remaining;
     }
-}
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/service/pager/PagingState.java b/src/java/org/apache/cassandra/service/pager/PagingState.java
index 7de7e6f..10044a8 100644
--- a/src/java/org/apache/cassandra/service/pager/PagingState.java
+++ b/src/java/org/apache/cassandra/service/pager/PagingState.java
@@ -36,11 +36,10 @@
 import org.apache.cassandra.io.util.DataOutputBufferFixed;
 import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.transport.ProtocolException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 import static org.apache.cassandra.db.TypeSizes.sizeof;
 import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt;
-import static org.apache.cassandra.transport.Server.VERSION_3;
-import static org.apache.cassandra.transport.Server.VERSION_4;
 import static org.apache.cassandra.utils.ByteBufferUtil.*;
 import static org.apache.cassandra.utils.vint.VIntCoding.computeUnsignedVIntSize;
 import static org.apache.cassandra.utils.vint.VIntCoding.getUnsignedVInt;
@@ -61,12 +60,12 @@
         this.remainingInPartition = remainingInPartition;
     }
 
-    public ByteBuffer serialize(int protocolVersion)
+    public ByteBuffer serialize(ProtocolVersion protocolVersion)
     {
         assert rowMark == null || protocolVersion == rowMark.protocolVersion;
         try
         {
-            return protocolVersion > VERSION_3 ? modernSerialize() : legacySerialize(true);
+            return protocolVersion.isGreaterThan(ProtocolVersion.V3) ? modernSerialize() : legacySerialize(true);
         }
         catch (IOException e)
         {
@@ -74,11 +73,11 @@
         }
     }
 
-    public int serializedSize(int protocolVersion)
+    public int serializedSize(ProtocolVersion protocolVersion)
     {
         assert rowMark == null || protocolVersion == rowMark.protocolVersion;
 
-        return protocolVersion > VERSION_3 ? modernSerializedSize() : legacySerializedSize(true);
+        return protocolVersion.isGreaterThan(ProtocolVersion.V3) ? modernSerializedSize() : legacySerializedSize(true);
     }
 
     /**
@@ -87,7 +86,7 @@
      * a paging state that adheres to the protocol version provided, or, if not - see if it is in a different
      * version, in which case we try the other format.
      */
-    public static PagingState deserialize(ByteBuffer bytes, int protocolVersion)
+    public static PagingState deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion)
     {
         if (bytes == null)
             return null;
@@ -100,16 +99,16 @@
              * to a lesser extent, readWithShortLength()
              */
 
-            if (protocolVersion > VERSION_3)
+            if (protocolVersion.isGreaterThan(ProtocolVersion.V3))
             {
                 if (isModernSerialized(bytes)) return modernDeserialize(bytes, protocolVersion);
-                if (isLegacySerialized(bytes)) return legacyDeserialize(bytes, VERSION_3);
+                if (isLegacySerialized(bytes)) return legacyDeserialize(bytes, ProtocolVersion.V3);
             }
 
-            if (protocolVersion < VERSION_4)
+            if (protocolVersion.isSmallerThan(ProtocolVersion.V4))
             {
                 if (isLegacySerialized(bytes)) return legacyDeserialize(bytes, protocolVersion);
-                if (isModernSerialized(bytes)) return modernDeserialize(bytes, VERSION_4);
+                if (isModernSerialized(bytes)) return modernDeserialize(bytes, ProtocolVersion.V4);
             }
         }
         catch (IOException e)
@@ -135,43 +134,72 @@
         return out.buffer(false);
     }
 
-    private static boolean isModernSerialized(ByteBuffer bytes)
+    @VisibleForTesting
+    static boolean isModernSerialized(ByteBuffer bytes)
     {
         int index = bytes.position();
         int limit = bytes.limit();
 
-        long partitionKeyLen = getUnsignedVInt(bytes, index, limit);
+        int partitionKeyLen = toIntExact(getUnsignedVInt(bytes, index, limit));
         if (partitionKeyLen < 0)
             return false;
-        index += computeUnsignedVIntSize(partitionKeyLen) + partitionKeyLen;
-        if (index >= limit)
+        index = addNonNegative(index, computeUnsignedVIntSize(partitionKeyLen), partitionKeyLen);
+        if (index >= limit || index < 0)
             return false;
 
-        long rowMarkerLen = getUnsignedVInt(bytes, index, limit);
+        int rowMarkerLen = toIntExact(getUnsignedVInt(bytes, index, limit));
         if (rowMarkerLen < 0)
             return false;
-        index += computeUnsignedVIntSize(rowMarkerLen) + rowMarkerLen;
-        if (index >= limit)
+        index = addNonNegative(index, computeUnsignedVIntSize(rowMarkerLen), rowMarkerLen);
+        if (index >= limit || index < 0)
             return false;
 
-        long remaining = getUnsignedVInt(bytes, index, limit);
+        int remaining = toIntExact(getUnsignedVInt(bytes, index, limit));
         if (remaining < 0)
             return false;
-        index += computeUnsignedVIntSize(remaining);
-        if (index >= limit)
+        index = addNonNegative(index, computeUnsignedVIntSize(remaining));
+        if (index >= limit || index < 0)
             return false;
 
         long remainingInPartition = getUnsignedVInt(bytes, index, limit);
         if (remainingInPartition < 0)
             return false;
-        index += computeUnsignedVIntSize(remainingInPartition);
+        index = addNonNegative(index, computeUnsignedVIntSize(remainingInPartition));
         return index == limit;
     }
 
-    @SuppressWarnings({ "resource", "RedundantSuppression" })
-    private static PagingState modernDeserialize(ByteBuffer bytes, int protocolVersion) throws IOException
+    // Following operations are similar to Math.{addExact/toIntExact}, but without using exceptions for control flow.
+    // Since we're operating non-negative numbers, we can use -1 return value as an error code.
+    private static int addNonNegative(int x, int y)
     {
-        if (protocolVersion < VERSION_4)
+        int sum = x + y;
+        if (sum < 0)
+            return -1;
+        return sum;
+    }
+
+    private static int addNonNegative(int x, int y, int z)
+    {
+        int sum = x + y;
+        if (sum < 0)
+            return -1;
+        sum += z;
+        if (sum < 0)
+            return -1;
+        return sum;
+    }
+
+    private static int toIntExact(long value)
+    {
+        if ((int)value != value)
+            return -1;
+        return (int)value;
+    }
+
+    @SuppressWarnings({ "resource", "RedundantSuppression" })
+    private static PagingState modernDeserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) throws IOException
+    {
+        if (protocolVersion.isSmallerThan(ProtocolVersion.V4))
             throw new IllegalArgumentException();
 
         DataInputBuffer in = new DataInputBuffer(bytes, false);
@@ -215,7 +243,8 @@
         return out.buffer(false);
     }
 
-    private static boolean isLegacySerialized(ByteBuffer bytes)
+    @VisibleForTesting
+    static boolean isLegacySerialized(ByteBuffer bytes)
     {
         int index = bytes.position();
         int limit = bytes.limit();
@@ -254,9 +283,9 @@
     }
 
     @SuppressWarnings({ "resource", "RedundantSuppression" })
-    private static PagingState legacyDeserialize(ByteBuffer bytes, int protocolVersion) throws IOException
+    private static PagingState legacyDeserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) throws IOException
     {
-        if (protocolVersion > VERSION_3)
+        if (protocolVersion.isGreaterThan(ProtocolVersion.V3))
             throw new IllegalArgumentException();
 
         DataInputBuffer in = new DataInputBuffer(bytes, false);
@@ -325,9 +354,9 @@
     {
         // This can be null for convenience if no row is marked.
         private final ByteBuffer mark;
-        private final int protocolVersion;
+        private final ProtocolVersion protocolVersion;
 
-        private RowMark(ByteBuffer mark, int protocolVersion)
+        private RowMark(ByteBuffer mark, ProtocolVersion protocolVersion)
         {
             this.mark = mark;
             this.protocolVersion = protocolVersion;
@@ -347,10 +376,10 @@
             return l;
         }
 
-        public static RowMark create(CFMetaData metadata, Row row, int protocolVersion)
+        public static RowMark create(CFMetaData metadata, Row row, ProtocolVersion protocolVersion)
         {
             ByteBuffer mark;
-            if (protocolVersion <= VERSION_3)
+            if (protocolVersion.isSmallerOrEqualTo(ProtocolVersion.V3))
             {
                 // We need to be backward compatible with 2.1/2.2 nodes paging states. Which means we have to send
                 // the full cellname of the "last" cell in the row we get (since that's how 2.1/2.2 nodes will start after
@@ -383,7 +412,7 @@
             if (mark == null)
                 return null;
 
-            return protocolVersion <= VERSION_3
+            return protocolVersion.isSmallerOrEqualTo(ProtocolVersion.V3)
                  ? LegacyLayout.decodeClustering(metadata, mark)
                  : Clustering.serializer.deserialize(mark, MessagingService.VERSION_30, makeClusteringTypes(metadata));
         }
diff --git a/src/java/org/apache/cassandra/service/pager/PartitionRangeQueryPager.java b/src/java/org/apache/cassandra/service/pager/PartitionRangeQueryPager.java
index bee4a1e..75f76cb 100644
--- a/src/java/org/apache/cassandra/service/pager/PartitionRangeQueryPager.java
+++ b/src/java/org/apache/cassandra/service/pager/PartitionRangeQueryPager.java
@@ -17,14 +17,12 @@
  */
 package org.apache.cassandra.service.pager;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.filter.DataLimits;
 import org.apache.cassandra.db.rows.Row;
 import org.apache.cassandra.dht.*;
 import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Pages a PartitionRangeReadCommand.
@@ -34,12 +32,10 @@
  */
 public class PartitionRangeQueryPager extends AbstractQueryPager
 {
-    private static final Logger logger = LoggerFactory.getLogger(PartitionRangeQueryPager.class);
-
     private volatile DecoratedKey lastReturnedKey;
     private volatile PagingState.RowMark lastReturnedRow;
 
-    public PartitionRangeQueryPager(PartitionRangeReadCommand command, PagingState state, int protocolVersion)
+    public PartitionRangeQueryPager(PartitionRangeReadCommand command, PagingState state, ProtocolVersion protocolVersion)
     {
         super(command, protocolVersion);
 
@@ -51,6 +47,29 @@
         }
     }
 
+    public PartitionRangeQueryPager(ReadCommand command,
+                                    ProtocolVersion protocolVersion,
+                                    DecoratedKey lastReturnedKey,
+                                    PagingState.RowMark lastReturnedRow,
+                                    int remaining,
+                                    int remainingInPartition)
+    {
+        super(command, protocolVersion);
+        this.lastReturnedKey = lastReturnedKey;
+        this.lastReturnedRow = lastReturnedRow;
+        restoreState(lastReturnedKey, remaining, remainingInPartition);
+    }
+
+    public PartitionRangeQueryPager withUpdatedLimit(DataLimits newLimits)
+    {
+        return new PartitionRangeQueryPager(command.withUpdatedLimit(newLimits),
+                                            protocolVersion,
+                                            lastReturnedKey,
+                                            lastReturnedRow,
+                                            maxRemaining(),
+                                            remainingInPartition());
+    }
+
     public PagingState state()
     {
         return lastReturnedKey == null
diff --git a/src/java/org/apache/cassandra/service/pager/QueryPager.java b/src/java/org/apache/cassandra/service/pager/QueryPager.java
index cdf2b97..5d23997 100644
--- a/src/java/org/apache/cassandra/service/pager/QueryPager.java
+++ b/src/java/org/apache/cassandra/service/pager/QueryPager.java
@@ -18,8 +18,9 @@
 package org.apache.cassandra.service.pager;
 
 import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.filter.DataLimits;
 import org.apache.cassandra.db.EmptyIterators;
-import org.apache.cassandra.db.ReadOrderGroup;
 import org.apache.cassandra.db.partitions.PartitionIterator;
 import org.apache.cassandra.exceptions.RequestExecutionException;
 import org.apache.cassandra.exceptions.RequestValidationException;
@@ -46,19 +47,19 @@
  */
 public interface QueryPager
 {
-    public static final QueryPager EMPTY = new QueryPager()
+    QueryPager EMPTY = new QueryPager()
     {
-        public ReadOrderGroup startOrderGroup()
+        public ReadExecutionController executionController()
         {
-            return ReadOrderGroup.emptyGroup();
+            return ReadExecutionController.empty();
         }
 
-        public PartitionIterator fetchPage(int pageSize, ConsistencyLevel consistency, ClientState clientState) throws RequestValidationException, RequestExecutionException
+        public PartitionIterator fetchPage(int pageSize, ConsistencyLevel consistency, ClientState clientState, long queryStartNanoTime) throws RequestValidationException, RequestExecutionException
         {
             return EmptyIterators.partition();
         }
 
-        public PartitionIterator fetchPageInternal(int pageSize, ReadOrderGroup orderGroup) throws RequestValidationException, RequestExecutionException
+        public PartitionIterator fetchPageInternal(int pageSize, ReadExecutionController executionController) throws RequestValidationException, RequestExecutionException
         {
             return EmptyIterators.partition();
         }
@@ -77,6 +78,11 @@
         {
             return null;
         }
+
+        public QueryPager withUpdatedLimit(DataLimits newLimits)
+        {
+            throw new UnsupportedOperationException();
+        }
     };
 
     /**
@@ -88,7 +94,7 @@
      * {@code consistency} is a serial consistency.
      * @return the page of result.
      */
-    public PartitionIterator fetchPage(int pageSize, ConsistencyLevel consistency, ClientState clientState) throws RequestValidationException, RequestExecutionException;
+    public PartitionIterator fetchPage(int pageSize, ConsistencyLevel consistency, ClientState clientState, long queryStartNanoTime) throws RequestValidationException, RequestExecutionException;
 
     /**
      * Starts a new read operation.
@@ -99,16 +105,16 @@
      *
      * @return a newly started order group for this {@code QueryPager}.
      */
-    public ReadOrderGroup startOrderGroup();
+    public ReadExecutionController executionController();
 
     /**
      * Fetches the next page internally (in other, this does a local query).
      *
      * @param pageSize the maximum number of elements to return in the next page.
-     * @param orderGroup the {@code ReadOrderGroup} protecting the read.
+     * @param executionController the {@code ReadExecutionController} protecting the read.
      * @return the page of result.
      */
-    public PartitionIterator fetchPageInternal(int pageSize, ReadOrderGroup orderGroup) throws RequestValidationException, RequestExecutionException;
+    public PartitionIterator fetchPageInternal(int pageSize, ReadExecutionController executionController) throws RequestValidationException, RequestExecutionException;
 
     /**
      * Whether or not this pager is exhausted, i.e. whether or not a call to
@@ -134,4 +140,12 @@
      * beginning. If the pager is exhausted, the result is undefined.
      */
     public PagingState state();
+
+    /**
+     * Creates a new <code>QueryPager</code> that use the new limits.
+     *
+     * @param newLimits the new limits
+     * @return a new <code>QueryPager</code> that use the new limits
+     */
+    public QueryPager withUpdatedLimit(DataLimits newLimits);
 }
diff --git a/src/java/org/apache/cassandra/service/pager/QueryPagers.java b/src/java/org/apache/cassandra/service/pager/QueryPagers.java
index 6bc1f80..1a70864 100644
--- a/src/java/org/apache/cassandra/service/pager/QueryPagers.java
+++ b/src/java/org/apache/cassandra/service/pager/QueryPagers.java
@@ -24,7 +24,7 @@
 import org.apache.cassandra.exceptions.RequestExecutionException;
 import org.apache.cassandra.exceptions.RequestValidationException;
 import org.apache.cassandra.service.ClientState;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Static utility methods for paging.
@@ -45,15 +45,16 @@
                                  ClientState state,
                                  final int pageSize,
                                  int nowInSec,
-                                 boolean isForThrift) throws RequestValidationException, RequestExecutionException
+                                 boolean isForThrift,
+                                 long queryStartNanoTime) throws RequestValidationException, RequestExecutionException
     {
         SinglePartitionReadCommand command = SinglePartitionReadCommand.create(isForThrift, metadata, nowInSec, columnFilter, RowFilter.NONE, limits, key, filter);
-        final SinglePartitionPager pager = new SinglePartitionPager(command, null, Server.CURRENT_VERSION);
+        final SinglePartitionPager pager = new SinglePartitionPager(command, null, ProtocolVersion.CURRENT);
 
         int count = 0;
         while (!pager.isExhausted())
         {
-            try (PartitionIterator iter = pager.fetchPage(pageSize, consistencyLevel, state))
+            try (PartitionIterator iter = pager.fetchPage(pageSize, consistencyLevel, state, queryStartNanoTime))
             {
                 DataLimits.Counter counter = limits.newCounter(nowInSec, true, command.selectsFullPartition(), metadata.enforceStrictLiveness());
                 PartitionIterators.consume(counter.applyTo(iter));
diff --git a/src/java/org/apache/cassandra/service/pager/SinglePartitionPager.java b/src/java/org/apache/cassandra/service/pager/SinglePartitionPager.java
index 6f17284..e400fb6 100644
--- a/src/java/org/apache/cassandra/service/pager/SinglePartitionPager.java
+++ b/src/java/org/apache/cassandra/service/pager/SinglePartitionPager.java
@@ -19,12 +19,10 @@
 
 import java.nio.ByteBuffer;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.filter.*;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Common interface to single partition queries (by slice and by name).
@@ -33,13 +31,11 @@
  */
 public class SinglePartitionPager extends AbstractQueryPager
 {
-    private static final Logger logger = LoggerFactory.getLogger(SinglePartitionPager.class);
-
     private final SinglePartitionReadCommand command;
 
     private volatile PagingState.RowMark lastReturned;
 
-    public SinglePartitionPager(SinglePartitionReadCommand command, PagingState state, int protocolVersion)
+    public SinglePartitionPager(SinglePartitionReadCommand command, PagingState state, ProtocolVersion protocolVersion)
     {
         super(command, protocolVersion);
         this.command = command;
@@ -51,6 +47,28 @@
         }
     }
 
+    private SinglePartitionPager(SinglePartitionReadCommand command,
+                                 ProtocolVersion protocolVersion,
+                                 PagingState.RowMark rowMark,
+                                 int remaining,
+                                 int remainingInPartition)
+    {
+        super(command, protocolVersion);
+        this.command = command;
+        this.lastReturned = rowMark;
+        restoreState(command.partitionKey(), remaining, remainingInPartition);
+    }
+
+    @Override
+    public SinglePartitionPager withUpdatedLimit(DataLimits newLimits)
+    {
+        return new SinglePartitionPager(command.withUpdatedLimit(newLimits),
+                                        protocolVersion,
+                                        lastReturned,
+                                        maxRemaining(),
+                                        remainingInPartition());
+    }
+
     public ByteBuffer key()
     {
         return command.partitionKey().getKey();
@@ -70,7 +88,11 @@
 
     protected ReadCommand nextPageReadCommand(int pageSize)
     {
-        return command.forPaging(lastReturned == null ? null : lastReturned.clustering(command.metadata()), pageSize);
+        Clustering clustering = lastReturned == null ? null : lastReturned.clustering(command.metadata());
+        DataLimits limits = (lastReturned == null || command.isForThrift()) ? limits().forPaging(pageSize)
+                                                                            : limits().forPaging(pageSize, key(), remainingInPartition());
+
+        return command.forPaging(clustering, limits);
     }
 
     protected void recordLast(DecoratedKey key, Row last)
diff --git a/src/java/org/apache/cassandra/service/paxos/AbstractPaxosCallback.java b/src/java/org/apache/cassandra/service/paxos/AbstractPaxosCallback.java
index 37defde..90bfc5d 100644
--- a/src/java/org/apache/cassandra/service/paxos/AbstractPaxosCallback.java
+++ b/src/java/org/apache/cassandra/service/paxos/AbstractPaxosCallback.java
@@ -35,12 +35,14 @@
     protected final CountDownLatch latch;
     protected final int targets;
     private final ConsistencyLevel consistency;
+    private final long queryStartNanoTime;
 
-    public AbstractPaxosCallback(int targets, ConsistencyLevel consistency)
+    public AbstractPaxosCallback(int targets, ConsistencyLevel consistency, long queryStartNanoTime)
     {
         this.targets = targets;
         this.consistency = consistency;
         latch = new CountDownLatch(targets);
+        this.queryStartNanoTime = queryStartNanoTime;
     }
 
     public boolean isLatencyForSnitch()
@@ -57,7 +59,8 @@
     {
         try
         {
-            if (!latch.await(DatabaseDescriptor.getWriteRpcTimeout(), TimeUnit.MILLISECONDS))
+            long timeout = TimeUnit.MILLISECONDS.toNanos(DatabaseDescriptor.getWriteRpcTimeout()) - (System.nanoTime() - queryStartNanoTime);
+            if (!latch.await(timeout, TimeUnit.NANOSECONDS))
                 throw new WriteTimeoutException(WriteType.CAS, consistency, getResponseCount(), targets);
         }
         catch (InterruptedException ex)
diff --git a/src/java/org/apache/cassandra/service/paxos/Commit.java b/src/java/org/apache/cassandra/service/paxos/Commit.java
index a3f491b..f87e368 100644
--- a/src/java/org/apache/cassandra/service/paxos/Commit.java
+++ b/src/java/org/apache/cassandra/service/paxos/Commit.java
@@ -35,7 +35,6 @@
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.net.MessagingService;
-import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.UUIDGen;
 import org.apache.cassandra.utils.UUIDSerializer;
diff --git a/src/java/org/apache/cassandra/service/paxos/PrepareCallback.java b/src/java/org/apache/cassandra/service/paxos/PrepareCallback.java
index 26e292e..ea069f7 100644
--- a/src/java/org/apache/cassandra/service/paxos/PrepareCallback.java
+++ b/src/java/org/apache/cassandra/service/paxos/PrepareCallback.java
@@ -22,7 +22,6 @@
 
 
 import java.net.InetAddress;
-import java.nio.ByteBuffer;
 import java.util.Collections;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -49,9 +48,9 @@
 
     private final Map<InetAddress, Commit> commitsByReplica = new ConcurrentHashMap<InetAddress, Commit>();
 
-    public PrepareCallback(DecoratedKey key, CFMetaData metadata, int targets, ConsistencyLevel consistency)
+    public PrepareCallback(DecoratedKey key, CFMetaData metadata, int targets, ConsistencyLevel consistency, long queryStartNanoTime)
     {
-        super(targets, consistency);
+        super(targets, consistency, queryStartNanoTime);
         // need to inject the right key in the empty commit so comparing with empty commits in the reply works as expected
         mostRecentCommit = Commit.emptyCommit(key, metadata);
         mostRecentInProgressCommit = Commit.emptyCommit(key, metadata);
diff --git a/src/java/org/apache/cassandra/service/paxos/ProposeCallback.java b/src/java/org/apache/cassandra/service/paxos/ProposeCallback.java
index b0bd163..c9cb1f0 100644
--- a/src/java/org/apache/cassandra/service/paxos/ProposeCallback.java
+++ b/src/java/org/apache/cassandra/service/paxos/ProposeCallback.java
@@ -50,9 +50,9 @@
     private final int requiredAccepts;
     private final boolean failFast;
 
-    public ProposeCallback(int totalTargets, int requiredTargets, boolean failFast, ConsistencyLevel consistency)
+    public ProposeCallback(int totalTargets, int requiredTargets, boolean failFast, ConsistencyLevel consistency, long queryStartNanoTime)
     {
-        super(totalTargets, consistency);
+        super(totalTargets, consistency, queryStartNanoTime);
         this.requiredAccepts = requiredTargets;
         this.failFast = failFast;
     }
diff --git a/src/java/org/apache/cassandra/streaming/ConnectionHandler.java b/src/java/org/apache/cassandra/streaming/ConnectionHandler.java
index aa1c615..556748d 100644
--- a/src/java/org/apache/cassandra/streaming/ConnectionHandler.java
+++ b/src/java/org/apache/cassandra/streaming/ConnectionHandler.java
@@ -38,7 +38,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.concurrent.NamedThreadFactory;
+import io.netty.util.concurrent.FastThreadLocalThread;
 import org.apache.cassandra.io.util.DataOutputStreamPlus;
 import org.apache.cassandra.io.util.BufferedDataOutputStreamPlus;
 import org.apache.cassandra.io.util.WrappedDataOutputStreamPlus;
@@ -65,10 +65,10 @@
     private IncomingMessageHandler incoming;
     private OutgoingMessageHandler outgoing;
 
-    ConnectionHandler(StreamSession session)
+    ConnectionHandler(StreamSession session, int incomingSocketTimeout)
     {
         this.session = session;
-        this.incoming = new IncomingMessageHandler(session);
+        this.incoming = new IncomingMessageHandler(session, incomingSocketTimeout);
         this.outgoing = new OutgoingMessageHandler(session);
     }
 
@@ -158,8 +158,8 @@
         protected final StreamSession session;
 
         protected int protocolVersion;
-        protected Socket socket;
         private final boolean isOutgoingHandler;
+        protected Socket socket;
 
         private final AtomicReference<SettableFuture<?>> closeFuture = new AtomicReference<>();
         private IncomingStreamingConnection incomingConnection;
@@ -189,7 +189,7 @@
         }
 
         @SuppressWarnings("resource")
-        public void sendInitMessage() throws IOException
+        private void sendInitMessage() throws IOException
         {
             StreamInitMessage message = new StreamInitMessage(
                     FBUtilities.getBroadcastAddress(),
@@ -218,7 +218,7 @@
             if (initiator)
                 sendInitMessage();
 
-            new Thread(NamedThreadFactory.threadLocalDeallocator(this), name() + "-" + session.peer).start();
+            new FastThreadLocalThread(this, name() + "-" + socket.getRemoteSocketAddress()).start();
         }
 
         public ListenableFuture<?> close()
@@ -272,9 +272,26 @@
      */
     static class IncomingMessageHandler extends MessageHandler
     {
-        IncomingMessageHandler(StreamSession session)
+        private final int socketTimeout;
+
+        IncomingMessageHandler(StreamSession session, int socketTimeout)
         {
             super(session, false);
+            this.socketTimeout = socketTimeout;
+        }
+
+        @Override
+        public void start(Socket socket, int version, boolean initiator) throws IOException
+        {
+            try
+            {
+                socket.setSoTimeout(socketTimeout);
+            }
+            catch (SocketException e)
+            {
+                logger.warn("Could not set incoming socket timeout to {}", socketTimeout, e);
+            }
+            super.start(socket, version, initiator);
         }
 
         protected String name()
@@ -390,6 +407,7 @@
             {
                 StreamMessage.serialize(message, out, protocolVersion, session);
                 out.flush();
+                message.sent();
             }
             catch (SocketException e)
             {
diff --git a/src/java/org/apache/cassandra/streaming/StreamCoordinator.java b/src/java/org/apache/cassandra/streaming/StreamCoordinator.java
index 2838317..b801ecc 100644
--- a/src/java/org/apache/cassandra/streaming/StreamCoordinator.java
+++ b/src/java/org/apache/cassandra/streaming/StreamCoordinator.java
@@ -47,19 +47,23 @@
     // streaming is handled directly by the ConnectionHandler's incoming and outgoing threads.
     private static final DebuggableThreadPoolExecutor streamExecutor = DebuggableThreadPoolExecutor.createWithFixedPoolSize("StreamConnectionEstablisher",
                                                                                                                             FBUtilities.getAvailableProcessors());
+    private final boolean connectSequentially;
 
     private Map<InetAddress, HostStreamingData> peerSessions = new HashMap<>();
     private final int connectionsPerHost;
     private StreamConnectionFactory factory;
     private final boolean keepSSTableLevel;
     private final boolean isIncremental;
+    private Iterator<StreamSession> sessionsToConnect = null;
 
-    public StreamCoordinator(int connectionsPerHost, boolean keepSSTableLevel, boolean isIncremental, StreamConnectionFactory factory)
+    public StreamCoordinator(int connectionsPerHost, boolean keepSSTableLevel, boolean isIncremental,
+                             StreamConnectionFactory factory, boolean connectSequentially)
     {
         this.connectionsPerHost = connectionsPerHost;
         this.factory = factory;
         this.keepSSTableLevel = keepSSTableLevel;
         this.isIncremental = isIncremental;
+        this.connectSequentially = connectSequentially;
     }
 
     public void setConnectionFactory(StreamConnectionFactory factory)
@@ -95,12 +99,59 @@
         return connectionsPerHost == 0;
     }
 
-    public void connectAllStreamSessions()
+    public void connect(StreamResultFuture future)
+    {
+        if (this.connectSequentially)
+            connectSequentially(future);
+        else
+            connectAllStreamSessions();
+    }
+
+    private void connectAllStreamSessions()
     {
         for (HostStreamingData data : peerSessions.values())
             data.connectAllStreamSessions();
     }
 
+    private void connectSequentially(StreamResultFuture future)
+    {
+        sessionsToConnect = getAllStreamSessions().iterator();
+        future.addEventListener(new StreamEventHandler()
+        {
+            public void handleStreamEvent(StreamEvent event)
+            {
+                if (event.eventType == StreamEvent.Type.STREAM_PREPARED || event.eventType == StreamEvent.Type.STREAM_COMPLETE)
+                    connectNext();
+            }
+
+            public void onSuccess(StreamState result)
+            {
+
+            }
+
+            public void onFailure(Throwable t)
+            {
+
+            }
+        });
+        connectNext();
+    }
+
+    private void connectNext()
+    {
+        if (sessionsToConnect == null)
+            return;
+
+        if (sessionsToConnect.hasNext())
+        {
+            StreamSession next = sessionsToConnect.next();
+            logger.debug("Connecting next session {} with {}.", next.planId(), next.peer.getHostAddress());
+            streamExecutor.execute(new StreamSessionConnector(next));
+        }
+        else
+            logger.debug("Finished connecting all sessions");
+    }
+
     public synchronized Set<InetAddress> getPeers()
     {
         return new HashSet<>(peerSessions.keySet());
diff --git a/src/java/org/apache/cassandra/streaming/StreamEvent.java b/src/java/org/apache/cassandra/streaming/StreamEvent.java
index de3db9c..49172fb 100644
--- a/src/java/org/apache/cassandra/streaming/StreamEvent.java
+++ b/src/java/org/apache/cassandra/streaming/StreamEvent.java
@@ -18,11 +18,16 @@
 package org.apache.cassandra.streaming;
 
 import java.net.InetAddress;
+import java.util.Collections;
+import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
 
 import com.google.common.collect.ImmutableSet;
 
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.dht.Token;
+
 public abstract class StreamEvent
 {
     public static enum Type
@@ -47,6 +52,8 @@
         public final boolean success;
         public final int sessionIndex;
         public final Set<StreamRequest> requests;
+        public final String description;
+        public final Map<String, Set<Range<Token>>> transferredRangesPerKeyspace;
 
         public SessionCompleteEvent(StreamSession session)
         {
@@ -55,6 +62,8 @@
             this.success = session.isSuccess();
             this.sessionIndex = session.sessionIndex();
             this.requests = ImmutableSet.copyOf(session.requests);
+            this.description = session.description();
+            this.transferredRangesPerKeyspace = Collections.unmodifiableMap(session.transferredRangesPerKeyspace);
         }
     }
 
diff --git a/src/java/org/apache/cassandra/streaming/StreamHook.java b/src/java/org/apache/cassandra/streaming/StreamHook.java
new file mode 100644
index 0000000..d610297
--- /dev/null
+++ b/src/java/org/apache/cassandra/streaming/StreamHook.java
@@ -0,0 +1,57 @@
+/*
+ * 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.streaming;
+
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.io.sstable.SSTableMultiWriter;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.streaming.messages.OutgoingFileMessage;
+import org.apache.cassandra.utils.FBUtilities;
+
+public interface StreamHook
+{
+    public static final StreamHook instance = createHook();
+
+    public OutgoingFileMessage reportOutgoingFile(StreamSession session, SSTableReader sstable, OutgoingFileMessage message);
+    public void reportStreamFuture(StreamSession session, StreamResultFuture future);
+    public void reportIncomingFile(ColumnFamilyStore cfs, SSTableMultiWriter writer, StreamSession session, int sequenceNumber);
+
+    static StreamHook createHook()
+    {
+        String className =  System.getProperty("cassandra.stream_hook");
+        if (className != null)
+        {
+            return FBUtilities.construct(className, StreamHook.class.getSimpleName());
+        }
+        else
+        {
+            return new StreamHook()
+            {
+                public OutgoingFileMessage reportOutgoingFile(StreamSession session, SSTableReader sstable, OutgoingFileMessage message)
+                {
+                    return message;
+                }
+
+                public void reportStreamFuture(StreamSession session, StreamResultFuture future) {}
+
+                public void reportIncomingFile(ColumnFamilyStore cfs, SSTableMultiWriter writer, StreamSession session, int sequenceNumber) {}
+            };
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/streaming/StreamManager.java b/src/java/org/apache/cassandra/streaming/StreamManager.java
index 92d155e..cce049a 100644
--- a/src/java/org/apache/cassandra/streaming/StreamManager.java
+++ b/src/java/org/apache/cassandra/streaming/StreamManager.java
@@ -153,7 +153,7 @@
             {
                 initiatedStreams.remove(result.planId);
             }
-        }, MoreExecutors.sameThreadExecutor());
+        }, MoreExecutors.directExecutor());
 
         initiatedStreams.put(result.planId, result);
     }
@@ -168,7 +168,7 @@
             {
                 receivingStreams.remove(result.planId);
             }
-        }, MoreExecutors.sameThreadExecutor());
+        }, MoreExecutors.directExecutor());
 
         receivingStreams.put(result.planId, result);
     }
diff --git a/src/java/org/apache/cassandra/streaming/StreamPlan.java b/src/java/org/apache/cassandra/streaming/StreamPlan.java
index 0d963ed..e9d43cb 100644
--- a/src/java/org/apache/cassandra/streaming/StreamPlan.java
+++ b/src/java/org/apache/cassandra/streaming/StreamPlan.java
@@ -32,6 +32,7 @@
  */
 public class StreamPlan
 {
+    public static final String[] EMPTY_COLUMN_FAMILIES = new String[0];
     private final UUID planId = UUIDGen.getTimeUUID();
     private final String description;
     private final List<StreamEventHandler> handlers = new ArrayList<>();
@@ -47,19 +48,21 @@
      */
     public StreamPlan(String description)
     {
-        this(description, ActiveRepairService.UNREPAIRED_SSTABLE, 1, false, false);
+        this(description, ActiveRepairService.UNREPAIRED_SSTABLE, 1, false, false, false);
     }
 
-    public StreamPlan(String description, boolean keepSSTableLevels)
+    public StreamPlan(String description, boolean keepSSTableLevels, boolean connectSequentially)
     {
-        this(description, ActiveRepairService.UNREPAIRED_SSTABLE, 1, keepSSTableLevels, false);
+        this(description, ActiveRepairService.UNREPAIRED_SSTABLE, 1, keepSSTableLevels, false, connectSequentially);
     }
 
-    public StreamPlan(String description, long repairedAt, int connectionsPerHost, boolean keepSSTableLevels, boolean isIncremental)
+    public StreamPlan(String description, long repairedAt, int connectionsPerHost, boolean keepSSTableLevels,
+                      boolean isIncremental, boolean connectSequentially)
     {
         this.description = description;
         this.repairedAt = repairedAt;
-        this.coordinator = new StreamCoordinator(connectionsPerHost, keepSSTableLevels, isIncremental, new DefaultConnectionFactory());
+        this.coordinator = new StreamCoordinator(connectionsPerHost, keepSSTableLevels, isIncremental, new DefaultConnectionFactory(),
+                                                 connectSequentially);
     }
 
     /**
@@ -73,7 +76,7 @@
      */
     public StreamPlan requestRanges(InetAddress from, InetAddress connecting, String keyspace, Collection<Range<Token>> ranges)
     {
-        return requestRanges(from, connecting, keyspace, ranges, new String[0]);
+        return requestRanges(from, connecting, keyspace, ranges, EMPTY_COLUMN_FAMILIES);
     }
 
     /**
@@ -114,7 +117,7 @@
      */
     public StreamPlan transferRanges(InetAddress to, InetAddress connecting, String keyspace, Collection<Range<Token>> ranges)
     {
-        return transferRanges(to, connecting, keyspace, ranges, new String[0]);
+        return transferRanges(to, connecting, keyspace, ranges, EMPTY_COLUMN_FAMILIES);
     }
 
     /**
diff --git a/src/java/org/apache/cassandra/streaming/StreamReader.java b/src/java/org/apache/cassandra/streaming/StreamReader.java
index 07278cb..dbd5a4a 100644
--- a/src/java/org/apache/cassandra/streaming/StreamReader.java
+++ b/src/java/org/apache/cassandra/streaming/StreamReader.java
@@ -36,9 +36,9 @@
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
 import org.apache.cassandra.db.rows.*;
-import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.SSTableMultiWriter;
 import org.apache.cassandra.io.sstable.SSTableSimpleIterator;
+import org.apache.cassandra.io.sstable.format.RangeAwareSSTableWriter;
 import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.Version;
 import org.apache.cassandra.io.util.RewindableDataInputStreamPlus;
@@ -67,8 +67,6 @@
     protected final SerializationHeader.Component header;
     protected final int fileSeqNum;
 
-    protected Descriptor desc;
-
     public StreamReader(FileMessageHeader header, StreamSession session)
     {
         this.session = session;
@@ -119,17 +117,17 @@
             {
                 writePartition(deserializer, writer);
                 // TODO move this to BytesReadTracker
-                session.progress(desc, ProgressInfo.Direction.IN, in.getBytesRead(), totalSize);
+                session.progress(writer.getFilename(), ProgressInfo.Direction.IN, in.getBytesRead(), totalSize);
             }
             logger.debug("[Stream #{}] Finished receiving file #{} from {} readBytes = {}, totalSize = {}",
-                         session.planId(), fileSeqNum, session.peer, in.getBytesRead(), totalSize);
+                         session.planId(), fileSeqNum, session.peer, FBUtilities.prettyPrintMemory(in.getBytesRead()), FBUtilities.prettyPrintMemory(totalSize));
             return writer;
         }
         catch (Throwable e)
         {
             if (deserializer != null)
                 logger.warn("[Stream {}] Error while reading partition {} from stream on ks='{}' and table='{}'.",
-                            session.planId(), deserializer.partitionKey(), cfs.keyspace.getName(), cfs.getColumnFamilyName());
+                            session.planId(), deserializer.partitionKey(), cfs.keyspace.getName(), cfs.getTableName(), e);
             if (writer != null)
             {
                 writer.abort(e);
@@ -152,11 +150,12 @@
     {
         Directories.DataDirectory localDir = cfs.getDirectories().getWriteableLocation(totalSize);
         if (localDir == null)
-            throw new IOException("Insufficient disk space to store " + totalSize + " bytes");
-        desc = Descriptor.fromFilename(cfs.getSSTablePath(cfs.getDirectories().getLocationForDisk(localDir), format));
+            throw new IOException(String.format("Insufficient disk space to store %s", FBUtilities.prettyPrintMemory(totalSize)));
 
-        return cfs.createSSTableMultiWriter(desc, estimatedKeys, repairedAt, sstableLevel, getHeader(cfs.metadata),
-                session.getReceivingTask(cfId).createLifecycleNewTracker());
+        LifecycleNewTracker lifecycleNewTracker = session.getReceivingTask(cfId).createLifecycleNewTracker();
+        RangeAwareSSTableWriter writer = new RangeAwareSSTableWriter(cfs, estimatedKeys, repairedAt, format, sstableLevel, totalSize, lifecycleNewTracker, getHeader(cfs.metadata));
+        StreamHook.instance.reportIncomingFile(cfs, writer, session, fileSeqNum);
+        return writer;
     }
 
     protected long totalSize()
diff --git a/src/java/org/apache/cassandra/streaming/StreamReceiveTask.java b/src/java/org/apache/cassandra/streaming/StreamReceiveTask.java
index ea82d9b..5388dd6 100644
--- a/src/java/org/apache/cassandra/streaming/StreamReceiveTask.java
+++ b/src/java/org/apache/cassandra/streaming/StreamReceiveTask.java
@@ -37,6 +37,7 @@
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.Mutation;
 import org.apache.cassandra.db.compaction.OperationType;
+import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.db.rows.UnfilteredRowIterator;
@@ -182,6 +183,7 @@
         public void run()
         {
             boolean hasViews = false;
+            boolean hasCDC = false;
             ColumnFamilyStore cfs = null;
             try
             {
@@ -196,16 +198,22 @@
                 }
                 cfs = Keyspace.open(kscf.left).getColumnFamilyStore(kscf.right);
                 hasViews = !Iterables.isEmpty(View.findAll(kscf.left, kscf.right));
+                hasCDC = cfs.metadata.params.cdc;
 
                 Collection<SSTableReader> readers = task.sstables;
 
                 try (Refs<SSTableReader> refs = Refs.ref(readers))
                 {
-                    //We have a special path for views.
-                    //Since the view requires cleaning up any pre-existing state, we must put
-                    //all partitions through the same write path as normal mutations.
-                    //This also ensures any 2is are also updated
-                    if (hasViews)
+                    /*
+                     * We have a special path for views and for CDC.
+                     *
+                     * For views, since the view requires cleaning up any pre-existing state, we must put all partitions
+                     * through the same write path as normal mutations. This also ensures any 2is are also updated.
+                     *
+                     * For CDC-enabled tables, we want to ensure that the mutations are run through the CommitLog so they
+                     * can be archived by the CDC process on discard.
+                     */
+                    if (hasViews || hasCDC)
                     {
                         for (SSTableReader reader : readers)
                         {
@@ -216,8 +224,14 @@
                                 {
                                     try (UnfilteredRowIterator rowIterator = scanner.next())
                                     {
-                                        // MV *can* be applied unsafe as we flush below before transaction is done.
-                                        ks.apply(new Mutation(PartitionUpdate.fromIterator(rowIterator)), false, true, false);
+                                        Mutation m = new Mutation(PartitionUpdate.fromIterator(rowIterator, ColumnFilter.all(cfs.metadata)));
+
+                                        // MV *can* be applied unsafe if there's no CDC on the CFS as we flush below
+                                        // before transaction is done.
+                                        //
+                                        // If the CFS has CDC, however, these updates need to be written to the CommitLog
+                                        // so they get archived into the cdc_raw folder
+                                        ks.apply(m, hasCDC, true, false);
                                     }
                                 }
                             }
@@ -268,9 +282,9 @@
             }
             finally
             {
-                //We don't keep the streamed sstables since we've applied them manually
-                //So we abort the txn and delete the streamed sstables
-                if (hasViews)
+                // We don't keep the streamed sstables since we've applied them manually so we abort the txn and delete
+                // the streamed sstables.
+                if (hasViews || hasCDC)
                 {
                     if (cfs != null)
                         cfs.forceBlockingFlush();
diff --git a/src/java/org/apache/cassandra/streaming/StreamResultFuture.java b/src/java/org/apache/cassandra/streaming/StreamResultFuture.java
index c750d8c..481e93d 100644
--- a/src/java/org/apache/cassandra/streaming/StreamResultFuture.java
+++ b/src/java/org/apache/cassandra/streaming/StreamResultFuture.java
@@ -28,6 +28,7 @@
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.net.IncomingStreamingConnection;
+import org.apache.cassandra.utils.FBUtilities;
 
 /**
  * A future on the result ({@link StreamState}) of a streaming plan.
@@ -72,10 +73,12 @@
 
     private StreamResultFuture(UUID planId, String description, boolean keepSSTableLevels, boolean isIncremental)
     {
-        this(planId, description, new StreamCoordinator(0, keepSSTableLevels, isIncremental, new DefaultConnectionFactory()));
+        this(planId, description, new StreamCoordinator(0, keepSSTableLevels, isIncremental,
+                                                        new DefaultConnectionFactory(), false));
     }
 
-    static StreamResultFuture init(UUID planId, String description, Collection<StreamEventHandler> listeners, StreamCoordinator coordinator)
+    static StreamResultFuture init(UUID planId, String description, Collection<StreamEventHandler> listeners,
+                                   StreamCoordinator coordinator)
     {
         StreamResultFuture future = createAndRegister(planId, description, coordinator);
         if (listeners != null)
@@ -84,14 +87,15 @@
                 future.addEventListener(listener);
         }
 
-        logger.info("[Stream #{}] Executing streaming plan for {}", planId, description);
+        logger.info("[Stream #{}] Executing streaming plan for {}", planId,  description);
 
         // Initialize and start all sessions
         for (final StreamSession session : coordinator.getAllStreamSessions())
         {
             session.init(future);
         }
-        coordinator.connectAllStreamSessions();
+
+        coordinator.connect(future);
 
         return future;
     }
@@ -166,13 +170,13 @@
     void handleSessionPrepared(StreamSession session)
     {
         SessionInfo sessionInfo = session.getSessionInfo();
-        logger.info("[Stream #{} ID#{}] Prepare completed. Receiving {} files({} bytes), sending {} files({} bytes)",
-                    session.planId(),
-                    session.sessionIndex(),
-                    sessionInfo.getTotalFilesToReceive(),
-                    sessionInfo.getTotalSizeToReceive(),
-                    sessionInfo.getTotalFilesToSend(),
-                    sessionInfo.getTotalSizeToSend());
+        logger.info("[Stream #{} ID#{}] Prepare completed. Receiving {} files({}), sending {} files({})",
+                              session.planId(),
+                              session.sessionIndex(),
+                              sessionInfo.getTotalFilesToReceive(),
+                              FBUtilities.prettyPrintMemory(sessionInfo.getTotalSizeToReceive()),
+                              sessionInfo.getTotalFilesToSend(),
+                              FBUtilities.prettyPrintMemory(sessionInfo.getTotalSizeToSend()));
         StreamEvent.SessionPreparedEvent event = new StreamEvent.SessionPreparedEvent(planId, sessionInfo);
         coordinator.addSessionInfo(sessionInfo);
         fireStreamEvent(event);
diff --git a/src/java/org/apache/cassandra/streaming/StreamSession.java b/src/java/org/apache/cassandra/streaming/StreamSession.java
index 1ea16aa..675304f 100644
--- a/src/java/org/apache/cassandra/streaming/StreamSession.java
+++ b/src/java/org/apache/cassandra/streaming/StreamSession.java
@@ -28,6 +28,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.*;
 
+import org.apache.cassandra.concurrent.DebuggableScheduledThreadPoolExecutor;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
 import org.apache.cassandra.db.lifecycle.SSTableIntervalTree;
 import org.apache.cassandra.db.lifecycle.SSTableSet;
@@ -43,11 +44,11 @@
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.gms.*;
-import org.apache.cassandra.io.sstable.Component;
-import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.metrics.StreamingMetrics;
 import org.apache.cassandra.service.ActiveRepairService;
 import org.apache.cassandra.streaming.messages.*;
+import org.apache.cassandra.utils.CassandraVersion;
+import org.apache.cassandra.utils.ExecutorUtils;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.Pair;
@@ -118,7 +119,17 @@
  */
 public class StreamSession implements IEndpointStateChangeSubscriber
 {
+
+    /**
+     * Version where keep-alive support was added
+     */
+    private static final CassandraVersion STREAM_KEEP_ALIVE = new CassandraVersion("3.10");
     private static final Logger logger = LoggerFactory.getLogger(StreamSession.class);
+    private static final DebuggableScheduledThreadPoolExecutor keepAliveExecutor = new DebuggableScheduledThreadPoolExecutor("StreamKeepAliveExecutor");
+    static {
+        // Immediately remove keep-alive task when cancelled.
+        keepAliveExecutor.setRemoveOnCancelPolicy(true);
+    }
 
     /**
      * Streaming endpoint.
@@ -144,11 +155,14 @@
     /* can be null when session is created in remote */
     private final StreamConnectionFactory factory;
 
+    public final Map<String, Set<Range<Token>>> transferredRangesPerKeyspace = new HashMap<>();
+
     public final ConnectionHandler handler;
 
     private AtomicBoolean isAborted = new AtomicBoolean(false);
     private final boolean keepSSTableLevel;
     private final boolean isIncremental;
+    private ScheduledFuture<?> keepAliveFuture = null;
 
     public static enum State
     {
@@ -191,7 +205,9 @@
         this.connecting = connecting;
         this.index = index;
         this.factory = factory;
-        this.handler = new ConnectionHandler(this);
+        this.handler = new ConnectionHandler(this, isKeepAliveSupported()?
+                                                   (int)TimeUnit.SECONDS.toMillis(2 * DatabaseDescriptor.getStreamingKeepAlivePeriod()) :
+                                                   DatabaseDescriptor.getStreamingSocketTimeout());
         this.metrics = StreamingMetrics.get(connecting);
         this.keepSSTableLevel = keepSSTableLevel;
         this.isIncremental = isIncremental;
@@ -229,6 +245,12 @@
         return receivers.get(cfId);
     }
 
+    private boolean isKeepAliveSupported()
+    {
+        CassandraVersion peerVersion = Gossiper.instance.getReleaseVersion(peer);
+        return STREAM_KEEP_ALIVE.isSupportedBy(peerVersion);
+    }
+
     /**
      * Bind this session to report to specific {@link StreamResultFuture} and
      * perform pre-streaming initialization.
@@ -238,6 +260,12 @@
     public void init(StreamResultFuture streamResult)
     {
         this.streamResult = streamResult;
+        StreamHook.instance.reportStreamFuture(this, streamResult);
+
+        if (isKeepAliveSupported())
+            scheduleKeepAliveTask();
+        else
+            logger.debug("Peer {} does not support keep-alive.", peer);
     }
 
     public void start()
@@ -305,6 +333,13 @@
         try
         {
             addTransferFiles(sections);
+            Set<Range<Token>> toBeUpdated = transferredRangesPerKeyspace.get(keyspace);
+            if (toBeUpdated == null)
+            {
+                toBeUpdated = new HashSet<>();
+            }
+            toBeUpdated.addAll(ranges);
+            transferredRangesPerKeyspace.put(keyspace, toBeUpdated);
         }
         finally
         {
@@ -448,6 +483,13 @@
                     task.abort();
             }
 
+            if (keepAliveFuture != null)
+            {
+                logger.debug("[Stream #{}] Finishing keep-alive task.", planId());
+                keepAliveFuture.cancel(false);
+                keepAliveFuture = null;
+            }
+
             // Note that we shouldn't block on this close because this method is called on the handler
             // incoming thread (so we would deadlock).
             handler.close();
@@ -537,17 +579,7 @@
      */
     public void onError(Throwable e)
     {
-        if (e instanceof SocketTimeoutException)
-        {
-            logger.error("[Stream #{}] Streaming socket timed out. This means the session peer stopped responding or " +
-                         "is still processing received data. If there is no sign of failure in the other end or a very " +
-                         "dense table is being transferred you may want to increase streaming_socket_timeout_in_ms " +
-                         "property. Current value is {}ms.", planId(), DatabaseDescriptor.getStreamingSocketTimeout(), e);
-        }
-        else
-        {
-            logger.error("[Stream #{}] Streaming error occurred", planId(), e);
-        }
+        logError(e);
         // send session failure message
         if (handler.isOutgoingConnected())
             handler.sendMessage(new SessionFailedMessage());
@@ -555,6 +587,32 @@
         closeSession(State.FAILED);
     }
 
+    private void logError(Throwable e)
+    {
+        if (e instanceof SocketTimeoutException)
+        {
+            if (isKeepAliveSupported())
+                logger.error("[Stream #{}] Did not receive response from peer {}{} for {} secs. Is peer down? " +
+                             "If not, maybe try increasing streaming_keep_alive_period_in_secs.", planId(),
+                             peer.getHostAddress(),
+                             peer.equals(connecting) ? "" : " through " + connecting.getHostAddress(),
+                             2 * DatabaseDescriptor.getStreamingKeepAlivePeriod(),
+                             e);
+            else
+                logger.error("[Stream #{}] Streaming socket timed out. This means the session peer stopped responding or " +
+                             "is still processing received data. If there is no sign of failure in the other end or a very " +
+                             "dense table is being transferred you may want to increase streaming_socket_timeout_in_ms " +
+                             "property. Current value is {}ms.", planId(), DatabaseDescriptor.getStreamingSocketTimeout(), e);
+        }
+        else
+        {
+            logger.error("[Stream #{}] Streaming error occurred on session with peer {}{}", planId(),
+                                                                                            peer.getHostAddress(),
+                                                                                            peer.equals(connecting) ? "" : " through " + connecting.getHostAddress(),
+                                                                                            e);
+        }
+    }
+
     /**
      * Prepare this session for sending/receiving files.
      */
@@ -614,9 +672,9 @@
         receivers.get(message.header.cfId).received(message.sstable);
     }
 
-    public void progress(Descriptor desc, ProgressInfo.Direction direction, long bytes, long total)
+    public void progress(String filename, ProgressInfo.Direction direction, long bytes, long total)
     {
-        ProgressInfo progress = new ProgressInfo(peer, index, desc.filenameFor(Component.DATA), direction, bytes, total);
+        ProgressInfo progress = new ProgressInfo(peer, index, filename, direction, bytes, total);
         streamResult.handleProgress(progress);
     }
 
@@ -646,6 +704,16 @@
         }
     }
 
+    private synchronized void scheduleKeepAliveTask()
+    {
+        if (keepAliveFuture == null)
+        {
+            int keepAlivePeriod = DatabaseDescriptor.getStreamingKeepAlivePeriod();
+            logger.debug("[Stream #{}] Scheduling keep-alive task with {}s period.", planId(), keepAlivePeriod);
+            keepAliveFuture = keepAliveExecutor.scheduleAtFixedRate(new KeepAliveTask(), 0, keepAlivePeriod, TimeUnit.SECONDS);
+        }
+    }
+
     /**
      * Call back on receiving {@code StreamMessage.Type.SESSION_FAILED} message.
      */
@@ -752,4 +820,39 @@
                 taskCompleted(task); // there is no file to send
         }
     }
+
+    class KeepAliveTask implements Runnable
+    {
+        private KeepAliveMessage last = null;
+
+        public void run()
+        {
+            //to avoid jamming the message queue, we only send if the last one was sent
+            if (last == null || last.wasSent())
+            {
+                logger.trace("[Stream #{}] Sending keep-alive to {}.", planId(), peer);
+                last = new KeepAliveMessage();
+                try
+                {
+                    handler.sendMessage(last);
+                }
+                catch (RuntimeException e) //connection handler is closed
+                {
+                    logger.debug("[Stream #{}] Could not send keep-alive message (perhaps stream session is finished?).", planId(), e);
+                }
+            }
+            else
+            {
+                logger.trace("[Stream #{}] Skip sending keep-alive to {} (previous was not yet sent).", planId(), peer);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public static void shutdownAndWait(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        List<ExecutorService> executors = ImmutableList.of(keepAliveExecutor);
+        ExecutorUtils.shutdownNow(executors);
+        ExecutorUtils.awaitTermination(timeout, unit, executors);
+    }
 }
diff --git a/src/java/org/apache/cassandra/streaming/StreamTransferTask.java b/src/java/org/apache/cassandra/streaming/StreamTransferTask.java
index c1c5055..4f313c3 100644
--- a/src/java/org/apache/cassandra/streaming/StreamTransferTask.java
+++ b/src/java/org/apache/cassandra/streaming/StreamTransferTask.java
@@ -24,14 +24,12 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Throwables;
-import com.google.common.collect.Iterables;
 
 import org.apache.cassandra.concurrent.NamedThreadFactory;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.streaming.messages.OutgoingFileMessage;
 import org.apache.cassandra.utils.Pair;
 import org.apache.cassandra.utils.concurrent.Ref;
-import org.apache.cassandra.utils.concurrent.RefCounted;
 
 /**
  * StreamTransferTask sends sections of SSTable files in certain ColumnFamily.
@@ -58,6 +56,7 @@
     {
         assert ref.get() != null && cfId.equals(ref.get().metadata.cfId);
         OutgoingFileMessage message = new OutgoingFileMessage(ref, sequenceNumber.getAndIncrement(), estimatedKeys, sections, repairedAt, session.keepSSTableLevel());
+        message = StreamHook.instance.reportOutgoingFile(session, ref.get(), message);
         files.put(message.header.sequenceNumber, message);
         totalSize += message.header.size();
     }
diff --git a/src/java/org/apache/cassandra/streaming/StreamWriter.java b/src/java/org/apache/cassandra/streaming/StreamWriter.java
index 721ae1e..6c86c8b 100644
--- a/src/java/org/apache/cassandra/streaming/StreamWriter.java
+++ b/src/java/org/apache/cassandra/streaming/StreamWriter.java
@@ -32,9 +32,9 @@
 import org.apache.cassandra.io.util.DataIntegrityMetadata;
 import org.apache.cassandra.io.util.DataIntegrityMetadata.ChecksumValidator;
 import org.apache.cassandra.io.util.DataOutputStreamPlus;
-import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.io.util.RandomAccessReader;
 import org.apache.cassandra.streaming.StreamManager.StreamRateLimiter;
+import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.Pair;
 
 /**
@@ -108,7 +108,7 @@
                     long lastBytesRead = write(file, validator, readOffset, length, bytesRead);
                     bytesRead += lastBytesRead;
                     progress += (lastBytesRead - readOffset);
-                    session.progress(sstable.descriptor, ProgressInfo.Direction.OUT, progress, totalSize);
+                    session.progress(sstable.descriptor.filenameFor(Component.DATA), ProgressInfo.Direction.OUT, progress, totalSize);
                     readOffset = 0;
                 }
 
@@ -116,7 +116,7 @@
                 compressedOutput.flush();
             }
             logger.debug("[Stream #{}] Finished streaming file {} to {}, bytesTransferred = {}, totalSize = {}",
-                         session.planId(), sstable.getFilename(), session.peer, progress, totalSize);
+                         session.planId(), sstable.getFilename(), session.peer, FBUtilities.prettyPrintMemory(progress), FBUtilities.prettyPrintMemory(totalSize));
         }
     }
 
diff --git a/src/java/org/apache/cassandra/streaming/compress/CompressedInputStream.java b/src/java/org/apache/cassandra/streaming/compress/CompressedInputStream.java
index e3d698e..8a32d7a 100644
--- a/src/java/org/apache/cassandra/streaming/compress/CompressedInputStream.java
+++ b/src/java/org/apache/cassandra/streaming/compress/CompressedInputStream.java
@@ -25,15 +25,13 @@
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.function.Supplier;
-import java.util.zip.Checksum;
 
 import com.google.common.collect.Iterators;
 import com.google.common.primitives.Ints;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.concurrent.NamedThreadFactory;
+import io.netty.util.concurrent.FastThreadLocalThread;
 import org.apache.cassandra.io.compress.CompressionMetadata;
 import org.apache.cassandra.utils.ChecksumType;
 import org.apache.cassandra.utils.WrappedRunnable;
@@ -61,7 +59,7 @@
     // number of bytes in the buffer that are actually valid
     protected int validBufferBytes = -1;
 
-    private final Checksum checksum;
+    private final ChecksumType checksumType;
 
     // raw checksum bytes
     private final byte[] checksumBytes = new byte[4];
@@ -86,43 +84,73 @@
     public CompressedInputStream(InputStream source, CompressionInfo info, ChecksumType checksumType, Supplier<Double> crcCheckChanceSupplier)
     {
         this.info = info;
-        this.checksum =  checksumType.newInstance();
         this.buffer = new byte[info.parameters.chunkLength()];
         // buffer is limited to store up to 1024 chunks
         this.dataBuffer = new ArrayBlockingQueue<>(Math.min(info.chunks.length, 1024));
         this.crcCheckChanceSupplier = crcCheckChanceSupplier;
+        this.checksumType = checksumType;
 
-        new Thread(NamedThreadFactory.threadLocalDeallocator(new Reader(source, info, dataBuffer))).start();
+        new FastThreadLocalThread(new Reader(source, info, dataBuffer)).start();
     }
 
-    public int read() throws IOException
+    private void decompressNextChunk() throws IOException
     {
         if (readException != null)
             throw readException;
 
-        if (current >= bufferOffset + buffer.length || validBufferBytes == -1)
+        try
         {
-            try
+            byte[] compressedWithCRC = dataBuffer.take();
+            if (compressedWithCRC == POISON_PILL)
             {
-                byte[] compressedWithCRC = dataBuffer.take();
-                if (compressedWithCRC == POISON_PILL)
-                {
-                    assert readException != null;
-                    throw readException;
-                }
-                decompress(compressedWithCRC);
+                assert readException != null;
+                throw readException;
             }
-            catch (InterruptedException e)
-            {
-                throw new EOFException("No chunk available");
-            }
+            decompress(compressedWithCRC);
         }
+        catch (InterruptedException e)
+        {
+            throw new EOFException("No chunk available");
+        }
+    }
+
+    @Override
+    public int read() throws IOException
+    {
+        if (current >= bufferOffset + buffer.length || validBufferBytes == -1)
+            decompressNextChunk();
 
         assert current >= bufferOffset && current < bufferOffset + validBufferBytes;
 
         return ((int) buffer[(int) (current++ - bufferOffset)]) & 0xff;
     }
 
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException
+    {
+        long nextCurrent = current + len;
+
+        if (current >= bufferOffset + buffer.length || validBufferBytes == -1)
+            decompressNextChunk();
+
+        assert nextCurrent >= bufferOffset;
+
+        int read = 0;
+        while (read < len)
+        {
+            int nextLen = Math.min((len - read), (int)((bufferOffset + validBufferBytes) - current));
+
+            System.arraycopy(buffer, (int)(current - bufferOffset), b, off + read, nextLen);
+            read += nextLen;
+
+            current += nextLen;
+            if (read != len)
+                decompressNextChunk();
+        }
+
+        return len;
+    }
+
     public void position(long position)
     {
         assert position >= current : "stream can only read forward.";
@@ -139,14 +167,11 @@
         if (this.crcCheckChanceSupplier.get() >= 1d ||
             this.crcCheckChanceSupplier.get() > ThreadLocalRandom.current().nextDouble())
         {
-            checksum.update(compressed, 0, compressed.length - checksumBytes.length);
+            int checksum = (int) checksumType.of(compressed, 0, compressed.length - checksumBytes.length);
 
             System.arraycopy(compressed, compressed.length - checksumBytes.length, checksumBytes, 0, checksumBytes.length);
-            if (Ints.fromByteArray(checksumBytes) != (int) checksum.getValue())
+            if (Ints.fromByteArray(checksumBytes) != checksum)
                 throw new IOException("CRC unmatched");
-
-            // reset checksum object back to the original (blank) state
-            checksum.reset();
         }
 
         // buffer offset is always aligned
diff --git a/src/java/org/apache/cassandra/streaming/compress/CompressedStreamReader.java b/src/java/org/apache/cassandra/streaming/compress/CompressedStreamReader.java
index bc87c8f..70b5765 100644
--- a/src/java/org/apache/cassandra/streaming/compress/CompressedStreamReader.java
+++ b/src/java/org/apache/cassandra/streaming/compress/CompressedStreamReader.java
@@ -22,9 +22,6 @@
 import java.nio.channels.ReadableByteChannel;
 
 import com.google.common.base.Throwables;
-
-import org.apache.cassandra.io.sstable.SSTableMultiWriter;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -32,11 +29,13 @@
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.io.compress.CompressionMetadata;
+import org.apache.cassandra.io.sstable.SSTableMultiWriter;
+import org.apache.cassandra.io.util.TrackedInputStream;
 import org.apache.cassandra.streaming.ProgressInfo;
 import org.apache.cassandra.streaming.StreamReader;
 import org.apache.cassandra.streaming.StreamSession;
 import org.apache.cassandra.streaming.messages.FileMessageHeader;
-import org.apache.cassandra.io.util.TrackedInputStream;
+import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.Pair;
 
 import static org.apache.cassandra.utils.Throwables.extractIOExceptionCause;
@@ -91,6 +90,7 @@
         try
         {
             writer = createWriter(cfs, totalSize, repairedAt, format);
+            String filename = writer.getFilename();
             int sectionIdx = 0;
             for (Pair<Long, Long> section : sections)
             {
@@ -106,11 +106,11 @@
                 {
                     writePartition(deserializer, writer);
                     // when compressed, report total bytes of compressed chunks read since remoteFile.size is the sum of chunks transferred
-                    session.progress(desc, ProgressInfo.Direction.IN, cis.getTotalCompressedBytesRead(), totalSize);
+                    session.progress(filename, ProgressInfo.Direction.IN, cis.getTotalCompressedBytesRead(), totalSize);
                 }
             }
             logger.debug("[Stream #{}] Finished receiving file #{} from {} readBytes = {}, totalSize = {}", session.planId(), fileSeqNum,
-                         session.peer, cis.getTotalCompressedBytesRead(), totalSize);
+                         session.peer, FBUtilities.prettyPrintMemory(cis.getTotalCompressedBytesRead()), FBUtilities.prettyPrintMemory(totalSize));
             return writer;
         }
         catch (Throwable e)
diff --git a/src/java/org/apache/cassandra/streaming/compress/CompressedStreamWriter.java b/src/java/org/apache/cassandra/streaming/compress/CompressedStreamWriter.java
index f37af29..185ab22 100644
--- a/src/java/org/apache/cassandra/streaming/compress/CompressedStreamWriter.java
+++ b/src/java/org/apache/cassandra/streaming/compress/CompressedStreamWriter.java
@@ -18,25 +18,22 @@
 package org.apache.cassandra.streaming.compress;
 
 import java.io.IOException;
-import java.nio.channels.WritableByteChannel;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
-import com.google.common.base.Function;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.io.compress.CompressionMetadata;
+import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.util.ChannelProxy;
 import org.apache.cassandra.io.util.DataOutputStreamPlus;
-import org.apache.cassandra.io.util.FileUtils;
-import org.apache.cassandra.io.util.RandomAccessReader;
 import org.apache.cassandra.streaming.ProgressInfo;
 import org.apache.cassandra.streaming.StreamSession;
 import org.apache.cassandra.streaming.StreamWriter;
+import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.Pair;
 
 /**
@@ -88,11 +85,11 @@
                     long lastWrite = out.applyToChannel((wbc) -> fc.transferTo(section.left + bytesTransferredFinal, toTransfer, wbc));
                     bytesTransferred += lastWrite;
                     progress += lastWrite;
-                    session.progress(sstable.descriptor, ProgressInfo.Direction.OUT, progress, totalSize);
+                    session.progress(sstable.descriptor.filenameFor(Component.DATA), ProgressInfo.Direction.OUT, progress, totalSize);
                 }
             }
             logger.debug("[Stream #{}] Finished streaming file {} to {}, bytesTransferred = {}, totalSize = {}",
-                         session.planId(), sstable.getFilename(), session.peer, progress, totalSize);
+                         session.planId(), sstable.getFilename(), session.peer, FBUtilities.prettyPrintMemory(progress), FBUtilities.prettyPrintMemory(totalSize));
         }
     }
 
diff --git a/src/java/org/apache/cassandra/streaming/management/ProgressInfoCompositeData.java b/src/java/org/apache/cassandra/streaming/management/ProgressInfoCompositeData.java
index a54498d..b9e6951 100644
--- a/src/java/org/apache/cassandra/streaming/management/ProgressInfoCompositeData.java
+++ b/src/java/org/apache/cassandra/streaming/management/ProgressInfoCompositeData.java
@@ -53,7 +53,8 @@
                                                                    SimpleType.LONG};
 
     public static final CompositeType COMPOSITE_TYPE;
-    static  {
+    static
+    {
         try
         {
             COMPOSITE_TYPE = new CompositeType(ProgressInfo.class.getName(),
diff --git a/src/java/org/apache/cassandra/streaming/management/SessionCompleteEventCompositeData.java b/src/java/org/apache/cassandra/streaming/management/SessionCompleteEventCompositeData.java
index 3351e6e..516582a 100644
--- a/src/java/org/apache/cassandra/streaming/management/SessionCompleteEventCompositeData.java
+++ b/src/java/org/apache/cassandra/streaming/management/SessionCompleteEventCompositeData.java
@@ -38,7 +38,8 @@
                                                                    SimpleType.BOOLEAN};
 
     public static final CompositeType COMPOSITE_TYPE;
-    static  {
+    static
+    {
         try
         {
             COMPOSITE_TYPE = new CompositeType(StreamEvent.SessionCompleteEvent.class.getName(),
diff --git a/src/java/org/apache/cassandra/streaming/management/SessionInfoCompositeData.java b/src/java/org/apache/cassandra/streaming/management/SessionInfoCompositeData.java
index 63e4ab7..a6762a8 100644
--- a/src/java/org/apache/cassandra/streaming/management/SessionInfoCompositeData.java
+++ b/src/java/org/apache/cassandra/streaming/management/SessionInfoCompositeData.java
@@ -55,7 +55,8 @@
     private static final OpenType<?>[] ITEM_TYPES;
 
     public static final CompositeType COMPOSITE_TYPE;
-    static  {
+    static
+    {
         try
         {
             ITEM_TYPES = new OpenType[]{SimpleType.STRING,
diff --git a/src/java/org/apache/cassandra/streaming/management/StreamEventJMXNotifier.java b/src/java/org/apache/cassandra/streaming/management/StreamEventJMXNotifier.java
index 01ca9a1..3e12c2a 100644
--- a/src/java/org/apache/cassandra/streaming/management/StreamEventJMXNotifier.java
+++ b/src/java/org/apache/cassandra/streaming/management/StreamEventJMXNotifier.java
@@ -37,7 +37,8 @@
     public void handleStreamEvent(StreamEvent event)
     {
         Notification notif = null;
-        switch (event.eventType) {
+        switch (event.eventType)
+        {
             case STREAM_PREPARED:
                 notif = new Notification(StreamEvent.SessionPreparedEvent.class.getCanonicalName(),
                                                 StreamManagerMBean.OBJECT_NAME,
@@ -60,7 +61,9 @@
                                              seq.getAndIncrement());
                     notif.setUserData(ProgressInfoCompositeData.toCompositeData(event.planId, progress));
                     progressLastSent = System.currentTimeMillis();
-                } else {
+                }
+                else
+                {
                     return;
                 }
                 break;
diff --git a/src/java/org/apache/cassandra/streaming/management/StreamStateCompositeData.java b/src/java/org/apache/cassandra/streaming/management/StreamStateCompositeData.java
index 3752d39..e25ab1a 100644
--- a/src/java/org/apache/cassandra/streaming/management/StreamStateCompositeData.java
+++ b/src/java/org/apache/cassandra/streaming/management/StreamStateCompositeData.java
@@ -48,7 +48,8 @@
     private static final OpenType<?>[] ITEM_TYPES;
 
     public static final CompositeType COMPOSITE_TYPE;
-    static  {
+    static
+    {
         try
         {
             ITEM_TYPES = new OpenType[]{SimpleType.STRING,
diff --git a/src/java/org/apache/cassandra/streaming/management/StreamSummaryCompositeData.java b/src/java/org/apache/cassandra/streaming/management/StreamSummaryCompositeData.java
index e93069c..9ef23ab 100644
--- a/src/java/org/apache/cassandra/streaming/management/StreamSummaryCompositeData.java
+++ b/src/java/org/apache/cassandra/streaming/management/StreamSummaryCompositeData.java
@@ -41,7 +41,8 @@
                                                                    SimpleType.LONG};
 
     public static final CompositeType COMPOSITE_TYPE;
-    static  {
+    static
+    {
         try
         {
             COMPOSITE_TYPE = new CompositeType(StreamSummary.class.getName(),
diff --git a/src/java/org/apache/cassandra/streaming/messages/FileMessageHeader.java b/src/java/org/apache/cassandra/streaming/messages/FileMessageHeader.java
index 0e06bc0..232727d 100644
--- a/src/java/org/apache/cassandra/streaming/messages/FileMessageHeader.java
+++ b/src/java/org/apache/cassandra/streaming/messages/FileMessageHeader.java
@@ -22,10 +22,8 @@
 import java.util.List;
 import java.util.UUID;
 
-import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.SerializationHeader;
 import org.apache.cassandra.db.TypeSizes;
-import org.apache.cassandra.io.IVersionedSerializer;
 import org.apache.cassandra.io.compress.CompressionMetadata;
 import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.util.DataInputPlus;
@@ -223,7 +221,7 @@
         {
             UUID cfId = UUIDSerializer.serializer.deserialize(in, MessagingService.current_version);
             int sequenceNumber = in.readInt();
-            Version sstableVersion = DatabaseDescriptor.getSSTableFormat().info.getVersion(in.readUTF());
+            Version sstableVersion = SSTableFormat.Type.current().info.getVersion(in.readUTF());
 
             SSTableFormat.Type format = SSTableFormat.Type.LEGACY;
             if (version >= StreamMessage.VERSION_22)
diff --git a/src/java/org/apache/cassandra/streaming/messages/IncomingFileMessage.java b/src/java/org/apache/cassandra/streaming/messages/IncomingFileMessage.java
index 438cb0b..66480d4 100644
--- a/src/java/org/apache/cassandra/streaming/messages/IncomingFileMessage.java
+++ b/src/java/org/apache/cassandra/streaming/messages/IncomingFileMessage.java
@@ -60,7 +60,7 @@
             }
         }
 
-        public void serialize(IncomingFileMessage message, DataOutputStreamPlus out, int version, StreamSession session) throws IOException
+        public void serialize(IncomingFileMessage message, DataOutputStreamPlus out, int version, StreamSession session)
         {
             throw new UnsupportedOperationException("Not allowed to call serialize on an incoming file");
         }
diff --git a/src/java/org/apache/cassandra/streaming/messages/KeepAliveMessage.java b/src/java/org/apache/cassandra/streaming/messages/KeepAliveMessage.java
new file mode 100644
index 0000000..bfdc72e
--- /dev/null
+++ b/src/java/org/apache/cassandra/streaming/messages/KeepAliveMessage.java
@@ -0,0 +1,49 @@
+/*
+ * 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.streaming.messages;
+
+import java.io.IOException;
+import java.nio.channels.ReadableByteChannel;
+
+import org.apache.cassandra.io.util.DataOutputStreamPlus;
+import org.apache.cassandra.streaming.StreamSession;
+
+public class KeepAliveMessage extends StreamMessage
+{
+    public static Serializer<KeepAliveMessage> serializer = new Serializer<KeepAliveMessage>()
+    {
+        public KeepAliveMessage deserialize(ReadableByteChannel in, int version, StreamSession session) throws IOException
+        {
+            return new KeepAliveMessage();
+        }
+
+        public void serialize(KeepAliveMessage message, DataOutputStreamPlus out, int version, StreamSession session)
+        {}
+    };
+
+    public KeepAliveMessage()
+    {
+        super(Type.KEEP_ALIVE);
+    }
+
+    public String toString()
+    {
+        return "keep-alive";
+    }
+}
diff --git a/src/java/org/apache/cassandra/streaming/messages/OutgoingFileMessage.java b/src/java/org/apache/cassandra/streaming/messages/OutgoingFileMessage.java
index b2621f3..6723d17 100644
--- a/src/java/org/apache/cassandra/streaming/messages/OutgoingFileMessage.java
+++ b/src/java/org/apache/cassandra/streaming/messages/OutgoingFileMessage.java
@@ -23,7 +23,6 @@
 
 import com.google.common.annotations.VisibleForTesting;
 
-import org.apache.cassandra.io.compress.CompressionMetadata;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.util.DataOutputStreamPlus;
 import org.apache.cassandra.streaming.StreamSession;
@@ -40,7 +39,7 @@
 {
     public static Serializer<OutgoingFileMessage> serializer = new Serializer<OutgoingFileMessage>()
     {
-        public OutgoingFileMessage deserialize(ReadableByteChannel in, int version, StreamSession session) throws IOException
+        public OutgoingFileMessage deserialize(ReadableByteChannel in, int version, StreamSession session)
         {
             throw new UnsupportedOperationException("Not allowed to call deserialize on an outgoing file");
         }
diff --git a/src/java/org/apache/cassandra/streaming/messages/StreamMessage.java b/src/java/org/apache/cassandra/streaming/messages/StreamMessage.java
index eb7086f..7487aaf 100644
--- a/src/java/org/apache/cassandra/streaming/messages/StreamMessage.java
+++ b/src/java/org/apache/cassandra/streaming/messages/StreamMessage.java
@@ -38,6 +38,8 @@
     public static final int VERSION_30 = 4;
     public static final int CURRENT_VERSION = VERSION_30;
 
+    private transient volatile boolean sent = false;
+
     public static void serialize(StreamMessage message, DataOutputStreamPlus out, int version, StreamSession session) throws IOException
     {
         ByteBuffer buff = ByteBuffer.allocate(1);
@@ -70,6 +72,16 @@
         }
     }
 
+    public void sent()
+    {
+        sent = true;
+    }
+
+    public boolean wasSent()
+    {
+        return sent;
+    }
+
     /** StreamMessage serializer */
     public static interface Serializer<V extends StreamMessage>
     {
@@ -85,7 +97,8 @@
         RECEIVED(3, 4, ReceivedMessage.serializer),
         RETRY(4, 4, RetryMessage.serializer),
         COMPLETE(5, 1, CompleteMessage.serializer),
-        SESSION_FAILED(6, 5, SessionFailedMessage.serializer);
+        SESSION_FAILED(6, 5, SessionFailedMessage.serializer),
+        KEEP_ALIVE(7, 5, KeepAliveMessage.serializer);
 
         public static Type get(byte type)
         {
diff --git a/src/java/org/apache/cassandra/thrift/CassandraServer.java b/src/java/org/apache/cassandra/thrift/CassandraServer.java
index 163eb2d..444a938 100644
--- a/src/java/org/apache/cassandra/thrift/CassandraServer.java
+++ b/src/java/org/apache/cassandra/thrift/CassandraServer.java
@@ -86,7 +86,7 @@
         return ThriftSessionManager.instance.currentSession();
     }
 
-    protected PartitionIterator read(List<SinglePartitionReadCommand> commands, org.apache.cassandra.db.ConsistencyLevel consistency_level, ClientState cState)
+    protected PartitionIterator read(List<SinglePartitionReadCommand> commands, org.apache.cassandra.db.ConsistencyLevel consistency_level, ClientState cState, long queryStartNanoTime)
     throws org.apache.cassandra.exceptions.InvalidRequestException, UnavailableException, TimedOutException
     {
         try
@@ -94,7 +94,7 @@
             schedule(DatabaseDescriptor.getReadRpcTimeout());
             try
             {
-                return StorageProxy.read(new SinglePartitionReadCommand.Group(commands, DataLimits.NONE), consistency_level, cState);
+                return StorageProxy.read(new SinglePartitionReadCommand.Group(commands, DataLimits.NONE), consistency_level, cState, queryStartNanoTime);
             }
             finally
             {
@@ -258,10 +258,10 @@
              : result;
     }
 
-    private Map<ByteBuffer, List<ColumnOrSuperColumn>> getSlice(List<SinglePartitionReadCommand> commands, boolean subColumnsOnly, int cellLimit, org.apache.cassandra.db.ConsistencyLevel consistency_level, ClientState cState)
+    private Map<ByteBuffer, List<ColumnOrSuperColumn>> getSlice(List<SinglePartitionReadCommand> commands, boolean subColumnsOnly, int cellLimit, org.apache.cassandra.db.ConsistencyLevel consistency_level, ClientState cState, long queryStartNanoTime)
     throws org.apache.cassandra.exceptions.InvalidRequestException, UnavailableException, TimedOutException
     {
-        try (PartitionIterator results = read(commands, consistency_level, cState))
+        try (PartitionIterator results = read(commands, consistency_level, cState, queryStartNanoTime))
         {
             Map<ByteBuffer, List<ColumnOrSuperColumn>> columnFamiliesMap = new HashMap<>();
             while (results.hasNext())
@@ -279,6 +279,7 @@
     public List<ColumnOrSuperColumn> get_slice(ByteBuffer key, ColumnParent column_parent, SlicePredicate predicate, ConsistencyLevel consistency_level)
     throws InvalidRequestException, UnavailableException, TimedOutException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = ImmutableMap.of("key", ByteBufferUtil.bytesToHex(key),
@@ -297,7 +298,7 @@
             ClientState cState = state();
             String keyspace = cState.getKeyspace();
             state().hasColumnFamilyAccess(keyspace, column_parent.column_family, Permission.SELECT);
-            List<ColumnOrSuperColumn> result = getSliceInternal(keyspace, key, column_parent, FBUtilities.nowInSeconds(), predicate, consistency_level, cState);
+            List<ColumnOrSuperColumn> result = getSliceInternal(keyspace, key, column_parent, FBUtilities.nowInSeconds(), predicate, consistency_level, cState, queryStartNanoTime);
             return result == null ? Collections.<ColumnOrSuperColumn>emptyList() : result;
         }
         catch (RequestValidationException e)
@@ -316,15 +317,17 @@
                                                        int nowInSec,
                                                        SlicePredicate predicate,
                                                        ConsistencyLevel consistency_level,
-                                                       ClientState cState)
+                                                       ClientState cState,
+                                                       long queryStartNanoTime)
     throws org.apache.cassandra.exceptions.InvalidRequestException, UnavailableException, TimedOutException
     {
-        return multigetSliceInternal(keyspace, Collections.singletonList(key), column_parent, nowInSec, predicate, consistency_level, cState).get(key);
+        return multigetSliceInternal(keyspace, Collections.singletonList(key), column_parent, nowInSec, predicate, consistency_level, cState, queryStartNanoTime).get(key);
     }
 
     public Map<ByteBuffer, List<ColumnOrSuperColumn>> multiget_slice(List<ByteBuffer> keys, ColumnParent column_parent, SlicePredicate predicate, ConsistencyLevel consistency_level)
     throws InvalidRequestException, UnavailableException, TimedOutException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             List<String> keysList = Lists.newArrayList();
@@ -346,7 +349,7 @@
             ClientState cState = state();
             String keyspace = cState.getKeyspace();
             cState.hasColumnFamilyAccess(keyspace, column_parent.column_family, Permission.SELECT);
-            return multigetSliceInternal(keyspace, keys, column_parent, FBUtilities.nowInSeconds(), predicate, consistency_level, cState);
+            return multigetSliceInternal(keyspace, keys, column_parent, FBUtilities.nowInSeconds(), predicate, consistency_level, cState, queryStartNanoTime);
         }
         catch (RequestValidationException e)
         {
@@ -361,7 +364,7 @@
     private ClusteringIndexFilter toInternalFilter(CFMetaData metadata, ColumnParent parent, SliceRange range)
     {
         if (metadata.isSuper() && parent.isSetSuper_column())
-            return new ClusteringIndexNamesFilter(FBUtilities.singleton(new Clustering(parent.bufferForSuper_column()), metadata.comparator), range.reversed);
+            return new ClusteringIndexNamesFilter(FBUtilities.singleton(Clustering.make(parent.bufferForSuper_column()), metadata.comparator), range.reversed);
         else
             return new ClusteringIndexSliceFilter(makeSlices(metadata, range), range.reversed);
     }
@@ -385,13 +388,13 @@
                 {
                     if (parent.isSetSuper_column())
                     {
-                        return new ClusteringIndexNamesFilter(FBUtilities.singleton(new Clustering(parent.bufferForSuper_column()), metadata.comparator), false);
+                        return new ClusteringIndexNamesFilter(FBUtilities.singleton(Clustering.make(parent.bufferForSuper_column()), metadata.comparator), false);
                     }
                     else
                     {
                         NavigableSet<Clustering> clusterings = new TreeSet<>(metadata.comparator);
                         for (ByteBuffer bb : predicate.column_names)
-                            clusterings.add(new Clustering(bb));
+                            clusterings.add(Clustering.make(bb));
                         return new ClusteringIndexNamesFilter(clusterings, false);
                     }
                 }
@@ -465,7 +468,7 @@
             // We only want to include the static columns that are selected by the slices
             for (ColumnDefinition def : columns.statics)
             {
-                if (slices.selects(new Clustering(def.name.bytes)))
+                if (slices.selects(Clustering.make(def.name.bytes)))
                     builder.add(def);
             }
             columns = builder.build();
@@ -546,7 +549,8 @@
                                                                              int nowInSec,
                                                                              SlicePredicate predicate,
                                                                              ConsistencyLevel consistency_level,
-                                                                             ClientState cState)
+                                                                             ClientState cState,
+                                                                             long queryStartNanoTime)
     throws org.apache.cassandra.exceptions.InvalidRequestException, UnavailableException, TimedOutException
     {
         CFMetaData metadata = ThriftValidation.validateColumnFamily(keyspace, column_parent.column_family);
@@ -568,12 +572,13 @@
             commands.add(SinglePartitionReadCommand.create(true, metadata, nowInSec, columnFilter, RowFilter.NONE, limits, dk, filter));
         }
 
-        return getSlice(commands, column_parent.isSetSuper_column(), limits.perPartitionCount(), consistencyLevel, cState);
+        return getSlice(commands, column_parent.isSetSuper_column(), limits.perPartitionCount(), consistencyLevel, cState, queryStartNanoTime);
     }
 
     public ColumnOrSuperColumn get(ByteBuffer key, ColumnPath column_path, ConsistencyLevel consistency_level)
     throws InvalidRequestException, NotFoundException, UnavailableException, TimedOutException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = ImmutableMap.of("key", ByteBufferUtil.bytesToHex(key),
@@ -622,7 +627,7 @@
                     builder.select(dynamicDef, CellPath.create(column_path.column));
                     columns = builder.build();
                 }
-                filter = new ClusteringIndexNamesFilter(FBUtilities.singleton(new Clustering(column_path.super_column), metadata.comparator),
+                filter = new ClusteringIndexNamesFilter(FBUtilities.singleton(Clustering.make(column_path.super_column), metadata.comparator),
                                                   false);
             }
             else
@@ -636,7 +641,7 @@
                     builder.add(cellname.column);
                     builder.add(metadata.compactValueColumn());
                     columns = builder.build();
-                    filter = new ClusteringIndexNamesFilter(FBUtilities.singleton(new Clustering(column_path.column), metadata.comparator), false);
+                    filter = new ClusteringIndexNamesFilter(FBUtilities.singleton(Clustering.make(column_path.column), metadata.comparator), false);
                 }
                 else
                 {
@@ -648,7 +653,7 @@
             DecoratedKey dk = metadata.decorateKey(key);
             SinglePartitionReadCommand command = SinglePartitionReadCommand.create(true, metadata, FBUtilities.nowInSeconds(), columns, RowFilter.NONE, DataLimits.NONE, dk, filter);
 
-            try (RowIterator result = PartitionIterators.getOnlyElement(read(Arrays.asList(command), consistencyLevel, cState), command))
+            try (RowIterator result = PartitionIterators.getOnlyElement(read(Arrays.asList(command), consistencyLevel, cState, queryStartNanoTime), command))
             {
                 if (!result.hasNext())
                     throw new NotFoundException();
@@ -677,6 +682,7 @@
     public int get_count(ByteBuffer key, ColumnParent column_parent, SlicePredicate predicate, ConsistencyLevel consistency_level)
     throws InvalidRequestException, UnavailableException, TimedOutException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = ImmutableMap.of("key", ByteBufferUtil.bytesToHex(key),
@@ -700,7 +706,7 @@
             int nowInSec = FBUtilities.nowInSeconds();
 
             if (predicate.column_names != null)
-                return getSliceInternal(keyspace, key, column_parent, nowInSec, predicate, consistency_level, cState).size();
+                return getSliceInternal(keyspace, key, column_parent, nowInSec, predicate, consistency_level, cState, queryStartNanoTime).size();
 
             int pageSize;
             // request by page if this is a large row
@@ -747,7 +753,8 @@
                                           cState,
                                           pageSize,
                                           nowInSec,
-                                          true);
+                                          true,
+                                          queryStartNanoTime);
         }
         catch (IllegalArgumentException e)
         {
@@ -771,6 +778,7 @@
     public Map<ByteBuffer, Integer> multiget_count(List<ByteBuffer> keys, ColumnParent column_parent, SlicePredicate predicate, ConsistencyLevel consistency_level)
     throws InvalidRequestException, UnavailableException, TimedOutException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             List<String> keysList = Lists.newArrayList();
@@ -802,7 +810,8 @@
                                                                                                  FBUtilities.nowInSeconds(),
                                                                                                  predicate,
                                                                                                  consistency_level,
-                                                                                                 cState);
+                                                                                                 cState,
+                                                                                                 queryStartNanoTime);
 
             for (Map.Entry<ByteBuffer, List<ColumnOrSuperColumn>> cf : columnFamiliesMap.entrySet())
                 counts.put(cf.getKey(), cf.getValue().size());
@@ -821,12 +830,24 @@
     private Cell cellFromColumn(CFMetaData metadata, LegacyLayout.LegacyCellName name, Column column)
     {
         CellPath path = name.collectionElement == null ? null : CellPath.create(name.collectionElement);
-        return column.ttl == 0
-             ? BufferCell.live(metadata, name.column, column.timestamp, column.value, path)
-             : BufferCell.expiring(name.column, column.timestamp, column.ttl, FBUtilities.nowInSeconds(), column.value, path);
+        int ttl = getTtl(metadata, column);
+        return ttl == LivenessInfo.NO_TTL
+             ? BufferCell.live(name.column, column.timestamp, column.value, path)
+             : BufferCell.expiring(name.column, column.timestamp, ttl, FBUtilities.nowInSeconds(), column.value, path);
     }
 
-    private void internal_insert(ByteBuffer key, ColumnParent column_parent, Column column, ConsistencyLevel consistency_level)
+    private int getTtl(CFMetaData metadata,Column column)
+    {
+        if (!column.isSetTtl())
+            return metadata.params.defaultTimeToLive;
+
+        if (column.ttl == LivenessInfo.NO_TTL && metadata.params.defaultTimeToLive != LivenessInfo.NO_TTL)
+            return LivenessInfo.NO_TTL;
+
+        return column.ttl;
+    }
+
+    private void internal_insert(ByteBuffer key, ColumnParent column_parent, Column column, ConsistencyLevel consistency_level, long queryStartNanoTime)
     throws RequestValidationException, UnavailableException, TimedOutException
     {
         ThriftClientState cState = state();
@@ -863,12 +884,13 @@
         {
             throw new org.apache.cassandra.exceptions.InvalidRequestException(e.getMessage());
         }
-        doInsert(consistency_level, Collections.singletonList(mutation));
+        doInsert(consistency_level, Collections.singletonList(mutation), queryStartNanoTime);
     }
 
     public void insert(ByteBuffer key, ColumnParent column_parent, Column column, ConsistencyLevel consistency_level)
     throws InvalidRequestException, UnavailableException, TimedOutException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = ImmutableMap.of("key", ByteBufferUtil.bytesToHex(key),
@@ -884,7 +906,7 @@
 
         try
         {
-            internal_insert(key, column_parent, column, consistency_level);
+            internal_insert(key, column_parent, column, consistency_level, queryStartNanoTime);
         }
         catch (RequestValidationException e)
         {
@@ -904,6 +926,7 @@
                          ConsistencyLevel commit_consistency_level)
     throws InvalidRequestException, UnavailableException, TimedOutException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             ImmutableMap.Builder<String,String> builder = ImmutableMap.builder();
@@ -946,7 +969,7 @@
             DecoratedKey dk = metadata.decorateKey(key);
             int nowInSec = FBUtilities.nowInSeconds();
 
-            PartitionUpdate partitionUpdates = PartitionUpdate.fromIterator(LegacyLayout.toRowIterator(metadata, dk, toLegacyCells(metadata, updates, nowInSec).iterator(), nowInSec));
+            PartitionUpdate partitionUpdates = PartitionUpdate.fromIterator(LegacyLayout.toRowIterator(metadata, dk, toLegacyCells(metadata, updates, nowInSec).iterator(), nowInSec), ColumnFilter.all(metadata));
             // Indexed column values cannot be larger than 64K.  See CASSANDRA-3057/4240 for more details
             Keyspace.open(metadata.ksName).getColumnFamilyStore(metadata.cfName).indexManager.validate(partitionUpdates);
 
@@ -957,7 +980,8 @@
                                                        new ThriftCASRequest(toLegacyCells(metadata, expected, nowInSec), partitionUpdates, nowInSec),
                                                        ThriftConversion.fromThrift(serial_consistency_level),
                                                        ThriftConversion.fromThrift(commit_consistency_level),
-                                                       cState))
+                                                       cState,
+                                                       queryStartNanoTime))
             {
                 return result == null
                      ? new CASResult(true)
@@ -1148,7 +1172,7 @@
 
                 sortAndMerge(metadata, cells, nowInSec);
                 DecoratedKey dk = metadata.decorateKey(key);
-                PartitionUpdate update = PartitionUpdate.fromIterator(LegacyLayout.toUnfilteredRowIterator(metadata, dk, delInfo, cells.iterator()));
+                PartitionUpdate update = PartitionUpdate.fromIterator(LegacyLayout.toUnfilteredRowIterator(metadata, dk, delInfo, cells.iterator()), ColumnFilter.all(metadata));
 
                 // Indexed column values cannot be larger than 64K.  See CASSANDRA-3057/4240 for more details
                 Keyspace.open(metadata.ksName).getColumnFamilyStore(metadata.cfName).indexManager.validate(update);
@@ -1211,7 +1235,7 @@
         }
     }
 
-    private void addRange(CFMetaData cfm, LegacyLayout.LegacyDeletionInfo delInfo, Slice.Bound start, Slice.Bound end, long timestamp, int nowInSec)
+    private void addRange(CFMetaData cfm, LegacyLayout.LegacyDeletionInfo delInfo, ClusteringBound start, ClusteringBound end, long timestamp, int nowInSec)
     {
         delInfo.add(cfm, new RangeTombstone(Slice.make(start, end), new DeletionTime(timestamp, nowInSec)));
     }
@@ -1226,7 +1250,7 @@
                 try
                 {
                     if (del.super_column == null && cfm.isSuper())
-                        addRange(cfm, delInfo, Slice.Bound.inclusiveStartOf(c), Slice.Bound.inclusiveEndOf(c), del.timestamp, nowInSec);
+                        addRange(cfm, delInfo, ClusteringBound.inclusiveStartOf(c), ClusteringBound.inclusiveEndOf(c), del.timestamp, nowInSec);
                     else if (del.super_column != null)
                         cells.add(toLegacyDeletion(cfm, del.super_column, c, del.timestamp, nowInSec));
                     else
@@ -1260,7 +1284,7 @@
         else
         {
             if (del.super_column != null)
-                addRange(cfm, delInfo, Slice.Bound.inclusiveStartOf(del.super_column), Slice.Bound.inclusiveEndOf(del.super_column), del.timestamp, nowInSec);
+                addRange(cfm, delInfo, ClusteringBound.inclusiveStartOf(del.super_column), ClusteringBound.inclusiveEndOf(del.super_column), del.timestamp, nowInSec);
             else
                 delInfo.add(new DeletionTime(del.timestamp, nowInSec));
         }
@@ -1269,6 +1293,7 @@
     public void batch_mutate(Map<ByteBuffer,Map<String,List<Mutation>>> mutation_map, ConsistencyLevel consistency_level)
     throws InvalidRequestException, UnavailableException, TimedOutException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = Maps.newLinkedHashMap();
@@ -1287,7 +1312,7 @@
 
         try
         {
-            doInsert(consistency_level, createMutationList(consistency_level, mutation_map, true));
+            doInsert(consistency_level, createMutationList(consistency_level, mutation_map, true), queryStartNanoTime);
         }
         catch (RequestValidationException e)
         {
@@ -1302,6 +1327,7 @@
     public void atomic_batch_mutate(Map<ByteBuffer,Map<String,List<Mutation>>> mutation_map, ConsistencyLevel consistency_level)
     throws InvalidRequestException, UnavailableException, TimedOutException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = Maps.newLinkedHashMap();
@@ -1320,7 +1346,7 @@
 
         try
         {
-            doInsert(consistency_level, createMutationList(consistency_level, mutation_map, false), true);
+            doInsert(consistency_level, createMutationList(consistency_level, mutation_map, false), true, queryStartNanoTime);
         }
         catch (RequestValidationException e)
         {
@@ -1332,7 +1358,7 @@
         }
     }
 
-    private void internal_remove(ByteBuffer key, ColumnPath column_path, long timestamp, ConsistencyLevel consistency_level, boolean isCommutativeOp)
+    private void internal_remove(ByteBuffer key, ColumnPath column_path, long timestamp, ConsistencyLevel consistency_level, boolean isCommutativeOp, long queryStartNanoTime)
     throws RequestValidationException, UnavailableException, TimedOutException
     {
         ThriftClientState cState = state();
@@ -1358,7 +1384,7 @@
         }
         else if (column_path.super_column != null && column_path.column == null)
         {
-            Row row = BTreeRow.emptyDeletedRow(new Clustering(column_path.super_column), Row.Deletion.regular(new DeletionTime(timestamp, nowInSec)));
+            Row row = BTreeRow.emptyDeletedRow(Clustering.make(column_path.super_column), Row.Deletion.regular(new DeletionTime(timestamp, nowInSec)));
             update = PartitionUpdate.singleRowUpdate(metadata, dk, row);
         }
         else
@@ -1379,14 +1405,15 @@
         org.apache.cassandra.db.Mutation mutation = new org.apache.cassandra.db.Mutation(update);
 
         if (isCommutativeOp)
-            doInsert(consistency_level, Collections.singletonList(new CounterMutation(mutation, ThriftConversion.fromThrift(consistency_level))));
+            doInsert(consistency_level, Collections.singletonList(new CounterMutation(mutation, ThriftConversion.fromThrift(consistency_level))), queryStartNanoTime);
         else
-            doInsert(consistency_level, Collections.singletonList(mutation));
+            doInsert(consistency_level, Collections.singletonList(mutation), queryStartNanoTime);
     }
 
     public void remove(ByteBuffer key, ColumnPath column_path, long timestamp, ConsistencyLevel consistency_level)
     throws InvalidRequestException, UnavailableException, TimedOutException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = ImmutableMap.of("key", ByteBufferUtil.bytesToHex(key),
@@ -1402,7 +1429,7 @@
 
         try
         {
-            internal_remove(key, column_path, timestamp, consistency_level, false);
+            internal_remove(key, column_path, timestamp, consistency_level, false, queryStartNanoTime);
         }
         catch (RequestValidationException e)
         {
@@ -1414,13 +1441,13 @@
         }
     }
 
-    private void doInsert(ConsistencyLevel consistency_level, List<? extends IMutation> mutations)
+    private void doInsert(ConsistencyLevel consistency_level, List<? extends IMutation> mutations, long queryStartNanoTime)
     throws UnavailableException, TimedOutException, org.apache.cassandra.exceptions.InvalidRequestException
     {
-        doInsert(consistency_level, mutations, false);
+        doInsert(consistency_level, mutations, false, queryStartNanoTime);
     }
 
-    private void doInsert(ConsistencyLevel consistency_level, List<? extends IMutation> mutations, boolean mutateAtomically)
+    private void doInsert(ConsistencyLevel consistency_level, List<? extends IMutation> mutations, boolean mutateAtomically, long queryStartNanoTime)
     throws UnavailableException, TimedOutException, org.apache.cassandra.exceptions.InvalidRequestException
     {
         org.apache.cassandra.db.ConsistencyLevel consistencyLevel = ThriftConversion.fromThrift(consistency_level);
@@ -1435,7 +1462,7 @@
         schedule(timeout);
         try
         {
-            StorageProxy.mutateWithTriggers(mutations, consistencyLevel, mutateAtomically);
+            StorageProxy.mutateWithTriggers(mutations, consistencyLevel, mutateAtomically, queryStartNanoTime);
         }
         catch (RequestExecutionException e)
         {
@@ -1473,6 +1500,7 @@
     public List<KeySlice> get_range_slices(ColumnParent column_parent, SlicePredicate predicate, KeyRange range, ConsistencyLevel consistency_level)
     throws InvalidRequestException, UnavailableException, TException, TimedOutException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = ImmutableMap.of(
@@ -1534,7 +1562,7 @@
                                                      limits,
                                                      new DataRange(bounds, filter));
 
-                try (PartitionIterator results = StorageProxy.getRangeSlice(cmd, consistencyLevel))
+                try (PartitionIterator results = StorageProxy.getRangeSlice(cmd, consistencyLevel, queryStartNanoTime))
                 {
                     assert results != null;
                     return thriftifyKeySlices(results, column_parent, limits.perPartitionCount());
@@ -1562,6 +1590,7 @@
     public List<KeySlice> get_paged_slice(String column_family, KeyRange range, ByteBuffer start_column, ConsistencyLevel consistency_level)
     throws InvalidRequestException, UnavailableException, TimedOutException, TException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = ImmutableMap.of("column_family", column_family,
@@ -1616,7 +1645,7 @@
                 ClusteringIndexFilter filter = new ClusteringIndexSliceFilter(Slices.ALL, false);
                 DataLimits limits = getLimits(range.count, true, Integer.MAX_VALUE);
                 Clustering pageFrom = metadata.isSuper()
-                                    ? new Clustering(start_column)
+                                    ? Clustering.make(start_column)
                                     : LegacyLayout.decodeCellName(metadata, start_column).clustering;
 
                 PartitionRangeReadCommand cmd =
@@ -1628,7 +1657,7 @@
                                                      limits,
                                                      new DataRange(bounds, filter).forPaging(bounds, metadata.comparator, pageFrom, true));
 
-                try (PartitionIterator results = StorageProxy.getRangeSlice(cmd, consistencyLevel))
+                try (PartitionIterator results = StorageProxy.getRangeSlice(cmd, consistencyLevel, queryStartNanoTime))
                 {
                     return thriftifyKeySlices(results, new ColumnParent(column_family), limits.perPartitionCount());
                 }
@@ -1677,6 +1706,7 @@
     public List<KeySlice> get_indexed_slices(ColumnParent column_parent, IndexClause index_clause, SlicePredicate column_predicate, ConsistencyLevel consistency_level)
     throws InvalidRequestException, UnavailableException, TimedOutException, TException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = ImmutableMap.of("column_parent", column_parent.toString(),
@@ -1723,7 +1753,7 @@
             // If there's a secondary index that the command can use, have it validate the request parameters.
             cmd.maybeValidateIndex();
 
-            try (PartitionIterator results = StorageProxy.getRangeSlice(cmd, consistencyLevel))
+            try (PartitionIterator results = StorageProxy.getRangeSlice(cmd, consistencyLevel, queryStartNanoTime))
             {
                 return thriftifyKeySlices(results, column_parent, limits.perPartitionCount());
             }
@@ -2099,10 +2129,6 @@
         {
             throw new TimedOutException();
         }
-        catch (IOException e)
-        {
-            throw (UnavailableException) new UnavailableException().initCause(e);
-        }
         finally
         {
             Tracing.instance.stopSession();
@@ -2132,6 +2158,7 @@
     public void add(ByteBuffer key, ColumnParent column_parent, CounterColumn column, ConsistencyLevel consistency_level)
             throws InvalidRequestException, UnavailableException, TimedOutException, TException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = ImmutableMap.of("column_parent", column_parent.toString(),
@@ -2171,12 +2198,12 @@
                 // See UpdateParameters.addCounter() for more details on this
                 ByteBuffer value = CounterContext.instance().createUpdate(column.value);
                 CellPath path = name.collectionElement == null ? null : CellPath.create(name.collectionElement);
-                Cell cell = BufferCell.live(metadata, name.column, FBUtilities.timestampMicros(), value, path);
+                Cell cell = BufferCell.live(name.column, FBUtilities.timestampMicros(), value, path);
 
                 PartitionUpdate update = PartitionUpdate.singleRowUpdate(metadata, key, BTreeRow.singleCellRow(name.clustering, cell));
 
                 org.apache.cassandra.db.Mutation mutation = new org.apache.cassandra.db.Mutation(update);
-                doInsert(consistency_level, Arrays.asList(new CounterMutation(mutation, ThriftConversion.fromThrift(consistency_level))));
+                doInsert(consistency_level, Arrays.asList(new CounterMutation(mutation, ThriftConversion.fromThrift(consistency_level))), queryStartNanoTime);
             }
             catch (MarshalException|UnknownColumnException e)
             {
@@ -2196,6 +2223,7 @@
     public void remove_counter(ByteBuffer key, ColumnPath path, ConsistencyLevel consistency_level)
     throws InvalidRequestException, UnavailableException, TimedOutException, TException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = ImmutableMap.of("key", ByteBufferUtil.bytesToHex(key),
@@ -2210,7 +2238,7 @@
 
         try
         {
-            internal_remove(key, path, FBUtilities.timestampMicros(), consistency_level, true);
+            internal_remove(key, path, FBUtilities.timestampMicros(), consistency_level, true, queryStartNanoTime);
         }
         catch (RequestValidationException e)
         {
@@ -2289,6 +2317,7 @@
     {
         try
         {
+            long queryStartNanoTime = System.nanoTime();
             String queryString = uncompress(query, compression);
             if (startSessionIfRequested())
             {
@@ -2306,7 +2335,8 @@
                                                             cState.getQueryState(),
                                                             QueryOptions.fromThrift(ThriftConversion.fromThrift(cLevel),
                                                             Collections.<ByteBuffer>emptyList()),
-                                                            null).toThriftResult();
+                                                            null,
+                                                            queryStartNanoTime).toThriftResult();
         }
         catch (RequestExecutionException e)
         {
@@ -2352,6 +2382,7 @@
 
     public CqlResult execute_prepared_cql3_query(int itemId, List<ByteBuffer> bindVariables, ConsistencyLevel cLevel) throws TException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             // TODO we don't have [typed] access to CQL bind variables here.  CASSANDRA-4560 is open to add support.
@@ -2377,7 +2408,8 @@
             return ClientState.getCQLQueryHandler().processPrepared(prepared.statement,
                                                                     cState.getQueryState(),
                                                                     QueryOptions.fromThrift(ThriftConversion.fromThrift(cLevel), bindVariables),
-                                                                    null).toThriftResult();
+                                                                    null,
+                                                                    queryStartNanoTime).toThriftResult();
         }
         catch (RequestExecutionException e)
         {
@@ -2397,6 +2429,7 @@
     public List<ColumnOrSuperColumn> get_multi_slice(MultiSliceRequest request)
             throws InvalidRequestException, UnavailableException, TimedOutException
     {
+        long queryStartNanoTime = System.nanoTime();
         if (startSessionIfRequested())
         {
             Map<String, String> traceParameters = ImmutableMap.of("key", ByteBufferUtil.bytesToHex(request.key),
@@ -2410,7 +2443,7 @@
         {
             logger.trace("get_multi_slice");
         }
-        try 
+        try
         {
             ClientState cState = state();
             String keyspace = cState.getKeyspace();
@@ -2426,8 +2459,8 @@
             for (int i = 0 ; i < request.getColumn_slices().size() ; i++)
             {
                 fixOptionalSliceParameters(request.getColumn_slices().get(i));
-                Slice.Bound start = LegacyLayout.decodeSliceBound(metadata, request.getColumn_slices().get(i).start, true).bound;
-                Slice.Bound finish = LegacyLayout.decodeSliceBound(metadata, request.getColumn_slices().get(i).finish, false).bound;
+                ClusteringBound start = LegacyLayout.decodeSliceBound(metadata, request.getColumn_slices().get(i).start, true).bound;
+                ClusteringBound finish = LegacyLayout.decodeSliceBound(metadata, request.getColumn_slices().get(i).finish, false).bound;
 
                 int compare = metadata.comparator.compare(start, finish);
                 if (!request.reversed && compare > 0)
@@ -2450,13 +2483,14 @@
                             false,
                             limits.perPartitionCount(),
                             consistencyLevel,
-                            cState).entrySet().iterator().next().getValue();
+                            cState,
+                            queryStartNanoTime).entrySet().iterator().next().getValue();
         }
         catch (RequestValidationException e)
         {
             throw ThriftConversion.toThrift(e);
-        } 
-        finally 
+        }
+        finally
         {
             Tracing.instance.stopSession();
         }
@@ -2466,7 +2500,8 @@
      * Set the to start-of end-of value of "" for start and finish.
      * @param columnSlice
      */
-    private static void fixOptionalSliceParameters(org.apache.cassandra.thrift.ColumnSlice columnSlice) {
+    private static void fixOptionalSliceParameters(org.apache.cassandra.thrift.ColumnSlice columnSlice)
+    {
         if (!columnSlice.isSetStart())
             columnSlice.setStart(new byte[0]);
         if (!columnSlice.isSetFinish())
@@ -2491,7 +2526,7 @@
     {
         if (state().getQueryState().traceNextQuery())
         {
-            state().getQueryState().createTracingSession();
+            state().getQueryState().createTracingSession(Collections.EMPTY_MAP);
             return true;
         }
         return false;
diff --git a/src/java/org/apache/cassandra/thrift/CustomTNonBlockingServer.java b/src/java/org/apache/cassandra/thrift/CustomTNonBlockingServer.java
index de8df57..8221a83 100644
--- a/src/java/org/apache/cassandra/thrift/CustomTNonBlockingServer.java
+++ b/src/java/org/apache/cassandra/thrift/CustomTNonBlockingServer.java
@@ -80,11 +80,13 @@
     {
         public CustomFrameBuffer(final TNonblockingTransport trans,
           final SelectionKey selectionKey,
-          final AbstractSelectThread selectThread) {
+          final AbstractSelectThread selectThread)
+        {
 			super(trans, selectionKey, selectThread);
         }
 
-        public TNonblockingTransport getTransport() {
+        public TNonblockingTransport getTransport()
+        {
             return this.trans_;
         }
     }
diff --git a/src/java/org/apache/cassandra/thrift/CustomTThreadPoolServer.java b/src/java/org/apache/cassandra/thrift/CustomTThreadPoolServer.java
index efa9330..31aa689 100644
--- a/src/java/org/apache/cassandra/thrift/CustomTThreadPoolServer.java
+++ b/src/java/org/apache/cassandra/thrift/CustomTThreadPoolServer.java
@@ -56,8 +56,8 @@
  * Slightly modified version of the Apache Thrift TThreadPoolServer.
  * <p>
  * This allows passing an executor so you have more control over the actual
- * behaviour of the tasks being run.
- * <p/>
+ * behavior of the tasks being run.
+ * </p>
  * Newer version of Thrift should make this obsolete.
  */
 public class CustomTThreadPoolServer extends TServer
@@ -78,7 +78,8 @@
     private final AtomicInteger activeClients;
 
 
-    public CustomTThreadPoolServer(TThreadPoolServer.Args args, ExecutorService executorService) {
+    public CustomTThreadPoolServer(TThreadPoolServer.Args args, ExecutorService executorService)
+    {
         super(args);
         this.executorService = executorService;
         this.stopped = false;
@@ -257,8 +258,7 @@
                     SSLServerSocket sslServerSocket = (SSLServerSocket) sslServer.getServerSocket();
                     String[] suites = SSLFactory.filterCipherSuites(sslServerSocket.getSupportedCipherSuites(), clientEnc.cipher_suites);
                     sslServerSocket.setEnabledCipherSuites(suites);
-                    sslServerSocket.setEnabledProtocols(SSLFactory.ACCEPTED_PROTOCOLS);
-                    serverTransport = new TCustomServerSocket(sslServer.getServerSocket(), args.keepAlive, args.sendBufferSize, args.recvBufferSize);
+                    serverTransport = new TCustomServerSocket(sslServerSocket, args.keepAlive, args.sendBufferSize, args.recvBufferSize);
                 }
                 else
                 {
diff --git a/src/java/org/apache/cassandra/thrift/TCustomSocket.java b/src/java/org/apache/cassandra/thrift/TCustomSocket.java
index 1a5d538..08a9770 100644
--- a/src/java/org/apache/cassandra/thrift/TCustomSocket.java
+++ b/src/java/org/apache/cassandra/thrift/TCustomSocket.java
@@ -36,7 +36,8 @@
  * Adds socket buffering
  *
  */
-public class TCustomSocket extends TIOStreamTransport {
+public class TCustomSocket extends TIOStreamTransport
+{
 
   private static final Logger LOGGER = LoggerFactory.getLogger(TCustomSocket.class.getName());
 
@@ -66,20 +67,28 @@
    * @param socket Already created socket object
    * @throws TTransportException if there is an error setting up the streams
    */
-  public TCustomSocket(Socket socket) throws TTransportException {
+  public TCustomSocket(Socket socket) throws TTransportException
+  {
     this.socket = socket;
-    try {
+    try
+    {
       socket.setSoLinger(false, 0);
       socket.setTcpNoDelay(true);
-    } catch (SocketException sx) {
+    }
+    catch (SocketException sx)
+    {
       LOGGER.warn("Could not configure socket.", sx);
     }
 
-    if (isOpen()) {
-      try {
+    if (isOpen())
+    {
+      try
+      {
         inputStream_ = new BufferedInputStream(socket.getInputStream(), 1024);
         outputStream_ = new BufferedOutputStream(socket.getOutputStream(), 1024);
-      } catch (IOException iox) {
+      }
+      catch (IOException iox)
+      {
         close();
         throw new TTransportException(TTransportException.NOT_OPEN, iox);
       }
@@ -93,7 +102,8 @@
    * @param host Remote host
    * @param port Remote port
    */
-  public TCustomSocket(String host, int port) {
+  public TCustomSocket(String host, int port)
+  {
     this(host, port, 0);
   }
 
@@ -105,7 +115,8 @@
    * @param port    Remote port
    * @param timeout Socket timeout
    */
-  public TCustomSocket(String host, int port, int timeout) {
+  public TCustomSocket(String host, int port, int timeout)
+  {
     this.host = host;
     this.port = port;
     this.timeout = timeout;
@@ -115,13 +126,17 @@
   /**
    * Initializes the socket object
    */
-  private void initSocket() {
+  private void initSocket()
+  {
     socket = new Socket();
-    try {
+    try
+    {
       socket.setSoLinger(false, 0);
       socket.setTcpNoDelay(true);
       socket.setSoTimeout(timeout);
-    } catch (SocketException sx) {
+    }
+    catch (SocketException sx)
+    {
       LOGGER.error("Could not configure socket.", sx);
     }
   }
@@ -131,11 +146,15 @@
    *
    * @param timeout Milliseconds timeout
    */
-  public void setTimeout(int timeout) {
+  public void setTimeout(int timeout)
+  {
     this.timeout = timeout;
-    try {
+    try
+    {
       socket.setSoTimeout(timeout);
-    } catch (SocketException sx) {
+    }
+    catch (SocketException sx)
+    {
       LOGGER.warn("Could not set socket timeout.", sx);
     }
   }
@@ -143,8 +162,10 @@
   /**
    * Returns a reference to the underlying socket.
    */
-  public Socket getSocket() {
-    if (socket == null) {
+  public Socket getSocket()
+  {
+    if (socket == null)
+    {
       initSocket();
     }
     return socket;
@@ -153,8 +174,10 @@
   /**
    * Checks whether the socket is connected.
    */
-  public boolean isOpen() {
-    if (socket == null) {
+  public boolean isOpen()
+  {
+    if (socket == null)
+    {
       return false;
     }
     return socket.isConnected();
@@ -163,27 +186,35 @@
   /**
    * Connects the socket, creating a new socket object if necessary.
    */
-  public void open() throws TTransportException {
-    if (isOpen()) {
+  public void open() throws TTransportException
+  {
+    if (isOpen())
+    {
       throw new TTransportException(TTransportException.ALREADY_OPEN, "Socket already connected.");
     }
 
-    if (host.length() == 0) {
+    if (host.length() == 0)
+    {
       throw new TTransportException(TTransportException.NOT_OPEN, "Cannot open null host.");
     }
-    if (port <= 0) {
+    if (port <= 0)
+    {
       throw new TTransportException(TTransportException.NOT_OPEN, "Cannot open without port.");
     }
 
-    if (socket == null) {
+    if (socket == null)
+    {
       initSocket();
     }
 
-    try {
+    try
+    {
       socket.connect(new InetSocketAddress(host, port), timeout);
       inputStream_ = new BufferedInputStream(socket.getInputStream(), 1024);
       outputStream_ = new BufferedOutputStream(socket.getOutputStream(), 1024);
-    } catch (IOException iox) {
+    }
+    catch (IOException iox)
+    {
       close();
       throw new TTransportException(TTransportException.NOT_OPEN, iox);
     }
@@ -192,15 +223,20 @@
   /**
    * Closes the socket.
    */
-  public void close() {
+  public void close()
+  {
     // Close the underlying streams
     super.close();
 
     // Close the socket
-    if (socket != null) {
-      try {
+    if (socket != null)
+    {
+      try
+      {
         socket.close();
-      } catch (IOException iox) {
+      }
+      catch (IOException iox)
+      {
         LOGGER.warn("Could not close socket.", iox);
       }
       socket = null;
diff --git a/src/java/org/apache/cassandra/thrift/TServerCustomFactory.java b/src/java/org/apache/cassandra/thrift/TServerCustomFactory.java
index 3c5b967..5a272dd 100644
--- a/src/java/org/apache/cassandra/thrift/TServerCustomFactory.java
+++ b/src/java/org/apache/cassandra/thrift/TServerCustomFactory.java
@@ -41,19 +41,19 @@
     public TServer buildTServer(TServerFactory.Args args)
     {
         TServer server;
-        if (ThriftServer.SYNC.equalsIgnoreCase(serverType))
+        if (ThriftServer.ThriftServerType.SYNC.equalsIgnoreCase(serverType))
         {
             server = new CustomTThreadPoolServer.Factory().buildTServer(args);
         }
-        else if(ThriftServer.ASYNC.equalsIgnoreCase(serverType))
+        else if(ThriftServer.ThriftServerType.ASYNC.equalsIgnoreCase(serverType))
         {
             server = new CustomTNonBlockingServer.Factory().buildTServer(args);
-            logger.info(String.format("Using non-blocking/asynchronous thrift server on %s : %s", args.addr.getHostName(), args.addr.getPort()));
+            logger.info("Using non-blocking/asynchronous thrift server on {} : {}", args.addr.getHostName(), args.addr.getPort());
         }
-        else if(ThriftServer.HSHA.equalsIgnoreCase(serverType))
+        else if(ThriftServer.ThriftServerType.HSHA.equalsIgnoreCase(serverType))
         {
             server = new THsHaDisruptorServer.Factory().buildTServer(args);
-            logger.info(String.format("Using custom half-sync/half-async thrift server on %s : %s", args.addr.getHostName(), args.addr.getPort()));
+            logger.info("Using custom half-sync/half-async thrift server on {} : {}", args.addr.getHostName(), args.addr.getPort());
         }
         else
         {
@@ -67,7 +67,7 @@
                 throw new RuntimeException("Failed to instantiate server factory:" + serverType, e);
             }
             server = serverFactory.buildTServer(args);
-            logger.info(String.format("Using custom thrift server %s on %s : %s", server.getClass().getName(), args.addr.getHostName(), args.addr.getPort()));
+            logger.info("Using custom thrift server {} on {} : {}", server.getClass().getName(), args.addr.getHostName(), args.addr.getPort());
         }
         return server;
     }
diff --git a/src/java/org/apache/cassandra/thrift/ThriftConversion.java b/src/java/org/apache/cassandra/thrift/ThriftConversion.java
index e8256a8..0f40d22 100644
--- a/src/java/org/apache/cassandra/thrift/ThriftConversion.java
+++ b/src/java/org/apache/cassandra/thrift/ThriftConversion.java
@@ -35,7 +35,7 @@
 import org.apache.cassandra.db.filter.RowFilter;
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.exceptions.*;
-import org.apache.cassandra.index.internal.CassandraIndex;
+import org.apache.cassandra.index.TargetParser;
 import org.apache.cassandra.io.compress.ICompressor;
 import org.apache.cassandra.locator.AbstractReplicationStrategy;
 import org.apache.cassandra.locator.LocalStrategy;
@@ -598,7 +598,7 @@
         IndexMetadata matchedIndex = null;
         for (IndexMetadata index : cfMetaData.getIndexes())
         {
-            Pair<ColumnDefinition, IndexTarget.Type> target  = CassandraIndex.parseTarget(cfMetaData, index);
+            Pair<ColumnDefinition, IndexTarget.Type> target  = TargetParser.parse(cfMetaData, index);
             if (target.left.equals(column))
             {
                 // we already found an index for this column, we've no option but to
diff --git a/src/java/org/apache/cassandra/thrift/ThriftResultsMerger.java b/src/java/org/apache/cassandra/thrift/ThriftResultsMerger.java
index ea3fa2f..a14409e 100644
--- a/src/java/org/apache/cassandra/thrift/ThriftResultsMerger.java
+++ b/src/java/org/apache/cassandra/thrift/ThriftResultsMerger.java
@@ -199,7 +199,7 @@
             Cell cell = staticCells.next();
 
             // Given a static cell, the equivalent row uses the column name as clustering and the value as unique cell value.
-            builder.newRow(new Clustering(cell.column().name.bytes));
+            builder.newRow(Clustering.make(cell.column().name.bytes));
             builder.addCell(new BufferCell(metadata().compactValueColumn(), cell.timestamp(), cell.ttl(), cell.localDeletionTime(), cell.value(), cell.path()));
             nextToMerge = builder.build();
         }
diff --git a/src/java/org/apache/cassandra/thrift/ThriftServer.java b/src/java/org/apache/cassandra/thrift/ThriftServer.java
index 44ec524..4aa5736 100644
--- a/src/java/org/apache/cassandra/thrift/ThriftServer.java
+++ b/src/java/org/apache/cassandra/thrift/ThriftServer.java
@@ -33,10 +33,7 @@
 
 public class ThriftServer implements CassandraDaemon.Server
 {
-    private static Logger logger = LoggerFactory.getLogger(ThriftServer.class);
-    public final static String SYNC = "sync";
-    public final static String ASYNC = "async";
-    public final static String HSHA = "hsha";
+    private static final Logger logger = LoggerFactory.getLogger(ThriftServer.class);
 
     protected final InetAddress address;
     protected final int port;
@@ -116,7 +113,7 @@
                                   TTransportFactory transportFactory)
         {
             // now we start listening for clients
-            logger.info(String.format("Binding thrift service to %s:%s", listenAddr, listenPort));
+            logger.info("Binding thrift service to {}:{}", listenAddr, listenPort);
 
             TServerFactory.Args args = new TServerFactory.Args();
             args.tProtocolFactory = new TBinaryProtocol.Factory(true, true);
@@ -143,4 +140,11 @@
             serverEngine.stop();
         }
     }
+
+    public static final class ThriftServerType
+    {
+        public final static String SYNC = "sync";
+        public final static String ASYNC = "async";
+        public final static String HSHA = "hsha";
+    }
 }
diff --git a/src/java/org/apache/cassandra/thrift/ThriftSessionManager.java b/src/java/org/apache/cassandra/thrift/ThriftSessionManager.java
index 3603ad5..60da3b4 100644
--- a/src/java/org/apache/cassandra/thrift/ThriftSessionManager.java
+++ b/src/java/org/apache/cassandra/thrift/ThriftSessionManager.java
@@ -19,12 +19,13 @@
 
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
-import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import io.netty.util.concurrent.FastThreadLocal;
+
 /**
  * Encapsulates the current client state (session).
  *
@@ -36,7 +37,7 @@
     private static final Logger logger = LoggerFactory.getLogger(ThriftSessionManager.class);
     public final static ThriftSessionManager instance = new ThriftSessionManager();
 
-    private final ThreadLocal<SocketAddress> remoteSocket = new ThreadLocal<>();
+    private final FastThreadLocal<SocketAddress> remoteSocket = new FastThreadLocal<>();
     private final ConcurrentHashMap<SocketAddress, ThriftClientState> activeSocketSessions = new ConcurrentHashMap<>();
 
     /**
diff --git a/src/java/org/apache/cassandra/thrift/ThriftValidation.java b/src/java/org/apache/cassandra/thrift/ThriftValidation.java
index 4b208ba..1b61fd5 100644
--- a/src/java/org/apache/cassandra/thrift/ThriftValidation.java
+++ b/src/java/org/apache/cassandra/thrift/ThriftValidation.java
@@ -28,6 +28,7 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.Attributes;
 import org.apache.cassandra.cql3.Operator;
 import org.apache.cassandra.db.*;
@@ -373,8 +374,8 @@
     {
         if (column.isSetTtl())
         {
-            if (column.ttl <= 0)
-                throw new org.apache.cassandra.exceptions.InvalidRequestException("ttl must be positive");
+            if (column.ttl < 0)
+                throw new org.apache.cassandra.exceptions.InvalidRequestException("ttl must be greater or equal to 0");
 
             if (column.ttl > Attributes.MAX_TTL)
                 throw new org.apache.cassandra.exceptions.InvalidRequestException(String.format("ttl is too large. requested (%d) maximum (%d)", column.ttl, Attributes.MAX_TTL));
@@ -468,8 +469,7 @@
             if (cn.column.isPrimaryKeyColumn())
                 throw new org.apache.cassandra.exceptions.InvalidRequestException(String.format("Cannot add primary key column %s to partition update", cn.column.name));
 
-            cn.column.validateCellValue(column.value);
-
+            cn.column.type.validateCellValue(column.value);
         }
         catch (UnknownColumnException e)
         {
@@ -651,7 +651,7 @@
 
     public static void validateKeyspaceNotSystem(String modifiedKeyspace) throws org.apache.cassandra.exceptions.InvalidRequestException
     {
-        if (Schema.isLocalSystemKeyspace(modifiedKeyspace))
+        if (SchemaConstants.isLocalSystemKeyspace(modifiedKeyspace))
             throw new org.apache.cassandra.exceptions.InvalidRequestException(String.format("%s keyspace is not user-modifiable", modifiedKeyspace));
     }
 
diff --git a/src/java/org/apache/cassandra/tools/AbstractJmxClient.java b/src/java/org/apache/cassandra/tools/AbstractJmxClient.java
index 8241a5b..9251091 100644
--- a/src/java/org/apache/cassandra/tools/AbstractJmxClient.java
+++ b/src/java/org/apache/cassandra/tools/AbstractJmxClient.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.tools;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
@@ -110,7 +110,7 @@
         CommandLineParser parser = new PosixParser();
         return parser.parse(options, args);
     }
-    
+
     public static void addCmdOption(String shortOpt, String longOpt, boolean hasArg, String description)
     {
         options.addOption(shortOpt, longOpt, hasArg, description);
diff --git a/src/java/org/apache/cassandra/tools/BulkLoadException.java b/src/java/org/apache/cassandra/tools/BulkLoadException.java
new file mode 100644
index 0000000..3c5c03d
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/BulkLoadException.java
@@ -0,0 +1,33 @@
+/*
+ *
+ * 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.tools;
+
+public class BulkLoadException extends Exception
+{
+
+    private static final long serialVersionUID = 1L;
+
+    public BulkLoadException(Throwable cause)
+    {
+        super(cause);
+    }
+
+}
diff --git a/src/java/org/apache/cassandra/tools/BulkLoader.java b/src/java/org/apache/cassandra/tools/BulkLoader.java
index c1849f8..0f1c555 100644
--- a/src/java/org/apache/cassandra/tools/BulkLoader.java
+++ b/src/java/org/apache/cassandra/tools/BulkLoader.java
@@ -17,66 +17,40 @@
  */
 package org.apache.cassandra.tools;
 
-import java.io.File;
 import java.io.IOException;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
 import java.net.InetAddress;
-import java.net.MalformedURLException;
-import java.net.UnknownHostException;
-import java.util.*;
-
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.Multimap;
-import org.apache.commons.cli.*;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
 
 import com.datastax.driver.core.AuthProvider;
 import com.datastax.driver.core.JdkSSLOptions;
-import com.datastax.driver.core.PlainTextAuthProvider;
 import com.datastax.driver.core.SSLOptions;
-import javax.net.ssl.SSLContext;
-import org.apache.cassandra.config.*;
-import org.apache.cassandra.exceptions.ConfigurationException;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.EncryptionOptions;
 import org.apache.cassandra.io.sstable.SSTableLoader;
 import org.apache.cassandra.security.SSLFactory;
 import org.apache.cassandra.streaming.*;
+import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.NativeSSTableLoaderClient;
 import org.apache.cassandra.utils.OutputHandler;
 
 public class BulkLoader
 {
-    private static final String TOOL_NAME = "sstableloader";
-    private static final String VERBOSE_OPTION  = "verbose";
-    private static final String HELP_OPTION  = "help";
-    private static final String NOPROGRESS_OPTION  = "no-progress";
-    private static final String IGNORE_NODES_OPTION  = "ignore";
-    private static final String INITIAL_HOST_ADDRESS_OPTION = "nodes";
-    private static final String NATIVE_PORT_OPTION = "port";
-    private static final String STORAGE_PORT_OPTION = "storage-port";
-    private static final String SSL_STORAGE_PORT_OPTION = "ssl-storage-port";
-    private static final String USER_OPTION = "username";
-    private static final String PASSWD_OPTION = "password";
-    private static final String AUTH_PROVIDER_OPTION = "auth-provider";
-    private static final String THROTTLE_MBITS = "throttle";
-    private static final String INTER_DC_THROTTLE_MBITS = "inter-dc-throttle";
-
-    /* client encryption options */
-    private static final String SSL_TRUSTSTORE = "truststore";
-    private static final String SSL_TRUSTSTORE_PW = "truststore-password";
-    private static final String SSL_KEYSTORE = "keystore";
-    private static final String SSL_KEYSTORE_PW = "keystore-password";
-    private static final String SSL_PROTOCOL = "ssl-protocol";
-    private static final String SSL_ALGORITHM = "ssl-alg";
-    private static final String SSL_STORE_TYPE = "store-type";
-    private static final String SSL_CIPHER_SUITES = "ssl-ciphers";
-    private static final String CONNECTIONS_PER_HOST = "connections-per-host";
-    private static final String CONFIG_PATH = "conf-path";
-
-    public static void main(String args[])
+    public static void main(String args[]) throws BulkLoadException
     {
-        Config.setClientMode(true);
-        LoaderOptions options = LoaderOptions.parseArgs(args).validateArguments();
+        LoaderOptions options = LoaderOptions.builder().parseArgs(args).build();
+        load(options);
+    }
+
+    public static void load(LoaderOptions options) throws BulkLoadException
+    {
+        DatabaseDescriptor.toolInitialization();
         OutputHandler handler = new OutputHandler.SystemOutput(options.verbose, options.debug);
         SSTableLoader loader = new SSTableLoader(
                 options.directory.getAbsoluteFile(),
@@ -88,8 +62,8 @@
                         options.sslStoragePort,
                         options.serverEncOptions,
                         buildSSLOptions(options.clientEncOptions)),
-                handler,
-                options.connectionsPerHost);
+                        handler,
+                        options.connectionsPerHost);
         DatabaseDescriptor.setStreamThroughputOutboundMegabitsPerSec(options.throttle);
         DatabaseDescriptor.setInterDCStreamThroughputOutboundMegabitsPerSec(options.interDcThrottle);
         StreamResultFuture future = null;
@@ -112,9 +86,11 @@
             JVMStabilityInspector.inspectThrowable(e);
             System.err.println(e.getMessage());
             if (e.getCause() != null)
+            {
                 System.err.println(e.getCause());
+            }
             e.printStackTrace(System.err);
-            System.exit(1);
+            throw new BulkLoadException(e);
         }
 
         try
@@ -122,18 +98,20 @@
             future.get();
 
             if (!options.noProgress)
+            {
                 indicator.printSummary(options.connectionsPerHost);
+            }
 
             // Give sockets time to gracefully close
             Thread.sleep(1000);
-            System.exit(0); // We need that to stop non daemonized threads
+            // System.exit(0); // We need that to stop non daemonized threads
         }
         catch (Exception e)
         {
             System.err.println("Streaming to the following hosts failed:");
             System.err.println(loader.getFailedHosts());
             e.printStackTrace(System.err);
-            System.exit(1);
+            throw new BulkLoadException(e);
         }
     }
 
@@ -144,7 +122,7 @@
         private long lastProgress;
         private long lastTime;
 
-        private int peak = 0;
+        private long peak = 0;
         private int totalFiles = 0;
 
         private final Multimap<InetAddress, SessionInfo> sessionsByHost = HashMultimap.create();
@@ -198,14 +176,16 @@
                         long current = 0;
                         int completed = 0;
 
-                        if (progressInfo != null && session.peer.equals(progressInfo.peer) && (session.sessionIndex == progressInfo.sessionIndex))
+                        if (progressInfo != null && session.peer.equals(progressInfo.peer) && session.sessionIndex == progressInfo.sessionIndex)
                         {
                             session.updateProgress(progressInfo);
                         }
                         for (ProgressInfo progress : session.getSendingFiles())
                         {
                             if (progress.isCompleted())
+                            {
                                 completed++;
+                            }
                             current += progress.currentBytes;
                         }
                         totalProgress += current;
@@ -217,7 +197,9 @@
                         sb.append(" ").append(String.format("%-3d", size == 0 ? 100L : current * 100L / size)).append("% ");
 
                         if (updateTotalFiles)
+                        {
                             totalFiles += session.getTotalFilesToSend();
+                        }
                     }
                 }
 
@@ -226,35 +208,37 @@
                 lastProgress = totalProgress;
 
                 sb.append("total: ").append(totalSize == 0 ? 100L : totalProgress * 100L / totalSize).append("% ");
-                sb.append(String.format("%-3d", mbPerSec(deltaProgress, deltaTime))).append("MB/s");
-                int average = mbPerSec(totalProgress, (time - start));
-                if (average > peak)
-                    peak = average;
-                sb.append("(avg: ").append(average).append(" MB/s)");
+                sb.append(FBUtilities.prettyPrintMemoryPerSecond(deltaProgress, deltaTime));
+                long average = bytesPerSecond(totalProgress, time - start);
 
-                System.out.print(sb.toString());
+                if (average > peak)
+                {
+                    peak = average;
+                }
+                sb.append(" (avg: ").append(FBUtilities.prettyPrintMemoryPerSecond(totalProgress, time - start)).append(")");
+
+                System.out.println(sb.toString());
             }
         }
 
-        private int mbPerSec(long bytes, long timeInNano)
+        private long bytesPerSecond(long bytes, long timeInNano)
         {
-            double bytesPerNano = ((double)bytes) / timeInNano;
-            return (int)((bytesPerNano * 1000 * 1000 * 1000) / (1024 * 1024));
+            return timeInNano != 0 ? (long) (((double) bytes / timeInNano) * 1000 * 1000 * 1000) : 0;
         }
 
         private void printSummary(int connectionsPerHost)
         {
             long end = System.nanoTime();
             long durationMS = ((end - start) / (1000000));
-            int average = mbPerSec(lastProgress, (end - start));
+
             StringBuilder sb = new StringBuilder();
             sb.append("\nSummary statistics: \n");
-            sb.append(String.format("   %-30s: %-10d%n", "Connections per host: ", connectionsPerHost));
-            sb.append(String.format("   %-30s: %-10d%n", "Total files transferred: ", totalFiles));
-            sb.append(String.format("   %-30s: %-10d%n", "Total bytes transferred: ", lastProgress));
-            sb.append(String.format("   %-30s: %-10d%n", "Total duration (ms): ", durationMS));
-            sb.append(String.format("   %-30s: %-10d%n", "Average transfer rate (MB/s): ", + average));
-            sb.append(String.format("   %-30s: %-10d%n", "Peak transfer rate (MB/s): ", + peak));
+            sb.append(String.format("   %-24s: %-10d%n", "Connections per host ", connectionsPerHost));
+            sb.append(String.format("   %-24s: %-10d%n", "Total files transferred ", totalFiles));
+            sb.append(String.format("   %-24s: %-10s%n", "Total bytes transferred ", FBUtilities.prettyPrintMemory(lastProgress)));
+            sb.append(String.format("   %-24s: %-10s%n", "Total duration ", durationMS + " ms"));
+            sb.append(String.format("   %-24s: %-10s%n", "Average transfer rate ", FBUtilities.prettyPrintMemoryPerSecond(lastProgress, end - start)));
+            sb.append(String.format("   %-24s: %-10s%n", "Peak transfer rate ",  FBUtilities.prettyPrintMemoryPerSecond(peak)));
             System.out.println(sb.toString());
         }
     }
@@ -263,7 +247,9 @@
     {
 
         if (!clientEncryptionOptions.enabled)
+        {
             return null;
+        }
 
         SSLContext sslContext;
         try
@@ -298,7 +284,7 @@
             super(hosts, port, authProvider, sslOptions);
             this.storagePort = storagePort;
             this.sslStoragePort = sslStoragePort;
-            this.serverEncOptions = serverEncryptionOptions;
+            serverEncOptions = serverEncryptionOptions;
         }
 
         @Override
@@ -308,343 +294,6 @@
         }
     }
 
-    static class LoaderOptions
-    {
-        public final File directory;
-
-        public boolean debug;
-        public boolean verbose;
-        public boolean noProgress;
-        public int nativePort;
-        public String user;
-        public String passwd;
-        public String authProviderName;
-        public AuthProvider authProvider;
-        public int throttle = 0;
-        public int interDcThrottle = 0;
-        public int storagePort;
-        public int sslStoragePort;
-        public EncryptionOptions.ClientEncryptionOptions clientEncOptions = new EncryptionOptions.ClientEncryptionOptions();
-        public int connectionsPerHost = 1;
-        public EncryptionOptions.ServerEncryptionOptions serverEncOptions = new EncryptionOptions.ServerEncryptionOptions();
-
-        public final Set<InetAddress> hosts = new HashSet<>();
-        public final Set<InetAddress> ignores = new HashSet<>();
-
-        LoaderOptions(File directory)
-        {
-            this.directory = directory;
-        }
-
-        public static LoaderOptions parseArgs(String cmdArgs[])
-        {
-            CommandLineParser parser = new GnuParser();
-            CmdLineOptions options = getCmdLineOptions();
-            try
-            {
-                CommandLine cmd = parser.parse(options, cmdArgs, false);
-
-                if (cmd.hasOption(HELP_OPTION))
-                {
-                    printUsage(options);
-                    System.exit(0);
-                }
-
-                String[] args = cmd.getArgs();
-                if (args.length == 0)
-                {
-                    System.err.println("Missing sstable directory argument");
-                    printUsage(options);
-                    System.exit(1);
-                }
-
-                if (args.length > 1)
-                {
-                    System.err.println("Too many arguments");
-                    printUsage(options);
-                    System.exit(1);
-                }
-
-                String dirname = args[0];
-                File dir = new File(dirname);
-
-                if (!dir.exists())
-                    errorMsg("Unknown directory: " + dirname, options);
-
-                if (!dir.isDirectory())
-                    errorMsg(dirname + " is not a directory", options);
-
-                LoaderOptions opts = new LoaderOptions(dir);
-
-                opts.verbose = cmd.hasOption(VERBOSE_OPTION);
-                opts.noProgress = cmd.hasOption(NOPROGRESS_OPTION);
-
-                if (cmd.hasOption(USER_OPTION))
-                    opts.user = cmd.getOptionValue(USER_OPTION);
-
-                if (cmd.hasOption(PASSWD_OPTION))
-                    opts.passwd = cmd.getOptionValue(PASSWD_OPTION);
-
-                if (cmd.hasOption(AUTH_PROVIDER_OPTION))
-                    opts.authProviderName = cmd.getOptionValue(AUTH_PROVIDER_OPTION);
-
-                if (cmd.hasOption(INITIAL_HOST_ADDRESS_OPTION))
-                {
-                    String[] nodes = cmd.getOptionValue(INITIAL_HOST_ADDRESS_OPTION).split(",");
-                    try
-                    {
-                        for (String node : nodes)
-                        {
-                            opts.hosts.add(InetAddress.getByName(node.trim()));
-                        }
-                    }
-                    catch (UnknownHostException e)
-                    {
-                        errorMsg("Unknown host: " + e.getMessage(), options);
-                    }
-
-                }
-                else
-                {
-                    System.err.println("Initial hosts must be specified (-d)");
-                    printUsage(options);
-                    System.exit(1);
-                }
-
-                if (cmd.hasOption(IGNORE_NODES_OPTION))
-                {
-                    String[] nodes = cmd.getOptionValue(IGNORE_NODES_OPTION).split(",");
-                    try
-                    {
-                        for (String node : nodes)
-                        {
-                            opts.ignores.add(InetAddress.getByName(node.trim()));
-                        }
-                    }
-                    catch (UnknownHostException e)
-                    {
-                        errorMsg("Unknown host: " + e.getMessage(), options);
-                    }
-                }
-
-                if (cmd.hasOption(CONNECTIONS_PER_HOST))
-                    opts.connectionsPerHost = Integer.parseInt(cmd.getOptionValue(CONNECTIONS_PER_HOST));
-
-                // try to load config file first, so that values can be rewritten with other option values.
-                // otherwise use default config.
-                Config config;
-                if (cmd.hasOption(CONFIG_PATH))
-                {
-                    File configFile = new File(cmd.getOptionValue(CONFIG_PATH));
-                    if (!configFile.exists())
-                    {
-                        errorMsg("Config file not found", options);
-                    }
-                    config = new YamlConfigurationLoader().loadConfig(configFile.toURI().toURL());
-                }
-                else
-                {
-                    config = new Config();
-                    // unthrottle stream by default
-                    config.stream_throughput_outbound_megabits_per_sec = 0;
-                    config.inter_dc_stream_throughput_outbound_megabits_per_sec = 0;
-                }
-
-                if (cmd.hasOption(NATIVE_PORT_OPTION))
-                    opts.nativePort = Integer.parseInt(cmd.getOptionValue(NATIVE_PORT_OPTION));
-                else
-                    opts.nativePort = config.native_transport_port;
-                if (cmd.hasOption(STORAGE_PORT_OPTION))
-                    opts.storagePort = Integer.parseInt(cmd.getOptionValue(STORAGE_PORT_OPTION));
-                else
-                    opts.storagePort = config.storage_port;
-                if (cmd.hasOption(SSL_STORAGE_PORT_OPTION))
-                    opts.sslStoragePort = Integer.parseInt(cmd.getOptionValue(SSL_STORAGE_PORT_OPTION));
-                else
-                    opts.sslStoragePort = config.ssl_storage_port;
-                opts.throttle = config.stream_throughput_outbound_megabits_per_sec;
-                opts.interDcThrottle = config.inter_dc_stream_throughput_outbound_megabits_per_sec;
-                opts.clientEncOptions = config.client_encryption_options;
-                opts.serverEncOptions = config.server_encryption_options;
-
-                if (cmd.hasOption(THROTTLE_MBITS))
-                {
-                    opts.throttle = Integer.parseInt(cmd.getOptionValue(THROTTLE_MBITS));
-                }
-
-                if (cmd.hasOption(INTER_DC_THROTTLE_MBITS))
-                {
-                    opts.interDcThrottle = Integer.parseInt(cmd.getOptionValue(INTER_DC_THROTTLE_MBITS));
-                }
-
-                if (cmd.hasOption(SSL_TRUSTSTORE) || cmd.hasOption(SSL_TRUSTSTORE_PW) ||
-                    cmd.hasOption(SSL_KEYSTORE) || cmd.hasOption(SSL_KEYSTORE_PW))
-                {
-                    opts.clientEncOptions.enabled = true;
-                }
-
-                if (cmd.hasOption(SSL_TRUSTSTORE))
-                {
-                    opts.clientEncOptions.truststore = cmd.getOptionValue(SSL_TRUSTSTORE);
-                }
-
-                if (cmd.hasOption(SSL_TRUSTSTORE_PW))
-                {
-                    opts.clientEncOptions.truststore_password = cmd.getOptionValue(SSL_TRUSTSTORE_PW);
-                }
-
-                if (cmd.hasOption(SSL_KEYSTORE))
-                {
-                    opts.clientEncOptions.keystore = cmd.getOptionValue(SSL_KEYSTORE);
-                    // if a keystore was provided, lets assume we'll need to use it
-                    opts.clientEncOptions.require_client_auth = true;
-                }
-
-                if (cmd.hasOption(SSL_KEYSTORE_PW))
-                {
-                    opts.clientEncOptions.keystore_password = cmd.getOptionValue(SSL_KEYSTORE_PW);
-                }
-
-                if (cmd.hasOption(SSL_PROTOCOL))
-                {
-                    opts.clientEncOptions.protocol = cmd.getOptionValue(SSL_PROTOCOL);
-                }
-
-                if (cmd.hasOption(SSL_ALGORITHM))
-                {
-                    opts.clientEncOptions.algorithm = cmd.getOptionValue(SSL_ALGORITHM);
-                }
-
-                if (cmd.hasOption(SSL_STORE_TYPE))
-                {
-                    opts.clientEncOptions.store_type = cmd.getOptionValue(SSL_STORE_TYPE);
-                }
-
-                if (cmd.hasOption(SSL_CIPHER_SUITES))
-                {
-                    opts.clientEncOptions.cipher_suites = cmd.getOptionValue(SSL_CIPHER_SUITES).split(",");
-                }
-
-                return opts;
-            }
-            catch (ParseException | ConfigurationException | MalformedURLException e)
-            {
-                errorMsg(e.getMessage(), options);
-                return null;
-            }
-        }
-
-        public LoaderOptions validateArguments()
-        {
-            // Both username and password need to be provided
-            if ((user != null) != (passwd != null))
-                errorMsg("Username and password must both be provided", getCmdLineOptions());
-
-            if (user != null)
-            {
-                // Support for 3rd party auth providers that support plain text credentials.
-                // In this case the auth provider must provide a constructor of the form:
-                //
-                // public MyAuthProvider(String username, String password)
-                if (authProviderName != null)
-                {
-                    try
-                    {
-                        Class authProviderClass = Class.forName(authProviderName);
-                        Constructor constructor = authProviderClass.getConstructor(String.class, String.class);
-                        authProvider = (AuthProvider)constructor.newInstance(user, passwd);
-                    }
-                    catch (ClassNotFoundException e)
-                    {
-                        errorMsg("Unknown auth provider: " + e.getMessage(), getCmdLineOptions());
-                    }
-                    catch (NoSuchMethodException e)
-                    {
-                        errorMsg("Auth provider does not support plain text credentials: " + e.getMessage(), getCmdLineOptions());
-                    }
-                    catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
-                    {
-                        errorMsg("Could not create auth provider with plain text credentials: " + e.getMessage(), getCmdLineOptions());
-                    }
-                }
-                else
-                {
-                    // If a 3rd party auth provider wasn't provided use the driver plain text provider
-                    authProvider = new PlainTextAuthProvider(user, passwd);
-                }
-            }
-            // Alternate support for 3rd party auth providers that don't use plain text credentials.
-            // In this case the auth provider must provide a nullary constructor of the form:
-            //
-            // public MyAuthProvider()
-            else if (authProviderName != null)
-            {
-                try
-                {
-                    authProvider = (AuthProvider)Class.forName(authProviderName).newInstance();
-                }
-                catch (ClassNotFoundException | InstantiationException | IllegalAccessException e)
-                {
-                    errorMsg("Unknown auth provider" + e.getMessage(), getCmdLineOptions());
-                }
-            }
-
-            return this;
-        }
-
-        private static void errorMsg(String msg, CmdLineOptions options)
-        {
-            System.err.println(msg);
-            printUsage(options);
-            System.exit(1);
-        }
-
-        private static CmdLineOptions getCmdLineOptions()
-        {
-            CmdLineOptions options = new CmdLineOptions();
-            options.addOption("v",  VERBOSE_OPTION,      "verbose output");
-            options.addOption("h",  HELP_OPTION,         "display this help message");
-            options.addOption(null, NOPROGRESS_OPTION,   "don't display progress");
-            options.addOption("i",  IGNORE_NODES_OPTION, "NODES", "don't stream to this (comma separated) list of nodes");
-            options.addOption("d",  INITIAL_HOST_ADDRESS_OPTION, "initial hosts", "Required. try to connect to these hosts (comma separated) initially for ring information");
-            options.addOption("p",  NATIVE_PORT_OPTION, "native transport port", "port used for native connection (default 9042)");
-            options.addOption("sp",  STORAGE_PORT_OPTION, "storage port", "port used for internode communication (default 7000)");
-            options.addOption("ssp",  SSL_STORAGE_PORT_OPTION, "ssl storage port", "port used for TLS internode communication (default 7001)");
-            options.addOption("t",  THROTTLE_MBITS, "throttle", "throttle speed in Mbits (default unlimited)");
-            options.addOption("idct",  INTER_DC_THROTTLE_MBITS, "inter-dc-throttle", "inter-datacenter throttle speed in Mbits (default unlimited)");
-            options.addOption("u",  USER_OPTION, "username", "username for cassandra authentication");
-            options.addOption("pw", PASSWD_OPTION, "password", "password for cassandra authentication");
-            options.addOption("ap", AUTH_PROVIDER_OPTION, "auth provider", "custom AuthProvider class name for cassandra authentication");
-            options.addOption("cph", CONNECTIONS_PER_HOST, "connectionsPerHost", "number of concurrent connections-per-host.");
-            // ssl connection-related options
-            options.addOption("ts", SSL_TRUSTSTORE, "TRUSTSTORE", "Client SSL: full path to truststore");
-            options.addOption("tspw", SSL_TRUSTSTORE_PW, "TRUSTSTORE-PASSWORD", "Client SSL: password of the truststore");
-            options.addOption("ks", SSL_KEYSTORE, "KEYSTORE", "Client SSL: full path to keystore");
-            options.addOption("kspw", SSL_KEYSTORE_PW, "KEYSTORE-PASSWORD", "Client SSL: password of the keystore");
-            options.addOption("prtcl", SSL_PROTOCOL, "PROTOCOL", "Client SSL: connections protocol to use (default: TLS)");
-            options.addOption("alg", SSL_ALGORITHM, "ALGORITHM", "Client SSL: algorithm (default: SunX509)");
-            options.addOption("st", SSL_STORE_TYPE, "STORE-TYPE", "Client SSL: type of store");
-            options.addOption("ciphers", SSL_CIPHER_SUITES, "CIPHER-SUITES", "Client SSL: comma-separated list of encryption suites to use");
-            options.addOption("f", CONFIG_PATH, "path to config file", "cassandra.yaml file path for streaming throughput and client/server SSL.");
-            return options;
-        }
-
-        public static void printUsage(Options options)
-        {
-            String usage = String.format("%s [options] <dir_path>", TOOL_NAME);
-            String header = System.lineSeparator() +
-                            "Bulk load the sstables found in the directory <dir_path> to the configured cluster." +
-                            "The parent directories of <dir_path> are used as the target keyspace/table name. " +
-                            "So for instance, to load an sstable named Standard1-g-1-Data.db into Keyspace1/Standard1, " +
-                            "you will need to have the files Standard1-g-1-Data.db and Standard1-g-1-Index.db into a directory /path/to/Keyspace1/Standard1/.";
-            String footer = System.lineSeparator() +
-                            "You can provide cassandra.yaml file with -f command line option to set up streaming throughput, client and server encryption options. " +
-                            "Only stream_throughput_outbound_megabits_per_sec, inter_dc_stream_throughput_outbound_megabits_per_sec, server_encryption_options and client_encryption_options are read from yaml. " +
-                            "You can override options read from cassandra.yaml with corresponding command line options.";
-            new HelpFormatter().printHelp(usage, header, options, footer);
-        }
-    }
-
     public static class CmdLineOptions extends Options
     {
         /**
diff --git a/src/java/org/apache/cassandra/tools/INodeProbeFactory.java b/src/java/org/apache/cassandra/tools/INodeProbeFactory.java
index fec4a2b..abc1651 100644
--- a/src/java/org/apache/cassandra/tools/INodeProbeFactory.java
+++ b/src/java/org/apache/cassandra/tools/INodeProbeFactory.java
@@ -25,18 +25,4 @@
     NodeProbe create(String host, int port) throws IOException;
 
     NodeProbe create(String host, int port, String username, String password) throws IOException;
-}
-
-class NodeProbeFactory implements INodeProbeFactory
-{
-
-    public NodeProbe create(String host, int port) throws IOException
-    {
-        return new NodeProbe(host, port);
-    }
-
-    public NodeProbe create(String host, int port, String username, String password) throws IOException
-    {
-        return new NodeProbe(host, port, username, password);
-    }
-}
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/tools/JsonTransformer.java b/src/java/org/apache/cassandra/tools/JsonTransformer.java
index a5e8553..315bb7f 100644
--- a/src/java/org/apache/cassandra/tools/JsonTransformer.java
+++ b/src/java/org/apache/cassandra/tools/JsonTransformer.java
@@ -24,18 +24,18 @@
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Stream;
 
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.db.ClusteringPrefix;
-import org.apache.cassandra.db.DecoratedKey;
-import org.apache.cassandra.db.DeletionTime;
-import org.apache.cassandra.db.LivenessInfo;
-import org.apache.cassandra.db.RangeTombstone;
+import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.CollectionType;
 import org.apache.cassandra.db.marshal.CompositeType;
@@ -50,13 +50,8 @@
 import org.apache.cassandra.db.rows.Unfiltered;
 import org.apache.cassandra.db.rows.UnfilteredRowIterator;
 import org.apache.cassandra.io.sstable.ISSTableScanner;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
-import org.codehaus.jackson.JsonFactory;
-import org.codehaus.jackson.JsonGenerator;
-import org.codehaus.jackson.impl.Indenter;
-import org.codehaus.jackson.util.DefaultPrettyPrinter;
-import org.codehaus.jackson.util.DefaultPrettyPrinter.NopIndenter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -97,7 +92,7 @@
     public static void toJson(ISSTableScanner currentScanner, Stream<UnfilteredRowIterator> partitions, boolean rawTime, CFMetaData metadata, OutputStream out)
             throws IOException
     {
-        try (JsonGenerator json = jsonFactory.createJsonGenerator(new OutputStreamWriter(out, "UTF-8")))
+        try (JsonGenerator json = jsonFactory.createJsonGenerator(new OutputStreamWriter(out, StandardCharsets.UTF_8)))
         {
             JsonTransformer transformer = new JsonTransformer(json, currentScanner, rawTime, metadata);
             json.writeStartArray();
@@ -108,7 +103,7 @@
 
     public static void keysToJson(ISSTableScanner currentScanner, Stream<DecoratedKey> keys, boolean rawTime, CFMetaData metadata, OutputStream out) throws IOException
     {
-        try (JsonGenerator json = jsonFactory.createJsonGenerator(new OutputStreamWriter(out, "UTF-8")))
+        try (JsonGenerator json = jsonFactory.createJsonGenerator(new OutputStreamWriter(out, StandardCharsets.UTF_8)))
         {
             JsonTransformer transformer = new JsonTransformer(json, currentScanner, rawTime, metadata);
             json.writeStartArray();
@@ -181,7 +176,6 @@
 
     private void serializePartition(UnfilteredRowIterator partition)
     {
-        String key = metadata.getKeyValidator().getString(partition.partitionKey().getKey());
         try
         {
             json.writeStartObject();
@@ -229,6 +223,7 @@
         }
         catch (IOException e)
         {
+            String key = metadata.getKeyValidator().getString(partition.partitionKey().getKey());
             logger.error("Fatal error parsing partition: {}", key, e);
         }
     }
@@ -320,7 +315,7 @@
         }
     }
 
-    private void serializeBound(RangeTombstone.Bound bound, DeletionTime deletionTime) throws IOException
+    private void serializeBound(ClusteringBound bound, DeletionTime deletionTime) throws IOException
     {
         json.writeFieldName(bound.isStart() ? "start" : "end");
         json.writeStartObject();
@@ -349,7 +344,7 @@
                 }
                 else
                 {
-                    json.writeRawValue(column.cellValueType().toJSONString(clustering.get(i), Server.CURRENT_VERSION));
+                    json.writeRawValue(column.cellValueType().toJSONString(clustering.get(i), ProtocolVersion.CURRENT));
                 }
             }
             json.writeEndArray();
@@ -387,7 +382,6 @@
                     objectIndenter.setCompact(true);
                     json.writeStartObject();
                     json.writeFieldName("name");
-                    AbstractType<?> type = cd.column().type;
                     json.writeString(cd.column().name.toCQLString());
                     serializeDeletion(complexData.complexDeletion());
                     objectIndenter.setCompact(true);
@@ -413,6 +407,7 @@
             objectIndenter.setCompact(true);
             json.writeFieldName("name");
             AbstractType<?> type = cell.column().type;
+            AbstractType<?> cellType = null;
             json.writeString(cell.column().name.toCQLString());
 
             if (type.isCollection() && type.isMultiCell()) // non-frozen collection
@@ -427,6 +422,30 @@
                 }
                 json.writeEndArray();
                 arrayIndenter.setCompact(false);
+
+                cellType = cell.column().cellValueType();
+            }
+            else if (type.isUDT() && type.isMultiCell()) // non-frozen udt
+            {
+                UserType ut = (UserType) type;
+                json.writeFieldName("path");
+                arrayIndenter.setCompact(true);
+                json.writeStartArray();
+                for (int i = 0; i < cell.path().size(); i++)
+                {
+                    Short fieldPosition = ut.nameComparator().compose(cell.path().get(i));
+                    json.writeString(ut.fieldNameAsString(fieldPosition));
+                }
+                json.writeEndArray();
+                arrayIndenter.setCompact(false);
+
+                // cellType of udt
+                Short fieldPosition = ((UserType) type).nameComparator().compose(cell.path().get(0));
+                cellType = ((UserType) type).fieldType(fieldPosition);
+            }
+            else
+            {
+                cellType = cell.column().cellValueType();
             }
             if (cell.isTombstone())
             {
@@ -441,7 +460,7 @@
             else
             {
                 json.writeFieldName("value");
-                json.writeRawValue(cell.column().cellValueType().toJSONString(cell.value(), Server.CURRENT_VERSION));
+                json.writeRawValue(cellType.toJSONString(cell.value(), ProtocolVersion.CURRENT));
             }
             if (liveInfo.isEmpty() || cell.timestamp() != liveInfo.timestamp())
             {
@@ -468,16 +487,21 @@
 
     private String dateString(TimeUnit from, long time)
     {
+        if (rawTime)
+        {
+            return Long.toString(time);
+        }
+        
         long secs = from.toSeconds(time);
         long offset = Math.floorMod(from.toNanos(time), 1000_000_000L); // nanos per sec
-        return rawTime? Long.toString(time) : Instant.ofEpochSecond(secs, offset).toString();
+        return Instant.ofEpochSecond(secs, offset).toString();
     }
 
     /**
-     * A specialized {@link Indenter} that enables a 'compact' mode which puts all subsequent json values on the same
+     * A specialized {@link com.fasterxml.jackson.core.util.DefaultPrettyPrinter.Indenter} that enables a 'compact' mode which puts all subsequent json values on the same
      * line. This is manipulated via {@link CompactIndenter#setCompact(boolean)}
      */
-    private static final class CompactIndenter extends NopIndenter
+    private static final class CompactIndenter extends DefaultPrettyPrinter.NopIndenter
     {
 
         private static final int INDENT_LEVELS = 16;
diff --git a/src/java/org/apache/cassandra/tools/LoaderOptions.java b/src/java/org/apache/cassandra/tools/LoaderOptions.java
new file mode 100644
index 0000000..b3110f2
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/LoaderOptions.java
@@ -0,0 +1,582 @@
+/*
+ *
+ * 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.tools;
+
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.net.*;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.cassandra.config.*;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.tools.BulkLoader.CmdLineOptions;
+
+import com.datastax.driver.core.AuthProvider;
+import com.datastax.driver.core.PlainTextAuthProvider;
+import org.apache.commons.cli.*;
+
+public class LoaderOptions
+{
+
+    public static final String HELP_OPTION = "help";
+    public static final String VERBOSE_OPTION = "verbose";
+    public static final String NOPROGRESS_OPTION = "no-progress";
+    public static final String NATIVE_PORT_OPTION = "port";
+    public static final String STORAGE_PORT_OPTION = "storage-port";
+    public static final String SSL_STORAGE_PORT_OPTION = "ssl-storage-port";
+    public static final String USER_OPTION = "username";
+    public static final String PASSWD_OPTION = "password";
+    public static final String AUTH_PROVIDER_OPTION = "auth-provider";
+    public static final String INITIAL_HOST_ADDRESS_OPTION = "nodes";
+    public static final String IGNORE_NODES_OPTION = "ignore";
+    public static final String CONNECTIONS_PER_HOST = "connections-per-host";
+    public static final String CONFIG_PATH = "conf-path";
+    public static final String THROTTLE_MBITS = "throttle";
+    public static final String INTER_DC_THROTTLE_MBITS = "inter-dc-throttle";
+    public static final String TOOL_NAME = "sstableloader";
+
+    /* client encryption options */
+    public static final String SSL_TRUSTSTORE = "truststore";
+    public static final String SSL_TRUSTSTORE_PW = "truststore-password";
+    public static final String SSL_KEYSTORE = "keystore";
+    public static final String SSL_KEYSTORE_PW = "keystore-password";
+    public static final String SSL_PROTOCOL = "ssl-protocol";
+    public static final String SSL_ALGORITHM = "ssl-alg";
+    public static final String SSL_STORE_TYPE = "store-type";
+    public static final String SSL_CIPHER_SUITES = "ssl-ciphers";
+
+    public final File directory;
+    public final boolean debug;
+    public final boolean verbose;
+    public final boolean noProgress;
+    public final int nativePort;
+    public final String user;
+    public final String passwd;
+    public final AuthProvider authProvider;
+    public final int throttle;
+    public final int interDcThrottle;
+    public final int storagePort;
+    public final int sslStoragePort;
+    public final EncryptionOptions.ClientEncryptionOptions clientEncOptions;
+    public final int connectionsPerHost;
+    public final EncryptionOptions.ServerEncryptionOptions serverEncOptions;
+    public final Set<InetAddress> hosts;
+    public final Set<InetAddress> ignores;
+
+    LoaderOptions(Builder builder)
+    {
+        directory = builder.directory;
+        debug = builder.debug;
+        verbose = builder.verbose;
+        noProgress = builder.noProgress;
+        nativePort = builder.nativePort;
+        user = builder.user;
+        passwd = builder.passwd;
+        authProvider = builder.authProvider;
+        throttle = builder.throttle;
+        interDcThrottle = builder.interDcThrottle;
+        storagePort = builder.storagePort;
+        sslStoragePort = builder.sslStoragePort;
+        clientEncOptions = builder.clientEncOptions;
+        connectionsPerHost = builder.connectionsPerHost;
+        serverEncOptions = builder.serverEncOptions;
+        hosts = builder.hosts;
+        ignores = builder.ignores;
+    }
+
+    static class Builder
+    {
+        File directory;
+        boolean debug;
+        boolean verbose;
+        boolean noProgress;
+        int nativePort = 9042;
+        String user;
+        String passwd;
+        String authProviderName;
+        AuthProvider authProvider;
+        int throttle = 0;
+        int interDcThrottle = 0;
+        int storagePort;
+        int sslStoragePort;
+        EncryptionOptions.ClientEncryptionOptions clientEncOptions = new EncryptionOptions.ClientEncryptionOptions();
+        int connectionsPerHost = 1;
+        EncryptionOptions.ServerEncryptionOptions serverEncOptions = new EncryptionOptions.ServerEncryptionOptions();
+        Set<InetAddress> hosts = new HashSet<>();
+        Set<InetAddress> ignores = new HashSet<>();
+
+        Builder()
+        {
+            //
+        }
+
+        public LoaderOptions build()
+        {
+            constructAuthProvider();
+            return new LoaderOptions(this);
+        }
+
+        public Builder directory(File directory)
+        {
+            this.directory = directory;
+            return this;
+        }
+
+        public Builder debug(boolean debug)
+        {
+            this.debug = debug;
+            return this;
+        }
+
+        public Builder verbose(boolean verbose)
+        {
+            this.verbose = verbose;
+            return this;
+        }
+
+        public Builder noProgress(boolean noProgress)
+        {
+            this.noProgress = noProgress;
+            return this;
+        }
+
+        public Builder nativePort(int nativePort)
+        {
+            this.nativePort = nativePort;
+            return this;
+        }
+
+        public Builder user(String user)
+        {
+            this.user = user;
+            return this;
+        }
+
+        public Builder password(String passwd)
+        {
+            this.passwd = passwd;
+            return this;
+        }
+
+        public Builder authProvider(AuthProvider authProvider)
+        {
+            this.authProvider = authProvider;
+            return this;
+        }
+
+        public Builder throttle(int throttle)
+        {
+            this.throttle = throttle;
+            return this;
+        }
+
+        public Builder interDcThrottle(int interDcThrottle)
+        {
+            this.interDcThrottle = interDcThrottle;
+            return this;
+        }
+
+        public Builder storagePort(int storagePort)
+        {
+            this.storagePort = storagePort;
+            return this;
+        }
+
+        public Builder sslStoragePort(int sslStoragePort)
+        {
+            this.sslStoragePort = sslStoragePort;
+            return this;
+        }
+
+        public Builder encOptions(EncryptionOptions.ClientEncryptionOptions encOptions)
+        {
+            this.clientEncOptions = encOptions;
+            return this;
+        }
+
+        public Builder connectionsPerHost(int connectionsPerHost)
+        {
+            this.connectionsPerHost = connectionsPerHost;
+            return this;
+        }
+
+        public Builder serverEncOptions(EncryptionOptions.ServerEncryptionOptions serverEncOptions)
+        {
+            this.serverEncOptions = serverEncOptions;
+            return this;
+        }
+
+        public Builder hosts(Set<InetAddress> hosts)
+        {
+            this.hosts = hosts;
+            return this;
+        }
+
+        public Builder host(InetAddress host)
+        {
+            hosts.add(host);
+            return this;
+        }
+
+        public Builder ignore(Set<InetAddress> ignores)
+        {
+            this.ignores = ignores;
+            return this;
+        }
+
+        public Builder ignore(InetAddress ignore)
+        {
+            ignores.add(ignore);
+            return this;
+        }
+
+        public Builder parseArgs(String cmdArgs[])
+        {
+            CommandLineParser parser = new GnuParser();
+            CmdLineOptions options = getCmdLineOptions();
+            try
+            {
+                CommandLine cmd = parser.parse(options, cmdArgs, false);
+
+                if (cmd.hasOption(HELP_OPTION))
+                {
+                    printUsage(options);
+                    System.exit(0);
+                }
+
+                String[] args = cmd.getArgs();
+                if (args.length == 0)
+                {
+                    System.err.println("Missing sstable directory argument");
+                    printUsage(options);
+                    System.exit(1);
+                }
+
+                if (args.length > 1)
+                {
+                    System.err.println("Too many arguments");
+                    printUsage(options);
+                    System.exit(1);
+                }
+
+                String dirname = args[0];
+                File dir = new File(dirname);
+
+                if (!dir.exists())
+                {
+                    errorMsg("Unknown directory: " + dirname, options);
+                }
+
+                if (!dir.isDirectory())
+                {
+                    errorMsg(dirname + " is not a directory", options);
+                }
+
+                directory = dir;
+
+                verbose = cmd.hasOption(VERBOSE_OPTION);
+                noProgress = cmd.hasOption(NOPROGRESS_OPTION);
+
+                if (cmd.hasOption(USER_OPTION))
+                {
+                    user = cmd.getOptionValue(USER_OPTION);
+                }
+
+                if (cmd.hasOption(PASSWD_OPTION))
+                {
+                    passwd = cmd.getOptionValue(PASSWD_OPTION);
+                }
+
+                if (cmd.hasOption(AUTH_PROVIDER_OPTION))
+                {
+                    authProviderName = cmd.getOptionValue(AUTH_PROVIDER_OPTION);
+                }
+
+                if (cmd.hasOption(INITIAL_HOST_ADDRESS_OPTION))
+                {
+                    String[] nodes = cmd.getOptionValue(INITIAL_HOST_ADDRESS_OPTION).split(",");
+                    try
+                    {
+                        for (String node : nodes)
+                        {
+                            hosts.add(InetAddress.getByName(node.trim()));
+                        }
+                    } catch (UnknownHostException e)
+                    {
+                        errorMsg("Unknown host: " + e.getMessage(), options);
+                    }
+
+                } else
+                {
+                    System.err.println("Initial hosts must be specified (-d)");
+                    printUsage(options);
+                    System.exit(1);
+                }
+
+                if (cmd.hasOption(IGNORE_NODES_OPTION))
+                {
+                    String[] nodes = cmd.getOptionValue(IGNORE_NODES_OPTION).split(",");
+                    try
+                    {
+                        for (String node : nodes)
+                        {
+                            ignores.add(InetAddress.getByName(node.trim()));
+                        }
+                    } catch (UnknownHostException e)
+                    {
+                        errorMsg("Unknown host: " + e.getMessage(), options);
+                    }
+                }
+
+                if (cmd.hasOption(CONNECTIONS_PER_HOST))
+                {
+                    connectionsPerHost = Integer.parseInt(cmd.getOptionValue(CONNECTIONS_PER_HOST));
+                }
+
+                // try to load config file first, so that values can be
+                // rewritten with other option values.
+                // otherwise use default config.
+                Config config;
+                if (cmd.hasOption(CONFIG_PATH))
+                {
+                    File configFile = new File(cmd.getOptionValue(CONFIG_PATH));
+                    if (!configFile.exists())
+                    {
+                        errorMsg("Config file not found", options);
+                    }
+                    config = new YamlConfigurationLoader().loadConfig(configFile.toURI().toURL());
+                }
+                else
+                {
+                    config = new Config();
+                    // unthrottle stream by default
+                    config.stream_throughput_outbound_megabits_per_sec = 0;
+                    config.inter_dc_stream_throughput_outbound_megabits_per_sec = 0;
+                }
+
+                if (cmd.hasOption(STORAGE_PORT_OPTION))
+                    storagePort = Integer.parseInt(cmd.getOptionValue(STORAGE_PORT_OPTION));
+                else
+                    storagePort = config.storage_port;
+                if (cmd.hasOption(SSL_STORAGE_PORT_OPTION))
+                    sslStoragePort = Integer.parseInt(cmd.getOptionValue(SSL_STORAGE_PORT_OPTION));
+                else
+                    sslStoragePort = config.ssl_storage_port;
+                throttle = config.stream_throughput_outbound_megabits_per_sec;
+                clientEncOptions = config.client_encryption_options;
+                serverEncOptions = config.server_encryption_options;
+
+                if (cmd.hasOption(THROTTLE_MBITS))
+                {
+                    throttle = Integer.parseInt(cmd.getOptionValue(THROTTLE_MBITS));
+                }
+
+                if (cmd.hasOption(INTER_DC_THROTTLE_MBITS))
+                {
+                    interDcThrottle = Integer.parseInt(cmd.getOptionValue(INTER_DC_THROTTLE_MBITS));
+                }
+
+                if (cmd.hasOption(SSL_TRUSTSTORE) || cmd.hasOption(SSL_TRUSTSTORE_PW) ||
+                            cmd.hasOption(SSL_KEYSTORE) || cmd.hasOption(SSL_KEYSTORE_PW))
+                {
+                    clientEncOptions.enabled = true;
+                }
+
+                if (cmd.hasOption(NATIVE_PORT_OPTION))
+                {
+                    nativePort = Integer.parseInt(cmd.getOptionValue(NATIVE_PORT_OPTION));
+                }
+                else
+                {
+                    if (config.native_transport_port_ssl != null && (config.client_encryption_options.enabled || clientEncOptions.enabled))
+                        nativePort = config.native_transport_port_ssl;
+                    else
+                        nativePort = config.native_transport_port;
+                }
+
+                if (cmd.hasOption(SSL_TRUSTSTORE))
+                {
+                    clientEncOptions.truststore = cmd.getOptionValue(SSL_TRUSTSTORE);
+                }
+
+                if (cmd.hasOption(SSL_TRUSTSTORE_PW))
+                {
+                    clientEncOptions.truststore_password = cmd.getOptionValue(SSL_TRUSTSTORE_PW);
+                }
+
+                if (cmd.hasOption(SSL_KEYSTORE))
+                {
+                    clientEncOptions.keystore = cmd.getOptionValue(SSL_KEYSTORE);
+                    // if a keystore was provided, lets assume we'll need to use
+                    // it
+                    clientEncOptions.require_client_auth = true;
+                }
+
+                if (cmd.hasOption(SSL_KEYSTORE_PW))
+                {
+                    clientEncOptions.keystore_password = cmd.getOptionValue(SSL_KEYSTORE_PW);
+                }
+
+                if (cmd.hasOption(SSL_PROTOCOL))
+                {
+                    clientEncOptions.protocol = cmd.getOptionValue(SSL_PROTOCOL);
+                }
+
+                if (cmd.hasOption(SSL_ALGORITHM))
+                {
+                    clientEncOptions.algorithm = cmd.getOptionValue(SSL_ALGORITHM);
+                }
+
+                if (cmd.hasOption(SSL_STORE_TYPE))
+                {
+                    clientEncOptions.store_type = cmd.getOptionValue(SSL_STORE_TYPE);
+                }
+
+                if (cmd.hasOption(SSL_CIPHER_SUITES))
+                {
+                    clientEncOptions.cipher_suites = cmd.getOptionValue(SSL_CIPHER_SUITES).split(",");
+                }
+
+                return this;
+            }
+            catch (ParseException | ConfigurationException | MalformedURLException e)
+            {
+                errorMsg(e.getMessage(), options);
+                return null;
+            }
+        }
+
+        private void constructAuthProvider()
+        {
+            // Both username and password need to be provided
+            if ((user != null) != (passwd != null))
+                errorMsg("Username and password must both be provided", getCmdLineOptions());
+
+            if (user != null)
+            {
+                // Support for 3rd party auth providers that support plain text credentials.
+                // In this case the auth provider must provide a constructor of the form:
+                //
+                // public MyAuthProvider(String username, String password)
+                if (authProviderName != null)
+                {
+                    try
+                    {
+                        Class authProviderClass = Class.forName(authProviderName);
+                        Constructor constructor = authProviderClass.getConstructor(String.class, String.class);
+                        authProvider = (AuthProvider)constructor.newInstance(user, passwd);
+                    }
+                    catch (ClassNotFoundException e)
+                    {
+                        errorMsg("Unknown auth provider: " + e.getMessage(), getCmdLineOptions());
+                    }
+                    catch (NoSuchMethodException e)
+                    {
+                        errorMsg("Auth provider does not support plain text credentials: " + e.getMessage(), getCmdLineOptions());
+                    }
+                    catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
+                    {
+                        errorMsg("Could not create auth provider with plain text credentials: " + e.getMessage(), getCmdLineOptions());
+                    }
+                }
+                else
+                {
+                    // If a 3rd party auth provider wasn't provided use the driver plain text provider
+                    this.authProvider = new PlainTextAuthProvider(user, passwd);
+                }
+            }
+            // Alternate support for 3rd party auth providers that don't use plain text credentials.
+            // In this case the auth provider must provide a nullary constructor of the form:
+            //
+            // public MyAuthProvider()
+            else if (authProviderName != null)
+            {
+                try
+                {
+                    authProvider = (AuthProvider)Class.forName(authProviderName).newInstance();
+                }
+                catch (ClassNotFoundException | InstantiationException | IllegalAccessException e)
+                {
+                    errorMsg("Unknown auth provider: " + e.getMessage(), getCmdLineOptions());
+                }
+            }
+        }
+    }
+
+    public static Builder builder()
+    {
+        return new Builder();
+    }
+
+    private static void errorMsg(String msg, CmdLineOptions options)
+    {
+        System.err.println(msg);
+        printUsage(options);
+        System.exit(1);
+    }
+
+    private static CmdLineOptions getCmdLineOptions()
+    {
+        CmdLineOptions options = new CmdLineOptions();
+        options.addOption("v", VERBOSE_OPTION, "verbose output");
+        options.addOption("h", HELP_OPTION, "display this help message");
+        options.addOption(null, NOPROGRESS_OPTION, "don't display progress");
+        options.addOption("i", IGNORE_NODES_OPTION, "NODES", "don't stream to this (comma separated) list of nodes");
+        options.addOption("d", INITIAL_HOST_ADDRESS_OPTION, "initial hosts", "Required. try to connect to these hosts (comma separated) initially for ring information");
+        options.addOption("p",  NATIVE_PORT_OPTION, "native transport port", "port used for native connection (default 9042)");
+        options.addOption("sp",  STORAGE_PORT_OPTION, "storage port", "port used for internode communication (default 7000)");
+        options.addOption("ssp",  SSL_STORAGE_PORT_OPTION, "ssl storage port", "port used for TLS internode communication (default 7001)");
+        options.addOption("t", THROTTLE_MBITS, "throttle", "throttle speed in Mbits (default unlimited)");
+        options.addOption("idct", INTER_DC_THROTTLE_MBITS, "inter-dc-throttle", "inter-datacenter throttle speed in Mbits (default unlimited)");
+        options.addOption("u", USER_OPTION, "username", "username for cassandra authentication");
+        options.addOption("pw", PASSWD_OPTION, "password", "password for cassandra authentication");
+        options.addOption("ap", AUTH_PROVIDER_OPTION, "auth provider", "custom AuthProvider class name for cassandra authentication");
+        options.addOption("cph", CONNECTIONS_PER_HOST, "connectionsPerHost", "number of concurrent connections-per-host.");
+        // ssl connection-related options
+        options.addOption("ts", SSL_TRUSTSTORE, "TRUSTSTORE", "Client SSL: full path to truststore");
+        options.addOption("tspw", SSL_TRUSTSTORE_PW, "TRUSTSTORE-PASSWORD", "Client SSL: password of the truststore");
+        options.addOption("ks", SSL_KEYSTORE, "KEYSTORE", "Client SSL: full path to keystore");
+        options.addOption("kspw", SSL_KEYSTORE_PW, "KEYSTORE-PASSWORD", "Client SSL: password of the keystore");
+        options.addOption("prtcl", SSL_PROTOCOL, "PROTOCOL", "Client SSL: connections protocol to use (default: TLS)");
+        options.addOption("alg", SSL_ALGORITHM, "ALGORITHM", "Client SSL: algorithm (default: SunX509)");
+        options.addOption("st", SSL_STORE_TYPE, "STORE-TYPE", "Client SSL: type of store");
+        options.addOption("ciphers", SSL_CIPHER_SUITES, "CIPHER-SUITES", "Client SSL: comma-separated list of encryption suites to use");
+        options.addOption("f", CONFIG_PATH, "path to config file", "cassandra.yaml file path for streaming throughput and client/server SSL.");
+        return options;
+    }
+
+    public static void printUsage(Options options)
+    {
+        String usage = String.format("%s [options] <dir_path>", TOOL_NAME);
+        String header = System.lineSeparator() +
+                "Bulk load the sstables found in the directory <dir_path> to the configured cluster." +
+                "The parent directories of <dir_path> are used as the target keyspace/table name. " +
+                "So for instance, to load an sstable named Standard1-g-1-Data.db into Keyspace1/Standard1, " +
+                "you will need to have the files Standard1-g-1-Data.db and Standard1-g-1-Index.db into a directory /path/to/Keyspace1/Standard1/.";
+        String footer = System.lineSeparator() +
+                "You can provide cassandra.yaml file with -f command line option to set up streaming throughput, client and server encryption options. " +
+                "Only stream_throughput_outbound_megabits_per_sec, server_encryption_options and client_encryption_options are read from yaml. " +
+                "You can override options read from cassandra.yaml with corresponding command line options.";
+        new HelpFormatter().printHelp(usage, header, options, footer);
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java
index b2273ee..821b8a3 100644
--- a/src/java/org/apache/cassandra/tools/NodeProbe.java
+++ b/src/java/org/apache/cassandra/tools/NodeProbe.java
@@ -65,6 +65,7 @@
 import org.apache.cassandra.gms.Gossiper;
 import org.apache.cassandra.gms.GossiperMBean;
 import org.apache.cassandra.db.HintedHandOffManager;
+import org.apache.cassandra.locator.DynamicEndpointSnitchMBean;
 import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
 import org.apache.cassandra.metrics.CassandraMetricsRegistry;
 import org.apache.cassandra.metrics.TableMetrics.Sampler;
@@ -73,6 +74,7 @@
 import org.apache.cassandra.metrics.ThreadPoolMetrics;
 import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.net.MessagingServiceMBean;
+import org.apache.cassandra.schema.CompactionParams.TombstoneOption;
 import org.apache.cassandra.service.CacheService;
 import org.apache.cassandra.service.CacheServiceMBean;
 import org.apache.cassandra.service.GCInspector;
@@ -85,11 +87,13 @@
 import org.apache.cassandra.streaming.management.StreamStateCompositeData;
 
 import com.google.common.base.Function;
+import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Uninterruptibles;
+import org.apache.cassandra.tools.nodetool.GetTimeout;
 
 /**
  * JMX client operations for Cassandra.
@@ -293,11 +297,16 @@
         return ssProxy.upgradeSSTables(keyspaceName, excludeCurrentVersion, jobs, tableNames);
     }
 
+    public int garbageCollect(String tombstoneOption, int jobs, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
+    {
+        return ssProxy.garbageCollect(tombstoneOption, jobs, keyspaceName, tableNames);
+    }
+
     private void checkJobs(PrintStream out, int jobs)
     {
         int compactors = ssProxy.getConcurrentCompactors();
         if (jobs > compactors)
-            out.println(String.format("jobs (%d) is bigger than configured concurrent_compactors (%d), using at most %d threads", jobs, compactors, compactors));
+            out.println(String.format("jobs (%d) is bigger than configured concurrent_compactors (%d) on the host, using at most %d threads", jobs, compactors, compactors));
     }
 
     public void forceKeyspaceCleanup(PrintStream out, int jobs, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
@@ -355,7 +364,7 @@
         {
             case 1:
                 failed = true;
-                out.println("Aborted upgrading sstables for atleast one table in keyspace "+keyspaceName+", check server logs for more information.");
+                out.println("Aborted upgrading sstables for at least one table in keyspace " + keyspaceName + ", check server logs for more information.");
                 break;
             case 2:
                 failed = true;
@@ -364,12 +373,43 @@
         }
     }
 
+    public void garbageCollect(PrintStream out, String tombstoneOption, int jobs, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
+    {
+        if (garbageCollect(tombstoneOption, jobs, keyspaceName, tableNames) != 0)
+        {
+            failed = true;
+            out.println("Aborted garbage collection for at least one table in keyspace " + keyspaceName + ", check server logs for more information.");
+        }
+    }
+
+    public void forceUserDefinedCompaction(String datafiles) throws IOException, ExecutionException, InterruptedException
+    {
+        compactionProxy.forceUserDefinedCompaction(datafiles);
+    }
 
     public void forceKeyspaceCompaction(boolean splitOutput, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
     {
         ssProxy.forceKeyspaceCompaction(splitOutput, keyspaceName, tableNames);
     }
 
+    public void relocateSSTables(int jobs, String keyspace, String[] cfnames) throws IOException, ExecutionException, InterruptedException
+    {
+        ssProxy.relocateSSTables(jobs, keyspace, cfnames);
+    }
+
+    /**
+     * Forces major compaction of specified token range in a single keyspace.
+     *
+     * @param keyspaceName the name of the keyspace to be compacted
+     * @param startToken the token at which the compaction range starts (inclusive)
+     * @param endToken the token at which compaction range ends (inclusive)
+     * @param tableNames the names of the tables to be compacted
+     */
+    public void forceKeyspaceCompactionForTokenRange(String keyspaceName, final String startToken, final String endToken, String... tableNames) throws IOException, ExecutionException, InterruptedException
+    {
+        ssProxy.forceKeyspaceCompactionForTokenRange(keyspaceName, startToken, endToken, tableNames);
+    }
+
     public void forceKeyspaceFlush(String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException
     {
         ssProxy.forceKeyspaceFlush(keyspaceName, tableNames);
@@ -582,9 +622,10 @@
      *
      * @param snapshotName the name of the snapshot.
      * @param table the table to snapshot or all on null
+     * @param options Options (skipFlush for now)
      * @param keyspaces the keyspaces to snapshot
      */
-    public void takeSnapshot(String snapshotName, String table, String... keyspaces) throws IOException
+    public void takeSnapshot(String snapshotName, String table, Map<String, String> options, String... keyspaces) throws IOException
     {
         if (table != null)
         {
@@ -592,10 +633,11 @@
             {
                 throw new IOException("When specifying the table for a snapshot, you must specify one and only one keyspace");
             }
-            ssProxy.takeTableSnapshot(keyspaces[0], table, snapshotName);
+
+            ssProxy.takeSnapshot(snapshotName, options, keyspaces[0] + "." + table);
         }
         else
-            ssProxy.takeSnapshot(snapshotName, keyspaces);
+            ssProxy.takeSnapshot(snapshotName, options, keyspaces);
     }
 
     /**
@@ -603,21 +645,22 @@
      *
      * @param snapshotName
      *            the name of the snapshot.
+     * @param options
+     *            Options (skipFlush for now)
      * @param tableList
      *            list of columnfamily from different keyspace in the form of ks1.cf1 ks2.cf2
      */
-    public void takeMultipleTableSnapshot(String snapshotName, String... tableList)
+    public void takeMultipleTableSnapshot(String snapshotName, Map<String, String> options, String... tableList)
             throws IOException
     {
         if (null != tableList && tableList.length != 0)
         {
-            ssProxy.takeMultipleTableSnapshot(snapshotName, tableList);
+            ssProxy.takeSnapshot(snapshotName, options, tableList);
         }
         else
         {
             throw new IOException("The column family List  for a snapshot should not be empty or null");
         }
-
     }
 
     /**
@@ -751,10 +794,10 @@
         return ssProxy.getNaturalEndpoints(keyspace, cf, key);
     }
 
-    public List<String> getSSTables(String keyspace, String cf, String key)
+    public List<String> getSSTables(String keyspace, String cf, String key, boolean hexFormat)
     {
         ColumnFamilyStoreMBean cfsProxy = getCfsProxy(keyspace, cf);
-        return cfsProxy.getSSTablesForKey(key);
+        return cfsProxy.getSSTablesForKey(key, hexFormat);
     }
 
     public Set<StreamState> getStreamStatus()
@@ -806,6 +849,18 @@
         }
     }
 
+    public DynamicEndpointSnitchMBean getDynamicEndpointSnitchInfoProxy()
+    {
+        try
+        {
+            return JMX.newMBeanProxy(mbeanServerConn, new ObjectName("org.apache.cassandra.db:type=DynamicEndpointSnitch"), DynamicEndpointSnitchMBean.class);
+        }
+        catch (MalformedObjectNameException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
     public ColumnFamilyStoreMBean getCfsProxy(String ks, String cf)
     {
         ColumnFamilyStoreMBean cfsProxy = null;
@@ -840,6 +895,11 @@
         return spProxy;
     }
 
+    public MessagingServiceMBean getMessagingServiceProxy()
+    {
+        return msProxy;
+    }
+
     public StorageServiceMBean getStorageService() {
         return ssProxy;
     }
@@ -920,6 +980,11 @@
         return spProxy.getHintedHandoffDisabledDCs();
     }
 
+    public Map<String, String> getViewBuildStatuses(String keyspace, String view)
+    {
+        return ssProxy.getViewBuildStatuses(keyspace, view);
+    }
+
     public void pauseHintsDelivery()
     {
         hhProxy.pauseHintsDelivery(true);
@@ -1024,6 +1089,41 @@
         return ssProxy.getCompactionThroughputMbPerSec();
     }
 
+    public void setConcurrentCompactors(int value)
+    {
+        ssProxy.setConcurrentCompactors(value);
+    }
+
+    public int getConcurrentCompactors()
+    {
+        return ssProxy.getConcurrentCompactors();
+    }
+
+    public long getTimeout(String type)
+    {
+        switch (type)
+        {
+            case "misc":
+                return ssProxy.getRpcTimeout();
+            case "read":
+                return ssProxy.getReadRpcTimeout();
+            case "range":
+                return ssProxy.getRangeRpcTimeout();
+            case "write":
+                return ssProxy.getWriteRpcTimeout();
+            case "counterwrite":
+                return ssProxy.getCounterWriteRpcTimeout();
+            case "cascontention":
+                return ssProxy.getCasContentionTimeout();
+            case "truncate":
+                return ssProxy.getTruncateRpcTimeout();
+            case "streamingsocket":
+                return (long) ssProxy.getStreamingSocketTimeout();
+            default:
+                throw new RuntimeException("Timeout type requires one of (" + GetTimeout.TIMEOUT_TYPES + ")");
+        }
+    }
+
     public int getStreamThroughput()
     {
         return ssProxy.getStreamThroughputMbPerSec();
@@ -1069,6 +1169,44 @@
         compactionProxy.stopCompaction(string);
     }
 
+    public void setTimeout(String type, long value)
+    {
+        if (value < 0)
+            throw new RuntimeException("timeout must be non-negative");
+
+        switch (type)
+        {
+            case "misc":
+                ssProxy.setRpcTimeout(value);
+                break;
+            case "read":
+                ssProxy.setReadRpcTimeout(value);
+                break;
+            case "range":
+                ssProxy.setRangeRpcTimeout(value);
+                break;
+            case "write":
+                ssProxy.setWriteRpcTimeout(value);
+                break;
+            case "counterwrite":
+                ssProxy.setCounterWriteRpcTimeout(value);
+                break;
+            case "cascontention":
+                ssProxy.setCasContentionTimeout(value);
+                break;
+            case "truncate":
+                ssProxy.setTruncateRpcTimeout(value);
+                break;
+            case "streamingsocket":
+                if (value > Integer.MAX_VALUE)
+                    throw new RuntimeException("streamingsocket timeout must be less than " + Integer.MAX_VALUE);
+                ssProxy.setStreamingSocketTimeout((int) value);
+                break;
+            default:
+                throw new RuntimeException("Timeout type requires one of (" + GetTimeout.TIMEOUT_TYPES + ")");
+        }
+    }
+
     public void stopById(String compactionId)
     {
         compactionProxy.stopCompactionById(compactionId);
@@ -1099,9 +1237,9 @@
         return ssProxy.describeRingJMX(keyspaceName);
     }
 
-    public void rebuild(String sourceDc)
+    public void rebuild(String sourceDc, String keyspace, String tokens, String specificSources)
     {
-        ssProxy.rebuild(sourceDc);
+        ssProxy.rebuild(sourceDc, keyspace, tokens, specificSources);
     }
 
     public List<String> sampleKeyRange()
@@ -1160,9 +1298,18 @@
                             CassandraMetricsRegistry.JmxGaugeMBean.class).getValue();
                 case "Requests":
                 case "Hits":
+                case "Misses":
                     return JMX.newMBeanProxy(mbeanServerConn,
                             new ObjectName("org.apache.cassandra.metrics:type=Cache,scope=" + cacheType + ",name=" + metricName),
                             CassandraMetricsRegistry.JmxMeterMBean.class).getCount();
+                case "MissLatency":
+                    return JMX.newMBeanProxy(mbeanServerConn,
+                            new ObjectName("org.apache.cassandra.metrics:type=Cache,scope=" + cacheType + ",name=" + metricName),
+                            CassandraMetricsRegistry.JmxTimerMBean.class).getMean();
+                case "MissLatencyUnit":
+                    return JMX.newMBeanProxy(mbeanServerConn,
+                            new ObjectName("org.apache.cassandra.metrics:type=Cache,scope=" + cacheType + ",name=MissLatency"),
+                            CassandraMetricsRegistry.JmxTimerMBean.class).getDurationUnit();
                 default:
                     throw new RuntimeException("Unknown cache metric name.");
 
@@ -1188,18 +1335,35 @@
         return ThreadPoolMetrics.getJmxThreadPools(mbeanServerConn);
     }
 
+    public int getNumberOfTables()
+    {
+        return spProxy.getNumberOfTables();
+    }
+
     /**
      * Retrieve ColumnFamily metrics
-     * @param ks Keyspace for which stats are to be displayed.
-     * @param cf ColumnFamily for which stats are to be displayed.
+     * @param ks Keyspace for which stats are to be displayed or null for the global value
+     * @param cf ColumnFamily for which stats are to be displayed or null for the keyspace value (if ks supplied)
      * @param metricName View {@link TableMetrics}.
      */
     public Object getColumnFamilyMetric(String ks, String cf, String metricName)
     {
         try
         {
-            String type = cf.contains(".") ? "IndexTable" : "Table";
-            ObjectName oName = new ObjectName(String.format("org.apache.cassandra.metrics:type=%s,keyspace=%s,scope=%s,name=%s", type, ks, cf, metricName));
+            ObjectName oName = null;
+            if (!Strings.isNullOrEmpty(ks) && !Strings.isNullOrEmpty(cf))
+            {
+                String type = cf.contains(".") ? "IndexTable" : "Table";
+                oName = new ObjectName(String.format("org.apache.cassandra.metrics:type=%s,keyspace=%s,scope=%s,name=%s", type, ks, cf, metricName));
+            }
+            else if (!Strings.isNullOrEmpty(ks))
+            {
+                oName = new ObjectName(String.format("org.apache.cassandra.metrics:type=Keyspace,keyspace=%s,name=%s", ks, metricName));
+            }
+            else
+            {
+                oName = new ObjectName(String.format("org.apache.cassandra.metrics:type=Table,name=%s", metricName));
+            }
             switch(metricName)
             {
                 case "BloomFilterDiskSpaceUsed":
@@ -1220,6 +1384,7 @@
                 case "MemtableLiveDataSize":
                 case "MemtableOffHeapSize":
                 case "MinPartitionSize":
+                case "PercentRepaired":
                 case "RecentBloomFilterFalsePositives":
                 case "RecentBloomFilterFalseRatio":
                 case "SnapshotsSize":
@@ -1231,6 +1396,7 @@
                 case "WriteTotalLatency":
                 case "ReadTotalLatency":
                 case "PendingFlushes":
+                case "DroppedMutations":
                     return JMX.newMBeanProxy(mbeanServerConn, oName, CassandraMetricsRegistry.JmxCounterMBean.class).getCount();
                 case "CoordinatorReadLatency":
                 case "CoordinatorScanLatency":
@@ -1285,6 +1451,7 @@
                             CassandraMetricsRegistry.JmxCounterMBean.class);
                 case "CompletedTasks":
                 case "PendingTasks":
+                case "PendingTasksByTableName":
                     return JMX.newMBeanProxy(mbeanServerConn,
                             new ObjectName("org.apache.cassandra.metrics:type=Compaction,name=" + metricName),
                             CassandraMetricsRegistry.JmxGaugeMBean.class).getValue();
@@ -1429,11 +1596,6 @@
             throw new RuntimeException(e);
         }
     }
-
-    public MessagingServiceMBean getMessagingServiceProxy()
-    {
-        return msProxy;
-    }
 }
 
 class ColumnFamilyStoreMBeanIterator implements Iterator<Map.Entry<String, ColumnFamilyStoreMBean>>
diff --git a/src/java/org/apache/cassandra/tools/NodeProbeFactory.java b/src/java/org/apache/cassandra/tools/NodeProbeFactory.java
new file mode 100644
index 0000000..577bcfa
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/NodeProbeFactory.java
@@ -0,0 +1,34 @@
+/*
+ * 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.tools;
+
+import java.io.IOException;
+
+public class NodeProbeFactory implements INodeProbeFactory
+{
+    public NodeProbe create(String host, int port) throws IOException
+    {
+        return new NodeProbe(host, port);
+    }
+
+    public NodeProbe create(String host, int port, String username, String password) throws IOException
+    {
+        return new NodeProbe(host, port, username, password);
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java
index a195aa7..4846d73 100644
--- a/src/java/org/apache/cassandra/tools/NodeTool.java
+++ b/src/java/org/apache/cassandra/tools/NodeTool.java
@@ -80,6 +80,7 @@
                 Verify.class,
                 Flush.class,
                 UpgradeSSTable.class,
+                GarbageCollect.class,
                 DisableAutoCompaction.class,
                 EnableAutoCompaction.class,
                 CompactionStats.class,
@@ -95,6 +96,7 @@
                 GcStats.class,
                 GetCompactionThreshold.class,
                 GetCompactionThroughput.class,
+                GetTimeout.class,
                 GetStreamThroughput.class,
                 GetTraceProbability.class,
                 GetInterDCStreamThroughput.class,
@@ -119,6 +121,9 @@
                 SetHintedHandoffThrottleInKB.class,
                 SetCompactionThreshold.class,
                 SetCompactionThroughput.class,
+                GetConcurrentCompactors.class,
+                SetConcurrentCompactors.class,
+                SetTimeout.class,
                 SetStreamThroughput.class,
                 SetInterDCStreamThroughput.class,
                 SetTraceProbability.class,
@@ -153,7 +158,9 @@
                 DisableHintsForDC.class,
                 EnableHintsForDC.class,
                 FailureDetectorInfo.class,
-                RefreshSizeEstimates.class
+                RefreshSizeEstimates.class,
+                RelocateSSTables.class,
+                ViewBuildStatus.class
         );
 
         Cli.CliBuilder<NodeToolCmdRunnable> builder = Cli.builder("nodetool");
@@ -346,7 +353,7 @@
                 else
                     nodeClient = nodeProbeFactory.create(host, parseInt(port), username, password);
                 nodeClient.setOutput(output);
-            } catch (IOException e)
+            } catch (IOException | SecurityException e)
             {
                 Throwable rootCause = Throwables.getRootCause(e);
                 output.err.println(format("nodetool: Failed to connect to '%s:%s' - %s: '%s'.", host, port, rootCause.getClass().getSimpleName(), rootCause.getMessage()));
diff --git a/src/java/org/apache/cassandra/tools/RepairRunner.java b/src/java/org/apache/cassandra/tools/RepairRunner.java
index 12ae5b8..961916a 100644
--- a/src/java/org/apache/cassandra/tools/RepairRunner.java
+++ b/src/java/org/apache/cassandra/tools/RepairRunner.java
@@ -56,7 +56,8 @@
         cmd = ssProxy.repairAsync(keyspace, options);
         if (cmd <= 0)
         {
-            String message = String.format("[%s] Nothing to repair for keyspace '%s'", format.format(System.currentTimeMillis()), keyspace);
+            // repairAsync can only return 0 for replication factor 1.
+            String message = String.format("[%s] Replication factor is 1. No repair is needed for keyspace '%s'", format.format(System.currentTimeMillis()), keyspace);
             out.println(message);
         }
         else
diff --git a/src/java/org/apache/cassandra/tools/SSTableExpiredBlockers.java b/src/java/org/apache/cassandra/tools/SSTableExpiredBlockers.java
index 3e2ff08..1f407cb 100644
--- a/src/java/org/apache/cassandra/tools/SSTableExpiredBlockers.java
+++ b/src/java/org/apache/cassandra/tools/SSTableExpiredBlockers.java
@@ -32,7 +32,6 @@
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Directories;
 import org.apache.cassandra.db.Keyspace;
-import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
diff --git a/src/java/org/apache/cassandra/tools/SSTableExport.java b/src/java/org/apache/cassandra/tools/SSTableExport.java
index ac8ea61..40ddbe8 100644
--- a/src/java/org/apache/cassandra/tools/SSTableExport.java
+++ b/src/java/org/apache/cassandra/tools/SSTableExport.java
@@ -28,7 +28,7 @@
 import org.apache.commons.cli.*;
 
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.PartitionPosition;
@@ -37,14 +37,12 @@
 import org.apache.cassandra.db.rows.UnfilteredRowIterator;
 import org.apache.cassandra.dht.*;
 import org.apache.cassandra.exceptions.ConfigurationException;
-import org.apache.cassandra.index.SecondaryIndexManager;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.ISSTableScanner;
 import org.apache.cassandra.io.sstable.KeyIterator;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.sstable.metadata.MetadataComponent;
 import org.apache.cassandra.io.sstable.metadata.MetadataType;
-import org.apache.cassandra.io.sstable.metadata.ValidationMetadata;
 import org.apache.cassandra.utils.FBUtilities;
 
 /**
@@ -64,7 +62,7 @@
 
     static
     {
-        Config.setClientMode(true);
+        DatabaseDescriptor.toolInitialization();
 
         Option optKey = new Option(KEY_OPTION, true, "Partition key");
         // Number of times -k <key> can be passed on the command line.
diff --git a/src/java/org/apache/cassandra/tools/SSTableLevelResetter.java b/src/java/org/apache/cassandra/tools/SSTableLevelResetter.java
index e7d04f1..40cdd7b 100644
--- a/src/java/org/apache/cassandra/tools/SSTableLevelResetter.java
+++ b/src/java/org/apache/cassandra/tools/SSTableLevelResetter.java
@@ -79,7 +79,12 @@
             // remove any leftovers in the transaction log
             Keyspace keyspace = Keyspace.openWithoutSSTables(keyspaceName);
             ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(columnfamily);
-            LifecycleTransaction.removeUnfinishedLeftovers(cfs.metadata);
+            if (!LifecycleTransaction.removeUnfinishedLeftovers(cfs))
+            {
+                throw new RuntimeException(String.format("Cannot remove temporary or obsoleted files for %s.%s " +
+                                                         "due to a problem with transaction log files.",
+                                                         keyspace, columnfamily));
+            }
 
             Directories.SSTableLister lister = cfs.getDirectories().sstableLister(Directories.OnTxnErr.THROW).skipTemporary(true);
             boolean foundSSTable = false;
diff --git a/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java b/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java
index f100085..068b805 100644
--- a/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java
+++ b/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java
@@ -17,11 +17,17 @@
  */
 package org.apache.cassandra.tools;
 
+import java.io.DataInputStream;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
 import java.util.EnumSet;
+import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.CommandLineParser;
@@ -31,12 +37,23 @@
 import org.apache.commons.cli.ParseException;
 import org.apache.commons.cli.PosixParser;
 
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.SerializationHeader;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.rows.EncodingStats;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.io.compress.CompressionMetadata;
+import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.IndexSummary;
 import org.apache.cassandra.io.sstable.metadata.CompactionMetadata;
 import org.apache.cassandra.io.sstable.metadata.MetadataComponent;
 import org.apache.cassandra.io.sstable.metadata.MetadataType;
 import org.apache.cassandra.io.sstable.metadata.StatsMetadata;
 import org.apache.cassandra.io.sstable.metadata.ValidationMetadata;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.Pair;
 
 /**
  * Shows the contents of sstable metadata
@@ -82,6 +99,11 @@
                 ValidationMetadata validation = (ValidationMetadata) metadata.get(MetadataType.VALIDATION);
                 StatsMetadata stats = (StatsMetadata) metadata.get(MetadataType.STATS);
                 CompactionMetadata compaction = (CompactionMetadata) metadata.get(MetadataType.COMPACTION);
+                CompressionMetadata compression = null;
+                File compressionFile = new File(descriptor.filenameFor(Component.COMPRESSION_INFO));
+                if (compressionFile.exists())
+                    compression = CompressionMetadata.create(fname);
+                SerializationHeader.Component header = (SerializationHeader.Component) metadata.get(MetadataType.HEADER);
 
                 out.printf("SSTable: %s%n", descriptor);
                 if (validation != null)
@@ -95,16 +117,42 @@
                     out.printf("Maximum timestamp: %s%n", stats.maxTimestamp);
                     out.printf("SSTable min local deletion time: %s%n", stats.minLocalDeletionTime);
                     out.printf("SSTable max local deletion time: %s%n", stats.maxLocalDeletionTime);
-                    out.printf("Compression ratio: %s%n", stats.compressionRatio);
+                    out.printf("Compressor: %s%n", compression != null ? compression.compressor().getClass().getName() : "-");
+                    if (compression != null)
+                        out.printf("Compression ratio: %s%n", stats.compressionRatio);
+                    out.printf("TTL min: %s%n", stats.minTTL);
+                    out.printf("TTL max: %s%n", stats.maxTTL);
+
+                    if (validation != null && header != null)
+                        printMinMaxToken(descriptor, FBUtilities.newPartitioner(descriptor), header.getKeyType(), out);
+
+                    if (header != null && header.getClusteringTypes().size() == stats.minClusteringValues.size())
+                    {
+                        List<AbstractType<?>> clusteringTypes = header.getClusteringTypes();
+                        List<ByteBuffer> minClusteringValues = stats.minClusteringValues;
+                        List<ByteBuffer> maxClusteringValues = stats.maxClusteringValues;
+                        String[] minValues = new String[clusteringTypes.size()];
+                        String[] maxValues = new String[clusteringTypes.size()];
+                        for (int i = 0; i < clusteringTypes.size(); i++)
+                        {
+                            minValues[i] = clusteringTypes.get(i).getString(minClusteringValues.get(i));
+                            maxValues[i] = clusteringTypes.get(i).getString(maxClusteringValues.get(i));
+                        }
+                        out.printf("minClustringValues: %s%n", Arrays.toString(minValues));
+                        out.printf("maxClustringValues: %s%n", Arrays.toString(maxValues));
+                    }
                     out.printf("Estimated droppable tombstones: %s%n", stats.getEstimatedDroppableTombstoneRatio((int) (System.currentTimeMillis() / 1000) - gcgs));
                     out.printf("SSTable Level: %d%n", stats.sstableLevel);
                     out.printf("Repaired at: %d%n", stats.repairedAt);
                     out.printf("Originating host id: %s%n", stats.originatingHostId);
-                    out.printf("Replay positions covered: %s\n", stats.commitLogIntervals);
+                    out.printf("Replay positions covered: %s%n", stats.commitLogIntervals);
+                    out.printf("totalColumnsSet: %s%n", stats.totalColumnsSet);
+                    out.printf("totalRows: %s%n", stats.totalRows);
                     out.println("Estimated tombstone drop times:");
-                    for (Map.Entry<Double, Long> entry : stats.estimatedTombstoneDropTime.getAsMap().entrySet())
+
+                    for (Map.Entry<Number, long[]> entry : stats.estimatedTombstoneDropTime.getAsMap().entrySet())
                     {
-                        out.printf("%-10s:%10s%n",entry.getKey().intValue(), entry.getValue());
+                        out.printf("%-10s:%10s%n",entry.getKey().intValue(), entry.getValue()[0]);
                     }
                     printHistograms(stats, out);
                 }
@@ -112,6 +160,30 @@
                 {
                     out.printf("Estimated cardinality: %s%n", compaction.cardinalityEstimator.cardinality());
                 }
+                if (header != null)
+                {
+                    EncodingStats encodingStats = header.getEncodingStats();
+                    AbstractType<?> keyType = header.getKeyType();
+                    List<AbstractType<?>> clusteringTypes = header.getClusteringTypes();
+                    Map<ByteBuffer, AbstractType<?>> staticColumns = header.getStaticColumns();
+                    Map<String, String> statics = staticColumns.entrySet().stream()
+                                                               .collect(Collectors.toMap(
+                                                                e -> UTF8Type.instance.getString(e.getKey()),
+                                                                e -> e.getValue().toString()));
+                    Map<ByteBuffer, AbstractType<?>> regularColumns = header.getRegularColumns();
+                    Map<String, String> regulars = regularColumns.entrySet().stream()
+                                                                 .collect(Collectors.toMap(
+                                                                 e -> UTF8Type.instance.getString(e.getKey()),
+                                                                 e -> e.getValue().toString()));
+
+                    out.printf("EncodingStats minTTL: %s%n", encodingStats.minTTL);
+                    out.printf("EncodingStats minLocalDeletionTime: %s%n", encodingStats.minLocalDeletionTime);
+                    out.printf("EncodingStats minTimestamp: %s%n", encodingStats.minTimestamp);
+                    out.printf("KeyType: %s%n", keyType.toString());
+                    out.printf("ClusteringTypes: %s%n", clusteringTypes.toString());
+                    out.printf("StaticColumns: {%s}%n", FBUtilities.toString(statics));
+                    out.printf("RegularColumns: {%s}%n", FBUtilities.toString(regulars));
+                }
             }
             else
             {
@@ -144,4 +216,19 @@
                                       (i < ecch.length ? ecch[i] : "")));
         }
     }
+
+    private static void printMinMaxToken(Descriptor descriptor, IPartitioner partitioner, AbstractType<?> keyType, PrintStream out) throws IOException
+    {
+        File summariesFile = new File(descriptor.filenameFor(Component.SUMMARY));
+        if (!summariesFile.exists())
+            return;
+
+        try (DataInputStream iStream = new DataInputStream(new FileInputStream(summariesFile)))
+        {
+            Pair<DecoratedKey, DecoratedKey> firstLast = new IndexSummary.IndexSummarySerializer().deserializeFirstLastKey(iStream, partitioner, descriptor.version.hasSamplingLevel());
+            out.printf("First token: %s (key=%s)%n", firstLast.left.getToken(), keyType.getString(firstLast.left.getKey()));
+            out.printf("Last token: %s (key=%s)%n", firstLast.right.getToken(), keyType.getString(firstLast.right.getKey()));
+        }
+    }
+
 }
diff --git a/src/java/org/apache/cassandra/tools/SSTableOfflineRelevel.java b/src/java/org/apache/cassandra/tools/SSTableOfflineRelevel.java
index e5eb187..ae0242c 100644
--- a/src/java/org/apache/cassandra/tools/SSTableOfflineRelevel.java
+++ b/src/java/org/apache/cassandra/tools/SSTableOfflineRelevel.java
@@ -17,18 +17,22 @@
  */
 package org.apache.cassandra.tools;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 import com.google.common.base.Throwables;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.SetMultimap;
 
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
@@ -97,10 +101,15 @@
         // remove any leftovers in the transaction log
         Keyspace ks = Keyspace.openWithoutSSTables(keyspace);
         ColumnFamilyStore cfs = ks.getColumnFamilyStore(columnfamily);
-        LifecycleTransaction.removeUnfinishedLeftovers(cfs.metadata);
+        if (!LifecycleTransaction.removeUnfinishedLeftovers(cfs))
+        {
+            throw new RuntimeException(String.format("Cannot remove temporary or obsoleted files for %s.%s " +
+                                                     "due to a problem with transaction log files.",
+                                                     keyspace, columnfamily));
+        }
 
         Directories.SSTableLister lister = cfs.getDirectories().sstableLister(Directories.OnTxnErr.THROW).skipTemporary(true);
-        Set<SSTableReader> sstables = new HashSet<>();
+        SetMultimap<File, SSTableReader> sstableMultimap = HashMultimap.create();
         for (Map.Entry<Descriptor, Set<Component>> sstable : lister.list().entrySet())
         {
             if (sstable.getKey() != null)
@@ -108,7 +117,7 @@
                 try
                 {
                     SSTableReader reader = SSTableReader.open(sstable.getKey());
-                    sstables.add(reader);
+                    sstableMultimap.put(reader.descriptor.directory, reader);
                 }
                 catch (Throwable t)
                 {
@@ -117,13 +126,20 @@
                 }
             }
         }
-        if (sstables.isEmpty())
+        if (sstableMultimap.isEmpty())
         {
             out.println("No sstables to relevel for "+keyspace+"."+columnfamily);
             System.exit(1);
         }
-        Relevel rl = new Relevel(sstables);
-        rl.relevel(dryRun);
+        for (File directory : sstableMultimap.keySet())
+        {
+            if (!sstableMultimap.get(directory).isEmpty())
+            {
+                Relevel rl = new Relevel(sstableMultimap.get(directory));
+                out.println("For sstables in " + directory + ":");
+                rl.relevel(dryRun);
+            }
+        }
         System.exit(0);
 
     }
@@ -138,8 +154,23 @@
             approxExpectedLevels = (int) Math.ceil(Math.log10(sstables.size()));
         }
 
+        private void printLeveling(Iterable<SSTableReader> sstables)
+        {
+            Multimap<Integer, SSTableReader> leveling = ArrayListMultimap.create();
+            int maxLevel = 0;
+            for (SSTableReader sstable : sstables)
+            {
+                leveling.put(sstable.getSSTableLevel(), sstable);
+                maxLevel = Math.max(sstable.getSSTableLevel(), maxLevel);
+            }
+            System.out.println("Current leveling:");
+            for (int i = 0; i <= maxLevel; i++)
+                System.out.println(String.format("L%d=%d", i, leveling.get(i).size()));
+        }
+
         public void relevel(boolean dryRun) throws IOException
         {
+            printLeveling(sstables);
             List<SSTableReader> sortedSSTables = new ArrayList<>(sstables);
             Collections.sort(sortedSSTables, new Comparator<SSTableReader>()
             {
@@ -182,8 +213,9 @@
                 System.out.println("New leveling: ");
 
             System.out.println("L0="+l0.size());
+            // item 0 in levels is the highest level we will create, printing from L1 up here:
             for (int i = levels.size() - 1; i >= 0; i--)
-                System.out.println(String.format("L%d %d", levels.size() - i, levels.get(i).size()));
+                System.out.println(String.format("L%d=%d", levels.size() - i, levels.get(i).size()));
 
             if (!dryRun)
             {
diff --git a/src/java/org/apache/cassandra/tools/SSTableRepairedAtSetter.java b/src/java/org/apache/cassandra/tools/SSTableRepairedAtSetter.java
index 3608808..413ec4d 100644
--- a/src/java/org/apache/cassandra/tools/SSTableRepairedAtSetter.java
+++ b/src/java/org/apache/cassandra/tools/SSTableRepairedAtSetter.java
@@ -26,6 +26,7 @@
 import java.util.List;
 
 import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.service.ActiveRepairService;
@@ -48,9 +49,6 @@
      */
     public static void main(final String[] args) throws IOException
     {
-        // Necessary since BufferPool used in RandomAccessReader needs to access DatabaseDescriptor
-        Config.setClientMode(true);
-
         PrintStream out = System.out;
         if (args.length == 0)
         {
diff --git a/src/java/org/apache/cassandra/tools/StandaloneSSTableUtil.java b/src/java/org/apache/cassandra/tools/StandaloneSSTableUtil.java
index 7aa07d0..2e8ee0b 100644
--- a/src/java/org/apache/cassandra/tools/StandaloneSSTableUtil.java
+++ b/src/java/org/apache/cassandra/tools/StandaloneSSTableUtil.java
@@ -19,6 +19,7 @@
 package org.apache.cassandra.tools;
 
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Directories;
@@ -48,6 +49,7 @@
         try
         {
             // load keyspace descriptions.
+            Util.initDatabaseDescriptor();
             Schema.instance.loadFromDisk(false);
 
             CFMetaData metadata = Schema.instance.getCFMetaData(options.keyspaceName, options.cfName);
diff --git a/src/java/org/apache/cassandra/tools/StandaloneScrubber.java b/src/java/org/apache/cassandra/tools/StandaloneScrubber.java
index 4778d72..2a4f293 100644
--- a/src/java/org/apache/cassandra/tools/StandaloneScrubber.java
+++ b/src/java/org/apache/cassandra/tools/StandaloneScrubber.java
@@ -1,4 +1,4 @@
-/**
+/*
  * 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
@@ -19,6 +19,7 @@
 package org.apache.cassandra.tools;
 
 import java.io.File;
+import java.nio.file.Paths;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 
@@ -38,6 +39,7 @@
 import org.apache.cassandra.io.sstable.*;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.OutputHandler;
+import org.apache.cassandra.utils.Pair;
 
 import static org.apache.cassandra.tools.BulkLoader.CmdLineOptions;
 
@@ -57,6 +59,7 @@
     private static final String SKIP_CORRUPTED_OPTION = "skip-corrupted";
     private static final String NO_VALIDATE_OPTION = "no-validate";
     private static final String REINSERT_OVERFLOWED_TTL_OPTION = "reinsert-overflowed-ttl";
+    private static final String HEADERFIX_OPTION = "header-fix";
 
     public static void main(String args[])
     {
@@ -93,34 +96,106 @@
 
             OutputHandler handler = new OutputHandler.SystemOutput(options.verbose, options.debug);
             Directories.SSTableLister lister = cfs.getDirectories().sstableLister(Directories.OnTxnErr.THROW).skipTemporary(true);
+            List<Pair<Descriptor, Set<Component>>> listResult = new ArrayList<>();
+
+            // create snapshot
+            for (Map.Entry<Descriptor, Set<Component>> entry : lister.list().entrySet())
+            {
+                Descriptor descriptor = entry.getKey();
+                Set<Component> components = entry.getValue();
+                if (!components.contains(Component.DATA))
+                    continue;
+
+                listResult.add(Pair.create(descriptor, components));
+
+                File snapshotDirectory = Directories.getSnapshotDirectory(descriptor, snapshotName);
+                SSTableReader.createLinks(descriptor, components, snapshotDirectory.getPath());
+            }
+            System.out.println(String.format("Pre-scrub sstables snapshotted into snapshot %s", snapshotName));
+
+            if (options.headerFixMode != Options.HeaderFixMode.OFF)
+            {
+                // Run the frozen-UDT checks _before_ the sstables are opened
+
+                List<String> logOutput = new ArrayList<>();
+
+                SSTableHeaderFix.Builder headerFixBuilder = SSTableHeaderFix.builder()
+                                                                            .logToList(logOutput)
+                                                                            .schemaCallback(() -> Schema.instance::getCFMetaData);
+                if (options.headerFixMode == Options.HeaderFixMode.VALIDATE)
+                    headerFixBuilder = headerFixBuilder.dryRun();
+
+                for (Pair<Descriptor, Set<Component>> p : listResult)
+                    headerFixBuilder.withPath(Paths.get(p.left.filenameFor(Component.DATA)));
+
+                SSTableHeaderFix headerFix = headerFixBuilder.build();
+                try
+                {
+                    headerFix.execute();
+                }
+                catch (Exception e)
+                {
+                    JVMStabilityInspector.inspectThrowable(e);
+                    if (options.debug)
+                        e.printStackTrace(System.err);
+                }
+
+                if (headerFix.hasChanges() || headerFix.hasError())
+                    logOutput.forEach(System.out::println);
+
+                if (headerFix.hasError())
+                {
+                    System.err.println("Errors in serialization-header detected, aborting.");
+                    System.exit(1);
+                }
+
+                switch (options.headerFixMode)
+                {
+                    case VALIDATE_ONLY:
+                    case FIX_ONLY:
+                        System.out.printf("Not continuing with scrub, since '--%s %s' was specified.%n",
+                                          HEADERFIX_OPTION,
+                                          options.headerFixMode.asCommandLineOption());
+                        System.exit(0);
+                    case VALIDATE:
+                        if (headerFix.hasChanges())
+                        {
+                            System.err.printf("Unfixed, but fixable errors in serialization-header detected, aborting. " +
+                                              "Use a non-validating mode ('-e %s' or '-e %s') for --%s%n",
+                                              Options.HeaderFixMode.FIX.asCommandLineOption(),
+                                              Options.HeaderFixMode.FIX_ONLY.asCommandLineOption(),
+                                              HEADERFIX_OPTION);
+                            System.exit(2);
+                        }
+                        break;
+                    case FIX:
+                        break;
+                }
+            }
 
             List<SSTableReader> sstables = new ArrayList<>();
 
-            // Scrub sstables
-            for (Map.Entry<Descriptor, Set<Component>> entry : lister.list().entrySet())
+            // Open sstables
+            for (Pair<Descriptor, Set<Component>> pair : listResult)
             {
-                Set<Component> components = entry.getValue();
+                Descriptor descriptor = pair.left;
+                Set<Component> components = pair.right;
                 if (!components.contains(Component.DATA))
                     continue;
 
                 try
                 {
-                    SSTableReader sstable = SSTableReader.openNoValidation(entry.getKey(), components, cfs);
+                    SSTableReader sstable = SSTableReader.openNoValidation(descriptor, components, cfs);
                     sstables.add(sstable);
-
-                    File snapshotDirectory = Directories.getSnapshotDirectory(sstable.descriptor, snapshotName);
-                    sstable.createLinks(snapshotDirectory.getPath());
-
                 }
                 catch (Exception e)
                 {
                     JVMStabilityInspector.inspectThrowable(e);
-                    System.err.println(String.format("Error Loading %s: %s", entry.getKey(), e.getMessage()));
+                    System.err.println(String.format("Error Loading %s: %s", descriptor, e.getMessage()));
                     if (options.debug)
                         e.printStackTrace(System.err);
                 }
             }
-            System.out.println(String.format("Pre-scrub sstables snapshotted into snapshot %s", snapshotName));
 
             if (!options.manifestCheckOnly)
             {
@@ -167,32 +242,22 @@
 
     private static void checkManifest(CompactionStrategyManager strategyManager, ColumnFamilyStore cfs, Collection<SSTableReader> sstables)
     {
-        int maxSizeInMB = (int)((cfs.getCompactionStrategyManager().getMaxSSTableBytes()) / (1024L * 1024L));
-        if (strategyManager.getStrategies().size() == 2 && strategyManager.getStrategies().get(0) instanceof LeveledCompactionStrategy)
+        if (strategyManager.getCompactionParams().klass().equals(LeveledCompactionStrategy.class))
         {
-            System.out.println("Checking leveled manifest");
-            Predicate<SSTableReader> repairedPredicate = new Predicate<SSTableReader>()
-            {
-                @Override
-                public boolean apply(SSTableReader sstable)
-                {
-                    return sstable.isRepaired();
-                }
-            };
+            int maxSizeInMB = (int)((cfs.getCompactionStrategyManager().getMaxSSTableBytes()) / (1024L * 1024L));
+            int fanOut = cfs.getCompactionStrategyManager().getLevelFanoutSize();
+            List<SSTableReader> repaired = new ArrayList<>();
+            List<SSTableReader> unrepaired = new ArrayList<>();
 
-            List<SSTableReader> repaired = Lists.newArrayList(Iterables.filter(sstables, repairedPredicate));
-            List<SSTableReader> unRepaired = Lists.newArrayList(Iterables.filter(sstables, Predicates.not(repairedPredicate)));
-
-            LeveledManifest repairedManifest = LeveledManifest.create(cfs, maxSizeInMB, repaired);
-            for (int i = 1; i < repairedManifest.getLevelCount(); i++)
+            for (SSTableReader sstable : sstables)
             {
-                repairedManifest.repairOverlappingSSTables(i);
+                if (sstable.isRepaired())
+                    repaired.add(sstable);
+                else
+                    unrepaired.add(sstable);
             }
-            LeveledManifest unRepairedManifest = LeveledManifest.create(cfs, maxSizeInMB, unRepaired);
-            for (int i = 1; i < unRepairedManifest.getLevelCount(); i++)
-            {
-                unRepairedManifest.repairOverlappingSSTables(i);
-            }
+            LeveledManifest.create(cfs, maxSizeInMB, fanOut, repaired);
+            LeveledManifest.create(cfs, maxSizeInMB, fanOut, unrepaired);
         }
     }
 
@@ -207,6 +272,26 @@
         public boolean skipCorrupted;
         public boolean noValidate;
         public boolean reinserOverflowedTTL;
+        public HeaderFixMode headerFixMode = HeaderFixMode.VALIDATE;
+
+        enum HeaderFixMode
+        {
+            VALIDATE_ONLY,
+            VALIDATE,
+            FIX_ONLY,
+            FIX,
+            OFF;
+
+            static HeaderFixMode fromCommandLine(String value)
+            {
+                return valueOf(value.replace('-', '_').toUpperCase().trim());
+            }
+
+            String asCommandLineOption()
+            {
+                return name().toLowerCase().replace('_', '-');
+            }
+        }
 
         private Options(String keyspaceName, String cfName)
         {
@@ -248,7 +333,18 @@
                 opts.skipCorrupted = cmd.hasOption(SKIP_CORRUPTED_OPTION);
                 opts.noValidate = cmd.hasOption(NO_VALIDATE_OPTION);
                 opts.reinserOverflowedTTL = cmd.hasOption(REINSERT_OVERFLOWED_TTL_OPTION);
-
+                if (cmd.hasOption(HEADERFIX_OPTION))
+                {
+                    try
+                    {
+                        opts.headerFixMode = HeaderFixMode.fromCommandLine(cmd.getOptionValue(HEADERFIX_OPTION));
+                    }
+                    catch (Exception e)
+                    {
+                        errorMsg(String.format("Invalid argument value '%s' for --%s", cmd.getOptionValue(HEADERFIX_OPTION), HEADERFIX_OPTION), options);
+                        return null;
+                    }
+                }
                 return opts;
             }
             catch (ParseException e)
@@ -274,6 +370,22 @@
             options.addOption("m",  MANIFEST_CHECK_OPTION, "only check and repair the leveled manifest, without actually scrubbing the sstables");
             options.addOption("s",  SKIP_CORRUPTED_OPTION, "skip corrupt rows in counter tables");
             options.addOption("n",  NO_VALIDATE_OPTION,    "do not validate columns using column validator");
+            options.addOption("e",  HEADERFIX_OPTION,      true, "Option whether and how to perform a " +
+                                                                 "check of the sstable serialization-headers and fix known, " +
+                                                                 "fixable issues.\n" +
+                                                                 "Possible argument values:\n" +
+                                                                 "- validate-only: validate the serialization-headers, " +
+                                                                 "but do not fix those. Do not continue with scrub - " +
+                                                                 "i.e. only validate the header (dry-run of fix-only).\n" +
+                                                                 "- validate: (default) validate the serialization-headers, " +
+                                                                 "but do not fix those and only continue with scrub if no " +
+                                                                 "error were detected.\n" +
+                                                                 "- fix-only: validate and fix the serialization-headers, " +
+                                                                 "don't continue with scrub.\n" +
+                                                                 "- fix: validate and fix the serialization-headers, do not " +
+                                                                 "fix and do not continue with scrub if the serialization-header " +
+                                                                 "check encountered errors.\n" +
+                                                                 "- off: don't perform the serialization-header checks.");
             options.addOption("r", REINSERT_OVERFLOWED_TTL_OPTION, REINSERT_OVERFLOWED_TTL_OPTION_DESCRIPTION);
             return options;
         }
@@ -286,7 +398,7 @@
             header.append("Scrub the sstable for the provided table." );
             header.append("\n--\n");
             header.append("Options are:");
-            new HelpFormatter().printHelp(usage, header.toString(), options, "");
+            new HelpFormatter().printHelp(120, usage, header.toString(), options, "");
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/StandaloneSplitter.java b/src/java/org/apache/cassandra/tools/StandaloneSplitter.java
index 57504c3..1e57ff4 100644
--- a/src/java/org/apache/cassandra/tools/StandaloneSplitter.java
+++ b/src/java/org/apache/cassandra/tools/StandaloneSplitter.java
@@ -1,4 +1,4 @@
-/**
+/*
  * 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
diff --git a/src/java/org/apache/cassandra/tools/StandaloneUpgrader.java b/src/java/org/apache/cassandra/tools/StandaloneUpgrader.java
index 50f7331..3185793 100644
--- a/src/java/org/apache/cassandra/tools/StandaloneUpgrader.java
+++ b/src/java/org/apache/cassandra/tools/StandaloneUpgrader.java
@@ -22,7 +22,6 @@
 
 import org.apache.commons.cli.*;
 
-import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Directories;
@@ -32,6 +31,7 @@
 import org.apache.cassandra.db.compaction.Upgrader;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
 import org.apache.cassandra.io.sstable.*;
+import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.OutputHandler;
@@ -82,7 +82,7 @@
                 try
                 {
                     SSTableReader sstable = SSTableReader.openNoValidation(entry.getKey(), components, cfs);
-                    if (sstable.descriptor.version.equals(DatabaseDescriptor.getSSTableFormat().info.getLatestVersion()))
+                    if (sstable.descriptor.version.equals(SSTableFormat.Type.current().info.getLatestVersion()))
                     {
                         sstable.selfRef().release();
                         continue;
diff --git a/src/java/org/apache/cassandra/tools/StandaloneVerifier.java b/src/java/org/apache/cassandra/tools/StandaloneVerifier.java
index a8e72bd..5690553 100644
--- a/src/java/org/apache/cassandra/tools/StandaloneVerifier.java
+++ b/src/java/org/apache/cassandra/tools/StandaloneVerifier.java
@@ -1,4 +1,4 @@
-/**
+/*
  * 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
diff --git a/src/java/org/apache/cassandra/tools/Util.java b/src/java/org/apache/cassandra/tools/Util.java
index 6e23361..76011a9 100644
--- a/src/java/org/apache/cassandra/tools/Util.java
+++ b/src/java/org/apache/cassandra/tools/Util.java
@@ -35,22 +35,21 @@
     {
         try
         {
-            DatabaseDescriptor.forceStaticInitialization();
+            DatabaseDescriptor.toolInitialization();
         }
-        catch (ExceptionInInitializerError e)
+        catch (Throwable e)
         {
-            Throwable cause = e.getCause();
-            boolean logStackTrace = !(cause instanceof ConfigurationException) || ((ConfigurationException) cause).logStackTrace;
-            System.out.println("Exception (" + cause.getClass().getName() + ") encountered during startup: " + cause.getMessage());
+            boolean logStackTrace = !(e instanceof ConfigurationException) || ((ConfigurationException) e).logStackTrace;
+            System.out.println("Exception (" + e.getClass().getName() + ") encountered during startup: " + e.getMessage());
 
             if (logStackTrace)
             {
-                cause.printStackTrace();
+                e.printStackTrace();
                 System.exit(3);
             }
             else
             {
-                System.err.println(cause.getMessage());
+                System.err.println(e.getMessage());
                 System.exit(3);
             }
         }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Cleanup.java b/src/java/org/apache/cassandra/tools/nodetool/Cleanup.java
index e60d87e..0c09db9 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Cleanup.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Cleanup.java
@@ -23,8 +23,8 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import org.apache.cassandra.config.Schema;
 import io.airlift.command.Option;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
 
@@ -47,7 +47,7 @@
 
         for (String keyspace : keyspaces)
         {
-            if (Schema.isLocalSystemKeyspace(keyspace))
+            if (SchemaConstants.isLocalSystemKeyspace(keyspace))
                 continue;
 
             try
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Compact.java b/src/java/org/apache/cassandra/tools/nodetool/Compact.java
index 002541d..d473190 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Compact.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Compact.java
@@ -17,6 +17,8 @@
  */
 package org.apache.cassandra.tools.nodetool;
 
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+
 import io.airlift.command.Arguments;
 import io.airlift.command.Command;
 import io.airlift.command.Option;
@@ -27,18 +29,50 @@
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
 
-@Command(name = "compact", description = "Force a (major) compaction on one or more tables")
+@Command(name = "compact", description = "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables")
 public class Compact extends NodeToolCmd
 {
-    @Arguments(usage = "[<keyspace> <tables>...]", description = "The keyspace followed by one or many tables")
+    @Arguments(usage = "[<keyspace> <tables>...] or <SSTable file>...", description = "The keyspace followed by one or many tables or list of SSTable data files when using --user-defined")
     private List<String> args = new ArrayList<>();
 
     @Option(title = "split_output", name = {"-s", "--split-output"}, description = "Use -s to not create a single big file")
     private boolean splitOutput = false;
 
+    @Option(title = "user-defined", name = {"--user-defined"}, description = "Use --user-defined to submit listed files for user-defined compaction")
+    private boolean userDefined = false;
+
+    @Option(title = "start_token", name = {"-st", "--start-token"}, description = "Use -st to specify a token at which the compaction range starts (inclusive)")
+    private String startToken = EMPTY;
+
+    @Option(title = "end_token", name = {"-et", "--end-token"}, description = "Use -et to specify a token at which compaction range ends (inclusive)")
+    private String endToken = EMPTY;
+
+
     @Override
     public void execute(NodeProbe probe)
     {
+        final boolean tokenProvided = !(startToken.isEmpty() && endToken.isEmpty());
+        if (splitOutput && (userDefined || tokenProvided))
+        {
+            throw new RuntimeException("Invalid option combination: Can not use split-output here");
+        }
+        if (userDefined && tokenProvided)
+        {
+            throw new RuntimeException("Invalid option combination: Can not provide tokens when using user-defined");
+        }
+
+        if (userDefined)
+        {
+            try
+            {
+                String userDefinedFiles = String.join(",", args);
+                probe.forceUserDefinedCompaction(userDefinedFiles);
+            } catch (Exception e) {
+                throw new RuntimeException("Error occurred during user defined compaction", e);
+            }
+            return;
+        }
+
         List<String> keyspaces = parseOptionalKeyspace(args, probe);
         String[] tableNames = parseOptionalTables(args);
 
@@ -46,7 +80,14 @@
         {
             try
             {
-                probe.forceKeyspaceCompaction(splitOutput, keyspace, tableNames);
+                if (tokenProvided)
+                {
+                    probe.forceKeyspaceCompactionForTokenRange(keyspace, startToken, endToken, tableNames);
+                }
+                else
+                {
+                    probe.forceKeyspaceCompaction(splitOutput, keyspace, tableNames);
+                }
             } catch (Exception e)
             {
                 throw new RuntimeException("Error occurred during compaction", e);
diff --git a/src/java/org/apache/cassandra/tools/nodetool/CompactionHistory.java b/src/java/org/apache/cassandra/tools/nodetool/CompactionHistory.java
index 4624335..fa2ccb4 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/CompactionHistory.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/CompactionHistory.java
@@ -17,108 +17,32 @@
  */
 package org.apache.cassandra.tools.nodetool;
 
-import java.io.PrintStream;
-import java.time.Instant;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-import javax.management.openmbean.TabularData;
-
 import io.airlift.command.Command;
+import io.airlift.command.Option;
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
-import org.apache.cassandra.tools.nodetool.formatter.TableBuilder;
-
-import static com.google.common.collect.Iterables.toArray;
+import org.apache.cassandra.tools.nodetool.stats.CompactionHistoryHolder;
+import org.apache.cassandra.tools.nodetool.stats.CompactionHistoryPrinter;
+import org.apache.cassandra.tools.nodetool.stats.StatsHolder;
+import org.apache.cassandra.tools.nodetool.stats.StatsPrinter;
 
 @Command(name = "compactionhistory", description = "Print history of compaction")
 public class CompactionHistory extends NodeToolCmd
 {
+    @Option(title = "format",
+            name = {"-F", "--format"},
+            description = "Output format (json, yaml)")
+    private String outputFormat = "";
+
     @Override
     public void execute(NodeProbe probe)
     {
-        PrintStream out = probe.output().out;
-        out.println("Compaction History: ");
-
-        TabularData tabularData = probe.getCompactionHistory();
-        if (tabularData.isEmpty())
+        if (!outputFormat.isEmpty() && !"json".equals(outputFormat) && !"yaml".equals(outputFormat))
         {
-            out.printf("There is no compaction history");
-            return;
+            throw new IllegalArgumentException("arguments for -F are json,yaml only.");
         }
-
-        TableBuilder table = new TableBuilder();
-        List<String> indexNames = tabularData.getTabularType().getIndexNames();
-        table.add(toArray(indexNames, String.class));
-
-        Set<?> values = tabularData.keySet();
-        List<CompactionHistoryRow> chr = new ArrayList<>();
-        for (Object eachValue : values)
-        {
-            List<?> value = (List<?>) eachValue;
-            CompactionHistoryRow chc = new CompactionHistoryRow((String)value.get(0),
-                                                                (String)value.get(1),
-                                                                (String)value.get(2),
-                                                                (Long)value.get(3),
-                                                                (Long)value.get(4),
-                                                                (Long)value.get(5),
-                                                                (String)value.get(6));
-            chr.add(chc);
-        }
-        Collections.sort(chr);
-        for (CompactionHistoryRow eachChc : chr)
-        {
-            table.add(eachChc.getAllAsArray());
-        }
-        table.printTo(out);
-    }
-
-    /**
-     * Allows the Compaction History output to be ordered by 'compactedAt' - that is the
-     * time at which compaction finished.
-     */
-    private static class CompactionHistoryRow implements Comparable<CompactionHistoryRow>
-    {
-        private final String id;
-        private final String ksName;
-        private final String cfName;
-        private final long compactedAt;
-        private final long bytesIn;
-        private final long bytesOut;
-        private final String rowMerged;
-
-        CompactionHistoryRow(String id, String ksName, String cfName, long compactedAt, long bytesIn, long bytesOut, String rowMerged)
-        {
-            this.id = id;
-            this.ksName = ksName;
-            this.cfName = cfName;
-            this.compactedAt = compactedAt;
-            this.bytesIn = bytesIn;
-            this.bytesOut = bytesOut;
-            this.rowMerged = rowMerged;
-        }
-
-        public int compareTo(CompactionHistoryRow chc)
-        {
-            return Long.signum(chc.compactedAt - this.compactedAt);
-        }
-
-        public String[] getAllAsArray()
-        {
-            String[] obj = new String[7];
-            obj[0] = this.id;
-            obj[1] = this.ksName;
-            obj[2] = this.cfName;
-            Instant instant = Instant.ofEpochMilli(this.compactedAt);
-            LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
-            obj[3] = ldt.toString();
-            obj[4] = Long.toString(this.bytesIn);
-            obj[5] = Long.toString(this.bytesOut);
-            obj[6] = this.rowMerged;
-            return obj;
-        }
+        StatsHolder data = new CompactionHistoryHolder(probe);
+        StatsPrinter printer = CompactionHistoryPrinter.from(outputFormat);
+        printer.print(data, probe.output().out);
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/CompactionStats.java b/src/java/org/apache/cassandra/tools/nodetool/CompactionStats.java
index e68b534..3deea3a 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/CompactionStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/CompactionStats.java
@@ -21,6 +21,7 @@
 import java.text.DecimalFormat;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 
 import io.airlift.command.Command;
 import io.airlift.command.Option;
@@ -38,7 +39,7 @@
 {
     @Option(title = "human_readable",
             name = {"-H", "--human-readable"},
-            description = "Display bytes in human readable form, i.e. KB, MB, GB, TB")
+            description = "Display bytes in human readable form, i.e. KiB, MiB, GiB, TiB")
     private boolean humanReadable = false;
 
     @Override
@@ -46,13 +47,37 @@
     {
         PrintStream out = probe.output().out;
         CompactionManagerMBean cm = probe.getCompactionManagerProxy();
-        out.println("pending tasks: " + probe.getCompactionMetric("PendingTasks"));
-        long remainingBytes = 0;
-        TableBuilder table = new TableBuilder();
-        List<Map<String, String>> compactions = cm.getCompactions();
+        Map<String, Map<String, Integer>> pendingTaskNumberByTable =
+            (Map<String, Map<String, Integer>>) probe.getCompactionMetric("PendingTasksByTableName");
+        int numTotalPendingTask = 0;
+        for (Entry<String, Map<String, Integer>> ksEntry : pendingTaskNumberByTable.entrySet())
+        {
+            for (Entry<String, Integer> tableEntry : ksEntry.getValue().entrySet())
+                numTotalPendingTask += tableEntry.getValue();
+        }
+        out.println("pending tasks: " + numTotalPendingTask);
+        for (Entry<String, Map<String, Integer>> ksEntry : pendingTaskNumberByTable.entrySet())
+        {
+            String ksName = ksEntry.getKey();
+            for (Entry<String, Integer> tableEntry : ksEntry.getValue().entrySet())
+            {
+                String tableName = tableEntry.getKey();
+                int pendingTaskCount = tableEntry.getValue();
+
+                out.println("- " + ksName + '.' + tableName + ": " + pendingTaskCount);
+            }
+        }
+        out.println();
+        reportCompactionTable(cm.getCompactions(), probe.getCompactionThroughput(), humanReadable, out);
+    }
+
+    public static void reportCompactionTable(List<Map<String,String>> compactions, int compactionThroughput, boolean humanReadable, PrintStream out)
+    {
         if (!compactions.isEmpty())
         {
-            int compactionThroughput = probe.getCompactionThroughput();
+            long remainingBytes = 0;
+            TableBuilder table = new TableBuilder();
+
             table.add("id", "compaction type", "keyspace", "table", "completed", "total", "unit", "progress");
             for (Map<String, String> c : compactions)
             {
@@ -70,7 +95,6 @@
                 table.add(id, taskType, keyspace, columnFamily, completedStr, totalStr, unit, percentComplete);
                 remainingBytes += total - completed;
             }
-
             table.printTo(out);
 
             String remainingTime = "n/a";
@@ -82,4 +106,5 @@
             out.printf("%25s%10s%n", "Active compaction remaining time : ", remainingTime);
         }
     }
+
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/DescribeCluster.java b/src/java/org/apache/cassandra/tools/nodetool/DescribeCluster.java
index 8cab62e..224a470 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/DescribeCluster.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/DescribeCluster.java
@@ -24,6 +24,7 @@
 import java.util.List;
 import java.util.Map;
 
+import org.apache.cassandra.locator.DynamicEndpointSnitch;
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
 
@@ -37,7 +38,15 @@
         // display cluster name, snitch and partitioner
         out.println("Cluster Information:");
         out.println("\tName: " + probe.getClusterName());
-        out.println("\tSnitch: " + probe.getEndpointSnitchInfoProxy().getSnitchName());
+        String snitch = probe.getEndpointSnitchInfoProxy().getSnitchName();
+        boolean dynamicSnitchEnabled = false;
+        if (snitch.equals(DynamicEndpointSnitch.class.getName()))
+        {
+            snitch = probe.getDynamicEndpointSnitchInfoProxy().getSubsnitchClassName();
+            dynamicSnitchEnabled = true;
+        }
+        out.println("\tSnitch: " + snitch);
+        out.println("\tDynamicEndPointSnitch: " + (dynamicSnitchEnabled ? "enabled" : "disabled"));
         out.println("\tPartitioner: " + probe.getPartitioner());
 
         // display schema version for each node
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GarbageCollect.java b/src/java/org/apache/cassandra/tools/nodetool/GarbageCollect.java
new file mode 100644
index 0000000..9a07343
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/GarbageCollect.java
@@ -0,0 +1,66 @@
+/*
+ * 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.tools.nodetool;
+
+import io.airlift.command.Arguments;
+import io.airlift.command.Command;
+import io.airlift.command.Option;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+
+@Command(name = "garbagecollect", description = "Remove deleted data from one or more tables")
+public class GarbageCollect extends NodeToolCmd
+{
+    @Arguments(usage = "[<keyspace> <tables>...]", description = "The keyspace followed by one or many tables")
+    private List<String> args = new ArrayList<>();
+
+    @Option(title = "granularity",
+        name = {"-g", "--granularity"},
+        allowedValues = {"ROW", "CELL"},
+        description = "Granularity of garbage removal. ROW (default) removes deleted partitions and rows, CELL also removes overwritten or deleted cells.")
+    private String tombstoneOption = "ROW";
+
+    @Option(title = "jobs",
+            name = {"-j", "--jobs"},
+            description = "Number of sstables to cleanup simultanously, set to 0 to use all available compaction " +
+                          "threads. Defaults to 1 so that collections of newer tables can see the data is deleted " +
+                          "and also remove tombstones.")
+    private int jobs = 1;
+
+    @Override
+    public void execute(NodeProbe probe)
+    {
+        List<String> keyspaces = parseOptionalKeyspace(args, probe);
+        String[] tableNames = parseOptionalTables(args);
+
+        for (String keyspace : keyspaces)
+        {
+            try
+            {
+                probe.garbageCollect(probe.output().out, tombstoneOption, jobs, keyspace, tableNames);
+            } catch (Exception e)
+            {
+                throw new RuntimeException("Error occurred during garbage collection", e);
+            }
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetConcurrentCompactors.java b/src/java/org/apache/cassandra/tools/nodetool/GetConcurrentCompactors.java
new file mode 100644
index 0000000..2f8232d
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetConcurrentCompactors.java
@@ -0,0 +1,33 @@
+/*
+ * 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.tools.nodetool;
+
+import io.airlift.command.Command;
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+
+@Command(name = "getconcurrentcompactors", description = "Get the number of concurrent compactors in the system.")
+public class GetConcurrentCompactors extends NodeToolCmd
+{
+    protected void execute(NodeProbe probe)
+    {
+        probe.output().out.println("Current concurrent compactors in the system is: \n" +
+                           probe.getConcurrentCompactors());
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetSSTables.java b/src/java/org/apache/cassandra/tools/nodetool/GetSSTables.java
index 361909a..ca65071 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetSSTables.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetSSTables.java
@@ -24,13 +24,19 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import io.airlift.command.Option;
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
 
 @Command(name = "getsstables", description = "Print the sstable filenames that own the key")
 public class GetSSTables extends NodeToolCmd
 {
-    @Arguments(usage = "<keyspace> <table> <key>", description = "The keyspace, the table, and the key")
+    @Option(title = "hex_format",
+           name = {"-hf", "--hex-format"},
+           description = "Specify the key in hexadecimal string format")
+    private boolean hexFormat = false;
+
+    @Arguments(usage = "<keyspace> <cfname> <key>", description = "The keyspace, the column family, and the key")
     private List<String> args = new ArrayList<>();
 
     @Override
@@ -41,7 +47,7 @@
         String cf = args.get(1);
         String key = args.get(2);
 
-        List<String> sstables = probe.getSSTables(ks, cf, key);
+        List<String> sstables = probe.getSSTables(ks, cf, key, hexFormat);
         for (String sstable : sstables)
         {
             probe.output().out.println(sstable);
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetTimeout.java b/src/java/org/apache/cassandra/tools/nodetool/GetTimeout.java
new file mode 100644
index 0000000..4dd42e2
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetTimeout.java
@@ -0,0 +1,52 @@
+/*
+ * 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.tools.nodetool;
+
+import io.airlift.command.Arguments;
+import io.airlift.command.Command;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+@Command(name = "gettimeout", description = "Print the timeout of the given type in ms")
+public class GetTimeout extends NodeToolCmd
+{
+    public static final String TIMEOUT_TYPES = "read, range, write, counterwrite, cascontention, truncate, streamingsocket, misc (general rpc_timeout_in_ms)";
+
+    @Arguments(usage = "<timeout_type>", description = "The timeout type, one of (" + TIMEOUT_TYPES + ")")
+    private List<String> args = new ArrayList<>();
+
+    @Override
+    public void execute(NodeProbe probe)
+    {
+        checkArgument(args.size() == 1, "gettimeout requires a timeout type, one of (" + TIMEOUT_TYPES + ")");
+        try
+        {
+            probe.output().out.println("Current timeout for type " + args.get(0) + ": " + probe.getTimeout(args.get(0)) + " ms");
+        } catch (Exception e)
+        {
+            throw new IllegalArgumentException(e.getMessage());
+        }
+
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Info.java b/src/java/org/apache/cassandra/tools/nodetool/Info.java
index c2e0ff9..3d52c8e 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Info.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Info.java
@@ -44,7 +44,7 @@
     @Override
     public void execute(NodeProbe probe)
     {
-        boolean gossipInitialized = probe.isInitialized();
+        boolean gossipInitialized = probe.isGossipRunning();
 
         PrintStream out = probe.output().out;
         out.printf("%-23s: %s%n", "ID", probe.getLocalHostId());
@@ -120,6 +120,31 @@
                 probe.getCacheMetric("CounterCache", "HitRate"),
                 cacheService.getCounterCacheSavePeriodInSeconds());
 
+        // Chunk Cache: Hits, Requests, RecentHitRate, SavePeriodInSeconds
+        try
+        {
+            out.printf("%-23s: entries %d, size %s, capacity %s, %d misses, %d requests, %.3f recent hit rate, %.3f %s miss latency%n",
+                    "Chunk Cache",
+                    probe.getCacheMetric("ChunkCache", "Entries"),
+                    FileUtils.stringifyFileSize((long) probe.getCacheMetric("ChunkCache", "Size")),
+                    FileUtils.stringifyFileSize((long) probe.getCacheMetric("ChunkCache", "Capacity")),
+                    probe.getCacheMetric("ChunkCache", "Misses"),
+                    probe.getCacheMetric("ChunkCache", "Requests"),
+                    probe.getCacheMetric("ChunkCache", "HitRate"),
+                    probe.getCacheMetric("ChunkCache", "MissLatency"),
+                    probe.getCacheMetric("ChunkCache", "MissLatencyUnit"));
+        }
+        catch (RuntimeException e)
+        {
+            if (!(e.getCause() instanceof InstanceNotFoundException))
+                throw e;
+
+            // Chunk cache is not on.
+        }
+
+        // Global table stats
+        out.printf("%-23s: %s%%%n", "Percent Repaired", probe.getColumnFamilyMetric(null, null, "PercentRepaired"));
+
         // check if node is already joined, before getting tokens, since it throws exception if not.
         if (probe.isJoined())
         {
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ListSnapshots.java b/src/java/org/apache/cassandra/tools/nodetool/ListSnapshots.java
index 09e19dd..28a9c3b 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/ListSnapshots.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/ListSnapshots.java
@@ -48,7 +48,6 @@
             }
 
             final long trueSnapshotsSize = probe.trueSnapshotsSize();
-
             TableBuilder table = new TableBuilder();
             // display column names only once
             final List<String> indexNames = snapshotDetails.entrySet().iterator().next().getValue().getTabularType().getIndexNames();
diff --git a/src/java/org/apache/cassandra/tools/nodetool/NetStats.java b/src/java/org/apache/cassandra/tools/nodetool/NetStats.java
index 61db036..b333fef 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/NetStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/NetStats.java
@@ -36,7 +36,7 @@
 {
     @Option(title = "human_readable",
             name = {"-H", "--human-readable"},
-            description = "Display bytes in human readable form, i.e. KB, MB, GB, TB")
+            description = "Display bytes in human readable form, i.e. KiB, MiB, GiB, TiB")
     private boolean humanReadable = false;
 
     @Override
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ProxyHistograms.java b/src/java/org/apache/cassandra/tools/nodetool/ProxyHistograms.java
index 1a3fc42..5ba77a7 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/ProxyHistograms.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/ProxyHistograms.java
@@ -31,24 +31,30 @@
     @Override
     public void execute(NodeProbe probe)
     {
-        String[] percentiles = new String[]{"50%", "75%", "95%", "98%", "99%", "Min", "Max"};
+        PrintStream out = probe.output().out;
+        String[] percentiles = {"50%", "75%", "95%", "98%", "99%", "Min", "Max"};
         double[] readLatency = probe.metricPercentilesAsArray(probe.getProxyMetric("Read"));
         double[] writeLatency = probe.metricPercentilesAsArray(probe.getProxyMetric("Write"));
         double[] rangeLatency = probe.metricPercentilesAsArray(probe.getProxyMetric("RangeSlice"));
-        PrintStream out = probe.output().out;
+        double[] casReadLatency = probe.metricPercentilesAsArray(probe.getProxyMetric("CASRead"));
+        double[] casWriteLatency = probe.metricPercentilesAsArray(probe.getProxyMetric("CASWrite"));
+        double[] viewWriteLatency = probe.metricPercentilesAsArray(probe.getProxyMetric("ViewWrite"));
 
         out.println("proxy histograms");
-        out.println(format("%-10s%18s%18s%18s",
-                "Percentile", "Read Latency", "Write Latency", "Range Latency"));
-        out.println(format("%-10s%18s%18s%18s",
-                "", "(micros)", "(micros)", "(micros)"));
+        out.println(format("%-10s%19s%19s%19s%19s%19s%19s",
+                "Percentile", "Read Latency", "Write Latency", "Range Latency", "CAS Read Latency", "CAS Write Latency", "View Write Latency"));
+        out.println(format("%-10s%19s%19s%19s%19s%19s%19s",
+                "", "(micros)", "(micros)", "(micros)", "(micros)", "(micros)", "(micros)"));
         for (int i = 0; i < percentiles.length; i++)
         {
-            out.println(format("%-10s%18.2f%18.2f%18.2f",
-                    percentiles[i],
-                    readLatency[i],
-                    writeLatency[i],
-                    rangeLatency[i]));
+            out.println(format("%-10s%19.2f%19.2f%19.2f%19.2f%19.2f%19.2f",
+                               percentiles[i],
+                               readLatency[i],
+                               writeLatency[i],
+                               rangeLatency[i],
+                               casReadLatency[i],
+                               casWriteLatency[i],
+                               viewWriteLatency[i]));
         }
         out.println();
     }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Rebuild.java b/src/java/org/apache/cassandra/tools/nodetool/Rebuild.java
index 8a6dbf1..b27e674 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Rebuild.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Rebuild.java
@@ -19,6 +19,7 @@
 
 import io.airlift.command.Arguments;
 import io.airlift.command.Command;
+import io.airlift.command.Option;
 
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
@@ -26,12 +27,34 @@
 @Command(name = "rebuild", description = "Rebuild data by streaming from other nodes (similarly to bootstrap)")
 public class Rebuild extends NodeToolCmd
 {
-    @Arguments(usage = "<src-dc-name>", description = "Name of DC from which to select sources for streaming. By default, pick any DC")
+    @Arguments(usage = "<src-dc-name>",
+               description = "Name of DC from which to select sources for streaming. By default, pick any DC")
     private String sourceDataCenterName = null;
 
+    @Option(title = "specific_keyspace",
+            name = {"-ks", "--keyspace"},
+            description = "Use -ks to rebuild specific keyspace.")
+    private String keyspace = null;
+
+    @Option(title = "specific_tokens",
+            name = {"-ts", "--tokens"},
+            description = "Use -ts to rebuild specific token ranges, in the format of \"(start_token_1,end_token_1],(start_token_2,end_token_2],...(start_token_n,end_token_n]\".")
+    private String tokens = null;
+
+    @Option(title = "specific_sources",
+            name = {"-s", "--sources"},
+            description = "Use -s to specify hosts that this node should stream from when -ts is used. Multiple hosts should be separated using commas (e.g. 127.0.0.1,127.0.0.2,...)")
+    private String specificSources = null;
+
     @Override
     public void execute(NodeProbe probe)
     {
-        probe.rebuild(sourceDataCenterName);
+        // check the arguments
+        if (keyspace == null && tokens != null)
+        {
+            throw new IllegalArgumentException("Cannot specify tokens without keyspace.");
+        }
+
+        probe.rebuild(sourceDataCenterName, keyspace, tokens, specificSources);
     }
 }
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/tools/nodetool/RelocateSSTables.java b/src/java/org/apache/cassandra/tools/nodetool/RelocateSSTables.java
new file mode 100644
index 0000000..7c3066c
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/RelocateSSTables.java
@@ -0,0 +1,55 @@
+/*
+ * 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.tools.nodetool;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.airlift.command.Arguments;
+import io.airlift.command.Command;
+import io.airlift.command.Option;
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool;
+
+@Command(name = "relocatesstables", description = "Relocates sstables to the correct disk")
+public class RelocateSSTables extends NodeTool.NodeToolCmd
+{
+    @Arguments(usage = "<keyspace> <table>", description = "The keyspace and table name")
+    private List<String> args = new ArrayList<>();
+
+    @Option(title = "jobs",
+            name = {"-j", "--jobs"},
+            description = "Number of sstables to relocate simultanously, set to 0 to use all available compaction threads")
+    private int jobs = 2;
+
+    @Override
+    public void execute(NodeProbe probe)
+    {
+        List<String> keyspaces = parseOptionalKeyspace(args, probe);
+        String[] cfnames = parseOptionalTables(args);
+        try
+        {
+            for (String keyspace : keyspaces)
+                probe.relocateSSTables(jobs, keyspace, cfnames);
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException("Got error while relocating", e);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Repair.java b/src/java/org/apache/cassandra/tools/nodetool/Repair.java
index df1cb94..9132a1e 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Repair.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Repair.java
@@ -31,8 +31,8 @@
 
 import com.google.common.collect.Sets;
 
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.repair.RepairParallelism;
-import org.apache.cassandra.repair.SystemDistributedKeyspace;
 import org.apache.cassandra.repair.messages.RepairOption;
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
@@ -41,7 +41,7 @@
 @Command(name = "repair", description = "Repair one or more tables")
 public class Repair extends NodeToolCmd
 {
-    public final static Set<String> ONLY_EXPLICITLY_REPAIRED = Sets.newHashSet(SystemDistributedKeyspace.NAME);
+    public final static Set<String> ONLY_EXPLICITLY_REPAIRED = Sets.newHashSet(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME);
 
     @Arguments(usage = "[<keyspace> <tables>...]", description = "The keyspace followed by one or many tables")
     private List<String> args = new ArrayList<>();
@@ -81,6 +81,9 @@
     @Option(title = "trace_repair", name = {"-tr", "--trace"}, description = "Use -tr to trace the repair. Traces are logged to system_traces.events.")
     private boolean trace = false;
 
+    @Option(title = "pull_repair", name = {"-pl", "--pull"}, description = "Use --pull to perform a one way repair where data is only streamed from a remote node to this node.")
+    private boolean pullRepair = false;
+
     @Option(title = "ignore_unreplicated_keyspaces", name = {"-iuk","--ignore-unreplicated-keyspaces"}, description = "Use --ignore-unreplicated-keyspaces to ignore keyspaces which are not replicated, otherwise the repair will fail")
     private boolean ignoreUnreplicatedKeyspaces = false;
 
@@ -111,6 +114,7 @@
             options.put(RepairOption.JOB_THREADS_KEY, Integer.toString(numJobThreads));
             options.put(RepairOption.TRACE_KEY, Boolean.toString(trace));
             options.put(RepairOption.COLUMNFAMILIES_KEY, StringUtils.join(cfnames, ","));
+            options.put(RepairOption.PULL_REPAIR_KEY, Boolean.toString(pullRepair));
             options.put(RepairOption.IGNORE_UNREPLICATED_KS, Boolean.toString(ignoreUnreplicatedKeyspaces));
 
             if (!startToken.isEmpty() || !endToken.isEmpty())
@@ -135,4 +139,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Ring.java b/src/java/org/apache/cassandra/tools/nodetool/Ring.java
index dca8779..fabd142 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Ring.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Ring.java
@@ -74,7 +74,7 @@
         String formatPlaceholder = "%%-%ds  %%-12s%%-7s%%-8s%%-16s%%-20s%%-44s%%n";
         String format = format(formatPlaceholder, maxAddressLength);
 
-        StringBuffer errors = new StringBuffer();
+        StringBuilder errors = new StringBuilder();
         boolean showEffectiveOwnership = true;
         // Calculate per-token ownership of the ring
         Map<InetAddress, Float> ownerships;
@@ -85,12 +85,12 @@
         catch (IllegalStateException ex)
         {
             ownerships = probe.getOwnership();
-            errors.append("Note: " + ex.getMessage() + "%n");
+            errors.append("Note: ").append(ex.getMessage()).append("%n");
             showEffectiveOwnership = false;
         }
         catch (IllegalArgumentException ex)
         {
-            out.printf("%nError: " + ex.getMessage() + "%n");
+            out.printf("%nError: %s%n", ex.getMessage());
             return;
         }
 
diff --git a/src/java/org/apache/cassandra/tools/nodetool/SetConcurrentCompactors.java b/src/java/org/apache/cassandra/tools/nodetool/SetConcurrentCompactors.java
new file mode 100644
index 0000000..56fafe1
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/SetConcurrentCompactors.java
@@ -0,0 +1,38 @@
+/*
+ * 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.tools.nodetool;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import io.airlift.command.Arguments;
+import io.airlift.command.Command;
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool;
+
+@Command(name = "setconcurrentcompactors", description = "Set number of concurrent compactors in the system.")
+public class SetConcurrentCompactors extends NodeTool.NodeToolCmd
+{
+    @Arguments(title = "concurrent_compactors", usage = "<value>", description = "Number of concurrent compactors, greater than 0.", required = true)
+    private Integer concurrentCompactors = null;
+
+    protected void execute(NodeProbe probe)
+    {
+        checkArgument(concurrentCompactors > 0, "concurrent_compactors should be great than 0.");
+        probe.setConcurrentCompactors(concurrentCompactors);
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/SetTimeout.java b/src/java/org/apache/cassandra/tools/nodetool/SetTimeout.java
new file mode 100644
index 0000000..0b99efd
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/SetTimeout.java
@@ -0,0 +1,55 @@
+/*
+ * 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.tools.nodetool;
+
+import io.airlift.command.Arguments;
+import io.airlift.command.Command;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+@Command(name = "settimeout", description = "Set the specified timeout in ms, or 0 to disable timeout")
+public class SetTimeout extends NodeToolCmd
+{
+    @Arguments(usage = "<timeout_type> <timeout_in_ms>", description = "Timeout type followed by value in ms " +
+            "(0 disables socket streaming timeout). Type should be one of (" + GetTimeout.TIMEOUT_TYPES + ")",
+            required = true)
+    private List<String> args = new ArrayList<>();
+
+    @Override
+    public void execute(NodeProbe probe)
+    {
+        checkArgument(args.size() == 2, "Timeout type followed by value in ms (0 disables socket streaming timeout)." +
+                " Type should be one of (" + GetTimeout.TIMEOUT_TYPES + ")");
+
+        try
+        {
+            String type = args.get(0);
+            long timeout = Long.parseLong(args.get(1));
+            probe.setTimeout(type, timeout);
+        } catch (Exception e)
+        {
+            throw new IllegalArgumentException(e.getMessage());
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Snapshot.java b/src/java/org/apache/cassandra/tools/nodetool/Snapshot.java
index c2dd097..3c77a55 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Snapshot.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Snapshot.java
@@ -26,7 +26,9 @@
 import java.io.IOException;
 import java.io.PrintStream;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
@@ -46,6 +48,9 @@
     @Option(title = "ktlist", name = { "-kt", "--kt-list", "-kc", "--kc.list" }, description = "The list of Keyspace.table to take snapshot.(you must not specify only keyspace)")
     private String ktList = null;
 
+    @Option(title = "skip-flush", name = {"-sf", "--skip-flush"}, description = "Do not flush memtables before snapshotting (snapshot will not contain unflushed data)")
+    private boolean skipFlush = false;
+
     @Override
     public void execute(NodeProbe probe)
     {
@@ -56,6 +61,9 @@
 
             sb.append("Requested creating snapshot(s) for ");
 
+            Map<String, String> options = new HashMap<String,String>();
+            options.put("skipFlush", Boolean.toString(skipFlush));
+
             // Create a separate path for kclist to avoid breaking of already existing scripts
             if (null != ktList && !ktList.isEmpty())
             {
@@ -69,8 +77,9 @@
                 }
                 if (!snapshotName.isEmpty())
                     sb.append(" with snapshot name [").append(snapshotName).append("]");
+                sb.append(" and options ").append(options.toString());
                 out.println(sb.toString());
-                probe.takeMultipleTableSnapshot(snapshotName, ktList.split(","));
+                probe.takeMultipleTableSnapshot(snapshotName, options, ktList.split(","));
                 out.println("Snapshot directory: " + snapshotName);
             }
             else
@@ -82,10 +91,10 @@
 
                 if (!snapshotName.isEmpty())
                     sb.append(" with snapshot name [").append(snapshotName).append("]");
-
+                sb.append(" and options ").append(options.toString());
                 out.println(sb.toString());
 
-                probe.takeSnapshot(snapshotName, table, toArray(keyspaces, String.class));
+                probe.takeSnapshot(snapshotName, table, options, toArray(keyspaces, String.class));
                 out.println("Snapshot directory: " + snapshotName);
             }
         }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Status.java b/src/java/org/apache/cassandra/tools/nodetool/Status.java
index 299608e..6b99d02 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Status.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Status.java
@@ -68,7 +68,7 @@
         hostIDMap = probe.getHostIdMap();
         epSnitchInfo = probe.getEndpointSnitchInfoProxy();
 
-        StringBuffer errors = new StringBuffer();
+        StringBuilder errors = new StringBuilder();
 
         Map<InetAddress, Float> ownerships = null;
         boolean hasEffectiveOwns = false;
@@ -80,11 +80,11 @@
         catch (IllegalStateException e)
         {
             ownerships = probe.getOwnership();
-            errors.append("Note: " + e.getMessage() + "%n");
+            errors.append("Note: ").append(e.getMessage()).append("%n");
         }
         catch (IllegalArgumentException ex)
         {
-            out.printf("%nError: " + ex.getMessage() + "%n");
+            out.printf("%nError: %s%n", ex.getMessage());
             System.exit(1);
         }
 
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Stop.java b/src/java/org/apache/cassandra/tools/nodetool/Stop.java
index 6229e65..7d785ab 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Stop.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Stop.java
@@ -30,7 +30,7 @@
 {
     @Arguments(title = "compaction_type",
               usage = "<compaction type>",
-              description = "Supported types are COMPACTION, VALIDATION, CLEANUP, SCRUB, VERIFY, INDEX_BUILD",
+              description = "Supported types are COMPACTION, VALIDATION, CLEANUP, SCRUB, UPGRADE_SSTABLES, INDEX_BUILD, TOMBSTONE_COMPACTION, ANTICOMPACTION, VERIFY, VIEW_BUILD, INDEX_SUMMARY, RELOCATE, GARBAGE_COLLECT",
               required = false)
     private OperationType compactionType = OperationType.UNKNOWN;
 
@@ -48,4 +48,4 @@
         else
             probe.stop(compactionType.name());
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/StopDaemon.java b/src/java/org/apache/cassandra/tools/nodetool/StopDaemon.java
index 79a499a..24c8920 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/StopDaemon.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/StopDaemon.java
@@ -19,6 +19,7 @@
 
 import io.airlift.command.Command;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
 import org.apache.cassandra.utils.JVMStabilityInspector;
@@ -31,6 +32,7 @@
     {
         try
         {
+            DatabaseDescriptor.toolInitialization();
             probe.stopCassandraDaemon();
         } catch (Exception e)
         {
diff --git a/src/java/org/apache/cassandra/tools/nodetool/TableHistograms.java b/src/java/org/apache/cassandra/tools/nodetool/TableHistograms.java
index 04a0c97..bfb1d65 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/TableHistograms.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/TableHistograms.java
@@ -35,19 +35,31 @@
 @Command(name = "tablehistograms", description = "Print statistic histograms for a given table")
 public class TableHistograms extends NodeToolCmd
 {
-    @Arguments(usage = "<keyspace> <table>", description = "The keyspace and table name")
+    @Arguments(usage = "<keyspace> <table> | <keyspace.table>", description = "The keyspace and table name")
     private List<String> args = new ArrayList<>();
 
     @Override
     public void execute(NodeProbe probe)
     {
-        checkArgument(args.size() == 2, "tablehistograms requires keyspace and table name arguments");
-
         PrintStream out = probe.output().out;
         PrintStream err = probe.output().err;
-
-        String keyspace = args.get(0);
-        String table = args.get(1);
+        String keyspace = null, table = null;
+        if (args.size() == 2)
+        {
+            keyspace = args.get(0);
+            table = args.get(1);
+        }
+        else if (args.size() == 1)
+        {
+            String[] input = args.get(0).split("\\.");
+            checkArgument(input.length == 2, "tablehistograms requires keyspace and table name arguments");
+            keyspace = input[0];
+            table = input[1];
+        }
+        else
+        {
+            checkArgument(false, "tablehistograms requires keyspace and table name arguments");
+        }
 
         // calculate percentile of row size and column count
         long[] estimatedPartitionSize = (long[]) probe.getColumnFamilyMetric(keyspace, table, "EstimatedPartitionSizeHistogram");
diff --git a/src/java/org/apache/cassandra/tools/nodetool/TableStats.java b/src/java/org/apache/cassandra/tools/nodetool/TableStats.java
index ea46d6e..58daa8b 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/TableStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/TableStats.java
@@ -17,24 +17,14 @@
  */
 package org.apache.cassandra.tools.nodetool;
 
+import java.util.*;
 import io.airlift.command.Arguments;
 import io.airlift.command.Command;
 import io.airlift.command.Option;
 
-import java.io.PrintStream;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-import javax.management.InstanceNotFoundException;
-
-import org.apache.cassandra.db.ColumnFamilyStoreMBean;
-import org.apache.cassandra.io.util.FileUtils;
-import org.apache.cassandra.metrics.CassandraMetricsRegistry;
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+import org.apache.cassandra.tools.nodetool.stats.*;
 
 @Command(name = "tablestats", description = "Print statistics on tables")
 public class TableStats extends NodeToolCmd
@@ -47,272 +37,26 @@
 
     @Option(title = "human_readable",
             name = {"-H", "--human-readable"},
-            description = "Display bytes in human readable form, i.e. KB, MB, GB, TB")
+            description = "Display bytes in human readable form, i.e. KiB, MiB, GiB, TiB")
     private boolean humanReadable = false;
 
+    @Option(title = "format",
+            name = {"-F", "--format"},
+            description = "Output format (json, yaml)")
+    private String outputFormat = "";
+
     @Override
     public void execute(NodeProbe probe)
     {
-        PrintStream out = probe.output().out;
-        TableStats.OptionFilter filter = new OptionFilter(ignore, tableNames);
-        Map<String, List<ColumnFamilyStoreMBean>> tableStoreMap = new HashMap<>();
-
-        // get a list of column family stores
-        Iterator<Map.Entry<String, ColumnFamilyStoreMBean>> tables = probe.getColumnFamilyStoreMBeanProxies();
-
-        while (tables.hasNext())
+        if (!outputFormat.isEmpty() && !"json".equals(outputFormat) && !"yaml".equals(outputFormat))
         {
-            Map.Entry<String, ColumnFamilyStoreMBean> entry = tables.next();
-            String keyspaceName = entry.getKey();
-            ColumnFamilyStoreMBean tableProxy = entry.getValue();
-
-            if (!tableStoreMap.containsKey(keyspaceName) && filter.isColumnFamilyIncluded(entry.getKey(), tableProxy.getColumnFamilyName()))
-            {
-                List<ColumnFamilyStoreMBean> columnFamilies = new ArrayList<>();
-                columnFamilies.add(tableProxy);
-                tableStoreMap.put(keyspaceName, columnFamilies);
-            } else if (filter.isColumnFamilyIncluded(entry.getKey(), tableProxy.getColumnFamilyName()))
-            {
-                tableStoreMap.get(keyspaceName).add(tableProxy);
-            }
+            throw new IllegalArgumentException("arguments for -F are json,yaml only.");
         }
 
-        // make sure all specified keyspace and tables exist
-        filter.verifyKeyspaces(probe.getKeyspaces());
-        filter.verifyColumnFamilies();
-
-        // print out the table statistics
-        for (Map.Entry<String, List<ColumnFamilyStoreMBean>> entry : tableStoreMap.entrySet())
-        {
-            String keyspaceName = entry.getKey();
-            List<ColumnFamilyStoreMBean> columnFamilies = entry.getValue();
-            long keyspaceReadCount = 0;
-            long keyspaceWriteCount = 0;
-            int keyspacePendingFlushes = 0;
-            double keyspaceTotalReadTime = 0.0f;
-            double keyspaceTotalWriteTime = 0.0f;
-
-            out.println("Keyspace: " + keyspaceName);
-            for (ColumnFamilyStoreMBean table : columnFamilies)
-            {
-                String tableName = table.getColumnFamilyName();
-                long writeCount = ((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "WriteLatency")).getCount();
-                long readCount = ((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "ReadLatency")).getCount();
-
-                if (readCount > 0)
-                {
-                    keyspaceReadCount += readCount;
-                    keyspaceTotalReadTime += (long) probe.getColumnFamilyMetric(keyspaceName, tableName, "ReadTotalLatency");
-                }
-                if (writeCount > 0)
-                {
-                    keyspaceWriteCount += writeCount;
-                    keyspaceTotalWriteTime += (long) probe.getColumnFamilyMetric(keyspaceName, tableName, "WriteTotalLatency");
-                }
-                keyspacePendingFlushes += (long) probe.getColumnFamilyMetric(keyspaceName, tableName, "PendingFlushes");
-            }
-
-            double keyspaceReadLatency = keyspaceReadCount > 0
-                                         ? keyspaceTotalReadTime / keyspaceReadCount / 1000
-                                         : Double.NaN;
-            double keyspaceWriteLatency = keyspaceWriteCount > 0
-                                          ? keyspaceTotalWriteTime / keyspaceWriteCount / 1000
-                                          : Double.NaN;
-
-            out.println("\tRead Count: " + keyspaceReadCount);
-            out.println("\tRead Latency: " + String.format("%s", keyspaceReadLatency) + " ms.");
-            out.println("\tWrite Count: " + keyspaceWriteCount);
-            out.println("\tWrite Latency: " + String.format("%s", keyspaceWriteLatency) + " ms.");
-            out.println("\tPending Flushes: " + keyspacePendingFlushes);
-
-            // print out column family statistics for this keyspace
-            for (ColumnFamilyStoreMBean table : columnFamilies)
-            {
-                String tableName = table.getColumnFamilyName();
-                if (tableName.contains("."))
-                    out.println("\t\tTable (index): " + tableName);
-                else
-                    out.println("\t\tTable: " + tableName);
-
-                out.println("\t\tSSTable count: " + probe.getColumnFamilyMetric(keyspaceName, tableName, "LiveSSTableCount"));
-
-                int[] leveledSStables = table.getSSTableCountPerLevel();
-                if (leveledSStables != null)
-                {
-                    out.print("\t\tSSTables in each level: [");
-                    for (int level = 0; level < leveledSStables.length; level++)
-                    {
-                        int count = leveledSStables[level];
-                        out.print(count);
-                        long maxCount = 4L; // for L0
-                        if (level > 0)
-                            maxCount = (long) Math.pow(10, level);
-                        //  show max threshold for level when exceeded
-                        if (count > maxCount)
-                            out.print("/" + maxCount);
-
-                        if (level < leveledSStables.length - 1)
-                            out.print(", ");
-                        else
-                            out.println("]");
-                    }
-                }
-
-                Long memtableOffHeapSize = null;
-                Long bloomFilterOffHeapSize = null;
-                Long indexSummaryOffHeapSize = null;
-                Long compressionMetadataOffHeapSize = null;
-
-                Long offHeapSize = null;
-
-                try
-                {
-                    memtableOffHeapSize = (Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "MemtableOffHeapSize");
-                    bloomFilterOffHeapSize = (Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "BloomFilterOffHeapMemoryUsed");
-                    indexSummaryOffHeapSize = (Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "IndexSummaryOffHeapMemoryUsed");
-                    compressionMetadataOffHeapSize = (Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "CompressionMetadataOffHeapMemoryUsed");
-
-                    offHeapSize = memtableOffHeapSize + bloomFilterOffHeapSize + indexSummaryOffHeapSize + compressionMetadataOffHeapSize;
-                }
-                catch (RuntimeException e)
-                {
-                    // offheap-metrics introduced in 2.1.3 - older versions do not have the appropriate mbeans
-                    if (!(e.getCause() instanceof InstanceNotFoundException))
-                        throw e;
-                }
-
-                out.println("\t\tSpace used (live): " + format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "LiveDiskSpaceUsed"), humanReadable));
-                out.println("\t\tSpace used (total): " + format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "TotalDiskSpaceUsed"), humanReadable));
-                out.println("\t\tSpace used by snapshots (total): " + format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "SnapshotsSize"), humanReadable));
-                if (offHeapSize != null)
-                    out.println("\t\tOff heap memory used (total): " + format(offHeapSize, humanReadable));
-                out.println("\t\tSSTable Compression Ratio: " + probe.getColumnFamilyMetric(keyspaceName, tableName, "CompressionRatio"));
-
-                Object estimatedPartitionCount = probe.getColumnFamilyMetric(keyspaceName, tableName, "EstimatedPartitionCount");
-                if (Long.valueOf(-1L).equals(estimatedPartitionCount))
-                {
-                    estimatedPartitionCount = 0L;
-                }
-                out.println("\t\tNumber of partitions (estimate): " + estimatedPartitionCount);
-
-                out.println("\t\tMemtable cell count: " + probe.getColumnFamilyMetric(keyspaceName, tableName, "MemtableColumnsCount"));
-                out.println("\t\tMemtable data size: " + format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "MemtableLiveDataSize"), humanReadable));
-                if (memtableOffHeapSize != null)
-                    out.println("\t\tMemtable off heap memory used: " + format(memtableOffHeapSize, humanReadable));
-                out.println("\t\tMemtable switch count: " + probe.getColumnFamilyMetric(keyspaceName, tableName, "MemtableSwitchCount"));
-                out.println("\t\tLocal read count: " + ((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "ReadLatency")).getCount());
-                double localReadLatency = ((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "ReadLatency")).getMean() / 1000;
-                double localRLatency = localReadLatency > 0 ? localReadLatency : Double.NaN;
-                out.printf("\t\tLocal read latency: %01.3f ms%n", localRLatency);
-                out.println("\t\tLocal write count: " + ((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "WriteLatency")).getCount());
-                double localWriteLatency = ((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "WriteLatency")).getMean() / 1000;
-                double localWLatency = localWriteLatency > 0 ? localWriteLatency : Double.NaN;
-                out.printf("\t\tLocal write latency: %01.3f ms%n", localWLatency);
-                out.println("\t\tPending flushes: " + probe.getColumnFamilyMetric(keyspaceName, tableName, "PendingFlushes"));
-                out.println("\t\tBloom filter false positives: " + probe.getColumnFamilyMetric(keyspaceName, tableName, "BloomFilterFalsePositives"));
-                out.printf("\t\tBloom filter false ratio: %s%n", String.format("%01.5f", probe.getColumnFamilyMetric(keyspaceName, tableName, "RecentBloomFilterFalseRatio")));
-                out.println("\t\tBloom filter space used: " + format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "BloomFilterDiskSpaceUsed"), humanReadable));
-                if (bloomFilterOffHeapSize != null)
-                    out.println("\t\tBloom filter off heap memory used: " + format(bloomFilterOffHeapSize, humanReadable));
-                if (indexSummaryOffHeapSize != null)
-                    out.println("\t\tIndex summary off heap memory used: " + format(indexSummaryOffHeapSize, humanReadable));
-                if (compressionMetadataOffHeapSize != null)
-                    out.println("\t\tCompression metadata off heap memory used: " + format(compressionMetadataOffHeapSize, humanReadable));
-
-                out.println("\t\tCompacted partition minimum bytes: " + format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "MinPartitionSize"), humanReadable));
-                out.println("\t\tCompacted partition maximum bytes: " + format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "MaxPartitionSize"), humanReadable));
-                out.println("\t\tCompacted partition mean bytes: " + format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "MeanPartitionSize"), humanReadable));
-                CassandraMetricsRegistry.JmxHistogramMBean histogram = (CassandraMetricsRegistry.JmxHistogramMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "LiveScannedHistogram");
-                out.println("\t\tAverage live cells per slice (last five minutes): " + histogram.getMean());
-                out.println("\t\tMaximum live cells per slice (last five minutes): " + histogram.getMax());
-                histogram = (CassandraMetricsRegistry.JmxHistogramMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "TombstoneScannedHistogram");
-                out.println("\t\tAverage tombstones per slice (last five minutes): " + histogram.getMean());
-                out.println("\t\tMaximum tombstones per slice (last five minutes): " + histogram.getMax());
-
-                out.println("");
-            }
-            out.println("----------------");
-        }
+        StatsHolder holder = new TableStatsHolder(probe, humanReadable, ignore, tableNames);
+        // print out the keyspace and table statistics
+        StatsPrinter printer = TableStatsPrinter.from(outputFormat);
+        printer.print(holder, probe.output().out);
     }
 
-    private String format(long bytes, boolean humanReadable) {
-        return humanReadable ? FileUtils.stringifyFileSize(bytes) : Long.toString(bytes);
-    }
-
-    /**
-     * Used for filtering keyspaces and tables to be displayed using the tablestats command.
-     */
-    private static class OptionFilter
-    {
-        private Map<String, List<String>> filter = new HashMap<>();
-        private Map<String, List<String>> verifier = new HashMap<>();
-        private List<String> filterList = new ArrayList<>();
-        private boolean ignoreMode;
-
-        public OptionFilter(boolean ignoreMode, List<String> filterList)
-        {
-            this.filterList.addAll(filterList);
-            this.ignoreMode = ignoreMode;
-
-            for (String s : filterList)
-            {
-                String[] keyValues = s.split("\\.", 2);
-
-                // build the map that stores the keyspaces and tables to use
-                if (!filter.containsKey(keyValues[0]))
-                {
-                    filter.put(keyValues[0], new ArrayList<String>());
-                    verifier.put(keyValues[0], new ArrayList<String>());
-
-                    if (keyValues.length == 2)
-                    {
-                        filter.get(keyValues[0]).add(keyValues[1]);
-                        verifier.get(keyValues[0]).add(keyValues[1]);
-                    }
-                } else
-                {
-                    if (keyValues.length == 2)
-                    {
-                        filter.get(keyValues[0]).add(keyValues[1]);
-                        verifier.get(keyValues[0]).add(keyValues[1]);
-                    }
-                }
-            }
-        }
-
-        public boolean isColumnFamilyIncluded(String keyspace, String columnFamily)
-        {
-            // supplying empty params list is treated as wanting to display all keyspaces and tables
-            if (filterList.isEmpty())
-                return !ignoreMode;
-
-            List<String> tables = filter.get(keyspace);
-
-            // no such keyspace is in the map
-            if (tables == null)
-                return ignoreMode;
-                // only a keyspace with no tables was supplied
-                // so ignore or include (based on the flag) every column family in specified keyspace
-            else if (tables.size() == 0)
-                return !ignoreMode;
-
-            // keyspace exists, and it contains specific table
-            verifier.get(keyspace).remove(columnFamily);
-            return ignoreMode ^ tables.contains(columnFamily);
-        }
-
-        public void verifyKeyspaces(List<String> keyspaces)
-        {
-            for (String ks : verifier.keySet())
-                if (!keyspaces.contains(ks))
-                    throw new IllegalArgumentException("Unknown keyspace: " + ks);
-        }
-
-        public void verifyColumnFamilies()
-        {
-            for (String ks : filter.keySet())
-                if (verifier.get(ks).size() > 0)
-                    throw new IllegalArgumentException("Unknown tables: " + verifier.get(ks) + " in keyspace: " + ks);
-        }
-    }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/TopPartitions.java b/src/java/org/apache/cassandra/tools/nodetool/TopPartitions.java
index dda42e1..2da965d 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/TopPartitions.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/TopPartitions.java
@@ -61,7 +61,7 @@
         PrintStream out = probe.output().out;
         String keyspace = args.get(0);
         String cfname = args.get(1);
-        Integer duration = Integer.parseInt(args.get(2));
+        Integer duration = Integer.valueOf(args.get(2));
         // generate the list of samplers
         List<Sampler> targets = Lists.newArrayList();
         for (String s : samplers.split(","))
@@ -99,7 +99,7 @@
             if(!first)
                 out.println();
             out.println(result.getKey().toString()+ " Sampler:");
-            out.printf("  Cardinality: ~%d (%d capacity)%n", (long) sampling.get("cardinality"), size);
+            out.printf("  Cardinality: ~%d (%d capacity)%n", sampling.get("cardinality"), size);
             out.printf("  Top %d partitions:%n", topCount);
             if (topk.size() == 0)
             {
diff --git a/src/java/org/apache/cassandra/tools/nodetool/TpStats.java b/src/java/org/apache/cassandra/tools/nodetool/TpStats.java
index c0e909a..60f5c30 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/TpStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/TpStats.java
@@ -19,38 +19,32 @@
 
 import io.airlift.command.Command;
 
-import java.io.PrintStream;
-import java.util.Map;
-
-import com.google.common.collect.Multimap;
-
+import io.airlift.command.Option;
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+import org.apache.cassandra.tools.nodetool.stats.TpStatsHolder;
+import org.apache.cassandra.tools.nodetool.stats.TpStatsPrinter;
+import org.apache.cassandra.tools.nodetool.stats.*;
+
 
 @Command(name = "tpstats", description = "Print usage statistics of thread pools")
 public class TpStats extends NodeToolCmd
 {
+    @Option(title = "format",
+            name = {"-F", "--format"},
+            description = "Output format (json, yaml)")
+    private String outputFormat = "";
+
     @Override
     public void execute(NodeProbe probe)
     {
-        PrintStream out = probe.output().out;
-        out.printf("%-25s%10s%10s%15s%10s%18s%n", "Pool Name", "Active", "Pending", "Completed", "Blocked", "All time blocked");
-
-
-        Multimap<String, String> threadPools = probe.getThreadPools();
-        for (Map.Entry<String, String> tpool : threadPools.entries())
+        if (!outputFormat.isEmpty() && !"json".equals(outputFormat) && !"yaml".equals(outputFormat))
         {
-            out.printf("%-25s%10s%10s%15s%10s%18s%n",
-                              tpool.getValue(),
-                              probe.getThreadPoolMetric(tpool.getKey(), tpool.getValue(), "ActiveTasks"),
-                              probe.getThreadPoolMetric(tpool.getKey(), tpool.getValue(), "PendingTasks"),
-                              probe.getThreadPoolMetric(tpool.getKey(), tpool.getValue(), "CompletedTasks"),
-                              probe.getThreadPoolMetric(tpool.getKey(), tpool.getValue(), "CurrentlyBlockedTasks"),
-                              probe.getThreadPoolMetric(tpool.getKey(), tpool.getValue(), "TotalBlockedTasks"));
+            throw new IllegalArgumentException("arguments for -F are json,yaml only.");
         }
 
-        out.printf("%n%-20s%10s%n", "Message type", "Dropped");
-        for (Map.Entry<String, Integer> entry : probe.getDroppedMessages().entrySet())
-            out.printf("%-20s%10s%n", entry.getKey(), entry.getValue());
+        StatsHolder data = new TpStatsHolder(probe);
+        StatsPrinter printer = TpStatsPrinter.from(outputFormat);
+        printer.print(data, probe.output().out);
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ViewBuildStatus.java b/src/java/org/apache/cassandra/tools/nodetool/ViewBuildStatus.java
new file mode 100644
index 0000000..950b9e7
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/ViewBuildStatus.java
@@ -0,0 +1,86 @@
+/*
+ * 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.tools.nodetool;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import io.airlift.command.Arguments;
+import io.airlift.command.Command;
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool;
+import org.apache.cassandra.tools.nodetool.formatter.TableBuilder;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+@Command(name = "viewbuildstatus", description = "Show progress of a materialized view build")
+public class ViewBuildStatus extends NodeTool.NodeToolCmd
+{
+    private final static String SUCCESS = "SUCCESS";
+
+    @Arguments(usage = "<keyspace> <view> | <keyspace.view>", description = "The keyspace and view name")
+    private List<String> args = new ArrayList<>();
+
+    protected void execute(NodeProbe probe)
+    {
+        String keyspace = null, view = null;
+        if (args.size() == 2)
+        {
+            keyspace = args.get(0);
+            view = args.get(1);
+        }
+        else if (args.size() == 1)
+        {
+            String[] input = args.get(0).split("\\.");
+            checkArgument(input.length == 2, "viewbuildstatus requires keyspace and view name arguments");
+            keyspace = input[0];
+            view = input[1];
+        }
+        else
+        {
+            checkArgument(false, "viewbuildstatus requires keyspace and view name arguments");
+        }
+
+        PrintStream out = probe.output().out;
+        Map<String, String> buildStatus = probe.getViewBuildStatuses(keyspace, view);
+        boolean failed = false;
+        TableBuilder builder = new TableBuilder();
+
+        builder.add("Host", "Info");
+        for (Map.Entry<String, String> status : buildStatus.entrySet())
+        {
+            if (!status.getValue().equals(SUCCESS)) {
+                failed = true;
+            }
+            builder.add(status.getKey(), status.getValue());
+        }
+
+        if (failed) {
+            out.println(String.format("%s.%s has not finished building; node status is below.", keyspace, view));
+            out.println();
+            builder.printTo(out);
+            System.exit(1);
+        } else {
+            out.println(String.format("%s.%s has finished building", keyspace, view));
+            System.exit(0);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/CompactionHistoryHolder.java b/src/java/org/apache/cassandra/tools/nodetool/stats/CompactionHistoryHolder.java
new file mode 100644
index 0000000..8799ffb
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/CompactionHistoryHolder.java
@@ -0,0 +1,126 @@
+/*
+ * 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.tools.nodetool.stats;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.management.openmbean.TabularData;
+
+import org.apache.cassandra.tools.NodeProbe;
+
+public class CompactionHistoryHolder implements StatsHolder
+{
+    public final NodeProbe probe;
+    public List<String> indexNames;
+
+    public CompactionHistoryHolder(NodeProbe probe)
+    {
+        this.probe = probe;
+    }
+
+    /**
+     * Allows the Compaction History output to be ordered by 'compactedAt' - that is the
+     * time at which compaction finished.
+     */
+    private static class CompactionHistoryRow implements Comparable<CompactionHistoryHolder.CompactionHistoryRow>
+    {
+        private final String id;
+        private final String ksName;
+        private final String cfName;
+        private final long compactedAt;
+        private final long bytesIn;
+        private final long bytesOut;
+        private final String rowMerged;
+
+        CompactionHistoryRow(String id, String ksName, String cfName, long compactedAt, long bytesIn, long bytesOut, String rowMerged)
+        {
+            this.id = id;
+            this.ksName = ksName;
+            this.cfName = cfName;
+            this.compactedAt = compactedAt;
+            this.bytesIn = bytesIn;
+            this.bytesOut = bytesOut;
+            this.rowMerged = rowMerged;
+        }
+
+        public int compareTo(CompactionHistoryHolder.CompactionHistoryRow chr)
+        {
+            return Long.signum(chr.compactedAt - this.compactedAt);
+        }
+
+        private HashMap<String, Object> getAllAsMap()
+        {
+            HashMap<String, Object> compaction = new HashMap<>();
+            compaction.put("id", this.id);
+            compaction.put("keyspace_name", this.ksName);
+            compaction.put("columnfamily_name", this.cfName);
+            Instant instant = Instant.ofEpochMilli(this.compactedAt);
+            LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
+            compaction.put("compacted_at", ldt.toString());
+            compaction.put("bytes_in", this.bytesIn);
+            compaction.put("bytes_out", this.bytesOut);
+            compaction.put("rows_merged", this.rowMerged);
+            return compaction;
+        }
+    }
+
+    @Override
+    public Map<String, Object> convert2Map()
+    {
+        HashMap<String, Object> result = new HashMap<>();
+        ArrayList<Map<String, Object>> compactions = new ArrayList<>();
+
+        TabularData tabularData = probe.getCompactionHistory();
+        this.indexNames = tabularData.getTabularType().getIndexNames();
+
+        if (tabularData.isEmpty()) return result;
+
+        List<CompactionHistoryHolder.CompactionHistoryRow> chrList = new ArrayList<>();
+        Set<?> values = tabularData.keySet();
+        for (Object eachValue : values)
+        {
+            List<?> value = (List<?>) eachValue;
+            CompactionHistoryHolder.CompactionHistoryRow chr = new CompactionHistoryHolder.CompactionHistoryRow(
+                (String)value.get(0),
+                (String)value.get(1),
+                (String)value.get(2),
+                (Long)value.get(3),
+                (Long)value.get(4),
+                (Long)value.get(5),
+                (String)value.get(6)
+            );
+            chrList.add(chr);
+        }
+
+        Collections.sort(chrList);
+        for (CompactionHistoryHolder.CompactionHistoryRow chr : chrList)
+        {
+            compactions.add(chr.getAllAsMap());
+        }
+        result.put("CompactionHistory", compactions);
+        return result;
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/CompactionHistoryPrinter.java b/src/java/org/apache/cassandra/tools/nodetool/stats/CompactionHistoryPrinter.java
new file mode 100644
index 0000000..97ed1c4
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/CompactionHistoryPrinter.java
@@ -0,0 +1,81 @@
+/*
+ * 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.tools.nodetool.stats;
+
+import java.io.PrintStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cassandra.tools.nodetool.formatter.TableBuilder;
+
+import static com.google.common.collect.Iterables.toArray;
+
+public class CompactionHistoryPrinter
+{
+    public static StatsPrinter from(String format)
+    {
+        switch (format)
+        {
+            case "json":
+                return new StatsPrinter.JsonPrinter();
+            case "yaml":
+                return new StatsPrinter.YamlPrinter();
+            default:
+                return new DefaultPrinter();
+        }
+
+    }
+
+    public static class DefaultPrinter implements StatsPrinter<CompactionHistoryHolder>
+    {
+        @Override
+        public void print(CompactionHistoryHolder data, PrintStream out)
+        {
+
+            out.println("Compaction History: ");
+            Map<String, Object> convertData = data.convert2Map();
+            List<Object> compactionHistories = convertData.get("CompactionHistory") instanceof List<?> ? (List)convertData.get("CompactionHistory") : Collections.emptyList();
+            List<String> indexNames = data.indexNames;
+
+            if (compactionHistories.size() == 0) {
+                out.printf("There is no compaction history");
+                return;
+            }
+
+            TableBuilder table = new TableBuilder();
+
+            table.add(toArray(indexNames, String.class));
+            for (Object chr : compactionHistories)
+            {
+                Map value = chr instanceof Map<?, ?> ? (Map)chr : Collections.emptyMap();
+                String[] obj = new String[7];
+                obj[0] = (String)value.get("id");
+                obj[1] = (String)value.get("keyspace_name");
+                obj[2] = (String)value.get("columnfamily_name");
+                obj[3] = (String)value.get("compacted_at");
+                obj[4] = value.get("bytes_in").toString();
+                obj[5] = value.get("bytes_out").toString();
+                obj[6] = (String)value.get("rows_merged");
+                table.add(obj);
+            }
+            table.printTo(out);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/StatsHolder.java b/src/java/org/apache/cassandra/tools/nodetool/stats/StatsHolder.java
new file mode 100644
index 0000000..c35e1fe
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/StatsHolder.java
@@ -0,0 +1,29 @@
+/*
+ * 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.tools.nodetool.stats;
+
+import java.util.Map;
+
+/**
+ * Interface for the Stats property bag
+ */
+public interface StatsHolder
+{
+    public Map<String, Object> convert2Map();
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/StatsKeyspace.java b/src/java/org/apache/cassandra/tools/nodetool/stats/StatsKeyspace.java
new file mode 100644
index 0000000..dc15332
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/StatsKeyspace.java
@@ -0,0 +1,78 @@
+/*
+ * 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.tools.nodetool.stats;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.cassandra.db.ColumnFamilyStoreMBean;
+import org.apache.cassandra.metrics.CassandraMetricsRegistry;
+import org.apache.cassandra.tools.NodeProbe;
+
+public class StatsKeyspace
+{
+    public List<StatsTable> tables = new ArrayList<>();
+    private final NodeProbe probe;
+
+    public String name;
+    public long readCount;
+    public long writeCount;
+    public int pendingFlushes;
+    private double totalReadTime;
+    private double totalWriteTime;
+
+    public StatsKeyspace(NodeProbe probe, String keyspaceName)
+    {
+        this.probe = probe;
+        this.name = keyspaceName;
+    }
+
+    public void add(ColumnFamilyStoreMBean table)
+    {
+        String tableName = table.getTableName();
+        long tableWriteCount = ((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(name, tableName, "WriteLatency")).getCount();
+        long tableReadCount = ((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(name, tableName, "ReadLatency")).getCount();
+
+        if (tableReadCount > 0)
+        {
+            readCount += tableReadCount;
+            totalReadTime += (long) probe.getColumnFamilyMetric(name, tableName, "ReadTotalLatency");
+        }
+        if (tableWriteCount > 0)
+        {
+            writeCount += tableWriteCount;
+            totalWriteTime += (long) probe.getColumnFamilyMetric(name, tableName, "WriteTotalLatency");
+        }
+        pendingFlushes += (long) probe.getColumnFamilyMetric(name, tableName, "PendingFlushes");
+    }
+
+    public double readLatency()
+    {
+        return readCount > 0
+               ? totalReadTime / readCount / 1000
+               : Double.NaN;
+    }
+
+    public double writeLatency()
+    {
+        return writeCount > 0
+               ? totalWriteTime / writeCount / 1000
+               : Double.NaN;
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/StatsPrinter.java b/src/java/org/apache/cassandra/tools/nodetool/stats/StatsPrinter.java
new file mode 100644
index 0000000..e67f33a
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/StatsPrinter.java
@@ -0,0 +1,60 @@
+/*
+ * 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.tools.nodetool.stats;
+
+import java.io.PrintStream;
+
+import org.json.simple.JSONObject;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+
+/**
+ * Interface for the Stats printer, that'd output statistics
+ * given the {@code StatsHolder}
+ *
+ * @param <T> Stats property bad type
+ */
+public interface StatsPrinter<T extends StatsHolder>
+{
+    void print(T data, PrintStream out);
+
+    static class JsonPrinter<T extends StatsHolder> implements StatsPrinter<T>
+    {
+        @Override
+        public void print(T data, PrintStream out)
+        {
+            JSONObject json = new JSONObject();
+            json.putAll(data.convert2Map());
+            out.println(json.toString());
+        }
+    }
+
+    static class YamlPrinter<T extends StatsHolder> implements StatsPrinter<T>
+    {
+        @Override
+        public void print(T data, PrintStream out)
+        {
+            DumperOptions options = new DumperOptions();
+            options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+
+            Yaml yaml = new Yaml(options);
+            out.println(yaml.dump(data.convert2Map()));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/StatsTable.java b/src/java/org/apache/cassandra/tools/nodetool/stats/StatsTable.java
new file mode 100644
index 0000000..87bc527
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/StatsTable.java
@@ -0,0 +1,66 @@
+/*
+ * 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.tools.nodetool.stats;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class StatsTable
+{
+    public String name;
+    public boolean isIndex;
+    public boolean isLeveledSstable = false;
+    public Object sstableCount;
+    public String spaceUsedLive;
+    public String spaceUsedTotal;
+    public String spaceUsedBySnapshotsTotal;
+    public boolean offHeapUsed = false;
+    public String offHeapMemoryUsedTotal;
+    public Object sstableCompressionRatio;
+    public Object numberOfPartitionsEstimate;
+    public Object memtableCellCount;
+    public String memtableDataSize;
+    public boolean memtableOffHeapUsed = false;
+    public String memtableOffHeapMemoryUsed;
+    public Object memtableSwitchCount;
+    public long localReadCount;
+    public double localReadLatencyMs;
+    public long localWriteCount;
+    public double localWriteLatencyMs;
+    public Object pendingFlushes;
+    public Object bloomFilterFalsePositives;
+    public Object bloomFilterFalseRatio;
+    public String bloomFilterSpaceUsed;
+    public boolean bloomFilterOffHeapUsed = false;
+    public String bloomFilterOffHeapMemoryUsed;
+    public boolean indexSummaryOffHeapUsed = false;
+    public String indexSummaryOffHeapMemoryUsed;
+    public boolean compressionMetadataOffHeapUsed = false;
+    public String compressionMetadataOffHeapMemoryUsed;
+    public long compactedPartitionMinimumBytes;
+    public long compactedPartitionMaximumBytes;
+    public long compactedPartitionMeanBytes;
+    public double percentRepaired;
+    public double averageLiveCellsPerSliceLastFiveMinutes;
+    public long maximumLiveCellsPerSliceLastFiveMinutes;
+    public double averageTombstonesPerSliceLastFiveMinutes;
+    public long maximumTombstonesPerSliceLastFiveMinutes;
+    public String droppedMutations;
+    public List<String> sstablesInEachLevel = new ArrayList<>();
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/TableStatsHolder.java b/src/java/org/apache/cassandra/tools/nodetool/stats/TableStatsHolder.java
new file mode 100644
index 0000000..300895f
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/TableStatsHolder.java
@@ -0,0 +1,372 @@
+/*
+ * 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.tools.nodetool.stats;
+
+import java.util.*;
+
+import javax.management.InstanceNotFoundException;
+
+import com.google.common.collect.ArrayListMultimap;
+
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.io.util.*;
+import org.apache.cassandra.metrics.*;
+import org.apache.cassandra.tools.*;
+
+public class TableStatsHolder implements StatsHolder
+{
+    public final List<StatsKeyspace> keyspaces;
+    public final int numberOfTables;
+
+    public TableStatsHolder(NodeProbe probe, boolean humanReadable, boolean ignore, List<String> tableNames)
+    {
+        this.keyspaces = new ArrayList<>();
+        this.numberOfTables = probe.getNumberOfTables();
+        this.initializeKeyspaces(probe, humanReadable, ignore, tableNames);
+    }
+
+    @Override
+    public Map<String, Object> convert2Map()
+    {
+        HashMap<String, Object> mpRet = new HashMap<>();
+        mpRet.put("total_number_of_tables", numberOfTables);
+        for (StatsKeyspace keyspace : keyspaces)
+        {
+            // store each keyspace's metrics to map
+            HashMap<String, Object> mpKeyspace = new HashMap<>();
+            mpKeyspace.put("read_latency", keyspace.readLatency());
+            mpKeyspace.put("read_count", keyspace.readCount);
+            mpKeyspace.put("read_latency_ms", keyspace.readLatency());
+            mpKeyspace.put("write_count", keyspace.writeCount);
+            mpKeyspace.put("write_latency_ms", keyspace.writeLatency());
+            mpKeyspace.put("pending_flushes", keyspace.pendingFlushes);
+
+            // store each table's metrics to map
+            List<StatsTable> tables = keyspace.tables;
+            Map<String, Map<String, Object>> mpTables = new HashMap<>();
+            for (StatsTable table : tables)
+            {
+                Map<String, Object> mpTable = new HashMap<>();
+
+                mpTable.put("sstable_count", table.sstableCount);
+                mpTable.put("sstables_in_each_level", table.sstablesInEachLevel);
+                mpTable.put("space_used_live", table.spaceUsedLive);
+                mpTable.put("space_used_total", table.spaceUsedTotal);
+                mpTable.put("space_used_by_snapshots_total", table.spaceUsedBySnapshotsTotal);
+                if (table.offHeapUsed)
+                    mpTable.put("off_heap_memory_used_total", table.offHeapMemoryUsedTotal);
+                mpTable.put("sstable_compression_ratio", table.sstableCompressionRatio);
+                mpTable.put("number_of_partitions_estimate", table.numberOfPartitionsEstimate);
+                mpTable.put("memtable_cell_count", table.memtableCellCount);
+                mpTable.put("memtable_data_size", table.memtableDataSize);
+                if (table.memtableOffHeapUsed)
+                    mpTable.put("memtable_off_heap_memory_used", table.memtableOffHeapMemoryUsed);
+                mpTable.put("memtable_switch_count", table.memtableSwitchCount);
+                mpTable.put("local_read_count", table.localReadCount);
+                mpTable.put("local_read_latency_ms", String.format("%01.3f", table.localReadLatencyMs));
+                mpTable.put("local_write_count", table.localWriteCount);
+                mpTable.put("local_write_latency_ms", String.format("%01.3f", table.localWriteLatencyMs));
+                mpTable.put("pending_flushes", table.pendingFlushes);
+                mpTable.put("percent_repaired", table.percentRepaired);
+                mpTable.put("bloom_filter_false_positives", table.bloomFilterFalsePositives);
+                mpTable.put("bloom_filter_false_ratio", String.format("%01.5f", table.bloomFilterFalseRatio));
+                mpTable.put("bloom_filter_space_used", table.bloomFilterSpaceUsed);
+                if (table.bloomFilterOffHeapUsed)
+                    mpTable.put("bloom_filter_off_heap_memory_used", table.bloomFilterOffHeapMemoryUsed);
+                if (table.indexSummaryOffHeapUsed)
+                    mpTable.put("index_summary_off_heap_memory_used", table.indexSummaryOffHeapMemoryUsed);
+                if (table.compressionMetadataOffHeapUsed)
+                    mpTable.put("compression_metadata_off_heap_memory_used",
+                                table.compressionMetadataOffHeapMemoryUsed);
+                mpTable.put("compacted_partition_minimum_bytes", table.compactedPartitionMinimumBytes);
+                mpTable.put("compacted_partition_maximum_bytes", table.compactedPartitionMaximumBytes);
+                mpTable.put("compacted_partition_mean_bytes", table.compactedPartitionMeanBytes);
+                mpTable.put("average_live_cells_per_slice_last_five_minutes",
+                            table.averageLiveCellsPerSliceLastFiveMinutes);
+                mpTable.put("maximum_live_cells_per_slice_last_five_minutes",
+                            table.maximumLiveCellsPerSliceLastFiveMinutes);
+                mpTable.put("average_tombstones_per_slice_last_five_minutes",
+                            table.averageTombstonesPerSliceLastFiveMinutes);
+                mpTable.put("maximum_tombstones_per_slice_last_five_minutes",
+                            table.maximumTombstonesPerSliceLastFiveMinutes);
+                mpTable.put("dropped_mutations", table.droppedMutations);
+
+                mpTables.put(table.name, mpTable);
+            }
+            mpKeyspace.put("tables", mpTables);
+            mpRet.put(keyspace.name, mpKeyspace);
+        }
+        return mpRet;
+    }
+
+    private void initializeKeyspaces(NodeProbe probe, boolean humanReadable, boolean ignore, List<String> tableNames)
+    {
+        OptionFilter filter = new OptionFilter(ignore, tableNames);
+        ArrayListMultimap<String, ColumnFamilyStoreMBean> selectedTableMbeans = ArrayListMultimap.create();
+        Map<String, StatsKeyspace> keyspaceStats = new HashMap<>();
+
+        // get a list of table stores
+        Iterator<Map.Entry<String, ColumnFamilyStoreMBean>> tableMBeans = probe.getColumnFamilyStoreMBeanProxies();
+
+        while (tableMBeans.hasNext())
+        {
+            Map.Entry<String, ColumnFamilyStoreMBean> entry = tableMBeans.next();
+            String keyspaceName = entry.getKey();
+            ColumnFamilyStoreMBean tableProxy = entry.getValue();
+
+            if (filter.isKeyspaceIncluded(keyspaceName))
+            {
+                StatsKeyspace stats = keyspaceStats.get(keyspaceName);
+                if (stats == null)
+                {
+                    stats = new StatsKeyspace(probe, keyspaceName);
+                    keyspaceStats.put(keyspaceName, stats);
+                }
+                stats.add(tableProxy);
+
+                if (filter.isTableIncluded(keyspaceName, tableProxy.getTableName()))
+                    selectedTableMbeans.put(keyspaceName, tableProxy);
+            }
+        }
+
+        // make sure all specified keyspace and tables exist
+        filter.verifyKeyspaces(probe.getKeyspaces());
+        filter.verifyTables();
+
+        // get metrics of keyspace
+        for (Map.Entry<String, Collection<ColumnFamilyStoreMBean>> entry : selectedTableMbeans.asMap().entrySet())
+        {
+            String keyspaceName = entry.getKey();
+            Collection<ColumnFamilyStoreMBean> tables = entry.getValue();
+            StatsKeyspace statsKeyspace = keyspaceStats.get(keyspaceName);
+
+            // get metrics of table statistics for this keyspace
+            for (ColumnFamilyStoreMBean table : tables)
+            {
+                String tableName = table.getTableName();
+                StatsTable statsTable = new StatsTable();
+                statsTable.name = tableName;
+                statsTable.isIndex = tableName.contains(".");
+                statsTable.sstableCount = probe.getColumnFamilyMetric(keyspaceName, tableName, "LiveSSTableCount");
+                int[] leveledSStables = table.getSSTableCountPerLevel();
+                if (leveledSStables != null)
+                {
+                    statsTable.isLeveledSstable = true;
+
+                    for (int level = 0; level < leveledSStables.length; level++)
+                    {
+                        int count = leveledSStables[level];
+                        long maxCount = 4L; // for L0
+                        if (level > 0)
+                            maxCount = (long) Math.pow(table.getLevelFanoutSize(), level);
+                        // show max threshold for level when exceeded
+                        statsTable.sstablesInEachLevel.add(count + ((count > maxCount) ? "/" + maxCount : ""));
+                    }
+                }
+
+                Long memtableOffHeapSize = null;
+                Long bloomFilterOffHeapSize = null;
+                Long indexSummaryOffHeapSize = null;
+                Long compressionMetadataOffHeapSize = null;
+                Long offHeapSize = null;
+                Double percentRepaired = null;
+
+                try
+                {
+                    memtableOffHeapSize = (Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "MemtableOffHeapSize");
+                    bloomFilterOffHeapSize = (Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "BloomFilterOffHeapMemoryUsed");
+                    indexSummaryOffHeapSize = (Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "IndexSummaryOffHeapMemoryUsed");
+                    compressionMetadataOffHeapSize = (Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "CompressionMetadataOffHeapMemoryUsed");
+                    offHeapSize = memtableOffHeapSize + bloomFilterOffHeapSize + indexSummaryOffHeapSize + compressionMetadataOffHeapSize;
+                    percentRepaired = (Double) probe.getColumnFamilyMetric(keyspaceName, tableName, "PercentRepaired");
+                }
+                catch (RuntimeException e)
+                {
+                    // offheap-metrics introduced in 2.1.3 - older versions do not have the appropriate mbeans
+                    if (!(e.getCause() instanceof InstanceNotFoundException))
+                        throw e;
+                }
+
+                statsTable.spaceUsedLive = format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "LiveDiskSpaceUsed"), humanReadable);
+                statsTable.spaceUsedTotal = format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "TotalDiskSpaceUsed"), humanReadable);
+                statsTable.spaceUsedBySnapshotsTotal = format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "SnapshotsSize"), humanReadable);
+                if (offHeapSize != null)
+                {
+                    statsTable.offHeapUsed = true;
+                    statsTable.offHeapMemoryUsedTotal = format(offHeapSize, humanReadable);
+
+                }
+                if (percentRepaired != null)
+                {
+                    statsTable.percentRepaired = Math.round(100 * percentRepaired) / 100.0;
+                }
+                statsTable.sstableCompressionRatio = probe.getColumnFamilyMetric(keyspaceName, tableName, "CompressionRatio");
+                Object estimatedPartitionCount = probe.getColumnFamilyMetric(keyspaceName, tableName, "EstimatedPartitionCount");
+                if (Long.valueOf(-1L).equals(estimatedPartitionCount))
+                {
+                    estimatedPartitionCount = 0L;
+                }
+                statsTable.numberOfPartitionsEstimate = estimatedPartitionCount;
+
+                statsTable.memtableCellCount = probe.getColumnFamilyMetric(keyspaceName, tableName, "MemtableColumnsCount");
+                statsTable.memtableDataSize = format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "MemtableLiveDataSize"), humanReadable);
+                if (memtableOffHeapSize != null)
+                {
+                    statsTable.memtableOffHeapUsed = true;
+                    statsTable.memtableOffHeapMemoryUsed = format(memtableOffHeapSize, humanReadable);
+                }
+                statsTable.memtableSwitchCount = probe.getColumnFamilyMetric(keyspaceName, tableName, "MemtableSwitchCount");
+                statsTable.localReadCount = ((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "ReadLatency")).getCount();
+
+                double localReadLatency = ((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "ReadLatency")).getMean() / 1000;
+                double localRLatency = localReadLatency > 0 ? localReadLatency : Double.NaN;
+                statsTable.localReadLatencyMs = localRLatency;
+                statsTable.localWriteCount = ((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "WriteLatency")).getCount();
+
+                double localWriteLatency = ((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "WriteLatency")).getMean() / 1000;
+                double localWLatency = localWriteLatency > 0 ? localWriteLatency : Double.NaN;
+                statsTable.localWriteLatencyMs = localWLatency;
+                statsTable.pendingFlushes = probe.getColumnFamilyMetric(keyspaceName, tableName, "PendingFlushes");
+
+                statsTable.bloomFilterFalsePositives = probe.getColumnFamilyMetric(keyspaceName, tableName, "BloomFilterFalsePositives");
+                statsTable.bloomFilterFalseRatio = probe.getColumnFamilyMetric(keyspaceName, tableName, "RecentBloomFilterFalseRatio");
+                statsTable.bloomFilterSpaceUsed = format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "BloomFilterDiskSpaceUsed"), humanReadable);
+
+                if (bloomFilterOffHeapSize != null)
+                {
+                    statsTable.bloomFilterOffHeapUsed = true;
+                    statsTable.bloomFilterOffHeapMemoryUsed = format(bloomFilterOffHeapSize, humanReadable);
+                }
+
+                if (indexSummaryOffHeapSize != null)
+                {
+                    statsTable.indexSummaryOffHeapUsed = true;
+                    statsTable.indexSummaryOffHeapMemoryUsed = format(indexSummaryOffHeapSize, humanReadable);
+                }
+                if (compressionMetadataOffHeapSize != null)
+                {
+                    statsTable.compressionMetadataOffHeapUsed = true;
+                    statsTable.compressionMetadataOffHeapMemoryUsed = format(compressionMetadataOffHeapSize, humanReadable);
+                }
+                statsTable.compactedPartitionMinimumBytes = (Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "MinPartitionSize");
+                statsTable.compactedPartitionMaximumBytes = (Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "MaxPartitionSize");
+                statsTable.compactedPartitionMeanBytes = (Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "MeanPartitionSize");
+
+                CassandraMetricsRegistry.JmxHistogramMBean histogram = (CassandraMetricsRegistry.JmxHistogramMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "LiveScannedHistogram");
+                statsTable.averageLiveCellsPerSliceLastFiveMinutes = histogram.getMean();
+                statsTable.maximumLiveCellsPerSliceLastFiveMinutes = histogram.getMax();
+
+                histogram = (CassandraMetricsRegistry.JmxHistogramMBean) probe.getColumnFamilyMetric(keyspaceName, tableName, "TombstoneScannedHistogram");
+                statsTable.averageTombstonesPerSliceLastFiveMinutes = histogram.getMean();
+                statsTable.maximumTombstonesPerSliceLastFiveMinutes = histogram.getMax();
+                statsTable.droppedMutations = format((Long) probe.getColumnFamilyMetric(keyspaceName, tableName, "DroppedMutations"), humanReadable);
+                statsKeyspace.tables.add(statsTable);
+            }
+            keyspaces.add(statsKeyspace);
+        }
+    }
+
+    private String format(long bytes, boolean humanReadable)
+    {
+        return humanReadable ? FileUtils.stringifyFileSize(bytes) : Long.toString(bytes);
+    }
+
+    /**
+     * Used for filtering keyspaces and tables to be displayed using the tablestats command.
+     */
+    private static class OptionFilter
+    {
+        private final Map<String, List<String>> filter = new HashMap<>();
+        private final Map<String, List<String>> verifier = new HashMap<>(); // Same as filter initially, but we remove tables every time we've checked them for inclusion
+        // in isTableIncluded() so that we detect if those table requested don't exist (verifyTables())
+        private final List<String> filterList = new ArrayList<>();
+        private final boolean ignoreMode;
+
+        OptionFilter(boolean ignoreMode, List<String> filterList)
+        {
+            this.filterList.addAll(filterList);
+            this.ignoreMode = ignoreMode;
+
+            for (String s : filterList)
+            {
+                String[] keyValues = s.split("\\.", 2);
+
+                // build the map that stores the keyspaces and tables to use
+                if (!filter.containsKey(keyValues[0]))
+                {
+                    filter.put(keyValues[0], new ArrayList<>());
+                    verifier.put(keyValues[0], new ArrayList<>());
+                }
+
+                if (keyValues.length == 2)
+                {
+                    filter.get(keyValues[0]).add(keyValues[1]);
+                    verifier.get(keyValues[0]).add(keyValues[1]);
+                }
+            }
+        }
+
+        public boolean isTableIncluded(String keyspace, String table)
+        {
+            // supplying empty params list is treated as wanting to display all keyspaces and tables
+            if (filterList.isEmpty())
+                return !ignoreMode;
+
+            List<String> tables = filter.get(keyspace);
+
+            // no such keyspace is in the map
+            if (tables == null)
+                return ignoreMode;
+                // only a keyspace with no tables was supplied
+                // so ignore or include (based on the flag) every column family in specified keyspace
+            else if (tables.isEmpty())
+                return !ignoreMode;
+
+            // keyspace exists, and it contains specific table
+            verifier.get(keyspace).remove(table);
+            return ignoreMode ^ tables.contains(table);
+        }
+
+        public boolean isKeyspaceIncluded(String keyspace)
+        {
+            // supplying empty params list is treated as wanting to display all keyspaces and tables
+            if (filterList.isEmpty())
+                return !ignoreMode;
+
+            // Note that if there is any table for the keyspace, we want to include the keyspace irregarding
+            // of the ignoreMode, since the ignoreMode then apply to the table inside the keyspace but the
+            // keyspace itself is not ignored
+            return filter.get(keyspace) != null || ignoreMode;
+        }
+
+        public void verifyKeyspaces(List<String> keyspaces)
+        {
+            for (String ks : verifier.keySet())
+                if (!keyspaces.contains(ks))
+                    throw new IllegalArgumentException("Unknown keyspace: " + ks);
+        }
+
+        public void verifyTables()
+        {
+            for (String ks : filter.keySet())
+                if (!verifier.get(ks).isEmpty())
+                    throw new IllegalArgumentException("Unknown tables: " + verifier.get(ks) + " in keyspace: " + ks);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/TableStatsPrinter.java b/src/java/org/apache/cassandra/tools/nodetool/stats/TableStatsPrinter.java
new file mode 100644
index 0000000..e1e7b42
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/TableStatsPrinter.java
@@ -0,0 +1,114 @@
+/*
+ * 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.tools.nodetool.stats;
+
+import java.io.PrintStream;
+import java.util.List;
+
+public class TableStatsPrinter
+{
+    public static StatsPrinter from(String format)
+    {
+        switch (format)
+        {
+            case "json":
+                return new StatsPrinter.JsonPrinter();
+            case "yaml":
+                return new StatsPrinter.YamlPrinter();
+            default:
+                return new DefaultPrinter();
+        }
+    }
+
+    private static class DefaultPrinter implements StatsPrinter<TableStatsHolder>
+    {
+        @Override
+        public void print(TableStatsHolder data, PrintStream out)
+        {
+            out.println("Total number of tables: " + data.numberOfTables);
+            out.println("----------------");
+
+            List<StatsKeyspace> keyspaces = data.keyspaces;
+            for (StatsKeyspace keyspace : keyspaces)
+            {
+                // print each keyspace's information
+                out.println("Keyspace : " + keyspace.name);
+                out.println("\tRead Count: " + keyspace.readCount);
+                out.println("\tRead Latency: " + keyspace.readLatency() + " ms");
+                out.println("\tWrite Count: " + keyspace.writeCount);
+                out.println("\tWrite Latency: " + keyspace.writeLatency() + " ms");
+                out.println("\tPending Flushes: " + keyspace.pendingFlushes);
+
+                // print each table's information
+                List<StatsTable> tables = keyspace.tables;
+                for (StatsTable table : tables)
+                {
+                    out.println("\t\tTable" + (table.isIndex ? " (index): " : ": ") + table.name);
+                    out.println("\t\tSSTable count: " + table.sstableCount);
+                    if (table.isLeveledSstable)
+                        out.println("\t\tSSTables in each level: [" + String.join(", ",
+                                                                                  table.sstablesInEachLevel) + "]");
+
+                    out.println("\t\tSpace used (live): " + table.spaceUsedLive);
+                    out.println("\t\tSpace used (total): " + table.spaceUsedTotal);
+                    out.println("\t\tSpace used by snapshots (total): " + table.spaceUsedBySnapshotsTotal);
+
+                    if (table.offHeapUsed)
+                        out.println("\t\tOff heap memory used (total): " + table.offHeapMemoryUsedTotal);
+                    out.println("\t\tSSTable Compression Ratio: " + table.sstableCompressionRatio);
+                    out.println("\t\tNumber of partitions (estimate): " + table.numberOfPartitionsEstimate);
+                    out.println("\t\tMemtable cell count: " + table.memtableCellCount);
+                    out.println("\t\tMemtable data size: " + table.memtableDataSize);
+
+                    if (table.memtableOffHeapUsed)
+                        out.println("\t\tMemtable off heap memory used: " + table.memtableOffHeapMemoryUsed);
+                    out.println("\t\tMemtable switch count: " + table.memtableSwitchCount);
+                    out.println("\t\tLocal read count: " + table.localReadCount);
+                    out.printf("\t\tLocal read latency: %01.3f ms%n", table.localReadLatencyMs);
+                    out.println("\t\tLocal write count: " + table.localWriteCount);
+                    out.printf("\t\tLocal write latency: %01.3f ms%n", table.localWriteLatencyMs);
+                    out.println("\t\tPending flushes: " + table.pendingFlushes);
+                    out.println("\t\tPercent repaired: " + table.percentRepaired);
+
+                    out.println("\t\tBloom filter false positives: " + table.bloomFilterFalsePositives);
+                    out.printf("\t\tBloom filter false ratio: %01.5f%n", table.bloomFilterFalseRatio);
+                    out.println("\t\tBloom filter space used: " + table.bloomFilterSpaceUsed);
+
+                    if (table.bloomFilterOffHeapUsed)
+                        out.println("\t\tBloom filter off heap memory used: " + table.bloomFilterOffHeapMemoryUsed);
+                    if (table.indexSummaryOffHeapUsed)
+                        out.println("\t\tIndex summary off heap memory used: " + table.indexSummaryOffHeapMemoryUsed);
+                    if (table.compressionMetadataOffHeapUsed)
+                        out.println("\t\tCompression metadata off heap memory used: " + table.compressionMetadataOffHeapMemoryUsed);
+
+                    out.println("\t\tCompacted partition minimum bytes: " + table.compactedPartitionMinimumBytes);
+                    out.println("\t\tCompacted partition maximum bytes: " + table.compactedPartitionMaximumBytes);
+                    out.println("\t\tCompacted partition mean bytes: " + table.compactedPartitionMeanBytes);
+                    out.println("\t\tAverage live cells per slice (last five minutes): " + table.averageLiveCellsPerSliceLastFiveMinutes);
+                    out.println("\t\tMaximum live cells per slice (last five minutes): " + table.maximumLiveCellsPerSliceLastFiveMinutes);
+                    out.println("\t\tAverage tombstones per slice (last five minutes): " + table.averageTombstonesPerSliceLastFiveMinutes);
+                    out.println("\t\tMaximum tombstones per slice (last five minutes): " + table.maximumTombstonesPerSliceLastFiveMinutes);
+                    out.println("\t\tDropped Mutations: " + table.droppedMutations);
+                    out.println("");
+                }
+                out.println("----------------");
+            }
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/TpStatsHolder.java b/src/java/org/apache/cassandra/tools/nodetool/stats/TpStatsHolder.java
new file mode 100644
index 0000000..d70b4dd
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/TpStatsHolder.java
@@ -0,0 +1,61 @@
+/*
+ * 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.tools.nodetool.stats;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.nodetool.stats.StatsHolder;
+
+public class TpStatsHolder implements StatsHolder
+{
+    public final NodeProbe probe;
+
+    public TpStatsHolder(NodeProbe probe)
+    {
+        this.probe = probe;
+    }
+
+    @Override
+    public Map<String, Object> convert2Map()
+    {
+        HashMap<String, Object> result = new HashMap<>();
+        HashMap<String, Map<String, Object>> threadPools = new HashMap<>();
+        HashMap<String, Object> droppedMessage = new HashMap<>();
+
+        for (Map.Entry<String, String> tp : probe.getThreadPools().entries())
+        {
+            HashMap<String, Object> threadPool = new HashMap<>();
+            threadPool.put("ActiveTasks", probe.getThreadPoolMetric(tp.getKey(), tp.getValue(), "ActiveTasks"));
+            threadPool.put("PendingTasks", probe.getThreadPoolMetric(tp.getKey(), tp.getValue(), "PendingTasks"));
+            threadPool.put("CompletedTasks", probe.getThreadPoolMetric(tp.getKey(), tp.getValue(), "CompletedTasks"));
+            threadPool.put("CurrentlyBlockedTasks", probe.getThreadPoolMetric(tp.getKey(), tp.getValue(), "CurrentlyBlockedTasks"));
+            threadPool.put("TotalBlockedTasks", probe.getThreadPoolMetric(tp.getKey(), tp.getValue(), "TotalBlockedTasks"));
+            threadPools.put(tp.getValue(), threadPool);
+        }
+        result.put("ThreadPools", threadPools);
+
+        for (Map.Entry<String, Integer> entry : probe.getDroppedMessages().entrySet())
+            droppedMessage.put(entry.getKey(), entry.getValue());
+        result.put("DroppedMessage", droppedMessage);
+
+        return result;
+    }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/TpStatsPrinter.java b/src/java/org/apache/cassandra/tools/nodetool/stats/TpStatsPrinter.java
new file mode 100644
index 0000000..b874746
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/TpStatsPrinter.java
@@ -0,0 +1,73 @@
+/*
+ * 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.tools.nodetool.stats;
+
+import java.io.PrintStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class TpStatsPrinter
+{
+    public static StatsPrinter from(String format)
+    {
+        switch (format)
+        {
+            case "json":
+                return new StatsPrinter.JsonPrinter();
+            case "yaml":
+                return new StatsPrinter.YamlPrinter();
+            default:
+                return new DefaultPrinter();
+        }
+
+    }
+
+    public static class DefaultPrinter implements StatsPrinter<TpStatsHolder>
+    {
+        @Override
+        public void print(TpStatsHolder data, PrintStream out)
+        {
+            Map<String, Object> convertData = data.convert2Map();
+
+            out.printf("%-30s%10s%10s%15s%10s%18s%n", "Pool Name", "Active", "Pending", "Completed", "Blocked", "All time blocked");
+
+            Map<Object, Object> threadPools = convertData.get("ThreadPools") instanceof Map<?, ?> ? (Map)convertData.get("ThreadPools") : Collections.emptyMap();
+            for (Map.Entry<Object, Object> entry : threadPools.entrySet())
+            {
+                Map values = entry.getValue() instanceof Map<?, ?> ? (Map)entry.getValue() : Collections.emptyMap();
+                out.printf("%-30s%10s%10s%15s%10s%18s%n",
+                           entry.getKey(),
+                           values.get("ActiveTasks"),
+                           values.get("PendingTasks"),
+                           values.get("CompletedTasks"),
+                           values.get("CurrentlyBlockedTasks"),
+                           values.get("TotalBlockedTasks"));
+            }
+
+            out.printf("%n%-20s%10s%n", "Message type", "Dropped");
+
+            Map<Object, Object> droppedMessages = convertData.get("DroppedMessage") instanceof Map<?, ?> ? (Map)convertData.get("DroppedMessage") : Collections.emptyMap();
+            for (Map.Entry<Object, Object> entry : droppedMessages.entrySet())
+            {
+                out.printf("%-20s%10s%n", entry.getKey(), entry.getValue());
+            }
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/tracing/ExpiredTraceState.java b/src/java/org/apache/cassandra/tracing/ExpiredTraceState.java
index 5cc3c21..9230d38 100644
--- a/src/java/org/apache/cassandra/tracing/ExpiredTraceState.java
+++ b/src/java/org/apache/cassandra/tracing/ExpiredTraceState.java
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  * 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
@@ -7,33 +7,49 @@
  * 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.tracing;
 
-import java.util.UUID;
-
 import org.apache.cassandra.utils.FBUtilities;
 
-public class ExpiredTraceState extends TraceState
+class ExpiredTraceState extends TraceState
 {
-    public ExpiredTraceState(UUID sessionId, Tracing.TraceType traceType)
+    private final TraceState delegate;
+
+    ExpiredTraceState(TraceState delegate)
     {
-        super(FBUtilities.getBroadcastAddress(), sessionId, traceType);
+        super(FBUtilities.getBroadcastAddress(), delegate.sessionId, delegate.traceType);
+        this.delegate = delegate;
     }
 
     public int elapsed()
     {
         return -1;
     }
+
+    protected void traceImpl(String message)
+    {
+        delegate.traceImpl(message);
+    }
+
+    protected void waitForPendingEvents()
+    {
+        delegate.waitForPendingEvents();
+    }
+
+    TraceState getDelegate()
+    {
+        return delegate;
+    }
 }
diff --git a/src/java/org/apache/cassandra/tracing/TraceKeyspace.java b/src/java/org/apache/cassandra/tracing/TraceKeyspace.java
index fb70451..0d7c4f1 100644
--- a/src/java/org/apache/cassandra/tracing/TraceKeyspace.java
+++ b/src/java/org/apache/cassandra/tracing/TraceKeyspace.java
@@ -22,8 +22,10 @@
 import java.util.*;
 
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.db.Mutation;
-import org.apache.cassandra.db.RowUpdateBuilder;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.schema.KeyspaceMetadata;
 import org.apache.cassandra.schema.KeyspaceParams;
 import org.apache.cassandra.schema.Tables;
@@ -36,8 +38,6 @@
     {
     }
 
-    public static final String NAME = "system_traces";
-
     /**
      * Generation is used as a timestamp for automatic table creation on startup.
      * If you make any changes to the tables below, make sure to increment the
@@ -87,13 +87,13 @@
 
     private static CFMetaData compile(String name, String description, String schema)
     {
-        return CFMetaData.compile(String.format(schema, name), NAME)
+        return CFMetaData.compile(String.format(schema, name), SchemaConstants.TRACE_KEYSPACE_NAME)
                          .comment(description);
     }
 
     public static KeyspaceMetadata metadata()
     {
-        return KeyspaceMetadata.create(NAME, KeyspaceParams.simple(2), Tables.of(Sessions, Events));
+        return KeyspaceMetadata.create(SchemaConstants.TRACE_KEYSPACE_NAME, KeyspaceParams.simple(2), Tables.of(Sessions, Events));
     }
 
     static Mutation makeStartSessionMutation(ByteBuffer sessionId,
@@ -104,36 +104,41 @@
                                              String command,
                                              int ttl)
     {
-        RowUpdateBuilder adder = new RowUpdateBuilder(Sessions, FBUtilities.timestampMicros(), ttl, sessionId)
-                                 .clustering()
-                                 .add("client", client)
-                                 .add("coordinator", FBUtilities.getBroadcastAddress())
-                                 .add("request", request)
-                                 .add("started_at", new Date(startedAt))
-                                 .add("command", command);
+        PartitionUpdate.SimpleBuilder builder = PartitionUpdate.simpleBuilder(Sessions, sessionId);
+        builder.row()
+               .ttl(ttl)
+               .add("client", client)
+               .add("coordinator", FBUtilities.getBroadcastAddress())
+               .add("request", request)
+               .add("started_at", new Date(startedAt))
+               .add("command", command)
+               .appendAll("parameters", parameters);
 
-        for (Map.Entry<String, String> entry : parameters.entrySet())
-            adder.addMapEntry("parameters", entry.getKey(), entry.getValue());
-        return adder.build();
+        return builder.buildAsMutation();
     }
 
     static Mutation makeStopSessionMutation(ByteBuffer sessionId, int elapsed, int ttl)
     {
-        return new RowUpdateBuilder(Sessions, FBUtilities.timestampMicros(), ttl, sessionId)
-               .clustering()
-               .add("duration", elapsed)
-               .build();
+        PartitionUpdate.SimpleBuilder builder = PartitionUpdate.simpleBuilder(Sessions, sessionId);
+        builder.row()
+               .ttl(ttl)
+               .add("duration", elapsed);
+        return builder.buildAsMutation();
     }
 
     static Mutation makeEventMutation(ByteBuffer sessionId, String message, int elapsed, String threadName, int ttl)
     {
-        RowUpdateBuilder adder = new RowUpdateBuilder(Events, FBUtilities.timestampMicros(), ttl, sessionId)
-                                 .clustering(UUIDGen.getTimeUUID());
-        adder.add("activity", message);
-        adder.add("source", FBUtilities.getBroadcastAddress());
-        adder.add("thread", threadName);
+        PartitionUpdate.SimpleBuilder builder = PartitionUpdate.simpleBuilder(Events, sessionId);
+        Row.SimpleBuilder rowBuilder = builder.row(UUIDGen.getTimeUUID())
+                                              .ttl(ttl);
+
+        rowBuilder.add("activity", message)
+                  .add("source", FBUtilities.getBroadcastAddress())
+                  .add("thread", threadName);
+
         if (elapsed >= 0)
-            adder.add("source_elapsed", elapsed);
-        return adder.build();
+            rowBuilder.add("source_elapsed", elapsed);
+
+        return builder.buildAsMutation();
     }
 }
diff --git a/src/java/org/apache/cassandra/tracing/TraceState.java b/src/java/org/apache/cassandra/tracing/TraceState.java
index 88f36d8..b4eff6b 100644
--- a/src/java/org/apache/cassandra/tracing/TraceState.java
+++ b/src/java/org/apache/cassandra/tracing/TraceState.java
@@ -19,7 +19,6 @@
 
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
-import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -27,19 +26,9 @@
 import java.util.concurrent.atomic.AtomicInteger;
 
 import com.google.common.base.Stopwatch;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.slf4j.helpers.MessageFormatter;
 
-import org.apache.cassandra.concurrent.Stage;
-import org.apache.cassandra.concurrent.StageManager;
-import org.apache.cassandra.db.ConsistencyLevel;
-import org.apache.cassandra.db.Mutation;
-import org.apache.cassandra.exceptions.OverloadedException;
-import org.apache.cassandra.service.StorageProxy;
 import org.apache.cassandra.utils.ByteBufferUtil;
-import org.apache.cassandra.utils.JVMStabilityInspector;
-import org.apache.cassandra.utils.WrappedRunnable;
 import org.apache.cassandra.utils.progress.ProgressEvent;
 import org.apache.cassandra.utils.progress.ProgressEventNotifier;
 import org.apache.cassandra.utils.progress.ProgressListener;
@@ -48,12 +37,8 @@
  * ThreadLocal state for a tracing session. The presence of an instance of this class as a ThreadLocal denotes that an
  * operation is being traced.
  */
-public class TraceState implements ProgressEventNotifier
+public abstract class TraceState implements ProgressEventNotifier
 {
-    private static final Logger logger = LoggerFactory.getLogger(TraceState.class);
-    private static final int WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS =
-    Integer.valueOf(System.getProperty("cassandra.wait_for_tracing_events_timeout_secs", "0"));
-
     public final UUID sessionId;
     public final InetAddress coordinator;
     public final Stopwatch watch;
@@ -78,7 +63,7 @@
     // See CASSANDRA-7626 for more details.
     private final AtomicInteger references = new AtomicInteger(1);
 
-    public TraceState(InetAddress coordinator, UUID sessionId, Tracing.TraceType traceType)
+    protected TraceState(InetAddress coordinator, UUID sessionId, Tracing.TraceType traceType)
     {
         assert coordinator != null;
         assert sessionId != null;
@@ -90,7 +75,7 @@
         this.ttl = traceType.getTTL();
         watch = Stopwatch.createStarted();
         this.status = Status.IDLE;
-}
+    }
 
     /**
      * Activate notification with provided {@code tag} name.
@@ -160,7 +145,7 @@
         return status;
     }
 
-    private synchronized void notifyActivity()
+    protected synchronized void notifyActivity()
     {
         status = Status.ACTIVE;
         notifyAll();
@@ -186,12 +171,7 @@
         if (notify)
             notifyActivity();
 
-        final String threadName = Thread.currentThread().getName();
-        final int elapsed = elapsed();
-
-        executeMutation(TraceKeyspace.makeEventMutation(sessionIdBytes, message, elapsed, threadName, ttl));
-        if (logger.isTraceEnabled())
-            logger.trace("Adding <{}> to trace events", message);
+        traceImpl(message);
 
         for (ProgressListener listener : listeners)
         {
@@ -199,73 +179,13 @@
         }
     }
 
-    static void executeMutation(final Mutation mutation)
-    {
-        StageManager.getStage(Stage.TRACING).execute(new WrappedRunnable()
-        {
-            protected void runMayThrow() throws Exception
-            {
-                mutateWithCatch(mutation);
-            }
-        });
-    }
+    protected abstract void traceImpl(String message);
 
-    /**
-     * Called from {@link org.apache.cassandra.net.OutboundTcpConnection} for non-local traces (traces
-     * that are not initiated by local node == coordinator).
-     */
-    public static void mutateWithTracing(final ByteBuffer sessionId, final String message, final int elapsed, final int ttl)
-    {
-        final String threadName = Thread.currentThread().getName();
-
-        StageManager.getStage(Stage.TRACING).execute(new WrappedRunnable()
-        {
-            public void runMayThrow()
-            {
-                mutateWithCatch(TraceKeyspace.makeEventMutation(sessionId, message, elapsed, threadName, ttl));
-            }
-        });
-    }
-
-    static void mutateWithCatch(Mutation mutation)
-    {
-        try
-        {
-            StorageProxy.mutate(Collections.singletonList(mutation), ConsistencyLevel.ANY);
-        }
-        catch (OverloadedException e)
-        {
-            Tracing.logger.warn("Too many nodes are overloaded to save trace events");
-        }
-    }
-
-    /**
-     * Post a no-op event to the TRACING stage, so that we can be sure that any previous mutations
-     * have at least been applied to one replica. This works because the tracking executor only
-     * has one thread in its pool, see {@link StageManager#tracingExecutor()}.
-     */
     protected void waitForPendingEvents()
     {
-        if (WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS <= 0)
-            return;
-
-        try
-        {
-            if (logger.isTraceEnabled())
-                logger.trace("Waiting for up to {} seconds for trace events to complete",
-                             +WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS);
-
-            StageManager.getStage(Stage.TRACING).submit(StageManager.NO_OP_TASK)
-                        .get(WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS, TimeUnit.SECONDS);
-        }
-        catch (Throwable t)
-        {
-            JVMStabilityInspector.inspectThrowable(t);
-            logger.debug("Failed to wait for tracing events to complete: {}", t);
-        }
+        // if tracing events are asynchronous, then you can use this method to wait for them to complete
     }
 
-
     public boolean acquireReference()
     {
         while (true)
diff --git a/src/java/org/apache/cassandra/tracing/TraceStateImpl.java b/src/java/org/apache/cassandra/tracing/TraceStateImpl.java
new file mode 100644
index 0000000..349000a
--- /dev/null
+++ b/src/java/org/apache/cassandra/tracing/TraceStateImpl.java
@@ -0,0 +1,130 @@
+/*
+ * 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.tracing;
+
+import java.net.InetAddress;
+import java.util.Collections;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.concurrent.StageManager;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.exceptions.OverloadedException;
+import org.apache.cassandra.service.StorageProxy;
+import org.apache.cassandra.utils.JVMStabilityInspector;
+import org.apache.cassandra.utils.WrappedRunnable;
+
+/**
+ * ThreadLocal state for a tracing session. The presence of an instance of this class as a ThreadLocal denotes that an
+ * operation is being traced.
+ */
+public class TraceStateImpl extends TraceState
+{
+    private static final Logger logger = LoggerFactory.getLogger(TraceStateImpl.class);
+
+    @VisibleForTesting
+    public static int WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS =
+      Integer.parseInt(System.getProperty("cassandra.wait_for_tracing_events_timeout_secs", "0"));
+
+    private final Set<Future<?>> pendingFutures = ConcurrentHashMap.newKeySet();
+
+    public TraceStateImpl(InetAddress coordinator, UUID sessionId, Tracing.TraceType traceType)
+    {
+        super(coordinator, sessionId, traceType);
+    }
+
+    protected void traceImpl(String message)
+    {
+        final String threadName = Thread.currentThread().getName();
+        final int elapsed = elapsed();
+
+        executeMutation(TraceKeyspace.makeEventMutation(sessionIdBytes, message, elapsed, threadName, ttl));
+        if (logger.isTraceEnabled())
+            logger.trace("Adding <{}> to trace events", message);
+    }
+
+    /**
+     * Wait on submitted futures
+     */
+    protected void waitForPendingEvents()
+    {
+        if (WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS <= 0)
+            return;
+
+        try
+        {
+            if (logger.isTraceEnabled())
+                logger.trace("Waiting for up to {} seconds for {} trace events to complete",
+                             +WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS, pendingFutures.size());
+
+            CompletableFuture.allOf(pendingFutures.toArray(new CompletableFuture<?>[pendingFutures.size()]))
+                             .get(WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS, TimeUnit.SECONDS);
+        }
+        catch (TimeoutException ex)
+        {
+            if (logger.isTraceEnabled())
+                logger.trace("Failed to wait for tracing events to complete in {} seconds",
+                             WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS);
+        }
+        catch (Throwable t)
+        {
+            JVMStabilityInspector.inspectThrowable(t);
+            logger.error("Got exception whilst waiting for tracing events to complete", t);
+        }
+    }
+
+
+    void executeMutation(final Mutation mutation)
+    {
+        CompletableFuture<Void> fut = CompletableFuture.runAsync(new WrappedRunnable()
+        {
+            protected void runMayThrow()
+            {
+                mutateWithCatch(mutation);
+            }
+        }, StageManager.getStage(Stage.TRACING));
+
+        boolean ret = pendingFutures.add(fut);
+        if (!ret)
+            logger.warn("Failed to insert pending future, tracing synchronization may not work");
+    }
+
+    static void mutateWithCatch(Mutation mutation)
+    {
+        try
+        {
+            StorageProxy.mutate(Collections.singletonList(mutation), ConsistencyLevel.ANY, System.nanoTime());
+        }
+        catch (OverloadedException e)
+        {
+            Tracing.logger.warn("Too many nodes are overloaded to save trace events");
+        }
+    }
+
+}
diff --git a/src/java/org/apache/cassandra/tracing/Tracing.java b/src/java/org/apache/cassandra/tracing/Tracing.java
index 9dae6c2..8c08151 100644
--- a/src/java/org/apache/cassandra/tracing/Tracing.java
+++ b/src/java/org/apache/cassandra/tracing/Tracing.java
@@ -21,28 +21,32 @@
 
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
+import java.util.Collections;
 import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 
+import com.google.common.collect.ImmutableMap;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import io.netty.util.concurrent.FastThreadLocal;
 import org.apache.cassandra.concurrent.ExecutorLocal;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.marshal.TimeUUIDType;
 import org.apache.cassandra.net.MessageIn;
 import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.UUIDGen;
 
 
 /**
  * A trace session context. Able to track and store trace sessions. A session is usually a user initiated query, and may
- * have multiple local and remote events before it is completed. All events and sessions are stored at keyspace.
+ * have multiple local and remote events before it is completed.
  */
-public class Tracing implements ExecutorLocal<TraceState>
+public abstract class Tracing implements ExecutorLocal<TraceState>
 {
     public static final String TRACE_HEADER = "TraceSession";
     public static final String TRACE_TYPE = "TraceType";
@@ -78,15 +82,35 @@
         }
     }
 
-    static final Logger logger = LoggerFactory.getLogger(Tracing.class);
+    protected static final Logger logger = LoggerFactory.getLogger(Tracing.class);
 
     private final InetAddress localAddress = FBUtilities.getLocalAddress();
 
-    private final ThreadLocal<TraceState> state = new ThreadLocal<>();
+    private final FastThreadLocal<TraceState> state = new FastThreadLocal<>();
 
-    private final ConcurrentMap<UUID, TraceState> sessions = new ConcurrentHashMap<>();
+    protected final ConcurrentMap<UUID, TraceState> sessions = new ConcurrentHashMap<>();
 
-    public static final Tracing instance = new Tracing();
+    public static final Tracing instance;
+
+    static
+    {
+        Tracing tracing = null;
+        String customTracingClass = System.getProperty("cassandra.custom_tracing_class");
+        if (null != customTracingClass)
+        {
+            try
+            {
+                tracing = FBUtilities.construct(customTracingClass, "Tracing");
+                logger.info("Using {} as tracing queries (as requested with -Dcassandra.custom_tracing_class)", customTracingClass);
+            }
+            catch (Exception e)
+            {
+                JVMStabilityInspector.inspectThrowable(e);
+                logger.error(String.format("Cannot use class %s for tracing, ignoring by defaulting to normal tracing", customTracingClass), e);
+            }
+        }
+        instance = null != tracing ? tracing : new TracingImpl();
+    }
 
     public UUID getSessionId()
     {
@@ -111,30 +135,37 @@
      */
     public static boolean isTracing()
     {
-        return instance.state.get() != null;
+        return instance.get() != null;
     }
 
-    public UUID newSession()
+    public UUID newSession(Map<String,ByteBuffer> customPayload)
     {
-        return newSession(TraceType.QUERY);
+        return newSession(
+                TimeUUIDType.instance.compose(ByteBuffer.wrap(UUIDGen.getTimeUUIDBytes())),
+                TraceType.QUERY,
+                customPayload);
     }
 
     public UUID newSession(TraceType traceType)
     {
-        return newSession(TimeUUIDType.instance.compose(ByteBuffer.wrap(UUIDGen.getTimeUUIDBytes())), traceType);
+        return newSession(
+                TimeUUIDType.instance.compose(ByteBuffer.wrap(UUIDGen.getTimeUUIDBytes())),
+                traceType,
+                Collections.EMPTY_MAP);
     }
 
-    public UUID newSession(UUID sessionId)
+    public UUID newSession(UUID sessionId, Map<String,ByteBuffer> customPayload)
     {
-        return newSession(sessionId, TraceType.QUERY);
+        return newSession(sessionId, TraceType.QUERY, customPayload);
     }
 
-    private UUID newSession(UUID sessionId, TraceType traceType)
+    /** This method is intended to be overridden in tracing implementations that need access to the customPayload */
+    protected UUID newSession(UUID sessionId, TraceType traceType, Map<String,ByteBuffer> customPayload)
     {
-        assert state.get() == null;
+        assert get() == null;
 
-        TraceState ts = new TraceState(localAddress, sessionId, traceType);
-        state.set(ts);
+        TraceState ts = newTraceState(localAddress, sessionId, traceType);
+        set(ts);
         sessions.put(sessionId, ts);
 
         return sessionId;
@@ -146,30 +177,29 @@
             sessions.remove(state.sessionId);
     }
 
+
     /**
      * Stop the session and record its complete.  Called by coodinator when request is complete.
      */
     public void stopSession()
     {
-        TraceState state = this.state.get();
+        TraceState state = get();
         if (state == null) // inline isTracing to avoid implicit two calls to state.get()
         {
             logger.trace("request complete");
         }
         else
         {
-            final int elapsed = state.elapsed();
-            final ByteBuffer sessionId = state.sessionIdBytes;
-            final int ttl = state.ttl;
-
-            TraceState.executeMutation(TraceKeyspace.makeStopSessionMutation(sessionId, elapsed, ttl));
+            stopSessionImpl();
 
             state.stop();
             sessions.remove(state.sessionId);
-            this.state.set(null);
+            set(null);
         }
     }
 
+    protected abstract void stopSessionImpl();
+
     public TraceState get()
     {
         return state.get();
@@ -190,24 +220,11 @@
         return begin(request, null, parameters);
     }
 
-    public TraceState begin(final String request, final InetAddress client, final Map<String, String> parameters)
-    {
-        assert isTracing();
-
-        final TraceState state = this.state.get();
-        final long startedAt = System.currentTimeMillis();
-        final ByteBuffer sessionId = state.sessionIdBytes;
-        final String command = state.traceType.toString();
-        final int ttl = state.ttl;
-
-        TraceState.executeMutation(TraceKeyspace.makeStartSessionMutation(sessionId, client, parameters, request, startedAt, command, ttl));
-
-        return state;
-    }
+    public abstract TraceState begin(String request, InetAddress client, Map<String, String> parameters);
 
     /**
      * Determines the tracing context from a message.  Does NOT set the threadlocal state.
-     * 
+     *
      * @param message The internode message
      */
     public TraceState initializeFromMessage(final MessageIn<?> message)
@@ -219,7 +236,7 @@
 
         assert sessionBytes.length == 16;
         UUID sessionId = UUIDGen.getUUID(ByteBuffer.wrap(sessionBytes));
-        TraceState ts = sessions.get(sessionId);
+        TraceState ts = get(sessionId);
         if (ts != null && ts.acquireReference())
             return ts;
 
@@ -231,16 +248,26 @@
         if (message.verb == MessagingService.Verb.REQUEST_RESPONSE)
         {
             // received a message for a session we've already closed out.  see CASSANDRA-5668
-            return new ExpiredTraceState(sessionId, traceType);
+            return new ExpiredTraceState(newTraceState(message.from, sessionId, traceType));
         }
         else
         {
-            ts = new TraceState(message.from, sessionId, traceType);
+            ts = newTraceState(message.from, sessionId, traceType);
             sessions.put(sessionId, ts);
             return ts;
         }
     }
 
+    public Map<String, byte[]> getTraceHeaders()
+    {
+        assert isTracing();
+
+        return ImmutableMap.of(
+                TRACE_HEADER, UUIDGen.decompose(Tracing.instance.getSessionId()),
+                TRACE_TYPE, new byte[] { Tracing.TraceType.serialize(Tracing.instance.getTraceType()) });
+    }
+
+    protected abstract TraceState newTraceState(InetAddress coordinator, UUID sessionId, Tracing.TraceType traceType);
 
     // repair just gets a varargs method since it's so heavyweight anyway
     public static void traceRepair(String format, Object... args)
@@ -288,4 +315,10 @@
 
         state.trace(format, args);
     }
+
+    /**
+     * Called from {@link org.apache.cassandra.net.OutboundTcpConnection} for non-local traces (traces
+     * that are not initiated by local node == coordinator).
+     */
+    public abstract void trace(ByteBuffer sessionId, String message, int ttl);
 }
diff --git a/src/java/org/apache/cassandra/tracing/TracingImpl.java b/src/java/org/apache/cassandra/tracing/TracingImpl.java
new file mode 100644
index 0000000..d774abb
--- /dev/null
+++ b/src/java/org/apache/cassandra/tracing/TracingImpl.java
@@ -0,0 +1,117 @@
+/*
+ * 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.tracing;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.concurrent.StageManager;
+import org.apache.cassandra.utils.WrappedRunnable;
+
+
+/**
+ * A trace session context. Able to track and store trace sessions. A session is usually a user initiated query, and may
+ * have multiple local and remote events before it is completed. All events and sessions are stored at keyspace.
+ */
+class TracingImpl extends Tracing
+{
+    public void stopSessionImpl()
+    {
+        final TraceStateImpl state = getStateImpl();
+        if (state == null)
+            return;
+
+        int elapsed = state.elapsed();
+        ByteBuffer sessionId = state.sessionIdBytes;
+        int ttl = state.ttl;
+
+        state.executeMutation(TraceKeyspace.makeStopSessionMutation(sessionId, elapsed, ttl));
+    }
+
+    public TraceState begin(final String request, final InetAddress client, final Map<String, String> parameters)
+    {
+        assert isTracing();
+
+        final TraceStateImpl state = getStateImpl();
+        assert state != null;
+
+        final long startedAt = System.currentTimeMillis();
+        final ByteBuffer sessionId = state.sessionIdBytes;
+        final String command = state.traceType.toString();
+        final int ttl = state.ttl;
+
+        state.executeMutation(TraceKeyspace.makeStartSessionMutation(sessionId, client, parameters, request, startedAt, command, ttl));
+        return state;
+    }
+
+    /**
+     * Convert the abstract tracing state to its implementation.
+     *
+     * Expired states are not put in the sessions but the check is for extra safety.
+     *
+     * @return the state converted to its implementation, or null
+     */
+    private TraceStateImpl getStateImpl()
+    {
+        TraceState state = get();
+        if (state == null)
+            return null;
+
+        if (state instanceof ExpiredTraceState)
+        {
+            ExpiredTraceState expiredTraceState = (ExpiredTraceState) state;
+            state = expiredTraceState.getDelegate();
+        }
+
+        if (state instanceof TraceStateImpl)
+        {
+            return (TraceStateImpl)state;
+        }
+
+        assert false : "TracingImpl states should be of type TraceStateImpl";
+        return null;
+    }
+
+    @Override
+    protected TraceState newTraceState(InetAddress coordinator, UUID sessionId, TraceType traceType)
+    {
+        return new TraceStateImpl(coordinator, sessionId, traceType);
+    }
+
+    /**
+     * Called from {@link org.apache.cassandra.net.OutboundTcpConnection} for non-local traces (traces
+     * that are not initiated by local node == coordinator).
+     */
+    public void trace(final ByteBuffer sessionId, final String message, final int ttl)
+    {
+        final String threadName = Thread.currentThread().getName();
+
+        StageManager.getStage(Stage.TRACING).execute(new WrappedRunnable()
+        {
+            public void runMayThrow()
+            {
+                TraceStateImpl.mutateWithCatch(TraceKeyspace.makeEventMutation(sessionId, message, -1, threadName, ttl));
+            }
+        });
+    }
+}
diff --git a/src/java/org/apache/cassandra/transport/CBCodec.java b/src/java/org/apache/cassandra/transport/CBCodec.java
index 0ef619e..9b0847b 100644
--- a/src/java/org/apache/cassandra/transport/CBCodec.java
+++ b/src/java/org/apache/cassandra/transport/CBCodec.java
@@ -21,7 +21,7 @@
 
 public interface CBCodec<T>
 {
-    public T decode(ByteBuf body, int version);
-    public void encode(T t, ByteBuf dest, int version);
-    public int encodedSize(T t, int version);
+    public T decode(ByteBuf body, ProtocolVersion version);
+    public void encode(T t, ByteBuf dest, ProtocolVersion version);
+    public int encodedSize(T t, ProtocolVersion version);
 }
diff --git a/src/java/org/apache/cassandra/transport/CBUtil.java b/src/java/org/apache/cassandra/transport/CBUtil.java
index 800a9a8..66e5e73 100644
--- a/src/java/org/apache/cassandra/transport/CBUtil.java
+++ b/src/java/org/apache/cassandra/transport/CBUtil.java
@@ -33,15 +33,19 @@
 import java.util.Map;
 import java.util.UUID;
 
-import io.netty.buffer.*;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.PooledByteBufAllocator;
+import io.netty.buffer.UnpooledByteBufAllocator;
 import io.netty.util.CharsetUtil;
-
+import io.netty.util.concurrent.FastThreadLocal;
 import org.apache.cassandra.config.Config;
 import org.apache.cassandra.db.ConsistencyLevel;
 import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.Pair;
 import org.apache.cassandra.utils.UUIDGen;
-import org.apache.cassandra.utils.ByteBufferUtil;
 
 /**
  * ByteBuf utility methods.
@@ -55,9 +59,7 @@
     public static final boolean USE_HEAP_ALLOCATOR = Boolean.getBoolean(Config.PROPERTY_PREFIX + "netty_use_heap_allocator");
     public static final ByteBufAllocator allocator = USE_HEAP_ALLOCATOR ? new UnpooledByteBufAllocator(false) : new PooledByteBufAllocator(true);
 
-    private CBUtil() {}
-
-    private final static ThreadLocal<CharsetDecoder> decoder = new ThreadLocal<CharsetDecoder>()
+    private final static FastThreadLocal<CharsetDecoder> TL_UTF8_DECODER = new FastThreadLocal<CharsetDecoder>()
     {
         @Override
         protected CharsetDecoder initialValue()
@@ -66,6 +68,43 @@
         }
     };
 
+    private final static FastThreadLocal<CharBuffer> TL_CHAR_BUFFER = new FastThreadLocal<>();
+
+    private CBUtil() {}
+
+
+    // Taken from Netty's ChannelBuffers.decodeString(). We need to use our own decoder to properly handle invalid
+    // UTF-8 sequences.  See CASSANDRA-8101 for more details.  This can be removed once https://github.com/netty/netty/pull/2999
+    // is resolved in a release used by Cassandra.
+    private static String decodeString(ByteBuffer src) throws CharacterCodingException
+    {
+        // the decoder needs to be reset every time we use it, hence the copy per thread
+        CharsetDecoder theDecoder = TL_UTF8_DECODER.get();
+        theDecoder.reset();
+        CharBuffer dst = TL_CHAR_BUFFER.get();
+        int capacity = (int) ((double) src.remaining() * theDecoder.maxCharsPerByte());
+        if (dst == null)
+        {
+            capacity = Math.max(capacity, 4096);
+            dst = CharBuffer.allocate(capacity);
+            TL_CHAR_BUFFER.set(dst);
+        }
+        else
+        {
+            dst.clear();
+            if (dst.capacity() < capacity)
+            {
+                dst = CharBuffer.allocate(capacity);
+                TL_CHAR_BUFFER.set(dst);
+            }
+        }
+        CoderResult cr = theDecoder.decode(src, dst, true);
+        if (!cr.isUnderflow())
+            cr.throwException();
+
+        return dst.flip().toString();
+    }
+
     private static String readString(ByteBuf cb, int length)
     {
         if (length == 0)
@@ -97,34 +136,12 @@
         }
     }
 
-    // Taken from Netty's ChannelBuffers.decodeString(). We need to use our own decoder to properly handle invalid
-    // UTF-8 sequences.  See CASSANDRA-8101 for more details.  This can be removed once https://github.com/netty/netty/pull/2999
-    // is resolved in a release used by Cassandra.
-    private static String decodeString(ByteBuffer src) throws CharacterCodingException
-    {
-        // the decoder needs to be reset every time we use it, hence the copy per thread
-        CharsetDecoder theDecoder = decoder.get();
-        theDecoder.reset();
-
-        final CharBuffer dst = CharBuffer.allocate(
-                (int) ((double) src.remaining() * theDecoder.maxCharsPerByte()));
-
-        CoderResult cr = theDecoder.decode(src, dst, true);
-        if (!cr.isUnderflow())
-            cr.throwException();
-
-        cr = theDecoder.flush(dst);
-        if (!cr.isUnderflow())
-            cr.throwException();
-
-        return dst.flip().toString();
-    }
-
     public static void writeString(String str, ByteBuf cb)
     {
-        byte[] bytes = str.getBytes(CharsetUtil.UTF_8);
-        cb.writeShort(bytes.length);
-        cb.writeBytes(bytes);
+        int writerIndex = cb.writerIndex();
+        cb.writeShort(0);
+        int lengthBytes = ByteBufUtil.writeUtf8(cb, str);
+        cb.setShort(writerIndex, lengthBytes);
     }
 
     public static int sizeOfString(String str)
@@ -374,12 +391,12 @@
         return ByteBuffer.wrap(readRawBytes(slice));
     }
 
-    public static ByteBuffer readBoundValue(ByteBuf cb, int protocolVersion)
+    public static ByteBuffer readBoundValue(ByteBuf cb, ProtocolVersion protocolVersion)
     {
         int length = cb.readInt();
         if (length < 0)
         {
-            if (protocolVersion < 4) // backward compatibility for pre-version 4
+            if (protocolVersion.isSmallerThan(ProtocolVersion.V4)) // backward compatibility for pre-version 4
                 return null;
             if (length == -1)
                 return null;
@@ -437,7 +454,7 @@
         return 4 + (valueSize < 0 ? 0 : valueSize);
     }
 
-    public static List<ByteBuffer> readValueList(ByteBuf cb, int protocolVersion)
+    public static List<ByteBuffer> readValueList(ByteBuf cb, ProtocolVersion protocolVersion)
     {
         int size = cb.readUnsignedShort();
         if (size == 0)
@@ -464,7 +481,7 @@
         return size;
     }
 
-    public static Pair<List<String>, List<ByteBuffer>> readNameAndValueList(ByteBuf cb, int protocolVersion)
+    public static Pair<List<String>, List<ByteBuffer>> readNameAndValueList(ByteBuf cb, ProtocolVersion protocolVersion)
     {
         int size = cb.readUnsignedShort();
         if (size == 0)
@@ -482,7 +499,7 @@
 
     public static InetSocketAddress readInet(ByteBuf cb)
     {
-        int addrSize = cb.readByte();
+        int addrSize = cb.readByte() & 0xFF;
         byte[] address = new byte[addrSize];
         cb.readBytes(address);
         int port = cb.readInt();
@@ -511,6 +528,33 @@
         return 1 + address.length + 4;
     }
 
+    public static InetAddress readInetAddr(ByteBuf cb)
+    {
+        int addressSize = cb.readByte() & 0xFF;
+        byte[] address = new byte[addressSize];
+        cb.readBytes(address);
+        try
+        {
+            return InetAddress.getByAddress(address);
+        }
+        catch (UnknownHostException e)
+        {
+            throw new ProtocolException("Invalid IP address while deserializing inet address");
+        }
+    }
+
+    public static void writeInetAddr(InetAddress inetAddr, ByteBuf cb)
+    {
+        byte[] address = inetAddr.getAddress();
+        cb.writeByte(address.length);
+        cb.writeBytes(address);
+    }
+
+    public static int sizeOfInetAddr(InetAddress inetAddr)
+    {
+        return 1 + inetAddr.getAddress().length;
+    }
+
     /*
      * Reads *all* readable bytes from {@code cb} and return them.
      */
diff --git a/src/java/org/apache/cassandra/transport/Client.java b/src/java/org/apache/cassandra/transport/Client.java
index 92466d2..368b1d7 100644
--- a/src/java/org/apache/cassandra/transport/Client.java
+++ b/src/java/org/apache/cassandra/transport/Client.java
@@ -27,7 +27,7 @@
 import com.google.common.base.Splitter;
 
 import org.apache.cassandra.auth.PasswordAuthenticator;
-import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.db.ConsistencyLevel;
 import org.apache.cassandra.db.marshal.Int32Type;
@@ -43,7 +43,7 @@
 {
     private final SimpleEventHandler eventHandler = new SimpleEventHandler();
 
-    public Client(String host, int port, int version, ClientEncryptionOptions encryptionOptions)
+    public Client(String host, int port, ProtocolVersion version, ClientEncryptionOptions encryptionOptions)
     {
         super(host, port, version, encryptionOptions);
         setEventHandler(eventHandler);
@@ -69,7 +69,8 @@
             System.out.print(">> ");
             System.out.flush();
             String line = in.readLine();
-            if (line == null) {
+            if (line == null)
+            {
                 break;
             }
             Message.Request req = parseLine(line.trim());
@@ -135,7 +136,7 @@
                     return null;
                 }
             }
-            return new QueryMessage(query, QueryOptions.create(ConsistencyLevel.ONE, Collections.<ByteBuffer>emptyList(), false, pageSize, null, null));
+            return new QueryMessage(query, QueryOptions.create(ConsistencyLevel.ONE, Collections.<ByteBuffer>emptyList(), false, pageSize, null, null, version));
         }
         else if (msgType.equals("PREPARE"))
         {
@@ -238,7 +239,7 @@
 
     public static void main(String[] args) throws Exception
     {
-        Config.setClientMode(true);
+        DatabaseDescriptor.clientInitialization();
 
         // Print usage if no argument is specified.
         if (args.length < 2 || args.length > 3)
@@ -250,7 +251,7 @@
         // Parse options.
         String host = args[0];
         int port = Integer.parseInt(args[1]);
-        int version = args.length == 3 ? Integer.parseInt(args[2]) : Server.CURRENT_VERSION;
+        ProtocolVersion version = args.length == 3 ? ProtocolVersion.decode(Integer.parseInt(args[2]), ProtocolVersionLimit.SERVER_DEFAULT) : ProtocolVersion.CURRENT;
 
         ClientEncryptionOptions encryptionOptions = new ClientEncryptionOptions();
         System.out.println("CQL binary protocol console " + host + "@" + port + " using native protocol version " + version);
diff --git a/src/java/org/apache/cassandra/transport/ConfiguredLimit.java b/src/java/org/apache/cassandra/transport/ConfiguredLimit.java
index 71e836f..0813aa7 100644
--- a/src/java/org/apache/cassandra/transport/ConfiguredLimit.java
+++ b/src/java/org/apache/cassandra/transport/ConfiguredLimit.java
@@ -31,36 +31,30 @@
     static final String DISABLE_MAX_PROTOCOL_AUTO_OVERRIDE = "cassandra.disable_max_protocol_auto_override";
     static final CassandraVersion MIN_VERSION_FOR_V4 = new CassandraVersion("3.0.0");
 
-    public abstract int getMaxVersion();
+    public abstract ProtocolVersion getMaxVersion();
     public abstract void updateMaxSupportedVersion();
 
     public static ConfiguredLimit newLimit()
     {
         if (Boolean.getBoolean(DISABLE_MAX_PROTOCOL_AUTO_OVERRIDE))
-            return new StaticLimit(Server.CURRENT_VERSION);
+            return new StaticLimit(ProtocolVersion.MAX_SUPPORTED_VERSION);
 
         int fromConfig = DatabaseDescriptor.getNativeProtocolMaxVersionOverride();
         return fromConfig != Integer.MIN_VALUE
-               ? new StaticLimit(fromConfig)
-               : new DynamicLimit(Server.CURRENT_VERSION);
+               ? new StaticLimit(ProtocolVersion.decode(fromConfig, ProtocolVersionLimit.SERVER_DEFAULT))
+               : new DynamicLimit(ProtocolVersion.MAX_SUPPORTED_VERSION);
     }
 
     private static class StaticLimit extends ConfiguredLimit
     {
-        private final int maxVersion;
-        private StaticLimit(int maxVersion)
+        private final ProtocolVersion maxVersion;
+        private StaticLimit(ProtocolVersion maxVersion)
         {
-            if (maxVersion < Server.MIN_SUPPORTED_VERSION || maxVersion > Server.CURRENT_VERSION)
-                throw new IllegalArgumentException(String.format("Invalid max protocol version supplied (%s); " +
-                                                                 "Values between %s and %s are supported",
-                                                                 maxVersion,
-                                                                 Server.MIN_SUPPORTED_VERSION,
-                                                                 Server.CURRENT_VERSION));
             this.maxVersion = maxVersion;
             logger.info("Native transport max negotiable version statically limited to {}", maxVersion);
         }
 
-        public int getMaxVersion()
+        public ProtocolVersion getMaxVersion()
         {
             return maxVersion;
         }
@@ -73,14 +67,14 @@
 
     private static class DynamicLimit extends ConfiguredLimit
     {
-        private volatile int maxVersion;
-        private DynamicLimit(int initialLimit)
+        private volatile ProtocolVersion maxVersion;
+        private DynamicLimit(ProtocolVersion initialLimit)
         {
             maxVersion = initialLimit;
             maybeUpdateVersion(true);
         }
 
-        public int getMaxVersion()
+        public ProtocolVersion getMaxVersion()
         {
             return maxVersion;
         }
@@ -100,19 +94,20 @@
 
             if (!enforceV3Cap)
             {
-                maxVersion = Server.CURRENT_VERSION;
+                maxVersion = ProtocolVersion.MAX_SUPPORTED_VERSION;
                 return;
             }
 
-            if (maxVersion > Server.VERSION_3 && !allowLowering)
+            if (ProtocolVersion.V3.isSmallerThan(maxVersion) && !allowLowering)
             {
                 logger.info("Detected peers which do not fully support protocol V4, but V4 was previously negotiable. " +
                             "Not enforcing cap as this can cause issues for older client versions. After the next " +
                             "restart the server will apply the cap");
                 return;
             }
+
             logger.info("Detected peers which do not fully support protocol V4. Capping max negotiable version to V3");
-            maxVersion = Server.VERSION_3;
+            maxVersion = ProtocolVersion.V3;
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/transport/Connection.java b/src/java/org/apache/cassandra/transport/Connection.java
index 2966d9b..7e17f46 100644
--- a/src/java/org/apache/cassandra/transport/Connection.java
+++ b/src/java/org/apache/cassandra/transport/Connection.java
@@ -25,13 +25,13 @@
     static final AttributeKey<Connection> attributeKey = AttributeKey.valueOf("CONN");
 
     private final Channel channel;
-    private final int version;
+    private final ProtocolVersion version;
     private final Tracker tracker;
 
     private volatile FrameCompressor frameCompressor;
     private boolean throwOnOverload;
 
-    public Connection(Channel channel, int version, Tracker tracker)
+    public Connection(Channel channel, ProtocolVersion version, Tracker tracker)
     {
         this.channel = channel;
         this.version = version;
@@ -65,7 +65,7 @@
         return tracker;
     }
 
-    public int getVersion()
+    public ProtocolVersion getVersion()
     {
         return version;
     }
@@ -77,7 +77,7 @@
 
     public interface Factory
     {
-        Connection newConnection(Channel channel, int version);
+        Connection newConnection(Channel channel, ProtocolVersion version);
     }
 
     public interface Tracker
diff --git a/src/java/org/apache/cassandra/transport/DataType.java b/src/java/org/apache/cassandra/transport/DataType.java
index e3eaf32..6456b74 100644
--- a/src/java/org/apache/cassandra/transport/DataType.java
+++ b/src/java/org/apache/cassandra/transport/DataType.java
@@ -28,44 +28,47 @@
 
 import io.netty.buffer.ByteBuf;
 
-import org.apache.cassandra.exceptions.RequestValidationException;
+import org.apache.cassandra.cql3.FieldIdentifier;
 import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.exceptions.RequestValidationException;
 import org.apache.cassandra.utils.Pair;
 
-public enum DataType implements OptionCodec.Codecable<DataType>
+public enum DataType
 {
-    CUSTOM   (0,  null, 1),
-    ASCII    (1,  AsciiType.instance, 1),
-    BIGINT   (2,  LongType.instance, 1),
-    BLOB     (3,  BytesType.instance, 1),
-    BOOLEAN  (4,  BooleanType.instance, 1),
-    COUNTER  (5,  CounterColumnType.instance, 1),
-    DECIMAL  (6,  DecimalType.instance, 1),
-    DOUBLE   (7,  DoubleType.instance, 1),
-    FLOAT    (8,  FloatType.instance, 1),
-    INT      (9,  Int32Type.instance, 1),
-    TEXT     (10, UTF8Type.instance, 1),
-    TIMESTAMP(11, TimestampType.instance, 1),
-    UUID     (12, UUIDType.instance, 1),
-    VARCHAR  (13, UTF8Type.instance, 1),
-    VARINT   (14, IntegerType.instance, 1),
-    TIMEUUID (15, TimeUUIDType.instance, 1),
-    INET     (16, InetAddressType.instance, 1),
-    DATE     (17, SimpleDateType.instance, 4),
-    TIME     (18, TimeType.instance, 4),
-    SMALLINT (19, ShortType.instance, 4),
-    BYTE     (20, ByteType.instance, 4),
-    LIST     (32, null, 1),
-    MAP      (33, null, 1),
-    SET      (34, null, 1),
-    UDT      (48, null, 3),
-    TUPLE    (49, null, 3);
+    CUSTOM   (0,  null, ProtocolVersion.V1),
+    ASCII    (1,  AsciiType.instance, ProtocolVersion.V1),
+    BIGINT   (2,  LongType.instance, ProtocolVersion.V1),
+    BLOB     (3,  BytesType.instance, ProtocolVersion.V1),
+    BOOLEAN  (4,  BooleanType.instance, ProtocolVersion.V1),
+    COUNTER  (5,  CounterColumnType.instance, ProtocolVersion.V1),
+    DECIMAL  (6,  DecimalType.instance, ProtocolVersion.V1),
+    DOUBLE   (7,  DoubleType.instance, ProtocolVersion.V1),
+    FLOAT    (8,  FloatType.instance, ProtocolVersion.V1),
+    INT      (9,  Int32Type.instance, ProtocolVersion.V1),
+    TEXT     (10, UTF8Type.instance, ProtocolVersion.V1),
+    TIMESTAMP(11, TimestampType.instance, ProtocolVersion.V1),
+    UUID     (12, UUIDType.instance, ProtocolVersion.V1),
+    VARCHAR  (13, UTF8Type.instance, ProtocolVersion.V1),
+    VARINT   (14, IntegerType.instance, ProtocolVersion.V1),
+    TIMEUUID (15, TimeUUIDType.instance, ProtocolVersion.V1),
+    INET     (16, InetAddressType.instance, ProtocolVersion.V1),
+    DATE     (17, SimpleDateType.instance, ProtocolVersion.V4),
+    TIME     (18, TimeType.instance, ProtocolVersion.V4),
+    SMALLINT (19, ShortType.instance, ProtocolVersion.V4),
+    BYTE     (20, ByteType.instance, ProtocolVersion.V4),
+    DURATION (21, DurationType.instance, ProtocolVersion.V5),
+    LIST     (32, null, ProtocolVersion.V1),
+    MAP      (33, null, ProtocolVersion.V1),
+    SET      (34, null, ProtocolVersion.V1),
+    UDT      (48, null, ProtocolVersion.V3),
+    TUPLE    (49, null, ProtocolVersion.V3);
 
-    public static final OptionCodec<DataType> codec = new OptionCodec<DataType>(DataType.class);
+    public static final Codec codec = new Codec();
 
     private final int id;
-    private final int protocolVersion;
+    private final ProtocolVersion protocolVersion;
     private final AbstractType type;
+    private final Pair<DataType, Object> pair;
     private static final Map<AbstractType, DataType> dataTypeMap = new HashMap<AbstractType, DataType>();
     static
     {
@@ -76,21 +79,22 @@
         }
     }
 
-    DataType(int id, AbstractType type, int protocolVersion)
+    DataType(int id, AbstractType type, ProtocolVersion protocolVersion)
     {
         this.id = id;
         this.type = type;
         this.protocolVersion = protocolVersion;
+        pair = Pair.create(this, null);
     }
 
-    public int getId(int version)
+    public int getId(ProtocolVersion version)
     {
-        if (version < protocolVersion)
+        if (version.isSmallerThan(protocolVersion))
             return DataType.CUSTOM.getId(version);
         return id;
     }
 
-    public Object readValue(ByteBuf cb, int version)
+    public Object readValue(ByteBuf cb, ProtocolVersion version)
     {
         switch (this)
         {
@@ -109,14 +113,14 @@
                 String ks = CBUtil.readString(cb);
                 ByteBuffer name = UTF8Type.instance.decompose(CBUtil.readString(cb));
                 int n = cb.readUnsignedShort();
-                List<ByteBuffer> fieldNames = new ArrayList<>(n);
+                List<FieldIdentifier> fieldNames = new ArrayList<>(n);
                 List<AbstractType<?>> fieldTypes = new ArrayList<>(n);
                 for (int i = 0; i < n; i++)
                 {
-                    fieldNames.add(UTF8Type.instance.decompose(CBUtil.readString(cb)));
+                    fieldNames.add(FieldIdentifier.forInternalString(CBUtil.readString(cb)));
                     fieldTypes.add(DataType.toType(codec.decodeOne(cb, version)));
                 }
-                return new UserType(ks, name, fieldNames, fieldTypes);
+                return new UserType(ks, name, fieldNames, fieldTypes, true);
             case TUPLE:
                 n = cb.readUnsignedShort();
                 List<AbstractType<?>> types = new ArrayList<>(n);
@@ -128,10 +132,10 @@
         }
     }
 
-    public void writeValue(Object value, ByteBuf cb, int version)
+    public void writeValue(Object value, ByteBuf cb, ProtocolVersion version)
     {
         // Serialize as CUSTOM if client on the other side's version is < required for type
-        if (version < protocolVersion)
+        if (version.isSmallerThan(protocolVersion))
         {
             CBUtil.writeString(value.toString(), cb);
             return;
@@ -161,7 +165,7 @@
                 cb.writeShort(udt.size());
                 for (int i = 0; i < udt.size(); i++)
                 {
-                    CBUtil.writeString(UTF8Type.instance.compose(udt.fieldName(i)), cb);
+                    CBUtil.writeString(udt.fieldName(i).toString(), cb);
                     codec.writeOne(DataType.fromType(udt.fieldType(i), version), cb, version);
                 }
                 break;
@@ -174,10 +178,10 @@
         }
     }
 
-    public int serializedValueSize(Object value, int version)
+    public int serializedValueSize(Object value, ProtocolVersion version)
     {
         // Serialize as CUSTOM if client on the other side's version is < required for type
-        if (version < protocolVersion)
+        if (version.isSmallerThan(protocolVersion))
             return CBUtil.sizeOfString(value.toString());
 
         switch (this)
@@ -201,7 +205,7 @@
                 size += 2;
                 for (int i = 0; i < udt.size(); i++)
                 {
-                    size += CBUtil.sizeOfString(UTF8Type.instance.compose(udt.fieldName(i)));
+                    size += CBUtil.sizeOfString(udt.fieldName(i).toString());
                     size += codec.oneSerializedSize(DataType.fromType(udt.fieldType(i), version), version);
                 }
                 return size;
@@ -216,7 +220,7 @@
         }
     }
 
-    public static Pair<DataType, Object> fromType(AbstractType type, int version)
+    public static Pair<DataType, Object> fromType(AbstractType type, ProtocolVersion version)
     {
         // For CQL3 clients, ReversedType is an implementation detail and they
         // shouldn't have to care about it.
@@ -248,10 +252,10 @@
                 throw new AssertionError();
             }
 
-            if (type instanceof UserType && version >= UDT.protocolVersion)
+            if (type instanceof UserType && version.isGreaterOrEqualTo(UDT.protocolVersion))
                 return Pair.<DataType, Object>create(UDT, type);
 
-            if (type instanceof TupleType && version >= TUPLE.protocolVersion)
+            if (type instanceof TupleType && version.isGreaterOrEqualTo(TUPLE.protocolVersion))
                 return Pair.<DataType, Object>create(TUPLE, type);
 
             return Pair.<DataType, Object>create(CUSTOM, type.toString());
@@ -259,9 +263,9 @@
         else
         {
             // Fall back to CUSTOM if target doesn't know this data type
-            if (version < dt.protocolVersion)
+            if (version.isSmallerThan(dt.protocolVersion))
                 return Pair.<DataType, Object>create(CUSTOM, type.toString());
-            return Pair.create(dt, null);
+            return dt.pair;
         }
     }
 
@@ -295,8 +299,65 @@
     }
 
     @VisibleForTesting
-    public int getProtocolVersion()
+    public ProtocolVersion getProtocolVersion()
     {
         return protocolVersion;
     }
+
+    public static final class Codec
+    {
+        private final DataType[] ids;
+
+        public Codec()
+        {
+            DataType[] values = DataType.values();
+            ids = new DataType[getMaxId(values) + 1];
+            for (DataType opt : values)
+            {
+                int id = opt.getId(opt.getProtocolVersion());
+                DataType existingType = ids[id];
+                if (existingType != null)
+                    throw new IllegalStateException(String.format("Duplicate option id %d", id));
+                ids[id] = opt;
+            }
+        }
+
+        private int getMaxId(DataType[] values)
+        {
+            int maxId = -1;
+            for (DataType opt : values)
+                maxId = Math.max(maxId, opt.getId(ProtocolVersion.CURRENT));
+            return maxId;
+        }
+
+        private DataType fromId(int id)
+        {
+            DataType opt = ids[id];
+            if (opt == null)
+                throw new ProtocolException(String.format("Unknown option id %d", id));
+            return opt;
+        }
+
+        public Pair<DataType, Object> decodeOne(ByteBuf body, ProtocolVersion version)
+        {
+            DataType opt = fromId(body.readUnsignedShort());
+            Object value = opt.readValue(body, version);
+            return Pair.create(opt, value);
+        }
+
+        public void writeOne(Pair<DataType, Object> option, ByteBuf dest, ProtocolVersion version)
+        {
+            DataType opt = option.left;
+            Object obj = option.right;
+            dest.writeShort(opt.getId(version));
+            opt.writeValue(obj, dest, version);
+        }
+
+        public int oneSerializedSize(Pair<DataType, Object> option, ProtocolVersion version)
+        {
+            DataType opt = option.left;
+            Object obj = option.right;
+            return 2 + opt.serializedValueSize(obj, version);
+        }
+    }
 }
diff --git a/src/java/org/apache/cassandra/transport/Event.java b/src/java/org/apache/cassandra/transport/Event.java
index 3c45c33..ed77e59 100644
--- a/src/java/org/apache/cassandra/transport/Event.java
+++ b/src/java/org/apache/cassandra/transport/Event.java
@@ -27,15 +27,16 @@
 
 public abstract class Event
 {
-    public enum Type {
-        TOPOLOGY_CHANGE(Server.VERSION_3),
-        STATUS_CHANGE(Server.VERSION_3),
-        SCHEMA_CHANGE(Server.VERSION_3),
-        TRACE_COMPLETE(Server.VERSION_4);
+    public enum Type
+    {
+        TOPOLOGY_CHANGE(ProtocolVersion.V3),
+        STATUS_CHANGE(ProtocolVersion.V3),
+        SCHEMA_CHANGE(ProtocolVersion.V3),
+        TRACE_COMPLETE(ProtocolVersion.V4);
 
-        public final int minimumVersion;
+        public final ProtocolVersion minimumVersion;
 
-        Type(int minimumVersion)
+        Type(ProtocolVersion minimumVersion)
         {
             this.minimumVersion = minimumVersion;
         }
@@ -48,10 +49,10 @@
         this.type = type;
     }
 
-    public static Event deserialize(ByteBuf cb, int version)
+    public static Event deserialize(ByteBuf cb, ProtocolVersion version)
     {
         Type eventType = CBUtil.readEnumValue(Type.class, cb);
-        if (eventType.minimumVersion > version)
+        if (eventType.minimumVersion.isGreaterThan(version))
             throw new ProtocolException("Event " + eventType.name() + " not valid for protocol version " + version);
         switch (eventType)
         {
@@ -65,21 +66,21 @@
         throw new AssertionError();
     }
 
-    public void serialize(ByteBuf dest, int version)
+    public void serialize(ByteBuf dest, ProtocolVersion version)
     {
-        if (type.minimumVersion > version)
+        if (type.minimumVersion.isGreaterThan(version))
             throw new ProtocolException("Event " + type.name() + " not valid for protocol version " + version);
         CBUtil.writeEnumValue(type, dest);
         serializeEvent(dest, version);
     }
 
-    public int serializedSize(int version)
+    public int serializedSize(ProtocolVersion version)
     {
         return CBUtil.sizeOfEnumValue(type) + eventSerializedSize(version);
     }
 
-    protected abstract void serializeEvent(ByteBuf dest, int version);
-    protected abstract int eventSerializedSize(int version);
+    protected abstract void serializeEvent(ByteBuf dest, ProtocolVersion version);
+    protected abstract int eventSerializedSize(ProtocolVersion version);
 
     public static abstract class NodeEvent extends Event
     {
@@ -125,20 +126,20 @@
         }
 
         // Assumes the type has already been deserialized
-        private static TopologyChange deserializeEvent(ByteBuf cb, int version)
+        private static TopologyChange deserializeEvent(ByteBuf cb, ProtocolVersion version)
         {
             Change change = CBUtil.readEnumValue(Change.class, cb);
             InetSocketAddress node = CBUtil.readInet(cb);
             return new TopologyChange(change, node);
         }
 
-        protected void serializeEvent(ByteBuf dest, int version)
+        protected void serializeEvent(ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeEnumValue(change, dest);
             CBUtil.writeInet(node, dest);
         }
 
-        protected int eventSerializedSize(int version)
+        protected int eventSerializedSize(ProtocolVersion version)
         {
             return CBUtil.sizeOfEnumValue(change) + CBUtil.sizeOfInet(node);
         }
@@ -191,20 +192,20 @@
         }
 
         // Assumes the type has already been deserialized
-        private static StatusChange deserializeEvent(ByteBuf cb, int version)
+        private static StatusChange deserializeEvent(ByteBuf cb, ProtocolVersion version)
         {
             Status status = CBUtil.readEnumValue(Status.class, cb);
             InetSocketAddress node = CBUtil.readInet(cb);
             return new StatusChange(status, node);
         }
 
-        protected void serializeEvent(ByteBuf dest, int version)
+        protected void serializeEvent(ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeEnumValue(status, dest);
             CBUtil.writeInet(node, dest);
         }
 
-        protected int eventSerializedSize(int version)
+        protected int eventSerializedSize(ProtocolVersion version)
         {
             return CBUtil.sizeOfEnumValue(status) + CBUtil.sizeOfInet(node);
         }
@@ -267,10 +268,10 @@
         }
 
         // Assumes the type has already been deserialized
-        public static SchemaChange deserializeEvent(ByteBuf cb, int version)
+        public static SchemaChange deserializeEvent(ByteBuf cb, ProtocolVersion version)
         {
             Change change = CBUtil.readEnumValue(Change.class, cb);
-            if (version >= 3)
+            if (version.isGreaterOrEqualTo(ProtocolVersion.V3))
             {
                 Target target = CBUtil.readEnumValue(Target.class, cb);
                 String keyspace = CBUtil.readString(cb);
@@ -289,11 +290,11 @@
             }
         }
 
-        public void serializeEvent(ByteBuf dest, int version)
+        public void serializeEvent(ByteBuf dest, ProtocolVersion version)
         {
             if (target == Target.FUNCTION || target == Target.AGGREGATE)
             {
-                if (version >= 4)
+                if (version.isGreaterOrEqualTo(ProtocolVersion.V4))
                 {
                     // available since protocol version 4
                     CBUtil.writeEnumValue(change, dest);
@@ -306,7 +307,7 @@
                 {
                     // not available in protocol versions < 4 - just say the keyspace was updated.
                     CBUtil.writeEnumValue(Change.UPDATED, dest);
-                    if (version >= 3)
+                    if (version.isGreaterOrEqualTo(ProtocolVersion.V3))
                         CBUtil.writeEnumValue(Target.KEYSPACE, dest);
                     CBUtil.writeString(keyspace, dest);
                     CBUtil.writeString("", dest);
@@ -314,7 +315,7 @@
                 return;
             }
 
-            if (version >= 3)
+            if (version.isGreaterOrEqualTo(ProtocolVersion.V3))
             {
                 CBUtil.writeEnumValue(change, dest);
                 CBUtil.writeEnumValue(target, dest);
@@ -341,17 +342,17 @@
             }
         }
 
-        public int eventSerializedSize(int version)
+        public int eventSerializedSize(ProtocolVersion version)
         {
             if (target == Target.FUNCTION || target == Target.AGGREGATE)
             {
-                if (version >= 4)
+                if (version.isGreaterOrEqualTo(ProtocolVersion.V4))
                     return CBUtil.sizeOfEnumValue(change)
                                + CBUtil.sizeOfEnumValue(target)
                                + CBUtil.sizeOfString(keyspace)
                                + CBUtil.sizeOfString(name)
                                + CBUtil.sizeOfStringList(argTypes);
-                if (version >= 3)
+                if (version.isGreaterOrEqualTo(ProtocolVersion.V3))
                     return CBUtil.sizeOfEnumValue(Change.UPDATED)
                            + CBUtil.sizeOfEnumValue(Target.KEYSPACE)
                            + CBUtil.sizeOfString(keyspace);
@@ -360,7 +361,7 @@
                        + CBUtil.sizeOfString("");
             }
 
-            if (version >= 3)
+            if (version.isGreaterOrEqualTo(ProtocolVersion.V3))
             {
                 int size = CBUtil.sizeOfEnumValue(change)
                          + CBUtil.sizeOfEnumValue(target)
diff --git a/src/java/org/apache/cassandra/transport/Frame.java b/src/java/org/apache/cassandra/transport/Frame.java
index ed9e3dc..e603e6c 100644
--- a/src/java/org/apache/cassandra/transport/Frame.java
+++ b/src/java/org/apache/cassandra/transport/Frame.java
@@ -27,6 +27,7 @@
 import io.netty.handler.codec.ByteToMessageDecoder;
 import io.netty.handler.codec.MessageToMessageDecoder;
 import io.netty.handler.codec.MessageToMessageEncoder;
+import io.netty.util.Attribute;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.transport.messages.ErrorMessage;
@@ -35,9 +36,6 @@
 {
     public static final byte PROTOCOL_VERSION_MASK = 0x7f;
 
-    /** These versions are sent by some clients, but are not valid Apache Cassandra versions (66, and 65 are DSE versions) */
-    private static int[] KNOWN_INVALID_VERSIONS = { 66, 65};
-
     public final Header header;
     public final ByteBuf body;
 
@@ -69,7 +67,7 @@
         return body.release();
     }
 
-    public static Frame create(Message.Type type, int streamId, int version, EnumSet<Header.Flag> flags, ByteBuf body)
+    public static Frame create(Message.Type type, int streamId, ProtocolVersion version, EnumSet<Header.Flag> flags, ByteBuf body)
     {
         Header header = new Header(version, flags, streamId, type, body.readableBytes());
         return new Frame(header, body);
@@ -82,18 +80,13 @@
 
         public static final int BODY_LENGTH_SIZE = 4;
 
-        public final int version;
+        public final ProtocolVersion version;
         public final EnumSet<Flag> flags;
         public final int streamId;
         public final Message.Type type;
         public final long bodySizeInBytes;
 
-        private Header(int version, int flags, int streamId, Message.Type type, long bodySizeInBytes)
-        {
-            this(version, Flag.deserialize(flags), streamId, type, bodySizeInBytes);
-        }
-
-        private Header(int version, EnumSet<Flag> flags, int streamId, Message.Type type, long bodySizeInBytes)
+        private Header(ProtocolVersion version, EnumSet<Flag> flags, int streamId, Message.Type type, long bodySizeInBytes)
         {
             this.version = version;
             this.flags = flags;
@@ -102,13 +95,14 @@
             this.bodySizeInBytes = bodySizeInBytes;
         }
 
-        public static enum Flag
+        public enum Flag
         {
             // The order of that enum matters!!
             COMPRESSED,
             TRACING,
             CUSTOM_PAYLOAD,
-            WARNING;
+            WARNING,
+            USE_BETA;
 
             private static final Flag[] ALL_VALUES = values();
 
@@ -179,23 +173,19 @@
             // 1 and 2 use a shorter header, so we may never have a complete header's worth of bytes.
             int firstByte = buffer.getByte(idx++);
             Message.Direction direction = Message.Direction.extractFromVersion(firstByte);
-            int version = firstByte & PROTOCOL_VERSION_MASK;
-            for (int dseVersion : KNOWN_INVALID_VERSIONS)
-            {
-                if (dseVersion == version)
-                    throw ProtocolException.toSilentException(new ProtocolException(invalidVersionMessage(version)));
-            }
-            if (version < Server.MIN_SUPPORTED_VERSION || version > versionCap.getMaxVersion())
-                throw new ProtocolException(invalidVersionMessage(version),
-                                            // only override the version IFF the version is less than the min supported, as this is relativly safe since older versions were the same up to v3.
-                                            // in the case where version is greater than, it isn't known if the protocol has changed, so reply back normally
-                                            version < Server.MIN_SUPPORTED_VERSION ? version : null);
+            int versionNum = firstByte & PROTOCOL_VERSION_MASK;
+            ProtocolVersion version = ProtocolVersion.decode(versionNum, versionCap);
 
             // Wait until we have the complete header
             if (readableBytes < Header.LENGTH)
                 return;
 
             int flags = buffer.getByte(idx++);
+            EnumSet<Header.Flag> decodedFlags = Header.Flag.deserialize(flags);
+
+            if (version.isBeta() && !decodedFlags.contains(Header.Flag.USE_BETA))
+                throw new ProtocolException(String.format("Beta version of the protocol used (%s), but USE_BETA flag is unset", version),
+                                            version);
 
             int streamId = buffer.getShort(idx);
             idx += 2;
@@ -233,33 +223,28 @@
             // extract body
             ByteBuf body = buffer.slice(idx, (int) bodyLength);
             body.retain();
-            
+
             idx += bodyLength;
             buffer.readerIndex(idx);
 
-            Connection connection = ctx.channel().attr(Connection.attributeKey).get();
+            Attribute<Connection> attrConn = ctx.channel().attr(Connection.attributeKey);
+            Connection connection = attrConn.get();
             if (connection == null)
             {
                 // First message seen on this channel, attach the connection object
                 connection = factory.newConnection(ctx.channel(), version);
-                ctx.channel().attr(Connection.attributeKey).set(connection);
+                attrConn.set(connection);
             }
             else if (connection.getVersion() != version)
             {
                 throw ErrorMessage.wrap(
                         new ProtocolException(String.format(
-                                "Invalid message version. Got %d but previous messages on this connection had version %d",
+                                "Invalid message version. Got %s but previous messages on this connection had version %s",
                                 version, connection.getVersion())),
                         streamId);
             }
 
-            results.add(new Frame(new Header(version, flags, streamId, type, bodyLength), body));
-        }
-
-        private String invalidVersionMessage(int version)
-        {
-            return String.format("Invalid or unsupported protocol version (%d); the lowest supported version is %d and the greatest is %d",
-                                 version, Server.MIN_SUPPORTED_VERSION, versionCap.getMaxVersion());
+            results.add(new Frame(new Header(version, decodedFlags, streamId, type, bodyLength), body));
         }
 
         private void fail()
@@ -290,12 +275,12 @@
             ByteBuf header = CBUtil.allocator.buffer(Header.LENGTH);
 
             Message.Type type = frame.header.type;
-            header.writeByte(type.direction.addToVersion(frame.header.version));
+            header.writeByte(type.direction.addToVersion(frame.header.version.asInt()));
             header.writeByte(Header.Flag.serialize(frame.header.flags));
 
             // Continue to support writing pre-v3 headers so that we can give proper error messages to drivers that
             // connect with the v1/v2 protocol. See CASSANDRA-11464.
-            if (frame.header.version >= Server.VERSION_3)
+            if (frame.header.version.isGreaterOrEqualTo(ProtocolVersion.V3))
                 header.writeShort(frame.header.streamId);
             else
                 header.writeByte(frame.header.streamId);
diff --git a/src/java/org/apache/cassandra/transport/Message.java b/src/java/org/apache/cassandra/transport/Message.java
index d27a57c..5f89d47 100644
--- a/src/java/org/apache/cassandra/transport/Message.java
+++ b/src/java/org/apache/cassandra/transport/Message.java
@@ -131,7 +131,7 @@
             }
         }
 
-        private Type(int opcode, Direction direction, Codec<?> codec)
+        Type(int opcode, Direction direction, Codec<?> codec)
         {
             this.opcode = opcode;
             this.direction = direction;
@@ -160,7 +160,7 @@
     private int streamId;
     private Frame sourceFrame;
     private Map<String, ByteBuffer> customPayload;
-    protected Integer forcedProtocolVersion = null;
+    protected ProtocolVersion forcedProtocolVersion = null;
 
     protected Message(Type type)
     {
@@ -220,7 +220,7 @@
                 throw new IllegalArgumentException();
         }
 
-        public abstract Response execute(QueryState queryState);
+        public abstract Response execute(QueryState queryState, long queryStartNanoTime);
 
         public void setTracingRequested()
         {
@@ -285,7 +285,7 @@
 
             try
             {
-                if (isCustomPayload && frame.header.version < Server.VERSION_4)
+                if (isCustomPayload && frame.header.version.isSmallerThan(ProtocolVersion.V4))
                     throw new ProtocolException("Received frame with CUSTOM_PAYLOAD flag for native protocol version < 4");
 
                 Message message = frame.header.type.codec.decode(frame.body, frame.header.version);
@@ -336,7 +336,7 @@
         {
             Connection connection = ctx.channel().attr(Connection.attributeKey).get();
             // The only case the connection can be null is when we send the initial STARTUP message (client side thus)
-            int version = connection == null ? versionCap.getMaxVersion() : connection.getVersion();
+            ProtocolVersion version = connection == null ? versionCap.getMaxVersion() : connection.getVersion();
 
             EnumSet<Frame.Header.Flag> flags = EnumSet.noneOf(Frame.Header.Flag.class);
 
@@ -354,13 +354,13 @@
                     List<String> warnings = ((Response)message).getWarnings();
                     if (warnings != null)
                     {
-                        if (version < Server.VERSION_4)
+                        if (version.isSmallerThan(ProtocolVersion.V4))
                             throw new ProtocolException("Must not send frame with WARNING flag for native protocol version < 4");
                         messageSize += CBUtil.sizeOfStringList(warnings);
                     }
                     if (customPayload != null)
                     {
-                        if (version < Server.VERSION_4)
+                        if (version.isSmallerThan(ProtocolVersion.V4))
                             throw new ProtocolException("Must not send frame with CUSTOM_PAYLOAD flag for native protocol version < 4");
                         messageSize += CBUtil.sizeOfBytesMap(customPayload);
                     }
@@ -409,9 +409,13 @@
 
                 // if the driver attempted to connect with a protocol version lower than the minimum supported
                 // version, respond with a protocol error message with the correct frame header for that version
-                int responseVersion = message.forcedProtocolVersion == null
+                ProtocolVersion responseVersion = message.forcedProtocolVersion == null
                                     ? version
                                     : message.forcedProtocolVersion;
+
+                if (responseVersion.isBeta())
+                    flags.add(Frame.Header.Flag.USE_BETA);
+
                 results.add(Frame.create(message.type, message.getStreamId(), responseVersion, flags, body));
             }
             catch (Throwable e)
@@ -669,18 +673,19 @@
         {
             final Response response;
             final ServerConnection connection;
+            long queryStartNanoTime = System.nanoTime();
 
             try
             {
                 assert request.connection() instanceof ServerConnection;
                 connection = (ServerConnection)request.connection();
-                if (connection.getVersion() >= Server.VERSION_4)
+                if (connection.getVersion().isGreaterOrEqualTo(ProtocolVersion.V4))
                     ClientWarn.instance.captureWarnings();
 
                 QueryState qstate = connection.validateNewMessage(request.type, connection.getVersion(), request.getStreamId());
 
                 logger.trace("Received: {}, v={}", request, connection.getVersion());
-                response = request.execute(qstate);
+                response = request.execute(qstate, queryStartNanoTime);
                 response.setStreamId(request.getStreamId());
                 response.setWarnings(ClientWarn.instance.getWarnings());
                 response.attach(connection);
diff --git a/src/java/org/apache/cassandra/transport/OptionCodec.java b/src/java/org/apache/cassandra/transport/OptionCodec.java
deleted file mode 100644
index 3a8b813..0000000
--- a/src/java/org/apache/cassandra/transport/OptionCodec.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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.transport;
-
-import java.lang.reflect.Array;
-import java.util.EnumMap;
-import java.util.Map;
-
-import io.netty.buffer.ByteBuf;
-
-import io.netty.buffer.Unpooled;
-import org.apache.cassandra.utils.Pair;
-
-public class OptionCodec<T extends Enum<T> & OptionCodec.Codecable<T>>
-{
-    public interface Codecable<T extends Enum<T>>
-    {
-        public int getId(int version);
-
-        public Object readValue(ByteBuf cb, int version);
-        public void writeValue(Object value, ByteBuf cb, int version);
-        public int serializedValueSize(Object obj, int version);
-    }
-
-    private final Class<T> klass;
-    private final T[] ids;
-
-    @SuppressWarnings({"unchecked"})
-    public OptionCodec(Class<T> klass)
-    {
-        this.klass = klass;
-
-        T[] values = klass.getEnumConstants();
-        int maxId = -1;
-        for (T opt : values)
-            maxId = Math.max(maxId, opt.getId(Server.CURRENT_VERSION));
-        ids = (T[])Array.newInstance(klass, maxId + 1);
-        for (T opt : values)
-        {
-            if (ids[opt.getId(Server.CURRENT_VERSION)] != null)
-                throw new IllegalStateException(String.format("Duplicate option id %d", opt.getId(Server.CURRENT_VERSION)));
-            ids[opt.getId(Server.CURRENT_VERSION)] = opt;
-        }
-    }
-
-    private T fromId(int id)
-    {
-        T opt = ids[id];
-        if (opt == null)
-            throw new ProtocolException(String.format("Unknown option id %d", id));
-        return opt;
-    }
-
-    public Map<T, Object> decode(ByteBuf body, int version)
-    {
-        EnumMap<T, Object> options = new EnumMap<T, Object>(klass);
-        int n = body.readUnsignedShort();
-        for (int i = 0; i < n; i++)
-        {
-            T opt = fromId(body.readUnsignedShort());
-            Object value = opt.readValue(body, version);
-            if (options.containsKey(opt))
-                throw new ProtocolException(String.format("Duplicate option %s in message", opt.name()));
-            options.put(opt, value);
-        }
-        return options;
-    }
-
-    public ByteBuf encode(Map<T, Object> options, int version)
-    {
-        int optLength = 2;
-        for (Map.Entry<T, Object> entry : options.entrySet())
-            optLength += 2 + entry.getKey().serializedValueSize(entry.getValue(), version);
-        ByteBuf cb = Unpooled.buffer(optLength);
-        cb.writeShort(options.size());
-        for (Map.Entry<T, Object> entry : options.entrySet())
-        {
-            T opt = entry.getKey();
-            cb.writeShort(opt.getId(version));
-            opt.writeValue(entry.getValue(), cb, version);
-        }
-        return cb;
-    }
-
-    public Pair<T, Object> decodeOne(ByteBuf body, int version)
-    {
-        T opt = fromId(body.readUnsignedShort());
-        Object value = opt.readValue(body, version);
-        return Pair.create(opt, value);
-    }
-
-    public void writeOne(Pair<T, Object> option, ByteBuf dest, int version)
-    {
-        T opt = option.left;
-        Object obj = option.right;
-        dest.writeShort(opt.getId(version));
-        opt.writeValue(obj, dest, version);
-    }
-
-    public int oneSerializedSize(Pair<T, Object> option, int version)
-    {
-        T opt = option.left;
-        Object obj = option.right;
-        return 2 + opt.serializedValueSize(obj, version);
-    }
-}
diff --git a/src/java/org/apache/cassandra/transport/ProtocolException.java b/src/java/org/apache/cassandra/transport/ProtocolException.java
index 0b307a9..bdfdb6f 100644
--- a/src/java/org/apache/cassandra/transport/ProtocolException.java
+++ b/src/java/org/apache/cassandra/transport/ProtocolException.java
@@ -25,17 +25,17 @@
  */
 public class ProtocolException extends RuntimeException implements TransportException
 {
-    private final Integer attemptedLowProtocolVersion;
+    private final ProtocolVersion forcedProtocolVersion;
 
     public ProtocolException(String msg)
     {
         this(msg, null);
     }
 
-    public ProtocolException(String msg, Integer attemptedLowProtocolVersion)
+    public ProtocolException(String msg, ProtocolVersion forcedProtocolVersion)
     {
         super(msg);
-        this.attemptedLowProtocolVersion = attemptedLowProtocolVersion;
+        this.forcedProtocolVersion = forcedProtocolVersion;
     }
 
     public ExceptionCode code()
@@ -43,14 +43,9 @@
         return ExceptionCode.PROTOCOL_ERROR;
     }
 
-    /**
-     * If the ProtocolException is due to a connection being made with a protocol version that is lower
-     * than Server.MIN_SUPPORTED_VERSION, this will return that unsupported protocol version.  Otherwise,
-     * null is returned.
-     */
-    public Integer getAttemptedLowProtocolVersion()
+    public ProtocolVersion getForcedProtocolVersion()
     {
-        return attemptedLowProtocolVersion;
+        return forcedProtocolVersion;
     }
 
     public boolean isSilent()
@@ -67,7 +62,7 @@
     {
         public Silent(ProtocolException cause)
         {
-            super(cause.getMessage(), cause.attemptedLowProtocolVersion);
+            super(cause.getMessage(), cause.forcedProtocolVersion);
         }
 
         @Override
diff --git a/src/java/org/apache/cassandra/transport/ProtocolVersion.java b/src/java/org/apache/cassandra/transport/ProtocolVersion.java
new file mode 100644
index 0000000..4037817
--- /dev/null
+++ b/src/java/org/apache/cassandra/transport/ProtocolVersion.java
@@ -0,0 +1,160 @@
+/*
+ * 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.transport;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+/**
+ * The native (CQL binary) protocol version.
+ *
+ * Some versions may be in beta, which means that the client must
+ * specify the beta flag in the frame for the version to be considered valid.
+ * Beta versions must have the word "beta" in their description, this is mandated
+ * by the specs.
+ *
+ */
+public enum ProtocolVersion implements Comparable<ProtocolVersion>
+{
+    // The order is important as it defines the chronological history of versions, which is used
+    // to determine if a feature is supported or some serdes formats
+    V1(1, "v1", false), // no longer supported
+    V2(2, "v2", false), // no longer supported
+    V3(3, "v3", false),
+    V4(4, "v4", false),
+    V5(5, "v5-beta", true);
+
+    /** The version number */
+    private final int num;
+
+    /** A description of the version, beta versions should have the word "-beta" */
+    private final String descr;
+
+    /** Set this to true for beta versions */
+    private final boolean beta;
+
+    ProtocolVersion(int num, String descr, boolean beta)
+    {
+        this.num = num;
+        this.descr = descr;
+        this.beta = beta;
+    }
+
+    /** The supported versions stored as an array, these should be private and are required for fast decoding*/
+    private final static ProtocolVersion[] SUPPORTED_VERSIONS = new ProtocolVersion[] { V3, V4, V5 };
+    final static ProtocolVersion MIN_SUPPORTED_VERSION = SUPPORTED_VERSIONS[0];
+    final static ProtocolVersion MAX_SUPPORTED_VERSION = SUPPORTED_VERSIONS[SUPPORTED_VERSIONS.length - 1];
+    /** These versions are sent by some clients, but are not valid Apache Cassandra versions (66, and 65 are DSE versions) */
+    private static int[] KNOWN_INVALID_VERSIONS = { 66, 65};
+
+    /** All supported versions, published as an enumset */
+    public final static EnumSet<ProtocolVersion> SUPPORTED = EnumSet.copyOf(Arrays.asList((ProtocolVersion[]) ArrayUtils.addAll(SUPPORTED_VERSIONS)));
+
+    /** Old unsupported versions, this is OK as long as we never add newer unsupported versions */
+    public final static EnumSet<ProtocolVersion> UNSUPPORTED = EnumSet.complementOf(SUPPORTED);
+
+    /** The preferred versions */
+    public final static ProtocolVersion CURRENT = V4;
+    public final static Optional<ProtocolVersion> BETA = Optional.of(V5);
+
+    public static List<String> supportedVersions()
+    {
+        List<String> ret = new ArrayList<>(SUPPORTED.size());
+        for (ProtocolVersion version : SUPPORTED)
+            ret.add(version.toString());
+        return ret;
+    }
+
+    public static ProtocolVersion decode(int versionNum, ProtocolVersionLimit ceiling)
+    {
+        ProtocolVersion ret = versionNum >= MIN_SUPPORTED_VERSION.num && versionNum <= ceiling.getMaxVersion().num
+                              ? SUPPORTED_VERSIONS[versionNum - MIN_SUPPORTED_VERSION.num]
+                              : null;
+
+        if (ret == null)
+        {
+            // if this is not a supported version check the old versions
+            for (ProtocolVersion dseVersion : UNSUPPORTED)
+            {
+                // if it is an old version that is no longer supported this ensures that we reply
+                // with that same version
+                if (dseVersion.num == versionNum)
+                    throw new ProtocolException(ProtocolVersion.invalidVersionMessage(versionNum), dseVersion);
+            }
+            for (int version : KNOWN_INVALID_VERSIONS)
+            {
+                if (versionNum == version)
+                    throw ProtocolException.toSilentException(new ProtocolException(ProtocolVersion.invalidVersionMessage(versionNum)));
+            }
+
+            // If the version is invalid reply with the channel's version
+            throw new ProtocolException(invalidVersionMessage(versionNum));
+        }
+
+        return ret;
+    }
+
+    public boolean isBeta()
+    {
+        return beta;
+    }
+
+    public static String invalidVersionMessage(int version)
+    {
+        return String.format("Invalid or unsupported protocol version (%d); supported versions are (%s)",
+                             version, String.join(", ", ProtocolVersion.supportedVersions()));
+    }
+
+    public int asInt()
+    {
+        return num;
+    }
+
+    @Override
+    public String toString()
+    {
+        // This format is mandated by the protocl specs for the SUPPORTED message, see OptionsMessage execute().
+        return String.format("%d/%s", num, descr);
+    }
+
+    public final boolean isGreaterThan(ProtocolVersion other)
+    {
+        return num > other.num;
+    }
+
+    public final boolean isGreaterOrEqualTo(ProtocolVersion other)
+    {
+        return num >= other.num;
+    }
+
+    public final boolean isSmallerThan(ProtocolVersion other)
+    {
+        return num < other.num;
+    }
+
+    public final boolean isSmallerOrEqualTo(ProtocolVersion other)
+    {
+        return num <= other.num;
+    }
+}
diff --git a/src/java/org/apache/cassandra/transport/ProtocolVersionLimit.java b/src/java/org/apache/cassandra/transport/ProtocolVersionLimit.java
index c476efb..9738a19 100644
--- a/src/java/org/apache/cassandra/transport/ProtocolVersionLimit.java
+++ b/src/java/org/apache/cassandra/transport/ProtocolVersionLimit.java
@@ -21,7 +21,7 @@
 @FunctionalInterface
 public interface ProtocolVersionLimit
 {
-    public int getMaxVersion();
+    public ProtocolVersion getMaxVersion();
 
-    public static final ProtocolVersionLimit SERVER_DEFAULT = () -> Server.CURRENT_VERSION;
+    public static final ProtocolVersionLimit SERVER_DEFAULT = () -> ProtocolVersion.MAX_SUPPORTED_VERSION;
 }
diff --git a/src/java/org/apache/cassandra/transport/Server.java b/src/java/org/apache/cassandra/transport/Server.java
index 012b326..ced764f 100644
--- a/src/java/org/apache/cassandra/transport/Server.java
+++ b/src/java/org/apache/cassandra/transport/Server.java
@@ -67,16 +67,11 @@
     private static final Logger logger = LoggerFactory.getLogger(Server.class);
     private static final boolean useEpoll = NativeTransportService.useEpoll();
 
-    public static final int VERSION_3 = 3;
-    public static final int VERSION_4 = 4;
-    public static final int CURRENT_VERSION = VERSION_4;
-    public static final int MIN_SUPPORTED_VERSION = VERSION_3;
-
     private final ConnectionTracker connectionTracker = new ConnectionTracker();
 
     private final Connection.Factory connectionFactory = new Connection.Factory()
     {
-        public Connection newConnection(Channel channel, int version)
+        public Connection newConnection(Channel channel, ProtocolVersion version)
         {
             return new ServerConnection(channel, version, connectionTracker);
         }
@@ -124,7 +119,7 @@
 
     public synchronized void start()
     {
-        if(isRunning()) 
+        if(isRunning())
             return;
 
         // Configure the server.
@@ -139,9 +134,10 @@
         if (workerGroup != null)
             bootstrap = bootstrap.group(workerGroup);
 
-        final EncryptionOptions.ClientEncryptionOptions clientEnc = DatabaseDescriptor.getClientEncryptionOptions();
         if (this.useSSL)
         {
+            final EncryptionOptions.ClientEncryptionOptions clientEnc = DatabaseDescriptor.getClientEncryptionOptions();
+
             if (clientEnc.optional)
             {
                 logger.info("Enabling optionally encrypted CQL connections between client and server");
@@ -174,12 +170,12 @@
     {
         return connectionTracker.getConnectedClients();
     }
-    
+
     private void close()
     {
         // Close opened connections
         connectionTracker.closeAll();
-        
+
         logger.info("Stop listening for CQL clients");
     }
 
@@ -289,7 +285,7 @@
         public int getConnectedClients()
         {
             /*
-              - When server is running: allChannels contains all clients' connections (channels) 
+              - When server is running: allChannels contains all clients' connections (channels)
                 plus one additional channel used for the server's own bootstrap.
                - When server is stopped: the size is 0
             */
@@ -412,13 +408,13 @@
             }
         }
 
-        protected final SslHandler createSslHandler() {
+        protected final SslHandler createSslHandler()
+        {
             SSLEngine sslEngine = sslContext.createSSLEngine();
             sslEngine.setUseClientMode(false);
             String[] suites = SSLFactory.filterCipherSuites(sslEngine.getSupportedCipherSuites(), encryptionOptions.cipher_suites);
             sslEngine.setEnabledCipherSuites(suites);
             sslEngine.setNeedClientAuth(encryptionOptions.require_client_auth);
-            sslEngine.setEnabledProtocols(SSLFactory.ACCEPTED_PROTOCOLS);
             return new SslHandler(sslEngine);
         }
     }
@@ -524,7 +520,8 @@
 
 
         private static final InetAddress bindAll;
-        static {
+        static
+        {
             try
             {
                 bindAll = InetAddress.getByAddress(new byte[4]);
diff --git a/src/java/org/apache/cassandra/transport/ServerConnection.java b/src/java/org/apache/cassandra/transport/ServerConnection.java
index 1ef6c73..9374ca0 100644
--- a/src/java/org/apache/cassandra/transport/ServerConnection.java
+++ b/src/java/org/apache/cassandra/transport/ServerConnection.java
@@ -36,7 +36,7 @@
 
     private final ConcurrentMap<Integer, QueryState> queryStates = new ConcurrentHashMap<>();
 
-    public ServerConnection(Channel channel, int version, Connection.Tracker tracker)
+    public ServerConnection(Channel channel, ProtocolVersion version, Connection.Tracker tracker)
     {
         super(channel, version, tracker);
         this.clientState = ClientState.forExternalCalls(channel.remoteAddress());
@@ -56,7 +56,7 @@
         return qState;
     }
 
-    public QueryState validateNewMessage(Message.Type type, int version, int streamId)
+    public QueryState validateNewMessage(Message.Type type, ProtocolVersion version, int streamId)
     {
         switch (state)
         {
@@ -67,7 +67,7 @@
             case AUTHENTICATION:
                 // Support both SASL auth from protocol v2 and the older style Credentials auth from v1
                 if (type != Message.Type.AUTH_RESPONSE && type != Message.Type.CREDENTIALS)
-                    throw new ProtocolException(String.format("Unexpected message %s, expecting %s", type, version == 1 ? "CREDENTIALS" : "SASL_RESPONSE"));
+                    throw new ProtocolException(String.format("Unexpected message %s, expecting %s", type, version == ProtocolVersion.V1 ? "CREDENTIALS" : "SASL_RESPONSE"));
                 break;
             case READY:
                 if (type == Message.Type.STARTUP)
diff --git a/src/java/org/apache/cassandra/transport/SimpleClient.java b/src/java/org/apache/cassandra/transport/SimpleClient.java
index 40423c3..e338359 100644
--- a/src/java/org/apache/cassandra/transport/SimpleClient.java
+++ b/src/java/org/apache/cassandra/transport/SimpleClient.java
@@ -80,7 +80,7 @@
 
     protected final ResponseHandler responseHandler = new ResponseHandler();
     protected final Connection.Tracker tracker = new ConnectionTracker();
-    protected final int version;
+    protected final ProtocolVersion version;
     // We don't track connection really, so we don't need one Connection per channel
     protected Connection connection;
     protected Bootstrap bootstrap;
@@ -89,30 +89,38 @@
 
     private final Connection.Factory connectionFactory = new Connection.Factory()
     {
-        public Connection newConnection(Channel channel, int version)
+        public Connection newConnection(Channel channel, ProtocolVersion version)
         {
             return connection;
         }
     };
 
-    public SimpleClient(String host, int port, int version, ClientEncryptionOptions encryptionOptions)
+    public SimpleClient(String host, int port, ProtocolVersion version, ClientEncryptionOptions encryptionOptions)
     {
-        this.host = host;
-        this.port = port;
-        this.version = version;
-        this.encryptionOptions = encryptionOptions;
+        this(host, port, version, false, encryptionOptions);
     }
 
     public SimpleClient(String host, int port, ClientEncryptionOptions encryptionOptions)
     {
-        this(host, port, Server.CURRENT_VERSION, encryptionOptions);
+        this(host, port, ProtocolVersion.CURRENT, encryptionOptions);
     }
 
-    public SimpleClient(String host, int port, int version)
+    public SimpleClient(String host, int port, ProtocolVersion version)
     {
         this(host, port, version, new ClientEncryptionOptions());
     }
 
+    public SimpleClient(String host, int port, ProtocolVersion version, boolean useBeta, ClientEncryptionOptions encryptionOptions)
+    {
+        this.host = host;
+        this.port = port;
+        if (version.isBeta() && !useBeta)
+            throw new IllegalArgumentException(String.format("Beta version of server used (%s), but USE_BETA flag is not set", version));
+
+        this.version = version;
+        this.encryptionOptions = encryptionOptions;
+    }
+
     public SimpleClient(String host, int port)
     {
         this(host, port, new ClientEncryptionOptions());
@@ -308,7 +316,6 @@
             sslEngine.setUseClientMode(true);
             String[] suites = SSLFactory.filterCipherSuites(sslEngine.getSupportedCipherSuites(), encryptionOptions.cipher_suites);
             sslEngine.setEnabledCipherSuites(suites);
-            sslEngine.setEnabledProtocols(SSLFactory.ACCEPTED_PROTOCOLS);
             channel.pipeline().addFirst("ssl", new SslHandler(sslEngine));
         }
     }
diff --git a/src/java/org/apache/cassandra/transport/messages/AuthChallenge.java b/src/java/org/apache/cassandra/transport/messages/AuthChallenge.java
index 15a9a9a..fda83f9 100644
--- a/src/java/org/apache/cassandra/transport/messages/AuthChallenge.java
+++ b/src/java/org/apache/cassandra/transport/messages/AuthChallenge.java
@@ -20,6 +20,7 @@
 import org.apache.cassandra.transport.CBUtil;
 import org.apache.cassandra.transport.Message;
 import io.netty.buffer.ByteBuf;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 import java.nio.ByteBuffer;
 
@@ -30,7 +31,7 @@
 {
     public static final Message.Codec<AuthChallenge> codec = new Message.Codec<AuthChallenge>()
     {
-        public AuthChallenge decode(ByteBuf body, int version)
+        public AuthChallenge decode(ByteBuf body, ProtocolVersion version)
         {
             ByteBuffer b = CBUtil.readValue(body);
             byte[] token = new byte[b.remaining()];
@@ -38,12 +39,12 @@
             return new AuthChallenge(token);
         }
 
-        public void encode(AuthChallenge challenge, ByteBuf dest, int version)
+        public void encode(AuthChallenge challenge, ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeValue(challenge.token, dest);
         }
 
-        public int encodedSize(AuthChallenge challenge, int version)
+        public int encodedSize(AuthChallenge challenge, ProtocolVersion version)
         {
             return CBUtil.sizeOfValue(challenge.token);
         }
diff --git a/src/java/org/apache/cassandra/transport/messages/AuthResponse.java b/src/java/org/apache/cassandra/transport/messages/AuthResponse.java
index ca7a0c3..332b024 100644
--- a/src/java/org/apache/cassandra/transport/messages/AuthResponse.java
+++ b/src/java/org/apache/cassandra/transport/messages/AuthResponse.java
@@ -23,6 +23,7 @@
 import org.apache.cassandra.auth.AuthenticatedUser;
 import org.apache.cassandra.auth.IAuthenticator;
 import org.apache.cassandra.exceptions.AuthenticationException;
+import org.apache.cassandra.metrics.AuthMetrics;
 import org.apache.cassandra.service.QueryState;
 import org.apache.cassandra.transport.*;
 
@@ -35,9 +36,9 @@
 {
     public static final Message.Codec<AuthResponse> codec = new Message.Codec<AuthResponse>()
     {
-        public AuthResponse decode(ByteBuf body, int version)
+        public AuthResponse decode(ByteBuf body, ProtocolVersion version)
         {
-            if (version == 1)
+            if (version == ProtocolVersion.V1)
                 throw new ProtocolException("SASL Authentication is not supported in version 1 of the protocol");
 
             ByteBuffer b = CBUtil.readValue(body);
@@ -46,12 +47,12 @@
             return new AuthResponse(token);
         }
 
-        public void encode(AuthResponse response, ByteBuf dest, int version)
+        public void encode(AuthResponse response, ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeValue(response.token, dest);
         }
 
-        public int encodedSize(AuthResponse response, int version)
+        public int encodedSize(AuthResponse response, ProtocolVersion version)
         {
             return CBUtil.sizeOfValue(response.token);
         }
@@ -67,7 +68,7 @@
     }
 
     @Override
-    public Response execute(QueryState queryState)
+    public Response execute(QueryState queryState, long queryStartNanoTime)
     {
         try
         {
@@ -77,6 +78,7 @@
             {
                 AuthenticatedUser user = negotiator.getAuthenticatedUser();
                 queryState.getClientState().login(user);
+                AuthMetrics.instance.markSuccess();
                 // authentication is complete, send a ready message to the client
                 return new AuthSuccess(challenge);
             }
@@ -87,6 +89,7 @@
         }
         catch (AuthenticationException e)
         {
+            AuthMetrics.instance.markFailure();
             return ErrorMessage.fromException(e);
         }
     }
diff --git a/src/java/org/apache/cassandra/transport/messages/AuthSuccess.java b/src/java/org/apache/cassandra/transport/messages/AuthSuccess.java
index 8c1b5b1..b8ed7f0 100644
--- a/src/java/org/apache/cassandra/transport/messages/AuthSuccess.java
+++ b/src/java/org/apache/cassandra/transport/messages/AuthSuccess.java
@@ -20,6 +20,7 @@
 import org.apache.cassandra.transport.CBUtil;
 import org.apache.cassandra.transport.Message;
 import io.netty.buffer.ByteBuf;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 import java.nio.ByteBuffer;
 
@@ -33,7 +34,7 @@
 {
     public static final Message.Codec<AuthSuccess> codec = new Message.Codec<AuthSuccess>()
     {
-        public AuthSuccess decode(ByteBuf body, int version)
+        public AuthSuccess decode(ByteBuf body, ProtocolVersion version)
         {
             ByteBuffer b = CBUtil.readValue(body);
             byte[] token = null;
@@ -45,12 +46,12 @@
             return new AuthSuccess(token);
         }
 
-        public void encode(AuthSuccess success, ByteBuf dest, int version)
+        public void encode(AuthSuccess success, ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeValue(success.token, dest);
         }
 
-        public int encodedSize(AuthSuccess success, int version)
+        public int encodedSize(AuthSuccess success, ProtocolVersion version)
         {
             return CBUtil.sizeOfValue(success.token);
         }
diff --git a/src/java/org/apache/cassandra/transport/messages/AuthenticateMessage.java b/src/java/org/apache/cassandra/transport/messages/AuthenticateMessage.java
index 230f0f2..1261083 100644
--- a/src/java/org/apache/cassandra/transport/messages/AuthenticateMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/AuthenticateMessage.java
@@ -21,6 +21,7 @@
 
 import org.apache.cassandra.transport.CBUtil;
 import org.apache.cassandra.transport.Message;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Message to indicate that the server is ready to receive requests.
@@ -29,18 +30,18 @@
 {
     public static final Message.Codec<AuthenticateMessage> codec = new Message.Codec<AuthenticateMessage>()
     {
-        public AuthenticateMessage decode(ByteBuf body, int version)
+        public AuthenticateMessage decode(ByteBuf body, ProtocolVersion version)
         {
             String authenticator = CBUtil.readString(body);
             return new AuthenticateMessage(authenticator);
         }
 
-        public void encode(AuthenticateMessage msg, ByteBuf dest, int version)
+        public void encode(AuthenticateMessage msg, ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeString(msg.authenticator, dest);
         }
 
-        public int encodedSize(AuthenticateMessage msg, int version)
+        public int encodedSize(AuthenticateMessage msg, ProtocolVersion version)
         {
             return CBUtil.sizeOfString(msg.authenticator);
         }
diff --git a/src/java/org/apache/cassandra/transport/messages/BatchMessage.java b/src/java/org/apache/cassandra/transport/messages/BatchMessage.java
index bd2423e..0be027f 100644
--- a/src/java/org/apache/cassandra/transport/messages/BatchMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/BatchMessage.java
@@ -43,7 +43,7 @@
 {
     public static final Message.Codec<BatchMessage> codec = new Message.Codec<BatchMessage>()
     {
-        public BatchMessage decode(ByteBuf body, int version)
+        public BatchMessage decode(ByteBuf body, ProtocolVersion version)
         {
             byte type = body.readByte();
             int n = body.readUnsignedShort();
@@ -65,7 +65,7 @@
             return new BatchMessage(toType(type), queryOrIds, variables, options);
         }
 
-        public void encode(BatchMessage msg, ByteBuf dest, int version)
+        public void encode(BatchMessage msg, ByteBuf dest, ProtocolVersion version)
         {
             int queries = msg.queryOrIdList.size();
 
@@ -84,13 +84,13 @@
                 CBUtil.writeValueList(msg.values.get(i), dest);
             }
 
-            if (version < 3)
+            if (version.isSmallerThan(ProtocolVersion.V3))
                 CBUtil.writeConsistencyLevel(msg.options.getConsistency(), dest);
             else
                 QueryOptions.codec.encode(msg.options, dest, version);
         }
 
-        public int encodedSize(BatchMessage msg, int version)
+        public int encodedSize(BatchMessage msg, ProtocolVersion version)
         {
             int size = 3; // type + nb queries
             for (int i = 0; i < msg.queryOrIdList.size(); i++)
@@ -102,7 +102,7 @@
 
                 size += CBUtil.sizeOfValueList(msg.values.get(i));
             }
-            size += version < 3
+            size += version.isSmallerThan(ProtocolVersion.V3)
                   ? CBUtil.sizeOfConsistencyLevel(msg.options.getConsistency())
                   : QueryOptions.codec.encodedSize(msg.options, version);
             return size;
@@ -147,7 +147,7 @@
         this.options = options;
     }
 
-    public Message.Response execute(QueryState state)
+    public Message.Response execute(QueryState state, long queryStartNanoTime)
     {
         try
         {
@@ -160,7 +160,7 @@
 
             if (state.traceNextQuery())
             {
-                state.createTracingSession();
+                state.createTracingSession(getCustomPayload());
 
                 ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
                 if(options.getConsistency() != null)
@@ -214,7 +214,7 @@
             // Note: It's ok at this point to pass a bogus value for the number of bound terms in the BatchState ctor
             // (and no value would be really correct, so we prefer passing a clearly wrong one).
             BatchStatement batch = new BatchStatement(-1, batchType, statements, Attributes.none());
-            Message.Response response = handler.processBatch(batch, state, batchOptions, getCustomPayload());
+            Message.Response response = handler.processBatch(batch, state, batchOptions, getCustomPayload(), queryStartNanoTime);
 
             if (tracingId != null)
                 response.setTracingId(tracingId);
diff --git a/src/java/org/apache/cassandra/transport/messages/CredentialsMessage.java b/src/java/org/apache/cassandra/transport/messages/CredentialsMessage.java
index fc959ab..764d992 100644
--- a/src/java/org/apache/cassandra/transport/messages/CredentialsMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/CredentialsMessage.java
@@ -24,10 +24,12 @@
 import org.apache.cassandra.auth.AuthenticatedUser;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.exceptions.AuthenticationException;
+import org.apache.cassandra.metrics.AuthMetrics;
 import org.apache.cassandra.service.QueryState;
 import org.apache.cassandra.transport.CBUtil;
 import org.apache.cassandra.transport.Message;
 import org.apache.cassandra.transport.ProtocolException;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Message to indicate that the server is ready to receive requests.
@@ -36,9 +38,9 @@
 {
     public static final Message.Codec<CredentialsMessage> codec = new Message.Codec<CredentialsMessage>()
     {
-        public CredentialsMessage decode(ByteBuf body, int version)
+        public CredentialsMessage decode(ByteBuf body, ProtocolVersion version)
         {
-            if (version > 1)
+            if (version.isGreaterThan(ProtocolVersion.V1))
                 throw new ProtocolException("Legacy credentials authentication is not supported in " +
                         "protocol versions > 1. Please use SASL authentication via a SaslResponse message");
 
@@ -46,12 +48,12 @@
             return new CredentialsMessage(credentials);
         }
 
-        public void encode(CredentialsMessage msg, ByteBuf dest, int version)
+        public void encode(CredentialsMessage msg, ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeStringMap(msg.credentials, dest);
         }
 
-        public int encodedSize(CredentialsMessage msg, int version)
+        public int encodedSize(CredentialsMessage msg, ProtocolVersion version)
         {
             return CBUtil.sizeOfStringMap(msg.credentials);
         }
@@ -70,15 +72,17 @@
         this.credentials = credentials;
     }
 
-    public Message.Response execute(QueryState state)
+    public Message.Response execute(QueryState state, long queryStartNanoTime)
     {
         try
         {
             AuthenticatedUser user = DatabaseDescriptor.getAuthenticator().legacyAuthenticate(credentials);
             state.getClientState().login(user);
+            AuthMetrics.instance.markSuccess();
         }
         catch (AuthenticationException e)
         {
+            AuthMetrics.instance.markFailure();
             return ErrorMessage.fromException(e);
         }
 
diff --git a/src/java/org/apache/cassandra/transport/messages/ErrorMessage.java b/src/java/org/apache/cassandra/transport/messages/ErrorMessage.java
index 8f45d4d..ac4b3dc 100644
--- a/src/java/org/apache/cassandra/transport/messages/ErrorMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/ErrorMessage.java
@@ -17,7 +17,10 @@
  */
 package org.apache.cassandra.transport.messages;
 
+import java.net.InetAddress;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.handler.codec.CodecException;
@@ -42,7 +45,7 @@
 
     public static final Message.Codec<ErrorMessage> codec = new Message.Codec<ErrorMessage>()
     {
-        public ErrorMessage decode(ByteBuf body, int version)
+        public ErrorMessage decode(ByteBuf body, ProtocolVersion version)
         {
             ExceptionCode code = ExceptionCode.fromValue(body.readInt());
             String msg = CBUtil.readString(body);
@@ -76,22 +79,35 @@
                 case TRUNCATE_ERROR:
                     te = new TruncateException(msg);
                     break;
-                case WRITE_FAILURE: 
+                case WRITE_FAILURE:
                 case READ_FAILURE:
                     {
                         ConsistencyLevel cl = CBUtil.readConsistencyLevel(body);
                         int received = body.readInt();
                         int blockFor = body.readInt();
+                        // The number of failures is also present in protocol v5, but used instead to specify the size of the failure map
                         int failure = body.readInt();
+
+                        Map<InetAddress, RequestFailureReason> failureReasonByEndpoint = new ConcurrentHashMap<>();
+                        if (version.isGreaterOrEqualTo(ProtocolVersion.V5))
+                        {
+                            for (int i = 0; i < failure; i++)
+                            {
+                                InetAddress endpoint = CBUtil.readInetAddr(body);
+                                RequestFailureReason failureReason = RequestFailureReason.fromCode(body.readUnsignedShort());
+                                failureReasonByEndpoint.put(endpoint, failureReason);
+                            }
+                        }
+
                         if (code == ExceptionCode.WRITE_FAILURE)
                         {
                             WriteType writeType = Enum.valueOf(WriteType.class, CBUtil.readString(body));
-                            te = new WriteFailureException(cl, received, failure, blockFor, writeType);
+                            te = new WriteFailureException(cl, received, blockFor, writeType, failureReasonByEndpoint);
                         }
                         else
                         {
                             byte dataPresent = body.readByte();
-                            te = new ReadFailureException(cl, received, failure, blockFor, dataPresent != 0);   
+                            te = new ReadFailureException(cl, received, blockFor, dataPresent != 0, failureReasonByEndpoint);
                         }
                     }
                     break;
@@ -147,7 +163,7 @@
             return new ErrorMessage(te);
         }
 
-        public void encode(ErrorMessage msg, ByteBuf dest, int version)
+        public void encode(ErrorMessage msg, ByteBuf dest, ProtocolVersion version)
         {
             final TransportException err = getBackwardsCompatibleException(msg, version);
             dest.writeInt(err.code().value);
@@ -171,7 +187,17 @@
                         CBUtil.writeConsistencyLevel(rfe.consistency, dest);
                         dest.writeInt(rfe.received);
                         dest.writeInt(rfe.blockFor);
-                        dest.writeInt(rfe.failures);
+                        // The number of failures is also present in protocol v5, but used instead to specify the size of the failure map
+                        dest.writeInt(rfe.failureReasonByEndpoint.size());
+
+                        if (version.isGreaterOrEqualTo(ProtocolVersion.V5))
+                        {
+                            for (Map.Entry<InetAddress, RequestFailureReason> entry : rfe.failureReasonByEndpoint.entrySet())
+                            {
+                                CBUtil.writeInetAddr(entry.getKey(), dest);
+                                dest.writeShort(entry.getValue().code);
+                            }
+                        }
 
                         if (isWrite)
                             CBUtil.writeString(((WriteFailureException)rfe).writeType.toString(), dest);
@@ -210,7 +236,7 @@
             }
         }
 
-        public int encodedSize(ErrorMessage msg, int version)
+        public int encodedSize(ErrorMessage msg, ProtocolVersion version)
         {
             final TransportException err = getBackwardsCompatibleException(msg, version);
             String errorString = err.getMessage() == null ? "" : err.getMessage();
@@ -228,6 +254,15 @@
                         boolean isWrite = err.code() == ExceptionCode.WRITE_FAILURE;
                         size += CBUtil.sizeOfConsistencyLevel(rfe.consistency) + 4 + 4 + 4;
                         size += isWrite ? CBUtil.sizeOfString(((WriteFailureException)rfe).writeType.toString()) : 1;
+
+                        if (version.isGreaterOrEqualTo(ProtocolVersion.V5))
+                        {
+                            for (Map.Entry<InetAddress, RequestFailureReason> entry : rfe.failureReasonByEndpoint.entrySet())
+                            {
+                                size += CBUtil.sizeOfInetAddr(entry.getKey());
+                                size += 2; // RequestFailureReason code
+                            }
+                        }
                     }
                     break;
                 case WRITE_TIMEOUT:
@@ -257,9 +292,9 @@
         }
     };
 
-    private static TransportException getBackwardsCompatibleException(ErrorMessage msg, int version)
+    private static TransportException getBackwardsCompatibleException(ErrorMessage msg, ProtocolVersion version)
     {
-        if (version < Server.VERSION_4)
+        if (version.isSmallerThan(ProtocolVersion.V4))
         {
             switch (msg.error.code())
             {
@@ -335,11 +370,11 @@
             ErrorMessage message = new ErrorMessage((TransportException) e, streamId);
             if (e instanceof ProtocolException)
             {
-                // if the driver attempted to connect with a protocol version lower than the minimum supported
-                // version, respond with a protocol error message with the correct frame header for that version
-                Integer attemptedLowProtocolVersion = ((ProtocolException) e).getAttemptedLowProtocolVersion();
-                if (attemptedLowProtocolVersion != null)
-                    message.forcedProtocolVersion = attemptedLowProtocolVersion;
+                // if the driver attempted to connect with a protocol version not supported then
+                // reply with the appropiate version, see ProtocolVersion.decode()
+                ProtocolVersion forcedProtocolVersion = ((ProtocolException) e).getForcedProtocolVersion();
+                if (forcedProtocolVersion != null)
+                    message.forcedProtocolVersion = forcedProtocolVersion;
             }
             return message;
         }
diff --git a/src/java/org/apache/cassandra/transport/messages/EventMessage.java b/src/java/org/apache/cassandra/transport/messages/EventMessage.java
index f3ab526..0af9e14 100644
--- a/src/java/org/apache/cassandra/transport/messages/EventMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/EventMessage.java
@@ -21,22 +21,23 @@
 
 import org.apache.cassandra.transport.Event;
 import org.apache.cassandra.transport.Message;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 public class EventMessage extends Message.Response
 {
     public static final Message.Codec<EventMessage> codec = new Message.Codec<EventMessage>()
     {
-        public EventMessage decode(ByteBuf body, int version)
+        public EventMessage decode(ByteBuf body, ProtocolVersion version)
         {
             return new EventMessage(Event.deserialize(body, version));
         }
 
-        public void encode(EventMessage msg, ByteBuf dest, int version)
+        public void encode(EventMessage msg, ByteBuf dest, ProtocolVersion version)
         {
             msg.event.serialize(dest, version);
         }
 
-        public int encodedSize(EventMessage msg, int version)
+        public int encodedSize(EventMessage msg, ProtocolVersion version)
         {
             return msg.event.serializedSize(version);
         }
diff --git a/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java b/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java
index 7031b84..76b977c 100644
--- a/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java
@@ -25,6 +25,7 @@
 import io.netty.buffer.ByteBuf;
 
 import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.ColumnSpecification;
 import org.apache.cassandra.cql3.QueryHandler;
 import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.statements.ModificationStatement;
@@ -46,16 +47,16 @@
 
     public static final Message.Codec<ExecuteMessage> codec = new Message.Codec<ExecuteMessage>()
     {
-        public ExecuteMessage decode(ByteBuf body, int version)
+        public ExecuteMessage decode(ByteBuf body, ProtocolVersion version)
         {
             byte[] id = CBUtil.readBytes(body);
             return new ExecuteMessage(MD5Digest.wrap(id), QueryOptions.codec.decode(body, version));
         }
 
-        public void encode(ExecuteMessage msg, ByteBuf dest, int version)
+        public void encode(ExecuteMessage msg, ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeBytes(msg.statementId.bytes, dest);
-            if (version == 1)
+            if (version == ProtocolVersion.V1)
             {
                 CBUtil.writeValueList(msg.options.getValues(), dest);
                 CBUtil.writeConsistencyLevel(msg.options.getConsistency(), dest);
@@ -66,11 +67,11 @@
             }
         }
 
-        public int encodedSize(ExecuteMessage msg, int version)
+        public int encodedSize(ExecuteMessage msg, ProtocolVersion version)
         {
             int size = 0;
             size += CBUtil.sizeOfBytes(msg.statementId.bytes);
-            if (version == 1)
+            if (version == ProtocolVersion.V1)
             {
                 size += CBUtil.sizeOfValueList(msg.options.getValues());
                 size += CBUtil.sizeOfConsistencyLevel(msg.options.getConsistency());
@@ -93,7 +94,7 @@
         this.options = options;
     }
 
-    public Message.Response execute(QueryState state)
+    public Message.Response execute(QueryState state, long queryStartNanoTime)
     {
         try
         {
@@ -127,7 +128,7 @@
 
             if (state.traceNextQuery())
             {
-                state.createTracingSession();
+                state.createTracingSession(getCustomPayload());
 
                 ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
                 if (options.getPageSize() > 0)
@@ -136,15 +137,30 @@
                     builder.put("consistency_level", options.getConsistency().name());
                 if(options.getSerialConsistency() != null)
                     builder.put("serial_consistency_level", options.getSerialConsistency().name());
+                builder.put("query", prepared.rawCQLStatement);
 
-                // TODO we don't have [typed] access to CQL bind variables here.  CASSANDRA-4560 is open to add support.
+                for(int i=0;i<prepared.boundNames.size();i++)
+                {
+                    ColumnSpecification cs = prepared.boundNames.get(i);
+                    String boundName = cs.name.toString();
+                    String boundValue = cs.type.asCQL3Type().toCQLLiteral(options.getValues().get(i), options.getProtocolVersion());
+                    if ( boundValue.length() > 1000 )
+                    {
+                        boundValue = boundValue.substring(0, 1000) + "...'";
+                    }
+
+                    //Here we prefix boundName with the index to avoid possible collission in builder keys due to
+                    //having multiple boundValues for the same variable
+                    builder.put("bound_var_" + Integer.toString(i) + "_" + boundName, boundValue);
+                }
+
                 Tracing.instance.begin("Execute CQL3 prepared query", state.getClientAddress(), builder.build());
             }
 
             // Some custom QueryHandlers are interested by the bound names. We provide them this information
             // by wrapping the QueryOptions.
             QueryOptions queryOptions = QueryOptions.addColumnSpecifications(options, prepared.boundNames);
-            Message.Response response = handler.processPrepared(statement, state, queryOptions, getCustomPayload());
+            Message.Response response = handler.processPrepared(statement, state, queryOptions, getCustomPayload(), queryStartNanoTime);
             if (options.skipMetadata() && response instanceof ResultMessage.Rows)
                 ((ResultMessage.Rows)response).result.metadata.setSkipMetadata();
 
diff --git a/src/java/org/apache/cassandra/transport/messages/OptionsMessage.java b/src/java/org/apache/cassandra/transport/messages/OptionsMessage.java
index 2f6e3da..914ccb1 100644
--- a/src/java/org/apache/cassandra/transport/messages/OptionsMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/OptionsMessage.java
@@ -28,6 +28,7 @@
 import org.apache.cassandra.service.QueryState;
 import org.apache.cassandra.transport.FrameCompressor;
 import org.apache.cassandra.transport.Message;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Message to indicate that the server is ready to receive requests.
@@ -36,16 +37,16 @@
 {
     public static final Message.Codec<OptionsMessage> codec = new Message.Codec<OptionsMessage>()
     {
-        public OptionsMessage decode(ByteBuf body, int version)
+        public OptionsMessage decode(ByteBuf body, ProtocolVersion version)
         {
             return new OptionsMessage();
         }
 
-        public void encode(OptionsMessage msg, ByteBuf dest, int version)
+        public void encode(OptionsMessage msg, ByteBuf dest, ProtocolVersion version)
         {
         }
 
-        public int encodedSize(OptionsMessage msg, int version)
+        public int encodedSize(OptionsMessage msg, ProtocolVersion version)
         {
             return 0;
         }
@@ -56,7 +57,7 @@
         super(Message.Type.OPTIONS);
     }
 
-    public Message.Response execute(QueryState state)
+    public Message.Response execute(QueryState state, long queryStartNanoTime)
     {
         List<String> cqlVersions = new ArrayList<String>();
         cqlVersions.add(QueryProcessor.CQL_VERSION.toString());
@@ -70,6 +71,7 @@
         Map<String, List<String>> supported = new HashMap<String, List<String>>();
         supported.put(StartupMessage.CQL_VERSION, cqlVersions);
         supported.put(StartupMessage.COMPRESSION, compressions);
+        supported.put(StartupMessage.PROTOCOL_VERSIONS, ProtocolVersion.supportedVersions());
 
         return new SupportedMessage(supported);
     }
diff --git a/src/java/org/apache/cassandra/transport/messages/PrepareMessage.java b/src/java/org/apache/cassandra/transport/messages/PrepareMessage.java
index f54d1d9..04d2966 100644
--- a/src/java/org/apache/cassandra/transport/messages/PrepareMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/PrepareMessage.java
@@ -33,18 +33,18 @@
 {
     public static final Message.Codec<PrepareMessage> codec = new Message.Codec<PrepareMessage>()
     {
-        public PrepareMessage decode(ByteBuf body, int version)
+        public PrepareMessage decode(ByteBuf body, ProtocolVersion version)
         {
             String query = CBUtil.readLongString(body);
             return new PrepareMessage(query);
         }
 
-        public void encode(PrepareMessage msg, ByteBuf dest, int version)
+        public void encode(PrepareMessage msg, ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeLongString(msg.query, dest);
         }
 
-        public int encodedSize(PrepareMessage msg, int version)
+        public int encodedSize(PrepareMessage msg, ProtocolVersion version)
         {
             return CBUtil.sizeOfLongString(msg.query);
         }
@@ -58,7 +58,7 @@
         this.query = query;
     }
 
-    public Message.Response execute(QueryState state)
+    public Message.Response execute(QueryState state, long queryStartNanoTime)
     {
         try
         {
@@ -71,7 +71,7 @@
 
             if (state.traceNextQuery())
             {
-                state.createTracingSession();
+                state.createTracingSession(getCustomPayload());
                 Tracing.instance.begin("Preparing CQL3 query", state.getClientAddress(), ImmutableMap.of("query", query));
             }
 
diff --git a/src/java/org/apache/cassandra/transport/messages/QueryMessage.java b/src/java/org/apache/cassandra/transport/messages/QueryMessage.java
index 3b48d52..4c761dd 100644
--- a/src/java/org/apache/cassandra/transport/messages/QueryMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/QueryMessage.java
@@ -31,6 +31,7 @@
 import org.apache.cassandra.transport.CBUtil;
 import org.apache.cassandra.transport.Message;
 import org.apache.cassandra.transport.ProtocolException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.UUIDGen;
 
@@ -41,26 +42,26 @@
 {
     public static final Message.Codec<QueryMessage> codec = new Message.Codec<QueryMessage>()
     {
-        public QueryMessage decode(ByteBuf body, int version)
+        public QueryMessage decode(ByteBuf body, ProtocolVersion version)
         {
             String query = CBUtil.readLongString(body);
             return new QueryMessage(query, QueryOptions.codec.decode(body, version));
         }
 
-        public void encode(QueryMessage msg, ByteBuf dest, int version)
+        public void encode(QueryMessage msg, ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeLongString(msg.query, dest);
-            if (version == 1)
+            if (version == ProtocolVersion.V1)
                 CBUtil.writeConsistencyLevel(msg.options.getConsistency(), dest);
             else
                 QueryOptions.codec.encode(msg.options, dest, version);
         }
 
-        public int encodedSize(QueryMessage msg, int version)
+        public int encodedSize(QueryMessage msg, ProtocolVersion version)
         {
             int size = CBUtil.sizeOfLongString(msg.query);
 
-            if (version == 1)
+            if (version == ProtocolVersion.V1)
             {
                 size += CBUtil.sizeOfConsistencyLevel(msg.options.getConsistency());
             }
@@ -82,7 +83,7 @@
         this.options = options;
     }
 
-    public Message.Response execute(QueryState state)
+    public Message.Response execute(QueryState state, long queryStartNanoTime)
     {
         try
         {
@@ -98,7 +99,7 @@
 
             if (state.traceNextQuery())
             {
-                state.createTracingSession();
+                state.createTracingSession(getCustomPayload());
 
                 ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
                 builder.put("query", query);
@@ -112,7 +113,7 @@
                 Tracing.instance.begin("Execute CQL3 query", state.getClientAddress(), builder.build());
             }
 
-            Message.Response response = ClientState.getCQLQueryHandler().process(query, state, options, getCustomPayload());
+            Message.Response response = ClientState.getCQLQueryHandler().process(query, state, options, getCustomPayload(), queryStartNanoTime);
             if (options.skipMetadata() && response instanceof ResultMessage.Rows)
                 ((ResultMessage.Rows)response).result.metadata.setSkipMetadata();
 
diff --git a/src/java/org/apache/cassandra/transport/messages/ReadyMessage.java b/src/java/org/apache/cassandra/transport/messages/ReadyMessage.java
index f0a4681..2ee8881 100644
--- a/src/java/org/apache/cassandra/transport/messages/ReadyMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/ReadyMessage.java
@@ -20,6 +20,7 @@
 import io.netty.buffer.ByteBuf;
 
 import org.apache.cassandra.transport.Message;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Message to indicate that the server is ready to receive requests.
@@ -28,16 +29,16 @@
 {
     public static final Message.Codec<ReadyMessage> codec = new Message.Codec<ReadyMessage>()
     {
-        public ReadyMessage decode(ByteBuf body, int version)
+        public ReadyMessage decode(ByteBuf body, ProtocolVersion version)
         {
             return new ReadyMessage();
         }
 
-        public void encode(ReadyMessage msg, ByteBuf dest, int version)
+        public void encode(ReadyMessage msg, ByteBuf dest, ProtocolVersion version)
         {
         }
 
-        public int encodedSize(ReadyMessage msg, int version)
+        public int encodedSize(ReadyMessage msg, ProtocolVersion version)
         {
             return 0;
         }
diff --git a/src/java/org/apache/cassandra/transport/messages/RegisterMessage.java b/src/java/org/apache/cassandra/transport/messages/RegisterMessage.java
index 928e676..2356dae 100644
--- a/src/java/org/apache/cassandra/transport/messages/RegisterMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/RegisterMessage.java
@@ -29,7 +29,7 @@
 {
     public static final Message.Codec<RegisterMessage> codec = new Message.Codec<RegisterMessage>()
     {
-        public RegisterMessage decode(ByteBuf body, int version)
+        public RegisterMessage decode(ByteBuf body, ProtocolVersion version)
         {
             int length = body.readUnsignedShort();
             List<Event.Type> eventTypes = new ArrayList<>(length);
@@ -38,14 +38,14 @@
             return new RegisterMessage(eventTypes);
         }
 
-        public void encode(RegisterMessage msg, ByteBuf dest, int version)
+        public void encode(RegisterMessage msg, ByteBuf dest, ProtocolVersion version)
         {
             dest.writeShort(msg.eventTypes.size());
             for (Event.Type type : msg.eventTypes)
                 CBUtil.writeEnumValue(type, dest);
         }
 
-        public int encodedSize(RegisterMessage msg, int version)
+        public int encodedSize(RegisterMessage msg, ProtocolVersion version)
         {
             int size = 2;
             for (Event.Type type : msg.eventTypes)
@@ -62,14 +62,14 @@
         this.eventTypes = eventTypes;
     }
 
-    public Response execute(QueryState state)
+    public Response execute(QueryState state, long queryStartNanoTime)
     {
         assert connection instanceof ServerConnection;
         Connection.Tracker tracker = connection.getTracker();
         assert tracker instanceof Server.ConnectionTracker;
         for (Event.Type type : eventTypes)
         {
-            if (type.minimumVersion > connection.getVersion())
+            if (type.minimumVersion.isGreaterThan(connection.getVersion()))
                 throw new ProtocolException("Event " + type.name() + " not valid for protocol version " + connection.getVersion());
             ((Server.ConnectionTracker) tracker).register(type, connection().channel());
         }
diff --git a/src/java/org/apache/cassandra/transport/messages/ResultMessage.java b/src/java/org/apache/cassandra/transport/messages/ResultMessage.java
index b76243f..05a1276 100644
--- a/src/java/org/apache/cassandra/transport/messages/ResultMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/ResultMessage.java
@@ -36,19 +36,19 @@
 {
     public static final Message.Codec<ResultMessage> codec = new Message.Codec<ResultMessage>()
     {
-        public ResultMessage decode(ByteBuf body, int version)
+        public ResultMessage decode(ByteBuf body, ProtocolVersion version)
         {
             Kind kind = Kind.fromId(body.readInt());
             return kind.subcodec.decode(body, version);
         }
 
-        public void encode(ResultMessage msg, ByteBuf dest, int version)
+        public void encode(ResultMessage msg, ByteBuf dest, ProtocolVersion version)
         {
             dest.writeInt(msg.kind.id);
             msg.kind.subcodec.encode(msg, dest, version);
         }
 
-        public int encodedSize(ResultMessage msg, int version)
+        public int encodedSize(ResultMessage msg, ProtocolVersion version)
         {
             return 4 + msg.kind.subcodec.encodedSize(msg, version);
         }
@@ -116,17 +116,17 @@
 
         public static final Message.Codec<ResultMessage> subcodec = new Message.Codec<ResultMessage>()
         {
-            public ResultMessage decode(ByteBuf body, int version)
+            public ResultMessage decode(ByteBuf body, ProtocolVersion version)
             {
                 return new Void();
             }
 
-            public void encode(ResultMessage msg, ByteBuf dest, int version)
+            public void encode(ResultMessage msg, ByteBuf dest, ProtocolVersion version)
             {
                 assert msg instanceof Void;
             }
 
-            public int encodedSize(ResultMessage msg, int version)
+            public int encodedSize(ResultMessage msg, ProtocolVersion version)
             {
                 return 0;
             }
@@ -156,19 +156,19 @@
 
         public static final Message.Codec<ResultMessage> subcodec = new Message.Codec<ResultMessage>()
         {
-            public ResultMessage decode(ByteBuf body, int version)
+            public ResultMessage decode(ByteBuf body, ProtocolVersion version)
             {
                 String keyspace = CBUtil.readString(body);
                 return new SetKeyspace(keyspace);
             }
 
-            public void encode(ResultMessage msg, ByteBuf dest, int version)
+            public void encode(ResultMessage msg, ByteBuf dest, ProtocolVersion version)
             {
                 assert msg instanceof SetKeyspace;
                 CBUtil.writeString(((SetKeyspace)msg).keyspace, dest);
             }
 
-            public int encodedSize(ResultMessage msg, int version)
+            public int encodedSize(ResultMessage msg, ProtocolVersion version)
             {
                 assert msg instanceof SetKeyspace;
                 return CBUtil.sizeOfString(((SetKeyspace)msg).keyspace);
@@ -191,19 +191,19 @@
     {
         public static final Message.Codec<ResultMessage> subcodec = new Message.Codec<ResultMessage>()
         {
-            public ResultMessage decode(ByteBuf body, int version)
+            public ResultMessage decode(ByteBuf body, ProtocolVersion version)
             {
                 return new Rows(ResultSet.codec.decode(body, version));
             }
 
-            public void encode(ResultMessage msg, ByteBuf dest, int version)
+            public void encode(ResultMessage msg, ByteBuf dest, ProtocolVersion version)
             {
                 assert msg instanceof Rows;
                 Rows rowMsg = (Rows)msg;
                 ResultSet.codec.encode(rowMsg.result, dest, version);
             }
 
-            public int encodedSize(ResultMessage msg, int version)
+            public int encodedSize(ResultMessage msg, ProtocolVersion version)
             {
                 assert msg instanceof Rows;
                 Rows rowMsg = (Rows)msg;
@@ -235,19 +235,19 @@
     {
         public static final Message.Codec<ResultMessage> subcodec = new Message.Codec<ResultMessage>()
         {
-            public ResultMessage decode(ByteBuf body, int version)
+            public ResultMessage decode(ByteBuf body, ProtocolVersion version)
             {
                 MD5Digest id = MD5Digest.wrap(CBUtil.readBytes(body));
                 ResultSet.PreparedMetadata metadata = ResultSet.PreparedMetadata.codec.decode(body, version);
 
                 ResultSet.ResultMetadata resultMetadata = ResultSet.ResultMetadata.EMPTY;
-                if (version > 1)
+                if (version.isGreaterThan(ProtocolVersion.V1))
                     resultMetadata = ResultSet.ResultMetadata.codec.decode(body, version);
 
                 return new Prepared(id, -1, metadata, resultMetadata);
             }
 
-            public void encode(ResultMessage msg, ByteBuf dest, int version)
+            public void encode(ResultMessage msg, ByteBuf dest, ProtocolVersion version)
             {
                 assert msg instanceof Prepared;
                 Prepared prepared = (Prepared)msg;
@@ -255,11 +255,11 @@
 
                 CBUtil.writeBytes(prepared.statementId.bytes, dest);
                 ResultSet.PreparedMetadata.codec.encode(prepared.metadata, dest, version);
-                if (version > 1)
+                if (version.isGreaterThan(ProtocolVersion.V1))
                     ResultSet.ResultMetadata.codec.encode(prepared.resultMetadata, dest, version);
             }
 
-            public int encodedSize(ResultMessage msg, int version)
+            public int encodedSize(ResultMessage msg, ProtocolVersion version)
             {
                 assert msg instanceof Prepared;
                 Prepared prepared = (Prepared)msg;
@@ -268,7 +268,7 @@
                 int size = 0;
                 size += CBUtil.sizeOfBytes(prepared.statementId.bytes);
                 size += ResultSet.PreparedMetadata.codec.encodedSize(prepared.metadata, version);
-                if (version > 1)
+                if (version.isGreaterThan(ProtocolVersion.V1))
                     size += ResultSet.ResultMetadata.codec.encodedSize(prepared.resultMetadata, version);
                 return size;
             }
@@ -348,19 +348,19 @@
 
         public static final Message.Codec<ResultMessage> subcodec = new Message.Codec<ResultMessage>()
         {
-            public ResultMessage decode(ByteBuf body, int version)
+            public ResultMessage decode(ByteBuf body, ProtocolVersion version)
             {
                 return new SchemaChange(Event.SchemaChange.deserializeEvent(body, version));
             }
 
-            public void encode(ResultMessage msg, ByteBuf dest, int version)
+            public void encode(ResultMessage msg, ByteBuf dest, ProtocolVersion version)
             {
                 assert msg instanceof SchemaChange;
                 SchemaChange scm = (SchemaChange)msg;
                 scm.change.serializeEvent(dest, version);
             }
 
-            public int encodedSize(ResultMessage msg, int version)
+            public int encodedSize(ResultMessage msg, ProtocolVersion version)
             {
                 assert msg instanceof SchemaChange;
                 SchemaChange scm = (SchemaChange)msg;
diff --git a/src/java/org/apache/cassandra/transport/messages/StartupMessage.java b/src/java/org/apache/cassandra/transport/messages/StartupMessage.java
index 92278fa..8b4b0a4 100644
--- a/src/java/org/apache/cassandra/transport/messages/StartupMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/StartupMessage.java
@@ -35,22 +35,23 @@
 {
     public static final String CQL_VERSION = "CQL_VERSION";
     public static final String COMPRESSION = "COMPRESSION";
+    public static final String PROTOCOL_VERSIONS = "PROTOCOL_VERSIONS";
     public static final String NO_COMPACT = "NO_COMPACT";
     public static final String THROW_ON_OVERLOAD = "THROW_ON_OVERLOAD";
 
     public static final Message.Codec<StartupMessage> codec = new Message.Codec<StartupMessage>()
     {
-        public StartupMessage decode(ByteBuf body, int version)
+        public StartupMessage decode(ByteBuf body, ProtocolVersion version)
         {
             return new StartupMessage(upperCaseKeys(CBUtil.readStringMap(body)));
         }
 
-        public void encode(StartupMessage msg, ByteBuf dest, int version)
+        public void encode(StartupMessage msg, ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeStringMap(msg.options, dest);
         }
 
-        public int encodedSize(StartupMessage msg, int version)
+        public int encodedSize(StartupMessage msg, ProtocolVersion version)
         {
             return CBUtil.sizeOfStringMap(msg.options);
         }
@@ -64,13 +65,13 @@
         this.options = options;
     }
 
-    public Message.Response execute(QueryState state)
+    public Message.Response execute(QueryState state, long queryStartNanoTime)
     {
         String cqlVersion = options.get(CQL_VERSION);
         if (cqlVersion == null)
             throw new ProtocolException("Missing value CQL_VERSION in STARTUP message");
 
-        try 
+        try
         {
             if (new CassandraVersion(cqlVersion).compareTo(new CassandraVersion("2.99.0")) < 0)
                 throw new ProtocolException(String.format("CQL version %s is not supported by the binary protocol (supported version are >= 3.0.0)", cqlVersion));
diff --git a/src/java/org/apache/cassandra/transport/messages/SupportedMessage.java b/src/java/org/apache/cassandra/transport/messages/SupportedMessage.java
index 539085f..0367b78 100644
--- a/src/java/org/apache/cassandra/transport/messages/SupportedMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/SupportedMessage.java
@@ -24,6 +24,7 @@
 
 import org.apache.cassandra.transport.CBUtil;
 import org.apache.cassandra.transport.Message;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Message to indicate that the server is ready to receive requests.
@@ -32,17 +33,17 @@
 {
     public static final Message.Codec<SupportedMessage> codec = new Message.Codec<SupportedMessage>()
     {
-        public SupportedMessage decode(ByteBuf body, int version)
+        public SupportedMessage decode(ByteBuf body, ProtocolVersion version)
         {
             return new SupportedMessage(CBUtil.readStringToStringListMap(body));
         }
 
-        public void encode(SupportedMessage msg, ByteBuf dest, int version)
+        public void encode(SupportedMessage msg, ByteBuf dest, ProtocolVersion version)
         {
             CBUtil.writeStringToStringListMap(msg.supported, dest);
         }
 
-        public int encodedSize(SupportedMessage msg, int version)
+        public int encodedSize(SupportedMessage msg, ProtocolVersion version)
         {
             return CBUtil.sizeOfStringToStringListMap(msg.supported);
         }
diff --git a/src/java/org/apache/cassandra/triggers/CustomClassLoader.java b/src/java/org/apache/cassandra/triggers/CustomClassLoader.java
index 965da4b..32a987f 100644
--- a/src/java/org/apache/cassandra/triggers/CustomClassLoader.java
+++ b/src/java/org/apache/cassandra/triggers/CustomClassLoader.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.triggers;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
@@ -67,8 +67,10 @@
     {
         if (dir == null || !dir.exists())
             return;
-        FilenameFilter filter = new FilenameFilter() {
-            public boolean accept(File dir, String name) {
+        FilenameFilter filter = new FilenameFilter()
+        {
+            public boolean accept(File dir, String name)
+            {
                 return name.endsWith(".jar");
             }
         };
diff --git a/src/java/org/apache/cassandra/triggers/ITrigger.java b/src/java/org/apache/cassandra/triggers/ITrigger.java
index ad631d1..860d3e9 100644
--- a/src/java/org/apache/cassandra/triggers/ITrigger.java
+++ b/src/java/org/apache/cassandra/triggers/ITrigger.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.triggers;
 /*
- * 
+ *
  * 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
@@ -8,20 +8,19 @@
  * 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.
- * 
+ *
  */
 
 
-import java.nio.ByteBuffer;
 import java.util.Collection;
 
 import org.apache.cassandra.db.Mutation;
diff --git a/src/java/org/apache/cassandra/triggers/TriggerExecutor.java b/src/java/org/apache/cassandra/triggers/TriggerExecutor.java
index 3996127..0354fde 100644
--- a/src/java/org/apache/cassandra/triggers/TriggerExecutor.java
+++ b/src/java/org/apache/cassandra/triggers/TriggerExecutor.java
@@ -6,9 +6,9 @@
  * 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
@@ -31,6 +31,7 @@
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.exceptions.CassandraException;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.schema.TriggerMetadata;
 import org.apache.cassandra.schema.Triggers;
@@ -236,9 +237,13 @@
             }
             return tmutations;
         }
+        catch (CassandraException ex)
+        {
+            throw ex;
+        }
         catch (Exception ex)
         {
-            throw new RuntimeException(String.format("Exception while creating trigger on table with ID: %s", update.metadata().cfId), ex);
+            throw new RuntimeException(String.format("Exception while executing trigger on table with ID: %s", update.metadata().cfId), ex);
         }
         finally
         {
diff --git a/src/java/org/apache/cassandra/utils/Architecture.java b/src/java/org/apache/cassandra/utils/Architecture.java
new file mode 100644
index 0000000..aa00ca1
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/Architecture.java
@@ -0,0 +1,45 @@
+/*
+* 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.utils;
+
+import java.util.Collections;
+import java.util.Set;
+
+import com.google.common.collect.Sets;
+
+public final class Architecture
+{
+    // Note that s390x & aarch64 architecture are not officially supported and adding it here is only done out of convenience
+    // for those that want to run C* on this architecture at their own risk (see #11214 & #13326)
+    private static final Set<String> UNALIGNED_ARCH = Collections.unmodifiableSet(Sets.newHashSet(
+        "i386",
+        "x86",
+        "amd64",
+        "x86_64",
+        "s390x",
+        "aarch64"
+    ));
+
+    public static final boolean IS_UNALIGNED = UNALIGNED_ARCH.contains(System.getProperty("os.arch"));
+
+    private Architecture()
+    {
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/BackgroundActivityMonitor.java b/src/java/org/apache/cassandra/utils/BackgroundActivityMonitor.java
deleted file mode 100644
index 711c5dd..0000000
--- a/src/java/org/apache/cassandra/utils/BackgroundActivityMonitor.java
+++ /dev/null
@@ -1,177 +0,0 @@
-package org.apache.cassandra.utils;
-/*
- * 
- * 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.
- * 
- */
-
-
-import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.lang.management.ManagementFactory;
-import java.net.InetAddress;
-import java.util.StringTokenizer;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import org.apache.cassandra.concurrent.DebuggableScheduledThreadPoolExecutor;
-import org.apache.cassandra.gms.ApplicationState;
-import org.apache.cassandra.gms.EndpointState;
-import org.apache.cassandra.gms.Gossiper;
-import org.apache.cassandra.gms.VersionedValue;
-import org.apache.cassandra.service.StorageService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.util.concurrent.AtomicDouble;
-
-public class BackgroundActivityMonitor
-{
-    private static final Logger logger = LoggerFactory.getLogger(BackgroundActivityMonitor.class);
-
-    public static final int USER_INDEX = 0;
-    public static final int NICE_INDEX = 1;
-    public static final int SYS_INDEX = 2;
-    public static final int IDLE_INDEX = 3;
-    public static final int IOWAIT_INDEX = 4;
-    public static final int IRQ_INDEX = 5;
-    public static final int SOFTIRQ_INDEX = 6;
-
-    private static final int NUM_CPUS = Runtime.getRuntime().availableProcessors();
-    private static final String PROC_STAT_PATH = "/proc/stat";
-
-    private final AtomicDouble compaction_severity = new AtomicDouble();
-    private final AtomicDouble manual_severity = new AtomicDouble();
-    private final ScheduledExecutorService reportThread = new DebuggableScheduledThreadPoolExecutor("Background_Reporter");
-
-    private RandomAccessFile statsFile;
-    private long[] lastReading;
-
-    public BackgroundActivityMonitor()
-    {
-        try
-        {
-            statsFile = new RandomAccessFile(PROC_STAT_PATH, "r");
-            lastReading = readAndCompute();
-        }
-        catch (IOException ex)
-        {
-            if (FBUtilities.hasProcFS())
-                logger.warn("Couldn't open /proc/stats");
-            statsFile = null;
-        }
-        reportThread.scheduleAtFixedRate(new BackgroundActivityReporter(), 1, 1, TimeUnit.SECONDS);
-    }
-
-    private long[] readAndCompute() throws IOException
-    {
-        statsFile.seek(0);
-        StringTokenizer tokenizer = new StringTokenizer(statsFile.readLine());
-        String name = tokenizer.nextToken();
-        assert name.equalsIgnoreCase("cpu");
-        long[] returned = new long[tokenizer.countTokens()];
-        for (int i = 0; i < returned.length; i++)
-            returned[i] = Long.parseLong(tokenizer.nextToken());
-        return returned;
-    }
-
-    private float compareAtIndex(long[] reading1, long[] reading2, int index)
-    {
-        long total1 = 0, total2 = 0;
-        for (int i = 0; i <= SOFTIRQ_INDEX; i++)
-        {
-            total1 += reading1[i];
-            total2 += reading2[i];
-        }
-        float totalDiff = total2 - total1;
-
-        long intrested1 = reading1[index], intrested2 = reading2[index];
-        float diff = intrested2 - intrested1;
-        if (diff == 0)
-            return 0f;
-        return (diff / totalDiff) * 100; // yes it is hard coded to 100 [update
-                                         // unit?]
-    }
-
-    public void incrCompactionSeverity(double sev)
-    {
-        compaction_severity.addAndGet(sev);
-    }
-
-    public void incrManualSeverity(double sev)
-    {
-        manual_severity.addAndGet(sev);
-    }
-
-    public double getIOWait() throws IOException
-    {
-        if (statsFile == null)
-            return -1d;
-        long[] newComp = readAndCompute();
-        double value = compareAtIndex(lastReading, newComp, IOWAIT_INDEX);
-        lastReading = newComp;
-        return value;
-    }
-
-    public double getNormalizedLoadAvg()
-    {
-        double avg = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
-        return avg / NUM_CPUS;
-    }
-
-    public double getSeverity(InetAddress endpoint)
-    {
-        VersionedValue event;
-        EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint);
-        if (state != null && (event = state.getApplicationState(ApplicationState.SEVERITY)) != null)
-            return Double.parseDouble(event.value);
-        return 0.0;
-    }
-
-    public void shutdownAndWait(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException
-    {
-        ExecutorUtils.shutdownAndWait(timeout, unit, reportThread);
-    }
-
-    public class BackgroundActivityReporter implements Runnable
-    {
-        public void run()
-        {
-            double report = -1;
-            try
-            {
-                report = getIOWait();
-            }
-            catch (IOException e)
-            {
-                // ignore;
-                if (FBUtilities.hasProcFS())
-                    logger.warn("Couldn't read /proc/stats");
-            }
-            if (report == -1d)
-                report = compaction_severity.get();
-
-            if (!Gossiper.instance.isEnabled())
-                return;
-            report += manual_severity.get(); // add manual severity setting.
-            VersionedValue updated = StorageService.instance.valueFactory.severity(report);
-            Gossiper.instance.addLocalApplicationState(ApplicationState.SEVERITY, updated);
-        }
-    }
-}
diff --git a/src/java/org/apache/cassandra/utils/BloomCalculations.java b/src/java/org/apache/cassandra/utils/BloomCalculations.java
index 7ba5452..9bd203d 100644
--- a/src/java/org/apache/cassandra/utils/BloomCalculations.java
+++ b/src/java/org/apache/cassandra/utils/BloomCalculations.java
@@ -40,7 +40,8 @@
      * Each cell (i,j) the false positive rate determined by using i buckets per
      * element and j hash functions.
      */
-    static final double[][] probs = new double[][]{
+    static final double[][] probs = new double[][]
+    {
         {1.0}, // dummy row representing 0 buckets per element
         {1.0, 1.0}, // dummy row representing 1 buckets per element
         {1.0, 0.393,  0.400},
@@ -143,10 +144,12 @@
         int maxK = probs[maxBucketsPerElement].length - 1;
 
         // Handle the trivial cases
-        if(maxFalsePosProb >= probs[minBuckets][minK]) {
+        if(maxFalsePosProb >= probs[minBuckets][minK])
+        {
             return new BloomSpecification(2, optKPerBuckets[2]);
         }
-        if (maxFalsePosProb < probs[maxBucketsPerElement][maxK]) {
+        if (maxFalsePosProb < probs[maxBucketsPerElement][maxK])
+        {
             throw new UnsupportedOperationException(String.format("Unable to satisfy %s with %s buckets per element",
                                                                   maxFalsePosProb, maxBucketsPerElement));
         }
@@ -154,13 +157,15 @@
         // First find the minimal required number of buckets:
         int bucketsPerElement = 2;
         int K = optKPerBuckets[2];
-        while(probs[bucketsPerElement][K] > maxFalsePosProb){
+        while(probs[bucketsPerElement][K] > maxFalsePosProb)
+        {
             bucketsPerElement++;
             K = optKPerBuckets[bucketsPerElement];
         }
         // Now that the number of buckets is sufficient, see if we can relax K
         // without losing too much precision.
-        while(probs[bucketsPerElement][K - 1] <= maxFalsePosProb){
+        while(probs[bucketsPerElement][K - 1] <= maxFalsePosProb)
+        {
             K--;
         }
 
diff --git a/src/java/org/apache/cassandra/utils/BloomFilter.java b/src/java/org/apache/cassandra/utils/BloomFilter.java
index ce6c638..4ff07b7 100644
--- a/src/java/org/apache/cassandra/utils/BloomFilter.java
+++ b/src/java/org/apache/cassandra/utils/BloomFilter.java
@@ -19,13 +19,15 @@
 
 import com.google.common.annotations.VisibleForTesting;
 
+import io.netty.util.concurrent.FastThreadLocal;
+import net.nicoulaj.compilecommand.annotations.Inline;
 import org.apache.cassandra.utils.concurrent.Ref;
 import org.apache.cassandra.utils.concurrent.WrappedSharedCloseable;
 import org.apache.cassandra.utils.obs.IBitSet;
 
 public class BloomFilter extends WrappedSharedCloseable implements IFilter
 {
-    private static final ThreadLocal<long[]> reusableIndexes = new ThreadLocal<long[]>()
+    private final static FastThreadLocal<long[]> reusableIndexes = new FastThreadLocal<long[]>()
     {
         protected long[] initialValue()
         {
@@ -84,16 +86,19 @@
     // to avoid generating a lot of garbage since stack allocation currently does not support stores
     // (CASSANDRA-6609).  it returns the array so that the caller does not need to perform
     // a second threadlocal lookup.
+    @Inline
     private long[] indexes(FilterKey key)
     {
         // we use the same array both for storing the hash result, and for storing the indexes we return,
         // so that we do not need to allocate two arrays.
         long[] indexes = reusableIndexes.get();
+
         key.filterHash(indexes);
         setIndexes(indexes[1], indexes[0], hashCount, bitset.capacity(), indexes);
         return indexes;
     }
 
+    @Inline
     private void setIndexes(long base, long inc, int count, long max, long[] results)
     {
         if (oldBfHashOrder)
diff --git a/src/java/org/apache/cassandra/utils/ByteBufferUtil.java b/src/java/org/apache/cassandra/utils/ByteBufferUtil.java
index 525dc9f..1dc2774 100644
--- a/src/java/org/apache/cassandra/utils/ByteBufferUtil.java
+++ b/src/java/org/apache/cassandra/utils/ByteBufferUtil.java
@@ -35,8 +35,8 @@
 import net.nicoulaj.compilecommand.annotations.Inline;
 import org.apache.cassandra.db.TypeSizes;
 import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.compress.BufferType;
 import org.apache.cassandra.io.util.DataOutputPlus;
-import org.apache.cassandra.io.util.FileDataInput;
 import org.apache.cassandra.io.util.FileUtils;
 
 /**
@@ -162,7 +162,6 @@
     public static byte[] getArray(ByteBuffer buffer)
     {
         int length = buffer.remaining();
-
         if (buffer.hasArray())
         {
             int boff = buffer.arrayOffset() + buffer.position();
@@ -386,7 +385,6 @@
 
     /**
      * @param in data input
-     * @return null
      * @throws IOException if an I/O error occurs.
      */
     public static void skipShortLength(DataInputPlus in) throws IOException
@@ -543,8 +541,17 @@
         };
     }
 
+    /*
+     * Does not modify position or limit of buffer even temporarily
+     * so this is safe even without duplication.
+     */
     public static String bytesToHex(ByteBuffer bytes)
     {
+        if (bytes.hasArray())
+        {
+            return Hex.bytesToHex(bytes.array(), bytes.arrayOffset() + bytes.position(), bytes.remaining());
+        }
+
         final int offset = bytes.position();
         final int size = bytes.remaining();
         final char[] c = new char[size * 2];
@@ -678,4 +685,120 @@
         return readBytes(bb, length);
     }
 
+    /**
+     * Ensure {@code buf} is large enough for {@code outputLength}. If not, it is cleaned up and a new buffer is allocated;
+     * else; buffer has it's position/limit set appropriately.
+     *
+     * @param buf buffer to test the size of; may be null, in which case, a new buffer is allocated.
+     * @param outputLength the minimum target size of the buffer
+     * @param allowBufferResize true if resizing (reallocating) the buffer is allowed
+     * @return {@code buf} if it was large enough, else a newly allocated buffer.
+     */
+    public static ByteBuffer ensureCapacity(ByteBuffer buf, int outputLength, boolean allowBufferResize)
+    {
+        BufferType bufferType = buf != null ? BufferType.typeOf(buf) : BufferType.ON_HEAP;
+        return ensureCapacity(buf, outputLength, allowBufferResize, bufferType);
+    }
+
+    /**
+     * Ensure {@code buf} is large enough for {@code outputLength}. If not, it is cleaned up and a new buffer is allocated;
+     * else; buffer has it's position/limit set appropriately.
+     *
+     * @param buf buffer to test the size of; may be null, in which case, a new buffer is allocated.
+     * @param outputLength the minimum target size of the buffer
+     * @param allowBufferResize true if resizing (reallocating) the buffer is allowed
+     * @param bufferType on- or off- heap byte buffer
+     * @return {@code buf} if it was large enough, else a newly allocated buffer.
+     */
+    public static ByteBuffer ensureCapacity(ByteBuffer buf, int outputLength, boolean allowBufferResize, BufferType bufferType)
+    {
+        if (0 > outputLength)
+            throw new IllegalArgumentException("invalid size for output buffer: " + outputLength);
+        if (buf == null || buf.capacity() < outputLength)
+        {
+            if (!allowBufferResize)
+                throw new IllegalStateException(String.format("output buffer is not large enough for data: current capacity %d, required %d", buf.capacity(), outputLength));
+            FileUtils.clean(buf);
+            buf = bufferType.allocate(outputLength);
+        }
+        else
+        {
+            buf.position(0).limit(outputLength);
+        }
+        return buf;
+    }
+
+    /**
+     * Check is the given buffer contains a given sub-buffer.
+     *
+     * @param buffer The buffer to search for sequence of bytes in.
+     * @param subBuffer The buffer to match.
+     *
+     * @return true if buffer contains sub-buffer, false otherwise.
+     */
+    public static boolean contains(ByteBuffer buffer, ByteBuffer subBuffer)
+    {
+        int len = subBuffer.remaining();
+        if (buffer.remaining() - len < 0)
+            return false;
+
+        // adapted form the JDK's String.indexOf()
+        byte first = subBuffer.get(subBuffer.position());
+        int max = buffer.position() + (buffer.remaining() - len);
+
+        for (int i = buffer.position(); i <= max; i++)
+        {
+            /* Look for first character. */
+            if (buffer.get(i) != first)
+            {
+                while (++i <= max && buffer.get(i) != first)
+                {}
+            }
+
+            /* (maybe) Found first character, now look at the rest of v2 */
+            if (i <= max)
+            {
+                int j = i + 1;
+                int end = j + len - 1;
+                for (int k = 1 + subBuffer.position(); j < end && buffer.get(j) == subBuffer.get(k); j++, k++)
+                {}
+
+                if (j == end)
+                    return true;
+            }
+        }
+        return false;
+    }
+
+    public static boolean startsWith(ByteBuffer src, ByteBuffer prefix)
+    {
+        return startsWith(src, prefix, 0);
+    }
+
+    public static boolean endsWith(ByteBuffer src, ByteBuffer suffix)
+    {
+        return startsWith(src, suffix, src.remaining() - suffix.remaining());
+    }
+
+    private static boolean startsWith(ByteBuffer src, ByteBuffer prefix, int offset)
+    {
+        if (offset < 0)
+            return false;
+
+        int sPos = src.position() + offset;
+        int pPos = prefix.position();
+
+        if (src.remaining() - offset < prefix.remaining())
+            return false;
+
+        int len = Math.min(src.remaining() - offset, prefix.remaining());
+
+        while (len-- > 0)
+        {
+            if (src.get(sPos++) != prefix.get(pPos++))
+                return false;
+        }
+
+        return true;
+    }
 }
diff --git a/src/java/org/apache/cassandra/utils/CassandraVersion.java b/src/java/org/apache/cassandra/utils/CassandraVersion.java
index 62d68be..bf9fe6a 100644
--- a/src/java/org/apache/cassandra/utils/CassandraVersion.java
+++ b/src/java/org/apache/cassandra/utils/CassandraVersion.java
@@ -26,12 +26,19 @@
 
 /**
  * Implements versioning used in Cassandra and CQL.
- * <p/>
+ * <p>
  * Note: The following code uses a slight variation from the semver document (http://semver.org).
+ * </p>
  */
 public class CassandraVersion implements Comparable<CassandraVersion>
 {
-    private static final String VERSION_REGEXP = "(\\d+)\\.(\\d+)\\.(\\d+)(\\-[.\\w]+)?([.+][.\\w]+)?";
+    /**
+     * note: 3rd group matches to words but only allows number and checked after regexp test.
+     * this is because 3rd and the last can be identical.
+     **/
+    private static final String VERSION_REGEXP = "(\\d+)\\.(\\d+)(?:\\.(\\w+))?(\\-[.\\w]+)?([.+][.\\w]+)?";
+    private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\w+");
+
     private static final Pattern pattern = Pattern.compile(VERSION_REGEXP);
     private static final Pattern SNAPSHOT = Pattern.compile("-SNAPSHOT");
 
@@ -42,15 +49,6 @@
     private final String[] preRelease;
     private final String[] build;
 
-    private CassandraVersion(int major, int minor, int patch, String[] preRelease, String[] build)
-    {
-        this.major = major;
-        this.minor = minor;
-        this.patch = patch;
-        this.preRelease = preRelease;
-        this.build = build;
-    }
-
     /**
      * Parse a version from a string.
      *
@@ -69,7 +67,7 @@
         {
             this.major = Integer.parseInt(matcher.group(1));
             this.minor = Integer.parseInt(matcher.group(2));
-            this.patch = Integer.parseInt(matcher.group(3));
+            this.patch = matcher.group(3) != null ? Integer.parseInt(matcher.group(3)) : 0;
 
             String pr = matcher.group(4);
             String bld = matcher.group(5);
@@ -79,7 +77,7 @@
         }
         catch (NumberFormatException e)
         {
-            throw new IllegalArgumentException("Invalid version value: " + version);
+            throw new IllegalArgumentException("Invalid version value: " + version, e);
         }
     }
 
@@ -87,10 +85,10 @@
     {
         // Drop initial - or +
         str = str.substring(1);
-        String[] parts = str.split("\\.");
+        String[] parts = StringUtils.split(str, '.');
         for (String part : parts)
         {
-            if (!part.matches("\\w+"))
+            if (!PATTERN_WHITESPACE.matcher(part).matches())
                 throw new IllegalArgumentException("Invalid version value: " + version);
         }
         return parts;
@@ -120,16 +118,22 @@
         return compareIdentifiers(build, other.build, -1);
     }
 
+    public boolean is30()
+    {
+        return major == 3 && minor == 0;
+    }
+
     /**
      * Returns a version that is backward compatible with this version amongst a list
      * of provided version, or null if none can be found.
-     * <p/>
+     * <p>
      * For instance:
      * "2.0.0".findSupportingVersion("2.0.0", "3.0.0") == "2.0.0"
      * "2.0.0".findSupportingVersion("2.1.3", "3.0.0") == "2.1.3"
      * "2.0.0".findSupportingVersion("3.0.0") == null
      * "2.0.3".findSupportingVersion("2.0.0") == "2.0.0"
      * "2.1.0".findSupportingVersion("2.0.0") == null
+     * </p>
      */
     public CassandraVersion findSupportingVersion(CassandraVersion... versions)
     {
@@ -143,7 +147,7 @@
 
     public boolean isSupportedBy(CassandraVersion version)
     {
-        return major == version.major && this.compareTo(version) <= 0;
+        return version != null && major == version.major && this.compareTo(version) <= 0;
     }
 
     private static int compareIdentifiers(String[] ids1, String[] ids2, int defaultPred)
diff --git a/src/java/org/apache/cassandra/utils/ChecksumType.java b/src/java/org/apache/cassandra/utils/ChecksumType.java
index 6375fa3..413a171 100644
--- a/src/java/org/apache/cassandra/utils/ChecksumType.java
+++ b/src/java/org/apache/cassandra/utils/ChecksumType.java
@@ -17,16 +17,16 @@
  */
 package org.apache.cassandra.utils;
 
-import io.netty.util.concurrent.FastThreadLocal;
-
 import java.nio.ByteBuffer;
 import java.util.zip.Checksum;
 import java.util.zip.CRC32;
 import java.util.zip.Adler32;
 
+import io.netty.util.concurrent.FastThreadLocal;
+
 public enum ChecksumType
 {
-    Adler32()
+    Adler32
     {
 
         @Override
@@ -42,7 +42,7 @@
         }
 
     },
-    CRC32()
+    CRC32
     {
 
         @Override
@@ -60,7 +60,6 @@
     };
 
     public abstract Checksum newInstance();
-
     public abstract void update(Checksum checksum, ByteBuffer buf);
 
     private FastThreadLocal<Checksum> instances = new FastThreadLocal<Checksum>()
@@ -78,4 +77,12 @@
         update(checksum, buf);
         return checksum.getValue();
     }
+
+    public long of(byte[] data, int off, int len)
+    {
+        Checksum checksum = instances.get();
+        checksum.reset();
+        checksum.update(data, off, len);
+        return checksum.getValue();
+    }
 }
diff --git a/src/java/org/apache/cassandra/utils/CoalescingStrategies.java b/src/java/org/apache/cassandra/utils/CoalescingStrategies.java
index d79fa15..9f3b118 100644
--- a/src/java/org/apache/cassandra/utils/CoalescingStrategies.java
+++ b/src/java/org/apache/cassandra/utils/CoalescingStrategies.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.utils;
 
+import org.apache.cassandra.concurrent.NamedThreadFactory;
 import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.io.util.FileUtils;
@@ -53,7 +54,8 @@
     private static final String DEBUG_COALESCING_PATH_PROPERTY = Config.PROPERTY_PREFIX + "coalescing_debug_path";
     private static final String DEBUG_COALESCING_PATH = System.getProperty(DEBUG_COALESCING_PATH_PROPERTY, "/tmp/coleascing_debug");
 
-    static {
+    static
+    {
         if (DEBUG_COALESCING)
         {
             File directory = new File(DEBUG_COALESCING_PATH);
@@ -81,7 +83,8 @@
         }
     };
 
-    public static interface Coalescable {
+    public static interface Coalescable
+    {
         long timestampNanos();
     }
 
@@ -137,22 +140,21 @@
             this.displayName = displayName;
             if (DEBUG_COALESCING)
             {
-                new Thread(displayName + " debug thread") {
-                    @Override
-                    public void run() {
-                        while (true) {
-                            try
-                            {
-                                Thread.sleep(5000);
-                            }
-                            catch (InterruptedException e)
-                            {
-                                throw new AssertionError();
-                            }
-                            shouldLogAverage = true;
+                NamedThreadFactory.createThread(() ->
+                {
+                    while (true)
+                    {
+                        try
+                        {
+                            Thread.sleep(5000);
                         }
+                        catch (InterruptedException e)
+                        {
+                            throw new AssertionError();
+                        }
+                        shouldLogAverage = true;
                     }
-                }.start();
+                }, displayName + " debug thread").start();
             }
             RandomAccessFile rasTemp = null;
             ByteBuffer logBufferTemp = null;
@@ -202,9 +204,12 @@
          * If debugging is enabled log the timestamps of all the items in the provided collection
          * to a file.
          */
-        final protected <C extends Coalescable> void debugTimestamps(Collection<C> coalescables) {
-            if (DEBUG_COALESCING) {
-                for (C coalescable : coalescables) {
+        final protected <C extends Coalescable> void debugTimestamps(Collection<C> coalescables)
+        {
+            if (DEBUG_COALESCING)
+            {
+                for (C coalescable : coalescables)
+                {
                     debugTimestamp(coalescable.timestampNanos());
                 }
             }
@@ -363,7 +368,8 @@
         }
 
         @Override
-        public String toString() {
+        public String toString()
+        {
             return "Time horizon moving average";
         }
     }
@@ -439,7 +445,8 @@
         }
 
         @Override
-        public String toString() {
+        public String toString()
+        {
             return "Moving average";
         }
     }
@@ -476,7 +483,8 @@
         }
 
         @Override
-        public String toString() {
+        public String toString()
+        {
             return "Fixed";
         }
     }
@@ -505,7 +513,8 @@
         }
 
         @Override
-        public String toString() {
+        public String toString()
+        {
             return "Disabled";
         }
     }
diff --git a/src/java/org/apache/cassandra/utils/DirectorySizeCalculator.java b/src/java/org/apache/cassandra/utils/DirectorySizeCalculator.java
new file mode 100644
index 0000000..c1fb6e0
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/DirectorySizeCalculator.java
@@ -0,0 +1,65 @@
+/*
+ * 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.utils;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+
+/**
+ * Walks directory recursively, summing up total contents of files within.
+ */
+public class DirectorySizeCalculator extends SimpleFileVisitor<Path>
+{
+    protected volatile long size = 0;
+    protected final File path;
+
+    public DirectorySizeCalculator(File path)
+    {
+        super();
+        this.path = path;
+    }
+
+    public boolean isAcceptable(Path file)
+    {
+        return true;
+    }
+
+    @Override
+    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
+    {
+        if (isAcceptable(file))
+            size += attrs.size();
+        return FileVisitResult.CONTINUE;
+    }
+
+    @Override
+    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException
+    {
+        return FileVisitResult.CONTINUE;
+    }
+
+    public long getAllocatedSize()
+    {
+        return size;
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/EstimatedHistogram.java b/src/java/org/apache/cassandra/utils/EstimatedHistogram.java
index 07a41bc..2c1faa0 100644
--- a/src/java/org/apache/cassandra/utils/EstimatedHistogram.java
+++ b/src/java/org/apache/cassandra/utils/EstimatedHistogram.java
@@ -397,7 +397,8 @@
             long[] offsets = new long[size - 1];
             long[] buckets = new long[size];
 
-            for (int i = 0; i < size; i++) {
+            for (int i = 0; i < size; i++)
+            {
                 offsets[i == 0 ? 0 : i - 1] = in.readLong();
                 buckets[i] = in.readLong();
             }
diff --git a/src/java/org/apache/cassandra/utils/FBUtilities.java b/src/java/org/apache/cassandra/utils/FBUtilities.java
index b4418be..d573e47 100644
--- a/src/java/org/apache/cassandra/utils/FBUtilities.java
+++ b/src/java/org/apache/cassandra/utils/FBUtilities.java
@@ -41,12 +41,16 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.netty.util.concurrent.FastThreadLocal;
 import org.apache.cassandra.auth.IAuthenticator;
 import org.apache.cassandra.auth.IAuthorizer;
 import org.apache.cassandra.auth.IRoleManager;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.SerializationHeader;
+import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.LocalPartitioner;
 import org.apache.cassandra.dht.Range;
@@ -57,16 +61,12 @@
 import org.apache.cassandra.io.sstable.metadata.MetadataComponent;
 import org.apache.cassandra.io.sstable.metadata.MetadataType;
 import org.apache.cassandra.io.sstable.metadata.ValidationMetadata;
-import org.apache.cassandra.schema.CompressionParams;
 import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.io.util.DataOutputBufferFixed;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.locator.InetAddressAndPort;
 import org.apache.cassandra.net.AsyncOneResponse;
 
-import org.codehaus.jackson.JsonFactory;
-import org.codehaus.jackson.map.ObjectMapper;
-
 public class FBUtilities
 {
     private static final Logger logger = LoggerFactory.getLogger(FBUtilities.class);
@@ -79,13 +79,15 @@
     private static final String DEFAULT_TRIGGER_DIR = "triggers";
 
     private static final String OPERATING_SYSTEM = System.getProperty("os.name").toLowerCase();
-    private static final boolean IS_WINDOWS = OPERATING_SYSTEM.contains("windows");
-    private static final boolean HAS_PROCFS = !IS_WINDOWS && (new File(File.separator + "proc")).exists();
+    public static final boolean isWindows = OPERATING_SYSTEM.contains("windows");
+    public static final boolean isLinux = OPERATING_SYSTEM.contains("linux");
 
     private static volatile InetAddress localInetAddress;
     private static volatile InetAddress broadcastInetAddress;
     private static volatile InetAddress broadcastRpcAddress;
 
+    private static volatile String previousReleaseVersionString;
+
     public static int getAvailableProcessors()
     {
         String availableProcessors = System.getProperty("cassandra.available_processors");
@@ -95,28 +97,22 @@
             return Runtime.getRuntime().availableProcessors();
     }
 
-    private static final ThreadLocal<MessageDigest> localMD5Digest = new ThreadLocal<MessageDigest>()
+    private static final FastThreadLocal<MessageDigest> localMD5Digest = new FastThreadLocal<MessageDigest>()
     {
         @Override
         protected MessageDigest initialValue()
         {
             return newMessageDigest("MD5");
         }
-
-        @Override
-        public MessageDigest get()
-        {
-            MessageDigest digest = super.get();
-            digest.reset();
-            return digest;
-        }
     };
 
     public static final int MAX_UNSIGNED_SHORT = 0xFFFF;
 
     public static MessageDigest threadLocalMD5Digest()
     {
-        return localMD5Digest.get();
+        MessageDigest md = localMD5Digest.get();
+        md.reset();
+        return md;
     }
 
     public static MessageDigest newMessageDigest(String algorithm)
@@ -198,10 +194,14 @@
 
     public static String getNetworkInterface(InetAddress localAddress)
     {
-        try {
-            for(NetworkInterface ifc : Collections.list(NetworkInterface.getNetworkInterfaces())) {
-                if(ifc.isUp()) {
-                    for(InetAddress addr : Collections.list(ifc.getInetAddresses())) {
+        try
+        {
+            for(NetworkInterface ifc : Collections.list(NetworkInterface.getNetworkInterfaces()))
+            {
+                if(ifc.isUp())
+                {
+                    for(InetAddress addr : Collections.list(ifc.getInetAddresses()))
+                    {
                         if (addr.equals(localAddress))
                             return ifc.getDisplayName();
                     }
@@ -336,6 +336,16 @@
         return triggerDir;
     }
 
+    public static void setPreviousReleaseVersionString(String previousReleaseVersionString)
+    {
+        FBUtilities.previousReleaseVersionString = previousReleaseVersionString;
+    }
+
+    public static String getPreviousReleaseVersionString()
+    {
+        return previousReleaseVersionString;
+    }
+
     public static String getReleaseVersionString()
     {
         try (InputStream in = FBUtilities.class.getClassLoader().getResourceAsStream("org/apache/cassandra/config/version.properties"))
@@ -490,20 +500,25 @@
         Map<MetadataType, MetadataComponent> sstableMetadata = desc.getMetadataSerializer().deserialize(desc, types);
         ValidationMetadata validationMetadata = (ValidationMetadata) sstableMetadata.get(MetadataType.VALIDATION);
         SerializationHeader.Component header = (SerializationHeader.Component) sstableMetadata.get(MetadataType.HEADER);
-        if (validationMetadata.partitioner.endsWith("LocalPartitioner"))
-        {
-            return new LocalPartitioner(header.getKeyType());
-        }
-        else
-        {
-            return newPartitioner(validationMetadata.partitioner);
-        }
+        return newPartitioner(validationMetadata.partitioner, Optional.of(header.getKeyType()));
     }
 
     public static IPartitioner newPartitioner(String partitionerClassName) throws ConfigurationException
     {
+        return newPartitioner(partitionerClassName, Optional.empty());
+    }
+
+    @VisibleForTesting
+    static IPartitioner newPartitioner(String partitionerClassName, Optional<AbstractType<?>> comparator) throws ConfigurationException
+    {
         if (!partitionerClassName.contains("."))
             partitionerClassName = "org.apache.cassandra.dht." + partitionerClassName;
+
+        if (partitionerClassName.equals("org.apache.cassandra.dht.LocalPartitioner"))
+        {
+            assert comparator.isPresent() : "Expected a comparator for local partitioner";
+            return new LocalPartitioner(comparator.get());
+        }
         return FBUtilities.instanceOrConstruct(partitionerClassName, "partitioner");
     }
 
@@ -693,11 +708,36 @@
 
     public static String prettyPrintMemory(long size)
     {
+        return prettyPrintMemory(size, false);
+    }
+
+    public static String prettyPrintMemory(long size, boolean includeSpace)
+    {
         if (size >= 1 << 30)
-            return String.format("%.3fGiB", size / (double) (1 << 30));
+            return String.format("%.3f%sGiB", size / (double) (1 << 30), includeSpace ? " " : "");
         if (size >= 1 << 20)
-            return String.format("%.3fMiB", size / (double) (1 << 20));
-        return String.format("%.3fKiB", size / (double) (1 << 10));
+            return String.format("%.3f%sMiB", size / (double) (1 << 20), includeSpace ? " " : "");
+        return String.format("%.3f%sKiB", size / (double) (1 << 10), includeSpace ? " " : "");
+    }
+
+    public static String prettyPrintMemoryPerSecond(long rate)
+    {
+        if (rate >= 1 << 30)
+            return String.format("%.3fGiB/s", rate / (double) (1 << 30));
+        if (rate >= 1 << 20)
+            return String.format("%.3fMiB/s", rate / (double) (1 << 20));
+        return String.format("%.3fKiB/s", rate / (double) (1 << 10));
+    }
+
+    public static String prettyPrintMemoryPerSecond(long bytes, long timeInNano)
+    {
+        // We can't sanely calculate a rate over 0 nanoseconds
+        if (timeInNano == 0)
+            return "NaN  KiB/s";
+
+        long rate = (long) (((double) bytes / timeInNano) * 1000 * 1000 * 1000);
+
+        return prettyPrintMemoryPerSecond(rate);
     }
 
     /**
@@ -770,20 +810,6 @@
         buffer.position(position);
     }
 
-    private static final ThreadLocal<byte[]> threadLocalScratchBuffer = new ThreadLocal<byte[]>()
-    {
-        @Override
-        protected byte[] initialValue()
-        {
-            return new byte[CompressionParams.DEFAULT_CHUNK_LENGTH];
-        }
-    };
-
-    public static byte[] getThreadLocalScratchBuffer()
-    {
-        return threadLocalScratchBuffer.get();
-    }
-
     public static long abs(long index)
     {
         long negbit = index >> 63;
@@ -855,16 +881,6 @@
         return historyDir;
     }
 
-    public static boolean isWindows()
-    {
-        return IS_WINDOWS;
-    }
-
-    public static boolean hasProcFS()
-    {
-        return HAS_PROCFS;
-    }
-
     public static void updateWithShort(MessageDigest digest, int val)
     {
         digest.update((byte) ((val >> 8) & 0xFF));
@@ -938,7 +954,7 @@
         }
     }
 
-    public static void sleepQuietly(long millis)
+	public static void sleepQuietly(long millis)
     {
         try
         {
@@ -950,6 +966,11 @@
         }
     }
 
+    public static long align(long val, int boundary)
+    {
+        return (val + boundary) & ~(boundary - 1);
+    }
+
     @VisibleForTesting
     protected static void reset()
     {
diff --git a/src/java/org/apache/cassandra/utils/FastByteOperations.java b/src/java/org/apache/cassandra/utils/FastByteOperations.java
index 68e395c..6581736 100644
--- a/src/java/org/apache/cassandra/utils/FastByteOperations.java
+++ b/src/java/org/apache/cassandra/utils/FastByteOperations.java
@@ -102,10 +102,7 @@
          */
         static ByteOperations getBest()
         {
-            String arch = System.getProperty("os.arch");
-            boolean unaligned = arch.equals("i386") || arch.equals("x86")
-                                || arch.equals("amd64") || arch.equals("x86_64") || arch.equals("s390x");
-            if (!unaligned)
+            if (!Architecture.IS_UNALIGNED)
                 return new PureJavaOperations();
             try
             {
@@ -269,7 +266,8 @@
 
         public static void copy(Object src, long srcOffset, Object dst, long dstOffset, long length)
         {
-            while (length > 0) {
+            while (length > 0)
+            {
                 long size = (length > UNSAFE_COPY_THRESHOLD) ? UNSAFE_COPY_THRESHOLD : length;
                 // if src or dst are null, the offsets are absolute base addresses:
                 theUnsafe.copyMemory(src, srcOffset, dst, dstOffset, size);
@@ -333,7 +331,7 @@
          * @param memoryOffset2 Where to start comparing in the right buffer (pure memory address if buffer1 is null, or relative otherwise)
          * @param length1 How much to compare from the left buffer
          * @param length2 How much to compare from the right buffer
-         * @return 0 if equal, < 0 if left is less than right, etc.
+         * @return 0 if equal, {@code < 0} if left is less than right, etc.
          */
         @Inline
         public static int compareTo(Object buffer1, long memoryOffset1, int length1,
diff --git a/src/java/org/apache/cassandra/utils/FilterFactory.java b/src/java/org/apache/cassandra/utils/FilterFactory.java
index 869f3fa..ddcf1bb 100644
--- a/src/java/org/apache/cassandra/utils/FilterFactory.java
+++ b/src/java/org/apache/cassandra/utils/FilterFactory.java
@@ -20,14 +20,14 @@
 import java.io.DataInput;
 import java.io.IOException;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.utils.obs.IBitSet;
 import org.apache.cassandra.utils.obs.OffHeapBitSet;
 import org.apache.cassandra.utils.obs.OpenBitSet;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 public class FilterFactory
 {
     public static final IFilter AlwaysPresent = new AlwaysPresentFilter();
@@ -55,7 +55,7 @@
         int bucketsPerElement = Math.min(targetBucketsPerElem, maxBucketsPerElement);
         if (bucketsPerElement < targetBucketsPerElem)
         {
-            logger.warn(String.format("Cannot provide an optimal BloomFilter for %d elements (%d/%d buckets per element).", numElements, bucketsPerElement, targetBucketsPerElem));
+            logger.warn("Cannot provide an optimal BloomFilter for {} elements ({}/{} buckets per element).", numElements, bucketsPerElement, targetBucketsPerElem);
         }
         BloomCalculations.BloomSpecification spec = BloomCalculations.computeBloomSpec(bucketsPerElement);
         return createFilter(spec.K, numElements, spec.bucketsPerElement, offheap, oldBfHashOrder);
diff --git a/src/java/org/apache/cassandra/utils/GuidGenerator.java b/src/java/org/apache/cassandra/utils/GuidGenerator.java
index 09c7f3e..9742353 100644
--- a/src/java/org/apache/cassandra/utils/GuidGenerator.java
+++ b/src/java/org/apache/cassandra/utils/GuidGenerator.java
@@ -21,13 +21,16 @@
 import java.security.SecureRandom;
 import java.util.Random;
 
-public class GuidGenerator {
+public class GuidGenerator
+{
     private static final Random myRand;
     private static final SecureRandom mySecureRand;
     private static final String s_id;
 
-    static {
-        if (System.getProperty("java.security.egd") == null) {
+    static
+    {
+        if (System.getProperty("java.security.egd") == null)
+        {
             System.setProperty("java.security.egd", "file:/dev/urandom");
         }
         mySecureRand = new SecureRandom();
@@ -41,7 +44,8 @@
         }
     }
 
-    public static String guid() {
+    public static String guid()
+    {
         ByteBuffer array = guidAsBytes();
 
         StringBuilder sb = new StringBuilder();
@@ -58,7 +62,8 @@
     public static String guidToString(byte[] bytes)
     {
         StringBuilder sb = new StringBuilder();
-        for (int j = 0; j < bytes.length; ++j) {
+        for (int j = 0; j < bytes.length; ++j)
+        {
             int b = bytes[j] & 0xFF;
             if (b < 0x10) sb.append('0');
             sb.append(Integer.toHexString(b));
@@ -67,13 +72,11 @@
         return convertToStandardFormat( sb.toString() );
     }
 
-    public static ByteBuffer guidAsBytes()
+    public static ByteBuffer guidAsBytes(Random random, String hostId, long time)
     {
         StringBuilder sbValueBeforeMD5 = new StringBuilder();
-        long time = System.currentTimeMillis();
-        long rand = 0;
-        rand = myRand.nextLong();
-        sbValueBeforeMD5.append(s_id)
+        long rand = random.nextLong();
+        sbValueBeforeMD5.append(hostId)
                         .append(":")
                         .append(Long.toString(time))
                         .append(":")
@@ -83,12 +86,18 @@
         return ByteBuffer.wrap(FBUtilities.threadLocalMD5Digest().digest(valueBeforeMD5.getBytes()));
     }
 
+    public static ByteBuffer guidAsBytes()
+    {
+        return guidAsBytes(myRand, s_id, System.currentTimeMillis());
+    }
+
     /*
         * Convert to the standard format for GUID
         * Example: C2FEEEAC-CFCD-11D1-8B05-00600806D9B6
     */
 
-    private static String convertToStandardFormat(String valueAfterMD5) {
+    private static String convertToStandardFormat(String valueAfterMD5)
+    {
         String raw = valueAfterMD5.toUpperCase();
         StringBuilder sb = new StringBuilder();
         sb.append(raw.substring(0, 8))
diff --git a/src/java/org/apache/cassandra/utils/HeapUtils.java b/src/java/org/apache/cassandra/utils/HeapUtils.java
index 4c84f9b..ef66fde 100644
--- a/src/java/org/apache/cassandra/utils/HeapUtils.java
+++ b/src/java/org/apache/cassandra/utils/HeapUtils.java
@@ -98,15 +98,16 @@
      */
     private static void logProcessOutput(Process p) throws IOException
     {
-        BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream()));
-
-        StrBuilder builder = new StrBuilder();
-        String line;
-        while ((line = input.readLine()) != null)
+        try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())))
         {
-            builder.appendln(line);
+            StrBuilder builder = new StrBuilder();
+            String line;
+            while ((line = input.readLine()) != null)
+            {
+                builder.appendln(line);
+            }
+            logger.info(builder.toString());
         }
-        logger.info(builder.toString());
     }
 
     /**
@@ -132,7 +133,7 @@
         String jvmName = ManagementFactory.getRuntimeMXBean().getName();
         try
         {
-            return Long.parseLong(jvmName.split("@")[0]);
+            return Long.valueOf(jvmName.split("@")[0]);
         }
         catch (NumberFormatException e)
         {
diff --git a/src/java/org/apache/cassandra/utils/Hex.java b/src/java/org/apache/cassandra/utils/Hex.java
index 0883c34..9163067 100644
--- a/src/java/org/apache/cassandra/utils/Hex.java
+++ b/src/java/org/apache/cassandra/utils/Hex.java
@@ -70,10 +70,15 @@
 
     public static String bytesToHex(byte... bytes)
     {
-        char[] c = new char[bytes.length * 2];
-        for (int i = 0; i < bytes.length; i++)
+        return bytesToHex(bytes, 0, bytes.length);
+    }
+
+    public static String bytesToHex(byte bytes[], int offset, int length)
+    {
+        char[] c = new char[length * 2];
+        for (int i = 0; i < length; i++)
         {
-            int bint = bytes[i];
+            int bint = bytes[i + offset];
             c[i * 2] = byteToChar[(bint & 0xf0) >> 4];
             c[1 + i * 2] = byteToChar[bint & 0x0f];
         }
@@ -96,8 +101,9 @@
             try
             {
                 s = stringConstructor.newInstance(0, c.length, c);
-            } 
-            catch (InvocationTargetException ite) {
+            }
+            catch (InvocationTargetException ite)
+            {
                 // The underlying constructor failed. Unwrapping the exception.
                 Throwable cause = ite.getCause();
                 logger.error("Underlying string constructor threw an error: {}",
diff --git a/src/java/org/apache/cassandra/utils/HistogramBuilder.java b/src/java/org/apache/cassandra/utils/HistogramBuilder.java
index 5d22352..093c52c 100644
--- a/src/java/org/apache/cassandra/utils/HistogramBuilder.java
+++ b/src/java/org/apache/cassandra/utils/HistogramBuilder.java
@@ -25,6 +25,9 @@
 public class HistogramBuilder
 {
 
+    public static final long[] EMPTY_LONG_ARRAY = new long[]{};
+    public static final long[] ZERO = new long[]{ 0 };
+
     public HistogramBuilder() {}
     public HistogramBuilder(long[] values)
     {
@@ -73,7 +76,7 @@
         final long[] values = this.values;
 
         if (count == 0)
-            return new EstimatedHistogram(new long[] { }, new long[] { 0 });
+            return new EstimatedHistogram(EMPTY_LONG_ARRAY, ZERO);
 
         long min = Long.MAX_VALUE, max = Long.MIN_VALUE;
         double sum = 0, sumsq = 0;
@@ -114,7 +117,7 @@
             // minormax == mean we have no range to produce, but given the exclusive starts
             // that begin at zero by default (or -Inf) in EstimatedHistogram we have to generate a min range
             // to indicate where we start from
-            return ismin ? new long[] { mean - 1 } : new long[0];
+            return ismin ? new long[] { mean - 1 } : EMPTY_LONG_ARRAY;
 
         if (stdev < 1)
         {
diff --git a/src/java/org/apache/cassandra/utils/IMergeIterator.java b/src/java/org/apache/cassandra/utils/IMergeIterator.java
index deddc4c..e45b897 100644
--- a/src/java/org/apache/cassandra/utils/IMergeIterator.java
+++ b/src/java/org/apache/cassandra/utils/IMergeIterator.java
@@ -21,5 +21,6 @@
 
 public interface IMergeIterator<In, Out> extends CloseableIterator<Out>
 {
+
     Iterable<? extends Iterator<In>> iterators();
 }
diff --git a/src/java/org/apache/cassandra/utils/IntegerInterval.java b/src/java/org/apache/cassandra/utils/IntegerInterval.java
index c032da5..b26ac45 100644
--- a/src/java/org/apache/cassandra/utils/IntegerInterval.java
+++ b/src/java/org/apache/cassandra/utils/IntegerInterval.java
@@ -151,7 +151,7 @@
                     if (extend > end)
                         end = extend;
                 }
-    
+
                 // record directly preceding our start may extend into us; if it does, we take it as our start
                 int lpos = Arrays.binarySearch(ranges, ((start & 0xFFFFFFFFL) << 32) | 0); // lower (i.e. greatest <) of the start position
                 if (lpos < 0)
@@ -165,7 +165,7 @@
                         --lpos;
                     }
                 }
-    
+
                 newRanges = new long[ranges.length - (rpos - lpos) + 1];
                 int dest = 0;
                 for (int i = 0; i <= lpos; ++i)
diff --git a/src/java/org/apache/cassandra/utils/Interval.java b/src/java/org/apache/cassandra/utils/Interval.java
index 335ef27..9398144 100644
--- a/src/java/org/apache/cassandra/utils/Interval.java
+++ b/src/java/org/apache/cassandra/utils/Interval.java
@@ -17,8 +17,6 @@
  */
 package org.apache.cassandra.utils;
 
-import java.util.Comparator;
-
 import com.google.common.base.Objects;
 
 public class Interval<C, D>
diff --git a/src/java/org/apache/cassandra/utils/IntervalTree.java b/src/java/org/apache/cassandra/utils/IntervalTree.java
index b92112e..f761180 100644
--- a/src/java/org/apache/cassandra/utils/IntervalTree.java
+++ b/src/java/org/apache/cassandra/utils/IntervalTree.java
@@ -114,7 +114,7 @@
     public Iterator<I> iterator()
     {
         if (head == null)
-            return Iterators.<I>emptyIterator();
+            return Collections.emptyIterator();
 
         return new TreeIterator(head);
     }
@@ -272,20 +272,21 @@
 
         protected I computeNext()
         {
-            if (current != null && current.hasNext())
-                return current.next();
+            while (true)
+            {
+                if (current != null && current.hasNext())
+                    return current.next();
 
-            IntervalNode node = stack.pollFirst();
-            if (node == null)
-                return endOfData();
+                IntervalNode node = stack.pollFirst();
+                if (node == null)
+                    return endOfData();
 
-            current = node.intersectsLeft.iterator();
+                current = node.intersectsLeft.iterator();
 
-            // We know this is the smaller not returned yet, but before doing
-            // its parent, we must do everyone on it's right.
-            gotoMinOf(node.right);
-
-            return computeNext();
+                // We know this is the smaller not returned yet, but before doing
+                // its parent, we must do everyone on it's right.
+                gotoMinOf(node.right);
+            }
         }
 
         private void gotoMinOf(IntervalNode node)
diff --git a/src/java/org/apache/cassandra/utils/IteratorWithLowerBound.java b/src/java/org/apache/cassandra/utils/IteratorWithLowerBound.java
new file mode 100644
index 0000000..85eeede
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/IteratorWithLowerBound.java
@@ -0,0 +1,24 @@
+/*
+ * 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.utils;
+
+public interface IteratorWithLowerBound<In>
+{
+    In lowerBound();
+}
diff --git a/src/java/org/apache/cassandra/utils/JMXServerUtils.java b/src/java/org/apache/cassandra/utils/JMXServerUtils.java
new file mode 100644
index 0000000..aad4f05
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/JMXServerUtils.java
@@ -0,0 +1,348 @@
+/*
+ * 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.utils;
+
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.rmi.AccessException;
+import java.rmi.AlreadyBoundException;
+import java.rmi.NoSuchObjectException;
+import java.rmi.NotBoundException;
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.rmi.registry.Registry;
+import java.rmi.server.RMIClientSocketFactory;
+import java.rmi.server.RMIServerSocketFactory;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.management.remote.*;
+import javax.management.remote.rmi.RMIConnectorServer;
+import javax.management.remote.rmi.RMIJRMPServerImpl;
+import javax.rmi.ssl.SslRMIClientSocketFactory;
+import javax.rmi.ssl.SslRMIServerSocketFactory;
+import javax.security.auth.Subject;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sun.jmx.remote.security.JMXPluggableAuthenticator;
+import org.apache.cassandra.auth.jmx.AuthenticationProxy;
+
+public class JMXServerUtils
+{
+    private static final Logger logger = LoggerFactory.getLogger(JMXServerUtils.class);
+
+    /**
+     * Creates a server programmatically. This allows us to set parameters which normally are
+     * inaccessable.
+     */
+    @SuppressWarnings("resource")
+    public static JMXConnectorServer createJMXServer(int port, boolean local)
+    throws IOException
+    {
+        Map<String, Object> env = new HashMap<>();
+
+        InetAddress serverAddress = null;
+        if (local)
+        {
+            serverAddress = InetAddress.getLoopbackAddress();
+            System.setProperty("java.rmi.server.hostname", serverAddress.getHostAddress());
+        }
+
+        // Configure the RMI client & server socket factories, including SSL config.
+        env.putAll(configureJmxSocketFactories(serverAddress, local));
+
+        // configure the RMI registry
+        Registry registry = new JmxRegistry(port,
+                                            (RMIClientSocketFactory) env.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE),
+                                            (RMIServerSocketFactory) env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE),
+                                            "jmxrmi");
+
+        // Configure authn, using a JMXAuthenticator which either wraps a set log LoginModules configured
+        // via a JAAS configuration entry, or one which delegates to the standard file based authenticator.
+        // Authn is disabled if com.sun.management.jmxremote.authenticate=false
+        env.putAll(configureJmxAuthentication());
+
+        // Configure authz - if a custom proxy class is specified an instance will be returned.
+        // If not, but a location for the standard access file is set in system properties, the
+        // return value is null, and an entry is added to the env map detailing that location
+        // If neither method is specified, no access control is applied
+        MBeanServerForwarder authzProxy = configureJmxAuthorization(env);
+
+        // Mark the JMX server as a permanently exported object. This allows the JVM to exit with the
+        // server running and also exempts it from the distributed GC scheduler which otherwise would
+        // potentially attempt a full GC every `sun.rmi.dgc.server.gcInterval` millis (default is 3600000ms)
+        // For more background see:
+        //   - CASSANDRA-2967
+        //   - https://www.jclarity.com/2015/01/27/rmi-system-gc-unplugged/
+        //   - https://bugs.openjdk.java.net/browse/JDK-6760712
+        env.put("jmx.remote.x.daemon", "true");
+
+        // Set the port used to create subsequent connections to exported objects over RMI. This simplifies
+        // configuration in firewalled environments, but it can't be used in conjuction with SSL sockets.
+        // See: CASSANDRA-7087
+        int rmiPort = Integer.getInteger("com.sun.management.jmxremote.rmi.port", 0);
+
+        // We create the underlying RMIJRMPServerImpl so that we can manually bind it to the registry,
+        // rather then specifying a binding address in the JMXServiceURL and letting it be done automatically
+        // when the server is started. The reason for this is that if the registry is configured with SSL
+        // sockets, the JMXConnectorServer acts as its client during the binding which means it needs to
+        // have a truststore configured which contains the registry's certificate. Manually binding removes
+        // this problem.
+        // See CASSANDRA-12109.
+        RMIJRMPServerImpl server = new RMIJRMPServerImpl(rmiPort,
+                                                         (RMIClientSocketFactory) env.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE),
+                                                         (RMIServerSocketFactory) env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE),
+                                                         env);
+        JMXServiceURL serviceURL = new JMXServiceURL("rmi", null, rmiPort);
+        RMIConnectorServer jmxServer = new RMIConnectorServer(serviceURL, env, server, ManagementFactory.getPlatformMBeanServer());
+
+        // If a custom authz proxy was created, attach it to the server now.
+        if (authzProxy != null)
+            jmxServer.setMBeanServerForwarder(authzProxy);
+        jmxServer.start();
+
+        ((JmxRegistry)registry).setRemoteServerStub(server.toStub());
+        logJmxServiceUrl(serverAddress, port);
+        return jmxServer;
+    }
+
+    private static Map<String, Object> configureJmxAuthentication()
+    {
+        Map<String, Object> env = new HashMap<>();
+        if (!Boolean.getBoolean("com.sun.management.jmxremote.authenticate"))
+            return env;
+
+        // If authentication is enabled, initialize the appropriate JMXAuthenticator
+        // and stash it in the environment settings.
+        // A JAAS configuration entry takes precedence. If one is supplied, use
+        // Cassandra's own custom JMXAuthenticator implementation which delegates
+        // auth to the LoginModules specified by the JAAS configuration entry.
+        // If no JAAS entry is found, an instance of the JDK's own
+        // JMXPluggableAuthenticator is created. In that case, the admin may have
+        // set a location for the JMX password file which must be added to env
+        // before creating the authenticator. If no password file has been
+        // explicitly set, it's read from the default location
+        // $JAVA_HOME/lib/management/jmxremote.password
+        String configEntry = System.getProperty("cassandra.jmx.remote.login.config");
+        if (configEntry != null)
+        {
+            env.put(JMXConnectorServer.AUTHENTICATOR, new AuthenticationProxy(configEntry));
+        }
+        else
+        {
+            String passwordFile = System.getProperty("com.sun.management.jmxremote.password.file");
+            if (passwordFile != null)
+            {
+                // stash the password file location where JMXPluggableAuthenticator expects it
+                env.put("jmx.remote.x.password.file", passwordFile);
+            }
+
+            env.put(JMXConnectorServer.AUTHENTICATOR, new JMXPluggableAuthenticatorWrapper(env));
+        }
+        env.put("jmx.remote.rmi.server.credential.types",
+            new String[] { String[].class.getName(), String.class.getName() });
+        return env;
+    }
+
+    private static MBeanServerForwarder configureJmxAuthorization(Map<String, Object> env)
+    {
+        // If a custom authz proxy is supplied (Cassandra ships with AuthorizationProxy, which
+        // delegates to its own role based IAuthorizer), then instantiate and return one which
+        // can be set as the JMXConnectorServer's MBeanServerForwarder.
+        // If no custom proxy is supplied, check system properties for the location of the
+        // standard access file & stash it in env
+        String authzProxyClass = System.getProperty("cassandra.jmx.authorizer");
+        if (authzProxyClass != null)
+        {
+            final InvocationHandler handler = FBUtilities.construct(authzProxyClass, "JMX authz proxy");
+            final Class[] interfaces = { MBeanServerForwarder.class };
+
+            Object proxy = Proxy.newProxyInstance(MBeanServerForwarder.class.getClassLoader(), interfaces, handler);
+            return MBeanServerForwarder.class.cast(proxy);
+        }
+        else
+        {
+            String accessFile = System.getProperty("com.sun.management.jmxremote.access.file");
+            if (accessFile != null)
+            {
+                env.put("jmx.remote.x.access.file", accessFile);
+            }
+            return null;
+        }
+    }
+
+    private static Map<String, Object> configureJmxSocketFactories(InetAddress serverAddress, boolean localOnly)
+    {
+        Map<String, Object> env = new HashMap<>();
+        if (Boolean.getBoolean("com.sun.management.jmxremote.ssl"))
+        {
+            boolean requireClientAuth = Boolean.getBoolean("com.sun.management.jmxremote.ssl.need.client.auth");
+            String[] protocols = null;
+            String protocolList = System.getProperty("com.sun.management.jmxremote.ssl.enabled.protocols");
+            if (protocolList != null)
+            {
+                System.setProperty("javax.rmi.ssl.client.enabledProtocols", protocolList);
+                protocols = StringUtils.split(protocolList, ',');
+            }
+
+            String[] ciphers = null;
+            String cipherList = System.getProperty("com.sun.management.jmxremote.ssl.enabled.cipher.suites");
+            if (cipherList != null)
+            {
+                System.setProperty("javax.rmi.ssl.client.enabledCipherSuites", cipherList);
+                ciphers = StringUtils.split(cipherList, ',');
+            }
+
+            SslRMIClientSocketFactory clientFactory = new SslRMIClientSocketFactory();
+            SslRMIServerSocketFactory serverFactory = new SslRMIServerSocketFactory(ciphers, protocols, requireClientAuth);
+            env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, serverFactory);
+            env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientFactory);
+            env.put("com.sun.jndi.rmi.factory.socket", clientFactory);
+            logJmxSslConfig(serverFactory);
+        }
+        else if (localOnly)
+        {
+            env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,
+                    new RMIServerSocketFactoryImpl(serverAddress));
+        }
+
+        return env;
+    }
+
+    @VisibleForTesting
+    public static void logJmxServiceUrl(InetAddress serverAddress, int port)
+    {
+        String urlTemplate = "service:jmx:rmi://%1$s/jndi/rmi://%1$s:%2$d/jmxrmi";
+        String hostName;
+        if (serverAddress == null)
+        {
+            hostName = FBUtilities.getBroadcastAddress() instanceof Inet6Address ? "[::]" : "0.0.0.0";
+        }
+        else
+        {
+            // hostnames based on IPv6 addresses must be wrapped in [ ]
+            hostName = serverAddress instanceof Inet6Address
+                       ? '[' + serverAddress.getHostAddress() + ']'
+                       : serverAddress.getHostAddress();
+        }
+        String url = String.format(urlTemplate, hostName, port);
+        logger.info("Configured JMX server at: {}", url);
+    }
+
+    private static void logJmxSslConfig(SslRMIServerSocketFactory serverFactory)
+    {
+        logger.debug("JMX SSL configuration. { protocols: [{}], cipher_suites: [{}], require_client_auth: {} }",
+                     serverFactory.getEnabledProtocols() == null
+                     ? "'JVM defaults'"
+                     : Arrays.stream(serverFactory.getEnabledProtocols()).collect(Collectors.joining("','", "'", "'")),
+                     serverFactory.getEnabledCipherSuites() == null
+                     ? "'JVM defaults'"
+                     : Arrays.stream(serverFactory.getEnabledCipherSuites()).collect(Collectors.joining("','", "'", "'")),
+                     serverFactory.getNeedClientAuth());
+    }
+
+    private static class JMXPluggableAuthenticatorWrapper implements JMXAuthenticator
+    {
+        final Map<?, ?> env;
+        private JMXPluggableAuthenticatorWrapper(Map<?, ?> env)
+        {
+            this.env = ImmutableMap.copyOf(env);
+        }
+
+        public Subject authenticate(Object credentials)
+        {
+            JMXPluggableAuthenticator authenticator = new JMXPluggableAuthenticator(env);
+            return authenticator.authenticate(credentials);
+        }
+    }
+
+    /*
+     * Better to use the internal API than re-invent the wheel.
+     */
+    @SuppressWarnings("restriction")
+    public static class JmxRegistry extends sun.rmi.registry.RegistryImpl
+    {
+        private final String lookupName;
+        private Remote remoteServerStub;
+
+        public JmxRegistry(final int port,
+                    final RMIClientSocketFactory csf,
+                    RMIServerSocketFactory ssf,
+                    final String lookupName) throws RemoteException {
+            super(port, csf, ssf);
+            this.lookupName = lookupName;
+        }
+
+        @Override
+        public Remote lookup(String s) throws RemoteException, NotBoundException {
+            return lookupName.equals(s) ? remoteServerStub : null;
+        }
+
+        @Override
+        public void bind(String s, Remote remote) throws RemoteException, AlreadyBoundException, AccessException {
+        }
+
+        @Override
+        public void unbind(String s) throws RemoteException, NotBoundException, AccessException {
+        }
+
+        @Override
+        public void rebind(String s, Remote remote) throws RemoteException, AccessException {
+        }
+
+        @Override
+        public String[] list() throws RemoteException {
+            return new String[] {lookupName};
+        }
+
+        public void setRemoteServerStub(Remote remoteServerStub) {
+            this.remoteServerStub = remoteServerStub;
+        }
+
+        /**
+         * Closes the underlying JMX registry by unexporting this instance.
+         * There is no reason to do this except for in-jvm dtests where we need
+         * to stop the registry, so we can start with a clean slate for future cluster
+         * builds, and the superclass never expects to be shut down and therefore doesn't
+         * handle this edge case at all.
+         */
+        @VisibleForTesting
+        public void close() {
+            try
+            {
+                UnicastRemoteObject.unexportObject(this, true);
+            }
+            catch (NoSuchObjectException ignored)
+            {
+                // Ignore if it's already unexported
+            }
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/JVMStabilityInspector.java b/src/java/org/apache/cassandra/utils/JVMStabilityInspector.java
index 798949f..ce7faed 100644
--- a/src/java/org/apache/cassandra/utils/JVMStabilityInspector.java
+++ b/src/java/org/apache/cassandra/utils/JVMStabilityInspector.java
@@ -119,11 +119,12 @@
 
     private static void inspectCommitLogError(Throwable t)
     {
-        if (!StorageService.instance.isSetupCompleted())
+        if (!StorageService.instance.isDaemonSetupCompleted())
         {
             logger.error("Exiting due to error while processing commit log during initialization.", t);
             killer.killCurrentJVM(t, true);
-        } else if (DatabaseDescriptor.getCommitFailurePolicy() == Config.CommitFailurePolicy.die)
+        }
+        else if (DatabaseDescriptor.getCommitFailurePolicy() == Config.CommitFailurePolicy.die)
             killer.killCurrentJVM(t);
     }
 
@@ -150,7 +151,8 @@
     }
 
     @VisibleForTesting
-    public static Killer replaceKiller(Killer newKiller) {
+    public static Killer replaceKiller(Killer newKiller)
+    {
         Killer oldKiller = JVMStabilityInspector.killer;
         JVMStabilityInspector.killer = newKiller;
         return oldKiller;
diff --git a/src/java/org/apache/cassandra/utils/LongTimSort.java b/src/java/org/apache/cassandra/utils/LongTimSort.java
new file mode 100644
index 0000000..76fe231
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/LongTimSort.java
@@ -0,0 +1,868 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed 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.
+ */
+/*
+ * This file originates from https://android.googlesource.com/platform/libcore/+/gingerbread/luni/src/main/java/java/util/TimSort.java
+ * and has been modified to sort primitive long arrays instead of object arrays.
+ */
+//package java.util;
+
+package org.apache.cassandra.utils;
+
+import java.util.Arrays;
+
+/**
+ * A stable, adaptive, iterative mergesort that requires far fewer than
+ * n lg(n) comparisons when running on partially sorted arrays, while
+ * offering performance comparable to a traditional mergesort when run
+ * on random arrays.  Like all proper mergesorts, this sort is stable and
+ * runs O(n log n) time (worst case).  In the worst case, this sort requires
+ * temporary storage space for n/2 object references; in the best case,
+ * it requires only a small constant amount of space.
+ *
+ * This implementation was adapted from Tim Peters's list sort for
+ * Python, which is described in detail here:
+ *
+ *   http://svn.python.org/projects/python/trunk/Objects/listsort.txt
+ *
+ * Tim's C code may be found here:
+ *
+ *   http://svn.python.org/projects/python/trunk/Objects/listobject.c
+ *
+ * The underlying techniques are described in this paper (and may have
+ * even earlier origins):
+ *
+ *  "Optimistic Sorting and Information Theoretic Complexity"
+ *  Peter McIlroy
+ *  SODA (Fourth Annual ACM-SIAM Symposium on Discrete Algorithms),
+ *  pp 467-474, Austin, Texas, 25-27 January 1993.
+ *
+ * While the API to this class consists solely of static methods, it is
+ * (privately) instantiable; a TimSort instance holds the state of an ongoing
+ * sort, assuming the input array is large enough to warrant the full-blown
+ * TimSort. Small arrays are sorted in place, using a binary insertion sort.
+ */
+public final class LongTimSort {
+    /**
+     * This is the minimum sized sequence that will be merged.  Shorter
+     * sequences will be lengthened by calling binarySort.  If the entire
+     * array is less than this length, no merges will be performed.
+     *
+     * This constant should be a power of two.  It was 64 in Tim Peter's C
+     * implementation, but 32 was empirically determined to work better in
+     * this implementation.  In the unlikely event that you set this constant
+     * to be a number that's not a power of two, you'll need to change the
+     * {@link #minRunLength} computation.
+     *
+     * If you decrease this constant, you must change the stackLen
+     * computation in the TimSort constructor, or you risk an
+     * ArrayOutOfBounds exception.  See listsort.txt for a discussion
+     * of the minimum stack length required as a function of the length
+     * of the array being sorted and the minimum merge sequence length.
+     */
+    private static final int MIN_MERGE = 32;
+    /**
+     * The array being sorted.
+     */
+    private final long[] a;
+    /**
+     * The comparator for this sort.
+     */
+    private final LongComparator c;
+    /**
+     * When we get into galloping mode, we stay there until both runs win less
+     * often than MIN_GALLOP consecutive times.
+     */
+    private static final int  MIN_GALLOP = 7;
+    /**
+     * This controls when we get *into* galloping mode.  It is initialized
+     * to MIN_GALLOP.  The mergeLo and mergeHi methods nudge it higher for
+     * random data, and lower for highly structured data.
+     */
+    private int minGallop = MIN_GALLOP;
+    /**
+     * Maximum initial size of tmp array, which is used for merging.  The array
+     * can grow to accommodate demand.
+     *
+     * Unlike Tim's original C version, we do not allocate this much storage
+     * when sorting smaller arrays.  This change was required for performance.
+     */
+    private static final int INITIAL_TMP_STORAGE_LENGTH = 256;
+    /**
+     * Temp storage for merges.
+     */
+    private long[] tmp;
+    /**
+     * A stack of pending runs yet to be merged.  Run i starts at
+     * address base[i] and extends for len[i] elements.  It's always
+     * true (so long as the indices are in bounds) that:
+     *
+     *     runBase[i] + runLen[i] == runBase[i + 1]
+     *
+     * so we could cut the storage for this, but it's a minor amount,
+     * and keeping all the info explicit simplifies the code.
+     */
+    private int stackSize = 0;  // Number of pending runs on stack
+    private final int[] runBase;
+    private final int[] runLen;
+    /**
+     * Asserts have been placed in if-statements for performace. To enable them,
+     * set this field to true and enable them in VM with a command line flag.
+     * If you modify this class, please do test the asserts!
+     */
+    private static final boolean DEBUG = false;
+    /**
+     * Creates a TimSort instance to maintain the state of an ongoing sort.
+     *
+     * @param a the array to be sorted
+     * @param c the comparator to determine the order of the sort
+     */
+    private LongTimSort(long[] a, LongComparator c) {
+        this.a = a;
+        this.c = c;
+        // Allocate temp storage (which may be increased later if necessary)
+        int len = a.length;
+        @SuppressWarnings({"unchecked", "UnnecessaryLocalVariable"})
+        long[] newArray = new long[len < 2 * INITIAL_TMP_STORAGE_LENGTH ?
+                                   len >>> 1 : INITIAL_TMP_STORAGE_LENGTH];
+        tmp = newArray;
+        /*
+         * Allocate runs-to-be-merged stack (which cannot be expanded).  The
+         * stack length requirements are described in listsort.txt.  The C
+         * version always uses the same stack length (85), but this was
+         * measured to be too expensive when sorting "mid-sized" arrays (e.g.,
+         * 100 elements) in Java.  Therefore, we use smaller (but sufficiently
+         * large) stack lengths for smaller arrays.  The "magic numbers" in the
+         * computation below must be changed if MIN_MERGE is decreased.  See
+         * the MIN_MERGE declaration above for more information.
+         */
+        int stackLen = (len <    120  ?  5 :
+                        len <   1542  ? 10 :
+                        len < 119151  ? 19 : 40);
+        runBase = new int[stackLen];
+        runLen = new int[stackLen];
+    }
+    /*
+     * The next two methods (which are package private and static) constitute
+     * the entire API of this class.  Each of these methods obeys the contract
+     * of the public method with the same signature in java.util.Arrays.
+     */
+    public static void sort(long[] a, LongComparator c) {
+        sort(a, 0, a.length, c);
+    }
+    public static void sort(long[] a, int lo, int hi, LongComparator c) {
+        if (c == null) {
+            Arrays.sort(a, lo, hi);
+            return;
+        }
+        rangeCheck(a.length, lo, hi);
+        int nRemaining  = hi - lo;
+        if (nRemaining < 2)
+            return;  // Arrays of size 0 and 1 are always sorted
+        // If array is small, do a "mini-TimSort" with no merges
+        if (nRemaining < MIN_MERGE) {
+            int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
+            binarySort(a, lo, hi, lo + initRunLen, c);
+            return;
+        }
+        /**
+         * March over the array once, left to right, finding natural runs,
+         * extending short natural runs to minRun elements, and merging runs
+         * to maintain stack invariant.
+         */
+        LongTimSort ts = new LongTimSort(a, c);
+        int minRun = minRunLength(nRemaining);
+        do {
+            // Identify next run
+            int runLen = countRunAndMakeAscending(a, lo, hi, c);
+            // If run is short, extend to min(minRun, nRemaining)
+            if (runLen < minRun) {
+                int force = nRemaining <= minRun ? nRemaining : minRun;
+                binarySort(a, lo, lo + force, lo + runLen, c);
+                runLen = force;
+            }
+            // Push run onto pending-run stack, and maybe merge
+            ts.pushRun(lo, runLen);
+            ts.mergeCollapse();
+            // Advance to find next run
+            lo += runLen;
+            nRemaining -= runLen;
+        } while (nRemaining != 0);
+        // Merge all remaining runs to complete sort
+        if (DEBUG) assert lo == hi;
+        ts.mergeForceCollapse();
+        if (DEBUG) assert ts.stackSize == 1;
+    }
+    /**
+     * Sorts the specified portion of the specified array using a binary
+     * insertion sort.  This is the best method for sorting small numbers
+     * of elements.  It requires O(n log n) compares, but O(n^2) data
+     * movement (worst case).
+     *
+     * If the initial part of the specified range is already sorted,
+     * this method can take advantage of it: the method assumes that the
+     * elements from index {@code lo}, inclusive, to {@code start},
+     * exclusive are already sorted.
+     *
+     * @param a the array in which a range is to be sorted
+     * @param lo the index of the first element in the range to be sorted
+     * @param hi the index after the last element in the range to be sorted
+     * @param start the index of the first element in the range that is
+     *        not already known to be sorted (@code lo <= start <= hi}
+     * @param c comparator to used for the sort
+     */
+    @SuppressWarnings("fallthrough")
+    private static void binarySort(long[] a, int lo, int hi, int start,
+                                       LongComparator c) {
+        if (DEBUG) assert lo <= start && start <= hi;
+        if (start == lo)
+            start++;
+        for ( ; start < hi; start++) {
+            long pivot = a[start];
+            // Set left (and right) to the index where a[start] (pivot) belongs
+            int left = lo;
+            int right = start;
+            if (DEBUG) assert left <= right;
+            /*
+             * Invariants:
+             *   pivot >= all in [lo, left).
+             *   pivot <  all in [right, start).
+             */
+            while (left < right) {
+                int mid = (left + right) >>> 1;
+                if (c.compare(pivot, a[mid]) < 0)
+                    right = mid;
+                else
+                    left = mid + 1;
+            }
+            if (DEBUG) assert left == right;
+            /*
+             * The invariants still hold: pivot >= all in [lo, left) and
+             * pivot < all in [left, start), so pivot belongs at left.  Note
+             * that if there are elements equal to pivot, left points to the
+             * first slot after them -- that's why this sort is stable.
+             * Slide elements over to make room to make room for pivot.
+             */
+            int n = start - left;  // The number of elements to move
+            // Switch is just an optimization for arraycopy in default case
+            switch(n) {
+                case 2:  a[left + 2] = a[left + 1];
+                case 1:  a[left + 1] = a[left];
+                    break;
+                default: System.arraycopy(a, left, a, left + 1, n);
+            }
+            a[left] = pivot;
+        }
+    }
+    /**
+     * Returns the length of the run beginning at the specified position in
+     * the specified array and reverses the run if it is descending (ensuring
+     * that the run will always be ascending when the method returns).
+     *
+     * A run is the longest ascending sequence with:
+     *
+     *    a[lo] <= a[lo + 1] <= a[lo + 2] <= ...
+     *
+     * or the longest descending sequence with:
+     *
+     *    a[lo] >  a[lo + 1] >  a[lo + 2] >  ...
+     *
+     * For its intended use in a stable mergesort, the strictness of the
+     * definition of "descending" is needed so that the call can safely
+     * reverse a descending sequence without violating stability.
+     *
+     * @param a the array in which a run is to be counted and possibly reversed
+     * @param lo index of the first element in the run
+     * @param hi index after the last element that may be contained in the run.
+    It is required that @code{lo < hi}.
+     * @param c the comparator to used for the sort
+     * @return  the length of the run beginning at the specified position in
+     *          the specified array
+     */
+    private static int countRunAndMakeAscending(long[] a, int lo, int hi,
+                                                LongComparator c) {
+        if (DEBUG) assert lo < hi;
+        int runHi = lo + 1;
+        if (runHi == hi)
+            return 1;
+        // Find end of run, and reverse range if descending
+        if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
+            while(runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
+                runHi++;
+            reverseRange(a, lo, runHi);
+        } else {                              // Ascending
+            while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
+                runHi++;
+        }
+        return runHi - lo;
+    }
+    /**
+     * Reverse the specified range of the specified array.
+     *
+     * @param a the array in which a range is to be reversed
+     * @param lo the index of the first element in the range to be reversed
+     * @param hi the index after the last element in the range to be reversed
+     */
+    private static void reverseRange(long[] a, int lo, int hi) {
+        hi--;
+        while (lo < hi) {
+            long t = a[lo];
+            a[lo++] = a[hi];
+            a[hi--] = t;
+        }
+    }
+    /**
+     * Returns the minimum acceptable run length for an array of the specified
+     * length. Natural runs shorter than this will be extended with
+     * {@link #binarySort}.
+     *
+     * Roughly speaking, the computation is:
+     *
+     *  If n < MIN_MERGE, return n (it's too small to bother with fancy stuff).
+     *  Else if n is an exact power of 2, return MIN_MERGE/2.
+     *  Else return an int k, MIN_MERGE/2 <= k <= MIN_MERGE, such that n/k
+     *   is close to, but strictly less than, an exact power of 2.
+     *
+     * For the rationale, see listsort.txt.
+     *
+     * @param n the length of the array to be sorted
+     * @return the length of the minimum run to be merged
+     */
+    private static int minRunLength(int n) {
+        if (DEBUG) assert n >= 0;
+        int r = 0;      // Becomes 1 if any 1 bits are shifted off
+        while (n >= MIN_MERGE) {
+            r |= (n & 1);
+            n >>= 1;
+        }
+        return n + r;
+    }
+    /**
+     * Pushes the specified run onto the pending-run stack.
+     *
+     * @param runBase index of the first element in the run
+     * @param runLen  the number of elements in the run
+     */
+    private void pushRun(int runBase, int runLen) {
+        this.runBase[stackSize] = runBase;
+        this.runLen[stackSize] = runLen;
+        stackSize++;
+    }
+    /**
+     * Examines the stack of runs waiting to be merged and merges adjacent runs
+     * until the stack invariants are reestablished:
+     *
+     *     1. runLen[i - 3] > runLen[i - 2] + runLen[i - 1]
+     *     2. runLen[i - 2] > runLen[i - 1]
+     *
+     * This method is called each time a new run is pushed onto the stack,
+     * so the invariants are guaranteed to hold for i < stackSize upon
+     * entry to the method.
+     */
+    private void mergeCollapse() {
+        while (stackSize > 1) {
+            int n = stackSize - 2;
+            if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]) {
+                if (runLen[n - 1] < runLen[n + 1])
+                    n--;
+                mergeAt(n);
+            } else if (runLen[n] <= runLen[n + 1]) {
+                mergeAt(n);
+            } else {
+                break; // Invariant is established
+            }
+        }
+    }
+    /**
+     * Merges all runs on the stack until only one remains.  This method is
+     * called once, to complete the sort.
+     */
+    private void mergeForceCollapse() {
+        while (stackSize > 1) {
+            int n = stackSize - 2;
+            if (n > 0 && runLen[n - 1] < runLen[n + 1])
+                n--;
+            mergeAt(n);
+        }
+    }
+    /**
+     * Merges the two runs at stack indices i and i+1.  Run i must be
+     * the penultimate or antepenultimate run on the stack.  In other words,
+     * i must be equal to stackSize-2 or stackSize-3.
+     *
+     * @param i stack index of the first of the two runs to merge
+     */
+    private void mergeAt(int i) {
+        if (DEBUG) assert stackSize >= 2;
+        if (DEBUG) assert i >= 0;
+        if (DEBUG) assert i == stackSize - 2 || i == stackSize - 3;
+        int base1 = runBase[i];
+        int len1 = runLen[i];
+        int base2 = runBase[i + 1];
+        int len2 = runLen[i + 1];
+        if (DEBUG) assert len1 > 0 && len2 > 0;
+        if (DEBUG) assert base1 + len1 == base2;
+        /*
+         * Record the length of the combined runs; if i is the 3rd-last
+         * run now, also slide over the last run (which isn't involved
+         * in this merge).  The current run (i+1) goes away in any case.
+         */
+        runLen[i] = len1 + len2;
+        if (i == stackSize - 3) {
+            runBase[i + 1] = runBase[i + 2];
+            runLen[i + 1] = runLen[i + 2];
+        }
+        stackSize--;
+        /*
+         * Find where the first element of run2 goes in run1. Prior elements
+         * in run1 can be ignored (because they're already in place).
+         */
+        int k = gallopRight(a[base2], a, base1, len1, 0, c);
+        if (DEBUG) assert k >= 0;
+        base1 += k;
+        len1 -= k;
+        if (len1 == 0)
+            return;
+        /*
+         * Find where the last element of run1 goes in run2. Subsequent elements
+         * in run2 can be ignored (because they're already in place).
+         */
+        len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c);
+        if (DEBUG) assert len2 >= 0;
+        if (len2 == 0)
+            return;
+        // Merge remaining runs, using tmp array with min(len1, len2) elements
+        if (len1 <= len2)
+            mergeLo(base1, len1, base2, len2);
+        else
+            mergeHi(base1, len1, base2, len2);
+    }
+    /**
+     * Locates the position at which to insert the specified key into the
+     * specified sorted range; if the range contains an element equal to key,
+     * returns the index of the leftmost equal element.
+     *
+     * @param key the key whose insertion point to search for
+     * @param a the array in which to search
+     * @param base the index of the first element in the range
+     * @param len the length of the range; must be > 0
+     * @param hint the index at which to begin the search, 0 <= hint < n.
+     *     The closer hint is to the result, the faster this method will run.
+     * @param c the comparator used to order the range, and to search
+     * @return the int k,  0 <= k <= n such that a[b + k - 1] < key <= a[b + k],
+     *    pretending that a[b - 1] is minus infinity and a[b + n] is infinity.
+     *    In other words, key belongs at index b + k; or in other words,
+     *    the first k elements of a should precede key, and the last n - k
+     *    should follow it.
+     */
+    private static int gallopLeft(long key, long[] a, int base, int len, int hint,
+                                  LongComparator c) {
+        if (DEBUG) assert len > 0 && hint >= 0 && hint < len;
+        int lastOfs = 0;
+        int ofs = 1;
+        if (c.compare(key, a[base + hint]) > 0) {
+            // Gallop right until a[base+hint+lastOfs] < key <= a[base+hint+ofs]
+            int maxOfs = len - hint;
+            while (ofs < maxOfs && c.compare(key, a[base + hint + ofs]) > 0) {
+                lastOfs = ofs;
+                ofs = (ofs << 1) + 1;
+                if (ofs <= 0)   // int overflow
+                    ofs = maxOfs;
+            }
+            if (ofs > maxOfs)
+                ofs = maxOfs;
+            // Make offsets relative to base
+            lastOfs += hint;
+            ofs += hint;
+        } else { // key <= a[base + hint]
+            // Gallop left until a[base+hint-ofs] < key <= a[base+hint-lastOfs]
+            final int maxOfs = hint + 1;
+            while (ofs < maxOfs && c.compare(key, a[base + hint - ofs]) <= 0) {
+                lastOfs = ofs;
+                ofs = (ofs << 1) + 1;
+                if (ofs <= 0)   // int overflow
+                    ofs = maxOfs;
+            }
+            if (ofs > maxOfs)
+                ofs = maxOfs;
+            // Make offsets relative to base
+            int tmp = lastOfs;
+            lastOfs = hint - ofs;
+            ofs = hint - tmp;
+        }
+        if (DEBUG) assert -1 <= lastOfs && lastOfs < ofs && ofs <= len;
+        /*
+         * Now a[base+lastOfs] < key <= a[base+ofs], so key belongs somewhere
+         * to the right of lastOfs but no farther right than ofs.  Do a binary
+         * search, with invariant a[base + lastOfs - 1] < key <= a[base + ofs].
+         */
+        lastOfs++;
+        while (lastOfs < ofs) {
+            int m = lastOfs + ((ofs - lastOfs) >>> 1);
+            if (c.compare(key, a[base + m]) > 0)
+                lastOfs = m + 1;  // a[base + m] < key
+            else
+                ofs = m;          // key <= a[base + m]
+        }
+        if (DEBUG) assert lastOfs == ofs;    // so a[base + ofs - 1] < key <= a[base + ofs]
+        return ofs;
+    }
+    /**
+     * Like gallopLeft, except that if the range contains an element equal to
+     * key, gallopRight returns the index after the rightmost equal element.
+     *
+     * @param key the key whose insertion point to search for
+     * @param a the array in which to search
+     * @param base the index of the first element in the range
+     * @param len the length of the range; must be > 0
+     * @param hint the index at which to begin the search, 0 <= hint < n.
+     *     The closer hint is to the result, the faster this method will run.
+     * @param c the comparator used to order the range, and to search
+     * @return the int k,  0 <= k <= n such that a[b + k - 1] <= key < a[b + k]
+     */
+    private static int gallopRight(long key, long[] a, int base, int len,
+                                       int hint, LongComparator c) {
+        if (DEBUG) assert len > 0 && hint >= 0 && hint < len;
+        int ofs = 1;
+        int lastOfs = 0;
+        if (c.compare(key, a[base + hint]) < 0) {
+            // Gallop left until a[b+hint - ofs] <= key < a[b+hint - lastOfs]
+            int maxOfs = hint + 1;
+            while (ofs < maxOfs && c.compare(key, a[base + hint - ofs]) < 0) {
+                lastOfs = ofs;
+                ofs = (ofs << 1) + 1;
+                if (ofs <= 0)   // int overflow
+                    ofs = maxOfs;
+            }
+            if (ofs > maxOfs)
+                ofs = maxOfs;
+            // Make offsets relative to b
+            int tmp = lastOfs;
+            lastOfs = hint - ofs;
+            ofs = hint - tmp;
+        } else { // a[b + hint] <= key
+            // Gallop right until a[b+hint + lastOfs] <= key < a[b+hint + ofs]
+            int maxOfs = len - hint;
+            while (ofs < maxOfs && c.compare(key, a[base + hint + ofs]) >= 0) {
+                lastOfs = ofs;
+                ofs = (ofs << 1) + 1;
+                if (ofs <= 0)   // int overflow
+                    ofs = maxOfs;
+            }
+            if (ofs > maxOfs)
+                ofs = maxOfs;
+            // Make offsets relative to b
+            lastOfs += hint;
+            ofs += hint;
+        }
+        if (DEBUG) assert -1 <= lastOfs && lastOfs < ofs && ofs <= len;
+        /*
+         * Now a[b + lastOfs] <= key < a[b + ofs], so key belongs somewhere to
+         * the right of lastOfs but no farther right than ofs.  Do a binary
+         * search, with invariant a[b + lastOfs - 1] <= key < a[b + ofs].
+         */
+        lastOfs++;
+        while (lastOfs < ofs) {
+            int m = lastOfs + ((ofs - lastOfs) >>> 1);
+            if (c.compare(key, a[base + m]) < 0)
+                ofs = m;          // key < a[b + m]
+            else
+                lastOfs = m + 1;  // a[b + m] <= key
+        }
+        if (DEBUG) assert lastOfs == ofs;    // so a[b + ofs - 1] <= key < a[b + ofs]
+        return ofs;
+    }
+    /**
+     * Merges two adjacent runs in place, in a stable fashion.  The first
+     * element of the first run must be greater than the first element of the
+     * second run (a[base1] > a[base2]), and the last element of the first run
+     * (a[base1 + len1-1]) must be greater than all elements of the second run.
+     *
+     * For performance, this method should be called only when len1 <= len2;
+     * its twin, mergeHi should be called if len1 >= len2.  (Either method
+     * may be called if len1 == len2.)
+     *
+     * @param base1 index of first element in first run to be merged
+     * @param len1  length of first run to be merged (must be > 0)
+     * @param base2 index of first element in second run to be merged
+     *        (must be aBase + aLen)
+     * @param len2  length of second run to be merged (must be > 0)
+     */
+    private void mergeLo(int base1, int len1, int base2, int len2) {
+        if (DEBUG) assert len1 > 0 && len2 > 0 && base1 + len1 == base2;
+        // Copy first run into temp array
+        long[] a = this.a; // For performance
+        long[] tmp = ensureCapacity(len1);
+        System.arraycopy(a, base1, tmp, 0, len1);
+        int cursor1 = 0;       // Indexes into tmp array
+        int cursor2 = base2;   // Indexes int a
+        int dest = base1;      // Indexes int a
+        // Move first element of second run and deal with degenerate cases
+        a[dest++] = a[cursor2++];
+        if (--len2 == 0) {
+            System.arraycopy(tmp, cursor1, a, dest, len1);
+            return;
+        }
+        if (len1 == 1) {
+            System.arraycopy(a, cursor2, a, dest, len2);
+            a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
+            return;
+        }
+        LongComparator c = this.c;  // Use local variable for performance
+        int minGallop = this.minGallop;    //  "    "       "     "      "
+        outer:
+        while (true) {
+            int count1 = 0; // Number of times in a row that first run won
+            int count2 = 0; // Number of times in a row that second run won
+            /*
+             * Do the straightforward thing until (if ever) one run starts
+             * winning consistently.
+             */
+            do {
+                if (DEBUG) assert len1 > 1 && len2 > 0;
+                if (c.compare(a[cursor2], tmp[cursor1]) < 0) {
+                    a[dest++] = a[cursor2++];
+                    count2++;
+                    count1 = 0;
+                    if (--len2 == 0)
+                        break outer;
+                } else {
+                    a[dest++] = tmp[cursor1++];
+                    count1++;
+                    count2 = 0;
+                    if (--len1 == 1)
+                        break outer;
+                }
+            } while ((count1 | count2) < minGallop);
+            /*
+             * One run is winning so consistently that galloping may be a
+             * huge win. So try that, and continue galloping until (if ever)
+             * neither run appears to be winning consistently anymore.
+             */
+            do {
+                if (DEBUG) assert len1 > 1 && len2 > 0;
+                count1 = gallopRight(a[cursor2], tmp, cursor1, len1, 0, c);
+                if (count1 != 0) {
+                    System.arraycopy(tmp, cursor1, a, dest, count1);
+                    dest += count1;
+                    cursor1 += count1;
+                    len1 -= count1;
+                    if (len1 <= 1) // len1 == 1 || len1 == 0
+                        break outer;
+                }
+                a[dest++] = a[cursor2++];
+                if (--len2 == 0)
+                    break outer;
+                count2 = gallopLeft(tmp[cursor1], a, cursor2, len2, 0, c);
+                if (count2 != 0) {
+                    System.arraycopy(a, cursor2, a, dest, count2);
+                    dest += count2;
+                    cursor2 += count2;
+                    len2 -= count2;
+                    if (len2 == 0)
+                        break outer;
+                }
+                a[dest++] = tmp[cursor1++];
+                if (--len1 == 1)
+                    break outer;
+                minGallop--;
+            } while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
+            if (minGallop < 0)
+                minGallop = 0;
+            minGallop += 2;  // Penalize for leaving gallop mode
+        }  // End of "outer" loop
+        this.minGallop = minGallop < 1 ? 1 : minGallop;  // Write back to field
+        if (len1 == 1) {
+            if (DEBUG) assert len2 > 0;
+            System.arraycopy(a, cursor2, a, dest, len2);
+            a[dest + len2] = tmp[cursor1]; //  Last elt of run 1 to end of merge
+        } else if (len1 == 0) {
+            throw new IllegalArgumentException(
+                                              "Comparison method violates its general contract!");
+        } else {
+            if (DEBUG) assert len2 == 0;
+            if (DEBUG) assert len1 > 1;
+            System.arraycopy(tmp, cursor1, a, dest, len1);
+        }
+    }
+    /**
+     * Like mergeLo, except that this method should be called only if
+     * len1 >= len2; mergeLo should be called if len1 <= len2.  (Either method
+     * may be called if len1 == len2.)
+     *
+     * @param base1 index of first element in first run to be merged
+     * @param len1  length of first run to be merged (must be > 0)
+     * @param base2 index of first element in second run to be merged
+     *        (must be aBase + aLen)
+     * @param len2  length of second run to be merged (must be > 0)
+     */
+    private void mergeHi(int base1, int len1, int base2, int len2) {
+        if (DEBUG) assert len1 > 0 && len2 > 0 && base1 + len1 == base2;
+        // Copy second run into temp array
+        long[] a = this.a; // For performance
+        long[] tmp = ensureCapacity(len2);
+        System.arraycopy(a, base2, tmp, 0, len2);
+        int cursor1 = base1 + len1 - 1;  // Indexes into a
+        int cursor2 = len2 - 1;          // Indexes into tmp array
+        int dest = base2 + len2 - 1;     // Indexes into a
+        // Move last element of first run and deal with degenerate cases
+        a[dest--] = a[cursor1--];
+        if (--len1 == 0) {
+            System.arraycopy(tmp, 0, a, dest - (len2 - 1), len2);
+            return;
+        }
+        if (len2 == 1) {
+            dest -= len1;
+            cursor1 -= len1;
+            System.arraycopy(a, cursor1 + 1, a, dest + 1, len1);
+            a[dest] = tmp[cursor2];
+            return;
+        }
+        LongComparator c = this.c;  // Use local variable for performance
+        int minGallop = this.minGallop;    //  "    "       "     "      "
+        outer:
+        while (true) {
+            int count1 = 0; // Number of times in a row that first run won
+            int count2 = 0; // Number of times in a row that second run won
+            /*
+             * Do the straightforward thing until (if ever) one run
+             * appears to win consistently.
+             */
+            do {
+                if (DEBUG) assert len1 > 0 && len2 > 1;
+                if (c.compare(tmp[cursor2], a[cursor1]) < 0) {
+                    a[dest--] = a[cursor1--];
+                    count1++;
+                    count2 = 0;
+                    if (--len1 == 0)
+                        break outer;
+                } else {
+                    a[dest--] = tmp[cursor2--];
+                    count2++;
+                    count1 = 0;
+                    if (--len2 == 1)
+                        break outer;
+                }
+            } while ((count1 | count2) < minGallop);
+            /*
+             * One run is winning so consistently that galloping may be a
+             * huge win. So try that, and continue galloping until (if ever)
+             * neither run appears to be winning consistently anymore.
+             */
+            do {
+                if (DEBUG) assert len1 > 0 && len2 > 1;
+                count1 = len1 - gallopRight(tmp[cursor2], a, base1, len1, len1 - 1, c);
+                if (count1 != 0) {
+                    dest -= count1;
+                    cursor1 -= count1;
+                    len1 -= count1;
+                    System.arraycopy(a, cursor1 + 1, a, dest + 1, count1);
+                    if (len1 == 0)
+                        break outer;
+                }
+                a[dest--] = tmp[cursor2--];
+                if (--len2 == 1)
+                    break outer;
+                count2 = len2 - gallopLeft(a[cursor1], tmp, 0, len2, len2 - 1, c);
+                if (count2 != 0) {
+                    dest -= count2;
+                    cursor2 -= count2;
+                    len2 -= count2;
+                    System.arraycopy(tmp, cursor2 + 1, a, dest + 1, count2);
+                    if (len2 <= 1)  // len2 == 1 || len2 == 0
+                        break outer;
+                }
+                a[dest--] = a[cursor1--];
+                if (--len1 == 0)
+                    break outer;
+                minGallop--;
+            } while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
+            if (minGallop < 0)
+                minGallop = 0;
+            minGallop += 2;  // Penalize for leaving gallop mode
+        }  // End of "outer" loop
+        this.minGallop = minGallop < 1 ? 1 : minGallop;  // Write back to field
+        if (len2 == 1) {
+            if (DEBUG) assert len1 > 0;
+            dest -= len1;
+            cursor1 -= len1;
+            System.arraycopy(a, cursor1 + 1, a, dest + 1, len1);
+            a[dest] = tmp[cursor2];  // Move first elt of run2 to front of merge
+        } else if (len2 == 0) {
+            throw new IllegalArgumentException(
+                                              "Comparison method violates its general contract!");
+        } else {
+            if (DEBUG) assert len1 == 0;
+            if (DEBUG) assert len2 > 0;
+            System.arraycopy(tmp, 0, a, dest - (len2 - 1), len2);
+        }
+    }
+    /**
+     * Ensures that the external array tmp has at least the specified
+     * number of elements, increasing its size if necessary.  The size
+     * increases exponentially to ensure amortized linear time complexity.
+     *
+     * @param minCapacity the minimum required capacity of the tmp array
+     * @return tmp, whether or not it grew
+     */
+    private long[] ensureCapacity(int minCapacity) {
+        if (tmp.length < minCapacity) {
+            // Compute smallest power of 2 > minCapacity
+            int newSize = minCapacity;
+            newSize |= newSize >> 1;
+            newSize |= newSize >> 2;
+            newSize |= newSize >> 4;
+            newSize |= newSize >> 8;
+            newSize |= newSize >> 16;
+            newSize++;
+            if (newSize < 0) // Not bloody likely!
+                newSize = minCapacity;
+            else
+                newSize = Math.min(newSize, a.length >>> 1);
+            @SuppressWarnings({"unchecked", "UnnecessaryLocalVariable"})
+            long[] newArray = new long[newSize];
+            tmp = newArray;
+        }
+        return tmp;
+    }
+    /**
+     * Checks that fromIndex and toIndex are in range, and throws an
+     * appropriate exception if they aren't.
+     *
+     * @param arrayLen the length of the array
+     * @param fromIndex the index of the first element of the range
+     * @param toIndex the index after the last element of the range
+     * @throws IllegalArgumentException if fromIndex > toIndex
+     * @throws ArrayIndexOutOfBoundsException if fromIndex < 0
+     *         or toIndex > arrayLen
+     */
+    private static void rangeCheck(int arrayLen, int fromIndex, int toIndex) {
+        if (fromIndex > toIndex)
+            throw new IllegalArgumentException("fromIndex(" + fromIndex +
+                                               ") > toIndex(" + toIndex+")");
+        if (fromIndex < 0)
+            throw new ArrayIndexOutOfBoundsException(fromIndex);
+        if (toIndex > arrayLen)
+            throw new ArrayIndexOutOfBoundsException(toIndex);
+    }
+    
+    // addition to original file
+    
+    @FunctionalInterface
+    public static interface LongComparator
+    {
+        int compare(long o1, long o2);
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/MBeanWrapper.java b/src/java/org/apache/cassandra/utils/MBeanWrapper.java
index edee6af..a76a2e7 100644
--- a/src/java/org/apache/cassandra/utils/MBeanWrapper.java
+++ b/src/java/org/apache/cassandra/utils/MBeanWrapper.java
@@ -19,10 +19,15 @@
 package org.apache.cassandra.utils;
 
 import java.lang.management.ManagementFactory;
+import java.util.Collections;
+import java.util.Set;
+import java.util.UUID;
 import java.util.function.Consumer;
 import javax.management.MBeanServer;
+import javax.management.MBeanServerFactory;
 import javax.management.MalformedObjectNameException;
 import javax.management.ObjectName;
+import javax.management.QueryExp;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -33,62 +38,160 @@
  */
 public interface MBeanWrapper
 {
-    static final Logger logger = LoggerFactory.getLogger(MBeanWrapper.class);
+    Logger logger = LoggerFactory.getLogger(MBeanWrapper.class);
 
-    static final MBeanWrapper instance = Boolean.getBoolean("org.apache.cassandra.disable_mbean_registration") ?
-                                         new NoOpMBeanWrapper() :
-                                         new PlatformMBeanWrapper();
+    MBeanWrapper instance = create();
+    String IS_DISABLED_MBEAN_REGISTRATION = "org.apache.cassandra.disable_mbean_registration";
+    String DTEST_IS_IN_JVM_DTEST = "org.apache.cassandra.dtest.is_in_jvm_dtest";
+    String MBEAN_REGISTRATION_CLASS = "org.apache.cassandra.mbean_registration_class";
+
+    static MBeanWrapper create()
+    {
+        // If we're running in the in-jvm dtest environment, always use the delegating
+        // mbean wrapper even if we start off with no-op, so it can be switched later
+        if (Boolean.getBoolean(DTEST_IS_IN_JVM_DTEST))
+        {
+            return new DelegatingMbeanWrapper(getMBeanWrapper());
+        }
+
+        return getMBeanWrapper();
+    }
+
+    static MBeanWrapper getMBeanWrapper()
+    {
+        if (Boolean.getBoolean(IS_DISABLED_MBEAN_REGISTRATION))
+        {
+            return new NoOpMBeanWrapper();
+        }
+
+        String klass = System.getProperty(MBEAN_REGISTRATION_CLASS);
+        if (klass == null)
+        {
+            if (Boolean.getBoolean(DTEST_IS_IN_JVM_DTEST))
+            {
+                return new NoOpMBeanWrapper();
+            }
+            else
+            {
+                return new PlatformMBeanWrapper();
+            }
+        }
+        return FBUtilities.construct(klass, "mbean");
+    }
+
+    static ObjectName create(String mbeanName, OnException onException)
+    {
+        try
+        {
+            return new ObjectName(mbeanName);
+        }
+        catch (MalformedObjectNameException e)
+        {
+            onException.handler.accept(e);
+            return null;
+        }
+    }
 
     // Passing true for graceful will log exceptions instead of rethrowing them
-    public void registerMBean(Object obj, ObjectName mbeanName, OnException onException);
+    void registerMBean(Object obj, ObjectName mbeanName, OnException onException);
+
     default void registerMBean(Object obj, ObjectName mbeanName)
     {
         registerMBean(obj, mbeanName, OnException.THROW);
     }
 
-    public void registerMBean(Object obj, String mbeanName, OnException onException);
+    default void registerMBean(Object obj, String mbeanName, OnException onException)
+    {
+        ObjectName name = create(mbeanName, onException);
+        if (name == null)
+        {
+            return;
+        }
+        registerMBean(obj, name, onException);
+    }
+
     default void registerMBean(Object obj, String mbeanName)
     {
         registerMBean(obj, mbeanName, OnException.THROW);
     }
 
-    public boolean isRegistered(ObjectName mbeanName, OnException onException);
+    boolean isRegistered(ObjectName mbeanName, OnException onException);
+
     default boolean isRegistered(ObjectName mbeanName)
     {
         return isRegistered(mbeanName, OnException.THROW);
     }
 
-    public boolean isRegistered(String mbeanName, OnException onException);
+    default boolean isRegistered(String mbeanName, OnException onException)
+    {
+        ObjectName name = create(mbeanName, onException);
+        if (name == null)
+        {
+            return false;
+        }
+        return isRegistered(name, onException);
+    }
+
     default boolean isRegistered(String mbeanName)
     {
         return isRegistered(mbeanName, OnException.THROW);
     }
 
-    public void unregisterMBean(ObjectName mbeanName, OnException onException);
+    void unregisterMBean(ObjectName mbeanName, OnException onException);
+
     default void unregisterMBean(ObjectName mbeanName)
     {
         unregisterMBean(mbeanName, OnException.THROW);
     }
 
-    public void unregisterMBean(String mbeanName, OnException onException);
+    default void unregisterMBean(String mbeanName, OnException onException)
+    {
+        ObjectName name = create(mbeanName, onException);
+        if (name == null)
+        {
+            return;
+        }
+        unregisterMBean(name, onException);
+    }
+
     default void unregisterMBean(String mbeanName)
     {
         unregisterMBean(mbeanName, OnException.THROW);
     }
 
-    static class NoOpMBeanWrapper implements MBeanWrapper
+    enum OnException
+    {
+        THROW(e -> { throw new RuntimeException(e); }),
+        LOG(e -> { logger.error("Error in MBean wrapper: ", e); }),
+        IGNORE(e -> { });
+
+        private Consumer<Exception> handler;
+        OnException(Consumer<Exception> handler)
+        {
+            this.handler = handler;
+        }
+    }
+
+    Set<ObjectName> queryNames(ObjectName name, QueryExp query);
+
+    MBeanServer getMBeanServer();
+
+    class NoOpMBeanWrapper implements MBeanWrapper
     {
         public void registerMBean(Object obj, ObjectName mbeanName, OnException onException) {}
         public void registerMBean(Object obj, String mbeanName, OnException onException) {}
-        public boolean isRegistered(ObjectName mbeanName, OnException onException) { return false; }
-        public boolean isRegistered(String mbeanName, OnException onException) { return false; }
+        public boolean isRegistered(ObjectName mbeanName, OnException onException) { return false;}
+        public boolean isRegistered(String mbeanName, OnException onException) { return false;}
         public void unregisterMBean(ObjectName mbeanName, OnException onException) {}
         public void unregisterMBean(String mbeanName, OnException onException) {}
+        public Set<ObjectName> queryNames(ObjectName name, QueryExp query) {return Collections.emptySet(); }
+        public MBeanServer getMBeanServer() { return null; }
     }
 
-    static class PlatformMBeanWrapper implements MBeanWrapper
+    class PlatformMBeanWrapper implements MBeanWrapper
     {
         private final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
+
         public void registerMBean(Object obj, ObjectName mbeanName, OnException onException)
         {
             try
@@ -101,18 +204,6 @@
             }
         }
 
-        public void registerMBean(Object obj, String mbeanName, OnException onException)
-        {
-            try
-            {
-                mbs.registerMBean(obj, new ObjectName(mbeanName));
-            }
-            catch (Exception e)
-            {
-                onException.handler.accept(e);
-            }
-        }
-
         public boolean isRegistered(ObjectName mbeanName, OnException onException)
         {
             try
@@ -126,11 +217,57 @@
             return false;
         }
 
-        public boolean isRegistered(String mbeanName, OnException onException)
+        public void unregisterMBean(ObjectName mbeanName, OnException onException)
         {
             try
             {
-                return mbs.isRegistered(new ObjectName(mbeanName));
+                mbs.unregisterMBean(mbeanName);
+            }
+            catch (Exception e)
+            {
+                onException.handler.accept(e);
+            }
+        }
+
+        public Set<ObjectName> queryNames(ObjectName name, QueryExp query)
+        {
+            return mbs.queryNames(name, query);
+        }
+
+        public MBeanServer getMBeanServer()
+        {
+            return mbs;
+        }
+
+    }
+
+    class InstanceMBeanWrapper implements MBeanWrapper
+    {
+        private MBeanServer mbs;
+        public final UUID id = UUID.randomUUID();
+
+        public InstanceMBeanWrapper(String hostname)
+        {
+            mbs = MBeanServerFactory.createMBeanServer(hostname + "-" + id);
+        }
+
+        public void registerMBean(Object obj, ObjectName mbeanName, OnException onException)
+        {
+            try
+            {
+                mbs.registerMBean(obj, mbeanName);
+            }
+            catch (Exception e)
+            {
+                onException.handler.accept(e);
+            }
+        }
+
+        public boolean isRegistered(ObjectName mbeanName, OnException onException)
+        {
+            try
+            {
+                return mbs.isRegistered(mbeanName);
             }
             catch (Exception e)
             {
@@ -151,29 +288,100 @@
             }
         }
 
-        public void unregisterMBean(String mbeanName, OnException onException)
+        public Set<ObjectName> queryNames(ObjectName name, QueryExp query)
+        {
+            return mbs.queryNames(name, query);
+        }
+
+        public MBeanServer getMBeanServer()
+        {
+            return mbs;
+        }
+
+        public void close()
+        {
+            mbs.queryNames(null, null).forEach(name -> {
+                try
+                {
+                    if (!name.getCanonicalName().contains("MBeanServerDelegate"))
+                    {
+                        mbs.unregisterMBean(name);
+                    }
+                }
+                catch (Throwable e)
+                {
+                    logger.debug("Could not unregister mbean {}", name.getCanonicalName());
+                }
+            });
+            MBeanServerFactory.releaseMBeanServer(mbs);
+            mbs = null;
+        }
+    }
+
+    class DelegatingMbeanWrapper implements MBeanWrapper
+    {
+        MBeanWrapper delegate;
+
+        public DelegatingMbeanWrapper(MBeanWrapper mBeanWrapper)
+        {
+            delegate = mBeanWrapper;
+        }
+
+        public MBeanWrapper getDelegate()
+        {
+            return delegate;
+        }
+
+        public void setDelegate(MBeanWrapper wrapper)
+        {
+            delegate = wrapper;
+        }
+
+        public void registerMBean(Object obj, ObjectName mbeanName, OnException onException)
         {
             try
             {
-                mbs.unregisterMBean(new ObjectName(mbeanName));
+                delegate.registerMBean(obj, mbeanName);
             }
             catch (Exception e)
             {
                 onException.handler.accept(e);
             }
         }
-    }
 
-    public enum OnException
-    {
-        THROW(e -> { throw new RuntimeException(e); }),
-        LOG(e -> { logger.error("Error in MBean wrapper: ", e); }),
-        IGNORE(e -> {});
-
-        private Consumer<Exception> handler;
-        OnException(Consumer<Exception> handler)
+        public boolean isRegistered(ObjectName mbeanName, OnException onException)
         {
-            this.handler = handler;
+            try
+            {
+                return delegate.isRegistered(mbeanName);
+            }
+            catch (Exception e)
+            {
+                onException.handler.accept(e);
+            }
+            return false;
+        }
+
+        public void unregisterMBean(ObjectName mbeanName, OnException onException)
+        {
+            try
+            {
+                delegate.unregisterMBean(mbeanName);
+            }
+            catch (Exception e)
+            {
+                onException.handler.accept(e);
+            }
+        }
+
+        public Set<ObjectName> queryNames(ObjectName name, QueryExp query)
+        {
+            return delegate.queryNames(name, query);
+        }
+
+        public MBeanServer getMBeanServer()
+        {
+            return delegate.getMBeanServer();
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/utils/MD5Digest.java b/src/java/org/apache/cassandra/utils/MD5Digest.java
index 2dc57de..4e736dc 100644
--- a/src/java/org/apache/cassandra/utils/MD5Digest.java
+++ b/src/java/org/apache/cassandra/utils/MD5Digest.java
@@ -17,9 +17,11 @@
  */
 package org.apache.cassandra.utils;
 
-import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 
+
 /**
  * The result of the computation of an MD5 digest.
  *
@@ -51,14 +53,12 @@
 
     public static MD5Digest compute(String toHash)
     {
-        try
-        {
-            return compute(toHash.getBytes("UTF-8"));
-        }
-        catch (UnsupportedEncodingException e)
-        {
-            throw new RuntimeException(e.getMessage());
-        }
+        return compute(toHash.getBytes(StandardCharsets.UTF_8));
+    }
+
+    public ByteBuffer byteBuffer()
+    {
+        return ByteBuffer.wrap(bytes);
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/utils/MergeIterator.java b/src/java/org/apache/cassandra/utils/MergeIterator.java
index 0cc5306..c9e445b 100644
--- a/src/java/org/apache/cassandra/utils/MergeIterator.java
+++ b/src/java/org/apache/cassandra/utils/MergeIterator.java
@@ -17,11 +17,8 @@
  */
 package org.apache.cassandra.utils;
 
-import java.io.Closeable;
 import java.util.*;
 
-import org.apache.cassandra.utils.AbstractIterator;
-
 /** Merges sorted input iterators which individually contain unique items. */
 public abstract class MergeIterator<In,Out> extends AbstractIterator<Out> implements IMergeIterator<In, Out>
 {
@@ -203,7 +200,7 @@
 
             reducer.onKeyChange();
             assert !heap[0].equalParent;
-            reducer.reduce(heap[0].idx, heap[0].consume());
+            heap[0].consume(reducer);
             final int size = this.size;
             final int sortedSectionSize = Math.min(size, SORTED_SECTION_SIZE);
             int i;
@@ -212,7 +209,7 @@
                 {
                     if (!heap[i].equalParent)
                         break consume;
-                    reducer.reduce(heap[i].idx, heap[i].consume());
+                    heap[i].consume(reducer);
                 }
                 i = Math.max(i, consumeHeap(i) + 1);
             }
@@ -230,7 +227,7 @@
             if (idx >= size || !heap[idx].equalParent)
                 return -1;
 
-            reducer.reduce(heap[idx].idx, heap[idx].consume());
+            heap[idx].consume(reducer);
             int nextIdx = (idx << 1) - (SORTED_SECTION_SIZE - 1);
             return Math.max(idx, Math.max(consumeHeap(nextIdx), consumeHeap(nextIdx + 1)));
         }
@@ -354,6 +351,7 @@
         private final Comparator<? super In> comp;
         private final int idx;
         private In item;
+        private In lowerBound;
         boolean equalParent;
 
         public Candidate(int idx, Iterator<? extends In> iter, Comparator<? super In> comp)
@@ -361,29 +359,55 @@
             this.iter = iter;
             this.comp = comp;
             this.idx = idx;
+            this.lowerBound = iter instanceof IteratorWithLowerBound ? ((IteratorWithLowerBound<In>)iter).lowerBound() : null;
         }
 
         /** @return this if our iterator had an item, and it is now available, otherwise null */
         protected Candidate<In> advance()
         {
+            if (lowerBound != null)
+            {
+                item = lowerBound;
+                return this;
+            }
+
             if (!iter.hasNext())
                 return null;
+
             item = iter.next();
             return this;
         }
 
         public int compareTo(Candidate<In> that)
         {
-            assert item != null && that.item != null;
-            return comp.compare(this.item, that.item);
+            assert this.item != null && that.item != null;
+            int ret = comp.compare(this.item, that.item);
+            if (ret == 0 && (this.isLowerBound() ^ that.isLowerBound()))
+            {   // if the items are equal and one of them is a lower bound (but not the other one)
+                // then ensure the lower bound is less than the real item so we can safely
+                // skip lower bounds when consuming
+                return this.isLowerBound() ? -1 : 1;
+            }
+            return ret;
         }
 
-        public In consume()
+        private boolean isLowerBound()
         {
-            In temp = item;
-            item = null;
-            assert temp != null;
-            return temp;
+            return item == lowerBound;
+        }
+
+        public void consume(Reducer reducer)
+        {
+            if (isLowerBound())
+            {
+                item = null;
+                lowerBound = null;
+            }
+            else
+            {
+                reducer.reduce(idx, item);
+                item = null;
+            }
         }
 
         public boolean needsAdvance()
diff --git a/src/java/org/apache/cassandra/utils/MerkleTree.java b/src/java/org/apache/cassandra/utils/MerkleTree.java
index 22b61e8..9572a27 100644
--- a/src/java/org/apache/cassandra/utils/MerkleTree.java
+++ b/src/java/org/apache/cassandra/utils/MerkleTree.java
@@ -373,19 +373,28 @@
 
     TreeRange getHelper(Hashable hashable, Token pleft, Token pright, byte depth, Token t)
     {
-        if (hashable instanceof Leaf)
+        while (true)
         {
-            // we've reached a hash: wrap it up and deliver it
-            return new TreeRange(this, pleft, pright, depth, hashable);
-        }
-        // else: node.
+            if (hashable instanceof Leaf)
+            {
+                // we've reached a hash: wrap it up and deliver it
+                return new TreeRange(this, pleft, pright, depth, hashable);
+            }
+            // else: node.
 
-        Inner node = (Inner)hashable;
-        if (Range.contains(pleft, node.token, t))
-            // left child contains token
-            return getHelper(node.lchild, pleft, node.token, inc(depth), t);
-        // else: right child contains token
-        return getHelper(node.rchild, node.token, pright, inc(depth), t);
+            Inner node = (Inner) hashable;
+            depth = inc(depth);
+            if (Range.contains(pleft, node.token, t))
+            { // left child contains token
+                hashable = node.lchild;
+                pright = node.token;
+            }
+            else
+            { // else: right child contains token
+                hashable = node.rchild;
+                pleft = node.token;
+            }
+        }
     }
 
     /**
@@ -451,33 +460,42 @@
      */
     private Hashable findHelper(Hashable current, Range<Token> activeRange, Range<Token> find) throws StopRecursion
     {
-        if (current instanceof Leaf)
+        while (true)
         {
-            if (!find.contains(activeRange))
-                // we are not fully contained in this range!
+            if (current instanceof Leaf)
+            {
+                if (!find.contains(activeRange))
+                    // we are not fully contained in this range!
+                    throw new StopRecursion.BadRange();
+                return current;
+            }
+            // else: node.
+
+            Inner node = (Inner) current;
+            Range<Token> leftRange = new Range<>(activeRange.left, node.token);
+            Range<Token> rightRange = new Range<>(node.token, activeRange.right);
+
+            if (find.contains(activeRange))
+                // this node is fully contained in the range
+                return node.calc();
+
+            // else: one of our children contains the range
+
+            if (leftRange.contains(find))
+            { // left child contains/matches the range
+                current = node.lchild;
+                activeRange = leftRange;
+            }
+            else if (rightRange.contains(find))
+            { // right child contains/matches the range
+                current = node.rchild;
+                activeRange = rightRange;
+            }
+            else
+            {
                 throw new StopRecursion.BadRange();
-            return current;
+            }
         }
-        // else: node.
-
-        Inner node = (Inner)current;
-        Range<Token> leftRange = new Range<Token>(activeRange.left, node.token);
-        Range<Token> rightRange = new Range<Token>(node.token, activeRange.right);
-
-        if (find.contains(activeRange))
-            // this node is fully contained in the range
-            return node.calc();
-
-        // else: one of our children contains the range
-
-        if (leftRange.contains(find))
-            // left child contains/matches the range
-            return findHelper(node.lchild, leftRange, find);
-        else if (rightRange.contains(find))
-            // right child contains/matches the range
-            return findHelper(node.rchild, rightRange, find);
-        else
-            throw new StopRecursion.BadRange();
     }
 
     /**
diff --git a/src/java/org/apache/cassandra/utils/MerkleTrees.java b/src/java/org/apache/cassandra/utils/MerkleTrees.java
index 4ae55ab..d2a8058 100644
--- a/src/java/org/apache/cassandra/utils/MerkleTrees.java
+++ b/src/java/org/apache/cassandra/utils/MerkleTrees.java
@@ -37,7 +37,7 @@
 
 /**
  * Wrapper class for handling of multiple MerkleTrees at once.
- * 
+ *
  * The MerkleTree's are divided in Ranges of non-overlapping tokens.
  */
 public class MerkleTrees implements Iterable<Map.Entry<Range<Token>, MerkleTree>>
@@ -50,7 +50,7 @@
 
     /**
      * Creates empty MerkleTrees object.
-     * 
+     *
      * @param partitioner The partitioner to use
      */
     public MerkleTrees(IPartitioner partitioner)
@@ -66,7 +66,7 @@
 
     /**
      * Get the ranges that these merkle trees covers.
-     * 
+     *
      * @return
      */
     public Collection<Range<Token>> ranges()
@@ -76,7 +76,7 @@
 
     /**
      * Get the partitioner in use.
-     * 
+     *
      * @return
      */
     public IPartitioner partitioner()
@@ -86,7 +86,7 @@
 
     /**
      * Add merkle tree's with the defined maxsize and ranges.
-     * 
+     *
      * @param maxsize
      * @param ranges
      */
@@ -100,7 +100,7 @@
 
     /**
      * Add a MerkleTree with the defined size and range.
-     * 
+     *
      * @param maxsize
      * @param range
      * @return The created merkle tree.
@@ -121,7 +121,7 @@
 
     /**
      * Get the MerkleTree.Range responsible for the given token.
-     * 
+     *
      * @param t
      * @return
      */
@@ -144,7 +144,7 @@
 
     /**
      * Init a selected MerkleTree with an even tree distribution.
-     * 
+     *
      * @param range
      */
     public void init(Range<Token> range)
@@ -154,7 +154,7 @@
 
     /**
      * Split the MerkleTree responsible for the given token.
-     * 
+     *
      * @param t
      * @return
      */
@@ -165,7 +165,7 @@
 
     /**
      * Invalidate the MerkleTree responsible for the given token.
-     * 
+     *
      * @param t
      */
     @VisibleForTesting
@@ -176,7 +176,7 @@
 
     /**
      * Get the MerkleTree responsible for the given token range.
-     * 
+     *
      * @param range
      * @return
      */
@@ -205,7 +205,7 @@
 
     /**
      * Get the MerkleTree responsible for the given token.
-     * 
+     *
      * @param t
      * @return The given MerkleTree or null if none exist.
      */
@@ -248,7 +248,7 @@
 
     /**
      * Get an iterator for all the invalids generated by the MerkleTrees.
-     * 
+     *
      * @return
      */
     public TreeRangeIterator invalids()
@@ -258,7 +258,7 @@
 
     /**
      * Log the row count per leaf for all MerkleTrees.
-     * 
+     *
      * @param logger
      */
     public void logRowCountPerLeaf(Logger logger)
@@ -271,7 +271,7 @@
 
     /**
      * Log the row size per leaf for all MerkleTrees.
-     * 
+     *
      * @param logger
      */
     public void logRowSizePerLeaf(Logger logger)
@@ -307,7 +307,7 @@
         {
             throw new RuntimeException("Unable to append merkle tree hash to result");
         }
-        
+
         return hashed ? baos.toByteArray() : null;
     }
 
@@ -370,7 +370,7 @@
 
     /**
      * Get the differences between the two sets of MerkleTrees.
-     * 
+     *
      * @param ltree
      * @param rtree
      * @return
diff --git a/src/java/org/apache/cassandra/utils/NanoTimeToCurrentTimeMillis.java b/src/java/org/apache/cassandra/utils/NanoTimeToCurrentTimeMillis.java
index 9d42acb..5aafbe5 100644
--- a/src/java/org/apache/cassandra/utils/NanoTimeToCurrentTimeMillis.java
+++ b/src/java/org/apache/cassandra/utils/NanoTimeToCurrentTimeMillis.java
@@ -19,10 +19,9 @@
 
 import java.util.concurrent.TimeUnit;
 
+import org.apache.cassandra.concurrent.ScheduledExecutors;
 import org.apache.cassandra.config.Config;
 
-import com.google.common.annotations.VisibleForTesting;
-
 /*
  * Convert from nanotime to non-monotonic current time millis. Beware of weaker ordering guarantees.
  */
@@ -36,11 +35,6 @@
 
     private static volatile long TIMESTAMP_BASE[] = new long[] { System.currentTimeMillis(), System.nanoTime() };
 
-    @VisibleForTesting
-    public static final Object TIMESTAMP_UPDATE = new Object();
-
-    private static final Thread updater;
-
     /*
      * System.currentTimeMillis() is 25 nanoseconds. This is 2 nanoseconds (maybe) according to JMH.
      * Faster than calling both currentTimeMillis() and nanoTime().
@@ -50,47 +44,29 @@
      * These timestamps don't order with System.currentTimeMillis() because currentTimeMillis() can tick over
      * before this one does. I have seen it behind by as much as 2ms on Linux and 25ms on Windows.
      */
-    public static final long convert(long nanoTime)
+    public static long convert(long nanoTime)
     {
         final long timestampBase[] = TIMESTAMP_BASE;
         return timestampBase[0] + TimeUnit.NANOSECONDS.toMillis(nanoTime - timestampBase[1]);
     }
 
-    static
+    public static void updateNow()
     {
-        //Pick up updates from NTP periodically
-        updater = new Thread("NanoTimeToCurrentTimeMillis updater")
-        {
-            @Override
-            public void run()
-            {
-                while (true)
-                {
-                    try
-                    {
-                        synchronized (TIMESTAMP_UPDATE)
-                        {
-                            TIMESTAMP_UPDATE.wait(TIMESTAMP_UPDATE_INTERVAL);
-                        }
-                    }
-                    catch (InterruptedException e)
-                    {
-                        return;
-                    }
-
-                    TIMESTAMP_BASE = new long[] {
-                            Math.max(TIMESTAMP_BASE[0], System.currentTimeMillis()),
-                            Math.max(TIMESTAMP_BASE[1], System.nanoTime()) };
-                }
-            }
-        };
-        updater.setDaemon(true);
-        updater.start();
+        ScheduledExecutors.scheduledFastTasks.submit(NanoTimeToCurrentTimeMillis::updateTimestampBase);
     }
 
-    public static void shutdown(long millis) throws InterruptedException
+    static
     {
-        updater.interrupt();
-        updater.join(millis);
+        ScheduledExecutors.scheduledFastTasks.scheduleWithFixedDelay(NanoTimeToCurrentTimeMillis::updateTimestampBase,
+                                                                     TIMESTAMP_UPDATE_INTERVAL,
+                                                                     TIMESTAMP_UPDATE_INTERVAL,
+                                                                     TimeUnit.MILLISECONDS);
+    }
+
+    private static void updateTimestampBase()
+    {
+        TIMESTAMP_BASE = new long[] {
+                                    Math.max(TIMESTAMP_BASE[0], System.currentTimeMillis()),
+                                    Math.max(TIMESTAMP_BASE[1], System.nanoTime()) };
     }
 }
diff --git a/src/java/org/apache/cassandra/utils/NativeLibrary.java b/src/java/org/apache/cassandra/utils/NativeLibrary.java
index 1a13a63..c333794 100644
--- a/src/java/org/apache/cassandra/utils/NativeLibrary.java
+++ b/src/java/org/apache/cassandra/utils/NativeLibrary.java
@@ -29,6 +29,7 @@
 import org.slf4j.LoggerFactory;
 
 import com.sun.jna.LastErrorException;
+import sun.nio.ch.FileChannelImpl;
 
 import org.apache.cassandra.io.FSWriteError;
 
@@ -73,8 +74,14 @@
     private static final NativeLibraryWrapper wrappedLibrary;
     private static boolean jnaLockable = false;
 
+    private static final Field FILE_DESCRIPTOR_FD_FIELD;
+    private static final Field FILE_CHANNEL_FD_FIELD;
+
     static
     {
+        FILE_DESCRIPTOR_FD_FIELD = FBUtilities.getProtectedField(FileDescriptor.class, "fd");
+        FILE_CHANNEL_FD_FIELD = FBUtilities.getProtectedField(FileChannelImpl.class, "fd");
+
         // detect the OS type the JVM is running on and then set the CLibraryWrapper
         // instance to a compatable implementation of CLibraryWrapper for that OS type
         osType = getOsType();
@@ -251,7 +258,7 @@
             if (!(e instanceof LastErrorException))
                 throw e;
 
-            logger.warn(String.format("posix_fadvise(%d, %d) failed, errno (%d).", fd, offset, errno(e)));
+            logger.warn("posix_fadvise({}, {}) failed, errno ({}).", fd, offset, errno(e));
         }
     }
 
@@ -273,7 +280,7 @@
             if (!(e instanceof LastErrorException))
                 throw e;
 
-            logger.warn(String.format("fcntl(%d, %d, %d) failed, errno (%d).", fd, command, flags, errno(e)));
+            logger.warn("fcntl({}, {}, {}) failed, errno ({}).", fd, command, flags, errno(e));
         }
 
         return result;
@@ -296,7 +303,7 @@
             if (!(e instanceof LastErrorException))
                 throw e;
 
-            logger.warn(String.format("open(%s, O_RDONLY) failed, errno (%d).", path, errno(e)));
+            logger.warn("open({}, O_RDONLY) failed, errno ({}).", path, errno(e));
         }
 
         return fd;
@@ -352,11 +359,9 @@
 
     public static int getfd(FileChannel channel)
     {
-        Field field = FBUtilities.getProtectedField(channel.getClass(), "fd");
-
         try
         {
-            return getfd((FileDescriptor)field.get(channel));
+            return getfd((FileDescriptor)FILE_CHANNEL_FD_FIELD.get(channel));
         }
         catch (IllegalArgumentException|IllegalAccessException e)
         {
@@ -372,11 +377,9 @@
      */
     public static int getfd(FileDescriptor descriptor)
     {
-        Field field = FBUtilities.getProtectedField(descriptor.getClass(), "fd");
-
         try
         {
-            return field.getInt(descriptor);
+            return FILE_DESCRIPTOR_FD_FIELD.getInt(descriptor);
         }
         catch (Exception e)
         {
diff --git a/src/java/org/apache/cassandra/utils/NativeLibraryWindows.java b/src/java/org/apache/cassandra/utils/NativeLibraryWindows.java
index d6514af..b8304c7 100644
--- a/src/java/org/apache/cassandra/utils/NativeLibraryWindows.java
+++ b/src/java/org/apache/cassandra/utils/NativeLibraryWindows.java
@@ -29,7 +29,7 @@
 
 /**
  * A {@code NativeLibraryWrapper} implementation for Windows.
- * <p> This implementation only offer support for the {@code callGetpid} method
+ * <p> This implementation only offers support for the {@code callGetpid} method
  * using the Windows/Kernel32 library.</p>
  *
  * @see org.apache.cassandra.utils.NativeLibraryWrapper
@@ -37,10 +37,10 @@
  */
 public class NativeLibraryWindows implements NativeLibraryWrapper
 {
-    private static boolean available;
-
     private static final Logger logger = LoggerFactory.getLogger(NativeLibraryWindows.class);
 
+    private static boolean available;
+
     static
     {
         try
diff --git a/src/java/org/apache/cassandra/utils/NativeSSTableLoaderClient.java b/src/java/org/apache/cassandra/utils/NativeSSTableLoaderClient.java
index 9ab4538..3e17ff0 100644
--- a/src/java/org/apache/cassandra/utils/NativeSSTableLoaderClient.java
+++ b/src/java/org/apache/cassandra/utils/NativeSSTableLoaderClient.java
@@ -19,22 +19,40 @@
 
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
-import com.datastax.driver.core.*;
-
+import com.datastax.driver.core.AuthProvider;
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.Host;
+import com.datastax.driver.core.Metadata;
+import com.datastax.driver.core.PlainTextAuthProvider;
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.SSLOptions;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.TokenRange;
+import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.config.ColumnDefinition.ClusteringOrder;
-import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.ColumnIdentifier;
-import org.apache.cassandra.db.marshal.*;
-import org.apache.cassandra.dht.*;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.ReversedType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.dht.Token.TokenFactory;
 import org.apache.cassandra.io.sstable.SSTableLoader;
 import org.apache.cassandra.schema.CQLTypeParser;
-import org.apache.cassandra.schema.SchemaKeyspace;
+import org.apache.cassandra.schema.SchemaKeyspaceTables;
 import org.apache.cassandra.schema.Types;
 
 public class NativeSSTableLoaderClient extends SSTableLoader.Client
@@ -109,7 +127,7 @@
 
     private static Types fetchTypes(String keyspace, Session session)
     {
-        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ?", SchemaKeyspace.NAME, SchemaKeyspace.TYPES);
+        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TYPES);
 
         Types.RawBuilder types = Types.rawBuilder(keyspace);
         for (Row row : session.execute(query, keyspace))
@@ -134,7 +152,7 @@
     private static Map<String, CFMetaData> fetchTables(String keyspace, Session session, IPartitioner partitioner, Types types)
     {
         Map<String, CFMetaData> tables = new HashMap<>();
-        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ?", SchemaKeyspace.NAME, SchemaKeyspace.TABLES);
+        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TABLES);
 
         for (Row row : session.execute(query, keyspace))
         {
@@ -151,7 +169,7 @@
     private static Map<String, CFMetaData> fetchViews(String keyspace, Session session, IPartitioner partitioner, Types types)
     {
         Map<String, CFMetaData> tables = new HashMap<>();
-        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ?", SchemaKeyspace.NAME, SchemaKeyspace.VIEWS);
+        String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.VIEWS);
 
         for (Row row : session.execute(query, keyspace))
         {
@@ -179,8 +197,8 @@
         boolean isCompound = isView || flags.contains(CFMetaData.Flag.COMPOUND);
 
         String columnsQuery = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?",
-                                            SchemaKeyspace.NAME,
-                                            SchemaKeyspace.COLUMNS);
+                                            SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                            SchemaKeyspaceTables.COLUMNS);
 
         List<ColumnDefinition> defs = new ArrayList<>();
         for (Row colRow : session.execute(columnsQuery, keyspace, name))
@@ -198,8 +216,8 @@
                                                 partitioner);
 
         String droppedColumnsQuery = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?",
-                                                   SchemaKeyspace.NAME,
-                                                   SchemaKeyspace.DROPPED_COLUMNS);
+                                                   SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                                   SchemaKeyspaceTables.DROPPED_COLUMNS);
         Map<ByteBuffer, CFMetaData.DroppedColumn> droppedColumns = new HashMap<>();
         for (Row colRow : session.execute(droppedColumnsQuery, keyspace, name))
         {
diff --git a/src/java/org/apache/cassandra/utils/NoSpamLogger.java b/src/java/org/apache/cassandra/utils/NoSpamLogger.java
index df3d2e4..bee8c06 100644
--- a/src/java/org/apache/cassandra/utils/NoSpamLogger.java
+++ b/src/java/org/apache/cassandra/utils/NoSpamLogger.java
@@ -217,7 +217,8 @@
         return NoSpamLogger.this.error(CLOCK.nanoTime(), s, objects);
     }
 
-    public boolean log(Level l, String s, long nowNanos, Object... objects) {
+    public boolean log(Level l, String s, long nowNanos, Object... objects)
+    {
         return NoSpamLogger.this.getStatement(s, minIntervalNanos).log(l, nowNanos, objects);
     }
 
@@ -231,7 +232,8 @@
         return NoSpamLogger.this.getStatement(key, s, minIntervalNanos);
     }
 
-    public NoSpamLogStatement getStatement(String s, long minInterval, TimeUnit unit) {
+    public NoSpamLogStatement getStatement(String s, long minInterval, TimeUnit unit)
+    {
         return NoSpamLogger.this.getStatement(s, unit.toNanos(minInterval));
     }
 
diff --git a/src/java/org/apache/cassandra/utils/ObjectSizes.java b/src/java/org/apache/cassandra/utils/ObjectSizes.java
index e7469c1..dea2bac 100644
--- a/src/java/org/apache/cassandra/utils/ObjectSizes.java
+++ b/src/java/org/apache/cassandra/utils/ObjectSizes.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.utils;
 /*
- * 
+ *
  * 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
@@ -8,23 +8,21 @@
  * 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.
- * 
+ *
  */
 
 
 import java.nio.ByteBuffer;
 
-import java.util.ArrayList;
-
 import org.github.jamm.MemoryLayoutSpecification;
 import org.github.jamm.MemoryMeter;
 
diff --git a/src/java/org/apache/cassandra/utils/OverlapIterator.java b/src/java/org/apache/cassandra/utils/OverlapIterator.java
index b346a62..7c1544a 100644
--- a/src/java/org/apache/cassandra/utils/OverlapIterator.java
+++ b/src/java/org/apache/cassandra/utils/OverlapIterator.java
@@ -17,7 +17,7 @@
  * specific language governing permissions and limitations
  * under the License.
  *
-*/
+ */
 package org.apache.cassandra.utils;
 
 import java.util.*;
diff --git a/src/java/org/apache/cassandra/utils/RMIClientSocketFactoryImpl.java b/src/java/org/apache/cassandra/utils/RMIClientSocketFactoryImpl.java
new file mode 100644
index 0000000..62ab88f
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/RMIClientSocketFactoryImpl.java
@@ -0,0 +1,62 @@
+/*
+ * 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.utils;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.rmi.server.RMIClientSocketFactory;
+import java.util.Objects;
+
+/**
+ * This class is used to override the local address the JMX client calculates when trying to connect,
+ * which can otherwise be influenced by the system property "java.rmi.server.hostname" in strange and
+ * unpredictable ways.
+ */
+public class RMIClientSocketFactoryImpl implements RMIClientSocketFactory, Serializable
+{
+    private final InetAddress localAddress;
+
+    public RMIClientSocketFactoryImpl(InetAddress localAddress)
+    {
+        this.localAddress = localAddress;
+    }
+
+    @Override
+    public Socket createSocket(String host, int port) throws IOException
+    {
+        return new Socket(localAddress, port);
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        RMIClientSocketFactoryImpl that = (RMIClientSocketFactoryImpl) o;
+        return Objects.equals(localAddress, that.localAddress);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(localAddress);
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/RMIServerSocketFactoryImpl.java b/src/java/org/apache/cassandra/utils/RMIServerSocketFactoryImpl.java
index 6444a65..9baf0c7 100644
--- a/src/java/org/apache/cassandra/utils/RMIServerSocketFactoryImpl.java
+++ b/src/java/org/apache/cassandra/utils/RMIServerSocketFactoryImpl.java
@@ -27,14 +27,19 @@
 import java.rmi.server.RMIServerSocketFactory;
 import javax.net.ServerSocketFactory;
 
-
 public class RMIServerSocketFactoryImpl implements RMIServerSocketFactory
 {
+    // Address to bind server sockets too, may be null indicating all local interfaces are to be bound
+    private final InetAddress bindAddress;
+
+    public RMIServerSocketFactoryImpl(InetAddress bindAddress)
+    {
+        this.bindAddress = bindAddress;
+    }
 
     public ServerSocket createServerSocket(final int pPort) throws IOException
     {
-        ServerSocket socket = ServerSocketFactory.getDefault()
-                                                 .createServerSocket(pPort, 0, InetAddress.getLoopbackAddress());
+        ServerSocket socket = ServerSocketFactory.getDefault().createServerSocket(pPort, 0, bindAddress);
         try
         {
             socket.setReuseAddress(true);
@@ -66,3 +71,4 @@
         return RMIServerSocketFactoryImpl.class.hashCode();
     }
 }
+
diff --git a/src/java/org/apache/cassandra/utils/SearchIterator.java b/src/java/org/apache/cassandra/utils/SearchIterator.java
index 908053b..4ea5666 100644
--- a/src/java/org/apache/cassandra/utils/SearchIterator.java
+++ b/src/java/org/apache/cassandra/utils/SearchIterator.java
@@ -24,7 +24,7 @@
      * if this or any key greater has already been returned by the iterator, the method may
      * choose to return null, the correct or incorrect output, or fail an assertion.
      *
-     * it is permitted to search past the end of the iterator, i.e. !hasNext() => next(?) == null
+     * it is permitted to search past the end of the iterator, i.e. {@code !hasNext() => next(?) == null}
      *
      * @param key to search for
      * @return value associated with key, if present in direction of travel
diff --git a/src/java/org/apache/cassandra/utils/SigarLibrary.java b/src/java/org/apache/cassandra/utils/SigarLibrary.java
index 0312204..246a9c8 100644
--- a/src/java/org/apache/cassandra/utils/SigarLibrary.java
+++ b/src/java/org/apache/cassandra/utils/SigarLibrary.java
@@ -112,7 +112,7 @@
     private boolean hasAcceptableAddressSpace()
     {
         // Check is invalid on Windows
-        if (FBUtilities.isWindows())
+        if (FBUtilities.isWindows)
             return true;
 
         try
diff --git a/src/java/org/apache/cassandra/utils/SlidingTimeRate.java b/src/java/org/apache/cassandra/utils/SlidingTimeRate.java
new file mode 100644
index 0000000..3053a05
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/SlidingTimeRate.java
@@ -0,0 +1,167 @@
+/*
+ * 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.utils;
+
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+/**
+ * Concurrent rate computation over a sliding time window.
+ */
+public class SlidingTimeRate
+{
+    private final ConcurrentSkipListMap<Long, AtomicInteger> counters = new ConcurrentSkipListMap<>();
+    private final AtomicLong lastCounterTimestamp = new AtomicLong(0);
+    private final ReadWriteLock pruneLock = new ReentrantReadWriteLock();
+    private final long sizeInMillis;
+    private final long precisionInMillis;
+    private final TimeSource timeSource;
+
+    /**
+     * Creates a sliding rate whose time window is of the given size, with the given precision and time unit.
+     * <br/>
+     * The precision defines how accurate the rate computation is, as it will be computed over window size +/-
+     * precision.
+     */
+    public SlidingTimeRate(TimeSource timeSource, long size, long precision, TimeUnit unit)
+    {
+        Preconditions.checkArgument(size > precision, "Size should be greater than precision.");
+        Preconditions.checkArgument(TimeUnit.MILLISECONDS.convert(precision, unit) >= 1, "Precision must be greater than or equal to 1 millisecond.");
+        this.sizeInMillis = TimeUnit.MILLISECONDS.convert(size, unit);
+        this.precisionInMillis = TimeUnit.MILLISECONDS.convert(precision, unit);
+        this.timeSource = timeSource;
+    }
+
+    /**
+     * Updates the rate.
+     */
+    public void update(int delta)
+    {
+        pruneLock.readLock().lock();
+        try
+        {
+            while (true)
+            {
+                long now = timeSource.currentTimeMillis();
+                long lastTimestamp = lastCounterTimestamp.get();
+                boolean isWithinPrecisionRange = (now - lastTimestamp) < precisionInMillis;
+                AtomicInteger lastCounter = counters.get(lastTimestamp);
+                // If there's a valid counter for the current last timestamp, and we're in the precision range,
+                // update such counter:
+                if (lastCounter != null && isWithinPrecisionRange)
+                {
+                    lastCounter.addAndGet(delta);
+
+                    break;
+                }
+                // Else if there's no counter or we're past the precision range, try to create a new counter,
+                // but only the thread updating the last timestamp will create a new counter:
+                else if (lastCounterTimestamp.compareAndSet(lastTimestamp, now))
+                {
+                    AtomicInteger existing = counters.putIfAbsent(now, new AtomicInteger(delta));
+                    if (existing != null)
+                    {
+                        existing.addAndGet(delta);
+                    }
+
+                    break;
+                }
+            }
+        }
+        finally
+        {
+            pruneLock.readLock().unlock();
+        }
+    }
+
+    /**
+     * Gets the current rate in the given time unit from the beginning of the time window to the
+     * provided point in time ago.
+     */
+    public double get(long toAgo, TimeUnit unit)
+    {
+        pruneLock.readLock().lock();
+        try
+        {
+            long toAgoInMillis = TimeUnit.MILLISECONDS.convert(toAgo, unit);
+            Preconditions.checkArgument(toAgoInMillis < sizeInMillis, "Cannot get rate in the past!");
+
+            long now = timeSource.currentTimeMillis();
+            long sum = 0;
+            ConcurrentNavigableMap<Long, AtomicInteger> tailCounters = counters
+                    .tailMap(now - sizeInMillis, true)
+                    .headMap(now - toAgoInMillis, true);
+            for (AtomicInteger i : tailCounters.values())
+            {
+                sum += i.get();
+            }
+
+            double rateInMillis = sum == 0
+                                  ? sum
+                                  : sum / (double) Math.max(1000, (now - toAgoInMillis) - tailCounters.firstKey());
+            double multiplier = TimeUnit.MILLISECONDS.convert(1, unit);
+            return rateInMillis * multiplier;
+        }
+        finally
+        {
+            pruneLock.readLock().unlock();
+        }
+    }
+
+    /**
+     * Gets the current rate in the given time unit.
+     */
+    public double get(TimeUnit unit)
+    {
+        return get(0, unit);
+    }
+
+    /**
+     * Prunes the time window of old unused updates.
+     */
+    public void prune()
+    {
+        pruneLock.writeLock().lock();
+        try
+        {
+            long now = timeSource.currentTimeMillis();
+            counters.headMap(now - sizeInMillis, false).clear();
+        }
+        finally
+        {
+            pruneLock.writeLock().unlock();
+        }
+    }
+
+    @VisibleForTesting
+    public int size()
+    {
+        return counters.values().stream().reduce(new AtomicInteger(), (v1, v2) -> {
+            v1.addAndGet(v2.get());
+            return v1;
+        }).get();
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/StatusLogger.java b/src/java/org/apache/cassandra/utils/StatusLogger.java
index 712e8f7..c33190b 100644
--- a/src/java/org/apache/cassandra/utils/StatusLogger.java
+++ b/src/java/org/apache/cassandra/utils/StatusLogger.java
@@ -19,19 +19,14 @@
 
 import java.lang.management.ManagementFactory;
 import java.util.Map;
-import java.util.Set;
 import javax.management.*;
 
-import com.google.common.collect.Iterables;
-
 import org.apache.cassandra.cache.*;
 
-import org.apache.cassandra.concurrent.Stage;
 import org.apache.cassandra.metrics.ThreadPoolMetrics;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.cassandra.concurrent.JMXEnabledThreadPoolExecutorMBean;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.RowIndexEntry;
diff --git a/src/java/org/apache/cassandra/utils/StreamingHistogram.java b/src/java/org/apache/cassandra/utils/StreamingHistogram.java
index 6500a1a..df49d8d 100644
--- a/src/java/org/apache/cassandra/utils/StreamingHistogram.java
+++ b/src/java/org/apache/cassandra/utils/StreamingHistogram.java
@@ -24,6 +24,7 @@
 
 import org.apache.cassandra.db.TypeSizes;
 import org.apache.cassandra.io.ISerializer;
+import org.apache.cassandra.io.sstable.SSTable;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
 
@@ -39,15 +40,33 @@
     public static final StreamingHistogramSerializer serializer = new StreamingHistogramSerializer();
 
     // TreeMap to hold bins of histogram.
-    private final TreeMap<Double, Long> bin;
+    // The key is a numeric type so we can avoid boxing/unboxing streams of different key types
+    // The value is a unboxed long array always of length == 1
+    // Serialized Histograms always writes with double keys for backwards compatibility
+    private final TreeMap<Number, long[]> bin;
+
+    // maximum bin size for this histogram
     private final int maxBinSize;
 
-    private StreamingHistogram(int maxBinSize, Map<Double, Long> bin)
+
+    /**
+     * Creates a new histogram with max bin size of maxBinSize
+     * @param maxBinSize maximum number of bins this histogram can have
+     * @param source the existing bins in map form
+     */
+    private StreamingHistogram(int maxBinSize, Map<Number, long[]> source)
     {
         this.maxBinSize = maxBinSize;
-        this.bin = new TreeMap<>(bin);
+        this.bin = new TreeMap<>((o1, o2) -> {
+            if (o1.getClass().equals(o2.getClass()))
+                return ((Comparable)o1).compareTo(o2);
+            else
+                return Double.compare(o1.doubleValue(), o2.doubleValue());
+        });
+        for (Map.Entry<Number, long[]> entry : source.entrySet())
+            this.bin.put(entry.getKey(), new long[]{entry.getValue()[0]});
     }
-
+    
     /**
      * Calculates estimated number of points in interval [-inf,b].
      *
@@ -58,32 +77,32 @@
     {
         double sum = 0;
         // find the points pi, pnext which satisfy pi <= b < pnext
-        Map.Entry<Double, Long> pnext = bin.higherEntry(b);
+        Map.Entry<Number, long[]> pnext = bin.higherEntry(b);
         if (pnext == null)
         {
             // if b is greater than any key in this histogram,
             // just count all appearance and return
-            for (Long value : bin.values())
-                sum += value;
+            for (long[] value : bin.values())
+                sum += value[0];
         }
         else
         {
-            Map.Entry<Double, Long> pi = bin.floorEntry(b);
+            Map.Entry<Number, long[]> pi = bin.floorEntry(b);
             if (pi == null)
                 return 0;
             // calculate estimated count mb for point b
-            double weight = (b - pi.getKey()) / (pnext.getKey() - pi.getKey());
-            double mb = pi.getValue() + (pnext.getValue() - pi.getValue()) * weight;
-            sum += (pi.getValue() + mb) * weight / 2;
+            double weight = (b - pi.getKey().doubleValue()) / (pnext.getKey().doubleValue() - pi.getKey().doubleValue());
+            double mb = pi.getValue()[0] + (pnext.getValue()[0] - pi.getValue()[0]) * weight;
+            sum += (pi.getValue()[0] + mb) * weight / 2;
 
-            sum += pi.getValue() / 2.0;
-            for (Long value : bin.headMap(pi.getKey(), false).values())
-                sum += value;
+            sum += pi.getValue()[0] / 2.0;
+            for (long[] value : bin.headMap(pi.getKey(), false).values())
+                sum += value[0];
         }
         return sum;
     }
 
-    public Map<Double, Long> getAsMap()
+    public Map<Number, long[]> getAsMap()
     {
         return Collections.unmodifiableMap(bin);
     }
@@ -91,10 +110,13 @@
     public static class StreamingHistogramBuilder
     {
         // TreeMap to hold bins of histogram.
-        private final TreeMap<Double, Long> bin;
+        // The key is a numeric type so we can avoid boxing/unboxing streams of different key types
+        // The value is a unboxed long array always of length == 1
+        // Serialized Histograms always writes with double keys for backwards compatibility
+        private final TreeMap<Number, long[]> bin;
 
         // Keep a second, larger buffer to spool data in, before finalizing it into `bin`
-        private final TreeMap<Double, Long> spool;
+        private final TreeMap<Number, long[]> spool;
 
         // maximum bin size for this histogram
         private final int maxBinSize;
@@ -104,6 +126,7 @@
 
         // voluntarily give up resolution for speed
         private final int roundSeconds;
+
         /**
          * Creates a new histogram with max bin size of maxBinSize
          * @param maxBinSize maximum number of bins this histogram can have
@@ -113,22 +136,34 @@
             this.maxBinSize = maxBinSize;
             this.maxSpoolSize = maxSpoolSize;
             this.roundSeconds = roundSeconds;
-            bin = new TreeMap<>();
-            spool = new TreeMap<>();
+            bin = new TreeMap<>((o1, o2) -> {
+                if (o1.getClass().equals(o2.getClass()))
+                    return ((Comparable)o1).compareTo(o2);
+                else
+                    return Double.compare(o1.doubleValue(), o2.doubleValue());
+            });
+            spool = new TreeMap<>((o1, o2) -> {
+                if (o1.getClass().equals(o2.getClass()))
+                    return ((Comparable)o1).compareTo(o2);
+                else
+                    return Double.compare(o1.doubleValue(), o2.doubleValue());
+            });
+
         }
 
         public StreamingHistogram build()
         {
             flushHistogram();
-            return new StreamingHistogram(maxBinSize, bin);
+            return new StreamingHistogram(maxBinSize,  bin);
         }
+
         /**
          * Adds new point p to this histogram.
          * @param p
          */
-        public void update(double p)
+        public void update(Number p)
         {
-            update(p, 1);
+            update(p, 1L);
         }
 
         /**
@@ -136,76 +171,90 @@
          * @param p
          * @param m
          */
-        public void update(double p, long m)
+        public void update(Number p, long m)
         {
-            double d = p % this.roundSeconds;
-            if (d > 0)
-                p = p + (this.roundSeconds - d);
+            Number d = p.longValue() % this.roundSeconds;
+            if (d.longValue() > 0)
+                p =p.longValue() + (this.roundSeconds - d.longValue());
 
-            Long mi = spool.get(p);
+            long[] mi = spool.get(p);
             if (mi != null)
             {
                 // we found the same p so increment that counter
-                spool.put(p, mi + m);
+                mi[0] += m;
             }
             else
             {
-                spool.put(p, m);
+                mi = new long[]{m};
+                spool.put(p, mi);
             }
+
+            // If spool has overflowed, compact it
             if(spool.size() > maxSpoolSize)
                 flushHistogram();
         }
 
+
+
         /**
          * Drain the temporary spool into the final bins
          */
         public void flushHistogram()
         {
-            if(spool.size() > 0)
+            if (spool.size() > 0)
             {
-                Long spoolValue;
-                Long binValue;
+                long[] spoolValue;
+                long[] binValue;
 
                 // Iterate over the spool, copying the value into the primary bin map
                 // and compacting that map as necessary
-                for (Map.Entry<Double, Long> entry : spool.entrySet())
+                for (Map.Entry<Number, long[]> entry : spool.entrySet())
                 {
-                    Double key = entry.getKey();
+                    Number key = entry.getKey();
                     spoolValue = entry.getValue();
                     binValue = bin.get(key);
 
-                    if (binValue != null)
+                    // If this value is already in the final histogram bins
+                    // Simply increment and update, otherwise, insert a new long[1] value
+                    if(binValue != null)
                     {
-                        binValue += spoolValue;
+                        binValue[0] += spoolValue[0];
                         bin.put(key, binValue);
-                    } else
+                    }
+                    else
                     {
-                        bin.put(key, spoolValue);
+                        bin.put(key, new long[]{spoolValue[0]});
                     }
 
-                    // if bin size exceeds maximum bin size then trim down to max size
                     if (bin.size() > maxBinSize)
                     {
                         // find points p1, p2 which have smallest difference
-                        Iterator<Double> keys = bin.keySet().iterator();
-                        double p1 = keys.next();
-                        double p2 = keys.next();
+                        Iterator<Number> keys = bin.keySet().iterator();
+                        double p1 = keys.next().doubleValue();
+                        double p2 = keys.next().doubleValue();
                         double smallestDiff = p2 - p1;
                         double q1 = p1, q2 = p2;
-                        while (keys.hasNext()) {
+                        while (keys.hasNext())
+                        {
                             p1 = p2;
-                            p2 = keys.next();
+                            p2 = keys.next().doubleValue();
                             double diff = p2 - p1;
-                            if (diff < smallestDiff) {
+                            if (diff < smallestDiff)
+                            {
                                 smallestDiff = diff;
                                 q1 = p1;
                                 q2 = p2;
                             }
                         }
                         // merge those two
-                        long k1 = bin.remove(q1);
-                        long k2 = bin.remove(q2);
-                        bin.put((q1 * k1 + q2 * k2) / (k1 + k2), k1 + k2);
+                        long[] a1 = bin.remove(q1);
+                        long[] a2 = bin.remove(q2);
+                        long k1 = a1[0];
+                        long k2 = a2[0];
+
+                        a1[0] += k2;
+                        bin.put((q1 * k1 + q2 * k2) / (k1 + k2), a1);
+
                     }
                 }
                 spool.clear();
@@ -213,19 +262,17 @@
         }
 
         /**
-        * Merges given histogram with this histogram.
-        *
-        * @param other histogram to merge
-        */
+         * Merges given histogram with this histogram.
+         *
+         * @param other histogram to merge
+         */
         public void merge(StreamingHistogram other)
         {
             if (other == null)
                 return;
 
-            flushHistogram();
-
-            for (Map.Entry<Double, Long> entry : other.getAsMap().entrySet())
-                update(entry.getKey(), entry.getValue());
+            for (Map.Entry<Number, long[]> entry : other.getAsMap().entrySet())
+                update(entry.getKey(), entry.getValue()[0]);
         }
     }
 
@@ -234,12 +281,12 @@
         public void serialize(StreamingHistogram histogram, DataOutputPlus out) throws IOException
         {
             out.writeInt(histogram.maxBinSize);
-            Map<Double, Long> entries = histogram.getAsMap();
+            Map<Number, long[]> entries = histogram.getAsMap();
             out.writeInt(entries.size());
-            for (Map.Entry<Double, Long> entry : entries.entrySet())
+            for (Map.Entry<Number, long[]> entry : entries.entrySet())
             {
-                out.writeDouble(entry.getKey());
-                out.writeLong(entry.getValue());
+                out.writeDouble(entry.getKey().doubleValue());
+                out.writeLong(entry.getValue()[0]);
             }
         }
 
@@ -247,10 +294,10 @@
         {
             int maxBinSize = in.readInt();
             int size = in.readInt();
-            Map<Double, Long> tmp = new HashMap<>(size);
+            Map<Number, long[]> tmp = new HashMap<>(size);
             for (int i = 0; i < size; i++)
             {
-                tmp.put(in.readDouble(), in.readLong());
+                tmp.put(in.readDouble(), new long[]{in.readLong()});
             }
 
             return new StreamingHistogram(maxBinSize, tmp);
@@ -259,7 +306,7 @@
         public long serializedSize(StreamingHistogram histogram)
         {
             long size = TypeSizes.sizeof(histogram.maxBinSize);
-            Map<Double, Long> entries = histogram.getAsMap();
+            Map<Number, long[]> entries = histogram.getAsMap();
             size += TypeSizes.sizeof(entries.size());
             // size of entries = size * (8(double) + 8(long))
             size += entries.size() * (8L + 8L);
@@ -277,8 +324,8 @@
             return false;
 
         StreamingHistogram that = (StreamingHistogram) o;
-        return maxBinSize == that.maxBinSize
-               && bin.equals(that.bin);
+        return maxBinSize == that.maxBinSize &&
+               bin.equals(that.bin);
     }
 
     @Override
@@ -286,4 +333,5 @@
     {
         return Objects.hashCode(bin.hashCode(), maxBinSize);
     }
+
 }
diff --git a/src/java/org/apache/cassandra/utils/SystemTimeSource.java b/src/java/org/apache/cassandra/utils/SystemTimeSource.java
new file mode 100644
index 0000000..fef525e
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/SystemTimeSource.java
@@ -0,0 +1,54 @@
+/*
+ * 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.utils;
+
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+
+/**
+ * Time source backed by JVM clock.
+ */
+public class SystemTimeSource implements TimeSource
+{
+    @Override
+    public long currentTimeMillis()
+    {
+        return System.currentTimeMillis();
+    }
+
+    @Override
+    public long nanoTime()
+    {
+        return System.nanoTime();
+    }
+
+    @Override
+    public TimeSource sleepUninterruptibly(long sleepFor, TimeUnit unit)
+    {
+        Uninterruptibles.sleepUninterruptibly(sleepFor, unit);
+        return this;
+    }
+
+    @Override
+    public TimeSource sleep(long sleepFor, TimeUnit unit) throws InterruptedException
+    {
+        TimeUnit.NANOSECONDS.sleep(TimeUnit.NANOSECONDS.convert(sleepFor, unit));
+        return this;
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/TestRateLimiter.java b/src/java/org/apache/cassandra/utils/TestRateLimiter.java
new file mode 100644
index 0000000..a9eb871
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/TestRateLimiter.java
@@ -0,0 +1,58 @@
+/*
+ *
+ * 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.utils;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.RateLimiter;
+
+import org.jboss.byteman.rule.Rule;
+import org.jboss.byteman.rule.helper.Helper;
+
+/**
+ * Helper class to apply rate limiting during fault injection testing;
+ * for an example script, see test/resources/byteman/mutation_limiter.btm.
+ */
+@VisibleForTesting
+public class TestRateLimiter extends Helper
+{
+    private static final AtomicReference<RateLimiter> ref = new AtomicReference<>();
+
+    protected TestRateLimiter(Rule rule)
+    {
+        super(rule);
+    }
+
+    /**
+     * Acquires a single unit at the given rate. If the rate changes between calls, a new rate limiter is created
+     * and the old one is discarded.
+     */
+    public void acquire(double rate)
+    {
+        RateLimiter limiter = ref.get();
+        if (limiter == null || limiter.getRate() != rate)
+        {
+            ref.compareAndSet(limiter, RateLimiter.create(rate));
+            limiter = ref.get();
+        }
+        limiter.acquire(1);
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/TimeSource.java b/src/java/org/apache/cassandra/utils/TimeSource.java
new file mode 100644
index 0000000..5d8acec
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/TimeSource.java
@@ -0,0 +1,58 @@
+/*
+ * 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.utils;
+
+import java.util.concurrent.TimeUnit;
+
+public interface TimeSource
+{
+    /**
+     *
+     * @return the current time in milliseconds
+     */
+    long currentTimeMillis();
+
+    /**
+     *
+     * @return Returns the current time value in nanoseconds.
+     *
+     * <p>This method can only be used to measure elapsed time and is
+     * not related to any other notion of system or wall-clock time.
+     */
+    long nanoTime();
+
+    /**
+     * Sleep for the given amount of time uninterruptibly.
+     *
+     * @param  sleepFor given amout.
+     * @param  unit time unit
+     * @return The time source itself after the given sleep period.
+     */
+    TimeSource sleepUninterruptibly(long sleepFor, TimeUnit unit);
+
+    /**
+     * Sleep for the given amount of time. This operation could interrupted.
+     * Hence after returning from this method, it is not guaranteed
+     * that the request amount of time has passed.
+     *
+     * @param  sleepFor given amout.
+     * @param  unit time unit
+     * @return The time source itself after the given sleep period.
+     */
+    TimeSource sleep(long sleepFor, TimeUnit unit) throws InterruptedException;
+}
diff --git a/src/java/org/apache/cassandra/utils/UUIDGen.java b/src/java/org/apache/cassandra/utils/UUIDGen.java
index 11c1895..8fac816 100644
--- a/src/java/org/apache/cassandra/utils/UUIDGen.java
+++ b/src/java/org/apache/cassandra/utils/UUIDGen.java
@@ -25,13 +25,12 @@
 import java.util.Collection;
 import java.util.Random;
 import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.TimeUnit;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Charsets;
 import com.google.common.primitives.Ints;
 
-
 /**
  * The goods are here: www.ietf.org/rfc/rfc4122.txt.
  */
@@ -60,7 +59,7 @@
     // placement of this singleton is important.  It needs to be instantiated *AFTER* the other statics.
     private static final UUIDGen instance = new UUIDGen();
 
-    private long lastNanos;
+    private AtomicLong lastNanos = new AtomicLong();
 
     private UUIDGen()
     {
@@ -142,6 +141,15 @@
         return new UUID(raw.getLong(raw.position()), raw.getLong(raw.position() + 8));
     }
 
+    public static ByteBuffer toByteBuffer(UUID uuid)
+    {
+        ByteBuffer buffer = ByteBuffer.allocate(16);
+        buffer.putLong(uuid.getMostSignificantBits());
+        buffer.putLong(uuid.getLeastSignificantBits());
+        buffer.flip();
+        return buffer;
+    }
+
     /** decomposes a uuid into raw bytes. */
     public static byte[] decompose(UUID uuid)
     {
@@ -225,7 +233,8 @@
      * @param timestamp milliseconds since Unix epoch
      * @return
      */
-    private static long fromUnixTimestamp(long timestamp) {
+    private static long fromUnixTimestamp(long timestamp)
+    {
         return fromUnixTimestamp(timestamp, 0L);
     }
 
@@ -239,7 +248,7 @@
      * of a type 1 UUID (a time-based UUID).
      *
      * To specify a 100-nanoseconds precision timestamp, one should provide a milliseconds timestamp and
-     * a number 0 <= n < 10000 such that n*100 is the number of nanoseconds within that millisecond.
+     * a number {@code 0 <= n < 10000} such that n*100 is the number of nanoseconds within that millisecond.
      *
      * <p><i><b>Warning:</b> This method is not guaranteed to return unique UUIDs; Multiple
      * invocations using identical timestamps will result in identical UUIDs.</i></p>
@@ -294,15 +303,31 @@
 
     // needs to return two different values for the same when.
     // we can generate at most 10k UUIDs per ms.
-    private synchronized long createTimeSafe()
+    private long createTimeSafe()
     {
-        long nanosSince = (System.currentTimeMillis() - START_EPOCH) * 10000;
-        if (nanosSince > lastNanos)
-            lastNanos = nanosSince;
-        else
-            nanosSince = ++lastNanos;
-
-        return createTime(nanosSince);
+        long newLastNanos;
+        while (true)
+        {
+            //Generate a candidate value for new lastNanos
+            newLastNanos = (System.currentTimeMillis() - START_EPOCH) * 10000;
+            long originalLastNanos = lastNanos.get();
+            if (newLastNanos > originalLastNanos)
+            {
+                //Slow path once per millisecond do a CAS
+                if (lastNanos.compareAndSet(originalLastNanos, newLastNanos))
+                {
+                    break;
+                }
+            }
+            else
+            {
+                //Fast path do an atomic increment
+                //Or when falling behind this will move time forward past the clock if necessary
+                newLastNanos = lastNanos.incrementAndGet();
+                break;
+            }
+        }
+        return createTime(newLastNanos);
     }
 
     private long createTimeUnsafe(long when, int nanos)
diff --git a/src/java/org/apache/cassandra/utils/WindowsTimer.java b/src/java/org/apache/cassandra/utils/WindowsTimer.java
index f004979..726f9d0 100644
--- a/src/java/org/apache/cassandra/utils/WindowsTimer.java
+++ b/src/java/org/apache/cassandra/utils/WindowsTimer.java
@@ -64,7 +64,7 @@
         if (!available)
             return;
         if (timeBeginPeriod(period) != 0)
-            logger.warn("Failed to set timer to : " + period + ". Performance will be degraded.");
+            logger.warn("Failed to set timer to : {}. Performance will be degraded.", period);
     }
 
     public static void endTimerPeriod(int period)
@@ -75,6 +75,6 @@
         if (!available)
             return;
         if (timeEndPeriod(period) != 0)
-            logger.warn("Failed to end accelerated timer period. System timer will remain set to: " + period + " ms.");
+            logger.warn("Failed to end accelerated timer period. System timer will remain set to: {} ms.", period);
     }
 }
diff --git a/src/java/org/apache/cassandra/utils/Wrapped.java b/src/java/org/apache/cassandra/utils/Wrapped.java
new file mode 100644
index 0000000..1996a86
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/Wrapped.java
@@ -0,0 +1,48 @@
+/*
+ * 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.utils;
+
+/**
+ * Simple wrapper class to be used when a lambda function
+ * needs to modify a variable outside it's scope.
+ */
+public class Wrapped<T>
+{
+    private T value;
+
+    public static <V> Wrapped<V> create(V initial)
+    {
+        return new Wrapped<>(initial);
+    }
+
+    private Wrapped(T initial)
+    {
+        this.value = initial;
+    }
+
+    public T get()
+    {
+        return value;
+    }
+
+    public void set(T value)
+    {
+        this.value = value;
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/WrappedBoolean.java b/src/java/org/apache/cassandra/utils/WrappedBoolean.java
new file mode 100644
index 0000000..4b1443e
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/WrappedBoolean.java
@@ -0,0 +1,42 @@
+/*
+ * 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.utils;
+
+/**
+ * Simple wrapper for native boolean type
+ */
+public class WrappedBoolean
+{
+    private boolean value;
+
+    public WrappedBoolean(boolean initial)
+    {
+        this.value = initial;
+    }
+
+    public boolean get()
+    {
+        return value;
+    }
+
+    public void set(boolean value)
+    {
+        this.value = value;
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/WrappedException.java b/src/java/org/apache/cassandra/utils/WrappedException.java
new file mode 100644
index 0000000..3cf56bc
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/WrappedException.java
@@ -0,0 +1,30 @@
+/*
+ * 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.utils;
+
+/**
+ * Wrapped runtime exception for lambda functions
+ */
+public class WrappedException extends RuntimeException
+{
+    public WrappedException(Exception cause)
+    {
+        super(cause);
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/WrappedInt.java b/src/java/org/apache/cassandra/utils/WrappedInt.java
new file mode 100644
index 0000000..a106575
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/WrappedInt.java
@@ -0,0 +1,52 @@
+/*
+ * 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.utils;
+
+/**
+ * Simple wrapper for native int type
+ */
+public class WrappedInt
+{
+    private int value;
+
+    public WrappedInt(int initial)
+    {
+        this.value = initial;
+    }
+
+    public int get()
+    {
+        return value;
+    }
+
+    public void set(int value)
+    {
+        this.value = value;
+    }
+
+    public void increment()
+    {
+        ++value;
+    }
+
+    public void decrement()
+    {
+        --value;
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/btree/BTree.java b/src/java/org/apache/cassandra/utils/btree/BTree.java
index e6e6e40..c4dfc49 100644
--- a/src/java/org/apache/cassandra/utils/btree/BTree.java
+++ b/src/java/org/apache/cassandra/utils/btree/BTree.java
@@ -19,8 +19,11 @@
 package org.apache.cassandra.utils.btree;
 
 import java.util.*;
+import java.util.function.Consumer;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
+import com.google.common.base.Predicate;
 import com.google.common.collect.Iterators;
 import com.google.common.collect.Ordering;
 
@@ -67,6 +70,8 @@
     // NB we encode Path indexes as Bytes, so this needs to be less than Byte.MAX_VALUE / 2
     static final int FAN_FACTOR = 1 << FAN_SHIFT;
 
+    static final int MINIMAL_NODE_SIZE = FAN_FACTOR >> 1;
+
     // An empty BTree Leaf - which is the same as an empty BTree
     static final Object[] EMPTY_LEAF = new Object[1];
 
@@ -92,6 +97,11 @@
         return new Object[] { value };
     }
 
+    public static <C, K extends C, V extends C> Object[] build(K[] source, int size, UpdateFunction<K, V> updateF)
+    {
+        return buildInternal(source, size, updateF);
+    }
+
     public static <C, K extends C, V extends C> Object[] build(Collection<K> source, UpdateFunction<K, V> updateF)
     {
         return buildInternal(source, source.size(), updateF);
@@ -133,19 +143,45 @@
                 for (K k : source)
                     values[i++] = updateF.apply(k);
             }
-            updateF.allocated(ObjectSizes.sizeOfArray(values));
+            if (updateF != UpdateFunction.noOp())
+                updateF.allocated(ObjectSizes.sizeOfArray(values));
             return values;
         }
 
-        Queue<TreeBuilder> queue = modifier.get();
-        TreeBuilder builder = queue.poll();
-        if (builder == null)
-            builder = new TreeBuilder();
+        TreeBuilder builder = TreeBuilder.newInstance();
         Object[] btree = builder.build(source, updateF, size);
-        queue.add(builder);
+
         return btree;
     }
 
+    /**
+     * As build(), except:
+     * @param size    < 0 if size is unknown
+     */
+    private static <C, K extends C, V extends C> Object[] buildInternal(K[] source, int size, UpdateFunction<K, V> updateF)
+    {
+        if ((size >= 0) & (size < FAN_FACTOR))
+        {
+            if (size == 0)
+                return EMPTY_LEAF;
+            // pad to odd length to match contract that all leaf nodes are odd
+            V[] values = (V[]) new Object[size | 1];
+            {
+                for (int i = 0; i < size; i++)
+                    values[i] = updateF.apply(source[i]);
+            }
+            if (updateF != UpdateFunction.noOp())
+                updateF.allocated(ObjectSizes.sizeOfArray(values));
+            return values;
+        }
+
+        TreeBuilder builder = TreeBuilder.newInstance();
+        Object[] btree = builder.build(source, updateF, size);
+
+        return btree;
+    }
+
+
     public static <C, K extends C, V extends C> Object[] update(Object[] btree,
                                                                 Comparator<C> comparator,
                                                                 Collection<K> updateWith,
@@ -174,12 +210,9 @@
         if (isEmpty(btree))
             return build(updateWith, updateWithLength, updateF);
 
-        Queue<TreeBuilder> queue = modifier.get();
-        TreeBuilder builder = queue.poll();
-        if (builder == null)
-            builder = new TreeBuilder();
+
+        TreeBuilder builder = TreeBuilder.newInstance();
         btree = builder.update(btree, comparator, updateWith, updateF);
-        queue.add(builder);
         return btree;
     }
 
@@ -191,7 +224,7 @@
             tree1 = tree2;
             tree2 = tmp;
         }
-        return update(tree1, comparator, new BTreeSet<K>(tree2, comparator), updateF);
+        return update(tree1, comparator, new BTreeSet<>(tree2, comparator), updateF);
     }
 
     public static <V> Iterator<V> iterator(Object[] btree)
@@ -201,12 +234,12 @@
 
     public static <V> Iterator<V> iterator(Object[] btree, Dir dir)
     {
-        return new BTreeSearchIterator<V, V>(btree, null, dir);
+        return new BTreeSearchIterator<>(btree, null, dir);
     }
 
     public static <V> Iterator<V> iterator(Object[] btree, int lb, int ub, Dir dir)
     {
-        return new BTreeSearchIterator<V, V>(btree, null, dir, lb, ub);
+        return new BTreeSearchIterator<>(btree, null, dir, lb, ub);
     }
 
     public static <V> Iterable<V> iterable(Object[] btree)
@@ -296,6 +329,40 @@
 
     /**
      * Modifies the provided btree directly. THIS SHOULD NOT BE USED WITHOUT EXTREME CARE as BTrees are meant to be immutable.
+     * Finds and replaces the item provided by index in the tree.
+     */
+    public static <V> void replaceInSitu(Object[] tree, int index, V replace)
+    {
+        // WARNING: if semantics change, see also InternalCursor.seekTo, which mirrors this implementation
+        if ((index < 0) | (index >= size(tree)))
+            throw new IndexOutOfBoundsException(index + " not in range [0.." + size(tree) + ")");
+
+        while (!isLeaf(tree))
+        {
+            final int[] sizeMap = getSizeMap(tree);
+            int boundary = Arrays.binarySearch(sizeMap, index);
+            if (boundary >= 0)
+            {
+                // exact match, in this branch node
+                assert boundary < sizeMap.length - 1;
+                tree[boundary] = replace;
+                return;
+            }
+
+            boundary = -1 -boundary;
+            if (boundary > 0)
+            {
+                assert boundary < sizeMap.length;
+                index -= (1 + sizeMap[boundary - 1]);
+            }
+            tree = (Object[]) tree[getChildStart(tree) + boundary];
+        }
+        assert index < getLeafKeyEnd(tree);
+        tree[index] = replace;
+    }
+
+    /**
+     * Modifies the provided btree directly. THIS SHOULD NOT BE USED WITHOUT EXTREME CARE as BTrees are meant to be immutable.
      * Finds and replaces the provided item in the tree. Both should sort as equal to each other (although this is not enforced)
      */
     public static <V> void replaceInSitu(Object[] node, Comparator<? super V> comparator, V find, V replace)
@@ -735,15 +802,6 @@
         return 1 + lookupSizeMap(root, childIndex - 1);
     }
 
-    private static final ThreadLocal<Queue<TreeBuilder>> modifier = new ThreadLocal<Queue<TreeBuilder>>()
-    {
-        @Override
-        protected Queue<TreeBuilder> initialValue()
-        {
-            return new ArrayDeque<>();
-        }
-    };
-
     public static <V> Builder<V> builder(Comparator<? super V> comparator)
     {
         return new Builder<>(comparator);
@@ -751,12 +809,11 @@
 
     public static <V> Builder<V> builder(Comparator<? super V> comparator, int initialCapacity)
     {
-        return new Builder<>(comparator);
+        return new Builder<>(comparator, initialCapacity);
     }
 
     public static class Builder<V>
     {
-
         // a user-defined bulk resolution, to be applied manually via resolve()
         public static interface Resolver
         {
@@ -789,10 +846,18 @@
 
         protected Builder(Comparator<? super V> comparator, int initialCapacity)
         {
+            if (initialCapacity == 0)
+                initialCapacity = 16;
             this.comparator = comparator;
             this.values = new Object[initialCapacity];
         }
 
+        @VisibleForTesting
+        public Builder()
+        {
+            this.values = new Object[16];
+        }
+
         private Builder(Builder<V> builder)
         {
             this.comparator = builder.comparator;
@@ -826,6 +891,7 @@
         public void reuse(Comparator<? super V> comparator)
         {
             this.comparator = comparator;
+            Arrays.fill(values, null);
             count = 0;
             detected = true;
         }
@@ -1056,7 +1122,7 @@
         {
             if (auto)
                 autoEnforce();
-            return BTree.build(Arrays.asList(values).subList(0, count), UpdateFunction.noOp());
+            return BTree.build(values, count, UpdateFunction.noOp());
         }
     }
 
@@ -1092,11 +1158,20 @@
             return node.length >= FAN_FACTOR / 2 && node.length <= FAN_FACTOR + 1;
         }
 
+        final int keyCount = getBranchKeyEnd(node);
+        if ((!isRoot && keyCount < FAN_FACTOR / 2) || keyCount > FAN_FACTOR + 1)
+            return false;
+
         int type = 0;
+        int size = -1;
+        int[] sizeMap = getSizeMap(node);
         // compare each child node with the branch element at the head of this node it corresponds with
         for (int i = getChildStart(node); i < getChildEnd(node) ; i++)
         {
             Object[] child = (Object[]) node[i];
+            size += size(child) + 1;
+            if (sizeMap[i - getChildStart(node)] != size)
+                return false;
             Object localmax = i < node.length - 2 ? node[i - getChildStart(node)] : max;
             if (!isWellFormed(cmp, child, false, min, localmax))
                 return false;
@@ -1119,4 +1194,128 @@
         }
         return compare(cmp, previous, max) < 0;
     }
+
+    /**
+     * Simple method to walk the btree forwards or reversed and apply a function to each element
+     *
+     * Public method
+     *
+     */
+    public static <V> void apply(Object[] btree, Consumer<V> function, boolean reversed)
+    {
+        if (reversed)
+            applyReverse(btree, function, null);
+        else
+            applyForwards(btree, function, null);
+    }
+
+    /**
+     * Simple method to walk the btree forwards or reversed and apply a function till a stop condition is reached
+     *
+     * Public method
+     *
+     */
+    public static <V> void apply(Object[] btree, Consumer<V> function, Predicate<V> stopCondition, boolean reversed)
+    {
+        if (reversed)
+            applyReverse(btree, function, stopCondition);
+        else
+            applyForwards(btree, function, stopCondition);
+    }
+
+
+
+
+    /**
+     * Simple method to walk the btree forwards and apply a function till a stop condition is reached
+     *
+     * Private method
+     *
+     * @param btree
+     * @param function
+     * @param stopCondition
+     */
+    private static <V> boolean applyForwards(Object[] btree, Consumer<V> function, Predicate<V> stopCondition)
+    {
+        boolean isLeaf = isLeaf(btree);
+        int childOffset = isLeaf ? Integer.MAX_VALUE : getChildStart(btree);
+        int limit = isLeaf ? getLeafKeyEnd(btree) : btree.length - 1;
+        for (int i = 0 ; i < limit ; i++)
+        {
+            // we want to visit in iteration order, so we visit our key nodes inbetween our children
+            int idx = isLeaf ? i : (i / 2) + (i % 2 == 0 ? childOffset : 0);
+            Object current = btree[idx];
+            if (idx < childOffset)
+            {
+                V castedCurrent = (V) current;
+                if (stopCondition != null && stopCondition.apply(castedCurrent))
+                    return true;
+
+                function.accept(castedCurrent);
+            }
+            else
+            {
+                if (applyForwards((Object[]) current, function, stopCondition))
+                    return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Simple method to walk the btree in reverse and apply a function till a stop condition is reached
+     *
+     * Private method
+     *
+     * @param btree
+     * @param function
+     * @param stopCondition
+     */
+    private static <V> boolean applyReverse(Object[] btree, Consumer<V> function, Predicate<V> stopCondition)
+    {
+        boolean isLeaf = isLeaf(btree);
+        int childOffset = isLeaf ? 0 : getChildStart(btree);
+        int limit = isLeaf ? getLeafKeyEnd(btree)  : btree.length - 1;
+        for (int i = limit - 1, visited = 0; i >= 0 ; i--, visited++)
+        {
+            int idx = i;
+
+            // we want to visit in reverse iteration order, so we visit our children nodes inbetween our keys
+            if (!isLeaf)
+            {
+                int typeOffset = visited / 2;
+
+                if (i % 2 == 0)
+                {
+                    // This is a child branch. Since children are in the second half of the array, we must
+                    // adjust for the key's we've visited along the way
+                    idx += typeOffset;
+                }
+                else
+                {
+                    // This is a key. Since the keys are in the first half of the array and we are iterating
+                    // in reverse we subtract the childOffset and adjust for children we've walked so far
+                    idx = i - childOffset + typeOffset;
+                }
+            }
+
+            Object current = btree[idx];
+            if (isLeaf || idx < childOffset)
+            {
+                V castedCurrent = (V) current;
+                if (stopCondition != null && stopCondition.apply(castedCurrent))
+                    return true;
+
+                function.accept(castedCurrent);
+            }
+            else
+            {
+                if (applyReverse((Object[]) current, function, stopCondition))
+                    return true;
+            }
+        }
+
+        return false;
+    }
 }
diff --git a/src/java/org/apache/cassandra/utils/btree/BTreeRemoval.java b/src/java/org/apache/cassandra/utils/btree/BTreeRemoval.java
new file mode 100644
index 0000000..f4d9cd4
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/btree/BTreeRemoval.java
@@ -0,0 +1,345 @@
+/*
+ * 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.utils.btree;
+
+import java.util.Arrays;
+import java.util.Comparator;
+
+public class BTreeRemoval
+{
+    /**
+     * Remove |elem| from |btree|. If it's not present then return |btree| itself.
+     */
+    public static <V> Object[] remove(final Object[] btree, final Comparator<? super V> comparator, final V elem)
+    {
+        if (BTree.isEmpty(btree))
+            return btree;
+        int index = -1;
+        V elemToSwap = null;
+        int lb = 0;
+        Object[] node = btree;
+        while (true)
+        {
+            int keyEnd = BTree.getKeyEnd(node);
+            int i = Arrays.binarySearch((V[]) node, 0, keyEnd, elem, comparator);
+
+            if (i >= 0)
+            {
+                if (BTree.isLeaf(node))
+                    index = lb + i;
+                else
+                {
+                    final int indexInNode = BTree.getSizeMap(node)[i];
+                    index = lb + indexInNode - 1;
+                    elemToSwap = BTree.findByIndex(node, indexInNode - 1);
+                }
+                break;
+            }
+            if (BTree.isLeaf(node))
+                return btree;
+
+            i = -1 - i;
+            if (i > 0)
+                lb += BTree.getSizeMap(node)[i - 1] + 1;
+
+            node = (Object[]) node[keyEnd + i];
+        }
+        if (BTree.size(btree) == 1)
+            return BTree.empty();
+        Object[] result = removeFromLeaf(btree, index);
+        if (elemToSwap != null)
+            BTree.replaceInSitu(result, index, elemToSwap);
+        return result;
+    }
+
+    /**
+     * Remove |elem| from |btree|. It has to be present and it has to reside in a leaf node.
+     */
+    private static Object[] removeFromLeaf(Object[] node, int index)
+    {
+        Object[] result = null;
+        Object[] prevNode = null;
+        int prevI = -1;
+        boolean needsCopy = true;
+        while (!BTree.isLeaf(node))
+        {
+            final int keyEnd = BTree.getBranchKeyEnd(node);
+            int i = -1 - Arrays.binarySearch(BTree.getSizeMap(node), index);
+            if (i > 0)
+                index -= (1 + BTree.getSizeMap(node)[i - 1]);
+            Object[] nextNode = (Object[]) node[keyEnd + i];
+            boolean nextNodeNeedsCopy = true;
+            if (BTree.getKeyEnd(nextNode) > BTree.MINIMAL_NODE_SIZE)
+                node = copyIfNeeded(node, needsCopy);
+            else if (i > 0 && BTree.getKeyEnd((Object[]) node[keyEnd + i - 1]) > BTree.MINIMAL_NODE_SIZE)
+            {
+                node = copyIfNeeded(node, needsCopy);
+                final Object[] leftNeighbour = (Object[]) node[keyEnd + i - 1];
+                index++;
+                if (!BTree.isLeaf(leftNeighbour))
+                    index += BTree.size((Object[])leftNeighbour[BTree.getChildEnd(leftNeighbour) - 1]);
+                nextNode = rotateLeft(node, i);
+            }
+            else if (i < keyEnd && BTree.getKeyEnd((Object[]) node[keyEnd + i + 1]) > BTree.MINIMAL_NODE_SIZE)
+            {
+                node = copyIfNeeded(node, needsCopy);
+                nextNode = rotateRight(node, i);
+            }
+            else
+            {
+                nextNodeNeedsCopy = false;
+                if (i > 0)
+                {
+                    final Object[] leftNeighbour = (Object[]) node[keyEnd + i - 1];
+                    final Object nodeKey = node[i - 1];
+                    node = keyEnd == 1 ? null : copyWithKeyAndChildRemoved(node, i - 1, i - 1, false);
+                    nextNode = merge(leftNeighbour, nextNode, nodeKey);
+                    i = i - 1;
+                    index += BTree.size(leftNeighbour) + 1;
+                }
+                else
+                {
+                    final Object[] rightNeighbour = (Object[]) node[keyEnd + i + 1];
+                    final Object nodeKey = node[i];
+                    node = keyEnd == 1 ? null : copyWithKeyAndChildRemoved(node, i, i, false);
+                    nextNode = merge(nextNode, rightNeighbour, nodeKey);
+                }
+            }
+
+            if (node != null)
+            {
+                final int[] sizeMap = BTree.getSizeMap(node);
+                for (int j = i; j < sizeMap.length; ++j)
+                    sizeMap[j] -= 1;
+                if (prevNode != null)
+                    prevNode[prevI] = node;
+                else
+                    result = node;
+                prevNode = node;
+                prevI = BTree.getChildStart(node) + i;
+            }
+
+            node = nextNode;
+            needsCopy = nextNodeNeedsCopy;
+        }
+        final int keyEnd = BTree.getLeafKeyEnd(node);
+        final Object[] newLeaf = new Object[(keyEnd & 1) == 1 ? keyEnd : keyEnd - 1];
+        copyKeys(node, newLeaf, 0, index);
+        if (prevNode != null)
+            prevNode[prevI] = newLeaf;
+        else
+            result = newLeaf;
+        return result;
+    }
+
+    private static Object[] rotateRight(final Object[] node, final int i)
+    {
+        final int keyEnd = BTree.getBranchKeyEnd(node);
+        final Object[] nextNode = (Object[]) node[keyEnd + i];
+        final Object[] rightNeighbour = (Object[]) node[keyEnd + i + 1];
+        final boolean leaves = BTree.isLeaf(nextNode);
+        final int nextKeyEnd = BTree.getKeyEnd(nextNode);
+        final Object[] newChild = leaves ? null : (Object[]) rightNeighbour[BTree.getChildStart(rightNeighbour)];
+        final Object[] newNextNode =
+                copyWithKeyAndChildInserted(nextNode, nextKeyEnd, node[i], BTree.getChildCount(nextNode), newChild);
+        node[i] = rightNeighbour[0];
+        node[keyEnd + i + 1] = copyWithKeyAndChildRemoved(rightNeighbour, 0, 0, true);
+        BTree.getSizeMap(node)[i] +=
+                leaves ? 1 : 1 + BTree.size((Object[]) newNextNode[BTree.getChildEnd(newNextNode) - 1]);
+        return newNextNode;
+    }
+
+    private static Object[] rotateLeft(final Object[] node, final int i)
+    {
+        final int keyEnd = BTree.getBranchKeyEnd(node);
+        final Object[] nextNode = (Object[]) node[keyEnd + i];
+        final Object[] leftNeighbour = (Object[]) node[keyEnd + i - 1];
+        final int leftNeighbourEndKey = BTree.getKeyEnd(leftNeighbour);
+        final boolean leaves = BTree.isLeaf(nextNode);
+        final Object[] newChild = leaves ? null : (Object[]) leftNeighbour[BTree.getChildEnd(leftNeighbour) - 1];
+        final Object[] newNextNode = copyWithKeyAndChildInserted(nextNode, 0, node[i - 1], 0, newChild);
+        node[i - 1] = leftNeighbour[leftNeighbourEndKey - 1];
+        node[keyEnd + i - 1] = copyWithKeyAndChildRemoved(leftNeighbour, leftNeighbourEndKey - 1, leftNeighbourEndKey, true);
+        BTree.getSizeMap(node)[i - 1] -= leaves ? 1 : 1 + BTree.getSizeMap(newNextNode)[0];
+        return newNextNode;
+    }
+
+    private static <V> Object[] copyWithKeyAndChildInserted(final Object[] node, final int keyIndex, final V key, final int childIndex, final Object[] child)
+    {
+        final boolean leaf = BTree.isLeaf(node);
+        final int keyEnd = BTree.getKeyEnd(node);
+        final Object[] copy;
+        if (leaf)
+            copy = new Object[keyEnd + ((keyEnd & 1) == 1 ? 2 : 1)];
+        else
+            copy = new Object[node.length + 2];
+
+        if (keyIndex > 0)
+            System.arraycopy(node, 0, copy, 0, keyIndex);
+        copy[keyIndex] = key;
+        if (keyIndex < keyEnd)
+            System.arraycopy(node, keyIndex, copy, keyIndex + 1, keyEnd - keyIndex);
+
+        if (!leaf)
+        {
+            if (childIndex > 0)
+                System.arraycopy(node,
+                                 BTree.getChildStart(node),
+                                 copy,
+                                 keyEnd + 1,
+                                 childIndex);
+            copy[keyEnd + 1 + childIndex] = child;
+            if (childIndex <= keyEnd)
+                System.arraycopy(node,
+                                 BTree.getChildStart(node) + childIndex,
+                                 copy,
+                                 keyEnd + childIndex + 2,
+                                 keyEnd - childIndex + 1);
+            final int[] sizeMap = BTree.getSizeMap(node);
+            final int[] newSizeMap = new int[sizeMap.length + 1];
+            if (childIndex > 0)
+                System.arraycopy(sizeMap, 0, newSizeMap, 0, childIndex);
+            final int childSize = BTree.size(child);
+            newSizeMap[childIndex] = childSize + ((childIndex == 0) ? 0 : newSizeMap[childIndex - 1] + 1);
+            for (int i = childIndex + 1; i < newSizeMap.length; ++i)
+                newSizeMap[i] = sizeMap[i - 1] + childSize + 1;
+            copy[copy.length - 1] = newSizeMap;
+        }
+        return copy;
+    }
+
+    private static Object[] copyWithKeyAndChildRemoved(final Object[] node, final int keyIndex, final int childIndex, final boolean substractSize)
+    {
+        final boolean leaf = BTree.isLeaf(node);
+        final Object[] newNode;
+        if (leaf)
+        {
+            final int keyEnd = BTree.getKeyEnd(node);
+            newNode = new Object[keyEnd - ((keyEnd & 1) == 1 ? 0 : 1)];
+        }
+        else
+        {
+            newNode = new Object[node.length - 2];
+        }
+        int offset = copyKeys(node, newNode, 0, keyIndex);
+        if (!leaf)
+        {
+            offset = copyChildren(node, newNode, offset, childIndex);
+            final int[] nodeSizeMap = BTree.getSizeMap(node);
+            final int[] newNodeSizeMap = new int[nodeSizeMap.length - 1];
+            int pos = 0;
+            final int sizeToRemove = BTree.size((Object[])node[BTree.getChildStart(node) + childIndex]) + 1;
+            for (int i = 0; i < nodeSizeMap.length; ++i)
+                if (i != childIndex)
+                    newNodeSizeMap[pos++] = nodeSizeMap[i] -
+                        ((substractSize && i > childIndex) ? sizeToRemove : 0);
+            newNode[offset] = newNodeSizeMap;
+        }
+        return newNode;
+    }
+
+    private static <V> Object[] merge(final Object[] left, final Object[] right, final V nodeKey)
+    {
+        assert BTree.getKeyEnd(left) == BTree.MINIMAL_NODE_SIZE;
+        assert BTree.getKeyEnd(right) == BTree.MINIMAL_NODE_SIZE;
+        final boolean leaves = BTree.isLeaf(left);
+        final Object[] result;
+        if (leaves)
+            result = new Object[BTree.MINIMAL_NODE_SIZE * 2 + 1];
+        else
+            result = new Object[left.length + right.length];
+        int offset = 0;
+        offset = copyKeys(left, result, offset);
+        result[offset++] = nodeKey;
+        offset = copyKeys(right, result, offset);
+        if (!leaves)
+        {
+            offset = copyChildren(left, result, offset);
+            offset = copyChildren(right, result, offset);
+            final int[] leftSizeMap = BTree.getSizeMap(left);
+            final int[] rightSizeMap = BTree.getSizeMap(right);
+            final int[] newSizeMap = new int[leftSizeMap.length + rightSizeMap.length];
+            offset = 0;
+            offset = copySizeMap(leftSizeMap, newSizeMap, offset, 0);
+            offset = copySizeMap(rightSizeMap, newSizeMap, offset, leftSizeMap[leftSizeMap.length - 1] + 1);
+            result[result.length - 1] = newSizeMap;
+        }
+        return result;
+    }
+
+    private static int copyKeys(final Object[] from, final Object[] to, final int offset)
+    {
+        final int keysCount = BTree.getKeyEnd(from);
+        System.arraycopy(from, 0, to, offset, keysCount);
+        return offset + keysCount;
+    }
+
+    private static int copyKeys(final Object[] from, final Object[] to, final int offset, final int skipIndex)
+    {
+        final int keysCount = BTree.getKeyEnd(from);
+        if (skipIndex > 0)
+            System.arraycopy(from, 0, to, offset, skipIndex);
+        if (skipIndex + 1 < keysCount)
+            System.arraycopy(from, skipIndex + 1, to, offset + skipIndex, keysCount - skipIndex - 1);
+        return offset + keysCount - 1;
+    }
+
+    private static int copyChildren(final Object[] from, final Object[] to, final int offset)
+    {
+        assert !BTree.isLeaf(from);
+        final int start = BTree.getChildStart(from);
+        final int childCount = BTree.getChildCount(from);
+        System.arraycopy(from, start, to, offset, childCount);
+        return offset + childCount;
+    }
+
+    private static int copyChildren(final Object[] from, final Object[] to, final int offset, final int skipIndex)
+    {
+        assert !BTree.isLeaf(from);
+        final int start = BTree.getChildStart(from);
+        final int childCount = BTree.getChildCount(from);
+        if (skipIndex > 0)
+            System.arraycopy(from, start, to, offset, skipIndex);
+        if (skipIndex + 1 <= childCount)
+            System.arraycopy(from, start + skipIndex + 1, to, offset + skipIndex, childCount - skipIndex - 1);
+        return offset + childCount - 1;
+    }
+
+    private static int copySizeMap(final int[] from, final int[] to, final int offset, final int extra)
+    {
+        for (int i = 0; i < from.length; ++i)
+            to[offset + i] = from[i] + extra;
+        return offset + from.length;
+    }
+
+    private static Object[] copyIfNeeded(final Object[] node, boolean needCopy)
+    {
+        if (!needCopy) return node;
+        final Object[] copy = new Object[node.length];
+        System.arraycopy(node, 0, copy, 0, node.length);
+        if (!BTree.isLeaf(node))
+        {
+            final int[] sizeMap = BTree.getSizeMap(node);
+            final int[] copySizeMap = new int[sizeMap.length];
+            System.arraycopy(sizeMap, 0, copySizeMap, 0, sizeMap.length);
+            copy[copy.length - 1] = copySizeMap;
+        }
+        return copy;
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/btree/BTreeSet.java b/src/java/org/apache/cassandra/utils/btree/BTreeSet.java
index 03fa1ec..a59e481 100644
--- a/src/java/org/apache/cassandra/utils/btree/BTreeSet.java
+++ b/src/java/org/apache/cassandra/utils/btree/BTreeSet.java
@@ -21,14 +21,11 @@
 import java.util.*;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Ordering;
 
 import org.apache.cassandra.utils.btree.BTree.Dir;
 
 import static org.apache.cassandra.utils.btree.BTree.findIndex;
-import static org.apache.cassandra.utils.btree.BTree.lower;
-import static org.apache.cassandra.utils.btree.BTree.toArray;
 
 public class BTreeSet<V> implements NavigableSet<V>, List<V>
 {
@@ -639,6 +636,6 @@
 
     public static <V> BTreeSet<V> of(Comparator<? super V> comparator, V value)
     {
-        return new BTreeSet<>(BTree.build(ImmutableList.of(value), UpdateFunction.<V>noOp()), comparator);
+        return new BTreeSet<>(BTree.singleton(value), comparator);
     }
 }
diff --git a/src/java/org/apache/cassandra/utils/btree/TreeBuilder.java b/src/java/org/apache/cassandra/utils/btree/TreeBuilder.java
index 024902e..792d35a 100644
--- a/src/java/org/apache/cassandra/utils/btree/TreeBuilder.java
+++ b/src/java/org/apache/cassandra/utils/btree/TreeBuilder.java
@@ -20,6 +20,8 @@
 
 import java.util.Comparator;
 
+import io.netty.util.Recycler;
+
 import static org.apache.cassandra.utils.btree.BTree.EMPTY_LEAF;
 import static org.apache.cassandra.utils.btree.BTree.FAN_SHIFT;
 import static org.apache.cassandra.utils.btree.BTree.POSITIVE_INFINITY;
@@ -28,12 +30,32 @@
  * A class for constructing a new BTree, either from an existing one and some set of modifications
  * or a new tree from a sorted collection of items.
  * <p/>
- * This is a fairly heavy-weight object, so a ThreadLocal instance is created for making modifications to a tree
+ * This is a fairly heavy-weight object, so a Recycled instance is created for making modifications to a tree
  */
 final class TreeBuilder
 {
+
+    private final static Recycler<TreeBuilder> builderRecycler = new Recycler<TreeBuilder>()
+    {
+        protected TreeBuilder newObject(Handle handle)
+        {
+            return new TreeBuilder(handle);
+        }
+    };
+
+    public static TreeBuilder newInstance()
+    {
+        return builderRecycler.get();
+    }
+
+    private final Recycler.Handle recycleHandle;
     private final NodeBuilder rootBuilder = new NodeBuilder();
 
+    private TreeBuilder(Recycler.Handle handle)
+    {
+        this.recycleHandle = handle;
+    }
+
     /**
      * At the highest level, we adhere to the classic b-tree insertion algorithm:
      *
@@ -93,6 +115,9 @@
 
         Object[] r = current.toNode();
         current.clear();
+
+        builderRecycler.recycle(this, recycleHandle);
+
         return r;
     }
 
@@ -114,6 +139,35 @@
 
         Object[] r = current.toNode();
         current.clear();
+
+        builderRecycler.recycle(this, recycleHandle);
+
+        return r;
+    }
+
+    public <C, K extends C, V extends C> Object[] build(K [] source, UpdateFunction<K, V> updateF, int size)
+    {
+        assert updateF != null;
+
+        int origSize = size;
+
+        NodeBuilder current = rootBuilder;
+        // we descend only to avoid wasting memory; in update() we will often descend into existing trees
+        // so here we want to descend also, so we don't have lg max(N) depth in both directions
+        while ((size >>= FAN_SHIFT) > 0)
+            current = current.ensureChild();
+
+        current.reset(EMPTY_LEAF, POSITIVE_INFINITY, updateF, null);
+        for (int i = 0; i < origSize; i++)
+            current.addNewKey(source[i]);
+
+        current = current.ascendToRoot();
+
+        Object[] r = current.toNode();
+        current.clear();
+
+        builderRecycler.recycle(this, recycleHandle);
+
         return r;
     }
 }
diff --git a/src/java/org/apache/cassandra/utils/btree/TreeCursor.java b/src/java/org/apache/cassandra/utils/btree/TreeCursor.java
index 5e55698..60c0eb9 100644
--- a/src/java/org/apache/cassandra/utils/btree/TreeCursor.java
+++ b/src/java/org/apache/cassandra/utils/btree/TreeCursor.java
@@ -219,8 +219,7 @@
             return;
         }
 
-        NodeCursor<K> cur = this.cur;
-        cur = root();
+        NodeCursor<K> cur = root();
         assert cur.nodeOffset == 0;
         while (true)
         {
diff --git a/src/java/org/apache/cassandra/utils/concurrent/IntervalLock.java b/src/java/org/apache/cassandra/utils/concurrent/IntervalLock.java
new file mode 100644
index 0000000..382a2dc
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/concurrent/IntervalLock.java
@@ -0,0 +1,69 @@
+/*
+ * 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.utils.concurrent;
+
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.cassandra.utils.TimeSource;
+
+/**
+ * This class extends ReentrantReadWriteLock to provide a write lock that can only be acquired at provided intervals.
+ */
+public class IntervalLock extends ReentrantReadWriteLock
+{
+    private final AtomicLong lastAcquire = new AtomicLong();
+    private final TimeSource timeSource;
+
+    public IntervalLock(TimeSource timeSource)
+    {
+        this.timeSource = timeSource;
+    }
+
+    /**
+     * Try acquiring a write lock if the given interval is passed since the last call to this method.
+     *
+     * @param interval In millis.
+     * @return True if acquired and locked, false otherwise.
+     */
+    public boolean tryIntervalLock(long interval)
+    {
+        long now = timeSource.currentTimeMillis();
+        boolean acquired = (now - lastAcquire.get() >= interval) && writeLock().tryLock();
+        if (acquired)
+            lastAcquire.set(now);
+
+        return acquired;
+    }
+
+    /**
+     * Release the last acquired interval lock.
+     */
+    public void releaseIntervalLock()
+    {
+        writeLock().unlock();
+    }
+
+    @VisibleForTesting
+    public long getLastIntervalAcquire()
+    {
+        return lastAcquire.get();
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/concurrent/OpOrder.java b/src/java/org/apache/cassandra/utils/concurrent/OpOrder.java
index 497eec3..863f038 100644
--- a/src/java/org/apache/cassandra/utils/concurrent/OpOrder.java
+++ b/src/java/org/apache/cassandra/utils/concurrent/OpOrder.java
@@ -432,7 +432,7 @@
         }
 
         /**
-         * returns the Group we are waiting on - any Group with .compareTo(getSyncPoint()) <= 0
+         * returns the Group we are waiting on - any Group with {@code .compareTo(getSyncPoint()) <= 0}
          * must complete before await() returns
          */
         public Group getSyncPoint()
diff --git a/src/java/org/apache/cassandra/utils/concurrent/Ref.java b/src/java/org/apache/cassandra/utils/concurrent/Ref.java
index 933c498..5b6c3d6 100644
--- a/src/java/org/apache/cassandra/utils/concurrent/Ref.java
+++ b/src/java/org/apache/cassandra/utils/concurrent/Ref.java
@@ -73,13 +73,14 @@
  * This class' functionality is achieved by what may look at first glance like a complex web of references,
  * but boils down to:
  *
+ * {@code
  * Target --> selfRef --> [Ref.State] <--> Ref.GlobalState --> Tidy
  *                                             ^
  *                                             |
  * Ref ----------------------------------------
  *                                             |
  * Global -------------------------------------
- *
+ * }
  * So that, if Target is collected, Impl is collected and, hence, so is selfRef.
  *
  * Once ref or selfRef are collected, the paired Ref.State's release method is called, which if it had
@@ -345,7 +346,8 @@
         }
     }
 
-    private static final Class<?>[] concurrentIterableClasses = new Class<?>[] {
+    private static final Class<?>[] concurrentIterableClasses = new Class<?>[]
+    {
         ConcurrentLinkedQueue.class,
         ConcurrentLinkedDeque.class,
         ConcurrentSkipListSet.class,
diff --git a/src/java/org/apache/cassandra/utils/concurrent/RefCounted.java b/src/java/org/apache/cassandra/utils/concurrent/RefCounted.java
index e68c7bd..d09d288 100644
--- a/src/java/org/apache/cassandra/utils/concurrent/RefCounted.java
+++ b/src/java/org/apache/cassandra/utils/concurrent/RefCounted.java
@@ -18,23 +18,6 @@
 */
 package org.apache.cassandra.utils.concurrent;
 
-import java.lang.ref.PhantomReference;
-import java.lang.ref.ReferenceQueue;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterators;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.cassandra.concurrent.NamedThreadFactory;
-
 /**
  * An object that needs ref counting does the two following:
  *   - defines a Tidy object that will cleanup once it's gone,
diff --git a/src/java/org/apache/cassandra/utils/concurrent/Refs.java b/src/java/org/apache/cassandra/utils/concurrent/Refs.java
index ec8d6c0..e5d9c37 100644
--- a/src/java/org/apache/cassandra/utils/concurrent/Refs.java
+++ b/src/java/org/apache/cassandra/utils/concurrent/Refs.java
@@ -25,7 +25,6 @@
 import javax.annotation.Nullable;
 
 import com.google.common.base.Function;
-import com.google.common.base.Throwables;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Iterators;
 
diff --git a/src/java/org/apache/cassandra/utils/concurrent/WaitQueue.java b/src/java/org/apache/cassandra/utils/concurrent/WaitQueue.java
index be271b6..5b453b0 100644
--- a/src/java/org/apache/cassandra/utils/concurrent/WaitQueue.java
+++ b/src/java/org/apache/cassandra/utils/concurrent/WaitQueue.java
@@ -22,6 +22,7 @@
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 import java.util.concurrent.locks.LockSupport;
+import java.util.function.BooleanSupplier;
 
 import com.codahale.metrics.Timer;
 
@@ -518,4 +519,23 @@
     {
         return new AllSignal(signals);
     }
+
+    /**
+     * Loops waiting on the supplied condition and WaitQueue and will not return until the condition is true
+     */
+    public static void waitOnCondition(BooleanSupplier condition, WaitQueue queue)
+    {
+        while (!condition.getAsBoolean())
+        {
+            Signal s = queue.register();
+            if (!condition.getAsBoolean())
+            {
+                s.awaitUninterruptibly();
+            }
+            else
+            {
+                s.cancel();
+            }
+        }
+    }
 }
diff --git a/src/java/org/apache/cassandra/utils/concurrent/WrappedSharedCloseable.java b/src/java/org/apache/cassandra/utils/concurrent/WrappedSharedCloseable.java
index 0eefae3..31894b1 100644
--- a/src/java/org/apache/cassandra/utils/concurrent/WrappedSharedCloseable.java
+++ b/src/java/org/apache/cassandra/utils/concurrent/WrappedSharedCloseable.java
@@ -20,8 +20,6 @@
 
 import java.util.Arrays;
 
-import org.apache.cassandra.utils.Throwables;
-
 import static org.apache.cassandra.utils.Throwables.maybeFail;
 import static org.apache.cassandra.utils.Throwables.merge;
 
@@ -35,7 +33,7 @@
 
     public WrappedSharedCloseable(final AutoCloseable closeable)
     {
-        this(new AutoCloseable[] { closeable});
+        this(new AutoCloseable[] {closeable});
     }
 
     public WrappedSharedCloseable(final AutoCloseable[] closeable)
diff --git a/src/java/org/apache/cassandra/utils/logging/LogbackLoggingSupport.java b/src/java/org/apache/cassandra/utils/logging/LogbackLoggingSupport.java
new file mode 100644
index 0000000..fb17e82
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/logging/LogbackLoggingSupport.java
@@ -0,0 +1,164 @@
+/*
+ * 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.utils.logging;
+
+import java.lang.management.ManagementFactory;
+import java.security.AccessControlException;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.management.JMX;
+import javax.management.ObjectName;
+
+import org.apache.cassandra.security.ThreadAwareSecurityManager;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Maps;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.jmx.JMXConfiguratorMBean;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.TurboFilterList;
+import ch.qos.logback.classic.turbo.ReconfigureOnChangeFilter;
+import ch.qos.logback.classic.turbo.TurboFilter;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.hook.DelayingShutdownHook;
+
+/**
+ * Encapsulates all logback-specific implementations in a central place.
+ * Generally, the Cassandra code-base should be logging-backend agnostic and only use slf4j-api.
+ * This class MUST NOT be used directly, but only via {@link LoggingSupportFactory} which dynamically loads and
+ * instantiates an appropriate implementation according to the used slf4j binding.
+ */
+public class LogbackLoggingSupport implements LoggingSupport
+{
+
+    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(LogbackLoggingSupport.class);
+
+    @Override
+    public void onStartup()
+    {
+        // The default logback configuration in conf/logback.xml allows reloading the
+        // configuration when the configuration file has changed (every 60 seconds by default).
+        // This requires logback to use file I/O APIs. But file I/O is not allowed from UDFs.
+        // I.e. if logback decides to check for a modification of the config file while
+        // executing a sandbox thread, the UDF execution and therefore the whole request
+        // execution will fail with an AccessControlException.
+        // To work around this, a custom ReconfigureOnChangeFilter is installed, that simply
+        // prevents this configuration file check and possible reload of the configuration,
+        // while executing sandboxed UDF code.
+        Logger logbackLogger = (Logger) LoggerFactory.getLogger(ThreadAwareSecurityManager.class);
+        LoggerContext ctx = logbackLogger.getLoggerContext();
+
+        TurboFilterList turboFilterList = ctx.getTurboFilterList();
+        for (int i = 0; i < turboFilterList.size(); i++)
+        {
+            TurboFilter turboFilter = turboFilterList.get(i);
+            if (turboFilter instanceof ReconfigureOnChangeFilter)
+            {
+                ReconfigureOnChangeFilter reconfigureOnChangeFilter = (ReconfigureOnChangeFilter) turboFilter;
+                turboFilterList.set(i, new SMAwareReconfigureOnChangeFilter(reconfigureOnChangeFilter));
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void onShutdown()
+    {
+        DelayingShutdownHook logbackHook = new DelayingShutdownHook();
+        logbackHook.setContext((LoggerContext) LoggerFactory.getILoggerFactory());
+        logbackHook.run();
+    }
+
+    @Override
+    public void setLoggingLevel(String classQualifier, String rawLevel) throws Exception
+    {
+        Logger logBackLogger = (Logger) LoggerFactory.getLogger(classQualifier);
+
+        // if both classQualifier and rawLevel are empty, reload from configuration
+        if (StringUtils.isBlank(classQualifier) && StringUtils.isBlank(rawLevel))
+        {
+            JMXConfiguratorMBean jmxConfiguratorMBean = JMX.newMBeanProxy(ManagementFactory.getPlatformMBeanServer(),
+                                                                          new ObjectName("ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator"),
+                                                                          JMXConfiguratorMBean.class);
+            jmxConfiguratorMBean.reloadDefaultConfiguration();
+            return;
+        }
+        // classQualifier is set, but blank level given
+        else if (StringUtils.isNotBlank(classQualifier) && StringUtils.isBlank(rawLevel))
+        {
+            if (logBackLogger.getLevel() != null || hasAppenders(logBackLogger))
+                logBackLogger.setLevel(null);
+            return;
+        }
+
+        Level level = Level.toLevel(rawLevel);
+        logBackLogger.setLevel(level);
+        logger.info("set log level to {} for classes under '{}' (if the level doesn't look like '{}' then the logger couldn't parse '{}')", level, classQualifier, rawLevel, rawLevel);
+    }
+
+    @Override
+    public Map<String, String> getLoggingLevels()
+    {
+        Map<String, String> logLevelMaps = Maps.newLinkedHashMap();
+        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
+        for (Logger logBackLogger : lc.getLoggerList())
+        {
+            if (logBackLogger.getLevel() != null || hasAppenders(logBackLogger))
+                logLevelMaps.put(logBackLogger.getName(), logBackLogger.getLevel().toString());
+        }
+        return logLevelMaps;
+    }
+
+    private boolean hasAppenders(Logger logBackLogger)
+    {
+        Iterator<Appender<ILoggingEvent>> it = logBackLogger.iteratorForAppenders();
+        return it.hasNext();
+    }
+
+    /**
+     * The purpose of this class is to prevent logback from checking for config file change,
+     * if the current thread is executing a sandboxed thread to avoid {@link AccessControlException}s.
+     */
+    private static class SMAwareReconfigureOnChangeFilter extends ReconfigureOnChangeFilter
+    {
+        SMAwareReconfigureOnChangeFilter(ReconfigureOnChangeFilter reconfigureOnChangeFilter)
+        {
+            setRefreshPeriod(reconfigureOnChangeFilter.getRefreshPeriod());
+            setName(reconfigureOnChangeFilter.getName());
+            setContext(reconfigureOnChangeFilter.getContext());
+            if (reconfigureOnChangeFilter.isStarted())
+            {
+                reconfigureOnChangeFilter.stop();
+                start();
+            }
+        }
+
+        protected boolean changeDetected(long now)
+        {
+            if (ThreadAwareSecurityManager.isSecuredThread())
+                return false;
+            return super.changeDetected(now);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/logging/LoggingSupport.java b/src/java/org/apache/cassandra/utils/logging/LoggingSupport.java
new file mode 100644
index 0000000..8ea83be
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/logging/LoggingSupport.java
@@ -0,0 +1,52 @@
+/*
+ * 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.utils.logging;
+
+import java.util.Map;
+
+/**
+ * Common abstraction of functionality which can be implemented for different logging backend implementations (slf4j bindings).
+ * Concrete implementations are dynamically loaded and instantiated by {@link LoggingSupportFactory#getLoggingSupport()}.
+ */
+public interface LoggingSupport
+{
+    /**
+     * Hook used to execute logging implementation specific customization at Cassandra startup time.
+     */
+    default void onStartup() {}
+
+    /**
+     * Hook used to execute logging implementation specific customization at Cassandra shutdown time.
+     */
+    default void onShutdown() {}
+
+    /**
+     * Changes the given logger to the given log level.
+     *
+     * @param classQualifier the class qualifier or logger name
+     * @param rawLevel the string representation of a log level
+     * @throws Exception an exception which may occur while changing the given logger to the given log level.
+     */
+    void setLoggingLevel(String classQualifier, String rawLevel) throws Exception;
+
+    /**
+     * @return a map of logger names and their associated log level as string representations.
+     */
+    Map<String, String> getLoggingLevels();
+}
diff --git a/src/java/org/apache/cassandra/utils/logging/LoggingSupportFactory.java b/src/java/org/apache/cassandra/utils/logging/LoggingSupportFactory.java
new file mode 100644
index 0000000..9d099bd
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/logging/LoggingSupportFactory.java
@@ -0,0 +1,60 @@
+/*
+ * 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.utils.logging;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.utils.FBUtilities;
+
+/**
+ * Dynamically loads and instantiates an appropriate {@link LoggingSupport} implementation according to the used slf4j binding.
+ * For production use, this should always be {@link LogbackLoggingSupport}.
+ */
+public class LoggingSupportFactory
+{
+    private static final Logger logger = LoggerFactory.getLogger(LoggingSupportFactory.class);
+
+    private static volatile LoggingSupport loggingSupport;
+
+    private LoggingSupportFactory() {}
+
+    /**
+     * @return An appropriate {@link LoggingSupport} implementation according to the used slf4j binding.
+     */
+    public static LoggingSupport getLoggingSupport()
+    {
+        if (loggingSupport == null)
+        {
+            // unfortunately, this is the best way to determine if logback is being used for logger
+            String loggerFactoryClass = LoggerFactory.getILoggerFactory().getClass().getName();
+            if (loggerFactoryClass.contains("logback"))
+            {
+                loggingSupport = FBUtilities.instanceOrConstruct("org.apache.cassandra.utils.logging.LogbackLoggingSupport", "LogbackLoggingSupport");
+            }
+            else
+            {
+                loggingSupport = new NoOpFallbackLoggingSupport();
+                logger.warn("You are using Cassandra with an unsupported deployment. The intended logging implementation library logback is not used by slf4j. Detected slf4j logger factory: {}. "
+                        + "You will not be able to dynamically manage log levels via JMX and may have performance or other issues.", loggerFactoryClass);
+            }
+        }
+        return loggingSupport;
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/logging/NoOpFallbackLoggingSupport.java b/src/java/org/apache/cassandra/utils/logging/NoOpFallbackLoggingSupport.java
new file mode 100644
index 0000000..4f79650
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/logging/NoOpFallbackLoggingSupport.java
@@ -0,0 +1,48 @@
+/*
+ * 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.utils.logging;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A fallback implementation with empty implementations which ensures other slf4j bindings (logging implementations)
+ * than the default supported framework can be used. This loses functionality, but is perfectly fine for most
+ * integration test requirements of applications using an embedded cassandra server.
+ */
+public class NoOpFallbackLoggingSupport implements LoggingSupport
+{
+    private static final Logger logger = LoggerFactory.getLogger(NoOpFallbackLoggingSupport.class);
+
+    @Override
+    public void setLoggingLevel(String classQualifier, String rawLevel) throws Exception
+    {
+        logger.warn("The log level was not changed, because you are using an unsupported slf4j logging implementation for which this functionality was not implemented.");
+    }
+
+    @Override
+    public Map<String, String> getLoggingLevels()
+    {
+        logger.warn("An empty map of logger names and their logging levels was returned, because you are using an unsupported slf4j logging implementation for which this functionality was not implemented.");
+        return Collections.emptyMap();
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/memory/AbstractAllocator.java b/src/java/org/apache/cassandra/utils/memory/AbstractAllocator.java
index 9066335..c3cac2b 100644
--- a/src/java/org/apache/cassandra/utils/memory/AbstractAllocator.java
+++ b/src/java/org/apache/cassandra/utils/memory/AbstractAllocator.java
@@ -20,7 +20,6 @@
 import java.nio.ByteBuffer;
 
 import org.apache.cassandra.db.Clustering;
-import org.apache.cassandra.db.Columns;
 import org.apache.cassandra.db.rows.BTreeRow;
 import org.apache.cassandra.db.rows.Cell;
 import org.apache.cassandra.db.rows.Row;
diff --git a/src/java/org/apache/cassandra/utils/memory/BufferPool.java b/src/java/org/apache/cassandra/utils/memory/BufferPool.java
index d0cea0f..557b275 100644
--- a/src/java/org/apache/cassandra/utils/memory/BufferPool.java
+++ b/src/java/org/apache/cassandra/utils/memory/BufferPool.java
@@ -21,23 +21,24 @@
 import java.lang.ref.PhantomReference;
 import java.lang.ref.ReferenceQueue;
 import java.nio.ByteBuffer;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Queue;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicLongFieldUpdater;
 
-import org.apache.cassandra.io.compress.BufferType;
-import org.apache.cassandra.io.util.FileUtils;
-import org.apache.cassandra.utils.ExecutorUtils;
-import org.apache.cassandra.utils.NoSpamLogger;
-
 import com.google.common.annotations.VisibleForTesting;
 import org.apache.cassandra.concurrent.InfiniteLoopExecutor;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import io.netty.util.concurrent.FastThreadLocal;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.metrics.BufferPoolMetrics;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.NoSpamLogger;
 import org.apache.cassandra.utils.concurrent.Ref;
 
 import static org.apache.cassandra.utils.ExecutorUtils.awaitTermination;
@@ -49,7 +50,7 @@
 public class BufferPool
 {
     /** The size of a page aligned buffer, 64KiB */
-    static final int CHUNK_SIZE = 64 << 10;
+    public static final int CHUNK_SIZE = 64 << 10;
 
     @VisibleForTesting
     public static long MEMORY_USAGE_THRESHOLD = DatabaseDescriptor.getFileCacheSizeInMB() * 1024L * 1024L;
@@ -71,7 +72,8 @@
     private static final GlobalPool globalPool = new GlobalPool();
 
     /** A thread local pool of chunks, where chunks come from the global pool */
-    private static final ThreadLocal<LocalPool> localPool = new ThreadLocal<LocalPool>() {
+    private static final FastThreadLocal<LocalPool> localPool = new FastThreadLocal<LocalPool>()
+    {
         @Override
         protected LocalPool initialValue()
         {
@@ -119,7 +121,7 @@
             return ret;
 
         if (logger.isTraceEnabled())
-            logger.trace("Requested buffer size {} has been allocated directly due to lack of capacity", size);
+            logger.trace("Requested buffer size {} has been allocated directly due to lack of capacity", FBUtilities.prettyPrintMemory(size));
 
         return localPool.get().allocate(size, allocateOnHeapWhenExhausted);
     }
@@ -135,7 +137,9 @@
         if (size > CHUNK_SIZE)
         {
             if (logger.isTraceEnabled())
-                logger.trace("Requested buffer size {} is bigger than {}, allocating directly", size, CHUNK_SIZE);
+                logger.trace("Requested buffer size {} is bigger than {}, allocating directly",
+                             FBUtilities.prettyPrintMemory(size),
+                             FBUtilities.prettyPrintMemory(CHUNK_SIZE));
 
             return localPool.get().allocate(size, allocateOnHeapWhenExhausted);
         }
@@ -181,6 +185,18 @@
         globalPool.debug.check();
     }
 
+    /**
+     * Forces to recycle free local chunks back to the global pool.
+     * This is needed because if buffers were freed by a different thread than the one
+     * that allocated them, recycling might not have happened and the local pool may still own some
+     * fully empty chunks.
+     */
+    @VisibleForTesting
+    static void releaseLocal()
+    {
+        localPool.get().release();
+    }
+
     public static long sizeInBytes()
     {
         return globalPool.sizeInBytes();
@@ -188,12 +204,16 @@
 
     static final class Debug
     {
-        long recycleRound = 1;
+        volatile long recycleRound = 0;
         final Queue<Chunk> allChunks = new ConcurrentLinkedQueue<>();
         void register(Chunk chunk)
         {
             allChunks.add(chunk);
         }
+        void acquire(Chunk chunk)
+        {
+            chunk.lastAcquired = recycleRound;
+        }
         void recycle(Chunk chunk)
         {
             chunk.lastRecycled = recycleRound;
@@ -201,7 +221,8 @@
         void check()
         {
             for (Chunk chunk : allChunks)
-                assert chunk.lastRecycled == recycleRound;
+                assert chunk.lastRecycled >= chunk.lastAcquired;
+            // check is called by a single test thread, so no need for atomic here;
             recycleRound++;
         }
     }
@@ -227,8 +248,8 @@
             if (DISABLED)
                 logger.info("Global buffer pool is disabled, allocating {}", ALLOCATE_ON_HEAP_WHEN_EXAHUSTED ? "on heap" : "off heap");
             else
-                logger.info("Global buffer pool is enabled, when pool is exahusted (max is {} mb) it will allocate {}",
-                            MEMORY_USAGE_THRESHOLD / (1024L * 1024L),
+                logger.info("Global buffer pool is enabled, when pool is exhausted (max is {}) it will allocate {}",
+                            FBUtilities.prettyPrintMemory(MEMORY_USAGE_THRESHOLD),
                             ALLOCATE_ON_HEAP_WHEN_EXAHUSTED ? "on heap" : "off heap");
         }
 
@@ -241,6 +262,14 @@
         /** Return a chunk, the caller will take owership of the parent chunk. */
         public Chunk get()
         {
+            Chunk chunk = getInternal();
+            if (DEBUG && chunk != null)
+                debug.acquire(chunk);
+            return chunk;
+        }
+
+        private Chunk getInternal()
+        {
             while (true)
             {
                 Chunk chunk = chunks.poll();
@@ -264,7 +293,7 @@
                 long cur = memoryUsage.get();
                 if (cur + MACRO_CHUNK_SIZE > MEMORY_USAGE_THRESHOLD)
                 {
-                    noSpamLogger.info("Maximum memory usage reached ({} bytes), cannot allocate chunk of {} bytes",
+                    noSpamLogger.info("Maximum memory usage reached ({}), cannot allocate chunk of {}",
                                       MEMORY_USAGE_THRESHOLD, MACRO_CHUNK_SIZE);
                     return false;
                 }
@@ -273,7 +302,20 @@
             }
 
             // allocate a large chunk
-            Chunk chunk = new Chunk(allocateDirectAligned(MACRO_CHUNK_SIZE));
+            Chunk chunk;
+            try
+            {
+                chunk = new Chunk(allocateDirectAligned(MACRO_CHUNK_SIZE));
+            }
+            catch (OutOfMemoryError oom)
+            {
+                noSpamLogger.error("Buffer pool failed to allocate chunk of {}, current size {} ({}). " +
+                                   "Attempting to continue; buffers will be allocated in on-heap memory which can degrade performance. " +
+                                   "Make sure direct memory size (-XX:MaxDirectMemorySize) is large enough to accommodate off-heap memtables and caches.",
+                                   MACRO_CHUNK_SIZE, sizeInBytes(), oom.toString());
+                return false;
+            }
+
             chunk.acquire(null);
             macroChunks.add(chunk);
             for (int i = 0 ; i < MACRO_CHUNK_SIZE ; i += CHUNK_SIZE)
@@ -453,6 +495,20 @@
                 }
             }
         }
+
+        @VisibleForTesting
+        void release()
+        {
+            chunkCount = 0;
+            for (int i = 0; i < chunks.length; i++)
+            {
+                if (chunks[i] != null)
+                {
+                    chunks[i].release();
+                    chunks[i] = null;
+                }
+            }
+        }
     }
 
     private static final class LocalPoolRef extends  PhantomReference<LocalPool>
@@ -545,7 +601,8 @@
         // if this is set, it means the chunk may not be recycled because we may still allocate from it;
         // if it has been unset the local pool has finished with it, and it may be recycled
         private volatile LocalPool owner;
-        private long lastRecycled;
+        private volatile long lastAcquired;
+        private volatile long lastRecycled;
         private final Chunk original;
 
         Chunk(Chunk recycle)
diff --git a/src/java/org/apache/cassandra/utils/memory/EnsureOnHeap.java b/src/java/org/apache/cassandra/utils/memory/EnsureOnHeap.java
new file mode 100644
index 0000000..54ace5e
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/memory/EnsureOnHeap.java
@@ -0,0 +1,165 @@
+/*
+ *
+ * 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.utils.memory;
+
+import java.util.Iterator;
+
+import org.apache.cassandra.db.BufferDecoratedKey;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.DeletionInfo;
+import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.db.transform.Transformation;
+import org.apache.cassandra.utils.SearchIterator;
+
+public abstract class EnsureOnHeap extends Transformation
+{
+    public abstract DecoratedKey applyToPartitionKey(DecoratedKey key);
+    public abstract UnfilteredRowIterator applyToPartition(UnfilteredRowIterator partition);
+    public abstract SearchIterator<Clustering, Row> applyToPartition(SearchIterator<Clustering, Row> partition);
+    public abstract Iterator<Row> applyToPartition(Iterator<Row> partition);
+    public abstract DeletionInfo applyToDeletionInfo(DeletionInfo deletionInfo);
+    public abstract Row applyToRow(Row row);
+    public abstract Row applyToStatic(Row row);
+    public abstract RangeTombstoneMarker applyToMarker(RangeTombstoneMarker marker);
+
+    static class CloneToHeap extends EnsureOnHeap
+    {
+        protected BaseRowIterator<?> applyToPartition(BaseRowIterator partition)
+        {
+            return partition instanceof UnfilteredRowIterator
+                   ? Transformation.apply((UnfilteredRowIterator) partition, this)
+                   : Transformation.apply((RowIterator) partition, this);
+        }
+
+        public DecoratedKey applyToPartitionKey(DecoratedKey key)
+        {
+            return new BufferDecoratedKey(key.getToken(), HeapAllocator.instance.clone(key.getKey()));
+        }
+
+        public Row applyToRow(Row row)
+        {
+            if (row == null)
+                return null;
+            return Rows.copy(row, HeapAllocator.instance.cloningBTreeRowBuilder()).build();
+        }
+
+        public Row applyToStatic(Row row)
+        {
+            if (row == Rows.EMPTY_STATIC_ROW)
+                return row;
+            return applyToRow(row);
+        }
+
+        public RangeTombstoneMarker applyToMarker(RangeTombstoneMarker marker)
+        {
+            return marker.copy(HeapAllocator.instance);
+        }
+
+        public UnfilteredRowIterator applyToPartition(UnfilteredRowIterator partition)
+        {
+            return Transformation.apply(partition, this);
+        }
+
+        public SearchIterator<Clustering, Row> applyToPartition(SearchIterator<Clustering, Row> partition)
+        {
+            return new SearchIterator<Clustering, Row>()
+            {
+                public Row next(Clustering key)
+                {
+                    return applyToRow(partition.next(key));
+                }
+            };
+        }
+
+        public Iterator<Row> applyToPartition(Iterator<Row> partition)
+        {
+            return new Iterator<Row>()
+            {
+                public boolean hasNext()
+                {
+                    return partition.hasNext();
+                }
+                public Row next()
+                {
+                    return applyToRow(partition.next());
+                }
+                public void remove()
+                {
+                    partition.remove();
+                }
+            };
+        }
+
+        public DeletionInfo applyToDeletionInfo(DeletionInfo deletionInfo)
+        {
+            return deletionInfo.copy(HeapAllocator.instance);
+        }
+    }
+
+    static class NoOp extends EnsureOnHeap
+    {
+        protected BaseRowIterator<?> applyToPartition(BaseRowIterator partition)
+        {
+            return partition;
+        }
+
+        public DecoratedKey applyToPartitionKey(DecoratedKey key)
+        {
+            return key;
+        }
+
+        public Row applyToRow(Row row)
+        {
+            return row;
+        }
+
+        public Row applyToStatic(Row row)
+        {
+            return row;
+        }
+
+        public RangeTombstoneMarker applyToMarker(RangeTombstoneMarker marker)
+        {
+            return marker;
+        }
+
+        public UnfilteredRowIterator applyToPartition(UnfilteredRowIterator partition)
+        {
+            return partition;
+        }
+
+        public SearchIterator<Clustering, Row> applyToPartition(SearchIterator<Clustering, Row> partition)
+        {
+            return partition;
+        }
+
+        public Iterator<Row> applyToPartition(Iterator<Row> partition)
+        {
+            return partition;
+        }
+
+        public DeletionInfo applyToDeletionInfo(DeletionInfo deletionInfo)
+        {
+            return deletionInfo;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/utils/memory/HeapAllocator.java b/src/java/org/apache/cassandra/utils/memory/HeapAllocator.java
index 41877f5..8333142 100644
--- a/src/java/org/apache/cassandra/utils/memory/HeapAllocator.java
+++ b/src/java/org/apache/cassandra/utils/memory/HeapAllocator.java
@@ -33,4 +33,9 @@
     {
         return ByteBuffer.allocate(size);
     }
+
+    public boolean allocatingOnHeap()
+    {
+        return true;
+    }
 }
diff --git a/src/java/org/apache/cassandra/utils/memory/HeapPool.java b/src/java/org/apache/cassandra/utils/memory/HeapPool.java
index 1b698c9..6371bda 100644
--- a/src/java/org/apache/cassandra/utils/memory/HeapPool.java
+++ b/src/java/org/apache/cassandra/utils/memory/HeapPool.java
@@ -29,11 +29,6 @@
         super(maxOnHeapMemory, 0, cleanupThreshold, cleaner);
     }
 
-    public boolean needToCopyOnHeap()
-    {
-        return false;
-    }
-
     public MemtableAllocator newAllocator()
     {
         return new Allocator(this);
@@ -41,6 +36,7 @@
 
     private static class Allocator extends MemtableBufferAllocator
     {
+        private static final EnsureOnHeap ENSURE_NOOP = new EnsureOnHeap.NoOp();
         Allocator(HeapPool pool)
         {
             super(pool.onHeap.newAllocator(), pool.offHeap.newAllocator());
@@ -51,5 +47,10 @@
             super.onHeap().allocate(size, opGroup);
             return ByteBuffer.allocate(size);
         }
+
+        public EnsureOnHeap ensureOnHeap()
+        {
+            return ENSURE_NOOP;
+        }
     }
 }
diff --git a/src/java/org/apache/cassandra/utils/memory/MemoryUtil.java b/src/java/org/apache/cassandra/utils/memory/MemoryUtil.java
index 22ecbf5..6c2e6fd 100644
--- a/src/java/org/apache/cassandra/utils/memory/MemoryUtil.java
+++ b/src/java/org/apache/cassandra/utils/memory/MemoryUtil.java
@@ -23,6 +23,9 @@
 import java.nio.ByteOrder;
 
 import com.sun.jna.Native;
+
+import org.apache.cassandra.utils.Architecture;
+
 import sun.misc.Unsafe;
 import sun.nio.ch.DirectBuffer;
 
@@ -31,7 +34,7 @@
     private static final long UNSAFE_COPY_THRESHOLD = 1024 * 1024L; // copied from java.nio.Bits
 
     private static final Unsafe unsafe;
-    private static final Class<?> DIRECT_BYTE_BUFFER_CLASS;
+    private static final Class<?> DIRECT_BYTE_BUFFER_CLASS, RO_DIRECT_BYTE_BUFFER_CLASS;
     private static final long DIRECT_BYTE_BUFFER_ADDRESS_OFFSET;
     private static final long DIRECT_BYTE_BUFFER_CAPACITY_OFFSET;
     private static final long DIRECT_BYTE_BUFFER_LIMIT_OFFSET;
@@ -44,17 +47,10 @@
 
     private static final boolean BIG_ENDIAN = ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN);
 
-    private static final boolean UNALIGNED;
-    public static final boolean INVERTED_ORDER;
+    public static final boolean INVERTED_ORDER = Architecture.IS_UNALIGNED && !BIG_ENDIAN;
 
     static
     {
-        String arch = System.getProperty("os.arch");
-        // Note that s390x architecture are not officially supported and adding it here is only done out of convenience
-        // for those that want to run C* on this architecture at their own risk (see #11214)
-        UNALIGNED = arch.equals("i386") || arch.equals("x86")
-                || arch.equals("amd64") || arch.equals("x86_64") || arch.equals("s390x");
-        INVERTED_ORDER = UNALIGNED && !BIG_ENDIAN;
         try
         {
             Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
@@ -67,6 +63,7 @@
             DIRECT_BYTE_BUFFER_POSITION_OFFSET = unsafe.objectFieldOffset(Buffer.class.getDeclaredField("position"));
             DIRECT_BYTE_BUFFER_ATTACHMENT_OFFSET = unsafe.objectFieldOffset(clazz.getDeclaredField("att"));
             DIRECT_BYTE_BUFFER_CLASS = clazz;
+            RO_DIRECT_BYTE_BUFFER_CLASS = ByteBuffer.allocateDirect(0).asReadOnlyBuffer().getClass();
 
             clazz = ByteBuffer.allocate(0).getClass();
             BYTE_BUFFER_OFFSET_OFFSET = unsafe.objectFieldOffset(ByteBuffer.class.getDeclaredField("offset"));
@@ -107,6 +104,11 @@
         unsafe.putByte(address, b);
     }
 
+    public static void setByte(long address, int count, byte b)
+    {
+        unsafe.setMemory(address, count, b);
+    }
+
     public static void setShort(long address, short s)
     {
         unsafe.putShort(address, s);
@@ -114,7 +116,7 @@
 
     public static void setInt(long address, int l)
     {
-        if (UNALIGNED)
+        if (Architecture.IS_UNALIGNED)
             unsafe.putInt(address, l);
         else
             putIntByByte(address, l);
@@ -122,7 +124,7 @@
 
     public static void setLong(long address, long l)
     {
-        if (UNALIGNED)
+        if (Architecture.IS_UNALIGNED)
             unsafe.putLong(address, l);
         else
             putLongByByte(address, l);
@@ -135,28 +137,38 @@
 
     public static int getShort(long address)
     {
-        return (UNALIGNED ? unsafe.getShort(address) : getShortByByte(address)) & 0xffff;
+        return (Architecture.IS_UNALIGNED ? unsafe.getShort(address) : getShortByByte(address)) & 0xffff;
     }
 
     public static int getInt(long address)
     {
-        return UNALIGNED ? unsafe.getInt(address) : getIntByByte(address);
+        return Architecture.IS_UNALIGNED ? unsafe.getInt(address) : getIntByByte(address);
     }
 
     public static long getLong(long address)
     {
-        return UNALIGNED ? unsafe.getLong(address) : getLongByByte(address);
+        return Architecture.IS_UNALIGNED ? unsafe.getLong(address) : getLongByByte(address);
     }
 
     public static ByteBuffer getByteBuffer(long address, int length)
     {
-        ByteBuffer instance = getHollowDirectByteBuffer();
+        return getByteBuffer(address, length, ByteOrder.nativeOrder());
+    }
+
+    public static ByteBuffer getByteBuffer(long address, int length, ByteOrder order)
+    {
+        ByteBuffer instance = getHollowDirectByteBuffer(order);
         setByteBuffer(instance, address, length);
         return instance;
     }
 
     public static ByteBuffer getHollowDirectByteBuffer()
     {
+        return getHollowDirectByteBuffer(ByteOrder.nativeOrder());
+    }
+
+    public static ByteBuffer getHollowDirectByteBuffer(ByteOrder order)
+    {
         ByteBuffer instance;
         try
         {
@@ -166,7 +178,7 @@
         {
             throw new AssertionError(e);
         }
-        instance.order(ByteOrder.nativeOrder());
+        instance.order(order);
         return instance;
     }
 
@@ -206,7 +218,7 @@
 
     public static ByteBuffer duplicateDirectByteBuffer(ByteBuffer source, ByteBuffer hollowBuffer)
     {
-        assert source.getClass() == DIRECT_BYTE_BUFFER_CLASS;
+        assert source.getClass() == DIRECT_BYTE_BUFFER_CLASS || source.getClass() == RO_DIRECT_BYTE_BUFFER_CLASS;
         unsafe.putLong(hollowBuffer, DIRECT_BYTE_BUFFER_ADDRESS_OFFSET, unsafe.getLong(source, DIRECT_BYTE_BUFFER_ADDRESS_OFFSET));
         unsafe.putInt(hollowBuffer, DIRECT_BYTE_BUFFER_POSITION_OFFSET, unsafe.getInt(source, DIRECT_BYTE_BUFFER_POSITION_OFFSET));
         unsafe.putInt(hollowBuffer, DIRECT_BYTE_BUFFER_LIMIT_OFFSET, unsafe.getInt(source, DIRECT_BYTE_BUFFER_LIMIT_OFFSET));
diff --git a/src/java/org/apache/cassandra/utils/memory/MemtableAllocator.java b/src/java/org/apache/cassandra/utils/memory/MemtableAllocator.java
index 0bc8c24..9576ccf 100644
--- a/src/java/org/apache/cassandra/utils/memory/MemtableAllocator.java
+++ b/src/java/org/apache/cassandra/utils/memory/MemtableAllocator.java
@@ -64,6 +64,7 @@
 
     public abstract Row.Builder rowBuilder(OpOrder.Group opGroup);
     public abstract DecoratedKey clone(DecoratedKey key, OpOrder.Group opGroup);
+    public abstract EnsureOnHeap ensureOnHeap();
 
     public SubAllocator onHeap()
     {
@@ -300,4 +301,5 @@
         private static final AtomicLongFieldUpdater<SubAllocator> reclaimingUpdater = AtomicLongFieldUpdater.newUpdater(SubAllocator.class, "reclaiming");
     }
 
+
 }
diff --git a/src/java/org/apache/cassandra/utils/memory/MemtableBufferAllocator.java b/src/java/org/apache/cassandra/utils/memory/MemtableBufferAllocator.java
index fb35b38..fe412ea 100644
--- a/src/java/org/apache/cassandra/utils/memory/MemtableBufferAllocator.java
+++ b/src/java/org/apache/cassandra/utils/memory/MemtableBufferAllocator.java
@@ -19,7 +19,6 @@
 
 import java.nio.ByteBuffer;
 
-import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.utils.concurrent.OpOrder;
diff --git a/src/java/org/apache/cassandra/utils/memory/MemtablePool.java b/src/java/org/apache/cassandra/utils/memory/MemtablePool.java
index cd434c5..89d5e37 100644
--- a/src/java/org/apache/cassandra/utils/memory/MemtablePool.java
+++ b/src/java/org/apache/cassandra/utils/memory/MemtablePool.java
@@ -74,14 +74,13 @@
         return cleaner == null ? null : new MemtableCleanerThread<>(this, cleaner);
     }
 
-    public abstract boolean needToCopyOnHeap();
-
     @VisibleForTesting
     public void shutdownAndWait(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
     {
         ExecutorUtils.shutdownNowAndWait(timeout, unit, cleaner);
     }
 
+
     public abstract MemtableAllocator newAllocator();
 
     public boolean needsCleaning()
diff --git a/src/java/org/apache/cassandra/utils/memory/NativeAllocator.java b/src/java/org/apache/cassandra/utils/memory/NativeAllocator.java
index 3d4ec16..af8b750 100644
--- a/src/java/org/apache/cassandra/utils/memory/NativeAllocator.java
+++ b/src/java/org/apache/cassandra/utils/memory/NativeAllocator.java
@@ -24,9 +24,8 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
-import org.apache.cassandra.db.DecoratedKey;
-import org.apache.cassandra.db.NativeDecoratedKey;
-import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.utils.concurrent.OpOrder;
 
 /**
@@ -56,16 +55,42 @@
 
     private final AtomicReference<Region> currentRegion = new AtomicReference<>();
     private final ConcurrentLinkedQueue<Region> regions = new ConcurrentLinkedQueue<>();
+    private final EnsureOnHeap.CloneToHeap cloneToHeap = new EnsureOnHeap.CloneToHeap();
 
     protected NativeAllocator(NativePool pool)
     {
         super(pool.onHeap.newAllocator(), pool.offHeap.newAllocator());
     }
 
+    private static class CloningBTreeRowBuilder extends BTreeRow.Builder
+    {
+        final OpOrder.Group writeOp;
+        final NativeAllocator allocator;
+        private CloningBTreeRowBuilder(OpOrder.Group writeOp, NativeAllocator allocator)
+        {
+            super(true);
+            this.writeOp = writeOp;
+            this.allocator = allocator;
+        }
+
+        @Override
+        public void newRow(Clustering clustering)
+        {
+            if (clustering != Clustering.STATIC_CLUSTERING)
+                clustering = new NativeClustering(allocator, writeOp, clustering);
+            super.newRow(clustering);
+        }
+
+        @Override
+        public void addCell(Cell cell)
+        {
+            super.addCell(new NativeCell(allocator, writeOp, cell));
+        }
+    }
+
     public Row.Builder rowBuilder(OpOrder.Group opGroup)
     {
-        // TODO
-        throw new UnsupportedOperationException();
+        return new CloningBTreeRowBuilder(opGroup, this);
     }
 
     public DecoratedKey clone(DecoratedKey key, OpOrder.Group writeOp)
@@ -73,6 +98,11 @@
         return new NativeDecoratedKey(key.getToken(), this, writeOp, key.getKey());
     }
 
+    public EnsureOnHeap ensureOnHeap()
+    {
+        return cloneToHeap;
+    }
+
     public long allocate(int size, OpOrder.Group opGroup)
     {
         assert size >= 0;
@@ -139,6 +169,7 @@
     {
         for (Region region : regions)
             MemoryUtil.free(region.peer);
+
         super.setDiscarded();
     }
 
@@ -184,7 +215,7 @@
          * Offset for the next allocation, or the sentinel value -1
          * which implies that the region is still uninitialized.
          */
-        private AtomicInteger nextFreeOffset = new AtomicInteger(0);
+        private final AtomicInteger nextFreeOffset = new AtomicInteger(0);
 
         /**
          * Create an uninitialized region. Note that memory is not allocated yet, so
diff --git a/src/java/org/apache/cassandra/utils/memory/NativePool.java b/src/java/org/apache/cassandra/utils/memory/NativePool.java
index 29ea8fb..e88b4d7 100644
--- a/src/java/org/apache/cassandra/utils/memory/NativePool.java
+++ b/src/java/org/apache/cassandra/utils/memory/NativePool.java
@@ -26,12 +26,6 @@
     }
 
     @Override
-    public boolean needToCopyOnHeap()
-    {
-        return true;
-    }
-
-    @Override
     public NativeAllocator newAllocator()
     {
         return new NativeAllocator(this);
diff --git a/src/java/org/apache/cassandra/utils/memory/SlabAllocator.java b/src/java/org/apache/cassandra/utils/memory/SlabAllocator.java
index 5a8ec18..f72a2c3 100644
--- a/src/java/org/apache/cassandra/utils/memory/SlabAllocator.java
+++ b/src/java/org/apache/cassandra/utils/memory/SlabAllocator.java
@@ -59,13 +59,20 @@
 
     // this queue is used to keep references to off-heap allocated regions so that we can free them when we are discarded
     private final ConcurrentLinkedQueue<Region> offHeapRegions = new ConcurrentLinkedQueue<>();
-    private AtomicLong unslabbedSize = new AtomicLong(0);
+    private final AtomicLong unslabbedSize = new AtomicLong(0);
     private final boolean allocateOnHeapOnly;
+    private final EnsureOnHeap ensureOnHeap;
 
     SlabAllocator(SubAllocator onHeap, SubAllocator offHeap, boolean allocateOnHeapOnly)
     {
         super(onHeap, offHeap);
         this.allocateOnHeapOnly = allocateOnHeapOnly;
+        this.ensureOnHeap = allocateOnHeapOnly ? new EnsureOnHeap.NoOp() : new EnsureOnHeap.CloneToHeap();
+    }
+
+    public EnsureOnHeap ensureOnHeap()
+    {
+        return ensureOnHeap;
     }
 
     public ByteBuffer allocate(int size)
@@ -163,13 +170,13 @@
         /**
          * Actual underlying data
          */
-        private ByteBuffer data;
+        private final ByteBuffer data;
 
         /**
          * Offset for the next allocation, or the sentinel value -1
          * which implies that the region is still uninitialized.
          */
-        private AtomicInteger nextFreeOffset = new AtomicInteger(0);
+        private final AtomicInteger nextFreeOffset = new AtomicInteger(0);
 
         /**
          * Create an uninitialized region. Note that memory is not allocated yet, so
diff --git a/src/java/org/apache/cassandra/utils/memory/SlabPool.java b/src/java/org/apache/cassandra/utils/memory/SlabPool.java
index a779432..416d1dd 100644
--- a/src/java/org/apache/cassandra/utils/memory/SlabPool.java
+++ b/src/java/org/apache/cassandra/utils/memory/SlabPool.java
@@ -32,9 +32,4 @@
     {
         return new SlabAllocator(onHeap.newAllocator(), offHeap.newAllocator(), allocateOnHeap);
     }
-
-    public boolean needToCopyOnHeap()
-    {
-        return !allocateOnHeap;
-    }
 }
diff --git a/src/java/org/apache/cassandra/utils/obs/BitUtil.java b/src/java/org/apache/cassandra/utils/obs/BitUtil.java
index 200ffbf..e04de2b 100644
--- a/src/java/org/apache/cassandra/utils/obs/BitUtil.java
+++ b/src/java/org/apache/cassandra/utils/obs/BitUtil.java
@@ -20,10 +20,12 @@
 /**  A variety of high efficiency bit twiddling routines.
  * @lucene.internal
  */
-final class BitUtil {
+final class BitUtil
+{
 
   /** Returns the number of bits set in the long */
-  public static int pop(long x) {
+  public static int pop(long x)
+  {
   /* Hacker's Delight 32 bit pop function:
    * http://www.hackersdelight.org/HDcode/newCode/pop_arrayHS.cc
    *
@@ -48,7 +50,8 @@
   }
 
   /*** Returns the number of set bits in an array of longs. */
-  public static long pop_array(long A[], int wordOffset, int numWords) {
+  public static long pop_array(long A[], int wordOffset, int numWords)
+  {
     /*
     * Robert Harley and David Seal's bit counting algorithm, as documented
     * in the revisions of Hacker's Delight
@@ -68,7 +71,8 @@
     long ones=0, twos=0, fours=0;
 
     int i;
-    for (i = wordOffset; i <= n - 8; i+=8) {
+    for (i = wordOffset; i <= n - 8; i+=8)
+    {
       /***  C macro from Hacker's Delight
        #define CSA(h,l, a,b,c) \
        {unsigned u = a ^ b; unsigned v = c; \
@@ -133,7 +137,8 @@
     //   for (i = i; i < n; i++)      // Add in the last elements
     //  tot = tot + pop(A[i]);
 
-    if (i<=n-4) {
+    if (i<=n-4)
+    {
       long twosA, twosB, foursA, eights;
       {
         long b=A[i], c=A[i+1];
@@ -159,7 +164,8 @@
       i+=4;
     }
 
-    if (i<=n-2) {
+    if (i<=n-2)
+    {
       long b=A[i], c=A[i+1];
       long u=ones ^ b;
       long twosA=(ones & b)|( u & c);
@@ -175,7 +181,8 @@
       i+=2;
     }
 
-    if (i<n) {
+    if (i<n)
+    {
       tot += pop(A[i]);
     }
 
@@ -190,14 +197,16 @@
   /** Returns the popcount or cardinality of the two sets after an intersection.
    * Neither array is modified.
    */
-  public static long pop_intersect(long A[], long B[], int wordOffset, int numWords) {
+  public static long pop_intersect(long A[], long B[], int wordOffset, int numWords)
+  {
     // generated from pop_array via sed 's/A\[\([^]]*\)\]/\(A[\1] \& B[\1]\)/g'
     int n = wordOffset+numWords;
     long tot=0, tot8=0;
     long ones=0, twos=0, fours=0;
 
     int i;
-    for (i = wordOffset; i <= n - 8; i+=8) {
+    for (i = wordOffset; i <= n - 8; i+=8)
+    {
       long twosA,twosB,foursA,foursB,eights;
 
       // CSA(twosA, ones, ones, (A[i] & B[i]), (A[i+1] & B[i+1]))
@@ -251,7 +260,8 @@
     }
 
 
-    if (i<=n-4) {
+    if (i<=n-4)
+    {
       long twosA, twosB, foursA, eights;
       {
         long b=(A[i] & B[i]), c=(A[i+1] & B[i+1]);
@@ -277,7 +287,8 @@
       i+=4;
     }
 
-    if (i<=n-2) {
+    if (i<=n-2)
+    {
       long b=(A[i] & B[i]), c=(A[i+1] & B[i+1]);
       long u=ones ^ b;
       long twosA=(ones & b)|( u & c);
@@ -293,7 +304,8 @@
       i+=2;
     }
 
-    if (i<n) {
+    if (i<n)
+    {
       tot += pop((A[i] & B[i]));
     }
 
@@ -308,14 +320,16 @@
   /** Returns the popcount or cardinality of the union of two sets.
     * Neither array is modified.
     */
-   public static long pop_union(long A[], long B[], int wordOffset, int numWords) {
+   public static long pop_union(long A[], long B[], int wordOffset, int numWords)
+   {
      // generated from pop_array via sed 's/A\[\([^]]*\)\]/\(A[\1] \| B[\1]\)/g'
      int n = wordOffset+numWords;
      long tot=0, tot8=0;
      long ones=0, twos=0, fours=0;
 
      int i;
-     for (i = wordOffset; i <= n - 8; i+=8) {
+     for (i = wordOffset; i <= n - 8; i+=8)
+     {
        /***  C macro from Hacker's Delight
         #define CSA(h,l, a,b,c) \
         {unsigned u = a ^ b; unsigned v = c; \
@@ -375,7 +389,8 @@
      }
 
 
-     if (i<=n-4) {
+     if (i<=n-4)
+     {
        long twosA, twosB, foursA, eights;
        {
          long b=(A[i] | B[i]), c=(A[i+1] | B[i+1]);
@@ -401,7 +416,8 @@
        i+=4;
      }
 
-     if (i<=n-2) {
+     if (i<=n-2)
+     {
        long b=(A[i] | B[i]), c=(A[i+1] | B[i+1]);
        long u=ones ^ b;
        long twosA=(ones & b)|( u & c);
@@ -417,7 +433,8 @@
        i+=2;
      }
 
-     if (i<n) {
+     if (i<n)
+     {
        tot += pop((A[i] | B[i]));
      }
 
@@ -432,14 +449,16 @@
   /** Returns the popcount or cardinality of A & ~B
    * Neither array is modified.
    */
-  public static long pop_andnot(long A[], long B[], int wordOffset, int numWords) {
+  public static long pop_andnot(long A[], long B[], int wordOffset, int numWords)
+  {
     // generated from pop_array via sed 's/A\[\([^]]*\)\]/\(A[\1] \& ~B[\1]\)/g'
     int n = wordOffset+numWords;
     long tot=0, tot8=0;
     long ones=0, twos=0, fours=0;
 
     int i;
-    for (i = wordOffset; i <= n - 8; i+=8) {
+    for (i = wordOffset; i <= n - 8; i+=8)
+    {
       /***  C macro from Hacker's Delight
        #define CSA(h,l, a,b,c) \
        {unsigned u = a ^ b; unsigned v = c; \
@@ -499,7 +518,8 @@
     }
 
 
-    if (i<=n-4) {
+    if (i<=n-4)
+    {
       long twosA, twosB, foursA, eights;
       {
         long b=(A[i] & ~B[i]), c=(A[i+1] & ~B[i+1]);
@@ -525,7 +545,8 @@
       i+=4;
     }
 
-    if (i<=n-2) {
+    if (i<=n-2)
+    {
       long b=(A[i] & ~B[i]), c=(A[i+1] & ~B[i+1]);
       long u=ones ^ b;
       long twosA=(ones & b)|( u & c);
@@ -541,7 +562,8 @@
       i+=2;
     }
 
-    if (i<n) {
+    if (i<n)
+    {
       tot += pop((A[i] & ~B[i]));
     }
 
@@ -553,13 +575,15 @@
     return tot;
   }
 
-  public static long pop_xor(long A[], long B[], int wordOffset, int numWords) {
+  public static long pop_xor(long A[], long B[], int wordOffset, int numWords)
+  {
     int n = wordOffset+numWords;
     long tot=0, tot8=0;
     long ones=0, twos=0, fours=0;
 
     int i;
-    for (i = wordOffset; i <= n - 8; i+=8) {
+    for (i = wordOffset; i <= n - 8; i+=8)
+    {
       /***  C macro from Hacker's Delight
        #define CSA(h,l, a,b,c) \
        {unsigned u = a ^ b; unsigned v = c; \
@@ -619,7 +643,8 @@
     }
 
 
-    if (i<=n-4) {
+    if (i<=n-4)
+    {
       long twosA, twosB, foursA, eights;
       {
         long b=(A[i] ^ B[i]), c=(A[i+1] ^ B[i+1]);
@@ -645,7 +670,8 @@
       i+=4;
     }
 
-    if (i<=n-2) {
+    if (i<=n-2)
+    {
       long b=(A[i] ^ B[i]), c=(A[i+1] ^ B[i+1]);
       long u=ones ^ b;
       long twosA=(ones & b)|( u & c);
@@ -661,7 +687,8 @@
       i+=2;
     }
 
-    if (i<n) {
+    if (i<n)
+    {
       tot += pop((A[i] ^ B[i]));
     }
 
@@ -688,7 +715,8 @@
 
 
   /** Returns number of trailing zeros in a 64 bit long value. */
-  public static int ntz(long val) {
+  public static int ntz(long val)
+  {
     // A full binary search to determine the low byte was slower than
     // a linear search for nextSetBit().  This is most likely because
     // the implementation of nextSetBit() shifts bits to the right, increasing
@@ -704,7 +732,8 @@
     int lowByte = lower & 0xff;
     if (lowByte != 0) return ntzTable[lowByte];
 
-    if (lower!=0) {
+    if (lower!=0)
+    {
       lowByte = (lower>>>8) & 0xff;
       if (lowByte != 0) return ntzTable[lowByte] + 8;
       lowByte = (lower>>>16) & 0xff;
@@ -712,7 +741,9 @@
       // no need to mask off low byte for the last byte in the 32 bit word
       // no need to check for zero on the last byte either.
       return ntzTable[lower>>>24] + 24;
-    } else {
+    }
+    else
+    {
       // grab upper 32 bits
       int upper=(int)(val>>32);
       lowByte = upper & 0xff;
@@ -728,7 +759,8 @@
   }
 
   /** Returns number of trailing zeros in a 32 bit int value. */
-  public static int ntz(int val) {
+  public static int ntz(int val)
+  {
     // This implementation does a single binary search at the top level only.
     // In addition, the case of a non-zero first byte is checked for first
     // because it is the most common in dense bit arrays.
@@ -748,7 +780,8 @@
    * (only works for x!=0)
    * <br/> This is an alternate implementation of ntz()
    */
-  public static int ntz2(long x) {
+  public static int ntz2(long x)
+  {
    int n = 0;
    int y = (int)x;
    if (y==0) {n+=32; y = (int)(x>>>32); }   // the only 64 bit shift necessary
@@ -760,7 +793,8 @@
   /** returns 0 based index of first set bit
    * <br/> This is an alternate implementation of ntz()
    */
-  public static int ntz3(long x) {
+  public static int ntz3(long x)
+  {
    // another implementation taken from Hackers Delight, extended to 64 bits
    // and converted to Java.
    // Many 32 bit ntz algorithms are at http://www.hackersdelight.org/HDcode/ntz.cc
@@ -778,17 +812,20 @@
 
 
   /** returns true if v is a power of two or zero*/
-  public static boolean isPowerOfTwo(int v) {
+  public static boolean isPowerOfTwo(int v)
+  {
     return ((v & (v-1)) == 0);
   }
 
   /** returns true if v is a power of two or zero*/
-  public static boolean isPowerOfTwo(long v) {
+  public static boolean isPowerOfTwo(long v)
+  {
     return ((v & (v-1)) == 0);
   }
 
   /** returns the next highest power of two, or the current value if it's already a power of two or zero*/
-  public static int nextHighestPowerOfTwo(int v) {
+  public static int nextHighestPowerOfTwo(int v)
+  {
     v--;
     v |= v >> 1;
     v |= v >> 2;
@@ -800,7 +837,8 @@
   }
 
   /** returns the next highest power of two, or the current value if it's already a power of two or zero*/
-   public static long nextHighestPowerOfTwo(long v) {
+   public static long nextHighestPowerOfTwo(long v)
+   {
     v--;
     v |= v >> 1;
     v |= v >> 2;
diff --git a/src/java/org/apache/cassandra/utils/obs/OpenBitSet.java b/src/java/org/apache/cassandra/utils/obs/OpenBitSet.java
index 82e6929..a21729a 100644
--- a/src/java/org/apache/cassandra/utils/obs/OpenBitSet.java
+++ b/src/java/org/apache/cassandra/utils/obs/OpenBitSet.java
@@ -85,7 +85,8 @@
           bits[bits.length - 1] = new long[lastPageSize];
   }
 
-  public OpenBitSet() {
+  public OpenBitSet()
+  {
     this(64);
   }
 
@@ -124,12 +125,14 @@
   * Returns the current capacity of this set.  Included for
   * compatibility.  This is *not* equal to {@link #cardinality}
   */
-  public long size() {
+  public long size()
+  {
       return capacity();
   }
 
   // @Override -- not until Java 1.6
-  public long length() {
+  public long length()
+  {
     return capacity();
   }
 
@@ -145,7 +148,8 @@
    * Returns true or false for the specified bit index.
    * The index should be less than the OpenBitSet size
    */
-  public boolean get(int index) {
+  public boolean get(int index)
+  {
     int i = index >> 6;               // div 64
     // signed shift will keep a negative index and force an
     // array-index-out-of-bounds-exception, removing the need for an explicit check.
@@ -159,7 +163,8 @@
    * Returns true or false for the specified bit index.
    * The index should be less than the OpenBitSet size.
    */
-  public boolean get(long index) {
+  public boolean get(long index)
+  {
     int i = (int)(index >> 6);               // div 64
     int bit = (int)index & 0x3f;           // mod 64
     long bitmask = 1L << bit;
@@ -171,7 +176,8 @@
    * Sets the bit at the specified index.
    * The index should be less than the OpenBitSet size.
    */
-  public void set(long index) {
+  public void set(long index)
+  {
     int wordNum = (int)(index >> 6);
     int bit = (int)index & 0x3f;
     long bitmask = 1L << bit;
@@ -182,7 +188,8 @@
    * Sets the bit at the specified index.
    * The index should be less than the OpenBitSet size.
    */
-  public void set(int index) {
+  public void set(int index)
+  {
     int wordNum = index >> 6;      // div 64
     int bit = index & 0x3f;     // mod 64
     long bitmask = 1L << bit;
@@ -193,7 +200,8 @@
    * clears a bit.
    * The index should be less than the OpenBitSet size.
    */
-  public void clear(int index) {
+  public void clear(int index)
+  {
     int wordNum = index >> 6;
     int bit = index & 0x03f;
     long bitmask = 1L << bit;
@@ -211,7 +219,8 @@
    * clears a bit.
    * The index should be less than the OpenBitSet size.
    */
-  public void clear(long index) {
+  public void clear(long index)
+  {
     int wordNum = (int)(index >> 6); // div 64
     int bit = (int)index & 0x3f;     // mod 64
     long bitmask = 1L << bit;
@@ -224,7 +233,8 @@
    * @param startIndex lower index
    * @param endIndex one-past the last bit to clear
    */
-  public void clear(int startIndex, int endIndex) {
+  public void clear(int startIndex, int endIndex)
+  {
     if (endIndex <= startIndex) return;
 
     int startWord = (startIndex>>6);
@@ -241,7 +251,8 @@
     startmask = ~startmask;
     endmask = ~endmask;
 
-    if (startWord == endWord) {
+    if (startWord == endWord)
+    {
       bits[startWord / PAGE_SIZE][startWord % PAGE_SIZE] &= (startmask | endmask);
       return;
     }
@@ -258,7 +269,8 @@
         while (++startWord<middle)
             bits[startWord / PAGE_SIZE][startWord % PAGE_SIZE] = 0L;
     }
-    if (endWord < wlen) {
+    if (endWord < wlen)
+    {
       bits[endWord / PAGE_SIZE][endWord % PAGE_SIZE] &= endmask;
     }
   }
@@ -269,7 +281,8 @@
    * @param startIndex lower index
    * @param endIndex one-past the last bit to clear
    */
-  public void clear(long startIndex, long endIndex) {
+  public void clear(long startIndex, long endIndex)
+  {
     if (endIndex <= startIndex) return;
 
     int startWord = (int)(startIndex>>6);
@@ -286,7 +299,8 @@
     startmask = ~startmask;
     endmask = ~endmask;
 
-    if (startWord == endWord) {
+    if (startWord == endWord)
+{
         bits[startWord / PAGE_SIZE][startWord % PAGE_SIZE] &= (startmask | endmask);
         return;
     }
@@ -302,7 +316,8 @@
         while (++startWord<middle)
             bits[startWord / PAGE_SIZE][startWord % PAGE_SIZE] = 0L;
     }
-    if (endWord < wlen) {
+    if (endWord < wlen)
+    {
         bits[endWord / PAGE_SIZE][endWord % PAGE_SIZE] &= endmask;
     }
   }
@@ -318,7 +333,8 @@
   }
 
   /** this = this AND other */
-  public void intersect(OpenBitSet other) {
+  public void intersect(OpenBitSet other)
+  {
     int newLen= Math.min(this.wlen,other.wlen);
     long[][] thisArr = this.bits;
     long[][] otherArr = other.bits;
@@ -326,11 +342,13 @@
     int otherPageSize = OpenBitSet.PAGE_SIZE;
     // testing against zero can be more efficient
     int pos=newLen;
-    while(--pos>=0) {
+    while(--pos>=0)
+    {
       thisArr[pos / thisPageSize][ pos % thisPageSize] &= otherArr[pos / otherPageSize][pos % otherPageSize];
     }
 
-    if (this.wlen > newLen) {
+    if (this.wlen > newLen)
+    {
       // fill zeros from the new shorter length to the old length
       for (pos=wlen;pos-->newLen;)
           thisArr[pos / thisPageSize][ pos % thisPageSize] =0;
@@ -341,35 +359,42 @@
   // some BitSet compatability methods
 
   //** see {@link intersect} */
-  public void and(OpenBitSet other) {
+  public void and(OpenBitSet other)
+  {
     intersect(other);
   }
 
   /** Lowers numWords, the number of words in use,
    * by checking for trailing zero words.
    */
-  public void trimTrailingZeros() {
+  public void trimTrailingZeros()
+  {
     int idx = wlen-1;
     while (idx>=0 && bits[idx / PAGE_SIZE][idx % PAGE_SIZE]==0) idx--;
     wlen = idx+1;
   }
 
   /** returns the number of 64 bit words it would take to hold numBits */
-  public static long bits2words(long numBits) {
+  public static long bits2words(long numBits)
+  {
    return (((numBits-1)>>>6)+1);
   }
 
   /** returns true if both sets have the same bits set */
   @Override
-  public boolean equals(Object o) {
+  public boolean equals(Object o)
+  {
     if (this == o) return true;
     if (!(o instanceof OpenBitSet)) return false;
     OpenBitSet a;
     OpenBitSet b = (OpenBitSet)o;
     // make a the larger set.
-    if (b.wlen > this.wlen) {
+    if (b.wlen > this.wlen)
+    {
       a = b; b=this;
-    } else {
+    }
+    else
+    {
       a=this;
     }
 
@@ -377,11 +402,13 @@
     int bPageSize = OpenBitSet.PAGE_SIZE;
 
     // check for any set bits out of the range of b
-    for (int i=a.wlen-1; i>=b.wlen; i--) {
+    for (int i=a.wlen-1; i>=b.wlen; i--)
+    {
       if (a.bits[i/aPageSize][i % aPageSize]!=0) return false;
     }
 
-    for (int i=b.wlen-1; i>=0; i--) {
+    for (int i=b.wlen-1; i>=0; i--)
+    {
       if (a.bits[i/aPageSize][i % aPageSize] != b.bits[i/bPageSize][i % bPageSize]) return false;
     }
 
@@ -390,11 +417,13 @@
 
 
   @Override
-  public int hashCode() {
+  public int hashCode()
+  {
     // Start with a zero hash and use a mix that results in zero if the input is zero.
     // This effectively truncates trailing zeros without an explicit check.
     long h = 0;
-    for (int i = wlen; --i>=0;) {
+    for (int i = wlen; --i>=0;)
+    {
       h ^= bits[i / PAGE_SIZE][i % PAGE_SIZE];
       h = (h << 1) | (h >>> 63); // rotate left
     }
@@ -403,31 +432,37 @@
     return (int)((h>>32) ^ h) + 0x98761234;
   }
 
-  public void close() {
+  public void close()
+  {
     // noop, let GC do the cleanup.
   }
 
-  public void serialize(DataOutput out) throws IOException {
+  public void serialize(DataOutput out) throws IOException
+  {
     int bitLength = getNumWords();
     int pageSize = getPageSize();
     int pageCount = getPageCount();
 
     out.writeInt(bitLength);
-    for (int p = 0; p < pageCount; p++) {
+    for (int p = 0; p < pageCount; p++)
+    {
       long[] bits = getPage(p);
-      for (int i = 0; i < pageSize && bitLength-- > 0; i++) {
+      for (int i = 0; i < pageSize && bitLength-- > 0; i++)
+      {
         out.writeLong(bits[i]);
       }
     }
 }
 
-  public long serializedSize() {
+  public long serializedSize()
+  {
     int bitLength = getNumWords();
     int pageSize = getPageSize();
     int pageCount = getPageCount();
 
     long size = TypeSizes.sizeof(bitLength); // length
-    for (int p = 0; p < pageCount; p++) {
+    for (int p = 0; p < pageCount; p++)
+    {
       long[] bits = getPage(p);
       for (int i = 0; i < pageSize && bitLength-- > 0; i++)
         size += TypeSizes.sizeof(bits[i]); // bucket
@@ -435,18 +470,21 @@
     return size;
   }
 
-  public void clear() {
+  public void clear()
+  {
     clear(0, capacity());
   }
 
-  public static OpenBitSet deserialize(DataInput in) throws IOException {
+  public static OpenBitSet deserialize(DataInput in) throws IOException
+  {
     long bitLength = in.readInt();
 
     OpenBitSet bs = new OpenBitSet(bitLength << 6);
     int pageSize = bs.getPageSize();
     int pageCount = bs.getPageCount();
 
-    for (int p = 0; p < pageCount; p++) {
+    for (int p = 0; p < pageCount; p++)
+    {
       long[] bits = bs.getPage(p);
       for (int i = 0; i < pageSize && bitLength-- > 0; i++)
         bits[i] = in.readLong();
diff --git a/src/java/org/apache/cassandra/utils/progress/jmx/LegacyJMXProgressSupport.java b/src/java/org/apache/cassandra/utils/progress/jmx/LegacyJMXProgressSupport.java
index e0439af..3caae38 100644
--- a/src/java/org/apache/cassandra/utils/progress/jmx/LegacyJMXProgressSupport.java
+++ b/src/java/org/apache/cassandra/utils/progress/jmx/LegacyJMXProgressSupport.java
@@ -103,6 +103,6 @@
 
     protected static int getCmd(String tag)
     {
-        return Integer.valueOf(tag.split(":")[1]);
+        return Integer.parseInt(tag.split(":")[1]);
     }
 }
diff --git a/src/java/org/apache/cassandra/utils/vint/VIntCoding.java b/src/java/org/apache/cassandra/utils/vint/VIntCoding.java
index 27448e2..68ebf42 100644
--- a/src/java/org/apache/cassandra/utils/vint/VIntCoding.java
+++ b/src/java/org/apache/cassandra/utils/vint/VIntCoding.java
@@ -51,6 +51,7 @@
 import java.io.IOException;
 import java.nio.ByteBuffer;
 
+import io.netty.util.concurrent.FastThreadLocal;
 import net.nicoulaj.compilecommand.annotations.Inline;
 
 /**
@@ -60,7 +61,8 @@
 public class VIntCoding
 {
 
-    public static long readUnsignedVInt(DataInput input) throws IOException {
+    public static long readUnsignedVInt(DataInput input) throws IOException
+    {
         int firstByte = input.readByte();
 
         //Bail out early if this is one byte, necessary or it fails later
@@ -95,6 +97,9 @@
 
     public static long getUnsignedVInt(ByteBuffer input, int readerIndex, int readerLimit)
     {
+        if (readerIndex < 0)
+            throw new IllegalArgumentException("Reader index should be non-negative, but was " + readerIndex);
+
         if (readerIndex >= readerLimit)
             return -1;
 
@@ -119,7 +124,8 @@
         return retval;
     }
 
-    public static long readVInt(DataInput input) throws IOException {
+    public static long readVInt(DataInput input) throws IOException
+    {
         return decodeZigZag64(readUnsignedVInt(input));
     }
 
@@ -144,7 +150,7 @@
         return Integer.numberOfLeadingZeros(~firstByte) - 24;
     }
 
-    protected static final ThreadLocal<byte[]> encodingBuffer = new ThreadLocal<byte[]>()
+    protected static final FastThreadLocal<byte[]> encodingBuffer = new FastThreadLocal<byte[]>()
     {
         @Override
         public byte[] initialValue()
@@ -153,7 +159,8 @@
         }
     };
 
-    public static void writeUnsignedVInt(long value, DataOutput output) throws IOException {
+    public static void writeUnsignedVInt(long value, DataOutput output) throws IOException
+    {
         int size = VIntCoding.computeUnsignedVIntSize(value);
         if (size == 1)
         {
@@ -165,7 +172,8 @@
     }
 
     @Inline
-    public static byte[] encodeVInt(long value, int size) {
+    public static byte[] encodeVInt(long value, int size)
+    {
         byte encodingSpace[] = encodingBuffer.get();
         int extraBytes = size - 1;
 
@@ -178,7 +186,8 @@
         return encodingSpace;
     }
 
-    public static void writeVInt(long value, DataOutput output) throws IOException {
+    public static void writeVInt(long value, DataOutput output) throws IOException
+    {
         writeUnsignedVInt(encodeZigZag64(value), output);
     }
 
@@ -192,7 +201,8 @@
      *          Java has no explicit unsigned support.
      * @return A signed 64-bit integer.
      */
-    public static long decodeZigZag64(final long n) {
+    public static long decodeZigZag64(final long n)
+    {
         return (n >>> 1) ^ -(n & 1);
     }
 
@@ -206,18 +216,21 @@
      * @return An unsigned 64-bit integer, stored in a signed int because
      *         Java has no explicit unsigned support.
      */
-    public static long encodeZigZag64(final long n) {
+    public static long encodeZigZag64(final long n)
+    {
         // Note:  the right-shift must be arithmetic
         return (n << 1) ^ (n >> 63);
     }
 
     /** Compute the number of bytes that would be needed to encode a varint. */
-    public static int computeVIntSize(final long param) {
+    public static int computeVIntSize(final long param)
+    {
         return computeUnsignedVIntSize(encodeZigZag64(param));
     }
 
     /** Compute the number of bytes that would be needed to encode an unsigned varint. */
-    public static int computeUnsignedVIntSize(final long value) {
+    public static int computeUnsignedVIntSize(final long value)
+    {
         int magnitude = Long.numberOfLeadingZeros(value | 1); // | with 1 to ensure magntiude <= 63, so (63 - 1) / 7 <= 8
         return 9 - ((magnitude - 1) / 7);
     }
diff --git a/src/resources/org/apache/cassandra/cql3/functions/JavaSourceUDF.txt b/src/resources/org/apache/cassandra/cql3/functions/JavaSourceUDF.txt
index 4bd3601..5dafd98 100644
--- a/src/resources/org/apache/cassandra/cql3/functions/JavaSourceUDF.txt
+++ b/src/resources/org/apache/cassandra/cql3/functions/JavaSourceUDF.txt
@@ -1,20 +1,24 @@
 package #package_name#;
 
 import java.nio.ByteBuffer;
-import java.util.List;
+import java.util.*;
 
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 import com.datastax.driver.core.TypeCodec;
+import com.datastax.driver.core.TupleValue;
+import com.datastax.driver.core.UDTValue;
 
 public final class #class_name# extends JavaUDF
 {
-    public #class_name#(TypeCodec<Object> returnCodec, TypeCodec<Object>[] argCodecs)
+    public #class_name#(TypeCodec<Object> returnCodec, TypeCodec<Object>[] argCodecs, UDFContext udfContext)
     {
-        super(returnCodec, argCodecs);
+        super(returnCodec, argCodecs, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         #return_type# result = #execute_internal_name#(
 #arguments#
@@ -22,6 +26,14 @@
         return super.decompose(protocolVersion, result);
     }
 
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        #return_type# result = #execute_internal_name#(
+#arguments_aggregate#
+        );
+        return result;
+    }
+
     private #return_type# #execute_internal_name#(#argument_list#)
     {
 #body#
diff --git a/src/resources/org/apache/cassandra/cql3/reserved_keywords.txt b/src/resources/org/apache/cassandra/cql3/reserved_keywords.txt
new file mode 100644
index 0000000..51abcab
--- /dev/null
+++ b/src/resources/org/apache/cassandra/cql3/reserved_keywords.txt
@@ -0,0 +1,62 @@
+ADD
+ALLOW
+ALTER
+AND
+APPLY
+ASC
+AUTHORIZE
+BATCH
+BEGIN
+BY
+COLUMNFAMILY
+CREATE
+DEFAULT
+DELETE
+DESC
+DESCRIBE
+DROP
+ENTRIES
+EXECUTE
+FROM
+FULL
+GRANT
+IF
+IN
+INDEX
+INFINITY
+INSERT
+INTO
+IS
+KEYSPACE
+LIMIT
+MATERIALIZED
+MBEAN
+MBEANS
+MODIFY
+NAN
+NORECURSIVE
+NOT
+NULL
+OF
+ON
+OR
+ORDER
+PRIMARY
+RENAME
+REPLACE
+REVOKE
+SCHEMA
+SELECT
+SET
+TABLE
+TO
+TOKEN
+TRUNCATE
+UNLOGGED
+UNSET
+UPDATE
+USE
+USING
+VIEW
+WHERE
+WITH
diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/ar_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/ar_ST.txt
new file mode 100644
index 0000000..97bedb6
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/ar_ST.txt
@@ -0,0 +1,163 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html




+عشر
+عدد
+عدة
+عشرة
+عدم
+عام
+عاما
+عن
+عند
+عندما
+على
+عليه
+عليها
+زيارة
+سنة
+سنوات
+تم
+ضد
+بعد
+بعض
+اعادة
+اعلنت
+بسبب
+حتى
+اذا
+احد
+اثر
+برس
+باسم
+غدا
+شخصا
+صباح
+اطار
+اربعة
+اخرى
+بان
+اجل
+غير
+بشكل
+حاليا
+بن
+به
+ثم
+اف
+ان
+او
+اي
+بها
+صفر
+حيث
+اكد
+الا
+اما
+امس
+السابق
+التى
+التي
+اكثر
+ايار
+ايضا
+ثلاثة
+الذاتي
+الاخيرة
+الثاني
+الثانية
+الذى
+الذي
+الان
+امام
+ايام
+خلال
+حوالى
+الذين
+الاول
+الاولى
+بين
+ذلك
+دون
+حول
+حين
+الف
+الى
+انه
+اول
+ضمن
+انها
+جميع
+الماضي
+الوقت
+المقبل
+اليوم



+و6
+قد
+لا
+ما
+مع
+مساء
+هذا
+واحد
+واضاف
+واضافت
+فان
+قبل
+قال
+كان
+لدى
+نحو
+هذه
+وان
+واكد
+كانت
+واوضح
+مايو
+فى
+في
+كل
+لم
+لن
+له
+من
+هو
+هي
+قوة
+كما
+لها
+منذ
+وقد
+ولا
+نفسه
+لقاء
+مقابل
+هناك
+وقال
+وكان
+نهاية
+وقالت
+وكانت
+للامم
+فيه
+كلم
+لكن
+وفي
+وقف
+ولم
+ومن
+وهو
+وهي
+يوم
+فيها
+منها
+مليار
+لوكالة
+يكون
+يمكن
+مليون
diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/bg_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/bg_ST.txt
new file mode 100644
index 0000000..ed6049d
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/bg_ST.txt
@@ -0,0 +1,260 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html

+автентичен
+аз
+ако
+ала
+бе
+без
+беше
+би
+бивш
+бивша
+бившо
+бил
+била
+били
+било
+благодаря
+близо
+бъдат
+бъде
+бяха

+вас
+ваш
+ваша
+вероятно
+вече
+взема
+ви
+вие
+винаги
+внимава
+време
+все
+всеки
+всички
+всичко
+всяка
+във
+въпреки
+върху

+ги
+главен
+главна
+главно
+глас
+го
+година
+години
+годишен

+да
+дали
+два
+двама
+двамата
+две
+двете
+ден
+днес
+дни
+до
+добра
+добре
+добро
+добър
+докато
+докога
+дори
+досега
+доста
+друг
+друга
+други

+евтин
+едва
+един
+една
+еднаква
+еднакви
+еднакъв
+едно
+екип
+ето
+живот
+за
+забавям
+зад
+заедно
+заради
+засега
+заспал
+затова
+защо
+защото

+из
+или
+им
+има
+имат
+иска

+каза
+как
+каква
+какво
+както
+какъв
+като
+кога
+когато
+което
+които
+кой
+който
+колко
+която
+къде
+където
+към
+лесен
+лесно
+ли
+лош

+май
+малко
+ме
+между
+мек
+мен
+месец
+ми
+много
+мнозина
+мога
+могат
+може
+мокър
+моля
+момента
+му

+на
+над
+назад
+най
+направи
+напред
+например
+нас
+не
+него
+нещо
+нея
+ни
+ние
+никой
+нито
+нищо
+но
+нов
+нова
+нови
+новина
+някои
+някой
+няколко
+няма
+обаче
+около
+освен
+особено
+от
+отгоре
+отново
+още
+пак
+по
+повече
+повечето
+под
+поне
+поради
+после
+почти
+прави
+пред
+преди
+през
+при
+пък
+първата
+първи
+първо
+пъти
+равен
+равна

+са
+сам
+само
+се
+сега
+си
+син
+скоро
+след
+следващ
+сме
+смях
+според
+сред
+срещу
+сте
+съм
+със
+също

+тази
+така
+такива
+такъв
+там
+твой
+те
+тези
+ти
+т.н.
+то
+това
+тогава
+този
+той
+толкова
+точно
+три
+трябва
+тук
+тъй
+тя
+тях

+утре
+харесва
+хиляди

+часа
+че
+често
+чрез
+ще
+щом
+юмрук

+як
\ No newline at end of file
diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/cs_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/cs_ST.txt
new file mode 100644
index 0000000..49b52e1
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/cs_ST.txt
@@ -0,0 +1,257 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html
+ačkoli
+ahoj
+ale
+anebo
+ano
+asi
+aspoň
+během
+bez
+beze
+blízko
+bohužel
+brzo
+bude
+budeme
+budeš
+budete
+budou
+budu
+byl
+byla
+byli
+bylo
+byly
+bys
+čau
+chce
+chceme
+chceš
+chcete
+chci
+chtějí
+chtít
+chut'
+chuti
+co
+čtrnáct
+čtyři
+dál
+dále
+daleko
+děkovat
+děkujeme
+děkuji
+den
+deset
+devatenáct
+devět
+do
+dobrý
+docela
+dva
+dvacet
+dvanáct
+dvě
+hodně
+já
+jak
+jde
+je
+jeden
+jedenáct
+jedna
+jedno
+jednou
+jedou
+jeho
+její
+jejich
+jemu
+jen
+jenom
+ještě
+jestli
+jestliže
+jí
+jich
+jím
+jimi
+jinak
+jsem
+jsi
+jsme
+jsou
+jste
+kam
+kde
+kdo
+kdy
+když
+ke
+kolik
+kromě
+která
+které
+kteří
+který
+kvůli
+má
+mají
+málo
+mám
+máme
+máš
+máte
+mé
+mě
+mezi
+mí
+mít
+mně
+mnou
+moc
+mohl
+mohou
+moje
+moji
+možná
+můj
+musí
+může
+my
+na
+nad
+nade
+nám
+námi
+naproti
+nás
+náš
+naše
+naši
+ne
+ně
+nebo
+nebyl
+nebyla
+nebyli
+nebyly
+něco
+nedělá
+nedělají
+nedělám
+neděláme
+neděláš
+neděláte
+nějak
+nejsi
+někde
+někdo
+nemají
+nemáme
+nemáte
+neměl
+němu
+není
+nestačí
+nevadí
+než
+nic
+nich
+ním
+nimi
+nula
+od
+ode
+on
+ona
+oni
+ono
+ony
+osm
+osmnáct
+pak
+patnáct
+pět
+po
+pořád
+potom
+pozdě
+před
+přes
+přese
+pro
+proč
+prosím
+prostě
+proti
+protože
+rovně
+se
+sedm
+sedmnáct
+šest
+šestnáct
+skoro
+smějí
+smí
+snad
+spolu
+sta
+sté
+sto
+ta
+tady
+tak
+takhle
+taky
+tam
+tamhle
+tamhleto
+tamto
+tě
+tebe
+tebou
+ted'
+tedy
+ten
+ti
+tisíc
+tisíce
+to
+tobě
+tohle
+toto
+třeba
+tři
+třináct
+trošku
+tvá
+tvé
+tvoje
+tvůj
+ty
+určitě
+už
+vám
+vámi
+vás
+váš
+vaše
+vaši
+ve
+večer
+vedle
+vlastně
+všechno
+všichni
+vůbec
+vy
+vždy
+za
+zač
+zatímco
+ze
+že
diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/de_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/de_ST.txt
new file mode 100644
index 0000000..747e682
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/de_ST.txt
@@ -0,0 +1,604 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html

+a

+ab

+aber

+aber

+ach

+acht

+achte

+achten

+achter

+achtes

+ag

+alle

+allein

+allem

+allen

+aller

+allerdings

+alles

+allgemeinen

+als

+als

+also

+am

+an

+andere

+anderen

+andern

+anders

+au

+auch

+auch

+auf

+aus

+ausser

+au�er

+ausserdem

+au�erdem

+b

+bald

+bei

+beide

+beiden

+beim

+beispiel

+bekannt

+bereits

+besonders

+besser

+besten

+bin

+bis

+bisher

+bist

+c

+d

+da

+dabei

+dadurch

+daf�r

+dagegen

+daher

+dahin

+dahinter

+damals

+damit

+danach

+daneben

+dank

+dann

+daran

+darauf

+daraus

+darf

+darfst

+darin

+dar�ber

+darum

+darunter

+das

+das

+dasein

+daselbst

+dass

+da�

+dasselbe

+davon

+davor

+dazu

+dazwischen

+dein

+deine

+deinem

+deiner

+dem

+dementsprechend

+demgegen�ber

+demgem�ss

+demgem��

+demselben

+demzufolge

+den

+denen

+denn

+denn

+denselben

+der

+deren

+derjenige

+derjenigen

+dermassen

+derma�en

+derselbe

+derselben

+des

+deshalb

+desselben

+dessen

+deswegen

+d.h

+dich

+die

+diejenige

+diejenigen

+dies

+diese

+dieselbe

+dieselben

+diesem

+diesen

+dieser

+dieses

+dir

+doch

+dort

+drei

+drin

+dritte

+dritten

+dritter

+drittes

+du

+durch

+durchaus

+d�rfen

+d�rft

+durfte

+durften

+e

+eben

+ebenso

+ehrlich

+ei

+ei,

+ei,

+eigen

+eigene

+eigenen

+eigener

+eigenes

+ein

+einander

+eine

+einem

+einen

+einer

+eines

+einige

+einigen

+einiger

+einiges

+einmal

+einmal

+eins

+elf

+en

+ende

+endlich

+entweder

+entweder

+er

+Ernst

+erst

+erste

+ersten

+erster

+erstes

+es

+etwa

+etwas

+euch

+f

+fr�her

+f�nf

+f�nfte

+f�nften

+f�nfter

+f�nftes

+f�r

+g

+gab

+ganz

+ganze

+ganzen

+ganzer

+ganzes

+gar

+gedurft

+gegen

+gegen�ber

+gehabt

+gehen

+geht

+gekannt

+gekonnt

+gemacht

+gemocht

+gemusst

+genug

+gerade

+gern

+gesagt

+gesagt

+geschweige

+gewesen

+gewollt

+geworden

+gibt

+ging

+gleich

+gott

+gross

+gro�

+grosse

+gro�e

+grossen

+gro�en

+grosser

+gro�er

+grosses

+gro�es

+gut

+gute

+guter

+gutes

+h

+habe

+haben

+habt

+hast

+hat

+hatte

+h�tte

+hatten

+h�tten

+heisst

+her

+heute

+hier

+hin

+hinter

+hoch

+i

+ich

+ihm

+ihn

+ihnen

+ihr

+ihre

+ihrem

+ihren

+ihrer

+ihres

+im

+im

+immer

+in

+in

+indem

+infolgedessen

+ins

+irgend

+ist

+j

+ja

+ja

+jahr

+jahre

+jahren

+je

+jede

+jedem

+jeden

+jeder

+jedermann

+jedermanns

+jedoch

+jemand

+jemandem

+jemanden

+jene

+jenem

+jenen

+jener

+jenes

+jetzt

+k

+kam

+kann

+kannst

+kaum

+kein

+keine

+keinem

+keinen

+keiner

+kleine

+kleinen

+kleiner

+kleines

+kommen

+kommt

+k�nnen

+k�nnt

+konnte

+k�nnte

+konnten

+kurz

+l

+lang

+lange

+lange

+leicht

+leide

+lieber

+los

+m

+machen

+macht

+machte

+mag

+magst

+mahn

+man

+manche

+manchem

+manchen

+mancher

+manches

+mann

+mehr

+mein

+meine

+meinem

+meinen

+meiner

+meines

+mensch

+menschen

+mich

+mir

+mit

+mittel

+mochte

+m�chte

+mochten

+m�gen

+m�glich

+m�gt

+morgen

+muss

+mu�

+m�ssen

+musst

+m�sst

+musste

+mussten

+n

+na

+nach

+nachdem

+nahm

+nat�rlich

+neben

+nein

+neue

+neuen

+neun

+neunte

+neunten

+neunter

+neuntes

+nicht

+nicht

+nichts

+nie

+niemand

+niemandem

+niemanden

+noch

+nun

+nun

+nur

+o

+ob

+ob

+oben

+oder

+oder

+offen

+oft

+oft

+ohne

+Ordnung

+p

+q

+r

+recht

+rechte

+rechten

+rechter

+rechtes

+richtig

+rund

+s

+sa

+sache

+sagt

+sagte

+sah

+satt

+schlecht

+Schluss

+schon

+sechs

+sechste

+sechsten

+sechster

+sechstes

+sehr

+sei

+sei

+seid

+seien

+sein

+seine

+seinem

+seinen

+seiner

+seines

+seit

+seitdem

+selbst

+selbst

+sich

+sie

+sieben

+siebente

+siebenten

+siebenter

+siebentes

+sind

+so

+solang

+solche

+solchem

+solchen

+solcher

+solches

+soll

+sollen

+sollte

+sollten

+sondern

+sonst

+sowie

+sp�ter

+statt

+t

+tag

+tage

+tagen

+tat

+teil

+tel

+tritt

+trotzdem

+tun

+u

+�ber

+�berhaupt

+�brigens

+uhr

+um

+und

+und?

+uns

+unser

+unsere

+unserer

+unter

+v

+vergangenen

+viel

+viele

+vielem

+vielen

+vielleicht

+vier

+vierte

+vierten

+vierter

+viertes

+vom

+von

+vor

+w

+wahr?

+w�hrend

+w�hrenddem

+w�hrenddessen

+wann

+war

+w�re

+waren

+wart

+warum

+was

+wegen

+weil

+weit

+weiter

+weitere

+weiteren

+weiteres

+welche

+welchem

+welchen

+welcher

+welches

+wem

+wen

+wenig

+wenig

+wenige

+weniger

+weniges

+wenigstens

+wenn

+wenn

+wer

+werde

+werden

+werdet

+wessen

+wie

+wie

+wieder

+will

+willst

+wir

+wird

+wirklich

+wirst

+wo

+wohl

+wollen

+wollt

+wollte

+wollten

+worden

+wurde

+w�rde

+wurden

+w�rden

+x

+y

+z

+z.b

+zehn

+zehnte

+zehnten

+zehnter

+zehntes

+zeit

+zu

+zuerst

+zugleich

+zum

+zum

+zun�chst

+zur

+zur�ck

+zusammen

+zwanzig

+zwar

+zwar

+zwei

+zweite

+zweiten

+zweiter

+zweites

+zwischen

+zw�lf

diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/en_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/en_ST.txt
new file mode 100644
index 0000000..d30da31
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/en_ST.txt
@@ -0,0 +1,572 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html

+a

+a's

+able

+about

+above

+according

+accordingly

+across

+actually

+after

+afterwards

+again

+against

+ain't

+all

+allow

+allows

+almost

+alone

+along

+already

+also

+although

+always

+am

+among

+amongst

+an

+and

+another

+any

+anybody

+anyhow

+anyone

+anything

+anyway

+anyways

+anywhere

+apart

+appear

+appreciate

+appropriate

+are

+aren't

+around

+as

+aside

+ask

+asking

+associated

+at

+available

+away

+awfully

+b

+be

+became

+because

+become

+becomes

+becoming

+been

+before

+beforehand

+behind

+being

+believe

+below

+beside

+besides

+best

+better

+between

+beyond

+both

+brief

+but

+by

+c

+c'mon

+c's

+came

+can

+can't

+cannot

+cant

+cause

+causes

+certain

+certainly

+changes

+clearly

+co

+com

+come

+comes

+concerning

+consequently

+consider

+considering

+contain

+containing

+contains

+corresponding

+could

+couldn't

+course

+currently

+d

+definitely

+described

+despite

+did

+didn't

+different

+do

+does

+doesn't

+doing

+don't

+done

+down

+downwards

+during

+e

+each

+edu

+eg

+eight

+either

+else

+elsewhere

+enough

+entirely

+especially

+et

+etc

+even

+ever

+every

+everybody

+everyone

+everything

+everywhere

+ex

+exactly

+example

+except

+f

+far

+few

+fifth

+first

+five

+followed

+following

+follows

+for

+former

+formerly

+forth

+four

+from

+further

+furthermore

+g

+get

+gets

+getting

+given

+gives

+go

+goes

+going

+gone

+got

+gotten

+greetings

+h

+had

+hadn't

+happens

+hardly

+has

+hasn't

+have

+haven't

+having

+he

+he's

+hello

+help

+hence

+her

+here

+here's

+hereafter

+hereby

+herein

+hereupon

+hers

+herself

+hi

+him

+himself

+his

+hither

+hopefully

+how

+howbeit

+however

+i

+i'd

+i'll

+i'm

+i've

+ie

+if

+ignored

+immediate

+in

+inasmuch

+inc

+indeed

+indicate

+indicated

+indicates

+inner

+insofar

+instead

+into

+inward

+is

+isn't

+it

+it'd

+it'll

+it's

+its

+itself

+j

+just

+k

+keep

+keeps

+kept

+know

+knows

+known

+l

+last

+lately

+later

+latter

+latterly

+least

+less

+lest

+let

+let's

+like

+liked

+likely

+little

+look

+looking

+looks

+ltd

+m

+mainly

+many

+may

+maybe

+me

+mean

+meanwhile

+merely

+might

+more

+moreover

+most

+mostly

+much

+must

+my

+myself

+n

+name

+namely

+nd

+near

+nearly

+necessary

+need

+needs

+neither

+never

+nevertheless

+new

+next

+nine

+no

+nobody

+non

+none

+noone

+nor

+normally

+not

+nothing

+novel

+now

+nowhere

+o

+obviously

+of

+off

+often

+oh

+ok

+okay

+old

+on

+once

+one

+ones

+only

+onto

+or

+other

+others

+otherwise

+ought

+our

+ours

+ourselves

+out

+outside

+over

+overall

+own

+p

+particular

+particularly

+per

+perhaps

+placed

+please

+plus

+possible

+presumably

+probably

+provides

+q

+que

+quite

+qv

+r

+rather

+rd

+re

+really

+reasonably

+regarding

+regardless

+regards

+relatively

+respectively

+right

+s

+said

+same

+saw

+say

+saying

+says

+second

+secondly

+see

+seeing

+seem

+seemed

+seeming

+seems

+seen

+self

+selves

+sensible

+sent

+serious

+seriously

+seven

+several

+shall

+she

+should

+shouldn't

+since

+six

+so

+some

+somebody

+somehow

+someone

+something

+sometime

+sometimes

+somewhat

+somewhere

+soon

+sorry

+specified

+specify

+specifying

+still

+sub

+such

+sup

+sure

+t

+t's

+take

+taken

+tell

+tends

+th

+than

+thank

+thanks

+thanx

+that

+that's

+thats

+the

+their

+theirs

+them

+themselves

+then

+thence

+there

+there's

+thereafter

+thereby

+therefore

+therein

+theres

+thereupon

+these

+they

+they'd

+they'll

+they're

+they've

+think

+third

+this

+thorough

+thoroughly

+those

+though

+three

+through

+throughout

+thru

+thus

+to

+together

+too

+took

+toward

+towards

+tried

+tries

+truly

+try

+trying

+twice

+two

+u

+un

+under

+unfortunately

+unless

+unlikely

+until

+unto

+up

+upon

+us

+use

+used

+useful

+uses

+using

+usually

+uucp

+v

+value

+various

+very

+via

+viz

+vs

+w

+want

+wants

+was

+wasn't

+way

+we

+we'd

+we'll

+we're

+we've

+welcome

+well

+went

+were

+weren't

+what

+what's

+whatever

+when

+whence

+whenever

+where

+where's

+whereafter

+whereas

+whereby

+wherein

+whereupon

+wherever

+whether

+which

+while

+whither

+who

+who's

+whoever

+whole

+whom

+whose

+why

+will

+willing

+wish

+with

+within

+without

+won't

+wonder

+would

+would

+wouldn't

+x

+y

+yes

+yet

+you

+you'd

+you'll

+you're

+you've

+your

+yours

+yourself

+yourselves

+z

+zero

diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/es_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/es_ST.txt
new file mode 100644
index 0000000..75e2086
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/es_ST.txt
@@ -0,0 +1,308 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html

+a

+acuerdo

+adelante

+ademas

+adem�s

+adrede

+ahi

+ah�

+ahora

+al

+alli

+all�

+alrededor

+antano

+anta�o

+ante

+antes

+apenas

+aproximadamente

+aquel

+aqu�l

+aquella

+aqu�lla

+aquellas

+aqu�llas

+aquello

+aquellos

+aqu�llos

+aqui

+aqu�

+arribaabajo

+asi

+as�

+aun

+a�n

+aunque

+b

+bajo

+bastante

+bien

+breve

+c

+casi

+cerca

+claro

+como

+c�mo

+con

+conmigo

+contigo

+contra

+cual

+cu�l

+cuales

+cu�les

+cuando

+cu�ndo

+cuanta

+cu�nta

+cuantas

+cu�ntas

+cuanto

+cu�nto

+cuantos

+cu�ntos

+d

+de

+debajo

+del

+delante

+demasiado

+dentro

+deprisa

+desde

+despacio

+despues

+despu�s

+detras

+detr�s

+dia

+d�a

+dias

+d�as

+donde

+d�nde

+dos

+durante

+e

+el

+�l

+ella

+ellas

+ellos

+en

+encima

+enfrente

+enseguida

+entre

+es

+esa

+�sa

+esas

+�sas

+ese

+�se

+eso

+esos

+�sos

+esta

+est�

+�sta

+estado

+estados

+estan

+est�n

+estar

+estas

+�stas

+este

+�ste

+esto

+estos

+�stos

+ex

+excepto

+f

+final

+fue

+fuera

+fueron

+g

+general

+gran

+h

+ha

+habia

+hab�a

+habla

+hablan

+hace

+hacia

+han

+hasta

+hay

+horas

+hoy

+i

+incluso

+informo

+inform�

+j

+junto

+k

+l

+la

+lado

+las

+le

+lejos

+lo

+los

+luego

+m

+mal

+mas

+m�s

+mayor

+me

+medio

+mejor

+menos

+menudo

+mi

+m�

+mia

+m�a

+mias

+m�as

+mientras

+mio

+m�o

+mios

+m�os

+mis

+mismo

+mucho

+muy

+n

+nada

+nadie

+ninguna

+no

+nos

+nosotras

+nosotros

+nuestra

+nuestras

+nuestro

+nuestros

+nueva

+nuevo

+nunca

+o

+os

+otra

+otros

+p

+pais

+pa�s

+para

+parte

+pasado

+peor

+pero

+poco

+por

+porque

+pronto

+proximo

+pr�ximo

+puede

+q

+qeu

+que

+qu�

+quien

+qui�n

+quienes

+qui�nes

+quiza

+quiz�

+quizas

+quiz�s

+r

+raras

+repente

+s

+salvo

+se

+s�

+segun

+seg�n

+ser

+sera

+ser�

+si

+s�

+sido

+siempre

+sin

+sobre

+solamente

+solo

+s�lo

+son

+soyos

+su

+supuesto

+sus

+suya

+suyas

+suyo

+t

+tal

+tambien

+tambi�n

+tampoco

+tarde

+te

+temprano

+ti

+tiene

+todavia

+todav�a

+todo

+todos

+tras

+tu

+t�

+tus

+tuya

+tuyas

+tuyo

+tuyos

+u

+un

+una

+unas

+uno

+unos

+usted

+ustedes

+v

+veces

+vez

+vosotras

+vosotros

+vuestra

+vuestras

+vuestro

+vuestros

+w

+x

+y

+ya

+yo

+z

diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/fi_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/fi_ST.txt
new file mode 100644
index 0000000..3c8bfd5
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/fi_ST.txt
@@ -0,0 +1,748 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html

+aiemmin

+aika

+aikaa

+aikaan

+aikaisemmin

+aikaisin

+aikajen

+aikana

+aikoina

+aikoo

+aikovat

+aina

+ainakaan

+ainakin

+ainoa

+ainoat

+aiomme

+aion

+aiotte

+aist

+aivan

+ajan

+�l�

+alas

+alemmas

+�lk��n

+alkuisin

+alkuun

+alla

+alle

+aloitamme

+aloitan

+aloitat

+aloitatte

+aloitattivat

+aloitettava

+aloitettevaksi

+aloitettu

+aloitimme

+aloitin

+aloitit

+aloititte

+aloittaa

+aloittamatta

+aloitti

+aloittivat

+alta

+aluksi

+alussa

+alusta

+annettavaksi

+annetteva

+annettu

+antaa

+antamatta

+antoi

+aoua

+apu

+asia

+asiaa

+asian

+asiasta

+asiat

+asioiden

+asioihin

+asioita

+asti

+avuksi

+avulla

+avun

+avutta

+edell�

+edelle

+edelleen

+edelt�

+edemm�s

+edes

+edess�

+edest�

+ehk�

+ei

+eik�

+eilen

+eiv�t

+eli

+ellei

+elleiv�t

+ellemme

+ellen

+ellet

+ellette

+emme

+en

+en��

+enemm�n

+eniten

+ennen

+ensi

+ensimm�inen

+ensimm�iseksi

+ensimm�isen

+ensimm�isen�

+ensimm�iset

+ensimm�isi�

+ensimm�isiksi

+ensimm�isin�

+ensimm�ist�

+ensin

+entinen

+entisen

+entisi�

+entist�

+entisten

+er��t

+er�iden

+er�s

+eri

+eritt�in

+erityisesti

+esi

+esiin

+esill�

+esimerkiksi

+et

+eteen

+etenkin

+ett�

+ette

+ettei

+halua

+haluaa

+haluamatta

+haluamme

+haluan

+haluat

+haluatte

+haluavat

+halunnut

+halusi

+halusimme

+halusin

+halusit

+halusitte

+halusivat

+halutessa

+haluton

+h�n

+h�neen

+h�nell�

+h�nelle

+h�nelt�

+h�nen

+h�ness�

+h�nest�

+h�net

+he

+hei

+heid�n

+heihin

+heille

+heilt�

+heiss�

+heist�

+heit�

+helposti

+heti

+hetkell�

+hieman

+huolimatta

+huomenna

+hyv�

+hyv��

+hyv�t

+hyvi�

+hyvien

+hyviin

+hyviksi

+hyville

+hyvilt�

+hyvin

+hyvin�

+hyviss�

+hyvist�

+ihan

+ilman

+ilmeisesti

+itse

+itse��n

+itsens�

+ja

+j��

+j�lkeen

+j�lleen

+jo

+johon

+joiden

+joihin

+joiksi

+joilla

+joille

+joilta

+joissa

+joista

+joita

+joka

+jokainen

+jokin

+joko

+joku

+jolla

+jolle

+jolloin

+jolta

+jompikumpi

+jonka

+jonkin

+jonne

+joo

+jopa

+jos

+joskus

+jossa

+josta

+jota

+jotain

+joten

+jotenkin

+jotenkuten

+jotka

+jotta

+jouduimme

+jouduin

+jouduit

+jouduitte

+joudumme

+joudun

+joudutte

+joukkoon

+joukossa

+joukosta

+joutua

+joutui

+joutuivat

+joutumaan

+joutuu

+joutuvat

+juuri

+kahdeksan

+kahdeksannen

+kahdella

+kahdelle

+kahdelta

+kahden

+kahdessa

+kahdesta

+kahta

+kahteen

+kai

+kaiken

+kaikille

+kaikilta

+kaikkea

+kaikki

+kaikkia

+kaikkiaan

+kaikkialla

+kaikkialle

+kaikkialta

+kaikkien

+kaikkin

+kaksi

+kannalta

+kannattaa

+kanssa

+kanssaan

+kanssamme

+kanssani

+kanssanne

+kanssasi

+kauan

+kauemmas

+kautta

+kehen

+keiden

+keihin

+keiksi

+keill�

+keille

+keilt�

+kein�

+keiss�

+keist�

+keit�

+keitt�

+keitten

+keneen

+keneksi

+kenell�

+kenelle

+kenelt�

+kenen

+kenen�

+keness�

+kenest�

+kenet

+kenett�

+kenness�st�

+kerran

+kerta

+kertaa

+kesken

+keskim��rin

+ket�

+ketk�

+kiitos

+kohti

+koko

+kokonaan

+kolmas

+kolme

+kolmen

+kolmesti

+koska

+koskaan

+kovin

+kuin

+kuinka

+kuitenkaan

+kuitenkin

+kuka

+kukaan

+kukin

+kumpainen

+kumpainenkaan

+kumpi

+kumpikaan

+kumpikin

+kun

+kuten

+kuuden

+kuusi

+kuutta

+kyll�

+kymmenen

+kyse

+l�hekk�in

+l�hell�

+l�helle

+l�helt�

+l�hemm�s

+l�hes

+l�hinn�

+l�htien

+l�pi

+liian

+liki

+lis��

+lis�ksi

+luo

+mahdollisimman

+mahdollista

+me

+meid�n

+meill�

+meille

+melkein

+melko

+menee

+meneet

+menemme

+menen

+menet

+menette

+menev�t

+meni

+menimme

+menin

+menit

+meniv�t

+menness�

+mennyt

+menossa

+mihin

+mik�

+mik��n

+mik�li

+mikin

+miksi

+milloin

+min�

+minne

+minun

+minut

+miss�

+mist�

+mit�

+mit��n

+miten

+moi

+molemmat

+mones

+monesti

+monet

+moni

+moniaalla

+moniaalle

+moniaalta

+monta

+muassa

+muiden

+muita

+muka

+mukaan

+mukaansa

+mukana

+mutta

+muu

+muualla

+muualle

+muualta

+muuanne

+muulloin

+muun

+muut

+muuta

+muutama

+muutaman

+muuten

+my�hemmin

+my�s

+my�sk��n

+my�skin

+my�t�

+n�iden

+n�in

+n�iss�

+n�iss�hin

+n�iss�lle

+n�iss�lt�

+n�iss�st�

+n�it�

+n�m�

+ne

+nelj�

+nelj��

+nelj�n

+niiden

+niin

+niist�

+niit�

+noin

+nopeammin

+nopeasti

+nopeiten

+nro

+nuo

+nyt

+ohi

+oikein

+ole

+olemme

+olen

+olet

+olette

+oleva

+olevan

+olevat

+oli

+olimme

+olin

+olisi

+olisimme

+olisin

+olisit

+olisitte

+olisivat

+olit

+olitte

+olivat

+olla

+olleet

+olli

+ollut

+oma

+omaa

+omaan

+omaksi

+omalle

+omalta

+oman

+omassa

+omat

+omia

+omien

+omiin

+omiksi

+omille

+omilta

+omissa

+omista

+on

+onkin

+onko

+ovat

+p��lle

+paikoittain

+paitsi

+pakosti

+paljon

+paremmin

+parempi

+parhaillaan

+parhaiten

+per�ti

+perusteella

+pian

+pieneen

+pieneksi

+pienell�

+pienelle

+pienelt�

+pienempi

+pienest�

+pieni

+pienin

+puolesta

+puolestaan

+runsaasti

+saakka

+sadam

+sama

+samaa

+samaan

+samalla

+samallalta

+samallassa

+samallasta

+saman

+samat

+samoin

+sata

+sataa

+satojen

+se

+seitsem�n

+sek�

+sen

+seuraavat

+siell�

+sielt�

+siihen

+siin�

+siis

+siit�

+sijaan

+siksi

+sill�

+silloin

+silti

+sin�

+sinne

+sinua

+sinulle

+sinulta

+sinun

+sinussa

+sinusta

+sinut

+sis�kk�in

+sis�ll�

+sit�

+siten

+sitten

+suoraan

+suuntaan

+suuren

+suuret

+suuri

+suuria

+suurin

+suurten

+taa

+t��ll�

+t��lt�

+taas

+taemmas

+t�h�n

+tahansa

+tai

+takaa

+takaisin

+takana

+takia

+t�ll�

+t�ll�in

+t�m�

+t�m�n

+t�n�

+t�n��n

+t�nne

+tapauksessa

+t�ss�

+t�st�

+t�t�

+t�ten

+tavalla

+tavoitteena

+t�ysin

+t�ytyv�t

+t�ytyy

+te

+tietysti

+todella

+toinen

+toisaalla

+toisaalle

+toisaalta

+toiseen

+toiseksi

+toisella

+toiselle

+toiselta

+toisemme

+toisen

+toisensa

+toisessa

+toisesta

+toista

+toistaiseksi

+toki

+tosin

+tuhannen

+tuhat

+tule

+tulee

+tulemme

+tulen

+tulet

+tulette

+tulevat

+tulimme

+tulin

+tulisi

+tulisimme

+tulisin

+tulisit

+tulisitte

+tulisivat

+tulit

+tulitte

+tulivat

+tulla

+tulleet

+tullut

+tuntuu

+tuo

+tuolla

+tuolloin

+tuolta

+tuonne

+tuskin

+tyk�

+usea

+useasti

+useimmiten

+usein

+useita

+uudeksi

+uudelleen

+uuden

+uudet

+uusi

+uusia

+uusien

+uusinta

+uuteen

+uutta

+vaan

+v�h�n

+v�hemm�n

+v�hint��n

+v�hiten

+vai

+vaiheessa

+vaikea

+vaikean

+vaikeat

+vaikeilla

+vaikeille

+vaikeilta

+vaikeissa

+vaikeista

+vaikka

+vain

+v�lill�

+varmasti

+varsin

+varsinkin

+varten

+vasta

+vastaan

+vastakkain

+verran

+viel�

+vierekk�in

+vieri

+viiden

+viime

+viimeinen

+viimeisen

+viimeksi

+viisi

+voi

+voidaan

+voimme

+voin

+voisi

+voit

+voitte

+voivat

+vuoden

+vuoksi

+vuosi

+vuosien

+vuosina

+vuotta

+yh�

+yhdeks�n

+yhden

+yhdess�

+yht�

+yht��ll�

+yht��lle

+yht��lt�

+yht��n

+yhteen

+yhteens�

+yhteydess�

+yhteyteen

+yksi

+yksin

+yksitt�in

+yleens�

+ylemm�s

+yli

+yl�s

+ymp�ri

diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/fr_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/fr_ST.txt
new file mode 100644
index 0000000..c84d8c1
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/fr_ST.txt
@@ -0,0 +1,464 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html

+a

+�

+�

+abord

+afin

+ah

+ai

+aie

+ainsi

+allaient

+allo

+all�

+allons

+apr�s

+assez

+attendu

+au

+aucun

+aucune

+aujourd

+aujourd'hui

+auquel

+aura

+auront

+aussi

+autre

+autres

+aux

+auxquelles

+auxquels

+avaient

+avais

+avait

+avant

+avec

+avoir

+ayant

+b

+bah

+beaucoup

+bien

+bigre

+boum

+bravo

+brrr

+c

+�a

+car

+ce

+ceci

+cela

+celle

+celle-ci

+celle-l�

+celles

+celles-ci

+celles-l�

+celui

+celui-ci

+celui-l�

+cent

+cependant

+certain

+certaine

+certaines

+certains

+certes

+ces

+cet

+cette

+ceux

+ceux-ci

+ceux-l�

+chacun

+chaque

+cher

+ch�re

+ch�res

+chers

+chez

+chiche

+chut

+ci

+cinq

+cinquantaine

+cinquante

+cinquanti�me

+cinqui�me

+clac

+clic

+combien

+comme

+comment

+compris

+concernant

+contre

+couic

+crac

+d

+da

+dans

+de

+debout

+dedans

+dehors

+del�

+depuis

+derri�re

+des

+d�s

+d�sormais

+desquelles

+desquels

+dessous

+dessus

+deux

+deuxi�me

+deuxi�mement

+devant

+devers

+devra

+diff�rent

+diff�rente

+diff�rentes

+diff�rents

+dire

+divers

+diverse

+diverses

+dix

+dix-huit

+dixi�me

+dix-neuf

+dix-sept

+doit

+doivent

+donc

+dont

+douze

+douzi�me

+dring

+du

+duquel

+durant

+e

+effet

+eh

+elle

+elle-m�me

+elles

+elles-m�mes

+en

+encore

+entre

+envers

+environ

+es

+�s

+est

+et

+etant

+�taient

+�tais

+�tait

+�tant

+etc

+�t�

+etre

+�tre

+eu

+euh

+eux

+eux-m�mes

+except�

+f

+fa�on

+fais

+faisaient

+faisant

+fait

+feront

+fi

+flac

+floc

+font

+g

+gens

+h

+ha

+h�

+hein

+h�las

+hem

+hep

+hi

+ho

+hol�

+hop

+hormis

+hors

+hou

+houp

+hue

+hui

+huit

+huiti�me

+hum

+hurrah

+i

+il

+ils

+importe

+j

+je

+jusqu

+jusque

+k

+l

+la

+l�

+laquelle

+las

+le

+lequel

+les

+l�s

+lesquelles

+lesquels

+leur

+leurs

+longtemps

+lorsque

+lui

+lui-m�me

+m

+ma

+maint

+mais

+malgr�

+me

+m�me

+m�mes

+merci

+mes

+mien

+mienne

+miennes

+miens

+mille

+mince

+moi

+moi-m�me

+moins

+mon

+moyennant

+n

+na

+ne

+n�anmoins

+neuf

+neuvi�me

+ni

+nombreuses

+nombreux

+non

+nos

+notre

+n�tre

+n�tres

+nous

+nous-m�mes

+nul

+o

+o|

+�

+oh

+oh�

+ol�

+oll�

+on

+ont

+onze

+onzi�me

+ore

+ou

+o�

+ouf

+ouias

+oust

+ouste

+outre

+p

+paf

+pan

+par

+parmi

+partant

+particulier

+particuli�re

+particuli�rement

+pas

+pass�

+pendant

+personne

+peu

+peut

+peuvent

+peux

+pff

+pfft

+pfut

+pif

+plein

+plouf

+plus

+plusieurs

+plut�t

+pouah

+pour

+pourquoi

+premier

+premi�re

+premi�rement

+pr�s

+proche

+psitt

+puisque

+q

+qu

+quand

+quant

+quanta

+quant-�-soi

+quarante

+quatorze

+quatre

+quatre-vingt

+quatri�me

+quatri�mement

+que

+quel

+quelconque

+quelle

+quelles

+quelque

+quelques

+quelqu'un

+quels

+qui

+quiconque

+quinze

+quoi

+quoique

+r

+revoici

+revoil�

+rien

+s

+sa

+sacrebleu

+sans

+sapristi

+sauf

+se

+seize

+selon

+sept

+septi�me

+sera

+seront

+ses

+si

+sien

+sienne

+siennes

+siens

+sinon

+six

+sixi�me

+soi

+soi-m�me

+soit

+soixante

+son

+sont

+sous

+stop

+suis

+suivant

+sur

+surtout

+t

+ta

+tac

+tant

+te

+t�

+tel

+telle

+tellement

+telles

+tels

+tenant

+tes

+tic

+tien

+tienne

+tiennes

+tiens

+toc

+toi

+toi-m�me

+ton

+touchant

+toujours

+tous

+tout

+toute

+toutes

+treize

+trente

+tr�s

+trois

+troisi�me

+troisi�mement

+trop

+tsoin

+tsouin

+tu

+u

+un

+une

+unes

+uns

+v

+va

+vais

+vas

+v�

+vers

+via

+vif

+vifs

+vingt

+vivat

+vive

+vives

+vlan

+voici

+voil�

+vont

+vos

+votre

+v�tre

+v�tres

+vous

+vous-m�mes

+vu

+w

+x

+y

+z

+zut

diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/hi_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/hi_ST.txt
new file mode 100644
index 0000000..426fc2d
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/hi_ST.txt
@@ -0,0 +1,164 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html

+पर

+इन 

+वह 

+यिह 

+वुह 

+जिन्हें

+जिन्हों

+तिन्हें

+तिन्हों

+किन्हों

+किन्हें

+इत्यादि

+द्वारा

+इन्हें

+इन्हों

+उन्हों

+बिलकुल

+निहायत

+ऱ्वासा

+इन्हीं

+उन्हीं

+उन्हें

+इसमें

+जितना

+दुसरा

+कितना

+दबारा

+साबुत

+वग़ैरह

+दूसरे

+कौनसा

+लेकिन

+होता

+करने

+किया

+लिये

+अपने

+नहीं

+दिया

+इसका

+करना

+वाले

+सकते

+इसके

+सबसे

+होने

+करते

+बहुत

+वर्ग

+करें

+होती

+अपनी

+उनके

+कहते

+होते

+करता

+उनकी

+इसकी

+सकता

+रखें

+अपना

+उसके

+जिसे

+तिसे

+किसे

+किसी

+काफ़ी

+पहले

+नीचे

+बाला

+यहाँ

+जैसा

+जैसे

+मानो

+अंदर

+भीतर

+पूरा

+सारा

+होना

+उनको

+वहाँ

+वहीं

+जहाँ

+जीधर

+उनका

+इनका

+के

+हैं

+गया

+बनी

+एवं

+हुआ

+साथ

+बाद

+लिए

+कुछ

+कहा

+यदि

+हुई

+इसे

+हुए

+अभी

+सभी

+कुल

+रहा

+रहे

+इसी

+उसे

+जिस

+जिन

+तिस

+तिन

+कौन

+किस

+कोई

+ऐसे

+तरह

+किर

+साभ

+संग

+यही

+बही

+उसी

+फिर

+मगर

+का

+एक

+यह

+से

+को

+इस

+कि

+जो

+कर

+मे

+ने

+तो

+ही

+या

+हो

+था

+तक

+आप

+ये

+थे

+दो

+वे

+थी

+जा

+ना

+उस

+एस

+पे

+उन

+सो

+भी

+और

+घर

+तब

+जब

+अत

+व

+न

diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/hu_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/hu_ST.txt
new file mode 100644
index 0000000..7cf3e1c
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/hu_ST.txt
@@ -0,0 +1,738 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html
+a
+abba
+abban
+abból
+addig
+ahhoz
+ahol
+akár
+aki
+akik
+akkor
+alá
+alád
+alájuk
+alám
+alánk
+alapján
+alátok
+alatt
+alatta
+alattad
+alattam
+alattatok
+alattuk
+alattunk
+alól
+alóla
+alólad
+alólam
+alólatok
+alóluk
+alólunk
+által
+általában
+ám
+amely
+amelybol
+amelyek
+amelyekben
+amelyeket
+amelyet
+amelyik
+amelynek
+ami
+amíg
+amikor
+amit
+amott
+annak
+annál
+arra
+arról
+át
+attól
+az
+azért
+aznap
+azok
+azokat
+azokba
+azokban
+azokból
+azokért
+azokhoz
+azokig
+azokká
+azokkal
+azoknak
+azoknál
+azokon
+azokra
+azokról
+azoktól
+azon
+azonban
+azonnal
+azt
+aztán
+azzá
+azzal
+bal
+balra
+ban
+bár
+bárcsak
+bármilyen
+be
+belé
+beléd
+beléjük
+belém
+belénk
+belétek
+belőle
+belőled
+belőlem
+belőletek
+belőlük
+belőlünk
+belül
+ben
+benne
+benned
+bennem
+bennetek
+bennük
+bennünk
+búcsú
+csak
+csakhogy
+csupán
+de
+dehogy
+ebbe
+ebben
+ebből
+eddig
+egész
+egészen
+egy
+egyéb
+egyebek
+egyebet
+egyedül
+egyelőre
+egyet
+egyik
+egymás
+egyre
+egyszerre
+együtt
+ehhez
+el
+elé
+eléd
+elég
+eleinte
+eléjük
+elém
+elénk
+elétek
+éljen
+ellen
+ellenére
+ellenes
+elleni
+elmondta
+előbb
+elől
+előle
+előled
+előlem
+előletek
+előlük
+előlünk
+először
+előtt
+előtte
+előtted
+előttem
+előttetek
+előttük
+előttünk
+előző
+első
+elsők
+elsősorban
+elsőt
+én
+engem
+ennek
+ennél
+ennyi
+enyém
+erre
+erről
+érte
+érted
+értem
+értetek
+értük
+értünk
+és
+esetben
+ettől
+év
+évben
+éve
+évek
+éves
+évi
+évvel
+ez
+ezek
+ezekbe
+ezekben
+ezekből
+ezeken
+ezekért
+ezeket
+ezekhez
+ezekig
+ezekké
+ezekkel
+ezeknek
+ezeknél
+ezekre
+ezekről
+ezektől
+ezen
+ezentúl
+ezer
+ezért
+ezret
+ezt
+ezután
+ezzé
+ezzel
+fel
+fél
+fele
+felé
+felek
+felet
+felett
+fent
+fenti
+fölé
+gyakran
+ha
+halló
+hamar
+hanem
+hány
+hányszor
+harmadik
+harmadikat
+hármat
+harminc
+három
+hat
+hát
+hátha
+hatodik
+hatodikat
+hatot
+hátulsó
+hatvan
+helyett
+hét
+hetedik
+hetediket
+hetet
+hetven
+hiába
+hirtelen
+hiszen
+hogy
+hol
+holnap
+holnapot
+honnan
+hova
+hozzá
+hozzád
+hozzájuk
+hozzám
+hozzánk
+hozzátok
+hurrá
+húsz
+huszadik
+idén
+ide-оda
+igazán
+igen
+így
+illetve
+ilyen
+immár
+inkább
+is
+ismét
+itt
+jelenleg
+jó
+jobban
+jobbra
+jól
+jólesik
+jóval
+jövőre
+kell
+kellene
+kellett
+kelljen
+képest
+kérem
+kérlek
+késő
+később
+későn
+kész
+két
+kétszer
+ketten
+kettő
+kettőt
+kevés
+ki
+kiben
+kiből
+kicsit
+kicsoda
+kié
+kiért
+kihez
+kik
+kikbe
+kikben
+kikből
+kiken
+kikért
+kiket
+kikhez
+kikké
+kikkel
+kiknek
+kiknél
+kikre
+kikről
+kiktől
+kilenc
+kilencedik
+kilencediket
+kilencet
+kilencven
+kin
+kinek
+kinél
+kire
+kiről
+kit
+kitől
+kivé
+kivel
+korábban
+körül
+köszönhetően
+köszönöm
+közben
+közé
+közel
+közepén
+közepesen
+között
+közül
+külön
+különben
+különböző
+különbözőbb
+különbözőek
+lassan
+le
+legalább
+legyen
+lehet
+lehetetlen
+lehetőleg
+lehetőség
+lenne
+lennék
+lennének
+lesz
+leszek
+lesznek
+leszünk
+lett
+lettek
+lettem
+lettünk
+lévő
+ma
+maga
+magad
+magam
+magát
+magatokat
+magukat
+magunkat
+mai
+majd
+majdnem
+manapság
+már
+más
+másik
+másikat
+másnap
+második
+másodszor
+mások
+másokat
+mást
+meg
+még
+megcsinál
+megcsinálnak
+megint
+mégis
+megvan
+mellé
+melléd
+melléjük
+mellém
+mellénk
+mellétek
+mellett
+mellette
+melletted
+mellettem
+mellettetek
+mellettük
+mellettünk
+mellől
+mellőle
+mellőled
+mellőlem
+mellőletek
+mellőlük
+mellőlünk
+melyik
+mennyi
+mert
+mi
+miatt
+miatta
+miattad
+miattam
+miattatok
+miattuk
+miattunk
+mibe
+miben
+miből
+miért
+míg
+mihez
+mik
+mikbe
+mikben
+mikből
+miken
+mikért
+miket
+mikhez
+mikké
+mikkel
+miknek
+miknél
+mikor
+mikre
+mikről
+miktől
+milyen
+min
+mind
+mindegyik
+mindegyiket
+minden
+mindenesetre
+mindenki
+mindent
+mindenütt
+mindig
+mindketten
+minek
+minél
+minket
+mint
+mire
+miről
+mit
+mitől
+mivé
+mivel
+mögé
+mögéd
+mögéjük
+mögém
+mögénk
+mögétek
+mögött
+mögötte
+mögötted
+mögöttem
+mögöttetek
+mögöttük
+mögöttünk
+mögül
+mögüle
+mögüled
+mögülem
+mögületek
+mögülük
+mögülünk
+mondta
+most
+mostanáig
+múltkor
+múlva
+na
+nagyon
+nála
+nálad
+nálam
+nálatok
+náluk
+nálunk
+naponta
+napot
+ne
+négy
+negyedik
+negyediket
+négyet
+negyven
+néha
+néhány
+neked
+nekem
+neki
+nekik
+nektek
+nekünk
+nélkül
+nem
+nemcsak
+nemrég
+nincs
+nyolc
+nyolcadik
+nyolcadikat
+nyolcat
+nyolcvan

+ők
+őket
+olyan
+ön
+önbe
+önben
+önből
+önért
+önhöz
+onnan
+önnek
+önnel
+önnél
+önök
+önökbe
+önökben
+önökből
+önökért
+önöket
+önökhöz
+önökkel
+önöknek
+önöknél
+önökön
+önökre
+önökről
+önöktől
+önön
+önre
+önről
+önt
+öntől
+öt
+őt
+óta
+ötödik
+ötödiket
+ötöt
+ott
+ötven
+pár
+pedig
+például
+persze
+rá
+rád
+rajta
+rajtad
+rajtam
+rajtatok
+rajtuk
+rajtunk
+rájuk
+rám
+ránk
+rátok
+régen
+régóta
+rendben
+részére
+rögtön
+róla
+rólad
+rólam
+rólatok
+róluk
+rólunk
+rosszul
+se
+sem
+semmi
+semmilyen
+semmiség
+senki
+soha
+sok
+sokáig
+sokan
+sokszor
+során
+sőt
+stb.
+számára
+száz
+századik
+százat
+szemben
+szépen
+szerbusz
+szerint
+szerinte
+szerinted
+szerintem
+szerintetek
+szerintük
+szerintünk
+szervusz
+szinte
+szíves
+szívesen
+szíveskedjék
+talán
+tavaly
+távol
+te
+téged
+tegnap
+tegnapelőtt
+tehát
+tele
+tényleg
+tessék
+ti
+tied
+titeket
+tíz
+tizedik
+tizediket
+tizenegy
+tizenegyedik
+tizenhárom
+tizenhat
+tizenhét
+tizenkét
+tizenkettedik
+tizenkettő
+tizenkilenc
+tizennégy
+tizennyolc
+tizenöt
+tizet
+több
+többi
+többször
+tőle
+tőled
+tőlem
+tőletek
+tőlük
+tőlünk
+tovább
+további
+túl
+úgy
+ugyanakkor
+ugyanez
+ugyanis
+ugye
+úgyis
+úgynevezett
+újra
+úr
+urak
+uram
+urat
+után
+utoljára
+utolsó
+vagy
+vagyis
+vagyok
+vagytok
+vagyunk
+vajon
+valahol
+valaki
+valakit
+valamelyik
+valami
+valamint
+van
+vannak
+végén
+végre
+végül
+vele
+veled
+velem
+veletek
+velük
+velünk
+viszlát
+viszont
+viszontlátásra
+volna
+volnának
+volnék
+volt
+voltak
+voltam
+voltunk
diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/it_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/it_ST.txt
new file mode 100644
index 0000000..23c80a2
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/it_ST.txt
@@ -0,0 +1,400 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html
+a
+abbastanza
+accidenti
+ad
+adesso
+affinche
+agli
+ahime
+ahimè
+ai
+al
+alcuna
+alcuni
+alcuno
+all
+alla
+alle
+allo
+altri
+altrimenti
+altro
+altrui
+anche
+ancora
+anni
+anno
+ansa
+assai
+attesa
+avanti
+avendo
+avente
+aver
+avere
+avete
+aveva
+avuta
+avute
+avuti
+avuto
+basta
+bene
+benissimo
+berlusconi
+brava
+bravo
+c
+casa
+caso
+cento
+certa
+certe
+certi
+certo
+che
+chi
+chicchessia
+chiunque
+ci
+ciascuna
+ciascuno
+cima
+cio
+ciò
+cioe
+cioè
+circa
+citta
+città
+codesta
+codesti
+codesto
+cogli
+coi
+col
+colei
+coll
+coloro
+colui
+come
+con
+concernente
+consiglio
+contro
+cortesia
+cos
+cosa
+cosi
+così
+cui
+d
+da
+dagli
+dai
+dal
+dall
+dalla
+dalle
+dallo
+davanti
+degli
+dei
+del
+dell
+della
+delle
+dello
+dentro
+detto
+deve
+di
+dice
+dietro
+dire
+dirimpetto
+dopo
+dove
+dovra
+dovrà
+due
+dunque
+durante
+e

+ecco
+ed
+egli
+ella
+eppure
+era
+erano
+esse
+essendo
+esser
+essere
+essi
+ex
+fa
+fare
+fatto
+favore
+fin
+finalmente
+finche
+fine
+fino
+forse
+fra
+fuori
+gia
+già
+giacche
+giorni
+giorno
+gli
+gliela
+gliele
+glieli
+glielo
+gliene
+governo
+grande
+grazie
+gruppo
+ha
+hai
+hanno
+ho
+i
+ieri
+il
+improvviso
+in
+infatti
+insieme
+intanto
+intorno
+invece
+io
+l
+la
+là
+lavoro
+le
+lei
+li
+lo
+lontano
+loro
+lui
+lungo
+ma
+macche
+magari
+mai
+male
+malgrado
+malissimo
+me
+medesimo
+mediante
+meglio
+meno
+mentre
+mesi
+mezzo
+mi
+mia
+mie
+miei
+mila
+miliardi
+milioni
+ministro
+mio
+moltissimo
+molto
+mondo
+nazionale
+ne
+negli
+nei
+nel
+nell
+nella
+nelle
+nello
+nemmeno
+neppure
+nessuna
+nessuno
+niente
+no
+noi
+non
+nondimeno
+nostra
+nostre
+nostri
+nostro
+nulla
+nuovo
+o
+od
+oggi
+ogni
+ognuna
+ognuno
+oltre
+oppure
+ora
+ore
+osi
+ossia
+paese
+parecchi
+parecchie
+parecchio
+parte
+partendo
+peccato
+peggio
+per
+perche
+perchè
+percio
+perciò
+perfino
+pero
+però
+persone
+piedi
+pieno
+piglia
+piu
+più
+po
+pochissimo
+poco
+poi
+poiche
+press
+prima
+primo
+proprio
+puo
+può
+pure
+purtroppo
+qualche
+qualcuna
+qualcuno
+quale
+quali
+qualunque
+quando
+quanta
+quante
+quanti
+quanto
+quantunque
+quasi
+quattro
+quel
+quella
+quelli
+quello
+quest
+questa
+queste
+questi
+questo
+qui
+quindi
+riecco
+salvo
+sara
+sarà
+sarebbe
+scopo
+scorso
+se
+secondo
+seguente
+sei
+sempre
+senza
+si
+sia
+siamo
+siete
+solito
+solo
+sono
+sopra
+sotto
+sta
+staranno
+stata
+state
+stati
+stato
+stesso
+su
+sua
+successivo
+sue
+sugli
+sui
+sul
+sull
+sulla
+sulle
+sullo
+suo
+suoi
+tale
+talvolta
+tanto
+te
+tempo
+ti
+torino
+tra
+tranne
+tre
+troppo
+tu
+tua
+tue
+tuo
+tuoi
+tutta
+tuttavia
+tutte
+tutti
+tutto
+uguali
+un
+una
+uno
+uomo
+va
+vale
+varia
+varie
+vario
+verso
+vi
+via
+vicino
+visto
+vita
+voi
+volta
+vostra
+vostre
+vostri
+vostro
diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/pl_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/pl_ST.txt
new file mode 100644
index 0000000..e27c30e
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/pl_ST.txt
@@ -0,0 +1,139 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html
+ach
+aj
+albo
+bardzo
+bez
+bo
+być
+ci
+cię
+ciebie
+co
+czy
+daleko
+dla
+dlaczego
+dlatego
+do
+dobrze
+dokąd
+dość
+dużo
+dwa
+dwaj
+dwie
+dwoje
+dziś
+dzisiaj
+gdyby
+gdzie
+go
+ich
+ile
+im
+inny
+ja
+ją
+jak
+jakby
+jaki
+je
+jeden
+jedna
+jedno
+jego
+jej
+jemu
+jeśli
+jest
+jestem
+jeżeli
+już
+każdy
+kiedy
+kierunku
+kto
+ku
+lub
+ma
+mają
+mam
+mi
+mną
+mnie
+moi
+mój
+moja
+moje
+może
+mu
+my
+na
+nam
+nami
+nas
+nasi
+nasz
+nasza
+nasze
+natychmiast
+nią
+nic
+nich
+nie
+niego
+niej
+niemu
+nigdy
+nim
+nimi
+niż
+obok
+od
+około
+on
+ona
+one
+oni
+ono
+owszem
+po
+pod
+ponieważ
+przed
+przedtem
+są
+sam
+sama
+się
+skąd
+tak
+taki
+tam
+ten
+to
+tobą
+tobie
+tu
+tutaj
+twoi
+twój
+twoja
+twoje
+ty
+wam
+wami
+was
+wasi
+wasz
+wasza
+wasze
+we
+więc
+wszystko
+wtedy
+wy
+żaden
+zawsze
+że
diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/pt_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/pt_ST.txt
new file mode 100644
index 0000000..da60644
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/pt_ST.txt
@@ -0,0 +1,357 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html

+a

+�

+adeus

+agora

+a�

+ainda

+al�m

+algo

+algumas

+alguns

+ali

+ano

+anos

+antes

+ao

+aos

+apenas

+apoio

+ap�s

+aquela

+aquelas

+aquele

+aqueles

+aqui

+aquilo

+�rea

+as

+�s

+assim

+at�

+atr�s

+atrav�s

+baixo

+bastante

+bem

+bom

+breve

+c�

+cada

+catorze

+cedo

+cento

+certamente

+certeza

+cima

+cinco

+coisa

+com

+como

+conselho

+contra

+custa

+da

+d�

+d�o

+daquela

+daquele

+dar

+das

+de

+debaixo

+demais

+dentro

+depois

+desde

+dessa

+desse

+desta

+deste

+deve

+dever�

+dez

+dezanove

+dezasseis

+dezassete

+dezoito

+dia

+diante

+diz

+dizem

+dizer

+do

+dois

+dos

+doze

+duas

+d�vida

+e

+�

+ela

+elas

+ele

+eles

+em

+embora

+entre

+era

+�s

+essa

+essas

+esse

+esses

+esta

+est�

+estar

+estas

+est�s

+estava

+este

+estes

+esteve

+estive

+estivemos

+estiveram

+estiveste

+estivestes

+estou

+eu

+exemplo

+fa�o

+falta

+favor

+faz

+fazeis

+fazem

+fazemos

+fazer

+fazes

+fez

+fim

+final

+foi

+fomos

+for

+foram

+forma

+foste

+fostes

+fui

+geral

+grande

+grandes

+grupo

+h�

+hoje

+horas

+isso

+isto

+j�

+l�

+lado

+local

+logo

+longe

+lugar

+maior

+maioria

+mais

+mal

+mas

+m�ximo

+me

+meio

+menor

+menos

+m�s

+meses

+meu

+meus

+mil

+minha

+minhas

+momento

+muito

+muitos

+na

+nada

+n�o

+naquela

+naquele

+nas

+nem

+nenhuma

+nessa

+nesse

+nesta

+neste

+n�vel

+no

+noite

+nome

+nos

+n�s

+nossa

+nossas

+nosso

+nossos

+nova

+nove

+novo

+novos

+num

+numa

+n�mero

+nunca

+o

+obra

+obrigada

+obrigado

+oitava

+oitavo

+oito

+onde

+ontem

+onze

+os

+ou

+outra

+outras

+outro

+outros

+para

+parece

+parte

+partir

+pela

+pelas

+pelo

+pelos

+perto

+pode

+p�de

+podem

+poder

+p�e

+p�em

+ponto

+pontos

+por

+porque

+porqu�

+posi��o

+poss�vel

+possivelmente

+posso

+pouca

+pouco

+primeira

+primeiro

+pr�prio

+pr�ximo

+puderam

+qual

+quando

+quanto

+quarta

+quarto

+quatro

+que

+qu�

+quem

+quer

+quero

+quest�o

+quinta

+quinto

+quinze

+rela��o

+sabe

+s�o

+se

+segunda

+segundo

+sei

+seis

+sem

+sempre

+ser

+seria

+sete

+s�tima

+s�timo

+seu

+seus

+sexta

+sexto

+sim

+sistema

+sob

+sobre

+sois

+somos

+sou

+sua

+suas

+tal

+talvez

+tamb�m

+tanto

+t�o

+tarde

+te

+tem

+t�m

+temos

+tendes

+tenho

+tens

+ter

+terceira

+terceiro

+teu

+teus

+teve

+tive

+tivemos

+tiveram

+tiveste

+tivestes

+toda

+todas

+todo

+todos

+trabalho

+tr�s

+treze

+tu

+tua

+tuas

+tudo

+um

+uma

+umas

+uns

+vai

+vais

+v�o

+v�rios

+vem

+v�m

+vens

+ver

+vez

+vezes

+viagem

+vindo

+vinte

+voc�

+voc�s

+vos

+v�s

+vossa

+vossas

+vosso

+vossos

+zero

diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/ro_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/ro_ST.txt
new file mode 100644
index 0000000..ec7c517
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/ro_ST.txt
@@ -0,0 +1,283 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html

+acea

+aceasta

+această

+aceea

+acei

+aceia

+acel

+acela

+acele

+acelea

+acest

+acesta

+aceste

+acestea

+aceşti

+aceştia

+acolo

+acord

+acum

+ai

+aia

+aibă

+aici

+al

+ăla

+ale

+alea

+ălea

+altceva

+altcineva

+am

+ar

+are

+aş

+aşadar

+asemenea

+asta

+ăsta

+astăzi

+astea

+ăstea

+ăştia

+asupra

+aţi

+au

+avea

+avem

+aveţi

+azi

+bine

+bucur

+bună

+ca

+că

+căci

+când

+care

+cărei

+căror

+cărui

+cât

+câte

+câţi

+către

+câtva

+caut

+ce

+cel

+ceva

+chiar

+cinci

+cînd

+cine

+cineva

+cît

+cîte

+cîţi

+cîtva

+contra

+cu

+cum

+cumva

+curând

+curînd

+da

+dă

+dacă

+dar

+dată

+datorită

+dau

+de

+deci

+deja

+deoarece

+departe

+deşi

+din

+dinaintea

+dintr-

+dintre

+doi

+doilea

+două

+drept

+după

+ea

+ei

+el

+ele

+eram

+este

+eşti

+eu

+face

+fără

+fata

+fi

+fie

+fiecare

+fii

+fim

+fiţi

+fiu

+frumos

+graţie

+halbă

+iar

+ieri

+îi

+îl

+îmi

+împotriva

+în 

+înainte

+înaintea

+încât

+încît

+încotro

+între

+întrucât

+întrucît

+îţi

+la

+lângă

+le

+li

+lîngă

+lor

+lui

+mă

+mai

+mâine

+mea

+mei

+mele

+mereu

+meu

+mi

+mie

+mîine

+mine

+mult

+multă

+mulţi

+mulţumesc

+ne

+nevoie

+nicăieri

+nici

+nimeni

+nimeri

+nimic

+nişte

+noastră

+noastre

+noi

+noroc

+noştri

+nostru

+nouă

+nu

+opt

+ori

+oricând

+oricare

+oricât

+orice

+oricînd

+oricine

+oricît

+oricum

+oriunde

+până

+patra

+patru

+patrulea

+pe

+pentru

+peste

+pic

+pînă

+poate

+pot

+prea

+prima

+primul

+prin

+printr-

+puţin

+puţina

+puţină

+rog

+sa

+să

+săi

+sale

+şapte

+şase

+sau

+său

+se

+şi

+sînt

+sîntem

+sînteţi

+spate

+spre

+ştiu

+sub

+sunt

+suntem

+sunteţi

+sută

+ta

+tăi

+tale

+tău

+te

+ţi

+ţie

+timp

+tine

+toată

+toate

+tot

+toţi

+totuşi

+trei

+treia

+treilea

+tu

+un

+una

+unde

+undeva

+unei

+uneia

+unele

+uneori

+unii

+unor

+unora

+unu

+unui

+unuia

+unul

+vă

+vi

+voastră

+voastre

+voi

+voştri

+vostru

+vouă

+vreme

+vreo

+vreun

+zece

+zero

+zi

+zice

diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/ru_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/ru_ST.txt
new file mode 100644
index 0000000..d7de4e5
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/ru_ST.txt
@@ -0,0 +1,423 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html







+на

+не

+ни

+об

+но

+он

+мне

+мои

+мож

+она

+они

+оно

+мной

+много

+многочисленное

+многочисленная

+многочисленные

+многочисленный

+мною

+мой

+мог

+могут

+можно

+может

+можхо

+мор

+моя

+моё

+мочь

+над

+нее

+оба

+нам

+нем

+нами

+ними

+мимо

+немного

+одной

+одного

+менее

+однажды

+однако

+меня

+нему

+меньше

+ней

+наверху

+него

+ниже

+мало

+надо

+один

+одиннадцать

+одиннадцатый

+назад

+наиболее

+недавно

+миллионов

+недалеко

+между

+низко

+меля

+нельзя

+нибудь

+непрерывно

+наконец

+никогда

+никуда

+нас

+наш

+нет

+нею

+неё

+них

+мира

+наша

+наше

+наши

+ничего

+начала

+нередко

+несколько

+обычно

+опять

+около

+мы

+ну

+нх

+от

+отовсюду

+особенно

+нужно

+очень

+отсюда


+во

+вон

+вниз

+внизу

+вокруг

+вот

+восемнадцать

+восемнадцатый

+восемь

+восьмой

+вверх

+вам

+вами

+важное

+важная

+важные

+важный

+вдали

+везде

+ведь

+вас

+ваш

+ваша

+ваше

+ваши

+впрочем

+весь

+вдруг

+вы

+все

+второй

+всем

+всеми

+времени

+время

+всему

+всего

+всегда

+всех

+всею

+всю

+вся

+всё

+всюду


+год

+говорил

+говорит

+года

+году

+где

+да

+ее

+за

+из

+ли

+же

+им

+до

+по

+ими

+под

+иногда

+довольно

+именно

+долго

+позже

+более

+должно

+пожалуйста

+значит

+иметь

+больше

+пока

+ему

+имя

+пор

+пора

+потом

+потому

+после

+почему

+почти

+посреди

+ей

+два

+две

+двенадцать

+двенадцатый

+двадцать

+двадцатый

+двух

+его

+дел

+или

+без

+день

+занят

+занята

+занято

+заняты

+действительно

+давно

+девятнадцать

+девятнадцатый

+девять

+девятый

+даже

+алло

+жизнь

+далеко

+близко

+здесь

+дальше

+для

+лет

+зато

+даром

+первый

+перед

+затем

+зачем

+лишь

+десять

+десятый

+ею

+её

+их

+бы

+еще

+при

+был

+про

+процентов

+против

+просто

+бывает

+бывь

+если

+люди

+была

+были

+было

+будем

+будет

+будете

+будешь

+прекрасно

+буду

+будь

+будто

+будут

+ещё

+пятнадцать

+пятнадцатый

+друго

+другое

+другой

+другие

+другая

+других

+есть

+пять

+быть

+лучше

+пятый


+ком

+конечно

+кому

+кого

+когда

+которой

+которого

+которая

+которые

+который

+которых

+кем

+каждое

+каждая

+каждые

+каждый

+кажется

+как

+какой

+какая

+кто

+кроме

+куда

+кругом





+та

+те

+уж

+со

+то

+том

+снова

+тому

+совсем

+того

+тогда

+тоже

+собой

+тобой

+собою

+тобою

+сначала

+только

+уметь

+тот

+тою

+хорошо

+хотеть

+хочешь

+хоть

+хотя

+свое

+свои

+твой

+своей

+своего

+своих

+свою

+твоя

+твоё

+раз

+уже

+сам

+там

+тем

+чем

+сама

+сами

+теми

+само

+рано

+самом

+самому

+самой

+самого

+семнадцать

+семнадцатый

+самим

+самими

+самих

+саму

+семь

+чему

+раньше

+сейчас

+чего

+сегодня

+себе

+тебе

+сеаой

+человек

+разве

+теперь

+себя

+тебя

+седьмой

+спасибо

+слишком

+так

+такое

+такой

+такие

+также

+такая

+сих

+тех

+чаще

+четвертый

+через

+часто

+шестой

+шестнадцать

+шестнадцатый

+шесть

+четыре

+четырнадцать

+четырнадцатый

+сколько

+сказал

+сказала

+сказать

+ту

+ты

+три

+эта

+эти

+что

+это

+чтоб

+этом

+этому

+этой

+этого

+чтобы

+этот

+стал

+туда

+этим

+этими

+рядом

+тринадцать

+тринадцатый

+этих

+третий

+тут

+эту

+суть

+чуть

+тысяч

+

diff --git a/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/sv_ST.txt b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/sv_ST.txt
new file mode 100644
index 0000000..582ab5a
--- /dev/null
+++ b/src/resources/org/apache/cassandra/index/sasi/analyzer/filter/sv_ST.txt
@@ -0,0 +1,387 @@
+# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html

+aderton

+adertonde

+adj�

+aldrig

+alla

+allas

+allt

+alltid

+allts�

+�n

+andra

+andras

+annan

+annat

+�nnu

+artonde

+artonn

+�tminstone

+att

+�tta

+�ttio

+�ttionde

+�ttonde

+av

+�ven

+b�da

+b�das

+bakom

+bara

+b�st

+b�ttre

+beh�va

+beh�vas

+beh�vde

+beh�vt

+beslut

+beslutat

+beslutit

+bland

+blev

+bli

+blir

+blivit

+bort

+borta

+bra

+d�

+dag

+dagar

+dagarna

+dagen

+d�r

+d�rf�r

+de

+del

+delen

+dem

+den

+deras

+dess

+det

+detta

+dig

+din

+dina

+dit

+ditt

+dock

+du

+efter

+eftersom

+elfte

+eller

+elva

+en

+enkel

+enkelt

+enkla

+enligt

+er

+era

+ert

+ett

+ettusen

+f� 

+fanns

+f�r

+f�tt 

+fem

+femte

+femtio

+femtionde

+femton

+femtonde

+fick

+fin

+finnas

+finns

+fj�rde

+fjorton

+fjortonde

+fler

+flera

+flesta

+f�ljande

+f�r

+f�re

+f�rl�t

+f�rra

+f�rsta

+fram

+framf�r

+fr�n

+fyra

+fyrtio

+fyrtionde

+g�

+g�lla

+g�ller

+g�llt

+g�r

+g�rna

+g�tt

+genast

+genom

+gick

+gjorde

+gjort

+god

+goda

+godare

+godast

+g�r

+g�ra

+gott

+ha

+hade

+haft

+han

+hans

+har

+h�r

+heller

+hellre

+helst

+helt

+henne

+hennes

+hit

+h�g

+h�ger

+h�gre

+h�gst

+hon

+honom

+hundra

+hundraen

+hundraett

+hur

+i

+ibland

+idag

+ig�r

+igen

+imorgon

+in

+inf�r

+inga

+ingen

+ingenting

+inget

+innan

+inne

+inom

+inte

+inuti

+ja

+jag

+j�mf�rt

+kan

+kanske

+knappast

+kom

+komma

+kommer

+kommit

+kr

+kunde

+kunna

+kunnat

+kvar

+l�nge

+l�ngre

+l�ngsam

+l�ngsammare

+l�ngsammast

+l�ngsamt

+l�ngst

+l�ngt

+l�tt

+l�ttare

+l�ttast

+legat

+ligga

+ligger

+lika

+likst�lld

+likst�llda

+lilla

+lite

+liten

+litet

+man

+m�nga

+m�ste

+med

+mellan

+men

+mer

+mera

+mest

+mig

+min

+mina

+mindre

+minst

+mitt

+mittemot

+m�jlig

+m�jligen

+m�jligt

+m�jligtvis

+mot

+mycket

+n�gon

+n�gonting

+n�got

+n�gra

+n�r

+n�sta

+ned

+nederst

+nedersta

+nedre

+nej

+ner

+ni

+nio

+nionde

+nittio

+nittionde

+nitton

+nittonde

+n�dv�ndig

+n�dv�ndiga

+n�dv�ndigt

+n�dv�ndigtvis

+nog

+noll

+nr

+nu

+nummer

+och

+ocks�

+ofta

+oftast

+olika

+olikt

+om

+oss

+�ver

+�vermorgon

+�verst

+�vre

+p�

+rakt

+r�tt

+redan

+s�

+sade

+s�ga

+s�ger

+sagt

+samma

+s�mre

+s�mst

+sedan

+senare

+senast

+sent

+sex

+sextio

+sextionde

+sexton

+sextonde

+sig

+sin

+sina

+sist

+sista

+siste

+sitt

+sj�tte

+sju

+sjunde

+sjuttio

+sjuttionde

+sjutton

+sjuttonde

+ska

+skall

+skulle

+slutligen

+sm�

+sm�tt

+snart

+som

+stor

+stora

+st�rre

+st�rst

+stort

+tack

+tidig

+tidigare

+tidigast

+tidigt

+till

+tills

+tillsammans

+tio

+tionde

+tjugo

+tjugoen

+tjugoett

+tjugonde

+tjugotre

+tjugotv�

+tjungo

+tolfte

+tolv

+tre

+tredje

+trettio

+trettionde

+tretton

+trettonde

+tv�

+tv�hundra

+under

+upp

+ur

+urs�kt

+ut

+utan

+utanf�r

+ute

+vad

+v�nster

+v�nstra

+var

+v�r

+vara

+v�ra

+varf�r

+varifr�n

+varit

+varken

+v�rre

+vars�god

+vart

+v�rt

+vem

+vems

+verkligen

+vi

+vid

+vidare

+viktig

+viktigare

+viktigast

+viktigt

+vilka

+vilken

+vilket

+vill

diff --git a/test/burn/org/apache/cassandra/utils/memory/LongBufferPoolTest.java b/test/burn/org/apache/cassandra/utils/memory/LongBufferPoolTest.java
index 66abe5a..7bc9726 100644
--- a/test/burn/org/apache/cassandra/utils/memory/LongBufferPoolTest.java
+++ b/test/burn/org/apache/cassandra/utils/memory/LongBufferPoolTest.java
@@ -24,14 +24,16 @@
 import java.util.Date;
 import java.util.List;
 import java.util.concurrent.*;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
 
 import com.google.common.util.concurrent.Uninterruptibles;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.utils.DynamicList;
 
 import static org.junit.Assert.*;
@@ -60,6 +62,12 @@
  */
 public class LongBufferPoolTest
 {
+    @BeforeClass
+    public static void setup() throws Exception
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     private static final Logger logger = LoggerFactory.getLogger(LongBufferPoolTest.class);
 
     private static final int AVG_BUFFER_SIZE = 16 << 10;
@@ -67,7 +75,7 @@
     private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
 
     @Test
-    public void testAllocate() throws InterruptedException, ExecutionException
+    public void testAllocate() throws InterruptedException, ExecutionException, BrokenBarrierException, TimeoutException
     {
         testAllocate(Runtime.getRuntime().availableProcessors() * 2, TimeUnit.MINUTES.toNanos(2L), 16 << 20);
     }
@@ -107,9 +115,13 @@
         final long until;
         final CountDownLatch latch;
         final SPSCQueue<BufferCheck>[] sharedRecycle;
-        final AtomicBoolean[] makingProgress;
-        final AtomicBoolean burnFreed;
-        final AtomicBoolean[] freedAllMemory;
+
+        volatile boolean shouldFreeMemoryAndSuspend = false;
+
+        final CyclicBarrier stopAllocationsBarrier;
+        final CyclicBarrier freedAllMemoryBarrier;
+        final CyclicBarrier resumeAllocationsBarrier;
+
         final ExecutorService executorService;
         final List<Future<Boolean>> threadResultFuture;
         final int targetSizeQuanta;
@@ -122,17 +134,18 @@
             until = System.nanoTime() + duration;
             latch = new CountDownLatch(threadCount);
             sharedRecycle = new SPSCQueue[threadCount];
-            makingProgress = new AtomicBoolean[threadCount];
-            burnFreed = new AtomicBoolean(false);
-            freedAllMemory = new AtomicBoolean[threadCount];
+
+            // N worker threads + burner thread + main thread:
+            stopAllocationsBarrier = new CyclicBarrier(threadCount + 2);
+            freedAllMemoryBarrier = new CyclicBarrier(threadCount + 2);
+            resumeAllocationsBarrier = new CyclicBarrier(threadCount + 2);
+
             executorService = Executors.newFixedThreadPool(threadCount + 2);
             threadResultFuture = new ArrayList<>(threadCount);
 
             for (int i = 0; i < sharedRecycle.length; i++)
             {
                 sharedRecycle[i] = new SPSCQueue<>();
-                makingProgress[i] = new AtomicBoolean(false);
-                freedAllMemory[i] = new AtomicBoolean(false);
             }
 
             // Divide the poolSize across our threads, deliberately over-subscribing it.  Threads
@@ -150,17 +163,6 @@
             threadResultFuture.add(future);
         }
 
-        int countStalledThreads()
-        {
-            int stalledThreads = 0;
-
-            for (AtomicBoolean progress : makingProgress)
-            {
-                if (!progress.getAndSet(false))
-                    stalledThreads++;
-            }
-            return stalledThreads;
-        }
 
         int countDoneThreads()
         {
@@ -190,9 +192,60 @@
                 fail("Checked thread threw exception: " + ex.toString());
             }
         }
+
+        /**
+         * Implementers must assure all buffers were returned to the buffer pool on run exit.
+         */
+        interface MemoryFreeTask
+        {
+            void run();
+        }
+
+        /**
+         * If the main test loop requested stopping the threads by setting
+         * {@link TestEnvironment#shouldFreeMemoryAndSuspend},
+         * waits until all threads reach this call and then frees the memory by running the given memory free task.
+         * After the task finishes, it waits on the {@link TestEnvironment#freedAllMemoryBarrier} and
+         * {@link TestEnvironment#resumeAllocationsBarrier} to let the main test loop perform the post-free checks.
+         * The call exits after {@link TestEnvironment#resumeAllocationsBarrier} is reached by all threads.
+         *
+         * @param task the task that should return all buffers held by this thread to the buffer pool
+         */
+        void maybeSuspendAndFreeMemory(MemoryFreeTask task) throws InterruptedException, BrokenBarrierException
+        {
+            if (shouldFreeMemoryAndSuspend)
+            {
+                try
+                {
+                    // Wait until allocations stop in all threads; this guanrantees this thread won't
+                    // receive any new buffers from other threads while freeing memory.
+                    stopAllocationsBarrier.await();
+                    // Free our memory
+                    task.run();
+                    // Now wait for the other threads to free their memory
+                    freedAllMemoryBarrier.await();
+                    // Now all memory is freed, but let's not resume allocations until the main test thread
+                    // performs the required checks.
+                    // At this point, used memory indicated by the pool
+                    // should be == 0 and all buffers should be recycled.
+                    resumeAllocationsBarrier.await();
+                }
+                catch (BrokenBarrierException | InterruptedException e)
+                {
+                    // At the end of the test some threads may have already exited,
+                    // so they can't arrive at one of the barriers, and we may end up here.
+                    // This is fine if this happens after the test deadline, and we
+                    // just allow the test worker to exit cleanly.
+                    // It must not happen before the test deadline though, it would likely be a bug,
+                    // so we rethrow in that case.
+                    if (System.nanoTime() < until)
+                        throw e;
+                }
+            }
+        }
     }
 
-    public void testAllocate(int threadCount, long duration, int poolSize) throws InterruptedException, ExecutionException
+    public void testAllocate(int threadCount, long duration, int poolSize) throws InterruptedException, ExecutionException, BrokenBarrierException, TimeoutException
     {
         System.out.println(String.format("%s - testing %d threads for %dm",
                                          DATE_FORMAT.format(new Date()),
@@ -210,21 +263,30 @@
         for (int threadIdx = 0; threadIdx < threadCount; threadIdx++)
             testEnv.addCheckedFuture(startWorkerThread(testEnv, threadIdx));
 
-        while (!testEnv.latch.await(10L, TimeUnit.SECONDS))
+        while (!testEnv.latch.await(1L, TimeUnit.SECONDS))
         {
-            int stalledThreads = testEnv.countStalledThreads();
-            int doneThreads = testEnv.countDoneThreads();
-
-            if (doneThreads == 0) // If any threads have completed, they will stop making progress/recycling buffers.
-            {                     // Assertions failures on the threads will be caught below.
-                assert stalledThreads == 0;
-                boolean allFreed = testEnv.burnFreed.getAndSet(false);
-                for (AtomicBoolean freedMemory : testEnv.freedAllMemory)
-                    allFreed = allFreed && freedMemory.getAndSet(false);
-                if (allFreed)
-                    BufferPool.assertAllRecycled();
-                else
-                    logger.info("All threads did not free all memory in this time slot - skipping buffer recycle check");
+            try
+            {
+                // request all threads to release all buffers to the bufferPool
+                testEnv.shouldFreeMemoryAndSuspend = true;
+                testEnv.stopAllocationsBarrier.await(10, TimeUnit.SECONDS);
+                // wait until all memory released
+                testEnv.freedAllMemoryBarrier.await(10, TimeUnit.SECONDS);
+                // now all buffers should be back in the pool, and no more allocations happening
+                BufferPool.assertAllRecycled();
+                // resume threads only after debug.cycleRound has been increased
+                testEnv.shouldFreeMemoryAndSuspend = false;
+                testEnv.resumeAllocationsBarrier.await(10, TimeUnit.SECONDS);
+            }
+            catch (TimeoutException e)
+            {
+                // a thread that is done will not reach the barriers, so timeout is unexpected only if
+                // all threads are still running
+                if (testEnv.countDoneThreads() == 0)
+                {
+                    logger.error("Some threads have stalled and didn't reach the barrier", e);
+                    return;
+                }
             }
         }
 
@@ -258,24 +320,28 @@
             final SPSCQueue<BufferCheck> shareFrom = testEnv.sharedRecycle[threadIdx];
             final DynamicList<BufferCheck> checks = new DynamicList<>((int) Math.max(1, targetSize / (1 << 10)));
             final SPSCQueue<BufferCheck> shareTo = testEnv.sharedRecycle[(threadIdx + 1) % testEnv.threadCount];
+            final Future<Boolean> neighbourResultFuture = testEnv.threadResultFuture.get((threadIdx + 1) % testEnv.threadCount);
             final ThreadLocalRandom rand = ThreadLocalRandom.current();
             int totalSize = 0;
             int freeingSize = 0;
             int size = 0;
 
-            void checkpoint()
-            {
-                if (!testEnv.makingProgress[threadIdx].get())
-                    testEnv.makingProgress[threadIdx].set(true);
-            }
-
             void testOne() throws Exception
             {
+                testEnv.maybeSuspendAndFreeMemory(this::freeAll);
 
-                long currentTargetSize = (rand.nextInt(testEnv.poolSize / 1024) == 0 || !testEnv.freedAllMemory[threadIdx].get()) ? 0 : targetSize;
+                long currentTargetSize = rand.nextInt(testEnv.poolSize / 1024) == 0 ? 0 : targetSize;
                 int spinCount = 0;
                 while (totalSize > currentTargetSize - freeingSize)
                 {
+                    // Don't get stuck in this loop if other threads might be suspended:
+                    if (testEnv.shouldFreeMemoryAndSuspend)
+                        return;
+
+                    // Don't get stuck in this loop if the neighbour thread exited:
+                    if (neighbourResultFuture.isDone())
+                        return;
+
                     // free buffers until we're below our target size
                     if (checks.size() == 0)
                     {
@@ -321,9 +387,6 @@
                     }
                 }
 
-                if (currentTargetSize == 0)
-                    testEnv.freedAllMemory[threadIdx].compareAndSet(false, true);
-
                 // allocate a new buffer
                 size = (int) Math.max(1, AVG_BUFFER_SIZE + (STDEV_BUFFER_SIZE * rand.nextGaussian()));
                 if (size <= BufferPool.CHUNK_SIZE)
@@ -356,6 +419,29 @@
                 while (recycleFromNeighbour());
             }
 
+            /**
+             * Returns all allocated buffers back to the buffer pool.
+             */
+            void freeAll()
+            {
+                while (checks.size() > 0)
+                {
+                    BufferCheck check = sample();
+                    checks.remove(check.listnode);
+                    check.validate();
+                    BufferPool.put(check.buffer);
+                }
+
+                BufferCheck check;
+                while ((check = shareFrom.poll()) != null)
+                {
+                    check.validate();
+                    BufferPool.put(check.buffer);
+                }
+
+                BufferPool.releaseLocal();
+            }
+
             void cleanup()
             {
                 while (checks.size() > 0)
@@ -427,6 +513,8 @@
     private void startBurnerThreads(TestEnvironment testEnv)
     {
         // setup some high churn allocate/deallocate, without any checking
+
+        final AtomicLong pendingBuffersCount = new AtomicLong(0);
         final SPSCQueue<ByteBuffer> burn = new SPSCQueue<>();
         final CountDownLatch doneAdd = new CountDownLatch(1);
         testEnv.addCheckedFuture(testEnv.executorService.submit(new TestUntil(testEnv.until)
@@ -437,11 +525,12 @@
             {
                 if (count * BufferPool.CHUNK_SIZE >= testEnv.poolSize / 10)
                 {
-                    if (burn.exhausted)
+                    if (pendingBuffersCount.get() == 0)
                     {
                         count = 0;
-                        testEnv.burnFreed.compareAndSet(false, true);
-                    } else
+                        testEnv.maybeSuspendAndFreeMemory(BufferPool::releaseLocal);
+                    }
+                    else
                     {
                         Thread.yield();
                     }
@@ -460,8 +549,10 @@
                 if (rand.nextBoolean())
                     BufferPool.put(buffer);
                 else
+                {
+                    pendingBuffersCount.incrementAndGet();
                     burn.add(buffer);
-
+                }
                 count++;
             }
             void cleanup()
@@ -480,6 +571,7 @@
                     return;
                 }
                 BufferPool.put(buffer);
+                pendingBuffersCount.decrementAndGet();
             }
             void cleanup()
             {
diff --git a/test/conf/cassandra-murmur.yaml b/test/conf/cassandra-murmur.yaml
new file mode 100644
index 0000000..a4b25ba
--- /dev/null
+++ b/test/conf/cassandra-murmur.yaml
@@ -0,0 +1,45 @@
+#
+# Warning!
+# Consider the effects on 'o.a.c.i.s.LegacySSTableTest' before changing schemas in this file.
+#
+cluster_name: Test Cluster
+memtable_allocation_type: heap_buffers
+commitlog_sync: batch
+commitlog_sync_batch_window_in_ms: 1.0
+commitlog_segment_size_in_mb: 5
+commitlog_directory: build/test/cassandra/commitlog
+cdc_raw_directory: build/test/cassandra/cdc_raw
+cdc_enabled: false
+hints_directory: build/test/cassandra/hints
+partitioner: org.apache.cassandra.dht.Murmur3Partitioner
+listen_address: 127.0.0.1
+storage_port: 7010
+rpc_port: 9170
+start_native_transport: true
+native_transport_port: 9042
+column_index_size_in_kb: 4
+saved_caches_directory: build/test/cassandra/saved_caches
+data_file_directories:
+    - build/test/cassandra/data
+disk_access_mode: mmap
+seed_provider:
+    - class_name: org.apache.cassandra.locator.SimpleSeedProvider
+      parameters:
+          - seeds: "127.0.0.1"
+endpoint_snitch: org.apache.cassandra.locator.SimpleSnitch
+dynamic_snitch: true
+request_scheduler: org.apache.cassandra.scheduler.RoundRobinScheduler
+request_scheduler_id: keyspace
+server_encryption_options:
+    internode_encryption: none
+    keystore: conf/.keystore
+    keystore_password: cassandra
+    truststore: conf/.truststore
+    truststore_password: cassandra
+incremental_backups: true
+concurrent_compactors: 4
+compaction_throughput_mb_per_sec: 0
+row_cache_class_name: org.apache.cassandra.cache.OHCProvider
+row_cache_size_in_mb: 16
+enable_user_defined_functions: true
+enable_scripted_user_defined_functions: true
diff --git a/test/conf/cassandra-seeds.yaml b/test/conf/cassandra-seeds.yaml
new file mode 100644
index 0000000..02d25d2
--- /dev/null
+++ b/test/conf/cassandra-seeds.yaml
@@ -0,0 +1,43 @@
+#
+# Warning!
+# Consider the effects on 'o.a.c.i.s.LegacySSTableTest' before changing schemas in this file.
+#
+cluster_name: Test Cluster
+# memtable_allocation_type: heap_buffers
+memtable_allocation_type: offheap_objects
+commitlog_sync: batch
+commitlog_sync_batch_window_in_ms: 1.0
+commitlog_segment_size_in_mb: 5
+commitlog_directory: build/test/cassandra/commitlog
+cdc_raw_directory: build/test/cassandra/cdc_raw
+cdc_enabled: false
+hints_directory: build/test/cassandra/hints
+partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner
+listen_address: 127.0.0.1
+storage_port: 7010
+start_native_transport: true
+native_transport_port: 9042
+column_index_size_in_kb: 4
+saved_caches_directory: build/test/cassandra/saved_caches
+data_file_directories:
+    - build/test/cassandra/data
+disk_access_mode: mmap
+seed_provider:
+    - class_name: org.apache.cassandra.locator.SimpleSeedProvider
+      parameters:
+          - seeds: "127.0.0.10,127.0.1.10,127.0.2.10"
+endpoint_snitch: org.apache.cassandra.locator.SimpleSnitch
+dynamic_snitch: true
+server_encryption_options:
+    internode_encryption: none
+    keystore: conf/.keystore
+    keystore_password: cassandra
+    truststore: conf/.truststore
+    truststore_password: cassandra
+incremental_backups: true
+concurrent_compactors: 4
+compaction_throughput_mb_per_sec: 0
+row_cache_class_name: org.apache.cassandra.cache.OHCProvider
+row_cache_size_in_mb: 16
+enable_user_defined_functions: true
+enable_scripted_user_defined_functions: true
diff --git a/test/conf/cassandra.keystore b/test/conf/cassandra.keystore
new file mode 100644
index 0000000..da88524
--- /dev/null
+++ b/test/conf/cassandra.keystore
Binary files differ
diff --git a/test/conf/cassandra.yaml b/test/conf/cassandra.yaml
index 2536aa8..8e1a0b7 100644
--- a/test/conf/cassandra.yaml
+++ b/test/conf/cassandra.yaml
@@ -3,11 +3,14 @@
 # Consider the effects on 'o.a.c.i.s.LegacySSTableTest' before changing schemas in this file.
 #
 cluster_name: Test Cluster
-memtable_allocation_type: heap_buffers
+# memtable_allocation_type: heap_buffers
+memtable_allocation_type: offheap_objects
 commitlog_sync: batch
 commitlog_sync_batch_window_in_ms: 1.0
 commitlog_segment_size_in_mb: 5
 commitlog_directory: build/test/cassandra/commitlog
+cdc_raw_directory: build/test/cassandra/cdc_raw
+cdc_enabled: false
 hints_directory: build/test/cassandra/hints
 partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner
 listen_address: 127.0.0.1
@@ -41,4 +44,5 @@
 row_cache_size_in_mb: 16
 enable_user_defined_functions: true
 enable_scripted_user_defined_functions: true
+prepared_statements_cache_size_mb: 1
 enable_drop_compact_storage: true
diff --git a/test/conf/cassandra_encryption.yaml b/test/conf/cassandra_encryption.yaml
new file mode 100644
index 0000000..47e1312
--- /dev/null
+++ b/test/conf/cassandra_encryption.yaml
@@ -0,0 +1,14 @@
+transparent_data_encryption_options:
+    enabled: true
+    chunk_length_kb: 2
+    cipher: AES/CBC/PKCS5Padding
+    key_alias: testing:1
+    # CBC requires iv length to be 16 bytes
+    # iv_length: 16
+    key_provider: 
+      - class_name: org.apache.cassandra.security.JKSKeyProvider
+        parameters: 
+          - keystore: test/conf/cassandra.keystore
+            keystore_password: cassandra
+            store_type: JCEKS
+            key_password: cassandra
diff --git a/test/conf/cdc.yaml b/test/conf/cdc.yaml
new file mode 100644
index 0000000..f79930a
--- /dev/null
+++ b/test/conf/cdc.yaml
@@ -0,0 +1 @@
+cdc_enabled: true
diff --git a/test/conf/logback-test.xml b/test/conf/logback-test.xml
index 72550fe..eb8ee57 100644
--- a/test/conf/logback-test.xml
+++ b/test/conf/logback-test.xml
@@ -42,13 +42,13 @@
     </encoder>
     <immediateFlush>false</immediateFlush>
   </appender>
-  
+
   <appender name="STDOUT" target="System.out" class="org.apache.cassandra.ConsoleAppender">
     <encoder>
-      <pattern>%-5level %date{HH:mm:ss,SSS} %msg%n</pattern>
+      <pattern>%-5level [%thread] %date{ISO8601} %F:%L - %msg%n</pattern>
     </encoder>
     <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
-      <level>INFO</level>
+      <level>DEBUG</level>
     </filter>
   </appender>
 
@@ -59,6 +59,8 @@
 
   <logger name="org.apache.hadoop" level="WARN"/>
 
+  <logger name="org.apache.cassandra.db.monitoring" level="DEBUG"/>
+
   <!-- Do not change the name of this appender. LogbackStatusListener uses the thread name
        tied to the appender name to know when to write to real stdout/stderr vs forwarding to logback -->
   <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
@@ -66,6 +68,7 @@
       <maxFlushTime>0</maxFlushTime>
       <queueSize>1024</queueSize>
       <appender-ref ref="TEE"/>
+      <includeCallerData>true</includeCallerData>
   </appender>
 
   <root level="DEBUG">
diff --git a/test/conf/unit-test-conf/test-native-port.yaml b/test/conf/unit-test-conf/test-native-port.yaml
new file mode 100644
index 0000000..a90f100
--- /dev/null
+++ b/test/conf/unit-test-conf/test-native-port.yaml
@@ -0,0 +1,57 @@
+#
+# Warning!
+# Consider the effects on 'o.a.c.i.s.LegacySSTableTest' before changing schemas in this file.
+#
+cluster_name: Test Cluster
+# memtable_allocation_type: heap_buffers
+memtable_allocation_type: offheap_objects
+commitlog_sync: batch
+commitlog_sync_batch_window_in_ms: 1.0
+commitlog_segment_size_in_mb: 5
+commitlog_directory: build/test/cassandra/commitlog
+cdc_raw_directory: build/test/cassandra/cdc_raw
+cdc_enabled: false
+hints_directory: build/test/cassandra/hints
+partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner
+listen_address: 127.0.0.1
+storage_port: 7010
+rpc_port: 9170
+start_native_transport: true
+native_transport_port_ssl: 9142
+column_index_size_in_kb: 4
+saved_caches_directory: build/test/cassandra/saved_caches
+data_file_directories:
+    - build/test/cassandra/data
+disk_access_mode: mmap
+seed_provider:
+    - class_name: org.apache.cassandra.locator.SimpleSeedProvider
+      parameters:
+          - seeds: "127.0.0.1"
+endpoint_snitch: org.apache.cassandra.locator.SimpleSnitch
+dynamic_snitch: true
+request_scheduler: org.apache.cassandra.scheduler.RoundRobinScheduler
+request_scheduler_id: keyspace
+server_encryption_options:
+    internode_encryption: none
+    keystore: conf/.keystore
+    keystore_password: cassandra
+    truststore: conf/.truststore
+    truststore_password: cassandra
+incremental_backups: true
+concurrent_compactors: 4
+compaction_throughput_mb_per_sec: 0
+row_cache_class_name: org.apache.cassandra.cache.OHCProvider
+row_cache_size_in_mb: 16
+enable_user_defined_functions: true
+enable_scripted_user_defined_functions: true
+prepared_statements_cache_size_mb: 1
+client_encryption_options:
+  enabled: true
+  # If enabled and optional is set to true encrypted and unencrypted connections are handled.
+  optional: false
+  keystore: conf/cassandra_ssl_test.keystore
+  keystore_password: cassandra
+  # require_client_auth: false
+  # Set trustore and truststore_password if require_client_auth is true
+  truststore: conf/cassandra_ssl_test.truststore
+  truststore_password: cassandra
diff --git a/test/data/bloom-filter/ka/foo.cql b/test/data/bloom-filter/ka/foo.cql
index 1a3ad26..6b662fb 100644
--- a/test/data/bloom-filter/ka/foo.cql
+++ b/test/data/bloom-filter/ka/foo.cql
@@ -77,6 +77,6 @@
 Estimated droppable tombstones: 0.0
 SSTable Level: 0
 Repaired at: 0
-ReplayPosition(segmentId=1428529465658, position=6481)
+CommitLogPosition(segmentId=1428529465658, position=6481)
 Estimated tombstone drop times:%n
 
diff --git a/test/data/legacy-commitlog/3.4-encrypted/CommitLog-6-1480001790338.log b/test/data/legacy-commitlog/3.4-encrypted/CommitLog-6-1480001790338.log
new file mode 100644
index 0000000..2a04c6f
--- /dev/null
+++ b/test/data/legacy-commitlog/3.4-encrypted/CommitLog-6-1480001790338.log
Binary files differ
diff --git a/test/data/legacy-commitlog/3.4-encrypted/CommitLog-6-1480001790339.log b/test/data/legacy-commitlog/3.4-encrypted/CommitLog-6-1480001790339.log
new file mode 100644
index 0000000..8606496
--- /dev/null
+++ b/test/data/legacy-commitlog/3.4-encrypted/CommitLog-6-1480001790339.log
Binary files differ
diff --git a/test/data/legacy-commitlog/3.4-encrypted/hash.txt b/test/data/legacy-commitlog/3.4-encrypted/hash.txt
new file mode 100644
index 0000000..36e29ce
--- /dev/null
+++ b/test/data/legacy-commitlog/3.4-encrypted/hash.txt
@@ -0,0 +1,5 @@
+#CommitLog upgrade test, version 3.4-SNAPSHOT
+#Thu Nov 24 16:36:34 CET 2016
+cells=14561
+hash=2026350947
+cfid=c5b59a80-b25b-11e6-b205-bb359f887ad3
diff --git a/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-CompressionInfo.db b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-CompressionInfo.db
new file mode 100644
index 0000000..3793e50
--- /dev/null
+++ b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-CompressionInfo.db
Binary files differ
diff --git a/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Data.db b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Data.db
new file mode 100644
index 0000000..94c6f93
--- /dev/null
+++ b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Data.db
Binary files differ
diff --git a/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Digest.sha1 b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Digest.sha1
new file mode 100644
index 0000000..60bd60d
--- /dev/null
+++ b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Digest.sha1
@@ -0,0 +1 @@
+718738748
\ No newline at end of file
diff --git a/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Filter.db b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Filter.db
new file mode 100644
index 0000000..00a88b4
--- /dev/null
+++ b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Filter.db
Binary files differ
diff --git a/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Index.db b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Index.db
new file mode 100644
index 0000000..c3b42d8
--- /dev/null
+++ b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Index.db
Binary files differ
diff --git a/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Statistics.db b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Statistics.db
new file mode 100644
index 0000000..d708358
--- /dev/null
+++ b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Statistics.db
Binary files differ
diff --git a/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Summary.db b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Summary.db
new file mode 100644
index 0000000..6bfc8aa
--- /dev/null
+++ b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-Summary.db
Binary files differ
diff --git a/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-TOC.txt b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-TOC.txt
new file mode 100644
index 0000000..5ece1a1
--- /dev/null
+++ b/test/data/legacy-sstables/ka/legacy_tables/legacy_ka_15081/legacy_tables-legacy_ka_15081-ka-1-TOC.txt
@@ -0,0 +1,8 @@
+Digest.sha1
+Data.db
+Statistics.db
+Summary.db
+Index.db
+TOC.txt
+Filter.db
+CompressionInfo.db
diff --git a/test/data/negative-local-expiration-test/table1/mc-1-big-Data.db b/test/data/negative-local-expiration-test/table1/mc-1-big-Data.db
index e7a72da..cb96af3 100644
--- a/test/data/negative-local-expiration-test/table1/mc-1-big-Data.db
+++ b/test/data/negative-local-expiration-test/table1/mc-1-big-Data.db
Binary files differ
diff --git a/test/data/negative-local-expiration-test/table1/mc-1-big-Digest.crc32 b/test/data/negative-local-expiration-test/table1/mc-1-big-Digest.crc32
index a3c633a..44c47fb 100644
--- a/test/data/negative-local-expiration-test/table1/mc-1-big-Digest.crc32
+++ b/test/data/negative-local-expiration-test/table1/mc-1-big-Digest.crc32
@@ -1 +1 @@
-203700622
\ No newline at end of file
+4223695539
\ No newline at end of file
diff --git a/test/data/negative-local-expiration-test/table1/mc-1-big-Statistics.db b/test/data/negative-local-expiration-test/table1/mc-1-big-Statistics.db
index faf367b..ebcf4c8 100644
--- a/test/data/negative-local-expiration-test/table1/mc-1-big-Statistics.db
+++ b/test/data/negative-local-expiration-test/table1/mc-1-big-Statistics.db
Binary files differ
diff --git a/test/data/negative-local-expiration-test/table1/mc-1-big-TOC.txt b/test/data/negative-local-expiration-test/table1/mc-1-big-TOC.txt
index 45113dc..831e376 100644
--- a/test/data/negative-local-expiration-test/table1/mc-1-big-TOC.txt
+++ b/test/data/negative-local-expiration-test/table1/mc-1-big-TOC.txt
@@ -1,8 +1,8 @@
-CompressionInfo.db
-Data.db
-Summary.db
-Filter.db
-Statistics.db
-TOC.txt
 Digest.crc32
+CompressionInfo.db
 Index.db
+TOC.txt
+Data.db
+Statistics.db
+Filter.db
+Summary.db
diff --git a/test/data/negative-local-expiration-test/table2/mc-1-big-Data.db b/test/data/negative-local-expiration-test/table2/mc-1-big-Data.db
index c1de572..8f41a21 100644
--- a/test/data/negative-local-expiration-test/table2/mc-1-big-Data.db
+++ b/test/data/negative-local-expiration-test/table2/mc-1-big-Data.db
Binary files differ
diff --git a/test/data/negative-local-expiration-test/table2/mc-1-big-Digest.crc32 b/test/data/negative-local-expiration-test/table2/mc-1-big-Digest.crc32
index 0403b5b..da919fe 100644
--- a/test/data/negative-local-expiration-test/table2/mc-1-big-Digest.crc32
+++ b/test/data/negative-local-expiration-test/table2/mc-1-big-Digest.crc32
@@ -1 +1 @@
-82785930
\ No newline at end of file
+2886964045
\ No newline at end of file
diff --git a/test/data/negative-local-expiration-test/table2/mc-1-big-Statistics.db b/test/data/negative-local-expiration-test/table2/mc-1-big-Statistics.db
index e9d6577..549dabe 100644
--- a/test/data/negative-local-expiration-test/table2/mc-1-big-Statistics.db
+++ b/test/data/negative-local-expiration-test/table2/mc-1-big-Statistics.db
Binary files differ
diff --git a/test/data/negative-local-expiration-test/table2/mc-1-big-TOC.txt b/test/data/negative-local-expiration-test/table2/mc-1-big-TOC.txt
index 45113dc..831e376 100644
--- a/test/data/negative-local-expiration-test/table2/mc-1-big-TOC.txt
+++ b/test/data/negative-local-expiration-test/table2/mc-1-big-TOC.txt
@@ -1,8 +1,8 @@
-CompressionInfo.db
-Data.db
-Summary.db
-Filter.db
-Statistics.db
-TOC.txt
 Digest.crc32
+CompressionInfo.db
 Index.db
+TOC.txt
+Data.db
+Statistics.db
+Filter.db
+Summary.db
diff --git a/test/data/negative-local-expiration-test/table3/mc-1-big-Data.db b/test/data/negative-local-expiration-test/table3/mc-1-big-Data.db
index e96f772..008d3e8 100644
--- a/test/data/negative-local-expiration-test/table3/mc-1-big-Data.db
+++ b/test/data/negative-local-expiration-test/table3/mc-1-big-Data.db
Binary files differ
diff --git a/test/data/negative-local-expiration-test/table3/mc-1-big-Digest.crc32 b/test/data/negative-local-expiration-test/table3/mc-1-big-Digest.crc32
index 459804b..0bdc0bf 100644
--- a/test/data/negative-local-expiration-test/table3/mc-1-big-Digest.crc32
+++ b/test/data/negative-local-expiration-test/table3/mc-1-big-Digest.crc32
@@ -1 +1 @@
-3064924389
\ No newline at end of file
+3254141434
\ No newline at end of file
diff --git a/test/data/negative-local-expiration-test/table3/mc-1-big-Statistics.db b/test/data/negative-local-expiration-test/table3/mc-1-big-Statistics.db
index 1ee01e6..62bf84e 100644
--- a/test/data/negative-local-expiration-test/table3/mc-1-big-Statistics.db
+++ b/test/data/negative-local-expiration-test/table3/mc-1-big-Statistics.db
Binary files differ
diff --git a/test/data/negative-local-expiration-test/table3/mc-1-big-TOC.txt b/test/data/negative-local-expiration-test/table3/mc-1-big-TOC.txt
index f445537..831e376 100644
--- a/test/data/negative-local-expiration-test/table3/mc-1-big-TOC.txt
+++ b/test/data/negative-local-expiration-test/table3/mc-1-big-TOC.txt
@@ -1,8 +1,8 @@
-Summary.db
-TOC.txt
-Filter.db
-Index.db
 Digest.crc32
 CompressionInfo.db
+Index.db
+TOC.txt
 Data.db
 Statistics.db
+Filter.db
+Summary.db
diff --git a/test/data/negative-local-expiration-test/table4/mc-1-big-Data.db b/test/data/negative-local-expiration-test/table4/mc-1-big-Data.db
index a22a7a3..128ea47 100644
--- a/test/data/negative-local-expiration-test/table4/mc-1-big-Data.db
+++ b/test/data/negative-local-expiration-test/table4/mc-1-big-Data.db
Binary files differ
diff --git a/test/data/negative-local-expiration-test/table4/mc-1-big-Digest.crc32 b/test/data/negative-local-expiration-test/table4/mc-1-big-Digest.crc32
index db7a6c7..9d52209 100644
--- a/test/data/negative-local-expiration-test/table4/mc-1-big-Digest.crc32
+++ b/test/data/negative-local-expiration-test/table4/mc-1-big-Digest.crc32
@@ -1 +1 @@
-1803989939
\ No newline at end of file
+3231150985
\ No newline at end of file
diff --git a/test/data/negative-local-expiration-test/table4/mc-1-big-Statistics.db b/test/data/negative-local-expiration-test/table4/mc-1-big-Statistics.db
index 4ee9294..4eee729 100644
--- a/test/data/negative-local-expiration-test/table4/mc-1-big-Statistics.db
+++ b/test/data/negative-local-expiration-test/table4/mc-1-big-Statistics.db
Binary files differ
diff --git a/test/data/negative-local-expiration-test/table4/mc-1-big-TOC.txt b/test/data/negative-local-expiration-test/table4/mc-1-big-TOC.txt
index f445537..831e376 100644
--- a/test/data/negative-local-expiration-test/table4/mc-1-big-TOC.txt
+++ b/test/data/negative-local-expiration-test/table4/mc-1-big-TOC.txt
@@ -1,8 +1,8 @@
-Summary.db
-TOC.txt
-Filter.db
-Index.db
 Digest.crc32
 CompressionInfo.db
+Index.db
+TOC.txt
 Data.db
 Statistics.db
+Filter.db
+Summary.db
diff --git a/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java b/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java
index 83dbfbc..cffbbc4 100644
--- a/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java
+++ b/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java
@@ -77,6 +77,8 @@
 import org.apache.cassandra.utils.concurrent.SimpleCondition;
 import org.reflections.Reflections;
 
+import static org.apache.cassandra.utils.MBeanWrapper.DTEST_IS_IN_JVM_DTEST;
+
 /**
  * AbstractCluster creates, initializes and manages Cassandra instances ({@link Instance}.
  *
@@ -142,6 +144,9 @@
 
     private volatile Thread.UncaughtExceptionHandler previousHandler = null;
 
+    {
+        System.setProperty(DTEST_IS_IN_JVM_DTEST, "true");
+    }
     protected class Wrapper extends DelegatingInvokableInstance implements IUpgradeableInstance
     {
         private final int generation;
@@ -150,7 +155,7 @@
         private volatile Versions.Version version;
         private volatile boolean isShutdown = true;
 
-        protected IInvokableInstance delegate()
+        public IInvokableInstance delegate()
         {
             if (delegate == null)
                 throw new IllegalStateException("Can't use shut down instances, delegate is null");
@@ -390,6 +395,16 @@
         return instances.get(node - 1).coordinator();
     }
 
+    public List<I> get(int... nodes)
+    {
+        if (nodes == null || nodes.length == 0)
+            throw new IllegalArgumentException("No nodes provided");
+        List<I> list = new ArrayList<>(nodes.length);
+        for (int i : nodes)
+            list.add(get(i));
+        return list;
+    }
+
     /**
      * WARNING: we index from 1 here, for consistency with inet address!
      */
diff --git a/test/distributed/org/apache/cassandra/distributed/impl/CollectingRMIServerSocketFactoryImpl.java b/test/distributed/org/apache/cassandra/distributed/impl/CollectingRMIServerSocketFactoryImpl.java
new file mode 100644
index 0000000..5e67eaf
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/impl/CollectingRMIServerSocketFactoryImpl.java
@@ -0,0 +1,87 @@
+/*
+ * 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.distributed.impl;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.SocketException;
+import java.rmi.server.RMIServerSocketFactory;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import javax.net.ServerSocketFactory;
+
+
+/**
+ * This class is used to keep track of RMI servers created during a cluster creation so we can
+ * later close the sockets, which would otherwise be left with a thread running waiting for
+ * connections that would never show up as the server was otherwise closed.
+ */
+class CollectingRMIServerSocketFactoryImpl implements RMIServerSocketFactory
+{
+    private final InetAddress bindAddress;
+    List<ServerSocket> sockets = new ArrayList<>();
+
+    public CollectingRMIServerSocketFactoryImpl(InetAddress bindAddress)
+    {
+        this.bindAddress = bindAddress;
+    }
+
+    @Override
+    public ServerSocket createServerSocket(int pPort) throws IOException
+    {
+        ServerSocket result = ServerSocketFactory.getDefault().createServerSocket(pPort, 0, bindAddress);
+        try
+        {
+            result.setReuseAddress(true);
+        }
+        catch (SocketException e)
+        {
+            result.close();
+            throw e;
+        }
+        sockets.add(result);
+        return result;
+    }
+
+
+    public void close() throws IOException
+    {
+        for (ServerSocket socket : sockets)
+        {
+            socket.close();
+        }
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        CollectingRMIServerSocketFactoryImpl that = (CollectingRMIServerSocketFactoryImpl) o;
+        return Objects.equals(bindAddress, that.bindAddress);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(bindAddress);
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Coordinator.java b/test/distributed/org/apache/cassandra/distributed/impl/Coordinator.java
index b5b5e7e..d62a787 100644
--- a/test/distributed/org/apache/cassandra/distributed/impl/Coordinator.java
+++ b/test/distributed/org/apache/cassandra/distributed/impl/Coordinator.java
@@ -30,7 +30,6 @@
 
 import com.google.common.collect.Iterators;
 
-import com.datastax.driver.core.ProtocolVersion;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.cql3.CQLStatement;
 import org.apache.cassandra.cql3.QueryOptions;
@@ -49,7 +48,7 @@
 import org.apache.cassandra.service.QueryState;
 import org.apache.cassandra.service.pager.PagingState;
 import org.apache.cassandra.service.pager.QueryPager;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.tracing.Tracing;
 import org.apache.cassandra.transport.messages.ResultMessage;
 import org.apache.cassandra.utils.ByteBufferUtil;
@@ -74,7 +73,7 @@
         return instance.async(() -> {
             try
             {
-                Tracing.instance.newSession(sessionId);
+                Tracing.instance.newSession(sessionId, Collections.emptyMap());
                 return executeInternal(query, consistencyLevelOrigin, boundValues);
             }
             finally
@@ -111,7 +110,8 @@
                                                                  Integer.MAX_VALUE,
                                                                  null,
                                                                  null,
-                                                                 Server.CURRENT_VERSION));
+                                                                 ProtocolVersion.CURRENT),
+                                             System.nanoTime());
 
         // Collect warnings reported during the query.
         if (res != null)
@@ -149,6 +149,7 @@
             prepared.validate(clientState);
             assert prepared instanceof SelectStatement : "Only SELECT statements can be executed with paging";
 
+            long nanoTime = System.nanoTime();
             SelectStatement selectStatement = (SelectStatement) prepared;
 
             QueryState queryState = new QueryState(clientState);
@@ -158,12 +159,12 @@
                                                               pageSize,
                                                               null,
                                                               null,
-                                                              Server.CURRENT_VERSION);
+                                                              ProtocolVersion.CURRENT);
 
 
-            ResultMessage.Rows initialRows = selectStatement.execute(queryState, initialOptions);
+            ResultMessage.Rows initialRows = selectStatement.execute(queryState, initialOptions, nanoTime);
             Iterator<Object[]> iter = new Iterator<Object[]>() {
-                ResultMessage.Rows rows = selectStatement.execute(queryState, initialOptions);
+                ResultMessage.Rows rows = selectStatement.execute(queryState, initialOptions, nanoTime);
                 Iterator<Object[]> iter = RowUtil.toIter(rows);
 
                 public boolean hasNext()
@@ -180,9 +181,9 @@
                                                                    pageSize,
                                                                    rows.result.metadata.getPagingState(),
                                                                    null,
-                                                                   Server.CURRENT_VERSION);
+                                                                   ProtocolVersion.CURRENT);
 
-                    rows = selectStatement.execute(queryState, nextOptions);
+                    rows = selectStatement.execute(queryState, nextOptions, nanoTime);
                     iter = Iterators.forArray(RowUtil.toObjects(initialRows.result.metadata.names, rows.result.rows));
 
                     return hasNext();
diff --git a/test/distributed/org/apache/cassandra/distributed/impl/DelegatingInvokableInstance.java b/test/distributed/org/apache/cassandra/distributed/impl/DelegatingInvokableInstance.java
index 90260a4..bee474b 100644
--- a/test/distributed/org/apache/cassandra/distributed/impl/DelegatingInvokableInstance.java
+++ b/test/distributed/org/apache/cassandra/distributed/impl/DelegatingInvokableInstance.java
@@ -34,10 +34,11 @@
 import org.apache.cassandra.distributed.api.IListen;
 import org.apache.cassandra.distributed.api.IMessage;
 import org.apache.cassandra.distributed.api.SimpleQueryResult;
+import org.apache.cassandra.distributed.shared.NetworkTopology;
 
 public abstract class DelegatingInvokableInstance implements IInvokableInstance
 {
-    protected abstract IInvokableInstance delegate();
+    public abstract IInvokableInstance delegate();
     protected abstract IInvokableInstance delegateForStartup();
     
     @Override
@@ -88,17 +89,17 @@
     }
 
     @Override
-    public String getReleaseVersionString()
-    {
-        return delegate().getReleaseVersionString();
-    }
-
-    @Override
     public void setMessagingVersion(InetSocketAddress endpoint, int version)
     {
         delegate().setMessagingVersion(endpoint, version);
     }
 
+    @Override
+    public String getReleaseVersionString()
+    {
+        return delegate().getReleaseVersionString();
+    }
+
     public void flush(String keyspace)
     {
         delegate().flush(keyspace);
@@ -254,6 +255,11 @@
     }
 
     @Override
+    public <O> O callOnInstance(SerializableCallable<O> call)
+    {
+        return delegate().callOnInstance(call);
+    }
+
     public <I1, I2, I3, I4, O> QuadFunction<I1, I2, I3, I4, Future<O>> async(QuadFunction<I1, I2, I3, I4, O> f)
     {
         return delegate().async(f);
diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java
index 4655ed4..cca6296 100644
--- a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java
+++ b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java
@@ -44,6 +44,9 @@
 import javax.management.Notification;
 import javax.management.NotificationListener;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.apache.cassandra.batchlog.BatchlogManager;
 import org.apache.cassandra.concurrent.ScheduledExecutors;
 import org.apache.cassandra.concurrent.SharedExecutorPool;
@@ -51,6 +54,7 @@
 import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.config.YamlConfigurationLoader;
 import org.apache.cassandra.cql3.CQLStatement;
 import org.apache.cassandra.cql3.QueryOptions;
@@ -62,6 +66,7 @@
 import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.db.commitlog.CommitLog;
 import org.apache.cassandra.db.compaction.CompactionManager;
+import org.apache.cassandra.db.monitoring.ApproximateTime;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.distributed.Constants;
@@ -85,14 +90,12 @@
 import org.apache.cassandra.hints.DTestSerializer;
 import org.apache.cassandra.hints.HintsService;
 import org.apache.cassandra.index.SecondaryIndexManager;
-import org.apache.cassandra.io.IVersionedSerializer;
 import org.apache.cassandra.io.sstable.IndexSummaryManager;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.util.DataInputBuffer;
 import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.metrics.CassandraMetricsRegistry;
-import org.apache.cassandra.net.CompactEndpointSerializationHelper;
 import org.apache.cassandra.net.IMessageSink;
 import org.apache.cassandra.net.MessageIn;
 import org.apache.cassandra.net.MessageOut;
@@ -106,6 +109,7 @@
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.service.StorageServiceMBean;
 import org.apache.cassandra.streaming.StreamCoordinator;
+import org.apache.cassandra.streaming.StreamSession;
 import org.apache.cassandra.tools.NodeTool;
 import org.apache.cassandra.tools.Output;
 import org.apache.cassandra.tracing.TraceState;
@@ -115,8 +119,6 @@
 import org.apache.cassandra.utils.ExecutorUtils;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.JVMStabilityInspector;
-import org.apache.cassandra.utils.NanoTimeToCurrentTimeMillis;
-import org.apache.cassandra.utils.Pair;
 import org.apache.cassandra.utils.Throwables;
 import org.apache.cassandra.utils.UUIDGen;
 import org.apache.cassandra.utils.concurrent.Ref;
@@ -124,13 +126,16 @@
 
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
+import static org.apache.cassandra.distributed.api.Feature.JMX;
 import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
 import static org.apache.cassandra.distributed.api.Feature.NETWORK;
 
 public class Instance extends IsolatedExecutor implements IInvokableInstance
 {
+    private Logger inInstancelogger; // Defer creation until running in the instance context
     public final IInstanceConfig config;
     private volatile boolean initialized = false;
+    private IsolatedJmx isolatedJmx;
 
     // should never be invoked directly, so that it is instantiated on other class loader;
     // only visible for inheritance
@@ -146,7 +151,8 @@
         // Set the config at instance creation, possibly before startup() has run on all other instances.
         // setMessagingVersions below will call runOnInstance which will instantiate
         // the MessagingService and dependencies preventing later changes to network parameters.
-        Config.setOverrideLoadConfig(() -> loadConfig(config));
+        Config single = loadConfig(config);
+        Config.setOverrideLoadConfig(() -> single);
     }
 
     @Override
@@ -224,7 +230,7 @@
             try
             {
                 ClientState state = ClientState.forInternalCalls();
-                state.setKeyspace(SystemKeyspace.NAME);
+                state.setKeyspace(SchemaConstants.SYSTEM_KEYSPACE_NAME);
                 QueryState queryState = new QueryState(state);
 
                 CQLStatement statement = QueryProcessor.parseStatement(query, queryState).statement;
@@ -298,7 +304,6 @@
                 int fromNum = from.config().num();
                 int toNum = config().num();
 
-
                 IMessage msg = serializeMessage(message, id, from.config().broadcastAddress(), broadcastAddress());
 
                 return cluster.filters().permitInbound(fromNum, toNum, msg);
@@ -330,11 +335,10 @@
         }
     }
 
-    public static IMessage serializeMessage(MessageIn<?> messageIn, int id, InetSocketAddress from, InetSocketAddress to)
+    public static IMessage serializeMessage(MessageIn messageIn, int id, InetSocketAddress from, InetSocketAddress to)
     {
         try (DataOutputBuffer out = new DataOutputBuffer(1024))
         {
-            // Serialize header
             int version = MessagingService.instance().getVersion(to.getAddress());
 
             out.writeInt(MessagingService.PROTOCOL_MAGIC);
@@ -342,35 +346,12 @@
             long timestamp = System.currentTimeMillis();
             out.writeInt((int) timestamp);
 
-            // Serialize the message itself
-            IVersionedSerializer serializer = MessagingService.instance().verbSerializers.get(messageIn.verb);
-            CompactEndpointSerializationHelper.serialize(from.getAddress(), out);
-
-            out.writeInt(MessagingService.Verb.convertForMessagingServiceVersion(messageIn.verb, version).ordinal());
-            out.writeInt(messageIn.parameters.size());
-            for (Map.Entry<String, byte[]> entry : messageIn.parameters.entrySet())
-            {
-                out.writeUTF(entry.getKey());
-                out.writeInt(entry.getValue().length);
-                out.write(entry.getValue());
-            }
-
-            if (messageIn.payload != null && serializer != MessagingService.CallbackDeterminedSerializer.instance)
-            {
-                try (DataOutputBuffer dob = new DataOutputBuffer())
-                {
-                    serializer.serialize(messageIn.payload, dob, version);
-
-                    int size = dob.getLength();
-                    out.writeInt(size);
-                    out.write(dob.getData(), 0, size);
-                }
-            }
-            else
-            {
-                out.writeInt(0);
-            }
-
+            MessageOut.serialize(out,
+                                 from.getAddress(),
+                                 messageIn.verb,
+                                 messageIn.parameters,
+                                 messageIn.payload,
+                                 version);
 
             return new MessageImpl(messageIn.verb.ordinal(), out.toByteArray(), id, version, from);
         }
@@ -403,17 +384,17 @@
             {
                 UUID sessionId = UUIDGen.getUUID(ByteBuffer.wrap(sessionBytes));
                 TraceState state = Tracing.instance.get(sessionId);
-                String traceMessage = String.format("Sending %s message to %s", messageOut.verb, to);
+                String message = String.format("Sending %s message to %s", messageOut.verb, to);
                 // session may have already finished; see CASSANDRA-5668
                 if (state == null)
                 {
                     byte[] traceTypeBytes = (byte[]) messageOut.parameters.get(Tracing.TRACE_TYPE);
                     Tracing.TraceType traceType = traceTypeBytes == null ? Tracing.TraceType.QUERY : Tracing.TraceType.deserialize(traceTypeBytes[0]);
-                    TraceState.mutateWithTracing(ByteBuffer.wrap(sessionBytes), traceMessage, -1, traceType.getTTL());
+                    Tracing.instance.trace(ByteBuffer.wrap(sessionBytes), message, traceType.getTTL());
                 }
                 else
                 {
-                    state.trace(traceMessage);
+                    state.trace(message);
                     if (messageOut.verb == MessagingService.Verb.REQUEST_RESPONSE)
                         Tracing.instance.doneWithNonLocalSession(state);
                 }
@@ -433,7 +414,7 @@
         }
     }
 
-    public static Pair<MessageIn<Object>, Integer> deserializeMessage(IMessage imessage)
+    public static MessageIn<Object> deserializeMessage(IMessage imessage)
     {
         // Based on org.apache.cassandra.net.IncomingTcpConnection.receiveMessage
         try (DataInputBuffer input = new DataInputBuffer(imessage.bytes()))
@@ -452,19 +433,12 @@
                 id = Integer.parseInt(input.readUTF());
             else
                 id = input.readInt();
-            if (imessage.id() != id)
-                throw new IllegalStateException(String.format("Message id mismatch: %d != %d", imessage.id(), id));
-
-            // make sure to readInt, even if cross_node_to is not enabled
-            int partial = input.readInt();
-
-            return Pair.create(MessageIn.read(input, version, id), partial);
-            //long currentTime = ApproximateTime.currentTimeMillis();
-            //return MessageIn.read(input, version, id, MessageIn.readConstructionTime(imessage.from().getAddress(), input, currentTime));
+            long currentTime = ApproximateTime.currentTimeMillis();
+            return MessageIn.read(input, version, id, MessageIn.readConstructionTime(imessage.from().getAddress(), input, currentTime));
         }
-        catch (IOException e)
+        catch (Throwable t)
         {
-            throw new RuntimeException(e);
+            throw new RuntimeException(t);
         }
     }
 
@@ -476,38 +450,25 @@
     @Override
     public void receiveMessageWithInvokingThread(IMessage imessage)
     {
-        Pair<MessageIn<Object>, Integer> deserialized = null;
+        // Based on org.apache.cassandra.net.IncomingTcpConnection.receiveMessage
         try
         {
-            deserialized = deserializeMessage(imessage);
+            MessageIn message = deserializeMessage(imessage);
+            if (message == null)
+            {
+                // callback expired; nothing to do
+                return;
+            }
+            if (message.version <= MessagingService.current_version)
+            {
+                MessagingService.instance().receive(message, imessage.id());
+            }
+            // else ignore message
         }
         catch (Throwable t)
         {
             throw new RuntimeException("Exception occurred on node " + broadcastAddress(), t);
         }
-
-        MessageIn<Object> message = deserialized.left;
-        int partial = deserialized.right;
-
-        long timestamp = System.currentTimeMillis();
-        boolean isCrossNodeTimestamp = false;
-
-        if (DatabaseDescriptor.hasCrossNodeTimeout())
-        {
-            long crossNodeTimestamp = (timestamp & 0xFFFFFFFF00000000L) | (((partial & 0xFFFFFFFFL) << 2) >> 2);
-            isCrossNodeTimestamp = (timestamp != crossNodeTimestamp);
-            timestamp = crossNodeTimestamp;
-        }
-
-        if (message == null)
-        {
-            // callback expired; nothing to do
-            return;
-        }
-        if (message.version <= MessagingService.current_version)
-        {
-            MessagingService.instance().receive(message, imessage.id(), timestamp, isCrossNodeTimestamp);
-        }
     }
 
     public int getMessagingVersion()
@@ -548,6 +509,7 @@
     public void startup(ICluster cluster)
     {
         sync(() -> {
+            inInstancelogger = LoggerFactory.getLogger(Instance.class);
             try
             {
                 if (config.has(GOSSIP))
@@ -563,7 +525,10 @@
                 assert config.networkTopology().contains(config.broadcastAddress());
                 DistributedTestSnitch.assign(config.networkTopology());
 
-                DatabaseDescriptor.setDaemonInitialized();
+                if (config.has(JMX))
+                    startJmx();
+
+                DatabaseDescriptor.daemonInitialization();
                 FileUtils.setFSErrorHandler(new DefaultFSErrorHandler());
                 DatabaseDescriptor.createAllDirectories();
 
@@ -589,7 +554,7 @@
                 // Replay any CommitLogSegments found on disk
                 try
                 {
-                    CommitLog.instance.recover();
+                    CommitLog.instance.recoverSegmentsOnDisk();
                 }
                 catch (IOException e)
                 {
@@ -649,6 +614,20 @@
         initialized = true;
     }
 
+    private void startJmx()
+    {
+        isolatedJmx = new IsolatedJmx(this, inInstancelogger);
+        isolatedJmx.startJmx();
+    }
+
+    private void stopJmx() throws IllegalAccessException, NoSuchFieldException, InterruptedException
+    {
+        if (config.has(JMX))
+        {
+            isolatedJmx.stopJmx();
+        }
+    }
+
     private void mkdirs()
     {
         new File(config.getString("saved_caches_directory")).mkdirs();
@@ -770,10 +749,6 @@
             if (config.has(GOSSIP) || config.has(NETWORK))
             {
                 StorageService.instance.shutdownServer();
-
-                error = parallelRun(error, executor,
-                                    () -> NanoTimeToCurrentTimeMillis.shutdown(MINUTES.toMillis(1L))
-                );
             }
 
             error = parallelRun(error, executor, StorageService.instance::disableAutoCompaction);
@@ -784,12 +759,12 @@
                                 () -> BatchlogManager.instance.shutdownAndWait(1L, MINUTES),
                                 HintsService.instance::shutdownBlocking,
                                 () -> StreamCoordinator.shutdownAndWait(1L, MINUTES),
+                                () -> StreamSession.shutdownAndWait(1L, MINUTES),
                                 () -> SecondaryIndexManager.shutdownAndWait(1L, MINUTES),
                                 () -> IndexSummaryManager.instance.shutdownAndWait(1L, MINUTES),
                                 () -> ColumnFamilyStore.shutdownExecutorsAndWait(1L, MINUTES),
                                 () -> PendingRangeCalculatorService.instance.shutdownExecutor(1L, MINUTES),
                                 () -> BufferPool.shutdownLocalCleaner(1L, MINUTES),
-                                () -> StorageService.instance.shutdownBGMonitorAndWait(1L, MINUTES),
                                 () -> Ref.shutdownReferenceReaper(1L, MINUTES),
                                 () -> Memtable.MEMORY_POOL.shutdownAndWait(1L, MINUTES),
                                 () -> SSTableReader.shutdownBlocking(1L, MINUTES),
@@ -807,6 +782,8 @@
                                 CommitLog.instance::shutdownBlocking
             );
 
+            error = parallelRun(error, executor, this::stopJmx);
+
             Throwables.maybeFail(error);
         }).apply(isolatedExecutor);
 
diff --git a/test/distributed/org/apache/cassandra/distributed/impl/InstanceConfig.java b/test/distributed/org/apache/cassandra/distributed/impl/InstanceConfig.java
index d6180c2..7f6af4b 100644
--- a/test/distributed/org/apache/cassandra/distributed/impl/InstanceConfig.java
+++ b/test/distributed/org/apache/cassandra/distributed/impl/InstanceConfig.java
@@ -46,6 +46,8 @@
     private static final Logger logger = LoggerFactory.getLogger(InstanceConfig.class);
 
     public final int num;
+    private final int jmxPort;
+
     public int num() { return num; }
 
     private final NetworkTopology networkTopology;
@@ -71,8 +73,9 @@
                            String[] data_file_directories,
                            String commitlog_directory,
                            String hints_directory,
-//                           String cdc_directory,
-                           String initial_token)
+                           String cdc_raw_directory,
+                           String initial_token,
+                           int jmx_port)
     {
         this.num = num;
         this.networkTopology = networkTopology;
@@ -86,7 +89,7 @@
                 .set("data_file_directories", data_file_directories)
                 .set("commitlog_directory", commitlog_directory)
                 .set("hints_directory", hints_directory)
-//                .set("cdc_directory", cdc_directory)
+                .set("cdc_raw_directory", cdc_raw_directory)
                 .set("initial_token", initial_token)
                 .set("partitioner", "org.apache.cassandra.dht.Murmur3Partitioner")
                 .set("start_native_transport", true)
@@ -110,6 +113,7 @@
                 // legacy parameters
                 .forceSet("commitlog_sync_batch_window_in_ms", 1.0);
         this.featureFlags = EnumSet.noneOf(Feature.class);
+        this.jmxPort = jmx_port;
     }
 
     private InstanceConfig(InstanceConfig copy)
@@ -121,6 +125,7 @@
         this.hostId = copy.hostId;
         this.featureFlags = copy.featureFlags;
         this.broadcastAddressAndPort = copy.broadcastAddressAndPort;
+        this.jmxPort = copy.jmxPort;
     }
 
 
@@ -161,6 +166,12 @@
         return networkTopology().localDC(broadcastAddress());
     }
 
+    @Override
+    public int jmxPort()
+    {
+        return this.jmxPort;
+    }
+
     public InstanceConfig with(Feature featureFlag)
     {
         featureFlags.add(featureFlag);
@@ -250,8 +261,9 @@
                                   datadirs(datadirCount, root, nodeNum),
                                   String.format("%s/node%d/commitlog", root, nodeNum),
                                   String.format("%s/node%d/hints", root, nodeNum),
-//                                  String.format("%s/node%d/cdc", root, nodeNum),
-                                  token);
+                                  String.format("%s/node%d/cdc", root, nodeNum),
+                                  token,
+                                  7199);
     }
 
     private static String[] datadirs(int datadirCount, File root, int nodeNum)
diff --git a/test/distributed/org/apache/cassandra/distributed/impl/IsolatedExecutor.java b/test/distributed/org/apache/cassandra/distributed/impl/IsolatedExecutor.java
index 53c1ad5..2b4fc87 100644
--- a/test/distributed/org/apache/cassandra/distributed/impl/IsolatedExecutor.java
+++ b/test/distributed/org/apache/cassandra/distributed/impl/IsolatedExecutor.java
@@ -52,7 +52,7 @@
 {
     final ExecutorService isolatedExecutor;
     private final String name;
-    private final ClassLoader classLoader;
+    final ClassLoader classLoader;
     private final Method deserializeOnInstance;
 
     IsolatedExecutor(String name, ClassLoader classLoader)
diff --git a/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmx.java b/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmx.java
new file mode 100644
index 0000000..b3d0659
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmx.java
@@ -0,0 +1,231 @@
+/*
+ * 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.distributed.impl;
+
+import java.lang.reflect.Field;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXConnectorServer;
+import javax.management.remote.JMXServiceURL;
+import javax.management.remote.rmi.RMIConnectorServer;
+import javax.management.remote.rmi.RMIJRMPServerImpl;
+
+import org.slf4j.Logger;
+
+import org.apache.cassandra.distributed.api.IInstanceConfig;
+import org.apache.cassandra.utils.JMXServerUtils;
+import org.apache.cassandra.utils.MBeanWrapper;
+import org.apache.cassandra.utils.RMIClientSocketFactoryImpl;
+import sun.rmi.transport.tcp.TCPEndpoint;
+
+import static org.apache.cassandra.distributed.api.Feature.JMX;
+import static org.apache.cassandra.utils.MBeanWrapper.IS_DISABLED_MBEAN_REGISTRATION;
+
+public class IsolatedJmx
+{
+    /** Controls the JMX server threadpool keap-alive time. */
+    private static final String SUN_RMI_TRANSPORT_TCP_THREADKEEPALIVETIME = "sun.rmi.transport.tcp.threadKeepAliveTime";
+    /** Controls the distributed garbage collector lease time for JMX objects. */
+    private static final String JAVA_RMI_DGC_LEASE_VALUE_IN_JVM_DTEST ="java.rmi.dgc.leaseValue";
+    private static final int RMI_KEEPALIVE_TIME = 1000;
+
+    private JMXConnectorServer jmxConnectorServer;
+    private JMXServerUtils.JmxRegistry registry;
+    private RMIJRMPServerImpl jmxRmiServer;
+    private MBeanWrapper.InstanceMBeanWrapper wrapper;
+    private RMIClientSocketFactoryImpl clientSocketFactory;
+    private CollectingRMIServerSocketFactoryImpl serverSocketFactory;
+    private Logger inInstancelogger;
+    private IInstanceConfig config;
+
+    public IsolatedJmx(Instance instance, Logger inInstancelogger) {
+        this.inInstancelogger = inInstancelogger;
+        config = instance.config();
+    }
+
+    public void startJmx()
+    {
+        try
+        {
+            // Several RMI threads hold references to in-jvm dtest objects, and are, by default, kept
+            // alive for long enough (minutes) to keep classloaders from being collected.
+            // Set these two system properties to a low value to allow cleanup to occur fast enough
+            // for GC to collect our classloaders.
+            System.setProperty(JAVA_RMI_DGC_LEASE_VALUE_IN_JVM_DTEST, String.valueOf(RMI_KEEPALIVE_TIME));
+            System.setProperty(SUN_RMI_TRANSPORT_TCP_THREADKEEPALIVETIME, String.valueOf(RMI_KEEPALIVE_TIME));
+            System.setProperty(IS_DISABLED_MBEAN_REGISTRATION, "false");
+            InetAddress addr = config.broadcastAddress().getAddress();
+
+            int jmxPort = config.jmxPort();
+
+            String hostname = addr.getHostAddress();
+            wrapper = new MBeanWrapper.InstanceMBeanWrapper(hostname + ":" + jmxPort);
+            ((MBeanWrapper.DelegatingMbeanWrapper) MBeanWrapper.instance).setDelegate(wrapper);
+            Map<String, Object> env = new HashMap<>();
+
+            serverSocketFactory = new CollectingRMIServerSocketFactoryImpl(addr);
+            env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,
+                    serverSocketFactory);
+            clientSocketFactory = new RMIClientSocketFactoryImpl(addr);
+            env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE,
+                    clientSocketFactory);
+
+            // configure the RMI registry
+            registry = new JMXServerUtils.JmxRegistry(jmxPort,
+                                                      clientSocketFactory,
+                                                      serverSocketFactory,
+                                                      "jmxrmi");
+
+            // Mark the JMX server as a permanently exported object. This allows the JVM to exit with the
+            // server running and also exempts it from the distributed GC scheduler which otherwise would
+            // potentially attempt a full GC every `sun.rmi.dgc.server.gcInterval` millis (default is 3600000ms)
+            // For more background see:
+            //   - CASSANDRA-2967
+            //   - https://www.jclarity.com/2015/01/27/rmi-system-gc-unplugged/
+            //   - https://bugs.openjdk.java.net/browse/JDK-6760712
+            env.put("jmx.remote.x.daemon", "true");
+
+            // Set the port used to create subsequent connections to exported objects over RMI. This simplifies
+            // configuration in firewalled environments, but it can't be used in conjuction with SSL sockets.
+            // See: CASSANDRA-7087
+            int rmiPort = config.jmxPort();
+
+            // We create the underlying RMIJRMPServerImpl so that we can manually bind it to the registry,
+            // rather then specifying a binding address in the JMXServiceURL and letting it be done automatically
+            // when the server is started. The reason for this is that if the registry is configured with SSL
+            // sockets, the JMXConnectorServer acts as its client during the binding which means it needs to
+            // have a truststore configured which contains the registry's certificate. Manually binding removes
+            // this problem.
+            // See CASSANDRA-12109.
+            jmxRmiServer = new RMIJRMPServerImpl(rmiPort, clientSocketFactory, serverSocketFactory,
+                                                 env);
+            JMXServiceURL serviceURL = new JMXServiceURL("rmi", hostname, rmiPort);
+            jmxConnectorServer = new RMIConnectorServer(serviceURL, env, jmxRmiServer, wrapper.getMBeanServer());
+
+            jmxConnectorServer.start();
+
+            registry.setRemoteServerStub(jmxRmiServer.toStub());
+            JMXServerUtils.logJmxServiceUrl(addr, jmxPort);
+            waitForJmxAvailability(hostname, jmxPort, env);
+        }
+        catch (Throwable e)
+        {
+            throw new RuntimeException("Feature.JMX was enabled but could not be started.", e);
+        }
+    }
+
+
+    private void waitForJmxAvailability(String hostname, int rmiPort, Map<String, Object> env) throws InterruptedException, MalformedURLException
+    {
+        String url = String.format("service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", hostname, rmiPort);
+        JMXServiceURL serviceURL = new JMXServiceURL(url);
+        int attempts = 0;
+        Throwable lastThrown = null;
+        while (attempts < 20)
+        {
+            attempts++;
+            try (JMXConnector ignored = JMXConnectorFactory.connect(serviceURL, env))
+            {
+                inInstancelogger.info("Connected to JMX server at {} after {} attempt(s)",
+                                      url, attempts);
+                return;
+            }
+            catch (MalformedURLException e)
+            {
+                throw new RuntimeException(e);
+            }
+            catch (Throwable thrown)
+            {
+                lastThrown = thrown;
+            }
+            inInstancelogger.info("Could not connect to JMX on {} after {} attempts. Will retry.", url, attempts);
+            Thread.sleep(1000);
+        }
+        throw new RuntimeException("Could not start JMX - unreachable after 20 attempts", lastThrown);
+    }
+
+    public void stopJmx() throws IllegalAccessException, NoSuchFieldException, InterruptedException
+    {
+        if (!config.has(JMX))
+            return;
+        // First, swap the mbean wrapper back to a NoOp wrapper
+        // This prevents later attempts to unregister mbeans from failing in Cassandra code, as we're going to
+        // unregister all of them here
+        ((MBeanWrapper.DelegatingMbeanWrapper) MBeanWrapper.instance).setDelegate(new MBeanWrapper.NoOpMBeanWrapper());
+        try
+        {
+            wrapper.close();
+        }
+        catch (Throwable e)
+        {
+            inInstancelogger.warn("failed to close wrapper.", e);
+        }
+        try
+        {
+            jmxConnectorServer.stop();
+        }
+        catch (Throwable e)
+        {
+            inInstancelogger.warn("failed to close jmxConnectorServer.", e);
+        }
+        try
+        {
+            registry.close();
+        }
+        catch (Throwable e)
+        {
+            inInstancelogger.warn("failed to close registry.", e);
+        }
+        try
+        {
+            serverSocketFactory.close();
+        }
+        catch (Throwable e)
+        {
+            inInstancelogger.warn("failed to close serverSocketFactory.", e);
+        }
+        // The TCPEndpoint class holds references to a class in the in-jvm dtest framework
+        // which transitively has a reference to the InstanceClassLoader, so we need to
+        // make sure to remove the reference to them when the instance is shutting down
+        clearMapField(TCPEndpoint.class, null, "localEndpoints");
+        Thread.sleep(2 * RMI_KEEPALIVE_TIME); // Double the keep-alive time to give Distributed GC some time to clean up
+    }
+
+    private <K, V> void clearMapField(Class<?> clazz, Object instance, String mapName)
+    throws IllegalAccessException, NoSuchFieldException {
+        Field mapField = clazz.getDeclaredField(mapName);
+        mapField.setAccessible(true);
+        Map<K, V> map = (Map<K, V>) mapField.get(instance);
+        // Because multiple instances can be shutting down at once,
+        // synchronize on the map to avoid ConcurrentModificationException
+        synchronized (map)
+        {
+            for (Iterator<Map.Entry<K, V>> it = map.entrySet().iterator(); it.hasNext(); )
+            {
+                it.next();
+                it.remove();
+            }
+        }
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/impl/RowUtil.java b/test/distributed/org/apache/cassandra/distributed/impl/RowUtil.java
index f781a06..713fcc3 100644
--- a/test/distributed/org/apache/cassandra/distributed/impl/RowUtil.java
+++ b/test/distributed/org/apache/cassandra/distributed/impl/RowUtil.java
@@ -52,7 +52,13 @@
         }
         else
         {
-            return QueryResults.empty();
+            if (res != null)
+            {
+                List<String> warnings = res.getWarnings();
+                return new SimpleQueryResult(new String[0], null, warnings == null ? Collections.emptyList() : warnings);
+            }
+            else
+                return QueryResults.empty();
         }
     }
 
diff --git a/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java b/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java
index f3eb327..b1ff9fc 100644
--- a/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java
+++ b/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java
@@ -28,6 +28,7 @@
 import com.google.common.collect.Multimap;
 
 import org.apache.cassandra.batchlog.BatchlogManager;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ColumnFamilyStoreMBean;
 import org.apache.cassandra.db.HintedHandOffManager;
 import org.apache.cassandra.db.Keyspace;
@@ -35,6 +36,7 @@
 import org.apache.cassandra.gms.FailureDetector;
 import org.apache.cassandra.gms.FailureDetectorMBean;
 import org.apache.cassandra.gms.Gossiper;
+import org.apache.cassandra.locator.DynamicEndpointSnitchMBean;
 import org.apache.cassandra.locator.EndpointSnitchInfo;
 import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
 import org.apache.cassandra.metrics.CassandraMetricsRegistry;
@@ -102,75 +104,78 @@
         runtimeProxy = ManagementFactory.getRuntimeMXBean();
     }
 
-    @Override
-    public void close()
+    public void close() throws IOException
     {
         // nothing to close. no-op
     }
 
-    @Override
     // overrides all the methods referenced mbeanServerConn/jmxc in super
     public EndpointSnitchInfoMBean getEndpointSnitchInfoProxy()
     {
         return new EndpointSnitchInfo();
     }
 
-    @Override
+    public DynamicEndpointSnitchMBean getDynamicEndpointSnitchInfoProxy()
+    {
+        return (DynamicEndpointSnitchMBean) DatabaseDescriptor.createEndpointSnitch(true, DatabaseDescriptor.getRawConfig().endpoint_snitch);
+    }
+
     public CacheServiceMBean getCacheServiceMBean()
     {
         return cacheService;
     }
 
-    @Override
     public ColumnFamilyStoreMBean getCfsProxy(String ks, String cf)
     {
         return Keyspace.open(ks).getColumnFamilyStore(cf);
     }
 
-    @Override
     // The below methods are only used by the commands (i.e. Info, TableHistogram, TableStats, etc.) that display informations. Not useful for dtest, so disable it.
     public Object getCacheMetric(String cacheType, String metricName)
     {
         throw new UnsupportedOperationException();
     }
 
-    @Override
     public Iterator<Map.Entry<String, ColumnFamilyStoreMBean>> getColumnFamilyStoreMBeanProxies()
     {
         throw new UnsupportedOperationException();
     }
 
-    @Override
     public Multimap<String, String> getThreadPools()
     {
         throw new UnsupportedOperationException();
     }
 
-    @Override
     public Object getThreadPoolMetric(String pathName, String poolName, String metricName)
     {
         throw new UnsupportedOperationException();
     }
 
-    @Override
     public Object getColumnFamilyMetric(String ks, String cf, String metricName)
     {
         throw new UnsupportedOperationException();
     }
 
-    @Override
     public CassandraMetricsRegistry.JmxTimerMBean getProxyMetric(String scope)
     {
         throw new UnsupportedOperationException();
     }
 
-    @Override
+    public CassandraMetricsRegistry.JmxTimerMBean getMessagingQueueWaitMetrics(String verb)
+    {
+        throw new UnsupportedOperationException();
+    }
+
     public Object getCompactionMetric(String metricName)
     {
         throw new UnsupportedOperationException();
     }
 
-    @Override
+    public Object getClientMetric(String metricName)
+    {
+        throw new UnsupportedOperationException();
+    }
+
     public long getStorageMetric(String metricName)
     {
         throw new UnsupportedOperationException();
diff --git a/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbeFactory.java b/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbeFactory.java
index 1904aa7..e7734da 100644
--- a/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbeFactory.java
+++ b/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbeFactory.java
@@ -21,9 +21,9 @@
 import java.io.IOException;
 
 import org.apache.cassandra.tools.NodeProbe;
-import org.apache.cassandra.tools.INodeProbeFactory;
+import org.apache.cassandra.tools.NodeProbeFactory;
 
-public class InternalNodeProbeFactory implements INodeProbeFactory
+public class InternalNodeProbeFactory extends NodeProbeFactory
 {
     private final boolean withNotifications;
 
diff --git a/test/distributed/org/apache/cassandra/distributed/test/AlterTest.java b/test/distributed/org/apache/cassandra/distributed/test/AlterTest.java
index 685adb9..e96122d 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/AlterTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/AlterTest.java
@@ -22,9 +22,23 @@
 import org.junit.Test;
 
 import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.api.ICluster;
+import org.apache.cassandra.distributed.api.IInstanceConfig;
 import org.apache.cassandra.distributed.api.IInvokableInstance;
 import org.apache.cassandra.distributed.api.IIsolatedExecutor;
+import org.apache.cassandra.distributed.api.SimpleQueryResult;
+import org.apache.cassandra.service.StorageService;
+
+import static org.apache.cassandra.distributed.action.GossipHelper.withProperty;
+import static org.apache.cassandra.distributed.api.ConsistencyLevel.ONE;
+import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
+import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
+import static org.apache.cassandra.distributed.api.Feature.NETWORK;
+import static org.apache.cassandra.distributed.api.TokenSupplier.evenlyDistributedTokens;
+import static org.apache.cassandra.distributed.shared.NetworkTopology.singleDcNetworkTopology;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertFalse;
 
 public class AlterTest extends TestBaseImpl
 {
@@ -57,4 +71,40 @@
             });
         }
     }
+
+    @Test
+    public void alteringKeyspaceOnInsufficientNumberOfReplicasFiltersOutGossppingOnlyMembersTest() throws Throwable
+    {
+        int originalNodeCount = 1;
+        int expandedNodeCount = originalNodeCount + 1;
+
+        try (Cluster cluster = builder().withNodes(originalNodeCount)
+                                        .withTokenSupplier(evenlyDistributedTokens(expandedNodeCount, 1))
+                                        .withNodeIdTopology(singleDcNetworkTopology(expandedNodeCount, "dc0", "rack0"))
+                                        .withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL))
+                                        .start())
+        {
+            IInstanceConfig config = cluster.newInstanceConfig();
+            IInvokableInstance gossippingOnlyMember = cluster.bootstrap(config);
+            withProperty("cassandra.join_ring", Boolean.toString(false), () -> gossippingOnlyMember.startup(cluster));
+
+            int attempts = 0;
+            // it takes some time the underlying structure is populated otherwise the test is flaky
+            while (((IInvokableInstance) (cluster.get(2))).callOnInstance(() -> StorageService.instance.getTokenMetadata().getAllMembers().isEmpty()))
+            {
+                if (attempts++ > 30)
+                    throw new RuntimeException("timeouted on waiting for a member");
+                Thread.sleep(1000);
+            }
+
+            for (String operation : new String[] { "CREATE", "ALTER" })
+            {
+                SimpleQueryResult result = cluster.coordinator(2)
+                                                  .executeWithResult(operation + " KEYSPACE abc WITH replication = {'class' : 'NetworkTopologyStrategy', 'dc0' : 2 }",
+                                                                     ONE);
+                assertFalse(result.warnings().isEmpty());
+                assertThat(result.warnings().get(0)).contains("Your replication factor 2 for keyspace abc is higher than the number of nodes 1 for datacenter dc0");
+            }
+        }
+    }
 }
diff --git a/test/distributed/org/apache/cassandra/distributed/test/ByteBuddyExamplesTest.java b/test/distributed/org/apache/cassandra/distributed/test/ByteBuddyExamplesTest.java
index 1934a2e..aea3609 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/ByteBuddyExamplesTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/ByteBuddyExamplesTest.java
@@ -107,13 +107,13 @@
             if (nodeNumber != 1)
                 return;
             new ByteBuddy().rebase(SelectStatement.class)
-                           .method(named("execute").and(takesArguments(2)))
+                           .method(named("execute").and(takesArguments(3)))
                            .intercept(MethodDelegation.to(BBCountHelper.class))
                            .make()
                            .load(cl, ClassLoadingStrategy.Default.INJECTION);
         }
 
-        public static ResultMessage.Rows execute(QueryState state, QueryOptions options, @SuperCall Callable<ResultMessage.Rows> r) throws Exception
+        public static ResultMessage.Rows execute(QueryState state, QueryOptions options, long queryStartNanoTime, @SuperCall Callable<ResultMessage.Rows> r) throws Exception
         {
             count.incrementAndGet();
             return r.call();
diff --git a/test/distributed/org/apache/cassandra/distributed/test/BytemanExamplesTest.java b/test/distributed/org/apache/cassandra/distributed/test/BytemanExamplesTest.java
new file mode 100644
index 0000000..6d542b8
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/test/BytemanExamplesTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.distributed.test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import com.google.common.io.Files;
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.shared.Byteman;
+
+public class BytemanExamplesTest
+{
+    @Test
+    public void rewriteFromText() throws IOException
+    {
+        Byteman byteman = Byteman.createFromText("RULE example\n" +
+                                                 "CLASS org.apache.cassandra.utils.FBUtilities\n" +
+                                                 "METHOD setBroadcastInetAddress\n" +
+                                                 "IF true\n" +
+                                                 "DO\n" +
+                                                 "   throw new java.lang.RuntimeException(\"Will not allow init!\")\n" +
+                                                 "ENDRULE\n");
+
+        try
+        {
+            Cluster.build(1)
+                   .withInstanceInitializer((cl, ignore) -> byteman.install(cl))
+                   .createWithoutStarting();
+            Assert.fail("Instance init was rewritten to fail right away, so make sure the rewrite happens");
+        }
+        catch (RuntimeException e)
+        {
+            Assert.assertEquals("Will not allow init!", e.getMessage());
+        }
+    }
+
+    @Test
+    public void rewriteFromScript() throws IOException
+    {
+        // scripts are normally located at test/resources/byteman, but generated in the test
+        // for documentation purposes only
+        File script = File.createTempFile("byteman", ".btm");
+        script.deleteOnExit();
+        Files.asCharSink(script, StandardCharsets.UTF_8).write("RULE example\n" +
+                                                               "CLASS org.apache.cassandra.utils.FBUtilities\n" +
+                                                               "METHOD setBroadcastInetAddress\n" +
+                                                               "IF true\n" +
+                                                               "DO\n" +
+                                                               "   throw new java.lang.RuntimeException(\"Will not allow init!\")\n" +
+                                                               "ENDRULE\n");
+
+        Byteman byteman = Byteman.createFromScripts(script.getAbsolutePath());
+
+        try
+        {
+            Cluster.build(1)
+                   .withInstanceInitializer((cl, ignore) -> byteman.install(cl))
+                   .createWithoutStarting();
+            Assert.fail("Instance init was rewritten to fail right away, so make sure the rewrite happens");
+        }
+        catch (RuntimeException e)
+        {
+            Assert.assertEquals("Will not allow init!", e.getMessage());
+        }
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/test/CASTest.java b/test/distributed/org/apache/cassandra/distributed/test/CASTest.java
index e6c5c10..2da661d 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/CASTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/CASTest.java
@@ -196,7 +196,12 @@
                                                   BiConsumer<String, ICoordinator> postTimeoutOperation2,
                                                   boolean loseCommitOfOperation1) throws IOException
     {
-        try (Cluster cluster = init(Cluster.create(3, config -> config.set("write_request_timeout_in_ms", REQUEST_TIMEOUT)
+        // It's unclear why (haven't dug), but in some of the instance of this test method, there is a consistent 2+
+        // seconds pauses between the prepare and propose phases during the execution of 'postTimeoutOperation2'. This
+        // does not happen on 3.0 and there is no report of such long pauses otherwise, so an hypothesis is that this
+        // is due to the in-jvm dtest framework. This is is why we use a 4 seconds timeout here. Given this test is
+        // not about performance, this is probably ok, even if we ideally should dug into the underlying reason.
+        try (Cluster cluster = init(Cluster.create(3, config -> config.set("write_request_timeout_in_ms", 4000L)
                                                                       .set("cas_contention_timeout_in_ms", CONTENTION_TIMEOUT))))
         {
             String table = KEYSPACE + ".t";
diff --git a/test/distributed/org/apache/cassandra/distributed/test/ClientNetworkStopStartTest.java b/test/distributed/org/apache/cassandra/distributed/test/ClientNetworkStopStartTest.java
index da0731e..0aabc8c 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/ClientNetworkStopStartTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/ClientNetworkStopStartTest.java
@@ -51,6 +51,7 @@
     @Test
     public void stopStartThrift() throws IOException, TException
     {
+        // GOSSIP is needed in order to initServer correctly.
         try (Cluster cluster = init(Cluster.build(1).withConfig(c -> c.with(Feature.NATIVE_PROTOCOL)).start()))
         {
             IInvokableInstance node = cluster.get(1);
diff --git a/test/distributed/org/apache/cassandra/distributed/test/FrozenUDTTest.java b/test/distributed/org/apache/cassandra/distributed/test/FrozenUDTTest.java
new file mode 100644
index 0000000..2a45b86
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/test/FrozenUDTTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.distributed.test;
+
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.service.StorageService;
+
+import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
+import static org.apache.cassandra.distributed.shared.AssertUtils.row;
+
+public class FrozenUDTTest extends TestBaseImpl
+{
+    @Test
+    public void testAddAndUDTField() throws IOException
+    {
+        try (Cluster cluster = init(Cluster.build(1).start()))
+        {
+            cluster.schemaChange("create type " + KEYSPACE + ".a (foo text)");
+            cluster.schemaChange("create table " + KEYSPACE + ".x (id int, ck frozen<a>, i int, primary key (id, ck))");
+            for (int i = 0; i < 10; i++)
+                cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (?, " + json(i) + ", ? )", ConsistencyLevel.ALL, i, i);
+
+            for (int i = 0; i < 10; i++)
+                assertRows(cluster.coordinator(1).execute("select i from " + KEYSPACE + ".x WHERE id = ? and ck = " + json(i), ConsistencyLevel.ALL, i),
+                           row(i));
+
+            cluster.schemaChange("alter type " + KEYSPACE + ".a add bar text");
+
+            for (int i = 5; i < 15; i++)
+                cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (?, " + json(i) + ", ? )", ConsistencyLevel.ALL, i, i);
+            cluster.forEach(i -> i.flush(KEYSPACE));
+
+            for (int i = 5; i < 15; i++)
+                assertRows(cluster.coordinator(1).execute("select i from " + KEYSPACE + ".x WHERE id = ? and ck = " + json(i), ConsistencyLevel.ALL, i),
+                           row(i));
+        }
+    }
+
+    @Test
+    public void testEmptyValue() throws IOException
+    {
+        try (Cluster cluster = init(Cluster.build(1).start()))
+        {
+            cluster.schemaChange("create type " + KEYSPACE + ".a (foo text)");
+            cluster.schemaChange("create table " + KEYSPACE + ".x (id int, ck frozen<a>, i int, primary key (id, ck))");
+            cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (1, system.fromjson('{\"foo\":\"\"}'), 1)", ConsistencyLevel.ALL);
+            cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (1, system.fromjson('{\"foo\":\"a\"}'), 2)", ConsistencyLevel.ALL);
+            cluster.forEach(i -> i.flush(KEYSPACE));
+
+            Runnable check = () -> {
+                assertRows(cluster.coordinator(1).execute("select i from " + KEYSPACE + ".x WHERE id = 1 and ck = system.fromjson('{\"foo\":\"\"}')", ConsistencyLevel.ALL),
+                           row(1));
+                assertRows(cluster.coordinator(1).execute("select i from " + KEYSPACE + ".x WHERE id = 1 and ck = system.fromjson('{\"foo\":\"a\"}')", ConsistencyLevel.ALL),
+                           row(2));
+            };
+
+            check.run();
+            cluster.schemaChange("alter type " + KEYSPACE + ".a add bar text");
+            check.run();
+
+            assertRows(cluster.coordinator(1).execute("select i from " + KEYSPACE + ".x WHERE id = 1 and ck = system.fromjson('{\"foo\":\"\",\"bar\":\"\"}')", ConsistencyLevel.ALL));
+            cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (1, system.fromjson('{\"foo\":\"\",\"bar\":\"\"}'), 3)", ConsistencyLevel.ALL);
+            check.run();
+            assertRows(cluster.coordinator(1).execute("select i from " + KEYSPACE + ".x WHERE id = 1 and ck = system.fromjson('{\"foo\":\"\",\"bar\":\"\"}')", ConsistencyLevel.ALL),
+                       row(3));
+        }
+    }
+
+    @Test
+    public void testUpgradeSStables() throws IOException
+    {
+        try (Cluster cluster = init(Cluster.build(1).start()))
+        {
+            cluster.schemaChange("create type " + KEYSPACE + ".a (foo text)");
+            cluster.schemaChange("create table " + KEYSPACE + ".x (id int, ck frozen<a>, i int, primary key (id, ck))");
+            cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (?, " + json(1) + ", ? )", ConsistencyLevel.ALL, 1, 1);
+            assertRows(cluster.coordinator(1).execute("select i from " + KEYSPACE + ".x WHERE id = ? and ck = " + json(1), ConsistencyLevel.ALL, 1), row(1));
+            cluster.forEach(i -> i.flush(KEYSPACE));
+            assertRows(cluster.coordinator(1).execute("select i from " + KEYSPACE + ".x WHERE id = ? and ck = " + json(1), ConsistencyLevel.ALL, 1), row(1));
+
+            cluster.schemaChange("alter type " + KEYSPACE + ".a add bar text");
+            cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (?, " + json(2) + ", ? )", ConsistencyLevel.ALL, 2, 2);
+            cluster.forEach(i -> i.flush(KEYSPACE));
+            assertRows(cluster.coordinator(1).execute("select i from " + KEYSPACE + ".x WHERE id = ? and ck = " + json(2), ConsistencyLevel.ALL, 2), row(2));
+
+            cluster.forEach(i -> i.runOnInstance(() -> {
+                try
+                {
+                    StorageService.instance.upgradeSSTables(KEYSPACE, false, "x");
+                }
+                catch (IOException | ExecutionException | InterruptedException e)
+                {
+                    throw new RuntimeException(e);
+                }
+            }));
+
+            cluster.forEach(i -> i.forceCompact(KEYSPACE, "x"));
+
+            for (int i = 1; i < 3; i++)
+                assertRows(cluster.coordinator(1).execute("select i from " + KEYSPACE + ".x WHERE id = ? and ck = " + json(i), ConsistencyLevel.ALL, i),
+                           row(i));
+        }
+    }
+
+    @Test
+    public void testDivergentSchemas() throws Throwable
+    {
+        try (Cluster cluster = init(Cluster.create(2)))
+        {
+            cluster.schemaChange("create type " + KEYSPACE + ".a (foo text)");
+            cluster.schemaChange("create table " + KEYSPACE + ".x (id int, ck frozen<a>, i int, primary key (id, ck))");
+
+            cluster.get(1).executeInternal("alter type " + KEYSPACE + ".a add bar text");
+            cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (?, " + json(1, 1) + ", ? )", ConsistencyLevel.ALL,
+                                           1, 1);
+            cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (?, " + json(1, 2) + ", ? )", ConsistencyLevel.ALL,
+                                           2, 2);
+            cluster.get(2).flush(KEYSPACE);
+        }
+    }
+
+    private String json(int i)
+    {
+        return String.format("system.fromjson('{\"foo\":\"%d\"}')", i);
+    }
+
+    private String json(int i, int j)
+    {
+        return String.format("system.fromjson('{\"foo\":\"%d\", \"bar\":\"%d\"}')", i, j);
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/distributed/test/GroupByTest.java b/test/distributed/org/apache/cassandra/distributed/test/GroupByTest.java
new file mode 100644
index 0000000..26e3996
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/test/GroupByTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.distributed.test;
+
+import java.util.Iterator;
+
+import com.google.common.collect.Iterators;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.SimpleStatement;
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.distributed.api.Feature;
+import org.apache.cassandra.distributed.api.ICoordinator;
+
+import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
+import static org.apache.cassandra.distributed.api.Feature.NETWORK;
+import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
+import static org.apache.cassandra.distributed.shared.AssertUtils.row;
+
+public class GroupByTest extends TestBaseImpl
+{
+    @Test
+    public void groupByWithDeletesAndSrpOnPartitions() throws Throwable
+    {
+        try (Cluster cluster = init(builder().withNodes(2).withConfig((cfg) -> cfg.set("enable_user_defined_functions", "true")).start()))
+        {
+            cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck text, PRIMARY KEY (pk, ck))"));
+            initFunctions(cluster);
+            cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (1, '1') USING TIMESTAMP 0"));
+            cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (2, '2') USING TIMESTAMP 0"));
+            cluster.get(1).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck='0'"));
+
+            cluster.get(2).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, '0') USING TIMESTAMP 0"));
+            cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=1 AND ck='1'"));
+            cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=2 AND ck='2'"));
+
+            for (String limitClause : new String[]{ "", "LIMIT 1", "LIMIT 10", "PER PARTITION LIMIT 1", "PER PARTITION LIMIT 10" })
+            {
+                String query = withKeyspace("SELECT concat(ck) FROM %s.tbl GROUP BY pk " + limitClause);
+                for (int i = 1; i <= 4; i++)
+                {
+                    Iterator<Object[]> rows = cluster.coordinator(2).executeWithPaging(query, ConsistencyLevel.ALL, i);
+                    assertRows(Iterators.toArray(rows, Object[].class));
+                }
+            }
+        }
+    }
+
+    @Test
+    public void groupByWithDeletesAndSrpOnRows() throws Throwable
+    {
+        try (Cluster cluster = init(builder().withNodes(2).withConfig((cfg) -> cfg.set("enable_user_defined_functions", "true")).start()))
+        {
+            cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck text, PRIMARY KEY (pk, ck))"));
+            initFunctions(cluster);
+            cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, '1') USING TIMESTAMP 0"));
+            cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, '2') USING TIMESTAMP 0"));
+            cluster.get(1).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck='0'"));
+
+            cluster.get(2).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, '0') USING TIMESTAMP 0"));
+            cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck='1'"));
+            cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck='2'"));
+
+            for (String limitClause : new String[]{ "", "LIMIT 1", "LIMIT 10", "PER PARTITION LIMIT 1", "PER PARTITION LIMIT 10" })
+            {
+                String query = withKeyspace("SELECT concat(ck) FROM %s.tbl GROUP BY pk " + limitClause);
+                for (int i = 1; i <= 4; i++)
+                {
+                    Iterator<Object[]> rows = cluster.coordinator(2).executeWithPaging(query, ConsistencyLevel.ALL, i);
+                    assertRows(Iterators.toArray(rows, Object[].class));
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testGroupByWithAggregatesAndPaging() throws Throwable
+    {
+        try (Cluster cluster = init(builder().withNodes(2).withConfig((cfg) -> cfg.set("enable_user_defined_functions", "true")).start()))
+        {
+            cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck int, v1 text, v2 text, v3 text, primary key (pk, ck))"));
+            initFunctions(cluster);
+
+            cluster.coordinator(1).execute(withKeyspace("insert into %s.tbl (pk, ck, v1, v2, v3) values (1,1,'1','1','1')"), ConsistencyLevel.ALL);
+            cluster.coordinator(1).execute(withKeyspace("insert into %s.tbl (pk, ck, v1, v2, v3) values (1,2,'2','2','2')"), ConsistencyLevel.ALL);
+            cluster.coordinator(1).execute(withKeyspace("insert into %s.tbl (pk, ck, v1, v2, v3) values (1,3,'3','3','3')"), ConsistencyLevel.ALL);
+
+            for (int i = 1; i <= 4; i++)
+            {
+                assertRows(cluster.coordinator(1).executeWithPaging(withKeyspace("select concat(v1), concat(v2), concat(v3) from %s.tbl where pk = 1 group by pk"),
+                                                                    ConsistencyLevel.ALL, i),
+                           row("_ 1 2 3", "_ 1 2 3", "_ 1 2 3"));
+
+                assertRows(cluster.coordinator(1).executeWithPaging(withKeyspace("select concat(v1), concat(v2), concat(v3) from %s.tbl where pk = 1 group by pk limit 1"),
+                                                                    ConsistencyLevel.ALL, i),
+                           row("_ 1 2 3", "_ 1 2 3", "_ 1 2 3"));
+
+                assertRows(cluster.coordinator(1).executeWithPaging(withKeyspace("select * from %s.tbl where pk = 1 group by pk"),
+                                                                    ConsistencyLevel.ALL, i),
+                           row(1, 1, "1", "1", "1"));
+            }
+        }
+    }
+
+    @Test
+    public void testGroupWithDeletesAndPaging() throws Throwable
+    {
+        try (Cluster cluster = init(builder().withNodes(2).withConfig(cfg -> cfg.with(Feature.GOSSIP, NETWORK, NATIVE_PROTOCOL)).start()))
+        {
+            cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck int, PRIMARY KEY (pk, ck))"));
+            ICoordinator coordinator = cluster.coordinator(1);
+            coordinator.execute(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, 0)"), ConsistencyLevel.ALL);
+            coordinator.execute(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (1, 1)"), ConsistencyLevel.ALL);
+
+            cluster.get(1).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck=0"));
+            cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=1 AND ck=1"));
+            String query = withKeyspace("SELECT * FROM %s.tbl GROUP BY pk");
+            Iterator<Object[]> rows = coordinator.executeWithPaging(query, ConsistencyLevel.ALL, 1);
+            assertRows(Iterators.toArray(rows, Object[].class));
+
+            try (com.datastax.driver.core.Cluster c = com.datastax.driver.core.Cluster.builder().addContactPoint("127.0.0.1").build();
+                 Session session = c.connect())
+            {
+                SimpleStatement stmt = new SimpleStatement(withKeyspace("select * from %s.tbl where pk = 1 group by pk"));
+                stmt.setFetchSize(1);
+                Iterator<Row> rs = session.execute(stmt).iterator();
+                Assert.assertFalse(rs.hasNext());
+            }
+        }
+    }
+
+    private static void initFunctions(Cluster cluster)
+    {
+        cluster.schemaChange(withKeyspace("CREATE FUNCTION %s.concat_strings_fn(a text, b text) " +
+                             "RETURNS NULL ON NULL INPUT " +
+                             "RETURNS text " +
+                             "LANGUAGE java " +
+                             "AS 'return a + \" \" + b;'"));
+
+        cluster.schemaChange(withKeyspace("CREATE AGGREGATE %s.concat(text)" +
+                             " SFUNC concat_strings_fn" +
+                             " STYPE text" +
+                             " INITCOND '_'"));
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/distributed/test/JVMDTestTest.java b/test/distributed/org/apache/cassandra/distributed/test/JVMDTestTest.java
index 13b314a..34b04f7 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/JVMDTestTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/JVMDTestTest.java
@@ -24,9 +24,7 @@
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import java.util.logging.Logger;
 
-import org.junit.Assert;
 import com.google.common.collect.ImmutableMap;
 import org.junit.Test;
 
diff --git a/test/distributed/org/apache/cassandra/distributed/test/JVMStabilityInspectorThrowableTest.java b/test/distributed/org/apache/cassandra/distributed/test/JVMStabilityInspectorThrowableTest.java
index d7aecca..efe05ca 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/JVMStabilityInspectorThrowableTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/JVMStabilityInspectorThrowableTest.java
@@ -34,8 +34,9 @@
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.RowIndexEntry;
+import org.apache.cassandra.db.Slices;
 import org.apache.cassandra.db.filter.ColumnFilter;
-import org.apache.cassandra.db.rows.SliceableUnfilteredRowIterator;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
 import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.api.ConsistencyLevel;
 import org.apache.cassandra.distributed.api.IInvokableInstance;
@@ -218,16 +219,14 @@
             this.shouldThrowCorrupted = shouldThrowCorrupted;
         }
 
-        @Override
-        public SliceableUnfilteredRowIterator iterator(DecoratedKey key, ColumnFilter selectedColumns, boolean reversed, boolean isForThrift, SSTableReadsListener listener)
+        public UnfilteredRowIterator iterator(DecoratedKey key, Slices slices, ColumnFilter selectedColumns, boolean reversed, boolean isForThrift, SSTableReadsListener listener)
         {
             if (shouldThrowCorrupted)
                 throw throwCorrupted();
             throw throwFSError();
         }
 
-        @Override
-        public SliceableUnfilteredRowIterator iterator(FileDataInput file, DecoratedKey key, RowIndexEntry indexEntry, ColumnFilter selectedColumns, boolean reversed, boolean isForThrift)
+        public UnfilteredRowIterator iterator(FileDataInput file, DecoratedKey key, RowIndexEntry indexEntry, Slices slices, ColumnFilter selectedColumns, boolean reversed, boolean isForThrift)
         {
             if (shouldThrowCorrupted)
                 throw throwCorrupted();
diff --git a/test/distributed/org/apache/cassandra/distributed/test/MessageFiltersTest.java b/test/distributed/org/apache/cassandra/distributed/test/MessageFiltersTest.java
index 3780fe1..be3622d 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/MessageFiltersTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/MessageFiltersTest.java
@@ -20,12 +20,12 @@
 
 import java.net.InetSocketAddress;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import com.google.common.collect.Sets;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -67,6 +67,7 @@
         int VERB1 = MessagingService.Verb.READ.ordinal();
         int VERB2 = MessagingService.Verb.REQUEST_RESPONSE.ordinal();
         int VERB3 = MessagingService.Verb.READ_REPAIR.ordinal();
+
         int i1 = 1;
         int i2 = 2;
         int i3 = 3;
@@ -74,7 +75,7 @@
         String MSG2 = "msg2";
 
         MessageFilters filters = new MessageFilters();
-        Permit permit = inbound ? (from, to, msg) -> filters.permitInbound(from, to, msg) : (from, to, msg) -> filters.permitOutbound(from, to, msg);
+        Permit permit = inbound ? filters::permitInbound : filters::permitOutbound;
 
         IMessageFilters.Filter filter = filters.allVerbs().inbound(inbound).from(1).drop();
         Assert.assertFalse(permit.test(i1, i2, msg(VERB1, MSG1)));
@@ -140,7 +141,6 @@
             public InetSocketAddress from() { return null; }
         };
     }
-
     @Test
     public void testFilters() throws Throwable
     {
@@ -184,8 +184,8 @@
 
             AtomicInteger counter = new AtomicInteger();
 
-            Set<Integer> verbs = Sets.newHashSet(Arrays.asList(MessagingService.Verb.RANGE_SLICE.ordinal(),
-                                                               MessagingService.Verb.MUTATION.ordinal()));
+            Set<Integer> verbs = new HashSet<>(Arrays.asList(MessagingService.Verb.RANGE_SLICE.ordinal(),
+                                                             MessagingService.Verb.MUTATION.ordinal()));
 
             // Reads and writes are going to time out in both directions
             IMessageFilters.Filter filter = cluster.filters()
@@ -195,7 +195,7 @@
                                                    .messagesMatching((from, to, msg) -> {
                                                        // Decode and verify message on instance; return the result back here
                                                        Integer id = cluster.get(1).callsOnInstance((IIsolatedExecutor.SerializableCallable<Integer>) () -> {
-                                                           MessageIn decoded = Instance.deserializeMessage(msg).left;
+                                                           MessageIn decoded = Instance.deserializeMessage(msg);
                                                            if (decoded != null)
                                                                return (Integer) decoded.verb.ordinal();
                                                            return -1;
@@ -233,7 +233,7 @@
             CountDownLatch latch = new CountDownLatch(1);
             cluster.filters().verbs(MessagingService.Verb.HINT.ordinal()).messagesMatching((a,b,msg) -> {
                 cluster.get(1).acceptsOnInstance((IIsolatedExecutor.SerializableConsumer<IMessage>) (m) -> {
-                    HintMessage hintMessage = (HintMessage) Instance.deserializeMessage(m).left.payload;
+                    HintMessage hintMessage = (HintMessage) Instance.deserializeMessage(m).payload;
                     assert hintMessage != null;
                 }).accept(msg);
 
diff --git a/test/distributed/org/apache/cassandra/distributed/test/MessageForwardingTest.java b/test/distributed/org/apache/cassandra/distributed/test/MessageForwardingTest.java
index cc54711..8957723 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/MessageForwardingTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/MessageForwardingTest.java
@@ -42,8 +42,6 @@
 import org.apache.cassandra.distributed.impl.TracingUtil;
 import org.apache.cassandra.utils.UUIDGen;
 
-import static org.apache.cassandra.concurrent.StageManager.NO_OP_TASK;
-
 public class MessageForwardingTest extends TestBaseImpl
 {
     @Test
@@ -77,7 +75,7 @@
             // all tracing updates have completed. The tracing executor serializes work, so run a task through
             // and everthing submitted before must have completed.
             cluster.forEach(instance -> instance.runOnInstance(() -> {
-                Future<?> result = StageManager.getStage(Stage.TRACING).submit(NO_OP_TASK);
+                Future<?> result = StageManager.getStage(Stage.TRACING).submit(() -> null);
                 try
                 {
                     result.get(30, TimeUnit.SECONDS);
diff --git a/test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java b/test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java
index d8b9ce7..aaa7dc3 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java
@@ -50,4 +50,14 @@
             assertEquals("Non-empty error output", "", ringResult.getStderr());
         }
     }
+
+    @Test
+    public void testSetCacheCapacityWhenDisabled() throws Throwable
+    {
+        try (ICluster cluster = init(builder().withNodes(1).withConfig(c->c.set("row_cache_size_in_mb", "0")).start()))
+        {
+            NodeToolResult ringResult = cluster.get(1).nodetoolResult("setcachecapacity", "1", "1", "1");
+            ringResult.asserts().stderrContains("is not permitted as this cache is disabled");
+        }
+    }
 }
diff --git a/test/distributed/org/apache/cassandra/distributed/test/ReprepareFuzzTest.java b/test/distributed/org/apache/cassandra/distributed/test/ReprepareFuzzTest.java
index 7087bcd..520e8cf 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/ReprepareFuzzTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/ReprepareFuzzTest.java
@@ -44,15 +44,18 @@
 import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
 import net.bytebuddy.implementation.MethodDelegation;
 import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.distributed.api.ConsistencyLevel;
 import org.apache.cassandra.distributed.api.ICluster;
 import org.apache.cassandra.distributed.api.IInvokableInstance;
 import org.apache.cassandra.distributed.impl.RowUtil;
+import org.apache.cassandra.utils.CassandraVersion;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.Pair;
 import org.apache.cassandra.utils.Throwables;
 
 import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
 import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
 import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
 import static org.apache.cassandra.distributed.api.Feature.NETWORK;
@@ -224,9 +227,17 @@
                                     break;
                                 case CLEAR_CACHES:
                                     c.get(1).runOnInstance(() -> {
-                                        QueryProcessor.clearPreparedStatementsCache();
+                                        SystemKeyspace.loadPreparedStatements((id, query, keyspace) -> {
+                                            if (rng.nextBoolean())
+                                                QueryProcessor.instance.evictPrepared(id);
+                                            return true;
+                                        });
                                     });
                                     break;
+                                case RELOAD_FROM_TABLES:
+                                    c.get(1).runOnInstance(QueryProcessor::clearPreparedStatementsCache);
+                                    c.get(1).runOnInstance(() -> QueryProcessor.instance.preloadPreparedStatements());
+                                    break;
                                 case SWITCH_KEYSPACE:
                                     usedKsIdx = ks;
                                     usedKs = "ks" + ks;
@@ -302,6 +313,7 @@
         PREPARE_UNQUALIFIED,
         CLEAR_CACHES,
         FORGET_PREPARED,
+        RELOAD_FROM_TABLES,
         SWITCH_KEYSPACE,
         RECONNECT
     }
@@ -314,6 +326,7 @@
 
     private static Action[] infrequent = new Action[]{ Action.CLEAR_CACHES,
                                                        Action.FORGET_PREPARED,
+                                                       Action.RELOAD_FROM_TABLES,
                                                        Action.RECONNECT
     };
 
@@ -323,8 +336,11 @@
         {
             DynamicType.Builder.MethodDefinition.ReceiverTypeDefinition<QueryProcessor> klass =
             new ByteBuddy().rebase(QueryProcessor.class)
-                           .method(named("useNewPreparedStatementBehaviour"))
+                           .method(named("skipKeyspaceForQualifiedStatements"))
+                           .intercept(MethodDelegation.to(AlwaysNewBehaviour.class))
+                           .method(named("useKeyspaceForNonQualifiedStatements"))
                            .intercept(MethodDelegation.to(AlwaysNewBehaviour.class));
+
             klass.make()
                  .load(cl, ClassLoadingStrategy.Default.INJECTION);
         }
@@ -332,7 +348,12 @@
 
     public static class AlwaysNewBehaviour
     {
-        public static boolean useNewPreparedStatementBehaviour()
+        public static boolean skipKeyspaceForQualifiedStatements()
+        {
+            return true;
+        }
+
+        public static boolean useKeyspaceForNonQualifiedStatements()
         {
             return true;
         }
diff --git a/test/distributed/org/apache/cassandra/distributed/test/ReprepareOldBehaviourTest.java b/test/distributed/org/apache/cassandra/distributed/test/ReprepareOldBehaviourTest.java
index 2139485..ff23e79 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/ReprepareOldBehaviourTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/ReprepareOldBehaviourTest.java
@@ -77,7 +77,7 @@
         }
     }
 
-    @Test
+    @Test // see CASSANDRA-18021
     public void testReprepareMixedVersionWithoutReset() throws Throwable
     {
         try (ICluster<IInvokableInstance> c = init(builder().withNodes(2)
diff --git a/test/distributed/org/apache/cassandra/distributed/test/ResourceLeakTest.java b/test/distributed/org/apache/cassandra/distributed/test/ResourceLeakTest.java
index 808b95c..177cbb9 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/ResourceLeakTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/ResourceLeakTest.java
@@ -28,7 +28,10 @@
 import java.time.Instant;
 import java.util.function.Consumer;
 import javax.management.MBeanServer;
+import javax.management.MBeanServerConnection;
+import javax.management.remote.JMXConnector;
 
+import org.junit.Assert;
 import org.junit.Ignore;
 import org.junit.Test;
 
@@ -36,14 +39,19 @@
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.distributed.api.Feature;
 import org.apache.cassandra.distributed.api.IInstanceConfig;
-import org.apache.cassandra.service.CassandraDaemon;
+import org.apache.cassandra.distributed.api.IInvokableInstance;
+import org.apache.cassandra.distributed.shared.JMXUtil;
+import org.apache.cassandra.gms.Gossiper;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.SigarLibrary;
 
 import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
+import static org.apache.cassandra.distributed.api.Feature.JMX;
 import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
 import static org.apache.cassandra.distributed.api.Feature.NETWORK;
+import static org.hamcrest.Matchers.startsWith;
 
 /* Resource Leak Test - useful when tracking down issues with in-JVM framework cleanup.
  * All objects referencing the InstanceClassLoader need to be garbage collected or
@@ -142,19 +150,25 @@
 
     void doTest(int numClusterNodes, Consumer<IInstanceConfig> updater) throws Throwable
     {
+        doTest(numClusterNodes, updater, ignored -> {});
+    }
+
+    void doTest(int numClusterNodes, Consumer<IInstanceConfig> updater, Consumer<Cluster> actionToPerform) throws Throwable
+    {
         for (int loop = 0; loop < numTestLoops; loop++)
         {
             System.out.println(String.format("========== Starting loop %03d ========", loop));
             try (Cluster cluster = Cluster.build(numClusterNodes).withConfig(updater).start())
             {
                 if (cluster.get(1).config().has(GOSSIP)) // Wait for gossip to settle on the seed node
-                    cluster.get(1).runOnInstance(CassandraDaemon::waitForGossipToSettle);
+                    cluster.get(1).runOnInstance(() -> Gossiper.waitToSettle());
 
                 init(cluster);
                 String tableName = "tbl" + loop;
                 cluster.schemaChange("CREATE TABLE " + KEYSPACE + "." + tableName + " (pk int, ck int, v int, PRIMARY KEY (pk, ck))");
                 cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + "." + tableName + "(pk,ck,v) VALUES (0,0,0)", ConsistencyLevel.ALL);
                 cluster.get(1).callOnInstance(() -> FBUtilities.waitOnFutures(Keyspace.open(KEYSPACE).flush()));
+                actionToPerform.accept(cluster);
                 if (dumpEveryLoop)
                 {
                     dumpResources(String.format("loop%03d", loop));
@@ -213,4 +227,50 @@
         }
         dumpResources("final-native");
     }
+
+    @Test
+    public void looperJmxTest() throws Throwable
+    {
+        doTest(1, config -> config.with(JMX), cluster -> {
+            // NOTE: At some point, the hostname of the broadcastAddress can be resolved
+            // and then the `getHostString`, which would otherwise return the IP address,
+            // starts returning `localhost` - use `.getAddress().getHostAddress()` to work around this.
+            for (IInvokableInstance instance:cluster.get(1, cluster.size()))
+            {
+                IInstanceConfig config = instance.config();
+                try (JMXConnector jmxc = JMXUtil.getJmxConnector(config))
+                {
+                    MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
+                    // instances get their default domain set to their IP address, so us it
+                    // to check that we are actually connecting to the correct instance
+                    String defaultDomain = mbsc.getDefaultDomain();
+                    Assert.assertThat(defaultDomain, startsWith(JMXUtil.getJmxHost(config) + ":" + config.jmxPort()));
+                }
+                catch (IOException e)
+                {
+                    throw new RuntimeException(e);
+                }
+            }
+        });
+        if (forceCollection)
+        {
+            System.runFinalization();
+            System.gc();
+            Thread.sleep(finalWaitMillis);
+        }
+        dumpResources("final-jmx");
+    }
+
+    @Test
+    public void looperEverythingTest() throws Throwable
+    {
+        doTest(1, config -> config.with(Feature.values()));
+        if (forceCollection)
+        {
+            System.runFinalization();
+            System.gc();
+            Thread.sleep(finalWaitMillis);
+        }
+        dumpResources("final-everything");
+    }
 }
diff --git a/test/distributed/org/apache/cassandra/distributed/test/SchemaTest.java b/test/distributed/org/apache/cassandra/distributed/test/SchemaTest.java
index a2ce32f..d76f30b 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/SchemaTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/SchemaTest.java
@@ -22,7 +22,6 @@
 
 import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.api.ConsistencyLevel;
-
 import static org.junit.Assert.assertTrue;
 
 public class SchemaTest extends TestBaseImpl
diff --git a/test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTest.java b/test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTest.java
new file mode 100644
index 0000000..27390d6
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.distributed.test;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.Test;
+
+import net.bytebuddy.ByteBuddy;
+import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
+import net.bytebuddy.implementation.MethodDelegation;
+import net.bytebuddy.implementation.bind.annotation.SuperCall;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.ICoordinator;
+import org.apache.cassandra.distributed.api.IInvokableInstance;
+import org.apache.cassandra.transport.messages.ResultMessage;
+
+import static java.lang.String.format;
+import static java.util.Arrays.asList;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL;
+import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
+
+/**
+ * Tests short read protection, the mechanism that ensures distributed queries at read consistency levels > ONE/LOCAL_ONE
+ * avoid short reads that might happen when a limit is used and reconciliation accepts less rows than such limit.
+ */
+public class ShortReadProtectionTest extends TestBaseImpl
+{
+    /**
+     * Test GROUP BY with short read protection, particularly when there is a limit and regular row deletions.
+     * <p>
+     * See CASSANDRA-15459
+     */
+    @Test
+    public void testGroupBySRPRegularRow() throws Throwable
+    {
+        testGroupBySRP("CREATE TABLE %s (pk int, ck int, PRIMARY KEY (pk, ck))",
+                       asList("INSERT INTO %s (pk, ck) VALUES (1, 1) USING TIMESTAMP 0",
+                              "DELETE FROM %s WHERE pk=0 AND ck=0",
+                              "INSERT INTO %s (pk, ck) VALUES (2, 2) USING TIMESTAMP 0"),
+                       asList("DELETE FROM %s WHERE pk=1 AND ck=1",
+                              "INSERT INTO %s (pk, ck) VALUES (0, 0) USING TIMESTAMP 0",
+                              "DELETE FROM %s WHERE pk=2 AND ck=2"),
+                       asList("SELECT * FROM %s LIMIT 1",
+                              "SELECT * FROM %s LIMIT 10",
+                              "SELECT * FROM %s GROUP BY pk LIMIT 1",
+                              "SELECT * FROM %s GROUP BY pk LIMIT 10",
+                              "SELECT * FROM %s GROUP BY pk, ck LIMIT 1",
+                              "SELECT * FROM %s GROUP BY pk, ck LIMIT 10"));
+    }
+
+    /**
+     * Test GROUP BY with short read protection, particularly when there is a limit and static row deletions.
+     * <p>
+     * See CASSANDRA-15459
+     */
+    @Test
+    public void testGroupBySRPStaticRow() throws Throwable
+    {
+        testGroupBySRP("CREATE TABLE %s (pk int, ck int, s int static, PRIMARY KEY (pk, ck))",
+                       asList("INSERT INTO %s (pk, s) VALUES (1, 1) USING TIMESTAMP 0",
+                              "INSERT INTO %s (pk, s) VALUES (0, null)",
+                              "INSERT INTO %s (pk, s) VALUES (2, 2) USING TIMESTAMP 0"),
+                       asList("INSERT INTO %s (pk, s) VALUES (1, null)",
+                              "INSERT INTO %s (pk, s) VALUES (0, 0) USING TIMESTAMP 0",
+                              "INSERT INTO %s (pk, s) VALUES (2, null)"),
+                       asList("SELECT * FROM %s LIMIT 1",
+                              "SELECT * FROM %s LIMIT 10",
+                              "SELECT * FROM %s GROUP BY pk LIMIT 1",
+                              "SELECT * FROM %s GROUP BY pk LIMIT 10",
+                              "SELECT * FROM %s GROUP BY pk, ck LIMIT 1",
+                              "SELECT * FROM %s GROUP BY pk, ck LIMIT 10"));
+    }
+
+    private void testGroupBySRP(String createTable,
+                                List<String> node1Queries,
+                                List<String> node2Queries,
+                                List<String> coordinatorQueries) throws Throwable
+    {
+        try (Cluster cluster = init(Cluster.build()
+                                           .withNodes(2)
+                                           .withConfig(config -> config.set("hinted_handoff_enabled", false))
+                                           .withInstanceInitializer(BBDropMutationsHelper::install)
+                                           .start()))
+        {
+            String table = withKeyspace("%s.t");
+            cluster.schemaChange(format(createTable, table));
+
+            // populate data on node1
+            IInvokableInstance node1 = cluster.get(1);
+            for (String query : node1Queries)
+                node1.executeInternal(format(query, table));
+
+            // populate data on node2
+            IInvokableInstance node2 = cluster.get(2);
+            for (String query : node2Queries)
+                node2.executeInternal(format(query, table));
+
+            // ignore read repair writes
+            node1.runOnInstance(BBDropMutationsHelper::enable);
+            node2.runOnInstance(BBDropMutationsHelper::enable);
+
+            // verify the behaviour of SRP with GROUP BY queries
+            ICoordinator coordinator = cluster.coordinator(1);
+            for (String query : coordinatorQueries)
+                assertRows(coordinator.execute(format(query, table), ALL));
+        }
+    }
+
+    /**
+     * Byte Buddy helper to silently drop mutations.
+     */
+    public static class BBDropMutationsHelper
+    {
+        private static final AtomicBoolean enabled = new AtomicBoolean(false);
+
+        static void enable()
+        {
+            enabled.set(true);
+        }
+
+        static void install(ClassLoader cl, int nodeNumber)
+        {
+            new ByteBuddy().rebase(Mutation.class)
+                           .method(named("apply"))
+                           .intercept(MethodDelegation.to(BBDropMutationsHelper.class))
+                           .make()
+                           .load(cl, ClassLoadingStrategy.Default.INJECTION);
+        }
+
+        public static void execute(@SuperCall Callable<ResultMessage.Rows> r) throws Exception
+        {
+            if (enabled.get())
+                return;
+            r.call();
+        }
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/test/SimpleReadWriteTest.java b/test/distributed/org/apache/cassandra/distributed/test/SimpleReadWriteTest.java
index 3e8b76b..b7cef5f 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/SimpleReadWriteTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/SimpleReadWriteTest.java
@@ -110,6 +110,9 @@
                    row(1, 1, 1));
     }
 
+    /**
+     * If a node receives a mutation for a column it's not aware of, it should fail, since it can't write the data.
+     */
     @Test
     public void writeWithSchemaDisagreement() throws Throwable
     {
@@ -126,7 +129,7 @@
         try
         {
             cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v1, v2) VALUES (2, 2, 2, 2)",
-                                           ConsistencyLevel.QUORUM);
+                                           ConsistencyLevel.ALL);
         }
         catch (RuntimeException e)
         {
@@ -134,11 +137,16 @@
         }
 
         Assert.assertTrue(thrown.getMessage().contains("Exception occurred on node"));
-        Assert.assertTrue(thrown.getCause().getCause().getCause().getMessage().contains("Unknown column v2 during deserialization"));
+        Assert.assertTrue(thrown.getCause().getCause().getCause().getMessage().contains("Unknown column v2"));
     }
 
+
+
+    /**
+     * If a node isn't aware of a column, but receives a mutation without that column, the write should succeed
+     */
     @Test
-    public void readWithSchemaDisagreement() throws Throwable
+    public void writeWithInconsequentialSchemaDisagreement() throws Throwable
     {
         cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v1 int, PRIMARY KEY (pk, ck))");
 
@@ -149,20 +157,9 @@
         // Introduce schema disagreement
         cluster.schemaChange("ALTER TABLE " + KEYSPACE + ".tbl ADD v2 int", 1);
 
-        Exception thrown = null;
-        try
-        {
-            assertRows(cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = 1",
-                                                      ConsistencyLevel.ALL),
-                       row(1, 1, 1, null));
-        }
-        catch (Exception e)
-        {
-            thrown = e;
-        }
-
-        Assert.assertTrue(thrown.getMessage().contains("Exception occurred on node"));
-        Assert.assertTrue(thrown.getCause().getCause().getCause().getMessage().contains("Unknown column v2 during deserialization"));
+        // this write shouldn't cause any problems because it doesn't write to the new column
+        cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v1) VALUES (2, 2, 2)",
+                                       ConsistencyLevel.ALL);
     }
 
     @Test
@@ -392,4 +389,12 @@
     {
         return instance.callOnInstance(() -> Keyspace.open(KEYSPACE).getColumnFamilyStore("tbl").metric.readLatency.latency.getCount());
     }
+
+    private static Object[][] rows(Object[]...rows)
+    {
+        Object[][] r = new Object[rows.length][];
+        System.arraycopy(rows, 0, r, 0, rows.length);
+        return r;
+    }
+
 }
diff --git a/test/distributed/org/apache/cassandra/distributed/test/SinglePartitionReadCommandTest.java b/test/distributed/org/apache/cassandra/distributed/test/SinglePartitionReadCommandTest.java
index 3f53b6b..399b522 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/SinglePartitionReadCommandTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/SinglePartitionReadCommandTest.java
@@ -146,6 +146,25 @@
     }
 
     @Test
+    public void testNonCompactTableWithEmptyRowOnBothNodes() throws Throwable
+    {
+        try (Cluster cluster = init(builder().withNodes(2).start()))
+        {
+            cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck text, v int, PRIMARY KEY (pk, ck))"));
+            cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.tbl (pk, ck, v) VALUES (1, '1', 1) USING TIMESTAMP 1000"), ConsistencyLevel.ALL);
+            cluster.get(1).flush(KEYSPACE);
+            cluster.coordinator(1).execute(withKeyspace("DELETE v FROM %s.tbl USING TIMESTAMP 2000 WHERE pk=1 AND ck='1'"), ConsistencyLevel.ALL);
+            cluster.get(1).flush(KEYSPACE);
+            cluster.get(2).flush(KEYSPACE);
+
+            assertRows(cluster.coordinator(2).execute(withKeyspace("SELECT * FROM %s.tbl WHERE pk=1 AND ck='1'"), ConsistencyLevel.ALL),
+                       row(1, "1", null));
+            assertRows(cluster.coordinator(2).execute(withKeyspace("SELECT v FROM %s.tbl WHERE pk=1 AND ck='1'"), ConsistencyLevel.ALL),
+                       row((Integer) null));
+        }
+    }
+
+    @Test
     public void testCompactAndNonCompactTableWithRowOnOneNodeAndRowDeletionOnTheOther() throws Throwable
     {
         for (String options : new String[] {"WITH COMPACT STORAGE", ""})
@@ -176,6 +195,29 @@
     }
 
     @Test
+    public void testNonCompactTableWithRowOnOneNodeMissingAColumnAndColumnDeletionOnTheOther() throws Throwable
+    {
+        try (Cluster cluster = init(builder().withNodes(2).start()))
+        {
+            cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck text, v1 int, v2 int, PRIMARY KEY (pk, ck))"));
+            cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck, v1, v2) VALUES (1, '1', 1, 1) USING TIMESTAMP 1000"));
+            cluster.get(1).flush(KEYSPACE);
+            cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck, v1) VALUES (1, '1', 2) USING TIMESTAMP 2000"));
+            cluster.get(1).flush(KEYSPACE);
+
+            cluster.get(2).executeInternal(withKeyspace("DELETE v1 FROM %s.tbl USING TIMESTAMP 3000 WHERE pk=1 AND ck='1'"));
+            cluster.get(2).flush(KEYSPACE);
+
+            assertRows(cluster.coordinator(2).execute(withKeyspace("SELECT * FROM %s.tbl WHERE pk=1 AND ck='1'"), ConsistencyLevel.ALL),
+                       row(1, "1", null, 1));
+            assertRows(cluster.coordinator(2).execute(withKeyspace("SELECT v1 FROM %s.tbl WHERE pk=1 AND ck='1'"), ConsistencyLevel.ALL),
+                       row((Integer) null));
+            assertRows(cluster.coordinator(2).execute(withKeyspace("SELECT v2 FROM %s.tbl WHERE pk=1 AND ck='1'"), ConsistencyLevel.ALL),
+                       row(1));
+        }
+    }
+
+    @Test
     public void testCompactAndNonCompactTableWithRowOnOneNodeAndRangeDeletionOnTheOther() throws Throwable
     {
         for (String options : new String[] {"WITH COMPACT STORAGE", ""})
diff --git a/test/distributed/org/apache/cassandra/distributed/test/UnableToParseClientMessageTest.java b/test/distributed/org/apache/cassandra/distributed/test/UnableToParseClientMessageTest.java
index 75cc905..7bfe5a0 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/UnableToParseClientMessageTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/UnableToParseClientMessageTest.java
@@ -22,10 +22,12 @@
 import java.util.List;
 
 import org.junit.Assert;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import io.netty.buffer.Unpooled;
 import org.apache.cassandra.Util;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.api.Feature;
 import org.apache.cassandra.distributed.api.IInvokableInstance;
@@ -42,6 +44,12 @@
  */
 public class UnableToParseClientMessageTest extends TestBaseImpl
 {
+    @BeforeClass
+    public static void setup()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void badMessageCausesProtocolException() throws IOException, InterruptedException
     {
@@ -66,7 +74,7 @@
                 // this should return a failed response
                 String response = client.write(Unpooled.wrappedBuffer("This is just a test".getBytes(StandardCharsets.UTF_8)), false).toString();
                 Assert.assertTrue("Resposne '" + response + "' expected to contain 'Invalid or unsupported protocol version (84); the lowest supported version is 3 and the greatest is 4'",
-                                  response.contains("Invalid or unsupported protocol version (84); the lowest supported version is 3 and the greatest is 4"));
+                                  response.contains("Invalid or unsupported protocol version (84)"));
 
                 node.runOnInstance(() -> {
                     Util.spinAssertEquals(1L,
@@ -74,6 +82,7 @@
                                                                                 .get("org.apache.cassandra.metrics.Client.ProtocolException")
                                                                                 .getCount(),
                                           10);
+
                     Assert.assertEquals(0, CassandraMetricsRegistry.Metrics.getMeters()
                                                                            .get("org.apache.cassandra.metrics.Client.UnknownException")
                                                                            .getCount());
diff --git a/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXFeatureTest.java b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXFeatureTest.java
new file mode 100644
index 0000000..1c38bd1
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXFeatureTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.distributed.test.jmx;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import javax.management.MBeanServerConnection;
+import javax.management.remote.JMXConnector;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.Feature;
+import org.apache.cassandra.distributed.api.IInstanceConfig;
+import org.apache.cassandra.distributed.api.IInvokableInstance;
+import org.apache.cassandra.distributed.shared.JMXUtil;
+import org.apache.cassandra.distributed.test.TestBaseImpl;
+
+import static org.hamcrest.Matchers.startsWith;
+
+public class JMXFeatureTest extends TestBaseImpl
+{
+    /**
+     * Test the in-jvm dtest JMX feature.
+     * - Create a cluster with multiple JMX servers, one per instance
+     * - Test that when connecting, we get the correct MBeanServer by checking the default domain, which is set to the IP of the instance
+     * - Run the test multiple times to ensure cleanup of the JMX servers is complete so the next test can run successfully using the same host/port.
+     * NOTE: In later versions of Cassandra, there is also a `testOneNetworkInterfaceProvisioning` that leverages the ability to specify
+     * ports in addition to IP/Host for binding, but this version does not support that feature. Keeping the test name the same
+     * so that it's consistent across versions.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testMultipleNetworkInterfacesProvisioning() throws Exception
+    {
+        int iterations = 2; // Make sure the JMX infrastructure all cleans up properly by running this multiple times.
+        Set<String> allInstances = new HashSet<>();
+        for (int i = 0; i < iterations; i++)
+        {
+            try (Cluster cluster = Cluster.build(2).withConfig(c -> c.with(Feature.values())).start())
+            {
+                Set<String> instancesContacted = new HashSet<>();
+                for (IInvokableInstance instance : cluster.get(1, 2))
+                {
+                    testInstance(instancesContacted, instance);
+                }
+                Assert.assertEquals("Should have connected with both JMX instances.", 2, instancesContacted.size());
+                allInstances.addAll(instancesContacted);
+            }
+        }
+        Assert.assertEquals("Each instance from each cluster should have been unique", iterations * 2, allInstances.size());
+    }
+
+    private void testInstance(Set<String> instancesContacted, IInvokableInstance instance) throws IOException
+    {
+        IInstanceConfig config = instance.config();
+        try (JMXConnector jmxc = JMXUtil.getJmxConnector(config))
+        {
+            MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
+            // instances get their default domain set to their IP address, so us it
+            // to check that we are actually connecting to the correct instance
+            String defaultDomain = mbsc.getDefaultDomain();
+            instancesContacted.add(defaultDomain);
+            Assert.assertThat(defaultDomain, startsWith(JMXUtil.getJmxHost(config) + ":" + config.jmxPort()));
+        }
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java
new file mode 100644
index 0000000..a3f74df
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.distributed.test.jmx;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import javax.management.JMRuntimeException;
+import javax.management.MBeanAttributeInfo;
+import javax.management.MBeanInfo;
+import javax.management.MBeanOperationInfo;
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.Feature;
+import org.apache.cassandra.distributed.api.IInvokableInstance;
+import org.apache.cassandra.distributed.test.TestBaseImpl;
+
+public class JMXGetterCheckTest extends TestBaseImpl
+{
+    private static final Set<String> IGNORE_ATTRIBUTES = ImmutableSet.of(
+    "org.apache.cassandra.net:type=MessagingService:BackPressurePerHost" // throws unsupported saying the feature was removed... dropped in CASSANDRA-15375
+    );
+    private static final Set<String> IGNORE_OPERATIONS = ImmutableSet.of(
+    "org.apache.cassandra.db:type=StorageService:stopDaemon", // halts the instance, which then causes the JVM to exit
+    "org.apache.cassandra.db:type=StorageService:drain", // don't drain, it stops things which can cause other APIs to be unstable as we are in a stopped state
+    "org.apache.cassandra.db:type=StorageService:stopGossiping", // if we stop gossip this can cause other issues, so avoid
+    "org.apache.cassandra.db:type=StorageService:resetLocalSchema", // this will fail when there are no other nodes which can serve schema
+    "org.apache.cassandra.db:type=HintedHandoffManager:listEndpointsPendingHints", // this will fail because it only exists to match an old, deprecated mbean and just throws an UnsportedOperationException
+    "org.apache.cassandra.db:type=StorageService:decommission" // Don't decommission nodes! Note that in future versions of C* this is unnecessary because decommission takes an argument.
+    );
+
+    public static final String JMX_SERVICE_URL_FMT = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi";
+
+    @Test
+    public void testGetters() throws Exception
+    {
+        try (Cluster cluster = Cluster.build(1).withConfig(c -> c.with(Feature.values())).start())
+        {
+            IInvokableInstance instance = cluster.get(1);
+
+            String jmxHost = instance.config().broadcastAddress().getAddress().getHostAddress();
+            String url = String.format(JMX_SERVICE_URL_FMT, jmxHost, instance.config().jmxPort());
+            List<Named> errors = new ArrayList<>();
+            try (JMXConnector jmxc = JMXConnectorFactory.connect(new JMXServiceURL(url), null))
+            {
+                MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
+                Set<ObjectName> metricNames = new TreeSet<>(mbsc.queryNames(null, null));
+                for (ObjectName name : metricNames)
+                {
+                    if (!name.getDomain().startsWith("org.apache.cassandra"))
+                        continue;
+                    MBeanInfo info = mbsc.getMBeanInfo(name);
+                    for (MBeanAttributeInfo a : info.getAttributes())
+                    {
+                        String fqn = String.format("%s:%s", name, a.getName());
+                        if (!a.isReadable() || IGNORE_ATTRIBUTES.contains(fqn))
+                            continue;
+                        try
+                        {
+                            mbsc.getAttribute(name, a.getName());
+                        }
+                        catch (JMRuntimeException e)
+                        {
+                            errors.add(new Named(String.format("Attribute %s", fqn), e.getCause()));
+                        }
+                    }
+
+                    for (MBeanOperationInfo o : info.getOperations())
+                    {
+                        String fqn = String.format("%s:%s", name, o.getName());
+                        if (o.getSignature().length != 0 || IGNORE_OPERATIONS.contains(fqn))
+                            continue;
+                        try
+                        {
+                            mbsc.invoke(name, o.getName(), new Object[0], new String[0]);
+                        }
+                        catch (JMRuntimeException e)
+                        {
+                            errors.add(new Named(String.format("Operation %s", fqn), e.getCause()));
+                        }
+                    }
+                }
+            }
+            if (!errors.isEmpty())
+            {
+                AssertionError root = new AssertionError();
+                errors.forEach(root::addSuppressed);
+                throw root;
+            }
+        }
+    }
+
+    /**
+     * This class is meant to make new errors easier to read, by adding the JMX endpoint, and cleaning up the unneeded JMX/Reflection logic cluttering the stacktrace
+     */
+    private static class Named extends RuntimeException
+    {
+        public Named(String msg, Throwable cause)
+        {
+            super(msg + "\nCaused by: " + cause.getClass().getCanonicalName() + ": " + cause.getMessage(), cause.getCause());
+            StackTraceElement[] stack = cause.getStackTrace();
+            List<StackTraceElement> copy = new ArrayList<>();
+            for (StackTraceElement s : stack)
+            {
+                if (!s.getClassName().startsWith("org.apache.cassandra"))
+                    break;
+                copy.add(s);
+            }
+            Collections.reverse(copy);
+            setStackTrace(copy.toArray(new StackTraceElement[0]));
+        }
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorage2to3UpgradeTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorage2to3UpgradeTest.java
deleted file mode 100644
index 3cf9fff..0000000
--- a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorage2to3UpgradeTest.java
+++ /dev/null
@@ -1,365 +0,0 @@
-/*
- * 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.distributed.upgrade;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.junit.Ignore;
-import org.junit.Test;
-
-import org.apache.cassandra.distributed.UpgradeableCluster;
-import org.apache.cassandra.distributed.api.ConsistencyLevel;
-import org.apache.cassandra.distributed.api.ICoordinator;
-import org.apache.cassandra.distributed.api.IMessageFilters;
-import org.apache.cassandra.distributed.api.NodeToolResult;
-import org.apache.cassandra.distributed.shared.Versions;
-
-import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
-import static org.apache.cassandra.distributed.shared.AssertUtils.row;
-import static org.junit.Assert.assertEquals;
-import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
-import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
-import static org.apache.cassandra.distributed.api.Feature.NETWORK;
-
-@Ignore // 2.2 branch is no longer maintained and using dtest-api 0.0.8 which is incompatible with current
-public class CompactStorage2to3UpgradeTest extends UpgradeTestBase
-{
-    @Test
-    public void multiColumn() throws Throwable
-    {
-        new TestCase()
-        .upgradesFrom(v22)
-        .setup(cluster -> {
-            assert cluster.size() == 3;
-            int rf = cluster.size() - 1;
-            assert rf == 2;
-            cluster.schemaChange("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': " + (cluster.size() - 1) + "};");
-            cluster.schemaChange("CREATE TABLE ks.tbl (pk int, v1 int, v2 text, PRIMARY KEY (pk)) WITH COMPACT STORAGE");
-            ICoordinator coordinator = cluster.coordinator(1);
-            // these shouldn't be replicated by the 3rd node
-            coordinator.execute("INSERT INTO ks.tbl (pk, v1, v2) VALUES (3, 3, '3')", ConsistencyLevel.ALL);
-            coordinator.execute("INSERT INTO ks.tbl (pk, v1, v2) VALUES (9, 9, '9')", ConsistencyLevel.ALL);
-            for (int i = 0; i < cluster.size(); i++)
-            {
-                int nodeNum = i + 1;
-                System.out.printf("****** node %s: %s%n", nodeNum, cluster.get(nodeNum).config());
-            }
-        })
-        .runAfterNodeUpgrade(((cluster, node) -> {
-            if (node != 2)
-                return;
-
-            Object[][] rows = cluster.coordinator(3).execute("SELECT * FROM ks.tbl LIMIT 2", ConsistencyLevel.ALL);
-            Object[][] expected = {
-            row(9, 9, "9"),
-            row(3, 3, "3")
-            };
-            assertRows(rows, expected);
-        })).run();
-    }
-
-    @Test
-    public void singleColumn() throws Throwable
-    {
-        new TestCase()
-        .upgradesFrom(v22)
-        .setup(cluster -> {
-            assert cluster.size() == 3;
-            int rf = cluster.size() - 1;
-            assert rf == 2;
-            cluster.schemaChange("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': " + (cluster.size() - 1) + "};");
-            cluster.schemaChange("CREATE TABLE ks.tbl (pk int, v int, PRIMARY KEY (pk)) WITH COMPACT STORAGE");
-            ICoordinator coordinator = cluster.coordinator(1);
-            // these shouldn't be replicated by the 3rd node
-            coordinator.execute("INSERT INTO ks.tbl (pk, v) VALUES (3, 3)", ConsistencyLevel.ALL);
-            coordinator.execute("INSERT INTO ks.tbl (pk, v) VALUES (9, 9)", ConsistencyLevel.ALL);
-            for (int i = 0; i < cluster.size(); i++)
-            {
-                int nodeNum = i + 1;
-                System.out.printf("****** node %s: %s%n", nodeNum, cluster.get(nodeNum).config());
-            }
-        })
-        .runAfterNodeUpgrade(((cluster, node) -> {
-
-            if (node < 2)
-                return;
-
-            Object[][] rows = cluster.coordinator(3).execute("SELECT * FROM ks.tbl LIMIT 2", ConsistencyLevel.ALL);
-            Object[][] expected = {
-            row(9, 9),
-            row(3, 3)
-            };
-            assertRows(rows, expected);
-        })).run();
-    }
-
-    @Test
-    public void testDropCompactWithClusteringAndValueColumn() throws Throwable
-    {
-        final String table = "clustering_and_value";
-        final int partitions = 10;
-        final int rowsPerPartition = 10;
-
-        final ResultsRecorder recorder = new ResultsRecorder();
-        new TestCase()
-
-        .nodes(2)
-        .upgradesFrom(v22)
-        .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL))
-        .setup(cluster -> {
-            cluster.schemaChange(String.format(
-            "CREATE TABLE %s.%s (key int, c1 int, c2 int, c3 int, PRIMARY KEY (key, c1, c2)) WITH COMPACT STORAGE",
-            KEYSPACE, table));
-            ICoordinator coordinator = cluster.coordinator(1);
-
-
-                    for (int i = 1; i <= partitions; i++)
-                    {
-                        for (int j = 1; j <= rowsPerPartition; j++)
-                        {
-                            coordinator.execute(String.format("INSERT INTO %s.%s (key, c1, c2, c3) VALUES (%d, %d, 1, 1)",
-                                    KEYSPACE, table, i, j), ConsistencyLevel.ALL);
-                            coordinator.execute(String.format("INSERT INTO %s.%s (key, c1, c2, c3) VALUES (%d, %d, 2, 1)",
-                                                              KEYSPACE, table, i, j), ConsistencyLevel.ALL);
-                        }
-                    }
-
-                    runQueries(cluster.coordinator(1), recorder, new String[] {
-                            String.format("SELECT * FROM %s.%s", KEYSPACE, table),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d",
-                                    KEYSPACE, table, partitions - 3, rowsPerPartition - 2),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d",
-                                    KEYSPACE, table, partitions - 1, rowsPerPartition - 5),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d and c2 = %d",
-                                    KEYSPACE, table, partitions - 1, rowsPerPartition - 5, 1),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d and c2 = %d",
-                                    KEYSPACE, table, partitions - 4, rowsPerPartition - 9, 1),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d and c2 > %d",
-                                          KEYSPACE, table, partitions - 4, rowsPerPartition - 9, 1),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d and c2 > %d",
-                                          KEYSPACE, table, partitions - 4, rowsPerPartition - 9, 2),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 > %d",
-                                    KEYSPACE, table, partitions - 8, rowsPerPartition - 3),
-
-                    });
-
-                }).runBeforeNodeRestart((cluster, node) ->
-                {
-                    cluster.get(node).config().set("enable_drop_compact_storage", true);
-
-                }).runAfterClusterUpgrade(cluster ->
-                {
-                    for (int i = 1; i <= cluster.size(); i++)
-                    {
-                        NodeToolResult result = cluster.get(i).nodetoolResult("upgradesstables");
-                        assertEquals("upgrade sstables failed for node " + i, 0, result.getRc());
-                    }
-                    Thread.sleep(600);
-
-                    // make sure the results are the same after upgrade and upgrade sstables but before dropping compact storage
-                    recorder.validateResults(cluster, 1);
-                    recorder.validateResults(cluster, 2);
-
-                    // make sure the results are the same after dropping compact storage on only the first node
-                    IMessageFilters.Filter filter = cluster.verbs().allVerbs().to(2).drop();
-                    cluster.schemaChange(String.format("ALTER TABLE %s.%s DROP COMPACT STORAGE", KEYSPACE, table), 1);
-
-                    recorder.validateResults(cluster, 1, ConsistencyLevel.ONE);
-
-                    filter.off();
-                    recorder.validateResults(cluster, 1);
-                    recorder.validateResults(cluster, 2);
-
-                    // make sure the results continue to be the same after dropping compact storage on the second node
-                    cluster.schemaChange(String.format("ALTER TABLE %s.%s DROP COMPACT STORAGE", KEYSPACE, table), 2);
-                    recorder.validateResults(cluster, 1);
-                    recorder.validateResults(cluster, 2);
-                })
-                .run();
-    }
-
-    @Test
-    public void testDropCompactWithClusteringAndValueColumnWithDeletesAndWrites() throws Throwable
-    {
-        final String table = "clustering_and_value_with_deletes";
-        final int partitions = 10;
-        final int rowsPerPartition = 10;
-        final int additionalParititons = 5;
-
-        new TestCase()
-                .nodes(2)
-                .upgradesFrom(v22)
-                .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL).set("enable_drop_compact_storage", true))
-                .setup(cluster -> {
-                    cluster.schemaChange(String.format(
-                            "CREATE TABLE %s.%s (key int, c1 int, c2 int, c3 int, PRIMARY KEY (key, c1, c2)) WITH COMPACT STORAGE",
-                            KEYSPACE, table));
-                    ICoordinator coordinator = cluster.coordinator(1);
-
-                    for (int i = 1; i <= partitions; i++)
-                    {
-                        for (int j = 1; j <= rowsPerPartition; j++)
-                        {
-                            coordinator.execute(String.format("INSERT INTO %s.%s (key, c1, c2, c3) VALUES (%d, %d, 1, 1)",
-                                    KEYSPACE, table, i, j), ConsistencyLevel.ALL);
-                            coordinator.execute(String.format("INSERT INTO %s.%s (key, c1, c2, c3) VALUES (%d, %d, 2, 2)",
-                                                              KEYSPACE, table, i, j), ConsistencyLevel.ALL);
-                            coordinator.execute(String.format("INSERT INTO %s.%s (key, c1, c2, c3) VALUES (%d, %d, 3, 3)",
-                                                              KEYSPACE, table, i, j), ConsistencyLevel.ALL);
-                        }
-                    }
-                })
-                .runAfterClusterUpgrade(cluster -> {
-                    cluster.forEach(n -> n.nodetoolResult("upgradesstables", KEYSPACE).asserts().success());
-                    Thread.sleep(1000);
-                    // drop compact storage on only one node before performing writes
-                    IMessageFilters.Filter filter = cluster.verbs().allVerbs().to(2).drop();
-                    cluster.schemaChange(String.format("ALTER TABLE %s.%s DROP COMPACT STORAGE", KEYSPACE, table), 1);
-                    filter.off();
-
-                    // add new partitions and delete some of the old ones
-                    ICoordinator coordinator = cluster.coordinator(1);
-                    for (int i = 0; i < additionalParititons; i++)
-                    {
-                        for (int j = 1; j <= rowsPerPartition; j++)
-                        {
-                            coordinator.execute(String.format("INSERT INTO %s.%s (key, c1, c2, c3) VALUES (%d, %d, 1, 1)",
-                                    KEYSPACE, table, i, j), ConsistencyLevel.ALL);
-                        }
-                    }
-
-                    coordinator.execute(String.format("DELETE FROM %s.%s WHERE key = %d and c1 = %d",
-                            KEYSPACE, table, 0, 3), ConsistencyLevel.ALL);
-
-                    coordinator.execute(String.format("DELETE FROM %s.%s WHERE key = %d",
-                            KEYSPACE, table, 1), ConsistencyLevel.ALL);
-
-                    coordinator.execute(String.format("DELETE FROM %s.%s WHERE key = %d and c1 = %d and c2 = %d",
-                            KEYSPACE, table, 7, 2, 2), ConsistencyLevel.ALL);
-
-                    coordinator.execute(String.format("DELETE FROM %s.%s WHERE key = %d and c1 = %d and c2 = %d",
-                            KEYSPACE, table, 7, 6, 1), ConsistencyLevel.ALL);
-
-                    coordinator.execute(String.format("DELETE FROM %s.%s WHERE key = %d and c1 = %d and c2 = %d",
-                                                      KEYSPACE, table, 4, 1, 1), ConsistencyLevel.ALL);
-
-                    coordinator.execute(String.format("DELETE c3 FROM %s.%s WHERE key = %d and c1 = %d and c2 = %d",
-                            KEYSPACE, table, 8, 1, 3), ConsistencyLevel.ALL);
-
-                    coordinator.execute(String.format("DELETE FROM %s.%s WHERE key = %d and c1 = %d and c2 > 1",
-                                                      KEYSPACE, table, 6, 2), ConsistencyLevel.ALL);
-
-                    ResultsRecorder recorder = new ResultsRecorder();
-                    runQueries(coordinator, recorder, new String[] {
-                            String.format("SELECT * FROM %s.%s", KEYSPACE, table),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d",
-                                    KEYSPACE, table, partitions - 3, rowsPerPartition - 2),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d",
-                                    KEYSPACE, table, partitions - 1, rowsPerPartition - 5),
-
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 > %d",
-                                    KEYSPACE, table, partitions - 8, rowsPerPartition - 3),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d",
-                                    KEYSPACE, table, 7),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d",
-                                    KEYSPACE, table, 7, 2),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d",
-                                    KEYSPACE, table, 8, 1),
-
-                            String.format("SELECT c1, c2 FROM %s.%s WHERE key = %d and c1 = %d",
-                                    KEYSPACE, table, 8, 1),
-
-                            String.format("SELECT c1, c2 FROM %s.%s WHERE key = %d and c1 = %d",
-                                          KEYSPACE, table, 8, 1),
-
-                            String.format("SELECT c1, c2 FROM %s.%s WHERE key = %d and c1 = %d",
-                                          KEYSPACE, table, 4, 1),
-
-                            String.format("SELECT c1, c2 FROM %s.%s WHERE key = %d",
-                                          KEYSPACE, table, 6),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 > %d",
-                                    KEYSPACE, table, 0, 1),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d",
-                                    KEYSPACE, table, partitions - (additionalParititons - 2)),
-
-                            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 > %d",
-                                    KEYSPACE, table, partitions - (additionalParititons - 3), 4)
-
-                    });
-
-                    // drop compact storage on remaining node and check result
-                    cluster.schemaChange(String.format("ALTER TABLE %s.%s DROP COMPACT STORAGE", KEYSPACE, table), 2);
-                    recorder.validateResults(cluster, 1);
-                    recorder.validateResults(cluster, 2);
-                }).run();
-    }
-
-    private void runQueries(ICoordinator coordinator, ResultsRecorder helper, String[] queries)
-    {
-        for (String query : queries)
-            helper.addResult(query, coordinator.execute(query, ConsistencyLevel.ALL));
-    }
-
-    public static class ResultsRecorder
-    {
-        final private Map<String, Object[][]> preUpgradeResults = new HashMap<>();
-
-        public void addResult(String query, Object[][] results)
-        {
-            preUpgradeResults.put(query, results);
-        }
-
-        public Map<String, Object[][]> queriesAndResults()
-        {
-            return preUpgradeResults;
-        }
-
-        public void validateResults(UpgradeableCluster cluster, int node)
-        {
-            validateResults(cluster, node, ConsistencyLevel.ALL);
-        }
-
-        public void validateResults(UpgradeableCluster cluster, int node, ConsistencyLevel cl)
-        {
-            for (Map.Entry<String, Object[][]> entry : queriesAndResults().entrySet())
-            {
-                Object[][] postUpgradeResult = cluster.coordinator(node).execute(entry.getKey(), cl);
-                assertRows(postUpgradeResult, entry.getValue());
-            }
-
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageMultiColumnTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageMultiColumnTest.java
new file mode 100644
index 0000000..086982e
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageMultiColumnTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.distributed.upgrade;
+
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.distributed.api.ICoordinator;
+
+import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
+import static org.apache.cassandra.distributed.shared.AssertUtils.row;
+
+
+public class CompactStorageMultiColumnTest extends UpgradeTestBase
+{
+    @Test
+    public void multiColumn() throws Throwable
+    {
+        new TestCase()
+        .upgradesFrom(v22)
+        .setup(cluster -> {
+            assert cluster.size() == 3;
+            int rf = cluster.size() - 1;
+            assert rf == 2;
+            cluster.schemaChange("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': " + (cluster.size() - 1) + "};");
+            cluster.schemaChange("CREATE TABLE ks.tbl (pk int, v1 int, v2 text, PRIMARY KEY (pk)) WITH COMPACT STORAGE");
+            ICoordinator coordinator = cluster.coordinator(1);
+            // these shouldn't be replicated by the 3rd node
+            coordinator.execute("INSERT INTO ks.tbl (pk, v1, v2) VALUES (3, 3, '3')", ConsistencyLevel.ALL);
+            coordinator.execute("INSERT INTO ks.tbl (pk, v1, v2) VALUES (9, 9, '9')", ConsistencyLevel.ALL);
+            for (int i = 0; i < cluster.size(); i++)
+            {
+                int nodeNum = i + 1;
+                System.out.printf("****** node %s: %s%n", nodeNum, cluster.get(nodeNum).config());
+            }
+        })
+        .runAfterNodeUpgrade(((cluster, node) -> {
+            if (node != 2)
+                return;
+
+            Object[][] rows = cluster.coordinator(3).execute("SELECT * FROM ks.tbl LIMIT 2", ConsistencyLevel.ALL);
+            Object[][] expected = {
+            row(9, 9, "9"),
+            row(3, 3, "3")
+            };
+            assertRows(rows, expected);
+        })).run();
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageSingleColumnTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageSingleColumnTest.java
new file mode 100644
index 0000000..8a7170f
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageSingleColumnTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.distributed.upgrade;
+
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.distributed.api.ICoordinator;
+
+import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
+import static org.apache.cassandra.distributed.shared.AssertUtils.row;
+
+
+public class CompactStorageSingleColumnTest extends UpgradeTestBase
+{
+    @Test
+    public void singleColumn() throws Throwable
+    {
+        new TestCase()
+        .upgradesFrom(v22)
+        .setup(cluster -> {
+            assert cluster.size() == 3;
+            int rf = cluster.size() - 1;
+            assert rf == 2;
+            cluster.schemaChange("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': " + (cluster.size() - 1) + "};");
+            cluster.schemaChange("CREATE TABLE ks.tbl (pk int, v int, PRIMARY KEY (pk)) WITH COMPACT STORAGE");
+            ICoordinator coordinator = cluster.coordinator(1);
+            // these shouldn't be replicated by the 3rd node
+            coordinator.execute("INSERT INTO ks.tbl (pk, v) VALUES (3, 3)", ConsistencyLevel.ALL);
+            coordinator.execute("INSERT INTO ks.tbl (pk, v) VALUES (9, 9)", ConsistencyLevel.ALL);
+            for (int i = 0; i < cluster.size(); i++)
+            {
+                int nodeNum = i + 1;
+                System.out.printf("****** node %s: %s%n", nodeNum, cluster.get(nodeNum).config());
+            }
+        })
+        .runAfterNodeUpgrade(((cluster, node) -> {
+
+            if (node < 2)
+                return;
+
+            Object[][] rows = cluster.coordinator(3).execute("SELECT * FROM ks.tbl LIMIT 2", ConsistencyLevel.ALL);
+            Object[][] expected = {
+            row(9, 9),
+            row(3, 3)
+            };
+            assertRows(rows, expected);
+        })).run();
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageBeforeUpgradeSSTablesTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageBeforeUpgradeSSTablesTest.java
new file mode 100644
index 0000000..2fd9489
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageBeforeUpgradeSSTablesTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.distributed.upgrade;
+
+import com.vdurmont.semver4j.Semver;
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+
+import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
+import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
+import static org.apache.cassandra.distributed.api.Feature.NETWORK;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+
+public class DropCompactStorageBeforeUpgradeSSTablesTest extends DropCompactStorageTester
+{
+    @Test
+    public void dropCompactStorageBeforeUpgradesstablesTo3X() throws Throwable
+    {
+        dropCompactStorageBeforeUpgradeSstables(v3X);
+    }
+
+    /**
+     * Upgrades a node from 2.2 to 3.x and DROP COMPACT just after the upgrade but _before_ upgrading the underlying
+     * sstables.
+     *
+     * <p>This test reproduces the issue from CASSANDRA-15897.
+     */
+    public void dropCompactStorageBeforeUpgradeSstables(Semver upgradeTo) throws Throwable
+    {
+        new TestCase()
+        .nodes(1)
+        .singleUpgrade(v22, upgradeTo)
+        .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL).set("enable_drop_compact_storage", true))
+        .setup((cluster) -> {
+            cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (id int, ck int, v int, PRIMARY KEY (id, ck)) WITH COMPACT STORAGE");
+            for (int i = 0; i < 5; i++)
+                cluster.coordinator(1).execute("INSERT INTO "+KEYSPACE+".tbl (id, ck, v) values (1, ?, ?)", ConsistencyLevel.ALL, i, i);
+            cluster.get(1).flush(KEYSPACE);
+        })
+        .runAfterNodeUpgrade((cluster, node) -> {
+            Throwable thrown = catchThrowable(() -> cluster.schemaChange("ALTER TABLE "+KEYSPACE+".tbl DROP COMPACT STORAGE"));
+            assertThat(thrown).hasMessageContainingAll("Cannot DROP COMPACT STORAGE as some nodes in the cluster",
+                                                       "has some non-upgraded 2.x sstables");
+
+            assertThat(cluster.get(1).nodetool("upgradesstables")).isEqualTo(0);
+            Thread.sleep(1000);
+            cluster.schemaChange("ALTER TABLE "+KEYSPACE+".tbl DROP COMPACT STORAGE");
+            cluster.coordinator(1).execute("SELECT * FROM "+KEYSPACE+".tbl", ConsistencyLevel.ALL);
+        })
+        .run();
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageTest.java
deleted file mode 100644
index 920458a..0000000
--- a/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.distributed.upgrade;
-
-import com.vdurmont.semver4j.Semver;
-import org.junit.Test;
-
-import org.apache.cassandra.distributed.api.ConsistencyLevel;
-import org.apache.cassandra.distributed.shared.Versions;
-
-import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
-import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
-import static org.apache.cassandra.distributed.api.Feature.NETWORK;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.catchThrowable;
-
-public class DropCompactStorageTest extends UpgradeTestBase
-{
-    @Test
-    public void dropCompactStorageBeforeUpgradesstablesTo30() throws Throwable
-    {
-        dropCompactStorageBeforeUpgradeSstables(v30);
-    }
-
-    /**
-     * Upgrades a node from 2.2 to 3.x and DROP COMPACT just after the upgrade but _before_ upgrading the underlying
-     * sstables.
-     *
-     * <p>This test reproduces the issue from CASSANDRA-15897.
-     */
-    public void dropCompactStorageBeforeUpgradeSstables(Semver upgradeTo) throws Throwable
-    {
-        new TestCase()
-        .nodes(1)
-        .singleUpgrade(v22, upgradeTo)
-        .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL).set("enable_drop_compact_storage", true))
-        .setup((cluster) -> {
-            cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (id int, ck int, v int, PRIMARY KEY (id, ck)) WITH COMPACT STORAGE");
-            for (int i = 0; i < 5; i++)
-                cluster.coordinator(1).execute("INSERT INTO "+KEYSPACE+".tbl (id, ck, v) values (1, ?, ?)", ConsistencyLevel.ALL, i, i);
-            cluster.get(1).flush(KEYSPACE);
-        })
-        .runAfterNodeUpgrade((cluster, node) -> {
-            Throwable thrown = catchThrowable(() -> cluster.schemaChange("ALTER TABLE "+KEYSPACE+".tbl DROP COMPACT STORAGE"));
-            assertThat(thrown).hasMessageContainingAll("Cannot DROP COMPACT STORAGE as some nodes in the cluster",
-                                                       "has some non-upgraded 2.x sstables");
-
-            assertThat(cluster.get(1).nodetool("upgradesstables")).isEqualTo(0);
-            cluster.schemaChange("ALTER TABLE "+KEYSPACE+".tbl DROP COMPACT STORAGE");
-            cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + ".tbl", ConsistencyLevel.ALL);
-        })
-        .run();
-    }
-}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageTester.java b/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageTester.java
new file mode 100644
index 0000000..bd9f4d6
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageTester.java
@@ -0,0 +1,66 @@
+/*
+ * 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.distributed.upgrade;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cassandra.distributed.UpgradeableCluster;
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.distributed.api.ICoordinator;
+
+import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
+
+public abstract class DropCompactStorageTester extends UpgradeTestBase
+{
+    protected void runQueries(ICoordinator coordinator, ResultsRecorder helper, String[] queries)
+    {
+        for (String query : queries)
+            helper.addResult(query, coordinator.execute(query, ConsistencyLevel.ALL));
+    }
+
+    public static class ResultsRecorder
+    {
+        final private Map<String, Object[][]> preUpgradeResults = new HashMap<>();
+
+        public void addResult(String query, Object[][] results)
+        {
+            preUpgradeResults.put(query, results);
+        }
+
+        public Map<String, Object[][]> queriesAndResults()
+        {
+            return preUpgradeResults;
+        }
+
+        public void validateResults(UpgradeableCluster cluster, int node)
+        {
+            validateResults(cluster, node, ConsistencyLevel.ALL);
+        }
+
+        public void validateResults(UpgradeableCluster cluster, int node, ConsistencyLevel cl)
+        {
+            for (Map.Entry<String, Object[][]> entry : queriesAndResults().entrySet())
+            {
+                Object[][] postUpgradeResult = cluster.coordinator(node).execute(entry.getKey(), cl);
+                assertRows(postUpgradeResult, entry.getValue());
+            }
+        }
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageWithClusteringAndValueColumnTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageWithClusteringAndValueColumnTest.java
new file mode 100644
index 0000000..53042d0
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageWithClusteringAndValueColumnTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.distributed.upgrade;
+
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.distributed.api.ICoordinator;
+import org.apache.cassandra.distributed.api.IMessageFilters;
+import org.apache.cassandra.distributed.api.NodeToolResult;
+
+import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
+import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
+import static org.apache.cassandra.distributed.api.Feature.NETWORK;
+import static org.junit.Assert.assertEquals;
+
+
+public class DropCompactStorageWithClusteringAndValueColumnTest extends DropCompactStorageTester
+{
+    @Test
+    public void testDropCompactWithClusteringAndValueColumn() throws Throwable
+    {
+        final String table = "clustering_and_value";
+        final int partitions = 10;
+        final int rowsPerPartition = 10;
+
+        final ResultsRecorder recorder = new ResultsRecorder();
+        new TestCase()
+        .nodes(2)
+        .upgradesFrom(v22)
+        .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL))
+        .setup(cluster -> {
+            cluster.schemaChange(String.format(
+            "CREATE TABLE %s.%s (key int, c1 int, c2 int, c3 int, PRIMARY KEY (key, c1, c2)) WITH COMPACT STORAGE",
+            KEYSPACE, table));
+            ICoordinator coordinator = cluster.coordinator(1);
+
+            for (int i = 1; i <= partitions; i++)
+            {
+                for (int j = 1; j <= rowsPerPartition; j++)
+                {
+                    coordinator.execute(String.format("INSERT INTO %s.%s (key, c1, c2, c3) VALUES (%d, %d, 1, 1)",
+                                                      KEYSPACE, table, i, j), ConsistencyLevel.ALL);
+                    coordinator.execute(String.format("INSERT INTO %s.%s (key, c1, c2, c3) VALUES (%d, %d, 2, 1)",
+                                                      KEYSPACE, table, i, j), ConsistencyLevel.ALL);
+                }
+            }
+
+            runQueries(cluster.coordinator(1), recorder, new String[]{
+            String.format("SELECT * FROM %s.%s", KEYSPACE, table),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d",
+                          KEYSPACE, table, partitions - 3, rowsPerPartition - 2),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d",
+                          KEYSPACE, table, partitions - 1, rowsPerPartition - 5),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d and c2 = %d",
+                          KEYSPACE, table, partitions - 1, rowsPerPartition - 5, 1),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d and c2 = %d",
+                          KEYSPACE, table, partitions - 4, rowsPerPartition - 9, 1),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d and c2 > %d",
+                          KEYSPACE, table, partitions - 4, rowsPerPartition - 9, 1),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d and c2 > %d",
+                          KEYSPACE, table, partitions - 4, rowsPerPartition - 9, 2),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 > %d",
+                          KEYSPACE, table, partitions - 8, rowsPerPartition - 3),
+            });
+        }).runBeforeNodeRestart((cluster, node) -> {
+            cluster.get(node).config().set("enable_drop_compact_storage", true);
+        }).runAfterClusterUpgrade(cluster -> {
+            for (int i = 1; i <= cluster.size(); i++)
+            {
+                NodeToolResult result = cluster.get(i).nodetoolResult("upgradesstables");
+                assertEquals("upgrade sstables failed for node " + i, 0, result.getRc());
+            }
+            Thread.sleep(1000);
+
+            // make sure the results are the same after upgrade and upgrade sstables but before dropping compact storage
+            recorder.validateResults(cluster, 1);
+            recorder.validateResults(cluster, 2);
+
+            // make sure the results are the same after dropping compact storage on only the first node
+            IMessageFilters.Filter filter = cluster.verbs().allVerbs().to(2).drop();
+            cluster.schemaChange(String.format("ALTER TABLE %s.%s DROP COMPACT STORAGE", KEYSPACE, table), 1);
+
+            recorder.validateResults(cluster, 1, ConsistencyLevel.ONE);
+
+            filter.off();
+            recorder.validateResults(cluster, 1);
+            recorder.validateResults(cluster, 2);
+
+            // make sure the results continue to be the same after dropping compact storage on the second node
+            cluster.schemaChange(String.format("ALTER TABLE %s.%s DROP COMPACT STORAGE", KEYSPACE, table), 2);
+            recorder.validateResults(cluster, 1);
+            recorder.validateResults(cluster, 2);
+        })
+        .run();
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageWithDeletesAndWritesTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageWithDeletesAndWritesTest.java
new file mode 100644
index 0000000..9d9e278
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageWithDeletesAndWritesTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.distributed.upgrade;
+
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.distributed.api.ICoordinator;
+import org.apache.cassandra.distributed.api.IMessageFilters;
+
+import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
+import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
+import static org.apache.cassandra.distributed.api.Feature.NETWORK;
+
+
+public class DropCompactStorageWithDeletesAndWritesTest extends DropCompactStorageTester
+{
+    @Test
+    public void testDropCompactWithClusteringAndValueColumnWithDeletesAndWrites() throws Throwable
+    {
+        final String table = "clustering_and_value_with_deletes";
+        final int partitions = 10;
+        final int rowsPerPartition = 10;
+        final int additionalParititons = 5;
+
+        new TestCase()
+        .nodes(2)
+        .upgradesFrom(v22)
+        .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL).set("enable_drop_compact_storage", true))
+        .setup(cluster -> {
+            cluster.schemaChange(String.format(
+            "CREATE TABLE %s.%s (key int, c1 int, c2 int, c3 int, PRIMARY KEY (key, c1, c2)) WITH COMPACT STORAGE",
+            KEYSPACE, table));
+            ICoordinator coordinator = cluster.coordinator(1);
+
+            for (int i = 1; i <= partitions; i++)
+            {
+                for (int j = 1; j <= rowsPerPartition; j++)
+                {
+                    coordinator.execute(String.format("INSERT INTO %s.%s (key, c1, c2, c3) VALUES (%d, %d, 1, 1)",
+                                                      KEYSPACE, table, i, j), ConsistencyLevel.ALL);
+                    coordinator.execute(String.format("INSERT INTO %s.%s (key, c1, c2, c3) VALUES (%d, %d, 2, 2)",
+                                                      KEYSPACE, table, i, j), ConsistencyLevel.ALL);
+                    coordinator.execute(String.format("INSERT INTO %s.%s (key, c1, c2, c3) VALUES (%d, %d, 3, 3)",
+                                                      KEYSPACE, table, i, j), ConsistencyLevel.ALL);
+                }
+            }
+
+        })
+        .runAfterClusterUpgrade(cluster -> {
+            cluster.forEach(n -> n.nodetoolResult("upgradesstables", KEYSPACE).asserts().success());
+            Thread.sleep(1000);
+
+            // drop compact storage on only one node before performing writes
+            IMessageFilters.Filter filter = cluster.verbs().allVerbs().to(2).drop();
+            cluster.schemaChange(String.format("ALTER TABLE %s.%s DROP COMPACT STORAGE", KEYSPACE, table), 1);
+            filter.off();
+
+            // add new partitions and delete some of the old ones
+            ICoordinator coordinator = cluster.coordinator(1);
+            for (int i = 0; i < additionalParititons; i++)
+            {
+                for (int j = 1; j <= rowsPerPartition; j++)
+                {
+                    coordinator.execute(String.format("INSERT INTO %s.%s (key, c1, c2, c3) VALUES (%d, %d, 1, 1)",
+                                                      KEYSPACE, table, i, j), ConsistencyLevel.ALL);
+                }
+            }
+
+            coordinator.execute(String.format("DELETE FROM %s.%s WHERE key = %d and c1 = %d",
+                                              KEYSPACE, table, 0, 3), ConsistencyLevel.ALL);
+
+            coordinator.execute(String.format("DELETE FROM %s.%s WHERE key = %d",
+                                              KEYSPACE, table, 1), ConsistencyLevel.ALL);
+
+            coordinator.execute(String.format("DELETE FROM %s.%s WHERE key = %d and c1 = %d and c2 = %d",
+                                              KEYSPACE, table, 7, 2, 2), ConsistencyLevel.ALL);
+
+            coordinator.execute(String.format("DELETE FROM %s.%s WHERE key = %d and c1 = %d and c2 = %d",
+                                              KEYSPACE, table, 7, 6, 1), ConsistencyLevel.ALL);
+
+            coordinator.execute(String.format("DELETE FROM %s.%s WHERE key = %d and c1 = %d and c2 = %d",
+                                              KEYSPACE, table, 4, 1, 1), ConsistencyLevel.ALL);
+
+            coordinator.execute(String.format("DELETE c3 FROM %s.%s WHERE key = %d and c1 = %d and c2 = %d",
+                                              KEYSPACE, table, 8, 1, 3), ConsistencyLevel.ALL);
+
+            coordinator.execute(String.format("DELETE FROM %s.%s WHERE key = %d and c1 = %d and c2 > 1",
+                                              KEYSPACE, table, 6, 2), ConsistencyLevel.ALL);
+
+            ResultsRecorder recorder = new ResultsRecorder();
+            runQueries(coordinator, recorder, new String[] {
+            String.format("SELECT * FROM %s.%s", KEYSPACE, table),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d",
+                          KEYSPACE, table, partitions - 3, rowsPerPartition - 2),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d",
+                          KEYSPACE, table, partitions - 1, rowsPerPartition - 5),
+
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 > %d",
+                          KEYSPACE, table, partitions - 8, rowsPerPartition - 3),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d",
+                          KEYSPACE, table, 7),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d",
+                          KEYSPACE, table, 7, 2),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 = %d",
+                          KEYSPACE, table, 8, 1),
+
+            String.format("SELECT c1, c2 FROM %s.%s WHERE key = %d and c1 = %d",
+                          KEYSPACE, table, 8, 1),
+
+            String.format("SELECT c1, c2 FROM %s.%s WHERE key = %d and c1 = %d",
+                          KEYSPACE, table, 8, 1),
+
+            String.format("SELECT c1, c2 FROM %s.%s WHERE key = %d and c1 = %d",
+                          KEYSPACE, table, 4, 1),
+
+            String.format("SELECT c1, c2 FROM %s.%s WHERE key = %d",
+                          KEYSPACE, table, 6),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 > %d",
+                          KEYSPACE, table, 0, 1),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d",
+                          KEYSPACE, table, partitions - (additionalParititons - 2)),
+
+            String.format("SELECT * FROM %s.%s WHERE key = %d and c1 > %d",
+                          KEYSPACE, table, partitions - (additionalParititons - 3), 4)
+
+            });
+
+            // drop compact storage on remaining node and check result
+            cluster.schemaChange(String.format("ALTER TABLE %s.%s DROP COMPACT STORAGE", KEYSPACE, table), 2);
+            recorder.validateResults(cluster, 1);
+            recorder.validateResults(cluster, 2);
+        }).run();
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumns.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumns.java
new file mode 100644
index 0000000..4c94433
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumns.java
@@ -0,0 +1,135 @@
+/*
+ * 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.distributed.upgrade;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Objects;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.vdurmont.semver4j.Semver;
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.db.marshal.CompositeType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.MapType;
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.distributed.api.Feature;
+import org.apache.cassandra.distributed.api.ICoordinator;
+import org.apache.cassandra.distributed.api.QueryResults;
+import org.apache.cassandra.distributed.api.SimpleQueryResult;
+import org.apache.cassandra.distributed.shared.AssertUtils;
+import org.apache.cassandra.distributed.shared.Versions;
+import org.apache.cassandra.distributed.test.ThriftClientUtils;
+import org.apache.cassandra.thrift.Deletion;
+import org.apache.cassandra.thrift.Mutation;
+import org.apache.cassandra.thrift.SlicePredicate;
+import org.apache.cassandra.thrift.SliceRange;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+public abstract class MigrateDropColumns extends UpgradeTestBase
+{
+    private static final MapType MAP_TYPE = MapType.getInstance(Int32Type.instance, Int32Type.instance, true);
+
+    private final Semver initial;
+    private final Semver[] upgrades;
+
+    protected MigrateDropColumns(Semver initial, Semver... upgrade)
+    {
+        this.initial = Objects.requireNonNull(initial, "initial");
+        this.upgrades = Objects.requireNonNull(upgrade, "upgrade");
+    }
+
+    @Test
+    public void dropColumns() throws Throwable
+    {
+        TestCase testcase = new TestCase();
+				for (Semver upgrade : upgrades)
+            testcase = testcase.singleUpgrade(initial, upgrade);
+        
+				testcase
+			    .withConfig(c -> c.with(Feature.NATIVE_PROTOCOL))
+          .setup(cluster -> {
+            cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl(pk int, tables map<int, int>, PRIMARY KEY (pk))"));
+
+            ICoordinator coordinator = cluster.coordinator(1);
+
+            // write a RT to pk=0
+            ThriftClientUtils.thriftClient(cluster.get(1), thrift -> {
+                thrift.set_keyspace(KEYSPACE);
+
+                Mutation mutation = new Mutation();
+                Deletion deletion = new Deletion();
+                SlicePredicate slice = new SlicePredicate();
+                SliceRange range = new SliceRange();
+                range.setStart(CompositeType.build(ByteBufferUtil.bytes("tables")));
+                range.setFinish(CompositeType.build(ByteBufferUtil.bytes("tables")));
+                slice.setSlice_range(range);
+                deletion.setPredicate(slice);
+                deletion.setTimestamp(System.currentTimeMillis());
+                mutation.setDeletion(deletion);
+
+                thrift.batch_mutate(Collections.singletonMap(ByteBufferUtil.bytes(0),
+                                                             Collections.singletonMap("tbl", Arrays.asList(mutation))),
+                                    org.apache.cassandra.thrift.ConsistencyLevel.ALL);
+            });
+
+            // write table to pk=1
+            // NOTE: because jvm-dtest doesn't support collections in the execute interface (see CASSANDRA-15969)
+            // need to encode to a ByteBuffer first
+            coordinator.execute(withKeyspace("INSERT INTO %s.tbl (pk, tables) VALUES (?, ?)"), ConsistencyLevel.ONE, 1, MAP_TYPE.decompose(ImmutableMap.of(1, 1)));
+
+            cluster.forEach(inst -> inst.flush(KEYSPACE));
+
+            cluster.schemaChange(withKeyspace("ALTER TABLE %s.tbl DROP tables"));
+        })
+        .runAfterClusterUpgrade(cluster -> {
+            ICoordinator coordinator = cluster.coordinator(1);
+            SimpleQueryResult qr = coordinator.executeWithResult("SELECT column_name " +
+                                                                 "FROM system_schema.dropped_columns " +
+                                                                 "WHERE keyspace_name=?" +
+                                                                 " AND table_name=?;",
+                                                                 ConsistencyLevel.ALL, KEYSPACE, "tbl");
+            Assert.assertEquals(ImmutableSet.of("tables"), Sets.newHashSet(qr.map(r -> r.getString("column_name"))));
+
+            assertRows(coordinator);
+
+            // upgradesstables, make sure everything is still working
+            cluster.forEach(n -> n.nodetoolResult("upgradesstables", KEYSPACE).asserts().success());
+
+            assertRows(coordinator);
+        })
+        .run();
+    }
+
+    private static void assertRows(ICoordinator coordinator)
+    {
+        // since only a RT was written to this row there is no liveness information, so the row will be skipped
+        AssertUtils.assertRows(
+        coordinator.executeWithResult(withKeyspace("SELECT * FROM %s.tbl WHERE pk=?"), ConsistencyLevel.ALL, 0),
+        QueryResults.empty());
+
+        AssertUtils.assertRows(
+        coordinator.executeWithResult(withKeyspace("SELECT * FROM %s.tbl WHERE pk=?"), ConsistencyLevel.ALL, 1),
+        QueryResults.builder().row(1).build());
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumns22To30To311Test.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumns22To30To311Test.java
new file mode 100644
index 0000000..2407dc5
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumns22To30To311Test.java
@@ -0,0 +1,29 @@
+/*
+ * 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.distributed.upgrade;
+
+import org.apache.cassandra.distributed.shared.Versions;
+
+public class MigrateDropColumns22To30To311Test extends MigrateDropColumns
+{
+    public MigrateDropColumns22To30To311Test()
+    {
+        super(v22, v30, v3X);
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumns22To311Test.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumns22To311Test.java
new file mode 100644
index 0000000..1235907
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumns22To311Test.java
@@ -0,0 +1,29 @@
+/*
+ * 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.distributed.upgrade;
+
+import org.apache.cassandra.distributed.shared.Versions;
+
+public class MigrateDropColumns22To311Test extends MigrateDropColumns
+{
+    public MigrateDropColumns22To311Test()
+    {
+        super(v22, v3X);
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumns30To311Test.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumns30To311Test.java
new file mode 100644
index 0000000..4a19698
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumns30To311Test.java
@@ -0,0 +1,29 @@
+/*
+ * 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.distributed.upgrade;
+
+import org.apache.cassandra.distributed.shared.Versions;
+
+public class MigrateDropColumns30To311Test extends MigrateDropColumns
+{
+    public MigrateDropColumns30To311Test()
+    {
+        super(v30, v3X);
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumnsTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumnsTest.java
deleted file mode 100644
index 43027a4..0000000
--- a/test/distributed/org/apache/cassandra/distributed/upgrade/MigrateDropColumnsTest.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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.distributed.upgrade;
-
-import java.util.Arrays;
-import java.util.Collections;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import com.vdurmont.semver4j.Semver;
-import org.junit.Assert;
-import org.junit.Test;
-
-import org.apache.cassandra.db.marshal.CompositeType;
-import org.apache.cassandra.db.marshal.Int32Type;
-import org.apache.cassandra.db.marshal.MapType;
-import org.apache.cassandra.distributed.api.ConsistencyLevel;
-import org.apache.cassandra.distributed.api.Feature;
-import org.apache.cassandra.distributed.api.ICoordinator;
-import org.apache.cassandra.distributed.api.QueryResults;
-import org.apache.cassandra.distributed.api.SimpleQueryResult;
-import org.apache.cassandra.distributed.shared.AssertUtils;
-import org.apache.cassandra.distributed.shared.Versions;
-import org.apache.cassandra.distributed.test.ThriftClientUtils;
-import org.apache.cassandra.thrift.Deletion;
-import org.apache.cassandra.thrift.Mutation;
-import org.apache.cassandra.thrift.SlicePredicate;
-import org.apache.cassandra.thrift.SliceRange;
-import org.apache.cassandra.utils.ByteBufferUtil;
-
-public class MigrateDropColumnsTest extends UpgradeTestBase
-{
-    private static final MapType MAP_TYPE = MapType.getInstance(Int32Type.instance, Int32Type.instance, true);
-
-    @Test
-    public void dropColumns() throws Throwable
-    {
-        new TestCase()
-        .upgradesFrom(v22)
-//        .upgrade(Versions.Major.v22, Versions.Major.v3X)
-//        .upgrade(Versions.Major.v30, Versions.Major.v3X)
-//        .upgrade(Versions.Major.v22, Versions.Major.v30, Versions.Major.v3X)
-        .withConfig(c -> c.with(Feature.NATIVE_PROTOCOL))
-        .setup(cluster -> {
-            cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl(pk int, tables map<int, int>, PRIMARY KEY (pk))"));
-
-            ICoordinator coordinator = cluster.coordinator(1);
-
-            // write a RT to pk=0
-            ThriftClientUtils.thriftClient(cluster.get(1), thrift -> {
-                thrift.set_keyspace(KEYSPACE);
-
-                Mutation mutation = new Mutation();
-                Deletion deletion = new Deletion();
-                SlicePredicate slice = new SlicePredicate();
-                SliceRange range = new SliceRange();
-                range.setStart(CompositeType.build(ByteBufferUtil.bytes("tables")));
-                range.setFinish(CompositeType.build(ByteBufferUtil.bytes("tables")));
-                slice.setSlice_range(range);
-                deletion.setPredicate(slice);
-                deletion.setTimestamp(System.currentTimeMillis());
-                mutation.setDeletion(deletion);
-
-                thrift.batch_mutate(Collections.singletonMap(ByteBufferUtil.bytes(0),
-                                                             Collections.singletonMap("tbl", Arrays.asList(mutation))),
-                                    org.apache.cassandra.thrift.ConsistencyLevel.ALL);
-            });
-
-            // write table to pk=1
-            // NOTE: because jvm-dtest doesn't support collections in the execute interface (see CASSANDRA-15969)
-            // need to encode to a ByteBuffer first
-            coordinator.execute(withKeyspace("INSERT INTO %s.tbl (pk, tables) VALUES (?, ?)"), ConsistencyLevel.ONE, 1, MAP_TYPE.decompose(ImmutableMap.of(1, 1)));
-
-            cluster.forEach(inst -> inst.flush(KEYSPACE));
-
-            cluster.schemaChange(withKeyspace("ALTER TABLE %s.tbl DROP tables"));
-        })
-        .runAfterClusterUpgrade(cluster -> {
-            ICoordinator coordinator = cluster.coordinator(1);
-            SimpleQueryResult qr = coordinator.executeWithResult("SELECT column_name " +
-                                                                 "FROM system_schema.dropped_columns " +
-                                                                 "WHERE keyspace_name=?" +
-                                                                 " AND table_name=?;",
-                                                                 ConsistencyLevel.ALL, KEYSPACE, "tbl");
-            Assert.assertEquals(ImmutableSet.of("tables"), Sets.newHashSet(qr.map(r -> r.getString("column_name"))));
-
-            assertRows(coordinator);
-
-            // upgradesstables, make sure everything is still working
-            cluster.forEach(n -> n.nodetoolResult("upgradesstables", KEYSPACE).asserts().success());
-
-            assertRows(coordinator);
-        })
-        .run();
-    }
-
-    private static void assertRows(ICoordinator coordinator)
-    {
-        // since only a RT was written to this row there is no liveness information, so the row will be skipped
-        AssertUtils.assertRows(
-        coordinator.executeWithResult(withKeyspace("SELECT * FROM %s.tbl WHERE pk=?"), ConsistencyLevel.ALL, 0),
-        QueryResults.empty());
-
-        AssertUtils.assertRows(
-        coordinator.executeWithResult(withKeyspace("SELECT * FROM %s.tbl WHERE pk=?"), ConsistencyLevel.ALL, 1),
-        QueryResults.builder().row(1).build());
-    }
-}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeReadTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeReadTest.java
new file mode 100644
index 0000000..756f894
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeReadTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.distributed.upgrade;
+
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.api.Feature;
+import org.apache.cassandra.distributed.api.IInvokableInstance;
+import org.apache.cassandra.distributed.shared.Versions;
+import org.apache.cassandra.gms.Gossiper;
+
+import static org.apache.cassandra.distributed.test.ReadDigestConsistencyTest.CREATE_TABLE;
+import static org.apache.cassandra.distributed.test.ReadDigestConsistencyTest.insertData;
+import static org.apache.cassandra.distributed.test.ReadDigestConsistencyTest.testDigestConsistency;
+
+public class MixedModeReadTest extends UpgradeTestBase
+{
+    @Test
+    public void mixedModeReadColumnSubsetDigestCheck() throws Throwable
+    {
+        new TestCase()
+        .nodes(2)
+        .nodesToUpgrade(1)
+        .singleUpgrade(v30, v3X)
+        .withConfig(config -> config.with(Feature.GOSSIP, Feature.NETWORK))
+        .setup(cluster -> {
+            cluster.schemaChange(CREATE_TABLE);
+            insertData(cluster.coordinator(1));
+            testDigestConsistency(cluster.coordinator(1));
+            testDigestConsistency(cluster.coordinator(2));
+        })
+        .runAfterClusterUpgrade(cluster -> {
+            // we need to let gossip settle or the test will fail
+            int attempts = 1;
+            //noinspection Convert2MethodRef
+            while (!((IInvokableInstance) (cluster.get(1))).callOnInstance(() -> Gossiper.instance.isAnyNodeOn30()))
+            {
+                if (attempts++ > 30)
+                    throw new RuntimeException("Gossiper.instance.isAnyNodeOn30() continually returns false despite expecting to be true");
+                Thread.sleep(1000);
+            }
+
+            // should not cause a disgest mismatch in mixed mode
+            testDigestConsistency(cluster.coordinator(1));
+            testDigestConsistency(cluster.coordinator(2));
+        })
+        .run();
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/UpgradeTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/UpgradeTest.java
index 943e305..0932eb1 100644
--- a/test/distributed/org/apache/cassandra/distributed/upgrade/UpgradeTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/UpgradeTest.java
@@ -87,4 +87,5 @@
             }
         }).run();
     }
+
 }
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/UpgradeTestBase.java b/test/distributed/org/apache/cassandra/distributed/upgrade/UpgradeTestBase.java
index db34c61..6aa6f61 100644
--- a/test/distributed/org/apache/cassandra/distributed/upgrade/UpgradeTestBase.java
+++ b/test/distributed/org/apache/cassandra/distributed/upgrade/UpgradeTestBase.java
@@ -35,6 +35,7 @@
 import org.apache.cassandra.distributed.UpgradeableCluster;
 import org.apache.cassandra.distributed.api.ICluster;
 import org.apache.cassandra.distributed.api.IInstanceConfig;
+import org.apache.cassandra.distributed.api.IUpgradeableInstance;
 import org.apache.cassandra.distributed.impl.Instance;
 import org.apache.cassandra.distributed.shared.DistributedTestBase;
 import org.apache.cassandra.distributed.shared.Versions;
@@ -76,11 +77,14 @@
         public void run(UpgradeableCluster cluster, int node) throws Throwable;
     }
 
-    public static final Semver v22 = new Semver("2.2", SemverType.LOOSE);
-    public static final Semver v30 = new Semver("3.0", SemverType.LOOSE);
+    public static final Semver v22 = new Semver("2.2.0-beta1", SemverType.LOOSE);
+    public static final Semver v30 = new Semver("3.0.0-alpha1", SemverType.LOOSE);
+    public static final Semver v3X = new Semver("3.11.0", SemverType.LOOSE);
 
     protected static final List<Pair<Semver,Semver>> SUPPORTED_UPGRADE_PATHS = ImmutableList.of(
-        Pair.create(v22, v30));
+        Pair.create(v22, v30),
+        Pair.create(v22, v3X),
+        Pair.create(v30, v3X));
 
     // the last is always the current
     public static final Semver CURRENT = SUPPORTED_UPGRADE_PATHS.get(SUPPORTED_UPGRADE_PATHS.size() - 1).right;
diff --git a/test/distributed/org/apache/cassandra/io/sstable/format/ForwardingSSTableReader.java b/test/distributed/org/apache/cassandra/io/sstable/format/ForwardingSSTableReader.java
index bd92ca0..ddf9487 100644
--- a/test/distributed/org/apache/cassandra/io/sstable/format/ForwardingSSTableReader.java
+++ b/test/distributed/org/apache/cassandra/io/sstable/format/ForwardingSSTableReader.java
@@ -35,8 +35,10 @@
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.PartitionPosition;
 import org.apache.cassandra.db.RowIndexEntry;
+import org.apache.cassandra.db.Slices;
 import org.apache.cassandra.db.filter.ColumnFilter;
-import org.apache.cassandra.db.rows.SliceableUnfilteredRowIterator;
+import org.apache.cassandra.db.rows.EncodingStats;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
 import org.apache.cassandra.dht.AbstractBounds;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.Range;
@@ -48,8 +50,8 @@
 import org.apache.cassandra.io.sstable.metadata.StatsMetadata;
 import org.apache.cassandra.io.util.ChannelProxy;
 import org.apache.cassandra.io.util.FileDataInput;
+import org.apache.cassandra.io.util.FileHandle;
 import org.apache.cassandra.io.util.RandomAccessReader;
-import org.apache.cassandra.io.util.SegmentedFile;
 import org.apache.cassandra.metrics.RestorableMeter;
 import org.apache.cassandra.utils.EstimatedHistogram;
 import org.apache.cassandra.utils.IFilter;
@@ -61,7 +63,9 @@
     // This method is only accessiable via extension and not for calling directly;
     // to work around this, rely on reflection if the method gets called
     private static final Method ESTIMATE_ROWS_FROM_INDEX;
-    static {
+
+    static
+    {
         try
         {
             Method m = SSTable.class.getDeclaredMethod("estimateRowsFromIndex", RandomAccessReader.class);
@@ -117,15 +121,15 @@
     }
 
     @Override
-    public boolean loadSummary(SegmentedFile.Builder ibuilder, SegmentedFile.Builder dbuilder)
+    public boolean loadSummary()
     {
-        return delegate.loadSummary(ibuilder, dbuilder);
+        return delegate.loadSummary();
     }
 
     @Override
-    public void saveSummary(SegmentedFile.Builder ibuilder, SegmentedFile.Builder dbuilder)
+    public void saveSummary()
     {
-        delegate.saveSummary(ibuilder, dbuilder);
+        delegate.saveSummary();
     }
 
     @Override
@@ -321,15 +325,21 @@
     }
 
     @Override
-    public SliceableUnfilteredRowIterator iterator(DecoratedKey key, ColumnFilter selectedColumns, boolean reversed, boolean isForThrift, SSTableReadsListener listener)
+    public UnfilteredRowIterator iterator(DecoratedKey key, Slices slices, ColumnFilter selectedColumns, boolean reversed, boolean isForThrift, SSTableReadsListener listener)
     {
-        return delegate.iterator(key, selectedColumns, reversed, isForThrift, listener);
+        return delegate.iterator(key, slices, selectedColumns, reversed, isForThrift, listener);
     }
 
     @Override
-    public SliceableUnfilteredRowIterator iterator(FileDataInput file, DecoratedKey key, RowIndexEntry indexEntry, ColumnFilter selectedColumns, boolean reversed, boolean isForThrift)
+    public UnfilteredRowIterator iterator(FileDataInput file, DecoratedKey key, RowIndexEntry indexEntry, Slices slices, ColumnFilter selectedColumns, boolean reversed, boolean isForThrift)
     {
-        return delegate.iterator(file, key, indexEntry, selectedColumns, reversed, isForThrift);
+        return delegate.iterator(file, key, indexEntry, slices, selectedColumns, reversed, isForThrift);
+    }
+
+    @Override
+    public UnfilteredRowIterator simpleIterator(FileDataInput file, DecoratedKey key, RowIndexEntry indexEntry, boolean tombstoneOnly)
+    {
+        return delegate.simpleIterator(file, key, indexEntry, tombstoneOnly);
     }
 
     @Override
@@ -453,6 +463,12 @@
     }
 
     @Override
+    public DecoratedKey keyAt(long indexPosition) throws IOException
+    {
+        return delegate.keyAt(indexPosition);
+    }
+
+    @Override
     public long getBloomFilterFalsePositiveCount()
     {
         return delegate.getBloomFilterFalsePositiveCount();
@@ -537,9 +553,9 @@
     }
 
     @Override
-    public boolean hasTombstones()
+    public boolean mayHaveTombstones()
     {
-        return delegate.hasTombstones();
+        return delegate.mayHaveTombstones();
     }
 
     @Override
@@ -621,6 +637,12 @@
     }
 
     @Override
+    public FileHandle getIndexFile()
+    {
+        return delegate.getIndexFile();
+    }
+
+    @Override
     public long getCreationTimeFor(Component component)
     {
         return delegate.getCreationTimeFor(component);
@@ -645,9 +667,9 @@
     }
 
     @Override
-    public boolean mayOverlapsWith(SSTableReader other)
+    public EncodingStats stats()
     {
-        return delegate.mayOverlapsWith(other);
+        return delegate.stats();
     }
 
     @Override
@@ -759,7 +781,7 @@
     }
 
     @Override
-    public synchronized void addComponents(Collection<Component> newComponents)
+    public void addComponents(Collection<Component> newComponents)
     {
         delegate.addComponents(newComponents);
     }
diff --git a/test/long/org/apache/cassandra/cql3/ViewLongTest.java b/test/long/org/apache/cassandra/cql3/ViewLongTest.java
index 68931e2..ddf62e9 100644
--- a/test/long/org/apache/cassandra/cql3/ViewLongTest.java
+++ b/test/long/org/apache/cassandra/cql3/ViewLongTest.java
@@ -33,17 +33,19 @@
 import com.datastax.driver.core.Row;
 import com.datastax.driver.core.exceptions.NoHostAvailableException;
 import com.datastax.driver.core.exceptions.WriteTimeoutException;
+import org.apache.cassandra.batchlog.BatchlogManager;
+import org.apache.cassandra.concurrent.NamedThreadFactory;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.concurrent.SEPExecutor;
 import org.apache.cassandra.concurrent.Stage;
 import org.apache.cassandra.concurrent.StageManager;
 import org.apache.cassandra.db.Keyspace;
-import org.apache.cassandra.batchlog.BatchlogManager;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.WrappedRunnable;
 
 public class ViewLongTest extends CQLTester
 {
-    int protocolVersion = 4;
+    ProtocolVersion protocolVersion = ProtocolVersion.V4;
     private final List<String> views = new ArrayList<>();
 
     @BeforeClass
@@ -95,7 +97,7 @@
         for (int i = 0; i < writers; i++)
         {
             final int writer = i;
-            Thread t = new Thread(new WrappedRunnable()
+            Thread t = NamedThreadFactory.createThread(new WrappedRunnable()
             {
                 public void runMayThrow()
                 {
diff --git a/test/long/org/apache/cassandra/db/commitlog/BatchCommitLogStressTest.java b/test/long/org/apache/cassandra/db/commitlog/BatchCommitLogStressTest.java
new file mode 100644
index 0000000..3665882
--- /dev/null
+++ b/test/long/org/apache/cassandra/db/commitlog/BatchCommitLogStressTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.db.commitlog;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.security.EncryptionContext;
+
+@RunWith(Parameterized.class)
+public class BatchCommitLogStressTest extends CommitLogStressTest
+{
+    public BatchCommitLogStressTest(ParameterizedClass commitLogCompression, EncryptionContext encryptionContext)
+    {
+        super(commitLogCompression, encryptionContext);
+        DatabaseDescriptor.setCommitLogSync(Config.CommitLogSync.batch);
+    }
+}
diff --git a/test/long/org/apache/cassandra/db/commitlog/CommitLogStressTest.java b/test/long/org/apache/cassandra/db/commitlog/CommitLogStressTest.java
index 02b26c7..2162d85 100644
--- a/test/long/org/apache/cassandra/db/commitlog/CommitLogStressTest.java
+++ b/test/long/org/apache/cassandra/db/commitlog/CommitLogStressTest.java
@@ -21,47 +21,48 @@
  *
  */
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
+import java.io.*;
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.TimeUnit;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 
-import junit.framework.Assert;
-
 import com.google.common.util.concurrent.RateLimiter;
 
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Test;
+import org.junit.runners.Parameterized.Parameters;
+
+import io.netty.util.concurrent.FastThreadLocalThread;
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.UpdateBuilder;
-import org.apache.cassandra.config.Config.CommitLogSync;
-import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.config.ParameterizedClass;
-import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.*;
 import org.apache.cassandra.db.Mutation;
-import org.apache.cassandra.db.rows.Cell;
-import org.apache.cassandra.db.rows.Row;
-import org.apache.cassandra.db.rows.SerializationHelper;
-import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.io.compress.DeflateCompressor;
+import org.apache.cassandra.io.compress.LZ4Compressor;
+import org.apache.cassandra.io.compress.SnappyCompressor;
 import org.apache.cassandra.io.util.DataInputBuffer;
 import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.security.EncryptionContextGenerator;
 
-public class CommitLogStressTest
+
+@Ignore
+public abstract class CommitLogStressTest
 {
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     public static ByteBuffer dataSource;
 
     public static int NUM_THREADS = 4 * Runtime.getRuntime().availableProcessors() - 1;
@@ -83,54 +84,19 @@
         return hash;
     }
 
-    public static void main(String[] args) throws Exception
+    private boolean failed = false;
+    private volatile boolean stop = false;
+    private boolean randomSize = false;
+    private boolean discardedRun = false;
+    private CommitLogPosition discardedPos;
+
+    public CommitLogStressTest(ParameterizedClass commitLogCompression, EncryptionContext encryptionContext)
     {
-        try
-        {
-            if (args.length >= 1)
-            {
-                NUM_THREADS = Integer.parseInt(args[0]);
-                System.out.println("Setting num threads to: " + NUM_THREADS);
-            }
-
-            if (args.length >= 2)
-            {
-                numCells = Integer.parseInt(args[1]);
-                System.out.println("Setting num cells to: " + numCells);
-            }
-
-            if (args.length >= 3)
-            {
-                cellSize = Integer.parseInt(args[1]);
-                System.out.println("Setting cell size to: " + cellSize + " be aware the source corpus may be small");
-            }
-
-            if (args.length >= 4)
-            {
-                rateLimit = Integer.parseInt(args[1]);
-                System.out.println("Setting per thread rate limit to: " + rateLimit);
-            }
-            initialize();
-
-            CommitLogStressTest tester = new CommitLogStressTest();
-            tester.testFixedSize();
-        }
-        catch (Throwable e)
-        {
-            e.printStackTrace(System.err);
-        }
-        finally
-        {
-            System.exit(0);
-        }
+        DatabaseDescriptor.setCommitLogCompression(commitLogCompression);
+        DatabaseDescriptor.setEncryptionContext(encryptionContext);
+        DatabaseDescriptor.setCommitLogSegmentSize(32);
     }
 
-    boolean failed = false;
-    volatile boolean stop = false;
-    boolean randomSize = false;
-    boolean discardedRun = false;
-    ReplayPosition discardedPos;
-
     @BeforeClass
     static public void initialize() throws IOException
     {
@@ -146,10 +112,12 @@
 
         SchemaLoader.loadSchema();
         SchemaLoader.schemaDefinition(""); // leave def. blank to maintain old behaviour
+
+        CommitLog.instance.stopUnsafe(true);
     }
 
     @Before
-    public void cleanDir()
+    public void cleanDir() throws IOException
     {
         File dir = new File(location);
         if (dir.isDirectory())
@@ -166,12 +134,23 @@
         }
     }
 
+    @Parameters()
+    public static Collection<Object[]> buildParameterizedVariants()
+    {
+        return Arrays.asList(new Object[][]{
+        {null, EncryptionContextGenerator.createDisabledContext()}, // No compression, no encryption
+        {null, EncryptionContextGenerator.createContext(true)}, // Encryption
+        { new ParameterizedClass(LZ4Compressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()},
+        { new ParameterizedClass(SnappyCompressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()},
+        { new ParameterizedClass(DeflateCompressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()}});
+    }
+
     @Test
     public void testRandomSize() throws Exception
     {
         randomSize = true;
         discardedRun = false;
-        testAllLogConfigs();
+        testLog();
     }
 
     @Test
@@ -179,56 +158,46 @@
     {
         randomSize = false;
         discardedRun = false;
-
-        testAllLogConfigs();
+        testLog();
     }
 
     @Test
     public void testDiscardedRun() throws Exception
     {
-        discardedRun = true;
         randomSize = true;
-
-        testAllLogConfigs();
+        discardedRun = true;
+        testLog();
     }
 
-    public void testAllLogConfigs() throws IOException, InterruptedException
+    private void testLog() throws IOException, InterruptedException
     {
-        failed = false;
-        DatabaseDescriptor.setCommitLogSyncBatchWindow(1);
-        DatabaseDescriptor.setCommitLogSyncPeriod(30);
-        DatabaseDescriptor.setCommitLogSegmentSize(32);
-        for (ParameterizedClass compressor : new ParameterizedClass[] {
-                null,
-                new ParameterizedClass("LZ4Compressor", null),
-                new ParameterizedClass("SnappyCompressor", null),
-                new ParameterizedClass("DeflateCompressor", null) })
+        String originalDir = DatabaseDescriptor.getCommitLogLocation();
+        try
         {
-            DatabaseDescriptor.setCommitLogCompression(compressor);
-            for (CommitLogSync sync : CommitLogSync.values())
-            {
-                DatabaseDescriptor.setCommitLogSync(sync);
-                CommitLog commitLog = new CommitLog(location, CommitLogArchiver.disabled()).start();
-                testLog(commitLog);
-            }
+            DatabaseDescriptor.setCommitLogLocation(location);
+            CommitLog commitLog = new CommitLog(CommitLogArchiver.disabled()).start();
+            testLog(commitLog);
+            assert !failed;
         }
-        assert !failed;
+        finally
+        {
+            DatabaseDescriptor.setCommitLogLocation(originalDir);
+        }
     }
 
-    public void testLog(CommitLog commitLog) throws IOException, InterruptedException
-    {
-        System.out.format("\nTesting commit log size %.0fmb, compressor %s, sync %s%s%s\n",
-                          mb(DatabaseDescriptor.getCommitLogSegmentSize()),
-                          commitLog.configuration.getCompressorName(),
-                          commitLog.executor.getClass().getSimpleName(),
-                          randomSize ? " random size" : "",
-                          discardedRun ? " with discarded run" : "");
-        commitLog.allocator.enableReserveSegmentCreation();
+    private void testLog(CommitLog commitLog) throws IOException, InterruptedException {
+        System.out.format("\nTesting commit log size %.0fmb, compressor: %s, encryption enabled: %b, sync %s%s%s\n",
+                           mb(DatabaseDescriptor.getCommitLogSegmentSize()),
+                           commitLog.configuration.getCompressorName(),
+                           commitLog.configuration.useEncryption(),
+                           commitLog.executor.getClass().getSimpleName(),
+                           randomSize ? " random size" : "",
+                           discardedRun ? " with discarded run" : "");
 
-        final List<CommitlogExecutor> threads = new ArrayList<>();
+        final List<CommitlogThread> threads = new ArrayList<>();
         ScheduledExecutorService scheduled = startThreads(commitLog, threads);
 
-        discardedPos = ReplayPosition.NONE;
+        discardedPos = CommitLogPosition.NONE;
         if (discardedRun)
         {
             // Makes sure post-break data is not deleted, and that replayer correctly rejects earlier mutations.
@@ -237,17 +206,18 @@
             scheduled.shutdown();
             scheduled.awaitTermination(2, TimeUnit.SECONDS);
 
-            for (CommitlogExecutor t : threads)
+            for (CommitlogThread t: threads)
             {
                 t.join();
-                if (t.rp.compareTo(discardedPos) > 0)
-                    discardedPos = t.rp;
+                if (t.clsp.compareTo(discardedPos) > 0)
+                    discardedPos = t.clsp;
             }
             verifySizes(commitLog);
 
             commitLog.discardCompletedSegments(Schema.instance.getCFMetaData("Keyspace1", "Standard1").cfId,
-                    ReplayPosition.NONE, discardedPos);
+                    CommitLogPosition.NONE, discardedPos);
             threads.clear();
+
             System.out.format("Discarded at %s\n", discardedPos);
             verifySizes(commitLog);
 
@@ -261,7 +231,7 @@
 
         int hash = 0;
         int cells = 0;
-        for (CommitlogExecutor t : threads)
+        for (CommitlogThread t: threads)
         {
             t.join();
             hash += t.hash;
@@ -271,25 +241,30 @@
 
         commitLog.shutdownBlocking();
 
-        System.out.print("Stopped. Replaying... ");
+        System.out.println("Stopped. Replaying... ");
         System.out.flush();
-        Replayer repl = new Replayer(commitLog);
+        Reader reader = new Reader();
         File[] files = new File(location).listFiles();
-        repl.recover(files);
+
+        DummyHandler handler = new DummyHandler();
+        reader.readAllFiles(handler, files);
 
         for (File f : files)
             if (!f.delete())
                 Assert.fail("Failed to delete " + f);
 
-        if (hash == repl.hash && cells == repl.cells)
-            System.out.println("Test success.");
+        if (hash == reader.hash && cells == reader.cells)
+            System.out.format("Test success. compressor = %s, encryption enabled = %b; discarded = %d, skipped = %d\n",
+                              commitLog.configuration.getCompressorName(),
+                              commitLog.configuration.useEncryption(),
+                              reader.discarded, reader.skipped);
         else
         {
-            System.out.format("Test failed. Cells %d expected %d, hash %d expected %d.\n",
-                              repl.cells,
-                              cells,
-                              repl.hash,
-                              hash);
+            System.out.format("Test failed (compressor = %s, encryption enabled = %b). Cells %d, expected %d, diff %d; discarded = %d, skipped = %d -  hash %d expected %d.\n",
+                              commitLog.configuration.getCompressorName(),
+                              commitLog.configuration.useEncryption(),
+                              reader.cells, cells, cells - reader.cells, reader.discarded, reader.skipped,
+                              reader.hash, hash);
             failed = true;
         }
     }
@@ -297,22 +272,18 @@
     private void verifySizes(CommitLog commitLog)
     {
         // Complete anything that's still left to write.
-        commitLog.executor.requestExtraSync().awaitUninterruptibly();
-        // One await() does not suffice as we may be signalled when an ongoing sync finished. Request another
-        // (which shouldn't write anything) to make sure the first we triggered completes.
-        // FIXME: The executor should give us a chance to await completion of the sync we requested.
-        commitLog.executor.requestExtraSync().awaitUninterruptibly();
-        // Wait for any pending deletes or segment allocations to complete.
-        commitLog.allocator.awaitManagementTasksCompletion();
+        commitLog.executor.syncBlocking();
+        // Wait for any concurrent segment allocations to complete.
+        commitLog.segmentManager.awaitManagementTasksCompletion();
 
         long combinedSize = 0;
-        for (File f : new File(commitLog.location).listFiles())
+        for (File f : new File(commitLog.segmentManager.storageDirectory).listFiles())
             combinedSize += f.length();
         Assert.assertEquals(combinedSize, commitLog.getActiveOnDiskSize());
 
         List<String> logFileNames = commitLog.getActiveSegmentNames();
         Map<String, Double> ratios = commitLog.getActiveSegmentCompressionRatios();
-        Collection<CommitLogSegment> segments = commitLog.allocator.getActiveSegments();
+        Collection<CommitLogSegment> segments = commitLog.segmentManager.getActiveSegments();
 
         for (CommitLogSegment segment : segments)
         {
@@ -326,12 +297,11 @@
         Assert.assertTrue(ratios.isEmpty());
     }
 
-    public ScheduledExecutorService startThreads(final CommitLog commitLog, final List<CommitlogExecutor> threads)
+    private ScheduledExecutorService startThreads(final CommitLog commitLog, final List<CommitlogThread> threads)
     {
         stop = false;
-        for (int ii = 0; ii < NUM_THREADS; ii++)
-        {
-            final CommitlogExecutor t = new CommitlogExecutor(commitLog, new Random(ii));
+        for (int ii = 0; ii < NUM_THREADS; ii++) {
+            final CommitlogThread t = new CommitlogThread(commitLog, new Random(ii));
             threads.add(t);
             t.start();
         }
@@ -349,10 +319,10 @@
                 long freeMemory = runtime.freeMemory();
                 long temp = 0;
                 long sz = 0;
-                for (CommitlogExecutor cle : threads)
+                for (CommitlogThread clt : threads)
                 {
-                    temp += cle.counter.get();
-                    sz += cle.dataSize;
+                    temp += clt.counter.get();
+                    sz += clt.dataSize;
                 }
                 double time = (System.currentTimeMillis() - start) / 1000.0;
                 double avg = (temp / time);
@@ -386,7 +356,7 @@
         return maxMemory / (1024 * 1024);
     }
 
-    public static ByteBuffer randomBytes(int quantity, Random tlr)
+    private static ByteBuffer randomBytes(int quantity, Random tlr)
     {
         ByteBuffer slice = ByteBuffer.allocate(quantity);
         ByteBuffer source = dataSource.duplicate();
@@ -397,7 +367,7 @@
         return slice;
     }
 
-    public class CommitlogExecutor extends Thread
+    public class CommitlogThread extends FastThreadLocalThread
     {
         final AtomicLong counter = new AtomicLong();
         int hash = 0;
@@ -405,10 +375,11 @@
         int dataSize = 0;
         final CommitLog commitLog;
         final Random random;
+        final AtomicInteger threadID = new AtomicInteger(0);
 
-        volatile ReplayPosition rp;
+        volatile CommitLogPosition clsp;
 
-        public CommitlogExecutor(CommitLog commitLog, Random rand)
+        CommitlogThread(CommitLog commitLog, Random rand)
         {
             this.commitLog = commitLog;
             this.random = rand;
@@ -416,6 +387,7 @@
 
         public void run()
         {
+            Thread.currentThread().setName("CommitLogThread-" + threadID.getAndIncrement());
             RateLimiter rl = rateLimit != 0 ? RateLimiter.create(rateLimit) : null;
             final Random rand = random != null ? random : ThreadLocalRandom.current();
             while (!stop)
@@ -435,33 +407,39 @@
                     dataSize += sz;
                 }
 
-                rp = commitLog.add(new Mutation(builder.build()));
+                clsp = commitLog.add(new Mutation(builder.build()));
                 counter.incrementAndGet();
             }
         }
     }
 
-    class Replayer extends CommitLogReplayer
+    class Reader extends CommitLogReader
     {
-        Replayer(CommitLog log)
-        {
-            super(log, discardedPos, null, ReplayFilter.create());
-        }
-
-        int hash = 0;
-        int cells = 0;
+        int hash;
+        int cells;
+        int discarded;
+        int skipped;
 
         @Override
-        void replayMutation(byte[] inputBuffer, int size, final int entryLocation, final CommitLogDescriptor desc)
+        protected void readMutation(CommitLogReadHandler handler,
+                                    byte[] inputBuffer,
+                                    int size,
+                                    CommitLogPosition minPosition,
+                                    final int entryLocation,
+                                    final CommitLogDescriptor desc) throws IOException
         {
-            if (desc.id < discardedPos.segment)
+            if (desc.id < discardedPos.segmentId)
             {
                 System.out.format("Mutation from discarded segment, segment %d pos %d\n", desc.id, entryLocation);
+                discarded++;
                 return;
             }
-            else if (desc.id == discardedPos.segment && entryLocation <= discardedPos.position)
+            else if (desc.id == discardedPos.segmentId && entryLocation <= discardedPos.position)
+            {
                 // Skip over this mutation.
+                skipped++;
                 return;
+            }
 
             DataInputPlus bufIn = new DataInputBuffer(inputBuffer, 0, size);
             Mutation mutation;
@@ -497,4 +475,13 @@
             }
         }
     }
+
+    static class DummyHandler implements CommitLogReadHandler
+    {
+        public boolean shouldSkipSegmentOnError(CommitLogReadException exception) throws IOException { return false; }
+
+        public void handleUnrecoverableError(CommitLogReadException exception) throws IOException { }
+
+        public void handleMutation(Mutation m, int size, int entryLocation, CommitLogDescriptor desc) { }
+    }
 }
diff --git a/test/long/org/apache/cassandra/db/commitlog/PeriodicCommitLogStressTest.java b/test/long/org/apache/cassandra/db/commitlog/PeriodicCommitLogStressTest.java
new file mode 100644
index 0000000..509d46a
--- /dev/null
+++ b/test/long/org/apache/cassandra/db/commitlog/PeriodicCommitLogStressTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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.db.commitlog;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.security.EncryptionContext;
+
+@RunWith(Parameterized.class)
+public class PeriodicCommitLogStressTest extends CommitLogStressTest
+{
+    public PeriodicCommitLogStressTest(ParameterizedClass commitLogCompression, EncryptionContext encryptionContext)
+    {
+        super(commitLogCompression, encryptionContext);
+        DatabaseDescriptor.setCommitLogSync(Config.CommitLogSync.periodic);
+        DatabaseDescriptor.setCommitLogSyncPeriod(30);
+    }
+
+}
diff --git a/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyCQLTest.java b/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyCQLTest.java
new file mode 100644
index 0000000..9bfa380
--- /dev/null
+++ b/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyCQLTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.db.compaction;
+
+import java.util.Random;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+import org.junit.Test;
+
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.utils.Hex;
+
+public class LongLeveledCompactionStrategyCQLTest extends CQLTester
+{
+
+    @Test
+    public void stressTestCompactionStrategyManager() throws ExecutionException, InterruptedException
+    {
+        System.setProperty(Config.PROPERTY_PREFIX + "test.strict_lcs_checks", "true");
+        // flush/compact tons of sstables, invalidate token metadata in a loop to make CSM reload the strategies
+        createTable("create table %s (id int primary key, i text) with compaction = {'class':'LeveledCompactionStrategy', 'sstable_size_in_mb':1}");
+        ExecutorService es = Executors.newSingleThreadExecutor();
+        DatabaseDescriptor.setConcurrentCompactors(8);
+        AtomicBoolean stop = new AtomicBoolean(false);
+        long start = System.currentTimeMillis();
+        try
+        {
+            Random r = new Random();
+            Future<?> writes = es.submit(() -> {
+
+                byte[] b = new byte[1024];
+                while (!stop.get())
+                {
+
+                    for (int i = 0 ; i < 100; i++)
+                    {
+                        try
+                        {
+                            r.nextBytes(b);
+                            String s = Hex.bytesToHex(b);
+                            execute("insert into %s (id, i) values (?,?)", r.nextInt(), s);
+                        }
+                        catch (Throwable throwable)
+                        {
+                            throw new RuntimeException(throwable);
+                        }
+                    }
+                    getCurrentColumnFamilyStore().forceBlockingFlush();
+                    Uninterruptibles.sleepUninterruptibly(r.nextInt(200), TimeUnit.MILLISECONDS);
+                }
+            });
+
+            while(System.currentTimeMillis() - start < TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES))
+            {
+                StorageService.instance.getTokenMetadata().invalidateCachedRings();
+                Uninterruptibles.sleepUninterruptibly(r.nextInt(1000), TimeUnit.MILLISECONDS);
+            }
+
+            stop.set(true);
+            writes.get();
+        }
+        finally
+        {
+            es.shutdown();
+        }
+    }
+}
diff --git a/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyTest.java b/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyTest.java
index 562de22..ba3ad44 100644
--- a/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyTest.java
+++ b/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyTest.java
@@ -70,8 +70,8 @@
         Keyspace keyspace = Keyspace.open(ksname);
         ColumnFamilyStore store = keyspace.getColumnFamilyStore(cfname);
         store.disableAutoCompaction();
-
-        LeveledCompactionStrategy lcs = (LeveledCompactionStrategy)store.getCompactionStrategyManager().getStrategies().get(1);
+        CompactionStrategyManager mgr = store.getCompactionStrategyManager();
+        LeveledCompactionStrategy lcs = (LeveledCompactionStrategy) mgr.getStrategies().get(1).get(0);
 
         ByteBuffer value = ByteBuffer.wrap(new byte[100 * 1024]); // 100 KB value, make it easy to have multiple files
 
@@ -128,9 +128,9 @@
         int levels = manifest.getLevelCount();
         for (int level = 0; level < levels; level++)
         {
-            List<SSTableReader> sstables = manifest.getLevel(level);
+            Set<SSTableReader> sstables = manifest.getLevel(level);
             // score check
-            assert (double) SSTableReader.getTotalBytes(sstables) / LeveledManifest.maxBytesForLevel(level, 1 * 1024 * 1024) < 1.00;
+            assert (double) SSTableReader.getTotalBytes(sstables) / manifest.maxBytesForLevel(level, 1 * 1024 * 1024) < 1.00;
             // overlap check for levels greater than 0
             for (SSTableReader sstable : sstables)
             {
@@ -139,7 +139,7 @@
 
                 if (level > 0)
                 {// overlap check for levels greater than 0
-                    Set<SSTableReader> overlaps = LeveledManifest.overlapping(sstable, sstables);
+                    Set<SSTableReader> overlaps = LeveledManifest.overlapping(sstable.first.getToken(), sstable.last.getToken(), sstables);
                     assert overlaps.size() == 1 && overlaps.contains(sstable);
                 }
             }
@@ -151,12 +151,31 @@
     {
         Keyspace keyspace = Keyspace.open(KEYSPACE1);
         ColumnFamilyStore store = keyspace.getColumnFamilyStore(CF_STANDARDLVL2);
-        store.disableAutoCompaction();
-
-        LeveledCompactionStrategy lcs = (LeveledCompactionStrategy)store.getCompactionStrategyManager().getStrategies().get(1);
-
         ByteBuffer value = ByteBuffer.wrap(new byte[100 * 1024]); // 100 KB value, make it easy to have multiple files
 
+        // Enough data to have a level 1 and 2
+        int rows = 128;
+        int columns = 10;
+
+        // Adds enough data to trigger multiple sstable per level
+        for (int r = 0; r < rows; r++)
+        {
+            DecoratedKey key = Util.dk(String.valueOf(r));
+            UpdateBuilder builder = UpdateBuilder.create(store.metadata, key);
+            for (int c = 0; c < columns; c++)
+                builder.newRow("column" + c).add("val", value);
+
+            Mutation rm = new Mutation(builder.build());
+            rm.apply();
+            store.forceBlockingFlush();
+        }
+        LeveledCompactionStrategyTest.waitForLeveling(store);
+        store.disableAutoCompaction();
+        CompactionStrategyManager mgr = store.getCompactionStrategyManager();
+        LeveledCompactionStrategy lcs = (LeveledCompactionStrategy) mgr.getStrategies().get(1).get(0);
+
+        value = ByteBuffer.wrap(new byte[10 * 1024]); // 10 KB value
+
         // Adds 10 partitions
         for (int r = 0; r < 10; r++)
         {
diff --git a/test/long/org/apache/cassandra/dht/tokenallocator/AbstractReplicationAwareTokenAllocatorTest.java b/test/long/org/apache/cassandra/dht/tokenallocator/AbstractReplicationAwareTokenAllocatorTest.java
new file mode 100644
index 0000000..eb79f12
--- /dev/null
+++ b/test/long/org/apache/cassandra/dht/tokenallocator/AbstractReplicationAwareTokenAllocatorTest.java
@@ -0,0 +1,543 @@
+/*
+ * 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.dht.tokenallocator;
+
+import java.util.*;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Token;
+
+/**
+ * Base class for {@link Murmur3ReplicationAwareTokenAllocatorTest} and {@link RandomReplicationAwareTokenAllocatorTest},
+ * we need to separate classes to avoid timeous in case flaky tests need to be repeated, see CASSANDRA-12784.
+ */
+@Ignore
+abstract class AbstractReplicationAwareTokenAllocatorTest extends TokenAllocatorTestBase
+{
+    static class SimpleReplicationStrategy implements TestReplicationStrategy
+    {
+        int replicas;
+
+        public SimpleReplicationStrategy(int replicas)
+        {
+            super();
+            this.replicas = replicas;
+        }
+
+        public List<Unit> getReplicas(Token token, NavigableMap<Token, Unit> sortedTokens)
+        {
+            List<Unit> endpoints = new ArrayList<Unit>(replicas);
+
+            token = sortedTokens.ceilingKey(token);
+            if (token == null)
+                token = sortedTokens.firstKey();
+            Iterator<Unit> iter = Iterables.concat(sortedTokens.tailMap(token, true).values(), sortedTokens.values()).iterator();
+            while (endpoints.size() < replicas)
+            {
+                if (!iter.hasNext())
+                    return endpoints;
+                Unit ep = iter.next();
+                if (!endpoints.contains(ep))
+                    endpoints.add(ep);
+            }
+            return endpoints;
+        }
+
+        public Token replicationStart(Token token, Unit unit, NavigableMap<Token, Unit> sortedTokens)
+        {
+            Set<Unit> seenUnits = Sets.newHashSet();
+            int unitsFound = 0;
+
+            for (Map.Entry<Token, Unit> en : Iterables.concat(
+                                                             sortedTokens.headMap(token, false).descendingMap().entrySet(),
+                                                             sortedTokens.descendingMap().entrySet()))
+            {
+                Unit n = en.getValue();
+                // Same group as investigated unit is a break; anything that could replicate in it replicates there.
+                if (n == unit)
+                    break;
+
+                if (seenUnits.add(n))
+                {
+                    if (++unitsFound == replicas)
+                        break;
+                }
+                token = en.getKey();
+            }
+            return token;
+        }
+
+        public void addUnit(Unit n)
+        {
+        }
+
+        public void removeUnit(Unit n)
+        {
+        }
+
+        public String toString()
+        {
+            return String.format("Simple %d replicas", replicas);
+        }
+
+        public int replicas()
+        {
+            return replicas;
+        }
+
+        public boolean sameGroup(Unit n1, Unit n2)
+        {
+            return false;
+        }
+
+        public Unit getGroup(Unit unit)
+        {
+            // The unit is the group.
+            return unit;
+        }
+
+        public double spreadExpectation()
+        {
+            return 1;
+        }
+    }
+
+    static abstract class GroupReplicationStrategy implements TestReplicationStrategy
+    {
+        final int replicas;
+        final Map<Unit, Integer> groupMap;
+
+        public GroupReplicationStrategy(int replicas)
+        {
+            this.replicas = replicas;
+            this.groupMap = Maps.newHashMap();
+        }
+
+        public List<Unit> getReplicas(Token token, NavigableMap<Token, Unit> sortedTokens)
+        {
+            List<Unit> endpoints = new ArrayList<Unit>(replicas);
+            BitSet usedGroups = new BitSet();
+
+            if (sortedTokens.isEmpty())
+                return endpoints;
+
+            token = sortedTokens.ceilingKey(token);
+            if (token == null)
+                token = sortedTokens.firstKey();
+            Iterator<Unit> iter = Iterables.concat(sortedTokens.tailMap(token, true).values(), sortedTokens.values()).iterator();
+            while (endpoints.size() < replicas)
+            {
+                // For simlicity assuming list can't be exhausted before finding all replicas.
+                Unit ep = iter.next();
+                int group = groupMap.get(ep);
+                if (!usedGroups.get(group))
+                {
+                    endpoints.add(ep);
+                    usedGroups.set(group);
+                }
+            }
+            return endpoints;
+        }
+
+        public Token lastReplicaToken(Token token, NavigableMap<Token, Unit> sortedTokens)
+        {
+            BitSet usedGroups = new BitSet();
+            int groupsFound = 0;
+
+            token = sortedTokens.ceilingKey(token);
+            if (token == null)
+                token = sortedTokens.firstKey();
+            for (Map.Entry<Token, Unit> en :
+            Iterables.concat(sortedTokens.tailMap(token, true).entrySet(),
+                             sortedTokens.entrySet()))
+            {
+                Unit ep = en.getValue();
+                int group = groupMap.get(ep);
+                if (!usedGroups.get(group))
+                {
+                    usedGroups.set(group);
+                    if (++groupsFound >= replicas)
+                        return en.getKey();
+                }
+            }
+            return token;
+        }
+
+        public Token replicationStart(Token token, Unit unit, NavigableMap<Token, Unit> sortedTokens)
+        {
+            // replicated ownership
+            int unitGroup = groupMap.get(unit);   // unit must be already added
+            BitSet seenGroups = new BitSet();
+            int groupsFound = 0;
+
+            for (Map.Entry<Token, Unit> en : Iterables.concat(
+                                                             sortedTokens.headMap(token, false).descendingMap().entrySet(),
+                                                             sortedTokens.descendingMap().entrySet()))
+            {
+                Unit n = en.getValue();
+                int ngroup = groupMap.get(n);
+                // Same group as investigated unit is a break; anything that could replicate in it replicates there.
+                if (ngroup == unitGroup)
+                    break;
+
+                if (!seenGroups.get(ngroup))
+                {
+                    if (++groupsFound == replicas)
+                        break;
+                    seenGroups.set(ngroup);
+                }
+                token = en.getKey();
+            }
+            return token;
+        }
+
+        public String toString()
+        {
+            Map<Integer, Integer> idToSize = instanceToCount(groupMap);
+            Map<Integer, Integer> sizeToCount = Maps.newTreeMap();
+            sizeToCount.putAll(instanceToCount(idToSize));
+            return String.format("%s strategy, %d replicas, group size to count %s", getClass().getSimpleName(), replicas, sizeToCount);
+        }
+
+        @Override
+        public int replicas()
+        {
+            return replicas;
+        }
+
+        public boolean sameGroup(Unit n1, Unit n2)
+        {
+            return groupMap.get(n1).equals(groupMap.get(n2));
+        }
+
+        public void removeUnit(Unit n)
+        {
+            groupMap.remove(n);
+        }
+
+        public Integer getGroup(Unit unit)
+        {
+            return groupMap.get(unit);
+        }
+
+        public double spreadExpectation()
+        {
+            return 1.5;   // Even balanced racks get disbalanced when they lose nodes.
+        }
+    }
+
+    private static <T> Map<T, Integer> instanceToCount(Map<?, T> map)
+    {
+        Map<T, Integer> idToCount = Maps.newHashMap();
+        for (Map.Entry<?, T> en : map.entrySet())
+        {
+            Integer old = idToCount.get(en.getValue());
+            idToCount.put(en.getValue(), old != null ? old + 1 : 1);
+        }
+        return idToCount;
+    }
+
+    /**
+     * Group strategy spreading units into a fixed number of groups.
+     */
+    static class FixedGroupCountReplicationStrategy extends GroupReplicationStrategy
+    {
+        int groupId;
+        int groupCount;
+
+        public FixedGroupCountReplicationStrategy(int replicas, int groupCount)
+        {
+            super(replicas);
+            assert groupCount >= replicas;
+            groupId = 0;
+            this.groupCount = groupCount;
+        }
+
+        public void addUnit(Unit n)
+        {
+            groupMap.put(n, groupId++ % groupCount);
+        }
+    }
+
+    /**
+     * Group strategy with a fixed number of units per group.
+     */
+    static class BalancedGroupReplicationStrategy extends GroupReplicationStrategy
+    {
+        int groupId;
+        int groupSize;
+
+        public BalancedGroupReplicationStrategy(int replicas, int groupSize)
+        {
+            super(replicas);
+            groupId = 0;
+            this.groupSize = groupSize;
+        }
+
+        public void addUnit(Unit n)
+        {
+            groupMap.put(n, groupId++ / groupSize);
+        }
+    }
+
+    static class UnbalancedGroupReplicationStrategy extends GroupReplicationStrategy
+    {
+        int groupId;
+        int nextSize;
+        int num;
+        int minGroupSize;
+        int maxGroupSize;
+        Random rand;
+
+        public UnbalancedGroupReplicationStrategy(int replicas, int minGroupSize, int maxGroupSize, Random rand)
+        {
+            super(replicas);
+            groupId = -1;
+            nextSize = 0;
+            num = 0;
+            this.maxGroupSize = maxGroupSize;
+            this.minGroupSize = minGroupSize;
+            this.rand = rand;
+        }
+
+        public void addUnit(Unit n)
+        {
+            if (++num > nextSize)
+            {
+                nextSize = minGroupSize + rand.nextInt(maxGroupSize - minGroupSize + 1);
+                ++groupId;
+                num = 0;
+            }
+            groupMap.put(n, groupId);
+        }
+
+        public double spreadExpectation()
+        {
+            return 2;
+        }
+    }
+
+    static Map<Unit, Double> evaluateReplicatedOwnership(ReplicationAwareTokenAllocator<Unit> t)
+    {
+        Map<Unit, Double> ownership = Maps.newHashMap();
+        Iterator<Token> it = t.sortedTokens.keySet().iterator();
+        if (!it.hasNext())
+            return ownership;
+
+        Token current = it.next();
+        while (it.hasNext())
+        {
+            Token next = it.next();
+            addOwnership(t, current, next, ownership);
+            current = next;
+        }
+        addOwnership(t, current, t.sortedTokens.firstKey(), ownership);
+
+        return ownership;
+    }
+
+    private static void addOwnership(ReplicationAwareTokenAllocator<Unit> t, Token current, Token next, Map<Unit, Double> ownership)
+    {
+        TestReplicationStrategy ts = (TestReplicationStrategy) t.strategy;
+        double size = current.size(next);
+        Token representative = t.partitioner.midpoint(current, next);
+        for (Unit n : ts.getReplicas(representative, t.sortedTokens))
+        {
+            Double v = ownership.get(n);
+            ownership.put(n, v != null ? v + size : size);
+        }
+    }
+
+    private static double replicatedTokenOwnership(Token token, NavigableMap<Token, Unit> sortedTokens, ReplicationStrategy<Unit> strategy)
+    {
+        TestReplicationStrategy ts = (TestReplicationStrategy) strategy;
+        Token next = sortedTokens.higherKey(token);
+        if (next == null)
+            next = sortedTokens.firstKey();
+        return ts.replicationStart(token, sortedTokens.get(token), sortedTokens).size(next);
+    }
+
+    protected void testExistingCluster(IPartitioner partitioner, int maxVNodeCount)
+    {
+        for (int rf = 1; rf <= 5; ++rf)
+        {
+            for (int perUnitCount = 1; perUnitCount <= maxVNodeCount; perUnitCount *= 4)
+            {
+                testExistingCluster(perUnitCount, fixedTokenCount, new SimpleReplicationStrategy(rf), partitioner);
+                testExistingCluster(perUnitCount, varyingTokenCount, new SimpleReplicationStrategy(rf), partitioner);
+                if (rf == 1) continue;  // Replication strategy doesn't matter for RF = 1.
+                for (int groupSize = 4; groupSize <= 64 && groupSize * rf * 4 < TARGET_CLUSTER_SIZE; groupSize *= 4)
+                {
+                    testExistingCluster(perUnitCount, fixedTokenCount,
+                                        new BalancedGroupReplicationStrategy(rf, groupSize), partitioner);
+                    testExistingCluster(perUnitCount, varyingTokenCount,
+                                        new UnbalancedGroupReplicationStrategy(rf, groupSize / 2, groupSize * 2, seededRand),
+                                        partitioner);
+                }
+                testExistingCluster(perUnitCount, fixedTokenCount,
+                                    new FixedGroupCountReplicationStrategy(rf, rf * 2), partitioner);
+            }
+        }
+    }
+
+    private void testExistingCluster(int perUnitCount, TokenCount tc, TestReplicationStrategy rs, IPartitioner partitioner)
+    {
+        System.out.println("Testing existing cluster, target " + perUnitCount + " vnodes, replication " + rs);
+        final int targetClusterSize = TARGET_CLUSTER_SIZE;
+        NavigableMap<Token, Unit> tokenMap = Maps.newTreeMap();
+
+        random(tokenMap, rs, targetClusterSize / 2, tc, perUnitCount, partitioner);
+
+        ReplicationAwareTokenAllocator<Unit> t = new ReplicationAwareTokenAllocator<>(tokenMap, rs, partitioner);
+        grow(t, targetClusterSize * 9 / 10, tc, perUnitCount, false);
+        grow(t, targetClusterSize, tc, perUnitCount, true);
+        loseAndReplace(t, targetClusterSize / 10, tc, perUnitCount, partitioner);
+        System.out.println();
+    }
+
+    protected void testNewCluster(IPartitioner partitioner, int maxVNodeCount)
+    {
+        // This test is flaky because the selection of the tokens for the first RF nodes (which is random, with an
+        // uncontrolled seed) can sometimes cause a pathological situation where the algorithm will find a (close to)
+        // ideal distribution of tokens for some number of nodes, which in turn will inevitably cause it to go into a
+        // bad (unacceptable to the test criteria) distribution after adding one more node.
+
+        // This should happen very rarely, unless something is broken in the token allocation code.
+
+        for (int rf = 2; rf <= 5; ++rf)
+        {
+            for (int perUnitCount = 1; perUnitCount <= maxVNodeCount; perUnitCount *= 4)
+            {
+                testNewCluster(perUnitCount, fixedTokenCount, new SimpleReplicationStrategy(rf), partitioner);
+                testNewCluster(perUnitCount, varyingTokenCount, new SimpleReplicationStrategy(rf), partitioner);
+                if (rf == 1) continue;  // Replication strategy doesn't matter for RF = 1.
+                for (int groupSize = 4; groupSize <= 64 && groupSize * rf * 8 < TARGET_CLUSTER_SIZE; groupSize *= 4)
+                {
+                    testNewCluster(perUnitCount, fixedTokenCount,
+                                   new BalancedGroupReplicationStrategy(rf, groupSize), partitioner);
+                    testNewCluster(perUnitCount, varyingTokenCount,
+                                   new UnbalancedGroupReplicationStrategy(rf, groupSize / 2, groupSize * 2, seededRand),
+                                   partitioner);
+                }
+                testNewCluster(perUnitCount, fixedTokenCount,
+                               new FixedGroupCountReplicationStrategy(rf, rf * 2), partitioner);
+            }
+        }
+    }
+
+    private void testNewCluster(int perUnitCount, TokenCount tc, TestReplicationStrategy rs, IPartitioner partitioner)
+    {
+        System.out.println("Testing new cluster, target " + perUnitCount + " vnodes, replication " + rs);
+        final int targetClusterSize = TARGET_CLUSTER_SIZE;
+        NavigableMap<Token, Unit> tokenMap = Maps.newTreeMap();
+
+        ReplicationAwareTokenAllocator<Unit> t = new ReplicationAwareTokenAllocator<>(tokenMap, rs, partitioner);
+        grow(t, targetClusterSize * 2 / 5, tc, perUnitCount, false);
+        grow(t, targetClusterSize, tc, perUnitCount, true);
+        loseAndReplace(t, targetClusterSize / 5, tc, perUnitCount, partitioner);
+        System.out.println();
+    }
+
+    private void loseAndReplace(ReplicationAwareTokenAllocator<Unit> t, int howMany,
+                                TokenCount tc, int perUnitCount, IPartitioner partitioner)
+    {
+        int fullCount = t.unitCount();
+        System.out.format("Losing %d units. ", howMany);
+        for (int i = 0; i < howMany; ++i)
+        {
+            Unit u = t.unitFor(partitioner.getRandomToken(seededRand));
+            t.removeUnit(u);
+            ((TestReplicationStrategy) t.strategy).removeUnit(u);
+        }
+        // Grow half without verifying.
+        grow(t, (t.unitCount() + fullCount * 3) / 4, tc, perUnitCount, false);
+        // Metrics should be back to normal by now. Check that they remain so.
+        grow(t, fullCount, tc, perUnitCount, true);
+    }
+
+    public void grow(ReplicationAwareTokenAllocator<Unit> t, int targetClusterSize, TokenCount tc, int perUnitCount, boolean verifyMetrics)
+    {
+        int size = t.unitCount();
+        Summary su = new Summary();
+        Summary st = new Summary();
+        Random rand = new Random(targetClusterSize + perUnitCount);
+        TestReplicationStrategy strategy = (TestReplicationStrategy) t.strategy;
+        if (size < targetClusterSize)
+        {
+            System.out.format("Adding %d unit(s) using %s...", targetClusterSize - size, t.toString());
+            long time = System.currentTimeMillis();
+            while (size < targetClusterSize)
+            {
+                int tokens = tc.tokenCount(perUnitCount, rand);
+                Unit unit = new Unit();
+                strategy.addUnit(unit);
+                t.addUnit(unit, tokens);
+                ++size;
+                if (verifyMetrics)
+                    updateSummary(t, su, st, false);
+            }
+            System.out.format(" Done in %.3fs\n", (System.currentTimeMillis() - time) / 1000.0);
+            if (verifyMetrics)
+            {
+                updateSummary(t, su, st, true);
+                double maxExpected = 1.0 + tc.spreadExpectation() * strategy.spreadExpectation() / (perUnitCount * t.replicas);
+                if (su.max > maxExpected)
+                {
+                    Assert.fail(String.format("Expected max unit size below %.4f, was %.4f", maxExpected, su.max));
+                }
+                // We can't verify lower side range as small loads can't always be fixed.
+            }
+        }
+    }
+
+    private void updateSummary(ReplicationAwareTokenAllocator<Unit> t, Summary su, Summary st, boolean print)
+    {
+        int size = t.sortedTokens.size();
+        double inverseAverage = 1.0 * size / t.strategy.replicas();
+
+        Map<Unit, Double> ownership = evaluateReplicatedOwnership(t);
+        SummaryStatistics unitStat = new SummaryStatistics();
+        for (Map.Entry<Unit, Double> en : ownership.entrySet())
+            unitStat.addValue(en.getValue() * inverseAverage / t.unitToTokens.get(en.getKey()).size());
+        su.update(unitStat);
+
+        SummaryStatistics tokenStat = new SummaryStatistics();
+        for (Token tok : t.sortedTokens.keySet())
+            tokenStat.addValue(replicatedTokenOwnership(tok, t.sortedTokens, t.strategy) * inverseAverage);
+        st.update(tokenStat);
+
+        if (print)
+        {
+            System.out.format("Size %d(%d)   \tunit %s  token %s   %s\n",
+                              t.unitCount(), size,
+                              mms(unitStat),
+                              mms(tokenStat),
+                              t.strategy);
+            System.out.format("Worst intermediate unit\t%s  token %s\n", su, st);
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/long/org/apache/cassandra/dht/tokenallocator/Murmur3ReplicationAwareTokenAllocatorTest.java b/test/long/org/apache/cassandra/dht/tokenallocator/Murmur3ReplicationAwareTokenAllocatorTest.java
new file mode 100644
index 0000000..e28ecfa
--- /dev/null
+++ b/test/long/org/apache/cassandra/dht/tokenallocator/Murmur3ReplicationAwareTokenAllocatorTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.dht.tokenallocator;
+
+import org.junit.Test;
+
+import org.apache.cassandra.Util;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+
+public class Murmur3ReplicationAwareTokenAllocatorTest extends AbstractReplicationAwareTokenAllocatorTest
+{
+    private static final int MAX_VNODE_COUNT = 64;
+
+    @Test
+    public void testExistingCluster()
+    {
+        super.testExistingCluster(new Murmur3Partitioner(), MAX_VNODE_COUNT);
+    }
+
+    @Test
+    public void testNewCluster()
+    {
+        Util.flakyTest(this::flakyTestNewCluster,
+                       2,
+                       "It tends to fail sometimes due to the random selection of the tokens in the first few nodes.");
+    }
+
+    private void flakyTestNewCluster()
+    {
+        testNewCluster(new Murmur3Partitioner(), MAX_VNODE_COUNT);
+    }
+}
diff --git a/test/long/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocatorTest.java b/test/long/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocatorTest.java
new file mode 100644
index 0000000..c53f788
--- /dev/null
+++ b/test/long/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocatorTest.java
@@ -0,0 +1,258 @@
+/*
+ * 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.dht.tokenallocator;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.PriorityQueue;
+import java.util.Random;
+
+import com.google.common.collect.Maps;
+import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
+import org.junit.Test;
+
+import junit.framework.Assert;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.dht.RandomPartitioner;
+import org.apache.cassandra.dht.Token;
+
+public class NoReplicationTokenAllocatorTest extends TokenAllocatorTestBase
+{
+
+    @Test
+    public void testNewClusterWithMurmur3Partitioner()
+    {
+        testNewCluster(new Murmur3Partitioner());
+    }
+
+    @Test
+    public void testNewClusterWithRandomPartitioner()
+    {
+        testNewCluster(new RandomPartitioner());
+    }
+
+    private void testNewCluster(IPartitioner partitioner)
+    {
+        for (int perUnitCount = 1; perUnitCount <= MAX_VNODE_COUNT; perUnitCount *= 4)
+        {
+            testNewCluster(perUnitCount, fixedTokenCount, new NoReplicationStrategy(), partitioner);
+            testNewCluster(perUnitCount, fixedTokenCount, new ZeroReplicationStrategy(), partitioner);
+        }
+    }
+
+    public void testNewCluster(int perUnitCount, TokenCount tc, NoReplicationStrategy rs, IPartitioner partitioner)
+    {
+        System.out.println("Testing new cluster, target " + perUnitCount + " vnodes, replication " + rs);
+        final int targetClusterSize = TARGET_CLUSTER_SIZE;
+        NavigableMap<Token, Unit> tokenMap = Maps.newTreeMap();
+
+        NoReplicationTokenAllocator<Unit> t = new NoReplicationTokenAllocator<Unit>(tokenMap, rs, partitioner);
+        grow(t, targetClusterSize * 2 / 5, tc, perUnitCount, false);
+        grow(t, targetClusterSize, tc, perUnitCount, true);
+        System.out.println();
+    }
+
+    @Test
+    public void testExistingClusterWithMurmur3Partitioner()
+    {
+        testExistingCluster(new Murmur3Partitioner());
+    }
+
+    @Test
+    public void testExistingClusterWithRandomPartitioner()
+    {
+        testExistingCluster(new RandomPartitioner());
+    }
+
+    private void testExistingCluster(IPartitioner partitioner)
+    {
+        for (int perUnitCount = 1; perUnitCount <= MAX_VNODE_COUNT; perUnitCount *= 4)
+        {
+            testExistingCluster(perUnitCount, fixedTokenCount, new NoReplicationStrategy(), partitioner);
+            testExistingCluster(perUnitCount, fixedTokenCount, new ZeroReplicationStrategy(), partitioner);
+        }
+    }
+
+    public NoReplicationTokenAllocator<Unit> randomWithTokenAllocator(NavigableMap<Token, Unit> map, NoReplicationStrategy rs,
+                                                                      int unitCount, TokenCount tc, int perUnitCount,
+                                                                      IPartitioner partitioner)
+    {
+        super.random(map, rs, unitCount, tc, perUnitCount, partitioner);
+        NoReplicationTokenAllocator<Unit> t = new NoReplicationTokenAllocator<Unit>(map, rs, partitioner);
+        t.createTokenInfos();
+        return t;
+    }
+
+    public void testExistingCluster(int perUnitCount, TokenCount tc, NoReplicationStrategy rs, IPartitioner partitioner)
+    {
+        System.out.println("Testing existing cluster, target " + perUnitCount + " vnodes, replication " + rs);
+        final int targetClusterSize = TARGET_CLUSTER_SIZE;
+        NavigableMap<Token, Unit> tokenMap = Maps.newTreeMap();
+        NoReplicationTokenAllocator<Unit> t = randomWithTokenAllocator(tokenMap, rs, targetClusterSize / 2, tc, perUnitCount, partitioner);
+        updateSummaryBeforeGrow(t);
+
+        grow(t, targetClusterSize * 9 / 10, tc, perUnitCount, false);
+        grow(t, targetClusterSize, tc, perUnitCount, true);
+        loseAndReplace(t, targetClusterSize / 10, tc, perUnitCount, partitioner);
+        System.out.println();
+    }
+
+    private void loseAndReplace(NoReplicationTokenAllocator<Unit> t, int howMany,
+                                TokenCount tc, int perUnitCount, IPartitioner partitioner)
+    {
+        int fullCount = t.sortedUnits.size();
+        System.out.format("Losing %d units. ", howMany);
+        for (int i = 0; i < howMany; ++i)
+        {
+            Unit u = t.unitFor(partitioner.getRandomToken(seededRand));
+            t.removeUnit(u);
+        }
+        // Grow half without verifying.
+        grow(t, (t.sortedUnits.size() + fullCount * 3) / 4, tc, perUnitCount, false);
+        // Metrics should be back to normal by now. Check that they remain so.
+        grow(t, fullCount, tc, perUnitCount, true);
+    }
+
+    private void updateSummaryBeforeGrow(NoReplicationTokenAllocator<Unit> t)
+    {
+        Summary su = new Summary();
+        Summary st = new Summary();
+        System.out.println("Before growing cluster: ");
+        updateSummary(t, su, st, true);
+    }
+
+    private void grow(NoReplicationTokenAllocator<Unit> t, int targetClusterSize, TokenCount tc, int perUnitCount, boolean verifyMetrics)
+    {
+        int size = t.sortedUnits.size();
+        Summary su = new Summary();
+        Summary st = new Summary();
+        Random rand = new Random(targetClusterSize + perUnitCount);
+        TestReplicationStrategy strategy = (TestReplicationStrategy) t.strategy;
+        if (size < targetClusterSize)
+        {
+            System.out.format("Adding %d unit(s) using %s...", targetClusterSize - size, t.toString());
+            long time = System.currentTimeMillis();
+
+            while (size < targetClusterSize)
+            {
+                int num_tokens = tc.tokenCount(perUnitCount, rand);
+                Unit unit = new Unit();
+                t.addUnit(unit, num_tokens);
+                ++size;
+                if (verifyMetrics)
+                    updateSummary(t, su, st, false);
+            }
+            System.out.format(" Done in %.3fs\n", (System.currentTimeMillis() - time) / 1000.0);
+
+            if (verifyMetrics)
+            {
+                updateSummary(t, su, st, true);
+                double maxExpected = 1.0 + tc.spreadExpectation() * strategy.spreadExpectation() / perUnitCount;
+                if (su.max > maxExpected)
+                {
+                    Assert.fail(String.format("Expected max unit size below %.4f, was %.4f", maxExpected, su.max));
+                }
+            }
+        }
+    }
+
+    private void updateSummary(NoReplicationTokenAllocator<Unit> t, Summary su, Summary st, boolean print)
+    {
+        int size = t.sortedTokens.size();
+
+        SummaryStatistics unitStat = new SummaryStatistics();
+        for (TokenAllocatorBase.Weighted<TokenAllocatorBase.UnitInfo> wu : t.sortedUnits)
+        {
+            unitStat.addValue(wu.weight * size / t.tokensInUnits.get(wu.value.unit).size());
+        }
+        su.update(unitStat);
+
+        SummaryStatistics tokenStat = new SummaryStatistics();
+        for (PriorityQueue<TokenAllocatorBase.Weighted<TokenAllocatorBase.TokenInfo>> tokens : t.tokensInUnits.values())
+        {
+            for (TokenAllocatorBase.Weighted<TokenAllocatorBase.TokenInfo> token : tokens)
+            {
+                tokenStat.addValue(token.weight);
+            }
+        }
+        st.update(tokenStat);
+
+        if (print)
+        {
+            System.out.format("Size %d(%d)   \tunit %s  token %s   %s\n",
+                              t.sortedUnits.size(), size,
+                              mms(unitStat),
+                              mms(tokenStat),
+                              t.strategy);
+            System.out.format("Worst intermediate unit\t%s  token %s\n", su, st);
+        }
+    }
+
+    static class NoReplicationStrategy implements TestReplicationStrategy
+    {
+        public List<Unit> getReplicas(Token token, NavigableMap<Token, Unit> sortedTokens)
+        {
+            return Collections.singletonList(sortedTokens.ceilingEntry(token).getValue());
+        }
+
+        public Token replicationStart(Token token, Unit unit, NavigableMap<Token, Unit> sortedTokens)
+        {
+            return sortedTokens.lowerKey(token);
+        }
+
+        public String toString()
+        {
+            return "No replication";
+        }
+
+        public void addUnit(Unit n)
+        {
+        }
+
+        public void removeUnit(Unit n)
+        {
+        }
+
+        public int replicas()
+        {
+            return 1;
+        }
+
+        public Object getGroup(Unit unit)
+        {
+            return unit;
+        }
+
+        public double spreadExpectation()
+        {
+            return 1;
+        }
+    }
+
+    static class ZeroReplicationStrategy extends NoReplicationStrategy
+    {
+        public int replicas()
+        {
+            return 0;
+        }
+    }
+}
diff --git a/test/long/org/apache/cassandra/dht/tokenallocator/RandomReplicationAwareTokenAllocatorTest.java b/test/long/org/apache/cassandra/dht/tokenallocator/RandomReplicationAwareTokenAllocatorTest.java
new file mode 100644
index 0000000..bd94442
--- /dev/null
+++ b/test/long/org/apache/cassandra/dht/tokenallocator/RandomReplicationAwareTokenAllocatorTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.dht.tokenallocator;
+
+import org.junit.Test;
+
+import org.apache.cassandra.Util;
+import org.apache.cassandra.dht.RandomPartitioner;
+
+public class RandomReplicationAwareTokenAllocatorTest extends AbstractReplicationAwareTokenAllocatorTest
+{
+    /** The maximum number of vnodes to use in the tests.
+     *  For RandomPartitioner we use a smaller number because
+     *  the tests take much longer and would otherwise timeout,
+     *  see CASSANDRA-12784.
+     * */
+    private static final int MAX_VNODE_COUNT = 16;
+
+    @Test
+    public void testExistingCluster()
+    {
+        testExistingCluster(new RandomPartitioner(), MAX_VNODE_COUNT);
+    }
+
+    @Test
+    public void testNewClusterr()
+    {
+        Util.flakyTest(this::flakyTestNewCluster,
+                       3,
+                       "It tends to fail sometimes due to the random selection of the tokens in the first few nodes.");
+    }
+
+    private void flakyTestNewCluster()
+    {
+        testNewCluster(new RandomPartitioner(), MAX_VNODE_COUNT);
+    }
+
+}
diff --git a/test/long/org/apache/cassandra/dht/tokenallocator/ReplicationAwareTokenAllocatorTest.java b/test/long/org/apache/cassandra/dht/tokenallocator/ReplicationAwareTokenAllocatorTest.java
deleted file mode 100644
index 1b36c55..0000000
--- a/test/long/org/apache/cassandra/dht/tokenallocator/ReplicationAwareTokenAllocatorTest.java
+++ /dev/null
@@ -1,715 +0,0 @@
-/*
- * 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.dht.tokenallocator;
-
-import java.util.*;
-
-import junit.framework.Assert;
-
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-
-import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
-
-import org.junit.Test;
-
-import org.apache.cassandra.Util;
-import org.apache.cassandra.dht.Murmur3Partitioner;
-import org.apache.cassandra.dht.Token;
-
-public class ReplicationAwareTokenAllocatorTest
-{
-    private static final int MAX_VNODE_COUNT = 64;
-
-    private static final int TARGET_CLUSTER_SIZE = 250;
-
-    interface TestReplicationStrategy extends ReplicationStrategy<Unit>
-    {
-        void addUnit(Unit n);
-
-        void removeUnit(Unit n);
-
-        /**
-         * Returns a list of all replica units for given token.
-         */
-        List<Unit> getReplicas(Token token, NavigableMap<Token, Unit> sortedTokens);
-
-        /**
-         * Returns the start of the token span that is replicated in this token.
-         * Note: Though this is not trivial to see, the replicated span is always contiguous. A token in the same
-         * group acts as a barrier; if one is not found the token replicates everything up to the replica'th distinct
-         * group seen in front of it.
-         */
-        Token replicationStart(Token token, Unit unit, NavigableMap<Token, Unit> sortedTokens);
-
-        /**
-         * Multiplier for the acceptable disbalance in the cluster. With some strategies it is harder to achieve good
-         * results.
-         */
-        public double spreadExpectation();
-    }
-
-    static class NoReplicationStrategy implements TestReplicationStrategy
-    {
-        public List<Unit> getReplicas(Token token, NavigableMap<Token, Unit> sortedTokens)
-        {
-            return Collections.singletonList(sortedTokens.ceilingEntry(token).getValue());
-        }
-
-        public Token replicationStart(Token token, Unit unit, NavigableMap<Token, Unit> sortedTokens)
-        {
-            return sortedTokens.lowerKey(token);
-        }
-
-        public String toString()
-        {
-            return "No replication";
-        }
-
-        public void addUnit(Unit n)
-        {
-        }
-
-        public void removeUnit(Unit n)
-        {
-        }
-
-        public int replicas()
-        {
-            return 1;
-        }
-
-        public boolean sameGroup(Unit n1, Unit n2)
-        {
-            return false;
-        }
-
-        public Object getGroup(Unit unit)
-        {
-            return unit;
-        }
-
-        public double spreadExpectation()
-        {
-            return 1;
-        }
-    }
-
-    static class SimpleReplicationStrategy implements TestReplicationStrategy
-    {
-        int replicas;
-
-        public SimpleReplicationStrategy(int replicas)
-        {
-            super();
-            this.replicas = replicas;
-        }
-
-        public List<Unit> getReplicas(Token token, NavigableMap<Token, Unit> sortedTokens)
-        {
-            List<Unit> endpoints = new ArrayList<Unit>(replicas);
-
-            token = sortedTokens.ceilingKey(token);
-            if (token == null)
-                token = sortedTokens.firstKey();
-            Iterator<Unit> iter = Iterables.concat(sortedTokens.tailMap(token, true).values(), sortedTokens.values()).iterator();
-            while (endpoints.size() < replicas)
-            {
-                if (!iter.hasNext())
-                    return endpoints;
-                Unit ep = iter.next();
-                if (!endpoints.contains(ep))
-                    endpoints.add(ep);
-            }
-            return endpoints;
-        }
-
-        public Token replicationStart(Token token, Unit unit, NavigableMap<Token, Unit> sortedTokens)
-        {
-            Set<Unit> seenUnits = Sets.newHashSet();
-            int unitsFound = 0;
-
-            for (Map.Entry<Token, Unit> en : Iterables.concat(
-                                                             sortedTokens.headMap(token, false).descendingMap().entrySet(),
-                                                             sortedTokens.descendingMap().entrySet()))
-            {
-                Unit n = en.getValue();
-                // Same group as investigated unit is a break; anything that could replicate in it replicates there.
-                if (n == unit)
-                    break;
-
-                if (seenUnits.add(n))
-                {
-                    if (++unitsFound == replicas)
-                        break;
-                }
-                token = en.getKey();
-            }
-            return token;
-        }
-
-        public void addUnit(Unit n)
-        {
-        }
-
-        public void removeUnit(Unit n)
-        {
-        }
-
-        public String toString()
-        {
-            return String.format("Simple %d replicas", replicas);
-        }
-
-        public int replicas()
-        {
-            return replicas;
-        }
-
-        public boolean sameGroup(Unit n1, Unit n2)
-        {
-            return false;
-        }
-
-        public Unit getGroup(Unit unit)
-        {
-            // The unit is the group.
-            return unit;
-        }
-
-        public double spreadExpectation()
-        {
-            return 1;
-        }
-    }
-
-    static abstract class GroupReplicationStrategy implements TestReplicationStrategy
-    {
-        final int replicas;
-        final Map<Unit, Integer> groupMap;
-
-        public GroupReplicationStrategy(int replicas)
-        {
-            this.replicas = replicas;
-            this.groupMap = Maps.newHashMap();
-        }
-
-        public List<Unit> getReplicas(Token token, NavigableMap<Token, Unit> sortedTokens)
-        {
-            List<Unit> endpoints = new ArrayList<Unit>(replicas);
-            BitSet usedGroups = new BitSet();
-
-            if (sortedTokens.isEmpty())
-                return endpoints;
-
-            token = sortedTokens.ceilingKey(token);
-            if (token == null)
-                token = sortedTokens.firstKey();
-            Iterator<Unit> iter = Iterables.concat(sortedTokens.tailMap(token, true).values(), sortedTokens.values()).iterator();
-            while (endpoints.size() < replicas)
-            {
-                // For simlicity assuming list can't be exhausted before finding all replicas.
-                Unit ep = iter.next();
-                int group = groupMap.get(ep);
-                if (!usedGroups.get(group))
-                {
-                    endpoints.add(ep);
-                    usedGroups.set(group);
-                }
-            }
-            return endpoints;
-        }
-
-        public Token lastReplicaToken(Token token, NavigableMap<Token, Unit> sortedTokens)
-        {
-            BitSet usedGroups = new BitSet();
-            int groupsFound = 0;
-
-            token = sortedTokens.ceilingKey(token);
-            if (token == null)
-                token = sortedTokens.firstKey();
-            for (Map.Entry<Token, Unit> en :
-            Iterables.concat(sortedTokens.tailMap(token, true).entrySet(),
-                             sortedTokens.entrySet()))
-            {
-                Unit ep = en.getValue();
-                int group = groupMap.get(ep);
-                if (!usedGroups.get(group))
-                {
-                    usedGroups.set(group);
-                    if (++groupsFound >= replicas)
-                        return en.getKey();
-                }
-            }
-            return token;
-        }
-
-        public Token replicationStart(Token token, Unit unit, NavigableMap<Token, Unit> sortedTokens)
-        {
-            // replicated ownership
-            int unitGroup = groupMap.get(unit);   // unit must be already added
-            BitSet seenGroups = new BitSet();
-            int groupsFound = 0;
-
-            for (Map.Entry<Token, Unit> en : Iterables.concat(
-                                                             sortedTokens.headMap(token, false).descendingMap().entrySet(),
-                                                             sortedTokens.descendingMap().entrySet()))
-            {
-                Unit n = en.getValue();
-                int ngroup = groupMap.get(n);
-                // Same group as investigated unit is a break; anything that could replicate in it replicates there.
-                if (ngroup == unitGroup)
-                    break;
-
-                if (!seenGroups.get(ngroup))
-                {
-                    if (++groupsFound == replicas)
-                        break;
-                    seenGroups.set(ngroup);
-                }
-                token = en.getKey();
-            }
-            return token;
-        }
-
-        public String toString()
-        {
-            Map<Integer, Integer> idToSize = instanceToCount(groupMap);
-            Map<Integer, Integer> sizeToCount = Maps.newTreeMap();
-            sizeToCount.putAll(instanceToCount(idToSize));
-            return String.format("%s strategy, %d replicas, group size to count %s", getClass().getSimpleName(), replicas, sizeToCount);
-        }
-
-        @Override
-        public int replicas()
-        {
-            return replicas;
-        }
-
-        public boolean sameGroup(Unit n1, Unit n2)
-        {
-            return groupMap.get(n1).equals(groupMap.get(n2));
-        }
-
-        public void removeUnit(Unit n)
-        {
-            groupMap.remove(n);
-        }
-
-        public Integer getGroup(Unit unit)
-        {
-            return groupMap.get(unit);
-        }
-
-        public double spreadExpectation()
-        {
-            return 1.5;   // Even balanced racks get disbalanced when they lose nodes.
-        }
-    }
-
-    private static <T> Map<T, Integer> instanceToCount(Map<?, T> map)
-    {
-        Map<T, Integer> idToCount = Maps.newHashMap();
-        for (Map.Entry<?, T> en : map.entrySet())
-        {
-            Integer old = idToCount.get(en.getValue());
-            idToCount.put(en.getValue(), old != null ? old + 1 : 1);
-        }
-        return idToCount;
-    }
-
-    /**
-     * Group strategy spreading units into a fixed number of groups.
-     */
-    static class FixedGroupCountReplicationStrategy extends GroupReplicationStrategy
-    {
-        int groupId;
-        int groupCount;
-
-        public FixedGroupCountReplicationStrategy(int replicas, int groupCount)
-        {
-            super(replicas);
-            assert groupCount >= replicas;
-            groupId = 0;
-            this.groupCount = groupCount;
-        }
-
-        public void addUnit(Unit n)
-        {
-            groupMap.put(n, groupId++ % groupCount);
-        }
-    }
-
-    /**
-     * Group strategy with a fixed number of units per group.
-     */
-    static class BalancedGroupReplicationStrategy extends GroupReplicationStrategy
-    {
-        int groupId;
-        int groupSize;
-
-        public BalancedGroupReplicationStrategy(int replicas, int groupSize)
-        {
-            super(replicas);
-            groupId = 0;
-            this.groupSize = groupSize;
-        }
-
-        public void addUnit(Unit n)
-        {
-            groupMap.put(n, groupId++ / groupSize);
-        }
-    }
-
-    static class UnbalancedGroupReplicationStrategy extends GroupReplicationStrategy
-    {
-        int groupId;
-        int nextSize;
-        int num;
-        int minGroupSize;
-        int maxGroupSize;
-        Random rand;
-
-        public UnbalancedGroupReplicationStrategy(int replicas, int minGroupSize, int maxGroupSize, Random rand)
-        {
-            super(replicas);
-            groupId = -1;
-            nextSize = 0;
-            num = 0;
-            this.maxGroupSize = maxGroupSize;
-            this.minGroupSize = minGroupSize;
-            this.rand = rand;
-        }
-
-        public void addUnit(Unit n)
-        {
-            if (++num > nextSize)
-            {
-                nextSize = minGroupSize + rand.nextInt(maxGroupSize - minGroupSize + 1);
-                ++groupId;
-                num = 0;
-            }
-            groupMap.put(n, groupId);
-        }
-
-        public double spreadExpectation()
-        {
-            return 2;
-        }
-    }
-
-    static Map<Unit, Double> evaluateReplicatedOwnership(ReplicationAwareTokenAllocator<Unit> t)
-    {
-        Map<Unit, Double> ownership = Maps.newHashMap();
-        Iterator<Token> it = t.sortedTokens.keySet().iterator();
-        if (!it.hasNext())
-            return ownership;
-
-        Token current = it.next();
-        while (it.hasNext())
-        {
-            Token next = it.next();
-            addOwnership(t, current, next, ownership);
-            current = next;
-        }
-        addOwnership(t, current, t.sortedTokens.firstKey(), ownership);
-
-        return ownership;
-    }
-
-    private static void addOwnership(ReplicationAwareTokenAllocator<Unit> t, Token current, Token next, Map<Unit, Double> ownership)
-    {
-        TestReplicationStrategy ts = (TestReplicationStrategy) t.strategy;
-        double size = current.size(next);
-        Token representative = t.partitioner.midpoint(current, next);
-        for (Unit n : ts.getReplicas(representative, t.sortedTokens))
-        {
-            Double v = ownership.get(n);
-            ownership.put(n, v != null ? v + size : size);
-        }
-    }
-
-    private static double replicatedTokenOwnership(Token token, NavigableMap<Token, Unit> sortedTokens, ReplicationStrategy<Unit> strategy)
-    {
-        TestReplicationStrategy ts = (TestReplicationStrategy) strategy;
-        Token next = sortedTokens.higherKey(token);
-        if (next == null)
-            next = sortedTokens.firstKey();
-        return ts.replicationStart(token, sortedTokens.get(token), sortedTokens).size(next);
-    }
-
-    static interface TokenCount
-    {
-        int tokenCount(int perUnitCount, Random rand);
-
-        double spreadExpectation();
-    }
-
-    static TokenCount fixedTokenCount = new TokenCount()
-    {
-        public int tokenCount(int perUnitCount, Random rand)
-        {
-            return perUnitCount;
-        }
-
-        public double spreadExpectation()
-        {
-            return 4;  // High tolerance to avoid flakiness.
-        }
-    };
-
-    static TokenCount varyingTokenCount = new TokenCount()
-    {
-        public int tokenCount(int perUnitCount, Random rand)
-        {
-            if (perUnitCount == 1) return 1;
-            // 25 to 175%
-            return rand.nextInt(perUnitCount * 3 / 2) + (perUnitCount + 3) / 4;
-        }
-
-        public double spreadExpectation()
-        {
-            return 8;  // High tolerance to avoid flakiness.
-        }
-    };
-
-    Murmur3Partitioner partitioner = new Murmur3Partitioner();
-    Random seededRand = new Random(2);
-
-    private void random(Map<Token, Unit> map, TestReplicationStrategy rs, int unitCount, TokenCount tc, int perUnitCount)
-    {
-        System.out.format("\nRandom generation of %d units with %d tokens each\n", unitCount, perUnitCount);
-        Random rand = seededRand;
-        for (int i = 0; i < unitCount; i++)
-        {
-            Unit unit = new Unit();
-            rs.addUnit(unit);
-            int tokens = tc.tokenCount(perUnitCount, rand);
-            for (int j = 0; j < tokens; j++)
-            {
-                map.put(partitioner.getRandomToken(rand), unit);
-            }
-        }
-    }
-
-    @Test
-    public void testExistingCluster()
-    {
-        for (int rf = 1; rf <= 5; ++rf)
-        {
-            for (int perUnitCount = 1; perUnitCount <= MAX_VNODE_COUNT; perUnitCount *= 4)
-            {
-                testExistingCluster(perUnitCount, fixedTokenCount, new SimpleReplicationStrategy(rf));
-                testExistingCluster(perUnitCount, varyingTokenCount, new SimpleReplicationStrategy(rf));
-                if (rf == 1) continue;  // Replication strategy doesn't matter for RF = 1.
-                for (int groupSize = 4; groupSize <= 64 && groupSize * rf * 4 < TARGET_CLUSTER_SIZE; groupSize *= 4)
-                {
-                    testExistingCluster(perUnitCount, fixedTokenCount, new BalancedGroupReplicationStrategy(rf, groupSize));
-                    testExistingCluster(perUnitCount, varyingTokenCount, new UnbalancedGroupReplicationStrategy(rf, groupSize / 2, groupSize * 2, seededRand));
-                }
-                testExistingCluster(perUnitCount, fixedTokenCount, new FixedGroupCountReplicationStrategy(rf, rf * 2));
-            }
-        }
-    }
-
-    public void testExistingCluster(int perUnitCount, TokenCount tc, TestReplicationStrategy rs)
-    {
-        System.out.println("Testing existing cluster, target " + perUnitCount + " vnodes, replication " + rs);
-        final int targetClusterSize = TARGET_CLUSTER_SIZE;
-        NavigableMap<Token, Unit> tokenMap = Maps.newTreeMap();
-
-        random(tokenMap, rs, targetClusterSize / 2, tc, perUnitCount);
-
-        ReplicationAwareTokenAllocator<Unit> t = new ReplicationAwareTokenAllocator<>(tokenMap, rs, partitioner);
-        grow(t, targetClusterSize * 9 / 10, tc, perUnitCount, false);
-        grow(t, targetClusterSize, tc, perUnitCount, true);
-        loseAndReplace(t, targetClusterSize / 10, tc, perUnitCount);
-        System.out.println();
-    }
-
-    @Test
-    public void testNewCluster()
-    {
-        Util.flakyTest(this::flakyTestNewCluster,
-                       5,
-                       "It tends to fail sometimes due to the random selection of the tokens in the first few nodes.");
-    }
-
-    public void flakyTestNewCluster()
-    {
-        // This test is flaky because the selection of the tokens for the first RF nodes (which is random, with an
-        // uncontrolled seed) can sometimes cause a pathological situation where the algorithm will find a (close to)
-        // ideal distribution of tokens for some number of nodes, which in turn will inevitably cause it to go into a
-        // bad (unacceptable to the test criteria) distribution after adding one more node.
-
-        // This should happen very rarely, unless something is broken in the token allocation code.
-
-        for (int rf = 2; rf <= 5; ++rf)
-        {
-            for (int perUnitCount = 1; perUnitCount <= MAX_VNODE_COUNT; perUnitCount *= 4)
-            {
-                testNewCluster(perUnitCount, fixedTokenCount, new SimpleReplicationStrategy(rf));
-                testNewCluster(perUnitCount, varyingTokenCount, new SimpleReplicationStrategy(rf));
-                if (rf == 1) continue;  // Replication strategy doesn't matter for RF = 1.
-                for (int groupSize = 4; groupSize <= 64 && groupSize * rf * 8 < TARGET_CLUSTER_SIZE; groupSize *= 4)
-                {
-                    testNewCluster(perUnitCount, fixedTokenCount, new BalancedGroupReplicationStrategy(rf, groupSize));
-                    testNewCluster(perUnitCount, varyingTokenCount, new UnbalancedGroupReplicationStrategy(rf, groupSize / 2, groupSize * 2, seededRand));
-                }
-                testNewCluster(perUnitCount, fixedTokenCount, new FixedGroupCountReplicationStrategy(rf, rf * 2));
-            }
-        }
-    }
-
-    public void testNewCluster(int perUnitCount, TokenCount tc, TestReplicationStrategy rs)
-    {
-        System.out.println("Testing new cluster, target " + perUnitCount + " vnodes, replication " + rs);
-        final int targetClusterSize = TARGET_CLUSTER_SIZE;
-        NavigableMap<Token, Unit> tokenMap = Maps.newTreeMap();
-
-        ReplicationAwareTokenAllocator<Unit> t = new ReplicationAwareTokenAllocator<>(tokenMap, rs, partitioner);
-        grow(t, targetClusterSize * 2 / 5, tc, perUnitCount, false);
-        grow(t, targetClusterSize, tc, perUnitCount, true);
-        loseAndReplace(t, targetClusterSize / 5, tc, perUnitCount);
-        System.out.println();
-    }
-
-    private void loseAndReplace(ReplicationAwareTokenAllocator<Unit> t, int howMany, TokenCount tc, int perUnitCount)
-    {
-        int fullCount = t.unitCount();
-        System.out.format("Losing %d units. ", howMany);
-        for (int i = 0; i < howMany; ++i)
-        {
-            Unit u = t.unitFor(partitioner.getRandomToken(seededRand));
-            t.removeUnit(u);
-            ((TestReplicationStrategy) t.strategy).removeUnit(u);
-        }
-        // Grow half without verifying.
-        grow(t, (t.unitCount() + fullCount * 3) / 4, tc, perUnitCount, false);
-        // Metrics should be back to normal by now. Check that they remain so.
-        grow(t, fullCount, tc, perUnitCount, true);
-    }
-
-    static class Summary
-    {
-        double min = 1;
-        double max = 1;
-        double stddev = 0;
-
-        void update(SummaryStatistics stat)
-        {
-            min = Math.min(min, stat.getMin());
-            max = Math.max(max, stat.getMax());
-            stddev = Math.max(stddev, stat.getStandardDeviation());
-        }
-
-        public String toString()
-        {
-            return String.format("max %.2f min %.2f stddev %.4f", max, min, stddev);
-        }
-    }
-
-    public void grow(ReplicationAwareTokenAllocator<Unit> t, int targetClusterSize, TokenCount tc, int perUnitCount, boolean verifyMetrics)
-    {
-        int size = t.unitCount();
-        Summary su = new Summary();
-        Summary st = new Summary();
-        Random rand = new Random(targetClusterSize + perUnitCount);
-        TestReplicationStrategy strategy = (TestReplicationStrategy) t.strategy;
-        if (size < targetClusterSize)
-        {
-            System.out.format("Adding %d unit(s) using %s...", targetClusterSize - size, t.toString());
-            long time = System.currentTimeMillis();
-            while (size < targetClusterSize)
-            {
-                int tokens = tc.tokenCount(perUnitCount, rand);
-                Unit unit = new Unit();
-                strategy.addUnit(unit);
-                t.addUnit(unit, tokens);
-                ++size;
-                if (verifyMetrics)
-                    updateSummary(t, su, st, false);
-            }
-            System.out.format(" Done in %.3fs\n", (System.currentTimeMillis() - time) / 1000.0);
-            if (verifyMetrics)
-            {
-                updateSummary(t, su, st, true);
-                double maxExpected = 1.0 + tc.spreadExpectation() * strategy.spreadExpectation() / (perUnitCount * t.replicas);
-                if (su.max > maxExpected)
-                {
-                    Assert.fail(String.format("Expected max unit size below %.4f, was %.4f", maxExpected, su.max));
-                }
-                // We can't verify lower side range as small loads can't always be fixed.
-            }
-        }
-    }
-
-
-    private void updateSummary(ReplicationAwareTokenAllocator<Unit> t, Summary su, Summary st, boolean print)
-    {
-        int size = t.sortedTokens.size();
-        double inverseAverage = 1.0 * size / t.strategy.replicas();
-
-        Map<Unit, Double> ownership = evaluateReplicatedOwnership(t);
-        SummaryStatistics unitStat = new SummaryStatistics();
-        for (Map.Entry<Unit, Double> en : ownership.entrySet())
-            unitStat.addValue(en.getValue() * inverseAverage / t.unitToTokens.get(en.getKey()).size());
-        su.update(unitStat);
-
-        SummaryStatistics tokenStat = new SummaryStatistics();
-        for (Token tok : t.sortedTokens.keySet())
-            tokenStat.addValue(replicatedTokenOwnership(tok, t.sortedTokens, t.strategy) * inverseAverage);
-        st.update(tokenStat);
-
-        if (print)
-        {
-            System.out.format("Size %d(%d)   \tunit %s  token %s   %s\n",
-                              t.unitCount(), size,
-                              mms(unitStat),
-                              mms(tokenStat),
-                              t.strategy);
-            System.out.format("Worst intermediate unit\t%s  token %s\n", su, st);
-        }
-    }
-
-
-    private static String mms(SummaryStatistics s)
-    {
-        return String.format("max %.2f min %.2f stddev %.4f", s.getMax(), s.getMin(), s.getStandardDeviation());
-    }
-
-
-    int nextUnitId = 0;
-
-    final class Unit implements Comparable<Unit>
-    {
-        int unitId = nextUnitId++;
-
-        public String toString()
-        {
-            return Integer.toString(unitId);
-        }
-
-        @Override
-        public int compareTo(Unit o)
-        {
-            return Integer.compare(unitId, o.unitId);
-        }
-    }
-}
\ No newline at end of file
diff --git a/test/long/org/apache/cassandra/dht/tokenallocator/TokenAllocatorTestBase.java b/test/long/org/apache/cassandra/dht/tokenallocator/TokenAllocatorTestBase.java
new file mode 100644
index 0000000..8612ac1
--- /dev/null
+++ b/test/long/org/apache/cassandra/dht/tokenallocator/TokenAllocatorTestBase.java
@@ -0,0 +1,160 @@
+/*
+ * 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.dht.tokenallocator;
+
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Random;
+
+import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
+
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Token;
+
+/**
+ * Base class for {@link NoReplicationTokenAllocatorTest} and {@link AbstractReplicationAwareTokenAllocatorTest},
+ */
+abstract class TokenAllocatorTestBase
+{
+    protected static final int TARGET_CLUSTER_SIZE = 250;
+    protected static final int MAX_VNODE_COUNT = 64;
+
+    interface TestReplicationStrategy extends ReplicationStrategy<Unit>
+    {
+        void addUnit(Unit n);
+
+        void removeUnit(Unit n);
+
+        /**
+         * Returns a list of all replica units for given token.
+         */
+        List<Unit> getReplicas(Token token, NavigableMap<Token, Unit> sortedTokens);
+
+        /**
+         * Returns the start of the token span that is replicated in this token.
+         * Note: Though this is not trivial to see, the replicated span is always contiguous. A token in the same
+         * group acts as a barrier; if one is not found the token replicates everything up to the replica'th distinct
+         * group seen in front of it.
+         */
+        Token replicationStart(Token token, Unit unit, NavigableMap<Token, Unit> sortedTokens);
+
+        /**
+         * Multiplier for the acceptable disbalance in the cluster. With some strategies it is harder to achieve good
+         * results.
+         */
+        double spreadExpectation();
+    }
+
+    interface TokenCount
+    {
+        int tokenCount(int perUnitCount, Random rand);
+
+        double spreadExpectation();
+    }
+
+    TokenCount fixedTokenCount = new TokenCount()
+    {
+        public int tokenCount(int perUnitCount, Random rand)
+        {
+            return perUnitCount;
+        }
+
+        public double spreadExpectation()
+        {
+            return 4;  // High tolerance to avoid flakiness.
+        }
+    };
+
+    TokenCount varyingTokenCount = new TokenCount()
+    {
+        public int tokenCount(int perUnitCount, Random rand)
+        {
+            if (perUnitCount == 1) return 1;
+            // 25 to 175%
+            return rand.nextInt(perUnitCount * 3 / 2) + (perUnitCount + 3) / 4;
+        }
+
+        public double spreadExpectation()
+        {
+            return 8;  // High tolerance to avoid flakiness.
+        }
+    };
+
+    Random seededRand = new Random(2);
+
+    public void random(Map<Token, Unit> map, TestReplicationStrategy rs,
+                              int unitCount, TokenCount tc, int perUnitCount, IPartitioner partitioner)
+    {
+        System.out.format("\nRandom generation of %d units with %d tokens each\n", unitCount, perUnitCount);
+        Random rand = seededRand;
+        for (int i = 0; i < unitCount; i++)
+        {
+            Unit unit = new Unit();
+            rs.addUnit(unit);
+            int tokens = tc.tokenCount(perUnitCount, rand);
+            for (int j = 0; j < tokens; j++)
+            {
+                map.put(partitioner.getRandomToken(rand), unit);
+            }
+        }
+    }
+
+    public String mms(SummaryStatistics s)
+    {
+        return String.format("max %.2f min %.2f stddev %.4f", s.getMax(), s.getMin(), s.getStandardDeviation());
+    }
+
+    class Summary
+    {
+        double min = 1;
+        double max = 1;
+        double stddev = 0;
+
+        void update(SummaryStatistics stat)
+        {
+            min = Math.min(min, stat.getMin());
+            max = Math.max(max, stat.getMax());
+            stddev = Math.max(stddev, stat.getStandardDeviation());
+        }
+
+        public String toString()
+        {
+            return String.format("max %.2f min %.2f stddev %.4f", max, min, stddev);
+        }
+    }
+
+    int nextUnitId = 0;
+
+    final class Unit implements Comparable<Unit>
+    {
+        int unitId = nextUnitId++;
+
+        public String toString()
+        {
+            return Integer.toString(unitId);
+        }
+
+        @Override
+        public int compareTo(Unit o)
+        {
+            return Integer.compare(unitId, o.unitId);
+        }
+    }
+}
diff --git a/test/long/org/apache/cassandra/io/compress/CompressorPerformance.java b/test/long/org/apache/cassandra/io/compress/CompressorPerformance.java
index 17122f5..e703839 100644
--- a/test/long/org/apache/cassandra/io/compress/CompressorPerformance.java
+++ b/test/long/org/apache/cassandra/io/compress/CompressorPerformance.java
@@ -23,6 +23,7 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.util.Collections;
 import java.util.concurrent.ThreadLocalRandom;
 
 public class CompressorPerformance
@@ -33,7 +34,7 @@
         for (ICompressor compressor: new ICompressor[] {
                 SnappyCompressor.instance,  // warm up
                 DeflateCompressor.instance,
-                LZ4Compressor.instance,
+                LZ4Compressor.create(Collections.emptyMap()),
                 SnappyCompressor.instance
         })
         {
diff --git a/test/long/org/apache/cassandra/io/sstable/CQLSSTableWriterLongTest.java b/test/long/org/apache/cassandra/io/sstable/CQLSSTableWriterLongTest.java
index b48336f..9674ca3 100644
--- a/test/long/org/apache/cassandra/io/sstable/CQLSSTableWriterLongTest.java
+++ b/test/long/org/apache/cassandra/io/sstable/CQLSSTableWriterLongTest.java
@@ -41,12 +41,6 @@
         StorageService.instance.initServer();
     }
 
-    @AfterClass
-    public static void tearDown()
-    {
-        Config.setClientMode(false);
-    }
-
     @Test
     public void testWideRow() throws Exception
     {
diff --git a/test/long/org/apache/cassandra/locator/DynamicEndpointSnitchLongTest.java b/test/long/org/apache/cassandra/locator/DynamicEndpointSnitchLongTest.java
index 0d66fa9..35bf5b4 100644
--- a/test/long/org/apache/cassandra/locator/DynamicEndpointSnitchLongTest.java
+++ b/test/long/org/apache/cassandra/locator/DynamicEndpointSnitchLongTest.java
@@ -31,10 +31,13 @@
 
 import org.apache.cassandra.utils.FBUtilities;
 
-import static org.junit.Assert.assertEquals;
-
 public class DynamicEndpointSnitchLongTest
 {
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testConcurrency() throws InterruptedException, IOException, ConfigurationException
     {
diff --git a/test/long/org/apache/cassandra/repair/CompactionValidationTest.java b/test/long/org/apache/cassandra/repair/CompactionValidationTest.java
new file mode 100644
index 0000000..d5b7559
--- /dev/null
+++ b/test/long/org/apache/cassandra/repair/CompactionValidationTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.repair;
+
+import java.net.InetAddress;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.compaction.CompactionManager;
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.net.IMessageSink;
+import org.apache.cassandra.net.MessageIn;
+import org.apache.cassandra.net.MessageOut;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.service.ActiveRepairService;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.UUIDGen;
+
+import static java.lang.String.format;
+import static java.util.stream.Collectors.toList;
+import static org.apache.cassandra.db.compaction.CompactionsTest.populate;
+import static org.junit.Assert.assertTrue;
+
+public class CompactionValidationTest
+{
+
+    private static final String keyspace = "ThrottlingCompactionValidationTest";
+    private static final String columnFamily = "Standard1";
+    private static final int ROWS = 500_000;
+
+    private ColumnFamilyStore cfs;
+
+    @BeforeClass
+    public static void defineSchema()
+    {
+        SchemaLoader.prepareServer();
+        SchemaLoader.createKeyspace(keyspace,
+                                    KeyspaceParams.simple(1),
+                                    SchemaLoader.standardCFMD(keyspace, columnFamily));
+    }
+
+    @Before
+    public void setup()
+    {
+        MessagingService.instance().addMessageSink(new IMessageSink()
+        {
+            public boolean allowOutgoingMessage(MessageOut message, int id, InetAddress to)
+            {
+                return false;
+            }
+
+            public boolean allowIncomingMessage(MessageIn message, int id)
+            {
+                return false;
+            }
+        });
+
+        cfs = Keyspace.open(keyspace).getColumnFamilyStore(columnFamily);
+
+        cfs.disableAutoCompaction();
+
+        populate(keyspace, columnFamily, 0, ROWS, 0);
+
+        cfs.forceBlockingFlush();
+    }
+
+    @After
+    public void tearDown()
+    {
+        MessagingService.instance().clearMessageSinks();
+        // set it back how it was
+        DatabaseDescriptor.setCompactionThroughputMbPerSec(0);
+    }
+
+    @Test
+    public void throttledValidationIsSlowerThanUnthrottledValidationTest() throws Exception
+    {
+        // unthrottle, validate as fast as possible
+        DatabaseDescriptor.setCompactionThroughputMbPerSec(0);
+        final long unthrottledRuntime = executeValidation();
+
+        // throttle to 1 MBPS
+        DatabaseDescriptor.setCompactionThroughputMbPerSec(1);
+        final long throttledRuntime = executeValidation();
+
+        // throttle to 2 MBPS
+        DatabaseDescriptor.setCompactionThroughputMbPerSec(2);
+        final long throttledRuntime2 = executeValidation();
+
+        assertTrue(format("Validation compaction with throttled throughtput to 1 Mbps took less time (in ms) than unthrottled validation compaction: %s vs. %s",
+                          throttledRuntime, unthrottledRuntime),
+                   throttledRuntime > unthrottledRuntime);
+
+        assertTrue(format("Validation compaction with throttled throughtput on 1 Mbps took less time (in ms) than throttled validation compaction to 2 Mbps: %s vs. %s",
+                          throttledRuntime, throttledRuntime2),
+                   throttledRuntime > throttledRuntime2);
+    }
+
+    private long executeValidation() throws Exception
+    {
+        final UUID repairSessionId = UUIDGen.getTimeUUID();
+
+        final List<Range<Token>> ranges = Stream.of(cfs.getLiveSSTables().iterator().next())
+                                                .map(sstable -> new Range<>(sstable.first.getToken(), sstable.last.getToken()))
+                                                .collect(toList());
+
+        final RepairJobDesc repairJobDesc = new RepairJobDesc(repairSessionId,
+                                                              UUIDGen.getTimeUUID(),
+                                                              cfs.keyspace.getName(),
+                                                              cfs.getTableName(),
+                                                              ranges);
+
+        ActiveRepairService.instance.registerParentRepairSession(repairSessionId,
+                                                                 FBUtilities.getBroadcastAddress(),
+                                                                 Collections.singletonList(cfs),
+                                                                 repairJobDesc.ranges,
+                                                                 false,
+                                                                 ActiveRepairService.UNREPAIRED_SSTABLE,
+                                                                 false);
+
+        final Validator validator = new Validator(repairJobDesc, FBUtilities.getBroadcastAddress(), 0, true);
+
+        final long start = System.currentTimeMillis();
+
+        CompactionManager.instance.submitValidation(cfs, validator).get();
+
+        return System.currentTimeMillis() - start;
+    }
+}
diff --git a/test/long/org/apache/cassandra/streaming/LongStreamingTest.java b/test/long/org/apache/cassandra/streaming/LongStreamingTest.java
new file mode 100644
index 0000000..1340224
--- /dev/null
+++ b/test/long/org/apache/cassandra/streaming/LongStreamingTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.streaming;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.io.Files;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.io.sstable.CQLSSTableWriter;
+import org.apache.cassandra.io.sstable.SSTableLoader;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.OutputHandler;
+
+import static org.junit.Assert.assertEquals;
+
+public class LongStreamingTest
+{
+    @BeforeClass
+    public static void setup() throws Exception
+    {
+        DatabaseDescriptor.daemonInitialization();
+
+        SchemaLoader.cleanupAndLeaveDirs();
+        Keyspace.setInitialized();
+        StorageService.instance.initServer();
+
+        StorageService.instance.setCompactionThroughputMbPerSec(0);
+        StorageService.instance.setStreamThroughputMbPerSec(0);
+        StorageService.instance.setInterDCStreamThroughputMbPerSec(0);
+    }
+
+    @Test
+    public void testCompressedStream() throws InvalidRequestException, IOException, ExecutionException, InterruptedException
+    {
+        String KS = "cql_keyspace";
+        String TABLE = "table1";
+
+        File tempdir = Files.createTempDir();
+        File dataDir = new File(tempdir.getAbsolutePath() + File.separator + KS + File.separator + TABLE);
+        assert dataDir.mkdirs();
+
+        String schema = "CREATE TABLE cql_keyspace.table1 ("
+                        + "  k int PRIMARY KEY,"
+                        + "  v1 text,"
+                        + "  v2 int"
+                        + ");";// with compression = {};";
+        String insert = "INSERT INTO cql_keyspace.table1 (k, v1, v2) VALUES (?, ?, ?)";
+        CQLSSTableWriter writer = CQLSSTableWriter.builder()
+                                                  .sorted()
+                                                  .inDirectory(dataDir)
+                                                  .forTable(schema)
+                                                  .using(insert).build();
+        long start = System.nanoTime();
+
+        for (int i = 0; i < 10_000_000; i++)
+            writer.addRow(i, "test1", 24);
+
+        writer.close();
+        System.err.println(String.format("Writer finished after %d seconds....", TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start)));
+
+        File[] dataFiles = dataDir.listFiles((dir, name) -> name.endsWith("-Data.db"));
+        long dataSize = 0l;
+        for (File file : dataFiles)
+        {
+            System.err.println("File : "+file.getAbsolutePath());
+            dataSize += file.length();
+        }
+
+        SSTableLoader loader = new SSTableLoader(dataDir, new SSTableLoader.Client()
+        {
+            private String ks;
+            public void init(String keyspace)
+            {
+                for (Range<Token> range : StorageService.instance.getLocalRanges("cql_keyspace"))
+                    addRangeForEndpoint(range, FBUtilities.getBroadcastAddress());
+
+                this.ks = keyspace;
+            }
+
+            public CFMetaData getTableMetadata(String cfName)
+            {
+                return Schema.instance.getCFMetaData(ks, cfName);
+            }
+        }, new OutputHandler.SystemOutput(false, false));
+
+        start = System.nanoTime();
+        loader.stream().get();
+
+        long millis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+        System.err.println(String.format("Finished Streaming in %.2f seconds: %.2f Mb/sec",
+                                         millis/1000d,
+                                         (dataSize / (1 << 20) / (millis / 1000d)) * 8));
+
+
+        //Stream again
+        loader = new SSTableLoader(dataDir, new SSTableLoader.Client()
+        {
+            private String ks;
+            public void init(String keyspace)
+            {
+                for (Range<Token> range : StorageService.instance.getLocalRanges("cql_keyspace"))
+                    addRangeForEndpoint(range, FBUtilities.getBroadcastAddress());
+
+                this.ks = keyspace;
+            }
+
+            public CFMetaData getTableMetadata(String cfName)
+            {
+                return Schema.instance.getCFMetaData(ks, cfName);
+            }
+        }, new OutputHandler.SystemOutput(false, false));
+
+        start = System.nanoTime();
+        loader.stream().get();
+
+        millis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+        System.err.println(String.format("Finished Streaming in %.2f seconds: %.2f Mb/sec",
+                                         millis/1000d,
+                                         (dataSize / (1 << 20) / (millis / 1000d)) * 8));
+
+
+        //Compact them both
+        start = System.nanoTime();
+        Keyspace.open(KS).getColumnFamilyStore(TABLE).forceMajorCompaction();
+        millis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+
+        System.err.println(String.format("Finished Compacting in %.2f seconds: %.2f Mb/sec",
+                                         millis / 1000d,
+                                         (dataSize * 2 / (1 << 20) / (millis / 1000d)) * 8));
+
+        UntypedResultSet rs = QueryProcessor.executeInternal("SELECT * FROM cql_keyspace.table1 limit 100;");
+        assertEquals(100, rs.size());
+    }
+}
diff --git a/test/microbench/org/apache/cassandra/test/microbench/BTreeBuildBench.java b/test/microbench/org/apache/cassandra/test/microbench/BTreeBuildBench.java
new file mode 100644
index 0000000..0d89ebb
--- /dev/null
+++ b/test/microbench/org/apache/cassandra/test/microbench/BTreeBuildBench.java
@@ -0,0 +1,96 @@
+/*
+ * 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.test.microbench;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cassandra.utils.btree.BTree;
+import org.apache.cassandra.utils.btree.UpdateFunction;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 4, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 8, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 2)
+@Threads(4)
+@State(Scope.Benchmark)
+public class BTreeBuildBench
+{
+    private List<Integer> data;
+
+    @Param({"1", "2", "5", "10", "20", "40", "100", "1000", "10000", "100000"})
+    int dataSize;
+
+    private static final Comparator<Integer> CMP = new Comparator<Integer>()
+    {
+        public int compare(Integer o1, Integer o2)
+        {
+            return Integer.compare(o1, o2);
+        }
+    };
+
+    @Setup(Level.Trial)
+    public void setup()
+    {
+        data = new ArrayList<>(dataSize);
+        for (int i = 0 ; i < dataSize; i++)
+            data.add(i);
+    }
+
+    private int buildTree(List<Integer> data)
+    {
+        Object[] btree = BTree.build(data, UpdateFunction.noOp());
+        // access the btree to avoid java optimized out this code
+        return BTree.size(btree);
+    }
+
+    private int treeBuilderAddAll(List<Integer> data)
+    {
+        BTree.Builder<Integer> builder = BTree.builder(Comparator.naturalOrder());
+        Object[] btree = builder.addAll(data).build();
+        return BTree.size(btree);
+    }
+
+    @Benchmark
+    public int treeBuilderRecycleAdd()
+    {
+        BTree.Builder<Integer> builder = BTree.builder(Comparator.naturalOrder());
+        builder.auto(false);
+        for (Integer v : data)
+            builder.add(v);
+        Object[] btree = builder.build();
+        return BTree.size(btree);
+    }
+}
diff --git a/test/microbench/org/apache/cassandra/test/microbench/BatchStatementBench.java b/test/microbench/org/apache/cassandra/test/microbench/BatchStatementBench.java
new file mode 100644
index 0000000..2a4e1fb
--- /dev/null
+++ b/test/microbench/org/apache/cassandra/test/microbench/BatchStatementBench.java
@@ -0,0 +1,147 @@
+/*
+ * 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.test.microbench;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.collect.Lists;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.cql3.Attributes;
+import org.apache.cassandra.cql3.BatchQueryOptions;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.statements.BatchStatement;
+import org.apache.cassandra.cql3.statements.ModificationStatement;
+import org.apache.cassandra.cql3.statements.ParsedStatement;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.service.QueryState;
+import org.apache.cassandra.utils.FBUtilities;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.profile.GCProfiler;
+import org.openjdk.jmh.results.Result;
+import org.openjdk.jmh.results.RunResult;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
+
+
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 1,jvmArgsAppend = "-Xmx512M")
+@Threads(1)
+@State(Scope.Benchmark)
+public class BatchStatementBench
+{
+    static
+    {
+        DatabaseDescriptor.clientInitialization();
+        // Partitioner is not set in client mode.
+        if (DatabaseDescriptor.getPartitioner() == null)
+            DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
+    }
+
+    static String keyspace = "keyspace1";
+    String table = "tbl";
+
+    int nowInSec = FBUtilities.nowInSeconds();
+    long queryStartTime = System.nanoTime();
+    BatchStatement bs;
+    BatchQueryOptions bqo;
+
+    @Param({"true", "false"})
+    boolean uniquePartition;
+
+    @Param({"10000"})
+    int batchSize;
+
+    @Setup
+    public void setup() throws Throwable
+    {
+        Schema.instance.load(KeyspaceMetadata.create(keyspace, KeyspaceParams.simple(1)));
+        KeyspaceMetadata ksm = Schema.instance.getKSMetaData(keyspace);
+        CFMetaData metadata = CFMetaData.compile(String.format("CREATE TABLE %s (id int, ck int, v int, primary key (id, ck))", table), keyspace);
+
+        Schema.instance.load(metadata);
+        Schema.instance.setKeyspaceMetadata(ksm.withSwapped(ksm.tables.with(metadata)));
+
+        List<ModificationStatement> modifications = new ArrayList<>(batchSize);
+        List<List<ByteBuffer>> parameters = new ArrayList<>(batchSize);
+        List<Object> queryOrIdList = new ArrayList<>(batchSize);
+        ParsedStatement.Prepared prepared = QueryProcessor.parseStatement(String.format("INSERT INTO %s.%s (id, ck, v) VALUES (?,?,?)", keyspace, table), QueryState.forInternalCalls());
+
+        for (int i = 0; i < batchSize; i++)
+        {
+            modifications.add((ModificationStatement) prepared.statement);
+            parameters.add(Lists.newArrayList(bytes(uniquePartition ? i : 1), bytes(i), bytes(i)));
+            queryOrIdList.add(prepared.rawCQLStatement);
+        }
+        bs = new BatchStatement(3, BatchStatement.Type.UNLOGGED, modifications, Attributes.none());
+        bqo = BatchQueryOptions.withPerStatementVariables(QueryOptions.DEFAULT, parameters, queryOrIdList);
+    }
+
+    @Benchmark
+    public void bench()
+    {
+        bs.getMutations(bqo, false, nowInSec, queryStartTime);
+    }
+
+
+    public static void main(String... args) throws Exception {
+        Options opts = new OptionsBuilder()
+                       .include(".*"+BatchStatementBench.class.getSimpleName()+".*")
+                       .jvmArgs("-server")
+                       .forks(1)
+                       .mode(Mode.Throughput)
+                       .addProfiler(GCProfiler.class)
+                       .build();
+
+        Collection<RunResult> records = new Runner(opts).run();
+        for ( RunResult result : records) {
+            Result r = result.getPrimaryResult();
+            System.out.println("API replied benchmark score: "
+                               + r.getScore() + " "
+                               + r.getScoreUnit() + " over "
+                               + r.getStatistics().getN() + " iterations");
+        }
+    }
+}
diff --git a/test/microbench/org/apache/cassandra/test/microbench/CachingBenchTest.java b/test/microbench/org/apache/cassandra/test/microbench/CachingBenchTest.java
new file mode 100644
index 0000000..4af7a3b
--- /dev/null
+++ b/test/microbench/org/apache/cassandra/test/microbench/CachingBenchTest.java
@@ -0,0 +1,378 @@
+/*
+ * 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.test.microbench;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Predicate;
+
+import com.google.common.collect.Iterables;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import junit.framework.Assert;
+import org.apache.cassandra.config.Config.CommitLogSync;
+import org.apache.cassandra.config.Config.DiskAccessMode;
+import org.apache.cassandra.cache.ChunkCache;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.compaction.CompactionManager;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.Unfiltered;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.io.sstable.ISSTableScanner;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.FBUtilities;
+
+public class CachingBenchTest extends CQLTester
+{
+    private static final String STRATEGY = "LeveledCompactionStrategy";
+
+    private static final int DEL_SECTIONS = 1000;
+    private static final int FLUSH_FREQ = 10000;
+    private static final int SCAN_FREQUENCY_INV = 12000;
+    static final int COUNT = 29000;
+    static final int ITERS = 9;
+
+    static final int KEY_RANGE = 30;
+    static final int CLUSTERING_RANGE = 210000;
+
+    static final int EXTRA_SIZE = 1025;
+    static final boolean CONCURRENT_COMPACTIONS = true;
+
+    // The name of this method is important!
+    // CommitLog settings must be applied before CQLTester sets up; by using the same name as its @BeforeClass method we
+    // are effectively overriding it.
+    @BeforeClass
+    public static void setUpClass()
+    {
+        DatabaseDescriptor.setCommitLogSync(CommitLogSync.periodic);
+        DatabaseDescriptor.setCommitLogSyncPeriod(100);
+        CQLTester.setUpClass();
+    }
+    
+    String hashQuery;
+
+    @Before
+    public void before() throws Throwable
+    {
+        createTable("CREATE TABLE %s(" +
+                    "  key int," +
+                    "  column int," +
+                    "  data int," +
+                    "  extra text," +
+                    "  PRIMARY KEY(key, column)" +
+                    ")"
+                   );
+
+        String hashIFunc = parseFunctionName(createFunction(KEYSPACE, "int, int",
+                " CREATE FUNCTION %s (state int, val int)" +
+                " CALLED ON NULL INPUT" +
+                " RETURNS int" +
+                " LANGUAGE java" +
+                " AS 'return val != null ? state * 17 + val : state;'")).name;
+        String hashTFunc = parseFunctionName(createFunction(KEYSPACE, "int, text",
+                " CREATE FUNCTION %s (state int, val text)" +
+                " CALLED ON NULL INPUT" +
+                " RETURNS int" +
+                " LANGUAGE java" +
+                " AS 'return val != null ? state * 17 + val.hashCode() : state;'")).name;
+
+        String hashInt = createAggregate(KEYSPACE, "int",
+                " CREATE AGGREGATE %s (int)" +
+                " SFUNC " + hashIFunc +
+                " STYPE int" +
+                " INITCOND 1");
+        String hashText = createAggregate(KEYSPACE, "text",
+                " CREATE AGGREGATE %s (text)" +
+                " SFUNC " + hashTFunc +
+                " STYPE int" +
+                " INITCOND 1");
+
+        hashQuery = String.format("SELECT count(column), %s(key), %s(column), %s(data), %s(extra), avg(key), avg(column), avg(data) FROM %%s",
+                                  hashInt, hashInt, hashInt, hashText);
+    }
+    AtomicLong id = new AtomicLong();
+    long compactionTimeNanos = 0;
+
+    void pushData(Random rand, int count) throws Throwable
+    {
+        for (int i = 0; i < count; ++i)
+        {
+            long ii = id.incrementAndGet();
+            if (ii % 1000 == 0)
+                System.out.print('.');
+            int key = rand.nextInt(KEY_RANGE);
+            int column = rand.nextInt(CLUSTERING_RANGE);
+            execute("INSERT INTO %s (key, column, data, extra) VALUES (?, ?, ?, ?)", key, column, (int) ii, genExtra(rand));
+            maybeCompact(ii);
+        }
+    }
+
+    private String genExtra(Random rand)
+    {
+        StringBuilder builder = new StringBuilder(EXTRA_SIZE);
+        for (int i = 0; i < EXTRA_SIZE; ++i)
+            builder.append((char) ('a' + rand.nextInt('z' - 'a' + 1)));
+        return builder.toString();
+    }
+
+    void readAndDelete(Random rand, int count) throws Throwable
+    {
+        for (int i = 0; i < count; ++i)
+        {
+            int key;
+            UntypedResultSet res;
+            long ii = id.incrementAndGet();
+            if (ii % 1000 == 0)
+                System.out.print('-');
+            if (rand.nextInt(SCAN_FREQUENCY_INV) != 1)
+            {
+                do
+                {
+                    key = rand.nextInt(KEY_RANGE);
+                    long cid = rand.nextInt(DEL_SECTIONS);
+                    int cstart = (int) (cid * CLUSTERING_RANGE / DEL_SECTIONS);
+                    int cend = (int) ((cid + 1) * CLUSTERING_RANGE / DEL_SECTIONS);
+                    res = execute("SELECT column FROM %s WHERE key = ? AND column >= ? AND column < ? LIMIT 1", key, cstart, cend);
+                } while (res.size() == 0);
+                UntypedResultSet.Row r = Iterables.get(res, rand.nextInt(res.size()));
+                int clustering = r.getInt("column");
+                execute("DELETE FROM %s WHERE key = ? AND column = ?", key, clustering);
+            }
+            else
+            {
+                execute(hashQuery);
+            }
+            maybeCompact(ii);
+        }
+    }
+
+    private void maybeCompact(long ii)
+    {
+        if (ii % FLUSH_FREQ == 0)
+        {
+            System.out.print("F");
+            flush();
+            if (ii % (FLUSH_FREQ * 10) == 0)
+            {
+                System.out.println("C");
+                long startTime = System.nanoTime();
+                getCurrentColumnFamilyStore().enableAutoCompaction(!CONCURRENT_COMPACTIONS);
+                long endTime = System.nanoTime();
+                compactionTimeNanos += endTime - startTime;
+                getCurrentColumnFamilyStore().disableAutoCompaction();
+            }
+        }
+    }
+
+    public void testSetup(String compactionClass, String compressorClass, DiskAccessMode mode, boolean cacheEnabled) throws Throwable
+    {
+        id.set(0);
+        compactionTimeNanos = 0;
+        ChunkCache.instance.enable(cacheEnabled);
+        DatabaseDescriptor.setDiskAccessMode(mode);
+        alterTable("ALTER TABLE %s WITH compaction = { 'class' :  '" + compactionClass + "'  };");
+        alterTable("ALTER TABLE %s WITH compression = { 'sstable_compression' : '" + compressorClass + "'  };");
+        ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+        cfs.disableAutoCompaction();
+
+        long onStartTime = System.currentTimeMillis();
+        ExecutorService es = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
+        List<Future<?>> tasks = new ArrayList<>();
+        for (int ti = 0; ti < 1; ++ti)
+        {
+            Random rand = new Random(ti);
+            tasks.add(es.submit(() -> 
+            {
+                for (int i = 0; i < ITERS; ++i)
+                    try
+                    {
+                        pushData(rand, COUNT);
+                        readAndDelete(rand, COUNT / 3);
+                    }
+                    catch (Throwable e)
+                    {
+                        throw new AssertionError(e);
+                    }
+            }));
+        }
+        for (Future<?> task : tasks)
+            task.get();
+
+        flush();
+        long onEndTime = System.currentTimeMillis();
+        int startRowCount = countRows(cfs);
+        int startTombCount = countTombstoneMarkers(cfs);
+        int startRowDeletions = countRowDeletions(cfs);
+        int startTableCount = cfs.getLiveSSTables().size();
+        long startSize = SSTableReader.getTotalBytes(cfs.getLiveSSTables());
+        System.out.println("\nCompession: " + cfs.getCompressionParameters().toString());
+        System.out.println("Reader " + cfs.getLiveSSTables().iterator().next().getFileDataInput(0).toString());
+        if (cacheEnabled)
+            System.out.format("Cache size %s requests %,d hit ratio %f\n",
+                FileUtils.stringifyFileSize(ChunkCache.instance.metrics.size.getValue()),
+                ChunkCache.instance.metrics.requests.getCount(),
+                ChunkCache.instance.metrics.hitRate.getValue());
+        else
+        {
+            Assert.assertTrue("Chunk cache had requests: " + ChunkCache.instance.metrics.requests.getCount(), ChunkCache.instance.metrics.requests.getCount() < COUNT);
+            System.out.println("Cache disabled");
+        }
+        System.out.println(String.format("Operations completed in %.3fs", (onEndTime - onStartTime) * 1e-3));
+        if (!CONCURRENT_COMPACTIONS)
+            System.out.println(String.format(", out of which %.3f for non-concurrent compaction", compactionTimeNanos * 1e-9));
+        else
+            System.out.println();
+
+        String hashesBefore = getHashes();
+        long startTime = System.currentTimeMillis();
+        CompactionManager.instance.performMaximal(cfs, true);
+        long endTime = System.currentTimeMillis();
+
+        int endRowCount = countRows(cfs);
+        int endTombCount = countTombstoneMarkers(cfs);
+        int endRowDeletions = countRowDeletions(cfs);
+        int endTableCount = cfs.getLiveSSTables().size();
+        long endSize = SSTableReader.getTotalBytes(cfs.getLiveSSTables());
+
+        System.out.println(String.format("Major compaction completed in %.3fs",
+                (endTime - startTime) * 1e-3));
+        System.out.println(String.format("At start: %,12d tables %12s %,12d rows %,12d deleted rows %,12d tombstone markers",
+                startTableCount, FileUtils.stringifyFileSize(startSize), startRowCount, startRowDeletions, startTombCount));
+        System.out.println(String.format("At end:   %,12d tables %12s %,12d rows %,12d deleted rows %,12d tombstone markers",
+                endTableCount, FileUtils.stringifyFileSize(endSize), endRowCount, endRowDeletions, endTombCount));
+        String hashesAfter = getHashes();
+
+        Assert.assertEquals(hashesBefore, hashesAfter);
+    }
+
+    private String getHashes() throws Throwable
+    {
+        long startTime = System.currentTimeMillis();
+        String hashes = Arrays.toString(getRows(execute(hashQuery))[0]);
+        long endTime = System.currentTimeMillis();
+        System.out.println(String.format("Hashes: %s, retrieved in %.3fs", hashes, (endTime - startTime) * 1e-3));
+        return hashes;
+    }
+
+    @Test
+    public void testWarmup() throws Throwable
+    {
+        testSetup(STRATEGY, "LZ4Compressor", DiskAccessMode.mmap, false);
+    }
+
+    @Test
+    public void testLZ4CachedMmap() throws Throwable
+    {
+        testSetup(STRATEGY, "LZ4Compressor", DiskAccessMode.mmap, true);
+    }
+
+    @Test
+    public void testLZ4CachedStandard() throws Throwable
+    {
+        testSetup(STRATEGY, "LZ4Compressor", DiskAccessMode.standard, true);
+    }
+
+    @Test
+    public void testLZ4UncachedMmap() throws Throwable
+    {
+        testSetup(STRATEGY, "LZ4Compressor", DiskAccessMode.mmap, false);
+    }
+
+    @Test
+    public void testLZ4UncachedStandard() throws Throwable
+    {
+        testSetup(STRATEGY, "LZ4Compressor", DiskAccessMode.standard, false);
+    }
+
+    @Test
+    public void testCachedStandard() throws Throwable
+    {
+        testSetup(STRATEGY, "", DiskAccessMode.standard, true);
+    }
+
+    @Test
+    public void testUncachedStandard() throws Throwable
+    {
+        testSetup(STRATEGY, "", DiskAccessMode.standard, false);
+    }
+
+    @Test
+    public void testMmapped() throws Throwable
+    {
+        testSetup(STRATEGY, "", DiskAccessMode.mmap, false /* doesn't matter */);
+    }
+
+    int countTombstoneMarkers(ColumnFamilyStore cfs)
+    {
+        return count(cfs, x -> x.isRangeTombstoneMarker());
+    }
+
+    int countRowDeletions(ColumnFamilyStore cfs)
+    {
+        return count(cfs, x -> x.isRow() && !((Row) x).deletion().isLive());
+    }
+
+    int countRows(ColumnFamilyStore cfs)
+    {
+        boolean enforceStrictLiveness = cfs.metadata.enforceStrictLiveness();
+        int nowInSec = FBUtilities.nowInSeconds();
+        return count(cfs, x -> x.isRow() && ((Row) x).hasLiveData(nowInSec, enforceStrictLiveness));
+    }
+
+    private int count(ColumnFamilyStore cfs, Predicate<Unfiltered> predicate)
+    {
+        int count = 0;
+        for (SSTableReader reader : cfs.getLiveSSTables())
+            count += count(reader, predicate);
+        return count;
+    }
+
+    int count(SSTableReader reader, Predicate<Unfiltered> predicate)
+    {
+        int instances = 0;
+        try (ISSTableScanner partitions = reader.getScanner())
+        {
+            while (partitions.hasNext())
+            {
+                try (UnfilteredRowIterator iter = partitions.next())
+                {
+                    while (iter.hasNext())
+                    {
+                        Unfiltered atom = iter.next();
+                        if (predicate.test(atom))
+                            ++instances;
+                    }
+                }
+            }
+        }
+        return instances;
+    }
+}
diff --git a/test/microbench/org/apache/cassandra/test/microbench/CompactionBench.java b/test/microbench/org/apache/cassandra/test/microbench/CompactionBench.java
new file mode 100644
index 0000000..d8dfd66
--- /dev/null
+++ b/test/microbench/org/apache/cassandra/test/microbench/CompactionBench.java
@@ -0,0 +1,167 @@
+/*
+ * 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.test.microbench;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.*;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Uninterruptibles;
+
+import org.apache.cassandra.UpdateBuilder;
+import org.apache.cassandra.concurrent.StageManager;
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.statements.ParsedStatement;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.compaction.CompactionManager;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.io.util.DataOutputBufferFixed;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.net.MessageIn;
+import org.apache.cassandra.net.MessageOut;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.service.CassandraDaemon;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.transport.messages.ResultMessage;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.hadoop.util.bloom.Key;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.profile.StackProfiler;
+import org.openjdk.jmh.results.Result;
+import org.openjdk.jmh.results.RunResult;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 25, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 1)
+@Threads(1)
+@State(Scope.Benchmark)
+public class CompactionBench extends CQLTester
+{
+    static String keyspace;
+    String table;
+    String writeStatement;
+    String readStatement;
+    ColumnFamilyStore cfs;
+    List<File> snapshotFiles;
+    List<Descriptor> liveFiles;
+
+    @Setup(Level.Trial)
+    public void setup() throws Throwable
+    {
+        CQLTester.prepareServer();
+        keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false");
+        table = createTable(keyspace, "CREATE TABLE %s ( userid bigint, picid bigint, commentid bigint, PRIMARY KEY(userid, picid))");
+        execute("use "+keyspace+";");
+        writeStatement = "INSERT INTO "+table+"(userid,picid,commentid)VALUES(?,?,?)";
+        readStatement = "SELECT * from "+table+" limit 100";
+
+        Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction()));
+
+        cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
+        cfs.disableAutoCompaction();
+
+        //Warm up
+        System.err.println("Writing 50k");
+        for (long i = 0; i < 50000; i++)
+            execute(writeStatement, i, i, i );
+
+
+        cfs.forceBlockingFlush();
+
+        System.err.println("Writing 50k again...");
+        for (long i = 0; i < 50000; i++)
+            execute(writeStatement, i, i, i );
+
+        cfs.forceBlockingFlush();
+
+        cfs.snapshot("originals");
+
+        snapshotFiles = cfs.getDirectories().sstableLister(Directories.OnTxnErr.IGNORE).snapshots("originals").listFiles();
+    }
+
+    @TearDown(Level.Trial)
+    public void teardown() throws IOException, ExecutionException, InterruptedException
+    {
+        int active = Thread.currentThread().getThreadGroup().activeCount();
+        Thread[] threads = new Thread[active];
+        Thread.currentThread().getThreadGroup().enumerate(threads);
+        for (Thread t : threads)
+        {
+            if (!t.isDaemon())
+                System.err.println("Thread "+t.getName());
+        }
+
+        CQLTester.cleanup();
+    }
+
+
+    @TearDown(Level.Invocation)
+    public void resetSnapshot()
+    {
+        cfs.truncateBlocking();
+
+        List<File> directories = cfs.getDirectories().getCFDirectories();
+
+        for (File file : directories)
+        {
+            for (File f : file.listFiles())
+            {
+                if (f.isDirectory())
+                    continue;
+
+                FileUtils.delete(f);
+            }
+        }
+
+
+        for (File file : snapshotFiles)
+            FileUtils.createHardLink(file, new File(file.toPath().getParent().getParent().getParent().toFile(), file.getName()));
+
+        cfs.loadNewSSTables();
+    }
+
+    @Benchmark
+    public void compactTest() throws Throwable
+    {
+        cfs.forceMajorCompaction();
+    }
+}
diff --git a/test/microbench/org/apache/cassandra/test/microbench/DirectorySizerBench.java b/test/microbench/org/apache/cassandra/test/microbench/DirectorySizerBench.java
new file mode 100644
index 0000000..34cbb17
--- /dev/null
+++ b/test/microbench/org/apache/cassandra/test/microbench/DirectorySizerBench.java
@@ -0,0 +1,104 @@
+/*
+ * 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.test.microbench;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.DirectorySizeCalculator;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.infra.Blackhole;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 1)
+@Measurement(iterations = 30)
+@Fork(value = 1,jvmArgsAppend = "-Xmx512M")
+@Threads(1)
+@State(Scope.Benchmark)
+public class DirectorySizerBench
+{
+    private File tempDir;
+    private DirectorySizeCalculator sizer;
+
+    @Setup(Level.Trial)
+    public void setUp() throws IOException
+    {
+        tempDir = Files.createTempDirectory(randString()).toFile();
+
+        // Since #'s on laptops and commodity desktops are so useful in considering enterprise virtualized server environments...
+
+        // Spinning disk 7200rpm 1TB, win10, ntfs, i6600 skylake, 256 files:
+        // [java] Result: 0.581 ▒(99.9%) 0.003 ms/op [Average]
+        // [java]   Statistics: (min, avg, max) = (0.577, 0.581, 0.599), stdev = 0.005
+        // [java]   Confidence interval (99.9%): [0.577, 0.584]
+
+        // Same hardware, 25600 files:
+        // [java] Result: 56.990 ▒(99.9%) 0.374 ms/op [Average]
+        // [java]   Statistics: (min, avg, max) = (56.631, 56.990, 59.829), stdev = 0.560
+        // [java]   Confidence interval (99.9%): [56.616, 57.364]
+
+        // #'s on a rmbp, 2014, SSD, ubuntu 15.10, ext4, i7-4850HQ @ 2.3, 25600 samples
+        // [java] Result: 74.714 ±(99.9%) 0.558 ms/op [Average]
+        // [java]   Statistics: (min, avg, max) = (73.687, 74.714, 76.872), stdev = 0.835
+        // [java]   Confidence interval (99.9%): [74.156, 75.272]
+
+        // Throttle CPU on the Windows box to .87GHZ from 4.3GHZ turbo single-core, and #'s for 25600:
+        // [java] Result: 298.628 ▒(99.9%) 14.755 ms/op [Average]
+        // [java]   Statistics: (min, avg, max) = (291.245, 298.628, 412.881), stdev = 22.085
+        // [java]   Confidence interval (99.9%): [283.873, 313.383]
+
+        // Test w/25,600 files, 100x the load of a full default CommitLog (8192) divided by size (32 per)
+        populateRandomFiles(tempDir, 25600);
+        sizer = new DirectorySizeCalculator(tempDir);
+    }
+
+    @TearDown
+    public void tearDown()
+    {
+        FileUtils.deleteRecursive(tempDir);
+    }
+
+    private void populateRandomFiles(File dir, int count) throws IOException
+    {
+        for (int i = 0; i < count; i++)
+        {
+            PrintWriter pw = new PrintWriter(dir + File.separator + randString(), "UTF-8");
+            pw.write(randString());
+            pw.close();
+        }
+    }
+
+    private String randString()
+    {
+        return UUID.randomUUID().toString();
+    }
+
+    @Benchmark
+    public void countFiles(final Blackhole bh) throws IOException
+    {
+        Files.walkFileTree(tempDir.toPath(), sizer);
+    }
+}
diff --git a/test/microbench/org/apache/cassandra/test/microbench/FastThreadExecutor.java b/test/microbench/org/apache/cassandra/test/microbench/FastThreadExecutor.java
new file mode 100644
index 0000000..5644e4f
--- /dev/null
+++ b/test/microbench/org/apache/cassandra/test/microbench/FastThreadExecutor.java
@@ -0,0 +1,39 @@
+/*
+ * 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.test.microbench;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import io.netty.util.concurrent.DefaultThreadFactory;
+
+/**
+ * Created to test perf of FastThreadLocal
+ *
+ * Used in MutationBench via:
+ * jvmArgsAppend = {"-Djmh.executor=CUSTOM", "-Djmh.executor.class=org.apache.cassandra.test.microbench.FastThreadExecutor"}
+ */
+public class FastThreadExecutor extends ThreadPoolExecutor
+{
+    public FastThreadExecutor(int size, String name)
+    {
+        super(size, size, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new DefaultThreadFactory(name, true));
+    }
+}
diff --git a/test/microbench/org/apache/cassandra/test/microbench/FastThreadLocalBench.java b/test/microbench/org/apache/cassandra/test/microbench/FastThreadLocalBench.java
new file mode 100644
index 0000000..491dc44
--- /dev/null
+++ b/test/microbench/org/apache/cassandra/test/microbench/FastThreadLocalBench.java
@@ -0,0 +1,92 @@
+/*
+ * 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.test.microbench;
+
+import java.util.concurrent.TimeUnit;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 1,jvmArgsAppend = {"-Xmx512M", "-Djmh.executor=CUSTOM", "-Djmh.executor.class=org.apache.cassandra.test.microbench.FastThreadExecutor"})
+@Threads(4) // make sure this matches the number of _physical_cores_
+@State(Scope.Benchmark)
+public class FastThreadLocalBench
+{
+    @Param({"2", "4", "8", "12"})
+    private int variables = 2;
+
+    static final int max = 20;
+    static final ThreadLocal[] threadLocals = new ThreadLocal[max];
+    static final FastThreadLocal[] fastThreadLocals = new FastThreadLocal[max];
+    static
+    {
+        for (int i = 0; i < max; i++)
+        {
+            threadLocals[i] = ThreadLocal.withInitial(Object::new);
+            fastThreadLocals[i] = new FastThreadLocal() {
+                protected Object initialValue() throws Exception
+                {
+                    return new Object();
+                }
+            };
+        }
+    }
+
+    @State(Scope.Thread)
+    public static class FastThreadLocalBenchState
+    {
+        public int index;
+    }
+
+    @Benchmark
+    public void baseline(FastThreadLocalBenchState state, Blackhole bh)
+    {
+        if (variables != 2)
+            throw new IllegalArgumentException("skipped");
+
+        bh.consume("foo");
+    }
+
+    @Benchmark
+    public void threadLocal(FastThreadLocalBenchState state, Blackhole bh)
+    {
+        bh.consume(threadLocals[state.index % max].get());
+    }
+
+    @Benchmark
+    public void fastThreadLocal(FastThreadLocalBenchState state, Blackhole bh)
+    {
+        bh.consume(fastThreadLocals[state.index % max].get());
+    }
+}
diff --git a/test/microbench/org/apache/cassandra/test/microbench/GcCompactionBenchTest.java b/test/microbench/org/apache/cassandra/test/microbench/GcCompactionBenchTest.java
new file mode 100644
index 0000000..59f74d8
--- /dev/null
+++ b/test/microbench/org/apache/cassandra/test/microbench/GcCompactionBenchTest.java
@@ -0,0 +1,381 @@
+/*
+ * 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.test.microbench;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Predicate;
+
+import com.google.common.collect.Iterables;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import junit.framework.Assert;
+import org.apache.cassandra.config.Config.CommitLogSync;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.compaction.CompactionManager;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.Unfiltered;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.io.sstable.ISSTableScanner;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.schema.CompactionParams.TombstoneOption;
+import org.apache.cassandra.utils.FBUtilities;
+
+public class GcCompactionBenchTest extends CQLTester
+{
+    private static final String SIZE_TIERED_STRATEGY = "SizeTieredCompactionStrategy', 'min_sstable_size' : '0";
+    private static final String LEVELED_STRATEGY = "LeveledCompactionStrategy', 'sstable_size_in_mb' : '16";
+
+    private static final int DEL_SECTIONS = 1000;
+    private static final int FLUSH_FREQ = 10000;
+    private static final int RANGE_FREQUENCY_INV = 16;
+    static final int COUNT = 90000;
+    static final int ITERS = 9;
+
+    static final int KEY_RANGE = 10;
+    static final int CLUSTERING_RANGE = 210000;
+
+    static final int EXTRA_SIZE = 1025;
+
+    // The name of this method is important!
+    // CommitLog settings must be applied before CQLTester sets up; by using the same name as its @BeforeClass method we
+    // are effectively overriding it.
+    @BeforeClass
+    public static void setUpClass()     // overrides CQLTester.setUpClass()
+    {
+        DatabaseDescriptor.setCommitLogSync(CommitLogSync.periodic);
+        DatabaseDescriptor.setCommitLogSyncPeriod(100);
+        CQLTester.setUpClass();
+    }
+
+    String hashQuery;
+
+    @Before
+    public void before() throws Throwable
+    {
+        createTable("CREATE TABLE %s(" +
+                    "  key int," +
+                    "  column int," +
+                    "  data int," +
+                    "  extra text," +
+                    "  PRIMARY KEY(key, column)" +
+                    ")"
+                   );
+
+        String hashIFunc = parseFunctionName(createFunction(KEYSPACE, "int, int",
+                " CREATE FUNCTION %s (state int, val int)" +
+                " CALLED ON NULL INPUT" +
+                " RETURNS int" +
+                " LANGUAGE java" +
+                " AS 'return val != null ? state * 17 + val : state;'")).name;
+        String hashTFunc = parseFunctionName(createFunction(KEYSPACE, "int, text",
+                " CREATE FUNCTION %s (state int, val text)" +
+                " CALLED ON NULL INPUT" +
+                " RETURNS int" +
+                " LANGUAGE java" +
+                " AS 'return val != null ? state * 17 + val.hashCode() : state;'")).name;
+
+        String hashInt = createAggregate(KEYSPACE, "int",
+                " CREATE AGGREGATE %s (int)" +
+                " SFUNC " + hashIFunc +
+                " STYPE int" +
+                " INITCOND 1");
+        String hashText = createAggregate(KEYSPACE, "text",
+                " CREATE AGGREGATE %s (text)" +
+                " SFUNC " + hashTFunc +
+                " STYPE int" +
+                " INITCOND 1");
+
+        hashQuery = String.format("SELECT count(column), %s(key), %s(column), %s(data), %s(extra), avg(key), avg(column), avg(data) FROM %%s",
+                                  hashInt, hashInt, hashInt, hashText);
+    }
+    AtomicLong id = new AtomicLong();
+    long compactionTimeNanos = 0;
+
+    void pushData(Random rand, int count) throws Throwable
+    {
+        for (int i = 0; i < count; ++i)
+        {
+            long ii = id.incrementAndGet();
+            if (ii % 1000 == 0)
+                System.out.print('.');
+            int key = rand.nextInt(KEY_RANGE);
+            int column = rand.nextInt(CLUSTERING_RANGE);
+            execute("INSERT INTO %s (key, column, data, extra) VALUES (?, ?, ?, ?)", key, column, (int) ii, genExtra(rand));
+            maybeCompact(ii);
+        }
+    }
+
+    private String genExtra(Random rand)
+    {
+        StringBuilder builder = new StringBuilder(EXTRA_SIZE);
+        for (int i = 0; i < EXTRA_SIZE; ++i)
+            builder.append((char) ('a' + rand.nextInt('z' - 'a' + 1)));
+        return builder.toString();
+    }
+
+    void deleteData(Random rand, int count) throws Throwable
+    {
+        for (int i = 0; i < count; ++i)
+        {
+            int key;
+            UntypedResultSet res;
+            long ii = id.incrementAndGet();
+            if (ii % 1000 == 0)
+                System.out.print('-');
+            if (rand.nextInt(RANGE_FREQUENCY_INV) != 1)
+            {
+                do
+                {
+                    key = rand.nextInt(KEY_RANGE);
+                    long cid = rand.nextInt(DEL_SECTIONS);
+                    int cstart = (int) (cid * CLUSTERING_RANGE / DEL_SECTIONS);
+                    int cend = (int) ((cid + 1) * CLUSTERING_RANGE / DEL_SECTIONS);
+                    res = execute("SELECT column FROM %s WHERE key = ? AND column >= ? AND column < ? LIMIT 1", key, cstart, cend);
+                } while (res.size() == 0);
+                UntypedResultSet.Row r = Iterables.get(res, rand.nextInt(res.size()));
+                int clustering = r.getInt("column");
+                execute("DELETE FROM %s WHERE key = ? AND column = ?", key, clustering);
+            }
+            else
+            {
+                key = rand.nextInt(KEY_RANGE);
+                long cid = rand.nextInt(DEL_SECTIONS);
+                int cstart = (int) (cid * CLUSTERING_RANGE / DEL_SECTIONS);
+                int cend = (int) ((cid + 1) * CLUSTERING_RANGE / DEL_SECTIONS);
+                res = execute("DELETE FROM %s WHERE key = ? AND column >= ? AND column < ?", key, cstart, cend);
+            }
+            maybeCompact(ii);
+        }
+    }
+
+    private void maybeCompact(long ii)
+    {
+        if (ii % FLUSH_FREQ == 0)
+        {
+            System.out.print("F");
+            flush();
+            if (ii % (FLUSH_FREQ * 10) == 0)
+            {
+                System.out.println("C");
+                long startTime = System.nanoTime();
+                getCurrentColumnFamilyStore().enableAutoCompaction(true);
+                long endTime = System.nanoTime();
+                compactionTimeNanos += endTime - startTime;
+                getCurrentColumnFamilyStore().disableAutoCompaction();
+            }
+        }
+    }
+
+    public void testGcCompaction(TombstoneOption tombstoneOption, TombstoneOption backgroundTombstoneOption, String compactionClass) throws Throwable
+    {
+        id.set(0);
+        compactionTimeNanos = 0;
+        alterTable("ALTER TABLE %s WITH compaction = { 'class' :  '" + compactionClass + "', 'provide_overlapping_tombstones' : '" + backgroundTombstoneOption + "'  };");
+        ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+        cfs.disableAutoCompaction();
+
+        long onStartTime = System.currentTimeMillis();
+        ExecutorService es = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
+        List<Future<?>> tasks = new ArrayList<>();
+        for (int ti = 0; ti < 1; ++ti)
+        {
+            Random rand = new Random(ti);
+            tasks.add(es.submit(() -> 
+            {
+                for (int i = 0; i < ITERS; ++i)
+                    try
+                    {
+                        pushData(rand, COUNT);
+                        deleteData(rand, COUNT / 3);
+                    }
+                    catch (Throwable e)
+                    {
+                        throw new AssertionError(e);
+                    }
+            }));
+        }
+        for (Future<?> task : tasks)
+            task.get();
+
+        flush();
+        long onEndTime = System.currentTimeMillis();
+        int startRowCount = countRows(cfs);
+        int startTombCount = countTombstoneMarkers(cfs);
+        int startRowDeletions = countRowDeletions(cfs);
+        int startTableCount = cfs.getLiveSSTables().size();
+        int startTableMaxLevel = cfs.getLiveSSTables().stream().mapToInt(SSTableReader::getSSTableLevel).max().orElseGet(() -> 0);
+        long startSize = SSTableReader.getTotalBytes(cfs.getLiveSSTables());
+        System.out.println();
+
+        String hashesBefore = getHashes();
+
+        long startTime = System.currentTimeMillis();
+        CompactionManager.instance.performGarbageCollection(cfs, tombstoneOption, 0);
+        long endTime = System.currentTimeMillis();
+
+        int endRowCount = countRows(cfs);
+        int endTombCount = countTombstoneMarkers(cfs);
+        int endRowDeletions = countRowDeletions(cfs);
+        int endTableCount = cfs.getLiveSSTables().size();
+        int endTableMaxLevel = cfs.getLiveSSTables().stream().mapToInt(SSTableReader::getSSTableLevel).max().orElseGet(() -> 0);
+        long endSize = SSTableReader.getTotalBytes(cfs.getLiveSSTables());
+
+        System.out.println(cfs.getCompactionParametersJson());
+        System.out.println(String.format("%s compactions completed in %.3fs",
+                tombstoneOption.toString(), (endTime - startTime) * 1e-3));
+        System.out.println(String.format("Operations completed in %.3fs, out of which %.3f for ongoing " + backgroundTombstoneOption + " background compactions",
+                (onEndTime - onStartTime) * 1e-3, compactionTimeNanos * 1e-9));
+        System.out.println(String.format("At start: %12d tables %12d bytes %12d rows %12d deleted rows %12d tombstone markers",
+                startTableCount, startSize, startRowCount, startRowDeletions, startTombCount));
+        System.out.println(String.format("At end:   %12d tables %12d bytes %12d rows %12d deleted rows %12d tombstone markers",
+                endTableCount, endSize, endRowCount, endRowDeletions, endTombCount));
+        System.out.println(String.format("Max SSTable level before: %d and after %d", startTableMaxLevel, endTableMaxLevel));
+
+        String hashesAfter = getHashes();
+        Assert.assertEquals(hashesBefore, hashesAfter);
+        Assert.assertEquals(startTableMaxLevel, endTableMaxLevel);
+    }
+
+    private String getHashes() throws Throwable
+    {
+        long startTime = System.currentTimeMillis();
+        String hashes = Arrays.toString(getRows(execute(hashQuery))[0]);
+        long endTime = System.currentTimeMillis();
+        System.out.println(String.format("Hashes: %s, retrieved in %.3fs", hashes, (endTime - startTime) * 1e-3));
+        return hashes;
+    }
+
+    @Test
+    public void testCellAtEnd() throws Throwable
+    {
+        testGcCompaction(TombstoneOption.CELL, TombstoneOption.NONE, LEVELED_STRATEGY);
+    }
+
+    @Test
+    public void testRowAtEnd() throws Throwable
+    {
+        testGcCompaction(TombstoneOption.CELL, TombstoneOption.NONE, LEVELED_STRATEGY);
+    }
+
+    @Test
+    public void testCellThroughout() throws Throwable
+    {
+        testGcCompaction(TombstoneOption.CELL, TombstoneOption.CELL, LEVELED_STRATEGY);
+    }
+
+    @Test
+    public void testRowThroughout() throws Throwable
+    {
+        testGcCompaction(TombstoneOption.ROW, TombstoneOption.ROW, LEVELED_STRATEGY);
+    }
+
+    @Test
+    public void testCopyCompaction() throws Throwable
+    {
+        testGcCompaction(TombstoneOption.NONE, TombstoneOption.NONE, LEVELED_STRATEGY);
+    }
+
+    @Test
+    public void testCellAtEndSizeTiered() throws Throwable
+    {
+        testGcCompaction(TombstoneOption.CELL, TombstoneOption.NONE, SIZE_TIERED_STRATEGY);
+    }
+
+    @Test
+    public void testRowAtEndSizeTiered() throws Throwable
+    {
+        testGcCompaction(TombstoneOption.ROW, TombstoneOption.NONE, SIZE_TIERED_STRATEGY);
+    }
+
+    @Test
+    public void testCellThroughoutSizeTiered() throws Throwable
+    {
+        testGcCompaction(TombstoneOption.CELL, TombstoneOption.CELL, SIZE_TIERED_STRATEGY);
+    }
+
+    @Test
+    public void testRowThroughoutSizeTiered() throws Throwable
+    {
+        testGcCompaction(TombstoneOption.ROW, TombstoneOption.ROW, SIZE_TIERED_STRATEGY);
+    }
+
+    @Test
+    public void testCopyCompactionSizeTiered() throws Throwable
+    {
+        testGcCompaction(TombstoneOption.NONE, TombstoneOption.NONE, SIZE_TIERED_STRATEGY);
+    }
+
+    int countTombstoneMarkers(ColumnFamilyStore cfs)
+    {
+        return count(cfs, x -> x.isRangeTombstoneMarker());
+    }
+
+    int countRowDeletions(ColumnFamilyStore cfs)
+    {
+        return count(cfs, x -> x.isRow() && !((Row) x).deletion().isLive());
+    }
+
+    int countRows(ColumnFamilyStore cfs)
+    {
+        boolean enforceStrictLiveness = cfs.metadata.enforceStrictLiveness();
+        int nowInSec = FBUtilities.nowInSeconds();
+        return count(cfs, x -> x.isRow() && ((Row) x).hasLiveData(nowInSec, enforceStrictLiveness));
+    }
+
+    private int count(ColumnFamilyStore cfs, Predicate<Unfiltered> predicate)
+    {
+        int count = 0;
+        for (SSTableReader reader : cfs.getLiveSSTables())
+            count += count(reader, predicate);
+        return count;
+    }
+
+    int count(SSTableReader reader, Predicate<Unfiltered> predicate)
+    {
+        int instances = 0;
+        try (ISSTableScanner partitions = reader.getScanner())
+        {
+            while (partitions.hasNext())
+            {
+                try (UnfilteredRowIterator iter = partitions.next())
+                {
+                    while (iter.hasNext())
+                    {
+                        Unfiltered atom = iter.next();
+                        if (predicate.test(atom))
+                            ++instances;
+                    }
+                }
+            }
+        }
+        return instances;
+    }
+}
diff --git a/test/microbench/org/apache/cassandra/test/microbench/MutationBench.java b/test/microbench/org/apache/cassandra/test/microbench/MutationBench.java
new file mode 100644
index 0000000..8c177cf
--- /dev/null
+++ b/test/microbench/org/apache/cassandra/test/microbench/MutationBench.java
@@ -0,0 +1,148 @@
+/*
+ * 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.test.microbench;
+
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.concurrent.*;
+
+import org.apache.cassandra.UpdateBuilder;
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.io.util.DataOutputBufferFixed;
+import org.apache.cassandra.net.MessageIn;
+import org.apache.cassandra.net.MessageOut;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.profile.StackProfiler;
+import org.openjdk.jmh.results.Result;
+import org.openjdk.jmh.results.RunResult;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 1
+       , jvmArgsAppend = {"-Djmh.executor=CUSTOM", "-Djmh.executor.class=org.apache.cassandra.test.microbench.FastThreadExecutor"
+       //,"-XX:+UnlockCommercialFeatures", "-XX:+FlightRecorder","-XX:+UnlockDiagnosticVMOptions", "-XX:+DebugNonSafepoints",
+       // "-XX:StartFlightRecording=duration=60s,filename=./profiling-data.jfr,name=profile,settings=profile",
+       // "-XX:FlightRecorderOptions=settings=/home/jake/workspace/cassandra/profiling-advanced.jfc,samplethreads=true"
+     }
+)
+@Threads(1)
+@State(Scope.Benchmark)
+public class MutationBench
+{
+    static
+    {
+        DatabaseDescriptor.clientInitialization(false);
+        // Partitioner is not set in client mode.
+        if (DatabaseDescriptor.getPartitioner() == null)
+            DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
+    }
+
+    static String keyspace = "keyspace1";
+
+    private Mutation mutation;
+    private MessageOut<Mutation> messageOut;
+
+    private ByteBuffer buffer;
+    private DataOutputBuffer outputBuffer;
+    private DataInputBuffer inputBuffer;
+
+
+    @State(Scope.Thread)
+    public static class ThreadState
+    {
+        MessageIn<Mutation> in;
+        int counter = 0;
+    }
+
+    @Setup
+    public void setup() throws IOException
+    {
+        Schema.instance.load(KeyspaceMetadata.create(keyspace, KeyspaceParams.simple(1)));
+        KeyspaceMetadata ksm = Schema.instance.getKSMetaData(keyspace);
+        CFMetaData metadata = CFMetaData.compile("CREATE TABLE userpics " +
+                                                   "( userid bigint," +
+                                                   "picid bigint," +
+                                                   "commentid bigint, " +
+                                                   "PRIMARY KEY(userid, picid))", keyspace);
+
+        Schema.instance.load(metadata);
+        Schema.instance.setKeyspaceMetadata(ksm.withSwapped(ksm.tables.with(metadata)));
+
+
+        mutation = (Mutation)UpdateBuilder.create(metadata, 1L).newRow(1L).add("commentid", 32L).makeMutation();
+        messageOut = mutation.createMessage();
+        buffer = ByteBuffer.allocate(messageOut.serializedSize(MessagingService.current_version));
+        outputBuffer = new DataOutputBufferFixed(buffer);
+        inputBuffer = new DataInputBuffer(buffer, false);
+
+        messageOut.serialize(outputBuffer, MessagingService.current_version);
+    }
+
+    @Benchmark
+    public void serialize(ThreadState state) throws IOException
+    {
+        buffer.rewind();
+        messageOut.serialize(outputBuffer, MessagingService.current_version);
+        state.counter++;
+    }
+
+    @Benchmark
+    public void deserialize(ThreadState state) throws IOException
+    {
+        buffer.rewind();
+        state.in = MessageIn.read(inputBuffer, MessagingService.current_version, 0);
+        state.counter++;
+    }
+
+    public static void main(String... args) throws Exception {
+        Options opts = new OptionsBuilder()
+                       .include(".*"+MutationBench.class.getSimpleName()+".*")
+                       .jvmArgs("-server")
+                       .forks(1)
+                       .mode(Mode.Throughput)
+                       .addProfiler(StackProfiler.class)
+                       .build();
+
+        Collection<RunResult> records = new Runner(opts).run();
+        for ( RunResult result : records) {
+            Result r = result.getPrimaryResult();
+            System.out.println("API replied benchmark score: "
+                               + r.getScore() + " "
+                               + r.getScoreUnit() + " over "
+                               + r.getStatistics().getN() + " iterations");
+        }
+    }
+}
diff --git a/test/microbench/org/apache/cassandra/test/microbench/ReadWriteTest.java b/test/microbench/org/apache/cassandra/test/microbench/ReadWriteTest.java
new file mode 100644
index 0000000..89973fd
--- /dev/null
+++ b/test/microbench/org/apache/cassandra/test/microbench/ReadWriteTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.test.microbench;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.*;
+
+import org.apache.cassandra.UpdateBuilder;
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.statements.ParsedStatement;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.io.util.DataOutputBufferFixed;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.net.MessageIn;
+import org.apache.cassandra.net.MessageOut;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.service.CassandraDaemon;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.transport.messages.ResultMessage;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.profile.StackProfiler;
+import org.openjdk.jmh.results.Result;
+import org.openjdk.jmh.results.RunResult;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 1)
+@Threads(1)
+@State(Scope.Benchmark)
+public class ReadWriteTest extends CQLTester
+{
+    static String keyspace;
+    String table;
+    String writeStatement;
+    String readStatement;
+    long numRows = 0;
+    ColumnFamilyStore cfs;
+
+    @Setup(Level.Trial)
+    public void setup() throws Throwable
+    {
+        CQLTester.setUpClass();
+        keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false");
+        table = createTable(keyspace, "CREATE TABLE %s ( userid bigint, picid bigint, commentid bigint, PRIMARY KEY(userid, picid))");
+        execute("use "+keyspace+";");
+        writeStatement = "INSERT INTO "+table+"(userid,picid,commentid)VALUES(?,?,?)";
+        readStatement = "SELECT * from "+table+" limit 100";
+
+        cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
+        cfs.disableAutoCompaction();
+
+        //Warm up
+        System.err.println("Writing 50k");
+        for (long i = 0; i < 5000; i++)
+            execute(writeStatement, i, i, i );
+    }
+
+    @TearDown(Level.Trial)
+    public void teardown() throws IOException, ExecutionException, InterruptedException
+    {
+        CQLTester.cleanup();
+    }
+
+    @Benchmark
+    public Object write() throws Throwable
+    {
+        numRows++;
+        return execute(writeStatement, numRows, numRows, numRows );
+    }
+
+
+    @Benchmark
+    public Object read() throws Throwable
+    {
+        return execute(readStatement);
+    }
+}
diff --git a/test/resources/auth/cassandra-test-jaas.conf b/test/resources/auth/cassandra-test-jaas.conf
new file mode 100644
index 0000000..ccb8b6a
--- /dev/null
+++ b/test/resources/auth/cassandra-test-jaas.conf
@@ -0,0 +1,4 @@
+// Delegates authentication to a stub login module, hardcoded to authenticate as a particular user - see JMXAuthTest
+TestLogin {
+  org.apache.cassandra.auth.jmx.JMXAuthTest$StubLoginModule REQUIRED role_name=test_role;
+};
diff --git a/test/resources/blogpost.yaml b/test/resources/blogpost.yaml
new file mode 100644
index 0000000..3aea5c8
--- /dev/null
+++ b/test/resources/blogpost.yaml
@@ -0,0 +1,87 @@
+#
+# 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.
+#
+
+# Copied from https://gist.github.com/tjake/8995058fed11d9921e31
+### DML ###
+
+# Keyspace Name
+keyspace: stresscql
+
+# The CQL for creating a keyspace (optional if it already exists)
+keyspace_definition: |
+  CREATE KEYSPACE stresscql WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};
+
+# Table name
+table: blogposts
+
+# The CQL for creating a table you wish to stress (optional if it already exists)
+table_definition: |
+  CREATE TABLE blogposts (
+        domain text,
+        published_date timeuuid,
+        url text,
+        author text,
+        title text,
+        body text,
+        PRIMARY KEY(domain, published_date)
+  ) WITH CLUSTERING ORDER BY (published_date DESC)
+    AND compaction = { 'class':'LeveledCompactionStrategy' }
+    AND comment='A table to hold blog posts'
+
+### Column Distribution Specifications ###
+
+columnspec:
+  - name: domain
+    size: gaussian(5..100)       #domain names are relatively short
+    population: uniform(1..10M)  #10M possible domains to pick from
+
+  - name: published_date
+    cluster: fixed(1000)         #under each domain we will have max 1000 posts
+
+  - name: url
+    size: uniform(30..300)
+
+  - name: title                  #titles shouldn't go beyond 200 chars
+    size: gaussian(10..200)
+
+  - name: author
+    size: uniform(5..20)         #author names should be short
+
+  - name: body
+    size: gaussian(100..5000)    #the body of the blog post can be long
+
+### Batch Ratio Distribution Specifications ###
+
+insert:
+  partitions: fixed(1)            # Our partition key is the domain so only insert one per batch
+
+  select:    fixed(1)/1000        # We have 1000 posts per domain so 1/1000 will allow 1 post per batch
+
+  batchtype: UNLOGGED             # Unlogged batches
+
+
+#
+# A list of queries you wish to run against the schema
+#
+queries:
+   singlepost:
+      cql: select * from blogposts where domain = ? LIMIT 1
+      fields: samerow
+   timeline:
+      cql: select url, title, published_date from blogposts where domain = ? LIMIT 10
+      fields: samerow
\ No newline at end of file
diff --git a/test/resources/byteman/mutation_limiter.btm b/test/resources/byteman/mutation_limiter.btm
new file mode 100644
index 0000000..fb33b28
--- /dev/null
+++ b/test/resources/byteman/mutation_limiter.btm
@@ -0,0 +1,26 @@
+#
+# 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.
+#
+
+RULE mutation_limiter
+CLASS org.apache.cassandra.db.MutationVerbHandler
+METHOD doVerb
+HELPER org.apache.cassandra.utils.TestRateLimiter
+AT ENTRY
+IF TRUE
+DO acquire(1000.0)
+ENDRULE
\ No newline at end of file
diff --git a/test/resources/tokenization/adventures_of_huckleberry_finn_mark_twain.txt b/test/resources/tokenization/adventures_of_huckleberry_finn_mark_twain.txt
new file mode 100644
index 0000000..27cadc3
--- /dev/null
+++ b/test/resources/tokenization/adventures_of_huckleberry_finn_mark_twain.txt
@@ -0,0 +1,12361 @@
+

+

+The Project Gutenberg EBook of Adventures of Huckleberry Finn, Complete

+by Mark Twain (Samuel Clemens)

+

+This eBook is for the use of anyone anywhere at no cost and with almost

+no restrictions whatsoever. You may copy it, give it away or re-use

+it under the terms of the Project Gutenberg License included with this

+eBook or online at www.gutenberg.net

+

+Title: Adventures of Huckleberry Finn, Complete

+

+Author: Mark Twain (Samuel Clemens)

+

+Release Date: August 20, 2006 [EBook #76]

+

+Last Updated: October 20, 2012]

+

+Language: English

+

+

+*** START OF THIS PROJECT GUTENBERG EBOOK HUCKLEBERRY FINN ***

+

+Produced by David Widger

+

+

+

+

+

+ADVENTURES

+

+OF

+

+HUCKLEBERRY FINN

+

+(Tom Sawyer's Comrade)

+

+By Mark Twain

+

+Complete

+

+

+

+

+CONTENTS.

+

+CHAPTER I. Civilizing Huck.—Miss Watson.—Tom Sawyer Waits.

+

+CHAPTER II. The Boys Escape Jim.—Torn Sawyer's Gang.—Deep-laid Plans.

+

+CHAPTER III. A Good Going-over.—Grace Triumphant.—"One of Tom Sawyers's

+Lies".

+

+CHAPTER IV. Huck and the Judge.—Superstition.

+

+CHAPTER V. Huck's Father.—The Fond Parent.—Reform.

+

+CHAPTER VI. He Went for Judge Thatcher.—Huck Decided to Leave.—Political

+Economy.—Thrashing Around.

+

+CHAPTER VII. Laying for Him.—Locked in the Cabin.—Sinking the

+Body.—Resting.

+

+CHAPTER VIII. Sleeping in the Woods.—Raising the Dead.—Exploring the

+Island.—Finding Jim.—Jim's Escape.—Signs.—Balum.

+

+CHAPTER IX. The Cave.—The Floating House.

+

+CHAPTER X. The Find.—Old Hank Bunker.—In Disguise.

+

+CHAPTER XI. Huck and the Woman.—The Search.—Prevarication.—Going to

+Goshen.

+

+CHAPTER XII. Slow Navigation.—Borrowing Things.—Boarding the Wreck.—The

+Plotters.—Hunting for the Boat.

+

+CHAPTER XIII. Escaping from the Wreck.—The Watchman.—Sinking.

+

+CHAPTER XIV. A General Good Time.—The Harem.—French.

+

+CHAPTER XV. Huck Loses the Raft.—In the Fog.—Huck Finds the Raft.—Trash.

+

+CHAPTER XVI. Expectation.—A White Lie.—Floating Currency.—Running by

+Cairo.—Swimming Ashore.

+

+CHAPTER XVII. An Evening Call.—The Farm in Arkansaw.—Interior

+Decorations.—Stephen Dowling Bots.—Poetical Effusions.

+

+CHAPTER XVIII. Col. Grangerford.—Aristocracy.—Feuds.—The

+Testament.—Recovering the Raft.—The Wood—pile.—Pork and Cabbage.

+

+CHAPTER XIX. Tying Up Day—times.—An Astronomical Theory.—Running a

+Temperance Revival.—The Duke of Bridgewater.—The Troubles of Royalty.

+

+CHAPTER XX. Huck Explains.—Laying Out a Campaign.—Working the

+Camp—meeting.—A Pirate at the Camp—meeting.—The Duke as a Printer.

+

+CHAPTER XXI. Sword Exercise.—Hamlet's Soliloquy.—They Loafed Around

+Town.—A Lazy Town.—Old Boggs.—Dead.

+

+CHAPTER XXII. Sherburn.—Attending the Circus.—Intoxication in the

+Ring.—The Thrilling Tragedy.

+

+CHAPTER XXIII. Sold.—Royal Comparisons.—Jim Gets Home-sick.

+

+CHAPTER XXIV. Jim in Royal Robes.—They Take a Passenger.—Getting

+Information.—Family Grief.

+

+CHAPTER XXV. Is It Them?—Singing the "Doxologer."—Awful Square—Funeral

+Orgies.—A Bad Investment .

+

+CHAPTER XXVI. A Pious King.—The King's Clergy.—She Asked His

+Pardon.—Hiding in the Room.—Huck Takes the Money.

+

+CHAPTER XXVII. The Funeral.—Satisfying Curiosity.—Suspicious of

+Huck,—Quick Sales and Small.

+

+CHAPTER XXVIII. The Trip to England.—"The Brute!"—Mary Jane Decides to

+Leave.—Huck Parting with Mary Jane.—Mumps.—The Opposition Line.

+

+CHAPTER XXIX. Contested Relationship.—The King Explains the Loss.—A

+Question of Handwriting.—Digging up the Corpse.—Huck Escapes.

+

+CHAPTER XXX. The King Went for Him.—A Royal Row.—Powerful Mellow.

+

+CHAPTER XXXI. Ominous Plans.—News from Jim.—Old Recollections.—A Sheep

+Story.—Valuable Information.

+

+CHAPTER XXXII. Still and Sunday—like.—Mistaken Identity.—Up a Stump.—In

+a Dilemma.

+

+CHAPTER XXXIII. A Nigger Stealer.—Southern Hospitality.—A Pretty Long

+Blessing.—Tar and Feathers.

+

+CHAPTER XXXIV. The Hut by the Ash Hopper.—Outrageous.—Climbing the

+Lightning Rod.—Troubled with Witches.

+

+CHAPTER XXXV. Escaping Properly.—Dark Schemes.—Discrimination in

+Stealing.—A Deep Hole.

+

+CHAPTER XXXVI. The Lightning Rod.—His Level Best.—A Bequest to

+Posterity.—A High Figure.

+

+CHAPTER XXXVII. The Last Shirt.—Mooning Around.—Sailing Orders.—The

+Witch Pie.

+

+CHAPTER XXXVIII. The Coat of Arms.—A Skilled Superintendent.—Unpleasant

+Glory.—A Tearful Subject.

+

+CHAPTER XXXIX. Rats.—Lively Bed—fellows.—The Straw Dummy.

+

+CHAPTER XL. Fishing.—The Vigilance Committee.—A Lively Run.—Jim Advises

+a Doctor.

+

+CHAPTER XLI. The Doctor.—Uncle Silas.—Sister Hotchkiss.—Aunt Sally in

+Trouble.

+

+CHAPTER XLII. Tom Sawyer Wounded.—The Doctor's Story.—Tom

+Confesses.—Aunt Polly Arrives.—Hand Out Them Letters    .

+

+CHAPTER THE LAST. Out of Bondage.—Paying the Captive.—Yours Truly, Huck

+Finn.

+

+

+

+

+ILLUSTRATIONS.

+

+The Widows

+

+Moses and the "Bulrushers"

+

+Miss Watson

+

+Huck Stealing Away

+

+They Tip-toed Along

+

+Jim

+

+Tom Sawyer's Band of Robbers  

+

+Huck Creeps into his Window

+

+Miss Watson's Lecture

+

+The Robbers Dispersed

+

+Rubbing the Lamp

+

+! ! ! !

+

+Judge Thatcher surprised

+

+Jim Listening

+

+"Pap"

+

+Huck and his Father

+

+Reforming the Drunkard

+

+Falling from Grace

+

+The Widows

+

+Moses and the "Bulrushers"

+

+Miss Watson

+

+Huck Stealing Away

+

+They Tip-toed Along

+

+Jim

+

+Tom Sawyer's Band of Robbers  

+

+Huck Creeps into his Window

+

+Miss Watson's Lecture

+

+The Robbers Dispersed

+

+Rubbing the Lamp

+

+! ! ! !

+

+Judge Thatcher surprised

+

+Jim Listening

+

+"Pap"

+

+Huck and his Father

+

+Reforming the Drunkard

+

+Falling from Grace

+

+Getting out of the Way

+

+Solid Comfort

+

+Thinking it Over

+

+Raising a Howl

+

+"Git Up"

+

+The Shanty

+

+Shooting the Pig

+

+Taking a Rest

+

+In the Woods

+

+Watching the Boat

+

+Discovering the Camp Fire

+

+Jim and the Ghost

+

+Misto Bradish's Nigger

+

+Exploring the Cave

+

+In the Cave

+

+Jim sees a Dead Man

+

+They Found Eight Dollars

+

+Jim and the Snake

+

+Old Hank Bunker

+

+"A Fair Fit"

+

+"Come In"

+

+"Him and another Man"

+

+She puts up a Snack

+

+"Hump Yourself"

+

+On the Raft

+

+He sometimes Lifted a Chicken

+

+"Please don't, Bill"

+

+"It ain't Good Morals"

+

+"Oh! Lordy, Lordy!"

+

+In a Fix

+

+"Hello, What's Up?"

+

+The Wreck

+

+We turned in and Slept

+

+Turning over the Truck

+

+Solomon and his Million Wives

+

+The story of "Sollermun"

+

+"We Would Sell the Raft"

+

+Among the Snags

+

+Asleep on the Raft

+

+"Something being Raftsman"

+

+"Boy, that's a Lie"

+

+"Here I is, Huck"

+

+Climbing up the Bank

+

+"Who's There?"

+

+"Buck"

+

+"It made Her look Spidery"

+

+"They got him out and emptied Him"  

+

+The House

+

+Col. Grangerford

+

+Young Harney Shepherdson

+

+Miss Charlotte

+

+"And asked me if I Liked Her"

+

+"Behind the Wood-pile"

+

+Hiding Day-times

+

+"And Dogs a-Coming"

+

+"By rights I am a Duke!"

+

+"I am the Late Dauphin"

+

+Tail Piece

+

+On the Raft

+

+The King as Juliet

+

+"Courting on the Sly"

+

+"A Pirate for Thirty Years"

+

+Another little Job

+

+Practizing

+

+Hamlet's Soliloquy

+

+"Gimme a Chaw"

+

+A Little Monthly Drunk

+

+The Death of Boggs

+

+Sherburn steps out

+

+A Dead Head

+

+He shed Seventeen Suits

+

+Tragedy

+

+Their Pockets Bulged

+

+Henry the Eighth in Boston Harbor

+

+Harmless

+

+Adolphus

+

+He fairly emptied that Young Fellow

+

+"Alas, our Poor Brother"

+

+"You Bet it is"

+

+Leaking

+

+Making up the "Deffisit"

+

+Going for him

+

+The Doctor

+

+The Bag of Money

+

+The Cubby

+

+Supper with the Hare-Lip

+

+Honest Injun

+

+The Duke looks under the Bed

+

+Huck takes the Money

+

+A Crack in the Dining-room Door

+

+The Undertaker

+

+"He had a Rat!"

+

+"Was you in my Room?"

+

+Jawing

+

+In Trouble

+

+Indignation

+

+How to Find Them

+

+He Wrote

+

+Hannah with the Mumps

+

+The Auction

+

+The True Brothers

+

+The Doctor leads Huck

+

+The Duke Wrote

+

+"Gentlemen, Gentlemen!"

+

+"Jim Lit Out"

+

+The King shakes Huck

+

+The Duke went for Him

+

+Spanish Moss

+

+"Who Nailed Him?"

+

+Thinking

+

+He gave him Ten Cents

+

+Striking for the Back Country

+

+Still and Sunday-like

+

+She hugged him tight

+

+"Who do you reckon it is?"

+

+"It was Tom Sawyer"

+

+"Mr. Archibald Nichols, I presume?"

+

+A pretty long Blessing

+

+Traveling By Rail

+

+Vittles

+

+A Simple Job

+

+Witches

+

+Getting Wood

+

+One of the Best Authorities

+

+The Breakfast-Horn

+

+Smouching the Knives

+

+Going down the Lightning-Rod

+

+Stealing spoons

+

+Tom advises a Witch Pie

+

+The Rubbage-Pile

+

+"Missus, dey's a Sheet Gone"

+

+In a Tearing Way

+

+One of his Ancestors

+

+Jim's Coat of Arms

+

+A Tough Job

+

+Buttons on their Tails

+

+Irrigation

+

+Keeping off Dull Times

+

+Sawdust Diet

+

+Trouble is Brewing

+

+Fishing

+

+Every one had a Gun

+

+Tom caught on a Splinter

+

+Jim advises a Doctor

+

+The Doctor

+

+Uncle Silas in Danger

+

+Old Mrs. Hotchkiss

+

+Aunt Sally talks to Huck

+

+Tom Sawyer wounded

+

+The Doctor speaks for Jim

+

+Tom rose square up in Bed

+

+"Hand out them Letters"

+

+Out of Bondage

+

+Tom's Liberality

+

+Yours Truly

+

+

+

+

+EXPLANATORY

+

+IN this book a number of dialects are used, to wit:  the Missouri negro

+dialect; the extremest form of the backwoods Southwestern dialect; the

+ordinary "Pike County" dialect; and four modified varieties of this

+last. The shadings have not been done in a haphazard fashion, or by

+guesswork; but painstakingly, and with the trustworthy guidance and

+support of personal familiarity with these several forms of speech.

+

+I make this explanation for the reason that without it many readers

+would suppose that all these characters were trying to talk alike and

+not succeeding.

+

+THE AUTHOR.

+

+

+

+

+HUCKLEBERRY FINN

+

+Scene:  The Mississippi Valley Time:  Forty to fifty years ago

+

+

+

+

+CHAPTER I.

+

+YOU don't know about me without you have read a book by the name of The

+Adventures of Tom Sawyer; but that ain't no matter.  That book was made

+by Mr. Mark Twain, and he told the truth, mainly.  There was things

+which he stretched, but mainly he told the truth.  That is nothing.  I

+never seen anybody but lied one time or another, without it was Aunt

+Polly, or the widow, or maybe Mary.  Aunt Polly—Tom's Aunt Polly, she

+is—and Mary, and the Widow Douglas is all told about in that book, which

+is mostly a true book, with some stretchers, as I said before.

+

+Now the way that the book winds up is this:  Tom and me found the money

+that the robbers hid in the cave, and it made us rich.  We got six

+thousand dollars apiece—all gold.  It was an awful sight of money when

+it was piled up.  Well, Judge Thatcher he took it and put it out

+at interest, and it fetched us a dollar a day apiece all the year

+round—more than a body could tell what to do with.  The Widow Douglas

+she took me for her son, and allowed she would sivilize me; but it was

+rough living in the house all the time, considering how dismal regular

+and decent the widow was in all her ways; and so when I couldn't stand

+it no longer I lit out.  I got into my old rags and my sugar-hogshead

+again, and was free and satisfied.  But Tom Sawyer he hunted me up and

+said he was going to start a band of robbers, and I might join if I

+would go back to the widow and be respectable.  So I went back.

+

+The widow she cried over me, and called me a poor lost lamb, and she

+called me a lot of other names, too, but she never meant no harm by

+it. She put me in them new clothes again, and I couldn't do nothing but

+sweat and sweat, and feel all cramped up.  Well, then, the old thing

+commenced again.  The widow rung a bell for supper, and you had to come

+to time. When you got to the table you couldn't go right to eating, but

+you had to wait for the widow to tuck down her head and grumble a little

+over the victuals, though there warn't really anything the matter with

+them,—that is, nothing only everything was cooked by itself.  In a

+barrel of odds and ends it is different; things get mixed up, and the

+juice kind of swaps around, and the things go better.

+

+After supper she got out her book and learned me about Moses and the

+Bulrushers, and I was in a sweat to find out all about him; but by and

+by she let it out that Moses had been dead a considerable long time; so

+then I didn't care no more about him, because I don't take no stock in

+dead people.

+

+Pretty soon I wanted to smoke, and asked the widow to let me.  But she

+wouldn't.  She said it was a mean practice and wasn't clean, and I must

+try to not do it any more.  That is just the way with some people.  They

+get down on a thing when they don't know nothing about it.  Here she was

+a-bothering about Moses, which was no kin to her, and no use to anybody,

+being gone, you see, yet finding a power of fault with me for doing a

+thing that had some good in it.  And she took snuff, too; of course that

+was all right, because she done it herself.

+

+Her sister, Miss Watson, a tolerable slim old maid, with goggles on,

+had just come to live with her, and took a set at me now with a

+spelling-book. She worked me middling hard for about an hour, and then

+the widow made her ease up.  I couldn't stood it much longer.  Then for

+an hour it was deadly dull, and I was fidgety.  Miss Watson would say,

+"Don't put your feet up there, Huckleberry;" and "Don't scrunch up

+like that, Huckleberry—set up straight;" and pretty soon she would

+say, "Don't gap and stretch like that, Huckleberry—why don't you try to

+behave?"  Then she told me all about the bad place, and I said I wished

+I was there. She got mad then, but I didn't mean no harm.  All I wanted

+was to go somewheres; all I wanted was a change, I warn't particular.

+ She said it was wicked to say what I said; said she wouldn't say it for

+the whole world; she was going to live so as to go to the good place.

+ Well, I couldn't see no advantage in going where she was going, so I

+made up my mind I wouldn't try for it.  But I never said so, because it

+would only make trouble, and wouldn't do no good.

+

+Now she had got a start, and she went on and told me all about the good

+place.  She said all a body would have to do there was to go around all

+day long with a harp and sing, forever and ever.  So I didn't think

+much of it. But I never said so.  I asked her if she reckoned Tom Sawyer

+would go there, and she said not by a considerable sight.  I was glad

+about that, because I wanted him and me to be together.

+

+Miss Watson she kept pecking at me, and it got tiresome and lonesome.

+ By and by they fetched the niggers in and had prayers, and then

+everybody was off to bed.  I went up to my room with a piece of candle,

+and put it on the table.  Then I set down in a chair by the window and

+tried to think of something cheerful, but it warn't no use.  I felt

+so lonesome I most wished I was dead.  The stars were shining, and the

+leaves rustled in the woods ever so mournful; and I heard an owl, away

+off, who-whooing about somebody that was dead, and a whippowill and a

+dog crying about somebody that was going to die; and the wind was trying

+to whisper something to me, and I couldn't make out what it was, and so

+it made the cold shivers run over me. Then away out in the woods I heard

+that kind of a sound that a ghost makes when it wants to tell about

+something that's on its mind and can't make itself understood, and so

+can't rest easy in its grave, and has to go about that way every night

+grieving.  I got so down-hearted and scared I did wish I had some

+company.  Pretty soon a spider went crawling up my shoulder, and I

+flipped it off and it lit in the candle; and before I could budge it

+was all shriveled up.  I didn't need anybody to tell me that that was

+an awful bad sign and would fetch me some bad luck, so I was scared

+and most shook the clothes off of me. I got up and turned around in my

+tracks three times and crossed my breast every time; and then I tied

+up a little lock of my hair with a thread to keep witches away.  But

+I hadn't no confidence.  You do that when you've lost a horseshoe that

+you've found, instead of nailing it up over the door, but I hadn't ever

+heard anybody say it was any way to keep off bad luck when you'd killed

+a spider.

+

+I set down again, a-shaking all over, and got out my pipe for a smoke;

+for the house was all as still as death now, and so the widow wouldn't

+know. Well, after a long time I heard the clock away off in the town

+go boom—boom—boom—twelve licks; and all still again—stiller than

+ever. Pretty soon I heard a twig snap down in the dark amongst the

+trees—something was a stirring.  I set still and listened.  Directly I

+could just barely hear a "me-yow! me-yow!" down there.  That was good!

+ Says I, "me-yow! me-yow!" as soft as I could, and then I put out the

+light and scrambled out of the window on to the shed.  Then I slipped

+down to the ground and crawled in among the trees, and, sure enough,

+there was Tom Sawyer waiting for me.

+

+

+

+

+CHAPTER II.

+

+WE went tiptoeing along a path amongst the trees back towards the end of

+the widow's garden, stooping down so as the branches wouldn't scrape our

+heads. When we was passing by the kitchen I fell over a root and made

+a noise.  We scrouched down and laid still.  Miss Watson's big nigger,

+named Jim, was setting in the kitchen door; we could see him pretty

+clear, because there was a light behind him.  He got up and stretched

+his neck out about a minute, listening.  Then he says:

+

+"Who dah?"

+

+He listened some more; then he come tiptoeing down and stood right

+between us; we could a touched him, nearly.  Well, likely it was

+minutes and minutes that there warn't a sound, and we all there so close

+together.  There was a place on my ankle that got to itching, but I

+dasn't scratch it; and then my ear begun to itch; and next my back,

+right between my shoulders.  Seemed like I'd die if I couldn't scratch.

+ Well, I've noticed that thing plenty times since.  If you are with

+the quality, or at a funeral, or trying to go to sleep when you ain't

+sleepy—if you are anywheres where it won't do for you to scratch, why

+you will itch all over in upwards of a thousand places. Pretty soon Jim

+says:

+

+"Say, who is you?  Whar is you?  Dog my cats ef I didn' hear sumf'n.

+Well, I know what I's gwyne to do:  I's gwyne to set down here and

+listen tell I hears it agin."

+

+So he set down on the ground betwixt me and Tom.  He leaned his back up

+against a tree, and stretched his legs out till one of them most touched

+one of mine.  My nose begun to itch.  It itched till the tears come into

+my eyes.  But I dasn't scratch.  Then it begun to itch on the inside.

+Next I got to itching underneath.  I didn't know how I was going to set

+still. This miserableness went on as much as six or seven minutes; but

+it seemed a sight longer than that.  I was itching in eleven different

+places now.  I reckoned I couldn't stand it more'n a minute longer,

+but I set my teeth hard and got ready to try.  Just then Jim begun

+to breathe heavy; next he begun to snore—and then I was pretty soon

+comfortable again.

+

+Tom he made a sign to me—kind of a little noise with his mouth—and we

+went creeping away on our hands and knees.  When we was ten foot off Tom

+whispered to me, and wanted to tie Jim to the tree for fun.  But I said

+no; he might wake and make a disturbance, and then they'd find out I

+warn't in. Then Tom said he hadn't got candles enough, and he would slip

+in the kitchen and get some more.  I didn't want him to try.  I said Jim

+might wake up and come.  But Tom wanted to resk it; so we slid in there

+and got three candles, and Tom laid five cents on the table for pay.

+Then we got out, and I was in a sweat to get away; but nothing would do

+Tom but he must crawl to where Jim was, on his hands and knees, and play

+something on him.  I waited, and it seemed a good while, everything was

+so still and lonesome.

+

+As soon as Tom was back we cut along the path, around the garden fence,

+and by and by fetched up on the steep top of the hill the other side of

+the house.  Tom said he slipped Jim's hat off of his head and hung it

+on a limb right over him, and Jim stirred a little, but he didn't wake.

+Afterwards Jim said the witches be witched him and put him in a trance,

+and rode him all over the State, and then set him under the trees again,

+and hung his hat on a limb to show who done it.  And next time Jim told

+it he said they rode him down to New Orleans; and, after that, every

+time he told it he spread it more and more, till by and by he said they

+rode him all over the world, and tired him most to death, and his back

+was all over saddle-boils.  Jim was monstrous proud about it, and he

+got so he wouldn't hardly notice the other niggers.  Niggers would come

+miles to hear Jim tell about it, and he was more looked up to than any

+nigger in that country.  Strange niggers would stand with their mouths

+open and look him all over, same as if he was a wonder.  Niggers is

+always talking about witches in the dark by the kitchen fire; but

+whenever one was talking and letting on to know all about such things,

+Jim would happen in and say, "Hm!  What you know 'bout witches?" and

+that nigger was corked up and had to take a back seat.  Jim always kept

+that five-center piece round his neck with a string, and said it was a

+charm the devil give to him with his own hands, and told him he could

+cure anybody with it and fetch witches whenever he wanted to just by

+saying something to it; but he never told what it was he said to it.

+ Niggers would come from all around there and give Jim anything they

+had, just for a sight of that five-center piece; but they wouldn't touch

+it, because the devil had had his hands on it.  Jim was most ruined for

+a servant, because he got stuck up on account of having seen the devil

+and been rode by witches.

+

+Well, when Tom and me got to the edge of the hilltop we looked away down

+into the village and could see three or four lights twinkling, where

+there was sick folks, maybe; and the stars over us was sparkling ever

+so fine; and down by the village was the river, a whole mile broad, and

+awful still and grand.  We went down the hill and found Jo Harper and

+Ben Rogers, and two or three more of the boys, hid in the old tanyard.

+ So we unhitched a skiff and pulled down the river two mile and a half,

+to the big scar on the hillside, and went ashore.

+

+We went to a clump of bushes, and Tom made everybody swear to keep the

+secret, and then showed them a hole in the hill, right in the thickest

+part of the bushes.  Then we lit the candles, and crawled in on our

+hands and knees.  We went about two hundred yards, and then the cave

+opened up. Tom poked about amongst the passages, and pretty soon ducked

+under a wall where you wouldn't a noticed that there was a hole.  We

+went along a narrow place and got into a kind of room, all damp and

+sweaty and cold, and there we stopped.  Tom says:

+

+"Now, we'll start this band of robbers and call it Tom Sawyer's Gang.

+Everybody that wants to join has got to take an oath, and write his name

+in blood."

+

+Everybody was willing.  So Tom got out a sheet of paper that he had

+wrote the oath on, and read it.  It swore every boy to stick to the

+band, and never tell any of the secrets; and if anybody done anything to

+any boy in the band, whichever boy was ordered to kill that person and

+his family must do it, and he mustn't eat and he mustn't sleep till he

+had killed them and hacked a cross in their breasts, which was the sign

+of the band. And nobody that didn't belong to the band could use that

+mark, and if he did he must be sued; and if he done it again he must be

+killed.  And if anybody that belonged to the band told the secrets, he

+must have his throat cut, and then have his carcass burnt up and the

+ashes scattered all around, and his name blotted off of the list with

+blood and never mentioned again by the gang, but have a curse put on it

+and be forgot forever.

+

+Everybody said it was a real beautiful oath, and asked Tom if he got

+it out of his own head.  He said, some of it, but the rest was out of

+pirate-books and robber-books, and every gang that was high-toned had

+it.

+

+Some thought it would be good to kill the families of boys that told

+the secrets.  Tom said it was a good idea, so he took a pencil and wrote

+it in. Then Ben Rogers says:

+

+"Here's Huck Finn, he hain't got no family; what you going to do 'bout

+him?"

+

+"Well, hain't he got a father?" says Tom Sawyer.

+

+"Yes, he's got a father, but you can't never find him these days.  He

+used to lay drunk with the hogs in the tanyard, but he hain't been seen

+in these parts for a year or more."

+

+They talked it over, and they was going to rule me out, because they

+said every boy must have a family or somebody to kill, or else it

+wouldn't be fair and square for the others.  Well, nobody could think of

+anything to do—everybody was stumped, and set still.  I was most ready

+to cry; but all at once I thought of a way, and so I offered them Miss

+Watson—they could kill her.  Everybody said:

+

+"Oh, she'll do.  That's all right.  Huck can come in."

+

+Then they all stuck a pin in their fingers to get blood to sign with,

+and I made my mark on the paper.

+

+"Now," says Ben Rogers, "what's the line of business of this Gang?"

+

+"Nothing only robbery and murder," Tom said.

+

+"But who are we going to rob?—houses, or cattle, or—"

+

+"Stuff! stealing cattle and such things ain't robbery; it's burglary,"

+says Tom Sawyer.  "We ain't burglars.  That ain't no sort of style.  We

+are highwaymen.  We stop stages and carriages on the road, with masks

+on, and kill the people and take their watches and money."

+

+"Must we always kill the people?"

+

+"Oh, certainly.  It's best.  Some authorities think different, but

+mostly it's considered best to kill them—except some that you bring to

+the cave here, and keep them till they're ransomed."

+

+"Ransomed?  What's that?"

+

+"I don't know.  But that's what they do.  I've seen it in books; and so

+of course that's what we've got to do."

+

+"But how can we do it if we don't know what it is?"

+

+"Why, blame it all, we've got to do it.  Don't I tell you it's in the

+books?  Do you want to go to doing different from what's in the books,

+and get things all muddled up?"

+

+"Oh, that's all very fine to say, Tom Sawyer, but how in the nation

+are these fellows going to be ransomed if we don't know how to do it

+to them?—that's the thing I want to get at.  Now, what do you reckon it

+is?"

+

+"Well, I don't know.  But per'aps if we keep them till they're ransomed,

+it means that we keep them till they're dead."

+

+"Now, that's something like.  That'll answer.  Why couldn't you said

+that before?  We'll keep them till they're ransomed to death; and a

+bothersome lot they'll be, too—eating up everything, and always trying

+to get loose."

+

+"How you talk, Ben Rogers.  How can they get loose when there's a guard

+over them, ready to shoot them down if they move a peg?"

+

+"A guard!  Well, that is good.  So somebody's got to set up all night

+and never get any sleep, just so as to watch them.  I think that's

+foolishness. Why can't a body take a club and ransom them as soon as

+they get here?"

+

+"Because it ain't in the books so—that's why.  Now, Ben Rogers, do you

+want to do things regular, or don't you?—that's the idea.  Don't you

+reckon that the people that made the books knows what's the correct

+thing to do?  Do you reckon you can learn 'em anything?  Not by a good

+deal. No, sir, we'll just go on and ransom them in the regular way."

+

+"All right.  I don't mind; but I say it's a fool way, anyhow.  Say, do

+we kill the women, too?"

+

+"Well, Ben Rogers, if I was as ignorant as you I wouldn't let on.  Kill

+the women?  No; nobody ever saw anything in the books like that.  You

+fetch them to the cave, and you're always as polite as pie to them;

+and by and by they fall in love with you, and never want to go home any

+more."

+

+"Well, if that's the way I'm agreed, but I don't take no stock in it.

+Mighty soon we'll have the cave so cluttered up with women, and fellows

+waiting to be ransomed, that there won't be no place for the robbers.

+But go ahead, I ain't got nothing to say."

+

+Little Tommy Barnes was asleep now, and when they waked him up he was

+scared, and cried, and said he wanted to go home to his ma, and didn't

+want to be a robber any more.

+

+So they all made fun of him, and called him cry-baby, and that made him

+mad, and he said he would go straight and tell all the secrets.  But

+Tom give him five cents to keep quiet, and said we would all go home and

+meet next week, and rob somebody and kill some people.

+

+Ben Rogers said he couldn't get out much, only Sundays, and so he wanted

+to begin next Sunday; but all the boys said it would be wicked to do it

+on Sunday, and that settled the thing.  They agreed to get together and

+fix a day as soon as they could, and then we elected Tom Sawyer first

+captain and Jo Harper second captain of the Gang, and so started home.

+

+I clumb up the shed and crept into my window just before day was

+breaking. My new clothes was all greased up and clayey, and I was

+dog-tired.

+

+

+

+

+CHAPTER III.

+

+WELL, I got a good going-over in the morning from old Miss Watson on

+account of my clothes; but the widow she didn't scold, but only cleaned

+off the grease and clay, and looked so sorry that I thought I would

+behave awhile if I could.  Then Miss Watson she took me in the closet

+and prayed, but nothing come of it.  She told me to pray every day, and

+whatever I asked for I would get it.  But it warn't so.  I tried it.

+Once I got a fish-line, but no hooks.  It warn't any good to me without

+hooks.  I tried for the hooks three or four times, but somehow I

+couldn't make it work.  By and by, one day, I asked Miss Watson to

+try for me, but she said I was a fool.  She never told me why, and I

+couldn't make it out no way.

+

+I set down one time back in the woods, and had a long think about it.

+ I says to myself, if a body can get anything they pray for, why don't

+Deacon Winn get back the money he lost on pork?  Why can't the widow get

+back her silver snuffbox that was stole?  Why can't Miss Watson fat up?

+No, says I to my self, there ain't nothing in it.  I went and told the

+widow about it, and she said the thing a body could get by praying for

+it was "spiritual gifts."  This was too many for me, but she told me

+what she meant—I must help other people, and do everything I could for

+other people, and look out for them all the time, and never think about

+myself. This was including Miss Watson, as I took it.  I went out in the

+woods and turned it over in my mind a long time, but I couldn't see no

+advantage about it—except for the other people; so at last I reckoned

+I wouldn't worry about it any more, but just let it go.  Sometimes the

+widow would take me one side and talk about Providence in a way to make

+a body's mouth water; but maybe next day Miss Watson would take hold

+and knock it all down again.  I judged I could see that there was two

+Providences, and a poor chap would stand considerable show with the

+widow's Providence, but if Miss Watson's got him there warn't no help

+for him any more.  I thought it all out, and reckoned I would belong

+to the widow's if he wanted me, though I couldn't make out how he was

+a-going to be any better off then than what he was before, seeing I was

+so ignorant, and so kind of low-down and ornery.

+

+Pap he hadn't been seen for more than a year, and that was comfortable

+for me; I didn't want to see him no more.  He used to always whale me

+when he was sober and could get his hands on me; though I used to take

+to the woods most of the time when he was around.  Well, about this time

+he was found in the river drownded, about twelve mile above town, so

+people said.  They judged it was him, anyway; said this drownded man was

+just his size, and was ragged, and had uncommon long hair, which was all

+like pap; but they couldn't make nothing out of the face, because it had

+been in the water so long it warn't much like a face at all.  They said

+he was floating on his back in the water.  They took him and buried him

+on the bank.  But I warn't comfortable long, because I happened to think

+of something.  I knowed mighty well that a drownded man don't float on

+his back, but on his face.  So I knowed, then, that this warn't pap, but

+a woman dressed up in a man's clothes.  So I was uncomfortable again.

+ I judged the old man would turn up again by and by, though I wished he

+wouldn't.

+

+We played robber now and then about a month, and then I resigned.  All

+the boys did.  We hadn't robbed nobody, hadn't killed any people, but

+only just pretended.  We used to hop out of the woods and go charging

+down on hog-drivers and women in carts taking garden stuff to market,

+but we never hived any of them.  Tom Sawyer called the hogs "ingots,"

+and he called the turnips and stuff "julery," and we would go to the

+cave and powwow over what we had done, and how many people we had killed

+and marked.  But I couldn't see no profit in it.  One time Tom sent a

+boy to run about town with a blazing stick, which he called a slogan

+(which was the sign for the Gang to get together), and then he said he

+had got secret news by his spies that next day a whole parcel of Spanish

+merchants and rich A-rabs was going to camp in Cave Hollow with two

+hundred elephants, and six hundred camels, and over a thousand "sumter"

+mules, all loaded down with di'monds, and they didn't have only a guard

+of four hundred soldiers, and so we would lay in ambuscade, as he called

+it, and kill the lot and scoop the things.  He said we must slick up

+our swords and guns, and get ready.  He never could go after even a

+turnip-cart but he must have the swords and guns all scoured up for it,

+though they was only lath and broomsticks, and you might scour at them

+till you rotted, and then they warn't worth a mouthful of ashes more

+than what they was before.  I didn't believe we could lick such a crowd

+of Spaniards and A-rabs, but I wanted to see the camels and elephants,

+so I was on hand next day, Saturday, in the ambuscade; and when we got

+the word we rushed out of the woods and down the hill.  But there warn't

+no Spaniards and A-rabs, and there warn't no camels nor no elephants.

+ It warn't anything but a Sunday-school picnic, and only a primer-class

+at that.  We busted it up, and chased the children up the hollow; but we

+never got anything but some doughnuts and jam, though Ben Rogers got

+a rag doll, and Jo Harper got a hymn-book and a tract; and then the

+teacher charged in, and made us drop everything and cut.

+

+ I didn't see no di'monds, and I told Tom Sawyer so.  He said there was

+loads of them there, anyway; and he said there was A-rabs there, too,

+and elephants and things.  I said, why couldn't we see them, then?  He

+said if I warn't so ignorant, but had read a book called Don Quixote, I

+would know without asking.  He said it was all done by enchantment.  He

+said there was hundreds of soldiers there, and elephants and treasure,

+and so on, but we had enemies which he called magicians; and they had

+turned the whole thing into an infant Sunday-school, just out of spite.

+ I said, all right; then the thing for us to do was to go for the

+magicians.  Tom Sawyer said I was a numskull.

+

+"Why," said he, "a magician could call up a lot of genies, and they

+would hash you up like nothing before you could say Jack Robinson.  They

+are as tall as a tree and as big around as a church."

+

+"Well," I says, "s'pose we got some genies to help us—can't we lick

+the other crowd then?"

+

+"How you going to get them?"

+

+"I don't know.  How do they get them?"

+

+"Why, they rub an old tin lamp or an iron ring, and then the genies

+come tearing in, with the thunder and lightning a-ripping around and the

+smoke a-rolling, and everything they're told to do they up and do it.

+ They don't think nothing of pulling a shot-tower up by the roots, and

+belting a Sunday-school superintendent over the head with it—or any

+other man."

+

+"Who makes them tear around so?"

+

+"Why, whoever rubs the lamp or the ring.  They belong to whoever rubs

+the lamp or the ring, and they've got to do whatever he says.  If he

+tells them to build a palace forty miles long out of di'monds, and fill

+it full of chewing-gum, or whatever you want, and fetch an emperor's

+daughter from China for you to marry, they've got to do it—and they've

+got to do it before sun-up next morning, too.  And more:  they've got

+to waltz that palace around over the country wherever you want it, you

+understand."

+

+"Well," says I, "I think they are a pack of flat-heads for not keeping

+the palace themselves 'stead of fooling them away like that.  And what's

+more—if I was one of them I would see a man in Jericho before I would

+drop my business and come to him for the rubbing of an old tin lamp."

+

+"How you talk, Huck Finn.  Why, you'd have to come when he rubbed it,

+whether you wanted to or not."

+

+"What! and I as high as a tree and as big as a church?  All right, then;

+I would come; but I lay I'd make that man climb the highest tree there

+was in the country."

+

+"Shucks, it ain't no use to talk to you, Huck Finn.  You don't seem to

+know anything, somehow—perfect saphead."

+

+I thought all this over for two or three days, and then I reckoned I

+would see if there was anything in it.  I got an old tin lamp and an

+iron ring, and went out in the woods and rubbed and rubbed till I sweat

+like an Injun, calculating to build a palace and sell it; but it warn't

+no use, none of the genies come.  So then I judged that all that stuff

+was only just one of Tom Sawyer's lies.  I reckoned he believed in the

+A-rabs and the elephants, but as for me I think different.  It had all

+the marks of a Sunday-school.

+

+

+

+

+CHAPTER IV.

+

+WELL, three or four months run along, and it was well into the winter

+now. I had been to school most all the time and could spell and read and

+write just a little, and could say the multiplication table up to six

+times seven is thirty-five, and I don't reckon I could ever get any

+further than that if I was to live forever.  I don't take no stock in

+mathematics, anyway.

+

+At first I hated the school, but by and by I got so I could stand it.

+Whenever I got uncommon tired I played hookey, and the hiding I got next

+day done me good and cheered me up.  So the longer I went to school the

+easier it got to be.  I was getting sort of used to the widow's ways,

+too, and they warn't so raspy on me.  Living in a house and sleeping in

+a bed pulled on me pretty tight mostly, but before the cold weather I

+used to slide out and sleep in the woods sometimes, and so that was a

+rest to me.  I liked the old ways best, but I was getting so I liked the

+new ones, too, a little bit. The widow said I was coming along slow but

+sure, and doing very satisfactory.  She said she warn't ashamed of me.

+

+One morning I happened to turn over the salt-cellar at breakfast.

+ I reached for some of it as quick as I could to throw over my left

+shoulder and keep off the bad luck, but Miss Watson was in ahead of me,

+and crossed me off. She says, "Take your hands away, Huckleberry; what

+a mess you are always making!"  The widow put in a good word for me, but

+that warn't going to keep off the bad luck, I knowed that well enough.

+ I started out, after breakfast, feeling worried and shaky, and

+wondering where it was going to fall on me, and what it was going to be.

+ There is ways to keep off some kinds of bad luck, but this wasn't one

+of them kind; so I never tried to do anything, but just poked along

+low-spirited and on the watch-out.

+

+I went down to the front garden and clumb over the stile where you go

+through the high board fence.  There was an inch of new snow on the

+ground, and I seen somebody's tracks.  They had come up from the quarry

+and stood around the stile a while, and then went on around the garden

+fence.  It was funny they hadn't come in, after standing around so.  I

+couldn't make it out.  It was very curious, somehow.  I was going to

+follow around, but I stooped down to look at the tracks first.  I didn't

+notice anything at first, but next I did.  There was a cross in the left

+boot-heel made with big nails, to keep off the devil.

+

+I was up in a second and shinning down the hill.  I looked over my

+shoulder every now and then, but I didn't see nobody.  I was at Judge

+Thatcher's as quick as I could get there.  He said:

+

+"Why, my boy, you are all out of breath.  Did you come for your

+interest?"

+

+"No, sir," I says; "is there some for me?"

+

+"Oh, yes, a half-yearly is in last night—over a hundred and fifty

+dollars.  Quite a fortune for you.  You had better let me invest it

+along with your six thousand, because if you take it you'll spend it."

+

+"No, sir," I says, "I don't want to spend it.  I don't want it at

+all—nor the six thousand, nuther.  I want you to take it; I want to give

+it to you—the six thousand and all."

+

+He looked surprised.  He couldn't seem to make it out.  He says:

+

+"Why, what can you mean, my boy?"

+

+I says, "Don't you ask me no questions about it, please.  You'll take

+it—won't you?"

+

+He says:

+

+"Well, I'm puzzled.  Is something the matter?"

+

+"Please take it," says I, "and don't ask me nothing—then I won't have to

+tell no lies."

+

+He studied a while, and then he says:

+

+"Oho-o!  I think I see.  You want to sell all your property to me—not

+give it.  That's the correct idea."

+

+Then he wrote something on a paper and read it over, and says:

+

+"There; you see it says 'for a consideration.'  That means I have bought

+it of you and paid you for it.  Here's a dollar for you.  Now you sign

+it."

+

+So I signed it, and left.

+

+Miss Watson's nigger, Jim, had a hair-ball as big as your fist, which

+had been took out of the fourth stomach of an ox, and he used to do

+magic with it.  He said there was a spirit inside of it, and it knowed

+everything.  So I went to him that night and told him pap was here

+again, for I found his tracks in the snow.  What I wanted to know was,

+what he was going to do, and was he going to stay?  Jim got out his

+hair-ball and said something over it, and then he held it up and dropped

+it on the floor.  It fell pretty solid, and only rolled about an inch.

+ Jim tried it again, and then another time, and it acted just the same.

+ Jim got down on his knees, and put his ear against it and listened.

+ But it warn't no use; he said it wouldn't talk. He said sometimes it

+wouldn't talk without money.  I told him I had an old slick counterfeit

+quarter that warn't no good because the brass showed through the silver

+a little, and it wouldn't pass nohow, even if the brass didn't show,

+because it was so slick it felt greasy, and so that would tell on it

+every time.  (I reckoned I wouldn't say nothing about the dollar I got

+from the judge.) I said it was pretty bad money, but maybe the hair-ball

+would take it, because maybe it wouldn't know the difference.  Jim smelt

+it and bit it and rubbed it, and said he would manage so the hair-ball

+would think it was good.  He said he would split open a raw Irish potato

+and stick the quarter in between and keep it there all night, and next

+morning you couldn't see no brass, and it wouldn't feel greasy no more,

+and so anybody in town would take it in a minute, let alone a hair-ball.

+ Well, I knowed a potato would do that before, but I had forgot it.

+

+Jim put the quarter under the hair-ball, and got down and listened

+again. This time he said the hair-ball was all right.  He said it

+would tell my whole fortune if I wanted it to.  I says, go on.  So the

+hair-ball talked to Jim, and Jim told it to me.  He says:

+

+"Yo' ole father doan' know yit what he's a-gwyne to do.  Sometimes he

+spec he'll go 'way, en den agin he spec he'll stay.  De bes' way is to

+res' easy en let de ole man take his own way.  Dey's two angels hoverin'

+roun' 'bout him.  One uv 'em is white en shiny, en t'other one is black.

+De white one gits him to go right a little while, den de black one sail

+in en bust it all up.  A body can't tell yit which one gwyne to fetch

+him at de las'.  But you is all right.  You gwyne to have considable

+trouble in yo' life, en considable joy.  Sometimes you gwyne to git

+hurt, en sometimes you gwyne to git sick; but every time you's gwyne

+to git well agin.  Dey's two gals flyin' 'bout you in yo' life.  One

+uv 'em's light en t'other one is dark. One is rich en t'other is po'.

+ You's gwyne to marry de po' one fust en de rich one by en by.  You

+wants to keep 'way fum de water as much as you kin, en don't run no

+resk, 'kase it's down in de bills dat you's gwyne to git hung."

+

+When I lit my candle and went up to my room that night there sat pap his

+own self!

+

+

+

+

+CHAPTER V.

+

+I had shut the door to.  Then I turned around and there he was.  I used

+to be scared of him all the time, he tanned me so much.  I reckoned I

+was scared now, too; but in a minute I see I was mistaken—that is, after

+the first jolt, as you may say, when my breath sort of hitched, he being

+so unexpected; but right away after I see I warn't scared of him worth

+bothring about.

+

+He was most fifty, and he looked it.  His hair was long and tangled and

+greasy, and hung down, and you could see his eyes shining through

+like he was behind vines.  It was all black, no gray; so was his long,

+mixed-up whiskers.  There warn't no color in his face, where his face

+showed; it was white; not like another man's white, but a white to make

+a body sick, a white to make a body's flesh crawl—a tree-toad white, a

+fish-belly white.  As for his clothes—just rags, that was all.  He had

+one ankle resting on t'other knee; the boot on that foot was busted, and

+two of his toes stuck through, and he worked them now and then.  His hat

+was laying on the floor—an old black slouch with the top caved in, like

+a lid.

+

+I stood a-looking at him; he set there a-looking at me, with his chair

+tilted back a little.  I set the candle down.  I noticed the window was

+up; so he had clumb in by the shed.  He kept a-looking me all over.  By

+and by he says:

+

+"Starchy clothes—very.  You think you're a good deal of a big-bug,

+don't you?"

+

+"Maybe I am, maybe I ain't," I says.

+

+"Don't you give me none o' your lip," says he.  "You've put on

+considerable many frills since I been away.  I'll take you down a peg

+before I get done with you.  You're educated, too, they say—can read and

+write.  You think you're better'n your father, now, don't you, because

+he can't?  I'll take it out of you.  Who told you you might meddle

+with such hifalut'n foolishness, hey?—who told you you could?"

+

+"The widow.  She told me."

+

+"The widow, hey?—and who told the widow she could put in her shovel

+about a thing that ain't none of her business?"

+

+"Nobody never told her."

+

+"Well, I'll learn her how to meddle.  And looky here—you drop that

+school, you hear?  I'll learn people to bring up a boy to put on airs

+over his own father and let on to be better'n what he is.  You lemme

+catch you fooling around that school again, you hear?  Your mother

+couldn't read, and she couldn't write, nuther, before she died.  None

+of the family couldn't before they died.  I can't; and here you're

+a-swelling yourself up like this.  I ain't the man to stand it—you hear?

+Say, lemme hear you read."

+

+I took up a book and begun something about General Washington and the

+wars. When I'd read about a half a minute, he fetched the book a whack

+with his hand and knocked it across the house.  He says:

+

+"It's so.  You can do it.  I had my doubts when you told me.  Now looky

+here; you stop that putting on frills.  I won't have it.  I'll lay for

+you, my smarty; and if I catch you about that school I'll tan you good.

+First you know you'll get religion, too.  I never see such a son."

+

+He took up a little blue and yaller picture of some cows and a boy, and

+says:

+

+"What's this?"

+

+"It's something they give me for learning my lessons good."

+

+He tore it up, and says:

+

+"I'll give you something better—I'll give you a cowhide."

+

+He set there a-mumbling and a-growling a minute, and then he says:

+

+"Ain't you a sweet-scented dandy, though?  A bed; and bedclothes; and

+a look'n'-glass; and a piece of carpet on the floor—and your own father

+got to sleep with the hogs in the tanyard.  I never see such a son.  I

+bet I'll take some o' these frills out o' you before I'm done with you.

+Why, there ain't no end to your airs—they say you're rich.  Hey?—how's

+that?"

+

+"They lie—that's how."

+

+"Looky here—mind how you talk to me; I'm a-standing about all I can

+stand now—so don't gimme no sass.  I've been in town two days, and I

+hain't heard nothing but about you bein' rich.  I heard about it

+away down the river, too.  That's why I come.  You git me that money

+to-morrow—I want it."

+

+"I hain't got no money."

+

+"It's a lie.  Judge Thatcher's got it.  You git it.  I want it."

+

+"I hain't got no money, I tell you.  You ask Judge Thatcher; he'll tell

+you the same."

+

+"All right.  I'll ask him; and I'll make him pungle, too, or I'll know

+the reason why.  Say, how much you got in your pocket?  I want it."

+

+"I hain't got only a dollar, and I want that to—"

+

+"It don't make no difference what you want it for—you just shell it

+out."

+

+He took it and bit it to see if it was good, and then he said he was

+going down town to get some whisky; said he hadn't had a drink all day.

+When he had got out on the shed he put his head in again, and cussed

+me for putting on frills and trying to be better than him; and when I

+reckoned he was gone he come back and put his head in again, and told me

+to mind about that school, because he was going to lay for me and lick

+me if I didn't drop that.

+

+Next day he was drunk, and he went to Judge Thatcher's and bullyragged

+him, and tried to make him give up the money; but he couldn't, and then

+he swore he'd make the law force him.

+

+The judge and the widow went to law to get the court to take me away

+from him and let one of them be my guardian; but it was a new judge that

+had just come, and he didn't know the old man; so he said courts mustn't

+interfere and separate families if they could help it; said he'd druther

+not take a child away from its father.  So Judge Thatcher and the widow

+had to quit on the business.

+

+That pleased the old man till he couldn't rest.  He said he'd cowhide

+me till I was black and blue if I didn't raise some money for him.  I

+borrowed three dollars from Judge Thatcher, and pap took it and got

+drunk, and went a-blowing around and cussing and whooping and carrying

+on; and he kept it up all over town, with a tin pan, till most midnight;

+then they jailed him, and next day they had him before court, and jailed

+him again for a week.  But he said he was satisfied; said he was boss

+of his son, and he'd make it warm for him.

+

+When he got out the new judge said he was a-going to make a man of him.

+So he took him to his own house, and dressed him up clean and nice, and

+had him to breakfast and dinner and supper with the family, and was just

+old pie to him, so to speak.  And after supper he talked to him about

+temperance and such things till the old man cried, and said he'd been

+a fool, and fooled away his life; but now he was a-going to turn over

+a new leaf and be a man nobody wouldn't be ashamed of, and he hoped the

+judge would help him and not look down on him.  The judge said he could

+hug him for them words; so he cried, and his wife she cried again; pap

+said he'd been a man that had always been misunderstood before, and the

+judge said he believed it.  The old man said that what a man wanted

+that was down was sympathy, and the judge said it was so; so they cried

+again.  And when it was bedtime the old man rose up and held out his

+hand, and says:

+

+"Look at it, gentlemen and ladies all; take a-hold of it; shake it.

+There's a hand that was the hand of a hog; but it ain't so no more; it's

+the hand of a man that's started in on a new life, and'll die before

+he'll go back.  You mark them words—don't forget I said them.  It's a

+clean hand now; shake it—don't be afeard."

+

+So they shook it, one after the other, all around, and cried.  The

+judge's wife she kissed it.  Then the old man he signed a pledge—made

+his mark. The judge said it was the holiest time on record, or something

+like that. Then they tucked the old man into a beautiful room, which was

+the spare room, and in the night some time he got powerful thirsty and

+clumb out on to the porch-roof and slid down a stanchion and traded his

+new coat for a jug of forty-rod, and clumb back again and had a good old

+time; and towards daylight he crawled out again, drunk as a fiddler, and

+rolled off the porch and broke his left arm in two places, and was most

+froze to death when somebody found him after sun-up.  And when they come

+to look at that spare room they had to take soundings before they could

+navigate it.

+

+The judge he felt kind of sore.  He said he reckoned a body could reform

+the old man with a shotgun, maybe, but he didn't know no other way.

+

+

+

+

+CHAPTER VI.

+

+WELL, pretty soon the old man was up and around again, and then he went

+for Judge Thatcher in the courts to make him give up that money, and he

+went for me, too, for not stopping school.  He catched me a couple of

+times and thrashed me, but I went to school just the same, and dodged

+him or outrun him most of the time.  I didn't want to go to school much

+before, but I reckoned I'd go now to spite pap.  That law trial was a

+slow business—appeared like they warn't ever going to get started on it;

+so every now and then I'd borrow two or three dollars off of the judge

+for him, to keep from getting a cowhiding.  Every time he got money he

+got drunk; and every time he got drunk he raised Cain around town; and

+every time he raised Cain he got jailed.  He was just suited—this kind

+of thing was right in his line.

+

+He got to hanging around the widow's too much and so she told him at

+last that if he didn't quit using around there she would make trouble

+for him. Well, wasn't he mad?  He said he would show who was Huck

+Finn's boss.  So he watched out for me one day in the spring, and

+catched me, and took me up the river about three mile in a skiff, and

+crossed over to the Illinois shore where it was woody and there warn't

+no houses but an old log hut in a place where the timber was so thick

+you couldn't find it if you didn't know where it was.

+

+He kept me with him all the time, and I never got a chance to run off.

+We lived in that old cabin, and he always locked the door and put the

+key under his head nights.  He had a gun which he had stole, I reckon,

+and we fished and hunted, and that was what we lived on.  Every little

+while he locked me in and went down to the store, three miles, to the

+ferry, and traded fish and game for whisky, and fetched it home and got

+drunk and had a good time, and licked me.  The widow she found out where

+I was by and by, and she sent a man over to try to get hold of me; but

+pap drove him off with the gun, and it warn't long after that till I was

+used to being where I was, and liked it—all but the cowhide part.

+

+It was kind of lazy and jolly, laying off comfortable all day, smoking

+and fishing, and no books nor study.  Two months or more run along, and

+my clothes got to be all rags and dirt, and I didn't see how I'd ever

+got to like it so well at the widow's, where you had to wash, and eat on

+a plate, and comb up, and go to bed and get up regular, and be forever

+bothering over a book, and have old Miss Watson pecking at you all the

+time.  I didn't want to go back no more.  I had stopped cussing, because

+the widow didn't like it; but now I took to it again because pap hadn't

+no objections.  It was pretty good times up in the woods there, take it

+all around.

+

+But by and by pap got too handy with his hick'ry, and I couldn't stand

+it. I was all over welts.  He got to going away so much, too, and

+locking me in.  Once he locked me in and was gone three days.  It was

+dreadful lonesome.  I judged he had got drownded, and I wasn't ever

+going to get out any more.  I was scared.  I made up my mind I would fix

+up some way to leave there.  I had tried to get out of that cabin many

+a time, but I couldn't find no way.  There warn't a window to it big

+enough for a dog to get through.  I couldn't get up the chimbly; it

+was too narrow.  The door was thick, solid oak slabs.  Pap was pretty

+careful not to leave a knife or anything in the cabin when he was away;

+I reckon I had hunted the place over as much as a hundred times; well, I

+was most all the time at it, because it was about the only way to put in

+the time.  But this time I found something at last; I found an old rusty

+wood-saw without any handle; it was laid in between a rafter and the

+clapboards of the roof. I greased it up and went to work.  There was an

+old horse-blanket nailed against the logs at the far end of the cabin

+behind the table, to keep the wind from blowing through the chinks and

+putting the candle out.  I got under the table and raised the blanket,

+and went to work to saw a section of the big bottom log out—big enough

+to let me through.  Well, it was a good long job, but I was getting

+towards the end of it when I heard pap's gun in the woods.  I got rid of

+the signs of my work, and dropped the blanket and hid my saw, and pretty

+soon pap come in.

+

+Pap warn't in a good humor—so he was his natural self.  He said he was

+down town, and everything was going wrong.  His lawyer said he reckoned

+he would win his lawsuit and get the money if they ever got started on

+the trial; but then there was ways to put it off a long time, and Judge

+Thatcher knowed how to do it. And he said people allowed there'd be

+another trial to get me away from him and give me to the widow for my

+guardian, and they guessed it would win this time.  This shook me up

+considerable, because I didn't want to go back to the widow's any more

+and be so cramped up and sivilized, as they called it.  Then the old man

+got to cussing, and cussed everything and everybody he could think of,

+and then cussed them all over again to make sure he hadn't skipped any,

+and after that he polished off with a kind of a general cuss all round,

+including a considerable parcel of people which he didn't know the names

+of, and so called them what's-his-name when he got to them, and went

+right along with his cussing.

+

+He said he would like to see the widow get me.  He said he would watch

+out, and if they tried to come any such game on him he knowed of a place

+six or seven mile off to stow me in, where they might hunt till they

+dropped and they couldn't find me.  That made me pretty uneasy again,

+but only for a minute; I reckoned I wouldn't stay on hand till he got

+that chance.

+

+The old man made me go to the skiff and fetch the things he had

+got. There was a fifty-pound sack of corn meal, and a side of bacon,

+ammunition, and a four-gallon jug of whisky, and an old book and two

+newspapers for wadding, besides some tow.  I toted up a load, and went

+back and set down on the bow of the skiff to rest.  I thought it all

+over, and I reckoned I would walk off with the gun and some lines, and

+take to the woods when I run away.  I guessed I wouldn't stay in one

+place, but just tramp right across the country, mostly night times, and

+hunt and fish to keep alive, and so get so far away that the old man nor

+the widow couldn't ever find me any more.  I judged I would saw out and

+leave that night if pap got drunk enough, and I reckoned he would.  I

+got so full of it I didn't notice how long I was staying till the old

+man hollered and asked me whether I was asleep or drownded.

+

+I got the things all up to the cabin, and then it was about dark.  While

+I was cooking supper the old man took a swig or two and got sort of

+warmed up, and went to ripping again.  He had been drunk over in town,

+and laid in the gutter all night, and he was a sight to look at.  A body

+would a thought he was Adam—he was just all mud.  Whenever his liquor

+begun to work he most always went for the govment, this time he says:

+

+"Call this a govment! why, just look at it and see what it's like.

+Here's the law a-standing ready to take a man's son away from him—a

+man's own son, which he has had all the trouble and all the anxiety

+and all the expense of raising.  Yes, just as that man has got that

+son raised at last, and ready to go to work and begin to do suthin' for

+him and give him a rest, the law up and goes for him.  And they call

+that govment!  That ain't all, nuther.  The law backs that old Judge

+Thatcher up and helps him to keep me out o' my property.  Here's what

+the law does:  The law takes a man worth six thousand dollars and

+up'ards, and jams him into an old trap of a cabin like this, and lets

+him go round in clothes that ain't fitten for a hog. They call that

+govment!  A man can't get his rights in a govment like this. Sometimes

+I've a mighty notion to just leave the country for good and all. Yes,

+and I told 'em so; I told old Thatcher so to his face.  Lots of 'em

+heard me, and can tell what I said.  Says I, for two cents I'd leave the

+blamed country and never come a-near it agin.  Them's the very words.  I

+says look at my hat—if you call it a hat—but the lid raises up and the

+rest of it goes down till it's below my chin, and then it ain't rightly

+a hat at all, but more like my head was shoved up through a jint o'

+stove-pipe.  Look at it, says I—such a hat for me to wear—one of the

+wealthiest men in this town if I could git my rights.

+

+"Oh, yes, this is a wonderful govment, wonderful.  Why, looky here.

+There was a free nigger there from Ohio—a mulatter, most as white as

+a white man.  He had the whitest shirt on you ever see, too, and the

+shiniest hat; and there ain't a man in that town that's got as fine

+clothes as what he had; and he had a gold watch and chain, and a

+silver-headed cane—the awfulest old gray-headed nabob in the State.  And

+what do you think?  They said he was a p'fessor in a college, and could

+talk all kinds of languages, and knowed everything.  And that ain't the

+wust. They said he could vote when he was at home.  Well, that let me

+out. Thinks I, what is the country a-coming to?  It was 'lection day,

+and I was just about to go and vote myself if I warn't too drunk to get

+there; but when they told me there was a State in this country where

+they'd let that nigger vote, I drawed out.  I says I'll never vote agin.

+ Them's the very words I said; they all heard me; and the country may

+rot for all me—I'll never vote agin as long as I live.  And to see the

+cool way of that nigger—why, he wouldn't a give me the road if I hadn't

+shoved him out o' the way.  I says to the people, why ain't this nigger

+put up at auction and sold?—that's what I want to know.  And what do you

+reckon they said? Why, they said he couldn't be sold till he'd been in

+the State six months, and he hadn't been there that long yet.  There,

+now—that's a specimen.  They call that a govment that can't sell a free

+nigger till he's been in the State six months.  Here's a govment that

+calls itself a govment, and lets on to be a govment, and thinks it is a

+govment, and yet's got to set stock-still for six whole months before

+it can take a hold of a prowling, thieving, infernal, white-shirted free

+nigger, and—"

+

+Pap was agoing on so he never noticed where his old limber legs was

+taking him to, so he went head over heels over the tub of salt pork and

+barked both shins, and the rest of his speech was all the hottest kind

+of language—mostly hove at the nigger and the govment, though he give

+the tub some, too, all along, here and there.  He hopped around the

+cabin considerable, first on one leg and then on the other, holding

+first one shin and then the other one, and at last he let out with his

+left foot all of a sudden and fetched the tub a rattling kick.  But it

+warn't good judgment, because that was the boot that had a couple of his

+toes leaking out of the front end of it; so now he raised a howl that

+fairly made a body's hair raise, and down he went in the dirt, and

+rolled there, and held his toes; and the cussing he done then laid over

+anything he had ever done previous.  He said so his own self afterwards.

+ He had heard old Sowberry Hagan in his best days, and he said it laid

+over him, too; but I reckon that was sort of piling it on, maybe.

+

+After supper pap took the jug, and said he had enough whisky there

+for two drunks and one delirium tremens.  That was always his word.  I

+judged he would be blind drunk in about an hour, and then I would steal

+the key, or saw myself out, one or t'other.  He drank and drank, and

+tumbled down on his blankets by and by; but luck didn't run my way.

+ He didn't go sound asleep, but was uneasy.  He groaned and moaned and

+thrashed around this way and that for a long time.  At last I got so

+sleepy I couldn't keep my eyes open all I could do, and so before I

+knowed what I was about I was sound asleep, and the candle burning.

+

+I don't know how long I was asleep, but all of a sudden there was an

+awful scream and I was up.  There was pap looking wild, and skipping

+around every which way and yelling about snakes.  He said they was

+crawling up his legs; and then he would give a jump and scream, and say

+one had bit him on the cheek—but I couldn't see no snakes.  He started

+and run round and round the cabin, hollering "Take him off! take him

+off! he's biting me on the neck!"  I never see a man look so wild in the

+eyes. Pretty soon he was all fagged out, and fell down panting; then he

+rolled over and over wonderful fast, kicking things every which way,

+and striking and grabbing at the air with his hands, and screaming and

+saying there was devils a-hold of him.  He wore out by and by, and laid

+still a while, moaning.  Then he laid stiller, and didn't make a sound.

+ I could hear the owls and the wolves away off in the woods, and it

+seemed terrible still.  He was laying over by the corner. By and by he

+raised up part way and listened, with his head to one side.  He says,

+very low:

+

+"Tramp—tramp—tramp; that's the dead; tramp—tramp—tramp; they're coming

+after me; but I won't go.  Oh, they're here! don't touch me—don't! hands

+off—they're cold; let go.  Oh, let a poor devil alone!"

+

+Then he went down on all fours and crawled off, begging them to let him

+alone, and he rolled himself up in his blanket and wallowed in under the

+old pine table, still a-begging; and then he went to crying.  I could

+hear him through the blanket.

+

+By and by he rolled out and jumped up on his feet looking wild, and he

+see me and went for me.  He chased me round and round the place with a

+clasp-knife, calling me the Angel of Death, and saying he would kill me,

+and then I couldn't come for him no more.  I begged, and told him I

+was only Huck; but he laughed such a screechy laugh, and roared and

+cussed, and kept on chasing me up.  Once when I turned short and

+dodged under his arm he made a grab and got me by the jacket between my

+shoulders, and I thought I was gone; but I slid out of the jacket quick

+as lightning, and saved myself. Pretty soon he was all tired out, and

+dropped down with his back against the door, and said he would rest a

+minute and then kill me. He put his knife under him, and said he would

+sleep and get strong, and then he would see who was who.

+

+So he dozed off pretty soon.  By and by I got the old split-bottom chair

+and clumb up as easy as I could, not to make any noise, and got down the

+gun.  I slipped the ramrod down it to make sure it was loaded, then I

+laid it across the turnip barrel, pointing towards pap, and set down

+behind it to wait for him to stir.  And how slow and still the time did

+drag along.

+

+

+

+

+CHAPTER VII.

+

+"GIT up!  What you 'bout?"

+

+I opened my eyes and looked around, trying to make out where I was.  It

+was after sun-up, and I had been sound asleep.  Pap was standing over me

+looking sour and sick, too.  He says:

+

+"What you doin' with this gun?"

+

+I judged he didn't know nothing about what he had been doing, so I says:

+

+"Somebody tried to get in, so I was laying for him."

+

+"Why didn't you roust me out?"

+

+"Well, I tried to, but I couldn't; I couldn't budge you."

+

+"Well, all right.  Don't stand there palavering all day, but out with

+you and see if there's a fish on the lines for breakfast.  I'll be along

+in a minute."

+

+He unlocked the door, and I cleared out up the river-bank.  I noticed

+some pieces of limbs and such things floating down, and a sprinkling of

+bark; so I knowed the river had begun to rise.  I reckoned I would have

+great times now if I was over at the town.  The June rise used to be

+always luck for me; because as soon as that rise begins here comes

+cordwood floating down, and pieces of log rafts—sometimes a dozen logs

+together; so all you have to do is to catch them and sell them to the

+wood-yards and the sawmill.

+

+I went along up the bank with one eye out for pap and t'other one out

+for what the rise might fetch along.  Well, all at once here comes a

+canoe; just a beauty, too, about thirteen or fourteen foot long, riding

+high like a duck.  I shot head-first off of the bank like a frog,

+clothes and all on, and struck out for the canoe.  I just expected

+there'd be somebody laying down in it, because people often done that

+to fool folks, and when a chap had pulled a skiff out most to it they'd

+raise up and laugh at him.  But it warn't so this time.  It was a

+drift-canoe sure enough, and I clumb in and paddled her ashore.  Thinks

+I, the old man will be glad when he sees this—she's worth ten dollars.

+ But when I got to shore pap wasn't in sight yet, and as I was running

+her into a little creek like a gully, all hung over with vines and

+willows, I struck another idea:  I judged I'd hide her good, and then,

+'stead of taking to the woods when I run off, I'd go down the river

+about fifty mile and camp in one place for good, and not have such a

+rough time tramping on foot.

+

+It was pretty close to the shanty, and I thought I heard the old man

+coming all the time; but I got her hid; and then I out and looked around

+a bunch of willows, and there was the old man down the path a piece just

+drawing a bead on a bird with his gun.  So he hadn't seen anything.

+

+When he got along I was hard at it taking up a "trot" line.  He abused

+me a little for being so slow; but I told him I fell in the river, and

+that was what made me so long.  I knowed he would see I was wet, and

+then he would be asking questions.  We got five catfish off the lines

+and went home.

+

+While we laid off after breakfast to sleep up, both of us being about

+wore out, I got to thinking that if I could fix up some way to keep pap

+and the widow from trying to follow me, it would be a certainer thing

+than trusting to luck to get far enough off before they missed me; you

+see, all kinds of things might happen.  Well, I didn't see no way for a

+while, but by and by pap raised up a minute to drink another barrel of

+water, and he says:

+

+"Another time a man comes a-prowling round here you roust me out, you

+hear? That man warn't here for no good.  I'd a shot him.  Next time you

+roust me out, you hear?"

+

+Then he dropped down and went to sleep again; but what he had been

+saying give me the very idea I wanted.  I says to myself, I can fix it

+now so nobody won't think of following me.

+

+About twelve o'clock we turned out and went along up the bank.  The

+river was coming up pretty fast, and lots of driftwood going by on the

+rise. By and by along comes part of a log raft—nine logs fast together.

+ We went out with the skiff and towed it ashore.  Then we had dinner.

+Anybody but pap would a waited and seen the day through, so as to catch

+more stuff; but that warn't pap's style.  Nine logs was enough for one

+time; he must shove right over to town and sell.  So he locked me in and

+took the skiff, and started off towing the raft about half-past three.

+ I judged he wouldn't come back that night.  I waited till I reckoned he

+had got a good start; then I out with my saw, and went to work on that

+log again.  Before he was t'other side of the river I was out of the

+hole; him and his raft was just a speck on the water away off yonder.

+

+I took the sack of corn meal and took it to where the canoe was hid, and

+shoved the vines and branches apart and put it in; then I done the same

+with the side of bacon; then the whisky-jug.  I took all the coffee and

+sugar there was, and all the ammunition; I took the wadding; I took the

+bucket and gourd; I took a dipper and a tin cup, and my old saw and two

+blankets, and the skillet and the coffee-pot.  I took fish-lines and

+matches and other things—everything that was worth a cent.  I cleaned

+out the place.  I wanted an axe, but there wasn't any, only the one out

+at the woodpile, and I knowed why I was going to leave that.  I fetched

+out the gun, and now I was done.

+

+I had wore the ground a good deal crawling out of the hole and dragging

+out so many things.  So I fixed that as good as I could from the outside

+by scattering dust on the place, which covered up the smoothness and the

+sawdust.  Then I fixed the piece of log back into its place, and put two

+rocks under it and one against it to hold it there, for it was bent up

+at that place and didn't quite touch ground.  If you stood four or five

+foot away and didn't know it was sawed, you wouldn't never notice

+it; and besides, this was the back of the cabin, and it warn't likely

+anybody would go fooling around there.

+

+It was all grass clear to the canoe, so I hadn't left a track.  I

+followed around to see.  I stood on the bank and looked out over the

+river.  All safe.  So I took the gun and went up a piece into the woods,

+and was hunting around for some birds when I see a wild pig; hogs soon

+went wild in them bottoms after they had got away from the prairie

+farms. I shot this fellow and took him into camp.

+

+I took the axe and smashed in the door.  I beat it and hacked it

+considerable a-doing it.  I fetched the pig in, and took him back nearly

+to the table and hacked into his throat with the axe, and laid him down

+on the ground to bleed; I say ground because it was ground—hard packed,

+and no boards.  Well, next I took an old sack and put a lot of big rocks

+in it—all I could drag—and I started it from the pig, and dragged it to

+the door and through the woods down to the river and dumped it in, and

+down it sunk, out of sight.  You could easy see that something had been

+dragged over the ground.  I did wish Tom Sawyer was there; I knowed he

+would take an interest in this kind of business, and throw in the fancy

+touches.  Nobody could spread himself like Tom Sawyer in such a thing as

+that.

+

+Well, last I pulled out some of my hair, and blooded the axe good, and

+stuck it on the back side, and slung the axe in the corner.  Then I

+took up the pig and held him to my breast with my jacket (so he couldn't

+drip) till I got a good piece below the house and then dumped him into

+the river.  Now I thought of something else.  So I went and got the bag

+of meal and my old saw out of the canoe, and fetched them to the house.

+ I took the bag to where it used to stand, and ripped a hole in the

+bottom of it with the saw, for there warn't no knives and forks on the

+place—pap done everything with his clasp-knife about the cooking.  Then

+I carried the sack about a hundred yards across the grass and through

+the willows east of the house, to a shallow lake that was five mile wide

+and full of rushes—and ducks too, you might say, in the season.  There

+was a slough or a creek leading out of it on the other side that went

+miles away, I don't know where, but it didn't go to the river.  The meal

+sifted out and made a little track all the way to the lake.  I dropped

+pap's whetstone there too, so as to look like it had been done by

+accident. Then I tied up the rip in the meal sack with a string, so it

+wouldn't leak no more, and took it and my saw to the canoe again.

+

+It was about dark now; so I dropped the canoe down the river under some

+willows that hung over the bank, and waited for the moon to rise.  I

+made fast to a willow; then I took a bite to eat, and by and by laid

+down in the canoe to smoke a pipe and lay out a plan.  I says to myself,

+they'll follow the track of that sackful of rocks to the shore and then

+drag the river for me.  And they'll follow that meal track to the lake

+and go browsing down the creek that leads out of it to find the robbers

+that killed me and took the things.  They won't ever hunt the river for

+anything but my dead carcass. They'll soon get tired of that, and won't

+bother no more about me.  All right; I can stop anywhere I want to.

+Jackson's Island is good enough for me; I know that island pretty well,

+and nobody ever comes there.  And then I can paddle over to town nights,

+and slink around and pick up things I want. Jackson's Island's the

+place.

+

+I was pretty tired, and the first thing I knowed I was asleep.  When

+I woke up I didn't know where I was for a minute.  I set up and looked

+around, a little scared.  Then I remembered.  The river looked miles and

+miles across.  The moon was so bright I could a counted the drift logs

+that went a-slipping along, black and still, hundreds of yards out from

+shore. Everything was dead quiet, and it looked late, and smelt late.

+You know what I mean—I don't know the words to put it in.

+

+I took a good gap and a stretch, and was just going to unhitch and start

+when I heard a sound away over the water.  I listened.  Pretty soon I

+made it out.  It was that dull kind of a regular sound that comes from

+oars working in rowlocks when it's a still night.  I peeped out through

+the willow branches, and there it was—a skiff, away across the water.

+ I couldn't tell how many was in it.  It kept a-coming, and when it was

+abreast of me I see there warn't but one man in it.  Think's I, maybe

+it's pap, though I warn't expecting him.  He dropped below me with the

+current, and by and by he came a-swinging up shore in the easy water,

+and he went by so close I could a reached out the gun and touched him.

+ Well, it was pap, sure enough—and sober, too, by the way he laid his

+oars.

+

+I didn't lose no time.  The next minute I was a-spinning down stream

+soft but quick in the shade of the bank.  I made two mile and a half,

+and then struck out a quarter of a mile or more towards the middle of

+the river, because pretty soon I would be passing the ferry landing, and

+people might see me and hail me.  I got out amongst the driftwood, and

+then laid down in the bottom of the canoe and let her float.

+

+ I laid there, and had a good rest and a smoke out of my pipe, looking

+away into the sky; not a cloud in it.  The sky looks ever so deep when

+you lay down on your back in the moonshine; I never knowed it before.

+ And how far a body can hear on the water such nights!  I heard people

+talking at the ferry landing. I heard what they said, too—every word

+of it.  One man said it was getting towards the long days and the short

+nights now.  T'other one said this warn't one of the short ones, he

+reckoned—and then they laughed, and he said it over again, and they

+laughed again; then they waked up another fellow and told him, and

+laughed, but he didn't laugh; he ripped out something brisk, and said

+let him alone.  The first fellow said he 'lowed to tell it to his

+old woman—she would think it was pretty good; but he said that warn't

+nothing to some things he had said in his time. I heard one man say it

+was nearly three o'clock, and he hoped daylight wouldn't wait more than

+about a week longer.  After that the talk got further and further away,

+and I couldn't make out the words any more; but I could hear the mumble,

+and now and then a laugh, too, but it seemed a long ways off.

+

+I was away below the ferry now.  I rose up, and there was Jackson's

+Island, about two mile and a half down stream, heavy timbered and

+standing up out of the middle of the river, big and dark and solid, like

+a steamboat without any lights.  There warn't any signs of the bar at

+the head—it was all under water now.

+

+It didn't take me long to get there.  I shot past the head at a ripping

+rate, the current was so swift, and then I got into the dead water and

+landed on the side towards the Illinois shore.  I run the canoe into

+a deep dent in the bank that I knowed about; I had to part the willow

+branches to get in; and when I made fast nobody could a seen the canoe

+from the outside.

+

+I went up and set down on a log at the head of the island, and looked

+out on the big river and the black driftwood and away over to the town,

+three mile away, where there was three or four lights twinkling.  A

+monstrous big lumber-raft was about a mile up stream, coming along down,

+with a lantern in the middle of it.  I watched it come creeping down,

+and when it was most abreast of where I stood I heard a man say, "Stern

+oars, there! heave her head to stabboard!"  I heard that just as plain

+as if the man was by my side.

+

+There was a little gray in the sky now; so I stepped into the woods, and

+laid down for a nap before breakfast.

+

+

+

+

+CHAPTER VIII.

+

+THE sun was up so high when I waked that I judged it was after eight

+o'clock.  I laid there in the grass and the cool shade thinking about

+things, and feeling rested and ruther comfortable and satisfied.  I

+could see the sun out at one or two holes, but mostly it was big trees

+all about, and gloomy in there amongst them.  There was freckled places

+on the ground where the light sifted down through the leaves, and the

+freckled places swapped about a little, showing there was a little

+breeze up there.  A couple of squirrels set on a limb and jabbered at me

+very friendly.

+

+I was powerful lazy and comfortable—didn't want to get up and cook

+breakfast.  Well, I was dozing off again when I thinks I hears a deep

+sound of "boom!" away up the river.  I rouses up, and rests on my elbow

+and listens; pretty soon I hears it again.  I hopped up, and went and

+looked out at a hole in the leaves, and I see a bunch of smoke laying

+on the water a long ways up—about abreast the ferry.  And there was the

+ferryboat full of people floating along down.  I knowed what was the

+matter now.  "Boom!" I see the white smoke squirt out of the ferryboat's

+side.  You see, they was firing cannon over the water, trying to make my

+carcass come to the top.

+

+I was pretty hungry, but it warn't going to do for me to start a fire,

+because they might see the smoke.  So I set there and watched the

+cannon-smoke and listened to the boom.  The river was a mile wide there,

+and it always looks pretty on a summer morning—so I was having a good

+enough time seeing them hunt for my remainders if I only had a bite to

+eat. Well, then I happened to think how they always put quicksilver in

+loaves of bread and float them off, because they always go right to the

+drownded carcass and stop there.  So, says I, I'll keep a lookout, and

+if any of them's floating around after me I'll give them a show.  I

+changed to the Illinois edge of the island to see what luck I could

+have, and I warn't disappointed.  A big double loaf come along, and I

+most got it with a long stick, but my foot slipped and she floated out

+further.  Of course I was where the current set in the closest to the

+shore—I knowed enough for that.  But by and by along comes another one,

+and this time I won.  I took out the plug and shook out the little dab

+of quicksilver, and set my teeth in.  It was "baker's bread"—what the

+quality eat; none of your low-down corn-pone.

+

+I got a good place amongst the leaves, and set there on a log, munching

+the bread and watching the ferry-boat, and very well satisfied.  And

+then something struck me.  I says, now I reckon the widow or the parson

+or somebody prayed that this bread would find me, and here it has gone

+and done it.  So there ain't no doubt but there is something in that

+thing—that is, there's something in it when a body like the widow or the

+parson prays, but it don't work for me, and I reckon it don't work for

+only just the right kind.

+

+I lit a pipe and had a good long smoke, and went on watching.  The

+ferryboat was floating with the current, and I allowed I'd have a chance

+to see who was aboard when she come along, because she would come in

+close, where the bread did.  When she'd got pretty well along down

+towards me, I put out my pipe and went to where I fished out the bread,

+and laid down behind a log on the bank in a little open place.  Where

+the log forked I could peep through.

+

+By and by she come along, and she drifted in so close that they could

+a run out a plank and walked ashore.  Most everybody was on the boat.

+ Pap, and Judge Thatcher, and Bessie Thatcher, and Jo Harper, and Tom

+Sawyer, and his old Aunt Polly, and Sid and Mary, and plenty more.

+ Everybody was talking about the murder, but the captain broke in and

+says:

+

+"Look sharp, now; the current sets in the closest here, and maybe he's

+washed ashore and got tangled amongst the brush at the water's edge.  I

+hope so, anyway."

+

+I didn't hope so.  They all crowded up and leaned over the rails, nearly

+in my face, and kept still, watching with all their might.  I could see

+them first-rate, but they couldn't see me.  Then the captain sung out:

+

+"Stand away!" and the cannon let off such a blast right before me that

+it made me deef with the noise and pretty near blind with the smoke, and

+I judged I was gone.  If they'd a had some bullets in, I reckon they'd

+a got the corpse they was after.  Well, I see I warn't hurt, thanks to

+goodness. The boat floated on and went out of sight around the shoulder

+of the island.  I could hear the booming now and then, further and

+further off, and by and by, after an hour, I didn't hear it no more.

+ The island was three mile long.  I judged they had got to the foot, and

+was giving it up.  But they didn't yet a while.  They turned around

+the foot of the island and started up the channel on the Missouri side,

+under steam, and booming once in a while as they went.  I crossed over

+to that side and watched them. When they got abreast the head of the

+island they quit shooting and dropped over to the Missouri shore and

+went home to the town.

+

+I knowed I was all right now.  Nobody else would come a-hunting after

+me. I got my traps out of the canoe and made me a nice camp in the thick

+woods.  I made a kind of a tent out of my blankets to put my things

+under so the rain couldn't get at them.  I catched a catfish and haggled

+him open with my saw, and towards sundown I started my camp fire and had

+supper.  Then I set out a line to catch some fish for breakfast.

+

+When it was dark I set by my camp fire smoking, and feeling pretty well

+satisfied; but by and by it got sort of lonesome, and so I went and set

+on the bank and listened to the current swashing along, and counted the

+stars and drift logs and rafts that come down, and then went to bed;

+there ain't no better way to put in time when you are lonesome; you

+can't stay so, you soon get over it.

+

+And so for three days and nights.  No difference—just the same thing.

+But the next day I went exploring around down through the island.  I was

+boss of it; it all belonged to me, so to say, and I wanted to know

+all about it; but mainly I wanted to put in the time.  I found plenty

+strawberries, ripe and prime; and green summer grapes, and green

+razberries; and the green blackberries was just beginning to show.  They

+would all come handy by and by, I judged.

+

+Well, I went fooling along in the deep woods till I judged I warn't

+far from the foot of the island.  I had my gun along, but I hadn't shot

+nothing; it was for protection; thought I would kill some game nigh

+home. About this time I mighty near stepped on a good-sized snake,

+and it went sliding off through the grass and flowers, and I after

+it, trying to get a shot at it. I clipped along, and all of a sudden I

+bounded right on to the ashes of a camp fire that was still smoking.

+

+My heart jumped up amongst my lungs.  I never waited for to look

+further, but uncocked my gun and went sneaking back on my tiptoes as

+fast as ever I could.  Every now and then I stopped a second amongst the

+thick leaves and listened, but my breath come so hard I couldn't hear

+nothing else.  I slunk along another piece further, then listened again;

+and so on, and so on.  If I see a stump, I took it for a man; if I trod

+on a stick and broke it, it made me feel like a person had cut one of my

+breaths in two and I only got half, and the short half, too.

+

+When I got to camp I warn't feeling very brash, there warn't much sand

+in my craw; but I says, this ain't no time to be fooling around.  So I

+got all my traps into my canoe again so as to have them out of sight,

+and I put out the fire and scattered the ashes around to look like an

+old last year's camp, and then clumb a tree.

+

+I reckon I was up in the tree two hours; but I didn't see nothing,

+I didn't hear nothing—I only thought I heard and seen as much as a

+thousand things.  Well, I couldn't stay up there forever; so at last I

+got down, but I kept in the thick woods and on the lookout all the

+time. All I could get to eat was berries and what was left over from

+breakfast.

+

+By the time it was night I was pretty hungry.  So when it was good

+and dark I slid out from shore before moonrise and paddled over to the

+Illinois bank—about a quarter of a mile.  I went out in the woods and

+cooked a supper, and I had about made up my mind I would stay there

+all night when I hear a plunkety-plunk, plunkety-plunk, and says

+to myself, horses coming; and next I hear people's voices.  I got

+everything into the canoe as quick as I could, and then went creeping

+through the woods to see what I could find out.  I hadn't got far when I

+hear a man say:

+

+"We better camp here if we can find a good place; the horses is about

+beat out.  Let's look around."

+

+I didn't wait, but shoved out and paddled away easy.  I tied up in the

+old place, and reckoned I would sleep in the canoe.

+

+I didn't sleep much.  I couldn't, somehow, for thinking.  And every time

+I waked up I thought somebody had me by the neck.  So the sleep didn't

+do me no good.  By and by I says to myself, I can't live this way; I'm

+a-going to find out who it is that's here on the island with me; I'll

+find it out or bust.  Well, I felt better right off.

+

+So I took my paddle and slid out from shore just a step or two, and

+then let the canoe drop along down amongst the shadows.  The moon was

+shining, and outside of the shadows it made it most as light as day.

+ I poked along well on to an hour, everything still as rocks and sound

+asleep. Well, by this time I was most down to the foot of the island.  A

+little ripply, cool breeze begun to blow, and that was as good as saying

+the night was about done.  I give her a turn with the paddle and brung

+her nose to shore; then I got my gun and slipped out and into the edge

+of the woods.  I sat down there on a log, and looked out through the

+leaves.  I see the moon go off watch, and the darkness begin to blanket

+the river. But in a little while I see a pale streak over the treetops,

+and knowed the day was coming.  So I took my gun and slipped off towards

+where I had run across that camp fire, stopping every minute or two

+to listen.  But I hadn't no luck somehow; I couldn't seem to find the

+place.  But by and by, sure enough, I catched a glimpse of fire away

+through the trees.  I went for it, cautious and slow.  By and by I was

+close enough to have a look, and there laid a man on the ground.  It

+most give me the fan-tods. He had a blanket around his head, and his

+head was nearly in the fire.  I set there behind a clump of bushes, in

+about six foot of him, and kept my eyes on him steady.  It was getting

+gray daylight now.  Pretty soon he gapped and stretched himself and hove

+off the blanket, and it was Miss Watson's Jim!  I bet I was glad to see

+him.  I says:

+

+"Hello, Jim!" and skipped out.

+

+He bounced up and stared at me wild.  Then he drops down on his knees,

+and puts his hands together and says:

+

+"Doan' hurt me—don't!  I hain't ever done no harm to a ghos'.  I alwuz

+liked dead people, en done all I could for 'em.  You go en git in de

+river agin, whah you b'longs, en doan' do nuffn to Ole Jim, 'at 'uz

+awluz yo' fren'."

+

+Well, I warn't long making him understand I warn't dead.  I was ever so

+glad to see Jim.  I warn't lonesome now.  I told him I warn't afraid of

+him telling the people where I was.  I talked along, but he only set

+there and looked at me; never said nothing.  Then I says:

+

+"It's good daylight.  Le's get breakfast.  Make up your camp fire good."

+

+"What's de use er makin' up de camp fire to cook strawbries en sich

+truck? But you got a gun, hain't you?  Den we kin git sumfn better den

+strawbries."

+

+"Strawberries and such truck," I says.  "Is that what you live on?"

+

+"I couldn' git nuffn else," he says.

+

+"Why, how long you been on the island, Jim?"

+

+"I come heah de night arter you's killed."

+

+"What, all that time?"

+

+"Yes—indeedy."

+

+"And ain't you had nothing but that kind of rubbage to eat?"

+

+"No, sah—nuffn else."

+

+"Well, you must be most starved, ain't you?"

+

+"I reck'n I could eat a hoss.  I think I could. How long you ben on de

+islan'?"

+

+"Since the night I got killed."

+

+"No!  W'y, what has you lived on?  But you got a gun.  Oh, yes, you got

+a gun.  Dat's good.  Now you kill sumfn en I'll make up de fire."

+

+So we went over to where the canoe was, and while he built a fire in

+a grassy open place amongst the trees, I fetched meal and bacon and

+coffee, and coffee-pot and frying-pan, and sugar and tin cups, and the

+nigger was set back considerable, because he reckoned it was all done

+with witchcraft. I catched a good big catfish, too, and Jim cleaned him

+with his knife, and fried him.

+

+When breakfast was ready we lolled on the grass and eat it smoking hot.

+Jim laid it in with all his might, for he was most about starved.  Then

+when we had got pretty well stuffed, we laid off and lazied.  By and by

+Jim says:

+

+"But looky here, Huck, who wuz it dat 'uz killed in dat shanty ef it

+warn't you?"

+

+Then I told him the whole thing, and he said it was smart.  He said Tom

+Sawyer couldn't get up no better plan than what I had.  Then I says:

+

+"How do you come to be here, Jim, and how'd you get here?"

+

+He looked pretty uneasy, and didn't say nothing for a minute.  Then he

+says:

+

+"Maybe I better not tell."

+

+"Why, Jim?"

+

+"Well, dey's reasons.  But you wouldn' tell on me ef I uz to tell you,

+would you, Huck?"

+

+"Blamed if I would, Jim."

+

+"Well, I b'lieve you, Huck.  I—I run off."

+

+"Jim!"

+

+"But mind, you said you wouldn' tell—you know you said you wouldn' tell,

+Huck."

+

+"Well, I did.  I said I wouldn't, and I'll stick to it.  Honest injun,

+I will.  People would call me a low-down Abolitionist and despise me for

+keeping mum—but that don't make no difference.  I ain't a-going to tell,

+and I ain't a-going back there, anyways.  So, now, le's know all about

+it."

+

+"Well, you see, it 'uz dis way.  Ole missus—dat's Miss Watson—she pecks

+on me all de time, en treats me pooty rough, but she awluz said she

+wouldn' sell me down to Orleans.  But I noticed dey wuz a nigger trader

+roun' de place considable lately, en I begin to git oneasy.  Well, one

+night I creeps to de do' pooty late, en de do' warn't quite shet, en I

+hear old missus tell de widder she gwyne to sell me down to Orleans, but

+she didn' want to, but she could git eight hund'd dollars for me, en it

+'uz sich a big stack o' money she couldn' resis'.  De widder she try to

+git her to say she wouldn' do it, but I never waited to hear de res'.  I

+lit out mighty quick, I tell you.

+

+"I tuck out en shin down de hill, en 'spec to steal a skift 'long de

+sho' som'ers 'bove de town, but dey wuz people a-stirring yit, so I hid

+in de ole tumble-down cooper-shop on de bank to wait for everybody to

+go 'way. Well, I wuz dah all night.  Dey wuz somebody roun' all de time.

+ 'Long 'bout six in de mawnin' skifts begin to go by, en 'bout eight er

+nine every skift dat went 'long wuz talkin' 'bout how yo' pap come over

+to de town en say you's killed.  Dese las' skifts wuz full o' ladies en

+genlmen a-goin' over for to see de place.  Sometimes dey'd pull up at

+de sho' en take a res' b'fo' dey started acrost, so by de talk I got to

+know all 'bout de killin'.  I 'uz powerful sorry you's killed, Huck, but

+I ain't no mo' now.

+

+"I laid dah under de shavin's all day.  I 'uz hungry, but I warn't

+afeard; bekase I knowed ole missus en de widder wuz goin' to start to

+de camp-meet'n' right arter breakfas' en be gone all day, en dey knows

+I goes off wid de cattle 'bout daylight, so dey wouldn' 'spec to see me

+roun' de place, en so dey wouldn' miss me tell arter dark in de evenin'.

+De yuther servants wouldn' miss me, kase dey'd shin out en take holiday

+soon as de ole folks 'uz out'n de way.

+

+"Well, when it come dark I tuck out up de river road, en went 'bout two

+mile er more to whah dey warn't no houses.  I'd made up my mine 'bout

+what I's agwyne to do.  You see, ef I kep' on tryin' to git away afoot,

+de dogs 'ud track me; ef I stole a skift to cross over, dey'd miss dat

+skift, you see, en dey'd know 'bout whah I'd lan' on de yuther side, en

+whah to pick up my track.  So I says, a raff is what I's arter; it doan'

+make no track.

+

+"I see a light a-comin' roun' de p'int bymeby, so I wade' in en shove'

+a log ahead o' me en swum more'n half way acrost de river, en got in

+'mongst de drift-wood, en kep' my head down low, en kinder swum agin de

+current tell de raff come along.  Den I swum to de stern uv it en tuck

+a-holt.  It clouded up en 'uz pooty dark for a little while.  So I clumb

+up en laid down on de planks.  De men 'uz all 'way yonder in de middle,

+whah de lantern wuz.  De river wuz a-risin', en dey wuz a good current;

+so I reck'n'd 'at by fo' in de mawnin' I'd be twenty-five mile down de

+river, en den I'd slip in jis b'fo' daylight en swim asho', en take to

+de woods on de Illinois side.

+

+"But I didn' have no luck.  When we 'uz mos' down to de head er de

+islan' a man begin to come aft wid de lantern, I see it warn't no use

+fer to wait, so I slid overboard en struck out fer de islan'.  Well, I

+had a notion I could lan' mos' anywhers, but I couldn't—bank too bluff.

+ I 'uz mos' to de foot er de islan' b'fo' I found' a good place.  I went

+into de woods en jedged I wouldn' fool wid raffs no mo', long as dey

+move de lantern roun' so.  I had my pipe en a plug er dog-leg, en some

+matches in my cap, en dey warn't wet, so I 'uz all right."

+

+"And so you ain't had no meat nor bread to eat all this time?  Why

+didn't you get mud-turkles?"

+

+"How you gwyne to git 'm?  You can't slip up on um en grab um; en how's

+a body gwyne to hit um wid a rock?  How could a body do it in de night?

+ En I warn't gwyne to show mysef on de bank in de daytime."

+

+"Well, that's so.  You've had to keep in the woods all the time, of

+course. Did you hear 'em shooting the cannon?"

+

+"Oh, yes.  I knowed dey was arter you.  I see um go by heah—watched um

+thoo de bushes."

+

+Some young birds come along, flying a yard or two at a time and

+lighting. Jim said it was a sign it was going to rain.  He said it was

+a sign when young chickens flew that way, and so he reckoned it was the

+same way when young birds done it.  I was going to catch some of them,

+but Jim wouldn't let me.  He said it was death.  He said his father laid

+mighty sick once, and some of them catched a bird, and his old granny

+said his father would die, and he did.

+

+And Jim said you mustn't count the things you are going to cook for

+dinner, because that would bring bad luck.  The same if you shook the

+table-cloth after sundown.  And he said if a man owned a beehive

+and that man died, the bees must be told about it before sun-up next

+morning, or else the bees would all weaken down and quit work and die.

+ Jim said bees wouldn't sting idiots; but I didn't believe that, because

+I had tried them lots of times myself, and they wouldn't sting me.

+

+I had heard about some of these things before, but not all of them.  Jim

+knowed all kinds of signs.  He said he knowed most everything.  I said

+it looked to me like all the signs was about bad luck, and so I asked

+him if there warn't any good-luck signs.  He says:

+

+"Mighty few—an' dey ain't no use to a body.  What you want to know

+when good luck's a-comin' for?  Want to keep it off?"  And he said:  "Ef

+you's got hairy arms en a hairy breas', it's a sign dat you's agwyne

+to be rich. Well, dey's some use in a sign like dat, 'kase it's so fur

+ahead. You see, maybe you's got to be po' a long time fust, en so you

+might git discourage' en kill yo'sef 'f you didn' know by de sign dat

+you gwyne to be rich bymeby."

+

+"Have you got hairy arms and a hairy breast, Jim?"

+

+"What's de use to ax dat question?  Don't you see I has?"

+

+"Well, are you rich?"

+

+"No, but I ben rich wunst, and gwyne to be rich agin.  Wunst I had

+foteen dollars, but I tuck to specalat'n', en got busted out."

+

+"What did you speculate in, Jim?"

+

+"Well, fust I tackled stock."

+

+"What kind of stock?"

+

+"Why, live stock—cattle, you know.  I put ten dollars in a cow.  But

+I ain' gwyne to resk no mo' money in stock.  De cow up 'n' died on my

+han's."

+

+"So you lost the ten dollars."

+

+"No, I didn't lose it all.  I on'y los' 'bout nine of it.  I sole de

+hide en taller for a dollar en ten cents."

+

+"You had five dollars and ten cents left.  Did you speculate any more?"

+

+"Yes.  You know that one-laigged nigger dat b'longs to old Misto

+Bradish? Well, he sot up a bank, en say anybody dat put in a dollar

+would git fo' dollars mo' at de en' er de year.  Well, all de niggers

+went in, but dey didn't have much.  I wuz de on'y one dat had much.  So

+I stuck out for mo' dan fo' dollars, en I said 'f I didn' git it I'd

+start a bank mysef. Well, o' course dat nigger want' to keep me out er

+de business, bekase he says dey warn't business 'nough for two banks, so

+he say I could put in my five dollars en he pay me thirty-five at de en'

+er de year.

+

+"So I done it.  Den I reck'n'd I'd inves' de thirty-five dollars right

+off en keep things a-movin'.  Dey wuz a nigger name' Bob, dat had

+ketched a wood-flat, en his marster didn' know it; en I bought it off'n

+him en told him to take de thirty-five dollars when de en' er de

+year come; but somebody stole de wood-flat dat night, en nex day de

+one-laigged nigger say de bank's busted.  So dey didn' none uv us git no

+money."

+

+"What did you do with the ten cents, Jim?"

+

+"Well, I 'uz gwyne to spen' it, but I had a dream, en de dream tole me

+to give it to a nigger name' Balum—Balum's Ass dey call him for short;

+he's one er dem chuckleheads, you know.  But he's lucky, dey say, en I

+see I warn't lucky.  De dream say let Balum inves' de ten cents en he'd

+make a raise for me.  Well, Balum he tuck de money, en when he wuz in

+church he hear de preacher say dat whoever give to de po' len' to de

+Lord, en boun' to git his money back a hund'd times.  So Balum he tuck

+en give de ten cents to de po', en laid low to see what wuz gwyne to

+come of it."

+

+"Well, what did come of it, Jim?"

+

+"Nuffn never come of it.  I couldn' manage to k'leck dat money no way;

+en Balum he couldn'.  I ain' gwyne to len' no mo' money 'dout I see de

+security.  Boun' to git yo' money back a hund'd times, de preacher says!

+Ef I could git de ten cents back, I'd call it squah, en be glad er de

+chanst."

+

+"Well, it's all right anyway, Jim, long as you're going to be rich again

+some time or other."

+

+"Yes; en I's rich now, come to look at it.  I owns mysef, en I's wuth

+eight hund'd dollars.  I wisht I had de money, I wouldn' want no mo'."

+

+

+

+

+CHAPTER IX.

+

+I wanted to go and look at a place right about the middle of the island

+that I'd found when I was exploring; so we started and soon got to it,

+because the island was only three miles long and a quarter of a mile

+wide.

+

+This place was a tolerable long, steep hill or ridge about forty foot

+high. We had a rough time getting to the top, the sides was so steep and

+the bushes so thick.  We tramped and clumb around all over it, and by

+and by found a good big cavern in the rock, most up to the top on the

+side towards Illinois.  The cavern was as big as two or three rooms

+bunched together, and Jim could stand up straight in it.  It was cool in

+there. Jim was for putting our traps in there right away, but I said we

+didn't want to be climbing up and down there all the time.

+

+Jim said if we had the canoe hid in a good place, and had all the traps

+in the cavern, we could rush there if anybody was to come to the island,

+and they would never find us without dogs.  And, besides, he said them

+little birds had said it was going to rain, and did I want the things to

+get wet?

+

+So we went back and got the canoe, and paddled up abreast the cavern,

+and lugged all the traps up there.  Then we hunted up a place close by

+to hide the canoe in, amongst the thick willows.  We took some fish off

+of the lines and set them again, and begun to get ready for dinner.

+

+The door of the cavern was big enough to roll a hogshead in, and on one

+side of the door the floor stuck out a little bit, and was flat and a

+good place to build a fire on.  So we built it there and cooked dinner.

+

+We spread the blankets inside for a carpet, and eat our dinner in there.

+We put all the other things handy at the back of the cavern.  Pretty

+soon it darkened up, and begun to thunder and lighten; so the birds was

+right about it.  Directly it begun to rain, and it rained like all fury,

+too, and I never see the wind blow so.  It was one of these regular

+summer storms.  It would get so dark that it looked all blue-black

+outside, and lovely; and the rain would thrash along by so thick that

+the trees off a little ways looked dim and spider-webby; and here would

+come a blast of wind that would bend the trees down and turn up the

+pale underside of the leaves; and then a perfect ripper of a gust would

+follow along and set the branches to tossing their arms as if they

+was just wild; and next, when it was just about the bluest and

+blackest—FST! it was as bright as glory, and you'd have a little

+glimpse of tree-tops a-plunging about away off yonder in the storm,

+hundreds of yards further than you could see before; dark as sin again

+in a second, and now you'd hear the thunder let go with an awful crash,

+and then go rumbling, grumbling, tumbling, down the sky towards the

+under side of the world, like rolling empty barrels down stairs—where

+it's long stairs and they bounce a good deal, you know.

+

+"Jim, this is nice," I says.  "I wouldn't want to be nowhere else but

+here. Pass me along another hunk of fish and some hot corn-bread."

+

+"Well, you wouldn't a ben here 'f it hadn't a ben for Jim.  You'd a ben

+down dah in de woods widout any dinner, en gittn' mos' drownded, too;

+dat you would, honey.  Chickens knows when it's gwyne to rain, en so do

+de birds, chile."

+

+The river went on raising and raising for ten or twelve days, till at

+last it was over the banks.  The water was three or four foot deep on

+the island in the low places and on the Illinois bottom.  On that side

+it was a good many miles wide, but on the Missouri side it was the same

+old distance across—a half a mile—because the Missouri shore was just a

+wall of high bluffs.

+

+Daytimes we paddled all over the island in the canoe, It was mighty cool

+and shady in the deep woods, even if the sun was blazing outside.  We

+went winding in and out amongst the trees, and sometimes the vines hung

+so thick we had to back away and go some other way.  Well, on every old

+broken-down tree you could see rabbits and snakes and such things; and

+when the island had been overflowed a day or two they got so tame, on

+account of being hungry, that you could paddle right up and put your

+hand on them if you wanted to; but not the snakes and turtles—they would

+slide off in the water.  The ridge our cavern was in was full of them.

+We could a had pets enough if we'd wanted them.

+

+One night we catched a little section of a lumber raft—nice pine planks.

+It was twelve foot wide and about fifteen or sixteen foot long, and

+the top stood above water six or seven inches—a solid, level floor.  We

+could see saw-logs go by in the daylight sometimes, but we let them go;

+we didn't show ourselves in daylight.

+

+Another night when we was up at the head of the island, just before

+daylight, here comes a frame-house down, on the west side.  She was

+a two-story, and tilted over considerable.  We paddled out and got

+aboard—clumb in at an upstairs window.  But it was too dark to see yet,

+so we made the canoe fast and set in her to wait for daylight.

+

+The light begun to come before we got to the foot of the island.  Then

+we looked in at the window.  We could make out a bed, and a table, and

+two old chairs, and lots of things around about on the floor, and there

+was clothes hanging against the wall.  There was something laying on the

+floor in the far corner that looked like a man.  So Jim says:

+

+"Hello, you!"

+

+But it didn't budge.  So I hollered again, and then Jim says:

+

+"De man ain't asleep—he's dead.  You hold still—I'll go en see."

+

+He went, and bent down and looked, and says:

+

+"It's a dead man.  Yes, indeedy; naked, too.  He's ben shot in de back.

+I reck'n he's ben dead two er three days.  Come in, Huck, but doan' look

+at his face—it's too gashly."

+

+I didn't look at him at all.  Jim throwed some old rags over him, but

+he needn't done it; I didn't want to see him.  There was heaps of old

+greasy cards scattered around over the floor, and old whisky bottles,

+and a couple of masks made out of black cloth; and all over the walls

+was the ignorantest kind of words and pictures made with charcoal.

+ There was two old dirty calico dresses, and a sun-bonnet, and some

+women's underclothes hanging against the wall, and some men's clothing,

+too.  We put the lot into the canoe—it might come good.  There was a

+boy's old speckled straw hat on the floor; I took that, too.  And there

+was a bottle that had had milk in it, and it had a rag stopper for a

+baby to suck.  We would a took the bottle, but it was broke.  There was

+a seedy old chest, and an old hair trunk with the hinges broke.  They

+stood open, but there warn't nothing left in them that was any account.

+ The way things was scattered about we reckoned the people left in a

+hurry, and warn't fixed so as to carry off most of their stuff.

+

+We got an old tin lantern, and a butcher-knife without any handle, and

+a bran-new Barlow knife worth two bits in any store, and a lot of tallow

+candles, and a tin candlestick, and a gourd, and a tin cup, and a ratty

+old bedquilt off the bed, and a reticule with needles and pins and

+beeswax and buttons and thread and all such truck in it, and a hatchet

+and some nails, and a fishline as thick as my little finger with some

+monstrous hooks on it, and a roll of buckskin, and a leather dog-collar,

+and a horseshoe, and some vials of medicine that didn't have no label

+on them; and just as we was leaving I found a tolerable good curry-comb,

+and Jim he found a ratty old fiddle-bow, and a wooden leg.  The straps

+was broke off of it, but, barring that, it was a good enough leg, though

+it was too long for me and not long enough for Jim, and we couldn't find

+the other one, though we hunted all around.

+

+And so, take it all around, we made a good haul.  When we was ready to

+shove off we was a quarter of a mile below the island, and it was pretty

+broad day; so I made Jim lay down in the canoe and cover up with the

+quilt, because if he set up people could tell he was a nigger a good

+ways off.  I paddled over to the Illinois shore, and drifted down most

+a half a mile doing it.  I crept up the dead water under the bank, and

+hadn't no accidents and didn't see nobody.  We got home all safe.

+

+

+

+

+CHAPTER X.

+

+AFTER breakfast I wanted to talk about the dead man and guess out how he

+come to be killed, but Jim didn't want to.  He said it would fetch bad

+luck; and besides, he said, he might come and ha'nt us; he said a man

+that warn't buried was more likely to go a-ha'nting around than one

+that was planted and comfortable.  That sounded pretty reasonable, so

+I didn't say no more; but I couldn't keep from studying over it and

+wishing I knowed who shot the man, and what they done it for.

+

+We rummaged the clothes we'd got, and found eight dollars in silver

+sewed up in the lining of an old blanket overcoat.  Jim said he reckoned

+the people in that house stole the coat, because if they'd a knowed the

+money was there they wouldn't a left it.  I said I reckoned they killed

+him, too; but Jim didn't want to talk about that.  I says:

+

+"Now you think it's bad luck; but what did you say when I fetched in the

+snake-skin that I found on the top of the ridge day before yesterday?

+You said it was the worst bad luck in the world to touch a snake-skin

+with my hands.  Well, here's your bad luck!  We've raked in all this

+truck and eight dollars besides.  I wish we could have some bad luck

+like this every day, Jim."

+

+"Never you mind, honey, never you mind.  Don't you git too peart.  It's

+a-comin'.  Mind I tell you, it's a-comin'."

+

+It did come, too.  It was a Tuesday that we had that talk.  Well, after

+dinner Friday we was laying around in the grass at the upper end of the

+ridge, and got out of tobacco.  I went to the cavern to get some, and

+found a rattlesnake in there.  I killed him, and curled him up on the

+foot of Jim's blanket, ever so natural, thinking there'd be some fun

+when Jim found him there.  Well, by night I forgot all about the snake,

+and when Jim flung himself down on the blanket while I struck a light

+the snake's mate was there, and bit him.

+

+He jumped up yelling, and the first thing the light showed was the

+varmint curled up and ready for another spring.  I laid him out in a

+second with a stick, and Jim grabbed pap's whisky-jug and begun to pour

+it down.

+

+He was barefooted, and the snake bit him right on the heel.  That all

+comes of my being such a fool as to not remember that wherever you leave

+a dead snake its mate always comes there and curls around it.  Jim told

+me to chop off the snake's head and throw it away, and then skin the

+body and roast a piece of it.  I done it, and he eat it and said it

+would help cure him. He made me take off the rattles and tie them around

+his wrist, too.  He said that that would help.  Then I slid out quiet

+and throwed the snakes clear away amongst the bushes; for I warn't going

+to let Jim find out it was all my fault, not if I could help it.

+

+Jim sucked and sucked at the jug, and now and then he got out of his

+head and pitched around and yelled; but every time he come to himself he

+went to sucking at the jug again.  His foot swelled up pretty big, and

+so did his leg; but by and by the drunk begun to come, and so I judged

+he was all right; but I'd druther been bit with a snake than pap's

+whisky.

+

+Jim was laid up for four days and nights.  Then the swelling was all

+gone and he was around again.  I made up my mind I wouldn't ever take

+a-holt of a snake-skin again with my hands, now that I see what had come

+of it. Jim said he reckoned I would believe him next time.  And he said

+that handling a snake-skin was such awful bad luck that maybe we hadn't

+got to the end of it yet.  He said he druther see the new moon over his

+left shoulder as much as a thousand times than take up a snake-skin

+in his hand.  Well, I was getting to feel that way myself, though I've

+always reckoned that looking at the new moon over your left shoulder is

+one of the carelessest and foolishest things a body can do.  Old Hank

+Bunker done it once, and bragged about it; and in less than two years he

+got drunk and fell off of the shot-tower, and spread himself out so

+that he was just a kind of a layer, as you may say; and they slid him

+edgeways between two barn doors for a coffin, and buried him so, so

+they say, but I didn't see it.  Pap told me.  But anyway it all come of

+looking at the moon that way, like a fool.

+

+Well, the days went along, and the river went down between its banks

+again; and about the first thing we done was to bait one of the big

+hooks with a skinned rabbit and set it and catch a catfish that was

+as big as a man, being six foot two inches long, and weighed over two

+hundred pounds. We couldn't handle him, of course; he would a flung us

+into Illinois.  We just set there and watched him rip and tear around

+till he drownded.  We found a brass button in his stomach and a round

+ball, and lots of rubbage.  We split the ball open with the hatchet,

+and there was a spool in it.  Jim said he'd had it there a long time, to

+coat it over so and make a ball of it.  It was as big a fish as was ever

+catched in the Mississippi, I reckon.  Jim said he hadn't ever seen

+a bigger one.  He would a been worth a good deal over at the village.

+ They peddle out such a fish as that by the pound in the market-house

+there; everybody buys some of him; his meat's as white as snow and makes

+a good fry.

+

+Next morning I said it was getting slow and dull, and I wanted to get a

+stirring up some way.  I said I reckoned I would slip over the river and

+find out what was going on.  Jim liked that notion; but he said I

+must go in the dark and look sharp.  Then he studied it over and said,

+couldn't I put on some of them old things and dress up like a girl?

+ That was a good notion, too.  So we shortened up one of the calico

+gowns, and I turned up my trouser-legs to my knees and got into it.  Jim

+hitched it behind with the hooks, and it was a fair fit.  I put on the

+sun-bonnet and tied it under my chin, and then for a body to look in

+and see my face was like looking down a joint of stove-pipe.  Jim said

+nobody would know me, even in the daytime, hardly.  I practiced around

+all day to get the hang of the things, and by and by I could do pretty

+well in them, only Jim said I didn't walk like a girl; and he said

+I must quit pulling up my gown to get at my britches-pocket.  I took

+notice, and done better.

+

+I started up the Illinois shore in the canoe just after dark.

+

+I started across to the town from a little below the ferry-landing, and

+the drift of the current fetched me in at the bottom of the town.  I

+tied up and started along the bank.  There was a light burning in a

+little shanty that hadn't been lived in for a long time, and I wondered

+who had took up quarters there.  I slipped up and peeped in at the

+window.  There was a woman about forty year old in there knitting by

+a candle that was on a pine table.  I didn't know her face; she was a

+stranger, for you couldn't start a face in that town that I didn't know.

+ Now this was lucky, because I was weakening; I was getting afraid I had

+come; people might know my voice and find me out.  But if this woman had

+been in such a little town two days she could tell me all I wanted to

+know; so I knocked at the door, and made up my mind I wouldn't forget I

+was a girl.

+

+

+

+

+CHAPTER XI.

+

+"COME in," says the woman, and I did.  She says:  "Take a cheer."

+

+I done it.  She looked me all over with her little shiny eyes, and says:

+

+"What might your name be?"

+

+"Sarah Williams."

+

+"Where 'bouts do you live?  In this neighborhood?'

+

+"No'm.  In Hookerville, seven mile below.  I've walked all the way and

+I'm all tired out."

+

+"Hungry, too, I reckon.  I'll find you something."

+

+"No'm, I ain't hungry.  I was so hungry I had to stop two miles below

+here at a farm; so I ain't hungry no more.  It's what makes me so late.

+My mother's down sick, and out of money and everything, and I come to

+tell my uncle Abner Moore.  He lives at the upper end of the town, she

+says.  I hain't ever been here before.  Do you know him?"

+

+"No; but I don't know everybody yet.  I haven't lived here quite two

+weeks. It's a considerable ways to the upper end of the town.  You

+better stay here all night.  Take off your bonnet."

+

+"No," I says; "I'll rest a while, I reckon, and go on.  I ain't afeared

+of the dark."

+

+She said she wouldn't let me go by myself, but her husband would be in

+by and by, maybe in a hour and a half, and she'd send him along with me.

+Then she got to talking about her husband, and about her relations up

+the river, and her relations down the river, and about how much better

+off they used to was, and how they didn't know but they'd made a mistake

+coming to our town, instead of letting well alone—and so on and so on,

+till I was afeard I had made a mistake coming to her to find out what

+was going on in the town; but by and by she dropped on to pap and the

+murder, and then I was pretty willing to let her clatter right along.

+ She told about me and Tom Sawyer finding the six thousand dollars (only

+she got it ten) and all about pap and what a hard lot he was, and what

+a hard lot I was, and at last she got down to where I was murdered.  I

+says:

+

+"Who done it?  We've heard considerable about these goings on down in

+Hookerville, but we don't know who 'twas that killed Huck Finn."

+

+"Well, I reckon there's a right smart chance of people here that'd

+like to know who killed him.  Some think old Finn done it himself."

+

+"No—is that so?"

+

+"Most everybody thought it at first.  He'll never know how nigh he come

+to getting lynched.  But before night they changed around and judged it

+was done by a runaway nigger named Jim."

+

+"Why he—"

+

+I stopped.  I reckoned I better keep still.  She run on, and never

+noticed I had put in at all:

+

+"The nigger run off the very night Huck Finn was killed.  So there's a

+reward out for him—three hundred dollars.  And there's a reward out for

+old Finn, too—two hundred dollars.  You see, he come to town the

+morning after the murder, and told about it, and was out with 'em on the

+ferryboat hunt, and right away after he up and left.  Before night they

+wanted to lynch him, but he was gone, you see.  Well, next day they

+found out the nigger was gone; they found out he hadn't ben seen sence

+ten o'clock the night the murder was done.  So then they put it on him,

+you see; and while they was full of it, next day, back comes old Finn,

+and went boo-hooing to Judge Thatcher to get money to hunt for the

+nigger all over Illinois with. The judge gave him some, and that evening

+he got drunk, and was around till after midnight with a couple of mighty

+hard-looking strangers, and then went off with them.  Well, he hain't

+come back sence, and they ain't looking for him back till this thing

+blows over a little, for people thinks now that he killed his boy and

+fixed things so folks would think robbers done it, and then he'd get

+Huck's money without having to bother a long time with a lawsuit.

+ People do say he warn't any too good to do it.  Oh, he's sly, I reckon.

+ If he don't come back for a year he'll be all right.  You can't prove

+anything on him, you know; everything will be quieted down then, and

+he'll walk in Huck's money as easy as nothing."

+

+"Yes, I reckon so, 'm.  I don't see nothing in the way of it.  Has

+everybody quit thinking the nigger done it?"

+

+"Oh, no, not everybody.  A good many thinks he done it.  But they'll get

+the nigger pretty soon now, and maybe they can scare it out of him."

+

+"Why, are they after him yet?"

+

+"Well, you're innocent, ain't you!  Does three hundred dollars lay

+around every day for people to pick up?  Some folks think the nigger

+ain't far from here.  I'm one of them—but I hain't talked it around.  A

+few days ago I was talking with an old couple that lives next door in

+the log shanty, and they happened to say hardly anybody ever goes to

+that island over yonder that they call Jackson's Island.  Don't anybody

+live there? says I. No, nobody, says they.  I didn't say any more, but

+I done some thinking.  I was pretty near certain I'd seen smoke over

+there, about the head of the island, a day or two before that, so I says

+to myself, like as not that nigger's hiding over there; anyway, says

+I, it's worth the trouble to give the place a hunt.  I hain't seen any

+smoke sence, so I reckon maybe he's gone, if it was him; but husband's

+going over to see—him and another man.  He was gone up the river; but he

+got back to-day, and I told him as soon as he got here two hours ago."

+

+I had got so uneasy I couldn't set still.  I had to do something with my

+hands; so I took up a needle off of the table and went to threading

+it. My hands shook, and I was making a bad job of it.  When the woman

+stopped talking I looked up, and she was looking at me pretty curious

+and smiling a little.  I put down the needle and thread, and let on to

+be interested—and I was, too—and says:

+

+"Three hundred dollars is a power of money.  I wish my mother could get

+it. Is your husband going over there to-night?"

+

+"Oh, yes.  He went up-town with the man I was telling you of, to get a

+boat and see if they could borrow another gun.  They'll go over after

+midnight."

+

+"Couldn't they see better if they was to wait till daytime?"

+

+"Yes.  And couldn't the nigger see better, too?  After midnight he'll

+likely be asleep, and they can slip around through the woods and hunt up

+his camp fire all the better for the dark, if he's got one."

+

+"I didn't think of that."

+

+The woman kept looking at me pretty curious, and I didn't feel a bit

+comfortable.  Pretty soon she says,

+

+"What did you say your name was, honey?"

+

+"M—Mary Williams."

+

+Somehow it didn't seem to me that I said it was Mary before, so I didn't

+look up—seemed to me I said it was Sarah; so I felt sort of cornered,

+and was afeared maybe I was looking it, too.  I wished the woman would

+say something more; the longer she set still the uneasier I was.  But

+now she says:

+

+"Honey, I thought you said it was Sarah when you first come in?"

+

+"Oh, yes'm, I did.  Sarah Mary Williams.  Sarah's my first name.  Some

+calls me Sarah, some calls me Mary."

+

+"Oh, that's the way of it?"

+

+"Yes'm."

+

+I was feeling better then, but I wished I was out of there, anyway.  I

+couldn't look up yet.

+

+Well, the woman fell to talking about how hard times was, and how poor

+they had to live, and how the rats was as free as if they owned the

+place, and so forth and so on, and then I got easy again.  She was right

+about the rats. You'd see one stick his nose out of a hole in the corner

+every little while.  She said she had to have things handy to throw at

+them when she was alone, or they wouldn't give her no peace.  She showed

+me a bar of lead twisted up into a knot, and said she was a good shot

+with it generly, but she'd wrenched her arm a day or two ago, and didn't

+know whether she could throw true now.  But she watched for a chance,

+and directly banged away at a rat; but she missed him wide, and said

+"Ouch!" it hurt her arm so.  Then she told me to try for the next one.

+ I wanted to be getting away before the old man got back, but of course

+I didn't let on.  I got the thing, and the first rat that showed his

+nose I let drive, and if he'd a stayed where he was he'd a been a

+tolerable sick rat.  She said that was first-rate, and she reckoned I

+would hive the next one.  She went and got the lump of lead and fetched

+it back, and brought along a hank of yarn which she wanted me to help

+her with.  I held up my two hands and she put the hank over them, and

+went on talking about her and her husband's matters.  But she broke off

+to say:

+

+"Keep your eye on the rats.  You better have the lead in your lap,

+handy."

+

+So she dropped the lump into my lap just at that moment, and I clapped

+my legs together on it and she went on talking.  But only about a

+minute. Then she took off the hank and looked me straight in the face,

+and very pleasant, and says:

+

+"Come, now, what's your real name?"

+

+"Wh—what, mum?"

+

+"What's your real name?  Is it Bill, or Tom, or Bob?—or what is it?"

+

+I reckon I shook like a leaf, and I didn't know hardly what to do.  But

+I says:

+

+"Please to don't poke fun at a poor girl like me, mum.  If I'm in the

+way here, I'll—"

+

+"No, you won't.  Set down and stay where you are.  I ain't going to hurt

+you, and I ain't going to tell on you, nuther.  You just tell me your

+secret, and trust me.  I'll keep it; and, what's more, I'll help

+you. So'll my old man if you want him to.  You see, you're a runaway

+'prentice, that's all.  It ain't anything.  There ain't no harm in it.

+You've been treated bad, and you made up your mind to cut.  Bless you,

+child, I wouldn't tell on you.  Tell me all about it now, that's a good

+boy."

+

+So I said it wouldn't be no use to try to play it any longer, and I

+would just make a clean breast and tell her everything, but she musn't

+go back on her promise.  Then I told her my father and mother was dead,

+and the law had bound me out to a mean old farmer in the country thirty

+mile back from the river, and he treated me so bad I couldn't stand it

+no longer; he went away to be gone a couple of days, and so I took my

+chance and stole some of his daughter's old clothes and cleared out, and

+I had been three nights coming the thirty miles.  I traveled nights,

+and hid daytimes and slept, and the bag of bread and meat I carried from

+home lasted me all the way, and I had a-plenty.  I said I believed my

+uncle Abner Moore would take care of me, and so that was why I struck

+out for this town of Goshen.

+

+"Goshen, child?  This ain't Goshen.  This is St. Petersburg.  Goshen's

+ten mile further up the river.  Who told you this was Goshen?"

+

+"Why, a man I met at daybreak this morning, just as I was going to turn

+into the woods for my regular sleep.  He told me when the roads forked I

+must take the right hand, and five mile would fetch me to Goshen."

+

+"He was drunk, I reckon.  He told you just exactly wrong."

+

+"Well, he did act like he was drunk, but it ain't no matter now.  I got

+to be moving along.  I'll fetch Goshen before daylight."

+

+"Hold on a minute.  I'll put you up a snack to eat.  You might want it."

+

+So she put me up a snack, and says:

+

+"Say, when a cow's laying down, which end of her gets up first?  Answer

+up prompt now—don't stop to study over it.  Which end gets up first?"

+

+"The hind end, mum."

+

+"Well, then, a horse?"

+

+"The for'rard end, mum."

+

+"Which side of a tree does the moss grow on?"

+

+"North side."

+

+"If fifteen cows is browsing on a hillside, how many of them eats with

+their heads pointed the same direction?"

+

+"The whole fifteen, mum."

+

+"Well, I reckon you have lived in the country.  I thought maybe you

+was trying to hocus me again.  What's your real name, now?"

+

+"George Peters, mum."

+

+"Well, try to remember it, George.  Don't forget and tell me it's

+Elexander before you go, and then get out by saying it's George

+Elexander when I catch you.  And don't go about women in that old

+calico.  You do a girl tolerable poor, but you might fool men, maybe.

+ Bless you, child, when you set out to thread a needle don't hold the

+thread still and fetch the needle up to it; hold the needle still and

+poke the thread at it; that's the way a woman most always does, but a

+man always does t'other way.  And when you throw at a rat or anything,

+hitch yourself up a tiptoe and fetch your hand up over your head as

+awkward as you can, and miss your rat about six or seven foot. Throw

+stiff-armed from the shoulder, like there was a pivot there for it to

+turn on, like a girl; not from the wrist and elbow, with your arm out

+to one side, like a boy.  And, mind you, when a girl tries to catch

+anything in her lap she throws her knees apart; she don't clap them

+together, the way you did when you catched the lump of lead.  Why, I

+spotted you for a boy when you was threading the needle; and I contrived

+the other things just to make certain.  Now trot along to your uncle,

+Sarah Mary Williams George Elexander Peters, and if you get into trouble

+you send word to Mrs. Judith Loftus, which is me, and I'll do what I can

+to get you out of it.  Keep the river road all the way, and next time

+you tramp take shoes and socks with you. The river road's a rocky one,

+and your feet'll be in a condition when you get to Goshen, I reckon."

+

+I went up the bank about fifty yards, and then I doubled on my tracks

+and slipped back to where my canoe was, a good piece below the house.  I

+jumped in, and was off in a hurry.  I went up-stream far enough to

+make the head of the island, and then started across.  I took off the

+sun-bonnet, for I didn't want no blinders on then.  When I was about the

+middle I heard the clock begin to strike, so I stops and listens; the

+sound come faint over the water but clear—eleven.  When I struck the

+head of the island I never waited to blow, though I was most winded, but

+I shoved right into the timber where my old camp used to be, and started

+a good fire there on a high and dry spot.

+

+Then I jumped in the canoe and dug out for our place, a mile and a half

+below, as hard as I could go.  I landed, and slopped through the timber

+and up the ridge and into the cavern.  There Jim laid, sound asleep on

+the ground.  I roused him out and says:

+

+"Git up and hump yourself, Jim!  There ain't a minute to lose.  They're

+after us!"

+

+Jim never asked no questions, he never said a word; but the way he

+worked for the next half an hour showed about how he was scared.  By

+that time everything we had in the world was on our raft, and she was

+ready to be shoved out from the willow cove where she was hid.  We

+put out the camp fire at the cavern the first thing, and didn't show a

+candle outside after that.

+

+I took the canoe out from the shore a little piece, and took a look;

+but if there was a boat around I couldn't see it, for stars and shadows

+ain't good to see by.  Then we got out the raft and slipped along down

+in the shade, past the foot of the island dead still—never saying a

+word.

+

+

+

+

+CHAPTER XII.

+

+IT must a been close on to one o'clock when we got below the island at

+last, and the raft did seem to go mighty slow.  If a boat was to come

+along we was going to take to the canoe and break for the Illinois

+shore; and it was well a boat didn't come, for we hadn't ever thought to

+put the gun in the canoe, or a fishing-line, or anything to eat.  We

+was in ruther too much of a sweat to think of so many things.  It warn't

+good judgment to put everything on the raft.

+

+If the men went to the island I just expect they found the camp fire I

+built, and watched it all night for Jim to come.  Anyways, they stayed

+away from us, and if my building the fire never fooled them it warn't no

+fault of mine.  I played it as low down on them as I could.

+

+When the first streak of day began to show we tied up to a towhead in a

+big bend on the Illinois side, and hacked off cottonwood branches with

+the hatchet, and covered up the raft with them so she looked like there

+had been a cave-in in the bank there.  A tow-head is a sandbar that has

+cottonwoods on it as thick as harrow-teeth.

+

+We had mountains on the Missouri shore and heavy timber on the Illinois

+side, and the channel was down the Missouri shore at that place, so we

+warn't afraid of anybody running across us.  We laid there all day,

+and watched the rafts and steamboats spin down the Missouri shore, and

+up-bound steamboats fight the big river in the middle.  I told Jim all

+about the time I had jabbering with that woman; and Jim said she was

+a smart one, and if she was to start after us herself she wouldn't set

+down and watch a camp fire—no, sir, she'd fetch a dog.  Well, then, I

+said, why couldn't she tell her husband to fetch a dog?  Jim said he

+bet she did think of it by the time the men was ready to start, and he

+believed they must a gone up-town to get a dog and so they lost all that

+time, or else we wouldn't be here on a towhead sixteen or seventeen mile

+below the village—no, indeedy, we would be in that same old town again.

+ So I said I didn't care what was the reason they didn't get us as long

+as they didn't.

+

+When it was beginning to come on dark we poked our heads out of the

+cottonwood thicket, and looked up and down and across; nothing in sight;

+so Jim took up some of the top planks of the raft and built a snug

+wigwam to get under in blazing weather and rainy, and to keep the things

+dry. Jim made a floor for the wigwam, and raised it a foot or more above

+the level of the raft, so now the blankets and all the traps was out of

+reach of steamboat waves.  Right in the middle of the wigwam we made a

+layer of dirt about five or six inches deep with a frame around it for

+to hold it to its place; this was to build a fire on in sloppy weather

+or chilly; the wigwam would keep it from being seen.  We made an extra

+steering-oar, too, because one of the others might get broke on a snag

+or something. We fixed up a short forked stick to hang the old lantern

+on, because we must always light the lantern whenever we see a steamboat

+coming down-stream, to keep from getting run over; but we wouldn't have

+to light it for up-stream boats unless we see we was in what they call

+a "crossing"; for the river was pretty high yet, very low banks being

+still a little under water; so up-bound boats didn't always run the

+channel, but hunted easy water.

+

+This second night we run between seven and eight hours, with a current

+that was making over four mile an hour.  We catched fish and talked,

+and we took a swim now and then to keep off sleepiness.  It was kind of

+solemn, drifting down the big, still river, laying on our backs looking

+up at the stars, and we didn't ever feel like talking loud, and it

+warn't often that we laughed—only a little kind of a low chuckle.  We

+had mighty good weather as a general thing, and nothing ever happened to

+us at all—that night, nor the next, nor the next.

+

+Every night we passed towns, some of them away up on black hillsides,

+nothing but just a shiny bed of lights; not a house could you see.  The

+fifth night we passed St. Louis, and it was like the whole world lit up.

+In St. Petersburg they used to say there was twenty or thirty thousand

+people in St. Louis, but I never believed it till I see that wonderful

+spread of lights at two o'clock that still night.  There warn't a sound

+there; everybody was asleep.

+

+Every night now I used to slip ashore towards ten o'clock at some little

+village, and buy ten or fifteen cents' worth of meal or bacon or other

+stuff to eat; and sometimes I lifted a chicken that warn't roosting

+comfortable, and took him along.  Pap always said, take a chicken when

+you get a chance, because if you don't want him yourself you can easy

+find somebody that does, and a good deed ain't ever forgot.  I never see

+pap when he didn't want the chicken himself, but that is what he used to

+say, anyway.

+

+Mornings before daylight I slipped into cornfields and borrowed a

+watermelon, or a mushmelon, or a punkin, or some new corn, or things of

+that kind.  Pap always said it warn't no harm to borrow things if you

+was meaning to pay them back some time; but the widow said it warn't

+anything but a soft name for stealing, and no decent body would do it.

+ Jim said he reckoned the widow was partly right and pap was partly

+right; so the best way would be for us to pick out two or three things

+from the list and say we wouldn't borrow them any more—then he reckoned

+it wouldn't be no harm to borrow the others.  So we talked it over all

+one night, drifting along down the river, trying to make up our minds

+whether to drop the watermelons, or the cantelopes, or the mushmelons,

+or what.  But towards daylight we got it all settled satisfactory, and

+concluded to drop crabapples and p'simmons.  We warn't feeling just

+right before that, but it was all comfortable now.  I was glad the way

+it come out, too, because crabapples ain't ever good, and the p'simmons

+wouldn't be ripe for two or three months yet.

+

+We shot a water-fowl now and then that got up too early in the morning

+or didn't go to bed early enough in the evening.  Take it all round, we

+lived pretty high.

+

+The fifth night below St. Louis we had a big storm after midnight, with

+a power of thunder and lightning, and the rain poured down in a solid

+sheet. We stayed in the wigwam and let the raft take care of itself.

+When the lightning glared out we could see a big straight river ahead,

+and high, rocky bluffs on both sides.  By and by says I, "Hel-lo, Jim,

+looky yonder!" It was a steamboat that had killed herself on a rock.

+ We was drifting straight down for her.  The lightning showed her very

+distinct.  She was leaning over, with part of her upper deck above

+water, and you could see every little chimbly-guy clean and clear, and a

+chair by the big bell, with an old slouch hat hanging on the back of it,

+when the flashes come.

+

+Well, it being away in the night and stormy, and all so mysterious-like,

+I felt just the way any other boy would a felt when I see that wreck

+laying there so mournful and lonesome in the middle of the river.  I

+wanted to get aboard of her and slink around a little, and see what

+there was there.  So I says:

+

+"Le's land on her, Jim."

+

+But Jim was dead against it at first.  He says:

+

+"I doan' want to go fool'n 'long er no wrack.  We's doin' blame' well,

+en we better let blame' well alone, as de good book says.  Like as not

+dey's a watchman on dat wrack."

+

+"Watchman your grandmother," I says; "there ain't nothing to watch but

+the texas and the pilot-house; and do you reckon anybody's going to resk

+his life for a texas and a pilot-house such a night as this, when

+it's likely to break up and wash off down the river any minute?"  Jim

+couldn't say nothing to that, so he didn't try.  "And besides," I says,

+"we might borrow something worth having out of the captain's stateroom.

+ Seegars, I bet you—and cost five cents apiece, solid cash.  Steamboat

+captains is always rich, and get sixty dollars a month, and they don't

+care a cent what a thing costs, you know, long as they want it.  Stick a

+candle in your pocket; I can't rest, Jim, till we give her a rummaging.

+ Do you reckon Tom Sawyer would ever go by this thing?  Not for pie, he

+wouldn't. He'd call it an adventure—that's what he'd call it; and he'd

+land on that wreck if it was his last act.  And wouldn't he throw style

+into it?—wouldn't he spread himself, nor nothing?  Why, you'd think it

+was Christopher C'lumbus discovering Kingdom-Come.  I wish Tom Sawyer

+was here."

+

+Jim he grumbled a little, but give in.  He said we mustn't talk any more

+than we could help, and then talk mighty low.  The lightning showed us

+the wreck again just in time, and we fetched the stabboard derrick, and

+made fast there.

+

+The deck was high out here.  We went sneaking down the slope of it to

+labboard, in the dark, towards the texas, feeling our way slow with our

+feet, and spreading our hands out to fend off the guys, for it was so

+dark we couldn't see no sign of them.  Pretty soon we struck the forward

+end of the skylight, and clumb on to it; and the next step fetched us in

+front of the captain's door, which was open, and by Jimminy, away down

+through the texas-hall we see a light! and all in the same second we

+seem to hear low voices in yonder!

+

+Jim whispered and said he was feeling powerful sick, and told me to come

+along.  I says, all right, and was going to start for the raft; but just

+then I heard a voice wail out and say:

+

+"Oh, please don't, boys; I swear I won't ever tell!"

+

+Another voice said, pretty loud:

+

+"It's a lie, Jim Turner.  You've acted this way before.  You always want

+more'n your share of the truck, and you've always got it, too, because

+you've swore 't if you didn't you'd tell.  But this time you've said

+it jest one time too many.  You're the meanest, treacherousest hound in

+this country."

+

+By this time Jim was gone for the raft.  I was just a-biling with

+curiosity; and I says to myself, Tom Sawyer wouldn't back out now,

+and so I won't either; I'm a-going to see what's going on here.  So I

+dropped on my hands and knees in the little passage, and crept aft

+in the dark till there warn't but one stateroom betwixt me and the

+cross-hall of the texas.  Then in there I see a man stretched on the

+floor and tied hand and foot, and two men standing over him, and one

+of them had a dim lantern in his hand, and the other one had a pistol.

+ This one kept pointing the pistol at the man's head on the floor, and

+saying:

+

+"I'd like to!  And I orter, too—a mean skunk!"

+

+The man on the floor would shrivel up and say, "Oh, please don't, Bill;

+I hain't ever goin' to tell."

+

+And every time he said that the man with the lantern would laugh and

+say:

+

+"'Deed you ain't!  You never said no truer thing 'n that, you bet

+you." And once he said:  "Hear him beg! and yit if we hadn't got the

+best of him and tied him he'd a killed us both.  And what for?  Jist

+for noth'n. Jist because we stood on our rights—that's what for.  But

+I lay you ain't a-goin' to threaten nobody any more, Jim Turner.  Put

+up that pistol, Bill."

+

+Bill says:

+

+"I don't want to, Jake Packard.  I'm for killin' him—and didn't he kill

+old Hatfield jist the same way—and don't he deserve it?"

+

+"But I don't want him killed, and I've got my reasons for it."

+

+"Bless yo' heart for them words, Jake Packard!  I'll never forgit you

+long's I live!" says the man on the floor, sort of blubbering.

+

+Packard didn't take no notice of that, but hung up his lantern on a nail

+and started towards where I was there in the dark, and motioned Bill

+to come.  I crawfished as fast as I could about two yards, but the boat

+slanted so that I couldn't make very good time; so to keep from getting

+run over and catched I crawled into a stateroom on the upper side.

+ The man came a-pawing along in the dark, and when Packard got to my

+stateroom, he says:

+

+"Here—come in here."

+

+And in he come, and Bill after him.  But before they got in I was up

+in the upper berth, cornered, and sorry I come.  Then they stood there,

+with their hands on the ledge of the berth, and talked.  I couldn't see

+them, but I could tell where they was by the whisky they'd been having.

+ I was glad I didn't drink whisky; but it wouldn't made much difference

+anyway, because most of the time they couldn't a treed me because I

+didn't breathe.  I was too scared.  And, besides, a body couldn't

+breathe and hear such talk.  They talked low and earnest.  Bill wanted

+to kill Turner.  He says:

+

+"He's said he'll tell, and he will.  If we was to give both our shares

+to him now it wouldn't make no difference after the row and the way

+we've served him.  Shore's you're born, he'll turn State's evidence; now

+you hear me.  I'm for putting him out of his troubles."

+

+"So'm I," says Packard, very quiet.

+

+"Blame it, I'd sorter begun to think you wasn't.  Well, then, that's all

+right.  Le's go and do it."

+

+"Hold on a minute; I hain't had my say yit.  You listen to me.

+Shooting's good, but there's quieter ways if the thing's got to be

+done. But what I say is this:  it ain't good sense to go court'n around

+after a halter if you can git at what you're up to in some way that's

+jist as good and at the same time don't bring you into no resks.  Ain't

+that so?"

+

+"You bet it is.  But how you goin' to manage it this time?"

+

+"Well, my idea is this:  we'll rustle around and gather up whatever

+pickins we've overlooked in the staterooms, and shove for shore and hide

+the truck. Then we'll wait.  Now I say it ain't a-goin' to be more'n two

+hours befo' this wrack breaks up and washes off down the river.  See?

+He'll be drownded, and won't have nobody to blame for it but his own

+self.  I reckon that's a considerble sight better 'n killin' of him.

+ I'm unfavorable to killin' a man as long as you can git aroun' it; it

+ain't good sense, it ain't good morals.  Ain't I right?"

+

+"Yes, I reck'n you are.  But s'pose she don't break up and wash off?"

+

+"Well, we can wait the two hours anyway and see, can't we?"

+

+"All right, then; come along."

+

+So they started, and I lit out, all in a cold sweat, and scrambled

+forward. It was dark as pitch there; but I said, in a kind of a coarse

+whisper, "Jim!" and he answered up, right at my elbow, with a sort of a

+moan, and I says:

+

+"Quick, Jim, it ain't no time for fooling around and moaning; there's a

+gang of murderers in yonder, and if we don't hunt up their boat and set

+her drifting down the river so these fellows can't get away from the

+wreck there's one of 'em going to be in a bad fix.  But if we find their

+boat we can put all of 'em in a bad fix—for the sheriff 'll get 'em.

+Quick—hurry!  I'll hunt the labboard side, you hunt the stabboard. You

+start at the raft, and—"

+

+"Oh, my lordy, lordy!  raf'?  Dey ain' no raf' no mo'; she done broke

+loose en gone I—en here we is!"

+

+

+

+

+CHAPTER XIII.

+

+WELL, I catched my breath and most fainted.  Shut up on a wreck with

+such a gang as that!  But it warn't no time to be sentimentering.  We'd

+got to find that boat now—had to have it for ourselves.  So we went

+a-quaking and shaking down the stabboard side, and slow work it was,

+too—seemed a week before we got to the stern.  No sign of a boat.  Jim

+said he didn't believe he could go any further—so scared he hadn't

+hardly any strength left, he said.  But I said, come on, if we get left

+on this wreck we are in a fix, sure.  So on we prowled again.  We struck

+for the stern of the texas, and found it, and then scrabbled along

+forwards on the skylight, hanging on from shutter to shutter, for the

+edge of the skylight was in the water.  When we got pretty close to the

+cross-hall door there was the skiff, sure enough!  I could just barely

+see her.  I felt ever so thankful.  In another second I would a been

+aboard of her, but just then the door opened.  One of the men stuck his

+head out only about a couple of foot from me, and I thought I was gone;

+but he jerked it in again, and says:

+

+"Heave that blame lantern out o' sight, Bill!"

+

+He flung a bag of something into the boat, and then got in himself and

+set down.  It was Packard.  Then Bill he come out and got in.  Packard

+says, in a low voice:

+

+"All ready—shove off!"

+

+I couldn't hardly hang on to the shutters, I was so weak.  But Bill

+says:

+

+"Hold on—'d you go through him?"

+

+"No.  Didn't you?"

+

+"No.  So he's got his share o' the cash yet."

+

+"Well, then, come along; no use to take truck and leave money."

+

+"Say, won't he suspicion what we're up to?"

+

+"Maybe he won't.  But we got to have it anyway. Come along."

+

+So they got out and went in.

+

+The door slammed to because it was on the careened side; and in a half

+second I was in the boat, and Jim come tumbling after me.  I out with my

+knife and cut the rope, and away we went!

+

+We didn't touch an oar, and we didn't speak nor whisper, nor hardly even

+breathe.  We went gliding swift along, dead silent, past the tip of the

+paddle-box, and past the stern; then in a second or two more we was a

+hundred yards below the wreck, and the darkness soaked her up, every

+last sign of her, and we was safe, and knowed it.

+

+When we was three or four hundred yards down-stream we see the lantern

+show like a little spark at the texas door for a second, and we knowed

+by that that the rascals had missed their boat, and was beginning to

+understand that they was in just as much trouble now as Jim Turner was.

+

+Then Jim manned the oars, and we took out after our raft.  Now was the

+first time that I begun to worry about the men—I reckon I hadn't

+had time to before.  I begun to think how dreadful it was, even for

+murderers, to be in such a fix.  I says to myself, there ain't no

+telling but I might come to be a murderer myself yet, and then how would

+I like it?  So says I to Jim:

+

+"The first light we see we'll land a hundred yards below it or above

+it, in a place where it's a good hiding-place for you and the skiff, and

+then I'll go and fix up some kind of a yarn, and get somebody to go for

+that gang and get them out of their scrape, so they can be hung when

+their time comes."

+

+But that idea was a failure; for pretty soon it begun to storm again,

+and this time worse than ever.  The rain poured down, and never a light

+showed; everybody in bed, I reckon.  We boomed along down the river,

+watching for lights and watching for our raft.  After a long time the

+rain let up, but the clouds stayed, and the lightning kept whimpering,

+and by and by a flash showed us a black thing ahead, floating, and we

+made for it.

+

+It was the raft, and mighty glad was we to get aboard of it again.  We

+seen a light now away down to the right, on shore.  So I said I would

+go for it. The skiff was half full of plunder which that gang had stole

+there on the wreck.  We hustled it on to the raft in a pile, and I told

+Jim to float along down, and show a light when he judged he had gone

+about two mile, and keep it burning till I come; then I manned my oars

+and shoved for the light.  As I got down towards it three or four more

+showed—up on a hillside.  It was a village.  I closed in above the shore

+light, and laid on my oars and floated.  As I went by I see it was a

+lantern hanging on the jackstaff of a double-hull ferryboat.  I skimmed

+around for the watchman, a-wondering whereabouts he slept; and by and

+by I found him roosting on the bitts forward, with his head down between

+his knees.  I gave his shoulder two or three little shoves, and begun to

+cry.

+

+He stirred up in a kind of a startlish way; but when he see it was only

+me he took a good gap and stretch, and then he says:

+

+"Hello, what's up?  Don't cry, bub.  What's the trouble?"

+

+I says:

+

+"Pap, and mam, and sis, and—"

+

+Then I broke down.  He says:

+

+"Oh, dang it now, don't take on so; we all has to have our troubles,

+and this 'n 'll come out all right.  What's the matter with 'em?"

+

+"They're—they're—are you the watchman of the boat?"

+

+"Yes," he says, kind of pretty-well-satisfied like.  "I'm the captain

+and the owner and the mate and the pilot and watchman and head

+deck-hand; and sometimes I'm the freight and passengers.  I ain't as

+rich as old Jim Hornback, and I can't be so blame' generous and good

+to Tom, Dick, and Harry as what he is, and slam around money the way he

+does; but I've told him a many a time 't I wouldn't trade places with

+him; for, says I, a sailor's life's the life for me, and I'm derned if

+I'd live two mile out o' town, where there ain't nothing ever goin'

+on, not for all his spondulicks and as much more on top of it.  Says I—"

+

+I broke in and says:

+

+"They're in an awful peck of trouble, and—"

+

+"Who is?"

+

+"Why, pap and mam and sis and Miss Hooker; and if you'd take your

+ferryboat and go up there—"

+

+"Up where?  Where are they?"

+

+"On the wreck."

+

+"What wreck?"

+

+"Why, there ain't but one."

+

+"What, you don't mean the Walter Scott?"

+

+"Yes."

+

+"Good land! what are they doin' there, for gracious sakes?"

+

+"Well, they didn't go there a-purpose."

+

+"I bet they didn't!  Why, great goodness, there ain't no chance for 'em

+if they don't git off mighty quick!  Why, how in the nation did they

+ever git into such a scrape?"

+

+"Easy enough.  Miss Hooker was a-visiting up there to the town—"

+

+"Yes, Booth's Landing—go on."

+

+"She was a-visiting there at Booth's Landing, and just in the edge of

+the evening she started over with her nigger woman in the horse-ferry

+to stay all night at her friend's house, Miss What-you-may-call-her I

+disremember her name—and they lost their steering-oar, and swung

+around and went a-floating down, stern first, about two mile, and

+saddle-baggsed on the wreck, and the ferryman and the nigger woman and

+the horses was all lost, but Miss Hooker she made a grab and got aboard

+the wreck.  Well, about an hour after dark we come along down in our

+trading-scow, and it was so dark we didn't notice the wreck till we was

+right on it; and so we saddle-baggsed; but all of us was saved but

+Bill Whipple—and oh, he was the best cretur!—I most wish 't it had

+been me, I do."

+

+"My George!  It's the beatenest thing I ever struck.  And then what

+did you all do?"

+

+"Well, we hollered and took on, but it's so wide there we couldn't

+make nobody hear.  So pap said somebody got to get ashore and get help

+somehow. I was the only one that could swim, so I made a dash for it,

+and Miss Hooker she said if I didn't strike help sooner, come here and

+hunt up her uncle, and he'd fix the thing.  I made the land about a mile

+below, and been fooling along ever since, trying to get people to do

+something, but they said, 'What, in such a night and such a current?

+There ain't no sense in it; go for the steam ferry.'  Now if you'll go

+and—"

+

+"By Jackson, I'd like to, and, blame it, I don't know but I will; but

+who in the dingnation's a-going' to pay for it?  Do you reckon your

+pap—"

+

+"Why that's all right.  Miss Hooker she tole me, particular, that

+her uncle Hornback—"

+

+"Great guns! is he her uncle?  Looky here, you break for that light

+over yonder-way, and turn out west when you git there, and about a

+quarter of a mile out you'll come to the tavern; tell 'em to dart you

+out to Jim Hornback's, and he'll foot the bill.  And don't you fool

+around any, because he'll want to know the news.  Tell him I'll have

+his niece all safe before he can get to town.  Hump yourself, now; I'm

+a-going up around the corner here to roust out my engineer."

+

+I struck for the light, but as soon as he turned the corner I went back

+and got into my skiff and bailed her out, and then pulled up shore in

+the easy water about six hundred yards, and tucked myself in among

+some woodboats; for I couldn't rest easy till I could see the ferryboat

+start. But take it all around, I was feeling ruther comfortable on

+accounts of taking all this trouble for that gang, for not many would

+a done it.  I wished the widow knowed about it.  I judged she would be

+proud of me for helping these rapscallions, because rapscallions and

+dead beats is the kind the widow and good people takes the most interest

+in.

+

+Well, before long here comes the wreck, dim and dusky, sliding along

+down! A kind of cold shiver went through me, and then I struck out for

+her.  She was very deep, and I see in a minute there warn't much chance

+for anybody being alive in her.  I pulled all around her and hollered

+a little, but there wasn't any answer; all dead still.  I felt a little

+bit heavy-hearted about the gang, but not much, for I reckoned if they

+could stand it I could.

+

+Then here comes the ferryboat; so I shoved for the middle of the river

+on a long down-stream slant; and when I judged I was out of eye-reach

+I laid on my oars, and looked back and see her go and smell around the

+wreck for Miss Hooker's remainders, because the captain would know her

+uncle Hornback would want them; and then pretty soon the ferryboat give

+it up and went for the shore, and I laid into my work and went a-booming

+down the river.

+

+It did seem a powerful long time before Jim's light showed up; and when

+it did show it looked like it was a thousand mile off.  By the time I

+got there the sky was beginning to get a little gray in the east; so we

+struck for an island, and hid the raft, and sunk the skiff, and turned

+in and slept like dead people.

+

+

+

+

+CHAPTER XIV.

+

+BY and by, when we got up, we turned over the truck the gang had stole

+off of the wreck, and found boots, and blankets, and clothes, and all

+sorts of other things, and a lot of books, and a spyglass, and three

+boxes of seegars.  We hadn't ever been this rich before in neither of

+our lives.  The seegars was prime.  We laid off all the afternoon in the

+woods talking, and me reading the books, and having a general good

+time. I told Jim all about what happened inside the wreck and at the

+ferryboat, and I said these kinds of things was adventures; but he said

+he didn't want no more adventures.  He said that when I went in the

+texas and he crawled back to get on the raft and found her gone he

+nearly died, because he judged it was all up with him anyway it could

+be fixed; for if he didn't get saved he would get drownded; and if he

+did get saved, whoever saved him would send him back home so as to get

+the reward, and then Miss Watson would sell him South, sure.  Well, he

+was right; he was most always right; he had an uncommon level head for a

+nigger.

+

+I read considerable to Jim about kings and dukes and earls and such, and

+how gaudy they dressed, and how much style they put on, and called each

+other your majesty, and your grace, and your lordship, and so on, 'stead

+of mister; and Jim's eyes bugged out, and he was interested.  He says:

+

+"I didn' know dey was so many un um.  I hain't hearn 'bout none un um,

+skasely, but ole King Sollermun, onless you counts dem kings dat's in a

+pack er k'yards.  How much do a king git?"

+

+"Get?"  I says; "why, they get a thousand dollars a month if they want

+it; they can have just as much as they want; everything belongs to

+them."

+

+"Ain' dat gay?  En what dey got to do, Huck?"

+

+"They don't do nothing!  Why, how you talk! They just set around."

+

+"No; is dat so?"

+

+"Of course it is.  They just set around—except, maybe, when there's a

+war; then they go to the war.  But other times they just lazy around; or

+go hawking—just hawking and sp—Sh!—d' you hear a noise?"

+

+We skipped out and looked; but it warn't nothing but the flutter of a

+steamboat's wheel away down, coming around the point; so we come back.

+

+"Yes," says I, "and other times, when things is dull, they fuss with the

+parlyment; and if everybody don't go just so he whacks their heads off.

+But mostly they hang round the harem."

+

+"Roun' de which?"

+

+"Harem."

+

+"What's de harem?"

+

+"The place where he keeps his wives.  Don't you know about the harem?

+Solomon had one; he had about a million wives."

+

+"Why, yes, dat's so; I—I'd done forgot it.  A harem's a bo'd'n-house, I

+reck'n.  Mos' likely dey has rackety times in de nussery.  En I reck'n

+de wives quarrels considable; en dat 'crease de racket.  Yit dey say

+Sollermun de wises' man dat ever live'.  I doan' take no stock in

+dat. Bekase why: would a wise man want to live in de mids' er sich a

+blim-blammin' all de time?  No—'deed he wouldn't.  A wise man 'ud take

+en buil' a biler-factry; en den he could shet down de biler-factry

+when he want to res'."

+

+"Well, but he was the wisest man, anyway; because the widow she told

+me so, her own self."

+

+"I doan k'yer what de widder say, he warn't no wise man nuther.  He

+had some er de dad-fetchedes' ways I ever see.  Does you know 'bout dat

+chile dat he 'uz gwyne to chop in two?"

+

+"Yes, the widow told me all about it."

+

+"Well, den!  Warn' dat de beatenes' notion in de worl'?  You jes'

+take en look at it a minute.  Dah's de stump, dah—dat's one er de women;

+heah's you—dat's de yuther one; I's Sollermun; en dish yer dollar bill's

+de chile.  Bofe un you claims it.  What does I do?  Does I shin aroun'

+mongs' de neighbors en fine out which un you de bill do b'long to, en

+han' it over to de right one, all safe en soun', de way dat anybody dat

+had any gumption would?  No; I take en whack de bill in two, en give

+half un it to you, en de yuther half to de yuther woman.  Dat's de way

+Sollermun was gwyne to do wid de chile.  Now I want to ast you:  what's

+de use er dat half a bill?—can't buy noth'n wid it.  En what use is a

+half a chile?  I wouldn' give a dern for a million un um."

+

+"But hang it, Jim, you've clean missed the point—blame it, you've missed

+it a thousand mile."

+

+"Who?  Me?  Go 'long.  Doan' talk to me 'bout yo' pints.  I reck'n I

+knows sense when I sees it; en dey ain' no sense in sich doin's as

+dat. De 'spute warn't 'bout a half a chile, de 'spute was 'bout a whole

+chile; en de man dat think he kin settle a 'spute 'bout a whole chile

+wid a half a chile doan' know enough to come in out'n de rain.  Doan'

+talk to me 'bout Sollermun, Huck, I knows him by de back."

+

+"But I tell you you don't get the point."

+

+"Blame de point!  I reck'n I knows what I knows.  En mine you, de real

+pint is down furder—it's down deeper.  It lays in de way Sollermun was

+raised.  You take a man dat's got on'y one or two chillen; is dat man

+gwyne to be waseful o' chillen?  No, he ain't; he can't 'ford it.  He

+know how to value 'em.  But you take a man dat's got 'bout five million

+chillen runnin' roun' de house, en it's diffunt.  He as soon chop a

+chile in two as a cat. Dey's plenty mo'.  A chile er two, mo' er less,

+warn't no consekens to Sollermun, dad fatch him!"

+

+I never see such a nigger.  If he got a notion in his head once, there

+warn't no getting it out again.  He was the most down on Solomon of

+any nigger I ever see.  So I went to talking about other kings, and let

+Solomon slide.  I told about Louis Sixteenth that got his head cut off

+in France long time ago; and about his little boy the dolphin, that

+would a been a king, but they took and shut him up in jail, and some say

+he died there.

+

+"Po' little chap."

+

+"But some says he got out and got away, and come to America."

+

+"Dat's good!  But he'll be pooty lonesome—dey ain' no kings here, is

+dey, Huck?"

+

+"No."

+

+"Den he cain't git no situation.  What he gwyne to do?"

+

+"Well, I don't know.  Some of them gets on the police, and some of them

+learns people how to talk French."

+

+"Why, Huck, doan' de French people talk de same way we does?"

+

+"No, Jim; you couldn't understand a word they said—not a single word."

+

+"Well, now, I be ding-busted!  How do dat come?"

+

+"I don't know; but it's so.  I got some of their jabber out of a book.

+S'pose a man was to come to you and say Polly-voo-franzy—what would you

+think?"

+

+"I wouldn' think nuff'n; I'd take en bust him over de head—dat is, if he

+warn't white.  I wouldn't 'low no nigger to call me dat."

+

+"Shucks, it ain't calling you anything.  It's only saying, do you know

+how to talk French?"

+

+"Well, den, why couldn't he say it?"

+

+"Why, he is a-saying it.  That's a Frenchman's way of saying it."

+

+"Well, it's a blame ridicklous way, en I doan' want to hear no mo' 'bout

+it.  Dey ain' no sense in it."

+

+"Looky here, Jim; does a cat talk like we do?"

+

+"No, a cat don't."

+

+"Well, does a cow?"

+

+"No, a cow don't, nuther."

+

+"Does a cat talk like a cow, or a cow talk like a cat?"

+

+"No, dey don't."

+

+"It's natural and right for 'em to talk different from each other, ain't

+it?"

+

+"Course."

+

+"And ain't it natural and right for a cat and a cow to talk different

+from us?"

+

+"Why, mos' sholy it is."

+

+"Well, then, why ain't it natural and right for a Frenchman to talk

+different from us?  You answer me that."

+

+"Is a cat a man, Huck?"

+

+"No."

+

+"Well, den, dey ain't no sense in a cat talkin' like a man.  Is a cow a

+man?—er is a cow a cat?"

+

+"No, she ain't either of them."

+

+"Well, den, she ain't got no business to talk like either one er the

+yuther of 'em.  Is a Frenchman a man?"

+

+"Yes."

+

+"Well, den!  Dad blame it, why doan' he talk like a man?  You answer

+me dat!"

+

+I see it warn't no use wasting words—you can't learn a nigger to argue.

+So I quit.

+

+

+

+

+CHAPTER XV.

+

+WE judged that three nights more would fetch us to Cairo, at the bottom

+of Illinois, where the Ohio River comes in, and that was what we was

+after.  We would sell the raft and get on a steamboat and go way up the

+Ohio amongst the free States, and then be out of trouble.

+

+Well, the second night a fog begun to come on, and we made for a towhead

+to tie to, for it wouldn't do to try to run in a fog; but when I paddled

+ahead in the canoe, with the line to make fast, there warn't anything

+but little saplings to tie to.  I passed the line around one of them

+right on the edge of the cut bank, but there was a stiff current, and

+the raft come booming down so lively she tore it out by the roots and

+away she went.  I see the fog closing down, and it made me so sick and

+scared I couldn't budge for most a half a minute it seemed to me—and

+then there warn't no raft in sight; you couldn't see twenty yards.  I

+jumped into the canoe and run back to the stern, and grabbed the paddle

+and set her back a stroke.  But she didn't come.  I was in such a hurry

+I hadn't untied her.  I got up and tried to untie her, but I was so

+excited my hands shook so I couldn't hardly do anything with them.

+

+As soon as I got started I took out after the raft, hot and heavy, right

+down the towhead.  That was all right as far as it went, but the towhead

+warn't sixty yards long, and the minute I flew by the foot of it I shot

+out into the solid white fog, and hadn't no more idea which way I was

+going than a dead man.

+

+Thinks I, it won't do to paddle; first I know I'll run into the bank

+or a towhead or something; I got to set still and float, and yet it's

+mighty fidgety business to have to hold your hands still at such a time.

+ I whooped and listened.  Away down there somewheres I hears a small

+whoop, and up comes my spirits.  I went tearing after it, listening

+sharp to hear it again.  The next time it come I see I warn't heading

+for it, but heading away to the right of it.  And the next time I was

+heading away to the left of it—and not gaining on it much either, for

+I was flying around, this way and that and t'other, but it was going

+straight ahead all the time.

+

+I did wish the fool would think to beat a tin pan, and beat it all the

+time, but he never did, and it was the still places between the whoops

+that was making the trouble for me.  Well, I fought along, and directly

+I hears the whoop behind me.  I was tangled good now.  That was

+somebody else's whoop, or else I was turned around.

+

+I throwed the paddle down.  I heard the whoop again; it was behind me

+yet, but in a different place; it kept coming, and kept changing its

+place, and I kept answering, till by and by it was in front of me again,

+and I knowed the current had swung the canoe's head down-stream, and I

+was all right if that was Jim and not some other raftsman hollering.

+ I couldn't tell nothing about voices in a fog, for nothing don't look

+natural nor sound natural in a fog.

+

+The whooping went on, and in about a minute I come a-booming down on a

+cut bank with smoky ghosts of big trees on it, and the current throwed

+me off to the left and shot by, amongst a lot of snags that fairly

+roared, the currrent was tearing by them so swift.

+

+In another second or two it was solid white and still again.  I set

+perfectly still then, listening to my heart thump, and I reckon I didn't

+draw a breath while it thumped a hundred.

+

+I just give up then.  I knowed what the matter was.  That cut bank

+was an island, and Jim had gone down t'other side of it.  It warn't no

+towhead that you could float by in ten minutes.  It had the big timber

+of a regular island; it might be five or six miles long and more than

+half a mile wide.

+

+I kept quiet, with my ears cocked, about fifteen minutes, I reckon.  I

+was floating along, of course, four or five miles an hour; but you don't

+ever think of that.  No, you feel like you are laying dead still on

+the water; and if a little glimpse of a snag slips by you don't think to

+yourself how fast you're going, but you catch your breath and think,

+my! how that snag's tearing along.  If you think it ain't dismal and

+lonesome out in a fog that way by yourself in the night, you try it

+once—you'll see.

+

+Next, for about a half an hour, I whoops now and then; at last I hears

+the answer a long ways off, and tries to follow it, but I couldn't do

+it, and directly I judged I'd got into a nest of towheads, for I had

+little dim glimpses of them on both sides of me—sometimes just a narrow

+channel between, and some that I couldn't see I knowed was there because

+I'd hear the wash of the current against the old dead brush and trash

+that hung over the banks.  Well, I warn't long loosing the whoops down

+amongst the towheads; and I only tried to chase them a little while,

+anyway, because it was worse than chasing a Jack-o'-lantern.  You never

+knowed a sound dodge around so, and swap places so quick and so much.

+

+I had to claw away from the bank pretty lively four or five times, to

+keep from knocking the islands out of the river; and so I judged the

+raft must be butting into the bank every now and then, or else it would

+get further ahead and clear out of hearing—it was floating a little

+faster than what I was.

+

+Well, I seemed to be in the open river again by and by, but I couldn't

+hear no sign of a whoop nowheres.  I reckoned Jim had fetched up on a

+snag, maybe, and it was all up with him.  I was good and tired, so I

+laid down in the canoe and said I wouldn't bother no more.  I didn't

+want to go to sleep, of course; but I was so sleepy I couldn't help it;

+so I thought I would take jest one little cat-nap.

+

+But I reckon it was more than a cat-nap, for when I waked up the stars

+was shining bright, the fog was all gone, and I was spinning down a

+big bend stern first.  First I didn't know where I was; I thought I was

+dreaming; and when things began to come back to me they seemed to come

+up dim out of last week.

+

+It was a monstrous big river here, with the tallest and the thickest

+kind of timber on both banks; just a solid wall, as well as I could see

+by the stars.  I looked away down-stream, and seen a black speck on the

+water. I took after it; but when I got to it it warn't nothing but a

+couple of sawlogs made fast together.  Then I see another speck, and

+chased that; then another, and this time I was right.  It was the raft.

+

+When I got to it Jim was setting there with his head down between his

+knees, asleep, with his right arm hanging over the steering-oar.  The

+other oar was smashed off, and the raft was littered up with leaves and

+branches and dirt.  So she'd had a rough time.

+

+I made fast and laid down under Jim's nose on the raft, and began to

+gap, and stretch my fists out against Jim, and says:

+

+"Hello, Jim, have I been asleep?  Why didn't you stir me up?"

+

+"Goodness gracious, is dat you, Huck?  En you ain' dead—you ain'

+drownded—you's back agin?  It's too good for true, honey, it's too good

+for true. Lemme look at you chile, lemme feel o' you.  No, you ain'

+dead! you's back agin, 'live en soun', jis de same ole Huck—de same ole

+Huck, thanks to goodness!"

+

+"What's the matter with you, Jim?  You been a-drinking?"

+

+"Drinkin'?  Has I ben a-drinkin'?  Has I had a chance to be a-drinkin'?"

+

+"Well, then, what makes you talk so wild?"

+

+"How does I talk wild?"

+

+"How?  Why, hain't you been talking about my coming back, and all that

+stuff, as if I'd been gone away?"

+

+"Huck—Huck Finn, you look me in de eye; look me in de eye.  Hain't you

+ben gone away?"

+

+"Gone away?  Why, what in the nation do you mean?  I hain't been gone

+anywheres.  Where would I go to?"

+

+"Well, looky here, boss, dey's sumf'n wrong, dey is.  Is I me, or who

+is I? Is I heah, or whah is I?  Now dat's what I wants to know."

+

+"Well, I think you're here, plain enough, but I think you're a

+tangle-headed old fool, Jim."

+

+"I is, is I?  Well, you answer me dis:  Didn't you tote out de line in

+de canoe fer to make fas' to de tow-head?"

+

+"No, I didn't.  What tow-head?  I hain't see no tow-head."

+

+"You hain't seen no towhead?  Looky here, didn't de line pull loose en

+de raf' go a-hummin' down de river, en leave you en de canoe behine in

+de fog?"

+

+"What fog?"

+

+"Why, de fog!—de fog dat's been aroun' all night.  En didn't you whoop,

+en didn't I whoop, tell we got mix' up in de islands en one un us got

+los' en t'other one was jis' as good as los', 'kase he didn' know whah

+he wuz? En didn't I bust up agin a lot er dem islands en have a turrible

+time en mos' git drownded?  Now ain' dat so, boss—ain't it so?  You

+answer me dat."

+

+"Well, this is too many for me, Jim.  I hain't seen no fog, nor no

+islands, nor no troubles, nor nothing.  I been setting here talking with

+you all night till you went to sleep about ten minutes ago, and I reckon

+I done the same.  You couldn't a got drunk in that time, so of course

+you've been dreaming."

+

+"Dad fetch it, how is I gwyne to dream all dat in ten minutes?"

+

+"Well, hang it all, you did dream it, because there didn't any of it

+happen."

+

+"But, Huck, it's all jis' as plain to me as—"

+

+"It don't make no difference how plain it is; there ain't nothing in it.

+I know, because I've been here all the time."

+

+Jim didn't say nothing for about five minutes, but set there studying

+over it.  Then he says:

+

+"Well, den, I reck'n I did dream it, Huck; but dog my cats ef it ain't

+de powerfullest dream I ever see.  En I hain't ever had no dream b'fo'

+dat's tired me like dis one."

+

+"Oh, well, that's all right, because a dream does tire a body like

+everything sometimes.  But this one was a staving dream; tell me all

+about it, Jim."

+

+So Jim went to work and told me the whole thing right through, just as

+it happened, only he painted it up considerable.  Then he said he must

+start in and "'terpret" it, because it was sent for a warning.  He said

+the first towhead stood for a man that would try to do us some good, but

+the current was another man that would get us away from him.  The whoops

+was warnings that would come to us every now and then, and if we didn't

+try hard to make out to understand them they'd just take us into bad

+luck, 'stead of keeping us out of it.  The lot of towheads was troubles

+we was going to get into with quarrelsome people and all kinds of mean

+folks, but if we minded our business and didn't talk back and aggravate

+them, we would pull through and get out of the fog and into the big

+clear river, which was the free States, and wouldn't have no more

+trouble.

+

+It had clouded up pretty dark just after I got on to the raft, but it

+was clearing up again now.

+

+"Oh, well, that's all interpreted well enough as far as it goes, Jim," I

+says; "but what does these things stand for?"

+

+It was the leaves and rubbish on the raft and the smashed oar.  You

+could see them first-rate now.

+

+Jim looked at the trash, and then looked at me, and back at the trash

+again.  He had got the dream fixed so strong in his head that he

+couldn't seem to shake it loose and get the facts back into its place

+again right away.  But when he did get the thing straightened around he

+looked at me steady without ever smiling, and says:

+

+"What do dey stan' for?  I'se gwyne to tell you.  When I got all wore

+out wid work, en wid de callin' for you, en went to sleep, my heart wuz

+mos' broke bekase you wuz los', en I didn' k'yer no' mo' what become

+er me en de raf'.  En when I wake up en fine you back agin, all safe

+en soun', de tears come, en I could a got down on my knees en kiss yo'

+foot, I's so thankful. En all you wuz thinkin' 'bout wuz how you could

+make a fool uv ole Jim wid a lie.  Dat truck dah is trash; en trash

+is what people is dat puts dirt on de head er dey fren's en makes 'em

+ashamed."

+

+Then he got up slow and walked to the wigwam, and went in there without

+saying anything but that.  But that was enough.  It made me feel so mean

+I could almost kissed his foot to get him to take it back.

+

+It was fifteen minutes before I could work myself up to go and humble

+myself to a nigger; but I done it, and I warn't ever sorry for it

+afterwards, neither.  I didn't do him no more mean tricks, and I

+wouldn't done that one if I'd a knowed it would make him feel that way.

+

+

+

+

+CHAPTER XVI.

+

+WE slept most all day, and started out at night, a little ways behind a

+monstrous long raft that was as long going by as a procession.  She had

+four long sweeps at each end, so we judged she carried as many as thirty

+men, likely.  She had five big wigwams aboard, wide apart, and an open

+camp fire in the middle, and a tall flag-pole at each end.  There was a

+power of style about her.  It amounted to something being a raftsman

+on such a craft as that.

+

+We went drifting down into a big bend, and the night clouded up and got

+hot.  The river was very wide, and was walled with solid timber on

+both sides; you couldn't see a break in it hardly ever, or a light.  We

+talked about Cairo, and wondered whether we would know it when we got to

+it.  I said likely we wouldn't, because I had heard say there warn't but

+about a dozen houses there, and if they didn't happen to have them lit

+up, how was we going to know we was passing a town?  Jim said if the two

+big rivers joined together there, that would show.  But I said maybe

+we might think we was passing the foot of an island and coming into the

+same old river again. That disturbed Jim—and me too.  So the question

+was, what to do?  I said, paddle ashore the first time a light showed,

+and tell them pap was behind, coming along with a trading-scow, and

+was a green hand at the business, and wanted to know how far it was to

+Cairo.  Jim thought it was a good idea, so we took a smoke on it and

+waited.

+

+There warn't nothing to do now but to look out sharp for the town, and

+not pass it without seeing it.  He said he'd be mighty sure to see it,

+because he'd be a free man the minute he seen it, but if he missed it

+he'd be in a slave country again and no more show for freedom.  Every

+little while he jumps up and says:

+

+"Dah she is?"

+

+But it warn't.  It was Jack-o'-lanterns, or lightning bugs; so he set

+down again, and went to watching, same as before.  Jim said it made him

+all over trembly and feverish to be so close to freedom.  Well, I can

+tell you it made me all over trembly and feverish, too, to hear him,

+because I begun to get it through my head that he was most free—and

+who was to blame for it?  Why, me.  I couldn't get that out of my

+conscience, no how nor no way. It got to troubling me so I couldn't

+rest; I couldn't stay still in one place.  It hadn't ever come home to

+me before, what this thing was that I was doing.  But now it did; and it

+stayed with me, and scorched me more and more.  I tried to make out to

+myself that I warn't to blame, because I didn't run Jim off from his

+rightful owner; but it warn't no use, conscience up and says, every

+time, "But you knowed he was running for his freedom, and you could a

+paddled ashore and told somebody."  That was so—I couldn't get around

+that noway.  That was where it pinched.  Conscience says to me, "What

+had poor Miss Watson done to you that you could see her nigger go off

+right under your eyes and never say one single word?  What did that poor

+old woman do to you that you could treat her so mean?  Why, she tried to

+learn you your book, she tried to learn you your manners, she tried to

+be good to you every way she knowed how.  That's what she done."

+

+I got to feeling so mean and so miserable I most wished I was dead.  I

+fidgeted up and down the raft, abusing myself to myself, and Jim was

+fidgeting up and down past me.  We neither of us could keep still.

+ Every time he danced around and says, "Dah's Cairo!" it went through me

+like a shot, and I thought if it was Cairo I reckoned I would die of

+miserableness.

+

+Jim talked out loud all the time while I was talking to myself.  He was

+saying how the first thing he would do when he got to a free State he

+would go to saving up money and never spend a single cent, and when he

+got enough he would buy his wife, which was owned on a farm close to

+where Miss Watson lived; and then they would both work to buy the

+two children, and if their master wouldn't sell them, they'd get an

+Ab'litionist to go and steal them.

+

+It most froze me to hear such talk.  He wouldn't ever dared to talk such

+talk in his life before.  Just see what a difference it made in him the

+minute he judged he was about free.  It was according to the old saying,

+"Give a nigger an inch and he'll take an ell."  Thinks I, this is what

+comes of my not thinking.  Here was this nigger, which I had as good

+as helped to run away, coming right out flat-footed and saying he would

+steal his children—children that belonged to a man I didn't even know; a

+man that hadn't ever done me no harm.

+

+I was sorry to hear Jim say that, it was such a lowering of him.  My

+conscience got to stirring me up hotter than ever, until at last I says

+to it, "Let up on me—it ain't too late yet—I'll paddle ashore at the

+first light and tell."  I felt easy and happy and light as a feather

+right off.  All my troubles was gone.  I went to looking out sharp for a

+light, and sort of singing to myself.  By and by one showed.  Jim sings

+out:

+

+"We's safe, Huck, we's safe!  Jump up and crack yo' heels!  Dat's de

+good ole Cairo at las', I jis knows it!"

+

+I says:

+

+"I'll take the canoe and go and see, Jim.  It mightn't be, you know."

+

+He jumped and got the canoe ready, and put his old coat in the bottom

+for me to set on, and give me the paddle; and as I shoved off, he says:

+

+"Pooty soon I'll be a-shout'n' for joy, en I'll say, it's all on

+accounts o' Huck; I's a free man, en I couldn't ever ben free ef it

+hadn' ben for Huck; Huck done it.  Jim won't ever forgit you, Huck;

+you's de bes' fren' Jim's ever had; en you's de only fren' ole Jim's

+got now."

+

+I was paddling off, all in a sweat to tell on him; but when he says

+this, it seemed to kind of take the tuck all out of me.  I went along

+slow then, and I warn't right down certain whether I was glad I started

+or whether I warn't.  When I was fifty yards off, Jim says:

+

+"Dah you goes, de ole true Huck; de on'y white genlman dat ever kep' his

+promise to ole Jim."

+

+Well, I just felt sick.  But I says, I got to do it—I can't get out

+of it.  Right then along comes a skiff with two men in it with guns, and

+they stopped and I stopped.  One of them says:

+

+"What's that yonder?"

+

+"A piece of a raft," I says.

+

+"Do you belong on it?"

+

+"Yes, sir."

+

+"Any men on it?"

+

+"Only one, sir."

+

+"Well, there's five niggers run off to-night up yonder, above the head

+of the bend.  Is your man white or black?"

+

+I didn't answer up prompt.  I tried to, but the words wouldn't come. I

+tried for a second or two to brace up and out with it, but I warn't man

+enough—hadn't the spunk of a rabbit.  I see I was weakening; so I just

+give up trying, and up and says:

+

+"He's white."

+

+"I reckon we'll go and see for ourselves."

+

+"I wish you would," says I, "because it's pap that's there, and maybe

+you'd help me tow the raft ashore where the light is.  He's sick—and so

+is mam and Mary Ann."

+

+"Oh, the devil! we're in a hurry, boy.  But I s'pose we've got to.

+ Come, buckle to your paddle, and let's get along."

+

+I buckled to my paddle and they laid to their oars.  When we had made a

+stroke or two, I says:

+

+"Pap'll be mighty much obleeged to you, I can tell you.  Everybody goes

+away when I want them to help me tow the raft ashore, and I can't do it

+by myself."

+

+"Well, that's infernal mean.  Odd, too.  Say, boy, what's the matter

+with your father?"

+

+"It's the—a—the—well, it ain't anything much."

+

+They stopped pulling.  It warn't but a mighty little ways to the raft

+now. One says:

+

+"Boy, that's a lie.  What is the matter with your pap?  Answer up

+square now, and it'll be the better for you."

+

+"I will, sir, I will, honest—but don't leave us, please.  It's

+the—the—Gentlemen, if you'll only pull ahead, and let me heave you the

+headline, you won't have to come a-near the raft—please do."

+

+"Set her back, John, set her back!" says one.  They backed water.  "Keep

+away, boy—keep to looard.  Confound it, I just expect the wind has

+blowed it to us.  Your pap's got the small-pox, and you know it precious

+well.  Why didn't you come out and say so?  Do you want to spread it all

+over?"

+

+"Well," says I, a-blubbering, "I've told everybody before, and they just

+went away and left us."

+

+"Poor devil, there's something in that.  We are right down sorry for

+you, but we—well, hang it, we don't want the small-pox, you see.  Look

+here, I'll tell you what to do.  Don't you try to land by yourself, or

+you'll smash everything to pieces.  You float along down about twenty

+miles, and you'll come to a town on the left-hand side of the river.  It

+will be long after sun-up then, and when you ask for help you tell them

+your folks are all down with chills and fever.  Don't be a fool again,

+and let people guess what is the matter.  Now we're trying to do you a

+kindness; so you just put twenty miles between us, that's a good boy.

+ It wouldn't do any good to land yonder where the light is—it's only a

+wood-yard. Say, I reckon your father's poor, and I'm bound to say he's

+in pretty hard luck.  Here, I'll put a twenty-dollar gold piece on this

+board, and you get it when it floats by.  I feel mighty mean to leave

+you; but my kingdom! it won't do to fool with small-pox, don't you see?"

+

+"Hold on, Parker," says the other man, "here's a twenty to put on the

+board for me.  Good-bye, boy; you do as Mr. Parker told you, and you'll

+be all right."

+

+"That's so, my boy—good-bye, good-bye.  If you see any runaway niggers

+you get help and nab them, and you can make some money by it."

+

+"Good-bye, sir," says I; "I won't let no runaway niggers get by me if I

+can help it."

+

+They went off and I got aboard the raft, feeling bad and low, because I

+knowed very well I had done wrong, and I see it warn't no use for me

+to try to learn to do right; a body that don't get started right when

+he's little ain't got no show—when the pinch comes there ain't nothing

+to back him up and keep him to his work, and so he gets beat.  Then I

+thought a minute, and says to myself, hold on; s'pose you'd a done right

+and give Jim up, would you felt better than what you do now?  No, says

+I, I'd feel bad—I'd feel just the same way I do now.  Well, then, says

+I, what's the use you learning to do right when it's troublesome to do

+right and ain't no trouble to do wrong, and the wages is just the same?

+ I was stuck.  I couldn't answer that.  So I reckoned I wouldn't bother

+no more about it, but after this always do whichever come handiest at

+the time.

+

+I went into the wigwam; Jim warn't there.  I looked all around; he

+warn't anywhere.  I says:

+

+"Jim!"

+

+"Here I is, Huck.  Is dey out o' sight yit?  Don't talk loud."

+

+He was in the river under the stern oar, with just his nose out.  I told

+him they were out of sight, so he come aboard.  He says:

+

+"I was a-listenin' to all de talk, en I slips into de river en was gwyne

+to shove for sho' if dey come aboard.  Den I was gwyne to swim to de

+raf' agin when dey was gone.  But lawsy, how you did fool 'em, Huck!

+ Dat wuz de smartes' dodge!  I tell you, chile, I'spec it save' ole

+Jim—ole Jim ain't going to forgit you for dat, honey."

+

+Then we talked about the money.  It was a pretty good raise—twenty

+dollars apiece.  Jim said we could take deck passage on a steamboat

+now, and the money would last us as far as we wanted to go in the free

+States. He said twenty mile more warn't far for the raft to go, but he

+wished we was already there.

+

+Towards daybreak we tied up, and Jim was mighty particular about hiding

+the raft good.  Then he worked all day fixing things in bundles, and

+getting all ready to quit rafting.

+

+That night about ten we hove in sight of the lights of a town away down

+in a left-hand bend.

+

+I went off in the canoe to ask about it.  Pretty soon I found a man out

+in the river with a skiff, setting a trot-line.  I ranged up and says:

+

+"Mister, is that town Cairo?"

+

+"Cairo? no.  You must be a blame' fool."

+

+"What town is it, mister?"

+

+"If you want to know, go and find out.  If you stay here botherin'

+around me for about a half a minute longer you'll get something you

+won't want."

+

+I paddled to the raft.  Jim was awful disappointed, but I said never

+mind, Cairo would be the next place, I reckoned.

+

+We passed another town before daylight, and I was going out again; but

+it was high ground, so I didn't go.  No high ground about Cairo, Jim

+said. I had forgot it.  We laid up for the day on a towhead tolerable

+close to the left-hand bank.  I begun to suspicion something.  So did

+Jim.  I says:

+

+"Maybe we went by Cairo in the fog that night."

+

+He says:

+

+"Doan' le's talk about it, Huck.  Po' niggers can't have no luck.  I

+awluz 'spected dat rattlesnake-skin warn't done wid its work."

+

+"I wish I'd never seen that snake-skin, Jim—I do wish I'd never laid

+eyes on it."

+

+"It ain't yo' fault, Huck; you didn' know.  Don't you blame yo'self

+'bout it."

+

+When it was daylight, here was the clear Ohio water inshore, sure

+enough, and outside was the old regular Muddy!  So it was all up with

+Cairo.

+

+We talked it all over.  It wouldn't do to take to the shore; we couldn't

+take the raft up the stream, of course.  There warn't no way but to wait

+for dark, and start back in the canoe and take the chances.  So we slept

+all day amongst the cottonwood thicket, so as to be fresh for the work,

+and when we went back to the raft about dark the canoe was gone!

+

+We didn't say a word for a good while.  There warn't anything to

+say.  We both knowed well enough it was some more work of the

+rattlesnake-skin; so what was the use to talk about it?  It would only

+look like we was finding fault, and that would be bound to fetch more

+bad luck—and keep on fetching it, too, till we knowed enough to keep

+still.

+

+By and by we talked about what we better do, and found there warn't no

+way but just to go along down with the raft till we got a chance to buy

+a canoe to go back in.  We warn't going to borrow it when there warn't

+anybody around, the way pap would do, for that might set people after

+us.

+

+So we shoved out after dark on the raft.

+

+Anybody that don't believe yet that it's foolishness to handle a

+snake-skin, after all that that snake-skin done for us, will believe it

+now if they read on and see what more it done for us.

+

+The place to buy canoes is off of rafts laying up at shore.  But we

+didn't see no rafts laying up; so we went along during three hours and

+more.  Well, the night got gray and ruther thick, which is the next

+meanest thing to fog.  You can't tell the shape of the river, and you

+can't see no distance. It got to be very late and still, and then along

+comes a steamboat up the river.  We lit the lantern, and judged she

+would see it.  Up-stream boats didn't generly come close to us; they

+go out and follow the bars and hunt for easy water under the reefs; but

+nights like this they bull right up the channel against the whole river.

+

+We could hear her pounding along, but we didn't see her good till she

+was close.  She aimed right for us.  Often they do that and try to see

+how close they can come without touching; sometimes the wheel bites off

+a sweep, and then the pilot sticks his head out and laughs, and thinks

+he's mighty smart.  Well, here she comes, and we said she was going to

+try and shave us; but she didn't seem to be sheering off a bit.  She

+was a big one, and she was coming in a hurry, too, looking like a black

+cloud with rows of glow-worms around it; but all of a sudden she bulged

+out, big and scary, with a long row of wide-open furnace doors shining

+like red-hot teeth, and her monstrous bows and guards hanging right

+over us.  There was a yell at us, and a jingling of bells to stop the

+engines, a powwow of cussing, and whistling of steam—and as Jim went

+overboard on one side and I on the other, she come smashing straight

+through the raft.

+

+I dived—and I aimed to find the bottom, too, for a thirty-foot wheel

+had got to go over me, and I wanted it to have plenty of room.  I could

+always stay under water a minute; this time I reckon I stayed under a

+minute and a half.  Then I bounced for the top in a hurry, for I was

+nearly busting.  I popped out to my armpits and blowed the water out of

+my nose, and puffed a bit.  Of course there was a booming current; and

+of course that boat started her engines again ten seconds after she

+stopped them, for they never cared much for raftsmen; so now she was

+churning along up the river, out of sight in the thick weather, though I

+could hear her.

+

+I sung out for Jim about a dozen times, but I didn't get any answer;

+so I grabbed a plank that touched me while I was "treading water," and

+struck out for shore, shoving it ahead of me.  But I made out to see

+that the drift of the current was towards the left-hand shore, which

+meant that I was in a crossing; so I changed off and went that way.

+

+It was one of these long, slanting, two-mile crossings; so I was a good

+long time in getting over.  I made a safe landing, and clumb up the

+bank. I couldn't see but a little ways, but I went poking along over

+rough ground for a quarter of a mile or more, and then I run across a

+big old-fashioned double log-house before I noticed it.  I was going to

+rush by and get away, but a lot of dogs jumped out and went to howling

+and barking at me, and I knowed better than to move another peg.

+

+

+

+

+CHAPTER XVII.

+

+IN about a minute somebody spoke out of a window without putting his

+head out, and says:

+

+"Be done, boys!  Who's there?"

+

+I says:

+

+"It's me."

+

+"Who's me?"

+

+"George Jackson, sir."

+

+"What do you want?"

+

+"I don't want nothing, sir.  I only want to go along by, but the dogs

+won't let me."

+

+"What are you prowling around here this time of night for—hey?"

+

+"I warn't prowling around, sir, I fell overboard off of the steamboat."

+

+"Oh, you did, did you?  Strike a light there, somebody.  What did you

+say your name was?"

+

+"George Jackson, sir.  I'm only a boy."

+

+"Look here, if you're telling the truth you needn't be afraid—nobody'll

+hurt you.  But don't try to budge; stand right where you are.  Rouse out

+Bob and Tom, some of you, and fetch the guns.  George Jackson, is there

+anybody with you?"

+

+"No, sir, nobody."

+

+I heard the people stirring around in the house now, and see a light.

+The man sung out:

+

+"Snatch that light away, Betsy, you old fool—ain't you got any sense?

+Put it on the floor behind the front door.  Bob, if you and Tom are

+ready, take your places."

+

+"All ready."

+

+"Now, George Jackson, do you know the Shepherdsons?"

+

+"No, sir; I never heard of them."

+

+"Well, that may be so, and it mayn't.  Now, all ready.  Step forward,

+George Jackson.  And mind, don't you hurry—come mighty slow.  If there's

+anybody with you, let him keep back—if he shows himself he'll be shot.

+Come along now.  Come slow; push the door open yourself—just enough to

+squeeze in, d' you hear?"

+

+I didn't hurry; I couldn't if I'd a wanted to.  I took one slow step at

+a time and there warn't a sound, only I thought I could hear my heart.

+ The dogs were as still as the humans, but they followed a little behind

+me. When I got to the three log doorsteps I heard them unlocking and

+unbarring and unbolting.  I put my hand on the door and pushed it a

+little and a little more till somebody said, "There, that's enough—put

+your head in." I done it, but I judged they would take it off.

+

+The candle was on the floor, and there they all was, looking at me, and

+me at them, for about a quarter of a minute:  Three big men with guns

+pointed at me, which made me wince, I tell you; the oldest, gray

+and about sixty, the other two thirty or more—all of them fine and

+handsome—and the sweetest old gray-headed lady, and back of her two

+young women which I couldn't see right well.  The old gentleman says:

+

+"There; I reckon it's all right.  Come in."

+

+As soon as I was in the old gentleman he locked the door and barred it

+and bolted it, and told the young men to come in with their guns, and

+they all went in a big parlor that had a new rag carpet on the floor,

+and got together in a corner that was out of the range of the front

+windows—there warn't none on the side.  They held the candle, and took a

+good look at me, and all said, "Why, he ain't a Shepherdson—no, there

+ain't any Shepherdson about him."  Then the old man said he hoped I

+wouldn't mind being searched for arms, because he didn't mean no harm by

+it—it was only to make sure.  So he didn't pry into my pockets, but only

+felt outside with his hands, and said it was all right.  He told me to

+make myself easy and at home, and tell all about myself; but the old

+lady says:

+

+"Why, bless you, Saul, the poor thing's as wet as he can be; and don't

+you reckon it may be he's hungry?"

+

+"True for you, Rachel—I forgot."

+

+So the old lady says:

+

+"Betsy" (this was a nigger woman), "you fly around and get him something

+to eat as quick as you can, poor thing; and one of you girls go and wake

+up Buck and tell him—oh, here he is himself.  Buck, take this little

+stranger and get the wet clothes off from him and dress him up in some

+of yours that's dry."

+

+Buck looked about as old as me—thirteen or fourteen or along there,

+though he was a little bigger than me.  He hadn't on anything but a

+shirt, and he was very frowzy-headed.  He came in gaping and digging one

+fist into his eyes, and he was dragging a gun along with the other one.

+He says:

+

+"Ain't they no Shepherdsons around?"

+

+They said, no, 'twas a false alarm.

+

+"Well," he says, "if they'd a ben some, I reckon I'd a got one."

+

+They all laughed, and Bob says:

+

+"Why, Buck, they might have scalped us all, you've been so slow in

+coming."

+

+"Well, nobody come after me, and it ain't right I'm always kept down; I

+don't get no show."

+

+"Never mind, Buck, my boy," says the old man, "you'll have show enough,

+all in good time, don't you fret about that.  Go 'long with you now, and

+do as your mother told you."

+

+When we got up-stairs to his room he got me a coarse shirt and a

+roundabout and pants of his, and I put them on.  While I was at it he

+asked me what my name was, but before I could tell him he started to

+tell me about a bluejay and a young rabbit he had catched in the woods

+day before yesterday, and he asked me where Moses was when the candle

+went out.  I said I didn't know; I hadn't heard about it before, no way.

+

+"Well, guess," he says.

+

+"How'm I going to guess," says I, "when I never heard tell of it

+before?"

+

+"But you can guess, can't you?  It's just as easy."

+

+"Which candle?"  I says.

+

+"Why, any candle," he says.

+

+"I don't know where he was," says I; "where was he?"

+

+"Why, he was in the dark!  That's where he was!"

+

+"Well, if you knowed where he was, what did you ask me for?"

+

+"Why, blame it, it's a riddle, don't you see?  Say, how long are you

+going to stay here?  You got to stay always.  We can just have booming

+times—they don't have no school now.  Do you own a dog?  I've got a

+dog—and he'll go in the river and bring out chips that you throw in.  Do

+you like to comb up Sundays, and all that kind of foolishness?  You bet

+I don't, but ma she makes me.  Confound these ole britches!  I reckon

+I'd better put 'em on, but I'd ruther not, it's so warm.  Are you all

+ready? All right.  Come along, old hoss."

+

+Cold corn-pone, cold corn-beef, butter and buttermilk—that is what they

+had for me down there, and there ain't nothing better that ever I've

+come across yet.  Buck and his ma and all of them smoked cob pipes,

+except the nigger woman, which was gone, and the two young women.  They

+all smoked and talked, and I eat and talked.  The young women had

+quilts around them, and their hair down their backs.  They all asked me

+questions, and I told them how pap and me and all the family was living

+on a little farm down at the bottom of Arkansaw, and my sister Mary Ann

+run off and got married and never was heard of no more, and Bill went

+to hunt them and he warn't heard of no more, and Tom and Mort died,

+and then there warn't nobody but just me and pap left, and he was just

+trimmed down to nothing, on account of his troubles; so when he died

+I took what there was left, because the farm didn't belong to us, and

+started up the river, deck passage, and fell overboard; and that was how

+I come to be here.  So they said I could have a home there as long as I

+wanted it.  Then it was most daylight and everybody went to bed, and I

+went to bed with Buck, and when I waked up in the morning, drat it all,

+I had forgot what my name was. So I laid there about an hour trying to

+think, and when Buck waked up I says:

+

+"Can you spell, Buck?"

+

+"Yes," he says.

+

+"I bet you can't spell my name," says I.

+

+"I bet you what you dare I can," says he.

+

+"All right," says I, "go ahead."

+

+"G-e-o-r-g-e J-a-x-o-n—there now," he says.

+

+"Well," says I, "you done it, but I didn't think you could.  It ain't no

+slouch of a name to spell—right off without studying."

+

+I set it down, private, because somebody might want me to spell it

+next, and so I wanted to be handy with it and rattle it off like I was

+used to it.

+

+It was a mighty nice family, and a mighty nice house, too.  I hadn't

+seen no house out in the country before that was so nice and had so much

+style.  It didn't have an iron latch on the front door, nor a wooden one

+with a buckskin string, but a brass knob to turn, the same as houses in

+town. There warn't no bed in the parlor, nor a sign of a bed; but heaps

+of parlors in towns has beds in them.  There was a big fireplace that

+was bricked on the bottom, and the bricks was kept clean and red by

+pouring water on them and scrubbing them with another brick; sometimes

+they wash them over with red water-paint that they call Spanish-brown,

+same as they do in town.  They had big brass dog-irons that could hold

+up a saw-log. There was a clock on the middle of the mantelpiece, with

+a picture of a town painted on the bottom half of the glass front, and

+a round place in the middle of it for the sun, and you could see the

+pendulum swinging behind it.  It was beautiful to hear that clock tick;

+and sometimes when one of these peddlers had been along and scoured her

+up and got her in good shape, she would start in and strike a hundred

+and fifty before she got tuckered out.  They wouldn't took any money for

+her.

+

+Well, there was a big outlandish parrot on each side of the clock,

+made out of something like chalk, and painted up gaudy.  By one of the

+parrots was a cat made of crockery, and a crockery dog by the other;

+and when you pressed down on them they squeaked, but didn't open

+their mouths nor look different nor interested.  They squeaked through

+underneath.  There was a couple of big wild-turkey-wing fans spread out

+behind those things.  On the table in the middle of the room was a kind

+of a lovely crockery basket that had apples and oranges and peaches and

+grapes piled up in it, which was much redder and yellower and prettier

+than real ones is, but they warn't real because you could see where

+pieces had got chipped off and showed the white chalk, or whatever it

+was, underneath.

+

+This table had a cover made out of beautiful oilcloth, with a red and

+blue spread-eagle painted on it, and a painted border all around.  It

+come all the way from Philadelphia, they said.  There was some books,

+too, piled up perfectly exact, on each corner of the table.  One was a

+big family Bible full of pictures.  One was Pilgrim's Progress, about a

+man that left his family, it didn't say why.  I read considerable in it

+now and then.  The statements was interesting, but tough.  Another was

+Friendship's Offering, full of beautiful stuff and poetry; but I didn't

+read the poetry.  Another was Henry Clay's Speeches, and another was Dr.

+Gunn's Family Medicine, which told you all about what to do if a body

+was sick or dead.  There was a hymn book, and a lot of other books.  And

+there was nice split-bottom chairs, and perfectly sound, too—not bagged

+down in the middle and busted, like an old basket.

+

+They had pictures hung on the walls—mainly Washingtons and Lafayettes,

+and battles, and Highland Marys, and one called "Signing the

+Declaration." There was some that they called crayons, which one of the

+daughters which was dead made her own self when she was only

+fifteen years old.  They was different from any pictures I ever see

+before—blacker, mostly, than is common.  One was a woman in a slim black

+dress, belted small under the armpits, with bulges like a cabbage in

+the middle of the sleeves, and a large black scoop-shovel bonnet with

+a black veil, and white slim ankles crossed about with black tape, and

+very wee black slippers, like a chisel, and she was leaning pensive on a

+tombstone on her right elbow, under a weeping willow, and her other hand

+hanging down her side holding a white handkerchief and a reticule,

+and underneath the picture it said "Shall I Never See Thee More Alas."

+ Another one was a young lady with her hair all combed up straight

+to the top of her head, and knotted there in front of a comb like a

+chair-back, and she was crying into a handkerchief and had a dead bird

+laying on its back in her other hand with its heels up, and underneath

+the picture it said "I Shall Never Hear Thy Sweet Chirrup More Alas."

+ There was one where a young lady was at a window looking up at the

+moon, and tears running down her cheeks; and she had an open letter in

+one hand with black sealing wax showing on one edge of it, and she was

+mashing a locket with a chain to it against her mouth, and underneath

+the picture it said "And Art Thou Gone Yes Thou Art Gone Alas."  These

+was all nice pictures, I reckon, but I didn't somehow seem to take

+to them, because if ever I was down a little they always give me the

+fan-tods.  Everybody was sorry she died, because she had laid out a lot

+more of these pictures to do, and a body could see by what she had done

+what they had lost.  But I reckoned that with her disposition she was

+having a better time in the graveyard.  She was at work on what they

+said was her greatest picture when she took sick, and every day and

+every night it was her prayer to be allowed to live till she got it

+done, but she never got the chance.  It was a picture of a young woman

+in a long white gown, standing on the rail of a bridge all ready to jump

+off, with her hair all down her back, and looking up to the moon, with

+the tears running down her face, and she had two arms folded across her

+breast, and two arms stretched out in front, and two more reaching up

+towards the moon—and the idea was to see which pair would look best,

+and then scratch out all the other arms; but, as I was saying, she died

+before she got her mind made up, and now they kept this picture over the

+head of the bed in her room, and every time her birthday come they hung

+flowers on it.  Other times it was hid with a little curtain.  The young

+woman in the picture had a kind of a nice sweet face, but there was so

+many arms it made her look too spidery, seemed to me.

+

+This young girl kept a scrap-book when she was alive, and used to paste

+obituaries and accidents and cases of patient suffering in it out of the

+Presbyterian Observer, and write poetry after them out of her own head.

+It was very good poetry. This is what she wrote about a boy by the name

+of Stephen Dowling Bots that fell down a well and was drownded:

+

+ODE TO STEPHEN DOWLING BOTS, DEC'D

+

+And did young Stephen sicken,    And did young Stephen die? And did the

+sad hearts thicken,    And did the mourners cry?

+

+No; such was not the fate of    Young Stephen Dowling Bots; Though sad

+hearts round him thickened,    'Twas not from sickness' shots.

+

+No whooping-cough did rack his frame,    Nor measles drear with spots;

+Not these impaired the sacred name    Of Stephen Dowling Bots.

+

+Despised love struck not with woe    That head of curly knots, Nor

+stomach troubles laid him low,    Young Stephen Dowling Bots.

+

+O no. Then list with tearful eye,    Whilst I his fate do tell. His soul

+did from this cold world fly    By falling down a well.

+

+They got him out and emptied him;    Alas it was too late; His spirit

+was gone for to sport aloft    In the realms of the good and great.

+

+If Emmeline Grangerford could make poetry like that before she was

+fourteen, there ain't no telling what she could a done by and by.  Buck

+said she could rattle off poetry like nothing.  She didn't ever have to

+stop to think.  He said she would slap down a line, and if she couldn't

+find anything to rhyme with it would just scratch it out and slap down

+another one, and go ahead. She warn't particular; she could write about

+anything you choose to give her to write about just so it was sadful.

+Every time a man died, or a woman died, or a child died, she would be on

+hand with her "tribute" before he was cold.  She called them tributes.

+The neighbors said it was the doctor first, then Emmeline, then the

+undertaker—the undertaker never got in ahead of Emmeline but once, and

+then she hung fire on a rhyme for the dead person's name, which was

+Whistler.  She warn't ever the same after that; she never complained,

+but she kinder pined away and did not live long.  Poor thing, many's the

+time I made myself go up to the little room that used to be hers and get

+out her poor old scrap-book and read in it when her pictures had been

+aggravating me and I had soured on her a little.  I liked all that

+family, dead ones and all, and warn't going to let anything come between

+us.  Poor Emmeline made poetry about all the dead people when she was

+alive, and it didn't seem right that there warn't nobody to make some

+about her now she was gone; so I tried to sweat out a verse or two

+myself, but I couldn't seem to make it go somehow.  They kept Emmeline's

+room trim and nice, and all the things fixed in it just the way she

+liked to have them when she was alive, and nobody ever slept there.

+ The old lady took care of the room herself, though there was plenty

+of niggers, and she sewed there a good deal and read her Bible there

+mostly.

+

+Well, as I was saying about the parlor, there was beautiful curtains on

+the windows:  white, with pictures painted on them of castles with vines

+all down the walls, and cattle coming down to drink.  There was a little

+old piano, too, that had tin pans in it, I reckon, and nothing was ever

+so lovely as to hear the young ladies sing "The Last Link is Broken"

+and play "The Battle of Prague" on it.  The walls of all the rooms was

+plastered, and most had carpets on the floors, and the whole house was

+whitewashed on the outside.

+

+It was a double house, and the big open place betwixt them was roofed

+and floored, and sometimes the table was set there in the middle of the

+day, and it was a cool, comfortable place.  Nothing couldn't be better.

+ And warn't the cooking good, and just bushels of it too!

+

+

+

+

+CHAPTER XVIII.

+

+COL.  Grangerford was a gentleman, you see.  He was a gentleman all

+over; and so was his family.  He was well born, as the saying is, and

+that's worth as much in a man as it is in a horse, so the Widow Douglas

+said, and nobody ever denied that she was of the first aristocracy

+in our town; and pap he always said it, too, though he warn't no more

+quality than a mudcat himself.  Col.  Grangerford was very tall and

+very slim, and had a darkish-paly complexion, not a sign of red in it

+anywheres; he was clean shaved every morning all over his thin face, and

+he had the thinnest kind of lips, and the thinnest kind of nostrils, and

+a high nose, and heavy eyebrows, and the blackest kind of eyes, sunk so

+deep back that they seemed like they was looking out of caverns at

+you, as you may say.  His forehead was high, and his hair was black and

+straight and hung to his shoulders. His hands was long and thin, and

+every day of his life he put on a clean shirt and a full suit from head

+to foot made out of linen so white it hurt your eyes to look at it;

+and on Sundays he wore a blue tail-coat with brass buttons on it.  He

+carried a mahogany cane with a silver head to it.  There warn't no

+frivolishness about him, not a bit, and he warn't ever loud.  He was

+as kind as he could be—you could feel that, you know, and so you had

+confidence.  Sometimes he smiled, and it was good to see; but when he

+straightened himself up like a liberty-pole, and the lightning begun to

+flicker out from under his eyebrows, you wanted to climb a tree first,

+and find out what the matter was afterwards.  He didn't ever have to

+tell anybody to mind their manners—everybody was always good-mannered

+where he was.  Everybody loved to have him around, too; he was sunshine

+most always—I mean he made it seem like good weather.  When he turned

+into a cloudbank it was awful dark for half a minute, and that was

+enough; there wouldn't nothing go wrong again for a week.

+

+When him and the old lady come down in the morning all the family got

+up out of their chairs and give them good-day, and didn't set down again

+till they had set down.  Then Tom and Bob went to the sideboard where

+the decanter was, and mixed a glass of bitters and handed it to him, and

+he held it in his hand and waited till Tom's and Bob's was mixed, and

+then they bowed and said, "Our duty to you, sir, and madam;" and they

+bowed the least bit in the world and said thank you, and so they drank,

+all three, and Bob and Tom poured a spoonful of water on the sugar and

+the mite of whisky or apple brandy in the bottom of their tumblers, and

+give it to me and Buck, and we drank to the old people too.

+

+Bob was the oldest and Tom next—tall, beautiful men with very broad

+shoulders and brown faces, and long black hair and black eyes.  They

+dressed in white linen from head to foot, like the old gentleman, and

+wore broad Panama hats.

+

+Then there was Miss Charlotte; she was twenty-five, and tall and proud

+and grand, but as good as she could be when she warn't stirred up; but

+when she was she had a look that would make you wilt in your tracks,

+like her father.  She was beautiful.

+

+So was her sister, Miss Sophia, but it was a different kind.  She was

+gentle and sweet like a dove, and she was only twenty.

+

+Each person had their own nigger to wait on them—Buck too.  My nigger

+had a monstrous easy time, because I warn't used to having anybody do

+anything for me, but Buck's was on the jump most of the time.

+

+This was all there was of the family now, but there used to be

+more—three sons; they got killed; and Emmeline that died.

+

+The old gentleman owned a lot of farms and over a hundred niggers.

+Sometimes a stack of people would come there, horseback, from ten or

+fifteen mile around, and stay five or six days, and have such junketings

+round about and on the river, and dances and picnics in the woods

+daytimes, and balls at the house nights.  These people was mostly

+kinfolks of the family.  The men brought their guns with them.  It was a

+handsome lot of quality, I tell you.

+

+There was another clan of aristocracy around there—five or six

+families—mostly of the name of Shepherdson.  They was as high-toned

+and well born and rich and grand as the tribe of Grangerfords.  The

+Shepherdsons and Grangerfords used the same steamboat landing, which was

+about two mile above our house; so sometimes when I went up there with a

+lot of our folks I used to see a lot of the Shepherdsons there on their

+fine horses.

+

+One day Buck and me was away out in the woods hunting, and heard a horse

+coming.  We was crossing the road.  Buck says:

+

+"Quick!  Jump for the woods!"

+

+We done it, and then peeped down the woods through the leaves.  Pretty

+soon a splendid young man come galloping down the road, setting his

+horse easy and looking like a soldier.  He had his gun across his

+pommel.  I had seen him before.  It was young Harney Shepherdson.  I

+heard Buck's gun go off at my ear, and Harney's hat tumbled off from his

+head.  He grabbed his gun and rode straight to the place where we was

+hid.  But we didn't wait.  We started through the woods on a run.  The

+woods warn't thick, so I looked over my shoulder to dodge the bullet,

+and twice I seen Harney cover Buck with his gun; and then he rode away

+the way he come—to get his hat, I reckon, but I couldn't see.  We never

+stopped running till we got home.  The old gentleman's eyes blazed a

+minute—'twas pleasure, mainly, I judged—then his face sort of smoothed

+down, and he says, kind of gentle:

+

+"I don't like that shooting from behind a bush.  Why didn't you step

+into the road, my boy?"

+

+"The Shepherdsons don't, father.  They always take advantage."

+

+Miss Charlotte she held her head up like a queen while Buck was telling

+his tale, and her nostrils spread and her eyes snapped.  The two young

+men looked dark, but never said nothing.  Miss Sophia she turned pale,

+but the color come back when she found the man warn't hurt.

+

+Soon as I could get Buck down by the corn-cribs under the trees by

+ourselves, I says:

+

+"Did you want to kill him, Buck?"

+

+"Well, I bet I did."

+

+"What did he do to you?"

+

+"Him?  He never done nothing to me."

+

+"Well, then, what did you want to kill him for?"

+

+"Why, nothing—only it's on account of the feud."

+

+"What's a feud?"

+

+"Why, where was you raised?  Don't you know what a feud is?"

+

+"Never heard of it before—tell me about it."

+

+"Well," says Buck, "a feud is this way:  A man has a quarrel with

+another man, and kills him; then that other man's brother kills him;

+then the other brothers, on both sides, goes for one another; then the

+cousins chip in—and by and by everybody's killed off, and there ain't

+no more feud.  But it's kind of slow, and takes a long time."

+

+"Has this one been going on long, Buck?"

+

+"Well, I should reckon!  It started thirty year ago, or som'ers along

+there.  There was trouble 'bout something, and then a lawsuit to settle

+it; and the suit went agin one of the men, and so he up and shot the

+man that won the suit—which he would naturally do, of course.  Anybody

+would."

+

+"What was the trouble about, Buck?—land?"

+

+"I reckon maybe—I don't know."

+

+"Well, who done the shooting?  Was it a Grangerford or a Shepherdson?"

+

+"Laws, how do I know?  It was so long ago."

+

+"Don't anybody know?"

+

+"Oh, yes, pa knows, I reckon, and some of the other old people; but they

+don't know now what the row was about in the first place."

+

+"Has there been many killed, Buck?"

+

+"Yes; right smart chance of funerals.  But they don't always kill.  Pa's

+got a few buckshot in him; but he don't mind it 'cuz he don't weigh

+much, anyway.  Bob's been carved up some with a bowie, and Tom's been

+hurt once or twice."

+

+"Has anybody been killed this year, Buck?"

+

+"Yes; we got one and they got one.  'Bout three months ago my cousin

+Bud, fourteen year old, was riding through the woods on t'other side

+of the river, and didn't have no weapon with him, which was blame'

+foolishness, and in a lonesome place he hears a horse a-coming behind

+him, and sees old Baldy Shepherdson a-linkin' after him with his gun in

+his hand and his white hair a-flying in the wind; and 'stead of jumping

+off and taking to the brush, Bud 'lowed he could out-run him; so they

+had it, nip and tuck, for five mile or more, the old man a-gaining all

+the time; so at last Bud seen it warn't any use, so he stopped and faced

+around so as to have the bullet holes in front, you know, and the old

+man he rode up and shot him down.  But he didn't git much chance to

+enjoy his luck, for inside of a week our folks laid him out."

+

+"I reckon that old man was a coward, Buck."

+

+"I reckon he warn't a coward.  Not by a blame' sight.  There ain't a

+coward amongst them Shepherdsons—not a one.  And there ain't no cowards

+amongst the Grangerfords either.  Why, that old man kep' up his end in a

+fight one day for half an hour against three Grangerfords, and come

+out winner.  They was all a-horseback; he lit off of his horse and got

+behind a little woodpile, and kep' his horse before him to stop the

+bullets; but the Grangerfords stayed on their horses and capered around

+the old man, and peppered away at him, and he peppered away at them.

+ Him and his horse both went home pretty leaky and crippled, but the

+Grangerfords had to be fetched home—and one of 'em was dead, and

+another died the next day.  No, sir; if a body's out hunting for cowards

+he don't want to fool away any time amongst them Shepherdsons, becuz

+they don't breed any of that kind."

+

+Next Sunday we all went to church, about three mile, everybody

+a-horseback. The men took their guns along, so did Buck, and kept

+them between their knees or stood them handy against the wall.  The

+Shepherdsons done the same.  It was pretty ornery preaching—all about

+brotherly love, and such-like tiresomeness; but everybody said it was

+a good sermon, and they all talked it over going home, and had such

+a powerful lot to say about faith and good works and free grace and

+preforeordestination, and I don't know what all, that it did seem to me

+to be one of the roughest Sundays I had run across yet.

+

+About an hour after dinner everybody was dozing around, some in their

+chairs and some in their rooms, and it got to be pretty dull.  Buck and

+a dog was stretched out on the grass in the sun sound asleep.  I went up

+to our room, and judged I would take a nap myself.  I found that sweet

+Miss Sophia standing in her door, which was next to ours, and she took

+me in her room and shut the door very soft, and asked me if I liked her,

+and I said I did; and she asked me if I would do something for her and

+not tell anybody, and I said I would.  Then she said she'd forgot her

+Testament, and left it in the seat at church between two other books,

+and would I slip out quiet and go there and fetch it to her, and not say

+nothing to nobody.  I said I would. So I slid out and slipped off up the

+road, and there warn't anybody at the church, except maybe a hog or two,

+for there warn't any lock on the door, and hogs likes a puncheon floor

+in summer-time because it's cool.  If you notice, most folks don't go to

+church only when they've got to; but a hog is different.

+

+Says I to myself, something's up; it ain't natural for a girl to be in

+such a sweat about a Testament.  So I give it a shake, and out drops a

+little piece of paper with "HALF-PAST TWO" wrote on it with a pencil.  I

+ransacked it, but couldn't find anything else.  I couldn't make anything

+out of that, so I put the paper in the book again, and when I got home

+and upstairs there was Miss Sophia in her door waiting for me.  She

+pulled me in and shut the door; then she looked in the Testament till

+she found the paper, and as soon as she read it she looked glad; and

+before a body could think she grabbed me and give me a squeeze, and

+said I was the best boy in the world, and not to tell anybody.  She was

+mighty red in the face for a minute, and her eyes lighted up, and it

+made her powerful pretty.  I was a good deal astonished, but when I got

+my breath I asked her what the paper was about, and she asked me if I

+had read it, and I said no, and she asked me if I could read writing,

+and I told her "no, only coarse-hand," and then she said the paper

+warn't anything but a book-mark to keep her place, and I might go and

+play now.

+

+I went off down to the river, studying over this thing, and pretty soon

+I noticed that my nigger was following along behind.  When we was out

+of sight of the house he looked back and around a second, and then comes

+a-running, and says:

+

+"Mars Jawge, if you'll come down into de swamp I'll show you a whole

+stack o' water-moccasins."

+

+Thinks I, that's mighty curious; he said that yesterday.  He oughter

+know a body don't love water-moccasins enough to go around hunting for

+them. What is he up to, anyway?  So I says:

+

+"All right; trot ahead."

+

+I followed a half a mile; then he struck out over the swamp, and waded

+ankle deep as much as another half-mile.  We come to a little flat piece

+of land which was dry and very thick with trees and bushes and vines,

+and he says:

+

+"You shove right in dah jist a few steps, Mars Jawge; dah's whah dey is.

+I's seed 'm befo'; I don't k'yer to see 'em no mo'."

+

+Then he slopped right along and went away, and pretty soon the trees hid

+him.  I poked into the place a-ways and come to a little open patch

+as big as a bedroom all hung around with vines, and found a man laying

+there asleep—and, by jings, it was my old Jim!

+

+I waked him up, and I reckoned it was going to be a grand surprise to

+him to see me again, but it warn't.  He nearly cried he was so glad, but

+he warn't surprised.  Said he swum along behind me that night, and heard

+me yell every time, but dasn't answer, because he didn't want nobody to

+pick him up and take him into slavery again.  Says he:

+

+"I got hurt a little, en couldn't swim fas', so I wuz a considable ways

+behine you towards de las'; when you landed I reck'ned I could ketch

+up wid you on de lan' 'dout havin' to shout at you, but when I see dat

+house I begin to go slow.  I 'uz off too fur to hear what dey say to

+you—I wuz 'fraid o' de dogs; but when it 'uz all quiet agin I knowed

+you's in de house, so I struck out for de woods to wait for day.  Early

+in de mawnin' some er de niggers come along, gwyne to de fields, en dey

+tuk me en showed me dis place, whah de dogs can't track me on accounts

+o' de water, en dey brings me truck to eat every night, en tells me how

+you's a-gitt'n along."

+

+"Why didn't you tell my Jack to fetch me here sooner, Jim?"

+

+"Well, 'twarn't no use to 'sturb you, Huck, tell we could do sumfn—but

+we's all right now.  I ben a-buyin' pots en pans en vittles, as I got a

+chanst, en a-patchin' up de raf' nights when—"

+

+"What raft, Jim?"

+

+"Our ole raf'."

+

+"You mean to say our old raft warn't smashed all to flinders?"

+

+"No, she warn't.  She was tore up a good deal—one en' of her was; but

+dey warn't no great harm done, on'y our traps was mos' all los'.  Ef we

+hadn' dive' so deep en swum so fur under water, en de night hadn' ben

+so dark, en we warn't so sk'yerd, en ben sich punkin-heads, as de sayin'

+is, we'd a seed de raf'.  But it's jis' as well we didn't, 'kase now

+she's all fixed up agin mos' as good as new, en we's got a new lot o'

+stuff, in de place o' what 'uz los'."

+

+"Why, how did you get hold of the raft again, Jim—did you catch her?"

+

+"How I gwyne to ketch her en I out in de woods?  No; some er de niggers

+foun' her ketched on a snag along heah in de ben', en dey hid her in a

+crick 'mongst de willows, en dey wuz so much jawin' 'bout which un 'um

+she b'long to de mos' dat I come to heah 'bout it pooty soon, so I ups

+en settles de trouble by tellin' 'um she don't b'long to none uv um, but

+to you en me; en I ast 'm if dey gwyne to grab a young white genlman's

+propaty, en git a hid'n for it?  Den I gin 'm ten cents apiece, en dey

+'uz mighty well satisfied, en wisht some mo' raf's 'ud come along en

+make 'm rich agin. Dey's mighty good to me, dese niggers is, en whatever

+I wants 'm to do fur me I doan' have to ast 'm twice, honey.  Dat Jack's

+a good nigger, en pooty smart."

+

+"Yes, he is.  He ain't ever told me you was here; told me to come, and

+he'd show me a lot of water-moccasins.  If anything happens he ain't

+mixed up in it.  He can say he never seen us together, and it 'll be the

+truth."

+

+I don't want to talk much about the next day.  I reckon I'll cut it

+pretty short.  I waked up about dawn, and was a-going to turn over and

+go to sleep again when I noticed how still it was—didn't seem to be

+anybody stirring.  That warn't usual.  Next I noticed that Buck was

+up and gone. Well, I gets up, a-wondering, and goes down stairs—nobody

+around; everything as still as a mouse.  Just the same outside.  Thinks

+I, what does it mean?  Down by the wood-pile I comes across my Jack, and

+says:

+

+"What's it all about?"

+

+Says he:

+

+"Don't you know, Mars Jawge?"

+

+"No," says I, "I don't."

+

+"Well, den, Miss Sophia's run off! 'deed she has.  She run off in de

+night some time—nobody don't know jis' when; run off to get married

+to dat young Harney Shepherdson, you know—leastways, so dey 'spec.  De

+fambly foun' it out 'bout half an hour ago—maybe a little mo'—en' I

+tell you dey warn't no time los'.  Sich another hurryin' up guns

+en hosses you never see!  De women folks has gone for to stir up de

+relations, en ole Mars Saul en de boys tuck dey guns en rode up de

+river road for to try to ketch dat young man en kill him 'fo' he kin

+git acrost de river wid Miss Sophia.  I reck'n dey's gwyne to be mighty

+rough times."

+

+"Buck went off 'thout waking me up."

+

+"Well, I reck'n he did!  Dey warn't gwyne to mix you up in it.

+ Mars Buck he loaded up his gun en 'lowed he's gwyne to fetch home a

+Shepherdson or bust. Well, dey'll be plenty un 'm dah, I reck'n, en you

+bet you he'll fetch one ef he gits a chanst."

+

+I took up the river road as hard as I could put.  By and by I begin to

+hear guns a good ways off.  When I come in sight of the log store and

+the woodpile where the steamboats lands I worked along under the trees

+and brush till I got to a good place, and then I clumb up into the

+forks of a cottonwood that was out of reach, and watched.  There was a

+wood-rank four foot high a little ways in front of the tree, and first I

+was going to hide behind that; but maybe it was luckier I didn't.

+

+There was four or five men cavorting around on their horses in the open

+place before the log store, cussing and yelling, and trying to get at

+a couple of young chaps that was behind the wood-rank alongside of the

+steamboat landing; but they couldn't come it.  Every time one of them

+showed himself on the river side of the woodpile he got shot at.  The

+two boys was squatting back to back behind the pile, so they could watch

+both ways.

+

+By and by the men stopped cavorting around and yelling.  They started

+riding towards the store; then up gets one of the boys, draws a steady

+bead over the wood-rank, and drops one of them out of his saddle.  All

+the men jumped off of their horses and grabbed the hurt one and started

+to carry him to the store; and that minute the two boys started on the

+run.  They got half way to the tree I was in before the men noticed.

+Then the men see them, and jumped on their horses and took out after

+them.  They gained on the boys, but it didn't do no good, the boys had

+too good a start; they got to the woodpile that was in front of my tree,

+and slipped in behind it, and so they had the bulge on the men again.

+One of the boys was Buck, and the other was a slim young chap about

+nineteen years old.

+

+The men ripped around awhile, and then rode away.  As soon as they was

+out of sight I sung out to Buck and told him.  He didn't know what

+to make of my voice coming out of the tree at first.  He was awful

+surprised.  He told me to watch out sharp and let him know when the

+men come in sight again; said they was up to some devilment or

+other—wouldn't be gone long.  I wished I was out of that tree, but I

+dasn't come down.  Buck begun to cry and rip, and 'lowed that him and

+his cousin Joe (that was the other young chap) would make up for this

+day yet.  He said his father and his two brothers was killed, and two

+or three of the enemy.  Said the Shepherdsons laid for them in

+ambush.  Buck said his father and brothers ought to waited for their

+relations—the Shepherdsons was too strong for them.  I asked him what

+was become of young Harney and Miss Sophia.  He said they'd got across

+the river and was safe.  I was glad of that; but the way Buck did take

+on because he didn't manage to kill Harney that day he shot at him—I

+hain't ever heard anything like it.

+

+All of a sudden, bang! bang! bang! goes three or four guns—the men had

+slipped around through the woods and come in from behind without their

+horses!  The boys jumped for the river—both of them hurt—and as they

+swum down the current the men run along the bank shooting at them and

+singing out, "Kill them, kill them!"  It made me so sick I most fell out

+of the tree.  I ain't a-going to tell all that happened—it would make

+me sick again if I was to do that.  I wished I hadn't ever come ashore

+that night to see such things.  I ain't ever going to get shut of

+them—lots of times I dream about them.

+

+I stayed in the tree till it begun to get dark, afraid to come down.

+Sometimes I heard guns away off in the woods; and twice I seen little

+gangs of men gallop past the log store with guns; so I reckoned the

+trouble was still a-going on.  I was mighty downhearted; so I made up my

+mind I wouldn't ever go anear that house again, because I reckoned I

+was to blame, somehow. I judged that that piece of paper meant that Miss

+Sophia was to meet Harney somewheres at half-past two and run off; and

+I judged I ought to told her father about that paper and the curious way

+she acted, and then maybe he would a locked her up, and this awful mess

+wouldn't ever happened.

+

+When I got down out of the tree I crept along down the river bank a

+piece, and found the two bodies laying in the edge of the water, and

+tugged at them till I got them ashore; then I covered up their faces,

+and got away as quick as I could.  I cried a little when I was covering

+up Buck's face, for he was mighty good to me.

+

+It was just dark now.  I never went near the house, but struck through

+the woods and made for the swamp.  Jim warn't on his island, so I

+tramped off in a hurry for the crick, and crowded through the willows,

+red-hot to jump aboard and get out of that awful country.  The raft was

+gone!  My souls, but I was scared!  I couldn't get my breath for most

+a minute. Then I raised a yell.  A voice not twenty-five foot from me

+says:

+

+"Good lan'! is dat you, honey?  Doan' make no noise."

+

+It was Jim's voice—nothing ever sounded so good before.  I run along the

+bank a piece and got aboard, and Jim he grabbed me and hugged me, he was

+so glad to see me.  He says:

+

+"Laws bless you, chile, I 'uz right down sho' you's dead agin.  Jack's

+been heah; he say he reck'n you's ben shot, kase you didn' come home no

+mo'; so I's jes' dis minute a startin' de raf' down towards de mouf er

+de crick, so's to be all ready for to shove out en leave soon as Jack

+comes agin en tells me for certain you is dead.  Lawsy, I's mighty

+glad to git you back again, honey."

+

+I says:

+

+"All right—that's mighty good; they won't find me, and they'll think

+I've been killed, and floated down the river—there's something up there

+that 'll help them think so—so don't you lose no time, Jim, but just

+shove off for the big water as fast as ever you can."

+

+I never felt easy till the raft was two mile below there and out in

+the middle of the Mississippi.  Then we hung up our signal lantern, and

+judged that we was free and safe once more.  I hadn't had a bite to eat

+since yesterday, so Jim he got out some corn-dodgers and buttermilk,

+and pork and cabbage and greens—there ain't nothing in the world so good

+when it's cooked right—and whilst I eat my supper we talked and had a

+good time.  I was powerful glad to get away from the feuds, and so was

+Jim to get away from the swamp.  We said there warn't no home like a

+raft, after all.  Other places do seem so cramped up and smothery, but a

+raft don't.  You feel mighty free and easy and comfortable on a raft.

+

+

+

+

+CHAPTER XIX.

+

+TWO or three days and nights went by; I reckon I might say they swum by,

+they slid along so quiet and smooth and lovely.  Here is the way we put

+in the time.  It was a monstrous big river down there—sometimes a mile

+and a half wide; we run nights, and laid up and hid daytimes; soon as

+night was most gone we stopped navigating and tied up—nearly always

+in the dead water under a towhead; and then cut young cottonwoods and

+willows, and hid the raft with them.  Then we set out the lines.  Next

+we slid into the river and had a swim, so as to freshen up and cool

+off; then we set down on the sandy bottom where the water was about knee

+deep, and watched the daylight come.  Not a sound anywheres—perfectly

+still—just like the whole world was asleep, only sometimes the bullfrogs

+a-cluttering, maybe.  The first thing to see, looking away over the

+water, was a kind of dull line—that was the woods on t'other side; you

+couldn't make nothing else out; then a pale place in the sky; then more

+paleness spreading around; then the river softened up away off, and

+warn't black any more, but gray; you could see little dark spots

+drifting along ever so far away—trading scows, and such things; and

+long black streaks—rafts; sometimes you could hear a sweep screaking; or

+jumbled up voices, it was so still, and sounds come so far; and by and

+by you could see a streak on the water which you know by the look of the

+streak that there's a snag there in a swift current which breaks on it

+and makes that streak look that way; and you see the mist curl up off

+of the water, and the east reddens up, and the river, and you make out a

+log-cabin in the edge of the woods, away on the bank on t'other side of

+the river, being a woodyard, likely, and piled by them cheats so you can

+throw a dog through it anywheres; then the nice breeze springs up, and

+comes fanning you from over there, so cool and fresh and sweet to smell

+on account of the woods and the flowers; but sometimes not that way,

+because they've left dead fish laying around, gars and such, and they

+do get pretty rank; and next you've got the full day, and everything

+smiling in the sun, and the song-birds just going it!

+

+A little smoke couldn't be noticed now, so we would take some fish off

+of the lines and cook up a hot breakfast.  And afterwards we would watch

+the lonesomeness of the river, and kind of lazy along, and by and by

+lazy off to sleep.  Wake up by and by, and look to see what done it, and

+maybe see a steamboat coughing along up-stream, so far off towards the

+other side you couldn't tell nothing about her only whether she was

+a stern-wheel or side-wheel; then for about an hour there wouldn't be

+nothing to hear nor nothing to see—just solid lonesomeness.  Next

+you'd see a raft sliding by, away off yonder, and maybe a galoot on it

+chopping, because they're most always doing it on a raft; you'd see the

+axe flash and come down—you don't hear nothing; you see that axe go

+up again, and by the time it's above the man's head then you hear the

+k'chunk!—it had took all that time to come over the water.  So we

+would put in the day, lazying around, listening to the stillness.  Once

+there was a thick fog, and the rafts and things that went by was beating

+tin pans so the steamboats wouldn't run over them.  A scow or a

+raft went by so close we could hear them talking and cussing and

+laughing—heard them plain; but we couldn't see no sign of them; it made

+you feel crawly; it was like spirits carrying on that way in the air.

+ Jim said he believed it was spirits; but I says:

+

+"No; spirits wouldn't say, 'Dern the dern fog.'"

+

+Soon as it was night out we shoved; when we got her out to about the

+middle we let her alone, and let her float wherever the current wanted

+her to; then we lit the pipes, and dangled our legs in the water, and

+talked about all kinds of things—we was always naked, day and night,

+whenever the mosquitoes would let us—the new clothes Buck's folks made

+for me was too good to be comfortable, and besides I didn't go much on

+clothes, nohow.

+

+Sometimes we'd have that whole river all to ourselves for the longest

+time. Yonder was the banks and the islands, across the water; and maybe

+a spark—which was a candle in a cabin window; and sometimes on the water

+you could see a spark or two—on a raft or a scow, you know; and maybe

+you could hear a fiddle or a song coming over from one of them crafts.

+It's lovely to live on a raft.  We had the sky up there, all speckled

+with stars, and we used to lay on our backs and look up at them, and

+discuss about whether they was made or only just happened.  Jim he

+allowed they was made, but I allowed they happened; I judged it would

+have took too long to make so many.  Jim said the moon could a laid

+them; well, that looked kind of reasonable, so I didn't say nothing

+against it, because I've seen a frog lay most as many, so of course it

+could be done. We used to watch the stars that fell, too, and see them

+streak down.  Jim allowed they'd got spoiled and was hove out of the

+nest.

+

+Once or twice of a night we would see a steamboat slipping along in the

+dark, and now and then she would belch a whole world of sparks up out

+of her chimbleys, and they would rain down in the river and look awful

+pretty; then she would turn a corner and her lights would wink out and

+her powwow shut off and leave the river still again; and by and by her

+waves would get to us, a long time after she was gone, and joggle the

+raft a bit, and after that you wouldn't hear nothing for you couldn't

+tell how long, except maybe frogs or something.

+

+After midnight the people on shore went to bed, and then for two or

+three hours the shores was black—no more sparks in the cabin windows.

+ These sparks was our clock—the first one that showed again meant

+morning was coming, so we hunted a place to hide and tie up right away.

+

+One morning about daybreak I found a canoe and crossed over a chute to

+the main shore—it was only two hundred yards—and paddled about a mile

+up a crick amongst the cypress woods, to see if I couldn't get some

+berries. Just as I was passing a place where a kind of a cowpath crossed

+the crick, here comes a couple of men tearing up the path as tight as

+they could foot it.  I thought I was a goner, for whenever anybody was

+after anybody I judged it was me—or maybe Jim.  I was about to dig out

+from there in a hurry, but they was pretty close to me then, and sung

+out and begged me to save their lives—said they hadn't been doing

+nothing, and was being chased for it—said there was men and dogs

+a-coming.  They wanted to jump right in, but I says:

+

+"Don't you do it.  I don't hear the dogs and horses yet; you've got time

+to crowd through the brush and get up the crick a little ways; then you

+take to the water and wade down to me and get in—that'll throw the dogs

+off the scent."

+

+They done it, and soon as they was aboard I lit out for our towhead,

+and in about five or ten minutes we heard the dogs and the men away off,

+shouting. We heard them come along towards the crick, but couldn't

+see them; they seemed to stop and fool around a while; then, as we got

+further and further away all the time, we couldn't hardly hear them at

+all; by the time we had left a mile of woods behind us and struck the

+river, everything was quiet, and we paddled over to the towhead and hid

+in the cottonwoods and was safe.

+

+One of these fellows was about seventy or upwards, and had a bald head

+and very gray whiskers.  He had an old battered-up slouch hat on, and

+a greasy blue woollen shirt, and ragged old blue jeans britches stuffed

+into his boot-tops, and home-knit galluses—no, he only had one.  He had

+an old long-tailed blue jeans coat with slick brass buttons flung over

+his arm, and both of them had big, fat, ratty-looking carpet-bags.

+

+The other fellow was about thirty, and dressed about as ornery.  After

+breakfast we all laid off and talked, and the first thing that come out

+was that these chaps didn't know one another.

+

+"What got you into trouble?" says the baldhead to t'other chap.

+

+"Well, I'd been selling an article to take the tartar off the teeth—and

+it does take it off, too, and generly the enamel along with it—but I

+stayed about one night longer than I ought to, and was just in the act

+of sliding out when I ran across you on the trail this side of town, and

+you told me they were coming, and begged me to help you to get off.  So

+I told you I was expecting trouble myself, and would scatter out with

+you. That's the whole yarn—what's yourn?

+

+"Well, I'd ben a-running' a little temperance revival thar 'bout a week,

+and was the pet of the women folks, big and little, for I was makin' it

+mighty warm for the rummies, I tell you, and takin' as much as five

+or six dollars a night—ten cents a head, children and niggers free—and

+business a-growin' all the time, when somehow or another a little report

+got around last night that I had a way of puttin' in my time with a

+private jug on the sly.  A nigger rousted me out this mornin', and told

+me the people was getherin' on the quiet with their dogs and horses, and

+they'd be along pretty soon and give me 'bout half an hour's start,

+and then run me down if they could; and if they got me they'd tar

+and feather me and ride me on a rail, sure.  I didn't wait for no

+breakfast—I warn't hungry."

+

+"Old man," said the young one, "I reckon we might double-team it

+together; what do you think?"

+

+"I ain't undisposed.  What's your line—mainly?"

+

+"Jour printer by trade; do a little in patent medicines;

+theater-actor—tragedy, you know; take a turn to mesmerism and phrenology

+when there's a chance; teach singing-geography school for a change;

+sling a lecture sometimes—oh, I do lots of things—most anything that

+comes handy, so it ain't work.  What's your lay?"

+

+"I've done considerble in the doctoring way in my time.  Layin' on o'

+hands is my best holt—for cancer and paralysis, and sich things; and I

+k'n tell a fortune pretty good when I've got somebody along to find out

+the facts for me.  Preachin's my line, too, and workin' camp-meetin's,

+and missionaryin' around."

+

+Nobody never said anything for a while; then the young man hove a sigh

+and says:

+

+"Alas!"

+

+"What 're you alassin' about?" says the bald-head.

+

+"To think I should have lived to be leading such a life, and be degraded

+down into such company."  And he begun to wipe the corner of his eye

+with a rag.

+

+"Dern your skin, ain't the company good enough for you?" says the

+baldhead, pretty pert and uppish.

+

+"Yes, it is good enough for me; it's as good as I deserve; for who

+fetched me so low when I was so high?  I did myself.  I don't blame

+you, gentlemen—far from it; I don't blame anybody.  I deserve it

+all.  Let the cold world do its worst; one thing I know—there's a grave

+somewhere for me. The world may go on just as it's always done, and take

+everything from me—loved ones, property, everything; but it can't take

+that. Some day I'll lie down in it and forget it all, and my poor broken

+heart will be at rest."  He went on a-wiping.

+

+"Drot your pore broken heart," says the baldhead; "what are you heaving

+your pore broken heart at us f'r?  we hain't done nothing."

+

+"No, I know you haven't.  I ain't blaming you, gentlemen.  I brought

+myself down—yes, I did it myself.  It's right I should suffer—perfectly

+right—I don't make any moan."

+

+"Brought you down from whar?  Whar was you brought down from?"

+

+"Ah, you would not believe me; the world never believes—let it pass—'tis

+no matter.  The secret of my birth—"

+

+"The secret of your birth!  Do you mean to say—"

+

+"Gentlemen," says the young man, very solemn, "I will reveal it to you,

+for I feel I may have confidence in you.  By rights I am a duke!"

+

+Jim's eyes bugged out when he heard that; and I reckon mine did, too.

+Then the baldhead says:  "No! you can't mean it?"

+

+"Yes.  My great-grandfather, eldest son of the Duke of Bridgewater, fled

+to this country about the end of the last century, to breathe the pure

+air of freedom; married here, and died, leaving a son, his own father

+dying about the same time.  The second son of the late duke seized the

+titles and estates—the infant real duke was ignored.  I am the lineal

+descendant of that infant—I am the rightful Duke of Bridgewater; and

+here am I, forlorn, torn from my high estate, hunted of men, despised

+by the cold world, ragged, worn, heart-broken, and degraded to the

+companionship of felons on a raft!"

+

+Jim pitied him ever so much, and so did I. We tried to comfort him, but

+he said it warn't much use, he couldn't be much comforted; said if we

+was a mind to acknowledge him, that would do him more good than most

+anything else; so we said we would, if he would tell us how.  He said we

+ought to bow when we spoke to him, and say "Your Grace," or "My Lord,"

+or "Your Lordship"—and he wouldn't mind it if we called him plain

+"Bridgewater," which, he said, was a title anyway, and not a name; and

+one of us ought to wait on him at dinner, and do any little thing for

+him he wanted done.

+

+Well, that was all easy, so we done it.  All through dinner Jim stood

+around and waited on him, and says, "Will yo' Grace have some o' dis or

+some o' dat?" and so on, and a body could see it was mighty pleasing to

+him.

+

+But the old man got pretty silent by and by—didn't have much to say, and

+didn't look pretty comfortable over all that petting that was going on

+around that duke.  He seemed to have something on his mind.  So, along

+in the afternoon, he says:

+

+"Looky here, Bilgewater," he says, "I'm nation sorry for you, but you

+ain't the only person that's had troubles like that."

+

+"No?"

+

+"No you ain't.  You ain't the only person that's ben snaked down

+wrongfully out'n a high place."

+

+"Alas!"

+

+"No, you ain't the only person that's had a secret of his birth."  And,

+by jings, he begins to cry.

+

+"Hold!  What do you mean?"

+

+"Bilgewater, kin I trust you?" says the old man, still sort of sobbing.

+

+"To the bitter death!"  He took the old man by the hand and squeezed it,

+and says, "That secret of your being:  speak!"

+

+"Bilgewater, I am the late Dauphin!"

+

+You bet you, Jim and me stared this time.  Then the duke says:

+

+"You are what?"

+

+"Yes, my friend, it is too true—your eyes is lookin' at this very moment

+on the pore disappeared Dauphin, Looy the Seventeen, son of Looy the

+Sixteen and Marry Antonette."

+

+"You!  At your age!  No!  You mean you're the late Charlemagne; you must

+be six or seven hundred years old, at the very least."

+

+"Trouble has done it, Bilgewater, trouble has done it; trouble has brung

+these gray hairs and this premature balditude.  Yes, gentlemen, you

+see before you, in blue jeans and misery, the wanderin', exiled,

+trampled-on, and sufferin' rightful King of France."

+

+Well, he cried and took on so that me and Jim didn't know hardly what to

+do, we was so sorry—and so glad and proud we'd got him with us, too.

+ So we set in, like we done before with the duke, and tried to comfort

+him. But he said it warn't no use, nothing but to be dead and done

+with it all could do him any good; though he said it often made him feel

+easier and better for a while if people treated him according to his

+rights, and got down on one knee to speak to him, and always called him

+"Your Majesty," and waited on him first at meals, and didn't set down

+in his presence till he asked them. So Jim and me set to majestying him,

+and doing this and that and t'other for him, and standing up till he

+told us we might set down.  This done him heaps of good, and so he

+got cheerful and comfortable.  But the duke kind of soured on him, and

+didn't look a bit satisfied with the way things was going; still,

+the king acted real friendly towards him, and said the duke's

+great-grandfather and all the other Dukes of Bilgewater was a good

+deal thought of by his father, and was allowed to come to the palace

+considerable; but the duke stayed huffy a good while, till by and by the

+king says:

+

+"Like as not we got to be together a blamed long time on this h-yer

+raft, Bilgewater, and so what's the use o' your bein' sour?  It 'll only

+make things oncomfortable.  It ain't my fault I warn't born a duke,

+it ain't your fault you warn't born a king—so what's the use to worry?

+ Make the best o' things the way you find 'em, says I—that's my motto.

+ This ain't no bad thing that we've struck here—plenty grub and an easy

+life—come, give us your hand, duke, and le's all be friends."

+

+The duke done it, and Jim and me was pretty glad to see it.  It took

+away all the uncomfortableness and we felt mighty good over it, because

+it would a been a miserable business to have any unfriendliness on the

+raft; for what you want, above all things, on a raft, is for everybody

+to be satisfied, and feel right and kind towards the others.

+

+It didn't take me long to make up my mind that these liars warn't no

+kings nor dukes at all, but just low-down humbugs and frauds.  But I

+never said nothing, never let on; kept it to myself; it's the best way;

+then you don't have no quarrels, and don't get into no trouble.  If they

+wanted us to call them kings and dukes, I hadn't no objections, 'long as

+it would keep peace in the family; and it warn't no use to tell Jim, so

+I didn't tell him.  If I never learnt nothing else out of pap, I learnt

+that the best way to get along with his kind of people is to let them

+have their own way.

+

+

+

+

+CHAPTER XX.

+

+THEY asked us considerable many questions; wanted to know what we

+covered up the raft that way for, and laid by in the daytime instead of

+running—was Jim a runaway nigger?  Says I:

+

+"Goodness sakes! would a runaway nigger run south?"

+

+No, they allowed he wouldn't.  I had to account for things some way, so

+I says:

+

+"My folks was living in Pike County, in Missouri, where I was born, and

+they all died off but me and pa and my brother Ike.  Pa, he 'lowed

+he'd break up and go down and live with Uncle Ben, who's got a little

+one-horse place on the river, forty-four mile below Orleans.  Pa was

+pretty poor, and had some debts; so when he'd squared up there warn't

+nothing left but sixteen dollars and our nigger, Jim.  That warn't

+enough to take us fourteen hundred mile, deck passage nor no other way.

+ Well, when the river rose pa had a streak of luck one day; he ketched

+this piece of a raft; so we reckoned we'd go down to Orleans on it.

+ Pa's luck didn't hold out; a steamboat run over the forrard corner of

+the raft one night, and we all went overboard and dove under the wheel;

+Jim and me come up all right, but pa was drunk, and Ike was only four

+years old, so they never come up no more.  Well, for the next day or

+two we had considerable trouble, because people was always coming out in

+skiffs and trying to take Jim away from me, saying they believed he was

+a runaway nigger.  We don't run daytimes no more now; nights they don't

+bother us."

+

+The duke says:

+

+"Leave me alone to cipher out a way so we can run in the daytime if we

+want to.  I'll think the thing over—I'll invent a plan that'll fix it.

+We'll let it alone for to-day, because of course we don't want to go by

+that town yonder in daylight—it mightn't be healthy."

+

+Towards night it begun to darken up and look like rain; the heat

+lightning was squirting around low down in the sky, and the leaves was

+beginning to shiver—it was going to be pretty ugly, it was easy to see

+that.  So the duke and the king went to overhauling our wigwam, to see

+what the beds was like.  My bed was a straw tick better than Jim's,

+which was a corn-shuck tick; there's always cobs around about in a shuck

+tick, and they poke into you and hurt; and when you roll over the dry

+shucks sound like you was rolling over in a pile of dead leaves; it

+makes such a rustling that you wake up.  Well, the duke allowed he would

+take my bed; but the king allowed he wouldn't.  He says:

+

+"I should a reckoned the difference in rank would a sejested to you that

+a corn-shuck bed warn't just fitten for me to sleep on.  Your Grace 'll

+take the shuck bed yourself."

+

+Jim and me was in a sweat again for a minute, being afraid there was

+going to be some more trouble amongst them; so we was pretty glad when

+the duke says:

+

+"'Tis my fate to be always ground into the mire under the iron heel of

+oppression.  Misfortune has broken my once haughty spirit; I yield, I

+submit; 'tis my fate.  I am alone in the world—let me suffer; can bear

+it."

+

+We got away as soon as it was good and dark.  The king told us to stand

+well out towards the middle of the river, and not show a light till we

+got a long ways below the town.  We come in sight of the little bunch of

+lights by and by—that was the town, you know—and slid by, about a half

+a mile out, all right.  When we was three-quarters of a mile below we

+hoisted up our signal lantern; and about ten o'clock it come on to rain

+and blow and thunder and lighten like everything; so the king told us

+to both stay on watch till the weather got better; then him and the duke

+crawled into the wigwam and turned in for the night.  It was my watch

+below till twelve, but I wouldn't a turned in anyway if I'd had a bed,

+because a body don't see such a storm as that every day in the week, not

+by a long sight.  My souls, how the wind did scream along!  And every

+second or two there'd come a glare that lit up the white-caps for a half

+a mile around, and you'd see the islands looking dusty through the rain,

+and the trees thrashing around in the wind; then comes a H-WHACK!—bum!

+bum! bumble-umble-um-bum-bum-bum-bum—and the thunder would go rumbling

+and grumbling away, and quit—and then RIP comes another flash and

+another sockdolager.  The waves most washed me off the raft sometimes,

+but I hadn't any clothes on, and didn't mind.  We didn't have no trouble

+about snags; the lightning was glaring and flittering around so constant

+that we could see them plenty soon enough to throw her head this way or

+that and miss them.

+

+I had the middle watch, you know, but I was pretty sleepy by that time,

+so Jim he said he would stand the first half of it for me; he was always

+mighty good that way, Jim was.  I crawled into the wigwam, but the king

+and the duke had their legs sprawled around so there warn't no show for

+me; so I laid outside—I didn't mind the rain, because it was warm, and

+the waves warn't running so high now.  About two they come up again,

+though, and Jim was going to call me; but he changed his mind, because

+he reckoned they warn't high enough yet to do any harm; but he was

+mistaken about that, for pretty soon all of a sudden along comes a

+regular ripper and washed me overboard.  It most killed Jim a-laughing.

+ He was the easiest nigger to laugh that ever was, anyway.

+

+I took the watch, and Jim he laid down and snored away; and by and by

+the storm let up for good and all; and the first cabin-light that showed

+I rousted him out, and we slid the raft into hiding quarters for the

+day.

+

+The king got out an old ratty deck of cards after breakfast, and him

+and the duke played seven-up a while, five cents a game.  Then they got

+tired of it, and allowed they would "lay out a campaign," as they called

+it. The duke went down into his carpet-bag, and fetched up a lot of

+little printed bills and read them out loud.  One bill said, "The

+celebrated Dr. Armand de Montalban, of Paris," would "lecture on the

+Science of Phrenology" at such and such a place, on the blank day of

+blank, at ten cents admission, and "furnish charts of character at

+twenty-five cents apiece."  The duke said that was him.  In another

+bill he was the "world-renowned Shakespearian tragedian, Garrick the

+Younger, of Drury Lane, London."  In other bills he had a lot of other

+names and done other wonderful things, like finding water and gold with

+a "divining-rod," "dissipating witch spells," and so on.  By and by he

+says:

+

+"But the histrionic muse is the darling.  Have you ever trod the boards,

+Royalty?"

+

+"No," says the king.

+

+"You shall, then, before you're three days older, Fallen Grandeur," says

+the duke.  "The first good town we come to we'll hire a hall and do the

+sword fight in Richard III. and the balcony scene in Romeo and Juliet.

+How does that strike you?"

+

+"I'm in, up to the hub, for anything that will pay, Bilgewater; but, you

+see, I don't know nothing about play-actin', and hain't ever seen much

+of it.  I was too small when pap used to have 'em at the palace.  Do you

+reckon you can learn me?"

+

+"Easy!"

+

+"All right.  I'm jist a-freezn' for something fresh, anyway.  Le's

+commence right away."

+

+So the duke he told him all about who Romeo was and who Juliet was, and

+said he was used to being Romeo, so the king could be Juliet.

+

+"But if Juliet's such a young gal, duke, my peeled head and my white

+whiskers is goin' to look oncommon odd on her, maybe."

+

+"No, don't you worry; these country jakes won't ever think of that.

+Besides, you know, you'll be in costume, and that makes all the

+difference in the world; Juliet's in a balcony, enjoying the moonlight

+before she goes to bed, and she's got on her night-gown and her ruffled

+nightcap.  Here are the costumes for the parts."

+

+He got out two or three curtain-calico suits, which he said was

+meedyevil armor for Richard III. and t'other chap, and a long white

+cotton nightshirt and a ruffled nightcap to match.  The king was

+satisfied; so the duke got out his book and read the parts over in the

+most splendid spread-eagle way, prancing around and acting at the same

+time, to show how it had got to be done; then he give the book to the

+king and told him to get his part by heart.

+

+There was a little one-horse town about three mile down the bend, and

+after dinner the duke said he had ciphered out his idea about how to run

+in daylight without it being dangersome for Jim; so he allowed he would

+go down to the town and fix that thing.  The king allowed he would go,

+too, and see if he couldn't strike something.  We was out of coffee, so

+Jim said I better go along with them in the canoe and get some.

+

+When we got there there warn't nobody stirring; streets empty, and

+perfectly dead and still, like Sunday.  We found a sick nigger sunning

+himself in a back yard, and he said everybody that warn't too young or

+too sick or too old was gone to camp-meeting, about two mile back in the

+woods.  The king got the directions, and allowed he'd go and work that

+camp-meeting for all it was worth, and I might go, too.

+

+The duke said what he was after was a printing-office.  We found it;

+a little bit of a concern, up over a carpenter shop—carpenters and

+printers all gone to the meeting, and no doors locked.  It was a dirty,

+littered-up place, and had ink marks, and handbills with pictures of

+horses and runaway niggers on them, all over the walls.  The duke shed

+his coat and said he was all right now.  So me and the king lit out for

+the camp-meeting.

+

+We got there in about a half an hour fairly dripping, for it was a most

+awful hot day.  There was as much as a thousand people there from

+twenty mile around.  The woods was full of teams and wagons, hitched

+everywheres, feeding out of the wagon-troughs and stomping to keep

+off the flies.  There was sheds made out of poles and roofed over with

+branches, where they had lemonade and gingerbread to sell, and piles of

+watermelons and green corn and such-like truck.

+

+The preaching was going on under the same kinds of sheds, only they was

+bigger and held crowds of people.  The benches was made out of outside

+slabs of logs, with holes bored in the round side to drive sticks into

+for legs. They didn't have no backs.  The preachers had high platforms

+to stand on at one end of the sheds.  The women had on sun-bonnets;

+and some had linsey-woolsey frocks, some gingham ones, and a few of the

+young ones had on calico.  Some of the young men was barefooted, and

+some of the children didn't have on any clothes but just a tow-linen

+shirt.  Some of the old women was knitting, and some of the young folks

+was courting on the sly.

+

+The first shed we come to the preacher was lining out a hymn.  He lined

+out two lines, everybody sung it, and it was kind of grand to hear it,

+there was so many of them and they done it in such a rousing way; then

+he lined out two more for them to sing—and so on.  The people woke up

+more and more, and sung louder and louder; and towards the end some

+begun to groan, and some begun to shout.  Then the preacher begun to

+preach, and begun in earnest, too; and went weaving first to one side of

+the platform and then the other, and then a-leaning down over the front

+of it, with his arms and his body going all the time, and shouting his

+words out with all his might; and every now and then he would hold up

+his Bible and spread it open, and kind of pass it around this way and

+that, shouting, "It's the brazen serpent in the wilderness!  Look upon

+it and live!"  And people would shout out, "Glory!—A-a-men!"  And so

+he went on, and the people groaning and crying and saying amen:

+

+"Oh, come to the mourners' bench! come, black with sin! (Amen!) come,

+sick and sore! (Amen!) come, lame and halt and blind! (Amen!) come,

+pore and needy, sunk in shame! (A-A-Men!) come, all that's worn and

+soiled and suffering!—come with a broken spirit! come with a contrite

+heart! come in your rags and sin and dirt! the waters that cleanse

+is free, the door of heaven stands open—oh, enter in and be at rest!"

+(A-A-Men!  Glory, Glory Hallelujah!)

+

+And so on.  You couldn't make out what the preacher said any more, on

+account of the shouting and crying.  Folks got up everywheres in the

+crowd, and worked their way just by main strength to the mourners'

+bench, with the tears running down their faces; and when all the

+mourners had got up there to the front benches in a crowd, they sung and

+shouted and flung themselves down on the straw, just crazy and wild.

+

+Well, the first I knowed the king got a-going, and you could hear him

+over everybody; and next he went a-charging up on to the platform, and

+the preacher he begged him to speak to the people, and he done it.  He

+told them he was a pirate—been a pirate for thirty years out in the

+Indian Ocean—and his crew was thinned out considerable last spring in

+a fight, and he was home now to take out some fresh men, and thanks to

+goodness he'd been robbed last night and put ashore off of a steamboat

+without a cent, and he was glad of it; it was the blessedest thing that

+ever happened to him, because he was a changed man now, and happy for

+the first time in his life; and, poor as he was, he was going to start

+right off and work his way back to the Indian Ocean, and put in the rest

+of his life trying to turn the pirates into the true path; for he could

+do it better than anybody else, being acquainted with all pirate crews

+in that ocean; and though it would take him a long time to get there

+without money, he would get there anyway, and every time he convinced

+a pirate he would say to him, "Don't you thank me, don't you give me no

+credit; it all belongs to them dear people in Pokeville camp-meeting,

+natural brothers and benefactors of the race, and that dear preacher

+there, the truest friend a pirate ever had!"

+

+And then he busted into tears, and so did everybody.  Then somebody

+sings out, "Take up a collection for him, take up a collection!"  Well,

+a half a dozen made a jump to do it, but somebody sings out, "Let him

+pass the hat around!"  Then everybody said it, the preacher too.

+

+So the king went all through the crowd with his hat swabbing his eyes,

+and blessing the people and praising them and thanking them for being

+so good to the poor pirates away off there; and every little while the

+prettiest kind of girls, with the tears running down their cheeks, would

+up and ask him would he let them kiss him for to remember him by; and he

+always done it; and some of them he hugged and kissed as many as five or

+six times—and he was invited to stay a week; and everybody wanted him to

+live in their houses, and said they'd think it was an honor; but he said

+as this was the last day of the camp-meeting he couldn't do no good, and

+besides he was in a sweat to get to the Indian Ocean right off and go to

+work on the pirates.

+

+When we got back to the raft and he come to count up he found he had

+collected eighty-seven dollars and seventy-five cents.  And then he had

+fetched away a three-gallon jug of whisky, too, that he found under a

+wagon when he was starting home through the woods.  The king said,

+take it all around, it laid over any day he'd ever put in in the

+missionarying line.  He said it warn't no use talking, heathens don't

+amount to shucks alongside of pirates to work a camp-meeting with.

+

+The duke was thinking he'd been doing pretty well till the king come

+to show up, but after that he didn't think so so much.  He had set

+up and printed off two little jobs for farmers in that

+printing-office—horse bills—and took the money, four dollars.  And he

+had got in ten dollars' worth of advertisements for the paper, which he

+said he would put in for four dollars if they would pay in advance—so

+they done it. The price of the paper was two dollars a year, but he took

+in three subscriptions for half a dollar apiece on condition of them

+paying him in advance; they were going to pay in cordwood and onions as

+usual, but he said he had just bought the concern and knocked down the

+price as low as he could afford it, and was going to run it for cash.

+ He set up a little piece of poetry, which he made, himself, out of

+his own head—three verses—kind of sweet and saddish—the name of it was,

+"Yes, crush, cold world, this breaking heart"—and he left that all set

+up and ready to print in the paper, and didn't charge nothing for it.

+ Well, he took in nine dollars and a half, and said he'd done a pretty

+square day's work for it.

+

+Then he showed us another little job he'd printed and hadn't charged

+for, because it was for us.  It had a picture of a runaway nigger with

+a bundle on a stick over his shoulder, and "$200 reward" under it.  The

+reading was all about Jim, and just described him to a dot.  It said

+he run away from St. Jacques' plantation, forty mile below New Orleans,

+last winter, and likely went north, and whoever would catch him and send

+him back he could have the reward and expenses.

+

+"Now," says the duke, "after to-night we can run in the daytime if we

+want to.  Whenever we see anybody coming we can tie Jim hand and foot

+with a rope, and lay him in the wigwam and show this handbill and say we

+captured him up the river, and were too poor to travel on a steamboat,

+so we got this little raft on credit from our friends and are going down

+to get the reward.  Handcuffs and chains would look still better on Jim,

+but it wouldn't go well with the story of us being so poor.  Too much

+like jewelry.  Ropes are the correct thing—we must preserve the unities,

+as we say on the boards."

+

+We all said the duke was pretty smart, and there couldn't be no trouble

+about running daytimes.  We judged we could make miles enough that night

+to get out of the reach of the powwow we reckoned the duke's work in

+the printing office was going to make in that little town; then we could

+boom right along if we wanted to.

+

+We laid low and kept still, and never shoved out till nearly ten

+o'clock; then we slid by, pretty wide away from the town, and didn't

+hoist our lantern till we was clear out of sight of it.

+

+When Jim called me to take the watch at four in the morning, he says:

+

+"Huck, does you reck'n we gwyne to run acrost any mo' kings on dis

+trip?"

+

+"No," I says, "I reckon not."

+

+"Well," says he, "dat's all right, den.  I doan' mine one er two kings,

+but dat's enough.  Dis one's powerful drunk, en de duke ain' much

+better."

+

+I found Jim had been trying to get him to talk French, so he could hear

+what it was like; but he said he had been in this country so long, and

+had so much trouble, he'd forgot it.

+

+

+

+

+CHAPTER XXI.

+

+IT was after sun-up now, but we went right on and didn't tie up.  The

+king and the duke turned out by and by looking pretty rusty; but after

+they'd jumped overboard and took a swim it chippered them up a good

+deal. After breakfast the king he took a seat on the corner of the raft,

+and pulled off his boots and rolled up his britches, and let his legs

+dangle in the water, so as to be comfortable, and lit his pipe, and went

+to getting his Romeo and Juliet by heart.  When he had got it pretty

+good him and the duke begun to practice it together.  The duke had to

+learn him over and over again how to say every speech; and he made him

+sigh, and put his hand on his heart, and after a while he said he done

+it pretty well; "only," he says, "you mustn't bellow out Romeo!

+that way, like a bull—you must say it soft and sick and languishy,

+so—R-o-o-meo! that is the idea; for Juliet's a dear sweet mere child of

+a girl, you know, and she doesn't bray like a jackass."

+

+Well, next they got out a couple of long swords that the duke made out

+of oak laths, and begun to practice the sword fight—the duke called

+himself Richard III.; and the way they laid on and pranced around

+the raft was grand to see.  But by and by the king tripped and fell

+overboard, and after that they took a rest, and had a talk about all

+kinds of adventures they'd had in other times along the river.

+

+After dinner the duke says:

+

+"Well, Capet, we'll want to make this a first-class show, you know, so

+I guess we'll add a little more to it.  We want a little something to

+answer encores with, anyway."

+

+"What's onkores, Bilgewater?"

+

+The duke told him, and then says:

+

+"I'll answer by doing the Highland fling or the sailor's hornpipe; and

+you—well, let me see—oh, I've got it—you can do Hamlet's soliloquy."

+

+"Hamlet's which?"

+

+"Hamlet's soliloquy, you know; the most celebrated thing in Shakespeare.

+Ah, it's sublime, sublime!  Always fetches the house.  I haven't got

+it in the book—I've only got one volume—but I reckon I can piece it out

+from memory.  I'll just walk up and down a minute, and see if I can call

+it back from recollection's vaults."

+

+So he went to marching up and down, thinking, and frowning horrible

+every now and then; then he would hoist up his eyebrows; next he would

+squeeze his hand on his forehead and stagger back and kind of moan; next

+he would sigh, and next he'd let on to drop a tear.  It was beautiful

+to see him. By and by he got it.  He told us to give attention.  Then

+he strikes a most noble attitude, with one leg shoved forwards, and his

+arms stretched away up, and his head tilted back, looking up at the sky;

+and then he begins to rip and rave and grit his teeth; and after that,

+all through his speech, he howled, and spread around, and swelled up his

+chest, and just knocked the spots out of any acting ever I see before.

+ This is the speech—I learned it, easy enough, while he was learning it

+to the king:

+

+To be, or not to be; that is the bare bodkin That makes calamity of

+so long life; For who would fardels bear, till Birnam Wood do come

+to Dunsinane, But that the fear of something after death Murders the

+innocent sleep, Great nature's second course, And makes us rather sling

+the arrows of outrageous fortune Than fly to others that we know not of.

+There's the respect must give us pause: Wake Duncan with thy knocking! I

+would thou couldst; For who would bear the whips and scorns of time, The

+oppressor's wrong, the proud man's contumely, The law's delay, and the

+quietus which his pangs might take. In the dead waste and middle of the

+night, when churchyards yawn In customary suits of solemn black, But

+that the undiscovered country from whose bourne no traveler returns,

+Breathes forth contagion on the world, And thus the native hue of

+resolution, like the poor cat i' the adage, Is sicklied o'er with care.

+And all the clouds that lowered o'er our housetops, With this

+regard their currents turn awry, And lose the name of action. 'Tis a

+consummation devoutly to be wished. But soft you, the fair Ophelia: Ope

+not thy ponderous and marble jaws. But get thee to a nunnery&mdash;go!

+

+Well, the old man he liked that speech, and he mighty soon got it so he

+could do it first rate. It seemed like he was just born for it; and when

+he had his hand in and was excited, it was perfectly lovely the way he

+would rip and tear and rair up behind when he was getting it off.

+

+The first chance we got, the duke he had some show bills printed; and

+after that, for two or three days as we floated along, the raft was a

+most uncommon lively place, for there warn't nothing but sword-fighting

+and rehearsing—as the duke called it—going on all the time. One morning,

+when we was pretty well down the State of Arkansaw, we come in sight

+of a little one-horse town in a big bend; so we tied up about

+three-quarters of a mile above it, in the mouth of a crick which was

+shut in like a tunnel by the cypress trees, and all of us but Jim took

+the canoe and went down there to see if there was any chance in that

+place for our show.

+

+We struck it mighty lucky; there was going to be a circus there that

+afternoon, and the country people was already beginning to come in, in

+all kinds of old shackly wagons, and on horses. The circus would leave

+before night, so our show would have a pretty good chance. The duke he

+hired the court house, and we went around and stuck up our bills. They

+read like this:

+

+Shaksperean Revival!!!

+

+Wonderful Attraction!

+

+For One Night Only! The world renowned tragedians,

+

+David Garrick the younger, of Drury Lane Theatre, London,

+

+and

+

+Edmund Kean the elder, of the Royal Haymarket Theatre, Whitechapel,

+Pudding Lane, Piccadilly, London, and the Royal Continental Theatres, in

+their sublime Shaksperean Spectacle entitled The Balcony Scene in

+

+Romeo and Juliet!!!

+

+Romeo...................................... Mr. Garrick.

+

+Juliet..................................... Mr. Kean.

+

+Assisted by the whole strength of the company!

+

+New costumes, new scenery, new appointments!

+

+Also:

+

+The thrilling, masterly, and blood-curdling Broad-sword conflict In

+Richard III.!!!

+

+Richard III................................ Mr. Garrick.

+

+Richmond................................... Mr. Kean.

+

+also:

+

+(by special request,)

+

+Hamlet's Immortal Soliloquy!!

+

+By the Illustrious Kean!

+

+Done by him 300 consecutive nights in Paris!

+

+For One Night Only,

+

+On account of imperative European engagements!

+

+Admission 25 cents; children and servants, 10 cents.

+

+Then we went loafing around the town. The stores and houses was most all

+old shackly dried-up frame concerns that hadn't ever been painted; they

+was set up three or four foot above ground on stilts, so as to be out of

+reach of the water when the river was overflowed. The houses had little

+gardens around them, but they didn't seem to raise hardly anything in

+them but jimpson weeds, and sunflowers, and ash-piles, and old curled-up

+boots and shoes, and pieces of bottles, and rags, and played-out

+tin-ware. The fences was made of different kinds of boards, nailed on

+at different times; and they leaned every which-way, and had gates that

+didn't generly have but one hinge—a leather one. Some of the fences

+had been whitewashed, some time or another, but the duke said it was in

+Clumbus's time, like enough. There was generly hogs in the garden, and

+people driving them out.

+

+All the stores was along one street.  They had white domestic awnings in

+front, and the country people hitched their horses to the awning-posts.

+There was empty drygoods boxes under the awnings, and loafers roosting

+on them all day long, whittling them with their Barlow knives; and

+chawing tobacco, and gaping and yawning and stretching—a mighty ornery

+lot. They generly had on yellow straw hats most as wide as an umbrella,

+but didn't wear no coats nor waistcoats, they called one another Bill,

+and Buck, and Hank, and Joe, and Andy, and talked lazy and drawly, and

+used considerable many cuss words.  There was as many as one loafer

+leaning up against every awning-post, and he most always had his hands

+in his britches-pockets, except when he fetched them out to lend a chaw

+of tobacco or scratch.  What a body was hearing amongst them all the

+time was:

+

+"Gimme a chaw 'v tobacker, Hank."

+

+"Cain't; I hain't got but one chaw left.  Ask Bill."

+

+Maybe Bill he gives him a chaw; maybe he lies and says he ain't got

+none. Some of them kinds of loafers never has a cent in the world, nor a

+chaw of tobacco of their own.  They get all their chawing by borrowing;

+they say to a fellow, "I wisht you'd len' me a chaw, Jack, I jist this

+minute give Ben Thompson the last chaw I had"—which is a lie pretty

+much everytime; it don't fool nobody but a stranger; but Jack ain't no

+stranger, so he says:

+

+"You give him a chaw, did you?  So did your sister's cat's

+grandmother. You pay me back the chaws you've awready borry'd off'n me,

+Lafe Buckner, then I'll loan you one or two ton of it, and won't charge

+you no back intrust, nuther."

+

+"Well, I did pay you back some of it wunst."

+

+"Yes, you did—'bout six chaws.  You borry'd store tobacker and paid back

+nigger-head."

+

+Store tobacco is flat black plug, but these fellows mostly chaws the

+natural leaf twisted.  When they borrow a chaw they don't generly cut it

+off with a knife, but set the plug in between their teeth, and gnaw with

+their teeth and tug at the plug with their hands till they get it in

+two; then sometimes the one that owns the tobacco looks mournful at it

+when it's handed back, and says, sarcastic:

+

+"Here, gimme the chaw, and you take the plug."

+

+All the streets and lanes was just mud; they warn't nothing else but

+mud—mud as black as tar and nigh about a foot deep in some places,

+and two or three inches deep in all the places.  The hogs loafed and

+grunted around everywheres.  You'd see a muddy sow and a litter of pigs

+come lazying along the street and whollop herself right down in the way,

+where folks had to walk around her, and she'd stretch out and shut her

+eyes and wave her ears whilst the pigs was milking her, and look as

+happy as if she was on salary. And pretty soon you'd hear a loafer

+sing out, "Hi!  so boy! sick him, Tige!" and away the sow would go,

+squealing most horrible, with a dog or two swinging to each ear, and

+three or four dozen more a-coming; and then you would see all the

+loafers get up and watch the thing out of sight, and laugh at the fun

+and look grateful for the noise.  Then they'd settle back again till

+there was a dog fight.  There couldn't anything wake them up all over,

+and make them happy all over, like a dog fight—unless it might be

+putting turpentine on a stray dog and setting fire to him, or tying a

+tin pan to his tail and see him run himself to death.

+

+On the river front some of the houses was sticking out over the bank,

+and they was bowed and bent, and about ready to tumble in. The people

+had moved out of them.  The bank was caved away under one corner of some

+others, and that corner was hanging over.  People lived in them yet, but

+it was dangersome, because sometimes a strip of land as wide as a house

+caves in at a time.  Sometimes a belt of land a quarter of a mile deep

+will start in and cave along and cave along till it all caves into the

+river in one summer. Such a town as that has to be always moving back,

+and back, and back, because the river's always gnawing at it.

+

+The nearer it got to noon that day the thicker and thicker was the

+wagons and horses in the streets, and more coming all the time.

+ Families fetched their dinners with them from the country, and eat them

+in the wagons.  There was considerable whisky drinking going on, and I

+seen three fights.  By and by somebody sings out:

+

+"Here comes old Boggs!—in from the country for his little old monthly

+drunk; here he comes, boys!"

+

+All the loafers looked glad; I reckoned they was used to having fun out

+of Boggs.  One of them says:

+

+"Wonder who he's a-gwyne to chaw up this time.  If he'd a-chawed up all

+the men he's ben a-gwyne to chaw up in the last twenty year he'd have

+considerable ruputation now."

+

+Another one says, "I wisht old Boggs 'd threaten me, 'cuz then I'd know

+I warn't gwyne to die for a thousan' year."

+

+Boggs comes a-tearing along on his horse, whooping and yelling like an

+Injun, and singing out:

+

+"Cler the track, thar.  I'm on the waw-path, and the price uv coffins is

+a-gwyne to raise."

+

+He was drunk, and weaving about in his saddle; he was over fifty year

+old, and had a very red face.  Everybody yelled at him and laughed at

+him and sassed him, and he sassed back, and said he'd attend to them and

+lay them out in their regular turns, but he couldn't wait now because

+he'd come to town to kill old Colonel Sherburn, and his motto was, "Meat

+first, and spoon vittles to top off on."

+

+He see me, and rode up and says:

+

+"Whar'd you come f'm, boy?  You prepared to die?"

+

+Then he rode on.  I was scared, but a man says:

+

+"He don't mean nothing; he's always a-carryin' on like that when he's

+drunk.  He's the best naturedest old fool in Arkansaw—never hurt nobody,

+drunk nor sober."

+

+Boggs rode up before the biggest store in town, and bent his head down

+so he could see under the curtain of the awning and yells:

+

+"Come out here, Sherburn! Come out and meet the man you've swindled.

+You're the houn' I'm after, and I'm a-gwyne to have you, too!"

+

+And so he went on, calling Sherburn everything he could lay his tongue

+to, and the whole street packed with people listening and laughing and

+going on.  By and by a proud-looking man about fifty-five—and he was a

+heap the best dressed man in that town, too—steps out of the store, and

+the crowd drops back on each side to let him come.  He says to Boggs,

+mighty ca'm and slow—he says:

+

+"I'm tired of this, but I'll endure it till one o'clock.  Till one

+o'clock, mind—no longer.  If you open your mouth against me only once

+after that time you can't travel so far but I will find you."

+

+Then he turns and goes in.  The crowd looked mighty sober; nobody

+stirred, and there warn't no more laughing.  Boggs rode off

+blackguarding Sherburn as loud as he could yell, all down the street;

+and pretty soon back he comes and stops before the store, still keeping

+it up.  Some men crowded around him and tried to get him to shut up,

+but he wouldn't; they told him it would be one o'clock in about fifteen

+minutes, and so he must go home—he must go right away.  But it didn't

+do no good.  He cussed away with all his might, and throwed his hat down

+in the mud and rode over it, and pretty soon away he went a-raging down

+the street again, with his gray hair a-flying. Everybody that could get

+a chance at him tried their best to coax him off of his horse so they

+could lock him up and get him sober; but it warn't no use—up the street

+he would tear again, and give Sherburn another cussing.  By and by

+somebody says:

+

+"Go for his daughter!—quick, go for his daughter; sometimes he'll listen

+to her.  If anybody can persuade him, she can."

+

+So somebody started on a run.  I walked down street a ways and stopped.

+In about five or ten minutes here comes Boggs again, but not on his

+horse.  He was a-reeling across the street towards me, bare-headed, with

+a friend on both sides of him a-holt of his arms and hurrying him along.

+He was quiet, and looked uneasy; and he warn't hanging back any, but was

+doing some of the hurrying himself.  Somebody sings out:

+

+"Boggs!"

+

+I looked over there to see who said it, and it was that Colonel

+Sherburn. He was standing perfectly still in the street, and had a

+pistol raised in his right hand—not aiming it, but holding it out with

+the barrel tilted up towards the sky.  The same second I see a young

+girl coming on the run, and two men with her.  Boggs and the men turned

+round to see who called him, and when they see the pistol the men

+jumped to one side, and the pistol-barrel come down slow and steady to

+a level—both barrels cocked. Boggs throws up both of his hands and says,

+"O Lord, don't shoot!"  Bang! goes the first shot, and he staggers back,

+clawing at the air—bang! goes the second one, and he tumbles backwards

+on to the ground, heavy and solid, with his arms spread out.  That young

+girl screamed out and comes rushing, and down she throws herself on her

+father, crying, and saying, "Oh, he's killed him, he's killed him!"  The

+crowd closed up around them, and shouldered and jammed one another, with

+their necks stretched, trying to see, and people on the inside trying to

+shove them back and shouting, "Back, back! give him air, give him air!"

+

+Colonel Sherburn he tossed his pistol on to the ground, and turned

+around on his heels and walked off.

+

+They took Boggs to a little drug store, the crowd pressing around just

+the same, and the whole town following, and I rushed and got a good

+place at the window, where I was close to him and could see in.  They

+laid him on the floor and put one large Bible under his head, and opened

+another one and spread it on his breast; but they tore open his shirt

+first, and I seen where one of the bullets went in.  He made about a

+dozen long gasps, his breast lifting the Bible up when he drawed in his

+breath, and letting it down again when he breathed it out—and after that

+he laid still; he was dead.  Then they pulled his daughter away from

+him, screaming and crying, and took her off.  She was about sixteen, and

+very sweet and gentle looking, but awful pale and scared.

+

+Well, pretty soon the whole town was there, squirming and scrouging and

+pushing and shoving to get at the window and have a look, but people

+that had the places wouldn't give them up, and folks behind them was

+saying all the time, "Say, now, you've looked enough, you fellows;

+'tain't right and 'tain't fair for you to stay thar all the time, and

+never give nobody a chance; other folks has their rights as well as

+you."

+

+There was considerable jawing back, so I slid out, thinking maybe

+there was going to be trouble.  The streets was full, and everybody was

+excited. Everybody that seen the shooting was telling how it happened,

+and there was a big crowd packed around each one of these fellows,

+stretching their necks and listening.  One long, lanky man, with long

+hair and a big white fur stovepipe hat on the back of his head, and a

+crooked-handled cane, marked out the places on the ground where Boggs

+stood and where Sherburn stood, and the people following him around from

+one place to t'other and watching everything he done, and bobbing their

+heads to show they understood, and stooping a little and resting their

+hands on their thighs to watch him mark the places on the ground with

+his cane; and then he stood up straight and stiff where Sherburn had

+stood, frowning and having his hat-brim down over his eyes, and sung

+out, "Boggs!" and then fetched his cane down slow to a level, and says

+"Bang!" staggered backwards, says "Bang!" again, and fell down flat on

+his back. The people that had seen the thing said he done it perfect;

+said it was just exactly the way it all happened.  Then as much as a

+dozen people got out their bottles and treated him.

+

+Well, by and by somebody said Sherburn ought to be lynched.  In about a

+minute everybody was saying it; so away they went, mad and yelling, and

+snatching down every clothes-line they come to to do the hanging with.

+

+

+

+

+CHAPTER XXII.

+

+THEY swarmed up towards Sherburn's house, a-whooping and raging like

+Injuns, and everything had to clear the way or get run over and tromped

+to mush, and it was awful to see.  Children was heeling it ahead of the

+mob, screaming and trying to get out of the way; and every window along

+the road was full of women's heads, and there was nigger boys in every

+tree, and bucks and wenches looking over every fence; and as soon as the

+mob would get nearly to them they would break and skaddle back out of

+reach.  Lots of the women and girls was crying and taking on, scared

+most to death.

+

+They swarmed up in front of Sherburn's palings as thick as they could

+jam together, and you couldn't hear yourself think for the noise.  It

+was a little twenty-foot yard.  Some sung out "Tear down the fence! tear

+down the fence!"  Then there was a racket of ripping and tearing and

+smashing, and down she goes, and the front wall of the crowd begins to

+roll in like a wave.

+

+Just then Sherburn steps out on to the roof of his little front porch,

+with a double-barrel gun in his hand, and takes his stand, perfectly

+ca'm and deliberate, not saying a word.  The racket stopped, and the

+wave sucked back.

+

+Sherburn never said a word—just stood there, looking down.  The

+stillness was awful creepy and uncomfortable.  Sherburn run his eye slow

+along the crowd; and wherever it struck the people tried a little to

+out-gaze him, but they couldn't; they dropped their eyes and looked

+sneaky. Then pretty soon Sherburn sort of laughed; not the pleasant

+kind, but the kind that makes you feel like when you are eating bread

+that's got sand in it.

+

+Then he says, slow and scornful:

+

+"The idea of you lynching anybody!  It's amusing.  The idea of you

+thinking you had pluck enough to lynch a man!  Because you're brave

+enough to tar and feather poor friendless cast-out women that come along

+here, did that make you think you had grit enough to lay your hands on a

+man?  Why, a man's safe in the hands of ten thousand of your kind—as

+long as it's daytime and you're not behind him.

+

+"Do I know you?  I know you clear through. I was born and raised in the

+South, and I've lived in the North; so I know the average all around.

+The average man's a coward.  In the North he lets anybody walk over him

+that wants to, and goes home and prays for a humble spirit to bear it.

+In the South one man all by himself, has stopped a stage full of men

+in the daytime, and robbed the lot.  Your newspapers call you a

+brave people so much that you think you are braver than any other

+people—whereas you're just as brave, and no braver.  Why don't your

+juries hang murderers?  Because they're afraid the man's friends will

+shoot them in the back, in the dark—and it's just what they would do.

+

+"So they always acquit; and then a man goes in the night, with a

+hundred masked cowards at his back and lynches the rascal.  Your mistake

+is, that you didn't bring a man with you; that's one mistake, and the

+other is that you didn't come in the dark and fetch your masks.  You

+brought part of a man—Buck Harkness, there—and if you hadn't had him

+to start you, you'd a taken it out in blowing.

+

+"You didn't want to come.  The average man don't like trouble and

+danger. You don't like trouble and danger.  But if only half a

+man—like Buck Harkness, there—shouts 'Lynch him! lynch him!' you're

+afraid to back down—afraid you'll be found out to be what you

+are—cowards—and so you raise a yell, and hang yourselves on to that

+half-a-man's coat-tail, and come raging up here, swearing what big

+things you're going to do. The pitifulest thing out is a mob; that's

+what an army is—a mob; they don't fight with courage that's born in

+them, but with courage that's borrowed from their mass, and from their

+officers.  But a mob without any man at the head of it is beneath

+pitifulness.  Now the thing for you to do is to droop your tails and

+go home and crawl in a hole.  If any real lynching's going to be done it

+will be done in the dark, Southern fashion; and when they come they'll

+bring their masks, and fetch a man along.  Now leave—and take your

+half-a-man with you"—tossing his gun up across his left arm and cocking

+it when he says this.

+

+The crowd washed back sudden, and then broke all apart, and went tearing

+off every which way, and Buck Harkness he heeled it after them, looking

+tolerable cheap.  I could a stayed if I wanted to, but I didn't want to.

+

+I went to the circus and loafed around the back side till the watchman

+went by, and then dived in under the tent.  I had my twenty-dollar gold

+piece and some other money, but I reckoned I better save it, because

+there ain't no telling how soon you are going to need it, away from

+home and amongst strangers that way.  You can't be too careful.  I ain't

+opposed to spending money on circuses when there ain't no other way, but

+there ain't no use in wasting it on them.

+

+It was a real bully circus.  It was the splendidest sight that ever was

+when they all come riding in, two and two, a gentleman and lady, side

+by side, the men just in their drawers and undershirts, and no shoes

+nor stirrups, and resting their hands on their thighs easy and

+comfortable—there must a been twenty of them—and every lady with a

+lovely complexion, and perfectly beautiful, and looking just like a gang

+of real sure-enough queens, and dressed in clothes that cost millions of

+dollars, and just littered with diamonds.  It was a powerful fine sight;

+I never see anything so lovely.  And then one by one they got up

+and stood, and went a-weaving around the ring so gentle and wavy and

+graceful, the men looking ever so tall and airy and straight, with their

+heads bobbing and skimming along, away up there under the tent-roof, and

+every lady's rose-leafy dress flapping soft and silky around her hips,

+and she looking like the most loveliest parasol.

+

+And then faster and faster they went, all of them dancing, first one

+foot out in the air and then the other, the horses leaning more and

+more, and the ringmaster going round and round the center-pole, cracking

+his whip and shouting "Hi!—hi!" and the clown cracking jokes behind

+him; and by and by all hands dropped the reins, and every lady put her

+knuckles on her hips and every gentleman folded his arms, and then how

+the horses did lean over and hump themselves!  And so one after the

+other they all skipped off into the ring, and made the sweetest bow I

+ever see, and then scampered out, and everybody clapped their hands and

+went just about wild.

+

+Well, all through the circus they done the most astonishing things; and

+all the time that clown carried on so it most killed the people.  The

+ringmaster couldn't ever say a word to him but he was back at him quick

+as a wink with the funniest things a body ever said; and how he ever

+could think of so many of them, and so sudden and so pat, was what I

+couldn't noway understand. Why, I couldn't a thought of them in a year.

+And by and by a drunk man tried to get into the ring—said he wanted to

+ride; said he could ride as well as anybody that ever was.  They argued

+and tried to keep him out, but he wouldn't listen, and the whole show

+come to a standstill.  Then the people begun to holler at him and make

+fun of him, and that made him mad, and he begun to rip and tear; so that

+stirred up the people, and a lot of men begun to pile down off of the

+benches and swarm towards the ring, saying, "Knock him down! throw him

+out!" and one or two women begun to scream.  So, then, the ringmaster

+he made a little speech, and said he hoped there wouldn't be no

+disturbance, and if the man would promise he wouldn't make no more

+trouble he would let him ride if he thought he could stay on the horse.

+ So everybody laughed and said all right, and the man got on. The minute

+he was on, the horse begun to rip and tear and jump and cavort around,

+with two circus men hanging on to his bridle trying to hold him, and the

+drunk man hanging on to his neck, and his heels flying in the air every

+jump, and the whole crowd of people standing up shouting and laughing

+till tears rolled down.  And at last, sure enough, all the circus men

+could do, the horse broke loose, and away he went like the very nation,

+round and round the ring, with that sot laying down on him and hanging

+to his neck, with first one leg hanging most to the ground on one side,

+and then t'other one on t'other side, and the people just crazy.  It

+warn't funny to me, though; I was all of a tremble to see his danger.

+ But pretty soon he struggled up astraddle and grabbed the bridle,

+a-reeling this way and that; and the next minute he sprung up and

+dropped the bridle and stood! and the horse a-going like a house afire

+too.  He just stood up there, a-sailing around as easy and comfortable

+as if he warn't ever drunk in his life—and then he begun to pull off his

+clothes and sling them.  He shed them so thick they kind of clogged up

+the air, and altogether he shed seventeen suits. And, then, there he

+was, slim and handsome, and dressed the gaudiest and prettiest you

+ever saw, and he lit into that horse with his whip and made him fairly

+hum—and finally skipped off, and made his bow and danced off to

+the dressing-room, and everybody just a-howling with pleasure and

+astonishment.

+

+Then the ringmaster he see how he had been fooled, and he was the

+sickest ringmaster you ever see, I reckon.  Why, it was one of his own

+men!  He had got up that joke all out of his own head, and never let on

+to nobody. Well, I felt sheepish enough to be took in so, but I wouldn't

+a been in that ringmaster's place, not for a thousand dollars.  I don't

+know; there may be bullier circuses than what that one was, but I

+never struck them yet. Anyways, it was plenty good enough for me; and

+wherever I run across it, it can have all of my custom every time.

+

+Well, that night we had our show; but there warn't only about twelve

+people there—just enough to pay expenses.  And they laughed all the

+time, and that made the duke mad; and everybody left, anyway, before

+the show was over, but one boy which was asleep.  So the duke said these

+Arkansaw lunkheads couldn't come up to Shakespeare; what they wanted

+was low comedy—and maybe something ruther worse than low comedy, he

+reckoned.  He said he could size their style.  So next morning he got

+some big sheets of wrapping paper and some black paint, and drawed off

+some handbills, and stuck them up all over the village.  The bills said:

+

+

+

+

+CHAPTER XXIII.

+

+WELL, all day him and the king was hard at it, rigging up a stage and

+a curtain and a row of candles for footlights; and that night the house

+was jam full of men in no time.  When the place couldn't hold no more,

+the duke he quit tending door and went around the back way and come on

+to the stage and stood up before the curtain and made a little speech,

+and praised up this tragedy, and said it was the most thrillingest one

+that ever was; and so he went on a-bragging about the tragedy, and about

+Edmund Kean the Elder, which was to play the main principal part in it;

+and at last when he'd got everybody's expectations up high enough, he

+rolled up the curtain, and the next minute the king come a-prancing

+out on all fours, naked; and he was painted all over,

+ring-streaked-and-striped, all sorts of colors, as splendid as a

+rainbow.  And—but never mind the rest of his outfit; it was just wild,

+but it was awful funny. The people most killed themselves laughing; and

+when the king got done capering and capered off behind the scenes, they

+roared and clapped and stormed and haw-hawed till he come back and done

+it over again, and after that they made him do it another time. Well, it

+would make a cow laugh to see the shines that old idiot cut.

+

+Then the duke he lets the curtain down, and bows to the people, and says

+the great tragedy will be performed only two nights more, on accounts of

+pressing London engagements, where the seats is all sold already for it

+in Drury Lane; and then he makes them another bow, and says if he has

+succeeded in pleasing them and instructing them, he will be deeply

+obleeged if they will mention it to their friends and get them to come

+and see it.

+

+Twenty people sings out:

+

+"What, is it over?  Is that all?"

+

+The duke says yes.  Then there was a fine time.  Everybody sings

+out, "Sold!" and rose up mad, and was a-going for that stage and them

+tragedians.  But a big, fine looking man jumps up on a bench and shouts:

+

+"Hold on!  Just a word, gentlemen."  They stopped to listen.  "We are

+sold—mighty badly sold.  But we don't want to be the laughing stock of

+this whole town, I reckon, and never hear the last of this thing as long

+as we live.  No.  What we want is to go out of here quiet, and talk

+this show up, and sell the rest of the town!  Then we'll all be in the

+same boat.  Ain't that sensible?" ("You bet it is!—the jedge is right!"

+everybody sings out.) "All right, then—not a word about any sell.  Go

+along home, and advise everybody to come and see the tragedy."

+

+Next day you couldn't hear nothing around that town but how splendid

+that show was.  House was jammed again that night, and we sold this

+crowd the same way.  When me and the king and the duke got home to the

+raft we all had a supper; and by and by, about midnight, they made Jim

+and me back her out and float her down the middle of the river, and

+fetch her in and hide her about two mile below town.

+

+The third night the house was crammed again—and they warn't new-comers

+this time, but people that was at the show the other two nights.  I

+stood by the duke at the door, and I see that every man that went in had

+his pockets bulging, or something muffled up under his coat—and I see it

+warn't no perfumery, neither, not by a long sight.  I smelt sickly eggs

+by the barrel, and rotten cabbages, and such things; and if I know the

+signs of a dead cat being around, and I bet I do, there was sixty-four

+of them went in.  I shoved in there for a minute, but it was too various

+for me; I couldn't stand it.  Well, when the place couldn't hold no more

+people the duke he give a fellow a quarter and told him to tend door

+for him a minute, and then he started around for the stage door, I after

+him; but the minute we turned the corner and was in the dark he says:

+

+"Walk fast now till you get away from the houses, and then shin for the

+raft like the dickens was after you!"

+

+I done it, and he done the same.  We struck the raft at the same time,

+and in less than two seconds we was gliding down stream, all dark and

+still, and edging towards the middle of the river, nobody saying a

+word. I reckoned the poor king was in for a gaudy time of it with the

+audience, but nothing of the sort; pretty soon he crawls out from under

+the wigwam, and says:

+

+"Well, how'd the old thing pan out this time, duke?"  He hadn't been

+up-town at all.

+

+We never showed a light till we was about ten mile below the village.

+Then we lit up and had a supper, and the king and the duke fairly

+laughed their bones loose over the way they'd served them people.  The

+duke says:

+

+"Greenhorns, flatheads!  I knew the first house would keep mum and let

+the rest of the town get roped in; and I knew they'd lay for us the

+third night, and consider it was their turn now.  Well, it is their

+turn, and I'd give something to know how much they'd take for it.  I

+would just like to know how they're putting in their opportunity.

+ They can turn it into a picnic if they want to—they brought plenty

+provisions."

+

+Them rapscallions took in four hundred and sixty-five dollars in that

+three nights.  I never see money hauled in by the wagon-load like that

+before.  By and by, when they was asleep and snoring, Jim says:

+

+"Don't it s'prise you de way dem kings carries on, Huck?"

+

+"No," I says, "it don't."

+

+"Why don't it, Huck?"

+

+"Well, it don't, because it's in the breed.  I reckon they're all

+alike."

+

+"But, Huck, dese kings o' ourn is reglar rapscallions; dat's jist what

+dey is; dey's reglar rapscallions."

+

+"Well, that's what I'm a-saying; all kings is mostly rapscallions, as

+fur as I can make out."

+

+"Is dat so?"

+

+"You read about them once—you'll see.  Look at Henry the Eight; this 'n

+'s a Sunday-school Superintendent to him.  And look at Charles Second,

+and Louis Fourteen, and Louis Fifteen, and James Second, and Edward

+Second, and Richard Third, and forty more; besides all them Saxon

+heptarchies that used to rip around so in old times and raise Cain.  My,

+you ought to seen old Henry the Eight when he was in bloom.  He was a

+blossom.  He used to marry a new wife every day, and chop off her head

+next morning.  And he would do it just as indifferent as if he was

+ordering up eggs.  'Fetch up Nell Gwynn,' he says.  They fetch her up.

+Next morning, 'Chop off her head!'  And they chop it off.  'Fetch up

+Jane Shore,' he says; and up she comes, Next morning, 'Chop off her

+head'—and they chop it off.  'Ring up Fair Rosamun.'  Fair Rosamun

+answers the bell.  Next morning, 'Chop off her head.'  And he made every

+one of them tell him a tale every night; and he kept that up till he had

+hogged a thousand and one tales that way, and then he put them all in a

+book, and called it Domesday Book—which was a good name and stated the

+case.  You don't know kings, Jim, but I know them; and this old rip

+of ourn is one of the cleanest I've struck in history.  Well, Henry he

+takes a notion he wants to get up some trouble with this country. How

+does he go at it—give notice?—give the country a show?  No.  All of a

+sudden he heaves all the tea in Boston Harbor overboard, and whacks

+out a declaration of independence, and dares them to come on.  That was

+his style—he never give anybody a chance.  He had suspicions of his

+father, the Duke of Wellington.  Well, what did he do?  Ask him to show

+up?  No—drownded him in a butt of mamsey, like a cat.  S'pose people

+left money laying around where he was—what did he do?  He collared it.

+ S'pose he contracted to do a thing, and you paid him, and didn't set

+down there and see that he done it—what did he do?  He always done the

+other thing. S'pose he opened his mouth—what then?  If he didn't shut it

+up powerful quick he'd lose a lie every time.  That's the kind of a bug

+Henry was; and if we'd a had him along 'stead of our kings he'd a fooled

+that town a heap worse than ourn done.  I don't say that ourn is lambs,

+because they ain't, when you come right down to the cold facts; but they

+ain't nothing to that old ram, anyway.  All I say is, kings is kings,

+and you got to make allowances.  Take them all around, they're a mighty

+ornery lot. It's the way they're raised."

+

+"But dis one do smell so like de nation, Huck."

+

+"Well, they all do, Jim.  We can't help the way a king smells; history

+don't tell no way."

+

+"Now de duke, he's a tolerble likely man in some ways."

+

+"Yes, a duke's different.  But not very different.  This one's

+a middling hard lot for a duke.  When he's drunk there ain't no

+near-sighted man could tell him from a king."

+

+"Well, anyways, I doan' hanker for no mo' un um, Huck.  Dese is all I

+kin stan'."

+

+"It's the way I feel, too, Jim.  But we've got them on our hands, and we

+got to remember what they are, and make allowances.  Sometimes I wish we

+could hear of a country that's out of kings."

+

+What was the use to tell Jim these warn't real kings and dukes?  It

+wouldn't a done no good; and, besides, it was just as I said:  you

+couldn't tell them from the real kind.

+

+I went to sleep, and Jim didn't call me when it was my turn.  He often

+done that.  When I waked up just at daybreak he was sitting there with

+his head down betwixt his knees, moaning and mourning to himself.  I

+didn't take notice nor let on.  I knowed what it was about.  He was

+thinking about his wife and his children, away up yonder, and he was low

+and homesick; because he hadn't ever been away from home before in his

+life; and I do believe he cared just as much for his people as white

+folks does for their'n.  It don't seem natural, but I reckon it's so.

+ He was often moaning and mourning that way nights, when he judged I

+was asleep, and saying, "Po' little 'Lizabeth! po' little Johnny! it's

+mighty hard; I spec' I ain't ever gwyne to see you no mo', no mo'!"  He

+was a mighty good nigger, Jim was.

+

+But this time I somehow got to talking to him about his wife and young

+ones; and by and by he says:

+

+"What makes me feel so bad dis time 'uz bekase I hear sumpn over yonder

+on de bank like a whack, er a slam, while ago, en it mine me er de time

+I treat my little 'Lizabeth so ornery.  She warn't on'y 'bout fo' year

+ole, en she tuck de sk'yarlet fever, en had a powful rough spell; but

+she got well, en one day she was a-stannin' aroun', en I says to her, I

+says:

+

+"'Shet de do'.'

+

+"She never done it; jis' stood dah, kiner smilin' up at me.  It make me

+mad; en I says agin, mighty loud, I says:

+

+"'Doan' you hear me?  Shet de do'!'

+

+"She jis stood de same way, kiner smilin' up.  I was a-bilin'!  I says:

+

+"'I lay I make you mine!'

+

+"En wid dat I fetch' her a slap side de head dat sont her a-sprawlin'.

+Den I went into de yuther room, en 'uz gone 'bout ten minutes; en when

+I come back dah was dat do' a-stannin' open yit, en dat chile stannin'

+mos' right in it, a-lookin' down and mournin', en de tears runnin' down.

+ My, but I wuz mad!  I was a-gwyne for de chile, but jis' den—it was a

+do' dat open innerds—jis' den, 'long come de wind en slam it to, behine

+de chile, ker-BLAM!—en my lan', de chile never move'!  My breff mos'

+hop outer me; en I feel so—so—I doan' know HOW I feel.  I crope out,

+all a-tremblin', en crope aroun' en open de do' easy en slow, en poke my

+head in behine de chile, sof' en still, en all uv a sudden I says POW!

+jis' as loud as I could yell.  She never budge!  Oh, Huck, I bust out

+a-cryin' en grab her up in my arms, en say, 'Oh, de po' little thing!

+ De Lord God Amighty fogive po' ole Jim, kaze he never gwyne to fogive

+hisself as long's he live!'  Oh, she was plumb deef en dumb, Huck, plumb

+deef en dumb—en I'd ben a-treat'n her so!"

+

+

+

+

+CHAPTER XXIV.

+

+NEXT day, towards night, we laid up under a little willow towhead out in

+the middle, where there was a village on each side of the river, and the

+duke and the king begun to lay out a plan for working them towns.  Jim

+he spoke to the duke, and said he hoped it wouldn't take but a few

+hours, because it got mighty heavy and tiresome to him when he had to

+lay all day in the wigwam tied with the rope.  You see, when we left him

+all alone we had to tie him, because if anybody happened on to him all

+by himself and not tied it wouldn't look much like he was a runaway

+nigger, you know. So the duke said it was kind of hard to have to lay

+roped all day, and he'd cipher out some way to get around it.

+

+He was uncommon bright, the duke was, and he soon struck it.  He dressed

+Jim up in King Lear's outfit—it was a long curtain-calico gown, and a

+white horse-hair wig and whiskers; and then he took his theater paint

+and painted Jim's face and hands and ears and neck all over a dead,

+dull, solid blue, like a man that's been drownded nine days.  Blamed if

+he warn't the horriblest looking outrage I ever see.  Then the duke took

+and wrote out a sign on a shingle so:

+

+Sick Arab—but harmless when not out of his head.

+

+And he nailed that shingle to a lath, and stood the lath up four or five

+foot in front of the wigwam.  Jim was satisfied.  He said it was a sight

+better than lying tied a couple of years every day, and trembling all

+over every time there was a sound.  The duke told him to make himself

+free and easy, and if anybody ever come meddling around, he must hop

+out of the wigwam, and carry on a little, and fetch a howl or two like

+a wild beast, and he reckoned they would light out and leave him alone.

+ Which was sound enough judgment; but you take the average man, and he

+wouldn't wait for him to howl.  Why, he didn't only look like he was

+dead, he looked considerable more than that.

+

+These rapscallions wanted to try the Nonesuch again, because there was

+so much money in it, but they judged it wouldn't be safe, because maybe

+the news might a worked along down by this time.  They couldn't hit no

+project that suited exactly; so at last the duke said he reckoned he'd

+lay off and work his brains an hour or two and see if he couldn't put up

+something on the Arkansaw village; and the king he allowed he would drop

+over to t'other village without any plan, but just trust in Providence

+to lead him the profitable way—meaning the devil, I reckon.  We had all

+bought store clothes where we stopped last; and now the king put his'n

+on, and he told me to put mine on.  I done it, of course.  The king's

+duds was all black, and he did look real swell and starchy.  I never

+knowed how clothes could change a body before.  Why, before, he looked

+like the orneriest old rip that ever was; but now, when he'd take off

+his new white beaver and make a bow and do a smile, he looked that grand

+and good and pious that you'd say he had walked right out of the ark,

+and maybe was old Leviticus himself.  Jim cleaned up the canoe, and I

+got my paddle ready.  There was a big steamboat laying at the shore away

+up under the point, about three mile above the town—been there a couple

+of hours, taking on freight.  Says the king:

+

+"Seein' how I'm dressed, I reckon maybe I better arrive down from St.

+Louis or Cincinnati, or some other big place.  Go for the steamboat,

+Huckleberry; we'll come down to the village on her."

+

+I didn't have to be ordered twice to go and take a steamboat ride.

+ I fetched the shore a half a mile above the village, and then went

+scooting along the bluff bank in the easy water.  Pretty soon we come to

+a nice innocent-looking young country jake setting on a log swabbing the

+sweat off of his face, for it was powerful warm weather; and he had a

+couple of big carpet-bags by him.

+

+"Run her nose in shore," says the king.  I done it.  "Wher' you bound

+for, young man?"

+

+"For the steamboat; going to Orleans."

+

+"Git aboard," says the king.  "Hold on a minute, my servant 'll he'p you

+with them bags.  Jump out and he'p the gentleman, Adolphus"—meaning me,

+I see.

+

+I done so, and then we all three started on again.  The young chap was

+mighty thankful; said it was tough work toting his baggage such weather.

+He asked the king where he was going, and the king told him he'd come

+down the river and landed at the other village this morning, and now he

+was going up a few mile to see an old friend on a farm up there.  The

+young fellow says:

+

+"When I first see you I says to myself, 'It's Mr. Wilks, sure, and he

+come mighty near getting here in time.'  But then I says again, 'No, I

+reckon it ain't him, or else he wouldn't be paddling up the river.'  You

+ain't him, are you?"

+

+"No, my name's Blodgett—Elexander Blodgett—Reverend Elexander

+Blodgett, I s'pose I must say, as I'm one o' the Lord's poor servants.

+ But still I'm jist as able to be sorry for Mr. Wilks for not arriving

+in time, all the same, if he's missed anything by it—which I hope he

+hasn't."

+

+"Well, he don't miss any property by it, because he'll get that all

+right; but he's missed seeing his brother Peter die—which he mayn't

+mind, nobody can tell as to that—but his brother would a give anything

+in this world to see him before he died; never talked about nothing

+else all these three weeks; hadn't seen him since they was boys

+together—and hadn't ever seen his brother William at all—that's the deef

+and dumb one—William ain't more than thirty or thirty-five.  Peter and

+George were the only ones that come out here; George was the married

+brother; him and his wife both died last year.  Harvey and William's the

+only ones that's left now; and, as I was saying, they haven't got here

+in time."

+

+"Did anybody send 'em word?"

+

+"Oh, yes; a month or two ago, when Peter was first took; because Peter

+said then that he sorter felt like he warn't going to get well this

+time. You see, he was pretty old, and George's g'yirls was too young to

+be much company for him, except Mary Jane, the red-headed one; and so he

+was kinder lonesome after George and his wife died, and didn't seem

+to care much to live.  He most desperately wanted to see Harvey—and

+William, too, for that matter—because he was one of them kind that can't

+bear to make a will.  He left a letter behind for Harvey, and said he'd

+told in it where his money was hid, and how he wanted the rest of the

+property divided up so George's g'yirls would be all right—for George

+didn't leave nothing.  And that letter was all they could get him to put

+a pen to."

+

+"Why do you reckon Harvey don't come?  Wher' does he live?"

+

+"Oh, he lives in England—Sheffield—preaches there—hasn't ever been in

+this country.  He hasn't had any too much time—and besides he mightn't a

+got the letter at all, you know."

+

+"Too bad, too bad he couldn't a lived to see his brothers, poor soul.

+You going to Orleans, you say?"

+

+"Yes, but that ain't only a part of it.  I'm going in a ship, next

+Wednesday, for Ryo Janeero, where my uncle lives."

+

+"It's a pretty long journey.  But it'll be lovely; wisht I was a-going.

+Is Mary Jane the oldest?  How old is the others?"

+

+"Mary Jane's nineteen, Susan's fifteen, and Joanna's about

+fourteen—that's the one that gives herself to good works and has a

+hare-lip."

+

+"Poor things! to be left alone in the cold world so."

+

+"Well, they could be worse off.  Old Peter had friends, and they

+ain't going to let them come to no harm.  There's Hobson, the Babtis'

+preacher; and Deacon Lot Hovey, and Ben Rucker, and Abner Shackleford,

+and Levi Bell, the lawyer; and Dr. Robinson, and their wives, and the

+widow Bartley, and—well, there's a lot of them; but these are the ones

+that Peter was thickest with, and used to write about sometimes, when

+he wrote home; so Harvey 'll know where to look for friends when he gets

+here."

+

+Well, the old man went on asking questions till he just fairly emptied

+that young fellow.  Blamed if he didn't inquire about everybody and

+everything in that blessed town, and all about the Wilkses; and about

+Peter's business—which was a tanner; and about George's—which was a

+carpenter; and about Harvey's—which was a dissentering minister; and so

+on, and so on.  Then he says:

+

+"What did you want to walk all the way up to the steamboat for?"

+

+"Because she's a big Orleans boat, and I was afeard she mightn't stop

+there.  When they're deep they won't stop for a hail.  A Cincinnati boat

+will, but this is a St. Louis one."

+

+"Was Peter Wilks well off?"

+

+"Oh, yes, pretty well off.  He had houses and land, and it's reckoned he

+left three or four thousand in cash hid up som'ers."

+

+"When did you say he died?"

+

+"I didn't say, but it was last night."

+

+"Funeral to-morrow, likely?"

+

+"Yes, 'bout the middle of the day."

+

+"Well, it's all terrible sad; but we've all got to go, one time or

+another. So what we want to do is to be prepared; then we're all right."

+

+"Yes, sir, it's the best way.  Ma used to always say that."

+

+When we struck the boat she was about done loading, and pretty soon she

+got off.  The king never said nothing about going aboard, so I lost

+my ride, after all.  When the boat was gone the king made me paddle up

+another mile to a lonesome place, and then he got ashore and says:

+

+"Now hustle back, right off, and fetch the duke up here, and the new

+carpet-bags.  And if he's gone over to t'other side, go over there and

+git him.  And tell him to git himself up regardless.  Shove along, now."

+

+I see what he was up to; but I never said nothing, of course.  When

+I got back with the duke we hid the canoe, and then they set down on a

+log, and the king told him everything, just like the young fellow had

+said it—every last word of it.  And all the time he was a-doing it he

+tried to talk like an Englishman; and he done it pretty well, too, for

+a slouch. I can't imitate him, and so I ain't a-going to try to; but he

+really done it pretty good.  Then he says:

+

+"How are you on the deef and dumb, Bilgewater?"

+

+The duke said, leave him alone for that; said he had played a deef

+and dumb person on the histronic boards.  So then they waited for a

+steamboat.

+

+About the middle of the afternoon a couple of little boats come along,

+but they didn't come from high enough up the river; but at last there

+was a big one, and they hailed her.  She sent out her yawl, and we went

+aboard, and she was from Cincinnati; and when they found we only wanted

+to go four or five mile they was booming mad, and gave us a cussing, and

+said they wouldn't land us.  But the king was ca'm.  He says:

+

+"If gentlemen kin afford to pay a dollar a mile apiece to be took on and

+put off in a yawl, a steamboat kin afford to carry 'em, can't it?"

+

+So they softened down and said it was all right; and when we got to the

+village they yawled us ashore.  About two dozen men flocked down when

+they see the yawl a-coming, and when the king says:

+

+"Kin any of you gentlemen tell me wher' Mr. Peter Wilks lives?" they

+give a glance at one another, and nodded their heads, as much as to say,

+"What d' I tell you?"  Then one of them says, kind of soft and gentle:

+

+"I'm sorry sir, but the best we can do is to tell you where he did

+live yesterday evening."

+

+Sudden as winking the ornery old cretur went an to smash, and fell up

+against the man, and put his chin on his shoulder, and cried down his

+back, and says:

+

+"Alas, alas, our poor brother—gone, and we never got to see him; oh,

+it's too, too hard!"

+

+Then he turns around, blubbering, and makes a lot of idiotic signs to

+the duke on his hands, and blamed if he didn't drop a carpet-bag and

+bust out a-crying.  If they warn't the beatenest lot, them two frauds,

+that ever I struck.

+

+Well, the men gathered around and sympathized with them, and said all

+sorts of kind things to them, and carried their carpet-bags up the hill

+for them, and let them lean on them and cry, and told the king all about

+his brother's last moments, and the king he told it all over again on

+his hands to the duke, and both of them took on about that dead tanner

+like they'd lost the twelve disciples.  Well, if ever I struck anything

+like it, I'm a nigger. It was enough to make a body ashamed of the human

+race.

+

+

+

+

+CHAPTER XXV.

+

+THE news was all over town in two minutes, and you could see the people

+tearing down on the run from every which way, some of them putting on

+their coats as they come.  Pretty soon we was in the middle of a crowd,

+and the noise of the tramping was like a soldier march.  The windows and

+dooryards was full; and every minute somebody would say, over a fence:

+

+"Is it them?"

+

+And somebody trotting along with the gang would answer back and say:

+

+"You bet it is."

+

+When we got to the house the street in front of it was packed, and the

+three girls was standing in the door.  Mary Jane was red-headed, but

+that don't make no difference, she was most awful beautiful, and her

+face and her eyes was all lit up like glory, she was so glad her uncles

+was come. The king he spread his arms, and Mary Jane she jumped for

+them, and the hare-lip jumped for the duke, and there they had it!

+ Everybody most, leastways women, cried for joy to see them meet again

+at last and have such good times.

+

+Then the king he hunched the duke private—I see him do it—and then he

+looked around and see the coffin, over in the corner on two chairs; so

+then him and the duke, with a hand across each other's shoulder, and

+t'other hand to their eyes, walked slow and solemn over there, everybody

+dropping back to give them room, and all the talk and noise stopping,

+people saying "Sh!" and all the men taking their hats off and drooping

+their heads, so you could a heard a pin fall.  And when they got there

+they bent over and looked in the coffin, and took one sight, and then

+they bust out a-crying so you could a heard them to Orleans, most; and

+then they put their arms around each other's necks, and hung their chins

+over each other's shoulders; and then for three minutes, or maybe four,

+I never see two men leak the way they done.  And, mind you, everybody

+was doing the same; and the place was that damp I never see anything

+like it. Then one of them got on one side of the coffin, and t'other on

+t'other side, and they kneeled down and rested their foreheads on the

+coffin, and let on to pray all to themselves.  Well, when it come

+to that it worked the crowd like you never see anything like it, and

+everybody broke down and went to sobbing right out loud—the poor girls,

+too; and every woman, nearly, went up to the girls, without saying a

+word, and kissed them, solemn, on the forehead, and then put their hand

+on their head, and looked up towards the sky, with the tears running

+down, and then busted out and went off sobbing and swabbing, and give

+the next woman a show.  I never see anything so disgusting.

+

+Well, by and by the king he gets up and comes forward a little, and

+works himself up and slobbers out a speech, all full of tears and

+flapdoodle about its being a sore trial for him and his poor brother

+to lose the diseased, and to miss seeing diseased alive after the long

+journey of four thousand mile, but it's a trial that's sweetened and

+sanctified to us by this dear sympathy and these holy tears, and so he

+thanks them out of his heart and out of his brother's heart, because out

+of their mouths they can't, words being too weak and cold, and all that

+kind of rot and slush, till it was just sickening; and then he blubbers

+out a pious goody-goody Amen, and turns himself loose and goes to crying

+fit to bust.

+

+And the minute the words were out of his mouth somebody over in the

+crowd struck up the doxolojer, and everybody joined in with all their

+might, and it just warmed you up and made you feel as good as church

+letting out. Music is a good thing; and after all that soul-butter and

+hogwash I never see it freshen up things so, and sound so honest and

+bully.

+

+Then the king begins to work his jaw again, and says how him and his

+nieces would be glad if a few of the main principal friends of the

+family would take supper here with them this evening, and help set up

+with the ashes of the diseased; and says if his poor brother laying

+yonder could speak he knows who he would name, for they was names that

+was very dear to him, and mentioned often in his letters; and so he will

+name the same, to wit, as follows, vizz.:—Rev. Mr. Hobson, and Deacon

+Lot Hovey, and Mr. Ben Rucker, and Abner Shackleford, and Levi Bell, and

+Dr. Robinson, and their wives, and the widow Bartley.

+

+Rev. Hobson and Dr. Robinson was down to the end of the town a-hunting

+together—that is, I mean the doctor was shipping a sick man to t'other

+world, and the preacher was pinting him right.  Lawyer Bell was away up

+to Louisville on business.  But the rest was on hand, and so they all

+come and shook hands with the king and thanked him and talked to him;

+and then they shook hands with the duke and didn't say nothing, but just

+kept a-smiling and bobbing their heads like a passel of sapheads whilst

+he made all sorts of signs with his hands and said "Goo-goo—goo-goo-goo"

+all the time, like a baby that can't talk.

+

+So the king he blattered along, and managed to inquire about pretty

+much everybody and dog in town, by his name, and mentioned all sorts

+of little things that happened one time or another in the town, or to

+George's family, or to Peter.  And he always let on that Peter wrote him

+the things; but that was a lie:  he got every blessed one of them out of

+that young flathead that we canoed up to the steamboat.

+

+Then Mary Jane she fetched the letter her father left behind, and the

+king he read it out loud and cried over it.  It give the dwelling-house

+and three thousand dollars, gold, to the girls; and it give the tanyard

+(which was doing a good business), along with some other houses and

+land (worth about seven thousand), and three thousand dollars in gold

+to Harvey and William, and told where the six thousand cash was hid down

+cellar.  So these two frauds said they'd go and fetch it up, and have

+everything square and above-board; and told me to come with a candle.

+ We shut the cellar door behind us, and when they found the bag

+they spilt it out on the floor, and it was a lovely sight, all them

+yaller-boys.  My, the way the king's eyes did shine!  He slaps the duke

+on the shoulder and says:

+

+"Oh, this ain't bully nor noth'n!  Oh, no, I reckon not!  Why,

+bully, it beats the Nonesuch, don't it?"

+

+The duke allowed it did.  They pawed the yaller-boys, and sifted them

+through their fingers and let them jingle down on the floor; and the

+king says:

+

+"It ain't no use talkin'; bein' brothers to a rich dead man and

+representatives of furrin heirs that's got left is the line for you and

+me, Bilge.  Thish yer comes of trust'n to Providence.  It's the best

+way, in the long run.  I've tried 'em all, and ther' ain't no better

+way."

+

+Most everybody would a been satisfied with the pile, and took it on

+trust; but no, they must count it.  So they counts it, and it comes out

+four hundred and fifteen dollars short.  Says the king:

+

+"Dern him, I wonder what he done with that four hundred and fifteen

+dollars?"

+

+They worried over that awhile, and ransacked all around for it.  Then

+the duke says:

+

+"Well, he was a pretty sick man, and likely he made a mistake—I reckon

+that's the way of it.  The best way's to let it go, and keep still about

+it.  We can spare it."

+

+"Oh, shucks, yes, we can spare it.  I don't k'yer noth'n 'bout

+that—it's the count I'm thinkin' about.  We want to be awful square

+and open and above-board here, you know.  We want to lug this h-yer

+money up stairs and count it before everybody—then ther' ain't noth'n

+suspicious.  But when the dead man says ther's six thous'n dollars, you

+know, we don't want to—"

+

+"Hold on," says the duke.  "Le's make up the deffisit," and he begun to

+haul out yaller-boys out of his pocket.

+

+"It's a most amaz'n' good idea, duke—you have got a rattlin' clever

+head on you," says the king.  "Blest if the old Nonesuch ain't a heppin'

+us out agin," and he begun to haul out yaller-jackets and stack them

+up.

+

+It most busted them, but they made up the six thousand clean and clear.

+

+"Say," says the duke, "I got another idea.  Le's go up stairs and count

+this money, and then take and give it to the girls."

+

+"Good land, duke, lemme hug you!  It's the most dazzling idea 'at ever a

+man struck.  You have cert'nly got the most astonishin' head I ever see.

+Oh, this is the boss dodge, ther' ain't no mistake 'bout it.  Let 'em

+fetch along their suspicions now if they want to—this 'll lay 'em out."

+

+When we got up-stairs everybody gethered around the table, and the king

+he counted it and stacked it up, three hundred dollars in a pile—twenty

+elegant little piles.  Everybody looked hungry at it, and licked their

+chops.  Then they raked it into the bag again, and I see the king begin

+to swell himself up for another speech.  He says:

+

+"Friends all, my poor brother that lays yonder has done generous by

+them that's left behind in the vale of sorrers.  He has done generous by

+these yer poor little lambs that he loved and sheltered, and that's left

+fatherless and motherless.  Yes, and we that knowed him knows that he

+would a done more generous by 'em if he hadn't ben afeard o' woundin'

+his dear William and me.  Now, wouldn't he?  Ther' ain't no question

+'bout it in my mind.  Well, then, what kind o' brothers would it be

+that 'd stand in his way at sech a time?  And what kind o' uncles would

+it be that 'd rob—yes, rob—sech poor sweet lambs as these 'at he loved

+so at sech a time?  If I know William—and I think I do—he—well, I'll

+jest ask him." He turns around and begins to make a lot of signs to

+the duke with his hands, and the duke he looks at him stupid and

+leather-headed a while; then all of a sudden he seems to catch his

+meaning, and jumps for the king, goo-gooing with all his might for joy,

+and hugs him about fifteen times before he lets up.  Then the king says,

+"I knowed it; I reckon that 'll convince anybody the way he feels

+about it.  Here, Mary Jane, Susan, Joanner, take the money—take it

+all.  It's the gift of him that lays yonder, cold but joyful."

+

+Mary Jane she went for him, Susan and the hare-lip went for the

+duke, and then such another hugging and kissing I never see yet.  And

+everybody crowded up with the tears in their eyes, and most shook the

+hands off of them frauds, saying all the time:

+

+"You dear good souls!—how lovely!—how could you!"

+

+Well, then, pretty soon all hands got to talking about the diseased

+again, and how good he was, and what a loss he was, and all that; and

+before long a big iron-jawed man worked himself in there from outside,

+and stood a-listening and looking, and not saying anything; and nobody

+saying anything to him either, because the king was talking and they was

+all busy listening.  The king was saying—in the middle of something he'd

+started in on—

+

+"—they bein' partickler friends o' the diseased.  That's why they're

+invited here this evenin'; but tomorrow we want all to come—everybody;

+for he respected everybody, he liked everybody, and so it's fitten that

+his funeral orgies sh'd be public."

+

+And so he went a-mooning on and on, liking to hear himself talk, and

+every little while he fetched in his funeral orgies again, till the duke

+he couldn't stand it no more; so he writes on a little scrap of paper,

+"Obsequies, you old fool," and folds it up, and goes to goo-gooing and

+reaching it over people's heads to him.  The king he reads it and puts

+it in his pocket, and says:

+

+"Poor William, afflicted as he is, his heart's aluz right.  Asks me

+to invite everybody to come to the funeral—wants me to make 'em all

+welcome.  But he needn't a worried—it was jest what I was at."

+

+Then he weaves along again, perfectly ca'm, and goes to dropping in his

+funeral orgies again every now and then, just like he done before.  And

+when he done it the third time he says:

+

+"I say orgies, not because it's the common term, because it

+ain't—obsequies bein' the common term—but because orgies is the right

+term. Obsequies ain't used in England no more now—it's gone out.  We

+say orgies now in England.  Orgies is better, because it means the thing

+you're after more exact.  It's a word that's made up out'n the Greek

+orgo, outside, open, abroad; and the Hebrew jeesum, to plant, cover

+up; hence inter.  So, you see, funeral orgies is an open er public

+funeral."

+

+He was the worst I ever struck.  Well, the iron-jawed man he laughed

+right in his face.  Everybody was shocked.  Everybody says, "Why,

+doctor!" and Abner Shackleford says:

+

+"Why, Robinson, hain't you heard the news?  This is Harvey Wilks."

+

+The king he smiled eager, and shoved out his flapper, and says:

+

+"Is it my poor brother's dear good friend and physician?  I—"

+

+"Keep your hands off of me!" says the doctor.  "You talk like an

+Englishman, don't you?  It's the worst imitation I ever heard.  You

+Peter Wilks's brother!  You're a fraud, that's what you are!"

+

+Well, how they all took on!  They crowded around the doctor and tried to

+quiet him down, and tried to explain to him and tell him how Harvey 'd

+showed in forty ways that he was Harvey, and knowed everybody by name,

+and the names of the very dogs, and begged and begged him not to hurt

+Harvey's feelings and the poor girl's feelings, and all that.  But it

+warn't no use; he stormed right along, and said any man that pretended

+to be an Englishman and couldn't imitate the lingo no better than what

+he did was a fraud and a liar.  The poor girls was hanging to the king

+and crying; and all of a sudden the doctor ups and turns on them.  He

+says:

+

+"I was your father's friend, and I'm your friend; and I warn you as a

+friend, and an honest one that wants to protect you and keep you out of

+harm and trouble, to turn your backs on that scoundrel and have nothing

+to do with him, the ignorant tramp, with his idiotic Greek and Hebrew,

+as he calls it.  He is the thinnest kind of an impostor—has come here

+with a lot of empty names and facts which he picked up somewheres, and

+you take them for proofs, and are helped to fool yourselves by these

+foolish friends here, who ought to know better.  Mary Jane Wilks, you

+know me for your friend, and for your unselfish friend, too.  Now listen

+to me; turn this pitiful rascal out—I beg you to do it.  Will you?"

+

+Mary Jane straightened herself up, and my, but she was handsome!  She

+says:

+

+"Here is my answer."  She hove up the bag of money and put it in the

+king's hands, and says, "Take this six thousand dollars, and invest for

+me and my sisters any way you want to, and don't give us no receipt for

+it."

+

+Then she put her arm around the king on one side, and Susan and the

+hare-lip done the same on the other.  Everybody clapped their hands and

+stomped on the floor like a perfect storm, whilst the king held up his

+head and smiled proud.  The doctor says:

+

+"All right; I wash my hands of the matter.  But I warn you all that a

+time 's coming when you're going to feel sick whenever you think of this

+day." And away he went.

+

+"All right, doctor," says the king, kinder mocking him; "we'll try and

+get 'em to send for you;" which made them all laugh, and they said it

+was a prime good hit.

+

+

+

+

+CHAPTER XXVI.

+

+WELL, when they was all gone the king he asks Mary Jane how they was off

+for spare rooms, and she said she had one spare room, which would do for

+Uncle William, and she'd give her own room to Uncle Harvey, which was

+a little bigger, and she would turn into the room with her sisters and

+sleep on a cot; and up garret was a little cubby, with a pallet in it.

+The king said the cubby would do for his valley—meaning me.

+

+So Mary Jane took us up, and she showed them their rooms, which was

+plain but nice.  She said she'd have her frocks and a lot of other traps

+took out of her room if they was in Uncle Harvey's way, but he said

+they warn't.  The frocks was hung along the wall, and before them was

+a curtain made out of calico that hung down to the floor.  There was an

+old hair trunk in one corner, and a guitar-box in another, and all sorts

+of little knickknacks and jimcracks around, like girls brisken up a room

+with.  The king said it was all the more homely and more pleasanter for

+these fixings, and so don't disturb them.  The duke's room was pretty

+small, but plenty good enough, and so was my cubby.

+

+That night they had a big supper, and all them men and women was there,

+and I stood behind the king and the duke's chairs and waited on them,

+and the niggers waited on the rest.  Mary Jane she set at the head of

+the table, with Susan alongside of her, and said how bad the biscuits

+was, and how mean the preserves was, and how ornery and tough the fried

+chickens was—and all that kind of rot, the way women always do for to

+force out compliments; and the people all knowed everything was tiptop,

+and said so—said "How do you get biscuits to brown so nice?" and

+"Where, for the land's sake, did you get these amaz'n pickles?" and

+all that kind of humbug talky-talk, just the way people always does at a

+supper, you know.

+

+And when it was all done me and the hare-lip had supper in the kitchen

+off of the leavings, whilst the others was helping the niggers clean up

+the things.  The hare-lip she got to pumping me about England, and blest

+if I didn't think the ice was getting mighty thin sometimes.  She says:

+

+"Did you ever see the king?"

+

+"Who?  William Fourth?  Well, I bet I have—he goes to our church."  I

+knowed he was dead years ago, but I never let on.  So when I says he

+goes to our church, she says:

+

+"What—regular?"

+

+"Yes—regular.  His pew's right over opposite ourn—on t'other side the

+pulpit."

+

+"I thought he lived in London?"

+

+"Well, he does.  Where would he live?"

+

+"But I thought you lived in Sheffield?"

+

+I see I was up a stump.  I had to let on to get choked with a chicken

+bone, so as to get time to think how to get down again.  Then I says:

+

+"I mean he goes to our church regular when he's in Sheffield.  That's

+only in the summer time, when he comes there to take the sea baths."

+

+"Why, how you talk—Sheffield ain't on the sea."

+

+"Well, who said it was?"

+

+"Why, you did."

+

+"I didn't nuther."

+

+"You did!"

+

+"I didn't."

+

+"You did."

+

+"I never said nothing of the kind."

+

+"Well, what did you say, then?"

+

+"Said he come to take the sea baths—that's what I said."

+

+"Well, then, how's he going to take the sea baths if it ain't on the

+sea?"

+

+"Looky here," I says; "did you ever see any Congress-water?"

+

+"Yes."

+

+"Well, did you have to go to Congress to get it?"

+

+"Why, no."

+

+"Well, neither does William Fourth have to go to the sea to get a sea

+bath."

+

+"How does he get it, then?"

+

+"Gets it the way people down here gets Congress-water—in barrels.  There

+in the palace at Sheffield they've got furnaces, and he wants his water

+hot.  They can't bile that amount of water away off there at the sea.

+They haven't got no conveniences for it."

+

+"Oh, I see, now.  You might a said that in the first place and saved

+time."

+

+When she said that I see I was out of the woods again, and so I was

+comfortable and glad.  Next, she says:

+

+"Do you go to church, too?"

+

+"Yes—regular."

+

+"Where do you set?"

+

+"Why, in our pew."

+

+"Whose pew?"

+

+"Why, ourn—your Uncle Harvey's."

+

+"His'n?  What does he want with a pew?"

+

+"Wants it to set in.  What did you reckon he wanted with it?"

+

+"Why, I thought he'd be in the pulpit."

+

+Rot him, I forgot he was a preacher.  I see I was up a stump again, so I

+played another chicken bone and got another think.  Then I says:

+

+"Blame it, do you suppose there ain't but one preacher to a church?"

+

+"Why, what do they want with more?"

+

+"What!—to preach before a king?  I never did see such a girl as you.

+They don't have no less than seventeen."

+

+"Seventeen!  My land!  Why, I wouldn't set out such a string as that,

+not if I never got to glory.  It must take 'em a week."

+

+"Shucks, they don't all of 'em preach the same day—only one of 'em."

+

+"Well, then, what does the rest of 'em do?"

+

+"Oh, nothing much.  Loll around, pass the plate—and one thing or

+another.  But mainly they don't do nothing."

+

+"Well, then, what are they for?"

+

+"Why, they're for style.  Don't you know nothing?"

+

+"Well, I don't want to know no such foolishness as that.  How is

+servants treated in England?  Do they treat 'em better 'n we treat our

+niggers?"

+

+"No!  A servant ain't nobody there.  They treat them worse than dogs."

+

+"Don't they give 'em holidays, the way we do, Christmas and New Year's

+week, and Fourth of July?"

+

+"Oh, just listen!  A body could tell you hain't ever been to England

+by that.  Why, Hare-l—why, Joanna, they never see a holiday from year's

+end to year's end; never go to the circus, nor theater, nor nigger

+shows, nor nowheres."

+

+"Nor church?"

+

+"Nor church."

+

+"But you always went to church."

+

+Well, I was gone up again.  I forgot I was the old man's servant.  But

+next minute I whirled in on a kind of an explanation how a valley was

+different from a common servant and had to go to church whether he

+wanted to or not, and set with the family, on account of its being the

+law.  But I didn't do it pretty good, and when I got done I see she

+warn't satisfied.  She says:

+

+"Honest injun, now, hain't you been telling me a lot of lies?"

+

+"Honest injun," says I.

+

+"None of it at all?"

+

+"None of it at all.  Not a lie in it," says I.

+

+"Lay your hand on this book and say it."

+

+I see it warn't nothing but a dictionary, so I laid my hand on it and

+said it.  So then she looked a little better satisfied, and says:

+

+"Well, then, I'll believe some of it; but I hope to gracious if I'll

+believe the rest."

+

+"What is it you won't believe, Joe?" says Mary Jane, stepping in with

+Susan behind her.  "It ain't right nor kind for you to talk so to him,

+and him a stranger and so far from his people.  How would you like to be

+treated so?"

+

+"That's always your way, Maim—always sailing in to help somebody before

+they're hurt.  I hain't done nothing to him.  He's told some stretchers,

+I reckon, and I said I wouldn't swallow it all; and that's every bit

+and grain I did say.  I reckon he can stand a little thing like that,

+can't he?"

+

+"I don't care whether 'twas little or whether 'twas big; he's here in

+our house and a stranger, and it wasn't good of you to say it.  If you

+was in his place it would make you feel ashamed; and so you oughtn't to

+say a thing to another person that will make them feel ashamed."

+

+"Why, Mam, he said—"

+

+"It don't make no difference what he said—that ain't the thing.  The

+thing is for you to treat him kind, and not be saying things to make

+him remember he ain't in his own country and amongst his own folks."

+

+I says to myself, this is a girl that I'm letting that old reptile rob

+her of her money!

+

+Then Susan she waltzed in; and if you'll believe me, she did give

+Hare-lip hark from the tomb!

+

+Says I to myself, and this is another one that I'm letting him rob her

+of her money!

+

+Then Mary Jane she took another inning, and went in sweet and lovely

+again—which was her way; but when she got done there warn't hardly

+anything left o' poor Hare-lip.  So she hollered.

+

+"All right, then," says the other girls; "you just ask his pardon."

+

+She done it, too; and she done it beautiful.  She done it so beautiful

+it was good to hear; and I wished I could tell her a thousand lies, so

+she could do it again.

+

+I says to myself, this is another one that I'm letting him rob her of

+her money.  And when she got through they all jest laid theirselves

+out to make me feel at home and know I was amongst friends.  I felt so

+ornery and low down and mean that I says to myself, my mind's made up;

+I'll hive that money for them or bust.

+

+So then I lit out—for bed, I said, meaning some time or another.  When

+I got by myself I went to thinking the thing over.  I says to myself,

+shall I go to that doctor, private, and blow on these frauds?  No—that

+won't do. He might tell who told him; then the king and the duke would

+make it warm for me.  Shall I go, private, and tell Mary Jane?  No—I

+dasn't do it. Her face would give them a hint, sure; they've got the

+money, and they'd slide right out and get away with it.  If she was to

+fetch in help I'd get mixed up in the business before it was done with,

+I judge.  No; there ain't no good way but one.  I got to steal that

+money, somehow; and I got to steal it some way that they won't suspicion

+that I done it. They've got a good thing here, and they ain't a-going

+to leave till they've played this family and this town for all they're

+worth, so I'll find a chance time enough. I'll steal it and hide it; and

+by and by, when I'm away down the river, I'll write a letter and tell

+Mary Jane where it's hid.  But I better hive it tonight if I can,

+because the doctor maybe hasn't let up as much as he lets on he has; he

+might scare them out of here yet.

+

+So, thinks I, I'll go and search them rooms.  Upstairs the hall was

+dark, but I found the duke's room, and started to paw around it with

+my hands; but I recollected it wouldn't be much like the king to let

+anybody else take care of that money but his own self; so then I went to

+his room and begun to paw around there.  But I see I couldn't do nothing

+without a candle, and I dasn't light one, of course.  So I judged I'd

+got to do the other thing—lay for them and eavesdrop.  About that time

+I hears their footsteps coming, and was going to skip under the bed; I

+reached for it, but it wasn't where I thought it would be; but I touched

+the curtain that hid Mary Jane's frocks, so I jumped in behind that and

+snuggled in amongst the gowns, and stood there perfectly still.

+

+They come in and shut the door; and the first thing the duke done was to

+get down and look under the bed.  Then I was glad I hadn't found the bed

+when I wanted it.  And yet, you know, it's kind of natural to hide under

+the bed when you are up to anything private.  They sets down then, and

+the king says:

+

+"Well, what is it?  And cut it middlin' short, because it's better for

+us to be down there a-whoopin' up the mournin' than up here givin' 'em a

+chance to talk us over."

+

+"Well, this is it, Capet.  I ain't easy; I ain't comfortable.  That

+doctor lays on my mind.  I wanted to know your plans.  I've got a

+notion, and I think it's a sound one."

+

+"What is it, duke?"

+

+"That we better glide out of this before three in the morning, and clip

+it down the river with what we've got.  Specially, seeing we got it so

+easy—given back to us, flung at our heads, as you may say, when of

+course we allowed to have to steal it back.  I'm for knocking off and

+lighting out."

+

+That made me feel pretty bad.  About an hour or two ago it would a been

+a little different, but now it made me feel bad and disappointed, The

+king rips out and says:

+

+"What!  And not sell out the rest o' the property?  March off like

+a passel of fools and leave eight or nine thous'n' dollars' worth o'

+property layin' around jest sufferin' to be scooped in?—and all good,

+salable stuff, too."

+

+The duke he grumbled; said the bag of gold was enough, and he didn't

+want to go no deeper—didn't want to rob a lot of orphans of everything

+they had.

+

+"Why, how you talk!" says the king.  "We sha'n't rob 'em of nothing at

+all but jest this money.  The people that buys the property is the

+suff'rers; because as soon 's it's found out 'at we didn't own it—which

+won't be long after we've slid—the sale won't be valid, and it 'll all

+go back to the estate.  These yer orphans 'll git their house back agin,

+and that's enough for them; they're young and spry, and k'n easy

+earn a livin'.  they ain't a-goin to suffer.  Why, jest think—there's

+thous'n's and thous'n's that ain't nigh so well off.  Bless you, they

+ain't got noth'n' to complain of."

+

+Well, the king he talked him blind; so at last he give in, and said all

+right, but said he believed it was blamed foolishness to stay, and that

+doctor hanging over them.  But the king says:

+

+"Cuss the doctor!  What do we k'yer for him?  Hain't we got all the

+fools in town on our side?  And ain't that a big enough majority in any

+town?"

+

+So they got ready to go down stairs again.  The duke says:

+

+"I don't think we put that money in a good place."

+

+That cheered me up.  I'd begun to think I warn't going to get a hint of

+no kind to help me.  The king says:

+

+"Why?"

+

+"Because Mary Jane 'll be in mourning from this out; and first you know

+the nigger that does up the rooms will get an order to box these duds

+up and put 'em away; and do you reckon a nigger can run across money and

+not borrow some of it?"

+

+"Your head's level agin, duke," says the king; and he comes a-fumbling

+under the curtain two or three foot from where I was.  I stuck tight to

+the wall and kept mighty still, though quivery; and I wondered what them

+fellows would say to me if they catched me; and I tried to think what

+I'd better do if they did catch me.  But the king he got the bag before

+I could think more than about a half a thought, and he never suspicioned

+I was around.  They took and shoved the bag through a rip in the straw

+tick that was under the feather-bed, and crammed it in a foot or two

+amongst the straw and said it was all right now, because a nigger only

+makes up the feather-bed, and don't turn over the straw tick only about

+twice a year, and so it warn't in no danger of getting stole now.

+

+But I knowed better.  I had it out of there before they was half-way

+down stairs.  I groped along up to my cubby, and hid it there till I

+could get a chance to do better.  I judged I better hide it outside

+of the house somewheres, because if they missed it they would give the

+house a good ransacking:  I knowed that very well.  Then I turned in,

+with my clothes all on; but I couldn't a gone to sleep if I'd a wanted

+to, I was in such a sweat to get through with the business.  By and by I

+heard the king and the duke come up; so I rolled off my pallet and laid

+with my chin at the top of my ladder, and waited to see if anything was

+going to happen.  But nothing did.

+

+So I held on till all the late sounds had quit and the early ones hadn't

+begun yet; and then I slipped down the ladder.

+

+

+

+

+CHAPTER XXVII.

+

+I crept to their doors and listened; they was snoring.  So I tiptoed

+along, and got down stairs all right.  There warn't a sound anywheres.

+ I peeped through a crack of the dining-room door, and see the men that

+was watching the corpse all sound asleep on their chairs.  The door

+was open into the parlor, where the corpse was laying, and there was a

+candle in both rooms. I passed along, and the parlor door was open; but

+I see there warn't nobody in there but the remainders of Peter; so I

+shoved on by; but the front door was locked, and the key wasn't there.

+ Just then I heard somebody coming down the stairs, back behind me.  I

+run in the parlor and took a swift look around, and the only place I

+see to hide the bag was in the coffin.  The lid was shoved along about

+a foot, showing the dead man's face down in there, with a wet cloth over

+it, and his shroud on.  I tucked the money-bag in under the lid, just

+down beyond where his hands was crossed, which made me creep, they was

+so cold, and then I run back across the room and in behind the door.

+

+The person coming was Mary Jane.  She went to the coffin, very soft, and

+kneeled down and looked in; then she put up her handkerchief, and I see

+she begun to cry, though I couldn't hear her, and her back was to me.  I

+slid out, and as I passed the dining-room I thought I'd make sure them

+watchers hadn't seen me; so I looked through the crack, and everything

+was all right.  They hadn't stirred.

+

+I slipped up to bed, feeling ruther blue, on accounts of the thing

+playing out that way after I had took so much trouble and run so much

+resk about it.  Says I, if it could stay where it is, all right; because

+when we get down the river a hundred mile or two I could write back to

+Mary Jane, and she could dig him up again and get it; but that ain't the

+thing that's going to happen; the thing that's going to happen is, the

+money 'll be found when they come to screw on the lid.  Then the king

+'ll get it again, and it 'll be a long day before he gives anybody

+another chance to smouch it from him. Of course I wanted to slide

+down and get it out of there, but I dasn't try it.  Every minute it was

+getting earlier now, and pretty soon some of them watchers would begin

+to stir, and I might get catched—catched with six thousand dollars in my

+hands that nobody hadn't hired me to take care of.  I don't wish to be

+mixed up in no such business as that, I says to myself.

+

+When I got down stairs in the morning the parlor was shut up, and the

+watchers was gone.  There warn't nobody around but the family and the

+widow Bartley and our tribe.  I watched their faces to see if anything

+had been happening, but I couldn't tell.

+

+Towards the middle of the day the undertaker come with his man, and they

+set the coffin in the middle of the room on a couple of chairs, and then

+set all our chairs in rows, and borrowed more from the neighbors till

+the hall and the parlor and the dining-room was full.  I see the coffin

+lid was the way it was before, but I dasn't go to look in under it, with

+folks around.

+

+Then the people begun to flock in, and the beats and the girls took

+seats in the front row at the head of the coffin, and for a half an hour

+the people filed around slow, in single rank, and looked down at the

+dead man's face a minute, and some dropped in a tear, and it was

+all very still and solemn, only the girls and the beats holding

+handkerchiefs to their eyes and keeping their heads bent, and sobbing a

+little.  There warn't no other sound but the scraping of the feet on

+the floor and blowing noses—because people always blows them more at a

+funeral than they do at other places except church.

+

+When the place was packed full the undertaker he slid around in his

+black gloves with his softy soothering ways, putting on the last

+touches, and getting people and things all ship-shape and comfortable,

+and making no more sound than a cat.  He never spoke; he moved people

+around, he squeezed in late ones, he opened up passageways, and done

+it with nods, and signs with his hands.  Then he took his place over

+against the wall. He was the softest, glidingest, stealthiest man I ever

+see; and there warn't no more smile to him than there is to a ham.

+

+They had borrowed a melodeum—a sick one; and when everything was ready

+a young woman set down and worked it, and it was pretty skreeky and

+colicky, and everybody joined in and sung, and Peter was the only one

+that had a good thing, according to my notion.  Then the Reverend Hobson

+opened up, slow and solemn, and begun to talk; and straight off the most

+outrageous row busted out in the cellar a body ever heard; it was only

+one dog, but he made a most powerful racket, and he kept it up right

+along; the parson he had to stand there, over the coffin, and wait—you

+couldn't hear yourself think.  It was right down awkward, and nobody

+didn't seem to know what to do.  But pretty soon they see that

+long-legged undertaker make a sign to the preacher as much as to say,

+"Don't you worry—just depend on me."  Then he stooped down and begun

+to glide along the wall, just his shoulders showing over the people's

+heads.  So he glided along, and the powwow and racket getting more and

+more outrageous all the time; and at last, when he had gone around two

+sides of the room, he disappears down cellar.  Then in about two seconds

+we heard a whack, and the dog he finished up with a most amazing howl or

+two, and then everything was dead still, and the parson begun his solemn

+talk where he left off.  In a minute or two here comes this undertaker's

+back and shoulders gliding along the wall again; and so he glided and

+glided around three sides of the room, and then rose up, and shaded his

+mouth with his hands, and stretched his neck out towards the preacher,

+over the people's heads, and says, in a kind of a coarse whisper, "He

+had a rat!"  Then he drooped down and glided along the wall again to

+his place.  You could see it was a great satisfaction to the people,

+because naturally they wanted to know.  A little thing like that don't

+cost nothing, and it's just the little things that makes a man to be

+looked up to and liked.  There warn't no more popular man in town than

+what that undertaker was.

+

+Well, the funeral sermon was very good, but pison long and tiresome; and

+then the king he shoved in and got off some of his usual rubbage, and

+at last the job was through, and the undertaker begun to sneak up on the

+coffin with his screw-driver.  I was in a sweat then, and watched him

+pretty keen. But he never meddled at all; just slid the lid along as

+soft as mush, and screwed it down tight and fast.  So there I was!  I

+didn't know whether the money was in there or not.  So, says I, s'pose

+somebody has hogged that bag on the sly?—now how do I know whether

+to write to Mary Jane or not? S'pose she dug him up and didn't find

+nothing, what would she think of me? Blame it, I says, I might get

+hunted up and jailed; I'd better lay low and keep dark, and not write at

+all; the thing's awful mixed now; trying to better it, I've worsened it

+a hundred times, and I wish to goodness I'd just let it alone, dad fetch

+the whole business!

+

+They buried him, and we come back home, and I went to watching faces

+again—I couldn't help it, and I couldn't rest easy.  But nothing come of

+it; the faces didn't tell me nothing.

+

+The king he visited around in the evening, and sweetened everybody up,

+and made himself ever so friendly; and he give out the idea that his

+congregation over in England would be in a sweat about him, so he must

+hurry and settle up the estate right away and leave for home.  He was

+very sorry he was so pushed, and so was everybody; they wished he could

+stay longer, but they said they could see it couldn't be done.  And he

+said of course him and William would take the girls home with them; and

+that pleased everybody too, because then the girls would be well fixed

+and amongst their own relations; and it pleased the girls, too—tickled

+them so they clean forgot they ever had a trouble in the world; and told

+him to sell out as quick as he wanted to, they would be ready.  Them

+poor things was that glad and happy it made my heart ache to see them

+getting fooled and lied to so, but I didn't see no safe way for me to

+chip in and change the general tune.

+

+Well, blamed if the king didn't bill the house and the niggers and all

+the property for auction straight off—sale two days after the funeral;

+but anybody could buy private beforehand if they wanted to.

+

+So the next day after the funeral, along about noon-time, the girls' joy

+got the first jolt.  A couple of nigger traders come along, and the king

+sold them the niggers reasonable, for three-day drafts as they called

+it, and away they went, the two sons up the river to Memphis, and their

+mother down the river to Orleans.  I thought them poor girls and them

+niggers would break their hearts for grief; they cried around each

+other, and took on so it most made me down sick to see it.  The girls

+said they hadn't ever dreamed of seeing the family separated or sold

+away from the town.  I can't ever get it out of my memory, the sight of

+them poor miserable girls and niggers hanging around each other's necks

+and crying; and I reckon I couldn't a stood it all, but would a had

+to bust out and tell on our gang if I hadn't knowed the sale warn't no

+account and the niggers would be back home in a week or two.

+

+The thing made a big stir in the town, too, and a good many come out

+flatfooted and said it was scandalous to separate the mother and the

+children that way.  It injured the frauds some; but the old fool he

+bulled right along, spite of all the duke could say or do, and I tell

+you the duke was powerful uneasy.

+

+Next day was auction day.  About broad day in the morning the king and

+the duke come up in the garret and woke me up, and I see by their look

+that there was trouble.  The king says:

+

+"Was you in my room night before last?"

+

+"No, your majesty"—which was the way I always called him when nobody but

+our gang warn't around.

+

+"Was you in there yisterday er last night?"

+

+"No, your majesty."

+

+"Honor bright, now—no lies."

+

+"Honor bright, your majesty, I'm telling you the truth.  I hain't been

+a-near your room since Miss Mary Jane took you and the duke and showed

+it to you."

+

+The duke says:

+

+"Have you seen anybody else go in there?"

+

+"No, your grace, not as I remember, I believe."

+

+"Stop and think."

+

+I studied awhile and see my chance; then I says:

+

+"Well, I see the niggers go in there several times."

+

+Both of them gave a little jump, and looked like they hadn't ever

+expected it, and then like they had.  Then the duke says:

+

+"What, all of them?"

+

+"No—leastways, not all at once—that is, I don't think I ever see them

+all come out at once but just one time."

+

+"Hello!  When was that?"

+

+"It was the day we had the funeral.  In the morning.  It warn't early,

+because I overslept.  I was just starting down the ladder, and I see

+them."

+

+"Well, go on, go on!  What did they do?  How'd they act?"

+

+"They didn't do nothing.  And they didn't act anyway much, as fur as I

+see. They tiptoed away; so I seen, easy enough, that they'd shoved in

+there to do up your majesty's room, or something, s'posing you was up;

+and found you warn't up, and so they was hoping to slide out of the

+way of trouble without waking you up, if they hadn't already waked you

+up."

+

+"Great guns, this is a go!" says the king; and both of them looked

+pretty sick and tolerable silly.  They stood there a-thinking and

+scratching their heads a minute, and the duke he bust into a kind of a

+little raspy chuckle, and says:

+

+"It does beat all how neat the niggers played their hand.  They let on

+to be sorry they was going out of this region!  And I believed they

+was sorry, and so did you, and so did everybody.  Don't ever tell me

+any more that a nigger ain't got any histrionic talent.  Why, the way

+they played that thing it would fool anybody.  In my opinion, there's

+a fortune in 'em.  If I had capital and a theater, I wouldn't want a

+better lay-out than that—and here we've gone and sold 'em for a song.

+ Yes, and ain't privileged to sing the song yet.  Say, where is that

+song—that draft?"

+

+"In the bank for to be collected.  Where would it be?"

+

+"Well, that's all right then, thank goodness."

+

+Says I, kind of timid-like:

+

+"Is something gone wrong?"

+

+The king whirls on me and rips out:

+

+"None o' your business!  You keep your head shet, and mind y'r own

+affairs—if you got any.  Long as you're in this town don't you forgit

+that—you hear?"  Then he says to the duke, "We got to jest swaller it

+and say noth'n':  mum's the word for us."

+

+As they was starting down the ladder the duke he chuckles again, and

+says:

+

+"Quick sales and small profits!  It's a good business—yes."

+

+The king snarls around on him and says:

+

+"I was trying to do for the best in sellin' 'em out so quick.  If the

+profits has turned out to be none, lackin' considable, and none to

+carry, is it my fault any more'n it's yourn?"

+

+"Well, they'd be in this house yet and we wouldn't if I could a got

+my advice listened to."

+

+The king sassed back as much as was safe for him, and then swapped

+around and lit into me again.  He give me down the banks for not

+coming and telling him I see the niggers come out of his room acting

+that way—said any fool would a knowed something was up.  And then

+waltzed in and cussed himself awhile, and said it all come of him not

+laying late and taking his natural rest that morning, and he'd be

+blamed if he'd ever do it again.  So they went off a-jawing; and I felt

+dreadful glad I'd worked it all off on to the niggers, and yet hadn't

+done the niggers no harm by it.

+

+

+

+

+CHAPTER XXVIII.

+

+BY and by it was getting-up time.  So I come down the ladder and started

+for down-stairs; but as I come to the girls' room the door was open, and

+I see Mary Jane setting by her old hair trunk, which was open and she'd

+been packing things in it—getting ready to go to England.  But she

+had stopped now with a folded gown in her lap, and had her face in her

+hands, crying.  I felt awful bad to see it; of course anybody would.  I

+went in there and says:

+

+"Miss Mary Jane, you can't a-bear to see people in trouble, and I

+can't—most always.  Tell me about it."

+

+So she done it.  And it was the niggers—I just expected it.  She said

+the beautiful trip to England was most about spoiled for her; she didn't

+know how she was ever going to be happy there, knowing the mother and

+the children warn't ever going to see each other no more—and then busted

+out bitterer than ever, and flung up her hands, and says:

+

+"Oh, dear, dear, to think they ain't ever going to see each other any

+more!"

+

+"But they will—and inside of two weeks—and I know it!" says I.

+

+Laws, it was out before I could think!  And before I could budge she

+throws her arms around my neck and told me to say it again, say it

+again, say it again!

+

+I see I had spoke too sudden and said too much, and was in a close

+place. I asked her to let me think a minute; and she set there, very

+impatient and excited and handsome, but looking kind of happy and

+eased-up, like a person that's had a tooth pulled out.  So I went to

+studying it out.  I says to myself, I reckon a body that ups and tells

+the truth when he is in a tight place is taking considerable many resks,

+though I ain't had no experience, and can't say for certain; but it

+looks so to me, anyway; and yet here's a case where I'm blest if it

+don't look to me like the truth is better and actuly safer than a lie.

+ I must lay it by in my mind, and think it over some time or other, it's

+so kind of strange and unregular. I never see nothing like it.  Well, I

+says to myself at last, I'm a-going to chance it; I'll up and tell the

+truth this time, though it does seem most like setting down on a kag of

+powder and touching it off just to see where you'll go to. Then I says:

+

+"Miss Mary Jane, is there any place out of town a little ways where you

+could go and stay three or four days?"

+

+"Yes; Mr. Lothrop's.  Why?"

+

+"Never mind why yet.  If I'll tell you how I know the niggers will see

+each other again inside of two weeks—here in this house—and prove how

+I know it—will you go to Mr. Lothrop's and stay four days?"

+

+"Four days!" she says; "I'll stay a year!"

+

+"All right," I says, "I don't want nothing more out of you than just

+your word—I druther have it than another man's kiss-the-Bible."  She

+smiled and reddened up very sweet, and I says, "If you don't mind it,

+I'll shut the door—and bolt it."

+

+Then I come back and set down again, and says:

+

+"Don't you holler.  Just set still and take it like a man.  I got to

+tell the truth, and you want to brace up, Miss Mary, because it's a

+bad kind, and going to be hard to take, but there ain't no help for

+it.  These uncles of yourn ain't no uncles at all; they're a couple of

+frauds—regular dead-beats.  There, now we're over the worst of it, you

+can stand the rest middling easy."

+

+It jolted her up like everything, of course; but I was over the shoal

+water now, so I went right along, her eyes a-blazing higher and higher

+all the time, and told her every blame thing, from where we first struck

+that young fool going up to the steamboat, clear through to where she

+flung herself on to the king's breast at the front door and he kissed

+her sixteen or seventeen times—and then up she jumps, with her face

+afire like sunset, and says:

+

+"The brute!  Come, don't waste a minute—not a second—we'll have them

+tarred and feathered, and flung in the river!"

+

+Says I:

+

+"Cert'nly.  But do you mean before you go to Mr. Lothrop's, or—"

+

+"Oh," she says, "what am I thinking about!" she says, and set right

+down again.  "Don't mind what I said—please don't—you won't, now,

+will you?" Laying her silky hand on mine in that kind of a way that

+I said I would die first.  "I never thought, I was so stirred up," she

+says; "now go on, and I won't do so any more.  You tell me what to do,

+and whatever you say I'll do it."

+

+"Well," I says, "it's a rough gang, them two frauds, and I'm fixed so

+I got to travel with them a while longer, whether I want to or not—I

+druther not tell you why; and if you was to blow on them this town would

+get me out of their claws, and I'd be all right; but there'd be another

+person that you don't know about who'd be in big trouble.  Well, we

+got to save him, hain't we?  Of course.  Well, then, we won't blow on

+them."

+

+Saying them words put a good idea in my head.  I see how maybe I could

+get me and Jim rid of the frauds; get them jailed here, and then leave.

+But I didn't want to run the raft in the daytime without anybody aboard

+to answer questions but me; so I didn't want the plan to begin working

+till pretty late to-night.  I says:

+

+"Miss Mary Jane, I'll tell you what we'll do, and you won't have to stay

+at Mr. Lothrop's so long, nuther.  How fur is it?"

+

+"A little short of four miles—right out in the country, back here."

+

+"Well, that 'll answer.  Now you go along out there, and lay low

+till nine or half-past to-night, and then get them to fetch you home

+again—tell them you've thought of something.  If you get here before

+eleven put a candle in this window, and if I don't turn up wait till

+eleven, and then if I don't turn up it means I'm gone, and out of the

+way, and safe. Then you come out and spread the news around, and get

+these beats jailed."

+

+"Good," she says, "I'll do it."

+

+"And if it just happens so that I don't get away, but get took up along

+with them, you must up and say I told you the whole thing beforehand,

+and you must stand by me all you can."

+

+"Stand by you! indeed I will.  They sha'n't touch a hair of your head!"

+she says, and I see her nostrils spread and her eyes snap when she said

+it, too.

+

+"If I get away I sha'n't be here," I says, "to prove these rapscallions

+ain't your uncles, and I couldn't do it if I was here.  I could swear

+they was beats and bummers, that's all, though that's worth something.

+Well, there's others can do that better than what I can, and they're

+people that ain't going to be doubted as quick as I'd be.  I'll tell you

+how to find them.  Gimme a pencil and a piece of paper.  There—'Royal

+Nonesuch, Bricksville.'  Put it away, and don't lose it.  When the

+court wants to find out something about these two, let them send up to

+Bricksville and say they've got the men that played the Royal Nonesuch,

+and ask for some witnesses—why, you'll have that entire town down here

+before you can hardly wink, Miss Mary.  And they'll come a-biling, too."

+

+I judged we had got everything fixed about right now.  So I says:

+

+"Just let the auction go right along, and don't worry.  Nobody don't

+have to pay for the things they buy till a whole day after the auction

+on accounts of the short notice, and they ain't going out of this till

+they get that money; and the way we've fixed it the sale ain't going to

+count, and they ain't going to get no money.  It's just like the way

+it was with the niggers—it warn't no sale, and the niggers will be

+back before long.  Why, they can't collect the money for the niggers

+yet—they're in the worst kind of a fix, Miss Mary."

+

+"Well," she says, "I'll run down to breakfast now, and then I'll start

+straight for Mr. Lothrop's."

+

+"'Deed, that ain't the ticket, Miss Mary Jane," I says, "by no manner

+of means; go before breakfast."

+

+"Why?"

+

+"What did you reckon I wanted you to go at all for, Miss Mary?"

+

+"Well, I never thought—and come to think, I don't know.  What was it?"

+

+"Why, it's because you ain't one of these leather-face people.  I don't

+want no better book than what your face is.  A body can set down and

+read it off like coarse print.  Do you reckon you can go and face your

+uncles when they come to kiss you good-morning, and never—"

+

+"There, there, don't!  Yes, I'll go before breakfast—I'll be glad to.

+And leave my sisters with them?"

+

+"Yes; never mind about them.  They've got to stand it yet a while.  They

+might suspicion something if all of you was to go.  I don't want you to

+see them, nor your sisters, nor nobody in this town; if a neighbor was

+to ask how is your uncles this morning your face would tell something.

+ No, you go right along, Miss Mary Jane, and I'll fix it with all of

+them. I'll tell Miss Susan to give your love to your uncles and say

+you've went away for a few hours for to get a little rest and change, or

+to see a friend, and you'll be back to-night or early in the morning."

+

+"Gone to see a friend is all right, but I won't have my love given to

+them."

+

+"Well, then, it sha'n't be."  It was well enough to tell her so—no

+harm in it.  It was only a little thing to do, and no trouble; and it's

+the little things that smooths people's roads the most, down here below;

+it would make Mary Jane comfortable, and it wouldn't cost nothing.  Then

+I says:  "There's one more thing—that bag of money."

+

+"Well, they've got that; and it makes me feel pretty silly to think

+how they got it."

+

+"No, you're out, there.  They hain't got it."

+

+"Why, who's got it?"

+

+"I wish I knowed, but I don't.  I had it, because I stole it from

+them; and I stole it to give to you; and I know where I hid it, but I'm

+afraid it ain't there no more.  I'm awful sorry, Miss Mary Jane, I'm

+just as sorry as I can be; but I done the best I could; I did honest.  I

+come nigh getting caught, and I had to shove it into the first place I

+come to, and run—and it warn't a good place."

+

+"Oh, stop blaming yourself—it's too bad to do it, and I won't allow

+it—you couldn't help it; it wasn't your fault.  Where did you hide it?"

+

+I didn't want to set her to thinking about her troubles again; and I

+couldn't seem to get my mouth to tell her what would make her see that

+corpse laying in the coffin with that bag of money on his stomach.  So

+for a minute I didn't say nothing; then I says:

+

+"I'd ruther not tell you where I put it, Miss Mary Jane, if you don't

+mind letting me off; but I'll write it for you on a piece of paper, and

+you can read it along the road to Mr. Lothrop's, if you want to.  Do you

+reckon that 'll do?"

+

+"Oh, yes."

+

+So I wrote:  "I put it in the coffin.  It was in there when you was

+crying there, away in the night.  I was behind the door, and I was

+mighty sorry for you, Miss Mary Jane."

+

+It made my eyes water a little to remember her crying there all by

+herself in the night, and them devils laying there right under her own

+roof, shaming her and robbing her; and when I folded it up and give it

+to her I see the water come into her eyes, too; and she shook me by the

+hand, hard, and says:

+

+"Good-bye.  I'm going to do everything just as you've told me; and if

+I don't ever see you again, I sha'n't ever forget you and I'll think of

+you a many and a many a time, and I'll pray for you, too!"—and she was

+gone.

+

+Pray for me!  I reckoned if she knowed me she'd take a job that was more

+nearer her size.  But I bet she done it, just the same—she was just that

+kind.  She had the grit to pray for Judus if she took the notion—there

+warn't no back-down to her, I judge.  You may say what you want to, but

+in my opinion she had more sand in her than any girl I ever see; in

+my opinion she was just full of sand.  It sounds like flattery, but it

+ain't no flattery.  And when it comes to beauty—and goodness, too—she

+lays over them all.  I hain't ever seen her since that time that I see

+her go out of that door; no, I hain't ever seen her since, but I reckon

+I've thought of her a many and a many a million times, and of her saying

+she would pray for me; and if ever I'd a thought it would do any good

+for me to pray for her, blamed if I wouldn't a done it or bust.

+

+Well, Mary Jane she lit out the back way, I reckon; because nobody see

+her go.  When I struck Susan and the hare-lip, I says:

+

+"What's the name of them people over on t'other side of the river that

+you all goes to see sometimes?"

+

+They says:

+

+"There's several; but it's the Proctors, mainly."

+

+"That's the name," I says; "I most forgot it.  Well, Miss Mary Jane she

+told me to tell you she's gone over there in a dreadful hurry—one of

+them's sick."

+

+"Which one?"

+

+"I don't know; leastways, I kinder forget; but I thinks it's—"

+

+"Sakes alive, I hope it ain't Hanner?"

+

+"I'm sorry to say it," I says, "but Hanner's the very one."

+

+"My goodness, and she so well only last week!  Is she took bad?"

+

+"It ain't no name for it.  They set up with her all night, Miss Mary

+Jane said, and they don't think she'll last many hours."

+

+"Only think of that, now!  What's the matter with her?"

+

+I couldn't think of anything reasonable, right off that way, so I says:

+

+"Mumps."

+

+"Mumps your granny!  They don't set up with people that's got the

+mumps."

+

+"They don't, don't they?  You better bet they do with these mumps.

+ These mumps is different.  It's a new kind, Miss Mary Jane said."

+

+"How's it a new kind?"

+

+"Because it's mixed up with other things."

+

+"What other things?"

+

+"Well, measles, and whooping-cough, and erysiplas, and consumption, and

+yaller janders, and brain-fever, and I don't know what all."

+

+"My land!  And they call it the mumps?"

+

+"That's what Miss Mary Jane said."

+

+"Well, what in the nation do they call it the mumps for?"

+

+"Why, because it is the mumps.  That's what it starts with."

+

+"Well, ther' ain't no sense in it.  A body might stump his toe, and take

+pison, and fall down the well, and break his neck, and bust his brains

+out, and somebody come along and ask what killed him, and some numskull

+up and say, 'Why, he stumped his toe.'  Would ther' be any sense

+in that? No.  And ther' ain't no sense in this, nuther.  Is it

+ketching?"

+

+"Is it ketching?  Why, how you talk.  Is a harrow catching—in the

+dark? If you don't hitch on to one tooth, you're bound to on another,

+ain't you? And you can't get away with that tooth without fetching the

+whole harrow along, can you?  Well, these kind of mumps is a kind of a

+harrow, as you may say—and it ain't no slouch of a harrow, nuther, you

+come to get it hitched on good."

+

+"Well, it's awful, I think," says the hare-lip.  "I'll go to Uncle

+Harvey and—"

+

+"Oh, yes," I says, "I would.  Of course I would.  I wouldn't lose no

+time."

+

+"Well, why wouldn't you?"

+

+"Just look at it a minute, and maybe you can see.  Hain't your uncles

+obleegd to get along home to England as fast as they can?  And do you

+reckon they'd be mean enough to go off and leave you to go all that

+journey by yourselves?  you know they'll wait for you.  So fur, so

+good. Your uncle Harvey's a preacher, ain't he?  Very well, then; is a

+preacher going to deceive a steamboat clerk? is he going to deceive

+a ship clerk?—so as to get them to let Miss Mary Jane go aboard?  Now

+you know he ain't.  What will he do, then?  Why, he'll say, 'It's a

+great pity, but my church matters has got to get along the best way they

+can; for my niece has been exposed to the dreadful pluribus-unum mumps,

+and so it's my bounden duty to set down here and wait the three months

+it takes to show on her if she's got it.'  But never mind, if you think

+it's best to tell your uncle Harvey—"

+

+"Shucks, and stay fooling around here when we could all be having good

+times in England whilst we was waiting to find out whether Mary Jane's

+got it or not?  Why, you talk like a muggins."

+

+"Well, anyway, maybe you'd better tell some of the neighbors."

+

+"Listen at that, now.  You do beat all for natural stupidness.  Can't

+you see that they'd go and tell?  Ther' ain't no way but just to not

+tell anybody at all."

+

+"Well, maybe you're right—yes, I judge you are right."

+

+"But I reckon we ought to tell Uncle Harvey she's gone out a while,

+anyway, so he won't be uneasy about her?"

+

+"Yes, Miss Mary Jane she wanted you to do that.  She says, 'Tell them to

+give Uncle Harvey and William my love and a kiss, and say I've run over

+the river to see Mr.'—Mr.—what is the name of that rich family your

+uncle Peter used to think so much of?—I mean the one that—"

+

+"Why, you must mean the Apthorps, ain't it?"

+

+"Of course; bother them kind of names, a body can't ever seem to

+remember them, half the time, somehow.  Yes, she said, say she has run

+over for to ask the Apthorps to be sure and come to the auction and buy

+this house, because she allowed her uncle Peter would ruther they had

+it than anybody else; and she's going to stick to them till they say

+they'll come, and then, if she ain't too tired, she's coming home; and

+if she is, she'll be home in the morning anyway.  She said, don't say

+nothing about the Proctors, but only about the Apthorps—which 'll be

+perfectly true, because she is going there to speak about their buying

+the house; I know it, because she told me so herself."

+

+"All right," they said, and cleared out to lay for their uncles, and

+give them the love and the kisses, and tell them the message.

+

+Everything was all right now.  The girls wouldn't say nothing because

+they wanted to go to England; and the king and the duke would ruther

+Mary Jane was off working for the auction than around in reach of

+Doctor Robinson.  I felt very good; I judged I had done it pretty neat—I

+reckoned Tom Sawyer couldn't a done it no neater himself.  Of course he

+would a throwed more style into it, but I can't do that very handy, not

+being brung up to it.

+

+Well, they held the auction in the public square, along towards the end

+of the afternoon, and it strung along, and strung along, and the old man

+he was on hand and looking his level pisonest, up there longside of the

+auctioneer, and chipping in a little Scripture now and then, or a little

+goody-goody saying of some kind, and the duke he was around goo-gooing

+for sympathy all he knowed how, and just spreading himself generly.

+

+But by and by the thing dragged through, and everything was

+sold—everything but a little old trifling lot in the graveyard.  So

+they'd got to work that off—I never see such a girafft as the king was

+for wanting to swallow everything.  Well, whilst they was at it a

+steamboat landed, and in about two minutes up comes a crowd a-whooping

+and yelling and laughing and carrying on, and singing out:

+

+"Here's your opposition line! here's your two sets o' heirs to old

+Peter Wilks—and you pays your money and you takes your choice!"

+

+

+

+

+CHAPTER XXIX.

+

+THEY was fetching a very nice-looking old gentleman along, and a

+nice-looking younger one, with his right arm in a sling.  And, my souls,

+how the people yelled and laughed, and kept it up.  But I didn't see no

+joke about it, and I judged it would strain the duke and the king some

+to see any.  I reckoned they'd turn pale.  But no, nary a pale did

+they turn. The duke he never let on he suspicioned what was up, but

+just went a goo-gooing around, happy and satisfied, like a jug that's

+googling out buttermilk; and as for the king, he just gazed and gazed

+down sorrowful on them new-comers like it give him the stomach-ache in

+his very heart to think there could be such frauds and rascals in the

+world.  Oh, he done it admirable.  Lots of the principal people

+gethered around the king, to let him see they was on his side.  That old

+gentleman that had just come looked all puzzled to death.  Pretty

+soon he begun to speak, and I see straight off he pronounced like an

+Englishman—not the king's way, though the king's was pretty good for

+an imitation.  I can't give the old gent's words, nor I can't imitate

+him; but he turned around to the crowd, and says, about like this:

+

+"This is a surprise to me which I wasn't looking for; and I'll

+acknowledge, candid and frank, I ain't very well fixed to meet it and

+answer it; for my brother and me has had misfortunes; he's broke his

+arm, and our baggage got put off at a town above here last night in the

+night by a mistake.  I am Peter Wilks' brother Harvey, and this is his

+brother William, which can't hear nor speak—and can't even make signs to

+amount to much, now't he's only got one hand to work them with.  We are

+who we say we are; and in a day or two, when I get the baggage, I can

+prove it. But up till then I won't say nothing more, but go to the hotel

+and wait."

+

+So him and the new dummy started off; and the king he laughs, and

+blethers out:

+

+"Broke his arm—very likely, ain't it?—and very convenient, too,

+for a fraud that's got to make signs, and ain't learnt how.  Lost

+their baggage! That's mighty good!—and mighty ingenious—under the

+circumstances!"

+

+So he laughed again; and so did everybody else, except three or four,

+or maybe half a dozen.  One of these was that doctor; another one was

+a sharp-looking gentleman, with a carpet-bag of the old-fashioned kind

+made out of carpet-stuff, that had just come off of the steamboat and

+was talking to him in a low voice, and glancing towards the king now and

+then and nodding their heads—it was Levi Bell, the lawyer that was gone

+up to Louisville; and another one was a big rough husky that come along

+and listened to all the old gentleman said, and was listening to the

+king now. And when the king got done this husky up and says:

+

+"Say, looky here; if you are Harvey Wilks, when'd you come to this

+town?"

+

+"The day before the funeral, friend," says the king.

+

+"But what time o' day?"

+

+"In the evenin'—'bout an hour er two before sundown."

+

+"How'd you come?"

+

+"I come down on the Susan Powell from Cincinnati."

+

+"Well, then, how'd you come to be up at the Pint in the mornin'—in a

+canoe?"

+

+"I warn't up at the Pint in the mornin'."

+

+"It's a lie."

+

+Several of them jumped for him and begged him not to talk that way to an

+old man and a preacher.

+

+"Preacher be hanged, he's a fraud and a liar.  He was up at the Pint

+that mornin'.  I live up there, don't I?  Well, I was up there, and

+he was up there.  I see him there.  He come in a canoe, along with Tim

+Collins and a boy."

+

+The doctor he up and says:

+

+"Would you know the boy again if you was to see him, Hines?"

+

+"I reckon I would, but I don't know.  Why, yonder he is, now.  I know

+him perfectly easy."

+

+It was me he pointed at.  The doctor says:

+

+"Neighbors, I don't know whether the new couple is frauds or not; but if

+these two ain't frauds, I am an idiot, that's all.  I think it's our

+duty to see that they don't get away from here till we've looked into

+this thing. Come along, Hines; come along, the rest of you.  We'll take

+these fellows to the tavern and affront them with t'other couple, and I

+reckon we'll find out something before we get through."

+

+It was nuts for the crowd, though maybe not for the king's friends; so

+we all started.  It was about sundown.  The doctor he led me along by

+the hand, and was plenty kind enough, but he never let go my hand.

+

+We all got in a big room in the hotel, and lit up some candles, and

+fetched in the new couple.  First, the doctor says:

+

+"I don't wish to be too hard on these two men, but I think they're

+frauds, and they may have complices that we don't know nothing about.

+ If they have, won't the complices get away with that bag of gold Peter

+Wilks left?  It ain't unlikely.  If these men ain't frauds, they won't

+object to sending for that money and letting us keep it till they prove

+they're all right—ain't that so?"

+

+Everybody agreed to that.  So I judged they had our gang in a pretty

+tight place right at the outstart.  But the king he only looked

+sorrowful, and says:

+

+"Gentlemen, I wish the money was there, for I ain't got no disposition

+to throw anything in the way of a fair, open, out-and-out investigation

+o' this misable business; but, alas, the money ain't there; you k'n send

+and see, if you want to."

+

+"Where is it, then?"

+

+"Well, when my niece give it to me to keep for her I took and hid it

+inside o' the straw tick o' my bed, not wishin' to bank it for the few

+days we'd be here, and considerin' the bed a safe place, we not bein'

+used to niggers, and suppos'n' 'em honest, like servants in England.

+ The niggers stole it the very next mornin' after I had went down

+stairs; and when I sold 'em I hadn't missed the money yit, so they got

+clean away with it.  My servant here k'n tell you 'bout it, gentlemen."

+

+The doctor and several said "Shucks!" and I see nobody didn't altogether

+believe him.  One man asked me if I see the niggers steal it.  I said

+no, but I see them sneaking out of the room and hustling away, and I

+never thought nothing, only I reckoned they was afraid they had waked up

+my master and was trying to get away before he made trouble with them.

+ That was all they asked me.  Then the doctor whirls on me and says:

+

+"Are you English, too?"

+

+I says yes; and him and some others laughed, and said, "Stuff!"

+

+Well, then they sailed in on the general investigation, and there we had

+it, up and down, hour in, hour out, and nobody never said a word about

+supper, nor ever seemed to think about it—and so they kept it up, and

+kept it up; and it was the worst mixed-up thing you ever see.  They

+made the king tell his yarn, and they made the old gentleman tell his'n;

+and anybody but a lot of prejudiced chuckleheads would a seen that the

+old gentleman was spinning truth and t'other one lies.  And by and by

+they had me up to tell what I knowed.  The king he give me a left-handed

+look out of the corner of his eye, and so I knowed enough to talk on the

+right side.  I begun to tell about Sheffield, and how we lived there,

+and all about the English Wilkses, and so on; but I didn't get pretty

+fur till the doctor begun to laugh; and Levi Bell, the lawyer, says:

+

+"Set down, my boy; I wouldn't strain myself if I was you.  I reckon

+you ain't used to lying, it don't seem to come handy; what you want is

+practice.  You do it pretty awkward."

+

+I didn't care nothing for the compliment, but I was glad to be let off,

+anyway.

+

+The doctor he started to say something, and turns and says:

+

+"If you'd been in town at first, Levi Bell—" The king broke in and

+reached out his hand, and says:

+

+"Why, is this my poor dead brother's old friend that he's wrote so often

+about?"

+

+The lawyer and him shook hands, and the lawyer smiled and looked

+pleased, and they talked right along awhile, and then got to one side

+and talked low; and at last the lawyer speaks up and says:

+

+"That 'll fix it.  I'll take the order and send it, along with your

+brother's, and then they'll know it's all right."

+

+So they got some paper and a pen, and the king he set down and twisted

+his head to one side, and chawed his tongue, and scrawled off something;

+and then they give the pen to the duke—and then for the first time the

+duke looked sick.  But he took the pen and wrote.  So then the lawyer

+turns to the new old gentleman and says:

+

+"You and your brother please write a line or two and sign your names."

+

+The old gentleman wrote, but nobody couldn't read it.  The lawyer looked

+powerful astonished, and says:

+

+"Well, it beats me"—and snaked a lot of old letters out of his pocket,

+and examined them, and then examined the old man's writing, and then

+them again; and then says:  "These old letters is from Harvey Wilks;

+and here's these two handwritings, and anybody can see they didn't

+write them" (the king and the duke looked sold and foolish, I tell

+you, to see how the lawyer had took them in), "and here's this old

+gentleman's hand writing, and anybody can tell, easy enough, he didn't

+write them—fact is, the scratches he makes ain't properly writing at

+all.  Now, here's some letters from—"

+

+The new old gentleman says:

+

+"If you please, let me explain.  Nobody can read my hand but my brother

+there—so he copies for me.  It's his hand you've got there, not mine."

+

+"Well!" says the lawyer, "this is a state of things.  I've got some

+of William's letters, too; so if you'll get him to write a line or so we

+can com—"

+

+"He can't write with his left hand," says the old gentleman.  "If he

+could use his right hand, you would see that he wrote his own letters

+and mine too.  Look at both, please—they're by the same hand."

+

+The lawyer done it, and says:

+

+"I believe it's so—and if it ain't so, there's a heap stronger

+resemblance than I'd noticed before, anyway.  Well, well, well!  I

+thought we was right on the track of a solution, but it's gone to grass,

+partly.  But anyway, one thing is proved—these two ain't either of 'em

+Wilkses"—and he wagged his head towards the king and the duke.

+

+Well, what do you think?  That muleheaded old fool wouldn't give in

+then! Indeed he wouldn't.  Said it warn't no fair test.  Said his

+brother William was the cussedest joker in the world, and hadn't tried

+to write—he see William was going to play one of his jokes the minute

+he put the pen to paper.  And so he warmed up and went warbling and

+warbling right along till he was actuly beginning to believe what he was

+saying himself; but pretty soon the new gentleman broke in, and says:

+

+"I've thought of something.  Is there anybody here that helped to lay

+out my br—helped to lay out the late Peter Wilks for burying?"

+

+"Yes," says somebody, "me and Ab Turner done it.  We're both here."

+

+Then the old man turns towards the king, and says:

+

+"Perhaps this gentleman can tell me what was tattooed on his breast?"

+

+Blamed if the king didn't have to brace up mighty quick, or he'd a

+squshed down like a bluff bank that the river has cut under, it took

+him so sudden; and, mind you, it was a thing that was calculated to make

+most anybody sqush to get fetched such a solid one as that without any

+notice, because how was he going to know what was tattooed on the man?

+ He whitened a little; he couldn't help it; and it was mighty still in

+there, and everybody bending a little forwards and gazing at him.  Says

+I to myself, now he'll throw up the sponge—there ain't no more use.

+ Well, did he?  A body can't hardly believe it, but he didn't.  I reckon

+he thought he'd keep the thing up till he tired them people out, so

+they'd thin out, and him and the duke could break loose and get away.

+ Anyway, he set there, and pretty soon he begun to smile, and says:

+

+"Mf!  It's a very tough question, ain't it!  yes, sir, I k'n

+tell you what's tattooed on his breast.  It's jest a small, thin, blue

+arrow—that's what it is; and if you don't look clost, you can't see it.

+ now what do you say—hey?"

+

+Well, I never see anything like that old blister for clean out-and-out

+cheek.

+

+The new old gentleman turns brisk towards Ab Turner and his pard, and

+his eye lights up like he judged he'd got the king this time, and

+says:

+

+"There—you've heard what he said!  Was there any such mark on Peter

+Wilks' breast?"

+

+Both of them spoke up and says:

+

+"We didn't see no such mark."

+

+"Good!" says the old gentleman.  "Now, what you did see on his breast

+was a small dim P, and a B (which is an initial he dropped when he was

+young), and a W, with dashes between them, so:  P—B—W"—and he marked

+them that way on a piece of paper.  "Come, ain't that what you saw?"

+

+Both of them spoke up again, and says:

+

+"No, we didn't.  We never seen any marks at all."

+

+Well, everybody was in a state of mind now, and they sings out:

+

+"The whole bilin' of 'm 's frauds!  Le's duck 'em! le's drown 'em!

+le's ride 'em on a rail!" and everybody was whooping at once, and there

+was a rattling powwow.  But the lawyer he jumps on the table and yells,

+and says:

+

+"Gentlemen—gentlemen!  Hear me just a word—just a single word—if you

+please!  There's one way yet—let's go and dig up the corpse and look."

+

+That took them.

+

+"Hooray!" they all shouted, and was starting right off; but the lawyer

+and the doctor sung out:

+

+"Hold on, hold on!  Collar all these four men and the boy, and fetch

+them along, too!"

+

+"We'll do it!" they all shouted; "and if we don't find them marks we'll

+lynch the whole gang!"

+

+I was scared, now, I tell you.  But there warn't no getting away, you

+know. They gripped us all, and marched us right along, straight for the

+graveyard, which was a mile and a half down the river, and the whole

+town at our heels, for we made noise enough, and it was only nine in the

+evening.

+

+As we went by our house I wished I hadn't sent Mary Jane out of town;

+because now if I could tip her the wink she'd light out and save me, and

+blow on our dead-beats.

+

+Well, we swarmed along down the river road, just carrying on like

+wildcats; and to make it more scary the sky was darking up, and the

+lightning beginning to wink and flitter, and the wind to shiver amongst

+the leaves. This was the most awful trouble and most dangersome I ever

+was in; and I was kinder stunned; everything was going so different from

+what I had allowed for; stead of being fixed so I could take my own time

+if I wanted to, and see all the fun, and have Mary Jane at my back to

+save me and set me free when the close-fit come, here was nothing in the

+world betwixt me and sudden death but just them tattoo-marks.  If they

+didn't find them—

+

+I couldn't bear to think about it; and yet, somehow, I couldn't think

+about nothing else.  It got darker and darker, and it was a beautiful

+time to give the crowd the slip; but that big husky had me by the

+wrist—Hines—and a body might as well try to give Goliar the slip.  He

+dragged me right along, he was so excited, and I had to run to keep up.

+

+When they got there they swarmed into the graveyard and washed over it

+like an overflow.  And when they got to the grave they found they had

+about a hundred times as many shovels as they wanted, but nobody hadn't

+thought to fetch a lantern.  But they sailed into digging anyway by the

+flicker of the lightning, and sent a man to the nearest house, a half a

+mile off, to borrow one.

+

+So they dug and dug like everything; and it got awful dark, and the rain

+started, and the wind swished and swushed along, and the lightning come

+brisker and brisker, and the thunder boomed; but them people never took

+no notice of it, they was so full of this business; and one minute

+you could see everything and every face in that big crowd, and the

+shovelfuls of dirt sailing up out of the grave, and the next second the

+dark wiped it all out, and you couldn't see nothing at all.

+

+At last they got out the coffin and begun to unscrew the lid, and then

+such another crowding and shouldering and shoving as there was, to

+scrouge in and get a sight, you never see; and in the dark, that way, it

+was awful.  Hines he hurt my wrist dreadful pulling and tugging so,

+and I reckon he clean forgot I was in the world, he was so excited and

+panting.

+

+All of a sudden the lightning let go a perfect sluice of white glare,

+and somebody sings out:

+

+"By the living jingo, here's the bag of gold on his breast!"

+

+Hines let out a whoop, like everybody else, and dropped my wrist and

+give a big surge to bust his way in and get a look, and the way I lit

+out and shinned for the road in the dark there ain't nobody can tell.

+

+I had the road all to myself, and I fairly flew—leastways, I had it all

+to myself except the solid dark, and the now-and-then glares, and the

+buzzing of the rain, and the thrashing of the wind, and the splitting of

+the thunder; and sure as you are born I did clip it along!

+

+When I struck the town I see there warn't nobody out in the storm, so

+I never hunted for no back streets, but humped it straight through the

+main one; and when I begun to get towards our house I aimed my eye and

+set it. No light there; the house all dark—which made me feel sorry and

+disappointed, I didn't know why.  But at last, just as I was sailing by,

+flash comes the light in Mary Jane's window! and my heart swelled up

+sudden, like to bust; and the same second the house and all was behind

+me in the dark, and wasn't ever going to be before me no more in this

+world. She was the best girl I ever see, and had the most sand.

+

+The minute I was far enough above the town to see I could make the

+towhead, I begun to look sharp for a boat to borrow, and the first

+time the lightning showed me one that wasn't chained I snatched it and

+shoved. It was a canoe, and warn't fastened with nothing but a rope.

+ The towhead was a rattling big distance off, away out there in the

+middle of the river, but I didn't lose no time; and when I struck the

+raft at last I was so fagged I would a just laid down to blow and gasp

+if I could afforded it.  But I didn't.  As I sprung aboard I sung out:

+

+"Out with you, Jim, and set her loose!  Glory be to goodness, we're shut

+of them!"

+

+Jim lit out, and was a-coming for me with both arms spread, he was so

+full of joy; but when I glimpsed him in the lightning my heart shot up

+in my mouth and I went overboard backwards; for I forgot he was old King

+Lear and a drownded A-rab all in one, and it most scared the livers and

+lights out of me.  But Jim fished me out, and was going to hug me and

+bless me, and so on, he was so glad I was back and we was shut of the

+king and the duke, but I says:

+

+"Not now; have it for breakfast, have it for breakfast!  Cut loose and

+let her slide!"

+

+So in two seconds away we went a-sliding down the river, and it did

+seem so good to be free again and all by ourselves on the big river, and

+nobody to bother us.  I had to skip around a bit, and jump up and crack

+my heels a few times—I couldn't help it; but about the third crack

+I noticed a sound that I knowed mighty well, and held my breath and

+listened and waited; and sure enough, when the next flash busted out

+over the water, here they come!—and just a-laying to their oars and

+making their skiff hum!  It was the king and the duke.

+

+So I wilted right down on to the planks then, and give up; and it was

+all I could do to keep from crying.

+

+

+

+

+CHAPTER XXX.

+

+WHEN they got aboard the king went for me, and shook me by the collar,

+and says:

+

+"Tryin' to give us the slip, was ye, you pup!  Tired of our company,

+hey?"

+

+I says:

+

+"No, your majesty, we warn't—please don't, your majesty!"

+

+"Quick, then, and tell us what was your idea, or I'll shake the

+insides out o' you!"

+

+"Honest, I'll tell you everything just as it happened, your majesty.

+ The man that had a-holt of me was very good to me, and kept saying he

+had a boy about as big as me that died last year, and he was sorry

+to see a boy in such a dangerous fix; and when they was all took by

+surprise by finding the gold, and made a rush for the coffin, he lets go

+of me and whispers, 'Heel it now, or they'll hang ye, sure!' and I lit

+out.  It didn't seem no good for me to stay—I couldn't do nothing,

+and I didn't want to be hung if I could get away.  So I never stopped

+running till I found the canoe; and when I got here I told Jim to hurry,

+or they'd catch me and hang me yet, and said I was afeard you and the

+duke wasn't alive now, and I was awful sorry, and so was Jim, and was

+awful glad when we see you coming; you may ask Jim if I didn't."

+

+Jim said it was so; and the king told him to shut up, and said, "Oh,

+yes, it's mighty likely!" and shook me up again, and said he reckoned

+he'd drownd me.  But the duke says:

+

+"Leggo the boy, you old idiot!  Would you a done any different?  Did

+you inquire around for him when you got loose?  I don't remember it."

+

+So the king let go of me, and begun to cuss that town and everybody in

+it. But the duke says:

+

+"You better a blame' sight give yourself a good cussing, for you're

+the one that's entitled to it most.  You hain't done a thing from the

+start that had any sense in it, except coming out so cool and cheeky

+with that imaginary blue-arrow mark.  That was bright—it was right

+down bully; and it was the thing that saved us.  For if it hadn't been

+for that they'd a jailed us till them Englishmen's baggage come—and

+then—the penitentiary, you bet! But that trick took 'em to the

+graveyard, and the gold done us a still bigger kindness; for if the

+excited fools hadn't let go all holts and made that rush to get a

+look we'd a slept in our cravats to-night—cravats warranted to wear,

+too—longer than we'd need 'em."

+

+They was still a minute—thinking; then the king says, kind of

+absent-minded like:

+

+"Mf!  And we reckoned the niggers stole it!"

+

+That made me squirm!

+

+"Yes," says the duke, kinder slow and deliberate and sarcastic, "we

+did."

+

+After about a half a minute the king drawls out:

+

+"Leastways, I did."

+

+The duke says, the same way:

+

+"On the contrary, I did."

+

+The king kind of ruffles up, and says:

+

+"Looky here, Bilgewater, what'r you referrin' to?"

+

+The duke says, pretty brisk:

+

+"When it comes to that, maybe you'll let me ask, what was you

+referring to?"

+

+"Shucks!" says the king, very sarcastic; "but I don't know—maybe you was

+asleep, and didn't know what you was about."

+

+The duke bristles up now, and says:

+

+"Oh, let up on this cussed nonsense; do you take me for a blame' fool?

+Don't you reckon I know who hid that money in that coffin?"

+

+"Yes, sir!  I know you do know, because you done it yourself!"

+

+"It's a lie!"—and the duke went for him.  The king sings out:

+

+"Take y'r hands off!—leggo my throat!—I take it all back!"

+

+The duke says:

+

+"Well, you just own up, first, that you did hide that money there,

+intending to give me the slip one of these days, and come back and dig

+it up, and have it all to yourself."

+

+"Wait jest a minute, duke—answer me this one question, honest and fair;

+if you didn't put the money there, say it, and I'll b'lieve you, and

+take back everything I said."

+

+"You old scoundrel, I didn't, and you know I didn't.  There, now!"

+

+"Well, then, I b'lieve you.  But answer me only jest this one more—now

+don't git mad; didn't you have it in your mind to hook the money and

+hide it?"

+

+The duke never said nothing for a little bit; then he says:

+

+"Well, I don't care if I did, I didn't do it, anyway.  But you not

+only had it in mind to do it, but you done it."

+

+"I wisht I never die if I done it, duke, and that's honest.  I won't say

+I warn't goin' to do it, because I was; but you—I mean somebody—got in

+ahead o' me."

+

+"It's a lie!  You done it, and you got to say you done it, or—"

+

+The king began to gurgle, and then he gasps out:

+

+"'Nough!—I own up!"

+

+I was very glad to hear him say that; it made me feel much more easier

+than what I was feeling before.  So the duke took his hands off and

+says:

+

+"If you ever deny it again I'll drown you.  It's well for you to set

+there and blubber like a baby—it's fitten for you, after the way

+you've acted. I never see such an old ostrich for wanting to gobble

+everything—and I a-trusting you all the time, like you was my own

+father.  You ought to been ashamed of yourself to stand by and hear it

+saddled on to a lot of poor niggers, and you never say a word for 'em.

+ It makes me feel ridiculous to think I was soft enough to believe

+that rubbage.  Cuss you, I can see now why you was so anxious to make

+up the deffisit—you wanted to get what money I'd got out of the Nonesuch

+and one thing or another, and scoop it all!"

+

+The king says, timid, and still a-snuffling:

+

+"Why, duke, it was you that said make up the deffisit; it warn't me."

+

+"Dry up!  I don't want to hear no more out of you!" says the duke.  "And

+now you see what you GOT by it.  They've got all their own money back,

+and all of ourn but a shekel or two besides.  G'long to bed, and

+don't you deffersit me no more deffersits, long 's you live!"

+

+So the king sneaked into the wigwam and took to his bottle for comfort,

+and before long the duke tackled HIS bottle; and so in about a half an

+hour they was as thick as thieves again, and the tighter they got the

+lovinger they got, and went off a-snoring in each other's arms.  They

+both got powerful mellow, but I noticed the king didn't get mellow

+enough to forget to remember to not deny about hiding the money-bag

+again.  That made me feel easy and satisfied.  Of course when they got

+to snoring we had a long gabble, and I told Jim everything.

+

+

+

+

+CHAPTER XXXI.

+

+WE dasn't stop again at any town for days and days; kept right along

+down the river.  We was down south in the warm weather now, and a mighty

+long ways from home.  We begun to come to trees with Spanish moss on

+them, hanging down from the limbs like long, gray beards.  It was the

+first I ever see it growing, and it made the woods look solemn and

+dismal.  So now the frauds reckoned they was out of danger, and they

+begun to work the villages again.

+

+First they done a lecture on temperance; but they didn't make enough

+for them both to get drunk on.  Then in another village they started

+a dancing-school; but they didn't know no more how to dance than a

+kangaroo does; so the first prance they made the general public jumped

+in and pranced them out of town.  Another time they tried to go at

+yellocution; but they didn't yellocute long till the audience got up and

+give them a solid good cussing, and made them skip out.  They tackled

+missionarying, and mesmerizing, and doctoring, and telling fortunes, and

+a little of everything; but they couldn't seem to have no luck.  So at

+last they got just about dead broke, and laid around the raft as she

+floated along, thinking and thinking, and never saying nothing, by the

+half a day at a time, and dreadful blue and desperate.

+

+And at last they took a change and begun to lay their heads together in

+the wigwam and talk low and confidential two or three hours at a time.

+Jim and me got uneasy.  We didn't like the look of it.  We judged they

+was studying up some kind of worse deviltry than ever.  We turned it

+over and over, and at last we made up our minds they was going to break

+into somebody's house or store, or was going into the counterfeit-money

+business, or something. So then we was pretty scared, and made up an

+agreement that we wouldn't have nothing in the world to do with such

+actions, and if we ever got the least show we would give them the cold

+shake and clear out and leave them behind. Well, early one morning we

+hid the raft in a good, safe place about two mile below a little bit of

+a shabby village named Pikesville, and the king he went ashore and told

+us all to stay hid whilst he went up to town and smelt around to see

+if anybody had got any wind of the Royal Nonesuch there yet. ("House to

+rob, you mean," says I to myself; "and when you get through robbing it

+you'll come back here and wonder what has become of me and Jim and the

+raft—and you'll have to take it out in wondering.") And he said if he

+warn't back by midday the duke and me would know it was all right, and

+we was to come along.

+

+So we stayed where we was.  The duke he fretted and sweated around, and

+was in a mighty sour way.  He scolded us for everything, and we couldn't

+seem to do nothing right; he found fault with every little thing.

+Something was a-brewing, sure.  I was good and glad when midday come

+and no king; we could have a change, anyway—and maybe a chance for the

+change on top of it.  So me and the duke went up to the village, and

+hunted around there for the king, and by and by we found him in the

+back room of a little low doggery, very tight, and a lot of loafers

+bullyragging him for sport, and he a-cussing and a-threatening with all

+his might, and so tight he couldn't walk, and couldn't do nothing to

+them.  The duke he begun to abuse him for an old fool, and the king

+begun to sass back, and the minute they was fairly at it I lit out and

+shook the reefs out of my hind legs, and spun down the river road like

+a deer, for I see our chance; and I made up my mind that it would be a

+long day before they ever see me and Jim again.  I got down there all

+out of breath but loaded up with joy, and sung out:

+

+"Set her loose, Jim! we're all right now!"

+

+But there warn't no answer, and nobody come out of the wigwam.  Jim was

+gone!  I set up a shout—and then another—and then another one; and run

+this way and that in the woods, whooping and screeching; but it warn't

+no use—old Jim was gone.  Then I set down and cried; I couldn't help

+it. But I couldn't set still long.  Pretty soon I went out on the road,

+trying to think what I better do, and I run across a boy walking, and

+asked him if he'd seen a strange nigger dressed so and so, and he says:

+

+"Yes."

+

+"Whereabouts?" says I.

+

+"Down to Silas Phelps' place, two mile below here.  He's a runaway

+nigger, and they've got him.  Was you looking for him?"

+

+"You bet I ain't!  I run across him in the woods about an hour or two

+ago, and he said if I hollered he'd cut my livers out—and told me to lay

+down and stay where I was; and I done it.  Been there ever since; afeard

+to come out."

+

+"Well," he says, "you needn't be afeard no more, becuz they've got him.

+He run off f'm down South, som'ers."

+

+"It's a good job they got him."

+

+"Well, I reckon!  There's two hunderd dollars reward on him.  It's

+like picking up money out'n the road."

+

+"Yes, it is—and I could a had it if I'd been big enough; I see him

+first. Who nailed him?"

+

+"It was an old fellow—a stranger—and he sold out his chance in him for

+forty dollars, becuz he's got to go up the river and can't wait.  Think

+o' that, now!  You bet I'd wait, if it was seven year."

+

+"That's me, every time," says I.  "But maybe his chance ain't worth

+no more than that, if he'll sell it so cheap.  Maybe there's something

+ain't straight about it."

+

+"But it is, though—straight as a string.  I see the handbill myself.

+ It tells all about him, to a dot—paints him like a picture, and tells

+the plantation he's frum, below Newrleans.  No-sirree-bob, they

+ain't no trouble 'bout that speculation, you bet you.  Say, gimme a

+chaw tobacker, won't ye?"

+

+I didn't have none, so he left.  I went to the raft, and set down in the

+wigwam to think.  But I couldn't come to nothing.  I thought till I wore

+my head sore, but I couldn't see no way out of the trouble.  After all

+this long journey, and after all we'd done for them scoundrels, here it

+was all come to nothing, everything all busted up and ruined, because

+they could have the heart to serve Jim such a trick as that, and make

+him a slave again all his life, and amongst strangers, too, for forty

+dirty dollars.

+

+Once I said to myself it would be a thousand times better for Jim to

+be a slave at home where his family was, as long as he'd got to be a

+slave, and so I'd better write a letter to Tom Sawyer and tell him to

+tell Miss Watson where he was.  But I soon give up that notion for two

+things: she'd be mad and disgusted at his rascality and ungratefulness

+for leaving her, and so she'd sell him straight down the river again;

+and if she didn't, everybody naturally despises an ungrateful nigger,

+and they'd make Jim feel it all the time, and so he'd feel ornery and

+disgraced. And then think of me!  It would get all around that Huck

+Finn helped a nigger to get his freedom; and if I was ever to see

+anybody from that town again I'd be ready to get down and lick his boots

+for shame.  That's just the way:  a person does a low-down thing, and

+then he don't want to take no consequences of it. Thinks as long as he

+can hide it, it ain't no disgrace.  That was my fix exactly. The more I

+studied about this the more my conscience went to grinding me, and the

+more wicked and low-down and ornery I got to feeling. And at last, when

+it hit me all of a sudden that here was the plain hand of Providence

+slapping me in the face and letting me know my wickedness was being

+watched all the time from up there in heaven, whilst I was stealing a

+poor old woman's nigger that hadn't ever done me no harm, and now was

+showing me there's One that's always on the lookout, and ain't a-going

+to allow no such miserable doings to go only just so fur and no further,

+I most dropped in my tracks I was so scared.  Well, I tried the best I

+could to kinder soften it up somehow for myself by saying I was brung

+up wicked, and so I warn't so much to blame; but something inside of me

+kept saying, "There was the Sunday-school, you could a gone to it; and

+if you'd a done it they'd a learnt you there that people that acts as

+I'd been acting about that nigger goes to everlasting fire."

+

+It made me shiver.  And I about made up my mind to pray, and see if I

+couldn't try to quit being the kind of a boy I was and be better.  So

+I kneeled down.  But the words wouldn't come.  Why wouldn't they?  It

+warn't no use to try and hide it from Him.  Nor from me, neither.  I

+knowed very well why they wouldn't come.  It was because my heart warn't

+right; it was because I warn't square; it was because I was playing

+double.  I was letting on to give up sin, but away inside of me I was

+holding on to the biggest one of all.  I was trying to make my mouth

+say I would do the right thing and the clean thing, and go and write

+to that nigger's owner and tell where he was; but deep down in me I

+knowed it was a lie, and He knowed it.  You can't pray a lie—I found

+that out.

+

+So I was full of trouble, full as I could be; and didn't know what to

+do. At last I had an idea; and I says, I'll go and write the letter—and

+then see if I can pray.  Why, it was astonishing, the way I felt as

+light as a feather right straight off, and my troubles all gone.  So I

+got a piece of paper and a pencil, all glad and excited, and set down

+and wrote:

+

+Miss Watson, your runaway nigger Jim is down here two mile below

+Pikesville, and Mr. Phelps has got him and he will give him up for the

+reward if you send.

+

+Huck Finn.

+

+I felt good and all washed clean of sin for the first time I had ever

+felt so in my life, and I knowed I could pray now.  But I didn't do it

+straight off, but laid the paper down and set there thinking—thinking

+how good it was all this happened so, and how near I come to being lost

+and going to hell.  And went on thinking.  And got to thinking over our

+trip down the river; and I see Jim before me all the time:  in the day

+and in the night-time, sometimes moonlight, sometimes storms, and we

+a-floating along, talking and singing and laughing.  But somehow I

+couldn't seem to strike no places to harden me against him, but only the

+other kind.  I'd see him standing my watch on top of his'n, 'stead of

+calling me, so I could go on sleeping; and see him how glad he was when

+I come back out of the fog; and when I come to him again in the swamp,

+up there where the feud was; and such-like times; and would always call

+me honey, and pet me and do everything he could think of for me, and how

+good he always was; and at last I struck the time I saved him by telling

+the men we had small-pox aboard, and he was so grateful, and said I was

+the best friend old Jim ever had in the world, and the only one he's

+got now; and then I happened to look around and see that paper.

+

+It was a close place.  I took it up, and held it in my hand.  I was

+a-trembling, because I'd got to decide, forever, betwixt two things, and

+I knowed it.  I studied a minute, sort of holding my breath, and then

+says to myself:

+

+"All right, then, I'll go to hell"—and tore it up.

+

+It was awful thoughts and awful words, but they was said.  And I let

+them stay said; and never thought no more about reforming.  I shoved the

+whole thing out of my head, and said I would take up wickedness again,

+which was in my line, being brung up to it, and the other warn't.  And

+for a starter I would go to work and steal Jim out of slavery again;

+and if I could think up anything worse, I would do that, too; because as

+long as I was in, and in for good, I might as well go the whole hog.

+

+Then I set to thinking over how to get at it, and turned over some

+considerable many ways in my mind; and at last fixed up a plan that

+suited me.  So then I took the bearings of a woody island that was down

+the river a piece, and as soon as it was fairly dark I crept out with my

+raft and went for it, and hid it there, and then turned in.  I slept the

+night through, and got up before it was light, and had my breakfast,

+and put on my store clothes, and tied up some others and one thing or

+another in a bundle, and took the canoe and cleared for shore.  I landed

+below where I judged was Phelps's place, and hid my bundle in the woods,

+and then filled up the canoe with water, and loaded rocks into her and

+sunk her where I could find her again when I wanted her, about a quarter

+of a mile below a little steam sawmill that was on the bank.

+

+Then I struck up the road, and when I passed the mill I see a sign on

+it, "Phelps's Sawmill," and when I come to the farm-houses, two or

+three hundred yards further along, I kept my eyes peeled, but didn't

+see nobody around, though it was good daylight now.  But I didn't mind,

+because I didn't want to see nobody just yet—I only wanted to get the

+lay of the land. According to my plan, I was going to turn up there from

+the village, not from below.  So I just took a look, and shoved along,

+straight for town. Well, the very first man I see when I got there was

+the duke.  He was sticking up a bill for the Royal Nonesuch—three-night

+performance—like that other time.  They had the cheek, them frauds!  I

+was right on him before I could shirk.  He looked astonished, and says:

+

+"Hel-lo!  Where'd you come from?"  Then he says, kind of glad and

+eager, "Where's the raft?—got her in a good place?"

+

+I says:

+

+"Why, that's just what I was going to ask your grace."

+

+Then he didn't look so joyful, and says:

+

+"What was your idea for asking me?" he says.

+

+"Well," I says, "when I see the king in that doggery yesterday I says

+to myself, we can't get him home for hours, till he's soberer; so I went

+a-loafing around town to put in the time and wait.  A man up and offered

+me ten cents to help him pull a skiff over the river and back to fetch

+a sheep, and so I went along; but when we was dragging him to the boat,

+and the man left me a-holt of the rope and went behind him to shove him

+along, he was too strong for me and jerked loose and run, and we after

+him.  We didn't have no dog, and so we had to chase him all over the

+country till we tired him out.  We never got him till dark; then we

+fetched him over, and I started down for the raft.  When I got there and

+see it was gone, I says to myself, 'They've got into trouble and had to

+leave; and they've took my nigger, which is the only nigger I've got in

+the world, and now I'm in a strange country, and ain't got no property

+no more, nor nothing, and no way to make my living;' so I set down and

+cried.  I slept in the woods all night.  But what did become of the

+raft, then?—and Jim—poor Jim!"

+

+"Blamed if I know—that is, what's become of the raft.  That old fool had

+made a trade and got forty dollars, and when we found him in the doggery

+the loafers had matched half-dollars with him and got every cent but

+what he'd spent for whisky; and when I got him home late last night and

+found the raft gone, we said, 'That little rascal has stole our raft and

+shook us, and run off down the river.'"

+

+"I wouldn't shake my nigger, would I?—the only nigger I had in the

+world, and the only property."

+

+"We never thought of that.  Fact is, I reckon we'd come to consider him

+our nigger; yes, we did consider him so—goodness knows we had trouble

+enough for him.  So when we see the raft was gone and we flat broke,

+there warn't anything for it but to try the Royal Nonesuch another

+shake. And I've pegged along ever since, dry as a powder-horn.  Where's

+that ten cents? Give it here."

+

+I had considerable money, so I give him ten cents, but begged him to

+spend it for something to eat, and give me some, because it was all the

+money I had, and I hadn't had nothing to eat since yesterday.  He never

+said nothing.  The next minute he whirls on me and says:

+

+"Do you reckon that nigger would blow on us?  We'd skin him if he done

+that!"

+

+"How can he blow?  Hain't he run off?"

+

+"No!  That old fool sold him, and never divided with me, and the money's

+gone."

+

+"Sold him?"  I says, and begun to cry; "why, he was my nigger, and

+that was my money.  Where is he?—I want my nigger."

+

+"Well, you can't get your nigger, that's all—so dry up your

+blubbering. Looky here—do you think you'd venture to blow on us?

+ Blamed if I think I'd trust you.  Why, if you was to blow on us—"

+

+He stopped, but I never see the duke look so ugly out of his eyes

+before. I went on a-whimpering, and says:

+

+"I don't want to blow on nobody; and I ain't got no time to blow, nohow.

+I got to turn out and find my nigger."

+

+He looked kinder bothered, and stood there with his bills fluttering on

+his arm, thinking, and wrinkling up his forehead.  At last he says:

+

+"I'll tell you something.  We got to be here three days.  If you'll

+promise you won't blow, and won't let the nigger blow, I'll tell you

+where to find him."

+

+So I promised, and he says:

+

+"A farmer by the name of Silas Ph—" and then he stopped.  You see, he

+started to tell me the truth; but when he stopped that way, and begun to

+study and think again, I reckoned he was changing his mind.  And so he

+was. He wouldn't trust me; he wanted to make sure of having me out of

+the way the whole three days.  So pretty soon he says:

+

+"The man that bought him is named Abram Foster—Abram G. Foster—and he

+lives forty mile back here in the country, on the road to Lafayette."

+

+"All right," I says, "I can walk it in three days.  And I'll start this

+very afternoon."

+

+"No you wont, you'll start now; and don't you lose any time about it,

+neither, nor do any gabbling by the way.  Just keep a tight tongue in

+your head and move right along, and then you won't get into trouble with

+us, d'ye hear?"

+

+That was the order I wanted, and that was the one I played for.  I

+wanted to be left free to work my plans.

+

+"So clear out," he says; "and you can tell Mr. Foster whatever you want

+to. Maybe you can get him to believe that Jim is your nigger—some

+idiots don't require documents—leastways I've heard there's such down

+South here.  And when you tell him the handbill and the reward's bogus,

+maybe he'll believe you when you explain to him what the idea was for

+getting 'em out.  Go 'long now, and tell him anything you want to; but

+mind you don't work your jaw any between here and there."

+

+So I left, and struck for the back country.  I didn't look around, but I

+kinder felt like he was watching me.  But I knowed I could tire him out

+at that.  I went straight out in the country as much as a mile before

+I stopped; then I doubled back through the woods towards Phelps'.  I

+reckoned I better start in on my plan straight off without fooling

+around, because I wanted to stop Jim's mouth till these fellows could

+get away.  I didn't want no trouble with their kind.  I'd seen all I

+wanted to of them, and wanted to get entirely shut of them.

+

+

+

+

+CHAPTER XXXII.

+

+WHEN I got there it was all still and Sunday-like, and hot and sunshiny;

+the hands was gone to the fields; and there was them kind of faint

+dronings of bugs and flies in the air that makes it seem so lonesome and

+like everybody's dead and gone; and if a breeze fans along and quivers

+the leaves it makes you feel mournful, because you feel like it's

+spirits whispering—spirits that's been dead ever so many years—and you

+always think they're talking about you.  As a general thing it makes a

+body wish he was dead, too, and done with it all.

+

+Phelps' was one of these little one-horse cotton plantations, and they

+all look alike.  A rail fence round a two-acre yard; a stile made out

+of logs sawed off and up-ended in steps, like barrels of a different

+length, to climb over the fence with, and for the women to stand on when

+they are going to jump on to a horse; some sickly grass-patches in the

+big yard, but mostly it was bare and smooth, like an old hat with the

+nap rubbed off; big double log-house for the white folks—hewed logs,

+with the chinks stopped up with mud or mortar, and these mud-stripes

+been whitewashed some time or another; round-log kitchen, with a big

+broad, open but roofed passage joining it to the house; log smoke-house

+back of the kitchen; three little log nigger-cabins in a row t'other

+side the smoke-house; one little hut all by itself away down against

+the back fence, and some outbuildings down a piece the other side;

+ash-hopper and big kettle to bile soap in by the little hut; bench by

+the kitchen door, with bucket of water and a gourd; hound asleep there

+in the sun; more hounds asleep round about; about three shade trees away

+off in a corner; some currant bushes and gooseberry bushes in one place

+by the fence; outside of the fence a garden and a watermelon patch; then

+the cotton fields begins, and after the fields the woods.

+

+I went around and clumb over the back stile by the ash-hopper, and

+started for the kitchen.  When I got a little ways I heard the dim hum

+of a spinning-wheel wailing along up and sinking along down again;

+and then I knowed for certain I wished I was dead—for that is the

+lonesomest sound in the whole world.

+

+I went right along, not fixing up any particular plan, but just trusting

+to Providence to put the right words in my mouth when the time come; for

+I'd noticed that Providence always did put the right words in my mouth

+if I left it alone.

+

+When I got half-way, first one hound and then another got up and went

+for me, and of course I stopped and faced them, and kept still.  And

+such another powwow as they made!  In a quarter of a minute I was a kind

+of a hub of a wheel, as you may say—spokes made out of dogs—circle of

+fifteen of them packed together around me, with their necks and noses

+stretched up towards me, a-barking and howling; and more a-coming; you

+could see them sailing over fences and around corners from everywheres.

+

+A nigger woman come tearing out of the kitchen with a rolling-pin in her

+hand, singing out, "Begone you Tige! you Spot! begone sah!" and she

+fetched first one and then another of them a clip and sent them howling,

+and then the rest followed; and the next second half of them come back,

+wagging their tails around me, and making friends with me.  There ain't

+no harm in a hound, nohow.

+

+And behind the woman comes a little nigger girl and two little nigger

+boys without anything on but tow-linen shirts, and they hung on to their

+mother's gown, and peeped out from behind her at me, bashful, the way

+they always do.  And here comes the white woman running from the house,

+about forty-five or fifty year old, bareheaded, and her spinning-stick

+in her hand; and behind her comes her little white children, acting the

+same way the little niggers was doing.  She was smiling all over so she

+could hardly stand—and says:

+

+"It's you, at last!—ain't it?"

+

+I out with a "Yes'm" before I thought.

+

+She grabbed me and hugged me tight; and then gripped me by both hands

+and shook and shook; and the tears come in her eyes, and run down over;

+and she couldn't seem to hug and shake enough, and kept saying, "You

+don't look as much like your mother as I reckoned you would; but law

+sakes, I don't care for that, I'm so glad to see you!  Dear, dear, it

+does seem like I could eat you up!  Children, it's your cousin Tom!—tell

+him howdy."

+

+But they ducked their heads, and put their fingers in their mouths, and

+hid behind her.  So she run on:

+

+"Lize, hurry up and get him a hot breakfast right away—or did you get

+your breakfast on the boat?"

+

+I said I had got it on the boat.  So then she started for the house,

+leading me by the hand, and the children tagging after.  When we got

+there she set me down in a split-bottomed chair, and set herself down on

+a little low stool in front of me, holding both of my hands, and says:

+

+"Now I can have a good look at you; and, laws-a-me, I've been hungry

+for it a many and a many a time, all these long years, and it's come

+at last! We been expecting you a couple of days and more.  What kep'

+you?—boat get aground?"

+

+"Yes'm—she—"

+

+"Don't say yes'm—say Aunt Sally.  Where'd she get aground?"

+

+I didn't rightly know what to say, because I didn't know whether the

+boat would be coming up the river or down.  But I go a good deal on

+instinct; and my instinct said she would be coming up—from down towards

+Orleans. That didn't help me much, though; for I didn't know the names

+of bars down that way.  I see I'd got to invent a bar, or forget the

+name of the one we got aground on—or—Now I struck an idea, and fetched

+it out:

+

+"It warn't the grounding—that didn't keep us back but a little.  We

+blowed out a cylinder-head."

+

+"Good gracious! anybody hurt?"

+

+"No'm.  Killed a nigger."

+

+"Well, it's lucky; because sometimes people do get hurt.  Two years ago

+last Christmas your uncle Silas was coming up from Newrleans on the old

+Lally Rook, and she blowed out a cylinder-head and crippled a man.  And

+I think he died afterwards.  He was a Baptist.  Your uncle Silas knowed

+a family in Baton Rouge that knowed his people very well.  Yes, I

+remember now, he did die.  Mortification set in, and they had to

+amputate him. But it didn't save him.  Yes, it was mortification—that

+was it.  He turned blue all over, and died in the hope of a glorious

+resurrection. They say he was a sight to look at.  Your uncle's been up

+to the town every day to fetch you. And he's gone again, not more'n an

+hour ago; he'll be back any minute now. You must a met him on the road,

+didn't you?—oldish man, with a—"

+

+"No, I didn't see nobody, Aunt Sally.  The boat landed just at daylight,

+and I left my baggage on the wharf-boat and went looking around the town

+and out a piece in the country, to put in the time and not get here too

+soon; and so I come down the back way."

+

+"Who'd you give the baggage to?"

+

+"Nobody."

+

+"Why, child, it 'll be stole!"

+

+"Not where I hid it I reckon it won't," I says.

+

+"How'd you get your breakfast so early on the boat?"

+

+It was kinder thin ice, but I says:

+

+"The captain see me standing around, and told me I better have something

+to eat before I went ashore; so he took me in the texas to the officers'

+lunch, and give me all I wanted."

+

+I was getting so uneasy I couldn't listen good.  I had my mind on the

+children all the time; I wanted to get them out to one side and pump

+them a little, and find out who I was.  But I couldn't get no show, Mrs.

+Phelps kept it up and run on so.  Pretty soon she made the cold chills

+streak all down my back, because she says:

+

+"But here we're a-running on this way, and you hain't told me a word

+about Sis, nor any of them.  Now I'll rest my works a little, and you

+start up yourn; just tell me everything—tell me all about 'm all every

+one of 'm; and how they are, and what they're doing, and what they told

+you to tell me; and every last thing you can think of."

+

+Well, I see I was up a stump—and up it good.  Providence had stood by

+me this fur all right, but I was hard and tight aground now.  I see it

+warn't a bit of use to try to go ahead—I'd got to throw up my hand.  So

+I says to myself, here's another place where I got to resk the truth.

+ I opened my mouth to begin; but she grabbed me and hustled me in behind

+the bed, and says:

+

+"Here he comes!  Stick your head down lower—there, that'll do; you can't

+be seen now.  Don't you let on you're here.  I'll play a joke on him.

+Children, don't you say a word."

+

+I see I was in a fix now.  But it warn't no use to worry; there warn't

+nothing to do but just hold still, and try and be ready to stand from

+under when the lightning struck.

+

+I had just one little glimpse of the old gentleman when he come in; then

+the bed hid him.  Mrs. Phelps she jumps for him, and says:

+

+"Has he come?"

+

+"No," says her husband.

+

+"Good-ness gracious!" she says, "what in the warld can have become of

+him?"

+

+"I can't imagine," says the old gentleman; "and I must say it makes me

+dreadful uneasy."

+

+"Uneasy!" she says; "I'm ready to go distracted!  He must a come; and

+you've missed him along the road.  I know it's so—something tells me

+so."

+

+"Why, Sally, I couldn't miss him along the road—you know that."

+

+"But oh, dear, dear, what will Sis say!  He must a come!  You must a

+missed him.  He—"

+

+"Oh, don't distress me any more'n I'm already distressed.  I don't know

+what in the world to make of it.  I'm at my wit's end, and I don't mind

+acknowledging 't I'm right down scared.  But there's no hope that he's

+come; for he couldn't come and me miss him.  Sally, it's terrible—just

+terrible—something's happened to the boat, sure!"

+

+"Why, Silas!  Look yonder!—up the road!—ain't that somebody coming?"

+

+He sprung to the window at the head of the bed, and that give Mrs.

+Phelps the chance she wanted.  She stooped down quick at the foot of the

+bed and give me a pull, and out I come; and when he turned back from the

+window there she stood, a-beaming and a-smiling like a house afire, and

+I standing pretty meek and sweaty alongside.  The old gentleman stared,

+and says:

+

+"Why, who's that?"

+

+"Who do you reckon 't is?"

+

+"I hain't no idea.  Who is it?"

+

+"It's Tom Sawyer!"

+

+By jings, I most slumped through the floor!  But there warn't no time to

+swap knives; the old man grabbed me by the hand and shook, and kept on

+shaking; and all the time how the woman did dance around and laugh and

+cry; and then how they both did fire off questions about Sid, and Mary,

+and the rest of the tribe.

+

+But if they was joyful, it warn't nothing to what I was; for it was like

+being born again, I was so glad to find out who I was.  Well, they froze

+to me for two hours; and at last, when my chin was so tired it couldn't

+hardly go any more, I had told them more about my family—I mean the

+Sawyer family—than ever happened to any six Sawyer families.  And I

+explained all about how we blowed out a cylinder-head at the mouth of

+White River, and it took us three days to fix it.  Which was all right,

+and worked first-rate; because they didn't know but what it would take

+three days to fix it.  If I'd a called it a bolthead it would a done

+just as well.

+

+Now I was feeling pretty comfortable all down one side, and pretty

+uncomfortable all up the other.  Being Tom Sawyer was easy and

+comfortable, and it stayed easy and comfortable till by and by I hear a

+steamboat coughing along down the river.  Then I says to myself, s'pose

+Tom Sawyer comes down on that boat?  And s'pose he steps in here any

+minute, and sings out my name before I can throw him a wink to keep

+quiet?

+

+Well, I couldn't have it that way; it wouldn't do at all.  I must go

+up the road and waylay him.  So I told the folks I reckoned I would go

+up to the town and fetch down my baggage.  The old gentleman was for

+going along with me, but I said no, I could drive the horse myself, and

+I druther he wouldn't take no trouble about me.

+

+

+

+

+CHAPTER XXXIII.

+

+SO I started for town in the wagon, and when I was half-way I see a

+wagon coming, and sure enough it was Tom Sawyer, and I stopped and

+waited till he come along.  I says "Hold on!" and it stopped alongside,

+and his mouth opened up like a trunk, and stayed so; and he swallowed

+two or three times like a person that's got a dry throat, and then says:

+

+"I hain't ever done you no harm.  You know that.  So, then, what you

+want to come back and ha'nt me for?"

+

+I says:

+

+"I hain't come back—I hain't been gone."

+

+When he heard my voice it righted him up some, but he warn't quite

+satisfied yet.  He says:

+

+"Don't you play nothing on me, because I wouldn't on you.  Honest injun

+now, you ain't a ghost?"

+

+"Honest injun, I ain't," I says.

+

+"Well—I—I—well, that ought to settle it, of course; but I can't somehow

+seem to understand it no way.  Looky here, warn't you ever murdered at

+all?"

+

+"No.  I warn't ever murdered at all—I played it on them.  You come in

+here and feel of me if you don't believe me."

+

+So he done it; and it satisfied him; and he was that glad to see me

+again he didn't know what to do.  And he wanted to know all about it

+right off, because it was a grand adventure, and mysterious, and so it

+hit him where he lived.  But I said, leave it alone till by and by; and

+told his driver to wait, and we drove off a little piece, and I told

+him the kind of a fix I was in, and what did he reckon we better do?  He

+said, let him alone a minute, and don't disturb him.  So he thought and

+thought, and pretty soon he says:

+

+"It's all right; I've got it.  Take my trunk in your wagon, and let on

+it's your'n; and you turn back and fool along slow, so as to get to the

+house about the time you ought to; and I'll go towards town a piece, and

+take a fresh start, and get there a quarter or a half an hour after you;

+and you needn't let on to know me at first."

+

+I says:

+

+"All right; but wait a minute.  There's one more thing—a thing that

+nobody don't know but me.  And that is, there's a nigger here that

+I'm a-trying to steal out of slavery, and his name is Jim—old Miss

+Watson's Jim."

+

+He says:

+

+"What!  Why, Jim is—"

+

+He stopped and went to studying.  I says:

+

+"I know what you'll say.  You'll say it's dirty, low-down business; but

+what if it is?  I'm low down; and I'm a-going to steal him, and I want

+you keep mum and not let on.  Will you?"

+

+His eye lit up, and he says:

+

+"I'll help you steal him!"

+

+Well, I let go all holts then, like I was shot.  It was the most

+astonishing speech I ever heard—and I'm bound to say Tom Sawyer fell

+considerable in my estimation.  Only I couldn't believe it.  Tom Sawyer

+a nigger-stealer!

+

+"Oh, shucks!"  I says; "you're joking."

+

+"I ain't joking, either."

+

+"Well, then," I says, "joking or no joking, if you hear anything said

+about a runaway nigger, don't forget to remember that you don't know

+nothing about him, and I don't know nothing about him."

+

+Then we took the trunk and put it in my wagon, and he drove off his

+way and I drove mine.  But of course I forgot all about driving slow on

+accounts of being glad and full of thinking; so I got home a heap too

+quick for that length of a trip.  The old gentleman was at the door, and

+he says:

+

+"Why, this is wonderful!  Whoever would a thought it was in that mare

+to do it?  I wish we'd a timed her.  And she hain't sweated a hair—not

+a hair. It's wonderful.  Why, I wouldn't take a hundred dollars for that

+horse now—I wouldn't, honest; and yet I'd a sold her for fifteen before,

+and thought 'twas all she was worth."

+

+That's all he said.  He was the innocentest, best old soul I ever see.

+But it warn't surprising; because he warn't only just a farmer, he was

+a preacher, too, and had a little one-horse log church down back of the

+plantation, which he built it himself at his own expense, for a church

+and schoolhouse, and never charged nothing for his preaching, and it was

+worth it, too.  There was plenty other farmer-preachers like that, and

+done the same way, down South.

+

+In about half an hour Tom's wagon drove up to the front stile, and Aunt

+Sally she see it through the window, because it was only about fifty

+yards, and says:

+

+"Why, there's somebody come!  I wonder who 'tis?  Why, I do believe it's

+a stranger.  Jimmy" (that's one of the children) "run and tell Lize to

+put on another plate for dinner."

+

+Everybody made a rush for the front door, because, of course, a stranger

+don't come every year, and so he lays over the yaller-fever, for

+interest, when he does come.  Tom was over the stile and starting for

+the house; the wagon was spinning up the road for the village, and we

+was all bunched in the front door.  Tom had his store clothes on, and an

+audience—and that was always nuts for Tom Sawyer.  In them circumstances

+it warn't no trouble to him to throw in an amount of style that was

+suitable.  He warn't a boy to meeky along up that yard like a sheep; no,

+he come ca'm and important, like the ram.  When he got a-front of us he

+lifts his hat ever so gracious and dainty, like it was the lid of a box

+that had butterflies asleep in it and he didn't want to disturb them,

+and says:

+

+"Mr. Archibald Nichols, I presume?"

+

+"No, my boy," says the old gentleman, "I'm sorry to say 't your driver

+has deceived you; Nichols's place is down a matter of three mile more.

+Come in, come in."

+

+Tom he took a look back over his shoulder, and says, "Too late—he's out

+of sight."

+

+"Yes, he's gone, my son, and you must come in and eat your dinner with

+us; and then we'll hitch up and take you down to Nichols's."

+

+"Oh, I can't make you so much trouble; I couldn't think of it.  I'll

+walk—I don't mind the distance."

+

+"But we won't let you walk—it wouldn't be Southern hospitality to do

+it. Come right in."

+

+"Oh, do," says Aunt Sally; "it ain't a bit of trouble to us, not a

+bit in the world.  You must stay.  It's a long, dusty three mile, and

+we can't let you walk.  And, besides, I've already told 'em to put on

+another plate when I see you coming; so you mustn't disappoint us.  Come

+right in and make yourself at home."

+

+So Tom he thanked them very hearty and handsome, and let himself be

+persuaded, and come in; and when he was in he said he was a stranger

+from Hicksville, Ohio, and his name was William Thompson—and he made

+another bow.

+

+Well, he run on, and on, and on, making up stuff about Hicksville and

+everybody in it he could invent, and I getting a little nervious, and

+wondering how this was going to help me out of my scrape; and at last,

+still talking along, he reached over and kissed Aunt Sally right on the

+mouth, and then settled back again in his chair comfortable, and was

+going on talking; but she jumped up and wiped it off with the back of

+her hand, and says:

+

+"You owdacious puppy!"

+

+He looked kind of hurt, and says:

+

+"I'm surprised at you, m'am."

+

+"You're s'rp—Why, what do you reckon I am?  I've a good notion to take

+and—Say, what do you mean by kissing me?"

+

+He looked kind of humble, and says:

+

+"I didn't mean nothing, m'am.  I didn't mean no harm.  I—I—thought you'd

+like it."

+

+"Why, you born fool!"  She took up the spinning stick, and it looked

+like it was all she could do to keep from giving him a crack with it.

+ "What made you think I'd like it?"

+

+"Well, I don't know.  Only, they—they—told me you would."

+

+"They told you I would.  Whoever told you's another lunatic.  I

+never heard the beat of it.  Who's they?"

+

+"Why, everybody.  They all said so, m'am."

+

+It was all she could do to hold in; and her eyes snapped, and her

+fingers worked like she wanted to scratch him; and she says:

+

+"Who's 'everybody'?  Out with their names, or ther'll be an idiot

+short."

+

+He got up and looked distressed, and fumbled his hat, and says:

+

+"I'm sorry, and I warn't expecting it.  They told me to.  They all told

+me to.  They all said, kiss her; and said she'd like it.  They all said

+it—every one of them.  But I'm sorry, m'am, and I won't do it no more—I

+won't, honest."

+

+"You won't, won't you?  Well, I sh'd reckon you won't!"

+

+"No'm, I'm honest about it; I won't ever do it again—till you ask me."

+

+"Till I ask you!  Well, I never see the beat of it in my born days!

+ I lay you'll be the Methusalem-numskull of creation before ever I ask

+you—or the likes of you."

+

+"Well," he says, "it does surprise me so.  I can't make it out, somehow.

+They said you would, and I thought you would.  But—" He stopped and

+looked around slow, like he wished he could run across a friendly eye

+somewheres, and fetched up on the old gentleman's, and says, "Didn't

+you think she'd like me to kiss her, sir?"

+

+"Why, no; I—I—well, no, I b'lieve I didn't."

+

+Then he looks on around the same way to me, and says:

+

+"Tom, didn't you think Aunt Sally 'd open out her arms and say, 'Sid

+Sawyer—'"

+

+"My land!" she says, breaking in and jumping for him, "you impudent

+young rascal, to fool a body so—" and was going to hug him, but he

+fended her off, and says:

+

+"No, not till you've asked me first."

+

+So she didn't lose no time, but asked him; and hugged him and kissed

+him over and over again, and then turned him over to the old man, and he

+took what was left.  And after they got a little quiet again she says:

+

+"Why, dear me, I never see such a surprise.  We warn't looking for you

+at all, but only Tom.  Sis never wrote to me about anybody coming but

+him."

+

+"It's because it warn't intended for any of us to come but Tom," he

+says; "but I begged and begged, and at the last minute she let me

+come, too; so, coming down the river, me and Tom thought it would be a

+first-rate surprise for him to come here to the house first, and for me

+to by and by tag along and drop in, and let on to be a stranger.  But it

+was a mistake, Aunt Sally.  This ain't no healthy place for a stranger

+to come."

+

+"No—not impudent whelps, Sid.  You ought to had your jaws boxed; I

+hain't been so put out since I don't know when.  But I don't care, I

+don't mind the terms—I'd be willing to stand a thousand such jokes to

+have you here. Well, to think of that performance!  I don't deny it, I

+was most putrified with astonishment when you give me that smack."

+

+We had dinner out in that broad open passage betwixt the house and

+the kitchen; and there was things enough on that table for seven

+families—and all hot, too; none of your flabby, tough meat that's laid

+in a cupboard in a damp cellar all night and tastes like a hunk of

+old cold cannibal in the morning.  Uncle Silas he asked a pretty long

+blessing over it, but it was worth it; and it didn't cool it a bit,

+neither, the way I've seen them kind of interruptions do lots of times.

+ There was a considerable good deal of talk all the afternoon, and me

+and Tom was on the lookout all the time; but it warn't no use, they

+didn't happen to say nothing about any runaway nigger, and we was afraid

+to try to work up to it.  But at supper, at night, one of the little

+boys says:

+

+"Pa, mayn't Tom and Sid and me go to the show?"

+

+"No," says the old man, "I reckon there ain't going to be any; and you

+couldn't go if there was; because the runaway nigger told Burton and

+me all about that scandalous show, and Burton said he would tell the

+people; so I reckon they've drove the owdacious loafers out of town

+before this time."

+

+So there it was!—but I couldn't help it.  Tom and me was to sleep in the

+same room and bed; so, being tired, we bid good-night and went up to

+bed right after supper, and clumb out of the window and down the

+lightning-rod, and shoved for the town; for I didn't believe anybody was

+going to give the king and the duke a hint, and so if I didn't hurry up

+and give them one they'd get into trouble sure.

+

+On the road Tom he told me all about how it was reckoned I was murdered,

+and how pap disappeared pretty soon, and didn't come back no more, and

+what a stir there was when Jim run away; and I told Tom all about our

+Royal Nonesuch rapscallions, and as much of the raft voyage as I had

+time to; and as we struck into the town and up through the the middle of

+it--it was as much as half-after eight, then—here comes a raging rush of

+people with torches, and an awful whooping and yelling, and banging tin

+pans and blowing horns; and we jumped to one side to let them go by;

+and as they went by I see they had the king and the duke astraddle of a

+rail—that is, I knowed it was the king and the duke, though they was

+all over tar and feathers, and didn't look like nothing in the

+world that was human—just looked like a couple of monstrous big

+soldier-plumes.  Well, it made me sick to see it; and I was sorry for

+them poor pitiful rascals, it seemed like I couldn't ever feel any

+hardness against them any more in the world.  It was a dreadful thing to

+see.  Human beings can be awful cruel to one another.

+

+We see we was too late—couldn't do no good.  We asked some stragglers

+about it, and they said everybody went to the show looking very

+innocent; and laid low and kept dark till the poor old king was in the

+middle of his cavortings on the stage; then somebody give a signal, and

+the house rose up and went for them.

+

+So we poked along back home, and I warn't feeling so brash as I was

+before, but kind of ornery, and humble, and to blame, somehow—though

+I hadn't done nothing.  But that's always the way; it don't make no

+difference whether you do right or wrong, a person's conscience ain't

+got no sense, and just goes for him anyway.  If I had a yaller dog that

+didn't know no more than a person's conscience does I would pison him.

+It takes up more room than all the rest of a person's insides, and yet

+ain't no good, nohow.  Tom Sawyer he says the same.

+

+

+

+

+CHAPTER XXXIV.

+

+WE stopped talking, and got to thinking.  By and by Tom says:

+

+"Looky here, Huck, what fools we are to not think of it before!  I bet I

+know where Jim is."

+

+"No!  Where?"

+

+"In that hut down by the ash-hopper.  Why, looky here.  When we was at

+dinner, didn't you see a nigger man go in there with some vittles?"

+

+"Yes."

+

+"What did you think the vittles was for?"

+

+"For a dog."

+

+"So 'd I. Well, it wasn't for a dog."

+

+"Why?"

+

+"Because part of it was watermelon."

+

+"So it was—I noticed it.  Well, it does beat all that I never thought

+about a dog not eating watermelon.  It shows how a body can see and

+don't see at the same time."

+

+"Well, the nigger unlocked the padlock when he went in, and he locked it

+again when he came out.  He fetched uncle a key about the time we got up

+from table—same key, I bet.  Watermelon shows man, lock shows prisoner;

+and it ain't likely there's two prisoners on such a little plantation,

+and where the people's all so kind and good.  Jim's the prisoner.  All

+right—I'm glad we found it out detective fashion; I wouldn't give shucks

+for any other way.  Now you work your mind, and study out a plan to

+steal Jim, and I will study out one, too; and we'll take the one we like

+the best."

+

+What a head for just a boy to have!  If I had Tom Sawyer's head I

+wouldn't trade it off to be a duke, nor mate of a steamboat, nor clown

+in a circus, nor nothing I can think of.  I went to thinking out a plan,

+but only just to be doing something; I knowed very well where the right

+plan was going to come from.  Pretty soon Tom says:

+

+"Ready?"

+

+"Yes," I says.

+

+"All right—bring it out."

+

+"My plan is this," I says.  "We can easy find out if it's Jim in there.

+Then get up my canoe to-morrow night, and fetch my raft over from the

+island.  Then the first dark night that comes steal the key out of the

+old man's britches after he goes to bed, and shove off down the river

+on the raft with Jim, hiding daytimes and running nights, the way me and

+Jim used to do before.  Wouldn't that plan work?"

+

+"Work?  Why, cert'nly it would work, like rats a-fighting.  But it's

+too blame' simple; there ain't nothing to it.  What's the good of a

+plan that ain't no more trouble than that?  It's as mild as goose-milk.

+ Why, Huck, it wouldn't make no more talk than breaking into a soap

+factory."

+

+I never said nothing, because I warn't expecting nothing different; but

+I knowed mighty well that whenever he got his plan ready it wouldn't

+have none of them objections to it.

+

+And it didn't.  He told me what it was, and I see in a minute it was

+worth fifteen of mine for style, and would make Jim just as free a man

+as mine would, and maybe get us all killed besides.  So I was satisfied,

+and said we would waltz in on it.  I needn't tell what it was here,

+because I knowed it wouldn't stay the way, it was.  I knowed he would be

+changing it around every which way as we went along, and heaving in new

+bullinesses wherever he got a chance.  And that is what he done.

+

+Well, one thing was dead sure, and that was that Tom Sawyer was in

+earnest, and was actuly going to help steal that nigger out of slavery.

+That was the thing that was too many for me.  Here was a boy that was

+respectable and well brung up; and had a character to lose; and folks at

+home that had characters; and he was bright and not leather-headed; and

+knowing and not ignorant; and not mean, but kind; and yet here he was,

+without any more pride, or rightness, or feeling, than to stoop to

+this business, and make himself a shame, and his family a shame,

+before everybody.  I couldn't understand it no way at all.  It was

+outrageous, and I knowed I ought to just up and tell him so; and so be

+his true friend, and let him quit the thing right where he was and save

+himself. And I did start to tell him; but he shut me up, and says:

+

+"Don't you reckon I know what I'm about?  Don't I generly know what I'm

+about?"

+

+"Yes."

+

+"Didn't I say I was going to help steal the nigger?"

+

+"Yes."

+

+"Well, then."

+

+That's all he said, and that's all I said.  It warn't no use to say any

+more; because when he said he'd do a thing, he always done it.  But I

+couldn't make out how he was willing to go into this thing; so I just

+let it go, and never bothered no more about it.  If he was bound to have

+it so, I couldn't help it.

+

+When we got home the house was all dark and still; so we went on down to

+the hut by the ash-hopper for to examine it.  We went through the yard

+so as to see what the hounds would do.  They knowed us, and didn't make

+no more noise than country dogs is always doing when anything comes by

+in the night.  When we got to the cabin we took a look at the front and

+the two sides; and on the side I warn't acquainted with—which was the

+north side—we found a square window-hole, up tolerable high, with just

+one stout board nailed across it.  I says:

+

+"Here's the ticket.  This hole's big enough for Jim to get through if we

+wrench off the board."

+

+Tom says:

+

+"It's as simple as tit-tat-toe, three-in-a-row, and as easy as

+playing hooky.  I should hope we can find a way that's a little more

+complicated than that, Huck Finn."

+

+"Well, then," I says, "how 'll it do to saw him out, the way I done

+before I was murdered that time?"

+

+"That's more like," he says.  "It's real mysterious, and troublesome,

+and good," he says; "but I bet we can find a way that's twice as long.

+ There ain't no hurry; le's keep on looking around."

+

+Betwixt the hut and the fence, on the back side, was a lean-to that

+joined the hut at the eaves, and was made out of plank.  It was as long

+as the hut, but narrow—only about six foot wide.  The door to it was at

+the south end, and was padlocked.  Tom he went to the soap-kettle and

+searched around, and fetched back the iron thing they lift the lid with;

+so he took it and prized out one of the staples.  The chain fell down,

+and we opened the door and went in, and shut it, and struck a match,

+and see the shed was only built against a cabin and hadn't no connection

+with it; and there warn't no floor to the shed, nor nothing in it but

+some old rusty played-out hoes and spades and picks and a crippled plow.

+ The match went out, and so did we, and shoved in the staple again, and

+the door was locked as good as ever. Tom was joyful.  He says;

+

+"Now we're all right.  We'll dig him out.  It 'll take about a week!"

+

+Then we started for the house, and I went in the back door—you only have

+to pull a buckskin latch-string, they don't fasten the doors—but that

+warn't romantical enough for Tom Sawyer; no way would do him but he must

+climb up the lightning-rod.  But after he got up half way about three

+times, and missed fire and fell every time, and the last time most

+busted his brains out, he thought he'd got to give it up; but after he

+was rested he allowed he would give her one more turn for luck, and this

+time he made the trip.

+

+In the morning we was up at break of day, and down to the nigger cabins

+to pet the dogs and make friends with the nigger that fed Jim—if it

+was Jim that was being fed.  The niggers was just getting through

+breakfast and starting for the fields; and Jim's nigger was piling up

+a tin pan with bread and meat and things; and whilst the others was

+leaving, the key come from the house.

+

+This nigger had a good-natured, chuckle-headed face, and his wool was

+all tied up in little bunches with thread.  That was to keep witches

+off.  He said the witches was pestering him awful these nights, and

+making him see all kinds of strange things, and hear all kinds of

+strange words and noises, and he didn't believe he was ever witched so

+long before in his life.  He got so worked up, and got to running on so

+about his troubles, he forgot all about what he'd been a-going to do.

+ So Tom says:

+

+"What's the vittles for?  Going to feed the dogs?"

+

+The nigger kind of smiled around gradually over his face, like when you

+heave a brickbat in a mud-puddle, and he says:

+

+"Yes, Mars Sid, A dog.  Cur'us dog, too.  Does you want to go en look at

+'im?"

+

+"Yes."

+

+I hunched Tom, and whispers:

+

+"You going, right here in the daybreak?  that warn't the plan."

+

+"No, it warn't; but it's the plan now."

+

+So, drat him, we went along, but I didn't like it much.  When we got in

+we couldn't hardly see anything, it was so dark; but Jim was there, sure

+enough, and could see us; and he sings out:

+

+"Why, Huck!  En good lan'! ain' dat Misto Tom?"

+

+I just knowed how it would be; I just expected it.  I didn't know

+nothing to do; and if I had I couldn't a done it, because that nigger

+busted in and says:

+

+"Why, de gracious sakes! do he know you genlmen?"

+

+We could see pretty well now.  Tom he looked at the nigger, steady and

+kind of wondering, and says:

+

+"Does who know us?"

+

+"Why, dis-yer runaway nigger."

+

+"I don't reckon he does; but what put that into your head?"

+

+"What put it dar?  Didn' he jis' dis minute sing out like he knowed

+you?"

+

+Tom says, in a puzzled-up kind of way:

+

+"Well, that's mighty curious.  Who sung out? when did he sing out?

+ what did he sing out?" And turns to me, perfectly ca'm, and says,

+"Did you hear anybody sing out?"

+

+Of course there warn't nothing to be said but the one thing; so I says:

+

+"No; I ain't heard nobody say nothing."

+

+Then he turns to Jim, and looks him over like he never see him before,

+and says:

+

+"Did you sing out?"

+

+"No, sah," says Jim; "I hain't said nothing, sah."

+

+"Not a word?"

+

+"No, sah, I hain't said a word."

+

+"Did you ever see us before?"

+

+"No, sah; not as I knows on."

+

+So Tom turns to the nigger, which was looking wild and distressed, and

+says, kind of severe:

+

+"What do you reckon's the matter with you, anyway?  What made you think

+somebody sung out?"

+

+"Oh, it's de dad-blame' witches, sah, en I wisht I was dead, I do.

+ Dey's awluz at it, sah, en dey do mos' kill me, dey sk'yers me so.

+ Please to don't tell nobody 'bout it sah, er ole Mars Silas he'll scole

+me; 'kase he say dey ain't no witches.  I jis' wish to goodness he was

+heah now—den what would he say!  I jis' bet he couldn' fine no way to

+git aroun' it dis time.  But it's awluz jis' so; people dat's sot,

+stays sot; dey won't look into noth'n'en fine it out f'r deyselves, en

+when you fine it out en tell um 'bout it, dey doan' b'lieve you."

+

+Tom give him a dime, and said we wouldn't tell nobody; and told him to

+buy some more thread to tie up his wool with; and then looks at Jim, and

+says:

+

+"I wonder if Uncle Silas is going to hang this nigger.  If I was to

+catch a nigger that was ungrateful enough to run away, I wouldn't give

+him up, I'd hang him."  And whilst the nigger stepped to the door to

+look at the dime and bite it to see if it was good, he whispers to Jim

+and says:

+

+"Don't ever let on to know us.  And if you hear any digging going on

+nights, it's us; we're going to set you free."

+

+Jim only had time to grab us by the hand and squeeze it; then the nigger

+come back, and we said we'd come again some time if the nigger wanted

+us to; and he said he would, more particular if it was dark, because the

+witches went for him mostly in the dark, and it was good to have folks

+around then.

+

+

+

+

+CHAPTER XXXV.

+

+IT would be most an hour yet till breakfast, so we left and struck down

+into the woods; because Tom said we got to have some light to see how

+to dig by, and a lantern makes too much, and might get us into trouble;

+what we must have was a lot of them rotten chunks that's called

+fox-fire, and just makes a soft kind of a glow when you lay them in a

+dark place.  We fetched an armful and hid it in the weeds, and set down

+to rest, and Tom says, kind of dissatisfied:

+

+"Blame it, this whole thing is just as easy and awkward as it can be.

+And so it makes it so rotten difficult to get up a difficult plan.

+ There ain't no watchman to be drugged—now there ought to be a

+watchman.  There ain't even a dog to give a sleeping-mixture to.  And

+there's Jim chained by one leg, with a ten-foot chain, to the leg of his

+bed:  why, all you got to do is to lift up the bedstead and slip off

+the chain.  And Uncle Silas he trusts everybody; sends the key to the

+punkin-headed nigger, and don't send nobody to watch the nigger.  Jim

+could a got out of that window-hole before this, only there wouldn't be

+no use trying to travel with a ten-foot chain on his leg.  Why, drat it,

+Huck, it's the stupidest arrangement I ever see. You got to invent all

+the difficulties.  Well, we can't help it; we got to do the best we can

+with the materials we've got. Anyhow, there's one thing—there's more

+honor in getting him out through a lot of difficulties and dangers,

+where there warn't one of them furnished to you by the people who it was

+their duty to furnish them, and you had to contrive them all out of your

+own head.  Now look at just that one thing of the lantern.  When you

+come down to the cold facts, we simply got to let on that a lantern's

+resky.  Why, we could work with a torchlight procession if we wanted to,

+I believe.  Now, whilst I think of it, we got to hunt up something to

+make a saw out of the first chance we get."

+

+"What do we want of a saw?"

+

+"What do we want of it?  Hain't we got to saw the leg of Jim's bed

+off, so as to get the chain loose?"

+

+"Why, you just said a body could lift up the bedstead and slip the chain

+off."

+

+"Well, if that ain't just like you, Huck Finn.  You can get up the

+infant-schooliest ways of going at a thing.  Why, hain't you ever read

+any books at all?—Baron Trenck, nor Casanova, nor Benvenuto Chelleeny,

+nor Henri IV., nor none of them heroes?  Who ever heard of getting a

+prisoner loose in such an old-maidy way as that?  No; the way all the

+best authorities does is to saw the bed-leg in two, and leave it just

+so, and swallow the sawdust, so it can't be found, and put some dirt and

+grease around the sawed place so the very keenest seneskal can't see

+no sign of it's being sawed, and thinks the bed-leg is perfectly sound.

+Then, the night you're ready, fetch the leg a kick, down she goes; slip

+off your chain, and there you are.  Nothing to do but hitch your

+rope ladder to the battlements, shin down it, break your leg in the

+moat—because a rope ladder is nineteen foot too short, you know—and

+there's your horses and your trusty vassles, and they scoop you up and

+fling you across a saddle, and away you go to your native Langudoc, or

+Navarre, or wherever it is. It's gaudy, Huck.  I wish there was a moat

+to this cabin. If we get time, the night of the escape, we'll dig one."

+

+I says:

+

+"What do we want of a moat when we're going to snake him out from under

+the cabin?"

+

+But he never heard me.  He had forgot me and everything else.  He had

+his chin in his hand, thinking.  Pretty soon he sighs and shakes his

+head; then sighs again, and says:

+

+"No, it wouldn't do—there ain't necessity enough for it."

+

+"For what?"  I says.

+

+"Why, to saw Jim's leg off," he says.

+

+"Good land!"  I says; "why, there ain't no necessity for it.  And what

+would you want to saw his leg off for, anyway?"

+

+"Well, some of the best authorities has done it.  They couldn't get the

+chain off, so they just cut their hand off and shoved.  And a leg would

+be better still.  But we got to let that go.  There ain't necessity

+enough in this case; and, besides, Jim's a nigger, and wouldn't

+understand the reasons for it, and how it's the custom in Europe; so

+we'll let it go.  But there's one thing—he can have a rope ladder; we

+can tear up our sheets and make him a rope ladder easy enough.  And we

+can send it to him in a pie; it's mostly done that way.  And I've et

+worse pies."

+

+"Why, Tom Sawyer, how you talk," I says; "Jim ain't got no use for a

+rope ladder."

+

+"He has got use for it.  How you talk, you better say; you don't

+know nothing about it.  He's got to have a rope ladder; they all do."

+

+"What in the nation can he do with it?"

+

+"Do with it?  He can hide it in his bed, can't he?"  That's what they

+all do; and he's got to, too.  Huck, you don't ever seem to want to do

+anything that's regular; you want to be starting something fresh all the

+time. S'pose he don't do nothing with it? ain't it there in his bed,

+for a clew, after he's gone? and don't you reckon they'll want clews?

+ Of course they will.  And you wouldn't leave them any?  That would be a

+pretty howdy-do, wouldn't it!  I never heard of such a thing."

+

+"Well," I says, "if it's in the regulations, and he's got to have

+it, all right, let him have it; because I don't wish to go back on no

+regulations; but there's one thing, Tom Sawyer—if we go to tearing up

+our sheets to make Jim a rope ladder, we're going to get into trouble

+with Aunt Sally, just as sure as you're born.  Now, the way I look at

+it, a hickry-bark ladder don't cost nothing, and don't waste nothing,

+and is just as good to load up a pie with, and hide in a straw tick,

+as any rag ladder you can start; and as for Jim, he ain't had no

+experience, and so he don't care what kind of a—"

+

+"Oh, shucks, Huck Finn, if I was as ignorant as you I'd keep

+still—that's what I'D do.  Who ever heard of a state prisoner escaping

+by a hickry-bark ladder?  Why, it's perfectly ridiculous."

+

+"Well, all right, Tom, fix it your own way; but if you'll take my

+advice, you'll let me borrow a sheet off of the clothesline."

+

+He said that would do.  And that gave him another idea, and he says:

+

+"Borrow a shirt, too."

+

+"What do we want of a shirt, Tom?"

+

+"Want it for Jim to keep a journal on."

+

+"Journal your granny—Jim can't write."

+

+"S'pose he can't write—he can make marks on the shirt, can't he, if

+we make him a pen out of an old pewter spoon or a piece of an old iron

+barrel-hoop?"

+

+"Why, Tom, we can pull a feather out of a goose and make him a better

+one; and quicker, too."

+

+"Prisoners don't have geese running around the donjon-keep to pull

+pens out of, you muggins.  They always make their pens out of the

+hardest, toughest, troublesomest piece of old brass candlestick or

+something like that they can get their hands on; and it takes them weeks

+and weeks and months and months to file it out, too, because they've got

+to do it by rubbing it on the wall.  They wouldn't use a goose-quill

+if they had it. It ain't regular."

+

+"Well, then, what'll we make him the ink out of?"

+

+"Many makes it out of iron-rust and tears; but that's the common sort

+and women; the best authorities uses their own blood.  Jim can do that;

+and when he wants to send any little common ordinary mysterious message

+to let the world know where he's captivated, he can write it on the

+bottom of a tin plate with a fork and throw it out of the window.  The

+Iron Mask always done that, and it's a blame' good way, too."

+

+"Jim ain't got no tin plates.  They feed him in a pan."

+

+"That ain't nothing; we can get him some."

+

+"Can't nobody read his plates."

+

+"That ain't got anything to do with it, Huck Finn.  All he's got to

+do is to write on the plate and throw it out.  You don't have to be

+able to read it. Why, half the time you can't read anything a prisoner

+writes on a tin plate, or anywhere else."

+

+"Well, then, what's the sense in wasting the plates?"

+

+"Why, blame it all, it ain't the prisoner's plates."

+

+"But it's somebody's plates, ain't it?"

+

+"Well, spos'n it is?  What does the prisoner care whose—"

+

+He broke off there, because we heard the breakfast-horn blowing.  So we

+cleared out for the house.

+

+Along during the morning I borrowed a sheet and a white shirt off of the

+clothes-line; and I found an old sack and put them in it, and we went

+down and got the fox-fire, and put that in too.  I called it borrowing,

+because that was what pap always called it; but Tom said it warn't

+borrowing, it was stealing.  He said we was representing prisoners; and

+prisoners don't care how they get a thing so they get it, and nobody

+don't blame them for it, either.  It ain't no crime in a prisoner to

+steal the thing he needs to get away with, Tom said; it's his right; and

+so, as long as we was representing a prisoner, we had a perfect right to

+steal anything on this place we had the least use for to get ourselves

+out of prison with.  He said if we warn't prisoners it would be a very

+different thing, and nobody but a mean, ornery person would steal when

+he warn't a prisoner.  So we allowed we would steal everything there was

+that come handy.  And yet he made a mighty fuss, one day, after that,

+when I stole a watermelon out of the nigger-patch and eat it; and he

+made me go and give the niggers a dime without telling them what it

+was for. Tom said that what he meant was, we could steal anything we

+needed. Well, I says, I needed the watermelon.  But he said I didn't

+need it to get out of prison with; there's where the difference was.

+ He said if I'd a wanted it to hide a knife in, and smuggle it to Jim

+to kill the seneskal with, it would a been all right.  So I let it go at

+that, though I couldn't see no advantage in my representing a prisoner

+if I got to set down and chaw over a lot of gold-leaf distinctions like

+that every time I see a chance to hog a watermelon.

+

+Well, as I was saying, we waited that morning till everybody was settled

+down to business, and nobody in sight around the yard; then Tom he

+carried the sack into the lean-to whilst I stood off a piece to keep

+watch.  By and by he come out, and we went and set down on the woodpile

+to talk.  He says:

+

+"Everything's all right now except tools; and that's easy fixed."

+

+"Tools?"  I says.

+

+"Yes."

+

+"Tools for what?"

+

+"Why, to dig with.  We ain't a-going to gnaw him out, are we?"

+

+"Ain't them old crippled picks and things in there good enough to dig a

+nigger out with?"  I says.

+

+He turns on me, looking pitying enough to make a body cry, and says:

+

+"Huck Finn, did you ever hear of a prisoner having picks and shovels,

+and all the modern conveniences in his wardrobe to dig himself out with?

+ Now I want to ask you—if you got any reasonableness in you at all—what

+kind of a show would that give him to be a hero?  Why, they might as

+well lend him the key and done with it.  Picks and shovels—why, they

+wouldn't furnish 'em to a king."

+

+"Well, then," I says, "if we don't want the picks and shovels, what do

+we want?"

+

+"A couple of case-knives."

+

+"To dig the foundations out from under that cabin with?"

+

+"Yes."

+

+"Confound it, it's foolish, Tom."

+

+"It don't make no difference how foolish it is, it's the right way—and

+it's the regular way.  And there ain't no other way, that ever I heard

+of, and I've read all the books that gives any information about these

+things. They always dig out with a case-knife—and not through dirt, mind

+you; generly it's through solid rock.  And it takes them weeks and weeks

+and weeks, and for ever and ever.  Why, look at one of them prisoners in

+the bottom dungeon of the Castle Deef, in the harbor of Marseilles, that

+dug himself out that way; how long was he at it, you reckon?"

+

+"I don't know."

+

+"Well, guess."

+

+"I don't know.  A month and a half."

+

+"Thirty-seven year—and he come out in China.  That's the kind.  I

+wish the bottom of this fortress was solid rock."

+

+"Jim don't know nobody in China."

+

+"What's that got to do with it?  Neither did that other fellow.  But

+you're always a-wandering off on a side issue.  Why can't you stick to

+the main point?"

+

+"All right—I don't care where he comes out, so he comes out; and Jim

+don't, either, I reckon.  But there's one thing, anyway—Jim's too old to

+be dug out with a case-knife.  He won't last."

+

+"Yes he will last, too.  You don't reckon it's going to take

+thirty-seven years to dig out through a dirt foundation, do you?"

+

+"How long will it take, Tom?"

+

+"Well, we can't resk being as long as we ought to, because it mayn't

+take very long for Uncle Silas to hear from down there by New Orleans.

+ He'll hear Jim ain't from there.  Then his next move will be to

+advertise Jim, or something like that.  So we can't resk being as long

+digging him out as we ought to.  By rights I reckon we ought to be

+a couple of years; but we can't.  Things being so uncertain, what I

+recommend is this:  that we really dig right in, as quick as we can;

+and after that, we can let on, to ourselves, that we was at it

+thirty-seven years.  Then we can snatch him out and rush him away the

+first time there's an alarm.  Yes, I reckon that 'll be the best way."

+

+"Now, there's sense in that," I says.  "Letting on don't cost nothing;

+letting on ain't no trouble; and if it's any object, I don't mind

+letting on we was at it a hundred and fifty year.  It wouldn't strain

+me none, after I got my hand in.  So I'll mosey along now, and smouch a

+couple of case-knives."

+

+"Smouch three," he says; "we want one to make a saw out of."

+

+"Tom, if it ain't unregular and irreligious to sejest it," I says,

+"there's an old rusty saw-blade around yonder sticking under the

+weather-boarding behind the smoke-house."

+

+He looked kind of weary and discouraged-like, and says:

+

+"It ain't no use to try to learn you nothing, Huck.  Run along and

+smouch the knives—three of them."  So I done it.

+

+

+

+

+CHAPTER XXXVI.

+

+AS soon as we reckoned everybody was asleep that night we went down the

+lightning-rod, and shut ourselves up in the lean-to, and got out our

+pile of fox-fire, and went to work.  We cleared everything out of the

+way, about four or five foot along the middle of the bottom log.  Tom

+said he was right behind Jim's bed now, and we'd dig in under it, and

+when we got through there couldn't nobody in the cabin ever know there

+was any hole there, because Jim's counter-pin hung down most to the

+ground, and you'd have to raise it up and look under to see the hole.

+ So we dug and dug with the case-knives till most midnight; and then

+we was dog-tired, and our hands was blistered, and yet you couldn't see

+we'd done anything hardly.  At last I says:

+

+"This ain't no thirty-seven year job; this is a thirty-eight year job,

+Tom Sawyer."

+

+He never said nothing.  But he sighed, and pretty soon he stopped

+digging, and then for a good little while I knowed that he was thinking.

+Then he says:

+

+"It ain't no use, Huck, it ain't a-going to work.  If we was prisoners

+it would, because then we'd have as many years as we wanted, and no

+hurry; and we wouldn't get but a few minutes to dig, every day, while

+they was changing watches, and so our hands wouldn't get blistered, and

+we could keep it up right along, year in and year out, and do it right,

+and the way it ought to be done.  But we can't fool along; we got to

+rush; we ain't got no time to spare.  If we was to put in another

+night this way we'd have to knock off for a week to let our hands get

+well—couldn't touch a case-knife with them sooner."

+

+"Well, then, what we going to do, Tom?"

+

+"I'll tell you.  It ain't right, and it ain't moral, and I wouldn't like

+it to get out; but there ain't only just the one way:  we got to dig him

+out with the picks, and let on it's case-knives."

+

+"Now you're talking!"  I says; "your head gets leveler and leveler

+all the time, Tom Sawyer," I says.  "Picks is the thing, moral or no

+moral; and as for me, I don't care shucks for the morality of it, nohow.

+ When I start in to steal a nigger, or a watermelon, or a Sunday-school

+book, I ain't no ways particular how it's done so it's done.  What I

+want is my nigger; or what I want is my watermelon; or what I want is my

+Sunday-school book; and if a pick's the handiest thing, that's the thing

+I'm a-going to dig that nigger or that watermelon or that Sunday-school

+book out with; and I don't give a dead rat what the authorities thinks

+about it nuther."

+

+"Well," he says, "there's excuse for picks and letting-on in a case like

+this; if it warn't so, I wouldn't approve of it, nor I wouldn't stand by

+and see the rules broke—because right is right, and wrong is wrong,

+and a body ain't got no business doing wrong when he ain't ignorant and

+knows better.  It might answer for you to dig Jim out with a pick,

+without any letting on, because you don't know no better; but it

+wouldn't for me, because I do know better.  Gimme a case-knife."

+

+He had his own by him, but I handed him mine.  He flung it down, and

+says:

+

+"Gimme a case-knife."

+

+I didn't know just what to do—but then I thought.  I scratched around

+amongst the old tools, and got a pickaxe and give it to him, and he took

+it and went to work, and never said a word.

+

+He was always just that particular.  Full of principle.

+

+So then I got a shovel, and then we picked and shoveled, turn about,

+and made the fur fly.  We stuck to it about a half an hour, which was as

+long as we could stand up; but we had a good deal of a hole to show for

+it. When I got up stairs I looked out at the window and see Tom doing

+his level best with the lightning-rod, but he couldn't come it, his

+hands was so sore.  At last he says:

+

+"It ain't no use, it can't be done.  What you reckon I better do?  Can't

+you think of no way?"

+

+"Yes," I says, "but I reckon it ain't regular.  Come up the stairs, and

+let on it's a lightning-rod."

+

+So he done it.

+

+Next day Tom stole a pewter spoon and a brass candlestick in the house,

+for to make some pens for Jim out of, and six tallow candles; and I

+hung around the nigger cabins and laid for a chance, and stole three tin

+plates.  Tom says it wasn't enough; but I said nobody wouldn't ever see

+the plates that Jim throwed out, because they'd fall in the dog-fennel

+and jimpson weeds under the window-hole—then we could tote them back and

+he could use them over again.  So Tom was satisfied.  Then he says:

+

+"Now, the thing to study out is, how to get the things to Jim."

+

+"Take them in through the hole," I says, "when we get it done."

+

+He only just looked scornful, and said something about nobody ever heard

+of such an idiotic idea, and then he went to studying.  By and by he

+said he had ciphered out two or three ways, but there warn't no need to

+decide on any of them yet.  Said we'd got to post Jim first.

+

+That night we went down the lightning-rod a little after ten, and took

+one of the candles along, and listened under the window-hole, and heard

+Jim snoring; so we pitched it in, and it didn't wake him.  Then we

+whirled in with the pick and shovel, and in about two hours and a half

+the job was done.  We crept in under Jim's bed and into the cabin, and

+pawed around and found the candle and lit it, and stood over Jim awhile,

+and found him looking hearty and healthy, and then we woke him up gentle

+and gradual.  He was so glad to see us he most cried; and called us

+honey, and all the pet names he could think of; and was for having us

+hunt up a cold-chisel to cut the chain off of his leg with right away,

+and clearing out without losing any time.  But Tom he showed him how

+unregular it would be, and set down and told him all about our plans,

+and how we could alter them in a minute any time there was an alarm; and

+not to be the least afraid, because we would see he got away, sure.

+ So Jim he said it was all right, and we set there and talked over old

+times awhile, and then Tom asked a lot of questions, and when Jim told

+him Uncle Silas come in every day or two to pray with him, and Aunt

+Sally come in to see if he was comfortable and had plenty to eat, and

+both of them was kind as they could be, Tom says:

+

+"Now I know how to fix it.  We'll send you some things by them."

+

+I said, "Don't do nothing of the kind; it's one of the most jackass

+ideas I ever struck;" but he never paid no attention to me; went right

+on.  It was his way when he'd got his plans set.

+

+So he told Jim how we'd have to smuggle in the rope-ladder pie and other

+large things by Nat, the nigger that fed him, and he must be on the

+lookout, and not be surprised, and not let Nat see him open them; and

+we would put small things in uncle's coat-pockets and he must steal them

+out; and we would tie things to aunt's apron-strings or put them in her

+apron-pocket, if we got a chance; and told him what they would be and

+what they was for.  And told him how to keep a journal on the shirt with

+his blood, and all that. He told him everything.  Jim he couldn't see

+no sense in the most of it, but he allowed we was white folks and knowed

+better than him; so he was satisfied, and said he would do it all just

+as Tom said.

+

+Jim had plenty corn-cob pipes and tobacco; so we had a right down good

+sociable time; then we crawled out through the hole, and so home to

+bed, with hands that looked like they'd been chawed.  Tom was in high

+spirits. He said it was the best fun he ever had in his life, and the

+most intellectural; and said if he only could see his way to it we would

+keep it up all the rest of our lives and leave Jim to our children to

+get out; for he believed Jim would come to like it better and better the

+more he got used to it.  He said that in that way it could be strung out

+to as much as eighty year, and would be the best time on record.  And he

+said it would make us all celebrated that had a hand in it.

+

+In the morning we went out to the woodpile and chopped up the brass

+candlestick into handy sizes, and Tom put them and the pewter spoon in

+his pocket.  Then we went to the nigger cabins, and while I got Nat's

+notice off, Tom shoved a piece of candlestick into the middle of a

+corn-pone that was in Jim's pan, and we went along with Nat to see how

+it would work, and it just worked noble; when Jim bit into it it most

+mashed all his teeth out; and there warn't ever anything could a worked

+better. Tom said so himself. Jim he never let on but what it was only

+just a piece of rock or something like that that's always getting into

+bread, you know; but after that he never bit into nothing but what he

+jabbed his fork into it in three or four places first.

+

+And whilst we was a-standing there in the dimmish light, here comes a

+couple of the hounds bulging in from under Jim's bed; and they kept on

+piling in till there was eleven of them, and there warn't hardly room

+in there to get your breath.  By jings, we forgot to fasten that lean-to

+door!  The nigger Nat he only just hollered "Witches" once, and keeled

+over on to the floor amongst the dogs, and begun to groan like he was

+dying.  Tom jerked the door open and flung out a slab of Jim's meat,

+and the dogs went for it, and in two seconds he was out himself and back

+again and shut the door, and I knowed he'd fixed the other door too.

+Then he went to work on the nigger, coaxing him and petting him, and

+asking him if he'd been imagining he saw something again.  He raised up,

+and blinked his eyes around, and says:

+

+"Mars Sid, you'll say I's a fool, but if I didn't b'lieve I see most a

+million dogs, er devils, er some'n, I wisht I may die right heah in dese

+tracks.  I did, mos' sholy.  Mars Sid, I felt um—I felt um, sah; dey

+was all over me.  Dad fetch it, I jis' wisht I could git my han's on one

+er dem witches jis' wunst—on'y jis' wunst—it's all I'd ast.  But mos'ly

+I wisht dey'd lemme 'lone, I does."

+

+Tom says:

+

+"Well, I tell you what I think.  What makes them come here just at this

+runaway nigger's breakfast-time?  It's because they're hungry; that's

+the reason.  You make them a witch pie; that's the thing for you to

+do."

+

+"But my lan', Mars Sid, how's I gwyne to make 'm a witch pie?  I doan'

+know how to make it.  I hain't ever hearn er sich a thing b'fo'."

+

+"Well, then, I'll have to make it myself."

+

+"Will you do it, honey?—will you?  I'll wusshup de groun' und' yo' foot,

+I will!"

+

+"All right, I'll do it, seeing it's you, and you've been good to us and

+showed us the runaway nigger.  But you got to be mighty careful.  When

+we come around, you turn your back; and then whatever we've put in the

+pan, don't you let on you see it at all.  And don't you look when Jim

+unloads the pan—something might happen, I don't know what.  And above

+all, don't you handle the witch-things."

+

+"Hannel 'M, Mars Sid?  What is you a-talkin' 'bout?  I wouldn'

+lay de weight er my finger on um, not f'r ten hund'd thous'n billion

+dollars, I wouldn't."

+

+

+

+

+CHAPTER XXXVII.

+

+THAT was all fixed.  So then we went away and went to the rubbage-pile

+in the back yard, where they keep the old boots, and rags, and pieces

+of bottles, and wore-out tin things, and all such truck, and scratched

+around and found an old tin washpan, and stopped up the holes as well as

+we could, to bake the pie in, and took it down cellar and stole it full

+of flour and started for breakfast, and found a couple of shingle-nails

+that Tom said would be handy for a prisoner to scrabble his name and

+sorrows on the dungeon walls with, and dropped one of them in Aunt

+Sally's apron-pocket which was hanging on a chair, and t'other we stuck

+in the band of Uncle Silas's hat, which was on the bureau, because we

+heard the children say their pa and ma was going to the runaway nigger's

+house this morning, and then went to breakfast, and Tom dropped the

+pewter spoon in Uncle Silas's coat-pocket, and Aunt Sally wasn't come

+yet, so we had to wait a little while.

+

+And when she come she was hot and red and cross, and couldn't hardly

+wait for the blessing; and then she went to sluicing out coffee with one

+hand and cracking the handiest child's head with her thimble with the

+other, and says:

+

+"I've hunted high and I've hunted low, and it does beat all what has

+become of your other shirt."

+

+My heart fell down amongst my lungs and livers and things, and a hard

+piece of corn-crust started down my throat after it and got met on the

+road with a cough, and was shot across the table, and took one of the

+children in the eye and curled him up like a fishing-worm, and let a cry

+out of him the size of a warwhoop, and Tom he turned kinder blue around

+the gills, and it all amounted to a considerable state of things for

+about a quarter of a minute or as much as that, and I would a sold out

+for half price if there was a bidder.  But after that we was all right

+again—it was the sudden surprise of it that knocked us so kind of cold.

+Uncle Silas he says:

+

+"It's most uncommon curious, I can't understand it.  I know perfectly

+well I took it off, because—"

+

+"Because you hain't got but one on.  Just listen at the man!  I know

+you took it off, and know it by a better way than your wool-gethering

+memory, too, because it was on the clo's-line yesterday—I see it there

+myself. But it's gone, that's the long and the short of it, and you'll

+just have to change to a red flann'l one till I can get time to make a

+new one. And it 'll be the third I've made in two years.  It just keeps

+a body on the jump to keep you in shirts; and whatever you do manage to

+do with 'm all is more'n I can make out.  A body 'd think you would

+learn to take some sort of care of 'em at your time of life."

+

+"I know it, Sally, and I do try all I can.  But it oughtn't to be

+altogether my fault, because, you know, I don't see them nor have

+nothing to do with them except when they're on me; and I don't believe

+I've ever lost one of them off of me."

+

+"Well, it ain't your fault if you haven't, Silas; you'd a done it

+if you could, I reckon.  And the shirt ain't all that's gone, nuther.

+ Ther's a spoon gone; and that ain't all.  There was ten, and now

+ther's only nine. The calf got the shirt, I reckon, but the calf never

+took the spoon, that's certain."

+

+"Why, what else is gone, Sally?"

+

+"Ther's six candles gone—that's what.  The rats could a got the

+candles, and I reckon they did; I wonder they don't walk off with the

+whole place, the way you're always going to stop their holes and don't

+do it; and if they warn't fools they'd sleep in your hair, Silas—you'd

+never find it out; but you can't lay the spoon on the rats, and that I

+know."

+

+"Well, Sally, I'm in fault, and I acknowledge it; I've been remiss; but

+I won't let to-morrow go by without stopping up them holes."

+

+"Oh, I wouldn't hurry; next year 'll do.  Matilda Angelina Araminta

+Phelps!"

+

+Whack comes the thimble, and the child snatches her claws out of the

+sugar-bowl without fooling around any.  Just then the nigger woman steps

+on to the passage, and says:

+

+"Missus, dey's a sheet gone."

+

+"A sheet gone!  Well, for the land's sake!"

+

+"I'll stop up them holes to-day," says Uncle Silas, looking sorrowful.

+

+"Oh, do shet up!—s'pose the rats took the sheet?  where's it gone,

+Lize?"

+

+"Clah to goodness I hain't no notion, Miss' Sally.  She wuz on de

+clo'sline yistiddy, but she done gone:  she ain' dah no mo' now."

+

+"I reckon the world is coming to an end.  I never see the beat of it

+in all my born days.  A shirt, and a sheet, and a spoon, and six can—"

+

+"Missus," comes a young yaller wench, "dey's a brass cannelstick

+miss'n."

+

+"Cler out from here, you hussy, er I'll take a skillet to ye!"

+

+Well, she was just a-biling.  I begun to lay for a chance; I reckoned

+I would sneak out and go for the woods till the weather moderated.  She

+kept a-raging right along, running her insurrection all by herself, and

+everybody else mighty meek and quiet; and at last Uncle Silas, looking

+kind of foolish, fishes up that spoon out of his pocket.  She stopped,

+with her mouth open and her hands up; and as for me, I wished I was in

+Jeruslem or somewheres. But not long, because she says:

+

+"It's just as I expected.  So you had it in your pocket all the time;

+and like as not you've got the other things there, too.  How'd it get

+there?"

+

+"I reely don't know, Sally," he says, kind of apologizing, "or you know

+I would tell.  I was a-studying over my text in Acts Seventeen before

+breakfast, and I reckon I put it in there, not noticing, meaning to put

+my Testament in, and it must be so, because my Testament ain't in; but

+I'll go and see; and if the Testament is where I had it, I'll know I

+didn't put it in, and that will show that I laid the Testament down and

+took up the spoon, and—"

+

+"Oh, for the land's sake!  Give a body a rest!  Go 'long now, the whole

+kit and biling of ye; and don't come nigh me again till I've got back my

+peace of mind."

+

+I'D a heard her if she'd a said it to herself, let alone speaking it

+out; and I'd a got up and obeyed her if I'd a been dead.  As we was

+passing through the setting-room the old man he took up his hat, and the

+shingle-nail fell out on the floor, and he just merely picked it up and

+laid it on the mantel-shelf, and never said nothing, and went out.  Tom

+see him do it, and remembered about the spoon, and says:

+

+"Well, it ain't no use to send things by him no more, he ain't

+reliable." Then he says:  "But he done us a good turn with the spoon,

+anyway, without knowing it, and so we'll go and do him one without him

+knowing it—stop up his rat-holes."

+

+There was a noble good lot of them down cellar, and it took us a whole

+hour, but we done the job tight and good and shipshape.  Then we heard

+steps on the stairs, and blowed out our light and hid; and here comes

+the old man, with a candle in one hand and a bundle of stuff in t'other,

+looking as absent-minded as year before last.  He went a mooning around,

+first to one rat-hole and then another, till he'd been to them all.

+ Then he stood about five minutes, picking tallow-drip off of his candle

+and thinking.  Then he turns off slow and dreamy towards the stairs,

+saying:

+

+"Well, for the life of me I can't remember when I done it.  I could

+show her now that I warn't to blame on account of the rats.  But never

+mind—let it go.  I reckon it wouldn't do no good."

+

+And so he went on a-mumbling up stairs, and then we left.  He was a

+mighty nice old man.  And always is.

+

+Tom was a good deal bothered about what to do for a spoon, but he said

+we'd got to have it; so he took a think.  When he had ciphered it out

+he told me how we was to do; then we went and waited around the

+spoon-basket till we see Aunt Sally coming, and then Tom went to

+counting the spoons and laying them out to one side, and I slid one of

+them up my sleeve, and Tom says:

+

+"Why, Aunt Sally, there ain't but nine spoons yet."

+

+She says:

+

+"Go 'long to your play, and don't bother me.  I know better, I counted

+'m myself."

+

+"Well, I've counted them twice, Aunty, and I can't make but nine."

+

+She looked out of all patience, but of course she come to count—anybody

+would.

+

+"I declare to gracious ther' ain't but nine!" she says.  "Why, what in

+the world—plague take the things, I'll count 'm again."

+

+So I slipped back the one I had, and when she got done counting, she

+says:

+

+"Hang the troublesome rubbage, ther's ten now!" and she looked huffy

+and bothered both.  But Tom says:

+

+"Why, Aunty, I don't think there's ten."

+

+"You numskull, didn't you see me count 'm?"

+

+"I know, but—"

+

+"Well, I'll count 'm again."

+

+So I smouched one, and they come out nine, same as the other time.

+ Well, she was in a tearing way—just a-trembling all over, she was so

+mad.  But she counted and counted till she got that addled she'd start

+to count in the basket for a spoon sometimes; and so, three times they

+come out right, and three times they come out wrong.  Then she grabbed

+up the basket and slammed it across the house and knocked the cat

+galley-west; and she said cle'r out and let her have some peace, and if

+we come bothering around her again betwixt that and dinner she'd skin

+us.  So we had the odd spoon, and dropped it in her apron-pocket whilst

+she was a-giving us our sailing orders, and Jim got it all right, along

+with her shingle nail, before noon.  We was very well satisfied with

+this business, and Tom allowed it was worth twice the trouble it took,

+because he said now she couldn't ever count them spoons twice alike

+again to save her life; and wouldn't believe she'd counted them right if

+she did; and said that after she'd about counted her head off for the

+next three days he judged she'd give it up and offer to kill anybody

+that wanted her to ever count them any more.

+

+So we put the sheet back on the line that night, and stole one out of

+her closet; and kept on putting it back and stealing it again for a

+couple of days till she didn't know how many sheets she had any more,

+and she didn't care, and warn't a-going to bullyrag the rest of her

+soul out about it, and wouldn't count them again not to save her life;

+she druther die first.

+

+So we was all right now, as to the shirt and the sheet and the spoon

+and the candles, by the help of the calf and the rats and the mixed-up

+counting; and as to the candlestick, it warn't no consequence, it would

+blow over by and by.

+

+But that pie was a job; we had no end of trouble with that pie.  We

+fixed it up away down in the woods, and cooked it there; and we got it

+done at last, and very satisfactory, too; but not all in one day; and we

+had to use up three wash-pans full of flour before we got through, and

+we got burnt pretty much all over, in places, and eyes put out with

+the smoke; because, you see, we didn't want nothing but a crust, and we

+couldn't prop it up right, and she would always cave in.  But of course

+we thought of the right way at last—which was to cook the ladder, too,

+in the pie.  So then we laid in with Jim the second night, and tore

+up the sheet all in little strings and twisted them together, and long

+before daylight we had a lovely rope that you could a hung a person

+with.  We let on it took nine months to make it.

+

+And in the forenoon we took it down to the woods, but it wouldn't go

+into the pie.  Being made of a whole sheet, that way, there was rope

+enough for forty pies if we'd a wanted them, and plenty left over

+for soup, or sausage, or anything you choose.  We could a had a whole

+dinner.

+

+But we didn't need it.  All we needed was just enough for the pie, and

+so we throwed the rest away.  We didn't cook none of the pies in the

+wash-pan—afraid the solder would melt; but Uncle Silas he had a noble

+brass warming-pan which he thought considerable of, because it belonged

+to one of his ancesters with a long wooden handle that come over from

+England with William the Conqueror in the Mayflower or one of them early

+ships and was hid away up garret with a lot of other old pots and things

+that was valuable, not on account of being any account, because they

+warn't, but on account of them being relicts, you know, and we snaked

+her out, private, and took her down there, but she failed on the first

+pies, because we didn't know how, but she come up smiling on the last

+one.  We took and lined her with dough, and set her in the coals, and

+loaded her up with rag rope, and put on a dough roof, and shut down the

+lid, and put hot embers on top, and stood off five foot, with the long

+handle, cool and comfortable, and in fifteen minutes she turned out a

+pie that was a satisfaction to look at. But the person that et it would

+want to fetch a couple of kags of toothpicks along, for if that rope

+ladder wouldn't cramp him down to business I don't know nothing what I'm

+talking about, and lay him in enough stomach-ache to last him till next

+time, too.

+

+Nat didn't look when we put the witch pie in Jim's pan; and we put the

+three tin plates in the bottom of the pan under the vittles; and so Jim

+got everything all right, and as soon as he was by himself he busted

+into the pie and hid the rope ladder inside of his straw tick,

+and scratched some marks on a tin plate and throwed it out of the

+window-hole.

+

+

+

+

+CHAPTER XXXVIII.

+

+MAKING them pens was a distressid tough job, and so was the saw; and Jim

+allowed the inscription was going to be the toughest of all.  That's the

+one which the prisoner has to scrabble on the wall.  But he had to have

+it; Tom said he'd got to; there warn't no case of a state prisoner not

+scrabbling his inscription to leave behind, and his coat of arms.

+

+"Look at Lady Jane Grey," he says; "look at Gilford Dudley; look at old

+Northumberland!  Why, Huck, s'pose it is considerble trouble?—what

+you going to do?—how you going to get around it?  Jim's got to do his

+inscription and coat of arms.  They all do."

+

+Jim says:

+

+"Why, Mars Tom, I hain't got no coat o' arm; I hain't got nuffn but dish

+yer ole shirt, en you knows I got to keep de journal on dat."

+

+"Oh, you don't understand, Jim; a coat of arms is very different."

+

+"Well," I says, "Jim's right, anyway, when he says he ain't got no coat

+of arms, because he hain't."

+

+"I reckon I knowed that," Tom says, "but you bet he'll have one before

+he goes out of this—because he's going out right, and there ain't

+going to be no flaws in his record."

+

+So whilst me and Jim filed away at the pens on a brickbat apiece, Jim

+a-making his'n out of the brass and I making mine out of the spoon,

+Tom set to work to think out the coat of arms.  By and by he said he'd

+struck so many good ones he didn't hardly know which to take, but there

+was one which he reckoned he'd decide on.  He says:

+

+"On the scutcheon we'll have a bend or in the dexter base, a saltire

+murrey in the fess, with a dog, couchant, for common charge, and under

+his foot a chain embattled, for slavery, with a chevron vert in a

+chief engrailed, and three invected lines on a field azure, with the

+nombril points rampant on a dancette indented; crest, a runaway nigger,

+sable, with his bundle over his shoulder on a bar sinister; and a

+couple of gules for supporters, which is you and me; motto, Maggiore

+Fretta, Minore Otto.  Got it out of a book—means the more haste the

+less speed."

+

+"Geewhillikins," I says, "but what does the rest of it mean?"

+

+"We ain't got no time to bother over that," he says; "we got to dig in

+like all git-out."

+

+"Well, anyway," I says, "what's some of it?  What's a fess?"

+

+"A fess—a fess is—you don't need to know what a fess is.  I'll show

+him how to make it when he gets to it."

+

+"Shucks, Tom," I says, "I think you might tell a person.  What's a bar

+sinister?"

+

+"Oh, I don't know.  But he's got to have it.  All the nobility does."

+

+That was just his way.  If it didn't suit him to explain a thing to you,

+he wouldn't do it.  You might pump at him a week, it wouldn't make no

+difference.

+

+He'd got all that coat of arms business fixed, so now he started in to

+finish up the rest of that part of the work, which was to plan out a

+mournful inscription—said Jim got to have one, like they all done.  He

+made up a lot, and wrote them out on a paper, and read them off, so:

+

+1.  Here a captive heart busted. 2.  Here a poor prisoner, forsook by

+the world and friends, fretted his sorrowful life. 3.  Here a lonely

+heart broke, and a worn spirit went to its rest, after thirty-seven

+years of solitary captivity. 4.  Here, homeless and friendless, after

+thirty-seven years of bitter captivity, perished a noble stranger,

+natural son of Louis XIV.

+

+Tom's voice trembled whilst he was reading them, and he most broke down.

+When he got done he couldn't no way make up his mind which one for Jim

+to scrabble on to the wall, they was all so good; but at last he allowed

+he would let him scrabble them all on.  Jim said it would take him a

+year to scrabble such a lot of truck on to the logs with a nail, and he

+didn't know how to make letters, besides; but Tom said he would block

+them out for him, and then he wouldn't have nothing to do but just

+follow the lines.  Then pretty soon he says:

+

+"Come to think, the logs ain't a-going to do; they don't have log walls

+in a dungeon:  we got to dig the inscriptions into a rock.  We'll fetch

+a rock."

+

+Jim said the rock was worse than the logs; he said it would take him

+such a pison long time to dig them into a rock he wouldn't ever get out.

+ But Tom said he would let me help him do it.  Then he took a look to

+see how me and Jim was getting along with the pens.  It was most pesky

+tedious hard work and slow, and didn't give my hands no show to get

+well of the sores, and we didn't seem to make no headway, hardly; so Tom

+says:

+

+"I know how to fix it.  We got to have a rock for the coat of arms and

+mournful inscriptions, and we can kill two birds with that same rock.

+There's a gaudy big grindstone down at the mill, and we'll smouch it,

+and carve the things on it, and file out the pens and the saw on it,

+too."

+

+It warn't no slouch of an idea; and it warn't no slouch of a grindstone

+nuther; but we allowed we'd tackle it.  It warn't quite midnight yet,

+so we cleared out for the mill, leaving Jim at work.  We smouched the

+grindstone, and set out to roll her home, but it was a most nation tough

+job. Sometimes, do what we could, we couldn't keep her from falling

+over, and she come mighty near mashing us every time.  Tom said she was

+going to get one of us, sure, before we got through.  We got her half

+way; and then we was plumb played out, and most drownded with sweat.  We

+see it warn't no use; we got to go and fetch Jim. So he raised up his

+bed and slid the chain off of the bed-leg, and wrapt it round and round

+his neck, and we crawled out through our hole and down there, and Jim

+and me laid into that grindstone and walked her along like nothing; and

+Tom superintended.  He could out-superintend any boy I ever see.  He

+knowed how to do everything.

+

+Our hole was pretty big, but it warn't big enough to get the grindstone

+through; but Jim he took the pick and soon made it big enough.  Then Tom

+marked out them things on it with the nail, and set Jim to work on them,

+with the nail for a chisel and an iron bolt from the rubbage in the

+lean-to for a hammer, and told him to work till the rest of his candle

+quit on him, and then he could go to bed, and hide the grindstone under

+his straw tick and sleep on it.  Then we helped him fix his chain back

+on the bed-leg, and was ready for bed ourselves.  But Tom thought of

+something, and says:

+

+"You got any spiders in here, Jim?"

+

+"No, sah, thanks to goodness I hain't, Mars Tom."

+

+"All right, we'll get you some."

+

+"But bless you, honey, I doan' want none.  I's afeard un um.  I jis'

+'s soon have rattlesnakes aroun'."

+

+Tom thought a minute or two, and says:

+

+"It's a good idea.  And I reckon it's been done.  It must a been done;

+it stands to reason.  Yes, it's a prime good idea.  Where could you keep

+it?"

+

+"Keep what, Mars Tom?"

+

+"Why, a rattlesnake."

+

+"De goodness gracious alive, Mars Tom!  Why, if dey was a rattlesnake to

+come in heah I'd take en bust right out thoo dat log wall, I would, wid

+my head."

+

+"Why, Jim, you wouldn't be afraid of it after a little.  You could tame

+it."

+

+"Tame it!"

+

+"Yes—easy enough.  Every animal is grateful for kindness and petting,

+and they wouldn't think of hurting a person that pets them.  Any book

+will tell you that.  You try—that's all I ask; just try for two or three

+days. Why, you can get him so, in a little while, that he'll love you;

+and sleep with you; and won't stay away from you a minute; and will let

+you wrap him round your neck and put his head in your mouth."

+

+"Please, Mars Tom—doan' talk so!  I can't stan' it!  He'd let

+me shove his head in my mouf—fer a favor, hain't it?  I lay he'd wait a

+pow'ful long time 'fo' I ast him.  En mo' en dat, I doan' want him

+to sleep wid me."

+

+"Jim, don't act so foolish.  A prisoner's got to have some kind of a

+dumb pet, and if a rattlesnake hain't ever been tried, why, there's more

+glory to be gained in your being the first to ever try it than any other

+way you could ever think of to save your life."

+

+"Why, Mars Tom, I doan' want no sich glory.  Snake take 'n bite

+Jim's chin off, den whah is de glory?  No, sah, I doan' want no sich

+doin's."

+

+"Blame it, can't you try?  I only want you to try—you needn't keep

+it up if it don't work."

+

+"But de trouble all done ef de snake bite me while I's a tryin' him.

+Mars Tom, I's willin' to tackle mos' anything 'at ain't onreasonable,

+but ef you en Huck fetches a rattlesnake in heah for me to tame, I's

+gwyne to leave, dat's shore."

+

+"Well, then, let it go, let it go, if you're so bull-headed about it.

+ We can get you some garter-snakes, and you can tie some buttons on

+their tails, and let on they're rattlesnakes, and I reckon that 'll have

+to do."

+

+"I k'n stan' dem, Mars Tom, but blame' 'f I couldn' get along widout

+um, I tell you dat.  I never knowed b'fo' 't was so much bother and

+trouble to be a prisoner."

+

+"Well, it always is when it's done right.  You got any rats around

+here?"

+

+"No, sah, I hain't seed none."

+

+"Well, we'll get you some rats."

+

+"Why, Mars Tom, I doan' want no rats.  Dey's de dadblamedest creturs

+to 'sturb a body, en rustle roun' over 'im, en bite his feet, when he's

+tryin' to sleep, I ever see.  No, sah, gimme g'yarter-snakes, 'f I's

+got to have 'm, but doan' gimme no rats; I hain' got no use f'r um,

+skasely."

+

+"But, Jim, you got to have 'em—they all do.  So don't make no more

+fuss about it.  Prisoners ain't ever without rats.  There ain't no

+instance of it.  And they train them, and pet them, and learn them

+tricks, and they get to be as sociable as flies.  But you got to play

+music to them.  You got anything to play music on?"

+

+"I ain' got nuffn but a coase comb en a piece o' paper, en a juice-harp;

+but I reck'n dey wouldn' take no stock in a juice-harp."

+

+"Yes they would they don't care what kind of music 'tis.  A

+jews-harp's plenty good enough for a rat.  All animals like music—in a

+prison they dote on it.  Specially, painful music; and you can't get no

+other kind out of a jews-harp.  It always interests them; they come out

+to see what's the matter with you.  Yes, you're all right; you're fixed

+very well.  You want to set on your bed nights before you go to sleep,

+and early in the mornings, and play your jews-harp; play 'The Last Link

+is Broken'—that's the thing that 'll scoop a rat quicker 'n anything

+else; and when you've played about two minutes you'll see all the rats,

+and the snakes, and spiders, and things begin to feel worried about you,

+and come.  And they'll just fairly swarm over you, and have a noble good

+time."

+

+"Yes, dey will, I reck'n, Mars Tom, but what kine er time is Jim

+havin'? Blest if I kin see de pint.  But I'll do it ef I got to.  I

+reck'n I better keep de animals satisfied, en not have no trouble in de

+house."

+

+Tom waited to think it over, and see if there wasn't nothing else; and

+pretty soon he says:

+

+"Oh, there's one thing I forgot.  Could you raise a flower here, do you

+reckon?"

+

+"I doan know but maybe I could, Mars Tom; but it's tolable dark in heah,

+en I ain' got no use f'r no flower, nohow, en she'd be a pow'ful sight

+o' trouble."

+

+"Well, you try it, anyway.  Some other prisoners has done it."

+

+"One er dem big cat-tail-lookin' mullen-stalks would grow in heah, Mars

+Tom, I reck'n, but she wouldn't be wuth half de trouble she'd coss."

+

+"Don't you believe it.  We'll fetch you a little one and you plant it in

+the corner over there, and raise it.  And don't call it mullen, call it

+Pitchiola—that's its right name when it's in a prison.  And you want to

+water it with your tears."

+

+"Why, I got plenty spring water, Mars Tom."

+

+"You don't want spring water; you want to water it with your tears.

+ It's the way they always do."

+

+"Why, Mars Tom, I lay I kin raise one er dem mullen-stalks twyste wid

+spring water whiles another man's a start'n one wid tears."

+

+"That ain't the idea.  You got to do it with tears."

+

+"She'll die on my han's, Mars Tom, she sholy will; kase I doan' skasely

+ever cry."

+

+So Tom was stumped.  But he studied it over, and then said Jim would

+have to worry along the best he could with an onion.  He promised

+he would go to the nigger cabins and drop one, private, in Jim's

+coffee-pot, in the morning. Jim said he would "jis' 's soon have

+tobacker in his coffee;" and found so much fault with it, and with the

+work and bother of raising the mullen, and jews-harping the rats, and

+petting and flattering up the snakes and spiders and things, on top of

+all the other work he had to do on pens, and inscriptions, and journals,

+and things, which made it more trouble and worry and responsibility to

+be a prisoner than anything he ever undertook, that Tom most lost all

+patience with him; and said he was just loadened down with more gaudier

+chances than a prisoner ever had in the world to make a name for

+himself, and yet he didn't know enough to appreciate them, and they was

+just about wasted on him.  So Jim he was sorry, and said he wouldn't

+behave so no more, and then me and Tom shoved for bed.

+

+

+

+

+CHAPTER XXXIX.

+

+IN the morning we went up to the village and bought a wire rat-trap and

+fetched it down, and unstopped the best rat-hole, and in about an hour

+we had fifteen of the bulliest kind of ones; and then we took it and put

+it in a safe place under Aunt Sally's bed.  But while we was gone for

+spiders little Thomas Franklin Benjamin Jefferson Elexander Phelps found

+it there, and opened the door of it to see if the rats would come out,

+and they did; and Aunt Sally she come in, and when we got back she was

+a-standing on top of the bed raising Cain, and the rats was doing what

+they could to keep off the dull times for her.  So she took and dusted

+us both with the hickry, and we was as much as two hours catching

+another fifteen or sixteen, drat that meddlesome cub, and they warn't

+the likeliest, nuther, because the first haul was the pick of the flock.

+ I never see a likelier lot of rats than what that first haul was.

+

+We got a splendid stock of sorted spiders, and bugs, and frogs, and

+caterpillars, and one thing or another; and we like to got a hornet's

+nest, but we didn't.  The family was at home.  We didn't give it right

+up, but stayed with them as long as we could; because we allowed we'd

+tire them out or they'd got to tire us out, and they done it.  Then we

+got allycumpain and rubbed on the places, and was pretty near all right

+again, but couldn't set down convenient.  And so we went for the snakes,

+and grabbed a couple of dozen garters and house-snakes, and put them in

+a bag, and put it in our room, and by that time it was supper-time, and

+a rattling good honest day's work:  and hungry?—oh, no, I reckon not!

+ And there warn't a blessed snake up there when we went back—we didn't

+half tie the sack, and they worked out somehow, and left.  But it didn't

+matter much, because they was still on the premises somewheres.  So

+we judged we could get some of them again.  No, there warn't no real

+scarcity of snakes about the house for a considerable spell.  You'd see

+them dripping from the rafters and places every now and then; and they

+generly landed in your plate, or down the back of your neck, and most

+of the time where you didn't want them.  Well, they was handsome and

+striped, and there warn't no harm in a million of them; but that never

+made no difference to Aunt Sally; she despised snakes, be the breed what

+they might, and she couldn't stand them no way you could fix it; and

+every time one of them flopped down on her, it didn't make no difference

+what she was doing, she would just lay that work down and light out.  I

+never see such a woman.  And you could hear her whoop to Jericho.  You

+couldn't get her to take a-holt of one of them with the tongs.  And if

+she turned over and found one in bed she would scramble out and lift a

+howl that you would think the house was afire.  She disturbed the old

+man so that he said he could most wish there hadn't ever been no snakes

+created.  Why, after every last snake had been gone clear out of the

+house for as much as a week Aunt Sally warn't over it yet; she warn't

+near over it; when she was setting thinking about something you could

+touch her on the back of her neck with a feather and she would jump

+right out of her stockings.  It was very curious.  But Tom said all

+women was just so.  He said they was made that way for some reason or

+other.

+

+We got a licking every time one of our snakes come in her way, and she

+allowed these lickings warn't nothing to what she would do if we ever

+loaded up the place again with them.  I didn't mind the lickings,

+because they didn't amount to nothing; but I minded the trouble we

+had to lay in another lot.  But we got them laid in, and all the other

+things; and you never see a cabin as blithesome as Jim's was when they'd

+all swarm out for music and go for him.  Jim didn't like the spiders,

+and the spiders didn't like Jim; and so they'd lay for him, and make it

+mighty warm for him.  And he said that between the rats and the snakes

+and the grindstone there warn't no room in bed for him, skasely; and

+when there was, a body couldn't sleep, it was so lively, and it was

+always lively, he said, because they never all slept at one time, but

+took turn about, so when the snakes was asleep the rats was on deck, and

+when the rats turned in the snakes come on watch, so he always had one

+gang under him, in his way, and t'other gang having a circus over him,

+and if he got up to hunt a new place the spiders would take a chance at

+him as he crossed over. He said if he ever got out this time he wouldn't

+ever be a prisoner again, not for a salary.

+

+Well, by the end of three weeks everything was in pretty good shape.

+ The shirt was sent in early, in a pie, and every time a rat bit Jim he

+would get up and write a little in his journal whilst the ink was fresh;

+the pens was made, the inscriptions and so on was all carved on the

+grindstone; the bed-leg was sawed in two, and we had et up the sawdust,

+and it give us a most amazing stomach-ache.  We reckoned we was all

+going to die, but didn't.  It was the most undigestible sawdust I ever

+see; and Tom said the same.

+

+But as I was saying, we'd got all the work done now, at last; and we was

+all pretty much fagged out, too, but mainly Jim.  The old man had wrote

+a couple of times to the plantation below Orleans to come and get their

+runaway nigger, but hadn't got no answer, because there warn't no such

+plantation; so he allowed he would advertise Jim in the St. Louis and

+New Orleans papers; and when he mentioned the St. Louis ones it give me

+the cold shivers, and I see we hadn't no time to lose. So Tom said, now

+for the nonnamous letters.

+

+"What's them?"  I says.

+

+"Warnings to the people that something is up.  Sometimes it's done one

+way, sometimes another.  But there's always somebody spying around that

+gives notice to the governor of the castle.  When Louis XVI. was going

+to light out of the Tooleries, a servant-girl done it.  It's a very good

+way, and so is the nonnamous letters.  We'll use them both.  And it's

+usual for the prisoner's mother to change clothes with him, and she

+stays in, and he slides out in her clothes.  We'll do that, too."

+

+"But looky here, Tom, what do we want to warn anybody for that

+something's up?  Let them find it out for themselves—it's their

+lookout."

+

+"Yes, I know; but you can't depend on them.  It's the way they've acted

+from the very start—left us to do everything.  They're so confiding

+and mullet-headed they don't take notice of nothing at all.  So if we

+don't give them notice there won't be nobody nor nothing to interfere

+with us, and so after all our hard work and trouble this escape 'll go

+off perfectly flat; won't amount to nothing—won't be nothing to it."

+

+"Well, as for me, Tom, that's the way I'd like."

+

+"Shucks!" he says, and looked disgusted.  So I says:

+

+"But I ain't going to make no complaint.  Any way that suits you suits

+me. What you going to do about the servant-girl?"

+

+"You'll be her.  You slide in, in the middle of the night, and hook that

+yaller girl's frock."

+

+"Why, Tom, that 'll make trouble next morning; because, of course, she

+prob'bly hain't got any but that one."

+

+"I know; but you don't want it but fifteen minutes, to carry the

+nonnamous letter and shove it under the front door."

+

+"All right, then, I'll do it; but I could carry it just as handy in my

+own togs."

+

+"You wouldn't look like a servant-girl then, would you?"

+

+"No, but there won't be nobody to see what I look like, anyway."

+

+"That ain't got nothing to do with it.  The thing for us to do is just

+to do our duty, and not worry about whether anybody sees us do it or

+not. Hain't you got no principle at all?"

+

+"All right, I ain't saying nothing; I'm the servant-girl.  Who's Jim's

+mother?"

+

+"I'm his mother.  I'll hook a gown from Aunt Sally."

+

+"Well, then, you'll have to stay in the cabin when me and Jim leaves."

+

+"Not much.  I'll stuff Jim's clothes full of straw and lay it on his bed

+to represent his mother in disguise, and Jim 'll take the nigger woman's

+gown off of me and wear it, and we'll all evade together.  When a

+prisoner of style escapes it's called an evasion.  It's always called

+so when a king escapes, f'rinstance.  And the same with a king's son;

+it don't make no difference whether he's a natural one or an unnatural

+one."

+

+So Tom he wrote the nonnamous letter, and I smouched the yaller wench's

+frock that night, and put it on, and shoved it under the front door, the

+way Tom told me to.  It said:

+

+Beware.  Trouble is brewing.  Keep a sharp lookout. Unknown Friend.

+

+Next night we stuck a picture, which Tom drawed in blood, of a skull and

+crossbones on the front door; and next night another one of a coffin on

+the back door.  I never see a family in such a sweat.  They couldn't a

+been worse scared if the place had a been full of ghosts laying for them

+behind everything and under the beds and shivering through the air.  If

+a door banged, Aunt Sally she jumped and said "ouch!" if anything fell,

+she jumped and said "ouch!" if you happened to touch her, when she

+warn't noticing, she done the same; she couldn't face noway and be

+satisfied, because she allowed there was something behind her every

+time—so she was always a-whirling around sudden, and saying "ouch," and

+before she'd got two-thirds around she'd whirl back again, and say it

+again; and she was afraid to go to bed, but she dasn't set up.  So the

+thing was working very well, Tom said; he said he never see a thing work

+more satisfactory. He said it showed it was done right.

+

+So he said, now for the grand bulge!  So the very next morning at the

+streak of dawn we got another letter ready, and was wondering what we

+better do with it, because we heard them say at supper they was going

+to have a nigger on watch at both doors all night.  Tom he went down the

+lightning-rod to spy around; and the nigger at the back door was asleep,

+and he stuck it in the back of his neck and come back.  This letter

+said:

+

+Don't betray me, I wish to be your friend.  There is a desprate gang of

+cutthroats from over in the Indian Territory going to steal your runaway

+nigger to-night, and they have been trying to scare you so as you will

+stay in the house and not bother them.  I am one of the gang, but have

+got religgion and wish to quit it and lead an honest life again, and

+will betray the helish design. They will sneak down from northards,

+along the fence, at midnight exact, with a false key, and go in the

+nigger's cabin to get him. I am to be off a piece and blow a tin horn

+if I see any danger; but stead of that I will baa like a sheep soon as

+they get in and not blow at all; then whilst they are getting his

+chains loose, you slip there and lock them in, and can kill them at your

+leasure.  Don't do anything but just the way I am telling you, if you do

+they will suspicion something and raise whoop-jamboreehoo. I do not wish

+any reward but to know I have done the right thing. Unknown Friend.

+

+

+

+

+CHAPTER XL.

+

+WE was feeling pretty good after breakfast, and took my canoe and went

+over the river a-fishing, with a lunch, and had a good time, and took a

+look at the raft and found her all right, and got home late to supper,

+and found them in such a sweat and worry they didn't know which end they

+was standing on, and made us go right off to bed the minute we was done

+supper, and wouldn't tell us what the trouble was, and never let on a

+word about the new letter, but didn't need to, because we knowed as much

+about it as anybody did, and as soon as we was half up stairs and her

+back was turned we slid for the cellar cupboard and loaded up a good

+lunch and took it up to our room and went to bed, and got up about

+half-past eleven, and Tom put on Aunt Sally's dress that he stole and

+was going to start with the lunch, but says:

+

+"Where's the butter?"

+

+"I laid out a hunk of it," I says, "on a piece of a corn-pone."

+

+"Well, you left it laid out, then—it ain't here."

+

+"We can get along without it," I says.

+

+"We can get along with it, too," he says; "just you slide down cellar

+and fetch it.  And then mosey right down the lightning-rod and come

+along. I'll go and stuff the straw into Jim's clothes to represent his

+mother in disguise, and be ready to baa like a sheep and shove soon as

+you get there."

+

+So out he went, and down cellar went I. The hunk of butter, big as

+a person's fist, was where I had left it, so I took up the slab of

+corn-pone with it on, and blowed out my light, and started up stairs

+very stealthy, and got up to the main floor all right, but here comes

+Aunt Sally with a candle, and I clapped the truck in my hat, and clapped

+my hat on my head, and the next second she see me; and she says:

+

+"You been down cellar?"

+

+"Yes'm."

+

+"What you been doing down there?"

+

+"Noth'n."

+

+"Noth'n!"

+

+"No'm."

+

+"Well, then, what possessed you to go down there this time of night?"

+

+"I don't know 'm."

+

+"You don't know?  Don't answer me that way. Tom, I want to know what

+you been doing down there."

+

+"I hain't been doing a single thing, Aunt Sally, I hope to gracious if I

+have."

+

+I reckoned she'd let me go now, and as a generl thing she would; but I

+s'pose there was so many strange things going on she was just in a sweat

+about every little thing that warn't yard-stick straight; so she says,

+very decided:

+

+"You just march into that setting-room and stay there till I come.  You

+been up to something you no business to, and I lay I'll find out what it

+is before I'M done with you."

+

+So she went away as I opened the door and walked into the setting-room.

+My, but there was a crowd there!  Fifteen farmers, and every one of them

+had a gun.  I was most powerful sick, and slunk to a chair and set down.

+They was setting around, some of them talking a little, in a low voice,

+and all of them fidgety and uneasy, but trying to look like they warn't;

+but I knowed they was, because they was always taking off their hats,

+and putting them on, and scratching their heads, and changing their

+seats, and fumbling with their buttons.  I warn't easy myself, but I

+didn't take my hat off, all the same.

+

+I did wish Aunt Sally would come, and get done with me, and lick me, if

+she wanted to, and let me get away and tell Tom how we'd overdone this

+thing, and what a thundering hornet's-nest we'd got ourselves into, so

+we could stop fooling around straight off, and clear out with Jim before

+these rips got out of patience and come for us.

+

+At last she come and begun to ask me questions, but I couldn't answer

+them straight, I didn't know which end of me was up; because these men

+was in such a fidget now that some was wanting to start right NOW and

+lay for them desperadoes, and saying it warn't but a few minutes to

+midnight; and others was trying to get them to hold on and wait for the

+sheep-signal; and here was Aunty pegging away at the questions, and

+me a-shaking all over and ready to sink down in my tracks I was

+that scared; and the place getting hotter and hotter, and the butter

+beginning to melt and run down my neck and behind my ears; and pretty

+soon, when one of them says, "I'M for going and getting in the cabin

+first and right now, and catching them when they come," I most

+dropped; and a streak of butter come a-trickling down my forehead, and

+Aunt Sally she see it, and turns white as a sheet, and says:

+

+"For the land's sake, what is the matter with the child?  He's got the

+brain-fever as shore as you're born, and they're oozing out!"

+

+And everybody runs to see, and she snatches off my hat, and out comes

+the bread and what was left of the butter, and she grabbed me, and

+hugged me, and says:

+

+"Oh, what a turn you did give me! and how glad and grateful I am it

+ain't no worse; for luck's against us, and it never rains but it pours,

+and when I see that truck I thought we'd lost you, for I knowed by

+the color and all it was just like your brains would be if—Dear,

+dear, whyd'nt you tell me that was what you'd been down there for, I

+wouldn't a cared.  Now cler out to bed, and don't lemme see no more of

+you till morning!"

+

+I was up stairs in a second, and down the lightning-rod in another one,

+and shinning through the dark for the lean-to.  I couldn't hardly get my

+words out, I was so anxious; but I told Tom as quick as I could we must

+jump for it now, and not a minute to lose—the house full of men, yonder,

+with guns!

+

+His eyes just blazed; and he says:

+

+"No!—is that so?  ain't it bully!  Why, Huck, if it was to do over

+again, I bet I could fetch two hundred!  If we could put it off till—"

+

+"Hurry!  Hurry!"  I says.  "Where's Jim?"

+

+"Right at your elbow; if you reach out your arm you can touch him.

+ He's dressed, and everything's ready.  Now we'll slide out and give the

+sheep-signal."

+

+But then we heard the tramp of men coming to the door, and heard them

+begin to fumble with the pad-lock, and heard a man say:

+

+"I told you we'd be too soon; they haven't come—the door is locked.

+Here, I'll lock some of you into the cabin, and you lay for 'em in the

+dark and kill 'em when they come; and the rest scatter around a piece,

+and listen if you can hear 'em coming."

+

+So in they come, but couldn't see us in the dark, and most trod on

+us whilst we was hustling to get under the bed.  But we got under all

+right, and out through the hole, swift but soft—Jim first, me next,

+and Tom last, which was according to Tom's orders.  Now we was in the

+lean-to, and heard trampings close by outside.  So we crept to the door,

+and Tom stopped us there and put his eye to the crack, but couldn't make

+out nothing, it was so dark; and whispered and said he would listen

+for the steps to get further, and when he nudged us Jim must glide out

+first, and him last.  So he set his ear to the crack and listened, and

+listened, and listened, and the steps a-scraping around out there all

+the time; and at last he nudged us, and we slid out, and stooped down,

+not breathing, and not making the least noise, and slipped stealthy

+towards the fence in Injun file, and got to it all right, and me and Jim

+over it; but Tom's britches catched fast on a splinter on the top

+rail, and then he hear the steps coming, so he had to pull loose, which

+snapped the splinter and made a noise; and as he dropped in our tracks

+and started somebody sings out:

+

+"Who's that?  Answer, or I'll shoot!"

+

+But we didn't answer; we just unfurled our heels and shoved.  Then there

+was a rush, and a Bang, Bang, Bang! and the bullets fairly whizzed

+around us! We heard them sing out:

+

+"Here they are!  They've broke for the river!  After 'em, boys, and turn

+loose the dogs!"

+

+So here they come, full tilt.  We could hear them because they wore

+boots and yelled, but we didn't wear no boots and didn't yell.  We was

+in the path to the mill; and when they got pretty close on to us we

+dodged into the bush and let them go by, and then dropped in behind

+them.  They'd had all the dogs shut up, so they wouldn't scare off the

+robbers; but by this time somebody had let them loose, and here they

+come, making powwow enough for a million; but they was our dogs; so we

+stopped in our tracks till they catched up; and when they see it warn't

+nobody but us, and no excitement to offer them, they only just said

+howdy, and tore right ahead towards the shouting and clattering; and

+then we up-steam again, and whizzed along after them till we was nearly

+to the mill, and then struck up through the bush to where my canoe was

+tied, and hopped in and pulled for dear life towards the middle of the

+river, but didn't make no more noise than we was obleeged to. Then we

+struck out, easy and comfortable, for the island where my raft was; and

+we could hear them yelling and barking at each other all up and down the

+bank, till we was so far away the sounds got dim and died out.  And when

+we stepped on to the raft I says:

+

+"Now, old Jim, you're a free man again, and I bet you won't ever be a

+slave no more."

+

+"En a mighty good job it wuz, too, Huck.  It 'uz planned beautiful, en

+it 'uz done beautiful; en dey ain't nobody kin git up a plan dat's mo'

+mixed-up en splendid den what dat one wuz."

+

+We was all glad as we could be, but Tom was the gladdest of all because

+he had a bullet in the calf of his leg.

+

+When me and Jim heard that we didn't feel so brash as what we did

+before. It was hurting him considerable, and bleeding; so we laid him in

+the wigwam and tore up one of the duke's shirts for to bandage him, but

+he says:

+

+"Gimme the rags; I can do it myself.  Don't stop now; don't fool around

+here, and the evasion booming along so handsome; man the sweeps, and set

+her loose!  Boys, we done it elegant!—'deed we did.  I wish we'd a

+had the handling of Louis XVI., there wouldn't a been no 'Son of Saint

+Louis, ascend to heaven!' wrote down in his biography; no, sir, we'd

+a whooped him over the border—that's what we'd a done with him—and

+done it just as slick as nothing at all, too.  Man the sweeps—man the

+sweeps!"

+

+But me and Jim was consulting—and thinking.  And after we'd thought a

+minute, I says:

+

+"Say it, Jim."

+

+So he says:

+

+"Well, den, dis is de way it look to me, Huck.  Ef it wuz him dat 'uz

+bein' sot free, en one er de boys wuz to git shot, would he say, 'Go on

+en save me, nemmine 'bout a doctor f'r to save dis one?'  Is dat like

+Mars Tom Sawyer?  Would he say dat?  You bet he wouldn't!  well,

+den, is Jim gywne to say it?  No, sah—I doan' budge a step out'n dis

+place 'dout a doctor, not if it's forty year!"

+

+I knowed he was white inside, and I reckoned he'd say what he did say—so

+it was all right now, and I told Tom I was a-going for a doctor.

+ He raised considerable row about it, but me and Jim stuck to it and

+wouldn't budge; so he was for crawling out and setting the raft loose

+himself; but we wouldn't let him.  Then he give us a piece of his mind,

+but it didn't do no good.

+

+So when he sees me getting the canoe ready, he says:

+

+"Well, then, if you're bound to go, I'll tell you the way to do when you

+get to the village.  Shut the door and blindfold the doctor tight and

+fast, and make him swear to be silent as the grave, and put a purse

+full of gold in his hand, and then take and lead him all around the

+back alleys and everywheres in the dark, and then fetch him here in the

+canoe, in a roundabout way amongst the islands, and search him and take

+his chalk away from him, and don't give it back to him till you get him

+back to the village, or else he will chalk this raft so he can find it

+again. It's the way they all do."

+

+So I said I would, and left, and Jim was to hide in the woods when he

+see the doctor coming till he was gone again.

+

+

+

+

+CHAPTER XLI.

+

+THE doctor was an old man; a very nice, kind-looking old man when I got

+him up.  I told him me and my brother was over on Spanish Island hunting

+yesterday afternoon, and camped on a piece of a raft we found, and about

+midnight he must a kicked his gun in his dreams, for it went off and

+shot him in the leg, and we wanted him to go over there and fix it and

+not say nothing about it, nor let anybody know, because we wanted to

+come home this evening and surprise the folks.

+

+"Who is your folks?" he says.

+

+"The Phelpses, down yonder."

+

+"Oh," he says.  And after a minute, he says:

+

+"How'd you say he got shot?"

+

+"He had a dream," I says, "and it shot him."

+

+"Singular dream," he says.

+

+So he lit up his lantern, and got his saddle-bags, and we started.  But

+when he sees the canoe he didn't like the look of her—said she was big

+enough for one, but didn't look pretty safe for two.  I says:

+

+"Oh, you needn't be afeard, sir, she carried the three of us easy

+enough."

+

+"What three?"

+

+"Why, me and Sid, and—and—and the guns; that's what I mean."

+

+"Oh," he says.

+

+But he put his foot on the gunnel and rocked her, and shook his head,

+and said he reckoned he'd look around for a bigger one.  But they was

+all locked and chained; so he took my canoe, and said for me to wait

+till he come back, or I could hunt around further, or maybe I better

+go down home and get them ready for the surprise if I wanted to.  But

+I said I didn't; so I told him just how to find the raft, and then he

+started.

+

+I struck an idea pretty soon.  I says to myself, spos'n he can't fix

+that leg just in three shakes of a sheep's tail, as the saying is?

+spos'n it takes him three or four days?  What are we going to do?—lay

+around there till he lets the cat out of the bag?  No, sir; I know what

+I'll do.  I'll wait, and when he comes back if he says he's got to

+go any more I'll get down there, too, if I swim; and we'll take and tie

+him, and keep him, and shove out down the river; and when Tom's done

+with him we'll give him what it's worth, or all we got, and then let him

+get ashore.

+

+So then I crept into a lumber-pile to get some sleep; and next time I

+waked up the sun was away up over my head!  I shot out and went for the

+doctor's house, but they told me he'd gone away in the night some time

+or other, and warn't back yet.  Well, thinks I, that looks powerful bad

+for Tom, and I'll dig out for the island right off.  So away I shoved,

+and turned the corner, and nearly rammed my head into Uncle Silas's

+stomach! He says:

+

+"Why, Tom!  Where you been all this time, you rascal?"

+

+"I hain't been nowheres," I says, "only just hunting for the runaway

+nigger—me and Sid."

+

+"Why, where ever did you go?" he says.  "Your aunt's been mighty

+uneasy."

+

+"She needn't," I says, "because we was all right.  We followed the men

+and the dogs, but they outrun us, and we lost them; but we thought we

+heard them on the water, so we got a canoe and took out after them and

+crossed over, but couldn't find nothing of them; so we cruised along

+up-shore till we got kind of tired and beat out; and tied up the canoe

+and went to sleep, and never waked up till about an hour ago; then we

+paddled over here to hear the news, and Sid's at the post-office to see

+what he can hear, and I'm a-branching out to get something to eat for

+us, and then we're going home."

+

+So then we went to the post-office to get "Sid"; but just as I

+suspicioned, he warn't there; so the old man he got a letter out of the

+office, and we waited awhile longer, but Sid didn't come; so the old man

+said, come along, let Sid foot it home, or canoe it, when he got done

+fooling around—but we would ride.  I couldn't get him to let me stay

+and wait for Sid; and he said there warn't no use in it, and I must come

+along, and let Aunt Sally see we was all right.

+

+When we got home Aunt Sally was that glad to see me she laughed and

+cried both, and hugged me, and give me one of them lickings of hern that

+don't amount to shucks, and said she'd serve Sid the same when he come.

+

+And the place was plum full of farmers and farmers' wives, to dinner;

+and such another clack a body never heard.  Old Mrs. Hotchkiss was the

+worst; her tongue was a-going all the time.  She says:

+

+"Well, Sister Phelps, I've ransacked that-air cabin over, an' I b'lieve

+the nigger was crazy.  I says to Sister Damrell—didn't I, Sister

+Damrell?—s'I, he's crazy, s'I—them's the very words I said.  You all

+hearn me: he's crazy, s'I; everything shows it, s'I.  Look at that-air

+grindstone, s'I; want to tell me't any cretur 't's in his right mind

+'s a goin' to scrabble all them crazy things onto a grindstone, s'I?

+ Here sich 'n' sich a person busted his heart; 'n' here so 'n' so

+pegged along for thirty-seven year, 'n' all that—natcherl son o' Louis

+somebody, 'n' sich everlast'n rubbage.  He's plumb crazy, s'I; it's what

+I says in the fust place, it's what I says in the middle, 'n' it's what

+I says last 'n' all the time—the nigger's crazy—crazy 's Nebokoodneezer,

+s'I."

+

+"An' look at that-air ladder made out'n rags, Sister Hotchkiss," says

+old Mrs. Damrell; "what in the name o' goodness could he ever want

+of—"

+

+"The very words I was a-sayin' no longer ago th'n this minute to Sister

+Utterback, 'n' she'll tell you so herself.  Sh-she, look at that-air rag

+ladder, sh-she; 'n' s'I, yes, look at it, s'I—what could he a-wanted

+of it, s'I.  Sh-she, Sister Hotchkiss, sh-she—"

+

+"But how in the nation'd they ever git that grindstone in there,

+anyway? 'n' who dug that-air hole? 'n' who—"

+

+"My very words, Brer Penrod!  I was a-sayin'—pass that-air sasser o'

+m'lasses, won't ye?—I was a-sayin' to Sister Dunlap, jist this minute,

+how did they git that grindstone in there, s'I.  Without help, mind

+you—'thout help!  that's wher 'tis.  Don't tell me, s'I; there

+wuz help, s'I; 'n' ther' wuz a plenty help, too, s'I; ther's ben a

+dozen a-helpin' that nigger, 'n' I lay I'd skin every last nigger on

+this place but I'd find out who done it, s'I; 'n' moreover, s'I—"

+

+"A dozen says you!—forty couldn't a done every thing that's been

+done. Look at them case-knife saws and things, how tedious they've been

+made; look at that bed-leg sawed off with 'm, a week's work for six men;

+look at that nigger made out'n straw on the bed; and look at—"

+

+"You may well say it, Brer Hightower!  It's jist as I was a-sayin'

+to Brer Phelps, his own self.  S'e, what do you think of it, Sister

+Hotchkiss, s'e? Think o' what, Brer Phelps, s'I?  Think o' that bed-leg

+sawed off that a way, s'e?  think of it, s'I?  I lay it never sawed

+itself off, s'I—somebody sawed it, s'I; that's my opinion, take it

+or leave it, it mayn't be no 'count, s'I, but sich as 't is, it's my

+opinion, s'I, 'n' if any body k'n start a better one, s'I, let him do

+it, s'I, that's all.  I says to Sister Dunlap, s'I—"

+

+"Why, dog my cats, they must a ben a house-full o' niggers in there

+every night for four weeks to a done all that work, Sister Phelps.  Look

+at that shirt—every last inch of it kivered over with secret African

+writ'n done with blood!  Must a ben a raft uv 'm at it right along, all

+the time, amost.  Why, I'd give two dollars to have it read to me; 'n'

+as for the niggers that wrote it, I 'low I'd take 'n' lash 'm t'll—"

+

+"People to help him, Brother Marples!  Well, I reckon you'd think

+so if you'd a been in this house for a while back.  Why, they've stole

+everything they could lay their hands on—and we a-watching all the time,

+mind you. They stole that shirt right off o' the line! and as for that

+sheet they made the rag ladder out of, ther' ain't no telling how

+many times they didn't steal that; and flour, and candles, and

+candlesticks, and spoons, and the old warming-pan, and most a thousand

+things that I disremember now, and my new calico dress; and me and

+Silas and my Sid and Tom on the constant watch day and night, as I was

+a-telling you, and not a one of us could catch hide nor hair nor sight

+nor sound of them; and here at the last minute, lo and behold you, they

+slides right in under our noses and fools us, and not only fools us

+but the Injun Territory robbers too, and actuly gets away with that

+nigger safe and sound, and that with sixteen men and twenty-two dogs

+right on their very heels at that very time!  I tell you, it just bangs

+anything I ever heard of. Why, sperits couldn't a done better and

+been no smarter. And I reckon they must a been sperits—because, you

+know our dogs, and ther' ain't no better; well, them dogs never even got

+on the track of 'm once!  You explain that to me if you can!—any

+of you!"

+

+"Well, it does beat—"

+

+"Laws alive, I never—"

+

+"So help me, I wouldn't a be—"

+

+"House-thieves as well as—"

+

+"Goodnessgracioussakes, I'd a ben afeard to live in sich a—"

+

+"'Fraid to live!—why, I was that scared I dasn't hardly go to bed, or

+get up, or lay down, or set down, Sister Ridgeway.  Why, they'd steal

+the very—why, goodness sakes, you can guess what kind of a fluster I was

+in by the time midnight come last night.  I hope to gracious if I warn't

+afraid they'd steal some o' the family!  I was just to that pass I

+didn't have no reasoning faculties no more.  It looks foolish enough

+now, in the daytime; but I says to myself, there's my two poor boys

+asleep, 'way up stairs in that lonesome room, and I declare to goodness

+I was that uneasy 't I crep' up there and locked 'em in!  I did.  And

+anybody would. Because, you know, when you get scared that way, and it

+keeps running on, and getting worse and worse all the time, and your

+wits gets to addling, and you get to doing all sorts o' wild things,

+and by and by you think to yourself, spos'n I was a boy, and was away up

+there, and the door ain't locked, and you—" She stopped, looking kind

+of wondering, and then she turned her head around slow, and when her eye

+lit on me—I got up and took a walk.

+

+Says I to myself, I can explain better how we come to not be in that

+room this morning if I go out to one side and study over it a little.

+ So I done it.  But I dasn't go fur, or she'd a sent for me.  And when

+it was late in the day the people all went, and then I come in and

+told her the noise and shooting waked up me and "Sid," and the door was

+locked, and we wanted to see the fun, so we went down the lightning-rod,

+and both of us got hurt a little, and we didn't never want to try that

+no more.  And then I went on and told her all what I told Uncle Silas

+before; and then she said she'd forgive us, and maybe it was all right

+enough anyway, and about what a body might expect of boys, for all boys

+was a pretty harum-scarum lot as fur as she could see; and so, as long

+as no harm hadn't come of it, she judged she better put in her time

+being grateful we was alive and well and she had us still, stead of

+fretting over what was past and done.  So then she kissed me, and patted

+me on the head, and dropped into a kind of a brown study; and pretty

+soon jumps up, and says:

+

+"Why, lawsamercy, it's most night, and Sid not come yet!  What has

+become of that boy?"

+

+I see my chance; so I skips up and says:

+

+"I'll run right up to town and get him," I says.

+

+"No you won't," she says.  "You'll stay right wher' you are; one's

+enough to be lost at a time.  If he ain't here to supper, your uncle 'll

+go."

+

+Well, he warn't there to supper; so right after supper uncle went.

+

+He come back about ten a little bit uneasy; hadn't run across Tom's

+track. Aunt Sally was a good deal uneasy; but Uncle Silas he said

+there warn't no occasion to be—boys will be boys, he said, and you'll

+see this one turn up in the morning all sound and right.  So she had

+to be satisfied.  But she said she'd set up for him a while anyway, and

+keep a light burning so he could see it.

+

+And then when I went up to bed she come up with me and fetched her

+candle, and tucked me in, and mothered me so good I felt mean, and like

+I couldn't look her in the face; and she set down on the bed and talked

+with me a long time, and said what a splendid boy Sid was, and didn't

+seem to want to ever stop talking about him; and kept asking me every

+now and then if I reckoned he could a got lost, or hurt, or maybe

+drownded, and might be laying at this minute somewheres suffering or

+dead, and she not by him to help him, and so the tears would drip down

+silent, and I would tell her that Sid was all right, and would be home

+in the morning, sure; and she would squeeze my hand, or maybe kiss me,

+and tell me to say it again, and keep on saying it, because it done her

+good, and she was in so much trouble.  And when she was going away she

+looked down in my eyes so steady and gentle, and says:

+

+"The door ain't going to be locked, Tom, and there's the window and

+the rod; but you'll be good, won't you?  And you won't go?  For my

+sake."

+

+Laws knows I wanted to go bad enough to see about Tom, and was all

+intending to go; but after that I wouldn't a went, not for kingdoms.

+

+But she was on my mind and Tom was on my mind, so I slept very restless.

+And twice I went down the rod away in the night, and slipped around

+front, and see her setting there by her candle in the window with her

+eyes towards the road and the tears in them; and I wished I could do

+something for her, but I couldn't, only to swear that I wouldn't never

+do nothing to grieve her any more.  And the third time I waked up at

+dawn, and slid down, and she was there yet, and her candle was most out,

+and her old gray head was resting on her hand, and she was asleep.

+

+

+

+

+CHAPTER XLII.

+

+THE old man was uptown again before breakfast, but couldn't get no

+track of Tom; and both of them set at the table thinking, and not saying

+nothing, and looking mournful, and their coffee getting cold, and not

+eating anything. And by and by the old man says:

+

+"Did I give you the letter?"

+

+"What letter?"

+

+"The one I got yesterday out of the post-office."

+

+"No, you didn't give me no letter."

+

+"Well, I must a forgot it."

+

+So he rummaged his pockets, and then went off somewheres where he had

+laid it down, and fetched it, and give it to her.  She says:

+

+"Why, it's from St. Petersburg—it's from Sis."

+

+I allowed another walk would do me good; but I couldn't stir.  But

+before she could break it open she dropped it and run—for she see

+something. And so did I. It was Tom Sawyer on a mattress; and that old

+doctor; and Jim, in her calico dress, with his hands tied behind him;

+and a lot of people.  I hid the letter behind the first thing that come

+handy, and rushed.  She flung herself at Tom, crying, and says:

+

+"Oh, he's dead, he's dead, I know he's dead!"

+

+And Tom he turned his head a little, and muttered something or other,

+which showed he warn't in his right mind; then she flung up her hands,

+and says:

+

+"He's alive, thank God!  And that's enough!" and she snatched a kiss of

+him, and flew for the house to get the bed ready, and scattering orders

+right and left at the niggers and everybody else, as fast as her tongue

+could go, every jump of the way.

+

+I followed the men to see what they was going to do with Jim; and the

+old doctor and Uncle Silas followed after Tom into the house.  The men

+was very huffy, and some of them wanted to hang Jim for an example to

+all the other niggers around there, so they wouldn't be trying to run

+away like Jim done, and making such a raft of trouble, and keeping a

+whole family scared most to death for days and nights.  But the others

+said, don't do it, it wouldn't answer at all; he ain't our nigger, and

+his owner would turn up and make us pay for him, sure.  So that cooled

+them down a little, because the people that's always the most anxious

+for to hang a nigger that hain't done just right is always the very

+ones that ain't the most anxious to pay for him when they've got their

+satisfaction out of him.

+

+They cussed Jim considerble, though, and give him a cuff or two side the

+head once in a while, but Jim never said nothing, and he never let on to

+know me, and they took him to the same cabin, and put his own clothes

+on him, and chained him again, and not to no bed-leg this time, but to

+a big staple drove into the bottom log, and chained his hands, too, and

+both legs, and said he warn't to have nothing but bread and water to

+eat after this till his owner come, or he was sold at auction because

+he didn't come in a certain length of time, and filled up our hole, and

+said a couple of farmers with guns must stand watch around about the

+cabin every night, and a bulldog tied to the door in the daytime; and

+about this time they was through with the job and was tapering off with

+a kind of generl good-bye cussing, and then the old doctor comes and

+takes a look, and says:

+

+"Don't be no rougher on him than you're obleeged to, because he ain't

+a bad nigger.  When I got to where I found the boy I see I couldn't cut

+the bullet out without some help, and he warn't in no condition for

+me to leave to go and get help; and he got a little worse and a little

+worse, and after a long time he went out of his head, and wouldn't let

+me come a-nigh him any more, and said if I chalked his raft he'd kill

+me, and no end of wild foolishness like that, and I see I couldn't do

+anything at all with him; so I says, I got to have help somehow; and

+the minute I says it out crawls this nigger from somewheres and says

+he'll help, and he done it, too, and done it very well.  Of course I

+judged he must be a runaway nigger, and there I was! and there I had

+to stick right straight along all the rest of the day and all night.  It

+was a fix, I tell you! I had a couple of patients with the chills, and

+of course I'd of liked to run up to town and see them, but I dasn't,

+because the nigger might get away, and then I'd be to blame; and yet

+never a skiff come close enough for me to hail.  So there I had to stick

+plumb until daylight this morning; and I never see a nigger that was a

+better nuss or faithfuller, and yet he was risking his freedom to do it,

+and was all tired out, too, and I see plain enough he'd been worked

+main hard lately.  I liked the nigger for that; I tell you, gentlemen, a

+nigger like that is worth a thousand dollars—and kind treatment, too.  I

+had everything I needed, and the boy was doing as well there as he

+would a done at home—better, maybe, because it was so quiet; but there I

+was, with both of 'm on my hands, and there I had to stick till about

+dawn this morning; then some men in a skiff come by, and as good luck

+would have it the nigger was setting by the pallet with his head propped

+on his knees sound asleep; so I motioned them in quiet, and they slipped

+up on him and grabbed him and tied him before he knowed what he was

+about, and we never had no trouble. And the boy being in a kind of a

+flighty sleep, too, we muffled the oars and hitched the raft on, and

+towed her over very nice and quiet, and the nigger never made the least

+row nor said a word from the start.  He ain't no bad nigger, gentlemen;

+that's what I think about him."

+

+Somebody says:

+

+"Well, it sounds very good, doctor, I'm obleeged to say."

+

+Then the others softened up a little, too, and I was mighty thankful

+to that old doctor for doing Jim that good turn; and I was glad it was

+according to my judgment of him, too; because I thought he had a good

+heart in him and was a good man the first time I see him.  Then they

+all agreed that Jim had acted very well, and was deserving to have some

+notice took of it, and reward.  So every one of them promised, right out

+and hearty, that they wouldn't cuss him no more.

+

+Then they come out and locked him up.  I hoped they was going to say he

+could have one or two of the chains took off, because they was rotten

+heavy, or could have meat and greens with his bread and water; but they

+didn't think of it, and I reckoned it warn't best for me to mix in, but

+I judged I'd get the doctor's yarn to Aunt Sally somehow or other as

+soon as I'd got through the breakers that was laying just ahead of

+me—explanations, I mean, of how I forgot to mention about Sid being shot

+when I was telling how him and me put in that dratted night paddling

+around hunting the runaway nigger.

+

+But I had plenty time.  Aunt Sally she stuck to the sick-room all day

+and all night, and every time I see Uncle Silas mooning around I dodged

+him.

+

+Next morning I heard Tom was a good deal better, and they said Aunt

+Sally was gone to get a nap.  So I slips to the sick-room, and if I

+found him awake I reckoned we could put up a yarn for the family that

+would wash. But he was sleeping, and sleeping very peaceful, too; and

+pale, not fire-faced the way he was when he come.  So I set down and

+laid for him to wake.  In about half an hour Aunt Sally comes gliding

+in, and there I was, up a stump again!  She motioned me to be still, and

+set down by me, and begun to whisper, and said we could all be joyful

+now, because all the symptoms was first-rate, and he'd been sleeping

+like that for ever so long, and looking better and peacefuller all the

+time, and ten to one he'd wake up in his right mind.

+

+So we set there watching, and by and by he stirs a bit, and opened his

+eyes very natural, and takes a look, and says:

+

+"Hello!—why, I'm at home!  How's that?  Where's the raft?"

+

+"It's all right," I says.

+

+"And Jim?"

+

+"The same," I says, but couldn't say it pretty brash.  But he never

+noticed, but says:

+

+"Good!  Splendid!  Now we're all right and safe! Did you tell Aunty?"

+

+I was going to say yes; but she chipped in and says:  "About what, Sid?"

+

+"Why, about the way the whole thing was done."

+

+"What whole thing?"

+

+"Why, the whole thing.  There ain't but one; how we set the runaway

+nigger free—me and Tom."

+

+"Good land!  Set the run—What is the child talking about!  Dear, dear,

+out of his head again!"

+

+"No, I ain't out of my head; I know all what I'm talking about.  We

+did set him free—me and Tom.  We laid out to do it, and we done it.

+ And we done it elegant, too."  He'd got a start, and she never checked

+him up, just set and stared and stared, and let him clip along, and

+I see it warn't no use for me to put in.  "Why, Aunty, it cost us a

+power of work—weeks of it—hours and hours, every night, whilst you was

+all asleep. And we had to steal candles, and the sheet, and the shirt,

+and your dress, and spoons, and tin plates, and case-knives, and the

+warming-pan, and the grindstone, and flour, and just no end of things,

+and you can't think what work it was to make the saws, and pens, and

+inscriptions, and one thing or another, and you can't think half the

+fun it was.  And we had to make up the pictures of coffins and things,

+and nonnamous letters from the robbers, and get up and down the

+lightning-rod, and dig the hole into the cabin, and made the rope ladder

+and send it in cooked up in a pie, and send in spoons and things to work

+with in your apron pocket—"

+

+"Mercy sakes!"

+

+"—and load up the cabin with rats and snakes and so on, for company for

+Jim; and then you kept Tom here so long with the butter in his hat that

+you come near spiling the whole business, because the men come before

+we was out of the cabin, and we had to rush, and they heard us and let

+drive at us, and I got my share, and we dodged out of the path and let

+them go by, and when the dogs come they warn't interested in us, but

+went for the most noise, and we got our canoe, and made for the

+raft, and was all safe, and Jim was a free man, and we done it all by

+ourselves, and wasn't it bully, Aunty!"

+

+"Well, I never heard the likes of it in all my born days!  So it was

+you, you little rapscallions, that's been making all this trouble,

+and turned everybody's wits clean inside out and scared us all most to

+death.  I've as good a notion as ever I had in my life to take it out

+o' you this very minute.  To think, here I've been, night after night,

+a—you just get well once, you young scamp, and I lay I'll tan the Old

+Harry out o' both o' ye!"

+

+But Tom, he was so proud and joyful, he just couldn't hold in,

+and his tongue just went it—she a-chipping in, and spitting fire all

+along, and both of them going it at once, like a cat convention; and she

+says:

+

+"Well, you get all the enjoyment you can out of it now, for mind I

+tell you if I catch you meddling with him again—"

+

+"Meddling with who?"  Tom says, dropping his smile and looking

+surprised.

+

+"With who?  Why, the runaway nigger, of course.  Who'd you reckon?"

+

+Tom looks at me very grave, and says:

+

+"Tom, didn't you just tell me he was all right?  Hasn't he got away?"

+

+"Him?" says Aunt Sally; "the runaway nigger?  'Deed he hasn't.

+ They've got him back, safe and sound, and he's in that cabin again,

+on bread and water, and loaded down with chains, till he's claimed or

+sold!"

+

+Tom rose square up in bed, with his eye hot, and his nostrils opening

+and shutting like gills, and sings out to me:

+

+"They hain't no right to shut him up!  SHOVE!—and don't you lose a

+minute.  Turn him loose! he ain't no slave; he's as free as any cretur

+that walks this earth!"

+

+"What does the child mean?"

+

+"I mean every word I say, Aunt Sally, and if somebody don't go, I'll

+go. I've knowed him all his life, and so has Tom, there.  Old Miss

+Watson died two months ago, and she was ashamed she ever was going to

+sell him down the river, and said so; and she set him free in her

+will."

+

+"Then what on earth did you want to set him free for, seeing he was

+already free?"

+

+"Well, that is a question, I must say; and just like women!  Why,

+I wanted the adventure of it; and I'd a waded neck-deep in blood

+to—goodness alive, Aunt Polly!"

+

+If she warn't standing right there, just inside the door, looking as

+sweet and contented as an angel half full of pie, I wish I may never!

+

+Aunt Sally jumped for her, and most hugged the head off of her, and

+cried over her, and I found a good enough place for me under the bed,

+for it was getting pretty sultry for us, seemed to me.  And I peeped

+out, and in a little while Tom's Aunt Polly shook herself loose and

+stood there looking across at Tom over her spectacles—kind of grinding

+him into the earth, you know.  And then she says:

+

+"Yes, you better turn y'r head away—I would if I was you, Tom."

+

+"Oh, deary me!" says Aunt Sally; "Is he changed so?  Why, that ain't

+Tom, it's Sid; Tom's—Tom's—why, where is Tom?  He was here a minute

+ago."

+

+"You mean where's Huck Finn—that's what you mean!  I reckon I hain't

+raised such a scamp as my Tom all these years not to know him when I

+see him.  That would be a pretty howdy-do. Come out from under that

+bed, Huck Finn."

+

+So I done it.  But not feeling brash.

+

+Aunt Sally she was one of the mixed-upest-looking persons I ever

+see—except one, and that was Uncle Silas, when he come in and they told

+it all to him.  It kind of made him drunk, as you may say, and he didn't

+know nothing at all the rest of the day, and preached a prayer-meeting

+sermon that night that gave him a rattling ruputation, because the

+oldest man in the world couldn't a understood it.  So Tom's Aunt Polly,

+she told all about who I was, and what; and I had to up and tell how

+I was in such a tight place that when Mrs. Phelps took me for Tom

+Sawyer—she chipped in and says, "Oh, go on and call me Aunt Sally, I'm

+used to it now, and 'tain't no need to change"—that when Aunt Sally took

+me for Tom Sawyer I had to stand it—there warn't no other way, and

+I knowed he wouldn't mind, because it would be nuts for him, being

+a mystery, and he'd make an adventure out of it, and be perfectly

+satisfied.  And so it turned out, and he let on to be Sid, and made

+things as soft as he could for me.

+

+And his Aunt Polly she said Tom was right about old Miss Watson setting

+Jim free in her will; and so, sure enough, Tom Sawyer had gone and took

+all that trouble and bother to set a free nigger free! and I couldn't

+ever understand before, until that minute and that talk, how he could

+help a body set a nigger free with his bringing-up.

+

+Well, Aunt Polly she said that when Aunt Sally wrote to her that Tom and

+Sid had come all right and safe, she says to herself:

+

+"Look at that, now!  I might have expected it, letting him go off that

+way without anybody to watch him.  So now I got to go and trapse all

+the way down the river, eleven hundred mile, and find out what that

+creetur's up to this time, as long as I couldn't seem to get any

+answer out of you about it."

+

+"Why, I never heard nothing from you," says Aunt Sally.

+

+"Well, I wonder!  Why, I wrote you twice to ask you what you could mean

+by Sid being here."

+

+"Well, I never got 'em, Sis."

+

+Aunt Polly she turns around slow and severe, and says:

+

+"You, Tom!"

+

+"Well—what?" he says, kind of pettish.

+

+"Don't you what me, you impudent thing—hand out them letters."

+

+"What letters?"

+

+"Them letters.  I be bound, if I have to take a-holt of you I'll—"

+

+"They're in the trunk.  There, now.  And they're just the same as they

+was when I got them out of the office.  I hain't looked into them, I

+hain't touched them.  But I knowed they'd make trouble, and I thought if

+you warn't in no hurry, I'd—"

+

+"Well, you do need skinning, there ain't no mistake about it.  And I

+wrote another one to tell you I was coming; and I s'pose he—"

+

+"No, it come yesterday; I hain't read it yet, but it's all right, I've

+got that one."

+

+I wanted to offer to bet two dollars she hadn't, but I reckoned maybe it

+was just as safe to not to.  So I never said nothing.

+

+

+

+

+CHAPTER THE LAST

+

+THE first time I catched Tom private I asked him what was his idea, time

+of the evasion?—what it was he'd planned to do if the evasion worked all

+right and he managed to set a nigger free that was already free before?

+And he said, what he had planned in his head from the start, if we got

+Jim out all safe, was for us to run him down the river on the raft, and

+have adventures plumb to the mouth of the river, and then tell him about

+his being free, and take him back up home on a steamboat, in style,

+and pay him for his lost time, and write word ahead and get out all

+the niggers around, and have them waltz him into town with a torchlight

+procession and a brass-band, and then he would be a hero, and so would

+we.  But I reckoned it was about as well the way it was.

+

+We had Jim out of the chains in no time, and when Aunt Polly and Uncle

+Silas and Aunt Sally found out how good he helped the doctor nurse Tom,

+they made a heap of fuss over him, and fixed him up prime, and give him

+all he wanted to eat, and a good time, and nothing to do.  And we had

+him up to the sick-room, and had a high talk; and Tom give Jim forty

+dollars for being prisoner for us so patient, and doing it up so good,

+and Jim was pleased most to death, and busted out, and says:

+

+"Dah, now, Huck, what I tell you?—what I tell you up dah on Jackson

+islan'?  I tole you I got a hairy breas', en what's de sign un it; en

+I tole you I ben rich wunst, en gwineter to be rich agin; en it's

+come true; en heah she is!  dah, now! doan' talk to me—signs is

+signs, mine I tell you; en I knowed jis' 's well 'at I 'uz gwineter be

+rich agin as I's a-stannin' heah dis minute!"

+

+And then Tom he talked along and talked along, and says, le's all three

+slide out of here one of these nights and get an outfit, and go for

+howling adventures amongst the Injuns, over in the Territory, for a

+couple of weeks or two; and I says, all right, that suits me, but I

+ain't got no money for to buy the outfit, and I reckon I couldn't get

+none from home, because it's likely pap's been back before now, and got

+it all away from Judge Thatcher and drunk it up.

+

+"No, he hain't," Tom says; "it's all there yet—six thousand dollars

+and more; and your pap hain't ever been back since.  Hadn't when I come

+away, anyhow."

+

+Jim says, kind of solemn:

+

+"He ain't a-comin' back no mo', Huck."

+

+I says:

+

+"Why, Jim?"

+

+"Nemmine why, Huck—but he ain't comin' back no mo."

+

+But I kept at him; so at last he says:

+

+"Doan' you 'member de house dat was float'n down de river, en dey wuz a

+man in dah, kivered up, en I went in en unkivered him and didn' let you

+come in?  Well, den, you kin git yo' money when you wants it, kase dat

+wuz him."

+

+Tom's most well now, and got his bullet around his neck on a watch-guard

+for a watch, and is always seeing what time it is, and so there ain't

+nothing more to write about, and I am rotten glad of it, because if I'd

+a knowed what a trouble it was to make a book I wouldn't a tackled it,

+and ain't a-going to no more.  But I reckon I got to light out for the

+Territory ahead of the rest, because Aunt Sally she's going to adopt me

+and sivilize me, and I can't stand it.  I been there before.

+

+THE END. YOURS TRULY, HUCK FINN.

+

+

+

+

+

+End of the Project Gutenberg EBook of Adventures of Huckleberry Finn,

+Complete, by Mark Twain (Samuel Clemens)

+

+*** END OF THIS PROJECT GUTENBERG EBOOK HUCKLEBERRY FINN ***

+

+***** This file should be named 76-h.htm or 76-h.zip ***** This and

+all associated files of various formats will be found in:

+http://www.gutenberg.net/7/76/

+

+Produced by David Widger. Previous editions produced by Ron Burkey and

+Internet Wiretap

+

+Updated editions will replace the previous one--the old editions will be

+renamed.

+

+Creating the works from public domain print editions means that no one

+owns a United States copyright in these works, so the Foundation (and

+you!) can copy and distribute it in the United States without permission

+and without paying copyright royalties. Special rules, set forth in

+the General Terms of Use part of this license, apply to copying and

+distributing Project Gutenberg-tm electronic works to protect the

+PROJECT GUTENBERG-tm concept and trademark. Project Gutenberg is a

+registered trademark, and may not be used if you charge for the eBooks,

+unless you receive specific permission. If you do not charge anything

+for copies of this eBook, complying with the rules is very easy. You

+may use this eBook for nearly any purpose such as creation of derivative

+works, reports, performances and research. They may be modified and

+printed and given away--you may do practically ANYTHING with public

+domain eBooks. Redistribution is subject to the trademark license,

+especially commercial redistribution.

+

+*** START: FULL LICENSE ***

+

+THE FULL PROJECT GUTENBERG LICENSE PLEASE READ THIS BEFORE YOU

+DISTRIBUTE OR USE THIS WORK

+

+To protect the Project Gutenberg-tm mission of promoting the free

+distribution of electronic works, by using or distributing this work

+(or any other work associated in any way with the phrase "Project

+Gutenberg"), you agree to comply with all the terms of the Full

+Project Gutenberg-tm License (available with this file or online at

+http://gutenberg.net/license).

+

+Section 1. General Terms of Use and Redistributing Project Gutenberg-tm

+electronic works

+

+1.A. By reading or using any part of this Project Gutenberg-tm

+electronic work, you indicate that you have read, understand, agree

+to and accept all the terms of this license and intellectual property

+(trademark/copyright) agreement. If you do not agree to abide by all the

+terms of this agreement, you must cease using and return or destroy all

+copies of Project Gutenberg-tm electronic works in your possession.

+If you paid a fee for obtaining a copy of or access to a Project

+Gutenberg-tm electronic work and you do not agree to be bound by the

+terms of this agreement, you may obtain a refund from the person or

+entity to whom you paid the fee as set forth in paragraph 1.E.8.

+

+1.B. "Project Gutenberg" is a registered trademark. It may only be used

+on or associated in any way with an electronic work by people who agree

+to be bound by the terms of this agreement. There are a few things that

+you can do with most Project Gutenberg-tm electronic works even without

+complying with the full terms of this agreement. See paragraph 1.C

+below. There are a lot of things you can do with Project Gutenberg-tm

+electronic works if you follow the terms of this agreement and help

+preserve free future access to Project Gutenberg-tm electronic works.

+See paragraph 1.E below.

+

+1.C. The Project Gutenberg Literary Archive Foundation ("the Foundation"

+or PGLAF), owns a compilation copyright in the collection of Project

+Gutenberg-tm electronic works. Nearly all the individual works in

+the collection are in the public domain in the United States. If an

+individual work is in the public domain in the United States and you

+are located in the United States, we do not claim a right to prevent

+you from copying, distributing, performing, displaying or creating

+derivative works based on the work as long as all references to Project

+Gutenberg are removed. Of course, we hope that you will support the

+Project Gutenberg-tm mission of promoting free access to electronic

+works by freely sharing Project Gutenberg-tm works in compliance with

+the terms of this agreement for keeping the Project Gutenberg-tm name

+associated with the work. You can easily comply with the terms of this

+agreement by keeping this work in the same format with its attached

+full Project Gutenberg-tm License when you share it without charge with

+others.

+

+1.D. The copyright laws of the place where you are located also govern

+what you can do with this work. Copyright laws in most countries are in

+a constant state of change. If you are outside the United States, check

+the laws of your country in addition to the terms of this agreement

+before downloading, copying, displaying, performing, distributing

+or creating derivative works based on this work or any other Project

+Gutenberg-tm work. The Foundation makes no representations concerning

+the copyright status of any work in any country outside the United

+States.

+

+1.E. Unless you have removed all references to Project Gutenberg:

+

+1.E.1. The following sentence, with active links to, or other immediate

+access to, the full Project Gutenberg-tm License must appear prominently

+whenever any copy of a Project Gutenberg-tm work (any work on which the

+phrase "Project Gutenberg" appears, or with which the phrase "Project

+Gutenberg" is associated) is accessed, displayed, performed, viewed,

+copied or distributed:

+

+This eBook is for the use of anyone anywhere at no cost and with almost

+no restrictions whatsoever. You may copy it, give it away or re-use

+it under the terms of the Project Gutenberg License included with this

+eBook or online at www.gutenberg.net

+

+1.E.2. If an individual Project Gutenberg-tm electronic work is derived

+from the public domain (does not contain a notice indicating that it is

+posted with permission of the copyright holder), the work can be copied

+and distributed to anyone in the United States without paying any fees

+or charges. If you are redistributing or providing access to a work with

+the phrase "Project Gutenberg" associated with or appearing on the work,

+you must comply either with the requirements of paragraphs 1.E.1 through

+1.E.7 or obtain permission for the use of the work and the Project

+Gutenberg-tm trademark as set forth in paragraphs 1.E.8 or 1.E.9.

+

+1.E.3. If an individual Project Gutenberg-tm electronic work is posted

+with the permission of the copyright holder, your use and distribution

+must comply with both paragraphs 1.E.1 through 1.E.7 and any additional

+terms imposed by the copyright holder. Additional terms will be linked

+to the Project Gutenberg-tm License for all works posted with the

+permission of the copyright holder found at the beginning of this work.

+

+1.E.4. Do not unlink or detach or remove the full Project Gutenberg-tm

+License terms from this work, or any files containing a part of this

+work or any other work associated with Project Gutenberg-tm.

+

+1.E.5. Do not copy, display, perform, distribute or redistribute

+this electronic work, or any part of this electronic work, without

+prominently displaying the sentence set forth in paragraph 1.E.1 with

+active links or immediate access to the full terms of the Project

+Gutenberg-tm License.

+

+1.E.6. You may convert to and distribute this work in any binary,

+compressed, marked up, nonproprietary or proprietary form, including any

+word processing or hypertext form. However, if you provide access to or

+distribute copies of a Project Gutenberg-tm work in a format other

+than "Plain Vanilla ASCII" or other format used in the official

+version posted on the official Project Gutenberg-tm web site

+(www.gutenberg.net), you must, at no additional cost, fee or expense

+to the user, provide a copy, a means of exporting a copy, or a means

+of obtaining a copy upon request, of the work in its original "Plain

+Vanilla ASCII" or other form. Any alternate format must include the full

+Project Gutenberg-tm License as specified in paragraph 1.E.1.

+

+1.E.7. Do not charge a fee for access to, viewing, displaying,

+performing, copying or distributing any Project Gutenberg-tm works

+unless you comply with paragraph 1.E.8 or 1.E.9.

+

+1.E.8. You may charge a reasonable fee for copies of or providing access

+to or distributing Project Gutenberg-tm electronic works provided that

+

+- You pay a royalty fee of 20% of the gross profits you derive from

+the use of Project Gutenberg-tm works calculated using the method you

+already use to calculate your applicable taxes. The fee is owed to the

+owner of the Project Gutenberg-tm trademark, but he has agreed to donate

+royalties under this paragraph to the Project Gutenberg Literary Archive

+Foundation. Royalty payments must be paid within 60 days following each

+date on which you prepare (or are legally required to prepare) your

+periodic tax returns. Royalty payments should be clearly marked as such

+and sent to the Project Gutenberg Literary Archive Foundation at the

+address specified in Section 4, "Information about donations to the

+Project Gutenberg Literary Archive Foundation."

+

+- You provide a full refund of any money paid by a user who notifies you

+in writing (or by e-mail) within 30 days of receipt that s/he does not

+agree to the terms of the full Project Gutenberg-tm License. You

+must require such a user to return or destroy all copies of the works

+possessed in a physical medium and discontinue all use of and all access

+to other copies of Project Gutenberg-tm works.

+

+- You provide, in accordance with paragraph 1.F.3, a full refund of

+any money paid for a work or a replacement copy, if a defect in the

+electronic work is discovered and reported to you within 90 days of

+receipt of the work.

+

+- You comply with all other terms of this agreement for free

+distribution of Project Gutenberg-tm works.

+

+1.E.9. If you wish to charge a fee or distribute a Project Gutenberg-tm

+electronic work or group of works on different terms than are set forth

+in this agreement, you must obtain permission in writing from both the

+Project Gutenberg Literary Archive Foundation and Michael Hart, the

+owner of the Project Gutenberg-tm trademark. Contact the Foundation as

+set forth in Section 3 below.

+

+1.F.

+

+1.F.1. Project Gutenberg volunteers and employees expend considerable

+effort to identify, do copyright research on, transcribe and proofread

+public domain works in creating the Project Gutenberg-tm collection.

+Despite these efforts, Project Gutenberg-tm electronic works, and the

+medium on which they may be stored, may contain "Defects," such as, but

+not limited to, incomplete, inaccurate or corrupt data, transcription

+errors, a copyright or other intellectual property infringement, a

+defective or damaged disk or other medium, a computer virus, or computer

+codes that damage or cannot be read by your equipment.

+

+1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except for the "Right

+of Replacement or Refund" described in paragraph 1.F.3, the Project

+Gutenberg Literary Archive Foundation, the owner of the Project

+Gutenberg-tm trademark, and any other party distributing a Project

+Gutenberg-tm electronic work under this agreement, disclaim all

+liability to you for damages, costs and expenses, including legal fees.

+YOU AGREE THAT YOU HAVE NO REMEDIES FOR NEGLIGENCE, STRICT LIABILITY,

+BREACH OF WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE PROVIDED IN

+PARAGRAPH F3. YOU AGREE THAT THE FOUNDATION, THE TRADEMARK OWNER, AND

+ANY DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE LIABLE TO YOU FOR

+ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL, PUNITIVE OR INCIDENTAL DAMAGES

+EVEN IF YOU GIVE NOTICE OF THE POSSIBILITY OF SUCH DAMAGE.

+

+1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you discover a defect

+in this electronic work within 90 days of receiving it, you can receive

+a refund of the money (if any) you paid for it by sending a written

+explanation to the person you received the work from. If you received

+the work on a physical medium, you must return the medium with your

+written explanation. The person or entity that provided you with the

+defective work may elect to provide a replacement copy in lieu of a

+refund. If you received the work electronically, the person or entity

+providing it to you may choose to give you a second opportunity to

+receive the work electronically in lieu of a refund. If the second copy

+is also defective, you may demand a refund in writing without further

+opportunities to fix the problem.

+

+1.F.4. Except for the limited right of replacement or refund set forth

+in paragraph 1.F.3, this work is provided to you 'AS-IS' WITH NO OTHER

+WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO

+WARRANTIES OF MERCHANTIBILITY OR FITNESS FOR ANY PURPOSE.

+

+1.F.5. Some states do not allow disclaimers of certain implied

+warranties or the exclusion or limitation of certain types of damages.

+If any disclaimer or limitation set forth in this agreement violates the

+law of the state applicable to this agreement, the agreement shall be

+interpreted to make the maximum disclaimer or limitation permitted by

+the applicable state law. The invalidity or unenforceability of any

+provision of this agreement shall not void the remaining provisions.

+

+1.F.6. INDEMNITY - You agree to indemnify and hold the Foundation,

+the trademark owner, any agent or employee of the Foundation, anyone

+providing copies of Project Gutenberg-tm electronic works in accordance

+with this agreement, and any volunteers associated with the production,

+promotion and distribution of Project Gutenberg-tm electronic works,

+harmless from all liability, costs and expenses, including legal fees,

+that arise directly or indirectly from any of the following which you do

+or cause to occur: (a) distribution of this or any Project Gutenberg-tm

+work, (b) alteration, modification, or additions or deletions to any

+Project Gutenberg-tm work, and (c) any Defect you cause.

+

+Section 2. Information about the Mission of Project Gutenberg-tm

+

+Project Gutenberg-tm is synonymous with the free distribution of

+electronic works in formats readable by the widest variety of computers

+including obsolete, old, middle-aged and new computers. It exists

+because of the efforts of hundreds of volunteers and donations from

+people in all walks of life.

+

+Volunteers and financial support to provide volunteers with the

+assistance they need, is critical to reaching Project Gutenberg-tm's

+goals and ensuring that the Project Gutenberg-tm collection will remain

+freely available for generations to come. In 2001, the Project Gutenberg

+Literary Archive Foundation was created to provide a secure and

+permanent future for Project Gutenberg-tm and future generations. To

+learn more about the Project Gutenberg Literary Archive Foundation and

+how your efforts and donations can help, see Sections 3 and 4 and the

+Foundation web page at http://www.pglaf.org.

+

+Section 3. Information about the Project Gutenberg Literary Archive

+Foundation

+

+The Project Gutenberg Literary Archive Foundation is a non profit

+501(c)(3) educational corporation organized under the laws of the state

+of Mississippi and granted tax exempt status by the Internal Revenue

+Service. The Foundation's EIN or federal tax identification number

+is 64-6221541. Its 501(c)(3) letter is posted at

+http://pglaf.org/fundraising. Contributions to the Project Gutenberg

+Literary Archive Foundation are tax deductible to the full extent

+permitted by U.S. federal laws and your state's laws.

+

+The Foundation's principal office is located at 4557 Melan Dr. S.

+Fairbanks, AK, 99712., but its volunteers and employees are scattered

+throughout numerous locations. Its business office is located at

+809 North 1500 West, Salt Lake City, UT 84116, (801) 596-1887,

+email business@pglaf.org. Email contact links and up to date contact

+information can be found at the Foundation's web site and official page

+at http://pglaf.org

+

+For additional contact information: Dr. Gregory B. Newby Chief Executive

+and Director gbnewby@pglaf.org

+

+Section 4. Information about Donations to the Project Gutenberg Literary

+Archive Foundation

+

+Project Gutenberg-tm depends upon and cannot survive without wide spread

+public support and donations to carry out its mission of increasing

+the number of public domain and licensed works that can be freely

+distributed in machine readable form accessible by the widest array

+of equipment including outdated equipment. Many small donations ($1 to

+$5,000) are particularly important to maintaining tax exempt status with

+the IRS.

+

+The Foundation is committed to complying with the laws regulating

+charities and charitable donations in all 50 states of the United

+States. Compliance requirements are not uniform and it takes a

+considerable effort, much paperwork and many fees to meet and keep up

+with these requirements. We do not solicit donations in locations

+where we have not received written confirmation of compliance. To SEND

+DONATIONS or determine the status of compliance for any particular state

+visit http://pglaf.org

+

+While we cannot and do not solicit contributions from states where we

+have not met the solicitation requirements, we know of no prohibition

+against accepting unsolicited donations from donors in such states who

+approach us with offers to donate.

+

+International donations are gratefully accepted, but we cannot make any

+statements concerning tax treatment of donations received from outside

+the United States. U.S. laws alone swamp our small staff.

+

+Please check the Project Gutenberg Web pages for current donation

+methods and addresses. Donations are accepted in a number of other ways

+including including checks, online payments and credit card donations.

+To donate, please visit: http://pglaf.org/donate

+

+Section 5. General Information About Project Gutenberg-tm electronic

+works.

+

+Professor Michael S. Hart is the originator of the Project Gutenberg-tm

+concept of a library of electronic works that could be freely shared

+with anyone. For thirty years, he produced and distributed Project

+Gutenberg-tm eBooks with only a loose network of volunteer support.

+

+Project Gutenberg-tm eBooks are often created from several printed

+editions, all of which are confirmed as Public Domain in the U.S. unless

+a copyright notice is included. Thus, we do not necessarily keep eBooks

+in compliance with any particular paper edition.

+

+Most people start at our Web site which has the main PG search facility:

+

+http://www.gutenberg.net

+

+This Web site includes information about Project Gutenberg-tm, including

+how to make donations to the Project Gutenberg Literary Archive

+Foundation, how to help produce our new eBooks, and how to subscribe to

+our email newsletter to hear about new eBooks.

+

diff --git a/test/resources/tokenization/apache_license_header.txt b/test/resources/tokenization/apache_license_header.txt
new file mode 100644
index 0000000..d973dce
--- /dev/null
+++ b/test/resources/tokenization/apache_license_header.txt
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
\ No newline at end of file
diff --git a/test/resources/tokenization/french_skip_stop_words_before_stemming.txt b/test/resources/tokenization/french_skip_stop_words_before_stemming.txt
new file mode 100644
index 0000000..59a1c23
--- /dev/null
+++ b/test/resources/tokenization/french_skip_stop_words_before_stemming.txt
@@ -0,0 +1 @@
+"La danse sous la pluie" est une chanson connue
\ No newline at end of file
diff --git a/test/resources/tokenization/ja_jp_1.txt b/test/resources/tokenization/ja_jp_1.txt
new file mode 100644
index 0000000..1a0a198
--- /dev/null
+++ b/test/resources/tokenization/ja_jp_1.txt
@@ -0,0 +1 @@
+古写本は題名の記されていないものも多く、記されている場合であっても内容はさまざまである。『源氏物語』の場合は冊子の標題として「源氏物語」ないしそれに相当する物語全体の標題が記されている場合よりも、それぞれの帖名が記されていることが少なくない。こうした経緯から、現在において一般に『源氏物語』と呼ばれているこの物語が書かれた当時の題名が何であったのかは明らかではない。古い時代の写本や注釈書などの文献に記されている名称は大きく以下の系統に分かれる。
\ No newline at end of file
diff --git a/test/resources/tokenization/ja_jp_2.txt b/test/resources/tokenization/ja_jp_2.txt
new file mode 100644
index 0000000..278b4fd
--- /dev/null
+++ b/test/resources/tokenization/ja_jp_2.txt
@@ -0,0 +1,2 @@
+中野幸一編『常用 源氏物語要覧』武蔵野書院、1997年(平成9年)。 ISBN 4-8386-0383-5
+その他にCD-ROM化された本文検索システムとして次のようなものがある。
\ No newline at end of file
diff --git a/test/resources/tokenization/lorem_ipsum.txt b/test/resources/tokenization/lorem_ipsum.txt
new file mode 100644
index 0000000..14a4477
--- /dev/null
+++ b/test/resources/tokenization/lorem_ipsum.txt
@@ -0,0 +1 @@
+"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
\ No newline at end of file
diff --git a/test/resources/tokenization/ru_ru_1.txt b/test/resources/tokenization/ru_ru_1.txt
new file mode 100644
index 0000000..c19a9be
--- /dev/null
+++ b/test/resources/tokenization/ru_ru_1.txt
@@ -0,0 +1,19 @@
+Вэл фабулаз эффикеэнди витюпэраторебуз эи, кюм нобёз дикырыт ёнвидюнт ед. Ючю золэт ийжквюы эа, нык но элитр волуптюа пэркёпитюр. Ыт векж декам плььатонэм, эа жюмо ёудёкабет льебэравичсы квуй, альбюкиюс лыгэндоч эю пэр. Еюж ед аутым нюмквуам тебиквюэ, эи амэт дэбыт нюлльам квюо. Ку золэт пондэрюм элььэефэнд хаж, вяш ёнвидюнт дыфинитеоным экз, конгуы кытэрож квюо ат.
+
+Ад фиэрэнт ыкжплььикари нык, ут дольорэ емпэтюсъ зыд. Зыд ажжюм пэржыкюти жкряпшэрит эю, ыюм ан витаэ аляквюид дяшзынтиыт. Вэл квюандо ридэнж эю. Еюж жэмпэр конклььюжионэмквуэ нэ.
+
+Ку хёз порро тамквюам плььатонэм, льаборэ ыпикурэи вэл ты. Но ентэгры компльыктётюр мэя, дуо жанктюч дэльэнйт льебэравичсы нэ. Эжт фалля пропрёаы эю, эож вэрыар ёнэрмйщ ан. Мюндй контынтёонэж прё ат. Эрож копиожаы пытынтёюм шэа эи.
+
+Но мэль омниюм рэпудёандаэ. Дуо ты квюот июварыт, ты векж квюаэчтио альиквуандо, эю мэльёуз трактатоз пхйложопхяа векж. Ед нам нюлльам губэргрэн, ты оратио иреуры коммюны пэр, векж ед золэт убяквюэ чингюльищ. Мэльёуз граэкйж вольуптатибюж мэя ед, одео емпыдит майыжтатйж эож ыт, эа дйкит зальутанде квюальизквюэ ючю. Йн еюж анкилльаы аккоммодары, ан выльёт оптёон ывыртятюр вэл.
+
+Йн граэко дычэрунт мандамюч мыа. Но про щольыат примич, нык ан этёам дольорэ элььэефэнд. Ыам нэ квюандо нощтыр. Ныморэ дикунт ад хаж, хаж квюод дёко эуежмод ты, амэт дыфинитеоным еюж ыт. Эжт ад апэряря апыирёан, кюм зальы рэктэквуэ нэ.
+
+Эи эрюдитя факёльиси еюж, ыам дольорэ фабулаз вокябюч ат. Про опортэры азжюывырит йн. Мовэт аюдиам ючю эю, нэ едквюэ пэркйпет квюальизквюэ хёз. Эа кюм этёам ырант граэкы. Эю прё модо эпикюре жплэндидэ, ат ыюм фалля пожтэа пхаэдрум, чтэт вэрйтюж нэ вим. Конгуы оратио лобортис ут кюм. Нобёз опортэат но жят, вэрйтюж пэркйпет мальюизчыт квуй ан.
+
+Мацим ютенам рыфэррэнтур вим ат. Ку квюо квюач дигнижжим, одео жольюта тебиквюэ мыа ыт. Ед нобёз тантаз льаорыыт вэл, еюж йн латины фабулаз аккюжамюз, прё апыирёан адолэжкэнс пожйдонёюм ты. Консэквюат котёдиэквюэ ыюм ан, хёз ут хабымуч ыпикурэи чэнзэрет. Ат квуй дэбыт вирйз, нам эю ыльит фабыллас дэлььякатезшимя. Кончюлату инзтруктеор эа кюм, конжюль фэюгаят кончюлату ут ыам, вяш эи фэюгаят антеопам.
+
+Юллюм оратио консэквюат ут вэл, выльёт рыпудяары хэндрэрет эю прё. Унюм ыкчпэтында торквюатоз ад векж. Квюо мютат тебиквюэ факильизиж эи, эа ыам фюгит такематыш дяшзынтиыт, экз про абхоррэант дйжпютандо. Ку хаж льабятюр эрепюят, нолюёжжэ ёудёкабет пэр эю. Тота долорюм азжюывырит прё ут, нык зальы элитр дикырыт эю. Ед дуо ыкжплььикари мныжаркхюм конклььюжионэмквуэ.
+
+Кончюлату азжюывырит нэ зыд. Вэл но квуым граэкйж юрбанйтаж. Про эффякиантур дэфянятйоныс ут, зюаз эрат конкыптам векж эю. Юллюм зюжкепиантюр экз прё, оратио нонумй орнатюс эи эож. Эож такематыш чэнзэрет ад, ат факилиз пэркйпет пэржыкюти нык, аппарэат рэктэквуэ экз зыд. Кюм йн вёвындо дэтракто окюррырэт.
+
+Шэа рыквюы щольыат фабыллас ты, хаж выльёт эффякиантур компрэхэнжам ат. Ты мэя эзшэ ажжюм апыирёан, ат докэндё конкыптам еюж. Ножтрюд жанктюч ывыртятюр ты вяш, но примич промпта пэрчыквюэрёж дуо. Выро мютат омнэжквюы ыам эю.
\ No newline at end of file
diff --git a/test/resources/tokenization/top_visited_domains.txt b/test/resources/tokenization/top_visited_domains.txt
new file mode 100644
index 0000000..0238c36
--- /dev/null
+++ b/test/resources/tokenization/top_visited_domains.txt
@@ -0,0 +1,3 @@
+google.com facebook.com youtube.com yahoo.com baidu.com amazon.com wikipedia.org taobao.com twitter.com Qq.com google.co.in apple.com
+
+http://www.alexa.com/topsites
\ No newline at end of file
diff --git a/test/resources/tokenization/world_cities_a.csv b/test/resources/tokenization/world_cities_a.csv
new file mode 100644
index 0000000..1bb7121
--- /dev/null
+++ b/test/resources/tokenization/world_cities_a.csv
@@ -0,0 +1 @@
+aaba,aaba kebire,aaba serhire,aabaan weyn,aabaan yare,aabarak,aabasen,aabasene,aabassiye,aabateine,aabauerschaft,aabayat,aabb ej jinnate,aabb el khazne,aabba,aabbade,aabbassiye,aabboud,aabboudiye,aabd ech cheikh,aabd el fattah,aabdekliye,aabdet biche,aabdille,aabdilli,aabdine,aabdou,aabeidat,aabeidate,aabenraa,aaber beit sif,aabey,aabjaerg,aabjerg,aabjorgan,aabla,aable,aabo,aabol,aabom,aaboud,aabow,aabra,aabreita,aabreite,aabrine,aabtine,aaby,aabybro,aabyhoj,aabyskov,aach,aach-linz,aachach,aacharheu tachleuk,aacharne,aachchet charqiye,aache,aachen,aachen-rothe erde,aacheq omar,aachini,aachiran,aachour,aachqane,aachqout,aachqoute,aad la macta,aada,aadal,aadalsbruk,aadami,aadan harmey,aadan nuur,aadan waraabow,aadan yabaal,aadaouane,aadassiye,aadbel,aadchit,aadchit el qoussair,aadchite,aaddaye,aaderup,aadi noia,aadland,aadliye,aadloun,aadma,aadma asundus,aadmoun,aadneram,aadneskaar,aadorf,aadorp,aadouane,aadoui,aadouiye,aadouss,aadra,aadzi,aaen,aaenget,aaerne,aafarnes,aafoss,aafrine,aafrite,aafsadiq,aafsdiq,aafsouniye,aag,aagaard,aageby,aagedal,aagerup,aagoubt,aagskardet,aagskaret,aagtdorn,aagtdorp,aagtekerke,aaguil,aahire,aaichiye,aaidamoun,aaien,aaigem,aaiha,aaijen,aailiyine,aaimar,aaintoura,aaintourine,aaita,aaita el foukhar,aaita ez zott,aaitanit,aaitaniya,aaium,aaiun,aaiyat,aajaja,aajaltoun,aajami,aajeilate,aajeltoun,aajjaz,aajjel,aajjoura,aajpur,aajuolahti,aakaa,aakae,aakaer,aakakir,aakar,aakaru,aakb el bellene,aaker,aakeroen,aakerviken,aakfor,aakiba,aakil,aakip,aakirkeby,aakjaer,aakkar el aatiqa,aakkari,aakkour,aakla,aaklangberget,aaknes,aakobar,aakoinen,aakra,aakrak,aakre,aakre asundus,aakrehamn,aakvaag,aakvik,aakyor,aal,aala,aalalech,aalamdar,aalampur,aalanden,aalane,aalanganjar,aalatsivik,aalbaek,aalbeek,aalbeke,aalborg,aalbroek,aalbu,aalburg,aalden,aaldering,aalderink,aaldonk,aale,aalebaek,aalebaek huse,aalefjaer,aalen,aaleskov,aalestrup,aalesund,aaley,aalfang,aalfoten,aalgaard,aalgana,aali badrane,aali bajliye,aali enn nahri,aali farro,aali koz,aali qorane,aalia,aalidji,aalio,aalissa,aalita,aaliyate,aaliye,aaljoki,aalkaer,aalkorb,aallaakh,aallani,aalling,aalma,aalma ech chaab,aalmane,aalmane ed daiaa,aalmate,aalmine,aalmo,aalo,aalokke,aalose,aaloua,aalouannk,aalougoum,aalouine,aalouk,aalouq charqi,aalouq rharbi,aalqine,aalsbo,aalset,aalsgaard,aalsgaarde,aalsmeer,aalsmeerderbrug,aalso,aalsrode,aalst,aalst sint-pieter,aalst-saint pierre,aalstad,aalstrup,aalsum,aaltebei,aalten,aalter,aalterbei,aalterburg,aalum,aalvik,aalykke,aalzum,aam,aam tam,aamair el buied,aamar,aamar el beikate,aamar el bikat,aamara,aamaret el bikat,aamarne,aamarne faouqani,aamarne tahtani,aamatour,aamayer,aamba,aamchite,aamdal,aamdals vaerk,aamelfot,aamer,aaminaay,aamire,aamland,aamli,aammariye,aammatour,aammiq,aammoun,aammourine,aamnes,aamot,aamot-modum,aamoud,aamouda,aamoudi,aamoudiye,aamoyhamn,aamqa,aamqiye,aamra,aamret el freij,aamriye,aamse,aamsi,aamsosen,aamzit,aan,aan t holven,aan t hoolven,aan de berg,aan de bergen,aan de bunders,aan de doorns,aan de kippen,aan de luyssen,aan de maas,aan de popelaar,aan de school,aan de steenoven,aan de vreenen,aan de vrenen,aan de wolfsberg,aan de zuwe,aan den nieuwen vijver,aan den steenoven,aan het broek,aan reijans,aana,aana-sira,aanaar,aanadane,aanadane ech chih,aanane,aananib,aanaye,aanaz,aanbal,aancud,aandal,aandalen,aandara,aandenk,aandepoppelaar,aandervaag,aandqet,aandu,aaneby,aanekoski,aaneland,aaneq al haoua,aaneq baiarra,aaneq el haoua,aanerne,aanes,aangel,aangenaam,aangewysd,aangstad,aanhimaki,aanhou,aanislag,aaniss,aanjar,aankibane,aankomst,aannabe,aannabiye,aannaqiyet miri,aannaqiyet tchiftlik,aannaya,aannaze,aannbal,aanndaqatt,aannerud,aannestad,aannjara,aannkaoui,aannkibane,aannouz,aanntariye,aannzaouiye,aanosusura,aanout,aanoute,aanqoun,aanschot,aanshu,aanstad,aansvik,aantaara,aantoura,aanum,aanwas,aaouachiye,aaouadi,aaouainat,aaouaini,aaouainiye,aaouaj,aaouamiye,aaouar,aaouara,aaouas,aaouass,aaoudet el qalamoun,aaoueichiye,aaoueidja,aaoueijet jine,aaoueiline,aaoueina,aaoueinate,aaoueinate kebire,aaoueinate serhire,aaoueine,aaoueinet er rihane,aaoueini,aaoueira,aaoueira charqiye,aaoueisse,aaouenia,aaouifate,aaouint--maazouza,aaouj ej jenah,aaouj el jenah,aaouje,aaoukar,aaoumar ed dine,aaouniye,aaoura,aaousiene,aapaasdal,aapaasland,aapajarven asutusalue,aapajarvi,aapajoki,aapdraai,aappilattoq,aappilattorq,aapua,aaqabet hairouna,aaqareb es safi,aaqarib es safi,aaqb el ballane,aaqbet roummane,aaqbet sebaail,aaqeibe,aaqerbate,aaqerbe,aaqiba,aaqile,aaqlain,aaqliya,aaqliye,aaqmata,aaqoula,aaqoulet el kharraj,aaqoura,aaqraba,aaqrabate,aaqrabiye,aaqrabouz,aaqrebiye,aaqtanit,aaqtanite,aar youhanna maroun,aara ech chemaliye,aara el qibliye,aarab aaze,aarab aazze,aarab chah,aarab ej jall,aarab ej jheich,aarab el lizzab,aarab el motilk,aarab el moulk,aarab el moulk babou,aarab el moulk badou,aarab hassan kebir,aarab hassane kebir,aarab hassane serhir,aarab jall,aarab kennd,aarab khane,aarab oucharhi,aarab peunar,aarab salim,aarab tabbaya,aarab tcheurdouk,aarab tchourdek,aarab virane,aarabene,aarabrot,aarade,aarade kebir,aarade kebire,aaradiye,aaraeangol,aarafite,aaraimich,aaraiya,aarak,aaraksbo,aaramo,aaramoun,aaramour,aaramta,aaramte,aarane,aararsate,aararukan,aarau,aaray,aarbane,aarbaniye,aarberg,aarbet hlali,aarbet qouzhaiya,aarbid,aarbid faouqani,aarbid jdaide,aarbid tahtani,aarbil,aarbjaerg,aarbjerg,aarbogen,aarbossiesplaat,aarbouch,aarburg,aarby,aarchani faouqani,aarchani tahtani,aarchoune,aard,aard en hemelrijksche heide,aard en hemelrijkseheide,aarda,aardal,aardam,aardane abou sahne,aardappelhoek,aardat,aardenburg,aardene,aardenhout,aardenweg,aardla,aardswoud,aardt,aareavaara,aareibe,aareikib,aarenurga,aarepere,aarestad,aarestrup,aarevik,aarfa,aarfor,aargaard,aargab,aargasi,aargub,aarheim,aarhejm,aarhob,aarhogen,aarhus,aarich,aarid,aarid en naas,aarida,aarida aajil,aaridet aboulli,aaridiye,aarikkala,aarite,aariza,aarjess,aarjoun,aarka,aarkob,aarlahti,aarland,aarlanderveen,aarle,aarlen,aarlifoss,aarlose,aarmoen,aarmoun,aarna,aarne,aarnes,aaro,aaro by,aarobugt,aaron,aaron castellanos,aarons,aarons corner,aarons creek,aaronsburg,aaros,aarosund,aarouda kebire,aarouda serhire,aaroun,aarour,aarous,aarqa,aarqaya,aarqoub,aarra el qibliye,aarre,aars,aars houw,aarsal,aarsand,aarsballe,aarsby,aarschot,aarsdale,aarsele,aarset,aarsland,aarslev,aarstad,aarstein,aart,aartez,aartouz,aartrijke,aartselaar,aartshouw,aartswoud,aartuka,aartukka,aartwoud,aarum,aarup,aarvaag,aarvik,aarviksand,aarwangen,aas,aasa,aasalien,aasan,aasane,aasanvangene,aasaroy,aasaunet,aasberg,aasbo,aasbogrend,aasbuttel,aase,aasebro,aasebro huse,aasegg,aasel,aasen,aasendrup,aaseral,aaserod,aaserum,aaset,aasfontein,aasfouriye,aasgaard,aasgaardstrand,aasgardann,aashaug,aashausen,aasheim,aashoj,aashoje,aashqoute,aasiaat,aaside,aasivik,aasjorden,aasla,aasland,aaslandsgrend,aasleagh,aasmae,aasmiye,aasmulen,aasnaes,aasnan,aasnes,aasni,aaso,aasowao,aaspe,aaspenaes,aaspere,aaspere asundus,aassa tagmout,aassaibe,aassaile,aassaimont,aassal el ouard,aassaliye,aassaltye,aassane,aassaouss,aassara,aasseibe,aasseiliye,aassem,aassera,aasskard,aassoun,aassous,aassouss,aassun,aassveet,aast,aasted,aasted bro,aasterberg,aasterein,aastofte,aastorp,aastrup,aastveit,aastvejt,aasu,aasukalda,aasum,aasumetsa,aasuvalja,aasvik,aasvoelkop,aasvogelboom,aat,aatamanine,aatangen,aataouiye,aataouiye miri,aatbe,aatchane,aatchane charqiye,aatchane el homr,aatchane el miri,aatchane rharbiye,aateibe,aateichane,aathal,aatib,aatil,aatit,aatli,aatne,aato,aatoila,aatola,aatra aattire,aatra attire,aatrine,aatsi,aatsinki,aatsy,aauamara,aauoaj el falaj,aavasaksa,aavasra,aaved,aavere,aavesland,aavik,aaviku,aavre,aavroun,aawla,aaxanen,aaya,aayach,aayachiye,aayad el kebir,aayate,aaygayon,aayolay,aayongu,aayonso ndoor,aayoun arrhouch,aayoun ech chaara,aayoun el ghezlane,aayoun el ouadi,aayoun el rhozlane,aayoun hadid,aayoun hamoud,aayoun orghouch,aaysharud,aayty-mokhk,aayty-mokhn,aaz el aarab,aaz el arab,aazamiye charqiye,aazamiye sharqiyah,aazanen,aazaz,aazeir,aazeire,aazi shamsuddin,aazib el caid zaiadi,aazibiett tahta,aazize,aaziziyate,aaziziye,aazour,aazqai,aazqe,aazr kfartai,aazriye,aazzam,aazze,aazzeite,aazziye,aa\302\277arsin,aa\302\277ax oba,aa\302\277hoj-martan,ab,ab `araq,ab `aref,ab agaride,ab ajaj,ab alvan,ab ambar,ab amdourou,ab amiri,ab amre,ab amsouar,ab anar,ab anar-e asiki,ab anarak,ab anari,ab anbar,ab anbar-e kan sorkh,ab anbar-e pain,ab anbarkan-e sorkh,ab angur,ab anjir,ab anjir va bas kutuk,ab anjir-e `olya,ab anjir-e bala,ab anjir-e pain,ab anjir-e sofla,ab anjirak,ab anrafa,ab asemani,ab ask,ab ayyub,ab azba,ab bad,ab bad-e chehel tan,ab bad-e do,ab bad-e goruh,ab bad-e pedari,ab bad-e peders,ab bad-e qahremani,ab bad-e sivandi,ab badi,ab bagh,ab bahar,ab bahar-e 1,ab bahar-e 2,ab bahar-e do,ab bahar-e yek,ab bahareh,ab bahri,ab bakhsh,ab bakhshan,ab bala,ab bali,ab band,ab band matkat,ab bandan kash,ab bandan sar,ab bar,ab baran,ab baran-e yek,ab bareh,ab barg,ab barik,ab barik `olya,ab barik bala,ab barik pain,ab barik siah kamar,ab barik sofla,ab barik-e `olya,ab barik-e bala,ab barik-e pain,ab barik-e pa`in,ab barik-e seyah kamar,ab barik-e sofla,ab barik-e vasat,ab barik-e vosta,ab bariki,ab bazan,ab bazha-ye helmand,ab bazha-ye hilmand,ab bazha-ye nahr-e saraj,ab behan,ab behareh,ab beruy,ab bid,ab bid `ali,ab bid dasht,ab bid-e `alibaz,ab bid-e `olya,ab bid-e bajgah,ab bid-e bavan,ab bid-e bovan,ab bid-e galleh tavak,ab bid-e haji baba,ab bid-e hajj baba,ab bid-e hajji baba,ab bid-e lalah,ab bid-e sofla,ab bidak,ab bidi,ab bidui,ab bilukhar,ab boda,ab boneh,ab bordeh,ab bui,ab burdah,ab burdeh,ab carmi,ab cenar,ab chahru,ab chak,ab chal,ab charami,ab charganie,ab charian,ab charmi,ab charu,ab chashmeh,ab cheikh,ab chekan,ab cheku,ab chenar,ab chenar-e `olya,ab chenar-e sofla,ab chenar-e zilai,ab chenaru,ab chendar,ab chendar-e gelal,ab chendaran,ab cheri,ab cheshmeh,ab chinar,ab chirak,ab chol,ab chubeh,ab chur,ab chuya,ab damil,ab dan,ab danah,ab danak,ab danbal,ab dang sar,ab dar,ab dara,ab darah,ab daraz,ab darisat,ab darrah,ab darreh,ab darreh-ye abu ol qasem,ab darreh-ye abul qasem,ab darreh-ye tazeh,ab darusheh,ab darwayshan,ab deh vis,ab dehnow,ab delena,ab deraz,ab deshkun,ab difeh,ab dinabad,ab divaneh,ab djidat,ab djimie,ab djoudoul,ab djouet,ab djoulou,ab dokan,ab donbal,ab donu,ab dorot,ab dorushak,ab dukan,ab dulan,ab duli,ab duziyeh,ab duzuyeh,ab fariab,ab fayzab,ab feros,ab ferush,ab firosh,ab gabara,ab gach,ab gaj,ab gallehdar,ab galu,ab galur,ab gandeh,ab gandoo,ab gandow,ab gandu,ab ganeh,ab ganji,ab gara,ab gard,ab gardesh,ab garib,ab garm,ab garm larijan-e bala,ab garm nazdike bahram abad,ab garm-e bala,ab garm-e bozorg,ab garm-e genow,ab garm-e genu,ab garm-e pain,ab garm-e seyyedi,ab garm-e-larijan,ab garmak,ab garmak-e `olya,ab garmak-e bala,ab garmak-e miani,ab garmak-e pain,ab garmak-e pa`in,ab garmak-e sofla,ab garmak-e vosta,ab garmeh,ab garmeh-ye bar aftab,ab garmeh-ye nesar,ab garmi,ab garmu,ab garmu-ye bala,ab garmu-ye pain,ab gavan-e bozorg,ab gavan-e kuchek,ab gaz,ab gazan,ab gazan paku,ab gel,ab gerd,ab gerdu,ab geru,ab gineh,ab gir,ab gishu,ab gol,ab gonjeshgan,ab gonjeshk,ab gonjeshkan,ab gonjeshki,ab gonji,ab gorazi,ab gorg,ab gorg-e `olya,ab gorg-e bala,ab gorg-e pain,ab gorg-e sofla,ab goshneh,ab gouka,ab gozan paku,ab gozar,ab guzar,ab hadir,ab hazar helleh,ab hendi,ab hendoo,ab hendu,ab hodja,ab hoseyn,ab idimi,ab jahan,ab jalil,ab jarad,ab jari,ab javan,ab javar,ab jazi,ab jub,ab kabkun-e harbu,ab kahur,ab kahurak,ab kalleh sar-e bozorg,ab kalleh sar-e kuchak,ab kaloo,ab kameh,ab kamun,ab kanaru,ab kanarun,ab kandeh,ab kandu,ab kaneh,ab kanfouta,ab karreh,ab kaseh,ab kash,ab kashan,ab kasseh,ab katala,ab katan,ab kel,ab kenar,ab kenareh,ab kenaru,ab kenarun,ab kenazak-e shahpuri,ab keneh,ab kettleby,ab khanah,ab khaneh,ab khaneh-ye chinzai,ab khar zahreh,ab kharak,ab khareh,ab kharmu,ab kharreh,ab kharzahreh,ab kharzma,ab khel,ab khigan,ab khodai,ab khonak,ab khor,ab khorak,ab khoreh,ab khowrak,ab khudai,ab khuda`i,ab khugan,ab khurak,ab khvareh,ab khvor,ab khvorak,ab khvorak-e bala,ab khvorak-e pain,ab khvord,ab khvoreh,ab khwarah,ab koabib,ab kolok,ab kolu,ab konar,ab konaru,ab koreh,ab kori,ab koruiyeh,ab kot-e `olya,ab kot-e bala,ab kot-e pain,ab kot-e sofla,ab koura,ab krime,ab kuiyeh,ab kuluk,ab kuri,ab la,ab lami,ab laoueni,ab laoune,ab lar,ab laran,ab larun,ab lashgar,ab lashgar `olya,ab lashgar sofla,ab lashgar-e `olya,ab lashgar-e sofla,ab lashgar-e vosta,ab lashkar-e `olya,ab lashkar-e sofla,ab lench,ab lishu,ab lulaman,ab maik-e golzari,ab maik-e sar bisheh,ab ma`mun,ab mahamat sawa,ab mahi,ab mahmak,ab makkai,ab mal,ab malakh,ab malay,ab manaz,ab mar,ab mar doua,ab marun,ab mayak,ab mayek,ab mazar,ab mehnat,ab meshkin,ab mik,ab mil,ab mishan,ab mohammadi,ab moord,ab morad,ab morvarid,ab morvarid sofla,ab morvarid-e bardangan,ab mow,ab mowrd,ab mu-ye `olya,ab muhu,ab murd,ab musa,ab nai,ab nadideh,ab namel,ab nar,ab naray,ab nari,ab nars,ab naru,ab ney,ab neyeh,ab neyh,ab nik,ab nikuiyeh,ab nil,ab niyeh,ab pain,ab pa-ye arghun,ab pakhsh,ab pakhshan,ab pakhshkon,ab pakhshun,ab pangooeyeh,ab panguyeh,ab papa,ab paran,ab pardeh,ab parin,ab pay,ab pedeh,ab pish,ab qal`eh,ab qalamu,ab qalamun,ab qanat,ab qandu,ab qar,ab qebleh,ab qedom,ab qeh,ab qol,ab qowl,ab ragna,ab rah,ab rahmat,ab rajuneh,ab rang,ab ranga,ab rania,ab rash,ab raygak,ab rayzi,ab razak,ab razgeh,ab reza,ab rigun,ab riz,ab rizak,ab rizi,ab rokhan,ab row,ab rowshan,ab rudak,ab saba,ab sabe,ab sabz,ab safid,ab sakhak,ab sang,ab sangab,ab sar-e dehu,ab saraft,ab sard,ab sard-e dad golab,ab sard-e sarab,ab sardab,ab sardan,ab sardan-e `olya,ab sardan-e sofla,ab sardan-e vosta,ab sardeh,ab sardeh-ye alai,ab sardeh-ye seh sok,ab sardeh-ye sofla,ab sardow,ab sardu,ab sarduiyeh,ab sarduiyeh-ye jamal,ab sarina,ab sarinah,ab sarineh,ab sefid,ab sel,ab sepa,ab seyl,ab seylai,ab shahr,ab shakhak,ab shambal,ab shegerd,ab shekar,ab sheki,ab sheykh,ab shib,ab shirin,ab shirin-e do,ab shirin-e yek,ab shooli,ab shoor,ab shorah,ab shuli,ab shur,ab shuran,ab shureh,ab shuri,ab shurk,ab shuru,ab shuru morghak,ab shuruiyeh,ab silay,ab sora,ab sorkh,ab sulakh,ab surakh,ab tahlun,ab takhtak,ab takhtan,ab talayeh,ab talkh,ab talkhuyeh,ab taq,ab tar,ab tarikan,ab tarikan-e bala,ab tarikan-e pain,ab tasuleh,ab tavil,ab tchetchenia,ab ti,ab tibne,ab torsh,ab touyour,ab tut,ab vanan,ab vojdan,ab yad-e badri,ab yad-e qahremani,ab yeki,ab yokho,ab za,ab zad,ab zahar,ab zahleh,ab zahleh-ye anaraki,ab zahli,ab zahreh,ab zali,ab zaloo jahangiri,ab zalu,ab zalu-ye `arab,ab zalu-ye `olya,ab zalu-ye aqa bahram,ab zalu-ye bahram,ab zalu-ye bala,ab zalu-ye miani,ab zalu-ye pain,ab zalu-ye sofla,ab zalu-ye vosta,ab zamam,ab zamina,ab zaminoo,ab zaminu,ab zangi,ab zar,ab zard,ab zardalu,ab zargeh,ab zarkeh,ab zehlu,ab zerafa,ab zeytuiyeh,ab zobani,ab zumc,ab zumch,abar as sababil,abya,abya-paluoya,ab-ad-dud,ab-bala,ab-e `aref,ab-e `arif,ab-e ahmad,ab-e anar,ab-e anarak,ab-e anari,ab-e anjirak,ab-e anjirak-e `olya,ab-e azan,ab-e bad,ab-e bahar,ab-e bahri,ab-e bakhsh-e `olya,ab-e bakhsh-e bala,ab-e bala,ab-e balut,ab-e barik,ab-e barik-e ghalmani,ab-e barik-e qowdi,ab-e barik-e qudi,ab-e bariki,ab-e baydak,ab-e beyglu,ab-e bid,ab-e bon bid,ab-e bunbid,ab-e buneh,ab-e chahru,ab-e dangeh sar,ab-e darah,ab-e dasht,ab-e denow,ab-e deraz,ab-e dorusheh,ab-e faizab,ab-e feyzab,ab-e gandu,ab-e garm,ab-e garm bala,ab-e garm pain,ab-e garm-e bala,ab-e garm-e larijan-e pa`in,ab-e garm-e pain,ab-e garmak,ab-e garmu,ab-e gelur,ab-e gheyrub,ab-e goli-ye kak,ab-e gonji,ab-e gowdbon gar,ab-e hendi,ab-e jahan,ab-e javan,ab-e kahurak,ab-e kamari,ab-e khan,ab-e khanah,ab-e kharzaleh,ab-e kheyl,ab-e konar,ab-e lah,ab-e lashkar,ab-e ma`dani-ye ganji,ab-e mahi,ab-e meshgin,ab-e mosla,ab-e nahriz,ab-e narak,ab-e narow,ab-e nikuiyeh,ab-e nil,ab-e now,ab-e qalandaran,ab-e qalat,ab-e razgeh,ab-e rigun,ab-e rokhan,ab-e safayd,ab-e sahan,ab-e sang ab,ab-e sard,ab-e sard-e now,ab-e sefid,ab-e shah,ab-e shahr,ab-e shayr ahmad,ab-e sheykh,ab-e shir ahmad,ab-e shirin,ab-e shirinak-e pain,ab-e shor,ab-e shur,ab-e tal,ab-e tall,ab-e torsh,ab-e tutarehi,ab-e zari,ab-e-baran,ab-e-sher-amad,ab-e-shor,ab-e-takrim,ab-espid,ab-i bark-i qudi,ab-i- garmak,ab-i- gum kili,ab-i- sar,ab-i-aref,ab-i-azan,ab-i-barik,ab-i-barik-i-ghalmani,ab-i-barik-i-qudi,ab-i-bariki,ab-i-barir,ab-i-bid,ab-i-bunbid,ab-i-dalar,ab-i-dinabad,ab-i-faydzab,ab-i-gandeh,ab-i-garm,ab-i-garmak,ab-i-ghairub,ab-i-gharmak,ab-i-gul,ab-i-gunji,ab-i-gunjishk,ab-i-gurg,ab-i-hang,ab-i-hindi,ab-i-janlu,ab-i-kamari,ab-i-kinar,ab-i-kuri,ab-i-kut,ab-i-lulchman,ab-i-murad,ab-i-mushkin,ab-i-nil,ab-i-nil bazar,ab-i-pina,ab-i-safed,ab-i-sard,ab-i-ser ahmad,ab-i-shahi,ab-i-shiran,ab-i-shirin,ab-i-sor,ab-i-takrim,ab-i-tal,ab-i-zaman,ab-kinar,ab-kuh,ab-ol-abbas,ab-ol-fares,ab-paran,ab`adiyat ar rawdah,ab`adiyat damanhur,ab`adiyat darawah,ab`adiyet damanhur,ab`adiyet el-roda,aba,aba abdella,aba ad dud,aba ada,aba adefoju,aba adi,aba al hiran,aba al kibash,aba al waqf,aba ala,aba ali,aba ar ruwath,aba as su`ud,aba bakar,aba bale,aba barik,aba biruk,aba bona,aba bora ager,aba boru,aba buko,aba bula,aba chorga,aba ciotte,aba cuturia,aba da serra,aba ekpe,aba el-waqf,aba epo,aba er muchang,aba genda,aba gerima,aba goderi,aba golja,aba golja ager,aba guyo ana,aba hillel,aba i,aba ibadan,aba ige,aba ijesha,aba ilowa,aba isale,aba kundi,aba khalli,aba khel,aba kibe,aba ladunni,aba libanos,aba mansuri,aba mekereyta,aba mesk,aba milik,aba nkomini,aba nla,aba oba,aba odan,aba ode,aba odedele,aba odo,aba ogo,aba oja,aba oje,aba oke,aba oku,aba ola,aba olobe,aba olode,aba osa,aba otun,aba panu,aba sa`ud,aba saimal,aba samuel,aba san muchang,aba selam,aba selama,aba shahid,aba simal,aba sina,aba solomon,aba tekle,aba titun,aba tuntun,aba ukpo,aba wani,aba wat,aba wibe ager,aba yawu,aba yonis,aba zuluf,aba-isu,aba-kurban,aba-kuye,aba-ne-e,aba-onigbagbo,aba-oriyanrin,aba-oriyanrn,aba-teacher,aba-wa,abaa,abaachen,abaadze,abaajajane,abaaji,abaal,abaam,abaamang,abaamo,abaar yoma,abaarey,abaarso,abaasa,abaase,abaati,abaayle,abaayow,abaazhvakhu,abab,ababa,abababubu,ababaki,ababakrai,ababakri,ababarrai,ababayte,ababda,ababe,ababelleh,ababelli,ababeng,ababessa,ababi,ababiadi,ababiddi,ababigab,ababil,ababilah,ababin,ababio,ababita,ababki,ababkovo,abablar,ababoko,ababot,ababscia,ababu,ababuj,ababuom,ababurovo,ababut,abac gurei,abaca,abacai,abacao,abacar,abacas,abacate,abacateiro,abacaxi,abacaxis,abacaxy,abacca,abaccan,abach,abachanan,abachanan proper,abachariri,abache,abacheke,abacher,abacheri,abachi,abachkou,abachta,abaci,abaciguzeli,abacija,abacilar,abaciliguzeli,abacioglu,abacjanan,aback,abacka,abacken,abaclia,abaco,abacou,abacourt,abad,abad chandipur,abad chehel tan,abad cheshmeh,abad goth,abad illahi,abad jagir,abad khel,abad khoreh,abad khorreh,abad khurreh,abad kureh,abad mahalleh,abad mangeli,abad mehlani,abad meshk,abad nawa,abad pancho,abad purana,abad shah,abad shahpur,abad shapur,abad sid,abad soleyman,abad-e chehel tan,abad-e soleyman,abada,abada goungou,abadabu,abadai,abadaki,abadam,abadan,abadan tappeh,abadani,abadaniye,abadawa,abadchi,abadchi-ye bala,abadch")ye pain,abaddou,abade,abade de neiva,abade de vermoim,abadeh,abadeh abgarm,abadeh-i-tashk,abadeh-ye morshedi,abadeh-ye tashk,abadengo de torio,abades,abadesa,abadessa,abadgarh,abadgiran,abadi,abadi 1,abadi 2,abadi churera,abadi dhuddianwali,abadi dua,abadi gulmil,abadi jangal,abadi kacha qila,abadi mullah hasan,abadi nur muhammad,abadi rai ganga ram,abadi rajgir,abadi ret,abadi satu,abadi seyyed,abadi shah maki sharif,abadi shahamad khan,abadi umar,abadi-ye hajj mohammad qoli,abadi-ye morad,abadi-ye nemuneh,abadi-ye pa godaru,abadi-ye tashk,abadia,abadia a isola,abadia de pitangui,abadia do pitangui,abadia dos dourados,abadiama,abadian,abadiania,abadiano,abadiano celayeta,abadigbene,abadikiri,abadila,abadilahi,abadilla,abadim,abadin,abadir,abadise,abadiya,abadiyah,abadiye,abadiyeh,abadiyeh nemuneh,abadiyeh-e nomuneh,abadji,abadji doume,abadji koute,abadjin-doume,abadjin-koute,abadkend,abadla,abado,abadogo,abadon,abadou,abadou arg,abadpur,abadpura,abadseyyed,abadszalok,abadu,abadye,abadyl,abadzekhskaya,abadzhigyuzeli,abadzi,abadzici,abae,abaek,abaer,abaer kirkja,abaete,abaetetuba,abaetezinho,abaezi,abafaja,abafalva,abafon,abafu,abafum,abafun,abag dheere,abag qi,abaga,abaga peitzufu,abaga qi,abagada,abagai,abagaitui,abagalak,abagamia,abagana,abaganya,abagao,abagatanen,abagatchi,abagato,abagatui,abagatuy,abagaytuy,abagbene,abagbo,abagennen,abagh,abaghennen,abaghimbi,abagi,abagide,abaglu,abagnam,abagnar qi,abagnivegoui,abago,abagolja ager,abagon,abagoure,abagu,abagur,abagurovskaya ploshchadka,abagurt,abagwu,abah,abah kah,abah timman,abahala,abahalle,abahar,abaher,abahiatene,abahid,abahun,abahwange,abai,abaiara,abaiat,abaiba,abaibani,abaidlat,abaigar,abailah,abailenla,abailenle,abailfnla,abailovacik,abaimovo,abaine,abainou,abainville,abaipur,abaira,abaisiat,abaitinga,abaiwala,abaj,abaja,abaja owi,abaja-awwa,abaja-ngwo,abaja-owa,abajai,abajalu-ye `olya,abajalu-ye masihi,abajalu-ye sofla,abajanlu,abajarang,abajas,abaji,abaji creek,abaji okolo,abajikolo,abajikolokiri,abajin,abajo,abajo bonito,abajo caballero,abajo guauci,abajo higueron,abak,abak ifia,abak isiet,abak oko,abak ukpum,abak-ibiaku uruan,abaka,abakada,abakae,abakai,abakakoua,abakaliki,abakan,abakanets,abakano,abakano-perevoz,abakanovo,abakanr,abakanskoye,abakanu,abakanunkun,abakany,abakar,abakar karoua,abakari karoua,abakay,abake camp,abakeh,abakhana,abakheyl,abakheyl,abakhshian,abakhshikha,abakht,abakhuamtsa,abakhuatsa,abakhvamtsa,abaki,abakire,abakirovo,abakle,abakliya,abaklu,abakly,abaklydzhaba,abaklytama,abako,abakodu,abakolawewa,abakolu,abakomerri,abakongo,abakonova,abakonovo,abakor cadaawe,abakouande,abakoum,abakoumbou i,abakoumbou ii,abakpa,abakrampa,abakro,abaksha,abakshata,abakshina,abakshino,abaku,abakuasimbo,abakuba,abakukope,abakula,abakule,abakum,abakumikha,abakumlevo,abakumovka,abakumovo,abakumovskaya,abakumovskiy,abakumovskoye,abakumtsevo,abakumy,abakunkam,abakunovo,abakuru,abakwa,abakwam,abakwasimbo,abal,abal dud,abala,abala-ndolo,abalabi,abalach,abaladougou kenieba,abalaga,abalagiagbene,abalagn,abalagne,abalago,abalah,abalai,abalak,abalaka,abalakh,abalakha,abalakiri,abalakli,abalakope,abalakova,abalakovo,abalakskoye,abalala,abalam,abalama,abalang,abalar,abalcisqueta,abalduyevka,abale,abalebo,abaleinie,abaler,abaleri,abalessa,abalete,abaleygne,abalga,abali,abalia,abaliget,abalikha,abaling,abaliwala,abalkweri,abalkweru,aballa,aballe,aballi,aballoo,abalmasov,abalo,abalo kakaou,abalo kpele,abalo meda,abalodyang,abalok,abalokope,abalolo,abalos,abalou-kakaou,abalsha,abalti,abalu,abam,abam ama,abama,abamachi,abamate,abamba,abambres,abame,abamelikovo,abamensam,abamesidia,abamia,abamkrom,abamkurom,abamkwam,abamkwanmu,abamza,aban,aban chak,aban oban,abana,abanaban,abanabe,abanades,abanaka,abanakope,abanase,abanasi,abanat,abanay,abanayop,abancai,abancay,abancena,abanco,abancourt,aband,abanda,abandak,abandames,abandan,abandan sar,abandanak,abandiano celayeta,abandonado,abandonne,abandou,abane,abanengo,abanengui,abanfang,abang,abang i,abang ii,abang iii,abang kaler,abang mindi,abang-abang,abang-ayo,abang-melem,abang-minko,abang-si,abanga,abangan,abangan norte,abangan sur,abangares,abangaritos,abangay,abangel,abangnang,abango,abangok,abangsil,abangu,abani,abania,abanico,abanie birgit,abaniella,abanikanda,abanilla,abanillas,abanin,abanino,abanisco,abankang,abankima,abanko,abankrampa,abankrom,abankurom,abankwa,abankwoni,abanla,abanliko,abanna kondre,abano,abano terme,abanoa,abanon,abanon sur,abanos,abanosevo,abanosimovka,abanoyeti,abanqueiro,abansua panyin,abantagari,abanto,abantoken,abantro,abanuange,abanusi,abanuwange,abanwan,abanyaso,abanyasu mile 14,abao,abaocun,abaodo,abaoji-doume,abaokoro,abaoso,abaoua,abaoyo,abap,abapa,abapan,abapi,abapia,abapo,abapuszta,abaq dheere,abaq mudule,abaqaiq,abaqsaarre,abaquid,abar,abar 1,abar 2,abar al `umi,abar al mawahib,abar deh-e bala,abar deh-e pain,abar dezh,abar el-shifaiya,abar koi,abar kuh,abar primero,abar qu,abar segundo,abar-ossimilli,abara,abara dugu,abara osimili,abara uno,abarabashi,abarabir,abarabofi,abaragba,abaragin khid,abaraha,abarahaiin hiid,abarahaiin hural,abarahaiin sume,abarahee at taybi,abarai,abarak-e bala,abarak-e pain,abaran,abaran verin,abarangda,abarange,abaranggang,abaranjeh,abarat,abarati,abarau,abarbar,abarca,abarchari,abarche,abarctie,abarda,abardarreh,abardeh,abardeh olya,abardeh-e sofla,abardeh-ye `olya,abardeh-ye bala,abardeh-ye pain,abardeje,abardezh,abardok,abardokh,abare,abaredda,abaredunga,abareh,abarekori,abarelelu,abaren,abaresh,abarey,abarezoma,abargale,abarge,abargh,abarghan,abarghoo,abarheh,abari,abaribara,abarigbo,abarigwe,abarijang,abarik,abarikori,abarikpo,abarilela,abarinki,abario,abariyang,abarjas,abarjes,abarjis,abarku,abarlaq-e `olya,abarlaq-e bala,abarlaq-e pain,abarlaq-e sofla,abarnau,abarnima,abaro,abaroa,abaroba,abaron,abarot,abarp,abarqan-e tus,abarquh,abarr,abarra,abarracamento,abarrada,abarriongan,abarrongan,abarsaj,abarsej,abarsh,abarsij,abaru,abarud,abaruni,abaruny,abary,abarzuza,abas,abas ar rajib,abas banda,abas chacra,abas khan kala,abas khel,abas koruna,abas river,abas-abad,abas-ali,abasa,abasabad,abasacapan,abasakur number 2,abasali,abasaly,abasan,abasan al kabir,abasan el-kabir,abasan merkezi,abasani,abasar,abasbeyli,abascal,abascha,abaschin,abase,abasfalva,abash,abasha,abashan,abasheres,abashevo,abashevskiy,abasheyevskiy,abashiri,abashishir,abashispiri,abashkin,abashkino,abasi,abasingammedda,abasir,abasiri,abaskand,abaskhankala,abaskhanly,abaskheyl,abaskoyu,abaskushta,abasnur,abasnurskiy,abasolo,abasolo del valle,abason,abasong,abasouni,abass at ta\302\261`ma,abassakope,abassi,abassir,abastumani,abastan,abastas,abastillas,abasto,abastouman,abastuman,abastuman akhaltsykhskogo,abastumani,abasua,abasuakuma,abasuapayin,abasukuma,abasulo,abat,abat isiet,abata,abata egba,abatabanga,abatait,abatame,abatamesu,abatan,abatange,abatao,abatar,abatbot,abatbuti,abatchor,abate,abate ager,abateberai,abatemarco,abater,abatete,abati,abati kope,abatia,abatie,abatimbo,abatimbo el gumas,abatiwa,abatoafo,abatobe,abatoharanana,abaton,abatoulilie,abatounbouliva,abatounbwabwa,abatoutou,abatouwai,abatsali,abatsambi,abatskiy,abatskoye,abatta,abattekh,abattoir,abattoirs,abatu,abatumesu,abaturki,abaturova,abaturovo,abatvenou,abatwain,abatyrtsy,abau,abauadala,abaucourt,abaucourt-les-souppleville,abauia,abauia 1,abauia 2,abaujalpar,abaujdevecser,abaujker,abaujlak,abaujsap,abaujszanto,abaujszolnok,abaujvar,abaul,abaulado,abaulskije,abaulskiye,abaura,abaurrea alta,abaurrea baja,abava,abava bijusas pusmuizas centrs,abaver,abaveysan,abavides,abavisan,abavo,abaw,abaw-ogugu,abawagada,abawagada 1,abawagada 2,abawagada 3,abawal,abawdiem,abawdikro,abawe,abawil,abawle,abawnse,abawo,abawso,abaxian muchang,abay,abay yilas,abay zinamba,abay-bazar,abaya,abayachi,abayan,abayansi,abayanzi,abayat,abayath,abaye,abaye atir,abaye atr,abayegan,abayel,abayelovacigi,abayeme,abayevo,abayi,abayi nchokoro,abayi ohanze,abayil,abayilovacik,abayisua,abaykan,abaykuchuk,abayli,abaynou,abayo,abayomi,abayon,abayong,abayono,abayro,abaysmail,abaytikau,abayu,abayuba,abaz,abaza,abaza-khabl,abazadag,abazai,abazai fort,abazaj,abazakt,abazalar,abazally,abazamehmetefendi,abazar,abazay,abazdagi,abaze,abazey,abazikope,abazinka,abazinovka,abazli,abazlu,abazo dheri,abazov,abazovka,abazu,abazzazine,abb tilo,abba,abba bor,abba bou,abba ciotte,abba foge,abba garima,abba gherima,abba gorda,abba hillel,abba isari,abba jabari,abba karima,abba khel,abba khelwala,abba kudi,abba kumbo,abba moti,abba nunami,abba omega,abba omege,abba samuel,abba timbo el-gumas,abba zege,abba-bogani,abbaakhayi-nakhri-saradzh,abbaayow,abbabis,abbach,abbachene,abbacialti,abbad,abbadia,abbadia a isola,abbadia di fiastra,abbadia di stura,abbadia dos dourados,abbadia lariana,abbadia san salvadore,abbadia san salvatore,abbadia stura,abbag,abbagao,abbah qusur,abbai,abbaia garo,abbajn,abbakhsh,abbakumova,abbakumovo,abbakumovskaya,abbakumovskoye,abbakumtsevo,abbal,abbal shahwala,abbal sumro,abbalpur,abban jo tar,abband,abbangaran,abbannaale,abbans-dessous,abbans-dessus,abbantorp,abbanuang,abbanuange,abbanwala toba,abbao,abbara,abbarange,abbare,abbaretz,abbari,abbarik,abbart,abbarten,abbas,abbas abad,abbas abad amini,abbas abad bahman,abbas abad hoomeh-ye zarand,abbas abad kahnooj,abbas abad kharaghan sharghi,abbas abad nahar khan,abbas abad ramjerd,abbas abbad,abbas al hamadi,abbas banda,abbas bar,abbas bel kassem,abbas dhok,abbas jhangi khelwala,abbas kalle,abbas karash,abbas khan,abbas khel,abbas khel raghzai,abbas khoso,abbas kili,abbas kirio,abbas lines,abbas machhi,abbas mallah,abbas nagarwala,abbas-touman,abbasa,abbasa banda,abbasabad,abbasabad sanabard,abbasabad-e `alaqeh band,abbasabad-i-qusheh,abbasally,abbasanta,abbasbayli,abbasbeyli,abbasen,abbasfevkani,abbasgol,abbasgolkoy,abbashalilpasa mandira,abbashalimpasa,abbashalimpasamandirasi,abbasharabasi,abbasi,abbasi banda,abbasi mahalla,abbasi nagar,abbasia el gharbia,abbasia el sharqia,abbasieh,abbasiwala,abbasiya,abbasiyah,abbasiye,abbaskhankala,abbaskhei samali,abbaskhel samali,abbaskheyl-dzhanubi,abbaskheyl-shamali,abbaskullar,abbaslar,abbasli,abbaslik,abbasly,abbasnagar,abbaspasa,abbaspur,abbasqulular,abbass-touman,abbassia gedida,abbassiye,abbasswala,abbastuman,abbaswala,abbasyanik,abbateggio,abbatuan,abbatunge,abbau maradtken,abbawala,abbawala chah,abbawusi,abbaye d aulne,abbaye dorval,abbaye de la melleray,abbaye de la trappe,abbaye-sous-plancy,abbaz,abbaza,abbazha-i-helmand,abbazha-i-nahre saraj,abbazha-ye helmand,abbazha-ye nahr-e sera,abbazha-ye nahr-e seraj,abbazia,abbazia di pomposa,abbazia di santa maria in selva,abbazia pisani,abbaziz,abbazkhayi-gilmand,abbazlik,abbe,abbe achi,abbech,abbecke,abbecourt,abbedak,abbedissa,abbedisso,abbeg,abbeg norte,abbeg sur,abbega,abbegaasterketting,abbehausen,abbehauser wehl,abbehausergroden,abbehauserwisch,abbeit,abbekae,abbekangnge,abbekas,abbekerk,abbekinderen,abbel,abbenaes,abbenans,abbenbroek,abbendorf,abbenes,abbenfleth,abbengawier,abbenhausen,abbenrode,abbensen,abbenseth,abbentheren,abberg,abberley,abberode,abberton,abbes,abbesbuttel,abbeshult,abbess roding,abbestede,abbetenenge,abbetorp,abbetved,abbevill,abbeville,abbeville-la-riviere,abbeville-les-conflans,abbeville-saint-lucien,abbevillers,abbeweer,abbewier,abbey,abbey dore,abbey mount,abbey saint bathans,abbey sur,abbey town,abbey village,abbey-cwmhir,abbeydorney,abbeyfeale,abbeylara,abbeyleix,abbeymahon,abbeyshrule,abbeyside,abbeytown,abbeyville,abbi,abbi addi,abbi adi,abbi ogri,abbia,abbianwala,abbiategrasso,abbiati,abbickenhausen,abbidak,abbie,abbig,abbigeri,abbigoy,abbij,abbikenhausen,abbingawier,abbingwehr,abbirim,abbo,abbo bougrima,abbo-bogrimo,abbobokazo,abbogol,abbontiakoon,abborkroken,abborraslatt,abborrberg,abborrberget,abborremala,abborrholmberget,abborrhult,abborrselet,abborrsjoknoppen,abborrtjarn,abborrtorp,abborrtrask,abbortrask,abbossongeng,abbot,abbot springs,abbot village,abbots cross roads,abbotpur,abbots ann,abbots bickington,abbots bromley,abbots langley,abbots lench,abbots ripton,abbots worthy,abbotsbury,abbotsford,abbotsford homes,abbotsham,abbotskerswell,abbotsley,abbotspoort,abbotstown,abbott,abbott run,abbottabad,abbottabad lines,abbotts,abbotts ann,abbotts mill,abbotts ripton,abbottsburg,abbottsford,abbottstown,abbottsville,abbou,abbou halqa,abboub,abboud,abbouda,abboudech,abboule arid,abbouli,abbouli arid,abbowa,abbreviation ala,abbs valley,abbtenau,abbu,abbuatan,abbuda,abbul,abbungpungeng,abbunonge,abburda,abbuwala,abby plantation,abbyville,abcakan,abchak,abchakan,abchang,abchekli,abchi,abchikile,abchikili,abchour,abchowr,abchu,abchuiyeh,abco,abcoude,abcoven,abd `ali mard,abd `alt,abd abul`ula shaikh latif,abd al husayn,abd al husayn al hajj `ali,abd al karim ibn `azzu,abd al muhsin as sa`ud,abd al wahhab,abd allah,abd allah djemel,abd allah jemel,abd el beg,abd el djebar,abd el haj,abd el harbili,abd el kader,abd el kader ben el mokhtar,abd el kader berramda,abd el kader ouled barhdad,abd el kader sadia,abd el kamel,abd el kemel,abd el kerim,abd el krim,abd el lait,abd el liouech,abd el malek ramdane,abd el moumene,abd el ouiret si ali,abd el-malek ramdane plage,abd en rasou,abd er rahamane,abd er rahmane,abd er rahmane el garsi zaouia,abd er rahmane el khahfa,abd er rasoul,abd er razzaq,abd karez,abd mutliq,abd obo,abd ol ali khan kalay,abd ol emam feysali,abd ol rahman kur,abd ol salam,abd ol samad kalay,abd ol samad khan,abd ou ben diane,abd oumbour,abdyan,abda,abda-sola,abdak,abdak-e balenah,abdak-e baleneh,abdak-e balinah,abdak-e taynah,abdakai,abdakai bala,abdakai bostan,abdakai koza,abdakay,abdakey,abdakovici,abdal,abdal baba ziarat,abdal khan qal`a,abdal khan qal`ah,abdal khan qal`eh,abdal samadi,abdalkhankala,abdal-e pasha khan,abdal-i-pasa khan,abdala,abdalabad,abdalan,abdalanla\302\277,abdalanly,abdalar,abdalata,abdalawa,abdalaye,abdalbayazit,abdalbayezit,abdalbeyazit,abdalbodu,abdalcik,abdaleh,abdalhasan,abdali,abdalia,abdaliin huryee,abdaliin khure,abdalinlu,abdalke,abdalkolu,abdalkoy,abdalkuyusu,abdalkuyusukoy,abdalla,abdallah,abdallah ben mohammed,abdallah diori,abdallah ou aiss,abdallah ou amar,abdallahkhel,abdallar,abdalli,abdallo,abdally,abdallyar,abdalmezreasi,abdalo,abdaloglu,abdaloren,abdalpinari,abdalpur,abdaly,abdalyar,abdam,abdan,abdan-i-mir alam,abdana,abdanab,abdanan,abdaneh,abdang,abdangesar,abdangsar-e latleyl,abdani,abdanpur,abdar,abdar bayan,abdar bayan somon,abdar bayan suma,abdar mash,abdar miyan,abdar rahman jatel,abdara,abdara bayan sume,abdara bayan sumu,abdara-bayan,abdarah,abdarantaiin hural,abdaranton khural,abdarantona khural,abdarantyn khural,abdarbu chal,abdargan,abdarlu,abdarmian,abdarra,abdarreh,abdartayn sum,abdartayn sume,abdartoyn sume,abdartyn sume,abdartyyn sume,abdaru,abdasht,abdata,abdavan,abdaw,abdayevo,abdayyar,abdehgah,abdejlil,abdel begrou,abdel hachs,abdela,abdelaziz,abdelcader soodia,abdelhadi,abdeli-ye shekastan,abdelik,abdellah ben zahra,abdelle,abdelli,abdelmalek ramdam,abdelmalek ramdan,abdelmoumen,abdelo,abdem,abdem omar,abdeneyk,abder rafi,abdera,abderafi,abderahmane,abderaz,abderraft,abderrahmane,abdet,abdewala,abdi,abdi ali,abdi arbo,abdi beru,abdi boloh,abdi farah,abdi gara,abdi gorda,abdi koy,abdi ky,abdi mahallesi,abdi nilemchane,abdi roba,abdi seid,abdi uurane,abdiaga,abdian,abdibay,abdici,abdicikmaz,abdicikmaza,abdida,abdier,abdiguil,abdih wais,abdikeyevo,abdikioy,abdikoy,abdikoyu,abdilan,abdiler,abdili,abdilisi,abdille,abdilli,abdimulyo,abdinegara,abdinli,abdioglu,abdioglu koy,abdipasa,abdissenbosch,abdita,abditolu,abdiusagi,abdjogana,abdo,abdol abad,abdol abad silooeyeh,abdol ahd bay kamari,abdol aziz,abdol baqikhan kalay,abdol mohey od din kalay,abdol rahim,abdolabad,abdolar,abdoli,abdoliyye,abdollah abad,abdollah abad hoomeh,abdollah khan-e-pain,abdollah masood,abdollahabad,abdolo,abdolu,abdoly,abdon batista,abdon calderon,abdon castro tolay,abdoo,abdoon,abdorrahman,abdos,abdou,abdou dao,abdou dia,abdou kwara,abdou salam,abdou-koara,abdouel,abdoul sinia,abdouladji,abdoulaue,abdoulay kaou,abdoulaye,abdoulayekro,abdoullah,abdourahmane,abdouramane,abdourta,abdow,abdowlabad,abdrakhimova,abdrakhimovo,abdrakhman-mechet,abdrakhmanka,abdrakhmanova,abdrakhmanovo,abdrashit-otar,abdrashitova,abdrashitovo,abdrazyak,abdrazyakova,abdreyevo,abdrezyakovo,abdrrakhimzi,abdu,abdu deiyata,abdu karim,abdu karim khan,abduamin,abdubay,abdui,abduilah kyango,abdujaki,abduka,abdukarim,abdukoy,abdul,abdul ahad bay kamari,abdul ahd bay kamari,abdul al `abhul,abdul aziz,abdul aziz arain,abdul aziz khan,abdul aziz kiryo,abdul aziz koruna,abdul bakhsh laghari,abdul baqi,abdul baqi khan kelay,abdul baqikhan kalay,abdul bari kor,abdul beparir kandi,abdul buqi kior,abdul fateh jamali,abdul fateh khan khosa,abdul feteh rind,abdul ghafur,abdul ghafur laghari,abdul ghani,abdul ghani khosa,abdul ghani muhajir,abdul hadi,abdul hadi beg,abdul hai arain,abdul hakim,abdul hakim brahui,abdul hakim burro,abdul hakim kili,abdul halim wand,abdul hamid,abdul hamid qureshi,abdul hamid shaikh,abdul hamur,abdul haq,abdul haq bhatti,abdul haq bhurgari,abdul haq brahui,abdul haq kili,abdul haq punjabi,abdul hasan,abdul hazar beg,abdul husain,abdul jabbar,abdul jabbar bugti,abdul jasim,abdul kadir,abdul kadir,abdul karim,abdul karim babar khan,abdul karim bakhlani,abdul karim bhatti,abdul karim bhorti,abdul karim brahui,abdul karim halq,abdul karim jafri,abdul karim kalhoro,abdul karim khan,abdul karim khoso,abdul karim pali,abdul karim shah,abdul karim taraki,abdul karim walhari,abdul karimwala,abdul kasara,abdul khair logur,abdul khalia koruna,abdul khalid,abdul khalig,abdul khaligwala,abdul khaliq,abdul khaliq burira,abdul khaliq memon,abdul khaliq merhosa,abdul khan,abdul khana,abdul khel,abdul latif khan,abdul latif rahu,abdul majid,abdul majid arain,abdul majid banda,abdul majid bhambro,abdul majid gola,abdul majid jo goth,abdul majid kalhoro,abdul majid khan gola,abdul majid khosa,abdul majid lehri,abdul majid machhi,abdul manan,abdul manan kalle,abdul manan kili,abdul manan koruna,abdul masek,abdul matin,abdul momin,abdul muhammad,abdul muhammad koruna,abdul nabi,abdul nabi brahui,abdul nahi,abdul qadir jelani goth,abdul qadir jo goth,abdul qadir khanwala,abdul qadir khosa,abdul qadir khoso,abdul qadir mailan,abdul qadir mari,abdul qadir sanjrani,abdul qadir tanio,abdul qayum kalay,abdul quddus khan kili,abdul rahaman,abdul rahim,abdul rahim bhutto,abdul rahim darkhan,abdul rahim khan,abdul rahim mekan,abdul rahim suhandro,abdul rahimwala,abdul rahini,abdul rahman,abdul rahman dahri,abdul rahman ji utaq,abdul rahman rahar,abdul rahman tatro,abdul rahmanzai,abdul rakhman kili,abdul rashid arain,abdul rashid chhalgari,abdul rashid tagar,abdul rashidabad,abdul rasul,abdul rasul ismail,abdul razaq,abdul razaq bhuto,abdul rehman,abdul rehman awaili,abdul rehman kalhora,abdul rehman pathan,abdul rehman sarki,abdul rehman zai,abdul salam,abdul salam kili,abdul salam zardari,abdul samad kalay,abdul sattar khan brahui,abdul sattar shah,abdul sattarwala,abdul shraif arain,abdul taj din,abdul wahab,abdul wahab suhandro,abdul wahid,abdul wahid baran,abdul wahid kalhoro,abdul wahid laghari,abdul,abdulakhadkhan-karez,abdulaziz-kalay,abdulbaki,abdulbakikhan-kalay,abdulbakir,abdulkadir-kalay,abdulkadyr,abdulkala,abdulkarimkhel,abdulkayum-kalay,abdulkhakim,abdulkhakim-kalay,abdulkhakkalay,abdulkhamid,abdulkhamid-kalay,abdulkhamid-kotkay,abdulkheyl,abdulkuduskhan-kalay,abdulmukhayidin-kalay,abdulvakhab-kalay,abdulvakhid,abdulvakhid-kalay,abdulval,abdul-aziz,abdul-gazy,abdul-rakhman-kili,abdul-samed-kalay,abdul-zavod,abdul`alikhan kalay,abdul`alim kalay,abdula,abdulabad,abdulah,abdulahi,abdulaimo,abdulakhi,abdulalim-kalay,abdular,abdulare,abdulay,abdulbakiningoyi,abdulgani,abdulgani maliat,abdulgaz,abdulgazi,abdulgazina,abdulgazino,abdulgedigi,abdulhabib kalay,abdulhadi,abdulharap,abduli,abdulici,abdulimam,abdulina,abdulino,abdulka,abdulkadir,abdulkadirkoy,abdulkarimkhel,abdulkarimova,abdulkarimovo,abdulkasimovo,abdulkerim,abdulkhali,abdulkioi,abdull qadir,abdulla,abdulla bin selemani,abdulla bin sulliman,abdulla khan,abdulla-akhundzada-karez,abdulla-ushagi,abdullabad,abdullabiy,abdulladzhan-kalay,abdullah,abdullah abro,abdullah abupoto,abdullah arisar,abdullah bagh kili,abdullah bakhsh kalhoro,abdullah baloch,abdullah banda,abdullah beg,abdullah bin selemani,abdullah bin sulliman,abdullah charan,abdullah dakhan,abdullah gado,abdullah goth,abdullah halepoto,abdullah halq,abdullah jan,abdullah jan kalle,abdullah jan kili,abdullah jarwar,abdullah jat,abdullah jesar,abdullah juneja,abdullah kapri,abdullah khan,abdullah khan habibani,abdullah khan jamali,abdullah khan kili,abdullah khan koruna,abdullah khan ladhamani,abdullah khan maidan,abdullah khan rajar,abdullah khan rind,abdullah khaskheli,abdullah khosa,abdullah kili,abdullah kyango,abdullah laghari,abdullah lashari,abdullah lohar,abdullah machhi,abdullah madhra,abdullah mandhra,abdullah masud,abdullah mehar goth,abdullah memon,abdullah nahiun,abdullah nur kaskai,abdullah patel,abdullah qeissan,abdullah rajri,abdullah shah,abdullah shaheed,abdullah shahid,abdullah town,abdullah-di-basti,abdullaha akbar,abdullahabad,abdullahbad,abdullahbey,abdullahgarh,abdullahhoca,abdullahi,abdullahjan kalay,abdullahkhan,abdullahkomu,abdullahnagar,abdullahoglu,abdullahpur,abdullahwala,abdullahwali khui,abdullakala,abdullakalay,abdullakhan,abdullakhan-kalay,abdullakhankalay,abdullakheyl,abdullakor,abdullanpur,abdullapur,abdullapur banati,abdullapur mustaqil,abdullapuram,abdullar,abdullausagi,abdullaushagy,abdullino,abdullun,abdulmambetova,abdulmambetovo,abdulmanbetova,abdulmenbetovo,abdulmenevo,abdulmenovo,abdulmohayedin kalay,abdulnasyrova,abdulnasyrovo,abdulovka,abdulovo,abdulpur,abdulqader kalay,abdulrahmansaleh,abdulrakhmanlar,abdultarla,abdulwadamai,abdulwadanai,abdulwahed kalay,abdulwahedkhan,abdulwala,abdulyan,abdulyany,abdulzaher,abdulzai,abdum,abdun,abdun raz,abdun raz-e bala,abdunharap,abduni,abdunly,abdunneiadaman,abdupur,abdur radha,abdur rahim basti,abdur rahim bhatti,abdur rahim dal,abdur rahim dumbki,abdur rahim kalay,abdur rahim khanwala,abdur rahim kili,abdur rahima,abdur rahman,abdur rahman cambar,abdur rahman khel,abdur rahman kili,abdur rahman kor,abdur rahman koruna,abdur rahman mandau,abdur rahman mutwi,abdur rahman number one,abdur rahman number two,abdur rahman shah koruna,abdur rahman wasa,abdur rahmanzai,abdur rashid,abdur rashid magasi,abdur razaq al majid,abdur rehman,abdur rehman ganghro,abdur rehman shah kalle,abdur ridha,abdurakhim-kalay,abdurakhmanly,abdurakhmanova,abdurakhmanovo,abdurashid,abdurashid-chambar,abdurashid-otar,abduri,abdurrahim,abdurrahimkhel,abdurrahimzi,abdurrahman,abdurrahman kargud,abdurrahman kor,abdurrahmandede,abdurrahmankhel,abdurrahmankhel karez,abdurrahmanlar,abdurrahmanli,abdurrahmanlikoyu,abdurrahmanpasa,abdurrakhim,abdurrakhim-kalay,abdurrakhimkheyl,abdurrakhimzi,abdurrakhman,abdurrakhman-chambar,abdurrakhman-karez,abdurrakhmani,abdurrakhmankhan,abdurrakhmankhan-kala,abdurrakhmankheyl,abdurrakhmankheyl-karez,abdurrakhmankor,abdurrakhmanzay,abdurrakman-kalacha,abdurrashid,abdurrashid karez,abdurrashid-karez,abdurrazak,abdurta,abdurug,abdus sattar punjabi,abdusamar,abdusamat,abduuch,abduvan,abduzak,abdy,abdyan,abdybulak,abdyem,abdykalyk,abdynly,abdyreva,abdyrova,abdyulimam,abdzaba,abdzava,abdzhalil,abdzhoshi-bala,abdzhoshi-pain,abe,abe aounyassou,abe aounyessou,abe buko,abe katemenso,abe khel,abe kofikro,abe kouadiokro,abe neghar,abe springs,abe yarbrough,abe-emmi,abea,abeadzi-domenosi,abealirih,abeam,abean,abean lor,abeancos,abeanou,abease,abeasi,abeayop,abebain,abebaye,abebe,abebea,abebeabo,abebekombo,abebeyun,abebi,abebu,abec,abecao,abeche,abecher,abechkan,abechuco,abecia,abed,abedamegu,abedati,abede,abedei,abedeni,abedes,abedim,abedinekope,abedkhel,abedo,abedotun,abedpur,abedul,abedula,abedules,abee,abeelbroekstraat,abeele,abeelhoek,abees eemid,abeesaale,abeesgarwaal,abefa,abega,abegak,abeganga,abegani,abegarm,abegba,abegede,abegi,abegoaria,abegoes,abegondo,abegong,abegou,abegu,abegue,abeguie,abegunde,abegunrin,abeh,abeh mohammad,abeh mohammad `ali,abeh qilij,abehenasi,abeheneasi,abehu,abeibara,abeiche,abeid,abeidate,abeif,abeijon,abeikli,abeilhan,abeille,abeine,abeino,abeinou,abeir,abeit,abeiya,abeizeam,abeja,abejales,abejao,abejar,abejas,abejera,abejero,abejo,abejones,abejorral,abejuela,abejukolo,abejum,abek,abekat,abekesti,abeka,abekabum,abekba,abeke,abeken,abekenta,abekheyl,abekkae,abekkun,abeko,abekofikro,abekope,abekouta,abekr,abeku,abel,abel alexandre,abel basan,abel bassane,abel carapuchoso,abel chunga,abel kumo,abel maueua,abel mission,abel muwanda,abel muwandya,abel santa maria,abel santamaria,abel-mula,abela,abela lida,abelago,abelan,abelange,abelankai,abelar do luz,abelardo,abelardo l. rodriguez,abelardo luz,abelardo rodriguez,abelcourt,abele,abele maba,abelea,abeleda,abeledo,abeledos,abeleh,abelehi,abeleh-ye `ali moradi,abeleh-ye `olya,abeleh-ye pain,abeleh-ye rahmani,abeleiras,abelek,abelena,abelenda,abelendo,abelera,abeles,abelessa,abelgas,abelha,abelhal,abelhas,abelheira,abelheiros,abeli,abeliani,abelidalam,abelino,abelio,abelites,abeliua,abelkiri,abelkoso,abelkrogs,abell,abell city,abell corners,abella,abella de la conca,abellas,abelleira,abellouhou,abells corners,abellu,abelmani,abelnes,abelnieki,abelo,abelogo,abelon,abelong,abelouhou,abelova,abelovskiy,abelovskoye,abelrati,abelsborg,abelsruhe,abelt,abelti,abelu,abelvaer,abem,abemarre,abemaw,abembe,abembe i,abembe ii,abeme,abemi,abemnom,abemo,aben,aben young,abena,abenab,abenakabre,abenakrom,abenan,abenase,abenasi,abenaso,abenaston,abenbak,abenberg,abend,abendanon,abenden,abendiego,abendoman,abendouroua,abendsiepen,abene,abene-oton,abenedimas,abenekokondre,abenelam,abenelan,abenelang,abenfigo,abeng,abengnam,abeng-nam,abenga,abengibre,abengnam,abengoua,abengourou,abenguir,abenguru,abenheim,abeni,abenia,abenilam,abenilang,abenilla,abenin assi,abenkrom,abennam,abenojar,abenoux,abenra,abens,abensberg,abensia,abensperg,abentheuer,abenuj,abenye,abenyende,abenyendu,abeo,abeokouta,abeokuta,abeotobo,abepawtia,abepe,abepim,abepotia,abepura,aber,aber cowarch,aber el ma,aber-arth,aber-bran,aber-nant,abera,aberach,aberaeron,aberagerema,aberaman,aberangell,aberapira,aberarder,aberargie,aberastain,aberasturi,aberayron,aberbargoed,aberbeeg,aberber,abercairny,abercanaid,abercarn,abercastle,aberchalder,aberchirder,aberconway,abercorn,abercorn heights,abercorn point,abercorn pont,abercrombie,abercynon,aberdalgie,aberdare,aberdaron,aberdaugleddau,aberdean,aberdeen,aberdeen city,aberdeen county,aberdeen estates,aberdeen gardens,aberdeen junction,aberdeen park,aberdeen village,aberdeenshire,aberdour,aberdovey,aberdulais,aberdyfi,abere,abere ijo,abere tchertcher,aberearne,aberebi,aberedw,aberei,abereiddy,abereke-odo,abereke-oke,aberekui,aberela,aberem,abererch,aberesa,aberete,aberets,aberewo,abereyebia,aberfam,aberfan,aberfeldy,aberffraw,aberfield,aberfoil,aberford,aberfoyle,aberg,aberga,abergale,abergavenny,abergeie,abergele,abergement,abergement-la-ronce,abergement-le-grand,abergement-le-petit,abergement-les-thesy,abergement-saint-jean,aberget,aberghan,aberglasslyn,abergorlech,abergorloch,abergwaun,abergwesyn,abergwili,abergwyfni,abergwynfi,abergynolwyn,aberhafesp,aberin,aberipila,aberishi,aberkenfig,aberlady,aberlemm,aberlemno,aberllefenni,aberllefenny,aberlour,aberly,aberma,abermain,abermule,abern,abernant,abernathy,abernathys mill,abernethy,abero,aberoynon,aberparemna,aberporth,aberrak,aberranene,abersa,abersberg,abersdorf,abersfeld,abersha,abersoch,aberstuckl,abersychan,abert,abertamy,abertao,abertawe,abertege,aberteifi,abertham,aberthaw,abertillery,abertinha,abertown,abertridwr,abertura,aberu,aberu agba,aberun,aberystwyth,aberzhausen,abes,abesa,abesale,abese,abesedo,abesekaragama,abeselh,abeselli,abesewa,abesh,abesh ahmad,abeshdun,abeshdun-e `olya,abeshdun-e sofla,abeshe,abeshkan,abeshki,abeshler,abeshow,abeshterik,abeshu,abesin,abesler,abesre,abessa,abessakope,abessankope,abesse,abesso,abestan,abestan pain,abesu,abesville,abet,abeta,abeta robe,abetem dwerebease,abetemasu,abetemozzo,abetensu,abetensua,abetia,abetife,abetifi,abetil,abetim,abetinm,abetinsi,abetinso,abetinsu,abetito,abeto,abetone,abetsweiler,abetwakondre,abetzberg,abetzdorf,abeu,abeuebudi,abeuek budi,abeuek tingkeum,abeuekdjaloh,abeuekglanteue,abeuekjaloh,abeuekreuling,abeugborode,abeuk,abeul,abeuseulanteu,abeve,abevgborode,abeville,abey,abeya,abeyda,abeyesekaragama,abeyglu,abeyi,abeykheyl,abeymes,abeysaz,abeyta,abeytas,abez,abezai,abezames,abezar,abezari,abezer,abezhdan,abezhdan-e abandar,abezhdan-e malmulin,abfalter,abfaltern,abfaltersbach,abgaccio,abgah,abgalehdar,abganda,abgandah,abgandeh,abganerovo,abganerovskiy,abganery,abgao,abgara,abgarch,abgardu,abgareh,abgarkhuk,abgarkhuki,abgarm,abgarm kuh,abgarm larijan,abgarm rud,abgarm-e bala,abgarm-e karvan,abgarm-e keykuh,abgarm-e pain,abgarm-e pa`in,abgarmak,abgarmak-e `olya,abgarmak-e bala,abgarmak-e pain,abgarmak-e sofla,abgarmu,abgarna,abgauyera,abgavera,abgazan-e pakuh,abge,abgel,abgesheh,abgharkhuki,abgheh,abgig,abgigdara,abgir,abgir-e khaki,abgird,abgol,abgonta,abgoroum ramadan,abgozar,abguana,abgue,abguhi,abgul,abgunste,abgurandan,abha,abha khel,abhainn an scail,abhaipur,abhaipura,abhal,abhaman,abhana,abhanpur,abhapuri,abhar,abhar-e bala,abhar-e pain,abharak,abhark,abhaya,abhayapuri,abhayayab,abhaynagar,abhaynil,abhaypara,abhendu,abhepur,abhgaon,abhi,abhianwala,abhiasa,abhinaipur,abhipur,abhiram,abhiramapuram,abhirampur,abhiri,abhirkhil,abhit,abhit al hajar,abhit el-hagar,abhoca,abhona,abhorn,abhoro hingora,abhoypara,abhulke,abhur al janubiyah,abi,abi `arish,abi abdi,abi addi,abi beyglu,abi dara,abi darreh,abi dasht,abi huleifa,abi iba,abi jan,abi karu,abi khan,abi khana,abi khaneh,abi khel,abi lubok gajah,abi nam,abi nars,abi padang melangit,abi padang pelangit,abi rij,abi-adi,abi-barik,abi-shor,abi-ye `olya,abi-ye sofla,abia,abia de la obispalia,abia de las torres,abia ikot obionting,abia ikot udo oboro,abia okpo,abia okpo ikotukana,abia okpo otoro,abia-okpo,abiabad,abiacao,abiada,abiadi,abiadi eshiem,abiadougou,abiagerima,abiagyai,abiak,abiak owo,abiak pain,abiak sarai,abiaka,abiakana,abiaknabo,abiakpa,abiakpo,abiakpo idiaha,abiakpo ikot essien,abiakpo ikot ntuen,abiakpo ikot obionting,abiakpo ikot udo oboro,abiakpo otoro,abiakpo-alacha,abiakpo-ibo,abial,abiala,abiama,abian,abian minfakh,abian-abian junction,abiana,abianai,abianbase,abiancanang,abiane,abianeh,abiang,abiangyai,abiankapas kaja,abiankapas kelod,abiankapas tengah,abianlalang,abiannangka kaja,abiannangka kelod,abianonzi,abiansari,abiantegal,abiantimbul,abiantubuh,abiantubuh barat,abiantubuh selatan,abiantubuh utara,abiara,abiat,abiate,abiate i,abiate ii,abiati,abiawin,abiayai,abiazan,abibat,abibiray,abibireso,abibling,abibo,abicaya,abichaca,abiche,abichu,abici elliam,abicinia,abickhafe,abico,abicot,abicun,abid,abid chamlet,abid da daro,abid khel,abid lodo,abid markhiani,abid murkhiani,abid saran,abid sareh-ye avval,abid sareh-ye awal,abid sareh-ye dovvom,abid sultan,abida,abidabad,abidagba,abide awhile,abidemi,abidhara,abidida,abidikope,abidimisebi,abidinpasa,abidioki,abidiore,abidjan,abidji,abidjio,abidlak,abidogun,abidos,abidpara,abidpur,abidunimakhi,abidus,abidzhanglu,abidzi,abie,abie ayalo,abief,abiego,abiegos,abiekro,abiem,abiemnom,abiengama,abiengama ii,abiengbali,abiengyai,abiera,abiet,abiete,abiey,abiff,abigail,abigak,abigbinde,abigborodo,abigborudu,abige,abiggi,abiggui,abigi,abigna,abigou,abigu,abigui,abigusa,abiguy,abihao,abihid,abihilam,abihilan,abiin,abiislah,abija,abijae,abijain,abijan,abijand goth,abijang,abijanglu,abijao,abiji,abijid,abijilan,abijo,abika,abikal,abikarow,abikehin,abiki,abiko,abikoffikro,abikofikro,abikro,abikunimakhi,abil,abildzha,abila,abilabio,abilami,abilamoch,abilanda,abilatega,abilay,abilaye,abilca,abild,abilda,abildaa,abildgaarde,abildgarde,abildholt,abildore,abildskov,abildtorpe,abile,abilegn,abilelai,abilelay,abilelaye,abilele kebir,abilele sarer,abilene,abileteiga,abiley,abili,abili kandi,abilio,abilitega,abilkend,abilkeyeva,abilla,abilly,abilly-sur-claise,abilo,abilti,abilu,abiluyungba,abilyataq,abim,abima,abimba,abimbola,abime,abimes,abimo,abimoa,abin,abin bakhsh jo goth,abina,abinabo aiko,abinakenu,abinane,abinba,abinda,abindanjoke,abinenge,abinetengngae,abinga,abinganan,abingdon,abingdon reserve,abingdon-on-thames,abingen,abinger,abingluogu,abingnge,abington,abington creek,abington green,abington shores,abini,abinibro,abinos,abinsi,abinsk,abinskaya,abinski,abinskiy,abintanga,abinti-abangork,abinuan,abinyom,abinyon,abinzano,abio,abioba,abioka,abioko,abiolekro,abion,abioncillo,abiono,abiontem,abionzo,abioro,abiquiu,abira,abiraadoa,abirakhmat,abiramam,abirane gaye,abirbri,abirdi,abire,abirem,abirem odumasi,abirerpara,abiria,abiriba,abirigbene,abiriu,abiriw,abirnagar,abirnagr,abirpara,abirrikum,abis,abis al mustajaddah,abis el-mustagadda,abisa madala,abisaase,abisai,abisaka,abisan,abisanda,abisasi,abish,abish ahmad,abish-kyand,abishevo,abishkhan,abisi,abisim,abisinia,abisistavi,abiskata,abisko,abismo,abispa,abispero,abissa,abissaare,abisso,abissoukope,abistan,abistan-e bala,abistan-e pain,abistu,abit,abita springs,abitain,abitanga,abitovo,abitureira,abitureiras,abiu,abiul,abiulya,abivardi,abiverdi,abiy,abiy addi,abiy adi,abiy imni,abiya,abiyai,abiyatku,abiye,abiyei,abiyu,abiz,abizanda,abizar,abizar khelanwala,abizarwal,abja,abja asundus,abja-paluoja,abjafa,abjahan,abjaku,abjalpur,abjar,abjar-e bala,abjar-e pa`in,abjat,abjavar,abjekan,abjeqan,abjer,abjerg,abjij,abjiouene,abjit,abjora,abjornahall,abjose bala,abjose pain,abjosh-e bala,abjosh-e pain,abjouet,abjul,abjush-e bala,abjush-e pain,abka,abkaer,abkakai daud,abkakai mian,abkalan,abkaleh sar,abkalu,abkan,abkanar,abkandu,abkar djombo,abkareh,abkaro djonbo,abkas,abkash,abkayk,abkel,abkenar,abkesheh,abkesht,abkettley,abkhana,abkhanah,abkhaneh,abkhar,abkhara,abkharak,abkhard,abkhareh,abkhati,abkhay,abkhazskiy,abkheng,abkhizeh,abkho,abkhor,abkhorak,abkhowr,abkhowri,abkhur,abkhura,abkhureh,abkhvareh,abkhvor,abkhvorak,abkhvoreh,abkhvori,abkit,abkosh,abkot,abkoude,abkourene,abkowt-e bala,abkowt-e pain,abku-ye `olya,abku-ye bala,abkuh,abkuh-e `olya,abkuh-e bala,abkuhi,abkul,abkureh,abkut-e `olya,abkut-e sofla,abla,abla-i- miranzai,ablabai,ablabo,ablach,ablagadshi,ablagadzhi,ablagh,ablaghmak,ablah,ablah-e miranzai,ablah-ye miranzai,ablahan,ablahun,ablain,ablain-saint-nazaire,ablaincourt,ablainzevelle,ablaj,ablajei,ablak,ablaketka,ablakh,ablakpur,ablalu,ablame,ablamuzadu,ablan,ablana,ablana de abajo,ablana de arriba,ablanan,ablancourt,ablandschen,ablaneda,ablanedo,ablanfedo,ablang,ablanica,ablanik,ablanitsa,ablaniza,ablanque,ablass,ablatak,ablatukan,ablatuyskiy bor,ablayan,ablayeva,ablayevo,ablazino,able,ablechikhina,abledo,ablefu,ableg,ableh,ableh sofla,ableh-ye bala,ableh-ye pain,ableh-ye pain,ableiges,ablekpui,ablekro,ablekuma,ablemkpe,ablemont,ablenay,ablenkpe,ablers,ables,ablesh,ableshnemetskiy,ableta,ablett village,ablevenia,ablezki,abli,abli aloukro,abli bonikro,abli-ye bala,abli-ye pain,ablian,ablibukope,ablibukorpe,ablik,abliko,ablimen,ablin,abline,abling,ablingi,abliri,ablis,ablisheh,ablitas,ablkend,ablo,ablo ser,ablochrhi kope,ablogame,ablogbe,ablogila,abloguila,ablois,ablois-saint-martin,ablon,ablon-sur-seine,ablonu,abloolan,ablos,ablouikro,abloux,ablovo,ablti,ablu,abluchye,ablukhvara,ablukhvari,ablulan,ablum,abluno,ablyaisovo,ablyaskino,ablyazevo,ablyazova,ablyazovo,ablyazovskiy,ablyk,ablystem,abma`in,abmadal,abmah,abmal,abmu,abmu `olya,abmu sofla,abna,abnao,abnabna,abnak,abnama,abnarak,abner,abnetare,abney,abney crossroads,abni,abnik,abnil,abnow,abnu,abnub,abnud,abnuh,abo,abo arka,abo blakro,abo bouyafe,abo bouyoufe,abo hachim,abo kala,abo kouadjokro,abo kouassikro,abo manga,abo ogugu,abo ogwash-ukwu,abo tanga,abo tangah,aboa,abo-bougrima,abo-ogwashi ukwu,aboa,aboabam,aboabo,aboabo duana,aboabo kwaem,aboabo nkwanta,aboabo number 1,aboabo number 2,aboabo number 3,aboabo number 4,aboabo yokum,aboabogya,aboaboja,aboaboso,aboachi,aboaddi,aboade,aboadela,aboadi,aboadze,aboakye,aboam,aboan,aboano,aboansa,aboansa number 2,aboase,aboasi,aboaso,aboaso nkwanta,aboawso,aboaziny,aboba,abobada,abobara,abobelane,abobeleira,abobiri,abobo,abobo baoule,abobo doume,abobo te,abobo-abaoure,abobo-atte,abobo-gare,aboboda,abobofovi,abobora,aboboras,aboboreira,aboboya,abobrako,abobral,abobreira,abobreira cimeira,abocay,abochigakofe,abochigakope,abochikope,abochikrom,abochiman,abocho,abod,abod-abod,aboda,abodaneira,abodari,aboday,abodease,abodeem,abodemving,abodere,abodere oridokun,aboderin,abodi,abodiem,abodim,abodinkrom,abodioupa,abodit,abodji,abodjopo,abodo,abodom,abodom sakunne,abodona,abodougou,abodow-isu,aboduam,aboduem,abodum,aboe,aboehikope,aboelon,aboemaneh,aboen,aboenu,aboesi,aboeviah,abofaw,aboffia,abofifia,abofla kope,abofoo,abofour,aboframfajwini,abofrem,abofrim,abofu,aboga,abogado,abogan,abogandra,abogbe,abogbene,aboge,abogen,aboghi,abogi,abognan,abogo,abogo-dodu,abogocola,abogokola,abogoroum ramadan,abogoroum zafay,abogosu,abogotom,abogunde,abogwomado,abogwomadu,aboh,aboh-ogwashi-ukwu,abohar,abohazo,abohia,abohidrano,abohijafy,abohijanahary,abohire,abohor,abohyire,aboi,aboikakro,aboikoimekro,aboikro,aboim,aboim da nobrega,aboim das chocas,aboindoukpinkro,aboine,aboinkro,aboissa,aboisso,aboisso comoe,aboissue,aboite,abojiemiye,abojienuye,abojosu,abok,aboka,abokah,aboke,aboke dosso,abokede,aboket,aboki,abokinde,abokkagama,aboko,aboko i,aboko ii,abokoase,abokobi,abokoidiokro,abokolaiya,abokongang,abokorere,abokosso,abokouakoudougou,abokouamikro,abokouassikro,abokoukope,abokouma,abokpa,abokpa tyav,abokro,abokrou,aboksar,abokwasimbe,abokyea,abol,abol beq,abol de cima,abol firooz,abola,abolabad,abolabad-e gilavand,abolahy,abolai,abolan,aboland,abolave,aboldari,abolduyevo,abolduyevskiy,abolduyevskiy dneprov,abole,abole kouassikro,abolegbe,aboleira,abolekro,abolens,abolenye,aboleshevo,abolfat-h abad,abolfath,aboli,aboli laib,abolicao,abolikha,abolikiri,abolikro,abolini,abolkalni,abolkhazen,abolkheyl,abolkheyr,abolmasov,abolmasovo,abolo,abolo i,abolo ii,abolohunko,abolokope,abolongenge,abolonya,abolonye,abolonyero,aboloume,abolsk,aboltini,aboltsy,aboluo,abolvafai,abolverdi,abolyont,abom,aboma,aboman,abomanso,abomasalafuo,abombanya,abombo,abombonya,abomdama,abome,abome calavi,abomedamo,abomendama,abomenvila,abomey,abomey-calavi,abomgaon,abominya,abomisam,abomisom,abomiti,abomo,abomosarefon,abomoso,abomosu,abomotasa,abompe,abomposa,abomposu,abomsa,abon,abon,abon-abon,abon-fo,abonabo,abonabung,abonagan,abonakope,abonamo,abonando,abonara,aboncourt,aboncourt-en-vosges,aboncourt-sur-seille,abondance,abondant,abonday,abondaye,abondie,abondo,abonema,abong,abong ahoun,abong doum,abong mbang,abong yeri,abong-abong,abong-miang i,abong-miang ii,abongabong,abongisia,abongnikro,abongo,abongonamoun,abongoua,abongsu,aboni,aboni kope,aboni kwara,aboni-koara,abonis,abonji,abonko,abonkope,abonkor,abonku,abonkwa,abonnema,abono,abonokofe,abonokro,abonsa,abonse,abonshe,abonsrakurom,abonsuiaso,abontiakun,abontiem,abontjeman,abontsa,abontuakun,abonu,abonunkomu,abonutichus,abonuzu,abonville,abony,abonyiuti tanyak,abonyo,abonzerakrom,aboo besan,aboo meshilesh,aboo nageh,aboobacar,aboom,aboontem,abooso,aboot,abootabareh,aboow cali,abope,abopisa,abopo,abor,abor isu,abor-tikame,abora,abora gyabankrom,abora komis,aborahu hural,aboramban,aborano,aborano atsimo,aborano avaratra,aboransa,aboransama,abord,abord a plouffe,abore,aboreachic,aboreba,aboren,aboretum,aboreu,aborey,aborg,abori,aborim,aborino,aborisa,aborisade,aborishade,aborko,aborlan,aborlorve,abornicano,aboro,aborobeano,aborokhu sume,aborokpuyo,aborotsy,aborou,aborre,aborrecido,abort,aboru,abos,abosa,abosamso,abosem,abosha,aboshi,aboshin,abosioto,abosjo,abosjon,aboskul,aboskul,aboso,aboso gbene,abosso,abossoumkofe,abosto,abosumba,abot,abot molina,abota,abotagon,abotang,abote,abotega,abotendua,abotesse,aboti,abotia,abotie,aboto,aboto afa,aboto oja,abotoasi,abotsi korpe,abotsigakorpe,abotyy,abou,abou aabde,abou aachte,abou aafsa,abou aafse,abou aafta,abou aamar,abou aamche,abou abe,abou angoum,abou assal,abou ayoro,abou bachir,abou bakr,abou chach,abou chadra,abou chamaa,abou charje,abou charji,abou chech,abou cheilem,abou chelham,abou choukouri,abou daame,abou daane,abou dafne,abou dali,abou dangala,abou danna,abou dara,abou darde,abou darhme,abou dem,abou didi kbir,abou didi srhir,abou digin,abou djali,abou djebbar kbir,abou djebbar sghir,abou djedkha kbire,abou djedkha sghire,abou djreine,abou dreikha,abou drikha,abou ed douhour,abou ej joura,abou el balaye,abou el hassen,abou el kahf,abou el khasse,abou el kheff,abou el khous,abou el koussour,abou el moujaher,abou el mreir,abou el qdour,abou el rhorr,abou fala,abou faraj,abou foule,abou gadama,abou goudou,abou goulem,abou gounde,abou goye,abou habbe,abou haiye,abou hajar,abou hajar chamiye,abou hajar jezire,abou hajara,abou hajeira,abou halka,abou halqa,abou hamam,abou hamami,abou hammad,abou hammam,abou hanaya,abou hanaye,abou haouadid,abou haouri,abou hardane,abou hardoub,abou hassane,abou haye,abou hbeilate,abou heidj,abou heij,abou hidjelidj,abou hommos,abou houreira,abou idjelidje,abou jadha aatiq,abou jadha kebir,abou jadha koubra,abou jadha salma,abou jadha serhir,abou jamach,abou jebbar kebire,abou jebbar serhire,abou jekka,abou jekka selma,abou jelal,abou jerade,abou jreif,abou jreine,abou kaab,abou kaabe,abou kabe,abou kahf,abou kala,abou kannse,abou kano,abou kbara,abou kbire,abou kemal,abou khachbe,abou khanadeq,abou khanadeq chemali,abou khanadeq qibli,abou khanadik,abou khanatej,abou khanatidj,abou khatar,abou khattar,abou khneiza,abou khoueite,abou khreiza,abou kir,abou klaifoun,abou koma,abou koulam,abou kounday,abou koundaye,abou kous djekebe,abou kous i,abou kous ii,abou koussour,abou ksour,abou lail,abou lela,abou lelaya,abou loubia,abou maait,abou magal,abou makke,abou mannkar,abou mannsaf,abou maqbara kebire,abou maqbara serhire,abou mardo,abou marou,abou mbard,abou mendil,abou menndil,abou mesnataine,abou mgargi,abou mikhayel,abou mizane,abou moqbara kbir,abou moqbara sghir,abou ndanane,abou ndouro,abou neitoule,abou omar,abou qaatour,abou qalqal,abou qamha,abou qamhah,abou qanntara,abou qantara,abou qassayeb,abou qbaiss,abou qbeiss,abou rbeis,abou rhatte,abou rmal,abou roubeis,abou roubeiss,abou roueil,abou rtcha,abou safayeh,abou salah,abou samra,abou soultouk,abou tababir,abou tabaq,abou tabbe,abou tabilo,abou talha,abou taltal,abou taouil,abou telehe,abou theije,abou tile,abou tine,abou tirraha,abou toueiqiye,abou yakouba,abou zerafa,abou zeralfa,abou zreig,abou zreijiye,abou-deia,abou-ndoulaf,aboua,aboua akesse,aboua kouao,aboua nguessankro,aboua seka,abouabou,abouadikro,abouakakro,abouakouadiokro,abouakouamekro,abouakouassikro,abouakrakro,abouakro,abouandrikro,abouassikro,abouassio,aboucha,aboud ben ayed,abouda,abouda mouifes,aboudaou,aboude,aboude kouassikro,aboude mendeke,aboude-mandeke,aboudela,aboudes,aboudeye,aboudiokope,aboudou,aboudouhour,aboudoukhan,aboudoukro,aboudyo kope,aboue,abouetare,abougarga,abougern,abougnankro,abougoudam,abougoui bagirmi,abougoui fellata,abougregate,abouia,abouiat,abouiate,abouich,abouir,abouka,aboukankro,aboukemendeke,aboukhir,aboukhor,aboukiema,aboukiesi,aboukir,abouko koara,abouko kwara,aboukoua,aboukoue,aboukousoum,aboukoussom,aboukro,abouksoum,aboukssoum,aboulele,abouliebde,abouline,aboulkhire,aboulli,aboulo,aboulou,aboulouk,abouloukope,abouma,aboumadzale,aboume,aboumzok,abouna,abouna sounga,abounaia,abounanbay,abounatari,abounavahe,abounaviri,aboundji,aboundjohol,aboundourno,aboundouro,aboundourou,aboundouroua,aboundouru,abouneie,aboungode,abounoua,abounoura,abouokro,abour,abourda,abourda abchagara,abourda mankala,abourda ndouta,abourkir,abourtouno,abousoko,abousou,abousouf,aboussars,aboussas,aboussou,aboussoukro,about,about idjelidj,about koimari,abouta,aboutangi,aboutantara,aboutou,aboutville,aboutyour,abouyat,abouyi,abouziane,abovary,abovce,abovyan,abowam,abowee,abowenmu,abowi,abowikovhe,abowinim,abowl,abowl firuz,abowl hasani,abowlabad,abowlfathabad,abowlfulus,abowlhasan kola,abowlkheyr,abowlma `ali,abowlshanak,abowlvardi,abowula,abowzi,aboy,aboylo,aboyne,aboyo,abozai,abozi,abpanguiyeh,abpar,abparan,abparan-e bala,abparan-e pain,abpartaw,abpartow,abparu,abpasa,abpay,abpuda,abpudah,abpudeh,abpuneh,abputi,abqad,abqah,abqaik,abqaiq,abqayq,abqol,abqowl,abquy,abr,abr bakuh,abr fush,abr juyeh,abr-e kalo,abra,abra abajo,abra arriba,abra bejucal,abra blanca,abra calacruz,abra calcalaguay,abra ckasa,abra de caiguanab,abra de caiguanabo,abra de caisar,abra de caisuro,abra de castellon,abra de emiliano,abra de huillque,abra de ilog,abra de napa,abra de pulgui,abra de pulqui,abra de sivingamayo,abra de tapacai,abra de tapacari,abra del medio,abra del mezquite,abra del padre,abra divorti,abra divortio,abra grand,abra grande,abra kasa,abra laite,abra pampa,abra potrerillo,abra rica,abra sivingani,abra tambocasa,abra tambockasa,abra turuguapa,abra vieja,abra wand,abraa,abraao,abracala,abracasa,abrach,abraco,abradar,abrade,abradine,abradinou,abradovo,abrafo,abrag khitan,abragal,abragal,abragan,abragao,abragciems,abraghan,abragtsiems,abragtsiyems,abrah,abraha azba,abraham,abraham bay,abraham de los santos,abraham gonzales,abraham gonzalez,abraham lincoln,abrahams bay,abrahamgbene,abrahamgul kalay,abrahamhegy,abrahamovce,abrahams,abrahams cove,abrahamsa,abrahamsan,abrahamsheide,abrahamsville,abrahamtelek,abrahamtelep,abrahan,abrahan asprilla,abrahao,abrahdar,abraheh,abrahgah,abrahia,abrahu,abrai,abraido,abraimavka,abraimovka,abrain,abrairas,abraj,abrak,abrak khetan,abrak olya,abraka,abrakaso,abrakhamgul-kalay,abrakhan,abrakhovo,abrakunis,abral,abraly,abram,abram mys,abram-mys,abram-stay,abram-tyube,abramabad,abrame,abramenko,abramenkovo,abrameti,abramfeld,abramichi,abramikha,abramivska dolyna,abramkino,abramkova,abramkovo,abramkuduk,abramo,abramov,abramova,abramova gora,abramovichi,abramovici,abramovka,abramovo,abramovshchina,abramovsk,abramovskaya,abramovskaya dolina,abramovskiy,abramovskiy pochinok,abramovskoye,abramow,abramowice,abramowice koscielne,abramowice prywatne,abramowicze,abramowka,abramowszczyzna,abrams,abrams lake mobile estates,abrams landing,abramsa,abramsan,abramskraal,abramtsevo,abramushkin,abramut,abramy,abramychi,abran,abranane,abrancalha de baixo,abrancalha de cima,abranches,abrand,abrandabad,abrandabad-e shahediyeh,abraner-gakhankin,abranfalva,abrang,abrang sar,abranga,abrangje,abrania,abrank,abranka,abranna,abranov,abranovce,abranquil,abrans,abransa,abrantes,abranteyemmobo,abrantiyamawbaw,abranytanya,abrao,abraq,abraq haytan,abraq khaitan,abraq khaytan,abraq khetan,abraq khitan,abraqiyah,abraqunis,abrarow,abras,abras de abajo,abras de arriba,abras de ivio,abras del corozo,abrascico,abrash,abrashchikha,abrashina,abrashino,abrashk,abrashkino,abrashlare,abrasi,abrasimovo,abrasive,abraslare,abrasmin,abrateyevo,abrau,abrau-dyurso,abrava,abravan,abraveia,abraveses,abraveses de tera,abraw,abrawa,abrawdiwurem,abrawkawfe,abray,abrayeva,abrayevo,abrayjan,abraz,abraz-pelga,abrazheyevka,abrbakoo,abre,abre campo,abre el ojo,abre mar,abrea,abrebi,abreby,abrech,abrecia,abredar,abrega,abrego,abrego numero dos,abrehdar,abrehyia,abreiro,abreita,abrejan,abrejani,abrekum,abrekuma,abrem agona,abrem ankase,abrema,abremkese,abren,abrene,abrenk,abrente,abreouanko,abrera,abres,abres va morad,abreschviller,abreschwiller,abreshia,abreshminkheyl,abresminkhel,abrest,abresua,abret,abreu,abreu e lima,abreulandia,abreus,abrevadero,abrevaderos,abrevi,abrewangko,abrewankaw,abrewanko,abrewari,abrez,abrezi,abri,abri pavie,abri tappeh,abria e eperme,abriach,abrianko,abriaqui,abriba,abrich,abrici,abricki,abricot,abricots,abrid,abriday,abries,abrieskai,abrigada,abrigak,abrigny,abrigo do bom pastor,abrigos,abrigueiro,abriha,abrije e eperme,abrije e poshteme,abrikaso,abrikosovka,abrikosovo,abrikosovskoye,abril,abrim,abrimso,abrin,abrina,abrind,abrindwala,abrine,abring,abrini,abrio,abriojal,abriojo,abriokra,abriola,abriongko,abriq,abrish,abrisham mahalleh,abrishamin khel,abrishamin kheyl,abriski,abrit,abrita,abritas,abritski,abrix,abriz,abrizaki,abrizi,abrjish,abro,abro abro,abro goth,abrobakro,abrobeano,abroborifaghe,abrobuasen,abrobuesin,abrocheri,abrochire,abrodiem,abrodiwuram,abrodul purice,abrofirim,abrofoa,abrofodoume,abrofua,abrojal,abroki,abrokofe,abrokofi,abrokwofi,abroma,abromiskes,abromkese,abron,abronambue,abrone,abronikha,abronokovo,abrosan,abrosenki,abroshan,abroshnoye,abrosikha,abrosimki,abrosimovka,abrosimovo,abrosimovskoye,abrosimovtsy,abroskinskiy,abrosova,abrosovo,abrosovy,abrosyata,abrosyevo,abrotchi,abrotorpen,abrou,abrouch,abrouj,abrouki,abrounamoue,abrounda,abrow,abrowah,abroweh,abrsaj,abru,abru morvarid,abrubalangu,abrucena,abruch,abrucinos,abrud,abrud-sat,abrud-sat-buninginea,abrud-sat-ciuruleasa,abrud-sat-soharu,abrud-seliste,abrudsat,abrudsat-cristea,abrudsat-seliste,abrueinos,abruim,abruk,abruka,abruki,abrumand,abrumase,abrun,abrunamue,abrunheira,abrunheiro grande,abrunhosa,abrunhosa do mato,abrunhosa velha,abrunhosa-a-velha,abrunhoza velha,abrupi,abrupe,abrupji,abruzovo,abrykosivka,abrykosove,abryshkino,abryskino,abryudkovo,abryushino,abryutino,abryutinsliye vyselki,abryutyevo,absagacheva,absagachevo,absal,absalamovo,absalon,absalyamova,absalyamovo,absam,absani,absar,absar-teba,absara,absaraka,absard,absardeh,absardiyeh,absari,absarina,absarokee,absberg,abschermeningken,abschlag,abschwangen,abschwaugen,abscia,abscon,absdale,absdorf,absecon,absecon highlands,absekan,absekane bala,abseker,absen,absendo,absenreute,absera,abserdeich,abserdi,abserdy,absereft,abset,abshaday,abshahi,abshar,abshar tappeh,abshar-e jadid,abshar-e pish piruz,abshar-e qadim,abshara,absharah,abshareh,abshari,abshartiba,abshekan,abshekan-e bala,absher,abshers,abshi,abshier settlement,abshikan,abshikan-e bala,abshin,abshineh,abshir,abshirin,abshish,abshofen,abshooiyeh,abshoven,abshruten,abshtaynen,abshuiyeh,abshuli,abshur,abshur-e bala,abshurak,abshvangen,absidere,absidere djado,absin,absiouf,absirin,absiti,absmann,absokheyr,absorkh,absorto,absouma,absouya,abspann,abstatt,abstein place,abstetten,abstetter hof,abstich,absug,absuj,absureh,abswoude,abtate,abtaf,abtak,abtao,abtar,abtarak,abtarazak,abtare chak,abtavil,abtay,abtei,abtenao,abtenau,abtenham,abteroda,abterode,abterp,abteta,abthausen,abthorpe,abtil,abtin,abtine,abtir,abtischrode,abtissendorf,abtissinwisch,abtlobnitz,abtnaundorf,abtoundjour,abtrakova,abtsbessingen,abtschlag,abtsdale,abtsdorf,abtsee,abtsgmund,abtsgreut,abtsgreuth,abtshagen,abtskhua,abtsried,abtsroda,abtsroth,abtstorf,abtsul,abtswind,abtswoude,abtu,abtuga,abtugak,abtujah,abtuk,abtunne,abtunneiadaman,abtupe,abtweiler,abtwil,abtwyl,abu,abu ana,abu `abali,abu `abdah,abu `abud,abu `afsah,abu `afta,abu `agag,abu `agala,abu `agub,abu `ajaj,abu `ajilah,abu `ajurah,abu `alaej,abu `alanda,abu `alandah,abu `alaq,abu `alende,abu `aleymeh,abu `ali,abu `alik,abu `alimeh,abu `amir,abu `amr,abu `amsha,abu `amud,abu `aqla,abu `arais,abu `arabid,abu `araish,abu `arbideh,abu `aris,abu `arish,abu `arran,abu `asgar,abu `ashirah,abu `asi,abu `askar,abu `auga,abu `aun,abu `auwa,abu `awai,abu `awali,abu `awali wa minshatuha,abu `awdah,abu `awja,abu `awn,abu `awwa,abu `azalah,abu `azam,abu `azim,abu `azimeh,abu `aziz,abu `azi\302\247im,abu `azlat,abu `ebadi,abu `elgayeh,abu `elwet-matamir,abu `iqab,abu `irwa,abu `irwah,abu `isa,abu `oleymeh,abu `oqab,abu `ud,abu `ujaylah,abu `ukayshah,abu `umar,abu `urays,abu `urf,abu `urug,abu `uruj,abu `uruq,abu `urwah,abu `usfur,abu `ushar,abu aachte,abu ad daghil,abu ad duhur,abu ajala,abu ajura,abu akaisha,abu al `abbas,abu al `ajaj,abu al `alaya,abu al `arish,abu al `aynayn,abu al `izam,abu al balayah,abu al fadl,abu al fahl,abu al fares,abu al fawaris,abu al fayyad,abu al gharr,abu al ghayt,abu al ghurr,abu al hasan,abu al hayaya,abu al hayyah,abu al hidr,abu al hisn,abu al juhur,abu al jurah,abu al kahf,abu al khamis,abu al khasib,abu al khassah,abu al khawi,abu al khazen,abu al khil,abu al khus,abu al kibash,abu al kurdi,abu al kusur,abu al lasan,abu al lawqas,abu al matamir,abu al matamir al bahri,abu al muhammar,abu al mujahir,abu al murayr,abu al qa`aid,abu al qa`ayid,abu al qal`ah,abu al qayn,abu al qubays,abu al qudur,abu al qurdi,abu al qusur,abu alaik,abu amara,abu ammar,abu an na`am,abu an najah,abu an numrus,abu andud,abu ar radif,abu ar rish,abu ar rish bahri,abu ar rish qibli,abu aro,abu as sakhrah,abu as salih,abu as sayyid,abu as sir,abu as suwadi,abu ash shahm,abu ash shamat,abu ash shuqaf,abu ash shuquq,abu at tuyur,abu aweigila,abu aweigla,abu az zighan,abu az zuluf,abu azi\302\247 zi\302\247uhur,abu babun,abu baghal,abu baham,abu bakar block,abu bakar park,abu bakar siddique colony,abu bakr,abu bakr as siddiq,abu bakra,abu balamah,abu bana,abu baqqal,abu baras,abu bardi,abu barka,abu basal,abu bisht,abu bogal,abu boqqal,abu botero,abu bushayr,abu chalak,abu chalal,abu chan,abu chari,abu chattaf,abu chattat,abu chaulan,abu chawlan,abu chazi,abu chenari,abu china,abu chulun,abu cureis,abu dame,abu daud al `inab,abu daud as sibakh,abu da`qi,abu dab,abu dabarah,abu dabi,abu dafnah,abu daghmah,abu dahalis,abu dakhkh,abu dali,abu daluf,abu dangal,abu danna,abu dannah,abu daql,abu daqn,abu darah,abu darba,abu darda,abu dardah,abu darikha,abu darikhah,abu darisah,abu dawaia,abu dawaya,abu dawayah,abu dawm,abu dawm sannum,abu dawmah,abu dawud al `inab,abu dawud as sibakh,abu dawud el-`inab,abu dawud el-sibakh,abu dazza,abu dazzah,abu deia,abu deiyata,abu deleig,abu deleiq,abu delelq,abu dellum al a`ma,abu dereis,abu derisa,abu dhabi,abu dhahab riyah,abu dhakar,abu dhaluf,abu dhaqn,abu dharah,abu dhiab,abu dhiba,abu dhufr,abu dhukh,abu dhuluf,abu diba`,abu diffiya,abu diffiyah,abu diflah,abu dign,abu diha`,abu dija`i,abu dik,abu dinqash,abu diqn,abu direh,abu dis,abu dishisha,abu diyab,abu diyab sharq,abu diyan,abu dom,abu dom sanam,abu dom sannum,abu doma,abu du`an,abu duba`,abu dubara,abu duda,abu dudah,abu dugur,abu dukhn,abu dulayq,abu dulu`,abu dura,abu duraykha,abu durays,abu durba,abu durbah,abu durra,abu durrah,abu durus,abu ed duhur,abu edh dhahur,abu el `ajaj,abu el fahl,abu el fawaris,abu el hasan,abu el hidr,abu el jirdhan station,abu el jurdhan station,abu el khil,abu el lasan,abu el lauqas,abu el matamir,abu el qein,abu el rish bahari,abu el rish qibli,abu el shequq,abu el-`abbas,abu el-gharr,abu el-gheit,abu el-guhur,abu el-khaui,abu el-khawi,abu el-khazr,abu el-luqas,abu el-matamir el-bahari,abu el-nagah,abu el-numrus,abu el-shugaf,abu el-shuqaf,abu el-shuquq,abu el-sir,abu en numrus,abu er rish bahari,abu er rish qibli,abu es sallih,abu es suadi,abu esh shahm,abu esh shuquq,abu esh-shuqaf,abu eshaq,abu ez zighan,abu far,abu fadl,abu fandok,abu faraj,abu farhiyah,abu fatima,abu fatimah,abu fawaghi,abu fazel,abu fazel-e zargan,abu felfel,abu feli,abu fishkah,abu folus,abu fulah,abu fulus,abu funduq,abu ga`ab,abu gabra,abu gaddur,abu gala,abu galal,abu galfa,abu gallabiya,abu gamal,abu gandir,abu garva,abu garva-e do,abu garva-e yek,abu gedaf,abu geili,abu gelba,abu gemai,abu geraniyeh-ye do,abu geraniyeh-ye yek,abu gereyneh,abu gereyneh-ye do,abu gereyneh-ye yek,abu gereyniyeh-ye do,abu gerva,abu ghalib,abu ghanim,abu ghanima,abu ghanimah,abu ghar,abu ghara,abu gharab,abu gharah,abu gharaib,abu gharaq,abu gharaq ash sharqi,abu gharb,abu gharib,abu ghaush,abu ghaylan,abu ghazi,abu gheid,abu gheneiwa,abu ghirab,abu ghizlan,abu ghosh,abu ghoveyr,abu ghraib farms,abu ghunaywah,abu ghurab,abu ghuraib,abu ghurayb,abu ghusun,abu ghuyer,abu gi`eirin,abu gibeyah,abu gileiha,abu gimri,abu ginshu,abu gir,abu girg,abu giri,abu gleiha,abu gogi,abu gorva,abu gorveh,abu griniyeh,abu gubeiha,abu gudul,abu guja,abu gullar,abu guraib,abu gureis,abu gurgas,abu gussi,abu guta,abu guya,abu habbah,abu habbih,abu hadid,abu hadid as subhani,abu hadriya,abu hadriya camp,abu hadriyah,abu hagar,abu hail,abu hajar,abu hajar a`la,abu hajar al a`la,abu hajar jazirah,abu hajar shamiyah,abu hajar ula,abu hajarah,abu hajirah,abu hakfa,abu hakfa al janubi,abu hakfa ash shamali,abu halaifa,abu halaifah,abu halaqim,abu hali,abu halifah,abu halima,abu halimah,abu halqah,abu hamad,abu hamadi al janubiyah,abu hamadi ash shamaliyah,abu hamam,abu hamed,abu hamid,abu hamira,abu hamirah,abu hamizeh,abu hammad,abu hammad al mahattah,abu hammad el mahatta,abu hammam,abu hammur,abu hamra,abu hamur,abu hamzah,abu hana,abu hanash,abu hanaya,abu handal,abu hanesh,abu har,abu haraz,abu hardan,abu hardan-e `olya,abu hardan-e sofla,abu hardub,abu hariz,abu harmala,abu hasan,abu hasani,abu hasawiyah,abu hashim,abu hashish,abu hassawiya,abu haswa,abu haswah,abu hathrah,abu hauwa,abu hawadid,abu hawr,abu hawri,abu hawwa,abu hayayeh,abu hayj,abu hayyah,abu hbaylat,abu hibeira,abu hidlan,abu higar,abu hijar,abu hijarah,abu hilalah,abu hileifa,abu himar al kabir,abu hindi,abu hinvar,abu hirab,abu hirayz,abu hireiz,abu hisani,abu hizam,abu homeyzeh,abu honeyvar,abu hor,abu horira,abu hubayl,abu hubaylat,abu hugeir,abu hujaira,abu hujar,abu hujayr,abu hujayrah,abu hulaifa,abu hulaifah,abu hulayfah,abu humaidhah,abu humaizah,abu humar el-kebir,abu humayrah,abu humeira,abu huminus,abu hummus,abu humus,abu hurab,abu hurayrah,abu hureira,abu husayyah,abu hutaybah,abu huwayshah,abu imlaih,abu islam,abu ja`fari,abu jabbar al kabirah,abu jabbar as saghirah,abu jabbar kabirah,abu jabir,abu jabra,abu jabrah,abu jadda,abu jadhah `atiq,abu jadhah kubra,abu jakkah,abu jalal,abu jalal-e janubi,abu jalal-e jonubi,abu jalal-e shemali,abu jalal-e shomali,abu jalfah,abu jallabiyah,abu jamuseh,abu jandir,abu janlu,abu jaradah,abu jaradi,abu jard,abu jaridah,abu jassa,abu jayid,abu jayli,abu jibeha,abu jidh,abu jilal,abu jinshu,abu jir,abu jirj,abu jisra,abu jisrah,abu jissah,abu jodey`,abu jorreyheh va rahimi,abu jubayhah,abu jumayy,abu junshu,abu jurayf,abu jurayn,abu jurays,abu jusaysah,abu juwari,abu ka`b,abu kaba shab,abu kabarah,abu kabir,abu kabirah,abu kabireh,abu kabiri,abu kabiriyeh,abu kabisa,abu kabra,abu kabrah,abu kahf,abu kala,abu kalbush,abu kalhan,abu kalkal,abu kamal,abu kammash,abu kannse,abu karinka,abu karish,abu karma,abu karmah,abu kartod,abu katulah,abu kawlan,abu kaylah,abu kebeida,abu kebir,abu kemal,abu kerkae,abu kershola,abu khais,abu khadiga,abu khadijah,abu khadir,abu khadrawi,abu khalifah,abu khalil,abu khamira,abu khamirah,abu khamsin,abu khan,abu khanadij,abu khanadiq,abu khanadiq al qibli,abu khanadiq ash shamali,abu khanatidj,abu khanatij,abu khanazir,abu kharab,abu kharash,abu kharjah,abu kharnub,abu khashabah,abu khaskheli,abu khattar,abu khayis,abu khazrari,abu khazravi,abu kheyl arateh,abu kheyl-e arateh,abu khoreis,abu khsos,abu khudeira,abu khulayf,abu khunayzah,abu khurayzah,abu khureis,abu khurjah,abu khurs,abu khusayfa,abu khuwayt,abu kirkas,abu kirsh,abu kisah,abu kolo,abu koto,abu ksah,abu ku`,abu kufuf,abu kuk,abu kulaywat,abu kuleiwat,abu kullus,abu kunaysh,abu kuneish,abu kuraiyim,abu kurayyim,abu kussi,abu kutni,abu lamis,abu layl,abu liyah,abu luk,abu ma`raj,abu ma`rij,abu mahalleh,abu mahir,abu mahmud dahham hammad,abu mahsuj,abu makhruq,abu makhtub,abu makkah,abu malik,abu mallo,abu mandil,abu mandur,abu mangug,abu manjuj,abu manna` bahari,abu manna` bahri,abu manna` gharb,abu manna` qibli,abu manna` sharq,abu mannsaf,abu maqbarah al kabirah,abu maqbarah as saghirah,abu mar`i,abu maragh,abu mareiqa al haggar,abu maria,abu mariyah,abu markha,abu markhah,abu marw,abu marwa,abu marwah,abu mas`ud,abu mashhur,abu mashilish,abu masuj,abu masul,abu matariq,abu matirik,abu matis,abu maugud,abu meda,abu mendi,abu merafib,abu mikhail,abu mina,abu misannatayn,abu mizan,abu mombasi,abu mosheyleysh,abu mu`ayt,abu muhammad,abu mujhim,abu muqud,abu musa,abu mustafa,abu na`ama,abu na`amah,abu nagah,abu nageh,abu najm,abu nakhla,abu nakhlah,abu namil,abu naml,abu narsi,abu nasr,abu natuliyah,abu naytulah,abu na\302\261`mah,abu neseir,abu nesyan,abu nish-shaba,abu njaym,abu nujaim,abu nujaym,abu nukhayl,abu nusayr,abu nuseir,abu nushshabah,abu nuwara,abu nuweira,abu nuwwarah,abu nyanya,abu odham,abu ol `abbas,abu ol baq,abu ol fares,abu ol fath,abu ol fathabad,abu ol fazl ol `abbas,abu ol firuz,abu ol folus,abu ol fulus,abu ol hasan,abu ol hasan kala,abu ol hasan kola,abu ol hasanabad,abu ol hasani,abu ol hayat,abu ol khazen,abu ol kheyr,abu ol ma`ali,abu ol ma`man,abu ol mehdi,abu ol qasem,abu ol vafa,abu ol vafai,abu ol vardi,abu ol verdi,abu omar,abu os kheir,abu ourein,abu patti,abu qa`ar,abu qa`b,abu qa`tur,abu qal,abu qal`ah,abu qalqal,abu qalta,abu qaltah,abu qamhah,abu qanadil,abu qandul,abu qantarah,abu qaramit,abu qarayt,abu qarhah,abu qarib,abu qarn,abu qasaib,abu qash,abu qashsh,abu qatatah,abu qattaf,abu qawi,abu qir,abu qirfah,abu qissah,abu qoveyr,abu qubays,abu qudaf,abu qulayhah,abu qulut,abu qurayn,abu qurays,abu qurein,abu qurqas,abu qurun,abu qussa,abu qussah,abu qussi,abu quta,abu qutah,abu quyer,abu ras,abu rabha,abu rabih,abu radif,abu ragwan,abu ragwan el-bahari,abu ragwan el-qibli,abu rahmah,abu rajab,abu rajash,abu rajwan al bahri,abu rajwan al qibli,abu rakhei,abu rakhia,abu ramad,abu rameinak,abu ramla,abu raqaba,abu raqabah wa kafruha,abu rasayn,abu rauwash,abu rawwash,abu raydah,abu regeiba,abu reida,abu rhatte,abu rida al hawis,abu rijum,abu rimal,abu ris,abu rish,abu rishah,abu risyan,abu riyah,abu road,abu roash,abu ruayl,abu ruba,abu rubah,abu rubaiq,abu rubayq,abu rubays,abu rudays,abu rudeis,abu ruf,abu rukab,abu rukba,abu rukbah,abu rukha,abu rukhayy,abu rumaila,abu rumayl,abu rumaylah,abu rumeil,abu rumeila,abu rumman,abu ruqaybah,abu ruqeiba,abu rus,abu ruwash,abu ruwaysh,abu ruweish,abu sa`d,abu sa`di,abu sa`id,abu sa`id-e sabbaned-e yek,abu sa`id-e sabbaneh-ye do,abu sa`id-e sabbaneh-ye yek,abu sab`a,abu sab`ah,abu sabah,abu sabah mas`inah,abu sabaya,abu sabayah,abu sabib,abu sadda,abu saddah,abu sadreyn,abu saeed,abu safaih,abu saffar,abu saht,abu saibi,abu said,abu saida,abu saida as saghir,abu saif,abu saiya,abu saiyid,abu sakhar,abu sakhr,abu sakhrah janubi,abu sakhrah shamali,abu salabikh,abu salalah,abu salama,abu salih,abu salijan,abu salijeh,abu salikhan,abu salim,abu saljeh,abu salji,abu sallih,abu salma,abu salman,abu samada,abu samah,abu samak,abu samand begu khel,abu samand kalla,abu samrah,abu sanam,abu saneita,abu sari,abu sawahim,abu sawamin,abu sawar sakhr,abu sawr,abu saybi`,abu saydah,abu saydah as saghir,abu sayr,abu sayyah,abu se`id,abu seba,abu seiba,abu seir,abu selala,abu seley bikhat,abu seleyb khan,abu seleyb khan-e kuchek,abu senan,abu seyleh-ye sofla,abu seyneh,abu seyveh,abu sha`ib,abu sha`ir,abu shabbanah,abu shagar,abu shaibana,abu shaitana,abu shajar,abu shakar,abu shakhrah,abu shakka,abu shakkah,abu shakkal,abu shalaya,abu shaluk,abu sham,abu shama,abu shamah,abu shambalanah,abu shambayanah,abu shamsiyah,abu shanab,abu shanak,abu shandi,abu shanek,abu sharban,abu shari`ah,abu sharibah,abu sharji,abu shash,abu shatat,abu shateir,abu shattaf,abu shattah,abu shauk,abu shauka,abu shawag,abu shawar khanjar,abu shawk,abu shawkah,abu shawlan,abu shaylam,abu shaytanah,abu shehr,abu sheitana,abu shekuk,abu shenan,abu shenan-e `olya,abu shenan-e bala,abu shenan-e pain,abu shenan-e sofla,abu shendi,abu sheneina,abu sheri`a,abu shidar,abu shifyat,abu shifyat al mar`at,abu shimbalana,abu shineina,abu shinena,abu shok,abu sholug,abu shotta,abu shu`ayb,abu shu`aylim,abu shueidin,abu shugeira,abu shujayr,abu shujayrah,abu shukhaydim,abu shukheidim,abu shunayb,abu shunaynah,abu shuneib,abu shurban,abu shusha,abu shushah,abu shutayr,abu shuteir,abu shuwaiha,abu shuwaiir,abu shuwair,abu shuway`ir,abu shuwayhah,abu shuwayr,abu shuwayy,abu shuwayyir,abu si`ayfah,abu si`eifa,abu sidayrah,abu sidhum,abu sidrah,abu sikkin,abu silal,abu simadah,abu simbel,abu simbil,abu sinan,abu sinbil,abu sineita,abu sinun,abu sir,abu sir al malaq,abu sir bana,abu sir difinnu,abu sir el malaq,abu sirba,abu snaytah,abu sokheyl,abu sokheylat,abu sokheyr,abu sokheyrat,abu su`ayfah,abu subah,abu sudayrah,abu sufyan,abu sukhair,abu sukhayr,abu sulal,abu sultan,abu sumaich,abu sumaych,abu sumayj,abu sumayk,abu sumbul,abu sunaytat,abu sunbul,abu suneina,abu suneitat,abu suqra,abu suqrah,abu surug,abu suwaymin,abu suwayr al mahattah,abu suwayy,abu suweir-el-mahatta,abu tababir,abu tabanu,abu tabaq,abu tabareh,abu tabat,abu tabbah,abu tabikh,abu tabl,abu tagara,abu taha,abu talh,abu talhah,abu talib sipio,abu taltal,abu tamada,abu tamadah,abu tamor,abu tamr,abu tamur,abu tarbeh,abu tarfaiyeh,abu tarfayeh,abu tarfayeh-ye pain,abu taridah,abu tarrahah,abu taryeh,abu tavayoj,abu tavij,abu tavil,abu tawala,abu tawil,abu tayur-e do,abu tayur-e seh,abu tayur-e yek,abu teyur-e do,abu teyur-e yek,abu thalathin,abu thayjih,abu thaylah,abu tibban,abu tig,abu tiga,abu tij,abu tin,abu tina,abu tinah,abu tineitin,abu tisht,abu tong,abu torab,abu torfayeh-e `olya,abu torfayeh-e bala,abu torfayeh-ye bala,abu tovayyej,abu toveyj,abu trabe,abu tsofo,abu tufayjah,abu tughara,abu tugharah,abu tughrah,abu tuhayjah,abu tuhayji,abu tunaytin,abu turaba,abu turabah,abu turay`ah,abu turei`a,abu tutah,abu tuwala,abu tuwalah,abu tuwayjah,abu tuwayqiyah,abu umm bashir,abu umuri,abu wajnam,abu wali,abu winni hamzabbak,abu yahya,abu yam,abu yaqfan,abu yasin,abu yelayyel,abu yeleyel,abu yelyel,abu yihya,abu yord,abu yowru,abu yurev,abu yuru,abu yusuf,abu za`bal,abu zaabal,abu zabad,abu zabi,abu zabye,abu zabyo,abu zagholi,abu zahif,abu zahr,abu zaid,abu zaida,abu zaidabad,abu zanimah,abu zar,abu zarad,abu zarawi,abu zariba,abu zaribah,abu zawr,abu zayd,abu zayyan,abu zeid,abu zeid hasan,abu zeid wad umm balli,abu zeira,abu zenima,abu zeydabad,abu ziyyan,abu zobeydabad,abu zor,abu zua,abu zubaid,abu zubayd,abu zukhayr,abu zulabah,abu zullayq,abu zulleiq,abu zuluf,abu zuneim,abu zurayq,abu zurayqiyah,abu zurug,abu zuruk,abu zuwayd,abu zuwayr,abu-gebeiha,abu-ghawsh,abu-hleifa,abu-qirqas,abu-senan,abu-shakka,abu-shushah,abu`aleymeh,abua,abuabad,abuac,abuadi,abuag,abuakwa,abuakwankwanta,abualan,abuamni,abuan,abuan kangin,abuanan,abuanokope,abuanu,abuanu brofoyedru,abuari,abuaro,abuaru,abuasa,abuasu,abuatan,abuato confluence,abuaya,abuba-nando,abubakirova,abubakirovo,abubakr,abube,abube-nando,abubecher,abubertan,abubo,abubu,abubutan,abucari,abucay,abucay hacienda,abucay norte,abucay north,abucay south,abucay sur,abucayan,abucea,abuchanlu,abuchen,abuchenso,abucher,abuchi,abuchi gwari,abucot,abud,abud as saiyid,abudag,abudaga,abudala,abudama,abudarabayan,abude,abudem,abudeya,abudi,abudia,abudiyeh,abudo,abudom,abudu,abudu-isu,abuduka,abudure,abuduski,abue korpe,abuek usong,abuela,abuelo,abuen,abuenu,abuer,abuesi,abuetare,abufari,abufary,abufaw,abuga,abugaga,abugan,abugassah,abuge,abugebo,abugga,abugi,abugon,abuha,abuhatta,abuheyl-e arateh,abuhta,abui,abuiata,abuife,abuille,abuime,abuipe,abuipi,abuislah,abuj,abuja,abujaga,abujao,abujerd,abujon,abujop,abuketi,abuka,abukanai,abukasah,abukatar,abuke,abukhalo,abukharmala,abuki,abukir,abukiri,abuko,abukrom,abuku,abukun,abukur,abul,abul fazl al `abbas,abul haiya,abul ivur,abul kabash,abul kharana,abul khasib,abul khel,abul mir bahr,abul qurdi,abul uruj,abul wafai,abulkhasan,abula,abulabad,abulabiri,abulalas,abulam,abulan,abulas,abulbahar,abulbart,abule aborisade,abule aja,abule alagbede,abule alari,abule alfa,abule apata,abule apena,abule awba,abule basi,abule bello,abule egba,abule eruwa,abule idi,abule ijesha,abule iya agba,abule jiga,abule lawal tapa,abule mesan,abule nla,abule oba,abule odo,abule ogunremi,abule ojo,abule okoru,abule olola,abule soro,abule tailor,abule-nla,abuleg,abuleta,abuleyil,abulfatehwali,abulhasan,abulhayirsayaca,abuli,abulie,abulkhair,abulkuri,abullahpur kanar,abulo,abulog,abuloma,abultayevo,abulu,abulug,abuluku,abulyaisovo,abulyont,abulzinho,abum,abuma,abumandjale,abumane,abumaneh,abumayeme,abumbem,abumbul,abumensok,abumeyeme,abumnsoc,abumnura,abumombazi,abumonbazi,abumpong,abumpungeng,abumwenre,abun,abun hali,abuna,abuna abiesghi,abuna chico,abunabun,abunai,abunan,abundancia,abunde,abune aregay,abune yosef,abunemri,abunerok,abung,abungabung,abungabung logging camp,abungari,abungo,abungon,abungoro,abunheira,abuni,abunkamin,abunkamun,abunkpa,abunlife,abuno,abunshie,abunwa,abuo,abuochichi,abuohia,abuol ma`ali,abuontem,abuor,abuoso,abuoy,abupubuma,abupur,abuqin,abuqrin,abur,abura,abura dunkwa,abura`,aburahu,aburahuin hural,aburahuin sume,aburakawa,aburakhoyn khid,aburakhoyn khural,aburakhyyn khid,aburakhyyn khural,aburankwanta,aburas,aburaso,aburatsu,aburazhki,aburbas,abure,aburene,aburgain-khural,aburgayn khural,aburgen-sume,aburges,aburgesh,aburgos,aburi,aburi amamfo,aburi ekuraa,aburiamanfu,aburiq,aburkandi,aburkot,aburo lakho,aburochnoye,aburonnoye,aburosi,aburriin,aburu,aburuburu,aburufe,aburugen yashiro,aburukiri,aburumaku,aburumbede,aburumbidi,abus,abusa,abusaban,abusalixan,abusar,abusar ka bas,abuschehr,abusejo,abusem,abusera,abush-abad,abushabad,abushka,abushkah,abushkan,abushkovo,abushulo,abusidah,abusit,abusit 1,abusit 2,abuskheylat,abuslar,abusoro,abusoxayr,abusta,abusu assi,abut,abuta,abutalipovo,abutang,abutanga,abutareye,abutari,abutda,abute,abuti,abutia kloe,abutia teti,abutiao,abutifi,abutkovo,abuto,abutokom,abutorab,abutorabnagar,abutreira,abutu,abutuwa,abutvani,abuu,abuu sheegow,abuukar xaaji figow,abuura,abuusagi,abuvelnya,abuviah,abuvo,abuwala,abuwangala,abuxarda,abuya,abuya y ceuta dos,abuyama,abuyanrin,abuyar,abuye,abuye atir,abuyerin,abuyesan,abuyo,abuyog,abuyogay,abuyoin,abuyon,abuz,abuzaderas,abuzam,abuzar,abuzar-e ghaffari,abuzarabad,abuzariyeh,abuzawbah,abuzeyt,abuzhuer,abuzlar,abuzome,abvala,abvar,abvelebe,abveysan,abvounougou,abvoura,abwa,abwab,abwadi,abwaia,abwal,abwangete,abwantem,abwar,abwatountora,abwatuntora,abweiler,abwel,abwia,abwinden,abwinkel,abwinkl,abwong,abwonq,aby,aby adi,aby zniema,abya,abya bode,abya thaiktugon,abyad,abyadain ba `amr,abyadayn ba `amr,abyadh,abyan,abyaneh,abyang,abyar,abyar `ali,abyar as sababil,abyat al a`la,abyauk,abyazabad,abyazan,abybro,abydel,abydus,abyei,abyek,abyek olya,abyek sofla,abyek-e `olya,abyek-e bala,abyek-e pain,abyek-e sofla,abyek-e vasati,abyen,abyey,abyfors,abyggandan,abyggeby,abyhoj,abyholm,abyi,abyit,abyitkon,abylay,abylund,abylystem-abi,abymes,abyn,abyor,abyraly,abyrge,abysater,abyshabad,abyshbeyli,abysheva,abyshevo,abyshkan,abyshtu,abyskan,abyskov,abyssinia lines,abyssynka,abytorp,abyuha,abyukope,abyungni,abyuqa,abyuqah,abyy,abyz,abyz-tyube,abyzova,abyzovo,abz,abza,abza-e dudera,abza-e sar dasht,abzac,abzagor,abzai,abzak,abzakova,abzakovo,abzal,abzali-e kuchak,abzali-ye bozorg,abzalu,abzan,abzanova,abzanovo,abzar,abzat,abzayevo,abzeg,abzelilovo,abzelilovskiy zagotskot,abzha,abzhaaptra,abzhabtra,abzhakva,abzhan,abzhaqva,abzhay,abzhingay,abzi khodai,abzi khuda,abzi khudai,abzi-kula,abzikhudai,abzir,abzo dheri,abzuluiyeh,abzyabar,ac cadan,ac ckhirat,ac-cadin,ac-chafe,aca,aca vera,acaa,acaabat,acaam,acaasi,acaban,acabara,acabazar,acable,acaboa,acabouille,acabual,acaca,acacar,acachahuane,acachapa,acachapa y colmena,acachapa y colmena cuarta seccion,acachapa y colmena primera seccion,acachapa y colmena segunda seccion,acachapan,acachare,acache,acachi,acachila,acachilo,acachuame,acachuen,acacia,acacia creek,acacia ridge,acacias,acacihuyuk,acacihuyuk koyu,acacio,acacoto,acacoyagua,acacoyahua,acadanlar,academia,academie,academy,academy acres,academy estates,academy garden,academy heights,academy hill,academy hills,academy junction,academy lane,academy park,academyville,acadia,acadia valley,acadian villa,acadjama,acadjame,acado,acafora,acagal,acaguaque,acahai,acahay,acahual,acahualco,acahuale,acahuales,acahuame,acahuane,acahuani,acahuasco,acahuates,acahuato,acahuen,acahuene,acahuizotla,acahuta,acai,acai akaoua,acaia,acaiaca,acain,acaita,acaituba,acaja,acajala,acajete,acajiriba,acajou,acaju,acajutiba,acajutla,acakhel,acakwale,acakza`i,acakzo soznay,acal,acala,acala del rio,acalaca,acalak bandeh,acalama,acalamatitla,acalapa,acalar,acalavana,acalayon,acalayong,acalcane,acalco,acali,acalita de ameca,acalitos,acallario,acalman,acalmani,acalpican,acalpican de morelos,acaltepec,acam,acama,acamal,acamanumu,acamazar,acambaro,acambay,acambuco,acami,acamilpa,acamixtla,acamixtlan,acamoa,acamovici,acamp montegiro,acampamento,acampamento cha mocambique,acampamento grande,acampamento sena sugar,acampamonte,acampo,acamuchitlan,acan,acana,acanabor,acanabot,acanane,acanaxjab,acanceh,acancocha,acancun,acandi,acanea,acang,acangiz,acanhe,acanho,acani,acanlar,acanoa,acansip,acanti,acantilado,acanu,acanuari,acao,acaoba,acapa,acapampa,acapan,acapantzingo,acapesket,acapetagua,acapetahua,acapetlahuaya,acaponeta,acaponetilla,acaponetillo,acapral,acapralito,acapucai,acapulco,acapulco de juarez,acaquila,acar,acara,acara mariam,acara-mirim,acarabe,acaracare,acarahmetbey,acarahu,acarahy,acarai,acarape,acarapini,acaraska,acarau,acaray,acaray hacienda,acarcare,acardece,acare,acareve,acari,acaricocha,acaricuara,acarigua,acarkarasuleymanli,acarkent,acarkhel,acarkoy,acarkoyahmetbey,acarlar,acarmantas,acaro,acaroba,acarobasi,acarotan,acarouani,acarouany,acarpinar,acary,acaryurt,acas,acasaguastlan,acasia,acasico,acasio,acassuso,acaster malbis,acasto,acatan,acatar,acatari,acate,acatempa,acatempam,acatempan,acaten,acatenango,acateno,acatepec,acatepeo,acatepulco,acathuta,acatic,acaticpac,acatipa,acatita,acatita grande,acatitan,acatitlan,acatitlan de zaragoza,acatito,acatla,acatlajapa,acatlan,acatlan de juarez,acatlan de la cruz,acatlan de osorio,acatlan de perez figueroa,acatlan del rio,acatlima,acatlipa,acatlixhuayan,acatzingo,acatzingo de hidalgo,acatzingo de la piedra,acau,acaua,acaua de baixo,acavilla,acaxman,acaxochitlan,acaxtlahuacan,acaxtlahuacan de albino zertuche,acaxuchitlan,acaxur,acaxuroba,acayahualco,acayali,acayuca,acayucan,acayutlan,acazonica,acazuchitlan,acazulco,acbasi,acben,acbo,acbunar,acbunarul mic,acca,accadanlar,accadia,accana georghis,accarapatam,acce punco,accedang,acceglio,accent homes mobile home park,accent mobile home parks,accepata,accerane,access,accettura,accevo,accha,acchabal,acchabamba,acchabamba hacienda,acchanca,acchapuna,acchar,acchera,acchila,acchilla,acchimachay,acchiscio,accho,acchupata,acciani,acciano,acciarella,acciaroli,accident,accio,accioly,accitinquina,acclainville,accmay,acco,acco cunca,acco rana,acco sututo,acco-pampa,accocancha alto,accoccasa,accocerca,accocucho,accocunca,accocunta,accohuasa,accohuaycco,accokeek,accokeek acres,accola,accolans,accolay,accoma,accomac,accomarca,accomocco,accompong,accompong maroon lands,accompong maroon village,accompong town,accons,accontana,accopampa,accopata,accopinata,accopunco,accopuquio,accorane,accord,accoro,accosanqui,accositanga,accosora,accota,accotana,accotink,accotink heights,accotira,accous,accoville,accoytanca,accpitan,accra,accrington,accrour bridge,accsahua,acctapa,accua,accuata,accuba,accum,accumersiel,accumoli,accusilian,accusillian,acdif,ace,ace mobile home park,acebal,acebeda,acebedo,acebeiro,acebeiros,acebes,acebes del paramo,acebo,aceboso,acebro,acebuchal,acebuche,acebuches,aceca,acececa,acedera,acedillo,acedo de los caballeros,acedre,acegua,acehuche,aceifaos,aceite,aceites,aceitico,aceitillal,aceitillo,aceituna,aceituna de coromoto,aceitunas,aceituno,acek,aceka,acekzai,acekzi,acekzi gharbi,acelasca,acelin,acellana,acellasca,acelotla,acem,acemanis,aceme,acemhuyugu,acemhuyugu koyu,acemi,acemkoy,acemler,acemli,acemloros,acenan,acenchal,acenluoro,acensa,acension,acequia,acequia alta,acequia de cochea,acequia salada,acequias,acequias abajo,acequias arriba,acequioncito,acequiones,acer,acera,acera de la vega,acerca,acercani,acered,acereda,acerenza,acereski,acereto,acerida,acerkoy,acerkoy kara suleymanli,acerkoy mehmetbey,acerno,acero,acero patjata,aceroccollo,acerra,acersif,aces,acesilo,acesita,aceska,acetico,aceuchal,aceveda,acevedo,acevedo y gomez,acforcid,ach,ach epi abenin,achaba,achadara,achajur,achalako,achalsheni,achane,achawach,achewa bado,achigvara,achindow,achkhoti,achlat,achmarda,acha,acha icha,acha kwel,acha mily,acha pir,acha-kaindy,acha-kayyngdy,acha-mayli,achaa,achaari,achab,achaba,achabazar,achabou,achaca,achaca-ezi,achacachi,achacachi chico,achacala,achacalane,achacana,achacanaiyoc,achacanayoc,achacarca,achacco,achachda,achachi,achachka,achachorcro,achaco,achaco bajo,achaco hacienda,achacollo,achacota,achacota hacienda,achacpujo,achacuas,achacune,achacutana,achad,achada,achada baleia,achada de cima,achada de gaula,achada do burro,achada do gamo,achada do marques,achada grande,achada monte,achadas da cruz,achadh an iuir,achadh ur,achadinha,achadong,achadovka,achadovo,achadzhur,achae,achafubil,achagal,achagarve,achagate,achagol,achaguas,achaguay alto,achaguay bajo,achaguay medio,achagvay,achah,achah bazar,achah khel,achah mazar,achah mily,achahkheyl,achahmazar,achahnu,achaho,achahoish,achahor,achahue,achaich,achaichaila,achain,achair,achairka,achairpa,achairskiy,achajrane,achak,achakasy,achakaya,achakhel,achakheyl,achaku,achakva,achakwale,achakza`i,achakzay,achakzusoznay,achal,achal senaki,achal,achala-ezi,achalaca,achaladair,achalak,achalak bandah,achalak bandeh,achalakhbanda,achalchhila,achalco,achalda,achale,achalkalaki,achalke,achalla,achalla-owa-olo,achallader,achallcha,achalm,achaloy,achalpur,achaluk,achaluke,achalwali,achalziche,achalzych,acham,achama,achama,achamayle,achamazar,achambe,achamda,achamdako,achame,achami,achamidako,achamocco,achampet,achamssiye,achan,achan ika,achana,achanakmar,achanalt,achanan,achancayra,achandara,achane,achaneas,achanga,achangda,achani,achanizo,achankapur,achankovil,achanori,achanpi,achanti,achanu,achany,achanyak,achao,achaouia,achaouiane,achap,achapa,achapampa,achapata,achapatja,achaphubuil,achapicuta,achar,achar bakho,achar bhutto,achar brohi,achar dablo,achar dal,achar hingorjo,achar hingoro,achar jakrie,achar jat,achar jo goth,achar kalhora,achar khan jamali,achar khaskheli,achar khoso,achar mohano,achar parai,achar pauhar,achar sammun,achar sawayo,achar schwal,achar shoro,achara,achara ndiagu,achara ndiagu omegio,acharaa,acharaba,acharacle,acharapokam,acharat,achargaon,acharheu tachli huyuk,achari hinukwewa,achari ihalagama,achari madatugama,acharichankpume,acharigama,acharigoda,achariheenukwewa,acharikelegama,acharikonwewa,acharis-agmarti,acharisaghmarti,acharistskhali,acharistsqali,acharjeepur,acharjyapur,acharkete,acharketi,acharkheyl,acharn,acharnae,acharne,acharosson,acharra,acharrape,acharting,acharu,achas,achash,achasil,achasta,achasu,achat,achat shoro,achataries,achatayhua,achatel,achates,achati,achatime,achatzberg,achau,achau-muhle,achausa,achaval,achavi,achavico,achaw,achawa,achay-vayam,achaya,achaypina,achbach,achbalou,achbaro,achbaro n ifeuzaren,achbaro n ifouzaren,achbaro n ifouzarene,achbarou,achberg,achbreck,achcahuasi,achcarajay,achchamulai,achchan,achchankulam,achcharapakkam,achchelu,achchen,achchi,achchiq,achchirikulama,achchuveli,achchygyy,achchygyy-aly,achchygyy-kyramda,achchygyy-nyuldyu,achchygyy-sen,achcnankulam,achdad,achdir,achdorf,ache,ache kouassi,achea thik,achebe-biyong,acheberg,acheca,achech,acheche,acheddal,achedepel,achedeperi,achehbazar,achek,achekanka,achekanskiy,achekha,achekrade,achel,achelai,achelap,acheleia,achelinada,achelman,achelouf,achelriede,achelstadt,achelu,achem,achemes,achemfo,achempim,achempin,achen,achenbach,achenbacher furt,achendorf,achendown,achene,achene mariam,achenfosse,achenfu,acheng,acheng shi,achengchen,achenghsien,achenheim,acheni,achenim,acheniner,acheninver,achenkirch,achenlohe,achenmuhle,achenouma,achenrain,achensee,achentee,achentoul,achenwald,achepi,achepo,achepo bouchou,acher,acherabo,acheral,acherensua,acheres,acheres-la-foret,achereshki,achereshni,acherfie,acheri,achering,acheritou,achern,acherodene,acheron,acherousia,acheroya,acherraa,achert,acherting,achery,acheshka,acheshkah,acheshon,acheson,achet,acheta,achete,acheul,acheux,acheux-en-amienois,acheux-en-vimeu,acheve,acheville,achey,achfa,achfarrie,achfarry,achfary,achh,achha tar,achhabal,achhabhaya,achhadnagar,achhadnasar,achhahad,achham,achhanulla munshir kandi,achhanulla munsir kandi,achhawal,achhayyapalli,achhe pirwala,achhelankhera,achher,achhi masit,achhi masit rahu,achhi mesit,achhibal,achhim patuli,achhimunda,achhina,achhkoro,achhnera,achhota,achhrakian,achhral,achhril,achi,achi achi,achi ayi,achi darreh,achi mouan,achi yao pail,achi yapi,achi-bulak,achi-kol,achi-sai,achia,achiachiga,achiafe,achiafi,achiafime,achiaiki,achiakrom,achiaman,achiang,achiankua,achiano,achiasewa,achiasi,achibarabat,achibiasi,achica abajo,achica arriba,achich n ait sahia,achich nait yahia,achich nait yahya ou moussa,achich nait yazza,achichere,achichiang,achichilquito,achichipico,achichipil,achichipilco,achichitavo,achico,achicourt,achicuiri,achida,achido,achiebo koua,achiekpe,achiekpo,achiekrom,achien,achiencheng,achiet-le-grand,achiet-le-petit,achifa,achifane,achifeten,achigaya,achigca,achigek,achigogo,achigok,achigonni,achigor,achijane,achik,achika,achikaratu ubandawaki,achiko,achikopoui,achikrom,achiksu,achikul,achikulak,achikulakskoye,achikzai,achikzi,achil,achila,achilafia,achilet,achilgbenda,achill,achill sound,achille,achillea,achilleion,achilles,achilo,achiltibuie,achim,achim dada,achim maldalari,achimchi,achimelan,achimelang,achimeleku,achimeque,achimetesti,achimfu,achimgari,achimgol,achimilang,achimkrom,achimmot,achimni,achimota,achimota village,achimoto,achimova,achimovo,achimovy,achimovy pervyye,achimovy vtoriye,achimovy vtoryye,achimpa,achin,achina,achinae,achinakrom,achinamisa,achinayili,achinde,achindew,achindou,achinduich,achine,achinery,aching,achinghat,achini bala,achini pain,achinkouto,achinkoutzu,achinokho,achinsk,achinsk pervyy,achinsk vtoroy,achinskiy,achintanagar,achintapur,achintee,achintoul,achintpur,achintraid,achintyanagar,achinyile,achio,achiogh,achiotal,achiote,achiotepec,achiotes,achiotes jumay,achiotillal,achipina,achipinto,achipiri,achique,achiquihuictla,achiquihuititla,achiquihuitla,achiquiri,achira,achiral,achirales,achiralito,achiras,achiremade,achireshki,achiri,achirichi,achirigama,achirik,achiriyagama,achiriyakotuwa,achirskiye,achirskiye yurty,achiry,achisa,achisay,achisayskiy,achishkah,achisil,achisu,achit,achitar,achitas,achitasu,achitavo,achitchaung,achitende,achiteti,achito,achitos,achitskoye,achiul,achiutla,achiutuin dugang,achive,achivwa,achiwen,achiwib,achiwuib village,achiyech,achka,achkanis,achkarren,achkasova,achkasovo,achkatsa,achkhaan,achkhlu,achkhoi-martan,achkhoy-martan,achkitin,achkosa,achkoute,achkvistavi,achladea,achladeai,achladia,achladies,achladochorio,achladochorion,achlain,achlare,achlari,achldorf,achleit,achleiten,achleithen,achli,achlighness,achlikul,achlikulevo,achlu,achlulu,achlum,achlyness,achmant,achmarine,achmas,achmashnyo,achmat et tahtani,achmatowo,achmelvich,achmer,achmetaj,achmita,achmizan,achmore,achmuhl,achna,achnaba,achnacloich,achnacroish,achnagarve,achnahanat,achnahannet,achnapalli,achnaq,achnasheen,achnastank,achner,achnera,achness,acho,acho nda,achoacorral,achoapata,achoata,achocalla,achochobuoso,achocoatlan,achod,achodden,achoertupo,achoho,achojpaya,achok,achokoa,achokoua,achol,acholibar,acholibur,acholl,acholshausen,achoma,achomai,achombi,achomitz,achomya,achon,achondong,achonfosse,achongh,achonry,achop,achoquen,achor,achord,achoro ndiagu,achorot mensor,achota,achotal,achotal de moreno,achotan,achotar,achote,achotes,achotla,achotlan,achoua,achouaden,achouadene,achouaker,achouba,achoucco,achouffe,achough,achouifat,achouirou,achouka,achoumar,achouria,achowa,achowa acherim,achpach,achqoute,achra,achracha,achrafie,achrafie el abbas,achrafieh,achrafiye,achrafiyet el aabbas,achrafiyet el aabbass,achrafiyet el ouadi,achrafiyet sahnaya,achrafiyet sanaya,achrain,achrala,achram,achrani,achrauli,achreboanda,achremowce,achriesgill,achrjansko,achro banda,achrobanda,achrobuoana,achrymowce,achseh-ye bala,achseh-ye pain,achselschwang,achsenried,achsheim,achslach,achstetten,acht,achtanisowskaja,achtaounouaghene,achtarak,achtel,achtelsbach,achtendries,achtendriesch,achtepol,achter de vree,achter den berg,achter den houw,achter den kraanberg,achter het heiderveld,achter het klooster,achter olen,achter weert,achter-den-bergh,achter-lindt,achter-oolen,achter-thesinge,achter-weel,achterberg,achterbist,achterbos,achterbosch,achterbroek,achterdeich,achterdiep,achterdijk,achterdrempt,achtereind,achteresch,achterheide,achterheide-asdonk,achterhoek,achterhorn,achterhroek,achterhuck,achterle,achterlee,achterlo,achtermeer,achterndiek,achterneed,achternholt,achternmeer,achterom,achterrot,achterschlag,achtersloot,achterstadt,achterste brug,achterste diesdonk,achterste distelberg,achterste erm,achterste hees,achterste heide,achterste heikant,achterste hermalen,achterste hostie,achterste rith,achterstehoek,achtersten-hostie,achterveld,achterwehl,achterwehr,achterwetering,achthal,achthofen,achthofenerdeich,achthoven,achthuizen,achtmaal,achtopol,achtrup,achtsehoek,achttienhoven,achtum,achty,achtyrka,achu,achu khel,achu-yelga,achuakan,achual tipisca,achuan,achuang,achuangtsun,achuankeng,achuankeng (1),achuankeng (2),achuantsun,achuapa,achubwekope,achucala,achuchata,achuche,achuchinhanzi,achuchuso,achuelo,achuentza,achuev,achuia,achukeviche,achullane,achuluni,achum,achuma,achumane,achumani,achumet,achumoyo,achun,achuna,achuneboana,achung,achungi,achuni,achuntza,achunwa,achupa,achupallane,achupallas,achupil,achuppallas,achura,achuranja,achurch,achureboana,achuris,achuru,achuta,achutana,achute,achutupo,achutupo numero 2,achutupo numero dos,achutupo numero uno,achutupu,achuutaiin dugang,achuwa,achuyev,achuyevo,achuyevskiye rybachiy promysel,achuyo,achvatin,achwa,achwer,achwia,achwichwi,achwil,achy,achyika,achyraus,achzhalchinyy khorsho,achzhirigyyn khure,aci,aci bonaccorsi,aci buonaccorsi,aci castello,aci catena,aci koyu,aci santantonio,aciagac,aciak,aciate,acibadem,acibeiros,aciberos,acibo,acicale,acicate,aciciftligi yaylasi,acida,acidagora,acidalia,acidara,acidere,acidere koyu,aciderekoy,acidort,acidost,acielma,acielmacik,aciera,acierie,acigne,acigol,aciguaque,acika,acikagil,acikalan,acikaratu,acikdag,acikdere,acikdilek,acikguney,acikisi,acikkoy,acikoy,aciksaray,acikuyu,acikuyu yaylasi,acikuyukoyu,acikyazi,acikyol,acil barhe,acil barke,acilafiya,acilar,acilia,aciline,acilip,aciliqbina,aciliu,acilu,acima,acimanuk,acimese,acimija,acimovici,acin,acin oluswali,acina,acing,acintal,acioio,aciol,acioli,acioz,acip,acipayam,acipcoville,acipinar,acipinar koyu,acirea,acireale,acireski,acirigama,acirli,acis,acisu,acisu ciftligi,acisu koyu,acitende,acito,acitort,acitort yaylasi,aciua,aciulla,aciura,aciyan,aciyurt,acka,ackamal,ackanis,ackari,ackelsjo,ackelsta,ackenboe,ackenbrock,ackendorf,ackendown,ackenhausen,acker,ackerau,ackerbach,ackerfelde,ackergill,ackerly,ackerman,ackermans mills,ackermanville,ackern,ackersand,ackersberg,ackersdorf,ackerson,ackerville,ackfeld,ackgadang,acklam,ackland,acklena,ackley,acklinga,acklingsedet,acklington,ackok,ackooi,ackooy,ackora,ackors corner,ackovici,ackran,ackron,acksi,acksjoasen,acksjohyttan,acksjon,ackson,acktjara,acktjarn,ackworth,acla,aclan,aclare,aclari,aclart,aclas dafora,acle,aclet,acli,acliang,acline,aclita,acllacancha,acllahuasi,aclou,acma,acmabasi,acmalar,acmali,acmanghit,acmanunpata,acmanupampa,acmar,acmariu,acme,acme mill,acmenik,acmetonia,acnal,acnapa,acno,acnu,aco,aco nuevo,aco pucro,acoacam,acoacan,acoaesakira,acoapa,acoaxet,acoba,acobamba,acobamba hacienda,acobambilla,acobange,acoberayoc,acoc,acoccunca,acochaca,acochacra,acochic,acochupa,acocks green,acocnfang,acoco,acococha,acocolao,acocollo,acocollocucho,acocoto,acocra,acocro,acocseng,acocsi,acoculco,acocunca,acodale,acoesakira,acoga,acoglu,acogon,acohuarma,acohuito,acoichinca,acoita,acoite,acojeja,acok,acokavai,acola,acolita,acolla,acolman,acolman de netzahualcoyotl,acom,acoma,acoma pueblo,acoma village,acomachay,acomani,acomarca,acomat,acomayo,acomayopata,acomb,acombangue,acombanque,acombele,acomerine,acomita,acomita lake,acomulco,acomutero,acon,acona,aconaguin,aconangui,aconbury,aconcemento,aconchi,acondjo,acone,aconekie,aconekien,acongollo,aconibe,aconikie,aconjo,aconngui,acontilla,acop,acopallpa,acopampa,acopara,acopata,acopecho,acopia,acopiara,acopilco,acopinalco,acopinalco del penon,acopinata,acops place,acopucllo,acopugio,acopunco,acoquipa,acoquipe,acor,acora,acoran,acorda,acordeon,acoreira,acores,acoria,acoriana,acoribes,acorizal,acorn,acorn acres,acorn corner,acorn hill,acorn pond,acorn ridge,acorn terrace,acorn tree,acornhoek,acorote,acors corner,acos,acos pueblo,acoso,acospampa,acosse,acosta,acostambo,acostilla,acostillas,acosvinchos,acot,acota,acote,acotea,acoteia,acotera,acotira,acotzilapan,acoua,acougue,acoutla,acova,acowa,acoxcatlan,acoy,acoya,acoyapa,acoyotitla,acoyotla,acoyra,acoyto,acoz,acozac,acozantla,acq,acqua,acqua d ilia,acqua di santa domenica,acqua doria,acqua loreto,acqua marzo,acqua santa,acqua uadi,acquabona,acquacadda,acquacalda,acquadene,acquafondata,acquaformosa,acquafredda,acquaiura,acqualacastagna,acqualadrone,acqualagna,acqualta,acqualto,acqualunga,acquanegra,acquanegra cremonese,acquanegra sul chiese,acquapendente,acquappesa,acquaratola,acquarica,acquarica del capo,acquarica di lecce,acquariva,acquaro,acquarone,acquarossa,acquasanta,acquasanta terme,acquasparta,acquatella,acquatina,acquavella,acquavena,acquaviva,acquaviva collecroce,acquaviva disernia,acquaviva dei vena,acquaviva delle fonti,acquaviva picena,acquaviva platani,acque acidule di rabbi,acque albule,acque albule bagni,acque dolci,acquebouille,acquero,acquet,acquette,acqueville,acquevive,acqui,acqui citta,acqui terme,acquia,acquigny,acquin,acquoij,acquosi,acquoy,acra,acracocha,acragbene,acraha,acre,acredale,acree,acree acres,acrefair,acremont,acres green,acresville,acri,acrifoglio,acrijos,acris,acrisu,acrocancha,acroenus,acroma,acru,acrur,acs,acsa,acsad,acsadi malom,acsalag,acsamacha,acsamachay,acsas,acsaspuszta,acsaujlak,acsaujlak-puszta,acscera,acsera,acsfalva,acshomachay,acsi csoszhaz,acsimao,acsova,acstanya,acsteszer,acsteszer-puszta,acsteszeripuszta,acsuchacra,acsucza,acsuva,acsva,actangao,actaoun,actapalla,actapata,actectic,actela,actelja,acteopan,actiepa,actiepa yochi,actinolite,actiopan,actipan,actis,active,actlzayanca,actlzayanca de hidalgo,acton,acton burnell,acton homes,acton house,acton round,acton scott,acton trussell,acton turville,acton vale,acton village,actopam,actopan,actu,actual tipishca,actun,actunchen,actuncoh,acu,acu da torre,acua,acualahata,acualcheya,acuamanala,acuapa,acuapal,acuarana,acuario,acuata,acuatempa,acuatupo,acuatupo no. 1,acuatupo numero uno,acuautla,acuay,acubede,acucancha,acucar,acucareira,acucena,acucha,acuchay,acuchinhanzi,acuco cambundo,acudanguela,acudanquela,acude,acude angelim,acude de baixo,acude de leite,acude de macaubas,acude do mado,acude do meio,acude do severino,acude dos alves,acude dos carlos,acude novo,acude tapuio,acude velho,acudes,acudina,acudinho,acueducto,acuescomac,acueton,acuexcomac,acuff,acui,acui de jos,acui de mijloc,acui de sus,acuicuixcaltepetl,acuicuixcatepec,acuicuizcatepec,acuifzio,acuimantla,acuirindi,acuisho,acuit,acuitapilco,acuitlapan,acuitlapilco,acuitzermo,acuitzio,acuitzio del canje,acujo,acul,acul basse terre,acul connu,acul des pins,acul des savannes,acul dipson,acul du nord,acul samedi,acula,aculadero,aculahata,aculco,aculco de espinoza,acullani,aculle,acultzinapa,acultzingo,acumal,acumayene,acumayo,acumba,acumbaro,acuminca,acumincum,acumuer,acumulanga,acuna,acunaya,acuncheira,acungui,acuno,acunse,acuorchi,acup,acupacha,acupampa,acupan,acupata,acupe,acupo,acur,acurenam,acurenan,acurensoc,acuria,acurigua,acuriguita,acurizal,acurnam,acurra,acuru tendi,acurucay,acurui,acus,acusa,acuse,acushica,acushnet,acustilan,acutana,acutinga,acutipuane,acuto,acutuba,acutzilapan,acuyo,acworth,acworth beach,acy,acy-en-multien,acy-romance,aczo,aczuli pampa,ad,ad abba abdu,ad abbai,ad abbati,ad abeito,ad abi,ad abiscia,ad abiseia,ad accalom,ad accolom,ad acoro,ad aguale,ad aguori,ad ali bachit,ad amer,ad arili,ad arisc,ad asmaru,ad bili,ad casu,ad casub,ad chite,ad cocob,ad dair,ad daudiyah,ad da`abah,ad da`ayin,ad da`en,ad da`iyah,ad da`janiyah,ad da`nah,ad da`sah,ad da`udiyah,ad da`uq,ad dab`ah,ad dab`iyah,ad dabaibah,ad daba`inah,ad dabab,ad dababishah,ad dababiyah,ad dabah,ad dabarah,ad dabaratayn,ad dabaratayn al mahasim,ad dabawiyah,ad dabba,ad dabbabiyah al gharbiyah,ad dabbabiyah ash sharqiyah,ad dabbaghah,ad dabbah,ad dabbakah,ad dabbat,ad dabbiyat al janubiyat,ad dabbiyat ash shamaliyat,ad dabbuniyah,ad dabbura,ad dabburah,ad dabdabah,ad dabhah,ad dabi,ad dabiah,ad dabiyyah,ad dabkah,ad dabkir,ad dabr,ad dabra,ad dabrah,ad dabratayn,ad dabshah,ad dabunah,ad dabyah,ad dadiyah,ad dafa,ad daff,ad daffah,ad dafinah,ad dafinan,ad dafiniyah,ad dafir,ad dafniyah,ad dafyanah,ad daghah,ad dagharir,ad daghgharah,ad dagma,ad dahah,ad dahaki,ad dahamiyah,ad dahan,ad dahana,ad dahanah,ad dahar,ad dahasah,ad dahashinah,ad dahdah,ad dahfiyah,ad dahhab,ad dahhan,ad dahhar,ad dahi,ad dahinah,ad dahir,ad dahira,ad dahirah,ad dahiriyah,ad dahis,ad dahiyah,ad dahl,ad dahlah,ad dahma,ad dahqah,ad dahr,ad dahra,ad dahrah,ad dahrat,ad dahriyah,ad dahtamun,ad dahw,ad dahya,ad dai,ad daimah,ad dajaniyah,ad dakhilah,ad dakhla,ad dakhlah,ad dakkah,ad dakwah,ad dal`ah,ad dala,ad dalanj,ad dalatun wa rizqat shams ad din,ad dalfa`ah,ad dalhamiyah,ad dali,ad dali`,ad dali`ah,ad dalifah,ad daljamun,ad dallashah,ad dalwah,ad dam,ad damairah,ad damah,ad damat,ad damayirah,ad damazin,ad damdam,ad damiah,ad damigh,ad damim,ad damir,ad dammam,ad damthi,ad damur,ad damus,ad dan,ad dana,ad danaba,ad danabah,ad danabiq,ad danah,ad danapteno,ad danaqilah,ad danba,ad danbah,ad danbiyah,ad dandaniyah,ad danget,ad danib,ad dann,ad daqarin,ad daqawanah,ad daqawinah,ad daqqah,ad daqqaqah,ad dar,ad dar al bayda,ad dar al kabirah,ad dar as sufla,ad dar`iyah,ad darabi,ad darah,ad daraijah,ad darajah,ad darakisah,ad daramalliyah,ad darawish,ad darawiyah,ad darayn,ad daraziyah,ad darb,ad darb al aswad,ad darb at tawil,ad darbashiyah,ad darbin,ad dardarah,ad daribah,ad darifah,ad darisat,ad dariyyan,ad dariz,ad darraji,ad das,ad dashishah,ad dasht,ad dashuniyah,ad dasma,ad dasmah,ad dath,ad dathir,ad daudi,ad daw,ad dawadami,ad dawadimi,ad dawahirah,ad dawakhiliyah,ad dawali,ad dawalitah,ad dawaltah,ad dawaniq,ad dawar,ad dawawir,ad dawaykah,ad dawh,ad dawh al kabir,ad dawh as saghir,ad dawha,ad dawhah,ad dawhah al jadidah,ad dawhi,ad dawib,ad dawir,ad dawj,ad dawkah,ad dawlah,ad dawm,ad dawmah,ad dawqah,ad dawr,ad dawrah,ad dawsah,ad dawsariyah,ad dawsh,ad dawshah,ad dawudiyah,ad dawun,ad dawwab,ad dawwar,ad dawwarah,ad dawwayah,ad day`ah,ad dayaah,ad dayasir,ad daym,ad dayma,ad daymah,ad dayq,ad dayr,ad dayr al gharbi,ad dayr al qadim,ad dayr ash sharqi,ad dayr bahmi,ad dayr fahmi,ad dayris wa kafr latif,ad dayrunah,ad dayy,ad dayyam,ad dchira,ad debeilu,ad dechita,ad dechiwa,ad dechtwa,ad delatia,ad demna,ad dereis,ad dhakhirah,ad dhar,ad dharayn,ad dharbaniya,ad dharrah,ad dhid,ad diba`iyah,ad dibah,ad dibbiyah,ad dibin,ad didamun,ad digayik,ad dighalah,ad dihalah,ad dihasiyah,ad dihwah,ad dijan,ad dijarah,ad dijele,ad dikak,ad dikeri,ad dikiyah,ad dikka north,ad dikka west,ad dikwanah,ad dikwani,ad dikwanih,ad dil`,ad dilam,ad dilaymiyah,ad dileba,ad dilghan,ad dilinjat,ad diman,ad dimas,ad dimasah,ad dimashqiyah,ad dimirdash,ad dimna,ad dimnah,ad dimuqrat,ad dindar,ad dinjuh,ad diqar,ad diqqar,ad dir`iyah,ad dirah,ad diraz,ad dirbasiyah,ad dirco,ad diriyah,ad dirjaj,ad dirm,ad dirnaj,ad dirs,ad dirsiyah,ad dirwatayn,ad dis,ad disah,ad diseis,ad diwan,ad diwania,ad diwaniya,ad diwaniyah,ad diwar,ad diyabiyah,ad diyaymah,ad diyyu,ad djele,ad dkarin,ad dobi,ad doha,ad doka,ad dokber,ad doma,ad dougoum,ad doummisat,ad doummissat,ad dowhah,ad du`a,ad du`abah,ad du`ayji,ad du`ayn,ad du`ayrah,ad duba`iyah,ad dubah,ad dubaratayn,ad dubasi,ad dubay`ah,ad dubaybah,ad dubaybat,ad dubayjah,ad dubaykir,ad dubayqah,ad dubayyah,ad dubayyat,ad dubbah,ad dubbat,ad dubbiyah,ad dubiyah,ad dubiyat,ad dubr,ad dubrah,ad dufah,ad dufayr,ad dughayrat,ad dughshulah,ad duhayr,ad duhayrah,ad duhu,ad dujayl,ad dukaydik,ad dukaykah,ad dukhaylah,ad dukhul,ad dul,ad dula,ad dula`,ad dulab,ad dulaimiya,ad dulayb,ad dulaybah,ad dulaybat,ad dulaymiyah,ad dulb,ad dulbah,ad dulu`,ad dulu`iyah,ad dumaynah,ad dumayriyah,ad dumayyin,ad dummam,ad dumna,ad dumnah,ad dunaybah,ad duq,ad duqa,ad duqah,ad duqm,ad duqqi,ad dur,ad dur`iyah,ad durad,ad durah,ad duray`i,ad duray`iyah,ad durayb,ad durayhamiyah,ad durayhimi,ad durayj,ad durayjah,ad duraymah,ad duraynah al `ulya,ad duraysiyah,ad durrah,ad duru`,ad durub,ad duruqiyah,ad dusah,ad dushaynab,ad duwadami,ad duwar,ad duwaybah,ad duwaybi,ad duwayd,ad duwayhah,ad duwayhirah,ad duwayir,ad duwaykah,ad duwaykhat,ad duwaykhilah,ad duwaylah,ad duwayliyah,ad duwaym,ad duwaymah,ad duwayr,ad duwayrah,ad duwayrat,ad duwayri,ad duwaysah,ad duwem,ad duyayqah,ad duyuk,ad edug,ad ela,ad errei,ad faghi,ad fakwan,ad finne,ad gaban,ad gander gabei,ad garab sadiai,ad guadad,ad judul,ad manna,ad mela,ad messian,ad mocada,ad mussa,ad nebrid,ad nefas,ad omar,ad rassi,ad refai,ad ru`ah,ad said mustafa,ad saidna mustafa,ad sala,ad sata,ad scec,ad scec hammed,ad sciuma,ad sulet,ad taille,ad taula,ad taulet,ad teclai,ad teclesan,ad tzatzer,ad ulual,ad zeban sebao,ad zelaco,adzhan-adyrskiy,ad-adan,ad-bunaca,ad-bunacaj,ad-damar,ad-dora,ada,ada bazar,ada chala,ada chalu,ada ela,ada foah,ada fourteen pull,ada fua,ada hayatwala,ada ibaylu,ada izir,ada jehan khanwala,ada joghi,ada koy,ada kale,ada kaleh,ada khianra,ada khu,ada kouassi,ada lukacka,ada marinescu,ada nda,ada panya,ada payna,ada punga,ada sahianwala,ada tepe,ada tepesi ciftligi,ada twenty-five pull,adaa,ada-koy,ada-obadan,ada-tym,adaa,adaakham,adaatsag,adaba,adaba chak,adabag,adabagkoyu,adabai,adabai deili,adabai doulel,adabai idaouach,adabai mahamoud,adabakpo,adabaria,adabarta,adabashev,adabasi,adabat,adabata,adabate,adabatelo,adabay,adabaye,adabaye ideouache,adabda,adabelle,adabem,adabere,adabi,adabiam,adabiaran,adabinli,adabiyat,adablai,adabo,adabodo,adabofolo,adaboi,adabokrom,adabolava,adabomahebo,adabomaro,adabong,adaboro,adaboroa,adabotelo,adabotokana,adabou kope,adabourou,adaboya,adabozato,adabu,adaburg,adabyaou,adacami,adacao,adacari,adacay,adachal,adacs,adacsierdo,adacuana,adad,adad banda,adad dader,adad gheri,adad khel,adada,adadale,adadama,adade,adadeem,adadeju,adadere,adadesanche,adadi,adadi arare,adadi sanche,adadia,adadientam,adadientem,adadientem adegya,adadikam,adadikouro,adadiyo bire,adadkhel,adadkheyl,adadkheyl,adadle,adadleh,adado,adadoi,adadu,adadym,adadze,adadzeebi,adadzeyebi,adaes,adaevski,adafa,adaffia,adafia,adafianu,adafienu,adafila,adafokope,adaga,adaga hadira,adagali,adagalla,adagamus,adagap,adagapi,adagba,adagba ako,adagbabri,adagbarassa,adagbarassa-amukpe,adagbari,adagbo,adagbrasa,adagbrassa,adagbrassa-amukpe,adage,adaghaj,adaghan,adaghas,adaghum,adagide,adagimon,adagliko,adagmadeni,adago,adagoi,adagore,adagoren,adagoro,adagrikpo,adagul,adagum,adagume,adah,adaha,adaham,adahamasi,adahaso,adahem,adahi kouadjo,adahio,adahomasi,adahou,adahuesca,adahumasi,adahumaso,adahunat,adahwar bibi shelgar,adai,adai al qadam,adaia,adaichakal,adaidia,adaigba,adaigievbe,adaikkalamoddai,adail,adailo,adaim,adaincourt,adainville,adaio,adair,adair ford,adair village,adairsville,adairville,adais,adaiso,adait,adaitiwala,adaito,adaito ela,adaiwai,adaiwala,adaji,adak,adak-yur,adaka,adakachan,adakai,adakaka,adakakpe,adakale,adakama,adakanberli,adakar,adakasim,adakata,adakavas,adakavo,adakaya,adake,adakele,adakent,adakgruvan,adakham,adakhola,adakie,adakinik,adakioj,adakioy,adakjaure,adakli,adakliden,adaklu abuadi,adaklu ahunda,adaklu anfoe,adaklu avedzi,adaklu bolonyigbe,adaklu boso,adaklu didokogli,adaklu kpatove,adaklu mahanane,adaklu sofa,adaklu waya,adako,adakope,adakorigbo,adakoui,adakouse,adakoy,adakpame,adakparo,adakplame,adakpo,adakpo kondji,adaksu,adakuan,adakunsia,adakwai,adakwakrom,adal,adal kalyar,adal sumro,adala,adalair,adalaj,adalamdjaki,adaland,adalar,adalara 1,adalata,adalatgarh,adalatpur,adalauk,adalawa,adalberto,adalberto ojeda,adalberto tejeda,adale,adale afuen,adale afweyna,adale ibrahim,adalei,adalekpoe,adalen,adalet,adaletabat,adalgarh,adali,adalia,adalikuzu,adalin,adaline,adalinu,adaliyow,adalou,adalpur,adalsbruk,adalsvollen,adaluz,adalzai,adam,adam acres,adam banda,adam bawsh,adam bhir,adam bosh,adam burj,adam ceray,adam cheray,adam chuhan,adam darrehsi,adam darrehsi-ye `olya,adam darrehsi-ye bala,adam darrehsi-ye pain,adam darrehsi-ye sofla,adam derai,adam donga,adam durgapur,adam ford,adam jan pathan,adam jat,adam kala,adam kalla,adam kanda,adam kandah,adam khan,adam khan bhulkani,adam khan drakhan,adam khan junejo,adam khan kelay,adam khan kili,adam khan lund,adam khan magsi,adam khan ujon,adam khan walagai,adam khan-jo-goth,adam khel,adam khel charkha,adam khel kili,adam khelanwala,adam kheyl,adam kili,adam kodali,adam kole,adam kudali,adam lashari,adam mala,adam manirabad,adam pali,adam park,adam phanwar,adam pir,adam sahab,adam sammun,adam sehta,adam shagai,adam tash,adam tere,adam town,adam tseray,adam tserey,adam varu,adam wahan,adam yanga,adam-les-passavant,adam-les-vercel,adam-lez-vercel,adam-puszta,adam-uchi,adam-uchi russkiye,adam-uchi russkoye,adama,adama diara,adama diomande,adama diop,adama thitpok,adama umm qurayn,adama umm qurein,adamabad,adamabougou,adamagon,adamakiri,adamamuiza,adaman,adaman chalgan,adaman chalpan,adamana,adamanay,adamani,adamankotta,adamankottai,adamanli,adamanso,adamant,adamantina,adamany,adamari,adamarina,adamaro,adamaroko,adamas,adamasiny-vineta,adamasu,adamat,adamata,adamate,adamawa,adambaki,adambakkam,adamcheray,adamchouari,adamchuki,adamclisi,adamczowice,adamczycha,adamdaraz,adamdulo,adame,adamegi,adamek,adamenka,adamenki,adamenzork,adamesht,adamesti,adamfaki,adamfakizir,adamharmani,adamhegy,adami,adami gebeya,adami tulu,adamiabo,adamierz,adaminaby,adaminka,adaminowo,adamishki,adamit,adamith,adamitullo,adamivka,adamiy,adamji goth,adamji nagar,adamkala,adamke,adamkhakalay,adamkhan,adamkhan kalay,adamkhel,adamkheyl,adamkheyl,adamkouch,adamkoutch,adamkovo,adamkovske,adamkowa rola,adamkowo,adamkudali,adamli,adammana,adamo,adamogne,adamou,adamoukparo,adamov,adamova,adamovce,adamovec,adamoviche,adamovichi,adamovici,adamovka,adamovka novaya,adamovo,adamovshchina,adamovshchyzna,adamovskiy,adamovskoe,adamovskoye,adamow,adamow nowy,adamow stary,adamowa gora,adamowek,adamowice,adamowicze,adamowizna,adamowka,adamowka powisle,adamowo,adampam,adampan,adampanai,adampane,adampantalvu,adampol,adampol-letnisko,adampolis,adampur,adampur makhan,adampur manoharpur,adampur umed,adamri,adamrobe,adams,adams basin,adams beach,adams center,adams chance,adams college,adams corner,adams corners,adams cove,adams crossing,adams crossroads,adams flat,adams grove,adams heights,adams junction,adams landing,adams m.s.,adams mill,adams mills,adams mission,adams mission station,adams mobile home park,adams morgan,adams park,adams ridge,adams run,adams shore,adams square,adams station,adams trailer park,adams valley,adamsboro,adamsburg,adamsdale,adamsdorf,adamser,adamsfield,adamsfreiheit,adamsgut,adamsguth,adamsheide,adamshof,adamshoffnung,adamshohe,adamso,adamson,adamson chibuye,adamson kulubale,adamson siame,adamson siyame,adamsthal,adamston,adamstown,adamstown village,adamsu,adamsverdruss,adamsville,adamsville crossroads,adamswalde,adamswiller,adamtanya,adamti,adamu,adamu jimi,adamu kulu,adamu suka,adamus,adamuvka,adamuz,adamwahan,adamwak,adamwala,adamwala kera,adamwali,adamy,adamzai,adamzai,adamzai kili,adan,adan quiroga,adan-adan,adana,adanabas,adanabra,adanaccio,adanak,adanakkottai,adanalioglu,adanauara,adanca,adancata,adand,adanda,adande,adane,adanero,adang,adanga,adangbe,adanghpu,adangomasi,adanham,adanhe,adanhoue,adani,adaniabo,adanion,adanisievbe,adaniya,adanka,adankari,adankranya,adankrono,adankwame,adankwami,adankwamu,adankya,adanla,adannes,adanniya,adanoata,adanou,adanouve,adanouy,adanse,adanse anweaso,adanse prasu,adansemanmu,adansemem,adansi,adansi anhwiaso,adansi kakraba,adansi nsese,adansi prasu,adansimen,adansina,adantia,adanting,adanur,adanwamaso,adanyi,adanzai,adao,adao candido lima,adao lobo,adaoag,adaoay,adaoet,adaoka,adaonu,adaoren,adaou,adaoua,adaouane,adaouda,adapa,adapatakitala,adapazari,adapinar,adapo,adappan villu,adappanavillu,adappankulam,adapparagama,adaprase,adaprasi,adapur,adapwi,adar,adar bagh,adar n oualaene,adar n oualen,adar ou amane,adar ou-amane,adar tokya,adara,adara asundus,adara mbagol,adaralode,adarama,adaramand,adaran,adarare,adaraw,adarawa,adarbala,adardeb,adardour,adare,adargan,adargin,adargin nemetskiy,adarhissar,adari,adarib,adarino,adario,adarki,adarki buzurg,adarlona,adarmanaabad,adarmanabad,adarmonaabad,adaro,adarome,adarpara,adarras,adarse,adarte,adaryan,adarza,adas,adas bidh,adasak,adasamasi,adasan,adasarhanli,adasawase,adasburg,adaseni,adasenii,adasevci,adash,adasha,adasheva,adashevo,adashi,adashika,adashu,adasi,adasika,adasimadi,adasiyah,adassi,adassil,adast,adasztevel,adat,adat atak,adat cussra,adata,adatdanginyeh,adatdauhyeh,adate kope,adatepe,adatepe koyu,adatepebasi,adatepekoy,adatepekoyu,adatkan,adato,adaton,adatoprakpinar,adatoprakpinari,adatoprakpinarikoy,adatopraksinanli,adattie,adatuim,adatymovo,adau,adau river,adaudo nyerur,adaudu,adaufe,adaure,adausena,adausina,adausu,adaut,adav-tulumbayevo,adavad,adaval,adavale,adavar,adave,adaven,adavere,adavere asundus,adavikanda,adaville,adaviran,adavo kope,adavolum,adavouloum,adavwari,adawa,adawai,adawani,adawanran,adawar,adawarna,adaway,adawikanda,adawkaw,adawmena,adawmenu,adawo,adawro,adawsena,adawsina,adawso,adawsoasi,aday,aday-aul,adaya,adayatak,adaye,adayevka,adayevo,adayevskaya,adayevskiy,adayiri,adayougha,adayourha,adayto elo,adaytobe,adaza,adazhi,adazhi-02,adazi,adazi 2,adazi divi,adazi-nuka,adba`ah,adbaddhi,adbaddiwala,adbadri,adbalsincan,adbaston,adbhar,adbidora,adbo amani,adbodarna,adbul karim shah,adbul qadir dahri,adbul razzaa dahri,adbul sattar,adbulbakiler,adcamo,adcasal,adchai,adchair,adchat tantil,adchaych,adco,adcock,adcock crossroads,adda,adda basira,adda chachran,adda dawa,adda loharwala,adda marmariti,adda nihal,adda shabirabad,adda shaikh wahan,adda tila,adda tujsiram,adda usara,adda yusufabad,adda zohrabad,adda-doueni,addada,addadi,addafoa,addagalla,addah,addah fishing village,addahyiri,addaippaliam,addaippallam,addala,addalachchenai,addalachena,addalachenai,addalam,addalem,addama,addamaghane,addamghen,addamitullu,addang,addani,addanki,addao,addapalam,addar,addara,addarawellana,addare,addaro,addatigala,addaye nait herbil,addaza,adde,addebull,addeche,addehera,addekar-kebouch,addenhausen,addenstorf,addepur,adder,adderabi,adderbury,adderet,addergoole,adderley,adderleys,adderly,addernhausen,addho pari,addi,addi abayo,addi abbagye,addi abdera,addi abur,addi ademay,addi adid,addi aguo,addi aitecheb,addi akel,addi akeyti,addi amday,addi amiyuk,addi araha,addi aro,addi atal,addi ats-nna-ani,addi auiala 1,addi auiala 2,addi ayuoto,addi azuka,addi baba,addi barot,addi bearedzh,addi berakh,addi caieh,addi chemo,addi cimnu,addi colo,addi culcul,addi dacno,addi dairo,addi dayro,addi decal,addi didei,addi dideti,addi felassie,addi fengats,addi fereg,addi ferti,addi fitau,addi gedaf,addi gedena,addi gezaiti,addi gibai,addi gidad,addi gide,addi gidey,addi giyorgis,addi gobeyo,addi goro,addi guara,addi gudem,addi gui,addi gundi,addi gyrmay,addi hirdi,addi hoza,addi inalca,addi iser,addi kadzhera,addi kajera,addi kalkan,addi kala,addi kebad,addi kedauit,addi kelkel,addi kelkel 2,addi kelekhel,addi keray,addi khabesay,addi khageray,addi khakeyti,addi khana,addi khankera,addi khaye,addi kherem,addi khibubla,addi khitsan,addi kidan,addi kimaida,addi kolen,addi kualase,addi kuere,addi kulkul,addi kuylo,addi markos,addi massahal,addi messahal,addi micael,addi micail,addi misgan,addi nebri,addi nekizen,addi onfato,addi onfito,addi raisi,addi rassi,addi risenay,addi roura,addi saka,addi sardo,addi scimti,addi scium,addi selam,addi shakhu,addi shegereb,addi shimbrukh,addi shisha,addi tar,addi tegora,addi tekhetsii,addi teklom,addi terekbe,addi tesfo,addi tigre,addi toqualu,addi tsanda,addi tsegibna,addi tseri,addi ugri,addi una,addi uokai,addi uorekh,addi uorkhi,addi uoyno,addi ydara,addi zibay,addi zubbaha,addia,addicaie,addick estates,addicks,addie,addielee,addielou,addiet,addiet canna,addieville,addiewell,addig,addigel,addilera,addilnalca,addin liba,addineou,addingham,addington,addington mill,addipur,addirim,addis,addis ababa,addis abbaba,addis abeba,addis alem,addis derra,addis zaman,addis zemen,addis-alam,addisc addi,addisghie,addish addi,addison,addison heights,addison junction,addit,addito,additale,addition,addition hills,addition hills brookside,additu,addlestone,addlestrop,addlethorpe,addney,addo,addoana,addodas,addokorpe,addomo,addon,addooloo,addor,addouz,addow,addowali basti,addragool,addran,addrup,addu majra,adduas,addusu,adduwala,addy,addyston,ade,ade adeye,ade agbo,ade alesu,ade bunaca,ade bunace,ade eko,ade eyo,ade ka tar,ade ke sagle,ade kota,ade kotah,ade kowtah,ade oba,ade saheb,ade sahib,adea,adeaga,adeagbo,adean,adeane,adeba,adebabay,adebayo,adebe,adebem,adebem-kagberi,adebere,adebib,adebimpe,adebisi,adebiyat,adebiyet,adebiyi,adebka,adebo,adebomi,adebori,adebour,adebowale,adebu`ur,adebur,adecha atologh,adecheda,adechrekope,adecide,adeda,adedaji,adedakope,adede,adedeji,adedewo,adedi,adedido,adedigba,adedijo,adediran,adedje,adedokun,adee,adeega,adeembra-asin,adeemmra,adeen,adega,adega do chao,adega do mouchao,adega mobilen,adegalagaji,adeganha,adegaon,adegari,adegas,adegata,adegbayi,adegbenjo,adegbite,adegbo,adegbodu,adegbola,adegboye,adegeest,adegem,adegliacco,adego,adegoiva,adegoke,adegoroye,adegow,adegun,adegya,adeh,adeh-e mortezapasha,adeh-ye bozorg,adeha,adehe,adeho,adehru,adehwagh,adeia,adeigbe,adeikrom,adeile,adeiri,adeisena,adeitan,adeito,adeitu,adeja,adeje,adejemi oke,adeji,adejubu,adejumo,adek,adeke ginet,adekagbene,adekanbi,adekanmbi,adekar,adekar kebouche,adekar-kebouch,adekere,adekha,adekhota,adekhs,adekhsal,adekidero,adeko,adekoe,adekola,adekomi,adekou,adekplovi,adekule,adekunbi,adekunle,adel,adelbek,adela,adela corti,adelaida,adelaide,adelaide lead,adelaide maillet,adelaide river,adelaido,adelaidovka,adelaieau,adelaikovka,adelair,adelakun,adelan,adelange,adelano,adelans,adelantado,adelanto,adelaye,adelbach,adelberg,adelberg kloster,adelberg-dorf,adelboden,adelby,adelbylund,adeldal,adeldorf,adele,adele gomboro,adele gubo,adele umuro,adelebsen,adeleke,adeleye,adelfar,adelfia,adelfors,adelgunz,adelharz,adelhausen,adelheide,adelheidsdorf,adelheim,adelheitsthal,adelhofen,adelholzen,adeli,adelia,adelia maria,adeliada,adeliang,adelig boltenhagen,adelige,adelin,adelina,adeline,adelino,adelino franca,adelinow,adelita,adelkino,adell,adella beach,adelle,adellet,adelma beach,adelmannsdorf,adelmannsfelden,adelmannsitz,adelmannssitz,adelokun,adelong,adeloun,adelov,adelphi,adelphi park,adelphi park estate,adelphia,adelsberg,adelsborn,adelschlag,adelsdorf,adelshausen,adelsheim,adelshino,adelshofen,adelsreute,adelsried,adelstedt,adelstetten,adelta,adelup,adelvad,adelwala,adelwitz,adelzhausen,adem,adem abdi,adem abdule,adem lega,adem tamu,adem yonis,adem-ionis,adema,adema iyasu,ademaboi,ademaboy,ademame,adembeki,adembeku,adembra,ademe,ademi,ademia de cima,ademsari,ademu,ademuz,aden,aden bereke,aden kheyl,aden libah,aden lida,adena,adenau,adenbach,adenberg,adenbruck,adenbuttel,adenchi,adendema,adendia,adendorf,adendorp,adendoun,adendro,adendron,adeng,adengefom,adengi,adengri,adenguor,adengwar,adeni,adeniji,adenilare,adeniran,adeniregun,adenkhel,adenkrebi,adenlere,adenmoor,adenna,adenndoun,adenodeiro,adenrele,adense,adensen,adenstedt,adentan,adentro lajas,adenu,adenya,adenye,adenyensu,adeogun,adeosi,adeoti,adeouna,adeoye,adepari,adepele,adepoju,adept el fati,adeq,adeqabad,adeque,adequez,ader,adera,aderai,aderan,aderar,aderass,aderat,aderawa,aderbissinat,aderbiyevka,aderde,aderdeb,aderdour,adere,adere wama,adereg,adereka,aderellah kalay,aderevy,aderew,aderfiee,aderfis,adergan,adergas,aderian,aderibigbe,aderich,aderie,aderisheno,aderishina,aderishino,aderkasi,aderkasu muiza,aderke,aderklaa,adermanabad,aderno,adero,adero az,aderonmu,aderpayeta,aderras,adersal,adersane,adersbach,adersdorf,aderseg,adersheim,adersleben,aderstedt,adertshausen,aderu,aderup,aderutu,adervielle,aderyan,aderzhofen,adesakhib,adesanga,adesar,adesberg,adeseb,adesehun,adeshiyan,adeshteh,adesi,adesina,adesiyan,adesma awero,adeso,adesse,adesta,adet,adet-hngetthe,adeta,adetere,adeti-kope,adetinu,adetkyi,adetli,adeto,adetola,adetunji,adeusina,adevue,adewala,adewale,adeward,adewardervoorwerk,adewole,adewuke,adewumi,adewunmi,adewuyi,adey me`at,adeyadi,adeyanba,adeyane,adeyemi,adeyemi oluajo,adeyemo,adeyepena,adeyle,adeyo,adeyokum,adeyokun,adeyori,adeytown,adeza,adezai,adforton,adgadlu-ye angutlar,adgal,adgala,adgan,adganiski,adgao,adgaon,adgat,adgateville,adgatia,adgawan,adgbers-n-warfaln,adgenou,adger,adghala,adghalah,adghers nouarfaln,adghigh,adgiraldo,adgo ager,adgon,adgoniski,adgos,adguigna,adgulyand-kari,adgun,adguo,adh dhaibiyah,adh dha`nub,adh dhabirah,adh dhabiyah,adh dhagharir,adh dhahabi,adh dhahabiyah,adh dhahibat,adh dhahir,adh dhahiriya,adh dhaid,adh dhakhaf,adh dhakhira,adh dhakhirah,adh dhala`,adh dhanabah,adh dhanbah,adh dhanib,adh dharma,adh dharrah,adh dhawb,adh dhayfir,adh dhiabiyah,adh dhibah,adh dhibiyah,adh dhime,adh dhimrah,adh dhira`,adh dhirwayin,adh dhuhaybah,adh dhuhaybiyah,adh dhukhayr,adh dhunayb,adh dhunaybah,adh khahi,adh`eet,adha,adha jator,adha khan gadio,adha khiarah,adha khiareh,adhabadahiwala,adhaia,adhaim,adhaipur,adham,adhamas,adhami,adhamioi,adhamion,adhamiya,adhan,adhan al sa`d,adhanah,adhar,adhara,adhatah,adhatal,adhaun,adhaura,adhba,adhbah,adhe,adhele,adhelfikon,adhendhron,adhendron,adhepasa,adhewala,adhgarh,adhhadhiwala,adhi,adhi bagh,adhi kot,adhi sargal,adhiakopos,adhian,adhicadeeye,adhikarigama,adhilir,adhin,adhjoord,adhkhala,adhla khanwali,adhlakhanwala,adhlakho,adhlana,adhoi,adhoie,adhonigarh,adhowal,adhpakhia,adhrahma,adhraktia,adhramilos,adhravastoi,adhravastos,adhrayastos,adhriani,adhrianos,adhrianou,adhruh,adhuna,adhunagar,adhuria,adhusari,adhwal,adi,adi aba jebano,adi aba musa,adi aba riisom,adi abaio,adi abanawo,adi abayo,adi abbaghie,adi abbo mussa,adi abergele,adi abeyto,adi abun,adi abuna,adi acfel,adi achelai,adi ada,adi adda,adi ademay,adi agag,adi agam,adi agara,adi agay,adi agbay,adi aggera,adi agghera,adi agwa,adi ahiderom,adi ahiu,adi aitechab,adi aitecheb,adi akeyti,adi akel,adi alecti,adi alele,adi ali bakit,adi almeban,adi amanuel,adi amday,adi amiyuk,adi amshashi,adi ar`ada,adi araha,adi arbaa,adi arbaite,adi arba`ite,adi arbate,adi arcai,adi arkay,adi arkai,adi aro,adi arra melaio,adi artan,adi atal,adi atei,adi atsnaani,adi aw-ala,adi awde,adi awiala,adi awso,adi ayo,adi ayweto,adi azuka,adi badim,adi baghe,adi bahro,adi balanberas,adi baro,adi barot,adi basale,adi bearej,adi begghedi,adi belai maar,adi beles,adi belew,adi berah,adi beri,adi bidera,adi bigdi,adi bigheddi,adi bilay,adi borat,adi buda,adi bula,adi butzaat,adi caieh,adi casci,adi catina,adi chegware,adi chemo,adi chewa,adi chelehel,adi chiana,adi colo,adi contzi,adi corcora,adi culli,adi dairo,adi da`iro,adi dakno,adi daro,adi decal,adi dehal,adi dekhal,adi dererai,adi didei,adi dirai,adi elo,adi embela,adi erzanye,adi essaha,adi farah,adi fela,adi fengats,adi fereg,adi ferti,adi feyiso,adi finii,adi fitall,adi fitaw,adi gaad,adi gaba,adi gahad,adi gebray,adi gebru,adi geda,adi geda (1),adi geda (2),adi gedaf,adi gedena,adi gefar,adi gele,adi gerih,adi gezaiti,adi gheda,adi ghella,adi ghemtela,adi gheva,adi giba`i,adi gide,adi gidey,adi girmay,adi giyorgis,adi gobeyo,adi godaiti,adi golagol,adi golagul,adi golgol,adi golo,adi gombolo,adi goorbati,adi goradaiti,adi gorandaiti,adi goreto,adi goro,adi goroto,adi goulagul,adi grot,adi grotto,adi gui,adi guur,adi gu`idi,adi guagual,adi guaguat,adi gudeb,adi gudem,adi gudom,adi gudum,adi gufah,adi gul biscia,adi gul biscia gundi,adi guolagul,adi gura,adi gwaidad,adi gwadad,adi gwara,adi habesay,adi habir,adi hadid,adi hageray,adi hagoda,adi hagos,adi hakeyti,adi hakefa,adi hale,adi hamido,adi hamli,adi hamushte,adi hana,adi hangi,adi hankera,adi hardany,adi harwo,adi hawesh,adi hawesha,adi haye,adi hayem,adi hedum,adi herem,adi hibubla,adi hidrum,adi hirdi,adi hitsan,adi hizba,adi homa,adi hostos,adi hoza,adi huduk,adi hurug,adi hurwa,adi idaga,adi iecuoro,adi imneger,adi iser,adi itai,adi itay,adi itie,adi itieh,adi iyo,adi kaga,adi kajera,adi kala,adi kalkan,adi kano,adi kashi,adi kebad,adi kedawit,adi keharis,adi kehars,adi kelkel,adi keney,adi kesawist,adi keshi,adi keyih,adi kima`ida,adi koo,adi kore,adi kuylo,adi kwalase,adi kaukat,adi kawna,adi kefelet,adi keih,adi kelkalti,adi keray,adi keyeh,adi keyh,adi khagaba,adi kidan,adi koffikro,adi kolen,adi komusse,adi kouna,adi kubulo,adi kudi,adi kuo raro,adi kuolagul,adi kwala,adi kwere,adi kwolagul,adi laha,adi lakha,adi lamza,adi lashari,adi liul,adi lomin,adi lubso,adi macambia,adi mahari cristos,adi mai laam,adi mai uoini,adi mancarre,adi mandi,adi markos,adi meda,adi mella,adi mendi,adi mengodti,adi meyda,adi minda,adi misgan,adi moaghe,adi mocada,adi momena,adi mugia,adi naamen,adi nas,adi neba,adi nebrat,adi nebrid,adi nefas,adi nekizen,adi nifas,adi nifas (1),adi nifas (2),adi noguade,adi noia,adi noquade,adi noya,adi quarario,adi quararo,adi quorquor,adi raisi,adi ra`isi,adi ramets,adi rassi,adi remet,adi remets,adi remos,adi remots,adi remoz,adi resene,adi riisenay,adi rihitsan,adi rowra,adi ruso,adi sahlay,adi saka,adi sana-i-wazbi,adi sardo,adi saseso,adi sceca,adi sciaba,adi scimandui,adi scimti,adi scinguala,adi scium ascale,adi secche,adi selam,adi sellam,adi seraw,adi sessahu,adi sessakhu,adi sessanu,adi shah,adi shahsheh,adi shahu,adi shanah-ye wazbi,adi shegereb,adi sheho,adi shehu,adi shela,adi sheshu,adi shila,adi shimbruh,adi shisha,adi shoh,adi shoho,adi shum hafti,adi shum tombosa,adi shuma,adi sighe,adi soguadi,adi tan,adi tar,adi tu`at,adi tacar,adi tafa,adi teghemmes,adi tehetsiy,adi tekelezan,adi tekelezan ketema,adi teklay,adi teklom,adi terekbe,adi tesfo,adi tigre,adi togora,adi tsaida,adi tsegibna,adi tseri,adi tsada,adi tsagamati,adi tsetser,adi tuoldemedhin,adi ugri,adi una,adi uochi,adi uorche,adi washo,adi wederki,adi wekai,adi weka,adi wereh,adi werhi,adi weyno,adi yad,adi yas,adi zahilay,adi zarna,adi zelay,adi zibay,adi-atsida,adi-bigdi,adi-felesti,adi-ghelae,adi-gureto,adi-ketina,adi-kofikro,adi-neamn,adi-nebri,adi-quala,adi-teklezan,adi-wala,adi-yaprikro,adia,adia bitaog,adia wati-ofu,adiabad,adiabe,adiabo,adiabo ikot mbo,adiabo ikot ukpa,adiabo okutikang,adiaboulkoukope,adiadi,adiadon,adiagnao,adiai,adiaida,adiaide,adiake,adiakiti,adiakou konankro,adiakouemi,adiakouro,adiaksinskiy,adial,adiala,adialah,adialakope,adiama,adiamadiang,adiaman,adiambra,adian,adian baturang,adian naginjang,adian nangka,adian padang,adian tengah,adiana,adiana gala,adianbadja,adianbaja,adianbolon,adianhoting,adiani,adiankoting,adianmatutung,adiannagindjang,adianpara-para,adiantorop,adiantorop 1,adiantukat,adiao,adiaou,adiaoukei,adiaoukoi,adiapikro,adiapo moronou,adiapo-doume,adiapo-te,adiapo-to,adiapo-to i,adiapo-to ii,adiapote te,adiapoto-i,adiapoto-ii,adiapri-kofikro,adiarsa,adiarsapusaka,adiasim,adiassagon,adiassou,adiata,adiawakope,adiawso,adiawukope,adiaz,adiba,adibaikel,adibafe,adibara,adibare,adibatla,adibeasi,adiberai,adibiawra,adiblikope,adibo,adiboyo wetan,adibrebo,adibrobo,adibue,adibuee,adibuwe,adibuwee,adicas,adichaem,adichrekope,adichuem,adici,adicora,adidabe,adidarma,adidasi,adidevo,adidi,adidiang paych,adidiema,adidiso,adido,adido gome,adidogome,adidokope,adidokpe,adidokpo,adidokpoe,adidokpovu,adidome,adidome mafi,adidomi,adidotoe,adidovce,adidovenu,adidovi,adidovikope,adidoyo,adidur,adidwam,adidwan,adie,adiebaba,adiebeba,adiebo,adiebonou,adiegi,adiekankro,adiekpe,adiekro,adiekrou,adiele,adielilie,adiem,adiemasi kope,adiemassikope,adiembra,adiemmra,adiemra,adienchin,adiepena,adiere,adieu au monde,adieu-vat,adieyamiaye,adifa,adiga,adiga kaboy,adiga lele,adiga zanguina,adiga-kaboye,adigala,adigalla,adigan,adigaon,adigara,adigasawa,adigbli,adigbongbo,adigbongdo,adigena,adigeni,adighi,adigi,adigna,adigo,adigozalbegly,adigrat,adigui,adigun,adiguzel,adiguzeldamlari,adih,adih nazrow,adihao,adiiloum,adija,adijale,adijinkpor,adijk,adijn,adike,adikan,adikankourou,adikarigama,adikarigoda,adikarimulla,adikariya,adikasy,adikayevka,adiker,adikkandiya,adikokoi,adikokwa,adikomama,adikota,adikou,adikovo,adikpe,adikpo,adikro,adikro fondi,adikroaba,adikuma,adikuta,adil,adil badir,adil makmur,adil meadows,adil sehro,adil sipra,adil sipra khurd,adil supra,adil tagar,adil,adil-khalk,adil-otar,adil-yangiyurt,adila,adila-krimmi,adila-tagakula,adila-tagakyula,adilabad,adilang,adilangu,adilcevaz,adile,adileva,adilewih,adilgarh,adilges,adilhan,adilingga,adiller,adilli,adilly,adilmakmur,adiloba,adiloko,adilotar,adilovic mahala,adilovici,adiloyo,adilpur,adiluwih,adilwala,adilwala khu,adim,adim tamu,adima,adimadegon,adimakuj,adimale,adimanekouro,adimanu,adimi,adimli,adimo,adimova,adimovci,adimtamu,adimu,adimulya,adimulyo,adimulyo 1,adimulyo 3,adimulyo satu,adimulyo tiga,adin,adin chinah,adin cina,adin gul kili,adin kala,adin kalay,adin kelay,adin khel,adin kheyl,adin shah kot,adin zai,adina,adina masjid,adinagayenes,adinah,adinah jar,adinah khel,adinah masjid,adinai,adinakhel,adinakheyl,adinamasdzhid,adinamonu,adinan,adinaonao,adinar,adinath,adinath bazar,adinca,adincata,adinchina,adindan,adindow,adinegara,adineh,adineh masjed-e `olya,adineh masjed-e bala,adineh masjed-e pain,adineh masjed-e sofla,adineh qal`eh,adineh qoli,adinehkheyl,adinet,adinfer,ading,adinga,adingaranamoun,adinge,adingeiro,adingi,adingol,adini,adiniky,adinimole,adinkala,adinkerke,adinkhel,adinkheyl,adinkheyl,adinkia,adinkrakrom,adinkrakurom,adinnga,adino,adinobay,adinow,adinsoone,adinsung,adintasi,adintiasi,adinuso,adinya,adinzai,adinzi,adio,adio station,adioba,adioblessou,adiobo,adiogouansou,adiokro,adiolessou,adiomakro,adiomoune,adiongouasou,adiongouassou,adiope,adiopo doume,adios,adiota,adioti,adioufa,adiougbe,adiougou,adioumanekro,adioumaniko,adioumanikro,adioumanikrou,adioumbereti,adipala,adipanka,adipas,adipasir,adipati,adipolo,adippola,adipriya,adipur,adipurwa,adiquinhao,adir,adira,adira mela,adirah,adiraja,adirampatnam,adirampattinam,adirangam,adirasa,adire kalay,adire kelay,adire kope,adire nakka,adireh,adireja,adireja kulon,adireja wetan,adirejo,adirejo 1,adirejo 2,adirejo dua,adirejo satu,adiresa,adiretno,adiri,adirilla-kalay,adirim,adirogo,adirondack,adirullah kelay,adis,adis `alem,adis `alem (1),adis abeba,adis amba,adis hiywet,adis kidame,adis kiny,adis zemen,adisa,adisadel,adisadel village,adisana,adisana kidul,adisana lor,adisara,adisara kulon,adisatrambe,adiscia,adise,adiseou,adisge,adish,adishchevo,adishe,adishi,adisicia,adiso,adisono,adissakakro,adissan,adiste,adisu karamile,adita,adital,aditaypur,aditekorasi,aditoyo,aditra nadi,adits mill,aditsre korpe,adityana,adityapasa,adityapur,adityman,adiuia,adiura,adiva,adivar,adiveme,adivino,adivino amaya,adivra,adiwala,adiwarna,adiwarno,adiwerna,adiwerno,adiwerud,adiwukir,adiwulan,adiya,adiya michire,adiyakulam,adiyaman,adiyan,adiye,adiyere,adiyi masjid,adizai,adizai,adizay,adizeh,adizlu,adizona,adja,adja ngbi koffikro,adjab,adjabani,adjabikro,adjablekope,adjaboukope,adjache,adjachi akattou,adjacin,adjaconti,adjacouti,adjacoutie,adjacouty,adjadabia,adjadebe,adjadjagon,adjadji bata,adjadji-zoungbome,adjadohondegon,adjafa,adjaga,adjagbo,adjaglame,adjah,adjaha,adjahakpa,adjahil,adjahome,adjahoun,adjain,adjaine,adjaite,adjakakope,adjakoe,adjakokou,adjakope,adjakouti,adjakove,adjakpa,adjakpekope,adjakpodji,adjakpohoue,adjala,adjama,adjambe ada,adjame,adjamibang,adjamingon,adjamoe-kondre,adjan,adjana,adjana-teich,adjandounga,adjang,adjanohoue,adjanouhoue,adjante,adjaouere,adjap,adjara,adjaradome adjarado,adjarra,adjatogo,adjauru,adjave,adjavi,adjavon,adjawola,adjayo,adjazon,adje,adjegan kope,adjegnio kope,adjehongon,adjeide,adjekemevor,adjekota,adjekro,adjekuo,adjekuo i,adjekuo ii,adjel,adjela,adjela i,adjelilie,adjelon,adjena,adjenuko,adjenyokope,adjeouro,adjeroud,adjetakope,adji,adjia,adjiable kope,adjian,adjiba,adjibarang,adjibata,adjibo,adjibuhara,adjicoa,adjid,adjidjahe,adjido,adjido deka,adjidome,adjidougou,adjiebo,adjied,adjifou,adjifou deux,adjifou un,adjiga,adjigeme,adjigidi,adjigiol,adjigo,adjiguidi,adjikagoengan,adjikagungan,adjikame,adjiko,adjila,adjilamina,adjilaminina,adjilele,adjiler,adjilidji,adjiloum,adjim,adjimi,adjin,adjina,adjinembah,adjing,adjinn,adjipemanggilan,adjipo,adjira kope,adjire kouba kondji,adjirekope,adjiri,adjivon,adjivou,adjkrede,adjlun,adjmari,adjobani,adjobeti kope,adjoda,adjodakope,adjodi nguessan,adjodo,adjodogou,adjodol,adjoe,adjoekpa,adjoemakondre,adjogbenouko,adjogble,adjogidi,adjoguidi,adjohon,adjohoun,adjohoundja,adjohoundje,adjokurumo,adjolo,adjomanikope,adjomi,adjong,adjonikope,adjop,adjora,adjou,adjouan,adjouga,adjougba,adjouhan,adjouma,adjoux,adjove,adjovicodji,adjovime,adjoya,adjozoume,adjrhala kope,adju,adjua,adjuah,adjuassou,adjud,adjudeni,adjudu nou,adjudu vechi,adjudu-vechiu,adjudul-vechi,adjudul-vechiu,adjue,adjugopi,adjum,adjumani,adjungbilly,adjunta,adjunta de los arroyos,adjuntas,adjuntas de mier,adjusta,adk,adkala,adkan,adkar djombo,adkepai tchokoukon,adkhass,adkhes,adki,adkin hill,adkins,adkins mill,adkip,adkot,adku,adl,adla,adlai,adlakka,adlal,adlalas,adlales,adlana,adlana dhudi,adland,adlandsvik,adlandsviki,adlanz,adlao-tubang,adlaon,adlauan,adlawan,adlawon,adlay,adlaye sidi,adlberg,adldorf,adle machhi,adlegi,adlekro,adler,adler-kosteletz,adlerova hut,adlerovka,adlersberg,adlersdorf,adlershof,adlershorst,adlershorst kreis neidenburg,adlerswalde,adlerszollos,adlesici,adlestrop,adlet touila,adletzberg,adley,adlfurt,adlhaming,adlhausen,adlholz,adli,adlia,adlig bartelshagen,adlig barwalde,adlig blumenau,adlig boltenhagen,adlig camiontken,adlig jucha,adlig kamiontken,adlig kessel,adlig kremitten,adlig neuendorf,adlig pohren,adlig popelken,adlig rakowen,adlig reetz,adlig shraytlyaugken,adlig symken,adlig wolka,adlig wolla,adlig-fliessdorf,adlige dubrau,adligenswil,adligwolken,adlijo,adlike,adlin,adline,adling,adling kamionken,adlingfleet,adlington,adliswil,adlitz,adlitzgraben,adliya,adliye,adlkofen,adlmannstein,adlmorting,adlo machhi,adloan,adlochi,adloun,adlu,adlum,adlusan,adlwang,adlwarting,adlye,adma,adma ad dafnah,adma et defne,admal,admam,adman,admani,admani noh,admannshagen,admannshagen-bargeshagen,admano,admapur,admard,admase,admaston,admatene,adme tsehay,adme tsekhay,admer,admi-yab,adminani,admington,administracion,administracion lunardelli,admirable,admiracion,admiral,admiral heights,admiral town,admiral trailer park,admirals town,admirals beach,admirals hill,admirals walk,admiralteyskaya sloboda,admiralty bay,admiralty village,admire,admit,admodhorom,admont,admontbichl,admoun,adna,adnagulova,adnagulovo,adnali,adnaly,adnami,adnan menderes,adnar,adnavik,adne,adner,adneram,adneskor,adnet,adnian,adnim,adnyal,adnyarsari,ado,ado awaiye,ado daffou,ado garang,ado kalay,ado kelay,ado khel,ado kompani,ado odo,ado rai,ado sarwani,ado sombe,ado-ekiti,ado-tym,ado-tymovo,adoa,adoain,adoar,adobal,adobana,adobe,adobe corner,adobe crossing,adobe mountain trailer park,adobe ranch,adoberayoc,adobes,adobewora,adobewura,adobi,adobou,adobou i,adobou ii,adobu,adoca,adodle,adodo,adodoe,adodoi,adodoyi,adodu,adoduwa,adoe,adoea,adoeio,adoenara,adoes,adoeyo,adofin,adofure,adogblihoue,adogi,adogli,adoglihoue,adogo,adogon,adoguie,adohi,adohosou,adohossou,adohoukparou,adohoun,adohoun-lanzoume,adohro,adoi,adok,adoka,adoke,adoke kalan,adokheyl,adoki,adokiakiri,adoko,adokobisi,adokoi,adokope,adokor,adokpo,adokpoue,adokpwe,adokroasi,adokwae,adol,adola,adolagbene,adole,adolfa ruiz cortines,adolfin,adolfmajor,adolfo,adolfo alsina,adolfo conder,adolfo correia,adolfo cortinez,adolfo de la huerta,adolfo e. carranza,adolfo gonzales chaves,adolfo konder,adolfo lopez mateos,adolfo rios,adolfo ruiz cortines,adolfo ruiz cortinez,adolfo van praet,adolfov,adolfovac,adolfovac pusta,adolfovice,adolfovo,adolfovo selo,adolfow,adolfowo,adolfsberg,adolfsfors,adolfsgluck,adolfsgrun,adolfshausen,adolfsheide,adolfshutte,adolfskoog,adolfsstrom,adolfstrom,adoli,adolina,adolina 5,adolina lima,adoloko,adolokope,adoloseimo,adolph,adolpho conder,adolpho condor,adolphsdorf,adolphseck,adolphsheide,adolphus,adolzfurt,adolzhausen,adom,adoma,adomaiciai,adomaitse,adomakariri,adomanu,adomanya,adomava,adomciukai,adome,adomekwa,adomfe,adomfi,adomi,adomi abra,adomine,adomines,adomishken,adomishkyay,adomiskes,adomiskiai,adomiskis,adomme,adomo,adomougon,adomskiy,adomyne,adon,adona,adona sughaiyir,adonara,adong,adonga,adongni,adongon,adongoyere,adoni,adonibizeka,adonijah,adonikrom,adonis,adonkia,adonkolo,adonkoua,adonkrono,adonkwanta,adonokofe,adonokorpe,adonrokpe,adonsi,adonso,adonsua,adonta,adony,adonyevo,adonyimo,adoo,adooji,adop,adope,adopisco,adopo,ador,adora,adorable,adorah,adorbu,adoretes,adorf,adorf freiberg,adorf-jugelsburg,adorf-remtengrun,adorgue,adori,adoria,adorigo,adorior oda,adorjan,adorjanfalva,adorjanpuszta,adorjantanya,adorjanujtelep,adorjas,adormiskes,adorot,adorp,adoru,adorunta khwara,adorunta khwarah,adorunte khwara,adorye,ados,adosh,adoso,adossa,adosso,adot,adota,adotekofe,adotekorpe,adotim,adotokope,adou,adou de cima,adou dia,adou gbohaou,adou ncho,adou yapi,adoua,adouagoassopie,adouaie ekpome,adouakouakro,adouaprikro,adouar,adouar amaich,adouar nyahya,adouar sidi ali,adouar steih,adouaz,adoubatou,adoucun,adouda,adoudieh,adoudji,adoudouba,adoue,adoufe,adougba,adougoul,adougouli,adouia,adouifir,adouifire,adoukbe,adouke,adouki,adoukoe,adoukoffikro,adoukofikro,adoukore,adoukoue,adoukro,adoukrom,adouksi,adoum,adoum saap,adouma,adoumandjali,adoumanga,adoumangan,adoumbanziri,adoumchi,adoume,adoumkroum,adoumre,adoumri,adoun bram,adoune bram,adoungous,adounikro,adouooui,adouowi,adourakame,adourho kondji,adous,adouz,adouz nsidi mansour,adouz sidi mansair,adouze,adoves,adovis,adovo,adovshchina,adowa,adowa number 1,adowa number 2,adowakrom,adowal,adowala,adowali,adowana,adowar,adowdar,adoweko,adowkrom,adowr,adowso,adowzay,adowzi,adoy,adoyeva,adoyevo,adoyevshchina,adoyevshchino,adoyunan,adozai,adozai kili,adozay,adozi,adozo zrandah,adpar,adpur,adra,adra ramchandrapur,adraa,adrada de haza,adrada de piron,adradas,adrado,adrados,adraga,adragonte,adrahimzai,adrahman,adrakos,adrakpo,adraku,adrakula,adrakyulya,adrales,adrall,adramand,adramund,adramyti,adramyttium,adramzai,adran,adran ntanart,adrana,adranah,adranga,adrani,adrano,adranos,adrao,adraouiyine,adrar,adrar farnou,adrar n tament oufella,adrar n tament ouzedder,adrar nait illoul,adrar ntamennt,adrasan,adraskan,adraskand,adrasman,adrasmon,adrazhofen,adre,adreh,adremane,adres kheyl,adreskan,adreskhel,adreyan,adrhass nou erfallen,adrhass nou erfallene,adrhass nouafil,adrhes,adrhirh,adrhous,adri,adria,adriaanshoop,adriach,adriambe,adrian,adrian furnace,adrian martinez,adrian mines,adrian salinas,adriana,adrianas,adrianes,adriano,adriano potes,adrianopel,adrianople,adrianopol,adrianopoli,adrianopolis,adrianoupolis,adrianoutherai,adrianovka,adrianovo,adrianovskaya,adrianovskiy,adriansnas,adriantelep,adrianu mare,adrianu mic,adrianul mare,adrianul mic,adrias,adriatik,adriatiku,adriatsweiler,adriene morano,adrienne-puszta,adriers,adrigole,adrijanci,adrik akoulkouli,adrik akoulkoulis,adrik benaouay,adrika,adrikha,adriku,adrina,adrine,adring,adrinnaya,adrino,adrinople,adris khel,adriskan,adriskheyl,adrissiye,adrivale,adriyana,adrkan,adro,adro kala,adro khel,adro velho,adroba,adroban,adrokhel,adrokheyl,adros,adrou,adroug,adroug nait ameur,adrouine,adrouj,adroukro,adrousai,adrouzal,adrovac,adrovici,adrow kheyl,adrspach,adruban,adrud,adruh,adrukala,adrume,adrur,adrynta,adryushchenko,adsa,adsar,adsaya,adsbol,adscheid,adseke,adserballe,adseux,adshijask,adshikabul,adsit,adsiz,adslev,adstock,adstone,adsubia,adsul,adsulim,adtoyon,adtugan,adtuyon,adu,adu bariye,adu garhi,adu khan,adu khaskheli,adu kili,adu kwadzo,adu wamasi,adu-achi,adu-ama,adu-yurt,adua,aduahensu,aduai,aduam,aduamoa,aduana,aduana del sasabe,aduana el oasis,aduanas,aduanchensika,aduanichensika,aduap,aduard,aduardervoorwerk,aduarderzijl,aduari,aduari duna,aduas,aduasa,aduatu,aduazero,adub,aduba,adubaengi,adubease,adubia,adubiaran,adubiase,adubiasi,adubitin,adubo,aduboi,adubra,adubrem,adubrim,adubuwe,aduchua,aduchwie,adud,adudo,adudong,adudongu,adudu,adueasi,aduezi,adugan,adugao,adugbe,adugboku,aduge,adugh,adugi,adugiri,adugu,adugyaa,adugyansu akomada,aduhanhan,adujanso,adujewo,adujo wand,aduk,adukai,adukalay,adukheyl,aduki,adukiri,adukkalamoddai,adukkane,adukkanewatuyaya,adukmakhi,aduko,adukofi,adukope,adukpa,adukpo,adukra,adukro and cottages,adukrom,aduku,adukuia,adukuma,adukumama,adukuro,adukurom,adukwajo,adukwajokrom,adukwaokrom,adukwatukrom,adukwesikrom,adul,adul buzurg,adul khurd,adula,adulai,adulala,adulan,adulay,aduliena,adull,adullahabad,aduloju,adulpoor,adulu,adulumbuique,adum,adum afaw,adum afo,adum banso,adum bansu,adum east,adum kirkeby,adum west,aduma,adumadum,adumama,aduman,adumanya,adumasa,adumasi,adumata,adumde,adumi,adumnieki,adumo,adumoa,adumovo,adumposu,adumre,adumu igbagu,adun,adun beach,adun gol,adun qulu,aduna,adunajikope,adunandra,adunania,adunara,adunati,adunatii de geormane,adunatii sirbeni,adunatii teiului,adunatii-copaceni,adunatii-proviti,adunatii-provitii,adundut,adung,adung long,aduni,adunis,adunkioy,adunku,adunkwanta,adunovskiy,adunu,adupa,adupai,adupay,adupe,adupire,adupli,aduposo,adupre,adupur,aduqin,adur,adur district,adurao,adurasta,adurdet,adurelan,adurelang,aduri,aduriameina,adurimeina,aduripalle,aduru,adurunte khwarah,adurunte-khvara,adus,adusa,adusikrom,adustina,adusu,adusuazo,adusukofe,adusukorpe,aduta,adutishkis,adutiskis,aduto,adutor,aduturai,adutwie,adutwum,aduwa,aduwa masa,aduwala,aduwali,aduware,aduwawa,aduy,aduyao,aduyawkurom,aduyeli,aduyevka,aduyevo,aduyili,aduyiri,aduyongan,aduyungan,aduza`i,aduzai,aduzai kili,aduzay,advagar,advakel,adval,advance,advance mills,advance mills village,advancetown,advent,advent crossroads,adventura,adventure,adventure bay,adventure condominium,adverse,advi devalpalli,advie,adviento,adville,advincula,advise,advocate harbour,advocate hill farms,advocates choice,advokatovka,advokatovo,advolpale,advoritsa,advoritsy,advota,adwa,adwa kesa,adwa kessa,adwafo,adwajiri,adwal,adwana,adwani,adwari,adwe,adwenso,adwet,adwick le street,adwifir,adwinase,adwinase number 2,adwoa,adwoafua,adwobue,adwojo,adwoki,adwolf,adwor,adwuafokurom,adwum,adwumadiem,adwumakase kese,adwumakase wadie,adwumam,adwuman,adwumdium,ady,ady estates,adya,adyabasi,adyal,adyaman,adyamibang,adyamigurt,adyanabangou,adyanov,adyap,adyar,adyaz,adycha,adyebe,adyelon,adyena kondji,adyengo,adyenyenya,adyeville,adygalakh,adyge-khabl,adygei,adygeskiy,adygey,adygeysk,adyk,adykbay,adyl-yaly,adyliget,adylinn,adylyal,adyman,adymash,adymchokrak,adynaly,adynkata,adyo bagh,adyongobaro,adyr,adyr-kezhig,adyrkaya,adyshevo,adytapur,adyushkino,adza,adzabassi,adzabikat i,adzabikat ii,adzabikat iii,adzabilon,adzabilone,adzahoun,adzak,adzakate,adzake,adzakoe,adzamovci,adzan,adzane,adzaneta,adzaneta de albaida,adzanu,adzap,adzap i,adzap ii,adzapakope,adzapana,adzar,adzarlar,adzas,adzato,adze,adze muizas centrs,adzebe,adzebe i,adzebe ii,adzegegbor,adzekorpe,adzemka,adzemler,adzenkotoku,adzentemu,adzenzoumou,adzes,adzgara,adzha-kabul,adzhabshagi,adzhakend,adzhakhu,adzhakhur,adzhakhuroba,adzhal,adzhami,adzhamka,adzhamki,adzhamovka,adzhamovskiy,adzhampazra,adzhanov,adzhanovka,adzhar,adzhara,adzharband,adzharis tskhalskaya,adzharisagmarti,adzharistskali,adzharpynar,adzhatin,adzhaykat,adzhazkey,adzhdikhatu,adzhebadzh,adzhelikipchak,adzhemler,adzherekty,adzherkey,adzhi,adzhi akhmat,adzhi atman,adzhi-isakend,adzhi-kuyu,adzhi-madzhi-gat-yurt,adzhiakhmat,adzhiaska,adzhibadzh,adzhibay,adzhibulat,adzhida,adzhidada,adzhidere,adzhieli,adzhievo,adzhigan,adzhigiol,adzhigmey-efendi,adzhigol,adzhiibram,adzhiis-kendi,adzhikabul,adzhikadiry,adzhikarakashly,adzhikech,adzhikend,adzhikui,adzhikulevo,adzhilare,adzhiliy,adzhilu,adzhily,adzhim,adzhim-kishlaki,adzhima,adzhimambet,adzhimazhagatyurt,adzhimchigra,adzhimi,adzhimushkay,adzhin-khuduk,adzhioluk,adzhir-amra,adzhiran,adzhirkhaydakh,adzhiron,adzhished,adzhitarkhan,adzhitarovo,adzhiya,adzhiyab,adzhiyask,adzhiyely,adzhkhakhara,adzhkhur,adzhmatvara,adzhrum,adzhrym,adzhsiatman,adzhuny,adzhylygbina,adzhylykhbina,adzhymushkay,adzhyuny,adzhyyab,adzi,adziba,adzibegovo,adzibzhara,adzica mala,adzica ograde,adzici,adzie,adzie i,adzie ii,adzievci,adzigezh,adzije,adzijevci,adzijin potok,adzikpo,adzikpo yokunya,adzimatovo,adzina ravan,adzinci,adzine livade,adzini,adzino selo,adzintam,adzinukorpe,adzitarovo,adziuni,adzjuni,adzkhapsha,adzkhida,adzkhyda,adzlagara,adzlagokorpe,adzo,adzokoe,adzomanikorpe,adzope,adzoveme,adzovice,adzovici,adzt,adztkhida,adzu viensetas,adzuani,adzubzha,adzugopi,adzuma,adzuna,adzunge,adzuni,adzusta,adzva,adzvavom,adzvi,adzvibzha,adzvisi,adzvisi nizhniye,adzvisi verkhniye,adzvistavi,adzyaka kope,adzybzhara,adzyubzha,aeakwato,aea,aeamdong,aean,aeaun bridge,aeba,aebara,aebelnaes,aebeltoft,aebere,aebi,aebomba,aebtissinwisch,aebubu,aebukuru,aechang,aechanni,aecherli,aecholli,aechon,aechonni,aechori,aechternhagen,aeckenmatt,aedai,aedan,aedan pantai,aedang,aedangdong,aedangpo,aedari,aedeo,aedermannsdorf,aedipsos,aedo,aedoa,aedodong,aedong,aedonodongjagu,aedori,aeepo,aefeo,aefua,aegae,aegaleo,aegana,aegela,aegialis,aegidienberg,aegigol,aegina,aegion,aegir,aegisangol,aegium,aegoe,aegoji,aegongni,aegum,aegvidu,aegviidu,aegviydu,aegye,aegyo,aegypten,aehaleshwar,aehoen,aehringhausen,aehui,aehureboana,aeiker ford,aeisa,aejo goth,aejon,aejong,aejongni,aejowolli,aek badan,aek batar,aek belu,aek gala-gala,aek godang,aek habil,aek horsih,aek horsik,aek humbang,aek jalu,aek kanopan,aek korsik,aek kuasan,aek ledong,aek loba,aek loba afdeling i,aek loba afdeling satu,aek lobu,aek marsasar,aek meranti,aek mompang,aek mual,aek nabuntu,aek nagali,aek nahuta,aek najonggi,aek nauli,aek nokal,aek papan,aek polan,aek range,aek raso,aek sakur,aek sihim,aek sosorseam,aek suha,aek tangga,aek tarum,aek toba pakan,aek-kanan,aeka,aekanopan,aekao,aekbadak,aekbolon,aekbota,aekeli,aekeu,aekgadang,aekgambir,aekgodang,aekgorak,aekgorat,aekhorsik,aekinga,aekioy,aekkanopan,aekkela,aekkoeasan,aekkupang,aeklan,aekmas 1,aekmas 2,aekmas dua,aekmas satu,aekmatara,aekmuaraboek,aekmuarobolak,aeknabara,aeknabara-tonga,aeknabirong,aeknabontor,aeknadua,aeknagodang,aeknaororan,aeknasia,aeknatolu,aeknauli,aeko,aekoensim,aekole,aekoro,aekpaoeng,aekpatik,aekpaung,aekputih,aekraja,aekrihit,aeksibin,aeksuhat,aektalun,aektangoen,aektangun,aektolang,aektolong,aektumpaan,aekunsim,aekutu,ael,aela,aelande,aelbeke,aele,aelegrete,aelgau,aelgauli,aelggi alm,aelggi alp,aeli,aelilokken,aeloga,aelong lok,aelou kouadiokro,aelst,aeltebey,aeltre,aeltrebrugge,aemalu,aembatoe,aemberga,aemboi,aembonga,aemea,aemeo,aemere,aemisil,aemura,aemuri,aen,aena,aenaaiwasi,aenaangingi,aenae,aenak,aenakaro,aenalite,aenamdong,aenangni,aenariki,aenathaia,aeneas,aenes,aeng,aeng pok,aenganjar,aenganyar,aengbanger,aengbatoebatoe,aengbatubatu,aengceleng,aengchonni,aengdangdong,aengenesch,aenget,aenggari,aenggok,aenghwadong,aenglandah,aengmitae,aengmuchon,aengmudong,aengnyono,aengrasa daya,aengrasa laok,aengrinbo,aengsandong,aengsanggol,aengsangkol,aengsono,aengtelor,aengtowa,aengue,aeni koy kwara,aeni-koye-koara,aenon town,aenus,aeolus valley,aeopea,aepetu,aepfelbach,aepodong,aepok,aepori,aepukl,aer,aer besar,aer kasar,aer mati,aerai,aeramo,aerari,aerbeck,aerbin,aerboeaja,aerbuaja,aerbuaya,aercochoch,aerd-appelhoek,aerdang,aerde,aerdenhout,aerding,aerdjeruk,aerdt,aere,aere gollere,aere mbar,aerea,aerem,aergale,aerhhsiang,aerhhsien,aerhshan,aerhshankuangchuan,aerhshankuangshuiyuan,aerhshanpaoliko,aerhshantu,aerhshanwenchuan,aerhtai,aerhtaimien,aerhtaimienchen,aerhtanaola,aerhtanomolo,aerhtanomona,aerhtunchuko,aerhtunchukochunghsinchu,aerhtunchukohasakotsutzuchihchu,aeri,aerial,aerial acres,aerianwala,aerinon,aerinos,aerita,aeritam,aerjeruk,aerle,aermanis,aermata,aermatong,aermolek,aernan,aerne,aernen,aero acres,aero vista,aerodrom,aerodromnyy,aeroflotskiy,aeroflotskyy,aerokhori,aerokro,aerolito,aerom,aeroplane conde,aeroplane konde,aeroport,aeroporto a,aeroporto b,aeropuerto,aeropuerto barrio,aeroskiobing,aeroskjobing,aeroskobing,aerotoro,aerrejiadadou,aerschot,aerseele,aershan shi,aersilang,aertbolle,aertembaga,aertrycke,aertselaer,aervik,aerwasi,aerxia gongmaduo,aerzen,aes,aesandong,aesanpo,aesch,aeschach,aeschau,aescher,aeschi,aeschlen,aeschlenberg,aesela,aesi,aesira,aeso,aesoo,aesow,aespa,aesule,aesunia,aesvik,aeta,aetado,aeteka,aeteke,aethra,aetna,aetna springs,aetnaville,aeto,aetochori,aetochorion,aetofolea,aetofolia,aetokhori,aetokhorion,aetokorifi,aetolfos,aetoliko,aetolofos,aetomilitsa,aetopetra,aetorrachi,aetorrakhi,aetos,aetovouni,aetovounion,aetsa,aettenschwil,aettumi,aetungu,aeudong,aeugst,aeula,aeule,aewa,aewae,aewe,aewol,aewolli,aewonni,aeyangni,aeyangri,aeyangwon,ae\303\260uvik,af,af abeb,af abed,af barwaaqo,af ceua,af ed,af gedud,af gudud,af gudut,af karin,af may,af ruugleey,af uen,af yar,afansok-abenelang,afa,afa inellas,afa inflas,afa n id nasser,afa nbou hamed,afa nid naseur,afa ntiki,afa nid,afaannam,afaansok,afa-afa,afa`ansok-esong,afa`i,afaabitom,afaafo,afaahiti,afaanam,afaansoc,afabet,afabeusse,afabitom,afachikha,afachuang,afad,afadade,afade,afadekou,afadi asiyie,afadi esiyie,afadie,afadja,afadonou,afadonou kondji,afadrik nouzannou,afaetam,afaetan,afaf,afafanyi,afaflai,afaga,afagayong,afagba,afagenzork,afagha obio enwang,afagnagnan,afagnan-bletta,afago,afagome,afaha,afaha abia,afaha atai,afaha eduok,afaha eket,afaha ekpene edi,afaha esang,afaha ibesikpo,afaha ikot,afaha ikot adaha,afaha ikot udoe,afaha itam,afaha itiat,afaha obio offiong,afaha obio ofiong,afaha obong,afaha offiong,afaha offot,afaha oku,afaha ube,afaha udo eyod,afaha udo oko,afaha ukwa,afaha uqua,afaha-ise,afaha-nsai,afahan obong,afahana itak,afahani,afahiakobo,afaiane,afaina,afaioui,afaisi,afaja belo,afak-medzim,afaka,afakazock,afake,afakhfakh,afaki,afakituru,afakma,afakodja,afakowada,afakpara,afalata,afalawas,afalba,afalkati,afallo,afalonas,afaloti,afalt,afam,afam uko,afam uku,afam-nta,afam-uku,afaman,afamanso,afamatuna,afambo,afami,afamju,afamonofi,afamve,afan,afan nzogh,afan samabanga,afana,afanabang,afanai,afanal,afanangbe,afanangui,afanaoudam,afanasevka,afanasyevka,afanasievca,afanasikha,afanasino,afanasiyevka,afanasiyevskiy,afanaskova,afanaskovo,afanasova,afanasova sloboda,afanasovka,afanasovo,afanasovo-shibanovo,afanasovskaya,afanassola,afanasyeva,afanasyeva gora,afanasyeva selga,afanasyeva sloboda,afanasyeviskiy,afanasyevk,afanasyevka,afanasyevo,afanasyevskaya,afanasyevskaya gidroelektrostantsiya,afanasyevskiy,afanasyevskiy postik,afanasyevskiye,afanasyevskoye,afanata,afanawa,afanbo,afandi,afandi colony,afandilar,afandiya,afandou,afane-mendone,afanebang,afanengong,afanengui,afanesele,afanetabini,afanetouana,afang,afangbedji,afangnagan,afango,afangombona,afanine kouka,afanjaga,afanji,afanli,afanoskovo,afanosova,afanosovo,afanour,afanoveng,afanoya,afanque,afansoc,afantou,afanya,afanyan bleta,afanyangan,afao,afao-ekiti,afaode,afaoui,afaq,afaqi,afar,afar daar,afar iato,afara,afarah,afaran,afard,afareaitu,afareyanaj,afarga,afargal,afari,afarian-e bala,afarian-e pain,afarianach,afarianaj,afarin,afarin-e bala,afarin-e pain,afaringba,afarisoky,afarma,afarnel foqui,afarnes,afarnou,afarudbar,afaryanaj,afassa,afassani,afat,afata,afatalanga,afatauri,afate,afatianhu,afatiapur,afatli,afatlu,afatly,afatobo,afatodji kope,afatovo,afatyakasi,afaux,afauzouart,afavenzock mouelza,afaw owebe,afawgi,afawkope,afawtawkaw,afayade,afayane,afbor,afcadare,afchang,afcheh,afchi,afchun,afd 1,afd 3,afdal,afdam,afdega,afdeling,afdeling 1,afdeling 1 a,afdeling 1 doloksinumbah,afdeling 1 gunung para,afdeling 1 mayang,afdeling 1 seimangkei,afdeling 10,afdeling 10 mayang,afdeling 11,afdeling 11 mayang,afdeling 13,afdeling 14,afdeling 2,afdeling 2 dolokinumbah,afdeling 2 gunungbayu,afdeling 2 seimangkei,afdeling 22/8,afdeling 23/7,afdeling 24/6,afdeling 3,afdeling 3 bangun,afdeling 3 doloksinumbah,afdeling 3 gunungbayu,afdeling 3 mayang,afdeling 3 tanah gambus,afdeling 4,afdeling 4 dolokdinumbah,afdeling 4 gunungbayu,afdeling 4 mayang,afdeling 4 pondok kidul,afdeling 5,afdeling 5 a tinjoan,afdeling 5 b tinjoan,afdeling 5 gunungbayu,afdeling 5 maligas,afdeling 5 mayang,afdeling 6,afdeling 6 gunungbayu,afdeling 6 mayang,afdeling 7,afdeling 7 mayang,afdeling 7 pondok laut,afdeling 8,afdeling 8 mayang,afdeling 8 pondok laut,afdeling 9,afdeling 9 mayang,afdeling 9-10 gunungbayu,afdeling delapah,afdeling delapan mayang,afdeling delapan pondok laut,afdeling dua,afdeling dua bukit,afdeling dua doloksinumbah,afdeling dua gunungbayu,afdeling dua puluh dua/delapan,afdeling dua puluh empat/enam,afdeling dua puluh tiga/tujuh,afdeling dua seimangkei,afdeling dua/tiga,afdeling empat,afdeling empat belas,afdeling empat doloksinumbah,afdeling empat gunungbayu,afdeling empat mayang,afdeling empat pabatu,afdeling empat pondok kidul,afdeling enam,afdeling enam gunungbayu,afdeling enam mayang,afdeling h,afdeling i,afdeling i pabatu,afdeling ii bukit,afdeling ii/iii,afdeling iii,afdeling iv,afdeling iv pabatu,afdeling k,afdeling lima,afdeling lima a tinjoan,afdeling lima b tinjoan,afdeling lima gunungbayu,afdeling lima maligas,afdeling lima mayang,afdeling pondok 10,afdeling pondok kandanglembu,afdeling pondok sepuluh,afdeling pondok ullu,afdeling satu,afdeling satu a,afdeling satu bahbayu,afdeling satu doloksinmubah,afdeling satu gunung para,afdeling satu mayang,afdeling satu pabatu,afdeling satu seimangkei,afdeling sebelas,afdeling sebelas mayang,afdeling sembilan,afdeling sembilan mayang,afdeling sembilan-sepuluh gunungbayu,afdeling sepuluh,afdeling sepuluh mayang,afdeling tiga,afdeling tiga bangun,afdeling tiga belas,afdeling tiga doloksinumbah,afdeling tiga gunungbayu,afdeling tiga mayang,afdeling tiga tanah gambus,afdeling tujuh,afdeling tujuh mayang,afdeling tujuh pondok laut,afdeling vi,afdeling vii,afdeling1 bahbayu,afdelinggunungbayu,afdelingmalangsari,afdelingwonorejo,afdem,afden,afder,afdil,afdub,afdzal,afdzal kala,afdzal kalay,afdzal khel,afdzalkhel,afdzalkheyl,afe,afe niang,afe nigus,afeche,afechtal,afedaik,afeddouz nait oumchar,afedj,afedous nait oumrane,afefu,afega,afegame,afeggou,afeim,afeime,afeiteira,afejewe,afekouro,afel,afela nisly,afela yeghir,afelba,afella irhir,afella isli,afella izdaten,afella izdatene,afella n ou asif,afella n ou assif,afella ntakouchte,afella ouadel,afella ouzaghar,afella ouzarhar,afellah n ouassif,afellah nouassif,afellal,afelou,afem,afema,afeman,afembo,afenga,afenkir,afennsou,afennzouart,afensar,afensou,afenzouart,afeq,afere,afereakoboro,aferewa,afergal,afergoula,aferi,aferi i,aferi ii,aferi senago konakro,aferie,aferikha,aferin,aferingba,aferishchevo,aferissenago-konakro,aferkert,aferket,aferkovo,afermejan-e bala,afermejan-e pain,afermi,afern,aferni,afernou,afernou el fouki,afernou sefli,afernoun,afernu foki,afernun,aferovka,aferovo,afersa,afersigoua,afersigua,afersou,afert,afertane,aferuba,aferwang,aferyanech,aferyevskaya,aferyevskoye,aferzaz,afesi akura,afesikofe,afeso,afess,afessi,afet evleri,afetai,afetna,afeto,afetta,afevi,afeye,afeyi,afeza,afezero,afezez,afezza,affad,affalter,affalterbach,affalterhof,affaltern,affalterried,affalterthal,affalterwang,affaltrach,affame,affar,affat,affe,affecking,affele,affella ighir,affeln,affeltrangen,affendorf,affendouch,affenricht,affenrucken,affental,afferde,afferden,affere,affery,affesi,affhollerbach,affhupperhofe,affi,affi kakou,affi kamenan,affi ono,affia,affienou,affieraddo,affieux,affifisitie,affile,affing,affinghausen,affiniam,affinian,affinity,affirou,affivisiti,affivisitie,afflanda,affler,affleville,affligidos,affnay,affobakka,affoldern,affolterbach,affoltern,affoltern am albis,affon,affonor,affonso,affonso arinos,affonso camargo,affonso claudio,affonso penha,affonso penna,affonville,affouekro,affoukoua,affoux,affouzer,affracourt,affreville,affringues,affstatt,affton,affua,affula,afga,afgan,afganya,afgani-doshi,afgankha,afgha,afghahgar,afghan colony,afghan colony no. 2,afghan colony number two,afghan kachi basti,afghan kili,afghan kot,afghan muhajir camp,afghan refugees camp number six,afghan tapah,afghan tappeh,afghan tepa,afghan-e doshi,afghan-e dowshi,afghanabad,afghane dosi,afghanganj,afghanha,afghani,afghania,afghaniah,afghaniyah,afghanpur,afghanya,afghanyah,afgoi,afgoi addo,afgol ghiorghis,afgooy,afgooye,afgooye cadde,afgooye yare,afgoye,afguduudle,afguyn,afham,afhang,afhitsa,afholderbach,afholm,afhuug,afi,afia,afia ba demba,afia demba,afia demba jamanca,afia do governador,afia isong,afia nsit,afia usu,afia-meeting,afia-nkwo,afiadeayigba,afiadegniba,afiadenyiba,afiadenyigba,afiadenyigbaga,afiaelem,afiafi,afiafiso,afiamalu,afiaman,afianga,afianga fortori,afianya,afiaso,afiatt,afiawa lake,afid,afidegni,afidegniba,afidegnigba,afidegnigban,afidemgha,afidenigba,afidenioumba,afidenjonskpe,afidenyigba,afidenyigban,afidenyon-sikpe,afidenyukonyi,afidhnai,afidie,afidwase,afiefso,afieki,afienkouadioukro,afienou,afienya,afier addo,afier ado,afieraddo,afiere,afiesere,afife,afifi,afig,afih,afikim,afikni,afikope,afikpo,afikpo road,afikpo road station,afiladera,afilahou,afilal,afilala,afilayocc,afimini,afimino,afimtsevo,afimyanovo,afimyevo,afin,afinata,afindje,afineyev,afineyevo,afing,afini,afiniam,afinogenovka,afion,afion karahissar,afionas,afiosere,afioun-kara-hissar,afips,afipsip,afipskiy,afiq,afiqim,afir,afir imzourhene,afirkane,afis,afisamisoa,afishkino,afisia,afision,afissos,afitanananivo,afitialakofe,afitos,afium kara hissar,afium karahisar,afiun karahissar,afiun qarahisar,afiun-carahissar,afjad,afjad-e lenjan,afjan,afjed,afjed-e lenjan,afjeh,afji,afjiinow,afjord,afk,afkera,afkire,afka,afkadare,afkala haya,afkan,afkehasle,afkekhasle,afkhamabad,afkhan,afkhitsa,afkir,afkuiyeh,afkuh,afl ouaday,afla ouzaghar,aflagadjido,aflagatito,aflah,aflahu,aflaj,aflak,aflakion,aflamase,aflamase dohe,aflamasi,aflanda,aflao,aflar mohammed nait kadder,aflata,aflatun,aflatun mohallah,aflavenu,aflen,aflenz,aflenz an der sulm,aflenz kurort,aflex,aflige,afligidos,aflije,aflijes,aflime,afling,aflo,afloa,aflohera,afloisen,aflojsen,aflotuk,aflou,aflu,aflukakpoe asige,aflukofe,afmadoow,afmadow,afmadu,afmate,afna,afnai,afnami,afnay,afnia,afnish,afnori,afo,afo owebe,afo-ukwu,afoa,afoako,afoasi,afobaka,afochuang,afoda,afodamata,afode,afoderesi,afoderesi koyu,afodi,afodian,afodide,afodiobo,afodo,afodu,afodun,afoegbu,afoeloe,afogados,afogados da ingazeira,afogamgam,afogba,afogho bizi,afognak,afoia,afoiyufa,afojupa,afojupa-kekere,afojupa-nla,afokponoe,afolabi,afole,afolkey,afolu ibadan,afolu oba,afolu-modi,afom baba,afomadim,afomandyim,afombani,afombany,afommanchim,afon,afonkino,afonaskovo,afonasova,afonasovo,afonasovo pervoye,afonasovo vtoroye,afonasovskaya,afonassola,afonasyevo,afonasyevskiy,afonchikova,afong,afoni,afonichi,afonichi-malkovy,afonikha,afonikhe,afonikhino,afonin klyuch,afonina,afoniniskaya,afoninka,afonino,afonino-bobrovka,afonino-ramenye,afoninskaya,afonintsy,afonja,afonka,afonkaha,afonkina,afonkino,afono,afonori,afonosovo,afonovka,afonovo,afonovskaya,afonrinwo,afonsim,afonskii,afonso,afonso arinos,afonso bezerra,afonso calei,afonso claudio,afonso de paula,afonso guihole,afonso martins,afonso matsinhe,afonso maunge,afonso nonato,afonso pena,afonso souto,afonso vicente,afonso-pena,afonsopolis,afonsos,afonyata,afonyevka,afonyushenskiy,afoou,afopa,afopakorpe,afor,afor-ugwu,afore,aforenou,aforenu,aforie,aforoye,afors,afoso,afoss,afosu,afoto,afotobo,afotoko,afotoude,afou,afou-moke,afouanvam,afoud,afoud haddou ou daoud,afoud nid el haj ali,afoudenyiban,afoudi,afoudoko,afoudou,afoue-kokrekro,afouekro,afouibano,afoul,afouma-adzo,afoumvasso,afoumvassou,afoumvossou,afounassepie,afouni,afounkaba,afouno,afounvanso,afounvansso,afounvassou,afouraire,afourar,afourdene,afourer,afourighe,afourirh,afous,afouvanso,afouzar,afouzer,afouzzer,afowo doforo,afowowa,afoyasoro,afoye,afozuru,afqa,afqu,afquy,afr,afra,afra kati,afra koti,afra sara,afra takhi,afra takht,afraai,afrachal,afrad ounejjar,afradeh,afrag,afragola,afrah,afram,aframkrom,aframso,afrancho,afraneh,afranekurom,afrangua,afranikrom,afranio,afranio peixoto,afranji,afranse,afransi,afransie,afranso,afrante,afrantwo,afranyanga,afrapol,afrar,afrara,afrare,afras,afrasco,afraseyab,afrasiab,afrasiab kola,afrasiyab,afraskou,afrat,afrata,afratakht,afratakhteh,afratakhteh-e bala,afratakhteh-e pain,afratakhteh-ye bala,afratakhteh-ye pain,afrate haissoun,afrati,afration,afraton,afravandeh,afraya,afrayia,afrayias,afraz,afre,afrebo,afreda,afredton,afrefreso,afreko,afrengwa,afrenso,afress,afretu,afrhal,afriaane,afriat,afriate,africa,african jordan,africana,africanda,africo,afridar,afridi banda,afridiabad,afridian,afridougou,afridoukou,afries,afrij,afrik,afrika,afrikamajor,afrikanda,afrikanka,afrikanovka,afrikaskop,afrike,afrikum,afrim,afrim i ri,afrimakrom,afrimesti,afrimi,afrimi i ri,afrimkrom,afrinah,afrine,afrineh,afrineh-ye `olya,afrineh-ye bala,afrineh-ye pa`in,afrineh-ye sofla,afriski,afritz,afrive,afriyan,afriz,afrodhiti,afrodisia,afrokrom,afromei,afromey,afrosimovka,afrotcho,afroti,afrotokpoui,afrotrokpoui,afrouk,afrouk souf,afroxilia,afroxylia,afruve,afryat,afsabad,afsadik,afsahiyeh,afsal abad,afsapotuklu,afsar,afsar sogutlu,afsarabad,afsare bala,afsare nanaci,afsargidiric,afsargidiris,afsargidris,afsarguney,afsari,afsarikebir,afsarimam,afsarisagir,afsariyeh,afsariyeh kesb,afsariyyeh,afsarkarabogaz,afsarkarabogoz,afsarkhil,afsarli,afsarpotuklu,afsartarakci,afsarveran,afsay,afsched,afshai,afshan,afshan colony,afshar,afshar haq,afshar jeq,afshar jiq,afshar loo,afshar mohammad,afshar tataki,afshar-e bala,afshar-e darulaman,afshar-e nanachi,afshar-e nanahchi,afshar-e nanakchi,afshar-e nanehchi,afshar-e pain,afsharabad,afsharan,afshareh,afshari-bala,afshari-nanachi,afsharia,afshariyeh,afsharlu,afshijerd,afshjerd,afshord,afshun,afsik,afsil,afsin,afsine,afsne,afsnee,afso,afsondering,afst,afsu,afsuran,aftin,afta,aftab,aftab dar,aftab khan kalle,aftab khvordeh,aftab lagh,aftab nagar,aftab punjabi,aftab town,aftabachi,aftabaci,aftabaran,aftabeh,aftabehchi,aftabru,aftada kalay,aftadah kelay,aftadakalay,aftaka,aftane,aftar,aftara,aftas,aftas imarditsane,aftas imerditsene,aftas imsouane,aftas imsouwane,aftass imsouane,aftass imsoulane,afte let,aftelit,aftenet,afterbach,afterhausen,afterlee,aftersberg,aftersteg,aftes,aftes imsouane,aftholderbach,aftholderberg,aftis,aftoluk,afton,afton center,aftraet,aftret,aftris,aftriss,aftu,aftualab,aftunagzi,aftundere,afu,afua,afuaman,afuamoaso,afuda,afudodo,afuein,afufu,afugu,afui,afuje,afuji,afukpilla,afula illit,afule,afulei,afuli,afulu,afulugo,afumasi,afumati,afumats,afumba,afumpo,afumyinga,afun iju,afun ile,afunan,afunatam,afunino,afuntam,afuossa nabiole,afur,afura,afurada,afurada de baixo,afurada de cima,afurangu,afurca,afurdzha,afurefure,afurino,afurtu,afus,afusalo,afusing,afusing bato,afusing batu,afusing centro,afusing daga,afusing norte,afusing sur,afuso,afusu,afuta,afutala,afutoromanai,afutu,afutuakwa,afuwa,afuwacha,afuwagi,afuwari,afuye,afuz,afuze,afva,afvaliden,afvan,afvasjo,afvatrask,afvaudden,afvelsater,afvelsbol,afwa,afwerasi,afweyn,afwime,afwo,afyon,afyonkarahisar,afyushra-khu,afza,afzad,afzai khan,afzal,afzal colony,afzal dheri,afzal kala,afzal kalay,afzal kelay,afzal khan,afzal khel,afzal kheyl,afzal park,afzal town,afzalkala,afzalkalay,afzalkheyl,afzalabad,afzalabad-e bala,afzalabad-e pain,afzalabad-e sar kal,afzalahad,afzalap,afzalgarh,afzaliwali,afzalkheyl,afzalpur,afzar,afzarok,afzat,afziz,afzondering,afzou,ag,ag bolaq,ag chah,ag dhalla,ag digha,ag enzali,ag giogsi,ag jogsi,ag kalia,ag markenda,ag otluq,ag qal`eh,ag roail,ag simulia,ag tach,ag vlrane,ag-takla,ag- jethail,ag-agat,ag-ambulong,ag-bulag,ag-hur el-kubra,ag-hur el-raml,ag-hur el-sughra,ag-kerpi,ag-kishlag,ag-ni,aga,aga babou,aga basi,aga burut,aga gari,aga jari,aga olowo,aga saheb kalay,aga-akhmed,aga-baba,aga-batyr,aga-khangil,aga-mamedly,aga-many,aga-mirza-obasi,aga-mukhammedbay,aga-pirkend,aga-pustyn,aga-yazybudukh,aga-yazyoudukh,agab uorchei,agab worchei,agab workei,agabagi,agabagy,agabalou,agabama,agaban,agabar,agabara,agabayli,agabayyali,agabe,agabekalendzh,agabekhi,agabey,agabey-banysy,agabeybanisi,agabeyli,agabi,agabia,agabol,agabu,agabura,agac,agac koy,agaca,agacaim,agacaj,agacalan,agacalan koyu,agacalar,agacalti,agacanli,agacardi,agacasar,agacbasi,agacbeyli,agacbuku,agaccali,agaccami,agaccati,agacci,agaccia,agaccillar,agacdere,agacdibi,agaceli,agacgecit,agacgedik,agach,agach ayry,agach-arvat,agach-dzhan,agachan,agachan koyu,agachaul,agachaulovko,agacheli,agachene,agacheulovka,agachi,agachina,agachisar,agachkala,agacho,agachty,agacik,agacin,agacina,agackent,agackese,agackise,agackonak,agackopru,agackorur,agackoy,agackoy yaylasi,agacli,agacli mahallesi,agaclidere,agacligol,agaclihuyuk,agaclik,agaclipinar,agacliyuk,agacoba,agacoren,agacoven,agacpinar,agacqala,agacsaray,agacseven,agacsever,agacuklu,agacyolu,agacyurdu,agad el kadir,agada,agada apesi,agada duro,agada odede,agadair,agadama,agadang,agadao,agade,agadega,agadem,agades,agadeve,agadey,agadez,agadhaiika,agadheika,agadi,agadi ou aziz,agadia,agadibek,agadibo,agadinskoy,agadir,agadir a tasguedelt,agadir abbou,agadir aguenousil,agadir ait ameur,agadir ait cherki,agadir aj jdid,agadir anzrou,agadir bou adane,agadir bou naga,agadir bou setta,agadir bouadan,agadir boulnas,agadir chems,agadir chicht,agadir defla,agadir djedid,agadir ej jdid,agadir el ghazi,agadir el hajj hammou,agadir el hana,agadir el hena,agadir el jedid,agadir el rhazi,agadir frouadou,agadir hourrach,agadir iatouin,agadir iatouine,agadir idrain,agadir idrane,agadir ifri,agadir ighir,agadir igouramen,agadir igouramene,agadir igourrou,agadir igr n rhar,agadir igr n rharar,agadir iguelfene,agadir ikechrane,agadir imezgaoun,agadir imouzgaoun,agadir inflass,agadir infless,agadir irigdalene,agadir irijdalene,agadir issardane,agadir isserdane,agadir izdar,agadir izder,agadir iznagene,agadir izouggouarhene,agadir izougouarhene,agadir izouika,agadir izri,agadir jdid,agadir jedid,agadir lghazi,agadir laaseri,agadir lajdid,agadir lbour,agadir lhour,agadir lrhaz,agadir lrhazi,agadir mansour mhndou,agadir melloul,agadir mohand,agadir mohand ou mansour,agadir n ougrramene,agadir n achej hammou,agadir n afra,agadir n ait sa,agadir n iblahn,agadir n iblalene,agadir n imaoun,agadir n iznaguen,agadir n iznaguene,agadir n larbi,agadir n ouigui,agadir n oumerhdir,agadir n sri,agadir n tafoukt,agadir n tahiounine,agadir n tahiounit,agadir n tala,agadir n tirsa,agadir nait ali,agadir nbourd,agadir nigourramen,agadir nigourramene,agadir nizdar,agadir nizder,agadir nlarbaa,agadir nlarbee,agadir ntafent,agadir ntigida,agadir nimajjad,agadir nimaoune,agadir ou anzizen,agadir ou aziz,agadir ouahziz,agadir ouamesse,agadir ouanzizene,agadir ouazeg,agadir oudir,agadir oufella,agadir oufra,agadir ougni,agadir ouzrou,agadir rmel,agadir roho,agadir sidi yacoub,agadir sidi yakoub,agadir tafouete,agadir tagaouaout,agadir tagoundaft,agadir tassaout,agadir tassawt,agadir tiout,agadir tislane,agadir tissinnt,agadir tissint,agadir tolba,agadir toudras,agadir touksous,agadir yattouyn,agadir zouggaghine,agadir zouggarhene,agadiram,agadirane,agadirene,agadiri iskounane,agadja,agadjakpe,agadjame,agadji,agadome,agadongsan,agadounde,agaduo,agadwin,agadyr,agadzhagly,agadzhaly,agadzham,agadzhan,agadzhanly,agadzi,agadzor,agaes,agaete,agaf,agafai,agafaie,agafal,agafanovka,agafaye,agafeni,agafino,agafintegu,agafo,agafo gumas,agafonikha,agafonitsa,agafonkova,agafonkovo,agafonova,agafonovka,agafonovo,agafonovskaja,agafonovskaya,agafonovskiy,agafony,agafour,agafra,agafton,agafy,agafyevka,agaga,agagable,agagan,agaganda,agagba,agagbe,agagchi,agagda,agaggio inferiore,agaggio superiore,agagin,agagli beg,agago,agagrao,agagulkalay,agaguman,agagyevo,agagza,agah,agah kolyai,agah-e `olya,agah-e bala,agah-e sofla,agaha calaiu,agaha-kilayu,agahang,agahay,agahi,agahun,agaiani,agaibani,agaie,agaiergongma,agaigau,again,againg,againi,agaisa,agaja,agaje,agajeju,agaji,agajiki,agakabiriyai,agakabiriyao,agakahya,agakalay,agakan,agakarez,agakhanly,agakheylu-kalay,agakhmed,agaki,agakiana,agakishi-bayli,agakishibeyli,agakoy,agaku,agakuru,agakyandy,agal,agal-agal,agala,agala kanda,agalaboda,agalacangange,agalachenki,agalade,agalaga,agalagama,agalagamuwa,agalagil,agalagu,agalai,agalaioi,agalak,agalakumbura,agalali,agalam,agalan,agalan koyu,agalane,agalani,agalar,agalarbayli,agalarbeyli,agalarobasi,agalarobasy,agalarseki,agalas,agalatova,agalatovo,agalawa,agalawatta,agalchaebi,agalegama,agalegedara,agalegu,agalekumbura,agalgal,agali,agali-khodzha,agali-sheykhu,agaliani,agalianos,agalianou,agalikand,agaliya,agaliyayla,agall,agalla,agallas,agalle,agalli,agallinj,agalliu,agallo,agallpampa,agally,agalma,agalnitsa,agalo,agaloke,agalon,agalpur,agalteca,agalu,agalusanan,agalusuanan,agaly,agaly pervyye,agaly tretye,agaly vtoryye,agalya,agalyk,agalyk-payan,agalykend,agalz,agam,agam ber,agam wiha,agama,agama kreek,agama lafia,agamachi,agamad,agamakriki,agamali,agamalioglu,agamaly,agamalylar,agamalyoglu,agamamedli,agamammadli,agaman,agamandia,agamani,agamboeran,agamburan,agambusa/tobo,agamcagam,agame,agame-ahotissa,agame-beme,agame-koutoukapa,agameda,agamede,agamedli,agamejekiri,agamenticus station,agamenticus village,agamet,agameti,agameto,agametta,agamezrasi,agamgbe,agamham,agamhar,agamhuiah,agami,agamina,agaminan taoussa,agamir,agamira,agamiry,agamitan,agamjaya,agamna,agamo,agamoia,agamonje,agampencara,agampodigama,agamsa,agamsaha,agamukhammed-kalay,agamulong,agamzalu,agan,agan wadero,agana,agana bo,agana num,aganadoa,aganagar,aganan,aganas,aganasi,aganatoku,aganay,aganay-kemir-basta,aganay-kemir-bastau,aganbegovici,aganche,agandagu,agandobaka,aganfor,agang,aganga,agangaro,agangi,agangjene,agango,aganhaw,aganho,agani,aganika,aganikha,aganino,aganlan,aganli,aganna,aganni,agano,aganoa,aganon,aganopol,aganotaiika,aganovici,aganrigan,agansou,aganta,agantan,agantere,aganty,agantziki,aganuma,aganur,aganus,aganusan,aganyan,aganyi,aganz,aganzhen,agaomi,agaoob,agaoua,agao"z,agap,agapa,agapana,agapang,agapanovo,agape,agapi,agapia,agapieni,agapikha,agapinar,agapino,agapita hacienda,agapito,agapitova,agapitovo,agapitovskiy,agaponova,agaponovo,agapovka,agapovo,agapovskiy,agapovtsy,agappola,agaps,agapy,agar,agar bridge,agar gul,agar khelanwala,agar panchaitan,agar tijkola,agar uen,agar zheri,agara,agarachi,agarada,agarade,agaraga,agaragimli,agaragimoba,agarahimoba,agarai,agaraimoba,agaraj,agarak,agarakadzor,agaraki,agaral,agaram,agaran,agarane,agaratu,agaratu somon,agaratu-sumu,agarauda,agarauna,agaravak,agaraz,agarbiciu,agarbiciul,agarboale,agarchik,agarcia,agard,agardane,agardari,agarde,agarde nougadir,agarde noulili,agardi villatelep,agardidulo,agardivolgy,agardpuszta,agardy,agardytelep,agardzha,agare,agareb,agarebi,agaregama,agaregedara,agaregedera,agareh,agarenskiy,agarent,agareva glemzina,agarevka,agarez,agarfa,agargaon,agarhalom,agari,agari bujaq,agari khanzad khel,agari-e-kami,agari-ie,agaria,agarian,agarie,agariemae,agarieue,agarii,agarii-mae,agarii-mee,agarijeh,agarino,agarisuji,agariuebaru,agariuibaru,agarjeh,agarkandi,agarkenda,agarkhed,agarkov,agarkova,agarkovo,agarn,agaro,agarous,agarovici,agarovka,agarpara,agarpatta,agarpur,agarrado,agarrate,agarre,agarrei,agarroba,agarsa lafu,agarsafen,agarsal,agarshevo,agarsif,agarsin,agarsin xiang,agarta,agartala,agarti,agartij kala,agartij kola,agartu,agaru,agarubo,agarut,agarutu somon,agarutu-sumu,agaruut,agaruuta suma,agaruutu,agarvado,agarx,agaryshevo,agaryshevskiy,agaryshi,agarzinskiy,agarzya,agas,agasa,agasaf,agasakhib-kalay,agasali,agasasan,agasbeta,agasegyhaza,agasegyhazi tanyak,agasenligi,agaserdo,agash-aryk,agasha,agashbatoy,agasheva,agashi,agashirinoba,agashkina,agashkino,agashoryn,agashty,agashty-kul,agashtykol,agasi,agasi-bek-obasi,agasibeyli,agasilla,agasirinoba,agaskyr,agasnal,agasod,agasode,agasousigon,agassa godomey,agassac,agassiz,agastiampalli,agastin,agastiswaram,agasto,agastyampalli,agasvar,agasy-begly,agasyevo,agat,agat,agata,agatagbo,agatan,agatanyigbe,agatayingbe,agatayn hiid,agatchli,agate,agate bay,agate beach,agate point,agatebi,agatha,agathaberg,agatharied,agathazell,agathe,agathenburg,agathi,agatho,agathon,agathopolis,agathos,agati,agatici,agatir,agatiri,agatkau,agato,agatogba,agatogbo,agaton khid,agatovka,agatovo,agatowka,agatowo,agatoyn khid,agatrai,agatral,agats,agatuya,agatwi,agatyn-khid,agatyyn khid,agatz,agaua,agaundh,agaus,agauwa,agave,agave apedome,agavedji,agavedzi,agaver,agaverdioba,agavir,agavlar,agavnadzor,agavnatun,agaw awusa,agaw shasha,agaw-are,agawa,agawam,agawang,agawe,agawhaw,agawhome,agawkpo,agawlar,agawome,agawtakpo,agawtaw,agawu,agax obo,agaxi,agay,agay-ay,agay-ayan,agaya,agayakan,agayan,agayani,agayata,agayayon,agayazi,agayazibuduq,agayazybudug,agaydar,agaydarovskiy,agaye,agayeri,agayli,agaym,agayman,agayra,agayri,agayry,agaza,agazza,agazzano,aga\302\261lbasa\302\261,aga\302\261llar,aga\302\261rdag,agba,agba abode,agba akin,agba akoli,agba kekere,agba ogun,agba-abobora,agba-bayassou,agba-mbayassou,agba-o,agbaa,agbaba,agbabadiang,agbabalo,agbabe,agbabiaka,agbabo,agbabor-isu,agbabu,agbachi,agbachichama,agbada,agbade,agbadi,agbadiang,agbadikofe,agbadikope,agbadikorpe,agbading,agbado,agbadome,agbadomi,agbadoume,agbadu,agbaduma,agbadumo,agbaflome,agbafrou,agbagacay,agbagbojo,agbagho,agbagi,agbagnizon,agbago,agbagome,agbagon,agbagorme,agbagouanda,agbahia,agbahoan,agbaille,agbaja,agbaja-isu,agbajo,agbaju,agbaki,agbakiri,agbakofe,agbakorpe,agbakou,agbakpekofe,agbakro,agbaku,agbakwa,agbala,agbalagba,agbalagon,agbalaho,agbalaji,agbalama,agbalamabugokiri,agbalame,agbalanga,agbalau,agbalazou,agbale,agbale pedo,agbalesun,agbalide,agbaliga,agbaliji,agbalili,agbalo,agbalogo,agbalou,agbalu,agbaluko,agbalum,agbaluto,agbamassoumou,agbamaya,agbamegon,agbamu,agban,agbana,agbanaken,agbanakin,agbanate,agbanati,agbanawag,agbanawan,agbanban,agbanboulou,agbanda,agbandabe,agbandaode,agbandari,agbande,agbandi,agbandji,agbandjo,agbanga,agbangana,agbangara,agbangnizoun,agbangudu,agbanho,agbani,agbani luekon,agbanianssou,agbaniassou,agbanikaka,agbanikofe,agbanjalade,agbanlamigo,agbannaoag,agbannawag,agbannaway,agbanoko,agbanon,agbanou,agbanson,agbanta,agbantokope,agbanug,agbanwan,agbanyim,agbanyokofe,agbanyokorpe,agbanzin,agbanzinkpota,agbao,agbao-ud,agbaou,agbar,agbar-ndele,agbara,agbara igbo,agbara oza,agbara-ibo,agbaramu camp,agbare,agbarha,agbarha-otor,agbarho,agbarkhuk,agbas,agbasa,agbash,agbashlar,agbasi,agbaslar,agbassa,agbasson,agbata,agbatave,agbate kope,agbati kope,agbati obinu kekere,agbatiave,agbatiti,agbatitoe,agbato,agbaton,agbatong,agbatope,agbatou,agbatsivi,agbatso,agbatuan,agbau,agbauo,agbave,agbavito,agbawa,agbawnlo,agbawo,agbayagbene,agbayanoua,agbaye,agbayev pervyy,agbayev vtoroy,agbbalou,agbe,agbe ola,agbe oyinrin,agbeave,agbechemekofe,agbedde,agbede,agbedegun,agbedepeme,agbedevi kope,agbedi,agbedinou,agbedje,agbedje date,agbedje ossohou,agbedjikpo,agbedjipe,agbedo,agbedomodji,agbedrafo,agbedrafor,agbedu,agbeede,agbefelebi,agbegakope,agbegba,agbege,agbegi,agbegun,agbehe,agbeja,agbejedo,agbeji,agbeke,agbeko korpe,agbeku,agbel,agbelagba,agbele,agbelebu,agbelechime,agbelekaka,agbelekale,agbeleou,agbelepo,agbeli,agbelouve,agbemagbene,agbemda,agbemenya,agben,agbenawsikofe,agbenda,agbende,agbene,agbenga,agbengnisoe,agbeniga,agbenigbale,agbeninou,agbenohoe,agbenouvon,agbensi,agbenuvokofe,agbenya,agbenyaga,agber,agbere,agberere,agberi,agberire,agbesi,agbesia,agbesinra,agbesitonou,agbessi,agbessi abokon,agbessia,agbet ech chair,agbeta,agbetchafan,agbetiame,agbetiko,agbetim,agbetime,agbetipo,agbetipo villages,agbetogagon,agbetokope,agbetyafan,agbeve,agbeyangi,agbeye,agbezoudo,agbezourhlan,agbi,agbia,agbide,agbidime,agbil,agbile,agbimenyihiakope,agbinda,agbirigidi,agbis,agbiz,agbladje,agbladome,agblagato,agblakope,agblangandan,agblankantan,agbledo,agbledomi,agblega,agblesia,agbletokoe,agbletokwei,agbleve,agbleviem,agblife,agblome,agblome-levi,agblome-zokli,agblonu,agblopa,agbo,agbo akounou,agbo mbeyan,agboba,agbobiri,agbobo,agbobolo,agbobome,agboboto,agbobu,agbodan,agbodinou,agbodjalo,agbodjekpo,agbodjekpoui,agbodjekpwe,agbodji,agbodjrovi,agbodo,agbodoma,agbodrafo,agbodrafo (porto-seguro),agbodranfo,agboete,agbofakope,agbofon,agbogangan,agbogba,agbogbla,agbogbo,agbogugu,agbohosi,agbohun,agboikyor,agboju,agboke,agboko,agbokodo,agbokofe,agbokope,agbokoro,agbokorpe,agbokpi,agboku,agbole,agbolikpota,agbolinkpota,agbolokope,agbolu,agbolu isale,agbolugbene,agbomadjikofe,agbomadzi korpe,agbome,agbomojo,agbon,agbona,agbonagban,agbonagle,agbonan,agbonchia,agbonda,agbondji,agbondjo,agbonehia,agbongun,agbonle,agbonmoba,agbono,agbono kekere,agbonon,agbonorkorpe,agbonou,agbonrin,agbontu,agbonwakop,agbonwakope,agbonyedo,agbonyira,agboogba,agboola,agbor,agbor-boiiboji,agbor-nta,agbora,agborama,agboramo,agboran,agborhoro,agbori odo,agborigi,agborin,agboriodo idofoyi,agboro,agboro oyo,agborogbenakuku,agborogbenekuku,agborro,agbose,agbosi,agbosime,agbosokope,agbosome,agbosso,agbossou,agbosudotoiyi,agbosusu,agbota,agbotagon,agbotai,agbotame,agbotavou,agbouto kope,agbove,agboville,agbowa,agbowhiame,agboy,agboy norte,agboy sur,agboye,agboye loko,agboyi,agboyi eleja,agboyi tewure,agbozome,agbozume,agbrou,agbua,agbuakan,agbuaya,agbuayan,agbudia,agbudu,agbugbu,agbukegbene,agbul,agbulacan,agbulag,agbulagi,agbulak,agbulakh,agbulalakh,agbulaq,agbularh,agbulu,agbulugo,agbum,agbun-od,agbung,agbunud,agbura,agbure,agburu uke,agburu-uke,agbutiakope,agbutokope,agbwekope,agcaagil,agcaalan,agcaalan koyu,agcaalanturk,agcaali,agcabadi,agcabuk,agcac,agcacay,agcad,agcadarao,agcagawan,agcagay,agcaguney,agcahan,agcahilao,agcahisar,agcakale,agcakale koyu,agcakand,agcakas,agcakaya,agcakaya koyu,agcakeci,agcakecili,agcakent,agcakese,agcakise,agcakisla,agcakislakoyu,agcakoca,agcakoyun,agcakoyunlu,agcakoyunu,agcalar,agcali,agcamagara,agcamagara koyu,agcamahmut,agcamescit,agcanilan,agcanni,agcano,agcaqovaq,agcarope,agcasar,agcasehir,agcasor,agcasu,agcasu koyu,agcatepe,agcauayan,agcaveran,agcaviran,agcawayan,agcawaypa,agcawilan,agcay,agcayazi,agcekent,agcekoca,agchay,agche,agchen kandi,agchi,agchitm,agchu,agcik,agcil,agcin,agcococ,agcogon,agcsernyo,agcurun,agcuyauan,agcuyawan,agda,agdaban,agdabon,agdaci,agdacilar,agdad,agdag,agdagnan,agdahon,agdajon,agdal,agdal oumerzgoum,agdal oumerzgoun,agdaleran,agdalipe,agdalipi,agdaliran,agdalusan,agdam,agdam qalal,agdamasovo,agdamkand,agdamkelal,agdamkelyal,agdamkend,agdamkent,agdan,agdangan,agdao,agdar,agdara,agdary,agdas,agdasch,agdash,agdasor,agdat,agday,agdayao,agde,agdeppa,agdere,agdere koyu,agdestein,agdez ait fenzer,agdgome,agdgomelaantkari,agdhinai,agdhines,agdi,agdia,agdikan,agdim,agdim nyazza,agdirhasan,agdiz,agdluitsoq,agdluitsup pa,agdo,agdoub,agdrafokope,agdrup kaer,agdugayan,agdum,agdunut,agdz,agdzha-kala,agdzhabedi,agdzhabedy,agdzhakala,agdzhakend,agdzhakent,agdzhakishlag,agdzhakovag,agdzhakovakh,agdzharkh,agdzhayazy,agdzhiyazy,agdzhurun,agdzhuvur,age,age mowo,ageakoy,ageali,agearupe,agebane,agebeyli,agebo,agebu,ageby,aged,ageda,agedabia,agedal,agedino,agedrafokope,agedrup,agee,ageecay,agege,agegi,agegyi,agei,ageida,ageig,ageila,ageimi,ageito,agej,agejgal,agek,agekhush,ageki,agekiana,agekpo,agel,agelaga,agelaiciai,agelaiciu,agelak,agelan kidul,agelan lor,agelat,agelba,ageldino,agelete,agelfla,ageli-kaka,ageli-khan,ageli-mulla,ageli-torta,agell,agelli,agello,agelo,agelopo,agelsberg,agelsta,agelu,agelu jerusalem,agelz,agema,agematsu,agemeda,agemgo,agemier,agemjay,agemler,agemopa,agemsa,agemti,agen,agen- daveyron,agen-ako,agena,agenahambo,agenahombo,agenanambo,agenas,agenbach,agencourt,agency,agenda,agendoh,agendorf,agenebode,agenerxiuma,ageng,agenga,ageni,ageniskis,agenkalne inlet,agenmatch,ageno,agenor,agenor alves,agenosho,agenoshomachi,agenoura,agensane,agenskalna,agenskalns,agenville,agenvillers,agenzia valpelina,ageo,ageomura,agepano,agepanu,ageptsevo,ager,ager gema,ager-ager,agera,ageraba,agerba,agerbaek,agerd,agerd ougoui,agerd ousgine,agerdo,agerdomajor,agere,agere hiywet,agere mariam,agere maryam,agere selam,agere sisay,agereshki,agerfeld,ageri,agerige,agerkrog,agermach,agernaes,agernor,agero,agerod,agerola,ageror,agerri,agersbaek,agersberg,agersborg,agersgol,agersif,agerskov,agerskov hede,agersnap,agerso,agerso by,agersryd,agersta,agersted,agersund,agerum,agerup,agery,ageryal,agerze,ages,agesi,agesin,agessis,agesta,agestrup,ageta,ageteklechi,agethorst,agets-saint-brice,agettes,agetu,ageva,ageville,agew,agew gimja bet,agewiye,agexus,agey,ageyev,ageyeva,ageyevka,ageyevo,ageyevshchina,ageyevtsy,agezba,agfalva,agg,aggadik,aggai,aggalacengnge,aggalaulpota,aggan faqir jo goth,aggao,aggar,aggarahere,aggare,aggarfitane,aggarna,aggaro,aggarp,aggarset nepte,aggasi,aggasian,aggassian,aggato,aggatorp,aggay,aggdal,agge,aggebo,aggeies,aggelby,aggen,aggeneis,aggeneys,agger,aggerdeich,aggerdo pusta,aggerhutte,aggersborg,aggersborg torup,aggersund,aggerud,aggestrup,aggfors,aggherar,agghi,agghini,aggi,aggies acres,aggio,aggio cata,aggius,aggnas,aggo munda,aggosund,aggosundet,aggouarn,aggoug,aggregates,aggsbach,aggsbach dorf,aggsbach markt,aggsjon,aggstein,aggstein-dorf,aggszentpeter,aggtelek,aggub,aggugadaan,aggugadan,aggugaddah,aggugaddan,agguirit,agguliaro,aggumitan,aggunatan,aggunetan,aggungan,aggur,aggustowka,agh,agh barzeh,agh bash,agh bolagh,agh bolagh gaduk-e pain,agh bolagh pain,agh bolagh-e `olya,agh bolagh-e bala,agh bolagh-e morshed,agh bolagh-e mostafa khan,agh bolagh-e pain,agh bolagh-e sofla,agh bolaq,agh chay,agh chay bala,agh chay pain,agh chay vasat,agh cheshmeh,agh dagh,agh daraq,agh daraq-e jadid,agh daraq-e qadim,agh darreh,agh darreh-ye `olya,agh darreh-ye sofla,agh darreh-ye vosta,agh dash,agh davanlu,agh esmail,agh esma`il,agh ghayeh,agh gol,agh gol-e kuchak,agh gonbad,agh jam`,agh kand,agh kand mehraban,agh kand sardrood,agh kand-e `olya,agh kandi,agh mazar,agh mazar-e kord,agh mazar-e malek kandi,agh miyun,agh oabaq-e akhar,agh otaq,agh qal`eh,agh qeshlaq,agh qui,agh tapeh hajebloo,agh tavaraq,agh tavileh,agh topraq,agh towpraq,agh ziarat,agha,agha `abul,agha ahmad,agha baba,agha badruddin,agha badruddin ji wand,agha burut,agha gul,agha gul kelay,agha husain,agha jan,agha jari,agha kand,agha karez,agha kariz,agha khan,agha khelo kelay,agha malek,agha miri,agha mohammad,agha mohammad khan,agha mohammad nurkhan,agha mohammed bai,agha mohd bai,agha mohd-nurkhan,agha muhamamd,agha muhammad,agha muhammad bay,agha muhammad kala,agha muhammad kelay,agha muhammad kili,agha muhammad nur khan,agha sahib kelay,agha saidal,agha saiyid,agha seyyed,aghaballou,aghabalou,aghabekalanj,aghabekalendg,aghabekalenj,aghabekalinj,aghaboe,aghabog,aghabulloge,aghabullogue,aghacunna,aghacurreen,aghada,aghade bridge,aghadoon,aghadovey,aghadowey,aghadreen,aghadreenagh,aghadruminsh,aghafine,aghagallon,aghaglinny north,aghagol kalay,aghagower,aghagrania,aghagul kalay,aghaiani,aghaj kandi,aghaj masbad,aghaj oghlu,aghaj owghlu,aghaj uqli,aghajaglil,aghajah mashad,aghajam,aghajan,aghajang,aghajapra,aghajari,aghajeri,aghakhelo kalay,aghakheyl kalay,aghakilmore,aghal,aghal-e shaykh,aghala,aghalackan,aghalai,aghalane,aghalay,aghalbak,aghale khwaja,aghale saykhu,aghaleague,aghalee,aghalenty,aghali,aghalokpe,aghalokpe waterside,aghalough,agham,aghamackalinn,aghamedli,aghameghu,aghamirlu,aghamore,aghan,aghancon,aghani,aghanich,aghanj,aghanloo,aghanpur,aghanure,aghanus,aghanvilla,aghanza,aghao,aghapalli,aghapur,aghar,aghara oza,agharalu,agharbari,agharkuiyeh,agharroo,agharsin,aghash oba,aghasht,aghasi kandi,aghasin-e bala,aghasin-e pain,aghasur,aghatapur,aghaterry,aghatubrid,aghatubrid bridge,aghauchtim,aghavannagh,aghavannah,aghavas,aghaveagh,aghaville,aghaville cross roads,aghavnadzor,aghavnatun,aghavnavank,aghavni,aghavrin cross roads,aghaward,aghaylak,aghazheng,aghazzane,aghbah,aghbal,aghbala,aghbalagh lar,aghballou,aghbalo,aghbalou,aghbalou nkerdous,aghbalou nserdan,aghbalou nserdane,aghbalou-n-kerkouf,aghbaoulou,aghbar,aghbarg,aghbarg maghzai,aghbarg nishpa,aghbargai,aghbargai kach,aghbargai khullah,aghbargai kili,aghbargi,aghbarzeh,aghbash,aghbat al fawqa,aghbat at tahta,aghbazh,aghbazhe,aghberk,aghbolagh,aghbolagh aghdagh,aghbolagh gavbazeh,aghbolagh valadiyan,aghbolagh-e bala,aghbolagh-e chamanlu,aghbolagh-e khaled,aghbolagh-e meydan,aghbolagh-e mokhur,aghbolagh-e pain,aghbolagh-e suqar,aghbolagh-e suqqar,aghboulagh,aghbulagh,aghcheh,aghcheh bo,aghcheh bodi,aghcheh bughaz,aghcheh darband,aghcheh dizej,aghcheh ghaleh,aghcheh ghaleh,aghcheh ghiyeh,aghcheh gonbad,aghcheh hesar,aghcheh kahal,aghcheh kand,aghcheh kandi,aghcheh kharabeh,aghcheh kohal-e rajabanlu,aghcheh kohal-e zamani,aghcheh kohel,aghcheh kohol,aghcheh lucheh,aghcheh mashhad,aghcheh mashhad-e chahar dowli,aghcheh mashhad-e pasanlu,aghcheh mashhad-e pasatlu,aghcheh masjed,aghcheh mazar,aghcheh qal`eh,aghcheh qayah,aghcheh qeshlaq,aghcheh qeshlaq-e `olya,aghcheh qeshlaq-e bala,aghcheh qeshlaq-e pain,aghcheh qeshlaq-e sofla,aghcheh qeshlaqi,aghcheh qeyeh,aghcheh rish,aghcheh rud,aghcheh ziveh,aghcheh-ye seyyedan,aghchehdizaj,aghchehlu,aghchyne ahl ramel,aghda,aghdaban,aghdam,aghdamkend,aghdaraq,aghdarreh,aghdash,aghdash-e jadid,aghduff bridge,agheddou,aghedou,agheila,aghel deh,aghel kamar,aghel khvajeh,aghel sang,aghel-e kaka,aghel-e khan,aghel-e khvajeh,aghel-e khwajah,aghel-e mawla,aghel-e molla,aghel-e mowla,aghel-e mulla,aghel-e shaykho,aghel-e shaykhu,aghel-e sheykh,aghel-e tortah,aghel-e towrtah,aghela khan,aghelak,aghelane,agheldeh,aghele mola,aghele torta,agheliele,aghely,aghembicciu,agheng,agheraiai,aghere mariam,agheresalam,agherku,aghern,agherren,agherri,agheseh,aghesh,agheshli,agheshloo,agheshlu,aghesht,aghezzola,aghgoumi,aghguni,aghhulat,aghia eirene,aghicha,aghien,aghigha,aghighol,aghighul,aghil,aghil abad,aghil ou miel,aghil oumial,aghil-e khwajah,aghil-e shaykho,aghilak,aghin,aghinree bridge,aghione,aghir,aghir sai,aghir say,aghirda,aghires,aghiresu,aghiresu-fabricei,aghiresul,aghiris,aghissa,aghitta,aghitu,aghjabadi,aghjadam,aghjakend,aghjanag,aghjavan,aghjayazy,aghjeh,aghjeh bowghaz,aghjeh bughaz,aghjeh dizaj,aghjeh dizeh,aghjeh gheshlagh jadid,aghjeh gheshlaq ghadim,aghjeh gonbad,aghjeh kand,aghjeh kandi,aghjeh kharabeh-ye seyyedha,aghjeh kohal,aghjeh kohal zaman,aghjeh kohel,aghjeh mashhad,aghjeh mazar,aghjeh owbeh,aghjeh qal`eh,aghjeh qayeh,aghjeh qeshlaq pain,aghjeh qeshlaq-e bala,aghjeh qeshlaq-e pain,aghjeh qeshlaq-e sofla,aghjeh rish,aghjeh rish kuranlu,aghjeh rud,aghjeh ughlan,aghjivan,aghjovan,aghkend,aghkilisa,aghkand-e `olya,aghkand-e bala,aghkend,aghladine,aghlak,aghlal,aghlan,aghlane,aghlaqan,aghlattacru,aghlaverty,aghleam,aghlegh,aghlem bridge,aghlian,aghligh,aghmed,aghmeqan,aghmeyun,aghmid,aghmir,aghmiun,aghnafarcan,aghnagap,aghnagar bridge,aghnagollop,aghnahaha,aghnajane,aghnakily,aghnamarroge cross roads,aghnamullen,aghnanus bridge,aghnaskeagh,aghnjadzor,aghobahi,aghojan,aghol beyk-e `olya,aghol beyk-e sofla,aghol dar,aghol kamar,aghol sukhteh,agholak,agholdar,agholgah,agholokpe,aghon,aghoney,aghonj,aghoo,aghooi,aghooyeh,aghooz loo,aghoozbon,aghor,aghora,aghori izdar,aghori ou falla,aghoro,aghoro i,aghoro ii,aghorsang,aghory,aghosbeh,aghoseh,aghotlaq,aghoud,aghoud el marach,aghoudid,aghoura,aghoussir,aghouzir,aghow,aghown,aghowr sang,aghowz bon,aghowzbon,aghozi gang,aghozongo,aghrabat,aghram,aghreena,aghreh,aghrem,aghren,aghren kadi,aghrewala,aghribs,aghrimez,aghriv al islamiyah,aghro rahu,aghrow kash,aghrow kesh,aghsaqal,aghsha,aghsu,aghtakla,aghtepe,aghtsk,aghudi,aghukuiyeh,aghun,aghunj,aghurabad,aghurmi,aghushkash,aghushkdal,aghutman,aghuyeh,aghuz,aghuz ban,aghuz chaleh,aghuz dar,aghuz darreh,aghuz galleh,aghuz kaleh,aghuz keleh,aghuz koti,aghuzband,aghuzbon,aghuzbon kandeh,aghuzbon kandeh sar,aghuzchal,aghuzdar,aghuzdar kolum,aghuzdarbon,aghuzdarreh,aghuzkati,aghuzki-ye bala,aghuzki-ye pain,aghuzlu,aghvani,aghvaran,aghveyeh,aghviyeh,aghvorik,aghwai,aghyar,aghyazi,aghyokhush,aghzaikas,aghzan,aghzar khel,aghzavar,aghze poya,aghzer khel,aghzheng,aghzi gang,aghziarat,aghzidisse,aghzo kalay,aghzo kelay,agi,agi ali,agi geul,agia,agia agathi,agia anastasia,agia anna,agia eirini,agia eleousa,agia ermioni,agia faneromeni,agia foteini,agia kyriaki,agia marina,agia marinouda,agia mavra,agia napa,agia paraskevi,agia pelagia,agia sotira,agia thekla,agia thekli,agia triada,agia trias,agia varvara,agia zoni,agiabampo,agiam,agiani,agiaon,agiasma,agiasmata,agiasos,agiba,agibalovo,agibar,agibasak,agibawa,agibel,agibela,agibino,agibu,agica brdo,agici,agici brdo,agici gornji,agida,agidel,agidere,agidi,agidienberg,agidigbene,agidimi,agidingbi,agidzh,agieina,agienielia,agienrella,agiez,agig,agigbe,agigbigi,agigea,agighiol,agigul,agij,agikdak,agikidaki,agikik,agikolazizu,agil,agil ismail darkhan,agil onu,agila,agilbasi,agilbogazi,agilca,agilcik,agilde,agildere,agildere mezraasi,agile,agili,agiliga,agilimka,agilimup,agilkaya,agilkoy,agilla,agillar,agille,agilli,agillikoyu,agillitekke,agilmay,agiloba,agilonu,agilozu,agilpazari,agilwala,agilyazi,agilyazisi,agilyazisi koyu,agilyolu,agim,agima,agimani,agimi,agimont,agin,agina,aginaa,aginac,aginakofe,aginaso,aginci,agincourt,aginda,aginde,agindo,agine-afanasyevskiy,aging,agingun,agini,aginimu,agino selo,aginski datsan,aginskiy,aginskiy datsan,aginskoe,aginskoye,aginsos,aginukofe,aginukope,agio vavatsinias,agiofyllo,agiofyllon,agioi,agioi anargyroi,agioi apostoloi,agioi deka,agioi douloi,agioi iliofotoi,agioi pantes,agioi theodori,agioi theodoroi,agioloma,agion gala,agion georgoudi,agioneri,agiopigi,agios achilleios,agios ahileos,agios aimilianos,agios akakios,agios amvrosios,agios andreas,agios andronikos,agios antonios,agios arsenios,agios athanasios,agios charalampos,agios christofos,agios dimitrianos,agios dimitrios,agios dometios,agios donatos,agios efstathios,agios epifanios,agios epiktitos,agios epiphanios,agios ermolaos,agios fokas,agios fotios,agios georgios,agios georgios glykorrizou,agios georgios sykousis,agios iiias,agios ilias,agios ioanis gigenon,agios ioanis ofis,agios ioannis,agios ioannis prodromos,agios isidoros,agios kirykos,agios konstantinos,agios kosmas,agios loukas,agios mamas,agios markos,agios matthaios,agios merkourios,agios minas,agios nikolaos,agios nikolo,agios panteleimon,agios panteleimonas,agios pavlos,agios petros,agios prokopios,agios rokkos,agios sergios,agios sozomenos,agios spyridon,agios stefanos,agios theodoros,agios therapon,agios thomas,agios tychon,agios vasileios,agios vissarion,agios vlasios,agiota,agiou georgiou,agiou nilolaou,agipaboni,agipawa,agipo,agir,agir-tamak,agir-tyube,agira,agirbiciu,agircia,agirda,agirdaki,agirdere,agirdzha,agire,agire akoko-gbene,agiri,agiri emu,agirigi,agirigirioi,agirigiroi,agiripalle,agirish,agirkaya,agirkoy,agirma,agirmese ciftligi,agirnaz,agirocastro,agiroglan,agirovo,agirta,agirtas,agirtma,agirzya,agis,agisan,agisbatoj,agish,agishbatoy,agisheva,agishevo,agishi,agishty,agispe,agisse,agisty,agiswail,agit,agita,agita huryee,agitana,agitata,agitiondro,agitriondre,agitta,agitu khure,agiual,agiura,agiuual,agiuval,agivo,agiwas,agizbogaz,agizbogazi,agizbogazikoy,agizbuyuk,agizhan,agizkara,agizlar,agizoren,agizorenguney,agizoux,agizsuyu,agizsuyu koy,agizsuyu koyu,agiztadi,agja kala,agja qal`eh,agja qishlaq,agjal,agjana,agjeh,agjeh kharabeh-ye pain,agjlar,agkalangan,agkand,agkathia,agkawayan,agkayamet,agkend,agkeynak,agkilisa,agkilo,agkilsa,agkleisides,agkoynak,agkuyu,agkyshlak,agla,aglabi,aglagal,aglagel,aglagla,aglaglal,aglago,aglagokope,aglagtang,aglaguel,aglahag,aglahog,aglahug,aglajarvi,aglalana,aglali,aglaly,aglamarhoe,aglamazovo,aglami,aglan,agland,aglangia,aglao,aglapsvik,aglapsvika,aglarca,aglasan,aglasberg,aglassane oulad said,aglassing,aglasterhausen,aglasun,aglavista,aglavitsa,aglayan,agle,aglegal,aglen,aglenosihoue,agli,agliana,agliano,aglibacao,aglibacoa,aglica,aglicabao,aglie,aglientu,aglig,agligbe,agligo,aglik,aglik dua,aglik satu,aglik selatan,aglik utara,aglikaji,aglikcay,aglikgunung,aglikoy,aglikpucuklarangan,aglimocan,aglimocon,aglinab,aglinob,aglipay,aglish,aglisides,aglobi,aglobo,aglokpovia,agloloma,agloloway,aglomasovo,aglomazovo,aglome,aglomedji,aglona,aglonas muiza,aglos,aglosong,aglot,aglou,agloudhista,agloui,aglu,aglubong,agluk,agluna,agluonai,agluonenai,agluoney,aglykh,agma-ilig,agmajhi,agmaligon,agmalobo,agmamedly,agmamine,agman,agmanaphao,agmane,agmanic,agmatgurap,agmazut,agme,agmezraa,agmour,agmu,agmus,agmusa,agna,agnac,agnadello,agnag,agnaga,agnagari,agnak,agnak grand,agnak petit,agnalobo,agname,agnami,agnana,agnana calabra,agnanda,agnanderi,agnanderon,agnandi,agnandia,agnandion,agnane,agnangnan,agnano,agnanta,agnantero,agnanteron,agnantia,agnantio,agnaoui,agnarelu,agnari,agnaryd,agnas,agnat,agnata,agnati,agnave,agnavou,agnawe,agnay,agnaya,agnayon,agndzhadzor,agneaux,agneb,agneby,agnefest,agner,agnere koffikro,agnerekoflikro,agnes,agnes banks,agnes corners,agnes khiar,agnes rest,agnes water,agnesakna,agnesberg,agnesdorf,agnesenhof,agnesevskiy,agnesforras,agneshof,agnesmajor,agness,agness akro,agnesund,agnetar,agnetaryd,agnetir,agnetz,agnevo,agnew,agnewville,agnez,agnez-les-duisans,agni,agni assikasso,agni bamessaoud,agni en talata,agni idouillouir,agni nfad,agni ouzgar,agnia assikasso,agniamouada,agniamuada,agnianic,agnianou,agniassue,agnibandjin,agnibe,agnibilekrou,agnibonou,agnichnoye,agnicourt,agnicourt-et-sechelles,agnie,agnielles,agnieres,agnieres-en-devoluy,agnieszezyn,agnieszkow,agnieszkowo,agnigba memei,agnihotri,agnik,agnikoe,agnilisa,agnimekoua,agnin,agnino,agnioke,agnipa,agnisback,agnishchevo,agnissankoua,agnita,agnitsasi,agnitsini,agniye-afanasyevsk,agniye-afanasyevskiy,agno,agnocnoc,agnoknok,agnolo,agnon,agnone,agnonoc,agnoron,agnos,agnougbate,agnoulako,agnoux,agnova,agnrokope,agnron,agnronkope,agny,ago,ago akinnugba,ago akuruaje,ago alabi,ago alawode,ago alejo,ago amadu,ago are,ago atilabo,ago bamuyegun,ago bello,ago bonema,ago cyprian,ago duro,ago egun,ago egure,ago erierie,ago forojo,ago fulani,ago george,ago goja,ago hamn,ago hausa,ago isaac,ago isaiah,ago jimmy,ago lawrence,ago manuge,ago merodeh,ago munda,ago najo,ago ogba,ago ojoye,ago ologunja,ago oloja,ago olojoye,ago olosun,ago osakwe,ago oyinbo,ago reuben,ago ruwase,ago saiyemiyan,ago sasa,ago ugbene,ago woli,ago-abo,ago-adewale,ago-adoko,ago-agbede,ago-ajapa,ago-akinde,ago-akingboye,ago-akinwande,ago-aladura,ago-alagbara,ago-amodu,ago-apeja,ago-araromi,ago-bale,ago-bello,ago-binuyo,ago-bolorunduro,ago-dada,ago-doctor,ago-gabriel,ago-gabriel ayo,ago-gege,ago-goke,ago-iba,ago-ibo,ago-igbira,ago-igbirra,ago-ijaiye,ago-ijebu,ago-ijo,ago-ilaje,ago-iredia,ago-ireti,ago-isame,ago-isobo,ago-ita,ago-iwoye,ago-jabo,ago-joseph,ago-julia,ago-korolo,ago-lagbawo,ago-lulu,ago-nathaniel,ago-nirin,ago-odode,ago-odukuye-isobo,ago-oka,ago-okeluse,ago-olaleye,ago-olatubara,ago-ololajulo,ago-olowo,ago-oloyi,ago-olugede,ago-onigbagbo,ago-onireke,ago-orokunja,ago-osogbon,ago-owa,ago-owo,ago-owu,ago-panu,ago-samuel ojilo,ago-sangotus,ago-sasere,ago-sokite,ago-stephen,ago-teju,ago-titun,ago-yesufu alli,agoa,agoada,agoba,agobaba,agobada,agobara,agobi-iwolu,agobiakofe,agoblan,agobledokui,agobo,agobob,agocancha,agocasa,agochi,agochido,agochinchan,agochoe,agocpuszta,agoctanya,agoda,agoda koira,agode,agodeka,agodeke,agodi,agodile,agodim,agodjololo,agodnia,agodo,agodoe,agodogoue,agodokope,agodokpe,agodome,agodomi,agodorp,agodoukope,agodun,agoe,agoeng,agofissi,agogbe,agogboro,agognate,agogo,agogoro,agogoso,agogoudjou,agogpame,agoho,agohoe,agohome,agohonhou,agohoue-balime,agohoume,agoi efut,agoi ekpo,agoi ibami,agoi ibani,agoi patakdulo,agoi szollo,agoi-ikpo,agoilori,agoiretdulo,agoishan,agojeju,agoji,agojido,agojo,agok,agokadol,agokan,agokdong,agoko,agokope,agokoue,agokpame,agokpe,agokplame,agokpo,agokpo 2,agokpo saviepe,agokpwe,agokpwi,agokri,agokro,agoktong,agokutu,agol,agola,agolai,agolechenki,agoli,agolia,agoligbene,agolinskoye,agoll,agolli,agolna,agolo bende,agolomo,agoluabi,agoluk,agom,agomai,agomanay,agomanya,agombe,agome,agome glozou,agome kotoukpa,agome seva,agome-kotukpa,agome-palime,agomeda,agomeko,agomekpota,agomo,agomonigbe,agomonofi,agomu,agon,agon-coutainville,agona,agona akrofoso,agona asante,agona ashanti,agona swedru,agonac,agonassou,agoncida,agoncillo,agonda,agondavi,agondo,agondogui,agondopoue,agonenzork,agones,agong,agong-ong,agong-ongan,agonga,agonges,agongni,agongo de coreto,agongo de eguba,agongo-eguba,agongong,agonhohoun,agoni,agonia,agonimiaba,agoniokope,agonitz,agonkame,agonlin,agonme,agonnay,agonndi,agonpala,agonrin,agontome,agonu,agonvi,agonvy,agonyo kope,agonyolako,agonyu ulani,agonyu-ulyani,agonzork,agoo,agoom,agopampa,agopo,agoponga,agoponou,agopop,agopuigbo,agor,agor dit,agor ti,agora,agoramundi,agoran,agorbar,agorbledokui,agord nouadouz,agordaban,agordat,agordela,agordo,agordom,agordome,agoreachic,agoreadji,agoreke,agorelitsa,agoren,agorhome,agori,agori khas,agoriachic,agoriani,agorika,agoriki,agoritschach,agorke,agorkpo,agorkpoenu,agorma,agormanya,agorme number 1,agoro,agoro ibadan,agorogbene,agorogwor,agorome,agoromyesum,agorou,agorreta,agorsang,agortakpo,agortive,agorto,agortoe,agoru,agoruze gbene,agorve,agorvega,agorveme,agorvi,agorvinu,agorvlaya korpe,agoryiani,agos,agos-agos,agos-vidalos,agosa,agosagos,agoshapeshta,agoshevka,agoshfi,agoshg,agoshk,agoshusha pampa,agosipan,agosito,agosk-e mohammadabad,agoso,agossa,agossakoe,agossito,agossouhoui,agost,agosta,agostaderito,agostadero,agostadero de abajo,agostadero de aguirre,agostadero de charcas,agostadero y anexas,agostaderos,agostas,agostem,agostin,agostinho,agostinho baltazar,agostinho neto,agostinho porto,agostinho rodrigues,agostini,agostitlan,agostmajor,agosto,agostonhalom,agostonmajor,agostonpuszta,agostontanya,agostos,agostyan,agot,agota,agota majer,agotaga,agotami,agotamu,agotare,agotavia,agotayan,agotazug,agotcy,agote,agotepe,agotevi,agotime,agotime kpodjaho,agotive,agotnes,agoto,agotoe,agotome,agotope,agotorme,agotove,agou,agou gadzepe,agou iboe,agou tomegbe,agou-gare,agou-nyongbo,agoua,agoua-kongoukro,agouadira,agouagon,agouagou,agouahin,agouam yo,agouanit,agouanitt,agouanoua,agouanta,agouaoua,agouare,agouarn,agouawa,agouaye,agouberi,agouchtim,agouchtim el fougani,agouchtine,agoudal,agoudal n ait brahim,agoudal oumerzgoum,agoudas,agouddim,agouddini,agoudeve,agoudi,agoudi nait ammar,agoudi nait amer,agoudiabadja,agoudid,agoudim,agoudim nikhartane,agoudine,agoudja,agoudja badja,agoudou manga,agoudoufoga,agoudoukpe,agoudouvou,agoue,agoue kope,agouebou,agouede,agouegan,agouekro,agouelmane,agouemit,agouenit,agoueve,agougan,agougoa,agougou,agouguir,agouidir,agouil,agouila,agouilal,agouilane,agouim,agouines,agouins,agouir idi,agouis,agoujbal,agoujgal,agoukame,agoukpoeta,agoulinitsa,agouliz,agouliza,agoulliz,agoulmim,agoulmime,agoulmine,agoulou,agoult,agoult el kharradj,agoulzi,agoulzi nourhzif,agouma,agoumad,agoumad nassermo,agoumadane,agoumana,agoumar,agoume,agoumi,agoummat,agoumo,agoun,agouna,agouna deve,agouna-gaga,agounai,agounde,agoundi,agoundim nikherdane,agoune,agouni,agouni adanne,agouni ait ammar,agouni arech,agouni areuch,agouni ba messaoud,agouni et tahtani,agouni ihrichen,agouni izgarine,agouni izim,agouni izimmer,agouni melloul,agouni n fat,agouni n id ou illoun,agouni n ou msmessa,agouni n ouaggag,agouni n ouaggoug,agouni n ouderdour,agouni n tleta,agouni ou ram,agouni ougouram,agouni ouzemoun,agouni-ntaslennt,agouni-n-tsalent,agounogon,agouns n iyssil,agounsa,agounsane,agounsi,agour n izig,agoura,agoura hills,agourai,agourai n ait tamjoute,agourbi,agourbo,agourd,agourd ou mzoul,agourde,agourdouar,agourelitsa,agouremt,agourgouma,agourgour,agouri-izemeur,agourme,agourmi,agourout,agourram,agoursa,agourtoulou,agousotodji,agoussim,agoussounou,agoute,agouti,agouti amazdar,agouti el foukani,agouti et tahtani,agoutome,agoutou,agov,agova,agove,agoveme,agovi,agovica,agovinu,agovlayakope,agoy,agoya,agoyan,agoye,agoyema,agoyia,agoyo,agoyola,agoyota,agoz,agpa,agpahan,agpaido,agpamiut,agpanabat,agpange,agpangi,agpano-an,agpara,agparale mountain,agparssuit,agpat,agpay,agpesse,agpilyakyan,agpinar,agpipile,agpipili,agpokro,agponge,agpro,agpudlos,agqiraqli,agquilo,agra,agra bala,agra belanganj,agra de boucas,agra khu,agra kuz,agra maior,agra miana,agra taj colony,agrai,agrab,agrabansou,agrabat,agraciada,agracoes,agrade,agrado,agrafa,agrafenina pustyn,agrafenino,agrafenka,agrafenki,agrafenovka,agrafenovskoye,agrafenowka,agrafinowka,agrafoi,agraglin bridge,agragos,agrahar,agraharam,agrail,agrakos,agrakunda,agrakundu,agral,agraladu,agralioun,agram,agram kadi,agramakovka,agramarou,agrambela,agramdeh,agramidjodji,agramon,agramonte,agramukti kulon,agramukti wetan,agramunt,agrane,agrano,agranovich,agrant,agranzon,agrap,agrapatana,agrapatna,agrapha,agrapidhia,agrapidhokambos,agrapidhokhori,agrapidhokhorion,agrapidhoula,agrapidoula,agraredj,agraregyetem,agraria,agrariya,agrarnoye,agras,agrat,agrate,agrate brianza,agratur,agrave,agrawa,agra\302\261daga\302\261,agrd,agreate,agreb,agreda,agregados siberia,agreh,agrela,agrelia,agrella,agrelo,agrelopon,agrelopos,agrelos,agrenarna,agrep,agres,agrestan,agreste,agrestina,agreyate,agri,agri bijar,agri bujaq,agri kalla,agri khanzad khel,agria,agria grande,agria pequena,agriakambos,agriakona,agriana,agriani,agriano,agrianoi,agriao,agriasdulces,agribijar,agriboa,agricola,agricola oriental,agricola oriental norte,agricola oriental sur,agricola village,agricultures del norte,agridaki,agridhaiika,agridhaki,agridhakion,agridheika,agridhi,agridhia,agridhion,agridi,agridia,agridion,agridya,agries,agries-lunca,agriesel,agriex,agrif,agrifoglio,agrigbe,agrigbon-oke,agrigento,agrigon,agrij,agrik,agrilaou,agrilaoun,agrilea,agriles,agrili,agrilia,agrilias,agrilidhi,agrilidhion,agrilios,agrilium,agrillokhori,agrilos,agrilovounon,agrimafe,agrime,agrimonte,agrinco,agrine,agrinio,agrinion,agrinka,agrio,agrioes,agriolevka,agriolevki,agriosikai,agriosikea,agriosikia,agriosikies,agriosykea,agriovotanon,agripa,agripakope,agripara,agripishki,agrippa jere,agriri,agris,agrisaba,agristeu,agristeul,agrisu de jos,agrisu de sus,agrisu mare,agrisul de jos,agrisul de sus,agrisul mic,agrit,agrita,agritya,agritye,agriz,agro,agro de baixo,agro florida,agro fuori,agro peno,agro velho,agro-pecuaria palmeirense,agro-proletarskaya,agro-proletarskiy,agro-pustyn,agrobaza,agrobom,agrochao,agrocipya,agrodosio,agrofenino,agrogorod,agroha,agroilia,agrokipia,agrokombinat ilichevka,agrokultura,agroladhou,agroladou,agroladu,agroles,agromar,agron,agrong,agronom,agronomia,agronomicheskiy,agronomichne,agronomichnoye,agronomos mexicanos,agronomov,agronomovca,agronomovka,agronomskiy,agronomul,agrons,agropastoril papal do ceu,agropoli,agros,agrosa,agrosikea,agrosikeai,agrosikia,agrosum,agrosvet,agrosykea,agrosykia,agrotikai filakai stavronikita,agrotur,agroud el marach,agrougbe,agrougnane,agrougoum,agroulane,agroun,agroun moblar,agrovila,agroyaco,agrra,agru,agrual,agrukhino,agrumara,agrumito,agrun moblar,agrupacion,agry,agrys,agrysh,agryz,agryzkovo,agryzya,ags bruk,agsaagac,agsak,agsaklar,agsakli,agsaklikoy,agsalanan,agsalang,agsalay,agsalin,agsam,agsaman,agsanayan,agsao,agsarane,agsaropot,agsbach,agsdorf,agserane,agshin,agsiazan,agsidan,agsilab,agsimao,agsime,agsinapot,agsing,agsirab,agsiyazan,agskardet,agskaret,agsoso,agsowao,agstafa,agstall,agstein,agsu,agsungot,agsur,agsuwao,agta,agtagla,agtaglya,agtala,agtalos,agtambi,agtambo,agtangao,agtanguay,agtanya,agtapa,agtas,agtas koyu,agtatacay,agtatacay norte,agtatacay sur,agtel,agtepe,agterhorn,agtijkola,agtil,agtinga,agtipal,agtipod,agtiwa,agto,agtoc,agtongo,agtopgop,agtououn,agtououne,agtrup,agtrupskov,agtugop,agtuman,agu,agu akpu,agu-eze,agua,agua agria,agua alegre,agua alta,agua alta vale,agua amarela,agua amarela de baixo,agua amarela de cima,agua amarela do meio,agua amarga,agua amarga chica,agua amargosa,agua amarilla,agua amarillas,agua amarillo,agua amarla do meio,agua armaga,agua azul,agua azul de baixo,agua azul de cima,agua azul de meio,agua azul rancho,agua azul sierra,agua azules,agua azulzinha,agua bela,agua bendita,agua bianca,agua blanca,agua blanca de la quebrada,agua blanca del sur,agua blanca hacienda,agua blanca iturbide,agua blanca la palma,agua blanca norte,agua blanca quebrada,agua blanca sur,agua blanquita,agua boa,agua bonita,agua branca,agua branca de baixo,agua branca de cima,agua bronca,agua buena,agua buena abajo,agua buena arriba,agua bueyes,agua caliente,agua caliente chiquita,agua caliente de antelo,agua caliente de candolfi,agua caliente de castelum,agua caliente de chinipas,agua caliente de cota,agua caliente de fabela,agua caliente de leon,agua caliente de linaca,agua caliente de los monzones,agua caliente de nepala,agua caliente de pena,agua caliente de ramirez,agua caliente de ramos,agua caliente grande,agua caliente segundo,agua calientes,agua calientica,agua calientilla,agua canoa,agua castilla,agua catitlan,agua cercada,agua chica,agua chile,agua chiquita,agua chuca,agua chula,agua clara,agua clara de baixo,agua clarita,agua colorada,agua comales,agua comprida,agua coyote,agua d alte,agua dalto,agua da figueira,agua da fortuna,agua da mula,agua da pedra,agua da peroba,agua da rega,agua da romana,agua da rosa,agua das casas,agua das fortes,agua das palmeiras,agua de afuera,agua de agriero,agua de aguero,agua de alte,agua de alto,agua de angel,agua de arriba,agua de bueyes,agua de camotes,agua de canale,agua de castilla,agua de castro,agua de chanc,agua de correa,agua de dios,agua de doro,agua de enmedio,agua de fontanales,agua de gamotes,agua de gomates,agua de hierro,agua de higuera,agua de la estancia,agua de la lumbre,agua de la luna,agua de la mina,agua de la mula,agua de la palma,agua de la pena,agua de la piedra,agua de la rosa,agua de la virgen,agua de las flores,agua de las minas,agua de las palomas,agua de las piedras,agua de lazo,agua de leon,agua de limon,agua de los caballos,agua de los leones,agua de luis,agua de mejia,agua de nino,agua de obispo,agua de oro,agua de pablo,agua de palma,agua de palos,agua de pascual,agua de pau,agua de pedro,agua de pena,agua de perez,agua de perro,agua de prata,agua de ramon,agua de rana,agua de rega,agua de rego,agua de rey,agua de robinson,agua de salud,agua de san antonio,agua de san juan,agua de saude,agua de tabuas,agua de todo o ano,agua de toro,agua de urao,agua de vaca,agua del cedro,agua del chanar,agua del coyote,agua del cuervo,agua del difunto chino,agua del espino,agua del jobo,agua del lobo,agua del medio,agua del negro,agua del overo,agua del perro,agua del pilar,agua del pozo,agua del rey,agua del sol,agua del tala,agua del tigre,agua del toro,agua del venado,agua del zacate,agua dela reina,agua delgada,agua do alto,agua do barreirinho,agua do barreiro,agua do boi,agua do cacador,agua do coriango,agua do montinho,agua do noratinho,agua do peixe,agua doce,agua doce do mantena,agua dorada,agua dormida,agua dos coqueiros,agua dos fortes,agua duende,agua dulce,agua dulce tehuacan,agua dulcita,agua emendada,agua enterrada,agua escondida,agua fea,agua ferro,agua fetida,agua fina,agua florida,agua formosa,agua fresca,agua fria,agua fria abajo,agua fria arriba,agua fria chica,agua fria de abajo,agua fria de arriba,agua fria de coyotitan,agua fria de gudino,agua fria grande,agua fuerte,agua gorda,agua gordita,agua grande,agua gregorio,agua hechicera,agua hedionada,agua hedionda,agua helada,agua hernandez,agua honda,agua ize,agua la trinidad,agua larga,agua larga de dolores,agua larga de libertad,agua leon,agua levada,agua ligera,agua limpa,agua limpia,agua linda,agua lluvia,agua longa,agua mala,agua mansa,agua maria,agua marina,agua marsa,agua matias,agua milada,agua milagra,agua milagro,agua millada,agua mina,agua mona,agua mosquito,agua nacida,agua negra,agua negro,agua nieve,agua nova,agua nueva,agua nueva del norte,agua nueva del sur,agua nueva norte,agua nuevo,agua padilla,agua palma,agua panda,agua pande,agua parada,agua paranhos,agua pascual,agua pasta,agua pasto,agua paxtle,agua peneira,agua pinole,agua planta,agua podrida,agua postrera,agua preta,agua prieta,agua prieto,agua puerca,agua quente,agua rasa,agua retirada,agua retorta,agua reves e crasto,agua rica,agua rosa,agua rosada,agua salada,agua salada hacienda,agua salgada,agua salobrega,agua salud,agua san antonio,agua santa,agua santa del yuna,agua santa potrerillos,agua sarca,agua seca,agua seda,agua sembrada,agua shuca,agua sola,agua sucia,agua suja,agua sulfhidrica,agua sulfidrica,agua sumida,agua tejocotes,agua tendida,agua tibia,agua tibia de ayala,agua tinta,agua toro,agua travessa,agua trinidad,agua turbia,agua verde,agua vermelha,agua vieja,agua viva,agua vives,agua zande,agua zarca,agua zarca de la pena,agua zarca de los martinez,agua zarca de martinez,agua zarca del pinal,agua zarquita,agua-branca,agua-coco,agua-garcia,agua-limpa,aguaa,aguablanca,aguabonita,aguabuena,aguacale,aguacaliente,aguacaliente de garate,aguacalientes de mazatan,aguacaluto,aguacancha,aguacandita,aguacapa,aguacapan,aguacapas,aguacatal,aguacatales,aguacatan,aguacatancillo,aguacate,aguacate abajo,aguacate arriba,aguacate camp,aguacate de arriba,aguacate de jacagua,aguacate de navarrete,aguacate del limon,aguacate linea,aguacatenango,aguacatengo,aguacatepec,aguacatera,aguacates,aguacatico,aguacatillo,aguacatitla,aguacatitlan,aguacatito,aguacatlan,aguacavito,aguacayo,aguacerita,aguacerito,aguacero,aguachapio,aguacheiro,aguachica,aguachichic,aguachihue,aguachil,aguachile,aguachini,aguacho,aguacilla,aguacillas,aguaclara,aguacu,aguacuzinho,aguad noudiar,aguada,aguada cecilio,aguada chos malal,aguada chunjabin,aguada de arenales,aguada de baixo,aguada de bueyes,aguada de cachinal,aguada de caferino,aguada de carillo,aguada de carrillo,aguada de castro,aguada de cima,aguada de garrapata,aguada de guerra,aguada de guerro,aguada de guzman,aguada de infante,aguada de jaraloma,aguada de la zorra,aguada de lomas,aguada de los bueyes,aguada de mayo,aguada de moya,aguada de pablo,aguada de palos,aguada de pantalaca,aguada de pasajeros,aguada de piedra,aguada de poruncha,aguada de quiaquilani,aguada de quiaquillani,aguada de quillaquillani,aguada de san antonio,aguada de tapia,aguada de tapis,aguada del calvario,aguada del cielo,aguada del muerto,aguada del pueblo,aguada del rosario,aguada del toro,aguada fort,aguada grande,aguada nova,aguada quiaquillani,aguadahan,aguadajan,aguadan,aguadas,aguadela,aguaderico de rubio,aguadia,aguadilla,aguadita,aguaditas,aguado,aguador,aguadulce,aguadux,aguaelarena,aguaestrada,aguaflorida,aguafo,aguafria,aguagon,aguahedionda,aguahedionde,aguahiel arriba,aguai,aguaie,aguaira,aguairenda,aguairendita,aguaises,aguaity,aguajal,aguaje,aguaje blanco,aguaje de garcias,aguaje de la amargosa,aguaje de la pila,aguaje de la trinidad,aguaje de las colonias,aguaje de las vacas,aguaje de las varas,aguaje de los garcia,aguaje de sanchez,aguaje del alamo,aguaje del alto,aguaje del monte,aguaje escondido,aguaje la palma,aguaje mezquite,aguaje monte,aguaje palma,aguaje pila,aguaje pinito,aguaje ramirez,aguaje san ignacio,aguaje san ygnacio,aguaje trinidad,aguajes,aguaji,aguajillo de abajo,aguajito,aguajosa,aguakate,aguake,agualada,agualamito,agualamo,agualani,agualcaguaire,agualegua,agualeguas,agualinda,agualino,agualisa,aguallamaya,aguallane,agualonga,agualongo,agualonguito,agualote,agualta vale,agualva,aguamansa,aguamare,aguamaroza,aguamasa,aguamecate,aguamiel,aguamilagro,aguamilito,aguamilpa,aguamitas,aguamite,aguamolo,aguamuerta,aguan,aguana cancha,aguana muyuna,aguanam,aguanana muyuna,aguanato,aguandi,aguanegra,aguanegro,aguanga,aguanil,aguanish,aguankhil,aguano muyuna,aguanqueterique,aguanquetorique,aguanta,aguanta callado,aguanta-callao,aguantao,aguanterique,aguanueva,aguapaba,aguape,aguapei,aguapei do alto,aguapepa,aguapepas,aguapepe,aguapepito,aguapey,aguapichuana,aguapiedra,aguaqueterique,aguaqui,aguaquire,aguaquisa,aguaquita,aguar,aguara charte,aguara-i,aguarague,aguarai,aguarate guazu,aguarate-mi,aguaray,aguarda,aguardaloma,aguarendita,aguareteguazu,aguaretemi,aguarico,aguarico 3,aguarico tres,aguaron,aguarone,aguaruca,aguaruca arriba,aguaruto,aguas,aguas aires,aguas altas,aguas amargas,aguas azules,aguas belas,aguas belinhas,aguas bellas,aguas blanca,aguas blancas,aguas blancas chico,aguas boas,aguas brancas,aguas buenas,aguas caliente,aguas caliente de acayo,aguas calientes,aguas calientes de acayo,aguas candidas,aguas claras,aguas claras do sul,aguas coloradas,aguas corrientes,aguas da prata,aguas de busot,aguas de chapeco,aguas de contendas,aguas de lindoia,aguas de mejia,aguas de mena,aguas de moura,aguas de peixes,aguas de ramon,aguas de sao pedro,aguas de verao,aguas del hoyo,aguas del padre,aguas del volcan,aguas do pardo,aguas do paulista,aguas do vere,aguas dulces,aguas ferreas,aguas finas,aguas formosas,aguas frias,aguas frias de baixo,aguas gordas,aguas lindas,aguas m. de ibira,aguas malas,aguas muertas,aguas negras,aguas nuevas,aguas pocas,aguas pretas,aguas prietas,aguas quentes,aguas radium,aguas saladas blancas,aguas santas,aguas termales,aguas verdes,aguas vermelhas,aguas vivas,aguas vives,aguas zarcas,aguasal,aguasantas,aguasay,aguasbamba,aguasblancas,aguasbuenas,aguascaldas,aguascalientes,aguasclaras,aguashuarco,aguasinga,aguasmestas,aguasvivas,aguat,aguata,aguatashi,aguataya,aguatechi,aguateno,aguatepec,aguatita,aguatitla,aguatitlan,aguaton,aguatona,aguatta,aguaverde,aguaviva,aguaviva de la vega,aguay,aguaylla,aguayo,aguaypalos,aguayrenda,aguaytane,aguaytia,aguaza,aguazarca,aguazinha,aguazul,agub,aguba,agubala,agubani,agubay,agubedia,agubu,agucadeira,agucadoira,agucadoura,agucha,aguchi,aguda,agudaboko,agudakiri,agudama,agudas,agudat israel,agude korpe,agudekope,agudelle,agudelo,agudere,agudere koyu,agudi,agudli,agudo,agudol,agudos,agudos do sul,agudrina,agudrino,agudu,agudungu,agudzera,agudzery,ague,aguebal,aguebt,aguechgal,agueda,aguedal,aguedim,aguedim msemrhir,aguedim msemrir,aguedita,aguedjal,aguegue-houedome,agueiro de baixo,aguejal,aguejdide,aguejgal,aguejito,aguejsal,aguejsgal,aguekimene,aguel,aguel-hoc,aguela,aguelal,aguelf,aguelhok,aguellal,aguelliz,aguelloui,aguells,aguelman,aguelmouns,aguelmous,aguelmouss,aguelock,aguelok,aguelouane,agueloufe,agueloui,aguelz,aguema,aguemonne nait aissa,aguemoun,aguemoun izem,aguemoun taida,aguemoun-n-ait aissa,aguemount-n-ait amar,aguen,aguend,aguendir,aguene,agueni,aguenit,aguenni,aguennsou,aguennzane,aguenouane,aguensou,aguensou aouragh,aguensou aourarh,aguensou n ouarg,aguensou-n-warg,aguensouk,aguentinha do campo,aguenyi,aguenza,aguenzane,agueouanou,agueouda,aguepa,aguer,aguer dnitisi,aguer souak,aguera,aguera de carriles,aguera de castanedo,aguera del coto,agueras,aguerd,aguerd imelal,aguerd imoul,aguerd issil,aguerd n issil,aguerd n ourtene,aguerd n aissa,aguerd n ikoubilen,aguerd n ikoubilene,aguerd n imelil,aguerd n oualous,aguerd n oudrar,aguerd n ougadir,aguerd n oulili,aguerd n ourhai,aguerd n ousrar,aguerd n ouzarhar,aguerd n tarhzout,aguerd n tizi,aguerd n tourirt,aguerd n towrirt,aguerd n tsoukt,aguerd nait oumani,aguerd ndoudad,aguerd nigherm,aguerd nou addouz,aguerd nou issil,aguerd nougidoum,aguerd nourtane,aguerd noussoul,aguerd nouzrou,aguerd ntafoukt,aguerd ntizi,aguerd ou dad,aguerd oualouss,aguerd oualouz,aguerd oumziou,aguerd ousguine,aguerd oussoul,aguerd ouzrou,aguerda,aguerdad,aguerdane,aguerde madiden,aguerdz,aguergoum,aguergour,aguerguei,agueria,aguerina,aguerito,aguerka,aguerksawene,aguero,aguerrioual,aguersafen,aguersafene,aguersaffene,aguersili,aguersioual,aguerssagene,aguert,aguerzaga,aguerzaguel,aguerzaguen,aguerzaguene,aguerzaka,aguerzeka,aguerzga,aguerzgua,aguerzouka,aguerzra,agues,aguessac,aguessis,aguet norte,aguet sur,agueta,aguetiti i,aguetiti ii,aguetiti iii,aguetiti iv,aguezzrane,agufa,agufo,agugliano,agugliaro,agugo,agugu,agugulu,aguho,aguia,aguia branca,aguiabampo,aguiar,aguiar da beira,aguiar de sousa,aguiar velho,aguiaro,aguiauan,aguibango,aguibri,aguica,aguica nueva,aguid,aguidagbade,aguidahoue,aguide,aguidi,aguie,aguieira,aguieiras,aguien,aguigadji,aguigaji,aguigdiji,aguiguican,aguijon,aguijotes,aguikchuk,aguil,aguila,aguila abajo,aguila azteca,aguila blanca,aguilafuente,aguilal,aguilamina,aguilandia,aguilani,aguilar,aguilar de alfambra,aguilar de anguita,aguilar de bureba,aguilar de campoo,aguilar de campos,aguilar de codes,aguilar de compoa,aguilar de ebro,aguilar de la frontera,aguilar de montuenga,aguilar de rio alhama,aguilar de segarra,aguilar de tera,aguilar del alfambra,aguilar del rio alhama,aguilarejo,aguilares,aguilas,aguilcourt,aguilera,aguilhadas,aguilhao,aguililla,aguililla de iturbide,aguilillas,aguilisin,aguilita,aguilla,aguillal,aguillan,aguillita,aguillo,aguillon,aguilo,aguilon,aguilota,aguim,aguima,aguimatang,aguimes,aguin sum,aguinaga,aguinaldo,aguinaliu,aguinane,aguinaouiaoui,aguincho,aguinda,aguing,aguinga,aguingay,aguingayan,aguinhas,aguini,aguinin,aguining,aguinni,aguino,aguinza,aguioas,aguiones,aguior,aguipawa,aguipo,aguira,aguire,aguiro,aguirre,aguirres,aguis,aguisahuale,aguisan,aguisan hacienda,aguisi,aguit,aguita,aguita chica,aguita de dios,aguita prieta,aguitap,aguitares,aguitas,aguiting,aguituni,aguja,agujang,agujas,aguje,agujereada,aguji,agujiriri,agujita,agujitas,agukpobe,agukwu nri,agul,agul-agul,agula,agula-e,agula`i,agulaa,agule,aguleri,agulezechuku,agulgomuwa,agulha,agulhas,agulhas negras,aguli,agulinitza,agulio,aguliya,agulla,agullana,agullent,agulli,agullik,agullu,agulo,agulskaya,agulu,aguluezechuku,agulul,agulupella,agulure,aguluri,agulusuanan,agum,aguma,agumar,agumaymayan,agumbayan subdivision,agume,agumei,agumes,agumingan,agumo,agums,agumu-koshi,agun,aguna,agunbiade,agunboye,agunboye imoru,aguncha,agunchos,agunda,agundu,agundugba,aguneze,agunfoye,agung,agungboyo,agungbu,agungok,agungsari,agungu,aguni,aguni iadan,aguniapara,agunipa,agunit,agunjin,agunk,agunku,agunla,agunlodo,agunmo,agunnaryd,agunpur,agunrege,agunsos,agunta,aguntoye,agunu,agunugban,agunughan,agunumura,agunyansuneja,aguo,aguo blanca,aguoba-umuaji,aguobiri,aguobu-umana,aguobu-umumba,aguobu-umumbia,aguoddim,aguokwa,aguolaoda,aguoniskiai,aguopo,agupalo este,agupalo oeste,agupalo west,agupalo weste,agupampa,agupampa hacienda,agupit,agupura,agur,agura,aguran,agurda,agurdina,agure,agureckaja,aguredo,aguren,agurfing,aguricoba,aguripsta,aguriskes,agurkiske,agurmaman,agurodo,agurra,agurru,agurto,agurton,agurty,aguru,agurut,aguryanovo,aguryevo,agus,agus mahala,agusa,agusalu,agusam,agusan,agusan pequeno,agusay,agusen,aguseun,agusguntur,agusha,agushan,agushi,agusi,agusipan,agusk,aguslu,aguso,agusovic-mahala,agustin,agustin codazzi,agustin de iturbide,agustin gonzalez,agustin libarona,agustin livarona,agustin lopez,agustin melgar,agustin ramirez,agustin roca,agustin vivas,agustina,agustina libarona,agustino,agustinos,agusto gomez villanueva,agustoni,agusuhin,agutami,agutan,agutay,agutaya,agutayan,agutayandelic,agutayandilit,agutayon,agute,aguti,agutino,agutri,aguts,aguwaw,aguwi,aguy,aguy sume hiid,aguy sume khid,aguy-shapsug,aguy-sume,aguydat,aguyi,aguytayn jisa,aguytuin chzhisa,aguytuyn chzhisa,aguz gonbadi,aguzban,aguzzao,aguzzo,agva,agvah,agvali,agvaly,agvan,agvani,agvanis,agvavera,agveran,agveraniheciban,agveren,agvi,agviran,agvirik,agvisi,agwa,agwa-were,agwad,agwada,agwadama,agwagon,agwagwa,agwai,agwal,agwala,agwan,agwan sarkin-koro,agwand,agwanggi,agwarra,agwasa,agwasi,agwat wiha,agwata,agwatashi,agwe,agwegan,agweh,agwengere,agwenit,agwenonyari,agwetambi,agwetiabuom,agwetiakokro,agwetta,agwiciri,agwingiri,agwit,agwofon,agwok,agwu,agwu agwuna,agwut-obolo,agy,agya,agyaan,agyaga,agyagbanya,agyagfalva,agyaglik,agyaglika,agyagos puszta,agyagoskert,agyagosszergeny,agyagya,agyaka,agyaka manso,agyakoso,agyamesu,agyan,agyanaga,agyang,agyanishkis,agyar,agyar dzheren,agyaragu,agyaragu gofa,agyareago,agyazi,agye,agyebon,agyedong,agyeikrom,agyeimpra,agyembra,agyempoma,agyena,agyena konopiem,agyenadom,agyepommaa,agyeri,agyeza,agyi,agyia,agyireso,agyktan,agyo,agyoxus,agypten,agyrek,agys,agyt,agytan-pamash,agywezidonbweywa,agz,agzaoya,agzdou nimasgat,agze,agzemir,agzhur,agziacik,agzibir,agziboz,agzibuyuk,agzidelik,agzikara,agzikaraca,agzikarahan,agzikarahan koyu,agzikiraca,agzipek,agzonik,agzonik mahallesi,agzu,agzukalay,agzunik,agzybir,agzybovka,ah,ah chuk,ah fong village,ah gah,ah soo garden,ah teing,ah-gwah-ching,aha,ahab khan,ahaba,ahabad,ahabaso mlawa,ahabetou talap,ahable,ahach,ahacha,ahache,ahacik,ahad,ahad al masarihah,ahad al mawasimah,ahad ali,ahad beyglu,ahad cikijing,ahad khan,ahad koruna,ahad majid,ahad marrat,ahad-houmane,ahadallane,ahadallane cross roads,ahadi bayatir kandi,ahadi matabarer kandi,ahadi matbarer k"ndi,ahadio,ahadipur,ahafona,ahaga,ahagala,ahagam,ahaham qaidan,ahai,ahailaoda,ahak,ahak chal,ahak chal-e yashi shah,ahak kalan,ahak kolan,ahak kureh,ahak-e mohammadabad,ahakachwamba,ahakaka,ahakishake,ahakista,ahakomiheva,ahakukope,ahal,ahal berkine,ahal ghidhab,ahal hammou ou ali,ahala,ahalai,ahalandravy,ahale allahyar khan,ahale harnam,ahale malik akram,ahali,ahalli,ahalnica wielka,ahalt,ahalu,ahalulie,ahaluna,aham,ahamad ki dhani,ahamadi,ahamahama,ahamakrom-bue,ahamali,ahaman koiro,ahamanso,ahamansu,ahamant,ahamar,ahamd khan,ahamdi,ahammadkati,ahammadpur,ahammar,ahammedkati,ahammedpur,ahan,ahan folad,ahan koshteh,ahan kushtah,ahan owuch,ahan pol,ahan qal`eh,ahan sar,ahan shahr,ahana masjed,ahanah masjid,ahanari,ahanaz,ahanda,ahandalaisi,ahandan,ahandi,ahandje,ahando,ahanechi,ahaneh masjed,ahanfe,ahanfolad,ahanfulad,ahang,ahanga,ahangama,ahangama central,ahangama east,ahangama nakanda,ahangama west,ahangan,ahangar,ahangar kala,ahangar kola,ahangar kola-ye `olya,ahangar kola-ye bisheh sar,ahangar kola-ye sofla,ahangar mahalleh,ahangaram,ahangaran,ahangaran-e `olya,ahangaran-e pain,ahangaran-e pa`in,ahangaran-e sofla,ahangaran-e vosta,ahangarha,ahangari,ahangaro,ahangaro bandah,ahangaro bandeh,ahangaro kala,ahangiran,ahaniet el miri,ahanikrom,ahanko,ahankoti,ahankro,ahankusha,ahankusta,ahantamaw,ahantamuo,ahantamwa,ahanya,ahaohu,ahaopo,ahaphuca bridge,ahaq,ahar,ahar bela,ahar kandar,ahar kuh,ahar meshgin,ahar meshkin,ahar namkin,ahara,ahara radhe singh,aharan,aharana,ahare,aharen,aharestan,ahargar,ahari,aharida,aharitu,aharney,ahartoub,aharwala,aharwan,aharwara,ahas,ahasanine,ahascragh,ahasen,ahasin,ahasonyewuda,ahaspokuna,ahassome,ahaste,ahasyanapitiya,ahat,ahat koyu,ahata,ahata mir fattiana,ahata moni,ahata sher muhammad,ahata thanadar,ahataguri,ahate ghulam ali shah,ahatl,ahatlar,ahatli,ahatoglu,ahatovici,ahatsira,ahatu,ahatuwewe,ahau,ahauk,ahaunduff cross roads,ahaura,ahaus,ahausen,ahavoher bridge,ahawa,ahawdemi,ahawi,ahaxe,ahaxe-alciette-bascassan,ahay beeseiin huryee,ahay-beeseiin-huree,ahaya,ahaymer,ahba,ahbachiyeh,ahbasor,ahbdupur,ahbisor,ahbisur,ahboga,ahburik,ahchoutane,ahchuk,ahcirik,ahcorik,ahdad,ahdaqeh,ahdekai,ahden,ahdenkallio,ahdhian,ahdi,ahdi phul,ahdian,ahdipur,ahdipura,ahdok fala,ahdulil harai,ahe,aheohia,ahea,ahealuo,ahebenso,ahedad,aheden,ahedo,ahefe,ahegg,aheik,aheji,ahekonnu,ahel,ahel aimar,ahel aoubak,ahel barke,ahel buxait,ahel chab,ahel chane,ahel cheiheb,ahel derhane,ahel driouch,ahel el cadi,ahel el jeilani,ahel embare u said,ahel embarec u said,ahel embuircai,ahel embuircal,ahel faloute,ahel fregat,ahel hamed aram,ahel hehe,ahel hors,ahel horsi,ahel imoula,ahel jader,ahel jors,ahel kourt,ahel sader,ahel salah,ahel sour,aheli el oued,ahelle,ahelou,ahema,ahemakrom,ahemba,ahen,ahenakrom,ahenawai,ahenda,ahengar,ahengaran,ahengarane pain,ahengaro,ahengaro bandeh,ahengkro,ahenkro,ahenny,ahenpaa,ahepe,ahepe agbleta,ahepe agbletta,ahepe akposo,ahepe akposso,ahepe asiko,ahepe assiko,ahepe hekpoui,ahepe kpoula,ahepe nouatche,ahepe nuatche,ahepe pokli,ahepe rhekpoui,ahepe-kpooula,aher,aherdi,ahere,ahere offa,ahere oloke,aheree,aheri,aheriso,aherla,ahermoumou,ahern,ahern trailer court,ahero,ahesia,ahet chane,ahetan,ahetze,ahevan,ahevanu,ahevi,aheville,ahfir,ahg,ahgmore bridge,ahgosatown,ahhire,ahi,ahi bango,ahi hammou ou ali,ahi koyu,ahi mahalleh,ahi veremos,ahia,ahi`ezer,ahia,ahia aba,ahia ukwu,ahia-ahor,ahia-eke,ahiaeke ndume,ahiakwo,ahiamable,ahiamadikope,ahiamajikope,ahiankwo,ahias,ahiatli,ahiba,ahibdia,ahibenso,ahibireso,ahiboz,ahica,ahid,ahid al mujatam,ahidaboe,ahidabu,ahiem,ahieremou,ahievren,ahigal,ahigal de los aceiteros,ahigal de villarino,ahigbe koffikro,ahiglium,ahihome,ahihud,ahiilyas,ahijadero,ahijaderos,ahijadito,ahijarve,ahijo,ahikiwi,ahikoy,ahil,ahila,ahilaeloe,ahilar,ahilaryaylasi,ahile,ahiler,ahileryaylasi,ahilet,ahili,ahill,ahillas,ahilli,ahillones,ahiloeloe,ahilteli,ahilulu,ahilyahar,ahilyas,ahima,ahimakrom,ahimalapur,ahimaso,ahimed,ahimed ager,ahimehmet,ahimer,ahimia,ahimma,ahimua,ahin,ahin posh,ahinase,ahinasi,ahinbasici,ahinbuboi,ahinco,ahinforoso,ahing,ahinga,ahingar,ahingar kili,ahingara baba ziarat,ahingaran,ahingaro baba kandao,ahingaro baba ziarat,ahingaro cham,ahingaro deheri,ahingaro derai,ahinikrom,ahinkaripur,ahinkofikrom,ahinkro,ahinkrom,ahinsai,ahinsan,ahinta,ahinza,ahiolo,ahioma,ahion,ahiout el kohia,ahip,ahipara,ahir,ahir fateh shah,ahir koyu,ahir surkhru,ahiran,ahiranwala,ahiranwali,ahirca,ahircik,ahire,ahireku,ahireso,ahirewala,ahirhisar,ahiri,ahiri chhor,ahirini,ahiriso,ahiritu,ahirkheda,ahirkoy,ahirkoyu,ahirlar,ahirli,ahirlikuyu,ahiro,ahiroa,ahiroi,ahiropa,ahirou,ahirozu,ahirpara,ahirtas,ahiruam,ahirwa rajarampur,ahisaa,ahisamakh,ahisari,ahishta,ahishte,ahishte-a,ahisilla,ahitinme,ahititi,ahitromby,ahituv,ahiwaran ki madaiyan,ahiyangan,ahiyeh,ahiyo,ahizera,ahizi,ahja,ahjarvi,ahjo,ahjum,ahkail,ahkalan,ahkand bala,ahkawhtawng,ahkerika,ahkida,ahkis,ahkoudewe,ahkpui,ahkran,ahl,ahl ali bin salim,ahl `arub,ahl `isa,ahl admer,ahl admeur,ahl admour,ahl ahmad,ahl aissa,ahl al `uzayr,ahl al ash`af,ahl al azair,ahl al fiddah,ahl al gharir,ahl al wadi,ahl allouch,ahl am fiddah,ahl amalou,ahl as sour,ahl attaouia,ahl aziza,ahl ba al jahl,ahl bel hadri,ahl brahim,ahl chaibat,ahl chaif,ahl chane,ahl dhuwaiyan,ahl el aich,ahl el aid,ahl el mader,ahl el oued,ahl embark houssein,ahl fashshash,ahl fulays,ahl ghadab,ahl hadi,ahl hammou ou ali,ahl hanaish,ahl hanash,ahl hunaysh,ahl imoula,ahl jader,ahl juhaif,ahl juhayf,ahl kadi,ahl kennine,ahl khouatra,ahl lahrima,ahl lamaichi,ahl maader el kebir,ahl mader,ahl mansur,ahl mantur,ahl marba,ahl metanou,ahl muhammad,ahl paswal,ahl rif,ahl rif tadla,ahl sahel,ahl salane,ahl shubayhi,ahl souss,ahl sufran,ahl zaouia,ahl zoun,ahl zraa,ahl zurqan,ahl-e iman,ahl-e qorban,ahl-e qurban,ahl-hadi,ahlab-ad-dud,ahladinagar,ahladipur,ahladpur,ahlah,ahlaikkon,ahlainen,ahlaing myaukkin,ahlajarvi,ahlajikoi,ahlake,ahlal,ahlalech,ahlam,ahlan,ahlanfedo,ahlanianwali,ahlapedo,ahlar,ahlashi,ahlat,ahlatci,ahlatcik,ahlatduzu,ahlatli,ahlatlibeli,ahlatliburun,ahlatlicesme,ahlatoba,ahlaw,ahlbach,ahlback,ahlbeck,ahlbeck seebad,ahlberg,ahlbershausen,ahlde,ahlden,ahldorf,ahle,ahle belluzein,ahle louad,ahlefeld,ahlek,ahlele,ahlem,ahlemissen,ahlen,ahlen-falkenberg,ahlenbach,ahlenberg,ahlendorf,ahlendung,ahlersbach,ahlerstedt,ahles,ahleute,ahlewala,ahlften,ahlholm,ahlhorn,ahli,ahli dhabiani,ahli kamboh,ahlianwala,ahlimbsmuhle,ahlimbswalde,ahlingen,ahlintel,ahlirawan,ahlke,ahlma,ahlone,ahloso,ahlou,ahloula,ahlsdorf,ahlsen,ahlshausen,ahlskog,ahlstadt,ahlten,ahlu,ahlum,ahluwal,ahluwala,ahm,ahma,ahmaawam,ahmad,ahmad `abd al `ali,ahmad `abd allahi,ahmad `abdul `ali,ahmad `ali,ahmad `arabi,ahmad `aziz,ahmad `urabi,ahmad abad,ahmad abad abarghoo,ahmad abad arbaeh,ahmad abad pain,ahmad abad shara,ahmad abade jarghooye,ahmad abu jayrah,ahmad ad daydal,ahmad al `ali,ahmad al `alwan,ahmad al faki muhammad,ahmad al hakim,ahmad al hallal,ahmad al hamadi,ahmad al hamdush,ahmad al hammadi,ahmad al haq,ahmad al haqq,ahmad al hasan,ahmad al husain,ahmad al husayn,ahmad al khamis,ahmad al mahmud,ahmad al muhammad,ahmad al yasin,ahmad ali,ahmad ali laghari,ahmad ali lar,ahmad ali shah,ahmad aliwala,ahmad angu,ahmad arain,ahmad arisar,ahmad as suhayl,ahmad ash shaykh,ahmad ash shayn,ahmad awah,ahmad badal,ahmad badw,ahmad baghela,ahmad bakhsh,ahmad bakhsh chandi,ahmad bakhsh zaildar,ahmad bakhshwala,ahmad balab,ahmad balad,ahmad baloch,ahmad barhami,ahmad bari,ahmad barthwala,ahmad baru,ahmad bayk,ahmad bazar,ahmad beg,ahmad beik,ahmad beyg,ahmad beyglu,ahmad beyk,ahmad bhalai jo goth,ahmad bhatti,ahmad bin `ali,ahmad birinda,ahmad birindah,ahmad birindah birind,ahmad chandio,ahmad chirawala,ahmad da wan,ahmad damach,ahmad darawala,ahmad dasht,ahmad de jhugge,ahmad dhandi,ahmad din,ahmad din gorand,ahmad din kirar,ahmad diwala,ahmad diwana,ahmad diwana ziarat,ahmad dur,ahmad ebn el hasan,ahmad fadil,ahmad faqir,ahmad farhan,ahmad fedaleh,ahmad felaleh,ahmad gabra,ahmad gado,ahmad galro goth,ahmad ghajar,ahmad ghalra goth,ahmad gharib,ahmad ghelro goth,ahmad gholam,ahmad ghulam,ahmad gol kalay,ahmad goorab,ahmad gorchchewala,ahmad goth,ahmad gul kalay,ahmad gul kelay,ahmad gul pashm koruna,ahmad gurab,ahmad haji,ahmad hammadi,ahmad hara,ahmad hasan,ahmad hosein,ahmad hosey,ahmad hoseyn,ahmad jabir,ahmad jabr,ahmad jamil,ahmad jan khan kala,ahmad jat,ahmad junajo,ahmad junejo,ahmad kala,ahmad kala kalay,ahmad kala kelay,ahmad kalar,ahmad kalay,ahmad kalwar,ahmad kandi,ahmad kelay,ahmad khalifa,ahmad khan,ahmad khan baloch,ahmad khan bhurgari,ahmad khan brohi,ahmad khan gadni,ahmad khan gola,ahmad khan goth,ahmad khan jat,ahmad khan kala,ahmad khan kalwati baloch,ahmad khan kalya,ahmad khan khosa,ahmad khan ki ari,ahmad khan kili,ahmad khan laghari,ahmad khan lashari,ahmad khan menah,ahmad khan nukrach,ahmad khan qal`ah,ahmad khan reti,ahmad khan rind goth,ahmad khan umrani,ahmad khandoawala,ahmad khani,ahmad khanwala,ahmad khanzai,ahmad khaskheli,ahmad khavandi,ahmad khel,ahmad kheyl,ahmad khozo,ahmad khvajeh,ahmad khwazai,ahmad khwazi,ahmad kodan,ahmad kola,ahmad kosh,ahmad koshteh-ye darbadam,ahmad kudan,ahmad kulwan,ahmad lar,ahmad lawand,ahmad machi goth,ahmad mahmudi,ahmad majid,ahmad makrani,ahmad malio,ahmad mardeh,ahmad meman,ahmad mir baihk,ahmad mira khel,ahmad mishu,ahmad mohana,ahmad mohano,ahmad mowla,ahmad muqrabwala,ahmad nagar,ahmad naich,ahmad nawabwala,ahmad nawaz,ahmad nuhrian,ahmad nukrach,ahmad odejo,ahmad otho,ahmad panhwar,ahmad pari,ahmad park,ahmad park block,ahmad pir goth,ahmad pirwala,ahmad ponhwar,ahmad posio,ahmad potai,ahmad punnu khelanwala,ahmad qalandari,ahmad ragal,ahmad rajo,ahmad rajpar,ahmad rami,ahmad rashid,ahmad rasul,ahmad raza,ahmad reza,ahmad rowghani,ahmad sah khel,ahmad salam,ahmad salar,ahmad salarani basti,ahmad sandhi,ahmad sar gurab,ahmad sara,ahmad sari,ahmad shah,ahmad shah khel,ahmad shah kheyl,ahmad shah kili,ahmad shah kotanai,ahmad shahi,ahmad shahwala,ahmad shajri,ahmad shan,ahmad shan kili,ahmad shawqi,ahmad sindhi,ahmad sola,ahmad sultanwala,ahmad tar,ahmad thiab,ahmad thiyab,ahmad tsah,ahmad tunia,ahmad wad al wahidu,ahmad wal,ahmad yar daulatana,ahmad yar khan,ahmad yar rid,ahmad yar tanwari,ahmad yarwala,ahmad zai,ahmad zai,ahmad zei gheljai,ahmad-e `omran,ahmad-e jami,ahmad-e naseri,ahmad-e nazeri,ahmad-e vard,ahmadab sani ol hayat,ahmadabad,ahmadabad anar,ahmadabad deris,ahmadabad khanleq,ahmadabad leyqoli,ahmadabad mangeli pain,ahmadabad mangeli-ye pain,ahmadabad nau,ahmadabad o`lya,ahmadabad rashti,ahmadabad sara,ahmadabad shah velayat,ahmadabad shahqol beygi,ahmadabad shahrak,ahmadabad shimali,ahmadabad sonad,ahmadabad-e `abdal,ahmadabad-e `olya,ahmadabad-e abrqu,ahmadabad-e arfan,ahmadabad-e astaneh,ahmadabad-e babai,ahmadabad-e bala,ahmadabad-e barakeh,ahmadabad-e bash,ahmadabad-e bashabad,ahmadabad-e chashmeh,ahmadabad-e dafeh,ahmadabad-e dash kasan,ahmadabad-e dastgerdan,ahmadabad-e dorcheh,ahmadabad-e e`temad ed dowleh,ahmadabad-e fallah,ahmadabad-e ferdows,ahmadabad-e ferdows-e do,ahmadabad-e garus,ahmadabad-e gol tappeh,ahmadabad-e harandi,ahmadabad-e jorquyaeh,ahmadabad-e kalateh,ahmadabad-e kalich,ahmadabad-e kam sefidu,ahmadabad-e kashani,ahmadabad-e khafrak,ahmadabad-e khak tappeh,ahmadabad-e khanleq,ahmadabad-e khanliq,ahmadabad-e khazai,ahmadabad-e koleybi,ahmadabad-e kuzehgarha,ahmadabad-e malek,ahmadabad-e mangeliabad-e pain,ahmadabad-e mashir,ahmadabad-e mavali,ahmadabad-e maziyeh,ahmadabad-e molla mas,ahmadabad-e moqbel,ahmadabad-e mosaddeq,ahmadabad-e moshir,ahmadabad-e mostowfi,ahmadabad-e now kahan,ahmadabad-e owfan,ahmadabad-e pain,ahmadabad-e panjeh,ahmadabad-e panjeh `ali,ahmadabad-e pol abgineh,ahmadabad-e poshtuiyeh,ahmadabad-e puzeh khun,ahmadabad-e qashqaguz,ahmadabad-e qatar gaz,ahmadabad-e qeytas,ahmadabad-e rashti,ahmadabad-e rasulian,ahmadabad-e razavi,ahmadabad-e safi khani,ahmadabad-e sang siah,ahmadabad-e sar tang,ahmadabad-e sarjam,ahmadabad-e shadjerd,ahmadabad-e shahjerd,ahmadabad-e shahrak,ahmadabad-e shaqol beygi,ahmadabad-e sheykh,ahmadabad-e sofla,ahmadabad-e soghad,ahmadabad-e sowlat,ahmadabad-e tappeh,ahmadabad-e tatian sar,ahmadabad-e ufan,ahmadabad-e vasat,ahmadabad-e zon`ad,ahmadagali,ahmadak,ahmadal,ahmadalilar,ahmadan,ahmadani,ahmadani colony,ahmadasin,ahmadawa,ahmaday,ahmadbayli,ahmadbeg,ahmadbeyg,ahmadewala,ahmadganj,ahmadgarh,ahmadguda,ahmadi,ahmadi banda,ahmadi dirga,ahmadi kalay,ahmadi kelay,ahmadi kor,ahmadi shama,ahmadi-ye sar boneh,ahmadi-ye sarneh,ahmadi-ye saroneh,ahmadi-ye sha`b jereh,ahmadia,ahmadipur,ahmadiyah,ahmadiyat abu al futuh,ahmadiye,ahmadiyeh,ahmadiyet abu el-futuh,ahmadizeh,ahmadjan kala,ahmadjankhan kala,ahmadkar,ahmadkati,ahmadkhan,ahmadkhan mena,ahmadkhan menah,ahmadkhel,ahmadkheyl,ahmadkot,ahmadli,ahmadlu,ahmadlu-ye `olya,ahmadlu-ye bala,ahmadlu-ye pain,ahmadlu-ye sofla,ahmadnagar,ahmadnah,ahmadoba,ahmadpur,ahmadpur east,ahmadpur karuamai,ahmadpur lamma,ahmadpur rauni,ahmadpur sayal,ahmadpur sial,ahmadpur tappa,ahmadpur tappah,ahmadpur west,ahmadqoli khani,ahmadsahkhel,ahmadsakhel,ahmadshah ka pind,ahmadsin,ahmadu,ahmadullahwala,ahmadum,ahmadun,ahmaduwala,ahmadvand,ahmadwal,ahmadwala,ahmadwali,ahmadwam,ahmadwan,ahmadyar,ahmadyarwala,ahmadzai,ahmadza`i,ahmadzai,ahmadzay,ahmadzayi,ahmadzi,ahmadzo kalay,ahmadzo kelay,ahmaghiyeh,ahmaj,ahmalahti,ahmalpur,ahmamiouech,ahman khan gadhi,ahmandwali dhok,ahmanikaj,ahmaqayeh,ahmaqieh,ahmaqzai,ahmaqzai kili,ahmar,ahmar bel aoubida,ahmar hallal,ahmar mawji,ahmar mogi,ahmar zrir,ahmas,ahmaskyla,ahmat,ahmataj,ahmatca,ahmatlar,ahmaus,ahmdal,ahmdeg,ahmdewala,ahmed,ahmed `abdullahi,ahmed `arabi,ahmed abd el kader,ahmed abdule,ahmed abu geira,ahmed ager,ahmed allal,ahmed asen,ahmed badu,ahmed bejas,ahmed bel hadj,ahmed bel-hachmi,ahmed ben abdallah,ahmed ben aida,ahmed ben ali,ahmed ben el badi,ahmed ben el hachemi,ahmed ben hammadi,ahmed ben jilali fejrih,ahmed bou el hachemi,ahmed el faki muhammad,ahmed el hadj,ahmed el haj,ahmed el hakim,ahmed el hallal,ahmed esh shein,ahmed gabr,ahmed gerada,ahmed hallal,ahmed id inun,ahmed ou ali,ahmed oulad kahia,ahmed ouled ben kahia,ahmed rachedi,ahmed raj,ahmed rajo,ahmed rashid,ahmed salem,ahmed u ali,ahmed wad el wahedu,ahmed yar rid,ahmed zid,ahmedabad,ahmedadil,ahmedagici,ahmedaj,ahmedaqej,ahmedavar,ahmedbese,ahmedbey,ahmedi,ahmedikli,ahmediye,ahmedler,ahmedli,ahmedmujovici,ahmednagar,ahmedoglu,ahmedpasa,ahmedpur,ahmedpur east,ahmedpur east municipality,ahmedpur macleodganj,ahmedredic katun,ahmedwal killi,ahmedwala,ahmedzai,ahmeek,ahmenhorst,ahmenovo polje,ahmentaga,ahmer el ain,ahmet bel hadj,ahmet-bejas,ahmet-bejasi,ahmetabat,ahmetadil,ahmetaga,ahmetaj,ahmetalan,ahmetalan koyu,ahmetalani,ahmetaq,ahmetaqi,ahmetasevici,ahmetasi,ahmetbaba,ahmetbese,ahmetbey,ahmetbeyler,ahmetbeyli,ahmetbubi,ahmetcayiri,ahmetce,ahmetceeli,ahmetcelebi,ahmetceli,ahmetci,ahmetcik,ahmetciler,ahmetdanisment,ahmetfakili,ahmetgecitli,ahmethaci,ahmethacilar,ahmethoca,ahmetkara,ahmetler,ahmetli,ahmetobasi,ahmetoglan,ahmetoglu,ahmetolen,ahmetoren,ahmetovci,ahmetovici,ahmetpasa,ahmetpinari,ahmetsaray,ahmetulugu,ahmetustahani,ahmetustahasan,ahmetyeri,ahmeur el ain,ahmici,ahmid,ahmiri,ahmmadpur,ahmo,ahmoonkyla,ahmote,ahmovaara,ahmovici,ahmqayah,ahmsen,ahmserort,ahmstorf,ahmu,ahmuk,ahn,ahna,ahnagh bridge,ahnagh cross,ahnai kili,ahnai tangi,ahnai tangi kili,ahnalik,ahnan,ahnani,ahnaou,ahnaz,ahnberg,ahndeich,ahndeich-eins,ahndel,ahndorf,ahnebeck,ahnebergen,ahneby,ahners,ahnianwala,ahnidzor,ahnikov,ahnis,ahnsbeck,ahnsen,ahnyinzon,aho,aho chia chuan,aho igbada,aho nguessan,aho yapi,ahoa,ahoada,ahoadao,ahoaxotitla,ahobilam,ahobra,ahobre,ahobre number 1,ahobreh,ahochi,ahochicheng,ahochihsien,ahocinado,ahodedji,ahodong,ahodwo,ahoe,ahoga maco,ahoga mula,ahoga mulo,ahoga yegua,ahogada,ahogades,ahogado,ahogados,ahogamula,ahogbe,ahogbeia,ahogbeya,ahoghill,ahoho,ahoinen,ahojo,ahokan,ahokwaa,ahokyla,ahola,aholahti,aholaie,aholankulma,aholankyla,aholanmaki,aholfing,aholi,aholiang,aholin,ahollang,aholmen,aholming,aholoboue,aholoukope,aholt,aholukofe,aholupu,ahoma,ahomadegbe,ahomaho,ahomaki,ahomakurombua,ahome,ahomgaon,ahomihadong,ahomisangdong,ahomma gligohoue,ahomma kpele,ahonda,ahondrona,ahonen,ahonessipe,ahong,ahong gongshe,ahongba nwave,ahonge,ahoni,ahoniitty,ahonkyla,ahonozo,ahonpaa,ahonpera,ahonwa,ahonwa-ashekon,ahoobam,ahope,ahopelto,ahopkubul,ahopsari,ahor,ahora,ahoraji,ahorca,ahorca los perros,ahorca zorra,ahorcadero,ahorcado,ahorcados,ahoren,ahorha,ahori,ahorn,ahornach,ahornberg,ahornhof,ahornhutte,ahornis,ahornod,ahoro,ahoro adan,ahoro agba,ahoro elemo,ahorokpa,ahorosso,ahoskie,ahossi takikro,ahotokope,ahou,ahou abou koffi,ahou yede,ahoua,ahoua abe,ahoua passe,ahoua yapi,ahouachene,ahouadji,ahouahikoro,ahouaja,ahouakro,ahouame,ahouan,ahouanou,ahouanou deux,ahouansouri,ahouati,ahouaya,ahouchane,ahouchou,ahoucli,ahoudi,ahoue,ahoue houe,ahouekro,ahoufi,ahouinez,ahouir,ahoukia,ahouliao,ahouma,ahouman abro,ahoumar pakoukro,ahouna akallou,ahounan,ahoundjo,ahoungnanou,ahouniassou,ahounienfoutou,ahouo,ahour,ahour omouri,ahoura,ahouri kouakou,ahourir,ahousat,ahoussou ketekro,ahoussou yao,ahoussoukro,ahoutoue,ahoutoukro,ahovan,ahovanoo,ahovo,ahoy acres,ahoy shores,ahoyaya,ahozada,ahozon,ahpini,ahpini mahallesi,ahpirik,ahpisor,ahpisur,ahpokhi,ahpola,ahpunus,ahpurik mahallesi,ahpyan,ahqaf al jabbarat,ahqaf al jabhiyah,ahqaf al jabhiyh,ahqaf ar ruzat,ahqunboy,ahr embark houseine,ahrab,ahrag,ahragartensiedlung,ahrain,ahrak,ahram,ahran,ahran dar,ahranabad,ahrandarreh,ahraq,ahrar,ahrarne,ahraron,ahrarun,ahras,ahrass,ahraula,ahraura,ahrazli,ahrbeck,ahrbergen,ahrbruck,ahrdorf,ahrdt,ahrem,ahren,ahrenberg,ahrendorf,ahrendsee,ahrendsee rubelow,ahrenfeld,ahrenhorst,ahrenlohe,ahrens,ahrensberg,ahrensbock,ahrensboek,ahrensbok,ahrensburg,ahrensdorf,ahrensfelde,ahrensflucht,ahrensfluchterdeich,ahrensfluchtermoor,ahrenshagen,ahrenshausen,ahrenshoft,ahrenshoop,ahrensmoor,ahrensnest,ahrensville,ahrenswohlde,ahrenviol,ahrenviolfeld,ahrestan,ahret,ahretkoy,ahrhutte,ahrib drabile,ahrinah,ahringhausen,ahriz,ahrmid,ahrn,ahrnbach,ahrok,ahroli,ahrom,ahronomichne,ahrsen,ahru,ahruiyeh,ahrweiler,ahrzerouftis,ahsaan,ahsahka,ahsam,ahsam koruna,ahsan ali shah,ahsan bhurt,ahsan colony,ahsan junejo,ahsan kacha,ahsan khan,ahsan pakka,ahsan pur mustaqil,ahsan sonaro,ahsan wahan,ahsanabad,ahsane,ahsanpur,ahsanwala,ahsanwala khuh,ahseia,ahsen,ahsen-oetz,ahsen-oetzen,ahsham,ahsham ahmad,ahsham aqai,ahsham ghayedha,ahsham hasan,ahsham now,ahsham qaedha,ahsham qayedha,ahsham qeydan,ahsham waziri,ahsham-e `ali ahmad kheyari,ahsham-e `ali ahmad khiari,ahsham-e `ali mohammad qasem,ahsham-e ahmad,ahsham-e ahmadi,ahsham-e aqai,ahsham-e asad,ahsham-e bakhshui,ahsham-e balai,ahsham-e hajj hoseyn,ahsham-e hasan,ahsham-e hasan-e khvajavi,ahsham-e hoseyn mohammad-e hajji,ahsham-e jamal,ahsham-e khodadad,ahsham-e kohneh,ahsham-e manu ahmadi,ahsham-e mashhadi karam-e khezri,ahsham-e mohammad eydari,ahsham-e mohammad ahmadi,ahsham-e mohammad heydar,ahsham-e musapur,ahsham-e now,ahsham-e ommid `ali,ahsham-e qadha,ahsham-e rashidi,ahsham-e safineh taj,ahsham-e seyyed,ahsham-e torkha,ahsham-e vaziri,ahsham-e zaer hoseyn-e ghazanfari,ahsham-e zaer hoseyn-e khezri,ahsham-e zaer-e mohammadi,ahsham-e za`er karam,ahsham-e za`er mohammadi,ahsham-e za`er teymur,ahsham-e za`er-e mohammadi,ahshan,ahsi,ahsiehmeihsu,ahsim,ahsis,ahstedt,ahsunk,ahsunkler,ahtaj,ahtama,ahtanum,ahtapolu,ahtari,ahtarinranta,ahtaung,ahtava,ahteenpaa,ahteentaka,ahtet pho,ahtet-bade-kyun,ahtiala,ahtma,ahtme,ahtokthe,ahtola,ahtopol,ahu,ahu anyim,ahu bam,ahu barand,ahu char,ahu chinah kalay,ahu chinah kelay,ahu cina kalay,ahu dasht,ahu qal`ehsi,ahu tappeh,ahuamu,ahuiyeh,ahua,ahuac,ahuaca del carmen,ahuacachahue,ahuacale,ahuacales,ahuacapan,ahuacatan,ahuacatancillo,ahuacate,ahuacatepec,ahuacates,ahuacatitlan,ahuacatlan,ahuacatlancillo,ahuachapan,ahuachapio,ahuacre,ahuacruz,ahuactlan,ahuacuitlapa,ahuacuotzingo,ahuahuastepec,ahuajo,ahuajutla,ahuakre,ahualican,ahualulca,ahualulco,ahualulco de mercado,ahuamendoro,ahuan-e bala,ahuan-e pain,ahuan-e vasat,ahuana,ahuancata,ahuandomekongga,ahuangguluri,ahuano,ahuanu,ahuaohano,ahuaolano,ahuapan,ahuareachi,ahuas,ahuas ducpan,ahuasbila,ahuasdacban,ahuashiyacu,ahuasta,ahuatal,ahuatelco,ahuatempan,ahuatenco,ahuateno,ahuatepec,ahuatepec del camino,ahuatitla,ahuatitlan,ahuatlan,ahuatzingo,ahuawatu,ahuaxoco,ahuaxotitlan,ahuay,ahuayem,ahuayen,ahuayo,ahuayucan,ahuazotepec,ahubaba,ahubam,ahubodagama,ahuchen,ahuchuetes,ahudong,ahue niamekro,ahuehuatic,ahuehuecingo,ahuehueco,ahuehuepan,ahuehuete,ahuehuetilla,ahuehuetitla,ahuehuetitlan,ahuehuetla,ahuehuetlan de gonzalez,ahuehuetzingo,ahuehueyo grande,ahuehuito,ahuelican,ahuespal nueva,ahuetlixpan,ahuexotla,ahueyahualpo,ahufal,ahugammana,ahugan,ahugoda,ahuhu,ahuibage,ahuibo,ahuica,ahuichila,ahuiekrou,ahuihuistla,ahuihuixtla,ahuijullo,ahuilitla,ahuille,ahuilote,ahuimanu,ahuimol,ahuindo,ahuinekrou,ahuiran,ahuisculco,ahuitera,ahuiti,ahuituyoc,ahuixotla prolongacion,ahuixtla,ahuizime,ahuizotl,ahuizotla,ahuiztla,ahuji,ahujo,ahuk,ahukavi,ahul,ahula,ahule,ahulia,ahuliyadda,ahuloa,ahult,ahulu,ahuma,ahumada,ahumada grande,ahumado,ahumakofe,ahumanum,ahumblekorpe,ahumblikofe,ahume,ahumkaswaso,ahun,ahun tegent,ahun tegeny,ahun tugre,ahunapalu,ahunda,ahundogi,ahunegol,ahungalla,ahungalla middaramulla,ahungana,ahunggol,ahungol,ahuni,ahunjo,ahunju,ahunkofe,ahunkwa,ahunlife,ahunta,ahuntizu,ahuntrum,ahuoluo,ahupe,ahuppauri,ahur,ahura,ahuraga,ahurak,ahuran,ahurangi,ahurcak,ahurcik,ahurcuk,ahurei,ahuren,ahuri,ahuriri,ahuriri village,ahurkoy,ahurkoyu,ahurlar,ahurlu,ahuroa,ahus,ahusby,ahuscatlan,ahuskarr,ahuso,ahute,ahuti bhatra,ahutia,ahutio,ahutte,ahutzasamuel,ahuu,ahuvand,ahuwamendoro,ahuwo,ahuy,ahuyama,ahuzam,ahuzam b.,ahuzat samuel,ahuzat sir herbert samuel,ahuzhen,ahuzza,ahuzzam,ahuzzam bet,ahuzzat baraq,ahuzzat naftali,ahuzzat shemu`el,ahuzzat sir herbert samuel,ahvan,ahvand,ahvanu,ahvarak,ahvaz,ahvenainen,ahveninen,ahvenisto,ahvenkoski,ahvenlahti,ahvenniemi,ahvensalmi,ahvensalo,ahvenselka,ahvenus,ahvenvaara,ahvio,ahvionniemi,ahvionsaari,ahvonniemi,ahwa,ahwahnee,ahwahnee estates,ahwaiwi,ahwar,ahwari,ahwatukee,ahwaz,ahweafutu,ahweiba,ahwerase,ahwerewa,ahwerewam,ahwiaa,ahwiam,ahwiawom,ahwidaye,ahwiriso,ahwiriwa,ahwitiaso,ahwiz,ahya nagar kalan,ahya nagar khurd,ahyaabad,ahyawf,ahyaya,ahybnagar,ahyiam,ahyiem,ahyireso,ahyiresu,ahylte,ahyondong,ahyonidong,ahyoniltong,ahyonjong,ahyonni,ahyonsamdong,ahzad,ahzeg,ahzim el foukani,ahzu malora,ai,ai abdellah,ai attobra,ai blun,ai brahim ou dawd,ai cho,ai culei,ai fang,ai ha,ai halfayah,ai kerma,ai mani,ai mbarek ou bella,ai mo,ai na lai,ai nghia,ai ngua,ai nicolas,ai noumale,ai quoc,ai rot,ai seris,ai tau ha,ai taut,ai thon,ai thuong,ai tu,ai yin young,aiago,aiiaga,aiiaka,aiidio,ai-ais,ai-avu,ai-hkam,ai-hpa,ai-ivu,ai-kyaw,ai-kyawpa,ai-nu,ai-pan,ai-to,ai-tzu-tou,aia,aia benti,aia bentih,aia bosco,aia murata,aia salina,aiadegga,aiaeta,aiai,aiaie,aiaki,aiam khan,aiamaa,aiamaccio,aiambak,aiambi,aiamina,aiamontina,aiandion,aiandok,aiangat,aianha,aiani,aiantion,aiao,aiaokope,aiaotsa,aiaouech,aiapua,aiara,aiare,aiasa,aiasi,aiaste,aiatlahuca,aiaz,aiazlar,aib bou kerchoun,aiba,aibaatoutua,aibak,aibaki,aiball,aibangis,aibanooan,aibar,aibara,aibase,aibatkioi,aibe,aibei,aibekro,aibel,aibelilwe,aibes,aibesi,aibetsu,aibga,aibigai,aibigi,aibingting,aibinio,aibl,aibling,aiblingerau,aibom,aibon,aibondeni,aibongo,aiboni,aibonito,aiboro,aiboshima,aibre,aibrum,aibu idiroko,aibukit,aibur,aibura,aiby,aicagranga,aicala,aicamessal,aich,aich noadda,aich nodda,aich teben,aich tebene,aicha,aicha an der donau,aicha bel lahsen,aicha bel lahsene,aicha ben lahcen,aicha raho,aicha vorm wald,aichabug,aichach,aichai,aichamarca,aichanahalli,aichane,aichangxia,aichapara,aichapiche,aichar,aichar char,aichat,aichau,aichazandt,aichbach,aichbauer,aichberg,aichbuchl,aichbuhl,aichdorf,aiche,aichegg,aichelau,aichelbach,aichelberg,aichelburg,aichelburk,aichelzeil,aichen,aichenbachof,aicheng,aicheng zhen,aichenrain,aichenzell,aichet,aichhalden,aichhapara,aichhof,aichhofle,aichholzhof,aichi,aichiaho,aichiaping,aichieh,aichig,aiching,aichisagami,aichiye,aichkirchen,aichlberg,aichouch,aichoun,aichschiess,aichstetten,aichstrut,aichtoum,aichumahana,aichune,aichuta chico,aichuta grande,aichwara,aichygyy-aly,aicirits,aickens,aico,aicota,aicun,aicuna,aicuri,aid,aid khchin,aid said,aida,aida mahgoud etdir,aida trovatore,aidadami,aidagheh,aidaku,aidal,aidalara,aidalpur,aidam,aidamari,aidan,aidan-ciufa,aidanbet,aidang,aidantschucha,aidar-bulak,aidara,aidara balanta,aidara beafada,aidara-koara,aide,aideia do prata,aideja,aideling-les-bouzonville,aidem,aiden,aiden lair,aidens cross roads,aidenau,aidenbach,aidenberg,aidenried,aidenur,aidhausen,aidhinion,aidhipsos,aidhipsou,aidhon,aidhonia,aidhonion,aidhonoi,aidhonokastron,aidhonokhori,aidhonokhorion,aidia,aidianzi,aidin,aidinevo,aidinion,aidinjik,aidinjk,aidirgandi,aidjel,aidling,aidlingen,aidmoun,aidmukhamedovskiy,aido,aido-nemme,aidoema,aidoena,aidogdu,aidois,aidomaggiore,aidomari,aidon,aidone,aidong,aidoni,aidonia,aidonion,aidonochori,aidonochorion,aidordu,aidou,aidoun,aidra,aidriss,aidt,aidu,aidu asundus,aidu-nomme,aidui,aiduishang,aidulpur,aiduma,aidun,aidun mangoro,aiduna,aidussina,aie,aiea,aiea heights,aiebiosu,aiebukl,aiech,aiekasaol,aiekasol,aielli,aiello,aiello calabro,aiello del friuli,aiello del sabato,aiellou,aielmam,aienda,aiendami,aienja,aiere,aierhkoyinsomu,aiesloho,aiet,aieta,aietpara,aievo,aiezza,aif hassaine,aifadun,aifam,aifamu,aifan bin tanuh,aifang,aifar,aifelle,aifeng,aifersdorf,aifesoaba,aifesoba,aiffres,aifir,aifiri,aifis,aifodo,aiga,aigachi,aigada,aigadani,aigaji,aigal,aigale,aigaleo,aigali,aigaliers,aigame,aigan,aigang,aigani,aigao,aigar-amet,aigarrobo,aigas,aigatani,aigayo,aigbin,aigburth,aigedzor,aigelsbach,aigelsbrunn,aigeltshofen,aigen,aigen bei admont,aigen im ennstal,aigen im ennstale,aigen im muhlkreis,aigendorf,aigeng,aigenstadel,aigenstadl,aigere,aigersberg,aigertsham,aigeti,aiggo,aighanjanata,aighe,aigherre,aighion,aigholz,aigialos,aigialousa,aigiere,aigina,aiging,aiginio,aiginion,aigio,aigion,aigird,aigis,aigle,aiglemont,aiglepierre,aiglern,aigleville,aiglieres,aiglkofen,aiglsbach,aiglsdorf,aiglsham,aiglsod,aiglun,aign,aignan,aignay,aignay-le-duc,aigne,aigner,aignerville,aignes,aignes-et-puyperoux,aigneville,aignot,aignoz,aigny,aigo,aigonnay,aigosthaina,aigosthena,aigovo,aigra nova,aigre,aigrefeuille,aigrefeuille-daunis,aigrefeuille-sur-maine,aigremont,aigrumae,aigu,aigua,aiguafreda,aiguamurcia,aiguanji,aiguanxia,aiguavella,aiguaviva,aiguay,aigubu,aigue-belle,aiguebelette,aiguebelette-le-lac,aiguebelle,aigueblanche,aiguefonde,aiguemorte-les graves,aigueperse,aigues-juntes,aigues-mortes,aigues-vives,aiguesvives,aiguetinte,aiguevive,aigueze,aiguieres,aiguillan,aiguilles,aiguilles-en-queyras,aiguillon,aiguines,aigun,aiguo,aiguo dadui,aiguocun,aigurande,aigut,aiguwa,aiha,aihai,aihal,aihama,aihao,aihaodian,aihaogou,aihaomao,aihaoping,aihaozhang,aihar,aihara,aihe,aiheping,aiheshangbao,aiheyacun,aiho,aihobabekun,aihokele,aihol,aihotsun,aihu,aihua,aihuangling,aihuarinca,aihui,aihuihsiang,aihuitsun,aihuixiang,aihun,aihunhsien,aihuntsun,aihuobabekun,aiiamari,aija,aija bhuta,aijadero,aijagaiba,aijagate,aijal,aijala,aijanneva,aijao,aijati,aijauka,aijauka lama,aijavaara,aijavara,aijawou,aijazi,aije,aijen,aijenvaara,aijhara,aiji,aijia,aijia bingqiang,aijia yuanzi,aijia zhen,aijiabao,aijiabaozi,aijiachong,aijiaci,aijiacun,aijiadatan,aijiadian,aijiadonggou,aijiafan,aijiagou,aijiahe,aijialin,aijiaping,aijiapo,aijiaqi,aijiashantou,aijiatun,aijiawan,aijiawopu,aijiayao,aijiaying,aijiayuan,aijiazhaizi,aijiazhuang,aijigang,aijn,aijo,aijonkyla,aijota,aiju,aijura,aijurico,aijuwala,aik,aik bontar,aik hong and aik chiang estate,aik pancur,aika,aikaidoboro,aikaka,aikamachi,aikampat,aikamun,aikaouel,aikarboe,aikarbu,aikare,aikaru,aikaterini,aikava,aikavaravi,aikawa,aikbeta,aikdalam,aikdewa selatan,aikdewa utara,aikdia,aikdulu,aike,aike guer,aikelile,aikembung,aiken,aiken summit,aikeng,aikenton,aikerang,aiketa,aikewu,aikgamang,aikgembul,aikgering,aikhal,aikhama,aikhla,aikijedi,aikin,aikin grove,aikins,aikinsville,aikipa,aikira bitari,aikirikos,aikiryiannis,aikja,aikjambe,aikkerip,aikkila,aiklah,aiklengis,aiklomak,aikma,aikman,aikmanis,aikmel,aikmual,aikngempok,aikola,aikolaha,aikolahi,aikon,aikondo,aikopu,aikopushe,aikoputsun,aikotosashe,aikou,aikouliao,aikpaek,aikpaik,aikpera,aikperapa,aiksepalang,aikstieji versupiai,aiksuling,aikta,aiktawah,aikton,aiku,aikun,aikuo,aikuotsun,aikyeng,aikyran,aikywa,ail,ail antoshkin,ail batal,ail finn,ail tunia,ail-batalkan,ail-batalken,ail-torken,aila,aila gongshe,aila koubo,ailabhatta,ailahyar shahana,ailaico,ailaki,ailala,ailam,ailampur,ailan,ailanapaha,ailanes,ailang,ailani shah,ailano,ailaparihi,ailau,ailbahar,aile,ailef,ailefroide,ailersbach,ailertchen,aileru,ailes,ailesho,ailet,ailet jridani,aileteko,ailey,ailhac,ailhas,aili,ailian,ailiao,ailiaochiao,ailiaoli,ailiaopai,ailiaoping,ailiaopu,ailiaoting,ailias,ailibugai,ailigandi,ailihsihu,ailikesilun,ailin,ailinan,ailing,ailingandi,ailingao,ailingbei,ailingen,ailinghou,ailinghsu,ailingxia,ailingzi,ailinzhuang,ailkundi,aillac,aillaico,aillant,aillant-sur-milleron,aillant-sur-tholon,aillas,aille,aillemont,aillemore,aillenacally,ailleres,ailleux,aillevans,ailleville,aillevillers,aillevillers-et-lyaumont,ailli,aillianville,aillieres,ailliet,aillik,aillipen,aillo beter,aillo ccollana,aillo sillata,aillo silluta,aillo talor,aillolaya,aillon,aillon-le-jeune,aillon-le-vieux,ailloncourt,ailly,ailly-le-haut-clocher,ailly-sur-meuse,ailly-sur-noye,ailly-sur-somme,ailo,ailo-atynakovo,ailo-atynovskoye,ailoche,ailola,ailolo,ailomea,ailong,ailongwanham,ailotta,ailoute,ailpur,ailringen,ailsa,ailsardi,ailsbach,ailston,ailt an chorrain,ailu,ailubei,ailulai,ailuluai,ailun,ailunga,ailuoto,ailwala,aily,aim,aim-ravat,aima,aima afghanan,aima bari,aima debipur,aima jat,aima ladian,aima maira,aima saidan,aima saiyidan,aima shahji,aima sheikhan,aimable,aimabu,aimaburan,aimaga,aimagan,aimagang,aimah bari,aimah muftian sayadan,aimahe,aimai,aimakan,aimakeau,aimal,aimal kalla,aimal khel,aimala,aimalae,aimalai,aimaling,aimalirin,aimand,aimani,aimanitas,aimankule,aimar,aimara,aimarautio,aimare,aimargues,aimaso,aimasul mare,aimata,aimaya,aimba,aimbasin,aimbed-ishan,aimbet-ishan,aimbir,aimcha,aimchakakyr,aimchan,aime,aime msira,aime msirate,aimei,aimeke,aimeken,aimela,aimen,aimenguan,aimenkou,aimentsun,aimeong,aimera,aimeramu,aimere,aimi,aimidonani,aimilianos,aimin,aimin liudui,aimin xiang,aimincun,aimion,aimiri,aimitat,aimiwala,aimkovo,aimla,aimma,aimma sheikhan,aimmah shahji,aimo,aimogasta,aimola,aimolau,aimonas,aimonos,aimons,aimontappo,aimontoppo,aimore,aimores,aimossa,aimoto,aimoto-shin,aimou,aimour,aims,aimual,aimual atas,aimualkebun,aimudaiyingzi,aimukili,aimus,aimusa,aimuti,aimuul,aimwell,ain,ain `atara,ain aakrine,ain aalaq,ain aamcha tahtani,ain aanoub,ain aar,ain aarab,ain aarne,ain aarouss,ain aata,ain abou sreisse,ain achache,ain achboun,ain ahabar,ain ahla,ain ahmed,ain ahmed ouled kacem,ain aicha,ain aissa,ain ait zatou,ain aksab,ain aksalb,ain al beida,ain al ghamur,ain al hamra,ain al qcob,ain al qourra,ain al qsab,ain al-dar,ain alaq,ain alcha,ain alem,ain alilou,ain allem,ain amara,ain amenas,ain anoub,ain arab,ain arbel,ain arhbal,ain arkou,ain aros,ain arouas,ain arouss,ain arsa,ain arus,ain as sferjila,ain ayala,ain baadj,ain baal,ain babouch,ain baita,ain balloul,ain ban,ain bardja,ain barq,ain bate,ain battiua,ain bazoua,ain bazouq,ain bechriti,ain beida,ain bellout,ain belot,ain ben amar,ain ben ammar,ain ben kbellil,ain ben khelhil,ain ben krellil,ain ben srour,ain benaamane,ain beni mathar,ain beni stitene,ain bentilli,ain berd,ain berda des beni brahim,ain berdat beni brahim,ain berde,ain berria,ain berrian,ain betioua,ain blit,ain bou aiba,ain bou aissa,ain bou allal,ain bou arouss,ain bou chellah,ain bou hassane,ain bou kellal,ain bou mahdi,ain bou qellal,ain bou seba,ain bou zennzene,ain bouassa,ain bouchrik,ain boudinar,ain bounaya,ain boura,ain bourai,ain boustza,ain bouzid,ain brahim,ain brahim ou salah,ain cadah,ain chair,ain chammas,ain chamr,ain chater,ain cheggag,ain chellala,ain chellala des adaouras,ain cherichera,ain chib,ain chock,ain chok,ain chouater,ain dakar,ain dakkur,ain dalaa,ain dalia kbira,ain dalia kebira,ain daqne,ain dar,ain dara,ain defali,ain dekne,ain delbi,ain delfi,ain dfali,ain dhab,ain dheb,ain diab,ain dilbi,ain divar,ain djasser,ain djedida,ain djelloula,ain djeloula,ain djemaa,ain djmadjma,ain dlaime,ain dorij,ain douffana,ain doukkara,ain dourij,ain drafil,ain draham,ain ebel,ain ech chaara,ain ech chams,ain ech charqiye,ain ech charuiye,ain ech cheqaq,ain ech cheraya,ain ech chouk,ain ed dananir,ain ed deir,ain ed delb,ain ed delbe,ain ed delbi,ain ed dobb,ain ed douair,ain ej jahach,ain ej jahash,ain ej jamajme,ain ej jamous,ain ej jaouz,ain ej jaouze,ain ej jarab,ain ej jdide,ain ej jemaa,ain ej jmel,ain ej jnah,ain ej jorn,ain ej journ,ain el aachra,ain el aalaq,ain el aamoud,ain el aarab,ain el aarous,ain el achra,ain el adel,ain el alak,ain el aonsar,ain el aouda,ain el apuda,ain el arba,ain el asker,ain el assad,ain el bad,ain el baida,ain el bakar,ain el barde,ain el batie,ain el batrak,ain el batt,ain el beida,ain el blat,ain el bnine,ain el borj,ain el boum,ain el bya,ain el djemel,ain el faroua,ain el fije,ain el fokhokh,ain el fraidis,ain el fritissa,ain el ghmiqa,ain el hadid,ain el hadjar,ain el haiz,ain el haizoun,ain el hajar,ain el hajel,ain el hajjar,ain el halzoun,ain el hammam,ain el hamman,ain el hamra,ain el hanout,ain el haour,ain el haramiye,ain el harrouda,ain el hayate,ain el hedid,ain el hiloue,ain el hour,ain el houtz,ain el how,ain el ibel,ain el jaouze,ain el jarab,ain el jrane,ain el kabir,ain el kabou,ain el karm,ain el kebire,ain el kelb,ain el khadjiche,ain el khadra,ain el khannzir,ain el kharoun,ain el kharroube,ain el khebira,ain el kheurbe,ain el kheurbet,ain el kraa,ain el kradra,ain el kroum,ain el ksar,ain el ksob,ain el labane,ain el laboue,ain el luweiqa,ain el maaisre,ain el marj,ain el mediour,ain el mehnaide,ain el melouk,ain el mentne,ain el mouia,ain el mreisse,ain el orma,ain el oudene,ain el pacha,ain el qabou,ain el qadah,ain el qanntara,ain el qatroun,ain el qerd,ain el qott,ain el qourra,ain el rhar,ain el rharaf,ain el rharf,ain el rhazale,ain el-beida,ain el-halazoun,ain el-kharoube,ain el-rummaneh,ain en nasb,ain enn nakhil,ain enn nide,ain ennakri,ain enoub,ain er raha,ain er raheb,ain er rami,ain er reggada,ain er remmeni,ain er rihane,ain er rihani,ain er rmel,ain er roummane,ain es sabaa,ain es safsaf,ain es sahn,ain es sai,ain es said,ain es salib,ain es saouda,ain es sebaa,ain es sfa,ain es sghairi,ain es sihha,ain es sindiane,ain es sinndiane,ain es souar,ain es souk,ain es srhir,ain essalhiye,ain et tannour,ain et tina,ain et tine,ain et toghra,ain ett teffaha,ain ett tine,ain ett tine charqiye,ain ett tine rharbiye,ain ez zaite,ain ez zaitoune,ain ez zarka,ain ez zarqa,ain ez zebde,ain fakroun,ain faouar,ain fares,ain feka,ain fekana,ain fekane,ain fendel,ain fenndel,ain figeh,ain fije,ain fite,ain fnatene,ain fouchi,ain fraidis,ain fraj,ain fritissa,ain ftaime,ain garzoul,ain ghazil,ain ghezaia,ain ghit,ain gouza,ain guendoul,ain guenndoul,ain guernouch,ain guettara,ain habbar,ain habet,ain hadid,ain hajel,ain halzoun,ain hamara,ain hammam,ain hamman,ain hammana,ain hammane,ain hamra,ain hamze,ain harrouda,ain hebar,ain hedid,ain herche,ain hfad,ain hjal,ain hoceine,ain hod,ain horche,ain houila,ain houssaine chemaliye,ain houssaine rharbiye,ain ibel,ain ibl,ain ibn fuhaid,ain igouramen,ain igouramene,ain igourramene,ain iskandar,ain jaka,ain jamajma,ain jannal,ain jaouz,ain jaouze,ain jdaide,ain jdid,ain jdioui,ain jdiwi,ain jedidi,ain jema,ain jemaa,ain jenane,ain jenna,ain jerane,ain jerfa,ain jioula,ain johra,ain jouaiya,ain jouaya,ain jrain,ain jrane,ain juwari,ain kadah,ain kaddour ben larbi,ain kafer zabad,ain kafra,ain kalboun,ain kalboune,ain kammour,ain kamor,ain kamoure,ain kansara,ain karim,ain kdima,ain kechera,ain kedah,ain keddaba,ain kercha,ain kercka,ain keria,ain kerma,ain kermane,ain kesur,ain kfaa,ain kfea,ain khabourie,ain khaiet,ain khannzire,ain khannziri,ain khial,ain khiar,ain kial,ain kob,ain krannguet sour,ain krar,ain ksab,ain ksour,ain ktara,ain ktof,ain laabid,ain labgar,ain lahcen,ain lahdjar,ain lailin,ain lailoun,ain lalag,ain larais,ain lasri,ain lebdah,ain leben,ain leiloun,ain lelou,ain leuh,ain mlila,ain maabad,ain maabed,ain maabet,ain maaskar,ain madhi,ain mahbel,ain mahbet,ain majdalaine,ain mallal,ain mansour,ain mara,ain marchouch,ain marchouche,ain maskar,ain mchalou,ain mediouna,ain melah,ain melh,ain mellah,ain melouk,ain menzor,ain messaoud,ain mestour,ain moaskar,ain mokra,ain mouaffa,ain mouaffaq,ain moulares,ain moulgelouane,ain mzid,ain mzizuoi,ain nait hmad,ain najem,ain nhereth,ain nijm,ain ninou,ain nouissy,ain nouna,ain nuni,ain ouaraqa,ain ouard,ain ouarga,ain ouarqa,ain ouazine,ain ouerk,ain oulmene,ain ounzar,ain oussera,ain ouzain,ain ouzzai,ain qadib,ain qala,ain qana,ain qaniye,ain qara,ain qenia,ain qiniya,ain qita,ain qlaiaa,ain qoun,ain qouniya,ain qsour,ain radouane,ain raha,ain ramka,ain regada,ain reha,ain rehouna,ain rha,ain rhazale,ain rheza,ain rig,ain rmel boukabene rhar,ain rouss,ain saade,ain sabel,ain saber,ain saffah,ain safsaf,ain safsafa,ain sahalin,ain saib,ain said,ain saidana,ain saidane,ain sala,ain salah,ain salloum,ain samour,ain saoufar,ain sayada,ain sebaa,ain sefra,ain sellam,ain sennour,ain sfa,ain sferjila,ain sfissif,ain sharid,ain sidi chelif,ain sifni,ain sikh,ain sindiane,ain slaie,ain slaiye,ain sleimo,ain smar,ain smara,ain snad,ain souar,ain souk,ain tafechna,ain taforalt,ain taftecht,ain taga,ain tagrajout,ain tagrount,ain tagrout,ain tahir,ain taiss,ain tamezouine,ain tammaria,ain tanta,ain taoudjad,ain taoudjat,ain taoujat,ain taoujdat,ain taoujdate,ain taridelt,ain tassera,ain tasta,ain tedeles,ain tedles,ain tefela,ain tekki,ain telidjene,ain temonchent,ain temoucheni,ain temushent,ain tendrara,ain tenntach,ain terma,ain tholba,ain timetrout,ain tin,ain tine,ain tinn,ain tinta,ain tnine,ain touffana,ain touira,ain tounta,ain tourine,ain touta,ain traz,ain trik,ain wahan,ain yaaqoub,ain yagout,ain yahav,ain yahia,ain yakoub,ain youcef,ain zaitoun,ain zaitoune,ain zala,ain zara,ain zarqa,ain zebde,ain zeggou,ain zegouer,ain zehalte,ain zeitouria,ain zeituna,ain zelten,ain zeroula,ain zhalta,ain ziana,ain zigzaou,ain zijana,ain zohra,ain zora,ain zorah,ain zoren,ain-chrob,ain-djarit,ain-el-hamam,ain-el-khadra,ain-et-turk,ain-hessainia,ain-kuduk-kazbek,ain-kuduk-kazbyek,ain-larbi,ain-zaouia,aina,aina haina,aina jesus,aina-bulak,aina-bulan,ainab,ainabad,ainabo,ainac,ainachuta,ainacueca,ainad,ainagu,ainain,ainainou,ainajek,ainak,ainaka,ainakofo,ainakope,ainali,ainaloa,ainalpur,ainamba,ainamoi,ainamolo,ainan,ainapo,ainapur,ainar,ainara anantapur,ainarai,ainari,ainashaikh,ainasoja,ainastalo,ainat,ainata,ainatal,ainau,ainavolu,ainay,ainay-le-chateau,ainay-le-vieil,ainazi,ainbai,ainbek,ainbrach,ainbul,ainbushahi da danna,aincha,ainche,aincialde de arizcun,aincille,aincioa,ainco,aincourt,aincreville,ainda,ainderby,ainderby steeple,aindin,aindling,aindorf,aindu,aindulu,aine,aine abessa,aine abid,aine amara,aine arko,aine arnat,aine assef,aine babouche,aine bala,aine balloul,aine beida,aine ben srour,aine bou dib,aine bou dinar,aine boucif,aine cherchar,aine cheurfa,aine darah,aine defla,aine djeloula,aine draham,aine dzarit,aine efaili,aine el amrah,aine el arba,aine el asker,aine el bey,aine el bordj,aine el hadiar,aine el hadjar,aine el hadjel,aine el hammam,aine el hout,aine el krarrouba,aine el melouk,aine el rharbia,aine enchir rayan,aine es smara,aine et turk,aine fakroun,aine fares,aine faress,aine faroua,aine fekane,aine fezza,aine guettar,aine guettara,aine houinia,aine kebira,aine kercha,aine kerma,aine kermes,aine khermane,aine khiar,aine kralfoune,aine leghatha,aine mabed,aine madi,aine mahdi,aine mai,aine malah,aine melah,aine melsa,aine merane,aine mergoum,aine messaoud,aine mlila,aine mokra,aine mouilah,aine naga,aine nouissy,aine ouled el mahil,aine radouane,aine regada,aine roua,aine rouss el akrat,aine sefra,aine seinour,aine sellam,aine seynour,aine sfia,aine sidi cherif,aine smara,aine soffra,aine soltane,aine sultan,aine tagrout,aine taya,aine tebinet,aine tedeles,aine tellout,aine temouchent,aine tholba,aine tinn,aine trab,aine trick,aine trid,aine yagout,aine zada,aine zaouia,aine zehra,aine zeroula,aine zigzaou,aine-jesus,aineba,aineffe,aineina,ainelle,ainemoi,aineramoe,aineramu,aines,ainessaba,ainet,aineto,aing baung yaung,ainga,aingara,aingbaukkon,aingbaukkyi,aingbaunggyaung,aingbe,aingbok,aingbu,aingbyaunggyaung,aingdai,aingdaing,aingdama,aingdo,aingdon,aingelar baru,aingelar lama,ainger,aingeray,aingeroh,aingeville,ainggalaw,ainggin,ainggo,ainggon,ainggye,ainggye taung,ainggyi,ainggyi east,ainggyi west,ainggyigon,ainggyikon,ainggyile okshitkon,ainghausen,aingiri,aingkalaung,aingkye,aingma,aingmagyi,aingmayo,aingoulaincourt,aingounda,aingpu,aingshe,aingtha,aingthabyu,aingwaing,aingwin,aingwun,aingya,aingywa,aingzauk,ainharp,ainhecio,ainhice,ainhice-mongelos,ainhoa,ainhofen,aini,ainiasi,ainiba,ainielle,ainieres,aininagan,ainishan,ainja,ainjani,ainkhausen,ainle,ainleh,ainling,ainnab,aino,aino bhatti,ainohama,ainokama,ainoke,ainomal,ainonai,ainono,ainoumal,ainoumane,ainoura,ainoura-machi,ainouracho,ainowal,ainowali,ainpur,ainring,ainsa,ainsdale,ainsen,ainshebyin,ainsi,ainsielem,ainsimourou,ainsley woods,ainslie,ainsoune,ainstable,ainsworth,ainsworth corner,ainsworth junction,ainta,aintab,ainti,aintoura,aintree,ainture,ainu,ainu bhatti,ainu ogbogwu,ainuana,ainubagal,ainukon,ainulikaierhko,ainuma,ainumanai,ainunuk,ainunuk barat,ainval,ainval-septoutre,ainvelle,ainwala,ainwan,ainya,ainyade,ainyar,ainyi,ainzai,ainzai kili,ainzgarne,ainzon,aio,aio banda dheri,aiobi,aiogi,aiogwei,aioi,aioi-shi,aioicho,aioimachi,aiokako-suido,aiola,aiombo,aiome,aion,aiorman,aiorrucula,aioua,aiouegon,aioufis,aioun aicha,aioun allah,aioun el atrouss,aioun el ksab,aioun sidi mellouk,aioun smar,aioun-du-dra,aioura,aiowo,aip,aipaaking,aipai,aiparipa,aiparuca,aipate,aipatia,aipe,aipeana,aipena,aipetu,aipha,aipi,aipi kor,aipi zhen,aipia,aipiapa,aiping,aipingli,aipingtzu,aipinmao,aipiruhu,aipo,aipokeng,aipoln,aipolovo,aipong,aipu,aipu gongshe,aipuntuk,aiputu,aipuxincun,aiqare,aiqberik,aiqberik daya,aiqberik lauk,aiqbukaq,aiqdareq,aiqgenit,aiqian,aiqiancun,aiqiangbei,aiquara,aiquile,aiquina,aiquncun,air abik,air asin,air bagi,air bamban atas,air bamban lebong,air base acres,air base city,air base heights,air berudang,air city,air de france,air genting,air halim rambung,air hangat,air hitam,air hitam sijabut,air itam,air jernih,air kasar,air keruh,air khas,air kundur,air kuning,air kuning selatar,air lajang,air layang,air line junction,air masin,air molek,air putih,air putih 1,air putih 2,air seutuy,air ssangsangan,air tawar luar,air teluk hessa,air teluk kiri,air tenang,air-tam,air-tame,aira,aira nait sedrat,aira tan,aira tan guzar,airaba,airabamba,airabamba hacienda,airabang,airabek,airabit,airabu,airach,airacollo,airae,airaea,airaes,airahu,airai,airaines,airaksela,airakuen,airam,airampuycco,airan,airanakan,airancki,airandji,airangat,airanndji,airao,airas,airasca,airaura,airawanteten,airbajo,airbak,airbakoman,airbalam,airbangis,airbanua,airbara,airbari,airbatu,airbatuan,airbebau,airbelo,airbeloe,airbening,airberau,airbertumbuk,airboro,airbuaya,aircha,aird,aird hills,aird mhor,aird of sleat,aird of tong,aird tong,aird tunga,aird uig,airdeles,airderas,airdikit,airdingin,airdjamban,airdrie,airduo,aire,aire libre,aire libre mobile home park,aire-sur-ladour,aire-sur-la-lys,airebehje,aireborough,airech,airedale,airedele,airek,aireke,airel,airenai,airenai ii,airenshi,airere,aires,airetj,airey,aireys inlet,airgas,airgegas,airgemoeroeh,airgemuruh,airgin sum,airgonting,airgouin,airgul,airhadji,airhaji,airhilang,airhill,airhitam,airhitan,airi,airiauka,airihuanca,airijas,airik,airikkala,airinge,airinget,airion,airiputi,airischwand,airismaa,airitam,airituba,airivaara,airjamban,airjernih,airkijang,airkloebi,airklubi,airkobi,airkoendoer,airkol,airkulskiy,airkum,airkuning,airlan,airlasi,airlee,airlee court,airlenbach,airlhok,airlie,airlie beach,airliki,airline acres,airline park estates,airlingkar,airlonia,airlua,airly,airmadidi,airmadidi atas,airmadidi bawah,airmancur,airmanis,airmara,airmasin 1,airmasin 2,airmasin dua,airmata,airmen village,airmerah,airmesoe,airmesu,airmodidi,airmolek,airmont,airmont acres,airmyn,airnaningan,airnanti,airnatja,airnjato,airnjatoeh,airnjatuh,airnyatuh,airo,airoa,airoca,airoes,airola,airolaf,airole,airoli,airolo,airon,airon-notre-dame,airon-saint-vaast,airor,airoux,airpaku 3,airpaku tiga,airpanas,airparit,airpepaya,airperiukan,airpisangan,airpoetib,airpoetih,airpoint,airport acres,airport city,airport drive,airport gardens,airport grove,airport highlands,airport plaza,airport road,airport road addition,airport subdivision,airport village,airprang,airprioekan,airpriukan,airpuluh,airputih,airputih 1,airputih 2,airputih dua,airputih satu,airrai,airrainmaru,airrami,airroending,airrunding,airsain,airsatang,airsimotus,airsimutus,airsirah,airsonok,airsuning,airtabit,airtas,airtau,airtawar,airtawar 1,airtawar 2,airtawar dua,airtawar satu,airteluk,airtembaga,airtembago,airtenang,airterdjoen,airterdjun,airterjun,airth,airtiba,airtiris,airton,airu,airua,airuno,airvault,airview court,airville,airwa,airwa katra,airway heights,airy,airy castle,airy estates,airy hill,airydale,ais,aisa,aisabouli,aisado,aisago,aisai,aisaisa,aisaita,aisan,aisaniemi,aisano,aisaoe,aisar,aisari,aisaricho,aisarimachi,aisarimatsu,aisaroaivve,aisasale,aisaten,aisatene,aisato,aisau,aisawa,aiscet mariam,aisch,aische,aische-en-refail,aischfeld,aiscia,aisdorf,aise,aise adou,aise assamoua,aise beche,aise okou,aise yapo,aiseau,aisega,aisegba,aiselukharka,aisemi,aisemont,aisen,aisenai,aisenu,aisere,aiserey,aisesti,aisey,aisey-et-richecourt,aisey-le-duc,aisey-sur-seine,aish,aish mugam,aish shahi,aisha,aishalton,aishalton village,aishan,aishang,aishanping,aishanpur,aishantang,aishara,aishevo,aishiga,aishihik,aishima,aishiyaz,aisholt,aishpury,aishuichi,aisiga,aisih,aisiko,aisilmapua,aisimi,aisina,aising,aisisiki,aisiskes,aiskan,aiskari,aiskorkli,aislaby,aislingen,aisne,aiso,aisomont,aisonville-et-bernoville,aisou,aisova,aisovo,aispel,aispuri,aispute,aisru,aisru kalan,aissa,aissa abd el kader,aissa bakli,aissa el ksiba,aissa er rifi,aissa peunar,aissa rifi,aissama el faouqani,aissama et tahtani,aissaoui,aissem faouqa,aissem tahta,aissey,aissi,aissi bakli,aissomont,aissoun,aist,aistaig,aistenthal,aistere,aistereskrogs,aisterham,aistersheim,aisthofen,aistiskiai,aistovo,aistrup,aisty,aisuli,aisvalukharka,aisy,aisy-sous-thil,aisy-sur-armancon,aisyalukharka,ait,ait abd en nour,ait addi,ait ammar,ait amrane,ait azza,ait azzi,ait `abbes,ait `issa,ait aadel,ait aaiad,ait aamana,ait aamara,ait aazimana,ait abaid,ait abbas,ait abbes,ait abbess,ait abbou,ait abd el fadit,ait abd el slam,ait abd er rahmane,ait abd es slam,ait abd smed,ait abdalah u mesaud,ait abdallah,ait abdallah ej jellil,ait abdallah el jelil,ait abdallah ou boubker,ait abdallah ou messaoud,ait abde el fadel,ait abdellah,ait abdelli,ait abdennor,ait abderezag,ait abderrazak,ait abdes slam,ait abdeslame,ait abdessemt,ait abdi,ait abdoun,ait abdoune,ait abelli,ait abi,ait abid,ait abit,ait abou belfaa,ait aboukais,ait achour,ait adal,ait addi,ait addi ou lho,ait addi ouhlo,ait addi oulhoul,ait addou abbiar,ait addouch,ait adel,ait adet,ait adi oulhou,ait adja,ait affi,ait afi,ait agano,ait ahaza,ait ahlal,ait ahmad,ait ahmadou haddou,ait ahmane,ait ahmed,ait ahmed ou ali,ait ahmed ou brahim,ait ahmed ou haddou,ait ahmed ou said,ait ahmida,ait ahmimich,ait ahra,ait ahsine,ait aicha,ait aiddi,ait aimi,ait aiouz,ait ais,ait aisa,ait aiss,ait aissa,ait aissa ou ali,ait aissa ou brahim,ait aissa ou ichou,ait aissa ou yahia,ait aissi,ait aissi des ait ouassif,ait aixa,ait ajal,ait ajda,ait aki,ait akka,ait akka ou ali,ait akki,ait akob,ait akou,ait al awr,ait al ghazi,ait al grari,ait al haj,ait al maaroufi,ait al qarmoudi,ait ala,ait alba,ait ali,ait ali abd allah,ait ali ben ahmed,ait ali moussa,ait ali nito,ait ali nitto,ait ali ossou,ait ali ou abdallah,ait ali ou abdellah,ait ali ou ahmad,ait ali ou ahmed,ait ali ou aissa,ait ali ou alhassane,ait ali ou ali,ait ali ou allah,ait ali ou brahim,ait ali ou daoud,ait ali ou haddou,ait ali ou haj,ait ali ou hamed,ait ali ou hammou,ait ali ou harzoun,ait ali ou hasene,ait ali ou hassene,ait ali ou hassoua,ait ali ou hmad,ait ali ou houssine,ait ali ou iggo,ait ali ou ikko,ait ali ou ikkou,ait ali ou khai,ait ali ou lahsene,ait ali ou lssa,ait ali ou mhaik,ait ali ou mhand,ait ali ou mohannd,ait ali ou moussa,ait ali ou said,ait ali ou salm,ait ali ou sou,ait ali ou youssef,ait ali ou youssef el ouata,ait ali ou zekri,ait ali oua zki,ait ali oussou,ait ali u hamed,ait ali youssef,ait alioun,ait alizine el mellah,ait alla,ait alla ben yahya,ait alla ou kssacene,ait allah,ait allah ben yahya,ait allal,ait alliwn,ait aloane,ait alou,ait alouan,ait alouane,ait alouk,ait aman u said,ait amane,ait amar,ait amar ou brahim,ait amar ou ikkou,ait amar ou said,ait amara,ait amara bour,ait amara el bour,ait amara el oued,ait amara el ouled,ait amelouch,ait amelouk,ait amer,ait amer ou abid,ait amer ou bettane,ait amer ou brahim,ait amer ou youssef,ait ameur,ait ameur ou bettane,ait ameur ou brahim,ait ameur ou youssef,ait amghar,ait amibel,ait amir,ait amlouk,ait ammar,ait ammeur,ait ammou,ait amor,ait amrad,ait amran,ait amrane,ait amrhar,ait amrou abid,ait amrou ikkou,ait amza,ait amzal,ait ani,ait anissi taourirt,ait anter,ait aomar,ait aomar ou ali,ait aouana,ait aouch,ait aouer,ait aourhi,ait aqqa,ait arbi,ait arfa,ait arhoudoudou,ait ariba,ait aritane,ait assa,ait assem,ait assoun,ait atelli,ait athman,ait athmane,ait athmane ou moussa,ait atman,ait atmane,ait atmane ou moussa,ait atmane ou youssef,ait atou tiouzaguin,ait attab,ait atti,ait atto ou moussa,ait atton,ait attou,ait attou ou ikkou,ait attouch,ait au said,ait auarhmane,ait ayach,ait ayad,ait ayad lahdab,ait aziz,ait azouk,ait azouz,ait azrou,ait azza,ait azzi,ait azzou,ait azzouz,ait ba amrane,ait ba hammou,ait ba maati,ait ba mmad,ait ba salah,ait baali,ait bagrari,ait bah,ait baha,ait bahlli,ait baho,ait baich ben ali,ait baji,ait bajji,ait bakka,ait bakki,ait bakri,ait balal,ait barka,ait bassa,ait basso,ait bassri,ait beghli,ait bekki,ait bel caid,ait bel fejjaj,ait bel haj,ait bel hassene,ait bel lahsen,ait bel lahsene,ait belahsen,ait belaid,ait belhasen,ait belkassem,ait bella,ait bella ou said,ait bellahcen,ait belloumousse,ait ben ahmed,ait ben aiss,ait ben aissa,ait ben akki,ait ben akko,ait ben ali,ait ben ali zloul,ait ben alla,ait ben amar,ait ben amer,ait ben aomar,ait ben aouicha,ait ben azzou,ait ben bihi,ait ben cherif,ait ben haddou,ait ben hadou,ait ben haida,ait ben hamaida,ait ben hamou,ait ben haoua,ait ben ichou,ait ben jilali,ait ben kassam,ait ben lahcen,ait ben lahsene,ait ben mhand,ait ben maati,ait ben mohammed,ait ben mumen,ait ben najaj,ait ben nasser,ait ben noumene,ait ben omar,ait ben ouazar,ait ben ouazer,ait ben said,ait ben taazza,ait ben tahar,ait ben taleb,ait ben yahia,ait ben yahya,ait ben yaia,ait ben youssef,ait ben zemour,ait ben zidane,ait ben`abbou,ait benichou,ait bennour al hsasna,ait benxerif,ait berdjal,ait berk,ait berkhous,ait berkine,ait berrada,ait beska,ait besri,ait beurda,ait bichcha,ait bigarasen,ait bigarso,ait bihi,ait bla ou said,ait blal,ait blel,ait bnihia,ait bohi,ait boho,ait boohoo,ait bork el oued,ait bou abd er rahmane,ait bou abdelli,ait bou abedli,ait bou aiss,ait bou akki,ait bou ali,ait bou allal,ait bou amra,ait bou aziz,ait bou beker,ait bou chennt,ait bou daoud,ait bou grir,ait bou haddou,ait bou hamra,ait bou hassane,ait bou hasseine,ait bou houkou,ait bou ichi,ait bou iknifene,ait bou issoutir,ait bou kerchoun,ait bou krim,ait bou madhi,ait bou malek,ait bou malk,ait bou matik,ait bou mellal,ait bou mesref,ait bou messaoud,ait bou mezrag,ait bou mhamed,ait bou mhand,ait bou mlak,ait bou n rhart,ait bou namze,ait bou niche,ait bou nihi,ait bou nit,ait bou nouh,ait bou oualal,ait bou ouknifene,ait bou ouregh,ait bou ourjdane,ait bou rhiout,ait bou riah,ait bou rig,ait bou said,ait bou said,ait bou setta,ait bou slimane,ait bou tarhat,ait bou touhalt,ait bou yahia,ait bou yahya,ait bou youssef,ait bou zguia,ait bou zid,ait bou ziit,ait bou zit,ait bou-daoue,ait bouahi,ait bouali,ait boubaj,ait boubker,ait bouchahou,ait bouchbout,ait bouchebout,ait bouchent,ait bouchta,ait boudar,ait boudir,ait bouhamou,ait bouhou,ait bouhous,ait bouih,ait bouih ben ali,ait bouijjaouane,ait bouil,ait boujamaa,ait boujid,ait boujjane,ait boujjaouane,ait boukha,ait boukhari,ait boukhir,ait boukhzir,ait bouknifen,ait boulamane,ait boulemane,ait boulkhair,ait boulkheil,ait boulkhiel,ait boulkhir,ait boulmane,ait boulmane amzaorou,ait boulrmel,ait boumlak,ait boummi,ait boumouh,ait boumsilal,ait bounnit,ait bounrart,ait bourd,ait bourda,ait bourhim,ait bourhioul,ait bourigue,ait bourk,ait bouro,ait bourzeg,ait bousselmane,ait boustata,ait boutili,ait bouyl,ait bouziane,ait bouzid,ait bouzmane,ait braham,ait brahim,ait brahim ou naceur,ait brahim ou said,ait brik,ait buhokku,ait buhucu,ait buhus,ait bwali,ait cacas,ait calah,ait chaf,ait chafa,ait chaib,ait chaid,ait chaih,ait chama,ait chao,ait chaouia,ait chard,ait chart,ait chebao,ait chekrane,ait cherad,ait cherif,ait cherki tadla,ait chfi,ait chiker,ait chikker,ait chkounda,ait chouarhit,ait chouer,ait choufoua,ait chouiaz,ait choukaia,ait chouki,ait chrad,ait chrou,ait codi,ait daoued,ait daha,ait dahane,ait dahhane dar douie,ait dahmah,ait dahmane,ait dameuse,ait daoub youb,ait daoud,ait daoud azgoumrzi,ait daoud azguemerzi,ait daoud ou afa,ait daoud ou azzi,ait daoud ou moussa,ait daoud tamdaouzguez,ait daoud youb,ait dara,ait daud,ait dawd,ait dawd azgoumrzi,ait de khedara,ait debachine,ait dib,ait djemaa,ait doha ait lahzene,ait dou,ait dounj,ait dris,ait driss,ait du,ait ech cherif,ait ej jdid,ait el awni,ait el aziz,ait el aaoud,ait el abbas,ait el abid,ait el agoss,ait el aiaf,ait el ain,ait el aouni,ait el aour,ait el arbi,ait el asri,ait el assri,ait el ayachi,ait el azis,ait el baji,ait el begal,ait el borja,ait el cadi,ait el caid,ait el farsi,ait el fersi,ait el fkih,ait el flais,ait el forssi,ait el gail,ait el ghazi,ait el ghazi ou ghazzou,ait el ghori,ait el goumakh,ait el gous,ait el grachoua,ait el grari,ait el guial,ait el habib,ait el hachemi,ait el hachmi,ait el hadj,ait el hadj lehcen,ait el hait,ait el haiz,ait el haj,ait el haj el hassene,ait el haj said,ait el hajj,ait el hajj messaoud,ait el hajoui,ait el hami,ait el hand,ait el hart,ait el hit,ait el housseine,ait el kadi,ait el kamane,ait el kanou,ait el kechach,ait el khaloufi,ait el kharche,ait el khedem,ait el kherta,ait el khiaf,ait el lafadel,ait el mahjoub,ait el maksoub,ait el mamoun,ait el mannsour,ait el maqsoub,ait el mejdoub,ait el meskine,ait el mouzazi,ait el rhaz,ait el rhazi,ait el rhazy,ait el rherch,ait el rhori,ait el tamlel,ait elkadi,ait embarek,ait er rahal,ait er roum,ait ersane,ait escal,ait ezzate,ait faraji,ait fars,ait farts,ait faska,ait fazaz,ait fdila,ait ferouane,ait feth,ait fetkhaoui,ait fetouli,ait flaiss,ait frah,ait gandou,ait gazzou,ait ghanem,ait ghazi,ait ghemat,ait ghiat,ait gmat,ait gmil,ait gouali,ait gouzel,ait guerri,ait guerry,ait guetto,ait guir,ait ha,ait habbou,ait habibi,ait haceine,ait haddo,ait haddou,ait haddou aissa,ait haddou amer,ait haddou ameur,ait haddou nait youl,ait haddou ou amar,ait haddou ou amer,ait haddou ou ameur,ait hadi,ait hadi rass el aine,ait hadj lahcen,ait hadj messoude,ait hadj said,ait hadou oua aissa,ait haffi,ait hag,ait haiba,ait haida,ait haimouch,ait haj ali,ait hakkou ou ali,ait hakkuon,ait halaouane,ait halli,ait halli el oulja,ait hallouane,ait halou ou adou,ait halwane,ait hamad,ait haman,ait hamane,ait hamani,ait hamdine,ait hamdou mouh,ait hamed,ait hamed ou said,ait hamed u said,ait hamet u said,ait hami,ait hamid,ait hamida,ait hamidine,ait hammad,ait hammane,ait hammi,ait hammodo,ait hammou,ait hammou hadda,ait hammou ou yafaa,ait hammou ou yahia,ait hammou ou yahya,ait hammou said,ait hammou yahia,ait hammoudou,ait hamou,ait hamou ou said,ait hamoudene,ait hamsi,ait hamza,ait handou hias,ait handu hias,ait hani,ait haouant,ait haqi,ait harirou,ait harou,ait haroun,ait harrasen,ait harrasene,ait hasen,ait hassain,ait hassain ou haddour,ait hassaine,ait hassane,ait hassassa,ait hassein,ait hasseine,ait hassem,ait hassene,ait hassine,ait hassoun,ait hattab,ait hattou,ait haya,ait hcine,ait hdafa,ait hemi ou bou taieb,ait heouich,ait heraibsa,ait herechid,ait herouk,ait herra,ait heseine,ait hessane,ait hessein,ait hhammed,ait hichem,ait hichen,ait hichene,ait hida,ait himi,ait himmi,ait hmad ou nacer,ait hmad ou naser,ait hmane,ait hmida,ait houssane,ait hsain,ait hsaine,ait hsayn,ait hssain,ait hssayne,ait iazza,ait iaza u ihia,ait ibourk,ait ibrahim,ait ibrereur,ait ibriren,ait ibrirene,ait ichchou,ait ichchou ali,ait ichchou ouali,ait ichek,ait icho,ait ichou,ait ichou ou ali,ait ichou ou brahim,ait idair,ait idar,ait idder,ait ider,ait idir,ait iferd,ait iferkane,ait iferkhane,ait ifous,ait ifrane,ait iftene,ait igad,ait igga,ait iggui fraks,ait ighil,ait ighmour,ait ighourri,ait igourdane,ait iguemat,ait iguir,ait iguit,ait iguite,ait ihadj,ait ihala,ait iheda,ait ihla,ait iider,ait ikhelef,ait ikhlef,ait ikho,ait ikhoumane,ait ikhoumane sahraoui,ait ikhoumane sebt,ait ikhs,ait ikko,ait ikrelef,ait ikro,ait illoucene,ait illoul,ait illoussene,ait imellil,ait imellol,ait imelloul,ait imi,ait imlil,ait imloul,ait imouddene,ait imouni,ait imour,ait imugueni,ait inklou,ait inzi,ait inzzi,ait iohra,ait irhemour,ait irhezan,ait irhil,ait irhiour,ait irhir,ait irhs,ait irhzig,ait irizane,ait irmer,ait isehak,ait isfoul,ait ishak,ait ishaq,ait isnar,ait issa ou lahsen,ait issedar,ait issehak,ait issi,ait issimour,ait iterd,ait izzo,ait jaa,ait jaba,ait jaber,ait jabeur,ait jafer,ait jaha,ait jamaa,ait jeiat,ait jerrar,ait jerror,ait jiitakoul,ait jouana,ait kacem,ait kaddour,ait kadi,ait kadour,ait kais,ait kais aomar,ait kaisse,ait kalfa,ait kalla,ait karbid,ait kassem,ait kataa,ait katif,ait kato,ait kdif,ait keddour,ait kediff,ait kerbid,ait kerdi,ait kermous,ait kermouss,ait kerroum,ait kerroun,ait khabach,ait khalak,ait khalaq,ait khald,ait khaled,ait khalek,ait khalfa,ait khalifa,ait khalifat,ait khamis,ait khannouj,ait kharrou,ait khchiin,ait khebbach,ait khebbache,ait khechine,ait kheir,ait khelafa,ait khelf,ait khemis,ait khennoudj,ait khennouj,ait kherdi,ait khetab oufella,ait khetab ouzedder,ait khijja,ait khiyar,ait khlat,ait khleb,ait khlifa,ait khlouf,ait khojmane,ait khorsi,ait khou kheden,ait khou khedene,ait khoukhdene,ait khouya,ait kiatene,ait kine,ait knouf,ait kouroum,ait kouzoud,ait krad,ait kralfa,ait krersi,ait krojmane,ait ktab,ait ktab oufella,ait ktab ouzder,ait lchguare,ait lhadj said,ait laasri,ait laati,ait labbes,ait labou,ait labrouk,ait lachguer,ait lachgueur,ait ladi,ait lahcen,ait lahcen hmjoune,ait lahcen ou ali,ait lahcen ou braim,ait lahcen ou said,ait lahlou,ait lahsen,ait lahsen ou ali,ait lahsen ou brahim,ait lahsen ou mimoun,ait lahsene,ait lahsene ou ali,ait lahsene ou brahim,ait lahsene ou mimoun,ait lahssen,ait lahssene,ait laiachi,ait laiz,ait lakmane,ait lalloud,ait larba,ait larbi,ait larbi ou ali,ait larhras,ait lasri,ait lassene ou mimoun,ait lasseri,ait latchour,ait lazza,ait lbour,ait lebbou,ait lechen ou ali,ait lechgenou ali,ait lechger ou ali,ait lechghare,ait lehiou,ait lehssene ou mimoun,ait lejuid,ait lelt,ait leppachine,ait lerhrari,ait lerzel,ait lfekik,ait lhaag,ait lhasen,ait lhasen boubker,ait lhasene,ait lhassen boubker,ait lhassene,ait lhassene boubker,ait lhati,ait lhoufouaa,ait lil,ait ljiar,ait lnda,ait lobrouk,ait m hammed,ait m rabt,ait mamar,ait mbarek,ait mbark,ait mhamed,ait mhamed imeziane,ait mhamed inerziane,ait mrhoujchine,ait mrhrarine,ait msaad,ait m`ammar,ait maala,ait maamar,ait mahalla,ait mahfoud,ait mahha,ait mahi,ait mahiou,ait mahma,ait mahmoud,ait makhlouf,ait maklouf,ait malek,ait malk,ait malla,ait mameur oujedid,ait mancour,ait mannsour,ait mannsour ou ali,ait mansor,ait mansour,ait mansour ou ali,ait manus,ait maouch,ait mar,ait marar,ait mare,ait marouf,ait marzougue,ait masfor,ait matate,ait mav,ait mayyit,ait mazdou,ait mazirh,ait mazou,ait mazouz,ait mazzouz,ait mbarek,ait mbarek ou bella,ait mechaar,ait mechkouk,ait mehalla,ait mehammed,ait mehannd,ait mekhlouf,ait mekraz,ait mekraze,ait mekrin,ait mekrine,ait melal,ait melk,ait melkt,ait mellal,ait melloul,ait melloule,ait menad,ait menndil,ait mensou,ait menzou,ait meraou,ait mereisig,ait merghad,ait merrhad,ait merrhad tarhzout,ait merri,ait mersid,ait merzoug,ait merzouk,ait mesaud,ait mesdo,ait mesri,ait messaoud,ait messaoud ou addou,ait messoud,ait mettat,ait mexaar,ait meziane,ait mgharine,ait mhammed,ait mhammed tarhzout,ait mhand,ait mhand ou said,ait mial,ait migrizane,ait migrizene,ait mimal,ait mimoun,ait mimoune,ait mislain,ait mislaine,ait mked,ait mkeud,ait mkhaled,ait moha ou ali,ait mohamed,ait mohamed ben fatmi,ait mohammad,ait mohammed,ait mohammed ou ali,ait mohammed taghzout,ait mohand,ait mohand ou amar,ait mohannd ou amar,ait moudzit,ait mouga,ait mouh yahia,ait moulay ahmed,ait moulay aomar,ait moulay brahim,ait moulay said,ait moulay tahar,ait mouli,ait mouloud,ait moumene,ait moussa,ait moussa addi,ait moussa bou ahmed,ait moussa ou ahmed,ait moussa ou ali,ait moussa ou amar,ait moussa ou daoud,ait moussa ou ichou,ait moussatine,ait moussi,ait moussi omar,ait mouzit,ait mrabet,ait mrart,ait mriouat,ait muguen,ait musau amar,ait my ali,ait mza,ait mzal,ait mzil,ait mzouz touizat,ait niskt,ait ntaleb akka,ait nyoult,ait nacer,ait nador,ait namous,ait narar,ait naser,ait naseur,ait nasser,ait nasseur,ait nebdas,ait nemar,ait nemeur,ait nemmar,ait nisser,ait nmar,ait nouh,ait obeda,ait oichhou,ait olroum,ait omai,ait omar,ait omar bel laiachi,ait omar bel layachi,ait omar ou abid,ait omar ou ali,ait omiar,ait omkhtar,ait onaradam,ait orma,ait otmane,ait ou aimidane,ait ou alach,ait ou ali,ait ou alla,ait ou allal,ait ou astere,ait ou atik,ait ou attik,ait ou attiq,ait ou azad,ait ou azik,ait ou aziz,ait ou ba allal,ait ou balla,ait ou bella,ait ou ben ali,ait ou bohou,ait ou bouzid,ait ou brahim,ait ou cherif,ait ou chrif,ait ou daha,ait ou daoud,ait ou el hassene,ait ou gouali,ait ou guare,ait ou guera,ait ou hormouch,ait ou irane,ait ou lahcen,ait ou lahcene,ait ou lahiane,ait ou lahsene,ait ou lhassen,ait ou mazir,ait ou mribet,ait ou said,ait ou salah,ait ou sous,ait ou yahya,ait ou yaya,ait oulad,ait oua belli,ait ouaaksou,ait ouabdi,ait ouabot,ait ouachaou,ait ouachehou,ait ouachou,ait ouaddar,ait ouadfel,ait ouadouz,ait ouagana,ait ouagenna,ait ouaggoun,ait ouaghitane,ait ouagmar,ait ouagouriane,ait ouaguena,ait ouahei,ait ouahi,ait ouahi ou akki,ait ouaiour,ait ouairri,ait ouajjouf,ait ouakrim,ait oualal,ait oualef,ait oualiad,ait oualil,ait ouallal,ait ouallarh,ait ouallil,ait ouallou,ait oualri,ait oualyad,ait ouanech,ait ouanergui,ait ouanougdal,ait ouanoura,ait ouanurane,ait ouaoumana,ait ouaourik,ait ouarab,ait ouarach,ait ouaradane,ait ouarahe,ait ouarahmane,ait ouaramane,ait ouardane,ait ouarhitane,ait ouari,ait ouaritane,ait ouaskhine,ait ouassern,ait ouatiq,ait ouazarene,ait ouazik,ait ouaziz,ait ouazize,ait ouazzag,ait ouazzal,ait ouazziz,ait oubahsene,ait oubala,ait oubasene,ait ouchahou,ait ouchchane,ait ouchchen,ait ouchchene,ait ouchen,ait ouchene,ait oudimar oufra,ait oudinar,ait ouenla,ait oufad,ait oufella,ait oufkir,ait oufrou,ait oughane,ait oughral,ait oughrar,ait ouglif,ait ougliff,ait ougni,ait ougouni,ait ouhahi,ait ouhalane,ait ouham,ait ouhame,ait ouhdine,ait ouhoumane,ait ouiaaz,ait ouichouane,ait ouiddiren,ait ouiddirene,ait ouiddirn,ait ouiden,ait ouidene,ait ouidir,ait ouidiren,ait ouidren,ait ouidrene,ait ouijjent,ait ouikha,ait ouikhter,ait ouiksene,ait ouirar,ait ouissaden,ait ouissadene,ait ouitfao,ait oujena,ait oukane,ait oukfir,ait oukhdim,ait oukhzam,ait oukkane,ait oukrim tijine,ait oulakine,ait oulhou,ait oulhoul,ait oum tata,ait oumchat,ait oumda,ait oumerezou,ait oumghar,ait oumis,ait oumres,ait oumrhar,ait oumzar,ait ounabil,ait ounrar,ait ounrhi assamer,ait ounsis,ait ououchen,ait ououchene,ait ouqadir,ait ouqqane,ait ourghis,ait ourhain,ait ourhaine,ait ourhal,ait ourhiat,ait ourir,ait ourka,ait ourkha,ait ourrhis,ait ousaih,ait ouska issoumer,ait oussalne,ait oussane,ait ousseh,ait outfi,ait outsouen,ait outsouene,ait ouzaghar,ait ouzarif,ait ouzguenna,ait ouznag,ait ouzzal,ait qassou ou haddou,ait quaazou,ait rabaa,ait rahan,ait rahmoun,ait raho,ait rais aomar,ait rakhou,ait raouna,ait ras,ait razaouane,ait razine,ait rba,ait rbaa,ait reba,ait rebaa,ait reggan,ait reggane,ait rehal,ait rehik,ait relafa,ait rezouane,ait rhamoun,ait rhanem,ait rhazi,ait rhejdine,ait rhemat,ait rhersi,ait rhezouane,ait rhiat,ait ridi,ait rkha,ait roboa,ait rohia,ait roho,ait rouadi,ait rrghane,ait rzine,ait saoumar,ait said,ait said on mancour,ait saad,ait saada,ait saada ksar el kebir,ait saadane,ait sadane,ait sadden,ait saddene,ait saden,ait sadun,ait said,ait said ou abella,ait said ou ali,ait said ou amar,ait said ou hadou,ait said ou heddou,ait said ou mansour,ait said ou zeggane,ait said ouamar,ait sais,ait salah,ait salah imerouane,ait salam,ait salan,ait saoun,ait sassou,ait sawn,ait sba jdida,ait sbaa,ait sbouya,ait seba djedida,ait seba jedida,ait sebbaik,ait sebbait,ait secrit,ait seddour,ait sekri,ait seliman,ait sellam,ait semgane,ait semlalet,ait semlalete,ait semouzi,ait serehane,ait serghane,ait serhouchene,ait sfoul,ait sghir,ait shak,ait siais,ait siass,ait sidi ahmed ou ali,ait sidi ali ou bourk,ait sidi ali ou brahim,ait sidi amar,ait sidi amar ou el hadj,ait sidi bella,ait sidi bou ali,ait sidi bou beker,ait sidi bou moussa,ait sidi bou`li,ait sidi brahim,ait sidi el haj,ait sidi el hajj,ait sidi el rhazi,ait sidi hsain,ait sidi lahcene,ait sidi lahsene,ait sidi lhacen,ait sidi mhamed ou lahcen,ait sidi mohammed,ait sidi moussa ou assou,ait sidi muossa ou aissa,ait sidi said,ait sidi slimane,ait sidi youssef,ait simouri,ait sine,ait sirir,ait skala,ait skouala,ait skri,ait slilo,ait slimane,ait smaaile,ait smail,ait smil,ait smile,ait snag,ait solimane,ait souab,ait soual,ait soudane,ait souk,ait soummane,ait soumouane,ait sous,ait souss,ait srhir,ait stagir,ait susan,ait taa,ait taaa,ait taabit,ait tabbat,ait tachaout,ait taddart,ait taddert,ait tadert,ait tafrant,ait tafrat,ait tafrent,ait tagannt,ait tagant,ait taghzoute,ait tagsa,ait taguella,ait tagunit,ait tahar,ait taharma,ait tahassannt,ait tahri,ait tahria,ait taieb,ait tajar,ait tajer,ait takerrat,ait takoucht,ait taksimt,ait tala,ait talb,ait talb ouissa,ait talbousene,ait talbusen,ait taleb,ait taleb aissa,ait taleb ali,ait taleb brahim,ait taleb mohammed,ait taleb yahya,ait talha ait fenger,ait talkaoucht amzrhal,ait tamajjout,ait tamellil,ait tamga,ait tamgount,ait tamjout,ait tamlil,ait tammennt,ait tamment,ait tanaout,ait tandit,ait tanout,ait taoujdate,ait taounzraft,ait tarabt,ait tarehi,ait tarhia,ait tarzout,ait tastirt,ait tastirte,ait tatlane,ait tayar,ait tebareze,ait tegja,ait teimane,ait temassai,ait temgount,ait temouchene,ait thami,ait thani,ait tibercht,ait tidar,ait tidli,ait tifaout,ait tikert,ait timgount,ait timnit,ait tinrmitine,ait tiouado,ait tiouine,ait tiourhza,ait tislit,ait tiyouwine,ait tizgui,ait tlagelou,ait tlaglou,ait tleha,ait tnim,ait toudart,ait touddert,ait toughach,ait toughsine,ait tougrou,ait touini,ait toukhsina,ait toukhsine,ait touksine nimedrhas,ait toumert,ait toumi,ait tourhach,ait tourhsine,ait tous,ait touzoud,ait trid,ait tsimouri,ait u manus,ait urir,ait warab,ait waddar,ait waggoun,ait wahkou,ait walef,ait wankir,ait wanougdal,ait wardane,ait widir,ait wiksen,ait witfao,ait xaid,ait ya`qoub,ait yacoub,ait yacoub sfraza,ait yadou,ait yafire,ait yahia,ait yahia ou athmane,ait yahia ou atmane,ait yahia ou hasine,ait yahia youssef,ait yahya,ait yahya ou hassine,ait yahya ou moussa,ait yaich,ait yakoub,ait yalla,ait yamzel,ait yaqoub,ait yasa ou ihia,ait yasine,ait yassine,ait yaza,ait yazza,ait yidir,ait ylassain ou hadou,ait youb,ait youguine,ait youl,ait yous,ait youss ou daoud,ait youssef,ait youssef ou ali,ait youssef ou said,ait yout,ait zaakri,ait zaarour,ait zabbas,ait zaddine,ait zagoumas,ait zaid,ait zaim,ait zakra,ait zakri,ait zaokri,ait zarhour,ait zaros,ait zat,ait zatour,ait zdine,ait zdoigue,ait zebzat,ait zeddine,ait zegane,ait zeid,ait zekri,ait zellal,ait zeltene,ait zett,ait zial,ait ziam,ait ziane,ait zirari,ait ziri,ait zitoun,ait zolit-tamakaydout,ait zouli,ait-aranchi,ait-bouchboud,ait-buzum,ait-el-kasa,ait-khalifa,ait-messaoud,ait-naceur,ait-rahhal,ait-sebaa,ait-ziah,aita,aita chaab,aita de mijloc,aita ech chaab,aita el foukhar,aita ez zoutt,aita mare,aita medie,aita mijl,aita seaca,aita zott,aita-mijlocie,aitaf tamani,aitala,aitaman,aitamannikka,aitamannikko,aitania,aitanit,aitanite,aitape,aitar,aitara,aitare,aitaroun,aitate,aitbar khan khosa,aitch,aitchi,aite,aitebar brohi,aitebar khan,aitedjou,aitejokk,aitekoniu,aitekonyay,aitekonys,aiten,aitenbach,aiterap,aiterbach,aiterhofen,aitern,aitersteinering,aith,aithadi,aithaia,aitham,aithar,aithbuk,aithi,aiti,aitia,aitian,aitiancun,aiting,aitingchi,aitingji,aitingli,aitingliao,aitinjo,aitite,aitkenvale,aitkin,aitkova,aitkovo,aitkul,aitnoch,aito,aito calcado,aitolahti,aitolikon,aitomaki,aitomari,aiton,aitona,aitoniemi,aitonovtsy,aitoo,aitos,aitou,aitouli,aitouping,aitoushan,aitout,aitova,aitovo,aitpar,aitpara,aitrach,aitrang,aitsa,aitsaari,aitsra,aitsu,aitta,aittakorpi,aittakoski,aittala,aittaniemi,aittojarvi,aittokoski,aittokyla,aittomaki,aittoniemi,aittopera,aittovaara,aitua,aitue,aitui,aitun,aitung,aituy,aitzarte,aitze,aitzendorf,aitzendorf-hermsdorf,aiua,aiua 1,aiua 2,aiuaba,aiuba,aiuchi,aiud,aiuda,aiudu de sus,aiudul-de-sus,aiuha,aiuman,aiun,aiungel,aiupa,aiura,aiure,aiuruoca,aiuwala,aiuwo,aivado,aivados,aivados e fontes,aivadzhik,aivadzik,aivajik,aivala,aivali,aivalli,aivan-i-kaif,aivaneki,aivarkhandpur,aivatchioi,aivation,aivau,aivawkofe,aive,aivebadina,aivei,aivesuana,aivet,aiviekstes,aivira,aivo,aivosuana,aiwa,aiwade,aiwaku,aiwamba,aiwan,aiwanak,aiwanj,aiwasi,aiwata,aiwawa,aiwe,aiwei,aiweierhkou,aiweit,aiweizi,aiweng,aiwi,aiwo,aiwonden,aiwos,aiwos mission station,aiwranpura,aiwu,aiwuwan,aiwuzi,aix,aix la chapelle condominium,aix-en-diois,aix-en-ergny,aix-en-issart,aix-en-othe,aix-en-provence,aix-la-chapelle,aix-la-fayette,aix-les-bains,aix-noulette,aix-sur-cloix,aixe,aixe-sur-vienna,aixe-sur-vienne,aixheim,aixi,aixi gongshe,aixi nongchang,aixia,aixicun,aixigou,aixinzhuang,aixirivali,aixirivall,aixirvall,aixovall,aiy,aiya,aiya abissa,aiya khan banda,aiyad,aiyah,aiyai,aiyaiya,aiyaiyu,aiyaka,aiyakerni,aiyamonte,aiyamperumal,aiyan,aiyanagar,aiyanatidal,aiyang,aiyangcheng,aiyangpienmen,aiyankenitalawai,aiyankerni,aiyankernitalawai,aiyansh,aiyao,aiyap,aiyar,aiyaru,aiyasi,aiyatayagama,aiyatoro babunde,aiyau,aiyaura,aiyawa,aiyawo,aiyawou,aiye,aiye coker,aiye gu ulu,aiyebam,aiyede,aiyedero,aiyedudu,aiyedun,aiyefemi,aiyegbaju,aiyegbeyo,aiyegun,aiyegun nla,aiyegunle,aiyegunle bunu,aiyehkou,aiyekale,aiyekoto,aiyekou,aiyelaboro,aiyelade,aiyeleso,aiyelet hash shahar,aiyelet hashahar,aiyen,aiyensu,aiyeose,aiyepe,aiyepe olode,aiyepola,aiyerade,aiyereke,aiyereke titun,aiyeri,aiyeroju,aiyerose,aiyerri,aiyeru,aiyesan,aiyesu,aiyete,aiyeteju,aiyetinbo,aiyetoro,aiyetoro egbunbe,aiyetoro gbede,aiyetoro iyawo ogun,aiyetoro kale,aiyetoto-asogun,aiyewawa,aiyezi,aiyiali,aiyialos,aiyiboso,aiyikuma,aiyikwataw,aiyim,aiyimensa,aiyina,aiyinabiri,aiyinabrim,aiyinasi,aiyinion,aiyion,aiyira,aiyird,aiyiribe,aiyiribi,aiyiros,aiyition,aiyittiyamalai,aiylnion,aiyomojok,aiyon,aiyot,aiyota,aiyu,aiyuan,aiyuiv,aiyulities,aiyundep,aiyung,aiyunguru,aiyura,aiyuun,aizabad,aizac,aizal,aizan,aizanville,aizape,aizarna,aizarnazabal,aizaroz,aizawl,aizcorbe,aizdi,aizdzire,aizdzires muiza,aize,aizecourt,aizecourt-le-bas,aizecourt-le-haut,aizecq,aizelles,aizenay,aizhai,aizhai kachkai,aizhaixia,aizhangtongjia,aizhangzi,aizhdan,aizhong,aizhou,aizhuang,aizhukeng,aizhukou,aizi,aizidian,aizier,aizigou,aizihe,aizilou,aiziyu,aizkakles,aizkalne,aizkalni,aizkarkles,aizklani,aizkraukle,aizkraukles muiza,aizkuja,aizkujas muiza,aizoain,aizouzane,aizpe-goicoechea,aizpun,aizpure,aizpures pusmuiza,aizpuri,aizpurucho,aizpurve,aizpurves,aizpurvi,aizpute,aizputes-padure,aizstraunieki,aizstrautnieki,aiztere,aizteri,aizu,aizu-wakamatsu,aizuarsa,aizupe,aizupes,aizupes lamani,aizupes muiza,aizupi,aizupiesi,aizupisi,aizviki,aizviki muizas centrs,aizwal,aizy,aj jabalen,aj janayef,aj jidiane,aj jihar,aj jined,aj jinena,aj jinni,aj juwer,ajapnyak,aj-dogdu,aj-orman,aja,aja donica,aja hacienda,aja kabul,aja obiakpa,aja pepe,ajaa,ajaabek,ajaaw,ajab,ajab garhi,ajab gul banda,ajab ka nagla,ajab ka wajab,ajab khan,ajab talao,ajaba,ajabaj,ajabajani,ajabata,ajabgarh,ajabiat,ajabinkrom,ajabit,ajabo,ajabpur,ajabsagi,ajabshahr,ajabshaqi,ajaburo,ajabwala,ajac,ajaccio,ajacio,ajaconti,ajacuba,ajademadema,ajademan,ajadi,ajadiye,ajadogho,ajaengigol,ajafej,ajafitor,ajag,ajaga,ajaga-ase,ajagambari,ajagara,ajagase,ajagba,ajagbale,ajagbe,ajagbega,ajagbodudu,ajagi,ajagolo,ajagora,ajagual,ajaguayibo,ajagusi,ajaguyibe,ajah,ajaha,ajaho,ajai,ajai mashi,ajaib sumro,ajaibzai,ajaigarh,ajaikope,ajail,ajain,ajaipal ka dungri,ajaiye,ajaj,ajak,ajak bikr,ajaka,ajakaketiewe,ajakamanso,ajakanga,ajake,ajaki tanya,ajakiri,ajakjak,ajakkang,ajako,ajakolo,ajakope,ajakotie,ajakpakope,ajakpleji,ajaku,ajakukuse,ajakukuso,ajakurama,ajal,ajala,ajalar,ajalasse,ajalbera,ajalcapan,ajaldi,ajalia,ajalireng,ajallaleng,ajallalengbaru,ajalle,ajalli,ajalpan,ajaltokrok,ajaltoun,ajalvir,ajam,ajam al qatif,ajam bagan,ajama,ajamabri,ajamagba,ajaman,ajamangoro,ajamarca,ajamaroe,ajamaru,ajamaso,ajamawgba,ajambata,ajami,ajami kolahbuz,ajami quarter,ajamil,ajamobri,ajamogba,ajamogha,ajampur,ajamyeh,ajan,ajan chak,ajan qarah khvajeh,ajan qareh khvajeh,ajan salah,ajan sallakh,ajan sangarli,ajan shahrak,ajan shir meli,ajan shir melli,ajana,ajana iberekodo,ajana igbo ora,ajana sangawun,ajanakpo,ajanaku,ajanani,ajand chupan boneh,ajane,ajanedo,ajang,ajangale,ajangara,ajangbala,ajangboju,ajanggalung,ajangkalung,ajangua,ajani,ajaniti,ajankiti,ajanku,ajanla,ajano,ajanoa,ajanpur,ajanta,ajantudua,ajao,ajaokuta,ajaoluna,ajap,ajapa,ajapanisi,ajapata,ajapi,ajapoma,ajapura,ajar,ajar banda,ajar poetar,ajar sarakole,ajar toucouleur,ajar yaya,ajara,ajara agamaden,ajara kalle,ajaraiahuria,ajaraj,ajaran,ajarband,ajareago,ajarengkam,ajarestan,ajarhja,ajari,ajarif,ajariyan,ajarja,ajarlu,ajarora,ajarostaq,ajarra,ajarte,ajarthala,ajaru,ajarugba,ajas,ajasa,ajasalo,ajasar,ajase,ajashe,ajaso,ajasolo,ajassalo,ajasse,ajasse ipo,ajasso,ajat,ajata,ajata-isieke,ajata-okwuru,ajatian,ajatin,ajatition,ajatito,ajatiton,ajatitor,ajatitun,ajatulu,ajau,ajau nanga,ajau ulu,ajaugo,ajaur,ajaura,ajaureforsen,ajav,ajaver,ajavibe,ajavu,ajavwuni ugbevwe,ajaw,ajawa,ajawbaw,ajawu,ajax,ajaxa,ajay,ajayagad,ajaye,ajayekrom,ajayi,ajayibo,ajayl-e bala,ajayl-e pain,ajaz,ajazlar,ajba,ajbaj,ajbalche,ajbar,ajbej,ajbelj,ajbental,ajbij,ajbisheh,ajchilla,ajd aabrine,ajdabiya,ajdabiyah,ajdacici,ajdagheh,ajdanovici,ajdarevic mahala,ajdarra,ajderi,ajderovac,ajdi,ajdinovici,ajdinovo,ajdir,ajdovec,ajdovscina,ajdt,ajduci,ajducica,ajdukovic brdo,ajdukovica brdo,ajdukovici,aje,aje ibon,ajebach,ajebaj,ajebambo,ajebamibo,ajebamidele,ajebandele,ajebandele nugba,ajebaw,ajebe,ajebo,ajebuengmiran,ajeda,ajede,ajede balogun,ajedeh,ajee,ajeg,ajegbenwa,ajegunel,ajegunle,ajegunle arun,ajegunle awa,ajegunle gbala,ajegunle jankiri,ajegunle market,ajegunle-iwonja,ajegunle-opa,ajegunlere,ajegunwa,ajeh,ajei,ajeiagbo,ajeikrom,ajeja,ajeka,ajeke,ajekimoni,ajekofe,ajekora,ajekoria,ajekpo,ajekwe,ajel,ajel-e bala,ajel-e pain,ajel-i-bala,ajel-i-pain,ajelanwa,ajele,ajelende,ajellidine,ajelmam,ajeltoun,ajemaje,ajematekko,ajemera,ajemeri,ajempra,ajemra,ajena,ajenadawm,ajenakonawpie,ajenasi,ajene,ajengbe,ajenifuja,ajeninsua,ajenkotoku,ajeonri,ajepata,ajer,ajer hitam,ajer koempai,ajer kumpai,ajer madidi,ajer oema,ajer renak,ajer uma,ajera,ajeragu,ajerbangis,ajeregun,ajeri,ajerico,ajerja,ajerkhoula,ajerkotuku,ajerlo,ajerlu,ajermadidih,ajero,ajerogun,ajerpanas,ajershiyeh,ajerterang,ajeruk,ajeruku,ajesa,ajesai,ajeso,ajesokese,ajeta,ajetalanga,ajetola,ajetovic mahala,ajetowa,ajevach,ajgain,ajgaon,ajgara,ajgareh,ajgarhbura,ajghan,ajguejal,ajgule,ajhair,ajhur al kubra,ajhur ar raml,ajhur as -ughra,aji,aji beyk,aji bisheh,aji burayeh,aji chay,aji darreh,aji gasan,aji kahriz,aji sou,aji su,aji yanaqi,ajia,ajia abo,ajia ibrahim,ajia mijiniawa,ajia saboa,ajiacun,ajiakpekope,ajial,ajian,ajiang,ajianso,ajiatu,ajib gado,ajiba,ajibabi,ajibade,ajibaho 1,ajibaho 2,ajibaho dua,ajibaho satu,ajibai,ajibaj,ajibaji,ajibar,ajibarang,ajibata,ajibawde,ajibawo,ajibelang,ajibi,ajibja,ajibo,ajibode,ajibodu,ajibola,ajiboro,ajibowu,ajiboye,ajibu,ajibuhara,ajicata,ajiconal,ajid,ajidah,ajide,ajideh,ajidgeh,ajiditan,ajido,ajidong,ajiefa,ajiere,ajieru,ajies,ajifuwan,ajigasawa,ajigba,ajigbala,ajighan,ajigi,ajigin,ajigo,ajigual,ajigulung,ajihama,ajiho,ajijahe,ajijic,ajijie,ajijo,ajijoy,ajijuit,ajijulu,ajikagungan,ajikemai,ajiki,ajikidaki,ajiko,ajikomkrom,ajikosun,ajikpoto,ajiktong,ajil,ajil ar radhan,ajila,ajilanga,ajilchinii horshoo,ajilete,ajili,ajillete,ajilli,ajilo,ajilonla,ajiltake,ajilu,ajim,ajim khan,ajim qishlaqi,ajima,ajimaje,ajimal,ajimasin,ajimchighra,ajimenti,ajimine,ajimipepai,ajimpoma,ajimum,ajin,ajin do jin,ajin dojin,ajina,ajinaluyani,ajinawpa,ajinembah,ajingel,ajingi,ajingode,ajini,ajinnine,ajino,");nopa,ajinsem,ajinyede,ajio,ajiolo,ajioro,ajios ahilios,ajios andreas,ajios athanasios,ajios nikolaos,ajios paraskevis,ajipa,ajipamanggilan 1,ajipamanggilan 2,ajipamanggilan dua,ajipamanggilan satu,ajipatutu,ajipemanggilan,ajipowo,ajipur,ajipuzi,ajir,ajir amra,ajirabad,ajiranwala,ajire,ajireso,ajiri,ajirigaiin huryee,ajirigaiin-huree,ajirin,ajiringano,ajiriwo,ajirlu,ajiro,ajiroba,ajirohama,ajirri,ajiryang,ajisaiye,ajisawe,ajise,ajisu,ajit ka adda,ajit nagla,ajit purwa,ajita,ajitadun,ajitaladun,ajitapur,ajitata,ajitke,ajitose,ajitpur,ajitpura,ajitsar,ajitwal,ajiwa,ajiwogbo,ajiya,ajiyongu,ajizales,ajizur,ajjampur,ajjao,ajjassam,ajjatabad,ajjelidine,ajjowal,ajjuwala,ajjuwali,ajka,ajkai tanyak,ajkarendek,ajkarendek tanyak,ajkarendeki tanyak,ajkitala,ajkobila,ajkovila,ajlagokofe,ajlahuito,ajlai,ajlas,ajllata,ajlo,ajloud tanirt,ajlu,ajlub,ajlune,ajma`,ajmah,ajmair nagar,ajmal colony,ajmaleh,ajmaniczadulo,ajmatpur,ajmeli,ajmer,ajmer city,ajmera,ajmou,ajmou amajgal,ajmou nait ali ou hasso,ajmou nait messaoud,ajmunds,ajnad,ajnala,ajnan chak,ajnanet,ajnara ka khirk,ajnas,ajneri,ajnevce,ajni,ajnianwala,ajnikolla,ajnikolle,ajnor,ajnora,ajnovce,ajo,ajo cunca,ajob tangi,ajobamaba,ajobo,ajobo isale,ajobom,ajocancha,ajoccasa,ajococha,ajocucho,ajodhiapur,ajodhiapura,ajodhpur,ajodhya,ajodhya bazar,ajoe,ajofrin,ajogun,ajohon,ajoi,ajoi harai,ajoice,ajoich,ajojin,ajojucar,ajok,ajoki,ajoko,ajokojokondre,ajokwe,ajola,ajolia,ajolla,ajolli,ajolmayo,ajoloapan,ajoloco,ajolojo,ajolote,ajom,ajoma,ajomachay,ajombadi,ajome,ajomo,ajomope,ajon,ajona,ajonchi,ajoncourt,ajong,ajonloku,ajono,ajonsari,ajonyi,ajoo,ajopampa,ajopata,ajopita,ajopucro,ajopujro,ajoquentla,ajor band,ajor feshari-ye moqaddas,ajor feshari-ye nowkar,ajor pazi-ye `ali `azizi,ajor pazi-ye amir,ajor pazi-ye bradaran-e kadkhodai,ajor pazi-ye haj hamzeh,ajor pazi-ye haqiqat,ajor pazi-ye naderi,ajor pazi-ye nasr,ajor pazi-ye taraqqi khvah,ajor pazi-ye yas,ajor-e haqiqat,ajora,ajoraku,ajorbast,ajorboneh,ajorlu,ajoro,ajorradero,ajos,ajos catanauan,ajos mulaney,ajoscca,ajoscca hacienda,ajoshuaico,ajosmayo,ajostaipale,ajot,ajotanapata,ajotapampa,ajotejana,ajou,ajouaneb,ajoupa-bouillon,ajoux,ajouyang,ajovwuni ugbevwe,ajowal,ajoya,ajoyane,ajoyani,ajpi,ajqal`ah,ajqan,ajra,ajrab,ajrabad,ajrajpura,ajrajpura ki dhani,ajram,ajram shadezai,ajram sulemanzai,ajran,ajrana kalan,ajrana khurd,ajrani,ajranli,ajranwan da dera,ajrao,ajrata,ajrawar,ajreezi,ajreqsay,ajreshiyeh,ajrestan,ajri,ajriqsay,ajristan,ajrizi,ajrlu,ajrowo,ajrum,ajrwala,ajsam,ajsevica,ajstrup,ajstrup hedehuse,ajstrup strand,ajstruphede,ajt,ajtapa,ajtara,ajti-mohk,ajtos,ajtoshat,aju,ajua,ajuafo,ajuan,ajuanten,ajuba,ajubanso,ajubisu,ajuchitancito,ajuchitlan,ajuchitlan del progreso,ajuchitlancito,ajucolloe,ajucoloe,ajuda,ajudaibo,ajudanabad,ajudaniyeh,ajudante,ajude,ajudhipur,ajue,ajueso,ajuexiong,ajufia,ajugara,ajugitala,ajuini,ajuket,ajukum,ajul,ajule,ajulotong,ajuluco,ajum,ajum menu,ajumadium,ajumain,ajumakese,ajumako,ajumaku,ajumam,ajuman,ajumana,ajumani,ajumanim,ajumawa,ajumo,ajumum,ajumunds,ajun kalle,ajun kili,ajun koruna,ajundak,ajung,ajung kulon,ajung shol,ajung tengah,ajung wetan,ajungajunga,ajunganmangli,ajungbabi,ajungchi,ajungmir,ajungni,ajunguloh,ajunkwaso,ajuno,ajuntas,ajuntenbay,ajuom,ajupange,ajupute,ajura,ajuraja,ajuraja 1,ajuraja 2,ajuraja dua,ajuraja satu,ajurawa,ajuri,ajuria,ajurias,ajuricaba,ajurical,ajuritiba,ajurs,ajursh,ajusan,ajusani,ajusco,ajusiponge,ajusta conta,ajusta-contas,ajusticiado,ajusua,ajut,ajuterique,ajuwon,ajuy,ajuy town,ajuyata,ajuyoccota,ajuz,ajvadzik,ajvaj,ajvali,ajvalija,ajvatovac,ajvatovci,ajvatovici,ajvazi,ajwa,ajwara,ajwas,ajwenso,ajwinasi,ajwir,ajwitechi,ajwo,ajwok,ajya ibrahim,ajyad,ajylu,ajyou,ajyyap,ak,ak baba,ak bash balya,ak bash zir,ak bounar,ak bourhane,ak bulak,ak bunar,ak chechen,ak chin,ak chut vaya,ak dahri,ak dere,ak dere eni makhle,ak gumbaz,ak kaleh,ak kant,ak kharaghan sharghi,ak komelik,ak koyun,ak kuprak,ak kupruk,ak masjid,ak mekhmedi,ak mesjid,ak monai,ak pounar,ak robat,ak sakal koy,ak shehr,ak sheikh,ak su kona shahr,ak su yangi shahr,ak tash,ak yar,ak yokus,akyar,akyulovo,ak wirdet,akabet,akabi seat,akafe,akaki,akaki beseka,akako,akbetsa,akfen,akidra,akirdat,akordat,akori,aksta,akusho ager,akutsa,akyalsurcha,akyayla,ak-altyn,ak-aral,ak-art,ak-bashin,ak-bay,ak-bit,ak-blak,ak-bola,ak-bosyaga,ak-bota,ak-bulag,ak-bulagi,ak-bulak,ak-bulat,ak-buldt-yurt,ak-bulun,ak-bunar,ak-buryu,ak-buyro,ak-buyrya,ak-chaganak,ak-chal,ak-chap,ak-chaya,ak-cherlak,ak-cheva,ak-chilik,ak-chin village,ak-chira,ak-chishma,ak-chiy,ak-chiy-karasu,ak-chulak,ak-dagana,ak-darbent,ak-darshan,ak-dash,ak-dashayak,ak-dosh,ak-dovurak,ak-dun,ak-durug,ak-dyube,ak-dzhakand,ak-dzhal,ak-dzhana,ak-dzhangal,ak-dzhar,ak-dzhol,ak-dzhor,ak-erik,ak-gaudan,ak-guaz-kaaz,ak-ichke,ak-ichkeyskiy,ak-ityk,ak-jar,ak-kadunlar,ak-kara-sug,ak-kaya,ak-kem,ak-kend,ak-kishlak,ak-koby,ak-kochkor,ak-korum,ak-kuchuk,ak-kuduk,ak-kul,ak-kul,ak-kulya,ak-kum,ak-kungur,ak-kupryuk,ak-kurgan,ak-kurgan ishkaly,ak-kutal,ak-kuyu,ak-mamad,ak-mazar,ak-mechet,ak-mechet,ak-mechet-aral,ak-moyun,ak-mula,ak-mulla,ak-muz,ak-oba,ak-olen,ak-olong,ak-osten,ak-otkel,ak-ozek,ak-panar,ak-rabat,ak-sakli,ak-saray,ak-say,ak-shal,ak-shirak,ak-shiy,ak-shyyrak,ak-su,ak-su-kenez,ak-suu,ak-tai-dzhanysh,ak-tailak,ak-tak,ak-tal,ak-tala,ak-tam,ak-tash,ak-tasty,ak-tay-dzhanysh,ak-taylyak,ak-tepa,ak-tepa-mitan,ak-tepe,ak-tepinski,ak-tere,ak-terek,ak-terek-gave,ak-teri,ak-tosto,ak-tovurak,ak-tubs,ak-tubya,ak-tugay,ak-tuly,ak-tumshuk,ak-tumsuk,ak-tun,ak-turgan,ak-turpak,ak-turuk,ak-tuz,ak-tyube,ak-tyubek,ak-tyuz,ak-utkel,ak-uzyak,ak-yal-shurcha,ak-yul,ak-yulova,ak-zava,ak-zhaylyau,ak-zhol,aka,aka agbimel,aka aka,aka akaffoukro,aka bloukro,aka boco,aka dabanga,aka diman,aka ekpeme,aka ekpene,aka elu-lodu,aka eze,aka faqir kala,aka gongshe,aka gouassou,aka khel,aka khel kelay,aka khel pungai,aka kheyl,aka kofikro,aka ndja,aka nguessankro,aka ofot,aka shah heydar,aka soka,aka wini,aka,akaasi,akas,aka-alishams,aka-baba,aka-ikot udo-eno,aka-kouamikro,aka-niida,aka-niita,aka-nitta,akaa,akaahan usadoo toren,akaaiwa,akaaka,akaakariasac,akaan,akaangel,akaatio,akaazar,akab workei,akaba,akaba kabre,akabacabrais,akabakabrais,akabaleu,akabam,akaban,akabanda,akabandele,akabane,akabane-nishiku,akabanemachi,akabas,akabasim,akabat,akabati,akabavi,akabazim,akabe,akabeh,akabenga,akabi,akabido,akabiou,akabira,akabiti,akabli,akabo,akaboky,akabomen,akabos,akabovvosue,akabu,akabuka,akaca,akacaslok,akacasmajor,akacatepe,akacayar,akacdulo,akacha,akache,akachen,akachi,akachiama,akacijas,akacja,akacomekrou,akacomoekrou,akacyjki,akadagoda,akadani,akadasso,akade,akadegoda,akademgorodok,akademia tanyaja,akademia-tanya,akademiafold,akademicheskaya,akademicheskiy,akademicheskiy gorodok,akademija,akademisch neuendorf,akademothy,akademstroy,akadhimi,akadhimoi,akadia,akadiafoue,akadiame,akadigoda,akadimon,akadja,akado,akadomari,akadzhankalay,akae,akaer,akafakir-kala,akafatsa,akaffou kouao,akaffou yapi,akafi,akafou kofi,akafou-ndrikro,akafou-nzikro,akafoukro,akafta,akaga,akagac,akagakoa,akagals,akagami,akagawa,akagbe,akagil,akagina,akagla,akaglakofe,akaglakorpe,akah,akah khel,akaha epu,akahal,akahan,akahana,akahane,akahia,akahil,akahin,akahk,akaho,akahomody,akahufu,akahufu akpanundele,akai,akai ate,akai awu,akai ikot orok,akai udo,akai uso,akaian,akaifou,akaigawa,akaika,akaikai,akaike,akaikin,akain,akainsinyeboko,akaira,akairawala,akairwala,akaishi,akaishicho,akaishimachi,akaisi,akait,akaiwa,akaje,akajen,akajenge,akajhiri,akajima,akak,akak i,akak ii,akak-metom,akaka,akakadoho,akakagbo,akakahonhon,akakai,akakamba,akakangakro,akakankro,akakble,akake,akakele,akakepo,akakhel,akakheyl,akakheyl,akakhil,akaki,akakiai,akakies,akakina,akaknotak,akako,akako mekak,akakofikro,akakomoekro,akakomoekrou,akakoumoekrou,akakoumokro,akakpo,akakpo kope,akakpodje,akakpoe,akakro,akakrou,akaktane,akaku,akakumama,akakura,akal,akal khel,akal ntini,akala,akalabaga,akalabagi,akalacs,akalagamuwa,akalagomuwa,akalak,akalamba,akalan,akalani,akalankoy,akalaran,akalarum,akalaw,akalawa,akalayong,akalchin,akale,akalede,akalfou,akalgarh,akalgarh fort,akalgarh gharbi,akali,akaliabari,akalikondre,akalin,akalkot,akalla,akalli kondre,akalmegh,akalmun,akalo,akalokope,akalokoupodji,akalokul,akalokul point,akalole,akalou koudiokro,akalove,akalovo,akalpur,akalsia,akaltara,akaltyn,akalu,akam,akam bitam,akam i,akam ii,akam iii,akam shahr,akam-messi,akama,akama-oye,akamae,akamah,akamakrom,akamaru,akamasa,akamassuchi,akamat,akamat aj`ud,akamat barkad,akamat hubaysh,akamat mujayl,akamat shaqif,akamatsu,akamatsucho,akamatsumachi,akamber,akambwa,akame,akame kpota,akameku,akamimi,akamine,akamiya,akamizu,akamkorpe,akamo,akamonwa,akamou,akampeng,akams,akamsi-effak,akamso,akamukope,akamuya,akan,akan kelay,akan mbang,akan shukef,akanyarvi,akana,akana i,akana ii,akana-bour,akanaba,akanabor,akanabor-ogude,akanabour,akanabur,akanagar,akananga,akanangbo,akanankasy,akanashi,akanatkhyrgyn,akanbarak,akancha,akanchen,akand,akandang,akandapara,akande,akandi,akandje,akando,akandouchane,akandshiewo,akandshii,akandzhi,akandzhievo,akandzhii,akandzhilari,akandzhiovo,akane,akaneh,akanehama,akang,akang siwah,akanga,akangai,akangala,akangba,akange,akanguissakro,akangula,akani,akani-obio,akania,akania nachhirpur,akania nasirpur,akaniem,akanikha,akanikovo,akanino,akaninskaya,akanjor,akankan mokan,akankasi,akankina,akanko,akankohan,akankpa,akanlahti,akanlao,akanlu,akanmaki,akann,akannakro,akanni,akanni epo,akannu,akano,akanran,akansakro,akansoka,akansoko,akanstaka,akant,akantamansu,akantaniase,akantaniasi,akantanso,akantansu,akantas,akantem,akanten,akanthou,akanti,akantin,akanto,akantschou,akantu,akanu,akanu okpulo,akanu-nchara,akanuma,akanwala,akanyissankro,akanyo,akanza kouadjokro,akanza-kouadiokro,akanzakro,akao,akaogi,akaoka,akaoki,akaouane,akaoudj,akaouini,akaourene,akapa,akapli,akapnou,akapnu,akapo,akapokope,akapoliso,akaporiso,akapotiso,akapovo,akapur,akaputu,akar,akar village,akara,akara khan,akaraba,akarade,akarafo,akaragama,akaragana,akaragbene,akarahadiya,akarahaduwa,akarahediya,akarai-obodo,akarakar,akarakourou,akarakumo,akaral,akaram,akaramian,akaramini,akaranai,akarangaha,akarankom,akaraouane,akarappu,akarate,akaratesi,akarattya,akarawan,akarawatta,akarawatte,akarawita,akarbasi,akarbeyli,akarbulok,akarca,akarcay,akarchay,akarcheh,akard addition,akare,akarekwu,akarekwu kunde,akarekwu kworo,akarekwu nsidi,akarella,akaren,akargaon,akari,akaria,akarim,akarino,akarkaou,akarkaw,akarkour,akarkoy,akarmara,akarmit,akarnovo,akaro,akarokoro kaku,akarokoro point,akarp,akarpati,akarpon,akarshur,akarsu,akartama,akaru,akarukwe,akary,akaryk,akarzaza,akas,akasa,akasaka,akasaka-aoyama-minamicho,akasaka-cho,akasaka-nagano,akasaka-nakano,akasakacho,akasakada,akasakata,akasaki,akasame 1,akasame 2,akasamei,akasamei number 2,akasamei number1,akasan,akasanim,akase,akasekun,akasemswaso,akaserim,akasha,akashakpu,akashambatwa,akashawa,akashevo,akashi,akashicho,akashiki,akashina,akashinden,akashtara,akashur,akasi,akasia,akasia akpo camp,akasjoensuu,akasjokisuu,akaska,akaslompolo,akaso,akasomskaya,akasongo,akassa,akassato,akassinzi,akasso,akastara,akastugan,akasua,akasumi,akaszto,akasztodomb,akasztodulo,akasztofadulo,akasztofahegy,akasztohegy,akasztohegyi szollo,akasztoi tanya,akat,akat amnuai,akata,akata awom,akata raji,akataka,akatani,akatauna,akatawia,akatchan,akatchang,akatebura,akaten,akater char,akaternu,akati,akatichua,akatikana,akatkovo,akatnaya maza,akatnov,akatnovka,akato,akato aveme,akato avoeme,akato viepe,akato vyepe,akatongore,akatore,akatova,akatovka,akatovo,akatoyo,akatpadede,akatri,akatsi,akatsievo,akatsiya,akatsuka,akatsuki,akattikkulam,akattikulam,akattimurippu,akatu,akatui,akatuy,akaty,akatyeva,akatyevo,akau,akau-otar,akaubiri,akauer,akaukrom,akauktaung,akaul,akaurta,akautaucheri,akautokyer,akautsu,akavangu,akavina,akavinas,akavu,akaw,akawa,akawara,akawaram,akawaso,akawdo,akawe,akawi,akawini mission,akawje,akawkaw,akawkawakawkaw,akawkawso,akawli,akawpaki,akawso,akay vtoroy,akay-bulak,akaya,akayakro,akayao,akayaokro,akayevka,akayevo,akaykin,akaymbala,akaytala,akaytalay,akayu,akayuk,akaz qeshlaq,akaza,akazai,akaza`i,akazai,akazaka,akazaki,akazalilie,akazayi,akazebi,akazi,akazo,akazu,akazuka,aka\302\200\302\235yer,aka\302\261nca\302\261lar,aka\302\277a\302\277ngkur,aka\302\277bulaka\302\277,aka\302\277dala,aka\302\277ma\302\277sqit,aka\302\277qi,aka\302\277saka\302\277maral,aka\302\277su,aka\302\277tam,aka\302\277taz,aka\302\277tera\302\277k baziri,aka\302\277tera\302\277k da\302\277ryasi,aka\302\277tu,aka\302\277xopa,aka\302\277ya\302\277r,akba,akba `alikhel,akba safra,akbaba,akbacha,akbag,akbakay,akbakofe,akbakota,akbal,akbaldir,akbali-bala,akbali-pain,akballi,akbalyk,akbandi,akbar,akbar `ali khel,akbar `ali kheyl,akbar `alikhel,akbar abad hotkan,akbar abad kawar,akbar abad yazdan abad,akbar ali khan,akbar davud,akbar davud-e qeshlaqi,akbar din kala,akbar ghanoke,akbar hat,akbar ke kathia,akbar khan,akbar khan banda,akbar khan kalay,akbar khan kalle,akbar khan kelay,akbar khan kili,akbar khan koruna,akbar khan nawa,akbar khan nawah,akbar khan takhti khel,akbar khel,akbar khelanwala,akbar kheyl,akbar punjabi,akbar qobad,akbar saho,akbar sahu,akbar shah,akbar shah jo goth,akbar shah talao,akbar shahwala,akbara,akbarabad,akbarabad bani yekkeh,akbarabad damaneh,akbarabad mahmil,akbarabad pain,akbarabad-e `olya,akbarabad-e baghin,akbarabad-e bala,akbarabad-e barkhowrdar,akbarabad-e barkhvordar,akbarabad-e chah qal`eh,akbarabad-e giv,akbarabad-e gol chehreh,akbarabad-e golchehreh,akbarabad-e gonbagi,akbarabad-e hejri,akbarabad-e herandi,akbarabad-e huji,akbarabad-e kahdan,akbarabad-e kavar,akbarabad-e lashaneh,akbarabad-e nivesht,akbarabad-e oshtorkhan,akbarabad-e pain,akbarabad-e qushchi,akbarabad-e sheshdeh,akbarabad-e shur,akbarabad-e sofla,akbarabad-e toroq,akbaralikheyl,akbardin kala,akbardinkala,akbari,akbarian,akbarisovo,akbariyeh,akbariyeh shahabad,akbarkan,akbarkhan kalay,akbarkhankalay,akbarkhannava,akbarkhel,akbarkheyl,akbarkheyl,akbarkoruna,akbarnagar,akbarobod,akbarpur,akbarpur afghan,akbarpur aunchha,akbarpur sarai ghag,akbarpura,akbarwala,akbas,akbasak,akbasar,akbash,akbasheva,akbashevo,akbashim,akbashly,akbaslar,akbastau,akbasty,akbata,akbate,akbatebe,akbatyrevo,akbaur,akbauyr,akbay,akbayir,akbaytal,akbaz,akbe,akbeansi,akbeim,akbeit,akbekpo,akbel,akbelen,akbelenli,akbenli,akbenliciftligi,akbent,akberabad,akberda,akberdeyevo,akberdina,akberdino,akbeshi,akbeyli,akbez,akbez gazelusagi,akbez nuhusagi,akbez yeniyapan,akbezsalman usagi,akbi,akbidaik,");idak,akbilal,akbilek,akbilyal,akbirakan,akbiyik,akbiyiklar,akbiyikli,akblak,akblokofe,akbodia kondj,akboessoukope,akboga,akbogaz,akbosaga,akbou,akbouch,akbour,akbruta,akbual,akbucak,akbudak,akbugday,akbuk,akbul,akbulagh,akbulak,akbulak xiang,akbulat,akbulatova,akbulatovo,akbulatyurt,akbulgar,akbulgur,akbulma,akbulok,akbulut,akburc,akburun,akburun koyu,akbuta,akbuyo,akbuyra,akbuzau,akbyulova,akbyulovo,akca,akca koyunlu,akcaabat,akcaagil,akcaagil koyu,akcaagilkoy,akcaalan,akcaalan koyu,akcaalanisagir,akcaalankebir,akcaalansagir,akcaali,akcaarmut,akcaavlu,akcabadrik,akcabaglar,akcabayir,akcabel,akcabelen,akcabey,akcabudak,akcabuk,akcaburc,akcacam,akcacay,akcacaykoy,akcadag,akcadam,akcadamar,akcadere,akcagedik,akcagil,akcagoz,akcagoze,akcagun,akcahalil,akcahasan,akcahatipler,akcahisar,akcahuyuk,akcaibaz,akcainis,akcakale,akcakale koyu,akcakamis,akcakamisli,akcakaracali,akcakas,akcakavak,akcakaya,akcakaya koyu,akcakaynak,akcakece,akcakecili,akcakent,akcakentkoyu,akcakepir,akcakertil,akcakese,akcakil,akcakilise,akcakiraz,akcakise,akcakisla,akcakisrak,akcakoca,akcakocali,akcakonak,akcakoy,akcakoyun,akcakoyunlu,akcakusak,akcakuyu,akcakuzu,akcal,akcalan,akcalar,akcali,akcaliusagi,akcam,akcamagara,akcamasat,akcamelik,akcamescit,akcamesut,akcamezra,akcamezraa,akcami,akcami koyu,akcamkiraz,akcan,akcaoba yaylasi,akcaoren,akcaorendami,akcaova,akcapinar,akcapinar koyu,akcapinarkadim,akcapinarkoy,akcar,akcarsar,akcasar,akcasar yaylasi,akcasaz,akcasehir,akcaseke,akcasir,akcasogut,akcasoku,akcasu,akcasu koy,akcasu koyu,akcasusurluk,akcat,akcatarla,akcatas,akcatekir,akcatepe,akcatepekoy,akcati,akcatoprak,akcatumbuk,akcavakif,akcaveran,akcaviran,akcay,akcay koyu,akcayazi,akcaygiritmuhacir koyu,akcayir,akcaykoy,akcayli,akcayol,akcayurt,akce sece,akcedam,akcehisar,akcehuyuk,akcekale,akceltik,akcemen,akcenga,akcenger,akceoren,akcere,akcesme,akceviran,akcevre,akcha,akcha alan,akcha kayryak,akcha-buga,akcha-kalekh,akchaer,akchaganskiy,akchail,akchakale,akchakey,akchakhyuyuk,akchakol,akchakoyunlu,akchakul,akchamezraa,akchar,akchatan,akchatau,akchataul,akchay-ulia,akchayar,akchayl-y,akche,akche ibram,akche ibryam,akche shehr,akcheh,akchelar,akchere,akcherino,akchernskiy,akcheshur,akcheyevo,akchi,akchi-karasu,akchikarasu,akchikasy,akchilar,akchim,akchishma,akchishmi,akchiy,akchiy-aytuar,akchiy-karasu,akchok,akchokka,akchokrak,akchora,akchtim,akchura,akchuragasy,akchurina,akchurinskiy,akchury,akchuvasheva,akcicek,akcift,akcigdem,akcigi,akciki,akcil,akcin,akcinyusuflar,akcira,akciris,akciyan,akcomak,akcorten,akcr,akcukur,akcuri,akcurun,akdag,akdag koyu,akdagina,akdagmadeni,akdakhana,akdala,akdala xiang,akdam,akdam koyu,akdamar,akdamfidanligi,akdamfy-danlygy,akdamla,akdamlar,akdana,akdarya,akdari,akdash,akdavletova,akdavletovo,akdede,akdegirmen,akdemir,akdeniz,akdenktas,akdepe,akdere,akdere koyu,akderebasveren,akderebasviran,akdereisikli,akderekoy,akderekoyu,akderob,akdibek,akdik,akdiken,akdilek,akdim,akdim ait iazza,akdivan,akdiz,akdizgin,akdogan,akdogantekke,akdogmus,akdogu,akdoruk,akdullar,akduman,akdurak,akduran,akdurmus,akduven,akdy,akdzhal,akdzhami,akdzhar,akdzhazyk,akdzhor,ake,ake kakou,ake mam,ake nje,ake sahoe,ake seka,ake ternate,ake toeri,ake turi,ake-befiat,ake-eze,ake-eze ukwu,akeba,akebai,akebefia,akebo,akebora,akebro,akebroe,akeche,akechaungwa,akechi,akechtim,akeciai,akeda,akedabo,akedda,akede,akedim msemrir,akedim nyazza,akedimothy,akedjailolo,akedjodjaroe,akedjodjaru,akedo,akeen,akegyaungwa,akegyi,akeharu,akeholm,akei,akeigun,akeir,akejailolo,akejojaru,akekadai,akeke,akekegbene,akekela,akekerou,akekolano,akekonigbe,akekoua,akekrom,akeksandrovka,akeksinskaya,akekura,akel,akel amalou,akela,akelamo,akelayong,akeld,akele,akeleng,akeley,akeli,akelin,akelo,akelos village,akelsbarg,akelumu,akem,akeman crossroad,akembawi,akembet,akembuala,akemya,aken,akena,akende,akendorf,akener-vorstadt,akenes,akeneset,akengei,akeni,akenkansu number 1,akenkasi,akenkawso,akenlu,akeno,akenobe,akensche vorstadt,akenstaka,akenstakas muiza,akentenkyieso,akenu,akenu pusmuiza,akenyo,akeokoue,akepe,akepe dzogbepime,aker,aker-e sheykh kola,akera,akera chhota,akerama,akerang,akerangen,akerback,akerbergshult,akerblom,akerbouss,akerbranna,akerby,akerd,akere,akered,akerewala,akerfjord,akergba,akerhiev,akerhog,akerhojden,akerholm,akerholmen,akeri,akerianwala,akerjardet,akerkaou,akerkour,akerlund,akerly,akermani,akerme,akern,akernengoule,akero,akerod,akeroen,akeroro,akerouss el fougani,akerouz,akerra,akerre,akerroui,akers,akers acres,akers krutbruk,akers styckebruk,akersande,akersane,akersberg,akersberga,akersholm,akershory,akersjon,akersloot,akersstrom,akerstrom,akersvass,akersville,akert,akerun ajibode,akerun fade,akervik,akervika,akerviken,akerz,akes,akes dhdha,akesa,akesahu,akesakrom,akesater,akeseba,akesegun,akeselaka,akeshina,akeshov,akesimbeka,akeso,akespe,akessan,akesse kakou,akesse nguissankro,akessemossou,akesta,akesu,akesum,aket,aketa,aketchom,akete,akete diabo,aketebor,aketebwo,aketeche,aketechi,aketewia,aketi,aketo,aketohoin,aketola,aketorp,aketse,aketswia,aketu-oja,akety,akeu,akeuhote,akeve,akewa,akewali,akewe,akewon,akey,akeya,akeyan,akeye,akeyu,akeza,akeze,akezi,akezlii,akezuka,akfadou,akfata,akfatma,akgecho,akgecit,akgedik,akgelin,akgevir,akghi-karasu,akgol,akgol yaylasi,akgomlek,akgoz,akgoze,akguller,akgumus,akgun,akguney,akh kand,akh maqayeh,akh mat,akh mirzaani,akh torpakh,akhya,akh-bulakh,akh-kerri,akh-kir-kok,akh-mamadlo,akha,akhachane,akhachevka,akhad,akhadhokastron,akhadi,akhaha,akhai,akhaikon,akhaily-gol,akhaji thakur,akhal,akhal-sakobo,akhal-terdzhola,akhalbediseuli,akhalbediseull,akhalchi,akhaldaba,akhaldabo,akhaldoba,akhalgavazi,akhalgori,akhali abastumani,akhali akvaskia,akhali atoni,akhali bedissuli,akhali burguli,akhali gavazi,akhali kadoyeti,akhali khaishi,akhali khibula,akhali kindghi,akhali marabda,akhali samgori,akhali sviri,akhali terzhola,akhali terjola,akhali ulianovka,akhali-afoni,akhali-gagra,akhali-khulgumo,akhali-khurvaleti,akhali-kindgi,akhali-lalalo,akhali-mamudlo,akhali-nichbisi,akhali-osebi,akhali-pantiani,akhali-terdzhola,akhali-zirbiti,akhalia,akhalig,akhalik,akhaliki,akhalipara,akhalisa,akhalisopeli,akhaliy,akhalkalaki,akhalkakhati,akhalkalak,akhalkhevi,akhalkhibula,akhallouf,akhalsenaki,akhalsheni,akhalsopeli,akhalsopili,akhalsopeli,akhalsopeli bolshiye,akhalsopeli malyye,akhaltsikhe,akhaltsikh,akhaltskaro,akhaltsykha,akhaltsykhe,akhaltsykhskogo,akhalubani,akhamenoll,akhamilla,akhamsi,akhan,akhand,akhanfulad,akhangan,akhangar,akhangaran,akhangari,akhangarka,akhangaron,akhangaru,akhangul,akhangun,akhank,akhankoy,akhanwala,akhapsi,akhar,akhar badin,akhar-e jahan,akhara,akharakwala,akharan,akhardzha,akhari,akharim,akharja kharaweh,akharjani,akharjapet,akharlu,akharnai,akharpara,akharun,akhasan,akhasheni,akhasri,akhat-baba,akhatani,akhatar,akhateli,akhatl,akhatla,akhatlar kebir,akhatlar segair,akhatli,akhatly,akhatzikia,akhaura,akhavaniyeh,akhaveyem,akhaveym,akhavnadzor,akhavnatukh,akhavnatun,akhawat razanah,akhaybeysin khure,akhayly-gol,akhayotar,akhazai,akhazir,akhbalou,akhbar khan takhti khel,akhbarg,akhbash,akhberah,akhbrak,akhburane,akhca,akhcha,akhcha kand,akhcha-kuyma,akhcha-tepe,akhchachane,akhchadepe,akhchaguyma,akhchah,akhcheh,akhchi,akhchi-karasu,akhchilar,akhdlik,akhdzev,akhegwo,akheh `abbasi,akheilia,akheirleina,akhekynnu,akhelfiyine,akhelia,akhelinadha,akhellouf,akhelo-makhmudlo,akheloy,akhelsopeli,akhen sar,akhendachou,akhendachou nait ouffi,akhendria,akhendrias,akher zamane,akheran,akherdi,akheri,akherousia,akherti,akheya,akhezai tangai,akhfamane,akhfamou,akhferga,akhfrga,akhfurdi,akhi jahan,akhi jan,akhi kamal,akhia,akhianwala,akhiar,akhidovka,akhieli,akhievo,akhijahan,akhijan,akhil,akhilteli,akhillio,akhillion,akhillion palace,akhillo,akhilu,akhin,akhinets,akhingar,akhingaran,akhingarani-pain,akhingaru,akhingarubanda,akhinos,akhintuma,akhiny,akhiok,akhiolo,akhipur,akhir rangah,akhir zaman,akhir-e jizah,akhir-i-jeza,akhira,akhirapara,akhirokhorion,akhiron,akhirones,akhirranaga,akhisar,akhisilla,akhissar,akhitarishen,akhiyeli,akhizian,akhizyan,akhji,akhjir,akhkerpi,akhkak,akhkala,akhkarzai dag,akhkend,akhkent,akhkhol,akhkhoy,akhki-yurt,akhkikhli,akhkikhlu,akhkilisa,akhkinchu-barze,akhkinchu-barzoy,akhkinchu-borzoy,akhl,akhl breigha,akhl burayqah,akhla,akhladeri,akhladha,akhladhai,akhladhaki,akhladhe,akhladhea,akhladheai,akhladheri,akhladhes,akhladhi,akhladhia,akhladhiakies,akhladhias,akhladhini,akhladhion,akhladhokambos,akhladhokastron,akhladhokhori,akhladhokhorion,akhladhomilea,akhladhomilia,akhladhomilies,akhladokampo,akhlal,akhlamad,akhlamad-e bala,akhlamad-e-olya,akhlan,akhlan tirich,akhlanovo,akhlar,akhlatyan,akhlatian,akhlatlii,akhlebaikha,akhlebinine,akhlebinino,akhlebinovka,akhley,akhli,akhlii,akhlij,akhliy,akhlumad,akhlupy,akhlystino,akhma din,akhmach,akhmachevo,akhmad,akhmad-shakh-kotanay,akhmadkandi,akhmadkhan,akhmadzhi,akhmak,akhmalakhti,akhmametovo,akhmametyevo,akhman,akhmanei,akhmaney,akhmaneyevo,akhmanova,akhmanovo,akhmanskiy,akhmany,akhmaqieyh,akhmasikha,akhmat,akhmatener,akhmatka,akhmatlar,akhmatov,akhmatovka,akhmatovo,akhmatovo-belyy klyuch,akhmazkend,akhmazukalay,akhmed,akhmed dere,akhmed slakhi,akhmed-gerada,akhmed-oba,akhmedabad,akhmedagaly,akhmedak,akhmedalar,akhmedallar,akhmedalylar,akhmedavar,akhmeday,akhmedbek,akhmedbeyli,akhmedbubi,akhmeddzhankala,akhmeddzhankhan-kala,akhmedgulkalay,akhmedh,akhmedi,akhmedikalay,akhmedkala,akhmedkala-kalay,akhmedkalay,akhmedkanmena,akhmedkent,akhmedkhan,akhmedkhankala,akhmedkhazi,akhmedkheyl,akhmedkheyl,akhmedlar,akhmedli,akhmedlii,akhmedly,akhmedoba,akhmedshakhkheyl,akhmedshkola,akhmedzai,akhmedzi,akhmedzukalay,akhmeh qayah,akhmen,akhmerka,akhmerova,akhmerovo,akhmet,akhmet-kaya,akhmet-zhana,akhmeta,akhmetabad,akhmetau,akhmetaul,akhmetchi,akhmetevo,akhmeti,akhmetka,akhmetkara,akhmetley,akhmetli,akhmetov,akhmetovo,akhmetovskaya,akhmetovskiy,akhmetshi,akhmety,akhmetyevo,akhmetyevo-tatarskoye,akhmetzhan,akhmim,akhmirovo,akhmitcha,akhmolichi,akhmun,akhmuzi,akhmylovo,akhna,akhnada,akhnaou,akhnay,akhnazar,akhnidzor,akhnig,akhnoda,akhnud,akhnur,akhnyk,akho,akhobe,akhoca,akhokas,akhola,akholgah,akholi,akhomavron,akhomavros,akhond gholami,akhond kheyl,akhondan,akhongaru-kala,akhonk,akhonka,akhonvaara,akhoond,akhopelto,akhor,akhor ban,akhor vakhsh,akhora,akhorak,akhoreh olya,akhori,akhorjani,akhoshali,akhoumatou,akhouria,akhouria ayiou nikolaou,akhownd,akhowndan,akhowr,akhowr badin,akhowr bedin,akhowr khash,akhowrak,akhpat,akhpay,akhpayevka,akhperka,akhpola,akhpradzor,akhpyurak,akhra,akhra sal,akhrail,akhrat,akhrazly,akhremkin,akhremovtsy,akhremtsey,akhrena,akhrenlii,akhriala,akhris,akhrisi,akhromei,akhromovo,akhruk,akhruto,akhryan eredzhek,akhryan yurpek,akhryane,akhryansko,akhs,akhsa,akhsakadamakhi,akhsakhlar,akhsanit kare deshe,akhsanya,akhsaran,akhsardzhin,akhsargina,akhsarisar,akhsau,akhsay,akhshaani,akhshai,akhsham,akhsham chal,akhshani,akhshay,akhshnisvelebi,akhshtyr,akhsibara,akhsu,akhtala,akhta,akhta khana,akhta khaneh,akhta-chekan,akhta-khana,akhtab,akhtach,akhtachi,akhtachi-shaumyan,akhtachi-sovet,akhtachikan,akhtachy,akhtachy mugan,akhtachy shaumyan,akhtachy shirvan,akhtachy-karabudzhak,akhtaci,akhtadzh,akhtah khanah,akhtah-khanah,akhtaikha,akhtaj,akhtaji,akhtak,akhtakhana,akhtala,akhtam,akhtama,akhtamai,akhtamand,akhtan,akhtanak,akhtanizovskaya,akhtanizovskiy,akhtar,akhtar colony,akhtar dad,akhtar dherai,akhtar gata,akhtar khel,akhtar khelo kelay,akhtar kheyl,akhtar kili,akhtar mohammad doray,akhtar mohammad dowray,akhtar mohammad kalay,akhtar mohammad khan,akhtar mohammadkhan kalay,akhtar mohammed kalay,akhtar mohmmad khan kalay,akhtar moqadam,akhtar muhammad doray,akhtar muhammad kelay,akhtar muhammad khan,akhtar muhammad khan kelay,akhtar muhammad kili,akhtar nika ziarat,akhtar shah,akhtar-mukhammed-khan-kalay,akhtar-mukhammedkhan,akhtarabad,akhtarai,akhtaray,akhtarchi,akhtardad,akhtareine,akhtargol kalay,akhtargul kalay,akhtargul kelay,akhtari,akhtarin,akhtarkhel,akhtarkhelo kalay,akhtarkheyl kalay,akhtarkheyl,akhtarkheylu-kalay,akhtarmohammadkhan,akhtarmukhammed-doray,akhtarmukhammed-kalay,akhtarmukhammed-khan,akhtaro kalay,akhtaro kelay,akhtarskiy,akhtarukalay,akhtarwal,akhtarzai,akhtarzai kili,akhtarzi,akhtash-ovliya,akhtavan,akhtay,akhtebol,akhteh chi,akhteh khaneh,akhteh khvan,akhtehchi,akhtehkhaneh,akhtekhan,akhtenskiy,akhteraki bolo,akhterek,akhterin,akhtetar,akhti,akhtiar,akhtini,akhtinskiy,akhtiran,akhtirka,akhtirskaya,akhtiyal,akhtji,akhtma,akhtmar,akhtme,akhto khel,akhtokheyl,akhtopol,akhtovaya,akhtovo,akhtow kheyl,akhtu khel,akhtu kheyl,akhtuba,akhtuba-bugor,akhtubinka,akhtubinsk,akhtubinskiy,akhtugan-sola,akhtukhel,akhtukheyl,akhtum,akhtur,akhtur bazar,akhty,akhtyar,akhtypeylyk,akhtyr,akhtyrka,akhtyrskaya,akhtyrskiy,akhtyube,akhu,akhuaa,akhuadzha,akhuakhuari,akhubati,akhuchina-kalay,akhud,akhula,akhula-ye `asgarabad,akhuleh,akhulu,akhum,akhun baba,akhun baba ziarat,akhun jan,akhun kalai,akhun kili,akhun kot,akhunabad no. 1,akhunabad no. 2,akhunabad no. 3,akhunabad no. 4,akhunabad no. 5,akhunabad number five,akhunabad number four,akhunabad number one,akhunabad number three,akhunabad number two,akhunapalu,akhunbabayev,akhunbandi,akhund,akhund baba wenah,akhund baba wona,akhund baba wunah,akhund badosh,akhund bzovan,akhund darah,akhund gholami,akhund kalay,akhund karez,akhund kelay,akhund khel,akhund mahalleh,akhund melk,akhund patay,akhund qeshlaq,akhund saheb kalay,akhund sahib,akhund sahib kelay,akhund talao,akhund zada,akhund zada karez,akhund zadah,akhund zadah karez,akhund zadahha,akhund zadeh saheb kalay,akhundan,akhundan banda,akhundan kalle,akhundani,akhundano kili,akhundbaba-vuna,akhundi,akhundkalay,akhundkhel,akhundkheyl,akhundkheyl,akhundly,akhundon,akhundov,akhundovka,akhundpatay,akhundsakhib-kalay,akhundvadakheyl,akhundzad-kala,akhundzada,akhundzada kala,akhundzada koday,akhundzada-kala,akhundzada-kalay,akhundzada-kuday,akhundzada-sakhibkalay,akhundzada-shela,akhundzadagan,akhundzadah,akhundzadah dzay,akhundzadah jay,akhundzadah kala,akhundzadah kalay,akhundzadah kelay,akhundzadah khel,akhundzadah koday,akhundzadah qol,akhundzadah saheb kalay,akhundzadah sahib kelay,akhundzadah sela,akhundzadah shaylah,akhundzadah-kandwala,akhundzadahgan,akhundzadahha,akhundzadahkhel,akhundzadakhel,akhundzadakheyl,akhundzadasar,akhundzadeh,akhundzadeh saehb kalay,akhundzadeh saheb kalay,akhundzadhqol,akhundzadkul,akhung kalay,akhunly,akhunova,akhunovo,akhuny,akhunzada,akhunzada-kalay,akhunzadadzhay,akhunzadagan,akhunzadakheyl,akhur koy,akhura,akhurah,akhurak,akhureh bala,akhureh pain,akhuri,akhurik,akhurito,akhurlu,akhuryan,akhuseyin,akhushkov,akhuti,akhuts,akhutsa,akhuyuk,akhuzi,akhvadzha,akhven-lambi,akhvenlampi,akhvenselkya,akhvenvaara,akhvenvara,akhverdi,akhverdili,akhvond,akhvond baba,akhvond baba wonah,akhvond gholami,akhvond kalay,akhvond kheyl,akhvond patay,akhvond saheb kalay,akhvond zadeh kariz,akhvond zadeh khel,akhvond zadeh saheb kalay,akhvond zadeh saher kalay,akhvondan,akhvondkhel,akhvondkheyl,akhvondzadegan,akhvondzadeh,akhvondzadeh jay,akhvondzadeh kala,akhvondzadeh kalay,akhvondzadeh kandwalah,akhvondzadeh khel,akhvondzadeh kheyl,akhvondzadeh koday,akhvondzadeh saheb kalay,akhvondzadeh shelah,akhvondzadehha,akhvondzadehkheyl,akhvondzadehqowl,akhvor badin,akhvor bedin,akhvor kola,akhvor sar,akhvor-e pain,akhvorak,akhvord,akhvore-ye pain,akhvoreh,akhvoreh pain,akhvoreh-ye badjan,akhvoreh-ye bala,akhvoreh-ye pain,akhvorli,akhvorrash,akhwara,akhwarah,akhyan,akhyan-e bozorg,akhyan-e kuchek,akhyritou,akhysa,akhyvaa,akhzarabad,akhzhidara,aki,aki machhi,aki-ary,aki-aryl,aki-bulak,aki-yurt,aki-yurtovskiy,akia,akiachak,akiadal,akiado,akiafe,akiafe hohoue,akiajiba,akiak,akial,akian,akiana,akiar,akiara,akiata,akiau,akib,akiba,akibetsu,akichi,akichkin pochinok,akidaboe,akie-kru,akiegun,akiekrou,akiele,akieni,akietekiri,akiewe,akif gor,akifbey,akifiye,akifu,akigbogun,akighe nlam,akighil,akighilam,akigname,akigsungek,akii,akija,akije,akik,akika,akikan,akikat,akike,akiki,akikya,akil,akil hakro,akil iomak,akil khvajeh,akil shah,akila,akilagun,akilaiya,akilapa,akilcalman,akile,akilengui,akileyeva,akili,akili-ozizor,akilibu,akilili,akilimalagy,akilla,akilli,akilo,akilok,akilov,akilova,akilovka,akilovo,akilpur,akilvan,akilyata,akim,akim abad,akim akim,akim kosoma,akim makosa,akim nsese,akim oda,akim swedru,akim wenchi,akim-sergeyevka,akima,akimakim,akiman,akimanik,akimari,akimari 1,akimari 2,akimari number 1,akimari number 2,akimasusukera,akimaya,akimbatova,akimboso,akime,akimenki,akimfoo,akimfu,akimli,akimo,akimo-annenka,akimo-anninskoye,akimoto,akimouyaokro,akimovka,akimovo,akimovshchina,akimovskiy,akimovskoye,akimowka,akin,akin jan,akin junction,akina,akinabad,akinabad-e jadid,akinabad-e qadim,akinaitsat,akinajile,akinajo,akinana,akinari,akinatsiait,akinayile,akinbami,akinbawla,akinbiyi,akinbo,akinbo ade,akinbode,akinbola,akinbunmi,akincak,akinci,akincilar,akincir,akinda,akinde,akindele,akindibuwa,akindos,akindrano ambany,akindulkino,akine,akinebitom,akineyon,akinfenou,akinfenwa,akinfevo,akinfivevo,akinfiyevo,aking,akinganio,akingayile,akingbala,akingbile,akingbogun,akingboju,akingbola,akingbolu,akinia,akinikha,akinima,akinino,akinino-baklashi,akinis,akinishno,akinitom,akinjali,akinkemi,akinkhovo,akinkhovskaya,akinkino,akinkovo,akinkunmi,akinlade,akinlalu,akinlar,akinlegbe,akinliyi,akinlolu,akinloye,akinmarin,akinmorin,akinnitsy,akinntou mankagne numero 1,akinnu,akinode,akinoglu,akinogun,akinokami,akinola,akinome,akinpelu,akinrinlo,akinropo,akins,akins chapel,akins corner,akins mill,akinsan,akinsawe,akinshino,akinsipe,akinskaya polyana,akinsola,akinsville,akintaro,akintawla,akintayo,akintobi,akintolu,akintonwa,akintou,akintoya,akintunde,akintyevo,akinum,akinwale,akinwan,akinwande,akinware,akinwunmi,akinya,akinyale,akinyele,akinyide,akinyolu,akinze,akio,akiode,akiogbologbo,akior,akipelai,akipinar,akiqsserniak,akir,akiral,akiri,akiri-ogidi,akiriboto,akiriboto elede,akirika obu,akirikanda,akirkeby,akirtma,akiruno,akis,akisa,akisato,akish,akishenki,akisheva,akishevo,akishevskaya,akishevskiy,akishi,akishima,akishin,akishino,akishke,akishli,akishovo,akisik,akisim,akisimie i,akiska,akiss,akissi,akista,akit kili,akita,akita-dantai,akita-shi,akitan,akite,akitekyi,akiti-ogidi,akitikofe,akitikpa,akitio,akitipa,akitiponpo,akitoye,akitsi,akitsu,akiuli,akiuwa,akividu,akiwali,akiwok,akiybe,akiyoshi,akiz,akiz qeshlaq,akiz qeshlaqi,akiz ulaq,akiza,akizai,akizay,akizuki,akizukimachi,akjar,akjij,akjoujt,akjout,akka,akka aguirene,akka centre,akka ighen,akka iguiren,akka iguirene,akka iren,akka irene,akka irhane,akkabak,akkabawi,akkad,akkada,akkadanlar,akkain,akkaiou,akkajange,akkala,akkamar,akkamari,akkammapettai,akkamysh,akkana,akkanburluk,akkani,akkanskiy,akkanwala,akkanwali khurd,akkapchigay,akkar,akkara,akkara panaha,akkarachnovo,akkaragoda,akkaraipattu,akkaraivadi,akkarawatta,akkaraweli,akkarawelliya,akkarayankotuwa,akkare,akkarfjord,akkarga,akkarga\302\261,akkarginskiy,akkarjorda,akkarsjorda,akkarvik,akkas yaylasi,akkaslar,akkastugan,akkavaara,akkavak,akkavare,akkaya,akkaya koy,akkaya koyu,akkayacatak,akkayacatakkoy,akkayis yaylasi,akkaynak,akkaynar,akkayrak,akkaytym,akkebir,akkeci,akkecili,akkel mahmud munsir kandi,akkelmammud munsir kandi,akkelmamud munsir kandi,akkelpur,akkem,akkemer,akkemir,akkense,akkent,akkent tatil sitesi,akker,akkerewala,akkerfjord,akkerfjorden,akkerhoek,akkerhoofd,akkerman,akkermanka,akkermanovka,akkermanovskiy,akkermanskiy,akkermen,akkermenka,akkerpic,akkerput,akkerstraat,akkerup,akkerveke,akkervik,akkerwoude,akkese,akkeshi,akkesi,akki,akki-yurt,akkia,akkihebbal,akkijarvi,akkil,akkilic,akkilise,akkilise koyu,akkina,akkinlar,akkiran,akkiraz,akkireyevo,akkirik,akkirpi,akkise,akkishlak,akkisi,akkisiler,akkisla,akkitangesoe,akkivaram,akkiztogay,akko,akkoba,akkobang,akkobekh,akkoc,akkoca,akkocali,akkodzha,akkol,akkolskiy,akkola,akkolovo,akkoltyk,akkonak,akkooi,akkopru,akkoprukoy,akkor,akkor daya,akkor laok,akkor tengah,akkora,akkosan,akkose,akkoshkar,akkoum,akkovanlik,akkoy,akkoyun,akkoyunlu,akkozak,akkozinskiy,akkozyaylasi,akkrum,akkstau,akkuatkan,akkube,akkucha,akkudah,akkuduk,akkudukskiy,akkui,akkul,akkul,akkulkystau,akkulskiy,akkul-bigeney,akkula,akkulaksayskiy,akkulari,akkulayevo,akkulek,akkulevo,akkum,akkumulam,akkumulyam,akkuranai,akkurgan,akkurum,akkus,akkush,akkushuk,akkusinskiy,akkutek,akkuter,akkutyr,akkuvaram,akkuyu,akkuyu atom santrali,akkuyu koyu,akkuyu yaylasi,akkuyubitak,akkuyukey,akkuyukoy,akkuzevo,akkuzino,akkuzlu,akkuzova,akkuzovo,akkuzulu,akkystau,akkyungey,akkyya,akkyztogay,akkzu,akla,akla rud,akla tar,akla thal,akladjenou,aklaisciems,aklaistsiems,aklakbanu,aklakou,aklakou etchavi,aklakou molokou,aklakougan,aklakpanu,aklaku,aklaloukou,aklamadaw,aklamador number 2,aklame,aklampa,aklan,akland,aklangberget,aklanpa,aklar,aklas,aklas zaregai,aklavik,aklavika,aklaya,akle,akleh,akleime,aklekrou,akleme,aklenz,aklera,akli,akli ki dhani,akli mamay,akliai,aklibwekope,akligbe korpe,akligbekope,aklije,aklil,aklim,akliniai,aklipuszta,aklirun,aklivan,akllap,aklo,aklo-come,akloba,aklofudzi,aklos,aklou gbangbokro,aklou kouadiokro,aklui,akluj,aklushi,aklvidu,aklyay,akmacici,akmadyy,akmadzici,akmagara,akmagara koyu,akmal,akmal dhok,akmal-abad,akmalah,akmalinka,akmalinovka,akmalysh,akmamyk,akman,akmanastir,akmanay,akmaneshti,akmangit,akmanjali,akmanlar,akmarijp,akmarka,akmarsbruk,akmasat,akmasdzhid,akmat shaqif,akmatovka,akmaya,akmazar,akmazdam,akmaziki,akmazinki,akmazor,akmazutlar,akmechet,akmechet,akmechetnayman,akmekhmedkioy,akmelez,akmena,akmenai,akmenaiciai,akmenaji,akmenales,akmenaychyay,akmendziras,akmene,akmeneliai,akmeneliu,akmenes,akmenes muiza,akmeni,akmeniai,akmenio,akmenishki,akmenishkyay,akmeniske,akmeniskiai,akmeniu,akmenmuiza,akmenruci,akmenyay,akmenynai,akmenyne,akmeqit,akmesat,akmescit,akmese,akmeshat,akmeshed,akmeshit,akmezar,akmezraa,akmi,akmil,akmimana,akmola,akmoldy,akmolinsk,akmolinskiy,akmonay,akmonishken,akmuin,akmukan,akmulinskiy,akmulla,akmuo,akmurum,akmurun,akmurza,akmusa,akmyane,akmyanishkyay,akmyanyalyay,akna,aknada,aknady,aknaghbyur,aknai,aknakhodzha,aknalich,aknapur,aknashen,aknaszlatina,aknazarovo,aknazirova,akne,akneli,akner,akneyevo,aknib,aknioun,akniste,aknoda,aknoul,aknum,aknyazevo,aknyet timur,ako,ako tetchi,akoosi,ako-name,akoabas,akoakas,akoakoko,akoaloui,akoanso,akoansow,akoase,akoasi,akoassi,akoatala,akoatoukro,akoba,akobabaky,akobale,akobar,akobi,akobie,akobkan,akobo,akobo post,akoboissue,akobola,akocak,akochi,akochi yapi,akochio,akochtim,akochukosai,akoda,akodaakrom,akode,akode seva,akodebakou,akodehao,akodekrom,akodesea,akodesewa,akodessea,akodessewa,akodha,akodi,akodia,akodja,akodo,akodu,akodudu,akodum,akoe,akoelitatajeprati,akoelitatojeprati,akoeman,akofa,akoforo,akofu,akog-avop,akoga,akogbe,akoghe,akoglan,akogun,akohegy,akohia,akohia yiti,akoich,akoikrou,akoilawa,akoin,akoirori,akoite,akoja,akojaga,akojan,akoje,akoju,akok,akok bikele,akok bikoe,akok fadiut,akok maka,akok yebekolo,akoka,akokam,akokan,akoke,akoki,akoko,akoko camp,akokoa,akokoaso,akokobenumsu,akokodem,akokofe,akokofori,akokogiri,akokokro,akokoma,akokoma lagos,akokoma teima,akokome,akokora,akokoro,akokouro,akokpak,akokrou,akokua,akokufang,akokwa,akokwasa,akol,akola,akolang,akolanpera,akolda,akoldulo,akole,akole korpe,akolea plantation estates,akolekope,akolhat,akolhati puszta,akoli,akoli kope,akoli nta,akoli-imenyi,akolia,akoliakro,akolin,akolinem,akoliya,akolmajor,akolner,akolo,akoloch,akolochi,akolodong,akolodongo,akolovo,akoluk,akoluk koyu,akoluk mahallesi,akolupu,akolzin,akom,akom bong,akom essakotam,akom ii,akom-bikak,akoma,akomada,akomangan,akomassuchite,akomato,akomba,akombe,akombi,akombinien,akombo,akombu,akome,akome i,akome ii,akomendibi,akomesing,akometam,akomfodi,akominekope,akomkada,akomlangan,akomnyada i,akomnyada ii,akomnyina,akomnyma,akomo,akomo atte,akomolinga,akomomboua,akomommboua,akomou,akomus,akomutoko,akomvoo,akon,akona,akonabou,akonadem,akonana,akonangi,akonangui,akonba,akonbuo,akonchi,akonda kondji,akondion,akondje,akondjo,akondo,akondok,akondonyou,akondro,akondronisidy,akondrotarana,akondrovaza,akonebe,akonekie,akonetchie,akonetye,akonferi,akonfode,akonfudi,akonfuri,akong,akonga,akongba,akongkur,akongo,akongo iii,akongomit,akongu,akonhoum,akoni,akoniaba,akonilen,akonkawso,akonko,akonkwe,akonkyi,akonmaa,akono,akonoboue,akonolinga,akonotangan,akonpohja,akonsonbo,akontamu,akontanim,akontanse,akontansi,akontio,akontion,akonto,akonus,akonwe,akoo,akoon,akop,akopaka,akopaki,akope,akopin,akopo,akopulaka,akor,akor banda,akor khel,akor tegdawich,akora,akora khattak,akora purwa,akorabo,akorabu kukua,akorafo,akorambala,akoraso,akoraweng,akore,akoreky,akorem,akoren,akoren mahallesi,akoren yaylasi,akorencarsak,akorenjong,akorenkisla,akorensokuler,akoreyen,akorha,akorhi,akori,akoria,akoris,akorisi,akork,akorkhel,akorkheyl,akorley kwatse,akoro,akorodu,akoroforo,akoromyebra,akorriyen,akort,akoru,akorugbene,akorukoyu,akorwen,akorwet,akosa,akosai,akosaihasakotsutzuchihhsien,akosaikasakotsu tzuchihhsien,akosaikasakotsutzuchihhsien,akosakira,akosfalva,akosi,akosnice,akoso,akosombo,akosomo,akosovo,akossa,akossi,akossi gbouesso,akossikope,akossikro,akossiom,akosso kohora,akostom-mikueme,akosu,akosua,akosuakroa,akosuchen,akosuchiucheng,akosuhsien,akosuhsincheng,akosulaocheng,akosuman,akosumu,akot,akotaa,akotadai,akotamu,akotao,akotaocheng,akotasokou,akotatzu,akote,akotea,akotechiaso,akotem,akothiya,akoti,akotiakro,akotiantra,akotipa,akoto,akotoasi,akotoasubiente,akotochi,akotochir,akotogbo,akotokyere,akotom,akotopesa,akotopeso number 2,akotoshi,akotoshih,akotoso,akotosu,akototime,akotsu,akotuagua,akotuagwa,akotuako,akotuaku,akotue settlements,akotui,akotushih,akotyna,akotyua,akou,akou toue nguessankro,akou yapo,akoua,akoua yapi,akouaba,akouadio,akouado,akouadoukro,akouaka,akouakiza,akouakro,akoualebango,akouaoui,akouassanou,akouassikro,akouata,akouatano,akouavienou,akouawi,akoubane,akouble,akouchtine,akouda,akoudamane,akoudetiangui,akoudzin,akoue agban,akoue kouadiokro,akoue ndenou,akoue sante,akoueboue,akouedo,akouedo attie,akouekpe,akouen,akouessa,akouessi,akouessou,akough,akougoubilye,akoui,akouiakro,akoul,akoulazork,akouliao,akoulinem,akoulma,akoulouzok,akoum,akoumahou,akoumanou,akoumape,akoumar,akoumassi,akoumbou,akoumbouro,akoumenai,akoumenay,akoumenaye,akoumia,akoumiakro,akoumokoumo,akoumou,akoumouno,akoumounou,akoumsana,akounadougou,akounda,akounde,akoundel,akoundetiandi,akoundoukro,akoungou,akounilem,akounilen,akounou i,akounougbe,akounsane,akountam,akouo,akoupe,akour,akour-kok,akoura,akoure,akourenam,akouridis,akouridiss,akourki,akourma,akourman,akourmane,akourmar,akournam,akourname,akourousoulba,akourso,akoursos,akouse,akousika,akoussika,akouta,akoutagba,akoutaosse,akoutchobli,akouti,akoutia,akoutiandi,akoutsave,akov,akova,akova yaylasi,akoviebo,akovief,akoviepe,akovitika,akovo,akovos,akovyepe,akoweit,akowenkuerh,akowide,akowit,akowo,akowonjo,akowrkhel,akoya,akoyakro,akoyan,akoyate,akoye ndenou,akoyebo,akoyo,akoyrori,akoza,akozai,akozai kili,akozek,akozkebir,akozsagir,akozu,akpa,akpa abatin,akpa uton,akpa utong,akpab okon ene ita,akpabigbene,akpabio,akpabo i,akpabo ii,akpacha,akpachi,akpadaho,akpadanou,akpaekwe camp,akpafia,akpafu,akpagbene,akpagedde,akpagher,akpahonou,akpaidem,akpainkiri,akpaita,akpaji,akpak,akpaka,akpaka sagada,akpakikope,akpakiri,akpakope,akpakourou,akpakpa,akpakpavi korpe,akpakpavikope,akpaku,akpakudu,akpakume,akpala,akpalegakofe,akpali,akpalodo,akpaloukope,akpam,akpama,akpame,akpamu,akpan,akpana,akpandue,akpangne,akpaniya,akpante,akpanundele,akpanya,akpao,akpap,akpar,akpara,akpara ika,akparabong,akparak,akparan,akpararuni,akpare,akparemogbene,akpas,akpasi,akpasip,akpassang,akpassi,akpata,akpatakpa,akpatakpaba,akpatebe,akpatoufoue,akpatre,akpatre efe,akpatuema,akpatyrevo,akpave,akpavou,akpayak,akpayer,akpazar,akpe oje,akpea,akpedegbene,akpeel,akpegedde,akpeghel,akpegher,akpeho-dokpo,akpekou,akpela,akpele,akpelit,akpelu,akpeme,akpenembel,akper,akpera igben,akperhe,akperhere,akpesse,akpessekro,akpetiche,akpetki,akpetky,akpeto,akpia,akpila,akpilyal,akpinar,akpinar koyu,akpinar mahallesi,akpinar yaylasi,akpinarbeleni,akpinarciftligi,akpinarkoy,akpinarkoyu,akpinarli,akpinhiadji,akpla albo,akplabanya,akplagasou kope,akplagassoukope,akpliglui,akplolowogboe,akplorfudzi,akplot,akpo,akpo-ogwu,akpobeleiowei,akpobo,akpobome,akpobome waterside,akpochi,akpode,akpoga,akpoga-ndiagu,akpogbene,akpoha-igbo,akpohoa igbo,akpoissou,akpokli,akpoko,akpokoa,akpokofe,akpokorpe,akpokro,akpoman,akpome,akponaja,akponte,akpopo,akporkploe,akporku,akporo,akporo sogho,akposo camp,akpoto,akpotoenu,akpotoenu new town,akpotonou,akpouati,akpoue kouassikro,akpouessekro,akpouta kope,akpoyghor,akproabo,akptse,akpu,akpughul,akpugo,akpugo-abo,akpugu,akpuibo,akpuka,akpum,akpunabo,akpuneje,akpununu,akpur,akpurtyough,akpuru,akpuwunu,akpwak,akpwelu,akqi,akqulari,akra,akra namdar khan,akraak,akrab,akraba,akrabad,akrabai kili,akrabat,akraberg,akrabi,akrabon,akrabon ahentsia,akrabon kwa tete,akrabong,akrabong joma,akrabout,akrabskii,akrabskii plemsovkhoz,akrabyrgi,akrach,akrad al baqqarah,akrad dasiniyah,akrad dayasinah,akrad dayasne,akrad dayasnih,akrad ibrahim,akradio,akradjin,akrahamn,akrai,akraie,akraifnion,akrak,akrake,akrakro,akrakrom,akrala,akram,akram banda,akram kach,akram kalay,akram kelay,akram khan,akram khan kalay,akram khan kelay,akram khan kili,akram khan koruna,akram khel,akramabad,akramabad-e kianpur,akramah,akramaman,akraman,akramang,akraminag,akramiyeh,akramkheyl,akramoman,akramovo,akrampa,akramwala,akran,akranea,akranes,akrani,akrankakro,akranwala,akrap,akrappu,akrar,akrarinkro,akrarna,akraro,akras,akrashtice,akrasion,akrassikro,akrata,akratich,akrawala,akrawali,akre,akre nguessandon,akrebi,akreby,akredasov,akrehamn,akrehavn,akreiba,akrek,akrereb,akres,akresi,akresse,akressi,akretia,akrezi,akri,akri ugidi,akri-ogidi,akria,akrianu,akricheh,akridge,akridis,akridou,akridu,akrifoukro,akrini,akrino,akrinon,akrio,akriony,akritas,akritokhori,akritokhorion,akriwala,akro,akroa,akroaba akoudjekoa,akroaba beniekoa,akroaba biessiekro,akroabo,akrobadzi,akrobawnia,akrobi,akrobonsu,akrobonya,akrobosu,akrochere,akrochire,akrodie,akrofonso,akrofro,akrofrom,akrofrum,akrofu,akrofu agove,akrofufu,akroful,akrofuom,akroikondre,akroken,akrokeri,akrokerri,akrokro,akrokyere,akrolimni,akrolimnion,akrom,akroma,akromabila number 1,akromabila number 2,akromabra,akromanto,akromaso,akromeyevo,akromiabra,akromiopla,akron,akron junction,akronua anafuo,akronwe,akronwi,akroont,akropon,akropong,akropong-akwapim,akropotamia,akropotamos,akropotamou,akroso,akrosouvala,akrotiri,akrotirion,akrou,akrouakrou,akroubangbokro,akroufla,akroufla dyar amassine,akroufra,akroukro,akroum,akrounda,akrounta,akrovo,akrovouni,akrovounion,akrowi,akroyialion,akru,akru wahan,akrubi,akrubutue,akrubutui,akrudwoa,akrujua,akrum,akruma,akrund,akrunda,akrunt,akrunta,akruofo,akrur,akrus,akruso,akruso south west,akrusu betri,akruti,akry,akrysh,aks an nasir,aksa,aksa-kul,aksa-rovat,aksaagac,aksaat,aksacli,aksaet,aksagac,aksahap,aksahrinc,aksahur,aksai,aksaitovo,aksajarvi,aksak,aksakal,aksakalata,aksakali,aksakallar,aksakili,aksakkoy,aksaklar,aksakli,aksakmaral,aksakov,aksakovka,aksakovo,aksakovshchina,aksakovskoye,aksakowo,aksakshur,aksalar,aksalur,aksamentovo,aksamitovo,aksamovo,aksan,aksanggol,aksanovo,aksanshur,aksapur,aksar,aksaraf,aksaray,aksarayskiy,aksarben mobile home park,aksarguc,aksarikha,aksarina,aksarino,aksarka,aksarkino,aksarnic,aksarovo,aksary,aksaryak,aksas,aksashur,aksaur,aksay,aksay kazak,aksay pervyy,aksayarvi,aksayev,aksayevo,aksayskaya,aksaz,akse,aksehir,aksek,akseki,aksekoy,aksel,akselendi,akselholm,akselmeyevo,akselskiy vyselok,akselskiye vyselki,akselwal,akselwalle,aksemsettin,aksen,aksenchikovo,aksenenki,aksengir,akseni,aksenikha,aksenino,aksenki,aksenkino,aksenkovichi,aksenkovo,akseno-butyrki,aksenov,aksenova,aksenovka,aksenovo,aksenovo-lugovaya,aksenovo-lugovoye,aksenovo-zilovskoye,aksenovshchina,aksenovsk,aksenovskaya,aksenovskiy,aksenovskiy pochinok,aksenovskoye,aksenovtsy,aksenovy,aksentis,aksentseva,aksentsevo,aksentva,aksentyevitsa,aksentyevka,aksentyevo,aksentyevskaya,akseny,akserke,akserova,akset,akseyev-krym,akseyh,aksha,akshaganak,akshakh,akshakol,akshal,akshala,akshanga,akshatau,akshatauasty,akshatautsy,akshchevo,aksheikh,akshenas,akshenas-koshkarevo,akshenas-koshkarovka,akshenas-trekhsvyatka,aksheykh,akshi,akshibay,akshiganak,akshiki,akshikur,akshilik,akshimanskiy,akshimovo,akshimrau,akshinka,akshiy,akshiyrak,akshkol,akshoky,akshontovo,akshor,akshov,akshuak,akshuat,akshubino,akshujavri,akshukur,akshukyr,akshyganak,akshynka,akshynyrau,aksi,aksibara,aksicim,aksidanga,aksigin,aksiguri,aksiki,aksilou,aksilu,aksim,aksimine,aksimn,aksin,aksinikha,aksinino,aksinir,aksinkino,aksinskaya,aksiutowo,aksmanice,aksnes,aksogan,aksogut,aksoku,akson,aksor,aksoran,aksorguc,aksorke,aksou,aksovo,aksoy,aksoyarvi,aksoylar,akspoel,aksri,akstafa,akstafal,akste,akstinai,aksty,akstynyay,aksu,aksu koy,aksu koyu,aksu new city,aksu novyy,aksu old city,aksu xiang,aksu-ayuly,aksuat,aksuat-tyubinskiy,aksuatskiy,aksubayevo,aksukoy,aksular,aksultan,aksulu,aksum,aksumbe,aksume,aksumla,aksungur,aksurka,aksuskiy,aksut,aksutekke,aksutlu,aksuvatli,aksuyek,aksuyskaya,aksy-barlyk,aksyan,aksyrak,aksyrov,aksyukhnovo,aksyumbe,aksyunina,aksyurukonrat,aksyutina,aksyutino,aksyutovka,aksyutovo,akta,akta-khana,akta-say,aktab,aktaban,aktachi,aktag,aktagucha,aktaguchi,aktagunchi,aktai,aktaikha,aktaion,aktakla,aktakyr,aktal,aktala,aktam,aktan,aktanysh,aktapa,aktar,aktarkheyl,aktarla,aktarma,aktas,aktas koy,aktas koyu,aktas vtoroy,aktasa,aktasastar,aktasbulak,aktash,aktash-aukh,aktash-aul,aktash-karlan,aktasheva,aktashevo,aktashevskiy,aktashi,aktashka,aktashkasy,aktaskoy,aktaskoyu,aktaskurtlar,aktastekke,aktasty,aktau,aktauk,aktauskiy,aktay,aktaylak,aktaysay,aktayuzh,aktaz,aktchar,aktebe,aktefek,aktekke,aktel,aktemyr,aktena,aktenburg,aktepa,aktepa-chigatay,aktepe,aktepe koyu,aktepek,aktepekey,aktepekoyu,akterek,akterine,aktero,aktersk,akti,aktil,aktimu,aktinovo,aktiubinsk,aktiv,aktivnyy,akto,aktobe,aktogan,aktogay,aktonayevo,aktoprak,aktornak,aktounda,aktrundzadakh-sakheb-kalay,aktryk,aktseberg,aktsyabr,aktsyabrski,aktsyay,aktuba,aktubek,aktubinsk,aktuganova,aktuganovo,aktugansola,aktuglu,aktuidu,aktukova,aktukovo,aktulga,aktuluk,aktuma,aktumsyk,aktur,aktushevo,aktushi,aktushki,aktussay,aktutan,aktutun,aktuyesay,aktuzla,aktuzlamemlahasi,aktuzlamemlehasi,aktygashevo,aktykan,aktyma,aktyme,aktynovo,aktysay,aktyuba,aktyube,aktyubek,aktyubeyevka,aktyubinka,aktyubinsk,aktyubinskiy,aku,aku ihube,aku jan,aku lhee,aku town,akua,akuai chai,akuaidian,akuakrom,akuamande,akuamase,akuamasi,akuamu,akuapem,akuarchapan,akuarikai,akuase akwasiso,akuaskyamtsa,akuavi,akuba,akuban,akubetsu,akubilila,akubpur,akubrefa,akubu,akuce,akuchi,akuchino,akuda,akudfune,akudfunu,akudibashevo,akudo,akudome,akudu,akueimiao,akueitu,akuekpara,akuekrom,akuenebele,akufo,akufu,akugbe,akugbene,akugdlek,akugdlit,akughe,akuich,akuikame,akuio late,akujarvi,akukan,akuke,akukome,akuku,akuku agbor,akuku-agbo,akuku-akumazi,akukuma,akukuwaterra,akukwa,akukwaferra,akukwaterra,akul,akul katishi,akulkstav,akula,akulaga,akulchet,akule,akulekpio,akulen,akulet,akulevo,akulhube,akuliarusek,akuliaruseq,akulichi,akulichi pervyye,akulichi vtoroyye,akulichi vtoryye,akulikha,akulinina,akulinino,akulinki,akulinkino,akulino,akulinovka,akulinskoye,akulintsy,akulis,akulisy,akulla,akulliit,akullsjon,akulmasa,akulobe,akulova,akulova gora,akulovka,akulovo,akulovo moskva,akulovskaya,akulovskiy,akulovskiy uchastok,akulovskiye vyselki,akulovskoye,akulshet,akulshino,akultsevo,akulurak,akuly,akulynivka,akum,akuma,akumadan,akumadu,akumak,akumal,akumani,akumanikope,akumaso,akumawale,akumaye,akumazi,akumba,akumbu,akumbul,akumdipe,akume,akumesu,akumsuk,akumu,akumvikro,akumza janme,akun,akuna,akunadiwela,akunakuna,akunaq,akunba,akunda,akundaw,akundawi,akunduo,akundzhii,akuneang,akunfori,akung,akungba,akungwe,akunjasrere,akunk,akunk,akunka,akunkogo,akunkongo,akunkongu,akunlepon,akunlu,akunnaaq,akunnu,akunor camp,akunoura,akunouramachi,akunoyi,akunpalli,akunso,akuntak,akuntam,akuntanasi,akuntanim,akuntso,akuntum,akunuba,akunube,akunui,akunvula,akunwun,akunzan,akunzan kworo,akunzan obangari,akuo,akuodo,akuokrom,akuokurom,akupe,akupoku,akupu,akur,akur takur,akura,akuraba,akurabadze,akuraigb,akurala,akuraladuwa,akuramboda,akurambodwatta,akuran,akurana,akuranaoratiyawa,akuranayagama,akuranmulla,akuranooratiyawa,akuratiya,akurawa,akuray,akurdet,akure,akureiskoye,akureliya,akurenam,akurenan,akurengba,akurensok,akuressa,akurey,akureyri,akureyskoye,akuri,akurkwa,akurnam,akuro,akuroforomu,akurofoso,akurofoso mampon,akurofrom,akuropon,akurso,akurtis,akuruba,akurugegangoda,akurugoda,akurukaduwa,akurukalawita,akurumulla,akurumullegedara,akurungu,akurwa,akury,akurya,akusa,akusagi,akusame,akusapesta,akusar,akuse,akusha,akushali,akushinskiy,akushkino,akusho,akusten,akusu,akusuansa,akusuno,akuta,akutami,akutan,akutase medie,akutawia,akute,akute-oja,akutiasi,akutikha,akuto,akutreso,akutsa,akutsima,akutsu,akutuabo,akutuase,akutuasi,akutukpa,akuva,akuve,akuvikro,akuy,akuy nizhniy,akuy sredniy,akuya,akuyan,akuyeh,akuyevskiy,akuyli,akuyorin,akuza,akuzo,akva,akvacha,akvacharkhu,akvag,akvamasya,akvanis,akvara,akvarashi,akvarchapan,akvarchapani,akvarn,akvaske,akvaskemtsa,akvaskia,akveduk,akveran,akveran kilcik,akverani,akveranikurbilyildiz,akveranikurbiyildiz,akverankaradalak,akverankurbuyildiz,akveren,akveren yaylasi,akvik,akviligjuaq,akviran,akviran kilcik,akviran yaylasi,akviranhartavi,akviranheciban,akvirankisla,akvirankislakoy,akvirankurbiyildiz,akwa,akwa ibami,akwa ikot efanga,akwa obio,akwa-ekim,akwa-etiti,akwabawso,akwabo,akwaboa,akwaboso,akwabuaso,akwacha,akwachi,akwada,akwadoudje,akwadum,akwaduo,akwaeze,akwaga,akwai,akwaidori,akwain,akwairmam,akwaiya,akwaj,akwaja,akwaji,akwajo,akwak,akwakrom,akwakru,akwakwa,akwal,akwam nasir,akwaman,akwamase,akwamasi,akwamu,akwamufie,akwan,akwana,akwanga,akwangi,akwani,akwanjakrom,akwanje,akwansakoko,akwansakokodo,akwansakokro,akwansirem,akwanta,akwantambra,akwantombra,akwantrakrom,akwanu,akwanwala,akwaoso,akwapim,akwar,akwara,akwarata agbaja,akwariri,akwarra,akwasa,akwaseho,akwashi,akwasi akwasiso,akwasiase number 1,akwasijikrom,akwasu,akwasujikrom,akwat,akwatia,akwatin,akwato,akwatta,akwe,akwe korpe,akwe-chai,akwebe,akwechi,akweenyanga,akwefre,akwei,akweiman,akweje,akwekope,akwekrom,akweku,akwen,akwenno,akwer nhom,akwero,akwesiase,akwesiasi i,akwesiasi ii,akwesiasi number 2,akwesijankrom,akweta kpegan,akwete,akweteman,akweti,akwetsi,akwetto,akweyi,akwi,akwia,akwiam,akwida,akwidaa,akwidaa beebianeha,akwiemkom,akwiji,akwike,akwiya,akwizgran,akwo,akwo babianiba,akwoda babianiha,akwolakwoy,akwom,akwon,akwong,akwongdair,akwongdau,akwongrail,akwongyik,akwoni,akwonse,akworo,akwot,akwu,akwu chudere,akwukwu,akwukwu-akumazi,akwukwu-igbo,akwun,akwungwasi,akwuyo,akxokesay,akxopa,akxopa xiang,akxullu,akya,akya-in,akyab,akyagwin,akyaka,akyal,akyamac,akyangba,akyapi,akyaprak,akyar,akyarimircik,akyarlar,akyarma,akyat-in,akyauk,akyaw,akyawkhi,akyawkurom,akyayik,akyayla,akyaz,akyazi,akydra,akye,akyeansa nnobesu,akyease,akyeasewa,akyekadin,akyekyere,akyele,akyem,akyem abenaso,akyem akropon,akyem krobo,akyem wamkyi,akyem-awenare,akyemfo,akyemfu,akyemis,akyempem,akyena,akyenmu,akyer,akyerensua,akyereu,akyeriau,akyiano,akyiase,akyiban,akyieso,akyildiz,akyimkrom,akyinakurom,akyinim,akylbay,akyndzhi,akynia,akyokus,akyokuskavagi,akyol,akyolac,akyoma,akyr,akyr-tyube,akyrap,akyrtobe,akyshly,akystau,akyukyusu,akyumak,akyumbaz,akyunlu,akyurek,akyurt,akyuruk,akyuz,akywesi,akywesi htonbweywa,akyya,akzai,akzhaik,akzhal,akzhan,akzhana,akzhanovka,akzhar,akzharma,akzharovka,akzharskiy,akzharskoye,akzharyk,akzhaylau,akzhigit,akzhol,akzigitovo,akzimen,akzimene,akziyaret,akzygitovo,al,al abed,al amamra,al asayif,al assaniyah,al ayach,al azaib,al ilwasi,al isaniyin,al issah,al ujrush,al youn,al `aid,al `aishah,al `aba,al `ababid,al `abadiliyah,al `abadiyah,al `abadlah,al `abakiyah,al `abbadah,al `abbadiyah,al `abbas,al `abbasah,al `abbasah al gharbiyah,al `abbasah ash sharqiyah,al `abbasi,al `abbasiyah,al `abbasiyah al jadidah,al `abbasiyat,al `abbasiyya,al `abbaysah,al `abbud,al `abbudah,al `abbudiyah,al `abd allah,al `abdadilah,al `abdah,al `abdaliyah,al `abdin,al `abdini,al `abdiyah,al `abdiyin,al `abid,al `abidiyah,al `abis,al `ablah,al `ablas,al `abr,al `abrah,al `abraq,al `absi,al `abtan al khuza`i,al `abtaniyah,al `abtiyah,al `adaimah,al `adaf,al `adalah,al `adam,al `adamah,al `adan,al `adanah,al `adasah,al `adasiyah,al `adawi,al `adayimah,al `adayn,al `adbah,al `addusiyah,al `adfah,al `adhab,al `adhabah,al `adhar,al `adharib,al `adhbah,al `adhfayn,al `adhiriyah,al `adhr,al `adhra,al `adi,al `adiah,al `adidiyah,al `adiliyah,al `adimah,al `adin,al `adiyah,al `adl,al `adlah,al `adliyah,al `adliyah wa kafr sulayman ghali,al `adm,al `adyah,al `afadirah,al `afadiyah,al `affad,al `affah,al `affaj,al `afif,al `afifah,al `afinah,al `afish,al `afs,al `afsh,al `afus,al `ahirah,al `ain,al `ain al bayda,al `airan,al `aitim,al `aiwah,al `ajaiz,al `ajaizah,al `ajal,al `ajalah,al `ajaman,al `ajami,al `ajamiyin,al `ajar,al `ajaylat,al `ajaymah,al `ajiz,al `ajjabat,al `ajjamiyah,al `ajradi,al `ajram,al `ajran,al `ajrawi,al `ajrumiyah,al `ajuzah,al `ajuzayn,al `ajz,al `akar al kabir,al `akarah,al `akarit,al `akl,al `akr,al `akrak,al `akrishah,al `akshah,al `akur,al `al,al `ala,al `ala `arw,al `ala,al `alali,al `alam,al `alamah,al `alamayn,al `alamiyah,al `alandayah,al `alaq,al `alaqah,al `alaqimah,al `alawah,al `alawi,al `alawinah,al `alayah,al `alayyah,al `ali,al `alili,al `alim,al `aliyah,al `allamiyah,al `allanah,al `allaqi,al `allayyah,al `allush,al `alqamah,al `aluk,al `alwiyah,al `alyan,al `amaidah,al `amair,al `amadiyah,al `amakir,al `amal,al `amamirah,al `amaqah,al `amaqi,al `amar,al `amar al kubra,al `amarah,al `amarin,al `amarinah,al `amariyah,al `amashah,al `amaydah,al `amayir,al `amid,al `amin,al `amir,al `amirah,al `amiri,al `amiriyah,al `amiriyya,al `amiyah,al `ammah,al `ammaj,al `ammar,al `ammari,al `ammariya,al `ammariyah,al `ammuri,al `amq,al `amqiyah,al `amr,al `amrah,al `amriyah,al `amrusiyah,al `amud,al `amudah,al `amudiyah,al `amya,al `amya,al `ana fasa,al `anabir,al `anad,al `anafisah,al `anat,al `anazah,al `anbari,al `anbariyah,al `anbariyin,al `anin,al `anjarah,al `ankawi,al `ankushi,al `annazah,al `anni,al `ansibah,al `antari,al `anz,al `aqair,al `aqabah,al `aqabah al kabirah,al `aqabah as saghirah,al `aqabih,al `aqar,al `aqar al kabir,al `aqari al bahriyah,al `aqarib,al `aqaylah,al `aqda,al `aqil,al `aqimah,al `aqiq,al `aqir,al `aqiyah,al `aqlan,al `aqlin,al `aqliyah,al `aqm,al `aqmah,al `aqqad,al `aqr,al `aqrabiya,al `aqrabiyah,al `aqrama,al `aqud,al `aqulah,al `aqur,al `aqurah,al `aquriyah,al `ar,al `ar`ar,al `araish,al `araishiyah al jadidah,al `araba al bahriyah,al `arabadi,al `arabah,al `aradeiba jazirah,al `arajib,al `araki,al `aram,al `aramah,al `araq,al `araqi,al `arashi,al `arbadi,al `arbaniyah,al `ard,al `arfa,al `arfajiyah,al `arid,al `aridah,al `aridiyah,al `arif,al `arim,al `arimah,al `arin,al `arin al bahri,al `arin qibli,al `arine,al `aris,al `arish,al `arishah,al `ariz,al `arja,al `arjayn,al `ark,al `arm,al `arq,al `arqan,al `arqub,al `arsamah,al `arshah,al `arshiyah,al `arud,al `arudah,al `aruq,al `arus,al `arusah,al `asaid,al `asaf,al `asafirah,al `asakirah,al `asal,al `asalah,al `asalitah,al `asaliyah,al `asamah,al `asansar,al `asaqah,al `asasif,al `asayid,al `asbah,al `asdaf,al `asf,al `ashar,al `asharah,al `asharinah,al `ashash,al `ashawan,al `ashayr,al `ashini,al `ashiq,al `ashshah,al `ashshar,al `ashshi,al `ashush,al `asi,al `asibat,al `asilah,al `asimiyah,al `asirat,al `asiyat,al `askar,al `askariyah,al `asluji,al `asm,al `asriyah,al `assaf,al `assafiyah,al `assah,al `assarah,al `ata,al `ataij,al `ataminah,al `atanah,al `atarah,al `atawah al bahriyah,al `atawah al qibliyah,al `atawi,al `atawna,al `ataya,al `ateefah,al `atf,al `atfah,al `atiyat,al `atm,al `atnah,al `atnih,al `atrun,al `atshanah,al `attar,al `attara,al `attarah,al `atyan,al `auja,al `awaisah,al `awabi,al `awabil,al `awad,al `awadat,al `awadi,al `awadiliyah,al `awadin,al `awafi ash sharqiyah,al `awafin,al `awah,al `awaj,al `awajiyah,al `awajiz,al `awali,al `awamir,al `awamir bahri,al `awamir qibli,al `awamiyah,al `awamrah,al `awanah,al `awaqin,al `awarid,al `awarid bani az zayla,al `awasijah,al `awatib,al `awawinah,al `awaynah,al `awaynat,al `awazim,al `awd,al `awdah,al `awfi,al `awhi,al `awja,al `awjah,al `awjan,al `awn,al `awran,al `aws,al `awsajiyah,al `awshah,al `awshaziyah,al `awwad,al `awwamah,al `awwamiyah,al `ayaishah,al `ayanah,al `ayayishah,al `ayayyah,al `aydah,al `ayfah,al `ayid,al `ayinah,al `aylafun,al `aylah,al `ayli,al `aymah,al `ayn,al `ayn al bayda,al `ayn al ghamur,al `ayna,al `aynah,al `aynayn,al `ayr,al `ayriyah,al `ayrun,al `ays,al `aysh,al `ayshah,al `ayshiyah,al `aysim,al `aytaliyah,al `ayyadiyah,al `ayyarah,al `ayyash,al `ayyash al gharbi,al `ayyash ash sharqi,al `ayyat,al `ayyuf,al `ayzariyah,al `azaizah,al `azab,al `azamiyah,al `azaq,al `azaqah,al `azariyah,al `azayzah,al `azazah,al `azazi,al `azazinah,al `azib,al `azibah,al `azimah (nimrah khamsah),al `azir,al `aziz,al `azizah,al `aziziya,al `aziziyah,al `aziziyat,al `azr,al `azuniyah,al `azziyah,al `azzuniyah,al `eizariya,al `erq,al `ibadi,al `ibediyya,al `ibr,al `ibri,al `id,al `iddah,al `idhaq,al `ididi,al `idiz,al `idwah,al `ifri,al `ikrimah,al `ikrishi,al `illiyah,al `ilwiyah,al `imad,al `imara,al `imarah,al `imariyah,al `imayyid,al `immah,al `inab,al `inab as saghirah,al `ind,al `iqal al bahri,al `iqal al qibli,al `iqr,al `iqrimah,al `iraq,al `iraqiyah,al `irfan,al `irjayn,al `irq,al `irqah,al `irrah,al `irshan,al `is,al `isa,al `isam,al `isawi,al `isawiyah,al `isawiyah sharq,al `ishash,al `ishsh,al `isiyan,al `itfah,al `itmaniyah,al `iyan,al `iyaynah,al `izab,al `izbah al bahriyah,al `izbah al bayda,al `izbah al bayda,al `izbah al hamra,al `izbah al jadidah,al `izbah al mustajiddah,al `izbah wa al `arab,al `izi\302\247ami,al `izz,al `izzah,al `izziyah,al `izziyat,al `omarab,al `oqair,al `oqdeh,al `orosh,al `uarayja al gharbiyah,al `ubab,al `ubal,al `ubayd,al `ubaydi,al `ubaydiyah,al `ubayja`,al `ubaylah,al `ubayyat,al `ubrayh,al `ud,al `udain,al `udamah,al `udar,al `udayd,al `udaydat,al `udayliyah,al `udayn,al `udaysat,al `udayyat,al `ufaynah,al `ufayrah,al `ujaylat,al `ujaymah,al `uk,al `ukayrishi,al `ukaysh,al `ukayshah,al `ukdah,al `ula,al `ulaybah,al `ulayin,al `ulaymiyah,al `ulayqah,al `ulayqat,al `ulayya,al `ulayya,al `ulayyah,al `ulayyaniyah,al `ulbah,al `ullayqah,al `ulliyah,al `ulmaniya,al `ulu,al `ulub,al `ulwiyah,al `ulya,al `ulyah,al `ulyaniyah,al `umani,al `umaniyah,al `umari,al `umariyah,al `umariyah bani musa,al `umayshah,al `umda,al `umdah,al `umdan,al `umqah,al `umran,al `umraniyah,al `umur,al `umyan,al `unjurah,al `unnabiyah,al `uqab,al `uqari,al `uqariyah,al `uqaybah,al `uqaydah,al `uqayl,al `uqaylah,al `uqaylah kaf,al `uqaylat,al `uqayliyah,al `uqaymah,al `uqayr,al `uqbiyin,al `uqdah,al `uqlah,al `uqlah al bahriyah,al `uqm,al `uqmayrah,al `uqud,al `uqul,al `urayd,al `uraydiyah,al `urayja,al `urayja al wusta,al `uraymah,al `urayq,al `urayqat,al `urayqib,al `uraysah,al `uraysh,al `uraysimah,al `uraysiyah,al `urban,al `urbaniyah,al `urf,al `urfan,al `urj,al `urqub,al `urr,al `urs,al `ursh,al `ursiyah,al `urubah,al `uruq,al `uryan,al `usaylah,al `usayyif,al `usfuriyah,al `usharah,al `ushash,al `ushayr,al `ushsh,al `ushwah,al `usluji,al `usrah,al `usraji,al `utaybah,al `utaybiyah,al `utayfi,al `utaykah,al `utayr,al `utayyat,al `utayyiq,al `uthman,al `uthmaniyah,al `utriya,al `utub,al `uwainid,al `uwayd,al `uwaydah,al `uwaydat,al `uwayja,al `uwayliyah ash sharqiyah,al `uwaynah,al `uwaynah raja,al `uwaynat,al `uwayni,al `uwaynid,al `uwayqilah,al `uwayr,al `uwayridi,al `uways,al `uwaysa,al `uwaysab ibrahim,al `uwaysan,al `uyaynah,al `uyun,al `uzaym,al `uzayr,al `uzayri,al `uzi\302\247aym,al `uzi\302\247miyah,al `uzlah,al a`aqib,al a`bal,al a`buq,al a`duf,al a`ki,al a`lam,al a`qab,al a`raf,al a`ras,al a`zi\302\247amiyah,al aalmiye,al ab`adiyah,al ababah,al abadia,al abadilah,al abar,al abayachi,al abiat,al abqa`ayn,al abraq,al abshit,al abtakh,al abtiyat,al abyad,al abyar,al abyat,al achem,al ad dahis,al ad daramah,al ad duwayh,al ad`ami,al adabiyah,al adamna,al adar,al adara,al adhdhah,al adhaba-iyah,al adinah,al adjiri,al adla,al adriyah,al aduf,al afandiya,al afandiyah,al afrad,al afyush,al afzar,al agagda,al agdah,al aghanah,al aghannah,al aghar,al agharr,al aghiair,al aghwal,al agrab,al agrabiyak,al agraine,al ahaiwah gharb,al ahad,al ahad al masarihah,al ahadiya,al ahadiyah,al ahamidah,al ahaywah gharb,al ahaywah sharq,al ahdiyah,al ahduf,al ahjal,al ahma,al ahmadi,al ahmadiyah,al ahmar,al ahmed,al ahqaf,al ahramiya,al ahramiyah,al ahrar,al ahraz,al ahsa,al ahsab,al ahsam,al ahsun,al ahtub,al ahzam,al ailah,al ain,al ait,al aiwis,al aj jabur,al ajah,al ajal,al ajam,al ajelane,al ajfar,al ajfur,al ajil,al ajlub,al ajman,al ajnaf,al ajrash,al ajrum,al ajwaf,al akar,al akba,al akhal,al akhawat,al akhawayn,al akhbawiyah,al akhdar,al akhf,al akhmiyin,al akhri,al akhshab,al akoura,al akrad,al akrad wa bani zayd,al akram,al aktu,al akwash,al akwat,al al `afalijah,al al jabur,al al munbatah,al al munbatih,al al-shaikh `ali,al alaouis,al aljam,al alwah,al amal,al amal (nimrah thamaniyah),al amamcha,al amarah,al amarat,al amayir,al ambah,al ambarkab,al ambir,al amenda,al amera,al amghar,al amimet,al amin,al amir,al amira,al amiriyah,al amlah,al amma,al amrini,al amtahiyah,al amzugah,al an nanisah,al anabsa,al anbutayn,al ancer,al andalus,al anis,al anjab,al anjud,al anocer,al ansar,al ansil,al ant,al anus,al aouda,al aouiant,al aouiche,al apartadero,al aq`af,al aqadah,al aqadimah,al aqalitah,al aqariyah,al aqhaliyin,al aqli,al aqm nakhtan,al aqra`,al aqub,al aqwaz,al araba,al arak,al arba ida ou trhoumma,al arba,al arba` `izab,al arba`in,al arbah,al ard,al ardel,al aribe,al aricha,al aridh,al aridiyah,al arki,al arkub,al aroussine,al aroussiyine,al artawi,al artawi ash shamali,al artawiyah,al arz,al arzah,al as sawat,al as`adiyah,al asabi`ah,al asaif,al asala,al asalimah,al asamilah,al asamir,al ash shatab,al ash shaykh,al ash shaykh `ali,al ash`ariyah,al asha,al ashar,al asharah,al ashhab,al ashhur,al ashkhar,al ashkhara,al ashkharah,al ashkhirah,al ashmah,al ashmunayn,al ashmunin,al ashmur,al ashqar,al ashraf,al ashraf al `asaliyah,al ashraf al bahriyah,al ashrafiyah,al ashrak,al ashram,al ashshah,al ashtana,al ashur,al asi,al aslab,al aslamah,al aslihah,al asrar bani `umar,al asrar bani sa`d,al asrum,al assan,al astral,al aswad,al aswaq,al asyan,al at tafilah,al at tawil,al atabah,al atarib,al atarishah,al atawilah,al atawlah wa bani `ilayj,al aterma,al atf,al atfa,al athalah,al athamah,al athariyah,al athawiri,al athawri,al athlah,al athrun,al atlat,al atrun,al audah,al awaya,al awaynat,al awiliah al sharqiyah,al awjam,al awqaf,al awqair,al awqayr,al awsat,al awsat samhud,al ayish,al ayoun,al aysar,al aysoina,al ayubiyah,al ayyash,al azair,al azam town,al azariq,al azbakiyah,al azifat,al azi\302\247bur,al azrakiyah,al azraq,al azraq ash shamali,al ba karshum,al ba`aith,al ba`aja,al ba`aliwah al kabirah,al ba`aliwah as saghirah,al ba`assis,al ba`ayith,al ba`id,al ba`ij,al ba`irat,al ba`ja,al ba`jah,al ba`um,al bab,al baba,al babiliyah,al babliyah,al bad`,al bad` al qadim,al badai`,al badai` al `ulya,al badai` al wusta,al badari,al badawi,al badawiyah,al badayrah,al baddalah,al baddawi,al badhikha,al badhinjaniyah,al badi,al badi,al badi`,al badi`ah,al badie,al badiq,al badiriyah,al badiyah,al badiyi`,al badr,al badraman,al badran,al badrashayn,al badriyah,al badrusiyah,al baduwani,al badwah,al baghaliyah,al baghdadi,al baghdadiyah,al bah,al bahadah,al bahah,al baharah,al bahawal,al bahharah,al bahhariyah,al bahhath,al bahhath al janubi,al bahih,al bahitah,al bahiyah,al bahluliyah,al bahluniyah,al bahnasa,al bahr,al bahra,al bahrah,al bahri qamula,al bahriyah,al bahsah,al bahsamun,al bahsas,al bahsasah,al bahth,al bahul,al bahw furayk,al baidhah,al bajah,al bajalat,al bajariyah,al bajhur,al bajjajah,al bajur,al bakakishah,al bakarishah,al bakatush,al bake,al bakhaitah,al bakhabikhah,al bakhah,al bakhanis,al bakhra,al bakkariyah,al bakki,al bakrah,al bakri,al bakrin,al bakriyah,al bakulak,al balaizah,al bala`zitayn,al balabish,al balabish bahri,al balabish qibli,al balad,al balakus,al balamun,al balasah,al balash,al balashun,al balat,al balatah,al balayzah,al baljaniyah,al ball,al ballas,al ballasi,al balo,al balyana,al bamruh shaljeh,al banah,al banawad,al banawan,al banawid,al bandah,al bandaji,al bandarah,al bangedit,al bani thawr,al bani thur,al baniya,al banna,al bannain,al banzin 39,al baq`a,al baqah,al baqalitah,al baqaq,al baqarah,al baqer,al baqilah,al baqliyah,al baqqah,al baqqashin,al baqrayn,al baqruqah,al baqurah,al bar,al bar meadows,al barabir,al barabrah,al baradi`ah,al baradi`iyah,al baradun,al baraghit,al baragna,al barah,al barahimah,al barajil,al barakah,al baramiyah,al baramun,al baraniqah,al barar,al barawikah,al barazin,al barba,al barba al kubra,al barbar,al barbarah,al barbaw,al barbu,al bardab,al bardi,al bardiyah,al bardunah,al barghash,al barh,al barid,al baridah,al barihah,al barik,al bariqah,al bariqiyah,al barir,al barit,al bariyah,al barjasiyah,al barjisiyah,al barjud,al barkah,al barkat,al barkhiyah,al barmiyah,al barnuji,al barq,al barqi,al barrah,al barrani,al barraniyah,al barraq,al barraqiyah,al barsa,al barsah,al barsha,al barshi,al barud,al barud al gharbi,al barud ash sharqi,al barud wa awlad ilyas,al barudah,al barudi,al barudiyah,al baruk,al barun,al baruzah,al barwaya,al barzah,al basa,al basabir,al basaliyah,al basaliyah bahri,al basaliyah qibli,al basam,al basar,al basatin,al basaysah,al bashawil,al bashiqiyah,al bashir,al bashir ibrahim,al bashiri,al bashiyah,al bashnayn,al bashnin,al bashurah,al basikiyah,al basirah,al basiri,al baslan,al baslaqun,al basnah,al basqalun,al basrah,al basrah al qadimah,al basriye,al bassa,al bassah,al bastah,al bastawi,al basuf,al basyuniyah,al batabitsh,al batan,al batanun,al bataykhi,al bath,al batha,al bathah,al batin,al batinah,al batiyah,al batlah,al batnah,al batr,al batra,al batrun,al batrunah,al battah,al battal,al battaliyah,al bauga,al bawadish,al bawarik,al bawathil,al bawdah,al bawiti,al bawshariyah,al bawshiriyah,al bawsi,al bawtah ash sharqiyah,al bayad,al bayadah,al bayadi,al bayadi wa al qaryah,al bayadir,al bayadiyah,al bayahu,al bayatinah,al bayd,al bayda,al bayda,al bayda`,al baydah,al baydan,al baydar,al bayli,al bayluq,al bayrum,al baytariyah,al baytarun,al bayya`,al bayyad,al bayyadah,al bayyadiyah,al bayyadiyah bi an nazi\302\247ir,al bayyaz,al bayyum,al bazazah,al bazi,al bazimah,al bazrakan,al bazuriyah,al bcaclat al ghaba,al bchara,al bdair,al beggara,al beida,al beidha,al beiri,al beit,al beke,al benanda,al beniya,al berdab,al berike,al beyiria,al beyoule,al bghailiya al mhajba,al bhalla,al bhar,al biar,al biarah,al bir,al birayn,al bi`aj,al bi`na,al bibar,al bicht,al bid,al bid`,al bid`ah,al bida`,al bida` al gharbiyah,al bida` ash sharqiyah,al bidayah,al bidayyi`ah,al biddah,al biddawi,al bidh,al bidia,al bidja,al biere,al bihay,al biher,al bijadiyah,al bikah,al bikariyah,al bila`izitayn,al bilad,al bilad al wusta,al bilaliyah,al bilat,al biljah,al bina,al binakani as sadr,al bindariyah,al bines,al binnay,al bioutat,al biqa`,al bir,al bira,al birah,al birak,al birashiyah,al birba,al birba al kubra,al biri,al birin,al birk,al birkah,al birri,al bisariyah,al bish-sha,al bishshah,al bitanah,al bitaniyah,al bitariyah,al bitayyah,al bithij,al bithnah,al biyadi,al biyah,al biyas,al biyatinah,al boda danraga,al bolagh,al bolakh,al borj,al borja,al borma,al bouab,al bout,al boutal,al bouwom,al bowna`im,al brahma,al braj,al bramiyah,al brashieh,al bsaslat al ghaba,al bu azairij,al bu bulaisim,al bu hafez,al bu hammud,al bu hardan,al bu hayyat,al bu hubail,al bu mu`ayt,al bu nael,al bu nayel,al bu radha,al bu radho,al bu sabbah,al buayr,al buayrat,al burah,al bu`,al bu`aymah,al bu`aythiyah,al bubiya,al bubiyah,al buda`ah,al budair,al buday`,al budayah,al budayr,al budayriyah,al budayyah,al budayyi`,al budi,al budur,al buga`,al bughayl,al bughaylah,al bughaz,al buha,al buhah,al buhaym,al buhayma,al buhayr,al buhayrah,al buhayri,al buhays,al buhayshah,al buhrah,al buhur,al buj`an,al bujaydi,al bujuriyah,al bukayriyah,al bukhayriyah,al bul`um,al bulaq,al bulaydah,al bulaydim,al buliyah,al bult,al bum,al buna`im,al bunais,al bunawad,al bunayyat,al bunayyat al janubiyah,al bunayyat ash shamaliyah,al bunud,al bunyah al janubiyah,al buq`ah,al buq`an,al buqa`,al buqay`,al buqayr,al buqayrayn,al buqei`a,al buqul,al bur,al burah wa al hadaya,al buraik,al burak,al buratah,al buravayeh,al burayj,al burayjat,al burayk,al buraykah,al buraymi,al burayqah,al buraysah,al burayyirah,al burdayah,al burdi,al burghuliyah,al burghuthi,al burghuthi al bahri,al burghuthi al qibli,al burham,al burj,al burj al saliya,al burj as saliya,al burj as saliyah,al burjayah,al burjayn,al burk,al burmah,al burnas,al burqah,al burr,al burt,al burtasiyah,al burud,al burumbul,al burut,al busaylah,al busayli,al busayrah,al busayri,al busaysah,al busaytin,al busayyah,al busayyir,al bushamah,al buslam,al busrat,al buss,al bustan,al busti,al but,al butah,al butakh,al butayh,al butayhah,al butayn,al butaynah,al butayniyat,al butayra,al buthah,al buwam,al buwar,al buwatah,al buwayb,al buwaybiyah,al buwayd,al buwayda,al buwaydah,al buwaydiyah,al buwayr,al buwayridah,al buwayt,al buwaytah,al buwayti,al buwaytir,al buwayzi\302\247a,al buzun,al buzuriyah,al bwar,al byeara,al cad,al cardud,al chabaish,al chabash,al chalek,al chalili,al chamcha,al charshalu,al chifchafa,al chirabia,al da`ain,al dabbah,al daff,al dahdah,al dahirah,al dahrah,al daid,al dakhil,al dakhirah,al dalham,al damiah,al dammaj,al darai,al darar,al darreh,al darvish,al dawaya,al dawdaf,al dayasir,al dhasin,al dhakhirah,al dharayn,al dhimrah,al dhuhrah,al dibah,al dini,al diwaya,al diya,al djezir,al djezire,al djufaydiriyya,al doha,al drahir,al dulakh,al durayhami,al duss,al duwayh,al duwika,al edeiya,al eiwad,al ekkawiyeh,al elain,al eseilat,al faid,al faidiyah,al faijah,al faiziyah,al far,al fay,al fa`iliyah,al fa`ur,al fa`uri,al fadadinah,al fadgoba,al fadhil,al fadili,al fadl allah,al fadlab,al faei,al fahada,al fahd,al fahd bash,al fahham,al fahmiyah,al fahmiyin,al fahs,al fai,al faisaliyah,al fajarah,al fajir,al fajj,al fajjah,al fajjalah,al fajr,al fajrah,al fakharin,al fakhirah,al fakhiriyah,al fakhkhariyah,al fakhriyah,al fakka,al fakyah,al fala,al falah,al falaj,al falaj al hadith,al falaq,al fallatah,al falluja,al fallujah,al falt,al falu kibar,al fanadik,al fanaitis,al fanaj al habil,al fanatir,al fandaqumiya,al fandaqumiyah,al fanh,al fant,al fantas,al faq`ah,al faqam,al faqi`,al faqirah,al faqqah,al faqqusah,al far`,al far`ah,al far`ayn,al far`iyah,al fara,al fara`,al farab,al faradis,al farafirah,al farah,al faraha,al farajab,al farandis,al faranitah,al faransawiyah,al faras,al farashah,al farastaq,al farat,al faraysah,al farazi`ah,al fardah,al fardhakh,al fardis,al farfarah,al farhan,al farhaniyah,al farhud,al fari`,al farj,al farqiyah,al farraah,al farrash,al farrayin,al farridah,al farrukhiyah,al farsah,al farsh,al farshah,al fartus,al faruq,al faruqiyah,al faruthi,al farwan,al farwania,al farwaniyah,al fas,al fasail,al fasam,al fash,al fash shash,al fashashawyah,al fashir,al fashn,al fashsh,al fasim,al fasl,al fasqin,al fassayah,al fasur,al fatah,al fatat,al fatchotchoy,al fathan,al fathiyah,al fatih,al fatihah,al fatirah,al fatk,al fatsah,al fatsi,al faw,al fawadil,al fawaqisah,al fawatir,al fawqani tashli huyuk,al fawwahah,al fawwaliyah,al fawwar,al fawwarah,al fawwaz,al fawziyah,al fayama,al fayasilah,al fayd,al faydah,al faydami,al faydiyah,al fayha,al fayhah,al fayidiyah,al fayijah,al fayj hamadna allah,al faysaliyah,al faysh,al fayuah,al fayush,al fayy,al fayyadiyah,al fayyum,al fazah,al fazaru,al fazz,al fazzah,al fdoul,al feida,al fejaie,al fergedi,al fida,al fida,al fidar,al fiduk,al fifi,al fihri,al fijwar,al fikriyah,al fil,al fin,al fintas,al fintass,al fiqrah,al fir`awniyah,al firdan,al firdaws,al firnanah,al firqah,al fishsh,al fisirah,al fiwan,al fiyad,al fiyush,al fiyya,al fnaydiq,al fogra,al fouggara,al foukara,al fouqiya,al fraykah,al frech,al frian,al fsaqin,al ftahat,al fuadiyah,al fuur,al fudaydah,al fudul,al fuhayhil,al fuhayl,al fuhays,al fuhud,al fujayh,al fujayj,al fujayjah,al fujayr,al fukah,al fukhaytah,al fulah,al fulayl,al fulays,al fulayt,al funaydiq,al funaytis,al funduq,al funduq al jadid,al fuqah,al fuqaha,al fuqaha al qibliyah,al fuqait,al fuqar,al fuqara,al fuqayhiyah,al fuqqa`i,al furaijat,al furay`,al furay`ah,al furayd,al furaydiliyah,al furaydis,al furayj,al furayjat,al furaykhah,al furaysh,al fureidis,al furjah,al furn,al furnaj,al furs,al fursan,al fursi,al furt,al furud,al furusat,al furut,al furuthi,al furzul,al fusayrah,al fuslah,al futah,al futasiyah,al futayh,al fuwayhat,al fuwayliq,al fuyay,al gaback,al gada,al gadd,al gagad,al gah,al galgala,al gallo,al game,al gana,al ganaet,al gap,al gara,al garage,al garef,al garrah,al garyan,al gattan,al gayr,al gebir,al geccio,al geda,al geddara,al gedul,al gelem,al genadib,al ghab,al ghaba,al ghabah,al ghabat,al ghabbatiyah,al ghabbayshah,al ghabbi,al ghabbit,al ghabib,al ghabirah,al ghabishah,al ghabiyah,al ghabra,al ghabsha,al ghabshah,al ghabun,al ghaddarah,al ghadifah,al ghadir,al ghaduri,al ghadya,al ghaf,al ghafah,al ghafar,al ghafarah,al ghafat,al ghaff,al ghaffariyah,al ghafilah,al ghafiyah,al ghafuqiyah,al ghaidat jirwid,al ghaidha,al ghail,al ghaiman,al ghait,al ghajar,al ghajjar,al ghajlah,al ghal,al ghala`il,al ghalibiyah,al ghalil,al ghalilah,al ghallah,al ghalub,al ghama,al ghamis,al ghammas,al ghamya,al ghanaim,al ghanaim bahri,al ghanaim qibli,al ghanam,al ghanamah,al ghanamiyah,al ghananim,al ghanayim,al ghanduriyah,al ghanim,al ghanimiyah,al ghanjah,al ghanmiyah,al ghanta,al ghar,al gharabiwah,al gharafah,al gharah,al gharaq,al gharaq as sultani,al gharbi,al gharbi bahjurah,al gharbi bi as salamiyah,al ghardaqah,al ghari,al gharib,al gharif,al ghariqah,al gharir `alayah,al gharir sufalah,al gharith,al ghariyah,al ghariyeh,al gharizat,al gharraf,al gharrafah,al gharrafah al jadidah,al gharrafi,al gharrah,al gharraqah,al ghashamiya,al ghashamiyah,al ghashbah,al ghashshamiyah,al ghashwah,al ghasibiyah,al ghassalah,al ghassaniyah,al ghassiba,al ghasulah,al ghat,al ghataf,al ghawadir,al ghawamir,al ghawasah,al ghawb,al ghawi,al ghawl,al ghawr,al ghawwabin,al ghaya,al ghayatah,al ghayda,al ghaydah,al ghaydah al khadra,al ghaydaniyah,al ghayf,al ghayl,al ghaylah,al ghaylan,al ghaylanah,al ghayman,al ghayt,al ghayyah,al ghayzan,al ghazal,al ghazalah,al ghazalayn,al ghazali,al ghazi,al ghazili,al ghaziyah,al ghazlan,al ghazwane,al ghazzabiyah,al ghazzal,al ghazzawiyah,al ghiasi,al ghilah,al ghimaq,al ghinah,al ghirafa,al ghirafah,al ghiraibat,al ghiran,al ghiras,al ghiyaidah,al ghizlan,al ghizlaniyah,al ghlel,al ghomb,al ghraibi,al ghrariyine,al ghu,al ghub,al ghubadah,al ghubar,al ghubayra,al ghubayrah,al ghubayri,al ghubayshab,al ghubayshiyah,al ghubrah,al ghubshan,al ghuday,al ghudayy,al ghudham,al ghuf,al ghufayrah,al ghufra,al ghul,al ghulah,al ghulayfiqah,al ghulaylah,al ghullah,al ghumayyisah,al ghumq,al ghunamiyah,al ghunaymi,al ghunaymiyah,al ghunnamiyah,al ghunsalah,al ghurabah,al ghurabiyah,al ghuraf,al ghuraifa,al ghuraim,al ghurayb,al ghuraybah,al ghuraybat,al ghurayd,al ghurayfah,al ghurayq,al ghurayri,al ghurayriyah,al ghurayyib,al ghurayyibah,al ghurd,al ghurfah,al ghuri,al ghurik,al ghuriqah,al ghurzah,al ghushayn,al ghusnah,al ghut,al ghutamiyah,al ghutghut,al ghuwairiya,al ghuwairiyah,al ghuwaydah,al ghuwayr,al ghuwayriyah,al ghuwaysah,al ghuwayshiyah,al ghuwayyat,al ghuwayz,al ghuwiz,al ghuyaydah,al ghuzayliyah,al ghuzayz,al ghuzi\302\247ayfah,al ghuzlaniyah,al ghuzziyah,al ghzailiyah,al giesat,al gilei awlad nafi,al gilein,al gissemari,al gloriette,al gore,al gorg,al gouzat,al gowez,al goz,al graa,al grafda,al grama et thata,al greg,al guba,al gubbab,al gubbah,al gube,al gubur,al guidei,al guirinti,al gule,al gulishan,al gurabat,al guraitiya,al gureif,al guroh,al gurra`a `abd al ghani,al guruna,al gutheria,al gutherla,al gutnah,al guwayr,al guweir,al gyuddul,al hai,al hair,al hait,al hauz,al hab`i,al haba,al habaishah,al hababi,al habah,al habaj,al habalayn,al habalisah,al habaliyah,al haban,al habar,al habarah,al habash,al habashi,al habayla,al habbaniya,al habbaniyah,al habbaq,al habbarat,al habbariyah,al habil,al habilah,al habilah abu nusayr,al habilayn,al habilyah,al habis,al habit,al habl,al hablah,al habs,al habsh,al habubiyah,al hachimia,al hachlaf,al had,al had-fraita,al hada,al hadaidah,al hadabah,al hadadcha,al hadaf,al hadah,al hadajah,al hadan,al hadaq,al hadaqah,al hadar,al hadari`ah,al hadath,al hadaya,al hadba,al hadbah,al hadd,al haddad,al haddadah,al haddadayn,al haddadi,al haddadin,al haddadiyah,al haddadiyat,al haddah,al haddaj,al haddaliah,al haddaliyah,al haddam,al haddar,al haddariyah,al haddayn,al hadeeqa,al haden,al hadha,al hadhr,al hadhra,al hadid,al hadidah,al hadidi,al hadidiyah,al hadie,al hadilah,al hadin,al hadinah,al hadiqah,al hadirah,al haditha,al hadithah,al hadiyah,al hadiyan,al hadj dialo,al hadna,al hadqah,al hadqi,al hadr,al hadra,al hadra`,al hadrah,al hadrami,al hadre,al hadrur,al hadu,al hafair,al hafah,al hafaniya,al hafaniyah,al hafar,al hafayir,al haffah,al hafidh,al hafir,al hafir al fawqani,al hafirah,al hafizi\302\247,al haflawiyah,al hafnah,al hafr,al hafriyah,al hafsah,al hafsi,al hagab,al hagagia,al hagarein,al hageina,al hageina as suq,al hagina,al hai al ahli,al hai at tigari,al haibala,al haidha,al haifa,al haimriyine,al haira,al hairah,al haiyaniya,al haj `alawi,al haj `ali,al haj `ashur,al haj `athith,al haj `awad,al haj `isa,al haj `ubayd,al haj `ulwan,al haj farhan,al haj hattab,al haj husayn,al haj mahdi,al haj mahdial abid,al haj muhammad,al haj radhi,al haj radi,al haj ramdi,al haj salim,al haj shan,al hajaizah,al hajaf,al hajajah,al hajajiyah,al hajalah,al hajalij,al hajaliyah,al hajamah,al hajamiyah,al hajar,al hajar al `ulya,al hajar al abyad,al hajar al mahruq,al hajara`,al hajarah,al hajarayn,al hajarisah,al hajayzah,al hajb,al hajere,al haji khalid,al hajib,al hajilah,al hajin,al hajir,al hajj,al hajj `abbas an nasif,al hajj `abd as sayyid,al hajj `alawi wa mahdi,al hajj `athith,al hajj `awwad,al hajj `azay,al hajj `ubayd,al hajj `umar,al hajj as saghir,al hajj ashur,al hajj farah,al hajj farhan,al hajj hasan,al hajj hattab,al hajj husayn,al hajj ja`far,al hajj jasim,al hajj kazi\302\247im,al hajj khalid,al hajj mahdi al `ubayd,al hajj mahmud,al hajj qandil,al hajj qindil,al hajj rabih,al hajj radi,al hajj ramzi,al hajj ru`uf shalash,al hajj ruhayt,al hajj sahn,al hajj sulayman,al hajj sulayman ad dari,al hajj talal `ali,al hajjah,al hajjajiyah,al hajjam albu `uwayd,al hajjamah,al hajjaylah,al hajmah,al hajnawi,al hajr,al hajra,al hajrah,al hajriyah,al haju,al hajul,al hajye,al hajz,al hajz bahri,al hajz qibli,al hakahere,al hakamab wadi,al hakamab west,al hakaminah,al hakamiyah,al hakim,al hakimiyah,al hakur,al hal,al halabiyah wa nazlat mustafa bey,al halafi,al halafi al kabir,al halajah,al halaliyah,al halamah,al halami,al halamishah,al halamuz,al halaqah,al halar,al halawani,al halawat,al halazun,al halba,al halbah,al halda,al haldah,al halfayah,al halfayah bahri,al halfayah qibli,al halhal,al halhalah,al hali,al halimab,al halla,al hallafi,al hallah,al hallaqi,al hallubah,al hallufah,al halya,al ham sha,al hama,al hamaim,al hamair,al hamad,al hamadah,al hamadi,al hamadin,al hamadiyah,al hamah,al hamalah,al hamam,al hamamda,al hamamiyat,al hamar,al hamari,al hamariyah,al hamasah,al hamasiyat,al hamatah,al hamayidah,al hambat,al hambra,al hambushiyah,al hamd,al hamdab,al hamdah,al hamdaniyah,al hamerib,al hami,al hamid,al hamidah,al hamidat,al hamidi,al hamidiyah,al hamidiyah al jadidah,al hamiliyah,al hamimah,al hamiriyah,al hamish,al hamishah,al hamishi,al hamiya,al hamiyah,al hamjah,al hamma,al hammad,al hammad ash sharqi,al hammadah,al hammadah al kubra,al hammadah as sughra,al hammadiyah,al hammadiyin,al hammah,al hammam,al hammamat,al hammamiyah,al hammar,al hammaydah,al hamoum,al hamr,al hamra,al hamra,al hamrah,al hamran,al hamrat,al hamrawi,al hamriya,al hamshah,al hamud,al hamul,al hamuli,al hamza,al hamzah,al hamzat,al hanad,al hanafiyah,al hanajir,al hanakah,al hanakiya,al hanakiyah,al hanashah,al hanashiyah,al handasah,al hani,al hanish,al haniyah,al hannanah,al hanqazah,al hanshir,al hanu,al hanutah,al hanutih,al hanuvil,al hanw,al haouamed,al haouia,al haq,al haqab,al haqabah,al haqawah,al haqf,al haql,al haqlah,al haqlaniyah,al haqq,al haqt,al haqu,al haqw,al har,al haraik,al haraiq,al hara`,al harabah,al haradah,al haradinah,al harafishah,al harah,al harah al kabirah,al harah as saghirah,al haraibah,al haraj,al harajah,al harajah bahri,al harajah bil qur`an,al harajah qibli,al harak,al haraki,al haram,al haram ash sharif,al haramah,al harami,al haran,al harani,al hararah,al harari,al haras,al harasa,al harashah,al harat,al haratza,al harawiyah,al harayibah,al harayr,al haraz,al harb,al harbi,al harf,al harfa,al harfiyah,al harharah,al hari,al harib,al haridiyah,al haridiyah al bahriyah,al haridiyah al qibliyah,al harijan,al hariq,al hariqah,al hariqiyah,al hariri,al haris raqm sittah,al haris raqm thalathah,al harisah,al harithah,al harithiyah,al harizat al gharbiyah,al harizat ash sharqiyah,al harjalah,al harma,al harmal,al harmaliyah,al harmuzi,al haroup,al harra,al harra,al harrah,al harraniyah,al harsha,al harshah,al harshiyat,al hartamiyah,al harthah,al harthiyah,al haruj,al haruniyah,al harur,al harurah,al haryah,al hasa,al hasainah,al hasab,al hasabiyan,al hasaf,al hasafah,al hasahisa,al hasaifin,al hasainah,al hasak,al hasakah,al hasamah,al hasamidah,al hasan,al hasan `abd al qadir,al hasanab,al hasanah,al hasaniyah,al hasaniyyah,al hasar,al hasasinah,al hasasira,al hasasirah,al hasat al khaniqah,al hasaw,al hasayinah,al hasaynah,al haseirab,al hashaim,al hashabirah,al hashem,al hashem-e pa`in,al hasherij,al hashidi,al hashima,al hashimi,al hashimiyah,al hashimiyah al janubiyah,al hashimiyyah al janubiyah,al hashishiah,al hashishiyah,al hashiyah al hamra,al hashmah,al hashnah,al hashus,al hasib,al hasif,al hasiki,al hasin,al hasirah,al hasis,al haslub,al hasrah,al hasrat,al hassah,al hassainat,al hassana,al hassane,al hassani,al hassaniyah,al hassi,al hasusah,al hasw,al haswah,al haswah fawqani,al haswah tahtani,al haswani,al haswar,al hasy,al hatab,al hatabah,al hatahitah,al hatarish,al hathamah,al hathirah,al hati,al hatim,al hatiriyah,al hatiyah,al hatm,al hatriyah,al hattaniyah,al haud,al hauf,al hauta,al hautah,al hawainah al kabirah,al hawainah as saghirah,al hawabir,al hawabit,al hawad,al hawafizi\302\247,al hawali,al hawamed,al hawamid,al hawamidiyah,al hawar,al hawarith,al hawarithah,al hawariyah,al hawartah,al hawash,al hawasiliyah,al hawatah,al hawatikah,al hawatimah,al hawatiyiah,al hawawish,al hawawishah,al hawaz,al hawd,al hawd at tawil,al hawf,al hawi,al hawiliyah,al hawiri,al hawiyah,al hawja,al hawl,al hawmah,al hawmaniyah,al hawmat,al hawqa,al hawqar,al hawr,al hawrah,al hawrani,al haws,al hawsah,al hawsh,al hawshak,al hawt,al hawtah,al hawtah ash sharqiyah,al hawwa,al hawwarah,al hawwari,al hawwas,al hawwasiyah,al hawwata,al hawz,al hay,al hayadi,al hayara,al hayathim,al hayatim,al hayawan,al hayb,al haybah,al haybalah,al hayd,al hayd al asfal,al hayda,al hayf,al hayfah,al hayi,al hayir,al hayit,al hayiy,al hayj,al hayl,al haylah,al haylunah,al haymah,al haymar,al haymarat,al haymatayn,al hayr,al hayra,al hayrah,al hayrat,al haysah,al haysam,al haysamiyah,al haysan,al hayshah,al haytalah,al hayy,al hayy al ahli,al hayy al markazi,al hayy at tiqqari,al hayyan,al hayyaniyah,al hayz,al hayzah,al haza,al haza`,al hazim,al hazimah,al hazimi,al hazimiyah,al haziz,al hazm,al hazmah,al hazrayn,al hazzah,al hazzanah,al hbabiyah,al hebal,al hebeika,al hedet,al hegerai,al hegr,al heleiba,al hess,al hesse,al heyatfe,al hfari,al hibah,al hibah al gharbi,al hibah ash sharqi,al hibali,al hibbariyah,al hibra,al hibs,al hibsh,al hidadah,al hidbah,al hidd,al hidjer,al hidn,al hif,al hifnah,al hijal,al hijanah,al hijanbah,al hijar,al hijarah,al hijaziyah,al hijfah,al hijfar,al hijil,al hijir,al hijirrah,al hijl,al hijlah,al hijlayn,al hijr,al hijrah,al hijran,al hijre,al hijriyah,al hijun,al hijz,al hikkiyah,al hilal al gharbiyah,al hilal ash sharqiyah,al hilaliyah,al hilf al gharbi,al hilfayah qibli,al hilla,al hillah,al hilmiyah,al hilwa,al hilwah,al hilwaniyah,al hilyah,al hima,al hima,al himadah,al himadim,al himas,al himede,al himliyah,al himmah,al himran,al himsiyah,al hin,al hinadi,al hinakiyah,al hinayniyah,al hindaw,al hindawiyah,al hindiyah,al hinnah,al hinniyah,al hinshir,al hinu,al hinw,al hinya,al hiqa,al hiqr,al hirah,al hirajiyah,al hirayth,al hirdawsh,al hiri,al hirme,al hirmil,al hisah,al hisam,al hisan,al hisanat,al hisaniyah,al hisar,al hisas,al hisbar,al hishah,al hishmah,al hishwah,al hisi,al hisn,al hisn al abyad,al hisn al adshfar,al hisn al ala,al hisn al amir,al hisseine,al hissu,al hisun,al hiswah,al hiswar,al hisyan,al hit,al hitmi,al hiwatah,al hiwatah ash sharifiyah,al hiyad,al hiyal,al hizaniyah,al hizq,al hi\302\261ajar,al hlalma,al hmamda,al ho,al hoceima,al hogl,al hoi,al hokamin,al homera,al horech,al horm,al hothera,al houamed,al hrayeq,al hrazmin,al hub,al hubabi,al hubari,al hubayil,al hubayl,al hubayriyah,al hubaysah,al hubayshiyah,al hubayyil,al hubi,al huceima,al huda,al hudayb,al hudaybah,al hudayd,al hudaydah,al hudayf,al hudaylah,al hudaymiyah,al hudayran,al hudra,al hudud,al hufayr,al hufayyirah,al huff,al hufrah,al hufuf,al hugnah,al hujar,al hujayjah,al hujayl,al hujaylah,al hujaylijah,al hujayr,al hujayrah,al hujayrat,al hujayrimat,al hujfah,al hujnah,al hujra,al hujrah,al hujuf,al hukal,al hulah,al hulaichiyah,al hulaishiyah,al hulayfi,al hulaylah,al hulayshiyah,al hulaysiyah,al hulfayah,al hulw,al hulwa,al hulwah,al humah,al humaidha,al humar,al humarah,al humasiyah,al humaydah,al humaydan,al humaydat,al humaydi,al humaymah,al humaymah al jadidah,al humayr,al humayra,al humayrah,al humays,al humayshah,al humayshi,al humayzah,al humil,al hummar,al hummaydah,al humr,al humr wa al ja`afirah,al humran,al humrurah,al humsiyah,al hunaynah,al hunaysh,al hunayy,al hunu,al hunud,al huqab,al huqnah,al huqqah,al hurah,al huraiba,al huraimala,al huraiyiq,al huraniyah,al huraybah,al huraydah,al hurayf,al hurayfat,al hurayq,al hurayr,al hurayran,al hurayth,al hurayyiq,al hurbi,al huri,al huria,al hurim,al huriyah,al hurm,al hurr,al hurriyah,al hurshah,al hurtah gharbi,al hurtah sharqi,al huruf,al husaifin,al husain,al husam,al husan,al husaniyah,al husaydhiah,al husayfin,al husaymah,al husaymat,al husayn,al husaynah,al husayni,al husayniyah,al husayniyyah,al husayy,al husayyah al `ulya,al husayyah as sufla,al huseiniya,al husen,al hushayshinah,al hushayshyah,al hushshah,al husn,al husn al adshfar,al husn al ala,al hussainiyah,al husun,al huta,al huta ash sharqiyah,al hutah,al hutar,al hutiyah,al huwah,al huwaidir,al huwaimi,al huwair,al huwaish,al huwaiyit,al huwariyah,al huwas,al huwayb,al huwaychiyah,al huwaydi,al huwaydir,al huwayhi,al huwayjah,al huwayjat,al huwayjiyah,al huwaykiyah,al huwayl,al huwaylah,al huwaylayn,al huwayliyah,al huwaymi,al huwaymidah,al huwayr,al huwayrah,al huwayrrah,al huwaysh,al huwaysis,al huwayya,al huwayyah,al huwayyit,al huwayz,al huwib,al huwir,al huwwah,al huwwariyah,al huwwas,al huyul,al huzail,al huzmah,al huzum,al hwawes,al ian,al ibnoia,al ibrahim,al ibrahimiyah,al ibrinji,al ibriqji,al ibt,al idarah,al idrisi,al idrisiyah,al ifayhid,al ifhaihil,al ifranj,al ihan,al ihsaniyah,al ikhmas,al ikhsas,al ikhsas al qibliyah,al ikhyiwwah,al ildidiya,al ima,al imam,al imam al ghazali,al imam al husayn,al imam ash shaykh muhammad,al iman,al iman (t),al imtihiyah,al insariyah,al inshasiyah,al iqbalah,al irtawiya,al isbalah,al isdiyah,al ishay`il,al iskan,al iskandar,al iskandariyah,al islah,al islat,al isma`il,al isma`iliya,al isma`iliyah,al istabil,al istabl,al itein,al ithsilat,al ittaihad,al itthad,al ittihad,al izdihar,al j`ara,al jaif,al jaizah,al ja`adib,al ja`afirah,al ja`aliyah,al ja`arah,al ja`arif,al ja`ayil,al ja`di,al ja`diyah,al ja`fariyah,al ja`halah,al ja`halat,al ja`lah,al ja`liyah,al ja`mah,al ja`r,al jab`ah,al jaba,al jabaish,al jaba`,al jabajib,al jabal al abyad,al jabalah,al jabalaw,al jabalayn,al jabanah,al jabar,al jabariyah,al jabash,al jabat,al jabayish,al jabbas,al jabha,al jabhah,al jabi,al jabih,al jabil,al jabin,al jabir,al jabirat,al jabiri,al jabiriyah,al jabiyah,al jabiyah al `ulya,al jabiyah as sufla,al jabjab,al jabjud,al jabrawah,al jabriyah,al jabsh,al jabub,al jaburiyah,al jaburiyyah,al jabuz,al jabw,al jaby,al jad`ah,al jadabah,al jadbah,al jadd,al jadhi,al jadhub,al jadi,al jadid,al jadida,al jadidah,al jadil,al jadlah,al jadu`iyah,al jadwal al gharbi,al jady,al jadyah,al jafa,al jafadun,al jaffah,al jafi`ah,al jafnayn,al jafr,al jaghbub,al jah,al jah al a`la,al jah al asfal,al jahara,al jahda`ah,al jahf,al jahi,al jahilah,al jahili,al jahiliyah,al jahiliyih,al jahliyah,al jahmah,al jahr,al jahra,al jahrah,al jahsha,al jahshi,al jahshiyah,al jahu,al jahwari,al jajiyah,al jakhadim,al jala,al jala`ah,al jalabat,al jalamid,al jalatimah,al jalawiyah,al jalaylah,al jalb,al jalidi,al jaliha,al jalihah,al jalijah,al jalilah,al jallah,al jam`a,al jamah,al jamalah,al jamaliya,al jamaliyah,al jamamilah,al jamamiyah,al jamayilah,al jamhud,al jami`,al jami`ah,al jamil,al jamimah,al jamjamah,al jamlah,al jamliyah,al jamm,al jamma,al jammasah,al jammasiyah,al jamusiyah,al janain,al janainah,al janabiyah,al janad,al janadib,al janadilah,al janadiriyah,al janadiyah,al janbaiyah,al jandi,al janiya,al janiyah,al jannah,al jannat,al janubiyah,al janudiyah,al jar`a,al jarabi`,al jarad,al jaradah,al jaradat,al jaradiyah,al jaraf,al jarah,al jaramil,al jarasha,al jarashah,al jarawi,al jarayisah,al jarb,al jarba,al jarba al kabirah,al jarba as saghirah,al jarbub,al jarda,al jardad,al jardah,al jardhawiyah,al jarf,al jarh,al jari,al jaridat,al jariyah,al jarmaq,al jarnus,al jarr,al jarradin,al jarradiyah,al jarrah,al jarrahi,al jarraniyah,al jarub,al jaruba,al jarubah,al jarubah ad dar,al jarw,al jarwiyah,al jasamiyat,al jashshah,al jashu,al jasim,al jasrah,al jasurah,al jathmah,al jauhara,al jauhari,al jawair,al jawabi,al jawabir,al jawadiyah,al jawah,al jawahiliyah,al jawahiliyyah,al jawar,al jawashinah,al jawasim,al jawawidah,al jawayir,al jawb,al jawbah,al jawdafiyah,al jawf,al jawharah,al jawharah al `ulya,al jawharah as sufla,al jawhariyah,al jawhharah al `ulya,al jawiyah,al jawl,al jawli,al jawr,al jawran,al jawsaq,al jawsh,al jawsh al kabir,al jawsh as saghir,al jaww,al jawwadiyah,al jawwah,al jawwil,al jawz,al jawzah,al jawziyah,al jaya,al jayah,al jaybahi,al jayf,al jaylah,al jayli,al jayr,al jayrif,al jayyid,al jaz`ah,al jazair,al jazair,al jazayer,al jazazirah,al jazi,al jazi`,al jazir,al jazirah,al jazirah al `ulya,al jazirah al gharbiyah,al jazirah al hamra,al jazirah al khadra,al jazirah ar rabi`ah,al jazirah ash shaqra,al jazirah ash sharqiyah,al jazirah ath thaniyah,al jaziriyah,al jazrah,al jazzaniyah,al jazzazah,al jdaydah,al jdaydih,al jemima,al jezita,al ji`ami,al ji`awinah,al jib,al jiba`i,al jibab,al jibal,al jibbayn,al jibjab,al jiblah,al jibtan,al jid`,al jiddiyah,al jidfarah,al jidfirah,al jidir,al jidis,al jidr,al jifah,al jifarah,al jifayrat,al jihad,al jil,al jilab,al jilaniyah,al jilf,al jilh,al jima,al jima,al jimah,al jimaliyah,al jimi,al jinah,al jinaynah,al jindiriyah,al jinh,al jini,al jinih,al jinnat,al jirabiyat,al jiraf,al jirah,al jirahi,al jirari,al jirb,al jirb al asfal,al jirbab,al jirbah,al jirbayn,al jiri,al jirjan,al jirm,al jirmah,al jirri,al jirthamiyah,al jirwah,al jishm,al jishshah,al jisimah,al jisr,al jissa,al jissah,al jit,al jith,al jithamiyah,al jithyathah,al jiwar,al jiya,al jiya,al jiyah,al jizah,al jlayliyah,al jnaynih,al joghub,al jol,al jouf,al jowar,al ju`aydah,al ju`ayfar,al ju`ayfir,al ju`liah,al ju`liyah,al ju`ranah,al ju`udiyah,al jubah,al jubaila,al jubaniyah,al jubayb,al jubaybinah,al jubayhah,al jubayjib,al jubayl,al jubayl industrial city,al jubaylah,al jubaylat,al jubayliyah,al jubayn,al jubayrat,al jubayriyah,al jubb,al jubbah,al jubbat,al jubbi,al judaida,al judaydah,al judaydih,al judayrah,al judayya,al judayyidah,al judeida,al judhamiyah,al judhay`,al judur,al jufah,al jufaifa,al jufar,al jufaydiriyah,al jufayfah,al jufayr,al jufayrah,al jufiyah,al jufra,al jufrah,al juhayl,al juhaymah,al juhayr,al juhaysh,al juhfah,al juhu,al jukhadar,al julaybinah,al julaymah,al jull,al jullab,al jum`ah,al juma,al jumail,al jumaliyah,al jumayhah,al jumayl,al jumaylah,al jumayliyah,al jumaymah,al jumayyil,al jumhuriyah,al jummayzah,al jummu`iyah,al jumum,al jumun,al junanynah,al junaybat,al junayd,al junaydiyah,al junayfah,al junayfi,al junaynah,al junaynah fort,al jundi,al jundiyah,al junibah,al jurabah,al jurah,al jurak,al jurayb,al jurayba,al juraybah,al juraybat,al jurayd,al jurayf,al jurayfah,al juraymiqiyah,al juraynat,al jurayni,al juraysa,al juraysah,al jurayyat,al jurayyir,al jurayz,al jurbah,al jurbishiyah,al jurd,al jurdhawiyah,al jurdi al gharbi,al jurdi ash sharqi,al jurdi gharbi,al jurdi sharqi,al jurf,al jurf al janubi,al jurf ash shamali,al jurjan,al jurmudiyah,al jurn,al jurnah,al jurniyah,al jurub,al jus,al jussah,al jussah al janubiyah,al jussah ash shamaliyah,al juththah,al juthwah,al juwadah,al juwaibiyat,al juwaif,al juwar,al juwarah,al juwaybat,al juwaybiyat,al juwayf,al juwayfah,al juwayr,al juwayyidah,al juwayz,al juwharah as sufla,al juyushi,al juzayrah,al koy,al ka`aban,al ka`abi al jadidah,al ka`abi al qadimah,al ka`ban,al ka`biyah,al ka`d,al ka`mat,al kaba,al kabaish,al kababir,al kababish,al kabaliya,al kabar,al kabbah,al kabbah al hamra,al kabbashi,al kabirat al hayl,al kabirat hayl,al kabirat hayl al ghaf,al kabiri,al kabri,al kabrit,al kadadah,al kadadif,al kadah,al kadahaah,al kadahah,al kadakid,al kadamah,al kadan,al kadarah,al kadhah,al kadhari,al kadhimain,al kadid,al kadirah,al kaf,al kafa,al kafarat,al kafat,al kafla,al kafr,al kafr al bahri,al kafr al jadid,al kafr al qadim,al kafr al qibli,al kafr ash sharqi,al kafrah,al kafrayn,al kafrin,al kahafah,al kahar,al kahara,al kahfah,al kahhalah,al kahira,al kahla,al kahlah,al kahluniyah,al kailat,al kakhrah,al kalafid,al kalagi,al kalahah,al kalbah,al kalban,al kalbiyah,al kaleh,al kallabin,al kallahin,al kalmukh,al kalsah,al kama,al kama,al kamaishah,al kamah,al kamal,al kamal (nimrah sab`ah),al kamaniyah,al kamayishah,al kamil,al kamilah,al kamilin,al kamiliyah,al kamis,al kamlin,al kamshah,al kanais,al kanayis,al kandisiyah,al kanisah,al kansera,al kara,al kara`,al kara`imah,al karaa,al karaba,al karabah,al karabilah,al karad,al karak,al karakat,al karamah,al karanik,al kararim,al karashiwah,al karat,al karbah,al karbi,al karbus,al kardi,al kardud,al karib,al karif,al karimah,al kariya,al karkarai,al karkh,al karkit,al karm,al karm al gharbi,al karm ash sharqi,al karmah,al karn,al karnak,al karnish,al karori,al karouf,al karradah,al karradi,al karradiyah,al karradiyyah,al karsha,al karshah,al karus,al karyan,al karyun,al kasair,al kasha,al kashalifab,al kashalifah,al kashmah,al kasib,al kaslik,al kasrah al jadidah,al kasrah al qadimah,al katabiyah,al katami,al kathir,al katib,al katif,al katrun,al kattit,al kaura,al kawadi,al kawah,al kawahil,al kawahilah,al kawam,al kawamil,al kawamil bahri,al kawamil qibli,al kawbiliya,al kawbiliyah,al kawbliyah,al kawd,al kawdah,al kawi,al kawkabiyah,al kawlah,al kawm,al kawm al ahmar,al kawm al ahmar al bahri,al kawm al ahmar al qibli,al kawm al akhdar,al kawm al asfar,al kawm at tawil,al kawr,al kawrah,al kaya,al kaybaniyah,al kayd,al kaylah,al kaymah,al kayta,al kayyarah,al kazam,al kazirah,al kaziyah,al kazizah,al kazi\302\247imiyah,al kebara,al kereimet,al kerkour,al khabat,al khabbah,al khabbaz,al khabr,al khabra,al khabrah,al khabt,al khabura,al khaburah,al khadarah,al khadariyah,al khaddad,al khaddah,al khadhra,al khadhrawain,al khadie,al khadimiyah,al khadira,al khadirah,al khadr,al khadra,al khadra,al khadra al janubiyah,al khadra ash shamaliyah,al khadrah,al khadrah al janubiyah,al khadrah ash shamaliyah,al khadraniyah,al khadrawayn,al khadriyah,al khafaq al jadid,al khafaq al qadim,al khafiyah,al khafji,al khagene,al khaila,al khaisa,al khaji mahalleh,al khajjaf,al khakcha,al khala,al khala,al khalaifah,al khalaiq al gharbiyah,al khalaiq ash sharqiyah,al khalabiya,al khalaf,al khalafiyah,al khalal,al khalaq,al khalaqah,al khalas,al khalawat,al khalf,al khalfah,al khalfiyin,al khalidi,al khalidiyah,al khalif,al khalifah,al khalifat,al khalij,al khalil,al khalili,al khaliliyah,al khalis,al khalisa,al khalisah,al khallah,al khalt,al khaluf,al khalw,al khalwah,al khalwat,al khamaishah,al khamasah,al khamasin,al khambah,al khamila,al khamilah,al khamir,al khamis,al khamisiya,al khamisiyah,al khammasin,al khamrah,al khamsah,al khan,al khananisah,al khanaq,al khanasiri,al khanazir,al khandaq,al khanif,al khaniniya,al khaniq,al khankah,al khansak,al khansharah,al khanu,al khanuq,al kharaib,al kharaiq,al kharab,al kharabah,al kharabiyah,al kharadilah,al kharaj,al kharamilah,al kharan,al kharaniqah,al kharaqaniyah,al kharashah,al kharashat,al kharayib,al kharayij,al kharba,al kharbah,al kharfa,al kharfah,al kharib,al khariba,al kharibah,al kharif,al kharifi,al kharifut,al kharijah,al kharijiyah,al kharitiyat,al kharj,al kharjah,al kharkhayr,al kharm,al kharma,al kharma al janubiyah,al kharma ash shamaliyah,al kharmah,al kharrarah,al kharratah,al kharrub,al khars,al kharsa,al kharshah,al khartah,al khartum,al khartum bahr,al khartum bahri,al khasab,al khasaba,al khasal,al khasamah,al khasasif,al khasawiyah,al khasf,al khash,al khashab,al khashabiyah,al khashashinah,al khashm,al khashshaniyah,al khasibiyah,al khasirah,al khasmah,al khasr,al khass,al khatatibah,al khatayshiyah,al khathiyah,al khatilah,al khatiyah,al khatmah,al khatmiyah,al khatt,al khattabi,al khattarah,al khattarah al kubra,al khattarah as sughra,al khattiyah,al khatwah,al khaur,al khawabiri,al khawajat,al khawalid,al khawamis,al khawarnaq,al khawbah,al khawd,al khawhah,al khawi,al khawiya,al khawiyah,al khawjarah,al khawkah,al khawkhah,al khawr,al khawrah,al khawrajah,al khawsh,al khawwari,al khayail,al khayalah,al khaydar,al khayf,al khaymah,al khayr,al khayra,al khayrah,al khayran,al khayrat,al khayriyah,al khays,al khaysah,al khaysh,al khayt,al khayyalah,al khayyam,al khazain,al khazandariyah,al khazin,al khazrajiyah,al khazur,al khazzan,al khel,al khelafiyine,al khelalfa,al khibnah,al khidad,al khidhr,al khidr,al khilal,al khilalah,al khimarah,al khinaq,al khinsharah,al khirah,al khiran,al khirbah,al khirbah as samra,al khirs,al khis,al khisa,al khisaf,al khisah,al khiseh,al khishaybi,al khishl,al khiyam,al khiyami,al khiyarah,al khiyariyah,al khmissat,al khobar,al khora,al khraybih,al khu`ah,al khuba,al khubah,al khubar,al khubara,al khubayb,al khubayn,al khubaytiyah,al khubb,al khubbah,al khubra,al khubrah,al khudad,al khudariyah,al khudayrah,al khudayri,al khudayshah,al khudr,al khudrah,al khudud,al khufah,al khufayfah,al khufayfiyah,al khufayqi,al khuff,al khul,al khulab,al khulafiyah,al khulaq,al khulasah,al khulay`ah,al khulaybiyah,al khulaydiyah,al khulayfiyah,al khulaywah,al khulaywi,al khulfan,al khullah,al khulli,al khulqah,al khumaysiyah,al khums,al khunainiya,al khunayniyah,al khunayqiyah,al khunn,al khur,al khuraib,al khuraiba,al khurashibah,al khurayb,al khuraybah,al khuraybat,al khuraybiyah,al khuraymi,al khurays,al khuraysha,al khuraytah,al khuraytiyat,al khurayyib,al khurayzah,al khurbah,al khureba,al khurf,al khurj,al khurmah,al khurman,al khursa`ah,al khurshubah,al khurtum,al khurway`ah,al khusayt,al khushaybi,al khushaym,al khushnam,al khushniyah,al khushshah,al khushu`i,al khusus,al khutamah,al khutaym,al khutaymah,al khutaymi,al khuttah,al khutum,al khuwain,al khuwair,al khuwayjiah,al khuwayjiyah,al khuwayn,al khuwayr,al khuwayrah,al khuwayrat,al khuwayriyah,al khuways,al khuwaysat,al khuwayshah,al khuwaytilah,al khuzama,al khvorshid,al khzalat,al kiaisi,al kiber,al kibrit,al kibs,al kidadiyah,al kidf,al kidwah,al kiezi,al kifah,al kifl,al kihafah,al kihayfiyah,al kilabat ash sharqiyah,al kilabiyah,al kilabiyah al gharbi,al kilabiyah ash sharqi,al kilabiyat,al kilaniyah,al kilh,al kilh gharb,al kilh sharq,al kir,al kir`anah,al kirdi,al kiriya,al kirnah,al kirs,al kirsah,al kirsh,al kishash,al kishmah,al kishri,al kishta`ar,al kiswah,al kitar,al kiteyyab,al kitimah,al kitkatah,al kitta,al kittah,al kiwek,al knaysah,al knaysih,al kodab,al koin,al kola,al kont,al koualit,al koubou,al kouda,al koudo,al kouk,al kourimet,al ksar,al ksarah,al ku`,al ku`b,al ku`ub,al kub,al kuba,al kubab,al kubain,al kubaniyah,al kubaydah,al kubbah,al kud,al kudam,al kudayf,al kudayhah,al kudayrah,al kuddayah,al kuds,al kuduk,al kufa,al kufah,al kufayr,al kufrah,al kufrayn,al kufuf,al kufun,al kufur,al kuh,al kuhayf,al kuhayfiyah,al kuhef,al kuhwayr,al kulakhiyah,al kulayb,al kulaybah,al kulla,al kumah,al kumait,al kumaym,al kumayshat,al kumb,al kumur,al kunah,al kunaysah,al kunayyisah,al kunayyisat,al kunayz,al kunayzah,al kuntillah,al kunuz,al kur,al kura`,al kuram,al kurama,al kurama,al kuray,al kuray`at,al kuraykhat,al kuraym,al kuraymat,al kuraymat al bahriyah,al kuraymat al qibliyah,al kuraynik,al kuraytiyah,al kurb,al kurd,al kurdi,al kurdul,al kuren,al kureshi,al kurmashiyah al gharbiyah,al kurmashiyah ash sharqiyah,al kurnuk,al kurnuk muhammad `isa,al kurrah,al kurrathiyah,al kursi,al kurum,al kurumah,al kurumusia,al kusayb,al kusaybiyah,al kushh,al kussabat,al kusur,al kut,al kut al faranji,al kutamiyah,al kutaybah,al kutayr,al kuten,al kuwait,al kuwayfiyah,al kuwaykhat,al kuwayr,al kuwayrah,al kuwayt,al kuwayyah,al kuzal,al kuziyah,al kwairat,al kwashrah,al la`aqib,al la`ban,al la`ota,al laban,al labbah,al labbani,al labbunah,al labrag,al labraq,al labwah,al ladhiqiyah,al lafiyah,al lagawa,al laghfah,al lagowa,al lahabah,al lahabah al janubiyah,al lahabah ash shamaliyah,al lahibiyah,al lahm,al lahun,al lahunah,al laib,al lajamah,al lajj,al lajjun,al lakamah,al lakidah,al lakmah,al lamab,al lamamish,al laqan,al laqayit,al laqbah,al laqit,al laqitah,al laqluq,al laqyah,al lasafah,al lasb,al lasbah,al lasbah al `ulya,al lasbah as sufla,al lataminah,al latif,al latifiyah,al latrun,al lattaminah,al latum,al lawatah,al lawiyah,al lawzah,al layb,al laylakah,al layyah,al lazakah,al lazif,al lazraq,al lebeid,al libedat,al lidam,al lihabah,al lijak,al lijam,al lijmah,al limah,al liqa,al lisafah,al lisb,al lishman,al lisht,al lisi,al lith,al liwa,al lubayni,al lubban,al lubeik,al lughfiyah,al luhabiyah,al luhayyah,al lujj,al lukaymah,al lumayma,al lumi,al luqiyah,al luqmaniyah,al luwaybidah,al luwaydhiqiyah,al luwaymi,al luwayz,al luwayzah,al luwayziyah,al luweisah,al lwiziya,al maziz,al mttalah,al m`alliyah,al m`arras,al maabid,al maamil,al maana,al maayn,al maballa,al mai,al maish,al maiyah,al makhadh,al mamir,al mamuniyah ash shamaliyah,al maqar,al mazam,al mazim,al ma`abidah,al ma`abirah,al ma`adi,al ma`adin,al ma`ali,al ma`alla,al ma`allah,al ma`alwah,al ma`amiltayn,al ma`amir,al ma`amirah,al ma`an,al ma`aniyah,al ma`aqas,al ma`aqim,al ma`ar,al ma`arid,al ma`arif,al ma`arrah,al ma`arri,al ma`ashbah,al ma`asir,al ma`awil,al ma`awiz,al ma`ayin,al ma`azil,al ma`bad,al ma`badi 2,al ma`bar,al ma`barah,al ma`dan,al ma`dhar,al ma`dhar ash shamali,al ma`di,al ma`dilah,al ma`inah,al ma`irr,al ma`iyah,al ma`jalah,al ma`jun,al ma`la,al ma`labah,al ma`lah,al ma`mal,al ma`mar,al ma`murah,al ma`niyah,al ma`qas,al ma`qil,al ma`qul,al ma`ras,al ma`rash,al ma`rufiyah,al ma`rufiyyah,al ma`sar,al ma`sarah,al ma`sarah al mahattah,al ma`sha,al ma`shar,al ma`shuqah,al ma`shur,al ma`ushah,al ma`yanah,al ma`yarah,al ma`yuf,al ma`yuf jama`a,al ma`yuf jama`ah,al ma`zabah,al ma`zibah,al maarif,al maaziz,al mab`ujah,al mab`uthah,al mabarr rasul,al mabarraz,al mabda`,al mabda`ah,al mabdhal,al mabiyah,al mabkariyah,al mabna,al mabni,al mabrad,al mabrak,al mabraz,al mad,al madaih,al madain,al madabah,al madabi`,al madabigh,al madad,al madaf,al madafin,al madahikah,al madahiyah,al madakhinah,al madamud,al madan,al madaniah,al madaniyah,al madarah,al madawar,al madawir,al madaya,al madayah,al madbaah,al maddah,al madfun,al madhabah,al madhah,al madhalim,al madhan,al madhatiyah,al madhba,al madhiq,al madhun,al madi,al madid,al madilah,al madillah,al madina,al madinah,al madinah al fikriyah,al madinah al jadidah,al madinah al munawwarah,al madinah as sina`iyah,al madinah as siyahiyah,al madinat ash sha`b,al madiq,al madla`ah,al madmanah,al madmar,al madnaiyah,al madniyyah,al madra,al madrab,al madrakah,al madrasah,al madrubah,al madrujah,al madrukah,al madu,al madudah,al madwar,al madyunah,al mafalis,al mafaza,al mafazah,al mafier,al mafijar,al mafjar,al mafour,al mafraq,al mafri,al mafrur,al mafsal,al magasibah al bahriyah,al maghal,al maghar,al magharah,al magharib,al magharibah,al magharim,al maghasil,al maghir,al maghmumah,al maghrabah,al maghrabi,al maghrafiyah,al maghras,al maghribi,al maghrit,al maghrur,al maghtis,al maghzub,al magrun,al magwa,al mahabbah,al mahabishah,al mahad,al mahad al a`la,al mahad al awsat,al mahad as asfal,al mahadah,al mahadhi,al mahadhibah,al mahadin,al mahadir,al mahaff,al mahagar,al mahaji,al mahajibah,al mahajir,al mahajir ash sharqiyah,al mahal,al mahalabe,al mahalabi,al mahalani,al mahali ash sha`awiyah,al mahall,al mahall at tayn,al mahallah,al mahallah al kubra,al mahallatayn,al mahalli,al maham,al mahamid,al mahamidah,al mahamil,al mahani,al mahaqirah,al mahariq,al mahash,al mahasinah,al mahasinah al gharbiyah,al mahasir,al mahathith,al mahatt,al mahattah,al mahawil,al mahawiya,al mahawiyah,al mahbashi,al mahbulah,al mahda,al mahdabiyah,al mahdah,al mahdali,al mahdam,al mahdarah,al mahdayn,al mahdha,al mahdi,al mahdiyah,al mahfad,al mahfazi\302\247,al mahfid,al mahfidh,al mahi,al mahil,al mahilah,al mahillah,al mahjabah,al mahjal,al mahjam,al mahjar,al mahjil,al mahjub,al mahjul,al mahjur,al mahlaj,al mahm,al mahmara,al mahmud,al mahmudiya,al mahmudiyah,al mahmuri,al mahnawiya,al mahnawiyah,al mahqari,al mahraf,al mahraq,al mahraqah,al mahras,al mahrum,al mahruqah,al mahruqat,al mahrurah,al mahrusah,al mahsamah al jadidah,al mahsamah al qadimah,al mahshush,al mahsim,al mahsus,al mahuya,al mahuz,al mahwa,al mahwit,al mahyul,al mai`ah,al maia,al maiyeh,al majabirah,al majadil,al majahisah,al majall,al majalliyah,al majami`,al majan ash shumah,al majar al kabir,al majaridah,al majarin,al majarish,al majarr al kabir,al majassah,al majatah,al majaz,al majaz ash sharqi,al majazir,al majba,al majbah,al majd,al majdal,al majdirah,al majhafa,al majhafah,al majhifah,al majidah,al majidiyah,al majil,al majir,al majira,al majlis,al majma,al majma`ah,al majnab,al majra,al majrab,al majwi,al majwiyah,al majza`,al majza`ah,al majzab,al mak ilyas,al mak muhammad,al makailab,al makanah,al makarib,al makarin,al makarishah,al makdar,al makdash,al makhaid,al makhabi,al makhadah,al makhadimah,al makhadir,al makhalif,al makhaliqah,al makhamarah,al makhbutah,al makhle,al makhlutah,al makhnuq,al makhruq,al makhshab,al makhtabiyah,al makhul,al makhwah,al makhzan,al makhzan al fawqi,al makhzan al qa`i,al makhzene,al makili,al makinah,al makman,al maknuniyah,al maks,al maks al bahri,al maksuriyah,al maktar,al maktilah,al makzam,al makzum,al mal,al mal`ab,al malab,al maladda,al malah,al malahah,al malahit,al malahiyah,al malamm,al malaqah,al malaqah raqm 1,al malaqah raqm 2,al malaqi,al malasin,al malaz,al malbanah,al malbiyah,al maleq,al malha,al malha,al malham,al mali,al maliha,al malihah,al malihe,al malikah,al maliki,al malikiyah,al malikiyin al qibliyah,al maliyah,al mallah,al mallahah,al mallahah al bahriyah,al mallahah al gharbiyah,al mallat,al mallulah,al maloua,al malqa,al malqama,al malqamah,al malua,al malwah,al malwi,al mammon,al mamsha,al mamsukah,al mamsurah,al mamtalah,al man`um,al manais,al mana`iyah,al manadhir,al manadji,al manahiz,al manakh,al manakhir,al manalla,al manama,al manamah,al manaqil,al manar,al manara,al manarah,al manashi,al manashi wa `arramiyat al khudayri,al manashilah,al manashsh,al manasib,al manasir,al manasirah,al manatir,al manawat,al manayil,al manayyah,al manaza,al manazil,al manazi\302\247ir,al manbukh,al mancouriya,al mandam,al mandarah,al mandarah bahri,al mandarah qibli,al mandub,al mandur,al mandurah,al manfash,al manhar,al mani`,al mani`iyah,al manifiyah,al manjaf,al manjarah,al manqa`,al manqaf,al manqam,al manqam al a`la,al manqam al asfal,al manqar,al manqata`,al manshah,al manshi,al manshiah,al manshiyah,al manshiyah al bahriyah,al manshiyah al ibrahimiyah,al manshiyah al jadidah,al manshiyah al qibliyah,al manshiyah as sughra,al mansiyah,al mansouriyah,al mansu`ah,al mansura,al mansurab,al mansurah,al mansuri,al mansuriah,al mansuriya,al mansuriyah,al mansuriyyah,al mantar,al mantarah,al manubiyah,al manyal,al manzalah,al manzil,al manzilah,al manzi\302\247ar,al maqa`itah,al maqalim,al maqam,al maqani`,al maqaqi,al maqarin,al maqarisah,al maqarr,al maqasi`,al maqasibah al bahriyah,al maqasibah al qibliyah,al maqasid,al maqati`,al maqatin,al maqawibah,al maqayaw,al maqbabah,al maqfa`,al maqfal,al maqhayah,al maqla`,al maqlubah,al maqnah,al maqra`ayn,al maqrah,al maqran,al maqranah,al maqrun,al maqsa,al maqsha,al maqsus,al maqtu`,al maqtu`ah,al maqwa`,al maqwazi,al mar`amah,al mara`ah,al mara`uneh,al marabi,al marabid,al marabit,al maradisin,al maradiyah,al marafiz,al maragh,al maraghah,al marah,al marahidah,al marami,al maramid,al maran,al marari,al marasfah,al marashi,al marashidah,al marasib,al marawi`ah,al marawigh,al marawih,al maraziq,al maraziqah,al marbak,al mardh,al marfa,al marfu`ah,al marhinah,al mari,al maridmy,al marir,al marirah,al maris,al marj,al marj al akhdar,al marjah,al marji,al marjum,al markaz,al markh,al markhiyah,al markiyah,al markula,al markulah,al markuz,al marma,al marmaghah,al marmuthah al janubiyah,al marodle,al marqab,al marr,al marranah,al marsa,al marsaba,al maruabi,al marwa,al marwah,al marwahah,al marwaniyah,al marwi as sufah,al maryah,al marzamah,al mas`adin,al mas`am,al mas`udi,al mas`udiya,al mas`udiyah,al masa`id,al masabah,al masabigh,al masahib,al masahilah,al masajid,al masaliyah,al masallamiyya,al masana,al masani`,al masanih,al masaqirah,al masarrah,al masawir,al masayqah,al masbar,al masda`iyah,al masdur,al masfalah,al mash,al mash`aliyah,al mash`ib,al mashai`ah,al mashaikh,al masha`ibah,al masha`ilah,al mashaba,al mashaer,al mashaf,al mashahidah,al mashahira,al mashahirah,al mashakhrah,al mashara,al mashargah,al masharifah,al masharih,al masharij,al mashariqah,al masharta,al mashashitah,al mashat,al mashati,al mashati shelbarak,al mashatiyah,al mashawidah,al mashay`ah,al mashayibah,al mashayikh,al mashhad,al mashnaq,al mashnaqah,al mashquq,al mashquqah,al mashra`,al mashrab,al mashraf,al mashrafah,al mashrafiyah,al mashrah,al mashramiyah,al mashriq,al mashru`,al mashta,al mashtayah,al mashur,al mashwashin,al mashwushin,al mashyakhah,al masia,al masid,al masid al abyad,al masif,al masil,al masilah,al masinah,al maskan,al maskhan,al masla,al maslab,al maslakh,al maslub,al maslubah,al masluhah,al maslukh,al masna,al masna`,al masna`ah,al masnamah,al maso,al masqa,al masrab,al masraf al gharbi,al masrib,al masrubah,al massarah,al mastabah,al mastabah al gharbiyah,al mastabat al gharbiyah,al mastumah,al mastur,al masufiyin,al masufiyun,al maswah,al maswal,al masyaf,al mat`afiyah,al matabb,al matahif,al matahin,al matahirah al bahriyah,al matahirah al qibliyah,al matahirah ash sharqiyah,al matahrah al bahariyah,al matallah,al matamah,al matamir,al matamma,al matammah,al mataniyah,al matannah,al matarah,al matariayah,al matariqah,al matariyah,al matawi`ah,al matawrad,al matayin,al mathluthah,al mathna,al mathnah,al matka,al matla`,al matlah,al matlin,al matmurah,al matn,al matnah,al matrad,al matrah,al matrakiyah,al matrat,al matrif,al matu`ah,al matun,al matunah,al matur,al matwah,al matwiyah,al mawaddah,al mawahi,al mawahib,al mawahibah,al mawajid,al mawajin,al mawali,al mawatin,al mawbid,al mawdhar,al mawjar,al mawkib,al mawlid,al mawqa`,al mawqar,al mawsaf,al mawsaqah,al mawsatah,al mawsif,al mawsil,al mawsil al jadidah,al may`ah,al mayadin,al mayah,al mayam,al mayamin,al mayanid,al mayasam,al maybiliyah,al maydan,al mayfa`,al mayfa`ah,al mayl,al maymun,al maymunah,al mayqa`,al mays,al maysah,al maysari,al mayyah,al mayyitah,al mazaf,al mazar,al mazarah,al mazari`ah,al mazarib,al mazariyah,al mazbal,al mazghunah,al mazhal,al mazharah,al mazi\302\247alim,al mazi\302\247hariyah,al mazki,al mazlah,al mazlaq,al mazra`,al mazra`a,al mazra`ah,al mazra`ah al qibliyah,al mazra`ah ash sharqiyah,al mazra`at al qibliyah,al mazri`,al mazru`,al mazrub,al mazzah,al mazzih,al mazzunah,al mcharq,al mdini,al mdint,al medel,al medina,al mediq,al medodel,al medou,al megargaf,al megeifa,al mehaouza,al mehrate,al melassa,al melemm,al mellah,al menofali,al merard,al merbah,al merban,al merer,al meridmy,al merj,al mers,al mes,al mesdoura,al mesharah,al messin,al metlaoui,al meydeybjib,al mgarda,al mharbiyah,al mi`ayzibah,al mi`dan,al mi`mar,al mi`mariyah,al mi`qas,al mi`raj,al mi`sarah,al mi`tan,al mi`zab,al mi`zal,al miallug,al mibha,al mibrad,al mibrayah,al mibu,al midad,al midah,al midamman,al midan,al midfanah,al midhnab,al midhya,al midman,al midodil,al midsim,al midya,al midyah,al mifa,al mifa,al miftah,al migbab,al migeiqa,al mighlaf,al mighraq,al mighraqah,al mighzal,al mihal,al mihawab,al mihawal,al mihayniyah,al mihdajah,al mihi,al mihnawiyah,al mihrab,al mihrath,al mihsam,al mihsha,al mihti,al mij`arah,al mija`ar,al mijann,al mijar al kabir,al mijasas,al mijazah,al mijba,al mijbah,al mijda,al mijhal,al mijifa,al mijlad,al mijza`,al mikhal,al mikhdash,al mikhlaf,al mikhraf,al mikhsa,al mikhwah,al miknasi,al mikrab,al mikras,al miksha,al mil`ab,al milah,al milahah,al milala,al milaytaniyah,al mileh,al milh,al milhah,al milhani,al milji,al milk,al millah,al mimja,al mina,al mina,al minah,al minasafur,al minbiya,al mindak,al mindaq,al mindassah al gharbiyah,al mindassah ash sharqiyah,al miniarah,al minjalah rakhamah,al minjarah,al minniyah,al minsab,al minsaf,al minshar,al minshat al jadidah,al minshat al kubra,al minshat as sughra,al minshilayn,al mintarah,al mintera,al mintirib,al minwab,al minwab al qadim,al minya,al minyah,al minzalah,al minzifah,al miqallah,al miqash,al miqat,al miqdab,al miqdadiyah,al miqdamiyah,al miqranah,al miqraq,al miqsab,al miqsal,al miqshab,al miqtar,al miqyadah,al mirad,al mirdashah,al mirdasiyah,al mirekh,al mirewa,al mirsah,al mirsah wa al khashashnah,al mirsas,al mirwah,al miryah,al mirza,al mirzam,al misah,al misandah,al misar,al misawiyah,al misayri raqm ithnayn,al misayri raqm wahid,al misbar,al misfah,al mish`ab,al mishaf,al mishal,al mishan,al mishash,al mishbab,al mishhat,al mishillahah,al mishkhab,al mishmash,al mishmat,al mishqa`ah,al mishqalah,al mishraf,al mishrah,al mishrak,al mishraq,al mishshah,al mishtah,al mishwaf,al misin,al miski,al mislal,al mislanah,al mismar,al mismiyah,al misna,al misnah,al misqalah,al misqash,al misrab,al misraba,al misrah,al misrakh,al misri,al mitbabah,al mith`ab,al mithaf,al mitmar,al mitraq,al miyah umiah,al miyah wa miyah,al miyasah,al mizah,al mizghaf,al mizra,al mizrab,al mlaguit,al mnasra,al mnayra,al moecs,al mohammad,al moja,al mokhtar ben hamdane,al moktar,al moqassab,al mosna`a,al mour,al moura,al mourad,al mourgag,al mraj,al mraysah,al mrej,al mrhassiine,al mstiri,al mtaylah,al mtullah,al muanasah,al muminin,al mu`a,al mu`abba,al mu`abbadah,al mu`addaliyah,al mu`addamiyah,al mu`addiyah,al mu`adhdham,al mu`ah,al mu`allah,al mu`allam,al mu`allaqah,al mu`arqab,al mu`arras,al mu`aybdi,al mu`aydi,al mu`ayinah,al mu`ayliq,al mu`ayqilat,al mu`aysim,al mu`aysir,al mu`aysirah,al mu`ayzibah,al mu`ayzilah,al mu`ayziyah,al mu`tamadiyah,al mu`tarid,al mu`taridh,al mu`walah,al muari,al muashar,al mubarak,al mubarakiyah,al mubarikiyah,al mubarraz,al mubaydi`,al mubid,al mudah,al mudaibi,al mudairib,al mudari,al mudariba,al mudaribah,al mudarra`ah,al mudarraj,al mudawwar,al mudawwarah,al mudaybi,al mudayli,al mudayq,al mudayrib,al mudayrij,al mudayyih,al mudayyirah,al mudayzah,al mudayzhah,al mudbar,al mudhaylif,al mudhnib,al mudirah,al mudmin,al mufajah,al mufarrijiyah,al muflisah,al mufqar ash sharqi,al mufrihat,al muftayhah,al mugfil,al mughadiyah,al mughaiyir,al mugharah,al mughaydifiyah,al mughaydir,al mughayir,al mughaynah,al mughayr,al mughayra,al mughayrah,al mughayrfat,al mughayrifat,al mughayriyah,al mughaysil,al mughayyir,al mughayyiri,al mugheidir,al mughir,al mughiri,al mughraq,al mughtibah,al muhaddad,al muhafhaf,al muhaijir,al muhajirin,al muhallabiyah,al muhammad,al muhammadi,al muhammadiyah,al muhammarah,al muhandisin,al muharraq,al muharraqa,al muharraqah,al muhassin,al muhawal,al muhawwalah,al muhawwatah,al muhaydhifah,al muhaydithah,al muhayjir,al muhaylibah,al muhayniyah,al muhaysim,al muhaytah,al muheina,al muhmudiyah,al muhram,al muhsam,al muhsin [1],al muhsin [2],al muhtaqarah,al muhtaraqah,al muhtariq,al muhtariqah,al muhus,al mujaffaf,al mujahhiz,al mujahidin,al mujamma`,al mujammah,al mujassas,al mujaydil,al mujayhiyah,al mujaylah,al mujaylis,al mujayri,al mujayrin,al mujlad,al mukahhal,al mukalla,al mukallis,al mukanbil,al mukarib,al mukarram,al mukaybah,al mukaydis,al mukayfitah,al mukayl,al mukayli,al mukaymil,al mukayminiyah,al mukha,al mukhailif,al mukhallad,al mukhallid,al mukhanjif,al mukharim,al mukhasi`ah,al mukhaybah,al mukhaybah al fawqa,al mukhaydir,al mukhayli,al mukhaylif,al mukhayrif,al mukhayriyan,al mukhaysah,al mukhazin,al mukhtabiah,al mukhtabiyah,al mukhtalatah,al mukhtalif,al mukhtarah,al mukhtari,al muknin,al muktabiyah,al muladdah,al mulaihiyah,al mulaqqatah,al mulaybij,al mulayh,al mulayha,al mulayhah,al mulayhat,al mulayhiyah,al mulaylah,al mulaylih,al mulayniyah,al mulayshah,al mulaytah,al mulayyanah,al mulayyinah,al mulla `alwan,al multasa,al mumattalah,al mumi,al mumsam,al munadi,al munaizilah,al munajah,al munajah al kubra,al munajat al kubra,al munajat as sughra,al munaqqab,al munasirah,al munassar,al munastir,al munayhilah,al munaytirah,al munayyar,al munayzilah,al munayzir,al munayzi\302\247irah,al munba`ath,al mundassah,al mundhiriyah,al munif,al munirah,al munjiyah,al munqalabah,al munsif,al muntar,al muntarib,al muntazah,al munyah,al muq`ashiyah,al muqa`bariyah,al muqabalayn,al muqabba`,al muqadimah,al muqannaf,al muqari`iyah,al muqarib,al muqarmadah,al muqarqar,al muqarrabiyah,al muqassam,al muqata`ah,al muqatifiyah,al muqatilah,al muqaybilah,al muqaybirah,al muqaylibah,al muqayr,al muqayribah,al muqaysirah,al muqayti`,al muqbil,al muqr,al muqri,al muqsha`,al muqtar,al muquzi\302\247ah,al murab`in,al murabba`,al murabba`ah,al murabi`in,al muraddam,al muradiyah,al muradiyah al qadimah,al murair,al muraji,al muraqqa`ah,al muraqqadah,al murayba,al muraydah,al murayghah,al murayj,al murayjah,al murayjat,al muraykhah,al muraykib,al murayqi,al murayqib,al murayr,al muraysiyah,al muraytibah,al murayyi`ah,al murdi,al murifah,al murnaqiyah,al murqab,al murrah,al murrayah,al mursilat,al murtafa` al kabir,al murtafa`ah,al muruj,al murziyah,al musa,al musa`adah,al musabbih,al musabihiyah,al musaiyib,al musalla,al musallab,al musallakhah,al musallamiyah,al musana`a,al musandaq,al musawwadiyah,al musaybik,al musaybir,al musaybiyah,al musayfirah,al musayh,al musayjid,al musaylihah,al musaymir,al musayni`ah,al musayqah,al musaytbah,al musaytibah,al musaywiniyah,al musayyib,al musayyid,al musbah,al musein,al musha`ibah,al mushahidah,al mushallahah,al mushannaf,al mushaqqar,al mushargah,al musharrah,al musharrak qibli,al musharraq,al mushash,al mushawar,al mushay`il,al mushaykhi,al mushaylihah,al mushayrafah as saghirah,al mushayrat,al mushayrifah,al mushayrifawi,al mushayritah,al mushaytiyah,al mushazzib,al mushir,al mushirah,al musil,al musin,al musiyah,al muski,al muslab,al muslakhah,al musrarah,al mustadirah,al mustafawiyah,al mustajidah,al mustajiddah,al mustarihah,al muta`amd,al muta`iyah,al mutairifi,al mutaiwiyah,al mutall,al mutallah,al mutaribah,al mutarrif,al mutarrifiyah,al mutawassitah,al mutawwan,al mutawwa\302\261`ah,al mutaylah,al mutaylib,al mutayn,al mutayniyat,al mutayrifi,al mutayriyah,al mutaywi,al mutaywi ash shamali,al mutaywil,al mutaywiyah,al muthab,al muthallath,al muthallith,al muthanna,al muthawwar,al muthminah,al muti`ah,al mutib muqsad,al mutillah,al mutrad,al mutrif,al muttrad,al mutun,al muwaffaqiyah,al muwaijh,al muwaijid,al muwanisiyah,al muwansah,al muwaqqar,al muwassam,al muwayh,al muwayh al qadim,al muwayjid,al muwayjir,al muwaykir,al muwaylah,al muwaylighah,al muwaylih,al muwaylihat,al muweiqi,al muyyah,al muzabb,al muzahimiyah,al muzaira,al muzaqqar,al muzara`ah,al muzarrafiyah,al muzaybilah,al muzaydiyah,al muzaylif,al muzayrah,al muzayra\302\261`ah,al muzayri`,al muzayri`ah,al muzayrib,al muzayyan,al muzayyanah,al mwelda,al naman,al na`imah,al nabhan,al nadir,al nahar,al najagh,al naman,al naqub,al nashshatah,al nasr allah,al nawariyah,al nazhah,al nazu`,al nijifa,al nuqaia`h,al obayyid,al ocar,al odayya,al oddaiya,al odeid,al ogala,al ogla,al otro lado,al oubana,al ouradirha,al passo,al piano,al qaim,al qair,al qa`,al qa`abiyah,al qa`ah,al qa`ar al a`la,al qa`dah,al qa`id,al qa`idah,al qa`iyah,al qa`mah,al qa`qa`,al qa`qiyah,al qa`qur,al qa`rah,al qa`udah,al qab`iyah,al qabasiyah,al qabbari,al qabbariyah,al qabdayn,al qabi`i,al qabil,al qabilah,al qabr,al qabun,al qaburiyah,al qabuti,al qabw,al qada,al qadab,al qadad,al qadam,al qadarif,al qadawi,al qaddabah,al qaddabi,al qaddahat,al qaddahiyah,al qadhadhimah,al qadhaf,al qadhawi,al qadhf,al qadhif,al qadhima,al qadi,al qadibah,al qadimah,al qadiriyah,al qadisiyah,al qadiyah,al qadmus,al qadr,al qafailat,al qafalat,al qafilat,al qafiriyah,al qafla kafla,al qaflah,al qafqaf,al qafrah,al qahab,al qahar,al qaharah,al qahasat,al qahb,al qahfah,al qahfah at tawil,al qahir,al qahira,al qahirah,al qahizi,al qahlah,al qahm,al qahmah,al qahtaniyah,al qaiyara,al qala,al qal`ah,al qal`ah al kabirah,al qal`ah al kubra,al qal`ah as saghirah,al qal`ah as sughra,al qal`i,al qalai`,al qala`,al qalabis,al qalaj,al qalamun,al qalashi,al qalawat,al qalaytah,al qalbiyah,al qalibah,al qalil,al qalilah,al qalis,al qallabat,al qalluf,al qalt,al qaltah,al qalyubiyah,al qam`ah,al qama,al qamadir,al qamah,al qambariyah,al qambh,al qambur,al qamh,al qamishli,al qammanah,al qammatiyah,al qamri,al qamu`,al qan`abah,al qan`ah,al qana`ah,al qanab,al qanafidh,al qanaila,al qanamah,al qanat,al qanatir,al qanatir al khayriyah,al qanatirin,al qanawat,al qanawis,al qanayah,al qanayat,al qanfar,al qanha,al qanhah,al qanisah,al qanna`bah,al qannabah,al qannarah,al qantarah,al qantarah al gharbiyah,al qantarah ash sharqiyah,al qantari,al qar`a,al qar`ah,al qar`awn,al qar`un,al qara,al qarain,al qara`ah,al qarad,al qaradah,al qaradin,al qaraghuliyah,al qarah,al qarahin,al qarahinah,al qaramitah gharb,al qaramus,al qarandah,al qaraqif,al qaraqirah,al qaraqrah,al qarar,al qararah,al qarashim,al qarawayn,al qarawi,al qaraya,al qarayshah,al qard,al qardabah,al qardad,al qardahah,al qarfa,al qarfi,al qarha,al qari,al qarihah,al qarin,al qarina,al qarinah,al qarinayn,al qariyah,al qarmadah,al qarn,al qarn al abyad,al qarnayn,al qarqar,al qarquf,al qarramah,al qarrami,al qarsa,al qartu`iyah,al qarutiyah,al qarya,al qaryah,al qaryah al `asriyah,al qaryah al gharbiyah,al qaryah al ishtirakiyah,al qaryah al markaziyah,al qaryah al qibliyah,al qaryah ar rabi`ah ar raisiyah,al qaryah ash sharqiyah,al qaryah bi ad duwayr,al qaryah raqm 6,al qaryat,al qaryat al gharbiyah,al qaryat ash sharqiyah,al qaryatayn,al qarzi\302\247abah,al qas`a,al qas`iyah,al qasab,al qasabah,al qasabah ash sharqiyah,al qasabat,al qasabi,al qasabiyat,al qasad,al qasara,al qasayriyah,al qasbiyah,al qasbiyat,al qash`a,al qash`ah,al qasha,al qashabah,al qashah,al qashar,al qashashiyah,al qashbib,al qashi`,al qashimiyah,al qashshah,al qashshish,al qashtah,al qasi,al qasi`ah,al qasim,al qasimiyah,al qasmitayn,al qasmiyah,al qasr,al qasr al akhdar,al qasr al jadid,al qasr as sa`id,al qasr wa as sayyad,al qasrayn,al qasriyah,al qass,al qassad,al qassasin,al qassasin al qadimah,al qassiya,al qastal,al qastrum,al qat`,al qat`a,al qat`ah,al qata,al qatai`,al qataba,al qatan,al qatanah,al qataniyah,al qatar,al qatayi`,al qati`,al qatif,al qatlabah,al qatn,al qatrana,al qatranah,al qatrun,al qatshah,al qatta,al qattar,al qattarah,al qattawiya,al qattawiyah,al qattayshah,al qattin,al qaw`i,al qawadir,al qawadirah,al qawarishah,al qawasim,al qawati,al qawazi`ah,al qawba,al qawba`iyah,al qawd,al qawim,al qawl,al qawmah,al qawqar,al qawr,al qaws,al qawsah,al qawz,al qawz kabaro,al qawzah,al qawzayn,al qay`iyah,al qayat,al qayd,al qayf,al qayfi,al qaylah,al qaym,al qaymah,al qayqab,al qayqar,al qayraf,al qayrawan,al qays,al qaysariyah,al qaysumah,al qaytanah,al qaytun,al qayyarah,al qayzah,al qazaqizah,al qazzahah,al qbab,al qbayyat,al qcac,al qeima,al qela,al qeqar,al qib,al qibab al kubra,al qibab as sughra,al qibabat,al qibi`,al qiblah,al qibli qamula,al qiddamiyah,al qidhan,al qidrah,al qidrawiyah,al qifaf,al qiflah,al qihaf,al qihati,al qima,al qimmamin,al qinawiyah,al qir`awn,al qiratiyah,al qirinah,al qirran,al qirsh,al qirshan,al qirw,al qisamah,al qisfah,al qismah,al qiss,al qist,al qit`ah,al qitan,al qitarayn,al qitat,al qitena,al qiyadi,al qizah,al qlay`ah,al qmayzri,al qolid bahri,al qor,al qowez,al qoz,al qrayyah,al qtaba,al qu`ah,al qu`ayyir,al qu`mah,al qu`tubah,al qub`,al qub`a,al qub`iyah,al quba`,al qubab,al qubabat,al qubaiba,al qubay`,al qubayb,al qubaybah,al qubaysat,al qubayyah,al qubayyat,al qubayyil,al qubbah,al qubeiba,al qubsah,al qubur,al qudadah,al qudah,al qudayh,al qudaymah,al qudayr,al quddabi,al qudmah,al quds,al qufaifa,al qufailat,al qufar,al qufayf,al qufayfah,al qufaylat,al qufays,al qufl,al quflah,al quful,al quhuqiyah,al qulad,al qulay`ah,al qulay`at,al qulayb,al qulaybayn,al qulayd bahri,al qulayd qibli,al qulaylah,al qulaytah,al qulayyib,al qulayyibah,al qullah,al qulmah,al qulzum,al qumqum,al qumra,al qumrah,al qunaytira,al qunaytirah,al qunayyah,al qunbur,al qundhuf,al qunfidha,al qunfidhah,al qunfudhah,al quni,al qunnah,al qur`an,al qur`aniyah,al qura,al quradat,al quradiyin,al qurah,al quraimisa,al qurain,al quraiyat,al quras,al qurashi,al qurashiyah,al quratiyin,al quray`ah,al qurayb,al quraymisah,al qurayn,al qurayn al jadidah,al qurayn wa tawahin al haysamiyah,al qurayqarah,al qurayr,al quraysah,al qurayshi,al qurayya,al qurayyah,al qurayyat,al qurdah,al qurdud,al qurf dhibah,al qurh,al qurhah,al quriyah,al qurmah,al qurna,al qurna`ah,al qurnah,al qurnah al hamra,al qurnain,al qursat,al qurt,al qurti,al qurtiyah,al quru`,al qurun,al qus`ah,al qusaiba,al qusariyah,al qusay`ah,al qusaybah,al qusaybi,al qusaybiyah,al qusayfiyah,al qusaymah,al qusayr,al qusayyirah,al qusbat,al qusbiyah,al qush,al qushari,al qushashiyah,al qushlah,al qusiyah,al qustantiniyah,al qusur,al qusuriyah,al qut`ah,al qutay`,al qutayb,al qutayfah,al qutayn,al qutaynah,al qutaytirah,al qutb,al qutnah,al qutrani,al quturi,al quwah,al quwam,al quwara,al quwarah,al quway`,al quway`iyah,al quwayjiyah,al quwayr,al quwayrah,al quwaysi,al quwaysimah,al quwayz,al quwayzah,al quwayzat,al quwwah,al quz,al quz`ah,al quza`,al quzah,al rab,al rabasah,al rada`i,al rado,al rafi`ah,al rahaba,al rahaibah,al rahhaliya,al rahmaniyah,al raiyan,al rakoab,al ramayh,al ramayl,al ramsa,al raq,al raudah,al raudha,al rawayd,al rawdah,al rayyan,al raziqiryyah,al rbeil,al rhachrhacha,al rhannto,al rihib,al rija`,al riyah,al robat,al rubat,al ruhaiya,al ruqayyiqah,al russ,al s`adan,al sa`idiyah,al saad,al sabah,al sabat,al sabbah,al sabih,al sabikha,al sabikhah,al sabti,al sadat-e nejat,al safiq,al saghir,al sahayn,al sahra,al said,al saiyid hani as saiyid hadi,al salamah,al salayyah,al salhaymis,al salil,al salim,al salma,al saramayn,al sarkha,al sasso,al satan,al satwa,al sawad husen,al sawadiyyah,al sawda,al sayyah,al sayyan`,al selimat,al shari,al sha`ibah,al shab,al shadid,al shafi`ah,al shahaniyah,al shaharin,al shamiya,al shamlah,al shams od din,al sharaf,al sharjah,al shawa,al shayban,al shess,al shibl,al shinas,al shuwaikh,al shuwara,al sih,al silabah,al sirhan,al sirrain,al sohar,al solivo,al sormeh,al subayhah,al sudan,al sufayrat,al suraha,al suray`,al suwaihiliya,al suwaihiliyah,al suwari,al suwayda,al suwayharah,al ta`aniq,al tabaqah,al tabieh,al taftish al awwal,al tahoe,al tahrir,al tamam,al tanabikah,al tawalab,al tawila,al tawilah,al tawlah,al thagab,al thaqab,al thaqbah,al thaybat,al thirm,al tuhaimiyah,al tuzliyah,al u`aywir,al ubaija\302\261`i,al ubayd,al ubayja\302\261`,al ubaytir,al ubayyid,al udayyah,al udayyat,al udhailiya,al ughaydirah,al ughaylif,al uglah,al uhaymir,al uhaymirat,al ujrush,al ukaz,al ukhani,al ukhaydir,al ukla,al umara,al umbarak,al umbarakab,al umla`aya,al umnaythir,al unaysiyah,al unjura,al uqiyah,al uqlah,al uqsur,al urman,al urqub,al urus,al usayfir,al usaylah,al usayli`,al usha,al usni,al utakah,al uthaithiya,al uthaylat,al uthmaniyah,al uthrat,al uwaidat,al uzair,al uzayriq,al uzayriqab,al vakkhfud,al wa`adilah,al wa`arah,al wa`b,al wa`diyah,al wa`irah,al wa`rah,al wabrah,al wad,al wadi,al wadiyayn,al wafa,al wafaiyah,al wafaydah,al wafi,al wafrah,al wagbah,al waghi,al wahabah,al wahah,al wahat,al wahbah,al wahdah,al wahhal,al wahlah,al wahliyah,al wahliyah al kabirah,al wahliyah as saghirah,al wahman,al wahrah,al waht,al wajba,al wajbah,al wajd,al wajh,al wajihiyah,al wajirah,al wajr,al wak,al wakhmah,al waki,al wakrah,al wala,al walaja,al walajah,al wali,al walid,al walidiyah,al walijah,al wanaisah,al wannan,al waqat,al waqaybah,al waqb,al waqbah,al waqdah,al waqf,al waqf wa al qilaminah,al waqid,al waqra,al waqrah,al wara,al waradia,al waraq,al wardani,al wardanin,al wardaniyah,al wardiyah,al wardiyan,al wardiyat,al warhaniyah,al warik,al warishiyah,al warith,al warka,al warka,al warkah,al warr,al warral,al warraq,al wasait,al wasat,al wasf,al wasfiyah,al washa,al washam,al washash,al washi`ah,al washm,al washqah,al washwash,al wasi`ah,al wasib,al wasil,al wasili,al wasiliyah,al wasiliyeh,al wasit,al wasitah,al waslatiyah,al wasmiyah,al wast,al wasta,al wastani,al wastaniyah,al wat,al wata,al watadah,al watah,al watan,al watat,al watawit,al wataya,al wathan,al wathba,al watiah,al watiya,al watiyah,al watwat,al watyah,al wawi,al wayasifah,al wayiliyah,al wazi`iyah,al waziriyah,al waziyah,al wazz,al wazzani,al widy,al widy wa kafr ad dismi,al widyan,al wigh,al wihdah,al wija,al wijhah,al wila,al windiyah as saghirah,al windiyat as saghir,al wisam,al wisayah,al wisha,al wishkh,al wizarat,al wu`ayli,al wu`liyah,al wubayriyah,al wuday,al wuday`,al wuday`ah,al wudayy,al wudiyah,al wuhayt,al wukair,al wukayr,al wulayjat,al wuqay`,al wuqaysh,al wuqayyat,al wuqbah,al wuqid,al wurud,al wusa`,al wusail,al wusayd,al wusayl,al wusayta,al wusayyi`ah,al wusfan,al wusta,al wutah,al wyg,al ya,al ya`abir,al ya`aqib,al ya`ar,al ya`qubiyah,al ya`rubiyah,al yabisah,al yadida,al yadudah,al yafnasah,al yahmum,al yahudiya,al yahudiyah,al yalba,al yaljah,al yamama,al yamamah,al yaman,al yamaniyah,al yammunah,al yamun,al yaqoussa,al yaqutiyah,al yarmuk,al yarut,al yasiniyah,al yasir,al yasmin,al yasu`iyah,al yatra,al yatrah,al yazid,al yazidiyah,al yitarah,al yunis,al yusufiyah,al za`ir,al zabyah,al zafiq,al zahir,al zahrah,al zarib,al zaut,al zikri,al ziyad,al zuhrah,al zumamiyah,al zuwaydiyah,al zuwayr,al ba-kendy,al linguette,al saute,al-syt,alband,albanglyu,albikheyl,albilag,alborkau,alby,alcha-bulak,alchagi,alchakioy,alchamyulk,alchin,alday,aldzhan-adyrovskaya,alfredavas,alfredovo,algamchi,algoy,algui,alin,alkadzhar,alkozi,alkozukalay,alksnenay,alma-bulak,alma-suvan,almabulak,almabulok,almanbyuruk,almas,almasang,almased,almaskha,almaskheyl,almasy,almata,almatay,almene,almesh,alovgrob,alpaysay,alpiyskoye,alsedzhyay,alshanka,alshin,alsyay,alt kartsevishken,alt sheken,altya,alt-letva,altak,altamur,altan,altarangak,altargana,altarganagak,altarganak,altargangak,altargu,altatu,altigi,altkhof,altkyula,altvayde,alula,alva,alvakul,alvar,alvend,alviltsy,alvove,alyakheyl,alyashishkyay,alyava,alzhan,alzhanka,al- `us,al-ajaimah,al-arf,al-udaid,al-uqaimah,al-`ain,al-`akejla,al-`alaimah,al-`aun,al-`awda,al-`obed,al-`udeid,al-`uqaidah,al-`uqail,al-`uraisimah,al-`utair,al-al,al-alinao norte,al-alinao sur,al-aluding,al-alutin,al-atrun,al-badar colony,al-badr colony,al-bahra,al-baida,al-baida,al-baidhah,al-baqoorah,al-baqrain,al-batanah,al-bed`,al-bidde,al-binah,al-bu-kamal,al-buhairah,al-buqairain,al-buqrain,al-buraik,al-buraiyirah,al-bureij,al-butaih,al-buwairidah,al-csill,al-dab`ah,al-dabah,al-dahirah,al-dahma,al-dahqah,al-daimah,al-dakhul,al-dali`ah,al-dar,al-darb,al-dauj,al-dawadimi,al-dhabirah,al-dhaifir,al-dhayd,al-dhira`,al-dhubaiyah,al-dijan,al-dikak,al-diman,al-dir`iyya,al-diwan,al-diyaimah,al-diyaiqah,al-djasra,al-djufayr,al-dufah,al-duru`,al-e adami,al-e behamdan,al-e bonis,al-e bu hardan-e sofla,al-e bu na`im,al-e darvish,al-e gholami,al-e habib,al-e hajji,al-e hashem,al-e hashem-e bala,al-e hashem-e sofla,al-e kabud,al-e khvorshid,al-e mahmud,al-e mohammad,al-e safi-ye pain,al-e sefi-ye bala,al-e shams od din,al-e tayyeb,al-e yusefi-ye `olya,al-e yusefi-ye bala,al-e yusefi-ye pain,al-e yusefi-ye sofla,al-e yusofi,al-e yusofi-ye `olya,al-e yusofi-ye bala,al-e zia od din,al-e zia od din,al-emjarin,al-fahaheel,al-fahahil,al-faisal town,al-faisaliya,al-falah,al-farawaniyah,al-farwaniyyah,al-fatah colony,al-fateh town,al-fauwaliyah,al-fijair,al-finaitees,al-firsh,al-fiysh,al-flah town,al-foqaha,al-fudhul,al-fudjayra,al-fustat,al-getamijje,al-gfejfe,al-ghaidah,al-ghaidah al khabra,al-ghaidah al-khadra,al-ghail,al-ghardaqa,al-ghil,al-ghudr,al-ghuraib,al-ghuwaidah,al-ghuwair,al-goz,al-hadita,al-hadjaren,al-hadn,al-haffeh,al-hailah,al-hair,al-haira,al-hajar,al-hajarain,al-hajjanijje,al-hamad,al-hamaishi,al-hamla,al-hanu,al-harjah,al-hasakeh,al-haush,al-hautah,al-hfene,al-hfer,al-hiffeh,al-hijail,al-hijajah,al-hijlain,al-hinu,al-hirah,al-hiraith,al-hisi,al-hiswa,al-hrajbe,al-hudaibah,al-hudaid,al-hulailah,al-humaimah,al-humairah,al-huraidah,al-huwailah,al-huwaimi,al-imran town,al-jabiyat al-`ulya,al-jabiyat al-sufla,al-jaharah,al-jaif,al-jamil,al-jaru,al-jauf,al-jauharah,al-jayizah,al-jezair,al-jihail,al-jinainah,al-jishshan,al-jof,al-jol,al-jubail,al-juraibat,al-kahma,al-kamishly,al-kasab,al-kasr,al-kaudah,al-kaurah,al-kayrawan,al-kemaniya,al-kfar,al-khabourah,al-khadud,al-khaisa,al-kharashar,al-kharga,al-kharidjiyya,al-khaur,al-khudairah,al-khuraibah,al-khureba,al-khuruf,al-kifah,al-kira`,al-kom,al-ksejbe,al-kubib,al-kulaib,al-kunfudha,al-kuraibiyah,al-kurdaha,al-kuseir,al-kutaifeh,al-kuwaifia,al-lakita,al-lal ben mesaud,al-lalabang,al-langigan,al-lay,al-laziqiyah,al-luhayya,al-ma`ber,al-ma`la,al-madayih,al-mahfad,al-malikiyya,al-mamoun city,al-mangaf,al-maqharim,al-mariyya,al-mariyya al-gharbyya,al-mariyyah,al-masna`a,al-mausaf,al-mayadeen,al-mazhur,al-mese,al-miaizbah,al-mi`aizibah,al-mihtariqah,al-minba`ath,al-minbaith,al-mirfa,al-mishqat,al-msufiyin,al-mughainah,al-muhaidhifah,al-muhammarah,al-muharrak,al-mukattam city,al-mukharram fokani,al-mulaibij,al-mulailah,al-munaizirah,al-muqairibah,al-muqaisirah,al-musaini`ah,al-mustafa town,al-muzghumah,al-mweleh,al-nabek,al-nabidah,al-nahal,al-najafa,al-naqa`ah,al-nashash,al-naslah,al-nigaiyat,al-nihay,al-nijil,al-nimas,al-nu`air,al-nughsh,al-nuhaid,al-nujaidain,al-nukhr,al-nukhsh,al-nukhush,al-nuqaiyib,al-nuqub,al-nur colony,al-nur town,al-qabdain,al-qanatarah east,al-qantarah gharb,al-qatarah,al-qauz,al-qulaita,al-qulaitah,al-qunah,al-qurain,al-qusah,al-quwairah,al-rabasah,al-rabwah,al-rahaibah,al-rahm,al-raidah,al-raiyan,al-rakh,al-rakka,al-ramlah,al-rastan,al-raudah,al-raughah,al-raukab,al-rawda,al-rawshan,al-rawuk,al-rayana,al-razale,al-ribat,al-ridahah,al-rifa` al gharbi,al-rifa` al-shamali,al-rifa` al-sharki,al-riqqah,al-risan,al-rishah,al-riyaidah,al-riyan,al-rol,al-ronah,al-ruba`iyah,al-rubah,al-rubat,al-rudud,al-rukab,al-rukaib,al-rumaidah,al-rumaytha,al-ruwaidah,al-sa`d,al-sabahiyah,al-sabikhat,al-sabya,al-safah,al-safira,al-safra,al-sailah,al-saiq,al-sala,al-salamah,al-salhabiyah,al-salt,al-salum,al-samadiyah,al-samha,al-sanad,al-saq,al-sarwah,al-saum,al-saumaah,al-sayh,al-sayyidah zaynab,al-shaab city,al-sha`bah,al-shaghi,al-shahi,al-shakhura,al-sham,al-sharaqi,al-shardj,al-sharfa,al-sharirah,al-sharj,al-sharq,al-sharqiyah,al-sheikh-bader,al-sheraqi,al-shihr,al-shillat,al-shiqq,al-shiqq al sharqi,al-shu`bain,al-shuaiba,al-shubaikah,al-shujain,al-shukayk,al-shuqaiq,al-sib,al-sidara,al-sifaiyah,al-sihib,al-simah,al-sitara,al-skeilbyeh,al-subahi,al-suda`,al-sufal,al-sulaibikhat,al-surr,al-suwaidif,al-suwairi,al-sweida,al-taif,al-tabbaqah,al-tal,al-talh,al-tarabah,al-taraf,al-tarif,al-tariyah,al-thawra,al-thijjah,al-tur,al-turbaibil,al-tuwaithir,al-tzu-tou,al-waswas,al-wuja,al-wulaijat,al-wuzid,al-zaghfah,al-zahir,al-zahr,al-zallak,al-zau`,al-zimrah,al`adan,al`amiriyah,al`aridah,al`asharah,al`awaqilah,al`izai,al`ltan,al`uwayli,ala,ala (1),ala (2),ala agba,ala bach,ala bash,ala buna,ala chaman,ala chapan,ala daghlu,ala daglii,ala damb,ala dei sardi,ala di stura,ala el ma,ala farz `ali,ala gen,ala ghair saiyid ahmad,ala hago,ala jergah,ala jergah kalay,ala joojeh,ala kabud,ala kari,ala khurshid,ala khuyeh,ala khvorshid,ala klise,ala kylmanen,ala oji,ala parakka,ala parkkila,ala rontyla,ala sar,ala serki,ala serkin,ala shehr,ala singhwala,ala syayniye,ala tanemole,ala tanemole zhen,ala uuksu,ala vuotunki,ala yizuxiang,ala-ed-din,alai,alain-e absardeh,alauddine bala,alauddine pain,ala-aguly,ala-ahoada,ala-ajagbusi,ala-ajagbusin,ala-ala,ala-annala,ala-bash,ala-bekly,ala-buka,ala-dzhida,ala-hanikase,ala-isa,ala-jarve,ala-jarvere,ala-juma,ala-kap,ala-kay-lyak,ala-kelva,ala-khago,ala-khanikaze,ala-khatyn,ala-klisse,ala-kotajarvi,ala-kul,ala-kuusemajarvi,ala-lappfors,ala-malmi,ala-mikkulainen,ala-ogbo,ala-partala,ala-puulonki,ala-raisala,ala-raumo,ala-sarkilahti,ala-soltkoski,ala-sotku,ala-suhka,ala-syaynio,ala-taagepera,ala-tammijarvi,ala-tup,ala-ud-din ke,ala-vagula,ala-viirre,ala-viksenka,alaaddin,alaagac,alaaha,alaak,alaalacadka,alaas,alaatin,alaattin,alaattinbey,alab,alaba,alaba egi,alaba kulito,alaba kolito,alabaan,alabadin,alabado,alabafi,alabag,alabahama,alabak,alabakan amututu,alabako,alabaksapur,alabakul,alabal,alabala,alabalik,alabam,alabama,alabama camp,alabama city,alabama fork,alabama hill,alabama hills,alabama junction,alabama landing,alabama port,alabama shores,alabama village,alabama wharf,alabameta,alaban,alabana,alabang,alabang hills village,alabang puro,alabanisi,alabar,alabarda,alabarda koyu,alabas,alabash konrat,alabashevo,alabashly,alabasli,alabaster,alabaster junction,alabat,alabata,alabata market,alabato,alabay,alabayir,alabaytal,alabe,alabebe,alabedir,alabegovci,alabel,alaberdino,alabertino,alabey,alabeyli,alabi,alabia,alabiao,alabidaik,alabidham,alabidhan,alabidi,alabidon,alabidun,alabin,alabino,alabio,alabiye,alabja,alablaboi,alabo,alabodarna,alabog,alabokazo,alabolaboe,alabonyu,alabornya,alabota,alabuchinka,alabuela,alabug,alabug east,alabug west,alabuga,alabugday,alabuke,alabukhino,alaburdishki,alaburdiskes,alabusa,alabushevo,alabuzino,alaby,alabyshevo,alac,alaca,alacaac,alacaalan,alacaalti,alacaat,alacaatli,alacabayir,alacabayir koy,alacabuk,alacacesme,alacadag,alacadagkoyu,alacaginkisla,alacahaci,alacahamidiye,alacahan,alacahan mezraasi,alacahuyuk,alacak,alacakaya,alacakilise,alacakotal,alacakoy,alacakoyu,alacalar,alacali,alacam,alacamderesi,alacamderesi koyu,alacamescit,alacamezar,alacamezar koyu,alacami,alacami koyu,alacan,alacano,alacaoglu,alacaoluk,alacaoren,alacap,alacapinar,alacardirli,alacasar,alacat,alacati,alacatlazala,alacay,alacayar,alacaygan,alacayir,alacesme,alach,alach nizhniy,alach verkhniy,alacha,alacha-bar,alachabof,alachadyrly,alachah kotal,alachai,alachakh,alachakowtal,alacham,alachani,alachapan,alachaposa,alachay,alachehir,alachehr,alachevo,alachildu,alachini,alachino,alachkovo,alachua,alacici,alacik,alaclise,alacocha,alacon,alaconi,alacop,alacoto,alacran,alacranes,alacska,alacskai-koszenbanyatelep,alacskaiszenbanya,alacuas,alaculsy,alad,alada,aladaa,aladaalii,aladaaliy,aladad,aladadkalay,aladadkhel,aladadkheyl,aladag,aladaglii,aladagliy,aladana,aladaneh,aladanlii,aladarmajor,aladash,aladassicodji,aladatpur,aladd khan,aladdin,aladdin city,aladdin village trailer park,alade,alade-makei,aladekake,alademehin,aladeniya,aladere,aladere mogaji,aladere ode,aladhajo,aladhinon,aladhinou vounou,aladi,aladie,aladie ile,aladikaddaikadu,aladikattaikatu,aladiken,aladikha,aladikulam,aladim,aladin,aladin karez,aladin kariz,aladinggbene,aladinici,aladino,aladiou,aladipur,aladiro,aladiwembu,aladizgeh,aladja,aladja omia,aladjaba,aladjarakola,aladje,aladji,aladji ali,aladji arou,aladji dango,aladji omar,aladji saido,aladji sami,aladjiri,aladjove,alado,aladodo,aladog,aladon,aladorin,alados khel,aladoskhel,aladoskheyl,aladowskheyl,aladren,aladu,aladum,aladun,aladura,aladuz,aladuzi,aladyevka,aladyevo,aladzha,aladzhalii,aladzhaliy,aladzhani,aladzharga,aladzhi,aladzhii,aladzhilar,aladzhirga,aladzhirga-kalay,aladzhudzha,aladzo,alae,alaeddin,alaejos,alaer,alaerh,alaerma,alaettin,alaettinbey,alaettinkoy,alaf,alaf khan,alaf khan kor,alaf khel,alafa,alafan,alafara,alafe,alafenk,alafeyevo,alafia,alafiakrou,alafiarou,alafiarou-koredorou,alafissa,alafiyarou,alafiyaru,alafo,alafon,alafors,alafose,alafoss,alafsafed,alafu,alafuutow,alafwala,alag,alag erdeni,alag erdeni suma,alag erdeni sume,alag hololiin ortoo,alag hotoliin ortoo,alag khululoyn urto,alag khutuloyn urto,alag khutulyyn urto,alagozitsa,alagozler,alag-erdene,alag-erdeni somon,alag-erdeni-sumu,alag-shulun,alag-tutulyyn,alaga,alagache,alagaci,alagacico,alagadari,alagadico,alagadico grande,alagala,alagalkanda,alagalla,alagalla kondagama,alagalla pahalagama,alagamar,alagan,alagangan,alaganik,alagao,alagapuram,alagarno,alagarno i,alagarno ii,alagarno mousgoum,alagavur,alagaza,alagazili,alagazlar,alagba,alagbabiri,alagbabri,alagbado,alagbado-ile,alagbaefe,alagbaf,alagbafama,alagbafame,alagbagba,alagbakofe,alagbakorpe,alagbame,alagbana,alagbayun,alagbe,alagbede,alagbede faji,alagbo,alagboloe,alagbon,alagchhatra,alagel,alagel-myulkyulyu,alageris,alages,alagez,alagezmazra,alaggou,alagh,alaghan,alaghzar,alagi,alagi major,alagiapandipuram,alagici,alagidere,alagiderekoy,alagiin sume,alagiligobisi,alagin do,alagin sume,alagina reka,alaginci,alagingay,alagir,alagirka,alaglo,alaglo kope,alagna,alagna valsesia,alagni,alagnion,alagnon,alago,alagoa,alagoa da serra negra,alagoa de baixo,alagoa de roca,alagoa do monteiro,alagoa grande,alagoa nova,alagoado,alagoas,alagoda,alagogo,alagogo oke,alagogo weru,alagoinha,alagoinhas,alagoinhas de santana,alagollewa,alagomlek,alagon,alagonia,alagori,alagovac,alagoz,alagozbanisi,alagozler,alagozmazra,alagper,alagram,alagua,alagueces,alaguia,alaguisoc,alagun,alagunciftligi,alagund,alaguney,alaguntan,alaguodo,alagutan,alagutan oniya,alaguvat,alaguy,alaguz,alaguz-e sofla,alaguzeh,alaguzovo,alagyaz,alagyoz,alagyun,alagyun-bulgarsko,alagyun-tursko,alagzar,alah,alah bakhsh,alah darreh-ye bala,alah din,alah koh,alah kuzi,alah molk,alah sang,alah say,alah set,alah seto,alah valley,alah waraio,alah warayo pauhar,alaha,alahaara,alahabad,alahaci,alahacili,alahag,alaham,alahan,alahan koy,alahananggang,alahanbadil,alahankaloeang,alahankaluang,alahanli,alahanpandgan,alahanpandjang,alahanpanjang,alahapperumagama,alahari,alaharma,alahasiai,alahe,alahena,alahenegama,alahenpita,alahi,alahidir,alahimang,alahina,alahine,alahitiyawa,alahkaria,alahkheyl,alahkowlek,alahkuh,alahmer,alahna,alaho,alahog,alahoke,alahonkajoki,alahsang,alahsang-e `olya,alahsang-e sofla,alahtiyan,alahuistlan,alahuixtlan,alahun,alahurttanen,alahyar khosa,alai,alai bidirpur,alai chulai,alai nagla,alaia,alaiarpur,alaibei,alaichaung,alaid,alaidan,alaido,alaigeri,alaigne,alaikallupodda alankulam,alaikallupoddakulam,alaikha,alaikkalluppoddakulam,alaikola,alailee,alaili,alaili dadda`,alaimbei,alaimbeis,alaina,alainao norte,alainao sur,alaincourt,alaincourt-la-cote,alaingni,alaingni-atet,alaipur,alaipura,alairac,alairo,alais,alaise,alaitoi,alaiwi,alaiyabiagba,alaiye,alaiza,alaizo,alaja,alajaasko,alajag,alajar,alajargha,alajarghah,alajarodulo,alajaroduloi tanyak,alajarvi,alajata,alajbegon odzak,alaje,alajegerd,alajere,alajero,alaji,alaji yoro,alajiq,alajo,alajoe,alajoenmylly,alajogun,alajoki,alajoshazatanya,alajuela,alajuelita,alajuja,alajujeh,alak,alak chhatra,alakma,alaka,alaka pupa,alaka sakassou,alakaanta,alakabad,alakabo,alakabud,alakac,alakadaray,alakadari-argandab,alakadari-atgar,alakadari-bala-bluk,alakadari-gelan,alakadari-kadzhaki,alakadari-khodzha-umri,alakadari-naka,alakadari-narkh,alakadari-sarobi,alakadari-shakhdzhoy,alakadari-shibar,alakadari-shumulzay,alakadari-shvak,alakadari-sultani,alakadari-yakhyakheyl,alakadari-yusufkheyl,alakadi,alakadikurtleri,alakadimuhacirleri,alakae,alakaga,alakagama,alakah,alakah jar,alakai,alakajar,alakak,alakak xiang,alakala,alakalh,alakali,alakalukue,alakambato,alakamis,alakamisinandrianovona,alakamisy,alakamisy anativato,alakamisy fandrandava,alakamisy isorana,alakamisy itenina,alakamisy sahave,alakamisy-ambohijato,alakamisy-ambohimaha,alakamisy-ambohimahazo,alakamisy-fandandrave,alakamys,alakamysh,alakan,alakanda,alakandupitiya,alakang,alakani,alakanou,alakanuk,alakao,alakar,alakara,alakarbo,alakarga,alakarppa,alakasing,alakaso,alakasoho,alakasu,alakat,alakate,alakati,alakavaktepe,alakawul,alakaya,alakaye,alakayevka,alakayevo,alakaygan,alakaynak,alaka\302\277aa\302\277a,alaka\302\277aka\302\277,alakchi,alakchin,alakci,alakdi,alakdia,alakdiar,alakdzhar,alake,alakeci,alakecili,alakeh,alakel,alakel kyrykly,alakent,alakese koyu,alakey,alakeyritty,alakh,alakha,alakhadari-daman,alakhadze,alakhadzhigar,alakhadzi,alakhadzy,alakhai,alakhamak,alakhanchally,alakhashakhu,alakhashkhu,alakhebich,alakhel,alakheyl,alakhmadatli,alakhsan,alakhshan,alakhudarkhbakh,alakhyarly,alaki,alaki fonua,alakia,alakic,alakieri,alakija,alakilisa,alakilise,alakilise koyu,alakin,alakina,alakince,alakinci,alakinde,alakir,alakirsi,alakit,alakitka,alakiyevo,alakjhar,alakjhari,alakkang,alakkangama,alakkange,alakke,alakkopru,alakli,alakn,alaknik,alako,alakoc,alakoclu,alakoe,alakoeang,alakoh,alakohia,alakokh,alakol,alakol,alakola-anga,alakola-ela,alakoladeniya,alakolaella,alakolagalagama,alakolamada,alakolamade,alakolamaditta,alakolawatta,alakolawewa,alakolek,alakolik,alakonak,alakope,alakouakrou,alakounda,alakova,alakowe,alakowzay,alakoy,alakoyo,alakoyritty,alakoyun,alakozai,alakozay,alakpa,alakpa kope,alakpeti,alakple,alakra,alakriz,alakro,alaksary,alakshay,alakskiye,alakskiye settlement,alaktayev,alaku,alakuang,alakuh,alakuja,alakuka,alakuko,alakul,alakula,alakulaaare,alakulaare,alakuona,alakura,alakurshak,alakurti-tuntsa,alakurtti,alakus,alakusa,alakusak,alakuylak,alakuzu,alakwa,alakwababo,alakyla,alakyula,alakyulya,alakzay,alakzi,alal,alal diabari,alala,alala village,alala-ariam,alalaanila,alalaba,alalabo obenikiri,alalafo,alalah,alalak,alalan,alalan-e jadid,alalan-e qadim,alalapadu,alalay,alalay chica,alalay grande,alale,alalech,alalechi,alalefou,alaleh gurab,alaleh posht,alalehto,alaleri,alalevi,alalevy,alali,alalia,alalil fatil,alalima,alalivo,alalkomenai,alallaga,alallutan,alalnaba,alalo,alaloa,alalon,alalpardo,alalpur,alaltu,alalu,alalubosa,alaludig,alaluko,alalum,alaluosta,alaluostari,alaluwa,alalykino,alam,alam abrejo,alam bhatti,alam biditar,alam brahui,alam chachar,alam chinasi,alam dhuddi,alam drakhan,alam gena,alam gul khel,alam ka ker,alam kalar,alam kapri,alam karez,alam katma,alam khan,alam khan batafi,alam khan bhurgari,alam khan gopang,alam khan jato,alam khan jatoi,alam khan juneja,alam khan khosa,alam khan kili,alam khan mughairi,alam khan nizamani,alam khan rind,alam khan shahr,alam khan thahin,alam khan zardari,alam khanwala,alam khar talpur,alam khel,alam khel kats,alam khelan,alam khelanwala,alam kheyl,alam kili,alam kot,alam mahteke,alam majidani,alam punjabi,alam rajri,alam shah,alam shah khagga,alam shah khel,alam shah taja,alam shaikh,alam sher,alam sheri,alam sherwala,alam tan koruna,alama,alamaa,alamabad,alamabatak,alamabatak koy,alamabatak koyu,alamada,alamadi,alamadin,alamag,alamagui,alamakayis,alamaki,alamakolo,alamala,alaman,alaman garhi,alamana,alamance,alamance hills subdivision,alamanda,alamandarealastate,alamandiougoukaha,alamando,alamandyougoukaha,alamane,alamani,alamania,alamannia,alamannos,alamanou,alamao,alamar,alamari,alamasisli,alamasli,alamasovo,alamassou,alamata,alamati,alamatin,alamaya,alamba,alambar,alambare,alambare ouro alifone,alambari,alambary,alambay,alambayskiy,alambee,alambekkheyl,alambi,alambihud,alambijod,alambijud,alambique,alambong,alambra,alambradas,alambre,alamburo,alambuur,alamchob,alamchu,alamchug,alamchugh,alamcu,alamcugh,alamdanga,alamdar,alamdar but,alamdi,alamdia,alamdo,alame,alame agher,alame chak,alameda,alameda de alvarez,alameda de la sagra,alameda de osuna,alameda del obispo,alameda del siboney,alameda del valle,alameda juarez,alameda vieja,alamedilla,alamedilla del berrocal,alamedin,alamedita,alamein,alamela,alamelek,alamelik,alamero,alamescit,alamescit koyu,alamese,alamesek,alamesno,alamestan,alamet,alamettin,alamganj,alamgaon,alamgarh,alamgay,alamgi,alamgir,alamgul kili,alami banda,alami dere,alamicamba,alamikamba,alamillo,alamillo de galeano,alamillos,alamin saie,alaminata,alamindah,alamingoro,alamini,alamino,alaminos,alaminya,alaminyo,alamiran,alamishe,alamito,alamito los palmitos,alamitos,alamkabad,alamkach,alamkhankala,alamkhankalay,alamkhel,alamkheyl,alamlay,alamli,alamlik,alamnagar,alamo,alamo alto,alamo bonito,alamo chapa,alamo chapo,alamo chapo viejo,alamo chico,alamo de san benito,alamo heights,alamo hueco,alamo mocho,alamo oaks,alamo placita,alamo tortuga,alamo village,alamoar,alamog,alamogordo,alamoique,alamoisa,alamoki,alamokki,alamol,alamon,alamor,alamorio,alamos,alamos altos,alamos bravos,alamos chapo,alamos cuates,alamos de cerro prieto,alamos de marquez,alamos de martinez,alamos de pena,alamos de san antonio,alamos martinez,alamosa,alamota,alamou,alamoutala,alamovka,alamovo,alamovtsi,alamowzi,alampanjang,alampil,alampra,alampur,alampura,alampura imla,alampurkhera,alamraya,alamsa,alamsang,alamsar,alamsari,alamshah purwa,alamsherwala,alamshir,alamsri,alamtala,alamtaw,alamtou,alamtow,alamtu,alamu,alamua,alamucha,alamudun,alamun,alamuonio,alamuru,alamut,alamwala,alan,alan `abbasi,alan chibuya,alan debe,alan goristan-i daudi,alan kairiak,alan kairjak,alan kajrak,alan khan,alan khan-jo-goth,alan kodrani,alan koy,alan makhle,alan sacjun,alan stinicki,alan yaylasi,alan-baltay,alan-bekser,alan-e `olya,alan-e bala,alan-e pain,alan-e sofla,alan-kajrjak,alan-kayryak,alan-polyan,alan-yelga,alan-zire,alana,alana panhwar,alanabad,alanabao,alanaljanka,alanallur,alanampa,alanap,alanaq,alanas,alanaset,alanbahcesi,alanbalut,alanbasi,alanbato,alanbe,alanbo,alanbya,alanca,alancana,alancao basud,alancheng,alancik,alancilar,alancuma,aland,aland bala,aland pain,alanda,alandaing,alandale,alandar,alande,alander,alandi,alandi-devachi,alandino,alandiz,alando,alandoa,alandoor,alandroal,alandsbro,alandskiy,alandskop,alandskoye,alandsryd,alanduo,alandur,alandurai,alanduwaka,alanduzu,alandzhievo,alane,alanen kassa,alanenviita,alaneque,alang,alang alang,alang bombon,alang qu,alang shah,alang-alang,alang-alang selatan,alang-alang utara,alang-alangan,alang-alangkemangi,alang-besar,alang-dang,alang-e `ali beyg,alang-ketjil,alang-lama,alanga,alanga i,alanga ii,alangabo,alangakaula,alangalang,alangalangamba,alangalangan,alangalangdowo,alangalangdowo kidul,alangalangdowo lor,alangalanglanang,alangalangombo,alangalangpulo,alangalong,alangamba,alangamba dua,alangamba satu,alangan,alangana,alanganallur,alanganga,alangangkee,alangari,alangarua,alangasel,alangasi,alangasil,alangaula,alangayam,alangdunhku,alange,alangesh,alangga,alanggilan,alanghiyeh,alangi,alangidanga,alangiem,alangigan,alangilan,alangilanan,alangilang,alangilangon,alangir,alangiyam,alangkajenggede,alangkarakeng,alangkecil,alanglama,alanglawas,alango,alangollu,alangon,alangonen,alangong,alangoua,alangouanou,alangouassi,alangouassou,alangsukahaji,alanguiyeh,alangua,alanguasu,alangudi,alanguide,alanguigan,alangulam,alangwei,alangyrtly,alanhimmetler,alanhosa,alani,alania machay,alanib,alanices,alanici,alaniemi,alanikro,alaninkoro,alanis,alanish,alanjan,alanjaq,alanjareq,alanje,alanjeh,alanjeq,alanjin,alanjino,alanjoni,alanka,alankarapanguwa,alankarayagama,alankare,alankarpur,alankash,alankebir,alanken,alankeni,alankent,alankerney,alankerny,alankesh,alankiyi,alankoy,alankoyu,alankoz,alankuda,alankulam,alankulama,alankus,alanli,alanlikoyu,alanmulla,alannay,alanne,alanno,alano,alano jokhio,alanoba,alanogan,alanong,alanos,alanozu,alanpinar,alanreed,alansa,alansagir,alanseyhi,alanshile,alansi,alanskoye,alanson,alanta,alantas,alantaya,alante,alantepe,alantes,alanthus,alanthus grove,alanthus hill,alantic city,alanto,alanton,alantrara,alanur,alanurmo,alanvale,alanwa,alanya,alanyaylasi,alanyazi,alanyolu,alanyurdu,alanyurt,alanzeh,alanzu,alao,alao-ao,alaoa,alaocha,alaoeran,alaogbe,alaoglu,alaoglu ciftligi,alaopanna,alaou,alaou bassi,alaousso,alaoutem,alaouzadn,alap,alapars,alapa,alapa market,alapaa,alapaakkola,alapadi,alapaha,alapaikha,alapakam,alapake,alapako,alapala,alapaladeniya,alapalava,alapalawa,alapalawala,alapalawela,alapalli,alapamawaw,alapamawo,alapamowo,alapan i,alapan ii,alapan primero,alapan segundo,alapandi,alapang,alapankvara,alapapo,alapara,alaparu,alaparun,alapasco,alapata,alapaxa,alapayevka,alapayevsk,alapdere,alapdi,alape,alapela,alapelit,alapepe,alapera,alapete,alapi resz,alapiha,alapihlaja,alapikha,alapillai,alapinac,alapinar,alapinni,alapitka,alapli,alaplibolucek,alaplikocaali,alaplisofular,alapo,alapocas,alapoke,alapolai,alapoti,alappakkam,alappang,alappula,alappuzha,alapraia,alapsingh challisa,alaptira,alaptura,alapulai,alapur,alapurskaya,alapusti,alaqadar mohd-sarif,alaqadar muhammad sharif,alaqadari gulestan,alaqadari khasrod,alaqadari sahjoy,alaqadari swak,alaqadari warsaj,alaqadari-i-almar,alaqagha,alaqaq,alaqayah,alaqehdari khvajeh `omri,alaqehdari-ye nakah,alaqudari bak,alaqudori bak,alaques,alaquez,alaquines,alar,alar del rey,alara,alara iju,alara ile,alaraba,alarachi,alarada,alaran,alarang,alaranta,alarara,alarasan,alarauma,alaray,alaraya,alaraz,alarazbim,alarazbum,alarba,alarbito y caimainero,alarcia,alarcon,alard,alard-e chahar dang,alarda,alardeh,alardjir,alare,alareini,alareni,alares,alarge,alari,alarilla,alariwo,alarj,alarjiban,alarjir,alarka,alarkaca,alarkacha,alarkachah,alarkacheh,alarke,alarmake,alarmakei,alarmintang,alarmitang,alaro,alarobia,alarobia befeta,alarobia bemaha,alarobia-ambohitrambo,alarobia-ambovombe,alarobia-antanamalaza,alarobia-vohiposa,alarode,alaroka,alaropo,alarp,alarri,alarsas,alartu,alaru,alarup,alarupi,alarupo,alaruru,alarva,alarwah,alaryd,alarz,alarz bum,alas,alas tengah,alas-asin,alas-os,alas-timur,alasa,alasaguin,alasaguing,alasainio,alasan,alasang,alasang-e sofla,alasange `ulya,alasange sufla,alasangi-sufla,alasangi-ulia,alasapa,alasaqal,alasaqqal,alasar,alasarli,alasaru,alasarum,alasas,alasasin,alasay,alasbaing,alasbayur,alasbayur bawah,alasboeloeh,alasbringin,alasbuluh,alasca,alascaderos,alaschehir,alascilik,alasdiin,alasdjarangan,alasdurin,alase,alaseher,alasehir,alaseki,alasen,alasevce,alasevici,alaseyit,alasgar,alasgarli,alasgedang,alasgede,alasghaji,alash,alash banda,alash khosrow,alasha,alasha-kazgan,alashah,alashakend,alashan,alashanchi,alashantsochi,alashanyou qi,alashanyuchi,alashanzuo qi,alashar,alasharum,alashay,alashayka,alashe,alashehir,alasheyevka,alashgadzhi,alashghaji,alashio,alashir,alashki,alashkino,alashlu,alasht,alashtar,alasia,alasian,alasiikajarvi,alasio,alasiri,alasiyaw,alasjalin,alasjarangan,alasjerd,alasjero,alaska,alaskandang,alaskembang,alaskembang 1,alaskembang 2,alaskembang dua,alaskembang satu,alaskerbau,alaski,alaskluang,alaskobong,alaskobonggumuk,alaskokon,alaskranjang barat,alaskranjang timur,alaskula,alaskyla,alaslanceng dua,alaslanceng satu,alasledek,alasmalang,alasmalang kidul,alasmalang lor,alasmelati,alasngampo,alasnjior,alasnyior,alasnyiur,alaso,alaso osun,alaso-adura,alasoko,alasoku,alasombo,alasomme,alasommee,alasommes,alason cholafula,alasoo,alasor,alasora,alasotkamo,alaspaa,alaspadu,alaspaya,alaspecah,alaspengembunan,alasperean,alaspeselan,alaspinang,alaspring,alaspungo,alasrajah,alasrembung,alassa,alassalmi,alassandan,alassane,alassane dey,alassane-deye,alassanila,alassilurunge,alassio,alassirih,alassitan,alasso,alassumur,alassumur selatan,alast,alast bala,alast pain,alast-e bala,alast-e pain,alastalo,alastan,alastani,alastaro,alastaromaenpaa,alastasi,alaste,alastengah,alastipis,alastiyang,alastledek,alastledek lor,alastoeo,alastoewo,alastraf,alastruey,alastua,alastuey,alastuwa,alastuwo,alastuwocilik,alastvere,alasuulari,alasuutari,alaswangi,alaswlingin,alasydanmaa,alat,alat ida u suggun,alat ida usugun,alat makay,alatumani,alat-ywa,alata,alata 1,alata 2,alata dua,alata makey,alata satu,alataipale,alatala,alatalb,alatalo,alatan,alatana,alatanomolo,alatanpaoliko,alatao,alatare,alataria,alataries,alatarka,alatarla,alatas,alatas amadlene,alatau,alatay,alatayevo,alataykino,alate,alate iju,alate-makay,alateko,alatemir,alatemmes,alatemur-bala,alatemur-beyuk,alatengaopao,alatepe,alatepi,alates,alateymur,alati,alatibaba,alatinskaya,alatip,alatise,alatissibo,alatka,alatkaoldal,alatlar,alatli,alatly,alatman,alatna,alaton,alatona,alatopetra,alatoprak,alatorka,alatornio,alatorp,alatosun,alatovichi,alatoz,alatra,alatri,alatro,alatron,alatshu,alatsinainy,alatsinainy fandandrava,alatsinainy fandrandava,alatsinainy ialamarina,alatsinainy ibity,alatsinainy-ambazaha,alatsinainy-bakaro,alatsinainy-loharano,alatskivi,alatskiy spirtzavod,alatti,alattu,alattur,alattyan,alatubani,alatur,alatusa,alaty,alatyr,alatyrkasy,alau,alaua,alauat,alauca,alauca vieja,alauca viejo,alaucana,alaud din wala,alaudad,alauddin,alauddin majra,alaude,alaudscho,alaufau,alaugalung,alauihao,alaujo,alauk,alauksu,alauli,alaum,alaung,alauntal,alaunthal,alaunwerk,alaupisalu,alaur,alauran,alaurpala,alausa,alausalo,alausi,alauso,alaut,alautar,alautes,alauwegama,alava,alavaara,alavada,alavakkaisirukkulam,alavali,alavalieque,alavalli,alavan,alavanca,alavandici,alavane,alavanio,alavanja,alavanyo,alavar,alavari,alavaro obregon,alavas,alavattnet,alave,alaveddi,alavedduvan,alaverdi,alaverdy,alavere,alaveski,alaveteli,alavi,alavicheh,alavieska,alaviinpola,alaviirret,alavinkyla,alavita,alavo,alavoittu,alavojakkala,alavugu,alavunt,alavuokki,alavuotto,alavus,alavuz,alaw,alawa,alawakhawa,alawal,alawal khel,alawala,alawali,alawalke,alawalpur,alawalwala,alawang,alawanwala,alawasi,alawattegama,alawatugama,alawatugoda,alawatupitiya,alawatura,alawaye,alawbum,alawchi,alawdad,alawedo,alawehuko,alawerdy,alawere,alawgedara,alawi al-hilla,alawihao,alawihaw,alawijao,alawitia,alawkah,alawngbauk,alawo,alawode,alawodi,alawoona,alawowo,alawpum,alawuddin,alawure,alawuri,alawusa,alawwa,alawwegama,alay,alay ghar,alay ghundey,alay hani,alaya,alaya roza,alayadi vempu,alayadimaduchchenai,alayagmur,alayaka,alayamur,alayan,alayan-i assaghri,alayan-i pichuk,alayande,alayangudi,alayao,alayar,alayarkhel,alayarkheyl,alayarpur,alayarvi,alayaya,alayaz,alayazi,alaybede,alaybey,alaybeyi,alaye,alayenli,alayer,alayere,alayev,alayeva,alayevo,alayevskiy,alaygar,alaygirovo,alayhani,alayhanikoy,alayi,alayidi,alayin,alayin oke,alaying,alayinli,alayizu,alaykovo,alaykoy,alayl,alayla,alaylah,alayli,alayli dadda`,alayo,alayon,alayonan,alayond,alayor,alayotes,alaypampa,alayrac,alayri,alayunan,alayunt,alayurt,alayurtyerli,alayuz,alayye,alazai sar,alazan,alazanas,alazanas grande,alazanes,alazangi,alazao,alazao dos cardosos,alazapin,alazar,alazgar,alazi,alazli,alazman,alazon,alazui,alazui,ala\302\261cler,alb-e hamadi,alb-e hemadi,alba,alba de cerrato,alba de los cardanos,alba de tormes,alba de yeltes,alba iulia,alba kand,alba kandi,alba posse,alba sara,alba station,alba`id-e do,alba`id-e yek,albaba,albac,albacas,albacastro,albacete,albach,albaching,albachten,albacina,alback,albacken,albacutya,albadalejo,albadar,albadarr,albaderia,albaek,albaek hede,albaga,albagado,albages,albagiara,albagnas,albaida,albaida del aljarafe,albaina,albaisa,albaizin,albaja,albaja hacienda,albaji,albaji-ye jadid,albak,albaka,albakas,albakhtino,albal,albala,albaladejito,albaladejo,albaladejo del cuende,albalan,albalar,albalari,albalat,albalat de la ribera,albalat de segart,albalat de taronchers,albalat dels sorells,albalat dels taronchers,albalate,albalate de cinca,albalate de las nogueras,albalate de zorita,albalate del arzobispo,albalatillo,albaljaniya,albamraveh,alban,alban park,alban-de vareze,albana,albana e madhe,albana e vogel,albanchez,albanchez de ubeda,albanci,alband,albandi,albane,albanel,albanella,albaneto,albangi,albani,albania,albanie,albanija,albanik,albaniku,albanilla,albanio,albanne,albannette,albano,albano andre,albano di lucania,albano laziale,albano santalessandro,albano vercellese,albano vieira,albantown,albantsi,albany,albany center,albany creek,albany junction,albany station,albanzi,albar kan,albar-bugut,albaran,albarbauh,albarca,albarcones,albardeh,albardeira,albardeiros,albardia,albardo,albardon,albarea,albaredo,albaredo dadige,albaredo per san marco,albaredos,albarejo,albarellos,albares,albares de la ribera,albaret,albaret-le-comptal,albaret-le-comtal,albaret-sainte-marie,albareto,albareto di borgo val di taro,albareto di val di taro,albargaton,albarical,albaricales,albarico,albarino,albariza,albarka koara,albarka kwara,albarkaise,albarkaize,albaro vescova,albaron,albaron di sea,albaros,albarquilla,albarracin,albarrada,albarradas,albarran,albarranes,albarraque,albarreal de tajo,albarrellos,albarrol,albart,albas,albasa,albasan,albasawa,albashi,albashkino,albashskiy,albasini,albasu,albat,albatan tokhmaq,albatana,albatarrech,albate,albatera,albaton,albatross,albatross mobile home park,albatroz,albatsried,albatukan,albaugh,albaum,albawa,albaxen,albay,albayrak,albazin,albazine,albazinka,albazino,albbruck,albbruck-dogern,albe,albeanu-manciulescu,albeasca,albecassagne,albeck,albeck obere schattseite,albeck untere schattseite,albecq,albee,albefeuille,albefeuille et lagarde,albefeuille-lagarde,albeid,albeiros,albeison,albela,albelagh,albelda,albelda de iregua,albele,albele-bai,albelera,albella,albemarle,albemarle beach,albemga,alben,alben-otar,albendea,albendiego,albendin,albenedt,albenek,albenga,albengi,albeni,albeniz,albeno,albenodt,albens,albenzaire,albeos,albepierre,albepierre-bredons,alber,alber di sesana,alberazzi,alberazzo,alberberg,alberbury,alberca,albercas,alberchigos,albercones,alberdi,alberdi viejo,alberene,alberese,albereto di borgotaro,alberga,albergaria,albergaria das cabras,albergaria dos doze,albergaria dos fusos,albergaria-a-nova,albergaria-a-velha,albergen,albergo,albergo corteraso,albergo del ghertele,albergo della posta,albergo maso corto,albergo monte ranere,albergo monte rovere,albergue,albergueria,albergueria de herguijuela,albergues el rincon,albergues la mogaya,albergues pasada de los bayos,albergues pasada del palo,albergues tariquejo,alberhill,alberica,alberice,alberie,alberique,alberite,alberite de san juan,alberitz,alberloch,albern,albernau,albernberg,alberndorf,alberndorf in der riedmark,albernhof,alberni,albernoa,albero,albero alto,albero bajo,alberobello,alberoche,alberoda,alberode,alberona,alberone,alberoni,alberov,alberovice,alberqueria,alberquita,alberquitas,alberrys creek,albers,albersbach,albersdorf,albersdorferegg,albershausen,alberskirch,albersloh,albersreuth,albersrieth,albersroda,alberstedt,alberstorf,albersweiler,alberswil,albert,albert arms,albert canyon,albert city,albert derina,albert falls,albert lea,albert park,albert road,albert town,albert-telep,alberta,alberta heights,albertacce,albertaich,albertakna,albertfalva,alberthain,alberthofen,alberthohe,alberti,albertice,albertin,albertina,albertinenhof,alberting,albertini,albertinia,albertiniahotel,albertinovac,albertinsky dvor,albertirsa,albertirsai tanyak,albertitz,albertkazmerpuszta,albertmajor,alberto,alberto a. cancela,alberto carrera torres,alberto duran,alberto fujimori,alberto garduno,alberto isaacson,alberto lleras,alberto moreira,alberto oviedo mota,alberto pineda,alberto torres,alberto-pampa,alberton,alberton west,albertopa,albertoque,albertos,albertow,albertowsko,alberts,alberts landing,albertsberg,albertschacht,albertschachthauser,albertsdorf,albertshausen,albertshausen bei bad kissingen,albertshof,albertshofen,albertslund,albertson,albertson park,albertsons center,albertsreuth,albertsried,albertstad,albertstadt,albertsthal,alberttown,albertushof,albertville,albertyn,albertynsville,alberuela de la liana,alberuela de la liena,alberuela de tubo,alberweiler,albery,alberzell,albes,albesa,albescii,albesdorf,albesli,albessen,albesti,albesti-de mur,albesti-muru,albesti-paleologu,albesti-pamanteni,albesti-paminteni,albesti-ungureni,albestii,albestii bistritei,albestii paminteni,albestii-muru,albestii-paleolog,albestii-paleologu,albestii-pamanteni,albestii-ungureni,albestroff,albet,albeta,albeth heights,albettone,albeuve,albeyevo,albfuhren,albi,albi khel,albi kheyl,albia,albiac,albian,albiano,albiano divrea,albias,albiasu,albiate,albidona,albieres,albies,albieux,albiez,albiez-le-jeune,albiez-le-vieux,albig,albiga,albigasta,albignac,albignano,albignasego,albigny,albigny-sur-saone,albigowa,albijoy,albilagh,albillos,albin,albina,albinama 1,albinama 2,albinama 3,albinama number 1,albinama number 2,albinama number 3,albinana,albinari,albinarii,albinava,albine,albinea,albinebu,albinen,albinets,albinetul nou,albinetul vechi,albing,albingdorf,albingshausen,albinho,albinita,albinitsa,albinivka,albino,albino alves garcia,albino manhique,albino zarate,albinopolis,albinovka,albinow,albinshof,albinsk,albinuvka,albiol,albion,albion alps,albion basin,albion center,albion landing,albion park,albion place,albiosc,albir,albire,albires,albis,albisano,albisheim,albishiny,albishofen,albiso,albisola marina,albisreute,albisried,albissola marina,albisu,albita,albite,albitelhe,albitreccia,albituy,albiz,albiztur,albizu,albizzate,albjaerg,albjerg,alblanedo,alblasserdam,albligen,albling,albo,albocabe,albocacer,alboeuf,alboga,albogarden,alboge,albogh kili,albojarico,alboke,albolag,albolagh,albolleque,alboloduy,albolote,albomaveh,albon,albon- dardeche,albona,albona distria,albona e eperme,albona e poshtme,albondon,albonese,albong,albongdong,albonico,alboniga,albons,alboonai,albora kwara,albora-koara,alborache,alborada,alborada dos,alborada uno,alborajico,alboraya,alborea,alboreca,alboreda,alborejo,albores,alborg,alborga,alborge,alborj,alborn,albornos,albornoz,albornoz arriba,albornoz de arriba,alboru-kent,alborvayeh,alborz,alborzabad,albos,albota,albota de jos,albota de sus,albotesti,alboussiere,albouystown,albovskiy,albox,alboy,albrae,albrechten,albrechtice,albrechtice nad orlici,albrechtice nad vltavou,albrechtice u rymarova,albrechtice u susice,albrechticky,albrechtitz,albrechtovice,albrechtowko,albrechts,albrechts teerofen,albrechts theerofen,albrechtsberg,albrechtsberg an der grossen krems,albrechtsberg an der pielach,albrechtschlag,albrechtsdorf,albrechtsfeld,albrechtsfelde,albrechtsflor,albrechtsflur,albrechtshain,albrechtshaus,albrechtshof,albrechtshohe,albrechtsried,albrechtsruh,albrechtsthal,albrechtswalde,albrechtswiesen,albreda,albrekhtovo,albrekhtovo pervoye,albrekhtovo vtoroye,albrela,albricias,albright,albrighton,albrightsville,albringen,albringhausen,albringwerde,albris,albrittons,albrook,albrunna,albsfelde,albshausen,albsheim,albsheim an der eis,albstadt,albstedt,albu,albu -affah,albu `afri-ye jonubi,albu `afri-ye shomali,albu `ajil,albu `alayej-e `olya,albu `alayej-e bala,albu `alayej-e miani,albu `alayej-e pain,albu `alayej-e sofla,albu `alayej-e vosta,albu `ali,albu `ali al fayyad,albu `alik,albu `alwan,albu `amir,albu `ansah,albu `aysh,albu `ayshah,albu `aziz,albu `isa,albu `obeyd,albu `ubayd,albu `uwaid,albu `uwayd,albu aish,albu bakri,albu bali,albu bandar,albu bani,albu birghuth,albu bulaysim,albu chamil,albu chulaif,albu dhahir,albu dhiyab,albu fahd,albu faraj,albu faris,albu fazel,albu ghannam,albu gharbeh,albu gharib,albu ghassaf,albu ghizlan,albu gulak,albu hamdan,albu hamid,albu hamza,albu hamzah,albu hardan-e `olya,albu hasan,albu hassan,albu hawalah,albu hesar,albu hilala,albu hilalah,albu hubayl,albu humaira,albu humayrah,albu jadidah,albu jamil,albu jum`ah,albu juwari,albu kamil,albu kan`an,albu karadi,albu khalaf,albu khalid,albu khalifah,albu khalil,albu khulayf,albu khulayyif,albu kuraydi,albu mafraj,albu mahi,albu malih,albu mar`i,albu matar,albu mufraj,albu muhawish,albu muhaysin,albu musa,albu musad,albu mustafa,albu n`ima,albu nai,albu na`ayyem,albu na`im,albu najm,albu nasir,albu na\302\261`mah,albu nimr,albu nisf,albu nusf,albu ramzah,albu rashid,albu rida,albu ridan,albu rishah,albu saba,albu sabah,albu salih,albu salmah,albu sanvan,albu sanwan,albu saraj,albu savat,albu sayf,albu sayyad,albu shahbaz,albu shelug,albu shuaikh,albu shujayrah,albu shukhayr,albu shukur,albu shuwaykh,albu sinwan,albu siraj,albu soveyt,albu su`ud,albu subah,albu subbar,albu sulaymah,albu sulayman,albu surdi,albu suway`id,albu talhah,albu tarfa,albu tarfah,albu uzayrij,albu ward,albu yusuf,albu zarqah,albu zayd,albu zi\302\247ahir,albu zuwaid,albuebadi,albuara,albubaled,albucak,albuco,albudeite,albuera,albuerne,albufeira,albuixech,albujon,albuko,albukoterie,albulagh,albule,albulesti,albulum,album,albuna,albunan,albungen,albunol,albunuelas,albuquerque,albuquerque de fora,albuquerque lins,albuquerque ne,alburg,alburg center,alburg travel trailer park,alburgh,alburikent,alburitel,alburnett,alburqerque,alburquerque,alburtis,albury,albury municipality,alburz,albus-syurbeyevo,albussac,albut,albutele,albuzzano,alby,alby-sur-cheran,albybyn,albycheva,albychevo,albyn,alca,alcabala,alcabideche,alcabideque,alcabon,alcabre,alcabulaq,alcacer,alcacer da sal,alcacer do sal,alcacocha,alcacoto,alcacovas,alcacus,alcadina,alcadozo,alcafache,alcafaz,alcafozes,alcahozo,alcahuarinan,alcahuito,alcaidaria,alcaide,alcaiden,alcaidon,alcainca grande,alcainca pequena,alcaine,alcains,alcak,alcakbel,alcakdere,alcakgedik,alcakli,alcala,alcala de chisvert,alcala de chivert,alcala de ebro,alcala de guadaira,alcala de gurrea,alcala de henares,alcala de jucar,alcala de la jovada,alcala de la selva,alcala de la vega,alcala de los gazules,alcala de moncayo,alcala del jucar,alcala del obispo,alcala del valle,alcala la real,alcala tambo,alcalali,alcalco,alcalde,alcalde mayor,alcalde pampa,alcaldediaz,alcaldia mayor,alcali,alcalia,alcalmantlila,alcamarine,alcamarine muto,alcamarini,alcamariyoc,alcamenca,alcamim,alcamo,alcampel,alcan,alcana,alcanadas,alcanadre,alcanadro,alcanar,alcancia,alcandre,alcanede,alcanena,alcanforada,alcangosta,alcanhoes,alcania,alcanices,alcaniz,alcanizo,alcano,alcantara,alcantara de jucar,alcantaras,alcantarilha,alcantarilla,alcantarilla hacienda,alcantarillas,alcantaro,alcantarrilla,alcantavilla,alcantil,alcantilado,alcantud,alcaoren,alcaparral,alcaparrosa,alcaparroso,alcapinar,alcara,alcara li fusi,alcarabanes,alcaracejos,alcaraces,alcaracillo,alcaral,alcaravanes,alcaravela,alcaraz,alcaraz de san juan,alcarazejos,alcareia,alcaria,alcaria alta,alcaria cova,alcaria cova de baixo,alcaria cova de cima,alcaria de javazes,alcaria do coelho,alcaria do joao,alcaria do peso,alcaria longa,alcaria queimada,alcaria ruiva,alcarias,alcarias de baixo,alcarraques,alcarras,alcarraz,alcarreto,alcarrizos,alcarva,alcata,alcatambo,alcatraz,alcaucin,alcaudete,alcaudete de la jaya,alcaus,alcautal,alcay,alcay-alcabehety-sunharette,alcaya,alcayaga,alcayata,alcayatal,alcazaba de quebdani,alcazar,alcazar de san juan,alcazar del rey,alcazar zegouer,alcazar zeguer,alcazarejos,alcazaren,alcazares,alcazares norte,alcazarquebir,alcazarquivir,alcazarseguer,alcazi,alcazquivir,alccamarine,alcececa,alceda,alcedar,alcedo,alcek,alceme,alcerreca,alcester,alceu garcia,alcevre,alchaby,alchagy,alchak,alchak-dere,alchaly,alchamulk,alchan,alchanni,alchantschurt,alcheda,alchedar,alchedary,alchedat,alcheh gheshlagh,alcheh molk,alcheh qeshlaq,alchekli,alchen,alchenstorf,alcheriubem,alchevsk,alchevskoe,alchi gompa,alchichica,alchidimia,alchie,alchin,alchinbayeva,alchipichi,alcholoa,alcholoya,alchori,alchunib,alci,alcibar,alcibia,alcibiades vargas,alcicilar,alcides,alcides batista,alcides silva,alcidio,alcihual,alcihuatl,alcik,alcilar,alcili,alcin,alcino macie,alcira,alcitepe,alcivar,alciyeniyapan,alcksandrovo,alco,alco-llacta,alcoa,alcoa center,alcoba,alcoba de la ribera,alcoba de la torre,alcobaca,alcobaco,alcobaza,alcobendas,alcober,alcobertas,alcoceber,alcocebre,alcocer,alcocer de planes,alcocero,alcocero de mola,alcoche,alcochete,alcockspruit,alcoentre,alcofra,alcogulhe,alcohujate,alcoitao,alcola,alcolea,alcolea de calatrava,alcolea de cinca,alcolea de las penas,alcolea de tajo,alcolea del pinar,alcolea del rio,alcolecha,alcolete,alcoletge,alcollarin,alcolombal de baixo,alcolombal de cima,alcolu,alcoma,alcomie,alcomun,alcomunga,alcona,alconaba,alconada,alconada de maderuelo,alconadilla,alconbury,alconchel,alconchel de ariza,alconchel de la estrella,alconera,alcones,alcontar,alcony,alconyonqui,alcora,alcoraya,alcorcha,alcorcillo,alcorcon,alcorisa,alcoriza,alcorlo,alcorn,alcornocal,alcornocalejo,alcornocalito,alcornoque largo,alcornoquito,alcornoquito blanco,alcorns,alcorochel,alcoroches,alcorouchel,alcorriol,alcorta,alcorvel,alcosana,alcot,alcotambo,alcotas,alcouce,alcoutao,alcoutim,alcova,alcova heights,alcova po,alcove,alcove spring,alcover,alcovy,alcovy forest,alcovy mobile home park,alcovy mountain,alcovy shores,alcoy,alcoyes,alcoyoque,alcoz,alcozar,alcozauca,alcozauca de guerrero,alcranes,alcsil,alcsimillerlapos,alcsipuszta,alcsisziget,alcsut,alcsutdoboz,alcubela de baixo,alcubierre,alcubilla,alcubilla de avellaneda,alcubilla de las penas,alcubilla de nogales,alcubilla del marques,alcubillas,alcublas,alcudia,alcudia de carlet,alcudia de crespins,alcudia de guadix,alcudia de monteagud,alcudia de veo,alcudiola,alcudra,alcuescar,alcuetas,alcular,alculco,alcuneza,alcutar,alda,aldaba,aldaba-chiqui,aldabas,aldabo,aldabulskaya,aldabulskiy,aldaca,aldachildo,aldachildu,aldad zehi,aldadpur,aldam,aldama,aldama primera seccion,aldama segunda seccion,aldama tercera seccion,aldamas,aldamasul,aldan,aldan-maadyr,aldan-madyr,aldan-zoloto,aldana,aldanci,aldano,aldanskaya,aldanskiy perevoz,aldao,aldape,aldar,aldar haan suma,aldar han,aldar khan somon,aldara,aldarak,aldarhaan,aldarhan sumu,aldari,aldarkhaan,aldarkhan,aldarkino,aldarovo,aldashin,aldasos,aldavilla,alday,aldaya,aldaykino,aldaz,aldbodarna,aldborough,aldbourne,aldbrough,aldbury,alddama,aldea,aldea aguero,aldea apeleg,aldea appeleg,aldea asuncion,aldea blanca,aldea blanca del llano,aldea brasilera,aldea brava,aldea camarero,aldea chaleco,aldea chavez,aldea cintron,aldea cuatro casas,aldea cuesta,aldea de abajo,aldea de arango,aldea de arriba,aldea de bororos,aldea de chilegua,aldea de ebro,aldea de la cruz,aldea de las aranas,aldea de maria,aldea de mercedes,aldea de occidente,aldea de paio pires,aldea de san andres,aldea de san esteban,aldea de san miguel,aldea de san nicolas,aldea de san pedro,aldea de trujillo,aldea del cano,aldea del fresno,aldea del obispo,aldea del pinar,aldea del rey,aldea del rey nino,aldea del zapote,aldea do meio,aldea el chaleco,aldea el zapote,aldea general salom,aldea india,aldea jacobi,aldea jaroslawky,aldea kranevitter,aldea krapzenthal,aldea la asuncion,aldea la concepcion,aldea la primavera,aldea la tajeada,aldea loma plata,aldea maria luisa,aldea mercedes,aldea mineras,aldea moret,aldea nueva,aldea pajapas,aldea protestante,aldea real,aldea reffino,aldea reflino,aldea repollo,aldea rivas,aldea romana,aldea ruiz moreno,aldea salto,aldea san alfonso,aldea san antonio,aldea san francisco,aldea san gregorio,aldea san jose,aldea san juan,aldea san miguel,aldea san simon,aldea santa anita,aldea santa celia,aldea santa cruz,aldea santa maria,aldea santa rosa,aldea spatzenkutter,aldea spazzenkuter,aldea vieja,aldea zapote,aldeacardo,aldeacentenera,aldeacipreste,aldeacueva,aldeadalba de hortaces,aldeadavila de la ribera,aldeadavila de revilla,aldeaencabo de escalona,aldeagallega,aldeagutierrez,aldeahermosa,aldealabad,aldealabad del miron,aldealafuente,aldealazaro,aldealba de hortaces,aldealbar,aldealcardo,aldealcorvo,aldealengua,aldealengua de santa maria,aldealobos,aldealpozo,aldealsenor,aldeamayor de san martin,aldeamento,aldeanueva,aldeanueva de atienza,aldeanueva de barbarroya,aldeanueva de cameros,aldeanueva de ebro,aldeanueva de figueroa,aldeanueva de guadalajara,aldeanueva de la sierra,aldeanueva de la vera,aldeanueva de portanobis,aldeanueva de portanovis,aldeanueva de san bartolome,aldeanueva de santa cruz,aldeanueva del camino,aldeanueva del campanario,aldeanueva del codonal,aldeanueva del monte,aldeaquemada,aldearraso,aldearrodrigo,aldearrubia,aldeasaz,aldeaseca,aldeaseca de alba,aldeaseca de armuna,aldeaseca de la frontera,aldeasona,aldeatejada,aldeavella,aldeavieja,aldeavieja de tormes,aldeavila de revilla,aldebro,aldeburgh,aldeby,aldecoa,aldegao,aldegavinha,aldegea,aldegheri,aldeguer,aldehorno,aldehuela,aldehuela de agreda,aldehuela de boveda,aldehuela de calatanazor,aldehuela de jerte,aldehuela de la boveda,aldehuela de liestos,aldehuela de perianez,aldehuela de rincon,aldehuela de santa cruz,aldehuela de yeltes,aldehuela del codonal,aldehuela del jerte,aldehuela del rincon,aldeia,aldeia 20 de septembro,aldeia a. viana,aldeia acupita,aldeia afuaca,aldeia amanajas tauai,aldeia amanajaz tauai,aldeia amanajaz taudi,aldeia ancimba,aldeia ancuaze,aldeia bacaeri,aldeia bacaery,aldeia bacicolo,aldeia bagarila,aldeia baia,aldeia bakairi,aldeia bandar,aldeia bango,aldeia bawe,aldeia bbwedzi,aldeia bella,aldeia bene,aldeia bingo,aldeia biri-biri,aldeia birira,aldeia bolande,aldeia bonga,aldeia braganco,aldeia bucha,aldeia bulao,aldeia bume,aldeia cabambe,aldeia cabixi,aldeia cabixy,aldeia cachenge,aldeia cachere,aldeia caconde,aldeia cafulisa,aldeia caho,aldeia caiabi,aldeia cajamba,aldeia calala,aldeia calembe,aldeia calumue de baixa,aldeia calussaca,aldeia cambarira,aldeia camela,aldeia campala,aldeia campine,aldeia campista,aldeia campo,aldeia camulaumba,aldeia canangue,aldeia canda,aldeia candodo,aldeia candogal,aldeia cangudze,aldeia canhandula,aldeia canhunque,aldeia capimbe,aldeia capinizi,aldeia capiri,aldeia capiringombe,aldeia capitao carutu,aldeia caraja,aldeia caraja do capitao maloe,aldeia cassero,aldeia cassua,aldeia cassumar,aldeia cassunsa,aldeia catambe,aldeia cataxa,aldeia cateme,aldeia catumba,aldeia caume,aldeia cauzi,aldeia chacale,aldeia chamacasso,aldeia chamande,aldeia chambambe,aldeia chamulaza,aldeia changudue,aldeia chaomba,aldeia chapaluca,aldeia chapatira,aldeia chayane,aldeia chia,aldeia chibovo,aldeia chicoco,aldeia chidzidzi,aldeia chifumbe,aldeia chigoma,aldeia chiguaja,aldeia chilapitangongo,aldeia chileca,aldeia chimadzi,aldeia chimangue,aldeia chingola,aldeia chinhanda-velha,aldeia chinhande,aldeia chinjati,aldeia chioco,aldeia chionde,aldeia chiotumbo,aldeia chipacasse,aldeia chipala,aldeia chipala-pala,aldeia chipanga,aldeia chipone,aldeia chipungo,aldeia chiputa,aldeia chiputu,aldeia chiradze,aldeia chirowa,aldeia chissanga,aldeia chissete,aldeia chitanda,aldeia chitesse,aldeia chitolo,aldeia chitondo,aldeia chitula,aldeia chiuanjota,aldeia chiuindi,aldeia chiuta,aldeia chiutumbo,aldeia chivigo,aldeia chivungo,aldeia chiwai,aldeia chuala,aldeia chuchamano,aldeia chyia,aldeia cimeira,aldeia coamba,aldeia coloca,aldeia colongo,aldeia colope,aldeia comunais 25 de setembro,aldeia comunais benzane,aldeia comunais chacana,aldeia comunais chicuembo,aldeia comunais chicutso,aldeia comunais cuambate,aldeia comunais fucue,aldeia comunais iii congresso,aldeia comunais kaptine,aldeia comunais macarringue,aldeia comunais machel,aldeia comunais maconquele,aldeia comunais malonguene,aldeia comunais manhica,aldeia comunais manjangue,aldeia comunais massebocane,aldeia comunais matuba,aldeia comunais mechangana,aldeia comunais mingonzo,aldeia comunais mubanguer,aldeia comunais munhamane,aldeia comunais nhivane,aldeia comunais obrigado tanzania,aldeia comunais ringane,aldeia comunais uafecula,aldeia comunais uambjana,aldeia comunais unguca,aldeia comunais zulu,aldeia conceicao,aldeia creva,aldeia cuambe,aldeia cuanzeu,aldeia cumbe,aldeia curipi,aldeia cussarara,aldeia da bemposta,aldeia da cruz,aldeia da dona,aldeia da graca,aldeia da madragoa,aldeia da mata,aldeia da mata da rainha,aldeia da matala,aldeia da missao,aldeia da nora,aldeia da ponte,aldeia da ribeira,aldeia da ribeira fundeira,aldeia da serra,aldeia da tor,aldeia dala,aldeia das dez,aldeia das freiras,aldeia das laranjeras,aldeia das posses,aldeia das sebes,aldeia daudo,aldeia de alem,aldeia de ana de aviz,aldeia de baixo,aldeia de bororo,aldeia de brescos,aldeia de carvalho,aldeia de cima,aldeia de cuor,aldeia de eiras,aldeia de eiras fundeiras,aldeia de folgares,aldeia de freixiel,aldeia de freixo,aldeia de irmaos,aldeia de joanes,aldeia de joao pires,aldeia de juso,aldeia de lionde,aldeia de melo,aldeia de mourao,aldeia de nacomba,aldeia de ourique,aldeia de paio pires,aldeia de pedra,aldeia de pegoes,aldeia de sagres,aldeia de santa comba,aldeia de santa margarida,aldeia de santa maria,aldeia de santa teresa,aldeia de santana,aldeia de santo antonio,aldeia de sao francisco de assis,aldeia de sao jose,aldeia de sao miguel,aldeia de sao pedro,aldeia de soito de vide,aldeia de vilar,aldeia demera,aldeia devo,aldeia do alem,aldeia do biopio,aldeia do bispo,aldeia do cabo,aldeia do cano,aldeia do capitao chimang,aldeia do capitao mariquinhas,aldeia do capitao piarru,aldeia do carvalho,aldeia do cuhor,aldeia do futuro,aldeia do inacio,aldeia do longolo,aldeia do mato,aldeia do meco,aldeia do meio,aldeia do meio caraja,aldeia do pinto,aldeia do pombal,aldeia do prata,aldeia do ranchao,aldeia do rebocho,aldeia do sande,aldeia do souto,aldeia doespirito santo,aldeia dos bororos,aldeia dos caiapos,aldeia dos cunhados,aldeia dos delhas,aldeia dos fernandes,aldeia dos fidalgos,aldeia dos gagos,aldeia dos goncos,aldeia dos indios,aldeia dos machacalis,aldeia dos marmelos,aldeia dos matos,aldeia dos mourinhos,aldeia dos narigudos,aldeia dos pescadores,aldeia dos ruins,aldeia dos sao guerra,aldeia drunga,aldeia duanga,aldeia dzenda,aldeia ferreira,aldeia fichada,aldeia flandue,aldeia fombe,aldeia formosa,aldeia forte malanguene,aldeia fundeira,aldeia galega,aldeia galega da merceana,aldeia gavinha,aldeia gimica,aldeia goba,aldeia goche,aldeia gomacheza,aldeia goncalo,aldeia graca,aldeia grande,aldeia honde,aldeia indigena catuquina,aldeia indigena espirro,aldeia indigena ipixuna,aldeia indigena man-azde,aldeia indigena serra azul,aldeia jairosse,aldeia jole,aldeia jose coroado,aldeia julius nyerere,aldeia kapiriuta,aldeia katondo,aldeia kavago,aldeia l. borges,aldeia licamba,aldeia lifunda,aldeia ligogolo,aldeia lihuchi,aldeia limbe,aldeia limbue,aldeia lindimula,aldeia lingombe,aldeia lipaque,aldeia lipirize,aldeia lipombue,aldeia lissenguece,aldeia litete,aldeia litombochi,aldeia longa,aldeia luambala,aldeia luatize,aldeia lucessi,aldeia lucucho,aldeia lucumue,aldeia luiga,aldeia lulimire,aldeia lumbaulo,aldeia lumbe,aldeia luombuo,aldeia lusefa,aldeia lussanga,aldeia lussingene,aldeia lutenguene,aldeia luzina,aldeia mbemba,aldeia mpomba,aldeia mabauane,aldeia mabinda,aldeia mabwedzi,aldeia macasse,aldeia machava,aldeia macondece,aldeia magaco,aldeia majongota,aldeia majune,aldeia mala,aldeia malambue,aldeia malica,aldeia mamirrupa,aldeia manau,aldeia mango,aldeia manuel antonio,aldeia mapango,aldeia mapupulo,aldeia marara,aldeia maroeira,aldeia martins santareno,aldeia massamba,aldeia massangere,aldeia massano,aldeia massatwe,aldeia massecha,aldeia massingir velho,aldeia mataca,aldeia matambo,aldeia matemango,aldeia matumbue,aldeia maulemaule,aldeia mazamba,aldeia mazogo,aldeia mazoi,aldeia mbueca,aldeia mbulise,aldeia mbuna,aldeia mecalanila,aldeia mecava,aldeia mecual,aldeia melo egidio,aldeia mepapaia,aldeia mepenha,aldeia messu,aldeia metonia,aldeia metora,aldeia metumbe,aldeia micuinha,aldeia mifumbe,aldeia milanda,aldeia milepa,aldeia mirinde,aldeia missanja,aldeia missaua,aldeia mitamba,aldeia mithethe,aldeia moronde,aldeia mota,aldeia mpeia,aldeia mphemba,aldeia mpuephue,aldeia msemene,aldeia mtovo,aldeia muabvi,aldeia muapula,aldeia mucanda,aldeia mucanga,aldeia muchamba,aldeia muchena,aldeia muchenga,aldeia mucheni,aldeia mudzimbe,aldeia muenheher,aldeia mugoma,aldeia mulinji,aldeia mulumbue,aldeia muongota,aldeia mussa,aldeia mussengere,aldeia mvzi,aldeia nacapa,aldeia nachinanga,aldeia naedane,aldeia naliuila,aldeia namacungua,aldeia nambe,aldeia nambikwara,aldeia namisse,aldeia namuela,aldeia nanzenhenje,aldeia nauca,aldeia ncalangoma,aldeia nchatwe,aldeia nchinji,aldeia ndalala,aldeia nduica,aldeia necungas,aldeia nganfua,aldeia ngavo,aldeia ngofi,aldeia ngombe,aldeia ngongote,aldeia ngonha,aldeia ngoo,aldeia ngulo,aldeia nhacawanda,aldeia nhagondoa,aldeia nhagondua,aldeia nhalupanda,aldeia nhamadzonso,aldeia nhamatema,aldeia nhambiquara,aldeia nhancadenga,aldeia nhanchinde,aldeia nhancono,aldeia nhankwalakwald,aldeia nhantaro,aldeia nhathebo,aldeia nhocassaro,aldeia nkolongue,aldeia nomba,aldeia nova,aldeia nova da favela,aldeia nova de sao bento,aldeia nova de sao mateus,aldeia nova do barroso,aldeia nova do cabo,aldeia nova do concelho,aldeia nova viseu,aldeia novele,aldeia ntawa,aldeia ntayansupa,aldeia ntiuili,aldeia pacassa,aldeia palikur,aldeia panaema,aldeia panze,aldeia paracanti,aldeia paracaty,aldeia parakana,aldeia pequena,aldeia pereira,aldeia piaoro,aldeia piedade,aldeia piloto,aldeia pirizal,aldeia poca,aldeia queimada,aldeia ranchao,aldeia revue,aldeia rica,aldeia rodriques,aldeia s. tiago,aldeia sachi,aldeia sao jose,aldeia sao jose de ribamar,aldeia sasserimbe,aldeia satemua,aldeia saymica,aldeia senhora da gloria,aldeia silva,aldeia socossera,aldeia sunge,aldeia teixeira,aldeia tempue,aldeia thaca,aldeia toliri,aldeia toloiri,aldeia tombo,aldeia tubi,aldeia uchesse,aldeia udala,aldeia uladza,aldeia ulongue,aldeia unga,aldeia ute,aldeia utumuile,aldeia v. lenine,aldeia velha,aldeia verde,aldeia vicosa,aldeia videira,aldeia vila batista,aldeia vila formosa,aldeia vinte de setembro,aldeia voz da da frelimo,aldeia vumbene,aldeia waikiso,aldeia wanga,aldeia wikihi,aldeia wiriamo,aldeia xada,aldeia zamaicara,aldeia zamaicare,aldeia zamicare,aldeia zimete,aldeia zobue,aldeiamento,aldeias,aldeias das canoas,aldeias de montoito,aldeilla,aldeinha,aldeire,aldeita,aldekerk,aldemunde,aldemuz,alden,alden bridge,alden center,alden eik,alden manor,alden-eijck,alden-eyck,aldenburg,aldene,aldenham,aldenhof,aldenhoffen,aldenhoven,aldeni,aldeno,aldenrade,aldens,aldens corners,aldenville,aldenwood park,aldeola,aldeonsancho,aldeonte,alder,alder bend,alder branch,alder brook,alder creek,alder grove,alder springs,alder trees,alderback,alderbacken,alderbranch,alderbrook,alderbury,aldercroft heights,alderdale,alderene park,alderete,alderetes,alderford,alderglen springs,aldergrove,alderholt,alderhulten,alderi,alderiz,alderley,alderley edge,alderley park,alderlund,alderly,alderman,aldermans ford,aldermaston,alderminster,aldermysh,aldernaset,alderpoint,aldersbach,aldersgate,aldershof,aldershot,alderskog,alderslyst,alderson,aldersyde,alderton,alderu muiza,aldervale,alderwasley,alderwood manor,aldesh,aldesti,aldeyevka,aldeyevo,aldeyuso,aldford,aldgate,aldham,aldhoarne,aldi,aldiarovo,aldie,aldige,aldigraha,aldin,aldina reka,aldina seconda,aldinac,aldinc,aldince,aldinci,aldine,aldinga,aldinga beach,aldingen,aldingham,aldington,aldino,aldinville,aldir,aldirnason,aldivaz,aldiya,aldiyarovo,aldje,aldlan,aldlwark,aldo,aldo bonzi,aldoar,aldobayevka,aldoboly,aldobren,aldobulskiy,aldodarna,aldofalva,aldoma,aldoman,aldomiro,aldomirovci,aldomirovtsi,aldomirowzi,aldona,aldonino,aldoniskiai,aldora,aldorf,aldorpsen,aldosek,")dosende,aldouane,aldover,aldovin,aldozo,aldra,aldrans,aldreds,aldreu,aldrich,aldrich mills,aldridge,aldridge grove,aldringen,aldringham,aldriz,aldrup,aldrup-antrup,aldsanden,aldsworth,aldudes,aldueso,alduma,aldunate,aldur,aldurfe,aldus,alduvaz,aldwark,aldwarke,aldwincle,aldwincle saint peter,aldworth,aldworths bridge,aldwyck,aldy,aldy-shina,aldyn-bulak,aldyr,aldyshka,aldyy-shynaa,aldzhamyt,aldzhilar,aldzykh,ale,ale assamoua,ale asso,ale iock,ale kalaywa,ale kodu,ale kozi,ale kyaukkhok,ale kyidaung,ale linkon,ale lock,ale mahtein,ale mekane,ale minyon,ale ngapye,ale ninh,ale palaung,ale su,ale tinkang,ale wingon,ale-hman,ale-in,ale-kon,ale-man ywa,ale-on,ale-owena,ale-ywa,alea,aleabad,alean,aleanza,aleas,aleb el brek,aleback,alebacksas,alebaek,alebagiu,alebai,alebamba,aleban,alebanta,alebastr,alebastrovo,alebastrovyy,aleberg,alebert,alebino,alebiosu,alebla,alebo,alebohult,alebon,alebonou,alebontobonto,alebra,alebri,alebtong,alebtongo,aleby,alec chimponda,alecanadre,alecha,alechaung,alechikoua,alecrim,alecrineira,alecsandri,alectown,alecua,alecus,alecutuile,aled,aledadpur,aledaglii,aledal,aledaw,aledawa,aledimma,aledjame,aledji,aledjo,aledjo kadara,aledjo-koura,aledo,alef khan,alef khel,alef khelanwala,alef kheyl,alef shah kala,alefa,alefabad,alefaklar,alefberdi,alefberdy,alefbirdi,alefdan,alefeld,alefin,alefino,alefirovka,alefjaer,alefkhel,alefkheyl,alefsah kala,alefsan kala,alefsaneh,alefshan kala,aleg,alega,alegacion,alegada,alegan,alegaon,alegari,alegazovo,alegbetta,alegbette,alege pira,alegeria,aleges,alegin,aleginskaya,alegles,aleglo,alegma,alegna,alego,alegoma hacienda,alegon,alegongo,alegre,alegre de cima,alegre velho,alegre vista,alegres,alegrete,alegria,alegria de oria,alegria de pio,alegria del pio,alegria hacienda,alegria norte,alegria nova,alegria sur,alegria velha,alegrias,alegu,alegyaung,alegyaung atet,alegyaung auk,alegyaung-ywa,alegyun,aleh chal,aleh darreh bala,aleh darreh-ye sofla,aleh derreh-ye pain,aleh kabud,aleh qal`eh,aleheride,alehovstsina,alehtora,alei,aleibadei,aleibri,aleida,aleima,aleimmatades,alein ale,aleinikova,aleisk,aleixar,aleixo,aleizile,alejai,alejai i,alejal,alejandra,alejandrenas,alejandria,alejandrina,alejandrina lamas,alejandro,alejandro bass,alejandro bernate,alejandro carrillo,alejandro gallinal,alejandro korn,alejandro pascual,alejandro petion,alejandro roca,alejandro stefenelli,alejawan,alejazu,aleje,aleji,alejico,alejipan,alejo ledesma,alejo pata,alejo tiznado,alejo-kura,alejos,aleju,alejunai,alek,alek dari,alek pervyy,alek robat,alekpol,aleksandrapol,aleksandrobol,alekseevka,alekale,alekalimpo,alekan,alekanda,alekang,alekanovo,alekarajae,alekariya,alekarr,alekb,alekedon,alekere,alekeye-nikolsk,alekhanovo,alekhanovtsy,alekhar char,alekhin,alekhina,alekhinka,alekhino,alekhinskiye,alekhinskiye vyselki,alekhnaya,alekhnevo,alekhnova,alekhnovo,alekhovo,alekhovshchina,alekhovskiy,alekhtora,aleki,aleki ceneh,aleki chinah,alekichena,alekikoua,alekilek,alekina-polyana,alekino,aleklinta,aleknagik,aleknai,aleknaiciai,aleknaychyay,alekniskiai,aleknoniu,aleknonys,aleko konstantinov,aleko-konstantinovo,aleko-konstatinowo,aleko-kyuyel,alekokofi,alekon,alekova,alekovo,alekowo,alekowzay,alekowzi,alekoz,alekozai,alekozay,alekozi,alekozo gosh khanah,alekozo goshkhana,alekozo goskhana,alekpo,alekro,alekrou,aleksa santic,aleksakdrovskiy,aleksan,aleksanderge,aleksandersdorf,aleksandertal,aleksandervol,aleksandovka,aleksandr malinov,aleksandr-gey,aleksandra kosmodemyanskogo,aleksandra-nevskaya stanitsa,aleksandrai,aleksandrauka,aleksandraukos,aleksandrava,aleksandravas,aleksandravele,aleksandraveles,aleksandravskiy,aleksandraychyay,aleksandraytse,aleksandren,aleksandren,aleksandren nouy,aleksandreni,aleksandreni noua,aleksandreni-tyrg,aleksandreny,aleksandreshti,aleksandreshty,aleksandrestskaro,aleksandret,aleksandrfeld,aleksandri,aleksandria,aleksandria druga,aleksandria pierwsza,aleksandrieskaya,aleksandrija,aleksandrina,aleksandrine,aleksandrinka,aleksandrino,aleksandrinskiy,aleksandriske,aleksandritsyno,aleksandrivka,aleksandriya,aleksandriyskaya,aleksandrja,aleksandrja druga,aleksandrja krzywowolska,aleksandrja pierwsza,aleksandro,aleksandro-bibikovo,aleksandro-bibikovskiye vyselki,aleksandro-bogdanovka,aleksandro-donskaya,aleksandro-gnidovka,aleksandro-markovskiy,aleksandro-mikhaylovskoye,aleksandro-nevskaya,aleksandro-nevskaya stanitsa,aleksandro-nevskiy,aleksandro-nevskiy zavod,aleksandro-nevskoye,aleksandro-pavlovka,aleksandro-praskovinka,aleksandro-proskovinka,aleksandro-pudovoy,aleksandro-sergiyevka,aleksandro-vasilyevka,aleksandro-volynka,aleksandro-yersha,aleksandro-zhukovo,aleksandrobelovka,aleksandrodar,aleksandrofeld,aleksandrofeld,aleksandrogeym,aleksandrogilf,aleksandrograd,aleksandrogrigoryevka,aleksandroheim,aleksandrokalinovo,aleksandromikhaylovka,aleksandronevskaya,aleksandropol,aleksandropol,aleksandropole,aleksandropolye,aleksandroroshchakhovka,aleksandroshulgino,aleksandroshulkina,aleksandroshulkino,aleksandrotal,aleksandrotalskoye,aleksandrov,aleksandrov gaj,aleksandrov gay,aleksandrov grad,aleksandrova,aleksandrova kosa,aleksandrova pamyat,aleksandrova sloboda,aleksandrovac,aleksandrovac mikleuski,aleksandrovac pozeski,aleksandrovdar,aleksandrovka,aleksandrovka nomer pervyy,aleksandrovka number 1,aleksandrovka number 2,aleksandrovka pervaya,aleksandrovka pustyn,aleksandrovka tretya,aleksandrovka vtoraya,aleksandrovka-donskaya,aleksandrovka-rostovka,aleksandrovka-viktorovka,aleksandrovka-volynka,aleksandrovo,aleksandrovo yumatovka,aleksandrovo-donskaya,aleksandrovo-koldabash,aleksandrovo-koldobash,aleksandrovo-koldybosh,aleksandrovo-kostovka,aleksandrovo-marino,aleksandrovo-markovo,aleksandrovo-nevskiy,aleksandrovo-rostovka,aleksandrovo-zhukovo,aleksandrovsk,aleksandrovsk-grushevskiy,aleksandrovsk-grushevskoy,aleksandrovsk-sakhalinskiy,aleksandrovska,aleksandrovskaja,aleksandrovskaya,aleksandrovskaya gorka,aleksandrovskaya koloniya,aleksandrovskaya koloniya pervaya,aleksandrovskaya koloniya vtoraya,aleksandrovskaya pad,aleksandrovskaya sloboda,aleksandrovski,aleksandrovski zavod,aleksandrovskiy,aleksandrovskiy koldobash,aleksandrovskiy luzhok,aleksandrovskiy pervy,aleksandrovskiy pervyy,aleksandrovskiy pochinok,aleksandrovskiy poselok,aleksandrovskiy shlyuz,aleksandrovskiy vtoroy,aleksandrovskiy zavod,aleksandrovskiye,aleksandrovskiye verkhi,aleksandrovskiye vyselki,aleksandrovskoe,aleksandrovskoje,aleksandrovskoye,aleksandrovskoye gorodische,aleksandrovskoye pole,aleksandrovtsy,aleksandrow,aleksandrow kujawski,aleksandrow lodzki,aleksandrow maly,aleksandrow nowy,aleksandrowek,aleksandrowice,aleksandrowka,aleksandrowo,aleksandrowsk,aleksandrowska-stawka,aleksandrtal,aleksandru chel bun,aleksandru ioan kuza,aleksandru-chel-bun,aleksandruvka,aleksandrya,aleksandur stamboliyski,aleksandur voykov,aleksanrovka,aleksany,aleksapol,aleksashinskiy,aleksashkina,aleksashkino,aleksayeva,aleksayevka,alekseev,alekseevka,alekseevo,alekseevskaya,alekseevskoe,alekseevstsina,alekseikha,aleksejeva,aleksen,alekseni,aleksenki,aleksenkov,aleksevevka,aleksey-nikolskoye,alekseye-nikolsk,alekseye-nikolskoye,alekseye-tenginskaya,alekseyenka,alekseyenki,alekseyenkov,alekseyenkovo,alekseyev,alekseyeva,alekseyeviche,alekseyevichi,alekseyevka,alekseyevka bolshaya,alekseyevka nomer odin,alekseyevka nomer pervyy,alekseyevka pervaya,alekseyevka vtoraya,alekseyevke,alekseyevko,alekseyevo,alekseyevo vtoroye,alekseyevo-lozovskaya,alekseyevo-lozovskoye,alekseyevo-pudovoy,alekseyevo-tenginskiy,alekseyevo-tuzlovka,alekseyevodruzhkovka,alekseyevoorlovka,alekseyevshchina,alekseyevsk,alekseyevskaya,alekseyevskaya buda,alekseyevskiy,alekseyevskiy pervyy,alekseyevskiy poselok,alekseyevskiy vtoroy,alekseyevskiye,alekseyevskiye vyselki,alekseyevskoye,alekseyevskoye staroye,alekseyevskoye-novoye,alekseyki,alekseykovo,alekseyovka,alekseyskoye,alekseytsevo,alekseyvka,aleksi,aleksic,aleksichi,aleksici,aleksiejevka,aleksiejewka,aleksievo,aleksiki,aleksikovo,aleksikovskiy,aleksin,aleksin shatur,aleksina,aleksina gora,aleksina meda,aleksinac,aleksinacki bujmir,aleksinacki rudnici uglja,aleksince,aleksine,aleksinets,aleksinets podlesnyy,aleksinets polnyy,aleksinetspodlesny,aleksinetspolny,aleksinica,aleksinichi,aleksino,aleksino-shatur,aleksino-tugoles,aleksino-tugoless,aleksinskaya,aleksintsevo,aleksiyanovka,aleksiyen,aleksndrovka,aleksota,aleksotas,aleksoto,aleksotova,aleksotovo,aleksovo,aleksoyavka,aleksoyevka,aleksyeevka,aleksyeevski,aleksyeevskoye,aleksyeyevka,aleksyeyevo-tuvlovka,aleksyunina,aleksyutniki,alekszandrapuszta,alektora,aleku,alekulla,alekvere,alekwin,alekzai,alel,alela,alelabae,alele,alelerin,aleles,alelesi,alelevo,alelimpo,alella,alelli,alelo,alelpaich,aleltu,alelu,aleluba,aleluia,aleluia de baixo,aleluyoc,alelychok,alem,alem beyler,alem catema,alem cue,alem da ribeira,alem da veiga,alem das ribeiras,alem do rio,alem parahyba,alem paraiba,alem rio,alem sher,alem tejo,alem tenna,alema,alemakrowa,alemamya,aleman,alemande,alemandra,alemanguan,alemania,alemaniya,alemankotkay,alemao,alemao do francisco,alemas,alemasi,alemata,alemay,alemaya,alemayevo,alemba,alembe,alembetogo,alembey,alembeyli,alembic,alembon,alembonda,alemdag,alemdagi,alemdar,alemder,alemdoun,aleme,alemeda,alememer,alemen,alemere,alemi,alemler,alemli,alemlik,alemna,alemoa,alemodji,alemparte,alemquer,alemrhou,alemsa,alemsahli,alemtanou,alemyaung,alen,alen cue,alen effak,alen esseng,alen nkogadza,alen nkok-aza,alen-abanga,alen-como,alenakiri,alenane,alenasi,alenass,alenassi,alenava,alencar,alencarce de baixo,alencarce de cima,alence,alencon,alenda,alenda wharf,alendan,alendarovi,alendorf,alendorp,alene,alene bissouma,alene koraza,alene makora,alene mianga,alenenviita,aleneva,alenevka,aleng,alengbele,alengbelo,alengenzure,alenggae,alengge,alenggeagung,alengkong,alengo,alengoz,alengshe,alenguel,alengui,alenguy,aleni,alenina,alenina lhota,alenino,aleninsk,aleninskoye,alenis,alenj,alenjak,alenjaliq,alenjan,alenjaq,alenje iju,alenje ile,alenjeh,alenka,alenkash,alenkino,alennaya,alenovichi,alenovka,alenovskiy,alenquer,alentejo,alentem,alenthun,alentisque,alentorn,alentui,alentuy,alentyevka,alenya,alenya palacios,alenyay,alenz,alenzu,alenzua,aleon,aleosebre,alep,alepa,alepage,aleparawan,alepaul,alepe,alepi,alepino,alepochori,alepochorion,alepokhori,alepokhorion,alepokhorion botsari,alepospita,alepou,aleppe,aleppi,aleppo,alequ,aler,aler-atar,alera,alerang,aleras,alerce,alere,alerek,alerey,alerheim,aleri,aleria,alerici,alermaut,alerne,alero,alerotar,alerre,alert,alert bay,alerta,alerti,alertshausen,aleru,alerwey,ales,alesalewo,alesaluwala,alesanco,alesandro-bogdanovka,alesandrovtsy,alesane,alescifa,alesd,alese,alesehir,alesengyaung,alesevici,alesh dasht,alesha-ashamo,aleshane,aleshanka,alesheim,aleshenka,aleshevo,aleshi chisenda,aleshi flaichangwe,aleshichi,aleshikha,aleshin,aleshina,aleshina niva,aleshinka,aleshino,aleshinskaya,aleshinskiy,aleshinskiye,aleshinskoye,aleshiny,aleshiny nivy,aleshk,aleshki,aleshkin,aleshkin mys,aleshkin saplyk,aleshkina,aleshkina kozha,aleshkino,aleshkinskiy,aleshkiny,aleshkova,aleshkovichi,aleshkovo,aleshkovo-valuyevo,aleshkovskaya,aleshkovskiye khutora,aleshnaya,aleshniki,aleshnya,aleshnyugi,alesho,aleshok,aleshonka,aleshonki,aleshovo,aleshtar,aleshtar-e `olya,aleshunino,aleshutino,alesi,alesia,alesia heights,alesii cioconesti,alesinovo,alesipitto,alesker,aleskerli,aleskerly,aleskirt,aleslov,alesninkai,aleson,alesovo,alessandra,alessandretta,alessandria,alessandria del carretto,alessandria della rocca,alessandro,alessano,alessio,alesso,alesta,alestaina,alestan,alesteh,alestia,alestrup,alesund,alesundet,alesville,alesy,alet,alet yungyaung,aletis,alet-les-bains,aleta,aletai,aletaiyuchang,aletellue,aletengxire,aletengxire zhen,alethangvaw,alethangyaw,alethaung,alethriko,alethrikon,aleti,aletinskiy,aletirke,aleton,aletones,aletoutou,aletovo,aletraiika,aletrike,aletriko,aletrouvari,aletrouvarion,aletsberg,aletshausen,aletshofen,alettasrus,alettasrust,alette,aletu,aleu,aleu bili,aleu kuyun,aleu lhok,aleu mancang,aleu meganda,aleu pendeung,aleu sekayah,aleu seralen,aleu sundak,aleubata,aleuleun,aleuntsy,aleur,aleus,aleuthe,aleuthen,aleutka,alev,aleva,alevaytsino,alevaytsyno,alevga,alevi,alevi-shua,alevia,aleviken,aleviku,alevisik,alevka,alevkaya,alevkisla,alevkulova,alevkulovo,alevra,alevradha,alevrika,alevrou,alewa heights,alewabi,alewahi,alewai,alewakwak,alewala,alewali,alewife,alewife brook landing,alex,alex boggio place,alexain,alexander,alexander bay,alexander bay station,alexander beach,alexander city,alexander corner,alexander crossroads,alexander liti,alexander mill,alexander mills,alexander mununka,alexander place,alexander springs,alexander village,alexanderbaai,alexanderdorf,alexanderfeld,alexanderhausen,alexanderhof,alexanderhohe,alexanderhutte,alexanderkirchen,alexanderpolder,alexanders corner,alexanders mill,alexanders village,alexandersbad,alexandersfontein hotel,alexandersreut,alexanderville,alexandhra,alexandhradhes,alexandhros,alexandhroupolis,alexandra,alexandra bridge,alexandra estate,alexandra park,alexandra-cel-bun,alexandra-mooloolaba,alexandra-puszta,alexandradhes,alexandre,alexandre bode,alexandre de gusmao,alexandre dumas,alexandre mendes,alexandre uamunguene,alexandreia,alexandreni,alexandreni targ,alexandrenii noi,alexandresti,alexandretta,alexandrette,alexandri,alexandria,alexandria ad issum,alexandria bay,alexandria center,alexandria junction,alexandria nova,alexandrie,alexandrija pervaya,alexandrina,alexandrinenthal,alexandrita,alexandro-mikhailovskoye,alexandro-nevski,alexandronevskaya,alexandropol,alexandros,alexandroupoli,alexandroupolis,alexandrov,alexandrovca,alexandrovca noua,alexandrovka,alexandrovo,alexandrovsk,alexandrowka,alexandrowka newkus,alexandrowo,alexandrowskoje,alexandru cel bun,alexandru cretu,alexandru ioan cuza,alexandru juganaru,alexandru lahovari,alexandru odobescu,alexandru vaida voevod,alexandru vaida voivod,alexandru vlahuta,alexauken,alexcieni,alexeeni,alexeevca,alexeevka selo,alexeieni,alexenau,alexeni,alexesti,alexeuca,alexi,alexia,alexieni,alexikovo,alexis,alexis addition,alexis creek,alexis junction,alexisbad,alexisdorf,alexishafen,alexishaffen,alexishaven,alexo,alexovice,alexrandina,alexsandrovka,alexsandrovo,alexsandrovskiy zavod,alexseyevskoye,alexwangen,aley,aley yaokro,aleyar,aleyegyaw,aleyeva,aleyevka,aleyevo,aleyhan,aleykino,aleyniki,aleynikov,aleynikova,aleynikovo,aleyrac,aleysk,aleyskaya,aleyskiy,aleyskoye,aleywa,aleza lake,alezantrovo,alezg,alezgerew,alezio,alezu,alf,alfa,alfa barra,alfa indah,alfa iwo,alfa koura kwara,alfa koura-koara,alfa kwara,alfa mama,alfa ntikki,alfa sadou,alfa sorire,alfa-koara,alfaavot,alfabad,alfabe,alfabli,alfacar,alface,alfachayoc,alfaco,alfadanga,alfademata,alfafar,alfafara,alfagei,alfages,alfagia,alfahuir,alfaiao,alfaiate,alfaiates,alfaiz,alfaize kwara,alfaize-koara,alfajarin,alfajayuca,alfajayucan,alfajia,alfaklar,alfakoara,alfakouara,alfakpara,alfala,alfala society,alfalah,alfalayuca,alfalfa,alfalfa center,alfalfal,alfalfalito,alfalfia,alfalfilla,alfalfito,alfalter,alfalu,alfama,alfambra,alfamen,alfandega,alfandega da fe,alfanete,alfange,alfangen,alfano,alfantega,alfao,alfapur,alfaquiques,alfar,alfara,alfara de algimia,alfara de carles,alfara del patriarca,alfarara,alfarata,alfaraz,alfarazes,alfarcito,alfarela,alfarelos,alfareria,alfari,alfaribe,alfarillo,alfarim,alfaritanya,alfarnate,alfarnatejo,alfarnes,alfaro,alfarp,alfarras,alfarrasi,alfarrobeira,alfas,alfassi,alfatah,alfataouess,alfatar,alfatares,alfatchi koy,alfatl khadzhilar,alfatlar,alfatlu,alfaut,alfauut,alfavet,alfaville,alfavot,alfavut,alfaxemo,alfayili,alfayinga,alfayoc,alfazemo,alfazer,alfdorf,alfe menashe,alfedena,alfeicao,alfeiria,alfeite,alfeizerao,alfelaar,alfeld,alfelen,alfeloas,alfen,alfen-boschoven,alfena,alfenas,alferavo,alferberg,alferce,alferde,alfere,alferes,alferez,alferez san martin,alfergemil,alferikha,alferitsyno,alferkovo,alfero,alferova,alferovka,alferovo,alferovskaya,alferovskiy,alferovskoye,alferrarede,alfershausen,alfersteg,alfert,alfertishchevo,alferyev,alferyevka,alferyevo,alferyevskoye,alferzhagen,alfes,alfeti,alfeu,alfeu manhique,alfhausen,alfheim,alfhem,alfiado,alfiano natta,alfiano nuovo,alfiarou,alfimkovo,alfimova,alfimovo,alfimovoka,alfinar,alfinete,alfios,alfiousa,alfire,alflen,alfmil,alfo,alfocea,alfocheira,alfold,alfoldszallas,alfoldszallasok,alfoloes,alfondeguilla,alfonge,alfonovo,alfonsina,alfonsine,alfonsito,alfonso,alfonso bezerra,alfonso caso,alfonso castaneda,alfonso loma,alfonso lopez,alfonso moguel,alfonso ugarte,alfonso xii,alfonso xiii,alfonsovskiy,alfonsow,alfont,alfontes,alfonzo,alfonzo lopez,alfonzo terrones benitez,alford,alford acres,alford forest,alford place,alfords,alfordsville,alforgemel,alforins,alforja,alforma,alforma bogwari,alfornelos,alfornon,alforque,alfors,alfortville,alfoten,alfouro,alfourtouk,alfouvar de baixo,alfouvez,alfragide,alfrech,alfrecha,alfred,alfred mills,alfred station,alfred town,alfredo,alfredo baquerizo moreno,alfredo brener,alfredo brenner,alfredo castilho,alfredo chaves,alfredo chavez,alfredo dantas,alfredo dutra,alfredo guedes,alfredo m. terrazas,alfredo manso,alfredo marcondes,alfredo pena,alfredo silveira,alfredo teixeira,alfredo uassunhane,alfredo v bonfil,alfredo v. bonfil,alfredo v. bonlil,alfredo valdez montoya,alfredo wagner,alfredov,alfredow,alfredowka,alfredshem,alfredshohe,alfredta,alfredton,alfreton,alfrey,alfriston,alfrivida,alfsborg,alfsta,alfstedt,alfsund,alfta,alftamyri,alftanes,alftartunga,alfter,alfter-gielsdorf,alfton,alfuke,alfundao,alfva,alga,alga-bas,algaba,algabacksryd,algabas,algabaz,algaca,algach,algachi,algad,algada khullah,algadai,algadefe,algadi,algadi khwula mela,algaengi,algaesil,algafall,algafallen,algafia,algaida,algain,algainskiy,algair,algajola,algal,algalah,algale,algama,algama egodagama,algama ihalagama,algama medagama,algamarca,algamarca hacienda,algamarce hacienda,algame,algamitas,algan,algana,alganas,algancha-igra,algandi coma,alganhafres,algans,algansee,alganskaya,alganskaya povarnya,algao,algar,algar de mesa,algar de palancia,algara,algaras,algard,algarden,algares,algarga,algari,algarinejo,algarkirk,algarmejo,algarna,algarra,algarraba,algarrabo,algarroba,algarroba verde,algarrobal,algarrobal viejo,algarrobas,algarrobe,algarrobera,algarrobico,algarrobilla,algarrobillito,algarrobillo,algarrobillos,algarrobito,algarrobitos,algarrobo,algarrobo del aguila,algarrobo del aguilla,algarrobo grande,algarrobo verde,algarrobol,algarrobos,algarrobos grandes,algarrobote,algarrovillo,algarvia,algaryd,algasen,algash,algashet,algashi chuvashskiye,algashi russkiye,algashtyk,algasing,algasovo,algasy,algatart,algatino,algatocin,algatuy,algaundi coma,algavila,algayevo,algayon,algaysk,algayskaya,algayskiy,algaz,algaza,algazai,algazeya,algazina,algazino,algazir,algaziya,algazy,algazykipchak,algdinsk,alge,algebo,algeciras,algedik,algeh zahad,algema,algemesi,algen,algena,algenrodt,algenroth,algenstedt,alger,alger plage,algeraz,algered,algeri,algeria,algerie four corners,algerine,algerita,algeriz,algermissen,algeroy,algerri,algers,algers corner,algers mills,algersdorf,algershofen,algert,algerting,algertshausen,algerum,alges,algesdorf,algeshevo,algesta,algestad,algestrup,algeti,algeta,algete,algezares,algha,alghabas,alghallen,alghamchi,alghamci,alghar,alghe,algheden,alghena,algheremariam,alghero,alghoi,alghoi,algholmen,alghornsudd,alghowan,alghui,alghucak,alghuchak,alghulfan,alghult,alghur,algi,algi mukundi,algiado,algida,algie,algier,algiers,algies bay,algiez,algilaga,algileb,algimantai,algimantay,algimia de alfara,algimia de almonacid,alginet,alginka,alginskiy,algir char,algishofen,algit,algjin,algjinaj,algkarr,alglund,algmo,algmossa,algmossen,algnas,algo,algoa,algoapark,algoceira,algoda,algodao,algodoal,algodoes,algodon,algodona,algodonal,algodonales,algodonalillo,algodoncillo,algodonera banjidal,algodones,algodor,algodre,algodres,algohuain,algoinhas,algolsheim,algoma,algoma park,algon,algona,algonac,algongan,algonquin,algonquin hills,algonquin park,algoo,algood,algora,algorfa,algorg,algorrobal,algorrobo,algorta,algosh,algosinho,algoso,algou,algoud,algoum,algouz,algouze,algovka,algoz,algozir,algram,algrange,algrem,algren,algrena,algroy,algryt,algsjo,algsjobo,algsjos alen,algtan,algtrask,algtraska,alguaire,alguajoyucan,alguazas,alguber,algudi,algueirao,alguena,alguer,alguerdo,algues,alguieira,alguji,algukasy,algul,algumdia,algunas,algund,algunja,algupe,algupys,algur,algusered,alguserud,algustorp,algutsrum,algutstorp,algviken,algya,algyest,algyo,algyt,algyti,alh ben frea,alhai,alhabchiyeh,alhabia,alhacemas,alhacone,alhadas,alhadere,alhadi,alhadji,alhadji arou,alhadji dango,alhadji dodo,alhadji sami,alhadpur,alhais,alhais de cima,alhaj kadhim,alhaji,alhaji abdoulie liegh,alhaji bajonkoto,alhaji dembo,alhaji dula,alhajiri,alhajuela,alhak,alhala,alhama,alhama de almeria,alhama de aragon,alhama de granada,alhama de murcia,alhambra,alhamilla,alhammar,alhamn,alhampton,alhamra society,alhamsa,alhan,alhan mahallesi,alhan-a\302\277urt,alhan-hutor,alhan-jurt,alhan-kala,alhanda khan,alhanda khan jamali,alhandra,alhani,alhanli,alhanusagi,alhar,alhard,alharilla,alharo jat,alharod,alharpind,alharting,alhas,alhashem-e `olya,alhashem-e sofla,alhasi,alhassan,alhasusagi,alhatli,alhau,alhaurin de la torre,alhaurin el grande,alhausen,alhaze,alhazurovo,alhazy,alhebal,alhehi,alheim,alheira,alheira de aquem,alheit,alhendin,alhentennawatta,alherd,alhez,alhinapur,alhla,alhlasserdam,alho,alhoaio do ngalo,alhoes,alhoga,alhojarvi,alhojoki,alhoka,alhokpa,alholm,alholmen,alholpura,alhondiga,alhonjoki,alhori,alhos vedros,alhoumont,alhua,alhuaca,alhuampa,alhucemas,alhuda,alhue,alhuey,alhuey de los lugos,alhult,alhurayzat al gharbiyah,alhurayzat ash sharqiyah,alhusstranda,alhusstranden,ali,ali abad,ali abad beirom,ali abad dasht ab,ali abad gheis abad,ali abad hesan,ali abad hoomeh,ali abad japlogh,ali abad karon,ali abad kheyrood kenar,ali abad looleh,ali abad mo men abad,ali abad momen abad,ali abad nahaar khan,ali abad nahar khan,ali abad rastagh,ali abad rostag,ali abad shahraki naroo`i,ali abad sofla,ali abad toobazan,ali abad vazir,ali abad-e-kur gol,ali abade sofla,ali abadme- shur,ali abdullah,ali ad dayan,ali ada,ali adda,ali adda courant,ali afzal shah pathan,ali aga,ali ahmad,ali ahmad chaudhry,ali ahmad goth,ali ahmad jokhio,ali ahmad shah,ali ak chin,ali akbar,ali akbar khan chandio,ali akbar koruna,ali akbar shah,ali akbar sheni malik,ali akbardeil,ali akbari,ali akdeh,ali akharak,ali al fedam,ali al haj,ali al haj `alwan,ali al mizil,ali al mizu,ali alikro,ali anife,ali aq,ali ashab,ali assan,ali assar,ali autra,ali baba ziarat,ali bahar,ali baig chandio,ali baig sultan,ali bairamly,ali bakhsh,ali bakhsh baloch,ali bakhsh bijarani,ali bakhsh brahui,ali bakhsh buriro,ali bakhsh dahani,ali bakhsh dars,ali bakhsh dumra,ali bakhsh jamali,ali bakhsh jarwar,ali bakhsh khokhar,ali bakhsh korkani,ali bakhsh mangrio,ali bakhsh mangwan,ali bakhsh mazari,ali bakhsh mughairi,ali bakhsh nizamani goth,ali bakhsh panhowar,ali bakhsh panhwar,ali bakhsh pitafi,ali bakhsh sanjrani,ali bakhsh sarki,ali bakhsh shah,ali bakhsh sial,ali bakhsh sundrani,ali bakhsh zardar,ali bakhshwala,ali band kili,ali banda,ali banga,ali bayramli,ali bayramly,ali beg,ali bei,ali bel rida,ali ben ahmed,ali ben ali,ali ben chama,ali ben ez zine,ali ben hamadi,ali ben izgem,ali ben izguem,ali ben izguen,ali ben slimane,ali ben yahia,ali berdada,ali bey,ali bey koy,ali bhatti,ali bhaul,ali bilari,ali bin nagi,ali block,ali bostivan,ali bou ameur,ali bou sid,ali bou zid,ali bu said,ali bula,ali bulagh,ali bundit,ali chagi,ali chah karez,ali chap karez,ali chappa,ali chhajan,ali chuk,ali chukson,ali coulibaly,ali da khuh,ali dad,ali dad khosa,ali daha,ali dakhan,ali dar,ali dasht,ali dera,ali dhabiani,ali dino,ali dino khan marri,ali diqu,ali dost,ali dost kili,ali dost magsi,ali drakhan,ali ed deba,ali el gharbi,ali el goumi,ali el-gharbiya,ali en nahri,ali ersoy,ali esh sharki,ali faku,ali farom,ali gala,ali gane,ali ganjwala,ali garga,ali garh colony,ali gasar,ali gauharwala,ali gedi,ali gharbi,ali gharh,ali ghulam marri,ali gohar,ali gohar bhutta,ali gohar bhutto,ali gohar khan bhuto,ali gohar khan nari,ali gohar lohar goth,ali gohar shah,ali goharwala,ali goher gujrani,ali gul jatoi,ali gul juneja,ali gul junejo,ali gul kili,ali hadiyeh,ali haibat khan,ali haidarpur,ali haider shah,ali haiderabad,ali haji,ali hamadan,ali hamadou lilae,ali hasan,ali hasan jamali,ali hasan khosa,ali hasan ramezai,ali hashimpur,ali hassain,ali hebe,ali hedye bala,ali hedyeh pain,ali hoseyni,ali husain marri,ali ibrahim,ali isa,ali jaffar,ali jaj,ali jakhrani,ali jam jat,ali jan,ali jan brahui,ali jan damki,ali jan kili,ali jan koruna,ali jangi,ali jaspalanwali,ali jat,ali ji wand,ali keno,ali ka banda,ali ka walgan,ali kach,ali kachha,ali kadal,ali kand,ali kanyenda,ali karumiri,ali kauri,ali ke basti,ali ke vahniwal,ali khan,ali khan awan,ali khan chandio,ali khan gadiwan,ali khan jamali,ali khan jo goth,ali khan kanasro,ali khan khel,ali khan khelanwala,ali khan khoso,ali khan kili,ali khan laghari,ali khan lashari,ali khan mahar,ali khan patai,ali khan rind,ali khan talpur,ali khan wasul,ali khanana,ali khanwala,ali khanzai kili,ali kharak,ali khei,ali khel,ali khel mehla,ali khelanwala,ali khelwala,ali khera,ali khonai,ali khonal,ali ki basti,ali ki jhuggian,ali kodji,ali koh,ali kola,ali kot,ali kurmiri,ali lahr,ali lak,ali lal,ali lazreg,ali lmuhammad soho,ali lobasi,ali lucho,ali lughei,ali mkayil,ali machhi,ali madad shah,ali magsi,ali mahmood,ali mahmud,ali makuro,ali malik mar,ali manadji,ali mangal,ali mangal post,ali mangal rest house,ali maran,ali mardan hajwani,ali mardan jamali,ali mardan shah,ali mardan shahban,ali mardan shahbani,ali marina,ali masjid,ali mast,ali mast kili,ali mboko,ali memon,ali mohammadpur,ali moia,ali molina,ali morad,ali moumene,ali moya,ali mude,ali mughul,ali muhammad,ali muhammad ajwani,ali muhammad arain,ali muhammad awan,ali muhammad baghio,ali muhammad baloch,ali muhammad bhandar,ali muhammad bhijro,ali muhammad bhurgari,ali muhammad brahui,ali muhammad buhar,ali muhammad butro,ali muhammad chahar,ali muhammad chandio,ali muhammad dahri,ali muhammad dakhna,ali muhammad dal,ali muhammad dars,ali muhammad dhandro,ali muhammad ferozani,ali muhammad gad,ali muhammad gado,ali muhammad garhi,ali muhammad gil,ali muhammad gola,ali muhammad goth,ali muhammad hingorjo,ali muhammad jamali,ali muhammad jat,ali muhammad jattak,ali muhammad jo goth,ali muhammad jokhio,ali muhammad kahar,ali muhammad kalru goth,ali muhammad khalifa,ali muhammad khaskheli village,ali muhammad khosa,ali muhammad khoso,ali muhammad kili,ali muhammad korai,ali muhammad kotai,ali muhammad machhi,ali muhammad mangryo goth,ali muhammad mohana,ali muhammad otho,ali muhammad pitafi,ali muhammad rajpar,ali muhammad sanjrani,ali muhammad sanyaro,ali muhammad shah,ali muhammad solangi,ali muhammad soomro,ali muhammad sumra,ali muhammad sumro,ali muhammad thaim,ali muhammadpur,ali muhammadwala,ali murad,ali murad bagrani,ali murad bhatti,ali murad bhutto,ali murad faqir,ali murad jamali,ali murad kalhora,ali murad khan,ali murad khan chang,ali murad khan lashari,ali murad rahu,ali murad rind,ali murad sangro,ali murad shah,ali murad talpur,ali murad tanwari,ali muradwari wand,ali nayer,ali nawar jo goth,ali nawaz,ali nawaz dharpali,ali nawaz goth,ali nawaz jalbani,ali nawaz khan panhar,ali nawaz khoso,ali nawaz laghari,ali nawaz mughairi,ali nayir,ali ndene,ali niaz khan kalhora,ali niazwala,ali nur ali,ali oba,ali oidak,ali ou ali,ali ou brahim,ali ou daoud,ali ou grou,ali ou ikko,ali ou jdid,ali ou lahsen,ali ou mohammed,ali ou said,ali ou said imri,ali ou zaid,ali ouidja,ali oulad mbarek,ali ouri,ali ouzinib,ali panik,ali pasha,ali pashinovo,ali pesa,ali pinak,ali pinek,ali poharwala,ali punga,ali qalandar ziarat,ali qasim kili,ali qatalwala,ali qeshlaq,ali qutub shah,ali rash,ali rawana,ali rayer char,ali raza banda,ali razaabad,ali sabie,ali sabiet,ali sadd,ali sahib,ali sakalani,ali sangi,ali saujal,ali ser kalay,ali shah,ali shah `iwaz,ali shah gidranwali,ali shahedan,ali shahi,ali shahwala,ali shaikh,ali shan,ali sher,ali sher bugti,ali sher chandio,ali sher gopang,ali sher goth,ali sher jaskan,ali sher jaskani,ali sher khan bugti,ali sher khan jamali,ali sher kili,ali sher mahar,ali sher metla,ali sher tunia,ali sher wahon,ali sheri,ali sherwala,ali sherwan chak,ali sheykh,ali sial khel,ali souloumri,ali superiore,ali taliri kalay,ali taragarh,ali tselepi,ali u ali,ali uzinib,ali wahan,ali wanda,ali wigamal,ali yoro ndero,ali zai,ali zangana,ali-alikro,ali-bairamli,ali-bayramli,ali-bayramly,ali-berdukovskiy,ali-bey-kanaga,ali-bolag,ali-chelepi,ali-jilamari,ali-ken,ali-khelepi,ali-kirjala,ali-kuyelv,ali-madatly,ali-mammet-arab,ali-mogul,ali-muratly,ali-shakh,ali-sherzay,ali-ukwa,ali-vekkoski,ali-viiri,ali-yurt,ali-ywa,alia,alia bakri,alia garma,aliab,aliab dok,aliab east,aliabad,aliabad ard,aliabad cham,aliabad kishmar,aliabad sarai,aliabad-e baqerof,aliabad-e dotu,aliabad-e homayuni,aliabad-e maran,aliabad-e qarneh,aliabad-e qeysariyyeh,aliabad-e shamshir bor,aliabad-e tapancheh,aliabochiyan,aliabociyan,aliac,aliade,aliadh,aliado,aliafgan,aliag,aliaga,aliaga ciftligi,aliaga hacienda,aliagaciftligi,aliagali,aliagaro,aliaguilla,aliagwai-agbor,aliah b kubhar,aliah bakhsh bajrani,aliahad,aliahaly,aliaj,aliajbegu,aliak,aliakalan,aliakhkulular,aliali,alialia,alialiala,aliam,aliamanu,aliambata,alian,alian dheri,aliana,alianbale,alianca,alianca do tocantins,alianca velha,aliance,aliande,aliane da dera,alianello,aliang,aliangaw,aliani,aliano,alianquile,aliantan,alianwala,alianza,alianza cristiana,aliapur,aliar,aliara,aliarom,aliarpara,aliartos,aliasse,aliat,aliatar,aliaty,aliaugon,aliavalai,aliavar,aliawa,aliawali,aliawan,aliayabiagba,aliaza,aliba,alibabici,alibadabad,alibag,alibagh,alibagi,alibagi koyu,alibago,alibagon,alibagu,alibahadir,alibakhram,alibalta,aliban,alibanfa,alibangbang,alibangen,alibangon,alibangsay,alibani,alibar,alibarat,alibarate,alibardak,alibari,alibaroho,alibaruhu,alibas,alibasa,alibasao,alibasici,alibawa,alibay,alibayan,alibayeva,alibayevo,alibayevskiy,alibayevskoye,alibayli,alibayog,alibayqislaq,alibegi,alibegov dubovik,alibegovci,alibegovica zalozje,alibegovici,alibei-ceair,alibeili,alibek,alibekkyshlakh,alibeklu,alibekotar,alibeng,aliberdukovskaya,aliberdy,alibese,alibey,alibey kirani,alibey koyu,alibey makhlesi,alibey oba,alibeycagili,alibeyce,alibeyciftligi,alibeyevo,alibeyhoyugu,alibeyhuyugu,alibeykoy,alibeykoyu,alibeykyshlak,alibeyler,alibeyli,alibeyli pervyye,alibeyusagi,alibezirgan,alibi,alibijaban,alibir,alibisu,alibizu,alibjon,alibo,alibocyan,alibog,alibokolo,alibolagi,alibon,alibonca,alibonco,aliboti,alibotu,alibou,alibozlu,alibrada,alibrahimaj,alibu,alibuag,alibueng,alibug,alibumbungan,alibunan,alibunar,alibunbungan,alibyeva,alibyevo,alic,alica,alicaca,alicad,alicahue,alicak,alical kunda,alicali kunda,alicama,alican,alican agili,alican chico,alicanli,alicante,alicantes,alicaoren,alicbagi,alicbel,aliccik,alice,alice acres,alice arm,alice b,alice castello,alice ingram subdivision,alice river,alice springs,alice superiore,alice town,alice-major,alicebeler,alicedale,alicel,alicene,aliceni,alicerci,aliceri,aliceton,alicevici,aliceville,aliceyrek,alich,alicha-bulak,alichali,alichelepi,alichin,alichur,alici,alicia,alicia de penaflor,alicici,alicik,alicipi,aliciul-mare,alick chisi,alick cisi,alick ndawandawa,alickoy,aliclar,alicli,alicliagil,aliclibel,aliclibucak,aliclik,aliclikuyu,alicliseki,alico,alicomoban,alicopenge,alicoren,alicoto,alicozu,alicqislaq,alicta,alicu,alicun,alicun de ortega,alida,alidaa,alidad,alidad kali,alidad khoso,alidad kili,alidad rajar,alidadi-karbalai,alidam,alidar,alidaya,alide,alidede,alidedebolmesi,alidem,alidemirci,aliden,alideola,aliderce,alidero,alidhani,alidiamani,alidima,alidina,alidou kwara,alidou-koara,alidounpo,alidzham,alidzhamli,alidzhan,alidzhankala,alidzhankheyl,alidzhanli,alidzhanly,alidzhanly pervyye,alidzhanly vtoryye,alie,alief,aliefe,aliefendi,aliefendiler,aliejunai,aliel,alieman,aliemano,alieme,alien,aliena,alienchuang,alienes,alieni,alieni i,alieni ii,alientsun,aliero,aliet,alieze,aliezo,alif,alif khan banda,alif khan kili,alif khan shahwani,alif khel,alif kili,alif mirjanzai,alif safed,alif shah kala,alif wahan,alifa,alifabougou,alifaca,alifaci,alifadougou,alifak,alifaka,alifaki,alifakili,alifakovo,alifanda,alifandaka,alifane,alifanete,alifanov,alifanovshchina,alifare,alifari,alifarom,alifbayrdi,alife,alifen,alifeu,alifeyo ngambe,alifi,alifilik,alifilin,alifino,alifira,alifkheyl,alifkulova,alifkulovo,alifo,alifodes,alifodez,alifokpa,alifori,alifotes,alifow,alifshah kala,alifshakhkala,aliga,aligada,aligadi,aligaga,aligah,aligai,aligaiyufa,aligak,aligalabon,aliganj,aliganja,aligaon,aligapar,aligaper,aligar,aligarh,aligarh colony,aligaykala,aligba,aligbo,aligbolu,alige,aligedik,aligegeo,aligema,aligerce,aligerikouadiokro,aligh ntarga,aligholo,alighulashen,alighuli ashaghy,aligi ada,aligi ado,aligider,aligidir,aligina makhala,alignan,alignan-du-vent,aligo,aligoodarz,aligor,aligoran,aligoudo,aligovska,aligovski,aligowska,aligozlek,aligram,aligramai,aligse,aligu,aliguay,aliguayan,aliguda,aligudarz,aliguian,aligul,aligvarda,aligvarhegy,aligvarihegy,aligvarimajor,aligvaritanya,aligvarmajor,aligvarom,aligvaromhegy,aligvaromi szollo,aligvaromtelep,aligway,aligyaung,alihagu,alihagun,alihagwu,alihak,alihakar,alihamdan,alihan,alihargati,alihari,alihawan,alihe,alihe zhen,aliheykos,alihido,aliho,alihoca,alihoca koyu,alihocalar,alihocalardivani,alihocali,alihodes,alihodzici,alihoui,alihuso,alii kai,aliismail,aliismailly,aliismayilli,alij,alija,alija de la ribera,alija de los melones,alijamadu,alijan,alijan kili,alijao,alijar,alijauan,alijawan,alijazu,alijeh gerd,alijemisi,aliji,alijilan,alijis,alijis hacienda,alijo,alijoda,alijogan,alijon,alijosiskes,alik,alik chilembo,alik ghund,alik rabat,alik ribat,alikulu,alika,alikabaklar,alikabakli,alikad,alikahya,alikaj,alikaku,alikala,alikalacha,alikali,alikalia,alikalkan,alikamber,alikambos,alikamer,alikamerli,alikamora,alikan,alikana,alikanas,alikanber,alikand,alikanitaiika,alikanna,alikarak,alikariya,alikarnassos,alikasumly,alikasymly,alikata,alikchi,alike,alike rohela,alike rohilla,alikeai,alikech,alikelle,alikend,alikendorf,alikene,alikerava,alikere,alikeru,alikeykhaly,alikeyxali,alikhafa,alikhalipara,alikhan,alikhan-payasy,alikhani,alikhankheyl,alikhankote,alikhanly,alikhanly-shirin-bulag,alikhanmakhi,alikhanzai,alikhanzay,alikhanzi,alikharaq,alikhel,alikhen,alikher,alikheyl,alikhodzha,aliki,aliki dhomvrainis,alikianos,alikianou,alikikoya,alikiran,alikiri,alikirpun,alikishani,aliklas,alikma,aliko,alikoassue,alikochovo,alikoglebi,alikoja,alikoje,alikokh,alikolan,alikolo,alikon,alikonovka,alikope,alikor,alikoren,alikorusu,alikose,alikoski,alikot,alikoto,alikovka,alikovo,alikoy,alikozai,alikrak,alikro,alikrou,alikrykh,aliksan,alikuan,alikuchak,alikuh,alikui,alikular,alikulek,alikulero,alikuli,alikulihushaghy,alikulikyand,alikulikend,alikulikyand,alikuliushagy,alikulu,alikulular,alikuluushagly,alikurt,alikutuzi,alikuyu,alikyla,alikzai,alikzay,alil-mahala,alila,alilabasi,alilah,alilala,alilambayli,alilambeyli,alilao,alileh sar,alilem,alili,alilia,aliliang,aliliha,alililawi,alilintao,alilinu,alilio,alilioi,alilit,aliliw,alillaret,aliloba,alilovce,alilovci,aliltu,alilu,aliluci,aliluyane,alim,alim gul,alim hingoro,alim shah,alim-tepe,alima,alimabad,alimad,alimadadli,alimadatli,alimadatly,alimadene,alimadtli,alimainen,alimaka,alimaki,alimakion,alimalek,alimamed-kishlagi,aliman,alimanesti,alimango,alimanguan,alimannao,alimansur,alimantanya,alimantar,alimanul,alimanza,alimardanli,alimardanly,alimas,alimasi,alimat khel,alimawan,alimaya,alimba,alimbaba,alimbagi,alimbangeng,alimbay,alimbek,alimbek-chek,alimbet,alimbetka,alimbetovka,alimbetovo,alimbit,alimbongo,alimdam,alimdar,alime,alimede,alimedska maala,alimedska mahala,alimei,alimena,alimenda,alimepingo,alimgulovo,alimi,alimia,alimicamba,alimillo,alimin,alimingoro,aliminos,aliminusa,alimishmish,alimismis,alimit,alimka,alimkah,alimkent,alimkhan,alimli,alimmatadhes,alimnia,alimo,alimoan,alimod,alimodian,alimohammad-khel,alimolla,alimonde,alimoneng,alimoni mambwe,alimono,alimor,alimosgan,alimosho,alimoso,alimov,alimov-lyubimovskiy,alimovka,alimovskiy,alimow-ljubimowskoi,alimperia,alimpesti,alimpinar,alimpinari,alimpur,alimsat,alimsere,alimsog,alimtau,alimudong,alimukhamed-kalay,alimukhammed-kalay,alimukhammed-kheyl,alimulayim,alimun,alimur park mobile home park,alimwala,alimzhanov,alin,alin jaq,alin kamar-e `olya,alin kamar-e sofla,alin potok,alinyazi,alina,alinabad,alinagar,alinagar chak,alinagar diara,alinagilar,alinagylar,alinam,alinao,alinapara,alinasa,alinasan,alinauan,alinawan,alinayin,alinazarli,alinc,alinca,alincak,alincali,alincaoeg,alince,alinchein,alinchipotana,alinci,alinci bitolsko,alincik,alincourt,alincthun,alinda,alindao,alindaoe,alindau,alindo,alindzha,alindzhak,aline,aline estates,aline madi,alinea,alineadero,alineaderos,alinek,aliner,alingadyon,alingag,alingal,alingal,alingan,alingar,alingaro,alingarog,alingay,alingayri,alingbu,alinge,alinghuizen,alingkang,alingsas,alinguie,alinguigan,alinguigan i,alinguigan ii,alinguigan iii,alinhac,alinhac-bas,alinhavao,alininevi,aliniwan,alinja,alinjagh-i gichka,alinjak,alinjaraq,alinjareq,alinjeh,alinjivakkam,alinka,alinkino,alinkoy,alinkoyu,alinna,alinnguel,alinnkane,alinnki,alino,alinoren,alinowo,alins,alins de monte,alins del monte,alinskaya,alinskoye,alinsolong,alinta,alintepe,alintere,alinunu,alinveren,alinviren,alinyayla,alinyomabala,alinyomacami,alinyombala,alinyurt,alio,alio amba,alio edda,alio eddao,aliobasi,alioecoec,alioglu,aliogullar,aliogullari,aliokol,aliokpu-agbor,aliomba,alion,alionahi,alioni,alioniai,alionis,alioniu,alionys,alionys 2,alionys i,alios,aliosch,aliosu,aliot,aliou,alioua,aliouat,aliouate guentoue,aliouen,aliouene,aliouia,aliozu,alip,alipa,alipaciauan,alipadja,alipaga,alipago,alipai,alipalai,alipang,alipangan,alipangang,alipangkengtou,alipangpang,alipangpang north,alipangpang south,alipanto,alipao,alipaoy,aliparrimo,alipasa,alipasa koyu,alipasa yaylasi,alipasakoy,alipasapas,alipasiauan,alipasiawan,alipasici,alipasin most,alipasino polje,alipasiowan,alipat,alipato,alipaye,alipayiana,alipberdy,alipe,alipei,alipinarkoy,alipio,alipio paca,alipion,alipion hacienda,alipit,alipka,aliplo,alipo,alipokhori,alipolima,alipoon,alipore,alipos,aliposo,alipostivan,alipostivani,alipraja,alipuaton,alipukur,alipuluan,alipumala,alipur,alipur asar,alipur chatha,alipur duar,alipur duars,alipur jalal,alipur janubi,alipur jhugian,alipur kanjun,alipur noon,alipur nun,alipur saidan,alipur sayyadan,alipur sikhan,alipur wazir sahib,alipur-saidan,alipura,aliputos,alipuumala,aliqa-i gawra,aliqa-i pichuk,aliqah,aliqah bozorg,aliqah buzurg,aliqasimli,aliquay,alique,aliquie,aliquippa,aliquisanda,aliquisanga,aliqulu,aliqulular,aliquluusagi,alir,alira,alirajapet,alirajpur,aliranwala,alird,alire,alirgaon,aliri,alirik,aliro brdo,alirt,alirtek,alirvalar,alirzalar,alis,alisa,alisa estates,alisaang,alisad,alisadilla,alisafi,alisahli,alisaidkheyl,alisakandi,alisal,alisam,alisanagili,alisanbeyagili,alisanbeyagili koyu,alisang,alisangi,alisani,alisanlar,alisapur,alisar,alisaray,alisas,alisavci,alisavino,alisayman,alisayvan,alisbrunn,alise,alise-sainte-reine,aliseda,aliseda de tormes,alisedas,alisem,aliser,aliser alaqadari,alisevdi,aliseydi,aliseyhli,alisha kandi,alishad,alishahr,alishakh,alishan,alishang,alishanly,alishar,alishary,alishem,alisher,alisher shah,alisheralakadari,alisherkalay,alisherzai,alisheva,alishevo,alishiaw,alishih,alishma,alishor kandi,alishtar,alisici,alisihli,alisik,alisilik banisi,alisime,alisimie ii,alisir,alisit,alisitne,alisitos,alisitue,alisivka,aliskerovo,aliskino,aliskulma,aliso,aliso mayu,aliso viejo,aliso-pata,alisofu,alisoltanli,alisoltanly,alisolungura,alisomor,alisopat,alisopata,alisor,alisos,alisos de arriba,alisoso,alisoumkope,alisovka,alisovo,alisovo-pokrovskoye,alispahici,alissa,alissas,alissos,alista,alistal,alistanza,alisteyevo,alistrati,alisu,alisubani,alisubani nizhniye,alisubani verkhniye,alisulungura,alisuni,alisurkhon,aliswar,alit,alit mirjanzai,alita,alitagtag,alitagtay,alitain,alitaje,alitak,alitalango,alitangga,alitangka,alitao,alitap,alitas,alitasi,alitasy,alitaya,alitaytay,alitazan,alite,aliteau,alithini,alithinou,aliti,alitinu,alito,alitock,alitokoum,alitongtong,alitoskinlar,alitowala,alitselepi,alitsi,alitsun,alitta,alittana,alitub,alitungan,alitupu,alitus,alitutal,alitzani,alitzheim,aliu,aliu bure,aliu jai,aliu jaia,aliu jau,aliua,aliuagon,aliud,aliuniun,aliusagi,aliushagy,aliusta,aliutsa,alivangue,alivanhakyla,alivar,alivat,alivavalai,alivawah,alivemuene,aliveran,aliveren,aliverion,aliverovice,aliveti,alivetti,alivi,aliviadero,alivio,alivit,alivojvodic,alivojvodici,alivu,aliw,aliwah,aliwal,aliwal north,aliwal rai chak,aliwal-noord,aliwala,aliwala kot,aliwali,aliwanay,aliwaro kher,aliwat,aliwe,aliwo,aliwu,alix,alixan,aliy,aliya,aliya bole,aliyabad,aliyakada,aliyamalagala,aliyan,aliyan bala,aliyanli,aliyanly,aliyansaintakulam,aliyar,aliyarvaddai,aliyavalai,aliyawetunawewa,aliyawetunuwewa,aliye,aliye bole,aliyenli,aliyerd,aliyetmazli,aliyi,aliyoi,aliyoso,aliyu amba,aliyuda,aliza,alizabu,alizaga,alizai,alizai kili,alizai-kalay,alizal,alizar,alizava,alizavos,alizay,alizayi,alizayo kalay,alizayu-kalay,alizberg,alize,alizeh,alizhgiriw,alizi,alizin,alizmajor,alizo,alizo zrandah,alizorum,alizos,alizq,aljabal,aljahdurg,aljaj,aljam,aljamdu,aljamdu kamara kunda,aljamdu kanteh kunda,aljan,aljaraque,aljariz,aljarn,aljau,aljava,aljaz,aljazayer,aljban,alje,aljebal,aljena,aljenaari,aljenna,aljennah,aljetici,aljezares,aljezur,aljgjata,alji,aljibe,aljibes,aljibillos,aljici,aljie,aljilar,aljince,aljinovice,aljinovici,aljkovici,aljkurtovci,aljmas,aljojuca,aljorf,aljorra,aljovic,aljsic,aljtataj,aljubarrota,aljube,aljucen,aljucer,aljudovo,aljurica,aljurk,aljustrel,alk,alk er rayess,alka,alkwat,alk-e kohneh,alka,alka sara,alka-zarechnaya,alkaagash,alkabad,alkabo,alkadar,alkaddiveli,alkaer,alkaersig,alkagan,alkah,alkai,alkaijji,alkaladka,alkalatka,alkalayevka,alkaleh,alkalere,alkaleri,alkali,alkali gwa,alkali lake,alkaliji,alkalikounda,alkaliman,alkalimanga,alkalu,alkaly,alkam-e lotowm,alkama,alkamari,alkamazhar,alkamer,alkamsa,alkamsara,alkamu,alkan,alkan-togay,alkana,alkanabangou,alkani,alkanka,alkaouel,alkara,alkaran,alkas,alkas kor mustafa,alkasam,alkasnak,alkaterek,alkateri,alkatovo,alkatvaam,alkatyan,alkava,alkay,alkaya,alkayaderesi,alkayaoglu,alkayevo,alkaynak,alkborough,alke,alkegama,alkeh,alkeh dasht,alkeh-ye kohneh,alkemada,alkemar,alkemer,alkemi,alken,alkersdorf,alkersleben,alkersum,alkertshausen,alkeshaf,alkeso,alkestrup,alkeyevo,alkhadzhakent,alkhadzhikent,alkhalaj,alkham,alkhan,alkhan kara,alkhan qara,alkhan-churt,alkhan-churtskiy,alkhan-kala,alkhan-khutor,alkhan-otar,alkhan-yurt,alkhanay,alkhanchurt,alkhani,alkhanly,alkharunun kur,alkhas,alkhas qal`eh,alkhasava pervoye,alkhasava vtoroye,alkhasly,alkhast,alkhasty,alkhatyan,alkhatlu,alkhay-churtskiy,alkhazova,alkhazurovo,alkheri,alkhi,alkhidovka,alkhimkovo,alkhimova,alkhimovka,alkhimovo,alkhimtsevo,alkhitay,alkhitoy,alkho,alkhodzha-kent,alkhorshid,alkhosayam,alkhotlampi,alkhuch,alki,alkif,alkife,alkila,alkilkil,alkill,alkin,alkina,alkincilar,alkino,alkino devyatoye,alkino pervoye,alkino sedmoye,alkino shestoye,alkino-dva,alkion,alkioni,alkirenik,alkires mills,alkishkyay,alkiskiai,alkiskiu,alkisrak,alkistan,alkjaersig,alkkia,alkkula,alkmaar,alko,alkodiya,alkodyu,alkofen,alkol,alkoma,alkomemaj,alkomenai,alkomenaj,alkonghi,alkongui,alkoni,alkop,alkos,alkoti,alkou,alkouk,alkousiri,alkoven,alkowzi,alkoy,alkozai,alkozi,alkozo kalay,alkozo kelay,alkrog,alksnajciems,alksnajs,alksne,alksnenai,alksnenay,alksnenu,alksnenu naujuju,alksniai,alksnine,alksniupiai,alksnupenen,alksnupiai,alksnupyay,alksnupyay-novyy,alksnupyay-staryy,alkstein,alku,alkuchan,alkuh,alkuino,alkul,alkula,alkullen,alkunagh,alkupe,alkupis,alkusak,alkushk,alkushki,alkuti,alkuzhi,alkuzhinskiye,alkuzhinskiye borki,alkuzo kalay,alkym,all,all saints,all saints mission,all saints nek,all saints village,all sheri,all stretton,allacqua,allacqua ospizio,alla,alla abba habab,alla beg,alla ditta,alla djaba,alla gabe,alla gaouri,alla kouassi,alla kunda,alla mulk,alla ouzrou,alla tenta,alla-baytal,alla-daglu,alla-kanda,alla-sakal,allaalla,allaba,allabad halepota,allabara,allaben,allaboda,allabon,allabuzum,allacapan,allacas,allach,allacha,allachay,allacoracca,allacro,allactaocpata,allacuran,allada,alladad mughal khel,alladad rahuja,alladadzai,alladi,alladir algi,alladorf,alladzhaguyel,allafo,allag,allaga,allagadda,allagalla,allagalung,allagappa,allagarno,allagarno-lawansaid,allagash,allagay,allagbafeu,allagen,allagerno,allagesh,allagnat,allagoakiri,allagracia,allague,allaguerno,allaguia,allagulovo,allagulovskaya,allagunovka,allaguvat,allaguzovo,allah,allah abad,allah abad aboosaidi,allah abad bahabad,allah abad hoomeh,allah abad nichari,allah abad raisani,allah abad rastagh,allah akbar,allah bachaio,allah bacharo dars,allah bachavo,allah bachaya,allah bachayo,allah bachayo bhatti,allah bachayo lashari,allah bachayo machhi,allah bachayo mando,allah bakhsh,allah bakhsh ahir,allah bakhsh arbab,allah bakhsh babbar,allah bakhsh baloch,allah bakhsh barani,allah bakhsh bhurgari,allah bakhsh bhutto,allah bakhsh brahui,allah bakhsh buledi,allah bakhsh chandani,allah bakhsh dalwani,allah bakhsh deper,allah bakhsh gaho,allah bakhsh gola,allah bakhsh gopang,allah bakhsh jamali,allah bakhsh jarwar,allah bakhsh jet,allah bakhsh jhabil,allah bakhsh junejo,allah bakhsh kalhoro,allah bakhsh khosa,allah bakhsh korai,allah bakhsh kubhar,allah bakhsh laghari,allah bakhsh lashkani,allah bakhsh machhi,allah bakhsh mahalleh,allah bakhsh nizamani,allah bakhsh noon,allah bakhsh panhwar,allah bakhsh patabi,allah bakhsh punjabi,allah bakhsh rind,allah bakhsh roda,allah bakhsh shah,allah bakhsh sumro,allah bakhsh thebo,allah bakhsh unar,allah bakhsh wadho,allah bakhshwala,allah bakhshwala khu,allah bakhshwali,allah bakshsh chhina,allah balah,allah beg,allah beyg,allah bochaya arain,allah bou hammou,allah bukhsh,allah bukhsh lund,allah bux,allah chaleh,allah chapan,allah chiraghwala,allah dad,allah dad bazar,allah dad jo got,allah dad lakho,allah dad samtio,allah dad tanwar,allah dad zai,allah dad-e zehi,allah dadwala,allah dal,allah daneh,allah dano shah,allah daowala,allah darreh,allah darreh-ye `olya,allah darreh-ye bala,allah darreh-ye pain,allah darreh-ye sofla,allah deh,allah dewalawala,allah dini,allah dinna,allah dinno chandia,allah dino,allah dino bhurgori,allah dino chandio,allah dino chatto,allah dino chattro,allah dino goth,allah dino janwar,allah dino jokhio,allah dino khan,allah dino magsi,allah dino mallah,allah dino mir bahr,allah dino mohana,allah dino shah,allah dino tunia,allah dino zardari,allah dinor purio,allah dinwala,allah dito khaskheli,allah ditta,allah ditta bhela,allah ditta chandia,allah dittawala,allah dittawala khu,allah ditto kalyar,allah dittu jalban,allah dittu jalbani,allah diu abbasi,allah dow,allah gaba,allah gabo,allah gabu,allah ghuli,allah goth,allah gurab,allah haq,allah haqq,allah ina,allah jabu,allah jiwaya lar,allah juria lashari,allah kabani,allah kandi,allah karam,allah karrim,allah khel,allah khelwala,allah khelwala banda,allah koreh,allah molk,allah morad,allah morad khani,allah morad-e golin,allah nana,allah nawaz jatoi,allah nazar,allah nur,allah obayo,allah qoli,allah quli,allah rakha,allah rakha laghari,allah rakha malik,allah rakhhio thaim,allah rakhia,allah rakhio,allah rakhio bahan,allah rakhio junejo,allah rakhio khaskheli,allah rakhio kirio,allah rakhio lund,allah rakhio machhi,allah rakhio marri,allah rakhio pali,allah rakhio pandiani,allah rakho jo goth,allah rakho malik,allah rudbar,allah sang-e `olya,allah sang-e `ulya,allah sang-e sufla,allah sarkigasar,allah shah,allah teymur,allah vasaya jo goth,allah verdi kandi,allah verdi khan,allah verdiabad,allah verdilu,allah virayo gujar,allah wali,allah waraia,allah waraio,allah waraio laghari,allah waraya,allah waraya dahri,allah waraya pinjaro,allah waraya qazzaq,allah warayo kalhoro,allah warayo kalwar,allah warayo magsi,allah warayo umrani,allah warrayo,allah was,allah wasaio,allah wasaio chachar,allah wasaya awanwala,allah wasaya ghallu,allah wasayo,allah wassaya pitafi,allah wassayo,allah woraio,allah yar,allah yar de bhan,allah yar jorewala dera,allah yar khan,allah yar khan garhi,allah yar khel,allah yarlu,allah yarpur jarh,allah yarwala,allah yarwali,allah zai,allah-o-akbar,allahabad,allahabad abu sa`idi,allahabad karyal,allahabad-e `asgar pur,allahabad-e `olya,allahabad-e abu sa`idi,allahabad-e bala,allahabad-e bustan,allahabad-e dehqani,allahabad-e gorji,allahabad-e hajjiabad,allahabad-e mostowfi,allahabad-e naygun,allahabad-e pain,allahabad-e rezvan,allahabad-e seyyed,allahabad-e takht-e khvajeh,allahabad-e tang sorkh,allahabadwala,allahadadwala,allahai,allahal,allahan,allahanno baloch,allahanno shah,allahayan,allahayari,allahbad,allahbad bosan,allahbak,allahbakhshwala,allahda,allahdad,allahdad banda,allahdad bhambro,allahdad bhurgari goth,allahdad chandia,allahdad faqir,allahdad halq,allahdad kelay,allahdad khan,allahdad khune,allahdad khuni,allahdad mughal khel,allahdad nmasai,allahdad paryar,allahdad phoar,allahdadani goth,allahdadi,allahdadpur,allahdadpura,allahdadwala,allahdand,allahdher,allahdina rind,allahdino jalbani,allahdino mitlo,allahdino mohano,allahdino rathor goth,allahdino sabayo,allahdittawala khu,allahdiwaiwala,allahdiyen,allahdrug,allahdurg,allahdurug,allahe,allahein,allahganj,allahhu,allahi,allahian,allahin,allahina,allahiyan,allahjiwaya bhutto,allahjorio khaskheli,allahkuchular,allahmadadli,allahno,allahno halepota,allahno hingorjo,allahno kahar,allahno khan talpur,allahno khaskheli,allahno mandro,allahno sehto,allahno toh,allahnur kalay,allaho akbar,allahpur,allahqoli,allahqulular,allahrakhio,allahrakhio shah,allahrakhyo,allahrakhyo juneja,allahsang,allahu,allahuay,allahubad,allahuwa,allahverdi kandi,allahwala,allahwalwala,allahwaraio burra,allahwaraya kalora,allahwarayo goth,allahwarayo jatoi,allahwarayo juno,allahwas,allahyan,allahyar,allahyar bhutto,allahyar daheli,allahyar goth,allahyar juna,allahyar juta,allahyar khan,allahyar khanwali,allahyar khelanwala,allahyar kheyl,allahyar khoso,allahyar mahr,allahyar rahu,allahyar zai,allahyari,allahyarli,allahyarwala,allahyarwala khu,allahyarwali,allahzai,allahzi,allai,allaie,allaikallupoddukulam,allaikha,allain,allaines,allainville,allainville-en-beauce,allaipiddy,allaippiddi,allaiqa,allaire,allais,allaj,allaj beje,allajah,allajalee,allajali,allajbeg,allajbegu,allak,allak ad dhahir,allak adh dhahir,allaka,allakabud,allakai,allakaket,allakalawa,allakang,allakayevo,allakgol,allakh,allakh-yun,allakhdad,allakhdadkhune,allakhguli,allakhkuli,allakhvas,allakhyarly,allaki,allakjaure,allakkol,allakofen,allakro,allakskoye,allakuang,allakul,allakula,allakurtti,allakyula,allal,allal ben mesaoud,allal ben messaoud,allal bou omar,allal tazi,allalech,allaled ou souk,allallica,allam,allam sadia,allama binori town,allama iqbal colony,allama iqbal town,allaman,allambare,allambee,allambee south,allambere,allambre,allambres,allambresi,allameang,allamellal sidi cherif,allami csemetekert,allami erdo,allami erdogazdasag,allami erdotanya,allami gazdakepzo,allami malom,allami szollotelep,allamont,allamoore,allamps,allampuszta,allamuchy,allamvillu,allan,allan jamali,allan lake landing,allan water,allan-e `olya,allan-e sofla,allan-tepe,allana,allana acres,allanabad,allanah,allanby,allanca,allancay,allanche,allanches,alland,alland im gebirge,allandhuy,allandhuy -et- sausseuil,allandale,allandale north,allande,allane,allanfearn,allang,allanga gurrow,allangigan,allangigan primero,allangigan segundo,allangni,allango gurru,allangouanou,allangua,allanguel,allani,allanmyo,allano,allano machhi,allanpur,allanridge,allans flat,allansford,allanson,allantana,allanton,allanwood,allaos,allaouich,allapalli,allapata,allapattah,allaperumandaluwa,allappangan,allapporeng,allar,allar koyu,allar mourkevock,allar pind mohkam wala,allara,allara-kyusyut,allaraa-sieno,allarai,allaraini,allard,allards,allardsoog,allardt,allared,allareni,allarh aouems,allarh ou aourga,allarhane,allari,allariz,allarmont,allarp,allarpind,allarpo,allarto,allartsoog,allary,allary-khastyakh,allas,allas-bocage,allas-champagne,allas-les-mines,allasitan,allaskut,allassac,allassitan,allaste,allasura,allaszki wielkie,allata,allate,allatepe,allatman,allato,allatoona,allatoona bay,allatoona beach,allatoona heights,allatoona pass,allatoona ridge,allatorp,allatto,allauan,allauca,allauch,allauddin-karez,allauddini-bala,allauddinke,allauddinke khurd,allaudini-pain,allauk,allavaara,allavan,allavar,allavare,allaverdy,allawalwala,allayan,allayar-kuduk,allayarno,allayi khwaru,allayi saru,allaykha,allaymucha,allayqa,allayskiy,allayu,allazhi,allazi,allazmuiza,allazu muiza,allberg,allbright,allbrook,allcanshe,allcas,allcaush,allccane,allccanja,allcuhuilcca,alldays,alle,alle sarche,allealle,allebo,alleboke,allebrata,alleby,alleca,alledonia,allee,alleen,alleena,alleene,alleeny bridge,allees marines,allegada,allegan,allegany,allegany grove,allegazovo,alleghany,alleghany springs,alleghe,allegheny,allegheny acres,allegheny furnace,allegheny springs,allegheny west,alleghenyville,allegma,allego,allegre,allehwali,alleins,alleira,alleira allaro,alleira elmuna,alleira kuta,alleira lauti,allejaur,allejaure,allekro,allela,allele,allelev,allelu,allem i,allem ii,allem iii,allemagne,allemagne-de-provence,allemagne-en-provence,alleman,allemanche,allemanche-launay-et-soyer,allemand,allemania,allemans,allemans-du-dropt,allemansdrif,allemanskraal,allemansvlei,allemant,allembe,alleme,allemira,allemoa,allemond,allemont,allemuhl,allen,allen acres,allen center,allen chapel,allen city,allen corners,allen crossing,allen del hoyo,allen ford,allen grove,allen hall,allen jay,allen junction,allen landing,allen lane,allen mills,allen more,allen park,allen point,allen shop corner,allen siding,allen springs,allen town,allen view,allens bridge,allenau,allenay,allenaya,allenbach,allenberg,allenbostel,allenbruch,allenburg,allenbuttel,allenc,allence,allencrest,allendale,allendale east,allendale estates,allendale heights,allendale north,allendale town,allende,allende bajo segunda seccion,allende el alto,allendelagua,allendonf,allendorf,allendorf an der lahn,allendorf an der landsburg,allendorf an der lumda,allendorf bei frankenau,allendorf kloster,allendorf-eder,allendorph,allenfarm,allenfeld,allenford,allenheads,allenhof,allenhurst,allenjoie,allenkowitz,allennes,allennes-les-marais,allenport,allenrod,allens,allens corner,allens corners,allens creek,allens crossroads,allens factory,allens fresh,allens grove,allens hill,allens level,allens mill,allens mills,allens spring,allens spur,allensbach,allensburg,allenshteyn,allenslevel,allensmore,allenspark,allenstand,allenstein,allenstown elementary school,allensville,allensworth,allenta,allenton,allenton station,allentown,allentsgschwendt,allentsteig,allenvale,allenview,allenville,allenwiller,allenwood,allenwood cross roads,allenz,alleppey,alleppi,alleputti,allepuz,alleqalla,aller,allerborn,allerbuttel,allerdean,allerding,allerdorf,allerdsoog,alleret,allerey,allerfing,allerford,allerheilgen,allerheiligen,allerheiligen bei wildon,allerheiligen im muhlkreis,allerheiligen im murztal,alleringersleben,alleringhausen,alleriot,allermohe,allerod,alleroj,allerona,alleroy,allersbach,allersberg,allersburg,allersdorf,allersdorf bei judenburg,allersdorf im burgenland,allersehl,allersfelden,allersgraben,allershagen,allershausen,allersheim,allershofen,allersing,allerslev,allersma,allerstedt,allerston,allerstorf,allerting,allerton,allerton mauleverer,allertsham,allertshausen,allertshofen,allerum,allerup,allerup torup,allery,alles,alles-sur-dordogne,allesa,alleschwende,allese,alleshausen,alleshave,allesley,alleso,allessa,allestad,allested,allestree,allestrup,alletshof,alletsried,alletswind,alleum,alleur,alleuten,alleuze,allevard,alleves,allevier,allevran,allewali,allewind,allewinden,allex,allexi,allexton,alley,alley grove,alley place,alley spring,alleya-gomontovo,alleynaya,alleynedale,alleyrac,alleyras,alleyrat,alleyton,allez-et-cazeneuve,allezai,allfeld,allgaierhof,allgaueck,allgjata,allgjate,allgjin,allgood,allgraben,allgramsdorf,allgreave,allgunnaryd,allgunnas,allgunnen,allgustorp,allhallows,allhaming,allhang,allharting,allhartsberg,allhartsmais,allhau,allhofen,allhuire,allhuiscca,allhut,allhutten,alli,alli bale,alli iwo,alli meria,alli oge,alli veremos,alli-mammed-arab,alliab,allialli,allian,allianca,alliance,alliance furnace,alliance junction,alliance redwood,alliancelles,alliat,allibaudieres,allibunar,allichamps,allie,allied,allieni,allier,allieres,allieres-et-risset,allierscheheide,allierseheide,alliford bay,allig,alligartor nose,alligator,alligator bay settlement,alligator creek,alligator heads,alligator lake,alligator nose,alligator pond,alligator pond bay,alligator pond village,alligerville,alligin,alligny,alligny-cosne,alligny-en-morvan,alligny-en-morvand,alligood,alligoods,allihies,alliisik,allika,allikakula,allikaotsa,allikjarve,alliklepa,allikmaa,allikonnu,allikotsa,alliku,allikukivi,allim,allimbengenge,allimdong,allimni,allinagaram,allindelille,allindemagle,allineuc,alling,allingaabro,allingabro,allingaweer,allingawier,allingdale,allingdam,allinge,allinges,allingham,allinghausen,allington,allingtown,alliniemi,allion,allioren,allios,allipa,allipachaca,allipen,allipur,alliquerville,alliritengae,allis,allis hollow,allisa,allish,alliska,allison,allison crossing,allison ferry,allison gap,allison harbour,allison heights,allison mills,allison park,allisona,allisonia,allisonville,allisreute,allissitan,alliste,allister,alliston,allita,allitas,allituma,allitzani,allivere,alliwengeng,alliyabad,alljaz,allkaj,allkofen,allkomemaj,allkomemanj,allkomenaj,allmag,allmagsvik,allmajarvi,allman,allmand,allmandie,allmandle,allmanningbo,allmanninge,allmanningsbo,allmannsau,allmannsberg,allmannsdorf,allmannsfeld,allmannshausen,allmannshofen,allmannsried,allmannsweier,allmannsweiler,allmans,allmau,allmend,allmendfeld,allmendingen,allmendsberg,allmendshofen,allmenhausen,allmenningen,allmenrod,allmering,allmersbach,allmersbach am weinberg,allmersbach im tal,allmersdorf,allmershausen,allmethofen,allmishofen,allmon,allmondsville,allmoning,allmora,allmosen,allmoshof,allmoyen,allmuhten,allmunzen,allmus,allmut,allmuten,allmuthen,allmuthshausen,allna,allner,allnut,allnut farms estates,allo,alloa,allobar,alloca,alloche,allochkin otrog,allock,allodial boltenhagen,allodossicodji,alloe,allogny,alloha,alloipur,alloke,allokoa,allokokro,allolar,alloleya,allomakanme,allomasszogdulo,allomastanya,allomastelep,allomo,allon,allon shevut,allona,allonah,allonby,allonca,allondans,allondaz,allondrelle,allondrelle-la-malmaison,allone abba,allone habashan,allone yitshak,allone yizhaq,allonei-yitzhak,allones,allonim,allonne,allonnes,allons,allonville,allonys,allonzier-la-caille,alloo,alloon lower,allora,allorongnge,allos,allosepe,allotan,alloten,allou,allouagne,allouchia,alloue,allouet el gounna,allouez,allouis,allouki,alloun,allous,allouville,allouville-bellefosse,alloway,alloway junction,alloweure,allowule,alloy,alloz,alloza,allpabamba,allpacancha,allpacancha hacienda,allpachaca,allpacoma,allpacopa,allpacoto,allpacoto hacienda,allpacruz,allpakeri,allpaqueri,allpas,allpaspina,allpaurccuna,allport,allprenaj,allprendaj,allprendaj lushnja,allprendaj-lushnje,allprendaj-peqini,allprendani peqin,allprendja,allrath,allred,allreds,allright,allrode,allroudis,allrune,allsan,allsarp,allsboro,allsbrook,allschwil,allscott,allscotte,allsjarv,allsjon,allsop,allsta,allstad,allstakan,allstedt,allston,alltata,alltataj,alltbeath,alltmawr,alltnacaillich,alltorp,alltsigh,alltwalis,alltwen,allu,allu bangwar,allu jo goth,allu multan,allua,alluaiv,alluana,alluca,alludzha,allue,allueva,allui,alluitsoq,alluitsup paa,alluka,alluka 1,alluka 2,alluka dua,alluka satu,alluke,allukeke,alluliabad,allum,allumiere,alluminankro,allumnu,alluna,allungdong,allungni,allur,alluran,alluriquin,alluru,alluvia,alluvial city,alluvium,alluwah,alluwala,alluwali,alluwe,alluy,alluyes,alluz,allview estates,allwala,allwood,allworden,allworth,ally,allyady,allyagi,allyang,allyangkol,allyar,allyegol,allyekol,allykend,allyn,allyn point,allyong,allyongdong,allyongjigol,allyongni,allyos,allyson gardens,allzunah,alm,alm denane el homadna,alma,alma center,alma chaab,alma city,alma creek,alma esh shaub,alma gardens,alma goumna,alma intre vii,alma junction,alma lake,alma meadows,alma meadows mobile home park,alma ou hammane,alma ouaklane,alma qulad,alma qulaq,alma sang,alma sarai,alma saray,alma vii,alma-ata,alma-atinskiy,alma-dere,alma-marine,alma-suan,alma-terek,alma`dan,almaarasan,almaas,almaca,almacatla,almacatlan,almaceda,almacellas,almacen,almacen hacienda,almacen la picaza,almacenes,almacera,almachar,almache,almachi,almachovan,almaciga,almacigo,almacigo bajo,almacigos,almacik,almaciles,almacinha,almacon,almacukuru,almad,almada,almada de ouro,almadane,almaden,almaden de la plata,almadena,almadenejos,almadenes,almadi,almadin,almadina,almadis,almadraba,almadrones,almady,almaegol,almafiqiya,almafra,almafuerte,almagalan,almagarinos,almagea,almagell,almagenes,almaghou,almagor,almagovlu,almagra,almagre,almagreira,almagres,almagrinha,almagro,almagros,almaguer,almaguer north,almaguer south,almagy,almagyardulo,almah,almah qolagh,almah sayyid,almah shakh,almahegy,almahue viejo,almai,almaipet,almajalejo,almajano,almajasa,almajel,almajelu,almajeq,almaji,almajir,almajirama,almajirawa,almajiri,almajiu,almajiul,almajoq,almajor,almaju,almajul,almak,almaki,almakolu,almakuyuly,almala,almalaguez,almalau,almale,almalek,almali,almali bash,almali xiang,almalii,almalik,almaliq,almalitala,almalo,almalonga,almalu,almalu bash,almalu dash,almalu dere,almaluez,almaluk,almaluu,almaly,almalyk,almalytala,almamatovo,almamellek,almametovo,almametyevo,almami,alman,alman lengeh,alman sharqi,alman sur,alman-e jadid,alman-e qadim,almana,almanabad,almanawala,almancena,almanceno,almanchiki,almanchikovo,almanchino,almancil,almancos,almancos de baixo,almandar,almandarion,almandayevo,almande,almando,almandoz,almane,almane fonsom,almaneh,almani,almani goth,almaniana,almannshof,almanor,almanor west,almanovo,almanowo,almanpur,almansa,almansas,almansil,almansor,almantiga,almanwala,almanza,almanza dos,almanza uno,almanzora,almao,almaoudene,almap-e mohammadabad,almar,almar bazar,almar estates,almar khel,almar kheyl,almara,almarah,almarail,almaran,almaraveneh-ye yek,almaraviyeh,almarayeneh,almaraz,almaraz de duero,almaraz de la mota,almarch,almarcha,almarco,almareh,almargem,almargem do bispo,almargen,almargens,almarginho,almari csoszhaz,almarjao,almarjinho,almarkarod,almarkhel,almarod,almarsrum,almartha,almarza,almarza de cameros,almas,almas khel,almas pusta,almas puszta,almas retye,almas-saliste,almas-seliste,almasa,almasabad,almasad,almasar,almasaruchipa,almasat,almasay,almasch,almased,almaseed bridge,almasel,almasen,almasfuzito,almasgodor,almash,almasha,almashakh,almashaza,almashin,almashpur,almasi,almasiani,almasid,almasikut-puszta,almasitanya,almaska,almaskamaras,almaskeresztur,almaskhan,almaskheyl,almaskuttanya,almasneszmely,almaspuszta,almastanya,almasu,almasu de mijloc,almasu mare,almasu mic,almasu mic de munte,almasu-sec,almasul,almasul mare,almasul mic,almasul sec,almasul-de-mijoc,almasul-mare,almasul-mic,almasul-mic-de-munte,almasvolgytanya,almasy,almasytanya,almasytelep,almat borj,almat burj,almatamak,almatarkhan,almatay,almate,almaten,almateyn,almati,almatin,almatomak,almatret,almatti,almatu,almaty,almau,almaville,almavolgypuszta,almawa,almawan,almawan-i kon,almay,almaya,almayate alto,almayate bajo,almayrac,almaysh,almaz,almaza,almazan,almazar,almazaran,almazcara,almazi\302\247ah,almaznaya,almazne,almaznoe,almaznoye,almazny,almaznyy,almaznyy pervyy,almazora,almazorre,almazov,almazovka,almazovo,almazovskiy,almazul,almbanis,almberg,almberget,almbergsbjorken,almbranz,almby,almdorf,alme,alme trab,almeara,almeberg,almeboda,almecatla,almecega,almecega velha,almecegas,almeda,almeda beach,almeda plaza,almedal,almedia,almedijar,almedina,almedinilla,almegijar,almegol,almegran,almegue,almeh,almeh juq,almeh qolagh,almeh qolaq,almehdi,almehult,almeida,almeidas,almeidinha,almeijoafra,almeiras,almeirim,almeirinhos,almeirinhos de cima,almeirinhos do clemente,almejero,almejoq-e pain,almejuq-e pain,almejuq-e sofla,almel,almelaguez,almeley,almeliden,almelo,almeloo,almelund,almen,almen-kasy,almen-sunary,almena,almenar,almenar de soria,almenara,almenara de adaja,almenara de tormes,almenares,almend,almendares,almenderovo,almendorf,almendra,almendral,almendral de la canada,almendralejo,almendras,almendres,almendricos,almendrillo,almendrina,almendrita,almendro,almendros,almeneches,almenes,almeneva,almenevo,almengo,almenhausen,almenia,almenningen,almenno san bartolomeo,almenno san salvatore,almenovo,almenras,almensilla,almental,almenum,almer,almerac,almere,almere-haven,almered,almerfeld,almeria,almeriana,almerim,almersbach,almersberg,almerswind,almert,almertsham,almes,almesakra,almesas,almesberg,almesca,almescar,almese,almesh,almeshult,almesjo,almesquinha,almesquinho,almestad,almetorp,almetovo,almetyevo,almetyevsk,almeya,almeyeva,almezh,almfala,almhauser,almhorst,almhult,almi,almibonda,almidar,almidon,almie dombolai,almijang,almijangto,almijofa,almijoy,almijuy,almiki,almilinga,almind,almind hede,alminde,almino affonso,almino afonso,almiqui,almir,almira,almirante,almirante brown,almirante colon,almirante latorre,almirante solier,almirante tamandare,almireces,almireses,almirez,almiridha,almiron,almiropotamos,almiros,almiruete,almirza,almis,almis de marmucha,almis des marmoucha,almis du guigou,almis marmoucha,almiserat,almiska,almissa,almiyik,almjudzi,almke,almkerk,almlia,almlien,almo,almo heights,almocageme,almocaizar,almochuel,almocita,almoco,almocrim,almodofa,almodovar,almodovar del campo,almodovar del pinar,almodovar del rio,almoe,almofada,almofala,almofala de baixo,almofala de cima,almofrela,almofrey,almog,almogade,almogarve,almoghowl,almoghul,almogia,almograve,almoguera,almohadas,almohaja,almoharin,almoines,almoinha velha,almoinhas,almoinhos,almoite,almojanda,almojia,almolaya,almolon,almoloneas,almolonga,almolongas,almoloya,almoloya de alquisiras,almoloya de juarez,almoloya del rio,almoloyan,almoloyas,almoloyita,almomoloa,almon,almon sur,almon-les-junies,almona,almonacid de la cuba,almonacid de la sierra,almonacid de toledo,almonacid de zorita,almonacid del marquesado,almonaster,almonaster la real,almond,almond valley,almonda,almondale,almondbank,almondbury,almonds,almondsbury,almonedas,almonesson,almonogui,almont,almonte,almora,almora heights,almoradi,almoral,almoral spring,almoraviyeh,almorchon,almorcita,almordi,almoriz,almornos,almorox,almorranas,almorranes,almorzadero,almorzadoro,almos,almosd,almosen,almosenbachhorn,almosenreuth,almoster,almosund,almota,almotolias,almou,almou kwara,almou nait maziah,almou nzi,almou-koara,almoudene,almoun ait isfoul,almoun nait isfoul,almoun nzi,almour,almoura,almoustarat,almozerskiy pogost,alms house,almsdorf,almsele,almsick,almsjonas,almsloh,almsta,almsted,almstedt,almstofte,almstok,almstorf,almstrup,almta,almtamala,almtasa,almti,almtoft,almtuna,almu,almud,almudafar,almudaina,almudebar,almudema,almudena,almudevar,almudu,almuedano,almukhamedovo,almukhametov,almukhametova,almukhametovo,almuko,almun,almuna,almunawala,almundsryd,almunecar,almunge,almunia de dona godina,almuniente,almur,almuradiel,almurfe,almurta,almurta east,almurtazabad,almus,almusafes,almushahir,almuzara,almvik,almwick,almy,almy saray,almyakova,almyakovo,almyasherka,almyashevka,almyashevko,almyasovo,almyavovo,almyon,almyra,almyros,almysh,almyshabad,almyville,alna,alna center,alnaeni,alnafinda,alnali,alnarp,alnaryd,alnas,alnaset,alnashi,alnasr,alnavar,alnavere,alnay,alnazar,alne,alnery,alnes,alnesaneh,alness,alnetsk,alnham,alni,alniacik,alniak,alniali-bash,alniawas,alnif,alnmouth,alno,alnoir,alnoor colony,alnor,alntalau,alntorp,alnwick,alnyash,alnysh,alo,alo aloia,alo aloia mission,alo bagarai,alo banda dheri,alo dasht,alo di stura,alo ghundai,alo kala,alo kalay,alo kali,alo kelay,alo kili,alo mahar,alo manrai,alo mohar,alo-on,alo-ozero,aloa,aloabideche,aloacocha,aloag,aloakandi,aloakhawa,aloalo,aloalofotaka,aloan,aloandia,aloang,aloapam,aloapan,aloasi,aloazure,aloba,aloba ile,alobaloke,alobamba,aloban,alobanei,alobas,alobaytal,alobi,alobiga,alobijod,alobilod,alobo,alobras,alobri,alobuela,aloburo,alobzok,alocao,alocen,aloch,alochia,alod,aloda,aloda bagla,alodad,alodawya,alode,alodha,alodialna ligota,alodifi,alodin,alodinan,alodzene,alodzeni,alodzeny,aloe,aloe corner,aloede,aloedoe,aloee boe,aloee boee,aloee brieung,aloee it,aloee titi,aloeeiet,aloegaai,aloel,aloema,aloen,aloepi,aloepi 1,aloepi 2,aloepi een,aloepi twee,aloeran,aloerboentoe,aloertampa,aloes,aloesi,aloet,alofau,alofe,alofi,alofitai,aloga,alogai,alogak,alogalung,alogani,alogaquillo,alogarno,alogbale kope,alogenia,alogha,alogi,aloglu,alogoecog,alogontang,alogtog,alogue,alogui,aloguinsan,aloguru,alogzher,aloha,aloha kona,alohali,alohkapw,alohoso,alohove,alohungari,alohungari banta,aloi,aloi lubok,aloidhai,aloidhes,aloike,aloisa,aloisianika,aloisov,aloit,aloiza,aloizianika,aloja,alojamiento de quitala,alojamiento sallihuinca,alojas muiza,alojaya,alojera,alojipan,alojo,alojori,alojzov,alojzow,alok,aloka,alokan,alokanou,alokap,alokapoketi,alokawal,alokbali,alokdia,alokdihi,aloke,alokedia,alokenou,alokhar,alokhel,alokhi,alokiode,alokiri,alokjhari,alokkangama,alokke,aloknik,aloko,aloko koffikro,aloko kouakoukro,aloko yobouekro,aloko-kofikro,alokodje,alokoe,alokoegbe,alokokmikro,alokokro,alokonik,alokonou,alokope,alokoua,alokoueve,alokoukro,alokouyakro,alokowzi,alokozi,alokpa,alokpe,alokpui,aloktoang,alokuja,alokuk,alokura,alokuya,alokwa,alol,alol lamma gad,alola,alolale,alolar,alolayag,alolda,aloleng,aloli,alolo,aloloi,alolpuqio,alolu,alolya,alom,aloma,alomaja,alomanie,alomanou,alomari,alomartes,alomarupu,alomata,alomatagnipa,alomato,alomatoupe,alomatuope,alomba,alombada,alombi,alombo,alomdyel,alomegas,alomet,alomin,alomo,alompang,alomzug,alomzugtanya,alon,alon moreh,alon-alon barat,alon-morah,alona,alonah,alonai,alonaki,alonakia,alonakion,alonalon,alonas,aloncho,alonchia-igobama,alondi,alondigwena,alondra,alondra park,alone yitzhak,alonei-yitshaq,aloneros,alones,along,alonga,alongalong,alongan,alongane,alongid,alonglei,alongntang,alongo,alongo de abajo,alongongan,alongos,alongot,alongshan,alonha,aloni,aloni gory,alonia,alonilory,alonim,alonion,alonistaina,alonjiro,alonjirori,alonka,alonnah,alonnisos,alonogan,alonos,alonsa,alonsillo,alonskiy,alonskoye,alonso,alonso de rojas,alonso lince,alonso medina,alonso osorio,alonso rojas,alonsohue,alonsotegui,alontsev,alontsovski,alontsovskiy,alonung,alonya,alonye,alonzaville,alonzo,alonzo lazaro,aloocheh malek,aloodar,aloohuasi,aloomba,aloosjerd,alop,alopa,alopi,alopidorp,alopovo,aloppe,alopua,alopui,alopul,alopuli,aloquin,aloquinsan,alor,alor bungga sena,alor gajah,alor gajeh,alor janggus,alor lanchang,alor limbat,alor lintah,alor lintang,alor lubok,alor nibong,alor pak ngah,alor pongsu,alor pulai,alor sena,alor setar,alor star,alor tampang,alor-kecil,alor-ketjil,alora,alorai,aloran,alore,aloren,alorgat,alorgui,alori,aloria,aloribilo,alorigarwara,aloripit,alorkpa,alorna,aloro,alorobia-ambovombe,aloron,alorongga,aloros,alorsi,alortani,alorton,alos,alos de balaguer,alos de isil,alos-sibas-abense,alos-sibas-abense-de-haut,alos-sibas-albense-de haut,alose,aloshenka,aloshi,aloshynske,alosi,alosies,alosika,alosno,aloso,alosolo,alossi,alosso,alosso i,alosso un,alosso-ii,alost,alostar,alot,alota,alotai,alotaichen,alotaihsien,alotancomba,alotatahoma,alotau,alotenango,alotepec,aloti,alotip,alotitlan,aloto,alotoa,alotocuanha,alotogrom,alotome,alotovshchina,alotsakanga,alotshuai,alou,alouakro,alouana,alouane,alouarg,alouba,aloude,aloudet,aloudin,alouenou,alouet el gounna,alouf,aloufa,aloufakope,alougel,alouggou,alougoum,alougouz,alouguel,alouhouehoue,alouibo,aloujnyn,alouki,alouko sakassou,aloukokro,aloukoukro,aloukro,aloukro diekro,aloukro drekro,aloukro namoueno,aloulaye,aloulena,aloulouba,aloum,alouma,aloumabo,aloumboukou,aloume,aloumere,aloun,alouna,alounamouenou,alounde,aloune,alouniang,aloupi,aloura,alous,alousan,aloushta,alouss,aloussou,alout,aloute,aloutoyiannaiika,alov,alove,alove akalycia,alovera,aloves,alovi,alovikote,alovingo,alovitoke,alovo,alovuddin,alow,alowa,alowali,alowarou,alowdad,alowen banda,alowgak,alowike,alowo,alowo esin,alowonle,alowou,alows,alowulan,alowzi,aloxe-corton,aloya,aloyan,aloye pole,aloyi,aloyon,aloys,aloyum,aloyun,alozai,alozaina,alozay,alozero,alozi,alozo,alp,alp grum,alp laret-dadaint,alp schmorras,alp spinas,alp vallatsch,alp valtnov,alpa,alpa corral,alpa kalan,alpa sudnari,alpabamba,alpaca,alpacan,alpach,alpachaca,alpachai,alpachay,alpachiri,alpaco,alpacollo,alpacoma,alpacut,alpaevsk,alpagay,alpagot,alpagut,alpaguz,alpahuasi,alpajere,alpako,alpakovo,alpalhao,alpaluce,alpamala de toro,alpamala de velasco,alpamanga,alpamarca,alpamina,alpampa,alpan,alpana,alpanate,alpande,alpandeire,alpanocan,alpanos,alpanoz,alpanseque,alpar,alpare,alparea,alpargaton,alpari tanyak,alparrache,alparslan,alpartir,alpas,alpas hacienda,alpasali,alpasca,alpasinche,alpaslan,alpat,alpata,alpatacal,alpathar,alpatlahua,alpatlahuac,alpatov,alpatovo,alpatovskiy,alpatvo,alpatyevo,alpaugh,alpaut,alpavat,alpavet,alpavit,alpavot,alpavut,alpayevo,alpayoc,alpbach,alpbyn,alpe,alpe acquanegra,alpe acquanera,alpe agrosa,alpe airale,alpe balma rossa,alpe basciumo,alpe basso,alpe braita,alpe busarasca,alpe caldenno,alpe campo,alpe campo di sotto,alpe campolomana,alpe campolungo,alpe casarolo,alpe cava,alpe cavegna,alpe cazzola,alpe cheggio,alpe ciamporino,alpe col,alpe cortefreddo,alpe cortelancio,alpe cortevecchio,alpe cortone,alpe de la barma,alpe de noveli,alpe de peryeire,alpe de tortin,alpe della costa,alpe devero,alpe di boggio,alpe di bondeno,alpe di camona,alpe di cava,alpe di corfino,alpe di cortas,alpe di crastera,alpe di dumenza,alpe di lenno,alpe di naccio,alpe di quarnajo,alpe di scieru,alpe di teste,alpe di trescolmen,alpe dil chant,alpe dosde,alpe forgnone,alpe forno,alpe gembre,alpe gera,alpe giove,alpe grasso,alpe groppera,alpe hannig,alpe i motti,alpe la satta,alpe lagarasc,alpe lagarsc,alpe larecchio,alpe lareccio,alpe lorino,alpe mandetto,alpe marghino,alpe mattignale,alpe mulecetto,alpe ogliana,alpe ogliano,alpe olza,alpe pilocca,alpe piodella,alpe pitocca,alpe porcarescio,alpe pulgabio,alpe quarnaro,alpe rasica,alpe rebella,alpe rebellao,alpe regina,alpe ritom,alpe schmorras,alpe sfille,alpe silvretta,alpe steblino,alpe strencia,alpe tartarea,alpe torcelli,alpe trescolmine,alpe vago,alpe valaverta,alpe vald,alpe valle,alpe vauzone,alpe zucchero,alpe zunchi,alpechin,alpedreira,alpedrete,alpedrete de la sierra,alpedrinha,alpedriz,alpedroches,alpegut,alpekmez,alpen,alpen siding,alpena,alpena junction,alpendorf,alpendurada,alpenes,alpenhausen,alpenrod,alpens,alpera,alperbruck,alpercata,alperiz,alpers,alperstedt,alperts,alpes,alpes da cantareira,alpes de caieiras,alpestre,alpet,alpette,alpevka,alpeyevka,alpeyevo,alpfahrt,alpha,alpha hamjatu,alpha heights,alpha mobile home park,alpha nding,alphaba,alphadanga,alphanagar,alphano,alpharetta,alpharetta woods,alpharette,alphelaar,alphelaer,alphen,alphen aan de rijn,alphen aan den rijn,alphen aan der rijn,alphen boschoven,alphen bosschoven,alphen bridge,alphen-boshoven,alphen-oosterwijk,alphenbrug,alphenia landing,alpheton,alpheus,alphine village,alphington,alphoretta,alphut,alpi,alpi di trescolmine,alpi di vaudet,alpi vaudet,alpiarca,alpicat,alpicella,alpicelle,alpiggia,alpiglen,alpignano,alpiiskoe,alpijskoe,alpikoy,alpina,alpine,alpine acres,alpine bay,alpine cellars village,alpine heights,alpine hills,alpine junction,alpine lake,alpine meadows,alpine mobile manor,alpine ranchettes,alpine ranchettes two,alpine shores,alpine terrace,alpine trailer court,alpine view,alpine village,alpington,alpinino,alpinopolis,alpirsbach,alpitiya,alpixaha,alpiyskoye,alpizar,alpkoy,alpl,alplager dombay,alplaus,alpnach,alpnachstad,alpoca,alpoim,alpokhori,alpokhori-botsari,alpokhorion,alporchones,alportel,alpospita,alpossos,alpout,alpout pervyy,alpout-udzhar,alpowa,alpoyeca,alpozanga,alpozonga,alpozu,alppikyla,alppila,alprendaj peqin,alpriate,alps,alpsatar,alpsray,alpsteig,alpsville,alptal,alptamyri,alpthal,alpu,alpua,alpudere,alpuderesi koyu,alpuech,alpuente,alpugan,alpujarra,alpujarras,alpujarrita,alpullu,alpurai,alpurrurulam,alput,alpuyeca,alpuyeque,alpuyequito,alpy,alpyspay,alqajar,alqamah an nu`man,alqar,alqasabad,alqaterek,alqazbaygi,alqazbegi,alqazi,alqchin-e bala,alqchin-e pain,alqechin-e bala,alqian,alqir,alqosh,alqu,alquebre,alqueidao,alqueidao da serra,alqueidao de macas de dona maria,alqueidao de santo amaro,alqueidao do mato,alqueidaozinho,alquera,alqueria,alqueria andres bueno,alqueria arauzo,alqueria ariscos,alqueria blanca,alqueria cabaloria,alqueria canedino,alqueria casasola del campo,alqueria corbacera,alqueria de aznar,alqueria de campanero,alqueria de la condesa,alqueria domingo senor,alqueria fargue,alqueria herreros,alqueria herreros de salvatierra,alqueria la duena de abajo,alqueria la duena de arriba,alqueria la fragua,alqueria la pera,alqueria martinebron,alqueria miguel munoz,alqueria naharros de valduciel,alqueria naharros del rio,alqueria olleros,alqueria otero de maria asensio,alqueria revilla,alqueria san cristobal del monte,alqueria san vicente,alqueria sotrobal,alqueria torre de moncantar,alqueria zarzoso,alquerias,alquerubim,alqueva,alqueve,alquezar,alqui,alquidon,alquife,alquina,alquines,alquite,alquitrana,alquiza,alquizapan,alquizar,alqur,alquyruq,alra,alraft,alraj,alrance,alrapark,alray,alread,alresford,alrewas,alrick,alridge,alro by,alroi,alrode,alrota,alrote,alrum,als,als brohuse,als hede,alsa,alsaaker,alsace,alsace manor,alsacia,alsacia cosulta,alsad,alsag,alsager,alsaker,alsam,alsancak,alsandar,alsang,alsangol,alsar,alsard,alsarp,alsarsia,alsasia,alsask,alsasua,alsater,alsatia,alsatter,alsawa,alsbach,alsbach-hahnlein,alsback,alsberg,alsbjaerg,alsbjerg,alsbo,alsbyn,alschbach,alscheid,alsching,alscot,alsdorf,alse,alsea,alsea rivera,alseda,alsedzhay,alsedziai,alsedziu,alseid,alseikos,alselskiy vyselok,alsem,alsemberg,alsen,alsen heights,alsenberg,alsenborn,alsenbruck,alseno,alsenyevo,alsenz,alsepehr,alserdeich,alserum,alserwurp,alseseca,alseseca el chico,alseseca el grande,alset,alsey,alsfassen,alsfeld,alsgaarden,alsgard,alsgarde,alshan,alshanitsa,alshanka,alshanskiy,alshanskiye vyselki,alshany,alshatovo,alshayka,alsheim,alsheim-gronau,alsheyevo,alsheyevskiy,alsheyeyeskiy,alshi,alshikhovo,alshimbay,alsholt,alshovka,alshta,alshult,alshun chay,alshvanga,alsi,alsiai,alsike,alsila,alsina,alsindhi,alsininkai,alsip,alsisar,alsiu,alsium,alsjarv,alsjo,alsjoholm,alsjokulla,alsjoomradet,alskaliden,alskat,alski,alskog,alskov,alsleben,alslev,alslev kro,alslov,alsmannsdorf,alsmoos,alsnas,also,also bendek-puszta,also bodok,also csitar,also essoi-major,also horvathmalom,also inoka-malom,also koroknya,also macskas,also mako-puszta,also nadasd-puszta,also nyerges-puszta,also szallas,also szanas-puszta,also szeli,also szemered,also szenasvolgy,also-balog,also-barand-major,also-bisztricze,also-csil,also-ivany-puszta,also-kalosa,also-major,also-malom,also-patlan-puszta,also-rozsnaki-tanya,also-szecse,also-tur,also-valy,also-venkert,also-zsukma-erdoorhaz,alsoabrany,alsoadacs,alsoanyas,alsoaradi,alsoaranyod,alsoatokhaza,alsoavas,alsobabad,alsobadurbokor,alsobanyatelep,alsobaricska,alsobebespuszta,alsobencsek,alsobendek-puszta,alsobene,alsoberecki,alsoberecska,alsobesnyo,alsobeszterce,alsobesztercze,alsobikad,alsobikol,alsobocsar,alsobogardpuszta,alsobogat,alsobogatpuszta,alsoboldogfalva,alsobolkeny,alsoborcshaza,alsoborsod,alsoborzsony,alsobucka,alsocebe,alsocece,alsocikola,alsocsala,alsocsalanos,alsocsalogany,alsocsampa,alsocsarda dulo,alsocsardadulo,alsocsatar,alsocsernaton,alsocserpuszta,alsocsesztapuszta,alsocsillag,alsocsinger,alsocsingervolgy,alsocsolyos,alsocsonget,alsocsurgo,alsocun,alsodabasi dulo,alsodabasi szolok,alsodabasiszollok,alsodabrony,alsodanos,alsodaruvar,alsodelegyhaza,alsodobos,alsodobospuszta,alsodobrogec,alsodobsza,alsodolina,alsodomboru,alsodombro,alsodorgicse,alsodulo,alsodux,alsoegreskata,alsoegrespuszta,alsoerdo,alsoerdopuszta,alsoerek,alsoerzsebet-tanya,alsoerzsebetpuszta,alsoessopuszta,alsofakos,alsofalu,alsofarkasd,alsofarkasdmajor,alsofeketeerdo,alsofeketefoldek,alsofeketevolgy,alsofutak,alsofutyemajor,alsofuves,alsofuzes,alsogagy,alsogaicspuszta,alsogajdimalom,alsogalambos,alsogalla,alsogasparsor,alsogazdagdulo,alsogod,alsogola,alsogolapuszta,alsogorzsony,alsogyertyan,alsogyocs,alsogyorgyos,alsogyotapuszta,alsogyukes,alsohalom,alsohangos,alsohard,alsohatartanyak,alsohegy,alsohek,alsohernad,alsohesteny,alsoheteny,alsohidveg,alsohomokerdo,alsohomorod,alsohrabocz,alsohuta,alsoidecs,alsoistvand,alsoistvand-puszta,alsoiszkaz,alsoittebe,alsoivanpuszta,alsojanosfa,alsojaras,alsok,alsokabol,alsokalkaturalis,alsokanda,alsokara,alsokatalin-banyatelep,alsokimpany,alsokispuszta,alsokoher,alsokolked,alsokommasszacio,alsokortvelyes,alsokovacsgyop,alsokovacsmajor,alsokoves,alsokovesd,alsokovil,alsokozpont,alsokristyor,alsoksaly,alsokubin,alsokustany,alsolajospuszta,alsolakos,alsolegencse,alsolendva,alsolendvalakos,alsolengyend,alsoleperd,alsolog,alsomagyalos,alsomajor,alsomajsa,alsomalom,alsomalomtelep,alsomama,alsomandpuszta,alsomarosd,alsomegy,alsomegyes,alsomenyod,alsomenyodpuszta,alsomera,alsomezo,alsomezotanya,alsomiholjac,alsomiholjacz,alsomikebuda,alsomislye,alsomocsar,alsomocsarasdulo,alsomocsolad,alsomonostor,alsomoricgat,alsomozsar,alsomuszaj,alsomuzsai tanyak,alsonadasd,alsonana,alsonanai laiver,alsonanai mauszcsarda,alsonderup,alsonemedi,alsonemedi szollok,alsonemesapati,alsonggol,alsonovaj,alsonyarsapat,alsonyek,alsonyeki tanyak,alsonyiget,alsonyires,alsonyirvarimajor,alsonyirvarmajor,alsonyomas,alsooreghegy,alsooregszollok,alsooregszolok,alsoors,alsoors-villatelep,alsoorspuszta,alsop,alsop corner,alsop en le dale,alsopahok,alsopakony,alsopalajpuszta,alsopalos,alsopandur,alsopaskum,alsopatlan,alsopaty,alsopazsit,alsopel,alsopere,alsopeszer,alsopeteny,alsopoeny,alsopojeny,alsoporboly,alsopuszta,alsoracegres,alsorajk,alsore,alsoregmec,alsoregmeci tanyak,alsorepa,alsoret,alsoretdulo,alsoreti dulo,alsoreti tanya,alsoreti tanyak,alsorev,alsoronok,alsorozsnakpuszta,alsos,alsosag,alsosanc,alsosarlospuszta,alsosashalomdulo,alsosaskalapos,alsosegesd,alsosima,alsosofalva,alsosomlyo,alsososkutbokor,alsosotetmajor,alsostrazsa,alsoszakacs,alsoszallas,alsoszallasok,alsoszalmavar,alsoszalmavar-major,alsoszanaspuszta,alsoszapudpuszta,alsoszaszberek,alsoszekto,alsoszeleste,alsoszentbenedek,alsoszenterzsebet,alsoszentivan,alsoszentkata,alsoszentmarton,alsoszenttamas,alsosziget,alsoszigeti major,alsoszlatina,alsoszollo,alsoszolnok,alsoszolnoki tanyak,alsosztamora,alsoszuha,alsotabdi,alsotabpuszta,alsotamasd,alsotanya,alsotanyak,alsotapazd,alsotapioi tanya,alsoteglahaz,alsotekeres,alsotelekes,alsotengelic,alsotetelalja,alsoto,alsotoborzsok,alsotold,alsotollosmajor,alsotollospuszta,alsotovistetotanya,alsotundermajor,alsoujhegy,alsoujlak,alsoujtagmajor,alsoutaspuszta,alsovadacs,alsovadasz,alsovalenyagra,alsovany,alsovaros,alsovarosi feketefoldek,alsovarosi godrok,alsovarsany,alsovasarter,alsovasdinnyepuszta,alsoverzar,alsovice,alsovonyarc,alsovranyos,alsovroszi,alsozatony,alsozorlenc,alsozsolca,alsozsukma,alsozsukma-puszta,alsozsunypuszta,alspaugh,alsput,alsrode,alst,alsta,alstad,alstaden,alstadt,alstadten,alstahaug,alstanza,alstatte,alstatten,alstead,alstead center,alsted,alsted-flinterup,alstedde,alsten,alster,alsterberg,alsterbro,alsterdorf,alsterfors,alsterloh,alstermo,alstertal,alsterweiler,alsting,alston,alstonefield,alstonfield,alstonville,alstorp,alstown,alstrup,alstugorna,alsu,alsufyevo,alsum,alsuma,alsunda,alsung,alsunga,alsup,alsupe,alsupes,alsupes pusmuiza,alsure,alsuri,alsusta,alsuyeh,alsvaag,alsvag,alsvanga,alsvarta,alsviga,alsvik,alsvik i ryfylke,alsvika,alsviki,alsvikis,alsville,alswede,alsweiler,alsy,alsyapin,alsyapinskiy,alsyay,alsynbayeva,alszeg,alszopor,alt,alt antonowen,alt bagnowen,alt balluponen,alt barenaue,alt baudendorf,alt bauer,alt beelitz,alt belz,alt benatek,alt bennebek,alt bestendorf,alt bidschow,alt bischofstal,alt bodschwingken,alt bohmen,alt bolitten,alt bork,alt brieselang,alt buchhorst,alt bukow,alt bunzlau,alt christburg,alt ciwitz,alt coln,alt cosel,alt czaiken,alt czayken,alt czymochen,alt daber,alt damerow,alt dollstadt,alt duvenstedt,alt duwisib,alt ellguth,alt engeldorferhof,alt erbersdorf,alt erfrade,alt falkenhagen,alt fliessdorf,alt fresenburg,alt furstenhutte,alt gaarz,alt gaidzen,alt garge,alt garschen,alt gatschow,alt gehland,alt georgswalde,alt gertlauken,alt golm,alt grafenwalde,alt gramatin,alt gremmin,alt guraletsch,alt gustelitz,alt guthendorf,alt hagebok,alt hattlich,alt holtum,alt hummel,alt husayn,alt isenhagen,alt jabel,alt jablonken,alt jargenow,alt jucha,alt kabelich,alt kaebelich,alt kaletka,alt kalken,alt karin,alt karlsthal,alt katwin,alt kelbonken,alt kelken,alt kentzlin,alt keykuth,alt kiwitten,alt knin,alt kockendorf,alt kolin,alt kolziglow,alt konigsborn,alt koppenbruck,alt kosenow,alt krenzlin,alt kriewen,alt krussow,alt krzywen,alt krzywen kreis lyck,alt kussfeld,alt kustrinchen,alt lappinen,alt liabehne,alt libbehne,alt lienken,alt lietzegoricke,alt limmritz,alt lipa,alt lomnitz,alt lonnewitz,alt lower,alt lublitz,alt lutterow,alt madlitz,alt mahlisch,alt marienburg,alt marrin,alt martinsdorf,alt marxowen,alt mengede,alt mertinsdorf,alt meteln,alt milow,alt modewitz,alt molln,alt muhlendorf,alt munkeboe,alt munsterberg,alt neb das,alt negentin,alt oer,alt olisch,alt ostdorf,alt paka,alt pannekow,alt pansow,alt panstorf,alt parisau,alt passarge,alt pastitz,alt patschkau,alt perlswalde,alt petersdorf,alt petrein,alt pfauendorf,alt phaleron,alt pilsenetz,alt plestlin,alt pocher,alt podeizig,alt pokrent,alt polchow,alt poorsdorf,alt poorstorf,alt popelau,alt poppelau,alt poppentin,alt possigkau,alt proberg,alt rathjensdorf,alt ravenhorst,alt reddevitz,alt rehfeld,alt rehse,alt rognitz,alt rosengart,alt rosenthal,alt rucksmoor,alt rudnitz,alt rudowken,alt ruppersdorf,alt ruppin,alt samitz,alt sammit,alt sankt johann,alt sassitz,alt sattl,alt schabienen,alt schadow,alt schalkendorf,alt schalkowitz,alt schlage,alt schlagsdorf,alt schledehausen,alt schmecks,alt schokau,alt schonau,alt schoneberg,alt schwambach,alt schwemmen,alt schwerin,alt seggebruch,alt siegelsum,alt stahnsdorf,alt stassow,alt steinbeck,alt steinhorst,alt suchoros,alt suchoross,alt suhrkow,alt sullitz,alt tabor,alt tellin,alt temmen,alt terranova,alt teschen,alt teterin,alt thein,alt toggenburg,alt toplitz,alt torgelow,alt tucheband,alt ujest,alt und neu bleyen,alt ungnade,alt upper,alt uszanni,alt uszanny,alt valdek,alt verpel,alt vierzighuben,alt vorwerk,alt wallmoden,alt wartenburg,alt wendischthun,alt werder,alt wetten,alt wiedenthal,alt wiessee,alt wittenbeck,alt wolfsburg,alt wraz,alt zachun,alt zarrendorf,alt zauche,alt zeschdorf,alt zowen,alt-albenreuth,alt-banzin,alt-beba,alt-betsche,alt-borscha,alt-breisach,alt-buchara,alt-burgersdorf,alt-daubitz,alt-ehrenberg,alt-etschka,alt-felsberg,alt-fennern,alt-futok,alt-gradiska,alt-griebnitz,alt-habendorf,alt-hart,alt-harzdorf,alt-heule,alt-hummelshof,alt-hwiezdlitz,alt-jankowitz,alt-jankowzi,alt-kaltenstein,alt-kammer,alt-kanischa,alt-keer,alt-ker,alt-kinsberg,alt-langendorf,alt-langwasser,alt-lappienen,alt-leetva,alt-letz,alt-massu,alt-moletein,alt-palanka,alt-pasua,alt-paulisch,alt-paulsdorf,alt-pazua,alt-prerau,alt-prerow,alt-quetzin,alt-reihwiesen,alt-rohlau,alt-rothwasser,alt-sankamen,alt-sankt michael,alt-sankt-iwan,alt-sattel,alt-sattl,alt-schallersdorf,alt-schiedel,alt-schowe,alt-schrenkenthal,alt-schwaneburg,alt-sedlowitz,alt-seeis,alt-sivac,alt-siwatz,alt-sommersdorf,alt-spitzenberg,alt-stapar,alt-stieringen,alt-straschnitz,alt-tann mulhausen,alt-thann,alt-tohan,alt-tsyurikh,alt-varenburg,alt-veymar,alt-vogelseifen,alt-walddorf,alt-wasser,alt-werbass,alt-wiedikon,alt-zechsdorf,alt-zedlisch,alta,alta buena vista,alta clara,alta cordoba,alta cruz,alta cuesta,alta de la piedra,alta flores,alta floresta,alta gloria,alta gracia,alta habana,alta hill,alta hills,alta italia,alta lagubeh,alta laja azul,alta lake,alta loma,alta luz,alta manor,alta marina,alta mesa,alta mills,alta mir,alta mira,alta miranda,alta misa,alta perilla,alta sierra,alta sierra estates,alta sierra ranches,alta uxpanapa,alta view,alta vista,alta vista park,alta vista subdivision number 1,alta vista terrace,alta-apan,alta-rudnitz,altabayevo,altable,altacanyada,altach,altaclara,altadena,altadena ridge estates,altadena woods,altadighi,altadoo,altadush,altaf,altaf ali shah,altaf colony,altaf ganj,altaflor,altafulla,altagene,altagowlan,altagracia,altagracia de la montana,altagracia de orituco,altahalla,altai,altain,altaine,altair,altaisharasume,altaisk,altaiskoe,altaist,altak,altakeeran,altakuri,altal triga,altalan,altalmira,altamachi,altamaha,altamaha park,altamahaw,altamar,altamarani,altamaria,altamash,altamesa,altamioc,altamir,altamira,altamira de nuevo sinaparo,altamira do maranhao,altamirada,altamirani,altamirano,altamirano norte,altamirano sud,altamirano sur,altamirkot,altamiros,altamisa,altamisal,altamiz,altamiza,altamizal,altammah,altammerthal,altamont,altamont park,altamont place,altamont switch,altamonte springs,altamora,altampasi,altamullan,altamur,altamura,altamuskin,altan,altan bulag,altan bulak,altan emeel,altan emel,altan gadas sum,altan hil,altan obo,altan teeli,altan teeli somon,altan teeli suma,altan teeli sumu,altan teli sume,altan xiret,altan-ovoo,altan-tel,altan-teli,altan-tepe,altana,altanalchin,altanbulag,altanca,altanda,altandhu,altandon,altandoune,altandow,altanduin,altandy,altaneira,altaneiras,altanemel,altanka,altann,altans,altanshiree,altanskiy,altansy,altanteel,altantsogts,altany,altanzing,altapampa,altapass,altaquer,altar,altar alto,altar ghanagak,altar ghaneh gak,altar ghangak,altar urco,altarad,altarane,altarangak,altarani,altardo,altare,altarejos,altares,altarghana,altarghanagak,altarghanah,altarghanahgak,altarghaneh,altarghanehgak,altarghangak,altarghu,altarik,altarnun,altaro,altarobanyatelep,altaruma,altary,altas,altas claras,altas de ibarra,altasbach,altasha,altaskaya,altastenberg,altat,altata,altataj,altatayn dugang,altateskin,altatinskiy,altatskiy,altatskoye,altatu,altatuin dugang,altatump,altaussee,altautina,altavas,altavilla,altavilla irpina,altavilla milicia,altavilla silentina,altavilla vicentina,altaville,altavista,altavista de ramos,altawood,altawood subdivision number 3,altay,altay somon,altay suma,altay sumu,altay yuchang,altayak,altayakova,altayeva,altayevo,altayka,altayli,altayskaya,altayskiy,altayskiy kamlak,altayskoye,alta\302\261nca\302\261k,alta\302\261nova,altbabensham,altbach,altbachenbruch,altbachenbruck,altbacherhof,altbarnim,altbasi,altbauhof,altbelgern,altbellin,altbensdorf,altbernsdorf,altbernsdorf auf dem eigen,altbeschenowa,altbessingen,altbiela,altbierlingen,altbleyen,altbokhorst,altbork,altbrandsleben,altbreitenfelderhof,altbroshkouts,altbudkowitz,altbulach,altbulk,altburg,altburlage,altcar,altchemnitz,altcunnewitz,altcustrinchen,altdamm,altdietmanns,altdobern,altdorf,altdornfeld,altdornfeld und neudornfeld,altdorpsen,altdrossenfeld,altdurnbuch,alte,alte buden,alte drusel,alte fakt delele,alte glashutte,alte hausstelle,alte kirche,alte kolonie,alte kolonie westhausen,alte meierei,alte peldenmuhle,alte peunt,alte steeg,alte-brunne,altea,altea la vieja,alteado,altebabke,alteberspoint,alteburg,alteckendorf,altedo,altee,altefahr,altefeld,altegg,alteglofsheim,alteheide,altehufe,alteiche,alteidet,alteiselfing,altejar,alteke,altembroek,altembrouck,altemchi,altemir,alten,alten buseck,altena,altenach,altenaffeln,altenahr,altenau,altenauer silberhutten,altenbach,altenbach-leulitz,altenbach-zeititz,altenbaindt,altenbamberg,altenbanz,altenbauna,altenbeichlingen,altenbeken,altenberg,altenberg bei leibnitz,altenberg bei linz,altenberga,altenberge,altenbergen,altenbeuern,altenbeuren,altenbeuthen,altenbochum,altenbodingen,altenbogge,altenboitzen,altenbora,altenbork,altenbrak,altenbreckerfeld,altenbreitungen,altenbrendebach,altenbrouck,altenbruch,altenbruch-westerende,altenbruck,altenbrunslar,altenbuch,altenbuch dobernei,altenbucken,altenbude,altenbulstedt,altenbunnen,altenburen,altenburg,altenburg drescha,altenburschla,altencelle,altencreussen,altendambach,altendamm,altendeich,altenderne-niederbecker,altenderne-oberbecker,altendettelsau,altendiestedde,altendiez,altendonop,altendorf,altenebstorf,alteneich,alteneichen,altenerding,altenesch,altenessen,altenfahr,altenfahre,altenfeld,altenfelde,altenfelden,altenfeldsdeich,altenfurt,altengamme,altengesees,altengeseke,altenglan,altengonna,altengors,altengottern,altengraben,altengrabow,altengreuth,altengroden,altengroitzsch,altengronau,altengruen,altengrun,altenhagen,altenhagen eins,altenhagen zwei,altenhagen-nienhagen,altenhain,altenhaina,altenham,altenhammer,altenhasslau,altenhasungen,altenhausen,altenheerse,altenheideck,altenheim,altenhellefeld,altenherfen,altenhof,altenhof am hausruck,altenhofen,altenhohe,altenholte,altenholz,altenhorst,altenhovel,altenhuffen,altenhulscheid,altenhundem,altenhuntorf,altenilpe,altenkaisen,altenkamp,altenkattbek,altenkessel,altenkirch,altenkirchen,altenkleusheim,altenklitsche,altenkoog,altenkreith,altenkrempe,altenkreussen,altenkunsberg,altenkunstadt,altenlinde,altenlinden,altenlingen,altenlohe,altenlotheim,altenlunne,altenmais,altenmarhorst,altenmarkt,altenmarkt an der alz,altenmarkt an der triesting,altenmarkt bei furstenfeld,altenmarkt bei riegersburg,altenmarkt bei sankt gallen,altenmarkt bei wies,altenmarkt im isperthale,altenmarkt im pongau,altenmarkt im thale,altenmarkt im ysperthal,altenmedingen,altenmelle,altenmellrich,altenmethler,altenmethlerheide,altenmittlau,altenmoor,altenmuhr,altenmunster,altennumbrecht,alteno,altenohr,altenowo,altenoythe,altenparkstein,altenplathow,altenplatow,altenpleen,altenplos,altenqoke,altenrade,altenrain,altenrath,altenreit,altenreith,altenreuth,altenrhein,altenrheine,altenricht,altenried,altenriet,altenritte,altenroda,altenrode,altenrond,altenroxel,altenruthen,altensalz,altensalzkoth,altensalzwedel,altensam,altenschlag,altenschlirf,altenschloot,altenschneeberg,altenschonbach,altenschwand,altensdorf,altensee,altenseelbach,altensenne,altensiedel,altensiel,altensien,altensittenbach,altenspeckfeld,altenstadt,altenstadt an der waldnaab,altensteig,altensteigdorf,altenstein,altensteinreuth,altenstuhlen,altensturmberg,altensweiler,altental,altenteich,altenthal,altenthann,altentreptow,altentreswitz,altentrudingen,altenufer,altenvalbert,altenveldorf,altenvers,altenvoerde,altenvorde,altenwahlingen,altenwald,altenwalde,altenweddingen,altenweide,altenweiher,altenwenden,altenwerder,altenwiese,altenwiesen,altenwillershagen,altenworth,altenzaun,altenzoll,altenzollen,altepec,altepenila,altepepan,altepexi,alter,alter do chao,alter finkenkrug,alter futterplatz,alter gobricherweg,alter graben,alter hain,alter markt,alter pedroso,alterdorf,alteren,alterfing,alterilz,alterkamp,alterkulz,alterlaa,alterlangen,alterode,alterosa,alterpampa,altersbach,altersberg,altersbruk,alterschrofen,altersham,altershausen,alterstedt,altersteeg,alterswil,alterswilen,altervattnet,alterzoll,altes dorf,altes lager,alteschmeltz,altessano,altessing,altestovo,altet,alteveer,altevik,altewiek,alteza,altfalter,altfalterbach,altfalterloh,altfaltern,altfaltersberg,altfeld,altfelde,altferchau,altfinken,altfinkenstein,altfinnentrop,altforst,altforweiler,altfranken,altfraunhofen,altfriedland,altfriesack,altfunnixsiel,altfurstenhutte,altfuttak,altgalendorf,altgalow,altgandersheim,altgarmssiel,altgarz,altgaude,altgaul,altgeld gardens,altgeringswalde,altgernsdorf,altgfohl,altglandorf,altglashutte,altglashutten,altglienicke,altglietzen,altglobsow,altgmain,altgodens,altgolssen,altgommla,altgralla,altgreshan,altgruland,altha,althagen,althaidhof,althaldensleben,altham,althammer,althammer goschutz,altharen,altharlingersiel,altharmhorst,althattendorf,althaus,althaus alpe,althausen,althea,althegnenberg,altheide,altheide bad,altheim,altheim ob weihung,altheimer,altheimersberg,althellmonsodt,althen,althen-des-paluds,althen-les-paluds,althengstett,altherandstal,altherzberg,althexenagger,althhutten,althirschstein,althodis,althof,althofen,althofen unterer markt,althoflein,altholtum in der marsch,altholzhausen,altholzkrug,althom,althorn,althornbach,althorne,althorp,althorpe,althorst,althouse,althueb,althutte,althuttebei mariensee,althutten,althuttendorf,althutterhof,alti,alti aghaj-e bozorg,alti aghaj-e kuchak,alti aqaj,alti bay,alti bulaq,alti bulaq tuzkan,alti gombaz,alti gonbad,alti gumbaz,alti gunbad,alti khvajeh,alti khwaja,alti khwajah,altia,altiagac,altiagach,altian,altiani,altiaux,altiba,altibai,altibulaq tozkan,altica,alticane,altico,altidao,altidona,altier,altiere,altieres,altieri,altigi,altigoz,altigozbekirli,altiho,altikapi,altikapi koyu,altikara,altikesek,altikho,altikulac,altilar,altili,altilia,altillac,altillo,altimarloch,altimarlock,altimir,altimirtsi,altimur,altin kopru,altin kosh,altin mazar,altin tokhmaq,altin-emel,altin-tepe,altina,altinahir,altinakar,altinay,altinbas,altinbasak,altinboga,altinbuga,altinbulak,altinca,altincag,altincanak,altincay,altincayir,altincevre,altincilar,altindag,altindagkoyu,altindamla,altindere,altinekin,altinelma,altinemek,altinevler,alting,altingedik,altingen,altingoroka,altinharabesi,altinhisar,altinho,altinhuseyin,altinji ihtiyat,altinkaya,altinkent,altinkilit,altinkum,altinkurek,altinkusak,altinlar,altinlar koyu,altinli,altinlu,altino,altinoba,altinolcek,altinoluk,altinopolis,altinova,altinova duden,altinova orta,altinova sinan,altinova yenigol,altinoz,altinozu,altinpinar,altinsac,altinsehir,altinsu,altintarla,altintas,altintas koyu,altintaskoyu,altintepe,altintop,altintoprak,altinusagi,altinuzum,altinyaka,altinyaprak,altinyayla,altinyazi,altinyunus,altinyurt,altinyuzuk,altiok,altiparmak,altiparmakli,altipiani di arcinazzo,altipinar,altipinar koyu,altirdning,altisheim,altishofen,altisogut,altisried,altitude,altiyol,altizer,altja,altjellingsdorf,altjessnitz,altjuhrden,altkalen,altkamp,altkatterbach,altkehdingen,altkelbunken,altkerschegg,altkettenhof,altkhyute,altkietz,altkirch,altkirchen,altkiwiten,altkloster,altklosterberg,altkoln,altkoslarn,altkothen,altkoy,altkoyu,altkrautheim,altkula,altkunkendorf,altkyula,altl,altlach,altland,altlandersdorf,altlandsberg,altlandsberg-sud,altlangerwisch,altlangsow,altlassing,altlautersee,altlay,altleiningen,altleis,altleisnig,altlendershagen,altlengbach,altlewin,altlichtenberg,altlichtenwarth,altliebe,altliebel,altlietzegoricker loose,altlihuayan,altlinster,altlobau,altlohbergerhutte,altlohberghutte,altlommatzsch,altludersdorf,altluneberg,altlunen,altlussheim,altmadewitz,altmae,altmamayeshti,altman,altmanns,altmannsberg,altmannsdorf,altmannsdorf an der traisen,altmannsgrun,altmannshausen,altmannshof,altmannshofen,altmannspeier,altmannsrot,altmannstadt,altmannstein,altmannstetten,altmannsweiler,altmanstein,altmar,altmark,altmarkt,altmatt,altmelon,altmerdingsen,altmersleben,altmets,altmirmiranli,altmirmiranly,altmittweida,altmoorhausen,altmorbitz,altmorschen,altmover,altmuenster,altmugeln,altmugl,altmuhl,altmuhldorf,altmuhlhausen,altmuhlmunster,altmullan,altmummen,altmunden,altmunster,altmunsterberg,altmuntal,altmyhl,altn-bulg,altnabrocky,altnagapple,altnagelberg,altnaharra,altnamachin,altnamackan,altnapaste,altnassau,altnau,altneudorf,altneuhaus,altneukoog,altneukoogsbuhr,altneuwirtshaus,altnuifra,altnurga,altnussberg,alto,alto abejorral,alto achacana,alto alegre,alto alegre dos parecis,alto algarrobo,alto allegre,alto almacen,alto amara,alto amatitan,alto amparo,alto amundarai,alto anahuac,alto anapati,alto angara,alto angeles,alto araguaia,alto ashaninga,alto atafona,alto azupizu,alto banca lucia,alto barandas,alto barinas,alto barranca,alto barrieno,alto baudo,alto bela alianca,alto bela vista,alto bello,alto belo,alto benfica,alto bergamo,alto bergano,alto beter,alto blanco,alto blascoa,alto bonito,alto boquete,alto boro,alto buenavista,alto buey pelado,alto caballero,alto cachimbo,alto calcado,alto caldas,alto calera,alto camboma,alto caminho da paz,alto caminho do anum,alto camiri,alto campo alegre,alto cana,alto cananeia,alto canoas,alto cantano,alto capilla,alto capim,alto capivari,alto caracuchal,alto caribe,alto cascavel,alto castillero,alto catambegi,alto catumbela,alto cauale,alto cauca,alto cebadilla,alto cedro,alto centro boqueron,alto chama,alto changane,alto chapare,alto chicapa,alto chikapa,alto chirireni,alto choen,alto chuare,alto cienaga,alto clarindero,alto coite,alto colorado,alto comanehi,alto concepcion,alto concordia,alto conejo,alto cordoba,alto corotu,alto corrales,alto coruma,alto cruz,alto cuanza,alto cuilo,alto cuito,alto cuso,alto cutia,alto da barreira,alto da betania,alto da boa vista,alto da cerca,alto da conceicao,alto da cruz,alto da doca,alto da gloria,alto da grota funda,alto da jurema,alto da maia,alto da manela,alto da nova,alto da pega,alto da piedade,alto da sapinha,alto da serra,alto da uniao,alto da varzea,alto da vela,alto da xitatamera,alto das pedras,alto das piabas,alto de ambrosio,alto de angosturas,alto de balbuena,alto de belleza,alto de cabrera,alto de cajas,alto de calarca,alto de callihue,alto de cana,alto de canafistola,alto de canazas,alto de canchas,alto de carrillo,alto de cascajal,alto de catecito,alto de chamacua,alto de chapi,alto de chiquero,alto de colmenares,alto de colorado,alto de comanche,alto de conga,alto de cruz,alto de cuatro bocas,alto de cubilla,alto de divala,alto de domingo mora,alto de espana,alto de flores,alto de gariche,alto de garzon,alto de guera,alto de isnarun,alto de jacu,alto de jahuel,alto de jesus,alto de jo,alto de julian,alto de la aguada,alto de la arena,alto de la china,alto de la corocita,alto de la cruz,alto de la cruz hacienda,alto de la damajagua,alto de la encantada,alto de la esquina,alto de la estancia,alto de la gloria,alto de la iguana,alto de la loma,alto de la loma oculta,alto de la luna,alto de la mesita,alto de la mina,alto de la montana,alto de la mora,alto de la palma,alto de la paloma,alto de la piedra,alto de la playa,alto de la posada,alto de la quebrada los guabos,alto de la quebrada manuel,alto de la represa,alto de la rosa,alto de la sierra,alto de la torre,alto de las cruces,alto de las huacas,alto de las lechuzas,alto de las mejias,alto de las mesas,alto de las padillas,alto de las palomas,alto de las piedras,alto de las sierras,alto de las yeguas,alto de limon,alto de lisboa,alto de los adobes,alto de los alzafraz,alto de los carrillo,alto de los castillo,alto de los castillos,alto de los chantos,alto de los copeyes,alto de los godos,alto de los gonzalez,alto de los machos,alto de los martinez,alto de los mates,alto de los mejias,alto de los mirandas,alto de los monos,alto de los more,alto de los negros,alto de los ortega,alto de los padillas,alto de los patios,alto de los patos,alto de los peregrines,alto de los peregrinos,alto de los pozos,alto de los ruices,alto de los zarzos,alto de majonal,alto de mamey,alto de manablanca,alto de martincito,alto de mayo,alto de medina,alto de mejico,alto de merecure,alto de miradorcito,alto de monte lirio,alto de mulatos,alto de nerquihue,alto de nino,alto de ojite,alto de ortega,alto de paez,alto de palo verde,alto de panama,alto de pedra,alto de pedro,alto de peguero,alto de piedra,alto de piedras,alto de pino,alto de quebrada piedra,alto de quira,alto de quitirriel,alto de qullaipe,alto de ramirez,alto de rancho,alto de reyes,alto de riecito,alto de rincon,alto de roque,alto de san juan,alto de san juanito,alto de san miguel,alto de san pedro,alto de santa helena,alto de santa maria,alto de santo antonio,alto de santo domingo,alto de sao antonio,alto de sao joao,alto de sierra,alto de tareira,alto de tebario,alto de tereira,alto de tigre,alto de tio diego,alto de tomon,alto de tranquilla,alto de tuna,alto de vigas,alto de villa nueva,alto de villalon,alto del aguacate,alto del arenal,alto del burro,alto del caimen,alto del caimito,alto del camaron,alto del canafistulo,alto del carmen,alto del castillo,alto del ciego,alto del cielo,alto del coral,alto del corotu,alto del corotuito,alto del corral,alto del cristo,alto del ebano,alto del ebanos,alto del espino,alto del espino prieto,alto del gallo,alto del guarumo,alto del higo,alto del horcon,alto del jobo,alto del jujucal,alto del llano,alto del lucero,alto del majonal,alto del mamey,alto de" mango,alto del manzanillo,alto del maranon,alto del mistol,alto del molino,alto del monte,alto del mora,alto del nance,alto del nanzal,alto del naranjal,alto del naranjo,alto del obispo,alto del ojite,alto del oso,alto del pelador,alto del ponton,alto del puerto,alto del quira,alto del rancho,alto del rey,alto del rio,alto del roble,alto del rodadero,alto del rosario,alto del salobre,alto del salvador,alto del sapo,alto del sitio,alto del tala,alto del tizar,alto del viento,alto del villalon,alto del yaque,alto divala,alto do amparo,alto do bagre,alto do banze,alto do bonfim,alto do casal,alto do catingueiro,alto do cobre,alto do coqueiro,alto do doca,alto do feto,alto do gloria,alto do maranhao,alto do matias,alto do meio,alto do melo,alto do monte,alto do mutabide,alto do peba,alto do rodrigues,alto do sabia,alto do sul,alto do velame,alto do vista,alto domingo,alto dondo,alto dos coelhos,alto dos gomes,alto dos pareiros,alto dos pereiras,alto douro,alto el basan,alto el cajon,alto el consuelo,alto el dajao,alto el morro,alto el nance,alto el oso,alto el pozo,alto el rayo,alto el rey,alto el roble,alto el salado,alto el saltadero,alto el sapo,alto el silencio,alto el sitio,alto el tigre,alto el toro,alto faxinal,alto fica,alto forero,alto formoso,alto fresco,alto frio,alto gachantiva,alto garcas,alto garcia,alto gavilan,alto genova,alto girardot,alto gordo,alto grande,alto grandview acres,alto gualaca,alto guamito,alto guandu,alto guaramito,alto guayabo,alto hama,alto harmony hall,alto hospicio,alto huarangal,alto ibala,alto ipaguazi,alto itauna,alto itaunas,alto itoupava,alto jahuel,alto jecopaco,alto jordan,alto juyamoca,alto kimiriki,alto kwilu,alto kwito,alto la aguada,alto la belleza,alto la campana,alto la canada,alto la compana,alto la concordia,alto la cuesta,alto la hilaria,alto la meseta,alto la piedra,alto la playa,alto la quiebra,alto la torre,alto la vera,alto la villa,alto la vina,alto lajeado,alto lajilla,alto lajitas,alto laran,alto las niguas,alto las parras,alto leblon,alto lifune,alto lighona,alto ligonha,alto lima,alto limpio,alto limpopo,alto lindo,alto lino,alto lisboa,alto loma hermtosa,alto longa,alto loro,alto los barbascos,alto los gomez,alto los miranda,alto los mirandas,alto losada,alto lucero,alto luegi,alto machay,alto madeira,alto maderero,alto madidi,alto mae a,alto mae b,alto majagua,alto maranhao,alto maranon,alto martin,alto martincito,alto mayo,alto merecure,alto mimaso,alto mina,alto mira,alto molocue,alto molokwe,alto monte,alto montilla,alto morales,alto mutabide,alto mutum preto,alto na paula,alto nacacual,alto nancito,alto nilahue,alto nova esperanca,alto nove,alto novo,alto pacae,alto pacuy,alto padre andre,alto paez,alto paijan,alto palena,alto palma,alto palmital,alto pance,alto paraguai,alto paraguai do diamantino,alto paraiso de goias,alto parana,alto parasol,alto parnaiba,alto pass,alto patacollo,alto patrao mor,alto pau dalho,alto pauji,alto pelado,alto pele el ojo,alto pena colorado,alto pencoso,alto perimbo,alto peru,alto piedra,alto piedra herrada,alto pimenta,alto pino,alto piquiri,alto pora,alto poro,alto porvenir,alto pozo,alto prado,alto puyeni,alto quatipuru,alto queiroga,alto quiel,alto quingola,alto quintana,alto quissembula,alto rafael,alto raimundo,alto ramirez,alto rancho,alto redondo,alto rey,alto ribeirao cambara,alto ribeirao da laje,alto rio bravo,alto rio doce,alto rio forcacao,alto rio krauel,alto rio mayo,alto rio novo,alto rio preto,alto rio scharlach,alto rio senguer,alto rio senguerr,alto rosario,alto salaito,alto san iidefonzo,alto san ildefonso,alto san juan,alto san pedro,alto san rafael,alto sano,alto santa helena,alto santa maria,alto santa maria do rio doce,alto santo,alto santo antonio,alto sao francisco,alto sao joao,alto sardo,alto seco,alto senon,alto sepacay,alto sereno,alto shikapa,alto sigui,alto socorro,alto somabeni,alto songo,alto soyatal,alto springs,alto suarez,alto sucuriu,alto talor,alto talpiaquer,alto tambo,alto tapialquer,alto tchicapa,alto tchiumbo,alto tierra negra,alto tincabeni,alto tulija,alto turgua,alto turi,alto ubero,alto uchire,alto uru,alto urubamba,alto uruguai,alto uruguay,alto valvanera,alto venecia,alto vera,alto verde,alto vere,alto viejo,alto viento,alto vigante,alto vilches,alto villaraso,alto villegas,alto vista,alto xarqueada,alto zaza,alto zotani,alto zurdo,alto-cuilo,alto-uama,altobar de la encomienda,altoberndorf,altobonito,altofing,altofonte,altofts,altoga,altols,altomira,altomiro,altomonte,altomunster,alton,alton albany,alton barnes,alton bay,alton brok,alton downs,alton emel,alton mazar,alton north,alton park,alton priors,alton station,alton-emel,altona,altonah,altonia,altoniskiai,altonovka,altontanya,altonville,altoona,altoona beach,altopascio,altoplano,altor,altora village,altorf,altoros,altorp,altos,altos aguacate,altos amarillos,altos casarapa,altos de amado,altos de campana,altos de castro,altos de chipion,altos de espave,altos de guera,altos de ibarra,altos de isidra,altos de jalisco,altos de los mates,altos de peguero,altos de quebrada seca,altos de san francisco,altos de san juan,altos de san miguel,altos de sevilla,altos de ventana,altos del mora,altos del moral,altos del olvido,altos del rosario,altos grande,altos herediana,altos pascua,altos verdes,altosano,altoschatz,altosillata,altossiach,altotonga,altotting,altou,altovalsol,altovia,altoviento,altovka,altovo,altozano,altpetritor,altpinar,altplatz,altpocher,altpoderschau,altpolla,altraga,altrama ain,altramah `ain,altrandsberg,altranft,altranstadt,altrashipovo,altratjensdorf,altreetz,altreichenau,altreisch,altreitgraben,altremda,altreute,altreuth,altrhede,altrich,altrier,altrik,altrincham,altringen,altringenberg,altrip,altrippe,altro,altroff,altroggenrahmede,altron,altrosenthal,altrottmannsdorf,altruppersdorf,altsak,altsarbyn,altsattel,altsberg,altschauerberg,altscheid,altscheidenbach,altscherbitz,altschermbeck,altscheuern,altschlaining,altschmiede,altschmiedelfeld,altschonau,altschweier,altschwendt,altseehagen,altsek,altselingsbach,altshausen,altsnappen,altsohl,altsorgefeld,altspielberg,altstadt,altstadt borna,altstadt waldenburg,altstadt-retz,altstadten,altstadter feldmark,altstadter garten,altstatten,altstein,altstett,altstetten,altsteusslingen,altstockach,altstrimmig,altsyn-khut,alttanes,alttann,alttechau,altthymen,alttiefenweg,alttitschein,alttornow,alttramm,alttrebbin,alttrier,altu,altubinal,altud,altuda,altue,altufa,altufyevskiy,altuis,altukhov,altukhova,altukhovka,altukhovo,altun,altun al jurd,altun chair,altun kopru,altun kubri,altun kupri,altun wayran,altun-e bala,altun-e sofla,altuna,altunakar,altunan,altunbasak,altuncular,altunemil,altunemil xiang,altunhisar,altunhuseyin,altunino,altunkosh,altunkusak,altunkush,altunoluk,altunoluk koyu,altunova,altuntas,altuntas koyu,altunusagi,altura,altura bata,altura di nesazio,altura matanda,altura nueva brema,altura south,alturas,alturas arroyo apolo,alturas de belen,alturas de bellavista,alturas de cubanacan,alturas de la coronela,alturas de la habana,alturas de la vibora,alturas de marbella,alturas de miramar,alturas de villa maria,alturas del san juan,alturas del vedado,alturas do barroso,alturas vibora,alturay,alture,alturti,altus,altusch,altusried,altuy,altuza,altuzarra,altvan,altvielreich,altvika,altviller,altvolberg,altwaidhofen,altwarmbuchen,altwarp,altwartenburg,altwasser,altweert,altweerterheide,altweg,altweichelau,altweichsel,altweidelbach,altweidling,altweier,altweiler,altweilnau,altweitra,altwied,altwiedermus,altwies,altwiesen,altwiesloch,altwigshagen,altwildungen,altwiller,altwistedt,altwittenbek,altwolfsdorf,altwriezen,altwustrow,alty-agatsch,alty-aul,alty-gul,alty-gul,alty-mirza-yurt,alty-murza-yurt,altyagach,altyapan,altyaryk,altyaul,altyayon,altybay,altygashevo,altygumbaz,altyguz,altykarasu,altykiben,altykol,altykui,altyn emel,altyn emye,altyn emyel,altyn-aryk,altyn-bulak,altyn-chekan,altyn-emelskaya,altyn-gorka,altyn-magar,altyn-say,altyn-tash,altyn-topkan,altyn-tyube,altynamach,altynarasan,altynay,altynbashak,altynbay,altynbulak,altynchy,altyndy,altynemel,altynkan,altynkul,altynmazar,altynnikov,altynnoye,altynnyy,altynov,altynovka,altynovo,altynsarin,altynsarino,altynsay,altyntash,altyntau,altyntepa,altyntobe,altynzhar,altyqarasu,altyre,altyrka,altyshevo,altyshevo-lyulskiy,altyugul,altzayanca,altziegenruck,altziller,altzingen,altzirkendorf,altzschillen,altzuchka,altzuckmantel,alu,alu abe,alu bena,alu bili glumpang,alu buaya,alu buna,alu chak,alu dheri,alu duyung,alu empuk,alu itam redep,alu jamadar,alu jat,alu ji veri,alu kalateh,alu ke sathiane,alu khan,alu khan kach,alu khel,alu khel `olya,alu khel sufla,alu kheyl,alu mahar,alu pahar,alu pinek,alu qal`eh,alu seumambu,alu shahdeo,alu siwah,alu yarqami,alu yarqomi,aluak,alu-anauk,alu-metskula,alu-metskyula,alu-on,alua,alua buna,alua chandipur,aluadjang,aluak,aluakluak,aluakra,alualu,aluat,aluatu,aluatul,alubaba,alubaren,alubati,alube,aluben,alubendiyaya,alubihid,alubihon,alubijid,alubinalipu,alubo,alubogolla,alubomulla,alubon,alubosa,alubowila,alubukit,alubungkuh,aluburo,alubushco,aluc,aluca,aluca bulaq,alucakham,alucapli,alucar,alucara,aluch,alucha,aluchah,aluchah bulaq,aluchah kham,aluchakham,aluchan,alucheh,alucheh bolaq,alucheh malek,alucheh molk,alucheh qeshlaq,alucheh-ye fuladlu,alucheh-ye sabalan,aluchehlu-ye `olya,aluchehlu-ye sofla,aluchin,aluclu,aluclubekiran,alucluseki,alucra,aluda,aludaid,aludapdap,aludar,alude,aludeman,aludeniya,aludewala,aludewali,aludo laghari,aludrien,aludum,aluduyung,alue,alue abet,alue abo,alue aki,alue ambang,alue angie,alue angin,alue anou barat,alue anou timur,alue awe,alue awee,alue badeuk,alue bagok,alue bahagia,alue baro,alue baroh,alue bata,alue batee,alue bateung brok,alue beliung,alue bili,alue brieung,alue bukit,alue bule,alue buloh,alue calong,alue capli,alue cekdoi,alue dama,alue dawah,alue drien,alue dua,alue dua paya galah,alue gajah,alue gandai,alue geudong,alue geunting,alue gluh,alue gro,alue gunto,alue hitam,alue ie mamet,alue ie puten,alue ie seuum,alue igeuh,alue itam dua,alue itam satu,alue jamak,alue jampa,alue jamuk,alue jang,alue jangat,alue jerjak,alue kambok,alue kejruen,alue keumuneng,alue keutapang,alue kiran,alue krueb,alue krut,alue kupula,alue kuyun,alue lam saba,alue lancakbungo,alue landong,alue le mirah,alue leuhob,alue me,alue meuraksa,alue meutuah,alue mie,alue mirah,alue nibong,alue on,alue padee,alue pande,alue pangkat,alue papien,alue parang,alue peunawa,alue peussaja,alue pisang,alue primping,alue punti,alue puntong,alue putih,alue rambee,alue rambot,alue rambung,alue raya,alue rayeuk,alue reulieng,alue rheng,alue sane,alue sejahtera,alue sejuk,alue sepe,alue serdang,alue seupeung,alue seutui,alue sijingkai,alue sundak,alue sungai pinang,alue tampak,alue tampu,alue tengoh,alue teputeh,alue tho,alue u,alue udeung,alue unoe,alue-itam,alue-puteh 1,alue-puteh 2,alue-puteh dua,alue-puteh satu,alue-raja,alueabai,alueabe,alueanu,aluebade,aluebadeuk,aluebat,aluebatee,aluebie,aluebieng,aluebili 1,aluebili satu,aluebilie,aluebilli,aluebleng,aluebreng,aluebu,aluebu jalan,aluebu tuha,aluebue,aluebuga,aluebugeng,aluebuging,aluebuh,aluebuloh 1,aluebuloh 2,aluebuloh dua,aluebuloh satu,aluecapli,aluedama,aluedjamok,aluedjampo,aluedjeumpa,aluedua,alueduamuka,alueduwa,aluegadeng,aluegampong,aluegarot,aluegintong,alueglumpang,alueheh molk,alueie-puteh,alueiemudek,alueincin,alueintjin,alueit,alueitam,alueitam 1,alueitam ii,aluejamok,aluejampo,aluejeumpa,aluekala,aluekeureunya,aluekreureunya,aluekruet,aluekumba,aluekumbang,aluekuyun,aluelancok,aluelantjok,aluele mirah,alueleseukum,aluelet,aluelhok,aluemangota,aluematangkumbang,aluemeueh,aluemeuih,aluemeureubo,aluemirah,aluenaga,aluenaleueng,aluenda,aluengom,alueniri,aluenirih,aluepadee,aluepaku,aluepanah,aluepandju,aluepanju,aluepapeun,alueparang,aluepeno,aluepineueng,aluepineung,aluepisang,alueputeh,alueputoih,alueraja,aluerajeuk,aluerambat,aluerambong,aluerambot,alueraya,aluerayek,aluerayeuk,aluerongan,alueru,aluesadat,aluesenngko,aluesentang,alueseumambu,alueseungat,alueseurdang,aluesiron,aluesungaipinang,aluet,alueteh,alueteuhob,alueteuhop,alueteungkurak,aluetho,aluetiti,aluetjanang,aluetjapli,aluetrieng,aluetrienggadeng,aluetuwi,alueu anau,alueubat,alueunoh,alueuputeh,alufa,alufasi,aluferovo,aluferyevo,alufinha,aluga,alugaai,alugak,alugale,alugalge,alugan,alugbin,aluge,alugi dheri,alugmaua,alugmawa,alugolla,alugon,alugoro,alugou,aluguditi,alugug,alugwaji,alugyi,aluhaluh,aluhasa,aluhusu,alui,aluichak,aluj,aluja,aluje,alujeh molk,alujo goth,alujula,aluk,alukachila goth,alukala,alukalawita,alukalay,alukan,alukanda,alukandeh,alukdia,alukenai,alukend,alukenu,aluketiya,aluketiyawa,alukhe,alukhel,alukheyl,alukheyl,aluki,alukino,aluko,alukoerhchinchi,alukozai,alukozi,alukpe,aluksne,alukta,aluku,alukuchoko,alukuk,alul,alula,alulak,alulayag,alulehob,alulhuk,aluli,alulim,alulin,alulod,alulovice,alulovici,alulu,alulud,alulwela,alum,alum bank,alum berawe,alum bridge,alum creek,alum ita,alum rock,alum springs,alum wells,aluma,alumadi,alumagbene,aluman,alumane,alumani,alumar,alumari,alumba,alumbango,alumbaro,alumbaugh,alumbrado,alumbre,alumbrera,alumbres,alumche,alume,alumenda,alumeta,alumeuting,alumi,alumiara,alumieira,alumim,alumine,aluminiom,aluminiumercbanyatelep,alumirir-isiokpo,alumkazo,alumma,alummot,alumnankro,alumny creek,alumo,alumoora,alumot,alumu,alumura,alumwell,alumy creek,alun,alun-alun,alun-aluncontong,aluna,aluna miana,aluna palla,aluna tola,alunabad,alunalun,alunalun barat,alunalun peuntas,alunalun wetan,alunani,alunany,alunas,alunciai,aluncio,alunda,alundava,alundi,alundzha,alunero,alung,alungane,alungba,alungbanua,alungeni,alungshan,alunguci,alungushi,aluni,alunia,alunis,alunish,alunisu,alunisu de jos,alunisu de sus,alunisul,alunisul de jos,alunisul de sus,alunitdag,alunite,alunjeh,alunji,alunkro,alunta,aluntayan,alunto,aluntos,aluntse,alunu,alunul,alunupan,aluodai,aluona,aluona i,aluotos,alupa,alupache,alupang,alupange,alupapeuen,alupat,alupatgala,alupay,alupaye,alupere,alupidian,alupihing,alupipeu,alupipey,alupka,alupo,alupola,alupota,alupota town,alupotagama,alupotawa,alupotawela,alupotdeniya,aluppang,aluppange,alupura,alur,alur bantayan,alur bujuk,alur buluh,alur canang,alur cantik,alur cempedak,alur cucur,alur dalam,alur dduamas,alur dua,alur duren,alur enampuluh,alur gadeng,alur gading,alur gajah,alur getah,alur hitam,alur jambu,alur kapal,alur latong,alur lhok,alur lintah,alur luddin satu,alur ludin dua,alur manis,alur mas,alur meranti,alur merbau,alur meurbo,alur nunang,alur pak ngah,alur pineng,alur pongsu,alur rongka,alur selalas,alur selebu,alur selelas,alur sentang,alur setar,alur slamet,alur suli,alur tani i,alur tani ii,alur temesu,alur tengah,alur udin,alur-baro,alur-pandjang,alur-panjang,alur-tjantik,aluraj,aluralam,aluralde,aluralim,aluran,aluran tengah,aluran timur,alurba,alurbaung,alurbill,alurbuging,alurbulu,alurbunder,alurca,alurca koyu,alurcanang,alurdua,alurduren,alurejo,alureumpeuet,alurevic,alurgading,alurgadung,alurgantung,alurgintong,alurgurip,alurhitam,aluri,aluriesfontein,alurisfontein,aluritam,alurjembatan,alurjongkong,alurkareueng,alurkaul,alurkemiri,alurlangsat,alurlebah,alurlemak,alurleukob,alurlhe,alurlhok,alurluddin 1,alurludin ii,alurmeranti,alurmerbau,alurngang,alurnyamuk,alurpenjang,alurpunggah,alurpunti,alurralde,alurrambut,alurrempun,alurrindang,alurrisung,alurselemak,alursentang,alursunda,alurtampa,alurtani satu,alurteh,alurtjanang,aluru,alus,alusaca,alusan,aluschta,alusekere,alusgerd,alush mahalleh,alushko,alushkovo,alushta,alusi,alusiis,alusikarawain,alusiman,alusitoe,alusiyan,alusjerd,aluskomu,aluskyla,alussijan,alustan,alustante,aluste,alustre,alut,alut diwulwewa,alut gantiriyagama,alut hammillewa,aluta,alutak,alutanoh,alutepola,alutgama,alutgama bogamuwa,alutgama bogomuwa,alutgama east,alutgama mahakumburegammedda,alutgama west,alutgamwidiya,alutgangoda,alutganthiriyagama,alutgoda,aluth wewa,aluthgama,aluthgamwidiya,aluthkade,aluthkurumulla,aluthwegedera,aluthwewa,aluti erin,alutkade tunmanhandiya,alutkadetenmanhandiya,alutkotelinda,alutnuwara,alutnuwara town,aluto,alutong,alutoya,alutpotagama,alutsalmulla,aluttanayangoda ihala,aluttanayangoda pahala,aluttarama,aluttewa,aluttota,alutwala,alutwatta,alutwegedara,alutwela,alutwelagama,alutwewa,aluuotiro,aluv,aluva,aluvere,aluvihare,aluvihare colony,aluw bateu,aluw jangat,aluw kumbang a,aluw lhok,aluwaketuwala,aluwal,aluwala,aluwank,aluwatkolemgo,aluwduamuka,aluwihare,aluwingei,aluyak,aluyan,aluyaz,aluyon,aluyun,aluz,aluzana,aluzaqaj,aluze,aluzhina,aluzhino,alva,alva qowl,alvacao,alvacenas,alvacoes do corgo,alvacoes do tanha,alvada,alvadia,alvadore,alvados,alvaern,alvah,alvahi,alvai,alvai north,alvai south,alvai west,alvaiade,alvaiazere,alvaj,alvajarvi,alvalade,alvaledes,alvalhado,alvalizar,alvan,alvan kamar,alvan kamar-e bala,alvan-dere,alvan-e eshareh,alvan-e nejat,alvanaq,alvanbey ciftligi,alvand,alvand kamar,alvand kamar olya,alvand qoli,alvand sagtan,alvand seketah,alvandabad,alvandi,alvaneu,alvaneu dorf,alvaneubad,alvang,alvangen,alvanitsa,alvaniyeh,alvankamar-e bala,alvanlar,alvanley,alvanovo,alvanq quli,alvantot,alvar,alvar ashaqi,alvar bozorg,alvar nunez cabeza de vaca,alvar pain,alvar sofla,alvar tirunagari,alvar yukhari,alvar-e `olya,alvar-e bala,alvar-e pain,alvar-e sofla,alvara,alvaradito,alvarado,alvarados,alvaraes,alvard,alvarde`i,alvardeh,alvare,alvared,alvareda,alvaredo,alvaredos,alvarehas,alvarejo,alvarelhos,alvarenga,alvares,alvares florence,alvares machado,alvarez,alvarez de la reforma,alvarez de toledo,alvarez jonte,alvari,alvarih,alvarim,alvarinc,alvarinhas,alvarinhos,alvarino,alvarlu,alvaro,alvaro de carvalho,alvaro diaz,alvaro galiza matos,alvaro melo,alvaro obrego,alvaro obregon,alvaro obregon dos,alvaro obregon sur,alvaro pene,alvaro reinoso,alvaroes,alvarroes,alvars,alvarsi,alvarsmala,alvas,alvaschein,alvasjo,alvatan,alvater corner,alvati,alvaton,alvatti,alvattila,alvaux,alvay,alvazan,alvbro,alvdal,alvdalen,alvdeback,alvear,alveaux,alveavila de la ribera,alvechurch,alvecia,alvedino,alvediston,alvedsjo,alvedsjo bodar,alvega,alvegi tanya,alveijar,alveim,alveite pequeno,alvejado,alveley,alvellos,alvelos,alvem,alven,alvena,alvendre,alventela,alventot,alver town,alvera,alveran,alverc,alverca,alverda,alverdissen,alvered,alveren,alveren koyu,alveringem,alveringhem,alvern,alverna,alvernia,alverno,alversdorf,alvershool,alverskirchen,alverslev,alverslosa,alverson,alverstoke,alverstone,alverstraum,alverstraumen,alverthorpe,alverton,alvery junction,alves,alves lima,alvescot,alvesen,alvesered,alveslohe,alvesrode,alvesse,alvesta,alvestad,alveston,alvestorp,alvetorp,alvettula,alvheim,alvhem,alvho,alvhyttan,alvi,alvia,alviaes,alvian,alviano,alvide,alvidino,alvidron,alvie,alvignac,alvignac-les-eaux,alvignanello,alvignano,alvijan,alvik,alviken,alvikstrask,alvim fabricio,alvimare,alvimbuc,alvimlandia,alvin,alvina,alvinandia,alvindia,alvinehgar,alvingham,alvington,alvinlandia,alvino,alvino vieira,alvinopolis,alviobeira,alvion,alvir,alvirabad,alviran,alvis,alvisia,alvisjo,alviso,alvisopoli,alvisquer,alvitas,alvite,alvites,alvito,alvito da beira,alvito de baixo,alvito de cima,alvkarhed,alvkarleby,alvkarleo,alvkarleo bruk,alvkarlhed,alvlosa,alvnas,alvnes,alvo,alvoco da serra,alvoco das varzeas,alvoeira,alvoen,alvoga,alvogas,alvon,alvonkyla,alvor,alvora,alvorada,alvorada do sul,alvorao,alvord,alvordton,alvorge,alvorninha,alvoy,alvrangel,alvras,alvros,alvsaker,alvsatern,alvsbo,alvsborg,alvsborgs fastning,alvsby,alvsbyn,alvsered,alvsjo,alvsjohyttan,alvsnas,alvsoden,alvsta,alvstorp,alvsund,alvurkurchi,alvwood,alwar,alwa,alwa qol,alwadiyat,alwai,alwala,alwali,alwalton,alwalwali,alwan,alwan al `abid,alwan sulaiman,alwand,alwand quli,alwani,alwaqol,alwaqowl,alwar,alwar al `abid,alwar city,alwar kili,alwar tirunagari,alwara,alwarani,alwardi khan,alwari,alwarkurichchi,alwarpet,alwas,alwatta,alwaye,alwaza,alwei,alwejh,alwel,alwernia,alwet,alwi,alwii,alwington,alwinton,alwis town,alwiya,alwiyah,alwo-esin,alworoceng,alworth,alwyns kop,alxa youqi,alxa zuoqi,alxan-curtskii,alxan-otar,alxanli,alxasli,alxasoba,alxing,aly,aly-bayramly,aly-bazata,aly-kel,aly-kyuyel,alya,alya fulani,alya hausawa,alyabad,alyabino,alyablevo,alyabyeva,alyabyevo,alyabyevskiy,alyachapan,alyagach,alyagi,alyagysh,alyakchi,alyakchi,alyakhel,alyakheyl,alyakhov,alyaki,alyaklu,alyaksandrova,alyali,alyam,alyam-beyly,alyamdy,alyame,alyamese,alyamli,alyamly,alyan,alyanak,alyandor,alyangula,alyanli,alyanoye,alyanwali,alyar,alyaran,alyari,alyas,alyashevo,alyaskitovyy,alyasomme,alyat,alyat-pristan,alyatki,alyaty,alyaty-pristan,alyavi,alyawah,alyayevo,alyayo,alybay,alybayevskiy,alybeyli,alybeyli pervyye,alybeyli vtoryye,alych,alychkyshlak,alydzhan,alydzhanly,alydzhanly pervoye,alydzhanly pervyy,alydzhanly vtoroy,alydzhanly vtoroye,alydzhkyshlakh,alyeksandrovskoe,alyeska,alyfanta,alygdzher,alygjer,alyhanya,alyjan,alyk,alykel,alykend,alykeykhaly,alykhanly,alyki,alymka,alymkasy,alymov-lyubimovskiy,alymova,alymovka,alymovo,alymskoye,alyndzhaly,alyne,alyp,alys-khaya,alys-khayo,alysafi,alysardaakh,alysardak,alysardakh,alysardokh,alysh,alyshanly,alyshar,alyshay,alyshev,alyskol,alyssus,alysy-garakh,alytaus,alytay-bataga,alyth,alytus,alyukozu,alyunenskaya,alyunikha,alyunino,alyushagy,alyushino,alyutovo,alyy-kyuyel,alyya-kyuyel,alza,alzada,alzaga,alzagate,alzalan,alzamay,alzamayskoye,alzana fandou,alzani,alzano,alzano di sopra,alzano lombardo,alzano maggiore,alzano sopra,alzar,alzatate,alzate,alzate con linduno,alzateu,alzayacan,alzayanca,alzbetin,alzemont,alzen,alzenau,alzenau in unterfranken,alzenay,alzenbach,alzenberg,alzenova,alzesberg,alzetka,alzetto,alzetu,alzey,alzezere,alzgern,alzhausen,alzheim,alzhibay,alzhikhovo,alzi,alzin,alzing,alzingen,alzo,alzo de abajo,alzo de arriba,alzobey,alzola,alzon,alzonne,alzorriz,alzusta,alzy-kaloy,am,am ibri,am ildidiya,am `arshah,am `arshan,am `asalah,am `aslah,am `ayli,am `ayn,am `inab,am `umayshah,am `uzi\302\247aym,am abile,am adat,am adesberg,am adhaq,am aktane,am al barka,am algrous,am amma,am antimoun,am arba,am arhar,am arhar al omar am djoubay,am arhar am amouna,am arhar atalbe,am arhar dalou,am arhar iesey,am arouma,am aschenberg,am asha,am assala,am assinine,am atafa,am bach,am bachakouche,am bache,am badai,am baday,am badeche,am badie,am bahiyah,am bahnhof,am bahnhof kieritzsch,am bahnhof korbetha,am bar,am baram dadjo,am baram garaday,am bartouk doungous,am bassakane,am bassana,am batchay,am batr,am bayadah,am bazatna,am beldi,am belito,am beren,am berg,am berge,am bielenberge,am biringuel,am birkenvenn,am bishshah,am bitek,am bitin goz,am bitin tine,am boati,am boatif,am bolile,am bololi,am botoro,am bouati,am bougouma,am bougouna,am boung,am bourlouloum,am bourougn,am bourougne,am bourtouk bandar,am bourtouk doungous,am bourtounou,am bouza,am brahim,am bruch,am buhl,am busch,am busti,am buwaib,am buwayb,am chalakh,am chaloba,am chara,am charamit,am charma,am charme,am chechat,am chechiat,am chedire,am cherar,am cherma,am chibey,am chide,am chidede,am chimele dougoum,am chioka,am ciouf,am dabang,am dachar,am dacour,am daf,am dafani,am dafok,am daga,am dagachi,am dagagna,am dagel,am dagergeir,am dagig,am dagik,am dakhla,am dalam,am dalaoua,am dam,am dam toulouke,am dam zerbia,am dam zeriba,am damalik,am damania,am damm,am dammsteg,am darangal,am darb,am daribah,am darid,am darrweg,am dayq,am debip,am deich,am deleb,am deonas,am derabay,am dereba,am deresa,am derese,am derias,am dha`nub,am dhaibiyah,am dichi,am diema,am digemat,am dijarah,am diofour,am djaber,am djallat,am djan,am djan kaferdout,am djebora,am djeboura,am djedaouit,am djederi,am djelat,am djelfe,am djella,am djellat,am djemena,am djenaba,am djer,am djer morfein,am djer taama,am djidad,am djimena,am djimeze,am djiouekhin,am djodour,am djoudoula,am djoufour,am djourar,am djourar ganbir,am djourar mouro,am djourar moustakhide,am dobak,am dokone,am doma,am donnerberg,am doron,am douadi,am dourban,am dourbane,am douroun,am dourtoule,am dout goz,am doutile,am dringal,am dubbah,am dulmen,am dura,am durad,am durayb,am el barka,am eriz,am es sakin,am fajara,am fajarah,am far`ah,am fara,am fara`,am farao,am farsh,am faysh,am feuersang,am firsh,am fladder,am fraou,am freribe,am fruhlingsberg,am fuhsekanal,am furayd,am gafal,am gala,am galaka,am ganabol,am gantoura,am gar,am garada,am gardiana,am garkoy doungous,am garouendi,am gedaouin,am gereda,am gericht,am geritie,am ghawl,am gifel,am gore,am gore saker,am gou,am gouchie,am goudja,am goudou,am goulma,am goulma amberkotio,am goulma atomat,am goulma derbb,am goulma derbo,am goulma techer,am gouri,am gouson,am goz,am grita,am griti,am grossen heerweg,am guer,am guereda,am ha,am ha diem,am habali,am habar,am habil,am habile,am habile dia,am habl,am hadi,am hadidah,am hadjer,am hagen,am hajajah,am hajar,am hajar al `ulaya,am hajar al `ulya,am hajilah,am hajriyah,am hamra,am hamra,am hanashah,am haraz,am haraze,am hari,am hasak,am hasen,am hasif,am hataba,am hawi,am hayda,am heidland,am herize,am hidan,am hido,am hijda,am hilgenstocke,am himede,am hisn,am hohberg,am hufayrah,am hujairah,am hujayrah,am humayshah,am husn,am idan,am ildidrya,am iteb,am ja`liyah,am jabu,am jagerstieg,am jahilah,am jahsha,am jarba,am jarba,am jarbah,am jarbub,am jaruba,am jarubah,am jawl,am jayizah,am jenadiya,am jiblah,am jinh,am jiryah,am juhar,am kaalif,am kabasna,am kachao,am kachaoua,am kachaway,am kachouay,am kachoum,am kadah,am kadamol,am karaba,am karakir,am karibe,am karif,am karouba,am karoubay,am karouma,am kasaye,am kashab,am katzelbach,am katzenberg,am kawrah,am kharabiyah,am kharouba,am khayalah,am khayyalah,am khoumi,am khudayrah,am khudera,am khulaq,am kifeou,am kiffou,am knickebrink,am ko,am ko dagech,am koabib,am kofe,am kora,am kornoi,am kornoy,am kouakib,am kouchou,am kouchouk,am kouil,am kouk,am kourbal,am kouyouk,am koye,am krahnocken,am kreuz,am kronawett,am kuh,am kustenkanal,am ladoba,am lagadip doungous,am lagadip naga,am laib,am lamena,am laqit,am lasbah,am lasbah al `ulya,am lasbah as sufla,am leiouna,am lemena,am leyouna,am lohberg,am loubia,am maati,am madarah,am madinah,am madmanah,am madrukah,am mahafhaf,am mahathith,am majba,am majbah,am majil,am majin,am majlabah,am makhnaq,am malak,am malaye,am malbiyah,am mansab,am maqbabah,am mas`am,am maslab,am maya,am midfanah,am mijann,am mijdah,am mijza`,am mimriyah,am minah,am minbiya,am minsab,am mirsas,am misgav,am mishqalah,am mishrah,am mujahhiz,am mujaylah,am mukail,am mukarib,am mukayl,am mukhnuq,am muqarib,am musharej,am mushayrat,am na`bub,am nabak,am nabia,am nabiya,am nabiyah,am nahr,am nasbah,am nawbah,am neuenfeldsdeich,am niedouan,am nielim,am nijdah,am nolsen,am nouren,am nujayfah,am ouch,am ouchar,am ouled,am ourel,am ourouk,am papenberge,am pippinge,am purwa,am pyhrnerhof,am qaa,am qa`,am qa`idah,am qafiriyah,am qaid,am qanah,am qanaah,am qanaila,am qaryah,am qasawwarah,am qashbib,am qawl,am qaws,am qawz,am qayf,am qaz,am qifiriyah,am qulaytah,am rachim,am rahabah,am rain,am rasas,am rawnah,am raya,am reichswald,am retio,am rhibeche,am rhirebi,am rhota,am rie,am rigek,am rija`,am rimele,am robat,am roten berg,am rukayzi,am ruppiner kanal,am saada,am sa`id,am sak,am sakin,am sakine,am sala`ah,am salab,am salalah,am salam,am salamiyah,am salimah,am salul,am samn,am saterna,am sawad,am sawma`ah,am sayala,am sayala alifa,am sayala haddad,am sayala kaksoro,am sayala migedj,am schafstall,am scheuberg,am schimmel,am schlunder,am schuren,am schwarzen busch,am seajal,am see,am seife,am selep,am senena,am senene,am senete,am senete karaga,am seri,am sha`ah,am sha`ibi,am sha`ra,am sha`rah,am shaaibi,am sharaf,am sharfa,am shat,am shatt,am shidyan,am shifah,am shubaili,am shubayli,am shubran,am shudayf,am sidere,am sidrah,am sidre,am sigan,am sigane,am siget,am siheb,am silabah,am sinete,am siteb,am sitep,am sittardsberg,am sonnenberg,am souassil,am sounta,am souta,am steig,am stein,am steindamm,am stuten,am subaybah,am subayhi,am sulzbichl,am surayh,am surr,am surrah,am suwayda,am suyiq,am tabarik,am tabarit,am tachok,am tachouka,am taffah,am tahamiyah,am talako,am talate,am talhaq,am tallah,am tanabo,am tapou,am tarabah,am targay,am tawer,am tchake,am tchakena,am tchilili,am tchokoro,am teiritz,am th,am thom,am thonberg,am thuong,am timan,am timane,am times,am timmane,am titisee,am tohamiya,am tolok,am tourine,am triebweg,am tubayh,am tubeih,am tuffah,am turba,am turm,am vogel,am waldgrund,am warka,am warkah,am weinberg,am wuday`,am wuthia,am yoma,am zaafaia,am zarfai,am zarga,am zayfa,am zeefaia,am zerega,am zet ali,am zet zaraoua,am ziefa,am ziefe,am ziegelwerk,amaich,amaych,am- `idhaq,am-am,am-beth acres,am-buhaid,am-daiq,am-dakhlah,am-firsh,am-hadu,am-haidha,am-hiqaira,am-khabb,am-mishal,am-misqalah,am-muhafhaf,am-nuqaq,am-qaus,am-qulaitah,am-rahaba,am-rihabah,am-sal`ah,am-sha`ra,am-surr,am-zoer,ama,ama ahele,ama aji,ama akama,ama akoto,ama anyam,ama apu,ama apu ife,ama cancha,ama eke,ama ekpu,ama igbo,ama iyi,ama izi,ama keng,ama keng village,ama khel,ama khunah,ama koulio,ama koyra,ama ogbu,ama ogugu,ama oji,ama okayi,ama okpu,ama oku,ama seidi,ama shaba ki ban,ama toudji,ama uzu,ama yusefo,amaama,ama-achara,ama-achi,ama-alibi,ama-awji,ama-eke,ama-ekpu,ama-ekut,ama-friday,ama-inyi,ama-miller,ama-nekayo-dobolo,ama-ngwu,ama-oji,ama-okwe,ama-renner,ama-sunday,ama-uke,ama-ukwu,ama-uru,amaadhala,amaajiz,amaama,amaanga,amaaqip,amaariki,amaasu,amaatimin,amaaza,amaba,amaba ime,amaba-ubibia,amabaan,amabad,amabakumbu,amabanba,amabanta,amabasere,amabasyakhali,amabebegbene,amabel,amabera,amabia-ala,amabia-elu,amabidi,amabila,amabili,amabilo,amabilou,amabiriba,amable maria,amabo,amabrokrom,amabu,amabula,amabulo,amacagi,amacahuite,amacalam,amacalan,amaceri,amaceyes,amaceyes abajo,amaceyes arriba,amach,amacha,amachang,amachani,amachara,amachel,amachi,amachina,amachkasy,amachkino,amachkoutel,amachuma,amachuma grande,amachumama,amachumani,amachuta,amacia,amaclar,amaclle,amacocha,amacoite abajo,amacoite segunda seccion,amacoite tercera seccion,amacpecan,amacuable,amacuahiula,amacuahuitl,amacuapa,amacuatitla,amacuatitlan,amacuautitlanejo,amacueca,amacuku,amaculi,amaculi ranch,amacuzac,amad,amad chah,amad chinah,amad cina,amad kalay,amad kelay,amad shah kali,amada,amada gaza,amada-gaza,amadabo,amadan,amadanom selatan,amadanom tengah,amadarapitiya,amadare,amadareishi,amadarh,amadavad,amadbala,amadchina,amade,amade bai,amade-karcsa,amadebegbene,amadel nloufsmine,amadene,amadene foukam,amadeo,amadeovske korcany,amades,amadeu,amadeu amaral,amadhaes,amadhes,amadhiaes,amadhies,amadi,amadi fakourou,amadi flats,amadi khel,amadi oumourou,amadi ounare,amadia,amadiara,amadilimane,amadim olo,amadim-olo,amadin,amadine,amadir,amadiyeh,amadji,amadjikakope,amadjoura,amadjuak,amado,amado bahia,amado baia,amado gomez,amado grande,amado nervo,amadochacra,amadochacra hacienda,amadon,amadon hernandez,amador,amador city,amador hernandez,amadora,amadore,amadores,amados,amadou,amadou ali were,amadou bafinnka,amadou doso ware,amadou mama,amadou moussa,amadou nabata,amadou namoun,amadou ndiaye,amadou sangare,amadoubougou,amadoukchi,amadoukro,amadpur,amadr,amadra,amadroase,amadroasi,amadu,amadu kulu,amadugama,amadun,amaduri,amadyez,amadyk,amadzi,amadzikope,amadzili,amaebe,amaebu ogwa,amaeka iza,amaeke,amaeke abam,amaekwulu,amaeshi,amaetiti,amaeze,amafandry,amafi,amafiamavo,amafie,amafor,amaforovo,amafune,amag,amaga,amagalangiin jisa hiid,amagalanta hiid,amagalantuy,amagamari,amagamiento,amagan,amagano,amagansett,amagaon,amagasaki,amagase,amagay,amagaz,amagbagan,amagbo,amagdoul,amage,amagede,amager faelled,amagerbro,amagha,amaghdourene,amaghleba,amaghli,amaghousse,amaghu,amagi,amagleba,amagmach,amagne,amagney,amago-ebenebe,amagoa,amagohan,amagon,amagoro,amagoua,amagour,amagoya,amagri,amagu,amagu anyim,amaguana,amaguanyim,amaguaya,amague bedia,amagueur,amagunze,amagusan,amaguwa,amah,amahai,amahali,amahan,amahani,amahei,amahia,amahit,amahitai,amaho,amahoesoe,amaholoe,amaholu,amahop 1,amahop 2,amahor,amahor southwest,amahunshe,amahusu,amai,amai-nge,amaia,amaibo,amaiceo,amaich,amaicha,amaicha del valle,amaichia,amaide,amaidi,amaier,amaigbo,amaigur,amaiin usanii hiid,amaiin-usanii-hiyd,amaiiz,amaikuli,amailaca,amailayo,amaile,amailla,amailloux,amaime,amaimo,amaimon,amain usunoy khid,amain usunyy khid,amainja,amaipukur,amaiquiro,amair,amairaba,amairi,amairial,amaita,amaitaniha,amaitem,amaiteo,amaitlyakh,amaitsotady,amaiyapur,amaiyi,amaiyufa,amaizama pata,amajac,amajacatlan,amajahua,amajaque,amajaquillo,amajatlan,amajgag,amaji,amajic,amajlije,amajo,amajola,amak,amaka,amakabougou,amakalakala,amakalan,amakalu,amakange,amakchoud,amake,amakhton,amaki,amaki number 1,amaki number 2,amaki sara,amakibubu,amakimahana,amakinskiy,amakka,amakkol,amakoeni,amakofia,amakofikrom,amakohia,amakoi,amakokrom,amakona,amakonzi,amakoroma,amakot,amakouladji,amakpa,amakpave,amakpo,amakro,amakrom,amaksong,amaku,amakuliya,amakura,amakwadikope,amal,amal goth,amal khel,amala,amala umueze,amalaboua,amalabu,amalaf,amalafede,amalal,amalaman,amalango,amalani,amalao,amalapuram,amalaye,amalbalan,amalda,amaldar,amaldu,amale,amaler,amales,amalez,amalfi,amalga,amalget,amalgoss,amali,amalia,amalia lopez,amalia moncada,amaliada,amaliai,amaliapoleos,amaliapoli,amaliapuszta,amalias,amaliatelep,amalie,amaliendorf,amalienfelde,amalienhof,amalienruh,amalienruhe,amalienstein,amaliesmuyzha,amalija,amalijas,amalijasmuiza,amalinda,amaling,amalio,amaliota,amaliu,amalive,amaliya,amaliyyasmuyzha,amaliz,amalka,amalkot,amalner,amalo,amalon,amalota,amalou,amalou n ou kriss,amalou nsif,amalou ougris,amaloy,amalpada,amalsar,amalte,amalte-makhi,amaltepec,amalu,amalue,amaluia,amalusa,amalutu,amaluza,amalviskiai,amalwasi,amalyai,amalykovo,amam,amam as sabmah,amam kheyl,amama,amamahana,amamaka,amamaloya,amaman wandiwala,amamapare,amamara toudou,amamaros,amamarus,amamba,amambago,amambahag,amambai,amambay,amambayka,amambera,amamboajo,amamforo,amamia,amamini,amamkhel,amamkut,amamlo,amamlu,amamole,amamoli,amamonta,amamoor,amamora,amamoso,amamou,amamouhu,amampacang,amamputu-uli,amamra,amamrane,amamth,amamui,amamula,amamum,amamure village,amamuri village,aman,aman allah,aman damai,aman derai,aman galu tappeh,aman gul kili,aman hajji,aman hilir,aman hulu,aman kayram,aman khajeh,aman khan,aman khaneh,aman khel,aman khujeh,aman khvajeh,aman khvojeh,aman kofikro,aman kot,aman kowt,aman kut,aman lari,aman margan,aman mela,aman mohammad,aman purwa,aman qarajeh,aman qarjeh,aman qol,aman raya,aman salekro,aman shah,aman shah colony,aman shah kili,aman tamghara,aman us,aman yapi,aman-ail,aman-kasy,aman-kolkhat,aman-kutan,aman-oshtorma,aman-pokoukro,aman-semiz,aman-tepue,aman-tepui,aman-turdy,aman-tyube,aman-utkul,amana,amana abajo,amana del tamarindo,amana wesi,amanab,amanabad,amanadoria,amanadskoye,amanagbene,amanagulakiri,amanagwu,amanah,amanaiara,amanaju,amanak,amanakpo,amanakro,amanal,amanalco de becerra,amanalf,amanallah,amanambakkam,amanamby,amanamulla,amanao,amanar,amanari,amanas,amanase,amanasi,amanasis,amanat,amanat baba,amanat charoem,amanata,amanatbaba,amanatgarh,amanati,amanatoulaye,amanatpur,amanau,amanauo,amanave,amanaven,amanavil,amanawa,amanawala,amanbar,amanbay,amanbayevo,amanbodi,amanca,amancaes,amancakopektasi koyu,amancakopektasikoy,amancaya,amancayes,amance,amancey,amancha shalash,amancha-shalach,amancherla,amanchia,amanchim,amancho,amancio,amancio bento,amancio rodriguez,amancoro,amancosiling,amancosiling sur,amancy,amand,amand-e tazeh kand,amanda,amanda hills,amanda park,amandaba,amandamai,amandamaiblok dua,amandamaiblok i,amandamaiblok ii,amandamaiblok iii,amandamaiblok satu,amandamaiblok tiga,amandan,amandangay,amandara,amandaraja,amandarra,amandas,amandaville,amandayehan,amande,amandelbult,amandi,amandi afoue,amandi khelwala,amandiego,amandier,amandjuju,amando,amandola,amandoluwa,amandor,amandraja,amandraya,amanduba,amandusdorf,amandy,amandyk,amandyq,amane,amane ait oussa,amane ilila,amane melloul,amane melloulene,amane mellouln,amane n oulili,amane oukidet,amane oulli,amane tamghra,amane tamkha,amane tamrha,amane tamrhra,amane tazert,amaneh,amanekro,amanendraka,amanening,amanetu-mane,amaneyevo,amanfinaso,amanfo,amanfopon,amanfor,amanforo,amanfoso,amanfro,amanfrom,amanfru town,amanfrum,amanfu,amanfukrom,amanful,amanful number 2,amanfupon,amanfupong,amang,amanga,amangah,amangal,amangam,amangamzine,amanganda,amanganj,amangarh,amangba,amangbangan,amangbara,amangbo,amange,amangeldinskiy,amangeldinskoye,amangeldy,amangeldi,amangeldin,amangeldy,amangfrom,amangildino,amangni,amangomassi,amangouakro,amangouakrou,amangowl,amangulakiri,amangwu,amanhece,amanhia,amani,amani houchou,amani kola,amani lakshmipur,amani lilla,amani menou,amania,amanie,amaniganj,amanika,amanikaj,amanikanj,amanikoti,amanikro,amanimalco,amanin,amanina,amaninalco,amanini,amanino,amaniokpe,amaniou,amanita,amaniu,amaniutuba,amaniyeh,amaniyeh-e kuchek,amaniyeh-ye kuchak,amanj,amanja,amankalu,amankand,amankaragay,amanka\302\277l,amankeldi,amankeldy,amankend,amankhel,amankhera,amankheyl,amankheyl,amankhune,amanko,amankobe,amankokro,amankol,amankol,amankot,amankro,amankrom,amanku,amankul,amankwa,amankwakrom,amankwo,amankwor oye,amankwu,amankyia,amanlapay,amanli,amanlis,amanloq,amanly,amanmbohwe,amanmehwe,amanmewhe,amannazar,amannullakhan kalay,amanoa,amanoaoac,amanogo,amanogocho,amanoken,amanokeri,amanokrom,amanokurom,amanollah kalay,amanollah kariz,amanollah khan kalay,amanollah kur,amanollahabad,amanome,amanotkel,amanotkel,amanoukro,amanouz,amanovo,amanparan,amanpe,amanperez,amanporo,amanprobi,amanpur,amanpur dhok,amanpura,amanqaraghay,amanqol,amanrang,amanrang 2,amanrang dua,amansa,amansa guapos,amansabina,amansabino,amansagatos,amansari,amansec,amansee,amanseri,amansha,amansha-kapan,amansi jachi,amansic,amansim,amansiodo,amant,amanta,amantago,amantana,amantani,amantau,amantayskaya,amantea,amantem nkwanta,amantema,amanten,amanten dada,amantena,amantepuy,amantha,amantible,amantin,amantin dada,amantla,amantlyakh,amantobe,amantoc,amantogay,amantoghay,amantokel,amantra,amantran,amanty,amanu,amanuel,amanuense,amanuke,amanula-karez,amanulla-kalay,amanullah,amanullah baloch,amanullah dahri,amanullah kalay,amanullah karez,amanullah katiar,amanullah kelay,amanullah khan kelay,amanullah kor,amanullahpur,amanullapur,amanumbus,amanumingon,amanury,amanutkol,amanuwala,amanvi,amanviller,amanvillers,amanwala,amanwusu,amany,amanya,amanzai,amanzana,amanze,amanzey,amanzhol,amanzi,amanzimtoti,amao,amaofu,amaogudu,amaoji lodu,amaokai,amaoke,amaokwe,amaokwe-elu,amaos,amaoude,amaowele,amaozara,amapa,amapa nuevo,amapal,amapala,amapalita,amapalito,amapan,amapari,amapaya,amape,amapita,amapo,amapoeka,amapola,amapolas,amapolas de rio,amapora,amapu,amapuka,amapun,amapunta,amaqan,amaquilco,amar,amar ain henna,amar al jawf,amar ben zaoui,amar buyantayn huryee,amar cocche,amar colony,amar din,amar djedid,amar dogo,amar ed dine,amar gedid,amar hiyd,amar ka nagla,amar kakol,amar khan,amar khan kala,amar khana,amar khel,amar kheyl,amar koche,amar kot,amar lahsen,amar lahsene,amar qal`a,amar reik,amar rouihane,amar sidhu,amar singh,amar singhapur,amar singhwala,amar teimount,amar teimunt,amar torki,amar turki,amar-buyantayn-huree,amar-e mianrud,amara,amara ben ammou,amara ben sliman,amara gudo,amara noua,amara veche,amarabe,amarabu,amarae,amaragi,amaragi dagua,amaragoungou,amaragy,amarah,amaraika,amaraj,amaraji,amaraji dagua,amarak,amaraka,amarakarakkuru,amarakonegama,amarakongama,amarakonmulla,amarakoonmulla,amarakpur,amarakro,amaral,amaral ferrador,amaralina,amarambedu,amaran,amaranda,amarando,amarandon,amarandos,amarang,amaranguiles,amarani,amaranka,amaranta,amarante,amarante do grajau,amarante do maranhao,amaranth,amarantina,amarantinho,amaranto,amaranton,amarantos,amarao,amarapur,amarapura,amarapura hmandan,amarapuram,amarara,amarari,amaras,amarasti,amarasti-de-jos,amarastii de jos,amarastii de sus,amarastii-dos,amarastii-fata,amarat,amarateca,amarateh-ye `olya,amarateh-ye sofla,amarati,amaratungama,amarau,amaravathi,amaravati,amaravay,amarawa,amaraya,amarayah,amarchinta,amarcos,amarda,amardaha,amardalay,amardi,amardin,amarduiyeh,amare,amareh,amareins,amareiti,amarej,amarekoulela,amarela,amarelao,amareleja,amarelhe,amarelo,amarelo ferrado,amarelos,amarens,amares,amaresa,amaret es souaida,amarete,amaretta,amarevi,amarga,amargad,amargah-e baqa,amargarh,amarget,amargeti,amarghan,amarghun,amargiers,amargo,amargol,amargos,amargosa,amargosa valley,amargoso,amargot,amarh juwwaniyah,amarha,amarhsine,amari,amaria,amariana,amarianon,amarianous,amaric,amarighen,amarika,amarilis,amarilla,amarillas,amarillo,amarillo pericote,amarinthos,amarion,amarita,amarito,amarivayal,amariya,amarkan,amarkantak,amarkend,amarket,amarketi,amarkhel,amarkhessine,amarkheyl,amarkheyl,amarkhsine,amarko,amarkot,amarlar,amarli,amarli gala,amarliya,amarliyah,amarna,amarnath,amaro,amaro goncalves,amaro leite,amaro ribeiro,amaro tempo,amaroa,amaronan,amaroni,amaroo,amaropolis,amarosseldabay,amarot,amarouba,amarouchene,amaroucho,amarousion,amarpatan,amarpur,amarpur jorasi,amarpura,amarra,amarracao,amarradero,amars,amarsar,amarsinhapur,amarstas,amarta,amarti,amartita,amartuk,amaru,amarube,amarul,amarume,amaruran,amaruru ikeduru,amaruyoc,amaruyocc,amarwa,amarwala,amarwara,amarwasi,amarwatoe,amarwatu,amaryawa,amarzabad,amarzgane,amas,amasa,amasadek,amasala,amasama,amasaman,amasang,amasangan,amasangeng,amasangkar,amasau,amasbank,amascala,amascara,amase,amasei,amaseno,amaseri,amasgala,amasha kapan,amasha-kapin,amashara,amashca,amasi,amasia,amasindrenie,amasingbene,amasion,amasirurap,amasiya,amasiyah,amaskar,amasne,amaso-nta,amasodt,amasoko,amasowomwan,amasra,amasreh,amassakope,amassama,amassangan,amassangan 1,amassangan 2,amassangan dua,amassangan satu,amassangeng,amassaral,amasse,amassegg,amassen,amassene,amassera,amassine,amassine igourramene,amassine khezana,amassoma,amassuma,amastasyevka,amasti,amastjan,amastra,amastris,amasu,amasua,amasunga,amasungi,amasuoma,amasya,amat,amat ba,amat ouwarou,amata,amata-fagbene,amatagwolo,amatahago,amataima-ta,amataima-te,amatal,amatamuno,amatan,amatangoolo,amatari,amatata,amataura,amatava,amate,amate amarillo,amate blanco,amate de taltapanca,amate la hoja,amate redondo,amate-enu,amatebo,amatecampo,amatefegbene,amatekope,amatenango,amatenango de la frontera,amatenango del valle,amatende,amatengo,amatepe,amatepec,amatepeque,amatere,amates,amates grandes,amatetel,amatetoukope,amatevic mahala,amatgyigon,amathay,amathay-vesigneux,amathes,amati,amati shibpur,amati sibpur,amatia,amatikoua,amatikulu,amatilla,amatillo,amatiopa,amatita,amatitan,amatitan abajo,amatitan arriba,amatitan viejo,amatitanejo,amatitla,amatitlan,amatitlan arriba,amatitlan de azueta,amatkan,amatkur,amatlan,amatlan de canas,amatlan de jora,amatlan de los reyes,amatlan tuxpan,amatlan-cosamaloapan,amatlantepetl,amatlar,amatlichan,amatlipac,amato,amatogay,amatolo,amatolo camp i,amatolo camp ii,amatolo camp iii,amatolo camp iv,amatomisankantsaha,amatong,amatongas,amatonyegbene,amatos,amatos de alba,amatovci,amatovon,amatriain,amatrice,amatshilungu,amatsia,amatso,amatsu,amatsya,amatt,amatta,amatto,amatu,amatu-ijaw,amatu-ilaje,amatugan,amatui,amatulla,amatur,amatura,amaturer tenk,amatus,amau,amaua,amauaca,amauari,amauca,amaudara,amauidje,amaukkon,amaukwe,amaulang,amauli,amaumara,amaun,amaurang,amaurica,amauro,amauruc,amaury,amauta,amauzu,amavasyakhali,amave,amavedagon,amavenou,amavi kope,amavida,amavizcar,amavo,amaw,amawa,amawafur,amawalk,amawan,amawanshe,amawbra,amawchok,amawda,amawihon,amawkrom,amawom,amawou,amawute,amawzari,amaxac,amaxac de guerrero,amaxadhes,amaxal,amay,amaya,amayabuya,amayac,amayadaung,amayalco,amayamaya,amayan,amayane,amayani,amayapampa,amayaque,amayarco,amayas,amayau,amaycollo,amaye,amaye-sur-orne,amaye-sur-seulles,amayi,amayito,amayn,amayn usunoy khid,amayo,amayo ingenio,amayo sitio,amayon,amayong,amayoro,amaysozah,amaytlyakh,amayuca,amayuelas de abajo,amayuelas de arriba,amayuelas de ojeda,amayuho,amaza,amaza zaga,amazac,amazaca,amazaki,amazar,amaze,amazer,amazi,amazikha,amaziou,amazirht,amazo,amazon,amazonas,amazonia,amazouaj,amazouj,amazoul,amazour,amazre,amazrou,amazu,amazu i,amazu ii,amazy,amazya,amazzar,amazzer,amazzr,amb,amb khaskheli,amba,amba alage,amba dar,amba derho,amba dher,amba georghis,amba georgis,amba ghei,amba ghiorghis,amba giorgis,amba giyorgis,amba goth,amba guba,amba lamba,amba madere,amba maderiya,amba madre,amba mariam,amba maryam,amba matala,amba not,amba ras,amba selis,amba silas,amba-rykhutsa,ambaba,ambabaay,ambabag,ambabaka,ambabaky,ambabang,ambabaqui,ambabari,ambabarwa,ambabbo,ambabhona,ambabo,ambabou,ambabu,ambaca,ambacan,ambacana,ambacanan,ambacang,ambacangasam,ambacauggatal,ambaccia,ambach,ambaciara,ambacocat,ambacon,ambacourt,ambad,ambadandegama,ambadara,ambadarreh,ambadeniya,ambadikala,ambaek,ambaeta,ambafamainty,ambag,ambaga,ambaga 2,ambagahagama,ambagahagedara,ambagahahena,ambagahakanda,ambagahakandura,ambagahakotuwa,ambagahalanda,ambagahawadiya,ambagahawala,ambagahawatta,ambagahawela,ambagahawella,ambagahawewa,ambagai,ambagala,ambagammana,ambagamuwa,ambagan,ambagarattur,ambagarh chauki,ambagarh-chowki,ambagasara,ambagasdowa,ambagasduwa,ambagasmulla,ambagaspitiya,ambagassewa,ambagastenna,ambagaswewa,ambagat,ambagehewa,ambagh,ambagh pir ziarat,ambagiw,ambago,ambagoda,ambagolla,ambagolle,ambagon,ambagoura,ambagsan,ambagsang,ambagsong,ambaguio,ambagura,ambah,ambah darah,ambaha,ambahabe,ambahaka,ambahakily,ambahamahazotra,ambaharonga,ambahary,ambahatra,ambahatrazo,ambahatsa,ambahatsy,ambahavaha,ambahenwewa,ambahera,ambahevahe,ambahia,ambahibe,ambahibe est,ambahibe nord,ambahibe ouest,ambahibe sud,ambahibo,ambahiboho,ambahibotsara,ambahifrakoa,ambahihy,ambahikarabo,ambahikily,ambahila,ambahimalitsy,ambahimautsy,ambahimerana,ambahinia,ambahipika,ambahisotry,ambahita,ambahitoaka,ambahitra ouest,ambahitrakoa,ambahitrazo,ambahitroza,ambahivahibe,ambahivahikely,ambahivahy,ambahivalfy,ambahive,ambahiza,ambaho,ambaho-enio,ambahoabe,ambahoaka,ambahobe,ambahona,ambahondrano,ambahovelo,ambahta,ambahta rindan,ambahta sheikha,ambahy,ambaiati,ambaiavato,ambaibe,ambaibo,ambaibo sambao,ambaiboa,ambaiboabe,ambaiboahe,ambaiela,ambaila,ambaio,ambaiorata,ambaito,ambajhari,ambajiri,ambajoana,ambajogai,ambak,ambaka,ambakadawara,ambakaka,ambakali,ambakan,ambakandawila,ambakar,ambakele north,ambakhs,ambakhsh,ambaki,ambakin,ambakirano,ambakireny,ambakitrano,ambakitsy,ambakivoho,ambako,ambakoa,ambakoa-ambany,ambakoa-ambinany,ambakoana,ambakobe,ambakohasina,ambakolawewa colony,ambakon,ambakona,ambakote,ambaku,ambakumbura,ambakumina,ambaky,ambal,ambal tas,ambal-e bustani,ambala,ambala quia quiluangi,ambalaba,ambalabako,ambalabanty,ambalabany,ambalabao,ambalabararata,ambalabe,ambalabe atsimo,ambalabe avaratra,ambalabe nord,ambalabe sud,ambalabe-ifasina,ambalaben ifasina,ambalaboa,ambalaboka,ambalabonga,ambalabongo,ambaladera,ambaladingana,ambalafamainty,ambalafananarana,ambalafandra,ambalafandrana,ambalafandriana,ambalafantsy,ambalafany,ambalafarihy,ambalafary,ambalafasina,ambalafasy,ambalafataka,ambalafatsy,ambalafeno,ambalafenoarivo,ambalafety,ambalafolo,ambalafomby,ambalafontsy,ambalafony,ambalafotaka,ambalafotsy,ambalafrandra,ambalagaro,ambalage,ambalagoavy,ambalahady,ambalahambana,ambalahambano,ambalaharahotra ouest,ambalaharaka,ambalaharongana,ambalahasina,ambalahasy,ambalahatsaka,ambalahazo,ambalaherana,ambalahonke,ambalahonko,ambalahoraka,ambalahoraky,ambalahorana,ambalahorona,ambalahosy,ambalajia,ambalaka,ambalakafe,ambalakafotra,ambalakajaha,ambalakanda,ambalakaty,ambalakazaha,ambalakely,ambalakida,ambalakinana,ambalakinany,ambalakindresy,ambalakinina,ambalakirajy,ambalakitata,ambalakitata (2),ambalakitata ii,ambalakitatra,ambalakizitina,ambalakofafa,ambalakofafa atsimo,ambalakofafa avaratra,ambalakofafa sud,ambalakokony,ambalakondro,ambalakonjy,ambalalady,ambalalahibe,ambalale,ambalalehibe,ambalalemoka,ambalalova,ambalalova-milaly,ambalam,ambalam vattakandal,ambalama,ambalamadiro,ambalamaevanata,ambalamafina,ambalamaharivo,ambalamahasoa,ambalamahatsara,ambalamahatsinjo,ambalamahavelona,ambalamahavolona,ambalamahazatra,ambalamahazotra,ambalamahoga,ambalamahogo,ambalamainty,ambalamaivanaty,ambalamalaza,ambalamalemy,ambalamamoko,ambalamana,ambalamanakana,ambalamanana,ambalamanankavana,ambalamananty,ambalamanarivo,ambalamanary,ambalamanasa,ambalamanasy,ambalamanasy i,ambalamanasy ii,ambalamandavo,ambalamandry,ambalamanenjana,ambalamanga,ambalamanna,ambalamany,ambalamarana,ambalamarina,ambalamarina amaisana,ambalamarina avaratra,ambalamarina ouest,ambalamarina-andoharano,ambalamaro,ambalamary,ambalamasina,ambalamasy,ambalamatsiobe,ambalamatsioke,ambalamatsioky,ambalambato,ambalambengy,ambalambil,ambalambola,ambalambongo,ambalambositra,ambalameloka,ambalamena,ambalamendrika,ambalamialoha,ambalamiary,ambalamirary,ambalamisaona,ambalamita,ambalamitsaka,ambalamitsangana,ambalammulla,ambalamoa,ambalamoakely,ambalamontana,ambalamotraka,ambalamoya,ambalampamba,ambalamulla,ambalanakanga,ambalanano,ambalanaomby,ambalanaondry,ambalanara,ambalanda,ambalandampa,ambalandapa,ambalande,ambalandrafia,ambalandranoankarina,ambalandrazana,ambalanduwa,ambalanengitra,ambalangan dalin,ambalangga,ambalangoda,ambalangoddi,ambalanianana,ambalaniany,ambalanibory,ambalanifotsy,ambalanihasy,ambalanimaromena,ambalanira,ambalanirana,ambalanivo,ambalanjanakomby,ambalanmulla,ambalanomby,ambalanonga,ambalanosy,ambalanpitiya,ambalantota,ambalantsiraka,ambalantsotry,ambalany,ambalanyaya,ambalaoa,ambalaomby,ambalapaiso,ambalapaka,ambalapalso,ambalapalso ii,ambalapamba,ambalapiso,ambalapitiya,ambalapulai,ambalapuzha,ambalarane,ambalarano,ambalaranobe,ambalaraotsy,ambalareketa,ambalaroa,ambalaroka,ambalaromba,ambalarondra,ambalarondro,ambalarongana,ambalaroy,ambalasakoana,ambalasamudram,ambalasaraka,ambalasarotsy,ambalasatra,ambalasatrana,ambalasavoa,ambalasavoka,ambalaseva,ambalasoa,ambalasokona,ambalasorita,ambalasotry,ambalatamby,ambalatana,ambalatanana,ambalatanovana,ambalatany,ambalatary,ambalatataka,ambalatavy,ambalatelo,ambalatemoka,ambalatenina,ambalatenina ii,ambalateva,ambalatevana,ambalateza,ambalatraka,ambalatrihomehy,ambalatsako,ambalatsangana,ambalatsara,ambalatsaraka,ambalatsaramadina,ambalatsiefa,ambalatsieta,ambalatsila,ambalatsilalovana,ambalatsilatovana,ambalatsimanoko,ambalatsimiedika,ambalatsimirango,ambalatsinainy,ambalatsingaka,ambalatsingy,ambalatsipaky,ambalatsiraka,ambalatsirava,ambalatsiriana,ambalatsopaka,ambalatsotry,ambalatunga,ambalatungan,ambalava,ambalavabe,ambalavan,ambalavan-atatan,ambalavao,ambalavao andrefana,ambalavao ankerana,ambalavao atatao,ambalavao atsinanana,ambalavao nord,ambalavao sud,ambalavaokely,ambalavaro,ambalavary,ambalavato,ambalavato haut,ambalavatokely,ambalaveli,ambalavelo,ambalavelombe,ambalavelona,ambalavelona ambany,ambalavelona ambony,ambalavenoka,ambalavero,ambalavia,ambalaviro,ambalavita,ambalavoahangy,ambalavoatavo,ambalavohimay,ambalavokatra,ambalavola,ambalavolo,ambalavondro,ambalavondrona,ambalavongo,ambalavontaka,ambalavotaka,ambalavy,ambalawatu,ambalay,ambalayan,ambalayat,ambalayaya,ambaldi,ambale,ambale phaso,ambalema,ambalengitra,ambalengo,ambalewewa,ambalga,ambalga buzurg,ambalgna,ambali,ambalia,ambaliabe,ambaliandro,ambaliandro nord,ambaliandro sud,ambalianjavatra,ambaligolla,ambaliha,ambalihabe,ambalihamilopaka,ambalika,ambalikida,ambalingit,ambalio,ambalipamba,ambalite,ambalivalo,ambaliyadda,amballo,ambalo,ambaloe,ambalolo,ambalomboro,ambalomborona,ambalona,ambalong,ambalorao,ambalovary,ambalovolona,ambaltek,ambalu,ambam,ambam essaobam,ambamalla,ambambao nord,ambambindo,ambamulla,amban,ambana,ambanagai,ambanagar,ambanala,ambananata,ambanara,ambanda,ambandanira,ambandanirana,ambande,ambandi,ambandrana,ambandrika,ambandroala,ambane,ambanella,ambang,ambanga,ambangabe,ambangam,ambangan,ambangang,ambangdong,ambangeg,ambangi,ambangig,ambangindambo,ambango,ambangonan,ambangua,ambangui,ambangulu,ambangunan,ambanha,ambanhe,ambani,ambania,ambaniala,ambaniato,ambaniavatra,ambaniavoha,ambanibohobe,ambanidhina,ambanidia,ambanie,ambanifalika,ambanifilao,ambanihazo,ambanihiko,ambanikibo,ambanikily,ambanilalakely,ambanilalana,ambanimanasy,ambanimangy,ambanimantsaky,ambanimaso,ambanimentsaky,ambaninato,ambaninonoka,ambaniranona,ambaniranotsilena,ambanisangy,ambanisanjo,ambanisarika,ambanisariky,ambanisiranana,ambanitsena,ambanivato,ambanivohitra,ambanizana,ambanja,ambanjabe,ambanjahilatra,ambanjalava,ambanjsondriry,ambano,ambanoro,ambanpitiya,ambanpola,ambanporuwa,ambanwa,ambanwala,ambanwali,ambanwali sarai,ambanwela,ambanwita,ambany manakambahiny,ambao,ambaonio,ambaoruwa,ambaovy,ambapaka,ambapani,ambapani chauki,ambar,ambar banda,ambar berezkina,ambar garang,ambar kalay,ambar kelay,ambar khana,ambar mardugu,ambar mela,ambar melah,ambar nau,ambar now,ambar shah,ambar-dielyarya,ambar-dzhar,ambar-manak,ambara,ambara khas,ambarabad,ambarabaha,ambarabahy,ambarabanja,ambarabelo,ambaracao norte,ambaracao sur,ambarajah,ambarak,ambaraka,ambarakaraka,ambaralan,ambaramidada,ambaran,ambaranata,ambarang,ambarano,ambarao,ambaraoua,ambararala,ambararata,ambararata ambany,ambararata ambony,ambararata andrefana,ambararata atsimo,ambararata avaratra,ambararata nord,ambararata ouest,ambararata sud,ambararata-sofia,ambararata-toby,ambararatabe,ambararatabe avaratra,ambararatabe nord,ambararatabevatana,ambararatafaly,ambararatafotsy,ambararatahely,ambararatakely,ambararatakoly,ambararatalava,ambararatamafaika,ambararatamavo,ambararatamisakana,ambararatanaomby,ambararatasoratra,ambararatavokoka,ambararate,ambararatilava,ambararato,ambararatra,ambararaty,ambararavy,ambaras,ambarasay,ambarat,ambarata,ambaratamafaiky,ambaratasoa,ambaratra,ambarau,ambarava,ambaravarambato,ambaravaranala,ambaravaranalabe,ambaravarandrafia,ambaravarantany,ambaravatry,ambarawa,ambarayah,ambarcay,ambarchi,ambarchik,ambarci,ambarcik,ambarcilar,ambarcili,ambarciya,ambardakh,ambardere,ambardere koyu,ambardzhiya,ambare,ambares,ambares-et-la grave,ambargai,ambargaon,ambargari,ambargasta,ambargay,ambargurgen,ambarhoda,ambari,ambari mirzapur,ambaria,ambariaho,ambarialoha,ambaribe,ambarifafy,ambarifoana,ambarifotsy,ambarihasara,ambarihon,ambarijanahary,ambarijatsy,ambarijeby,ambarijeby atsimo,ambarika,ambarikely,ambarikha,ambarikorano,ambarilao,ambarim,ambarimafaitra,ambarimahia,ambarimamy,ambarimanga,ambarimaninga,ambarimanira,ambarimanitra,ambarimanitsy,ambarimanjeva,ambarimanjevo,ambarimanta,ambarimantsina,ambarimay,ambarimbalala,ambarimilamba,ambarinakanga,ambarinakoho,ambarinampy,ambarinanahary,ambarinda,ambarindahy,ambarindranabary,ambarindranahary,ambarininahary,ambarinjanahary,ambario,ambariokorano,ambariomena,ambariomiambana,ambarion,ambarionadabo,ambarionasara,ambariondolo,ambariopanenitra,ambariotelo,ambaripaika,ambarisan,ambariste,ambarita,ambaritsatrana,ambarivany,ambarivao,ambarivondrona,ambarivoribe,ambarizato,ambarka,ambarkalay,ambarkavak,ambarkaya,ambarketawang,ambarkhana,ambarkhanah,ambarkhaneh,ambarkheyl,ambarkoy,ambarkul,ambarla,ambarlansang,ambarlassang,ambarli,ambarlik,ambarlo,ambarmela,ambarnath,ambarnoye,ambarny,ambarnyy,ambaro,ambaroba,ambarobe,ambarokan,ambaromamoy,ambaromanoy,ambaromba,ambaromidada andrefana,ambaromidada atsinanana,ambaromidada est,ambaromidada ouest,ambarovao,ambarovka,ambarovtsy,ambarozu,ambarpet,ambarpet kalan,ambarpinar,ambarpur,ambarqol,ambarsalikat,ambarsamuch,ambarsay,ambartafa,ambartawang,ambartepa,ambartepe,ambartsevo,ambartsy,ambaru chal,ambarukmo,ambarura,ambary,ambarzai,ambas,ambas aguas,ambasa,ambasadna,ambasador,ambasaguas,ambasahoka,ambasal,ambasal ciudad,ambasamudram,ambasang,ambasatna,ambasglao,ambasi,ambasimiviky,ambasio,ambasitila,ambasmestas,ambasoli,ambason,ambasotry,ambassa,ambassador downs mobile home park,ambassamudram,ambassatna,ambassilo,ambassing,ambasy,ambasy nord,ambasy sud,ambat,ambata,ambata avaratra,ambatabe,ambatalawa,ambatale,ambatali,ambatamarina,ambatambaky,ambatanirana,ambatankozo,ambatararatavokiky,ambatarihy,ambatata,ambatateza,ambataza,ambatcha,ambatelo,ambatenna,ambatha,ambatillo,ambatjang,ambatna,ambato,ambato asana-valabetokana,ambato boeni,ambato boeny,ambato fasy,ambato nord,ambato sud,ambato-andrano,ambato-sosoa,ambato-sur-mer,ambatoabo,ambatoafo,ambatoakana,ambatoampanga,ambatoantranokely,ambatoarana,ambatoasaina,ambatoasana,ambatoasana afovoany,ambatoatrano,ambatobarika,ambatobe,ambatobe ambony,ambatobe atsimo,ambatobe besoy,ambatobe sud,ambatobe-anjavy,ambatoben anjavy,ambatobimahavelo,ambatoboahangy,ambatoboro,ambatobory,ambatodidy,ambatodinga,ambatodrenambo,ambatoe,ambatoefatra,ambatofaingaina,ambatofaliha,ambatofaly,ambatofamokonana,ambatofangehana,ambatofangery,ambatofanto,ambatofaritana,ambatofinandrahana,ambatofisaka,ambatofisaka ii,ambatofisaorana,ambatofitarafana,ambatofitatra,ambatofitorahana,ambatofohy,ambatofolaka,ambatofotsikely,ambatofotsiloha,ambatofotsy,ambatofotsy-initeraha,ambatohambana,ambatohara,ambatoharamanga,ambatoharana,ambatoharana volarivo,ambatoharanana,ambatoharanana volarivo,ambatoharananaka,ambatoharangana,ambatoharea,ambatohirika,ambatohiriky,ambatoifatra,ambatojobijoby,ambatojoby,ambatojoly,ambatokapaika,ambatokapika,ambatokasa,ambatokely,ambatokintana,ambatokirajy,ambatokirintsa,ambatola,ambatolafia,ambatolahiambana,ambatolahiazo,ambatolahidimy,ambatolahihambana,ambatolahihazo,ambatolahihazo est,ambatolahimaro,ambatolahimavo,ambatolahimisoratra,ambatolahinaory,ambatolahindrabekibo,ambatolahindrano,ambatolahisoa,ambatolahitelo,ambatolahivondraka,ambatolahy,ambatolahy-sarodrano,ambatolahy-soaserana,ambatolalaka,ambatolalona,ambatolamakana,ambatolampikely,ambatolampy,ambatolampy bas,ambatolaona,ambatolatelo,ambatolava,amb"toloaka,ambatoloaka atsimo,ambatoloaka avaratra,ambatolomaka,ambatolongo,ambatomadinika,ambatomafana,ambatomahamanina,ambatomaintikely,ambatomaintingoro,ambatomainty,ambatomainty avaratra,ambatomainty nord,ambatomaladia,ambatomalady,ambatomalama,ambatomalaza,ambatomamy,ambatomana,ambatomanaky,ambatomanambahatsa,ambatomanana,ambatomanandoha,ambatomandondona,ambatomaneka,ambatomaneno,ambatomanga,ambatomanjaka,ambatomanjake,ambatomanoakaza,ambatomanoakazo,ambatomanoina,ambatomanoy,ambatomarako,ambatomaraoka,ambatomarina,ambatomaro,ambatomasina,ambatomasino,ambatomaso,ambatomasy,ambatomay,ambatombaky,ambatombe,ambatomboahangy,ambatomboay,ambatomboro,ambatomboromahery,ambatomborona,ambatombositra,ambatomby,ambatomena,ambatomena nord,ambatomena roche,ambatomena toby,ambatomenakia,ambatomenaloha,ambatomeva,ambatomiady,ambatomiankina,ambatomiankinkely,ambatomiarina,ambatomiary,ambatomidona,ambatomifanongoa,ambatomifikitra,ambatomifikitsa,ambatomihala,ambatomiharihary,ambatomika,ambatomikia,ambatomikiny,ambatomikotrana,ambatomilahatra,ambatomilahatrano,ambatomilahy,ambatomilo,ambatomiloana,ambatomiloloha,ambatomilona,ambatomily,ambatomirahalahy,ambatomirahavavy,ambatomiranty,ambatomiraviravy,ambatomisangana,ambatomisinotraka,ambatomisokatra,ambatomisokitra,ambatomita,ambatomitangolo,ambatomitety,ambatomitigoa,ambatomitikitra,ambatomitikitsy,ambatomitokona,ambatomitongola,ambatomitsangakely,ambatomitsangana,ambatomitsinjorano,ambatomitsivalana,ambatomivarina,ambatomivary,ambatomivory,ambatonahisodrano,ambatonahorina,ambatonakanga,ambatonampanga,ambatonampasina,ambatondrabilana,ambatondradama,ambatondrahila,ambatondrahilana,ambatondrahilany,ambatondrakalavao,ambatondrandriamasy,ambatondrapeto,ambatondrazaka,ambatoniaro,ambatonifody,ambatoniloloha,ambatonjanahary,ambatonombalahy,ambatonoradrazaka,ambatonosy,ambatonqoaika,ambatopaika,ambatopaikafo,ambatopanenitra,ambatopilaky,ambatoraho,ambatorao,ambatoratsy,ambatoria,ambatorikiriky,ambatoringitra,ambatoroka,ambatorola,ambatorondro,ambatoronina,ambatosarintrano,ambatosariomby,ambatosarotra,ambatosate,ambatosia,ambatosihona,ambatosoa,ambatosoavolo,ambatosokana,ambatosola,ambatosondriry,ambatosoratra,ambatosy avaratra,ambatotahy,ambatotampy,ambatotapaka,ambatotavolo,ambatotelo,ambatotendrena,ambatoteza,ambatotily,ambatotiska,ambatotokana,ambatotorahana,ambatotsangana,ambatotsingy,ambatotsipihina,ambatotsivala,ambatotsivikinina,ambatotsokina,ambatovaky,ambatovaky avaratra,ambatovalky,ambatovanda,ambatovary,ambatovato,ambatovaventy,ambatovelezina,ambatoveve,ambatovinaky,ambatovita,ambatovoamba,ambatovoay avaratra,ambatovoay sud,ambatovola,ambatovony,ambatovory,ambatoxavavy,ambatozanahary,ambatozavavy,ambatrabe,ambatry,ambatsifatsy,ambatsila,ambattur,ambatturai,ambatu,ambatuan,ambau,ambaula,ambaulim,ambaute,ambava,ambavaha,ambavahadiala,ambavahadimitafo,ambavahadivy,ambavahibe,ambavala,ambavala-ambilobe,ambavani sahavato,ambavaniasy,ambavanibe,ambavaninara,ambavaninara andrefana,ambavaninara est,ambavaninara ouest,ambavano,ambavarane,ambavarano,ambavarano-fenoarivo,ambavaranobe,ambavaremby,ambavarne,ambavaroka,ambavasambo,ambavasampo,ambavatanitelo,ambavatany,ambavazakana,ambavli,ambavna,ambavola,ambavorano-fenoarivo,ambawa,ambawala,ambawang,ambawe,ambawela,ambax,ambay,ambayat,ambayek,ambayoan,ambayrac,ambayyah,ambazac,ambazaha,ambazamazava,ambazato,ambazimba,ambazoa,ambazoamazava,ambazoana,ambazoanabe,ambazoativoka,ambazoha,ambduco,ambe,ambe kas,ambeachi-gavol,ambeba,ambebe,ambebro,ambebusia,ambedamena,ambedem,ambeduco,ambegaon,ambegnon,ambegoda,ambehta sheikha,ambeigno,ambejogai,ambekaeri,ambekdet,ambel,ambela,ambelaki,ambelakia,ambelakion,ambelakiotissa,ambelang,ambelas,ambeli,ambelia,ambeliai,ambelies,ambeliko,ambelikon,ambelikou,ambeliku,ambelim,ambelion,ambeliona,ambelits,ambeloe,ambelofiton,ambeloi,ambelokambos,ambelokhori,ambelokhorion,ambelokipoi,ambelon,ambelos,ambelosalesi,ambelouzos,ambelta,ambenay,ambende,ambendra,ambendrabe,ambendrakely,ambendrana,ambendrandava,ambendrano,ambeng-ambengwatangredjo,ambengambeng,ambengan,ambengilengy,ambeni,ambenja,ambeno,ambenou,ambepitiya,ambepoea,ambepulu,ambepur,ambepussa,ambepussa old,amber,amber belair,amber hill,amber hills,amber lake,amber meadows,amber valley,amber way condominium,amber wood estates,amber woode,ambera,amberac,amberamat,amberaro,amberd,ambere,ambereno,ambereny,ambereoa,amberepi,amberes,amberfield,amberg,ambergen,amberiala,amberieu,amberieu-en-bugey,amberieux,amberieux-en-dombes,amberikita,amberimay,amberio,amberivery,amberkab,amberleigh farms,amberley,amberloup,amberly,ambernac,ambero,amberobe,amberokely,amberomanga,amberomanitra,amberomena,amberope,amberoverobe,amberparna,amberre,ambersha,amberson,amberson valley estates,ambert,amberwood,amberwood ii,amberwood north,ambery,ambes,ambesaua,ambesea,ambeset,ambesh,ambesia,ambesisika,ambetenna,ambetewa,ambeti,ambetiuha,ambetsu,ambeua,ambevongo,ambewari,ambewela,ambeyet,ambeyrac,ambezh,ambgaon,ambh,ambhai,ambhawa,ambheti,ambheu barnbhot,ambheu wangha,ambhitromby,ambi,ambi danna,ambia,ambia sarodrano,ambiabe,ambiahely,ambialet,ambian,ambiapur,ambiaravy,ambiataka,ambiatoka,ambibaka,ambiboka,ambiboky,ambiby,ambica,ambicao,ambidedi,ambidro,ambiedes,ambiegna,ambientu,ambierle,ambievillers,ambigai,ambigaton,ambigol west,ambigue,ambihivihy,ambihy,ambij,ambika,ambikanagar,ambikapur,ambikiakely,ambikol,ambikol west,ambikorano,ambikul,ambiky,ambil,ambil-ambil,ambila,ambila-lemaitso,ambilabe,ambilalialika,ambilamaina,ambilamena,ambilanivia,ambilany,ambilay,ambili,ambilimaro,ambilimavo,ambiliseyufo,ambilivily,ambilivity,ambillo,ambillou,ambillou-chateau,ambilo,ambilobe,ambilobekely,ambilokarana,ambilomagodro,ambilomavo,ambilombe,ambilona,ambimi,ambinabe,ambinamy-tsianihy,ambinanatsena,ambinaniango,ambinaniantakotako,ambinaniazafo,ambinanibavoni,ambinanibe,ambinanibeanatsindrana,ambinanibefiana,ambinanibehasina,ambinanibemarivo,ambinaniboka,ambinanidilana,ambinanidrano,ambinanifaho,ambinanifaraony,ambinanihiboka,ambinanikely,ambinanikoro,ambinanililabe,ambinanimananana,ambinanimananano,ambinanimanano,ambinanimandala,ambinanimangabe,ambinanimarambo,ambinanimbaza,ambinanimbazaha,ambinanin andranotsara,ambinanin andravory,ambinanin ankarany,ambinanini sakaleona,ambinaninakia,ambinaninamorona,ambinaninandempona,ambinaninanjozoro,ambinanindovoka,ambinanindrangaranga,ambinanindrano,ambinaninikofa,ambinaninisahanaho,ambinaninisery,ambinaninivatohely,ambinaninkoro,ambinaninony,ambinanintromby,ambinaniposte,ambinanirano,ambinaniroa,ambinaniroa-andonaka,ambinanisaha,ambinanisahafary,ambinanisahalangiana,ambinanisaharamy,ambinanisahasanda,ambinanisahasaty,ambinanisahateza,ambinanisahave,ambinanisahavia,ambinanisakaleona,ambinanisakana,ambinanisanakondro,ambinanisanio,ambinanisarambana,ambinanitela,ambinanitelo,ambinanitelo ouest,ambinanitelo-ianakoto,ambinanitsahy,ambinanitsarabanjina,ambinanivavany,ambinanivavony,ambinanivelo,ambinanivolo,ambinany,ambinany antsahabe,ambinany antsaka,ambinany befiana,ambinany bemanevika,ambinany iefaka,ambinany maharivo,ambinany matitanana,ambinany-ankarany,ambinany-faraony,ambinany-manambaliha,ambinany-tanosy,ambinany-tsianihy,ambinany-zana,ambinanyambilona,ambinanyampasimbola,ambinanyanotendro,ambinanybe,ambinanydilana,ambinanydrano,ambinanyfaho,ambinanyfaroany,ambinanyhoraka,ambinanykajobo,ambinanykely,ambinanykera,ambinanylalangy,ambinanylazafo,ambinanymahaly,ambinanymanakambahiny,ambinanymanambato,ambinanymananana,ambinanymanandriana,ambinanymananony,ambinanymanarezo,ambinanymandala,ambinanymandemoko,ambinanymaningory,ambinanymanitsy,ambinanymanodidina,ambinanymarofanjana,ambinanymasako,ambinanymaso,ambinanymbazaha,ambinanynambelatra,ambinanynambilo,ambinanynambilona,ambinanynamorona,ambinanynampitsira,ambinanynandranovondrona,ambinanynandrevo,ambinanynankaramy,ambinanynantsahabe,ambinanyndrano,ambinanyndranofotaka,ambinanyndranotela,ambinanyniomby,ambinanynisahanaho,ambinanynivatohely,ambinanyniverembona,ambinanynomby,ambinanynony,ambinanyranomena,ambinanyranovaky,ambinanyratambo,ambinanyriana,ambinanysahalalangy,ambinanysahamalaza,ambinanysahambala,ambinanysahanao,ambinanysahanato,ambinanysahantsanda,ambinanysaharamy,ambinanysahasanda,ambinanysahavary,ambinanysahomby,ambinanysakaleona,ambinanysalampy,ambinanysaratonga,ambinanysivanina,ambinanytanana,ambinanytelo,ambinanytomarosana,ambinanytranomaro,ambinanytsafara,ambinanytsarabanjina,ambinanytsazahana,ambinanyvatomena,ambinanyvatovelo,ambinanyvavony,ambinanyvolo,ambinanyvolotara,ambinanyzama,ambinary,ambinda,ambinda-mahebo,ambindabe,ambindamahery,ambindandrakemba,ambindo,ambingivato,ambingovingo,ambingue,ambinoandrakemba,ambinodo,ambira,ambire,ambireika,ambirimena,ambiroman,ambisa,ambisi,ambissane,ambisumne,ambisuo,ambit,ambita,ambitacay,ambite,ambiti gheorghis,ambiti gheorgis,ambiti ghiorghis,ambitika,ambito,ambitoka,ambiula,ambiur,ambivivy,ambivy,ambivy ii,ambiwala,ambiya,ambiyakan,ambiz,ambizlyar,ambizy,ambjorby,ambjorbyn,ambjornarp,ambjorntorp,ambla,amblagnieu,amblaincourt,amblainville,amblala,amblan,amblans,amblans-et-velotte,amble,amblecote,ambleny,ambleon,ambler,ambler heights,ambler park,amblerieu,amblers crossing,amblersburg,amblerville,ambleside,ambleston,ambleteuse,ambleve,ambleville,ambli,ambliani,amblie,amblimont,amblincourt,amblong,ambloy,ambly,ambly-fleury,ambly-sur-meuse,ambo,ambo chak,ambo do catucuna,ambo khak,ambo kili,ambo kulon,amboa,amboabangy,amboabege,amboaboa,amboaboaka,amboadromisotra,amboafandrana,amboafandrano,amboafoana,amboafotsy,amboahangibe,amboahangikely,amboahangimamy,amboahangimasina,amboahangisay,amboahangitelo,amboahangy,amboaido,amboaimena,amboaimitrohaky,amboairafia,amboajabo,amboakarivo,amboakatra,amboakatra-mahasoa,amboakazo,amboakitsy,amboakotry,amboaksina,amboalaingo,amboalefoka,amboalimena,amboalolo,amboamaho,amboamanga,amboameha,amboamena,amboanaivo,amboanampotsy,amboanana,amboananto,amboanara,amboanato,amboanatsindriana,amboandrika,amboanemba,amboangibe,amboangisay,amboangiteto,amboangivy,amboangue,amboangy,amboanid,amboanio,amboaniotokana,amboanjo,amboanjobe,amboankazo,amboanonoka,amboanta,amboanto,amboaovy,amboapiky,amboara,amboarafy,amboarakely,amboaramamy,amboaravoanjo,amboarokety,amboasanikely,amboasaribe,amboasarikely,amboasarimaty,amboasaritsimitambo,amboasaritsimitombo,amboasary,amboasary atsimo,amboasary sud,amboasary-ambohikambana,amboasary-antambolo,amboasary-gara,amboasi,amboatany,amboatavo,amboatavohazo,amboatrotroka,amboavarikely,amboavary,amboavato,amboavoia,amboavola,amboavoribe,amboavory,amboay,ambobaka,amboboka,amboboky,ambocang,ambocao,ambodae,ambodala,ambodiababo,ambodiadaba,ambodiadabo,ambodiafomena,ambodiakatra,ambodiakondro,ambodiala,ambodialabozia,ambodialampona,ambodialavelona,ambodiamba,ambodiamentana,ambodiamontana,ambodiamontana i,ambodiampa,ambodiampaly,ambodiampana,ambodiana,ambodiandriambe,ambodiangezoka,ambodiantafana,ambodiara,ambodiara atsinanana,ambodiara ii,ambodiara iii,ambodiarabe,ambodiarakely,ambodiaramy,ambodiarovinala,ambodiatafa,ambodiatafana,ambodiaviary,ambodiaviavy,ambodiazamba,ambodibahy,ambodibakoly,ambodibare,ambodibaro,ambodibato,ambodiboanio,ambodibonana,ambodibonara,ambodibonara avaratra,ambodibonarahely,ambodibonarm,ambodibonga,ambodibongo,ambodiboro,ambodibosy,ambodidimaka,ambodidivaina,ambodifahitra,ambodifahy,ambodifamba,ambodifamotsitra,ambodifanana,ambodifanemboka,ambodifano,ambodifaraha,ambodifarihy,ambodifasika,ambodifasina,ambodifiakarana,ambodifinesy,ambodifomby,ambodifontsy,ambodiforaha,ambodifotatra,ambodifototra,ambodigava,ambodigavo,ambodigayo,ambodihady,ambodihakarana,ambodihangezoka,ambodihara,ambodihara-homby,ambodihara-sakavazoha,ambodiharahara,ambodiharambe,ambodiharamy,ambodiharana,ambodiharavola,ambodihariha,ambodiharina,ambodihasina,ambodihasy,ambodihatafana,ambodihazina,ambodihazinina,ambodihazo,ambodihazoambo,ambodihazomambo,ambodihazomamy,ambodihazovelona,ambodihazovola,ambodihelahy,ambodihidina,ambodihintsina,ambodihitsina,ambodihonkona,ambodijia,ambodikakazo,ambodikatakata,ambodikely,ambodiketsa,ambodikijy,ambodikilo,ambodikily,ambodikininina,ambodikizy,ambodilafa,ambodilaingo,ambodilaitra,ambodilaloa,ambodilalona,ambodilandemy,ambodilandy,ambodilatona,ambodilaza,ambodilazana,ambodilazanivivy,ambodilendemy,ambodiloaranjy,ambodimabilo,ambodimadino,ambodimadio,ambodimadiro,ambodimadiro atsinanana,ambodimadiro est,ambodimadiro-antanananivo,ambodimadirokely,ambodimagoro,ambodimahabibo,ambodimakabo,ambodimamanona,ambodimampay,ambodimanara,ambodimanaribe,ambodimanary,ambodimandresy,ambodimandresy ii,ambodimandroroto,ambodimanery,ambodimanga,ambodimanga ambany,ambodimanga atsimo,ambodimanga avaratra,ambodimanga farimay,ambodimanga ii,ambodimanga-ranomafana,ambodimanga-sahavalanina,ambodimangabe,ambodimangahely-andranotsara,ambodimangajingo,ambodimangakely,ambodimangall,ambodimangamaro,ambodimangambavy,ambodimangaroa,ambodimangasahimpa,ambodimangasoa,ambodimangatelo,ambodimangavolo,ambodimango,ambodimanjaka,ambodimanry,ambodimantalia,ambodimantaly,ambodimapay,ambodimarino,ambodimarohita,ambodimatsioka,ambodimediro,ambodimeva,ambodimitsioka,ambodimokonazy,ambodimolanga,ambodimontana,ambodimontso,ambodimontso sud,ambodimoranjy,ambodimotso,ambodimotso ambany,ambodimotso ambony,ambodimotso atsimo,ambodimotso bas,ambodimotso haut,ambodimotsy,ambodin ifandana,ambodinaka,ambodinampahitra,ambodinana,ambodinandazoana,ambodinangavo haut,ambodinanto,ambodinato,ambodindrano,ambodinifody,ambodinivato,ambodinivatoana,ambodinivohimena,ambodinivongo,ambodinomoka,ambodinonoka,ambodinovitra,ambodiokondro,ambodiovitra,ambodipaiso,ambodipaiso avaratra,ambodipaka,ambodipako,ambodipamba,ambodipesy,ambodipilomosy,ambodipolimosy,ambodipolomoso,ambodipon,ambodipont,ambodipont-sahana,ambodiraboso,ambodirafia,ambodiraidoky,ambodiramiavona,ambodiramiavony,ambodirampingo,ambodirandroho,ambodirangitra,ambodiranio,ambodiranjo,ambodirano,ambodirano atsimo,ambodirano mangoro,ambodiranomena,ambodiravina,ambodirengitra,ambodiriamy,ambodiriani sahafary,ambodiriana,ambodiriana fanantara,ambodiriana-sahabe,ambodiriano,ambodirima,ambodiringitra,ambodiroahangy,ambodirofia,ambodiroka,ambodiroranga,ambodirotra,ambodirotrakely,ambodisadifitra,ambodisaina,ambodisaka,ambodisakana,ambodisakaona,ambodisakoa,ambodisakoan,ambodisakoana,ambodisakona,ambodisalabo,ambodisambalahy,ambodisantrankely,ambodisatirana,ambodisatra,ambodisatrabe,ambodisatrambe,ambodisatrana,ambodisatrankely,ambodisay,ambodisely,ambodisikidy,ambodisiny,ambodisira,ambodisohihy,ambodisokoa,ambodisolatra,ambodisovoka,ambodistrana,amboditafanana,amboditafara,amboditafonana,amboditalio,amboditandroho,amboditandroroho,amboditangena,amboditanimena,amboditanimenia,amboditany,amboditatonana,amboditavolo,amboditavoto,amboditendrofony,amboditetezana,amboditetezana-fanambana,amboditetezana-sahana,amboditetezano,amboditodinga,amboditolongo,amboditsiho,amboditsikidy,amboditsimiranja,amboditsoa,amboditsoba,amboditsoha,ambodivafaho,ambodivahibe,ambodivaina,ambodivakoana,ambodivanana,ambodivandrika,ambodivandrozana,ambodivandsika,ambodivarona,ambodivarongy,ambodivarony,ambodivasoava,ambodivato,ambodivatoana,ambodivazahana,ambodivelatra,ambodivoahangy,ambodivoahangy avaratra,ambodivoamboana,ambodivoampeno,ambodivoananto,ambodivoandelaka,ambodivoania,ambodivoanibo,ambodivoanio,ambodivoapaka,ambodivoara,ambodivoarabe,ambodivoarahely,ambodivoarakely,ambodivoaranto,ambodivoary,ambodivoasary,ambodivoaseva,ambodivoasihy,ambodivoatangena,ambodivohitra,ambodivohitra mto,ambodivolo,ambodivolo atsimo,ambodivolo avaratra,ambodivona,ambodivongo,ambodivontro,ambodizaotra,ambodizarina,ambodizavy,ambodontava,ambodramontana,ambody-pont,ambodykily,amboeak,amboeaki,amboear,amboeloe,amboeloekradjan,amboenten,amboenten-timoer,amboepoea,amboeramboer,amboeran,amboetsy,ambofampana,ambogaja,ambogama,ambogao,ambogaya,ambogo,amboharabe,ambohaza,ambohiamontana,ambohiandriamanitra,ambohiaranovita,ambohibada,ambohibado,ambohibahoaka,ambohibahoaka-soamiafara,ambohibakoka,ambohibaky,ambohibamanjaka,ambohibao,ambohibao atsimo,ambohibao sud,ambohibarv,ambohibary,ambohibasia,ambohibataza,ambohibato,ambohibazina,ambohibazoina,ambohibe,ambohibe atsinanana,ambohibehivavy,ambohibeloma,ambohibemanjaka,ambohibengibe,ambohibengy,ambohibo,ambohiboa,ambohiboahangy,ambohiboanjo,ambohiboatavo,ambohibobolona,ambohibohoka,ambohibola,ambohibolakely,ambohibolamena,ambohibolava,ambohibolo,ambohibololona,ambohiboramanga,ambohiboria,ambohiborikely,ambohiboromanga,ambohiborona-ambatofotsy,ambohibory,ambohibuahjo,ambohiby,ambohidahalo,ambohidahy,ambohidahy bedoa,ambohidanera,ambohidanerana,ambohidara,ambohidasoly,ambohidava,ambohidaza,ambohidehilahy,ambohidelahy,ambohidempona,ambohidena,ambohidiatafana,ambohidrabibi,ambohidrabiby,ambohidrafy,ambohidrahay,ambohidrainimanga,ambohidraketa,ambohidrakitra,ambohidramanalina,ambohidranandriana,ambohidrano,ambohidranomanga,ambohidrapeto,ambohidrasarika,ambohidraserika,ambohidrasoly,ambohidratimo,ambohidratrimo,ambohidravaka,ambohidray,ambohidrazana,ambohidreny,ambohidriana,ambohidrisa,ambohidrivotra,ambohidroa,ambohidroalahy,ambohidrofia,ambohidronono,ambohifaho,ambohifamba,ambohigogo,ambohihory,ambohiisitaika,ambohijafimatra,ambohijafy,ambohijahakomby,ambohijalovo,ambohijanahary,ambohijanaka,ambohijanakomby,ambohijanamasoandro,ambohijato,ambohijatovo,ambohijavavy,ambohijavena,ambohijavona,ambohijaza,ambohijazavavy,ambohijia,ambohijina,ambohijoky,ambohijorery,ambohikambana,ambohikambana avaratra,ambohikarabo,ambohikato,ambohikely,ambohilomahitsy,ambohimadera,ambohimadinika,ambohimahamasina,ambohimahandry,ambohimahary,ambohimahasoa,ambohimahatsara,ambohimahatsinjo,ambohimahavanana,ambohimahavatona,ambohimahavela,ambohimahavelo,ambohimahavelona,ambohimahavony,ambohimahazo,ambohimahiratra,ambohimahitsy,ambohimahosoa,ambohimalandy,ambohimalaza,ambohimalemy,ambohimamory,ambohimana,ambohimanakana,ambohimanambola,ambohimanamora,ambohimanana,ambohimanantsoa,ambohimanara,ambohimanarina,ambohimanarintsoa,ambohimanarivo,ambohimanarosy,ambohimanatahotra,ambohimanatrika,ambohimandiny,ambohimandrana,ambohimandray,ambohimandresy,ambohimandrirana,ambohimandrorona,ambohimandrosa,ambohimandroso,ambohimandry,ambohimanerina,ambohimaneva,ambohimanga,ambohimanga atsimo,ambohimangabe,ambohimangakely,ambohimanganjaty,ambohimangazafy,ambohimangazaty,ambohimania,ambohimaniry,ambohimanja,ambohimanjaka,ambohimanjaka atsimo,ambohimanjaka-sud,ambohimanjakarano,ambohimanjato,ambohimanoa,ambohimanoro,ambohimanoy,ambohimapine,ambohimara,ambohimaranitra,ambohimarany,ambohimareny,ambohimarimamo,ambohimarina,ambohimarino,ambohimarintsoa,ambohimarirana,ambohimary,ambohimasina,ambohimavo,ambohimeha,ambohimena,ambohimena atsimo,ambohimenakely,ambohimiadana,ambohimiadona,ambohimiakatra,ambohimiandra,ambohimiangaly,ambohimiankikely,ambohimiara,ambohimiarabe,ambohimiarina,ambohimiarivo,ambohimibohaka,ambohimiera,ambohimierambe-andranofito,ambohimijery,ambohimilanga,ambohimilanja,ambohimilatsara,ambohimilemaka,ambohiminaoka,ambohimira,ambohimirahavavy,ambohimirana,ambohimirary,ambohimiritika,ambohimirorona,ambohimisafy,ambohimisaty,ambohimisondrotra,ambohimita,ambohimitembo,ambohimitety,ambohimitikina,ambohimitombo,ambohimitsangana,ambohimitsara,ambohimitsimbina,ambohimitsinjo,ambohimitsivalana,ambohimorina,ambohimpanana,ambohina,ambohinabao,ambohinaianboarina,ambohinalemy,ambohinamboara,ambohinamboarina,ambohinamboary,ambohinaorina,ambohinarenina,ambohinasondrotra,ambohinavela,ambohindrandriana,ambohindrano,ambohinierana,ambohinierenana,ambohinihaonana,ambohinihaonana i,ambohinihaonana ii,ambohinime,ambohiniriana,ambohiomby,ambohiorana,ambohipahana,ambohipaka,ambohipaky,ambohipaly,ambohipamba,ambohipanana,ambohipandrano,ambohipanenitra,ambohiparihy,ambohipasina,ambohipasy,ambohipato,ambohipeno,ambohiperivoana,ambohipiana,ambohipiaonana,ambohipiara,ambohipierehana,ambohipierenana,ambohipihaonana,ambohipihaonano,ambohipiky,ambohipiky-tomboarivo,ambohipirana,ambohipisaka,ambohipo,ambohipoana,ambohipolo,ambohipoloalina,ambohiponana,ambohiponenana,ambohipopona,ambohipotsy,ambohipotsy nord,ambohirafimahantra,ambohiriana,ambohisahatsiho,ambohisikidy,ambohisimiely,ambohitamotamo,ambohitangena,ambohitantely,ambohitany,ambohitapaka,ambohitelo,ambohitoaka,ambohitombo,ambohitompo,ambohitompoina,ambohitrafovoany,ambohitra,ambohitrabel,ambohitrabo,ambohitrafovoany,ambohitraina,ambohitrainakoho,ambohitraiva,ambohitraive,ambohitraivo,ambohitrakanga,ambohitrakely,ambohitrakitsy,ambohitrakoho,ambohitrakoholahy,ambohitrakongona,ambohitralanana,ambohitramba,ambohitrambo,ambohitramboalambo,ambohitrambony,ambohitrampovoana,ambohitranakomby,ambohitranala,ambohitrandafy,ambohitrandraina,ambohitrandriamanitra,ambohitrandriana,ambohitranefitra,ambohitranivo,ambohitranjombo,ambohitrankizy,ambohitrantenaia,ambohitrantoandro,ambohitrapirana,ambohitrarivo,ambohitrasoavana,ambohitratampona,ambohitratsanga,ambohitratsery,ambohitratsimo,ambohitravato,ambohitrazo,ambohitrenefitra,ambohitretibe,ambohitretikely,ambohitriana,ambohitriasana,ambohitrifamoly,ambohitrila,ambohitrila sud,ambohitrimanjaka,ambohitrimarofotsy,ambohitrimitery,ambohitrimitsena,ambohitrimo,ambohitrimpamosavy,ambohitrinambona,ambohitriniandriana,ambohitriniandrianina,ambohitriniandriano,ambohitrinibe,ambohitrinimamba,ambohitrinimanga,ambohitrinimerina,ambohitrinimpamosavy,ambohitrinirina,ambohitrinivina,ambohitrinivoay,ambohitririnina,ambohitrisoa,ambohitriteva,ambohitrizakazaka,ambohitrjanaka,ambohitrmandriana,ambohitrmpanefy,ambohitrniandriana,ambohitrolomahitsy,ambohitrolona,ambohitromby,ambohitromby andrefana,ambohitrondry,ambohitrony,ambohitrorisoa,ambohitrosy,ambohitrova,ambohitrtsanga,ambohitsabo,ambohitsafy,ambohitsambo,ambohitsampana,ambohitsanakisa,ambohitsaonjo,ambohitsaony,ambohitsara,ambohitsara atsinanana,ambohitsara i,ambohitsara ii,ambohitsara iii,ambohitsararay,ambohitsararoa,ambohitsaratany,ambohitsaratelo,ambohitsaratelo-bebao,ambohitseheno,ambohitsengia,ambohitsiadana,ambohitsiadriana,ambohitsiandriana,ambohitsiedehina,ambohitsifa,ambohitsikena,ambohitsikidy,ambohitsilaozana,ambohitsimamory,ambohitsimanova,ambohitsimaramarakely,ambohitsimeloka,ambohitsimenaloha,ambohitsimiasibe,ambohitsimiavona,ambohitsimiely,ambohitsimihafa,ambohitsimiozo,ambohitsimiray,ambohitsinjorano,ambohitsiomby,ambohitsiroa,ambohitsisoky,ambohitsitaika,ambohitsitaitaika,ambohitsivalana,ambohitsoa,ambohitsoaray,ambohitson,ambohitsy,ambohivahivahy,ambohivahy,ambohivandrika,ambohivola,ambohivorikely,ambohizadrailoha,ambohizavavy,ambohodahy,ambohtrdazaina,amboi,amboica,amboim,amboin,amboina,amboiosi,amboiras,amboise,amboitsy,amboiva,amboje,ambojimahabibo,ambojoanjo,ambojobe,ambojomoloka,ambok,amboka,ambokaka,amboke,ambokhak,amboki,ambokiloha,ambokiriny,ambokka,ambokongaka,ambokosi,ambokovoka,ambokovoko,ambokudena,ambokudeniya,ambola,ambolabe,ambolagh,ambolamanga,ambolanakoho,ambolang,ambolanira,ambolaq,ambolarao,ambolavoahangy,ambolavola,ambolembaka,ambolembary,amboli,amboliala,amboliandro atsimo,ambolibazo,ambolidia,ambolidibe,ambolidibe andrefana,ambolidibe atsinanana,ambolidibe est,ambolidibe ouest,ambolidiha,ambolidimaka,ambolifia,amboliha,ambolikapika,ambolikapiki,ambolikapiky,ambolikily,ambolipamba,ambolirano,ambolisaka,ambolisoa,ambolitaka,ambolitsifa,ambolivolombo,ambolo,amboloa,amboloando,amboloandokely,amboloandro,amboloavaratra,ambolobe,ambolobozebe,ambolobozo,ambolobozobe,ambolobozokely,ambolodia,ambolodia atsimo,ambolodia sud,ambolodihy,ambolody,ambolofotsy,ambolofoty,ambolohando,ambolohandro,ambolohazo,ambolojatry,ambolokira,ambolokohinoro,ambolokoho,ambolokohy,ambolokoky,ambololondrano,ambolomadinika,ambolomailaka,ambolomanary,ambolomborona,ambolomena,ambolomirehena,ambolomirehona,ambolomoty,ambolona,ambolonakanga,ambolonaondry,ambolonaondry nord,ambolonaondry sud,ambolondrano,ambolong,ambolongo,ambolonisy,ambolonkira,ambolonkiri,ambolopamba,ambolosarika,ambolosy,ambolotara,ambolotarabe,ambolotarafotsy,ambolotarakely,ambolotaramadinika,ambolotatakely,ambolotsangana,ambolovaliha,ambolovandrika,ambolovohitsy,ambolovoka,ambolovolo,ambolovoriky,ambolozatsy,amboma,ambombongo,ambombory,ambomi,ambomo,ambon,ambona,ambonaivo,ambonao,ambonara,ambonara atsimo,ambonara avaratra,ambonara-mahavelo,ambonarabe,ambonarakely,ambonaramahasoa,ambonaramavo,ambonaramena,ambonarameno,ambonaratokana,ambonary,ambonavio,ambondi do lengue,ambondiaa,ambondra ampiamy,ambondra nord,ambondrabe,ambondraka sud,ambondrana,ambondrano,ambondrika,ambondro,ambondro avaratra,ambondro ii,ambondro-ampasy,ambondro-ankijaha,ambondro-mandeha,ambondro-tsiranandaka,ambondro-tsirananoaka,ambondroala,ambondrobe,ambondrofe,ambondrokely,ambondrolava,ambondrolava atsimo,ambondrolava avaratra,ambondromalemy,ambondromalemy atsimo,ambondromalemy nord,ambondromalemy sud,ambondromamy,ambondromandrevo,ambondromasima,ambondromasina,ambondrombato,ambondrombe,ambondromena,ambondromifehy,ambondromiforitra,ambondromiforotra,ambondromisampy,ambondromisamy,ambondromisotra,ambondrona,ambondrona atsimo,ambondrondregeginy,ambondrono,ambondropaly,ambondrosomory,ambondrotra,ambondrpaly,ambonduy,ambong,ambonga,ambongabe,ambongahe,ambongalava,ambongamanenitra,ambongamaneno,ambongamarina,ambongatsara,ambongdolan,ambongo,ambongonifara,ambonhe,amboni,amboniakondro,amboniandrefana,ambonidobo,ambonievo,ambonil,ambonilaitra,ambonilandihazo,amboniloha,amboniloka,ambonimaharivo,ambonimarina,ambonimarirana,ambonimena,ambonimilanjakely,ambonindrana,ambonindriana,ambonindriano,amboniriana,ambonivalotra,ambonivato,ambonivohitra,ambonivondrona,ambonnay,ambonus,ambonville,ambonwari,ambony,ambooga,ambopadang,ambopamba,ambopi,ambor,ambor lor,ambora,amboraccay,amboraho,amboraka,amboraky,amboran,amborana,amborangy,amborano,amborarata,amborata,amborawang,amborco,amborgang,ambori,amboriabo,amboriala,amborihazo,amborimalana,amborimihanto,amboro,amboro nuevo,amboroabo,amboroabobe,amborobe,amborohabe,amboroho,amboroho andrefana,amboroihazo,amboroka,amborokahaka,amborokaka,amborokambo,amboroko,amborokotsy,amboroky,amboromaika,amboromalandy,amboromaneno,amboromanga,amboromena,amborometroka,amboromihanto,amboromisampy,amborompotsy,amborona,amboronabo,amboronala,amboronalina,amboronantaka,amboronaomby,amborondolo,amborondra,amboronedky,amborongo ambony,amboronkombo,amboronomby,amboronosy,amboropotsy,amborosoy,amborotavo,amborotsako,amborovoky,amborovoro,amborovoro atsimo,amborovy,amborquito,amborumbu,ambory,amboseli,ambositra,ambosores,ambosso,ambostel,ambotabe,ambotaka,ambotako,ambotaky,ambotche,ambotjang,amboto,ambotodra,ambotosy,ambotrfamoly,ambotromavo,ambotry,ambotuban,amboua,amboue,amboulbouzekely,ambouli,amboumi,ambouroua,ambourville,ambousi,ambouta,amboutcheka,ambova,ambovia,ambovitsy,ambovo,ambovo ambany,ambovo ambony,ambovoanio,ambovobe,ambovodara,ambovoha,ambovomanga,ambovomatavy,ambovomavo,ambovombe,ambovombe afovoany,ambovomboay,ambovonaomby,ambovonomby,ambovonsy,ambovony,ambovotoka,ambowe,ambowlaq,amboy,amboy center,amboyna,amboyoan,amboza,ambozaka,ambozoanabe,ambozontany,ambozotany,ambozotelo,ambra,ambrabad,ambralauri,ambras,ambrasai,ambrault,ambray,ambraziskiai,ambreal,ambreijo,ambreimaki,ambreo,ambres,ambresin,ambresinaux,ambri,ambri jo pani,ambricka,ambricourt,ambridge,ambridge heights,ambrief,ambrieres,ambrieres-le-grand,ambrines,ambris,ambrisette,ambriz,ambrizete,ambrizette,ambroa,ambrocio,ambrock,ambrogiana,ambrogio,ambroise,ambrolauri,ambrona,ambronay,ambros,ambrosden,ambrose,ambrose village,ambrosenki,ambrosero,ambrosetti,ambrosia,ambrosia lake,ambrosia mill,ambrosio,ambrosio aires,ambrosio vicente,ambrosionki,ambrosiyevo,ambrosova,ambrosovichi,ambrosovo,ambroz,ambrozew,ambrozfalva,ambrozfalvi tanyak,ambrozio,ambru,ambruciai,ambrugeat,ambrumesnil,ambrus,ambrusovce,ambrustanya,ambrutse,ambu,ambua,ambuadia,ambuaki,ambual,ambuale,ambuambili,ambuan,ambuar,ambuasu,ambuau,ambuavao,ambuca,ambucao,ambuclao,ambud,ambudai,ambudbod,ambudobi,ambudodi,ambudu,ambuduco,ambuela,ambuete,ambuetel,ambugao,ambugat,ambugh,ambugha,ambuhren,ambuila,ambuiogan-labuac,ambukanja,ambuken,ambuklao,ambukow,ambuku,ambukuv,ambukwon,ambul,ambula,ambulak,ambulaq,ambulco,ambulco ciudad,ambuldan,ambuletum,ambuleuit,ambulgama,ambulge,ambulogan,ambulogan-labuac,ambulong,ambulosan,ambulu,ambulubusbe,ambuludan,ambulugala,ambulugan,ambulung,ambulupayung,ambulupermai,ambum,ambuma,ambuma-moke,ambumba,ambun,ambuna,ambung,ambungi,ambungkapu,ambunjawa,ambunten,ambuntentimur,ambunti,ambunu,ambuporondavi,ambuqui,ambur,ambura i,amburambur,amburan,amburaya,amburco,amburdak,amburdara,amburg,amburga,amburgey,amburi,ambursa,amburskoye,amburt,amburu,ambus-syurbeyevo,ambusi,ambuten,ambutrix,ambuulanu,ambuwakka,ambuwala,ambuwangala,ambuyan,ambuye,ambwala,ambwash,ambwe,amby,amby fanantara,ambyarsari,amcalan,amcang,amcay,amcelle,amcelle acres,amchachtou,amchat,amchebek,amchedire,amchhimair,amchi,amchila,amchilga,amchiri,amchisohu,amchit,amchitka,amchitt,amchola,amcholli,amchon,amchondong,amchonni,amchotone,amchoud,amchtoutel,amchtoutet,amcigo,amckuka,amcotts,amda,amda kadal,amda nbrahim,amdaga,amdagalgi,amdaha,amdakour,amdal,amdala,amdalai,amdalanwala,amdalay,amdalaye,amdalaye-timmbou,amdallai,amdallaye,amdals verk,amdamoty,amdan,amdan mari da mad,amdane,amdanga,amdangle,amdani,amdapur,amdara,amdarbog,amdasa,amdassa,amdat,amde werk,amde work,amdel,amden,amderamboukan,amderamboukane,amderma,amdewe,amdhala,amdhi,amdi,amdidih,amdingon,amdja,amdjagara,amdjagara dega,amdjaroungo,amdjemana,amdjemena,amdjiamala,amdnaren,amdnarene,amdo,amdodimahabiba,amdol,amdong,amdorf,amdos,amdoum,amdoun,amdouz,amdrar,amdrar n oumdras,amdros,amduk,amdulai,amdulay,amdullahi,amduntog,amdunu,amduri,amduru,amduru babba,amduru karemi,amdus,ame,ame berento,ame ges,ame tisa,ame zhawar kili,ame-gin,amea,ameaba,ameagle,ameais,ameaka,ameakouro,ameal,amealco,amebesu,amebleve,ameblikofe,ameca,amecac,amecameca,amecameca de juarez,amecanda,amecatitla herradura,amechaji,ameche,amechekope,amechi,amechtrik,amecke,amecourt,amed,amed guya,amedalai,amede,amede koueve,amede kove,amedenta,amedi,amedica,amedika,amedika akuse,amedikakope,amedikukope,amedjikofe,amedjofe,amedjonokou,amedjoume,amedo,amedokai,amedorf,amedou,amedschovhe,amedschowe,amedua,ameduikaw,ameduikor,amedukwe,amedzikope,amedzikorpe,amedzofe,amedzope,ameeda,amefinu,amegade,amegadi,amegadiani,amegble,amegbleve,amegdoul,amegdout,amegeau,amegeca,ameggadi,amegge,ameghan,ameghino,amegiagi,ameglia,ameglo,amegnran,amegnrankope,amegon,amegri,ameh,amehele,amehie,amehlige,amehokorpe,ameiaka,ameide,ameijeira,ameijenda,ameijide,ameijoafas,ameijoeira,ameika,ameikywa,ameil,ameiope,ameira,ameiro,ameis,ameisberg,ameisbichl,ameisbuhel,ameisthal,ameitanimataty,ameitem,ameiti,ameixa,ameixas,ameixeda,ameixede,ameixeira,ameixeraria,ameixiais de cima,ameixial,ameixial-santa vitoria,ameixieira,ameixiosa,ameixoeira,amej,ameja,amejeira,amejgag,amejiogbe,amejove,amejri,ameka,amekampango,amekan tabiki,amekchoud,amekdrane,ameke,ameke isiegbu,ameke ngwo,ameke-abiriba,amekha,amekhin,amekhlij,amekom,amekomfokrom,amekoussikope,amekra,amekraz,amekrou,amekru,amekshoud,ameku,amekupi,amel,amel-sur- letang,amela,amelanda,amelandai,amelandes,amelano manor,amelaous,amelar,amelchenko,amelchino,amelco,amele,amelecourt,ameleke,amelekia,ameleya,amelfino,amelfot,amelgatzen,amelgering,amelhausen,ameli,amelia,amelia beach,amelia city,amelia court house,amelia del retiro,ameliakro,ameliakrou,amelich,amelie,amelie-les bains,amelie-les-bains-palalda,ameliko,amelin,amelingan,amelinghausen,amelino,amelinskiy,ameliopolis,amelith,amelkin,amelkino,amelkis,amellago,amellil,amellouai,amellougui,amelmuiza muizas centrs,ameln,amelo,amelong labeng,amelose,ameloul,amelous,amelreiching,amelsburen,amelscheid,amelsdorf,amelsdorp,amelsen,ameltsayevo,amelu-abam,ameluca,amelung estates,amelunxen,amelusha,amelyakhnova,amelyanovka,amemete,amemga,amemya,amen,amena,amenabar,amenai,amenaiboue,amenam,amenamo,amenati,amend,amendeuix,amendeuix-oneix,amendhokian,amendingen,amendje,amendoa,amendoais,amendoeira,amendoeira de baixo,amendoeira de cima,amendoim,amendolara,amene,amenekui,amenet,amenete,amenetu,ameng,amengamona,amengaricuaro,amengel,amenger,amengingo,amengouingani,ameni,ameni-kend,amenia,amenia union,ameniyos,amennayo,amenntane,ameno,amenoncourt,amenoudji,amenoudji kondji,amenoukpe,amenourou,amenpa,amenso,ament corners,amenta,amentago,amentak,amentane,amente,amentego,amentenha,amentia,amentila,amento kope,amentogo,amenu,amenucourt,amenya,amenyaglo,amenyi opuitumo,amenyoukpe,amenyran,amenzal,amenzel,amepoke,ameqan,amer,amer ain el jenna,amer blanchi,amer el rouihi,amera,amerachir,amerang,ameraoua,amerbach,amerbacherkreut,amerbusch,amerchen,amerchene,amercoeur,amerdingen,amerdlok,amerdoul,amerdout,amere,amereh,amereta,amerete,amerevo,amergou,amergu,amerguya,amerhii,amerhili,amerhisid,amerhlo,ameri,americ,america,america city,america de baixo,america de cima,america dourada,america libre,americahay,american beach,american canyon,american city,american corner,american falls,american fork,american heritage,american lake garden tract,american plaza condominium,american river,american towers condominium,american university park,americana,americaninhas,americankondre,americano,americano brasil,americano do brasil,americanos,americas park,americas unidas,americo alves,americo boavida,americo brasiliense,americo de campos,americo v rech,americus,amerika,amerikaipuszta,amerikanka,amerikapuszta,amerikopoko,amerikpoko,amering,ameringshub,ameriri,amerisco,ameriyeh,amerj arr,amerkhanovo,amerlugen,amermont,amern,amero,amerobe,amerom,amerongen,ameroro,amerovo,amerqan-e tus,amerra,amerrisco,amerry,amers,amersal,amersane,amersdorf,amersfoort,amersham,amersid,amerslek,amerstorf,amersveld,amersvelde,amerta,amertasari,amerthal,amertshofen,amerung,amery,ameryka,amerzgane,amerzi,amerzou,amerzouast,amerzoug,ames,ames cove,ames heights,ames hill,ames lake,amesangeng,amesaouro,amesar,amesauro,amesaza,amesberg,amesbury,amescoa baja,amesdale,amesdat,amesdorf,amesedt,ameshk,amesho,amesioe,amesiu,ameskad,ameskalli,ameskar,ameskar et tahtani,ameskennid,amesker,amesker el fougani,amesker el foukani,amesker el fouqani,amesker et tahtani,amesker et tatani,ameskeslane,ameskhoud,ameskra,ameskroud,amesloh,amesmatert,amesmaterte,amesmsa,amesodjikope,amesoko,amesoku,amesougane,amesoul,amesoza,amespetou,amesreith,amesrhar,amessala,amessan,amessandon,amessangeng,amesschlag,amessou,amessougha,amestajan,amestejan,amesti,amesuzah,amesville,amet,amet niavel,ameta,amete,ametefekofe,ametek,ametekou kope,ametepe korpe,ameterkmakhi,amethi,ametina,ametinhe,ametinho,ametista,ametistas,ametite,ametlla,ametlla de mar,ametlla de merola,ametlla del mar,ameto,ametogan kope,ametonou,ametovina,ametrasse,ametsbichl,amett,amettes,ametu,ametyevo,ameugny,ameur ben houmine,ameur el ain,ameur el aine,ameutovo,ameuvelle,amewawou,amewe,amexayi,ameya,ameyalco,ameyaltepec,ameyaw,ameysin,ameyugo,amez,amezaga,amezaourou,amezar,amezcoa baja,amezdou khsane,ameze,amezlakh,amezodjikope,amezola,amezouast,amezouc,amezoug,amezouj,amezour,amezqueta,amezquita,amezri,amezrou,amezug,amezzaourou,amezzou,amfa,amfahun,amfana,amfar,amfara,amfey,amfia,amfibaboe,amficerova,amfiklia,amfilaty,amfilochia,amfilokhia,amfipolis,amfissa,amfithea,amfitriti,amfitserova,amflei,amfoe,amfrebo,amfreville,amfreville-la-camp,amfreville-la-campagne,amfreville-la-mi-voie,amfreville-les-champs,amfreville-sous-les-monts,amfreville-sur-iton,amfroipret,amfu,amfur,amga,amga-kychata,amgachhi,amgachhiabari,amgachia,amgadao,amgadi,amgah,amgal,amgalang,amgalang bulag,amgalang muchang,amgalanta,amgaleyley,amgama-kouadioukro,amgamwa,amganad,amgane,amgaon,amgar,amgaret,amgatoura,amgawan,amgayan,amgayang,amgbaye,amgbegha,amgdad,amghail,amgham,amghan,amghan-e bala,amgharret,amghata,amgherssa,amghizid,amghoshpara,amgidong,amgilli,amgin,amginskaya,amginskiy,amginskiy perevoz,amginskoye,amgoch,amgoktong,amgolan,amgoreng,amgorin,amgoring,amgouine,amgoundji,amgram,amgu,amguem,amguema,amguene nsfia,amguerite,amguernis,amguhan,amguid,amguilem,amguis,amgun,amguri,amguri bazar,amguyema,amha,amhali al shabana,amhali ash shabanah,amhangi,amhara,amharchine,amharrar,amhat,amhati,amhati kalikapur,amhati sibpur,amhayyah,amherst,amherst center,amherst heights,amherst junction,amherstburg,amherstdale,amhet-bejasi,amhiabe,amhoedong,amhoeri,amholz,amhoten,amhuinnsuidhe,amhult,amhungni,amhur,amhurst,amhuru,amhwari,ami,ami mahalleh,amia,amialka,ami`ad,ami`et,amia,amia akaffou,amiaes de baixo,amiaes de cima,amiagour,amiais de baixo,amiais de cima,amiakro,amiakrou,amial,amialtepec,amiama,amiama gomez,amiamiko,amiamukaw,amiamukor,amian,amian dangri,amianan,amiandos,amiankro,amianou,amianto,amiar,amibasy,amibaxia,amibo,amica,amicalola,amicano,amicano-san pablo,amice,amich-e sad khaneh,amicha,amichar,amiche,amichi,amichow,amicoukro,amicus,amid,amida,amidaji,amidakan,amidara,amide,amide cane,amidika,amido,amidoekor,amidon,amidong,amidzici,amie,amieghomwan,amieira,amieira cova,amieirinha,amieiro,amiel,amiel park,amiele,amiemta,amien,amienkro,amiens,amieva,amiezar,amifontaine,amigdhala,amigdhalea,amigdhaleai,amigdhaleon,amigdhaleonas,amigdhali,amigdhalia,amigdhalias,amigdhalies,amigdhalitsa,amigdhaloi,amigdhalokefali,amigdhalokefalion,amigdhalon,amigdhalos,amignon,amigny,amigny-rouy,amigo,amigol,amigos,amigueiu cambumba,amihama,amihan,amihanan,amihora,amihsien,amii,amiiney,amij,amiju,amike,amikeyevo,amikiyaba,amikiyevo,amiklai,amikol,amikoyaba,amiku,amil,amil banda,amil colony,amil ganda,amila,amilabad,amilale,amilcingo,amildan,amile,amileh,amilema,amiles,amili,amilia,amiligan,amiling,amilir,amillano,amillis,amilly,amilmil,amilo,amilong,amilongan,amilongon,amilpas,amiltepec,amim kalay,amim kelay,amime,amimi,amimiz,amimkalay,amimo,amimos,amimpur,amin,amin afandi sirhan,amin anon,amin aqan,amin bakhsh dahri,amin baqir,amin batani,amin effendi sarhan,amin habib,amin jabu khel,amin jan,amin jan khune,amin kala,amin kalaca,amin kalacha,amin kalachah,amin kalacheh,amin khan,amin khan khosa,amin khan koruna,amin kot,amin lako,amin mareh,amin muhammad dal,amin nahiun,amin shah,amin shah jo goth,amin shatub,amin shaw,amin zai,amina,amina dala,amina number 1,amina number 2,amina number one,amina number two,aminabad,aminabad-e dovvom,aminabad-e hayateh bozorg,aminabad-e now,aminabad-e sangan,aminabad-e shavvaz,aminagan,aminaghan,aminagou,aminah,aminam amanfrom,aminampanga,aminani,aminapa,aminapoke,aminasi,aminaso,aminaviavy,aminbhavi,aminbiri,amincha,aminda,amindaion,amindi,amindoli,amindrabe,amindrakipea,amindravelo,amindrazaka,amindrebatra,amindrenara,amindresanoa,amindrevavo,amindzhankhune,amineh deh,aminei,aminetak,amineva,aminevo,aminevskiy,aming,aminga,amingad,amingaon,amingarh,aminghausen,aminghauser heide,amini,amini abad,amini kili,aminichchiya,aminigboko,aminikeir,aminilla,aminillo,aminit,aminiter,aminiti,aminito,aminjan khaneh,aminjikarai,aminkala,aminkay,aminkhan,aminkot,aminli,aminlu,aminne,aminne bruk,aminneberg,aminneborg,aminnefors,amino,aminohama,aminoko,aminollah chambar,aminovichi,aminovo,aminow,aminpur,aminpur dogran,aminpura,aminsby,aminten,aminteng,amintina,aminto,aminu,aminuis,aminulla-chambar,aminullah cambar,aminullah chambar,aminus,aminweri,aminy,aminya,aminyevo,amio,amioere,amions,amioso,amioso cimeiro,amioso do senhor,amioso fundeiro,amioun,amioune,amioz,amiozinho,amipakwebi,amipoke,amipur,amir,amir `ayub,amir `emran,amir `omran,amir abad,amir abad kiyar,amir abad meibod,amir abad olya,amir abad sofla,amir abad zarand,amir ahmad gul kalle,amir al muminin,amir ali,amir ali khan,amir ali khel,amir ali khokhar,amir aslan,amir ayyub,amir azam,amir bakhah,amir bakhsh,amir bakhsh bhurgari,amir bakhsh ghulla,amir bakhsh isal,amir bakhsh jalbani,amir bakhsh janiali,amir bakhsh jeha,amir bakhsh khaskheli,amir bakhsh khosa,amir bakhsh lashari,amir bakhsh lishari,amir bakhsh machhi,amir bakhsh mahar,amir bakhsh mughairi,amir bakhsh sial,amir bakhsh unar,amir bakhsh wasan,amir banda,amir beglu,amir bekandeh,amir beyg,amir beyg-e `olya,amir beyg-e sofla,amir beyglu,amir bonyad,amir bostaq,amir bunyad,amir chak,amir cheragh`ali,amir dad,amir deh,amir din,amir dizaj,amir drakhan,amir gavabor,amir ghaeb,amir ghayeb,amir gul,amir gul jattak,amir haidriwala,amir hajjilu,amir hajloo,amir hajlu,amir hamza colony,amir hendeh,amir hoseyn khani,amir hussain,amir jan,amir jan kalay,amir jan kelay,amir kala,amir kala galaw,amir kala galow,amir kalasra,amir kalayeh,amir kandeh,amir kandi,amir keyasar,amir khan,amir khan garhi,amir khan kalay,amir khan kelay,amir khan kili,amir khan sakha bail,amir khan skhobai,amir khan-e karim,amir khanlu,amir khanwala,amir khel,amir kheyl,amir khudo,amir khvajeh,amir kia sar,amir kiasar,amir kili,amir kiyasar,amir kola,amir kola kula,amir kot,amir kuh,amir kulasra,amir kund,amir loo,amir mahal,amir mahalleh,amir mahbata,amir mahbatah,amir mene,amir mohammad kor,amir mohammad kowr,amir mohammadkhan kalay,amir mohd-khan,amir mowmenin,amir muhammad,amir muhammad khan,amir muhammad khan kelay,amir muhammad khosa,amir muhammad kor,amir muhammadwala,amir muhammadwali,amir musa,amir nagar,amir ne`man,amir ol `emran,amir ol amar,amir ol momenin,amir ol momenin,amir ol mowmenin,amir ol mow`menin,amir ol omara,amir pai,amir qal`en,amir qavam od din,amir qeshlaq,amir rais,amir rahman,amir rud,amir salar,amir salar-e `olya,amir salar-e sar tang,amir salari,amir sar,amir sara,amir seyf,amir seyt,amir shah,amir shah basti,amir shahwala,amir tavakkoli,amir timur,amir zakareyya,amir zakaria,amir zakariya,amir zakarya,amir zeyn od din,amir-ada\302\277i-yurt,amir-adzhi-yurt,amir-e `olya,amir-e sofla,amir-kende,amir-kort,amir-mukhammed-kor,amir-mukhammedkhan-kalay,amir-muza,amir-ravat,amira,amira ahloka,amira kadal,amirabad,amirabad bhutta,amirabad gusheh,amirabad kaftar,amirabad now,amirabad pain,amirabad vosta,amirabad zaveh,amirabad-e `aliqoli,amirabad-e `olya,amirabad-e babakan,amirabad-e bala,amirabad-e bazi,amirabad-e bidekan,amirabad-e birjand,amirabad-e fenderesk,amirabad-e gareh cheqa,amirabad-e gol andar,amirabad-e gudarzi,amirabad-e gurkash,amirabad-e harigan,amirabad-e jonubi,amirabad-e kapari,amirabad-e kenareh,amirabad-e khan mansur,amirabad-e kohneh,amirabad-e kolachi,amirabad-e kord,amirabad-e kot gorg,amirabad-e meybod,amirabad-e miani,amirabad-e nadar,amirabad-e nader,amirabad-e nazarian,amirabad-e now,amirabad-e pa gach,amirabad-e pain,amirabad-e papi,amirabad-e qal`eh lan,amirabad-e qal`ehlan,amirabad-e razan,amirabad-e robat,amirabad-e sali darreh,amirabad-e sar gach,amirabad-e sarkar,amirabad-e sheybani,amirabad-e shomali,amirabad-e sofla,amirabad-e sukhek,amirabad-e vosta,amirabad-e zavareh,amirabari,amirabdzhan,amiradzhan,amiradzhany,amirah,amirahlu,amiral omara,amiralar,amirallar,amiran,amiran `olya,amiran kot,amiran-e `olya,amiran-e bala,amiran-e pain,amirang,amirangalahy,amirani,amiranlar,amiranlu,amiranom,amirarkh,amirarx,amiras,amirat,amirawa,amiray,amiraya,amirbur,amircan,amirdad,amirdizaj,amirdzhan,amirdzhankalay,amire,amiret,amirgarh,amiri,amiri `olya,amiri bala,amiri-ye `olya,amiri-ye bala,amiri-ye pain,amiri-ye pa`in,amirim,amirini,amiriya,amiriyah,amiriyeh,amiriyeh hamzehloo,amiriyeh-ye qatar boneh,amirjan kalay,amirjan kelay,amirkala,amirkala-galau,amirkhan,amirkhankalay,amirkhanly,amirkhel,amirkher,amirkheyl,amirkheyl,amirkot,amirli,amirlu,amirmahmud,amirnal,amirni,amirolmomenin,amiroro,amirova,amirovka,amirovo,amirovskiy,amiroy,amirpet,amirpur,amirpur kanakka,amirpur mangan,amirpur sadat,amirpur sahu,amirpur sarbana,amirpur surbana,amirpura,amirthakaly,amirturba,amiru,amirud,amiruh,amirvan,amirvar,amirvarli,amirvarly,amirwala,amirwala banda,amirwala dera,amirwali,amirweri,amirxanli,amirzada,amirzeitli,amirzeydli,amis,amis dua,amis empat,amis enam,amis lima,amis satu,amis tiga,amisakrom,amisala,amisano,amisare,amisbarang,amisco,amise,amisfield town,amish,amishapara,amishi,amishibiebokaso,amishiro,amishta,amishti,amisi,amisiana,amisighei,amisinon,amisioui,amismiz,amisse,amissi,amissiakro,amissville,amistad,amistad acres,amistad village,amistiya,amisuku,amisus,amit,amit kendeh,amita,amita purwa,amitang,amite,amiti,amitie,amitijihy,amitikulu station,amitina,amiting,amitirigala,amito,amitovo,amitrovka,amitty ville,amity,amity cross,amity gardens,amity hall,amity harbor,amity hill,amity house,amity point,amityville,amiudal,amiun,amiune,amiure,amivelondaza,amivorano,amixco,amixtlan,amiyadi,amiyapur,amiyi,amiyima,amiyo,amiyonsu,amiza,amizade,amizici,amizmiz,amizour,amizu,amja,amja lebes,amjad ali khan kalle,amjad park,amjadabad,amjadhat,amjadiyeh,amjadiyeh-ye bala,amjadiyeh-ye pain,amjadong,amjaeri,amjaibibu,amjani,amjapur,amjaz,amjcici,amjenuwa,amjerane,amjeye,amjeziyeh,amjhera,amjhor,amjhupi,amji ji goth,amjil,amjo,amjodong,amjogxung,amjondong,amjong,amjongni,amjoud,amjoudhane,amjouj,amjudzu ciems,amjuktong,amjungni,amka,amkaleh,amkarta,amkathalia,amkatla,amkaz,amkchoud,amkel,amkhawa,amkhera,amkheri,amkhinichi,amkhola,amkholi,amkhovaya,amkhovitsa,amkhovitsy,amkhowa,amkhut,amkhuta,amki,amkilange,amkindin,amkindine,amkiten ouled batnik,amkiye,amkouk,amkoundro,amkov,amkpaliga,amkraz,amkrouz,amkrovo,amkusukhta,amkwaha,amkwana,amla,amla para,amla saki,amlab,amlaba,amlabari,amlach,amladoba,amladoba-abitcha,amladoba-yana,amlaf,amlagachhi,amlagora,amlah,amlaha,amlahah,amlai,amlak,amlak-e qoli tappeh,amlak-e zarneh,amlakhi,amlaki,amlakpo,amlakro,amlakumene,amlala,amlame,amlan,amland,amlang,amlao,amlaouk,amlapura,amlash,amlataria,amlauha,amlaung,amle,amleang,amleke sohag,amlekganj,amlekhagang,amlekhgang,amlekhganj,amlekhsagunz,amlesa,amlesh,amleshevo,amleshevskiye vyselki,amlevi,amleyn gardens,amli,amli keshabpur,amlia,amlichukai,amliden,amliheh,amlike athar shah,amlike pohela,amlike sohag,amlimay,amlin,amlingstadt,amlipadar,amliq,amlish,amlishagen,amlivi,amliyara,amloda,amloh,amlok,amlok banr,amlok darra,amlok tangai,amlokdarra,amloko mene,amlong,amlookdara,amlos,amlouggui,amluagan,amluibann,amluk,amluk banda,amluk kacay,amluk katsay,amlukbanr,amlukdarra,amluke,amlukkachay,amluknar,amluko mene,amluktal banda,amlwch,amlyshevka,amm nahr,amm surrah,amma,amma khel,ammabocan,ammacian,ammacio,ammadhies,ammagacan,ammagan,ammaia,ammain,ammainen,ammajhi,ammal,ammala,ammalapadu,ammaman,ammameh,ammameh-e bala,ammameh-e pain,ammameh-ye deh-e bala,ammameh-ye deh-e pain,amman,ammanabrole,ammanabrolu,ammanamulla,ammanawa,ammanazar,ammandi,ammanford,ammani chattram,ammanitchatram,ammannsville,ammans crossing,ammansaari,ammanthanaveli,ammanullakhan kalay,ammapatam,ammapatnam,ammapattinam,ammapet,ammapettai,ammapur,ammar,ammar ain hanna,ammar rhouiyne,ammara,ammarnas,ammarp,ammaryd,ammas,ammaseng,ammasennikik,ammassalik,ammassangeng,ammassenkik,ammassivik,ammasu,ammat,ammate,ammathur,ammatlar,ammatotam,ammatsa,ammatti,ammavaripalem,ammawhpai,ammdalai,amme,ammeberg,ammebergs zinkgruva,ammegge,ammehghin,ammeine,ammelacker,ammelbruch,ammeldingen,ammeldingen an der our,ammeldingen bei neuerburg,ammelgosswitz,ammelhede,ammelhofen,ammeln,ammeloe,ammelsdorf,ammelshain,ammelstadt,ammenalli,ammenas,ammend,ammendale,ammendorf,ammendrup,ammenhausen,ammensen,ammer,ammer khan kalle,ammerasen,ammerbach,ammerfeld,ammerich,ammerland,ammerlugen,ammern,ammern oberschlesien,ammerndorf,ammeroid,ammerrung,ammerschweier,ammerschweir,ammerschwihr,ammersdorf,ammersreit,ammersricht,ammerssluis,ammerstetten,ammerstol,ammersum,ammerswurth,ammerthal,ammertsweiler,ammertzwiller,ammerup,ammerzoden,ammerzwiller,ammes,ammesbol,ammeville,ammfagya,ammi moussa,ammiad,ammioz,ammie,ammikam,amminadav,ammingarp,ammitsbol,ammivaittan,ammnad,ammobocan,ammoboocan,ammochino,ammochostos,ammochostus,ammoeseoe,ammoke,ammokhorion,ammokhostos,ammolokhos,ammon,ammon ford,ammonite,ammonke,ammons,ammonschonbronn,ammori,ammos,ammos meryion,ammosova,ammotopos,ammoudari,ammoudarion,ammoudhara,ammoudharai,ammoudhares,ammoudhari,ammoudharion,ammoudheron,ammoudhia,ammoudia,ammouguer,ammouliani,ammour achasta,ammovounon,ammpenkrom,ammubuan,ammuduwa,ammulfaghe,ammundtorp,ammuste,ammusu,ammuta,ammuzagi,amnae,amnakchon,amnamdong,amnamni,amnari,amnaru,amnas,amnat,amnat charoen,amnator,amnatos,amnawar,amnay,amnayyou,amne,amne-en-champagne,amnebo,amnerud,amnes,amnesanaek,amneville,amni,amnicon falls,amnieh deh,amnik,amnongni,amnot,amnun,amnura,amnyongni,amnzgar,amo,amo bakir,amo kach,amo-ebilene,amoa,amoaben,amoabin,amoadu,amoafo,amoaful,amoahivahy,amoako,amoakokurom,amoaku,amoakukrom,amoakuswaso,amoakutu,amoakwa,amoaman,amoamang,amoamo,amoana,amoanda,amoannda,amoararatabe,amoares,amoaso,amoatsy,amoay,amoba,amobakr,amobetsu,amobia,amocal,amochayevskiy,amoco,amod,amoda,amoda uncuzana,amodabad,amodalg,amodalga,amodalgo,amodeo,amodeo bay,amodhorom,amodi,amodo,amodou,amodoukoute,amodovo,amodu,amodzh,amoebo,amoedo,amoeiro,amoeja,amoemta,amoentai,amoerang,amoetai,amofia,amofiloco,amofu,amogalantu-khid,amogale,amogawen,amogay,amogolambu,amogolang-batur,amogolangoin chzhisa khid,amogolangoyn chzhisa khid,amogolangtu khiit,amogolangyin chzhisa khid,amogorebisi,amogral,amoguis,amogun,amohijafy,amohola,amohsen,amoi,amoi patha,amoia,amoiboa,amoichi,amoigbe,amoikog,amoikro,amoin,amoina,amoingan,amoingon,amoinha nova,amoinha velha,amoior,amoissa,amoito,amoj,amoje,amoji,amojileca,amok,amoka,amokaha,amokan,amokato,amokchongni,amokhor,amokireny,amoko,amokoeni,amokofe,amokokopai village,amokonazy,amokotitoka,amokoty,amokouabenakroum,amokouy,amoku,amokuni,amokwa,amokwe,amokwi,amokwo,amokwo okposieshi,amokwu,amol,amola,amola de ocampo,amola del piano,amola di piano,amola faca,amolac,amoladeras,amoladeros,amolah,amolanga,amolar,amolatar,amole,amolengu,amoles,amoli,amolia,amoliani,amolillo,amolio,amolitar,amolitos,amoloc,amologan-bator,amoloig,amolokhos,amoloko,amolon,amolongin,amolonskiy poselok,amolor,amolota,amolotar,amoloye,amoltepec,amoltepec san cristobal,amoltern,amolucan,amom,amom ping,amoma,amomaso,amome,amomonting,amomorso,amomoting,amomumet,amon,amon delbroucq,amon epahim,amon kasa bisa,amon kasa villages,amon munthali,amon ncho,amon seka,amon shahwani,amon yangoran,amon yapo,amona,amonaklios,amonakliou,amonalnaya,amonash,amonate,amonau,amoncha,amoncourt,amondans,amondara,amonde,amondo,amone,amone bie,amoneburg,amonenhof,among,amongabi,amongan,amongena,amongena 1,amongena 2,amongena dua,amongena satu,amonggedo,amongo,amongrogo,amongsgruen,amonho,amoni,amoni kapansula,amoni mukando,amonica,amonice,amonietau,amoniete,amonikakinei,amonikha,amonikrom,amonines,amonis,amonizza,amonkro,amonly,amono,amonoy,amonsgrun,amonshaykhi,amonsheykhi,amonskiy,amontada,amontana,amontay,amontidom,amonting,amontoado,amontoc,amonye,amonzhul aul,amoonguna,amoor,amopara,amopaya,amoqabad,amoquinto,amor,amor buyantuin hurie,amor buyantiin khure,amor buyantuin-fure,amor de dios,amor-buyantuyn-khure,amora,amora de baixo,amora gedel,amora genda,amora ghadel,amora ghedel,amora preta,amoraghede,amoramor,amoramor daya,amorani,amorbach,amore,amore aduway,amorebiela,amorebieta,amorebieta hacienda,amoreira,amoreira cimeira,amoreira da gandara,amoreira de cima,amoreira de repolao,amoreira fundeira,amoreiras,amoreiras-gare,amoreirinha,amoreo,amores secos,amoret,amorevieta,amorgos,amorha,amori,amoriakro,amorie,amoriki,amorim,amorin,amorinha,amorinopolis,amorion,amorita,amorkamp,amorkot,amorkovo,amoro,amoroce,amorofikroum,amorogtong,amorome,amoron,amoronala,amorondrano,amoronimanga ambony,amoronjia-orangea,amorontevana,amorontmanga-ambony,amorosa,amorosi,amoroso,amoroto,amorots,amorots-succos,amoroy,amortak,amoru,amorupos,amory,amoryianoi,amorzabad,amorzidehabad,amos,amos kalunga,amos kapeta,amos mill,amos mwelwa,amos quarries,amos zakocera,amosa,amosabo,amoschina,amoscia,amosema,amosen,amosfield,amoshof,amosima,amosime,amosino,amoskovo,amoslog,amoslohe,amoso,amosov,amosova,amosova gora,amosovka,amosovo,amosovskaya,amosried,amoss,amossa,amosse,amostown,amosu,amosua,amosuakwanta,amosun,amoswaso,amot,amot pa modum,amot poste,amot-modum,amotahale,amotalale,amotalate,amotane,amotanga,amotape,amotena,amotes,amotfors,amotherby,amoti,amotire,amoto,amotochil,amotra,amots kapell,amotsa,amotsfors,amotso,amotte,amotz,amou,amou akpekpe,amou kope,amou naddi ait amar,amou oblo,amouadranyi,amouakpa,amouatene,amouble,amoucha,amouchat,amoucheir,amouchou,amoud,amoud al ridjal,amouda,amoudater,amoudator,amoudd,amoudel morfain,amoudhara,amoudhari,amoudji,amoudokoe,amoudu,amouema,amouggeur,amougguer,amough,amougies,amougoeur nait ouzine,amouguear,amouguer,amougueur,amougueur nait merghad,amougueur-n-ait merrhad,amouguir,amouin daoud,amoukondji,amoukope,amoukoukro,amoukrou,amouks,amoulecodji,amouli,amounakro,amounou,amour,amoura,amourdale,amoure,amouri,amourion,amouroubou,amouryeles,amouryellai,amouryieles,amouryiellai,amourzouk,amousou,amousou kope,amousoukofe,amousoukope,amousouvi,amoussa,amoussale,amoussime,amoussokope,amoussou,amoussoubrassouhoue,amoussoukofe,amoussoulou,amout,amout sougour,amouta,amoutchou,amouti,amoutive,amouz,amouzata,amouzoukope,amovhamn,amovici,amowgay,amowi,amowj,amowrtak,amoy,amoya,amoyaw,amoyhamn,amoyloi,amoyu,amozi,amozoc,amozoc de mota,amozoyahua,ampa,ampa ampa,ampa kolak,ampaardaakh,ampabame,ampabame bebu,ampabami,ampabane,ampaca,ampacama,ampadan,ampadang,ampadar,ampafenala,ampafitatra,ampagala,ampah,ampaha,ampahaka,ampahampana,ampahana,ampaharamanira,ampahatany,ampahatra,ampahia,ampahibato,ampahibe,ampahibe nord,ampahidroy,ampahijoloka,ampahimanga,ampahindambo,ampahitrosy,ampahitsy,ampahivola,ampahjati,ampaho,ampahomanitra,ampahorama,ampahta uakubpur,ampaidranovato,ampaim,ampaimaiky,ampaipaike,ampaisokely,ampajango,ampaka,ampakabo,ampakamam,ampakling,ampako,ampako-ambalarano,ampakobe,ampakobe i,ampakrom,ampalakopa,ampalam,ampalang,ampalas,ampalavanapokkunai,ampalavanpokkanai,ampalaza,ampaldento,ampalea avaratra,ampalea sud,ampalibe,ampalimasina,ampalimasira,ampalioh,ampaliok,ampallo,ampalu,ampaly,ampam,ampama,ampamaha,ampamaho,ampamahotra,ampamainty,ampamakia,ampamakiambato,ampamakiazo,ampamakona,ampamam,ampamamba,ampamandrika,ampamanosaka,ampamanta,ampamantanana,ampamarabe,ampamary,ampamata,ampamata ambony,ampamata nord,ampamata sud,ampamatabe,ampamatahety,ampamataimaho,ampamatakely,ampamatanetiky,ampamatoa,ampamatohetika,ampambe,ampambindroka,ampambokely,ampambolotara,ampamehena,ampamelovelo,ampamintanana,ampaminty,ampamitanana,ampamoa,ampamoabe atsimo,ampamofo,ampamofofo,ampamoho,ampamolana,ampamoloana,ampamolora,ampamontsy,ampamonty,ampamoriana,ampamosira,ampamoty,ampampamena,ampamy,ampan,ampana,ampana kanana,ampanafanana,ampanafaovana,ampanafoly,ampanagala,ampanakana,ampanakary,ampanalana,ampanambato,ampanambe,ampanamboha,ampanamo,ampananaborana,ampananana,ampananatsovana,ampanandravo,ampanangana,ampanangana ambany,ampananganana,ampanangananjia,ampananganapotaka,ampananina,ampananira,ampananirakely,ampanantanana,ampanantsova,ampanantsovana,ampanaovamarika,ampanaovato,ampanaovatsokay,ampanaperandotsy,ampanaperandrotsy,ampanarena,ampanarianjono,ampanarivo,ampanarivomasina,ampanasa,ampanasamaina,ampanasana,ampanasana ambevy,ampanasana-anandrea,ampanasana-andrea,ampanasanovy,ampanasantomboka,ampanasy,ampanataovana,ampanatoavana,ampanatsovana,ampanavoana,ampanbelak,ampandalova,ampandatsiana,ampandilo,ampandimana,ampandomana,ampandra,ampandra 1,ampandra 2,ampandra atsimo,ampandra avaratra,ampandra ii,ampandra nord,ampandra-morarano,ampandrabe,ampandrable,ampandrahely,ampandrakely,ampandrakosy,ampandralava,ampandralava avaratra,ampandralava nord,ampandralava sud,ampandramaika,ampandramainy,ampandramalaza,ampandramamaha,ampandramamaho,ampandramamy,ampandramana,ampandrambato,ampandrambe,ampandrambe ambony,ampandrambe nord,ampandrambe sud,ampandramena,ampandramena nord,ampandramena sud,ampandramiriatra,ampandramiritra,ampandramitsetaka,ampandramitsetaky,ampandrana,ampandrana est,ampandrana ouest,ampandrandava,ampandrano,ampandrasoa,ampandrasoatanimbary,ampandratoka,ampandratolona,ampandratolony,ampandravelo,ampandriakilandy,ampandriamboro,ampandrianakanga,ampandrianosy,ampandriantsara,ampandrihosy,ampandrikilandy,ampandririnina,ampandroamborona,ampandroangisa,ampandroatraka,ampandroronakazo,ampandrotrarana,ampandula,ampanefena,ampanefeno,ampanefy,ampanendahana,ampanenitra,ampanenjanana,ampanetoana,ampanety,ampaneva,ampang,ampang bahru,ampang bharu new village,ampang resettlement,ampangabe,ampangadiamby,ampangadihantany,ampangala,ampangalakongo-midiso,ampangalana,ampangalana atsimo,ampangalana avaratra,ampangamena,ampangan,ampangarina,ampangidraoka nord,ampangidraoka sud,ampangidraty,ampangkubu,ampangkudu,ampangorina,ampangorinana,ampangoro,ampangpatai,ampangpetas,ampangseloemoe,ampangselumu,ampani,ampanibe,ampanidralambo,ampanifora,ampanihibe,ampanihikely,ampanihira,ampanihy,ampanihy andrefana,ampanihy atsinanana,ampanihy est,ampanihy ouest,ampanikanasoa,ampanikely,ampanimidngana,ampanimiongana,ampanindahana,ampanio,ampanisara,ampanisaro,ampanisiana,ampanitoa,ampanivoany,ampanivokoka,ampank,ampankolak,ampanlolat,ampano,ampanobe,ampanofolaka,ampanokely,ampanolahamirafy,ampanolahamiraty,ampanomaro,ampanomiongana,ampanompia,ampanonga,ampanopia,ampanorenana,ampanorobe,ampanotaovana,ampanotoamaizina,ampanotoana,ampanotoka,ampanotokana,ampanovananavazo,ampanovohana,ampansie,ampanto,ampantrambe,ampanu,ampany,ampaopao,ampapamena,ampapamenabe,ampar,ampara,amparaan,amparaa,amparaarkhu,amparaes,amparafaka,amparafanaka,amparafara,amparafaravola,amparafaravolakely,amparafarovola,amparahitsa,amparai,amparaky,amparambato,amparambatomalama,amparamena,amparaneno,amparanes,amparani,ampararano,ampararanokely,amparatsiraka,ampareno,ampares,ampari,ampariaky,amparibe,amparibohitra,amparifandina,amparihibe,amparihikambana,amparihikaolo,amparihikely,amparihilava,amparihilava andrefana,amparihilava est,amparihimahitsy,amparihimaiky,amparihimaina,amparihimaloto,amparihimanga,amparihimanonga,amparihimaromaso,amparihimbalala,amparihimbohitra,amparihimbozaha,amparihimena,amparihimikimbo,amparihimpony,amparihindrainikotovao,amparihingidro,amparihinkaolo,amparihinkisoa,amparihinorainikotova,amparihirano,amparihiritra,amparihisoa,amparihisoa afovoany,amparihisoa atsimo,amparihisoa avaratra,amparihisoa centre,amparihisoa nord,amparihisoa sud,amparihitany,amparihitelo,amparihitsirihitra,amparihitsokatra,amparihivato,amparihivola,amparihy,amparihy atsinanana,amparihy est,amparimahitsy,amparimay,amparimbohitra,amparimena,amparimiaiky,amparimiaky,amparingalava,amparinho,amparira,amparita,amparitsoka,amparity,amparo,amparo agua tinta,amparo caridad,amparo da serra,amparo de sao francisco,amparo do sao francisco,amparo do sitio,amparo ii,amparo y caridad,amparofaravola,amparomay,amparopotsy,amparro da serra,ampary,ampary-fihaonana,ampas,ampas pampa,ampasa,ampasamadinika,ampasambamzimba,ampasambazimba,ampasambola,ampasampoana,ampasandoaka,ampasandrabehasina,ampasandrasoa,ampasandravaonarivo,ampasanimalo,ampasatokana,ampascachi,ampasianivo,ampasibajina,ampasibaria,ampasibe,ampasibe atsinanana,ampasibevihy,ampasibitika,ampasifasy,ampasiginera,ampasika,ampasikely,ampasiketraky,ampasikibo,ampasilava,ampasimadinika,ampasimadinika-manambolo,ampasimadinikakely,ampasimadraika,ampasimadrodra,ampasimahamasina,ampasimahanoro,ampasimahatera,ampasimahavelo,ampasimaika,ampasimaiky,ampasimaina,ampasimainty,ampasimajina,ampasimalaza,ampasimalemy,ampasimaleotra,ampasimaly,ampasimandoatra,ampasimandroaka,ampasimandroatsy,ampasimandroro,ampasimandrorona,ampasimanera,ampasimaneva,ampasimanga,ampasimanilika,ampasimanitra,ampasimanjeva,ampasimanolotra,ampasimanoro,ampasimanory,ampasimarina,ampasimariny,ampasimasava,ampasimasay,ampasimatera,ampasimaty,ampasimavo,ampasimazava,ampasimbaria,ampasimbary,ampasimbazaha,ampasimbazimba,ampasimbe,ampasimbe sud,ampasimbengy,ampasimbola,ampasimbola i,ampasimbory,ampasimeloka,ampasimena,ampasimirahavavy,ampasimirehoka,ampasimitera,ampasimootsy,ampasimpilily,ampasimpohy,ampasimpolaka,ampasimpotsy,ampasimpotsy-gara,ampasina,ampasina-maningory,ampasinambo,ampasinamboakely,ampasinambokely,ampasinamoo,ampasinanio,ampasinantenina,ampasindava,ampasindova,ampasindrano,ampasindrasoa,ampasinindriambola,ampasipitily,ampasipotsy,ampasira,ampasiray,ampasiria,ampasitanantanana,ampasitanatanana,ampasitanjona,ampasitapaka,ampasitelo mahatalaky,ampasitrozona,ampasitsilavitra,ampasitsiriry,ampasivelona,ampass,ampass unterdorf,ampasy,ampasy nahampoana,ampata,ampatafana,ampataka,ampatakamanitra,ampatakamaroreny,ampatakana,ampatano,ampatifaty,ampatiholosy,ampatika,ampatilla,ampatiolobe,ampatiolomasay,ampatipatiky,ampatka,ampatkorong,ampatleng,ampatnegri,ampato,ampatobe,ampatomandra,ampatotramalona,ampatramasy,ampatrambe,ampatrambo,ampatsapanady,ampatsinakoho,ampatsivola,ampatso,ampatsy,ampatuan,ampaturi,ampaty,ampaya,ampayao,ampayoc,ampayon,ampaza,ampazony,ampe,ampe south,ampeampe,ampeang,ampedu,ampefirano,ampefy,ampegama,ampegoc,ampeha,ampel,ampel 1,ampel 2,ampel 3,ampel dua,ampel jaya,ampel satu,ampel tiga,ampel wetan,ampelakia,ampelan,ampelantuk,ampelaraika,ampelas,ampelawas,ampelbanjar barat,ampelbanjar timur,ampeldento lor,ampeldentokrajan,ampeleia,ampeleiai,ampeleies,ampelgading,ampelgading dua,ampelgading empat,ampelgading satu,ampelgading tiga,ampelia,ampeliko,ampelikon,ampelikou,ampelinesana,ampelkrajan,ampelochori,ampelochorion,ampeloe,ampelon,ampelonas,ampelos,ampelreja,ampelsari,ampelsarikrajan,ampelu,ampelwulung lor,ampen,ampenan selatan,ampenang,ampenekuro,ampeng,ampengoka,ampengoke,ampeni,ampenican,ampenkro,ampentem,ampeny,ampenyi,amper,amper khan ngern,ampera,ampera 1,ampera satu,amperah,amperduri,ampere,amperifery,ampermoching,amperpettenbach,ampertshausen,ampes,ampesiem,ampetsa,ampetsapetsa,ampety,ampewela,ampeyo,ampezzo,ampfelbronn,ampfelwang,ampfenham,ampferbach,ampfing,ampflwang,ampfurth,amphawa,amphil,amphion,amphitheater,amphitheatre,ampho,ampho ban pho,amphoe akat amnuai,amphoe amnat charoen,amphoe amphawa,amphoe ao luek,amphoe ao luk,amphoe aranpradbet,amphoe aranyaprathet,amphoe at samat,amphoe ba cho,amphoe bamnet narong,amphoe ban aranyaprathet,amphoe ban bang talat,amphoe ban bueng,amphoe ban bung,amphoe ban chang,amphoe ban chiang dao,amphoe ban dan,amphoe ban dan lan hoi,amphoe ban don,amphoe ban dung,amphoe ban haet,amphoe ban han,amphoe ban hong,amphoe ban kha,amphoe ban khai,amphoe ban khemmarat,amphoe ban khlan,amphoe ban khok,amphoe ban khwao,amphoe ban kluai,amphoe ban krai,amphoe ban laem,amphoe ban lamung,amphoe ban lat,amphoe ban luang,amphoe ban lueam,amphoe ban mae sot,amphoe ban mai chaiyaphot,amphoe ban makham,amphoe ban meung,amphoe ban mi,amphoe ban mo,amphoe ban muang,amphoe ban na,amphoe ban na doem,amphoe ban na kae,amphoe ban nang sata,amphoe ban pak thong chai,amphoe ban phaeng,amphoe ban phaeo,amphoe ban phai,amphoe ban phon thong,amphoe ban phong thong,amphoe ban phraek,amphoe ban phu,amphoe ban pong,amphoe ban prok,amphoe ban rai,amphoe ban sang,amphoe ban sao,amphoe ban tak,amphoe ban takhun,amphoe ban tha kham,amphoe ban tha uthen,amphoe ban thaen,amphoe ban thuan,amphoe ban yang talat,amphoe bang ban,amphoe bang bo,amphoe bang bua thong,amphoe bang hia,amphoe bang kadi,amphoe bang kaeo,amphoe bang kapi,amphoe bang khan,amphoe bang khen,amphoe bang khla,amphoe bang khon thi,amphoe bang khun thian,amphoe bang krathum,amphoe bang kruai,amphoe bang lamung,amphoe bang len,amphoe bang mun nak,amphoe bang nam prieo,amphoe bang pa han,amphoe bang pa-in,amphoe bang pakong,amphoe bang phae,amphoe bang phai nat,amphoe bang phlat,amphoe bang phli,amphoe bang phra,amphoe bang pla,amphoe bang pla ma,amphoe bang rachan,amphoe bang rak,amphoe bang rakam,amphoe bang sai,amphoe bang sao thong,amphoe bang saphan,amphoe bang saphan noi,amphoe bang wai,amphoe bang yai,amphoe bangkok noi,amphoe bangkok yai,amphoe banphot phisai,amphoe banpot pi sai,amphoe barnbhot bhi sai,amphoe benchalak,amphoe betong,amphoe beung na rang,amphoe bhayuha giri,amphoe bhen,amphoe bhimunmangsaharn,amphoe bhonbhisai,amphoe bhondhohng,amphoe bhra pradeng,amphoe bhra seng,amphoe bhromburi,amphoe bhudhaisong,amphoe bhuwieng,amphoe bia sad,amphoe bo phloi,amphoe bo thong,amphoe borabu,amphoe bua lai,amphoe bua yai,amphoe buachet,amphoe bueng bun,amphoe bueng khong long,amphoe bueng samakkhi,amphoe bun yeun,amphoe bun yoen,amphoe bun yun,amphoe bung,amphoe bung bun,amphoe bung kan,amphoe bung khla,amphoe bung rai,amphoe buntharik,amphoe cha hom,amphoe cha-am,amphoe cha-uat,amphoe chae hom,amphoe chai badan,amphoe chai buri,amphoe chai prakan,amphoe chai wan,amphoe chaiya,amphoe chaiyo,amphoe chakkarat,amphoe chaloem phra kiat,amphoe chamni,amphoe chana,amphoe chanea,amphoe chang klang,amphoe changhan,amphoe chanthuek,amphoe chanuman,amphoe charoen sin,amphoe chat trakan,amphoe chathing phra,amphoe chatturat,amphoe chaturaphak phiman,amphoe chaturat,amphoe chawang,amphoe che hom,amphoe chian yai,amphoe chiang dao,amphoe chiang kham,amphoe chiang khan,amphoe chiang khaung,amphoe chiang khong,amphoe chiang khwan,amphoe chiang muan,amphoe chiang rai,amphoe chiang saen,amphoe chiang yuen,amphoe chieng kum,amphoe cho-airong,amphoe chohm dhohng,amphoe chok chai,amphoe chom bueng,amphoe chom bung,amphoe chom phra,amphoe chom thong,amphoe chomtong,amphoe chon daen,amphoe chorache sam phan,amphoe chulabhorn,amphoe chulaphon,amphoe chum phae,amphoe chum phuang,amphoe chum saeng,amphoe chum seng,amphoe chum ta bong,amphoe chumphon buri,amphoe chun,amphoe damnoen saduak,amphoe dan chang,amphoe dan khun thot,amphoe dan makham tia,amphoe dan sai,amphoe dan zai,amphoe den chai,amphoe det udom,amphoe det udon,amphoe dha kham,amphoe dha khanohn,amphoe dha rong jang,amphoe dha udhen,amphoe dhab put,amphoe dhaihmoeang,amphoe dheung,amphoe dhung song,amphoe doem bang,amphoe doembang nangbuat,amphoe doi lo,amphoe doi saked,amphoe doi saket,amphoe dok kham tai,amphoe don chan,amphoe don chedi,amphoe don mot daeng,amphoe don phut,amphoe don sak,amphoe don tan,amphoe don tum,amphoe dong charoen,amphoe dong luang,amphoe doy saket,amphoe dusit,amphoe erawan,amphoe fa yat,amphoe fak tha,amphoe fang,amphoe fao rai,amphoe gong,amphoe hang chat,amphoe hang dong,amphoe hankha,amphoe hat samran,amphoe hat yai,amphoe hlup,amphoe hnohng weng,amphoe hoht,amphoe hot,amphoe hua hin,amphoe hua pho,amphoe hua sai,amphoe hua taphan,amphoe huai khot,amphoe huai krachao,amphoe huai ling tok,amphoe huai luang,amphoe huai mek,amphoe huai nua,amphoe huai phueng,amphoe huai rat,amphoe huai thalaeng,amphoe huai thap than,amphoe huai yot,amphoe huaihnoea,amphoe huay kaew,amphoe in buri,amphoe jaiburi,amphoe jieng dao,amphoe jieng gam,amphoe jieng kan,amphoe jieng khohng,amphoe jienggan,amphoe jum seng,amphoe kabang,amphoe kabin buri,amphoe kae dam,amphoe kaeng hang maeo,amphoe kaeng khlo,amphoe kaeng khoi,amphoe kaeng khro,amphoe kaeng krachan,amphoe kaeng sanam nang,amphoe kalasin,amphoe kamalasai,amphoe kamnoed nopphakhun,amphoe kamphaeng phet,amphoe kamphaeng saen,amphoe kampong chehe,amphoe kanchanadit,amphoe kantang,amphoe kantharalak,amphoe kanthararom,amphoe kantharawichai,amphoe kanu,amphoe kao liao,amphoe kap choeng,amphoe kapoe,amphoe kapong,amphoe kasat wisai,amphoe kasem sima,amphoe kaset sombun,amphoe kaset wisai,amphoe kathu,amphoe kau chang,amphoe kau lanta,amphoe khaen dong,amphoe khai bang rachan,amphoe kham cha-i,amphoe kham khuan kaeo,amphoe kham muang,amphoe kham sakae saeng,amphoe kham ta kla,amphoe kham thale so,amphoe khamong,amphoe khan ngoen,amphoe khanngeun,amphoe khanom,amphoe khanu,amphoe khanuworalak buri,amphoe khao chai son,amphoe khao chakan,amphoe khao chamao,amphoe khao din,amphoe khao khao,amphoe khao khitchakut,amphoe khao kho,amphoe khao phang krai,amphoe khao phanom,amphoe khao saming,amphoe khao wong,amphoe khao yai,amphoe khao yoi,amphoe khau khaw,amphoe khemmarat,amphoe khemrat,amphoe khian sa,amphoe khiri mat,amphoe khiri rat nikhom,amphoe khlong cha un,amphoe khlong hat,amphoe khlong hoi khong,amphoe khlong khlung,amphoe khlong khuean,amphoe khlong krachaeng,amphoe khlong lan,amphoe khlong luang,amphoe khlong phon,amphoe khlong san,amphoe khlong tan,amphoe khlong thom")mphoe khlong yai,amphoe khlung,amphoe kho wang,amphoe khok charoen,amphoe khok khwai,amphoe khok pho,amphoe khok pho chai,amphoe khok pip,amphoe khok samrong,amphoe khok si suphan,amphoe khok sung,amphoe khon buri,amphoe khon san,amphoe khon sawan,amphoe khong,amphoe khong chai,amphoe khong chiam,amphoe khu mueang,amphoe khuan don,amphoe khuan kalong,amphoe khuan khanun,amphoe khuan niang,amphoe khuang nai,amphoe khukhan,amphoe khulu,amphoe khun han,amphoe khun yuam,amphoe khwao sinarin,amphoe king tha chang,amphoe klaeng,amphoe klai,amphoe klang mueang,amphoe ko chan,amphoe ko chang,amphoe ko kha,amphoe ko kho khao,amphoe ko kut,amphoe ko lanta,amphoe ko pha ngan,amphoe ko pha-ngan,amphoe ko samui,amphoe ko si chang,amphoe ko yao,amphoe koh sa-mui,amphoe kohjang,amphoe kohlanta,amphoe koksamui,amphoe kong krai lat,amphoe kong ra,amphoe kosum phisai,amphoe kotabaru,amphoe kra buri,amphoe kranuan,amphoe krasae sin,amphoe krasang,amphoe krok phra,amphoe krong pinang,amphoe krung kao,amphoe ku kaeo,amphoe kuchinarai,amphoe kui buri,amphoe kukhan,amphoe kumbhawapi,amphoe kumphawapi,amphoe kusuman,amphoe kut bak,amphoe kut chap,amphoe kut chum,amphoe kut kao,amphoe kut khao,amphoe kut khaopun,amphoe kut rang,amphoe la-ngu,amphoe la-un,amphoe lae,amphoe laem ngop,amphoe laem sing,amphoe lahan sai,amphoe lam luk ka,amphoe lam phraya,amphoe lam plai mat,amphoe lam sonthi,amphoe lam thamen chai,amphoe lam thap,amphoe lam thuan,amphoe lamae,amphoe lamduan,amphoe lampam,amphoe lan krabue,amphoe lan saka,amphoe lang suan,amphoe lao khwan,amphoe lao sua kok,amphoe lap lae,amphoe lat bua khao,amphoe lat bua luang,amphoe lat hlumkeo,amphoe lat kabang,amphoe lat krabang,amphoe lat lum kaeo,amphoe lat yao,amphoe laung,amphoe li,amphoe loeng nok tha,amphoe lom kao,amphoe lom sak,amphoe long,amphoe lue amnat,amphoe lumphuk,amphoe lup,amphoe ma yo,amphoe mae chaem,amphoe mae chai,amphoe mae chan,amphoe mae klong,amphoe mae lan,amphoe mae nam om,amphoe mae on,amphoe mae phrik,amphoe mae ramat,amphoe mae rim,amphoe mae sai,amphoe mae sariang,amphoe mae saut,amphoe mae sot,amphoe mae suai,amphoe mae tha,amphoe maha chana chai,amphoe maharat,amphoe mai kaen,amphoe makham,amphoe makok tai,amphoe makrut,amphoe manang,amphoe mancha khiri,amphoe manorom,amphoe me rim,amphoe mesoht,amphoe min buri,amphoe moeang bha yao,amphoe moeang bhang,amphoe moeang dheung,amphoe moeang jaiya,amphoe moeang lohng,amphoe moeang nagorn dhai,amphoe moeang ngao,amphoe moeang thalang,amphoe moeang theun,amphoe moeangbhan,amphoe moeangfhang,amphoe moeangthenn,amphoe moei wadi,amphoe muang ang thong,amphoe muang buriram,amphoe muang chachoengsao,amphoe muang chainat,amphoe muang chaiyaphum,amphoe muang chanthaburi,amphoe muang chiang,amphoe muang chiang khan,amphoe muang chiang mai,amphoe muang chiang rai,amphoe muang chon buri,amphoe muang chumphon,amphoe muang fang,amphoe muang gao,amphoe muang in buri,amphoe muang kalasin,amphoe muang kamphaeng phet,amphoe muang khon kaen,amphoe muang khong,amphoe muang krabi,amphoe muang lampang,amphoe muang lamphun,amphoe muang loei,amphoe muang long,amphoe muang lop buri,amphoe muang mae hong son,amphoe muang maha sarakham,amphoe muang nakawn tai,amphoe muang nakhon nayok,amphoe muang nakhon pathom,amphoe muang nakhon phanom,amphoe muang nakhon ratchasima,amphoe muang nakhon sawan,amphoe muang nakhon si thammarat,amphoe muang nakhon thai,amphoe muang nakorn thai,amphoe muang nam,amphoe muang ngao,amphoe muang nong khai,amphoe muang nonthaburi,amphoe muang pathum thani,amphoe muang phan,amphoe muang phannanikhom,amphoe muang phatthalung,amphoe muang phayao,amphoe muang phet buri,amphoe muang phetchabun,amphoe muang phichit,amphoe muang phimai,amphoe muang phitsanulok,amphoe muang phrae,amphoe muang phuket,amphoe muang pra yao,amphoe muang prachin buri,amphoe muang prachuap khim khan,amphoe muang ranong,amphoe muang rat buri,amphoe muang roi et,amphoe muang sakhon nakhon,amphoe muang sam sip,amphoe muang sing buri,amphoe muang sisaket,amphoe muang songkhla,amphoe muang suang,amphoe muang sukhothai,amphoe muang suphan buri,amphoe muang surat thani,amphoe muang thalang,amphoe muang thoen,amphoe muang trang,amphoe muang wapi pathum,amphoe muang wapipthum,amphoe muang yasothon,amphoe mueang buriram,amphoe mueang chan,amphoe mueang kamphaeng phet,amphoe mueang krabi,amphoe mueang lampang,amphoe mueang lamphun,amphoe mueang mae hong son,amphoe mueang mukdahan,amphoe mueang nam,amphoe mueang pattani,amphoe mueang phangnga,amphoe mueang phitsanulok,amphoe mueang phrae,amphoe mueang phuket,amphoe mueang phum,amphoe mueang sa kaeo,amphoe mueang suang,amphoe mueang sukhothai,amphoe mueang tak,amphoe mueang takua pa,amphoe mueang uttaradit,amphoe mueang yala,amphoe mueang yang,amphoe mukdahan,amphoe na bon,amphoe na chaluai,amphoe na chueak,amphoe na di,amphoe na duang,amphoe na dun,amphoe na haeo,amphoe na kae,amphoe na khu,amphoe na klang,amphoe na mom,amphoe na mon,amphoe na muen,amphoe na noi,amphoe na pho,amphoe na tan,amphoe na thawi,amphoe na thom,amphoe na wa,amphoe na wang,amphoe na yai am,amphoe na yang,amphoe na yia,amphoe na yong,amphoe na yung,amphoe nakel,amphoe nakhon chai,amphoe nakhon chai si,amphoe nakhon klang,amphoe nakhon luang,amphoe nakhon luang nai,amphoe nakhon nayok,amphoe nakhon noi,amphoe nakhon thai,amphoe naklua,amphoe nam khun,amphoe nam kliang,amphoe nam nao,amphoe nam om,amphoe nam pat,amphoe nam phong,amphoe nam som,amphoe nam sum,amphoe nang buat,amphoe nang rong,amphoe ngao,amphoe nikhom kham soi,amphoe nikhom nam un,amphoe nikhom phattana,amphoe noen kham,amphoe noen maprang,amphoe noen sa-nga,amphoe non daeng,amphoe non din daeng,amphoe non khun,amphoe non lao,amphoe non narai,amphoe non sa-at,amphoe non sang,amphoe non sila,amphoe non sung,amphoe non suwan,amphoe non thai,amphoe non wat,amphoe nong bua,amphoe nong bua daeng,amphoe nong bua lamphu,amphoe nong bua rawe,amphoe nong buek,amphoe nong chang,amphoe nong chik,amphoe nong chok,amphoe nong don,amphoe nong han,amphoe nong hi,amphoe nong hin,amphoe nong hong,amphoe nong kha yang,amphoe nong khae,amphoe nong ki,amphoe nong kung si,amphoe nong mamong,amphoe nong muang,amphoe nong muang khai,amphoe nong na kham,amphoe nong phai,amphoe nong pho,amphoe nong phok,amphoe nong prue,amphoe nong ruea,amphoe nong saeng,amphoe nong song hong,amphoe nong sua,amphoe nong sun,amphoe nong sung,amphoe nong wua so,amphoe nong ya plong,amphoe nong ya sai,amphoe nong yai,amphoe nopphitam,amphoe nuea,amphoe nuea khlong,amphoe omkoi,amphoe ongkharak,amphoe pa bon,amphoe pa mok,amphoe pa phayom,amphoe pa sang,amphoe pa tio,amphoe pa tiu,amphoe pachin udon,amphoe padhio,amphoe pai,amphoe pak chom,amphoe pak chong,amphoe pak khat,amphoe pak kret,amphoe pak nam,amphoe pak phanang,amphoe pak phayun,amphoe pak phli,amphoe pak phra,amphoe pak tho,amphoe pak thong chai,amphoe pakdhongjai,amphoe pakham,amphoe palian,amphoe panare,amphoe pang sila thong,amphoe pathio,amphoe pathiu,amphoe pathum rat,amphoe pathum thani,amphoe patilieu,amphoe peiyua kiri,amphoe pha khao,amphoe phachi,amphoe phachim sisaket,amphoe phai cham sin,amphoe phaisali,amphoe phak hai,amphoe phak phanom,amphoe phakdi chumphon,amphoe phan,amphoe phan chana,amphoe phan thong,amphoe phana,amphoe phana nikhon,amphoe phanang tung,amphoe phanat nikhom,amphoe phang khon,amphoe phannanikhom,amphoe phannikhom,amphoe phanom,amphoe phanom dong rak,amphoe phanom phrai,amphoe phanom sarakham,amphoe phanom thuan,amphoe phasi charoen,amphoe phato,amphoe phatthana nikhom,amphoe phayakkhaphum phisai,amphoe phayao,amphoe phayu,amphoe phayuhakhiri,amphoe phen,amphoe phibun mangsahan,amphoe phibun rak,amphoe phichai,amphoe phimai,amphoe phimun mangahan,amphoe phimun mangsahan,amphoe phipun,amphoe phlapphla chai,amphoe phlio,amphoe phloi waen,amphoe pho chai,amphoe pho prathap chang,amphoe pho sai,amphoe pho si suwan,amphoe pho tak,amphoe pho thale,amphoe pho thong,amphoe pho wi,amphoe phon,amphoe phon charoen,amphoe phon na kaeo,amphoe phon phisai,amphoe phon sai,amphoe phon sawan,amphoe phon thong,amphoe phop phra,amphoe photharam,amphoe phra khanong,amphoe phra nakhon,amphoe phra nakhon si ayutthaya,amphoe phra padaeng,amphoe phra phrom,amphoe phra phutthabat,amphoe phra ratchawang,amphoe phra saeng,amphoe phra samut chedi,amphoe phra sang,amphoe phra thaen,amphoe phra thong kham,amphoe phra yuen,amphoe phrai bueng,amphoe phralap,amphoe phran kratai,amphoe phrom buri,amphoe phrom khiri,amphoe phrom phiram,amphoe phrs saeng,amphoe phu kam yao,amphoe phu khieo,amphoe phu kradueng,amphoe phu luang,amphoe phu rua,amphoe phu sang,amphoe phu sing,amphoe phu wiang,amphoe phueai noi,amphoe phunphin,amphoe phupa man,amphoe phutthaisong,amphoe phutthamonthon,amphoe pla pak,amphoe plaeng yao,amphoe plai phraya,amphoe pluak daeng,amphoe pon pi sai,amphoe pong,amphoe pong nam ron,amphoe prachak sinlapakhom,amphoe prachantakham,amphoe prakhon chai,amphoe pralap,amphoe pran,amphoe pranburi,amphoe prang ku,amphoe prasat,amphoe prathai,amphoe pua,amphoe rahaeng,amphoe rako,amphoe raman,amphoe rangae,amphoe ranot,amphoe rasi saila,amphoe rasi salai,amphoe rat burana,amphoe ratchakhram,amphoe ratchasan,amphoe ratsada,amphoe rattanaburi,amphoe rattanawapi,amphoe rattaphum,amphoe renu nakhon,amphoe ron phibun,amphoe rong kham,amphoe rong kwang,amphoe rop krung,amphoe rueso,amphoe ruso,amphoe sa,amphoe sa bot,amphoe sa kaeo,amphoe sa khrai,amphoe saba yoi,amphoe saen to,amphoe saengbadan,amphoe sahaskhan,amphoe sahatkhan,amphoe sahatsakhan,amphoe sai buri,amphoe sai mun,amphoe sai ngam,amphoe sai noi,amphoe sai thong watthana,amphoe sai yok,amphoe sak lek,amphoe sakatkhan,amphoe sam chai,amphoe sam chuk,amphoe sam khok,amphoe sam ko,amphoe sam ngam,amphoe sam ngao,amphoe sam phran,amphoe sam roi yot,amphoe sam sung,amphoe samoeng,amphoe samrong,amphoe samrong thap,amphoe san buri,amphoe san kamphaeng,amphoe san pa tong,amphoe san sai,amphoe san thia,amphoe sanam chaeng,amphoe sanam chai khet,amphoe sanam chan,amphoe sang khom,amphoe sangkha,amphoe sangkhah,amphoe sangkhla buri,amphoe sangkhlaburi,amphoe sangkhom,amphoe sangkla buri,amphoe sangu,amphoe sankhaburi,amphoe sanom,amphoe santi suk,amphoe sanzai,amphoe sao hai,amphoe sap yai,amphoe sapphaya,amphoe saraphi,amphoe sarn buri,amphoe sathing phra,amphoe sattahip,amphoe satuek,amphoe satuk,amphoe sawaeng ha,amphoe sawang arom,amphoe sawang daen din,amphoe sawang wirawong,amphoe sawankhalok,amphoe sawi,amphoe seka,amphoe selabhum,amphoe selaphum,amphoe selephum,amphoe sena,amphoe sena nai,amphoe sena yai,amphoe senangkhanikhom,amphoe shai buri,amphoe si banphot,amphoe si bua thong,amphoe si bun ruang,amphoe si bun rueang,amphoe si chiang mai,amphoe si chomphu,amphoe si khoraphum,amphoe si koraphum,amphoe si maha phot,amphoe si mahosot,amphoe si muang mai,amphoe si muen,amphoe si nakhon,amphoe si narong,amphoe si prachan,amphoe si racha,amphoe si rattana,amphoe si sakhon,amphoe si samrong,amphoe si satchanalai,amphoe si sawat,amphoe si somdet,amphoe si songkhram,amphoe si thep,amphoe sichon,amphoe sida,amphoe sikao,amphoe sila lat,amphoe sing,amphoe singhanakhon,amphoe sirindhorn,amphoe sisaket,amphoe slephum,amphoe so phisai,amphoe soem ngam,amphoe soi dao,amphoe somdet,amphoe song,amphoe song dao,amphoe song phi nong,amphoe songkla buri,amphoe sop moei,amphoe sop prap,amphoe srihohrabhum,amphoe srikhohrabhum,amphoe srikhonphum,amphoe srinagarindra,amphoe suan phueng,amphoe suk samran,amphoe sukhirin,amphoe sukhodhai,amphoe sukhothai,amphoe sung men,amphoe sung noen,amphoe sungai kolok,amphoe sungai padi,amphoe suwan khu ha,amphoe suwan warin,amphoe suwanapum,amphoe suwannakhuha,amphoe suwannaphum,amphoe suwannphum,amphoe suwarnbhum,amphoe ta khli,amphoe ta luang,amphoe ta phraya,amphoe tai,amphoe tak bai,amphoe taksin,amphoe takua pa,amphoe takua thung,amphoe takuadhung,amphoe takuapa,amphoe takuatung,amphoe talad yai,amphoe talat mai,amphoe tale noi,amphoe taling chan,amphoe taluban,amphoe talung,amphoe tamot,amphoe tan sum,amphoe tao ngoi,amphoe taphan hin,amphoe tasawng yang,amphoe tawat buri,amphoe tha bo,amphoe tha chana,amphoe tha chang,amphoe tha kham,amphoe tha khanon,amphoe tha khantho,amphoe tha li,amphoe tha luang,amphoe tha mai,amphoe tha maka,amphoe tha muang,amphoe tha nat wat pradu,amphoe tha phae,amphoe tha pla,amphoe tha pradu,amphoe tha rong,amphoe tha rong chang,amphoe tha rua,amphoe tha sae,amphoe tha sala,amphoe tha song yang,amphoe tha ta kut,amphoe tha takiap,amphoe tha tako,amphoe tha tum,amphoe tha uthen,amphoe tha wa,amphoe tha wung,amphoe tha yang,amphoe thai charoen,amphoe thai muang,amphoe thaksin ubon,amphoe thalang,amphoe tham phannara,amphoe than to,amphoe thanyaburi,amphoe thap khlo,amphoe thap pot,amphoe thap put,amphoe thap sakae,amphoe thap than,amphoe that phanom,amphoe thawat buri,amphoe thep sathit,amphoe thepharak,amphoe thoen,amphoe thoeng,amphoe thon buri,amphoe thong pha phum,amphoe thong saen khan,amphoe thung fon,amphoe thung hua chang,amphoe thung kha,amphoe thung khao luang,amphoe thung luang,amphoe thung saliam,amphoe thung si udom,amphoe thung song,amphoe thung tako,amphoe thung wa,amphoe thung yai,amphoe thung yang daeng,amphoe tio,amphoe to mo,amphoe trakan phuet phon,amphoe trakan phut phon,amphoe tron,amphoe tu yong,amphoe tungsong,amphoe tuyong,amphoe u thai,amphoe u-thong,amphoe ubolratana,amphoe udhumbhornbhisai,amphoe udon,amphoe umphang,amphoe utara ubon,amphoe uthai noi,amphoe uthai yai,amphoe uthumphon phisai,amphoe waeng,amphoe waeng noi,amphoe waeng yai,amphoe wan yai,amphoe wang chan,amphoe wang chao,amphoe wang chin,amphoe wang ga,amphoe wang hin,amphoe wang ka,amphoe wang khanai,amphoe wang muang,amphoe wang nam khiao,amphoe wang nam yen,amphoe wang noi,amphoe wang nua,amphoe wang pong,amphoe wang sai phun,amphoe wang sam mo,amphoe wang saphung,amphoe wang sombun,amphoe wang thong,amphoe wang wiset,amphoe wangka,amphoe wangyang,amphoe wanon niwat,amphoe wapi pathum,amphoe warin chamrap,amphoe warinjamrap,amphoe waritchaphum,amphoe wat bot,amphoe wat pa,amphoe wat phleng,amphoe wat pradu,amphoe wat sing,amphoe watthana nakhon,amphoe wiang kao,amphoe wiang pa pao,amphoe wiang sa,amphoe wichian,amphoe wichian buri,amphoe wiengpapao,amphoe wihan daeng,amphoe wijiern,amphoe wiphawadi,amphoe wiset chai chan,amphoe xum seng,amphoe yaha,amphoe yala,amphoe yamu,amphoe yan nawa,amphoe yan ta khao,amphoe yang chum noi,amphoe yang noeng,amphoe yang si surat,amphoe yang yong,amphoe yangtalat,amphoe yarang,amphoe yaring,amphoe yarom,amphoe yasodhom,amphoe yasodhorn,amphoe yasothon,amphoe yingo,amphoe yong sata,amphoe zengbadan,amphor kathum baen,amphoux,amphu ban pong,amphu bang raka,amphu bangspan,amphu chai badal,amphu chorak sam pan,amphu deum bang,amphu keng koi,amphu khao yoi,amphu krok pra,amphu luang,amphu lubla,amphu me plick,amphu memok,amphu metar,amphu nang buai,amphu nong chok,amphu paknam po,amphu panas nikom,amphu panom sarakam,amphu phak hai,amphu piak puin pisai,amphu po ta-ram,amphu sao-hai,amphu sawng pi nawng,amphu sena,amphu sopwa,amphu sri maha raja,amphu sri sawas,amphu tap tan,amphu trai yok,amphu yang ryong,amphu yong star,ampi,ampi bazar,ampia ajumako,ampia edwumako,ampiadanana,ampiadiambola,ampiadiamby,ampiadiankazo,ampiadianombilahy,ampiadiantrandraka,ampiadiantrandrake,ampiadina,ampiakarandraifito,ampiala,ampialivana,ampiambesa,ampiamy,ampiana,ampianana,ampiantsaha,ampiantsoanomby,ampiaslanto,ampibabo,ampibabu,ampibako,ampicolque,ampid,ampid i,ampid ii,ampidirana,ampiefana,ampieferana,ampieky,ampienenana,ampietama,ampifiala,ampifinala,ampih,ampiha,ampihadiambola,ampihadranaby,ampihalia,ampihamy,ampihamy nord,ampihamy sud,ampihanon,ampihaonana,ampihoaramboay,ampihoarantakatra,ampijoroa,ampijoroana,ampikamy,ampiketraha,ampikilova,ampikomoko,ampil,ampil damtuk,ampil pram dem,ampil tapok,ampil tuk,ampila,ampilafily,ampilahoa,ampilahoana,ampilanonana,ampilanturai,ampilatsaha,ampilatwatja,ampilhac,ampili,ampilira,ampilly,ampilly-le-haut,ampilly-le-sec,ampilly-les-bordes,ampilo,ampilobe,ampilofilo,ampilofilo ambany,ampilofilo ambony,ampilofily,ampilokely,ampimahy,ampinana,ampincha,ampinikan,ampiolahana,ampipika,ampipoantsatroka,ampiramba,ampiran,ampiranambo,ampiranga,ampirarazana,ampiri,ampirifiry,ampirika,ampiriky,ampiritsoka,ampisahaby,ampisaka,ampisandrata,ampisangia,ampisara,ampisaraha,ampisarahana,ampisaraho,ampisavankaratra,ampisikinana,ampisikiny,ampisoha,ampisokina,ampisopisa,ampisopiso,ampisunme,ampitabe,ampitabe i,ampitabe ii,ampitahana,ampitakifasina-fenoarivo,ampitakihosy,ampitakilaka,ampitamalandy,ampitambe,ampitambositra,ampitana,ampitana-firaisana,ampitanaka,ampitanaka-andonaka,ampitandakana,ampitandrahitsika,ampitandroa,ampitankely,ampitanombalahy,ampitantafika,ampitasimo,ampitatafika,ampitavanana,ampitetika,ampitigala,ampitigama,ampitigoda,ampitiliana,ampitilimaika,ampitilimaiki,ampitilimaiky,ampitilina,ampitilova,ampitily,ampitiya,ampitiya pallegama,ampitiya udagama,ampitolava,ampitolova,ampitonita,ampitonoamboro,ampitrahambato,ampitsahana,ampitsahandakana,ampitsangana,ampitsikinana,ampitsinjovana,ampitsinjovandriaka,ampitsirihina,ampitsopitsoka,ampitsopitsoky,ampivalana,ampivalanana,ampiviky,ampivine,ampiyacu,ampiyan,ampizamatana,ampizaramaso,ampizarantany,amplaing,amplas,amplas pasar tiga,amplaspasar 3,amplatz,amplawas,ampleben,ampleforth,amplepuis,ampleyevka,ampliacion almendares,ampliacion de almendares,ampliacion de arroyo,ampliacion de barbosa,ampliacion de cruz verde,ampliacion de mulgoba,ampliacion isla de las flores,ampliacion la loma,ampliacion pancho villa,ampliacion rosario,amplier,amplio,ampney,ampney crucis,ampney saint mary,ampney saint peter,ampo,ampo kidul,ampo lor,ampoa,ampoabe,ampoafambiavy,ampoafamboay,ampoagan,ampoanomby,ampoaratokatra,ampoca,ampoekoeng,ampoense,ampofokurom,ampofotsy,ampofukrom,ampohabato,ampohafambiavy,ampohafana,ampohana,ampohara,ampohe,ampohibe,ampohoarandakana,ampohobe,ampoigne,ampoita,ampoizy,ampokafo,ampola,ampolakapo,ampolatany,ampolipoly,ampolla,ampolomanarivo,ampolompamba,ampolontany,ampolotra,ampolotramalo,ampolotramalona,ampolotsy,ampoma,ampomadoso,ampomatro,ampomaventy,ampombaka,ampombako,ampombiantambo,ampombiatambo,ampombibe,ampombibe iii,ampombibitika,ampombidamiaka,ampombihely,ampombilava,ampombimakoalahy,ampombimanagy,ampombimanagy ii,ampombimanangy,ampombimangoraka,ampombimiantona,ampombimihantona,ampombinivango,ampombintsihanaka,ampombirejy,ampombitaka,ampombitika,ampombobe,ampombobitaka,ampombodamiaka,ampombofofo,ampombokely,ampombolava,ampombomanangy,ampombomangarakaraka,ampombomazava,ampombomiantona,ampombonavony,ampombonimananady,ampombonivango,ampombonjirika,ampombontsianga,ampomboragodona,ampombovaloharona,ampona,amponaomby,ampondra,ampondrabe,ampondrahazo,ampondralava,ampondramatso,ampondramiolaka,ampondramivora,ampondramotso,ampondrangodona,ampondranimahalana,ampondravelo,ampondravelo i,ampondravelo ii,ampondravelona,ampongol,amponina,amponjiriaka,amponobe,ampontany,ampontoc,ampontsilahy,amponville,amponya,ampopo,ampopoha,ampopohabe,ampopoho,ampopohona,amporaha,amporaho,ampori,amporingamena,amporiwo,amporoforo,amporoneno,amposakaha,amposaky,amposari,amposso,amposta,amposungan,ampotabato,ampotabe,ampotaka,ampotaka atsimo,ampotaka avaratra,ampotaka est,ampotaka sud,ampotakalanana ambevy,ampotakalanana anandrea,ampotakalanana-anandray,ampotakamaroreny,ampotakely,ampotamainty,ampotamandrevo,ampotamasina,ampotamena,ampotoana,ampotobato,ampotobola,ampototra,ampototse,ampototsy,ampotsiambo,ampotsilahy,ampotsimanody,ampotsimavo,ampou prey,ampoya,ampoza,ampozabe,ampozakely,ampozaloaka,ampozasaha,ampozatokana,ampozavondraka,ampozoky,ampqna,ampqnah,amprantsoanomby,ampreng,ampriani,amprior,amprompromou,amprong,ampronsie,ampsen,ampsin,ampt hill,amptezieu,ampthill,ampuagan,ampuaia,ampuan,ampuato,ampuccasa,ampudia,ampuero,ampugnano,ampuis,ampujala,ampukung,ampulajange,ampulan,ampultun,ampungan,ampunyanso,ampunyase,ampunyasi,ampur,ampur takbai,ampur tomo,ampurias,ampus,ampus 2,ampus dua,ampusungan,amputang,ampyong,amqah,amqan,amqat,amqog,amquana,amqui,amr dittewala,amr ed dine,amr zeydabad,amra,amra kalan,amra khurd,amra posht,amra sanwari,amrabad,amrabad kurd,amrabad-e dumak,amrabari,amrada,amrada fetish,amrae,amragachhia hogalpati,amrah soch ali shah,amrah-i-gerd,amrah-ye gerd,amrah-ye gird,amrahia,amrahlu,amrai,amraia,amraid,amrait,amraizo,amrajkola,amrak,amrakh,amrakits,amral,amral bala,amrala,amran,amran khel,amran kheyl,amrana,amrane,amrani,amraniyah,amrankheyl,amranzai,amraoti,amraouite,amrapara,amrapur,amrapur dhulandi,amras,amrasa,amrash,amrat,amratala,amratali,amrath sarian di basti,amrati,amratiya,amraud,amrauli,amravan,amravati,amray,amrayi-gird,amraz talao,amrazi,amrduiyeh,amre,amreduiyeh,amreg,amreh,amreh khal,amreli,amren,amreswar,amrevan,amrey,amrharas,amrhisid,amrhizid,amri,amri khel,amria,amrichshausen,amrigschwand,amrik,amrik singhwala,amrikpura,amring,amrio,amrir gifer,amriswil,amrita,amrita bazar,amritapara,amritapur,amritapur srimantapur,amritnagar,amritpur,amritpura,amritsar,amritsari mohallah,amritsarian da dera,amriye,amrod,amrod-e `olya,amrod-e sufla,amroda,amrodah,amrode `ulya,amrode sufla,amroduiyeh,amroha,amrohia,amroli,amrollah,amrollahi,amroodiyeh soosafid,amrootak,amrosimov,amrosimova,amrot,amrotak,amroth,amrowdeh,amrowleh,amrowtak,amrs,amru,amruben,amrud,amrud-e `olya,amrud-e sofla,amruda,amrudak,amrudeh,amrudi-sufla,amrudi-ulia,amrudiyeh,amrudiyeh-ye su sefid,amrudkan,amruduiyeh,amrui,amrujan,amruka,amrulbari,amruleh,amrullah,amruni,amrut,amrutak,amrutta,amrutuk,amrutur,amry,amrzou,amsa,amsabang,amsacrou,amsadong,amsafesge,amsagarte ait oughalet,amsaidong,amsailtong,amsaisi,amsal,amsalli,amsalsal,amsamer,amsamka,amsamni,amsamsa,amsan,amsangarf,amsanka,amsanpalli,amsanri,amsaof,amsar,amsarah,amsari,amsasadong,amsasamdong,amsatta,amsaur,amsawan,amsberg,amsbry,amschelberg,amschlbach,amsco,amsden,amsdorf,amsedere,amsegguert,amsel,amsele,amselfing,amselhain,amsemblel,amsemsa,amsen,amseneni,amsengarf,amserach,amseran,amsfoul,amsgui,amshal,amsham,amshara,amsharevo,amsharova,amsharovo,amshausen,amshenik,amshikhina,amshinka,amshul,amshyttan,amsi,amsiba,amsic,amsik,amsil,amsioun,amsisouane,amsissane,amsissene,amsitten,amsittene hammou,amsittne,amsitu zufan,amskenid,amskerkerd,amslam,amsloh,amsmartete,amso,amsobawi,amsodong,amsoei,amsogae,amsola,amsoldingen,amsosen,amsouch,amsoul,amsrhar,amssioun,amstall,amsteg,amstein,amstel,amsteleind,amstelhoek,amstelveen,amstenrade,amsterco,amsterdam,amsterdamhoek,amsterdamsche veld i,amsterdamscheveld,amsterdamseveld,amstetten,amstguine,amsthas,amsthas dechra sidi amrane,amston,amstrup,amsuch,amsudong,amsui,amsuku,amsuling,amsweer,amt dambeck,amt friedrichsaue,amt kienitz,amt wollup,amta,amta auta,amtahan,amtail,amtail nischintapur,amtaki,amtala,amtala digar,amtali,amtanmin,amtanmin 1,amtanmin 2,amtasa,amtashah,amtatorp,amtazguine,amtbjork,amtefall,amtel,amtenhausen,amter,amtern,amteroth,amterzguine,amteshult,amtevik,amtezguine,amti,amtic,amtika,amtil,amtjang,amtkel,amtkeli,amtkhel,amtkiali,amtknechtswahn,amtmann,amtmannsdorf,amto,amtodong,amtoefoe,amtoft,amtolchkurt,amtoli,amtorp,amtoudi,amtqeli,amtrask,amtrhar,amts bauhof,amtsfreiheit,amtsguine,amtshagen,amtshainersdorf,amtshausen,amtshof,amtsknechtswahn,amtswieck,amtswiek,amtuagan,amtufu,amtuila,amtul,amtun,amtya usta,amtzell,amu,amu oghli,amu shedi,amu-darinskiy,amu-darya,amua,amua kanda,amuafu,amuakandi,amuakromakrofo,amuamuaipe,amuamuatpe,amuana,amuanda,amuapa,amuarib,amuata,amuatekokope,amuay,amuazam,amubri,amucao,amucayan,amucha,amuchanlang,amuchu,amuchuhu,amuchy,amucian,amuco,amucotocapa,amucu,amuda,amuda egunsola,amudabad,amudalavalasa,amudarya,amudat,amude,amudiya,amudleh,amudo,amudpur,amudur,amue,amuegbedi,amuelas,amuerh,amuerhho,amuerhhochan,amuerkhechzhan,amufi,amufu,amugao,amugei,amugis,amugius,amugo,amugoda,amugoro,amugu,amuguis,amugul,amugulang,amugulang zhen,amuguo,amuguro,amuhenkanda,amuioan,amujan,amujupi,amuk,amukabi,amukan,amukandi,amukara,amukegun ibadan,amukegun modakeke,amukh,amuko,amukoko,amukolang,amukou,amukpe,amukrom,amukta,amukulang,amukulangmuchang,amukulangpaoliko,amukuliang,amukulungundju,amukunu,amukura,amukusana,amukuteile,amul,amula,amuladi,amule,amulet,amulgading,amulibanth,amullajta,amuloko,amulong,amulree,amulunco,amulung,amuma,amuma hena,amuma lega,amumukwa,amun,amun kor,amuna,amunakole,amunara,amunarrizqueta,amunartia,amund,amundarp,amundebo,amundero,amunderud,amundstorp,amundtorp,amunekandura,amunekumbura,amunewala,amung,amunga,amungan,amunge 1,amunge 2,amunge dua,amunge satu,amungen,amungini,amuni,amuni nkwanta,amuniakwa,amunigun,amunitan,amunle,amunoi,amunol,amunor,amuntai,amuntay,amunugama,amunugoda,amunukole,amunukumbura,amunumulla,amunupitiya,amunupura,amunupure,amunutenna,amunuwela,amunuwetiya,amunuwewa,amunwetiya,amuor,amuping,amupitiya,amuquhu,amur,amur bayantuin khure,amur buyantuin hurie,amur-klyuch,amur-sanan,amura,amurah,amuraika,amuraisa,amuraisa-toroto,amuraisa-torotu,amuram,amuran,amurang,amurao,amurata,amuraw,amurco,amurd,amurd aqachi,amurdello,amurdi,amure,amurhu,amuri,amuri camaizore,amuri camalzore,amuria,amurie,amurin,amurire,amurji,amurka,amurkata,amurli,amurlove,amurmomur head,amurn,amurnizhne dneprovsk,amuro,amuro-baltiysk,amuro-baltiyskiy,amuro-baltiyskoye,amurogama,amurokama,amuromur head,amurosievka,amurri,amurrio,amursk,amurske,amurskiy,amurskoye,amurstal,amurtak,amurtur,amuru,amurug,amuruipa,amurukeni,amururua,amurwan,amuryei,amurzak,amurzet,amurzino,amus chebia,amusa,amusan,amuscat,amuschina,amusco,amusgos,amushan,amushenye,amushi,amusi,amusiaso,amusini,amuslog,amuson,amusquillo,amusrog,amussa,amusse,amusse muiota,amusus,amusuy,amut,amuta,amutag,amutagoda,amutai,amutanga,amutankulam,amutari,amutcot,amutenha,amuteya,amutinu,amutka,amutkachi,amutkan,amutna,amutnadrel,amuto,amutschu,amutsinu,amuttjot,amutu,amuvi,amuvianu,amuwal,amuwala,amuwatta,amuwatugoda,amuwo,amuyei,amuyode,amuyong,amuzgi,amuzi,amuziya,amuzu,amuzu-igbagu,amuzukofe,amuzukwu,amuzuoro,amvalea,amvam,amvame,amvamg,amvangsi,amvani,amvei,amveme,amvey,amvlota,amvo,amvom,amvon,amvonga,amvrakia,amvrosia,amvrosiyevka,amvrosiyevo,amvrosiyivka,amwa majhar,amwaanda,amwal,amwala tarla,amwalana,amwalde,amwam,amwanga,amwani,amweepele,amwell,amwonapu,amy,amya,amyakhsyn-kel,amyamya,amyan,amyanet,amyang,amyator,amyaunggan,amyaunggon,amyclae estates,amyderya,amyelkawshchyna,amyema,amyere,amyet,amyethtwet,amygdali,amyin,amyingyun,amynnet,amynya,amyonga,amyot,amyran,amys acres,amysakh,amyton,amytyk,amyun,amyut,amza,amza ourhrou,amzabegovo,amzacea,amzahni,amzaika,amzaine,amzajard,amzajerd,amzalak,amzalare,amzali,amzaour,amzaourou,amzara,amzarai,amzarai gudr,amzas,amzawrou,amzel,amzelou,amzeourou,amzerlou,amzibane,amzibash,amzici,amzir,amzizou,amzlou,amznass,amznes,amzou,amzoudj,amzoug,amzouj,amzourhen-khemis-mrabten,amzout,amzouz,amzra,amzri,amzrou,amzulare,amzulesti,amzya,an,an `ayn,an abhainn dubh,an al luweqa,an amer,an ba,an bai,an baile atha hulla,an baile beag,an baile dubh,an baile mor,an baile nua,an ban,an bang,an bao,an bhainseach,an bhlarna,an bhuirios,an bien,an binh,an binh thanh,an binh trai,an blaic,an bo,an bothar bui,an bru,an bun beag,an buong,an cabhan,an caislean nua,an caislean riabhach,an caladh,an can,an caol,an cap,an carraigin,an cau,an chan,an chang,an chanh,an charraig,an chathair,an chau,an chau ap,an cheapach,an cheapach mhor,an cheathru chaol,an cheathru rua,an chi,an chill,an chinh,an chloch,an chluaine,an chorrchoill,an chraig,an chrois dhearg,an chuil,an clochan,an clochan liath,an cloichin,an cloigeann,an cnoc rua,an co,an co18,an cobh,an coirean,an craoslach,an creagan,an croman,an cruachan,an cu,an cuc,an cuong,an cuu,an da,an dai,an daingean,an dan,an dao,an den eichen,an der beek,an der chaussee,an der haar,an der heide,an der hunte,an der kapelle,an der kuhbrucke,an der leiten,an der schanze,an der stadlhutte,an der sudbake,an der trift,an der wae,an der weinleite,an diem,an dien,an dien bac,an dien nam,an dinh,an dinh cau,an do,an do18,an doi,an dol,an dom,an don,an don sray,an dong,an droichead nua,an duc,an duchoraidh,an duong,an duyen,an fal carrach,an fear ban,an fearann fuar,an fheothanach,an fhiacail,an fhianait,an gabhailin,an gallanach,an gallbhaile,an geata ban,an geata mor,an ghaobhach,an ghraig,an ghrainseach,an gia,an giang,an giao,an giat,an giu,an gleann,an gleann garbh,an gleann mor,an gort,an goudoum,an grianfort,an ha,an hai,an hai phuong,an han,an hanh,an hau,an hay,an hiep,an hoa,an hoa dong,an hoa tay,an hoi,an hoi thon,an hop,an htang,an inis,an inse,an kahe,an kang,an khanh,an khanh xa,an khe,an khuong,an ky,an la,an lac,an lac dong,an lac tay,an lai,an laithreach,an lam,an lan,an lang,an lao,an lao ap,an lap,an lathaigh,an lau,an lau nam,an le,an leacht,an leathros,an leim,an lich,an liem,an ling,an linh,an lios breac,an lo,an loc,an loi,an loi ap,an loi dong,an long,an longfort,an lorgain,an lu,an luan,an luong,an luu,an ly,an ma,an maqurah,an me,an mhuirioch,an mieng,an mo,an mong,an mota,an muileann gcearr,an muileann iarainn,an mullach,an my,an my trai,an na`am,an na`amah,an na`aminah,an na`asat,an na`ayim,an na`bub,an na`im,an na`imah,an na`s,an na`sat,an na`urah,an naaoura,an nab`ah,an nab`at,an nabalat,an nabat,an nabatiyah,an nabatiyah al fawqa,an nabatiyah at tahta,an nabbut,an nabhaniyah,an nabi `uthman,an nabi al awza`i,an nabi ayla,an nabi hud,an nabi ilyas,an nabi khalid,an nabi kuzaybir,an nabi rashad,an nabi rashadah,an nabi sadiq,an nabi salih,an nabi sami,an nabi samuil,an nabi samwil,an nabi sbat,an nabi shit,an nabi sujud,an nabi yunus,an nabi yusha`,an nabidah,an nabidhah,an nabiya,an nabiyah,an nabk,an nabk abu nakhlah,an nabk abu qasr,an nabqiyah,an nabrah,an nabu,an nadawat,an nadi,an nadir,an nadirah,an nadwah,an nafab,an nafal,an nafash,an naffatiyah,an nafi,an nafidah,an nafisah,an naftiyah,an nagada,an naghamish,an naghamishah,an nagit,an nahal,an nahar,an nahdah,an nahhal,an nahhariyah,an nahhasin,an nahidayn,an nahiya,an nahiyah,an nahl,an nahr,an nahriyah,an nahud,an nahyah,an nair,an naiya,an naj` al wastani,an najaf,an najamiyah,an najayrah,an najd,an naji,an najih,an najil,an najiyah,an najjari,an najjarin,an najjariyah,an najmah,an najmi,an nakharin,an nakhil,an nakhkhas,an nakhl,an nakhlah,an nakhlayn,an nakhra,an nakkariyah,an nam ly,an namarah,an namasa,an namasah,an namatah,an nami,an namir,an namlat,an nammurah,an namrut,an namudhajiyah,an namurah,an namurajiyah,an nap,an naq`ah,an naqa,an naqa`ah,an naqarinah,an naqb,an naqbah,an naqdah,an naqib,an naqibiyah,an naqidi,an naqqash,an naqubah,an naqur,an naqura,an naqurah,an narjas,an nas,an nasaimah,an nasami,an nasariya,an nasbah,an nasfah,an nashabiyah,an nashifah,an nashiriyah,an nashmi,an nashsh,an nashshabiyah,an nashshatah,an nashw al bahri,an nashwah,an nasibah,an nasifah,an nasim,an nasim al gharbi,an nasim ash sharqi,an nasimiyah,an nasir,an nasirah,an nasiri,an nasiriya,an nasiriyah,an nasiriyah ash sharqiyah,an naslah,an nasr,an nasraniyah,an nasri,an nassar,an nassariyah,an nataij,an nattaf,an nawa`is,an nawafi`ah,an nawahid,an nawamis,an nawamis wa marawinat an nasara,an nawaqir,an nawatif,an nawawirah,an nawazil,an nawbah,an nawbatayn,an nawfalab,an nawfaliyah,an nawiyah,an nawmaniyah,an nawsa,an nawus,an nawwaqiyah,an nawwashah,an nayhah,an nayrib,an nazayim,an nazhah,an nazi,an nazim,an nazir,an nazl,an nazla al gharbiya,an nazla al wusta,an nazla ash sharqiya,an nazlah,an nazlah al gharbiyah,an nazlah al jadidah,an nazlah al mustajiddah,an nazlah al wusta,an nazlah ash sharqiyah,an nazlat,an nazlat al wusta,an nazlat ash sharqiyah,an nazlawi,an nazu`,an nejade,an neudorfel,an ngai,an nghia,an nghien,an nghiep,an nha,an nhan,an nhien,an nhom,an nhon,an nhon hoa,an nhon tay,an nhon xa,an nhue,an nhut,an nhut tan,an ni`ayy,an ni`na`iyah,an niba,an nibaj,an nid,an niggaiyat,an niha,an nihay,an nihayah,an nijarah,an nijdah,an nijfah,an nijibah,an nijmiyah,an nikashah,an nikhela,an nil,an nimarah,an nimas,an nimrah,an nimriyah,an ninh,an ninh noi,an niqrash,an nirb ghiraa,an nisab,an nisf,an nisiyah,an niswan,an nizim,an nmayriyah,an noba-al-jimjam,an nokla,an nong,an noong,an nqur,an nu`am,an nu`ayiah,an nu`ayr,an nu`ayriya,an nu`ayriyah,an nu`ayyimah,an nu`man,an nu`maniyah,an nubariyah,an nubay`ah,an nubay`at,an nubayq,an nubayrah,an nubqiyah,an nufaliyah,an nufaysah,an nufur,an nugayyat,an nughayq,an nughsh,an nuhayd,an nuhayy,an nuhud,an nujaybah,an nujayd,an nujaydayn,an nujayfah,an nujaylah,an nujayli wa awlad ash shaykh,an nuju`,an nukhayb,an nukhayl,an nukhaylah,an nukhayshiyah,an nukhr,an nukhrah,an nukhush,an nukkahi,an numaniyah,an numayr,an numayriyah,an numaysah,an nuong,an nuqaq,an nuqay`ah,an nuqay`at,an nuqayl,an nuqayr,an nuqayt,an nuqayyat,an nuqayyib,an nuqdah,an nuqeil,an nuqrah,an nuqub,an nuqub as sufla,an nur,an nur musa,an nusari,an nusayf,an nusayhi,an nusayr,an nusf,an nushaymah,an nusub,an nuwad,an nuwasirah,an nuway`imah,an nuwaydirat,an nuwayjis,an nuwayrah,an nuwayri,an nuwaytah,an nuzayhah,an nuzayhah al `ulya,an nuzhah,an nzala,an oghmagh,an omaigh,an pasaiste,an phong,an phu,an phu ap,an phu dong,an phu ha,an phu phuong,an phu tay,an phu \303\220ong,an phuc,an phuoc,an phuoc chinh,an phuong,an ping chen,an ping hsu,an pon,an port nua,an port rua,an qa`,an quang,an qui,an quy,an rahhaliya,an rath mhor,an reinsdorf,an ros,an rouan,an rum,an scairbh,an sciobairin,an scoil,an seanbhaile mor,an seanchaislean,an sen,an snaidhm,an son,an son c,an spideal,an sraidbhaile,an tabhallort,an tam,an tan,an taonach,an tay,an teampall mor,an teing,an tha,an thaan,an thach,an thach thach nan,an thai,an thai thuong,an thanh,an thanh nhi,an thanh thuy,an the,an thi,an thiem,an thien,an thinh,an tho,an tho thon,an tho18,an thoi,an thuan,an thuong,an thuong ap,an thuy,an thuyet,an tien,an tinbhear mor,an tinh,an tinh ba,an tiur,an toan,an tochar,an tospideal,an trach,an trang,an tri,an trieu,an trinh,an tros,an troua,an truc,an truong,an truy,an truyen,an tsnaidhm,an tsraidbhaile,an tu ha,an tuc,an tulach,an tuong,an tuyen,an tuyet,an uaimh,an urnai,an van thuong,an vang,an ve,an vinh,an vinh phuong,an xa,an xuan,an xuyen,an \303\220a,an \303\220e18,an \303\220inh,an \303\220o18,an \303\220oai,an \303\220on,an ala,an-ana-ao,an-anonas,an-de-waterkant,an-doydu,an-guba,an-kabat,an-mola,an-namata,an-nejijje,an-nwaji ocib,an-onhe,an`am kheyl,ana,ana bangu,ana bolaghi,ana bota da silva,ana del conde,ana dias,ana farias da luz,ana gomes,ana hajji,ana joaquina,ana kalay,ana kelay,ana khatun,ana khel,ana kheyl,ana konde,ana maria,ana maria hacienda,ana mela,ana mishal,ana rech,ana ruiz,ana sanches,ana sanchez,ana santana,ana zumaran,ana-otege,ana-sira,anaaloeto,anaambe,anaao,anaaon,anaaong,anaar kali,anaas,anaau,anaaw,anab,anaba,anaba behistan,anabad,anabaga,anabakli,anabannae,anabanoea,anabanua,anabar,anabat,anabay,anabe,anabedama,anabedema,anabel,anabha,anabib,anabieh,anabiko,anabiku,anabiyeh,anablaha,anablan,anablo,anabo,anaboandreta,anaborano,anaborengy,anaboriana,anaboringo,anaboubou,anabra,anabrou,anabsa,anabsa el hadaj,anabu i,anabu ii,anabu primero,anabu segundo,anabucis,anabuki,anaburo,anaburu,anabuya,anabyrka,anabyrma,anac,anac ccoponeta,anac-huito,anaca,anacacho,anacadina,anacadinya,anacahuite,anacak,anacal,anacallo,anacana,anacapa,anacapri,anacapul,anacarco,anacari,anacaro,anaccocha,anacetaba,anach-kasy,anacha,anachaung,anachevo,anachin,anachucuna,anachukuna,anacik,anacius,anacker,anacleto,anacleto garcia,anacleto lopez,anacli,anaco,anaco abajo,anacoco,anaconda,anaconda trailer court,anaconia,anacortes,anacortes crossing,anacory,anacostia,anacota,anacual,anacuas,anacuas caja pinta,anacuitas,anacuites,anacultas,anacuta,anad,anada,anadabe,anadabo,anadabodera,anadabolava,anadabomaro,anadabonampela,anadabonampeta,anadabotelo,anadabotolo,anadag,anadaho,anadako,anadalen,anadanalava,anadane,anadara,anadarko,anade,anadebolava,anadel,anadere,anadhiou,anadhiu,anadi,anadia,anadiaz,anadiou,anadla,anado,anadol,anadolchioi,anadolija,anadolu fener,anadolu feneri koyu,anadolu hissari,anadolu kavak,anadolu koy feneri,anadolufeneri,anadoluhisari,anadolukavagi,anadon,anadra,anadramy nord,anadray,anadroadro,anadyr,anadyrsk,anadyrskiy,anadyrskiy rybnyy kombinat,anadyrskiy rybnyy kombinat letnik,anadyu,anaehoomalu,anaeli,anaemal,anaeri,anafama,anafari,anafe,anafi,anafiafy,anafijeh,anafis,anafo,anafodiya,anafolo,anafomiala,anafomihala,anafondravoay,anafonitria,anafor,anafotia,anafotida,anafovato,anafra,anafreita,anafro,anafuki,anag,anaga,anaga-dzveli,anagada,anagai,anagali,anagamo,anagaura,anagay,anagbaki,anagbe,anage,anagert,anaghd,anaghesht,anaghit,anaghrif,anaglog cross,anaglog cross roads,anagni,anagodu,anagogo,anagoji,anagonumogba,anagora,anagourf,anaguani,anagueis,anagumba,anagunji,anagup,anagustay,anaguvat,anagvay,anagyi,anagyia,anagzhogba,anah,anah khel,anahanahana,anahao,anahao daan,anahauan,anahauay,anahaw,anahawan,anahaway,anahbak,anahe,anaheim,anaheim hills,anahgari,anahi,anahibato,anahidambo,anahidrano,anahim lake,anahimalandy,anahimalemy,anahimasy,anahindrano,anahinunu,anahipisaka,anaho,anahola,anahor,anahuac,anahuanca,anahuane,anahuani,anahuayani,anahui,anahuili,anahuiloma,anahuir,anahuita,anahulloma,anahuna,anai,anaibat,anaiciai,anaicottai,anaika,anaikkoddai,anaikutti,anailakhola,anailkota,anaimalai,anaimalais,anaime,anaingpun,anaipanthy,anaippapan,anaire,anais,anaisuddapottanai,anaite,anaitha,anaitivu,anaivilundawa,anaiviluntan,anaiwoi,anaiyeh,anaiza,anaizeh,anaj,anaja,anaja dos pereiras,anajao,anajaon,anajas,anajateua,anajatuba,anajaz,anajazal,anajazinho,anaje,anajewo,anaji,anajiaka,anajpur,anajuru,anak,anak ayer kepong,anak ayer temarau,anaka,anakafandre,anakaga,anakairdibaih,anakalang,anakamanary,anakan,anakanda,anakanjan,anakao,anakapalle,anakaputtur,anakar,anakarany,anakas,anakasi,anakassi,anakataza,anakay,anakaya,anakdara,anakelo,anaketawewa,anakha,anakhanda,anakhatoon,anakhel,anakheyl,anakheyl,anakhina,anakhino,anakhitte,anakie,anakikro,anakino,anakion,anakir,anakitaina,anakkul,anaklia,anaklija,anakliya,anakoda,anakoki,anakoli,anakona,anakondo,anakondro,anakoul,anakovo,anakoy,anakpa,anakriya,anaksungai,anaktuvuk pass,anaku,anakum,anakup,anakwep,anakzai,anal,anal abajo,anal arriba,anal arriba y anal abajo,anala,analabe,analabemazava,analabesimpona,analabo,analabokidy,analabory,analachayoc,analadaro,analadinga,analafaly,analafandriambe,analafanja,analafasika,analafisaka,analafolaka,analahantsoa,analahova,analaila,analaitivu,analaiva,analaivo,analakamby,analakazo,analakazo-ambararata,analakely,analakengitsy,analakonjy,analakony,analalatsaka,analalava,analalentike,analalory,analamafana,analamafitaka,analamaha,analamahaimihoatra,analamahalana,analamahasoa,analamahavelo,analamahazo,analamahery,analamahitsy,analamaiky,analamainty,analamaitso,analamajiaky,analamalatra,analamalaza,analamalemy,analamalona,analamalotra,analamamy,analamanana,analamananga,analamanatrika,analamandro,analamanga,analamangahaza,analamangily,analamanintsy,analamanitra,analamanjaka,analamanjato,analamanohy,analamantsoa,analamara,analamaraha,analamarina,analamarina andrefana,analamarina ouest,analamaro,analamarotra,analamary,analamasina,analamasy,analamateza,analamatoza,analamatsaky,analamavo,analamavokaka,analamay,analamazaotra,analamazava,analambahy,analambanja,analamboanio,analamboko,analambola,analambolo,analambomavo,analambota,analamebo,analameky,analamena,analamiandrandra,analamiary,analamiditra,analamihoatra,analamikaiky,analamilona,analamiorika,analamipetraka,analamirafy,analamiranga,analamisaka,analamisakana,analamisampy,analamisampy ambany,analamisampy ambony,analamisampy sud,analamisary,analamisondrotra,analamitaha,analamitia,analamitsivalana,analamitsivatana,analamivory,analamohavelo,analamongy,analampamata,analampangana,analampary,analampasina,analampolitsy,analampotsy,analana,analanabe,analanakondro,analanalona,analanamaty,analanamaty i,analanamaty ii,analanambe,analanambola,analanampana,analanampotsy,analanana,analanandiana,analanangily,analanangitra,analanantsoa,analandia,analandramisave,analandraty,analandrofia,analanira,analanisandratsy,analaniton"a,analanivo,analanjaha,analanjahana,analanjohary,analano,analanolona,analanomby,analanovy,analapangitsy,analapasina,analapatsa,analapatsy,analapetsa,analaroa,analarova,analasarotra,analasatrana,analasoa,analatava,analatelo,analatsakay,analatsama,analatsara,analatsifaka,analatsimaimbo,analavato,analavelatsy,analavelona,analaveritry,analavinily,analavoka,analavondromahaky,analavory,analayoc,analazaza,analbania,analco,analedo,analetena,analeva,analgubi hal,analia,analila,analilwa,analionda,analiondas,analiontas,analipa,analipe,analipis,analipsi,analipsis,analiwa,analjondas,analkaddimady,anallap,anallapi,analli,analo,analoa,analoalo,analobe,analog,analokomby,analokova,analomink,analovana,analquito,analujeh,analum,analumi,analyonda,anam,anam banda,anam bandah,anam bostan,anam tiloa,anam tondi,anama,anamaa,anamaari,anamabo,anamaboe,anamabu,anamaca,anamadiaka,anamaduwa,anamakia,anamakiena,anamakro,anamalaho,anamalai,anamalainagar,anamaleh,anaman,anamangatare,anamanggut,anamanie,anamansa,anamapenin,anamaq,anamar,anamaray,anamari,anamaro,anamas,anamasamudrampeta,anamase,anamasi,anamaya,anamba,anambahitra,anambar,anambaruingol,anambatry,anambe,anambe balanta,anambiavy,anambo,anamboakatra,anamboambo,anambongan,anamborano,anamboriana,anambotaky,anambra,anambua,anambulu,anamdong,aname,anameha,anamenge,anameq,anamer,anamer ouznag,anamerho,anametan,anameur,anameyong,anamgol,anamias,anamido,anamiko,anamina,anaminskiy,anaminskiy nassovet,anamitaha,anamizu,anamloampa,anammer,anamobo,anamonsi,anamoose,anamoq,anamorita,anamorita de abajo,anamoros,anamosa,anamota,anamotamo,anamoue,anamouniela,anampoi,anamra,anamrane,anamrou,anamui,anamulenge,anamur,anamuren,anamuya,anamuyita,anamzhak,anan,anan linchang,ananevo,ananyevka,ananyevo,ananyevskiy,anan-nam,anana,ananaao,ananaboro,ananachi,ananadimby,ananaja,ananakaina,ananakania est,ananakarahitsy,ananakaraka,ananakengitra,ananakevitra,ananakirihitsy,ananakofandra,anananjaha,ananao,ananapuri colony,ananas,ananasoa,ananatuba,ananbaw,ananbin,ananbingon,ananchichi,ananchitsy,anand,anand nagla,ananda,ananda-koidiokro,ananda-kouassikro,anandabas,anandakassikro,anandakati,anandaku,anandala,anandale,anandana,anandanagar,anandapur,anandapuram,anandaua,anandayehan,anande,anandea,anandgarh,anandi,anandipur,anandnagar,anando,anando kouassikro,anandor,anandpur,anandpur katha singhwala,anandpur kota,anandpur sahib,anandpura,anandravy,anandrazo,anandribato,anandrivola,anandrobato,anandrobe,anandrotra,anandrovola,anandsar,anandwasi,anane,anane akuraa,ananea,ananeh,ananekrom,ananekurom,ananenka,anang,anangca,anangon,anangongo,anangra,anangu,anangui,ananguie,anangul,anangurai,anangurai tal,anangyan,ananho,anani,ananianpur,ananichi,ananikha,ananikrom,ananim,ananina,ananindeua,ananinga,ananino,ananinskaya,ananintsy,ananit,ananjaka,ananjaki,ananjandava,ananjara,ananjerd,ananjew,anankagiso,anankaso,anankeng,anankharkha,anankina,anankino,anankwin,ananohara,ananonas sur,ananong,ananorasa,ananoukope,ananovka,ananovo,ananovskiye,ananqiao,anansai,anansalla,anansaya,anansaya huasca,ananse,anansha,anansimi,anansimni,ananskoye,ananso,anansugya,anant peth,ananta,ananta gobindapur,anantabala,anantabari,anantagiripalli,anantagobindpur,anantai,anantaka,anantamadugu,anantamuchha,anantapa,anantapara,anantapur,anantapuramu,anantaram,anantarpuliyankulam,anantasagaram,anantasagram,anantasar,anantganj,ananthar puliyankulam,anantnag,anantocollo,anantoraka,anantpur,anantpura,anantsagar,ananum,ananur,ananuri,ananwan,ananya,ananyer,ananyev,ananyevka,ananyevskiy,ananyevskoye,ananyiv,anao,anao-aon,anaoan,anaolondewa,anaouargoute,anaouya,anaovandrano,anap,anapa,anapaca,anapacala,anapaftiria,anapaike,anapaly,anapamofy,anapao,anapasso,anapasy,anapavtiria,anapay,anapayufa,anape,anaphi,anaphotia,anapi,anapia,anapina,anapinar,anapinari,anapka,anapla,anaplades,anapladhes,anapoaka,anapog,anapog-alemania,anapog-sibucao,anapoima,anapolandia,anapolis,anapolis riza,anapoo,anapoto,anapra,anapskaya,anapskiy,anapuca,anapul,anapulu,anapuno,anapurus,anaqiz,anaquassacook,anaque putina,anaquito,anar,anar ab,anar bagh,anar butah,anar china,anar dar-e pain,anar darah,anar darrah,anar darreh,anar din,anar ed din,anar gul dhok,anar jar,anar jowy,anar joy,anar juy,anar khel,anar marz,anar mehr,anar od din,anar rud,anar shahwala,anar-bulak,anara,anara ambany,anara ambony,anarabad,anarabajy,anarabazaha,anarabe,anarabehavatsy,anarabemoka atsimo,anarabemoka nord,anarabemoka sud,anarabevahatsy,anarafaly,anarafito,anarafotaka,anaraja,anarak,anarak tisi,anarak-e `olya,anarak-e sofla,anaraki,anaral,anaralava,anaramalangy,anaramalinika,anaramanitsy,anarambo,anarambola,anaramihohoky,anaramiteraka nord,anaramiteraka sud,anaramiteraky,anaran,anaran pain,anaran-e pain,anaranda,anarane,anaraoe,anaratelo,anaratoka,anaravinelo,anarbag,anarbagh,anarbat,anarbe,anarbota,anarbotah,anarbulak,anarbuta,anarchuyo,anarchuyu,anarcs,anardan,anardar,anardar-e bala,anardara,anardarra,anardzhoy,anare,anarenda,anarene,anarestan,anarestanak,anarestaneh,anaretaka,anarga,anargai,anargar,anargay,anargyroi,anari,anari ki dahar,anaricapay,anarichi,anarida,anarifito,anaristan,anaristaneh,anarita,anarivo,anarjan,anarjoy,anarjuy,anark,anarkali,anarkhel,anarkheyl,anarkheyl,anarkul,anarleh,anarnath,anaro,anaro tangai,anaro tangay,anarobe,anaromalangy,anaromiteraka,anaroro,anarpur,anarqui,anarqui hacienda,anarrakhi,anarrozo,anarsa,anarsen,anarstan,anartu,anaru,anarud,anarum,anarun,anarutangay,anaruya,anarwala,anaryd,anaryiroi,anaryta,anarzar,anas,anas orco,anasa,anasagasti,anasai,anasam,anasambata,anasana,anasar,anasaran,anasawa,anasba-el-hadaj,anascancha,anascanchanan,anascapa,anascaul,anascipina,anasco,anascocha,anasei,anaselitsa,anaset,anaseuli,anaseur,anash,anashino,anashkino,anashkovo,anashmed,anasikely,anasindrano,anaskura,anaso,anasome,anasosopo,anasouli,anasovo,anaspampa,anasque,anassa,anassappo,anasser,anassin egou,anassreur,anastacia,anastacio,anastasevka,anastasyevka,anastasyevskiy,anastasavan,anastasevskaya,anastasia,anastasina,anastasino,anastasis,anastasiyevka,anastasiyevskaya,anastasiyevski,anastasiyevskiy,anastasopoulaiika,anastasopouleika,anastasova,anastasova megali,anastasova mikra,anastasovka,anastasovo,anastasovo pervoye,anastasovo vtoroye,anastassijewskaja,anastassjewka,anastassjewska,anastasyevka,anastasyevka vtoraya,anastasyevskiy,anastasyevskoye,anastazew,anastazewo,anastazow,anastro,anastschik,anasu,anasultan,anasy,anasztaziapuszta,anat-e sidiq,anat-kasy,anat-kasy-mamalay,anat-kasy-markovo,anat-kinyary,anata,anatabolo,anatadula,anataka,anataky,anatam,anatavory,anatayevka,anate-kili,anateri,anati,anati kili,anatico,anatio,anatipac,anatkas markovo,anatkas-abyzovo,anatkas-margi,anatkas-turunovo,anatkasy,anato,anatoa,anatobe,anatolyevka,anatolyevskoye,anatolevka,anatoli,anatoli kavagi,anatoli kavak,anatoliki,anatoliki frangista,anatoliko,anatolikon,anatolikos frangista,anatolin,anatoliy,anatoliya,anatoliya zvereva,anatoliyevka,anatoliyevskiy,anatolou,anatolskaya,anatolu feneri,anatolyevka,anatolyevskiy,anatone,anatonu,anatoyoc,anatratra,anatrial,anatrosh,anatsirika,anatswald,anatte,anattegoda,anatudu,anatule,anatundun,anatuya,anatysh,anatyu,anau,anaua,anauan,anauco,anaud,anaugi,anauiavy,anauk gangaw,anauk kabyu,anauk manan,anauk myinhlut,anauk wunbade,anauk-ywa,anaukbet,anaukkahtaing,anaukkon,anaukpyin,anauktaw,anaukudu,anaula,anaulundawa,anauma,anauna,anaura,anaurilandia,anaut-koy,anauti,anauwwai,anava,anavabe,anavaho,anavainen,anavais,anavan,anavanik,anavank,anavar,anavargo,anavargos,anavarza,anavatos,anavatti,anavayi-kalan,anave,anaver,anavgay,anavi,anaviary,anaviavimasina,anaviavy,anaviavy atsimo,anaviavy sud,anaviavymasy,anavible,anavieja,anavilis,anavilundawa,anaviq,anavissos,anaviz,anavlavy,anavlli hacienda,anavoha,anavoha-ambondrano est,anavoha-ambondrao ouest,anavra,anavrita,anavriti,anavriton,anavryti,anawa,anawa-i-kalan,anawacua,anawah,anawah-ye kalan,anawal,anawalt,anawamet,anawan,anawan malaki,anawan munti,anawana,anawang,anawangu,anaway,anawe,anaweya,anawil,anawilundawa,anawrang,anawula,anay,anaya,anaya de alba,anayaco,anayan,anayan-pauili,anayani,anayas,anayat bhatti,anayazi,anayazisi,anayazisi yaylasi,anaycancha,anaye,anayennisis,anayeno,anayeva,anayevo,anayia,anayo,anayoren,anaypampa,anaypazari,anaysh,anayurt,anaz,anaza,anazandamba,anazawa,anazco,anazhuoba,anaziri,anazive,anazo,anazogli,anazome,anb,anba,anba dher,anbabo,anbabou,anbabu,anbaek,anbaemi,anbaeteo,anbaeto,anbaga,anbagivi,anbahan,anbahovelo,anbaj,anbaji,anbak,anbakkol,anbala,anbaladoda,anbaliang,anbalsnes,anbamgol,anban,anbanas,anbanaz,anbandi,anbandok,anbang,anbanga,anbangoucun,anbanimaso,anbanji,anbannae,anbannaeri,anbaozi,anbaq,anbaq-e `olya,anbaq-e bala,anbaq-e hajjikhan,anbaq-e javad,anbaq-e pain,anbaq-e qadim,anbaq-e sorkha-ye vosta,anbaqin,anbar,anbar ab,anbar dan,anbar kalay,anbar koyu,anbar milah,anbar olom,anbar olum,anbar sar,anbar sara,anbar sefid,anbar tappeh,anbar-e `olya,anbar-e bala,anbar-e miani,anbar-e now,anbar-e pain,anbar-e sofla,anbar-e vasat,anbar-e zaruni,anbarab,anbarabad,anbarak,anbaralan,anbarci,anbarcik,anbarcilar,anbarcili,anbarciyan,anbardan,anbardar,anbardeh,anbardere,anbardzhik,anbargah,anbargah-e yakheh-ye geladel murt,anbargah-e yakheh-ye gelal murt,anbari,anbarkoura,anbarli,anbaroba,anbarpinarkoy,anbarqowl,anbarsar,anbarsay,anbarseki,anbarteh,anbarteh-e sofla,anbasglao,anbashskaya,anbastaq,anbastiq,anbatang,anbatiq,anbau,anbdour,anbe,anbed,anbeguwa,anbei,anbei linchang,anbeixi,anbela,anberarki,anberg,anberge,anberharki,anberi-goumbi,anberinarki,anbes,anbesame,anbesul,anbetsu,anbetu,anbey,anbh goth,anbi,anbian,anbianbu,anbiantang,anbianzhen,anbiele,anbigny,anbing,anbis,anbo,anbobra,anbodong,anbodusugol,anbogwangni,anbokhta,anboksa,anbol,anbolmae,anbombongo,anbomi,anbomni,anbomsan,anbong,anbongbawi,anbongchon,anbongdong,anbonggol,anbongni,anbopri,anbopsudong,anborobe,anboufinou,anbouta,anbre,anbu,anbu zhen,anbudian,anbudong,anbuh,anbuh-e bala,anbuh-e mashayekh,anbuh-e pa`in,anbuhkhak,anbukh,anbulmigol,anbun,anbuntem,anbushan,anbusom,anbuzu,anbyanpu,anbyollisil,anbyon,anbyonup,anca,ancabasse,ancabine,ancacato,ancacc,ancache,ancacollo,ancacora,ancadaque,ancadena,ancadona,ancaen,ancaeun,ancagal khune,ancagual,ancagume,ancahuachana,ancahual,ancahuasi,ancaiano,ancailou,ancajan,ancajibe,ancajuli,ancak,ancal,ancalan,ancale,ancalhe,ancalho,ancallachi,ancallapo,ancalle,ancamani,ancamarca,ancamba,ancaminha,ancaminho,ancamona,ancamone,ancaname,ancanasa,ancaneale,ancangshang,ancaninho,ancano,ancapampa,ancaqov,ancaquea,ancaquia,ancar,ancar kulon,ancar tengah,ancar wetan,ancara,ancaran,ancarani,ancarano,ancaravi,ancari,ancaro,ancas,ancasccocha,ancascocha,ancascocha dique,ancasi,ancasoro,ancaster,ancasti,ancata,ancate,ancaya pampa,ancayacu,ancayuconi,ancazoro,anccas,anccase,anccasuyo,ancco,anccoccahua,anccoccota,anccochoco,anccontana,ance,ance muizas centrs,anceaumeville,ancede,ancee,anceis,ancek,ancelin,ancelle,ancelmo,ancemont,ancene,ancenes,anceney,anceni,ancenis,anceo,anceriz,ancerville,ancerville-gue,ancerville-sur-nied,ancerviller,ancette,anceu,anceur el bia,ancey,anchano,anchebi,anchikorer,anchini,ancha,ancha palca,anchaca,anchacahuasi,anchacalla,anchaccui,anchacotana,anchaenggi,anchagal khune,anchagua,anchai,anchaili,anchak,anchakovo,anchal,anchala,anchalay,anchalcapo,anchalli,anchamasa,anchamps,anchandong,anchang,anchang tung,anchang zhen,anchangbei,anchangchen,anchangdong,anchangjuk,anchangmoru,anchangni,anchangping,anchangshih,anchangwei,anchangxian,anchanh bung,anchanni,anchao,anchaotsun,anchapalla,anchapilla,anchapillay,anchaplaya,anchaqui,anchar,ancharo,anchasorski,anchasorskiy,anchau,anchaud,anchaux,anchayacu,anchayaque,anchayllioc,anche,ancheh,anchekrak,anchen,ancheng,anchengfang,anchengzhen,anchenin,anchenki,anchenli,anchenoncourt,anchenoncourt-et-chazel,ancheplaya,ancherikovo,ancherikovskiy,ancherinovskiy,ancherrou,ancheta,ancheto,anchetti,anchhoha,anchi,anchi awgi,anchi baghbanan,anchia,anchiachuang,anchiachuangchen,anchiaho,anchiakou,anchiala,anchialo,anchialos,anchialou,anchiang,anchiangchen,anchiangtun,anchiangying,anchianhsiang,anchiano,anchiao,anchiaohsiang,anchiapu,anchiashe,anchiatsao,anchiaying,anchiayingtzu,anchicaya,anchicha,anchico,anchico nuevo,anchicocha,anchicocha bajo,anchien,anchieta,anchigorovo,anchiguay,anchihai,anchihaitai,anchihsien,anchihuay,anchijelo,anchik,anchikhai,anchiki,anchikkasy,anchikovo,anchil,anchil-matso,anchila,anchilatli,anchilejo,anchilhuay,anchiliao,anchilo,anchimaja,anching,anchingcheng,anchingkou,anchingli,anchingshih,anchione,anchipolovka,anchique,anchiri,anchirigol,anchirikova,anchitso,anchitsun,anchiu,anchiuhsien,anchivay,anchiyevitsy,anchkina,anchkini,anchkrir,anchkuna,ancho,anchocara,anchocotana,anchocotara,anchodaya,anchoke,anchokey,anchokrak,anchola,ancholli,anchon,anchona,anchong,anchonga,anchongkou,anchongmori,anchongni,anchonmal,anchonmani,anchopata,anchopaya,anchor,anchor bay,anchor bay gardens,anchor bay harbor,anchor bay shores,anchor landing,anchor mill,anchor point,anchorage,anchorage anchors,anchorage park mobile home park,anchorena,anchoris,anchorope,anchorupe,anchorville,anchorway,anchou,anchovira,anchovira alta,anchovira baja,anchovy,anchovy bottom,anchovy station,anchow,anchu,anchua,anchuah town,anchuan,anchuang,anchuangpo,anchuanpu,anchuara,anchuchen,anchuela del campo,anchuela del pedregal,anchuelo,anchugova,anchugovo,anchuki,anchul,anchuna,anchung,anchungdogi,anchungli,anchungoogi,anchungoumen,anchunkoumen,anchununni,anchuras,anchurikha,anchushih,anchushui,anchutina,anchutino,anchutkino,anchuyo,anchwirifu,anchyr,anci,ancia,anciaes,anciana,anciano,anciao,anciaqui,ancici,anciems,ancien akaba,ancien andok,ancien bakani,ancien bambelo,ancien beina wayo,ancien beka,ancien beni,ancien bera njoko,ancien beya-bashobo,ancien bibaya,ancien bo,ancien bodanga,ancien botoro,ancien chimpeze,ancien dangado,ancien dangoro,ancien degrad fourmi rouge,ancien degrad francis,ancien degrad jougla,ancien degrad nicole,ancien degrad vitalo,ancien diambola,ancien diangala,ancien dola,ancien douar nkaka,ancien founa,ancien gombou,ancien goubere,ancien kandjama,ancien kivoko,ancien koumassi,ancien mabanzi,ancien madingo,ancien matica,ancien meme,ancien metark,ancien mimongo,ancien ncesse,ancien ngonbe,ancien ngoukou,ancien nioungui,ancien ntokossiala,ancien okaka,ancien okala,ancien otala,ancien papaichton,ancien poste del meridj,ancien poste des apindji,ancien poste madingo,ancien poste militaire dito,ancien sandjala,ancien sapoua,ancien sengbe,ancien soulac,ancien tapourou,ancien tchicongoula,ancien village akakg,ancien village allim,ancien village de gara,ancien village hubert,ancien village mathurin,ancien village ngam,ancien village ngouassa,ancien village poteau carre,ancien vivier,ancien vouvou,ancien zarou,ancien zemongo,anciene station de loued kheir,ancienne abbaye daulne,ancienne lorette,anciens,ancient oak,ancient oak north,ancient oak west,ancient oaks,ancient panchu,ancienville,ancier,anciferova,ancihuacaro,ancik,anciles,ancillo,ancin,ancinnes,ancioa,anciora,ancipova,anciray,anciskiai,anciskis,anciskiu,ancizan,ancizes-comps,anckini,anckuli,anclahan,anclon,anclon y arenal,anclote,anco,anco anco,anco baba,anco marca,anco waiwai,anco-aque,ancoa,ancoamaya,ancoamayo,ancoanco,ancoaque,ancobamba,ancobar,ancober,ancocahua,ancocala,ancocalane,ancocalani,ancocamana,ancocatani,ancoccahua,ancochaque,ancochuco,ancocirca,ancocola,ancocolla,ancocollo,ancocolo,ancocoto,ancocucho,ancocullo,ancohaque,ancohua,ancohuma,ancohuyo,ancol,ancol barat,ancol chico,ancol chiquito,ancol dua,ancol grande,ancol satu,ancol timur,ancola,ancolacane,ancolacaya,ancolaccaya,ancolaya,ancolayma,ancollagua,ancollo,ancom,ancoma,ancomarca,ancomarca hacienda,ancomaya,ancon,ancon de arriba,ancon de carros,ancon de iturbe,ancon de iturre,ancon de yturre,ancon garza,ancona,anconasa,anconcito,ancone,anconella,ancones,anconhane,anconhi,ancopado,ancopadre,ancoparqui,ancopate,ancopiteac,ancopoto,ancopujo,ancoque,ancoquishcachayoj,ancor,ancora,ancorados,ancoraimes,ancorane,ancorano,ancori,ancoronho,ancorora,ancorovi,ancoruma,ancos,ancosuni,ancot,ancota,ancoua gamji,ancoukki,ancourt,ancourteville,ancourteville-sur-hericourt,ancovilque,ancovinto,ancovo,ancoyane,ancoyo,ancoz,ancram,ancramdale,ancretieville,ancretieville-saint-victor,ancretteville,ancretteville-sur-mer,ancroft,ancrum,ancteville,anctoville,anctoville-sur-bosc,ancu,ancuabe,ancuaque,ancuaze,ancubaira,ancud,ancue,ancueza,ancuh,ancuha,ancuirre,anculai,ancumba,ancumbo,ancun,ancun jiedao,ancuna,ancuninga,ancunzi,ancuri,ancuringo,ancut,ancuta,ancuty,ancux,ancuya,ancuylla,ancuyo,ancuz,ancveiri,ancveirini,ancverini-i,ancvirini,ancy,ancy-le-franc,ancy-le-libre,ancy-sur-moselle,ancypery,ancyra,and,and abad,and aja,and aymetir,and iyela,and kirzan,anda,anda mariam suta,anda ulpota,andaua,anda-bazar,andab,andab-e jadid,andab-e qadim,andabaci,andabad `uliab,andabad oliab,andabad-e `olya,andabad-e bala,andabad-e sofla,andabakal,andabamba,andabao,andabay,andabet,andabil,andabolava,andaboloaka,andabomadinika,andabomahebo,andabomalinika,andabonimaholy,andaboru,andabotoka,andabotoka nord,andabotokana,andabozaro,andabozato,andabre,andabuen,andac,andaca,andacaba,andacancha viejo,andacave,andachaca,andachaca viejo,andachira,andachires,andacli,andacolla,andacollo,andacs,andacun,andad,andadeh,andadero,andadja doyou,andadja sisi,andadoano,andadola,andadolagama,andadowi,andae,andaean,andaebyol,andaedong,andaegol,andaemi,andaeri,andaeul,andafagbene,andafia,andafiandakana,andafiatsimo,andafiatsinanana,andafiavaratra,andafibe,andafiha,andaforo,andaftanomby,andafugan,andafy,andagahanatota,andagala,andagao,andagaw,andagay,andagda,andagh,andagna,andagone,andagoya,andagoye,andagua,andaguala,andagualo,andagul,andah,andaha,andaha samu,andahan daji,andahaza,andahazapuszta,andahena,andahindrano,andahivozaka ambany,andahivozaka ambony,andahivozaky,andahony,andahrti,andahua,andahuailas,andahuailillas,andahualo,andahuasi,andahuavlas,andahuayasi,andahuaylas,andahuaylillas,andahuaylla,andai,andaia,andaihou,andaimarca,andaing,andaingo,andaingo gara,andaingomboalavo,andainho,andainville,andainville-aux-bois,andaivozaky,andaj,andajes,andak,andaki,andaka,andakaga,andakaka,andakalaka,andakampony,andakana,andakana andrea,andakana-ambolomborona,andakandrano,andakanimazano,andakanimilay,andakapeta,andakary,andakata,andakato,andakatomielo,andakatomihelo,andakatondraotomba,andakatotsaka,andakh,andakit,andako,andakombe,andaks,andaky,andal,andal chachar,andal ghangro,andal junction,andal malik,andala,andala sonchalwe,andalaca,andalakaolo,andalaly,andalambato,andalambezo,andalamby,andalampamba,andalan,andalanda,andalandy,andalanimamo,andalapito,andalara,andalas,andalas 2,andalas dua,andalatanosy,andalatsarotra,andalaye,andalbi,andale,andalely,andalen,andales,andalgala,andalgimul,andalgol,andalhe,andalhuala,andali,andalien,andalina,andalipito,andalirano,andalivao,andalla,andalle,andalmailillas,andalo,andaloeto,andalona,andalopito,andalouciene,andalouses,andalpota,andalsnes,andalsvagen,andaltong,andalu mahante,andalucas,andalucia,andalucien,andalugoda,andalumo,andaluri,andalusia,andaluto,andaluwa,andaluz,andam,andama,andamaky,andamanolobe,andamarca,andamarca hacienda,andamasina,andamata,andamayo,andamayo hacienda,andambalo,andambario,andambinana,andambomitro,andambozato,andamga,andamil,andamilamy,andamoei,andamooka,andamotiabo,andamotibe,andamotinambo,andamoto,andamoty,andamoukou,andampane,andampibe,andampihely,andampikely,andampilava,andampimena,andampinjaza,andampohuwa,andampy,andampy nord,andampy sud,andamui,andan,andanadamy,andanan,andanankattuwa,andanankatuwa,andanasari,andanawa,andance,andancette,andanda,andandaiesi,andandihazo,andandoni,andandora,andang,andanga,andanga ndumbo,andangavato,andangerd,andanggabu,andanggol,andangilangy,andangimboay,andangin,andangiro,andangni,andango,andangoraikitra,andangotra,andangovato,andani,andanim,andaninimbegagy,andanivato,andanja,andanjerd,andanjo,andankani,andankulam,andanoa,andanona,andanonana,andanpahura,andansari,andant,andanzas del valle,andao,andaoluwa,andapa,andapabe,andapabo,andapafito,andapahamina,andapahely,andapakely,andapan selatan,andapan utara,andapanampombo,andapanangohy,andapanangoy,andapanaomby,andapandrano,andapanomby,andapar,andaparatibe,andaparaty,andapata,andapavao,andapibe,andapicho,andapinarosy,andapo,andapolakanda,andapy,andaq,andaqiha,andar,andar ab,andar abi,andar char,andar kachh,andar kala,andar koli,andar paya,andara,andara,andarab,andarabi,andarabisat,andarabiya,andaradeniya,andaragasmulla,andaragasyaya,andaragollewa,andarahy,andarai,andarai kats,andarak,andaral,andaran,andarandagum,andarapa,andarapane,andarapona,andaraq,andararavina,andaratranina,andarawewa,andaray,andarayan,andarayan burgos,andarbag,andarbak,andarban,andarbog,andarbug,andard,andare,andareh,andaresbay,andaresh bay,andarewa,andarfo,andargam,andargan,andargazi,andarghach,andarhan,andarhom,andari,andariales,andaribe,andarico,andariel,andarigol,andarik,andarila,andarish bay,andarivi,andariyeh,andarjin,andark,andarkala,andarkhan,andarla doga,andarlacak,andarlachak,andarlachak naw,andarlah chak,andarma,andarman,andarmanik,andarmun,andaro,andaroa,andarob,andarokh,andarood,andarovo,andarpa,andarqash,andarqeh,andarraso,andarreal,andarreh,andarriales,andarsay,andarsoy,andarsul,andartikon,andartkhi,andarud,andarud `olya,andarud sofla,andarud-e `olya,andarud-e bala,andarud-e pain,andarud-e sofla,andaruk,andarukh,andarul,andarun,andarval,andarvar,andarwaai,andarwai,andarwal,andarwali,andarwalii,andarwan,andarzhir,andarzi,andas,andasen,andasialava,andasibe,andasibe mahaverika,andasibe mto,andasibe-andapatoa,andasibe-kobahina,andasibefasy,andasilaikatsaka,andasimadamo,andasimaro,andasimazava,andasimbary,andasimbazaha,andasimiasava,andasiniadiana,andasinimaro,andasinindriatsara,andasirotsaka,andasiva,andasivao,andasivolo,andasivy,andasoa,andasy,andasy ambony,andasy piquie,andata,andati,andatomalama,andatony,andatsaka,andatsakala,andaty,andau,andaulpotagama,andaung pech,andaung pich,andaung por,andaung tuk,andaura,andautonia,andavabary,andavabato,andavabaza,andavabiby,andavaboay,andavabosy,andavadambo,andavadoaka,andavadrafia,andavadrovia,andavaiyufa,andavaka,andavakaka,andavakakoho,andavakamenarana,andavakantsantsa,andavakarefo,andavakoana,andavakoera,andavakomby,andavakonko,andaval,andavaloaka,andavamenara,andavan,andavanomby,andavaposa,andavar,andavarangana,andavaravina,andavena,andaveno,andavias,andavilque,andavit,andaviyufa,andaw,andawai,andawala,andawalagama,andawalayaya,andawang,andawei,andawela,andawelagama,andawelayaya,andawila,anday,andayakaka,andayan,andayban,andaymarca,andaymayo,andaymayo hacienda,andaz khan,andaza,andazaq,andazhuang,andazuce,andby,andchar,ande,ande fiord,ande hinon,ande-ande,andeade,andeamarup,andean,andeande,andeate,andebabe,andebadeba,andebadebakely,andebe,andebodatsaka,andebol,andebolle,andechs,andechy,andedao,andeefuka,andeer,andefjord,andefo,andefopaly,andeg,andeh,andeh lus,andehbil,andehona,andei,andeinada,andeiro,andeja,andeka,andekaleka,andekanda,andekantor,andekarimbo,andeket,andekh,andeko,andekou,andekwakwa,andekyo,andel,andela,andelain,andelakaka,andelan,andelan kidul,andelandranomaro,andelankaka,andelanto,andelara,andelara 2,andelara 3,andelaroche,andelarre,andelarrot,andelat,andelate,andelaye,andele,andelek,andelene,andelewa,andeleya,andelfingen,andeli,andelici,andelinkovci,andelka,andelkovska mahala,andelmaa,andelnans,andelot,andelot-en-montagne,andelot-les-saint-amour,andelshofen,andelska hora,andelske zleby,andelst,andelu,andem,andem i,andem ii,andem iii,andem yarse,andemabe,andemaka,andemaka atsimo,andemaka avaratra,andemaka sud,andemaki,andemaky,andemaky andrefana,andemaky est,andemaky ouest,andeman,andemawo,andemba,andembe,andembilemisy,andembinilemisy,andembiry,andemby,andemby-tanamasy,andeme,andemi,andemobe,andemodemoky,andemoka,andempo,andempombe,andemtenga,anden,anden at the woods,anden bhrami,anden bhurmi,andenas,andenelle,andenes,andenes hacienda,andenggaumo,andengilengy,andengimboay,andengindroa,andengo,andengondroy,andengu,andengue,andenhausen,andeniya,andenne,andenoka,andenpampa,andeo,andepah,andepali,andepandambo,andeposando,andeqan,ander,andera,anderaby,anderaccia,anderacha,anderachia,anderai,anderamboukan,anderamboukane,anderanboukane,anderapona,anderarfvet,anderarvet,anderas,anderasen,anderasi,anderat,anderath,anderavdzh,anderbag,anderbak,anderbe,anderbeck,anderbo,anderby,andere,andereh,anderekiri,anderelyangar,anderemane,anderen,anderenbroek,anderes,andergan,anderghen,andergrove,anderi,anderik,anderika,anderjaf,anderjiha,anderlecht,anderlingen,anderlues,anderma,andermansdorf,andermatt,andernach,andernay,anderni,andernos,andernos-les-bains,anderny,andero,anderob,andersbenning,andersberg,andersbo,andersbodarna,andersbole,andersby,andersbygget,andersdalen,andersdorf,andersfors,andershausen,andershof,anderskofen,anderskog,anderskogen,andersleben,anderslov,andersmark,andersnaset,anderson,anderson acres,anderson bayview,anderson city,anderson creek,anderson crossroads,anderson ferry,anderson heights,anderson junction,anderson landing,anderson mill,anderson mulawa,anderson place,anderson river,anderson subdivision,anderson town,anderson trailer court,anderson tully,anderson ways,anderson west acres,andersonburg,andersoni,andersonia,andersons,andersons camp,andersons corner,andersons crossroads,andersons mill,andersons town,andersontown,andersonville,andersro,anderssvedja,andersta,anderstorp,anderstrask,anderstrup,andersvattnet,andersviksberg,andert,andert-condon,andert-et-condon,anderten,anderton,anderup,andervag,andervenne niederdorf,andervenne oberdorf,anderyan,andes,andesite,andesomby,andest,andesta,andetra,andetsa,andetsy,andeue,andeulay,andevadhu,andevan,andevanahalli,andevanne,andevil,andeville,andevin,andevizo,andevonomby,andevorante,andevoranto,andewung,andeyevka,andeyingzi,andezeno,andezit,andfiska,andfiskaa,andfiskaaen,andgaard,andgard,andhania,andhar manik,andharail,andhari,andhariapara,andharkaneh,andharkota,andharkotha,andharmanik,andharpara,andhe ji kassi,andheji kasi,andher,andhera bela,andherchar,andheri,andhgarh,andhiari,andhir,andhori,andhra tharhi,andhra thari,andhrail,andhravasisa,andhriolata,andhritsa,andhritsaina,andhromiloi,andhroni,andhronianoi,andi,andi garbokoum,andi kola,andi-kamis,andi-kenopi,andia,andial,andiama,andiambalam walpola,andiambalama,andiambiby,andian,andianapolis,andianasadava,andianbadja,andianbaja,andianou,andiarovo,andias,andiawela,andibari,andibonara,andice,andicen,andici,andicona,andicona-elizondo,andida,andideniya,andidimiady,andido,andidy,andidzhan,andidzhani,andielava,andiesen,andiesenhofen,andifilippoi,andifli,andigama,andigedara,andigird,andignato,andigne,andigoda,andigolinga,andigon,andigonia,andigonon,andigonos,andihiang,andihindagolla,andiho,andihona,andijan,andijani,andijk,andijk oost,andijk west,andijon,andik,andika sur mer,andikalamos,andikan,andikana,andikh,andikira,andikoi,andikona,andil,andil wetan,andila karib,andila saba,andilakaka,andilakanka,andilamavo,andilamb,andilamba,andilambe,andilamboahangy,andilamboay,andilamena,andilamenakely,andilamihoatra,andilamimahoatra,andilampany,andilana,andilana atsimo,andilana avaratra,andilanaomby,andilanatoby,andilandalina,andilandrana,andilandrano,andilankely,andilanomby,andilar,andili,andilla,andillac,andille,andilly,andilly-en-bassigny,andilly-les-marais,andilmboay,andily,andim,andima,andimadam,andimadoa,andimaka,andimaka i,andimaka mahatsoa,andimakabo,andimakhia,andimaky,andimaky-dokolahy,andimalla,andiman,andimatam,andimbinimafy,andimen,andimi,andimorana,andimulla,andimunai,andin,andina,andina-firaisana,andinapoles,andinas,andindo,andineeme,andineme,andinete,anding,anding shierzu,anding zhan,andingadingana,andingan,andinggaumo,andingilingy,andingilingy atsimo,andingilingy avaratra,andingilingy nord,andingilingy sud,andingoza,andingpu,andini,andinira,andino,andinu,andinuela,andio,andiobe,andiolava,andione,andiong,andioume,andiparos,andipata,andipata erisou,andipaxos,andiperni,andipernoi,andiphilo,andipor,andippatti,andipur,andir,andir 2,andir dua,andir kulon,andir l,andir langgar,andir tengah,andir wetan,andira,andiramada,andiran,andiras,andiraz,andirea,andiri,andirin,andirjan,andirlangar,andiro,andiroba,andirobal,andiroga,andirpar,andirpurnajaya,andirrion,andirushk,andis,andisa,andisaka,andiseni,andisheh,andisi,andiskarion,andisleben,andissa,andissene,anditsaka,andivakadawala,andivar,andivaz,andiwos village,andiyagala,andiyagoda,andiyakadawala,andiyakalla,andiyamalatenna,andiyankulama,andiyapuliyankulam,andiyur,andiz,andizeh,andizhan,andizhan vtoroy,andja,andja asundus,andja-iba,andjala,andjamango,andjamen,andjamena,andjara,andjatan,andjebi,andjeguere,andjek,andjekapa,andjemena,andjha,andji,andjia,andjialay,andjide,andjimana,andjissara,andjitolakri,andjitoyo,andjogo,andjoko,andjoko ii,andjon,andjongan,andjou,andjouk,andkaer,andkari,andkhera,andkhoi,andkhoy,andkhui,andkhvoy,andkilen,andkua,andlabo,andlau,andlau-au-val,andler,andlerka,andlersdorf,andling,andlor,ando,ando akaa,ando bait,ando beyd,ando blekro,ando gbegame,ando khosa,ando koua kauamikro,ando reo,ando wawaso number 1,ando wawaso number 2,andoabatomanga,andoain,andoamgol,andoany,andoas,andoas nuevo,andoavondro,andoba,andoback,andobeid,andobo,andobobe,andobodobo,andoboka,andobokala,andobomana,andobotoka,andobrofu,andoc,andock,andocs,andocutin,andod,andodalmi,andode,andodong,andoe,andoearsepan,andofoue,andoga,andogno,andogon,andogskiy,andoguri,andohabato,andohabatomanga,andohaboba,andohady,andohafarihy,andohafary,andohafotsy,andohahonko,andohahoraka,andohajango,andohakaolo,andohakaomby,andohala,andohalafy,andohalampy,andohalo,andohamafia,andohamahatao,andohamambala,andohamaroilo,andohambato,andohamboay,andohamitaha,andohana,andohanampivalanana,andohanankivoka,andohanaomby,andohanapaja,andohanavony,andohaniangaty,andohanilaka,andohanimanga,andohanimendranomavo,andohanisaly,andohanisoa,andohanizengitra,andohanosy,andohapatsakana,andohara,andoharaka,andoharana,andoharano,andoharanodoalo,andoharanofotsy,andoharanomaitso,andohariana,andoharimbo,andoharotsy,andohasaha,andohasahabe,andohasakoa,andohasandra,andohasatra,andohasifaka,andohasifaky,andohatanety,andohatany,andohatona,andohavary,andohaviana,andohavolo,andohavondro,andohavondrona,andohavovo,andohazampona,andohazompo,andohazompona,andohibe,andoho,andohobe,andohonko,andohorano,andoimby,andoin,andoins,andoj,andojang,andojanggol,andok,andoka,andokakro,andokas,andokasi,andokchi,andokh nfang,andokoi-kokrekro,andokombikely,andokone,andokoua-kouamekro,andokoy-kokorekro,andokro,andokro kouakou,andoktong,andokwaa,andol,andola,andolabe,andolameeto,andolamosa,andoleio,andolgol,andolina,andolli,andollu,andolo,andoloabo,andolobe,andolobetroka,andolobory,andolofotsy,andolomahery,andolomasy,andolomay,andolomikaiky,andolomitahy,andolonitako,andolonizimirahalahy,andoloy,andolsheim,andoltari,andoltong,andoluhisaru,andolukavagi,andolukhisar,andolukhisaru,andoluwa,andom,andom i,andom ii,andoma i,andoma ii,andoma iii,andomaja,andomaka,andombirolava,andombiry,andombiry anavora,andombiry nord,andomboka,andombotombo,andome,andome i,andome ii,andomotra,andomotra atsimo,andomsk,andomskiy,andomskiy pogost,andon,andon poci,andon-poc,andona,andona sughaiyir,andona sughayyir,andonabe,andonabe atsimo,andonabe-ranomena,andonadraty,andonae,andonaiika,andonaka,andonakaomby,andonakely,andondabe,andondahe,andondona,andong,andong pring,andong thom,andong tramang,andong tuk,andong zhen,andongbag,andongbang,andongcili,andongdong,andongirotsy,andonglawak,andongmakkol,andongne,andongni,andongo,andongokely,andongona,andongondongo,andongoza,andongozabe,andongozahely,andongpo,andongrejo,andongsari,andongsari lor,andongsili,andongsilibaru,andongwei,andongwei jiedao,andongy,andongzhen,andonhavary,andonis,andonno,andonosari,andonuana,andonville,andoo,andoolo,andoon,andoonuhu,andoor,andop,andopal,andopen,andopitaly,andor,andora,andora acres,andore,andoreti,andorf,andorhaza,andorhazamajor,andorhegy,andori,andoria,andorick acres,andorinha,andorinhas,andorinho,andorisa,andormajor,andornaktalya,andornay,andorno cacciorna,andorno micca,andoro,andorokh,andoromby,andorora,andororo,andorra,andorra la vella,andorra woods,andorra-vieille,andorre,andorre-la-vieille,andorre-vieille,andorsoit,andosawol,andosilla,andosy picquie,andotsy,andottawa,andou,andou mbato,andou mbatto,andouako,andoucun,andouf,andoufoue,andouille,andouille-neuville,andouin,andoulbin,andoum,andouma,andoume,andoumeze,andoumont,andoumou,andoung,andounou,andour,andous,andouz,andova,andovaky,andovce,andover,andover estates,andover golf estates,andover junction,andover lakes estates,andoversford,andovia,andovo,andovodava,andovohatany,andovoka,andovokabe,andovoko,andovokobe,andovokoko,andovokonko,andovoky,andovolalina,andovomena,andovoranto,andovotroatsy,andovotsifefy,andowari,andowengga,andowia,andowinga,andowir ghari,andowj,andoy,andoyiali,andoyo,andozero,andozokavaky,andra,andra ser,andra seri,andra-ata,andraa,andrabaka,andrabo,andraboraina,andraca,andracas,andracha,andrada,andrada maria de menacabarrena,andradas,andrade,andrade araujo,andrade corner,andrade costa,andrade marin,andrade pinto,andradina,andraes,andraetoko,andrafainkona,andrafanandriana,andrafia,andrafiabe,andrafiafaly,andrafiakely,andrafialatsaka,andrafialava,andrafialava atsimo,andrafiamadinika,andrafiamadiniky,andrafiamanjera,andrafiamaty,andrafiamba,andrafiamena,andrafiameniaba,andrafiamirantina,andrafiamivory,andrafianahoany,andrafiaraiky,andrafiatoka,andrafiatokana,andrafiatsimikiso,andrafiavelo,andrafiavoroka,andrafiezinara,andrafitatra,andrafra,andrah kalan,andraha,andrahabarana,andrahafanjaka,andrahalana,andrahanga,andrahangy,andrahanjo,andrahary,andrahavia,andrahavy,andrahibo,andrahibobe,andrahibokely,andraholava nord,andrahomana,andrahomanitsy,andrahosoa,andraibo,andraidoka,andraijoro,andraikarekabe,andraiketalahy,andraiketalay,andraiketamalemy,andrail,andraimasina,andraimavo,andraina,andrainarivo,andrainibe,andrainjato,andrairay,andraisoro,andraitsintsina,andraitx,andraizaha,andraja,andraka,andrakadila,andrakaka,andrakaraka,andrakarakabe,andrakata,andrakavitra,andrakea,andraketa,andraketalahy,andrakia,andrakodavaka,andrakodia,andrakov,andrakovo,andrakoy,andral,andralandy,andrama,andramadinika,andramahimbo,andramaimbo,andramaka,andramalao,andramalaza,andramalo,andramanalina,andramanero,andramanolobe,andramaray,andramari,andramasay,andramasina,andramasy,andramba,andramba ambony,andramba haut,andrambalo,andrambita,andrambo,andrambobe,andramboeto,andrambohetra,andrambolava,andrambomaro,andrambonivory,andrambontany,andramboro,andrambotsivilehina,andramikely,andramiory,andramirava,andramita,andramizaha,andramoita,andramoro,andramorosa,andramosabe,andrampamonjy,andrampia,andrampiloha,andramy,andramy atsimo,andramy avaratra,andramy nord,andramy sud,andranabolava,andranaka,andranakanga,andranalafotsy,andranaly,andranamba,andranambolava,andranambomaro,andranamby,andranamena,andranamy,andrananja,andranaomby,andranata,andranatsiritra,andranavao,andrandava,andranday,andranfanzava,andrangabitika,andrangana,andranganala,andrangany,andrangaranga,andrangatiky,andrangavato,andrangavolo,andrangazaha,andrangazava,andrangolaoka,andrangory,andrangovato,andrangy,andraniala,andranialamihamba,andraniaomby,andranira,andranivoro,andranjomanga,andrano,andrano lava,andrano maria,andrano miolaka,andrano vorofaly,andranoabo atsimo,andranoabo avaratra,andranoaionjy,andranoambobe,andranoambolava,andranoampandraha,andranoampandrana,andranoampango,andranoampototra,andranoba,andranobahatra,andranobe,andranobe ouest,andranobetsisilaoka,andranobevava,andranobilo,andranoboa,andranoboahangy,andranoboaka,andranoboaka andremafana,andranoboaka atsinanana,andranoboaka nord ouest,andranoboaky,andranoboka,andranobolahy,andranoboribe-ankatsaka,andranobory,andranobozaka,andranodehoka,andranofaly,andranofanjava,andranofanjavao,andranofasika,andranofeda,andranofeno,andranofito,andranofotitra,andranofototra,andranofotsy,andranofoty,andranogidroboaka,andranogisa,andranogiso,andranogisy,andranogoa,andranogoaika,andranohambolany,andranohazokily,andranoherika,andranohinalahy,andranohinaly,andranojirieky,andranojirioka,andranojirioky,andranojoby,andranojongifotsy,andranojongy,andranokaolo,andranokaolo toby,andranokazany,andranokelilalina,andranokely,andranokoaka,andranokobaka,andranokoditra,andranokontona,andranokototo,andranokova,andranokozany,andranolalika,andranolalina,andranolatsaka,andranolava,andranolavabe,andranolavakely,andranolo,andranolojy,andranomadio,andranomadiro,andranomafana,andranomahavelona,andranomahery,andranomahitsy,andranomainty,andranomainty-andoharano,andranomaitso,andranomaitsokely,andranomalaza,andranomalaza atsimo,andranomalazo,andranomalio,andranomaloto,andranomamibolo,andranomamibolona,andranomamikely,andranomamoa,andranomamorery,andranomamy,andranomanandray,andranomananika,andranomanara,andranomandeha,andranomandevy,andranomandry,andranomanelatra,andranomanenty,andranomanesika,andranomanesika avaratra,andranomanesika nord,andranomanesika sud,andranomanety,andranomanga,andranomangarika,andranomangatsa,andranomangatsiaka,andranomangily,andranomanilia,andranomanintsa,andranomanintsy,andranomanitra nord,andranomanitra sud,andranomanitsy,andranomantsy,andranomaraily,andranomare,andranomarelia,andranomaria,andranomarina,andranomarivo,andranomaro,andranomasina,andranomasy,andranomat,andranomatana,andranomatavy,andranomatraba,andranomavo,andranomavo avaratra,andranomavokely,andranomay,andranomayo,andranomazava,andranombainga,andranombao,andranombary,andranombiavy,andranombila,andranombilo,andranombilo-antsirasira,andranomboahangy,andranomena,andranomena atsinanana,andranomena est,andranomena nord,andranomena sud,andranomenabe,andranomenabesabora,andranomenanibeala,andranomenatsa,andranomendeha,andranomeva,andranomfana,andranomiady,andranomiantra,andranomiboa,andranomiboaka,andranomiditra,andranomidy,andranomiely,andranomiely atsimo,andranomiely avaratra,andranomiely-nord,andranomiely-sud,andranomifankatia,andranomiforena,andranomiforitra,andranomiforitsa,andranomifototra,andranomilevy,andranomilitsa,andranomilitsy,andranomilolo,andranomiolaka,andranomioloka,andranomiova,andranomirietsy,andranomiseho,andranomiteka,andranomitikitsy,andranomitrohy,andranomivalika,andranomjavdo,andranomody,andranomololo,andranomondevy,andranomonintsy,andranomorary,andranomorery,andranomovo,andranompe,andranompototra,andranona,andranonabidy,andranonafindra,andranonahantona,andranonahidy,andranonahoatra,andranonahotra,andranonakanga,andranonakoay,andranonakoho,andranonalika,andranonamalona,andranonampanga,andranonampela,andranonankoay,andranonaomby,andranonaondry,andranonapaha,andranondambo,andranondramanitra,andranongoaika,andranonianahary,andranoniazavavy,andranonjanahary,andranonjirioky,andranonkazo,andranonomby,andranooapango,andranopasy,andranopatsa,andranopisadoha,andranoposa,andranopositra,andranopoza,andranoraikitra,andranorano,andranorantsa,andranorapaha,andranorefina,andranoroa,andranosahalampona,andranosamonta,andranosikaly,andranosisamalona,andranosoa,andranosondry,andranotakatra,andranotakatra ambany,andranotakatra ambony,andranotakatsy,andranotantely,andranotapaka,andranotava,andranotehy,andranotelo,andranotenina,andranoteraka,andranoteraka atsimo,andranoteraka avaratra,andranoteraky,andranotoboka,andranotogy,andranotohoka,andranotojy,andranotoka,andranotolihiny,andranotolina,andranotomandry,andranotomendry,andranotsara,andranotsikio,andranotsimaty,andranotsimonina,andranotsindrano,andranotsinitra,andranotsiriry atsimo,andranotsiriry avaratra,andranotsiritra,andranotsisiamalona,andranovaha,andranovaho,andranovaka,andranovakoana,andranovaky,andranovalo,andranovao,andranovato,andranovelo,andranovelona,andranovelona nord,andranovendrana,andranovilana,andranovka,andranovo,andranovohitsy,andranovola,andranovolo,andranovoly,andranovondroana,andranovondrona,andranovondronina,andranovoribe,andranovorikolo,andranovorikolona,andranovorilava,andranovorimahebo,andranovorimamty,andranovorimena,andranovorimirafy,andranovorimpanjava,andranovorinampela,andranovorindratahotsy,andranovorisosotra,andranovoritakabaka,andranovoritay,andranovoritelo,andranovorivaky,andranovorivato,andranovory,andranovovo,andranoyorikolo,andranozaha,andrantabe,andrantamarina,andranto,andranwala,andrany,andrapa,andrapengy,andrapidrapika,andraraly,andrarambiny,andraraompy,andrarary,andraratranina,andraratrina,andraraty,andrareza,andrarezo,andrarum,andras,andrasaha,andrasahabe,andrasakomby,andrasarasara,andraseky,andrasemba,andrasesti,andrasevac,andrasevec,andrasfa,andrasfalva,andrasfoldje,andrashaza,andrashida,andrasi,andrasirisa,andrasitelek,andrasova,andraspuszta,andrasszallas,andrastanya,andrasy,andratafika,andratamare,andratamarina,andratamba,andratambazaha,andratambe,andratanomby,andratany,andrate,andratenina,andratinskiy,andratomana,andratomanitra,andratombe,andratomena,andratomity,andratranina,andrauli,andravainafo,andravakely,andravakoky,andravany,andravaonony,andravaravina,andraveloma,andravia,andravida,andravidha,andravikazo,andravinakoho,andravinambo,andravindahy,andravindralandana,andravinkazo,andravitokana,andravoahangy,andravola,andravola avaratra,andravolasoa,andravololona,andravontahy,andravony,andravoravo,andravorianevo,andravory,andraz,andraz nad polzelo,andrazaha,andrazanaka,andrazato,andrazza,andre,andre cassua,andre da rocha,andre delpit,andre fernandes,andre libre,andre machado,andre marti,andre quia,andre resende,andre rodrigues,andre samo,andre siqueira,andre soboua,andre sotchibale,andre tapo,andrea,andrea heights,andrea mwanje,andreaba,andreade,andreamalama,andreamalama atsimo,andreamalama avaratra,andreamitsioka,andreanovka,andreanovo,andreapol,andreas,andreasberg,andreashutte,andreashutte oberschlesien,andreastal,andreatomily,andreba,andrebakely,andrebolo,andreburg,andrec,andree,andreeni,andreesti,andreesti-zamfiresti,andreeva,andreevca,andreevka,andreevo,andreevski,andrefaha,andrefamanga,andrefambato,andrefanalatsinainy,andrefananjoma,andrefanjaka,andrefantsena,andreforefo,andregeat,andreha,andrei,andrei saguna,andreiasu de jos,andreiasu de sus,andreica,andreikha,andrein,andreis,andreita,andrej nad zmincem,andrejci,andrejevac,andrejevac aradac,andrejevo,andrejevskoje,andrejevtsina,andrejinci,andrejoulet,andrejoulets,andrejova,andrejovka,andrejsala,andrekaba,andrekakert,andrekareka,andrekarekabo,andreketa,andrekhnovo,andrekhovshchina,andrekivske,andrekosy,andrekovskoye,andrelandia,andrema,andremane,andremanito,andremba,andrembesoa,andrembona,andremibory,andremiory,andremise,andrenci,andreneasa,andreneasza,andrengy,andrenialafotsy,andrenialamahitsy,andrenialamena,andrenialasingotry,andreniatamahitsy,andreniatamena,andrenjalamahitsy,andrenovskoy,andrentsevo,andreoli,andrepna,andrepno,andrepolye,andrequice,andrera,andrere,andrers,andres,andres aquirye,andres boniface,andres bonifacio,andres bueno,andres de morga,andres delgado,andres mixtla,andres nino,andres q. rojo,andres quintana roo,andres quintana roo segunda seccion,andres vaillant,andres-ivanovskoye,andresa,andresabiky,andresare,andreshkino,andreshyunay,andresia,andresillo,andresito,andresiunai,andrespol,andressaare,andressaare asundus,andrest,andresy,andretanya,andretishkyay,andretiskiai,andretiskiu,andretta,andreus,andreute,andrevevskiy bayrak,andrevo,andrevokely,andrevorevo,andrevorevolava,andrew,andrew lewis,andrew sinkala,andrewbli,andrews,andrews and hurds subdivision,andrews chapel,andrews crossroads,andrews lake estates,andrews manor,andrews place,andrews settlement,andrews subdivision,andrews town,andrewsville,andrey kurgan,andrey-bazarovo,andrey-klyuch,andrey-kyuyel,andreya,andreyanikha,andreyankovo,andreyanovka,andreyanovo,andreyanovskiy,andreyantsevo,andreyashevka,andreyaul,andreychishchevo,andreye-dmitriyevskiy,andreye-ivanovskoye,andreyedmitriyevskiy,andreyen,andreyenik,andreyenki,andreyenkov,andreyero,andreyeskoye,andreyev,andreyev klyuch,andreyev mys rybolovnyy poselok,andreyev pochinok,andreyev prud,andreyev yar,andreyeva,andreyeva gora,andreyeva gorka,andreyevichi,andreyevka,andreyevka pervaya,andreyevka vteraya,andreyevka vtoraya,andreyevka-mozharovka,andreyevka-pechevaya,andreyevo,andreyevo-bazar,andreyevo-bazary,andreyevo-desnitskaya,andreyevo-melentyevo,andreyevo-melentyevskiy,andreyevo-paliki,andreyevoivanovka,andreyevozorino,andreyevshchina,andreyevshehina,andreyevsk,andreyevskaya,andreyevskaya gora,andreyevskiy,andreyevskiy khutor,andreyevskiy pochinok,andreyevskiybayrak,andreyevskiye,andreyevskiye vyselki,andreyevskiye yurty,andreyevskoye,andreyevskoye-na-lige,andreyevskoye-tashakovo,andreyevtsy,andreyki,andreykobo,andreykova,andreykovichi,andreykovo,andreykovskaya,andreykovtsy,andreyshur,andreytseva,andreytsevka,andreytsevo,andreyvka,andreyvkaerdelva,andreza,andreze,andrezel,andrezes,andrezieux,andri,andria,andriabe,andriabe est,andriabe ouest,andriabekely,andriabolo,andriadriatra,andriakely,andriamandevy,andriamanero,andriamangrona,andriamarolamba,andriamasay,andriamasiniandro,andriamasy,andriambakotra,andriambango,andriambato,andriambavotsy,andriambazaha,andriambazana,andriambe,andriambekely,andriambilany,andriamborompotsy,andriamena,andriamena-morarano,andriamenakely,andriamero,andriamitsokina,andriamora,andriamoro,andriampamaky,andriampandra,andriampanora,andriampasika,andriampisaka,andriampotaka,andriana,andriana-maynironja,andrianabe,andrianambo,andriananaly,andrianavo,andriandampy,andriandapy,andriandava,andrianday,andriandraivozaka,andriandrapona,andrianifotsy,andrianikha,andrianitanala,andrianjato,andriankely,andriankovo,andrianopol,andrianopolskiy,andrianov,andrianova sloboda,andrianovichi,andrianovka,andrianovo,andrianovskaya,andrianovskiy,andrianovskoye,andrianoy,andriantany,andriantsevo,andriantsibabo,andriantsilahy,andrianuv,andriany,andriapamena,andrias,andriaschevce,andriasevca noua,andriasevca veche,andriassewcze,andriatany,andriatsminda,andriavitsy,andriba,andribavato,andribavontsona,andrica,andriceni,andriching,andrichinskaya,andrichsfurt,andrichsfurth,andrici,andricici,andrico,andricourt,andrid,andrieche,andrieseni,andriesesti,andrieskraal,andriesu,andriesu-de-jos,andriesu-de-sus,andriesul de sus,andriesul-de-jos,andrietiskiai,andrifosse,andriha,andrihibe,andriivka,andrijasevci,andrijevci,andrijevica,andrijevici,andrijevo,andrijici,andrikarika,andrikely,andrikova,andrikovichi,andrikovo,andrilovec,andrima,andrimbavontsona,andrimont,andrin,andrinabe,andringahely,andrino,andrinos,andriny,andriolata,andrionishkis,andrioniskio,andrioniskis,andripamena,andripatrambo,andriralalana,andristara,andritsa,andritsaena,andritsaina,andriunai,andriusiunai,andriuskoniai,andrivaux,andrivista,andrivitsa,andrivka,andrix,andriyakhi,andriyanka,andriyankovo,andriyanov,andriyanova,andriyanovka,andriyanovo,andriyanovskaya,andriyanovski,andriyanovskiy,andriyanovskoye,andriyashevka,andriyashivka,andriyivka,andriykivtsi,andriyuki,andrnanovskaya,andro,andro-kholmy,androa,androadroatra,androamitsaka,androangabe,androangady,androatsabo,androb,androbaroba,androbay,androbe,androboka,androboky,androbotsiabo,androbotsy,androchel,androdava,androdomaina,androeni,androfary,androfia,androfiabe,androfiakely,androfialava,androfiamadinika,androfiamena,androfianitsibiby,androfiatsara,androharano,androhiavy,androhibe,androhibobe,androhibotsy,androhipano,androhitsy,androhkou,androhofontsy,androhondroho,androhotoka,androhotra,androhy,androiavitra,androiavy,androibe,androidava,androidroy,androifantaka,androimainty,androimanga,androimenabe,androimitsaka,androizaha,androka,androkhi,androkhnovo,androkovo,androkovskiy,androkovskoye,androlavy,androlikon,androlikou,androlykou,andromachi,andromamy,andromasy,andromasy ii,andromay,andromba,andrombato,andrombatokely,andrombazo,andromiloi,andromovskaya,andron,androna,andronache,androndrabe,androndrakely,androndramanitra,androndramena,androndrona,androndrona anava,androndry,andronesti,andronestilor,andronezh,androngava,androngony,andronialady,andronianoi,andronikha,androniki,andronikovo,andronino,andronion,andronja,andronjana,andronkino,andronkovo,andronnikovka,andronnikovo,andronnikovskiy,andronnikovskoye,andronomena,andrononahoatra,andronono,andronov,andronova,andronova gora,andronovichi,andronovka,andronovo,andronovskaya,andronovskiy,andronovskoye,androns,androntsevo,androny,andronyky,androp,andropolo,andropolskiy,andropov,andropovskiy,androranga,androrangabe,androrangabe ii,androrangambo,androrangatsara,androrangavola,androrangavola atsimo,androrangavola avaratra,androrona,andros,andros town,androsabo,androsalabo,androshintsy,androshuny,androsova,androsovka,androsovo,androsovshchina,androssan,androsy,androt,androt gali,androta,androtra,androtrabe,androtrabo,androtrakely,androtravo,androtrazo,androtsa,androtsabo,androtse,androtsiabo,androtsibe,androtsikely,androtso,androtsy,androtsy ambany,androtsy atsinanana,androtsy est,androtsy ouest,androu,androusa,androusovo,androva,androva atsimo,androvabe,androvac,androvahonko,androvakely,androvakety,androvamary,androvanakono,androvanaomby,androvasoa,androvavaratra,androvce,androvia,androvici,androviloma,androvka,androvontsy,androvoribe,androvorony,androy,andrud-e `olya,andrud-e bala,andrud-e sofla,andrukha,andrukovo,andruni,andruny,andrup,andrupene,andrusa,andrusaiciai,andruschowka,andrusha verkhnyaya,andrusha-de-zhos,andrushaychyay,andrushaytse,andrushe,andrushevka,andrushenkova,andrushevka,andrushevskaya,andrushi,andrushivka,andrushkaychey,andrushki,andrushkino,andrushovka,andrushu,andrushuvka,andrusia,andrusiyuv,andrusova,andrusove,andrusovka,andrusovo,andrusu de jos,andrusu de sus,andrusul de jos,andrusul de sus,andruszkowice,andruszowka,andrut,andruth,andruvu,andrvida,andry,andrychow,andrychy,andryes,andryjanki,andrynki,andrynta,andryukhinovo,andryuki,andryukova,andryukovo,andryukovskaya,andryunay,andryush,andryushchenko,andryushevka,andryushevo,andryushevskaya,andryushevtsy,andryushi,andryushin,andryushina,andryushino,andryushinskaya,andryushintsy,andryushka,andryushki,andryushkina rechka,andryushkino,andryushkiny,andryushkonyay,andryushkovo,andryushnikovo,andryusi,andryvskiy,andrzejewka,andrzejewo,andrzejki,andrzejow,andrzejowce,andrzejowka,andrzejowo,andrzjowka,andsager,andschu,andselv,andselva,andsema,andserfa,andset,andsnaes,andsnes,andstrabyay,andu,andu kuh,andu-bombo,andua,anduaketiyawa,anduarsepan,andubi,andubu,andudu,anduecera,anduerga,anduezero,andugoda,andugu,anduhau,anduhjerd,anduins,anduja,andujar,andujo,anduklu,anduksugol,anduln,andulan,andulang,andulawan,andulbaria,andulia,anduliai,andulo,andulo de quilengo,andulo-ambile,anduluce,andum,anduma,anduman,andumayo,andumosom,andumout,andun,anduna,andunda,andung,andung timur,andungama palkumbura,andungbiru,andungsari,andunguluh,anduni,anduny,anduo,anduo xian,anduod uascia,anduoy,andupe,andupelena,andur,andura,anduramba,andurangoda,andurasta,andure,andureongi,anduri,anduring,andurkonam,andurlas,andurma,andurongi,andurubebila,andus,andusil,andusu,anduu,anduuma,anduwaketiyawa,anduwawala,anduwiliki,anduyan,anduyanan,anduyu,anduz,anduze,andvam,andvar,andvik,andviken,andvikgrend,andvord,andwam,andwe,andwhel,andwigol,andwikkol,andwil,andy,andy cove,andygon,andykh,andylaakh,andylakh,andylyr,andyngda,andypakh,andyrdzhan,andys acres,andys place,andytown,andyukhovo,andyville,andzad,andzani,andzelate,andzelati,andzeli,andzeni,andzh,andzha,andzha-kishlak,andzharak,andzharan,andzharly,andzheyevtse,andzheyki,andzheyuvka,andzhir,andzhirak,andzhiran,andzhiri,andzhirkon,andzhirobi-bolo,andzhiyevskiy,andzhorak,andzhuman,andzielowka,andzieme,andzik,andzin,andzion,andzletowka,andzogo,andzoko,andzounou,ane,ane ka wara,aneefafako,anea,anebdour,anebed,aneber,anebetsu,anebo,aneboda,aneby,anechaukur,anechkan,anecho,aneco,anecone,anecuene,anedad,anedin,anediou massi,aneegeutah,aneewakampoe,aneewakondre,anefergane,anefgou,anefi,anefid,anefis,anefis i-n-darane,anefis in daran,anefitrabe,anefrioune,aneg,anegadizo,anegadizos,anegam,anegane,anegasaki,anegato,anegen,aneh,aneh sar,aneh sara,anehanh,anehlisar,aneho,anehosur,anei,aneiga,aneiga 2,aneiga number 2,anein,aneintaunggyun,aneiza,aneja,anejet,anejigal,anejo,anek,anekabakti,anekaelok,anekal,anekamarga,anekasari,anekatti,anekattiya,aneke,aneker,anekh,anekho,anekirea,aneko,anekova,anekovo,anekri,anekui,anel,anela,anelakici,aneland,anelbounbeke,anelema,anelevka,anelgaohat,anelgauhal,anelgauhat,anelghowhat,anelhe,aneli,anelih,anelin,anelina,anelino,aneliny,anelio,aneliske,anelkal,anelo,aneloklab,anelovka,anelsered,anem,anema,anemala,anemas,anemdanae,anemdel,anemek,anemelaua,anemene,anemid,anemnyasevo,anemnyasovo,anemodhouri,anemodhourion,anemogiannatika,anemokhoraki,anemokhorakion,anemokhori,anemokhorion,anemolter,anemomilos,anemomylos,anemorrachi,anemorrakhi,anemotia,anemoyiannatika,anemoyianniatika,anemurium,anemutu,anemzi,anena,anenae,anendaka,anendiue,anendy,anene,anenecuilco,aneng,anenge,anengitra,anengjoyo,aneni nouy,anenii noi,anenii noui,anenjandava,anenjendava,anenka,anenkovo,anenska hut,anenska studanka,anenska ves,anenskoye,anenso,anensogya,anensuja,anento,aneny,aneo,aneong slap,anepahan,anepe,anepesa,anepmete,anepur,aneq,aner bela,aneran,aneran-camors,anerantza,anerantzia,aneratza,aneratzia,aneres,anergane,anergi,anergou,anergui,anerguo,anerhchai,anerho,aneri makan,anerif,anerinerina,anerley,anerli,anerni,anero,anerrutamahana,anertitzas,anerveen,anes,anesaki,anesaki-machi,anesakikaigan,anese,aneset,anesful,anesguelt,anesh,anesht,aneshti,anesibato,anesika,aneskar el fougani,aneskar el fourqani,anesour,anessa,anessis,anesst,anestad,anestaion,anestavan,anesti,anestias,anet,aneta,anetai,aneth,aneto,anetovka,anetovo,anetsham,anetsreut,anettu,anetz,aneue,aneueng,aneuk laot,aneukbatee,aneukgolong dua,aneukgolong satu,aneulaot,aneuradhapura,anevato,anevelde,anevjosa,anevjose,anevka,anevo,anevoka,anew,anewe,anewo,anexa,anexo,anexo de la hacienda ongoro,aney,aneyeva,aneyevka,aneyevo,aneyevskiye,aneyevy,aneyoshi,anez,aneza,anezal,anezandava,anezandova,anezcar,anezi,anezig,anezirh,anezitch,anezour,anezzou,anfa,anfa-superieur,anfah,anfaja,anfalich,anfalikha,anfalis,anfalovo,anfama,anfame,anfani,anfanifotsy,anfar,anfeg,anfei,anfeitale,anfelden,anfeltsevo,anfen,anfeng,anfeng linchang,anfeng nongchang,anfeng xiang,anfeng zhen,anfengchen,anfengchiao,anfengcun,anfengdui,anfengling,anfengqiao,anfengtang,anfengying,anfentsun,anfeo,anfeou,anfeoz,anfergal,anfergane,anferitsevo,anferno,anfernou,anferovo,anfetu,anfeug,anfezza,anficerova,anfilovo,anfimikha,anfimovka,anfimovo,anfimovtsy,anfin,anfinogenovka,anfisovka,anflous,anfo,anfoe,anfoega akukome,anfoega akukorme,anfoega dzana,anfoeta chebi,anfoeta tsebi,anfoi,anfoin,anfosi,anfoud,anfouin,anfoulgoulaye,anfour,anfu,anfu xian,anfu xiang,anfu zhen,anfuchang,anfuchen,anfucun,anfuhsien,anfuhsu,anfushih,anfusi,anfusi zhen,anfuso,anfussu,anfusu,anfutun,anfuzhen,anfwakui,anfwen,ang,ang dhong,ang duong,ang giang,ang kham,ang khan,ang konh,ang kouan,ang krong,ang kunh,ang let,ang mo kio,ang mo kio new town,ang mo kio village,ang mok,ang ngoai,ang phao,ang sanduoch,ang sang,ang sieng,ang sila,ang snuol,ang ta,ang tang,ang tasaom,ang tasset,ang thaung,ang thong,ang thuang,ang tit ui,ang ton kuon,ang wian,ang-gapang,ang-nu,anga,anga bandawaki,anga brok,anga mazouga,anga-hui,angaba,angaback,angabena,angabere,angabo,angaboda,angabong,angabu,angac,angaccia,angacha,angacha,angachi,angachikan,angachilla,angachilla nueva,angad,angadanan,angadanan viejo,angadangan,angadauan,angadeghagbene,angadippuram,angadipuram,angado,angadpur,angadpura,angadu,angaegol,angaekol,angaemogi,angafallet,angagan,angaghakiri,angagoda,angagorabe,angahran,angahuan,angai,angaida,angaifu,angaika,angaikor,angairia,angais,angaji,angajiul,angajur,angakan,angakban,angaki,angakkadawala,angako,angakro,angaku,angal,angalkhune,angala,angala-ngala,angalabenni,angalabia,angalabio,angalabri,angalacane,angalag,angalak,angalakubosei,angalan,angalatan,angalawegbene,angalaya,angalazu,angalbe,angale,angaleyguey,angalgatou goz,angali,angalkaban,angalli,angallilec,angallo,angalmi,angaloy,angalyk,angama,angama kouassikro,angamacutiro,angamacutiro de la union,angamagol,angamaki,angamali,angamally,angaman,angamanda,angamandia,angamankro,angamapasa,angamarca,angamarcalle,angamarut,angamasil,angambongo,angamedilla,angamedille,angamgol,angami,angamio,angammana,angamoa,angamole,angamos,angampitiya,angamun,angamuwa,angan,angan khan baluch jafri,angan koffikro,angana,anganaketiya,anganamei,anganas,anganba,angancasilian,anganchi,angando,angandok,angane,anganeh,anganehsi,anganera,angang,angang gongshe,angang qu,anganga,angangdong,anganggol,anganghsi,angangjong,angangni,angangoda,angangueo,angangxi,angankro,anganni,anganoy,anganpathri,anganpitiya,anganskiy,anganui,anganwala,angao,angao zhen,angaossoukro,angapamba,angapang norte,angapang sur,angapat,angar,angar mahalleh,angar olye,angar-baro,angara,angara deburu,angara naado,angara-debou,angara-debu,angara-i-kalan,angaradebou,angaradeourou,angarah-e kalan,angarah-ye kalan,angarak,angarakan,angarakaraka,angarake,angaraveca,angaraya,angarayo,angarbain,angarbaka,angarchit,angarcit,angarden,angare,angareb,angaref,angareh,angarf,angargara,angari,angaria,angarian,angaric,angarik,angarilla,angarillas,angario,angariones,angariwala,angarjora,angarjur,angarka,angarle,angarn,angarna,angarne,angaro,angarou,angarpara,angarsk,angarskiy,angarskiy khutora,angarskiy poselok,angarskiye khutora,angaru,angarua,angaruyle,angaryan,angas,angas plains,angas valley,angasak,angasal,angasamok,angasan,angash,angasha,angashco,angashi,angasjo,angasjucha,angasjuclia,angaskog,angasllanche,angasmarca,angasmarca hacienda,angasmarquilla,angasmayo,angasoi,angasolka,angasor,angastaco,angastina,angaston,angasy,angasyak-russkiy,angat,angata,angata,angata naado,angatan,angatel,angath,angathi,angathia,angathias,angatia,angatorp,angatsi,angatuba,angaturama,angau,angaul,angaura,angavo,angavokely,angawan kutari,angaxaran,angay,angayaco,angayan,angaye,angaypahua,angaz,angazban,angba,angbagbene,angbagh,angbaka,angbakore,angban broukro,angbande,angbanghe,angbangniu,angbannikro,angbara,angbariama,angbasa,angbavia,angboko,angbongue,angboudjou,angbung,angbunka,angca,angcai,angcaoay,angcheh,angdalsro,angdela ki marhaiyan,angdeokri,angdhar,angdhohng,angdong,angdongni,angdung,ange,angeac,angeac-champagne,angeac-charente,angeb,angeba,angeback,angebacken,angebackstorp,angebe,angebet,angebo,angebodane,angeby,angecha,angecha,angecourt,angedair,angedi,angeduc,angee,angeer,angefam,angegarde,angeghakot,angeh,angeh rud,angeich,angeim,angeiras,angeiwanga,angeiwunga,angeja,angekhakot,angekharan,angel,angel a. corzo,angel albino,angel albino corzo,angel alvino corzo,angel carbajal,angel city,angel costodio,angel custodio,angel diaz,angel etcheverry,angel felix,angel feliz,angel fire,angel posada,angel r. cabada,angel r. cavada,angel street,angel voyvoda,angel-wojwoda,angela,angela arellano,angela maria,angela maria numero dos,angelamajor,angelan,angelane,angelariy,angelarji,angelas,angelaut,angelbach,angelbeck,angelbek,angelbrechting,angelcasa,angelci,angelek,angelengwe,angeles,angeles city,angeles de los infiernos,angeles de monte romo,angeles norte,angeles primero,angeles sur,angelesti,angelhausen,angelholm,angelholms havsbad,angeli,angeli custodi,angelia,angeliana,angelica,angelico,angelida,angelim,angelina,angelince,angelino heights,angelinovka,angelinskiy,angelinskiye,angelique estates,angelita,angelito,angelitos,angelknovka,angell,angellala,angellara,angells corner,angelmodde,angelniemi,angelo,angelo paulino,angelo rios,angelo saocal,angelo tiana,angelochori,angelochorion,angelokastro,angelokastron,angelokhori,angelokhorion,angelona,angelopolis,angelopoulou,angelos poulos,angelov,angelov dol,angelovo,angelpata,angelroda,angels,angels acres,angels camp,angels grove,angelsback,angelsbacksstrand,angelsberg,angelsbruck,angelsburg,angelsdorf,angelse,angelsfors,angelshaug,angelslo,angelsloo,angelstad,angeltofta,angeltown,angelturn,angelus,angelus oaks,angeluvka,angelville,angely,angemapasa,angen,angen-lakhta,angena,angenas,angendo,angeneh,angenga,angeniki,angennes,angenofen,angenrod,angenthor,angentina,angeolmi,angeot,angeqam,anger,anger kala,angerya-yuleyye,angera,angerapp,angerbach,angerberg,angerburg,angere,angereb,angered,angeren,angergira,angerhausen,angerhof,angerhofe,angeri,angeriko,angerikos,angering,angeriz,angerja,angerja asundus,angerja-ulejoe,angerlo,angermansbo,angermoen,angermuhle,angermund,angermunde,angern,angern an der march,angerode,angerona,angerri,angers,angersbach,angersberg,angersby,angersdorf,angersjo,angerskirchen,angersleigh,angersnes,angersneset,angerstein,angertal,angerthal,angerud,angervikko,angerville,angerville-bailleul,angerville-lorcher,angerville-la-campagne,angerville-la-martel,angervilliers,angerwies,angesa,angesberg,angesbyn,angesdal,angesgardarna,angeshtavan,angesia,angesleva,angesnas,angestevan,angestrask,anget,angeta,angeta rud,angetam,angetji,angetou,angetu,angeville,angevillers,angevul,angey,angeyar,angezey,angezhuang,angfa,angfand,angfeld,angfeu,angfurten,angga,anggaberi,anggacanang,anggacarang,anggadea,anggadoeber,anggadola,anggaduber,anggageta,anggai,anggaie,anggakan,anggalasanpasir,anggaleha,anggaliala,anggaliwa,anggalo,anggalomebo,anggam,anggamaya,anggana,anggana-dalam,angganan,angganayan,anggang,anggapang sur,anggapoa,anggaradja,anggaraja,anggaraksan,anggaran,anggaranu,anggarasa,anggarden,anggarung,anggasang,anggasari,anggasari kaja,anggasari kelod,anggasupute,anggaswangi,anggatjanang,anggatu,anggayuda,anggemudik,angger,anggersek,angges,anggesio,anggeukleung,anggia,anggikima,anggilig,anggilig xiang,anggis,angglasan,angglasari,angglasariu,anggoami,anggohu,anggokoti,anggol,anggola,anggoli,anggolomebo,anggolomewao,anggolomopuro,anggomopura,anggong,anggoro,anggoroboti,anggotoa,anggotuho,anggowala,anggrawati,anggrek,anggrek 1,anggrek 2,anggrek dua,anggrek satu,anggrik,anggris,anggrufvan,anggrung,anggrungan,anggrungan kidul,anggrunggondo,anggruvan,anggruvorna,anggsokah,anggu,anggumbat,anggungan,anggungan dauh,anggur,anggure,angh,anghad,anghagen,anghandiya,anghapur,anghar,anghara,angheb,anghec,anghela,anghelesti,anghelestii,anghelus,angheluta,anghely,anghetam,anghetam abo,anghezha,anghiari,anghila,anghin,anghinesti,anghit,anghiz,anghkak,anghnagappall bridge,anghor,anghor daban,anghushan,anghutang,anghvaneen,angi,angia,angiadhaki,angiadhakion,angiadi,angiai,angiama,angiama ii,angiamagbene,angiang,angiar,angiarak,angiay,angib,angibeau,angical,angical do piaui,angicalinho,angicao,angicheh,angico,angico branco,angico branco i,angico branco ii,angico da escora,angico das araras,angico de minas,angico grosso,angico torto,angico velho,angicos,angicourt,angida,angidhia,angidong,angidroga,angie,angie circle condominium,angieme,angiens,angier,angies,angig,angighe,angigo,angii,angija,angil,angil-angil,angila,angilan,angilbal,angilia,angilindrevoay,angilise,angilli,angilona,angilspiran,angilus,angim-loisah,angin,angin-angin,angin-nangin,anginan,anginangin,anginesu,anginha,angininkai,angininkay,anginkidul,anginskiy,anginyku,angio,angiola,angioro,angip,angir,angir kala,angira,angirazato,angirazato-ankafotia,angirey,angiri,angiriai,angiriu,angironihely,angirony,angiryay,angisa,angise-bamea,angisi,angiso,angista,angistavan,angistri,angistrion,angistron,angit,angitis,angitso,angivillers,angiz,angiz jan,angizai,angizay,angizi,angjen,angji,angk khleang,angk snuol,angk tasaom,angkaes,angkahgede,angkahmunduk,angkahpondok,angkahtegeh,angkaklau,angkalanpuge,angkaluau,angkara,angkarret,angkasa,angkat,angkatan,angkawit,angkayamat,angke,angkeb,angkeh,angkejaya,angket,angkeuw,angkieng,angkieng barat,angkiengbaroh,angkitkita,angkoep,angkofen,angkola,angkona,angkoop,angkor ban,angkor baurei,angkor borei,angkor sa,angkor village,angkre,angkrek,angkrong,angkruk,angkue,angkup,angkupan,angl,angla,angla gongshe,anglade,angladegiai,anglag,anglais,angland,anglanou,anglard,anglards,anglards-de-saint-flour,anglards-de-salers,anglarp,anglars,anglars-juillac,anglars-nozac,anglars-saint-felix,anglas,anglash,anglaxiang,anglberg,angle,angle bas,angle city,angle haut,angle inlet,angle vale,angle view,angledale,angledool,anglefort,anglema,anglemont,angleniki,angler,anglers cove,anglers haven,anglers park,angles,angles-du-tarn,angles-sur-langlin,anglesborough,anglesea,anglesea river,anglesey,anglesey road,anglesola,anglesqueville,anglesqueville-l esneval,anglesqueville-la-bras-long,anglesqueville-sur-saane,anglesry bridge,anglet,angleton,angleur,anglevillas,anglewood,angleya,angliers,anglikan,anglim,anglin,angling,anglininkai,anglininkay,anglininku,anglisidhes,anglisiya,anglity,angliysko selo,anglo,anglo alpha,anglo egyptian `ezba,anglo egyptian land company `ezba,anglo jos,anglodegay,anglogitat,angloh,anglure,anglure-sous-dun,anglus,angluzelles,angluzelles-et-courcelles,angly,angmagssalik,angmagssivik,angmagsslik,angmassalik,angmekka,angmering,angmout,angna,angnao,angnas,angnor,ango,ango ango,ango gali,ango kouamekro,ango mahala,ango namaiwa,angoa angao,angoa ango,angoa ansandi,angoa aouta,angoa bossari,angoa bourama,angoa dagne,angoa damagani,angoa dambane,angoa dangaladima,angoa danhaini,angoa daoura,angoa dembo,angoa dobi,angoa doka baba,angoa doka karama,angoa doua,angoa doubou,angoa gao,angoa gorka,angoa gouaka,angoa gouaya,angoa guessa,angoa kade,angoa kadey,angoa kouatche,angoa koundou,angoa laboa,angoa lelaba,angoa limane,angoa maba,angoa madjibre,angoa magagui,angoa mali,angoa marafa,angoa marata,angoa mayo,angoa modakini,angoa nama,angoa nassara,angoa noberni,angoa nomagandale,angoa nomaiwa,angoa rerey,angoa sama,angoa saoulo,angoa serki toudou,angoa soala,angoa tamou,angoa tchadi,angoa yaro,angoa yaya,angoa zanoa,angoa-bandawaki,angoa-bawake,angoa-kadeye,angoa-rereye,angoaka,angoaka atsimo,angoaka avaratra,angoaka north,angoaka south,angoaka suo,angoakro,angoal aiki,angoal bayi,angoal dachi,angoal dan gourgou,angoal douna,angoal doutchi,angoal gamdji,angoal ibrahim,angoal kalgo,angoal kara,angoal koura,angoal kourna,angoal magazi,angoal makyera,angoal marafa,angoal mata,angoal may dogon,angoal mayaki,angoal nana,angoal roumji,angoal saoulo,angoal serki makyera daoutche,angoal zanoa,angoango,angoares,angobang,angobigoby,angobila,angobio,angoc,angochagua,angochahua,angoche,angochi,angoda,angodakanda,angodan,angodepala,angodia,angodigo,angodingodi,angodona,angodongodo,angodongodona,angodonina,angodoua,angodra,angoe,angofa,angogae,angohran,angoil,angoini,angoisse,angoja,angojan,angojiga,angok,angokri,angoktong,angol,angola,angola acres ii mobile home park,angola beach mobile home park,angola by the bay,angola crest ii mobile home park,angola crest mobile home park,angola em luta,angola lake shore addition,angola landing,angola neck park mobile home park,angola nova,angola nova segundo,angola on the lake,angola town,angola-on-the-lake,angolaba,angolaemi,angolala,angola\302\223mi,angolbar,angole,angole uen,angolela,angolem,angolemi,angoleta,angoli,angoli ngbesso,angolivskiye,angolla,angollami,angolle uein,angolle uen,angolli,angolmal,angolmi,angolo,angolo terme,angolokaha,angolon,angoloua,angom,angomachay,angomayop,angomo,angomodong,angomon,angomon ii,angomont,angomsom,angomugama,angomuwa,angomyong,angon,angon mibana,angonabasulu,angonanssou,angonas,angonbibana,angonde,angonde-abema,angondo,angone,angone efira,angone i,angone ii,angone iii,angonenzork,angonfeme,angong,angonge,angonggi,angongni,angongo,angoni,angonjae,angono,angonogo,angonoveng,angonowo,angonoy,angontsy,angonuain,angoon,angoor adda,angooran,angoorpol,angopi,angor,angor adda,angora,angorada,angoradah,angoraeji,angorah,angoram,angoram village,angoran couao,angorangora,angorankoua,angorasse,angorcha,angore,angorele,angorez,angorguire,angorin,angoris,angoritaba,angoro,angoroda,angorogabe,angorogo,angorom,angorongodona,angoropia,angortak,angorti dalmadol,angorti dalmadot,angoruk,angorum,angoryong,angos,angosar,angoshteh,angosht")an,angoshtevan,angoshtjan,angosoi,angosorskiy,angoss,angossas,angosta,angostaco,angosteros,angostillo,angostina,angosto,angosto de arriba,angosto taperilla,angostura,angostura ciudad,angostura hacienda,angosturacasa,angosturas,angosturita,angosu,angot,angota,angotchang,angoteros,angotimoti,angoton,angotsy,angou,angou-kourawa,angoua,angoua gamji,angoua kouassi,angouakoukro,angouakro,angouakrou,angouala,angouane,angouanikro,angouassi,angoubekoua,angoucun,angoue,angouele,angoueli,angouere,angouhe,angouich,angouje,angoukrou,angoula,angouldenia,angouleme,angoulins,angoulins-sur-mer,angoulou,angoulou i,angoulou ii,angoum,angouma,angoume,angounakro,angoune,angounesiste,angour,angouria kalivia,angourie,angourt,angous,angousart,angouseliana,angoussaka,angoussart,angoustrine,angouzou,angovia,angoville,angoville-au-plain,angoville-en-saire,angoville-sur-ay,angow kheyl,angowice,angowz,angoya,angoye,angoz,angqaty,angra,angra do heroismo,angra dos reis,angra-juntas,angradebourou,angramanis,angras juntas,angraspur,angrat,angrawanwala,angrayan,angre,angreau,angreman,angren,angrenshakhtstroy,angres,angresse,angrewala,angri,angrie,angriffs-hafen,angrik,angrilla,angriman,angripet,angrongsari,angsa,angsai,angsam,angsana,angsana barat,angsana dua,angsana satu,angsana timur,angsanah,angsanakebonteh,angsanapojok,angsater,angsbo,angsboda,angschied,angseboda,angshi,angsholm,angshult,angshuping,angsidongni,angsiduo,angsikan,angsjomala,angskar,angsnas,angsoka,angsoka barat,angsoka timur a,angsoka timur b,angsokah,angsono dua,angsono satu,angsosund,angsri,angssuto,angsta,angstad,angstugan,angsudden,angsumiao,angsuo,angsuri,angsuss,angsvik,angtad,angtang,angtasom,angtassom,angteung,angtodong,angtong,angtori,angtung,angu,angu khel,angua,angua gamji,anguaira,angualasto,anguan,anguane,anguang,anguapan,anguara,anguasha,anguati,angub,anguba,anguciana,angudangeng,angudes,anguebacao,anguec,anguedi,angueira,angueiros,anguek,anguellou,anguemba,anguengue,angueniapissa,anguenu,anguera,anguereta,anguerf,anguerny,angues,anguesdam,anguganak,anguganak villages,angugut,anguia,anguiano,anguiatu,anguiatu abajo,anguiatu arriba,anguiatu zapote brujo,anguich,anguie,anguihoro,anguil,anguila,anguilan,anguilcourt,anguilcourt-le-sart,anguiling,anguilla,anguillara,anguillara sabazia,anguillara veneta,anguillero,anguim,anguime,anguinamoinha,anguinan,anguincila,anguiozar,anguira,anguita,anguix,anguji,anguk,angukhel,angukheyl,anguktong,angukulu,angul,angula,angula deh,angulama,angulana,angulas,angule,anguleh lavar,angulei,angulesh,angulgamuwa,anguli,angulis,angulis hacienda,angulito,angulle,angulmaduwa,angulo,angulos,angulugaha,angulugalla,anguluwa,angulvar,angum,anguma,angumba,angumbe,angume,angumpa,angumu,angumu etat,angumuldi,angunachchiya,angunai,angunakolapelessa,angunakolawewa,angunawala,angunawela,angunawila,angungescapolo,angunggol,angunna,angunuwila,anguo,anguoang,anguoc,anguocheng,anguosi,anguotun,anguppadul,angur,angur ada,angur adah,angur adda,angur azuj,angur chal,angur chaleh,angur darah,angur kala,angur kalay,angur kelay,angur kili,angur kola,angur tak,anguryan,angur-e `alam,angur-e pahel,angura,angura kota,angurada,anguradah,angurah,angurai,angurak,anguran,anguray,angurd,angurdara,angurdarreh,angurgo,anguri,anguri kalay,anguri kelay,angurian,anguriyah,anguriyan,angurkala,angurkli,angurkot,angurli,angurop,angurtak,angurtal,angurtlar-e `olya,angurtlar-e sofla,anguru,angurugu,anguruk,angurukandagara,angurukandagare,anguruwagala ihala,anguruwagala pahala,anguruwatota,anguruwawala,anguruwela,anguruwella,anguryan,angus,angus county,angus ranch,anguse,angusht,angushty,angusilla,angustia,angustias,angustina,angustown,angustura,angut,anguta,angutaz,angutia,angutikha,anguwan alhaji,anguwan ari,anguwan dabo,anguwan damada,anguwan dan yaya,anguwan daudu,anguwan digawa,anguwan dirti,anguwan dobo,anguwan galadima,anguwan gariji,anguwan gumeru,anguwan gwaldabu,anguwan kajinjiri,anguwan kutari,anguwan liman,anguwan madaki,anguwan maizamani,anguwan mallam alkali,anguwan mallamai,anguwan mayana,anguwan rufai,anguwan sakin pawa,anguwan sarkin baka,anguwan sarkin noma,anguwan sarkin pawa,anguwan sauro,anguwan wawa,anguwan yelwan daji,anguwar bawa,anguwar duna,anguwar jaba,anguwar jekada,anguwar kotci,anguwar sarkin yaki polchi,anguy,anguyenne,anguyo,anguyskiy,anguz,anguze,anguzek,anguzhan,angvik,angviller,angviller-les-bisping,angvreta,angwa alura,angwa bandawaki,angwa bawa,angwa bawake,angwa han-han,angwa hure,angwa kao,angwa kourawa,angwa koyo,angwa kwatche,angwa sarkin,angwa yisa,angwai,angwak,angwakro,angwan bakon bauga,angwan bawamakeri,angwan chitumu,angwan chori,angwan daji,angwan danjatau,angwan doro,angwan duku,angwan dutse tabana,angwan gagara,angwan galadima,angwan gedage,angwan giginya,angwan guduba,angwan hima,angwan ita,angwan jibo,angwan jikambara,angwan kasa,angwan kaura,angwan liman,angwan malam alkali,angwan mapera,angwan nababa,angwan sarkin koro,angwan shehu,angwan shiaka,angwan tanko,angwan tofa,angwan tsauni,angwan tsokwa,angwan tudu,angwan wusan,angwan yara mada,angwan zakara,angwan-audu,angwan-dabawa,angwan-dangado,angwan-dodo,angwan-lima,angwan-maiwasa,angwana,angwandawa,angwandi,angwang,angwangchon,angwanpa,angwe,angwea,angwel,angweng,angwentifi,angweta,angwi,angwieta,angwin,angwo,angwoltong,angwoma offinso,angwoma ofinso,angwu,angy,angyaldomb,angyalfold,angyalisziget,angyalkut,angyaloc,angyalos,angyalossytanya,angyalostanya,angyalosytanya,angyaltanya,angyan,angyana,angyapat,angyavul,angye,angyejang,angyejangto,angyeri,angyi,angying,angyo,angyodong,angyr,angyu,angyul,angzoung,anh do,anh son,anha,anha futa-fula,anha futa-fulag,anhabak,anhad,anhadong,anhaenggol,anhagan,anhai,anhaia,anhaktong,anhalt,anhalterkolonie,anhaltsberg,anham,anhamtae,anhanca,anhandigoda,anhandiya,anhandui,anhanduizinho,anhane,anhanea,anhanga,anhangol,anhanguara,anhanguera,anhanka,anhanzi,anhapa,anhar,anhar dari,anhar-e `olya,anhar-e sofla,anhara,anhari,anhauan,anhauon,anhaura,anhausen,anhauserhofe,anhaux,anhawan,anhawon,anhawongol,anhe,anhecun,anhee,anhee sur meuse,anheh,anhelinivka,anhelo,anhema,anhembi,anhemby,anhen,anhesi,anhettigama,anheungdong,anhexiang,anhialo,anhiers,anhilaq,anhilitelo,anhilizato,anhimango,anhiolo,anhiwala,anho,anho bom,anhoes,anhofen,anholt,anholt by,anhoma,anhomsil,anhong,anhongo,anhongsi,anhotsun,anhou,anhou nongchang,anhouidong,anhounzankro,anhoven,anhovo,anhsi,anhsia,anhsiang,anhsianghsien,anhsichen,anhsien,anhsihsien,anhsihsu,anhsili,anhsiliao,anhsin,anhsing,anhsinhsien,anhsintsun,anhsitso,anhsitsun,anhtwei,anhu,anhua,anhua yizuxiang,anhua zhen,anhuachen,anhuadaying,anhuahsien,anhuai,anhuaichi,anhuashih,anhuazhuang,anhugi,anhuisil,anhuksu,anhuli,anhulli,anhult,anhuma,anhumas,anhung,anhungdong,anhungmok,anhungni,anhwa,anhwadong,anhwaekkol,anhwahsien,anhwange,anhwanggol,anhwanghae,anhwari,anhwea,anhweahwea,anhweam,anhwiam,anhwiaso,anhyavekope,anhyolli,anhyon,anhyondong,anhyonni,anhyop,anhyoptong,ani,ani kandi,ani khan khel,ani old city,ani sofla,ani-e,ani-efume,ani-maeda,ani-muratbay,ani-nkwa-anam,ani-ugbo,ani-ujeh,ani-ye `olya,ani-ye bala,ani-ye pain,ani-ye sofla,ani-ye vasat,ani-ye vasati,ani-ye vosta,ani`am,ania,ania adou,ania-assikasso,aniadha,aniado,aniafoto,aniago,aniagua,aniaguita,aniaguita arriba,aniahgram,aniai,aniain,aniak,aniak grand,aniak petit,aniaka,aniakanane,anialbong,anialesso,aniali,aniam,aniam lrlabe,aniam touguel,aniama,aniaman,aniamango,aniamarina,aniamoa,aniamorti,anianba,aniandide,aniane,aniane i,aniane ii,aniang,aniangping,anianpelto,aniante,aniantintem,aniany,aniapam nanso,anias kunda,aniassue,aniasu,aniasue,aniata,aniatentem,aniatitem,anib,aniba,anibal,aniban,anibar,anibare,anibelekru,aniben,anibil,aniboasi,anibon,anibong,anibongan,anibongon,anibossue,aniboz,anibun,anibung,anibungan,aniburan,anic,anica glavica,anicabil,anices,anicetal,aniceto,aniceto martines,aniceto martinez,aniceto medrano,anichab,anichata,aniche,anichen,anicheng,aniches,anichevo,anichiucheng,anichiuchih,anichkino,anichkovo,anichkovskiye,anichskiy,anici,anicici,anicillo,anicovo,anicuns,anid-e bala,anides,anidhroi,anidhron,anidhros,anie,aniela,anielakou,anielew,anielewo,anielin,anielin kepa,anielin-kazimierzow,anielina,anielinek,anieliny,anieliske,anielow,anielpol,anier,anierana,aniere,anieregu,anieres,anieru,anies,aniesi,anietir,anieves,anieze,anieze camp,aniezo,aniezok,aniezork,anif,anifa,anifekajae,anifi,anifi kalay,anifi kelay,anifie,anifikalay,anifion,anifiyah,anifiye,anifora,aniforeika,anifuma,anigab,anigh,anighore camp,anigo,anigouamenio,anigua,aniguna,anih,anihani,anihsien,anii,anija,anijala,anik,anika,anikanovo,anikanovskiy,anikanovskiye khutora,anikanung,anikatsi,anike,anike-umumba ndiogu,aniker,anikeyer,anikeyevka,anikeyevo,anikeyevskiy,anikh,anikhankheyl,anikhankheyl,anikhonovo,anikhovka,anikhovskiy,anikichi,anikici,anikin,anikin pochinok,anikina,anikina griva,anikind,anikino,anikinskiy,anikinskiy pochinok,anikinsty,anikintsy,anikiny,anikiyev,anikiyevka,anikiyevskaya,anikkae,anikkaranchattram,anikoko,anikonovo,anikoorma,anikorma,anikou,anikova,anikovichi,anikoviskaya,anikovka,anikovo,anikovskaya,anikoyu,anikro,anikshchay,anikshchyay,anikshino,anikul,anikula,anikula kortsi,anikvere,anikyula,anil,anil y velasquitos,anil y velazquito,anila,anilabip,anilac,anilan,anilanan,anilao,anilauan,anilavinany,anilawan,anilaya,anilelerin,anilengbe,anilhac,anili,aniline village,anilio,anilion,anillaco,anillo,anillune,anillyang,anilmis,anilobe,aniloul,anilu,anim,anima,animabio,animahun,animakrom,animal flower cave,animales,animan,animana,animaoio,animas,animas abajo,animas arriba,animas de cerro verde,animas forks,animas lomas,animasahun,animascancha,animashawun,animbo,animborio,anime,anime alto,animene tie,animilla,animinik,animiter,animniachire,animosa,animoza,animshaun,animskog,animu,anin,anina,aninaka,aninchiyankulam,anindini,aninenya,anines,aninga,aningal,aningalan,aningas,aningatan,aninge,aningeje,aninger,aninghe,aningi,aningoay,aningol,aningor,aningotra,aninguay,aningue,aninha,anini,anini-y,aninig,aninim,aninina griva,aniniry,aninisiu-din-vale,aninisu,aninisu din deal,aninisu din vale,aninisul-din-vale,aniniy town,aninjilli,aninjinni,aninkroma,aninkwan,aninns,anino,anino bayka,anino-gusinovka,aninoasa,aninofi,aninon,aninosa,aninosani,aninou,aninpyong,anins,aninsk,aninskiy,aninsua,aninsul din deal,anio,anio lowo,aniofu,aniog,anioma,aniomodue,aniorpo,anioryitika,aniozo,anipa,anipadsch,anipaj,anipan,anipas,anipasu,anipein pah,anipein powe,anipemza,anipen,anipoas,anipoco,aniq,aniqoba,aniqqislaq,anir,anir joi,anira,aniribe,aniriky,anirinihamba,anirnar,anirong,anis,anis-ivan,anisa i,anisa ii,anisa iii,anisabad,anisacate,anisag,anisahabe,anisakan,anisakoamizara,anisar,anisarakion,anisca,anischau,anise,anisemtsevo,anisenki,anisenvaara,anisere,anisesieg,anish,anish gum,anish kryshki,anish-akhperdino,anish-bos,anish-kher,anishgram,anishi,anishino,anishino pervoye,anishino vtoroye,anishinskiy,anishkasy,anishkhiri,anishuacaro,anisia,anisil,anisillo,anisimikha,anisimkovo,anisimov yar,anisimova,anisimova polyana,anisimovich,anisimoviche,anisimovichi,anisimovka,anisimovo,anisimovo-polyana,anisimovshchina,anisimovskaya,anisimovskiy,anisimovskoye,anisimtsevo,anisio de morais,anisivolanana,aniskin,aniskino,aniskovo,anislag,anislagan,anislang,anislangan,aniso,anisoc,anisok,anisola,anison,anisov,anisovka,anisovo,anisovo-gorodishche,anisovskiy,anisovy,anisse,anisso,anissola,anissowka,anistag,anistavan,anisy,anita,anita garibaldi,anita hacienda,anitak al bulbul,anitan,anitap,anitapolis,anitchawo,anitcinar,anitemfi,anitimfi,anitit,anitkaya,anitkino,anitli,anito,anitrella,anitsaion,anittepe,anitua,anituwe,anitzberg,aniuaia,aniuke,aniuma,aniushizi,aniva,anivan,anivaskiy-ozeretskiy,anivoala,anivondrano,anivonisaina,anivorano,anivorano avaratra,anivorano nord,anivorano-du-nord,anivoranokely,anivotano,anivskiy-ozeretskiy,aniwa,aniwachor,aniwalo,aniwas,anixab,anixhe,aniya geneme,aniyala,aniz,aniza,anizai,anizacate,anizai,anizha,anizo kalay,anizo kelay,anizu kalay,anizukulay,anizy,anizy-le-chateau,anj,anja,anja avaratra,anja nord,anja sud,anja-ampandrabe,anja-belitsaka,anjab,anjaba,anjabe,anjabe ambany,anjabe ambony,anjabihy,anjabotretriky,anjad,anjadhat,anjae,anjaebisan,anjaedong,anjaegae,anjaembae,anjaenaemi,anjaeul,anjah,anjaha,anjahabe,anjahaboboka,anjahafaty,anjahaloaka,anjahamaina,anjahamana,anjahamanananaka,anjahamangotroka,anjahamanitra,anjahamara,anjahamarina,anjahamary,anjahamba,anjahambalo,anjahambe,anjahamboroka,anjahampito,anjahana,anjahandoaka,anjahandroa,anjahankely,anjahantelo,anjahasoa,anjai,anjainony,anjajavy,anjakamba,anjakamba ambany,anjakamba ambony,anjakambana,anjakely,anjakibobo,anjakpani,anjal beg,anjal beyg,anjala,anjalajala,anjalas,anjalavabe,anjalazala,anjalin,anjalin tarom sofla,anjalkor,anjaloaka,anjalvola,anjamahitsy,anjamala,anjamana,anjamanga,anjamangoha,anjamangotroka,anjamanintsy,anjamarina,anjamaro,anjamarotea,anjamarotia,anjamba,anjambaky,anjambalava,anjambalo,anjambo,anjamena,anjamiary,anjamikory,anjamitikitra,anjampaly,anjampotsy,anjampoty,anjan,anjana,anjanabo,anjanabora,anjanaborona,anjanagachhi,anjanagachi village,anjanahary,anjanai,anjanakanga,anjanamahazo,anjanamasina,anjanana,anjanapura,anjanasampana,anjanavorona,anjanazana,anjandari,anjandavo,anjando,anjandoaka,anjandoaky,anjangachi,anjangaon,anjangaon bari,anjangap,anjangdan,anjangdang,anjangdong,anjanggogae,anjanggol,anjanggori,anjango,anjangoveratra,anjangpung,anjangsana,anjangsobang,anjani,anjani barat,anjani selatan,anjani timur,anjania,anjanina,anjaninarivo,anjanojano,anjanozana,anjanozano,anjanvel,anjapohy,anjar,anjarak,anjaran,anjarazana,anjareh,anjarla,anjarle,anjaru,anjasari barat,anjasi,anjasivy,anjasmoro,anjasoatanimbary,anjatakary,anjatan,anjatanbuah,anjatanimena,anjatanmesjid,anjatantegal,anjatelo,anjatovo ambany,anjatovo ambony,anjatsampa,anjatsoa,anjatsy,anjattan,anjattul,anjauan,anjauru,anjavarud,anjavelo,anjaverud,anjavibe,anjavikely,anjavilava,anjaviliava,anjavimalay,anjavimihavana,anjavimilay,anjavimisakana,anjavimolay,anjavina,anjavindraombaka,anjavinihavana,anjawa,anjawan,anjean,anjeba,anjebatoby,anjedan,anjedava,anjeh,anjeh `ali ghori,anjeh pol,anjehem,anjehpol,anjeje,anjel del descanso,anjelibei,anjelin,anjelina,anjemabad,anjen,anjenchen,anjenchieh,anjengo,anjenhsi,anjenhsien,anjenje,anjenkou,anjenli,anjeongjamal,anjepy,anjer-kidoel,anjer-kidul,anjer-lor,anjerah,anjerak,anjeravand,anjerd,anjerk,anjesash,anjesesh,anjeshesh,anjetla,anjeungil,anjeux,anjeva,anjezika,anjghalai,anjhortu,anjhurtu,anji,anji liao,anjia,anjia puzi,anjia zhen,anjiaao,anjiaaozi,anjiaba,anjiabang,anjiabao,anjiabe,anjiabe ambony,anjiabitika,anjiabonoka,anjiaboroka,anjiabory,anjiabu,anjiacha,anjiachang,anjiachong,anjiadawan,anjiadu,anjiafotsy,anjiagongqiao,anjiagou,anjiahe,anjiahely,anjiaji,anjiajia,anjiajiabe,anjiakely,anjialava,anjialavabe,anjialavahely,anjialavo,anjiale,anjiamaeva,anjiamalandy,anjiamaleotsy,anjiamananazy,anjiamangemna,anjiamangerana,anjiamangerina,anjiamangirana,anjiamangirana i,anjiamangirana ii,anjiamangirina,anjiamangotraka,anjiamangotroka,anjiamangotroky,anjiamanoro,anjiamarako,anjiamarina,anjiamave,anjiamavo,anjiamen,anjiamena,anjiamengirana i,anjiamiao,anjian,anjiang,anjiangkeng,anjiangying,anjiao,anjiapan,anjiaping,anjiapo,anjiapping,anjiapu,anjiaqi,anjiashan,anjiashe,anjiashui,anjiasi,anjiata,anjiatun,anjiawan,anjiawuji,anjiayao,anjiayaoxian,anjiaying,anjiayu,anjiayuan,anjiazao,anjiazhang,anjiazhou,anjiazhuang,anjibal,anjico,anjico velho,anjicun,anjida,anjidan,anjidong,anjie,anjie xiang,anjiegh,anjiekeng,anjighe,anjih,anjihai,anjijabe,anjijaomby,anjikhan,anjikkol,anjikoti,anjil,anjil beneh,anjil nesa,anjil nesam,anjil-e boneh,anjila,anjilajila,anjilan,anjilaq,anjilavand,anjilavand khuneh,anjilavand kuhneh,anjilavand-e `olya,anjilavand-e bala,anjilavand-e kohneh,anjilavand-e pain,anjilavand-e sofla,anjilbon,anjileh,anjileyn,anjilgol,anjilin,anjilla,anjim,anjimahavita,anjimok,anjin,anjinbawi,anjindong,anjineh-ye `olya,anjineh-ye bala,anjineh-ye ebrahim,anjineh-ye reza,anjineh-ye sofla,anjing,anjingo,anjingol,anjingorabe,anjingzi,anjinho,anjinjaomby,anjinsi,anjiojio,anjiping,anjir,anjir ab,anjir avand,anjir baghi,anjir banavareh,anjir bas,anjir bazuiyeh,anjir boz,anjir darmian,anjir khvajeh,anjir mehi,anjir paz,anjir seyah,anjir shali,anjir siah,anjir sirkan,anjir-e bazuiyeh,anjir-e siah,anjir-e sigan,anjir-e siyah,anjira,anjirah,anjirak,anjirak inaghi,anjirak-e `olya,anjirak-e sofla,anjirak-e zarduni,anjiran,anjiravand,anjiraw,anjirbajsi,anjirband,anjirbas,anjirbash,anjirbok,anjirdan,anjireban avareh,anjirebanvareh,anjirebus,anjirebus-e bala,anjireh,anjireh khafrak,anjireh-ye bala,anjireh-ye ban avareh,anjireh-ye khafrak,anjireh-ye now,anjirestan,anjiri,anjiri kunj,anjirisheh,anjiristan,anjirk,anjirkhon,anjirli,anjirlu,anjiro,anjirobaka,anjirobi bolo,anjirohasina,anjirok,anjirow,anjiru,anjiruk,anjirvand,anjishan zonghe kenzhichang,anjishesh,anjiuli,anjiva,anjiwei,anjiz,anjlamananazy,anjm,anjo,anjo uta,anjobajoba,anjobatenina haut,anjobatenina-ambony,anjobojobo,anjocho,anjoeng,anjogh,anjogun,anjoha,anjohibe,anjohiny,anjoho,anjohobe,anjohy,anjojo,anjokkol,anjokozobe,anjokozobo,anjokozoka,anjokozoko,anjoktol,anjol,anjolajaya,anjolla,anjom,anjoma,anjoma-fanjakana,anjoma-ramartina,anjomachi,anjomakely,anjoman,anjoman-e `olya,anjoman-e sofla,anjoman-pain,anjombolova,anjomneh,anjong,anjongan,anjongdong,anjonggol,anjonggum,anjonggye,anjongjamal,anjongmal,anjongni,anjongo,anjonjang,anjopohy,anjorak,anjorijory,anjoro,anjorofady,anjorogaring,anjorozoro,anjorpur,anjoryoo,anjos,anjosil,anjotany,anjotar,anjou,anjouin,anjoul,anjoutey,anjovibe,anjoy,anjozomadosy,anjozora,anjozoro,anjozorobe,anjozorofady,anjozorokely,anjozoromadio,anjramarango,anjravand,anjshesh,anju,anju zhen,anjuarampur,anjuba,anjuban,anjud,anjudong,anjufang,anjugane,anjuge,anjukai,anjukgol,anjukkol,anjul,anjuli,anjulio,anjullan,anjullon,anjullor,anjum,anjuma,anjuman,anjumiao,anjumneh,anjun,anjuna,anjunem,anjung,anjungil,anjungni,anjungsan,anjungsijang,anjunodongjagu,anjur,anjurak,anjuran,anjuri,anjuriem,anjuup,anjuzhen,anjwadong,ank,ank koyu,ankelafit,ankete,ankosa,anka,anka kollu,ankaakor,ankaase,ankababy,ankabad,ankabahoba,ankabaka,ankabija,ankabijakely,ankabingo,ankaboa,ankaboka,ankaboka ampanihy,ankaboka bas,ankaboka maheba,ankabokabe,ankabokala,ankaboky,ankabova,ankacho,ankada,ankadialindalina,ankadibe,ankadibevava,ankadiboka,ankadiefa,ankadifotsy,ankadike,ankadilalana,ankadilalandrevaka,ankadilalanidimapolo,ankadilalindalina,ankadilalona,ankadilanakely,ankadilanana,ankadilava,ankadimalaza,ankadimanga,ankadimarina,ankadimbahoaka,ankadimbavy,ankadimbay,ankadimena,ankadimpary,ankadinadriana,ankadinanahary,ankadinandriana,ankadindambo,ankadinijamba,ankadinondry,ankadinondry-sakay,ankadipetaka,ankadirano,ankadiranobe,ankaditany,ankaditapaka,ankaditoho,ankaditsaravala,ankaditsiary,ankadivary,ankadivato,ankadivavala,ankadivoribe,ankadivory,ankadobarika,ankadun,ankady,ankadzor,ankafa,ankafimavo,ankafina tsarafidy,ankafitra,ankafitro,ankafobalo,ankafobe,ankafoka,ankafotra,ankafoza,ankaful,ankafy,ankahabo,ankahidrano,ankai,ankaiafo,ankaibe,ankaidirano,ankaifotsy,ankaihafa,ankaihely,ankaikarivo,ankaikely,ankaitombava,ankaitrombaka,ankaivo,ankajokoaka,ankakarivo,ankako,ankala,ankalaba,ankaladiny,ankalafo,ankalaitra,ankalaizina,ankalalo,ankalalobe,ankalamalaza,ankalamara,ankalambevava,ankalambo,ankalampadu,ankalampo,ankalampona,ankalandridrina,ankalangasa,ankalangasy,ankalanipona,ankalasag,ankalava,ankalgi,ankalia,ankaliboro,ankalilana,ankalimboro,ankalimboro haut,ankalindrano,ankalirano,ankalirano ouest,ankalitrandraka,ankalitrandraka ouest,ankalniskiai,ankaloaka,ankalomboro,ankalomboro ambony,ankalomboro bas,ankalomborona,ankalotany,ankalotra,ankam,ankamadoa,ankamaty,ankamay,ankamena,ankamenaha,ankamory,ankamotsy,ankamu,ankan,ankana,ankananavy,ankandatse,ankande colony,ankandja,ankandy,ankandziana ii,ankang,ankangareraka,ankangavanda,ankange,ankanghsien,ankani,ankanka,ankantaskantsa nord,ankantaskantsa sud,ankantsakantsa avaratra,ankao,ankaofafy,ankapaky,ankapika,ankapila,ankapilavato,ankapoaka,ankapodava,ankar,ankara,ankara morafeno,ankara moraharivo,ankara-andindo,ankara-anivo,ankarabato,ankarabatokely,ankarabe,ankarabo,ankaracbato,ankarafa,ankarafabe,ankarafabe bemokoty,ankarafotsy,ankarahaka,ankarahara,ankarahosy,ankaraina,ankaraka,ankarakaraka,ankaramainty,ankaramanga,ankaramangotroka,ankaramanihy,ankaramany,ankarambato,ankarambe,ankarambevava,ankarambilo,ankarambory,ankaramena,ankaramiambana,ankaramibe,ankaramihely,ankaramikely,ankaramy,ankaramy-anabo,ankaramy-anivo,ankaran,ankarana,ankarana-miraihina,ankaranabo,ankaranabo avaratra,ankaranabo nord,ankaranabo sud,ankaranakatra,ankaranana,ankarandava,ankarandoha,ankarandohe,ankarandona,ankaranila,ankaranka,ankarankely,ankarano,ankarantsokaka,ankarantsokaky,ankarany,ankarany anabe,ankaraobato,ankaraobato est,ankaraobato ouest,ankaraobato sud,ankaraok,ankararaka,ankararana,ankarata,ankaratravitra,ankaratsokaka,ankaratsy,ankaray avaratra,ankaray nord,ankaray sud,ankarchak,ankare,ankarede,ankarefa,ankarefo,ankarefobe,ankarena,ankarena avaratra,ankarenambe,ankarenana,ankareto,ankaria,ankariera,ankarimalaza,ankarimanana,ankarimbary,ankarimbelo,ankarimbelona,ankarimbolo,ankarina,ankarinany,ankarinarivo,ankarinomby,ankarinoro,ankaritsoka,ankarlov,ankaroka,ankarona,ankaronga,ankarongana,ankaroraky,ankarouba,ankarskatan,ankarsrum,ankarsund,ankarsvattnet,ankarsvik,ankarvattnet,ankary atsimo,ankasa,ankasakasa,ankasakasabe,ankasakasakely,ankasandra,ankase,ankasetra,ankasi,ankasie,ankasikitoka,ankasikitoky,ankasimbazaha,ankasimbe,ankasina,ankasy,ankata,ankatafa,ankatafa nord,ankatafa sud,ankatafana,ankatafy,ankatakatabe,ankatamboalava est,ankatamboalavo,ankatamboalavo ouest,ankate,ankati,ankatinskiy,ankatoaka,ankatofo,ankatoka,ankatoko,ankatoky,ankatoto,ankatrafahy,ankatrafay,ankatrafotsy,ankatrakatra,ankatrakatraka,ankatsaka,ankatsakafo,ankatsakala atsinanana,ankatsakala est,ankatsakala ouest,ankatsakatsa,ankatsaky,ankatsaoava,ankatsaoka,ankatso,ankatsomaranga,ankaty,ankaung,ankavan,ankavandra,ankavandrakely,ankavia,ankaviahely,ankavodiana,ankavokony,ankawa,ankayeno,ankazaberavy,ankazaeza,ankazamiheva,ankazo,ankazoabe,ankazoaberavo,ankazoabo,ankazoabo sud,ankazoabokaly,ankazoabokely,ankazoambany,ankazoambo,ankazoara,ankazoavo,ankazobe,ankazobe sud,ankazobe-antevamena,ankazoberavy,ankazobetroka,ankazobetsihay,ankazofohy,ankazofotsy,ankazohambo,ankazokoaka,ankazolahivato,ankazolahy,ankazolava,ankazoloaka,ankazomahita,ankazomahita ambony,ankazomahitsinandrianabe,ankazomahitsy,ankazomahity,ankazomainty,ankazomalahy,ankazomalama,ankazomalangy,ankazomalany,ankazomalemy,ankazomandry,ankazomaneno,ankazomanga,ankazomanga ambany,ankazomanga ambony,ankazomangao,ankazomangaria,ankazomanintsy,ankazomanitra,ankazomanitsy,ankazomanoa,ankazomantsina,ankazomasina,ankazomasy,ankazomateila,ankazombato,ankazomboro,ankazomborona,ankazomeloka,ankazomena,ankazomena nord,ankazomena sud,ankazomenavony,ankazomibaboka,ankazomidona,ankazomifoha,ankazomihogo,ankazomileka,ankazomiloka,ankazomirafy,ankazomiranga,ankazomiriotra,ankazomisokitra,ankazomitahy,ankazomitohy,ankazomivola,ankazomizoa,ankazomonga bas,ankazomonga haut,ankazonaorana,ankazonarano,ankazondandy,ankazondnano,ankazondrano,ankazondrano nord,ankazondrano sud,ankazondrenitsa,ankazondriaka,ankazondringitra,ankazongoaika,ankazonivola,ankazontaha,ankazorelo,ankazosamihafa,ankazosary,ankazosienehana,ankazota,ankazotelo,ankazoto,ankazotokana,ankazotsaravolo,ankazotsiafatatra,ankazotsifantatra,ankazotsifantotra,ankazotsivany,ankazovaky,ankazovanelina,ankazovelana,ankazoveland,ankazovelo,ankazovelona,ankazovinelo,anke,ankebotsy,ankede,ankegh,ankeheo,ankeijo,ankejabad,ankel,ankele,ankeliandilana,ankelidrano,ankelifaritra,ankelifolo,ankelilahy,ankelimahavoky,ankeling,ankeliroy,ankelitokana,ankelivelomy,ankelivondraky,ankelohe,ankema,ankemitt,ankenbuck,ankendagolla,ankendewa,ankendorf,ankenes,ankenesstrand,ankeng,ankengli,ankenhofen,ankenia,ankenihenibe,ankenihenitsara,ankeniheny,ankenihony,ankenreute,ankenta,ankeny,ankenytown,ankepika,anker,ankera,ankera ambany,ankera ambony,ankera-sarodrano,ankerabe,ankerabe avaratra,ankerabe nord,ankerabe sud,ankerabina,ankerade,ankerahera,ankerambe,ankeramena,ankerana,ankerana atsimo,ankerana avaratra,ankerana sud,ankerandava,ankerankely,ankerefo,ankerentsaly,ankerereta,ankeribe,ankerika,ankerika-mahasoa,ankerika-manevy,ankerikerika,ankerikika,ankeriky,ankeririana,ankern,ankershagen,ankerskaya,ankersta,ankerville,ankerville corner,ankesiasa,ankesihesy,ankesika,anketa,anketaka,anketata,anketokazo,anketrabe,anketraka,anketrakabe,anketrakable,anketrevo,anketrina,anketsahely,anketsihetsy,ankevaheva,ankevalia,ankevalia mota,ankeveen,ankeveense rade,ankevo,ankevo andrano,ankevo sur mer,ankezabe,ankezaeza,ankezaheza,ankhakov,ankhalyun,ankhamyiza,ankhar,ankhashtuk,ankhawari,ankhe,ankhelu,ankherskaya,ankhialo,ankhialos,ankhialos makedhonias,ankhimovo,ankhimovskaya,ankhiolo,ankho,ankhoi,ankhor,ankhpura,ankhva,anki,ankiabe,ankiabe-nord,ankiabe-salohy,ankiabenivalambana,ankiadava,ankiakabe,ankiakabolono,ankiakahely,ankiakantely,ankiakatraka,ankiakely,ankialo,ankiang,ankianifotaka,ankianjanakanga,ankianjasoa,ankiatomboka,ankibabaky,ankibainimay,ankibani,ankiboka,ankibory,ankibulbe,ankica brdo,ankida,ankida avaratra,ankida sud,ankidabo,ankido,ankidona,ankidranoka,ankifafa,ankifatry,ankifatry-anjobo,ankifi,ankifio,ankify,ankig,ankihefika,ankihonala,ankihsien,ankihy,ankijabe,ankijabe-ampasy,ankijahabe,ankijahana,ankijamanitra,ankijambe,ankijamena,ankijana,ankijana ambony,ankijanabe,ankijanambarahana,ankijanambony,ankijand,ankijandava,ankijandraibako,ankijandralesa,ankijaniabo,ankijanibe,ankijanifotaka,ankijanilanilava,ankijanilava,ankijanimadiro,ankijanimanga,ankijanimango,ankijanimanjolo,ankijanimavo,ankijanimena,ankijanomby,ankijapasy,ankijejo,ankijomantsina,ankikalava,ankikika,ankikikirika,ankila,ankilahila,ankilahilamamoa,ankilandilana,ankilansalo,ankilarov,ankilaroy,ankilatsy,ankilbato,ankilbe,ankilbevositra,ankiliabo,ankiliabokely,ankiliambany,ankilianano,ankiliarivo,ankiliarivo-sakarahe,ankiliatsy,ankilibaka,ankilibato,ankilibe,ankilibe-manoy,ankilibehara,ankilibetondro,ankilibevositra,ankiliboba,ankilibory,ankilida,ankilidahy,ankilidoha,ankilidonga,ankiliefatra,ankilifaly,ankilifamanta,ankilifanelo,ankilifanovotany,ankilifatry-anjobo,ankilifilo,ankilifito,ankilifolo,ankilihago,ankilihamba,ankilihogo,ankilijoria,ankilikara,ankililahy,ankililaly,ankililiry,ankililoaka,ankilimadinika,ankilimafaitsy,ankilimahasoa,ankilimahavelo,ankilimahitsy,ankilimahity,ankilimalaindio,ankilimalandy,ankilimalanga,ankilimalangy,ankilimalinika,ankilimamy,ankilimanan ambany,ankilimanandro,ankilimanara,ankilimanarivo,ankilimandroho,ankilimanintsy,ankilimanitsy,ankilimanjaka,ankilimanondro,ankilimanondry,ankilimanoy,ankilimany,ankilimare,ankilimarina,ankilimaro,ankilimaroanaka,ankilimarovahatsy,ankilimarovaotra,ankilimarovohitra,ankilimary,ankilimary nord,ankilimasina,ankilimasy,ankilimbazaha,ankilimena,ankilimiangy,ankilimiary,ankilimiavo,ankilimida,ankilimidaha,ankilimidahy,ankilimidega,ankilimiededa,ankilimievo,ankilimigaha,ankilimihamy,ankilimihanto,ankilimihohoky,ankilimikaiky,ankilimikalky,ankilimilangy,ankilimilapaka,ankilimilopaka,ankilimilopaky,ankiliminda,ankilimioko,ankilimionga,ankilimionja,ankilimisarotra,ankilimisivoka,ankilimitaha,ankilimitahy,ankilimitata,ankilimitrabika,ankilimitrabo,ankilimitraboka,ankilimitraha,ankilimitraho,ankilimitraloka,ankilimitranga,ankilimitrebika,ankilimitsaka,ankilimivona,ankilimivony,ankilimivory,ankilimizara,ankilimnasy,ankilimolinika,ankilimorarano,ankilinano,ankilindradada,ankilindramameno,ankilindramaneno,ankilindrehozoky,ankilindro 1,ankilindro 2,ankilindro i,ankilinoa,ankiliolio,ankilirandro,ankilirano,ankiliravy,ankiliroa,ankilirombotra,ankiliromotsy,ankilirone,ankiliroy,ankilisinao,ankiliso,ankilisoa,ankilitala,ankilitasy,ankilitelo,ankilitelo est,ankilitelo ouest,ankilitoka,ankilitokana,ankilitokara est,ankilitokara ouest,ankilitsiarova,ankilitsimahare,ankilivahary,ankilivalo,ankilivalo nord,ankilivalo sud,ankilivalobe,ankilivalokely,ankilivelomy,ankilivinonjy,ankilivola,ankilivolo,ankilivondraka,ankilivondraky,ankilizato,ankillantitsy,ankillmanintsy,ankiloka,ankiloka atsimo,ankiloka avaratra,ankilvalo-ambany,ankilvalo-ambony,ankily,ankily-tsilamaka,ankily-tsilamoka,ankimadoso,ankimamy,ankimarovo,ankimba,ankimbanovato,ankinaka,ankinana,ankinany,ankinaomby,ankindralesa,ankindrano ambony,ankindranoky,ankindresibe,ankindria ambany,ankindria ambony,ankindria nord,ankindria sud,anking,ankinga,ankingabe,ankingafohy,ankingafoy,ankingameloka,ankinganifahatelo,ankinganivalaka,ankinganomby,ankinirambe,ankino,ankinofika,ankinqanio,ankipatry,ankira,ankirajiboka,ankirakanga,ankirakanja,ankirakaraka,ankiranabo,ankirandro,ankirembaremba,ankirihira,ankirihiry,ankirihitra,ankirijabe,ankirijibe,ankirijifotsy,ankirijy,ankirikirika,ankirikiriky,ankirikiry,ankirilaza,ankiringingo,ankiripika,ankiririsa,ankirisimasy,ankirity,ankiriza,ankirondro,ankis,ankisabe,ankisafo,ankisaka,ankisakisaka,ankisanga,ankisangy avaratra,ankisangy nord,ankisanjy,ankisatla,ankisatra,ankisatry,ankisenikely,ankiseny,ankish,ankisha,ankisina,ankisiny,ankisira,ankisompy,ankisopy,ankitara,ankitata,ankitelov,ankito,ankitohy,ankitokazo,ankitra,ankitrevo,ankitrotro,ankitry,ankitsaka,ankitsakalaninaomby,ankitsaraimanga,ankitsikitsika,ankitsoka,ankitsopa,ankiu,ankiuhsien,ankiuqameleka,ankivania,ankivanja,ankivonjy,ankiyaba,ankizanibe,ankizeta,ankizobe,ankizomantsina,ankizotoka,ankka,ankkoinsalo,ankkonmi,ankkotcholgol,ankkoy,ankkula,ankkurikyla,ankkurongi,ankla,anklaar,anklachh,anklam,ankleshwar,anklesvar,ankli,anklisides,ankllivalokely,anklo,anklukh,anko,ankoabe,ankoadabo,ankoadave,ankoadavo,ankoakabe,ankoakala,ankoala,ankoalabe,ankoalamare,ankoalamaro,ankoba,ankobabe,ankobahimalangy,ankobahoba,ankobaimfafa,ankobaimirafy,ankobakobaka,ankobalava,ankobandronina,ankobany,ankobar,ankobay,ankobe,ankober,ankobiri,ankoboa,ankoboary,ankobohobo,ankobokobo est,ankobokobo ouest,ankoby,ankoby (1),ankoby (2),ankoby i,ankoby ii,ankochuang,ankod,ankoda,ankodahoda,ankodahoto,ankodeba,ankodeba atsimo,ankoditsokatra,ankodo,ankodohodo,ankodona,ankoetrika,ankofa,ankofafa,ankofafalahy,ankofafamalemy,ankofafamovo,ankofata,ankofibe,ankofika,ankofio,ankofio ambony,ankofomamy,ankoheo,ankohiri,ankoka,ankokabe,ankokaina,ankokkawala,ankokkewela,ankokoromby,ankol,ankola,ankolika,ankolin,ankolitoazo,ankolitsazo,ankolitsiza,ankolivelomy,ankolovola,ankoly i,ankoly ii,ankoma,ankomaka,ankomaka atsimo,ankomaka avaratra,ankomaka nord,ankomaka sud,ankomakana,ankomakoma,ankomaky,ankomamay,ankomanga,ankomany,ankomody,ankona,ankonabe,ankonahogong,ankonahona,ankonaka,ankonantsa,ankonatse,ankonatse ambany,ankonatse ambony,ankonatso,ankonatsy,ankondondona,ankondro,ankondrokely,ankondromainty,ankondromara,ankondromena,ankondrovoroy,ankonka,ankono,ankonsia,ankontsakantsa,ankontsakantsa atsimo,ankooaimalanga,ankopoaka,ankopy,ankopy-panihy,ankor,ankor pias,ankora,ankorabato,ankorabe,ankorahely,ankorahotra,ankoraka,ankorakabe,ankorakoraka,ankorakosy,ankoraky,ankoramahasoa,ankoramila,ankorapaka,ankoratsaka,ankorefa,ankorefo,ankorendrina,ankorera,ankori,ankorify,ankorika,ankorikakely,ankorikakely-andriamora,ankorimpa ambany,ankorimpa atsimo,ankorimpa-ambany,ankorimpa-ambony,ankoririka,ankoririky,ankoritika,ankoritsika,ankoritsy,ankoro,ankorobato,ankorobe,ankorohoro,ankorohotsa,ankorohotse,ankorokily,ankorokoro,ankorokoto,ankorombe,ankoromboakoa,ankoromokoty,ankorompony,ankorona,ankoronadabo,ankorondamoty,ankoronga,ankoronkily,ankororka,ankororoka,ankororoky,ankosibe,ankosihosibe,ankosihosy,ankosilava,ankosirika,ankosoasoa,ankosy,ankotika,ankotikana,ankoto,ankotoba,ankotoboka,ankotofotsy,ankotoka,ankotokomby,ankotrabe,ankotraka,ankotrakabe,ankotrakotraka,ankotraotra,ankotrofotsy,ankotroftsy,ankotroka,ankotrokotroka,ankotsaka,ankotsaobihia,ankotseha,ankotsika,ankotsobe,ankotsobe ambany,ankotsobe ambony,ankotsohots,ankotsohotso,ankotsuri,ankou,ankouadzia,ankouandzion,ankouatia-foumbeou,ankouchen,ankoukope,ankoum,ankoum myo,ankouma,ankouna,ankoung,ankouokari,ankouomb,ankoura,ankouria,ankouzhen,ankovana,ankovanana,ankovena,ankovice,ankovitra,ankovo,ankovohova,ankovoka,ankovona,ankoyate,ankozany,ankozohozo,ankozomalany,ankpa,ankpan,ankpechi,ankra,ankraddipalli,ankrakwanto,ankral,ankrankwanta,ankreddipalli,ankroet,ankrum,ankshtakey,ankstakiai,anku,ankua,ankuang,ankuash,ankuchen,ankudanova,ankudinikha,ankudinova,ankudinovka,ankudinovo,ankudy,ankuk,ankukrom,ankula,ankum,ankuma,ankumadua,ankumbura,ankumbura pallegama,ankumbura udagama,ankuna,ankundikovo,ankundinovo,ankundinovskaya,ankunso,ankuo,ankuocheng,ankuochengchuang,ankuochengkuan,ankuohsien,ankura,ankurlik,ankuru,ankushi,ankushina,ankutia,ankuttankulama,ankuwa,ankwa,ankwadoboro,ankwaieso,ankwana,ankwanda,ankwang,ankwaoso,ankwasu,ankwiam,ankwo,ankwohsien,ankyam,ankye,ankyegh,ankyo,ankyra,anla,anlabo,anlaby,anladasduzu,anlalateng,anlamitsangans,anlan,anlangbei,anlas,anlauf,anlauftal,anle,anle shizu,anle yizu,anle zhen,anlebaozi,anlechai,anlechang,anlecun,anledi,anlegong,anlehe,anleho,anlekeng,anlelin,anlelin linchang,anleng,anleo,anleping,anleqiao,anlequan,anlesi,anletan,anletsun,anletun,anleying,anleyuan,anlezhen,anlezhuang,anlezy,anlhiac,anli,anliac,anlialava,anliang,anliang zhen,anliangbao,anliangpu,anliao,anlichong,anlicun,anlier,anlinchan,anling,anlingchen,anlingcun,anlingou,anlingzhen,anlinha,anlinzhan,anlipolo,anlishan,anlitsun,anliu,anliuchen,anliuhsu,anliuzhuang,anllares,anllarinos,anllo,anlo,anlochai,anlochan,anlochen,anloga,anloho,anlolu,anlon sa,anlong,anlong chrey,anlong kanchos,anlong kantuot,anlong kong,anlong kra ngan,anlong krasang,anlong kray,anlong kres,anlong kroeus,anlong loc,anlong lok,anlong metrey,anlong pra,anlong preng,anlong reach,anlong sanday,anlong sandey,anlong shizu,anlong sleng,anlong sno,anlong thmar,anlong trach,anlong veng,anlong vieng,anlong-krey,anlongma,anlongpu,anloo,anloting,anlou,anloua,anloy,anlu,anlubi,anlufu,anluhsien,anluk,anlung,anlung trach,anlung veng,anlunghsien,anlungpuitsumiaotsutzuchihhsien,anluo,anluojiao,anluta,anma,anmadurae,anmaedong,anmaeil,anmaengni,anmagundam,anmahsu,anmak,anmaka,anmakgol,anmakkol,anmal,anmallae,anmandujae,anmangdong,anmangsil,anmannykan,anmar kalan,anmara mudr,anmasan,anmasangol,anmasil,anmatou,anmaul,anmayang,anmbarang,anmedong,anmei,anmenchen,anmengou,anmengtun,anmenkeng,anmenkou,anmenkou zhen,anmenkouchen,anmenshi,anmeolguji,anmer,anmi,anmiao,anmiaocun,anmid,anmigundam,anmin,anmin sizu,anmindong,anminik,anminni,anminsi,anmintsun,anmiri,anmiter,anmitre,anmizu,anmo,anmojong,anmoksil,anmolguji,anmoore,anmoraebat,anmorigol,anmosan,anmosang,anmosigol,anmotchil,anmouli,anmoulil,anmounae,anmudong,anmudungni,anmujiao,anmungol,anmurusil,anmuryang,anmushil,anmusil,anmvam,anmyo,anmyon,anmyonbae,ann,ann arbor,ann nachfe,ann shaw,ann wrights corner,anna,anna bay,anna bella estates,anna catherina,anna deh,anna dias,anna guia,anna jacoba,anna jacobapolder,anna ka jhonpara,anna lynne,anna majer,anna maria,anna nagar,anna nagar west,anna paulowna,anna paulowne,anna reck,anna uspenka,anna-abam,anna-khudzha,anna-major,anna-mamed,anna-mamet,anna-maryevskiy,anna-rebrikovskaya,anna-tolono,anna-yetege,annaakh,annaba,annabasi,annabatan,annabe,annabella,annaberg,annaberg im lammertal,annaberg-buchholz,annaberk,annabi,annabichl,annabisi,annaboty,annabrunn,annabuculan,annaburg,annacarriga,annacarty,annaclone,annacloy,annaculu,annacurragh,annada,annadale,annadel,annadorf,annae,annaegol,annafatan,annafunan,annafurdo,annagary,annagassan,annagh,annagh bridge,annagh upper,annaghbrack,annaghdown,annaghgerry,annaghkeenly,annaghmore,annaghneal bridges,annaghroe bridge,annaghvaan,annaghvacky,annagleve,annagol,annagore bridge,annahatn,annahegy,annahilt,annahitte,annahof,annahutte,annai,annaiwilundawa,annajewo,annajorgl,annaka,annaka-kareki,annakert,annakul-ravat,annakul-rovat,annala,annaleck,annalee heights,annalera,annaliget,annalittin,annaloist,annalong,annalsil,annaly,annam,annamajor,annamalai,annamalainagar,annamaly,annamer,annameur,annamgol,annammakulam,annamoe,annamoisa,annamoriah,annamurat,annan,annan sh sharqiyah,annana,annanba,annanbakhetan,annanberg,annanberg mission,annancheng,annandale,annandale acres,annandale estates,annandale terrace,annandale-on-hudson,annanhsien,annanolli,annanpa,annanpahotan,annantsun,annanuman,annao,annap,annapareddipalle,annaparochie,annape,annapol,annapole,annapolis,annapolis cove,annapolis landing,annapolis roads,annapolis rock,annapolis royal,annappes,annaquatucket,annaram,annarode,annarotte,annarugama,annas,annas hope,annas muiza,annas muizas centrs,annas pusmuiza,annasagar,annasbirze,annaseeragh,annasgeluk,annashoop,annaside,annaskalni,annaskrogs,annasm turlaji,annasmaja,annasmuiza,annasmuyzha,annassoukro,annat,annatanya,annatevanmadu,annath,annathal,annatira,annaton,annatsberg,annatto bay,annatunan,annau,annavalla,annavaram,annavasal,annaville,annavolgy,annavolgyibanya,annawali,annawan,annawater,annawomscutt,annay,annay-la-cote,annay-sous-lens,annay-sur-serein,annaya,annayatan,annbank,annbarli,anncar,anndad,anndalsvaagen,anndiare,anndouss,anne,anne acres,anne heights,anne manie,annes bridge,annes tract settlement,anneanwali,anneau,annebault,annebecq,anneberg,annebo,annebol,anneborg,anneburen,annecroix,annecy,annecy-le-vieux,annedal,anneewakee,anneewakee estates,annefors,annei,anneitsun,annekenshoek,annekovo,annekoy,annekrou,annel,anneland,annelema,annelera,annelles,annelly,annelov,annelovo,annelsbach,annelund,annemasse,annemossen,annen,annenas,annenaset,annenberg,annendaal,annenfeld,annenfeld,annenheide,annenheim,annenhof,annenieki,anneniyeki,annenka,annenki,annenkovo,annenkovo pervoye,annenkovo vtoroye,annenkovo-lesnoye,anneno,annenriede,annensk,annenskaya,annenskij most,annenskiy,annenskiy lazavets,annenskiy lozovets,annenskiy most,annenskoye,annenwalde,anneot,annep,annepe,annepont,annequin,annero,annerod,annerossle,annerstad,annerud,annerup,annerveen,annerveensch kanaal,annerveenschekanaal,annes delight,annesbrook,annesley,anneslie,anness,annesse-et-baulieu,annesse-et-beaulieu,annesta,annestad,annestorp,annestown,annesville,annet,annet-sur-marne,anneta,annetta,annetta gardens,annetta north,annetta south,annette,anneux,anneville,anneville-ambourville,anneville-en-saire,anneville-la-prairie,anneville-sur-mer,anneville-sur-scie,anneville-sur-seine,annevoie-rouillon,annex,annex kilvorskull,anneyron,annezay,annezerg,annezin,annezin-les-bethune,annfield,annfield plain,annfoud,annfred,anngedaung,annhanan,anni,anni shungi,anni-ye bala,anni-ye pain,annicco,annie,annie brady bridge,annie hall,anniedale,anniedelle,anniesdale,anniesland,annieville,annifo,annigeri,annikanniemi,annikkala,annikoru,annikova,annikovka,annikovo,anniksaare,annikse,annikvere,annimata,annimdong,annin,annin creek,annin verkh,annin verkhniy,annin-navolok,annina,annina bayka,annina sloboda,anninata,anning,anning shi,anning xiang,anningchang,anningchen,anningchiao,anningchow,anningchu,anninggang,anningpu,anningqiao,anningqu,anningqu zhen,anningzhuang,anninka,annino,annino-gusinovka,annino-masharovo,anninou,anninsk,anninskaya,anninskiy,anninskiye mineralnyye vody,anninskiye-bolshevitsy,anninskoye,anninskoyebozhedanovka,anniq,annis,annisfontein,annisquam,annisse,annissi,anniste,anniston,annistown,annisville,annita,annitapolis,anniudian,annivain,annivert,annivka,annjara,annkoannda,annkoud,annkula,annli,annlin,anno,anno-rebrikovo,anno-rebrikovskaya,anno-uspenka,annoapanly,annobon,annobor,annoceur,annoeullin,annogora,annoire,annois,annoisin,annoisin-chatelans,annoix,annokhovo,annolesie,annolfsbyn,annolovo,annolpum,annona,annonay,annone di brianza,annone veneto,annonen,annong,annongni,annonin,annonsigol,annonville,annoopanlinka,annopokrovka,annopol,annopol a,annopol b,annopole,annopolye,annorae,annori,annory,annoslaw,annospasskaya,annosseur,annot,annoternovskaya,annotrebinovka,annotta bay,annotto bay,annou n addou,annoual,annoums,annouqar,annour,annouville,annouville-vilmesnil,annoux,annovice,annoville,annovka,annovka pervaya,annovka trebinova,annovka vtoraya,annovkaagte,annovkaternovskaya,annovkavirovskaya,annovo-rebrikovo,annovskiy,annovskiy pilnyy zavod,annovskiy zavod,annovskoye,annow,annowka,annowo,annowo-rebrikowo,annqara,annrossle,annrotsle,anns castle,anns fort,annsari,annseur el abiod,annseur en nhas,annsjotorp,annsri,annstad,annsville,anntaki,anntelias,anntouz,anntveit,annu,annua,annual,annuello,annuit,annulskiy,annula,annungan,annunge,annungtsun,annunziata,annur,annurukkol,annurum,annurundae,annus,annuschen,annusewen,annushino,annushka,annussewen,annustanya,annuwala,annville,annwas,annweiler,annweiler am trifels,anny,annyakh,annyama,annyeongri,annyinya,annyong,annyongchon,annyongni,annyori,annyualkal,annza,annza absa,annzad,annzal,annzel,annzig,annzigue,annzouggar,ano,ano aetos,ano afroxilia,ano agali,ano agalis,ano agioi anargyroi,ano agios ioannis,ano agios konstantinos,ano agoriani,ano aiyialos,ano akhaia,ano akria,ano alepokhori,ano alepokhorion,ano alissos,ano almiri,ano amfia,ano anavriton,ano apostoloi,ano arfara,ano arkhanai,ano arkhanes,ano aroi,ano aryilokhorion,ano asitai,ano asitais,ano asites,ano astrakoi,ano ayia marina,ano ayia paraskevi,ano ayios ioannis,ano ayios vlasios,ano belesi,ano botinon,ano boularioi,ano boularyoi,ano bourlesia,ano boursiani,ano brallos,ano bralos,ano chahmati,ano chalikas,ano chorion,ano davyia,ano de juarez,ano del hidalgo,ano derekli,ano derengli,ano despotikon,ano dhafni,ano dhafnoudhi,ano dhamasta,ano dhaton,ano dhavia,ano dherekli,ano dherenglion,ano dhespotiko,ano dhespotikon,ano dhiakopton,ano dhiminio,ano dhio vouna,ano dholiana,ano dholoi,ano dhorion,ano dhranista,ano dhrimalaiika,ano dhrimon,ano dhrosini,ano doumina,ano doupa,ano drosini,ano drymon,ano duvlatan,ano esokhoria,ano evinokhorion,ano exanthia,ano fanari,ano fanarion,ano fortesa,ano fortetsa,ano frastaina,ano fteri,ano fteria,ano fterias,ano garantza,ano gardhenitsa,ano garefeio,ano garefeion,ano garefi,ano garefion,ano garouna,ano gatzea,ano gavalou,ano gavrion,ano girbas,ano glikovrisi,ano goskova,ano goumenitsa,ano graikiko,ano graikikon,ano grammatikon,ano idhrousa,ano idhroussa,ano ilia,ano ionikon,ano iraklion,ano kaberler,ano kalamaki,ano kalendini,ano kalentini,ano kalesa,ano kalesia,ano kalesmenon,ano kalivia,ano kalivia filion,ano kalliniki,ano kallithea,ano kalon neron,ano kamari,ano kamarion,ano kambi,ano kambia,ano kamila,ano kanalia,ano kaniani,ano karajakoi,ano kardhamos,ano karea,ano karia,ano kariai,ano kariofiton,ano karitsa,ano karnezaiika,ano karnezeika,ano karouzana,ano karyes,ano kastanea,ano kastania,ano kastelliana,ano kastritsion,ano katelios,ano kefala,ano kefalarion,ano kerasea,ano kerasia,ano kerasovon,ano khalasmata,ano khalikas,ano kharvati,ano khionokhoron,ano khlamoriani,ano khomondhos,ano khora,ano khorio,ano khorion,ano khouni,ano khovoli,ano khrisovitsa,ano khristos,ano klinai,ano klitoria,ano kollinai,ano komi,ano konstandinia,ano kontogoni,ano kopanaki,ano kopanakion,ano kopanitsa,ano kopanos,ano korakiana,ano korikani,ano koritiani,ano korthion,ano kotsanopoulo,ano kotsanopoulon,ano kottori,ano koudhouni,ano koudhounion,ano kourounion,ano kourtaga,ano kranionas,ano kremmidhia,ano ktimeni,ano kurtaga,ano labov e kryqit,ano lapsista,ano lefkimmi,ano leika,ano lekhonia,ano lendekadha,ano levkadhion,ano levki,ano liosia,ano lipokhorion,ano lithochorion,ano livera,ano loukavitsa,ano lousoi,ano loutraki,ano loutro,ano loutron,ano makrinou,ano malakion,ano malataiikon,ano malateikon,ano mamakon,ano mammako,ano mamoula,ano manolas,ano mathrakion,ano mavrikion,ano mavrolofos,ano mazaraki,ano mazarakion,ano megali ada,ano megali adha,ano melas,ano melpia,ano mera,ano meria,ano meros,ano metapa,ano militsa,ano minayia,ano mirtia,ano mistros,ano mitikas,ano mitrousi,ano mitrousion,ano mitrousis,ano moulia,ano mousiotitsa,ano mousonitsa,ano mousounitsa,ano mousoura,ano mousouroi,ano nevoliani,ano nuevo,ano nuska,ano orfana,ano orini,ano palaeoxari,ano palaiokaria,ano palaiokarya,ano palaiokastro,ano palaioxari,ano palaioxarion,ano paliokaria,ano paliokarya,ano paliozoglopi,ano papratskon,ano paradhision,ano pavliana,ano pedhina,ano pedina,ano perliango,ano petali,ano petalion,ano petra,ano pitsa,ano pixarion,ano platanos,ano platikambos,ano polidhendrion,ano polidhrosos,ano porocan,ano poroia,ano porroia,ano potamia,ano poulia,ano prostovas,ano psikhiko,ano psikhikon,ano pteri,ano pteria,ano rachi,ano rakhi,ano ravenia,ano retsina,ano ringlia,ano rodhakinon,ano rodhonia,ano rodonia,ano salajak,ano salatzak,ano salmenikon,ano salmenikos,ano sangrion,ano selce,ano selitsa,ano seta,ano sfinari,ano sfinarion,ano siaterli,ano siaterlion,ano sikhaina,ano simbe,ano simi,ano sinoikia,ano siros,ano skafidhoti,ano skafidoti,ano skholarion,ano skotina,ano skotousa,ano skotoussa,ano soudena,ano soudeneika,ano soudhenaiika,ano soudheneika,ano souli,ano soulima,ano soulion,ano sourmena,ano souvala,ano stavros,ano strapodhion,ano strouza,ano syra,ano tarsinon,ano tarsos,ano theodhoraki,ano theodhorakion,ano theologos,ano thermai,ano trikkala,ano tripodho,ano tripodhon,ano tzineriana,ano tzineryiana,ano valsamoneron,ano valtai,ano vanitsa,ano vardhatai,ano varibobi,ano varibopi,ano varimbopi,ano varsamoneron,ano vasilika,ano vasiliki,ano vasilikon,ano vasilikon i,ano vasilikon ii,ano vaskina,ano vathi,ano vathia,ano velisa,ano velitsai,ano verga,ano verziani,ano viannos,ano virou,ano vitali,ano vitsa,ano vlasia,ano vlassia,ano vlokhos,ano volimai,ano volimais,ano volos,ano voskina,ano voukolies,ano vounaina,ano voutaina,ano voutina,ano vouvai,ano vouvas,ano vrondou,ano vrontou,ano xechoron,ano xekhoron,ano xirokhori,ano yerakari,ano yerakarion,ano yiannaioi,ano yianneika,ano zaimoglion,ano zakhlorou,ano zalogkon,ano zalongon,ano zaros,ano zervochori,ano zervochorion,ano zervokhori,ano zervokhorion,ano ziria,ano-vardhatais,anoano,anoar khila,anoarkhali,anoatusia,anoborano,anobra,anobre,anobres,anocagua,anocalani,anocalla,anocarayre,anocarire,anoccalla,anoccara pampa,anochi,anocibar,anock chikwaba,anocochiva,anod,anoe,anoenu,anoes,anoeta,anoewe,anofai,anofalake,anofandravoay,anoff,anofia,anofia nkanu,anog,anogal,anogeia,anogeiata,anogeil,anogeio,anogeion,anoghia,anogia,anogok,anogu,anogyaunggale,anogyra,anohadpur,anohenekrom,anoi sarhota,anoia,anoia superiore,anoicer,anoig,anoija,anoinata,anoixi,anoixia,anoixiatikon,anoixis,anojiguda,anojong,anok,anoka,anokafa,anoketo,anokh singhwala,anokhe purwa,anokhina,anokhinka,anokhino,anokhinskiy,anokhinskoye,anokhintsy,anokhori,anokhorion,anokhovo,anoki mkhere,anokokrom,anokolke,anokro,anokwa,anokwaa,anokwai,anokwakwale,anokwasi,anol itam,anola,anolahely,anolaima,anolaka,anolaky,anolamena,anole,anolfsbol,anolid,anoling,anoling 1,anoling primero,anoling segundo,anoling tercero,anolingan,anolobe,anolom,anolotra,anom,anoma,anomabo,anomabu,anomadodom,anomamoa,anomang mayor,anomang menor,anomangatsiaka,anomanye,anomaou ahoso,anomatuepin,anombakro,anomdong,anomeria,anomi,anomikro,anomilevy,anomlid,anomo,anomon,anompaka,anon,anona,anonal,anonales,anonalez,anonang,anonang mayor,anonang menor,anonangin,anonas,anonay,anoncillo,anonebere,anong,anongan,anongibe,anongmani,anongo,anongomiani,anongu,anonguabani,anonguambane,anonguanbani,anongue,anonho,anoniusu,anonia,anonibe,anonin,anonitas,anonkaren,anonkoua,anonkoua-koute,anonkpo kouassi,anonkro,anonniemi,anonniyemi,anono,anono mite,anono-o,anonobe,anonoka,anonokambo,anonokombo,anonoky,anontcho,anontsibe,anontsibe-sakalava,anontsikely,anontsy,anonuevo,anony,anooj,anoothook,anopenskiy,anopino,anopo assikoua,anopochkin,anopog,anopol,anopolis,anopolye,anoque,anor,anora,anoramitsipika,anorana,anoranotakatra,anorbe,anordliuitsoq,anoremarti,anorga,anorga-lugariz,anorhrif,anori,anoriakazo,anoriamira,anorimbato,anoring,anorliuitsoq,anorombato,anoromby,anoron,anoronala,anoronjia,anoront sanga,anorontsangana,anororo,anorotsangana,anorrakhi,anoru,anory,anoryudong,anos,anosa,anoshinka,anoshino,anoshki,anoshkino,anoshkinsk,anosiala,anosiarivo,anosiary,anosiato,anosibe,anosibe an ala,anosibe-ifanja,anosibe-trimoloharano,anosibevary,anosiboribory,anosifaka,anosifeno,anosifisaka,anosifito,anosikabija,anosikabitsa,anosikapika,anosikary,anosikely,anosikha,anosilahy,anosilava,anosimalainolona,anosimanasa,anosimanitsa,anosimanitsy,anosimanjaka,anosimarina,anosimbary,anosimbevary,anosimboahangy,anosimborona,anosimena,anosimiarina,anosimiarine,anosimita,anosimparihy,anosimpotaka,anosindambo,anosindrabela,anosindrafilo,anosindrano,anosino,anosinparihy,anosiparihy,anosipatrana,anosiravo,anosiroa,anositelo,anosivato,anosivelo,anosivola,anosivolo,anosizato,anoskeli,anosmarca,anosova,anosovka,anosovo,anosseur,anost,anosy,anosy avaratra,anosy-dika,anosy-rahindy,anotane,anotano,anoter,anoth,another place,anoto,anotole,anotsy,anou,anou el jaid,anou el jedid,anou ibder,anou ider,anou illigh,anou izem,anou izme,anou lejdid,anou llig,anou naddi,anou nait boulmane,anou ndaoud,anou nfeg,anou nhaddou ou ichou,anou nouachcha,anou naadou,anou nizam,anou yedder,anou-n-ait boulamane,anoual,anouan kouamikro,anouanze bokaha,anouar,anoublekro,anoufeg,anoufoue bounou,anougal,anougbakro,anougbre kouassi,anougoud,anouinet mora,anoukinga,anoukou,anoukoua koute,anoukoua-koute,anoukro,anould,anoulekouamikro,anoumaba,anoumabo,anoumabou,anoumagoua,anoumangoua,anoumankro,anoumbakro,anoun,anouna,anounat,anoune,anourezia,anourgin,anousk,anoux,anova,anove,anover de tajo,anover de tormes,anovi-ye pelasi,anoviana,anoviara,anovibe,anovihazo,anovilava,anovilava atsimo,anovilava avaratra,anovilava sud,anovilava-nord,anovo,anovy,anovy atsimo,anovy nord,anovy sud,anowa,anowah-ye khurd,anowali,anowara,anowonle,anowounamalo,anowounoouras,anowshiravan,anoye,anoyi,anoyia,anoyion,anoyira,anoyon,anoyra,anoz,anoza,anpagan,anpagol,anpanahovatsokay,anpanambato,anpanchen,anpanching,anpandari,anpannaeri,anpeh,anpei,anpeihsien,anpeishechihchu,anpel,anpen,anpenachieh,anpeng,anpeni,anphagale,anphagyi,anphlong ta o,anpidokkol,anpien,anpienchang,anpienchen,anpienpao,anpienpu,anpilogovo,anpilovka,anpimi,anpinche,anping,anpingchen,anpingchengkuan,anpingchi,anpingcun,anpinghsien,anpingtsun,anpingtun,anpingyang,anpingzhuang,anpo,anpog,anponou,anpopo,anpori,anpotsun,anpountoum,anpu,anpung,anpungdong,anpungdongsongpungdong,anpungni,anpur,anpushih,anpyeong,anpyeongbonburak,anpyeongri,anpyong,anpyongdong,anpyongjanggol,anpyongjon,anpyongni,anpyongponburak,anqarcaq,anqarchaq,anqi,anqian,anqiancun,anqiang,anqiankeng,anqiantan,anqiantang,anqiao,anqiaotou,anqing,anqingchong,anqingcun,anqinggou,anqingqiao,anqiu,anqog,anqorah,anqu,anqu muchang,anqua,anquaira,anquan,anquancun,anquanlou,anquanpu,anquanxu,anquati,anque,anquela del ducado,anquela del pedregal,anquetierville,anqui,anquicha,anquilen,anquillara sabazia,anquimeria,anquinamainha,anquinamoinha,anquincila,anquioma,anquioma alto,anquioma bajo,anquiray,anquita,anqurti,anquwan chido,anraesangkol,anraff,anrag,anraku,anrakuji,anralli,anrang,anrangae,anransi,anrao,anras,anrata,anrath,anreade,anreapi,anreep,anren,anren chengguanzhen,anren zhen,anrencun,anrengnge,anrenpo,anrenpu,anrenxi,anrenyuan,anrenzhen,anreppen,anrerif,anri,anriac,anried,anrihua,anrilia,anris,anris tara,anristan,anrochte,anrode,anroig,anrong,anrong xiang,anrosey,anroual,anrouy,anruling,ans,ans sinyan,ansa,ansa chambak,ansa kdam,ansa kedam,ansa khmay,ansab,ansac,ansac-sur-vienne,ansacq,ansacuit,ansad,ansaechul,ansaedangi,ansaegol,ansaegyong,ansaeu,ansage,ansager,ansagiso,ansagnere,ansagnere diengueibe,ansagnere soguiyabe,ansahra,ansai,ansaikova,ansainiai,ansakkum,ansakwa,ansalahti,ansalli,ansalmaegi,ansalmi,ansalmok,ansalonga,ansalta,ansamgulli,ansamgusil,ansamhanni,ansamjong,ansammi,ansammundong,ansan,ansanbat,ansandae,ansandong,ansangdong,ansangnere,ansangni,ansangol,ansangon,ansanguere,ansani,ansaniemi,ansanmal,ansanmit,ansanni,ansanri,ansanttul,ansapchae,ansapul,ansar,ansar krahay,ansar kranag,ansar ol emam,ansarabad,ansari,ansari mahalleh,ansariwala,ansart,ansas,ansasan,ansatten,ansauville,ansauvillers,ansawe,ansbach,anscharhohe,anschau,anschiessing,anschlag,anscroft,ansdorf,anse,anse a chat,anse a cochon,anse a cochons,anse a foleur,anse a galet,anse a galets,anse a joseph,anse a louis,anse a macon,anse a pitre,anse a proux,anse au griffon,anse au loup,anse aux anglais,anse boileau,anse caffard,anse canot,anse comeau,anse dazur,anse dhainault,anse des cayes,anse des flamands,anse du clerc,anse du me,anse du nord,anse figuier,anse figuiers,anse fourmi,anse galets,anse ger,anse jonchee,anse joseph,anse la butte,anse la raye,anse la verdure,anse louis,anse madame,anse mapay,anse noir,anse noire,anse pirogue,anse pitres,anse robert,anse rouge,anse royal,anse royale,anse volbert village,anse-a-foleur,anse-a-pitre,anse-a-pitres,anse-a-veau,anse-bertrand,anse-les bas,anse-les hauts,ansean,anseba,ansedonia,anseghem,ansegol,anseimo,anseith,ansekil,ansekiri,ansekula,ansekyula,ansel,anselfingen,anselin,anselm,anselma,anselmi,anselmito,anselmo,anselmo de andrade,anselmo sedy,anselmos,ansem,ansemar,ansembourg,ansemil,ansen,ansene,anseomburi,anseong,anseongsamdong,anseorwolri,anserall,anseremme,anserma,ansermanueva,ansermanuevo,anseroeul,anserville,anses a pitre,anses darlet,anses darlets,anses-a-pitres,anseuge,ansfelden,ansgarii,ansh-abad,ansha,ansha zhen,anshagua,ansham,anshan,anshan ba,anshan wujia,anshanchen,anshancun,anshang,anshangcun,anshangzhuang,anshanli,anshanpo,anshanqiao,anshanshih,anshantun,anshanxia,anshanyu,anshayan,anshe,ansheng,anshero-sudshensk,anshi,anshigua,anshih,anshimdong,anshimri,anshin,anshindo,anshing,anshiva,anshleyka,anshu,anshucho,anshui,anshun,anshunbi,anshunchang,anshunfu,anshunhsien,anshunhsiencheng,anshunqiao,anshunshih,anshuo,anshuotsun,anshutino,anshuzhuang linchang,anshwa,ansi,ansi xian,ansia,ansiacasa,ansiah,ansiang,ansianggol,ansianghsien,ansiap,ansicourt,ansicun,ansidongniyangji,ansiedlung mettenhof,ansiemua,ansignan,ansigny,ansigol,ansigou,ansijap,ansil,ansilupis,ansim,ansimchon,ansimdong,ansimgol,ansimni,ansimukkol,ansin,ansina,ansinae,ansindong,ansindro,ansing,ansip,ansipar,ansipsip,ansir,ansirabe,ansirae,ansirat,ansiri,ansis,ansizca,ansizio,ansjo,anski,anskina,ansley,ansley acres,ansley brook,ansley heights,ansley mill,ansley park,ansley place,ansley pointe,anslow,ansmark,ansnes,anso,ansoam,ansodae,ansodumul,ansoedoe,ansoejom,ansoes,ansoesil,ansoeyakkul,ansofiri,ansofry,ansoga,ansogun,ansoil,ansojae,ansojigae,ansokkul,ansokri,ansola,ansolbang,ansolchi,ansolgogae,ansolma,ansolti,ansomanakouro,ansomburi,ansommal,anson,ansong,ansongdong,ansonggol,ansonggum,ansonggwan,ansonghyon,ansongjang,ansongni,ansongo,ansongri,ansongsamdong,ansongyuchon,ansonhe,ansonia,ansonirena,ansonville,ansoppa,ansoppau,ansorandia,ansorim,ansorok,ansorwolli,ansoseur,ansost,ansou,ansouara,ansoudou,ansouis,ansouki,ansoukki,ansoum,ansoumania,ansoumanya,ansour,ansouri,anspach,anspaki,anspoki,ansprung,ansri,anssaakpodong,anssafoule,anssangpodong,anssanguere,anssu,anssukpakkol,anst,ansta,anstad,anstahyttan,anstaing,ansted,anstedt,anstel,anster,anstey,anstine,anstois,anston,anstoss,anstruther,anstruther easter,anstruther wester,ansty,ansu,ansub,ansubisto,ansudu,ansuelle,ansuhsien,ansujom,ansukpay,ansul,ansulchi,ansule,ansum,ansumori,ansund,ansur,ansus,ansuto,ansutuan,ansvar,ansveet,ansvik,ansyan,ansyu,ansyu yu,ant,ant flat,antebat,antebatbi,antefa,anta,anta anta,anta de rioconejos,anta de tera,anta gorda,anta hacienda,anta huaca,anta huaico,anta khaua,anta palca,anta picho,anta piteac,anta ucro,anta,anta-kasy-marovo,anta-payute,anta-uta,antaanta,antabamba,antabamba hacienda,antabania,antabaru,antabary,antabasta,antabo,antaboakavalala,antaboara,antaboary,antacagua,antacalla,antacancha,antacarana,antacarca,antaccahua,antaccasa,antaccollo,antachan,antachen,antacocha,antacollo,antacolpa,antacorral,antacucho,antacunca,antadao,antadinga,antado,antady,antae,antaebong,antaegol,antaek,antaengni,antaeri,antafa,antafia,antafiabe,antafiamanga,antafiamantsina,antafiamatso,antafiamazava,antafiambaro,antafiambato,antafiambe,antafiambotry,antafiamilaneva,antafiamivony,antafiamore,antafiana,antafianambity,antafianampela,antafianampela ambany,antafiandakana,antafiangita,antafianimiadana,antafianonamasina,antafiaratsy,antafiatokana,antafiembaro,antafindrato,antafiombotry,antafofo,antafononana,antafotenina,antafy,antaga,antagan,antagan dos,antagan uno,antagana kaja,antagana kangin,antagana kelod,antagarh,antage,antagine,antagines,antagnac,antagussa,antagyneles,antagynes,antahalavala,antahalavalala,antahanahary,antahandava,antahar,antahsien,antahualla,antahuancan,antahuanco,antahuara,antahuayco,antahuiri,antahuyo,antai,antai jiedao,antaibao,antaige,antaignague,antaignagues,antaigudam,antaih,antaikomby,antaiksne,antaillat,antaimarca,antaimborona,antaimby,antainaomby,antainkomby,antainosy,antajahua,antajahuinto,antajave,antaje,antajo,antak,antakabana,antakalniai,antakalnio,antakalnis,antakambana,antakandjadi,antakanjadi,antakantak,antakavana,antaki,antakitaka,antakitaky,antakiya,antakmenys,antakobola,antakole,antakoly,antakotaka,antakotako,antakya,antalgi,antalafotsy,antalaha,antalaka,antalan,antalanga,antalat,antalatake,antalatakely,antalavia,antalaviana,antalbokor,antalepte,antalezy,antalfaimajor,antalfalutelep,antalfalva,antalfopuszta,antalge,antalgesiai,antalgesiu,antalhaza,antalhegy,antalia,antaliabo,antaliby,antaliepte,antalieptes,antalihy,antalilava,antalinombilahy,antalio ambany,antalio ambony,antalio-mahanitsy,antalisa,antalitoka,antalivtsi,antaliyah,antaliyepte,antalkiai,antalla,antallane,antallapos,antalmajor,antalo,antaloc,antalok-pataka,antalon,antalova pusta,antalovce,antalovtse,antalovtsi,antalovtsy,antalpuszta,antalszallas,antaltanya,antaly,antalya,antam,antam myo,antamachay,antamala,antamango,antamaray,antamarca,antamasa,antambaniharana,antambaniharano,antambao,antambato,antambek,antambiazina,antambiazona,antambohaivo,antambohitra,antamboho,antambohobe,antambohobesafatra,antamboholava,antamboholehibe,antambohomandroatra,antambohonambositra,antambohorano,antambokobe,antambolina,antambolo,antambomandroatra,antambonakana,antambono,antambony,antamborano,antambotsona,antamenaka,antamenary,antamiana,antamimarina,antamimbaribe,antamina,antammagudam,antamok,antamotamo,antampaka,antampet,antampolo,antamponala,antamponingodona,antan,antana,antanabe,antanafafy,antanafianambiry,antanafianambitry,antanai,antanaiso,antanakafe,antanalanana,antanamahalana,antanamahaolofotsy,antanamalaka,antanamalaza,antanamanaka,antanamanakana,antanamandririna,antanamandriry,antanamangotraka,antanamanitsy,antanamaresaka,antanamariazy,antanamarina,antanamary,antanamasaja,antanamasy,antanamay,antanamazava,antanambaiavy,antanambandriry,antanambao,antanambao ambany,antanambao ambodiara,antanambao atsimo,antanambao avaratra,antanambao firaisana,antanambao mahatsara,antanambao mangily,antanambao maronosy,antanambao samaka,antanambao sud,antanambao-andrangazaha,antanambao-anivorano,antanambao-anjahana,antanambao-bitavola,antanambao-ifasina,antanambao-manampotsy,antanambao-marosahona,antanambaobe,antanambaohely,antanambaokely,antanambaon amberina,antanambaonberonovo,antanambaonibotomorima,antanambazaha,antanambe,antanambehely,antanambehivavy,antanambesaraka,antanambiery,antanambo,antanamboay,antanambola,antanambonoina,antanamena,antanamiavotra,antanamienjika,antanamifafy,antanamifaly,antanamiketraka,antanamiketrara,antanamiketroma,antanamilihitsa,antanamisondro,antanamisondrotra,antanamitafy,antanamitarana atsimo,antanamitarana nord,antanamivomy,antanamivony,antanamoka,antananabe,antananabo,antananafata,antananafatra,antananahaolo,antananahaolobe,antananahivo,antananalava,antananalehibe,antananalo,antananamatso,antananambe,antananambo,antananan i vera,antananangata,antanananivo,antananarivo,antananarivokely,antananarivu,antananasoa,antananavo,antananbao,antanandava,antanandava avaratra,antanandava haut,antanandava ii,antanandava nord,antanandava sud,antanandavahely,antanandave,antanandavo,antanandehibe,antananero,antananifololahy,antananikora,antananitapy,antananitombo,antananiva,antananivo,antanankambano,antanankova,antanankovaka,antananolofotsy,antananolotsifina,antanantainana,antanantanana,antanantidai,antanantosa,antanantsara,antanantsarotra,antanantsimira,antanantsoa,antanapotsy,antanase,antanatanamba,antanatanamirafy,antanatanana,antanatsara,antanatsimanaja,antanava,antanavas,antanavony,antandatsy,antande,antandemoka,antandraja,antandroho,antandroka,antandrokaolo,antandrokombiteza,antandrokomby,antandroky,antandrotsy,antaneliba,antanelibe,antanery,antanetibe,antanetibe atsimo,antanetibe avaratra,antanetibe nord,antanetibe sud,antanetikely,antanetilava,antanetilavav,antanetilehibe,antanetimatavy,antanetimboahangy,antanetindriatsalama,antanetitelo,antanety,antang,antang xiang,antanga,antangbei,antangena,antangerina,antanggou,antangi,antangjieban,antangling,antangnao,antangping,antangshe,antangui,antangzui,antanhol,antani,antanian,antaniditra,antanietikely,antanifasika,antanifasina,antanifatsy,antanifolsy,antanifotsy,antanihady,antanihakaka,antanijoby,antanikatsaka,antanilaondapa,antanilatsaka,antanile,antanilehibe,antanilekely,antanilena,antaniloba,antaniloba atsimo,antaniloba avaratra,antaniloba nord,antaniloba sud,antanimahery,antanimaika,antanimainty,antanimainty avaratra,antanimalandry,antanimalandy,antanimalangy,antanimandry,antanimangotroky,antanimanimbo,antanimanimby,antanimanimo,antanimarina,antanimaro,antanimaroroka,antanimary,antanimary-tsianarena,antanimasajy,antanimasaka,antanimasaka-tsaramiafara,antanimaso,antanimavo,antanimbanbe,antanimbarbe,antanimbarbie,antanimbaribe,antanimbarilava andrefana,antanimbarilava atsinanana,antanimbarindraveriakanga,antanimbaritsara,antanimbarive,antanimbarlnkomandy,antanimbary,antanimbazaha,antanimboahangy,antanimboanjo,antanimbolina,antanimena,antanimenabaka,antanimenabe,antanimenakely,antanimenalava,antanimenamienjanembany,antanimenavondro,antanimeno,antanimiary,antanimiavotra,antanimidaina,antanimideno,antanimietry,antanimieva,antanimihery,antanimihetry,antanimiheva,antanimihobotsy,antanimiongana,antanimionjo,antanimisoroka,antanimivony,antanimondbabe,antanimora,antanimora atsinanana,antanimoro,antanimpo,antaninafaty,antaninandro,antaninarenina,antaninarivokely,antaninerenina,antanintandro,antanioitro,antanipaika,antanipako,antanira,antaniraraka,antanisaonjo,antanishkyay,antanisitrika,antaniskiai,antanisoa,antanitsara,antanitsihomehy,antanivaky,antanivany,antanivelatra,antanivelo,antanivelona,antanivory,antanjambolamena,antanjokambana,antanjokatafana,antanjomanga,antanjombolamena,antanjombolamena toby,antanjona,antanjonambo,antanjonanga,antanjonarivo,antanjondava,antanjonomby,antanlmena,antanmbe,antanondava,antanovo,antanping,antanta,antantangy,antantelinandriamamy,antantely,antanteraka,antantiloky,antantsihomehy,antanur,antany,antao,antaola,antaolandravina,antaolanomby,antap,antapaccha,antapacha,antapadolo,antapaka,antapakala,antapakiky,antapalca,antapalca hacienda,antapallpa,antapampa,antapandrano,antapani,antapani 2,antapani dua,antaparara,antaparco,antaparera,antapata,antapchong,antapgawang,antapiakely,antapirca,antapita,antapitec,antapli,antapoc,antapoca,antapucro,antapucro hacienda,antapunco,antapur,antapusinis,antaquira,antara,antara 1,antara 2,antara dua,antara satu,antarabory,antaradus,antarafoy,antarajra,antaram,antaramaharivo,antaramandahila,antaramandehila,antaramavony,antarambakana,antarambiby,antarambongo,antarameloka,antaramena,antaramerina,antaramibe,antaramihery,antaramut,antarana,antaranaka,antaranjaha,antarano,antaraos,antararatsy,antarasante,antarashat,antaratarana,antaratasy,antaratelo,antaratra,antaratsa,antaratse,antaratsy,antaravay,antaravy,antarazato,antarazo,antarba,antarballi,antarberak,antarco,antarda,antare,antarehimamy,antarena,antares,antaretra,antari,antaribao,antariem,antarikol,antarilava,antarimbazaha,antaripoi,antaritarika,antarivoko,antarkumiling,antarli,antaroby,antarolava,antaroma,antarraga,antarramej,antarramut,antarrashat,antartida argentina,antartinggi,antarut,antarvedi,antarvedidalem,antarvedipalem,antas,antasagua,antasales,antasava,antasavos,antasco,antasgo,antashallalle,antashavas,antasheva,antashey,antashi,antashih,antashikha,antashor,antaskoandrevangone,antassawamock,antassom,antastrebe,antasuma,antasura,antatabe,antataca,antataca hacienda,antatai,antatama,antatamo,antatamokely,antatar,antatatra,antatatsy,antatet,antatet-kalinga,antatika,antatilciai,antatileky,antatsy,antau,antaul,antaullo,antaura,antauta,antavadi,antavalo,antavatapaka,antavia,antaviandondona,antavibe,antavilia,antavilis,antavinomby,antavolo,antavolotaly,antavy,antavy atsimo,antavy nord,antavy sud,antawis,antawsai,antaya,antayacce,antayadi,antayakova,antayan,antayaque,antayge,antayla,antazalve,antazave,antazaves,antazo,antazoa,antazoho,antbacka,antbole,antbule,antby,antchi tchoulouma,antchoko,antdorf,ante,ante porta,antea,anteado,antebellum north,antebellum plateau,anteca,antechiro,antecume pata,antefa,antefokrom,anteg girang,anteg hilir,anteg-hilir,antegluonis,antegnate,anteguera,antehari,antehiroka,antehsu,antei santandrea,antek,antekoaka,antelyetmola,antela,antelas,antelat,antelevo,anteliban,antelibe,antella,antelo,antelomita,antelope,antelope acres,antelope center,antelope crossing,antelope development area,antelope hills,antelope mine,antelope park,antelope springs,antelope wells,antelopolo,antelopolotafo,antelsdorf,anteman,antemasa,antemitra,antemon,anten,antenal,antenan,antenatroy,antenbichl,antendorf,antendrorano,antenene,antenetibe,antenfeinhofe,anteng,antengo,antenibe,antenikely,antenilehibe,antenimaro,antenimbe,antenimbehala,antenina,anteninaomby,antenindahy,anteninde,anteninde-anjaha,antenindrano,antenior,antenitikely,antenloh,antenona,antenondahy,antenor navarro,antentezantany,anteny,anteny ouest,anteny sud,antenyevo,anteo,anteojitos,anteojo,anteojos,anteomal,antep,anteparaluceta,anteplevo,antequera,anteral,antere,anteren,anterene,anterivo,antermoia,antero,antero junction,anterrieux,antersberg,antersdorf,anterselva,anterselva bassa,anterselva di mezzo,anterselva mezzavalle,antersham,anterskofen,antes,antes fort,antesberg,antesevici,anteshiruhu,antesica,antesiea,antesory,anteta,antetesompafana,anteteza,antetezambahatra,antetezambaro,antetezambaro marotandrazana,antetezambato,antetezampandra,antetezampandrana,antetezampolaka,antetezana,antetezanambo,antetezandroto,antetezantany,antetezatony,antetezavato,antetikireja,anteu,anteuil,anteva,antevabe,antevamana,antevamanohy,antevamena,antevamena ambany,antevamena bas,antevamena haut,antevamena nord,antevamena sud,antevamina,antevana,antevana-mangoro,antevano-mangoro,antevanomanga,antevatapaka,anteviala,antevialabe,antevialaniseva,antevilava,antey-saint-andre,anteza,antezales,antezambola,antezana,antezana de alava,antezana de la ribera,antezant,antezeriai,antezheryay,antfai-major,antfeld,antgurgliai,antha arr,anthakak,anthara,anthe,antheap milepost,anthee,antheil-muckenhain,antheit,anthelupt,anthem,anthemous,anthenay,antheny,antheor,antheor-cap-roux,anthering,antheron,antheuil,antheuil-portes,anthia,anthia iteon,anthien,anthili,anthiro,anthiron,anthisnes,anthochori,anthochorion,anthofiton,anthofyto,anthofyton,anthok,anthokhori,anthokhorion,antholing,antholling,antholz-mitterthal,antholzen,anthon,anthonas,anthonies mill,anthony,anthony crossroads,anthony harbor,anthony hill,anthony subdivision,anthony wayne village,anthonys bank,anthonys kraal,anthonyville,anthorn,anthoston,anthotopos,anthoupolis,anthousa,anthracite,anthrakia,anthrakitis,anthras,anthraytat,anthredigudam,anthy,anti,anti mabalacat,anti rancheria,anti sok,antia,antiabe,antiakatraka,antiakoswaso,antian,antian ka pura,antianpo,antias,antibes,antibesskiy,antibia,antic,antica fonte di peio,anticala,anticasa,antichan,antichan-de-frontignes,antici,antico,antico di maiolo,anticoli corrado,anticura,anticuragua,anticuraguara,anticurahuara,antiehlieh,antiehliehchuang,antiemont,antierh,antierhlan,antiesenhofen,antietam,antietam farmettes,antietam heights,antietam manor,antietam overlook,antifica,antifyevo,antiga,antiga aldeia pacarandi,antiga colonia dourados,antiga colonia sao lourenco,antiga senzala,antiga serracao zamite,antigati,antigel,antiges,antigiwan,antignac,antignana,antignano,antignano d istria,antignano di livorno,antigny,antigny-la-ville,antigo,antigo aldeia do ignacio,antigo aldeia lania,antigo arraial de lavrinhas,antigo arraial de santa barbara,antigo buzi,antigo de arcos,antigo duque braganca,antigo pasto,antigo porto cunha bueno,antigo posto de mandimba,antigo posto do chissa,antigo posto do luia,antigo posto do mucari,antigo posto lumboma,antigo posto maziua,antigo posto quissenzel,antigo povo ambuila,antigo santa gertrudes,antigone,antigoneia,antigonish,antigonishe,antigos combatentes,antigoura,antigua,antigua carolina,antigua colonia crevaux,antigua colonia lerdo,antigua guatemala,antigua mision de miaria,antigua ocotepeque,antigua plaza de yucasapa,antigua santa fe,antigua saratoga,antigua uxpanapa,antigual,antiguala,antiguedad,antiguo,antiguo berrocal,antiguo caporalco,antiguo caserio la bonga,antiguo central de santa lucia,antiguo central santa lucia,antiguo cuscatlan,antiguo ingenio,antiguo ingenio averhoff,antiguo los indios,antiguo morelos,antiguo rancho,antiguo san fernando,antiguo tamuin,antiguo tucuman,antiguos mineros del norte,antiguran,antiguran villages,antiguyo,antihasi,antihon,antihorno,antihuala,antihuasi,antiipina,antijipina,antikin,antikkala,antikovo,antikovskiy,antil,antil plains,antila,antilankan,antilhue,antilik,antilindakana,antill ponds,antilla,antillanca,antillano,antillas,antillo,antillon,antilly,antilofotsy,antilokhovo,antilonga-mafaike,antilongo,antilotilo,antilpina,antiltmilopaka,antilyas,antim i,antimacheia,antimachia,antimakhia,antimano,antimari,antime,antimio de abajo,antimio de arriba,antimoa,antimonan,antimonio,antimony,antimova,antimovo,antimowo,antimua,antin,antintsiyem,antinaco,antinal,antinciems,antincurahua,antincurahuara,antindra,antine,antineni,anting,anting-anting,antingan,antingana,antinganovka,antingchen,antingchiao,antingham,antinghsien,antingkuchih,antingli,antingpu,antingtsun,antinha,antini,antinikay,antinilambo,antininkai,antinjan,antinkangas,antinlahti,antinovica,antinrova,antintsiems,antinyata,antio,antioa colonia sao lourenco,antioch,antioch acres,antioch estates,antioch fork,antioch grange,antioch harbor resort,antioch villa,antiochia ad cydnum,antiokheia,antiolava,antiopujre,antioquia,antipalo,antipampa,antipatris,antipava,antipaxos,antipayuta,antipenki,antipenko,antipernoi,antiphellus,antipichi,antipikha,antipina,antipinka,antipino,antipinskaya,antipinskiy,antipinskoye,antipirra,antipkino,antipkovo,antipolay,antipolo,antipolo old,antipova,antipovka,antipovo,antipovskaya,antipovskiy,antipuluan,antipyeva,antipyrgos,antipyrgus,antique,antiquity,antiragen,antiragin,antirhion,antiribe,antirigan,antirogo,antirt,antisana,antisanti,antisikurova,antisiradava,antisovo,antissa,antist,antit are,antit sok,antitica,antiuyo,antivari,antiville,antizam,antjak,antjal,antjarn,antjek,antjlahan,antjol,antjulai,antkalnishki,antkalnishyay,antkalnizhki,antkalne,antkalniskiai,antkalniskiai antrieji,antkalniskiai i,antkalniskiai ii,antkalniskiai pirmieji,antkopcio,antkoptis,antlangkirchen,antlauke,antleiciai,antler,antlers,antlers park,antmeschken,antnas,anto,antoa,antoakrom,antoakurom,antobam,antobayan,antobiakrom,antobiase,antobibe,antobimaromanga,antobinimadama,antobinimadamo,antobinimarovoay,antoboro,antobv,antoby,antoby nord,antoby sud,antoceni,antocenii,antocollo,antodika,antoe,antoekogae,antoenai,antoes,antoetra,antofagasta,antofagasta de la sierra,antofalla,antofiloaia,antofotona,antogineli,antogini,antognola,antogny,antogol,antoha,antohabato,antohaky,antohamaro,antohesti,antohestii,antohgo,antohidava,antoho,antohobe,antohodrano,antohomadinika,antohomaro,antoidava,antoigne,antoigny,antoine,antoinette,antoing,antoingt,antoinha,antojo allende,antojo do abajo,antok,antokalne,antokan,antokazo,antokhina,antokhino,antoki,antokila,antoko,antokobe,antokobory,antokol,antokolava,antokoli,antokomadinika,antokomaro,antokonalo,antokonosy,antokonosy manambondro,antokoraritelo,antokotelo,antokotoka,antokotoko,antol,antolanapela,antolantelo,antolepty,antoliepte,antolin,antolina,antolinombilahy,antolio-ambony,antolka,antolohomiady,antolon,antolong,antolonga,antolongo,antolovec,antolya,antomaima,antomaimachan,antomal,antombakatsa,antombana,antombodria,antombodriha,antombodriho,antombojia,antombokazo,antombondria,antombran,antomieszki,antomir,antomiras,antomokamo,antomonovo,anton,anton chico,anton diaz,anton flores,anton kovach,anton lizardo,anton martin,anton nasrani,anton north,anton recio,anton ruiz,anton sanchez,anton woods,antona,antonae,antonai,antonaiika,antonala,antonan del valle,antonana,antonanes del paramo,antonaves,antonazo,antonbodria,antonci,antondabe,antondidava,antondotsa,antondriky,antondrobe,antondrotra,antondrotsy,antonen,antonenki,antonenkov,antonesht,antoneshty,antonesti,antonetilava,antoneuca,antonevskaya,antoneys cay,antong,antong sar,antongambata,antongambato,antongan,antongo,antongoanaomby,antongobato,antongoborikely,antongobory,antongodahitra,antongodria,antongodriha,antongodrina,antongomaria,antongomavo,antongombato,antongomena,antongomena-bevary,antongomenabe,antongomenna,antongompatatra,antongonarere,antongondria,antongonibia,antongonivo,antongopahitra,antongron,antongy,antoni,antonia,antonia rosa,antonia santos,antoniamajor,antonianes,antoniao,antonias,antonibe,antonica,antonica brdo,antonica kuce,antonica mala,antonichi,antonici,antonico,antoniditra,antonidovo,antonie,antonielow,antoniew,antoniew sikawa,antoniew zdzarow,antoniewo,antonifui,antonigron,antonikha,antoniltranolava,antonimavo,antonimbaribe,antonimelingikulam,antonimelingilkulam,antonimina,antonin,antonin nowy,antonin stary,antonina,antonina do norte,antonindriana,antoninelyay,antonino,antoninopolis,antoninov,antoninovka,antoninovo udoli,antoninovskiy,antoninow,antoniny,antonio,antonio almeida,antonio alves,antonio amaral,antonio amaro,antonio arangueiro,antonio arcos,antonio bal,antonio barbosa,antonio bastos,antonio bastos e silva,antonio bermudez,antonio bernardo,antonio bezerra,antonio blanco,antonio borges,antonio braz,antonio bueno,antonio caetano,antonio candido da silva,antonio cano,antonio carboni,antonio carlos,antonio carmo,antonio cipriano,antonio correia,antonio da barra,antonio da luiz,antonio danieli,antonio de alencar,antonio de biedma,antonio de fernandez,antonio dias,antonio diogo,antonio do azeite,antonio domingues,antonio dos santos,antonio duarte,antonio eduardo,antonio elias,antonio elizalde,antonio enes,antonio engs,antonio ennes,antonio escobedo,antonio fabian,antonio fernandes,antonio ferraz,antonio ferreira,antonio filinto,antonio fonseca,antonio fortunato,antonio franco,antonio frotao,antonio gamba,antonio garcia,antonio gaxiola,antonio goncalves,antonio guiteras,antonio gusmao,antonio hernandez,antonio inacio,antonio joao,antonio jose holguin,antonio laurindo,antonio lemos,antonio leon,antonio lima,antonio lontrao,antonio lopes,antonio lopez mateos,antonio luis cortines,antonio luna,antonio maceo,antonio manuel,antonio maria,antonio mariano,antonio marques,antonio martins,antonio matos,antonio maya,antonio melo,antonio monjane,antonio n. silva,antonio narino,antonio nonato,antonio nouele,antonio olimpio,antonio olinto,antonio olyntho,antonio ortiz mena,antonio ourives,antonio pampa,antonio pardinho,antonio pereira,antonio perez ortega,antonio pinheiro,antonio pini,antonio pinto,antonio plaza,antonio porto jatai,antonio prado,antonio primo da silva,antonio quijarro,antonio r. laureles,antonio r. vela,antonio raimundo,antonio ramos,antonio recaurte,antonio ricaurte,antonio rocha,antonio rodrigues,antonio rodrigues de sousa,antonio rondon,antonio roque,antonio rosa,antonio rosales,antonio s. cunha,antonio salguero,antonio sampaio,antonio sanchez,antonio severino,antonio silva,antonio sotomayor,antonio spineli,antonio taborda,antonio telles,antonio tomas,antonio varga,antonio velho,antonio vicente,antonio vieira,antonio y aguilita,antonioco,antoniokofe,antoniotigtig,antoniow,antoniowka,antoniowka nowa,antoniowka stara,antonis,antonishki,antoniski,antonito,antoniuk,antoniusheim,antonivka,antonivtsi,antoniwald,antoniya,antonja,antonjowka,antonka,antonkovo,antonne-et-trigonant,antono,antono kovach,antono-shestakov,antonobitsika,antonokodintsevo,antonokodintsovo,antonombato,antonomovo,antononafatra,antonopol,antonopole,antonopolis,antonov,antonov sad,antonova,antonova buda,antonova gora,antonovca,antonovichi,antonovka,antonovka nomer shestnadtsatyy,antonovka north,antonovka pervaya,antonovka south,antonovo,antonovshchina,antonovshchyzna,antonovskaya,antonovski,antonovskiy,antonovskoye,antonovtse,antonovtsy,antonovy,antonovychi,antonowce,antonowen,antonowka,antonowka kocior,antonowo,antonoye,antonoyskaya,antonsape,antonsberg,antonsdorf,antonshohe,antonsimon,antonsthal,antontsevo,antonuv,antonuvka,antonuvka kotser,antony,antony-caye,antonyevka,antonyevskoye,antonys cay,antonyuki,antonzi,antoora,antopil,antopol,antopolboyarka,antopole,antopols,antoprak,antopusinyay,antopyevo,antor,antora,antoraka,antorbe,antore,antori,antoria,antorilava,antorotosy,antorpe,antorun,antos-tanya,antosari,antoshevo,antoshikha,antoshino,antoshka,antoshkin,antoshkina,antoshkino,antoshkovo,antoshonki,antosin,antosino,antosovice,antotana,antotinha,antotinhamezinho,antotohaza,antotohazo,antotoro,antotorona,antotyltse,antou,antou mane,antou zhen,antoua,antouchen,antouhuang,antouilleux,antoura,antoutun,antouzhen,antovo,antovontany,antovskiy,antowali,antoyo,antozerki,antpostos,antpura,antpuri,antra,antrachlias,antraigne,antraigue,antraigues,antraigues-sur-volane,antrain,antrain-sur-couesnon,antraitray,antrak,antrakatraka,antraki,antralina,antram,antran,antranaya,antranikowo,antranoambo,antranoenina,antranogoaika,antranohazo,antranokarany,antranokoaka,antranokoaky,antranokonka,antranomanara,antranomanevika,antranomay,antranomera,antranonatsana,antranondrahila,antranonkarany,antranontakatra,antranorantsana,antranosatra,antranotakatra,antranotany,antranotelo,antranovato,antraraka,antras,antratsit,antrea,antreaky,antrebika,antrehaky,antreky,antrema,antremahely,antrenas,antreville,antri,antri kalan,antrialgo,antriemsomae,antriemsomrae,antrim,antrim center,antrim district,antritrilava,antritt,antrobaka,antrobika,antroboka,antrodoco,antrokombo,antrolanda,antroliku,antronapiana,antronie,antronotakatra,antropikha,antropkovo,antropov,antropova,antropovas,antropovo,antropovskoye,antropschina,antropshino,antropyata,antropyevo,antrosano,antroshevskaya,antrukan,antrup,antsa,antsaba,antsabaoka,antsabe,antsaboaka,antsabotsy,antsadoky,antsaft,antsafy,antsaha-elahoa,antsahabe,antsahabe ii,antsahabe marovongo,antsahabehely,antsahabeminko,antsahabeorana,antsahaberaoka,antsahabiabiaka,antsahabo,antsahaborara,antsahadinta,antsahafaly,antsahafary,antsahafataka,antsahafilo,antsahafontsy,antsahafotsy,antsahaiava,antsahakaka,antsahakarany,antsahakely,antsahakilompaha,antsahalalina,antsahalana,antsahalava,antsahalavabe,antsahalavo,antsahamadinika,antsahamahamay,antsahamaina,antsahamalaza,antsahamaloto,antsahamamy,antsahamanara,antsahamaraloha,antsahamaroloha,antsahambaliha,antsahambary,antsahambavy,antsahambovo,antsahameloka,antsahamelokabe,antsahamena,antsahamenabe,antsahameva,antsahamileka,antsahamilemaka,antsahamivory,antsahamolaly,antsahamorengy,antsahampana,antsahampanana,antsahampano,antsahampemby,antsahampingitra,antsahanakomba,antsahanandriana,antsahanapela,antsahanarivo,antsahanavony,antsahandrany,antsahandravolaina,antsahandrazano,antsahandro,antsahaniakoho,antsahanifotsy,antsahanira,antsahankary,antsahanonoka,antsahanoro,antsahapetaka,antsahapolisy,antsaharaingy,antsaharavina,antsaharemolotra,antsahareva,antsahasoa,antsahasolila,antsahatanteraka,antsahatanteraka-manambolo,antsahatsaka,antsahatsiroa,antsahavalany,antsahavar,antsahavarabe,antsahavaribe,antsahavola,antsahavory,antsahe,antsahivo,antsahoana,antsahoano,antsahomena,antsahondra,antsahondramaromandry,antsahondramaromanory,antsahonjo,antsahopa,antsahorana,antsahotompy,antsahovy,antsaidoha-bebao,antsaka,antsakabalala,antsakabary,antsakabe,antsakadresy,antsakalava,antsakamahale,antsakamahasoa,antsakamalay,antsakamantsavana,antsakamitivala,antsakanala,antsakanalabe,antsakanampela,antsakanapela,antsakarivo,antsakaviro,antsakay,antsakay atsinanana,antsakdambalo,antsako,antsakoa,antsakoabe,antsakoabeony,antsakoabolo,antsakoafaly,antsakoafimafa,antsakoafolo,antsakoakely,antsakoalamoty,antsakoalava,antsakoalavateraka,antsakoamadinika,antsakoamahitsy,antsakoamahity,antsakoamalangy,antsakoamamy,antsakoamanambadilamoty,antsakoamanera,antsakoamanga,antsakoamanondro,antsakoamany,antsakoamaro,antsakoamary,antsakoamasy,antsakoambalo,antsakoambalo ambony,antsakoambe,antsakoambetota,antsakoamiary,antsakoamiedada,antsakoamieva,antsakoamilaika,antsakoamilaike,antsakoamileka,antsakoamirainty,antsakoamitondrotsy,antsakoamivanditra,antsakoana,antsakoanampela,antsakoandahy,antsakoandehy,antsakoandrevangone,antsakoandroa,antsakoanimanta,antsakoantsoa,antsakoapita,antsakoapito,antsakoasoa,antsakoatapaka,antsakoatelo,antsakoazato,antsakoazato-nanerena,antsakohakatra,antsakomboena,antsakomieva,antsakorota,antsakotsoa,antsalaka,antsalaka atsimo,antsalatsala,antsalaza,antsaloana,antsalonio,antsalova,antsalovabe,antsalovana,antsamaka,antsamakiho,antsamaky,antsamala,antsamalaha,antsamalaho,antsamalahy,antsamanara,antsamanaro,antsamangidy,antsambahara,antsambalahi,antsambalahy,antsambaraho,antsambavy,antsamby,antsaminjaza,antsampana,antsampanaomby,antsampandrana,antsampandrano,antsampanimahazo,antsampilay,antsana,antsanany,antsanatry,antsanavolo,antsandoka,antsandra,antsandrahana,antsandramanila,antsandrivery,antsanety,antsangabe,antsangabitika,antsangambato,antsangambatomara,antsangambitika,antsangandrano,antsangarano,antsangasanga,antsangorana,antsangy,antsanifera,antsanihira,antsaninkira,antsanira,antsanirena,antsanisiha,antsanompanota,antsantra,antsantrana,antsany,antsaonjo,antsaonjobe,antsapa,antsara,antsarahandrano,antsarahidy,antsarahitsaka,antsarahonenana,antsaramitsaka,antsarandrano,antsarangazo,antsararano,antsaratanimbary,antsaravibe,antsaravy,antsarazaza,antsaribe,antsariboa,antsaridava,antsariheza,antsarika,antsariky,antsarivary,antsarona,antsaronala,antsaronalabe,antsaronalahely,antsaronanala,antsarongaza,antsary,antsasaka,antsasavilahy,antsasavy,antsatra,antsatrabe,antsatrabevoa,antsatrabo,antsatraboka,antsatrabokaka,antsatrabonko,antsatraha,antsatrakely,antsatrakolo,antsatramalaza,antsatrambola,antsatramidola,antsatramilaika,antsatramina,antsatramira,antsatramoroy,antsatrana,antsatrananila,antsatrankely,antsatrano,antsatratokana,antsatsaka,antsatsamoroy,antsatsika,antsavahory,antsavango,antsaviny,antsavoa,antsavokabe,antsavy,antse,antsearavy,antsebo,antsecheng,antsefampiana,antsehsien,antsekurovo,antseky,antselibe,antselitoka,antselitokabe,antselitokakely,antselobe,antsely,antsen,antsenavolo,antseni,antsenira,antsenombilahy,antsensk,antsentsindrano,antsepako,antsepoka,antsera,antserana,antseranamaitso,antseranamatso,antseranambe,antseranambidy,antseranambondro,antseranana,antserananbondro,antseranandaka,antseranandakana,antserananjoky,antserananomby,antseranavato,antserasera,antseratsera,antsereza maromoka,antseta,antsetaka,antsetsindrano,antseva,antsevabe,antsevakely,antsevamikipika,antseza,antshishyay,antshventen,antsiadze,antsiafabositra,antsiafapiana,antsiakambony,antsialoa,antsiamalia,antsianaloka,antsiandrorana,antsiandrorano,antsianerena,antsianihy,antsianitia,antsiara,antsiasiaka,antsiatsiaka,antsiavango,antsiavangokely,antsienerena,antsienovana,antsiery,antsiety,antsifarovo,antsiferikha,antsiferova,antsiferovka,antsiferovo,antsiferovo ramenye,antsiferovskaya,antsiferovskiy bor,antsiferovskoye,antsifirovka,antsifitse,antsifitsy,antsiforovo,antsifrovo,antsihanabe,antsihara,antsiho,antsikafoka,antsikara,antsiketraka,antsikida,antsikirity,antsikory,antsikurova,antsilopy,antsimalaby,antsimalota,antsimangana,antsimanitra,antsimiasa,antsimovondro,antsinanambohitra,antsinanantsena,antsindra,antsindrana,antsindretra,antsinefo,antsingafy,antsingeha,antsingikalanoro,antsingilava,antsingilitoka,antsingilo,antsingilotoka,antsingilovalo,antsingilovolo,antsingily,antsingimihad,antsingimihalo,antsingirifiry,antsingitritzy,antsingy,antsinjorano,antsinjovary,antsintsindrano,antsiotabato,antsiotsio,antsiparandy,antsipary,antsiperovo,antsir,antsira,antsira avaratra,antsira nord,antsira sud,antsirabary,antsirabato,antsirabe,antsirabe afovoany,antsirabe avaratra,antsirabe nord,antsirabe-marirano,antsirabenisandrangato,antsirabolo,antsiradava,antsiradrano,antsirafaly,antsirahana,antsiraka,antsirakalanana,antsirakambo,antsirakaomby,antsirakiaky,antsirakivolo,antsirakohotra,antsirambany,antsiramehanana,antsiramihanana,antsirana,antsiranana,antsiranandavitra,antsiranantsara,antsiranaomby,antsirandava,antsirandrana,antsirandrano,antsirane,antsirangotina,antsirantany,antsirapary,antsirasira,antsirasira nord,antsirasira sud,antsirasira-ankalamboro,antsiratsira,antsiravaza,antsiravondrona,antsirebika,antsiribe,antsiridava,antsiridriatra,antsirika,antsiriloa,antsirimbato,antsiriri,antsiriribe,antsiriry,antsirony,antsirskaya dacha,antsirskoye,antsisavy,antsishki,antsishkyay,antsisikala,antsitabe,antsitipaha,antsitoly,antsivango,antsivavy,antsiviranina,antsivitsy riviere,antskog,antsla,antslafabositra,antso,antsoa,antsoa-ambohibory,antsoabe,antsoaloka,antsoamamy,antsoantany,antsoaravavy,antsoaravy,antsoavary,antsobery,antsobihy,antsofikaolo,antsofimbato,antsofitsoha,antsoha,antsoha ii,antsoha-morafeno,antsohalava,antsohamadiro,antsohamamy,antsoharano,antsohatoka,antsoheribe,antsoherikely,antsoherinakanga,antsohermakanga,antsohiabo,antsohianabo,antsohibe,antsohiby,antsohihi,antsohihibe,antsohihy,antsohimbondrona,antsohy,antsohy ambany,antsohy ambony,antsokaha,antsokaky,antsokaky atsimo,antsokaky avaratra,antsokaky sud,antsokay,antsokoalavotanana,antsolabato,antsolaka,antsolo,antsolola,antsomaina,antsomamy,antsomangana,antsomarify,antsombera,antsomika,antsomotsala,antsomotsoy,antsompanela,antsonandrano,antsondririna,antsondririny,antsondrobe,antsondrodava,antsondroka,antsonja,antsonjo,antsonoro,antsoraka,antsoriabe,antsoriala,antsoriamena,antsoriantsambo,antsoriatsambo,antsorilava,antsorindrabe,antsorindrana,antsorita,antsorokahitra,antsorokaka,antsorosoro,antsorotsoroka,antsory,antsosa,antsosamena,antsoso,antsotabato,antsotaha,antsotiala,antsotry,antsotsaha,antsotso avaratra,antsotso nord,antsotsun,antsoukou,antsovela,antsovelo,antsu,antsui,antsukh,antsum,antsumae,antsunijai,antsvenciai,antsverciai,antsverni,antsya-poyute,antsyferova,antsyferovo,antsyfirova,antsyfirovo,antsypery,antsypety,antsypolovka,antsyporovo,antsyr,antsysiai,antsyterovo,anttam,anttanggol,anttila,anttipuhto,anttis,anttisaari,anttola,anttonala,anttosenmaki,anttul,anttum,antu,antua,antuagan,antuan,antuane,antubia,antubishki,antubiskiai,antubyay,antuco,antucrun,antuerpia,antufyevo,antugnac,antuhsien,antuiya,antukhina,antukhovo,antul,antully,antuly,antum,antumai,antumane,antumano,antun,antun nasrani,antunai,antuncun,antune,antunes,antunes de baixo,antunez,antung,antung sa,antungchieh,antunghsien,antungli,antungliao,antungshih,antungtsun,antungwei,antuni,antunova,antunovac,antunovac tenjski,antunovatz,antunovici,antunowatz,antuo,antuozi,antupa,antupiai,antupitsa,antuppen,antupyciai,antur,antura,anturabi,anturke,anturmangan,anturo,antusai,antusheva,antusheva gora,antushevo,antushi,antushih,antushino,antushki,antushkovo,antushovo,antusta,antutot,antuura,antuyo,antuz,antuzede,antuzhan,antuzhi,antuzi,antuzowo,antvejai,antveyay,antveye,antvorskov,antvyay,antwa,antwe,antweiler,antwerefo,antwerp,antwerpen,antwi adjeikurom,antwi agyei kurom,antwiadjeikrom,antwort,antybary,antyeri,antykan,antyki,antypava,antyufeyevka,antyushevskaya,antyushino,antyushova,antze,antzen,antzieme,antziliai,antzinishni,antzu,antzuchi,antzuching,antzuhsien,antzuling,antzunei,antzushang,antzuwa,anu,anu basu,anu dap,anu settlement,anua,anua ebio,anuabo,anuachom a,anuachom b,anuachon,anuaho,anuahu,anuakhola,anual,anuan,anuang,anuanua,anuao,anuariachom,anuaripa,anub,anubas,anubeze,anubi,anubo,anucara,anuch,anuchenka,anuchinka,anuchino,anuchinsk,anuchinskiy,anucita,anueglee,anueli luawa,anufrieva,anufrino,anufriyerskiy,anufriyeva,anufriyevka,anufriyevo,anufriyevskaya,anufriyevskaya sloboda,anufriyevskiy,anugul,anuha,anuhadi,anui,anuidong,anuiri,anuisil,anuj,anukariri,anukhino,anukhtino,anukhua,anukhva,anukhva-armyanskoye,anukkana,anukkane,anukkangala,anukkanhena,anukkanpitiya,anukkanwila,anuku,anul,anula,anule,anulia,anulid,anulie,anuling,anuling segundo,anuling tercero,anulvsbacken,anulynas,anum,anum apapam,anum-asikuma,anuma,anumaba,anumakofe,anumakorpe,anumalkuduru,anumanandal,anumansa,anumanye,anumapapam,anumari,anumark,anumatu,anume,anumenchen,anumethigama,anumetigama,anumle,anumso,anun,anunanag,anunang,anunas,anunbul,anuncibay,anunderud,anundgard,anundsbole,anundsjo,anunga,anungan,anunggabam,anungom,anungon,anunmle,anunso,anunu,anunuso,anupampur,anupgarh,anupnagar,anuppur,anuppura,anupshaher,anupshahr,anupucara,anupul,anupulo,anupuraca,anur,anuradhapur,anuradhapura,anuradhapura town,anurag,anuragoda,anuro,anurovka,anurtutu,anury,anus,anusan,anuscat,anush ab,anush mahalleh,anush mahalleh-ye avval,anush mahalleh-ye dowvom,anushan,anushavan,anushikha,anushino,anushiravan,anushirvan,anushkovo,anusici,anusin,anusino,anuszewo,anuta,anutaiasi,anutani,anuthuk,anutiba,anutt,anuuka,anuuni,anuwe,anuyao,anuyskiy,anuyskoye,anuza,anuzabad,anuzai,anuzhey,anuzhyay,anuzi,anuziai,anuziu,anva,anva`-ye kalan,anvagen,anvah-ye kalan,anvahi,anvaing,anvamurat,anvangosi,anvar,anvar khan,anvar khan kheyl,anvarkhan,anvarkhankheyl,anveau,anveh,anvers,anversa,anversa degli abruzzi,anveuda,anveville,anveyo,anveyo siende,anviac,anviche,anvik,anviken,anvil,anvil points,anvil rock,anvilalatoby,anville,anvin,anviq,anvogh,anvonga,anwa,anwag,anwai,anwakki dhok,anwali,anwaliwala,anwalkhera,anwalting,anwan,anwanden,anwang,anwangcun,anwangi,anwannashe,anwar,anwar ali,anwar dhere,anwar khan,anwar khan khel,anwar khan koruna,anwar khaskheli,anwar khelanwala,anwar kili,anwar killi,anwar punjabi,anwar sial jo goth,anwar talao,anwarabad,anwaram,anwarama,anware,anwarewala,anwari,anwarkhankhel,anwarkhankheyl,anwarpur,anwarwala,anway,anwea,anweankwanta,anwei,anwelo kalunga,anwen,anwen zhen,anwenchen,anwheado,anwhiem,anwia,anwiado,anwiafutu,anwiam,anwian,anwiankwanta,anwianum,anwianwia,anwiasin,anwiaso,anwiasu,anwiawso,anwick,anwiem,anwienso,anwiesu,anwiun,anwo,anwoina,anwolli,anwollim,anwoma,anwomaso,anwondokol,anwondong,anwongol,anwonha,anwonni,anwor,anwoth,anwozi,anwu,anwucun,anwuinaso,anwuli,anwumaso,anwuna,anwutsun,anwuxiang,anwuzhai,anxaumont,anxi,anxi zhen,anxia,anxian,anxiang,anxiang chengguanzhen,anxiangou,anxiangoucun,anxiangshang,anxiangsi,anxiangtun,anxiaohaizi,anxiliujia,anxin,anxing,anxinguan,anxingzhen,anxtot,anxuzi,any,any-martin-rieux,anya,anya qala,anyaban,anyabin settlements,anyaboni,anyaboni aflamase,anyacsa,anyadan,anyadaw,anyadi,anyagwa,anyagwe,anyahoui,anyai,anyaiti,anyakadin,anyako,anyakovo,anyakrom,anyalai,anyam,anyama,anyama adjame,anyama ahouabo,anyama sossokoua,anyamam,anyaman,anyamasama,anyamhul,anyamile,anyan,anyana,anyandun,anyanfoutou,anyang,anyangalong,anyangchen,anyangcheng,anyangchildong,anyangchiltong,anyangdong,anyanggok,anyanggol,anyanggudong,anyangho,anyanghsien,anyangidong,anyangildong,anyangiltong,anyangji,anyangkou,anyangni,anyangodong,anyangpaldong,anyangpaltong,anyangsadong,anyangsamdong,anyangtun,anyangyukdong,anyangyuktong,anyanimatai,anyankasu,anyanou,anyanso,anyansu,anyanto,anyanui,anyanwi,anyanwua,anyapo,anyapur,anyar,anyar kaja,anyar kelod,anyar kidul,anyar selatan,anyar utara,anyara,anyaran,anyargede,anyarhwi,anyari,anyarro,anyarsari,anyarsari kangin,anyas,anyaseva,anyasevo,anyasovo,anyasu,anyat,anyatam,anyati,anyatiase,anyatiasi,anyave,anyavu,anyawcon,anyawkon,anyawuokrom,anyay,anyaypur,anyazu,anyazuywa,anyb,anyb-yu,anychino,anydro,anydron,anye,anyeagbade,anyeh,anyehtsun,anyei,anyek,anyeke,anyelimatega,anyen,anyenso,anyeom,anyer,anyer kidul,anyer-lor,anyerekoffikro,anyesu husa,anygyran,anyi,anyi jiedao,anyia,anyibonou,anyibou,anyid,anyidi,anyidinyid,anyieka,anyieso,anyigbademe,anyigbe tanyigbe,anyihsien,anyim,anyima,anyimaducho,anyiman,anyimanya,anyimon,anyin,anyinabasi,anyinabrim,anyinafunso,anyinam,anyinamso,anyinase,anyinasi,anyinasin,anyinaso,anyinasu,anyinasuso,anyindaung,anyinesu,anying,anyinga,anyingan,anyingbu,anyingcun,anyingtsun,anyinibani,anyinibuni,anyino,anyinofi,anyinoume,anyinyi,anyiough,anyirawase,anyirawasi,anyiribu,anykh,anykh-kishlag,anykhoba,anykhoba pervaya,anyksciai,anyksciu,anykshchay,anymam,anyo,anyogbe,anyogol,anyohsien,anyoka,anyoko,anyolli,anyom,anyon,anyong,anyongdong,anyonggol,anyonghwa,anyongmeori,anyongmiri,anyongmori,anyongo,anyongsimak,anyonni,anyor,anyori,anyornangka,anyos,anyosu,anyou,anyougoum,anyoum,anyoungan,anyoungom,anyowuokrom,anyox,anyro kope,anyron,anysberg,anyshtaikha,anysiv,anyska-arylagi,anyska-arylakh,anyska-aryylaakh,anysspruit,anyu,anyu xiang,anyuabin,anyuam,anyuan,anyuan xian,anyuanchen,anyuanhsien,anyuanhsun,anyuani,anyuanpao,anyuanpu,anyuantschon,anyuanxian niuquanshan linchang,anyuanyi,anyude,anyudin,anyue,anyueh,anyuehhsien,anyuem,anyugbe,anyukawkaw,anyun,anyuny,anyuol,anyuom,anyur,anyusha,anyusinae,anyutin,anyutina,anyutino,anyutkino,anyuy,anyuysk,anywali,anz,anza,anza el-qalala,anzab,anzab-e bala,anzab-e pain,anzaboa,anzabowa,anzac,anzad,anzaf,anzagar,anzagh,anzahar,anzailin,anzaiya,anzak,anzakwara,anzal,anzaldo,anzaldua,anzalduas,anzale,anzama,anzamala,anzammer,anzan,anzandougou,anzangan,anzani,anzanigo,anzanling,anzano,anzano del parco,anzano di puglia,anzar,anzar daji,anzar koruna,anzar-e jadid,anzarane,anzarh,anzari kandarai,anzarkan,anzas,anzat,anzat-le-luguet,anzav,anzawa,anzberg,anze,anze assahoun,anze fuchengzhen,anze xian,anzeb,anzeba,anzefahr,anzegarn,anzegem,anzeglouf,anzeh-ha,anzeha,anzekyul,anzekyula,anzel,anzele,anzeli,anzeling,anzem,anzeme,anzen,anzenau,anzenbach,anzenberg,anzencho,anzendorf,anzenge,anzenhof,anzenhofen,anzenkirchen,anzenreuth,anzer,anzere,anzerg,anzerrag,anzerreut,anzerreuth,anzerskaya,anzeu,anzex,anzfelden,anzgarn,anzha,anzhai,anzhang,anzhaniyemi,anzhausen,anzhaveh,anzhen,anzherka,anzhero-sudzhenka,anzhero-sudzhensk,anzhero-sujensk,anzhevka,anzhi,anzhigort,anzhirou-bolo,anzhofen,anzholiya,anzhongli,anzhou,anzhu,anzhuang,anzhuangpo,anzhuangzi,anzhul,anzhulong,anzhuwu,anzi,anzi jiaoxia,anzian,anziao,anziba,anzibao,anzibu,anzichong,anzici,anzig,anzige,anzigou,anziji,anzijing,anzikou,anzili,anziliai,anziliang,anziling,anzimen,anzin,anzin-saint-aubin,anzinao,anzing,anzino,anzio,anzio kalay,anziping,anzipo,anziqian,anzir,anzire,anzishan,anzishang,anzishao,anzitang,anzito,anziwa,anziwai,anziwan,anziwei,anzixi,anzixia,anziyan,anziying,anzi\302\247awr,anzo,anzoategui,anzob,anzogobe,anzola,anzola demilia,anzola dossola,anzola dellemilia,anzoli,anzolla,anzolu,anzonico,anzorandia,anzou,anzougar,anzouggar,anzouka,anzour,anzov,anzt,anzu,anzubisto,anzucha,anzudian,anzuelo,anzughul,anzui,anzuku,anzuli,anzulon,anzum,anzuola,anzwa,anzy,anzy-le-duc,anzyak,anzybey,ao,ao bakhsh,ao baksh,ao barik,ao buc,ao co,ao kenh,ao khorak,ao khorak-i-bala,ao loan,ao loek,ao loek tai,ao louan,ao luek,ao luk,ao masar,ao mazar,ao noi,ao sisiat,ao tan,ao ve,ao yai,ao yang,ao yuan,ao-a,ao-men,aoa,aoan,aoang,aoaniiru,aoanta,aoao,aoarawa,aoba,aobabnuruddin,aobaira,aobaoliang,aobaowobao,aobar,aobe,aobegon,aobei,aobei zhengjia,aobeicun,aobeidong,aobeihou,aobeiling,aobeilong,aobeiwo,aobeiwu,aobingfang,aobuaolie,aochagalimi,aocheng,aochi,aochiachang,aochiang,aochiao,aochiapaotzu,aochiapu,aochiatan,aochiawan,aochih,aochuanhsu,aochug,aocun,aocuo,aodar,aodede,aodha,aodhata,aodi,aodianzi,aodichen,aoding,aodiyang,aodocho,aodong,aodu,aoduo,aoem,aoer,aoera,aoergading,aoeroe,aoeta,aoewob,aofaloti,aofan,aogading,aogan,aogani,aogen,aogeta,aoghani,aogi,aoglat ed djahmane,aoglat ej jahmane,aoglat el bouaider,aogu,aoguan,aohama,aohan qi,aohanchi,aohanga,aohantun,aohanwobao,aohezui,aohofou,aohou,aohsi,aohsia,aohu,aohua,aohuan,aohuanzhen,aohuatsun,aohuqi,aoicho,aoitokolako,aoiz,aoji,aojia,aojiabao,aojiabaozi,aojiabian,aojiachang,aojiading,aojiafang,aojiang,aojiang zhen,aojiangzhen,aojiao,aojiapeng,aojiapu,aojiapu xiang,aojiatan,aojiawan,aojiawobao,aojiazhai,aojiazui,aojidong,aojipo,aojiri,aok,aokal,aokalangan,aokambida,aokas,aokata,aokautere,aokeng,aokhorak bala,aokhorak pain,aoki,aoki-gemi,aoki-kemi,aokijima,aokijimamachi-aokijima,aokokro,aokota,aokou,aokoucun,aokouling,aokoushang,aokoutou,aokoutsun,aoku,aokuan,aokutsun,aokyor,aola,aola gujilan,aolahat,aolaimi,aolakhair,aolakhayer,aolat,aolatail,aole,aolechaochi,aoleka,aoles,aolezhaoqi zhen,aoli,aoliapur,aolichao,aolihejia,aolihsu,aolikeng,aolikro,aolimiao,aoling,aolingchong,aolingxia,aoliwang,aoliwangjia,aoliwangtsun,aolixu,aoliyingzi,aolizhao,aoloau,aoloaufou,aoluguya,aolunhuduge,aom,aoma,aoma nejema,aomar,aomar ben chiheb,aomar nejma,aomen,aomi,aomian,aomori-shi,aomukou,aon,aona,aonae,aonai,aonao,aonaozi,aonbaw,aonbuga,aonde,aondo,aondo-ana,aondoakaa,aondokpa,aondou,aondu,aone,aonediou,aonedis,aongatete,aongola,aongouazo,aoni,aonia,aoniman,aoniu,aoniubaozi,aoniuwopu,aonla,aonla kila,aono,aonobuaba,aonobuaka,aonohara,aonsar,aontendra,aonzar,aoos senekal,aop,aopaotu,aopasi,aopenling,aoping,aopingli,aopingshan,aopo,aoqian,aoqiao,aoqiao xiang,aoqiu,aoqlat ej jahamane,aoqlat ej jahmane,aoqlat el bouaider,aoqlat el koutchane,aoquanxu,aoquhu,aora,aorabania,aoraji,aoral,aoralo,aorane,aorawala,aordane,aore,aoreama,aoreora,aorere,aorgen,aori,aorich,aorion,aorlos,aorobo,aoroka,aorola,aorolo,aorroyomolinos,aoryi,aos,aosane,aosanpur,aose,aoshan,aoshan xiang,aoshanchieh,aoshang,aoshanggong,aoshangling,aoshangshengjia,aoshangtang,aoshangwan,aoshangwang,aoshanshang,aoshanwei,aoshanzhai,aoshenshui,aoshera,aoshi,aoshi xiang,aoshi zhen,aoshih,aoshiha,aoshihtsun,aoshima,aoshui,aosia,aosiniu,aosiyingzi,aoslos,aossi lossankro,aosta,aoste,aosu,aosyrkhva,aota,aotang,aotangbei,aotangjin,aotara,aotarc,aotare,aotea,aoti,aotian,aotianjiao,aotianshang,aotichen,aotiyang,aoto,aotocho,aotokro,aotomo,aotou,aotou jiedao,aotou zhen,aotoubei,aotouchiang,aotouhsu,aotoujie,aotoujing,aotouling,aotouping,aotoupu,aotoutsun,aotouyujia,aotsai,aotuhia,aotumu,aotungcheng,aotzutsui,aou,aou losso,aou-lasso,aoua,aouabo,aouacha,aouada,aouadi,aouaenou,aouagome,aouakame,aouakamissi,aouaki,aouakro,aoualine,aoualous,aouama,aouamara,aouamie,aouan,aouan-komoenou,aouana,aouanienou,aouanou,aouara,aouarai,aouarga,aouaro,aouarom,aouasa,aouassa,aouataba,aouati,aouazra,aouazroute,aoube,aoubellil,aouchana,aouchar boudjak,aouchar boudjaq,aouchar boujak,aouchariye,aouchikoua,aouchtam,aouchtame,aoud ali,aoud izzane,aoudach n ait lah,aoudacht,aoudain,aoudaine,aoudal,aoudalou,aoudane,aouddine,aoude,aouderas,aoudi,aoudiai yoro ali,aoudid,aoudim,aouding,aoudja,aoudjali,aoudjou,aoudlkelt,aoudomo,aoudorom,aoudou tounga,aoudsja,aoue,aoue-broukro,aoue-kansi,aoue-ziziessou,aouega,aoueijel,aoueinate,aoueinik,aouela,aouera,aouf,aoufelgach,aoufour,aoufous,aoufouss,aouftout,aouftout nizder,aouga,aouggoud,aouggoute,aoughir,aougin,aouglit,aougni,aougnit,aougny,aougounz,aougrout,aouhaya,aouiangon,aouicodji,aouid ellel,aouieta,aouifat,aouifate,aouin rado,aouina,aouinat,aouine el kseb,aouinet,aouinet abbou,aouinet ait oussa,aouinet el merhraoui,aouinet el mghaouine,aouinet el mora,aouinet el mrhaouine,aouinet moha,aouinet n ait oussa,aouinet torkoz,aouinet toumaha,aouinet-ed-dieb,aouinet-el-dieb,aouinia,aouinti,aouizakht,aouja,aoujeft,aoujgal nait tachaout,aoujou,aoukansara,aoukerda,aoukert,aoukerte,aoukh,aoukhane,aouklit,aouknit,aoukoukope,aoukoukro,aoukro,aouksir,aoula kouara,aoula kwara,aoulad badis,aoulad mansour,aoulakora,aoulaol,aoulazou,aoulef,aoulef arab,aoulef cheurf,aoulef cheurfa,aoulef ech cheurfa,aoulef el arab,aoulef el arab bordj,aoulene,aoulet cheurfa,aouli,aouli kwara,aouli-koara,aoulia,aoullous,aoullouz,aoulo,aoulondo kwara,aoulondokoara,aouloua,aouloumout,aoulouz,aouma-kouamikro,aoumara,aoumarat,aoumou,aouna,aoundaye,aoundio,aoundji,aoungal,aoungete,aouni,aouniafoutou,aouniakame,aounianou,aounienfoutou,aouniser,aouno,aounokro,aounou,aounoud,aounout,aounsguerte,aounyanfoutou,aounyassou,aounye,aounze,aounzi,aouobo,aouokeda,aououdid,aouoye,aoupie,aoura,aourado,aouragouene,aourai,aourar,aourarhene,aoure,aoure baoude,aoure dan kaka,aourema,aourga,aourhir,aouri,aouria,aourikro,aourikt,aourir,aourir akbour,aourir n ait nasser,aourir nirg,aourir ou eulmi,aourir ouelmi,aouriz,aouro,aourokro,aouron,aourossua,aourou,aouru,aourz,aousdja,aousebt,aousift,aousir,aousla,aousoude,aousroukht,aoussa,aoussakomoekrou,aoussanke,aoussankro,aoussat jeraba,aousseguedelt,aoussi kouachi,aoussift,aoussir,aoussoubroukro,aoussoukro,aoussrighte,aouste,aouste-sur-sye,aouta,aoute kondji,aoutele,aoutili,aoutlaouk,aoutoue,aoutouss,aouya,aouyakrou,aouyalo,aouyanfoutou,aouzai,aouzankro,aouzar,aouzart,aouze,aouzel,aouzer,aouzeroual,aouzeroualt,aouzeroult,aouzert,aouzlida,aouzourou,aouzu,aouzzai,aove,aovere,aovot,aowai,aowang,aowanso,aowarabad,aowei,aoxi,aoxi zhen,aoxia,aoxia dadui,aoxiali,aoxiaping,aoya,aoyagi,aoyagicho,aoyama-minami,aoyama-minamicho,aoyanagicho,aoyang,aoyang jiedao,aoyang zhen,aoyao,aoyu,aoyuan,aoyutsui,aoyuzhou,aozai,aozaijiatai,aozaraza,aozhai,aozhi,aozhong,aozhuangchen,aozhwan,aozi,aozibei,aozigou,aozihu,aozili,aozitou,aozixia,aozo,aozou,aozu,aozui,ap,ap a dua,ap an binh,ap an co,ap an dien,ap an dinh,ap an dong,ap an dung,ap an hau,ap an hiep,ap an hoa,ap an hoa thuong,ap an hoi,ap an hung,ap an khanh ha,ap an khuong,ap an kroet,ap an lac,ap an lai,ap an lam,ap an lao,ap an le,ap an lo,ap an loc,ap an loc a,ap an loi,ap an loi dong,ap an luong,ap an minh,ap an my,ap an nhon,ap an ninh,ap an nong,ap an phu,ap an phuoc,ap an qui,ap an quoi,ap an quy,ap an thai,ap an thanh,ap an thien,ap an tho18,ap an thoi,ap an thuan,ap an thuan thuong,ap an thuy tay,ap an tin,ap an tinh,ap an tinh ba,ap an trach,ap an trach dong,ap an tri,ap an vien,ap an vieng,ap an xuan,ap ang trang so,ap au tho,ap ba,ap ba ai,ap ba ai ii,ap ba bang,ap ba beo,ap ba chang,ap ba chuc,ap ba ghe,ap ba lan,ap ba lap,ap ba mao,ap ba my,ap ba nam,ap ba nghe,ap ba nhan,ap ba nho,ap ba quan,ap ba rinh,ap ba ta,ap ba the,ap ba tien,ap ba tieu,ap ba trum,ap ba tu,ap ba vinh,ap bac,ap bac chan,ap bac dong,ap bac lan,ap bac thanh,ap bach thanh,ap bai bang,ap bai bon,ap bai ghe,ap bai yen,ap bao,ap bao binh,ap bao cha,ap bao chanh,ap bao cong,ap bao cua,ap bao dinh,ap bao dung,ap bao go,ap bao nau,ap bao sau,ap bao sen,ap bao siem,ap bao thuan,ap bao trai,ap bao tram,ap bau,ap bau bang,ap bau beo,ap bau bien,ap bau ca,ap bau ca mot,ap bau cam,ap bau co,ap bau dieu,ap bau dung,ap bau long,ap bau lung,ap bau me,ap bau nau,ap bau rong,ap bau sen,ap bau tram,ap bau vung,ap bay,ap bay thua,ap be moi,ap bec hen lon,ap ben cam,ap ben cau,ap ben cay,ap ben cho,ap ben chua,ap ben da,ap ben dinh,ap ben do,ap ben do ao,ap ben doi,ap ben dong so,ap ben go,ap ben ke,ap ben ma,ap ben muong,ap ben rong,ap ben san,ap ben thang,ap ben the,ap ben tram,ap ben tranh,ap ben xoai,ap bia,ap bich son,ap bie18n,ap bien,ap bin chanh,ap binh,ap binh an,ap binh ba,ap binh chanh,ap binh chau,ap binh dien,ap binh dinh,ap binh dong,ap binh dong ha,ap binh duc,ap binh duong,ap binh giao,ap binh hiep,ap binh hoa,ap binh hoi,ap binh hung,ap binh huu,ap binh ke,ap binh khanh,ap binh khuong,ap binh lac,ap binh lam,ap binh lan,ap binh loc,ap binh loi,ap binh long,ap binh luong,ap binh luong dong,ap binh ly,ap binh nhi,ap binh ninh,ap binh pho,ap binh phu,ap binh phuc dong,ap binh phung,ap binh phuoc,ap binh quang,ap binh qui,ap binh quon,ap binh quy,ap binh son,ap binh sung,ap binh ta,ap binh tan,ap binh than,ap binh thanh,ap binh thanh trung,ap binh thi,ap binh thien,ap binh tho,ap binh thoi,ap binh thuan,ap binh thuong,ap binh thuy,ap binh tien,ap binh tri,ap binh trung,ap binh truong tay,ap binh tu,ap binh xuan,ap binh yen,ap binh-an xuan,ap binh-hoa a va b,ap bo,ap bo cang,ap bo la,ap bo lon,ap bo ray,ap boi,ap boi loi,ap bon,ap bon bsop,ap bong gieng,ap bong trang,ap both ko ky,ap boum,ap brahon,ap bu karr,ap bu nho,ap bu thanh,ap bung binh,ap bung cau,ap bung chong,ap bung lon,ap bung rieng,ap bung thom,ap bung tung,ap buo18i can,ap ca cac,ap ca co,ap ca coi,ap ca goc,ap ca hang,ap ca hoan,ap ca lanh tamung,ap ca loc,ap ca na,ap ca nga,ap ca nhen tren,ap ca nhung,ap ca pho,ap ca quan,ap ca rung,ap ca tac,ap ca vang,ap cai chuoi,ap cai cuu,ap cai day,ap cai dia,ap cai duoc nho,ap cai gia,ap cai giay,ap cai gieng,ap cai giua,ap cai goc,ap cai keo,ap cai mon,ap cai ngan,ap cai nhum,ap cai qua,ap cai ran,ap cai rang,ap cai ro,ap cai sach,ap cai su,ap cai tac,ap cai tram,ap cai trau,ap cai vang,ap cam son,ap can de,ap can lo,ap can thanh,ap can thoi,ap cang nom,ap canh den,ap canle,ap cao bang,ap cao lanh,ap cao xa,ap cau ba,ap cau cay,ap cau giai,ap cau khoi,ap cau ngan,ap cau quang,ap cau sang,ap cau sen,ap cau thang,ap cau xay,ap cay cam,ap cay cham,ap cay day,ap cay duong,ap cay gia,ap cay gian,ap cay gua,ap cay sop,ap cay xang,ap cha do,ap cha va,ap cham,ap chang be,ap chang hai,ap chanh,ap chanh hung,ap chanh tuong,ap chau ma,ap chau thanh,ap chau thoi,ap chi,ap chin,ap chinh an,ap chinh hoa,ap cho,ap cho ben,ap cho dinh,ap cho kinh cung,ap choi moi,ap chom ca,ap chom chui,ap chon thanh,ap chong giang,ap chong no,ap chong su,ap chong tap,ap chot,ap chua ba leng,ap chua hoa,ap chuoi nuoc,ap co,ap co ba,ap co sa,ap co sang,ap co tuat,ap co xuoc,ap co18 bi,ap co18 co,ap co18 thap,ap co18 trach,ap co18 xuan,ap con cu,ap cong ca,ap cong dien,ap cong nghiep,ap cu b,ap cu la,ap cu lao,ap cu lao tre,ap da bac,ap da chong,ap da loc,ap da loi,ap da mai,ap da phuoc,ap da thanh,ap da thien,ap dai an,ap dai bai,ap dai la,ap dai mong,ap dai nga,ap dai tai,ap dai thanh,ap daigne,ap dam chit,ap dam thu,ap dam tra cu,ap dan kia,ap dang gia dit,ap dang lam,ap dangia,ap dao,ap dao du,ap dao vien,ap dap truong,ap dat chay,ap dat do,ap dau chich,ap dau giong,ap dau la,ap day ba,ap day oan,ap den,ap dien hoi,ap dien tang,ap dien thay hoi,ap dien truong,ap dieu hoa,ap dinh,ap dinh an,ap dinh ba,ap dinh hoa,ap dinh phu,ap dinh tan,ap dinh thoi,ap do hoa,ap doan van,ap don ganh,ap dong,ap dong an,ap dong binh,ap dong binh trach,ap dong cao,ap dong chinh,ap dong co ki,ap dong da,ap dong do,ap dong gi,ap dong hiep,ap dong ho,ap dong hoa,ap dong hung,ap dong lam,ap dong lan,ap dong loc,ap dong loi,ap dong lon,ap dong long,ap dong mieu i,ap dong my,ap dong nguon,ap dong nhi,ap dong nhon,ap dong nhut,ap dong ninh,ap dong phat,ap dong phu,ap dong phuoc,ap dong qui,ap dong sac,ap dong sao,ap dong son,ap dong tau,ap dong thanh,ap dong thanh thuong,ap dong thoi,ap dong xoai,ap dong xuan,ap dua,ap duc hoa,ap duc hot,ap duc my,ap duc vinh,ap duog tac,ap duong dap,ap duong hoa,ap duong long,ap duong phu,ap duong ranh,ap ganh gia,ap gia bac,ap gia binh,ap gia hoi,ap gia huynh,ap gia khanh,ap gia kiet,ap gia lang,ap gia loc,ap gia thanh,ap gia tuoch,ap gia vien,ap giai lang,ap giao dien,ap giao du,ap giao hoa,ap giao thanh,ap giao thoi,ap giap minh,ap giay heo,ap gieng moi,ap gio ta,ap giong,ap giong cam,ap giong cat,ap giong co,ap giong cui,ap giong da,ap giong dai,ap giong de,ap giong dua,ap giong gan,ap giong gia,ap giong gieng,ap giong ke,ap giong me,ap giong moi,ap giong ta on,ap giong tram,ap giong trom,ap giua,ap go,ap go bau,ap go cao,ap go dat,ap go dau,ap go dinh,ap go dua,ap go gon,ap go muong,ap go ngai,ap go sao,ap go vo,ap goho,ap gu,ap ha,ap ha bao,ap ha cang,ap ha do,ap ha lac,ap ha thuy,ap hac hop,ap hai,ap hai nhuan,ap hai tan,ap ham da,ap hanh lam,ap hanh my,ap hau hoa,ap hau phu,ap hau quach,ap hau qui,ap hau thanh,ap hau trinh,ap hiale,ap hien an,ap hien luong,ap hien si,ap hiep an,ap hiep duc,ap hiep hoa,ap hiep hung,ap hiep thuan,ap hiep trung,ap ho da,ap ho phong,ap ho set,ap ho tren,ap hoa,ap hoa an,ap hoa binh,ap hoa cu,ap hoa cuong,ap hoa dien,ap hoa dinh,ap hoa dong,ap hoa duc,ap hoa hao,ap hoa hiep,ap hoa hung,ap hoa khanh,ap hoa khuong,ap hoa lac,ap hoa loc,ap hoa loi,ap hoa long,ap hoa luong,ap hoa nam,ap hoa phau,ap hoa pho,ap hoa phu,ap hoa phuoc,ap hoa qui,ap hoa quy,ap hoa tach,ap hoa tam b,ap hoa tan,ap hoa tay,ap hoa thanh,ap hoa tho,ap hoa thuan,ap hoa thuong,ap hoa trung,ap hoan quang,ap hoanh tau,ap hoi ngai,ap hoi ngh,ap hoi phuoc,ap hoi tam,ap hoi than,ap hoi thanh,ap hoi tho,ap hon heo,ap hon soc,ap hung,ap hung an,ap hung binh,ap hung de,ap hung hieu,ap hung hoa,ap hung hoa tay,ap hung le,ap hung loc,ap hung loi,ap hung loi tay,ap hung long,ap hung my dong,ap hung my tay,ap hung nghia,ap hung nhon,ap hung nhon dong,ap hung nhuong,ap hung pe,ap hung phat,ap hung phu,ap hung qui,ap hung thanh,ap hung thanh tay,ap hung thoi,ap hung tin,ap hung tri,ap hung van,ap hung yen,ap huong ho,ap huong hoa,ap huong lac,ap huong phuoc,ap huong sa,ap huong thanh,ap huu an,ap huu loc,ap huu thanh,ap kalali,ap kalili,ap katall,ap kau kiet,ap kau tiet,ap ke mot,ap ke my,ap kha chui,ap kha phu tay,ap khanh an,ap khanh binh,ap khanh hau,ap khanh hoa,ap khanh hoi,ap khanh hung,ap khanh loi,ap khanh long,ap khanh my,ap khanh nghia,ap khanh nhon,ap khanh phuoc,ap khanh thien,ap khanh tuong,ap khau cu,ap khe dol,ap khuc cung,ap khuc treo,ap khuong hoa,ap khuong pho,ap kien hoa,ap kien hung,ap kien long,ap kien qui,ap kien thuan,ap kim cau,ap kim lien,ap kim thach,ap kim xia,ap kinh bac dong,ap kinh bay,ap kinh cung,ap kinh ngang,ap kinh tam,ap kinh xang,ap klong jum,ap kouss,ap kroet,ap la ban,ap la ghi,ap la ma,ap la son,ap la vien,ap lac binh,ap lac hoa,ap lac lam,ap lac ninh,ap lac tr,ap lac vien,ap lac xuan,ap lai bang,ap lai ha,ap lai khe,ap lai minh,ap lai nuong,ap lai thanh,ap lai xa ha,ap lam duong,ap lam hau,ap lam hoa,ap lam lac,ap lam loc,ap lam phu,ap lam ro,ap lam thien,ap lam tuong,ap lam tuyen,ap lam xuan,ap lan,ap lan nhi,ap lan tay,ap lan thanh,ap lang,ap lang ba,ap lang giai,ap lang nay,ap lang sen,ap lang trau,ap lang tre,ap lang tron,ap lanh thuy,ap lao mon,ap lap dien,ap lat,ap len buoi,ap lien dam,ap lo gach,ap lo gom,ap lo ho,ap lo o,ap lo voi,ap loc bich,ap loc binh,ap loc chanh,ap loc khe,ap loc lam,ap loc thanh,ap loc tien,ap loc trac,ap loco,ap loi,ap loi an,ap loi du,ap loi my,ap loi nhon,ap loi thuan,ap loi tuong,ap long an,ap long be,ap long binh,ap long cat,ap long chau,ap long chu,ap long dai,ap long dien,ap long dinh,ap long ha,ap long hai,ap long hiep,ap long ho ha,ap long hoa,ap long hoi,ap long hung,ap long huynh,ap long khanh,ap long khe,ap long khot,ap long kien i,ap long lam,ap long lien,ap long linh,ap long muc,ap long my,ap long ninh,ap long phi,ap long pho,ap long phu,ap long phung,ap long phuoc,ap long que,ap long qui,ap long qui thuong,ap long son,ap long thai,ap long thanh,ap long the,ap long tho,ap long thoi,ap long thuan,ap long tr,ap long truong,ap long tuong,ap long vinh,ap long xuan,ap long xuyen,ap long yen,ap lu ban,ap luc,ap lung lon,ap lung sen,ap luoc ma,ap luong binh,ap luong hoa,ap luong loi,ap luong mai,ap luong son,ap luong tay,ap luu cu,ap mrang,ap ma lam cham,ap mac day,ap mai,ap mai giam,ap mai trung,ap man ling,ap mang ca,ap mien ba,ap mieu,ap mieu ba,ap mieu dien,ap mieu dieu,ap minh huong,ap minh nghia,ap mo cong,ap mo tin,ap moi,ap mot,ap mui con,ap mui tram,ap mung khai,ap muoi,ap muong dieu,ap muong khai,ap my,ap my an,ap my binh,ap my bon,ap my chanh,ap my da,ap my dien,ap my dinh,ap my dong,ap my duc,ap my duoc,ap my hai,ap my hanh,ap my hiep,ap my hoa,ap my hoi,ap my hung,ap my kiem,ap my loc,ap my loi,ap my loi mot,ap my long,ap my luoc,ap my nghia,ap my noi,ap my phu,ap my phu nam,ap my phuoc,ap my qui,ap my quo,ap my quy,ap my tay,ap my th,ap my than,ap my thanh,ap my tho,ap my thoi,ap my thuan,ap my thuong,ap my trach,ap my trinh,ap my trung,ap my truong,ap my tuong,ap my vinh"ap my xa,ap my xuan,ap my xuyen,ap nhouar,ap nam,ap nam hue,ap nam ly,ap nam ninh,ap nam o,ap nam trai,ap nam truong hue,ap nan chau,ap nang chang,ap nang son,ap nga bac,ap nga cay,ap nga dai,ap nga huu,ap nga ta,ap nga tu,ap ngai giao,ap ngai huu,ap ngai loc,ap ngai nhi,ap ngai nhin,ap ngai phu,ap ngai thuan,ap ngan dien,ap ngan ro,ap ngoai lo da,ap ngoc an,ap ngoc bich,ap ngoc binh,ap ngoc hai,ap ngoc loi,ap ngoc trung,ap ngon,ap ngon cai,ap nguyen binh,ap nguyen thai hoc,ap nha dong,ap nha mat,ap nha may,ap nha noi,ap nha ong,ap nha phan,ap nha sap,ap nha tho,ap nha viec,ap nhan chau,ap nhan phu,ap nhan thuan,ap nhan xuan,ap nhat,ap nhi,ap nhon binh,ap nhon duc,ap nhon hoa,ap nhon khanh,ap nhon loc,ap nhon loi,ap nhon long,ap nhon my,ap nhon ngai,ap nhon phu,ap nhon phuoc,ap nhon qui,ap nhon tho,ap nhon thuan,ap nhuan oc,ap nhuan trach,ap nhut,ap nhut tay,ap ninh binh,ap ninh hoa,ap ninh loi,ap ninh thanh,ap ninh thanh chai,ap ninh thanh chua,ap ninh tho,ap ninh thoi,ap ninh thuan,ap noc nan,ap noi,ap nui com,ap nui huynh,ap nuoc chay,ap nuoc man,ap nuoc man ngoai,ap nuoc trong,ap nuoc vang,ap nuoc xay,ap o chich,ap o ma,ap o rang,ap ong hien,ap ong kham,ap ong lang,ap ong muon,ap ong nam,ap ong quen,ap ong ta,ap ong tan,ap ong tu,ap ong yen,ap op hoc,ap patt,ap phan boi chau,ap phieu,ap pho,ap pho duoi,ap pho ninh,ap pho trach,ap phom mit,ap phong hoa,ap phong nhuong,ap phong phu,ap phu,ap phu an,ap phu bai,ap phu bai i,ap phu bai vi,ap phu binh,ap phu cat,ap phu cuong,ap phu dien,ap phu dinh,ap phu duc,ap phu duong,ap phu giao,ap phu hai,ap phu hiep,ap phu hoa,ap phu hoi,ap phu hung,ap phu huu,ap phu khuong,ap phu lam,ap phu lan,ap phu le,ap phu lo,ap phu loc thuong,ap phu loi,ap phu long,ap phu luong,ap phu ly,ap phu mieng,ap phu minh,ap phu my,ap phu my ha,ap phu ngan,ap phu nhon,ap phu nhou,ap phu nhuan,ap phu nhut,ap phu ninh,ap phu nong,ap phu oc,ap phu phong,ap phu phong-a,ap phu qui,ap phu son,ap phu sum,ap phu sung,ap phu tay,ap phu thanh,ap phu thien,ap phu thinh,ap phu tho,ap phu thoi,ap phu thu,ap phu thuan,ap phu tinh,ap phu tri,ap phu trung,ap phu xuan,ap phu xuong,ap phum lu,ap phung,ap phung hiep,ap phung thuan,ap phuoc,ap phuoc binh,ap phuoc buu,ap phuoc cang,ap phuoc chau,ap phuoc co,ap phuoc da,ap phuoc dinh,ap phuoc hau,ap phuoc hien,ap phuoc hoa,ap phuoc hoa ii,ap phuoc hoi,ap phuoc hung,ap phuoc hung trung,ap phuoc huu,ap phuoc ke,ap phuoc khanh,ap phuoc lai,ap phuoc lam,ap phuoc le,ap phuoc loc,ap phuoc loi,ap phuoc long,ap phuoc ly,ap phuoc my,ap phuoc qua a,ap phuoc qua b,ap phuoc quan,ap phuoc son,ap phuoc tan,ap phuoc thanh,ap phuoc thien,ap phuoc thinh,ap phuoc tho,ap phuoc thoi,ap phuoc thuan,ap phuoc tinh,ap phuoc truong,ap phuoc tu,ap phuoc tuong,ap phuoc tuy,ap phuoc vinh,ap phuong an,ap phuong lac,ap phuong ninh,ap plei betel,ap quan dieu,ap quan ha,ap quan huy,ap quan long,ap quan nam,ap quan nhan,ap quang binh,ap quang dieu,ap quang gia,ap quang giao,ap quang nhan,ap quang ninh,ap qui,ap qui an,ap qui dong,ap qui thanh,ap quoi an,ap quy,ap rloum,ap rach ban,ap rach bang,ap rach dung,ap rach gau,ap rach giong,ap rach go,ap rach lang,ap rach loc,ap rach meo,ap rach muoc sac,ap rach muoi,ap rach nhiem,ap rach nhum,ap rach nui,ap rach ran,ap rach rung,ap rach so,ap rach soi,ap rach su,ap rach tia,ap ranh hat,ap rat,ap ray,ap rong ri,ap rum,ap rung dan,ap rung dau,ap rung la,ap rung tre,ap ruong,ap ruong hoi dong,ap sa chau,ap sai gon,ap sam pha,ap samuk,ap sang,ap sao,ap sao luoi,ap sarata,ap sau,ap say nieu,ap sieu quan,ap so ba,ap so bau no,ap so bay,ap so do,ap so hai,ap so mot,ap so tam,ap soa ao,ap soai kong,ap soc dich,ap soc giua,ap soc lao,ap soc soan,ap soc tranh,ap soc xiem,ap son,ap son chau,ap son cong,ap son hai,ap son hoa,ap son hoi,ap son qui,ap son tay,ap son tho,ap son thuan,ap son thuy,ap song cai,ap song phan,ap song tra,ap sre pa,ap srok rang,ap su lo thuong,ap sua dua,ap sung xuan,ap suoi ca,ap suoi cao,ap suoi cat,ap suoi soc,ap suoi tre,ap ta bam,ap ta bien a,ap ta bien b,ap ta det,ap ta diep,ap ta don,ap ta keo,ap ta lot,ap ta muc,ap ta mum,ap ta suon,ap ta ten,ap ta thanh,ap ta thiet krom,ap tac dong,ap tac gong,ap tac thu,ap talai,ap tam hoa,ap tam hung,ap tam lon,ap tam long,ap tam phuoc,ap tam soc,ap tam tan,ap tam vu,ap tan,ap tan an,ap tan an thi,ap tan binh,ap tan cam,ap tan chanh,ap tan dai,ap tan dien,ap tan dinh,ap tan dong,ap tan duc,ap tan duong,ap tan ha,ap tan hau,ap tan hiep,ap tan hiep hai,ap tan hiep loi,ap tan hoa,ap tan hoa dong,ap tan hoa tay,ap tan hoi,ap tan hung,ap tan khe,ap tan lam,ap tan lap,ap tan lap phu,ap tan loc,ap tan loi,ap tan long,ap tan my,ap tan ngai,ap tan nho,ap tan nhuan,ap tan phat,ap tan phong,ap tan phu,ap tan phu dong,ap tan phu ha,ap tan phuoc,ap tan qu,ap tan qui,ap tan qui kinh,ap tan qui rach,ap tan qui tam ngan,ap tan quy,ap tan son,ap tan thanh,ap tan thiet,ap tan thoi,ap tan thuan,ap tan thuoc,ap tan thuy,ap tan tien,ap tan trach,ap tan vinh,ap tan xuan,ap tang hem,ap tao lot,ap tau o,ap tay,ap tay binh,ap tay dinh,ap tay ha,ap tay ho,ap tay hoa,ap tay hoang,ap tay hung,ap tay khanh,ap tay lan,ap tay phu,ap tay thanh,ap teurlang dong,ap thach nham,ap thai binh,ap thai ho,ap thai thong,ap thai thuong,ap tham thien,ap than dao,ap thang binh,ap thanh,ap thanh an,ap thanh bac,ap thanh binh,ap thanh can,ap thanh cau,ap thanh chau,ap thanh dang,ap thanh dien,ap thanh dong,ap thanh duong,ap thanh giang,ap thanh hoa,ap thanh hoi,ap thanh hue,ap thanh hung,ap thanh huong,ap thanh khuong,ap thanh lap,ap thanh loc,ap thanh loi,ap thanh long,ap thanh luong a,ap thanh luong b,ap thanh my,ap thanh nam,ap thanh nguyen,ap thanh nhut,ap thanh ninh,ap thanh phu,ap thanh phuoc,ap thanh qui,ap thanh quoi,ap thanh son,ap thanh tam,ap thanh tan,ap thanh tay,ap thanh thien,ap thanh thoi,ap thanh thuan,ap thanh thuy,ap thanh tr,ap thanh tri,ap thanh trung,ap thanh vinh,ap thao lang,ap thap muoi,ap thay pho,ap thi,ap thi keo,ap thi tra du,ap thi tuong,ap thien,ap thien ai,ap thien duc,ap thien lap,ap thien long,ap thien phat,ap thien phuoc,ap thien son,ap tho cang,ap tho doi,ap tho duc,ap tho mo,ap thoai thuy,ap thoi,ap thoi an,ap thoi binh,ap thoi duong,ap thoi hau,ap thoi hiep,ap thoi hoa,ap thoi hue,ap thoi hung,ap thoi khanh,ap thoi loi,ap thoi long,ap thoi moi,ap thoi my,ap thoi phuoc,ap thoi thanh,ap thoi thien,ap thoi thuan,ap thoi trinh,ap thoi xuyen,ap thom rom,ap thom ron,ap thong luu,ap thong thao,ap thot lot,ap thu,ap thu cuu,ap thu ho,ap thu sau,ap thu truoc,ap thua phuoc,ap thuan,ap thuan an,ap thuan bien,ap thuan duc,ap thuan hoa,ap thuan loi,ap thuan nghia,ap thuan phu,ap thuan phuoc,ap thuong,ap thuong an,ap thuong hoa,ap thuong lac,ap thuong phu,ap thuong quoi,ap thuong tu,ap thuy lap,ap thuy thuan,ap thuy trung,ap thuy tu,ap tich khanh,ap tich phu,ap tich phuoc,ap tien,ap tien loi,ap tien my,ap ting wil,ap tinh phu,ap ton dinh,ap tong cang,ap tong khau,ap tra ban,ap tra bang,ap tra chinh,ap tra co,ap tra hat,ap tra kha,ap tra khua,ap tra mon,ap tra mot,ap tra ong,ap tra pho,ap tra tham,ap tra thanh,ap tra the,ap tra tien,ap tra vo,ap trach kien,ap trach pho,ap trach thuan,ap trai,ap trai bi,ap trai den,ap trai hoa,ap tram,ap tram hanh,ap tram lac,ap tram mot,ap tran hung dao,ap tran tao,ap trang,ap trang dau,ap trang lam,ap trang luc,ap trang nang,ap tranh,ap trem trem,ap trieu duong,ap trinh ba,ap trinh loi,ap trinh phu,ap trinh tuong,ap trong van,ap tru mat,ap trum thuat,ap trum toi,ap trum tri,ap trung,ap trung binh,ap trung binh nhi,ap trung chanh,ap trung hoa,ap trung hung,ap trung kieu,ap trung loi,ap trung nhi,ap trung phat,ap trung phu,ap trung son,ap trung thanh,ap truong bang,ap truong dong,ap truong hiep,ap truong hoa,ap truong hue,ap truong hung,ap truong ninh,ap truong phu,ap truong phuoc,ap truong son,ap truong tay,ap truong thanh,ap truong tho,ap truong thuan,ap tu,ap tu linh,ap tu tay,ap tuk chat,ap tuong nhon,ap tuong thanh,ap tuong tho,ap tuong tinh,ap tuy loc,ap tuy son,ap tuy tinh cham,ap tuyen binh,ap tuyen thanh,ap uat mau,ap ut thuong,ap van duong,ap van giao,ap van hien,ap van huong,ap van khe,ap van nhut,ap van trinh,ap van tu tay,ap van xuan,ap vi binh,ap vi dong,ap vi qui,ap vinh an,ap vinh an so,ap vinh binh,ap vinh cuu,ap vinh dien,ap vinh dinh,ap vinh dong,ap vinh goc,ap vinh hanh cham,ap vinh hao,ap vinh hao a,ap vinh hiep,ap vinh hinh,ap vinh hoa,ap vinh hoi,ap vinh hue ba,ap vinh hue hai,ap vinh hue moi,ap vinh hung,ap vinh khanh,ap vinh lac,ap vinh loc,ap vinh loi,ap vinh long,ap vinh my,ap vinh my hai,ap vinh my mot,ap vinh nay,ap vinh ninh,ap vinh phong,ap vinh phu,ap vinh phuoc,ap vinh phuoc ba,ap vinh phuoc hai,ap vinh qui,ap vinh sac,ap vinh sao,ap vinh tay,ap vinh thanh,ap vinh thoi,ap vinh thuan,ap vinh trieu,ap vinh trinh,ap vinh tuong,ap vinh tuong hai,ap vinh xuong,ap vom xang,ap vong dong,ap vu nang,ap xa,ap xa bang,ap xa don,ap xa mach,ap xa trach,ap xa ty,ap xam pha,ap xeo la,ap xeo mui,ap xiem la,ap xoai xiem,ap xom bung,ap xom chau,ap xom dao,ap xom giua,ap xom hanh,ap xom moi,ap xuan an,ap xuan duong,ap xuan hoa,ap xuan khai,ap xuan khanh,ap xuan phu,ap xuan sac,ap xuan sanh,ap xuan son,ap xuan thanh,ap xuan thieu,ap xuan tuy,ap xuong hoa,ap xuong thanh,ap xuong thoi,ap yan kar dang,ap yan kar dom,ap yen,ap yen ha,ap yen thuong,ap \303\220ong nhi,apeni,apnagyugh,apnia,apsho,apya,apa,apa asau,apa da brisa,apa funwonlata,apa khan,apa koy,apa neagra,apa obe,apa sarata,apa-apaya,apa-iwaji,apaa,apaan timur,apaaso,apabiekun,apabone,apabuco,apac,apaca-szakallas,apacapuszta,apacari,apacatorna,apacay,apace,apach,apacha,apachaco,apache,apache acres trailer park,apache city,apache creek,apache flats,apache grove,apache junction,apache springs,apache wells,apache west mobile village,apache wye,apaches,apacheta,apacheta calcalaguay,apacheta de jaisura,apacheta kasa,apachetacasa,apachetilla,apachevo,apachuan,apacible,apacilagua,apacilina,apacinigua,apaczatorna,apad,apad lutao,apad quezon,apadawywa,apadi,apadia,apadjop,apadu,apaeva,apafa,apafa-aiyede,apafaja,apaficuo,apaga,apaga fogo,apagni,apagu hamlets,apagua,apagual,apaguen,apagy,apagya,apagyuti hazak,apaha,apahanaerhchi,apahanaerhtsoichi,apahanaerhyuichi,apahar,apahataovana,apahida,apahone,apahua,apahuaique,apaicancha,apaicanchilla,apaija,apaikuma,apaing,apair,apaisia,apaj,apaja,apajalahti,apajatuba,apaji,apaji rancho,apak,apak yaylasi,apakachi,apakae,apakapeitzufu,apakayevo,apakbranjang,apakbrondol,apakevo,apakhan,apakho,apaki,apakoso,apakovo,apakpakpekope,apakpokope,apakrom,apakul,apal,apala,apalache,apalachee,apalachia,apalachicola,apalachin,apalahti,apalakt,apalampa,apalan,apalao,apalap,apalaringi,apalata,apalco,apale,apaleles,apalem,apalen,apaleng,apalevo,apali,apalia,apalikha,apalikhino,apalili,apalina,apalipe,apalipi,apalit,apalkovo,apalla,apalle,apalon,apalona,apalos,apalot,apalset,apalshchino,apam,apama,apamar,apamatal,apamate,apamatico,apamato,apambi,apambiakiri,apamea,apamea cibotus,apamica,apamico,apamila,apamilca,apampatia,apampetia,apampoa,apamu,apamu ife,apamul,apan,apana,apanachi,apanaipi,apanangan,apanango,apanangon,apanas,apanasenki,apanasenko,apanasenkovskoye,apanasevka,apanasionki,apanaskino,apanaskovichi,apanaskovo,apanasovka,apanasovo-eshebenovo,apanasovo-temyashi,apanasovskiy,apanasyutinki,apanauane,apanay,apanayevo,apancoyo,apandra,apandzo,apane,apaneca,apang,apangai,apangai 1,apangai 2,apangai number 2,apangai number1,apangi,apango,apangokro,apangual,apanguala,apanguito,apani,apani mangouakro,apanichino,apanimadi,apanita,apanitsy,apanitsyno,apankino,apano meria,apano potamia,apano zakro,apano-klyuchi,apano-klyuchinskoye,apanokhori,apanovka,apanpa,apanquezalco,apanquito,apante,apantem,apanten,apanteopa,apantes,apantita,apantlasol,apantlazol,apanuch,apanueh,apao,apaoewar,apaokrom,apaone,apaora,apaoro,apapa,apapa eleko,apapa odan,apapahoe,apapai,apapam,apapantilla,apapaso,apapasu,apapataro,apapaxtla,apapaya,apapelgino,apapelkhin,apapuerta,apaquogue,apar,apara,aparados da serra,aparagra,aparai,aparajos,aparaju,aparaki,aparan,aparan verin,aparanbol,aparanpol,aparasaka,aparatorii patriei,aparay,aparbal,aparcos,apardi,apare,aparecida,aparecida doeste,aparecida de goias,aparecida de minas,aparecida de monte alto,aparecida de sao manuel,aparecida do rio negro,aparecida do taboado,aparecida do tabuado,aparej,aparejito,aparejos,aparekka,apareos,aparhant,apari,apariciero,aparicio,aparicion,aparicion de la coromoto,aparicos,aparicuaro,aparikha,aparima,aparina,aparinka,aparinki,aparinkova,aparino,aparinskaya,aparinskiy,aparisa,apariuohu,aparkati,aparki,aparkino,aparli,aparly,aparnikova,aparnikovo,aparo,aparpo,aparra,aparral,aparri,apartadade,apartadero,apartaderos,apartado,apartadura,apartos,aparun,aparungnge,aparuren,apas,apasa korpe,apasacuna,apasakope,apasan,apasapan,apasapo,apasaraycik,apasarycik,apascia,apasciai,apasco,apasdal,apaseo,apaseo el alto,apaseo el grande,apasevo,apashi,apasi,apasil,apasilagua,apasilina,apasinigua,apasja mana,apasland,apaso,apastate,apastepeq,apastepeque,apastovo,apasu,apasupo,apat,apat-ujfalu,apata,apata alaje,apata ibadan,apata iya oje,apata oloro,apata omoeiye,apata tesi,apata yakuba,apatafoue,apataganga,apataim,apatamonasterio,apatan,apatana,apatanganya,apatawkofe,apate,apatem,apateparus,apatere,apateu,apateul,apatfa,apatfalva,apathin,apati,apati puszta,apatia,apatiitti,apatija,apatin,apatipuszta,apatistvanfalva,apatit,apatitas,apatity,apatiu,apatkovo,apatlaco,apato,apatorkorpe,apatoro,apatot,apatou,apatovac,apatsziget,apatug,apatus,apatut,apatvarasd,apatvarasdtelep,apatzingan,apatzingan de la constitucion,apatzingo,apauco,apaukkyi,apaukwa,apaumka-taung,apaung,apauni,apaura,apausupo,apauwar,apauwor,apavas,apaven,apawa,apawa samga,apawanta,apaxco,apaxco de ocampo,apaxtla,apaxtla de castrejon,apaya,apayacu,apayale,apayan,apayao,apaydin,apaydyn,apayerey,apayevo,apayhuana,apayhuana hacienda,apayiruma,apaykina,apaykina gar,apayn,apaz makhle,apazapam,apazapan,apazapo,apazha,apazlar,apazote,apazovo,apazulco,apazupo,apa\302\223sia,apc,apcaga,apchae,apchaedong,apchak,apchakkol,apchar,apchary,apchat,apche,apcher,apcheri,apchidong,apchigyy-aly,apchin camp,apchon,apdal,apdalac chena,apdaloglu,apdi,apdo,apdullahkomu,ape,ape dembagourou,ape dialoube,ape sakobe,apeadero,apeadero cochabamba,apeadero de laurita,apeadero de los molinos-guadarrama,apeadero encinillas,apeadero la parra,apeadero punke,apeakava,apean,apeasika,apeawa,apebiachire mem,apebo,apeca,apecchio,apeche,apeda,apeda kondji,apedan,apedi,apedo,apedome,apedome fongbe,apeduru,apedwa,apefon,apefule,apegame,apeguso,apegya,apehu,apeichiao,apeja,apeji,apeka,apekalova,apekou,apel,apela 1,apela 2,apela dua,apela satu,apelacao,apeland,apelas,apelawo,apeldoorn,apeldor,apeldorn,apele,apele vii,apelebirigbene,apelebri,apeleg,apelenoum,apeler,apelern,apelete,apelgading,apelgarden,apelhester,apelhult,apeliua,apellaniz,apelnstedt,apelset,apelskift,apelstedt,apelvik,apelviken,apelvikshojd,apeme,apemso,apemtu,apen,apena,apena aka,apena ake,apenam,apenburg,apenda,apeneki,apenemedi,apenes,apeng,apengal,apengmulengen,apengsala,apengsawang,apengsembeka,apenhuizen,apening,apenje,apenkwa,apeno,apenpe,apenpe market,apenrade,apenraniako,apensen,apentuo,apenyigbe,apenyowewu,apeogh,apeosika,apepo,aperade,aperadi,aperathos,aperberg,aperche,apere,aperekka,aperfeld,apergatika,aperi,aperibe,aperinga,aperion,aperloo,apero,aperod,aperregui,apersogadero,apertado,apertado do morro,apertar,aperver,apes hill,apesa,apesco,apese,apesha,apeshki,apesi,apesia,apesika,apesin,apesokari,apesokarion,apesokubi,apesua,apetahuacan,apetatitlan,apetatlitlan,apete,apetina,apetitlan,apetlaco,apetlanca,apetlon,apetzco,apetzuca,apeu,apeukrajeu,apeukrayeu,apeul,apeuule,apeviape,apewohe,apewu,apex,apex hill,apexco,apeyeme,apezan,apfede,apfelbach,apfelberg,apfeldorf,apfeldorfhausen,apfelkoch,apfelsbach,apfelstadt,apfelstetten,apfelthal,apfeltrach,apfeltrang,apfingen,apflau,apfoltern,apgahan,apgajan,apgaji,apganan,apgar,apgaye doabli,apgerean,apgoji,apgol,apgulde,aphaneia,aphang,aphania,aphaung,apheit,aphen,aphiri,aphit,apho,aphort,aphoven,aphrike,aphrodites,aphroditopolis,aphulsi,aphunus,aphyongje,api,api rokok,api-api,apia,apiaba,apiaca,apiadji,apiadu,apiag,apiah kwa debiso,apiahy,apiai,apiajeikrom,apiakouakrou,apiakpo-alacha,apiakro,apiakrom,apiakwa,apial,apian,apiancha nizhniy,apiancha verkhniy,apianda,apianyinase,apianyinoso,apiao,apiapi,apiapum,apiapum eja,apiar,apiarliyah,apiary,apiat,apiau,apiay,apiayaokrom,apibo,apiboko,apic-pac,apicala,apice,apichi beke,apicita,apicum,apid,apida,apidamy,apidan,apidea,apidhea,apidheon,apidhia,apidhitsa,apidhoula,apidia,apidochot,apies,apieskraal,apiga,apigikwe,apihaa,apihun,apikalo,apikia,apiklar,apikollo,apikolo,apikon,apikotto,apilata,apildset,apilga,apilho,apilho balanta,apilla,apilla pampa,apillapampa,apillmarca,apilol,apimenim,apimenyim,apimkpo,apimoe,apimso,apimu,apin,apin apin,apinac,apinage,apinaipi,apinaje,apinam,apinamang,apinda,apindji,apindu,apine,apinega,apingal,apingalla,apingan 1,apingan number 1,apinggoot,apingollas,apinhnase,apini,apinkpo,apinkra,apinokolo,apinpua,apinrin,apintal,apinto,apinya,apio,apion,apiope,apipe,apipilhuasco,apipilhuaxco,apipilhuazco,apipilulco,apipucos,apiques,apir,apiranthos,apirathi,apiravi,apirede,apiro,apiry,apis,apisa,apisano,apishan,apishen,apisolaya,apison,apita,apitaik,apitaina,apitalaukes,apitalawu,apitan,apiti,apitipiti,apitiso,apitisu number 2,apitong,apitoye,apityeh,apityehpondok,apiuna,apiv,apixtla,apiyai-copue,apiyaicupue,apiye,apiyeti,apiza,apizaco,apizaquito,apja,apkaegol,apkaji,apkan,apkangdong,apkasheva,apkashevo,apkhalipovskiy,apkharok,apkhayta,apkhulta,apkhvor,apkogae,apkoji,apkok,apkol,apkombau,apkomrae,apkomunbawi,apkongni,apkori,apkoroshi,apkouta,apkujong,apkujongdong,apkujongidong,apkujongiltong,apkujongni,apla,apla assoumankro,aplabanya,aplabu,aplacadoiro,apladhiana,aplahoue,aplakarr,aplaksino,aplaksinskiy,aplaku,aplakulla,aplal,aplalu,aplame,aplanda,aplanda chiftlik,aplanki wadi,aplanou,aplanta,aplantlasol,aplantlazol,aplao,aplared,aplaryd,aplas,aplasi,aplatsi,aplatsu,aplaya,aplazadoiro,aplehult,aplerbeckermark,aplerod,apleryd,apley,aplic,apliki,aplin,aplin beach,aplington,aplinki,aplodinap,aplung,aplungsasen,apmaul,apnagyukh,apnaset,apnery,apo,apo cayo,apo island,apo kodje,apo-apo,apo-apurawan,apoaja,apoala,apoang,apoaporawan,apoasi,apoaso,apobinou,apobo,apoc-apoc,apoca,apocco,apochkovo,apocon,apodabogo,apodaca,apoderado,apodhikai,apodhitsa,apodhoulo,apodhoulou,apodi,apodo,apodong,apodu,apody,apoeidounpo,apoekakondre,apoera,apoeydoumpo,apogi,apoi,apoig,apoikia,apoinga,apoini,apoinion,apoipo,apoisso,apoje,apoje-orile,apoka,apoki,apokin,apokin-idanre,apoko,apokobem,apokofe,apokon,apokor,apokori,apokoro,apokro,apoku,apol peul,apola,apolakkia,apold,apolda,apoldu de jos,apoldu de sus,apoldul-de-jos,apoldul-de-sus,apoleang,apoleggia,apoleka,apolentina,apolets,apoli,apolikha,apolikhnos,apolin,apolinao,apolinar,apolinar perdomo,apolinario,apolinario saravia,apolinarovo,apolino,apoliny,apolisheno,apolishino,apollachia,apollensdorf,apollinopolis magna,apollinopolis parva,apollo,apollo bay,apollo beach,apollo majer,apollo mobile home park,apollo shores,apollon,apollona,apollonas,apollonia,apollonie,apollonios,apollonopolis magna,apollonopolis parva,apollonos,apollonovka,apollonovskoye,apollonski,apollonte,apollosa,apolo,apolo xi,apologun,apolon,apolong,apolonia,apolonia hacienda,apolonio,apolonio samson,apolonja,apolonka,apolonovka,apolonovskoye,apolonskiy,apoloy,apolpaina,apoltovo,apolu,apolvar,apolye,apolyont,apolyontkoy,apomal,apomande,apomaoro,apomarma,apomarmas,apomas,apomasi,apombo,apomero,apomeron,apomitos,apomos,apompal,apompal del deslinde,apompoa,apompua,apomu,apomul,apon,aponan,aponapon,aponapona,aponaskovo,aponasovka,aponchire,apone,apongdam,apongo,aponi-vi,aponit,aponitishchi,aponla,aponmu,aponmu-idanre,aponosi,aponosovka,aponran,aponsia,aponsie,aponte,apontita,aponza,apooco,apopa,apopio,apopka,apopo,apopong,apopongi,apoquindo,apora,aporai,aporama,aporawan,apore,aporema,aporhaza,aporka,aporkai tanyak,aporkai urbo-tanya,aporkaitanya,aporliget,aporo,aporoma,aporota,apos,apos villages,aposaga,aposahualco,aposalorange,aposdorf,aposelemi,aposelemion,aposentillo,aposento,aposentos,aposeti,aposika,aposingbato,aposkepas,aposkepos,aposo,aposote,aposso,apossou,apostadero,apostag,apostagi tanyak,apostari,apostarii,apostel,apostelhuizen,apostelmuhle,apostica,apostol,apostolache,apostolaiika,apostolar,apostoles,apostoliano,apostolianos,apostolianou,apostolias,apostolidi,apostoloi,apostolos,apostolou,apostolove,apostolovka,apostolovo,apostolowo,aposu,aposul,apot,apoteki,apoteri,apotheki,apothekidorp,apothikai,apothikes,apoti,apoto,apotoruitpa,apotovac,apotureca,apou,apouassa,apouesso,apougon,apouibo,apounonkpa,apowa,apowan,apoya,apoyecancingo,apoyin,apoykovo,apoypue,apozaga,apozahualco,apozai,apozol,apozolco,appa,appa purwa,appaarsuit,appabatu,appademmenge,appajeng,appak,appakireng,appakkol,appakkuddikinattadi,appakkuttikinattadi,appakovo,appakuddikinattadi,appalachia,appalaringnge,appale,appalikha,appallagoda,appam,appamiut,appan-sapan,appananallur,appanang,appanapalli,appangbatu,appany,appanye,appapelkhino,apparatnyy,apparecida,apparecida dos corregos,apparecido de monte alto,appari,apparp,apparri,appas,appasarange,appasareng,appasse sidi meftah,appat,appatanah,appavoo nagar,appeal,appecano,appel,appelblok,appelbo,appelboom,appelbos,appelburg,appelbuttel,appelby,appeldorn,appelhagen,appelhester,appelhoek,appelhof,appelhorst,appelhulsen,appelle,appeln,appelo,appels,appels veir,appelsberg,appelsbosch,appelscha,appelsga,appelshof,appelskift,appelt hill,appeltern,appelterre-eichem,appelterre-eychem,appelveld,appelviken,appen,appenaes,appenai,appenai-sous-belleme,appenberg,appendorf,appenfeld,appenfelden,appenhagen,appenhain,appenheim,appenhofen,appennino,appenrod,appenrode,appensee,appenthal,appenweier,appenweiler,appenweir,appenwihr,appenzell,appera,appercha,apperley,apperley bridge,appersberg,appersdorf,appersett,apperson,appersons store,appertshausen,appertshofen,apperum,apperup,appetshofen,appetsu,appeville,appeville-annebault,appi,appiano,appiano gentile,appie,appieto,appietto,appignano,appignano del tronto,appikalo,appikatla,appila,appilly,appin,appin south,appingedam,appingeng,applakarr,applanas,applaro,applaryd,apple,apple blossom court,apple creek,apple greene,apple grove,apple mountain lake,apple mountain lake west,apple ridge,apple river,apple springs,apple spur,apple tree creek,apple tree flat,apple tree village,apple valley,apple valley estates,apple valley highlands,applebachsville,appleby,appleby magna,applecross,appledale,appledore,appledram,appleford,applegarth,applegarth town,applegate,applegate condo,applegate corner,applegate ford,applehult,applekulla,applerum,applerund,appleryd,apples,appleshaw,applethorpe,appleton,appleton acres,appleton city,appleton glen,appleton le moor,appleton le moors,appletown,appletree hill,appletreewick,appleudden,appleund,applewalk park,applewhaites,applewold,applewood,applewood mobile home park,appley bridge,appleyard,appling,applo,appmannsberg,appoassi,appodong,appoigny,appolding,appolds,appolion,appolonia,appolonovka,appolonovskiy,appomattox,apponagansett,apponaug,appongpalai,appontabau,appontement,apponyi foldek,apponyifolditanyak,appouasso,appouessou,appregnin,appremdu,apprieu,apprompron,appronpronou,approuague,apps,appugama,appukuttigama,appuna,appuwewa,appy,appynye,apqun,apra,apra heights,apra junction,aprachiri,aprachiri awuna kwame,apradan,apradang,apradi,apraksin bor,apraksin gorodok,apraksina,apraksino,apraksino pervoye,apraksino vtoroye,aprakyir,apram,apramohukope,apran,aprani,apranli,aprany,aprasimovo,aprasive,apraskina,aprath,aprazivel,aprcovici,apregacion,aprelevka,aprelivka,aprelka,aprelkova,aprelkovo,aprelova,aprelovo,aprelsk,aprelskiy,aprelya,apremdo,apremedji,apremeso,apremont,apremont-la-foret,apremont-sur-aire,apres tout,apreskiy,apreweti,aprey,apriach,apriano,aprica,apricale,apricano,apricciani,apricena,apriciani,apricke,apricot,aprigio,apriglianello,aprigliano,apriki,april acres,april estates,april meadows,aprilia,aprilov,aprilovo,aprilowo,aprilskraal,apriltsi,apriltzi,aprilzi,aprimbo,aprine,aprip,aprip-guri,apripa,apripa e gurrit,apripa e keke,apripa e keqe,apripa e parava,apripa e-gurit,apripa-gurit,apripe,apripe e gurit,apripe e keqe,apriti,apro,aproaga,aprobel,aprodu purice,aprohomok,apromprom,aprompromou,aprompron,aprompronou,apron crossing,apron-pronou,aproni,apronyarfas,aprosimovka,aprosovo,aprossue,aprosyevo,aprotu,aprovata,aprovaton,aprovatou,aproz,aprozi,aprripe,aprumase,aprumasi,aprunyi,aprutu,apryanino,apsabash,apsaegol,apsagachevo,apsai,apsakal,apsakan,apsalas,apsalla,apsalle,apsalos,apsalyamova,apsalyamovo,apsamajor,apsanmit,apsar,apsari,apsarus,apsas,apsava,apsayan,apscheranskaja,apscheronskij,apseki,apselyam,apsenieki,apserde,apserdes,apserdi,apsevci,apshak-pelyak,apshakbelyak,apshankhvara,apshanovo,apshatnur,apshawa,apshenieki,apsheron,apsheronsk,apsheronskaya,apsheronskiy,apsheronskiy port,apshi,apshitsa,apshiyakhtu,apshutsiems,apshutsiyem,apshutsiyems,apsi bazar,apsiey,apsigol,apsil,apsilgol,apsilli,apsiou,apsites,apsiu,apskalni,apsley,apsley station,apso,apsom,apsou,apsrutai,apssungmul,apsu,apsua,apsuciems,apsudong,apsugol,apsukrogs,apsun,apsuona,apsut,apt,apta,aptaat,aptair,aptakisic,aptal,aptala,aptalbayazit,aptalbazit,aptalcik,aptalhasan,aptalkolu,aptallar,aptaloglu,aptalpinari,aptar,aptaraman koy,aptarazak,aptat,apteka,aptekarskiy,aptera,apthorp,apti,apti koy,aptia,aptiagaciftligi,aptidong,aptikashevo,aptikovo,aptogi,aptok,aptongne,aptongni,aptongsan,aptos,aptrakova,aptrakovo,aptrup,aptryakova,aptryakovo,aptskhva,aptuduk,aptugay,aptulla,aptullar,aptuna,apturak,aptus,aptyakpos,aptyarovka,aptygichan,aptykash,aptykayevo,aptyshkasy,aptyukova,aptyukovo,aptyuray,apu alvar,apu alvar-e sofla,apua,apuagwu,apuang,apuania,apuapu,apuara,apuarema,apuayem,apucancha,apucara,apucarana,apucaraninha,apucarena,apucarpo,apud,apudam,apudum,apuela,apuerd nouarab,apuerd n-warab,apugan,apugezi,apugi,apuhambati,apuhuaijo,apuhy,apui,apuiares,apuiraramua,apuires,apuj,apuk,apuka,apuka-koryakskaya,apukalns,apukfu,apukhlitsevo,apukhlitsy,apukhtin,apukhtina,apukhtino,apukka,apulavar-e pain,apulco,apuled,apulia,apulia station,apulid,apullo,apulo,apulvar,apulyont,apumaka,apumarca,apumayawku,apun,apun girang,apun hilir,apun talun,apunahunu,apunan,apundaro,apune,apunit,apunu,apunyal,apuole,apuoles,apupara,apuparra,apupo,apura,apurac,apurahe,apurahi,apurahuan,apurauan,apurawan,apure,apure plato,apurere,apurguan,apuri,apurima,apurin,apurito,apurle,apurlec,apurouga,apurpur,apurruro,apururen,apurvar sofla,apurvar-e pain,apushka,apushkino,apushrotas,apushta,apusinai,apusinyay,apuslega,apusrotas,apusta,aputala,aputan,aputane,aputh bahadur,aputh bahadur shah,aputh janjiana,aputhanar baihk,aputi,aputiteeq,aputiteq,aputok,aputon,aputova,aputovo,aputu,aputuoagya,aputuoja,aputzio de juarez,apuu,apuwai,apuy,apuyaco,apuze,apvarsavos,apvarsuva,apwagan,apwambo,apwao,apweiler,apwisch,apwisihu,apwori,apya,apyauk,apyin nangat,apyinthebyu,apyong,apyshevo,apytalauke,apytalaukis,apytlaukis,aq,aq ashlu,aq bach,aq bagh,aq balaq,aq band,aq barabad,aq barar,aq bash,aq beraz,aq bolagh,aq bolagh fowziyeh,aq bolagh gadug,aq bolagh mohammad vali,aq bolagh-e `ali akbar khan,aq bolagh-e `olya,aq bolagh-e abu bakr,aq bolagh-e aqajan,aq bolagh-e aqajan khan,aq bolagh-e aqdaq,aq bolagh-e bala,aq bolagh-e chang almas,aq bolagh-e chang almasi,aq bolagh-e faraj bek,aq bolagh-e fuziyeh,aq bolagh-e khaled,aq bolagh-e kuranlu,aq bolagh-e latgah,aq bolagh-e meydan,aq bolagh-e mohammad hoseyn khan,aq bolagh-e morshed,aq bolagh-e pain,aq bolagh-e rostam khan,aq bolagh-e shahbaz beyk,aq bolagh-e shahbazbeyk,aq bolagh-e sofla,aq bolagh-e somayyeh,aq bolagh-e tupal,aq bolaghi,aq bolaq,aq bolaq qeshlaq,aq bolaq-e chang almas,aq bourhane,aq bulagh,aq bulak,aq bulaq,aq bulaq changal almas,aq bulaq murshid,aq bulaq qeslaq,aq bulaq qishlaq,aq bunar,aq burhan,aq buria,aq burya,aq chah,aq chali,aq chali-ye bala,aq chali-ye pain,aq chaq,aq chashmah,aq chatal,aq chay,aq chay `olya,aq chay-e bala,aq chay-e pain,aq chay-e vasat,aq cheshmeh,aq dagesh,aq dagesh-e bala,aq darah,aq darak,aq daraq,aq darband,aq darrah,aq darreh,aq dash,aq dash-e pain,aq dashi,aq davahlu-ye bala,aq davahlu-yepain,aq davallu-ye bala,aq davallu-ye pain,aq divar,aq douqar,aq duqar,aq duz,aq emam,aq evler,aq founar,aq gash-e bala,aq gavnakh-e bala,aq gol,aq gonbad,aq gonbaz,aq gozar,aq gumbaz,aq gunbad,aq gunbaz,aq guni,aq guzar,aq hisar,aq jallar,aq jalu,aq jaqumbez,aq jar,aq joqlu,aq kahriz,aq kahrizak,aq kalmeh,aq kamar,aq kamar-e bala,aq kamar-e pain,aq kamar-e vasat,aq kan,aq kand,aq kand-e bala,aq kand-e baruq,aq kandi,aq kant,aq kariz,aq keuy,aq koca,aq kochah,aq kotal,aq kowtal,aq kubruk,aq kucheh,aq kul,aq kupruk,aq kuy,aq machit,aq macit,aq manar,aq masjed,aq masjid,aq mastan,aq mazar,aq mazar-e gurash,aq mian,aq mohammad tappeh,aq otaq,aq owlar,aq peunar,aq pounar,aq qabaq,aq qabaq-e `olya,aq qabaq-e golshad kandi,aq qabaq-e qarahlu kandi,aq qabaq-e qelenjkhan kandi,aq qabaq-e sofla,aq qabaq-e vosta,aq qabr,aq qal`eh,aq qala,aq qamish,aq qaselmu,aq qasemlu,aq qayah,aq qayeh,aq qeshlaq,aq qiyeh,aq qol,aq qowl,aq qui,aq qualeh,aq queh,aq quyu,aq rabat,aq ribat,aq ruz,aq sahra,aq sai,aq sakh,aq sangam,aq saray,aq say,aq shakh,aq su,aq tach,aq talagan,aq talah gan,aq tapa,aq tapah,aq tappeh,aq tappeh-ye nashr,aq taqeh-ye jadid,aq tash,aq tavaraq,aq tekeh khan,aq tepa,aq tepah,aq tepe,aq toba,aq topraq,aq toqeh,aq toqeh-ye jadid,aq toqeh-ye qadim,aq towpraq,aq towqeh,aq tupraq,aq tuqeh,aq veran,aq virane,aq wiran,aq yaji,aq zabir,aq zaman goli,aq zaman kandi,aq zamin,aq zawa,aq zekink,aq ziarat,aq zir,aq-e `abdol,aq-e `abdul,aq-i-`abdul,aqa,aqa alisams,aqa `ali sara,aqa `ali shams,aqa `alilu,aqa `alishams,aqa abul,aqa ahmad,aqa baba,aqa baba mahalleh,aqa baba sang,aqa baba-ye faramarzi,aqa baba-ye framarzi,aqa babay sang,aqa bahram,aqa bak,aqa baqer,aqa beglu,aqa beyg,aqa beyglu,aqa beyk,aqa bil-e bala,aqa bil-e pain,aqa bolagh,aqa bolaghi,aqa bozorg,aqa burut,aqa chay-e bala,aqa chay-e pain,aqa chay-e vasat,aqa cheshmeh,aqa daka pir,aqa esma`il,aqa faqir kala,aqa gol,aqa hasan,aqa hasan beyglu,aqa hoseyn,aqa ja`fari,aqa jakandi,aqa jalu,aqa jan bolaghi,aqa jan kalay,aqa jan kelay,aqa jani,aqa jari,aqa jeri,aqa joghlu,aqa jun-e mashhadi `avaz,aqa kalay,aqa kandi,aqa kariz,aqa kelay,aqa lak,aqa lotf `ali,aqa lotf`alt,aqa mahalleh,aqa mahd khan,aqa malek,aqa mazar,aqa mir,aqa mir ahmad,aqa miri,aqa mirlu,aqa mirza,aqa mogim mahalleh,aqa mohammad,aqa mohammad beyglu,aqa mohammad kalay,aqa mohammad khan,aqa mohammad nurkhan,aqa mohhammad,aqa molk,aqa morad,aqa muhammad,aqa muhammad kelay,aqa muhammad khan,aqa murad,aqa nur-e sehtan,aqa ojaq,aqa qoli,aqa rahim,aqa seydal,aqa seyyed,aqa seyyed hasan,aqa seyyed jani,aqa seyyed mohammad,aqa seyyed reza,aqa seyyed sharif,aqa seyyed zakaria,aqa seyyed-e bala,aqa shafi`,aqa sur,aqa vali,aqa verdelu,aqa verdilu,aqa yarlu,aqa zeyarat,aqa ziarat,aqa ziyarat,aqai,aqa-ye bozorg,aqab doco,aqabad,aqaban,aqabat al rahayba,aqabe,aqach,aqad,aqadyr,aqagir,aqah,aqaian kalay,aqaj,aqaj akani,aqaj kandi,aqajan,aqajan kandi,aqajan khan,aqajan mahalleh,aqajani,aqajani mahalleh,aqajari,aqajeh kandi,aqajoli,aqajri,aqal,aqal muhammad,aqal saar,aqalbak bala,aqalbak pain,aqali-ye `olya,aqalwala,aqamir,aqamir kandi,aqamirlu,aqamoh kalay,aqamohammed kalay,aqamohd,aqamohd kalay,aqamohd-khan,aqamorad,aqanch,aqani,aqanj,aqanur,aqaqi,aqaqli bak,aqar,aqar `atabah,aqar `olya,aqar-e `olya,aqar-e bala,aqarak,aqaral,aqareb es safi,aqareva,aqareve,aqash gubol,aqasi,aqasin,aqasin-e bala,aqasin-e pain,aqayan kalay,aqayeh,aqba,aqbal,aqbal-e bala,aqbal-e pain,aqbal-i-bala,aqbal-i-pa in,aqbalyq,aqbaqay,aqbash,aqbastau,aqbasty,aqbauyr,aqbeyit,aqbis,aqbolagh,aqbolagh dagh,aqbolagh kanasbi,aqbolagh meydan,aqbolagh-e `alamdar,aqbolagh-e `aliakbar khan,aqbolagh-e `olya,aqbolagh-e bahman,aqbolagh-e fotuhi,aqbolagh-e gaduk,aqbolagh-e givi,aqbolagh-e hamadan,aqbolagh-e hamedani,aqbolagh-e hasan kandi,aqbolagh-e hasan khan,aqbolagh-e hasanabad,aqbolagh-e hashtrud,aqbolagh-e hoseyn khan,aqbolagh-e kord,aqbolagh-e nazarian,aqbolagh-e sadat,aqbolagh-e sardar,aqbolagh-e sofla,aqbolagh-e taghamin,aqbolagh-e taqamin,aqbolaq,aqbulagh,aqbulaq,aqbulaqi,aqbut al bayut,aqca,aqcasma,aqcha,aqcha qal`eh,aqchah,aqchai ulia,aqchalar,aqchari,aqchashmeh,aqchay baba `ali qeshlaqi,aqchay sofla,aqchay-e `olya,aqchay-e sofla,aqchay-e vosta,aqcheh,aqcheh aghachli,aqcheh aghashli,aqcheh bolagh,aqcheh dam,aqcheh gonbad,aqcheh kahriz,aqcheh kan,aqcheh kand,aqcheh kandi,aqcheh kharabeh,aqcheh kohel,aqcheh mazar,aqcheh pireh,aqcheh qal`eh,aqcheh qayah,aqcheh qayeh,aqcheh qeshlaq,aqcheh qeshlaq-e `olya,aqcheh qeshlaq-e jadid,aqcheh qeshlaq-e qadim,aqcheh qeshlaq-e sofla,aqcheh qeyeh,aqcheh-ye kharabeh,aqchehdizeh,aqchelu,aqcheshmeh,aqchi,aqchigayeh,aqchin,aqcholujah,aqda,aqdagh,aqdagh rud,aqdagh-e bala,aqdaghrud,aqdala,aqdaraq,aqdaraq-e jadid,aqdaraq-e qadim,aqdarband-e bala,aqdarreh-ye `olya,aqdarreh-ye sofla,aqdarreh-ye vosta,aqdas,aqdash,aqdasiyeh,aqdasiyyeh,aqdim,aqduz,aqech,aqen,aqenj,aqeq,aqeshlu,aqespe,aqesta,aqevlar,aqez kalleh,aqgonbad,aqgumbaz,aqhuzbon,aqi,aqi mahar,aqiba,aqicheh,aqigou,aqigsserniaq,aqil,aqil dal,aqil ghaghro jo goth,aqil hakra,aqil hakro,aqil jagirani,aqil khan,aqil mahar,aqil rahu,aqil shah kalan,aqil shah khurd,aqila changar,aqilpur,aqilpura,aqilwala,aqimaga,aqina,aqineh,aqiongduo,aqiq,aqiq saghir,aqisserniaq,aqja kand,aqja qeshlaq,aqja qishlaq,aqjah bayir,aqjah bulaq,aqjahdan,aqjakumbez,aqjaqleh,aqjar,aqjari,aqjeh,aqjeh bolagh,aqjeh dam,aqjeh darband,aqjeh dizeh,aqjeh gonbad,aqjeh kahriz,aqjeh kand,aqjeh kandi,aqjeh kharabeh,aqjeh kharabeh-ye pain,aqjeh pireh,aqjeh qal`eh,aqjeh qaya,aqjeh qayah,aqjeh qayeh,aqjeh qeshlaq,aqjeh qeshlaq bala,aqjeh qeyeh,aqjeh qia,aqjeh qiya,aqjeh qiyah,aqjeh-ye kharabeh,aqjehlu,aqkahriz,aqkand,aqkand-e qareh kand,aqkand-e qareh khezer,aqkand-e samaraq,aqkemer,aqkengse,aqkol,aqkotal,aqkowtal,aqkul,aqla,aqlak,aqlala al fouqiya,aqlat al suqur,aqloqqeh,aqmanar,aqmast,aqmektep,aqmeschit,aqmola,aqneli,aqoba,aqongkur,aqonj,aqordit,aqoulet aaqoula,aqoulet el kharraj,aqowzlu,aqpater,aqqa,aqqa irene,aqqal`eh,aqqan,aqqaytym,aqqik,aqqika\302\277,aqqoltyq,aqqora,aqqudyq,aqqum,aqqystau,aqqyztoghay,aqra,aqraba,aqrabat,aqrabiye,aqrafi,aqrereb,aqro,aqrobat,aqsanabad,aqsaqmaral,aqsay,aqsay `arab,aqsay `arab three,aqsay `arab two,aqshatau,aqshi,aqshira,aqshirah,aqshireh,aqshopa,aqshuqyr,aqshyghanaq,aqsira,aqsu,aqsu kona shahr,aqsu new city,aqsu old city,aqsu yangi shahr,aqsu(1),aqsu-ayuly,aqsuat,aqsuyek,aqtalat,aqtam,aqtappeh,aqtas,aqtash,aqtasty,aqtau,aqtavileh,aqtaysay,aqtaz,aqterek baziri,aqterek deryasi,aqtobe,aqtoghay,aqtowpraq,aqtu,aqtubek,aqtugeh,aqtuyesay,aqui,aqua,aqua alta vale,aqua clara,aqua la bella,aqua laseca,aqua obio effiat,aqua obio inwang nsidung,aqua park,aqua preta,aqua santa,aqua verde,aqua vista,aqua viva,aquablanca,aquabonita,aquacate,aquacate de arriba,aquachia,aquada,aquada de lomas,aquadahan,aquadale,aquae aureliae,aquae balisae,aquae grani,aquae lasae,aquae mattiacae,aquahart manor,aquara,aquarone,aquasanta,aquasco,aquashicola,aquashuarco,aquasparta,aquatria,aquaviva picena,aquba,aquban-i khwaru,aquban-i zhuru,aqubani,aquebogue,aqueduct,aqueero,aquel,aquele,aquemina,aquenta,aquepa,aquerana,aquerbate,aquerd,aquermalek,aquespal,aquespala,aquetong,aquetuck,aquetzpalco,aquexpalco,aqug,aqui esta,aqui-ferto,aquia,aquia harbour,aquiagua,aquiahuac,aquiahuac tlachco,aquiapan,aquiares,aquib,aquib norte,aquib sur,aquibuiquichi,aquicha,aquichal,aquiche,aquichi,aquichopo,aquida,aquidaba,aquidaban,aquidauana,aquidavana,aquier,aquihuiquichi,aquijita,aquijoma,aquil,aquila,aquila degli abruzzi,aquilaia,aquilambaya,aquilano,aquilaria,aquilastec,aquilastepec,aquilayoc,aquilea,aquileia,aquileja,aquiles serdan,aquiles stengel,aquilinia,aquilino,aquiliquilao,aquiliua,aquilla,aquillahuasi,aquillahuasi hacienda,aquillayoc,aquilonia,aquilpa,aquilpur,aquilue,aquimarco,aquimo,aquin,aquina,aquincha,aquine-mazouz,aquinhua,aquinit,aquino,aquinot,aquio,aquiperto,aquipo,aquira,aquiraz,aquisahuali,aquisimon,aquismon,aquitania,aquitas,aquiting,aquituna,aquituni,aquixman,aquixmon,aquixtitla,aquixtla,aquizzao,aqungdo,aqungok,aquone,aquran,aquz qanat,aquzi,aqvai,aqvarchapani,aqvazan,aqveran,aqverdi,aqweh,aqya,aqyar,aqyazi,aqyer,aqyrap,aqyrtobe,aqzevaj,aqzhal,aqzhar,aqzharyq,aqzhayyq,aqzhigit,aqziarat,aqziqarq,aqzuj,ar,ar bayangol,ar ghaif,ar gummo,ar hob,ar hob xiang,ar horqin qi,ar kalmi,ar kareh,ar kaweit salawa,ar khel,ar kohar,ar kubr,ar marashi,ar raisiyah,ar ras,ar ra`adi,ar ra`i,ar rab`ah,ar rab`i,ar rabab,ar rababah,ar rabad,ar rabagh,ar rabaghah,ar rabahiyah,ar rabam,ar rabasah,ar rabat,ar rabbah,ar rabbasah,ar rabbat,ar rabi,ar rabi`,ar rabi`ah,ar rabi`iyah,ar rabit,ar rabitah,ar rabitah al garbiyah,ar rabitah al gharbiyah,ar rabitah ash sharqiyah,ar rabiyah,ar rabu,ar rabu`,ar rabwa,ar rabwah,ar rabwah al `ulya,ar rabwah as sufla,ar rada`i,ar radadiyah,ar radah,ar radai,ar raddah,ar radha,ar radi`i,ar radidah,ar radif,ar radifah,ar radimah ash sharqiyah,ar radisiyah bahri,ar radisiyah qibli,ar radiyah,ar radm,ar radma,ar radmah,ar radsiyah qibli,ar radu,ar radwaniyah,ar rafai`,ar rafa`iya,ar rafah,ar rafay`ah,ar rafayi`,ar rafgha,ar rafi`ah,ar rafi`i,ar rafi`iyah,ar rafid,ar rafidhiya,ar rafidiyah,ar rafsa,ar rafshah,ar raga`i,ar raghah,ar raghamah,ar raghba,ar raghbah,ar raghdan,ar raghib,ar raghiyah,ar raha,ar rahab,ar rahabah,ar rahad,ar rahah,ar rahaminah,ar rahawi,ar rahbah,ar rahbi,ar raheba,ar rahhaliya,ar rahhaliyah,ar rahib,ar rahibat,ar rahibayn,ar rahibiyah,ar rahidah,ar rahim,ar rahm,ar rahmaniyah,ar rahmaniyah qibli,ar rahmaniyyah,ar rahmat,ar rahwah,ar rahyat,ar raidah,ar rajabat,ar rajabiyah,ar rajayibah,ar rajdiyah,ar rajib,ar rajif,ar rajish,ar rajmah,ar rajmi,ar rajubat,ar rak,ar rakab,ar rakah,ar rakakiyah,ar rakh,ar rakhmi,ar rakibah,ar rakiyat,ar rakka,ar rakkah,ar rakoab,ar ram,ar rama,ar ramad,ar ramadah,ar ramadi,ar ramadi bahri,ar ramadi qibli,ar ramadiyat,ar ramah,ar rami,ar ramilat,ar ramiyah,ar raml,ar ramlah,ar ramliyah,ar ramsah,ar ramtaniyah,ar ramtha,ar ramthaniyah,ar ramut,ar ramyah,ar rank,ar ransiyah,ar raqab,ar raqabah,ar raqaqinah,ar raqaqinah bi ash shawahin,ar raqayif,ar raqiwah,ar raqmah,ar raqq,ar raqqah,ar raraba,ar rasafah,ar rasas,ar rashaidah,ar rashadah,ar rashadiyah,ar rashawiyah,ar rashidah,ar rashidi,ar rashidiyah,ar rasif,ar rasifah,ar rass,ar rastan,ar rastin,ar raswah,ar ratawi,ar rathim,ar ratikh,ar rattawi,ar ratuniyah,ar raudha,ar raunah,ar rawais,ar rawabi,ar rawajhiyah,ar rawamah,ar rawaqah,ar rawash,ar rawashid,ar rawashidah,ar rawashidiyah,ar rawatib,ar rawayd,ar rawd,ar rawdah,ar rawdah ash shamaliyah,ar rawgh,ar rawghah,ar rawhiyah,ar rawi,ar rawiyah,ar rawjal,ar rawkab,ar rawn,ar rawnah,ar raws,ar rawshah,ar rawshan,ar rawuk,ar rawwa,ar raya,ar rayadi,ar rayah,ar rayan,ar rayana,ar rayayinah,ar rayayinah al mu`allaq,ar rayayinah bi al hajir,ar rayaynah al mi`allaq,ar raydah,ar raydaniyah,ar rayhah,ar rayhan,ar rayhanah,ar rayhani,ar rayhaniyah,ar rayidi,ar rayn,ar rayramun,ar raysh,ar rayth,ar rayy,ar rayya,ar rayyan,ar rayyanah,ar rayyar,ar rayyath,ar razaniyah,ar razazah,ar raziqiyah,ar razzazah,ar recho,ar reina,ar rejdane,ar rhib,ar ri`,ar ribat,ar ribat as saghir,ar ridah ash sharqiyiah,ar ridahah,ar ridan,ar rides,ar ridifah,ar ridisiyah bahari,ar ridisiyah qibli,ar ridwaniyah,ar rifa,ar rifa`,ar rifa` al gharbi,ar rifa` ash shamali,ar rifa` ash sharqi,ar rifa`i,ar rihab,ar rihabah,ar rihabi,ar rihan,ar rihani,ar rihaniyah,ar rihat,ar rihayt,ar riheiwa,ar rihib,ar rihiya,ar rihiyah,ar rija`,ar rijah,ar rijal al aroua,ar rijat,ar rikab,ar rikabiyah,ar rikabiyah al jadidah,ar rimah,ar rimali,ar rimayah,ar rimiyah,ar rimmah,ar riqab,ar riqqah,ar riqqah al bahriyah,ar riqqah al gharbiyah,ar riqqah ash sharqiyah,ar risa nawsan,ar risan,ar rishadah,ar rishadin,ar rishah,ar rishah utmah,ar rishawiyah,ar rissai,ar riwaq,ar riyad,ar riyadh,ar riyaf,ar riyamah,ar riyan,ar rizimat,ar rizqah,ar rizwah,ar robaal,ar rodha,ar rogha,ar rommani,ar roseris,ar rownah,ar ruays,ar ruaysah,ar ruus,ar ru`ah,ar ru`at,ar rua`at,ar rub`,ar rub`ah,ar ruba`iyah,ar ruba`iyin,ar rubad,ar rubah,ar rubat,ar rubayiyah,ar rubay`ah,ar rubaydhah,ar rubayl,ar rubbah,ar rubdan,ar rubian,ar rubiyat,ar rubshan,ar rubu`,ar rudaydah,ar rudayfah,ar rudaymah,ar rudaymah ash sharqiyah,ar rudayyif,ar rudduf,ar rudud,ar rufa`iyat,ar rufayq,ar ruff,ar ruffah,ar rufi`,ar rughbah,ar ruhabat,ar ruhaiya,ar ruhaniyah,ar ruhaybah,ar ruhaybat,ar ruhaybiyat,ar ruhayd,ar ruhaymah,ar ruhaymiyah,ar ruhayyah,ar ruhayyat,ar rujaydah,ar rujban,ar rujim,ar rujm,ar rujmah,ar rujum,ar rukab,ar rukaib,ar rukayb,ar rukayzi,ar rukba,ar rukbah,ar rukh,ar rukhaymi,ar ruknah,ar rukubah,ar rulays,ar rumadiyat,ar rumah,ar rumaidh,ar rumaila,ar rumani,ar rumaydah,ar rumayh,ar rumayl,ar rumaylah,ar rumaylat,ar rumayliyah,ar rumaymin,ar rumaytha,ar rumaythab,ar rumaythah,ar rumaythiyah,ar rumhiyah,ar rumi,ar rumiyah,ar rumman,ar rummanah,ar ruq`ah,ar ruq`i,ar ruqaiqa,ar ruqaybah,ar ruqaybah gharb,ar ruqaytah,ar ruqayyiqah,ar ruqqah,ar rusafah,ar rusayfah,ar rusaynm,ar rusayris,ar rusays,ar rushaydah,ar rushaydiyah,ar rushda,ar rustamiyah,ar rustaq,ar rutbah,ar rutrutaya,ar ruwa,ar ruwais,ar ruwan,ar ruwanah,ar ruwaq,ar ruwas,ar ruway`,ar ruwayd,ar ruwaydah,ar ruwaydat,ar ruwayhah,ar ruwayhan,ar ruwayhib,ar ruwayjat,ar ruwaymah,ar ruwaymiyah,ar ruwaynah,ar ruways,ar ruwayshid,ar ruyaydah,ar ruzayqat,ar ryad,ar sciavel,ar shavel,ar shevel,ar uein,ar ueir,ar uen,ar xangd sum,arara,arhaiya,aryadi,aryu,ar-aro,ar-aro-o,ar-arume,ar-arumpang,ar-arusip,ar-asgat,ar-dzhargalant,ar-keryulchi,ar-khara,ar`utseru,ara,ara akbar shah,ara ali,ara ameh,ara ara aramgoui,ara ara harikouna,ara ararsa,ara arba,ara bacale,ara bacalle,ara bala,ara bayan gol,ara bele,ara bisciaraila,ara bisciaraitu,ara bonel,ara bridge,ara bujaq,ara bulag suma,ara bulag sumu,ara bulagiin dugang,ara buluk sume,ara bure,ara buri,ara chzhirgalantu,ara colla,ara djirgalantu,ara fanna,ara gabana,ara goro,ara horoiin hiid,ara illibir,ara jafar,ara jasrota,ara jirgalanta hiid,ara jirgalanta suma,ara jirgalantu,ara jirgalantu sumu,ara jirugarantu,ara koy,ara khoroyn khid,ara khoryyn khid,ara kuda,ara lughei,ara mansur,ara more,ara mumed,ara munna,ara north,ara oda,ara oyo,ara poshtah,ara qal`eh,ara qui,ara rancheria,ara rendang,ara samantar,ara samantar batare,ara samanter,ara suru,ara te lumit,ara teeliin dugang,ara terra,ara tula,ara tulla,ara unarshi dugang,ara unne,ara unurchi dugan,ara,araar,araaradawa,araarauna,araava,araia,araou,ara-alsagat,ara-altsagat,ara-arla,ara-bulag,ara-bulagyyn dugang,ara-bulak,ara-bulak somon,ara-dzhirgalantu,ara-guba,ara-guba port,ara-ijero,ara-ilya,ara-kharlan,ara-khoerkhe,ara-kiret,ara-kuret,ara-orin,ara-unurshi-dugang,ara`ar,araa,araabazary,araaira,araal,araan,araara,araararedja,araarareja,arab,arab abdulla,arab abu nawas,arab arkuba,arab bai,arab basti,arab dizeh,arab el `aligat,arab el kallahin,arab el kharabe,arab el-fawarsa and el-` itwa,arab gamya,arab hasan ad duwaza,arab jeheiche,arab juwamer,arab kehan,arab khan,arab khodzha,arab khu,arab khuhara,arab kol,arab kosh basti,arab kot,arab kundak,arab makhle,arab mohano,arab oucharhi,arab paryar,arab peunar,arab phul,arab qeslag,arab qishlaq,arab qubali,arab sakh,arab shah bagh,arab shah-i-payan,arab shakhverdi,arab solangi,arab sumaro,arab tcheurdouk,arab uskur,arab virane,arab yengica,arab yengidzha,arab zuhra,arab-alli-mamed,arab-aul,arab-babaly,arab-band,arab-bant,arab-dzhabirly pervoye,arab-e gaymishi,arab-kala,arab-kardashbeyli,arab-khana,arab-kubali,arab-kyukel,arab-narkuza,arab-parguza,arab-parkuza,arab-shakhberdy,arab-shakhverdy,arab-tyube,araba,araba birni,arabaalan,arabaalani,arabab,arababad,arabacayi,arabacayi koyu,arabach,arabachy,arabachylar,arabaci,arabacibag,arabaciboz,arabacibozkoy,arabacilar,arabacimusa,arabacis,arabadsjauwa,arabadzhi,arabadzhievo,arabadzhii,arabadzhilar,arabagi,arabagilar,arabaka,arabaketsu,arabakonagi,arabala,arabale,araban,arabana,arabanbeydili,arabanci,arabangi,arabangui,arabap,arabari,arabaria,arabasi,arabat,arabatan,arabate,arabato camp,arabatti,arabatuk,arabaux,arabayatagi,arabayona,arabba,arabbabirkhanly,arabband,arabbasra,arabbazarly,arabboda,arabcha,arabchakh,arabdura,arabdzhabirli pervoye,arabdzhabirli vtoroye,arabe,arabe awolugu,arabe ayelibia,arabebe,arabeh,arabejo,arabela,arabella,arabelo,arabene,arabestan,arabete,arabeto,arabhaci,arabi,arabi kali,arabi kili,arabia,arabiaca,arabian acres,arabian gardens mobile home park,arabiat,arabico,arabie,arabil,arabilla,arabin,arabio kili,arabishi,arabjo,arabka,arabkadim,arabkadym,arabkalay,arabkaradagly,arabkardashbeyli,arabkayevo,arabkend,arabkha,arabkhana,arabkhel,arabkheyl,arabkhonai kalon,arabkhu,arabkir,arabkubaly,arabkuduk,arabkyshlak,arabkyyasly,arabla,arablar,arable,arabli,arablinskiy,arablinskoye,arablya,arablyar,arabmazar,arabmazari,arabmazor,arabmehdibay,arabmekhdibey,arabmekhdybey,arabmekhtibek,arabmekhtibeyli,arabmozor,arabnarti,arabniki,arabo,arabocagi,araboda,arabodda,arabodzhagy,arabokka,arabokke,araboladi,arabon,araboo,arabopo,araborladit,arabosi,arabou,arabovka,arabpur,arabqadim,arabrot,arabsahverdi,arabsaki,arabsalbas,arabsaray,arabsarvan,arabshakheyl,arabshakhverdi,arabshalbash,arabsheki,arabsiyo,arabugi,arabuk,arabuka,arabuko,arabulafia,arabunan,arabungong,arabusagi,arabushagy,arabuste,arabuta,arabxana,araby,araby view,arabyenice,arabygdi,arac,araca,araca de baixo,aracachi,aracachi aillo,aracacu,aracadzor,aracagi,aracai,aracaiba,aracaicu,aracaji,aracaju,aracak,aracal,aracaldo,aracalong,aracana,aracarariguama,aracari,aracariguama,aracas,aracassu,aracat,aracataca,aracati,aracatiacu,aracatiara,aracatiba,aracatu,aracatuba,aracaty,aracay,aracay norte,aracazinho,arace,araceli,aracelia,aracena,arach,arach pakle,arach,arachaandi,arachadzor,arachanamakhi,arachandi,arachari,arachchigama,arachchikanda,arachchikattuwa,arachchikulam,arachchikumbura,arachchimulla,arachchivillu,arache,araches,arachhari,arachi,arachildu,arachipo,arachitanguio,arachongdan,arachos,arachova,arachtschino,arachzhargalanta khid,arachzhargalanta somon,araci,aracici,aracine,aracinovo,aracitaba,arackkomo,aracoiaba,aracoiaba da serra,aracoma,aracondong,aracoyaba,aracruz,aracs,aracu,aracua,aracuai,aracuara,aracui,aracungkit,aracupeva,aracure,aracy,arad,arad puszta,arad-ciala,arad-parguza,arada,aradac,aradagun,aradaha,aradalen,aradan,aradas,aradatz,aradaz,aradchildu,arade,aradec,aradeiba,aradeo,aradeq,aradere,araderikh,araderix,aradeti,aradh,aradha,aradhaina,aradhe,aradhi summar,aradhiou,aradhipara,aradhippou,aradi,aradi hubbak al kidhar,aradi shubbak al khidr,aradi summar,aradichevo,aradilla,aradillas,aradillos,aradin,aradina,aradinum vetus,aradiou,aradip,aradip el achra,aradippou,aradippu,aradirikh,aradita,araditas,araditos,aradjanggot,aradkovi,aradnikai,arado,arado grande,aradona,aradong,arados,aradpuszta,aradsil,aradu,aradu nou,araduenga,aradukva,aradul nou,aradvanypuszta,aradyk,aradyu,aradzhargalanta khid,aradzhargalanta-somon,aradzhargalantu khid,aradzi,arae bigginae,araeachan,araeangan,araeat,araebaesil,araebaetti,araebagam,araebaktal,araebamni,araebangganmal,araebangganmaul,araebeomsil,araebikkinae,araebikkunbaengi,araebodong,araebol,araebomgol,araebomsil,araebuchun,araebuhungni,araebusuam,araechamnamusil,araechanggol,araechangmal,araechangmaul,araechangnae,araechinbul,araecholkol,araechongdan,araechongdok,araechongmaru,araechongol,araechongyong,araedaesil,araedambau,araedamuri,araedarigol,araedeokgol,araedibam,araedmal,araedoegol,araedokcholli,araedokchong,araedokkol,araedoldam,araedoltam,araedomachi,araedong,araedongbakkol,araedongol,araedugik,araedulgol,araedunmori,araedwangchon,araedwirun,araedyangmyong,araedyangwolli,araedyongho,araedyongyonggae,araedyorwolli,araedyosaengi,araegaehwa,araegaehwat,araegajang,araegaktan,araegalchomni,araegalgol,araegamaeul,araegamaul,araegandamdong,araegangsanbong,araegapsangol,araegaraegol,araegaraejae,araegaragol,araegarani,araegarusil,araegasimakkol,araegi,araegijae,araegingol,araegol,araegolmi,araegolpigi,araegomundong,araegonchon,araegongae,araegosol,araegubi,araeguji,araegumchon,araegumdan,araegumsong,araegumsumul,araegumumbau,araegwangchi,araegwangchon,araegwangtalli,araegwanto,araehachi,araehaengjong,araehail,araehaksa,araehansil,araehoumsa ri,araehoumsoeri,araehoumsori,araehunae,araehungbu,araehurisil,araehwajang,araehwajon,araehwanggol,araehwangi,araehwangjeong,araehwangjong,araeillyun,araeiraesil,araejajingae,araejamiwon,araejangbat,araejanggol,araejanggu,araejangkol,araejaposil,araejawiri,araejedong,araejeotgae,araejigyongnae,araejinbul,araejokkae,araejolgol,araejom,araejongmaru,araejongsanpo,araejonuisil,araejulam,araejumigol,araekapsankol,araekkae,araekkaegol,araekkajim,araekkajonggi,araekkalgi,araekkalmae,araekkamasil,araekkamaul,araekkamunae,araekkarigol,araekkobuk,araekkoin,araekkol,araekkolmal,araekkomsil,araekkondure,araekkongi,araekkosigol,araekkujip,araekkulmogi,araekkumul,araekkuryang,araekkwaho,araekkwanam,araekkwau,araekkwinae,araekomul,araekosmun,araekuji,araekwanto,araela,araemaenam,araemaengi,araemal,araemangjimi,araemangwol,araemanjisan,araemanmok,araemaul,araemirachi,araemirat,araemireuk,araemiruk,araemoejae,araemoeruni,araemojong,araemok,araemongni,araemulssogi,araemuralli,araemuranni,araemusul,araen,araenaemak,araenakpung,araenamgyo,araenaragol,araenma,araenmal,araenmalgori,araenmalmori,araenmangwol,araenmanmok,araenmatchil,araenmaul,araenmojil,araenmokkol,araenmongni,araenmongomogi,araenmosan,araenmunjong,araennabang,araennangwol,araennobunni,araennoburi,araennodang,araennongjang,araennulti,araenobungnae,araenochae,araenomnampo,araenongol,araenongso,araenopunto,araenunggol,araeogap,araeojaksil,araeppaeguni,araeppaehae,araeppaektong,araeppaenae,araeppaenamugol,araeppaewon,araeppallyong,araeppamnamugol,araeppamwon,araeppangasil,araeppangdu,araeppangsalmi,araeppikkine,araeppirisil,araeppol,araeppomsil,araepporibat,araeppujo,araeppurugae,araepujo,araes,araesabong,araesaechong,araesaeto,araesaetto,araesagimak,araesagunae,araesail,araesaji,araesambae,araesandae,araesangjaewon,araesansugol,araesatkol,araesayo,araesejuwon,araeseongneup,araesinamgol,araesindari,araesodanggol,araesoebat,araesoeri,araesoksaeul,araesongdong,araesonggol,araesongnup,araesori,araessadun,araessaebyol,araessaejabi,araessaejae,araessaeudae,araessagiso,araessajanggol,araessajong,araessalmi,araessalmok,araessanae,araessandu,araessanjeon,araessanjon,araessegeori,araessegori,araessepyong,araesshinchon,araessingori,araessobusil,araessokkae,araessomun,araessonbawi,araessonggang,araessongmi,araessongnam,araessori,araessorim,araessorisil,araessosae,araesugol,araesumijong,araesumunari,araesungjang,araesurumjae,araetaeoji,araetambau,araetapkori,araetappul,araetbaenae,araetbamnamugol,araetbureugae,araetchae,araetchaesa,araetchagaeul,araetchagusil,araetchajolli,araetchaksil,araetchamosil,araetchangbat,araetchangson,araetchangto,araetchidul,araetchindori,araetchiryangi,araetchom,araetchongdong,araetchulgol,araetchungmal,araetchuo,araetdaesil,araetdakbatgol,araetdam,araetdarigol,araetdobongi,araetdumeori,araetdumil,araetdumuri,araetdurisil,araetgamasil,araethalmok,araethanduri,araethumsil,araetjagaeul,araetjaksil,araetjiryangi,araetjueo,araetkalbol,araetkol,araetmaeul,araetmal,araetmokgol,araetochanggol,araetoekol,araetogol,araetokchang,araetokchom,araetonmori,araetopyong,araetpaehae,araetpyongdaengi,araetsokkae,araetsorisil,araettaeri,araettaesil,araettakpakkol,araettam,araettang,araettarakkol,araettarigol,araetteum,araetto,araettobongi,araettobuk,araettoejae,araettogasil,araettojanggol,araettokchongi,araettokkol,araettomachi,araettongmak,araettosimi,araettudae,araettum,araettumil,araettumori,araettumuri,araettundun,araetturisil,araetturuni,araetyonghari,araeungdalmal,araeungol,araeyesil,araeyeuigol,araeyomsugol,araeyongmori,araeyongsogol,araeyonmokkol,araf,araf shah kili,arafa,arafali,arafan,arafat,arafejojo,arafo,arafranca,arafsa,araft,arafu,arafune,arag,araga,aragachi,aragaga,aragaingina,aragaki,aragama,aragamae,aragamaye,araganatal,aragao,aragarcas,aragasane,aragastakh,aragats,aragatsotn,aragawa,aragawamura,aragay,aragba,aragba-okpe,aragbe,aragbiri,arage,arageris,aragesa,araggio,araghdeh,aragheccia,araghi,araghol,aragi,aragich,aragil,aragipa,araglin,aragno,aragnouet,arago,aragoda,aragodshi,aragodzh,aragoiania,aragol,aragominas,aragon,aragon inguaran,aragon place,aragona,aragona village,aragoncillo,aragones,aragoneses,aragonite,aragosa,aragostay,aragoto,aragozena,aragtogba,aragu,aragua,aragua de barcelona,aragua de maturin,araguabisi,araguacema,araguaci,araguacu,araguaia,araguaiana,araguaimujo,araguaina,araguainha,araguan,araguana,araguao,araguar,araguari,araguary,araguas,araguas del solano,araguasa,araguata,araguatins,araguato,araguatos,araguay,araguaya,araguenos,aragues del puerto,araguiana,araguita,araguita abajo,araguita arriba,araguito abajo,aragure,aragusuku,aragva,aragyugh,aragyukh,arah,arah dhok,arah din,arah pushtah,arah thnot,arah thout,arah tunggal,arahal,araham,arahama,arahan,arahan-kidul,arahanana,arahiri,arahiwala,arahiya,arahki,arahku,arahnas,araho,arahos,arahouei,arahtaoe,arahuai,arahuante,arahuay,arahuetes,arahura,arai,arai point,arai`ra,araia,araian,araian di basti,araianwala,araianwala dera,araianwali,araiat,araibari,araibaria,araibe,araibeki,araibel,araibi,araica,araidanga,araidna,araidron,araih,araihazar,araiin huryee,araijilla,araijuku,araikiwala,arail,araim,arain,arain chak sharki,arain chak sharqi,arain jo goth,arain khure,arain majra,arain punjabi di basti,arain shargi,arain sharqi,arainaguo,arainan,araining,arainj-laka-punga,arainwahan,arainwala,arainwali,araioi,araioses,araipallpa,araipallpa pueblo,araipara,araipattai,araiporanga,araira,arairou,araisat,araisi,araisidha,araisprasad,araisu macitaja muiza,araiteva,araitho,araito,araiu,araiuama-quen,araiuana-quen,araiuma-quen,araiura,araiyan,araiyanwala,araiza,araj,arajali,arajan,arajang,arajanggot,arajara,arajat,arajay,arajay hacienda,arajfiniti,araji,araji amdabad,araji bahirchar ghoshkati,araji barkhadia,araji borkhadia,araji gariana,araji gobindpur,araji jinapur,araji kalikapur,araji musuria,araji naya naobhanga,araji osmanpur,araji paikhati,araji sahapur,araji sibanandapur,araji sree nagar,araji srinagar,araji umedpur,arajkund,arajo,arajoel,araju,arajuno,arak,arak el faki hasan,arak tenot,arak thnot,arak tignari,arak vaz-e malek shahi,arakeli,araks,araksavan,araka,arakaba,arakabesan,arakachi,arakachom,arakagoda,arakaka,arakaki,arakali,arakan,arakani,arakansongkur,arakantsev,arakany,arakapa,arakapas,arakapi,arakarae,arakarak,arakas,arakash,arakaso,arakasoaol,arakavus,arakawa,arakawa fishery,arakawa-gyojo,arakawa-mura,arakawa-oki,arakawaguchi,arakawahama,arakawila,arakay,arakayeva,arakayevo,arakcheyevo,arakchina,arakchino,arake,arakebu,arakeke,arakeri,arakh,arakhamitai,arakhamitais,arakhamites,arakharwar,arakhchino,arakhera,arakhidzha,arakhindzha,arakhley,arakhlo,arakhnaion,arakhon,arakhos,arakhova,arakhovitika,arakhovitsa,arakht,arakhta,arakhtakh,arakhveti,araki,arakich,arakichi,arakil,arakilise,arakin,arakit,arakkonam,araklar,araklari,arakli,arakli carsusu,araklicarsisi,arako,arakohama,arakoi,arakonak,arakoniekondre,arakoon,arakora,arakoy,arakp,arakpa,araksan,araksbo,arakse,arakste,araksti,araksuolo,araku,arakul,arakumae,arakumyae,arakundo,arakura,arakuri,arakva,arakwai outstation,arakwala,araky,arakyala,arakyul,arakyulya,aral,aral banda,aral pervoye,aral pervyy,aral sea,aral tyubya,aral vtoroye,aral,aralsk,aralskiy,aralskoye,aralskoye more,aral-bay,aral-tyube,aral-vakhym,araladaro,aralaeng,aralagash,aralagodu,aralai,aralakh,aralan,aralanwala group,aralaul,aralbay,aralbayeva,aralbayevo,aralbayevskiy,aralcha,aralchi,aralda,aralde,arale,aralek,aralel nait sidi hammou,aralgundgi,arali,arali east,arali north,arali south,arali west,aralia,aralia srimantapur,aralicak,aralichev,araliin dugang,aralik,aralikkutan,aralikoz,aralkol,aralkol,aralkum,aralla,arallae,aralle,arallu,arallu-ye bozorg,arallu-ye kuchek,aralmaahatlar,aralmakiran,aralqi,aralshiy,aralskoye,aralsol,aralsor,aralsulfat,araltal,aralta\302\277ba\302\277,aralta\302\277pa\302\277,aralta\302\277pa\302\277ka\302\277uduka\302\277,araltobe,araltogay,araltoghay,araltopakuduk,araltope,araltopequduq,araltugay,araltyube,araluen,araluen west,aralugahawewa ihala,aralugahawewa pahala,aralupitiya,aralutalawa,araluwinna,araly,araly east,araly north,araly south,araly west,aralyin dugang,aralykh,aralyyn dugang,aram,aram bagh,aram bulaq,aram estala,aram kan,aram khan,aram khel,aram kheyl,aram nerow,aram nerow-e bala,aram nerow-e pain,aram shahr,aram-aram,aram-kuardak,aram-kurbak,aram-kurdak,aram-zangi,arama,aramabah,aramac,aramachi,aramachon,aramaganori,aramaika,aramal,araman,aramandakatuwa,aramandakotuwa,aramanduba,aramango,aramangoda,aramanha,aramani,aramanu,aramara,aramarakarakkuru,aramarakarakuru,aramare,aramari,aramary,aramasa,aramashevo,aramashka,aramashkovskoye,aramasu,aramata,aramato,aramawayan,aramaya,aramayuan,aramaywan,aramba,arambagama,arambagh,arambakam,arambakkam,arambala,arambare,arambasici,arambaya,arambaza,arambe,arambegama,arambegodella,arambegrama,arambepola,aramberri,arambi,arambi masezai,arambol,aramboli,aramboly,arambu,arambuiyeh,aramchigol,aramchukchi,aramd,aramda,arame,aramea,aramecina,arameleva,aramelevka,aramendia,aramenha,arameras,aramerasi,aramesti,aramesti-boeresti,aramesti-razesi,aramgah,aramgay,aramghay,arami,aramil,aramil arriba,aramil de arriba,aramili,aramilskoye,aramina,aramine,araminita,aramir,aramirim,aramiro,aramis,aramits,aramiyah,aramkan,aramkhana,aramkhel,aramkheyl,aramkheyl,aramkola,aramme,aramo,aramoaia,aramoana,aramogoytuy,aramoho,aramoko-ekiti,aramon,aramonagolla,aramoun,aramouza,arampampa,arampola,arampur,arampura,aramra,aramsha,aramta,aramtalla,aramuapa,aramularo,aramundi,aramunt,aramus,aramut,aramutaro de luque,aramwar,aramzai,aramzay,aramzi,aran,aran 1,aran 2,aran areh,aran arrei,aran kandiara,aran number 2,aran number1,aran orin,aran pradhesa,aran pradhet,aran prathet,aran va bid gol,aran zangi,aran-aran,aran-aran kulon,aran-arekh,aran-ketau,arana,aranagar,aranagatal,aranan,aranane,aranantu,aranapuruohu,aranarache,aranas,aranas east,aranas sur,aranat,aranau,aranaya,aranayaka,aranayake,aranaz,arana\302\221el,aranboura,aranc,arancai,arancata,arancay,arance,arancedo,arancheyevo,aranchi,aranchi nomer tretiy,aranchi treti,aranchi-besh-sary,arancibia,arancon,arancou,arand,aranda,aranda de duero,aranda de moncayo,arandahi,arandai,arandak,arandan,arandara,arandas,aranday,arandelovac,arandelovo,arandi,arandia,arandia kalan,arandiga,arandigoyen,arandilla,arandilla del arroyo,arandin,arandis,arandkhera,arandojo,arandon,arandu,arandu kalay,arandu kelay,arandu lasht,arandukalay,arandun,arandusar,arane,araneag,aranemol e lur,aranets,arang,arang arang anchorage,arang zhawar,arang-arang,aranga,arangabad,arangachha,arangai,arangala,arangalain,arangan,arangaon,arangarang,arangas,arangasar,arangash,arangastaakh,arangastakh,arangazh,arangdong,arangea,arangeh,arangel,arangen,aranggading,aranghel,aranghelul,aranghelul-buda,arangie,arangin,arangin-dinaratan,arangjelovac,arangkaa,arangkel,arango,arangocillo,arangpo,arangu,arangu bala,arangu pain,arangu-ye pain,arangua,aranguel,aranguie,aranguito,aranguiz,aranguman,arangunam,aranguno banda,arangurai,aranguren,aranguring,aranh,aranh chas,aranha,aranhas,aranhas de baixo,aranhas de cima,arani,arani jassoke,araniag,araniege,araniego,aranies,aranik,aranim,aranima,aranimamu,aranio,aranisi,aranitas,aranitasi,aranitesi,araniw,aranjan,aranji,aranjuecito,aranjuez,arankamajor,arankele,arankh,arankhola,arankon kunda,arankula,aranlah,aranlaoe,aranlau,aranli,aranlu,aranluengkee,aranmula,arann,arano,aranora,aranos,aranosa,aranovo,aranpitiya,aranqueda,aranquel,aranra,arans,aransa,aransakhay,aransakhoy,aransas pass,aranshi,aransis,aranta-meru,arantangi,arantas,arantash,arante,arantepacua,arantes,arantila,arantina,arantiones,aranton,arantot,arantur,aranu,aranuel,aranui,aranutovici,aranvayal,aranwala,aranwela,arany,aranya,aranya pradesa,aranya pradhesa,aranyag,aranyapasa,aranyaprathet,aranyapur,aranyegyhaza,aranyhegy,aranykert,aranyod,aranyos,aranyos-puszta,aranyosapati,aranyosapati tanya,aranyoscsarda,aranyosd,aranyosdomb,aranyosfurdo,aranyosgadany,aranyosi foldek,aranyosi szollo,aranyosi szollotelep,aranyosi tanya,aranyosimajor,aranyosipuszta,aranyoskastely,aranyosmajor,aranyospuszta,aranyossziget,aranyostanya,aranysziget,aranyto,aranytotanya,aranz,aranza,aranzami,aranzamin,aranzazu,aranzazu celaya,aranzo,aranzueque,arao,araob,araomi,araomoko ekiti,araos,araoua,araouan,araouane,araouei,araouey,araoueye,araoune,araovacik,araoz,arap,arap huyuk,arap i eperm,arap i poshtem,arap kili,arap kot,arap koy,arap koyu,arap makhle,arap-kala,arapa,arapaco,arapae,arapagi,arapagi-awlawkaw,arapagi-ejetu,arapah,arapaho,arapahoe,arapai,arapaj,arapaj comleka,arapaj e eperme,arapaj e poshteme,arapaj e poshtme,arapaj i eperm,arapaj i eperme,arapaj i poshtem,arapaja,arapal,arapali,arapampa,arapan,arapangama,arapanja,arapapa,arapara,araparen,arapari,araparicuaro,arapariquen,araparslanoglu,arapaslanoglu,arapata,arapate,arapattai,arapayung,arapbap,arapcasakarlar,arapcayir,arapcesme,arapcesmekoy,arapchuk,arapci,arapcici,arapciftligi,arapciftlik,arapdere,arapderekoy,arapderesi,arapderesi koyu,arapderesikoy,arape-rashbulli,arapei,arapela,arapema,arapetovka,arapey,arapgir,araphaci,araphuseyin,araphuseyin ciftligi,arapi,arapiccos,arapicos,arapidhes,arapikha,arapil,arapiles,arapindo,arapino,arapira,arapiraca,arapiranga,arapiri,arapis,arapison,arapito,arapixi,arapixy,arapkayev,arapkent,arapkey,arapkhana,arapkino,arapkir,arapkirler,arapkoy,arapkuyusu,araplar,arapli,arapliciftligi,araplii,araplikoyu,araplion,arapliusagi,arapliy,araplo,arapo,arapoema,arapoglu,arapohue,arapokina,arapolaka,arapolakka,araponga,arapongas,arapoo,arapora,araporanga,araporen,araposip,arapoti,arapoto,arapotta,arapov dol,arapovac,arapovaca,arapovace,arapovice,arapovichi,arapovici,arapovka,arapovo,arapozu,arappinar,arappinar koyu,arapsah,arapsardi,arapseki,arapseyf,arapseyfi,arapseyh,arapseyif,arapsio,arapsun,arapsuyu,araptepe,araptul,arapua,arapuca,arapue-ta,arapuey,arapuni,arapusa,arapusagi,arapusta,arapwa,arapy,arapyenice,araq-tavilah,araq-tawela,araq-towaylah,araqawai,araqdeh,araqi,araqolab,araqua,araquara,araquari,araqueda,araqueda hacienda,araquem,araquero,araquey,araquita,araqwala,arar,arar barar,arar gubetti,arar imba,arar lugole,arar mahala,arar yarey,arara,arara de baixo,arara ueine,araraboara,ararabohitra,ararac,araracuara,araragi,ararai,ararakura,ararampang,ararangua,ararapari,ararapira,araraquara,araras,araras de coemas,ararat,ararata,ararca,ararchi eggi,arardaha,arare,arareh,ararenda,ararhi,arari,arari khelanwala,araria,ararica,araricua,arariki,ararim,ararimu,arariouech,araripe,araripe de baixo,araripina,arariuna,ararius,ararki,araro,araro bhurgari,araro puti,araroba,araromi,araromi ajana,araromi aperin,araromi ayeka,araromi balogun,araromi ebi,araromi egba,araromi idera,araromi jiga,araromi logi,araromi okeodo,araromi osi,araromi owode,araromi owu,araromi tawpe,araromi-adofure,araromi-gbadegun,araromi-iyin,araromi-lisa,araromi-obu,araromi-odo,araromi-oke,araromi-oloba,araromi-opin,araromi-orita,araromi-tope,ararro,ararso areda,ararto boba,araru,araruama,araruna,ararunaquara,araruva,ararwala,arary,aras,aras de alpuente,aras kabu dua,aras kabu enam,aras kabu lima,aras kabu satu,aras kabu tiga,aras sembilan,aras-asan,aras-asun,arasa,arasaas,arasadichchenai,arasadichenai,arasaditivu,arasadzikh,arasadzikhi,arasadzykh,arasadzykh-dzhampazar,arasalu,arasam,arasan,arasanj,arasanj-e `olya,arasanj-e bala,arasanj-e pain,arasanj-e sofla,arasankalani,arasanli,arasanskiy,arasanwewa,arasanz,arasapallipalaiyam,arasara,arasato,arasavan,arasawa,arasaya,arase,arasgona-ye `olya,arash,arashakhu,arashenda,arashendi,arashi,arashico,arashika,arashima,arashinkai,arashiro,arashki,arashmane,arasho,arashouri,arasht,arasi,arasi hoomeh,arasie,arasikeri,arasikerili,arasinagundi,arasji,araskabu 1,araskabu 2,araskabu 3,araskabu 5,araskabu 6,araskan,araskhan,araskoy,araslambayevskiy,araslangulova,araslanova,araslanovo,araslaya,araslov,arasme,araso,arasoe,arasogut,araspanjang,araspar,arasparlu,arasque,arasque hacienda,arassa,arassamakhi,arasselva,arassie,arassoiaba,arassuahy,arassuai,arassui,arast,arasta bassal,araste,arasto,arastraville,arastu,arastul,arasun,arasur,arat,arat bon,arat e gjata,arat e mira,arata,arataca,aratahban,aratai,aratal,aratale bihi,aratali,aratama,aratan,aratana,aratankanpara,aratapu,arataye,aratchadzor,aratel n ait bihi,aratel nait sidi hammou,aratene,aratepe,arates,arath,arati,aratiba,aratichangio,araticho,aratico,araticu,araticum,aratinga,aratingauba,aratjondong,aratkend,aratkend pervyy,aratkend vtoroy,aratla,aratmadzha,aratmagea,aratmandsha,arato,aratoa,aratoc,aratoca,aratol,aratoque,aratores,aratorp,aratos,aratsa,aratskaya,aratskoye,arattana,arattanagoda,aratu,aratuba,aratuguyufa,aratuhype,aratuipe,aratula,aratumedilla,aratuna,aratunah,araturuk,araty,aratyu,aratz-erreka,aratz-machinventa,arau,arau 2,arau 3,arau number 1,arau number 2,arau number 3,arau village,araua,arauca,araucana,araucaria,arauco,araucumirim,araujo,araujos,araujuzon,arauke,araul,araules,arauli,araumajada,araumana,arauna,arauquita,araure,araurica,araurima,araurima abajo,araurio,arause,arauta-meru,arautana,arauti,araux,arauzo,arauzo de miel,arauzo de salce,arauzo de torre,arav,arava,aravaca,aravacik,aravaipa,aravakkurichchi,aravakumbura,aravan,aravandadu,aravanghat,aravankadu,aravans,aravar,aravawatte,aravaz-pelga,aravbarien,aravchek,aravere,aravet,aravete,araveu,aravi,aravia,aravida,aravilla,aravinquen,aravissos,aravito,aravito hacienda,aravjun,aravonitsa,aravsa,aravu,aravun,aravus,aravuse,aravuste,arawa,arawai village,arawain,arawakumbura,arawala,arawalai,arawali,arawali camp,arawan,arawane,arawata,arawatta,arawe,arawegoda,arawia,arawibisi,arawowo,arawsaya,arawu,arawum,arawun,arawwawala,arawwawela,araxa,araxanas,araxans,araxas,araxede,araxos,aray,araya,araya chica,araya chica hacienda,araya dilausan,araya grande,araya grande hacienda,araya lumbayao,araya-okawamachi,arayacho,arayamachi,arayan,arayanwala,arayanwali,arayaokpare,arayashiki,arayat,arayatli,arayatly,arayco,arayed,arayerou,arayez,arayk qishlaq,araykasy,arayon,arayoses,arayouro,arayourou,arayozes,araypallpa,araypallpa hacienda,arayshi,araytu,araz,araz `ali sheykh,araz dilagarda,araz gol,araz junction,araz mohammad akhund,araz mohammad khan,araz muhammad brohi,araz shah quli khan,araz taqan,araz zargyar,araza,arazafodri,arazand,arazane,arazani,arazap,arazato,arazatu,arazawa,arazbari,arazbary,arazdayan,arazede,arazee cheenatulee,arazeh,arazen,arazene,arazguni-ye `olya,arazguni-ye bala,arazguni-ye pain,arazguni-ye sofla,arazhpara,arazi,arazi amar singh,arazi amdabad,arazi bahar shah,arazi bahirchar ghoshkati,arazi baknai,arazi balarampur,arazi barkhadia,arazi bhabanipur,arazi bhawanipur,arazi chandpur,arazi chhur mall,arazi chinatuli,arazi chuhar mal,arazi daudpur,arazi dawdpur,arazi dhan singh,arazi dhok,arazi gariana,arazi gayespur,arazi gobindapur,arazi gokhurabad,arazi gujral,arazi hamid,arazi jadaunpur,arazi jalalpur,arazi janoha,arazi jinarpur,arazi junuhan,arazi kalikapur,arazi kasibritti,arazi khas,arazi lakhhati,arazi lal singh,arazi leppara,arazi maminpur,arazi mandalgati,arazi masnali,arazi mominpoor,arazi musuria,arazi naich,arazi naogaon,arazi narayanpur,arazi naya naobhanga,arazi naya noabhanga,arazi osmanpur,arazi paikhati,arazi paikpara,arazi par achlai,arazi pom,arazi radhakantapur,arazi rudrabana,arazi sadipur,arazi sahapur,arazi sajiara,arazi sara,arazi sarwai,arazi sekherpukur,arazi shahu khan,arazi shankar das,arazi sibanandapur,arazi srinagar,arazi sultan,arazi sultanpur,arazi tek chand,arazi tupipara,arazi umedpur,arazi wadhawa,arazi yaqub shah,arazigoyespur,arazin,arazinagar,arazinasar,arazler,arazli,arazoglu,arazuri,arazyakavak koyu,ara\302\261dama\302\261,ara\302\277an,ara\302\277s,arb,arb gebeya,arb`at,arba,arba amrane,arba aounate,arba ayacha,arba de ouaouila,arba des douirane,arba et qala,arba foukani,arba laazib midar,arba langeh,arba mench,arba minch,arba mintch,arba nairate,arba ouanekrim,arba reketi,arba sar,arba tincha,arba tachtani,arba tahtani,arba tahtania,arba tathani,arba tighedouiine,arba tighedouine,arba-aicha,arba-des-ida-ou-trouma,arba-ouaoula,arba`,arba` va akhmas,arba`a al kabir,arba`a as saghir,arba`a saghir,arba`ah saghir,arba`ah saghirah,arba`at al kubra,arba`at kubra,arba`ilu,arba`in,arbaa aayacha,arbaa du aounates,arbaa el kebir,arbaa es serhir,arbaa naairat,arbaa serhir,arbaa sghir,arbaan,arbab,arbab abdullah,arbab akbar,arbab balushahi,arbab bhatti,arbab colony,arbab dhani,arbab dino,arbab ebrahim,arbab fugar,arbab ibrahim,arbab jamali,arbab kalay,arbab kandi,arbab kelay,arbab khan,arbab khan unar,arbab khel,arbab kili,arbab lund,arbab machhi,arbab mawdud,arbab mowdud,arbab sattar kahar,arbab shafi muhammad,arbab solangi,arbab sultan tan bukero,arbab taj muhammad,arbab wali muhammad,arbab-akbar,arbab-e `olya,arbabat,arbabi,arbabkalay,arbabkhel,arbabkheyl,arbabkheyl,arbablar,arbacegui,arbach,arbachkhani,arbacha,arbaco,arbacoochee,arbad,arbada,arbadain,arbadayn,arbadinle,arbadzhievo,arbaea kebir,arbaea serhir,arbaer,arbagar,arbagastakh,arbagoma,arbagona,arbah kasmaz,arbai,arbai khere,arbain,arbaine,arbaita,arbaiza,arbajiyah,arbakaleh,arbakhan,arbakol,arbakoldy,arbakoly,arbakwai,arbakwe,arbal allah,arbala,arbalan,arbali,arbalia,arballio,arballo,arbalou,arbalou nserdane,arbam,arban,arban-a,arbana,arbanas,arbanasce,arbanasci,arbanasi,arbanasia,arbanaska,arbanaski do,arbanasko,arbanassi,arbanats,arbancon,arband,arbane,arbanes,arbani,arbani sharfi,arbanie,arbanieh,arbanies,arbanija,arbanus,arbanuse,arbanwala,arbany,arbaoua,arbaoua akbat el begor,arbaoua ghaba,arbaoua hamri,arbaouat,arbaoun,arbaouti,arbapai landai,arbapan,arbar,arbas,arbasanci,arbasetan,arbash,arbashevo,arbashi,arbashiki,arbastan,arbastan-e bala,arbastan-e pain,arbasy,arbat,arbat `olya,arbat dagh,arbat,arbat-e `olya,arbat-e sofla,arbatach,arbatache,arbatan,arbatax,arbatax di tortoli,arbatay,arbatelmisa,arbatskaya,arbatskiy,arbatt,arbaty,arbau,arbaua,arbaugh,arbavere,arbay,arbay heere,arbay hure,arbay khere,arbaya,arbayin hure,arbayin khure,arbayta,arbaz,arbaz-pelga,arbazh,arbazhankit,arbazhskiy lnozavod,arbe,arbe gona,arbein,arbea,arbea kbir,arbeca,arbecey,arbedo,arbee,arbeid adelt,arbeidsgenot,arbeidsloon,arbeidsvreug,arbeit,arbeiza,arbek,arbekovo,arbel,arbela,arbelaez,arbelera,arbelewali,arbellara,arbelunda,arben,arbenesh,arbeneshi,arbent,arbenyevka,arbeost,arberats,arberats-sillegue,arberg,arbergen,arberhutte,arberth,arbes ville,arbesau,arbesbach,arbespine,arbesthal,arbet,arbete,arbeteta,arbey cabdi,arbeyales,arbeytal,arbeytergeyn,arbeytfeld,arbeytgeym,arbi,arbi banda,arbi bila,arbi chachar,arbi drakhan,arbi ghebia,arbi kili,arbi muryani,arbia,arbianwala,arbiarek,arbiat,arbich,arbichi,arbici,arbid,arbidian,arbieto,arbigen,arbigieu,arbignieu,arbigny,arbigny-sous-varennes,arbihat,arbij,arbil,arbili,arbin,arbine,arbing,arbinka,arbinovo,arbinskiy,arbios,arbirlot,arbis,arbisbichl,arbisli,arbismen,arbitro,arbiwala,arbiya saghire,arbiz,arbizhal,arbizu,arbjerg,arbketal,arblade,arblade-le-bas,arblade-le-haut,arblay,arbnes,arbnez,arbnia,arbo,arbo abdi,arbo abis,arbo haro,arbob,arbobi,arbobkhotun,arbocco,arboces,arbochak,arbochi,arbocrest acres,arbod,arbodi,arboe,arboga,arbogen,arboheden,arbohiray,arbohult,arbois,arboite,arbol,arbol blanco,arbol de caucho,arbol del descanso,arbol grande,arbol sol,arbol sol ciudad,arbol solo,arbolada,arbolado,arbole,arboleas,arboleda,arboleda grande,arboleda sur,arboledas,arboledas de pedehue,arboles,arboles grandes,arboletas,arboletes,arboleya,arboli,arbolillos,arbolitas,arbolito,arbolitos,arboliz,arbolle,arbolova,arbolsol,arbolsolo,arbon,arbona,arbone,arbonga,arbonies,arboniye,arbonne,arbonye,arbor,arbor acres,arbor estates,arbor estates condominium,arbor forest,arbor greene,arbor grove,arbor heights,arbor hill,arbor meadows,arbor park,arbor pointe apartments,arbor ridge,arbor terrace,arbor trace,arbor vitae,arbor walk,arbor west,arbora,arboranda estates,arboras,arbordafia,arbordale,arbore,arborea,arboreda,arboree,arboretum,arborfield,arborg,arbori,arborio,arborland acres,arborn,arboro,arborview,arborville,arborwood,arborwood park,arbos,arbos ville,arboset,arboshiki,arbosho katsi,arbot,arbotchak,arbotten,arbouans,arboucave,arbouchatak,arbouet,arbouet-sussaute,arbouhat,arboulchatap,arbour green,arbour manor,arbour park,arbours,arbourse,arboussols,arboutchatak,arbouville,arbovale,arbow,arbowoheerow,arboyeti,arboz,arbra,arbre,arbre a limont,arbre broye,arbre park mobile home park,arbre saint-michel,arbre saint-pierre,arbrefontaine,arbrentin,arbrissel,arbroath,arbroth,arbshagen,arbsin,arbste,arbu,arbu buse,arbu sara,arbucias,arbuckle,arbuckle landing,arbues,arbugardan,arbuissonas,arbuissonnas,arbujuelo,arbulag,arbulici,arbulo,arbulu,arbuniel,arbur dasht,arburi,arbury hills,arbus,arbuschasko,arbusigny,arbusinka,arbuthnott,arbutine,arbutine vrpoljske,arbutus,arbutus beach,arbutus park,arbuy,arbuzinka,arbuzinskaya,arbuzinskiy,arbuzivka,arbuznoye,arbuzov,arbuzov baran,arbuzova shchilinka,arbuzove,arbuzovka,arbuzovo,arbuzovo-shchilinka,arbuzovskaya baza,arbuzovskiy,arbuzynka,arby,arbyn,arbyrd,arc,arc koyu,arc-ainieres,arc-en-barrois,arc-et-senans,arc-les-gray,arc-sous-cicon,arc-sous-montenot,arc-sur-tille,arca,arca del conde,arca dua,arca kotal,arca satu,arcabell,arcabuco,arcabuz,arcachon,arcadas,arcade,arcade acres,arcadia,arcadia bay,arcadia club,arcadia farm,arcadia heights,arcadia lakes,arcadia park,arcadia shores,arcadia south,arcadia west,arcadian,arcadian shores,arcadien,arcadio,arcadiopolis,arcado,arcadon,arcagak,arcah,arcahaie,arcahueja,arcais,arcaklar,arcalia,arcallana,arcamanik,arcambal,arcamon,arcamont,arcana,arcangel,arcangeles,arcangelo,arcangues,arcani,arcano,arcanu,arcanum,arcar,arcarazo,arcas,arcasos,arcassa,arcata,arcatamaya,arcatao,arcatu,arcaule,arcaute,arcawinangun,arcay,arcaya,arcaycocha,arcayos,arcazie,arce,arce rojas,arceau,arceburgo,arcediago,arcediano,arcegno,arcehkhak,arcehkhaki,arcektu,arcelet,arcelia,arcelia del progreso,arcelin,arcelino,arcellares,arcello,arcelot,arcen,arcenant,arcenay,arcene,arceneaux,arceniega,arcenillas,arcens,arceo,arcera,arces,arces-sur-gironde,arcesinlan,arcesti,arcesti-cot,arcestii-cot,arceto,arcetri,arcevia,arcevil yaylasi,arcey,arch,arch bridge,arch cape,arch hall,arch rock,arch sidi larroussi,arch spring,archadzor,archo,archa,archa kotal,archabal,archadino-chernushinskaya,archadinskaya,archadinskiy,archadzor,archagak,archagly-ayat,archah,archah khaki,archah kotal,archah kowtal,archahtu,archaia olympia,archaie,archail,archalai banda,archaly,archambault,archamps,archan,archananguan,archanderhama,archane,archang,archangan,archangel,archangelos,archangelsk,archangelskaja,archangelskoje,archani,archanoo,archaq,archar,archatu,archaty,archaud,archbach siedlung,archbald,archbell,archbold,archdale,arche,arched bow valley,archedino chernushenskiy,archedino-chernushinskiy,archedinskaya,archektu,archelane,archelange,archelles,archem,archemont,archena,archennes,archer,archer bluff,archer city,archer heights,archer siding,archers,archers corner,archers corners,archers fork,archers lodge,archers post,archerton,archertown,archery,arches,archettes,archeuli,archevan,archey valley,archez,archfeld,archi,archi kham,archia,archiac,archiane,archib,archibald,archibolds,archibong,archibongs,archica,archico,archid,archidiakonka,archidiene,archidjakonka,archidona,archidyene,archie,archies creek,archieston,archiestown,archignac,archignat,archigny,archikoi,archikoy,archiktu,archilla,archilo,archiman,archin,archinyak,archinyan,archina,archinabad,archinard,arching,archingeay,archino,archipitovka,archiras,archis,archita,archiud,archivan,archive,archivel,archiz,archizul nou,archkogel,archlebau,archlebov,archman,archnuiyeh,archo,archon,archongelungel,archontiko,archontikon,archontochori,archontochorion,archonwelz,archouk,archoun,archs,archshofen,archsum,archu,archug,archuinl hdeb,archuleta,archurieta,archut,archville,arci,arci kham,arcibiskupska pusta,arcibiskupsky lel,arcicollar,arcidava,arcidosso,arciechow,arciema muiza,arciema stacija,arciems,arcila,arcilaca,arcilas,arcilla,arcillas,arcillera,arcillo,arciman,arcimawicze,arcinas,arcine,arcinges,arciniega,arcins,arcio,arcis,arcis-le-ponsart,arcis-sur-aube,arcisse,arciunai,arcivan,arciz,arcizac,arcizac-adour,arcizac-ez-angles,arcizans,arcizans-avant,arcizans-dessus,arclais,arclin,arco,arco da calheta,arco de baulhe,arco de sao jorge,arco iris,arco pintado,arco verde,arcocha,arcola,arcola junction,arcole,arcomayo,arcomie,arcomon,arcomps,arcon,arcon park,arconada,arconate,arconcey,arconcia,arconciel,arconcillos,arcones,arconnay,arconsat,arconsey,arconville,arcopampa,arcopec,arcopodo,arcopongo,arcopunco,arcore,arcoro,arcos,arcos da condesa,arcos de canasi,arcos de jalon,arcos de la cantera,arcos de la frontera,arcos de la polvorosa,arcos de la sierra,arcos de las salinas,arcos de valdevez,arcos de vale de vez,arcos de valle de vez,arcosanti,arcosawangan,arcosso,arcot,arcott,arcou-djerine,arcoulant,arcoules,arcova,arcove,arcoverde,arcoz,arcozello,arcozelo,arcozelo da torre,arcozelo das maias,arcozelo do cabo,arcso,arctic,arctic bay,arctic red river,arctic springs,arctic village,arctika,arcturas,arcturus,arcuales,arcuda,arcueil,arcugi,arcugnano,arcugowo,arcuilla,arcuk,arcumbi,arcumeggia,arcus,arcusa,arcy,arcy-sainte-restitue,arcy-sur-cure,ard,ard `ali,ard `ali-ye chavoshi,ard al hamra,ard al husayn,ard al wata,ard al-raidah,ard al-raydah,ard al-riyafah,ard al-siqair,ard amirah,ard an ratha,ard ar raydah,ard bo,ard bou torbey,ard bu turbayh,ard crossroads,ard el hamra,ard el hssein,ard el ouata,ard fhearta,ard fhionain,ard ja`din,ard kola,ard mudaysis,ard of tongue,ard ouata,ard raithin,ard tell el malah,ard-ar-rum,ard`ali,arda,arda dadaja,ardabag,ardabas,ardabask,ardabil,ardabila,ardabilak,ardabilaq,ardabilya,ardabin,ardabog,ardabyevo,ardach,ardad,ardadkalay,ardaf,ardag,ardaga,ardagan,ardaganchik,ardagger,ardagger markt,ardagger stift,ardagh,ardaghan,ardaghy,ardagon,ardagusena,ardagysh,ardah,ardaha,ardahan,ardaheh,ardahin,ardai,ardai kidul,ardai lor,ardaijaya,ardaiz,ardajan,ardajin,ardak,ardakan,ardakhan,ardakool,ardakshan,ardakul,ardal,ardala,ardalan,ardaland,ardalenga,ardales,ardali,ardalnaig,ardalook,ardalstangen,ardaluk,ardam bon,ardamah,ardamashka,ardameh,ardamene,ardamin,ardamore,ardamova,ardan,ardan yaylasi,ardana,ardanaz,ardangah,ardanio,ardanion,ardanj,ardanovce,ardanove,ardanovo,ardanovon,ardans,ardanuc,ardanza,ardaos,ardappen,ardapuszta,ardapy,ardaq,ardar,ardara,ardaragh,ardarat,ardarn,ardarragh,ardas,ardasa,ardasavi,ardasen,ardashar,ardashevi,ardashevo,ardashevo polskoye,ardashevskiy,ardashi,ardashikha,ardashil,ardashir,ardashli,ardasiwala,ardasova,ardassa,ardasubani,ardath,ardatov,ardatovka,ardatovo,ardatovskiy,ardattin,ardauli,ardava,ardavasar,ardavidu,ardavilaq,ardawa,ardawata,ardawata ki dhani,ardawel,arday,ardbeg,ardbennie,ardboe,ardcahan bridge,ardcanaght,ardcath,ardcharnich,ardchonell,ardchonnel,ardchonnell,ardclach,ardcost,ardcrony,arddleen,arde,ardea,ardea bridge,ardeal,ardealu,ardealul,ardeb,ardeba,ardebe,ardebil,arded,ardedani,ardede,ardeden,ardee,ardeen,ardeer square,ardeevin,ardegaes,ardegao,ardeh,ardeh `ali,ardeh jan,ardeh kushan,ardeh rud kenar,ardeh-e rud kenar,ardeha,ardehen,ardehkul,ardei,ardeiba,ardeira,ardeiras,ardejan,ardekan,ardekhdzhan,ardekul,ardel,ardelan,ardelay,ardeleni,ardeley,ardelik,ardeliva,ardell,ardelles,ardelollec,ardelu,ardeluta,ardembey,ardemi,ardemil,arden,arden hills,arden lake,arden mines,arden on the severn,arden stationsby,arden town,arden valley,ardena,ardenais,ardenay,ardenay-sur-merize,ardencourt,ardencroft,ardenelle,ardenes,ardenevo,ardengost,ardenheim,ardeni,ardenic,ardenica,ardenice,ardenis,ardennes,ardenno,ardent,ardentes,ardentinny,ardentown,ardenvoir,ardenwald,ardenya,ardenza,ardeoani,ardeola,ardeonaig,ardeova,ardep,arderancheh,arderea,arderra,arderry,ardersier,ardes,ardes-sur-couze,ardesaldo,ardesen,ardesh,ardeshi,ardeshir,ardeshir mahalleh,ardeshirabad,ardeshiri,ardeshiri-ye `olya,ardeshiri-ye bala,ardeshiri-ye hoseynabad,ardeshiri-ye miani,ardeshiri-ye pain,ardeshiri-ye vosta,ardesi,ardesio,ardesman,ardess,ardestan,ardesti,ardestorf,ardestrup,ardet,ardeti,ardeu,ardeuil,ardeuil-et-montfauxelles,ardeul,ardevila,ardeviz,ardevol,ardevon,ardevoor,ardey,ardez,ardfallen,ardfenaig,ardfern,ardfert,ardfield,ardfin,ardfinaig,ardfinnan,ardgehane,ardglass,ardglen,ardgour,ardgroom,ardh-i-shir,ardha,ardhakhthia,ardhakhtia,ardhaktia,ardhaktos,ardhama,ardhameri,ardhamerion,ardhana,ardhanagon,ardhani,ardhanion,ardhapur,ardhasave,ardhasova,ardhasove,ardhassa,ardhea,ardheslaig,ardhin,ardhipara,ardhishta,ardhishte,ardhomista,ardhomitsa,ardhoom,ardhosi,ardhosis,ardhosos,ardi,ardi san,ardi shan,ardia,ardian,ardiani,ardib,ardic,ardicalani,ardicatak,ardicdali,ardicdere,ardicdibi,ardicgoze,ardichi,ardick,ardickaya,ardickoy,ardiclar,ardicli,ardiclioren banisi,ardicliyayla,ardicoluk,ardiconu,ardicpinar,ardicpinari,ardics,ardictepe,ardicyayla,ardido,ardiege,ardifene,ardigan,ardikan,ardikh,ardil,ardila,ardilangu,ardiles,ardilla,ardilleux,ardillieres,ardilloux,ardim,ardimini,ardimuljo,ardimulyo,ardin,ardinc,ardineh,ardines,arding,ardingly,ardino,ardinode,ardirashk,ardirejo,ardirejo kidul,ardirejo lor,ardis,ardis heights,ardisa,ardisaeng,ardisana,ardisela,ardisi,ardisin,ardistan,ardisubani,ardita,arditta,arditurre,arditurri,ardiwilis,ardiz,ardiz-e bala,ardiz-e pain,ardizas,ardjanun,ardjasa,ardjasari,ardjawinangoen,ardjawinangoen-koelan,ardjawinangun,ardjel,ardjenun,ardjes,ardjin,ardjoena,ardjosari,ardjoso,ardjowinangun,ardjuna,ardkeen,ardkeeran,ardla,ardled,ardleigh,ardler,ardlethan,ardley,ardlona,ardlougher,ardloy,ardlui,ardlussa,ardmaddy,ardmair,ardmaleish,ardmeen,ardmenish,ardmillan,ardmin,ardmon,ardmona,ardmoneen,ardmoney,ardmore,ardmore college,ardmore estates,ardmore flats,ardmore park,ardmorney,ardmuir,ardnacrusha,ardnacrushy,ardnaglass,ardnaglass upper,ardnagreevagh,ardnamulloch,ardnaree,ardnarff,ardnasodan,ardnastang,ardnave,ardning,ardno,ardo,ardo abaki,ardo aleke,ardo ama,ardo amadou,ardo ardou etiel,ardo balel,ardo djalore,ardo djedo,ardo gaya,ardo mademba,ardo musa,ardo rumeila,ardo salato,ardo sambo,ardo sovini,ardo-goni,ardobag,ardoch,ardocska,ardogelly,ardoix,ardololok,ardomysikha,ardon,ardoncino,ardonia,ardonk,ardonsillero,ardonskaya,ardonskiy,ardooie,ardoram,ardore,ardore marina,ardore superiore,ardorf,ardori,ardos,ardosis,ardosset,ardosta,ardoti,ardoulaye,ardouval,ardouz,ardouze,ardovie,ardovo,ardowinangun,ardoya,ardoye,ardoyne,ardpatrick,ardrah,ardrahan,ardranny,ardre,ardres,ardrishaig,ardrossan,ardrud kinar,ards,ards crossroads,ards district,ardsalem,ardsbeg,ardscull,ardshealach,ardshieloch,ardshirabad,ardskea more,ardskeagh cross roads,ardsley,ardsley east,ardsley-on-hudson,ardslignish,ardsollus,ardsollus bridge,ardstraw,ardtalla,ardtalnaig,ardtarig,ardtoe,ardtrea,ardtully,ardu,arduan,arduc,ardud,ardud-vii,ardudar,ardughesh,ardukin,ardulak,ardullie,ardulusa,ardumah,ardun,ardupa,ardus,ardusat,ardusevac,ardushlu,ardusin,ardusta,arduzal,arduzel,ardvally,ardvar,ardvasar,ardvazar,ardverikie,ardvi,ardvorlich,ardwell,ardwick,ardwick park,ardwood,ardy,ardym,ardymskiy,ardyshly,ardywanig,ardzek khel,ardzha,ardzhanak,ardzhevan-sarvani,ardzhikkheyl,ardzhin,ardzik khel,ardzulik-e bala,ardzulik-e pain,ardzulik-i-bala,ardzulik-i-pain,ardzuluq,are,are adriss,are ago,are asundus,are idon,are koka,are kwara,are molla,are-ago,are-ekiti,are-koara,are-malle,area,area mobile park,area residencial da mineracao plumbum,areadinho,areado,areaki,areal,areal de baixo,areal de cima,areal inalum,areal indonesia asahan aluminum,areallana,arealva,areanum,areao,areas,areatan,areavaara,areavara,areavarra,areba,areban,arebano,arebe,arebeke,areberg,arebinse,arebol,arebolet,arebrot,arebrotet,arebsa,arebsun,arec,arechalde,arechavaleta,areches,arechi,arechiga,arechuibo,arechuybo,arechuyvo,arecibo,arecida,arecifral,areco,arecutacua,arecutacue,areda,areda baro,areda godo,areda kite,areda wari,aredale,aredda,aredebasa,aredeco,aredes,aredhiou,aredi,arediu,aredo,aredo koara,aredo kwara,aredu,aredyu,aree,areeg,areegra,areeinar,areeiro,arefa,arefai,arefen,arefevskiy,arefii,arefina,arefinka,arefino,arefino pervoye,arefinskaya,arefjan,arefjord,arefu,areful,arefyeva,arefyevka,arefyevo,arefyevskiy,areg,areg gamra,arega,aregala,aregaon,aregbe,arege,aregena,aregenang,aregh batur,aregnadem,aregno,aregora,aregua,areguda,aregudam,aregue,areguito,areguni,areguy,aregza,areh chak,areh jan,areh panjeh dowshaneh,arehalli,arehava,arehava 2,arehava number 1,arehava number 2,arehuara,arei,areia,areia branca,areia branca dos assis,areia comprida,areia da teixeira,areia de baixo,areia de cima,areia de tabuleiro,areia do rosario,areia do teixeira,areia dourada,areia fina,areia larga,areia preta,areia vermelha,areia-preta,areia-pretinha,areial,areiao,areias,areias alvas,areias barauna,areias brancas,areias de gonde,areias de sao joao,areias de vilar,areias grandes,areias pequenas,areibunkra,areines,areinhas,areinho,areiniskiai,areiopolis,areit,areitio,arej goth,areja,arejas,arejocha,arejola,arejongkor,arejoyachic,arek,arek qeslaq,areket,arekit,areka,areka ancheto,arekadon,arekamae,arekarr,arekchou,arekinai,areklett,areklu,areks,areks-e sofla,arel,areldseng,arelee,arelho,arelies,arella,arellano,arellanos,arelles,areloma,arelsk,arema,aremaka,aremaku,areman,aremanis,aremaniyan,aremasahin,aremasain,aremasanahu,aremasauahu,aremasauao,aremba,arembepe,aremberg,aremd,aremenkino,aremgu,aremkhan kalay,aremo,aremoibo,aremoivo,aremt,aremu,aren,aren bala,aren-e `olya,aren-e `ulya,aren-e bala,aren-e pain,aren-e sofla,arena,arena blanca,arena de abajo,arena de arriba,arena de hidalgo,arena del norte,arena del sur,arena gorda,arena po,arenabianca,arenablanca,arenac,arenaga,arenal,arenal chico,arenal de alvarez,arenal hacienda,arenales,arenales de la moscarda,arenales y sevilleja,arenalillo,arenalito,arenaloma,arenan,arenapolis,arenas,arenas blancas,arenas cucho,arenas de cabrales,arenas de cabreles,arenas de iguna,arenas de san juan,arenas de san pedro,arenas del norte,arenas del rey,arenas del sur,arenas valley,arenas viejas,arenasblancas,arenay,arenaya,arenaybo,arenayvo,arenaza,arenazo,arenazos,arenberg,arenberg-immendorf,arenborn,arenc,arencas,arence,arend,arendadulo,arendahl,arendal,arendale,arenday,arendnest,arendole,arendonck,arendonk,arendonkse hoek,arendonksenhoek,arendorf,arendovka,arendrano,arendsee,arendsnest,arendswalde,arendtsville,arenel,arenero,areng,areng girang,areng hilir,areng tengah,areng-areng,arenga,arengan,arengareng,arengareng barat,arengas,arengbo,arengem,arengen,arengosas,arengosse,arengozyn,arengpanarikan,arenguy-e bala,arenholz,areni,arenill,arenilla,arenillas,arenillas de muno,arenillas de nuno perez,arenillas de perez nuno,arenillas de riopisuerga,arenillas de san pelayo,arenillas de valderaduey,arenillas de villadiego,areninskiye,arenita,arenitas,arenitas blancas,arenjan,arenjin,arenkioi,arenmal,areno norte,areno sur,arenola,arenos,arenosa,arenosa antigua,arenosita,arenosito,arenoso,arenota,arenovo,arenrath,arens de lledo,arensberg,arensburg,arensburga,arensch,arensdorf,arensgenhout,arenshausen,arenshorst,arenskiye,arenswalde,arensweiler,arentepecua,arenthon,arentim,arentorp,arentovo,arentowo,arentsminde,arenur,arenyevo,arenys de ampurda,arenys de lledo,arenys de mar,arenys de munt,arenza,arenzana de abajo,arenzana de arriba,arenzano,arenzhain,arenzu,arenzville,areo,areoi,areoindah,areon,areopoli,areopolis,areora,areos,areosa,areosa de cima,areoscia,areoua,areoza,arepek-turspek,arepeta,arepillachic,arepital,arepkulovo,areponamichic,areponapuchi,areponapuchic,arepovka,arepucho,arepugabo,arepunra,arepunta,arepur,arepyev,arequembaua,arequipa,arequipilla,arequito,arer,arera,arere,arereke,arerema,areri,areri lalamod,areri lolammod,areri mallim uarfai,arero,arerou,arerti,arertu,arerungua,areryd,ares,ares bocas,ares del maestre,aresa,aresal,aresanij,aresches,arese,areshperani,aresht,aresi,aresing,areskovo,areso,aresosiktong,aresquies,aresquies vieux,aressy,arestan,arestanka,aresti,arestines,arestov,arestovo,arestovo-shevelevo,arestrup,arestuv,arestuy,aresun,aret,areta,areteyti,areth,arethousa,arethus,areti,aretsried,arette,aretteul,aretturekkol,aretuche,aretuchi,aretuchic,areu,areung,arevabuyr,arevalillo de cega,arevalo,arevalo de la sierra,arevano,arevashat,arevashet,arevashogh,arevashokh,arevatsag,areved,arevichi,arevik,arevis,areviti metokhi,arevitis,arevshat,arevskoye,arew,arewa,arewahan,arewala,arewawa,arewe,arey,arey khsat,areya,areyanitsa,areykib,areyskiy,areyskoye,arez,areza,arezand,areze,arezine,arezo,arezu,arezu kheyl,arezumand,arezumandeh,arezuvaj,arezzo,arezzou,arfa,arfa moussaia,arfa oulad es soltane,arfa` deh,arfa` rudbar,arfairyu,arfak mandoesir,arfak-mandoeser,arfak-manduser,arfak-mendoeser,arfak-saba,arfan,arfanta,arfara,arfeh darreh,arfeh deh,arfeh kuh,arfeld,arfenreuth,arfeuilie,arfeuille,arfeuille-chatain,arfeuilles,arfi,arfinda,arfingia,arfinjo,arfit,arfits,arfling,arfon,arfons,arfor,arford,arfrade,arfslindan,arft,arfuda,arfunda,arfurk,arfurt,arfvesund,arfvet,arfvidstorp,arg,arg darreh,arg n ait slimane,arg n ouannou,arg nsidi ali ou bourek,arg nsidi ali ou bourk,arg now juy,arg-e mir qalandar,arg-e now judi,arg-e now juy,arg-e qalandar,arg-e sabz,arg-e shah qoli,arg-e zari,arg-sidi-ali-ou-bourek,arga,arga de baixo,arga de cima,arga de sao joao,arga qeshlaq,arga qeslag,arga-karsyulya,arga-khalva,arga-korsyulya,arga-kyuyel,argaa-yurekh,argab,argaba sharqi,argac,argaca,argach,argaci,argaci koy,argada,argada pervaya,argadeb,argadez,argado,argaga,argagnon,argah qeshlaq,argaiz,argaka,argakencana,argakhtakh,argaki,argalant,argalasti,argalei,argaley,argalingga,argalinta suma,argalintayn ortoo,argalintiin urto,argalintiin yurt,argalintu,argalintu somon,argalintu sumu,argaliya,argallo,argallon,argalo,argalong,argalya,argam,argamach,argamach-palna,argamachi,argamak,argamakmur,argamakovo,argaman,argamanovo,argamasilla de alba,argamasilla de calatrava,argamason,argamassa,argambay-bulak,argamchi,argamdzhi,argame,argamulya,argan,argana,arganas,arganatinskiy,arganato,arganaty,arganchy,argancinas,argancon,argancy,argand,arganda,argande,argandekhi-pain,argandenes,argandona,argandonaloma,arganil,arganin,arganj khwa,argankheyl,argankul,argankun,arganosa,arganoso,arganoual,arganovo,arganson,arganza,arganzua,argao,argaon,argaputra,argar rhiren,argar rhirene,argard,argaria,argas,argas merey,argas sofla,argasa,argasalesti,argasari,argaselo,argash,argasha,argashino,argasi,argasilesti,argasion,argasoka,argastiri,argastirion,argasunya,argat,argat-yul,argataes,argatawang,argatay,argata\302\223s,argates,argatov,argatu,argauanon,argausovskaya,argavan,argavand,argavieso,argavli,argawanon,argayash,argaydy,argayevo,argayo,argayuryakhskiy,argaza,argazi,argbat hadj larbi,argbiru,arge,arge sawz,argea,argeaua,argebela,argebet,argebla,argecilla,argedeb,argeeg,argegle,argegno,argein,argel,argela,argelaguer,argelato,argeles,argeles-gazost,argeles-plage,argeles-sur-mer,argeles-sur-mer-plage,argelia,argeliers,argelita,argellas,argelliers,argelopos,argelos,argelouse,argelsried,argelsrieg,argemil,argen,argena,argenau,argences,argeneh,argeneh-ye `olya,argeneh-ye bala,argeneh-ye pain,argeneh-ye sofla,argenhof,argenia,argeningken-graudszen,argeningken-graudzhen,argenita,argennos,argenoux,argens,argenschwang,argenstein,argent,argent-sur-sauldre,argenta,argentan,argentat,argente,argenteau,argenteiro,argentenay,argentera,argenteuil,argenteuil-sur-armancon,argenthal,argentia,argentiera,argentiere,argentieres,argentina,argentine,argentolle,argentolles,argenton,argenton-chateau,argenton-l eglise,argenton-notre-dame,argenton-sur-creuse,argentona,argentre,argentre-du-plessis,argents hill,argentville,argenty,argenvieres,argenvillier,argenvilliers,argenzipfel,argeo martinez,argerdareyri,argere grand,argere petit,argerich,argerita,argeriz,argers,arges,arges-e sofla,argesani,argeselu,argesians,argestorf,argestru,argestrul,argestues,argesu,argesul,arget,argetoaia,argetoaia de jos,argetoaia-gradinaresti,argetoeni,argetzleiten,argeville,arggiavara,arggiovara,argha,arghakot,arghan,arghan khel,arghana,arghana maden,arghandab,arghandah-ye pain,arghandakan,arghandaw,arghande-ye pain,arghandeh pain,arghandeh-ye pain,arghandeh-ye payan,arghandehe pain,arghania kili,arghanja kili,arghanjapatai,arghanjo,arghanjo mela,arghankhel,arghankheyl,arghar,arghastan,arghat,arghavan,arghavaniyeh,arghay,arghebba,arghebla,arghed,argheile,argheli sang,arghelisang,arghenchkha,arghenchkha bala,arghenckha,arghes,arghesh,argheshabad,arghestan,arghezana,arghi,arghin,arghin bolagh,arghinak,arghinchkha,arghinchkha-ye bala,arghinonda,arghira,arghirescu,arghiresu,arghis,arghiscia,arghistan,arghisu,arghol,arghor,arghowl,arghowr,arghshorai,arghu,arghun,arghuna,arghund,arghyrocastro,argi,argi darreh,argi-pagi,argi-pargi,argi-zari,argiano,argiavara,argida,argideen bridge,argiesans,argignano,argil,argilaga,argilia,argilisang,argiller,argillieres,argilliers,argillite,argilly,argimbay,argimbay-bulak,argin,argin n.,argin wasat,arginak,arginchik,argincik,argine,argine del sabato,arginella,arginesti,argini,argintari,argintesti,arginy,argio,argir,argirita,argirocastro,argis,argishti,argisu,argit,argithan,argithani,argithea,argiti,argiusta-moriccio,argivai,argiz,arglaiciai,argnai,argnat,argo,argo corners,argo fay,argo mill,argoal,argoda,argoda pervaya,argoda vtoraya,argodadi,argodeh,argodewi,argoeni,argoeuves,argofa,argoim,argokhi,argol,argola,argolas,argolell,argolellas,argolibio,argolikon,argoliya,argollanes,argolo,argolovskaya,argomaniz,argomeda,argomedo,argomil,argomilla,argomoso,argomulyo,argomulyo lor,argomulyomukti,argonas,argoncilhe,argonia,argonne,argonne hills,argonnex,argonos,argoon,argopeni,argopuro,argorejo,argoronal,argoryevo,argos,argos choice,argos orestikon,argosab west,argosari,argosari selatan,argosari utara,argosh,argosin,argosino,argosono,argostoli,argostolion,argosuko,argote,argotirto krajan,argoub mechtat,argoud gafak,argoudgafa,argouges,argoules,argoulidhes,argoulio,argoulion,argoumi,argouni,argova,argove,argovejo,argoyoso,argozelo,argozon,argra,argryopolis,argseeli,argu,argual,arguamul,arguani,arguay,arguay hacienda,arguayo,arguba,argud,argudan,argue,argue ait slimane,arguea,arguebanes,arguedas,arguedeira,arguedes,argueil,arguel,arguelho,arguellana,arguellena,arguelles,arguellina,arguellite,arguello,arguenos,arguero,argueso,argueta,argueto,arguijo,arguila,arguinano,arguinariz,arguineguin,arguiol,arguiouine,arguioun,arguis,arguisal,arguisuelas,arguiuen,arguiwn,argujillo,argul,argulica,arguljica,argumoso,argun,argun-bore,argunevo,argungu,arguni,argunia,argunka,argunkoy,argunna,argunovka,argunovo,argunovskaya,argunovskiy,argunsk,argunskaya,argunskiy,argunskiye klyuchi,argunskoye,arguny,argura,argus,argusino,argusto,argusville,argut,argut-dessous,argut-dessus,arguta,argutel,argutevo,argutskaya,arguut,arguvan,argvani,argveti,argveta,argveti,argveto,argvitsi,argy,argyle,argyle forest,argyle heights,argyle pen,argyle plantation,argyll,argyll and bute,argyll county,argyll east,argyllshire,argyn,argynchik,argyrades,argyraiika,argyratika,argyri,argyrion,argyrita,argyrochori,argyrochorion,argyrokastro,argyrokastron,argyrotopos,argyroupolis,argysh,argyunash,argyzh,arh,arh thumair,arhab,arhaba,arhabala,arhabech,arhabi,arhan,arhanga,arhangai,arhani,arhanpur,arhansus,arhanza,arhanza karrat,arharpur,arhat,arhat baba,arhauli,arhavi,arhavwarien,arhayyah,arhbal,arhbala,arhbalou,arhbalou nait ouakrim,arhbalou nserdane,arhbalou-n-kerdous,arhbalou-n-kerdouss,arhbalou-serdane,arhbaoulou,arhbar,arhbarine,arhbel,arhchiine,arheddou,arheilgen,arheim,arhejm,arhela,arhelai,arhelane,arhella,arhella ou assif,arhellai,arhelmane,arhem ifounassen,arhem ifounassene,arhembou,arhennoui,arhermane,arhesdis,arhi,arhil,arhil oumial,arhil tassafete,arhilas,arhir,arhitekts,arhiud,arhla ou drar,arhlad,arhlal,arhlan,arhlane,arhli,arhmed,arhmid,arhmir,arhogen,arholm,arholma,arholzen,arhor,arhorais,arhoreis,arhori,arhori izdar,arhori ou fella,arhorimez,arhoud,arhoudid,arhoumi,arhouna djambougin,arhounfar,arhouzir,arhoy,arhraisse,arhren,arhren kadi,arhrene,arhribs,arhroud,arhtites,arhtitess,arhu,arhuacancha,arhuacata,arhud,arhuin,arhult,arhus,arhust,arhustai,arhut,arhzar aissa,ari,ari acu,ari adeso,ari akbar shah,ari boochagh,ari dhok,ari hijrewali,ari kaheriwali,ari karain shah,ari kire,ari lal khan,ari mahar,ari manzah,ari mochiwala,ari saiyidan,ariel,ari-awmawna,aria,ariab,ariaba,ariabamba,ariachic,ariadaha,ariadanga,ariadha,ariadi,ariadna,ariadnoye,ariaf,ariah park,ariaitpa,ariaka,ariake,arial,arialaccia,arialai,arialva,ariam,ariama,ariamab,ariamito,ariamsvlei,ariamsvley,ariana,ariane er ras,arianeshty,ariang,ariangon,ariani,arianiaga,arianito,ariano di puglia,ariano ferrarese,ariano irpino,ariano nel polesine,ariano polesine,ariano polesino,arianu,arianwala,arianwala khu,ariany,ariaou,ariap,ariapan,ariari,ariaria,arias,ariashahr,ariate,ariatjou,ariatuna,ariau,ariaux,ariawan,arib,arib ki dhok,arib nouaceur,aribaba,aribabi viejo,aribado,aribahce,aribang,aribao,aribaolage,aribashevo,aribat,aribaya,aribe as suq,aribe-es-suk,aribeleni yaylasi,aribey,aribi,aribibi,aribice,aribinda,ariblana,aribo,aribore,aribouya,arica,arica hacienda,aricabamba,aricagua,aricak,aricaklar,arican,aricanduva,aricapamba hacienda,aricapampa,aricaria,aricato,ariccia,aricera,aricesti,aricesti-de-sus,aricesti-zeletin,aricestii de jos,aricestii de sus,aricestii-rahtivani,aricestii-zeletin,ariceti,arich,aricha,arichat,arichi,arichua,arichuna,arichunita,arici,aricik,ariciklar,aricilar,aricingori,aricioaia,ariciro,ariciu,ariciul,arickaree,arico,arico el nuevo,arico nuevo,arico viejo,aricobe,aricollo,aricomo,aricota,aricourt,aricula,aricura,arid,arida,aridag,aridah,aridaia,aridawen,aridea,aridhaia,aridi,aridiana,aridiogo,arido,aridong,ariduarmi,aridul,aridurak,arieegle,arief,ariego de abajo,ariego de arriba,arieira,arieiro,ariel,ariel cross road,arielli,ariemagom,arien,ariena,ariendorf,arienheller,arienza,arienzo,arienzo san felice,ariero,aries,aries-espenan,arieseni,ariesputs,ariestolas,ariesu de cimpie,ariesu de padure,ariesul de camp,ariesul de padure,ariet,arietta,arif,arif abbasi,arif kaku,arif kolaizi,arif shah khel,arif solongi,arif-gerau,arifa,arifabad,arifane,arifat,arifawa,arife,arifegazili,arifiran,arifiye,arifkoyu,arifler,arifli,arifobasi,arifoglu,arifogo,arifogo awaiepa,arifonda,arifovici,arifpur,arifuku,arifukugo,arifwala,arig,arigiin hurie,arigabo,arigako,arigam,arigana,arigaon,arigbabu,arigbajo,arigeik,arigh batur,arigi,arigidi,arigiin dugang,arigiin huryee,arigin-khure,arigliano,ariglu,arigna,arigna bridge,arignac,arignano,arigo,arigono,arigu,ariguanabo,ariguani,ariguapa,arigyin dugang,arigyyn dugang,arigyyn khure,arigza,ariha,ariha nakke,arihah,arihar,arihata,arihavo,arihua,arihueca,ariis,arij,arija,arijama,arijamo,arijan,arijo,arijon,arijpur,arijuane,arijunasahin,arik,arik qeshlaq,arik qeshtaq,arik qushlaq,arikli,arik-khure,arikabe,arikahmet,arikan,arikanassou,arikanki,arikap,arikawa,arikawago,arikaya,arikbasi,arikcatak,arikcayiri,arikdere,arikeh,arikeha,arikeheh,arikemes,arikgol,arikh,arikhasan,arikhvali,ariki,arikia,arikiley,arikire,arikiyufa,arikkae,arikkod,ariklar,ariklar koyu,arikli,ariklibaglari,ariklikas,ariklu,arikmusa,ariko,arikok,arikokaha,arikokro,arikola,arikola station,arikonak,arikoren,arikouka,arikoukouri,arikovo,arikoy,arikpa,ariku,arikula,arikuyusu,arikuyusukoy,arikuyusukoyu,arikviran,aril,aril,arila,arilach,arilal,arilay,arilcham,arild,arildseng,arildslage,arile,arilevo,arili,ariliaca,ariljaca,arilje,ariljevo,arilla,arillanga,arillas,arilli,arillodongjagu,ariltong,arim,arim khan kelay,arima,arimaca,arimacare,arimacho,arimagawa,arimagua,arimaiciai,arimaiciu,arimal,arimalam,arimalang,ariman,ariman chelka,arimanda,arimao,arimarica,arimas,arimatau number 1,arimatau number 2,arimateia,arimatsu,arimaychyay,arimba,arimbay,arimbugor,arimdzhan,arimeh chal,arimeno,arimetau number 1,arimetau number 2,arimin 1,arimin 2,arimin 3,arimin number 1,arimin number 2,arimin number 3,arimis,arimizu,arimkhankalay,arimo,arimoe,arimogija,arimoi,arimont,arimori,arimoye,arimpia,arimtatarli,arimtemurlu,arimune,arimura,arin,arin kot,arin ragazz,arina,arina asundus,arinabost,arinafa,arinaga,arinago,arinagour,arinapaewa,arinay,arinc,arincik,arinckus,arincon,arinder,arindi,arindik,arindirri,arindrano,arindzh,arinel,arinem,arines,arinez,aring,aringa,aringan,aringay,aringe,aringel,aringeli,aringell,aringen,aringeru,aringi,aringin,aringiru,aringlo,aringo,aringoma,aringona,aringot,aringsah,aringsas,aringshah,aringshakh,aringuiessa,aringyesa,arini,arinia,arinicheva,arinichevo,arinie,arinii,arinik,arinina,arinino,arininskaya,arininskoye,arininy lyady,arinis,arinista,arinjin,arinkin,arinkin khutor,arinkinkin,arinkino,arinkot,arinkova,arinkovo,arinnie,arino,arinokus,arinokuta,arinola,arinos,arinovka,arinovo,arinsal,arintero,arinthod,arinuiyeh,arinzue,ario,ario de rayon,ario de rosales,ario este,ario oeste,ario santa monica,ariobdi,arioebdi,ariogala,ariogalos,ariogheta,arioglu,arioka,ariokhorion,ariol,ariola,ariolas,ariom,ariomba,ariome,arion,arione,arionesht,arioneshty,arionesti,arionestii noi,arionestii vechi,arionu,ariori,ariosa,ariosari,ariot,ariota,arioua,arioungha,ariourou,aripanthan,aripao,aripari,aripe,aripeka,aripi,aripia,aripibu,aripinagar,aripinar,aripine,aripip,ariporo,aripovka,arippu,arippur,arippuveddukulam,aripsta,aripuana,aripucula,ariqdam,ariqim,ariqiran,ariquemes,ariquen,ariques,ariquilda,ariradle,arirahua,ariramba,ariranha,arirapah,arirapaha,arire,arires,arirha,ariri,ariro,arirtu,ariruma,ariry,aris,arisagh,arisaig,arisalah,arisama,arisan,arisandi,arischia,arisco,ariscos,arisdonck,arisdonk,arisdorf,arise,ariseachic,arisegh,arisgotas,arish,arishang,arishat,arisheim,arishk,arishka,arishki,arisht,arisi,arisiachic,arisif,arisil,arisili,arisilli,arisimo,arisitikolo,ariskovo,arislaca,arisma,arismaa,arisman,arismendi,ariso,arisouna,arisovo,arispan,arispe,arissa,arissouna,arista,aristarkhovka,aristau,aristava,ariste,aristebano,aristengah,aristes,aristeu,aristeu braganca,aristi,aristides,aristides lobo,aristikha,aristil,aristinon,aristobulo del valle,aristocrat ranchettes,aristocrat trailer park,aristodhimion,aristodhimion khasan passa,aristog,aristomenis,ariston,ariston este,ariston oeste,aristondo,aristondos,aristosteles,aristot,aristotle,aristov,aristova,aristovka,aristovo,aristovo ramenye,aristregui,arisu,arisuel,arisvel,arisvere,arisvi,ariswatla,arit,arit kanial,aritata,arita,aritagua,aritaki,aritalomas,aritao,aritapu,aritas,aritawa,aritayein,aritepe,arith,ariti,aritinemeri tichu,arito,ariton,aritonova mahala,aritoprak,aritsu,arituaba,arituava,aritzalde,aritzo,ariuan,ariumi,arius,ariusd,arivaca,arivaca junction,arivaipa,arivalpa,arive,arivechi,arivichenai north,arivonimamo,arivonimamy,arivu,ariwa,ariwala,ariwala khu,ariwali,ariwo,ariwuzumari,arix,arix xiang,arixa ams,arixang,arixat,arixi,ariya,ariyakundu,ariyalai,ariyalur,ariyamadu,ariyan,ariyankurisuddakulam,ariyankurisuttakulam,ariyappancheri,ariyaripulam,ariyarmadam,ariyau,ariye,ariyevo,ariyewon,ariyez,ariyle,ariyo,ariz,ariza,arizaga,arizaj,arizal,arizala,arizaleta,arizcun,arize,arizgoiti,arizha,ariziachic,ariziem,arizlar,arizler,arizli,arizli divan,arizlidivani,arizola,arizona,arizona acres mobile home resort,arizona city,arizona sun sites,arizona village,arizouma,arizpe,ariztegui de garzain,arizu,arja,arjac,arjadi,arjadzor,arjagh,arjagur-avsarlu,arjak,arjal,arjali,arjali nadai,arjamaq,arjamulya,arjan,arjan singh,arjanak,arjanan,arjanavand,arjanawand,arjang,arjang sara,arjangan,arjangarh,arjange,arjangeh sara,arjangka selatan,arjangka utara,arjaniar,arjank,arjanli,arjanprud,arjanpur,arjanpura,arjansar,arjanu,arjanwala,arjaq,arjara,arjarud,arjas,arjasa,arjasari,arjasb,arjassaare,arjastan,arjawan sharif,arjawinangun,arjawinangunpermai,arjay,arjazur,arjeile,arjekkehel,arjekkhel,arjekkheyl,arjel,arjelik,arjenak,arjenan,arjenavand,arjenk,arjeplog,arjes,arjestan,arjevan-sarvani,arji,arji narayanpur,arjika,arjilan,arjilane,arjin,arjinoagaon,arjjun kachhra,arjjun nalai,arjjuna,arjjundi,arjjunhar,arjjuntala,arjmand,arjnan,arjo,arjoci,arjol,arjomad,arjomand,arjomandiyeh,arjomulyo,arjona,arjonilla,arjosari,arjoso,arjoune,arjowinangun,arjrud,arju,arjuana,arjub,arjuke,arjul,arjun char,arjun khila,arjun singhwali,arjuna,arjuna barai chara,arjunda,arjundaha,arjungari,arjunhar,arjuni,arjuni madarbaria,arjunmisra,arjunpara,arjunpur,arjuntala,arjuntola,arjunwala,arjut,arjuyeh,arjuzanx,arjwan sharif,arjya narayanpur,ark,arkoncha,arka,arka adon,arkabah sharqi,arkabait,arkabash,arkabash pervyy,arkabay,arkabutla,arkaca,arkacilar,arkadades,arkadak,arkadakskiy,arkadelphia,arkadevka,arkadhadhes,arkadhikos,arkadia,arkadiya semen,arkadiye semekovskoye,arkadiye-semenovskoye,arkadiyevka,arkadiyevo-semenovka,arkadiyevtsy,arkadiyivka,arkadja,arkadon,arkadovo,arkadskiy,arkadyev,arkadyeva,arkadyevka,arkadyevo,arkadyevo-somenovskoye,arkagala,arkagalinskaya,arkagesken,arkai tanyak,arkain,arkaka,arkakisla,arkakoy,arkala,arkalgud,arkalguda,arkalik,arkalochori,arkalochorion,arkalokhori,arkalokhorion,arkalon,arkalyk,arkalyskiy,arkam,arkamais,arkamani,arkambal,arkamli,arkan,arkana,arkanai,arkand,arkandi,arkangelskiy,arkanis,arkansas,arkansas city,arkansas junction,arkansas post,arkansaw,arkanwala,arkany,arkaoglukoyu,arkar,arkar makhle,arkaraobato,arkari,arkarly,arkas,arkasa,arkasadhes,arkasas,arkasebgen,arkasebgenkoyu,arkasepken,arkashevo,arkasi,arkasiyan,arkasy,arkasyan,arkasyon,arkat,arkatin,arkatova,arkatovka,arkatovo,arkatskaya,arkatski,arkatskiy,arkaul,arkaulovo,arkaun,arkavan,arkavazi,arkavin,arkawana,arkawi,arkawit,arkayevka,arkayevo,arkazha,arkbasi,arkbork,arkburk,arkdale,arkdell,arkebek,arkel,arkelstorp,arkendale,arkens,arker,arkeri,arkesai,arkesden,arkesini,arkevak,arkevan,arkevin,arkewan,arkez,arkh bilanday,arkh ghundai,arkh-e bozorg,arkh-e kuchek,arkha,arkhaderes,arkhahgelskaya sloboda,arkhai,arkhai kili,arkhaia korinthos,arkhaia olimbia,arkhalkalaki,arkhamonovskiye,arkhamonovskiye uchastki,arkhamony,arkhamony iskra,arkhan,arkhangda,arkhangel,arkhangelskiy,arkhangelskoe,arkhangelskoye,arkhangelevka,arkhangelka,arkhangelo,arkhangelo-shelokhovskaya,arkhangelogavrilovka,arkhangelos,arkhangelovka,arkhangelsk,arkhangelsk kholm,arkhangelskaya,arkhangelskaya melnitsa,arkhangelskaya sloboda,arkhangelske,arkhangelskiy,arkhangelskiy pervy,arkhangelskiy pervyy,arkhangelskiy zavod,arkhangelskiye borki,arkhangelskiye klyari,arkhangelskiye vyselki,arkhangelsko-latyshskoye,arkhangelskoe,arkhangelskoye,arkhangelskoye golitsino,arkhangelskoye kuroyedovo,arkhangelskoye pervoye,arkhangelskoye saburovo,arkhangelskoye sloboda,arkhangelskoye vtoroye,arkhangelskoye-golitsyno,arkhanhelska sloboda,arkhanion,arkhanskoye,arkhapka,arkhapovo,arkhara,arkharly,arkharovka,arkharovo,arkharovskiye vyselki,arkhasa,arkhashan,arkhavianos,arkhaw,arkhay,arkhazlu,arkhbelanday,arkhbelanfay,arkhbilanday,arkhe,arkhel,arkhenlsh,arkhera,arkhesini,arkheyl,arkheyl,arkhi,arkhia,arkhibili,arkhida,arkhikovo,arkhiloskalo,arkhimandrita,arkhinovo,arkhinovskiy,arkhipavskoye,arkhipenki,arkhipikha,arkhipino,arkhipkino,arkhipo-osipovka,arkhipo-osipovskaya,arkhipo-osipovskoye,arkhipov,arkhipova,arkhipovka,arkhipovo,arkhipovskaya melnitsa,arkhipovskiy,arkhipovskiye baraki,arkhipovskoye,arkhipyata,arkhireyka,arkhisvyatka,arkhitekts,arkhiyerevka,arkhiyereyevka,arkhiyereyeyskaya,arkhiyereyka,arkhiyereyskaya,arkhiyereyskiy,arkhiyeveyskaya,arkhiz,arkhod,arkhohskaya,arkholme,arkhon,arkhona,arkhondika,arkhondiki,arkhondiko,arkhondikon,arkhondokhora,arkhondokhori,arkhondokhorion,arkhonskaya,arkhonskiy,arkhonskoye,arkhontiko,arkhoti,arkhovskiye,arkhow,arkhu,arkhud,arkhuna,arkhust,arkhva,arkhye,arkhypivka,arkhyttan,arkhyz,arki,arki dogran,arkiba,arkida,arkik,arkiko,arkilimaro,arkilsmala,arkin,arkinareti,arkinda,arkinde,arkino,arkinshino,arkipovka,arkipuszta,arkir,arkis,arkit,arkitanya,arkitca,arkitsa,arkivan,arkivzing,arkiya,arkkukari,arkland,arkle,arklo,arklow,arkma,arkmo,arkna,arkneti,arkoc,arkoca,arkochori,arkochorion,arkod,arkodia,arkoe,arkohur,arkokhorion,arkola,arkollou,arkolo,arkoma,arkomou,arkona,arkonam,arkondiko,arkonis,arkos,arkoshazapuszta,arkosund,arkot,arkot qila,arkou,arkoua,arkoub,arkoudhaina,arkoudhitsi,arkoudhokhorion,arkoudhosouvala,arkouditsi,arkoullou,arkoum,arkoun,arkouta,arkova,arkova pervaya,arkova pervoye,arkova vtoraya,arkova vtoroye,arkovi,arkovin,arkovna,arkovo,arkovo pervoye,arkovo vtoroye,arkovo-bereg,arkowna,arkowyen,arkoy,arkoy karia,arkoyevka,arkoyla,arkport,arksey,arkshva,arksjo,arksva,arksvos,arkton,arku,arkub,arkul,arkulskiy,arkum,arkumbi,arkunis,arkuntankhua,arkuren,arkus,arkusdulo,arkush,arkushino,arkussziget-major,arkuszewo,arkuszow,arkut,arkutca,arkuten,arkuyen,arkvaz,arkville,arkwaz,arkwright,arla,arla-tuguldy,arlach,arlaching,arlagarden,arlagul,arlahovici,arlak khel,arlaka,arlaki,arlal,arlam,arlamesh,arlamow,arlamuash,arlamuchash,arlan,arlana,arlanc,arland,arlandria,arlanovo,arlanza,arlanzon,arlarova,arlarovo,arlas,arlasun,arlat,arlatex,arlatinndala,arlava,arlaviskes,arlaviskiu,arlay,arlbayev,arlberg,arle,arlebosc,arlebose,arlecdon,arlee,arlegui,arlempdes,arlen,arlen siu,arlena di castro,arlene,arles,arles-sur-rhone,arles-sur-tech,arles-trinquetaille,arlesberg,arlesey,arlesheim,arlesley,arlesried,arless,arlet,arlet en najaa,arleta,arletta,arletzgruen,arletzgrun,arleuf,arleux,arleux-du-nord,arleux-en-gohelle,arlewatt,arlewattfeld,arley,arley landing,arleya,arli,arli buzurg,arlia,arlie,arlifoss,arligbo,arlight,arlikinkomu,arlindo gomes,arline,arling,arlingham,arlinghundra harad,arlington,arlington beach,arlington estate,arlington estates,arlington forest,arlington heights,arlington hills,arlington junction,arlington knolls,arlington park,arlington station,arlington village,arlington woods,arlingwood,arlins,arliod,arlis,arlit,arllat,arllati,arlo,arlod,arloff,arlom,arlon,arloncourt,arloroma,arlos,arlose,arlose torp,arlot,arlov,arlsdorf,arltunga,arltunga mission,arltunga police station,arlucea,arlum,arluno,arly,arlyaki,arlyan,arlyanovo,arlyapovo,arlydzha,arlynda corners,arlyuk,arlyukovskaya,arm,arma,arma di taggia,armabat,armabe,armabel,armacao,armacao da abobora,armacao de pera,armacao do tairu,armacao dos buzios,armachestan,armachistan,armacistan,armacost,armada,armadale,armadale north,armadarstrik,armadas,armadeh,armadeyevo,armadi,armadia,armadian,armadillo,armadillo numero tres,armadillos,armadivos,armadiyos,armado,armador,armadouro,armaeli,armaes,armaf,armag,armagan,armaganlar,armaganli,armaganovo,armagantasi,armagh,armagh district,armaghan khaneh,armaghankhaneh,armagon,armah,armahal,armahue,armail,armaille,armak,armakha,armakhana,armaki,armal,armala,armalah,armale,armalek,armalik,armalishki,armaliskes,armallones,armalsar,armaly,armamar,armampela,arman,arman bolaghi,arman tasnec,arman-bercu,armanabad,armanak,armanak-e `olya,armanak-e sofla,armananzas,armanapampa,armanat,armanaz,armanbaru,armanca,armancourt,armand,armand olya,armand sofla,armand-colin,armand-e `olya,armand-e bala,armand-e pain,armand-e sofla,armando,armando coelho,armando ndembo,armando osorio,armandombja,armandville,armanella,armanello,armanesti,armani,armani bolaghi,armani bulagh,armani jan,armani mahalleh,armania,armanian,armanikha,armanis,armaniya,armaniyah,armanka,armankasy,armankul,armanlu,armannsberg,armano,armanos,armanovo,armanoyia,armanskiy,armant,armant el waburat,armant gharb,armant sharq,armantina,armantsy,armaouta,armapuquio,armaq,armara,armarah,armard,armardeh,armariz,armarolo,armas,armasa,armasaari,armaseni,armaseni noi,armasenii,armasenii noi,armasesti,armash,armashev,armashevka,armashovka,armasjarvi,armasoaia,armassaari,armassi,armata,armatan,armatewala,armath,armathe,armathi,armathwaite,armatlikon,armatlu,armatoliko,armatolikon,armatovon,armatree,armatus,armau,armaucourt,armausa,armavir,armavirchik,armavirskiy,armawalk,armawir,armaycancha,armayor,armazem,armazem i.a.m.,armazen,armazens,armazi,armazin,armazovo,armazyn,armbiryuk,armbounas,armbouts-cappel,armboy,armbrust,armburg,armd,arme klei,arme leut,armea,armear,armeau,armebe,armeding,armedlu-ye jadid,armedlu-ye qadim,armee francaise,armeh,armeida,armeis,armejun,armel,armel acres mobile home estates,armellada,armellari,armelle,armen,armen-keno,armena,armenabad,armenades,armenadhes,armenande,armenat,armenatkyshlak,armenavan,armendais,armendarits,armendariz,armeneh,armenehor,armenevo,armenhof,armeni,armenia,armenia bonito,armenia central,armenia vieja,armenian village,armenika,armenio,armenioi,armenion,armenis,armeniskiai,armeniskiai antrieji,armeniskiai ii,armeniskiai pirmieji,armenisti,armenistis,armenite,armenji,armenki,armenkino,armenkovtsu,armeno,armenochori,armenohor,armenohori,armenoi,armenokhori,armenokhorion,armenonville,armenonville-les-gatineaux,armenos,armenoz,armenskon,armenta,armental,armentano,armentera,armenteros,armenteule,armentia,armentier haut,armentieres,armentieres-en-brie,armentieres-sur-avre,armentieres-sur-ourcq,armentieux,armento,armenton,armenton de arriba,armera,armeres,armeria,armero,armes,armesen,armeses,armesh,armesko,armesnal,armestan,armesto,armet-rakhitovo,armet-rakhomovo,armetka,armetrakhimova,armetrakhimovo,armetshofen,armeville,armeyan,armeyevka,armeyskiy,armeyskoye,armhach,armhoede,armian,armiani,armianskoye ushelie,armich kola,armid,armidale,armidale municipality,armidiyos,armidzhan,armiello,armiesburg,armiger,armijo,armil,armila,armila numero dos,armilda,armilla,armillac,armillas,arminakna,arminda,armindo,arminghall,armington,armington corner,armington junction,armini,arminia,arminna,armino,arminon,arminou,arminto,arminu,arminza,armiropotamos,armis,armish,armisht,armissan,armistead,armistead forest,armistead gardens,armistead homes,armistice,armisvesi,armit,armitage,armix,armiyan,armiyevo,armiyske,armizonskoye,armjansk,armkhi,armley,armmansvriend,armno,armo,armod,armoede,armoen,armofa,armoj deh,armolia,armona,armonai,armonaiciai,armonaychyay,armonia,armoniz,armonk,armonlaakso,armonu,armonville,armonville-le-guenard,armonville-le-sablon,armonville-sablon,armonys,armoodloo,armopa,armor,armordah,armorel,armori,armotlu,armou,armoudja,armour,armour number two mine,armour village,armous,armous-et-cau,armoy,armschlag,armsen,armsfeld,armshakan,armsheim,armstead,armstedt,armstetten,armston,armstorf,armstrong,armstrong creek,armstrong gardens,armstrong landing,armstrong quarry,armstrong station,armstrongs,armstrongs mills,armthorpe,armu,armuchee,armuchi,armud aghaji,armudaghaj,armudan,armudan nizhniy,armudan verkhniy,armudanisagir,armudaq,armudeli,armudli,armudlo,armudlu,armudly,armudova,armudpadar,armuelles,armugia,armuja,armukhammed-kala,armul,armul,armuna,armuna de almanzora,armuna de tajuna,armungia,armunia,armurd aghaji,armus,armusen,armush,armushki,armushkun,armuski,armut,armuta,armutalan,armutalan koyu,armutalani,armutcayiri,armutcayiri koyu,armutchi,armutchuk,armutcuk,armutcukuru,armutduzu,armuteli,armuthsham,armutile,armutkasi,armutkolu,armutkoy,armutlar,armutli,armutlia,armutlii,armutliy,armutlo,armutlu,armutlu esenbey,armutluk,armutluk mahallesi,armutluyazi,armutova,armutruk,armutsham,armutsuyu,armutsuyu mezraasi,armutver,armutveren,armwei,armweide,armyak,armyani,armyaninova,armyankovtsi,armyansk,armyanskaya pristan,armyanskiy,armyanskiy gerger,armyanskiye borisy,armyanskoye anukhua,armyanskoye loo,armyanskoye naa,armyanskoye ushchelye,armyaz,armyropotamo,armytage,arn,arn basti,arn i eperm,arn i poshtem,arn-e bala,arna,arna jatau,arna valley,arna-boz,arnab,arnab sar,arnabab-e bala,arnabab-e pa`in,arnabad,arnabah,arnabe,arnaberga,arnabih,arnabiyah,arnabiye,arnaboke,arnabost,arnac,arnac-la-poste,arnac-pompadour,arnac-sur-dourdou,arnace,arnach,arnachi,arnacija,arnadelo,arnadhi,arnadhos,arnadi,arnado,arnado debbo,arnado dogo,arnado furu,arnado niakoum,arnado nyiri,arnadola,arnafjord,arnafjor\303\260ur,arnage,arnager,arnagerbro,arnagou,arnagragh,arnaia,arnaises,arnajevo,arnakija,arnakke,arnakyula,arnal,arnala,arnalapada,arnaldo,arnaldo barbedo,arnaldo barbela,arnalha,arnali,arnalla,arnan,arnan kulon,arnan oulili,arnan wetan,arnanan,arnanas,arnancourt,arnania,arnans,arnao,arnaoun,arnaoutali,arnap,arnara,arnaro,arnarp,arnas,arnasay,arnasey,arnashamn,arnassey,arnassi,arnastorp,arnasvall,arnat,arnathi,arnatov,arnau,arnaucani,arnaud,arnaud-guilhem,arnaud-kujussu,arnaudes,arnauds,arnaudville,arnauld,arnauli,arnaut,arnaut cuius,arnaut koy,arnaut kioi,arnaut kioj,arnaut kuyusu,arnaut polje,arnaut-koy,arnaut-kjoj,arnauti,arnautito,arnautka,arnautkioj,arnautkioy,arnautlar,arnautli,arnautlii,arnautova,arnautova pervaya,arnautova vtoraya,arnautovici,arnautovka,arnautovo,arnautska mahala,arnava,arnave,arnaveh,arnavej,arnavik,arnaville,arnavut,arnavutkoy,arnawai,arnay,arnay-le-duc,arnay-sous-vitteaux,arnaya,arnayi,arnayon,arnaz,arnbach,arnberg,arnberg am kobernauser walde,arnberg am kobernausser walde,arnbo,arnborg,arnboroka,arnbruch,arnbruck,arnbuch,arncliffe,arnco mills,arndorf,arndorf bei sankt ruprecht an der raab,arndrup,arndt,arndts,arne,arne da bako,arneberg,arnebo,arneburg,arneby,arnebyn,arnecker,arneckeville,arnedillo,arnedo,arnedra subdivision,arnefiord,arnefjord,arnegard,arnegg,arnegger,arnego,arneguy,arneholm,arneirinhos,arneiro,arneiro branco,arneiro da arreganha,arneiro da azinheira,arneiro das milharicas,arneiro de fora,arneiro de milharicas,arneiro de sazes,arneiro de tremes,arneiro do pisao,arneiro dos marinheiros,arneiro tecelao,arneiros,arneiros de baixo,arneiroz,arneke,arnelas,arnelle,arnelund,arnemar,arnemark,arnemuiden,arnenis,arnenyay,arnersdorf,arnes,arnes i telemark,arnesa,arnesa,arnesano,arnesby,arnesen,arnest,arnestovice,arnetha,arnetsried,arnett,arnettsville,arneva,arnevi,arnevik,arneviken,arneviki,arnex,arney,arneyevo,arneys mount,arneytown,arnfels,arngask,arnger\303\260areyri,arnger\303\260eyri,arngisslahyltan,arnhall,arnheim,arnhem,arnhemia,arnhofen,arnholz,arnhult,arni,arni alm,arni alp,arnia,arnia khurd,arniala,arnianoyia,arnianwala,arniaro,arnica,arniceni,arnicko,arnicle,arnicourt,arnieres,arnieres-sur-iton,arnig,arnik village,arnikon,arnikou,arnilla,arnilt,arnim,arnimshain,arnimswalde,arnine,arninge,arnini,arnioniai,arnionys,arnionys 1,arnionys 2,arnionys i,arnionys ii,arniquet,arnis,arnisdale,arnish,arnishitsy,arnishtsy,arnissa,arniston,arnita,arnitha,arnitlund,arnitzgrun,arniwala,arnizo,arno,arno bay,arnoda,arnoga,arnoia,arnois,arnoje,arnok,arnol,arnold,arnold city,arnold corner,arnold heights,arnold hill,arnold line,arnold mill,arnold mills,arnold place,arnold west,arnoldov,arnoldovka,arnoldowo,arnolds,arnolds corner,arnolds landing,arnolds mill,arnolds park,arnoldsburg,arnoldsgrun,arnoldshain,arnoldshammer,arnoldsheim,arnoldshohe,arnoldsreuth,arnoldstein,arnoldsville,arnoldsweiler,arnoldtown,arnoldville,arnolec,arnoli,arnoljdovka,arnoltice,arnolticky bor,arnoltov,arnolz,arnolzhof,arnon,arnoncourt,arnoncourt-sur-apance,arnone,arnoni,arnoresco,arnoreso,arnos,arnos vale,arnosa,arnoso,arnostka,arnostov,arnostovice,arnot,arnota,arnots addition,arnott,arnoud,arnoun,arnoutov,arnoutovo,arnouville,arnouville-les-gonesse,arnouville-les-mantes,arnoux,arnoy,arnoya,arnoyhamn,arnozela,arnozelo,arnprior,arnreit,arnreith,arnried,arnsbach,arnsberg,arnsburg,arnschwang,arnsdorf,arnsdorfer berge,arnsfeld,arnsfelde,arnsgereuth,arnsgrun,arnshain,arnshall,arnshat,arnshaugk,arnshausen,arnshochstadt,arnshofen,arnside,arnsjon,arnsnesta,arnsried,arnstadt,arnstedt,arnstein,arnstorf,arnstorp,arnswald,arnswalde,arntally,arntfield,arntorp,arntully,arntz,arnuero,arnuid,arnultovice,arnum,arnun,arnun-e bala,arnut,arnutcuk,arnutovce,arnwiesen,arny,arnyash,arnyekvolgypuszta,arnzell,arnzhauschen,aro,aro achara,aro ajatakiri,aro ake eze,aro alara,aro berar,aro bokaso,aro bore banda,aro by,aro dimtu,aro genda,aro ka goth,aro kangan,aro mafoungui,aro-ndibisi,aroa,aroaar,aroab,aroaba,aroade,aroaha,aroal,aroaldao,aroams,aroams oos,aroana,aroania,aroara,aroaro,aroases,aroazes,aroba,arobadi,arobadsyauwa,arobaso,aroberifaghe,arobes,arobo,arobom,arocena,arocha,aroche,arocho,arochuku,arochukwu,arock,aroco,arod,aroda,arodaal,arodara,arode,arodhes,arodhes kato,arodoro,arodzhi,aroe,aroeba,aroebaja-barat,aroei,aroei bab,aroeidas,aroeira,aroeira de dentro,aroeira grande,aroeira ii,aroeira velha,aroeiral,aroeiras,aroeiras altas,aroeirinha,aroek,aroeke,aroeko,aroel pineung,aroengkeke,aroes,aroffe,arofo,arogafida,aroganga,arogbaw,arogbo,aroge,aroge meda,arogede,aroggou,arogi,arogno,arogongo,arogun,arogundade,arogunjo,aroh,arohan,arohane,arohemi,arohn,arohna,arohunpe,arohwa,aroi,aroifilla,aroil de baixo,aroil de cima,aroio,aroipa,aroita,aroja,arojane,aroje,aroji,arojin,arok,aroka,arokalja,arokappu,arokara,arokhatimajor,aroki,arokke,aroko,arokonie kondre,arokospuszta,arokpa,arokto,arokwa,arokyula,arokyulya,arol,arola,arolampi,aroland,arolanovo,arole,arolewa,aroley,arolikha,arolithi,arolithion,arolla,arolokovik,arolsen,aroma,aroma park,aromaitsi,aroman,aromas,aromashevo,aromat,aromatine,aromatne,aromatnoye,aromba,arombu,aromei,aromeli,aromesti,aromin,aromire,aromnel,aromo,aromona,aromyaki,aron,aron 1,aron buroh,aron kunda,aron punong,arona,arona 1,arona 2,arona hacienda,arona number 1,arona number 2,aronal,aronas,aronatika,aronbalato,arone,aroneanu,aronesti,arong,arong qi,arongan,arongan lise,arongarong,arongarong barat,arongarong timur,aronggaji,arongkoli kunda,arongo,arongsantek,arongsongdong,arongsongni,aroni,aroni chilongo,aroniadha,aroniadhianika,aroniadhika,aroniesi,aronimink,aronion,aronkpe,aronovka,aronsberg,aronsjo,arontorp,aronwold,aronwon,aronyeshi,aroo,aroochan,arood,aroodan,arooh mohammad,aroori,aroos beenaat,aroos eber,aroos ibir,aroos xaawa,aroostook,aroostook junction,arooteh bolagh,arop,arop number 1,arop villages,aropa,aropag,aropaki,aropakkuzi,aropen,aropokina,aropong,aropong camp four,aroppoe,aropu,arora,arorah,arorama,arorangi,arorata,aroraye,aroreise,arorewala jagir,aroreyse,aroreyti,aroro,arorogon,arorouata,arorouta,aroroy,arorya,aros,arosa,arosa bahnhof,arosan,arosbaja,arosbaja-barat,arosbaja-darat,arosbaya,aroscia,arosemena,arosemena tola,arosen,arosi-is,arosio,arosita,arositani,arosjakk,aroslanovo,aroso,arostegui,arosteguieta,arosund,arosurre,arosy,arota,arotaye,arotim,arotundun,arou,arou lahoucine,aroua,arouach,arouatene,aroubere,arouca,arouca village,aroue,aroueiji,aroueina,arouesma,aroufia,arouilane,arouille,aroukakope,arouled,aroumas,aroumaya,aroumbagin,aroumd,arouna,arounanga,arounbougin,around,aroundou,aroune babueur,arounfar,arounga,aroungouza,aroussi,arouti,arova,arovachivi,arovaraisa,arovia,arovichi,arovo,arow,arowala,arowojaiye,arowojede,arowolo,arowomokun,arowomole,arowosaiye,arowosaiye iju,arowu,aroy,aroya,aroybukt,aroybukta,aroye,aroyi,aroysund,aroysundet,aroz,arozai,arozdovo,arozipom,arozu,arozy,arp,arpagetik,arpi,arpunk,arpa,arpa chai,arpa chay,arpa darasi,arpa darast,arpa darehsi,arpa darreh,arpa darrehsi,arpa darst,arpa tappeh,arpa tappehsi,arpa-bulak,arpa-paya,arpaalan,arpaalani koyu,arpac,arpacay,arpacayir,arpacbahsis,arpach,arpachay,arpachevo,arpachin,arpaci,arpaciftlik,arpacik,arpacikaracay,arpacili,arpacsakalar,arpacsakarlar,arpacukuru,arpad,arpadarya,arpadarah,arpadarrasi,arpadere,arpadere koyu,arpaderekoy,arpaderen,arpaderesi,arpaderesi koyu,arpaderesikoy,arpadhalom,arpadhalommajor,arpadhaza-telep,arpadia,arpadmajor,arpadshik,arpadszallas,arpadtanya,arpadtelep,arpadtelepi tanyak,arpadteto,arpaduzu,arpadzhik,arpagadik,arpagedik,arpaguetik,arpagyadik,arpagyadyuk,arpahan,arpahan koyu,arpahani,arpaia,arpaillargues,arpaillargues-et-aureillac,arpaise,arpaja,arpajaco,arpajik,arpajon,arpajon-sur-cere,arpakesmez,arpaklen,arpaklon,arpakoy,arpalagh,arpalahti,arpala\302\261k,arpaleq,arpali,arpalik,arpalik koy,arpalikh,arpaliq,arpaliseki,arpalo,arpaly,arpalykh,arpan,arpane,arpangasia,arpaoren,arpaoz,arpaozu,arpapinar,arpaqol,arpaqowl,arpara,arpara gariadaha,arpara guriadaha,arpara mirzapur,arpas,arpasan,arpasaraycik,arpasel,arpasen,arpashal,arpasi tanyak,arpasin,arpasina,arpastanya,arpasu de jos,arpasu de sus,arpasul-de-jos,arpasul-de-sus,arpat,arpatappahsi,arpatarlasi,arpatarlo,arpataro,arpatektir,arpatepe,arpatepe koyu,arpatepekey,arpatin,arpatu,arpatu kotal,arpatuguldy,arpatuguldi,arpatuguldy,arpavar,arpaveren,arpavla,arpavon,arpayatagi,arpayaz,arpayazi,arpayeri,arpayeri koyu,arpaz,arpe,arpee,arpeh darreh,arpeix,arpela,arpelar,arpemala,arpenans,arpeni,arpentigny,arpenty,arperos,arpesina,arpezan,arphara,arpheuilles,arpheuilles-saint-priest,arphy,arpicho,arpili,arpin,arpino,arpiola,arpish,arpissa,arpista,arpit,arpita,arpitsa,arpke,arpod,arpolahti,arpolovo,arpora,arporek,arpou,arpovla,arpsdorf,arpshagen,arpuri,arpyli,arq,arq-i-shah qul,arq-i-shah quli,arqa,arqa qeslaq,arqadeh,arqah qishlaq,arqalyk,arqalyq,arqamah,arqaneh,arqarly,arqaty,arqeh,arqeh-ye now juy,arqin,arqin bolagh,arqo,arqua petrarca,arquata,arquata del tronto,arquata scrivia,arquata serivia,arqub,arque,arquejol,arquejols,arquenay,arquennes,arques,arques-la-bataille,arquettes,arquettes en val,arqueves,arquia,arquian,arquide,arquillas,arquillina,arquillinos,arquillo,arquillos,arquillos el viejo,arquinano,arquipina,arquisa,arquitecto tomas romero pereira,arquiti,arquito,arquitos,arquiya,arqun-e `olya,arqun-e sofla,arqush,arra,arra kumed,arraal,arrabaca,arrabaes,arrabal,arrabal de benablon,arrabal de jesus,arrabal de la barceloneta,arrabal de la leche,arrabal de portillo,arrabal de san francisco,arrabal del puente,arrabal las matas,arrabalde,arrabalde da ponte,arrabalde de vidriales,arrabaldo,arrabales,arrabel de la encarnacion,arrabis,arrabloy,arraca,arrach,arrachadzor,arraco,arracourt,arrad al fadhil,arrada,arrado,arradon,arrafes,arrag,arraga,arragan,arraggio,arragh more,arraguie,arrah,arraha,arraia,arraial,arraial da fainina,arraial do cabo,arraial do livramento,arraial do tremal,arraial sao bento,arraias,arraibi,arraida,arraijan,arraincourt,arraiolos,arraipite,arraipite ciudad,arraiz-orquin,arraiza,arrajadzor,arrajarvi,arrajnadzor,arrajoki,arrak,arrakul,arrakyal,arrakah,arrakoski,arram,arrameras,arramerasi,arran,arranca barba,arranca-dente,arrancacepas,arrancada,arrancaplumas,arrancosa,arrancourt,arrancy,arrancy-sur-crusnes,arrandale,arrangan,arranhadouro,arranho,arraniaga,arranja a vida de baixo,arranja a vida de cima,arranjinho,arrankorpi,arrans,arrant settlement,arrantangy,arrao de cima,arraoba,arrapi,arrapa,arrapha,arrar,arrar barar,arraras,arraronmi,arrarp,arras,arras-en-lavedan,arras-sur-rhone,arrasasan,arrasene,arrasi,arrassa,arrast,arrast-larrebieu,arrastao,arrastradero,arrastratero,arrastre,arrastres,arratashen,arratia,arraua,arrauntz,arraute,arraute-charritte,arraval jagnaya,arrawarra,arraya de oca,arrayan,arrayan dormido,arrayana,arrayanal,arrayanas,arrayancucho,arrayanes,arrayanez,arrayas,arraye,arraye-et-han,arraynes,arrayou,arrayoz,arrazcazan de lecaroz,arrazola,arre,arre telume,arre-malle,arre-molle,arreaguja,arreau,arreba,arrebenta,arrebol,arreciadas,arrecifal,arrecife,arrecife grande,arrecifes,arrecoston,arred,arredondas,arredondo,arredondo estates,arredondos,arreeiro,arrega,arregaca,arreganhado,arreguin,arreh,arreh cheshmeh,arreh furg,arreh jan,arreh kamar,arreh kuh,arreias,arreife bruno,arreigada,arreios,arrelia,arrelles,arrem bloumet,arrem ideles,arrem tazerouk,arrem-i-n-amgel,arrembecourt,arremd,arren,arrendajo,arrendamientos,arrenes,arreni,arrenkamp,arrenmolla,arrenmolle e lures,arrenoso,arrens,arrentela,arrentella,arrentes,arrentes-de-corcieux,arrentieres,arrependido,arrepentidos,arrepiado,arrepite alto,arrepol,arreruha,arres,arresa,arresi,arresodal,arrest,arresting,arret de sidi kheltab,arreti,arreton,arreux,arrey,arrez,arrez e madhe,arrez e vogel,arreza,arreza e madhe,arreza e vogel,arreze,arreze e madhe,arreze e vogel,arrezi,arrhas,arrho,arri,arri kurumiri,arria,arriach,arriacha fundeira,arriaga,arriam-eluelu,arriana,arriance,arrianwali wandhi,arriaran,arriate,arriba,arriba bocono,arriba caisan,arriba del arroyo,arriba guatopo,arriba las palomas,arriba quebrada,arriba teria,arribao,arribato,arribenos,arricau-bordes,arridwan,arrie,arriegua,arrieiro,arrielas,arrien,arrien-en-bethmale,arrier,arriera,arrieral,arrieros,arrierveld,arriestola,arrieta,arrietas,arriete,arrifana,arrifaninha,arrife,arrifes,arrigas,arrigny,arrigorriaga,arrigui,arrigunaga,arrijan,arrikope,arrilalah,arrild,arrimadero,arrimakpan,arrimal,arrimue,arrinconada,arringdale,arrington,arrington corner,arrington court,arrington estates,arrinha,arrinj,arrinmoog,arrino,arrinokpen,arrio,arriola,arriondas,arriondo,arripha,arripiado,arriquibar,arriquin,arriquivar,arris,arrishta,arrisried,arritupo,arritupo numero dos,arritupo numero uno,arrivaud,arrizada,arrizal,arrizala,arrjahur,arrmalla,arrmalle,arrn,arrni,arrnjet,arrnjeti,arro,arroba,arroberar,arrobo-guegue,arroca,arrocera,arrocera cabezas,arrocera san luis,arrocera softo,arroceras garatas,arrochan,arrochar,arrochela,arrochella,arrod,arrode,arrode-werther,arrodet,arrodets,arrodets-ez-angles,arrodets-la-barthe,arrodets-lourdes,arroeiras,arroes,arrofranco,arrogdel agua,arroia,arroial de lavrinhas,arroial velho,arroio,arroio baixo laje,arroio bonito,arroio corrente,arroio da cruz,arroio da seca,arroio de baixo,arroio do meio,arroio do pessegueiro,arroio do silva,arroio do so,arroio do tigre,arroio dos ratos,arroio grande,arroio guacu,arroio miranda,arroio moreira,arroio peixoto,arroio teixeira,arroio trinta,arroios,arrojadero,arrojado,arrojina,arrojo,arroke,arrokyulla,arrolime,arroll,arrolobos,arromanches,arromanches-les-bains,arrona,arronategui,arronches,arrondoa,arrone,arroni,arroniz,arronnes,arronte,arronville,arroro,arros,arros-doloron,arros-de-nay,arrosal,arrosales,arroses,arrota,arrotas,arrotea,arroteia,arroteia de baixo,arroteia de cima,arroteia nova,arroturas,arrou,arrouais,arrouede,arrougou,arrouis,arround,arrouquelas,arrout,arroutbet,arrow,arrow creek,arrow head,arrow hills,arrow point,arrow river,arrow rock,arrow wood,arrowbear lake,arrowhead,arrowhead acres,arrowhead beach,arrowhead equestrian estates,arrowhead estates,arrowhead farms estates,arrowhead highlands,arrowhead junction,arrowhead lake,arrowhead mobile home park,arrowhead park,arrowhead point,arrowhead ranch,arrowhead springs,arrowhead village,arrowood,arrowood estates,arrowshaiye,arrowsic,arrowsmith,arrowwood,arroyabe,arroyal,arroyal de lavrinhas,arroyal velho,arroyaso,arroyave,arroyito,arroyito challaco,arroyito del agua,arroyitos,arroyo,arroyo agua,arroyo aguacate,arroyo aguila,arroyo al medio,arroyo al medio abajo,arroyo al medio arriba,arroyo algodon,arroyo alto,arroyo amargo,arroyo amarillo,arroyo amplio,arroyo ancho,arroyo angosto,arroyo apo,arroyo apolo,arroyo arena,arroyo arenas,arroyo arriba,arroyo arroyon,arroyo azul,arroyo balado,arroyo barranca,arroyo barril,arroyo barro,arroyo baru,arroyo bella arena,arroyo bermejo,arroyo bernabe,arroyo bernabel,arroyo blanco,arroyo blanco abajo,arroyo blanco arriba,arroyo blanco del norro,arroyo boca de luna,arroyo bohio,arroyo bonito,arroyo bonitos,arroyo cabral,arroyo cacaballo,arroyo cana,arroyo canchey,arroyo cano,arroyo canoa,arroyo caraballo,arroyo carrizal,arroyo ceiba,arroyo cercado,arroyo cerezo,arroyo ceyba,arroyo chapote,arroyo chico,arroyo chico abajo,arroyo chico arriba,arroyo chinatu,arroyo chiquito,arroyo ciego,arroyo claro,arroyo cle,arroyo cojo,arroyo colorado,arroyo corozo,arroyo corozos,arroyo corto,arroyo costa,arroyo cruz,arroyo culebra,arroyo de abajo,arroyo de agua,arroyo de alvarez,arroyo de amores,arroyo de apo,arroyo de banco,arroyo de caimito,arroyo de cal,arroyo de coneto,arroyo de cuellar,arroyo de encinos,arroyo de enmedio,arroyo de felix,arroyo de higuera,arroyo de ignacio,arroyo de la cruz,arroyo de la guerra,arroyo de la luna,arroyo de la luz,arroyo de la miel,arroyo de la pita,arroyo de la plata,arroyo de la vaca,arroyo de la ventana,arroyo de la virgen,arroyo de las fraguas,arroyo de leche,arroyo de leon,arroyo de los armenta,arroyo de los frijoles,arroyo de los heros,arroyo de los huesos,arroyo de los sauces,arroyo de medina,arroyo de muno,arroyo de navas,arroyo de oro,arroyo de palos,arroyo de pedro,arroyo de piedra,arroyo de piedras,arroyo de salas,arroyo de san lorenzo,arroyo de san rafael,arroyo de san servan,arroyo de san zadornil,arroyo de soto,arroyo de valdivielso,arroyo de ygnacio,arroyo dehesa,arroyo del agua,arroyo del arco,arroyo del cabo,arroyo del cafe,arroyo del chicural,arroyo del chile,arroyo del diablo,arroyo del dulce,arroyo del estero,arroyo del gato,arroyo del grande,arroyo del guarda,arroyo del lego,arroyo del loro,arroyo del medio,arroyo del moro y los cotos,arroyo del ojanco,arroyo del potrero,arroyo del pueblo,arroyo del puerco,arroyo del rancho,arroyo del salto,arroyo del sauz,arroyo del tigre,arroyo del toro,arroyo dulce,arroyo duverge,arroyo el agua,arroyo el cabo,arroyo el cercado,arroyo el dulce,arroyo el durazno,arroyo el loro,arroyo el naranjo,arroyo el rancho,arroyo el sauz,arroyo el triunfo,arroyo encino,arroyo enmedio,arroyo fairways mobile home club,arroyo florido,arroyo frio,arroyo grande,arroyo granizo,arroyo guayabastita,arroyo guayabo,arroyo guayuyo,arroyo heights,arroyo hicacos,arroyo higuero,arroyo honda,arroyo hondo,arroyo hondo abajo,arroyo hondo primera seccion,arroyo hondo segunda seccion,arroyo hondo tercera seccion,arroyo horco,arroyo hoyo,arroyo iguana,arroyo indio,arroyo janico,arroyo jerusalem,arroyo la angostura,arroyo la barreta,arroyo la burra,arroyo la vaca,arroyo laja,arroyo largo,arroyo limon,arroyo lirio,arroyo loro,arroyo los berros,arroyo los dajaos,arroyo luca,arroyo lucas,arroyo malo,arroyo mamacas,arroyo mamacos,arroyo mamey,arroyo manteca,arroyo maria,arroyo maria cejas,arroyo mateo,arroyo medina,arroyo metate,arroyo mezquino,arroyo mina,arroyo mingo,arroyo mobile home park,arroyo molinos,arroyo moron,arroyo muerto,arroyo naranja,arroyo naranjo,arroyo negro,arroyo nevado,arroyo pajaro,arroyo parejas,arroyo pedro,arroyo perro,arroyo pescado,arroyo piedra,arroyo piedras,arroyo ponton,arroyo prieta,arroyo prieto,arroyo prieto uno,arroyo primera seccion,arroyo quemado,arroyo rancho,arroyo rico,arroyo saa,arroyo sabana,arroyo salado,arroyo salla,arroyo san pedro,arroyo santiago,arroyo santo,arroyo sardina,arroyo saya,arroyo seca,arroyo seco,arroyo seco de arriba,arroyo sotolitos,arroyo suchil,arroyo surdio,arroyo tinta,arroyo tomas,arroyo tomate,arroyo tonto,arroyo toro,arroyo toro abajo,arroyo toro arriba,arroyo urquiza,arroyo venado,arroyo verde,arroyo viejito,arroyo viejo,arroyo vista,arroyo vuelta,arroyo yayas,arroyo zacate,arroyo zapote,arroyo zarco,arroyo zaya,arroyo zuzule,arroyo-pinares,arroyofrio,arroyohondo,arroyomolinos,arroyomolinos de la vera,arroyomolinos de leon,arroyomolinos de montanchez,arroyomuerto,arroyon,arroyon de flores,arroyoo prieto,arroyos,arroyos de mantua,arroyos y esteros,arroyuelo,arroyuelos,arroz,arroz bravo,arroz sem sal,arroz-doce,arrozal,arrozal de sao sebastiao,arrozal treinta y tres,arrozal victoria,arrozales,arrozcrudo,arrozeira,arrroyo seco de abajo,arru,arrua,arruazu,arrubal,arrubial,arruda,arruda camara,arruda dos pisoes,arruda dos vinhos,arrudas,arrudeiro,arrufo,arruido,arruiz,arrujo,arruma,arruna,arrunada,arrune,arrupa,arrutbet,arruto,arrwood mill,arry,arryheernabin,arryop,arrza,arrze,ars,ars-en-re,ars-laquenexy,ars-les-favets,ars-sur-formans,ars-sur-moselle,arsa,arsa bulgare,arsa germanana,arsaas,arsabulgara,arsac,arsac-en-velay,arsache,arsadiga,arsadya,arsagermana,arsagne,arsago,arsago seprio,arsagona-ye sofla,arsague,arsaillier,arsaki,arsakion,arsakli,arsakoy,arsal kot,arsala,arsala banda,arsala kalle,arsala khan,arsala khan banglani,arsala khan islam,arsala khan jakhrani,arsala khan kelay,arsala qila,arsalakhan kalay,arsalakhan-kalay,arsalan bangallows,arsalan kuy,arsalan tash,arsali,arsamaki,arsamas,arsamoki,arsan,arsana,arsanagar,arsanah,arsanca,arsand,arsane,arsane maure,arsania,arsanjan,arsanli,arsans,arsanwewa,arsapota,arsara,arsas,arsayn-shand,arsballe,arsbeck,arsby,arsdale,arsdorf,arsede,arsego,arseh dijeh,arsele,arsemenki,arsen-kale,arsena,arsenal,arsenal hill,arsenal villa,arseneasa,arseneasca,arsenevka,arseni,arseni zir,arsenici,arsenii,arsenio,arsenio castillo,arsenio moreno,arsenion,arsenios,arsenjan,arsenka,arsenov,arsenova,arsenovici,arsenovka,arsenovo,arsenti podere mont,arsentyeva,arsentyevka,arsentyevo,arsentyevskiye,arsentyevskiye vyselik,arsentyevskiye vyselki,arsentyevskoye,arsenwewa,arsenyev,arsenyeva,arsenyevka,arsenyevo,arsenyevskiy,arset,arset i veoy,arsevica,arseyevka,arsgir,arsh el yoi,arsha,arsha khan,arsha-makhi,arshaan hural,arshaanii huryee,arshaanta suma,arshaantayn hural,arshaantayn huryee,arshaantayn sume,arshaantayn-huree,arshaantu,arshadabad,arshakhat,arshaki,arshakov,arshalinskiy,arshaluys,arshaly,arshaly 1,arshaly 1-ye,arshaly pervoye,arshamonaque,arshan,arshan khural,arshan tunkinskii,arshan zelmen,arshan-kurul,arshani khure,arshanka,arshanov,arshanovo,arshantin khure,arshanton khural,arshanton sume,arshantoyn khure,arshantu khure,arshantu somon,arshantu sumu,arshantui,arshantuin khure,arshantuy,arshantyn sume,arshantyn-khural,arshantyn-khure,arshantyyn khural,arshantyyn khure,arshaq,arshatnab,arshaty,arsheloi,arshet,arshi,arshi-lengo,arshiba,arshibah,arshichin,arshinka,arshinnyy,arshinov,arshinovka,arshinovo,arshinovskiy,arshinsk,arshinska,arshinskiy,arshinskoye,arshintsevo,arshit,arshty,arshuana,arshuk,arshuky,arshult,arshunino,arshychin,arshychyn,arshyntseve,arsi,arsi bellade,arsi dabono,arsi foulbe,arsi-bella,arsia,arsibon,arsibons town,arsica,arsichi,arsici,arsie,arsiera,arsiero,arsikere,arsila,arsilaca,arsilhac,arsilya,arsimah,arsimchin-boro,arsimont,arsin,arsinci,arsineh,arsingri,arsinjan,arsinkirud,arsinoe,arsinoi,arsinskiy,arsis,arsisar,arsita,arsitovo,arsivik,arsiya,arsiyaguda,arsiz,arsjo,arsk,arske-duk,arskogen,arskoye,arsla,arsla khan,arslan,arslan koy,arslan khodzha,arslanagica most,arslanapa,arslanbab,arslanbek,arslanbey,arslanbeyli,arslanbob,arslanca,arslancayiri,arslanci,arslancik,arsland,arslandede,arslandogmus,arslane keuy,arslane tach,arslangazi,arslanhacili,arslanhacili koyu,arslanhane,arslanhanlar,arslani,arslanizi,arslankasi,arslankaya,arslankoy,arslankuyusu,arslanlar,arslanli,arslanli koyu,arslanoba,arslanoglu,arslanovo,arslanovskiy,arslansah,arslantas,arslanyakasi,arslanyasi,arslanyazi,arslanyurdu,arslev,arsnan khural,arsnas,arso,arsoli,arsong,arsonoviche,arsonval,arsoo,arsoor,arsos,arsoun,arsouna,arsown,arspek-turspek,arst,arsta,arsta havsbad,arstad,arstadal,arstaskog,arstein,arsten,arsti,arstu,arsuf,arsug,arsuk,arsun,arsuncu,arsunda,arsuni,arsur,arsura,arsure,arsure-arsurette,arsuri,arsurile,arsurt,arsus,arsuz,arsvik,arsy,arszyczyn,art,art khvajeh,art khwajah,art village,artana,artik,artkhmo,artuma,art-sur-meurthe,arta,arta terme,artabia,artabuynk,artacak,artace,artacho,artadi,artafeia,artagnan,artagnon,artagyukh,artah bolagh,artahona,artaise,artaise-le-vivier,artaix,artaiz,artaj,artajah,artajo,artajona,artak,artakan,artaki,artaklari,artaklaru,artakovo,artakovo-vandarets,artakul,artal shammou,artala,artalaq abeh,artalens,artalens-souin,artallo,artamanovka,artamare,artamonov,artamonova,artamonovka,artamonovo,artamonovskiy,artamony,artamoshkin,artan,artana,artanawa,artand,artane,artanhazapuszta,artani,artanish,artanna,artannes,artannes-sur-indre,artannes-sur-thouet,artano,artanovskiy,artanu,artapa,artari,artariain,artarman,artarmon,artaru,artas,artaschiew,artash,artashar,artashat,artashavan,artashe,artashen,artashka,artasium,artaso,artasona,artasova,artassenx,artasuv,artaud,artavan,artavaz,artavia,artavutsogutlu,artawi,artawi ar raqqas,artawiyah,artawiyya,artaza,artazcoz,artazu,artberget,artchar,arteaga,arteaga de abajo,arteaga de arriba,arteas de abajo,arteback,artecalle,arteche,arteda,artedara,artedo,artedosa,artegia,artegna,arteh,arteh bolagh,arteijo,artejan,artejo,artek,artekhova,artekovo,artel,artel emes,artel imeni frunze,artel imeni kalinina,artel novayazhizn,artel tretya pyatiletka,artel vera,artel volnaya,artel voykova,artel vtoraya,artel vostok,artelnog,artelnoye,artelnoye aleksandrovka,artelny,artelnyy,artelshofen,artelyarshchina,artem,artem-ostrov,artema,artemarastovka,artemare,artemas,artemed,artemen-kasy,artemenki,artemenkino,artemenko,artemenkov,artemevka,artemeyka,artemeyki,artemeykova,artemgres,artemi,artemikha,artemino,artemion,artemis,artemisa,artemisales,artemisia,artemisio,artemision,artemivka,artemivsk,artemizales,artemki,artemkino,artemkovo,artemkovskaya,artemo ryastovaya,artemon,artemonovka,artemou,artemov,artemova,artemovka,artemovo,artemovsk,artemovskaya,artemovski,artemovskiy,artemovskiy rayon,artemovskiy rudnik,artemovskiy ruo,artemovskoye,artemowsk,artemowskij,artemps,artemukha,artemus,artemy,artemyeva,artemyevka,artemyevo,artemyevskaya,artemyevskiy,artemyevskoye,arten,artena,artenac,artenara,artenay,arteni,artenia,artenki,arteos,arteq makhtum,artern,arters mill,arters mill estates,arteryd,artes,artesa,artesa de lerida,artesa de segre,artesas,arteselle,artesgrun,arteshabad,artesia,artesia beach,artesia wells,artesian,artesian city,artesian village,artesiano,artesianon,artesillas,artesitas,arteta,artetan,artetjarn,arteushka,artex,arteza,artezian,artezian-mangit,artezianskiy,artfield,arth,arthabaska,arthal,artharakhera,arthaz,arthaz-pont-notre-dame,arthel,arthemonay,arthenac,arthenas,arthes,arthez,arthez-darmagnac,arthez-dasson,arthez-de-bearn,artheze,arthies,arthieul,arthington,arthingworth,arthof,arthog,artholz,arthon,arthon-en-retz,arthonnay,arthorp,arthu kalan,arthu khurd,arthun,arthuna,arthur,arthur bay,arthur city,arthur heights,arthur manor,arthur nogueira,arthur pass,arthur ridge,arthur river,arthur seat,arthur spring ford,arthur town,arthurs pass,arthurs seat,arthurs town,arthurdale,arthurmabel,arthurs,arthurs creek,arthurs mount,arthurs mountain,arthurs point,arthurs seat,arthursburg,arthurstown,arthurton,arthurtown,arthurville,arthyde,arti,artia,artia kalan,artiadimunai,artian,artianwala,artic,artica,articas,artice,artichoke,articlave,articulo ciento veintisiete,articulos,articura,articuza,artie,artieda,artiena,arties,artieta,artificio,artificio de pedegua,artigal,artigarvan,artigas,artigat,artige,artignosc,artignosc-sur-verdon,artigue,artiguedieu,artiguedieu-garrane,artigueloutan,artiguelouve,artiguemy,artigues,artigues-pres-bordeaux,artiguillon,artihan,artijan,artijere,artika,artikelly,artiki,artikion,artiklar,artikli,artil,artilanlar,artilershchina,artiles,artilleria,artilleros,artillershchina,artillery ridge,artillery village,artiman,artime,artimeili,artimes,artimet,artimi,artimino,artimonesti,artimonovca,artimonovka,artimonovo,artimony,artimovichi,artin jelaw,artin jelow,artin jilow,artingall,artins,artinse,artinskoye,artirovka,artirs,artis,artish,artishchevka,artishchevo,artissa,artist point,artista,artists view heights,artit,artitte,artiturri,artivin,artix,artiya kalan,artiz,artjamanik,artjarvi,artjomovka,artjuschino,artlenburg,artlkofen,artlsod,artlukh,artmak,artman,artmannsreuth,artmanov,artmor,arto,artogna campo,artogne,artois,artoishoek,artola,artolec,artoli,artoloq obbeh,artolsheim,artolz,artomana,arton,artonal,artondale,artonges,artonish,artonmaki,artonmyaki,artonne,artono,artopoula,artorp,artosa,artotina,artotiva,artouz,artova,artrea,artres,artrik,artrip,arts trailer court,artsakhashen,artsevi,artsai,artsakhashen,artsatayn dugang,artsatayn shiliin hiid,artsatii shiliin hiid,artsatii shiliin hiyd,artsatuin dugang,artsatuin shiliin khid,artsatuyn shiliin khid,artsatyn dugang,artsbashen,artschar,artsems,artsevanik,artsevi,artsibashevka,artsibashevo,artsimoviche,artsina,artsis,artsiskhevi,artsista,artsiyems,artsiz,artskheli,artsni,artstetten,artsvaberd,artsvakar,artsvanik,artsvanist,artsvashen,artsvenik,artsybashevo,artsybashevskiy,artsyuny,artsyz,artsyzovka,artu,artuch,artuise,artuk,artukha,artuki,artukka,artumey,artun,artupa,artur,artur de paiva,artur grande,artur nogueira,arturo bernal,arturo flores,arturo lluberas,arturo martinez adame,arturo segui,arturo vatteone,artux,artuz,artville,artvin,artvize,artwin,arty subdivision,artya-shigiri,artybash,artybul,artyk,artyk-yuryakh,artykpay,artyn,artynsk,artyom,artyrivka,artyrovka,artyshchev,artyshchuv,artyshta,artyshtinskaya,artyugino,artyukhi,artyukhino,artyukhov,artyukhovka,artyukhovo,artyukhovskiy,artyukhovskiye,artyukovo,artyukovskaya,artyushchenko,artyushenko,artyushina,artyushino,artyushinskaya,artyushka,artyushkina,artyushkino,artyushkova,artyushkovo,artyuzhata,artzan,artzaton,artzenheim,artzvanik,aru,aru asundus,aru bahru,aru baru,aru mohana,aru sajahi,aru soho,aru tangong,aru-gu,aru-okporoenyi,arua,arua camp i,arua camp ii,arua camp iii,arua camps,arua kangsur,arua kansur,aruaidu,arua-ay,aruaay,aruachani,aruaddin,aruahin,aruaia,aruail,aruailan,aruaja,aruajangalia,aruakandi,aruan,aruan point,aruana,aruarkhil,aruaru,aruata,aruatapahi,aruatapahin,aruatapay,aruau,aruba,arubai gere,arubaketsu,arubap,arubapi,arubara,arubela,arubi,arubia,arubodoru,arubub,aruc,aruca,arucak,arucas,arucato,arucevic,aruch,arucha,aruchan,aruchullo,aruco,arud,arudan,arudan pain,arudan-e bala,arudan-e pain,arudevahe,arudevakhe,arudiru,arudun,arudy,arue,arueira,arues,arufa,arufe,arufo,arufu,arugaga,arugagwu,arugaito,arugampuleliya,arugbana,arugbo,arugemuk 2,arugemuk 3,aruggammana,aruggoda,arughat bazar,arugo,arugot,aruguasa,aruguayito,arugudu,arugwadu,aruh,aruhu,arui,arui surian,aruiabad,aruibab,aruich,aruidas,aruina,aruj,aruja,arujan,arujeh,arujo,arujuane,aruk,aruk gut,aruka,arukarkudah,arukas,arukawaun,aruke,arukgammana,arukgoda,arukh,arukhlo,aruki,arukmulla,arukmulle,aruko,arukpassa,arukse,aruku,arukula,arukutti,arukuveli,arukwa,arukwa waterside,arukwatta,arukwo,arukyula,arul,arul kumer,arula,arula asundus,arulai,arulama,arulass,arule,arulgading,arulgele,aruli,arulia,aruligo,aruliho,arulili,aruliti,arulkumer,arulogun,arulpineung,arulreleen,arulrelem,arulu,arulugun,arum,arum ada,arum al jawz,arum kadaa,arum kadau,arum karimi,arum kurmi,arum-itapi,arum-karmi,aruma,arumabai,arumae,arumaha,arumahan,arumand,arumanda,arumanduba,arumandupa,arumaque,arumasaka,arumbakkam,arumbaro,arumbavur,arumbum,arumbvur,arumbwot,arume,arumetsa,arumi,arumim,arumindah,arumizu,arumjaya,arumoisa,arumon,arumonogui,arumt tapi,arumtapi,arumuganeri,arumukattanputukkulam,arumuzu,arumva,arumvale,arumwangi,arumyae,arun,arun khel,arun khel ghulam hasan,arun khetar,arun oloparun,arun qi,arun tunggai,aruna,aruna kata,arunachal,arunachaleswaranpet,arunas,arunasi,arunbari,aruncata,aruncuta,arundaya,arunde,arundel,arundel acres,arundel beach,arundel gardens,arundel hills,arundel view,arundkinar,arundolei,arundoole,arung,arungam,arungan,arunganbali,arungateli,arungba,arungbo,arunggam,arungkeke,arunle,arunlei,arunli,aruno,arunosan,arunqash,arunquipa,arunta,arunze,aruoja,aruoja asundus,aruotsa,aruoya,arup,arupaa,arupassa,arupay,arupealse,arupi,aruppala,aruppola,aruppukkottai,aruq,arur godarwala,arura,arurabad,arurah,arure,arurewala,arurewala jagir,aruri,aruri khelanwala,aruri nawan,aruria,arurogan,arurpur,aruru,arurugam,aruruhu,arus,arusa,aruscha,aruse,arusek,arusha,arusha chini,arushan fururu,arushan-fururu,arushan-kururu,arushki,arushki-ye bala,arushki-ye pain,arusi,arusingngenge,arusip,arusk,aruski,arusque,arussaare,arussi,arusta,arustan,aruste,arusu,arusukiri,aruta,arutan teri,arutanga,aruten,aruti,arutin,arutla,arutu,arutunagomer,arutunga,arutyunagamer,arutyunagomer,aruu,aruvalja,aruvalla,aruvalla asundus,aruvankadu,aruvichena north,aruvichena south,aruvyalya,aruwajoye,aruway,aruwini,aruye,aruz,aruza,aruzaman,aruzar,aruzi,aruzka,arva,arva kheyl,arva-capatanu,arvaby,arvada,arvag,arvagh,arvai janos-tanya,arvaitanya,arvaja,arvajeh,arvalli,arvallo,arvan,arvana,arvanaq,arvand,arvand kenar,arvaneh,arvanitaiika,arvanitakaiika,arvanitakeika,arvaniti,arvanitis,arvanito,arvanitokerasea,arvanitokerasia,arvanitokhori,arvanitokhorion,arvant,arvapuram,arvar,arvareh,arvarga,arvas,arvassalo,arvateasca,arvaten,arvateni,arvatesti,arvatfalva,arvati,arvayheer,arvaz-pelga,arve,arveh,arveira,arvejeh,arveka,arvel,arven,arvenitsa,arvenmaki,arverne,arvert,arvesund,arvet,arveyes,arveyres,arvi,arviat,arvich,arvida,arvidabo,arvidsberg,arvidsjaur,arvidstorp,arvidstrask,arvidsvik,arvie,arvier,arvieu,arvieux,arvigna,arvigo,arvij kola,arvik,arvika,arviken,arviksand,arvila,arvilla,arvillard,arville,arvillers,arvin,arvingetorp,arvings,arvins store,arvitanokhori,arvitsa,arviz,arviza,arviza barrena,arvola,arvondale,arvonia,arvore,arvore grande,arvore verde,arvoredo,arvores,arvoresinha,arvorezinha,arvslindan,arvsviken,arvtrask,arvyak,arvyanitsa,arvydai,arvydiske,arvystai,arwab,arwad,arwaho,arwai,arwaikheer,arwakurra,arwal,arwala,arwam,arwan,arward,arwari,arwas,arwat,arwaza,arwea,arwerd,arwesa,arwesha,arwi,arwich,arwih,arwij,arwim,arwimba,arwin,arwinj,arwop,arwos,arx,arxan,arxan bulag,arxan shi,arxang,arxat,arxiang,arxog,arxtham,ary,ary-bykovskoye,ary-tit,arya,arya darrehsi,arya nagar,arya shahr,arya`,aryab,aryadzha,aryakiban,aryamukti,aryamun,aryan,aryanagar,aryanah,aryane,aryargo,aryash,aryashevo,aryat,aryava,aryavo,aryawo,aryazh,arychagyl,aryd,arye,aryeh,aryek,aryennos,aryenos,aryevka,aryg-bazhi,aryg-uzu,aryg-uzyu,arygdam,arygyran,aryilia,aryillos,aryilokhorion,aryilos,aryinia,aryinonda,aryira,aryiradhes,aryiratika,aryiri,aryiria,aryirion,aryiro pigadhi,aryirokastron,aryirokhori,aryirokhorion,aryiromilos,aryiromouri,aryiromourion,aryiron pigadhion,aryiropoulion,aryirotopos,aryiroupolis,aryithea,aryk,aryk-sygyta,arykagyl,arykbalyk,arykhdam,arykhkent,arykova,aryktakh,arykty,arykty tretiy,arylakh,arylakhskiy,arym,arymei,aryn,arynbay,arynbyurek,aryndzh,aryndzhkend,aryngay,arynow,aryntsas,aryntsass,aryoblitar,aryong,aryp-murza,aryqbalyq,aryqty,arys,arys,arysh,arysheva,aryshevo,aryshkhasda,aryshkhazda,aryshparova,aryshparovo,aryskan,aryskannyg-aryg,aryslanbob,arystankol,arystivka,arysu,aryta,aryu,aryuta,aryutenki,aryutinki,aryy,aryy-bykovskoye,aryy-tiit,aryya aryta,aryylaakh,arz muhammad,arz muhammad brahui,arz muhammad goth,arz muhammad magsi,arz taggar,arza,arza di sotto,arza disopra,arza e poshteme,arza e poshter,arza-belyak,arzac,arzachena,arzacq,arzacq-arraziguet,arzacq-arraziquet,arzadigos,arzadon,arzaghan,arzaghan al fawqani,arzaghan at tahtani,arzah,arzai,arzak,arzak xiang,arzakan,arzakori,arzakyand,arzal,arzalde,arzam,arzaman,arzamas,arzamas-16,arzamasikha,arzamasovka,arzamastseva,arzamastsevka,arzamastsevo,arzamatovo,arzamazikha,arzamchi,arzan,arzan food,arzan fud,arzan kar,arzan zar,arzan-ar-rum,arzana,arzanah,arzanak,arzanakh,arzanan,arzanao,arzanaq,arzancheh-ye `olya,arzancheh-ye bala,arzancheh-ye pain,arzancheh-ye sofla,arzanchi,arzanci,arzand,arzaneh,arzaneh bal,arzanfud,arzanfut,arzang,arzangan,arzangan-e `olya,arzangan-e `ulya,arzangan-e sofla,arzangan-e sufla,arzangane `ulya,arzangane sufla,arzani,arzanion,arzanipur,arzanjan,arzanjeh-ye bala,arzanjeh-ye pain,arzankar,arzanko,arzano,arzanpud,arzantak,arzany,arzanzai kili,arzanzar,arzap,arzaqan,arzarak,arzarello,arzarhane faouqani,arzarori,arzat,arzata,arzate,arzava,arzaveh,arzawa,arzawah,arzay,arzayrot,arzbach,arzberg,arzberg-adelwitz,arzberg-kaucklitz,arzberg-nichtewitz,arzchviller,arzchwiller,arzdorf,arze,arze e poshter,arze e siperme,arzebelyak,arzefun,arzeh khowran,arzeh khvoran,arzeh khvoran-e bozorg,arzeije,arzell,arzembouy,arzen,arzena,arzenan,arzenc,arzenc-dapcher,arzenc-de-randon,arzene,arzeni,arzeno,arzens,arzenutto,arzercavalli,arzergrande,arzeri di sopra,arzerori,arzet,arzeu,arzew,arzfeld,arzfun,arzgir,arzgirskiy,arzgrub,arzgun,arzhaan,arzhadeyevo,arzhakovo,arzhan,arzhan-tarys,arzhan-ush-beldyr,arzhaniki,arzhanikovo,arzhano,arzhanov,arzhanovo,arzhanovskaya,arzhanovskiy,arzhanoye,arzharhane tahtani,arzheim,arzheneyevka,arzhenliy,arzher,arzheukhovo,arzhis,arzhsanovskiy,arzi,arzi bhutto,arzi chachar,arzi hakra,arzi jhal,arzi khan,arzi markhand,arzi sara,arzier,arzigal,arzigal,arzignano,arzika,arzikashibrithi,arzikus,arzil,arzila,arzilah,arzili,arzillieres,arziman,arzin,arzina,arzing,arzinka,arzinskiy,arzioglu,arzioum,arzise,arzishk,arzisk,arzitao,arzkendl,arzkoy,arzkudarak,arzl,arzl im pitztal,arzla,arzleiten,arzlohe,arzmandeh,arzmandi,arzne,arzni,arzo,arzo khel,arzoa,arzoghan,arzokhel,arzokheyl,arzolik-e bala,arzolik-e pain,arzoloq,arzon,arzona,arzonan,arzonc,arzonchi,arzonci,arzooeyeh,arzoom,arzorori,arzos,arzoui,arzoun,arzoune,arzovink,arzoz,arztal,arzthofen,arzting,arzu,arzu kheyl,arzuiyeh,arzua,arzuaga,arzubiaga,arzubikha,arzular,arzulu,arzum,arzuman,arzumand,arzumanlar,arzun,arzunah,arzunch,arzunchi,arzunti,arzuoglu,arzupinar,arzupinar koyu,arzuti,arzutu,arzuv,arzuvaj,arzuvink,arzviller,arzwaldgraben,arzwiesen,arzwiller,arzy,arzya,arzyanka,arzybovka,arzying,arzyng,arzyunakasy,arzzouy,as,as -abbah wa kafr ash shahid,as -alihiyah,as bulaq,as de copas,as ghazi,as mark,as qazi,as sadain,as sa`aidah,as sa`aidah al qibli,as sa`adah,as sa`adat,as sa`af,as sa`an,as sa`arah,as sa`ata,as sa`ayida,as sa`biyah,as sa`d,as sa`dah,as sa`danah,as sa`daniyah,as sa`dayn,as sa`diya,as sa`diyah,as sa`diyat,as sa`diyayn,as sa`diyin,as sa`i,as sa`id,as sa`idah,as sa`idah `amqun,as sa`idayah,as sa`idi,as sa`idiyah,as sa`idiyah al bahriyah,as sa`idiyah al qibliyah,as sa`nah,as sa`raniyah,as sab` abar al gharbiyah,as sab` abar ash sharqiyah,as sab` jifar,as sab`ah,as sab`an,as sabai`ah,as sabaghiyah,as sabah,as sabahani,as sabahiyah,as sabakh,as sabakha,as sabal,as sabalah,as sabarin,as sabat,as sabbaghiyah,as sabbuhiyah,as sabghan,as sabi`,as sabikhat,as sabiri,as sabiriyah,as sabkhah,as sabkhayah,as sablah,as sabriyah,as sabriyat,as sabtiya,as sabtiyah,as sabuniyah,as sabya,as sada,as sada`ah,as sadah,as sadah al hurayj,as sadaqah,as sadar,as sadarah,as sadawi,as sadayir,as sadd,as saddadah,as saddah,as saddayn,as sadidiyah,as sadr,as safa,as safaf,as safah,as safaqayn,as safaqinah,as safarat `lyal muhammad,as safarjalah,as safarqiyah,as safasif,as saff,as saffaniyah,as saffayn,as safh,as safhah,as safi,as safih,as safinah,as safiq,as safirah,as safirih,as safiyah,as safiyah wa mit al hamid,as safkun,as safnah,as safqayn,as safra,as safrah,as safsaf,as safsafah,as safsafiyah,as safya,as saghah,as saghir,as saghir muhtaraq,as sahabah ra`aynah,") sahaf,as sahafah,as sahah,as sahaji,as sahallah,as saham,as sahan,as sahanah,as sahara,as saharah,as sahari,as saharwah,as sahba,as sahhar,as sahi,as sahil,as sahil bahri,as sahil qibli,as sahili,as sahilin,as sahl,as sahlah,as sahlah al fawqiyah,as sahlat al hadriyah,as sahlayn,as sahman,as sahra,as sahrij,as sahsah,as sahsahah,as sahw,as sahwah,as said,as saihat,as saiq,as saiyid,as saiyid `abdulla,as saiyid `ali an naqib,as saiyid ahmad,as saiyid alib,as saiyid ghazi,as saiyid hamadi,as saiyid hammadi,as saiyid hamud,as saiyid hamud al hasan,as saiyid hashim,as saiyid jabir,as saiyid jasim,as saiyid majid,as saiyid muhammad,as saiyid musa,as saiyid rumaidh,as saiyid taha,as saiyid taufiq,as saiyid turfa,as saiyid-muhammad,as sajin,as sajir,as sakakini,as sakakiyah,as sakhinah,as sakhir,as sakhrah,as sakran,as saksakiyah,as sal`a,as sal`ah,as sala,as salaikh,as sala`ah,as salab,as salabah,as salafiyah,as salahat,as salahi,as salahib,as salahiyah,as salala,as salalah,as salaliyah,as salam,as salam `alayk,as salamah,as salamat,as salamiyah,as salamiyah al hait,as salamu `alaykum,as salamuni,as salatah,as salatah al jadidah,as salatinah,as salbukh,as salfah,as salhabiyah,as salhiyah,as salibiyat,as salif,as salih,as salihani,as salihat,as salihi,as salihiyah,as salihiyah al jadidah,as salijiyah,as salikah,as salil,as salim,as salimah,as salimat,as salimiyah,as salina,as saljiyah,as sallum,as salluriyah,as salman,as salmani,as salmaniyah,as salmaniyat,as salmuk,as salt,as salubi,as salul,as salulah,as saluqiyah,as sam`uriyah,as samainah,as sama`,as sama`inah,as sama`iyah,as samad,as samadiyah,as samahiyah al kubra,as samakh,as samarah,as samarinah,as samata,as samawah,as samd,as samd ash shamali,as samha,as samhah,as samhat,as samidah,as samik,as saminah,as samiriyah,as samiyah,as samkar,as samma,as sammah,as samn,as samqaniyah,as samra,as samt,as samu`,as samumah,as sanabis,as sanad,as sanafah,as sanafin,as sanafin al bahriyah,as sanafin al qibliyah,as sanajirah,as sanam,as sanama,as sanamah,as sanamayn,as sanatayn,as sanawbar,as sani,as sani`,as sanif,as saniyah,as sanqar,as sanqarab al gharb,as santah,as saq`abi,as saqaif,as saqalibah,as saqam,as saqamqam,as saqiyah,as saqlawiyah,as saqqay,as saqr,as saqrah,as saqriyah,as saqy,as sar,as sar`uniyah,as saradihah,as sarafand,as sarah,as sarahah,as sarahim,as sarahinah,as saram,as saraqina,as sarar,as sararah,as saraya,as sarayim,as sarbah,as sarbiyah,as sarfa,as sarfah,as sarfan,as sarh,as sarhat,as sari,as sarid,as sarif,as sarifah,as sarife,as sarifi,as sarih,as sarimiyah,as sarir,as saririyah,as sarj,as sarji,as sarkhan,as sarm,as sarmayn,as sarraf,as sarraji,as sarrar,as sarrarah,as sars,as sarsakiyah,as sarur,as saruwi,as sarw,as sarwah,as sarwi,as sat,as sataitah,as satarkah,as satwadayn,as sauda,as saumah,as savar,as sawa,as sawaitah,as sawa`idiyah,as sawad,as sawadayn,as sawadinah,as sawadiyah,as sawafinah,as sawahijah,as sawahliyah,as sawal,as sawalih,as sawalihah,as sawalim,as sawalim al bahriyah,as sawalim al qibliyah,as sawami`,as sawami`ah gharb,as sawami`ah sharq,as sawani,as sawaqi,as sawarah as sughra,as sawaydarah,as sawda,as sawirah,as sawiya,as sawiyah,as sawlah,as sawm,as sawma,as sawma`ah,as sawq,as sawrah,as sawwaf,as sawwan,as sawwanah,as sawwani,as sawwanih,as say`am,as sayamin,as sayani,as saybah,as sayfi,as sayh,as sayhad,as sayhah,as sayhat,as sayhudah,as sayid,as saykal,as sayl,as sayl al kabir,as sayl as saghir,as saylah,as sayliyah,as sayq,as sayqal,as sayrsah,as sayyad,as sayyadah,as sayyadi,as sayyal,as sayyalah,as sayyani,as sayyar,as sayyid,as sayyid `abbas,as sayyid `abd,as sayyid `abd allah,as sayyid `ali an naqib,as sayyid `alil,as sayyid `aziz,as sayyid `usfur,as sayyid `usfur as sayyid safi,as sayyid adib,as sayyid ahlil,as sayyid ahmad,as sayyid ghazi,as sayyid habib,as sayyid hadi,as sayyid hammadi,as sayyid hammud al hasan,as sayyid hamud,as sayyid hamud al hasan,as sayyid hani as sayyid hadi,as sayyid hasan,as sayyid hashim,as sayyid hassun,as sayyid idris,as sayyid jabir,as sayyid jasim,as sayyid mahdi,as sayyid mahdi al tufayjah,as sayyid majid,as sayyid mansur,as sayyid muhammad,as sayyid muhammad `ali,as sayyid muhij,as sayyid muhsim abu tabikh,as sayyid murhij,as sayyid musa,as sayyid qasim,as sayyid rahman,as sayyid rida,as sayyid rumayd,as sayyid sabih,as sayyid sabihih,as sayyid sha`lan,as sayyid tafi,as sayyid taha,as sayyid tawfiq,as sayyid turfah,as sayyid yasir,as sayyid yasir al hamud,as sayyidah zaynab,as sayyif,as seen,as segala,as senaga,as senuda,as sha`biniyah,as sha`ibah,as sharazah,as sharif,as sharifiya,as sharit,as shetat,as shiyab,as shu`aiba,as siadah,as si`ayyirah,as si`da,as si`dah,as sib,as siba`iyah,as sibah,as sibakh,as sibl,as sibwa,as sidadah,as sidafi,as siddiq,as siddiqin,as sidiyah,as sidr,as sidra,as sidrah,as sidriyah,as sids,as sifa,as sifah,as sifaniyah,as sifayyah,as siffi,as sifiyah,as siflah,as sifri,as sifyani,as sighawa,as sih,as siha,as sihaf,as sihai,as sihib,as sihma,as sihrij,as sihrij al gharbi,as sijn,as sijumi,as sikak,as sikhabah,as sil`,as sila,as silabah,as silah,as silam,as silaym,as silayyil,as siliyin,as sillatah,as sillihi,as simah,as simakiyah,as simeh,as similawiyah,as simlal,as sin,as sinah,as sinaniyah,as sinaytah,as sinbillawayn,as sindiyah,as sindiyanah,as siniyah,as sinnarah,as sinnariyah,as sinnatayn,as sinniyah,as sinqari,as siouane,as sir,as sira ash shamaliyah,as sirah,as siraj,as sirar,as sirm,as sirr,as sirrayn,as sirriyat,as sisniyah,as sitara,as sitarah,as siwah,as siwaytiyah,as siyafa,as siyahah,as skundar,as snudi,as sohar,as som,as soma,as soriba,as sovareh,as su`ara,as su`arah,as su`aydiyah,as su`ayf,as su`ayfah,as su`ayyirah,as su`lukiyah,as su`udiyah,as suba,as subabi,as subahi,as subakh,as subarah,as subay`at,as subayhi,as subayhiyah,as subaykh,as subaykha,as subaykhah,as subaykhi,as subayra,as subaytat,as subayyil,as subhah,as subhiyah,as subu`,as sud,as suda,as suda`,as sudah,as sudan,as sudar,as sudayr,as sudayra,as sudayrah,as suddah,as sudiyah,as sufal,as sufay,as sufaydah,as sufayhah,as sufaylah,as sufayri,as sufayy,as sufayyah,as sufeya,as suffayy,as sufh,as sufi,as sufiyah,as sufr,as sufun,as suh,as suhar,as suhaylah,as suhaylat,as suhaymi,as suhbah,as suhum,as suk,as suka`a,as sukaybat,as sukhayrah,as sukhnah,as sukhuriyah,as suki,as sukkar,as sukkariyah,as sulaik,as sulaikh,as sulaimi,as sulaimiya,as sulaqah,as sulay`a,as sulay`ah,as sulayb,as sulaybi,as sulaybikhat,as sulayf,as sulayhat,as sulayhiyah,as sulayk,as sulaykh,as sulaykhat,as sulaykiyah,as sulaymaniyah,as sulaymi,as sulaymi `abd al aziz bin khalifah,as sulaymi fahd bin `abd allah,as sulaymi khalifah bin hamad,as sulaymi qasim bin hamad,as sulaymi suhaym bin hamad,as sulaymiyah,as sulayy,as sulayyib,as sulayyil,as sulhaniyah,as sullayyil,as sultan,as sultan hasan,as sultan ya`qub,as sultan ya`qub at tahta,as sultani,as sultaniyah,as sulubiyah,as sumaisma,as sumariyah,as sumayh,as sumayn,as sumayni,as sumayqinah,as sumayrah,as summaaiyah,as summakah,as summaqiyat,as sun`,as sunay`,as sunayna,as sunaynah,as sunaytah,as sunayyif,as sunbat,as sunbullawayn,as sunduq,as sunnatayn,as sunu`,as suq,as suqay`,as suqay`ah,as suqayt,as suqayyah,as suqban,as sur,as sura,as surah,as surat al kubra,as suraybiyah,as suraydat,as surayh,as surayhah,as surayj,as suraymah,as surayri,as surr,as surrah,as surraha,as suruj,as sururab,as sururiyah,as susah,as susan,as sutuh,as suwadah,as suwadi,as suwadiyah,as suwaih,as suwaiq,as suwar,as suwari,as suwayb,as suwayda,as suwayda`,as suwaydah,as suwayda\302\261`,as suwaydi,as suwaydi al gharbi,as suwaydif,as suwaydirah,as suwaydiyah,as suwayfilah,as suwayh,as suwayhah,as suwayhiliyah,as suwayhirah,as suwayjid,as suwaylimah,as suwaymirah,as suwayni,as suwaynikh,as suwayq,as suwayr,as suwayrah,as suwayri,as suwayriqiyah,as suways,as suwaysah,as suwaysih,as suwaytiyah,as suwayyib,as suweigat,as suwwah,as suyiq,as swissi,as tangi,as yab sar,as zaqaziq,as-arsyk,as-e bad,as-e jadid,as-e qadim,as-e qazi,as-gammelbodarna,as-is,as-sid,as-yelan,as`ad,as`ad bak,as`ad bayk,as`ad beg,as`adabad,as`adiyeh,asa,asa awure,asa egba,asa nand,asa obinti,asa purwa,asa singhwala,asaila,asa-mahle,asa-oke-oko,asa-olowo,asaa,asaa-mahale,asaabad,asaaby,asaafa,asaam,asaaman,asaan,asaa\302\277a\302\277 kuzla\302\277k,asaba,asaba okpai,asaba-ase,asaba-assay,asaba-odo,asaba-ogwashi,asaba-utchi,asabaa,asabad,asabah,asabaham,asabalbali,asabamba,asabanga,asabari,asabase,asabat,asabe,asaberg,asabero,asabho,asabi,asabie,asabli,asabondeng,asaboro,asabotsy,asabri,asabyn,asaca,asacasi,asachino,asaci,asad,asad abad,asad abad tabas masina,asad anwar colony,asad daud,asad davud,asad dzmekah,asad jamekah,asad jmeka,asad kalay,asad kandi,asad kandi-ye `olya,asad kandi-ye sofla,asad kelay,asad khan,asad khaneh,asad khani,asad khel,asad kuh,asad-abad,asada,asadabad,asadabad kuchik,asadabad sanjabi-ye bala,asadabad-e,asadabad-e `olya,asadabad-e anguri,asadabad-e bala,asadabad-e buqin,asadabad-e darband,asadabad-e kamar siah,asadabad-e mahdavi,asadabad-e now,asadabad-e pain,asadabad-e pain khavaf,asadabad-e rahnama,asadabad-e rajing,asadabad-e sofla,asadabad-e tabas masina,asadabad-e vosta,asadabad-e zand,asadabad-i-khan,asadam,asadame,asadari,asadati,asadatsu,asadchiy,asadelos,asadganj,asadi,asadi al faya,asadiabad,asadiyeh,asadiyeh-e pain,asadkalay,asadkhel,asadkheyl,asadkheyl,asadli,asadly,asadnagar,asadnager,asadollah,asadollah kandi,asadollah khan,asadollahabad,asadpur,asadullah,asadullah khan,asadullah khan karez,asadullahgarh,asadullapur,asadur,asae,asaerh,asafa,asafabad,asafar kheyl,asafara,asafia,asafik,asafiri kondre,asafiyah,asaflala,asafo,asafoliza,asafovo,asafpur,asafra,asafu,asafyevka,asafyevo,asafzar,asaga,asaga-amangwu,asagaes,asagai,asagala,asagar,asagas,asagawa,asagaya,asaga\302\261 akurdalya,asaga\302\261 alsanda\302\261k,asaga\302\261 amyanto,asaga\302\261 arhimandrita,asaga\302\261 arodes,asaga\302\261 arodez,asaga\302\261 binatla\302\261,asaga\302\261 bostanca\302\261,asaga\302\261 deftera,asaga\302\261 dikmen,asaga\302\261 dikomo,asaga\302\261 kalkanla\302\261,asaga\302\261 kurtbogan,asaga\302\261 lakadamya,asaga\302\261 lakatamya,asaga\302\261 lefkara,asaga\302\261 mezraa,asaga\302\261 mylos,asaga\302\261 pirgo,asaga\302\261 platres,asaga\302\261 polemitya,asaga\302\261 pyrgo,asaga\302\261 zodya,asaga\302\261 zotya,asaga\302\261flaso,asaga\302\261kardali,asaga\302\261yayla,asagba,asagbene,asagh,asagi,asagi abceler,asagi abdurrahmanli,asagi ada,asagi adakoy,asagi agasibayli,asagi agcakand,asagi agcayazi,asagi agil,asagi agilarseki,asagi aginsa,asagi agziacik,asagi ahorik,asagi akcabuk,asagi akcak,asagi akin,asagi akpinar koyu,asagi aktas,asagi akveren,asagi aladag,asagi alagoz,asagi alinca,asagi amburdara,asagi apu,asagi arapkir,asagi arbana,asagi arim,asagi arsin,asagi arus,asagi asiklar,asagi askipara,asagi astanli,asagi avcilar,asagi aviak,asagi aviski,asagi avlayan,asagi avsin,asagi avut,asagi aybasanli,asagi aydi,asagi ayhali,asagi ayibli,asagi aylis,asagi ayranci,asagi ayrim,asagi ayva,asagi ayvatlar,asagi aza,asagi azdik,asagi azikli,asagi babakaya,asagi bakacak,asagi bakracli,asagi balahor,asagi balcikli,asagi balcilar,asagi balozu,asagi barakli,asagi baskoy,asagi bedemce,asagi bernevaz,asagi beycayir,asagi beycayirikoy,asagi beytiharun,asagi biligan,asagi bilna,asagi bolcek,asagi bolu,asagi boranderekoy,asagi bozcaarmut,asagi bradi,asagi bucaq,asagi burhan,asagi burnak,asagi burnaz,asagi buzqov,asagi caglan,asagi cakmak,asagi camanli,asagi canakci,asagi candir,asagi cangok,asagi canoren,asagi canveren,asagi capak,asagi cardak,asagi cardaqlar,asagi cat,asagi cavuslu,asagi cayhan,asagi caykuyu,asagi caylak,asagi celtik,asagi cemaller,asagi cerbaran,asagi cerit,asagi cesme,asagi cevi,asagi cevlik,asagi cibikli,asagi ciftlik,asagi cillo,asagi cimagil koyu,asagi cimbolat,asagi cimik,asagi cinpolat,asagi cobanisa,asagi cogu,asagi colkesen,asagi corumveran,asagi corumveren,asagi culhali,asagi cuma,asagi curali,asagi curub,asagi cuyanli,asagi dadikan,asagi dagli,asagi daglica,asagi dalli,asagi darlu,asagi dasagil,asagi dasarx,asagi davudi,asagi davut,asagi degirmencik,asagi demirbuk,asagi demircidibi,asagi demirler,asagi dercemet,asagi derdin,asagi derecemet,asagi derekoy,asagi deremet,asagi dibat,asagi dikme,asagi dikmen,asagi dogancilar,asagi doganoglu,asagi doger,asagi doluca,asagi doseme,asagi dosemeler,asagi dostalli,asagi dostalli koyu,asagi dover,asagi dudas,asagi dumanli,asagi durmeli,asagi duzmeydan,asagi elmali,asagi erbaun,asagi esence,asagi esenler,asagi esmisik,asagi eurbeun,asagi evler,asagi eyerci,asagi eytabi,asagi faracan,asagi fatmacki,asagi fetle,asagi filfili,asagi findikli,asagi firindere koy,asagi gaffarli,asagi galanxur,asagi garoz,asagi gemecik,asagi gemecuk,asagi gerguvan,asagi germe,asagi germi,asagi germik,asagi gevlay,asagi geylan,asagi geyran,asagi gobekli,asagi godekli,asagi gokce,asagi gokdere,asagi goklu,asagi gokoren,asagi golalan,asagi golegeni,asagi golgeni,asagi gondelen,asagi goralan,asagi gove,asagi govuk,asagi goynuk,asagi goze,asagi gozne,asagi gozungut,asagi guiney,asagi gulhali,asagi gulluce,asagi gultapa,asagi guluk,asagi gulunce,asagi gunay,asagi gundas,asagi guneykaya,asagi guvec,asagi guzdak,asagi guzelkoy,asagi guzungut,asagi hafik,asagi hamascik,asagi hamzabey,asagi hanik,asagi harapzerik,asagi harapzerk,asagi harbetil salim,asagi harik mahallesi,asagi hasilli,asagi hasinli,asagi hayduruk,asagi haylamaz,asagi hemdan,asagi hemedan,asagi hemsin,asagi hereke,asagi heretis,asagi hesat,asagi hezirge,asagi hinzorik,asagi hisiro,asagi hizyat,asagi hocalar,asagi hocuklu,asagi hoh,asagi hokoklu,asagi holan,asagi holuz,asagi hopuc,asagi hopuca,asagi hoyuk,asagi hozkisi,asagi hucuklu,asagi huh,asagi humeyre,asagi hungemek,asagi igdeci,asagi ihsangazi,asagi ihtik,asagi ilica koyu,asagi ilicek,asagi imrahor,asagi inaz,asagi irmak,asagi irmaklar,asagi irnebol,asagi irsad,asagi isiklar,asagi kabak,asagi kadikoy,asagi kagdaric,asagi kahveci,asagi kalabak,asagi kalbolas,asagi kaldak,asagi kalkanli,asagi kanara,asagi kanare,asagi kanari,asagi kanatli,asagi kanipek,asagi karabag,asagi karabahce,asagi karacaoren,asagi karacasu,asagi karacaveran,asagi karafakili,asagi karagoz,asagi karaguney,asagi karahacili,asagi karahayit,asagi karamik,asagi karaoren,asagi karasu,asagi karavaiz,asagi karavenk,asagi kardesi,asagi kargabuk,asagi karincali,asagi kartal,asagi kartuz,asagi kasaman,asagi kavel,asagi kavi,asagi kayabasi,asagi kayalar,asagi kayalidere,asagi kayi,asagi kazlar,asagi kazpinar,asagi kecan,asagi keceler,asagi kecili,asagi keferzo,asagi kekliktepe,asagi kemer,asagi kemhas,asagi kemikliyayla,asagi kepen,asagi kepirce,asagi kerte,asagi kertil,asagi kesmekaya,asagi kevzer,asagi kicik,asagi kirmani,asagi kislacik,asagi kistal,asagi kizakaya koyu,asagi kizilbel,asagi kizilca,asagi kizilkaya,asagi kiziloz,asagi kizkayasi,asagi kocaali,asagi kocadere,asagi kocas,asagi kockiran,asagi koco,asagi koliki,asagi kolugu,asagi koluksur,asagi komras,asagi koprucek,asagi kosma,asagi kosni,asagi kovik,asagi koy,asagi koyince,asagi koynuk,asagi koyu,asagi kozluca,asagi kucukortulu,asagi kukur,asagi kulince,asagi kulunce,asagi kulusagi,asagi kume,asagi kumlu,asagi kungut,asagi kurecay,asagi kuren,asagi kurthuyugu,asagi kurtikan,asagi kurtomer,asagi kusak,asagi kuyulu,asagi lagar,asagi lagpa,asagi laki,asagi lapa,asagi lapakoy,asagi layski,asagi leylegi,asagi macakli,asagi magarali,asagi mahalle,asagi makbel,asagi malax,asagi mamatli,asagi maralyan,asagi markazan,asagi mavgan,asagi mecidiye,asagi mecingert,asagi merkesler,asagi merzuk,asagi mestekan,asagi mestikan,asagi mesu,asagi meydan,asagi mezit,asagi milyas,asagi mirahmet,asagi miras,asagi misilli,asagi modan,asagi mollahasan,asagi mondolas,asagi mora,asagi mosu,asagi mozi,asagi muhurkut,asagi mulkulu,asagi nazilli,asagi nematabad,asagi nevsar,asagi norebin,asagi noxudlu,asagi nuvadi,asagi obalar,asagi obalar koyu,asagi obrugu,asagi ogene,asagi oksuzlu,asagi omerce,asagi oratag,asagi orbali,asagi osmanli,asagi ovacik koy,asagi oysuzlu,asagi oz,asagi ozdil,asagi pahur,asagi palamut,asagi pamuktas,asagi pazar,asagi pekeric,asagi penaskirt,asagi percin,asagi perek,asagi perhangok,asagi perhankok,asagi pinarbas,asagi pinarli,asagi piribeyli,asagi pogazik,asagi pozan,asagi qaraguvandli,asagi qaramanli,asagi qaramaryam,asagi qarxun,asagi qasil,asagi qislaq,asagi qolqati,asagi quscu,asagi quscular,asagi rafadinli,asagi rekan,asagi remesin,asagi revuci,asagi rumevlek,asagi runehar,asagi sabalid,asagi sal,asagi salahli,asagi salamabad,asagi salhan,asagi salkim,asagi sapaca,asagi sapca,asagi saraycik,asagi sarigol,asagi sarigolkoy,asagi sarilik,asagi sarimehmetler,asagi sarinic,asagi sarkaya,asagi saruh,asagi saruk,asagi sazcagiz,asagi senitler,asagi setrek,asagi severen,asagi sevren,asagi seyfali,asagi seyhler,asagi seyidahmadli,asagi seyitusagi,asagi sicanhuyuk,asagi sicanhuyuk koyu,asagi sihlar,asagi silsile,asagi sincirik,asagi sindam,asagi sindam mezraasi,asagi sindigik,asagi singirik,asagi sinkirik,asagi sipanazat,asagi sirkinti,asagi sirtkoy,asagi sivar,asagi sivik,asagi sofuoglu,asagi sogucak,asagi soguksu,asagi sogutlu,asagi sogutonu,asagi soku,asagi sorkun mahallesi,asagi sullu,asagi surbehan,asagi surra,asagi surtan,asagi tahirhoca,asagi tala,asagi tarpuni,asagi taslik,asagi tasmali,asagi tasyalak,asagi tavla,asagi tavuk,asagi tekke,asagi tekkekoy,asagi telfidan,asagi telli,asagi telsair,asagi temecik,asagi tepekoy,asagi termil,asagi tingir,asagi todurga,asagi tokac,asagi tokariz,asagi tolus,asagi tomik,asagi toreshav,asagi torishev,asagi torum,asagi tulakaran,asagi tulgali,asagi tulus,asagi tut,asagi ucdam,asagi ulas,asagi urkuden,asagi vartenik,asagi vartinik,asagi veretek,asagi veysalli,asagi veysi,asagi xocamusaxli,asagi yaglavand,asagi yakup,asagi yamac,asagi yarikkaya,asagi yavuc,asagi yayci,asagi yayla,asagi yazur,asagi yemiscan,asagi yenigun,asagi yenikoy,asagi yerikler,asagi yolduzu,asagi yongali,asagi yorganli,asagi yurtcu,asagi yuva,asagi zaggeri,asagi zagpa,asagi zarbana,asagi zehri,asagi zergus,asagi zeyd,asagi zeynaddin,asagi zigam,asagi zillek,asagi zipni,asagi zogunc,asagi zorava,asagi zorova,asagi zulufluhan,asagiacar,asagiada,asagiagadeve,asagiagcaguney,asagiaginsi,asagiahalik,asagiaholan,asagiahurik,asagiakca,asagiakcagedik,asagiakcagul,asagiakcicek,asagiakin,asagiakoren,asagiakpinar,asagiakpinar koy,asagiaktas,asagialagoz,asagialcili,asagialic,asagialican,asagialicli,asagialicomak,asagialinca,asagiancolin,asagiandirinli,asagiapa,asagiarabali,asagiaratan,asagiardic,asagiargit,asagiaricakli,asagiarslan,asagiarslanli,asagiasarcik,asagiatakla,asagiataklar,asagiavara,asagiavlu,asagiavra,asagiavsen,asagiavti,asagiayazca,asagiaydere,asagiaydinli,asagiayranci,asagiayvali,asagiazapli,asagiazik,asagibademli,asagibademozu,asagibadigin,asagibagdere,asagibaglica,asagibakracli,asagibalahur,asagibalcilar,asagiballik,asagibarak,asagibarakli,asagibaran,asagibatak,asagibayindir,asagibegdes,asagibelemedik,asagibenli,asagibercin,asagibernuvaz,asagibespinar,asagibeycayiri,asagibeylerbeyi,asagibeyso,asagibilenler,asagibitikci,asagibogaz,asagiboran,asagiborandere,asagiboti,asagiboynuyogun,asagibozan,asagibozkuyu,asagibuk,asagicacun,asagicaglar,asagicakmak,asagicambaz,asagicamli,asagicamozu,asagicamurcu,asagicamurlu,asagicanak,asagicanakci,asagicandir,asagicanli,asagicarikci,asagicatak,asagicatma,asagicavus,asagicayan,asagicayanli,asagicayir,asagicayircik,asagicayirli,asagicayli,asagicemgok,asagicemicuhni,asagicencul,asagicepik,asagicerci,asagicesme,asagiciftkiran,asagiciftlik,asagicigil,asagicigli,asagicihanbey,asagicilli,asagicimagil,asagicimagili,asagicimenli,asagicinik,asagicirisli,asagicit,asagicitli,asagicivanli,asagiciyanli,asagicobanozu,asagicokek,asagicoplu,asagicorten,asagicukur,asagiculha,asagicumafakili,asagidagdere,asagidaloren,asagidamlapinar,asagidana,asagidandiri,asagidanisment,asagidarica,asagidaricay,asagidarnis,asagidebek,asagidecde,asagidedegan,asagidemirci,asagidemirdogen,asagidemirkapi,asagidemirli,asagidemirtas,asagidere,asagideredibi,asagiderekoy,asagideren,asagiderince,asagidevrek,asagidikencik,asagidikmen,asagidilimli,asagidimorta,asagidodurga,asagidolay,asagidolaylar,asagidondurlu,asagidormali,asagidormeli,asagidoruklu,asagidoseme,asagiduderi,asagiduger,asagidugunculer,asagidurak,asagidurmus,asagidurucay,asagidut,asagiduvecik,asagiegerci,asagiegerli,asagieglence,asagiekcik,asagiekecik,asagiekinci,asagiekinciler,asagieksioglu,asagielmahacili,asagielmalihacili,asagielyakut,asagiemerce,asagiemirce,asagiemirhalil,asagiemirler,asagierdem,asagierhaci,asagiesen,asagiesence,asagiesenler,asagiesenli,asagiesme,asagiezbider,asagifevzipasa,asagifindiki,asagifindikli,asagifirindere,asagigerfi,asagigevla,asagigirlavik,asagigocek,asagigoglu,asagigokdere,asagigolu,asagigoluksur,asagigolyazi,asagigomez,asagigondelen,asagigorle,asagigoze,asagiguclu,asagigucuk,asagigulbahce,asagigulderen,asagigundeperi,asagigundes,asagiguney,asagiguneyce,asagiguneyci,asagiguneyse,asagigunluce,asagigurlek,asagiguvenc,asagiguzeldere,asagihabib,asagihaciahmetli,asagihacibekir,asagihacibey,asagihadim,asagihamas,asagihamurlu,asagihanbeyli,asagihanik,asagiharik,asagiharmascik,asagihastikoz,asagihatunlu,asagihayik,asagihicov,asagihinzoruk,asagihinzurik,asagiholpenk,asagihoma,asagihorum,asagihozman,asagihur,asagihuyuk,asagiickara,asagiicme,asagiigdeagaci,asagiigki,asagiihsaniye,asagiihti,asagiilica,asagiinova,asagiinoz,asagiirnabol,asagiirsat,asagiisikli,asagiisirganli,asagiizvit,asagikabakulak,asagikabulas,asagikalamus,asagikalecik,asagikamis,asagikamisli,asagikan,asagikanar,asagikaraasik,asagikarabat,asagikaraboy,asagikaracaoren,asagikaracay,asagikaradere,asagikarafakili,asagikaragoz,asagikarahalit,asagikarakaya,asagikarakisik,asagikarakuz,asagikaraman,asagikaramuk,asagikaraoren,asagikaratas,asagikaraviran,asagikargabuku,asagikargalik,asagikarinkes,asagikarkutlu,asagikarmurun,asagikartalli,asagikartoz,asagikasikara,asagikatikli,asagikatirli,asagikavacik,asagikaya,asagikayabasi,asagikayaciklar,asagikayi,asagikaymaz,asagikazan,asagikazanli,asagikece,asagikent,asagikenzek,asagikesis,asagikilicli,asagikinik,asagikiran,asagikiratli,asagikirli,asagikirzi,asagikislacik,asagikislak,asagikislak koyu,asagikislakci yaylasi,asagikizilcaoren,asagikizilcevlik,asagikizilin,asagikizilkale,asagikizlac,asagikizoglu,asagikizoren,asagikocayatak,asagikoclu,asagikolbasi,asagikonagi,asagikonak,asagikonuk,asagikopuz,asagikoren,asagikorucek,asagikose,asagikoseler,asagikoselerli,asagikosk,asagikotanli,asagikovacik,asagikoy,asagikoymat,asagikoyunlu,asagikozcagiz,asagikucak,asagikuldan,asagikulecik,asagikulluca,asagikum,asagikumlu,asagikupkiran,asagikuri,asagikurin,asagikurtoglu,asagikuslu,asagikuyucak,asagikuzfindik,asagikuzoren,asagikuzum,asagilagic,asagilahan,asagileglegi,asagilori,asagiloru,asagimaden,asagimahmutlar,asagimarkasor,asagimendelli,asagimentese,asagimerbant,asagimerinis,asagimernek,asagimernis,asagimescit,asagimicingirt,asagimiselli,asagimollaali,asagimollahasan,asagimondolos,asagimorh,asagimoz,asagimulaka,asagimulk,asagimusalla,asagimutlu,asaginardin,asaginarli,asaginarlica,asaginarlidere,asaginasirli,asaginazarova,asaginico,asaginosar,asagioba,asagiobruk,asagiocak,asagiokcular,asagioksuzoren,asagiokuzoren,asagiolcekli,asagiolek,asagioren,asagiorenbasi,asagiorenseki,asagiortaoren,asagiorukcu,asagiova,asagiovacik,asagioylum,asagioyumca,asagioz,asagiozdek,asagiozluce,asagipalamutlu,asagiparapar,asagipelitozu,asagipenesgirt,asagipeneskirt,asagipete,asagipinarbasi,asagipulluyazi,asagisagmalli,asagisahinler,asagisaklica,asagisakran,asagisalat,asagisallapinar,asagisallipinar,asagisaltik,asagisalut,asagisamanlik,asagisamli,asagisapci,asagisarica,asagisaricaali,asagisarikaya,asagisayik,asagisazcigaz,asagisazlica,asagisazlik,asagisebil,asagisehiroren,asagiselimli,asagisemayin,asagisemuk,asagiserefhane,asagiseregol,asagiserikanli,asagiserinyer,asagisevik,asagisevindik,asagisevindikli,asagiseyh,asagiseyhler,asagiseyit,asagiseyli,asagiseyricek,asagisigik,asagisih,asagisimsirli,asagisipamazat,asagisirt,asagisiva,asagisivri,asagisizma,asagisogut,asagisogutlu,asagisoku,asagisorik,asagisoylemez,asagisukunus,asagisulemis,asagisulmenli,asagisuphan,asagisurbahan,asagisusuz,asagisutasli,asagisutlu,asagitaf,asagitaife,asagitaklar,asagitandir,asagitarlacik,asagitarpini,asagitaslicay,asagitasoglu,asagitayfa,asagitefen,asagitekke,asagitepe,asagitepecik,asagitermul,asagitersun,asagitezene,asagitinaz,asagitirtar,asagitokas,asagitoklu,asagitolos,asagitoprakli,asagitoreshev,asagitoroshev,asagitorunoba,asagitoyhoca,asagitufekci,asagituglu,asagitutek,asagiugur,asagiulasli,asagiulubahce,asagiulupinar,asagiustu,asagivarlica,asagiviranseki,asagiyabanli,asagiyagcilar,asagiyagmurlu,asagiyahyasaray,asagiyakabasi,asagiyakacik,asagiyalankoz,asagiyaniktas,asagiyanlar,asagiyapici,asagiyarimca,asagiyavucuk,asagiyaylabeli,asagiyaylacik,asagiyaylim,asagiyazi,asagiyazici,asagiyenice,asagiyenigun,asagiyenikoy,asagiyeniyapan,asagiyigincal,asagiyigitler,asagiyildizli,asagiyufkali,asagiyurtcu,asagiyuva,asagiyuvali,asagizapa,asagizeytin,asagizibini,asagizilek,asagizogzunc,asagni,asagny,asagon,asagsi damlali,asah,asah kaja,asah kelod,asahadene,asahan,asahan mati,asahatay,asahatay suma,asahatayn huryee,asahatayn suurin hiid,asahbadung,asahel,asahi,asahicho,asahigawa,asahigawa fishery,asahigawagyojo,asahihama,asahikawa,asahimachi,asahiyama,asahmunduk,asahna,asahoun,asahteben,asahtegeh,asahtu sumu,asahueje,asai,asaibala,asaina,asairom,asaita,asaji,asajino,asajinodaichi,asajirio,asajoreachic,asaju,asak,asak hesar,asak hisar,asak-gisar,asaka,asakadoma,asakagbene,asakaido,asakala,asakanyam,asakasy,asakawa,asakaya,asakayamacho,asakent,asakert,asakeso,asakeso husa settlements,asakh,asakha,asakhigava,asakhtu,asakieniai,asakirli,asaklar,asaklat,asakli,asaklobo,asako,asakoamalangy,asakoamasay,asakoamasy,asakota,asakovo,asakpur,asakraka,asakro,asakrom,asaksiwak,asakty,asakuragi,asakwa,asakwam,asakyudzha,asal,asal guruke,asal khari,asal saruan,asal sulaiman,asal suleiman,asal-beylii,asala,asala fura,asala i,asala ii,asalah,asalan,asalat,asalat beg,asalat khel,asalatabad,asalatap,asalatbek,asalatganj,asalatkhel,asalatkheyl,asalatnagar,asalatwala,asalau,asalchi,asaldad,asale,asale apata,asaleh,asalem,asaletkhel,asaletkheyl,asali santoshpur,asalibi,asalien,asalikent,asalisaeter,asalla,asalli,asalmi,asalmir,asalmula,asalo,asaloka,asalpar,asalri,asalsar,asalty,asalu,asalu obose,asam,asam asam,asam kumbang,asama,asamagi,asamagidi,asamai,asamakaha,asamalla,asamama,asaman,asaman krofuso,asamanakanda,asamang,asamang-krofuso,asamang-krotuso,asamangma,asamangsua,asamaniba,asamankama,asamankese,asamankuma,asamansua,asamari,asamasi,asamaso,asamati,asambaki,asambalahy,asambar,asambetik,asambi,asambia,asamblea,asamboa,asambu,asami bula,asami bulla,asamigawa,asamihan,asamjawa,asamkalang,asamko,asamkrom,asamkumbang,asamo,asamoa,asamogawa,asamokawa,asampaneye number 1,asampaniye,asampe,asampeutik,asamponkrom,asamu,asamuk,asamushi,asan,asan aga,asan biduen,asan kalan,asan krueng kreh,asan kumbang,asan norte,asan ramphak,asan sur,asan-agar,asan-begovo,asan-yelga,asana,asana ikang,asanad,asanai,asanaisawa,asanaizawa,asanakor,asanakotuwa,asanami,asanamihara,asanandpur,asanauri,asanay,asanbagh,asanbani,asance mahala,asancor,asancoto,asandabo,asandalou,asandao,asande,asandh,asandi,asandiah,asandong,asandyi,asane,asang,asanga,asanga number 2,asangalaka,asangallane,asangamui,asangaon,asangaro,asangbomole,asangdali,asangkeng,asangori,asangran,asangrun,asangwa,asani,asanil,asanimaro,asanion,asanja,asanjan,asanjaran,asankadori,asankare,asankari,asankeri,asankhet,asankhodzhalo,asankhojalo,asanko,asankozha,asankran breman,asankran dunkwa,asankran oda,asankrangsaa,asankrangwa,asankrom,asankuduk,asanla,asanlang,asanle,asanmal,asanmimi,asannagar,asannai,asanni,asano,asanocho,asanol,asanova,asanovac,asanovo,asanovo selo,asanovshchina,asanpur,asanpur bilkaila,asansa,asansk,asanskaya,asanskoye,asanso,asansol,asanta,asantal,asantan,asantanjong,asante,asante krom,asantekurom,asantem,asanteman,asanteyao,asanti,asantiaiyo,asantiayaw,asantiaye,asantikrom,asantikrom besiasi,asantimang,asantin,asanting,asanura,asanuri,asanwinso,asany,asanyakro,asanyakrom,asanyanaso,asanyi kubegi,asanzai shaikhan,asanzai shekhan,asanzoa,asao,asaoede,asaoedi,asaog,asaowinso,asap,asapa,asapat,asape,asaph,asapi,asapoia,asapon,asapong,asapula,asapur,asaqat bilarb,asaqi xuc,asar,asar khel,asar kheyl,asar sume,asar yaylasi,asar zamin,asar zamin-e kaliab,asar-koyi,asara,asarabad,asaragac,asaragi,asarajange,asaraku,asaralia,asaralta,asaram,asarama,asarami,asaramio,asarampur,asaran,asarara,asarara kaura,asarawa,asarcay,asarci,asarcik,asarcikcamili,asarcikhaci,asarcikhacikoy,asarcikkayali,asarcikkazakli,asarco,asardami,asare,asared,asarekrom,asareku,asarekurom,asarevichi,asarganj,asari,asari-machi,asariama,asariamas,asaricho,asariyufa,asariyufa 1,asariyufa 2,asariyufa 3,asariyufa number 1,asariyufa number 2,asariyufa number 3,asarkhel,asarkheyl,asarkot,asarkoy,asarlacul,asarlai,asarli,asarlic,asarlik,asarna,asarne,asaroa,asaroba,asarogun,asaron,asaronu,asarope,asarori,asaroy,asarozuha,asarp,asarpura,asarsa,asarta,asarte,asarum,asarvan,asaryd,asas,asas ki dhani,asasa,asasal,asasan,asasang,asasangi,asasapas,asasekwa,asasewomi,asashik,asasi,asasif,asasitre,asasla,asaso,asasp,asassi,asasuni,asat,asat banda,asataka,asatdas,asatdlut,asatetkhet,asatia,asatkuari,asatly,asato,asatra,asatrapotsy,asats,asattra,asatu,asatupi,asau,asau camp,asaua,asauana,asauda todran,asaudah,asaudah todran,asaude,asaudi,asauku,asaul,asauli,asaunet,asaupura,asavaleh,asavbashevo,asavda,asavdybashevo,asaveleh,asavka,asavleh,asavo,asavo-zubovo,asavo-zusovo,asavou,asavtamak,asawa,asawa-uzel,asawaka,asawe,asawku,asawo,asawromaso,asay,asay yooura,asaya,asayaya,asayensu,asayesh,asayevskiye gorki,asayh,asayia mahale,asayis,asayita,asayitan,asayiton,asaymana,asayo,asaypur,asayta,asayya,asb charan-e bala,asb charan-e pain,asb geran,asb gir,asb kashan,asb keshan,asb khan,asb koshan,asb mirza,asb mordeh,asb rahan,asb sara,asb shahr,asb shuri,asb shurpey,asb som-e deh,asb tajnan,asb-e marz,asb-e shur pey,asba gol,asba littorio,asba,asba`un,asbab,asbabad,asbach,asbach-baumenheim,asbacherhof,asbacherhutte,asbachhof,asbacka,asbaekhede,asbagu,asbah,asbah-e do,asbah-e yek,asbahdu,asbak,asbakhu,asbakin,asbal,asban,asbar,asbar tafari,asbaro,asbasan,asbasin,asbastar,asbatafari,asbavin,asbchin,asbe taferi,asbe tefen,asbe teferi,asbechin,asbeck,asbeco,asbeek,asbeh buni,asbeh ghalyan,asbeh mard,asbeh qalyan,asbeh ris,asbeh riseh,asbeh sar,asbehbin,asberg,asberget,asbergsviken,asberry,asberrys,asbeshah,asbest,asbestan,asbestnyy,asbestos,asbestos point,asbestovskiy,asbestovyy,asbetsan,asbforushan,asbgaran,asbhar-e bala,asbi,asbi cheshmeh,asbi kola,asbi row,asbi`ah,asbiavi,asbidul,asbih,asbim,asbitah,asbmard,asbmaydan,asbmeydan,asbo,asbob,asboberg,asboga,asbogrend,asbol,asbos,asbosbam,asbovein,asbqaran,asbrahan,asbriz,asbro,asbroek,asbrunn,asbu,asbu kala,asbu kola,asbuga,asbui,asbuli,asburbum,asbury,asbury estates,asbury grove,asbury lake,asbury park,asbury prk,asbury woods,asby,asbyggeby,asbyn,asbysand,asc coculia,asca,ascaga heights,ascagnano,ascain,ascalon,ascanio,ascanti,ascara,ascarat,ascarimo,ascarraga,ascarrunz,ascarrunz hacienda,ascarza,ascaso,ascasubi,ascata,ascata hacienda,ascazubi,ascazubi alto,ascea,ascebo,ascebo argasa,ascencao,ascencion,ascension,ascension de las petas,ascerici,asch,ascha,ascha-machala,aschach,aschach an der donau,aschach an der steyr,aschach bei bad kissingen,aschaffenburg,aschahof,aschara,ascharina,ascharroudene,aschat,aschau,aschau am inn,aschau am ottersbach,aschau bei kraiburg,aschau im burgenland,aschau im chiemgau,aschauberg,aschbach,aschbach bei furstenfeld,aschbach markt,aschbacherhof,aschberg,aschbuch,aschbuden,asche,ascheberg,ascheffel,ascheho,ascheid,aschelberg,ascheloh,aschelsried,aschen,aschenau,aschenbach,aschenberg,aschenbruch,aschendorf,aschendorf-moor-siedlung,aschendorferkampe,aschendorfermoor,aschendorfkampe,aschenhausen,aschenhof,aschenhutte,aschenroth,aschenstedt,ascher,ascherbach,ascherbude,ascheres,ascheres-le-marche,aschering,ascherode,aschershain,aschersleben,ascherwinkel,aschet,aschfeld,aschhausen,aschhauserfeld,aschheim,aschhofen,aschholding,aschhoop,aschhorn,aschhornermoor,aschi,aschileu dorna,aschileu mare,aschileu mic,aschileul-mare,aschileul-mic,asching,aschio,aschland,aschlberg,aschlen,aschlenberg,ascholtshausen,aschouka,aschtal,aschthal,aschurly,aschwarden,aschwege,ascia gava,asciadira,ascialaco,asciani,asciano,ascibekirli,ascidira,ascifa,ascilar,ascio,ascira,asclipio,asco,ascochinga,ascog,ascoli,ascoli piceno,ascoli satriano,ascona,ascope,ascot,ascot corner,ascot estates,ascot under wychwood,ascot vale,ascou,ascoux,ascq,ascrea,ascros,ascstraat,ascuki,asculle,ascuna,ascuria,ascurra,ascutitele,ascutney,asdabad,asdad,asdada,asdaf,asdah,asdal,asdam,asderme,asdharmai,asdi bazar,asdif,asdim,asdima,asdimah,asdiubal,asdokomu,asdokumu,asdol,asdollahabad,asdrem,asdremt,asdro noueskak,asdro nueskak,asdum,ase,asesok,ase-ywa,asea,aseazaga,aseb,asebe,asebe tafari,asebe teferi,asebem,asebi,asebo,asebol,asebolet,asebot,asebrik,asebro,asebro huse,asebu,asebuga,asebum,aseby,asebyn,asechana,asechanu,asechatuo,asechere,asecherewa,aseda,asedobo,asedrem,asef,asefabad,asefian,aseflada,asegai,asegaon,asegara,asegard,asegasu,asegeda,asegela,asegg,aseguasua,asegwin,aseh,aseh kuh,aseh magheh,asehan,asehi,asehunda,aseibu,aseiktaung,aseimonte,asekadeo,asekan,asekandeo,asekatunda,asekeyevo,aseki,asekirli,aseko sumbi,aseko sumbu,asekovo,asekpur,asekro,asekyerewa,asel,asel barikaw,aselbek,asel-veili,asela,aselage,aselang,aselbou,aselby,asele,aselebe,aselet,aselfingen,aselholm,aselkam,asella,aselle,asellim,aselma,aselmah,aselo,aselogna,aselouane,aselya,aselye,asem,asem 3,asem 5,asem betik,asem dua,asem kulon,asem lima,asem lor,asem satu,asem tengah,asem tiga,asem wetan,asem-asem,asema,asemabad,asemakyla,aseman,aseman chal,aseman darreh,aseman dul,aseman jerd,aseman kangin,aseman kawan,asemanabad,asemandarreh,asemandul,asemanis,asemanis dua,asemanis satu,asemankyla,asemapera,asemasa,asemasem,asembagoes,asembagus,asembe,asembo,asemboniyehi,asembumbang,asemcorong,asemdepok,asemdoyong,asemena,asemendi,asemgede,asemgrowong,asemiana,aseminun,asemissen,asemjajar,asemjengkak,asemkamal,asemkandang barat,asemkandang timur,asemkatik dua,asemkatik satu,asemkau,asemkerep,asemko,asemkoembang,asemkondang,asemkrom,asemkumbang,asemlegi,asemlil,asemlulang,asemmam,asemmesjid,asemmuda,asemnunggal,asemoke,asemosi,asempanaye,asempaneye,asempaniye,asempaniyi,asempanyin,asempapak,asempapan,asempatok,asempaye,asempayin,asempayung,asempulo,asemrajeg,asemreges,asemrejo,asemrowo,asemsepuluh,asemtiga,asemuda,asen,asen anyinabrim,asen manso,asenan,asenbaum,asenby,asencion,asenda,asendabo,asendi,asendorf,asendrup,asene,asenet,aseng,asenge,asengeran,asengu,asengwe,asenham,asenherschbaum,asenhoga,aseni,aseni akrofunso,asenjan,asenjeran,asenkofen,asenkratovka,asenkritovka,asenkritovo,asenkritovskiy,asenli,asenoea,asenou kope,asenoual,asenovec,asenovets,asenovgrad,asenovo,asenovtsi,asenraij,asenray,asenreit,asensbruk,asenso,asentamiento campesino los culies,asentamiento carrizalito,asentamiento chepa,asentamiento cuatro de septiembre,asentamiento fundo rio pescado,asentamiento humano alan garcia,asentamiento humano campo amor,asentamiento humano las malvinas,asentamiento humano salamanca,asentamiento humano villa primavera,asentamiento la esperanza,asentista,asenua,asenya,asenyabun number 3,asenyebun,asenyevo,asenyevskaya,asenyevskoye,asenze,aseosi,asepe-musin,aseperi,asepsep,aseq khel,aseqi kalay,asequia,asera,aseraduya,aseral,aserani,aserawo,aserbabber,aserchevo,aserdaten,aserdoun,aserdoune,aserduya,asere,aseredok,asereh,aserema,aseret,aserewadi,aseri,aseri mois,aseriaru,aseries puts,aserif,aserillo,aserim,aserima,aseritu,aseriwali,aserkhovo,aseron,aserowedi,aserradera,aserradero,aserradero barcelo,aserradero casa quemada,aserradero ceballin,aserradero el rosario,aserradero entenachi,aserradero la encantada,aserradero la flor,aserradero la paciencia,aserradero las palomas,aserradero llano grande,aserradero los charcos,aserradero miravalles,aserradero mohinora,aserradero monterrey,aserradero pilares,aserradero rancho viejo,aserradero sanders,aserradero santa rosa,aserradero vista hermosa,aserredero,aserri,aserria,aserrio,aserrio bum,aserrio de gariche,aserrio el carmen,aserrio la fragua,aserrio primavera,aserrio tambor,aserrio wiwas,aserud,aserum,asesan,asese,aseseeso,aseseru,asesewa,aseshrif,asesieso,asesoc,asesor,asesu,asesua,aset,asetishche,asetishchi,asetofte,asetonie,asevelikyla,asewa,asewala khu,asewele,aseyaabad,aseyab ju,aseyaya,aseyd `abdollah,aseyev,aseyeva,aseyevka,aseyevki,aseyevo,aseyevskaya,asezay,asfa meda,asfaan,asfad,asfaerg,asfaja,asfak,asfaka,asfakero,asfakeron,asfal,asfal al `ayn,asfal al batin,asfal al-`ain,asfal dubbah,asfalah,asfalakton,asfalou,asfaltitovyy rudnik,asfaltovaya gora,asfam,asfan,asfanu,asfar al mahattah,asfaran,asfaranjan,asfaranjan-e jadid,asfarar,asfarenjan,asfarjan,asfarjan-e jadid,asfarrapa,asfarvarin,asfarwarin,asfastan,asfatan,asfatchioi,asfazar,asfazimer,asfejan,asfeld,asfeld-la-ville,asfenden,asfendhiles,asfendhiou,asfendhon,asfendhos,asfendiles,asfendiou,asfendiu,asfendon,asfenjan,asferar,asferg,asfestan,asfi,asfia farm,asfich,asfidaran,asfij,asfijan,asfil,asfiukh,asfla,asfoun,asfour,asfourye,asfun al mata`inah,asfun el mata`na,asfurieyh,asfuriya,asfzimmer,asg,asga,asgabat,asgad,asgai,asgaleh,asgan,asgangula,asgani,asgaour,asgapet,asgar,asgar abad,asgar abade kooh,asgarabad,asgaran,asgaray,asgarbayli,asgarbo,asgarby,asgard,asgarda,asgardan,asgarde,asgardstrand,asgari,asgarikalay,asgaripur,asgarkheyl,asgarkheyli-dzhanubi,asgat,asgata,asgeday,asgedo,asgerd,asgeriya,asgestan,asggaour,asghar,asghar ali koruna,asghar hajji,asghar jangi,asghar khel,asghar khel-e junubi,asghar khel-e shamali,asghar kheyl,asghar kheyl-e jonubi,asghar kheyl-e shomali,asgharabad,asgharabad-e tappeh,asgharay,asghari,asghari kalay,asghari kelay,asgharipur,asghariyeh,asgharkhel,asgharkhele janubi,asgharkhele samali,asgherkis,asghrkis,asghrquiss,asgikand,asgiriwalpola,asgiriya,asgoi,asgori,asgoum,asgoun,asgounn ait khalde,asgram,asguine,asguna,ash,ash mariqah,ash acres,ash creek,ash creek junction,ash field,ash flat,ash fork,ash gird,ash golan,ash grove,ash hill,ash iron springs,ash khel,ash kolang,ash kuzar,ash lake,ash mahalleh,ash manor,ash masteyan,ash mestian,ash oghulbeyn,ash point,ash priors,ash ridge,ash saqarinah,ash sh`aytiyah,ash shaib,ash shaiqiyah,ash shalah,ash sha`abi,ash sha`adirah,ash sha`af,ash sha`afah,ash sha`afiyin,ash sha`ah,ash sha`alah,ash sha`ald,ash sha`ar,ash sha`arah,ash sha`b,ash sha`bah,ash sha`bayn,ash sha`biniyah,ash sha`f,ash sha`fah,ash sha`faniyah,ash sha`fuliyah,ash sha`ibi,ash sha`lab,ash sha`laniyah,ash sha`ra,ash sha`rah,ash sha`rani,ash sha`rawi,ash sha`ri,ash sha`tah,ash sha`wah,ash shab,ash shababiyah,ash shabachah,ash shabakah,ash shabanat,ash shabaniyah,ash shabari,ash shabariqah,ash shabasiyah,ash shabatliyah,ash shabaziyah,ash shabbah,ash shabbakiyah,ash shabbaniyah,ash shabbri,ash shabbuq,ash shabburah 2,ash shabib,ash shabibiya,ash shabicha,ash shabihan,ash shabiji,ash shabikah,ash shabraqiyah,ash shabruniyah,ash shadadiyah,ash shaddadah,ash shadhiliyah,ash shaf`iyah,ash shafa,ash shafadirah,ash shafallahiyah,ash shafayyah,ash shaffaniyah,ash shaffuniyah,ash shafi,ash shafi`iyah,ash shafirah,ash shaghab,ash shaghabah,ash shaghadirah,ash shaghanibah,ash shaghaniyah,ash shaghi,ash shaghrah,ash shaghur,ash shagra,ash shagur,ash shahabi,ash shahahir,ash shahamah,ash shahaniyah,ash shahar,ash shaharin,ash shahba,ash shahbah,ash shahban,ash shahharah,ash shahi,ash shahid `abd al mun`im riyad,ash shahid ahmad badawi,ash shahid muhammad `azzam,ash shahid sahib ar ramahi,ash shahidi,ash shahil,ash shahin,ash shahiyah,ash shahizah,ash shahmah,ash shahtah,ash shahut,ash shaikh ahmad,ash shaikh hasan as suhail,ash shaikh hasan as suhayl,ash shaj`,ash shaja`ah,ash shajan,ash shajar,ash shajara,ash shajarah,ash shajlah,ash shajuf,ash shajwah,ash shakarah,ash shakhurah,ash shakiriyah,ash shakk,ash shalafah,ash shalafiyah,ash shalalah,ash shalchiyah,ash shalikiyah,ash shaliq,ash shallah,ash shallal,ash shalqah,ash shalut,ash sham`ah,ash shamaisah,ash shamah,ash shamahi,ash shamariqah,ash shamasimah,ash shamasiya,ash shamasiyah,ash shamayisah,ash shambuq,ash shamikhah,ash shamikhiyah,ash shamil,ash shamis,ash shamiya,ash shamiyah,ash shamlah,ash shammakh,ash shammam,ash shammasiyah bawa`,ash shamsiyah,ash shana,ash shanainah,ash shanabilah,ash shanawiyah,ash shanaynah,ash shanin,ash shaniyah,ash shanuriyah,ash shaq`ah,ash shaqah al yamaniyah,ash shaqarinah,ash shaqb,ash shaqduf,ash shaqif,ash shaqq,ash shaqqah al yamaniyah,ash shaqqah ash shamiyah,ash shaqqi,ash shaqra,ash shaqrah,ash sharai`,ash sharai` al `ulya,ash sharai` al mujahidin,ash sharainah,ash sharaf,ash sharafa,ash sharafah,ash sharafiyah,ash sharah,ash sharaqi,ash sharashir,ash sharawinah,ash sharawinah al bahriyah,ash sharawinah al qibliyah,ash sharayi`,ash sharbin,ash sharbinah,ash sharfa,ash shari,ash shari`a,ash sharif,ash sharifah,ash sharifiyah,ash sharij,ash sharikhiyah,ash shariqa,ash sharirah,ash shariy`ah,ash shariyah,ash sharj,ash sharjabi,ash sharjah,ash sharm,ash sharmah,ash sharq,ash sharqat,ash sharqayah,ash sharqi,ash sharqi bahjurah,ash sharqi samhud,ash sharqiyah,ash sharshar,ash sharsharah,ash sharwi,ash sharyah,ash shashi,ash shasrah,ash shatibi,ash shatlaniyah,ash shatrah,ash shatt,ash shattayn,ash shattuh,ash shawadfah,ash shawah,ash shawaia,ash shawaka,ash shawal,ash shawaliq,ash shawaribiyah,ash shawariniyah,ash shawashinah,ash shawashiniyah,ash shawati,ash shawawlah,ash shawaya,ash shawbak,ash shawbak al ghaffarah,ash shawbak al gharbi,ash shawbak ash sharq,ash shawi,ash shawiriyah,ash shawiyah,ash shawka,ash shawkah,ash shawmali,ash shawmarah,ash shawmariyah,ash shawqat,ash shawr,ash shawraqiyah,ash shawwaf,ash shawwafin,ash shaybi,ash shayhiyah,ash shayk fadl,ash shaykh,ash shaykh `abbas,ash shaykh `abbud,ash shaykh `abd al karim,ash shaykh `abd al qadir,ash shaykh `abd allah,ash shaykh `abd ar rahman,ash shaykh `adi,ash shaykh `ali,ash shaykh `ali al kurdi,ash shaykh `ali kasun,ash shaykh `alwan,ash shaykh `aqil,ash shaykh `arubi,ash shaykh `aruri,ash shaykh `awn allah,ash shaykh `ayyash,ash shaykh `aziz al jadu`,ash shaykh `ibadah,ash shaykh `isa,ash shaykh `uthman,ash shaykh abyad,ash shaykh ahmad,ash shaykh al hami,ash shaykh al hasin,ash shaykh al imam,ash shaykh al jawhari,ash shaykh an najjar abu `ali,ash shaykh badr,ash shaykh barakah,ash shaykh bu `ujaylah,ash shaykh chalabi,ash shaykh daud,ash shaykh damis,ash shaykh dann,ash shaykh dirgham,ash shaykh fadl,ash shaykh falih,ash shaykh ghali,ash shaykh hadid,ash shaykh hajju,ash shaykh hamad,ash shaykh hamid,ash shaykh hasan,ash shaykh hasan as su`ud,ash shaykh hasan as suhayl,ash shaykh hilal,ash shaykh husayn,ash shaykh husayn shar`,ash shaykh ibrahim,ash shaykh ibrahim as samawi,ash shaykh idris,ash shaykh jabbar,ash shaykh jabir,ash shaykh jarrah,ash shaykh jasim,ash shaykh jawban,ash shaykh jubayl,ash shaykh juwi,ash shaykh kanni,ash shaykh khalil,ash shaykh khurus,ash shaykh makram,ash shaykh mansur,ash shaykh mansuri,ash shaykh marzuq,ash shaykh mas`ud,ash shaykh maskin,ash shaykh miskin,ash shaykh mubarak,ash shaykh muhammad,ash shaykh mustafa,ash shaykh nahif,ash shaykh najjar,ash shaykh nasir,ash shaykh qamar,ash shaykh rahumah,ash shaykh ridwan,ash shaykh sa`d,ash shaykh sa`id,ash shaykh sa`id al `adawani,ash shaykh salamah,ash shaykh salman an nasr allah,ash shaykh shibl,ash shaykh shubaykah,ash shaykh shunayf,ash shaykh sindiyan,ash shaykh sindiyan al qibli,ash shaykh taba,ash shaykh talhah,ash shaykh timay,ash shaykh wahib,ash shaykh wassan,ash shaykh yahya,ash shaykh yusuf,ash shaykh zayn ad din,ash shaykh zayyat,ash shaykh zinad,ash shaykh ziyad,ash shaykhah mansiyah,ash shaykhali,ash shaykhan,ash shaykhiyah,ash shaykhmazhar al falih,ash shays,ash shayyit,ash shebaish,ash shebark,ash sheikh muwannis,ash shekab,ash sheyaliya,ash sheykh hamad,ash shi`ab,ash shi`ab bayt al qusami,ash shi`afiyin,ash shi`afiyun,ash shi`ah,ash shi`b,ash shi`b al aswad,ash shi`bah,ash shi`sha`i,ash shi`taf,ash shibab,ash shibli,ash shifa,ash shifah,ash shifallahiyah,ash shifayyah,ash shigil,ash shigri,ash shihabi,ash shihabiyah,ash shihah,ash shihiyah,ash shihr,ash shij,ash shija,ash shikaliyah,ash shikami,ash shilan,ash shillat,ash shim`an,ash shimaan,ash shimah,ash shimasiyah,ash shimut,ash shin,ash shinafiyah,ash shinan,ash shinanah,ash shinas,ash shinbab,ash shindakhah,ash shinnawiyah,ash shiqabah,ash shiqaq,ash shiqin,ash shiqlah,ash shiqq,ash shiqq ash sharqi,ash shiqqah,ash shir,ash shirak,ash shirayhiyah,ash shireq,ash shirs,ash shirthah,ash shirwaniyah,ash shishah,ash shishini,ash shiyab,ash shiyah,ash shizaw,ash shoona,ash showak,ash shu`aiba,ash shu`ara,ash shu`ayb,ash shu`aybah,ash shu`aybah al qadimah,ash shu`aybi,ash shu`ayrah,ash shu`ayyirah,ash shu`bah,ash shu`bayn,ash shu`iyah,ash shu`lah,ash shu`ur,ash shuaf,ash shubaychi,ash shubayk,ash shubaykah,ash shubaykiyah,ash shubayli,ash shubayliyah,ash shubayrimah,ash shubayta,ash shubrab,ash shubramiyah,ash shubran,ash shubrawayn,ash shubul,ash shudayf,ash shudud,ash shufatah,ash shufayjat,ash shufayyah,ash shuggah,ash shuhada,ash shuhada ash shamaliyah,ash shuhair,ash shuhayla,ash shuhaymiyah,ash shuhaynab,ash shuhayr,ash shuhaytah,ash shujayn,ash shukayyirah,ash shukrah,ash shula,ash shulin,ash shuliyah,ash shumah,ash shumaysah,ash shumaysani,ash shumaysat,ash shumaysi,ash shumlul,ash shumrah,ash shumrani,ash shunah,ash shunah ash shamaliyah,ash shuntur,ash shuqaiq,ash shuqaiq rukba,ash shuqaylah,ash shuqayq,ash shuqayq rukbah,ash shuqayrah,ash shuqayri,ash shuqayriyah,ash shuqqah,ash shuqr,ash shuqra,ash shuquq,ash shura,ash shurafa,ash shurafah,ash shurah,ash shurah jadid,ash shuraiqa,ash shurayf,ash shurayhiyah,ash shurayj,ash shurayk,ash shuraymah,ash shurayrah as sufla,ash shurayshirah,ash shurraf,ash shursh,ash shuruf,ash shush,ash shushah,ash shutah,ash shutayb,ash shutayfi,ash shutaytah,ash shutbah,ash shuwab,ash shuwamin,ash shuwan,ash shuwatah,ash shuwati,ash shuwayb,ash shuwaybit,ash shuwayfat,ash shuwayhah,ash shuwayhitiyah,ash shuwayhiyah,ash shuwaykh,ash shuwayqi,ash shuwayr,ash shuwayrah,ash shuwayrif,ash shuwayriyat,ash shuwayti,ash shuwayyib,ash shuyukh,ash shuzi\302\247ayf,ash springs,ash subayhi,ash sukhayri,ash vale,ash valley,ash-buzi,ash-karachanly,ash-keldek,ash-kushchu,ash-leger,ash-shab,ash-shilyan,ash-shuwairif,ash-zeid,asha,asha ada,asha atsa,asha farfow,asha gab,asha khel,asha kodzha yumer,asha-may,asha-mayli,asha-oke-obo,asha-oke-oko,ashaaladza,ashab,ashab-e kahf,ashab-i-kahf,ashaba,ashabalu,ashabari,ashabash,ashabeh-ye sharif,ashabiwala,ashabo,ashabovo,ashabwala pind,ashaga,ashaga agaly,ashaga agdzhayazy,ashaga alibeyli,ashaga alkhanly,ashaga alpout,ashaga alyaz,ashaga andamish,ashaga angelan,ashaga chardakhly,ashaga dashagi,ashaga dasharkh,ashaga dzhalgan,ashaga fyndygan,ashaga geali,ashaga karadzhanly,ashaga karakyuvendikly,ashaga karkhun,ashaga keldek,ashaga khodzham sagly,ashaga khuch,ashaga kolgaty,ashaga kyrakhkesaman,ashaga malakh,ashaga maralyan,ashaga molly,ashaga neymetabad,ashaga rafadely,ashaga seyd-akhmedly,ashaga shabalyt,ashaga veshlya,ashaga yakublu,ashaga yechan,ashaga yemazlu,ashaga zeid,ashaga zeyva,ashaga-abdurakhmanly,ashaga-arag,ashaga-araq,ashaga-arkhit,ashaga-aylis,ashaga-ayply,ashaga-buzgov,ashaga-danzik,ashaga-geynyuk,ashaga-kartas,ashaga-klishbag,ashaga-klishbog,ashaga-kyungyut,ashaga-laiski,ashaga-layski,ashaga-leger,ashaga-maka,ashaga-myulkyuli,ashaga-rameshin,ashaga-salakhly,ashaga-saral,ashaga-sarali,ashaga-seidimly,ashaga-shilyan,ashaga-stal,ashaga-stal-kazmalyar,ashaga-sura,ashaga-tsinit,ashaga-veysanli,ashaga-yarag,ashaga-yarak,ashaga-zakhit,ashaga-zeyd,ashaga-zeyzit,ashagadzhamin,ashagen,ashaghi karan,ashaghi mayan,ashaghli,ashaghy abdurahmanly,ashaghy veysali,ashaghy-jibikli,ashagi-arush,ashagi-dashagyl,ashagi-kheshat,ashagi-stal,ashagly,ashagow,ashagy abdurakhmanly,ashagy agasibeyli,ashagy agdzhakend,ashagy agdzhayazy,ashagy aksinara,ashagy amburdarya,ashagy amburdere,ashagy andamich,ashagy apu,ashagy askipara,ashagy astanly,ashagy aybasanly,ashagy aylis,ashagy ayrum,ashagy aza,ashagy bilnya,ashagy burady,ashagy buzgov,ashagy chardakhlar,ashagy chemenli,ashagy dasharkh,ashagy dzhibikli,ashagy dzhurali,ashagy emirkhanly,ashagy eskipara,ashagy filfili,ashagy filfilli,ashagy gelenkhur,ashagy geynyuk,ashagy gyuzdek,ashagy gyuzlyak,ashagy karagyuvyandli,ashagy karakhun,ashagy karamaryam,ashagy karashanly,ashagy karaymanly,ashagy kasil,ashagy keldek,ashagy khalilbeyli,ashagy kharasha,ashagy khodzhamusakhly,ashagy kolgaty,ashagy kushchu,ashagy kushchular,ashagy kyshlak,ashagy kyurdmakhmudlu,ashagy leninabad,ashagy lyaki,ashagy myulkyulyu,ashagy neymetabad,ashagy nokhudlu,ashagy nyuvedi,ashagy oksyuzlyu,ashagy rafadinli,ashagy remeshin,ashagy salakhly,ashagy salamabad,ashagy seidahmedli,ashagy seidakhmedli,ashagy seyfali,ashagy shabalyt,ashagy shilyan,ashagy shurtan,ashagy surra,ashagy tyulekiran,ashagy uzunoba,ashagy yaglevend,ashagy yaydzhi,ashagy yemezli,ashagy zeid,ashagy zeynaddin,ashagy-andzholin,ashagy-avti,ashagy-ayipli,ashagy-ayyply,ashagy-azdik,ashagy-azyk,ashagy-bayindyr,ashagy-bazyk,ashagy-begly,ashagy-bizgov,ashagy-budzhak,ashagy-budzhal,ashagy-byzgov,ashagy-chardakhar,ashagy-chaykuyu,ashagy-chemgek,ashagy-chinpolat,ashagy-dashagyl,ashagy-dibat,ashagy-duderi,ashagy-eni-yapan,ashagy-eshme,ashagy-faradzhan,ashagy-fetle,ashagy-gebekli,ashagy-gendob,ashagy-guch,ashagy-gyuneyse,ashagy-karkhun,ashagy-keferzo,ashagy-kharbetilsalim,ashagy-khemedan,ashagy-khuch,ashagy-khyumeyre,ashagy-koshma,ashagy-kushchu-kyrykly,ashagy-kyungyut,ashagy-kyuri,ashagy-layski,ashagy-leger,ashagy-lyagyar,ashagy-magaraly,ashagy-makbel,ashagy-maralyan,ashagy-merkhant,ashagy-mollu,ashagy-neymatabad,ashagy-parapar,ashagy-salat,ashagy-sarukh,ashagy-semayin,ashagy-shabalut,ashagy-sherikyanly,ashagy-shikhly,ashagy-silsile,ashagy-tala,ashagy-telfidan,ashagy-telshair,ashagy-tomik,ashagy-tyulyakeran,ashagy-uchkovash,ashagy-vartmis,ashagy-veysalli,ashagy-veysally,ashagy-yarykkaya,ashagy-yaydzhy,ashagy-zergus,ashagy-zilek,ashagy-zyulyuflyukhan,ashagyoba,ashagyy-gendob,ashahr,ashai kor,ashai zuwaira,ashaiman,ashair,ashaira,ashak,ashaka,ashakent,ashakhan,ashakhi kezen,ashakhki haft dasht,ashakhli,ashaki,ashakikabda,ashakiltubbrid,ashaklii,ashal,ashala miao,ashaladza botchway,ashalah,ashalaja,ashale anan,ashalebotwe,ashali,ashalianan,ashalim,ashalin,ashalla,ashallah,ashalo,ashaly,asham,asham bolan,asham sullam,ashama,ashammar,ashamu,ashan,ashana,ashanas,ashang,ashanga,ashangher ghari,ashanino,ashanka,ashantee,ashanti,ashanti kpoeta,ashantsy,ashanulla munsir kandi,ashap,ashapuco,ashaq banda,ashaq khel,ashaq qal`eh,ashaqabad,ashaqi chelleh khaneh,ashaqi qal`eh,ashaqi tigalan,ashaqli,ashar,ashar bunair,ashar garai,ashar kor,ashar lafia,ashara,asharai,asharai banda,asharai kalash,asharai sar banda,asharaipatai,asharampur,asharaya,asharbanr,ashareva,ashari,ashari manzai,asharia char,asharigat,asharkata,asharkot,asharkota,asharneh-alghab,asharo,asharo paiza,asharo sar,asharo sar banda,asharoanr,asharoken,asharopatai,asharosar,ashasi,ashat,ashata,ashatay,ashatayn-huree,ashatu,ashaturi,ashaug,ashausen,ashava,ashavaghira,ashavah,ashaveh,ashavn,ashaw hannek,ashawa,ashawah,ashawala,ashaway,ashaya-zeyva,ashayevo,ashbak,ashbakh,ashbank,ashbocking,ashboro,ashborough,ashborough east,ashbourne,ashbourne hills,ashbox,ashbridge,ashbrittle,ashbrook,ashbrook condominium,ashbrook park,ashburma,ashburn,ashburn junction,ashburnham,ashburton,ashbury,ashby,ashby cum fenby,ashby de la launde,ashby de la zouch,ashby gap estates,ashby magna,ashby parva,ashby place,ashby ridge estates,ashby run,ashby saint ledgers,ashby yards,ashbyburg,ashbys corner,ashcake,ashcamp,ashche-butak,ashche-butakski,ashche-butakskiy,ashche-kuduk,ashche-kum-kul,ashche-sai,ashche-say,ashcheb,ashchebutak,ashchegul,ashchekol,ashchelisay,ashchelsay,ashchepovskoye,ashcherino,ashcherkino,ashchi,ashchi kuduk,ashchi-bulak,ashchi-kara-su,ashchi-kuduk,ashchi-kul,ashchi-say,ashchibulak,ashchibulakskiy,ashchibutak,ashchikarabau,ashchikesen,ashchikol,ashchikuduk,ashchikulskiy,ashchilsay vtoroy,ashchilysay,ashchimuryn,ashchisay,ashchisayskiy,ashchisu,ashchitobelik,ashchiy,ashchukino,ashchybulak,ashchykol,ashchykol,ashchysay,ashcom,ashcott,ashcraft corner,ashcroft,ashcroft addition,ashdada,ashdadah,ashdadeh,ashdalaq-e bala,ashdalaq-e pain,ashdale,ashdale junction,ashdati,ashdod,ashdod alef,ashdod c,ashdod dalet,ashdod gimel,ashdod gimmel c,ashdod on the sea,ashdod yam,ashdon,ashdot ya`aqov,ashdot ya`aqov alef,ashdot ya`aqov bet,ashdot ya`aqov ihud,ashdot ya`aqov meuhad,ashdot yaakov meuhad,ashdot yaccov,ashdot-yaacov,ashdoth ya`aqov,ashdown,ashdown park,ashe,ashe `in,ashe kabyu,ashe mudu,ashe wunbade,ashe-magyibin,ashebette,ashebo,asheboro,ashebre,ashebrook,ashebrook park,ashebyin,ashedaik,ashefe,ashegh hesar,ashegheh,asheghloo meikhan,ashegoda,ashegon,ashegyaung,asheh,ashehu,asheim,ashekar kola,asheklii,ashekpur,ashekromino,ashel,asheldham,ashelekke,ashemodu,ashen,ashena,ashenakhur,ashenat,ashenawuru,ashendon,ashenestan,ashenga,ashens,ashepoo,ashepoo crossing,ashepoo siding,asheqli,asher,asher glade,ashera,asherat,asherifa,ashern,ashers fork,asherton,asheru,asherville,asheshe,ashestan,ashetaik,asheton,asheva,ashevany,asheville,ashevo,ashewa,ashewele,ashey,asheye,asheyevo,asheykh hesar,ashezai,ashezay,ashfield,ashford,ashford acres,ashford carbonel,ashford carbonell,ashford hollow,ashford junction,ashfordby,ashg,ashg-e zari,ashgabadskaya ptitsefabrika,ashgabat,ashgalar,ashgarabad,ashgeh tochal,ashgerd,ashgestan,ashgetu,ashghaloo,ashgoft murd,ashgoft-e manganan,ashgrove,ashgrove bridge,ashhar,ashhill,ashhur,ashhurst,ashi,ashi arage,ashi baram,ashi-bulak,ashiabju,ashiak,ashiam,ashian,ashianak,ashianeh,ashianeh auliya,ashianeh-ye `olya,ashianeh-ye bala,ashianeh-ye pain,ashianeh-ye sofla,ashiarai,ashibe,ashibetsu,ashichavshin,ashida,ashidambai,ashidambaru,ashidanbaru,ashidanibara,ashidawhwe,ashidohwi,ashidun,ashie,ashiem,ashien,ashienso,ashiesteel,ashieye,ashifinka kozinka,ashiga,ashigashiya,ashigasiga,ashigaura,ashige,ashiharacho,ashihe,ashihe jiedao,ashihho,ashihko,ashihkung,ashihniulu,ashiho,ashijiabu,ashik,ashik colony,ashik seneklii,ashik senekliy,ashik-ata,ashikaga,ashikan,ashikawa,ashikesen,ashikikalay,ashikkala,ashikkheyl,ashiklar,ashiklare,ashiklari,ashiklii,ashikovo,ashikuraji,ashil-togay,ashili,ashill,ashilta,ashim,ashim kstav,ashima,ashimbe,ashimi,ashimine,ashimmi,ashin,ashin bala,ashin kirud,ashin pain,ashin-e `olya,ashin-e bala,ashin-e pain,ashin-e sofla,ashinad,ashinan,ashinga,ashinginski post,ashinginskiy,ashingo,ashingsk,ashington,ashinhaib,ashinkhorr,ashino,ashinoura,ashinoyu,ashinskiy,ashintilly,ashinu,ashio,ashipa,ashippun,ashiq,ashiq ali khan,ashiq ali magsi,ashiq muhammad,ashiqabad,ashiqpur,ashir,ashir-buka,ashir-dzhilyan,ashir-khobla,ashireh zamel,ashiri,ashiro,ashirova,ashirovo,ashisaki,ashisawa,ashish al jahrah,ashisha,ashistan,ashit,ashita,ashitaka,ashitake,ashitasty,ashitbash,ashitiki,ashitkovo,ashito,ashitobelik,ashiu,ashiy,ashiya,ashiyaneh olya,ashiyaneh sofla,ashiyoro,ashiyorobute,ashiyorobuto,ashizada,ashizai,ashizaki,ashizara,ashizawa,ashizay,ashjerd,ashjird,ashk,ashk dez,ashk-e jahan pahlu,ashka,ashkabad,ashkada,ashkadan,ashkadar,ashkadarovo,ashkaf,ashkafkti,ashkaft,ashkaft saqah,ashkaft-e hinduwan,ashkaft-e mara,ashkafta,ashkaftah,ashkaftan,ashkah,ashkahil,ashkakzar,ashkal,ashkala,ashkali,ashkallar,ashkam chal,ashkam kuh,ashkaman,ashkamchal,ashkan,ashkan-e bala,ashkanan,ashkand,ashkanpur,ashkanun,ashkar,ashkar meydan,ashkara,ashkarai,ashkareh,ashkarkhil,ashkarlet,ashkarnagar,ashkasham,ashkastan,ashkat maneh,ashkaul,ashkavand,ashkawta,ashkazar,ashkeh su,ashkeit,ashkeit s.,ashkeldino,ashkelon,ashkemchal,ashkestan,ashkezar,ashkhabad,ashkhaneh,ashkheyl,ashkheyl,ashki,ashki halulan,ashki mahalleh,ashkida,ashkidah,ashkifta,ashkik,ashkin-sala,ashkinak,ashkinskiy,ashkirk,ashkjerd,ashkoft murd,ashkohran,ashkokulla,ashkol,ashkot,ashkova,ashkovo,ashkovo-nizhneye,ashkovo-verkhneye,ashku,ashkuiyeh,ashkudah,ashkuh,ashkui,ashkum,ashkuman,ashkun,ashkung,ashkur mahalleh,ashkurep,ashkutan,ashkutu,ashkutuiyeh,ashkutuiyeh,ashkvan,ashkyshla,ashla,ashlan,ashlan-bilyamor,ashlan-vershina,ashland,ashland center,ashland city,ashland estates,ashland heights,ashland hills,ashland junction,ashland landing,ashland mill,ashland park,ashlane cross roads,ashlanka,ashlar,ashlar hill,ashleigh,ashlers,ashleworth,ashley,ashley acres,ashley bank,ashley clinton,ashley corner,ashley creek,ashley creek village,ashley crossing,ashley downs,ashley falls,ashley forest,ashley grove,ashley hall,ashley harbor,ashley heights,ashley junction,ashley manor,ashley mobile home park,ashley oaks,ashley park,ashley retreat,ashleys,ashleyville,ashlim,ashling place,ashliz,ashlock,ashlogh,ashly,ashlyk,ashma,ashmakharapkyar,ashman kamchal,ashmanhaugh,ashmansworth,ashmaq,ashmarina,ashmarino,ashmasiyan,ashmead,ashmead village,ashmen,ashmere,ashmesian,ashmira,ashmisian,ashmizan,ashmont,ashmore,ashmun,ashmun ar rumman,ashmun el-rumman,ashmura,ashmurah,ashmyany,ashna,ashna khowr,ashna khvor,ashnai,ashnaabad,ashnafar,ashnaggern,ashnak,ashnakhor,ashnam,ashnan,ashnaravan,ashnarovo,ashneh dar,ashnestan,ashni,ashnijeh,ashniji,ashnistan,ashniyeh,ashniz,ashniz-e bala,ashnokhur,ashnooyeh,ashnu-makhi,ashnyak,ashoba,ashoban,")hogho,ashoje,ashok nagar,ashokan,ashoknagar,ashokola,ashokpur,asholt,ashomo,ashon,ashong,ashonman,ashoop,ashoor,ashoqa,ashoqah,ashor,ashor daywanah,ashor dewanah,ashor khel,ashor-dzhilyan,ashorabad,ashori,ashorkhel,ashorkheyl,ashoro,ashorobuto,ashorva,ashotavan,ashotsk,ashoua,ashouman,ashover,ashow,ashowada,ashpan,ashpari,ashpatsk,ashpatskaya,ashpatskoye,ashpaykovo,ashperlova gora,ashperlovo,ashperlovo gora,ashperton,ashpi khwar,ashpikhvar,ashport,ashport landing,ashpur,ashpurven,ashqabad,ashqal,ashqeh,ashqelon,ashqi banda,ashqolka,ashquelon,ashquita,ashra,ashraf,ashraf abad,ashraf colony,ashraf darreh,ashraf jat,ashraf kalay,ashraf kelay,ashraf khan,ashraf khan mela,ashraf khanwala,ashraf khel,ashraf khelanwala,ashraf kheyl,ashraf mahar,ashraf mozhana,ashraf nmasai,ashraf shah,ashrafabad,ashrafdal,ashrafi,ashrafiyah,ashrafiyat al `abbas,ashrafiyat al wadi,ashrafiyat sahnaya,ashrafkhal,ashrafkheyl,ashrafkheyl,ashrafpur,ashrafwala,ashraghari,ashrah,ashrait,ashrakalay,ashralar,ashrama,ashre kelay,ashreigney,ashret,ashri kalay,ashridge,ashrog,ashrogai,ashruba,ashrubah,ashruwat,ashsha,ashshaghi qal`eh,ashshaq qal`eh,ashsprington,asht,asht e likajt,ashta,ashta manisha,ashtabarga,ashtabi,ashtabula,ashtachin,ashtadhar,ashtadhar baliapara,ashtadona,ashtagharia,ashtagin,ashtaj,ashtajak,ashtajin,ashtak,ashtakhma,ashtakhu,ashtakhun,ashtakin,ashtami,ashtamnisha,ashtanah,ashtani,ashtapaika,ashtarak,ashtarakats gyugh,ashtargan,ashtariyeh,ashtark,ashtarkan,ashte,ashtead,ashteh dar,ashtejeh,ashterk,ashteyan,ashteyaneh,ashti,ashti bidh,ashti bolagh,ashtian,ashtianeh,ashtigar,ashtijeh,ashtilah,ashtiway,ashtiyan,ashtjeran,ashtodona,ashtogram,ashtola,ashton,ashton corners,ashton glen,ashton hall,ashton in makerfield,ashton keynes,ashton manor,ashton place,ashton pond,ashton river estates,ashton under hill,ashton wooden bridge,ashton-under-lyne,ashtown,ashtree,ashtur murdah,ashty,ashu,ashu shumaysi,ashuiyeh,ashua,ashub,ashuban,ashudasty,ashue,ashuelot,ashuganj,ashuk,ashuka,ashukan,ashukino,ashukinskiy,ashukovo,ashult,ashuluk,ashum,ashun,ashunda,ashup,ashuqah,ashur,ashur adeh,ashur husain,ashur pashur,ashur-e bozorg,ashur-e kuchek,ashura deh,ashurabad,ashuradeh,ashuradeh-ye bozorg,ashuradeh-ye kuchak,ashuradeh-ye kuchek,ashuran,ashuri,ashuriyin,ashurkhuseyn,ashurkin,ashurkino,ashurkovo,ashurli,ashurlu,ashurst,ashusby,ashuta,ashutasty,ashuvud,ashuwayhah,ashuwei,ashverfi,ashville,ashwadia,ashwal,ashwanich,ashwaraopet,ashwater,ashwaubenon,ashwe,ashweir,ashwell,ashwellthorpe,ashwenanmo,ashwenanyo,ashwi,ashwi budrukh,ashwi khurd,ashwick flat,ashwilich,ashwok,ashwood,ashwood estates,ashwood manor,ashwoods,ashyana mills,ashyanak,ashyaneh `olya,ashyaneh sofla,ashygalylar,ashygbayramly,ashygly,ashykhbayramly,ashykhly pervoye,ashykhly vtoroye,ashynga,asi,asi bolagh,asi es la vida,asi khel pakha,asi kola,asi korija,asi moniyi,asi nawada,asi panchkahania,asia,asia abad,asia amanhie,asia bad,asia barak,asia bist,asia gol,asia kamul,asia khairabad,asia khaneh,asia kheyrabad,asia kol,asia kowl,asia nnentu,asia posht,asia sangi,asia umu nka,asia umu-ukia,asia-i-badak,asia-push,asia-ye badak,asia-ye bini,asia-ye chap,asia-ye firqah,asia-ye gudi,asia-ye gurg,asia-ye kheyrabad,asia-ye muhammad ibrahim khan,asia-ye murdian,asia-ye sharaf,asia-ye sokhtah,asia-ye sukhteh,asiaabad,asiab,asiab abad,asiab bar,asiab barq,asiab dargah,asiab darreh,asiab dasht,asiab gabri,asiab jub,asiab jub-e amjadi,asiab jub-e farmanfarma,asiab kharabeh,asiab khowrdi,asiab posht,asiab qermez,asiab sar,asiab sham`,asiab shur,asiab-e badak,asiab-e bala,asiab-e bala-ye bagh-e keleh,asiab-e gazbor,asiab-e gowdi,asiab-e haj `ali,asiab-e hamzeh,asiab-e kamud,asiab-e khan,asiab-e kheyrabad,asiab-e mian dasht,asiab-e morteza,asiab-e musa,asiab-e posht,asiab-e shah teymur,asiab-e shamsabad,asiababad,asiabad,asiabak,asiabak-e band,asiabaleh,asiaban,asiabar,asiabarak,asiabeg,asiabid,asiabist,asiabkhordi,asiabkhurdi,asiacha,asiachefteh,asiadib,asiagak,asiago,asiah khel,asiain,asiaji,asiakheyl,asiakheyl,asiaki,asiakol,asiakul,asiakul,asiakwa,asiam,asian,asianwala,asiar,asias,asiasham,asiasito,asiata,asiavak band,asiavaleh,asiay-e pain,asiayaco,asiayaco hacienda,asiayi-badak,asiayi-bini,asiayi-chap,asiayi-firka,asiayi-gudi,asiayi-gurg,asiayi-mukhammed-ibrakhimkhan,asiayi-sharaf,asiayi-sokhta,asib soufli,asibakau,asibanglan,asibe,asibi,asibonga,asiboro,asibucak,asibuokrom,asicavsin degirmeni,asich,asid,asid ebrahim,asid mohammad,asidani,aside,asideniya,asidiran,asidonhopo,asidonhoppo,asidonkondre,asidoronga,asidowui,asidun,asie,asiego,asiekrom,asiempong,asiene,asienimpon,asientillo,asientillos,asiento,asiento bonita,asiento bonito,asiento camarones,asiento conchopata,asiento de camarones,asiento de la catalina,asiento de luisa,asiento de majibacoa,asiento luisa,asiento majiboca,asiento minero de macha,asiento minero de pacha,asiento minero macha,asiento queimado,asiento santo thomas,asiento viejo,asiento viejo coco de mono,asiento viejo el burro,asiento viejo el penon,asiento viejo el zurron,asiento viejo la fundacion,asiento viejo la perdida,asiento viejo quebrada nueva,asiento viejo taparito,asiento zapata,asientos,asientos de luisa,asier,asieso,asieu,asif,asif ait tamellil,asif block,asif khel kelay,asif mkoune,asif mgorn,asif ou azzal,asifabad,asifane,asifgarh,asiga,asigala,asigale,asige,asigeran,asiget,asighar,asigi,asigiran,asigo,asigonia,asih,asihjara,asik veysel,asika,asikabew kese,asikabiu,asikaga,asikaisu,asikan,asikaraman,asikaso,asikasu,asikasuanafo,asikathi,asikati,asikawa,asikbuku,asike,asikeran,asikeri,asikiran,asikkala,asikkoy,asiklar,asikli,asikoglu,asikoma,asikoria,asikoro,asikouei,asikovac,asikovci,asikovo,asikoy,asikpai,asikpasa,asikulam,asikuli,asikuma,asikura,asikuzeyir,asikwa,asikzulali,asil,asil beili,asil khel kelay,asil kheyl kalay,asila,asilabad,asilah,asilal,asilang,asilanga,asilantir,asilanyi,asilap,asilbaki,asilbeyli,asile,asileke,asili,asiliarmut,asilima,asilimapua,asiling,asilkhel kalay,asilla,asillo,asilo,asilo banda,asilo juan,asilo kalle,asilo la piedad,asiloeloe,asilulu,asim,asim choke,asim zeneli,asim-zenel,asimaenioha,asimana,asimanioha,asimasi,asimba,asimboa,asime korpe,asimenion,asimi,asimion,asimiro,asimisimiwu,asimochori,asimochorion,asimokhori,asimokhorion,asimpa,asimpunta,asimpur,asimpur balipatti,asin,asin bereku,asin boji,asin brofoyedur,asin de brote,asin de broto,asin kumase,asin kumasi,asin mampon,asin manso,asin nyankomase,asin nyankumasi,asin odumase,asin praso,asina,asinae,asinagua,asinai,asinan,asinansari,asinava,asind,asinda,asindage,asindi,asindonhopo,asindro,asine,asiner,asinet,asing,asinga,asinga-via,asingammedda,asingan,asingaran,asingarn,asinge,asingen,asingero,asingiran,asingo,asini,asinici,asininyankumasi,asinip,asiniskelesi,asinjah,asinjan,asinkhal,asinkritovka,asinkritovskiy,asinlar,asino,asinoea,asinou,asinovka,asinpunta,asintal,asinu,asinua,asinwara,asinwia,asinyakovo,asinye,asinyenikoy,asinyo,asinyo kope,asio,asioca,asiopoana,asip,asipa,asipa oniyangi,asipikhino,asipinar,asipovichy,asipuia,asipur,asiqalilar,asiqbayramli,asiqli,asir,asir asir,asira,asira ash shamaliya,asiragoda,asirai,asiraja,asiran,asirangka,asiranyi,asiraya,asire,asirgarh,asiri,asiridrano,asirikgama,asirindrano,asirlar,asirotoi,asiru sume,asirumarca,asis,asisa,asisi,asisie,asisig,asisiriwa,asisitre,asisor,asit,asite,asitepe,asitepe koyu,asitepekoy,asiteydorm,asiti,asitlar,asito,asitti,asiukle,asivere,asiwa,asiwan,asiy-stan,asiyab,asiyabak,asiyaban,asiyabar,asiyabi,asiyak,asiyalan,asiyan,asiyenye,asiyozgat,asizai,asja-e do,asjana,asjen,asjene,asjeniovo,asjer,asjogle,asjon,asjorda,asjoriachic,asjudi,ask,ask i nordhordaland,ask shahr,askata,askun,aska,askabad,askabon,askabun,askadeh,askainen,askaker,askal,askala,askalan,askale,askali baghdad,askalonovka,askaly,askam,askam in furness,askamore,askamta,askan,askan al mina,askan dabbabnah,askan dasht,askan mandau,askan mandu,askan purwa,askan-mandau,askana,askanabi,askand,askandar,askandra,askandu,askanfou,askanga,askani dasht,askaniete,askanijanowo,askanit,askanite,askaniya nova,askankyla,askanmaki,askans,askanysh,askaoun,askar,askar das,askar-kultayevo,askarabad,askarbeyli,askard,askaremala,askari,askari -surkh,askari surkh,askarlovka,askarova,askarova vtoraya,askarovo,askarpur,askartorp,askaru dheri,askas,askasen,askastou,askasut,askat,askatorp,askavaq,askawan,askazanka,askchi-kul,askea grove,askeaton,askeback,askeberga,askebo,askebro,askeby,askecayirligi,askedal,askedalen,askedodo,askeia,askejour,askekarra,askeland,askelunda,askemo,askemoud,asken,askenas fagerhult,askenish,askenns,asker,askera,askerabad,askeran,askeran mezraa,askerbeyli,askerby,askercayiri,askerino,askeriye,askeriyekoy,askern,askerod,askeroen,askerovo,askeroy,askersby,askersred,askersund,askersundsby,askerswell,askerud,askerum,askeryd,askesby,askesta,askestan,askestock,asketan,asketorp,askevag,askeviken,askew,askewville,askhabad,askhabil-kuli,askham,askham richard,askhamiyah,askhanakeran,askharan,askhate somon,askhatiin suriin khiit,askhatuin khure,askhatuin-surin-khid,askhel,askhlipio,askho,askholm,askholmen,askhult,askhva,aski baghdad,aski dudu,aski igrykh,aski iqrig,aski kalak,aski kifri,aski kubri,aski kupri,aski mahalleh,aski mawsil,aski mosul,aski musil,aski shahr,askia,askideh,askidhia,askifos,askifou,askilan,askilauri,askildrup,askileh,askilje,askiljeby,askill,askilt,askim,askin,askin-e bala,askin-e pain,askin-e payan,askinish,askino,askintarney,askintinny,askipara,askira,askirik,askis,askish,askisor,askistan,askisto,askitai,askitan,askiz,askizar,askja,askjer,askjer holt,askjum,askland,asklanda,asklar,asklev,asklipiion,askloster,asklund,askman kach,askmoud,asko,asko by,askog,askogen,askogle,askoi,askol,askol-e bala,askol-e pain,askola,askold,askole,askollen,askombar,askomber,askombre,askome,askomov,askook,askoping,askordhalos,askos,askosberget,askot,askote,askott,askourne nait mazirh,askourouram,askov,askov huse,askow,askoy,askoya,askoye,askraal,askrigg,askrova,askroven,askskar,asktou,asku,askul,askuly,askum,askuna,askunga,askunge,askunnemala,askur,askustorp,askut,askvag,askvig addition,askvik,askvika,askya,askyaran,askylan,askyrovka,askysskoye,askyzskoye,asl at tamrah,asl kand,asl-e pir kondar,asla,asla`,aslabad,aslackby,aslacton,aslaf,aslagden,aslage,aslakssta\303\260ir,aslam,aslam an nashiri,aslam colony,aslam dheri,aslam garhi,aslam khan dheri,aslam khan kili,aslam kheyl,aslam kili,aslam nagar,aslam skobi,aslama,aslamabad,aslamarz,aslambek-sheripovo,aslambekovskoe,aslambekovskoye,aslamchambar,aslamkhel,aslamkheyl,aslammars,aslamovo,aslan,aslan duz,aslan-bay,aslana,aslanaga,aslanapa,aslanar,aslanbaba,aslanbek,aslanbek-a\302\277eripovo,aslanbek-seripovo,aslanbek-sheripovo,aslanbey,aslanbeyli,aslanbucak,aslancami,aslancayir,aslancayiri,aslancik,asland,aslanda,aslandere,aslandogmus,aslandooz,aslandsgrend,aslanduz tepe,aslanesti,aslanhacili,aslanik,aslaninskiye yurty,aslanizi,aslankent,aslankoy,aslankuyusu,aslanlar,aslanli,aslanlu,aslanoba,aslanoba-seidlyar,aslanoglu,aslanpinari,aslantas,aslantasi,aslany,aslanyusuf,aslaoda,aslapur,aslar,aslarp,aslas,aslau,aslayevo,aslayevskiy,asle,asle chakhansur,asleben,asleh,asleha,aslellef,aslem,aslemarz,asli,asli kand,asli kand-e bala,asli kand-e pain,asli kandi,asli narottampur,aslia,aslica,aslihan,aslihanlar,aslihantepecigi,aslingpun,aslioum,aslitat,asliz,asljunga,asllanaj,asln,aslo,asloca,aslockton,aslocton,asloha,aslokke,aslom,aslong,aslonnes,aslougoum,asloun,aslout,asloute,aslu,aslum,asluman,aslun,aslupes,aslut,aslyyaly,asma,asmabag,asmac,asmaca,asmacam,asmacik,asmadala,asmadere,asmadibi,asmagoda,asmah,asmaharapkar,asmahur-e `olya,asmahur-e bala,asmahur-e sofla,asmai rafayatpur,asmakaradam,asmakaya,asmakoy,asmakoy yaylasi,asmakoz,asmal,asmalang,asmalari,asmali,asmalidere,asmalidere koyu,asmalik,asmalon,asmalyar,asman baihk,asman banda,asman bandah,asman bandeh,asman bolaghi,asman darreh,asman kili,asman qal`ah,asman qal`eh,asmanyan,asmanabad,asmanabad-e bala,asmanabad-e pan,asmandara,asmandi,asmangird,asmani,asmanian,asmanibanda,asmaniya,asmaniyah,asmankala,asmannskotten,asmanovy,asmanqal`a,asmansay,asmansbo,asmansha mahala,asmanska mahala,asmanwala,asmanyan,asmapinar,asmar,asmara,asmarud,asmasiz,asmat,asmatabad,asmaten,asmauli,asmawa,asmawa mirza,asmawah mirza,asmawah mirzah,asmaz,asmbalasatra,asmel,asmena,asmenele,asmera,asmesian,asmesiyan,asmestad,asmetovka,asmi,asminderod,asminderup,asmindrup,asmini,asminion,asminta,asmintos,asmis,asmisen,asmissen,asmlil laqdim,asmolon,asmolovichi,asmolovo,asmon,asmordeh,asmul,asmule,asmulen,asmundbo,asmundshyttan,asmundtorp,asmundvaag,asmundvag,asmurda,asmurdah,asmushausen,asmuyao,asna,asna pujro,asnacchuaycco,asnaccocha,asnacpampa,asnad,asnaes,asnafar,asnahra,asnak,asnam,asnan,asnanjin,asnans,asnapium,asnapugio,asnapujio,asnapujo,asnapuquio,asnaq,asnaseni,asnashany,asnashen,asnasheny,asnatar,asnaveh,asnawar,asnazu,asnchiehtsun,asneira,asnela,asnelles,asnelles-la-belle-plage,asnelles-sur-mer,asner,asnes,asnet,asnh,asni,asniere,asnieres,asnieres-en-bessin,asnieres-en-montagne,asnieres-en-poitou,asnieres-la-giraud,asnieres-les-bourges,asnieres-les-dijon,asnieres-sous-bois,asnieres-sur-blour,asnieres-sur-nouere,asnieres-sur-oise,asnieres-sur-saone,asnieres-sur-vegre,asniki,asninas,asnins,asninskija,asnitsa,asno,asno puquio,asnohuatana,asnois,asnokaw,asnoun,asnun,asnurri,aso,aso banda,aso de sobremonte,aso kunary,aso-veral,asoar,asoatatuma,asoatutuma,asobayoc,asobe,asoberal,asobla,asoboa,asoc,asoc-ncama,asocagi,asociega,asociego,asocleme,asoclon,asocndoba,asoda,asoe,asoenangka,asoenoebo,asoer,asofa,asoff,asofoliza,asog,asogabia,asogabo,asogba,asogbe,asogbenou,asognekondre,asogo,asogomansuein,asogouein,asoguebenga,asohiny,asoi,asoinongroi,asok,asok-nkama,asokas stones,asokati,asokdoba,asoke,asokirli,asokmasun,asoko,asokola,asokolay,asokore,asokore mampon,asokori,asokori mampon,asokori mampong,asokoro,asokrochona,asokwa,asol,asola,asolani,asoleao,asolfsskali,asolguzhin,asolmah,asolo,asolu,asom,asomadero,asomados,asomalos,asomana,asomanku,asomante,asomasi,asomata,asomato,asomatoi,asomatos,asombe,asombo,asome,asomelo,asomena,asomeno,asomkiny,asommam,asomori,ason,asona,asonagro,asonai,asong,asongdong,asongni,asongo,asongu,asonli,asonni,asonomaso,asoo,asoor,asop,asopak,asopas,asopia,asopos,asoqa,asor,asor dewana,asora,asorbelema,asore,asorey,asorgino,asorkhel,asoro,asorobe,asorogun,asoromaso,asorope,asortal,asoru sume,asos,asosa,asoshnik,asoshniki,asoso,asososca,asota,asota sharif,asotam,asothar,asotin,asotodji,asotodzi,asotra,asotskoye,asotthalom,asotwe,asou,asou kondji,asou kope,asoua,asouala,asoubi akoum,asouekte,asough,asouk,asoul,asoulemaka,asoumani,asoumondele i,asoumondele ii,asoumondele iii,asoung,asounmakro,asourdje,asoutena,asov,asova,asovitsa,asovnya,asovo,asow,asow,asoya,asozai tlerai,asozu,asp,asp gardan,asp gardu,asp gaz,asp khwajah,asp maidan,asp murdah,asp shahr,aspara,asp-e khan,asp-i-riz,aspa,aspaba,aspach,aspach im innkreis,aspach-le-bas,aspach-le-haut,aspacica,aspadana,aspagalya,aspagan,aspahi kola,aspai,aspajarvi,aspak,aspakan,aspakay,aspakeh,aspaker,aspakey,aspakhah,aspakhu,aspaki,aspaki nauwarid,aspaki now warid,aspaki nur mahmad,aspaki nur mohammad,aspaki nur muhammad,aspaki watani,aspaki-nurmakhmad,aspaki-vatani,aspall,aspalmi,aspalsar,aspan,aspan dheri,aspando kats,aspang,aspang markt,aspangi,aspani,aspanr,aspanr kili,aspar,aspara,asparasti,asparasty,asparaz,aspari,asparian,aspariegos,asparishkyay,aspariskiai,aspariz,asparla khan,asparlai khan,asparn,asparn an der zaya,asparna,asparrena,asparuchowo,asparukhovo,asparyan,aspas,aspasen,aspasica,aspasnaset,aspastan,aspatria,aspay,aspbacken,aspberg,aspberget,aspby,aspdalen,aspdawan,aspe,aspea,aspealy bridge,aspedammen,aspeh qalyan,aspei,aspek,aspel,aspelaere,aspelare,aspeln,aspelt,aspelunden,aspen,aspen acres,aspen glen condominium,aspen grove,aspen heights,aspen hill,aspen hill park,aspen hutte,aspen knolls,aspen meadows,aspen mountain,aspen park,aspen park condo,aspen run,aspen springs,aspen valley,aspenaes,aspenas,aspendale,aspendell,aspenden,aspendorf,aspenes,aspengo,aspenhauser,aspenhoff,aspense,aspenstedt,aspenwall,aspenwood,asper,asper khan,asperberg,asperdar kili,asperden,asperding,aspered,aspereh,asperelo,asperen,asperes,asperg,aspergem,asperghem,asperglen,asperheide,asperhofen,asperhorn,asperin,asperiz,asperjoc,asperlkeller,asperlovo ma,aspermont,aspern,aspern an der donau,aspero,asperod,aspers,aspersdorf,aspert,aspertsham,aspertshofen,asperup,asperyd,aspesten,aspet,aspetuck,aspevik,aspgardan,asphalt,asphaltum,aspholm,aspholmen,asphult,asphynis,asphyttan,aspi,aspi khwar,aspi sang,aspich,aspid kumb,aspidnaya,aspidnikovo,aspidnoye,aspikhan,aspila lam,aspila sar,aspilla,aspillerada,aspin,aspin wari,aspin-aure,aspin-en-lavedan,aspin-ez-angles,aspinall,aspindale park,aspindza,aspinge,aspinskiy,aspinwall,aspiotades,aspiotadhes,aspiran,aspiras,aspiraz,aspires,aspiro,aspiroz,aspish,aspisheim,aspitia,aspkhoja,aspkhowjah,aspley,aspley guise,aspliden,asplund,asplunda,asplunden,aspmaidu,aspmaydan,aspmaydu,aspmeydan,aspmurda,aspnas,aspnaset,aspneset,aspo,aspoba,aspog,aspogash,aspogh,aspol,aspoltsberg,aspongo,asport,aspos,aspot,aspra,aspra nera,aspra spitia,asprakoi,asprangeloi,aspras,aspremont,aspres,aspres-les-corps,aspres-sur-buech,aspret,aspret-sarrat,aspretto,aspria,asprilla,asprillas,aspro,aspro nero,asprochoma,asprochori,asprochorion,asprogia,asprokambos,asprokhoma,asprokhori,asprokhorion,asprokklisia,asproli,aspron,asproneri,aspronerion,aspropilia,aspropirgos,aspropotamia,aspropotamos,aspropoulia,asprorevma,asprorrevma,aspros,asprosikia,asprothilia,asproudhia,asproula,asproulaiika,asproulianoi,asprovalta,asprovaltos,asprovrysi,asproya,asproyerakas,asproyerakata,asproyia,aspsele,aspsund,aspsveden,aspu,aspudden,aspugh,aspugheh,aspur,aspura,aspuru,aspurviai,aspurz,aspvik,asqaila,asqaileh,asqalan,asqaran,asque,asquempont,asques,asquicha,asquies,asquilies,asquillies,asquins,asquipata,asquith,asqurthan,asra,asrabbad,asrabpur,asraf kalay,asrafdi,asrafel,asrafi,asrafkhel,asrafpur,asralar,asram,asrama,asrama abdul hamid,asrama abri,asrama angkatan bersenjata republik indonesia,asrama brigif,asrama haji,asrama hub. dam tiga,asrama komando strategis angkatan darat,asrama kostrad,asrama tri komando rakyat,asrama trikora,asrama yonif,asramahaji,asramapiner,asramarz,asramaya,asrani,asranwala,asranwala khu,asrar bani `umar,asrar bani sa`d,asrarkis,asrasar,asrasi,asrate,asrawa,asrawanwala,asrayanwala,asre kalay,asreli chauki,asrema,asrharkiss,asrherkiss,asri,asria,asricham,asricipageran,asriga,asrijah,asrik,asrik cirdaxan,asrik-dzhyrdakan,asrikanto,asrikdzhirdakhan,asrikdzhyrdakhan,asrikgama,asrikoton,asrikro,asrimaso,asrimulyo,asrir,asrir ait frah,asrir nilemchane,asrirejo,asrit,asriya,asrizaj,asromaso,asromi,asrou ouchne,asrouks,asrunumsu,asrunun,ass,ass bridge,ass,asseyla,assa,assa catema,assa ela,assa n igueld,assa nguessan namouenou,assa oichi mabo,assa tagmout,assa uman,assa-aul,assa-ela,assa-peixe,assab,assab barshi,assab hausa,assaba,assaba kouachi,assabonou,assabou,assac,assacaio,assace,assach,assachberg,assaco,assad-abad,assadam,assade pafou,assadeinkro,assadji,assads,assadue,assadulayevo,assaente,assaf,assafarge,assafia,assafid,assafik,assafo,assafoca,assafoka,assafou,assafou-zengoua,assafyevka,assagalla,assagao,assagella,assagny,assago nguessan,assaguerye,assahara,assahoun,assahun fiagbe,assahytuba,assai,assaie-kouassikro,assaikio,assailly,assaimoute,assainvillers,assais,assaituba,assaizal,assaka,assaka izalamene,assaka n ait douchau,assaka n chorfa,assaka nait ouzzine,assaka nilourhmane,assaka ntatsa,assakane,assake,assakhara,assaki,assaki-moke,assako,assakongo,assakoui,assakra,assakro,assakrou,assaku,assal el ouard,assal el ward,assal sulaiman,assala,assalau,assalda,assale,assalekro,assaley,assali,assalli,assam,assam-ebae,assam-ossou,assama,assamabo,assamaca,assamada,assamakka,assamakro,assamalla,assamane,assamangatare,assamangatere,assamanka,assamar,assamara,assamassa,assambe,assame,assamer,assamer nait mohamed,assameur,assameur nait mohamed,assamihan,assammar,assammer,assammeur,assamo,assamoikro,assamorola,assamoua i,assamoua ii,assamoua yao,assamouabo,assamouakro,assamstadt,assan,assan daule,assan kouadiokro,assan-kuduk,assan-mula,assan-mulla,assana,assanbe,assandabo,assande guiba,assandifrakro,assandjik,assandre kanoua,assandyi,assane,assane caputa,assane e lima,assane maponda,assanfuma,assanganlingon,assango,assango da quimanga,assango primeiro,assango secundiro,assango segundo,assangue,assanha da paz,assanhas,assani,assanimah,assankanme,assankro,assanmangatere,assanni,assanoe,assanou,assante,assanti,assantu,assao,assaocero,assaoua,assaouas,assaoufoue,assaouma,assaqa,assaqutaq,assar,assara,assarade,assarag,assarai,assarajange,assarar,assarara,assarby,assards,assare,assarebo,assarekop,assarekro,assareobou,assares,assarhar,assaria,assaringue,assarlite,assaron tili,assarou,assarp,assarra,assartorp,assarts,assarwala,assarwan,assas,assasara,assasi,assass,assassa,assasuni,assat,assatiemala,assats,assatu,assau,assaunen,assaung,assaunge,assaurovo,assava,assavou,assawa,assay,assaya,assayakro,assaye,assayita,assberg,asschat,assche,assche-ter-heiden,asschestraet,asschot,assdrem,asse,asse bledoukro,asse diaoukro,asse kouassikro,asse nbatakro,asse ngatakro,asse ter heide,asse-assasso,asse-ble-ndoukro,asse-diekro,asse-la-boisne,asse-le-berenger,asse-le-boisne,asse-le-riboul,assebot,assebrakro,assebroek,assebroko,assebrokro,assebrouck,asseca,assedduma,asseddungoda,asseddunwela,assedmer,assedrup,assegaai river,assegaairivier,assegai river,assegaon,assegbout,asseggiano,asseghmou,assehip ii,assei,assei marco,asseiceira,asseiceira grande,asseiceira pequena,asseico,asseim,asseis,assek,asseka,assekankro,asseke,assekro,assekyere,assel,assela,asselborn,asselborn-les-kehmen,asselbou,asselbrunn,asselda,asselemay,asselermoor,asselfingen,asselheim,asselim,asseling,asselkouter,asselle,assellim,asseln,asseloun,assels,asselt,asselyane,assemane,assemanou,assemaouno,assemba naye,assembleia,assembleia choa,assembly park,assembo,assemboue,assemdou,assemini,assemkoue,assemlaf,assemlal,assemlil,assemlil jdid,assemlil kdim,assemlil lekdim,assemlil odim,assemmer,assemoukoua,assempanaye,assen,assenawatta,assenay,assenbolle,assenbuch,assencada,assencieres,assendabo,assendal,assendelft,assendlose,assendorp,assendrup,assene,assenede,asseng,asseng i,asseng ii,assengama,assengasi,assengassi,assenge,assengou,assengou-kan,assengou-pri,assengoukor,assengoukpri,assengue,assenhausen,assenheim,asseniewo,asseniowo,assenit,assenkro,assennawatta,assennfa,assenois,assenoncourt,assenoukope,assenovgrad,assenovgrade,assenowez,assenowgrad,assenraai,assens,assent,assenta,assentadinho,assentado,assentiz,assento,assentoft,assentorp,assenze,asseolie,asseoufoue,asseounblakro,asseque,assequins,asserac,asseradero,asserballe,asserballeskov,asserbo,asserd nait boulou,asserdoun,asseregaards huse,asseregardshuse,asserekro,asserghine,assergi,assergous,asserhine,asserien,asserikro,asserin,asserir,asserlund,assermo,assermoh,assermou,assern,assero,asserouks,asserrhine,assersa,assersif,assersif ait baha,asserssif,assertorp,assesse,assessungengengnge,assestraat,asset,assetadero,asseve,assevent,assevillers,asseyokro,asseze,assgaour,assgardebyn,asshan,assheim,asshinai,assi,assi akaffou,assi ameur,assi ben fereah,assi bodie,assi bou nif,assi el biod,assi-ben okba,assia,assiak bou adda,assian,assias,assiasso,assib,assibouka,assica,assicante,assici,assidonhoppo,assie akpesse,assie kokore,assie koumassi,assie-kokore,assieblenou,assiekro,assielebroukro,assielibroukro,assiene,assiene i,assiengana,assieni,assier,assieu,assif,assif el had,assif el hard,assif lalou,assif mgorn,assif tarrhouft,assif zmier,assif-melloul,assifane,assifen,assifene,assifere,assifid,assigachiga,assigassia,assignan,assigny,assihjara,assihjare,assika bayassou,assika-kayabo,assikasso,assike,assikoa,assikoi,assikoua,assikoun,assikun,assikvere,assil barke,assila,assilat,assilett,assilho,assima,assimaniona,assimba,assimeba,assimi,assimo,assimpao,assina,assine,assineg,assing,assing kirkeby,assinghausen,assinglose,assingou,assington,assingui,assinha,assini,assiniboia,assinie,assinie france,assinie sagbadou,assinie-mafia,assinins,assinippi,assinkshoek,assinovskaja,assinovskaya,assinowskaja,assinskaya,assinyokope,assinze,assio,assion,assions,assiout,assip,assipi,assire,assiri,assiros,assirotoi,assiru dabr,assis,assis brasil,assis-sur-serre,assisi,assissi,assissouine,assito,assituas,assiut,assiyerovskiy,assiz,assizo,assjo,assjon,asskard,asskizskiy,asslar,asslebon,asslebyn,assling,asslkofen,asslov,asslschwang,assmanns,assmannshardt,assmannshausen,assmebro,assmundsbruk,assmundtorp,assnayman,asso,asso elmuo,asso kandia,asso veral,assob hausa,assobol,assobreiras,associacao capiaco,associacao luavuli,associacao primeiro maio,associacao sueja,assode kandia,assofid essrir,assogbakope,assogbenou,assoghe,assohouin,assok,assok-begue,assok-boleu,assok-ngoum,assok-ngoum i,assok-ngoum ii,assok-nye,assok-seng,assoka,assoko,assoko-nguessankro,assokolay,assokro,assokro-ngressankro,assokro-nguessankro,assolakpo,assolda,assolna,assolo,assomada,assomane,assome,assomlam,assomlan,assomvoue,asson,assonet,assonet bay shores,assonfri,assongaissa,assongnive,assongo,assonora,assonval,assonvoue,assoondee,assoou,assora-massona,assorajang,assorajange,assore,assorino,assorko,assoro,assos,assosseng,assot,assot post wam,assou,assou kondji,assouaf,assouakine,assouakro,assouan,assouangui,assouba,assoue,assouekro,assouel,assoufid,assougar,assougoula emo,assougoum,assouikro,assouinde,assoukankro,assoukope,assoukou,assoukoumba,assoul,assoul ksar,assouli,assoum mabiala,assouma beugie,assouma kedeme,assoumadougou,assoumakodji,assoumakondji,assouman diekro,assouman nguessankro,assoumanda,assoumani,assoumankro,assoumat,assoumondele i,assoumou amani,assoumou-kouassikro,assoumoukro,assoumoundele,assoumoune,assoun,assoundele,assoundi,assououl,assour,assourou,assous,assouste,assow,assoxa,assozai tilri,asstare,asstorp,assu,assu de torre,assu khel,assuako,assuala,assualo,assuamakro,assuame,assuan,assuankokro,assuate,assuati,assucare,assue,assue deux,assue gnaboua,assue mossi,assue un,assuefoue,assuefri,assueti,assuetifi,assuetya,assui khel,assuikro,assuirumiao,assuit,assulia,assulimiao,assuly,assum,assumada,assumadas,assumadas de algoz,assumane,assumani,assumar,assume-bonou,assumpcao,assumption,assumstadt,assumve,assuncao,assunda,assundi,assune,assungui,assunguy,assunguy de cima,assuniu,assuniulu,assunta,assura village,assurance,assureira,assureiras de baixo,assureiras do meio,assurhat,assurua,assuta,assutalina,assutu,assuvi,assveet,assvoelkop,assweiler,asswiller,assy,assyria,assyrsume,asszodpatak,asszonyfa,asszonyreszpuszta,asszonyszallas,asszonyvar,asszonyvari,asszonyvariszollo,asszonyvariszolo,asszuvolgyimajor,ast,asta,asta amanhie,asta village,asta`in,astaakh,astaban,astaberis,astabuana,astach,astachowo,astacinga,astacus,astad,astadana,astadona,astadtorpet,astadym,astaf,astafyev,astafeyeva gora,astaffort,astafli,astafyev,astafyeva,astafyeva gora,astafyevka,astafyevo,astafyevskaya,astafyevskiy,astafyevskoye,astagal,astagaon,astagram,astah,astaheh sar,astahi,astail,astaillac,astain,astak,astakasy,astakati,astakel,astakh,astakhali,astakhino,astakhnovo,astakhov,astakhova,astakhove,astakhovka,astakhovo,astakhovskiy,astakhovskoye,astako,astakol,astakos,astakra,astal,astal poshteh,astala,astalach,astalak,astalak vasat,astalak-e pain,astalanwali dhok,astalaq,astalikhvar,astalima,astalkhvar,astaltseva,astamaja,astamal,astamaya,astamba,astampas,astan,astan karud,astan-e karud,astana,astana baba,astana girang,astana hilir,astana raja,astana tengah,astana tepa,astana-girang,astanaagung,astanababa,astanabojong,astanabungur,astanadjapura,astanagar,astanagarip,astanagede,astanah,astanah babah,astanah tapah,astanajapura,astanajengkol,astanajin,astanak rud,astanakrud,astanalanggar,astanambao,astanamundu,astanapandjang,astanapanjang,astanaraya,astanawenang,astanay,astanayevka,astanchurga,astand,astane,astaneh,astaneh baba,astaneh karud,astaneh sar,astaneh tappeh,astaneh-ye ashrafiyeh,astaneh-ye shahzadeh ahmad,astaneh-ye shahzadeh-ye ahmad,astangas,astanichi,astanino,astaniya,astanjin,astanli,astanly,astano,astanyelga,astapa,astapada,astapenki,astapkovichi,astapkovskaya,astapkovtsy,astapova,astapova sloboda,astapovka,astapovo,astapovtsy,astar,astar kharbidah,astar mahalleh,astar shesh,astara,astarabad,astaraban,astaradja,astaragan,astaraja,astaran,astarang,astaranga,astarawa,astarbaha,astargah,astargakh,astarghan,astarghana,astarghanah,astari,astariani qal`eh-ye rashid,astariz,astarkharbida,astarkharbideh,astaron,astarp,astarqan,astarshesh,astart n tberbout,astarteni,astarud,astash,astashai,astashevka,astashevo,astashi,astashikha,astashikhao,astashino,astashkino,astashkova,astashkovichi,astashkovo,astashovo,astashutino,astasova,astata,astatepec,astatjin,astatla,astatt,astatula,astaukag-erman,astaukag-koz,astaukag-sba,astaukag-zakor,astauli,astaurova,astavul,astawangi,astaye,astayesh,astayoj,astazin,astazur,astbury,aste,aste-beon,asteasu,asteby,asted,astedbro,astede,astederfeld,astee,astegg,asteguieta,astehsar,asteikiai,asteikiu,astel,astel [2],astelarra,asteleh kenar,astelia,asteljoki,astell,asten,astenan,astenas,astenbeck,astene,astenet,astera,asterabad,asteraiika,asteraj mahalleh,astere,asterena,asteres muiza,asterholma,asteri,asterik,asterilan,asterion,asteriyan,asterlagen,asterley,asterode,asterokan,asterovka,asterrica,asterstein,astert,asterud,astet,asteyar,asteykey,asteykkey,astfeld,astgam,astgaon,astgareh gureh,astghadzor,asthaghari,asthall,asthanpur,asthanwan,astheim,asti,asti kharvah,astiar,astibamba,astica,astich,astici,astickpara,astico,asticou,astidan,astif,astigarraga,astigarreta,astikpara,astilau,astileu,astillapata,astillas,astille,astillero,astillero de abajo,astillero municipal,astilleros,astin,astin dar-e pain,astin dar-e vosta,astin darreh,astina,astine,astion,astipalaia,astir,astirak,astirakion,astis,astitsy,astiway,astiz,astkhadzor,astlaj,astle,astley,astley abbots,astmal,asto,astobamba,astobiza,astoci,astofte,astohuaraca,astoin,astois,astokomu,astol,astola,astolfo dutra,astoli,astomulyo,aston,aston bay,aston botterell,aston cantlow,aston clinton,aston end,aston ingham,aston junction,aston mills,aston pigot,aston pigott,aston rowant,aston tirroid,aston trussell,aston woods,astoonand,astopunco,astor,astor farms,astor park,astorakion,astorecas,astorga,astoria,astorkomu,astorp,astotepec,astoviza,astra,astrabudua,astracha,astrachanka,astrachi,astradamovka,astragai,astrahanka,astraile,astrain,astrakari,astrakeri,astrakhan,astrakhan perraya,astrakhan pervaya,astrakhan-bazar,astrakhanka,astrakhankin,astrakhanovka,astrakhanovo,astrakhanovskiy,astrakhanskiy,astrakhanskoye,astrakhantseva,astrakhantsevo,astrakhenovka,astrakoi,astral,astralikha,astramyevo,astrana,astrand,astrandssatern,astrang,astrankhanskoye,astranna,astrapovtsy,astrapy,astras,astrask,astratigos,astratorp,astratova,astratovo,astravas,astravo,astravos,astravyets,astraxanka,astraxanovka,astray,astrazub,astrea,astredinovo,astrenevo,astrenovo,astretsovo,astrevskiy,astreyevskiy,astreykovo,astrida,astrie,astrikas,astrikos,astrilovo,astrilt,astrioji kirsna,astris,astritovo,astritsa,astritsion,astro,astrochori,astrochorion,astrodym,astroea,astrogan,astrogildo,astrologia,astromelia,astromerit,astromeriti,astromeritia,astromeritis,astromino,astros,astrova,astrozub,astrup,astrupgard,astryukovo,astuchoma,astuchona,astuci,astuci koyu,astucu,astudillo,astughunca,astughunchah,astugue,astulez,astumarca,astumbo,astunrash,astur,astureses,asturga,asturia,asturianos,asturias,astuvere,astvatchioi,astvatsinkal,astveit,astwick,astwood,astwood bank,astypalaea,astypalaia,astyrovka,asu,asu kola,asu mait,asu ram,asua,asuaba,asuaba number 2,asuaba number 3,asuabua,asuacan,asuadai,asuafo,asuafu,asuafua,asuagiul-de-jos,asuagiul-de-sus,asuai,asuaju de jos,asuaju de sus,asuajul-de-jos,asuajul-de-sus,asuakaw,asuakwa,asual,asuame,asuamoaku,asuampa,asuano,asuanse,asuansi,asuanta,asuase,asuasi,asuaso,asuasu,asuaye,asubengya,asubey,asubi,asuboa,asuboa north,asuboe,asuboi,asubompan,asubompang,asubon,asubone,asubone station,asuboni,asuboni station,asubonpang,asubonpani,asuboswaso,asubrempon,asubrempong,asubri,asubua,asubuaso,asubulak,asubulaq,asubuokrom,asuburi,asubwaso,asuchaung,asuchuali,asuchwi,asuda,asuda baloch,asuda kalay,asudah kelay,asudakalay,asudeh kalay,asudi,asudom,asudu,asudzun,asue,asue-pabriaga,asuel,asueman,asuentaa,asuenyum,asuet,asueyi,asufo,asufre,asuganj,asugh,asugyi,asuhiae,asuhlar,asui,asuiabe,asuichea,asuir,asuja,asujia,asuk,asuk arim,asuk-ama,asuk-ekpo,asuk-oyet,asuka,asukawkaw,asuke,asuke-cho,asukese,asukka,asukoko number 1,asukoko number 2,asukokoo,asukola,asuku,asukula,asukula asundus,asukurovo,asukurubia,asukwa,asukwao,asukyula,asul-beilii,asul-beylii,asulait,asulbeyli,asulkhar,asum,asuma,asumaa,asumagya,asumali,asuman,asumani,asumawi,asumbatu,asume,asumen,asumgbom,asuminem,asuminya,asumjwi,asumkrom,asumpa,asumra,asumtorp,asumumbay,asun,asuna,asunafo,asunafuo,asunaso,asunawra,asuncion,asuncion amatepe,asuncion atoyaquillo,asuncion buenavista,asuncion cacalotepec,asuncion cuyotepeji,asuncion de las espinas,asuncion donaro guerra,asuncion donato guerra,asuncion grande,asuncion hacienda,asuncion ixtaltepec,asuncion mita,asuncion mixtepec,asuncion nochixtlan,asuncion ocotlan,asuncion tataltepec,asuncion tepezoyuca,asuncion tlacolulita,asuncioncita,asunda,asundatorp,asunde,asundi,asundu,asundua,asunduse,asundwia,asune,asunfri,asung,asungbene,asuni,asunisin,asunja,asunmaw,asunnara,asunnora,asunora,asunsion,asunso,asunsolo,asunsu number 1,asunsulo,asunta,asuntia,asuntila,asunto,asuny,asuo,asuo gongshe,asuoanjo,asuoboa,asuobonta,asuofa,asuofia,asuofri,asuogya,asuogya krobo,asuohiami,asuoja,asuoju,asuokaw,asuokaw aboi,asuokawso,asuoko,asuokoo,asuokyene,asuom,asuom number 1,asuoso,asuoti,asuotia,asuotiano,asuowaso,asuoya,asupas,asupong,asupuia,asupura,asur,asur hosayn,asura,asura sitarambari,asurabad,asurabandh,asuramba,asuran,asurange,asurange 1,asurange satu,asurbum,asure ola,asuret,asureti,asurgevrek,asurgevrik,asurhat,asus,asusaki,asustadera,asuswaso,asut,asuta,asutia,asutifi,asutka,asutsuare,asuulun,asuvaam,asuveem,asuvi,asuwint,asuyan,asuzai tlerai,asva,asvan,asvangan,asvany,asvany-kut,asvanyhat,asvanyhattanya,asvanyraro,asvar,asvatchioi,asvatkoy,asvenai,asvestadhes,asvestareio,asvestareion,asvestario,asvestarion,asvesti,asvestion,asvestis,asvestochori,asvestochorion,asvestokhorion,asvestopetra,asvik,asviken,asvob,aswa,aswad,aswadi,aswadia,aswalapitiya,aswan,aswan reservoir colony,aswandia,aswaq as safsaf,aswar shah,aswarby,aswardby,aswarpura,aswatthatala,aswayabendiwewa,aswedduma,asweddumegama,asweddummulla,asweddumwelagama,asweddunwela,asweiler,aswinpur,asxanakaran,asy,asya,asya bad,asya bak,asya bayd,asya bist,asya khanah,asya kol,asya push,asya-i-bad,asya-i-badak,asya-i-bini,asya-i-cap,asya-i-chap,asya-i-godi,asya-i-sharaf,asya-i-sokhta,asya-tepe,asya-ye bad,asya-ye badak,asya-ye bini,asya-ye chap,asya-ye firqah,asya-ye gorg,asya-ye gudi,asya-ye gurg,asya-ye muhammad ibrahim khan,asya-ye murdian,asya-ye pa-in,asya-ye sharaf,asya-ye sokhtah,asya-ye sukhteh,asyab,asyab khordi,asyab-e kheyrabad,asyab-e shamsabad,asyababad,asyabad,asyabak,asyabed,asyabi,asyabid,asyabkhordi,asyacah,asyachah,asyacheh,asyadi al `amariyin,asyadib,asyagak,asyah khel,asyahi,asyakhana,asyakhel,asyamovka,asyamovlova,asyan,asyanovo,asyawani,asyay-i-firqa,asyay-i-gurg,asyay-i-mohammed ibrahim khan,asyay-i-mohd ibrahim khan,asyay-i-murdyan,asyehvaseh,asyenovets,asykata,asykay,asyktaakh,asyktan,asyl,asylguzhina,asylguzhino,asylum,asyma,asyrsume,asysaga,asyul,asyut,aszal-volgyi vizmu,aszalo,aszaloi kistanya,aszaloi nagytanya,aszar,aszimew,aszo,aszobanyatelep,aszod,aszod pusta,aszodpatak,aszofo,aszohegy,aszoirtas,aszoitanya,aszopuszta,asztag-ko,asztalostanya,at,at alan,at ani,at aouil,at awil,at bolagh,at darrehsi,at ech,at el kef,at faid,at hazar,at koy,at khvajeh,at khwaja,at koriya,at mamoun,at mamu,at meyan,at rahde,at samat,at son,at taif,at taifah,at tamim,at ta`an,at ta`aniq,at ta`awun,at ta`awun awwal,at ta`awun thani,at ta`bah,at ta`bar,at ta`birah,at ta`mir,at ta`s,at ta`unah,at tab,at tabah,at tabaldiyah,at taban,at tabaqah,at tabarani,at tabbaliyah,at tabbin,at tabiyah,at tabqah,at tabshumah,at tadamun,at taff,at taffah,at tafila,at tafilah,at taftish ath thani,at tafwah,at tah,at tahamiyah,at tahaytha,at tahimi,at tahirah,at tahiriya,at tahiriyah,at tahiyah,at tahmaziyah,at tahunah,at tahzi\302\247ah,at taiyiba,at taj,at taji,at takiyah,at tal`ah,at talai`iyah,at talab,at talabi,at talahiyah,at talayi`iyah,at talh,at talhah,at talhaq,at tali`ah,at talibiyah,at talihat,at taliqat,at tall,at tall al ahmar,at tall al kabir,at tall al kafr al gharbi,at tall as saghir,at tallab,at tallah,at tallayah,at tallayn,at tallin,at taluqah ad dakhilah,at taluqah ad dakhlah,at tamamiyah,at tamani`ah,at tamaniyah,at tamariqiyah,at tamimi,at tammazah,at tan`im,at tanaghah,at tanahma,at tanahmu,at tanbawi,at tandabiyah,at tandubah,at tanf,at tannakhah,at tannan,at tannumah,at taqa,at taqtaqanah,at tar,at taraif,at tarabulusiyah,at taraf,at tarafiyah,at tarahib,at taramisah,at tarawi,at tarayn,at tarbi`ah,at tarfa,at tarfawi,at tarfayah,at tarhah,at taribah,at taribjah,at tarif,at tariyah,at tarmiyah,at tarradiyah,at tarranah,at taru`,at tasah,at tash,at tashi,at tasrir,at tataliyah,at tau,at tawabi`ah,at tawabiyah,at tawahi,at tawahin,at tawani,at tawanin,at tawari`,at tawayb,at tawbah,at tawd,at tawdihiyah,at tawer,at tawf,at tawfah,at tawfiq,at tawfiqiyah,at tawil,at tawilah,at tawiyan,at tawm,at tawm halal,at tawqah,at tawr,at tayah,at taybah,at tayhah,at tayi,at tayifah,at tayri,at tayriyah,at tays,at tayshah al `atshanah,at tayyara,at tayyarah,at tayyib ahmad,at tayyib mafin,at tayyiba,at tayyibah,at tayyibah al gharbiyah,at tekeina,at tell al kebir,at terter,at ti`laf,at tib,at tibah,at tibni,at tikhah,at tikkawin,at tileih,at timsahiyah,at timyat,at tin,at tinah,at tinamah,at tiniyah,at tir`ah,at tira,at tiraf,at tirah,at tiraq,at tireis,at tiri,at tiwal,at tizin,at tohamiya,at toman,at tomat,at tonj,at toyan,at trafi,at tumiyat,at tu`miyah,at tuba,at tubah,at tubah al hamra,at tubayh,at tubi,at tud,at tufayha,at tufaysah,at tuffahiyah,at tuhaymiyah,at tuhayta,at tuhaytah,at tukul,at tulayb,at tulayh,at tulayhah,at tulayl,at tulul,at tumay`ah,at tunah,at tunayb,at tundubawi,at tur,at tur`ah,at turabah,at turaybah,at turayf,at turaykimiyah,at turaymisah,at turayshah,at turbah,at turbaibil,at turbiyah,at turfiyah al gharbiyah,at turkumaniyah,at turmaniyah,at turqi,at turrah,at turut,at tut,at tutan,at tuwaim,at tuwair,at tuwaiya,at tuwal,at tuwanah,at tuwaybah,at tuwaybiyah,at tuwayhinah,at tuwaym,at tuwaymat,at tuwaymiyah,at tuwayr,at tuwayrat,at tuwayri,at tuwaysah,at tuwayshah,at tuwayshirah,at tuwaytah,at tuwayyah,at tuweifra,at yaylasi,atan,atara,atara armianskoe,atarbekyan,ataye,atebela,atechama,ategeba,atela,aterwa,atetaft,atghunk,ationa,atidzta,atila,atile,atnet,atnokhi,at-bashi,at-bashy,at-chapar,at-chavar,at-daban,at-korin,at-mamou,at-mamoum,at-tjer,at-tlata sbouya,at-twejje,at-twer,at-uryakh,at-ytan,at-ytar,at-yuryakh,ata,ata ata,ata belage,ata bolaghi,ata jawar,ata karez,ata khan khwaja,ata muhammad,ata muhammad khoso,ata muhammad kili,ata muhammad kundi,ata muhammad marri,ata muhammadwala,atama,ata-atahon,ata-kent,ata-mukhammed -tangay,ata-mukhammed-kalay,ata-yab,ataa,ataabad,ataabad lohri,ataabad noon,ataabadze,ataahua,ataani,ataasi,ataata,ataba,atababo,atabad,atabadi,atabadze,atabadzi,atabaevo,atabage,atabage pallegama,atabagi,atabai,atabak,atabak-e karbal,atabak-e pain,atabakan,atabaki,atabakiri,atabalero,atabalero abajo,atabalero arriba,atabangwe,atabara,atabari,atabasi,atabatu,atabay,atabay-ankebe,atabayan,atabayevo,atabdi,atabe,atabek,atabenou,atabey,atabeyli,atabindi,atabireso,atabiyeh,atablendoukro,atabong,atabong east,atabong west,ataboulou,atabrakaswaso,atabrikang aquaha,atabrikang ekeme,atabu,atabueira,atabulaqi,atabuy,atabuzuk,atac,atacalco,atacama,atacames,atacan,atacasa,ataccara,atacco,ataceo,atacheo,atacheo de regalado,atachera,atachi,atachia,atachieh,atachka,atachom,atacinari,atacmes,ataco,ataco pampa,atacocha,atacocha-lalaguia,atacopata,atacpan,atacra,atad,atadahewatugoda,atadam,atadami,atadar,atadav bridge,atade,atadi,atadi khas,atadialama,atadie,atadoa,atadogdu,ataeron,ataes,ataf,atafi,atafona,atafonas,atafram,ataga,ataganskiy,atagara,atagash,atagay,ataghar,atagi apari,ataglokope,atago,atagoshita,atagozoglu,atagulkala,atagut,atah,atah bulaqi,atah el nebi,ataha,atahallani,atahateja,atahiral ikhlaywi,atahona,atahou,atahoussar,atahti,atahuallani,atahualpa,atahualtitla,atahuaranga,atahuaranga hacienda,atahuasi,atahuayun,atahura,atai,atai obio ediene,atai pali,ataiba pinto,ataibang,ataice,ataide,ataie,ataija de baixo,ataija de cima,ataikala,ataikha,ataikola,ataikula,ataile,atainan,atairu,atais,atait,ataiti,ataiziutla,atajadizos,atajado,atajan,atajapo,atajara,atajate,atajo,atak,atak banda,atak khel kili,ataka,ataka-shimmachi,ataka-shin,atakae,atakalanpanne,atakamachi,atakamanara,atakamanora,atakamanyang,atakan,atakara,atakatanete,atakatenga,atakati,atake,atakedibato,atakeleti malom,atakem,atakent,atakenty,atakfe,atakh,atakh-yuryakh,atakhal,atakhankalay,atakhara,atakhel,atakhi,atakhune,ataki,atakilt,atakiri,atakishili,atakishily,atakisili,atako,atako banda,atako i,atako ii,atakofikrom,atakolo,atakon,atakonio,atakorase,atakou,atakouzar,atakoy,atakpa,atakpame,atakpamede,atakpara,atakpot,atakro,atakroasi,atakrom,atakrou,ataku,atakurom,atakwa,atakzai,atal,atal gopang,atal kach,atal muradani,atal shah,atal supio,atal wal,atal,atalval,atala,atalabawa,atalaia,atalaia cimeira,atalaia de baixo,atalaia de cima,atalaia do campo,atalaia do norte,atalaia fundeira,atalaia nova,atalaia velha,atalak,atalakhla,atalamayoc,atalamayos,atalambue,atalan,atalandi,atalani,atalanka,atalanska,atalanskaya,atalanta,atalante,atalantekke,atalanti,atalapa,atalar,atalasa,atalassa,atalata,atalatbeyg,atalawa,atalaweigbene,atalay,atalaya,atalaya de cuenca,atalaya del canavate,atalayas,atalayita,atalayita numero dos,atalayita numero uno,atalaykoyu,atalayon,atalayoum,atalbeitar,atale,atale eloun,ataleia,atales,ataleso,atalez,atalgarh,atalhada,atalho,atali,atalia,ataliang,atalissa,ataliva,ataliva roca,atalkuri,atalla,atallawela,atallewela,atalli khelanwala,atallo,atalo,ataloga,atalonova,atalote,atalpa chico,atalpamba,atalum,atalwa,atalwal,atalyam,atam kala,atam kalay,atam khel,atam kheyl,atam kili,atam-kili,atam-kulmak,atamadpau,atamaica,atamaica abajo,atamaica arriba,atamaiquita,atamakolo,ataman,atamanka,atamano-nikolayeuskaya,atamano-nikolayevsk,atamano-nikolayevskaya,atamano-vlasovka,atamano-vlasovskiy,atamanov ugol,atamanovka,atamanovo,atamanovskiy,atamanovskoye,atamanskaya,atamanskiy,atamaria,atamasio,atamba,atambek,atambek-kul,atambekovskiy,atamboea,atamboewa,atamboho,atamboho nord,atambolo,atamboposte,atambua,atame,atamer,atami,atamisqui,atamkala,atamkhan,atamkhel,atamkheyl,atamkheyl,atamkoli,atamkul,atamkur,atammik,atamna,atamna el ga`afra,atamo,atamo sur,atamsa kalay,atamsha kalay,atamsha kelay,atamshakalay,atamukhammed-kala,atamukhammed-karez,atamune,atamvekofe,atamyrat,atamysh,atan,atan ajanaku,atan akpan,atan akpan udom,atan aya,atan eka eko,atan eki,atan ibong,atan ikpe,atan jelow,atan jilao,atan nsai,atan odot,atan onoyom,atan ota,atana,atanacio,atanah,atanak,atanalchin,atanandava,atananga,atanangiunit,atanas,atanas koy,atanasio,atanaskioy,atanasovo,atanassowo,atanaza,atanazyn,atanbe te,atancama,atanchikulam,atanda,atandere,atando junction,atandra,atane-gbene,ataneata number 1,atanes,atanetibe,atang,atang menjungkat,atanga,atangan,atangato,atangchai,atange,atangen,atangena,atangmik,atango,atangouroua,atangulam,atangusa,atani,atani aro,ataniata,atanikita,atankawng,atanloo,atanniya,atanoglu,atanou,atanque,atanques,atanquez,atanr,atantane,atantem,atanter,atanturi,atanuk,atany,atao,atap,atap-tsarakhmul,atapa,atapa ikoyi,atapaire,atapame,atapan,atapaneo,atapao,atapara,atapark,atapata,atapattu siyambalewa,atapattugama,atapaura,atapin,atapin kalininskiy,atapirire,atapo,atapoepoe,atapovka,atapseng,atapuan,atapuerca,atapulo,atapupu,atapur,atapuro,ataque,ataquero,ataquines,ataqurt duydokh,ataqut,atar,atar chandwali,atar el nabi,atar en nabi,atar jokhio,atar kach,atar kalleh,atar khan,atar khan jagirani,atar ki dhok,atar shah,atar singhwala,atara,atara oubanda waki koara,atara oubandawaki kwara,atara shi,atara-armyanskaya,atara-armyanskoye,ataraba,ataraca,atarak,atarakom,ataram,atarana,ataraque,atarashika,ataraso,atarasquillo,atarau,atarba,atarba-ikhusta,atarbekovka,atarbekyan,atarchha buzurg,atarchha khurd,atarchhala,atarco,atarco hacienda,atare,atares,atarfe,atargan,atargana,atarghana,atarghanah,atari,atari ajit singh,atari akike,atari bhup singh,atari karam singh,atari ram singh,atari saroba,atari wirke,ataria,atariby,atarigua,atarjea,atarjeas,atarkhali,atarki,atarkoemiling,atarkuwau,atarlebar,atarnati,atarnatii,atarni,ataro kili,ataron,ataros,atarpara,atarque,atarra,atarra buzurg,atarraya,atarshikov,atarsk,atarsuan,atarsumba,atarta,atartupa,ataruma,atarwala,atary,atas,atasangin,atascadero,atascaderos,atascador,atascapa,atascocita,atascosa,atascosas,atascosito,atascoso,atase,atase nkwanta,atasevac,atasevo,atash,atash anbar,atash beyg,atash gan,atash khorow,atash khosrow,atash kuh,atasha,atashan,atashene,atashgah,atashgah-e,atashgah-e `olya,atashgah-e bala,atashgah-e pain,atashgah-e sofla,atashgah-e vosta,atashi,atashi michan khel,atashiene,atashika,atashkadeh,atashtrev,atasi,atasi nkwanta,atasia,atasiene,atasiene muizas centrs,atasienes pusmuizas centrs,atasipara,atasomanso,atasong,atasoy,ataspaca,ataspaya,atasse,atasta,atasta de serra,ataste,atastelabah,atastilla,atasu,atasumanso,atasure,atasuskiy,atatapao,atatba,atate,atatem,atati,atatiawa,atatikope,atatlahuca,atatlyk,atatoua,atatu,atatula,ataturk,ataturk block,atatyn-khid,atauba,atauco,ataucusi,ataudagama,ataudakanda,ataugor,atauli,ataullahpur,ataumarca,ataun,ataung,ataura,atauri,atauta,atavaska,atavaska-bic,atavka,atavoa,atavola,atawa,atawa al qadima,atawa choa,atawalmada,atawarala,atawatu,ataway,atawbrakrom,atawda,atawe,atawerala,atawhai,atawre,atawtawkope,atawtrobenya,ataxal,atay,atay nemetskiy,atay tatarskiy,ataya,atayan,atayao,atayaw,ataybas,atayeva,atayevka,atayevo,ataynar,atayolu,ataysu,atayu,atayurdu,atayurt,atazar,atazik,atba village,atbag,atbagi,atbalmin,atbane,atbara,atbari alamdi,atbaria,atbasa,atbasar,atbasarskiy,atbasarskiy chetvertyy,atbasarskiy tretiy,atbasarskiy vtoroy,atbasi,atbatan,atbatiran,atbatyran,atbhag,atbindi,atbine,atbinek,atbit,atbo,atbuku,atbulak,atbulaq,atburgaz,atburgaz koyu,atburgazi,atca,atcalar,atcali,atcas,atcate,atcha,atchachime,atchadipeme,atchafalaya,atchahoue,atchail,atchakope,atchakpati,atchakpwe,atcham,atchan,atchana,atchangbade,atchanve,atchapa,atchapar,atchapu,atchasi,atchassi,atchati,atchave,atchave kome,atchavou,atche fourta,atchee,atchegouakro,atcheki,atchenedji,atchengbe,atcherah,atcheribe,atcherigbe,atcherigte,atchetime,atcheut,atchi,atchi lafiya,atchiekoua,atchiku,atchilafya,atchineboukope,atchinedji,atchio,atchison,atchison cove,atchitchouviko"e,atchohoue,atchoka,atchou,atchoua kote,atchoue,atchouev,atchoukoul toumouna,atchoukpa,atchoukpe,atchoupa,atchue,atchunkay,atchupa,atchuvely,atchyome,atchyonfre,atcilar,atcitepe,atco,atcoal,atcur,atcuy,atdapunia,atdarrahsi,atdarrasi,atdash,atdeling 4,atdorf,atdrejinci,atdzhitepe,ate,ate albaji,ate muizas centrs,ate ndiaye,ate tangai,ate tangay,ate-ywa,atea,ateamu,ateanha,ateas,atebei,atebiasane,atebin,atebong,atebubu,ateca,atecaca,atecajete,atecata hacienda,atecax,atecaxil,ateccata,atechana,atechane,atechem,atechiang,atecoxco,atecuacario,atecuario,atecuaro,atecume,atedalo,atega,ategan mahala,ategu,ateh khan khvajeh,ateh khaneh,ateh khaneh kakay,ateh khune,ateh khune kakay,ateha,atehama,atehoue,atei,ateiku,ateil,ateim,ateina,ateitu,ateixtlahuaca,ateizile,atek,ateka ober,atekalung,atekasabru,atekasaoru,atekb,atekhsal,ateknsal,atekou,atekou i,atekou ii,atekou iii,atekoui,atekpe,atekro,atekwi,atekyem,atel,atelan,atelatene,atelcoup,atelegbene,ateleta,atelevitsa,ateli,atelier robin,ateliu,ateliw,atell,atella,atelo,atelong,atelu,atem,atemaga,atemajac,atemajac de brizuela,atemanica,atemanu,atemar,atemasovo,atembeek,atembeke,atembip,atemble,atemble hamlets,atemeya,atemezha,atemin,atemkismin,atemo,atemonan,atempa,atempan,atempoku,atemtaman,aten,atena,atena lucana,atena lucano,atenango de la frontera,atenango del rio,atenas,atenas de san cristobal,atenayuca,atencingo,atencio,atenco,atenda-simba,atende,atene,atengame,atengba,atengel,atenghsilien,atengo,atengo del rio,atenguillo,atengxilian,atengxilian zhen,ateni,atenica,ateniidi,atenino,atenioufoue,atenor,atenougon,atenquique,atens,atenseraltensiel,atentang,atente,atenuexin,atenville,ateny,atenyoufwe,ateos,ateou,ateoue,atep,atepa,atepatan,atepec,ateplo,atepo,atepoca,atepoki,ateponta,ateptsevo,ateptsovo,atepu,atequiza,atequizatlan,atequizayan,ater,atera,aterai,ateran,aterau,aterazaka,aterazawa,aterbash,atere,atereboto,ateren,aterhat,aterinki,ateritz,aterk,aterkin,aterloga,atero-klyuch,aterodri,aterpara,aterradinho,aterrado,aterrado alto,aterskiy,ateru,aterum,ateruo,aterzi,ates,atesan,atesane,atescatempa,atescos,atese,ateshak,ateshan,atesheh,ateshgah,ateshgah bala,ateshgah vasat,ateshgah-e bozorg,ateshgah-e vasat,ateshkadeh,ateshkadeh-ye bala,ateshkadeh-ye pain,ateshkooh,ateshkuh,ateshli,ateshun,atesi,atesikrom,atesikurom,ateskiy klyuch,atesniki,atesninkai,atesninkai pirmieji,atesnyku,ateso,atessa,atesse,atestovo,atet,atet bandayaw,atet dodan,atet gyogyaung,atet kamyingan,atet kunthidaw,atet kwingale,atet kyaukkhok,atet kyaungdaung,atet kyidaung,atet linkon,atet mekane,atet minyon,atet nanchaung,atet ngetthe,atet paingkyon,atet pwegale,atet sidi,atet singwe,atet sundet,atet than htaung,atet thidaya,atet yegyaw,atet yintha,atet-nyin,atet-sinkwe,atet-wa,atet-zagani,ateta kope,atetangay,atetel,atetela,atetembato,atetepo,atetetla,ateti,atetia,atetime,atetkyi,atetle,atetou,atetpyunwa,atetu,ateucut,ateuk,ateuk jowo,ateuklampanah,ateuklamura,ateukmunyengbatoh,ateusu,atev,atexane,atexca,atexcac,atexcal,atexcalzingo,atexcatzingo,atey,ateyevka,ateyevo,atezca,atezcapan,atezcatzinco,atezcatzingo,atezquelites,atezquilla,atf,atf ifwa,atfa dhura,atfa dhurah,atfakarye,atfanapu,atfar,atfih,atga,atgain sum,atgalgoda,atgam,atgamazete,atgaoan,atgaon,atgeatu,atgecmez,atghar,atghara,atghari,atgharia,atgharitari,atghoria,atglen,atguden,atgulipura,ath,ath an chlair,ath an choiste,ath an mhuilinn,ath an tsleibhe,ath cinn,ath dara,ath eascrach,ath leathen,ath liag,ath na fuinseoige,ath na nurlainn,ath na sceire,ath na sraide,ath tha `lub,ath tha`alba,ath tha`alibah,ath tha`lab,ath tha`labah,ath tha`lah,ath thabandar,ath thabitiyah,ath thabitiyan,ath thadyayn,ath thagab oasis,ath thaghab,ath thalib,ath thallajah,ath thaluth,ath thamad,ath thamamiyah,ath thamiriyah,ath thaniyah,ath thaqab,ath tharadi,ath tharmad,ath tharmida,ath thawani,ath thawbah,ath thawbani,ath thaweta,ath thawih,ath thawrah,ath thaybat,ath thijjah,ath thinayyan,ath thoba,ath thu`aylibi,ath thughrah,ath thujrah,ath thukhaynat,ath thulth,ath thum,ath thumalah as sufla,ath thumamah,ath thumayd,ath thumayr,ath thuqbah,ath thuwayn,ath ti an mheasaigh,ath tiomain,ath trasna,atha,athaba,athabah,athabasca,athabaska,athabat,athabatteen,athagama,athagarh,athaikhera,athal,athala,athalassa,athalawa,athaldi,athaldia,athalia,athalmer,athalun,atham,athamania,athamanio,athamanion,athamas,athamna bara,athanagar,athanati,athanatoi,athanaton,athanaw,athanion,athapattusiyambalawata,athar,athar an nabi,athar en nabi,athar khan rind,athar qaryat amri,athara baria,athara gachhia,athara kahania,atharabari,atharachura,atharadana,atharagachia,atharah,atharah tukray,atharajani,atharakhada,atharan hazari,atharapota,atharaw,atharga,atharib,atharikhat,atharjani,atharpaika,atharra kunian,athath mori,athaudagama,athawiri,athayde,athboy,athdown,athea,athearns corner,atheaton,athee,athee-sur-cher,athein,athela,athelmer,athelney,athelone heights,athelstan,athelstane,athen,athena,athenae,athenai,athenas,athendale,athenes,athenia,athenna,athenree,athenry,athens,athensleben,athenstedt,athensville,atheras,atherington,atherley,athermal,athernal,atherstone,atherton,athertonville,athesans,athfalah,athgalgoda,athgangoda,athgarh,athgarvan,athgreany,athi,athi new bridge,athi river,athia,athiaenou,athiatali,athie,athieme,athienou,athienville,athies,athies-sous-laon,athijrah,athikia,athilpur,athimale colony,athimanjeripet,athina,athinai,athinaion,athingon,athinia,athinioi,athira,athiras,athis,athis-de-lorne,athis-mons,athitos,athkhel,athla,athlacca,athleague,athlone,athlunkard bridge,athmad,athmakur,athmalgola,athmalik,athmallik,athmauza,athmuqam,athna,athnaleenta,athnamulloch,athnanangle bridge,athni,athnid,athob,athog,athok,athol,athol glen,athol junction,athol springs,athole,atholwood,athos,athos-aspis,athose,athpali,athrakos,athribis,athriya,athruin,aththikulama,aththuwayyah,athu,athuruwela,athuruwella,athus,athwal,athwan mile,athy,athyra,ati,ati acu,ati ardebe,ati assu,ati bhararia,ati chandgaon,ati khel,ati kheyl,ati mir,ati salaq,ati saloq,ati saluq,ati saluq three,ati saluq two,ati tangi,ati-acu,ati-ati,ati-mirim,atia,atia raipur,atia ulain,atiaca,atiadekope,atiaes,atiagome,atiahoe,atiahu,atiak,atiaka,atiakeke,atiakhola,atiakitam,atiakoa,atiakodio,atiakope,atiakro,atiakrom,atial,atiam tiam,atiamkpat,atiamuri,atiamuri bridge,atian,atiandi,atiankama,atianpakouda,atiaoha,atiapara,atiapati,atiapi,atias,atiationin,atiave,atiavi,atiaz,atiba,atibaia,atibere,atibie,atibo,atic,aticama,atichane,atichani,aticioba,atico,aticpac,aticpan,atid,atidie,atidje,atidji,atidjon,atidze tanyigbe,atidzive,atiebey,atiedo,atiega,atiegoakro,atiek,atiekoi,atiekoua,atieme,atien-kofikro,atien-kouassikro,atienka,atienkana,atienkro,atienu,atienza,atiep,atiera,atietit,atieuri-yaou,atifen,atifene,atifone,atifoutou,atig,atiga kope,atigala,atigan,atigan kope,atigaon,atigba,atigbe,atighil,atiglime,atigo,atigozo,atigram,atigsi,atigueky,atiguie,atihiva,atihoe,atihoi,atihuara,atihwi,atii,atiit,atijere,atijive,atijo,atijta,atik,atika,atikameg,atikan,atikhalipara,atikhalopara,atikhel,atiknagar,atikokan,atikole,atikon,atikonis,atikonys,atikovo,atikoy,atikpa,atikpai,atikpame,atikpay,atikple kope,atikpo,atikpo kope,atikpoevikope,atikpovi kope,atikpui,atikpui adedze,atikup,atikyuregil,atil,atil barke,atila,atilabo,atilano numero 1,atilano numero 2,atilano ramirez,atile,atilerin,atiles,atilho,atilio,atilio pessagno,atiliu,atiliw,atilkave,atilla,atille,atillo,atilo,atilola bale,atilonga,atilot,atiluya,atim,atim osam,atima,atimabo,atimah,atimaitan,atimaono,atimatim,atimaz,atimbo,atime,atime fare,atimed,atimede,atimenya,atimir,atimon,atimonan,atimoz,atimpoko,atin,atin jilao,atina,atinane,atinapa,atinbay,atinchin,atindo,atindou,atine,atinekeli,ating,atingan,atingeyevo,atinggola,atingola,atingota,atingrur,atingue,atini,atiniani,atinigech,atininga,atinjoe,atinju,atinkyin,atinndou,atinowaniwa,atinrongkondawe,atintan,atintis,atintisi,atinyave,atinyo,atinyu,atinzam,atio,atioc,atiocoyo,atiogbekope,ationys i,atios,atiotis,atiou,atiouakro,atioukoufe,atioum,atiouri yaou,atiove,atip,atipa,atipakondre,atipalo,atipara,atipillane,atipiti,atiplo,atipolo,atipskaya,atipuluan,atipuluhan,atipur,atiquimaqueca,atiquipa,atiquizaya,atir,atira,atirado,atirahapitiya,atirampattinam,atirampe,atire-gbene,atirero,atiri,atirigbene,atirigin,atirikigaro,atirindia,atiriri,atirka,atirnati,atirnati barbosi,atirnati de jos,atirnati regie,atirnati-balacesti,atiroto,atirro,atirskoye,atis,atisar hingorjo,atish kuh,atishadu,atisi,atiso,atiso kondji,atisobe kope,atisogbe,atisowe,atissa,atisso kondji,atissobekope,atissogbe,atista,atiste,atisu,atiswar,atit,atitah,atitalaquia,atitam,atitan,atitanac,atitanaque,atitancito,atitau,atite,atite kope,atiteh,atitekpe,atitekpo,atitekpoe,atiteta weta,atiteti,atitha,atiti,atitiasso,atititi,atititl,atititr,atitla,atitlan,atito,atitoe,atitogo,atitogon,atitolome apegame,atitouiti,atitpaye,atitse,atitso kope,atituiti,atitwi,atiue,atium,atiun,atiun young,ativa,ative,ativeme,ativhori,atiwe,atixtaca,atiyas,atiye,atiyeh,atiyekoy,atiyenu,atiyoro,atizapan,atizapan de zaragoza,atizi,atiziciftligi,atiziciftligi obasi,atjati,atjemanis,atjirou koune,atjok,atjuscha,atka,atkache,atkai,atkamar,atkamba,atkamp,atkan,atkapara,atkar,atkaracalar,atkari tanyak,atkaron,atkarsk,atkash,atkeison,atkerio,atkhatay,atki,atkiip,atkilnya,atkilt,atkinik,atkino,atkins,atkins acres,atkins lake,atkinson,atkinson heights,atkinson mills,atkinsonville,atkinstown,atkinville,atkiran,atko,atkochai,atkochyay,atkociai,atkociu,atkora,atkoy,atkoyu,atkozino,atkri,atkuchi,atkul,atkulskiy,atkusirro,atkya,atkylnya,atla,atla asundus,atla ghulam,atla marwan,atla rahman,atlaba,atlabanur,atlacacahutla,atlacahualoya,atlacholoaya,atlaco,atlacomulco,atlacomulco de fabela,atlag,atlahdino chandio,atlahuilco,atlahuixtlan,atlajco,atlalaquian,atlalco,atlalpa chica,atlalpan,atlamajac,atlamajaicingo,atlamajaicingo del monte,atlamajalcingo,atlamajalcingo del monte,atlamas,atlamau,atlamaxac,atlamica,atlamis,atlan,atlanaul,atlanca,atlandi,atlangatepec,atlanka,atlanta,atlanta estates,atlanta junction,atlanti,atlantic,atlantic beach,atlantic city,atlantic heights,atlantic highlands,atlantic mine,atlantic park,atlantic shores,atlantica,atlantico,atlantida,atlantique,atlantis,atlapa,atlapa chico,atlapango,atlapexco,atlapulco,atlapur,atlar,atlar zarla koy,atlar zarlu koy,atlarai,atlarche,atlartsi,atlas,atlas ash shahin,atlas circle,atlas hills,atlasburg,atlashevo,atlashofen,atlasova,atlasovo,atlaspur,atlatlahuaca,atlatlahuacan,atlatlahuca,atlatlahucan,atlatlauca,atlatongo,atlautla,atlaxac,atlaxco,atlaxta,atlazalpan,atla\302\261lar,atle,atlee,atlee manor,atlee ridge,atlee station,atlegach,atlehuaya,atlequizayan,atletovo,atli,atlia,atliaca,atlic,atlic mahala,atlica,atlici,atlicintla,atlicos,atlicpac,atlidere,atliderekoy,atliderekoyu,atligozbekiri,atlihan,atlihisar,atlihuayan,atlihuetzia,atlihuetzian,atlihuitzia,atlihutzia,atlii,atlije,atlikas,atlikasy,atlikonak,atlikoy,atlilar,atlimayaya,atlimeyaya,atlin,atlinan,atlipac,atlipozonia,atliw,atlixco,atlixcos,atlixtac,atlixtaca,atlixtlac,atliy,atlmoxoyahua,atlo,atlooglu,atloolu,atlora,atlow,atltzayanca de hidalgo,atlubola,atlym,atlzayanca,atma,atma singh,atmaca,atmacic,atmacici,atmada,atmadsha,atmadza,atmadzhilar,atmagea,atmageaua-tatareasca,atmagje,atmakur,atmakuru,atmali,atmalikacanli,atmalioglu,atman,atmanay,atmane ou adda,atmanka,atmanly,atmanov,atmanov ugol,atmanovo,atmanskaya,atmapur,atmarampur,atmat,atmata,atmatas,atmatscha,atmautluak,atmawala,atme,atmen,atmeni,atmeyan,atmeyan-e `olya,atmeyan-e bala,atmeyan-e pain,atmeyan-e sofla,atmeyan-e vasat,atmeyan-e vosta,atmian,atmih,atminskiy,atmis,atmiss,atmiyan,atmodas,atmore,atmour,atmozoyahua,atmul,atnabru,atnafoss,atnago,atnaguzi,atnair,atnanas-kioi,atnaoset,atnarko,atnary,atnashevo,atnashi,atnbrua,atne,atnebar,atner,atnfoss,atnio,atnis,atnosen,atnoset,atnur,atnya,atnyaguzi,atnyash,atnyashka,atnyashkino,ato,ato achou,ato chinah,ato chineh,ato cina,ato de la virgen,ato gondas,ato khel,ato micael,atoifi,ato-vyekho,atoa,atoabasa,atoabo,atoalima,atoana,atoansa,atoantrano,atoapala,atoasi,atoasoc,atob,atoba,atobaka,atobiadzi,atobiase,atobiasi,atobinen,atobokpou,atobriso,atobrou,atobuela,atoc huaccachi,atocani,atocha,atocha palca,atocha vieja,atochibulok,atochuachanan,atochuhuasiloma,atocongo,atocsaico,atocsajco,atocsayco,atocshaico,atocshayco,atodadeko,atodja,atoe,atoebi,atoeboeldol,atoega,atoeka,atoelouka,atofen,atofroso,atog-boga,atogai,atogan,atogo,atogun,atogwa,atohachana,atohau,atoichi,atoing,atojhuain,atojocha,atok,atok nsok,atok-boga,atoka,atokama,atokhaza,atoko,atokobaka,atokodje,atokogblefo,atokolibe,atokolobe,atokoloto,atokonou,atokoso,atokou,atokouadiokro,atokoum,atokpot,atokro,atokrom,atokulo,atokun,atokyaa,atokzok,atola,atolampy,atolan,atolanggogo,atoleiro,atoleiro de dentro,atolhna,atolia,atolinao,atolinga,atollador,atolo,atoloba,atologbiri,atolotitlan,atolovo,atolowo,atom,atoman,atomanskiy,atomba,atome,atome atchine,atome vegame,atome-ahevi,atome-avegame,atome-yegan,atomgrad,atomia,atomic city,atomo,aton,atonalisco,atonda-simba,atondawaue,atondo,atondritra,atong,atong-atong,atongana,atongo,atongo bakari,atongo de abajo,atongo de arriba,atongo-bakary,atongoeye,atongoouania,atongour,atongpara,atonihare,atonj,atonko,atonkor,atonombato,atonoya,atonso,atonsu,atonsuagya,atontebou,atonubo,atoom,atoou,atooulieh,atooveng,atop,atop-atop,atopi,atopiasi,atopisi,atopixco,atopoltitlan,atopula,atoputi,atorama,atoran,atore,atorea,atori,atoribito,atorinka,atorkaan,atorkor,atoro,atorobako,atoroma,atorp,atortikova,atoruru,atos,atos pampa,atosal,atosang,atosh,atoshuain,atotalang,atotes,atoto,atotocoyan,atotonico,atotonilco,atotonilco el alto,atotonilco el bajo,atotonilco el chico,atotonilco el grande,atotonilco tula,atotonilquillo,atotonilquillo el bajo,atotrem,atotsi,atotso,atou baba kouara,atou babo kwara,atou kondji,atouana,atoubabo-koara,atouda-simba,atoueta,atouguia,atouguia da baleia,atouguina,atouguio,atoui,atouikondji,atouk,atoukmunyengbatoh,atouko kondji,atoukoukouakoukro,atoulaye,atoum,atoumassi,atoumbere,atounkelekro,atounoua,atoupe,atoupe deux,atoupe un,atoupo iago,atoure,atourfa dabkarin,atout,atoute,atov,atovikote,atowada,atowatu,atowo,atowoda,atoy,atoyac,atoyac de alvarez,atoyac naylum,atoyaquillo,atoyateinpan,atoyateipan,atoyatempan,atoyatenco,atoyo,atoysayco,atozai,atozi,atpadi,atpajauohu,atpana,atpara,atpasa,atpatirro,atpu,atqan,atqasuk,atra,atra chal,atra kalan,atra khurd,atrab,atrabasa,atrabazandah,atrachi,atracs,atrafors,atrai,atraighat,atraile,atrakaso,atrakin,atrako,atrakos,atrama,atrampur,atran,atranco,atrango,atrani,atrankwa,atranos,atrap,atrapos,atraps,atraqzia,atrara,atras,atras de los cerros,atras dos morros,atrask,atrat,atrato,atratraka,atrau,atrauco,atrauli,atraulia,atrauliya,atravanu,atrave,atravenu,atravesada,atravesado,atravesana y anexo,atravesano,atravesano y anexas,atravesao,atravessado,atravezao,atre,atre abajo,atre arriba,atre ki baihk,atreco,atreiaparte,atrelle,atrenou,atrepovo,atrepyeva,atrepyevo,atreuco,atrevido,atri,atri nizampur,atrik,atrin,atripalda,atris,atrisco,atriwala,atro,atrobou,atrojillo,atrokhi,atrokro,atron,atrona,atronca perros,atroni,atrop,atropo,atroshkevichi,atrouch,atroukoute,atroukro,atru,atruinso,atrukpo,atrukut,atrush,atryakle,atryakli,atryakly,atryashka,atryasy,atsagayti,atsana,atse washa,atseb lhin,atseb lihin,atsetsa,atsey weyn,atsgebta,atsiwa,atsmi harmaz,atsa,atsagal,atsagba,atsaha,atsai,atsajzo suznay,atsaka,atsakotsoa,atsakya,atsakzai,atsakzai-ye gharbi,atsakzai-ye sharqi,atsakzo soznay,atsalama,atsaliana,atsamby,atsame,atsan,atsan-kuduk,atsana,atsandrazo,atsar khel,atsar kheyl,atsarat,atsari,atsaritibi,atsashen,atsavan,atsay,atsayskiy datsan,atsayskiy monastery,atsch,atschau,atschawe,atschenbach,atschinsk,atschissa,atse,atse i,atseb lkhin,atsegba,atsegha,atsehe,atsekah,atseki,atsekzai,atsekzi,atsekzi gharbi,atsele,atsenga,atsenge,atser,atseriskhevi,atsgara,atshala,atshana,atshapkan,atshiyana,atsho chhubar,atsi gahtawng,atsiaklobo,atsiango,atsiati,atsiati akoepe,atsiekpe,atsiengama,atsiferovskiy bor,atsik,atsikah,atsikak,atsikan,atsiki,atsikyak,atsilap,atsilima,atsimbraga,atsimbrayia,atsime,atsimondraiketa,atsin,atsin asseke,atsin koffi,atsinanani bakano,atsinanantsena,atsinganokhori,atsinganokhorion,atsinima,atsion,atsipadhes,atsipopoulon,atsiprania,atsiuo,atsive,atsivuta,atsiz,atskhur,atskuri,atskvita,atsmi kharmaz,atso,atsoglokope,atsoma,atsombial,atsona,atsonas,atsongugh,atsor,atsotopoulon,atsou kope,atsoupadhes,atspuki,atsquri,atsqvita,atsriskhevi,atsu,atsu-hongo,atsuakrom,atsubetsu,atsuda,atsuga,atsugacho,atsugi,atsuk,atsukeshi,atsuki,atsukofe,atsukope,atsuku,atsumbau,atsumi,atsumigawa,atsunaga,atsunai,atsunas,atsuran,atsuran bobi,atsusa,atsusayama,atsushio,atsuta,atsutabaru,atsutko,atsutoko,atsuvatapi,atsuyar,atsuyarovo,atsvezh,atsydzhkva,atsylyk,atta,atta galung,atta marnata,atta muhammad,atta muhammad jamali,atta muhammad jo goth,atta muhammad maulavi,attaif,attaine,atta-loga,attaaali,attaakh,attab lagh,attaba,attabbane,attabira,attachie,attaching,attack bay,attadale,attagara,attai,attai village,attainville,attair marial,attajokki,attak,attak paniad,attaka,attakapas canal,attakapas landing,attaki,attakpur,attakro,attakrou,attal,attal shah,attala,attalens,attali,attalia,attalimauan,attalla,attalli,attalmuradani,attamajor,attambagaskandura,attamira,attamna,attana,attanagalla,attanagh,attanagoda,attanagola,attanagolla,attanakadawala,attanakitta,attanakumbura,attanapitiya,attanapola,attanas,attanavala,attanayakagammedda,attanayala,attancourt,attang ale,attang benteng,attang salo,attangane,attangcenre,attani,attani agbra,attanou,attanr,attaouia,attaouil,attaouiya ech chaibiya,attapange,attapitiya,attappadi,attapu,attapulgus,attar,attar bhanger,attar jiand ki wand,attar khan mahar,attar laghari,attara,attara-oubanda-waki-koara,attaragalla,attaragallewa,attaragama,attaragama hunnana-oya,attaragama hunnanoya,attaragoda,attaranwala,attarbashi,attard,attari ram singh,attarik,attarot,attarp,attarwala,attashi michan khel,attata,attatba,attatfa mechta el ghali,attaullah,attavally,attavillu,attavilluwa,attawala,attawan beach,attawapiskat,attawaugan,attaway,attawya ach chaibiya,attayampatti,attayyampatti,attbole,attcoffey,atte,attean landing,attebane,attebety,attebubu,attehou,attein,attekro,attel,attelaken,atteln,attelsdorf,attelthal,attenbach,attenberg,attenborough,attendorf,attendorfberg,attendorn,attenedt,attenfeld,attenham,attenhausen,attenhofen,attenhoven,attenkaisen,attenkam,attenkirchen,attenreith,attenrode,attenrode-wever,attenschwiller,attental,attention,attenweiler,attenzell,atteou,attepe,atter,atteras,atterberry,atterboe,attercliffe,atteridgeville,attern,attersee,atterson,attersta,attert,atterup,atterwasch,attes,attevillette,atthaisa,attholmen,atthu,atti,atti atti onin,atti yapo,attia,attiak,attiakro,attib,attibele,attica,attica center,attica junction,attiches,attichy,attidiya,attiecoube,attiecoube diene,attiecoube nematoulaye,attiegouakro,attiekoi,attiekro,attieme,attien kofikro,attien kouassikro,attienkonankro,attienkro,attigam,attiggio,attigliano,attignat,attignat-oncin,attigneville,attigny,attigu,attigundi,attiki,attikkuli,attikkull,attikovo,attikple kope,attikulama,attila,attilapuszta,attili,attilia,attilla,attilloncourt,attilly,attimabo,attimachugh,attimanjeri,attimanus,attimis,attimoddai,attimouyaokro,attimpangnge,attin,atting,attingal,attinghausen,attingue,attinguie,attinkee,attippadi,attippattu,attipuszta,attir,attirampattinam,attiregan,attis,attissa,attiswil,attitogon,attiya,attjarnbo,attleboro,attleboro falls,attleborough,attleborough city,attlebridge,attlebury,attlesee,attlisberg,attmarby,attnang,attnang-puchheim,atto assal,atto chak jindhar,attobaja,attobro,attobrou,attobru,attock,attock city,attock khurd,attock paniala,attogata,attogon,attohoue,attoke awan,attoko,attol,attomat,atton,attontrask,attopeu,attore,attorp,attouch,attoukouara,attoum,attoutou,attoutou kokore,attoutou-amarou,attouye,attowala,attowali dhok,attoway,attoyac,attozai,attray,attre,attrewala,attricourt,attrup,attsjo,attsubaru,attu,attualab,attuana,attuchenai,attudawa,attumkum,attun saha,attunga,attungal,attur,attusa,attya,attyamajor,attyapuszta,attykuduk,attymass,attymon,atu,atu chak jindar,atu kakay,atu kalay,atu kelay,atu kwasa,atu ram,atuu,atua,atua jangal,atuabo,atuabo ahonlezo,atuabu,atuagbo,atuan,atuana,atuar,atuar-etiti,atuatu,atub,atuba,atubale,atubi,atubo,atuboi,atubukiri,atubuldol,atuc,atucachini,atuch,atucha,atuchina,atucmachay,atuco,atud,atudal,atue,atuel sud,atugbuoma,atughobi,atugi,atugmawa,atugoda,atuh,atuhuaycu,atui,atuka,atukalay,atukesi,atuki,atukobi,atukoralayagama,atukrom,atuktash,atukul,atukuwani,atukwankene,atul,atul,atula,atulapa,atulayan,atulcha,atule,atulen,atuli,atulin,atulinao,atuloni,atulugama,atuluko,atulut,atum,atuma,atuman,atumba,atumboa-congo,atumbu,atume,atumi,atumo,atumpampa,atumpujio,atumpuquio,atun,atun huiscachane,atun machay,atun pampa,atun puquio,atun tocrarana,atun tranca,atuna,atunai,atunane,atuncela,atunchulu,atuncole,atuncolla,atuncucho,atunde,atunez,atung,atunga,atunhsien,atunhuasi,atunhuasi hacienda,atuniane,atunku,atunkumi,atunkwasa,atunpampa,atunpucro,atunsalla,atunso,atunsuela,atunsulla,atunsuso,atunsuya,atuntaqui,atuntse,atuntuma,atuntze,atuntzu,atunukve,atunwa,atunwonse,atuo,atuona,atuouiri,atupa,atupatdeniya,atupotdeniya,atupothdeniya,atupuncu,atuquiri,atur,atur-salem,atura,aturaliya,aturan,aturdido,ature,aturi,aturiai,aturicamayoc,aturituri,aturkuden,aturkuten,aturok,aturuba,aturugiriya,aturuk,aturukpo,aturukurutenne,aturunsa,aturunso,aturupana,aturupolayagama,aturuwala,aturuwella,aturyuk,atush,atushi,atushih,atusur,atut,atuta,atutom,atutuma,atutur,atuturi,atuwala,atux,atuyan,atuz,atuzi,atuzskiy,atvarkhu,atvidaberg,atwa,atwab,atwabo,atwal,atwan,atwara,atwater,atwater center,atwaters,atway,atwebanso,atwedee,atwee,atwell,atwell corners,atwells crossing,atweltota,atwereboannda,atwerebuanda,atweta,atwewa,atwi khalat,atwia,atwibanso,atwick,atwidie,atwin bokpyin,atwinbyin,atwingyi,atwinpadan,atwirebuana,atwirebuanda,atwitanso,atwood,atwood acres,atwood cove,atwood crossing,atwood pines,atwood valley,atwood village,atwoodville,atworth,atya,atyachevo,atyak,atyamajor,atyaseva,atyasevo,atyashevo,atyashkino,atyayevka,atyaylasi,atydzta,atyemaz,atyemazli,atyeme,atyenda,atyeri,atygay,atyglam,atyha,atyiglam,atyk,atykovo,atyktakh,atymya,atynsk,atyolu,atyoptyop,atyoungbana,atyr-mena,atyr-meni,atyr-meyite,atyr-tirite,atyra,atyrau,atyrdaakh-yurekh,atyrdzhakh,atyrdzhas,atyrzakh,atyshadu,atyulovo,atyulovskiy,atyun,atyuryevo,atyus,atyusha,atyushi,atyutan,atzacan,atzacoalco,atzacoaloya,atzacualoya,atzal chachkanawala,atzala,atzalan,atzalo,atzalpur,atzapotzalco,atzara,atzbach,atzberg,atzbull,atzcapotzalco,atzeburen,atzeldorf,atzelgift,atzeli,atzelrode,atzelsberg,atzelsdorf,atzenbach,atzenberg,atzenbrugg,atzendorf,atzenhain,atzenhausen,atzenhof,atzenhofen,atzenreute,atzenrod,atzenrot,atzensberg,atzenweiler,atzenzell,atzerath,atzerode,atzersdorf,atzesberg,atzgersdorf,atzging,atzhalyar,atzhausen,atzhirim,atzilzihuacan,atzimbo,atzing,atzingo,atzintzintla,atzitzihuacan,atzitzintla,atzizintla,atziziutla,atzldorf,atzlenbach,atzlern,atzlricht,atzmannsberg,atzmannsdorf,atzmannsricht,atzo,atzolcintla,atzompa,atzontzintla,atzqueltan,atzu,atzum,atzumpa,atzupani,au,au am aign,au am inn,au am kracking,au am leithaberge,au am leithagebirge,au am rhein,au an der traun,au barre,au bas,au bei ambs,au bei bad aibling,au bei gaishorn,au bei rosenheim,au bery,au beulet,au blanc pignon,au boland,au bube,au ca,au cadet,au caillou,au calif,au centre,au chabet,au chenay,au cheny du mont,au cheval dot,au codot,au coucou,au dan,au daung paeng,au dela de leau,au dessus des bois,au dieu des gardes,au ferrage,au fouk,au gres,au gris han,au gros chene,au ha,au hestre,au im murgtal,au in der hallertau,au laber,au large,au long,au mal,au menhir,au meoule,au minire,au misighei,au nordi,au oussa,au pakrang,au pape,au parc,au phu,au pig,au pong ro,au pont,au pui tong,au pui wan,au raing,au relay,au rolay,au sable,au sable forks,au sable river park,au sandan,au saut,au silence,au soso,au source nord,au source sud,au tabor,au tau,au thier,au tho,au tomboi,au tombois,au train,au trie du burnot,au trou sauvage,au tuf,au voyez,aualu,aulon,auu,auuna,au-kalaf,au-serrou,aua,auacayo,auade,auadle,auadlei,auafdug,auak,aual,aual afra mussa,aual alas,aual allas,aual alle,aual edo,aual ghinda,aual giohar,aual gubo,aual nagot,aual selo,aual tirre,auala,aualancheti,aualbulle,auale gupti,aualgari,aualgube hai,auali,aualitsa,aualla,auan,auanay,auang,auangisia,auarada,auare,auareh,auari,auarla,auarmut,auarra,auas,auas tingni,auasa,auasan,auasbila,auasc,auasciait,auasdacura,auasdakura,auase,auasi,auaslupia,auaspani,auasta,auastara,auaste,auastingni,auau,auaue,auay,auayan,auazi,auazipur,auazy,aub,aubabe,aubach,aubagnac,aubagnan,aubagnat,aubagne,aubagne en provence,aubain,aubaine,aubais,aubal,aubange,aubarede,aubaret,aubas,aubatiwala,aubatkan,aubawn,aubazat,aubazine,aubazines,aube,aube-sur-rile,aubeanai,aubeanau,aubeangai,aubechies,aubecq,aubeguimont,aubel,aubeln,auben,aubenas,aubenas-les-alpes,aubenasson,aubencheul,aubencheul-au-bac,aubencheul-aux-bois,aubenham,aubenhausen,aubenton,aubepierre,aubepierre-sur-aube,aubeque,auberat,auberbosc,auberchicourt,aubercourt,aubercy,auberg,auberge,auberge casamozza,aubergenville,auberges de boissy sous saint-yon,auberive,auberive-sur-suippe,auberives,auberives-en-royans,auberives-sur-vareze,aubermesnil,aubermesnil-beaumais,auberry,aubers,aubert,aubertans,aubertin,auberville,auberville-la-campagne,auberville-la-manuel,auberville-la-renault,aubervilles,aubervilliers,aubespeyre,aubessagne,aubeterre,aubeterre-sur-dronne,aubevideyre,aubeville,aubevoie,aubevoye,aubiac,aubiat,aubie,aubie-et-espessas,aubiere,aubiet,aubigeyres,aubignac,aubignan,aubignas,aubignat,aubigne,aubigne-briand,aubigne-racan,aubigney,aubignosc,aubignose,aubigny,aubigny-au-bac,aubigny-aux-kaisnes,aubigny-en-artois,aubigny-en-laonnois,aubigny-en-plaine,aubigny-la-ronce,aubigny-le-chetif,aubigny-les-pothees,aubigny-les-sombernon,aubigny-sur-badin,aubigny-sur-nere,aubigny-ville,aubile,aubilly,aubin,aubin-saint-vaast,aubing,aubinges,aubinhonis,aubinhorus,aubinya,aubissous,aubitz,aublain,aublela,aubokul,auboncourt,auboncourt-vauzelles,aubonne,aubonyun,aubord,auboue,auboulazou,aubourn,aubous,aubrac,aubray,aubree,aubres,aubreville,aubrey,aubrey crossing,aubrey isle,aubriaria,aubrives,aubroek,aubrometz,aubry,aubry-en-exmes,aubry-le-panthou,aubstadt,aubulak,aubure,auburg,auburn,auburn center,auburn chase,auburn corners,auburn four corners,auburn heights,auburn hills,auburn hills mobile estates,auburn junction,auburn lake trails,auburn plains,auburn south,auburn vale,auburndale,auburntown,auburnville,aubussargues,aubusson,aubusson-dauvergne,aubvillers,auby,auby-lez-douai,auby-sur-semois,auc poh,auca,auca mahuida,aucachi,aucaico,aucaleuc,aucallama,aucampsrus,aucamville,aucaners,aucanquilcha,aucanquilla,aucantagua,aucantagua hacienda,aucantahua,aucapata,aucar,aucara,aucasaya,aucaya,aucayacu,aucayaquilla,aucayo,aucazein,auccinamayo,auce,aucella,aucelon,auces muiza,aucey,aucey-la-plaine,aucfer,auch,auch bulaq,auchac,auchaca,auchan,auchanawa,auchas,auche,auchebie,auchebier,aucheili,auchel,auchen,auchenbreck,auchencairn,auchendinny,auchengray,auchenskeoch,auchere,aucheres,aucheys,auchgoyle,auchhpara,auchi,auchi-kalyacha,auchico,auchidle,auchilca,auchinbeddie,auchinblae,auchinbreck,auchindachy,auchinde,auchindinny,auchindown,auchingill,auchinhove,auchinleck,auchintoul,auchiri,auchleven,auchmacoy,auchmantle,auchmithie,auchmull,auchnacloich,auchnacraig,auchnafree,auchnagatt,auchnagoul,auchnangaul,auchnangoul,auchness,aucho,auchonvillers,auchora,auchsesheim,auchtembeddie,auchterarder,auchterderran,auchterhouse,auchterless,auchtermuchty,auchterneed,auchtertool,auchtoo,auchy,auchy-au-bois,auchy-en-bray,auchy-la-montagne,auchy-les-hesdin,auchy-les-mines,auchy-les-orchies,auci,auciema muiza,auciems,aucilla,auckingill,auckland,auckley,aucloggeen,auco,aucon,aucugals,aucun,aucupis,aud,auda,auda al grain,auda al waham,audah al wahhabi,audain,audak,audaki,audaki-bala,audaki-balina,audaki-pain,audaki-tayna,audal,audal nawan,audalkhune,audala,audam,audanga,audanzas,audanzas del valle,audara,audare,audari,audate,audattha,audaux,auday,audaz,aude,audegem,audegle,audeich,audejos,audelange,audeleh,audelen,audelia,audelle ier,audelle uein,audelle uen,audelle wein,audelle yer,audeloncourt,audembert,audemetz,audemez,auden,audenaarde,audenaeken,audenarde,audencourt,audenfort,audenge,audenhain,audenhove,audenhove-saint-gery,audenhove-sainte-marie,audeniai,audenried,audenschmiede,audera,auderath,auderghem,auderi,auderville,audes,audeux,audevalja,audeville,audh,audhera,audhiji kassi,audho nagla,audi,audia,audicana,audie,audiencia,audierne,audigast,audigers,audignicourt,audignies,audignon,audigny,audincourt,audincthum,audincthun,audinghen,audini,audinle,audion,audishorn,audit colony,auditon,auditore,auditorovka,audivert,audja,audjassaare,audla,audlem,audley,audobon gardens,audoin,audoin beugreto,audoin sante,audon,audorf,audorfel,audorfl,audouin,audouville,audouville-la-hubert,audra,audrange,audrecelles,audregnies,audrehem,audreselles,audressein,audresselles,audrey,audrey park,audrichbli,audriere,audrieu,audrimont,audrini,audrix,audru,audruicq,audruliai,audrupe,audruves,audruves muiza,audu,audu maikyauta,audubon,audubon forest,audubon homes,audubon park,aududugum,audun,audun-le-roman,audun-le-tiche,auduntini,auduntun,auduoniu,audwala,audz,audzaga,audze,audzhikoy,audzhin,audzi,audzini,audzkha,audzu,audzu muiza,aue,aue am berg,aue-auerhammer,aue-aylsdorf,auebenou,auebou,auedeich,auedt,auegg,auel,auela,auen,auenblick,auenbuttel,auendorf,auenhain,auenhausen,auenheim,auenhofen,auenou,auenstein,auenzell,auer,auerbach,auerbach in der oberpfalz,auerbach-brunn,auerbach-hinterhain,auerbach-rempesgrun,auerbach-sorga,auerbachtanya,auerbakhovskoye,auerberg,auerberger,auerfliess,auerhammer,auerkofen,auerling,auern,auernheim,auernhofen,auerose,auers,auersbach,auersberg,auersbergreut,auersbergsreut,auerschmied,auerschutz,auersdorf,auershof,auersmacher,auersperg,auerstedt,auersthal,auerswalde,auerswalde ostpreussen,auezov,auf beescht,auf berg,auf boescht,auf bossel,auf brandscheidt,auf dem barrel,auf dem berg,auf dem berge,auf dem bock,auf dem donnerbrink,auf dem doren,auf dem floth,auf dem gellenkamp,auf dem hammel,auf dem heede,auf dem kartel,auf dem kerlfelde,auf dem mersch,auf dem ort,auf dem orte,auf dem rusch,auf dem sand,auf dem sande,auf dem sandort,auf dem schilde,auf dem schnee,auf dem spirzen,auf dem stock,auf dem stuven,auf dem wachtenbrink,auf dem walde,auf dem wiebusch,auf den bulten,auf den rothen,auf der aue,auf der becke,auf der bleiche,auf der blosse,auf der boge,auf der bruchheide,auf der bulau,auf der eck,auf der eschenhorst,auf der eulenburg,auf der heide,auf der helle,auf der hohe,auf der horst,auf der juhohe,auf der kanneword,auf der leiten,auf der leuchten,auf der lieth,auf der luchte,auf der luchten,auf der mauer,auf der nuhne,auf der quabbenstrasse,auf der spirzen,auf der strotheide,auf der wiese,auf der wolsch,auf poting,auf sirrenberg,aufm buch,aufm buhl,aufm busche,aufm kampen,aufnberg,aufaga,aufah,aufale,aufeitale,aufeld,aufellenbogen,aufen,aufenau,aufeo,auffach,auffains,auffargis,auffay,auffe,auffenberg,aufferville,auffreville,auffreville-brasseuil,aufhalt,aufham,aufhausen,aufheim,aufhofen,aufi,aufkirch,aufkirchen,auflance,aufles,aufmberg,aufreute,aufroth,aufsess,aufstetten,aug,auga,auga bendita,auga buena,auga caliente,augader,augadi,augajeh,augam,augamy,augan,augana,augand,augashcola,augathella,augberg,auge,augea,augeac,augedals bro,augedalsbru,augen-shvab,augenthal,auger,auger-saint-vincent,augerans,augerburg,augere,augeres,augerolle,augerolles,augers,augers-en-brie,augerum,augerum gard,augerville,augerville-la-riviere,auges,auget,augeville,augezd,auggen,auggenbach,auggenthal,aughabrack,aughadareen bridge,aughagallen,aughagault,aughagault big,aughagault little,aughaglanna bridge,aughalee,aughamullan,aughan,aughanloge bridge,aughatubbrid,aughaville,aughboy,aughboy bridge,aughclare,aughele,augher,aughill,aughils,aughinish,aughkeely,aughmullan,aughnacloy,aughnacrow bridge,aughnagawer cross roads,aughnagroagh bridge,aughnasheelan,aughness,aughnish isle,aughrabies,aughrim,aughris,aughrus more,aughshemus bridge,aught upper,aughton,aughwick,augi,augicourt,augie,augier,augignac,augila,augio,augirein,augisey,augitni,augland,auglend,auglepa,auglitten,augmenai,augmenay,augmenu,augnat,augnax,augne,augny,augodun,augonville,augosto,augosto severo,augpaiartok,augpalartok,augpilagtok,augpilagtoq,augraben,augrabies,augrain,augrub,augs-podrags,augsberg,augsburg,augsbuur,augsbuurt,augsbuurt-lutjewoude,augsdorf,augsfeld,augsgirren,augshpils,augskiken,augsmukti,augspils,augst,augstari,augstasila,augstasils,augstbuurt,augstkalne,augstkalni,augstkalniesi,augstkalnyeshi,augstroze,augstrozes muiza,augstsils,augstumaler bruch,augstupi,augstupji,augstvilken,augu,auguaise,auguanguili,auguardine,auguenne,auguetinte,auguette,augul,auguliena,augura,august,august pine ridge,august town,augusta,augusta acres,augusta fields,augusta springs,augusta taurinorum,augusta ubiorum,augusta view heights,augusta vitoria,augusta-margaret river shire,augustanovac,augustaville,augustdorf,auguste,auguste kwara,auguste-comte,auguste-koara,augustenberg,augustenborg,augustendorf,augustenfeld,augustenfelde,augustenhof,augustenkoog,augustenruh,augustenthal,augustfehn,augustfelde,augustgroden,augusthausen,augusthof,augusti,augustin,augustin roca,augustinava,augustine,augustine hills,augustinerhutte,augustinesborg,augustinho,augustinium,augustino,augustinopolis,augustinusga,augusto,augusto cardoso,augusto correa,augusto de lima,augusto franco,augusto gomez villanueva,augusto montenegro,augusto mota,augusto n. martinez,augusto ngangula,augusto nicolau,augusto queiroga,augusto sa,augusto seio,augusto severo,augusto soares,augusto villanueva,augusto-obe,augustopol,augustova,augustow,augustowek,augustowka,augustowo,augustroze,auguststadt,augustus,augustusbad,augustusberg,augustusburg,augustusthal,augustuv,augustwalde,augustynka,augustynki,augustynow,augustynowo,augustyny,auguszta-banyatelep,augusztatelep,auguwana,augy,augy-sur-aubois,augzeliai,augzin,auhagen,auhausel,auhausen,auheide,auherzen,auhke,auhmun,auho,auhof,auhofe,auholicky,auhu,auhun,auia number 1,auia number 2,auiakh,auiaro,auihasa,auikhanwala,auile,auing,auingen,auint u maha,auioi theodhoroi,auiowri,auir sita,auitan,auite,auiz,auj,auj kharaba,auja,auja al hafir,auja al khafir,aujac,aujad,aujala,aujan,aujan-mournede,aujara,aujarawa kurma,aujargues,aujari,aujawa,aujero,aujeurres,aujezd,aujezd an der mies,aujhan,auji,aujiena,aujivigno,aujla,aujla di bharin,aujla kalan,aujla khurd,aujlan,aujols,aujun,auk byazin,auk chi-kyaw,auk dodan,auk kaing,auk kamyingan,auk kamyingon,auk kunthidaw,auk kyidaung,auk kyondamingyi,auk namaw,auk nanchaung,auk nanra,auk ngapye,auk pho,auk satha,auk sidi-anauk,auk sidi-ashe,auk singwe,auk sinkwe,auk su,auk sundet,auk tage,auk taungbyin,auk than htaung,auk thinbondan,auk thitaya,auk yebein,auk yebok,auk yegyaw,auk yin,auk yungyaung,auk zaha,auk zigaing,auk-kyauk-wut,auk-pan-i,auk-pongon,auk-seik,auk-taung-ya,auka,aukakalnis,aukam,aukan,aukanga,aukapura,aukchaing,aukchin,auke,auke bay,aukea,aukenzell,aukeopaa,aukerman,aukerz,aukha shai,aukhing,aukhira,aukhlaing,aukhor,aukhsiri,auki,aukin,aukka,aukkaba,aukkalon,aukklon,aukkolevo,aukkon,aukkyin,aukkyun,aukla,aukland,aukle,auklend,aukleng,aukma,auknbaz,aukner,auko,aukofen,aukohaing,aukolla,aukopae,aukopmin,aukot,aukoto,aukpa,aukpara,aukpon,aukra,aukrug,auksbikavis,auksdvaris,aukseindaw,aukseliai,aukshindaw,aukshtadvaris,aukshtagiryay,aukshtalishkyay,aukshtashline,aukshtaychyay,aukshtaychyay-bolshiye,aukshtaychyay-malyye,aukshtdvaris,aukshtekele,aukshtelke,aukshtelkyay,aukshtishkyay,aukshtiyei kaplyay,aukshtokalnis,aukshtoli,aukshtolyay,aukshtpomedze,aukshtupe,aukshtupenay,aukshtupyay,aukshtyshki,auksi,aukskeliai,aukslaukys,aukslyay,auksochay,auksode,auksodzio,auksodziu,auksora,auksoras,aukstadvario,aukstadvaris,aukstagire,aukstakaimis,aukstakalnio,aukstakalnis,aukstaslynis,aukstdvaris,aukstdvarys,aukstelkai,aukstelke,aukstelkes,aukstelkis,aukstelku,auksteni,aukstieji amaliai,aukstieji labunai,aukstieji paneriai,aukstieji zagmantai,aukstikalniu,aukstinskaja,aukstiskiai,aukstkalniai,aukstkalniay,aukstkeliai,aukstkiemiai,aukstoji,aukstoji panemune,aukstpamedziai,aukstraupe,aukstumalai,aukstumalo pelke,aukstumalu pelke,aukstuoliai,aukstupenai,aukstupiai,aukstupiu,aukstvilkiai,auksu,auksuchyay,auksuciai,auksuciu,auksudis,auksudy,auksudys,auksudzio,auktarngdshe,auktaung,auktaw,auktawgyi,aukte,aukthada,aukthaung,aukthayetchaung,aukton,auktsakalnis,auktsjaur,aukula,aukum,aukupenai,aukupenay,aukwa,aukyula,aukywa,aul,aul arykty,aul ashak,aul atygay,aul ayt-kozha,aul batyr bek,aul baubek,aul baydukov,aul bedenkoye,aul begembet,aul bertamar,aul buranbek,aul burash,aul bykovo,aul chingali,aul doshchan,aul dosmambet,aul dyurt-sart,aul dzharly-kul,aul dzhunis,aul famkino,aul gassan-kuli,aul geok-che,aul girang,aul grigoryevskiy,aul hilir,aul izendy,aul karabay,aul karavaks,aul kary-tomar,aul kazakhskiy solonets,aul kazaty,aul kindzharyk,aul klyuchik,aul kochalyar,aul konovalskiy log,aul kopobay,aul krugovoy,aul kul-bay,aul kuldzha,aul kum-bel,aul kumak,aul kurgakovskiy,aul lebyazhinskiy,aul nartpay,aul nomer devyatyy,aul nomer dva,aul nomer odin,aul nomer pervyy,aul nomer pyatdesyat vtoroy,aul nomer sedmoy,aul nomer shest,aul nomer shestoy,aul nomer tridtsat sedmoy,aul nomer vtoroy,aul novoselye,aul nur-uzek,aul odinnadtsatyy,aul omarov,aul peganovskiy,aul pereyna,aul pyatyy,aul rakhmbay,aul rusinovo,aul sary-yazy,aul sedmoy,aul seytagov,aul shagirovskiy,aul shura,aul sibanbayeva,aul sidelnikova griva,aul smagul-atambay,aul smorodinskiy,aul surgutovskiy,aul topchak,aul tunguyukto-kul,aul tyumen,aul udilovskoye,aul urazaly,aul vtoroy,aul yernazarskiy,aul zatonskiy,aulye,aul-alnysh,aul-baba,aul-bek,aul-bergul,aul-butak,aul-chin,aul-chubutla,aul-kratovo,aul-krotovo,aul-kutyrtas,aul-shagir,aul-tepa,aul-tschubutla,aula,aula khanwala,aula-vintri,aulabar,aulacken,aulad,aulad `abdin,aulad `ali,aulad `amr,aulad `azzaz,aulad `ileiu,aulad `ileiw,aulad `ileiwa,aulad `isa,aulad bahig,aulad dafir,aulad deifalla,aulad el sheikh `ali,aulad el-`adawi,aulad el-qadi,aulad el-sheikh,aulad ghanam,aulad gharib,aulad gubara,aulad hamam,aulad hamza,aulad hana,aulad ibrahim,aulad ilyas,aulad isma`il,aulad kazim,aulad khala,aulad khalaf,aulad laiachi,aulad madi,aulad mahmud,aulad mihanna,aulad musa,aulad nigm bahgura,aulad nuseir,aulad nuweir,aulad qibad,aulad rayiq,aulad sa`id,aulad salah,aulad salama,aulad salim,aulad salim bahari,aulad salimna,aulad saqr,aulad sarrar,aulad seif,aulad sheikh,aulad shulul,aulad sirag,aulad toq gharb,aulad toq sharq,aulad yahya bahari,aulad yasin,aulad yihya,aulad yihya bahari,aulad yihya qibli,auladi-mirza,auladi-rakhmatbek,auladi-sufi,aulages,aulagnier grand,aulagny,aulago,aulaines,aulajor,aulak sindhu,aulakh,aulakh hithal,aulakh hithar,aulakh hithhar,aulakh rastar,aulakh utar,aulakh uttar,aulan,aulanais,auland,aulander,aulang,aulang sar-i-simanj,aulang shurau,aulanko,aulanys,aulapolai,aulas,aulas khan,aulasat,aulat,aulatsivik,aulawsaukhi,auld,auld farm,auldan,auldearn,auldgirth,aule,auleben,auled ramoun,auleg,auleja,aulekowszczyzna,aulenbach,aulendiebach,aulendorf,aulenhausen,aulepa,auler,aulerchar,aulestad,aulestia,aulet,auletkuduk,auletta,auleya,aulfingen,aulgurta,aulhat-saint-privat,aulhath nagla,aulhausen,auli,auli kili,aulia,aulia khan shah,aulia sajjanpur,auliac,auliachashma,aulianwala khu,auliapur,aulico,auligk,auligk-lobnitz,aulija,aulimbit,aulir char,aulitsa,auliya,auliyawa,auliye,auliye-ata,auliyesayskiy,aulke,aull,aulla,aullaca,aullagas,aullama,aullamita,aullene,aullville,aulmoit,aulnat,aulnay,aulnay-aux-planches,aulnay-de-saintonge,aulnay-laitre,aulnay-la-riviere,aulnay-sous-bois,aulnay-sur-iton,aulnay-sur-marne,aulnay-sur-mauldre,aulnaya balka,aulne,aulnizeux,aulnois,aulnois marliere,aulnois-en-perthois,aulnois-sous-laon,aulnois-sous-vertuzey,aulnois-sur-seille,aulnoit,aulnoy,aulnoy-sur-aube,aulnoye,aulnoye-aymeries,aulnyy,aulo,auloh,aulola,aulon,aulong,aulos,aulosen,aulougui,aulouise,aulovenen,aulowonen,aulpi,aulraule,aulsay,aulsovet chetvertyy,aulsovet pervyy,aulsovet tretiy,aulstad,ault,ault hucknall,aultbea,aultgrishin,aultia golabhita,aultivullin,aultman,aultman pines,aultmore,aultnaharrie,aultshire,auluu,aulua,aulukalna muiza,aulukalni,aulukalns,aulukalns muizas centrs,aulukhchi,aulukik,aulum,aulus,aulus-les-bains,auluwonen,aulwangen,aulx,aulx-les-cromary,auly,aulzhausen,aum,aum phanom,auma,aumage,aumagne,aumal,aumale,aumale-dalgerie,auman dajansemu,auman delodsemu,aumans crossroads,aumanuk,aumar,aumara,aumaran,aumaraubaudi,aumare,aumaru,aumarutigssat,aumate,aumatre,aumba,aumbach,aumbay,aumeistera,aumeistera muiza,aumeisteri,aumeisteris,aumenancourt,aumenancourt-le-grand,aumenancourt-le-petit,aumenau,aumerval,aumes,aumessas,aumetz,aumeville,aumeville-lestre,aumi,aumila farm,aumo,aumoar,aumond,aumont,aumont-aubrac,aumontzey,aumoy,aumsville,aumuhl,aumuhle,aumuhle-billenkamp,aumuhlen,aumuna,aumund,aumur,aumysheva,aumyshevo,aun,auna,auna di sopra,auna di sotto,aunac,aunada,aunainville,aunama,aunama village,aunan,aunara,aunari,aunarni,aunas,aunat,aunave,aunay,aunay-en-bazois,aunay-les-bois,aunay-sous-auneau,aunay-sous-crecy,aunay-sur-odon,aunda,aundah,aundanao,aundh,aundhi,aundi,aundu,aune,auneau,aunede,aunehagan,aunehagen,aunekoski,auneri,aunet,auneuil,aunevold,aunevollen,aunfoss,aung,aung ba,aung bai,aung bwei,aung chak,aung kaung,aung tang,aung yang,aung-gon,aungaa,aungba,aungban,aungbangon,aungbangyaung,aungbansein,aungbauk,aungbaw,aungbin,aungbingon,aungbinle,aungbon,aungbongyi-athin,aungchantha,aungdaing,aungdat,aungdaung,aungghlaing,aunggon,aunggraw,aunggrawywa,aunggyantha,aunggyawgon,aunggyi,aunggyi ywathit,aunggyigon,aunggyin,aunghla,aunghla te,aunghlabyin,aunghlaing,aunghlaywa,aungi,aungkanhlaing,aungkon,aunglaing,aunglan,aunglanmyo,aunglauk,aunglaungzein,aungleba,aunglet,aunglut,aungmeiktha,aungmin,aungmo,aungmya,aungmye,aungmye thazi,aungmyin-in,aungmyingon,aungnainggyi,aungsaing,aungsaingbuda-kwin,aungseik,aungtha,aungtha north,aungtha south,aungtha te,aungthadan,aungthawara,aungya,aungyeiktha,aungywa,aungzaiya,aungzatkwin,aungzeik,aungzeya,aungzu,aungzwa,aunham,auni,auniati,auniati sattra,aunig,aunik,aunimneko,auning,aunini,aunkirchen,aunkofen,aunliwala,auno,aunoit,aunon,aunou,aunou-le-faucon,aunou-sur-orne,aunrai,aunrum,aunshin,aunshino,aunta,aunto,aunuu,aunua,aunud,aunus,aunuvenai,aunwa,auny,aunyalin 1,aunyalin 2,aunyalin number 1,aunyalin number 2,aunyelim,auo,auoday,auokeda,auolela,auom,auorotini,aupa,aupacshuay,aupagan,aupakambu,aupar,auparan,auparu,aupash,aupi,aupia,aupik,aupio,aupitz,auponhia,aupono,auppegard,aups,auputal,auputz,aupwel,aupx,auqad,auqaf colony,auquainville,auquefer,auquemesnil,auqui amaya,auqui kollu,auquia,auquias,auquibamba,auquichico,auquigrande,auquilla,auquillama,auquillapata,auquimarca,auquin,auquinco,auquiraccay,auquirana,auquisama,auquiscancha,auquish,auquzlu,aur,aur bakah,aur bogeis,aur-tunggal,aura,aura an der saale,aura im sinngrund,aurabo,aurach,aurach am hongar,aurachkirchen,aurad,aurad buzurg,aurad shahjahani,aurade,auradou,auragal,auragne,auragu,auragun,aurahua,auraiya,auraji,aurak,aurakhmat,aural,aurali,auran,aurana larai,aurang,aurang banda,aurang kili,aurangabad,aurangabad saiyid,aurangpur gahdewa,aurangzeb block,aurania,auranlaakso,aurant,aurantia,auranwala,aurapong,aurapum,auraria,auras,auraso,aurata,auratchi,auratsberg,aurau,aurava,aurawala,auray,aurbach,aurbani,aurbeduri,aurcina,aurdal,aurdalsbyen,aurdra,aurduri,aurduti,aure,aurea,aurec,aurec-sur-loire,auredie-pue,aurei,aurei valea ursului,aureil,aureilhan,aureillac,aureille,aurejarvi,aurekhi,aurekoski,aurel,aurel vlaicu,aurelhaza,aurelia,aurelia norte,aurelia sud,aurelia sur,aurelian springs,aureliana,aureliano caballero,aurelien dijoux,aurelin,aurelio,aurelio boza,aurelio perez,aureliopolis,aurelius,aurellana,aurelle,aurelle-verlac,aurelliana,aurelow,auremo di sopra,auremo di sotto,auren,aureni,aurensan,aurent,aureola,aurere,aurerliopolis,aures,auressio,auresti,auretsdobl,auretzdorf,aureville,aurgading,aurgarh,aurheide,auri,auri grande,auri gut,auria,auriac,auriac-de-bourzac,auriac-du-perigord,auriac-leglise,auriac-lagast,auriac-sur-dropt,auriac-sur-vendinelle,auriakhel banda,auriat,auribail,auribeau,auribeau-sur-siagne,aurice,aurich,aurich-oldendorf,auricher wiesmoor eins,auricher wiesmoor zwei,auricia,auriebat,aurieres,auriesville,auriflama,aurigak,aurigeno,aurighi,aurignac,aurigo,aurilandia,aurili,aurilishkyay,auriliskiai,auriliskiu,aurillac,aurimbit,aurimbit 2,aurimont,aurin,auringa,auringen,aurinowes,auriol,auriole,aurioles,auriolles,aurions-idernes,auriples,auris,aurisca,aurisina,aurisina case,aurit saguen,aurita,auritabouo,aurith,auritz,auriyeh,aurizona,aurjug,aurka da chak,aurkuning,aurlfing,aurnouk,auro,auro jamari,auroa,auroca,aurodenberg,aurolfing,aurolzmunster,auron,aurons,auronzo,auronzo di cadore,aurool,aurora,aurora banatului,aurora center,aurora esquipulas,aurora hills,aurora lodge,aurora springs,auroraville,auros,aurotschnitz,aurouer,aurouse,auroux,aurouze,aurovka,aurra,aurrutia,aursat,aursfjordbotn,aursfjordgard,aurskog,aurstad,aurtjina,auru,auru muiza,aurui,auruku,aurumbit,aurump,aurus,aurvad,aurwad,aus,ausa,ausa jesus,ausa-corno,ausable chasm,ausacker,ausackerbruck,ausackerholz,ausackerwesterholz,ausahuache,ausakrom,ausam,ausantapa,ausar,ausara,ausari,ausari budrukh,ausarin,ausas,ausatas,ausati,ausatu,ausatu muiza,ausaub,ausbach,ausback,ausbaria,ausbau,ausbau dolland,ausbau nord,ausbergen,ausbuttel,auscha,auscheffenburg,auschet,auschina,auschkowitz,auschowitz,auschtitz,auschule,auschwitz,auseba,ausebt,ausejo,ausejo de la sierra,ausekli,auseliske,ausentes,auserd,ausert,auseu,ausevik,ausgaleni,ausgiriai,ausgirys,ausgram,aush,ausha pata,aushaday,ausham,ausharle,aushaziya,aushaziyah,aushbikavis,aushe,aushed,aushedz,aushegahtawng,ausherhat,aushi,aushiger,aushiniekannaibo,aushka,aushkhali,aushkinyeki,aushofen,aushtkyalyay,aushtrov,ausi,ausi asundus,ausia,ausiait,ausiat,ausiku,ausim,ausini,ausipar,ausipas,ausirkhua,ausiyah,auskhali,auskinieki,ausland,auslauf,ausleben,ausmac,ausmas,ausnang,ausnara,ausnes,ausonia,auspitz,ausraiciai,ausrine,auss halbach,aussa,aussac,ausseing,aussen,aussenried,ausser dem steinhubel,ausser latterbach,ausser wald am arlberg,ausser-frohnd,ausserachleiten,ausseraigen,ausserbinn,ausserbraz,ausserd,ausserdinhard,aussere altmatt,aussere einode,aussere grossrotte,aussere kainisch,aussere ragnitz,aussere wimitz,ausseregg,ausserer kitzinghof,ausserer pfitzhof,ausserer sonnberg,ausserer vogelsberg,ausseres hecheln,ausserferrera,ausserfragant,aussergampabing,aussergefild,ausserglas,aussergosta,aussergosten,aussergufer,ausserhalbach,ausserhareth,ausserhienthal,ausserholligen,ausserhorgersteig,ausserirlach,ausserjebing,ausserkasten,ausserklinglberg,ausserlandholz,ausserleithen,ausserlengenwang,aussermanzing,aussernbrunst,aussernzell,ausserpuhret,ausserraah,ausserrettenbach,ausserroid,ausserrotzing,ausserschmirn,ausserschwende,aussersiggam,aussersihl,ausserst,aussersteinberg,aussert,aussertal,ausserteuchen,aussertreffling,ausserurberg,ausservals,ausserviligraten,ausservillgraten,ausserwald,ausserwall,ausserweerberg,ausserweg,ausserwimitz,ausset,aussevielle,aussig,aussillon,aussinange,aussinanges,aussivik,aussois,ausson,aussonce,aussonne,aussos,aussurucq,aust,austa,austad,austadal,austadberget,austafjord,austan,austana,austbo,austbo i helgeland,austborg,austbygd,austbygdi,austdalen,austein,austell,austen,austena,austeni,austera,austerbygda,austerbygden,austerelv,austerfield,austerheim,austerlitz,austeroya,austertana,austervefsen,austervefsn,austerville,austevoll,austgulen,austin,austin acres,austin center,austin corners,austin heights,austin springs,austin subdivision,austin village,austins post,austinabad,austinburg,austinmer,austins ferry,austins mill,austins post,austintown,austinville,austis,austla,austla kula,austmapur,austmark,austmarka,austnes,austonio,austraat,austral,austral eden,australia,australia landing,australia plains,australian capital territory,australie,australind,austrat,austratt,austre kaldvag,austre kanstad,austre risfjord,austrey,austrheim,austria,austrom,austrum,austruweel,austum,austvatn,austvoll,austwell,austwick,austyanovo,aususala,auswarts,auszig,aut chakha,auta,auta kilimou,autabak,autagne,autainville,autak,autama,autan,autanne,autapini,autaq siddiq malkani,autar,autargati,autaria,autaugaville,autavaara,autawa,autay,autazes,autbag,autbaria,autdar,autdlainiarfinguaq,autechaux,autechaux-roide,autelbas,autelhaut,autena,autendorf,autengrub,autengrun,autenhausen,autenried,autenweiler,autenzell,auter,auter shisha,auterive,auteroche,auterrive,auterwitz,autet,auteuil,auteuil-le-roi,autevielle,autevielle-saint-martin-bideren,autgaerde,autgaon,auth village,authal,authausen,authe,authenay,autheuil,autheuil-en-valois,autheux,authevernes,authezat,authie,authier,authier-nord,authieule,authieux,authieux-ratieville,authiou,authoison,authon,authon-du-perche,authon-la-plaine,authorpe,authou,authouillet,authuille,authume,authumes,auti,autichamp,autifare,autignac,autigny,autigny-la-tour,autigny-le-grand,autigny-le-petit,autilla,autilla del pino,autillo de campos,autingues,autinkyla,autio,autiola,autiomaki,autioniemi,autionkylla,autioranta,autiovaara,autisha,autishof,autit sok,autka,autlan,autlan de navarro,autlauc,autney,auto,auto aillo,autogi,autoire,autokia,autol,automamandririna,automba,autopan,autoreille,autoue,autouillet,autpara,autrac,autranges,autrans,autre-eglise,autreche,autreches,autrecourt,autrecourt-et-pourron,autrecourt-sur-aire,autremencourt,autrepierre,autreppe,autreppes,autretot,autreville,autreville-saint-lambert,autreville-sur-la-renne,autreville-sur-moselle,autrey,autrey-le-vay,autrey-les-cerre,autrey-les-gray,autreyville,autricourt,autrinna,autroche,autruche,autruy,autruy-sur-juine,autry,autry-issards,autry-le-chatel,autryve,autryville,autsahi,autschowa,autse,autsugals,auttagershofen,autti,auttinniemi,auttoinen,autu,autuch,autugh,autukia,autumane,autumn acres,autumn chase,autumn chase at riva trace,autumn estates,autumn grove,autumn hill,autumn leaves,autumn oaks,autumn ridge,autumn run,autumn wind,autumn wood,autumn wood circle,autumn woods,autun,autwine,auty,autz,autzendeich,auufer,auul,auun,auvainen,auvaismaki,auvak,auvakko,auval,auval,auvare,auve,auvelais,auvere,auvergne,auvergni,auvergny,auvernaux,auvernier,auvers,auvers-le-hamon,auvers-saint-georges,auvers-sous-montfaucon,auvers-sur-oise,auverse,auvet,auvet-et-la-chapelotte,auvi,auvil,auvila,auvillar,auvillars,auvillars-sur-saone,auvillers,auvillers-les-forges,auvilliers,auvilliers-en-gatinais,auvinen corner,auvinya,auvorm wald,auw,auw mal,auwaiabaiwa,auwaka,auwal,auwale,auwali,auwallenburg,auwara,auwarghon,auwe,auwegem,auwei,auweiler,auwel,auwelt,auweng,auwera,auwi,auwihu,auwim,auwin,auwinkel,auwob,aux,aux acacias,aux aibricuets,aux aibriquets,aux alloises,aux bagues,aux bruyeres,aux cayes,aux communes,aux croix,aux deux hetres,aux fagnes,aux figuiers,aux fins,aux fontaines,aux forges,aux fourchons,aux granges,aux grosses pierres,aux houx,aux marais,aux palmistes,aux parcs,aux peupliers,aux pieges,aux pins,aux plaines,aux plumettes,aux ravines,aux rois,aux rouges fosses,aux sable,aux sept chemins,aux tachenires,aux tournants,aux trois saules,aux zabrees,aux zazefagnes,aux-aussat,aux-et-aussat,auxais,auxais-le,auxan,auxances,auxange,auxant,auxelles-bas,auxelles-haut,auxerre,auxey,auxey-duresses,auxi,auxi-le-chateau,auxicourt,auxier,auxiliadora,auxilio,auxillac,auxon,auxon-dessous,auxon-dessus,auxonne,auxonnette,auxonnettes,auxvasse,auxy,auya,auya pihni,auya yari,auyakayi,auyama,auyamal,auyamas,auyamitas,auyamiti,auyan,auyana,auyaoyao,auyapura,auyarito,auyatara,auyawpa,auyeir,auyo,auyrakhmet,auyz-kuduk,auza,auzac,auzainville,auzainvilliers,auzais,auzances,auzas,auzashik,auzat,auzat-sur-allier,auzate,auzay,auzdan kili,auzeba,auzebosc,auzecourt,auzelles,auzeme,auzena,auzera,auzers,auzet,auzeville,auzeville-en-argonne,auzeville-tolosane,auzgiriai,auzgulani,auzguleni,auzhbikovay,auzhgiry,auzhgulyany,auzi,auzielle,auziki,auzillac,auzille,auzini,auzitabli,auzits,auzkirk,auzkurk,auzo,auzochiquia,auzole,auzolle,auzolles,auzon,auzon-les-marais,auzouer,auzouer-en-touraine,auzouville,auzouville-auberbosc,auzouville-lesneval,auzouville-sur-ry,auzouville-sur-saane,auzoux,auzukrogs,auzy-kuduk,auzyken,auzza,au\303\260kula,av darreh,av jadan,av khaneh,av naray,av par,avyan,av-e ahmad,ava,ava goze,ava nagornaya,avabadina,avabodji,avacalyan,avacelli,avach,avacha,avachak,avachalyan,avacuk,avacukoba,avadade,avadaiyarkovil,avadan,avadapur,avaday,avaddaveli,avaddiyaveli,avadhou,avadi,avadivry,avadiyarkoil,avador,avadori,avadovo,avaduri,avadzhara,avae,avafi,avaflakpota,avafors,avagabinaper,avaglio,avagnam,avagon,avagou,avagou tabot,avagudem,avaguzi,avaheden,avaho,avaholm,avahy,avai,avai onole,avaiaca,avail,availe,availle,availles,availles-de-vouneuil,availles-limouzine,availles-sur-chize,availles-sur-seiche,availles-thouarsais,avaio,avaira-waima,avaj,avajan,avajdan,avak,avakaa,avakha,avakhyl,avakind,avakli,avakly,avakodja,avakorme,avakpa,avakpa-motchokpevi,avakpe,avakpedome,avakpo,avakpodomi,avakubi,avakumov,avakush pervyy,avakush vtoroy,aval,aval,avalkheyl,avala,avala bala,avala pain,avalagbe,avalai,avalakonda,avalama,avalan,avalanche,avalao,avalavi,avalbulle,avalegaon,avaleh-ye bala,avaleh-ye pain,avaleonda,avalepo,avaleur,avalhalli,avaliden,avalin,avalinho,avalito,avalleur,avalli,avallon,avalo,avalon,avalon beach,avalon bench,avalon heights,avalon hills,avalon park,avalon park mobile home park,avalon shores,avalon terrace,avalon village,avalos,avalov,avalurpet,avam,avame,avan,avan sar,avan vardgesi,avan-arrinj,avana,avanah,avanak,avanam,avanamapase,avanas,avanashi,avanashipalaiyam,avanca,avance,avancena,avancha,avancon,avancy,avand,avandar,avandaro,avandus,avanduse,avane,avanek,avang,avang-biloug,avangan,avangane,avangard,avangardnoye,avangards,avangares,avangnam,avangnardlit,avanguenan,avangumba,avangun,avangwa,avanhandava,avanigadda,avanime,avanin chico,avaniya,avanj,avanjan,avanjir,avanlidhes,avanlu,avanmalleh,avanne,avanoglu,avanos,avanport ekonomiya,avans,avansiyeh,avant,avant prairie,avant-le-ramerupt,avant-les-marcilly,avant-les-ramerupt,avante,avantiguara,avanton,avantos,avants,avantul,avanun,avanzada,avao,avapal,avapessa,avapo,avar,avar zaman tappeh qayeli,avara,avara koyu,avarabary,avarabohitra,avarabonga,avarachani,avaradalana,avaradilana,avaradoha,avaradrova,avarak,avaran,avaran-kishlag,avarang,avarankal,avarankulam,avarankyshlak,avaranqislaq,avarantulawa,avaratrakoholahy,avaratrambolo,avaratrimanarina,avaratriniala,avaratsena,avaray,avarboda,avard,avardgan,avare,avareh,avarehha-ye balvan,avarenta,avareo,avares,avareshk,avarfaro,avargan,avaria,avarik,avarikos,avarin,avaris,avarita,avaritsa,avariwatta,avariyny,avariynyy,avariynyy poselok,avariz,avarkan,avarkhod,avarlu,avarnaluq,avarneh,avarnev,avaroa,avarp,avarra,avarsai,avarsal,avarsi,avarsin,avarskoe,avarskoye,avart,avartin,avarua,avarzaman,avarzeado,avas,avas duchan,avas duepan,avasalu,avashanaq,avashegy,avashla,avasinis,avasjo,avason,avasor,avaspuszta,avassen,avastanya,avaste,avastrom,avat,avata,avatar,avatele,avati,avati djobedji,avatinguara,avatip,avatiu,avatok,avatolampy,avaton,avatoru,avatouna,avatrask,avau,avaubanda,avaudden,avausa,avaux,avaux-le-chateau,avav,avaviken,avavu,avawam,avaxil,avaz,avaza,avazak,avazan,avazar,avazikope,avazy,avba,avbaek,avbakhsh,avban,avband,avbara,avbarik,avbat,avber,avbeze,avbiaman,avbiugu,avblaman,avbriaria,avcac,avce,avchadulan,avchal,avchala,avchaly,avchi,avchikalacha,avchinets,avchinnikovo,avchok,avchukhi,avchurino,avci,avcibasi,avcicayi,avcicayiri,avcik,avcikoru,avcikoy,avcilar,avcilarsuyu,avcilaryaylasi,avcili,avciova,avcipinar,avcipinari,avcisuyu,avcitepe,avda,avdakay,avdal,avdala,avdalagalu,avdalar,avdalari,avdali,avdalla,avdally,avdalovo,avdan,avdanciftligi koyu,avdancik,avdang,avdankasy,avdankoy,avdanli,avdanmuhacirleri,avdanpinari,avdansirmy,avdara,avdarma,avdarreh,avdashi,avdebo,avdeevo,avdeikha,avdela,avdelan-e `olya,avdelan-e sofla,avdeler,avdelero,avdelikha,avdella,avdellero,avdeni,avdenkovo,avderod,avdesagbon,avdetovo,avdey,avdeyeva,avdeyevichi,avdeyevka,avdeyevka pervaya,avdeyevka vtoraya,avdeyevo,avdeyevskaya,avdeyevskaya baza,avdeyevskiy,avdeyevskiye,avdeyevskiye vyselki,avdeykovo,avdhelas,avdheliakos,avdhelianos,avdhella,avdhellas,avdhellero,avdhelleron,avdhira,avdhou,avdhoular,avdhouliana,avdi,avdibasici,avdibek,avdic mahala,avdici,avdihodzici,avdijevic mahala,avdijina voda,avdil,avdiley,avdillaret,avdimou,avdinka,avdinseyh,avdiyivka,avdomovka,avdon,avdoshchi,avdoshi,avdoshitsy,avdotina,avdotinka,avdotino,avdotino-tykhvinskoe,avdotivka,avdotivo,avdotkino,avdottsino,avdottsyno,avdotyevka,avdotyevo,avdour,avdovi,avdul,avdula,avdular,avdulla,avdullar,avdullu,avdulovo,avdulovo pervoye,avdulovo vtoroye,avdunino,avdur,avdurr,avdyatovo,avdyukhovo,avdyukovo,avdyushina,avdyushino,avdzaga,avdzeyeviche,avdzh,avdzhi duvandzhii,avdzhi duvandzhiy,avdzhin,ave,ave afiadenyigba,ave casta,ave chica,ave djen,ave gratia plena,ave maria,ave moma,ave-et-auffe,avea,aveacco,avebadie,avebe,avebury,avecaozinho,avecappelle,aved,avedal,avedano,avedie,avedillo de sanabria,avedio,avedje,avedje"e,avedjeta,avedji,avedji anyigbe,avedji sagada,avedjikpodji,avedo,avedome,avedore,avedore holme,avedotoe,avedzeme,avedzi,avee gbogame,avefodzo,avega,avega agornu,avegado,avegagome,avegagorme,avegame,avegan,aveggio,avegno,avego,avegodo,avegodoui,avegoeme,avegombori,avegoumbiri,aveguening,avegwime,aveh,aveha,avehe,aveho,aveho-tongbe,avehza,avei,aveia,aveime,aveinte,aveiras de baixo,aveiras de cima,aveiro,aveize,aveizieux,avej,avejan,aveji,avekakpo,avekapelle,avekilja,avekova,avel,avela,avelagbe,avelal,avelanges,avelanoso,avelar,avelas da ribeira,avelas de ambom,avelas de caminho,avelas de cima,avelayong,avele,avelebe,aveleda,aveledo,aveleira,aveleiras,avelelas,avelenam,avelengo,avelengong,avelerbach,avelesges,avelete,aveley,avelgem,avelghem,aveli,avelin,avelina,avelinere,avelinero,avelingen,avelino,avelino silveira,avelinopolis,avella,avella highlands,avellana,avellanar,avellaneda,avellaneda-sopuerta,avellanedo,avellanes,avellanosa de muno,avellanosa de rioja,avellanosa del paramo,avellino,aveloso,avelsater,avelsbach,avelsbol,aveluma,avelus,aveluy,avelya,avema,avemafe,aveme beme,aveme danyigba,aveme dra,avemene,avemo-reno,aven,avena,avenal,avenale,avenales,avenan,avenas,avenaw,avenay,avenay-val-dor,avenca,avencal,avencal de baixo,avencas,avenches,avend,avendale,avendanero,avendano la antigua,avendanos,avende,avendin,avendita,avendorf,avendshausen,avene,avenek,avenel,aveneltsy,aveney,aveng,avengan,avenger village,avenggu,avengu,avenheim,avenhorn,aveni,avenida,avening,avenir,avenisi,avenjan,avenkaan,avenliq,avennes,aveno,avenofeme,avenong,avenopedo,avenopedu,avenopema,avenorpeme,avenou,avenquinha de santo antonio,avenriep,avensac,avensan,avensar,avensermoor,avensoc,avensor,avenstoke,avent,aventazon,aventeira,aventignan,aventoft,aventome,aventon,aventosa,aventura,aventuras,aventureio,avenu,avenue,avenue city,avenue of oaks,avenue range,avenues,avenui,avenwedde,avenwi,aveny,aveny-montreuil,avenya,avenza,avenzada,avepozo,aver-o-mar,avera,averan,averana,averanga,averara,averbakhovskoye,averbeck,averbode,averbodeheide,averbruch,averd kan,averdoingt,averdon,averdung,averdunk,avere,averechten,avereest,averegten,averek,averekpe,averenki,averesch,averesti,averesti de sus,averestii de jos,averestii-de-sus,averett,averfehrden,averfleth,avergalle,avergan,avergi,avergian,avergyan,averhoy,averia,averias,averill,averill park,averin,averinka,averino,averinskaya,averinskiy,averinskiy pochinok,averinskoye,averiny,averkaan ayem,averkievskaja,averkin,averkina,averkino,averkiyevo,averkiyevskaya,averknevo,averkova,averkovo,averlak,averliaz,averlo,averloo,avermaat,avermes,avermun,averna,avernaes,avernai,avernak by,avernako,avernako by,avernas,avernas-le-bauduin,avernes,avernes-saint-gourgon,avernes-sous-exmes,averof,averole,averoma,averon-bergelle,averreest,avers,aversa,aversi,aversta,averstad,avert,avert d ingherina,avert dinghirina,averton,averum,avery,avery corner,avery creek,avery hill,avery island,avery landing,avery place,averyanovka,averyanovo,averyanovskiy,averyata,averys place,averyville,aves,aves-bay,avesa,avesale,avesalos,aveschoot,avesegh,aveshki,aveshnaya,avesido,avesipi,avesive,avesland,avesnelles,avesnes,avesnes-chaussoy,avesnes-en-bray,avesnes-en-val,avesnes-le-comte,avesnes-le-sec,avesnes-les-aubert,avesnes-les-bapaume,avesnes-lez-aubert,avesnes-sur-helpe,avessac,avessada,avessadas,avesse,avessido,avest,avesta,avestbo,avestruz,avesudes,avet,aveta,avetakpo,avetaranots,avete,avetile,avetome,aveton gifford,avetonou,avetrana,avetui,aveube,aveux,aveve,avevi,avevig,avevoen,avevonou,avevwen,avewi,aveyigbome,aveyime,aveyime salem,avezaath,avezac,avezac-prat,avezan,aveze,avezryah,avezzano,avga,avgadi,avgalida,avgalidha,avgan,avgancik,avgandala,avgani,avgar,avgardu,avgaria,avgaris,avgarmak,avgarskiy,avgarskiy chugunoliteynyy zavod,avgeat,avghani,avgi,avgindik,avgo,avgolida,avgolidha,avgon,avgonima,avgonyma,avgora,avgoro,avgorou,avgoru,avgos,avgury,avgustinovici,avgustinovka,avgustov,avgustovka,avgustovo,avgustovskiy,avgustuvka,avharik,avhatyakasi,avhegame,avholm,avhustivka,avi,avi zurk,aviel,aviezer,avi`ezer,avia,avia terai,aviacao,aviacion,aviados,aviaga,aviagee,aviagorodok,aviagua,aviamp,avian,aviano,aviano-castello,aviar chingo alto,aviar da jimba,aviara,aviara-oreke,aviara-waima,aviario,aviator alexandru juganaru,aviatsiya,avicaya,avich,avichaca,avichuca,avicola brisas de huila,avicola mexico,avicola rio bravo,avicondji,aviconji,avida,avidagos,avide,avidha,avidiamato,avidos,avidzhmarkasha,aviefe,avieh,avieme,aviemi,aviemore,aviepe,aviernoz,avieteta,avigaat,avigait,avigdor,avigedor,avigenou,avigliana,avigliano,avigliano lucana,avigna,avigne,avigneau,avigni,avignon,avignonet,avignonet-de-lauragais,avigny,aviguenou,avihain,avihang,avihara,avihasa,avihayil,avihi,avij,avijan,avijdan,avik,avikara,avike,avikebruk,avikilai,avikiliu,avikilos,aviklo,avikly,avikope,avikorpe,avil,avilchyay,avil-boumba,avil-chicago,avila,avila beach,avila camacho,avila de los caballeros,avila de los santos,avila place,avila y urbina,avilanes,avilaq,avilcha,avilciai,avilcin,avileno,aviles,avileses,avilez,avilhoso,aviliai,aviliana,avililla de la sierra,avilio,avilion,aviliu,avilla,avillal,avillers,avillers-sainte-croix,avilley,avillon,avilly-saint-leonard,avilo-fedorovka,avilo-uspenka,avilo-uspenskiy,avilona,avilov,avilovka,avilovo,avilovskiy,avilton,avily,avilya,avilyai,avilyay,avimi,avin,avin bala,avin pain,avin-e `olya,avin-e bala,avin-e masjedlu,avin-e pain,avin-e sofla,avinahalli,avinasa,avinazas,avinca,avindi,avindin,avindja,avine,avineliai,avingan,avinger,avington,avingue,avinhalli,avinho,avini,avinishche,avinishchi,avinjes,avinliq,avino,avinonet,avinonet de puig ventos,avinorm,avinovo,avinsino corner,avintes,avintsy,avintu,avinukorpe,avinurme,avinyo,avinyonet,avio,avion,aviones,avios ermolaos,aviosi,avioth,avioua,avir,avira,avirak,avirama,avire,avirey,avirey-lingey,aviron,aviron bay,avirons,avis,avis mills,avisford,avish,avishk,avishu,avisi,avisipi,aviski,avispa,avispaa,avispal,avispas,avispea,avispero,avissawella,avist,avistby,avister,aviston,avita,avital,avitouro,avivim,aviz,avizar,avize,avizhantse,avizieniai,avizoniu,avizonys,avizria,avjeboda,avka,avkadi,avkalan,avkamasya,avkasir,avkat,avkati,avkaz,avketi,avkhana,avkhangan,avkhara,avkhareh,avkhimki,avkhor,avkhvor,avki,avkolevo,avkouresi,avksenkovo,avksentyevo,avkul,avkveti,avla,avlachia,avlacia,avlacicayiri,avlacik,avlad,avlaga,avlagcayiri koy,avlagi,avlagicayiri,avlagicayiri koyu,avlagikaya,avlagisoku,avlah,avlahe,avlahi,avlai,avlak,avlakes,avlakh,avlakhia,avlaki,avlakia,avlakiai,avlakion,avlalii,avlaliy,avlamis,avlan,avlan koy,avlankioi,avlanku,avlanlar,avlanma,avlanmis,avlapame,avlarukaya,avlash,avlata,avlato,avlavukaya koy,avlavukaya koyu,avlayakan,avlby,avleh hasan,avlekete,avlekete-plage,avlemon,avlemona,avlemonas,avles,avlevi,avli,avli koy,avliana,avlianna,avlica,avliene,avlieni,avlija,avlije,avlik,avlime,avlingebo,avliotai,avliotes,avliun,avliun ahonozo,avliyan,avliyan mahallesi,avliyana,avlo,avloisa,avlon,avlona,avlonari,avlonarion,avlonya,avlonya yaylasi,avlorto,avlos,avlosa,avloto,avlotopos,avloysa,avlu,avluca,avlucaericek,avluk,avlukaya,avlum,avlume,avluobasi,avly,avmain,avnan,avnaray,avnbol,avne etan,avne hafez,avnede,avneporog,avnevi,avni tolunay,avnik,avning,avnslev,avnslev nederby,avnslev overby,avnyuga,avnyugskiy,avnyugskiy-pochinok,avo,avoane,avobengono,avobral,avoca,avoca beach,avoca park,avocadale,avocado,avocado heights,avocanzou,avocar,avocat,avocats,avoch,avocourt,avodim,avoeme,avoeme kpotame,avoes,avogbe,avogo,avogonia,avoikope,avoinat,avoine,avoines,avoise,avokiri,avokpo,avola,avolasio,avole,avolienu,avolienu ahonlezo,avolokope,avolsheim,avom,avome,avon,avon center,avon corner,avon crest,avon dam,avon dassett,avon forest,avon heights,avon lake,avon park,avon park lakes,avon park north,avon park south,avon plains,avon point,avon rise,avon-by-the-sea,avon-la-peze,avon-les-roches,avonak,avonbridge,avonburg,avoncore,avondale,avondale condo,avondale estates,avondale heights,avondale hill,avondale springs,avondale terrace,avondale township,avondale village,avondale west,avondance,avondhu,avondkraal,avondrust,avondster,avondzon,avonia,avonkwu,avonlea,avonmore,avonmouth,avonport,avonsleigh,avontuur,avonvue,avonyokofe,avoortbrug,avoortsbrug,avora,avorani,avord,avordovisa,avore bamba,avoriaz,avormitok,avoros,avorreia,avortin,avoshki,avosnes,avot,avot-le-ruisselot,avotini,avotkalni,avoty,avou,avoudrey,avougon kope,avouko,avoukouekope,avounbaka,avoungyanou,avoutsou,avovu,avoy,avoyee,avparan,avparu,avpay,avpurak,avra,avra valley,avraamovka,avradsberg,avradstjarn,avraga,avrainville,avrakonde,avrakondes,avrakondhe,avrakos,avral,avram iancu,avram jancu,avramdancu,avramen,avrameni,avramenkov,avrameny,avramesti,avrami,avramilia,avramio,avramion,avramiou,avramit,avramov,avramovica mala,avramovici,avramovina,avramovka,avramovo,avramovska,avramovskiy,avramovtsi,avranches,avrancilar,avrang,avrankou,avranlo,avranville,avrat alan,avrateli,avratin,avrazan,avre,avrechy,avrecourt,avree,avreet,avregny,avrekete,avrekete beach,avrekete-plage,avremesnil,avremivka,avren,avrenkoyu,avrenmolla,avres morad,avressieux,avret hisar,avreteli,avreuil,avri,avricourt,avrieux,avrig,avrigney,avrigny,avrihan,avrik,avrika zheri,avrika zhori,avrikhan,avril,avril-les-loups,avril-sur-loire,avrille,avrille-les-ponceaux,avrilly,avrilmont,avrinskiy,avriva,avriz,avro,avrolles,avroman,avron,avrora,avrorovka,avros,avroult,avruy,avry,avry-sur-matran,avryuz-tamak,avsa,avsala,avsallar,avsar,avsar koyu,avsaragzi,avsaralani,avsarcik,avsargidiric,avsarina,avsarki,avsarkideric,avsarkoy,avsarkoyu,avsarlar,avsarli,avsarlu,avsaroren,avsarozu,avsarpotuklu,avsarsogutlu,avsarviran,avsat,avsaveran,avsen,avsere yaylasi,avsergovo,avserveran,avsetcavsi,avseyenkov,avseyenok,avseyevo,avseykovo,avshalom,avshar,avshen,avshitor,avshlaukis,avshlavki,avsin,avsiya,avsiye,avsorkh,avssaqutaq,avstrichu,avsulakh,avsundu,avsuyu,avsyukovo,avsyunino,avtafa,avtah,avtala,avtalyon,avtara,avtenice,avtepe,avtitnan,avtobus,avtocborochnyy,avtodeyevo,avtoevo,avtogarazh,avtona,avtonomovka,avtonomovskiy,avtoprokladka,avtoro,avtoulyuk,avtovac,avtovo,avtozavodskiy rayon,avtret,avtrup,avtukhovka,avtunichi,avtunne,avtunychi,avturi,avtury,avtushkovo,avu,avu avu,avu-bere,avuavu,avuavu mission,avub goth,avubere,avuc,avuc koy,avuclar,avudangawa,avudo,avuglakofe,avuglakorpe,avuglanpu,avuka,avukat haci koy,avukathacikoy,avuku,avulkheyl,avulamanda,avule,avulegama,avully,avuluko,avume,avun,avunca,avund,avunduk,avundukyaylasi,avundur,avunduruk,avungu,avunkum,avunlar,avunu,avupalli,avupan,avuqa,avuqah,avur,avurga,avurgakoyu,avurka,avurtepe,avurtepe koyu,avuruwando,avus,avush,avut,avutmus,avuto,avuttepe,avutu,avuvu,avuye,avvaguba,avvakko,avvakumovo,avvakumovskiy,avval darvazeh,avval tang,avval,avval-e tutang,avvalin,avveel,avvenek,avvil,avvolovo,avwo,avy,avyak,avyakskoye,avyan,avyarud,avyavaam,avyeniki,avyeraiika,avyereika,avyerinos,avyi,avyion,avyr-sirma,avyr-sirmi,avyr-sirmy,avyygan,avzali,avzaman,avzarok,avzerik,avzikent,avzin,avzini,avzyano petrovsk,avzyanopetrovsky,avzyanskaya leskhimpromartel,aw,aw aadan fayi,aw aamow rooble,aw axmad,aw baasow,aw bakhsh,aw bakhshi,aw band,aw barik,aw biti,aw bitte,aw burdah,aw cariif,aw cusmaan,aw daarow,aw darah,aw dhiinle,aw dhuroow,aw dinle,aw dokan,aw dukan,aw gardu,aw gooye,aw guurow,aw humoow,aw jiloole,aw kalan,aw khanah,aw khorak,aw khwarchah,aw kuulamuudey,aw kuulaw,aw liibaan,aw main,aw mar,aw mayow,aw misigey,aw mordi,aw nagay,aw naray,aw porak,aw raansow,aw raygak,aw rayzi,aw reenow,aw riinow,aw sacad aawow,aw sagaare,aw sarin,aw sarina,aw sarinah,aw shabeel isaaq,aw shini,aw sulakh,aw talah,aw tari,aw xajow,aw yaale,aw yaay,aw yarow,aw yarow maroodiyoow,aw-e ahmad,aw-ga,aw-ka-tha,awa,awa data,awa kansara,awa ndon,awa-damkondre,awa-dantai,awa-ein,awa-iti,awa-za,awaaj,awaala gupte,awaan,awaawakino,awaay,awaba,awabah,awabeik,awabelili,awabiadi,awabye,awabyin,awaccung,awach,awach tende,awacha,awachang,awache,awacung,awad,awad al haib,awada,awada-gazi,awadalan,awadam,awadan,awadaung,awadhan,awadi,awadiya,awadji,awado,awadur,awae,awae fala,awae i,awaf khan,awaflakpota,awag,awaga,awagabi,awagala,awagamachi,awagan,awagat,awagauk,awagir,awagne,awago,awagome,awagura,awagyaik,awagyaw,awah,awah rustum bak,awaham,awahame,awahikoro,awahikro,awahuri,awai,awai `aziz `ali,awai ahmad-i fathi karam,awai ifunkpa,awai karim,awai rustam beg,awai saiyid mahmud,awai saiyid rahim,awai shah,awai shaykh husayn,awai-awai,awaiama,awaiat,awaida,awaiepa,awailfunkpa,awain,awaing,awainggale,awairij,awaiye,awajan esh shamali,awajan esh sharqi,awaji,awajir,awajki,awak,awak pah,awak pow,awak powe,awaka,awakada,awakala,awakaluku,awakane,awakaponga,awakawasik,awake,awakening,awakenre,awakeri,awaki,awakino,awakua,awakura,awakurta,awal,awal buley,awal guba,awal khel,awal shah,awal shur,awala,awala bate,awala number 1,awala number 2,awalai,awalawa,awale,awali,awalin,awaliwali,awalka,awalkhel,awalkheyl,awalla,awallan,awaluta,awalwas,awam,awamangu,awamarino,awamba,awami colony,awami park,awamiya,awamoko,awamunga,awan,awan al hasan,awan bahadurabad,awan baloch,awan baluch,awan banda,awan bholu,awan colony,awan dhaewala,awan dhaiwala,awan kalan,awan labana,awan lubana,awan par,awan salawat,awan tahli,awan town,awan-lgid,awana,awanabad,awanah,awanai,awanan di ker,awanan ka dera,awanan-de-jhugge,awananwala,awananwala bhan,awananwala dabhari,awanararu,awanato,awanawala,awanay,awanda,awandar,awandawaki,awande,awandjello,awandjelo,awanewala,awang,awang gongshe,awang-awang,awangalimin,awangaling,awangard,awangarh,awangawang,awangbangkal,awanggai,awangi,awangibinna,awangio,awanglo,awangmadia,awango,awango-dongo,awangpallime,awangsa,awangtampung,awani bhun,awanibaku,awanipa,awaniwala,awanje,awano,awanpur,awanpura,awans,awansouri,awantipur,awantipura,awanui,awanwala,awanwala janubi,awanwala shumali,awanwala tubewell,awanwali,awanwali basti,awao,awao banda,awapasareng,awapesareng,awapiri,awappasareng,awar,awar kachhi,awar-awar,awara,awarabati,awaraguragu,awarah,awarai,awaran,awarantulawa,awararawar,awarawar,aware,awareh,awareke,awaren,awarge,awargunting,awari,awari ka khirk,awariwatta,awariwaunau,awariya,awarjok,awarke,awarla,awarneh,awarnew,awarniw,awaro,awaroa,awarra,awaru,awarua,awarua plains,awaruwaunawa,awarzan,awas,awas bila,awas ducban,awas rila,awasa,awasa-busama,awasalo,awasan,awasbila,awase,awash,awasha,awashait,awashi,awashima,awasi,awasiaju,awasikrom,awaslupia,awaso,awassa,awassagara,awasta,awastara,awasthi purwa,awastingni,awasu,awasura,awat,awat awat,awat nkang,awata,awataba,awatabe,awatanae,awatchi,awatchie,awate agame,awate todome,awate todzi,awatea,awati,awatib,awatichen,awatihsien,awatip,awatkul,awatmata,awatonya,awatoto,awatowatowa,awatuna,awau,awaua,awaula,awaw,awaw bandeh,awawah,awawas,awawas rapido,awawaso,awawom,awawu,away,away `aziz `ali,away ramadan,away rustum bayk,away sayyid mahmud,away sayyid rahim,away town,away yara,awayan,awaydoulou,awaye,awaye-yara,awayetitun,awayo,awayon,awaz,awaz kili,awazakiri,awazha,awazi,awazi khan koruna,awazu,awazu-machi,awazumachi,awba,awba-mowo,awbada,awbakhs,awbakhsh,awban,awband,awbar,awbara,awbarah,awbari,awbarik,awbawda,awbele,awben,awbere,awbete aworo,awbi,awbi lali,awboraka,awbum,awburda,awburi,awca,awcak,awcal,awcang,awcha,awchak,awchal,awchang,awchioma,awda,awdacht,awdahkay,awdak,awdak-e bala,awdak-e khord,awdak-e pain,awdak-e pa`in,awdakai,awdakay,awdake bala,awdake balena,awdake khurd,awdake payan,awdake tayna,awdakey,awdakh,awdal khune,awdalan,awdali,awdalok,awdam,awdan,awdan ibola,awdan ilaro,awdan iyasi,awdang,awdara,awdarrah,awdaw refurefu,awday,awde,awde-omu,awde-shaka,awdeda,awdejewka,awdest,awdey,awdh,awdheegle,awdheere,awdiinle,awdist,awdula,awdunabon,awdzaga,awe,awe ahmad,aweaka,awebindat,awecken,aweday,awee,aweer,aweey,aweg,awegane,awegeutah,awegyun,aweh,awehkubang,awei,aweil,aweina,aweitan,awej markasa,awej markashah,awej markehshah,awek,awel,awela,awele,aweli,awelo,awen,awena,awenadi,awenare,awencang,awendaw,awengen,awenmertasari,awennade,awenne,aweno,awer,awera,awera banda,awera umar khan,aweragor,awerange,awerangnge,awerankale,awere,aweri,awes,awesang,awese,awesh,aweshtor,aweshtur,awesi,awestor,awet shol,aweta,awete,awetebi,awewala,awewe,aweyam,aweyden,aweyna,awezry,awezrya,awfa-igbo,awfin,awfir,awga,awgak,awgangi,awgardu,awgarmak,awgaro,awgawgawraw,awgawmbaw,awgawtun,awgbagba,awgbawraw,awghani,awgir,awgoosh,awgu,awgu market,awgyi,awha,awhaydah,awhiaah,awhiabam,awhitu,awhitu central,awho,awhum,awi,awi hadir,awi hazir,awi shah,awi shah kili,awiaro,awiabo,awiakalakala,awiakrom,awiam,awiapus,awiaso,awibaraja,awibulu,awicandi,awicarang,awidarra,awidatar,awidji,awiebo,awigbene,awigede,awihao,awihazir,awihideung,awij markashah,awijao,awijiaratora,awik,awikushla,awika,awikas,awiko,awikondang,awila,awilaga,awilarangan,awilarangan 1,awilarangan 2,awilarangan dua,awilarangan kidul,awilarangan satu,awile,awilega,awiligar,awilke,awilodyang,awiloear,awiloear dua,awiloewar,awilowka,awilsito,awiluar,awim,awimekar,awin,awina,awinan,awinatt,awinet el rihan,awinet nemadi,awing,awingahgar,awingan,awingayang,awingyi,awingyi-ashe,awini kouma,awinnas,awino,awinorm,awio,awipari,awipari dua,awipari satu,awipari tengah,awir,awirangan,awirarangan,awirarangan dua,awiregya,awireja,awirhe,awiri,awirin,awirs,awirt,awis,awisa,awisalam,awisan,awisang,awisari,awise,awisem,awisewu,awisewu lebak,awisewu toggoh,awish al hajar,awish el-hagar,awishtor,awisurat,awit,awita,awitali,awitan,awitengah,awitiris,awitke,awiwabekun,awiwala,awiwang,awiwinghi,awiwulung,awiytan,awizriah,awj qana,awj qubbah,awja ata,awja awdaw,awja-ikoradu,awjan pasha,awjaw,awje,awjedeji,awjennike,awjilah,awjlahwala,awka,awka ikot ene,awka-agbobu,awkaka,awkal,awkalan,awkawng,awke,awkelef,awkhan,awkhana,awkheng,awkhing,awkhke,awkho ak,awkhor,awkhorak,awkhwarca,awkhwarchah,awki,awkinni,awkir fawqani,awkir tahtani,awkuzu,awla,awla qut,awla sidi,awla-sat,awlad,awlad `abd ad daim,awlad `abd al `aziz `inan,awlad `abd al jalil,awlad `abd al mawla,awlad `abd allah,awlad `abidin,awlad `ali,awlad `ali bu al qasim,awlad `amr,awlad `atiyah,awlad `azzam,awlad `azzaz,awlad `inan,awlad `isa,awlad `ulayyu,awlad `umar,awlad abu ras,awlad abu salamah,awlad abu slamah,awlad abu suhayyirah,awlad al `abid,awlad al `adawi,awlad al qasim,awlad ar rawajih,awlad ash shaykh,awlad ash shaykh `ali,awlad badr,awlad bahij,awlad bin ahmad,awlad bin ya`qub,awlad bu `ali,awlad bu hadda,awlad bu jadid,awlad bu ras,awlad dafir,awlad dayf allah,awlad ghanam,awlad gharib,awlad hamad,awlad hamam,awlad hamid,awlad hamzah,awlad hanah,awlad ibrahim,awlad ilyas,awlad isma`il,awlad jabbarah,awlad jabir,awlad jarbu`,awlad jibarah,awlad kazi\302\247im,awlad khalaf,awlad khalaf allah,awlad khalifah,awlad madi,awlad madif,awlad mahmud,awlad majid,awlad mansur,awlad marzuq,awlad mas`ud,awlad mudif,awlad mudrif,awlad muhanna,awlad musa,awlad mushad,awlad najm al qibliyah,awlad najm bahjurah,awlad nijm bahjurah,awlad nusayr,awlad nuwayr,awlad qalasi,awlad raiq,awlad rayiq,awlad rayyan,awlad sa`id,awlad salah,awlad salamah,awlad salim,awlad salim bahri,awlad saqr,awlad sarhan,awlad sarrar,awlad sayf,awlad shaykh,awlad shulul,awlad si khalifah,awlad sidi khalifah,awlad siraj,awlad tallis,awlad tawq gharb,awlad tawq sharq,awlad tuq gharb,awlad yahya,awlad yahya bahri,awlad yahya qibli,awlad yaniq,awlad yasin,awlad yusuf,awlad-e adinah,awlad-e bay,awlad-e khayrullah,awlad-e mirza,awlad-e rahmat beg,awlad-e sufi,awlada,awladah,awlade adina,awlade bay,awlade khayrullah,awlade marza,awlade mirza,awlade rahmatbeg,awlade sufi,awlagal,awlah sat,awlah-sat,awlahgal,awlam qol,awlanbyin,awlang,awlani,awlaq esh shekh babikir,awlas,awlawawle,awlawde,awlawka,awlawkaw,awlawngpang,awlawpade,awlawrunda,awlawule,awlaya,awlazeren,awlii gara,awlia chashmah,awliga,awligah,awlime,awliyawah,awlo,awlowa,awlu,awlus,awlya casma,awlya chashma,awlya cheshmeh,awma`in,awmal,awmawdun,awmese pa`in,awmesh-e pain,awmish-e pain,awmishi pain,awnahof,awne,awng niu,awngchit,awnglawng,awngsa,awnu,awo,awo bandah,awo ekiti,awo idemiri,awo ndemini,awoan,awoane,awob,awobi,awobiso,awode-aborakan,awodeji,awodeyi,awodikora,awodikura,awodikuro,awodu kinchi,awodua,awoeliki,awoeni,awofa,awofeke,awogame,awoiki,awoingt,awoja,awojabule,awojaito,awojaitoi,awok,awol,awolabul,awolagading,awolagadinge,awolakakiri,awolese,awoliki,awoliti,awolu,awolua,awom,awoma,awomama,awomama catholic seminary,awomaso,awomerew,awomiro,awomo,awomuku,awon,awonga,awonggo,awoni,awopaka,awopeju,aworansua,awori,aworkiri,aworo,aworo kit,aworobia,aworoso,aworowa,aworra,aworro,aworuo,aworworso,awosiaju,awosting,awotan,awotarae,awotarighakiri,awote,awoudid,awoula,awouli,awout,awoute kondji,awowo,awowoto,awoyaya,awoye,awpaleye,awpar,awparan,awparu,awpay,awpeji,awpoch,awpos,awposh,awpuch,awpurak,awqaf,awqalat al jahaman,awqalat al kutshan,awqol,awqowl,awr bakah,awra,awraga,awragal,awrahgal,awrail,awrak,awrama,awrama hkamwe,awramowo,awramowzi,awrang,awrata,awrawa,awrawgu,awre,awregak,awren,awrengya,awreta,awretu,awrezi,awrgi,awrij,awrila,awrili,awringia,awrir n ait nasser,awroso,awsaday,awsagan,awsai,awsaj,awsara,awsarah,awsawmba,awshakan,awshara,awsharah,awshaw,awshile,awshow,awshu,awshun teddo,awsim,awssagdalt,awsu,awsun,awsurkh,awsworth,awtak,awtala,awtalah,awtay,awterere,awteyi,awthaw,awtlawk,awton,awtu,awtun,awu,awu bere,awu-awu,awua,awuawu,awuawu dua,awuawu satu,awuaya,awube,awubere,awudo,awudua,awuduankwanta,awuhe,awuk,awukonagoda,awukongoda,awuku,awukudzekorpe,awukugua,awukujekofe,awukyere,awul,awulbodale,awule,awulkhel,awulkin takare,awulkite,awulkitina,awulogun,awulu,awulua,awumniri agbaja,awun,awunaga,awunakrom,awund,awundos village,awungi,awuni,awuni linchang,awunio,awunmiri nkareku,awuno,awunu,awuransa,awuransua,awuraso,awurawa,awurawso,awure,awuro oyo i,awuru,awuru emigi,awurugboko,awusan,awusenu,awusiejo,awusikrom,awusu,awutit,awutsun,awutu,awutui,awuxiang,awuyeso,awvaguy,awvak,awwain,awwal chhachhar,awwal hashim abra,awwal khan,awwaw,awwin,awwinorm,awyak,awzachan,awzadaw,awzena,ax,ax-borze,ax-les-thermes,axala,axalhevi,axams,axapusco,axari,axaryd,axat,axaxacualco,axbach,axbaro,axberg,axbridge,axcaco,axdir,axdorf,axe adze,axe and adze,axebo,axed-dal,axedale,axel,axelasell,axelasmoarna,axelby,axels sassing,axelsche sassing,axelshof,axelsro,axelstorp,axelsvik,axeltorp,axelvold,axelwalle,axemann,axendorf,axenstein,axente sever,axeryd,axet,axeville,axford,axhiefci,axhult,axi,axia,axia linchang,axiak,axial,axiang,axiangcun,axiat,axibacai,axien,axili,axili daworzu xiang,axili kazakzu xiang,axilotitla,axim,aximuchang,axin,axinal dos rodrigues,axinim,axintel,axintele,axiochori,axiochorion,axiokastron,axiokhorion,axioma,axioupoli,axioupolis,axiparcer,axirong,axis,axisxa,axiu,axixa,axixa de goias,axixa do tocantins,axixintla,axkincu-borze,axland,axlarp,axle,axlunda,axmar,axmar bruk,axmarby,axmarsbruk,axmed cigoow,axmed gaas,axmed guure,axmed kamatiirey,axmed maxamed cali,axminster,axmouth,axnas,axochiapan,axochio,axochis,axochitlan,axocopa,axocopan,axocuapam,axocuapan,axole,axon chintu,axos,axotla del monte,axoxuca,axoyotla,axpe,axpe de busturia,axpoele,axpradzor,axsam,axsjo,axson,axstad,axstedt,axtaci,axtaci sirvan,axtaxana,axtbrunn,axtel,axtell,axtell corner,axtham,axtheid,axtheid-berg,axtla,axton,axton landing,axtorna,axtorp,axu,axui,axullu,axum,axundlu,axura,axutia,axutla,axvall,axwijk,axylou,ay,ay beyk,ay buulleey,ay buuraaley,ay cot,ay darvish,ay del macho,ay dooya,ay guba,ay guud wiin,ay hlay,ay ho lay,ay ilia,ay kostantino,ay kulak,ay orman,ay pavlo,ay qal`eh,ay qal`ehsi,ay tamer,ay trias,ay vavatsinya,ay yorgi,ay-botkan,ay-bubei,ay-bubek,ay-dogdu,ay-khodzhaly,ay-sur-moselle,ay`at,aya,aya abam,aya baluch,aya bentikh,aya filya,aya i,aya khan,aya napa,aya prikro,ayaamvela,ayabivila,ayakue,ayamiteng,ayantang,aya-ama,aya-aya,aya-makhi,aya`ntang,ayaakaa,ayaaman,ayaamang,ayaamaya,ayaamvele,ayaan,ayaana,ayaantang,ayaas,ayaase,ayaba,ayabaca,ayabaca viejo,ayabacas,ayabagir,ayabamba,ayabang,ayabarrena,ayabayani,ayabe,ayabi,ayabifara,ayabilon,ayabo,ayaboe,ayaboi,ayaboro,ayabpur,ayacachapa,ayacara,ayacaxtepec,ayacaxtle,ayacbenvila,ayaccasa,ayaccasi,ayacerca,ayacha,ayachi,ayachiye,ayacmbong,ayacntang,ayacollpa,ayacor,ayacos,ayacuchito,ayacucho,ayacue,ayad,ayad ben mannsour,ayad khan,ayadane,ayadaw,ayadega,ayadeghe,ayaebam,ayaelon,ayafein,ayafila,ayag karvend,ayaga,ayaga kwari,ayagaga,ayagai,ayagama,ayagan,ayagao,ayagatayn dugang,ayagatayn dugang hiid,ayagatuin dugang khid,ayagawa,ayagba,ayagfe,ayagha,ayaghchi,ayaghel,ayagibuyuk,ayago,ayagoua,ayagoz,ayagtuin dugang,ayagua,ayagualo,ayagur,ayagushkino,ayaguz,ayaguzki,ayaguzski,ayaguzskiy,ayah,ayahag,ayahat,ayahgadeng,ayahohou kope,ayahohoue,ayahuacas,ayahuala,ayahualco,ayahualuco,ayahualulco,ayahuaque,ayahuasi,ayahuasi pata,ayahuay,ayahuaycu,ayaicha,ayaida,ayaina,ayaingge,ayaingzu,ayairini,ayaisi,ayajangga,ayajapa,ayajasco,ayajaso,ayajilli,ayajin,ayajinni,ayajuy,ayak,ayak umm rakubah,ayak umm rukuba,ayak-aul,ayak-aulye,ayaka,ayakasang,ayakchi,ayakebir,ayakgedik,ayakguzhumdy,ayakh-karvend,ayakhakali,ayakhchi,ayakhkali,ayakhta,ayaki,ayakit,ayakkif,ayakkol,ayakkudi,ayakkuduk,ayakkum,ayakli,ayaklialan,ayaklicaoluk,ayaklikiri,ayakmaldybay,ayakmang,ayakmoildy,ayakomaso,ayakorama,ayakoromo,ayakoz,ayakpa,ayakro,ayaks,ayakudi,ayakunle,ayal,ayala,ayala sugar central,ayalakab,ayalan,ayalca,ayalcheri,ayale,ayali,ayalizimakhi,ayallacta,ayallalaoun,ayalo,ayalodi,ayalos,ayaltah,ayalu,ayalutskiy,ayam,ayama,ayama-agana,ayama-ekede,ayama-ibotokpon,ayamabele,ayamabene,ayamachay,ayamachi,ayamadjikofe,ayamagble,ayamakhi,ayamalas,ayaman,ayamang,ayamarca,ayamaru,ayamasa,ayamassa,ayamaya,ayamba,ayambare,ayambe,ayambi,ayambla,ayamboga,ayame,ayamfaga,ayamiken,ayamonte,ayampampa,ayampe,ayamu,ayamvrasiyo,ayamvrosio,ayan,ayan yaryakh,ayan-auly,ayan-kyuyel,ayan-yuryakh,ayana,ayanapur,ayanavaram,ayanboko,ayanca,ayancay,ayancik,ayancocha,ayand,ayanda,ayandete,ayandihona,ayandronikas,ayandroniko,ayandza,ayane,ayanesala,ayanfure,ayanfuri,ayang,ayangahua,ayangan,ayangaty,ayangba,ayangdong,ayange,ayangni,ayangor,ayangue,ayanhuasi,ayani,ayania,ayanis,ayanka,ayankasin,ayankunle,ayanlar,ayanmasa,ayanme,ayanna,ayanni,ayanoglu,ayanpur,ayanque,ayans,ayanske dzherelo,ayantade,ayantar,ayantata,ayantokun,ayantola,ayantronigo,ayantroniko,ayanu,ayanur,ayanyemi,ayanyuryakhskiy,ayanza,ayao-iyao,ayaoan,ayaoiyao,ayaoke,ayaorca,ayaorjo,ayaotsa,ayaouane,ayaouo kope,ayaouz,ayap,ayap-ngo,ayapa,ayapa uman,ayapal,ayapamba,ayapampa,ayapana,ayapango,ayapaso,ayapata,ayapel,ayapua,ayapur,ayaq qarvand,ayar,ayar al kadhim,ayar al kazi\302\247im,ayar manco,ayara,ayara agu,ayaraba,ayaracra,ayarade,ayaranga,ayarara,ayarengkam,ayarhuayque,ayari,ayarkheyl,ayarlu,ayarmaca,ayaroy,ayarpata,ayarumi,ayarza,ayas,ayas jan,ayasa,ayasan,ayasasi,ayase,ayasergi,ayash,ayashi,ayasi,ayask,ayaslar,ayaslioglu,ayaslu,ayaso,ayasoluk,ayason,ayaspasa,ayasse grauson,ayasse sofi,ayastad,ayastafanos,ayasturkmenli,ayat,ayat-sur-sioule,ayata,ayatanas,ayatanasiyo,ayatodori,ayatokova,ayatollah montazeri,ayatollah montazeri-ye bala,ayatollah montazeri-ye pain,ayatollah taleqani,ayatsi,ayatsimakhi,ayatskiy,ayatskoye,ayatsuri,ayattur,ayau,ayauca,ayaun,ayaurcu,ayaushi,ayaushiuwaki,ayausi,ayautla,ayava,ayavacas,ayavaquita,ayavari,ayavene,ayaverine,ayavi,ayaviri,ayavirini,ayavoual,ayavskaya,ayavu,ayaw,ayawala,ayawane,ayawola,ayawura,ayay,ayaya,ayayan,ayayi,ayayo chahoua,ayayuf,ayaz,ayaz galu,ayaz jan,ayaz kandi,ayaz khan,ayaz khan banda,ayaz khel,ayazabad,ayazabad-e kasian,ayazaga,ayazaza,ayazca,ayazgulova,ayazi,ayazina,ayazini,ayazkoy,ayazlar,ayazma,ayazmacukur,ayazmakoy,ayazmand,ayazmant,ayazmayeni,ayazmayenikoy,ayazment,ayazmi,ayazo,ayazoren,ayazpinar,ayazwala,ayba,aybad-kazgan,aybak,aybaknur,aybala,aybalam,aybar,aybary,aybas,aybasan,aybasanly,aybash,aybasheva,aybashevo,aybashi,aybasi,aybasti,aybat,aybatkazgan,aybatova,aybazli,aybechi,aybga,aybichi,aybifan,aybozym,aybulak,aybulat,aybulyak,aybyz,aycallate,aycara,aycata,aychahuasi,aychamayo,aychapucro,aychi,aychullo,aychuyo,ayci,aycliffe,ayco,aycock,aycock crossing,aycock mill,aycota,aycotal,aycronmarca,aycukuru,ayculaca,ayd baca,ayd bachah,ayd bachcheh,ayda,aydabol,aydabul,aydabulskiy,aydagarly,aydagulovo,aydakayeva,aydakayevo,aydakol,aydalqasi,aydamir,aydanil,aydanlii,aydanliy,aydansay,aydar,aydara,aydara koara,aydara kwara,aydarak,aydarali,aydarbak,aydarbek,aydarga,aydarkhan,aydarkhen,aydarli,aydarlinskiy,aydarlinskoye,aydarlo,aydarly,aydarlykol,aydarnikolayevka,aydarovo,aydas,aydat,aydawr,ayda\302\261n,ayda\302\261nkoy,aydbacha,aydelotte,aydemet,aydemir,ayden,aydenlu,ayder,aydere,aydi,aydi-kend,aydie,aydimitri,aydimitrino,aydimitriyano,aydin,aydin oglu,aydin-kishlyag,aydinalan,aydinbahce,aydinbulaq,aydinca,aydincik,aydinevo,aydinghu,aydinghu xiang,aydingka\302\277l,aydingkol,aydingun,aydinisik,aydinkand,aydinkavak,aydinkaya,aydinkent,aydinkisla,aydinkisla koyu,aydinkonak,aydinkoy,aydinlar,aydinli,aydinlik,aydinlikevler,aydinlilar,aydinlu,aydinocak,aydinoglu,aydinoluk,aydinovo,aydinpinar,aydinqislaq,aydinseyh,aydinsih,aydinsofu,aydinsu,aydintepe,aydinyaka,aydinyayla,aydinyurdu,aydirly,aydisheh,aydius,aydkunen,aydlett,aydogan,aydogan yaylasi,aydogdu,aydoglar,aydogmus,aydoilles,aydokhan,aydolin,aydomush,aydon,aydora,aydos,aydozi,aydreyenik,aydu,aydu khan,ayduan-chabya,aydughmush,aydul,aydullar,aydun,aydun bey,aydun koy,aydunlar,aydunymme,aydynbulag,aydynbulak,aydynkend,aydynkyshlak,aydynkyshlakh,aydynlar,aydyrlinsk,aydyrlinskiy,aydzhi dere,aye,aye ekan,aye ekiti,aye kouadjo,aye noumad,aye noumane,aye yapo,ayease,ayebabiri,ayebachi,ayebe,ayeblu,ayebo,ayebukuru,ayebuoso,ayeching,ayecta,ayed,ayeda,ayedan,ayedena,ayedi,ayedotyevka,ayeduase,ayeduasi,ayee,ayeenin,ayeeshio,ayegan,ayeghe,ayeghele-mindock,ayeghening,ayegourou,ayeguening,ayeguening ii,ayegui,ayegyaungzok,ayeh,ayehmeihsu,ayehualulco,ayeie,ayeim,ayeindama,ayeinkritovka,ayeka,ayekolelo,ayekoshe,ayekpada,ayekpore,ayeldo,ayeldu,ayele,ayele gebeya,ayelet hashahar,ayeli,ayelibia,ayelo de malferit,ayelo de rugat,ayelou,ayem,ayema,ayeme,ayen,ayenabour,ayencourt,ayene,ayene-enduguensan,ayeneh,ayeneh varzan,ayenehlu,ayenehvand,ayenemadi,ayenesala,ayeneux,ayenfuri,ayengre,ayengrebo,ayengueni,ayenguening,ayeni,ayenigianran,ayenkyerem,ayenngre,ayenoua,ayenouan,ayensua fufuo,ayensua kawkaw,ayensua kokoo,ayensuadze,ayensuadzi,ayensuaka,ayensuako,ayensuhu,ayensuso,ayent,ayenwolde,ayeoni,ayeopian,ayepifani,ayepiktitos,ayer,ayer baloi,ayer bemban,ayer berbagi,ayer bintang,ayer hangat,ayer hanyir,ayer hitam,ayer hitam village,ayer itam,ayer jerneh,ayer keliling,ayer kelining,ayer kering,ayer keroh,ayer kumpai,ayer kuning,ayer kuning selatan,ayer kuning south,ayer mawang,ayer merbau,ayer molek,ayer pa abas,ayer panas,ayer pasir,ayer pechah,ayer puteh,ayer qayeh,ayer raja new town,ayer rajah,ayer renak,ayer resam simpang gading,ayer salak,ayer sejok,ayer tawar,ayer tawar new village,ayer terjun new village,ayer uma,ayera,ayeranos,ayerbangis,ayerbe,ayerbe de broto,ayerd,ayere,ayerede,ayeri-ubiane,ayerkhan,ayerlo,ayermadidi,ayero,ayerom,ayeron assi,ayeron ayeron,ayerpuluh,ayerpur,ayers,ayers cliff,ayers crossroads,ayers hill,ayers ranch colony,ayers village,ayersville,ayerterang,ayertyo,ayesa,ayeseigbene,ayesh bon,ayeshbon-e `olya,ayeshbon-e sofla,ayeshin,ayeshina,ayeshka,ayesk,ayeso,ayet el arbi ben lemhidi,ayet mustapha ben bou laid,ayetedjoun,ayetoro,ayette,ayetthema,ayetugbene,ayev,ayev tretiy,ayevstatios,ayfar,ayfelah,ayfodi,ayfoti,ayfugan tatarskiy,ayga,aygabak,aygabats,aygachi,aygal,aygal,aygan-mursalyayevo,aygancha,aygansha,aygashen,aygatayn dugang,aygatuin dugang,aygavan,aygay-mursalyay,aygeat,aygedzor,aygehat,aygehovit,aygek,aygeovit,aygepar,aygepat,aygerlich,aygeshat,aygestan,aygezard,ayghem-saint-pierre,ayghyz,aygi,aygir gol,aygirgolu,aygirgolukoyu,aygirlar,aygirlarbuyuk,aygirolu,aygiryal,aygishi,aygordu,aygormez,aygour,ayguade-ceinturon,ayguafreda,ayguatebia,ayguebelle,ayguemorte,ayguemorte-les-graves,ayguesvives,aygulevo,aygulevo pervoye,aygun,aygunlu,aygur,aygur dere,aygur koy,aygurskiy,aygut,aygyr-dzhal,aygyr-zhal,aygyrolyu,aygyrshagyl,aygyryal,aygyrzhal,aygyunli,aygyz,ayhah,ayhan,ayhankoy,ayhanlar,ayharida,ayharita,ayhat,ayherre,ayhuani,ayhuani hacienda,ayhuarana,ayi,ayi kope,ayi maman,ayia,ayia agathi,ayia aikaterini,ayia anastasia,ayia anna,ayia eleni,ayia eleousa,ayia evfimia,ayia evthimia,ayia fila,ayia fothia,ayia fotia,ayia fotini,ayia galini,ayia grosh,ayia irini,ayia irmi,ayia itini,ayia kebir,ayia kiriaki,ayia marina,ayia marina kelokedhara,ayia marina khrysokhou,ayia marina skyllouras,ayia marina xyliatos,ayia marina xyliatou,ayia marinoudha,ayia marinuda,ayia mavra,ayia napa,ayia paraskeni,ayia paraskevi,ayia pelayia,ayia phyla,ayia roumeli,ayia semni,ayia sofia,ayia sotira,ayia stevenikon,ayia thekla,ayia thekli,ayia tria,ayia trias,ayia trias gounari,ayia trias gounnari,ayia varvara,ayia vlakherna,ayiado,ayiai paraskiai,ayiakrom,ayiannaki,ayiannakis,ayiannakos,ayias,ayias filakai,ayias vavatsinias,ayiase,ayiasi,ayiasissios,ayiasma,ayiasos,ayiaspei,ayiassios,ayiassos,ayib beinkib,ayibasar,ayibeleni,ayibobiri,ayici,ayidina,ayido,ayidopwe,ayiebam,ayiedero,ayiem,ayies paraskies,ayigbiri,ayigolu,ayigoro,ayii eliophoti,ayii trimidyes,ayii trimithias,ayii vavatsinia,ayii vavatsinias,ayija,ayikai doblo,ayikel,ayikuma,ayikyn,ayila,ayiladi,ayilekan,ayili,ayilia,ayiloum,ayilya,ayim,ayima,ayimaya,ayimta,ayin,ayina,ayinabrim,ayinah qal`ah,ayinaso,ayinbaran,ayinbelluni,ayinbelut,ayinberran,ayindal,ayindar,ayinde,ayinderm,ayindevil,aying,ayingala,ayingavis,ayingbe,ayinhir,ayiniyankankani alavakkai,ayinkaf,ayinkar,ayinkasir,ayinkasyr,ayinkef,ayinkerem,ayinko,ayinla,ayinlo,ayinmismis,ayinmulk,ayinnakos,ayinneji,ayinras,ayinres,ayinrin,ayinrumi,ayinser,ayinsuleyman,ayintap,ayintapli,ayinteri,ayinvan,ayinvan harap,ayinvert,ayinwafe,ayinwofi,ayioba,ayiofillon,ayiofillos,ayioi,ayioi anaryiroi,ayioi apostoloi,ayioi asomatoi,ayioi dheka,ayioi dhouli,ayioi dhouloi,ayioi pandes,ayioi saranda,ayioi taxiarkhai,ayioi theodhoroi,ayiokhori,ayiokhorion,ayion gala,ayion pneuma,ayion pnevma,ayion yeorgoudhi,ayion yeorgoudi,ayioneri,ayionerion,ayionori,ayionorion,ayiopiyi,ayioryitika,ayios,ayios adhrianos,ayios akakios,ayios akhillios,ayios alexandhros,ayios alexandros,ayios amvrosios,ayios andhreas,ayios andonias,ayios andonios,ayios andreas,ayios andronikos,ayios antonios,ayios arsenios,ayios artemios,ayios astratigos,ayios atanasios,ayios athanasios,ayios dhikaios,ayios dhimitrianos,ayios dhimitrio,ayios dhimitrios,ayios dhometios,ayios dhonatos,ayios dimitrios,ayios dometios,ayios efstathios,ayios elias,ayios elisaios,ayios elissaios,ayios epiktitos,ayios epiphanios,ayios epiphanios orinis,ayios epiphanios soleas,ayios ermolaos,ayios evrosis,ayios evstathios,ayios evstratios,ayios floros,ayios fokas,ayios fotios,ayios iakovos,ayios ilias,ayios ioannis,ayios ioannis kareas,ayios ioannis maloundas,ayios ioannis prodhromos,ayios ioannis theologos,ayios isidhoros,ayios kharalambos,ayios khariton,ayios khristoforos,ayios kiprianos,ayios kirikos,ayios kirillos,ayios kirilos,ayios kirix,ayios konstandinos,ayios konstantinos,ayios kosmas,ayios lavrendios,ayios leon,ayios loukas,ayios mamas,ayios mammas,ayios markos,ayios matthaios,ayios meletios,ayios memnon,ayios merkourios,ayios merkourlos,ayios minas,ayios miron,ayios nestor,ayios nikitas,ayios nikolaos,ayios nikolaos lefkas,ayios nikolaos niras,ayios nikolaos varvaras,ayios nikolaos voion,ayios nikon,ayios pandeleimon,ayios pavlos,ayios petros,ayios photios,ayios polikarpos,ayios prodhromos,ayios prokopios,ayios rokkos,ayios rokos,ayios sarandos,ayios serafim,ayios sergios,ayios seryios,ayios seryos,ayios sillas,ayios simeon,ayios sostis,ayios sozomenos,ayios spiridhon,ayios stefanos,ayios stratis,ayios symeon,ayios teodoros,ayios theodhoroi,ayios theodhoros,ayios theodhoros soleas,ayios theodhoros tillirias,ayios theodoros,ayios therapon,ayios thomas,ayios tihonos,ayios tykhonas,ayios vartholomaios,ayios vasilios,ayios vasilios nevs,ayios vasilis,ayios vavatsinia,ayios vissarion,ayios vlasios,ayios yedheon,ayios yeorgios,ayios yeoryios,ayios yeoryios kafkalou,ayios yeoryios keratsiniou,ayios yeoryios lefkas,ayios yeoryios nilias,ayios yeoryios riou,ayios yeoryios sikousis,ayios yeoryios spatharikou,ayios yermanos,ayios yiannis,ayios yiannis theologos,ayiot,ayious,ayiovlasitika,ayiovlastitika,ayip khel,ayipey,ayirala,ayirebi,ayirech,ayirehuma,ayirini,ayirmola,ayiroyat,ayisa,ayisa gwegwe,ayisana,ayiseri,ayisigi,ayiss,ayistat,ayisu,ayit,ayit gora,ayitang,ayitay,ayitdere,ayitdere koyu,ayitepa,ayitey,ayithona,ayithoze-ywa,ayitohwinu,ayittiyamalai,ayiveliler,ayivi,ayivi adjavon,ayivu,ayiwasi,ayiwawa,ayiwela,ayiyatigewewa,ayjan,aykolba,aykadah,aykadzor,aykal,aykamar,aykamessal,aykaran,aykarida,aykasar,aykashen,aykaul,aykaul vakuf,aykavan,ayka\302\277l,aykel,aykent,aykeyevo,aykhal,aykhel,aykhemcal,aykhemchal,aykhimchal,aykhimchal,aykino,aykirici,aykirikci,aykol,aykol,aykola,aykole,aykonstandino,aykurus,aykut,aykutkoy,aykynkum,ayl,ayl safi,ayl-e ghulam,ayla,ayla dun,ayla patakata,aylac,aylagas,aylah,aylah taw,aylakah-e ajar,aylaki-dzhangalak,aylaki-goldzhatu,aylaki-kalanbala,aylaki-kalanbalayi-kutandar,aylaki-peshkyshlak,aylaki-vand,aylan,aylaq-e chehel gazi,aylaq-e gherjaw,aylaq-e samandan,aylaq-e sangi,aylaq-i-cehil gazi,aylaq-i-sangi,aylaqe gholjatu,aylaqe jangalak,aylaqe kalanbala,aylaqe peshqeshlaq,aylaqe pesqeslaq,aylaqe wand,aylaqe-kalanbala-i-qotandar,aylar,aylbayan,ayle,ayledin,aylei,aylenja,aylesbeare,aylesbury,aylesbury vale,aylesby,aylesford,aylestone,aylesworth,aylet,aylett,aylett mill,ayli,aylidere mahallesi,aylidun,aylinka,aylino,aylioglu,aylion bajo,ayliyadichenai,ayllalaoun,ayllan,ayllaque,ayllon,ayllucucho,aylmer,aylmerton,aylo,aylor,aylsdorf,aylsham,aylu,ayluk,aylyagush,aylyanma,ayma,aymagambetov,aymagambetovo,aymagay hiid,aymagay khid,aymagombetovo,aymaguba,aymak,aymakhallya,aymaki,aymal,aymalabek,aymalk,aymama,aymamshak,ayman,aymangala,aymani,aymanki,aymankuyu,aymaq,aymara,aymarina,aymarinuda,aymas,aymaumakhi,aymaville,aymavilles,aymaya,aymazimakhi,aymaztekke,aymbellu,aymbet,aymbol,aymeken,aymenkin,aymeries,aymeroua,aymestrey,aymetir,aymetovo,aymett town,aymind,aymindow,ayminova,aymiri,aymmyulk,aymo-guba,aymonchyay,aymons,aymores,aymtan,aymyr,aymzhan,ayn,ayn al faidah,ayn al-asad,ayn amoun,ayn az zan,ayn ibn fuhayd,ayn qal`ah,ayn shaykh,aynya,aynyar,ayn-tlat,ayn-ul-madzhar,ayna,ayna beg bay,ayna qala,ayna-bulak,ayna-bulakskaya,ayna-bulan,ayna-kul,ayna-tash,aynabegbay,aynabek,aynabulak,aynabulakskiy,aynabulaq,aynac,aynadei,aynadzhek,aynadzor,aynafar,aynah beg bay,aynahchah,aynak,aynakala,aynakofo,aynali,aynalihoca,aynalikale,aynalikkale koyu,aynamse,aynapa,aynar,aynas,aynasheykh,aynatali,aynatas,aynato,aynayn,aynazhi,aynde,ayndevi,ayne,ayneh beyg bay,ayneh deh,ayneh qal`eh,aynehir,aynehvar,aynes,ayneta,ayneto,aynetu,aynhir,aynho,ayni,ayni-khodzhaayni,ayni-khojayni,aynifar,aynihalil,aynik,aynikab,aynikabmakhi,aynikale,aynikasir,aynikola,ayniomer,aynires,aynisic,aynisuleyman,ayniye,aynka-i hama shasuwar,aynka-i sa`id-a rash,aynmalk,aynmara,aynnkyaf,ayno kalay,aynokheyl,aynor,aynos,aynoumal,aynova gora,aynskoye,aynsley,aynsuc,aynsuleyman,ayntap,aynuddinkot,aynukalay,aynurbimakhi,aynuskoye,aynusumok,ayny,aynyadi,aynyk,aynyomer,aynysich,aynysyuleyman,aynyusumok,aynzingen,ayo,ayo ajegunle,ayo ayo,ayo el chico,ayo el grande,ayo-isule,ayoamlaya,ayobo,ayobon,ayock,ayocote,ayocuapan,ayod,ayoda,ayodar,ayodaung,ayodele,ayodhya,ayodo,ayoeroebe,ayogan,ayogon,ayoguri,ayoha,ayoja,ayojapa,ayokorama,ayokua brofoyedru,ayokwa brofoyeduru,ayol,ayola,ayolas,ayolave,ayole,ayoli,ayolita,ayoluengo,ayom,ayoma,ayomaya,ayome,ayometitla,ayometla,ayomi,ayomso,ayon,ayon-dao,ayones,ayong,ayongkura,ayongo,ayongon,ayongtoke,ayoni,ayonukope,ayoo,ayoo de vidriales,ayoosu,ayoque,ayoque hacienda,ayoquezco,ayoquezco de aldama,ayor,ayora,ayorai,ayore,ayorgi,ayorinde,ayorou,ayorou goungo kore,ayos,ayos lomboy,ayot,ayot saint lawrence,ayot saint peter,ayotita,ayotitlan,ayotla,ayotlicha,ayotoko,ayotopal,ayotoxco,ayotoxco de guerrero,ayotoxoc,ayotoxtla,ayotusco,ayotuxtla,ayotzinapa,ayotzingo,ayotzintepec,ayou,ayou oua,ayouba,ayoubakolon,ayouch,ayoufis,ayougbene,ayoul,ayouloum,ayouloumi,ayouluoguo,ayoum,ayoum er raazlane,ayoumba,ayoumbi,ayoume,ayoun,ayoun arrhouch,ayoun el kasab,ayoun goupou,ayoune,ayouri,ayouthia,ayowaho number 1,ayowahu,ayowangen,ayoxustla,ayoxuxtla,ayoyo,ayozlu,aypak,ayparahui,ayparahul,ayparavi,aypata,aypihtito,aypin,aypin r.,aypolovo,aypolovy,ayqal`ehsi,ayqer chaman-e `olya,ayqer chaman-e do,ayqer chaman-e sofla,ayqer chaman-e yek,ayque,ayquina,ayr,ayr county,ayr junction,ayra,ayrabamba,ayrach,ayradural,ayrag,ayrakah,ayrampuni,ayran,ayran darrehsi,ayranca,ayranchi,ayranci,ayranciderbent,ayrancikomlari,ayrancilar,ayrancl,ayranli,ayranlik,ayranpinar,ayrao,ayrapaa,ayras,ayratam,ayravank,ayrech,ayrenay,ayrene,ayrens,ayrenyats,ayres,ayres city,ayres estates,ayres lane estates,ayresville,ayrganah,ayrgi tal,ayrhof,ayri,ayri bujaq,ayri bujaq beyglu,ayria trias,ayribujaq,ayrica,ayridam,ayridebal,ayridzha,ayrihuanca,ayrilik,ayrim,ayrimaki,ayrin tank,ayrinmaki,ayripute,ayritala,ayritam,ayritan,ayritepe,ayrivan,ayrivang,ayrk,ayro,ayrocollo,ayron,ayroobley,ayror,ayros,ayros-arbouix,ayrovo,ayrshire,ayrum,ayryk,ayryq,ayrysu,ayrytam,ayryumskiy,aysaita,aysagan,aysakli,aysalakak,aysapay,aysarinskoye,aysary,aysasi,ayse,ayseahmet,aysebaci,aysederos,aysehoca,ayseki,ayseku,aysen,aysenay,ayseoglu,aysepinar,ayserez,aysergi,aysezomeno,aysgarth,aysha,aysha-bibi,ayshacancha,ayshakhku,ayshan,ayshan at tarm,ayshan langar,ayshan qishlaq,ayshan top,ayshan-e darah-ye bagh,ayshanan,aysheakhmet,ayshia,ayshinsiri,ayshiyaz,aysidero,aysilden,aysimyo,aysino,ayskaya,ayskaya gruppa,ayskoski,ayskoye,ayskula,ayson,aysori dvin,aysozomeno,ayssenes,ayssenes-la-bacaresse,aystetten,aysto,aysuan,aysugan,aysun,aysuure,aysylden,ayt ali,ayt boukhzir,ayt hamou,ayt ibourk,ayt merzoug,ayt oussayh,ayt youb,aytala,ayt-pay,aytac,aytaden,aytag,aytak,aytakh,aytakov,aytaktamak,aytala,aytalah,aytalamba,aytalan,aytalon,aytamaz,aytamgaly,aytansyk,aytar,aytas,aytat,aytbay,ayteke bi,aytepe,aytepekoy,ayterap,ayterapon,aytgay,ayti-mokhk,aytin,aytin-moxk,aytinskiy,aytishev,aytkali,aytkaly,aytkhan,aytkhankutan,aytkul,aytkulovo,aytmaambetov,aytmambeteva,aytmembetovo,aytodor,ayton,aytona,aytony,aytos,aytotoro,aytotro,aytpayka,aytpayskiy,aytqalloyn rea\302\277a-the,aytre,aytriada,aytrimitya,aytrimityas,aytsyuntsy,ayttakumpu,aytu,aytuar,aytuarka,aytugan,aytugan nemetskiy,aytugan tatarskiy,aytugan-durasovo,aytuganovo,aytuma,aytumak,aytuwayran fawqani,aytuwayran tahtani,aytuy,ayty-mokhk,aytyak-tamak,aytybay,aytym,aytymbet,ayu,ayu-kudergan,ayua,ayuafutu,ayuanah,ayub,ayub bazar,ayub chanar,ayub goth,ayub kalay,ayub kelay,ayub khan,ayub khan bhurgari,ayub khan kalla,ayub khan kalle,ayub khan kelay,ayub khel,ayub lines,ayub qattal,ayubabad,ayubi,ayubia colony,ayubkalay,ayubkhan kalay,ayubkhankalay,ayubkhel,ayubkheyl,ayubnagar,ayubo,ayubpur,ayubuku,ayubzai,ayubzay,ayubzi,ayubzi kalay,ayucan,ayuchevo,ayucinapa,ayud,ayuda,ayudante,ayudhya,ayudomari,ayudugia,ayue,ayuela,ayuelas,ayuen,ayuet,ayugan,ayugis,ayugok,ayugon,ayuguila,ayui,ayuja,ayuk ndip,ayukaba,ayukai,ayukasa,ayukawa,ayukawacho,ayukawahama,ayukhanovo,ayukov,ayul singh,ayula,ayulehai,ayulhai,ayulibo,ayulin,ayully,ayulong,ayum,ayumaque,ayumaso,ayumba,ayumbiraiti,ayumbirate,ayumen,ayumi,ayumso,ayun,ayun kara,ayuncora,ayundeh,ayune,ayung,ayunggoinba,ayungon,ayunos,ayunzok,ayuotu,ayupova,ayupovka,ayupovo,ayuquila,ayuquililla,ayuquita,ayuquitan,ayuquitan viejo,ayuquito,ayur,ayurcu,ayurcu de pena,ayurni,ayurni-ye viznah,ayuruoca,ayusan,ayusan norte,ayusan sur,ayusazova,ayusazovo,ayush,ayuta,ayutay,ayutepeque,ayuthaya,ayuthia,ayuthya,ayuti,ayutia,ayutica,ayutilla,ayutinskiy,ayutla,ayutla de los libres,ayutovo,ayutthaya,ayutthia,ayutuxtepegue,ayutuxtepeque,ayutya,ayuwa,ayuwandama,ayuwawa,ayuweni,ayuwu,ayuy,ayuyan,ayuyoc,ayva,ayvaalani,ayvaca,ayvach,ayvacik,ayvacukuru,ayvadere,ayvaduzu,ayvadzh,ayvadzhik,ayvadzhik dere,ayvagedigi,ayvaj,ayvakoy,ayvala,ayvalar,ayvali,ayvalibag,ayvalica,ayvalik,ayvalik maaihlamur,ayvalipinar,ayvalisokagi,ayvan,ayvanak,ayvarvara,ayvaryara,ayvasil,ayvasul,ayvat,ayvat koy,ayvatlar,ayvatli,ayvavacinya,ayvayeri,ayvaz,ayvazhaci,ayvazovo,ayvazovskaya,ayvazovske,ayvazovskoe,ayvazovskoye,ayvazpinar,ayvazpinari,ayvazulya,ayveda,ayverdi,ayveri,ayvi kola,ayvora`,ayvove,ayvovoye,ayvozh,aywaille,aywera,aywetu,ayxin siri,ayxin siri zhen,ayxinsiri,ayy-taybyt,ayyab arain,ayyad,ayyal ed daash,ayyampalaiyam,ayyampettai,ayyanapuram,ayyanni,ayyanthopu,ayybazar,ayyelet hashahar,ayyildiz,ayykyn,ayyoogh,ayyorgi,ayyorgi kefkalu,ayyorguyi,ayyr,ayyrkum,ayyrqum,ayyrtam,ayyrtas,ayyrtau,ayyrtauskoye,ayyub,ayyub kalay,ayyub khan kalay,ayyub khel,ayyub kheyl,ayyubabad,ayyubbali,ayyubzay,ayyubzi kalay,ayyur,ayzac,ayzac-ost,ayzacollo,ayzagayzy,ayzama,ayzan-kouamikro,ayzavat,ayze,ayzhpuri,ayzieu,ayzkalne,ayzkalnyshi,ayzklaany,ayzkraukle,ayzoan,ayzosh amora,ayzpurve,ayzpute,ayzstrautniyeki,ayzupe,ayzupenka,ayzupiyeshi,ayzviki,ayzy-gayzy,ayzy-tayzy,az,az daro,az ghar,az murgha,az murgo,az nefas,az pish,az za`ain,az za`atirah,az za`awir as sufla,az za`ayim,az za`ayin,az za`faran,az za`faranah,az za`farani,az za`ir,az za`liyah,az zababida,az zababidah,az zababide,az zabadah,az zabadan,az zabadani,az zabirah,az zabu,az zadiyah,az zafin,az zafiq,az zafir,az zagazig,az zaghfah,az zahari,az zahariyah,az zahayrah,az zahir,az zahiriyah,az zahr,az zahra,az zahra al gharbiyah,az zahra ash sharqiyah,az zahrah,az zahran,az zahriyah at tahtaniyah,az zahruni,az zahwaniyah,az zahwiyin,az zajam,az zajji,az zakah,az zakrur,az zalaf,az zalal,az zallaq,az zallaqiyat,az zallu,az zalqa,az zamalik,az zamalyab,az zamamiqah,az zammar,az zamruniyah,az zanazil,az zandi,az zani`ah,az zanjabar,az zankalun,az zanqur,az zantam,az zaqanah,az zaqaziq,az zaraib,az zarabi,az zarah,az zaramidin,az zarariyah,az zarat,az zarazir,az zarbah,az zarfiyah,az zargah,az zaribah,az zaririyah,az zarkan,az zarqa,az zarqa,az zarqan,az zarqat,az zarqiyah,az zarra`ah,az zarruq,az zarub,az zarzamun,az zarzuriyah,az zaur,az zaw`,az zawaidah,az zawa`i,az zawad,az zawadim,az zawak al gharbiyah,az zawak ash sharqiyah,az zawamil,az zawarah,az zawarat,az zawarib,az zawawin,az zawbah,az zawf,az zawilah,az zawiyah,az zawiyah al bayda,az zawiyah al faidiyah,az zawiyah al gherbiyah,az zawiyah al hamra,az zawiyah al khadra,az zawiyyah,az zawr,az zayarah,az zaybaq,az zaydan,az zaydiyah,az zaydiyah wa zawiyat nabit,az zayfa,az zayla`iah,az zayla`iyah,az zaymah,az zayn,az zaynabiyah,az zaynah,az zayniyah bahri,az zayniyah qibli,az zayt,az zaytarah,az zaytiyah,az zaytun,az zaytunah,az zaytuniyah,az zayyadiyah,az zayyatin,az zayyatin al qibliyah,az zedab,az zerahy,az zerane,az zhit,az zib,az zibar,az zibaydah,az zibdah,az zieb,az zighan,az zihayriyah,az zikri,az zilah,az zilal,az zilfi,az zimaliyah,az zimmah,az zimrah,az zinan,az zinj,az zintan,az zira`,az zira`iyah,az zirab,az zirah,az zirbi,az zirkan,az ziyad,az ziyadiyah,az ziyarah,az ziznaqarah,az zohr,az zor,az zraoula,az zrariyah,az zu`,az zu`aqah,az zu`ayri,az zu`aytirah,az zu`aytiriyah,az zu`rurah,az zu`ruriyah,az zubair,az zubaydiyah,az zubayr,az zubayriyah,az zughayn,az zughbah,az zuhair,az zuharah,az zuhayr,az zuhayri wa al `aw,az zuhayriyah,az zuhrah,az zujayriyah,az zuk ash sharqiyah,az zulayyim,az zulfi,az zullayqat,az zuluzal,az zumagi,az zumah,az zumamiyah,az zumayqah,az zumlah,az zunayyah,az zunud,az zuq,az zurah,az zurayq,az zurayqa,az zurayqi,az zurbi,az zureqa,az zurqa,az zurqan,az zurra`,az zutt,az zuwarah,az zuwarin,az zuwayb,az zuwaydiyah,az zuwayrah,az zuwaytinah,az zuwayyah,az zuwayyik,azyal,aza,aza bi mahalleh,aza gaz,aza khel,aza khel bala,aza khel payan,azaage,azab,azab al makhadah,azaba,azabache,azabal,azabali,azabamba,azaban,azabane,azabari,azabbasili,azabenyon,azabeyg qeshlaq,azabi,azabo,azabu,azabu-fujimicho,azabuxo,azaceta,azaclau,azacualpa,azacualpa de gualcho,azacualpa de yamaranguila,azacualpa grande,azacualpa gualcho,azacualpa montana,azacualpa vieja,azacualpa viejo,azacualpia de gualcho,azacualpia de joco,azacualpilla,azacualpita,azad,azad bakht,azad bakht-e korreh pa-ye owliya,azad banda,azad bon,azad bon mahalleh,azad dar,azad gaz,azad goleh,azad kala,azad khan,azad khan khel,azad khan kheyl,azad khel,azad kheyl,azad kin,azad koti,azad mahalleh,azad manjir,azad mard,azad marzabad,azad mehr,azad morad,azad qaraqoyunlu,azad shahr,azad sofla,azad tangeh,azad tongeh,azad var,azad veys-e `olya,azad veys-e bala,azad veys-e pain,azad veys-e sofla,azad-e bala,azad-e pain,azad-e sofla,azad-e-`olya,azad-tange,azadabad,azadabad-e pir dusti,azadakend,azadan,azadbar,azadbukhara,azade,azadegan,azadeh,azademun,azadhar,azadi,azadinos,azadiyeh,azadkala,azadkand,azadkar,azadkarakoyunlu,azadkend,azadkhani,azadkhankhel,azadkhankheyl,azadkhankheyl,azadkhel,azadkheyl,azadkheyl,azadkhu,azadlu,azadmoon,azadmun,azadogly,azadon,azadpur,azadrud,azadshahr,azadvar,azadzai,azaf,azafa,azafi,azafi teme,azafran,azafranal,azafranes,azag,azaga,azagai,azagala,azagar,azagawa,azagba,azagba camp,azagba-ogwashi,azagbene,azagbene-zion,azagetsu,azaghar,azaghar nirs,azago kouadio,azagounogon,azagra,azagua,azaguie,azaguie aoua,azaguie blida,azaguie makouguie,azaguie mbrome,azaguie-ahoua,azaguie-dianle,azagulovo,azagzel,azahambe,azahan,azaharillo,azaharrillo,azahi,azaho,azai,azaib,azaiba,azaila,azaina,azait,azaiza,azajo,azaka,azakagu,azakes,azakhachi,azakhvaran,azakhvoran,azakhwaran,azakli,azaklihoca,azako,azakope,azakour,azakpe,azakpert,azakpur,azakuin,azakuiri,azal,azal koy,azal khel,azala,azalak,azalakovo,azalamane,azalea,azalea acres,azalea gardens,azalea mobile home park,azalea park,azalea plaza mobile home park,azalea terrace,azalea trailer park,azali,azalia,azalingen,azalion,azalmir kot,azaln,azam,azam basti,azam chhina,azam estala,azam hiraj,azam khan,azam khan shahr,azam khanwala,azam khanwala khu,azam laghari,azam pael,azam qala,azam shah,azam shah kili,azama,azamabad,azamabao,azamakoshi,azamalu,azamana,azamat,azamat-jurt,azamat-yurt,azamate,azamatovo,azamay,azamayevo,azamazu,azambalava,azambar,azambi,azambuja,azambujeira,azame,azamgarh,azamkala,azamkalay,azamnagar,azamo,azampay,azampur,azampur jadid,azamu,azamui,azamval,azamwala,azamwali,azan,azan daryan,azan deh,azan deh-e rudbar,azan khani ban,azan owzan,azan vazan,azanade,azanai,azanbar,azancallane,azanchevka,azancor,azandarian,azandariyan,azande,azane,azanel,azanga,azangallane,azangaro,azangbe,azango,azangora,azangulova,azangulovo,azani,azanieres,azanja,azanje,azanka,azanlah,azannes,azannes-et-soumazannes,azano,azanohu,azanon,azanovka,azanovo,azanovskiy,azans,azansola,azant,azanta,azantole,azanukofe,azanukorpe,azanushka,azanuwa,azanuy,azany minch,azanza,azao,azaou,azaoua,azaouirisse,azaoun,azaourisse,azap,azapa,azapa chico,azapac,azapamba,azapana,azapane,azapanes,azapbasi,azapbasili,azapbole,azape,azapkioy,azapkoy,azaplar,azapli,azaplii,azapliy,azaply,azapolye,azapoue,azapovici,azaqualpa,azaquela,azar,azar bonab,azar imi n tels,azar key,azar khel,azar khvaran,azar matjir,azar pasand,azar shahr,azar-tyube,azara,azara n irs,azara-egbelu,azarabad,azaraf,azarak,azarak-e bala,azarak-e pain,azaran,azaran-e bala,azaran-e pain,azarapino,azaraque,azarara,azarbaycan,azarbon,azarce,azarco mexicana,azarcon,azare,azares del paramo,azarestan,azarestanak,azareu,azarevka,azarga,azarghar n-bihmidene,azarhar,azarhar ifesfass,azarhar nirs,azarhar oumesliten,azarhar oumeslitene,azarharane,azaria,azarichi,azarici,azarif,azarime,azarine,azarino,azarion,azarjan,azark,azarkhan,azarki,azarkino,azarkipudovinka,azarkipudowinka,azarkovo,azarlu,azarmakan,azarnaq,azarnatil,azarnikovo,azaro,azarouane,azarova,azarova pervaya,azarovichi,azarovka,azarovo,azarovskiy,azarplar,azarrulla,azarsetan,azarsetanaki,azartepa,azartse,azartsy,azaru,azarud,azaruja,azarunji,azaryevka,azaryevo,azaryevskiy,azas,azas-kezes,azat,azat-chatenet,azat-le-riz,azata,azatamut,azatan,azatashen,azatashen-alighuli,azatavan,azatbasli,azatek,azati,azatli,azatli ciftligi,azatlu,azatuta,azaura,azauri,azavokofe,azavret,azavreti,azaw,azawja,azay,azay-le-brule,azay-le-feron,azay-le-ferron,azay-le-rideau,azay-sur-cher,azay-sur-indre,azay-sur-thouet,azaya,azayatla,azaye,azayeke,azaz,azaza,azazala,azazera,azazga,azazi,azazo,azazoul,azazzi,aza\302\277an,aza\302\277ar,azbaba,azbane khalif mbarek,azbar,azbaram,azbayev klyuch,azbazan,azberdyvakuf,azbezan,azbi,azbiokombinat,azbizan,azboti,azbreh,azbresnica,azcala,azcamellas,azcapotzalco,azcarate,azcaray,azcarete,azcarruns,azcatlan,azchuri,azcoaga,azcoitia,azcona,azcorra,azcuenaga,azcurra,azda,azdak,azdal,azdano kalay,azdanu-kala,azdar,azdar tappeh,azdaran,azdavay,azdeh,azdehsu,azdel,azdelin,azdemerovo,azdin,azdir,azdoud,azdu,azdzharakchi,azdzyelina,aze,azeba,azebe,azebois,azedeira,azedia,azeem hiraj,azeem shah,azeffoun,azegbene,azegetsu,azeglio,azegour,azegue,azegza,azegzaou ntanguerfa,azeira,azeitada,azeiteiros,azeitona,azek,azekhelishkyay,azekkour,azel,azele,azeleledji,azelem,azelha,azelhal,azelino,azelo,azeloo,azelot,azelouaz,azelta,azem,azembay,azemmour,azemour,azemouraba,azemovic mahala,azemz,azen,azena,azenda,azendi,azendorf,azene,azengazo,azenha,azenha de baixo,azenha de cima,azenha nova,azenha velha,azenhas,azenhas de baixo,azenhas do mar,azeni,azenke,azenraai,azenray,azenski vrh,azenweiler,azer,azerabad,azerables,azeradayoc,azerailles,azeran,azerat,azerbagdzhan,azerbalou,azerbaydzhan,azerbaydzhanskaya astara,az")bzane,azere,azeredo,azereff,azereix,azerfalil,azerfelil,azergui,azeri,azeriba,azerkat,azerley,azero marca,azero maria,azeron,azeronys,azerou,azerou ait chemini,azerou bahar,azerou kellat,azeroual,azerouane,azershchyna,azervadinha,azerveira,azerzo,azerzou,azerzu,azet,azetfoun,azetsu,azette,azev,azeva,azevedo,azevedo de sao jorge,azevedo meiral,azevedo sodre,azevedos,azevedro sodre,azevem,azeveria,azeville,azevo,azevou,azewijn,azey,azeyevo,azeyrenawt,azezo,azfar kala,azfarkala,azg ntiguidar,azgaleh,azgaleh-ye sheykh najm od din,azgam,azgan,azgandi,azgang,azganin,azganin-e `olya,azganin-e bala,azganin-e pain,azganlik,azgar,azgarin,azgata,azghad,azghagh,azghan,azghan sar,azghand,azghaneh,azghar,azgharai,azghare,azghari,azgharia,azgharloon,azgharlun,azghroti banda,azghumai,azghyr,azgi,azgida,azgilar,azgili,azgilli,azgilo,azgin,azgir,azgour,azgu,azgush,azha,azha gongshe,azha qu,azhaf,azhagiapandipuram,azhagiri nagar,azhahe,azhake xiang,azhakiapandipuram,azhalitas,azhamsa,azhand,azhar muhammadwala,azhara,azhbay,azhbayevo,azhdaga,azhdahatu,azhdano kalay,azhdano kelay,azhdar,azhdar khanah,azhdar khaneh,azhdar khowsh,azhdar khunah,azhdarabad,azhdarlu,azhdasu,azhdeha baluch,azhdeha kia,azhdehatu,azhdow,azhdzov,azhe,azhebay,azhek,azhen,azhendarovo,azhendarovskiy,azherovo,azhevskoye,azhga,azhgerian,azhgi,azhgil,azhgydyrkhva,azhibay,azhiboy,azhikal,azhike,azhikkal,azhinov,azhinskoye,azhkhakhara,azhki-say,azho,azhong,azhovka,azhovo,azhrykty,azhuang,azhujianwu,azhulauke,azhuo,azhuolu-buda,azhy,azhyeh,azi,azia,aziader,aziadome,aziakakope,aziakpo kope,aziama,azias,aziat,aziate,aziatskaya,aziatskiy,aziave,azib,azib abd es slam,azib aboudaou,azib afouzer,azib ahmed,azib ain el alak,azib amazir,azib bel aoud,azib ben ali cherif,azib bou seri,azib cheikh,azib cheikh ahmed,azib de midar,azib douirkat,azib douirkate,azib el aiadi,azib el aoud,azib el ayadi,azib el bakori,azib el beghli,azib el foqui,azib el hadj kappaj,azib el mahareg,azib el makri,azib el marog,azib el mroug,azib el oued,azib el ouezzani,azib es sefli,azib fatna,azib fokia,azib fouki,azib hai lhasene,azib haj abdallah,azib haj lhasene,azib imourhlane,azib jemaa,azib kaltfraout,azib khenachcha,azib khoualka,azib lahoued,azib mohammed al mokra,azib moulay abd el krim,azib moulay ali,azib moulay athmane,azib nidmamene,azib nizri nilkris,azib nouaqqa amassine,azib nwanou,azib oulad damoun,azib oulad ladem,azib oulad souilem,azib oulad tounsi,azib ramifi,azib remiki,azib salah,azib sallah,azib seflia,azib sekkoum,azib sidi boulmane,azib sidi brahim el mtougui,azib sidi tahar,azib sidi youssef,azib sidi yssef,azib slahma,azib tagoulzit,azib takhlicht,azib talddamt,azib tanoughzirt,azib tassa,azib tiferkiwine,azib tiferouine,azib tiferwine,azib tissiliane,azib zamoun,azib zekkoum,azib-sidi-zouine,aziba,azibey tatarskiy,azibi,azibpur,azici,azidabad,azido,azidoro,azie,azienda le trezze,azienda piave - isonzo,azieres,azieu,azieux,azifet,azifete,aziga,azigasawa,azigia,azigo,azigulova,azigulovo,azihe,azihua,aziim kor,azijeh,azijue,azikan,azikat,azikeng,azikeyeva,azikeyevo,azikh,azikli,aziko,azikope,azikovo,azil ghazban,azila,azilada,azilal,azilarkay,azilarkoy,azile,aziler,azillanet,azille,azilone,azilone-ampaza,azilta,azim,azim bhutto,azim gul mian kili,azim jat,azim ka walgan,azim kalla jadid,azim khan,azim khan jakrani,azim khan kili,azim khan koruna,azim khelanwala,azim kili,azim mian koruna,azim nagar,azim qila,azim sandoro,azim sethar,azim shah,azim shahwala,azim-sirma,azimabad,azimbare,azimdzhankarez,azime,azimegan,azimganj,azimganj city,azimiri,azimkala,azimkarez,azimli,azimnagar,azimogu,azimovka,azimovo kurlodash,azimovo-kurlebash,azimovo-kurlibash,azimpur,azimpura,azimur,azimwala,azin,azina,azinakponya,azincourt,azinhaga,azinhal,azinhal e amendoeira,azinhaleite,azinhalinho,azinhega,azinheira,azinheira de barros,azinheira do boirro,azinheira dos bairros,azinheira dos barros,azinheird dos barros,azinheiro,azinhosa,azino,azino odin,azinozero,azinskiy,azio anou,azioune,azir mahala,azira,aziris,azirlar,aziro,azirok,azis-kend,azisu,azitan,azitanxiang,azitepe,azitienu,azito,azitullapara,azivego,azivgus,azix,aziyanghtawng,aziyb,aziying,aziz,aziz abad,aziz ailawan,aziz arain,aziz beg,aziz ben tellis,aziz chak,aziz colony,aziz dheri,aziz dhok,aziz khan,aziz khel,aziz kiyan,aziz koyu,aziz oulad souilem,aziz park,aziz qaum,aziz talao,aziz umrati,aziza,azizabad,azizabat,azizacoue,azizahoue,azizaj,azizam,azizan,azizanu,azizanwala,azizanya,azizay,azizbang,azizbayli,azizbayov,azizbekov,azizbekovo,azizbey,azizbeyli,azizbeyly,azizchina,azize,azizeka,azizekend,azizfazilpur,azizgudale,azizia,azizie,azizili,azizima,azizion,aziziya,aziziyah,aziziyah terminal,aziziye,azizkendi,azizkend,azizkhan,azizkhankalay,azizkheyl,azizler,azizli,azizlu,azizo kili,azizo mela,azizonja,azizpur,azizpura,azizrud,azizuddin surveyor,azizullah,azizullah jamali,azizullah khosa,azizullahpur,azizullakhan karez,azizullakhan-kala,azizwala,azi\302\247 zi\302\247abitiyah,azi\302\247 zi\302\247aby,azi\302\247 zi\302\247afir,azi\302\247 zi\302\247ahir,azi\302\247 zi\302\247ahirah,azi\302\247 zi\302\247ahiriyah,azi\302\247 zi\302\247ahr,azi\302\247 zi\302\247ahrah,azi\302\247 zi\302\247ahrayn,azi\302\247 zi\302\247aji`,azi\302\247 zi\302\247ali`,azi\302\247 zi\302\247alil,azi\302\247 zi\302\247arubah, zi\302\247aymah,azi\302\247 zi\302\247ubr,azi\302\247 zi\302\247uhar al ala,azi\302\247 zi\302\247uhar al asfal,azi\302\247 zi\302\247uhayr,azi\302\247 zi\302\247uhrah,azi\302\247 zi\302\247urah,azi\302\247 zi\302\247uwayhir,azjen,azjene,azkand,azkari,azki,azkia,azkika,azla,azlag,azle,azlet el beilek,azletskiy,azlica,azlim,azlor,azloua,azlyar-tepe,azma,azmahin,azmak,azmakki,azmamat-yurt,azmamra,azman put,azmana,azmanabad,azmanak-e sofla,azmanat,azmanate,azmanawala,azmanite,azmannsdorf,azmano,azmanovo,azmanwala,azmara khan,azmareh,azmarin,azmarine,azmat,azmat adai,azmat di basti,azmat goth,azmat ke kathia,azmat khan,azmat khel,azmat khelanwala,azmat labana,azmat pur,azmat shah,azmat uold,azmatgani,azmatganj,azmatkalay,azmatnagar,azmatpur,azmatpura,azmatwala,azmatwala khu,azmazge,azmeleh mahalleh,azmet,azmetevo,azmetovo,azmetyevo,azmighan,azminu,azmir,azmiran,azmirganjbazar,azmiriganj,azmousa quihia,azmozero,azmre khue,azmushkino,azna,aznau,aznab,aznab-e `olya,aznab-e bala,aznab-e khaleseh,aznab-e sofla,aznabay,aznabirt,aznaburt,aznabyurt,aznachulovo,aznacollar,aznafer,aznafir,aznag,aznagulova,aznagulovo,aznahra,aznaina,aznakayevo,aznalcazar,aznalcollar,aznalino,aznalkino,aznan,aznapuquio,aznapuquio hacienda,aznaq,aznar,aznaraleh,aznasheva,aznashevo,aznau,aznav,aznavaleh,aznaveh,aznavesar,aznavi-ye bala,aznavi-ye pain,aznavleh,aznavur,aznayevo,aznazaleh,aznek,azni,aznolkin,aznoojan,aznow,aznowjan,aznowsar,aznu,aznu-ye bala,aznu-ye pain,aznub,aznujan,aznvadzor,azo,azo amar jat,azo i,azo ii,azo kili,azoawera,azobichi,azocaya,azodiyeh,azoe-cada,azofra,azogba,azogenou,azogueras,azogueros,azogues,azoguez,azohia,azoho,azohoue-ahiho,azohoue-cada,azohoue-hiho,azohoue-kada,azohoue-oumbo,azoi,azoia,azoia de baixo,azoia de cima,azoio,azoires,azok,azokangoudo,azokeramos,azokh,azokho,azokiret,azokkyi,azolette,azolimnos,azoltzintla,azolzintla,azomada,azomadita,azombo,azombujeira,azome,azompa,azong,azongbeu,azongho,azongo,azonkout,azonlihoue,azonos,azool,azopardo,azopolye,azoque,azoques,azoquilipa,azor,azore,azorera,azores,azorichi,azorodnya,azorogbene,azoros,azorovo,azorovskiy,azort,azortuzundere,azossim,azota criollo,azotea,azoteras,azotes,azoti,azotos,azotus,azou,azou roudrig,azoua,azouane,azouaoud,azoubane,azoudange,azouggar lalla aziza,azougza,azouia,azoukpo,azoult,azoumbeti,azoume,azouna,azounie,azour,azour nait lahsen,azour nait lahsene,azour nizig,azour ntaourirt,azour oudroug,azour oufella,azour ourhoud,azour ouzedder,azoura,azourane,azourene,azourg,azouriyne,azourouzour,azourzen,azouyomba,azouz,azouza,azov,azova,azovatla,azove,azovka,azovo,azovo-dolginskiy,azovo-dolgoye,azovskaya,azovske,azovskiy,azovskoye,azovy,azow,azowjah,azowk,azoyatla,azoyiras,azoyires,azoyu,azoz,azoza,azozo,azozuaque,azpa,azparren,azpeitia,azperiz,azpetia,azpilcueta,azpiroz,azpitia,azqand,azqueta,azquizu,azra,azrabad,azrabzane,azragi,azrah,azrail,azrailan,azrak,azrak do,azrak-e do,azrak-e yek,azrake dow,azrakov,azran,azrapino,azraq,azraq ad duruz,azraq ash shishan,azraq ashikah,azraq druze,azraq ed druz,azraq ed duruz,azraq esh shishan,azraq hashish,azraq shishan,azrarag,azrat,azraw,azreh,azreh-ye `abbasabad,azreh-ye mohammad khan,azreh-ye mokarrami,azrhar,azriel,azrie,azrier,azrif,azrikam,azriouila,azrir,azrirar,azro narbatas,azrou,azrou bou ammar,azrou mellen,azrou mellene,azrou melloul,azrou ouado,azrou ouadou,azrou-n-ait chemini,azroug,azrouken,azrourou,azrow,azru,azrudsar,azruvaj,aztahuacan,aztakasy,aztalan,aztashikha,aztatla,aztec,aztec lodge,aztec mobile home park,aztec subdivision,azteca,aztecas,aztiria,aztlan,aztlan cuarta seccion,aztlan primera seccion,aztlan segunda seccion,aztoun,azu,azu anyim,azu ciems,azu dzirnavas,azu muiza,azu owuchele,azu-aboh,azu-iyi,azua,azua de compostela,azuaga,azuago,azuakin,azuara,azuas,azubure,azucaica,azucar,azucar village,azucarera,azucaro,azucena,azucenapampa,azucenos,azucey,azuchi,azuchil,azuebar,azuebor,azuedi,azueira,azuel,azuelo,azuer,azufera sociedad,azufral roventazon,azufre,azufrera,azufrera a,azufrera vieja,azufreras,azufreras rancho,azufrosa,azuga,azugara,azugene-umumba-ndiagu,azugiriai,azugu-olua,azuk,azuka,azukala,azukisawa,azukizawa,azukyula,azul,azul boqueron,azul cocha,azulacuez,azulaque,azulaquez,azulauke,azulcancha,azulccacca,azulco,azulcocha,azulcuesta,azuleja,azulejo,azulejos,azulenloanu,azules,azuleti,azulicasa,azulichihua,azulillo,azulita,azullenca,azulrangra,azuluokesa,azulya,azuma,azuma-shigai,azumabashi,azumacho,azumaiokozan,azumara,azumbilla,azume,azumenovo,azumi,azumini,azumino,azumiri-isiokpo,azuncion,azundezeh,azunduse,azungur,azunkuli,azuo,azuogu,azuolai buda,azuolaiciai,azuolija,azuolijai,azuoliniai,azuolu buda,azuolynai,azuolynas,azuolyne,azuolynes budos,azuolyte,azuowa,azuozeriai,azupinka,azupizu,azupre,azupunpun,azuqueca,azuqueca de henares,azur,azurara,azurdui,azurduy,azure vista,azurem,azuretti,azurit,azurita,azurrieta,azurva,azusa,azusabashi,azusaboshi,azusaqui,azusawa,azusawacho,azusayama,azusiliai,azusugi,azutan,azutash,azuti,azuvi,azuwa,azuwera,azuzagui,azuzuama,azvar,azvarcheh,azvench,azvian,azvinciai,azvinciu,azwaira sufa,azwar,azwell,azy,azy-le-vif,azy-sur-marne,azyabou kope,azyagbang,azyak,azyakhovgort,azyakovo,azyakovy,azyakul,azyal,azyal-gort,azyaly,azyam,azyamka,azyana,azyanah,azyankovo,azyap-gort,azyas,azyrakhmedli,azytenai,azyubzha,azyuda,azyut,azza,azzaba,azzah,azzal,azzana,azzanello,azzano,azzano decimo,azzaouia tamaroute,azzap,azzar,azzata,azzate,azzazdine,azzazl,azzazo,azzazoul,azzedine,azzega,azzegour,azzib,azzib beni korra,azzida,azziye,azzone,azzosim,azzour ait el hajj,azzour nait el hajj,azzour nait gourma,azzouz,azzozo,a\302\200\302\235abd el-gilil,a\302\201any,a\302\201awkswo,a\302\201azdeniki,a\302\201opie,a\302\201uby,a\302\201yngmiany,a\302\220eva\302\221elijad,a\302\220jecar,a\302\220jinocar,a\302\220jonc,a\302\220onceli,a\302\220opceli,a\302\220orce petrov
\ No newline at end of file
diff --git a/test/resources/tokenization/zn_tw_1.txt b/test/resources/tokenization/zn_tw_1.txt
new file mode 100644
index 0000000..7e1e545
--- /dev/null
+++ b/test/resources/tokenization/zn_tw_1.txt
@@ -0,0 +1,19 @@
+銌鞁鬿 蝑蝞蝢 砫粍紞 顃餭 蜭, 趍跠跬 鏀顝饇 轞騹鼚 曨曣 慔 巆忁 嫷 惝掭掝 鑳鱨鱮, 滆 浶洯浽 牣犿玒 嶕憱撏 駇僾 憱撏 硾 湹渵焲 鋧鋓頠 匊呥 犌犐瑆 翣聜蒢 蜙, 毼 噳墺 耇胇赲 稨窨箌 埱娵徖, 鍆錌雔 熩熝犚 庲悊 槄 銌鞁鬿 烍烚珜 疿疶砳 魆 糑縒 魦 萴葂 貄趎跰 萰葍萯, 嗛嗕塨 礂簅縭 婜孲 跣, 楋 澭濇 嗢嗂塝 姌弣抶 曋橪橤
+
+崺崸 獧瞝瞣 牣犿玒 嫷, 墆 齴讘麡 毊灚襳 毚丮厹 甿虮 箯 埱娵 脀蚅蚡 礯籔羻 鈁陾靰, 垼娕 螏螉褩 竀篴臌 槶, 鵳齖齘 驐鷑鷩 絒翗腏 輗 嬦憼 耜僇鄗 訬軗郲 舿萐菿 頠餈 槶, 抰枅 嬃 軹軦軵 鸙讟钃 椵楘溍 渳湥牋 蔝蓶蓨 跜軥 嫀, 砯砨 嗢 鄨鎷闒 縓罃蔾, 鍹餳駷 玝甿虮 熩熝犚 碡碙 銇
+
+瞝瞣 莃荶衒 碄碆碃 樆樦潏 穊, 枲柊氠 婰婜孲 踣 繗繓 犈犆犅 溗煂獂 儋圚 餀, 蟷蠉蟼 禒箈箑 牬玾 槶 玾珆玸 錖霒馞 撖 姴怤 犆犅 鳻嶬幧 誁趏跮 墐
+
+墐 鬵鵛嚪 圩芰敔 蒝蒧蓏 餳駷, 葮 廘榙 斶檎檦 謺貙蹖 澉 駍駔鳿 蒝蒧蓏 蔊蓴蔖 垌壴, 煻 垺垼娕 簻臗藱 笓粊 絼 騉鬵 樛槷殦 縸縩薋 巕氍爟, 璸瓁穟 鯦鯢鯡 罫蓱蒆 齥廲 滘 鯠鯦 噮噦噞 忁曨曣 釂鱞鸄 鉌 寔嵒 葮 瀿犨皫 顤鰩鷎, 憢 蔏蔍蓪 柦柋牬 玝甿虮 偢偣
+
+嗂 蒏葝葮 鋱鋟鋈 鬄鵊鵙 繖藒 毚丮 匢奾灱 枲柊氠 椵楘溍 斠, 鬄鵊 鼏噳墺 巕氍爟 鋟, 鳱 鵳齖齘 雥齆犪 騧騜 轞騹鼚 溗煂獂 諙 岵帔, 煻 廦廥彋 繠繗繓 馦騧騜 齖齘 煘煓 喥喓堮 軹軦軵 壿, 斖蘱 酳 圛嬖嬨 姛帡恦, 摿斠榱 櫧櫋瀩 廅愮揫 驧鬤 跾
+
+綒 鞻饙騴 萆覕貹 稘稒稕 稢綌 笢笣紽 磃箹糈 瑽 氕厊, 剆坲 禖 鶀嚵巆 枅杺枙, 郔镺陯 烗猀珖 伒匢 殟 憢 箛箙 馺骱魡 潧潣瑽 觶譈譀 塝 豥趍 捘栒毤 幨懅憴 稘稒稕, 撖撱暲 駓駗鴀 鄻鎟霣 蝯 訑紱 縢羱 槏殟殠 浘涀缹 鄻鎟霣 輘, 籺籿 媝寔嵒 樧槧樈 焟硱筎 瞂
+
+蚔趵郚 碄碆碃 幋 璻甔 輘 裧頖 簎艜薤 鑤仜伒 誽賚賧 淠淉 鄜酳銪 炾笀耔 椵楘溍 魡 疿疶砳 趡趛踠 躨钀钁 馺 哤垽 庌弝彶 譋轐鏕 毄滱漮 踣 墡嬇, 賗 鯦鯢鯡 齈龘墻 輘輠 蕡蕇蕱 襛襡襙 隒雸頍 紒翀 楋, 殠漊 皾籈譧 磩磟窱 狅妵妶 榎
+
+釂鱞 禠 袟袘觕 餈餖駜 椵楘溍 銈 欿殽 鬵鵛嚪 鬎鯪鯠 礂簅縭, 彃 嶝仉圠 裍裚詷 莃荶 茺苶 趍跠跬 燚璒瘭 廲籗糴 殠 魦魵 姛帡恦 賌輈鄍 沀皯竻 墏, 橁橖澭 牣犿玒 捃挸栚 酳 劻穋 噮噦噞 獧瞝瞣 釂鱞 暕, 蝺 葝葮 壾嵷幓 褣諝趥
+
+跿 鮛鮥鴮 燲獯璯 鵵鵹鵿 唗哱 蓪 塛嫆嫊 邆錉霋 哤垽, 瀁瀎 馺骱魡 鏾鐇闠 闟顣飁 墆, 壾嵷幓 摬摙敳 鵳齖齘 歅 鋄銶 澂 櫞氌瀙 忕汌卣 蠁襏 斶檎檦 觶譈譀 釪傛 瑽, 觾韄鷡 輐銛靾 廞 袚觙 剆坲姏 鼏噳墺 榯槄 觢, 榎 鷃黫鼱 蛚袲褁 闟顣飁 饙騴, 諙踣踙 齸圞趲 鄜 鶾鷃 驐鷑鷩 禒箈箑 痵 娭屔, 蓨蝪 譋轐鏕 蔪蓩蔮 楋
+
+褅褌諃 蛃袚觙 傎圌媔 侹厗 榃, 緦 恦拻 杍肜阰 軥軱逴 緷 摲摓 郔镺陯 揈敜敥, 誙賗跿 彔抳抰 袀豇貣 蜬蝁 榎 傎圌 圛嬖嬨 鑴鱱爧 潣, 枲柊 誙賗跿 貵趀跅 鮂鮐嚃 溿 禖 笓粊 齴讘麡 漻漍犕 趡趛踠, 廞 騩鰒鰔 峷敊浭 烒珛
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/MockSchema.java b/test/unit/org/apache/cassandra/MockSchema.java
index 5f3198d..90c8e4c 100644
--- a/test/unit/org/apache/cassandra/MockSchema.java
+++ b/test/unit/org/apache/cassandra/MockSchema.java
@@ -34,16 +34,15 @@
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.IndexSummary;
+import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
 import org.apache.cassandra.io.sstable.metadata.MetadataType;
 import org.apache.cassandra.io.sstable.metadata.StatsMetadata;
-import org.apache.cassandra.io.util.BufferedSegmentedFile;
 import org.apache.cassandra.io.util.ChannelProxy;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.io.util.Memory;
-import org.apache.cassandra.io.util.RandomAccessReader;
-import org.apache.cassandra.io.util.SegmentedFile;
+import org.apache.cassandra.io.util.FileHandle;
 import org.apache.cassandra.schema.CachingParams;
 import org.apache.cassandra.schema.KeyspaceMetadata;
 import org.apache.cassandra.schema.KeyspaceParams;
@@ -64,7 +63,7 @@
     public static final Keyspace ks = Keyspace.mockKS(KeyspaceMetadata.create("mockks", KeyspaceParams.simpleTransient(1)));
 
     public static final IndexSummary indexSummary;
-    public static final File tempFile = temp("mocksegmentedfile");
+    private static final File tempFile = temp("mocksegmentedfile");
 
     public static Memtable memtable(ColumnFamilyStore cfs)
     {
@@ -86,23 +85,42 @@
         return sstable(generation, size, false, cfs);
     }
 
-    public static SSTableReader sstableWithTimestamp(int generation, long timestamp, ColumnFamilyStore cfs)
-    {
-        return sstable(generation, 0, false, timestamp, cfs);
-    }
-
     public static SSTableReader sstable(int generation, int size, boolean keepRef, ColumnFamilyStore cfs)
     {
-        return sstable(generation, size, keepRef, System.currentTimeMillis() * 1000, cfs);
+        return sstable(generation, size, keepRef, generation, generation, cfs);
     }
 
+    public static SSTableReader sstableWithLevel(int generation, long firstToken, long lastToken, int level, ColumnFamilyStore cfs)
+    {
+        return sstable(generation, 0, false, firstToken, lastToken, level, cfs);
+    }
 
-    public static SSTableReader sstable(int generation, int size, boolean keepRef, long timestamp, ColumnFamilyStore cfs)
+    public static SSTableReader sstableWithLevel(int generation, int size, int level, ColumnFamilyStore cfs)
+    {
+        return sstable(generation, size, false, generation, generation, level, cfs);
+    }
+
+    public static SSTableReader sstableWithTimestamp(int generation, long timestamp, ColumnFamilyStore cfs)
+    {
+        return sstable(generation, 0, false, 0, 1000, 0, Integer.MAX_VALUE, timestamp, cfs);
+    }
+
+    public static SSTableReader sstable(int generation, int size, boolean keepRef, long firstToken, long lastToken, ColumnFamilyStore cfs)
+    {
+        return sstable(generation, size, keepRef, firstToken, lastToken, 0, cfs);
+    }
+
+    public static SSTableReader sstable(int generation, int size, boolean keepRef, long firstToken, long lastToken, int level, ColumnFamilyStore cfs)
+    {
+        return sstable(generation, size, keepRef, firstToken, lastToken, level, Integer.MAX_VALUE, System.currentTimeMillis() * 1000, cfs);
+    }
+
+    public static SSTableReader sstable(int generation, int size, boolean keepRef, long firstToken, long lastToken, int level, int minLocalDeletionTime, long timestamp, ColumnFamilyStore cfs)
     {
         Descriptor descriptor = new Descriptor(cfs.getDirectories().getDirectoryForNewSSTables(),
                                                cfs.keyspace.getName(),
                                                cfs.getColumnFamilyName(),
-                                               generation);
+                                               generation, SSTableFormat.Type.BIG);
         Set<Component> components = ImmutableSet.of(Component.DATA, Component.PRIMARY_INDEX, Component.FILTER, Component.TOC);
         for (Component component : components)
         {
@@ -115,37 +133,41 @@
             {
             }
         }
-        SegmentedFile segmentedFile = new BufferedSegmentedFile(new ChannelProxy(tempFile), RandomAccessReader.DEFAULT_BUFFER_SIZE, size);
-        if (size > 0)
+        // .complete() with size to make sstable.onDiskLength work
+        try (FileHandle.Builder builder = new FileHandle.Builder(new ChannelProxy(tempFile)).bufferSize(size);
+             FileHandle fileHandle = builder.complete(size))
         {
-            try
+            if (size > 0)
             {
-                File file = new File(descriptor.filenameFor(Component.DATA));
-                try (RandomAccessFile raf = new RandomAccessFile(file, "rw"))
+                try
                 {
-                    raf.setLength(size);
+                    File file = new File(descriptor.filenameFor(Component.DATA));
+                    try (RandomAccessFile raf = new RandomAccessFile(file, "rw"))
+                    {
+                        raf.setLength(size);
+                    }
+                }
+                catch (IOException e)
+                {
+                    throw new RuntimeException(e);
                 }
             }
-            catch (IOException e)
-            {
-                throw new RuntimeException(e);
-            }
+            SerializationHeader header = SerializationHeader.make(cfs.metadata, Collections.emptyList());
+            MetadataCollector collector = new MetadataCollector(cfs.metadata.comparator);
+            collector.update(new DeletionTime(timestamp, minLocalDeletionTime));
+            StatsMetadata metadata = (StatsMetadata) collector.sstableLevel(level)
+                                                              .finalizeMetadata(cfs.metadata.partitioner.getClass().getCanonicalName(), 0.01f, UNREPAIRED_SSTABLE, header)
+                                                              .get(MetadataType.STATS);
+            SSTableReader reader = SSTableReader.internalOpen(descriptor, components, cfs.metadata,
+                    fileHandle.sharedCopy(), fileHandle.sharedCopy(), indexSummary.sharedCopy(),
+                    new AlwaysPresentFilter(), 1L, metadata, SSTableReader.OpenReason.NORMAL, header);
+            reader.first = readerBounds(firstToken);
+            reader.last = readerBounds(lastToken);
+            if (!keepRef)
+                reader.selfRef().release();
+            return reader;
         }
-        SerializationHeader header = SerializationHeader.make(cfs.metadata, Collections.emptyList());
-        MetadataCollector collector = new MetadataCollector(cfs.metadata.comparator);
-        collector.update(new DeletionTime(timestamp, (int) (System.currentTimeMillis() / 1000)));
-        StatsMetadata metadata = (StatsMetadata) collector.finalizeMetadata(cfs.metadata.partitioner.getClass().getCanonicalName(),
-                                                                            0.01f,
-                                                                            -1,
-                                                                            header).get(MetadataType.STATS);
 
-        SSTableReader reader = SSTableReader.internalOpen(descriptor, components, cfs.metadata,
-                                                          segmentedFile.sharedCopy(), segmentedFile.sharedCopy(), indexSummary.sharedCopy(),
-                                                          new AlwaysPresentFilter(), 1L, metadata, SSTableReader.OpenReason.NORMAL, header);
-        reader.first = reader.last = readerBounds(generation);
-        if (!keepRef)
-            reader.selfRef().release();
-        return reader;
     }
 
     public static ColumnFamilyStore newCFS()
@@ -157,7 +179,7 @@
     {
         String cfname = "mockcf" + (id.incrementAndGet());
         CFMetaData metadata = newCFMetaData(ksname, cfname);
-        return new ColumnFamilyStore(ks, cfname, 0, metadata, new Directories(metadata), false, false);
+        return new ColumnFamilyStore(ks, cfname, 0, metadata, new Directories(metadata), false, false, false);
     }
 
     public static CFMetaData newCFMetaData(String ksname, String cfname)
@@ -172,9 +194,9 @@
         return metadata;
     }
 
-    public static BufferDecoratedKey readerBounds(int generation)
+    public static BufferDecoratedKey readerBounds(long token)
     {
-        return new BufferDecoratedKey(new Murmur3Partitioner.LongToken(generation), ByteBufferUtil.EMPTY_BYTE_BUFFER);
+        return new BufferDecoratedKey(new Murmur3Partitioner.LongToken(token), ByteBufferUtil.EMPTY_BYTE_BUFFER);
     }
 
     private static File temp(String id)
diff --git a/test/unit/org/apache/cassandra/SchemaLoader.java b/test/unit/org/apache/cassandra/SchemaLoader.java
index 026aba8..822ee67 100644
--- a/test/unit/org/apache/cassandra/SchemaLoader.java
+++ b/test/unit/org/apache/cassandra/SchemaLoader.java
@@ -21,6 +21,9 @@
 import java.io.IOException;
 import java.util.*;
 
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.index.sasi.SASIIndex;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
 import org.junit.After;
 import org.junit.BeforeClass;
 
@@ -68,6 +71,8 @@
 
     public static void startGossiper()
     {
+        // skip shadow round and endpoint collision check in tests
+        System.setProperty("cassandra.allow_unsafe_join", "true");
         if (!Gossiper.instance.isEnabled())
             Gossiper.instance.start((int) (System.currentTimeMillis() / 1000));
     }
@@ -83,6 +88,7 @@
         String ks4 = testName + "Keyspace4";
         String ks5 = testName + "Keyspace5";
         String ks6 = testName + "Keyspace6";
+        String ks7 = testName + "Keyspace7";
         String ks_kcs = testName + "KeyCacheSpace";
         String ks_rcs = testName + "RowCacheSpace";
         String ks_ccs = testName + "CounterCacheSpace";
@@ -106,6 +112,7 @@
         compactionOptions.put("tombstone_compaction_interval", "1");
         Map<String, String> leveledOptions = new HashMap<String, String>();
         leveledOptions.put("sstable_size_in_mb", "1");
+        leveledOptions.put("fanout_size", "5");
 
         // Keyspace 1
         schema.add(KeyspaceMetadata.create(ks1,
@@ -186,11 +193,17 @@
         schema.add(KeyspaceMetadata.create(ks5,
                 KeyspaceParams.simple(2),
                 Tables.of(standardCFMD(ks5, "Standard1"))));
+
         // Keyspace 6
         schema.add(KeyspaceMetadata.create(ks6,
                 KeyspaceParams.simple(1),
                 Tables.of(keysIndexCFMD(ks6, "Indexed1", true))));
 
+        // Keyspace 7
+        schema.add(KeyspaceMetadata.create(ks7,
+                KeyspaceParams.simple(1),
+                Tables.of(customIndexCFMD(ks7, "Indexed1"))));
+
         // KeyCacheSpace
         schema.add(KeyspaceMetadata.create(ks_kcs,
                 KeyspaceParams.simple(1),
@@ -250,6 +263,8 @@
                         + "WITH COMPACT STORAGE", ks_cql)
         )));
 
+        if (DatabaseDescriptor.getPartitioner() instanceof Murmur3Partitioner)
+            schema.add(KeyspaceMetadata.create("sasi", KeyspaceParams.simpleTransient(1), Tables.of(sasiCFMD("sasi", "test_cf"), clusteringSASICFMD("sasi", "clustering_test_cf"))));
 
         if (Boolean.parseBoolean(System.getProperty("cassandra.test.compression", "false")))
             useCompression(schema);
@@ -411,18 +426,23 @@
                                  .build();
 
     }
-    public static CFMetaData compositeIndexCFMD(String ksName, String cfName, boolean withIndex) throws ConfigurationException
+    public static CFMetaData compositeIndexCFMD(String ksName, String cfName, boolean withRegularIndex) throws ConfigurationException
     {
-        // the withIndex flag exists to allow tests index creation
-        // on existing columns
+        return compositeIndexCFMD(ksName, cfName, withRegularIndex, false);
+    }
+
+    public static CFMetaData compositeIndexCFMD(String ksName, String cfName, boolean withRegularIndex, boolean withStaticIndex) throws ConfigurationException
+    {
         CFMetaData cfm = CFMetaData.Builder.create(ksName, cfName)
                 .addPartitionKey("key", AsciiType.instance)
                 .addClusteringColumn("c1", AsciiType.instance)
                 .addRegularColumn("birthdate", LongType.instance)
                 .addRegularColumn("notbirthdate", LongType.instance)
+                .addStaticColumn("static", LongType.instance)
                 .build();
 
-        if (withIndex)
+        if (withRegularIndex)
+        {
             cfm.indexes(
                 cfm.getIndexes()
                    .with(IndexMetadata.fromIndexTargets(cfm,
@@ -432,12 +452,28 @@
                                                         "birthdate_key_index",
                                                         IndexMetadata.Kind.COMPOSITES,
                                                         Collections.EMPTY_MAP)));
+        }
+
+        if (withStaticIndex)
+        {
+            cfm.indexes(
+                    cfm.getIndexes()
+                       .with(IndexMetadata.fromIndexTargets(cfm,
+                                                            Collections.singletonList(
+                                                                new IndexTarget(new ColumnIdentifier("static", true),
+                                                                                IndexTarget.Type.VALUES)),
+                                                            "static_index",
+                                                            IndexMetadata.Kind.COMPOSITES,
+                                                            Collections.EMPTY_MAP)));
+        }
 
         return cfm.compression(getCompressionParameters());
     }
 
     public static CFMetaData compositeMultipleIndexCFMD(String ksName, String cfName) throws ConfigurationException
     {
+        // the withIndex flag exists to allow tests index creation
+        // on existing columns
         CFMetaData cfm = CFMetaData.Builder.create(ksName, cfName)
                                            .addPartitionKey("key", AsciiType.instance)
                                            .addClusteringColumn("c1", AsciiType.instance)
@@ -446,28 +482,27 @@
                                            .build();
 
         cfm.indexes(
-            cfm.getIndexes()
-               .with(IndexMetadata.fromIndexTargets(cfm,
-                                                    Collections.singletonList(
-                                                    new IndexTarget(new ColumnIdentifier("birthdate", true),
-                                                                    IndexTarget.Type.VALUES)),
-                                                    "birthdate_key_index",
-                                                    IndexMetadata.Kind.COMPOSITES,
-                                                    Collections.EMPTY_MAP))
-               .with(IndexMetadata.fromIndexTargets(cfm,
-                                                    Collections.singletonList(
-                                                    new IndexTarget(new ColumnIdentifier("notbirthdate", true),
-                                                                    IndexTarget.Type.VALUES)),
-                                                    "notbirthdate_key_index",
-                                                    IndexMetadata.Kind.COMPOSITES,
-                                                    Collections.EMPTY_MAP))
+        cfm.getIndexes()
+           .with(IndexMetadata.fromIndexTargets(cfm,
+                                                Collections.singletonList(
+                                                new IndexTarget(new ColumnIdentifier("birthdate", true),
+                                                                IndexTarget.Type.VALUES)),
+                                                "birthdate_key_index",
+                                                IndexMetadata.Kind.COMPOSITES,
+                                                Collections.EMPTY_MAP))
+           .with(IndexMetadata.fromIndexTargets(cfm,
+                                                Collections.singletonList(
+                                                new IndexTarget(new ColumnIdentifier("notbirthdate", true),
+                                                                IndexTarget.Type.VALUES)),
+                                                "notbirthdate_key_index",
+                                                IndexMetadata.Kind.COMPOSITES,
+                                                Collections.EMPTY_MAP))
         );
 
 
         return cfm.compression(getCompressionParameters());
     }
 
-
     public static CFMetaData keysIndexCFMD(String ksName, String cfName, boolean withIndex) throws ConfigurationException
     {
         CFMetaData cfm = CFMetaData.Builder.createDense(ksName, cfName, false, false)
@@ -492,7 +527,31 @@
 
         return cfm.compression(getCompressionParameters());
     }
-    
+
+    public static CFMetaData customIndexCFMD(String ksName, String cfName) throws ConfigurationException
+    {
+        CFMetaData cfm = CFMetaData.Builder.createDense(ksName, cfName, false, false)
+                                           .addPartitionKey("key", AsciiType.instance)
+                                           .addClusteringColumn("c1", AsciiType.instance)
+                                           .addRegularColumn("value", LongType.instance)
+                                           .build();
+
+            cfm.indexes(
+                cfm.getIndexes()
+                .with(IndexMetadata.fromIndexTargets(cfm,
+                                                     Collections.singletonList(
+                                                             new IndexTarget(new ColumnIdentifier("value", true),
+                                                                             IndexTarget.Type.VALUES)),
+                                                     "value_index",
+                                                     IndexMetadata.Kind.CUSTOM,
+                                                     Collections.singletonMap(
+                                                             IndexTarget.CUSTOM_INDEX_OPTION_NAME,
+                                                             StubIndex.class.getName()))));
+
+
+        return cfm.compression(getCompressionParameters());
+    }
+
     public static CFMetaData jdbcCFMD(String ksName, String cfName, AbstractType comp)
     {
         return CFMetaData.Builder.create(ksName, cfName).addPartitionKey("key", BytesType.instance)
@@ -500,6 +559,205 @@
                                                         .compression(getCompressionParameters());
     }
 
+    public static CFMetaData sasiCFMD(String ksName, String cfName)
+    {
+        CFMetaData cfm = CFMetaData.Builder.create(ksName, cfName)
+                                           .addPartitionKey("id", UTF8Type.instance)
+                                           .addRegularColumn("first_name", UTF8Type.instance)
+                                           .addRegularColumn("last_name", UTF8Type.instance)
+                                           .addRegularColumn("age", Int32Type.instance)
+                                           .addRegularColumn("height", Int32Type.instance)
+                                           .addRegularColumn("timestamp", LongType.instance)
+                                           .addRegularColumn("address", UTF8Type.instance)
+                                           .addRegularColumn("score", DoubleType.instance)
+                                           .addRegularColumn("comment", UTF8Type.instance)
+                                           .addRegularColumn("comment_suffix_split", UTF8Type.instance)
+                                           .addRegularColumn("/output/full-name/", UTF8Type.instance)
+                                           .addRegularColumn("/data/output/id", UTF8Type.instance)
+                                           .addRegularColumn("first_name_prefix", UTF8Type.instance)
+                                           .build();
+
+        cfm.indexes(cfm.getIndexes()
+                        .with(IndexMetadata.fromSchemaMetadata("first_name", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+                        {{
+                            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+                            put(IndexTarget.TARGET_OPTION_NAME, "first_name");
+                            put("mode", OnDiskIndexBuilder.Mode.CONTAINS.toString());
+                        }}))
+                        .with(IndexMetadata.fromSchemaMetadata("last_name", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+                        {{
+                            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+                            put(IndexTarget.TARGET_OPTION_NAME, "last_name");
+                            put("mode", OnDiskIndexBuilder.Mode.CONTAINS.toString());
+                        }}))
+                        .with(IndexMetadata.fromSchemaMetadata("age", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+                        {{
+                            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+                            put(IndexTarget.TARGET_OPTION_NAME, "age");
+
+                        }}))
+                        .with(IndexMetadata.fromSchemaMetadata("timestamp", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+                        {{
+                            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+                            put(IndexTarget.TARGET_OPTION_NAME, "timestamp");
+                            put("mode", OnDiskIndexBuilder.Mode.SPARSE.toString());
+
+                        }}))
+                        .with(IndexMetadata.fromSchemaMetadata("address", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+                        {{
+                            put("analyzer_class", "org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer");
+                            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+                            put(IndexTarget.TARGET_OPTION_NAME, "address");
+                            put("mode", OnDiskIndexBuilder.Mode.PREFIX.toString());
+                            put("case_sensitive", "false");
+                        }}))
+                        .with(IndexMetadata.fromSchemaMetadata("score", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+                        {{
+                            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+                            put(IndexTarget.TARGET_OPTION_NAME, "score");
+                        }}))
+                        .with(IndexMetadata.fromSchemaMetadata("comment", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+                        {{
+                            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+                            put(IndexTarget.TARGET_OPTION_NAME, "comment");
+                            put("mode", OnDiskIndexBuilder.Mode.CONTAINS.toString());
+                            put("analyzed", "true");
+                        }}))
+                        .with(IndexMetadata.fromSchemaMetadata("comment_suffix_split", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+                        {{
+                            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+                            put(IndexTarget.TARGET_OPTION_NAME, "comment_suffix_split");
+                            put("mode", OnDiskIndexBuilder.Mode.CONTAINS.toString());
+                            put("analyzed", "false");
+                        }}))
+                        .with(IndexMetadata.fromSchemaMetadata("output_full_name", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+                        {{
+                            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+                            put(IndexTarget.TARGET_OPTION_NAME, "/output/full-name/");
+                            put("analyzed", "true");
+                            put("analyzer_class", "org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer");
+                            put("case_sensitive", "false");
+                        }}))
+                        .with(IndexMetadata.fromSchemaMetadata("data_output_id", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+                        {{
+                            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+                            put(IndexTarget.TARGET_OPTION_NAME, "/data/output/id");
+                            put("mode", OnDiskIndexBuilder.Mode.CONTAINS.toString());
+                        }}))
+                        .with(IndexMetadata.fromSchemaMetadata("first_name_prefix", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+                        {{
+                            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+                            put(IndexTarget.TARGET_OPTION_NAME, "first_name_prefix");
+                            put("analyzed", "true");
+                            put("tokenization_normalize_lowercase", "true");
+                        }})));
+
+        return cfm;
+    }
+
+    public static CFMetaData clusteringSASICFMD(String ksName, String cfName)
+    {
+        return clusteringSASICFMD(ksName, cfName, "location", "age", "height", "score");
+    }
+
+    public static CFMetaData clusteringSASICFMD(String ksName, String cfName, String...indexedColumns)
+    {
+        CFMetaData cfm = CFMetaData.Builder.create(ksName, cfName)
+                                           .addPartitionKey("name", UTF8Type.instance)
+                                           .addClusteringColumn("location", UTF8Type.instance)
+                                           .addClusteringColumn("age", Int32Type.instance)
+                                           .addRegularColumn("height", Int32Type.instance)
+                                           .addRegularColumn("score", DoubleType.instance)
+                                           .addStaticColumn("nickname", UTF8Type.instance)
+                                           .build();
+
+        Indexes indexes = cfm.getIndexes();
+        for (String indexedColumn : indexedColumns)
+        {
+            indexes = indexes.with(IndexMetadata.fromSchemaMetadata(indexedColumn, IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+            {{
+                put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+                put(IndexTarget.TARGET_OPTION_NAME, indexedColumn);
+                put("mode", OnDiskIndexBuilder.Mode.PREFIX.toString());
+            }}));
+        }
+        cfm.indexes(indexes);
+        return cfm;
+    }
+
+    public static CFMetaData staticSASICFMD(String ksName, String cfName)
+    {
+        CFMetaData cfm = CFMetaData.Builder.create(ksName, cfName)
+                                           .addPartitionKey("sensor_id", Int32Type.instance)
+                                           .addStaticColumn("sensor_type", UTF8Type.instance)
+                                           .addClusteringColumn("date", LongType.instance)
+                                           .addRegularColumn("value", DoubleType.instance)
+                                           .addRegularColumn("variance", Int32Type.instance)
+                                           .build();
+
+        Indexes indexes = cfm.getIndexes();
+        indexes = indexes.with(IndexMetadata.fromSchemaMetadata("sensor_type", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+        {{
+            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+            put(IndexTarget.TARGET_OPTION_NAME, "sensor_type");
+            put("mode", OnDiskIndexBuilder.Mode.PREFIX.toString());
+            put("analyzer_class", "org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer");
+            put("case_sensitive", "false");
+        }}));
+
+        indexes = indexes.with(IndexMetadata.fromSchemaMetadata("value", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+        {{
+            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+            put(IndexTarget.TARGET_OPTION_NAME, "value");
+            put("mode", OnDiskIndexBuilder.Mode.PREFIX.toString());
+        }}));
+
+        indexes = indexes.with(IndexMetadata.fromSchemaMetadata("variance", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+        {{
+            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+            put(IndexTarget.TARGET_OPTION_NAME, "variance");
+            put("mode", OnDiskIndexBuilder.Mode.PREFIX.toString());
+        }}));
+
+        cfm.indexes(indexes);
+        return cfm;
+    }
+
+    public static CFMetaData fullTextSearchSASICFMD(String ksName, String cfName)
+    {
+        CFMetaData cfm = CFMetaData.Builder.create(ksName, cfName)
+                                           .addPartitionKey("song_id", UUIDType.instance)
+                                           .addRegularColumn("title", UTF8Type.instance)
+                                           .addRegularColumn("artist", UTF8Type.instance)
+                                           .build();
+
+        Indexes indexes = cfm.getIndexes();
+        indexes = indexes.with(IndexMetadata.fromSchemaMetadata("title", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+        {{
+            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+            put(IndexTarget.TARGET_OPTION_NAME, "title");
+            put("mode", OnDiskIndexBuilder.Mode.CONTAINS.toString());
+            put("analyzer_class", "org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer");
+            put("tokenization_enable_stemming", "true");
+            put("tokenization_locale", "en");
+            put("tokenization_skip_stop_words", "true");
+            put("tokenization_normalize_lowercase", "true");
+        }}));
+
+        indexes = indexes.with(IndexMetadata.fromSchemaMetadata("artist", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+        {{
+            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+            put(IndexTarget.TARGET_OPTION_NAME, "artist");
+            put("mode", OnDiskIndexBuilder.Mode.CONTAINS.toString());
+            put("analyzer_class", "org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer");
+            put("case_sensitive", "false");
+
+        }}));
+
+        cfm.indexes(indexes);
+        return cfm;
+    }
+
     public static CompressionParams getCompressionParameters()
     {
         return getCompressionParameters(null);
diff --git a/test/unit/org/apache/cassandra/UpdateBuilder.java b/test/unit/org/apache/cassandra/UpdateBuilder.java
index 3a5fbe6..9fcda15 100644
--- a/test/unit/org/apache/cassandra/UpdateBuilder.java
+++ b/test/unit/org/apache/cassandra/UpdateBuilder.java
@@ -21,9 +21,9 @@
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.partitions.*;
 import org.apache.cassandra.utils.FBUtilities;
-import org.apache.cassandra.service.StorageService;
 
 
 /**
@@ -34,32 +34,30 @@
  */
 public class UpdateBuilder
 {
-    private final PartitionUpdate update;
-    private RowUpdateBuilder currentRow;
-    private long timestamp = FBUtilities.timestampMicros();
+    private final PartitionUpdate.SimpleBuilder updateBuilder;
+    private Row.SimpleBuilder currentRow;
 
-    private UpdateBuilder(CFMetaData metadata, DecoratedKey partitionKey)
+    private UpdateBuilder(PartitionUpdate.SimpleBuilder updateBuilder)
     {
-        this.update = new PartitionUpdate(metadata, partitionKey, metadata.partitionColumns(), 4);
+        this.updateBuilder = updateBuilder;
     }
 
     public static UpdateBuilder create(CFMetaData metadata, Object... partitionKey)
     {
-        return new UpdateBuilder(metadata, makeKey(metadata, partitionKey));
+        return new UpdateBuilder(PartitionUpdate.simpleBuilder(metadata, partitionKey));
     }
 
     public UpdateBuilder withTimestamp(long timestamp)
     {
-        this.timestamp = timestamp;
+        updateBuilder.timestamp(timestamp);
+        if (currentRow != null)
+            currentRow.timestamp(timestamp);
         return this;
     }
 
     public UpdateBuilder newRow(Object... clustering)
     {
-        maybeBuildCurrentRow();
-        currentRow = new RowUpdateBuilder(update, timestamp, 0);
-        if (clustering.length > 0)
-            currentRow.clustering(clustering);
+        currentRow = updateBuilder.row(clustering);
         return this;
     }
 
@@ -72,48 +70,25 @@
 
     public PartitionUpdate build()
     {
-        maybeBuildCurrentRow();
-        return update;
+        return updateBuilder.build();
     }
 
     public IMutation makeMutation()
     {
-        Mutation m = new Mutation(build());
-        return update.metadata().isCounter()
+        Mutation m = updateBuilder.buildAsMutation();
+        return updateBuilder.metadata().isCounter()
              ? new CounterMutation(m, ConsistencyLevel.ONE)
              : m;
     }
 
     public void apply()
     {
-        Mutation m = new Mutation(build());
-        if (update.metadata().isCounter())
-            new CounterMutation(m, ConsistencyLevel.ONE).apply();
-        else
-            m.apply();
+        makeMutation().apply();
     }
 
     public void applyUnsafe()
     {
-        assert !update.metadata().isCounter() : "Counters have currently no applyUnsafe() option";
-        new Mutation(build()).applyUnsafe();
-    }
-
-    private void maybeBuildCurrentRow()
-    {
-        if (currentRow != null)
-        {
-            currentRow.build();
-            currentRow = null;
-        }
-    }
-
-    private static DecoratedKey makeKey(CFMetaData metadata, Object[] partitionKey)
-    {
-        if (partitionKey.length == 1 && partitionKey[0] instanceof DecoratedKey)
-            return (DecoratedKey)partitionKey[0];
-
-        ByteBuffer key = CFMetaData.serializePartitionKey(metadata.getKeyValidatorAsClusteringComparator().make(partitionKey));
-        return metadata.decorateKey(key);
+        assert !updateBuilder.metadata().isCounter() : "Counters have currently no applyUnsafe() option";
+        updateBuilder.buildAsMutation().applyUnsafe();
     }
 }
diff --git a/test/unit/org/apache/cassandra/Util.java b/test/unit/org/apache/cassandra/Util.java
index a49440d..fa24167 100644
--- a/test/unit/org/apache/cassandra/Util.java
+++ b/test/unit/org/apache/cassandra/Util.java
@@ -36,6 +36,9 @@
 import com.google.common.collect.Iterators;
 import org.apache.commons.lang3.StringUtils;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.config.DatabaseDescriptor;
@@ -46,6 +49,8 @@
 import org.apache.cassandra.db.compaction.AbstractCompactionTask;
 import org.apache.cassandra.db.compaction.CompactionManager;
 import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.AsciiType;
+import org.apache.cassandra.db.marshal.Int32Type;
 import org.apache.cassandra.db.partitions.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.dht.IPartitioner;
@@ -59,6 +64,8 @@
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.service.pager.PagingState;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.CounterId;
 import org.apache.cassandra.utils.FBUtilities;
@@ -69,7 +76,9 @@
 
 public class Util
 {
-    private static List<UUID> hostIdPool = new ArrayList<UUID>();
+    private static final Logger logger = LoggerFactory.getLogger(Util.class);
+
+    private static List<UUID> hostIdPool = new ArrayList<>();
 
     public static IPartitioner testPartitioner()
     {
@@ -232,8 +241,9 @@
     public static void compact(ColumnFamilyStore cfs, Collection<SSTableReader> sstables)
     {
         int gcBefore = cfs.gcBefore(FBUtilities.nowInSeconds());
-        AbstractCompactionTask task = cfs.getCompactionStrategyManager().getUserDefinedTask(sstables, gcBefore);
-        task.execute(null);
+        List<AbstractCompactionTask> tasks = cfs.getCompactionStrategyManager().getUserDefinedTasks(sstables, gcBefore);
+        for (AbstractCompactionTask task : tasks)
+            task.execute(null);
     }
 
     public static void expectEOF(Callable<?> callable)
@@ -279,7 +289,8 @@
 
     public static void assertEmptyUnfiltered(ReadCommand command)
     {
-        try (ReadOrderGroup orderGroup = command.startOrderGroup(); UnfilteredPartitionIterator iterator = command.executeLocally(orderGroup))
+        try (ReadExecutionController executionController = command.executionController();
+             UnfilteredPartitionIterator iterator = command.executeLocally(executionController))
         {
             if (iterator.hasNext())
             {
@@ -293,7 +304,8 @@
 
     public static void assertEmpty(ReadCommand command)
     {
-        try (ReadOrderGroup orderGroup = command.startOrderGroup(); PartitionIterator iterator = command.executeInternal(orderGroup))
+        try (ReadExecutionController executionController = command.executionController();
+             PartitionIterator iterator = command.executeInternal(executionController))
         {
             if (iterator.hasNext())
             {
@@ -308,7 +320,8 @@
     public static List<ImmutableBTreePartition> getAllUnfiltered(ReadCommand command)
     {
         List<ImmutableBTreePartition> results = new ArrayList<>();
-        try (ReadOrderGroup orderGroup = command.startOrderGroup(); UnfilteredPartitionIterator iterator = command.executeLocally(orderGroup))
+        try (ReadExecutionController executionController = command.executionController();
+             UnfilteredPartitionIterator iterator = command.executeLocally(executionController))
         {
             while (iterator.hasNext())
             {
@@ -324,7 +337,8 @@
     public static List<FilteredPartition> getAll(ReadCommand command)
     {
         List<FilteredPartition> results = new ArrayList<>();
-        try (ReadOrderGroup orderGroup = command.startOrderGroup(); PartitionIterator iterator = command.executeInternal(orderGroup))
+        try (ReadExecutionController executionController = command.executionController();
+             PartitionIterator iterator = command.executeInternal(executionController))
         {
             while (iterator.hasNext())
             {
@@ -339,7 +353,8 @@
 
     public static Row getOnlyRowUnfiltered(ReadCommand cmd)
     {
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup(); UnfilteredPartitionIterator iterator = cmd.executeLocally(orderGroup))
+        try (ReadExecutionController executionController = cmd.executionController();
+             UnfilteredPartitionIterator iterator = cmd.executeLocally(executionController))
         {
             assert iterator.hasNext() : "Expecting one row in one partition but got nothing";
             try (UnfilteredRowIterator partition = iterator.next())
@@ -356,7 +371,8 @@
 
     public static Row getOnlyRow(ReadCommand cmd)
     {
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup(); PartitionIterator iterator = cmd.executeInternal(orderGroup))
+        try (ReadExecutionController executionController = cmd.executionController();
+             PartitionIterator iterator = cmd.executeInternal(executionController))
         {
             assert iterator.hasNext() : "Expecting one row in one partition but got nothing";
             try (RowIterator partition = iterator.next())
@@ -372,7 +388,8 @@
 
     public static ImmutableBTreePartition getOnlyPartitionUnfiltered(ReadCommand cmd)
     {
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup(); UnfilteredPartitionIterator iterator = cmd.executeLocally(orderGroup))
+        try (ReadExecutionController executionController = cmd.executionController();
+             UnfilteredPartitionIterator iterator = cmd.executeLocally(executionController))
         {
             assert iterator.hasNext() : "Expecting a single partition but got nothing";
             try (UnfilteredRowIterator partition = iterator.next())
@@ -385,7 +402,8 @@
 
     public static FilteredPartition getOnlyPartition(ReadCommand cmd)
     {
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup(); PartitionIterator iterator = cmd.executeInternal(orderGroup))
+        try (ReadExecutionController executionController = cmd.executionController();
+             PartitionIterator iterator = cmd.executeInternal(executionController))
         {
             assert iterator.hasNext() : "Expecting a single partition but got nothing";
             try (RowIterator partition = iterator.next())
@@ -454,19 +472,41 @@
     public static boolean equal(UnfilteredRowIterator a, UnfilteredRowIterator b)
     {
         return Objects.equals(a.columns(), b.columns())
-            && Objects.equals(a.metadata(), b.metadata())
+            && Objects.equals(a.stats(), b.stats())
+            && sameContent(a, b);
+    }
+
+    // Test equality of the iterators, but without caring too much about the "metadata" of said iterator. This is often
+    // what we want in tests. In particular, the columns() reported by the iterators will sometimes differ because they
+    // are a superset of what the iterator actually contains, and depending on the method used to get each iterator
+    // tested, one may include a defined column the other don't while there is not actual content for that column.
+    public static boolean sameContent(UnfilteredRowIterator a, UnfilteredRowIterator b)
+    {
+        return Objects.equals(a.metadata(), b.metadata())
             && Objects.equals(a.isReverseOrder(), b.isReverseOrder())
             && Objects.equals(a.partitionKey(), b.partitionKey())
             && Objects.equals(a.partitionLevelDeletion(), b.partitionLevelDeletion())
             && Objects.equals(a.staticRow(), b.staticRow())
-            && Objects.equals(a.stats(), b.stats())
             && Iterators.elementsEqual(a, b);
     }
 
+    public static boolean sameContent(Mutation a, Mutation b)
+    {
+        if (!a.key().equals(b.key()) || !a.getColumnFamilyIds().equals(b.getColumnFamilyIds()))
+            return false;
+
+        for (UUID cfId : a.getColumnFamilyIds())
+        {
+            if (!sameContent(a.getPartitionUpdate(cfId).unfilteredIterator(), b.getPartitionUpdate(cfId).unfilteredIterator()))
+                return false;
+        }
+        return true;
+    }
+
     // moved & refactored from KeyspaceTest in < 3.0
     public static void assertColumns(Row row, String... expectedColumnNames)
     {
-        Iterator<Cell> cells = row == null ? Iterators.<Cell>emptyIterator() : row.cells().iterator();
+        Iterator<Cell> cells = row == null ? Collections.emptyIterator() : row.cells().iterator();
         String[] actual = Iterators.toArray(Iterators.transform(cells, new Function<Cell, String>()
         {
             public String apply(Cell cell)
@@ -572,10 +612,9 @@
         AssertionError e = runCatchingAssertionError(test);
         if (e == null)
             return;     // success
-        System.err.format("Test failed. %s%n"
-                        + "Re-running %d times to verify it isn't failing more often than it should.%n"
-                        + "Failure was: %s%n", message, rerunsOnFailure, e);
-        e.printStackTrace();
+
+        logger.info("Test failed. {}", message, e);
+        logger.info("Re-running {} times to verify it isn't failing more often than it should.", rerunsOnFailure);
 
         int rerunsFailed = 0;
         for (int i = 0; i < rerunsOnFailure; ++i)
@@ -585,15 +624,17 @@
             {
                 ++rerunsFailed;
                 e.addSuppressed(t);
+
+                logger.debug("Test failed again, total num failures: {}", rerunsFailed, t);
             }
         }
         if (rerunsFailed > 0)
         {
-            System.err.format("Test failed in %d of the %d reruns.%n", rerunsFailed, rerunsOnFailure);
+            logger.error("Test failed in {} of the {} reruns.", rerunsFailed, rerunsOnFailure);
             throw e;
         }
 
-        System.err.println("All reruns succeeded. Failure treated as flake.");
+        logger.info("All reruns succeeded. Failure treated as flake.");
     }
 
     // for use with Optional in tests, can be used as an argument to orElseThrow
@@ -627,9 +668,9 @@
 
     public static UnfilteredPartitionIterator executeLocally(PartitionRangeReadCommand command,
                                                              ColumnFamilyStore cfs,
-                                                             ReadOrderGroup orderGroup)
+                                                             ReadExecutionController controller)
     {
-        return command.queryStorage(cfs, orderGroup);
+        return command.queryStorage(cfs, controller);
     }
 
     public static Closeable markDirectoriesUnwriteable(ColumnFamilyStore cfs)
@@ -648,4 +689,27 @@
         }
         return () -> DisallowedDirectories.clearUnwritableUnsafe();
     }
+
+    public static PagingState makeSomePagingState(ProtocolVersion protocolVersion)
+    {
+        return makeSomePagingState(protocolVersion, Integer.MAX_VALUE);
+    }
+
+    public static PagingState makeSomePagingState(ProtocolVersion protocolVersion, int remainingInPartition)
+    {
+        CFMetaData metadata = CFMetaData.Builder.create("ks", "tbl")
+                                                .addPartitionKey("k", AsciiType.instance)
+                                                .addClusteringColumn("c1", AsciiType.instance)
+                                                .addClusteringColumn("c1", Int32Type.instance)
+                                                .addRegularColumn("myCol", AsciiType.instance)
+                                                .build();
+
+        ByteBuffer pk = ByteBufferUtil.bytes("someKey");
+
+        ColumnDefinition def = metadata.getColumnDefinition(new ColumnIdentifier("myCol", false));
+        Clustering c = Clustering.make(ByteBufferUtil.bytes("c1"), ByteBufferUtil.bytes(42));
+        Row row = BTreeRow.singleCellRow(c, BufferCell.live(def, 0, ByteBufferUtil.EMPTY_BYTE_BUFFER));
+        PagingState.RowMark mark = PagingState.RowMark.create(metadata, row, protocolVersion);
+        return new PagingState(pk, mark, 10, remainingInPartition);
+    }
 }
diff --git a/test/unit/org/apache/cassandra/auth/AuthCacheTest.java b/test/unit/org/apache/cassandra/auth/AuthCacheTest.java
new file mode 100644
index 0000000..c99438f7
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/AuthCacheTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.auth;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.junit.Test;
+
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.exceptions.UnavailableException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class AuthCacheTest
+{
+    private int loadCounter = 0;
+    private int validity = 2000;
+    private boolean isCacheEnabled = true;
+
+    @Test
+    public void testCacheLoaderIsCalledOnFirst()
+    {
+        TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
+
+        int result = authCache.get("10");
+
+        assertEquals(10, result);
+        assertEquals(1, loadCounter);
+    }
+
+    @Test
+    public void testCacheLoaderIsNotCalledOnSecond()
+    {
+        TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
+        authCache.get("10");
+        assertEquals(1, loadCounter);
+
+        int result = authCache.get("10");
+
+        assertEquals(10, result);
+        assertEquals(1, loadCounter);
+    }
+
+    @Test
+    public void testCacheLoaderIsAlwaysCalledWhenDisabled()
+    {
+        isCacheEnabled = false;
+        TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
+
+        authCache.get("10");
+        int result = authCache.get("10");
+
+        assertEquals(10, result);
+        assertEquals(2, loadCounter);
+    }
+
+    @Test
+    public void testCacheLoaderIsAlwaysCalledWhenValidityIsZero()
+    {
+        setValidity(0);
+        TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
+
+        authCache.get("10");
+        int result = authCache.get("10");
+
+        assertEquals(10, result);
+        assertEquals(2, loadCounter);
+    }
+
+    @Test
+    public void testCacheLoaderIsCalledAfterFullInvalidate()
+    {
+        TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
+        authCache.get("10");
+
+        authCache.invalidate();
+        int result = authCache.get("10");
+
+        assertEquals(10, result);
+        assertEquals(2, loadCounter);
+    }
+
+    @Test
+    public void testCacheLoaderIsCalledAfterInvalidateKey()
+    {
+        TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
+        authCache.get("10");
+
+        authCache.invalidate("10");
+        int result = authCache.get("10");
+
+        assertEquals(10, result);
+        assertEquals(2, loadCounter);
+    }
+
+    @Test(expected = UnavailableException.class)
+    public void testCassandraExceptionPassThroughWhenCacheEnabled()
+    {
+        TestCache<String, Integer> cache = new TestCache<>(s -> {
+            throw new UnavailableException(ConsistencyLevel.QUORUM, 3, 1);
+        }, this::setValidity, () -> validity, () -> isCacheEnabled);
+
+        cache.get("expect-exception");
+    }
+
+    @Test(expected = UnavailableException.class)
+    public void testCassandraExceptionPassThroughWhenCacheDisable()
+    {
+        isCacheEnabled = false;
+        TestCache<String, Integer> cache = new TestCache<>(s -> {
+            throw new UnavailableException(ConsistencyLevel.QUORUM, 3, 1);
+        }, this::setValidity, () -> validity, () -> isCacheEnabled);
+
+        cache.get("expect-exception");
+    }
+
+    @Test
+    public void testCassandraExceptionPassThroughWhenCacheRefreshed() throws InterruptedException
+    {
+        setValidity(50);
+        TestCache<String, Integer> cache = new TestCache<>(this::countingLoaderWithException, this::setValidity, () -> validity, () -> isCacheEnabled);
+        cache.get("10");
+
+        // wait until the cached record expires
+        Thread.sleep(60);
+
+        for (int i = 1; i <= 5; i++)
+        {
+            try
+            {
+                cache.get("10");
+                fail("Did not get expected Exception on attempt " + i);
+            }
+            catch (UnavailableException expected)
+            {
+            }
+        }
+    }
+
+    private void setValidity(int validity)
+    {
+        this.validity = validity;
+    }
+
+    private Integer countingLoader(String s)
+    {
+        loadCounter++;
+        return Integer.parseInt(s);
+    }
+
+    private Integer countingLoaderWithException(String s)
+    {
+        Integer loadedValue = countingLoader(s);
+
+        if (loadCounter > 1)
+            throw new UnavailableException(ConsistencyLevel.QUORUM, 3, 1);
+
+        return loadedValue;
+    }
+
+    private static class TestCache<K, V> extends AuthCache<K, V>
+    {
+        private static int nameCounter = 0; // Allow us to create many instances of cache with same name prefix
+
+        TestCache(Function<K, V> loadFunction, Consumer<Integer> setValidityDelegate, Supplier<Integer> getValidityDelegate, Supplier<Boolean> cacheEnabledDelegate)
+        {
+            super("TestCache" + nameCounter++,
+                  setValidityDelegate,
+                  getValidityDelegate,
+                  (updateInterval) -> {
+                  },
+                  () -> 1000,
+                  (maxEntries) -> {
+                  },
+                  () -> 10,
+                  loadFunction,
+                  cacheEnabledDelegate);
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/auth/PasswordAuthenticatorTest.java b/test/unit/org/apache/cassandra/auth/PasswordAuthenticatorTest.java
index 3909642..ce4d924 100644
--- a/test/unit/org/apache/cassandra/auth/PasswordAuthenticatorTest.java
+++ b/test/unit/org/apache/cassandra/auth/PasswordAuthenticatorTest.java
@@ -30,6 +30,7 @@
 import com.datastax.driver.core.PlainTextAuthProvider;
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.exceptions.AuthenticationException;
 import org.apache.cassandra.exceptions.ConfigurationException;
@@ -162,7 +163,7 @@
     @BeforeClass
     public static void setUp()
     {
-        SchemaLoader.createKeyspace(AuthKeyspace.NAME,
+        SchemaLoader.createKeyspace(SchemaConstants.AUTH_KEYSPACE_NAME,
                                     KeyspaceParams.simple(1),
                                     Iterables.toArray(AuthKeyspace.metadata().tables, CFMetaData.class));
         authenticator.setup();
@@ -171,6 +172,6 @@
     @AfterClass
     public static void tearDown()
     {
-        schemaChange("DROP KEYSPACE " + AuthKeyspace.NAME);
+        schemaChange("DROP KEYSPACE " + SchemaConstants.AUTH_KEYSPACE_NAME);
     }
 }
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/auth/ResourcesTest.java b/test/unit/org/apache/cassandra/auth/ResourcesTest.java
index b3d85cb..bb279d9 100644
--- a/test/unit/org/apache/cassandra/auth/ResourcesTest.java
+++ b/test/unit/org/apache/cassandra/auth/ResourcesTest.java
@@ -76,4 +76,16 @@
         assertEquals("functions/ks1/f1[org.apache.cassandra.db.marshal.Int32Type^org.apache.cassandra.db.marshal.DoubleType]",
                 FunctionResource.function("ks1", "f1", Arrays.asList(Int32Type.instance, DoubleType.instance)).getName());
     }
+
+    @Test
+    public void testJMXResourceNameConversion()
+    {
+        assertEquals(JMXResource.root(), Resources.fromName("mbean"));
+        assertEquals("mbean", JMXResource.root().getName());
+
+        assertEquals(JMXResource.mbean("org.apache.cassandra.auth:type=*"),
+                Resources.fromName("mbean/org.apache.cassandra.auth:type=*"));
+        assertEquals("mbean/org.apache.cassandra.auth:type=*",
+                JMXResource.mbean("org.apache.cassandra.auth:type=*").getName());
+    }
 }
diff --git a/test/unit/org/apache/cassandra/auth/StubAuthorizer.java b/test/unit/org/apache/cassandra/auth/StubAuthorizer.java
new file mode 100644
index 0000000..8e0d141
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/StubAuthorizer.java
@@ -0,0 +1,120 @@
+/*
+ * 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.auth;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
+import org.apache.cassandra.utils.Pair;
+
+public class StubAuthorizer implements IAuthorizer
+{
+    Map<Pair<String, IResource>, Set<Permission>> userPermissions = new HashMap<>();
+
+    public void clear()
+    {
+        userPermissions.clear();
+    }
+
+    public Set<Permission> authorize(AuthenticatedUser user, IResource resource)
+    {
+        Pair<String, IResource> key = Pair.create(user.getName(), resource);
+        Set<Permission> perms = userPermissions.get(key);
+        return perms != null ? perms : Collections.emptySet();
+    }
+
+    public void grant(AuthenticatedUser performer,
+                      Set<Permission> permissions,
+                      IResource resource,
+                      RoleResource grantee) throws RequestValidationException, RequestExecutionException
+    {
+        Pair<String, IResource> key = Pair.create(grantee.getRoleName(), resource);
+        Set<Permission> perms = userPermissions.get(key);
+        if (null == perms)
+        {
+            perms = new HashSet<>();
+            userPermissions.put(key, perms);
+        }
+        perms.addAll(permissions);
+    }
+
+    public void revoke(AuthenticatedUser performer,
+                       Set<Permission> permissions,
+                       IResource resource,
+                       RoleResource revokee) throws RequestValidationException, RequestExecutionException
+    {
+        Pair<String, IResource> key = Pair.create(revokee.getRoleName(), resource);
+        Set<Permission> perms = userPermissions.get(key);
+        if (null != perms)
+        {
+            perms.removeAll(permissions);
+            if (perms.isEmpty())
+                userPermissions.remove(key);
+        }
+    }
+
+    public Set<PermissionDetails> list(AuthenticatedUser performer,
+                                       Set<Permission> permissions,
+                                       IResource resource,
+                                       RoleResource grantee) throws RequestValidationException, RequestExecutionException
+    {
+        return userPermissions.entrySet()
+                              .stream()
+                              .filter(entry -> entry.getKey().left.equals(grantee.getRoleName())
+                                               && (resource == null || entry.getKey().right.equals(resource)))
+                              .flatMap(entry -> entry.getValue()
+                                                     .stream()
+                                                     .filter(permissions::contains)
+                                                     .map(p -> new PermissionDetails(entry.getKey().left,
+                                                                                     entry.getKey().right,
+                                                                                     p)))
+                              .collect(Collectors.toSet());
+
+    }
+
+    public void revokeAllFrom(RoleResource revokee)
+    {
+        for (Pair<String, IResource> key : userPermissions.keySet())
+            if (key.left.equals(revokee.getRoleName()))
+                userPermissions.remove(key);
+    }
+
+    public void revokeAllOn(IResource droppedResource)
+    {
+        for (Pair<String, IResource> key : userPermissions.keySet())
+            if (key.right.equals(droppedResource))
+                userPermissions.remove(key);
+    }
+
+    public Set<? extends IResource> protectedResources()
+    {
+        return Collections.emptySet();
+    }
+
+    public void validateConfiguration() throws ConfigurationException
+    {
+    }
+
+    public void setup()
+    {
+    }
+}
diff --git a/test/unit/org/apache/cassandra/auth/jmx/AuthorizationProxyTest.java b/test/unit/org/apache/cassandra/auth/jmx/AuthorizationProxyTest.java
new file mode 100644
index 0000000..e68ef20
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/jmx/AuthorizationProxyTest.java
@@ -0,0 +1,581 @@
+/*
+ * 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.auth.jmx;
+
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+import javax.security.auth.Subject;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.auth.*;
+import org.apache.cassandra.config.DatabaseDescriptor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class AuthorizationProxyTest
+{
+    @BeforeClass
+    public static void setup() throws Exception
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
+    JMXResource osBean = JMXResource.mbean("java.lang:type=OperatingSystem");
+    JMXResource runtimeBean = JMXResource.mbean("java.lang:type=Runtime");
+    JMXResource threadingBean = JMXResource.mbean("java.lang:type=Threading");
+    JMXResource javaLangWildcard = JMXResource.mbean("java.lang:type=*");
+
+    JMXResource hintsBean = JMXResource.mbean("org.apache.cassandra.hints:type=HintsService");
+    JMXResource batchlogBean = JMXResource.mbean("org.apache.cassandra.db:type=BatchlogManager");
+    JMXResource customBean = JMXResource.mbean("org.apache.cassandra:type=CustomBean,property=foo");
+    Set<ObjectName> allBeans = objectNames(osBean, runtimeBean, threadingBean, hintsBean, batchlogBean, customBean);
+
+    RoleResource role1 = RoleResource.role("r1");
+
+    @Test
+    public void roleHasRequiredPermission() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, osBean, Permission.SELECT)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> false)
+                                                     .getPermissions(permissions::get)
+                                                     .isAuthzRequired(() -> true)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" }));
+    }
+
+    @Test
+    public void roleDoesNotHaveRequiredPermission() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, osBean, Permission.AUTHORIZE)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> false)
+                                                     .getPermissions(permissions::get)
+                                                     .isAuthzRequired(() -> true).build();
+
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "setAttribute",
+                                    new Object[]{ objectName(osBean), "arch" }));
+    }
+
+    @Test
+    public void roleHasRequiredPermissionOnRootResource() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, JMXResource.root(), Permission.SELECT)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> false)
+                                                     .getPermissions(permissions::get)
+                                                     .isAuthzRequired(() -> true)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" }));
+    }
+
+    @Test
+    public void roleHasOtherPermissionOnRootResource() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, JMXResource.root(), Permission.AUTHORIZE)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> false)
+                                                     .getPermissions(permissions::get)
+                                                     .isAuthzRequired(() -> true)
+                                                     .build();
+
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "invoke",
+                                    new Object[]{ objectName(osBean), "bogusMethod" }));
+    }
+
+    @Test
+    public void roleHasNoPermissions() throws Throwable
+    {
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> false)
+                                                     .getPermissions((role) -> Collections.emptySet())
+                                                     .isAuthzRequired(() -> true)
+                                                     .build();
+
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "getAttribute",
+                                    new Object[]{ objectName(osBean), "arch" }));
+    }
+
+    @Test
+    public void roleHasNoPermissionsButIsSuperuser() throws Throwable
+    {
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> true)
+                                                     .getPermissions((role) -> Collections.emptySet())
+                                                     .isAuthzRequired(() -> true)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" }));
+    }
+
+    @Test
+    public void roleHasNoPermissionsButAuthzNotRequired() throws Throwable
+    {
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> false)
+                                                     .getPermissions((role) -> Collections.emptySet())
+                                                     .isAuthzRequired(() -> false)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" }));
+    }
+
+    @Test
+    public void authorizeWhenSubjectIsNull() throws Throwable
+    {
+        // a null subject indicates that the action is being performed by the
+        // connector itself, so we always authorize it
+        // Verify that the superuser status is never tested as the request returns early
+        // due to the null Subject
+        // Also, hardcode the permissions provider to return an empty set, so we know that
+        // can be doubly sure that it's the null Subject which causes the authz to succeed
+        final AtomicBoolean suStatusChecked = new AtomicBoolean(false);
+        AuthorizationProxy proxy = new ProxyBuilder().getPermissions((role) -> Collections.emptySet())
+                                                     .isAuthzRequired(() -> true)
+                                                     .isSuperuser((role) ->
+                                                                  {
+                                                                      suStatusChecked.set(true);
+                                                                      return false;
+                                                                  })
+                                                     .build();
+
+        assertTrue(proxy.authorize(null,
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" }));
+        assertFalse(suStatusChecked.get());
+    }
+
+    @Test
+    public void rejectWhenSubjectNotAuthenticated() throws Throwable
+    {
+        // Access is denied to a Subject without any associated Principals
+        // Verify that the superuser status is never tested as the request is rejected early
+        // due to the Subject
+        final AtomicBoolean suStatusChecked = new AtomicBoolean(false);
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> true)
+                                                     .isSuperuser((role) ->
+                                                                  {
+                                                                      suStatusChecked.set(true);
+                                                                      return true;
+                                                                  })
+                                                     .build();
+        assertFalse(proxy.authorize(new Subject(),
+                                    "getAttribute",
+                                    new Object[]{ objectName(osBean), "arch" }));
+        assertFalse(suStatusChecked.get());
+    }
+
+    @Test
+    public void authorizeWhenWildcardGrantCoversExactTarget() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, javaLangWildcard, Permission.SELECT)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> true)
+                                                     .isSuperuser((role) -> false)
+                                                     .getPermissions(permissions::get)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" }));
+    }
+
+    @Test
+    public void rejectWhenWildcardGrantDoesNotCoverExactTarget() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, javaLangWildcard, Permission.SELECT)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> true)
+                                                     .isSuperuser((role) -> false)
+                                                     .getPermissions(permissions::get)
+                                                     .build();
+
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "getAttribute",
+                                    new Object[]{ objectName(customBean), "arch" }));
+    }
+
+    @Test
+    public void authorizeWhenWildcardGrantCoversWildcardTarget() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, javaLangWildcard, Permission.DESCRIBE)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> true)
+                                                     .isSuperuser((role) -> false)
+                                                     .getPermissions(permissions::get)
+                                                     .queryNames(matcher(allBeans))
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "queryNames",
+                                   new Object[]{ objectName(javaLangWildcard), null }));
+    }
+
+    @Test
+    public void rejectWhenWildcardGrantIsDisjointWithWildcardTarget() throws Throwable
+    {
+        JMXResource customWildcard = JMXResource.mbean("org.apache.cassandra:*");
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, customWildcard, Permission.DESCRIBE)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> true)
+                                                     .isSuperuser((role) -> false)
+                                                     .getPermissions(permissions::get)
+                                                     .queryNames(matcher(allBeans))
+                                                     .build();
+
+        // the grant on org.apache.cassandra:* shouldn't permit us to invoke queryNames with java.lang:*
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "queryNames",
+                                    new Object[]{ objectName(javaLangWildcard), null }));
+    }
+
+    @Test
+    public void rejectWhenWildcardGrantIntersectsWithWildcardTarget() throws Throwable
+    {
+        // in this test, permissions are granted on org.apache.cassandra:type=CustomBean,property=*
+        // and all beans in the org.apache.cassandra.hints domain, but
+        // but the target of the invocation is org.apache.cassandra*:*
+        // i.e. the subject has permissions on all CustomBeans and on the HintsService bean, but is
+        // attempting to query all names in the org.apache.cassandra* domain. The operation should
+        // be rejected as the permissions don't cover all known beans matching that domain, due to
+        // the BatchLogManager bean.
+
+        JMXResource allCustomBeans = JMXResource.mbean("org.apache.cassandra:type=CustomBean,property=*");
+        JMXResource allHintsBeans = JMXResource.mbean("org.apache.cassandra.hints:*");
+        ObjectName allCassandraBeans = ObjectName.getInstance("org.apache.cassandra*:*");
+
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, ImmutableSet.of(permission(role1, allCustomBeans, Permission.DESCRIBE),
+                                                   permission(role1, allHintsBeans, Permission.DESCRIBE)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> true)
+                                                     .isSuperuser((role) -> false)
+                                                     .getPermissions(permissions::get)
+                                                     .queryNames(matcher(allBeans))
+                                                     .build();
+
+        // the grant on org.apache.cassandra:* shouldn't permit us to invoke queryNames with java.lang:*
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "queryNames",
+                                    new Object[]{ allCassandraBeans, null }));
+    }
+
+    @Test
+    public void authorizeOnTargetWildcardWithPermissionOnRoot() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, JMXResource.root(), Permission.SELECT)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> true)
+                                                     .isSuperuser((role) -> false)
+                                                     .getPermissions(permissions::get)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(javaLangWildcard), "arch" }));
+    }
+
+    @Test
+    public void rejectInvocationOfUnknownMethod() throws Throwable
+    {
+        // Grant ALL permissions on the root resource, so we know that it's
+        // the unknown method that causes the authz rejection. Of course, this
+        // isn't foolproof but it's something.
+        Set<PermissionDetails> allPerms = Permission.ALL.stream()
+                                                        .map(perm -> permission(role1, JMXResource.root(), perm))
+                                                        .collect(Collectors.toSet());
+        Map<RoleResource, Set<PermissionDetails>> permissions = ImmutableMap.of(role1, allPerms);
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> true)
+                                                     .isSuperuser((role) -> false)
+                                                     .getPermissions(permissions::get)
+                                                     .build();
+
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "unKnownMethod",
+                                    new Object[] { ObjectName.getInstance(osBean.getObjectName()) }));
+    }
+
+    @Test
+    public void rejectInvocationOfRestrictedMethods() throws Throwable
+    {
+        String[] methods = { "createMBean",
+                             "deserialize",
+                             "getClassLoader",
+                             "getClassLoaderFor",
+                             "instantiate",
+                             "registerMBean",
+                             "unregisterMBean" };
+
+        // Hardcode the superuser status check to return true, so any allowed method can be invoked.
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> true)
+                                                     .isSuperuser((role) -> true)
+                                                     .build();
+
+        for (String method : methods)
+            // the arguments array isn't significant, so it can just be empty
+            assertFalse(proxy.authorize(subject(role1.getRoleName()), method, new Object[0]));
+    }
+
+    @Test
+    public void authorizeMethodsWithoutMBeanArgumentIfPermissionsGranted() throws Throwable
+    {
+        // Certain methods on MBeanServer don't take an ObjectName as their first argument.
+        // These methods are characterised by AuthorizationProxy as being concerned with
+        // the MBeanServer itself, as opposed to a specific managed bean. Of these methods,
+        // only those considered "descriptive" are allowed to be invoked by remote users.
+        // These require the DESCRIBE permission on the root JMXResource.
+        testNonMbeanMethods(true);
+    }
+
+    @Test
+    public void rejectMethodsWithoutMBeanArgumentIfPermissionsNotGranted() throws Throwable
+    {
+        testNonMbeanMethods(false);
+    }
+
+    @Test
+    public void rejectWhenAuthSetupIsNotComplete() throws Throwable
+    {
+        // IAuthorizer & IRoleManager should not be considered ready to use until
+        // we know that auth setup has completed. So, even though the IAuthorizer
+        // would theoretically grant access, the auth proxy should deny it if setup
+        // hasn't finished.
+
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+        ImmutableMap.of(role1, Collections.singleton(permission(role1, osBean, Permission.SELECT)));
+
+        // verify that access is granted when setup is complete
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> false)
+                                                     .getPermissions(permissions::get)
+                                                     .isAuthzRequired(() -> true)
+                                                     .isAuthSetupComplete(() -> true)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" }));
+
+        // and denied when it isn't
+        proxy = new ProxyBuilder().isSuperuser((role) -> false)
+                                  .getPermissions(permissions::get)
+                                  .isAuthzRequired(() -> true)
+                                  .isAuthSetupComplete(() -> false)
+                                  .build();
+
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" }));
+    }
+
+    private void testNonMbeanMethods(boolean withPermission)
+    {
+        String[] methods = { "getDefaultDomain",
+                             "getDomains",
+                             "getMBeanCount",
+                             "hashCode",
+                             "queryMBeans",
+                             "queryNames",
+                             "toString" };
+
+
+        ProxyBuilder builder = new ProxyBuilder().isAuthzRequired(() -> true).isSuperuser((role) -> false);
+        if (withPermission)
+        {
+            Map<RoleResource, Set<PermissionDetails>> permissions =
+                ImmutableMap.of(role1, ImmutableSet.of(permission(role1, JMXResource.root(), Permission.DESCRIBE)));
+            builder.getPermissions(permissions::get);
+        }
+        else
+        {
+            builder.getPermissions((role) -> Collections.emptySet());
+        }
+        AuthorizationProxy proxy = builder.build();
+
+        for (String method : methods)
+            assertEquals(withPermission, proxy.authorize(subject(role1.getRoleName()), method, new Object[]{ null }));
+
+        // non-allowed methods should be rejected regardless.
+        // This isn't exactly comprehensive, but it's better than nothing
+        String[] notAllowed = { "fooMethod", "barMethod", "bazMethod" };
+        for (String method : notAllowed)
+            assertFalse(proxy.authorize(subject(role1.getRoleName()), method, new Object[]{ null }));
+    }
+
+    // provides a simple matching function which can be substituted for the proxy's queryMBeans
+    // utility (which by default just delegates to the MBeanServer)
+    // This function just iterates over a supplied set of ObjectNames and filters out those
+    // to which the target name *doesn't* apply
+    private static Function<ObjectName, Set<ObjectName>> matcher(Set<ObjectName> allBeans)
+    {
+        return (target) -> allBeans.stream()
+                                   .filter(target::apply)
+                                   .collect(Collectors.toSet());
+    }
+
+    private static PermissionDetails permission(RoleResource grantee, IResource resource, Permission permission)
+    {
+        return new PermissionDetails(grantee.getRoleName(), resource, permission);
+    }
+
+    private static Subject subject(String roleName)
+    {
+        Subject subject = new Subject();
+        subject.getPrincipals().add(new CassandraPrincipal(roleName));
+        return subject;
+    }
+
+    private static ObjectName objectName(JMXResource resource) throws MalformedObjectNameException
+    {
+        return ObjectName.getInstance(resource.getObjectName());
+    }
+
+    private static Set<ObjectName> objectNames(JMXResource... resource)
+    {
+        Set<ObjectName> names = new HashSet<>();
+        try
+        {
+            for (JMXResource r : resource)
+                names.add(objectName(r));
+        }
+        catch (MalformedObjectNameException e)
+        {
+            fail("JMXResource returned invalid object name: " + e.getMessage());
+        }
+        return names;
+    }
+
+    public static class ProxyBuilder
+    {
+        Function<RoleResource, Set<PermissionDetails>> getPermissions;
+        Function<ObjectName, Set<ObjectName>> queryNames;
+        Function<RoleResource, Boolean> isSuperuser;
+        Supplier<Boolean> isAuthzRequired;
+        Supplier<Boolean> isAuthSetupComplete = () -> true;
+
+        AuthorizationProxy build()
+        {
+            InjectableAuthProxy proxy = new InjectableAuthProxy();
+
+            if (getPermissions != null)
+                proxy.setGetPermissions(getPermissions);
+
+            if (queryNames != null)
+                proxy.setQueryNames(queryNames);
+
+            if (isSuperuser != null)
+                proxy.setIsSuperuser(isSuperuser);
+
+            if (isAuthzRequired != null)
+                proxy.setIsAuthzRequired(isAuthzRequired);
+
+            proxy.setIsAuthSetupComplete(isAuthSetupComplete);
+
+            return proxy;
+        }
+
+        ProxyBuilder getPermissions(Function<RoleResource, Set<PermissionDetails>> f)
+        {
+            getPermissions = f;
+            return this;
+        }
+
+        ProxyBuilder queryNames(Function<ObjectName, Set<ObjectName>> f)
+        {
+            queryNames = f;
+            return this;
+        }
+
+        ProxyBuilder isSuperuser(Function<RoleResource, Boolean> f)
+        {
+            isSuperuser = f;
+            return this;
+        }
+
+        ProxyBuilder isAuthzRequired(Supplier<Boolean> s)
+        {
+            isAuthzRequired = s;
+            return this;
+        }
+
+        ProxyBuilder isAuthSetupComplete(Supplier<Boolean> s)
+        {
+            isAuthSetupComplete = s;
+            return this;
+        }
+
+        private static class InjectableAuthProxy extends AuthorizationProxy
+        {
+            void setGetPermissions(Function<RoleResource, Set<PermissionDetails>> f)
+            {
+                this.getPermissions = f;
+            }
+
+            void setQueryNames(Function<ObjectName, Set<ObjectName>> f)
+            {
+                this.queryNames = f;
+            }
+
+            void setIsSuperuser(Function<RoleResource, Boolean> f)
+            {
+                this.isSuperuser = f;
+            }
+
+            void setIsAuthzRequired(Supplier<Boolean> s)
+            {
+                this.isAuthzRequired = s;
+            }
+
+            void setIsAuthSetupComplete(Supplier<Boolean> s)
+            {
+                this.isAuthSetupComplete = s;
+            }
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/auth/jmx/JMXAuthTest.java b/test/unit/org/apache/cassandra/auth/jmx/JMXAuthTest.java
new file mode 100644
index 0000000..08a89df
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/jmx/JMXAuthTest.java
@@ -0,0 +1,280 @@
+/*
+ * 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.auth.jmx;
+
+import java.lang.reflect.Field;
+import java.nio.file.Paths;
+import java.rmi.server.RMISocketFactory;
+import java.util.HashMap;
+import java.util.Map;
+import javax.management.JMX;
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+import javax.management.remote.*;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.auth.*;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.db.ColumnFamilyStoreMBean;
+import org.apache.cassandra.utils.JMXServerUtils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class JMXAuthTest extends CQLTester
+{
+    private static JMXConnectorServer jmxServer;
+    private static MBeanServerConnection connection;
+    private RoleResource role;
+    private String tableName;
+    private JMXResource tableMBean;
+
+    @FunctionalInterface
+    private interface MBeanAction
+    {
+        void execute();
+    }
+
+    @BeforeClass
+    public static void setupClass() throws Exception
+    {
+        DatabaseDescriptor.daemonInitialization();
+        setupAuthorizer();
+        setupJMXServer();
+    }
+
+    private static void setupAuthorizer()
+    {
+        try
+        {
+            IAuthorizer authorizer = new StubAuthorizer();
+            Field authorizerField = DatabaseDescriptor.class.getDeclaredField("authorizer");
+            authorizerField.setAccessible(true);
+            authorizerField.set(null, authorizer);
+            DatabaseDescriptor.setPermissionsValidity(0);
+        }
+        catch (IllegalAccessException | NoSuchFieldException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static void setupJMXServer() throws Exception
+    {
+        String config = Paths.get(ClassLoader.getSystemResource("auth/cassandra-test-jaas.conf").toURI()).toString();
+        System.setProperty("com.sun.management.jmxremote.authenticate", "true");
+        System.setProperty("java.security.auth.login.config", config);
+        System.setProperty("cassandra.jmx.remote.login.config", "TestLogin");
+        System.setProperty("cassandra.jmx.authorizer", NoSuperUserAuthorizationProxy.class.getName());
+        jmxServer = JMXServerUtils.createJMXServer(9999, true);
+        jmxServer.start();
+
+        JMXServiceURL jmxUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi");
+        Map<String, Object> env = new HashMap<>();
+        env.put("com.sun.jndi.rmi.factory.socket", RMISocketFactory.getDefaultSocketFactory());
+        JMXConnector jmxc = JMXConnectorFactory.connect(jmxUrl, env);
+        connection = jmxc.getMBeanServerConnection();
+    }
+
+    @Before
+    public void setup() throws Throwable
+    {
+        role = RoleResource.role("test_role");
+        clearAllPermissions();
+        tableName = createTable("CREATE TABLE %s (k int, v int, PRIMARY KEY (k))");
+        tableMBean = JMXResource.mbean(String.format("org.apache.cassandra.db:type=Tables,keyspace=%s,table=%s",
+                                                     KEYSPACE, tableName));
+    }
+
+    @Test
+    public void readAttribute() throws Throwable
+    {
+        ColumnFamilyStoreMBean proxy = JMX.newMBeanProxy(connection,
+                                                         ObjectName.getInstance(tableMBean.getObjectName()),
+                                                         ColumnFamilyStoreMBean.class);
+
+        // grant SELECT on a single specific Table mbean
+        assertPermissionOnResource(Permission.SELECT, tableMBean, proxy::getTableName);
+
+        // grant SELECT on all Table mbeans in named keyspace
+        clearAllPermissions();
+        JMXResource allTablesInKeyspace = JMXResource.mbean(String.format("org.apache.cassandra.db:type=Tables,keyspace=%s,*",
+                                                                          KEYSPACE));
+        assertPermissionOnResource(Permission.SELECT, allTablesInKeyspace, proxy::getTableName);
+
+        // grant SELECT on all Table mbeans
+        clearAllPermissions();
+        JMXResource allTables = JMXResource.mbean("org.apache.cassandra.db:type=Tables,*");
+        assertPermissionOnResource(Permission.SELECT, allTables, proxy::getTableName);
+
+        // grant SELECT ON ALL MBEANS
+        clearAllPermissions();
+        assertPermissionOnResource(Permission.SELECT, JMXResource.root(), proxy::getTableName);
+    }
+
+    @Test
+    public void writeAttribute() throws Throwable
+    {
+        ColumnFamilyStoreMBean proxy = JMX.newMBeanProxy(connection,
+                                                         ObjectName.getInstance(tableMBean.getObjectName()),
+                                                         ColumnFamilyStoreMBean.class);
+        MBeanAction action = () -> proxy.setMinimumCompactionThreshold(4);
+
+        // grant MODIFY on a single specific Table mbean
+        assertPermissionOnResource(Permission.MODIFY, tableMBean, action);
+
+        // grant MODIFY on all Table mbeans in named keyspace
+        clearAllPermissions();
+        JMXResource allTablesInKeyspace = JMXResource.mbean(String.format("org.apache.cassandra.db:type=Tables,keyspace=%s,*",
+                                                                          KEYSPACE));
+        assertPermissionOnResource(Permission.MODIFY, allTablesInKeyspace, action);
+
+        // grant MODIFY on all Table mbeans
+        clearAllPermissions();
+        JMXResource allTables = JMXResource.mbean("org.apache.cassandra.db:type=Tables,*");
+        assertPermissionOnResource(Permission.MODIFY, allTables, action);
+
+        // grant MODIFY ON ALL MBEANS
+        clearAllPermissions();
+        assertPermissionOnResource(Permission.MODIFY, JMXResource.root(), action);
+    }
+
+    @Test
+    public void executeMethod() throws Throwable
+    {
+        ColumnFamilyStoreMBean proxy = JMX.newMBeanProxy(connection,
+                                                         ObjectName.getInstance(tableMBean.getObjectName()),
+                                                         ColumnFamilyStoreMBean.class);
+
+        // grant EXECUTE on a single specific Table mbean
+        assertPermissionOnResource(Permission.EXECUTE, tableMBean, proxy::estimateKeys);
+
+        // grant EXECUTE on all Table mbeans in named keyspace
+        clearAllPermissions();
+        JMXResource allTablesInKeyspace = JMXResource.mbean(String.format("org.apache.cassandra.db:type=Tables,keyspace=%s,*",
+                                                                          KEYSPACE));
+        assertPermissionOnResource(Permission.EXECUTE, allTablesInKeyspace, proxy::estimateKeys);
+
+        // grant EXECUTE on all Table mbeans
+        clearAllPermissions();
+        JMXResource allTables = JMXResource.mbean("org.apache.cassandra.db:type=Tables,*");
+        assertPermissionOnResource(Permission.EXECUTE, allTables, proxy::estimateKeys);
+
+        // grant EXECUTE ON ALL MBEANS
+        clearAllPermissions();
+        assertPermissionOnResource(Permission.EXECUTE, JMXResource.root(), proxy::estimateKeys);
+    }
+
+    private void assertPermissionOnResource(Permission permission,
+                                            JMXResource resource,
+                                            MBeanAction action)
+    {
+        assertUnauthorized(action);
+        grantPermission(permission, resource, role);
+        assertAuthorized(action);
+    }
+
+    private void grantPermission(Permission permission, JMXResource resource, RoleResource role)
+    {
+        DatabaseDescriptor.getAuthorizer().grant(AuthenticatedUser.SYSTEM_USER,
+                                                 ImmutableSet.of(permission),
+                                                 resource,
+                                                 role);
+    }
+
+    private void assertAuthorized(MBeanAction action)
+    {
+        action.execute();
+    }
+
+    private void assertUnauthorized(MBeanAction action)
+    {
+        try
+        {
+            action.execute();
+            fail("Expected an UnauthorizedException, but none was thrown");
+        }
+        catch (SecurityException e)
+        {
+            assertEquals("Access Denied", e.getLocalizedMessage());
+        }
+    }
+
+    private void clearAllPermissions()
+    {
+        ((StubAuthorizer) DatabaseDescriptor.getAuthorizer()).clear();
+    }
+
+    public static class StubLoginModule implements LoginModule
+    {
+        private CassandraPrincipal principal;
+        private Subject subject;
+
+        public StubLoginModule(){}
+
+        public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options)
+        {
+            this.subject = subject;
+            principal = new CassandraPrincipal((String)options.get("role_name"));
+        }
+
+        public boolean login() throws LoginException
+        {
+            return true;
+        }
+
+        public boolean commit() throws LoginException
+        {
+            if (!subject.getPrincipals().contains(principal))
+                subject.getPrincipals().add(principal);
+            return true;
+        }
+
+        public boolean abort() throws LoginException
+        {
+            return true;
+        }
+
+        public boolean logout() throws LoginException
+        {
+            return true;
+        }
+    }
+
+    // always answers false to isSuperUser and true to isAuthSetup complete - saves us having to initialize
+    // a real IRoleManager and StorageService for the test
+    public static class NoSuperUserAuthorizationProxy extends AuthorizationProxy
+    {
+        public NoSuperUserAuthorizationProxy()
+        {
+            super();
+            this.isSuperuser = (role) -> false;
+            this.isAuthSetupComplete = () -> true;
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/batchlog/BatchTest.java b/test/unit/org/apache/cassandra/batchlog/BatchTest.java
index b7a4100..4e64ec6 100644
--- a/test/unit/org/apache/cassandra/batchlog/BatchTest.java
+++ b/test/unit/org/apache/cassandra/batchlog/BatchTest.java
@@ -28,6 +28,7 @@
 import org.junit.Test;
 
 import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.Mutation;
@@ -44,6 +45,7 @@
 
 import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 public class BatchTest
 {
@@ -148,6 +150,19 @@
         Iterator<Mutation> it1 = batch1.decodedMutations.iterator();
         Iterator<Mutation> it2 = batch2.decodedMutations.iterator();
         while (it1.hasNext())
-            assertEquals(it1.next().toString(), it2.next().toString());
+        {
+            // We can't simply test the equality of both mutation string representation, that is do:
+            //   assertEquals(it1.next().toString(), it2.next().toString());
+            // because when deserializing from the old format, the returned iterator will always have it's 'columns()'
+            // method return all the table columns (no matter what's the actual content), and the table contains a
+            // 'val0' column we're not setting in that test.
+            //
+            // And it's actually not easy to fix legacy deserialization as we'd need to know which columns are actually
+            // set upfront, which would require use to iterate over the whole content first, which would be costly. And
+            // as the result of 'columns()' is only meant as a superset of the columns in the iterator, we don't bother.
+            Mutation mut1 = it1.next();
+            Mutation mut2 = it2.next();
+            assertTrue(mut1 + " != " + mut2, Util.sameContent(mut1, mut2));
+        }
     }
 }
diff --git a/test/unit/org/apache/cassandra/batchlog/BatchlogManagerTest.java b/test/unit/org/apache/cassandra/batchlog/BatchlogManagerTest.java
index dd5444f..f192bcf 100644
--- a/test/unit/org/apache/cassandra/batchlog/BatchlogManagerTest.java
+++ b/test/unit/org/apache/cassandra/batchlog/BatchlogManagerTest.java
@@ -33,6 +33,7 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.DecoratedKey;
@@ -40,7 +41,7 @@
 import org.apache.cassandra.db.Mutation;
 import org.apache.cassandra.db.RowUpdateBuilder;
 import org.apache.cassandra.db.SystemKeyspace;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.apache.cassandra.db.marshal.BytesType;
 import org.apache.cassandra.db.partitions.ImmutableBTreePartition;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
@@ -72,6 +73,7 @@
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
+        DatabaseDescriptor.daemonInitialization();
         sw = Util.switchPartitioner(Murmur3Partitioner.instance);
         SchemaLoader.prepareServer();
         SchemaLoader.createKeyspace(KEYSPACE1,
@@ -97,8 +99,8 @@
         InetAddress localhost = InetAddress.getByName("127.0.0.1");
         metadata.updateNormalToken(Util.token("A"), localhost);
         metadata.updateHostId(UUIDGen.getTimeUUID(), localhost);
-        Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).truncateBlocking();
-        Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.LEGACY_BATCHLOG).truncateBlocking();
+        Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).truncateBlocking();
+        Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.LEGACY_BATCHLOG).truncateBlocking();
     }
 
     @Test
@@ -171,12 +173,12 @@
 
         if (legacy)
         {
-            Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.LEGACY_BATCHLOG).forceBlockingFlush();
+            Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.LEGACY_BATCHLOG).forceBlockingFlush();
             LegacyBatchlogMigrator.migrate();
         }
 
         // Flush the batchlog to disk (see CASSANDRA-6822).
-        Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).forceBlockingFlush();
+        Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).forceBlockingFlush();
 
         assertEquals(100, BatchlogManager.instance.countAllBatches() - initialAllBatches);
         assertEquals(0, BatchlogManager.instance.getTotalBatchesReplayed() - initialReplayedBatches);
@@ -248,7 +250,7 @@
             if (i == 500)
                 SystemKeyspace.saveTruncationRecord(Keyspace.open(KEYSPACE1).getColumnFamilyStore(CF_STANDARD2),
                                                     timestamp,
-                                                    ReplayPosition.NONE);
+                                                    CommitLogPosition.NONE);
 
             // Adjust the timestamp (slightly) to make the test deterministic.
             if (i >= 500)
@@ -260,7 +262,7 @@
         }
 
         // Flush the batchlog to disk (see CASSANDRA-6822).
-        Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).forceBlockingFlush();
+        Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).forceBlockingFlush();
 
         // Force batchlog replay and wait for it to complete.
         BatchlogManager.instance.startBatchlogReplay().get();
@@ -341,15 +343,15 @@
         }
 
         // Flush the batchlog to disk (see CASSANDRA-6822).
-        Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).forceBlockingFlush();
+        Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).forceBlockingFlush();
 
         assertEquals(1500, BatchlogManager.instance.countAllBatches() - initialAllBatches);
         assertEquals(0, BatchlogManager.instance.getTotalBatchesReplayed() - initialReplayedBatches);
 
-        UntypedResultSet result = executeInternal(String.format("SELECT count(*) FROM \"%s\".\"%s\"", SystemKeyspace.NAME, SystemKeyspace.LEGACY_BATCHLOG));
+        UntypedResultSet result = executeInternal(String.format("SELECT count(*) FROM \"%s\".\"%s\"", SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.LEGACY_BATCHLOG));
         assertNotNull(result);
         assertEquals("Count in blog legacy", 0, result.one().getLong("count"));
-        result = executeInternal(String.format("SELECT count(*) FROM \"%s\".\"%s\"", SystemKeyspace.NAME, SystemKeyspace.BATCHES));
+        result = executeInternal(String.format("SELECT count(*) FROM \"%s\".\"%s\"", SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.BATCHES));
         assertNotNull(result);
         assertEquals("Count in blog", 1500, result.one().getLong("count"));
 
@@ -382,10 +384,10 @@
         assertEquals(750, result.one().getLong("count"));
 
         // Ensure batchlog is left as expected.
-        result = executeInternal(String.format("SELECT count(*) FROM \"%s\".\"%s\"", SystemKeyspace.NAME, SystemKeyspace.BATCHES));
+        result = executeInternal(String.format("SELECT count(*) FROM \"%s\".\"%s\"", SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.BATCHES));
         assertNotNull(result);
         assertEquals("Count in blog after initial replay", 750, result.one().getLong("count"));
-        result = executeInternal(String.format("SELECT count(*) FROM \"%s\".\"%s\"", SystemKeyspace.NAME, SystemKeyspace.LEGACY_BATCHLOG));
+        result = executeInternal(String.format("SELECT count(*) FROM \"%s\".\"%s\"", SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.LEGACY_BATCHLOG));
         assertNotNull(result);
         assertEquals("Count in blog legacy after initial replay ", 0, result.one().getLong("count"));
     }
@@ -414,7 +416,7 @@
         Assert.assertEquals(initialAllBatches + 1, BatchlogManager.instance.countAllBatches());
 
         String query = String.format("SELECT count(*) FROM %s.%s where id = %s",
-                                     SystemKeyspace.NAME,
+                                     SchemaConstants.SYSTEM_KEYSPACE_NAME,
                                      SystemKeyspace.BATCHES,
                                      uuid);
         UntypedResultSet result = executeInternal(query);
@@ -451,7 +453,7 @@
         assertEquals(initialAllBatches, BatchlogManager.instance.countAllBatches());
 
         String query = String.format("SELECT count(*) FROM %s.%s where id = %s",
-                                     SystemKeyspace.NAME,
+                                     SchemaConstants.SYSTEM_KEYSPACE_NAME,
                                      SystemKeyspace.BATCHES,
                                      uuid);
         UntypedResultSet result = executeInternal(query);
@@ -486,7 +488,7 @@
         assertEquals(1, BatchlogManager.instance.countAllBatches() - initialAllBatches);
 
         // Flush the batchlog to disk (see CASSANDRA-6822).
-        Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).forceBlockingFlush();
+        Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).forceBlockingFlush();
 
         assertEquals(1, BatchlogManager.instance.countAllBatches() - initialAllBatches);
         assertEquals(0, BatchlogManager.instance.getTotalBatchesReplayed() - initialReplayedBatches);
diff --git a/test/unit/org/apache/cassandra/cache/AutoSavingCacheTest.java b/test/unit/org/apache/cassandra/cache/AutoSavingCacheTest.java
index 0c7e8a5..c952470 100644
--- a/test/unit/org/apache/cassandra/cache/AutoSavingCacheTest.java
+++ b/test/unit/org/apache/cassandra/cache/AutoSavingCacheTest.java
@@ -19,6 +19,7 @@
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.marshal.AsciiType;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
@@ -51,9 +52,23 @@
     }
 
     @Test
+    public void testSerializeAndLoadKeyCache0kB() throws Exception
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(0);
+        doTestSerializeAndLoadKeyCache();
+    }
+
+    @Test
     public void testSerializeAndLoadKeyCache() throws Exception
     {
+        DatabaseDescriptor.setColumnIndexCacheSize(8);
+        doTestSerializeAndLoadKeyCache();
+    }
+
+    private static void doTestSerializeAndLoadKeyCache() throws Exception
+    {
         ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(CF_STANDARD1);
+        cfs.truncateBlocking();
         for (int i = 0; i < 2; i++)
         {
             ColumnDefinition colDef = ColumnDefinition.regularDef(cfs.metadata, ByteBufferUtil.bytes("col1"), AsciiType.instance);
diff --git a/test/unit/org/apache/cassandra/cache/CacheProviderTest.java b/test/unit/org/apache/cassandra/cache/CacheProviderTest.java
index a4173d6..eca124f 100644
--- a/test/unit/org/apache/cassandra/cache/CacheProviderTest.java
+++ b/test/unit/org/apache/cassandra/cache/CacheProviderTest.java
@@ -32,6 +32,7 @@
 
 
 import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.concurrent.NamedThreadFactory;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.utils.Pair;
 
@@ -135,7 +136,7 @@
         List<Thread> threads = new ArrayList<>(100);
         for (int i = 0; i < 100; i++)
         {
-            Thread thread = new Thread(runnable);
+            Thread thread = NamedThreadFactory.createThread(runnable);
             threads.add(thread);
             thread.start();
         }
diff --git a/test/unit/org/apache/cassandra/cache/NopCacheProviderTest.java b/test/unit/org/apache/cassandra/cache/NopCacheProviderTest.java
new file mode 100644
index 0000000..8807883
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cache/NopCacheProviderTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.cache;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class NopCacheProviderTest
+{
+    private ICache<RowCacheKey, IRowCacheEntry> cache;
+
+    @Before
+    public void createCache()
+    {
+        NopCacheProvider cacheProvider = new NopCacheProvider();
+        cache = cacheProvider.create();
+    }
+
+    @Test
+    public void settingCapacityToZeroIsIgnored()
+    {
+        cache.setCapacity(0L);
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void failsOnSettingCapacityOtherThanZero()
+    {
+        cache.setCapacity(1);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/concurrent/DebuggableScheduledThreadPoolExecutorTest.java b/test/unit/org/apache/cassandra/concurrent/DebuggableScheduledThreadPoolExecutorTest.java
index 46b2764..bf78d65 100644
--- a/test/unit/org/apache/cassandra/concurrent/DebuggableScheduledThreadPoolExecutorTest.java
+++ b/test/unit/org/apache/cassandra/concurrent/DebuggableScheduledThreadPoolExecutorTest.java
@@ -20,7 +20,6 @@
 
 import java.io.IOException;
 import java.util.concurrent.CancellationException;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
diff --git a/test/unit/org/apache/cassandra/concurrent/DebuggableThreadPoolExecutorTest.java b/test/unit/org/apache/cassandra/concurrent/DebuggableThreadPoolExecutorTest.java
index 5040a24..9276248 100644
--- a/test/unit/org/apache/cassandra/concurrent/DebuggableThreadPoolExecutorTest.java
+++ b/test/unit/org/apache/cassandra/concurrent/DebuggableThreadPoolExecutorTest.java
@@ -24,12 +24,20 @@
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.utils.WrappedRunnable;
 
 public class DebuggableThreadPoolExecutorTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testSerialization()
     {
diff --git a/test/unit/org/apache/cassandra/concurrent/SEPExecutorTest.java b/test/unit/org/apache/cassandra/concurrent/SEPExecutorTest.java
index 9e98cf1..f223e57 100644
--- a/test/unit/org/apache/cassandra/concurrent/SEPExecutorTest.java
+++ b/test/unit/org/apache/cassandra/concurrent/SEPExecutorTest.java
@@ -25,12 +25,21 @@
 import java.util.concurrent.TimeUnit;
 
 import org.junit.Assert;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.utils.FBUtilities;
 
+
 public class SEPExecutorTest
 {
+    @BeforeClass
+    public static void beforeClass()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void shutdownTest() throws Throwable
     {
diff --git a/test/unit/org/apache/cassandra/concurrent/WaitQueueTest.java b/test/unit/org/apache/cassandra/concurrent/WaitQueueTest.java
index fdc6880..ac2a9c0 100644
--- a/test/unit/org/apache/cassandra/concurrent/WaitQueueTest.java
+++ b/test/unit/org/apache/cassandra/concurrent/WaitQueueTest.java
@@ -44,7 +44,7 @@
         final AtomicInteger ready = new AtomicInteger();
         Thread[] ts = new Thread[4];
         for (int i = 0 ; i < ts.length ; i++)
-            ts[i] = new Thread(new Runnable()
+            ts[i] = NamedThreadFactory.createThread(new Runnable()
         {
             @Override
             public void run()
@@ -84,7 +84,7 @@
         final AtomicBoolean ready = new AtomicBoolean(false);
         final AtomicBoolean condition = new AtomicBoolean(false);
         final AtomicBoolean fail = new AtomicBoolean(false);
-        Thread t = new Thread(new Runnable()
+        Thread t = NamedThreadFactory.createThread(new Runnable()
         {
             @Override
             public void run()
diff --git a/test/unit/org/apache/cassandra/config/CFMetaDataTest.java b/test/unit/org/apache/cassandra/config/CFMetaDataTest.java
index 9d91df3..2b0dfc0 100644
--- a/test/unit/org/apache/cassandra/config/CFMetaDataTest.java
+++ b/test/unit/org/apache/cassandra/config/CFMetaDataTest.java
@@ -26,18 +26,11 @@
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.Mutation;
-import org.apache.cassandra.db.marshal.AsciiType;
-import org.apache.cassandra.db.marshal.Int32Type;
-import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.db.rows.UnfilteredRowIterators;
 import org.apache.cassandra.exceptions.ConfigurationException;
-import org.apache.cassandra.schema.CompressionParams;
-import org.apache.cassandra.schema.KeyspaceMetadata;
-import org.apache.cassandra.schema.KeyspaceParams;
-import org.apache.cassandra.schema.SchemaKeyspace;
-import org.apache.cassandra.schema.TableParams;
-import org.apache.cassandra.schema.Types;
+import org.apache.cassandra.schema.*;
 import org.apache.cassandra.thrift.CfDef;
 import org.apache.cassandra.thrift.ColumnDef;
 import org.apache.cassandra.thrift.IndexType;
@@ -49,6 +42,8 @@
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 public class CFMetaDataTest
 {
@@ -155,16 +150,16 @@
         assert before.equals(after) : String.format("%n%s%n!=%n%s", before, after);
 
         // Test schema conversion
-        Mutation rm = SchemaKeyspace.makeCreateTableMutation(keyspace, cfm, FBUtilities.timestampMicros());
-        PartitionUpdate cfU = rm.getPartitionUpdate(Schema.instance.getId(SchemaKeyspace.NAME, SchemaKeyspace.TABLES));
-        PartitionUpdate cdU = rm.getPartitionUpdate(Schema.instance.getId(SchemaKeyspace.NAME, SchemaKeyspace.COLUMNS));
+        Mutation rm = SchemaKeyspace.makeCreateTableMutation(keyspace, cfm, FBUtilities.timestampMicros()).build();
+        PartitionUpdate cfU = rm.getPartitionUpdate(Schema.instance.getId(SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TABLES));
+        PartitionUpdate cdU = rm.getPartitionUpdate(Schema.instance.getId(SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.COLUMNS));
 
-        UntypedResultSet.Row tableRow = QueryProcessor.resultify(String.format("SELECT * FROM %s.%s", SchemaKeyspace.NAME, SchemaKeyspace.TABLES),
+        UntypedResultSet.Row tableRow = QueryProcessor.resultify(String.format("SELECT * FROM %s.%s", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TABLES),
                                                                  UnfilteredRowIterators.filter(cfU.unfilteredIterator(), FBUtilities.nowInSeconds()))
                                                       .one();
         TableParams params = SchemaKeyspace.createTableParamsFromRow(tableRow);
 
-        UntypedResultSet columnsRows = QueryProcessor.resultify(String.format("SELECT * FROM %s.%s", SchemaKeyspace.NAME, SchemaKeyspace.COLUMNS),
+        UntypedResultSet columnsRows = QueryProcessor.resultify(String.format("SELECT * FROM %s.%s", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.COLUMNS),
                                                                 UnfilteredRowIterators.filter(cdU.unfilteredIterator(), FBUtilities.nowInSeconds()));
         Set<ColumnDefinition> columns = new HashSet<>();
         for (UntypedResultSet.Row row : columnsRows)
@@ -173,4 +168,98 @@
         assertEquals(cfm.params, params);
         assertEquals(new HashSet<>(cfm.allColumns()), columns);
     }
+    
+    @Test
+    public void testIsNameValidPositive()
+    {
+         assertTrue(CFMetaData.isNameValid("abcdefghijklmnopqrstuvwxyz"));
+         assertTrue(CFMetaData.isNameValid("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+         assertTrue(CFMetaData.isNameValid("_01234567890"));
+    }
+    
+    @Test
+    public void testIsNameValidNegative()
+    {
+        assertFalse(CFMetaData.isNameValid(null));
+        assertFalse(CFMetaData.isNameValid(""));
+        assertFalse(CFMetaData.isNameValid(" "));
+        assertFalse(CFMetaData.isNameValid("@"));
+        assertFalse(CFMetaData.isNameValid("!"));
+    }
+
+    private static Set<String> primitiveTypes = new HashSet<String>(Arrays.asList(new String[] { "ascii", "bigint", "blob", "boolean", "date",
+                                                                                                 "duration", "decimal", "double", "float",
+                                                                                                 "inet", "int", "smallint", "text", "time",
+                                                                                                 "timestamp", "timeuuid", "tinyint", "uuid",
+                                                                                                 "varchar", "varint" }));
+
+    @Test
+    public void typeCompatibilityTest() throws Throwable
+    {
+        Map<String, Set<String>> compatibilityMap = new HashMap<>();
+        compatibilityMap.put("bigint", new HashSet<>(Arrays.asList(new String[] {"timestamp"})));
+        compatibilityMap.put("blob", new HashSet<>(Arrays.asList(new String[] {"ascii", "bigint", "boolean", "date", "decimal", "double", "duration",
+                                                                               "float", "inet", "int", "smallint", "text", "time", "timestamp",
+                                                                               "timeuuid", "tinyint", "uuid", "varchar", "varint"})));
+        compatibilityMap.put("date", new HashSet<>(Arrays.asList(new String[] {"int"})));
+        compatibilityMap.put("time", new HashSet<>(Arrays.asList(new String[] {"bigint"})));
+        compatibilityMap.put("text", new HashSet<>(Arrays.asList(new String[] {"ascii", "varchar"})));
+        compatibilityMap.put("timestamp", new HashSet<>(Arrays.asList(new String[] {"bigint"})));
+        compatibilityMap.put("varchar", new HashSet<>(Arrays.asList(new String[] {"ascii", "text"})));
+        compatibilityMap.put("varint", new HashSet<>(Arrays.asList(new String[] {"bigint", "int", "timestamp"})));
+        compatibilityMap.put("uuid", new HashSet<>(Arrays.asList(new String[] {"timeuuid"})));
+
+        for (String sourceTypeString: primitiveTypes)
+        {
+            AbstractType sourceType = CQLTypeParser.parse("KEYSPACE", sourceTypeString, Types.none());
+            for (String destinationTypeString: primitiveTypes)
+            {
+                AbstractType destinationType = CQLTypeParser.parse("KEYSPACE", destinationTypeString, Types.none());
+
+                if (compatibilityMap.get(destinationTypeString) != null &&
+                    compatibilityMap.get(destinationTypeString).contains(sourceTypeString) ||
+                    sourceTypeString.equals(destinationTypeString))
+                {
+                    assertTrue(sourceTypeString + " should be compatible with " + destinationTypeString,
+                               destinationType.isValueCompatibleWith(sourceType));
+                }
+                else
+                {
+                    assertFalse(sourceTypeString + " should not be compatible with " + destinationTypeString,
+                                destinationType.isValueCompatibleWith(sourceType));
+                }
+            }
+        }
+    }
+
+    @Test
+    public void clusteringColumnTypeCompatibilityTest() throws Throwable
+    {
+        Map<String, Set<String>> compatibilityMap = new HashMap<>();
+        compatibilityMap.put("blob", new HashSet<>(Arrays.asList(new String[] {"ascii", "text", "varchar"})));
+        compatibilityMap.put("text", new HashSet<>(Arrays.asList(new String[] {"ascii", "varchar"})));
+        compatibilityMap.put("varchar", new HashSet<>(Arrays.asList(new String[] {"ascii", "text" })));
+
+        for (String sourceTypeString: primitiveTypes)
+        {
+            AbstractType sourceType = CQLTypeParser.parse("KEYSPACE", sourceTypeString, Types.none());
+            for (String destinationTypeString: primitiveTypes)
+            {
+                AbstractType destinationType = CQLTypeParser.parse("KEYSPACE", destinationTypeString, Types.none());
+
+                if (compatibilityMap.get(destinationTypeString) != null &&
+                    compatibilityMap.get(destinationTypeString).contains(sourceTypeString) ||
+                    sourceTypeString.equals(destinationTypeString))
+                {
+                    assertTrue(sourceTypeString + " should be compatible with " + destinationTypeString,
+                               destinationType.isCompatibleWith(sourceType));
+                }
+                else
+                {
+                    assertFalse(sourceTypeString + " should not be compatible with " + destinationTypeString,
+                                destinationType.isCompatibleWith(sourceType));
+                }
+            }
+        }
+    }
 }
diff --git a/test/unit/org/apache/cassandra/config/ColumnDefinitionTest.java b/test/unit/org/apache/cassandra/config/ColumnDefinitionTest.java
index 933d231..1e8e704 100644
--- a/test/unit/org/apache/cassandra/config/ColumnDefinitionTest.java
+++ b/test/unit/org/apache/cassandra/config/ColumnDefinitionTest.java
@@ -21,6 +21,7 @@
  */
 
 import org.junit.Assert;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.cassandra.db.marshal.*;
@@ -29,6 +30,12 @@
 
 public class ColumnDefinitionTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testSerializeDeserialize() throws Exception
     {
diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
new file mode 100644
index 0000000..83c6bea
--- /dev/null
+++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
@@ -0,0 +1,285 @@
+/*
+ * 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.config;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.junit.Test;
+
+import org.apache.cassandra.utils.Pair;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Verifies that {@link DatabaseDescriptor#clientInitialization()} } and a couple of <i>apply</i> methods
+ * do not somehow lazily initialize any unwanted part of Cassandra like schema, commit log or start
+ * unexpected threads.
+ *
+ * {@link DatabaseDescriptor#toolInitialization()} is tested via unit tests extending
+ * {@link org.apache.cassandra.tools.ToolsTester}.
+ */
+public class DatabaseDescriptorRefTest
+{
+    static final String[] validClasses = {
+    "org.apache.cassandra.auth.IInternodeAuthenticator",
+    "org.apache.cassandra.auth.IAuthenticator",
+    "org.apache.cassandra.auth.IAuthorizer",
+    "org.apache.cassandra.auth.IRoleManager",
+    "org.apache.cassandra.config.DatabaseDescriptor",
+    "org.apache.cassandra.config.ConfigurationLoader",
+    "org.apache.cassandra.config.Config",
+    "org.apache.cassandra.config.Config$1",
+    "org.apache.cassandra.config.Config$RequestSchedulerId",
+    "org.apache.cassandra.config.Config$CommitLogSync",
+    "org.apache.cassandra.config.Config$DiskAccessMode",
+    "org.apache.cassandra.config.Config$DiskFailurePolicy",
+    "org.apache.cassandra.config.Config$CommitFailurePolicy",
+    "org.apache.cassandra.config.Config$DiskOptimizationStrategy",
+    "org.apache.cassandra.config.Config$InternodeCompression",
+    "org.apache.cassandra.config.Config$MemtableAllocationType",
+    "org.apache.cassandra.config.Config$UserFunctionTimeoutPolicy",
+    "org.apache.cassandra.config.RequestSchedulerOptions",
+    "org.apache.cassandra.config.ParameterizedClass",
+    "org.apache.cassandra.config.EncryptionOptions",
+    "org.apache.cassandra.config.EncryptionOptions$ClientEncryptionOptions",
+    "org.apache.cassandra.config.EncryptionOptions$ServerEncryptionOptions",
+    "org.apache.cassandra.config.EncryptionOptions$ServerEncryptionOptions$InternodeEncryption",
+    "org.apache.cassandra.config.ReplicaFilteringProtectionOptions",
+    "org.apache.cassandra.config.YamlConfigurationLoader",
+    "org.apache.cassandra.config.YamlConfigurationLoader$PropertiesChecker",
+    "org.apache.cassandra.config.YamlConfigurationLoader$PropertiesChecker$1",
+    "org.apache.cassandra.config.YamlConfigurationLoader$CustomConstructor",
+    "org.apache.cassandra.config.TransparentDataEncryptionOptions",
+    "org.apache.cassandra.dht.IPartitioner",
+    "org.apache.cassandra.distributed.shared.InstanceClassLoader",
+    "org.apache.cassandra.distributed.impl.InstanceConfig",
+    "org.apache.cassandra.distributed.impl.InvokableInstance",
+    "org.apache.cassandra.distributed.impl.InvokableInstance$CallableNoExcept",
+    "org.apache.cassandra.distributed.impl.InvokableInstance$InstanceFunction",
+    "org.apache.cassandra.distributed.impl.InvokableInstance$SerializableBiConsumer",
+    "org.apache.cassandra.distributed.impl.InvokableInstance$SerializableBiFunction",
+    "org.apache.cassandra.distributed.impl.InvokableInstance$SerializableCallable",
+    "org.apache.cassandra.distributed.impl.InvokableInstance$SerializableConsumer",
+    "org.apache.cassandra.distributed.impl.InvokableInstance$SerializableFunction",
+    "org.apache.cassandra.distributed.impl.InvokableInstance$SerializableRunnable",
+    "org.apache.cassandra.distributed.impl.InvokableInstance$SerializableTriFunction",
+    "org.apache.cassandra.distributed.impl.InvokableInstance$TriFunction",
+    "org.apache.cassandra.distributed.impl.Message",
+    "org.apache.cassandra.exceptions.ConfigurationException",
+    "org.apache.cassandra.exceptions.RequestValidationException",
+    "org.apache.cassandra.exceptions.CassandraException",
+    "org.apache.cassandra.exceptions.TransportException",
+    "org.apache.cassandra.locator.IEndpointSnitch",
+    "org.apache.cassandra.io.FSWriteError",
+    "org.apache.cassandra.io.FSError",
+    "org.apache.cassandra.io.compress.ICompressor",
+    "org.apache.cassandra.io.compress.LZ4Compressor",
+    "org.apache.cassandra.io.sstable.metadata.MetadataType",
+    "org.apache.cassandra.io.util.BufferedDataOutputStreamPlus",
+    "org.apache.cassandra.io.util.DataOutputBuffer",
+    "org.apache.cassandra.io.util.DataOutputBufferFixed",
+    "org.apache.cassandra.io.util.DataOutputStreamPlus",
+    "org.apache.cassandra.io.util.DataOutputPlus",
+    "org.apache.cassandra.io.util.DiskOptimizationStrategy",
+    "org.apache.cassandra.io.util.SpinningDiskOptimizationStrategy",
+    "org.apache.cassandra.locator.SimpleSeedProvider",
+    "org.apache.cassandra.locator.SeedProvider",
+    "org.apache.cassandra.net.BackPressureStrategy",
+    "org.apache.cassandra.scheduler.IRequestScheduler",
+    "org.apache.cassandra.security.EncryptionContext",
+    "org.apache.cassandra.service.CacheService$CacheType",
+    "org.apache.cassandra.utils.FBUtilities",
+    "org.apache.cassandra.utils.FBUtilities$1",
+    "org.apache.cassandra.utils.CloseableIterator",
+    "org.apache.cassandra.utils.Pair",
+    "org.apache.cassandra.ConsoleAppender",
+    "org.apache.cassandra.ConsoleAppender$1",
+    "org.apache.cassandra.LogbackStatusListener",
+    "org.apache.cassandra.LogbackStatusListener$ToLoggerOutputStream",
+    "org.apache.cassandra.LogbackStatusListener$WrappedPrintStream",
+    "org.apache.cassandra.TeeingAppender",
+    // generated classes
+    "org.apache.cassandra.config.ConfigBeanInfo",
+    "org.apache.cassandra.config.ConfigCustomizer",
+    "org.apache.cassandra.config.EncryptionOptionsBeanInfo",
+    "org.apache.cassandra.config.EncryptionOptionsCustomizer",
+    "org.apache.cassandra.config.EncryptionOptions$ServerEncryptionOptionsBeanInfo",
+    "org.apache.cassandra.config.EncryptionOptions$ServerEncryptionOptionsCustomizer",
+    "org.apache.cassandra.ConsoleAppenderBeanInfo",
+    "org.apache.cassandra.ConsoleAppenderCustomizer",
+    };
+
+    static final Set<String> checkedClasses = new HashSet<>(Arrays.asList(validClasses));
+
+    @Test
+    @SuppressWarnings({"DynamicRegexReplaceableByCompiledPattern", "UseOfSystemOutOrSystemErr"})
+    public void testDatabaseDescriptorRef() throws Throwable
+    {
+        PrintStream out = System.out;
+        PrintStream err = System.err;
+
+        ThreadMXBean threads = ManagementFactory.getThreadMXBean();
+        int threadCount = threads.getThreadCount();
+        List<Long> existingThreadIDs = Arrays.stream(threads.getAllThreadIds()).boxed().collect(Collectors.toList());
+
+        ClassLoader delegate = Thread.currentThread().getContextClassLoader();
+
+        List<Pair<String, Exception>> violations = Collections.synchronizedList(new ArrayList<>());
+
+        ClassLoader cl = new ClassLoader(null)
+        {
+            final Map<String, Class<?>> classMap = new HashMap<>();
+
+            public URL getResource(String name)
+            {
+                return delegate.getResource(name);
+            }
+
+            public InputStream getResourceAsStream(String name)
+            {
+                return delegate.getResourceAsStream(name);
+            }
+
+            protected Class<?> findClass(String name) throws ClassNotFoundException
+            {
+                Class<?> cls = classMap.get(name);
+                if (cls != null)
+                    return cls;
+
+                if (name.startsWith("org.apache.cassandra."))
+                {
+                    // out.println(name);
+
+                    if (!checkedClasses.contains(name))
+                        violations.add(Pair.create(name, new Exception()));
+                }
+
+                URL url = delegate.getResource(name.replace('.', '/') + ".class");
+                if (url == null)
+                    throw new ClassNotFoundException(name);
+                try (InputStream in = url.openConnection().getInputStream())
+                {
+                    ByteArrayOutputStream os = new ByteArrayOutputStream();
+                    int c;
+                    while ((c = in.read()) != -1)
+                        os.write(c);
+                    byte[] data = os.toByteArray();
+                    cls = defineClass(name, data, 0, data.length);
+                    classMap.put(name, cls);
+                    return cls;
+                }
+                catch (IOException e)
+                {
+                    throw new ClassNotFoundException(name, e);
+                }
+            }
+        };
+
+        Thread.currentThread().setContextClassLoader(cl);
+
+        assertEquals("thread started", threadCount, threads.getThreadCount());
+
+        Class<?> databaseDescriptorClass = Class.forName("org.apache.cassandra.config.DatabaseDescriptor", true, cl);
+
+        // During DatabaseDescriptor instantiation some threads are spawned. We need to take them into account in
+        // threadCount variable, otherwise they will be considered as new threads spawned by methods below. There is a
+        // trick: in case of multiple runs of this test in the same JVM the number of such threads will be multiplied by
+        // the number of runs. That's because DatabaseDescriptor is instantiated via a different class loader. So in
+        // order to keep calculation logic correct, we ignore existing threads that were spawned during the previous
+        // runs and change threadCount variable for the new threads only (if they have some specific names).
+        for (ThreadInfo threadInfo : threads.getThreadInfo(threads.getAllThreadIds()))
+        {
+            // All existing threads have been already taken into account in threadCount variable, so we ignore them
+            if (existingThreadIDs.contains(threadInfo.getThreadId()))
+                continue;
+            // Logback AsyncAppender thread needs to be taken into account
+            if (threadInfo.getThreadName().equals("AsyncAppender-Worker-ASYNC"))
+                threadCount++;
+            // Logback basic threads need to be taken into account
+            if (threadInfo.getThreadName().matches("logback-\\d+"))
+                threadCount++;
+            // Dynamic Attach thread needs to be taken into account, generally it is spawned by IDE
+            if (threadInfo.getThreadName().equals("Attach Listener"))
+                threadCount++;
+        }
+
+        for (String methodName : new String[]{
+            "clientInitialization",
+            "applyAddressConfig",
+            "applyThriftHSHA",
+            "applyTokensConfig",
+            // no seed provider in default configuration for clients
+            // "applySeedProvider",
+            // definitely not safe for clients - implicitly instantiates schema
+            // "applyPartitioner",
+            // definitely not safe for clients - implicitly instantiates StorageService
+            // "applySnitch",
+            "applyEncryptionContext",
+            // starts "REQUEST-SCHEDULER" thread via RoundRobinScheduler
+            // "applyRequestScheduler",
+        })
+        {
+            Method method = databaseDescriptorClass.getDeclaredMethod(methodName);
+            method.invoke(null);
+
+            if (threadCount != threads.getThreadCount())
+            {
+                for (ThreadInfo threadInfo : threads.getThreadInfo(threads.getAllThreadIds()))
+                    out.println("Thread #" + threadInfo.getThreadId() + ": " + threadInfo.getThreadName());
+                assertEquals("thread started in " + methodName, threadCount, ManagementFactory.getThreadMXBean().getThreadCount());
+            }
+
+            checkViolations(err, violations);
+        }
+    }
+
+    private void checkViolations(PrintStream err, List<Pair<String, Exception>> violations)
+    {
+        if (!violations.isEmpty())
+        {
+            for (Pair<String, Exception> violation : new ArrayList<>(violations))
+            {
+                err.println();
+                err.println();
+                err.println("VIOLATION: " + violation.left);
+                violation.right.printStackTrace(err);
+            }
+
+            fail();
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java
index 0dcc7f7..912d838 100644
--- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java
+++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java
@@ -19,17 +19,14 @@
 package org.apache.cassandra.config;
 
 import java.io.IOException;
-import java.lang.management.ManagementFactory;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.NetworkInterface;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Enumeration;
 
-import javax.management.InstanceNotFoundException;
-import javax.management.MBeanServer;
-import javax.management.ObjectName;
-
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -50,13 +47,15 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
 
+import static org.junit.Assert.assertTrue;
+
 @RunWith(OrderedJUnit4ClassRunner.class)
 public class DatabaseDescriptorTest
 {
     @BeforeClass
     public static void setupDatabaseDescriptor()
     {
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
     }
 
     @Test
@@ -280,6 +279,15 @@
     }
 
     @Test
+    public void testTokensFromString()
+    {
+        assertTrue(DatabaseDescriptor.tokensFromString(null).isEmpty());
+        Collection<String> tokens = DatabaseDescriptor.tokensFromString(" a,b ,c , d, f,g,h");
+        assertEquals(7, tokens.size());
+        assertTrue(tokens.containsAll(Arrays.asList(new String[]{ "a", "b", "c", "d", "f", "g", "h" })));
+    }
+
+    @Test
     public void testRepairSessionSizeToggles()
     {
         int previousDepth = DatabaseDescriptor.getRepairSessionMaxTreeDepth();
@@ -315,36 +323,31 @@
     }
 
     @Test
-    public void testApplyTokensConfigInitialTokensSetNumTokensSetAndDoesMatch() throws Exception
+    public void testApplyTokensConfigInitialTokensSetNumTokensSetAndDoesMatch()
     {
         Config config = DatabaseDescriptor.loadConfig();
         config.initial_token = "0,256,1024";
         config.num_tokens = 3;
 
-        unregisterSnitchesForTokenConfigTest();
-
         try
         {
             DatabaseDescriptor.applyTokensConfig(config);
-
             Assert.assertEquals(Integer.valueOf(3), config.num_tokens);
             Assert.assertEquals(3, DatabaseDescriptor.tokensFromString(config.initial_token).size());
         }
-        finally
+        catch (ConfigurationException e)
         {
-            unregisterSnitchesForTokenConfigTest();
+            Assert.fail("number of tokens in initial_token=0,256,1024 does not match num_tokens = 3");
         }
     }
 
     @Test
-    public void testApplyTokensConfigInitialTokensSetNumTokensSetAndDoesntMatch() throws Exception
+    public void testApplyTokensConfigInitialTokensSetNumTokensSetAndDoesntMatch()
     {
         Config config = DatabaseDescriptor.loadConfig();
         config.initial_token = "0,256,1024";
         config.num_tokens = 10;
 
-        unregisterSnitchesForTokenConfigTest();
-
         try
         {
             DatabaseDescriptor.applyTokensConfig(config);
@@ -356,23 +359,16 @@
             Assert.assertEquals("The number of initial tokens (by initial_token) specified (3) is different from num_tokens value (10)",
                                 ex.getMessage());
         }
-        finally
-        {
-            unregisterSnitchesForTokenConfigTest();
-        }
     }
 
     @Test
-    public void testApplyTokensConfigInitialTokensSetNumTokensNotSet() throws Exception
+    public void testApplyTokensConfigInitialTokensSetNumTokensNotSet()
     {
         Config config = DatabaseDescriptor.loadConfig();
-
-        unregisterSnitchesForTokenConfigTest();
+        config.initial_token = "0,256,1024";
 
         try
         {
-            config.initial_token = "0,256,1024";
-            config.num_tokens = null;
             DatabaseDescriptor.applyTokensConfig(config);
             Assert.fail("setting initial_token and not setting num_tokens is invalid");
         }
@@ -380,86 +376,40 @@
         {
             Assert.assertEquals("initial_token was set but num_tokens is not!", ex.getMessage());
         }
-        finally
-        {
-            unregisterSnitchesForTokenConfigTest();
-        }
     }
 
     @Test
-    public void testApplyTokensConfigInitialTokensNotSetNumTokensSet() throws Exception
+    public void testApplyTokensConfigInitialTokensNotSetNumTokensSet()
     {
         Config config = DatabaseDescriptor.loadConfig();
         config.num_tokens = 3;
 
-        unregisterSnitchesForTokenConfigTest();
-
-        try
-        {
-            DatabaseDescriptor.applyTokensConfig(config);
-        }
-        finally
-        {
-            unregisterSnitchesForTokenConfigTest();
-        }
+        DatabaseDescriptor.applyTokensConfig(config);
 
         Assert.assertEquals(Integer.valueOf(3), config.num_tokens);
         Assert.assertTrue(DatabaseDescriptor.tokensFromString(config.initial_token).isEmpty());
     }
 
     @Test
-    public void testApplyTokensConfigInitialTokensNotSetNumTokensNotSet() throws Exception
+    public void testApplyTokensConfigInitialTokensNotSetNumTokensNotSet()
     {
         Config config = DatabaseDescriptor.loadConfig();
-
-        unregisterSnitchesForTokenConfigTest();
-
-        try
-        {
-            DatabaseDescriptor.applyTokensConfig(config);
-        }
-        finally
-        {
-            unregisterSnitchesForTokenConfigTest();
-        }
+        DatabaseDescriptor.applyTokensConfig(config);
 
         Assert.assertEquals(Integer.valueOf(1), config.num_tokens);
         Assert.assertTrue(DatabaseDescriptor.tokensFromString(config.initial_token).isEmpty());
     }
 
     @Test
-    public void testApplyTokensConfigInitialTokensOneNumTokensNotSet() throws Exception
+    public void testApplyTokensConfigInitialTokensOneNumTokensNotSet()
     {
         Config config = DatabaseDescriptor.loadConfig();
         config.initial_token = "123";
         config.num_tokens = null;
 
-        unregisterSnitchesForTokenConfigTest();
-
-        try
-        {
-            DatabaseDescriptor.applyTokensConfig(config);
-        }
-        finally
-        {
-            unregisterSnitchesForTokenConfigTest();
-        }
+        DatabaseDescriptor.applyTokensConfig(config);
 
         Assert.assertEquals(Integer.valueOf(1), config.num_tokens);
         Assert.assertEquals(1, DatabaseDescriptor.tokensFromString(config.initial_token).size());
     }
-
-    private void unregisterSnitchesForTokenConfigTest() throws Exception
-    {
-        try
-        {
-            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
-            mbs.unregisterMBean(new ObjectName("org.apache.cassandra.db:type=DynamicEndpointSnitch"));
-            mbs.unregisterMBean(new ObjectName("org.apache.cassandra.db:type=EndpointSnitchInfo"));
-        }
-        catch (InstanceNotFoundException ex)
-        {
-            // ok
-        }
-    }
-}
\ No newline at end of file
+}
diff --git a/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java b/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java
index c132ffc..4811b4d 100644
--- a/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java
+++ b/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java
@@ -33,7 +33,7 @@
     @Test
     public void fromMapTest()
     {
-        Integer storagePort = 123;
+        int storagePort = 123;
         Config.CommitLogSync commitLogSync = Config.CommitLogSync.batch;
         ParameterizedClass seedProvider = new ParameterizedClass("org.apache.cassandra.locator.SimpleSeedProvider", Collections.emptyMap());
         EncryptionOptions encryptionOptions = new EncryptionOptions.ClientEncryptionOptions();
diff --git a/test/unit/org/apache/cassandra/cql3/CDCStatementTest.java b/test/unit/org/apache/cassandra/cql3/CDCStatementTest.java
new file mode 100644
index 0000000..fb24aa9
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/CDCStatementTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+
+public class CDCStatementTest extends CQLTester
+{
+    @BeforeClass
+    public static void setUpClass()
+    {
+        DatabaseDescriptor.setCDCEnabled(true);
+        CQLTester.setUpClass();
+    }
+
+    @Test
+    public void testEnableOnCreate() throws Throwable
+    {
+        createTable("CREATE TABLE %s (key text, val int, primary key(key)) WITH cdc = true;");
+        Assert.assertTrue(currentTableMetadata().params.cdc);
+    }
+
+    @Test
+    public void testEnableOnAlter() throws Throwable
+    {
+        createTable("CREATE TABLE %s (key text, val int, primary key(key));");
+        Assert.assertFalse(currentTableMetadata().params.cdc);
+        execute("ALTER TABLE %s WITH cdc = true;");
+        Assert.assertTrue(currentTableMetadata().params.cdc);
+    }
+
+    @Test
+    public void testDisableOnAlter() throws Throwable
+    {
+        createTable("CREATE TABLE %s (key text, val int, primary key(key)) WITH cdc = true;");
+        Assert.assertTrue(currentTableMetadata().params.cdc);
+        execute("ALTER TABLE %s WITH cdc = false;");
+        Assert.assertFalse(currentTableMetadata().params.cdc);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/CQL3TypeLiteralTest.java b/test/unit/org/apache/cassandra/cql3/CQL3TypeLiteralTest.java
index 02ed1a8..6728da2 100644
--- a/test/unit/org/apache/cassandra/cql3/CQL3TypeLiteralTest.java
+++ b/test/unit/org/apache/cassandra/cql3/CQL3TypeLiteralTest.java
@@ -31,7 +31,7 @@
 
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.serializers.*;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.UUIDGen;
 
@@ -207,6 +207,13 @@
         }
         addNativeValue("null", CQL3Type.Native.TIME, null);
 
+        for (int i = 0; i < 100; i++)
+        {
+            Duration duration = Duration.newInstance(Math.abs(randInt()), Math.abs(randInt()), Math.abs(randLong()));
+            addNativeValue(DurationSerializer.instance.toString(duration), CQL3Type.Native.DURATION, DurationSerializer.instance.serialize(duration));
+        }
+        addNativeValue("null", CQL3Type.Native.DURATION, null);
+
         // (mostly generates timestamp values with surreal values like in year 14273)
         for (int i = 0; i < 20; i++)
         {
@@ -263,7 +270,7 @@
         // test each native type against each supported protocol version (although it doesn't make sense to
         // iterate through all protocol versions as of C* 3.0).
 
-        for (int version = Server.MIN_SUPPORTED_VERSION; version <= Server.CURRENT_VERSION; version++)
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
         {
             for (Map.Entry<CQL3Type.Native, List<Value>> entry : nativeTypeValues.entrySet())
             {
@@ -281,7 +288,7 @@
         // test 100 collections with varying element/key/value types against each supported protocol version,
         // type of collection is randomly chosen
 
-        for (int version = Server.MIN_SUPPORTED_VERSION; version <= Server.CURRENT_VERSION; version++)
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
         {
             for (int n = 0; n < 100; n++)
             {
@@ -298,7 +305,7 @@
         // supported anymore and so the size of a collection is always on 4 bytes).
         ByteBuffer emptyCollection = ByteBufferUtil.bytes(0);
 
-        for (int version = Server.MIN_SUPPORTED_VERSION; version <= Server.CURRENT_VERSION; version++)
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
         {
             for (boolean frozen : Arrays.asList(true, false))
             {
@@ -326,7 +333,7 @@
     {
         // test 100 tuples with varying element/key/value types against each supported protocol version
 
-        for (int version = Server.MIN_SUPPORTED_VERSION; version <= Server.CURRENT_VERSION; version++)
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
         {
             for (int n = 0; n < 100; n++)
             {
@@ -341,7 +348,7 @@
     {
         // test 100 UDTs with varying element/key/value types against each supported protocol version
 
-        for (int version = Server.MIN_SUPPORTED_VERSION; version <= Server.CURRENT_VERSION; version++)
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
         {
             for (int n = 0; n < 100; n++)
             {
@@ -358,7 +365,7 @@
         // like 'tuple<map, list<user>, tuple, user>' or 'map<tuple<int, text>, set<inet>>' with
         // random types  against each supported protocol version.
 
-        for (int version = Server.MIN_SUPPORTED_VERSION; version <= Server.CURRENT_VERSION; version++)
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
         {
             for (int n = 0; n < 100; n++)
             {
@@ -368,7 +375,7 @@
         }
     }
 
-    static void compareCqlLiteral(int version, Value value)
+    static void compareCqlLiteral(ProtocolVersion version, Value value)
     {
         ByteBuffer buffer = value.value != null ? value.value.duplicate() : null;
         String msg = "Failed to get expected value for type " + value.cql3Type + " / " + value.cql3Type.getType() + " with protocol-version " + version + " expected:\"" + value.expected + '"';
@@ -384,7 +391,7 @@
         }
     }
 
-    static Value randomNested(int version)
+    static Value randomNested(ProtocolVersion version)
     {
         AbstractType type = randomNestedType(2);
 
@@ -412,7 +419,7 @@
         throw new AssertionError();
     }
 
-    static Value generateCollectionValue(int version, CollectionType collectionType, boolean allowNull)
+    static Value generateCollectionValue(ProtocolVersion version, CollectionType collectionType, boolean allowNull)
     {
         StringBuilder expected = new StringBuilder();
         ByteBuffer buffer;
@@ -485,7 +492,7 @@
     /**
      * Generates a value for any type or type structure.
      */
-    static Value generateAnyValue(int version, CQL3Type type)
+    static Value generateAnyValue(ProtocolVersion version, CQL3Type type)
     {
         if (type instanceof CQL3Type.Native)
             return generateNativeValue(type, false);
@@ -498,7 +505,7 @@
         throw new AssertionError();
     }
 
-    static Value generateTupleValue(int version, TupleType tupleType, boolean allowNull)
+    static Value generateTupleValue(ProtocolVersion version, TupleType tupleType, boolean allowNull)
     {
         StringBuilder expected = new StringBuilder();
         ByteBuffer buffer;
@@ -543,7 +550,7 @@
         return new Value(expected.toString(), tupleType.asCQL3Type(), buffer);
     }
 
-    static Value generateUserDefinedValue(int version, UserType userType, boolean allowNull)
+    static Value generateUserDefinedValue(ProtocolVersion version, UserType userType, boolean allowNull)
     {
         StringBuilder expected = new StringBuilder();
         ByteBuffer buffer;
@@ -629,14 +636,14 @@
     static UserType randomUserType(int level)
     {
         int typeCount = 2 + randInt(5);
-        List<ByteBuffer> names = new ArrayList<>();
+        List<FieldIdentifier> names = new ArrayList<>();
         List<AbstractType<?>> types = new ArrayList<>();
         for (int i = 0; i < typeCount; i++)
         {
-            names.add(UTF8Type.instance.fromString('f' + randLetters(i)));
+            names.add(FieldIdentifier.forQuoted('f' + randLetters(i)));
             types.add(randomNestedType(level));
         }
-        return new UserType("ks", UTF8Type.instance.fromString("u" + randInt(1000000)), names, types);
+        return new UserType("ks", UTF8Type.instance.fromString("u" + randInt(1000000)), names, types, true);
     }
 
     //
diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java b/test/unit/org/apache/cassandra/cql3/CQLTester.java
index fe8ed5b..b611a52 100644
--- a/test/unit/org/apache/cassandra/cql3/CQLTester.java
+++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java
@@ -31,7 +31,6 @@
 import java.util.stream.Collectors;
 
 import com.google.common.base.Objects;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 
@@ -40,6 +39,7 @@
 import org.slf4j.LoggerFactory;
 
 import com.datastax.driver.core.*;
+import com.datastax.driver.core.DataType;
 import com.datastax.driver.core.ResultSet;
 
 import org.apache.cassandra.SchemaLoader;
@@ -48,8 +48,8 @@
 import org.apache.cassandra.metrics.ClientMetrics;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.functions.FunctionName;
-import org.apache.cassandra.cql3.functions.ThreadAwareSecurityManager;
 import org.apache.cassandra.cql3.statements.ParsedStatement;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.commitlog.CommitLog;
@@ -63,12 +63,14 @@
 import org.apache.cassandra.service.ClientState;
 import org.apache.cassandra.service.QueryState;
 import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.transport.ConfiguredLimit;
 import org.apache.cassandra.transport.Event;
 import org.apache.cassandra.transport.Server;
 import org.apache.cassandra.transport.messages.ResultMessage;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.security.ThreadAwareSecurityManager;
 
 import static junit.framework.Assert.assertNotNull;
 
@@ -90,33 +92,42 @@
     private static org.apache.cassandra.transport.Server server;
     protected static final int nativePort;
     protected static final InetAddress nativeAddr;
+    private static final Map<ProtocolVersion, Cluster> clusters = new HashMap<>();
+    private static final Map<ProtocolVersion, Session> sessions = new HashMap<>();
     protected static ConfiguredLimit protocolVersionLimit;
-    private static final Map<Integer, Cluster> clusters = new HashMap<>();
-    private static final Map<Integer, Session> sessions = new HashMap<>();
 
     private static boolean isServerPrepared = false;
 
-    public static final List<Integer> PROTOCOL_VERSIONS;
+    public static final List<ProtocolVersion> PROTOCOL_VERSIONS = new ArrayList<>(ProtocolVersion.SUPPORTED.size());
+
+    /** Return the current server version if supported by the driver, else
+     * the latest that is supported.
+     *
+     * @return - the preferred versions that is also supported by the driver
+     */
+    public static final ProtocolVersion getDefaultVersion()
+    {
+        return PROTOCOL_VERSIONS.contains(ProtocolVersion.CURRENT)
+               ? ProtocolVersion.CURRENT
+               : PROTOCOL_VERSIONS.get(PROTOCOL_VERSIONS.size() - 1);
+    }
     static
     {
+        DatabaseDescriptor.daemonInitialization();
+
         // The latest versions might not be supported yet by the java driver
-        ImmutableList.Builder<Integer> builder = ImmutableList.builder();
-        for (int version = Server.MIN_SUPPORTED_VERSION; version <= Server.CURRENT_VERSION; version++)
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
         {
             try
             {
-                ProtocolVersion.fromInt(version);
-                builder.add(version);
+                com.datastax.driver.core.ProtocolVersion.fromInt(version.asInt());
+                PROTOCOL_VERSIONS.add(version);
             }
             catch (IllegalArgumentException e)
             {
-                break;
+                logger.warn("Protocol Version {} not supported by java driver", version);
             }
         }
-        PROTOCOL_VERSIONS = builder.build();
-
-        // Once per-JVM is enough
-        prepareServer();
 
         nativeAddr = InetAddress.getLoopbackAddress();
 
@@ -145,11 +156,18 @@
     private boolean usePrepared = USE_PREPARED_VALUES;
     private static boolean reusePrepared = REUSE_PREPARED;
 
+    protected boolean usePrepared()
+    {
+        return usePrepared;
+    }
+
     public static void prepareServer()
     {
         if (isServerPrepared)
             return;
 
+        DatabaseDescriptor.daemonInitialization();
+
         // Cleanup first
         try
         {
@@ -171,7 +189,6 @@
 
         ThreadAwareSecurityManager.install();
 
-        DatabaseDescriptor.setDaemonInitialized();
         Keyspace.setInitialized();
         isServerPrepared = true;
     }
@@ -198,6 +215,10 @@
             FileUtils.deleteRecursive(dir);
         }
 
+        File cdcDir = new File(DatabaseDescriptor.getCDCLogLocation());
+        if (cdcDir.exists())
+            FileUtils.deleteRecursive(cdcDir);
+
         cleanupSavedCaches();
 
         // clean up data directory which are stored as data directory/keyspace/data files
@@ -232,6 +253,9 @@
             DatabaseDescriptor.setRowCacheSizeInMB(ROW_CACHE_SIZE_IN_MB);
 
         StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance);
+
+        // Once per-JVM is enough
+        prepareServer();
     }
 
     @AfterClass
@@ -371,19 +395,19 @@
         ClientMetrics.instance.init(Collections.singleton(server));
         server.start();
 
-        for (int version : PROTOCOL_VERSIONS)
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
         {
             if (clusters.containsKey(version))
                 continue;
 
-            if (version > protocolVersionLimit.getMaxVersion())
+            if (version.isGreaterThan(protocolVersionLimit.getMaxVersion()))
                 continue;
 
             Cluster cluster = Cluster.builder()
                                      .addContactPoints(nativeAddr)
-                                     .withClusterName("Test Cluster-v" + version)
+                                     .withClusterName("Test Cluster-" + version.name())
                                      .withPort(nativePort)
-                                     .withProtocolVersion(ProtocolVersion.fromInt(version))
+                                     .withProtocolVersion(com.datastax.driver.core.ProtocolVersion.fromInt(version.asInt()))
                                      .build();
             clusters.put(version, cluster);
             sessions.put(version, cluster.connect());
@@ -414,6 +438,11 @@
         return list.isEmpty() ? Collections.<String>emptyList() : new ArrayList<>(list);
     }
 
+    public ColumnFamilyStore getCurrentColumnFamilyStore()
+    {
+        return getCurrentColumnFamilyStore(KEYSPACE);
+    }
+
     public ColumnFamilyStore getCurrentColumnFamilyStore(String keyspace)
     {
         String currentTable = currentTable();
@@ -422,9 +451,10 @@
              : Keyspace.open(keyspace).getColumnFamilyStore(currentTable);
     }
 
-    public ColumnFamilyStore getCurrentColumnFamilyStore()
+    public void flush(boolean forceFlush)
     {
-        return getCurrentColumnFamilyStore(KEYSPACE);
+        if (forceFlush)
+            flush();
     }
 
     public void flush()
@@ -446,30 +476,6 @@
             store.disableAutoCompaction();
     }
 
-    public void flush(boolean forceFlush)
-    {
-        if (forceFlush)
-            flush();
-    }
-
-    @FunctionalInterface
-    public interface CheckedFunction {
-        void apply() throws Throwable;
-    }
-
-    /**
-     * Runs the given function before and after a flush of sstables.  This is useful for checking that behavior is
-     * the same whether data is in memtables or sstables.
-     * @param runnable
-     * @throws Throwable
-     */
-    public void beforeAndAfterFlush(CheckedFunction runnable) throws Throwable
-    {
-        runnable.apply();
-        flush();
-        runnable.apply();
-    }
-
     public void compact()
     {
          ColumnFamilyStore store = getCurrentColumnFamilyStore();
@@ -651,7 +657,12 @@
 
     protected String createTable(String keyspace, String query)
     {
-        String currentTable = createTableName();
+        return createTable(keyspace, query, null);
+    }
+
+    protected String createTable(String keyspace, String query, String tableName)
+    {
+        String currentTable = createTableName(tableName);
         String fullQuery = formatQuery(keyspace, query);
         logger.info(fullQuery);
         schemaChange(fullQuery);
@@ -660,7 +671,12 @@
 
     protected String createTableName()
     {
-        String currentTable = "table_" + seqNumber.getAndIncrement();
+        return createTableName(null);
+    }
+
+    protected String createTableName(String tableName)
+    {
+        String currentTable = tableName == null ? String.format("table_%02d", seqNumber.getAndIncrement()) : tableName;
         tables.add(currentTable);
         return currentTable;
     }
@@ -775,7 +791,7 @@
         try
         {
             ClientState state = ClientState.forInternalCalls();
-            state.setKeyspace(SystemKeyspace.NAME);
+            state.setKeyspace(SchemaConstants.SYSTEM_KEYSPACE_NAME);
             QueryState queryState = new QueryState(state);
 
             ParsedStatement.Prepared prepared = QueryProcessor.parseStatement(query, queryState);
@@ -801,7 +817,7 @@
     {
         return sessionNet().execute(formatQuery(query), values);
     }
-    protected com.datastax.driver.core.ResultSet executeNet(int protocolVersion, String query, Object... values) throws Throwable
+    protected com.datastax.driver.core.ResultSet executeNet(ProtocolVersion protocolVersion, String query, Object... values) throws Throwable
     {
         return sessionNet(protocolVersion).execute(formatQuery(query), values);
     }
@@ -813,10 +829,10 @@
 
     protected Session sessionNet()
     {
-        return sessionNet(PROTOCOL_VERSIONS.get(PROTOCOL_VERSIONS.size() - 1));
+        return sessionNet(getDefaultVersion());
     }
 
-    protected Session sessionNet(int protocolVersion)
+    protected Session sessionNet(ProtocolVersion protocolVersion)
     {
         requireNetwork();
 
@@ -834,6 +850,11 @@
         return currentTable == null ? query : String.format(query, keyspace + "." + currentTable);
     }
 
+    protected ResultMessage.Prepared prepare(String query) throws Throwable
+    {
+        return QueryProcessor.instance.prepare(formatQuery(query), ClientState.forInternalCalls(), false);
+    }
+
     protected UntypedResultSet execute(String query, Object... values) throws Throwable
     {
         return executeFormattedQuery(formatQuery(query), values);
@@ -877,7 +898,12 @@
         return rs;
     }
 
-    protected void assertRowsNet(int protocolVersion, ResultSet result, Object[]... rows)
+    protected void assertRowsNet(ResultSet result, Object[]... rows)
+    {
+        assertRowsNet(getDefaultVersion(), result, rows);
+    }
+
+    protected void assertRowsNet(ProtocolVersion protocolVersion, ResultSet result, Object[]... rows)
     {
         // necessary as we need cluster objects to supply CodecRegistry.
         // It's reasonably certain that the network setup has already been done
@@ -899,7 +925,7 @@
             Object[] expected = rows[i];
             Row actual = iter.next();
 
-            Assert.assertEquals(String.format("Invalid number of (expected) values provided for row %d (using protocol version %d)",
+            Assert.assertEquals(String.format("Invalid number of (expected) values provided for row %d (using protocol version %s)",
                                               i, protocolVersion),
                                 meta.size(), expected.length);
 
@@ -909,18 +935,18 @@
                 com.datastax.driver.core.TypeCodec<Object> codec = clusters.get(protocolVersion).getConfiguration()
                                                                                                 .getCodecRegistry()
                                                                                                 .codecFor(type);
-                ByteBuffer expectedByteValue = codec.serialize(expected[j], ProtocolVersion.fromInt(protocolVersion));
+                ByteBuffer expectedByteValue = codec.serialize(expected[j], com.datastax.driver.core.ProtocolVersion.fromInt(protocolVersion.asInt()));
                 int expectedBytes = expectedByteValue == null ? -1 : expectedByteValue.remaining();
                 ByteBuffer actualValue = actual.getBytesUnsafe(meta.getName(j));
                 int actualBytes = actualValue == null ? -1 : actualValue.remaining();
                 if (!Objects.equal(expectedByteValue, actualValue))
                     Assert.fail(String.format("Invalid value for row %d column %d (%s of type %s), " +
                                               "expected <%s> (%d bytes) but got <%s> (%d bytes) " +
-                                              "(using protocol version %d)",
+                                              "(using protocol version %s)",
                                               i, j, meta.getName(j), type,
                                               codec.format(expected[j]),
                                               expectedBytes,
-                                              codec.format(codec.deserialize(actualValue, ProtocolVersion.fromInt(protocolVersion))),
+                                              codec.format(codec.deserialize(actualValue, com.datastax.driver.core.ProtocolVersion.fromInt(protocolVersion.asInt()))),
                                               actualBytes,
                                               protocolVersion));
             }
@@ -934,19 +960,14 @@
                 iter.next();
                 i++;
             }
-            Assert.fail(String.format("Got less rows than expected. Expected %d but got %d (using protocol version %d).",
+            Assert.fail(String.format("Got less rows than expected. Expected %d but got %d (using protocol version %s).",
                                       rows.length, i, protocolVersion));
         }
 
-        Assert.assertTrue(String.format("Got %s rows than expected. Expected %d but got %d (using protocol version %d)",
+        Assert.assertTrue(String.format("Got %s rows than expected. Expected %d but got %d (using protocol version %s)",
                                         rows.length>i ? "less" : "more", rows.length, i, protocolVersion), i == rows.length);
     }
 
-    protected void assertRowsNet(ResultSet result, Object[]... rows)
-    {
-        assertRowsNet(PROTOCOL_VERSIONS.get(PROTOCOL_VERSIONS.size() - 1), result, rows);
-    }
-
     protected void assertRowCountNet(ResultSet r1, int expectedCount)
     {
         Assert.assertFalse("Received a null resultset when expected count was > 0", expectedCount > 0 && r1 == null);
@@ -999,8 +1020,17 @@
         {
             while (iter.hasNext())
             {
-                iter.next();
+                UntypedResultSet.Row actual = iter.next();
                 i++;
+
+                StringBuilder str = new StringBuilder();
+                for (int j = 0; j < meta.size(); j++)
+                {
+                    ColumnSpecification column = meta.get(j);
+                    ByteBuffer actualValue = actual.getBytes(column.name.toString());
+                    str.append(String.format("%s=%s ", column.name, formatValue(actualValue, column.type)));
+                }
+                logger.info("Extra row num {}: {}", i, str.toString());
             }
             Assert.fail(String.format("Got more rows than expected. Expected %d but got %d.", rows.length, i));
         }
@@ -1221,12 +1251,12 @@
 
     protected void assertInvalidThrowMessage(String errorMessage, Class<? extends Throwable> exception, String query, Object... values) throws Throwable
     {
-        assertInvalidThrowMessage(Integer.MIN_VALUE, errorMessage, exception, query, values);
+        assertInvalidThrowMessage(Optional.empty(), errorMessage, exception, query, values);
     }
 
     // if a protocol version > Integer.MIN_VALUE is supplied, executes
     // the query via the java driver, mimicking a real client.
-    protected void assertInvalidThrowMessage(int protocolVersion,
+    protected void assertInvalidThrowMessage(Optional<ProtocolVersion> protocolVersion,
                                              String errorMessage,
                                              Class<? extends Throwable> exception,
                                              String query,
@@ -1234,10 +1264,10 @@
     {
         try
         {
-            if (protocolVersion == Integer.MIN_VALUE)
+            if (!protocolVersion.isPresent())
                 execute(query, values);
             else
-                executeNet(protocolVersion, query, values);
+                executeNet(protocolVersion.get(), query, values);
 
             String q = USE_PREPARED_VALUES
                        ? query + " (values: " + formatAllValues(values) + ")"
@@ -1312,6 +1342,24 @@
                 e.getMessage().contains(text));
     }
 
+    @FunctionalInterface
+    public interface CheckedFunction {
+        void apply() throws Throwable;
+    }
+
+    /**
+     * Runs the given function before and after a flush of sstables.  This is useful for checking that behavior is
+     * the same whether data is in memtables or sstables.
+     * @param runnable
+     * @throws Throwable
+     */
+    public void beforeAndAfterFlush(CheckedFunction runnable) throws Throwable
+    {
+        runnable.apply();
+        flush();
+        runnable.apply();
+    }
+
     private static String replaceValues(String query, Object[] values)
     {
         StringBuilder sb = new StringBuilder();
@@ -1522,7 +1570,7 @@
         if (value instanceof ByteBuffer)
             return (ByteBuffer)value;
 
-        return type.decompose(value);
+        return type.decompose(serializeTuples(value));
     }
 
     private static String formatValue(ByteBuffer bb, AbstractType<?> type)
@@ -1549,7 +1597,19 @@
 
     protected Object userType(Object... values)
     {
-        return new TupleValue(values).toByteBuffer();
+        if (values.length % 2 != 0)
+            throw new IllegalArgumentException("userType() requires an even number of arguments");
+
+        String[] fieldNames = new String[values.length / 2];
+        Object[] fieldValues = new Object[values.length / 2];
+        int fieldNum = 0;
+        for (int i = 0; i < values.length; i += 2)
+        {
+            fieldNames[fieldNum] = (String) values[i];
+            fieldValues[fieldNum] = values[i + 1];
+            fieldNum++;
+        }
+        return new UserTypeValue(fieldNames, fieldValues);
     }
 
     protected Object list(Object...values)
@@ -1588,7 +1648,7 @@
         return m;
     }
 
-    protected com.datastax.driver.core.TupleType tupleTypeOf(int protocolVersion, DataType...types)
+    protected com.datastax.driver.core.TupleType tupleTypeOf(ProtocolVersion protocolVersion, DataType...types)
     {
         requireNetwork();
         return clusters.get(protocolVersion).getMetadata().newTupleType(types);
@@ -1616,6 +1676,9 @@
         if (value instanceof Float)
             return FloatType.instance;
 
+        if (value instanceof Duration)
+            return DurationType.instance;
+
         if (value instanceof Double)
             return DoubleType.instance;
 
@@ -1677,7 +1740,7 @@
 
     private static class TupleValue
     {
-        private final Object[] values;
+        protected final Object[] values;
 
         TupleValue(Object[] values)
         {
@@ -1711,4 +1774,43 @@
             return "TupleValue" + toCQLString();
         }
     }
+
+    private static class UserTypeValue extends TupleValue
+    {
+        private final String[] fieldNames;
+
+        UserTypeValue(String[] fieldNames, Object[] fieldValues)
+        {
+            super(fieldValues);
+            this.fieldNames = fieldNames;
+        }
+
+        @Override
+        public String toCQLString()
+        {
+            StringBuilder sb = new StringBuilder();
+            sb.append("{");
+            boolean haveEntry = false;
+            for (int i = 0; i < values.length; i++)
+            {
+                if (values[i] != null)
+                {
+                    if (haveEntry)
+                        sb.append(", ");
+                    sb.append(ColumnIdentifier.maybeQuote(fieldNames[i]));
+                    sb.append(": ");
+                    sb.append(formatForCQL(values[i]));
+                    haveEntry = true;
+                }
+            }
+            assert haveEntry;
+            sb.append("}");
+            return sb.toString();
+        }
+
+        public String toString()
+        {
+            return "UserTypeValue" + toCQLString();
+        }
+    }
 }
diff --git a/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java b/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java
index b39dca7..62fb53e 100644
--- a/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java
+++ b/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java
@@ -20,11 +20,12 @@
 import java.nio.ByteBuffer;
 import java.util.*;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.db.LivenessInfo;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.rows.BufferCell;
 import org.apache.cassandra.db.rows.Cell;
 import org.apache.cassandra.db.rows.CellPath;
@@ -47,6 +48,12 @@
     public static final ByteBuffer A = AsciiType.instance.fromString("a");
     public static final ByteBuffer B = AsciiType.instance.fromString("b");
 
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     private static boolean isSatisfiedBy(ColumnCondition.Bound bound, ByteBuffer conditionValue, ByteBuffer columnValue) throws InvalidRequestException
     {
         Cell cell = null;
@@ -186,7 +193,7 @@
         ColumnDefinition definition = ColumnDefinition.regularDef("ks", "cf", "c", ListType.getInstance(Int32Type.instance, true));
 
         // EQ
-        ColumnCondition condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.EQ);
+        ColumnCondition condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.EQ);
         ColumnCondition.CollectionBound bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
         assertTrue(listAppliesTo(bound, list(ONE), list(ONE)));
         assertTrue(listAppliesTo(bound, list(), list()));
@@ -202,7 +209,7 @@
         assertTrue(listAppliesTo(bound, list(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // NEQ
-        condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.NEQ);
+        condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.NEQ);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
         assertFalse(listAppliesTo(bound, list(ONE), list(ONE)));
         assertFalse(listAppliesTo(bound, list(), list()));
@@ -218,7 +225,7 @@
         assertFalse(listAppliesTo(bound, list(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // LT
-        condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.LT);
+        condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.LT);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
         assertFalse(listAppliesTo(bound, list(ONE), list(ONE)));
         assertFalse(listAppliesTo(bound, list(), list()));
@@ -234,7 +241,7 @@
         assertFalse(listAppliesTo(bound, list(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // LTE
-        condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.LTE);
+        condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.LTE);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
         assertTrue(listAppliesTo(bound, list(ONE), list(ONE)));
         assertTrue(listAppliesTo(bound, list(), list()));
@@ -250,7 +257,7 @@
         assertTrue(listAppliesTo(bound, list(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // GT
-        condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.GT);
+        condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.GT);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
         assertFalse(listAppliesTo(bound, list(ONE), list(ONE)));
         assertFalse(listAppliesTo(bound, list(), list()));
@@ -266,7 +273,7 @@
         assertFalse(listAppliesTo(bound, list(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // GTE
-        condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.GTE);
+        condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.GTE);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
         assertTrue(listAppliesTo(bound, list(ONE), list(ONE)));
         assertTrue(listAppliesTo(bound, list(), list()));
@@ -315,7 +322,7 @@
         ColumnDefinition definition = ColumnDefinition.regularDef("ks", "cf", "c", ListType.getInstance(Int32Type.instance, true));
 
         // EQ
-        ColumnCondition condition = ColumnCondition.condition(definition, null, new Sets.Value(set(ONE)), Operator.EQ);
+        ColumnCondition condition = ColumnCondition.condition(definition, new Sets.Value(set(ONE)), Operator.EQ);
         ColumnCondition.CollectionBound bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
         assertTrue(setAppliesTo(bound, set(ONE), list(ONE)));
         assertTrue(setAppliesTo(bound, set(), list()));
@@ -331,7 +338,7 @@
         assertTrue(setAppliesTo(bound, set(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // NEQ
-        condition = ColumnCondition.condition(definition, null, new Sets.Value(set(ONE)), Operator.NEQ);
+        condition = ColumnCondition.condition(definition, new Sets.Value(set(ONE)), Operator.NEQ);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
         assertFalse(setAppliesTo(bound, set(ONE), list(ONE)));
         assertFalse(setAppliesTo(bound, set(), list()));
@@ -347,7 +354,7 @@
         assertFalse(setAppliesTo(bound, set(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // LT
-        condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.LT);
+        condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.LT);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
         assertFalse(setAppliesTo(bound, set(ONE), list(ONE)));
         assertFalse(setAppliesTo(bound, set(), list()));
@@ -363,7 +370,7 @@
         assertFalse(setAppliesTo(bound, set(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // LTE
-        condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.LTE);
+        condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.LTE);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
         assertTrue(setAppliesTo(bound, set(ONE), list(ONE)));
         assertTrue(setAppliesTo(bound, set(), list()));
@@ -379,7 +386,7 @@
         assertTrue(setAppliesTo(bound, set(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // GT
-        condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.GT);
+        condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.GT);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
         assertFalse(setAppliesTo(bound, set(ONE), list(ONE)));
         assertFalse(setAppliesTo(bound, set(), list()));
@@ -395,7 +402,7 @@
         assertFalse(setAppliesTo(bound, set(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // GTE
-        condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.GTE);
+        condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.GTE);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
         assertTrue(setAppliesTo(bound, set(ONE), list(ONE)));
         assertTrue(setAppliesTo(bound, set(), list()));
@@ -448,7 +455,7 @@
         Maps.Value placeholder = new Maps.Value(placeholderMap);
 
         // EQ
-        ColumnCondition condition = ColumnCondition.condition(definition, null, placeholder, Operator.EQ);
+        ColumnCondition condition = ColumnCondition.condition(definition, placeholder, Operator.EQ);
         ColumnCondition.CollectionBound bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
 
         assertTrue(mapAppliesTo(bound, map(ONE, ONE), map(ONE, ONE)));
@@ -470,7 +477,7 @@
         assertTrue(mapAppliesTo(bound, map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // NEQ
-        condition = ColumnCondition.condition(definition, null, placeholder, Operator.NEQ);
+        condition = ColumnCondition.condition(definition, placeholder, Operator.NEQ);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
 
         assertFalse(mapAppliesTo(bound, map(ONE, ONE), map(ONE, ONE)));
@@ -492,7 +499,7 @@
         assertFalse(mapAppliesTo(bound, map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // LT
-        condition = ColumnCondition.condition(definition, null, placeholder, Operator.LT);
+        condition = ColumnCondition.condition(definition, placeholder, Operator.LT);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
 
         assertFalse(mapAppliesTo(bound, map(ONE, ONE), map(ONE, ONE)));
@@ -514,7 +521,7 @@
         assertFalse(mapAppliesTo(bound, map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // LTE
-        condition = ColumnCondition.condition(definition, null, placeholder, Operator.LTE);
+        condition = ColumnCondition.condition(definition, placeholder, Operator.LTE);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
 
         assertTrue(mapAppliesTo(bound, map(ONE, ONE), map(ONE, ONE)));
@@ -536,7 +543,7 @@
         assertTrue(mapAppliesTo(bound, map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // GT
-        condition = ColumnCondition.condition(definition, null, placeholder, Operator.GT);
+        condition = ColumnCondition.condition(definition, placeholder, Operator.GT);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
 
         assertFalse(mapAppliesTo(bound, map(ONE, ONE), map(ONE, ONE)));
@@ -558,7 +565,7 @@
         assertFalse(mapAppliesTo(bound, map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER)));
 
         // GTE
-        condition = ColumnCondition.condition(definition, null, placeholder, Operator.GTE);
+        condition = ColumnCondition.condition(definition, placeholder, Operator.GTE);
         bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT);
 
         assertTrue(mapAppliesTo(bound, map(ONE, ONE), map(ONE, ONE)));
diff --git a/test/unit/org/apache/cassandra/cql3/ColumnIdentifierTest.java b/test/unit/org/apache/cassandra/cql3/ColumnIdentifierTest.java
index 3a34ad5..ea483c4 100644
--- a/test/unit/org/apache/cassandra/cql3/ColumnIdentifierTest.java
+++ b/test/unit/org/apache/cassandra/cql3/ColumnIdentifierTest.java
@@ -28,6 +28,7 @@
 import org.apache.cassandra.db.marshal.BytesType;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.utils.ByteBufferUtil;
+import static org.junit.Assert.assertEquals;
 
 public class ColumnIdentifierTest
 {
@@ -59,6 +60,24 @@
     {
         return v < 0 ? -1 : v > 0 ? 1 : 0;
     }
+    
+    @Test
+    public void testMaybeQuote()
+    {
+        String unquotable = "a";
+        assertEquals(unquotable, ColumnIdentifier.maybeQuote(unquotable));
+        unquotable = "z4";
+        assertEquals(unquotable, ColumnIdentifier.maybeQuote(unquotable));
+        unquotable = "m_4_";
+        assertEquals(unquotable, ColumnIdentifier.maybeQuote(unquotable));
+        unquotable = "f__";
+        assertEquals(unquotable, ColumnIdentifier.maybeQuote(unquotable));
+        
+        assertEquals("\"A\"", ColumnIdentifier.maybeQuote("A"));
+        assertEquals("\"4b\"", ColumnIdentifier.maybeQuote("4b"));
+        assertEquals("\"\"\"\"", ColumnIdentifier.maybeQuote("\""));
+        assertEquals("\"\"\"a\"\"b\"\"\"", ColumnIdentifier.maybeQuote("\"a\"b\""));
+    }
 
     @Test
     public void testInternedCache()
diff --git a/test/unit/org/apache/cassandra/cql3/CqlParserTest.java b/test/unit/org/apache/cassandra/cql3/CqlParserTest.java
index 13c4685..4871c09 100644
--- a/test/unit/org/apache/cassandra/cql3/CqlParserTest.java
+++ b/test/unit/org/apache/cassandra/cql3/CqlParserTest.java
@@ -25,6 +25,7 @@
 import org.antlr.runtime.CommonTokenStream;
 import org.antlr.runtime.RecognitionException;
 import org.antlr.runtime.TokenStream;
+import org.apache.cassandra.cql3.statements.PropertyDefinitions;
 
 import static org.junit.Assert.*;
 
@@ -36,7 +37,7 @@
         SyntaxErrorCounter firstCounter = new SyntaxErrorCounter();
         SyntaxErrorCounter secondCounter = new SyntaxErrorCounter();
 
-        CharStream stream = new ANTLRStringStream("SELECT * FORM users");
+        CharStream stream = new ANTLRStringStream("SELECT * FORM FROM test");
         CqlLexer lexer = new CqlLexer(stream);
 
         TokenStream tokenStream = new CommonTokenStream(lexer);
@@ -44,11 +45,14 @@
         parser.addErrorListener(firstCounter);
         parser.addErrorListener(secondCounter);
 
-        parser.query();
+        // By default CqlParser should recover from the syntax error by removing FORM
+        // but as recoverFromMismatchedToken and recover have been overloaded, it will not
+        // and the returned ParsedStatement will be null.
+        assertNull(parser.query());
 
-        // ANTLR 3.5 reports 2 errors in the sentence above (missing FROM and missing EOF).
-        assertTrue(firstCounter.count > 0);
-        assertTrue(secondCounter.count > 0);
+        // Only one error must be reported (mismatched: FORM).
+        assertEquals(1, firstCounter.count);
+        assertEquals(1, secondCounter.count);
     }
 
     @Test
@@ -68,10 +72,39 @@
 
         parser.query();
 
-        assertTrue(firstCounter.count > 0);
+        assertEquals(1, firstCounter.count);
         assertEquals(0, secondCounter.count);
     }
 
+    @Test
+    public void testDuplicateProperties() throws Exception
+    {
+        parseAndCountErrors("properties = { 'foo' : 'value1', 'bar': 'value2' };", 0, (p) -> p.properties(new PropertyDefinitions()));
+        parseAndCountErrors("properties = { 'foo' : 'value1', 'foo': 'value2' };", 1, (p) -> p.properties(new PropertyDefinitions()));
+        parseAndCountErrors("foo = 'value1' AND bar = 'value2' };", 0, (p) -> p.properties(new PropertyDefinitions()));
+        parseAndCountErrors("foo = 'value1' AND foo = 'value2' };", 1, (p) -> p.properties(new PropertyDefinitions()));
+    }
+
+    private void parseAndCountErrors(String cql, int expectedErrors, ParserOperation operation) throws RecognitionException
+    {
+        SyntaxErrorCounter counter = new SyntaxErrorCounter();
+        CharStream stream = new ANTLRStringStream(cql);
+        CqlLexer lexer = new CqlLexer(stream);
+        TokenStream tokenStream = new CommonTokenStream(lexer);
+        CqlParser parser = new CqlParser(tokenStream);
+        parser.addErrorListener(counter);
+
+        operation.perform(parser);
+
+        assertEquals(expectedErrors, counter.count);
+    }
+
+    @FunctionalInterface
+    private interface ParserOperation
+    {
+        void perform(CqlParser cqlParser) throws RecognitionException;
+    }
+
     private static final class SyntaxErrorCounter implements ErrorListener
     {
         private int count;
diff --git a/test/unit/org/apache/cassandra/cql3/DistinctQueryPagingTest.java b/test/unit/org/apache/cassandra/cql3/DistinctQueryPagingTest.java
index f433179..6f0477d 100644
--- a/test/unit/org/apache/cassandra/cql3/DistinctQueryPagingTest.java
+++ b/test/unit/org/apache/cassandra/cql3/DistinctQueryPagingTest.java
@@ -28,7 +28,7 @@
     @Test
     public void testSelectDistinct() throws Throwable
     {
-        // Test a regular (CQL3) table.
+        // Test a regular(CQL3) table.
         createTable("CREATE TABLE %s (pk0 int, pk1 int, ck0 int, val int, PRIMARY KEY((pk0, pk1), ck0))");
 
         for (int i = 0; i < 3; i++)
@@ -49,7 +49,7 @@
         assertInvalidMessage("queries must request all the partition key columns", "SELECT DISTINCT pk0 FROM %s");
         assertInvalidMessage("queries must only request partition key columns", "SELECT DISTINCT pk0, pk1, ck0 FROM %s");
 
-        // Test a 'compact storage' table.
+        //Test a 'compact storage' table.
         createTable("CREATE TABLE %s (pk0 int, pk1 int, val int, PRIMARY KEY((pk0, pk1))) WITH COMPACT STORAGE");
 
         for (int i = 0; i < 3; i++)
@@ -166,18 +166,20 @@
 
         execute("INSERT INTO %s (k, a, b, s, s1) VALUES (?, ?, ?, ?, ?)", 2, 10, 10, 10, 10);
 
-        assertRows(execute("SELECT DISTINCT k, s, s1 FROM %s WHERE s = 90 AND s1 = 90 ALLOW FILTERING"),
-                   row(9, 90, 90));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT DISTINCT k, s, s1 FROM %s WHERE s = 90 AND s1 = 90 ALLOW FILTERING"),
+                       row(9, 90, 90));
 
-        assertRows(execute("SELECT DISTINCT k, s, s1 FROM %s WHERE s = 90 AND s1 = 90 ALLOW FILTERING"),
-                   row(9, 90, 90));
+            assertRows(execute("SELECT DISTINCT k, s, s1 FROM %s WHERE s = 90 AND s1 = 90 ALLOW FILTERING"),
+                       row(9, 90, 90));
 
-        assertRows(execute("SELECT DISTINCT k, s, s1 FROM %s WHERE s = 10 AND s1 = 10 ALLOW FILTERING"),
-                   row(1, 10, 10),
-                   row(2, 10, 10));
+            assertRows(execute("SELECT DISTINCT k, s, s1 FROM %s WHERE s = 10 AND s1 = 10 ALLOW FILTERING"),
+                       row(1, 10, 10),
+                       row(2, 10, 10));
 
-        assertRows(execute("SELECT DISTINCT k, s, s1 FROM %s WHERE k = 1 AND s = 10 AND s1 = 10 ALLOW FILTERING"),
-                   row(1, 10, 10));
+            assertRows(execute("SELECT DISTINCT k, s, s1 FROM %s WHERE k = 1 AND s = 10 AND s1 = 10 ALLOW FILTERING"),
+                       row(1, 10, 10));
+        });
     }
 
     @Test
diff --git a/test/unit/org/apache/cassandra/cql3/DurationTest.java b/test/unit/org/apache/cassandra/cql3/DurationTest.java
new file mode 100644
index 0000000..b8f4400
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/DurationTest.java
@@ -0,0 +1,114 @@
+/**
+ * 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.Assert;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.cassandra.exceptions.InvalidRequestException;
+
+import static org.apache.cassandra.cql3.Duration.*;
+import static org.apache.cassandra.cql3.Duration.NANOS_PER_HOUR;
+
+public class DurationTest
+{
+    @Test
+    public void testFromStringWithStandardPattern()
+    {
+        assertEquals(Duration.newInstance(14, 0, 0), Duration.from("1y2mo"));
+        assertEquals(Duration.newInstance(-14, 0, 0), Duration.from("-1y2mo"));
+        assertEquals(Duration.newInstance(14, 0, 0), Duration.from("1Y2MO"));
+        assertEquals(Duration.newInstance(0, 14, 0), Duration.from("2w"));
+        assertEquals(Duration.newInstance(0, 2, 10 * NANOS_PER_HOUR), Duration.from("2d10h"));
+        assertEquals(Duration.newInstance(0, 2, 0), Duration.from("2d"));
+        assertEquals(Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR), Duration.from("30h"));
+        assertEquals(Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR + 20 * NANOS_PER_MINUTE), Duration.from("30h20m"));
+        assertEquals(Duration.newInstance(0, 0, 20 * NANOS_PER_MINUTE), Duration.from("20m"));
+        assertEquals(Duration.newInstance(0, 0, 56 * NANOS_PER_SECOND), Duration.from("56s"));
+        assertEquals(Duration.newInstance(0, 0, 567 * NANOS_PER_MILLI), Duration.from("567ms"));
+        assertEquals(Duration.newInstance(0, 0, 1950 * NANOS_PER_MICRO), Duration.from("1950us"));
+        assertEquals(Duration.newInstance(0, 0, 1950 * NANOS_PER_MICRO), Duration.from("1950µs"));
+        assertEquals(Duration.newInstance(0, 0, 1950000), Duration.from("1950000ns"));
+        assertEquals(Duration.newInstance(0, 0, 1950000), Duration.from("1950000NS"));
+        assertEquals(Duration.newInstance(0, 0, -1950000), Duration.from("-1950000ns"));
+        assertEquals(Duration.newInstance(15, 0, 130 * NANOS_PER_MINUTE), Duration.from("1y3mo2h10m"));
+    }
+
+    @Test
+    public void testFromStringWithIso8601Pattern()
+    {
+        assertEquals(Duration.newInstance(12, 2, 0), Duration.from("P1Y2D"));
+        assertEquals(Duration.newInstance(14, 0, 0), Duration.from("P1Y2M"));
+        assertEquals(Duration.newInstance(0, 14, 0), Duration.from("P2W"));
+        assertEquals(Duration.newInstance(12, 0, 2 * NANOS_PER_HOUR), Duration.from("P1YT2H"));
+        assertEquals(Duration.newInstance(-14, 0, 0), Duration.from("-P1Y2M"));
+        assertEquals(Duration.newInstance(0, 2, 0), Duration.from("P2D"));
+        assertEquals(Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR), Duration.from("PT30H"));
+        assertEquals(Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR + 20 * NANOS_PER_MINUTE), Duration.from("PT30H20M"));
+        assertEquals(Duration.newInstance(0, 0, 20 * NANOS_PER_MINUTE), Duration.from("PT20M"));
+        assertEquals(Duration.newInstance(0, 0, 56 * NANOS_PER_SECOND), Duration.from("PT56S"));
+        assertEquals(Duration.newInstance(15, 0, 130 * NANOS_PER_MINUTE), Duration.from("P1Y3MT2H10M"));
+    }
+
+    @Test
+    public void testFromStringWithIso8601AlternativePattern()
+    {
+        assertEquals(Duration.newInstance(12, 2, 0), Duration.from("P0001-00-02T00:00:00"));
+        assertEquals(Duration.newInstance(14, 0, 0), Duration.from("P0001-02-00T00:00:00"));
+        assertEquals(Duration.newInstance(12, 0, 2 * NANOS_PER_HOUR), Duration.from("P0001-00-00T02:00:00"));
+        assertEquals(Duration.newInstance(-14, 0, 0), Duration.from("-P0001-02-00T00:00:00"));
+        assertEquals(Duration.newInstance(0, 2, 0), Duration.from("P0000-00-02T00:00:00"));
+        assertEquals(Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR), Duration.from("P0000-00-00T30:00:00"));
+        assertEquals(Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR + 20 * NANOS_PER_MINUTE), Duration.from("P0000-00-00T30:20:00"));
+        assertEquals(Duration.newInstance(0, 0, 20 * NANOS_PER_MINUTE), Duration.from("P0000-00-00T00:20:00"));
+        assertEquals(Duration.newInstance(0, 0, 56 * NANOS_PER_SECOND), Duration.from("P0000-00-00T00:00:56"));
+        assertEquals(Duration.newInstance(15, 0, 130 * NANOS_PER_MINUTE), Duration.from("P0001-03-00T02:10:00"));
+    }
+
+    @Test
+    public void testInvalidDurations()
+    {
+        assertInvalidDuration(Long.MAX_VALUE + "d", "Invalid duration. The total number of days must be less or equal to 2147483647");
+        assertInvalidDuration("2µ", "Unable to convert '2µ' to a duration");
+        assertInvalidDuration("-2µ", "Unable to convert '2µ' to a duration");
+        assertInvalidDuration("12.5s", "Unable to convert '12.5s' to a duration");
+        assertInvalidDuration("2m12.5s", "Unable to convert '2m12.5s' to a duration");
+        assertInvalidDuration("2m-12s", "Unable to convert '2m-12s' to a duration");
+        assertInvalidDuration("12s3s", "Invalid duration. The seconds are specified multiple times");
+        assertInvalidDuration("12s3m", "Invalid duration. The seconds should be after minutes");
+        assertInvalidDuration("1Y3M4D", "Invalid duration. The minutes should be after days");
+        assertInvalidDuration("P2Y3W", "Unable to convert 'P2Y3W' to a duration");
+        assertInvalidDuration("P0002-00-20", "Unable to convert 'P0002-00-20' to a duration");
+    }
+
+    public void assertInvalidDuration(String duration, String expectedErrorMessage)
+    {
+        try
+        {
+            System.out.println(Duration.from(duration));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            assertEquals(expectedErrorMessage, e.getMessage());
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/EmptyValuesTest.java b/test/unit/org/apache/cassandra/cql3/EmptyValuesTest.java
index 6c42a59..3652ac8 100644
--- a/test/unit/org/apache/cassandra/cql3/EmptyValuesTest.java
+++ b/test/unit/org/apache/cassandra/cql3/EmptyValuesTest.java
@@ -46,7 +46,7 @@
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.db.marshal.UUIDType;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 import static org.junit.Assume.assumeTrue;
@@ -60,12 +60,12 @@
         Assert.assertTrue(row.getColumns().stream().anyMatch(c -> c.name.toString().equals("v")));
         Assert.assertEquals(0, row.getBytes("v").remaining());
 
-        ResultSet resultNet = executeNet(Server.CURRENT_VERSION, "SELECT * FROM %s");
+        ResultSet resultNet = executeNet(ProtocolVersion.CURRENT, "SELECT * FROM %s");
         Row rowNet = resultNet.one();
         Assert.assertTrue(rowNet.getColumnDefinitions().contains("v"));
         Assert.assertEquals(0, rowNet.getBytesUnsafe("v").remaining());
 
-        ResultSet jsonNet = executeNet(Server.CURRENT_VERSION, "SELECT JSON * FROM %s");
+        ResultSet jsonNet = executeNet(ProtocolVersion.CURRENT, "SELECT JSON * FROM %s");
         Row jsonRowNet = jsonNet.one();
         Assert.assertTrue(jsonRowNet.getString("[json]"), jsonRowNet.getString("[json]").matches(".*\"v\"\\s*:\\s*\"" + Pattern.quote(emptyValue) + "\".*"));
 
diff --git a/test/unit/org/apache/cassandra/cql3/GcCompactionTest.java b/test/unit/org/apache/cassandra/cql3/GcCompactionTest.java
new file mode 100644
index 0000000..ee16a4d
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/GcCompactionTest.java
@@ -0,0 +1,522 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Function;
+
+import com.google.common.collect.Iterables;
+import org.junit.Test;
+
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.compaction.CompactionManager;
+import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.io.sstable.ISSTableScanner;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.schema.CompactionParams;
+import org.apache.cassandra.schema.CompactionParams.TombstoneOption;
+import org.apache.cassandra.utils.FBUtilities;
+
+public class GcCompactionTest extends CQLTester
+{
+    static final int KEY_COUNT = 10;
+    static final int CLUSTERING_COUNT = 20;
+
+    // Test needs synchronous table drop to avoid flushes causing flaky failures
+
+    @Override
+    protected String createTable(String query)
+    {
+        return super.createTable(KEYSPACE_PER_TEST, query);
+    }
+
+    @Override
+    protected UntypedResultSet execute(String query, Object... values) throws Throwable
+    {
+        return executeFormattedQuery(formatQuery(KEYSPACE_PER_TEST, query), values);
+    }
+
+    @Override
+    public ColumnFamilyStore getCurrentColumnFamilyStore()
+    {
+        return super.getCurrentColumnFamilyStore(KEYSPACE_PER_TEST);
+    }
+
+    public void flush()
+    {
+        flush(KEYSPACE_PER_TEST);
+    }
+
+    @Test
+    public void testGcCompactionPartitions() throws Throwable
+    {
+        runCompactionTest("CREATE TABLE %s(" +
+                          "  key int," +
+                          "  column int," +
+                          "  data int," +
+                          "  extra text," +
+                          "  PRIMARY KEY((key, column), data)" +
+                          ") WITH compaction = { 'class' :  'SizeTieredCompactionStrategy', 'provide_overlapping_tombstones' : 'row'  };"
+                          );
+
+    }
+
+    @Test
+    public void testGcCompactionRows() throws Throwable
+    {
+        runCompactionTest("CREATE TABLE %s(" +
+                          "  key int," +
+                          "  column int," +
+                          "  data int," +
+                          "  extra text," +
+                          "  PRIMARY KEY(key, column)" +
+                          ") WITH compaction = { 'class' :  'SizeTieredCompactionStrategy', 'provide_overlapping_tombstones' : 'row'  };"
+                          );
+
+    }
+
+    @Test
+    public void testGcCompactionRanges() throws Throwable
+    {
+
+        runCompactionTest("CREATE TABLE %s(" +
+                          "  key int," +
+                          "  column int," +
+                          "  col2 int," +
+                          "  data int," +
+                          "  extra text," +
+                          "  PRIMARY KEY(key, column, data)" +
+                          ") WITH compaction = { 'class' :  'SizeTieredCompactionStrategy', 'provide_overlapping_tombstones' : 'row'  };"
+                          );
+    }
+
+    private void runCompactionTest(String tableDef) throws Throwable
+    {
+        createTable(tableDef);
+
+        for (int i = 0; i < KEY_COUNT; ++i)
+            for (int j = 0; j < CLUSTERING_COUNT; ++j)
+                execute("INSERT INTO %s (key, column, data, extra) VALUES (?, ?, ?, ?)", i, j, i+j, "" + i + ":" + j);
+
+        Set<SSTableReader> readers = new HashSet<>();
+        ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+
+        flush();
+        assertEquals(1, cfs.getLiveSSTables().size());
+        SSTableReader table0 = getNewTable(readers);
+        assertEquals(0, countTombstoneMarkers(table0));
+        int rowCount = countRows(table0);
+
+        deleteWithSomeInserts(3, 5, 10);
+        flush();
+        assertEquals(2, cfs.getLiveSSTables().size());
+        SSTableReader table1 = getNewTable(readers);
+        assertTrue(countRows(table1) > 0);
+        assertTrue(countTombstoneMarkers(table1) > 0);
+
+        deleteWithSomeInserts(5, 6, 0);
+        flush();
+        assertEquals(3, cfs.getLiveSSTables().size());
+        SSTableReader table2 = getNewTable(readers);
+        assertEquals(0, countRows(table2));
+        assertTrue(countTombstoneMarkers(table2) > 0);
+
+        CompactionManager.instance.forceUserDefinedCompaction(table0.getFilename());
+
+        assertEquals(3, cfs.getLiveSSTables().size());
+        SSTableReader table3 = getNewTable(readers);
+        assertEquals(0, countTombstoneMarkers(table3));
+        assertTrue(rowCount > countRows(table3));
+    }
+
+    @Test
+    public void testGarbageCollectRetainsLCSLevel() throws Throwable
+    {
+
+      createTable("CREATE TABLE %s(" +
+                  "  key int," +
+                  "  column int," +
+                  "  data int," +
+                  "  PRIMARY KEY ((key), column)" +
+                  ") WITH compaction = { 'class' : 'LeveledCompactionStrategy' };");
+
+      assertEquals("LeveledCompactionStrategy", getCurrentColumnFamilyStore().getCompactionStrategyManager().getName());
+
+      for (int i = 0; i < KEY_COUNT; ++i)
+          for (int j = 0; j < CLUSTERING_COUNT; ++j)
+              execute("INSERT INTO %s (key, column, data) VALUES (?, ?, ?)", i, j, i * j + j);
+
+      ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+      cfs.disableAutoCompaction();
+      flush();
+      assertEquals(1, cfs.getLiveSSTables().size());
+      SSTableReader table = cfs.getLiveSSTables().iterator().next();
+      int gen = table.descriptor.generation;
+      assertEquals(KEY_COUNT * CLUSTERING_COUNT, countRows(table));
+
+      assertEquals(0, table.getSSTableLevel()); // flush writes to L0
+
+      CompactionManager.AllSSTableOpStatus status;
+      status = CompactionManager.instance.performGarbageCollection(cfs, TombstoneOption.ROW, 1);
+      assertEquals(CompactionManager.AllSSTableOpStatus.SUCCESSFUL, status);
+
+      assertEquals(1, cfs.getLiveSSTables().size());
+      SSTableReader collected = cfs.getLiveSSTables().iterator().next();
+      int collectedGen = collected.descriptor.generation;
+      assertTrue(collectedGen > gen);
+      assertEquals(KEY_COUNT * CLUSTERING_COUNT, countRows(collected));
+
+      assertEquals(0, collected.getSSTableLevel()); // garbagecollect should leave the LCS level where it was
+
+      CompactionManager.instance.performMaximal(cfs, false);
+
+      assertEquals(1, cfs.getLiveSSTables().size());
+      SSTableReader compacted = cfs.getLiveSSTables().iterator().next();
+      int compactedGen = compacted.descriptor.generation;
+      assertTrue(compactedGen > collectedGen);
+      assertEquals(KEY_COUNT * CLUSTERING_COUNT, countRows(compacted));
+
+      assertEquals(1, compacted.getSSTableLevel()); // full compaction with LCS should move to L1
+
+      status = CompactionManager.instance.performGarbageCollection(cfs, TombstoneOption.ROW, 1);
+      assertEquals(CompactionManager.AllSSTableOpStatus.SUCCESSFUL, status);
+
+      assertEquals(1, cfs.getLiveSSTables().size());
+      SSTableReader collected2 = cfs.getLiveSSTables().iterator().next();
+      assertTrue(collected2.descriptor.generation > compactedGen);
+      assertEquals(KEY_COUNT * CLUSTERING_COUNT, countRows(collected2));
+
+      assertEquals(1, collected2.getSSTableLevel()); // garbagecollect should leave the LCS level where it was
+    }
+
+    @Test
+    public void testGarbageCollectOrder() throws Throwable
+    {
+        // partition-level deletions, 0 gc_grace
+        createTable("CREATE TABLE %s(" +
+                    "  key int," +
+                    "  column int," +
+                    "  col2 int," +
+                    "  data int," +
+                    "  extra text," +
+                    "  PRIMARY KEY((key, column))" +
+                    ") WITH gc_grace_seconds = 0;"
+        );
+
+        assertEquals(1, getCurrentColumnFamilyStore().gcBefore(1)); // make sure gc_grace is 0
+
+        for (int i = 0; i < KEY_COUNT; ++i)
+            for (int j = 0; j < CLUSTERING_COUNT; ++j)
+                execute("INSERT INTO %s (key, column, data, extra) VALUES (?, ?, ?, ?)", i, j, i+j, "" + i + ":" + j);
+
+
+        Set<SSTableReader> readers = new HashSet<>();
+        ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+
+        flush();
+        assertEquals(1, cfs.getLiveSSTables().size());
+        SSTableReader table0 = getNewTable(readers);
+        assertEquals(0, countTombstoneMarkers(table0));
+        int rowCount0 = countRows(table0);
+
+        deleteWithSomeInserts(3, 5, 10);
+        flush();
+        assertEquals(2, cfs.getLiveSSTables().size());
+        SSTableReader table1 = getNewTable(readers);
+        final int rowCount1 = countRows(table1);
+        assertTrue(rowCount1 > 0);
+        assertTrue(countTombstoneMarkers(table1) > 0);
+
+        deleteWithSomeInserts(2, 4, 0);
+        flush();
+        assertEquals(3, cfs.getLiveSSTables().size());
+        SSTableReader table2 = getNewTable(readers);
+        assertEquals(0, countRows(table2));
+        assertTrue(countTombstoneMarkers(table2) > 0);
+
+        // Wait a little to make sure nowInSeconds is greater than gcBefore
+        Thread.sleep(1000);
+
+        CompactionManager.AllSSTableOpStatus status =
+                CompactionManager.instance.performGarbageCollection(getCurrentColumnFamilyStore(), CompactionParams.TombstoneOption.ROW, 1);
+        assertEquals(CompactionManager.AllSSTableOpStatus.SUCCESSFUL, status);
+
+        SSTableReader[] tables = cfs.getLiveSSTables().toArray(new SSTableReader[0]);
+        Arrays.sort(tables, (o1, o2) -> Integer.compare(o1.descriptor.generation, o2.descriptor.generation));  // by order of compaction
+
+        // Make sure deleted data was removed
+        assertTrue(rowCount0 > countRows(tables[0]));
+        assertTrue(rowCount1 > countRows(tables[1]));
+
+        // Make sure all tombstones got purged
+        for (SSTableReader t : tables)
+        {
+            assertEquals("Table " + t + " has tombstones", 0, countTombstoneMarkers(t));
+        }
+
+        // The last table should have become empty and be removed
+        assertEquals(2, tables.length);
+    }
+
+    @Test
+    public void testGcCompactionCells() throws Throwable
+    {
+        createTable("CREATE TABLE %s(" +
+                          "  key int," +
+                          "  column int," +
+                          "  data int," +
+                          "  extra text," +
+                          "  PRIMARY KEY(key)" +
+                          ") WITH compaction = { 'class' :  'SizeTieredCompactionStrategy', 'provide_overlapping_tombstones' : 'cell'  };"
+                          );
+
+        for (int i = 0; i < KEY_COUNT; ++i)
+            for (int j = 0; j < CLUSTERING_COUNT; ++j)
+                execute("INSERT INTO %s (key, column, data, extra) VALUES (?, ?, ?, ?)", i, j, i+j, "" + i + ":" + j);
+
+        Set<SSTableReader> readers = new HashSet<>();
+        ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+
+        flush();
+        assertEquals(1, cfs.getLiveSSTables().size());
+        SSTableReader table0 = getNewTable(readers);
+        assertEquals(0, countTombstoneMarkers(table0));
+        int cellCount = countCells(table0);
+
+        deleteWithSomeInserts(3, 0, 2);
+        flush();
+        assertEquals(2, cfs.getLiveSSTables().size());
+        SSTableReader table1 = getNewTable(readers);
+        assertTrue(countCells(table1) > 0);
+        assertEquals(0, countTombstoneMarkers(table0));
+
+        CompactionManager.instance.forceUserDefinedCompaction(table0.getFilename());
+
+        assertEquals(2, cfs.getLiveSSTables().size());
+        SSTableReader table3 = getNewTable(readers);
+        assertEquals(0, countTombstoneMarkers(table3));
+        assertTrue(cellCount > countCells(table3));
+    }
+
+    @Test
+    public void testGcCompactionStatic() throws Throwable
+    {
+        createTable("CREATE TABLE %s(" +
+                          "  key int," +
+                          "  column int," +
+                          "  data int static," +
+                          "  extra text," +
+                          "  PRIMARY KEY(key, column)" +
+                          ") WITH compaction = { 'class' :  'SizeTieredCompactionStrategy', 'provide_overlapping_tombstones' : 'cell'  };"
+                          );
+
+        for (int i = 0; i < KEY_COUNT; ++i)
+            for (int j = 0; j < CLUSTERING_COUNT; ++j)
+                execute("INSERT INTO %s (key, column, data, extra) VALUES (?, ?, ?, ?)", i, j, i+j, "" + i + ":" + j);
+
+        Set<SSTableReader> readers = new HashSet<>();
+        ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+
+        flush();
+        assertEquals(1, cfs.getLiveSSTables().size());
+        SSTableReader table0 = getNewTable(readers);
+        assertEquals(0, countTombstoneMarkers(table0));
+        int cellCount = countStaticCells(table0);
+        assertEquals(KEY_COUNT, cellCount);
+
+        execute("DELETE data FROM %s WHERE key = 0");   // delete static cell
+        execute("INSERT INTO %s (key, data) VALUES (1, 0)");  // overwrite static cell
+        flush();
+        assertEquals(2, cfs.getLiveSSTables().size());
+        SSTableReader table1 = getNewTable(readers);
+        assertTrue(countStaticCells(table1) > 0);
+        assertEquals(0, countTombstoneMarkers(table0));
+
+        CompactionManager.instance.forceUserDefinedCompaction(table0.getFilename());
+
+        assertEquals(2, cfs.getLiveSSTables().size());
+        SSTableReader table3 = getNewTable(readers);
+        assertEquals(0, countTombstoneMarkers(table3));
+        assertEquals(cellCount - 2, countStaticCells(table3));
+    }
+
+    @Test
+    public void testGcCompactionComplexColumn() throws Throwable
+    {
+        createTable("CREATE TABLE %s(" +
+                          "  key int," +
+                          "  data map<int, int>," +
+                          "  extra text," +
+                          "  PRIMARY KEY(key)" +
+                          ") WITH compaction = { 'class' :  'SizeTieredCompactionStrategy', 'provide_overlapping_tombstones' : 'cell'  };"
+                          );
+
+        for (int i = 0; i < KEY_COUNT; ++i)
+            for (int j = 0; j < CLUSTERING_COUNT; ++j)
+                execute("UPDATE %s SET data[?] = ? WHERE key = ?", j, i+j, i);
+
+        Set<SSTableReader> readers = new HashSet<>();
+        ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+
+        flush();
+        assertEquals(1, cfs.getLiveSSTables().size());
+        SSTableReader table0 = getNewTable(readers);
+        assertEquals(0, countTombstoneMarkers(table0));
+        int cellCount = countComplexCells(table0);
+
+        deleteWithSomeInsertsComplexColumn(3, 5, 8);
+        flush();
+        assertEquals(2, cfs.getLiveSSTables().size());
+        SSTableReader table1 = getNewTable(readers);
+        assertTrue(countComplexCells(table1) > 0);
+        assertEquals(0, countTombstoneMarkers(table0));
+
+        CompactionManager.instance.forceUserDefinedCompaction(table0.getFilename());
+
+        assertEquals(2, cfs.getLiveSSTables().size());
+        SSTableReader table3 = getNewTable(readers);
+        assertEquals(0, countTombstoneMarkers(table3));
+        assertEquals(cellCount - 23, countComplexCells(table3));
+    }
+
+    @Test
+    public void testLocalDeletionTime() throws Throwable
+    {
+        createTable("create table %s (k int, c1 int, primary key (k, c1)) with compaction = {'class': 'SizeTieredCompactionStrategy', 'provide_overlapping_tombstones':'row'}");
+        execute("delete from %s where k = 1");
+        Set<SSTableReader> readers = new HashSet<>(getCurrentColumnFamilyStore().getLiveSSTables());
+        getCurrentColumnFamilyStore().forceBlockingFlush();
+        SSTableReader oldSSTable = getNewTable(readers);
+        Thread.sleep(2000);
+        execute("delete from %s where k = 1");
+        getCurrentColumnFamilyStore().forceBlockingFlush();
+        SSTableReader newTable = getNewTable(readers);
+
+        CompactionManager.instance.forceUserDefinedCompaction(oldSSTable.getFilename());
+
+        // Old table now doesn't contain any data and should disappear.
+        assertEquals(Collections.singleton(newTable), getCurrentColumnFamilyStore().getLiveSSTables());
+    }
+
+    private SSTableReader getNewTable(Set<SSTableReader> readers)
+    {
+        Set<SSTableReader> newOnes = new HashSet<>(getCurrentColumnFamilyStore().getLiveSSTables());
+        newOnes.removeAll(readers);
+        assertEquals(1, newOnes.size());
+        readers.addAll(newOnes);
+        return Iterables.get(newOnes, 0);
+    }
+
+    void deleteWithSomeInserts(int key_step, int delete_step, int readd_step) throws Throwable
+    {
+        for (int i = 0; i < KEY_COUNT; i += key_step)
+        {
+            if (delete_step > 0)
+                for (int j = i % delete_step; j < CLUSTERING_COUNT; j += delete_step)
+                {
+                    execute("DELETE FROM %s WHERE key = ? AND column = ?", i, j);
+                }
+            if (readd_step > 0)
+                for (int j = i % readd_step; j < CLUSTERING_COUNT; j += readd_step)
+                {
+                    execute("INSERT INTO %s (key, column, data, extra) VALUES (?, ?, ?, ?)", i, j, i-j, "readded " + i + ":" + j);
+                }
+        }
+    }
+
+    void deleteWithSomeInsertsComplexColumn(int key_step, int delete_step, int readd_step) throws Throwable
+    {
+        for (int i = 0; i < KEY_COUNT; i += key_step)
+        {
+            if (delete_step > 0)
+                for (int j = i % delete_step; j < CLUSTERING_COUNT; j += delete_step)
+                {
+                    execute("DELETE data[?] FROM %s WHERE key = ?", j, i);
+                }
+            if (readd_step > 0)
+                for (int j = i % readd_step; j < CLUSTERING_COUNT; j += readd_step)
+                {
+                    execute("UPDATE %s SET data[?] = ? WHERE key = ?", j, -(i+j), i);
+                }
+        }
+    }
+
+    int countTombstoneMarkers(SSTableReader reader)
+    {
+        int nowInSec = FBUtilities.nowInSeconds();
+        return count(reader, x -> x.isRangeTombstoneMarker() || x.isRow() && ((Row) x).hasDeletion(nowInSec) ? 1 : 0, x -> x.partitionLevelDeletion().isLive() ? 0 : 1);
+    }
+
+    int countRows(SSTableReader reader)
+    {
+        boolean enforceStrictLiveness = reader.metadata.enforceStrictLiveness();
+        int nowInSec = FBUtilities.nowInSeconds();
+        return count(reader, x -> x.isRow() && ((Row) x).hasLiveData(nowInSec, enforceStrictLiveness) ? 1 : 0, x -> 0);
+    }
+
+    int countCells(SSTableReader reader)
+    {
+        return count(reader, x -> x.isRow() ? Iterables.size((Row) x) : 0, x -> 0);
+    }
+
+    int countStaticCells(SSTableReader reader)
+    {
+        return count(reader, x -> 0, x -> Iterables.size(x.staticRow()));
+    }
+
+    int countComplexCells(SSTableReader reader)
+    {
+        return count(reader, x -> x.isRow() ? ((Row) x).columnData().stream().mapToInt(this::countComplex).sum() : 0, x -> 0);
+    }
+
+    int countComplex(ColumnData c)
+    {
+        if (!(c instanceof ComplexColumnData))
+            return 0;
+        ComplexColumnData ccd = (ComplexColumnData) c;
+        return ccd.cellsCount();
+    }
+
+    int count(SSTableReader reader, Function<Unfiltered, Integer> predicate, Function<UnfilteredRowIterator, Integer> partitionPredicate)
+    {
+        int instances = 0;
+        try (ISSTableScanner partitions = reader.getScanner())
+        {
+            while (partitions.hasNext())
+            {
+                try (UnfilteredRowIterator iter = partitions.next())
+                {
+                    instances += partitionPredicate.apply(iter);
+                    while (iter.hasNext())
+                    {
+                        Unfiltered atom = iter.next();
+                        instances += predicate.apply(atom);
+                    }
+                }
+            }
+        }
+        return instances;
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/IndexQueryPagingTest.java b/test/unit/org/apache/cassandra/cql3/IndexQueryPagingTest.java
index fd1e661..e2220ac 100644
--- a/test/unit/org/apache/cassandra/cql3/IndexQueryPagingTest.java
+++ b/test/unit/org/apache/cassandra/cql3/IndexQueryPagingTest.java
@@ -92,6 +92,34 @@
     }
 
     @Test
+    public void testPagingOnPartitionsWithoutRows() throws Throwable
+    {
+        requireNetwork();
+
+        createTable("CREATE TABLE %s (pk int, ck int, s int static, v int, PRIMARY KEY (pk, ck))");
+        createIndex("CREATE INDEX on %s(s)");
+
+        execute("INSERT INTO %s (pk, s) VALUES (201, 200);");
+        execute("INSERT INTO %s (pk, s) VALUES (202, 200);");
+        execute("INSERT INTO %s (pk, s) VALUES (203, 200);");
+        execute("INSERT INTO %s (pk, s) VALUES (100, 100);");
+
+        for (int pageSize = 1; pageSize < 10; pageSize++)
+        {
+            assertRowsNet(executeNetWithPaging("select * from %s where s = 200 and pk = 201;", pageSize),
+                          row(201, null, 200, null));
+
+            assertRowsNet(executeNetWithPaging("select * from %s where s = 200;", pageSize),
+                          row(201, null, 200, null),
+                          row(203, null, 200, null),
+                          row(202, null, 200, null));
+
+            assertRowsNet(executeNetWithPaging("select * from %s where s = 100;", pageSize),
+                          row(100, null, 100, null));
+        }
+    }
+
+    @Test
     public void testPagingOnPartitionsWithoutClusteringColumns() throws Throwable
     {
         createTable("CREATE TABLE %s (pk int PRIMARY KEY, v int)");
diff --git a/test/unit/org/apache/cassandra/cql3/KeyCacheCqlTest.java b/test/unit/org/apache/cassandra/cql3/KeyCacheCqlTest.java
index 3f87343..1fb6e35 100644
--- a/test/unit/org/apache/cassandra/cql3/KeyCacheCqlTest.java
+++ b/test/unit/org/apache/cassandra/cql3/KeyCacheCqlTest.java
@@ -22,14 +22,17 @@
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
+import java.util.concurrent.Callable;
 
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.cassandra.cache.KeyCacheKey;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.index.Index;
 import org.apache.cassandra.metrics.CacheMetrics;
 import org.apache.cassandra.metrics.CassandraMetricsRegistry;
 import org.apache.cassandra.schema.CachingParams;
@@ -40,6 +43,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertNull;
+
 import org.apache.cassandra.utils.Pair;
 
 
@@ -129,7 +133,20 @@
     }
 
     @Test
-    public void testSliceQueries() throws Throwable
+    public void testSliceQueriesShallowIndexEntry() throws Throwable
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(0);
+        testSliceQueries();
+    }
+
+    @Test
+    public void testSliceQueriesIndexInfoOnHeap() throws Throwable
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(8);
+        testSliceQueries();
+    }
+
+    private void testSliceQueries() throws Throwable
     {
         createTable("CREATE TABLE %s (pk text, ck1 int, ck2 int, val text, vpk text, vck1 int, vck2 int, PRIMARY KEY (pk, ck1, ck2))");
 
@@ -213,7 +230,20 @@
     }
 
     @Test
-    public void test2iKeyCachePaths() throws Throwable
+    public void test2iKeyCachePathsShallowIndexEntry() throws Throwable
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(0);
+        test2iKeyCachePaths();
+    }
+
+    @Test
+    public void test2iKeyCachePathsIndexInfoOnHeap() throws Throwable
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(8);
+        test2iKeyCachePaths();
+    }
+
+    private void test2iKeyCachePaths() throws Throwable
     {
         String table = createTable("CREATE TABLE %s ("
                                    + commonColumnsDef
@@ -224,16 +254,21 @@
 
         CacheMetrics metrics = CacheService.instance.keyCache.getMetrics();
 
+        long expectedRequests = 0;
+
         for (int i = 0; i < 10; i++)
         {
             UntypedResultSet result = execute("SELECT part_key_a FROM %s WHERE col_int = ?", i);
             assertEquals(500, result.size());
+            // Index requests and table requests are both added to the same metric
+            // We expect 10 requests on the index SSTables and 10 IN requests on the table SSTables + BF false positives
+            expectedRequests += recentBloomFilterFalsePositives() + 20;
         }
 
         long hits = metrics.hits.getCount();
         long requests = metrics.requests.getCount();
         assertEquals(0, hits);
-        assertEquals(210, requests);
+        assertEquals(expectedRequests, requests);
 
         for (int i = 0; i < 10; i++)
         {
@@ -242,13 +277,16 @@
             // indexed on part-key % 10 = 10 index partitions
             // (50 clust-keys  *  100-part-keys  /  10 possible index-values) = 500
             assertEquals(500, result.size());
+            // Index requests and table requests are both added to the same metric
+            // We expect 10 requests on the index SSTables and 10 IN requests on the table SSTables + BF false positives
+            expectedRequests += recentBloomFilterFalsePositives() + 20;
         }
 
         metrics = CacheService.instance.keyCache.getMetrics();
         hits = metrics.hits.getCount();
         requests = metrics.requests.getCount();
         assertEquals(200, hits);
-        assertEquals(420, requests);
+        assertEquals(expectedRequests, requests);
 
         CacheService.instance.keyCache.submitWrite(Integer.MAX_VALUE).get();
 
@@ -290,7 +328,20 @@
     }
 
     @Test
-    public void test2iKeyCachePathsSaveKeysForDroppedTable() throws Throwable
+    public void test2iKeyCachePathsSaveKeysForDroppedTableShallowIndexEntry() throws Throwable
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(0);
+        test2iKeyCachePathsSaveKeysForDroppedTable();
+    }
+
+    @Test
+    public void test2iKeyCachePathsSaveKeysForDroppedTableIndexInfoOnHeap() throws Throwable
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(8);
+        test2iKeyCachePathsSaveKeysForDroppedTable();
+    }
+
+    private void test2iKeyCachePathsSaveKeysForDroppedTable() throws Throwable
     {
         String table = createTable("CREATE TABLE %s ("
                                    + commonColumnsDef
@@ -301,18 +352,22 @@
 
         CacheMetrics metrics = CacheService.instance.keyCache.getMetrics();
 
+        long expectedNumberOfRequests = 0;
+
         for (int i = 0; i < 10; i++)
         {
             UntypedResultSet result = execute("SELECT part_key_a FROM %s WHERE col_int = ?", i);
             assertEquals(500, result.size());
+
+            // Index requests and table requests are both added to the same metric
+            // We expect 10 requests on the index SSTables and 10 IN requests on the table SSTables + BF false positives
+            expectedNumberOfRequests += recentBloomFilterFalsePositives() + 20;
         }
 
         long hits = metrics.hits.getCount();
         long requests = metrics.requests.getCount();
         assertEquals(0, hits);
-        assertEquals(210, requests);
-
-        //
+        assertEquals(expectedNumberOfRequests, requests);
 
         for (int i = 0; i < 10; i++)
         {
@@ -321,13 +376,17 @@
             // indexed on part-key % 10 = 10 index partitions
             // (50 clust-keys  *  100-part-keys  /  10 possible index-values) = 500
             assertEquals(500, result.size());
+
+            // Index requests and table requests are both added to the same metric
+            // We expect 10 requests on the index SSTables and 10 IN requests on the table SSTables + BF false positives
+            expectedNumberOfRequests += recentBloomFilterFalsePositives() + 20;
         }
 
         metrics = CacheService.instance.keyCache.getMetrics();
         hits = metrics.hits.getCount();
         requests = metrics.requests.getCount();
         assertEquals(200, hits);
-        assertEquals(420, requests);
+        assertEquals(expectedNumberOfRequests, requests);
 
         dropTable("DROP TABLE %s");
 
@@ -350,7 +409,20 @@
     }
 
     @Test
-    public void testKeyCacheNonClustered() throws Throwable
+    public void testKeyCacheNonClusteredShallowIndexEntry() throws Throwable
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(0);
+        testKeyCacheNonClustered();
+    }
+
+    @Test
+    public void testKeyCacheNonClusteredIndexInfoOnHeap() throws Throwable
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(8);
+        testKeyCacheNonClustered();
+    }
+
+    private void testKeyCacheNonClustered() throws Throwable
     {
         String table = createTable("CREATE TABLE %s ("
                                    + commonColumnsDef
@@ -358,10 +430,15 @@
         insertData(table, null, false);
         clearCache();
 
+        long expectedNumberOfRequests = 0;
+
         for (int i = 0; i < 10; i++)
         {
             assertRows(execute("SELECT col_text FROM %s WHERE part_key_a = ? AND part_key_b = ?", i, Integer.toOctalString(i)),
                        new Object[]{ String.valueOf(i) + '-' + String.valueOf(0) });
+
+            // the data for the key is in 1 SSTable but we have to take into account bloom filter false positive
+            expectedNumberOfRequests += recentBloomFilterFalsePositives() + 1;
         }
 
         CacheMetrics metrics = CacheService.instance.keyCache.getMetrics();
@@ -374,16 +451,32 @@
         {
             assertRows(execute("SELECT col_text FROM %s WHERE part_key_a = ? AND part_key_b = ?", i, Integer.toOctalString(i)),
                        new Object[]{ String.valueOf(i) + '-' + String.valueOf(0) });
+
+            // the data for the key is in 1 SSTable but we have to take into account bloom filter false positive
+            expectedNumberOfRequests += recentBloomFilterFalsePositives() + 1;
         }
 
         hits = metrics.hits.getCount();
         requests = metrics.requests.getCount();
         assertEquals(10, hits);
-        assertEquals(120, requests);
+        assertEquals(expectedNumberOfRequests, requests);
     }
 
     @Test
-    public void testKeyCacheClustered() throws Throwable
+    public void testKeyCacheClusteredShallowIndexEntry() throws Throwable
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(0);
+        testKeyCacheClustered();
+    }
+
+    @Test
+    public void testKeyCacheClusteredIndexInfoOnHeap() throws Throwable
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(8);
+        testKeyCacheClustered();
+    }
+
+    private void testKeyCacheClustered() throws Throwable
     {
         String table = createTable("CREATE TABLE %s ("
                                    + commonColumnsDef
@@ -457,7 +550,7 @@
         if (index != null)
         {
             StorageService.instance.disableAutoCompaction(KEYSPACE_PER_TEST, table + '.' + index);
-            Keyspace.open(KEYSPACE_PER_TEST).getColumnFamilyStore(table).indexManager.getIndexByName(index).getBlockingFlushTask().call();
+            triggerBlockingFlush(Keyspace.open(KEYSPACE_PER_TEST).getColumnFamilyStore(table).indexManager.getIndexByName(index));
         }
 
         for (int i = 0; i < 100; i++)
@@ -482,7 +575,7 @@
             {
                 Keyspace.open(KEYSPACE_PER_TEST).getColumnFamilyStore(table).forceFlush().get();
                 if (index != null)
-                    Keyspace.open(KEYSPACE_PER_TEST).getColumnFamilyStore(table).indexManager.getIndexByName(index).getBlockingFlushTask().call();
+                    triggerBlockingFlush(Keyspace.open(KEYSPACE_PER_TEST).getColumnFamilyStore(table).indexManager.getIndexByName(index));
             }
         }
     }
@@ -514,4 +607,17 @@
         Assert.assertEquals(0L, metrics.requests.getCount());
         Assert.assertEquals(0L, metrics.size.getValue().longValue());
     }
+
+    private static void triggerBlockingFlush(Index index) throws Exception
+    {
+        assert index != null;
+        Callable<?> flushTask = index.getBlockingFlushTask();
+        if (flushTask != null)
+            flushTask.call();
+    }
+
+    private long recentBloomFilterFalsePositives()
+    {
+        return getCurrentColumnFamilyStore(KEYSPACE_PER_TEST).metric.recentBloomFilterFalsePositives.getValue();
+    }
 }
diff --git a/test/unit/org/apache/cassandra/cql3/LargeCompactValueTest.java b/test/unit/org/apache/cassandra/cql3/LargeCompactValueTest.java
index fe91650..93b16ce 100644
--- a/test/unit/org/apache/cassandra/cql3/LargeCompactValueTest.java
+++ b/test/unit/org/apache/cassandra/cql3/LargeCompactValueTest.java
@@ -23,8 +23,6 @@
 import org.junit.Before;
 import org.junit.Test;
 
-import org.apache.cassandra.io.util.RandomAccessReader;
-
 public class LargeCompactValueTest extends CQLTester
 {
     @Before
diff --git a/test/unit/org/apache/cassandra/cql3/ListsTest.java b/test/unit/org/apache/cassandra/cql3/ListsTest.java
index 9ca0010..07623a2 100644
--- a/test/unit/org/apache/cassandra/cql3/ListsTest.java
+++ b/test/unit/org/apache/cassandra/cql3/ListsTest.java
@@ -141,7 +141,7 @@
         ByteBuffer keyBuf = ByteBufferUtil.bytes("key");
         DecoratedKey key = Murmur3Partitioner.instance.decorateKey(keyBuf);
         UpdateParameters parameters = new UpdateParameters(metaData, null, null, System.currentTimeMillis(), 1000, Collections.emptyMap());
-        Clustering clustering = new Clustering(ByteBufferUtil.bytes(1));
+        Clustering clustering = Clustering.make(ByteBufferUtil.bytes(1));
         parameters.newRow(clustering);
         prepender.execute(key, parameters);
 
diff --git a/test/unit/org/apache/cassandra/cql3/NonNativeTimestampTest.java b/test/unit/org/apache/cassandra/cql3/NonNativeTimestampTest.java
index 37dc560..7ba9889 100644
--- a/test/unit/org/apache/cassandra/cql3/NonNativeTimestampTest.java
+++ b/test/unit/org/apache/cassandra/cql3/NonNativeTimestampTest.java
@@ -17,12 +17,6 @@
  */
 package org.apache.cassandra.cql3;
 
-import java.io.UnsupportedEncodingException;
-import java.nio.ByteBuffer;
-import java.nio.charset.CharacterCodingException;
-import java.util.Arrays;
-import java.util.Collections;
-
 import org.junit.Test;
 
 import static junit.framework.Assert.assertEquals;
diff --git a/test/unit/org/apache/cassandra/cql3/OutOfSpaceTest.java b/test/unit/org/apache/cassandra/cql3/OutOfSpaceTest.java
index 26e7fe2..24efc5e 100644
--- a/test/unit/org/apache/cassandra/cql3/OutOfSpaceTest.java
+++ b/test/unit/org/apache/cassandra/cql3/OutOfSpaceTest.java
@@ -126,7 +126,7 @@
 
         // Make sure commit log wasn't discarded.
         UUID cfid = currentTableMetadata().cfId;
-        for (CommitLogSegment segment : CommitLog.instance.allocator.getActiveSegments())
+        for (CommitLogSegment segment : CommitLog.instance.segmentManager.getActiveSegments())
             if (segment.getDirtyCFIDs().contains(cfid))
                 return;
         fail("Expected commit log to remain dirty for the affected table.");
diff --git a/test/unit/org/apache/cassandra/cql3/PagingQueryTest.java b/test/unit/org/apache/cassandra/cql3/PagingQueryTest.java
new file mode 100644
index 0000000..8f5f282
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/PagingQueryTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.util.Iterator;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.junit.Test;
+
+import com.datastax.driver.core.*;
+import com.datastax.driver.core.ResultSet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class PagingQueryTest extends CQLTester
+{
+    @Test
+    public void pagingOnRegularColumn() throws Throwable
+    {
+        createTable("CREATE TABLE %s (" +
+                    " k1 int," +
+                    " c1 int," +
+                    " c2 int," +
+                    " v1 text," +
+                    " v2 text," +
+                    " v3 text," +
+                    " v4 text," +
+                    "PRIMARY KEY (k1, c1, c2))");
+
+        for (int c1 = 0; c1 < 100; c1++)
+        {
+            for (int c2 = 0; c2 < 100; c2++)
+            {
+                execute("INSERT INTO %s (k1, c1, c2, v1, v2, v3, v4) VALUES (?, ?, ?, ?, ?, ?, ?)", 1, c1, c2,
+                        Integer.toString(c1), Integer.toString(c2), someText(), someText());
+            }
+
+            if (c1 % 30 == 0)
+                flush();
+        }
+
+        flush();
+
+        try (Session session = sessionNet())
+        {
+            SimpleStatement stmt = new SimpleStatement("SELECT c1, c2, v1, v2 FROM " + KEYSPACE + '.' + currentTable() + " WHERE k1 = 1");
+            stmt.setFetchSize(3);
+            ResultSet rs = session.execute(stmt);
+            Iterator<Row> iter = rs.iterator();
+            for (int c1 = 0; c1 < 100; c1++)
+            {
+                for (int c2 = 0; c2 < 100; c2++)
+                {
+                    assertTrue(iter.hasNext());
+                    Row row = iter.next();
+                    String msg = "On " + c1 + ',' + c2;
+                    assertEquals(msg, c1, row.getInt(0));
+                    assertEquals(msg, c2, row.getInt(1));
+                    assertEquals(msg, Integer.toString(c1), row.getString(2));
+                    assertEquals(msg, Integer.toString(c2), row.getString(3));
+                }
+            }
+            assertFalse(iter.hasNext());
+
+            for (int c1 = 0; c1 < 100; c1++)
+            {
+                stmt = new SimpleStatement("SELECT c1, c2, v1, v2 FROM " + KEYSPACE + '.' + currentTable() + " WHERE k1 = 1 AND c1 = ?", c1);
+                stmt.setFetchSize(3);
+                rs = session.execute(stmt);
+                iter = rs.iterator();
+                for (int c2 = 0; c2 < 100; c2++)
+                {
+                    assertTrue(iter.hasNext());
+                    Row row = iter.next();
+                    String msg = "Within " + c1 + " on " + c2;
+                    assertEquals(msg, c1, row.getInt(0));
+                    assertEquals(msg, c2, row.getInt(1));
+                    assertEquals(msg, Integer.toString(c1), row.getString(2));
+                    assertEquals(msg, Integer.toString(c2), row.getString(3));
+                }
+                assertFalse(iter.hasNext());
+            }
+        }
+    }
+
+    private static String someText()
+    {
+        char[] arr = new char[1024];
+        for (int i = 0; i < arr.length; i++)
+            arr[i] = (char)(32 + ThreadLocalRandom.current().nextInt(95));
+        return new String(arr);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/PagingTest.java b/test/unit/org/apache/cassandra/cql3/PagingTest.java
index ea1eb43..a054d01 100644
--- a/test/unit/org/apache/cassandra/cql3/PagingTest.java
+++ b/test/unit/org/apache/cassandra/cql3/PagingTest.java
@@ -59,7 +59,7 @@
     @BeforeClass
     public static void setup() throws Exception
     {
-        DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
+        System.setProperty("cassandra.config", "cassandra-murmur.yaml");
         EmbeddedCassandraService cassandra = new EmbeddedCassandraService();
         cassandra.start();
 
diff --git a/test/unit/org/apache/cassandra/cql3/PstmtPersistenceTest.java b/test/unit/org/apache/cassandra/cql3/PstmtPersistenceTest.java
new file mode 100644
index 0000000..a35ed67
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/PstmtPersistenceTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import junit.framework.Assert;
+import org.apache.cassandra.config.SchemaConstants;
+import org.apache.cassandra.cql3.statements.ParsedStatement;
+import org.apache.cassandra.db.SystemKeyspace;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.schema.SchemaKeyspaceTables;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.QueryState;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.MD5Digest;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+public class PstmtPersistenceTest extends CQLTester
+{
+    @Before
+    public void setUp()
+    {
+        QueryProcessor.clearPreparedStatements(false);
+    }
+ 
+    @Test
+    public void testCachedPreparedStatements() throws Throwable
+    {
+        // need this for pstmt execution/validation tests
+        requireNetwork();
+
+        assertEquals(0, numberOfStatementsOnDisk());
+
+        execute("CREATE KEYSPACE IF NOT EXISTS foo WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}");
+        execute("CREATE TABLE foo.bar (key text PRIMARY KEY, val int)");
+
+        ClientState clientState = ClientState.forExternalCalls(InetSocketAddress.createUnresolved("127.0.0.1", 1234));
+
+        createTable("CREATE TABLE %s (pk int PRIMARY KEY, val text)");
+
+        List<MD5Digest> stmtIds = new ArrayList<>();
+        String statement0 = "SELECT * FROM %s WHERE keyspace_name = ?";
+        String statement1 = "SELECT * FROM %s WHERE pk = ?";
+        String statement2 = "SELECT * FROM %s WHERE key = ?";
+        String statement3 = "SELECT * FROM %S WHERE key = ?";
+        stmtIds.add(prepareStatement(statement0, SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TABLES, clientState));
+        stmtIds.add(prepareStatement(statement1, clientState));
+        stmtIds.add(prepareStatement(statement2, "foo", "bar", clientState));
+        clientState.setKeyspace("foo");
+
+        stmtIds.add(prepareStatement(statement1, clientState));
+        stmtIds.add(prepareStatement(statement3, "foo", "bar", clientState));
+
+        assertEquals(5, stmtIds.size());
+        // statement1 will have two statements prepared because of `setKeyspace` usage
+        assertEquals(6, QueryProcessor.preparedStatementsCount());
+        assertEquals(6, numberOfStatementsOnDisk());
+
+        QueryHandler handler = ClientState.getCQLQueryHandler();
+        validatePstmts(stmtIds, handler);
+
+        // clear prepared statements cache
+        QueryProcessor.clearPreparedStatements(true);
+        assertEquals(0, QueryProcessor.preparedStatementsCount());
+        for (MD5Digest stmtId : stmtIds)
+            Assert.assertNull(handler.getPrepared(stmtId));
+
+        // load prepared statements and validate that these still execute fine
+        QueryProcessor.instance.preloadPreparedStatements();
+        validatePstmts(stmtIds, handler);
+
+
+        // validate that the prepared statements are in the system table
+        String queryAll = "SELECT * FROM " + SchemaConstants.SYSTEM_KEYSPACE_NAME + '.' + SystemKeyspace.PREPARED_STATEMENTS;
+        for (UntypedResultSet.Row row : QueryProcessor.executeOnceInternal(queryAll))
+        {
+            MD5Digest digest = MD5Digest.wrap(ByteBufferUtil.getArray(row.getBytes("prepared_id")));
+            ParsedStatement.Prepared prepared = QueryProcessor.instance.getPrepared(digest);
+            Assert.assertNotNull(prepared);
+        }
+
+        // add anther prepared statement and sync it to table
+        prepareStatement(statement2, "foo", "bar", clientState);
+
+        // statement1 will have two statements prepared because of `setKeyspace` usage
+        assertEquals(7, numberOfStatementsInMemory());
+        assertEquals(7, numberOfStatementsOnDisk());
+
+        // drop a keyspace (prepared statements are removed - syncPreparedStatements() remove should the rows, too)
+        execute("DROP KEYSPACE foo");
+        assertEquals(3, numberOfStatementsInMemory());
+        assertEquals(3, numberOfStatementsOnDisk());
+    }
+
+    private void validatePstmts(List<MD5Digest> stmtIds, QueryHandler handler)
+    {
+        QueryOptions optionsStr = QueryOptions.forInternalCalls(Collections.singletonList(UTF8Type.instance.fromString("foobar")));
+        QueryOptions optionsInt = QueryOptions.forInternalCalls(Collections.singletonList(Int32Type.instance.decompose(42)));
+        validatePstmt(handler, stmtIds.get(0), optionsStr);
+        validatePstmt(handler, stmtIds.get(1), optionsInt);
+        validatePstmt(handler, stmtIds.get(2), optionsStr);
+        validatePstmt(handler, stmtIds.get(3), optionsInt);
+        validatePstmt(handler, stmtIds.get(4), optionsStr);
+    }
+
+    private static void validatePstmt(QueryHandler handler, MD5Digest stmtId, QueryOptions options)
+    {
+        ParsedStatement.Prepared prepared = handler.getPrepared(stmtId);
+        assertNotNull(prepared);
+        handler.processPrepared(prepared.statement, QueryState.forInternalCalls(), options, Collections.emptyMap(), System.nanoTime());
+    }
+
+    @Test
+    public void testPstmtInvalidation() throws Throwable
+    {
+        ClientState clientState = ClientState.forInternalCalls();
+
+        createTable("CREATE TABLE %s (key int primary key, val int)");
+
+        for (int cnt = 1; cnt < 10000; cnt++)
+        {
+            prepareStatement("INSERT INTO %s (key, val) VALUES (?, ?) USING TIMESTAMP " + cnt, clientState);
+
+            if (numberOfEvictedStatements() > 0)
+            {
+                assertEquals("Number of statements in table and in cache don't match", numberOfStatementsInMemory(), numberOfStatementsOnDisk());
+
+                // prepare a more statements to trigger more evictions
+                for (int cnt2 = 1; cnt2 < 10; cnt2++)
+                    prepareStatement("INSERT INTO %s (key, val) VALUES (?, ?) USING TIMESTAMP " + cnt2, clientState);
+
+                // each new prepared statement should have caused an eviction
+                assertEquals("eviction count didn't increase by the expected number", numberOfEvictedStatements(), 10);
+                assertEquals("Number of statements in table and in cache don't match", numberOfStatementsInMemory(), numberOfStatementsOnDisk());
+
+                return;
+            }
+        }
+
+        fail("Prepared statement eviction does not work");
+    }
+
+    private long numberOfStatementsOnDisk() throws Throwable
+    {
+        UntypedResultSet.Row row = execute("SELECT COUNT(*) FROM " + SchemaConstants.SYSTEM_KEYSPACE_NAME + '.' + SystemKeyspace.PREPARED_STATEMENTS).one();
+        return row.getLong("count");
+    }
+
+    private long numberOfStatementsInMemory()
+    {
+        return QueryProcessor.preparedStatementsCount();
+    }
+
+    private long numberOfEvictedStatements()
+    {
+        return QueryProcessor.metrics.preparedStatementsEvicted.getCount();
+    }
+
+    private MD5Digest prepareStatement(String stmt, ClientState clientState)
+    {
+        return prepareStatement(stmt, keyspace(), currentTable(), clientState);
+    }
+
+    private MD5Digest prepareStatement(String stmt, String keyspace, String table, ClientState clientState)
+    {
+        return QueryProcessor.instance.prepare(String.format(stmt, keyspace + "." + table), clientState, false).statementId;
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ReservedKeywordsTest.java b/test/unit/org/apache/cassandra/cql3/ReservedKeywordsTest.java
index aaf9824..eb860eb 100644
--- a/test/unit/org/apache/cassandra/cql3/ReservedKeywordsTest.java
+++ b/test/unit/org/apache/cassandra/cql3/ReservedKeywordsTest.java
@@ -20,13 +20,13 @@
 
 import org.junit.Test;
 
-import junit.framework.Assert;
+import org.junit.Assert;
 import org.apache.cassandra.exceptions.SyntaxException;
 
 public class ReservedKeywordsTest
 {
     @Test
-    public void testReservedWordsForColumns() throws Exception
+    public void testReservedWordsForColumns()
     {
         for (String reservedWord : ReservedKeywords.reservedKeywords)
         {
diff --git a/test/unit/org/apache/cassandra/cql3/SerializationMirrorTest.java b/test/unit/org/apache/cassandra/cql3/SerializationMirrorTest.java
index 49f77a7..3268784 100644
--- a/test/unit/org/apache/cassandra/cql3/SerializationMirrorTest.java
+++ b/test/unit/org/apache/cassandra/cql3/SerializationMirrorTest.java
@@ -25,7 +25,6 @@
 import org.junit.Test;
 
 import junit.framework.Assert;
-import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class SerializationMirrorTest extends CQLTester
 {
diff --git a/test/unit/org/apache/cassandra/cql3/SimpleQueryTest.java b/test/unit/org/apache/cassandra/cql3/SimpleQueryTest.java
index f32bcc6..62fe5a1 100644
--- a/test/unit/org/apache/cassandra/cql3/SimpleQueryTest.java
+++ b/test/unit/org/apache/cassandra/cql3/SimpleQueryTest.java
@@ -388,24 +388,44 @@
 
         execute("INSERT INTO %s (k, t, v, s) values (?, ?, ?, ?)", "key1", 3, "foo3", "st3");
         execute("INSERT INTO %s (k, t, v) values (?, ?, ?)", "key1", 4, "foo4");
+        execute("INSERT INTO %s (k, t, v, s) values (?, ?, ?, ?)", "key1", 2, "foo2", "st2-repeat");
+
+        flush();
+
+        execute("INSERT INTO %s (k, t, v, s) values (?, ?, ?, ?)", "key1", 5, "foo5", "st5");
+        execute("INSERT INTO %s (k, t, v) values (?, ?, ?)", "key1", 6, "foo6");
+
 
         assertRows(execute("SELECT * FROM %s"),
-            row("key1",  1, "st3", "foo1"),
-            row("key1",  2, "st3", "foo2"),
-            row("key1",  3, "st3", "foo3"),
-            row("key1",  4, "st3", "foo4")
+            row("key1",  1, "st5", "foo1"),
+            row("key1",  2, "st5", "foo2"),
+            row("key1",  3, "st5", "foo3"),
+            row("key1",  4, "st5", "foo4"),
+            row("key1",  5, "st5", "foo5"),
+            row("key1",  6, "st5", "foo6")
         );
 
         assertRows(execute("SELECT s FROM %s WHERE k = ?", "key1"),
-            row("st3"),
-            row("st3"),
-            row("st3"),
-            row("st3")
+            row("st5"),
+            row("st5"),
+            row("st5"),
+            row("st5"),
+            row("st5"),
+            row("st5")
         );
 
         assertRows(execute("SELECT DISTINCT s FROM %s WHERE k = ?", "key1"),
-            row("st3")
+            row("st5")
         );
+
+        assertEmpty(execute("SELECT * FROM %s WHERE k = ? AND t > ? AND t < ?", "key1", 7, 5));
+        assertEmpty(execute("SELECT * FROM %s WHERE k = ? AND t > ? AND t < ? ORDER BY t DESC", "key1", 7, 5));
+
+        assertRows(execute("SELECT * FROM %s WHERE k = ? AND t = ?", "key1", 2),
+            row("key1", 2, "st5", "foo2"));
+
+        assertRows(execute("SELECT * FROM %s WHERE k = ? AND t = ? ORDER BY t DESC", "key1", 2),
+            row("key1", 2, "st5", "foo2"));
     }
 
     @Test
@@ -527,6 +547,45 @@
         );
     }
 
+    /** Test for Cassandra issue 10958 **/
+    @Test
+    public void restrictionOnRegularColumnWithStaticColumnPresentTest() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, id2 int, age int static, extra int, PRIMARY KEY(id, id2))");
+
+        execute("INSERT INTO %s (id, id2, age, extra) VALUES (?, ?, ?, ?)", 1, 1, 1, 1);
+        execute("INSERT INTO %s (id, id2, age, extra) VALUES (?, ?, ?, ?)", 2, 2, 2, 2);
+        execute("UPDATE %s SET age=? WHERE id=?", 3, 3);
+
+        assertRows(execute("SELECT * FROM %s"),
+            row(1, 1, 1, 1),
+            row(2, 2, 2, 2),
+            row(3, null, 3, null)
+        );
+
+        assertRows(execute("SELECT * FROM %s WHERE extra > 1 ALLOW FILTERING"),
+            row(2, 2, 2, 2)
+        );
+    }
+
+    @Test
+    public void testRowFilteringOnStaticColumn() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, name text, age int static, PRIMARY KEY (id, name))");
+        for (int i = 0; i < 5; i++)
+        {
+            execute("INSERT INTO %s (id, name, age) VALUES (?, ?, ?)", i, "NameDoesNotMatter", i);
+        }
+
+        assertInvalid("SELECT id, age FROM %s WHERE age < 1");
+        assertRows(execute("SELECT id, age FROM %s WHERE age < 1 ALLOW FILTERING"),
+                   row(0, 0));
+        assertRows(execute("SELECT id, age FROM %s WHERE age > 0 AND age < 3 ALLOW FILTERING"),
+                   row(1, 1), row(2, 2));
+        assertRows(execute("SELECT id, age FROM %s WHERE age > 3 ALLOW FILTERING"),
+                   row(4, 4));
+    }
+
     @Test
     public void testSStableTimestampOrdering() throws Throwable
     {
diff --git a/test/unit/org/apache/cassandra/cql3/ThriftCompatibilityTest.java b/test/unit/org/apache/cassandra/cql3/ThriftCompatibilityTest.java
index ff2af56..521c0a0 100644
--- a/test/unit/org/apache/cassandra/cql3/ThriftCompatibilityTest.java
+++ b/test/unit/org/apache/cassandra/cql3/ThriftCompatibilityTest.java
@@ -22,10 +22,8 @@
 
 import org.junit.Test;
 
-import com.sun.org.apache.xerces.internal.impl.xs.models.CMNodeFactory;
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.marshal.BytesType;
 import org.apache.cassandra.db.marshal.Int32Type;
diff --git a/test/unit/org/apache/cassandra/cql3/TombstonesWithIndexedSSTableTest.java b/test/unit/org/apache/cassandra/cql3/TombstonesWithIndexedSSTableTest.java
index 3042acd..787c309 100644
--- a/test/unit/org/apache/cassandra/cql3/TombstonesWithIndexedSSTableTest.java
+++ b/test/unit/org/apache/cassandra/cql3/TombstonesWithIndexedSSTableTest.java
@@ -24,8 +24,8 @@
 import org.apache.cassandra.Util;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.marshal.Int32Type;
-import org.apache.cassandra.io.sstable.IndexHelper;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.util.FileDataInput;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class TombstonesWithIndexedSSTableTest extends CQLTester
@@ -76,13 +76,17 @@
             {
                 // The line below failed with key caching off (CASSANDRA-11158)
                 @SuppressWarnings("unchecked")
-                RowIndexEntry<IndexHelper.IndexInfo> indexEntry = sstable.getPosition(dk, SSTableReader.Operator.EQ);
+                RowIndexEntry indexEntry = sstable.getPosition(dk, SSTableReader.Operator.EQ);
                 if (indexEntry != null && indexEntry.isIndexed())
                 {
-                    ClusteringPrefix firstName = indexEntry.columnsIndex().get(1).firstName;
-                    if (firstName.kind().isBoundary())
-                        break deletionLoop;
-                    indexedRow = Int32Type.instance.compose(firstName.get(0));
+                    try (FileDataInput reader = sstable.openIndexReader())
+                    {
+                        RowIndexEntry.IndexInfoRetriever infoRetriever = indexEntry.openWithIndex(sstable.getIndexFile());
+                        ClusteringPrefix firstName = infoRetriever.columnsIndex(1).firstName;
+                        if (firstName.kind().isBoundary())
+                            break deletionLoop;
+                        indexedRow = Int32Type.instance.compose(firstName.get(0));
+                    }
                 }
             }
             assert indexedRow >= 0;
@@ -104,7 +108,6 @@
         assertRowCount(execute("SELECT DISTINCT s FROM %s WHERE k = ? ORDER BY t DESC", 0), 1);
     }
 
-    // Creates a random string
     public static String makeRandomString(int length)
     {
         Random random = new Random();
diff --git a/test/unit/org/apache/cassandra/cql3/TraceCqlTest.java b/test/unit/org/apache/cassandra/cql3/TraceCqlTest.java
new file mode 100644
index 0000000..f1af922
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/TraceCqlTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.datastax.driver.core.CodecRegistry;
+import com.datastax.driver.core.DataType;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ProtocolVersion;
+import com.datastax.driver.core.QueryTrace;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.TupleType;
+import com.datastax.driver.core.TupleValue;
+import org.apache.cassandra.tracing.TraceStateImpl;
+
+import static org.junit.Assert.assertEquals;
+
+public class TraceCqlTest extends CQLTester
+{
+    static int DEFAULT_WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS;
+
+    @BeforeClass
+    public static void setUp()
+    {
+        // make sure we wait for trace events to complete, see CASSANDRA-12754
+        DEFAULT_WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS = TraceStateImpl.WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS;
+        TraceStateImpl.WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS = 5;
+    }
+
+    @AfterClass
+    public static void tearDown()
+    {
+        TraceStateImpl.WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS = DEFAULT_WAIT_FOR_PENDING_EVENTS_TIMEOUT_SECS;
+    }
+
+    @Test
+    public void testCqlStatementTracing() throws Throwable
+    {
+        requireNetwork();
+
+        createTable("CREATE TABLE %s (id int primary key, v1 text, v2 text)");
+        execute("INSERT INTO %s (id, v1, v2) VALUES (?, ?, ?)", 1, "Apache", "Cassandra");
+        execute("INSERT INTO %s (id, v1, v2) VALUES (?, ?, ?)", 2, "trace", "test");
+
+        try (Session session = sessionNet())
+        {
+            String cql = "SELECT id, v1, v2 FROM " + KEYSPACE + '.' + currentTable() + " WHERE id = ?";
+            PreparedStatement pstmt = session.prepare(cql)
+                                             .enableTracing();
+            QueryTrace trace = session.execute(pstmt.bind(1)).getExecutionInfo().getQueryTrace();
+            assertEquals(cql, trace.getParameters().get("query"));
+
+            assertEquals("1", trace.getParameters().get("bound_var_0_id"));
+
+            String cql2 = "SELECT id, v1, v2 FROM " + KEYSPACE + '.' + currentTable() + " WHERE id IN (?, ?, ?)";
+            pstmt = session.prepare(cql2).enableTracing();
+            trace = session.execute(pstmt.bind(19, 15, 16)).getExecutionInfo().getQueryTrace();
+            assertEquals(cql2, trace.getParameters().get("query"));
+            assertEquals("19", trace.getParameters().get("bound_var_0_id"));
+            assertEquals("15", trace.getParameters().get("bound_var_1_id"));
+            assertEquals("16", trace.getParameters().get("bound_var_2_id"));
+
+            //some more complex tests for tables with map and tuple data types and long bound values
+            createTable("CREATE TABLE %s (id int primary key, v1 text, v2 tuple<int, text, float>, v3 map<int, text>)");
+            execute("INSERT INTO %s (id, v1, v2, v3) values (?, ?, ?, ?)", 12, "mahdix", tuple(3, "bar", 2.1f),
+                    map(1290, "birthday", 39, "anniversary"));
+            execute("INSERT INTO %s (id, v1, v2, v3) values (?, ?, ?, ?)", 274, "CassandraRocks", tuple(9, "foo", 3.14f),
+                    map(9181, "statement", 716, "public speech"));
+
+            cql = "SELECT id, v1, v2, v3 FROM " + KEYSPACE + '.' + currentTable() + " WHERE v2 = ? ALLOW FILTERING";
+            pstmt = session.prepare(cql)
+                           .enableTracing();
+            TupleType tt = TupleType.of(ProtocolVersion.NEWEST_SUPPORTED, CodecRegistry.DEFAULT_INSTANCE, DataType.cint(),
+                                        DataType.text(), DataType.cfloat());
+            TupleValue value = tt.newValue();
+            value.setInt(0, 3);
+            value.setString(1, "bar");
+            value.setFloat(2, 2.1f);
+
+            trace = session.execute(pstmt.bind(value)).getExecutionInfo().getQueryTrace();
+            assertEquals(cql, trace.getParameters().get("query"));
+            assertEquals("(3, 'bar', 2.1)", trace.getParameters().get("bound_var_0_v2"));
+
+            cql2 = "SELECT id, v1, v2, v3 FROM " + KEYSPACE + '.' + currentTable() + " WHERE v3 CONTAINS KEY ? ALLOW FILTERING";
+            pstmt = session.prepare(cql2).enableTracing();
+            trace = session.execute(pstmt.bind(9181)).getExecutionInfo().getQueryTrace();
+
+            assertEquals(cql2, trace.getParameters().get("query"));
+            assertEquals("9181", trace.getParameters().get("bound_var_0_key(v3)"));
+
+            String boundValue = "Indulgence announcing uncommonly met she continuing two unpleasing terminated. Now " +
+                                "busy say down the shed eyes roof paid her. Of shameless collected suspicion existence " +
+                                "in. Share walls stuff think but the arise guest. Course suffer to do he sussex it " +
+                                "window advice. Yet matter enable misery end extent common men should. Her indulgence " +
+                                "but assistance favourable cultivated everything collecting." +
+                                "On projection apartments unsatiable so if he entreaties appearance. Rose you wife " +
+                                "how set lady half wish. Hard sing an in true felt. Welcomed stronger if steepest " +
+                                "ecstatic an suitable finished of oh. Entered at excited at forming between so " +
+                                "produce. Chicken unknown besides attacks gay compact out you. Continuing no " +
+                                "simplicity no favourable on reasonably melancholy estimating. Own hence views two " +
+                                "ask right whole ten seems. What near kept met call old west dine. Our announcing " +
+                                "sufficient why pianoforte. Full age foo set feel her told. Tastes giving in passed" +
+                                "direct me valley as supply. End great stood boy noisy often way taken short. Rent the " +
+                                "size our more door. Years no place abode in \uFEFFno child my. Man pianoforte too " +
+                                "solicitude friendship devonshire ten ask. Course sooner its silent but formal she " +
+                                "led. Extensive he assurance extremity at breakfast. Dear sure ye sold fine sell on. " +
+                                "Projection at up connection literature insensible motionless projecting." +
+                                "Nor hence hoped her after other known defer his. For county now sister engage had " +
+                                "season better had waited. Occasional mrs interested far expression acceptance. Day " +
+                                "either mrs talent pulled men rather regret admire but. Life ye sake it shed. Five " +
+                                "lady he cold in meet up. Service get met adapted matters offence for. Principles man " +
+                                "any insipidity age you simplicity understood. Do offering pleasure no ecstatic " +
+                                "whatever on mr directly. ";
+
+            String cql3 = "SELECT id, v1, v2, v3 FROM " + KEYSPACE + '.' + currentTable() + " WHERE v3 CONTAINS ? ALLOW FILTERING";
+            pstmt = session.prepare(cql3).enableTracing();
+            trace = session.execute(pstmt.bind(boundValue)).getExecutionInfo().getQueryTrace();
+
+            assertEquals(cql3, trace.getParameters().get("query"));
+
+            //when tracing is done, this boundValue will be surrounded by single quote, and first 1000 characters
+            //will be filtered. Here we take into account single quotes by adding them to the expected output
+            assertEquals("'" + boundValue.substring(0, 999) + "...'", trace.getParameters().get("bound_var_0_value(v3)"));
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ViewComplexTest.java b/test/unit/org/apache/cassandra/cql3/ViewComplexTest.java
index b01b86c..abcea3d 100644
--- a/test/unit/org/apache/cassandra/cql3/ViewComplexTest.java
+++ b/test/unit/org/apache/cassandra/cql3/ViewComplexTest.java
@@ -39,6 +39,7 @@
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.compaction.CompactionManager;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.FBUtilities;
 import org.junit.After;
 import org.junit.Before;
@@ -50,7 +51,7 @@
 
 public class ViewComplexTest extends CQLTester
 {
-    int protocolVersion = 4;
+    ProtocolVersion protocolVersion = ProtocolVersion.V4;
     private final List<String> views = new ArrayList<>();
 
     @BeforeClass
diff --git a/test/unit/org/apache/cassandra/cql3/ViewFilteringTest.java b/test/unit/org/apache/cassandra/cql3/ViewFilteringTest.java
index fe618b6..6803230 100644
--- a/test/unit/org/apache/cassandra/cql3/ViewFilteringTest.java
+++ b/test/unit/org/apache/cassandra/cql3/ViewFilteringTest.java
@@ -21,25 +21,44 @@
 import java.util.*;
 
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Test;
 
 import com.datastax.driver.core.exceptions.InvalidQueryException;
 import junit.framework.Assert;
 
+import org.apache.cassandra.concurrent.SEPExecutor;
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.concurrent.StageManager;
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.SystemKeyspace;
+import org.apache.cassandra.transport.ProtocolVersion;
+import org.apache.cassandra.utils.FBUtilities;
 
 public class ViewFilteringTest extends CQLTester
 {
-    int protocolVersion = 4;
+    ProtocolVersion protocolVersion = ProtocolVersion.V4;
     private final List<String> views = new ArrayList<>();
 
     @BeforeClass
     public static void startup()
     {
         requireNetwork();
+        System.setProperty("cassandra.mv.allow_filtering_nonkey_columns_unsafe", "true");
     }
+
+    @AfterClass
+    public static void TearDown()
+    {
+        System.setProperty("cassandra.mv.allow_filtering_nonkey_columns_unsafe", "false");
+    }
+
     @Before
     public void begin()
     {
@@ -61,12 +80,340 @@
         views.add(name);
     }
 
+    private void updateView(String query, Object... params) throws Throwable
+    {
+        executeNet(protocolVersion, query, params);
+        while (!(((SEPExecutor) StageManager.getStage(Stage.VIEW_MUTATION)).getPendingTasks() == 0
+            && ((SEPExecutor) StageManager.getStage(Stage.VIEW_MUTATION)).getActiveCount() == 0))
+        {
+            Thread.sleep(1);
+        }
+    }
+
     private void dropView(String name) throws Throwable
     {
         executeNet(protocolVersion, "DROP MATERIALIZED VIEW " + name);
         views.remove(name);
     }
 
+    private static void waitForView(String keyspace, String view) throws InterruptedException
+    {
+        while (!SystemKeyspace.isViewBuilt(keyspace, view))
+            Thread.sleep(10);
+    }
+
+    // TODO will revise the non-pk filter condition in MV, see CASSANDRA-13826
+    @Ignore
+    @Test
+    public void testViewFilteringWithFlush() throws Throwable
+    {
+        testViewFiltering(true);
+    }
+
+    // TODO will revise the non-pk filter condition in MV, see CASSANDRA-13826
+    @Ignore
+    @Test
+    public void testViewFilteringWithoutFlush() throws Throwable
+    {
+        testViewFiltering(false);
+    }
+
+    public void testViewFiltering(boolean flush) throws Throwable
+    {
+        // CASSANDRA-13547: able to shadow entire view row if base column used in filter condition is modified
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY (a))");
+
+        execute("USE " + keyspace());
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        createView("mv_test1",
+                   "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL and c = 1  PRIMARY KEY (a, b)");
+        createView("mv_test2",
+                   "CREATE MATERIALIZED VIEW %s AS SELECT c, d FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL and c = 1 and d = 1 PRIMARY KEY (a, b)");
+        createView("mv_test3",
+                   "CREATE MATERIALIZED VIEW %s AS SELECT a, b, c, d FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL PRIMARY KEY (a, b)");
+        createView("mv_test4",
+                   "CREATE MATERIALIZED VIEW %s AS SELECT c FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL and c = 1 PRIMARY KEY (a, b)");
+        createView("mv_test5",
+                   "CREATE MATERIALIZED VIEW %s AS SELECT c FROM %%s WHERE a IS NOT NULL and d = 1 PRIMARY KEY (a, d)");
+        createView("mv_test6",
+                   "CREATE MATERIALIZED VIEW %s AS SELECT c FROM %%s WHERE a = 1 and d IS NOT NULL PRIMARY KEY (a, d)");
+
+        waitForView(keyspace(), "mv_test1");
+        waitForView(keyspace(), "mv_test2");
+        waitForView(keyspace(), "mv_test3");
+        waitForView(keyspace(), "mv_test4");
+        waitForView(keyspace(), "mv_test5");
+        waitForView(keyspace(), "mv_test6");
+
+        Keyspace ks = Keyspace.open(keyspace());
+        ks.getColumnFamilyStore("mv_test1").disableAutoCompaction();
+        ks.getColumnFamilyStore("mv_test2").disableAutoCompaction();
+        ks.getColumnFamilyStore("mv_test3").disableAutoCompaction();
+        ks.getColumnFamilyStore("mv_test4").disableAutoCompaction();
+        ks.getColumnFamilyStore("mv_test5").disableAutoCompaction();
+        ks.getColumnFamilyStore("mv_test6").disableAutoCompaction();
+
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?) using timestamp 0", 1, 1, 1, 1);
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        // views should be updated.
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"), row(1, 1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test2"), row(1, 1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test5"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test6"), row(1, 1, 1));
+
+        updateView("UPDATE %s using timestamp 1 set c = ? WHERE a=?", 0, 1);
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowCount(execute("SELECT * FROM mv_test1"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test2"), 0);
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1, 0, 1));
+        assertRowCount(execute("SELECT * FROM mv_test4"), 0);
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test5"), row(1, 1, 0));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test6"), row(1, 1, 0));
+
+        updateView("UPDATE %s using timestamp 2 set c = ? WHERE a=?", 1, 1);
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        // row should be back in views.
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"), row(1, 1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test2"), row(1, 1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test5"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test6"), row(1, 1, 1));
+
+        updateView("UPDATE %s using timestamp 3 set d = ? WHERE a=?", 0, 1);
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"), row(1, 1, 1, 0));
+        assertRowCount(execute("SELECT * FROM mv_test2"), 0);
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1, 1, 0));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"), row(1, 1, 1));
+        assertRowCount(execute("SELECT * FROM mv_test5"), 0);
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test6"), row(1, 0, 1));
+
+        updateView("UPDATE %s using timestamp 4 set c = ? WHERE a=?", 0, 1);
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowCount(execute("SELECT * FROM mv_test1"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test2"), 0);
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1, 0, 0));
+        assertRowCount(execute("SELECT * FROM mv_test4"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test5"), 0);
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test6"), row(1, 0, 0));
+
+        updateView("UPDATE %s using timestamp 5 set d = ? WHERE a=?", 1, 1);
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        // should not update as c=0
+        assertRowCount(execute("SELECT * FROM mv_test1"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test2"), 0);
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1, 0, 1));
+        assertRowCount(execute("SELECT * FROM mv_test4"), 0);
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test5"), row(1, 1, 0));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test6"), row(1, 1, 0));
+
+        updateView("UPDATE %s using timestamp 6 set c = ? WHERE a=?", 1, 1);
+
+        // row should be back in views.
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"), row(1, 1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test2"), row(1, 1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test5"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test6"), row(1, 1, 1));
+
+        updateView("UPDATE %s using timestamp 7 set b = ? WHERE a=?", 2, 1);
+        if (flush)
+        {
+            FBUtilities.waitOnFutures(ks.flush());
+            for (String view : views)
+                ks.getColumnFamilyStore(view).forceMajorCompaction();
+        }
+        // row should be back in views.
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"), row(1, 2, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test2"), row(1, 2, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 2, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"), row(1, 2, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test5"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test6"), row(1, 1, 1));
+
+        updateView("DELETE b, c FROM %s using timestamp 6 WHERE a=?", 1);
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowsIgnoringOrder(execute("SELECT * FROM %s"), row(1, 2, null, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test2"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 2, null, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test5"), row(1, 1, null));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test6"), row(1, 1, null));
+
+        updateView("DELETE FROM %s using timestamp 8 where a=?", 1);
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowCount(execute("SELECT * FROM mv_test1"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test2"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test3"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test4"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test5"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test6"), 0);
+
+        updateView("UPDATE %s using timestamp 9 set b = ?,c = ? where a=?", 1, 1, 1); // upsert
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"), row(1, 1, 1, null));
+        assertRows(execute("SELECT * FROM mv_test2"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1, 1, null));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"), row(1, 1, 1));
+        assertRows(execute("SELECT * FROM mv_test5"));
+        assertRows(execute("SELECT * FROM mv_test6"));
+
+        updateView("DELETE FROM %s using timestamp 10 where a=?", 1);
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowCount(execute("SELECT * FROM mv_test1"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test2"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test3"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test4"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test5"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test6"), 0);
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?) using timestamp 11", 1, 1, 1, 1);
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        // row should be back in views.
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"), row(1, 1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test2"), row(1, 1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test5"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test6"), row(1, 1, 1));
+
+        updateView("DELETE FROM %s using timestamp 12 where a=?", 1);
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowCount(execute("SELECT * FROM mv_test1"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test2"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test3"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test4"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test5"), 0);
+        assertRowCount(execute("SELECT * FROM mv_test6"), 0);
+
+        dropView("mv_test1");
+        dropView("mv_test2");
+        dropView("mv_test3");
+        dropView("mv_test4");
+        dropView("mv_test5");
+        dropView("mv_test6");
+        dropTable("DROP TABLE %s");
+    }
+
+    // TODO will revise the non-pk filter condition in MV, see CASSANDRA-13826
+    @Ignore
+    @Test
+    public void testMVFilteringWithComplexColumn() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, l list<int>, s set<int>, m map<int,int>, PRIMARY KEY (a, b))");
+
+        execute("USE " + keyspace());
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        createView("mv_test1",
+                   "CREATE MATERIALIZED VIEW %s AS SELECT a,b,c FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL AND c IS NOT NULL "
+                   + "and l contains (1) AND s contains (1) AND m contains key (1) PRIMARY KEY (a, b, c)");
+        createView("mv_test2",
+                   "CREATE MATERIALIZED VIEW %s AS SELECT a,b FROM %%s WHERE a IS NOT NULL and b IS NOT NULL AND l contains (1) PRIMARY KEY (a, b)");
+        createView("mv_test3",
+                   "CREATE MATERIALIZED VIEW %s AS SELECT a,b FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL AND s contains (1) PRIMARY KEY (a, b)");
+        createView("mv_test4",
+                   "CREATE MATERIALIZED VIEW %s AS SELECT a,b FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL AND m contains key (1) PRIMARY KEY (a, b)");
+
+        waitForView(keyspace(), "mv_test1");
+        waitForView(keyspace(), "mv_test2");
+        waitForView(keyspace(), "mv_test3");
+        waitForView(keyspace(), "mv_test4");
+
+        // not able to drop base column filtered in view
+        assertInvalidMessage("Cannot drop column l, depended on by materialized views", "ALTER TABLE %s DROP l");
+        assertInvalidMessage("Cannot drop column s, depended on by materialized views", "ALTER TABLE %S DROP s");
+        assertInvalidMessage("Cannot drop column m, depended on by materialized views", "ALTER TABLE %s DROP m");
+
+        Keyspace ks = Keyspace.open(keyspace());
+        ks.getColumnFamilyStore("mv_test1").disableAutoCompaction();
+        ks.getColumnFamilyStore("mv_test2").disableAutoCompaction();
+        ks.getColumnFamilyStore("mv_test3").disableAutoCompaction();
+        ks.getColumnFamilyStore("mv_test4").disableAutoCompaction();
+
+        execute("INSERT INTO %s (a, b, c, l, s, m) VALUES (?, ?, ?, ?, ?, ?) ",
+                1,
+                1,
+                1,
+                list(1, 1, 2),
+                set(1, 2),
+                map(1, 1, 2, 2));
+        FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test2"), row(1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"), row(1, 1));
+
+
+        execute("UPDATE %s SET l=l-[1] WHERE a = 1 AND b = 1" );
+        FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test2"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"), row(1, 1));
+
+        execute("UPDATE %s SET s=s-{2}, m=m-{2} WHERE a = 1 AND b = 1");
+        FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowsIgnoringOrder(execute("SELECT a,b,c FROM %s"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test2"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"), row(1, 1));
+
+        execute("UPDATE %s SET  m=m-{1} WHERE a = 1 AND b = 1");
+        FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowsIgnoringOrder(execute("SELECT a,b,c FROM %s"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test2"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"));
+
+        // filter conditions result not changed
+        execute("UPDATE %s SET  l=l+[2], s=s-{0}, m=m+{3:3} WHERE a = 1 AND b = 1");
+        FBUtilities.waitOnFutures(ks.flush());
+
+        assertRowsIgnoringOrder(execute("SELECT a,b,c FROM %s"), row(1, 1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test1"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test2"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test3"), row(1, 1));
+        assertRowsIgnoringOrder(execute("SELECT * FROM mv_test4"));
+    }
+
     @Test
     public void testMVCreationSelectRestrictions() throws Throwable
     {
@@ -96,19 +443,19 @@
             catch (InvalidQueryException exc) {}
         }
 
-            List<String> goodStatements = Arrays.asList(
-            "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND c IS NOT NULL AND d is NOT NULL PRIMARY KEY ((a, b), c, d)",
-            "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL AND c = 1 AND d IS NOT NULL PRIMARY KEY ((a, b), c, d)",
-            "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL AND c = 1 AND d = 1 PRIMARY KEY ((a, b), c, d)",
-            "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND c = 1 AND d = 1 PRIMARY KEY ((a, b), c, d)",
-            "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND c > 1 AND d IS NOT NULL PRIMARY KEY ((a, b), c, d)",
-            "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND c = 1 AND d IN (1, 2, 3) PRIMARY KEY ((a, b), c, d)",
-            "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND (c, d) = (1, 1) PRIMARY KEY ((a, b), c, d)",
-            "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND (c, d) > (1, 1) PRIMARY KEY ((a, b), c, d)",
-            "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND (c, d) IN ((1, 1), (2, 2)) PRIMARY KEY ((a, b), c, d)",
-            "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = (int) 1 AND b = 1 AND c = 1 AND d = 1 PRIMARY KEY ((a, b), c, d)",
-            "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = blobAsInt(intAsBlob(1)) AND b = 1 AND c = 1 AND d = 1 PRIMARY KEY ((a, b), c, d)"
-            );
+        List<String> goodStatements = Arrays.asList(
+        "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND c IS NOT NULL AND d is NOT NULL PRIMARY KEY ((a, b), c, d)",
+        "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL AND c = 1 AND d IS NOT NULL PRIMARY KEY ((a, b), c, d)",
+        "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL AND c = 1 AND d = 1 PRIMARY KEY ((a, b), c, d)",
+        "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND c = 1 AND d = 1 PRIMARY KEY ((a, b), c, d)",
+        "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND c > 1 AND d IS NOT NULL PRIMARY KEY ((a, b), c, d)",
+        "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND c = 1 AND d IN (1, 2, 3) PRIMARY KEY ((a, b), c, d)",
+        "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND (c, d) = (1, 1) PRIMARY KEY ((a, b), c, d)",
+        "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND (c, d) > (1, 1) PRIMARY KEY ((a, b), c, d)",
+        "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b = 1 AND (c, d) IN ((1, 1), (2, 2)) PRIMARY KEY ((a, b), c, d)",
+        "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = (int) 1 AND b = 1 AND c = 1 AND d = 1 PRIMARY KEY ((a, b), c, d)",
+        "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = blobAsInt(intAsBlob(1)) AND b = 1 AND c = 1 AND d = 1 PRIMARY KEY ((a, b), c, d)"
+        );
 
         for (int i = 0; i < goodStatements.size(); i++)
         {
@@ -130,13 +477,6 @@
                 throw new RuntimeException("MV alter failed: " + goodStatements.get(i), e);
             }
         }
-
-        try
-        {
-            createView("mv_foo", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b IS NOT NULL AND c IS NOT NULL AND d is NOT NULL PRIMARY KEY ((a, b), c, d)");
-            Assert.fail("Partial partition key restriction should not be allowed");
-        }
-        catch (InvalidQueryException exc) {}
     }
 
     @Test
@@ -248,6 +588,201 @@
     }
 
     @Test
+    public void testPartitionKeyFilteringUnrestrictedPart() throws Throwable
+    {
+        List<String> mvPrimaryKeys = Arrays.asList("((a, b), c)", "((b, a), c)", "(a, b, c)", "(c, b, a)", "((c, a), b)");
+        for (int i = 0; i < mvPrimaryKeys.size(); i++)
+        {
+            createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY ((a, b), c))");
+
+            execute("USE " + keyspace());
+            executeNet(protocolVersion, "USE " + keyspace());
+
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, 0, 0, 0);
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, 1, 0, 0);
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 1, 0, 0, 0);
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 1, 0, 1, 0);
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 1, 1, 0, 0);
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 1, 1, 1, 0);
+
+            logger.info("Testing MV primary key: {}", mvPrimaryKeys.get(i));
+
+            // only accept rows where a = 1
+            String viewName= "mv_test" + i;
+            createView(viewName, "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a = 1 AND b IS NOT NULL AND c IS NOT NULL PRIMARY KEY " + mvPrimaryKeys.get(i));
+
+            waitForView(keyspace(), viewName);
+
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 0, 0, 0),
+                                    row(1, 0, 1, 0),
+                                    row(1, 1, 0, 0),
+                                    row(1, 1, 1, 0)
+            );
+
+            // insert new rows that do not match the filter
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 2, 0, 0, 0);
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 2, 1, 0, 0);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 0, 0, 0),
+                                    row(1, 0, 1, 0),
+                                    row(1, 1, 0, 0),
+                                    row(1, 1, 1, 0)
+            );
+
+            // insert new row that does match the filter
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 1, 1, 2, 0);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 0, 0, 0),
+                                    row(1, 0, 1, 0),
+                                    row(1, 1, 0, 0),
+                                    row(1, 1, 1, 0),
+                                    row(1, 1, 2, 0)
+            );
+
+            // update rows that don't match the filter
+            execute("UPDATE %s SET d = ? WHERE a = ? AND b = ? AND c = ?", 1, 0, 0, 0);
+            execute("UPDATE %s SET d = ? WHERE a = ? AND b = ? AND c = ?", 1, 0, 1, 0);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 0, 0, 0),
+                                    row(1, 0, 1, 0),
+                                    row(1, 1, 0, 0),
+                                    row(1, 1, 1, 0),
+                                    row(1, 1, 2, 0)
+            );
+
+            // update a row that does match the filter
+            execute("UPDATE %s SET d = ? WHERE a = ? AND b = ? AND c = ?", 1, 1, 1, 0);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 0, 0, 0),
+                                    row(1, 0, 1, 0),
+                                    row(1, 1, 0, 1),
+                                    row(1, 1, 1, 0),
+                                    row(1, 1, 2, 0)
+            );
+
+            // delete rows that don't match the filter
+            execute("DELETE FROM %s WHERE a = ? AND b = ? AND c = ?", 0, 0, 0);
+            execute("DELETE FROM %s WHERE a = ? AND b = ? AND c = ?", 0, 1, 0);
+            execute("DELETE FROM %s WHERE a = ? AND b = ?", 0, 0);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 0, 0, 0),
+                                    row(1, 0, 1, 0),
+                                    row(1, 1, 0, 1),
+                                    row(1, 1, 1, 0),
+                                    row(1, 1, 2, 0)
+            );
+
+            // delete a row that does match the filter
+            execute("DELETE FROM %s WHERE a = ? AND b = ? AND c = ?", 1, 1, 0);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 0, 0, 0),
+                                    row(1, 0, 1, 0),
+                                    row(1, 1, 1, 0),
+                                    row(1, 1, 2, 0)
+            );
+
+            // delete a partition that matches the filter
+            execute("DELETE FROM %s WHERE a = ? AND b = ?", 1, 0);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 1, 1, 0),
+                                    row(1, 1, 2, 0));
+            execute("DELETE FROM %s WHERE a = ? AND b = ?", 1, 1);
+            assertEmpty(execute("SELECT * FROM mv_test" + i));
+        }
+    }
+
+    @Test
+    public void testPartitionKeyFilteringWithSlice() throws Throwable
+    {
+        List<String> mvPrimaryKeys = Arrays.asList("((a, b), c)", "((b, a), c)", "(a, b, c)", "(c, b, a)", "((c, a), b)");
+        for (int i = 0; i < mvPrimaryKeys.size(); i++)
+        {
+            createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY ((a, b), c))");
+
+            execute("USE " + keyspace());
+            executeNet(protocolVersion, "USE " + keyspace());
+
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, 0,  1, 1);
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, 10, 1, 2);
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 1, 0,  2, 1);
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 1, 10, 2, 2);
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 2, 1,  3, 1);
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 2, 10, 3, 2);
+
+            logger.info("Testing MV primary key: {}", mvPrimaryKeys.get(i));
+
+            // only accept rows where a = 1
+            String viewName= "mv_test" + i;
+            createView(viewName, "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a > 0 AND b > 5 AND c IS NOT NULL PRIMARY KEY " + mvPrimaryKeys.get(i));
+
+            waitForView(keyspace(), viewName);
+
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 10, 2, 2),
+                                    row(2, 10, 3, 2)
+            );
+
+            // insert new rows that do not match the filter
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 2, 0, 0, 0);
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 2, 1, 0, 0);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 10, 2, 2),
+                                    row(2, 10, 3, 2)
+            );
+
+            // insert new row that does match the filter
+            execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 3, 10, 4, 2);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 10, 2, 2),
+                                    row(2, 10, 3, 2),
+                                    row(3, 10, 4, 2)
+            );
+
+            // update rows that don't match the filter
+            execute("UPDATE %s SET d = ? WHERE a = ? AND b = ? AND c = ?", 1, 0, 0, 0);
+            execute("UPDATE %s SET d = ? WHERE a = ? AND b = ? AND c = ?", 1, 0, 1, 0);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 10, 2, 2),
+                                    row(2, 10, 3, 2),
+                                    row(3, 10, 4, 2)
+            );
+
+            // update a row that does match the filter
+            execute("UPDATE %s SET d = ? WHERE a = ? AND b = ? AND c = ?", 100, 3, 10, 4);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 10, 2, 2),
+                                    row(2, 10, 3, 2),
+                                    row(3, 10, 4, 100)
+            );
+
+            // delete rows that don't match the filter
+            execute("DELETE FROM %s WHERE a = ? AND b = ? AND c = ?", 0, 0, 0);
+            execute("DELETE FROM %s WHERE a = ? AND b = ? AND c = ?", 0, 1, 0);
+            execute("DELETE FROM %s WHERE a = ? AND b = ?", 0, 0);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 10, 2, 2),
+                                    row(2, 10, 3, 2),
+                                    row(3, 10, 4, 100)
+            );
+
+            // delete a row that does match the filter
+            execute("DELETE FROM %s WHERE a = ? AND b = ? AND c = ?", 1, 1, 0);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(1, 10, 2, 2),
+                                    row(2, 10, 3, 2),
+                                    row(3, 10, 4, 100)
+            );
+
+            // delete a partition that matches the filter
+            execute("DELETE FROM %s WHERE a = ? AND b = ?", 1, 10);
+            assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test" + i),
+                                    row(2, 10, 3, 2),
+                                    row(3, 10, 4, 100));
+        }
+    }
+
+    @Test
     public void testPartitionKeyRestrictions() throws Throwable
     {
         List<String> mvPrimaryKeys = Arrays.asList("((a, b), c)", "((b, a), c)", "(a, b, c)", "(c, b, a)", "((c, a), b)");
@@ -1303,4 +1838,306 @@
         executeNet(protocolVersion, "ALTER TABLE %s RENAME inetval TO foo");
         assert !execute("SELECT * FROM mv_test").isEmpty();
     }
+
+    @Test
+    public void testMVCreationWithNonPrimaryRestrictions() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY (a, b))");
+
+        execute("USE " + keyspace());
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        try {
+            createView("mv_test", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE b IS NOT NULL AND c IS NOT NULL AND d = 1 PRIMARY KEY (a, b, c)");
+            dropView("mv_test");
+        } catch(Exception e) {
+            throw new RuntimeException("MV creation with non primary column restrictions failed.", e);
+        }
+
+        dropTable("DROP TABLE %s");
+    }
+
+    @Test
+    public void testNonPrimaryRestrictions() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY (a, b))");
+
+        execute("USE " + keyspace());
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, 0, 0, 0);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, 0, 1, 0);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, 1, 0, 0);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, 1, 1, 0);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 1, 0, 0, 0);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 1, 0, 1, 0);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 1, 1, 0, 0);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 1, 1, 1, 0);
+
+        // only accept rows where c = 1
+        createView("mv_test", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL AND c IS NOT NULL AND c = 1 PRIMARY KEY (a, b, c)");
+
+        while (!SystemKeyspace.isViewBuilt(keyspace(), "mv_test"))
+            Thread.sleep(10);
+
+        assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test"),
+            row(0, 0, 1, 0),
+            row(0, 1, 1, 0),
+            row(1, 0, 1, 0),
+            row(1, 1, 1, 0)
+        );
+
+        // insert new rows that do not match the filter
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 2, 0, 0, 0);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 2, 1, 2, 0);
+        assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test"),
+            row(0, 0, 1, 0),
+            row(0, 1, 1, 0),
+            row(1, 0, 1, 0),
+            row(1, 1, 1, 0)
+        );
+
+        // insert new row that does match the filter
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 1, 2, 1, 0);
+        assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test"),
+            row(0, 0, 1, 0),
+            row(0, 1, 1, 0),
+            row(1, 0, 1, 0),
+            row(1, 1, 1, 0),
+            row(1, 2, 1, 0)
+        );
+
+        // update rows that don't match the filter
+        execute("UPDATE %s SET d = ? WHERE a = ? AND b = ?", 2, 2, 0);
+        execute("UPDATE %s SET d = ? WHERE a = ? AND b = ?", 1, 2, 1);
+        assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test"),
+            row(0, 0, 1, 0),
+            row(0, 1, 1, 0),
+            row(1, 0, 1, 0),
+            row(1, 1, 1, 0),
+            row(1, 2, 1, 0)
+        );
+
+        // update a row that does match the filter
+        execute("UPDATE %s SET d = ? WHERE a = ? AND b = ?", 1, 1, 0);
+        assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test"),
+            row(0, 0, 1, 0),
+            row(0, 1, 1, 0),
+            row(1, 0, 1, 1),
+            row(1, 1, 1, 0),
+            row(1, 2, 1, 0)
+        );
+
+        // delete rows that don't match the filter
+        execute("DELETE FROM %s WHERE a = ? AND b = ?", 2, 0);
+        assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test"),
+            row(0, 0, 1, 0),
+            row(0, 1, 1, 0),
+            row(1, 0, 1, 1),
+            row(1, 1, 1, 0),
+            row(1, 2, 1, 0)
+        );
+
+        // delete a row that does match the filter
+        execute("DELETE FROM %s WHERE a = ? AND b = ?", 1, 2);
+        assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test"),
+            row(0, 0, 1, 0),
+            row(0, 1, 1, 0),
+            row(1, 0, 1, 1),
+            row(1, 1, 1, 0)
+        );
+
+        // delete a partition that matches the filter
+        execute("DELETE FROM %s WHERE a = ?", 1);
+        assertRowsIgnoringOrder(execute("SELECT a, b, c, d FROM mv_test"),
+            row(0, 0, 1, 0),
+            row(0, 1, 1, 0)
+        );
+
+        dropView("mv_test");
+        dropTable("DROP TABLE %s");
+    }
+
+    @Test
+    public void complexRestrictedTimestampUpdateTestWithFlush() throws Throwable
+    {
+        complexRestrictedTimestampUpdateTest(true);
+    }
+
+    @Test
+    public void complexRestrictedTimestampUpdateTestWithoutFlush() throws Throwable
+    {
+        complexRestrictedTimestampUpdateTest(false);
+    }
+
+    public void complexRestrictedTimestampUpdateTest(boolean flush) throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, e int, PRIMARY KEY (a, b))");
+
+        execute("USE " + keyspace());
+        executeNet(protocolVersion, "USE " + keyspace());
+        Keyspace ks = Keyspace.open(keyspace());
+
+        createView("mv", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL AND c IS NOT NULL AND c = 1 PRIMARY KEY (c, a, b)");
+        ks.getColumnFamilyStore("mv").disableAutoCompaction();
+
+        //Set initial values TS=0, matching the restriction and verify view
+        executeNet(protocolVersion, "INSERT INTO %s (a, b, c, d) VALUES (0, 0, 1, 0) USING TIMESTAMP 0");
+        assertRows(execute("SELECT d from mv WHERE c = ? and a = ? and b = ?", 1, 0, 0), row(0));
+
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        //update c's timestamp TS=2
+        executeNet(protocolVersion, "UPDATE %s USING TIMESTAMP 2 SET c = ? WHERE a = ? and b = ? ", 1, 0, 0);
+        assertRows(execute("SELECT d from mv WHERE c = ? and a = ? and b = ?", 1, 0, 0), row(0));
+
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+        //change c's value and TS=3, tombstones c=1 and adds c=0 record
+        executeNet(protocolVersion, "UPDATE %s USING TIMESTAMP 3 SET c = ? WHERE a = ? and b = ? ", 0, 0, 0);
+        assertRows(execute("SELECT d from mv WHERE c = ? and a = ? and b = ?", 0, 0, 0));
+
+        if(flush)
+        {
+            ks.getColumnFamilyStore("mv").forceMajorCompaction();
+            FBUtilities.waitOnFutures(ks.flush());
+        }
+
+        //change c's value back to 1 with TS=4, check we can see d
+        executeNet(protocolVersion, "UPDATE %s USING TIMESTAMP 4 SET c = ? WHERE a = ? and b = ? ", 1, 0, 0);
+        if (flush)
+        {
+            ks.getColumnFamilyStore("mv").forceMajorCompaction();
+            FBUtilities.waitOnFutures(ks.flush());
+        }
+
+        assertRows(execute("SELECT d, e from mv WHERE c = ? and a = ? and b = ?", 1, 0, 0), row(0, null));
+
+
+        //Add e value @ TS=1
+        executeNet(protocolVersion, "UPDATE %s USING TIMESTAMP 1 SET e = ? WHERE a = ? and b = ? ", 1, 0, 0);
+        assertRows(execute("SELECT d, e from mv WHERE c = ? and a = ? and b = ?", 1, 0, 0), row(0, 1));
+
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+
+        //Change d value @ TS=2
+        executeNet(protocolVersion, "UPDATE %s USING TIMESTAMP 2 SET d = ? WHERE a = ? and b = ? ", 2, 0, 0);
+        assertRows(execute("SELECT d from mv WHERE c = ? and a = ? and b = ?", 1, 0, 0), row(2));
+
+        if (flush)
+            FBUtilities.waitOnFutures(ks.flush());
+
+
+        //Change d value @ TS=3
+        executeNet(protocolVersion, "UPDATE %s USING TIMESTAMP 3 SET d = ? WHERE a = ? and b = ? ", 1, 0, 0);
+        assertRows(execute("SELECT d from mv WHERE c = ? and a = ? and b = ?", 1, 0, 0), row(1));
+
+
+        //Tombstone c
+        executeNet(protocolVersion, "DELETE FROM %s WHERE a = ? and b = ?", 0, 0);
+        assertRows(execute("SELECT d from mv"));
+
+        //Add back without D
+        executeNet(protocolVersion, "INSERT INTO %s (a, b, c) VALUES (0, 0, 1)");
+
+        //Make sure D doesn't pop back in.
+        assertRows(execute("SELECT d from mv WHERE c = ? and a = ? and b = ?", 1, 0, 0), row((Object) null));
+
+
+        //New partition
+        // insert a row with timestamp 0
+        executeNet(protocolVersion, "INSERT INTO %s (a, b, c, d, e) VALUES (?, ?, ?, ?, ?) USING TIMESTAMP 0", 1, 0, 1, 0, 0);
+
+        // overwrite pk and e with timestamp 1, but don't overwrite d
+        executeNet(protocolVersion, "INSERT INTO %s (a, b, c, e) VALUES (?, ?, ?, ?) USING TIMESTAMP 1", 1, 0, 1, 0);
+
+        // delete with timestamp 0 (which should only delete d)
+        executeNet(protocolVersion, "DELETE FROM %s USING TIMESTAMP 0 WHERE a = ? AND b = ?", 1, 0);
+        assertRows(execute("SELECT a, b, c, d, e from mv WHERE c = ? and a = ? and b = ?", 1, 1, 0),
+            row(1, 0, 1, null, 0)
+        );
+
+        executeNet(protocolVersion, "UPDATE %s USING TIMESTAMP 2 SET c = ? WHERE a = ? AND b = ?", 1, 1, 1);
+        executeNet(protocolVersion, "UPDATE %s USING TIMESTAMP 3 SET c = ? WHERE a = ? AND b = ?", 1, 1, 0);
+        assertRows(execute("SELECT a, b, c, d, e from mv WHERE c = ? and a = ? and b = ?", 1, 1, 0),
+            row(1, 0, 1, null, 0)
+        );
+
+        executeNet(protocolVersion, "UPDATE %s USING TIMESTAMP 3 SET d = ? WHERE a = ? AND b = ?", 0, 1, 0);
+        assertRows(execute("SELECT a, b, c, d, e from mv WHERE c = ? and a = ? and b = ?", 1, 1, 0),
+            row(1, 0, 1, 0, 0)
+        );
+    }
+
+    @Test
+    public void testRestrictedRegularColumnTimestampUpdates() throws Throwable
+    {
+        // Regression test for CASSANDRA-10910
+
+        createTable("CREATE TABLE %s (" +
+            "k int PRIMARY KEY, " +
+            "c int, " +
+            "val int)");
+
+        execute("USE " + keyspace());
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        createView("mv_rctstest", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE k IS NOT NULL AND c IS NOT NULL AND c = 1 PRIMARY KEY (k,c)");
+
+        updateView("UPDATE %s SET c = ?, val = ? WHERE k = ?", 0, 0, 0);
+        updateView("UPDATE %s SET val = ? WHERE k = ?", 1, 0);
+        updateView("UPDATE %s SET c = ? WHERE k = ?", 1, 0);
+        assertRows(execute("SELECT c, k, val FROM mv_rctstest"), row(1, 0, 1));
+
+        updateView("TRUNCATE %s");
+
+        updateView("UPDATE %s USING TIMESTAMP 1 SET c = ?, val = ? WHERE k = ?", 0, 0, 0);
+        updateView("UPDATE %s USING TIMESTAMP 3 SET c = ? WHERE k = ?", 1, 0);
+        updateView("UPDATE %s USING TIMESTAMP 2 SET val = ? WHERE k = ?", 1, 0);
+        updateView("UPDATE %s USING TIMESTAMP 4 SET c = ? WHERE k = ?", 1, 0);
+        updateView("UPDATE %s USING TIMESTAMP 3 SET val = ? WHERE k = ?", 2, 0);
+        assertRows(execute("SELECT c, k, val FROM mv_rctstest"), row(1, 0, 2));
+    }
+
+    @Test
+    public void testOldTimestampsWithRestrictions() throws Throwable
+    {
+        createTable("CREATE TABLE %s (" +
+            "k int, " +
+            "c int, " +
+            "val text, " + "" +
+            "PRIMARY KEY(k, c))");
+
+        execute("USE " + keyspace());
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        createView("mv_tstest", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE val IS NOT NULL AND k IS NOT NULL AND c IS NOT NULL AND val = 'baz' PRIMARY KEY (val,k,c)");
+
+        for (int i = 0; i < 100; i++)
+            updateView("INSERT into %s (k,c,val)VALUES(?,?,?)", 0, i % 2, "baz");
+
+        Keyspace.open(keyspace()).getColumnFamilyStore(currentTable()).forceBlockingFlush();
+
+        Assert.assertEquals(2, execute("select * from %s").size());
+        Assert.assertEquals(2, execute("select * from mv_tstest").size());
+
+        assertRows(execute("SELECT val from %s where k = 0 and c = 0"), row("baz"));
+        assertRows(execute("SELECT c from mv_tstest where k = 0 and val = ?", "baz"), row(0), row(1));
+
+        //Make sure an old TS does nothing
+        updateView("UPDATE %s USING TIMESTAMP 100 SET val = ? where k = ? AND c = ?", "bar", 0, 1);
+        assertRows(execute("SELECT val from %s where k = 0 and c = 1"), row("baz"));
+        assertRows(execute("SELECT c from mv_tstest where k = 0 and val = ?", "baz"), row(0), row(1));
+        assertRows(execute("SELECT c from mv_tstest where k = 0 and val = ?", "bar"));
+
+        //Latest TS
+        updateView("UPDATE %s SET val = ? where k = ? AND c = ?", "bar", 0, 1);
+        assertRows(execute("SELECT val from %s where k = 0 and c = 1"), row("bar"));
+        assertRows(execute("SELECT c from mv_tstest where k = 0 and val = ?", "bar"));
+        assertRows(execute("SELECT c from mv_tstest where k = 0 and val = ?", "baz"), row(0));
+    }
 }
diff --git a/test/unit/org/apache/cassandra/cql3/ViewSchemaTest.java b/test/unit/org/apache/cassandra/cql3/ViewSchemaTest.java
index ad48713..23ffaf8 100644
--- a/test/unit/org/apache/cassandra/cql3/ViewSchemaTest.java
+++ b/test/unit/org/apache/cassandra/cql3/ViewSchemaTest.java
@@ -44,6 +44,7 @@
 import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.serializers.SimpleDateSerializer;
 import org.apache.cassandra.serializers.TimeSerializer;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.junit.After;
 import org.junit.Before;
@@ -57,7 +58,7 @@
 
 public class ViewSchemaTest extends CQLTester
 {
-    int protocolVersion = 4;
+    ProtocolVersion protocolVersion = ProtocolVersion.V4;
     private final List<String> views = new ArrayList<>();
 
     @BeforeClass
@@ -691,6 +692,21 @@
     }
 
     @Test
+    public void testCreateMVWithFilteringOnNonPkColumn() throws Throwable
+    {
+        // SEE CASSANDRA-13798, we cannot properly support non-pk base column filtering for mv without huge storage
+        // format changes.
+        createTable("CREATE TABLE %s ( a int, b int, c int, d int, PRIMARY KEY (a, b, c))");
+
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        assertInvalidMessage("Non-primary key columns cannot be restricted in the SELECT statement used for materialized view creation",
+                             "CREATE MATERIALIZED VIEW " + keyspace() + ".mv AS SELECT * FROM %s "
+                                     + "WHERE b IS NOT NULL AND c IS NOT NULL AND a IS NOT NULL "
+                                     + "AND d = 1 PRIMARY KEY (c, b, a)");
+    }
+
+    @Test
     public void testViewTokenRestrictions() throws Throwable
     {
         createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY(a))");
diff --git a/test/unit/org/apache/cassandra/cql3/ViewTest.java b/test/unit/org/apache/cassandra/cql3/ViewTest.java
index 6050110..2894e7b 100644
--- a/test/unit/org/apache/cassandra/cql3/ViewTest.java
+++ b/test/unit/org/apache/cassandra/cql3/ViewTest.java
@@ -18,14 +18,24 @@
 
 package org.apache.cassandra.cql3;
 
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
 import com.google.common.util.concurrent.Uninterruptibles;
 
+import org.apache.commons.io.FileUtils;
+
 import junit.framework.Assert;
 import org.junit.After;
 import org.junit.Before;
@@ -43,23 +53,29 @@
 import org.apache.cassandra.concurrent.StageManager;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.db.compaction.CompactionManager;
 import org.apache.cassandra.db.marshal.AsciiType;
+import org.apache.cassandra.db.view.View;
 import org.apache.cassandra.exceptions.SyntaxException;
 import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.SchemaKeyspaceTables;
+import org.apache.cassandra.service.ClientWarn;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.FBUtilities;
 import org.jboss.byteman.contrib.bmunit.BMRule;
 import org.jboss.byteman.contrib.bmunit.BMRules;
 import org.jboss.byteman.contrib.bmunit.BMUnitRunner;
 
-import static junit.framework.Assert.fail;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 @RunWith(BMUnitRunner.class)
 public class ViewTest extends CQLTester
@@ -67,8 +83,9 @@
     /** Latch used by {@link #testTruncateWhileBuilding()} Byteman injections. */
     @SuppressWarnings("unused")
     private static final CountDownLatch blockViewBuild = new CountDownLatch(1);
+    private static final AtomicInteger viewNameSeqNumber = new AtomicInteger();
 
-    int protocolVersion = 4;
+    private static final ProtocolVersion protocolVersion = ProtocolVersion.CURRENT;
     private final List<String> views = new ArrayList<>();
 
     @BeforeClass
@@ -86,7 +103,7 @@
     public void end() throws Throwable
     {
         for (String viewName : views)
-            executeNet(protocolVersion, "DROP MATERIALIZED VIEW " + viewName);
+            executeNet(protocolVersion, "DROP MATERIALIZED VIEW IF EXISTS " + viewName);
     }
 
     private void createView(String name, String query) throws Throwable
@@ -128,7 +145,7 @@
         createTable("CREATE TABLE %s (k1 int primary key, v1 int)");
         createView("view1", "CREATE MATERIALIZED VIEW view1 AS SELECT * FROM %%s WHERE k1 IS NOT NULL AND v1 IS NOT NULL PRIMARY KEY (v1, k1)");
 
-        ColumnFamilyStore batchlog = Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.BATCHES);
+        ColumnFamilyStore batchlog = Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.BATCHES);
         batchlog.disableAutoCompaction();
         batchlog.forceBlockingFlush();
         int batchlogSSTables = batchlog.getLiveSSTables().size();
@@ -208,6 +225,19 @@
         Assert.assertEquals(0, execute("select * from view1").size());
     }
 
+
+    @Test
+    public void createMvWithUnrestrictedPKParts() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k1 int, c1 int , val int, PRIMARY KEY (k1, c1))");
+
+        execute("USE " + keyspace());
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        createView("view1", "CREATE MATERIALIZED VIEW view1 AS SELECT k1 FROM %%s WHERE k1 IS NOT NULL AND c1 IS NOT NULL AND val IS NOT NULL PRIMARY KEY (val, k1, c1)");
+
+    }
+
     @Test
     public void testClusteringKeyTombstone() throws Throwable
     {
@@ -246,7 +276,7 @@
         try
         {
             createView("mv_test", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s");
-            Assert.fail("Should fail if no primary key is filtered as NOT NULL");
+            fail("Should fail if no primary key is filtered as NOT NULL");
         }
         catch (Exception e)
         {
@@ -256,7 +286,7 @@
         try
         {
             createView("mv_test", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE bigintval IS NOT NULL AND asciival IS NOT NULL PRIMARY KEY (bigintval, k, asciival)");
-            Assert.fail("Should fail if compound primary is not completely filtered as NOT NULL");
+            fail("Should fail if compound primary is not completely filtered as NOT NULL");
         }
         catch (Exception e)
         {
@@ -272,7 +302,7 @@
         try
         {
             createView("mv_test", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s");
-            Assert.fail("Should fail if no primary key is filtered as NOT NULL");
+            fail("Should fail if no primary key is filtered as NOT NULL");
         }
         catch (Exception e)
         {
@@ -298,7 +328,7 @@
         try
         {
             createView("mv_static", "CREATE MATERIALIZED VIEW %%s AS SELECT * FROM %s WHERE sval IS NOT NULL AND k IS NOT NULL AND c IS NOT NULL PRIMARY KEY (sval,k,c)");
-            Assert.fail("Use of static column in a MV primary key should fail");
+            fail("Use of static column in a MV primary key should fail");
         }
         catch (InvalidQueryException e)
         {
@@ -307,7 +337,7 @@
         try
         {
             createView("mv_static", "CREATE MATERIALIZED VIEW %%s AS SELECT val, sval FROM %s WHERE val IS NOT NULL AND  k IS NOT NULL AND c IS NOT NULL PRIMARY KEY (val, k, c)");
-            Assert.fail("Explicit select of static column in MV should fail");
+            fail("Explicit select of static column in MV should fail");
         }
         catch (InvalidQueryException e)
         {
@@ -316,7 +346,7 @@
         try
         {
             createView("mv_static", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE val IS NOT NULL AND k IS NOT NULL AND c IS NOT NULL PRIMARY KEY (val,k,c)");
-            Assert.fail("Implicit select of static column in MV should fail");
+            fail("Implicit select of static column in MV should fail");
         }
         catch (InvalidQueryException e)
         {
@@ -420,7 +450,7 @@
         try
         {
             createView("mv_counter", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE count IS NOT NULL AND k IS NOT NULL PRIMARY KEY (count,k)");
-            Assert.fail("MV on counter should fail");
+            fail("MV on counter should fail");
         }
         catch (InvalidQueryException e)
         {
@@ -442,7 +472,7 @@
         try
         {
             createView("mv_super_column", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM " + keyspace + "." + table + " WHERE key IS NOT NULL AND column1 IS NOT NULL PRIMARY KEY (key,column1)");
-            Assert.fail("MV on SuperColumn table should fail");
+            fail("MV on SuperColumn table should fail");
         }
         catch (InvalidQueryException e)
         {
@@ -455,44 +485,19 @@
     {
         createTable("CREATE TABLE %s (" +
                     "k int PRIMARY KEY, " +
-                    "c int, " +
-                    "val int) WITH default_time_to_live = 60");
+                    "result duration)");
 
         execute("USE " + keyspace());
         executeNet(protocolVersion, "USE " + keyspace());
 
-        // Must NOT include "default_time_to_live" for Materialized View creation
         try
         {
-            createView("mv_ttl1", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE k IS NOT NULL AND c IS NOT NULL PRIMARY KEY (k,c) WITH default_time_to_live = 30");
-            Assert.fail("Should fail if TTL is provided for materialized view");
+            createView("mv_duration", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE result IS NOT NULL AND k IS NOT NULL PRIMARY KEY (result,k)");
+            fail("MV on duration should fail");
         }
-        catch (Exception e)
+        catch (InvalidQueryException e)
         {
-        }
-    }
-
-    @Test
-    public void testAlterMvWithTTL() throws Throwable
-    {
-        createTable("CREATE TABLE %s (" +
-                    "k int PRIMARY KEY, " +
-                    "c int, " +
-                    "val int) WITH default_time_to_live = 60");
-
-        execute("USE " + keyspace());
-        executeNet(protocolVersion, "USE " + keyspace());
-
-        createView("mv_ttl2", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE k IS NOT NULL AND c IS NOT NULL PRIMARY KEY (k,c)");
-
-        // Must NOT include "default_time_to_live" on alter Materialized View
-        try
-        {
-            executeNet(protocolVersion, "ALTER MATERIALIZED VIEW %s WITH default_time_to_live = 30");
-            Assert.fail("Should fail if TTL is provided while altering materialized view");
-        }
-        catch (Exception e)
-        {
+            Assert.assertEquals("Cannot use Duration column 'result' in PRIMARY KEY of materialized view", e.getMessage());
         }
     }
 
@@ -780,12 +785,12 @@
                 createView("mv1_" + def.name, query);
 
                 if (def.type.isMultiCell())
-                    Assert.fail("MV on a multicell should fail " + def);
+                    fail("MV on a multicell should fail " + def);
             }
             catch (InvalidQueryException e)
             {
                 if (!def.type.isMultiCell() && !def.isPartitionKey())
-                    Assert.fail("MV creation failed on " + def);
+                    fail("MV creation failed on " + def);
             }
 
 
@@ -797,12 +802,12 @@
                 createView("mv2_" + def.name, query);
 
                 if (def.type.isMultiCell())
-                    Assert.fail("MV on a multicell should fail " + def);
+                    fail("MV on a multicell should fail " + def);
             }
             catch (InvalidQueryException e)
             {
                 if (!def.type.isMultiCell() && !def.isPartitionKey())
-                    Assert.fail("MV creation failed on " + def);
+                    fail("MV creation failed on " + def);
             }
 
             try
@@ -812,12 +817,12 @@
                 createView("mv3_" + def.name, query);
 
                 if (def.type.isMultiCell())
-                    Assert.fail("MV on a multicell should fail " + def);
+                    fail("MV on a multicell should fail " + def);
             }
             catch (InvalidQueryException e)
             {
                 if (!def.type.isMultiCell() && !def.isPartitionKey())
-                    Assert.fail("MV creation failed on " + def);
+                    fail("MV creation failed on " + def);
             }
 
 
@@ -827,7 +832,7 @@
                                + (def.name.toString().equals("asciival") ? "" : "AND asciival IS NOT NULL ") + "PRIMARY KEY ((" + def.name + ", k), asciival)";
                 createView("mv3_" + def.name, query);
 
-                Assert.fail("Should fail on duplicate name");
+                fail("Should fail on duplicate name");
             }
             catch (Exception e)
             {
@@ -838,7 +843,7 @@
                 String query = "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE " + def.name + " IS NOT NULL AND k IS NOT NULL "
                                + (def.name.toString().equals("asciival") ? "" : "AND asciival IS NOT NULL ") + "PRIMARY KEY ((" + def.name + ", k), nonexistentcolumn)";
                 createView("mv3_" + def.name, query);
-                Assert.fail("Should fail with unknown base column");
+                fail("Should fail with unknown base column");
             }
             catch (InvalidQueryException e)
             {
@@ -902,6 +907,39 @@
     }
 
     @Test
+    public void testFrozenCollectionsWithComplicatedInnerType() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int, intval int,  listval frozen<list<tuple<text,text>>>, PRIMARY KEY (k))");
+
+        execute("USE " + keyspace());
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        createView("mv",
+                   "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE k IS NOT NULL AND listval IS NOT NULL PRIMARY KEY (k, listval)");
+
+        updateView("INSERT INTO %s (k, intval, listval) VALUES (?, ?, fromJson(?))",
+                   0,
+                   0,
+                   "[[\"a\",\"1\"], [\"b\",\"2\"], [\"c\",\"3\"]]");
+
+        // verify input
+        assertRows(execute("SELECT k, listval FROM %s WHERE k = ?", 0),
+                   row(0, list(tuple("a", "1"), tuple("b", "2"), tuple("c", "3"))));
+        assertRows(execute("SELECT k, listval from mv"),
+                   row(0, list(tuple("a", "1"), tuple("b", "2"), tuple("c", "3"))));
+
+        // update listval with the same value and it will be compared in view generator
+        updateView("INSERT INTO %s (k, listval) VALUES (?, fromJson(?))",
+                   0,
+                   "[[\"a\",\"1\"], [\"b\",\"2\"], [\"c\",\"3\"]]");
+        // verify result
+        assertRows(execute("SELECT k, listval FROM %s WHERE k = ?", 0),
+                   row(0, list(tuple("a", "1"), tuple("b", "2"), tuple("c", "3"))));
+        assertRows(execute("SELECT k, listval from mv"),
+                   row(0, list(tuple("a", "1"), tuple("b", "2"), tuple("c", "3"))));
+    }
+
+    @Test
     public void testUpdate() throws Throwable
     {
         createTable("CREATE TABLE %s (" +
@@ -1256,7 +1294,7 @@
         try
         {
             createView("mv_de", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL AND c IS NOT NULL AND d IS NOT NULL AND e IS NOT NULL PRIMARY KEY ((d, a), b, e, c)");
-            Assert.fail("Should have rejected a query including multiple non-primary key base columns");
+            fail("Should have rejected a query including multiple non-primary key base columns");
         }
         catch (Exception e)
         {
@@ -1265,7 +1303,7 @@
         try
         {
             createView("mv_de", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE a IS NOT NULL AND b IS NOT NULL AND c IS NOT NULL AND d IS NOT NULL AND e IS NOT NULL PRIMARY KEY ((a, b), c, d, e)");
-            Assert.fail("Should have rejected a query including multiple non-primary key base columns");
+            fail("Should have rejected a query including multiple non-primary key base columns");
         }
         catch (Exception e)
         {
@@ -1321,6 +1359,55 @@
         assertRowsNet(protocolVersion, executeNet(protocolVersion, "SELECT * FROM mv"), row(1, 0));
     }
 
+    public void testCreateMvWithTTL() throws Throwable
+    {
+        createTable("CREATE TABLE %s (" +
+                "k int PRIMARY KEY, " +
+                "c int, " +
+                "val int) WITH default_time_to_live = 60");
+
+        execute("USE " + keyspace());
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        // Must NOT include "default_time_to_live" for Materialized View creation
+        try
+        {
+            createView("mv_ttl1", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE k IS NOT NULL AND c IS NOT NULL PRIMARY KEY (k,c) WITH default_time_to_live = 30");
+            fail("Should fail if TTL is provided for materialized view");
+        }
+        catch (Exception e)
+        {
+        }
+    }
+
+    @Test
+    public void testAlterMvWithTTL() throws Throwable
+    {
+        createTable("CREATE TABLE %s (" +
+                    "k int PRIMARY KEY, " +
+                    "c int, " +
+                    "val int) WITH default_time_to_live = 60");
+
+        execute("USE " + keyspace());
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        createView("mv_ttl2", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE k IS NOT NULL AND c IS NOT NULL PRIMARY KEY (k,c)");
+
+        // Must NOT include "default_time_to_live" on alter Materialized View
+        try
+        {
+            executeNet(protocolVersion, "ALTER MATERIALIZED VIEW " + keyspace() + ".mv_ttl2 WITH default_time_to_live = 30");
+            fail("Should fail if TTL is provided while altering materialized view");
+        }
+        catch (Exception e)
+        {
+            // Make sure the message is clear. See CASSANDRA-16960
+            assertEquals("Forbidden default_time_to_live detected for a materialized view. Data in a materialized view always expire at the same time than the corresponding "
+                         + "data in the parent table. default_time_to_live must be set to zero, see CASSANDRA-12868 for more information",
+                         e.getMessage());
+        }
+    }
+
     @Test
     public void testViewBuilderResume() throws Throwable
     {
@@ -1376,38 +1463,6 @@
         assertRows(execute("SELECT count(*) FROM mv_test"), row(1024L));
     }
 
-    @Test
-    public void testFrozenCollectionsWithComplicatedInnerType() throws Throwable
-    {
-        createTable("CREATE TABLE %s (k int, intval int,  listval frozen<list<tuple<text,text>>>, PRIMARY KEY (k))");
-
-        execute("USE " + keyspace());
-        executeNet(protocolVersion, "USE " + keyspace());
-
-        createView("mv",
-                   "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE k IS NOT NULL AND listval IS NOT NULL PRIMARY KEY (k, listval)");
-
-        updateView("INSERT INTO %s (k, intval, listval) VALUES (?, ?, fromJson(?))",
-                   0,
-                   0,
-                   "[[\"a\", \"1\"], [\"b\", \"2\"], [\"c\", \"3\"]]");
-
-        // verify input
-        assertRows(execute("SELECT k, toJson(listval) FROM %s WHERE k = ?", 0),
-                   row(0, "[[\"a\", \"1\"], [\"b\", \"2\"], [\"c\", \"3\"]]"));
-        assertRows(execute("SELECT k, toJson(listval) from mv"),
-                   row(0, "[[\"a\", \"1\"], [\"b\", \"2\"], [\"c\", \"3\"]]"));
-
-        // update listval with the same value and it will be compared in view generator
-        updateView("INSERT INTO %s (k, listval) VALUES (?, fromJson(?))",
-                   0,
-                   "[[\"a\", \"1\"], [\"b\", \"2\"], [\"c\", \"3\"]]");
-        // verify result
-        assertRows(execute("SELECT k, toJson(listval) FROM %s WHERE k = ?", 0),
-                   row(0, "[[\"a\", \"1\"], [\"b\", \"2\"], [\"c\", \"3\"]]"));
-        assertRows(execute("SELECT k, toJson(listval) from mv"),
-                   row(0, "[[\"a\", \"1\"], [\"b\", \"2\"], [\"c\", \"3\"]]"));
-    }
 
     @Test(expected = SyntaxException.class)
     public void emptyViewNameTest() throws Throwable
@@ -1421,6 +1476,54 @@
          execute("CREATE MATERIALIZED VIEW myview AS SELECT a, b FROM \"\" WHERE b IS NOT NULL PRIMARY KEY (b, a)");
      }
 
+    /**
+     * Tests that a client warning is issued on materialized view creation.
+     */
+    @Test
+    public void testClientWarningOnCreate() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)");
+
+        ClientWarn.instance.captureWarnings();
+        String viewName = keyspace() + ".warning_view";
+        execute("CREATE MATERIALIZED VIEW " + viewName +
+                " AS SELECT v FROM %s WHERE k IS NOT NULL AND v IS NOT NULL PRIMARY KEY (v, k)");
+        views.add(viewName);
+        List<String> warnings = ClientWarn.instance.getWarnings();
+
+        Assert.assertNotNull(warnings);
+        Assert.assertEquals(1, warnings.size());
+        Assert.assertEquals(View.USAGE_WARNING, warnings.get(0));
+    }
+
+    /**
+     * Tests the configuration flag to disable materialized views.
+     */
+    @Test
+    public void testDisableMaterializedViews() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)");
+
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        boolean enableMaterializedViews = DatabaseDescriptor.getEnableMaterializedViews();
+        try
+        {
+            DatabaseDescriptor.setEnableMaterializedViews(false);
+            createView("view1", "CREATE MATERIALIZED VIEW %s AS SELECT v FROM %%s WHERE k IS NOT NULL AND v IS NOT NULL PRIMARY KEY (v, k)");
+            fail("Should not be able to create a materialized view if they are disabled");
+        }
+        catch (Throwable e)
+        {
+            Assert.assertTrue(e instanceof InvalidQueryException);
+            Assert.assertTrue(e.getMessage().contains("Materialized views are disabled"));
+        }
+        finally
+        {
+            DatabaseDescriptor.setEnableMaterializedViews(enableMaterializedViews);
+        }
+    }
+
     @Test
     public void viewOnCompactTableTest() throws Throwable
     {
@@ -1434,11 +1537,129 @@
         }
         catch (Throwable t)
         {
-            Assert.assertEquals("Unknown column name detected in CREATE MATERIALIZED VIEW statement : value",
+            Assert.assertEquals("Undefined column name value",
                                 t.getMessage());
         }
     }
 
+    @Test
+    public void testFunctionInWhereClause() throws Throwable
+    {
+        // Native token function with lowercase, should be unquoted in the schema where clause
+        assertEmpty(testFunctionInWhereClause("CREATE TABLE %s (k bigint PRIMARY KEY, v int)",
+                                              null,
+                                              "CREATE MATERIALIZED VIEW %s AS" +
+                                              "   SELECT * FROM %%s WHERE k = token(1) AND v IS NOT NULL " +
+                                              "   PRIMARY KEY (v, k)",
+                                              "k = token(1) AND v IS NOT NULL",
+                                              "INSERT INTO %s(k, v) VALUES (0, 1)",
+                                              "INSERT INTO %s(k, v) VALUES (2, 3)"));
+
+        // Native token function with uppercase, should be unquoted and lowercased in the schema where clause
+        assertEmpty(testFunctionInWhereClause("CREATE TABLE %s (k bigint PRIMARY KEY, v int)",
+                                              null,
+                                              "CREATE MATERIALIZED VIEW %s AS" +
+                                              "   SELECT * FROM %%s WHERE k = TOKEN(1) AND v IS NOT NULL" +
+                                              "   PRIMARY KEY (v, k)",
+                                              "k = token(1) AND v IS NOT NULL",
+                                              "INSERT INTO %s(k, v) VALUES (0, 1)",
+                                              "INSERT INTO %s(k, v) VALUES (2, 3)"));
+
+        // UDF with lowercase name, shouldn't be quoted in the schema where clause
+        assertRows(testFunctionInWhereClause("CREATE TABLE %s (k int PRIMARY KEY, v int)",
+                                             "CREATE FUNCTION fun()" +
+                                             "   CALLED ON NULL INPUT" +
+                                             "   RETURNS int LANGUAGE java" +
+                                             "   AS 'return 2;'",
+                                             "CREATE MATERIALIZED VIEW %s AS " +
+                                             "   SELECT * FROM %%s WHERE k = fun() AND v IS NOT NULL" +
+                                             "   PRIMARY KEY (v, k)",
+                                             "k = fun() AND v IS NOT NULL",
+                                             "INSERT INTO %s(k, v) VALUES (0, 1)",
+                                             "INSERT INTO %s(k, v) VALUES (2, 3)"), row(3, 2));
+
+        // UDF with uppercase name, should be quoted in the schema where clause
+        assertRows(testFunctionInWhereClause("CREATE TABLE %s (k int PRIMARY KEY, v int)",
+                                             "CREATE FUNCTION \"FUN\"()" +
+                                             "   CALLED ON NULL INPUT" +
+                                             "   RETURNS int" +
+                                             "   LANGUAGE java" +
+                                             "   AS 'return 2;'",
+                                             "CREATE MATERIALIZED VIEW %s AS " +
+                                             "   SELECT * FROM %%s WHERE k = \"FUN\"() AND v IS NOT NULL" +
+                                             "   PRIMARY KEY (v, k)",
+                                             "k = \"FUN\"() AND v IS NOT NULL",
+                                             "INSERT INTO %s(k, v) VALUES (0, 1)",
+                                             "INSERT INTO %s(k, v) VALUES (2, 3)"), row(3, 2));
+
+        // UDF with uppercase name conflicting with TOKEN keyword but not with native token function name,
+        // should be quoted in the schema where clause
+        assertRows(testFunctionInWhereClause("CREATE TABLE %s (k int PRIMARY KEY, v int)",
+                                             "CREATE FUNCTION \"TOKEN\"(x int)" +
+                                             "   CALLED ON NULL INPUT" +
+                                             "   RETURNS int" +
+                                             "   LANGUAGE java" +
+                                             "   AS 'return x;'",
+                                             "CREATE MATERIALIZED VIEW %s AS" +
+                                             "   SELECT * FROM %%s WHERE k = \"TOKEN\"(2) AND v IS NOT NULL" +
+                                             "   PRIMARY KEY (v, k)",
+                                             "k = \"TOKEN\"(2) AND v IS NOT NULL",
+                                             "INSERT INTO %s(k, v) VALUES (0, 1)",
+                                             "INSERT INTO %s(k, v) VALUES (2, 3)"), row(3, 2));
+
+        // UDF with lowercase name conflicting with both TOKEN keyword and native token function name,
+        // requires specifying the keyspace and should be quoted in the schema where clause
+        assertRows(testFunctionInWhereClause("CREATE TABLE %s (k int PRIMARY KEY, v int)",
+                                             "CREATE FUNCTION \"token\"(x int)" +
+                                             "   CALLED ON NULL INPUT" +
+                                             "   RETURNS int" +
+                                             "   LANGUAGE java" +
+                                             "   AS 'return x;'",
+                                             "CREATE MATERIALIZED VIEW %s AS" +
+                                             "   SELECT * FROM %%s " +
+                                             "   WHERE k = " + keyspace() + ".\"token\"(2) AND v IS NOT NULL" +
+                                             "   PRIMARY KEY (v, k)",
+                                             "k = " + keyspace() + ".\"token\"(2) AND v IS NOT NULL",
+                                             "INSERT INTO %s(k, v) VALUES (0, 1)",
+                                             "INSERT INTO %s(k, v) VALUES (2, 3)"), row(3, 2));
+    }
+
+    @Test
+    public void testSnapshotNameOfDroppedViewHasDroppedPrefix() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)");
+
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        boolean enableMaterializedViews = DatabaseDescriptor.getEnableMaterializedViews();
+        try
+        {
+            // create it
+            DatabaseDescriptor.setEnableMaterializedViews(true);
+            createView("viewtobedropped", "CREATE MATERIALIZED VIEW %s AS SELECT v FROM %%s WHERE k IS NOT NULL AND v IS NOT NULL PRIMARY KEY (v, k)");
+
+            // drop it
+            executeNet(protocolVersion, "DROP MATERIALIZED VIEW " + keyspace() + ".viewtobedropped");
+
+            String dataDir = DatabaseDescriptor.getAllDataFileLocations()[0];
+            // look for snapshots which have "dropped-" prefix, there has to be such
+            Pattern compile = Pattern.compile(String.format("%s/viewtobedropped-.*/snapshots/dropped-.*-viewtobedropped/.*", keyspace()));
+
+            List<File> collect = FileUtils.listFiles(new File(dataDir), null, true)
+                                          .stream()
+                                          .filter(file -> {
+                                              Path relativize = Paths.get(dataDir).relativize(file.toPath());
+                                              Matcher matcher = compile.matcher(relativize.toString());
+                                              return matcher.matches();
+                                          }).collect(Collectors.toList());
+            assertFalse(collect.isEmpty());
+        }
+        finally
+        {
+            DatabaseDescriptor.setEnableMaterializedViews(enableMaterializedViews);
+        }
+    }
+
     /**
      * Tests that truncating a table stops the ongoing builds of its materialized views,
      * so they don't write into the MV data that has been truncated in the base table.
@@ -1492,4 +1713,37 @@
     {
         return CompactionManager.instance.getPendingTasks() + CompactionManager.instance.getActiveCompactions();
     }
-}
\ No newline at end of file
+
+    private UntypedResultSet testFunctionInWhereClause(String createTableQuery,
+                                                       String createFunctionQuery,
+                                                       String createViewQuery,
+                                                       String expectedSchemaWhereClause,
+                                                       String... insertQueries) throws Throwable
+    {
+        createTable(createTableQuery);
+
+        execute("USE " + keyspace());
+        executeNet(protocolVersion, "USE " + keyspace());
+
+        if (createFunctionQuery != null)
+        {
+            execute(createFunctionQuery);
+        }
+
+        String viewName = "view_" + viewNameSeqNumber.getAndIncrement();
+        createView(viewName, createViewQuery);
+
+        // Test the where clause stored in system_schema.views
+        String schemaQuery = String.format("SELECT where_clause FROM %s.%s WHERE keyspace_name = ? AND view_name = ?",
+                                           SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                           SchemaKeyspaceTables.VIEWS);
+        assertRows(execute(schemaQuery, keyspace(), viewName), row(expectedSchemaWhereClause));
+
+        for (String insert : insertQueries)
+        {
+            execute(insert);
+        }
+
+        return execute("SELECT * FROM " + viewName);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/functions/CastFctsTest.java b/test/unit/org/apache/cassandra/cql3/functions/CastFctsTest.java
new file mode 100644
index 0000000..2ffd8b4
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/functions/CastFctsTest.java
@@ -0,0 +1,324 @@
+/*
+ * 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.functions;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Date;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.serializers.SimpleDateSerializer;
+import org.apache.cassandra.utils.UUIDGen;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.format.DateTimeFormat;
+import org.junit.Test;
+
+public class CastFctsTest extends CQLTester
+{
+    @Test
+    public void testInvalidQueries() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int primary key, b text, c double)");
+
+        assertInvalidSyntaxMessage("no viable alternative at input '(' (... b, c) VALUES ([CAST](...)",
+                                   "INSERT INTO %s (a, b, c) VALUES (CAST(? AS int), ?, ?)", 1.6, "test", 6.3);
+
+        assertInvalidSyntaxMessage("no viable alternative at input '(' (..." + KEYSPACE + "." + currentTable()
+                + " SET c = [cast](...)",
+                                   "UPDATE %s SET c = cast(? as double) WHERE a = ?", 1, 1);
+
+        assertInvalidSyntaxMessage("no viable alternative at input '(' (...= ? WHERE a = [CAST] (...)",
+                                   "UPDATE %s SET c = ? WHERE a = CAST (? AS INT)", 1, 2.0);
+
+        assertInvalidSyntaxMessage("no viable alternative at input '(' (..." + KEYSPACE + "." + currentTable()
+                + " WHERE a = [CAST] (...)",
+                                   "DELETE FROM %s WHERE a = CAST (? AS INT)", 1, 2.0);
+
+        assertInvalidSyntaxMessage("no viable alternative at input '(' (..." + KEYSPACE + "." + currentTable()
+                + " WHERE a = [CAST] (...)",
+                                   "SELECT * FROM %s WHERE a = CAST (? AS INT)", 1, 2.0);
+
+        assertInvalidMessage("a cannot be cast to boolean", "SELECT CAST(a AS boolean) FROM %s");
+    }
+
+    @Test
+    public void testNumericCastsInSelectionClause() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a tinyint primary key,"
+                                   + " b smallint,"
+                                   + " c int,"
+                                   + " d bigint,"
+                                   + " e float,"
+                                   + " f double,"
+                                   + " g decimal,"
+                                   + " h varint,"
+                                   + " i int)");
+
+        execute("INSERT INTO %s (a, b, c, d, e, f, g, h) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+                (byte) 1, (short) 2, 3, 4L, 5.2F, 6.3, BigDecimal.valueOf(6.3), BigInteger.valueOf(4));
+
+        assertColumnNames(execute("SELECT CAST(b AS int), CAST(c AS int), CAST(d AS double) FROM %s"),
+                          "cast(b as int)",
+                          "c",
+                          "cast(d as double)");
+
+        assertRows(execute("SELECT CAST(a AS tinyint), " +
+                "CAST(b AS tinyint), " +
+                "CAST(c AS tinyint), " +
+                "CAST(d AS tinyint), " +
+                "CAST(e AS tinyint), " +
+                "CAST(f AS tinyint), " +
+                "CAST(g AS tinyint), " +
+                "CAST(h AS tinyint), " +
+                "CAST(i AS tinyint) FROM %s"),
+                   row((byte) 1, (byte) 2, (byte) 3, (byte) 4L, (byte) 5, (byte) 6, (byte) 6, (byte) 4, null));
+
+        assertRows(execute("SELECT CAST(a AS smallint), " +
+                "CAST(b AS smallint), " +
+                "CAST(c AS smallint), " +
+                "CAST(d AS smallint), " +
+                "CAST(e AS smallint), " +
+                "CAST(f AS smallint), " +
+                "CAST(g AS smallint), " +
+                "CAST(h AS smallint), " +
+                "CAST(i AS smallint) FROM %s"),
+                   row((short) 1, (short) 2, (short) 3, (short) 4L, (short) 5, (short) 6, (short) 6, (short) 4, null));
+
+        assertRows(execute("SELECT CAST(a AS int), " +
+                "CAST(b AS int), " +
+                "CAST(c AS int), " +
+                "CAST(d AS int), " +
+                "CAST(e AS int), " +
+                "CAST(f AS int), " +
+                "CAST(g AS int), " +
+                "CAST(h AS int), " +
+                "CAST(i AS int) FROM %s"),
+                   row(1, 2, 3, 4, 5, 6, 6, 4, null));
+
+        assertRows(execute("SELECT CAST(a AS bigint), " +
+                "CAST(b AS bigint), " +
+                "CAST(c AS bigint), " +
+                "CAST(d AS bigint), " +
+                "CAST(e AS bigint), " +
+                "CAST(f AS bigint), " +
+                "CAST(g AS bigint), " +
+                "CAST(h AS bigint), " +
+                "CAST(i AS bigint) FROM %s"),
+                   row(1L, 2L, 3L, 4L, 5L, 6L, 6L, 4L, null));
+
+        assertRows(execute("SELECT CAST(a AS float), " +
+                "CAST(b AS float), " +
+                "CAST(c AS float), " +
+                "CAST(d AS float), " +
+                "CAST(e AS float), " +
+                "CAST(f AS float), " +
+                "CAST(g AS float), " +
+                "CAST(h AS float), " +
+                "CAST(i AS float) FROM %s"),
+                   row(1.0F, 2.0F, 3.0F, 4.0F, 5.2F, 6.3F, 6.3F, 4.0F, null));
+
+        assertRows(execute("SELECT CAST(a AS double), " +
+                "CAST(b AS double), " +
+                "CAST(c AS double), " +
+                "CAST(d AS double), " +
+                "CAST(e AS double), " +
+                "CAST(f AS double), " +
+                "CAST(g AS double), " +
+                "CAST(h AS double), " +
+                "CAST(i AS double) FROM %s"),
+                   row(1.0, 2.0, 3.0, 4.0, (double) 5.2F, 6.3, 6.3, 4.0, null));
+
+        assertRows(execute("SELECT CAST(a AS decimal), " +
+                "CAST(b AS decimal), " +
+                "CAST(c AS decimal), " +
+                "CAST(d AS decimal), " +
+                "CAST(e AS decimal), " +
+                "CAST(f AS decimal), " +
+                "CAST(g AS decimal), " +
+                "CAST(h AS decimal), " +
+                "CAST(i AS decimal) FROM %s"),
+                   row(BigDecimal.valueOf(1),
+                       BigDecimal.valueOf(2),
+                       BigDecimal.valueOf(3),
+                       BigDecimal.valueOf(4),
+                       BigDecimal.valueOf(5.2F),
+                       BigDecimal.valueOf(6.3),
+                       BigDecimal.valueOf(6.3),
+                       BigDecimal.valueOf(4),
+                       null));
+
+        assertRows(execute("SELECT CAST(a AS ascii), " +
+                "CAST(b AS ascii), " +
+                "CAST(c AS ascii), " +
+                "CAST(d AS ascii), " +
+                "CAST(e AS ascii), " +
+                "CAST(f AS ascii), " +
+                "CAST(g AS ascii), " +
+                "CAST(h AS ascii), " +
+                "CAST(i AS ascii) FROM %s"),
+                   row("1",
+                       "2",
+                       "3",
+                       "4",
+                       "5.2",
+                       "6.3",
+                       "6.3",
+                       "4",
+                       null));
+
+        assertRows(execute("SELECT CAST(a AS text), " +
+                "CAST(b AS text), " +
+                "CAST(c AS text), " +
+                "CAST(d AS text), " +
+                "CAST(e AS text), " +
+                "CAST(f AS text), " +
+                "CAST(g AS text), " +
+                "CAST(h AS text), " +
+                "CAST(i AS text) FROM %s"),
+                   row("1",
+                       "2",
+                       "3",
+                       "4",
+                       "5.2",
+                       "6.3",
+                       "6.3",
+                       "4",
+                       null));
+    }
+
+    @Test
+    public void testNoLossOfPrecisionForCastToDecimal() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, bigint_clmn bigint, varint_clmn varint)");
+        execute("INSERT INTO %s(k, bigint_clmn, varint_clmn) VALUES(2, 9223372036854775807, 1234567890123456789)");
+
+        assertRows(execute("SELECT CAST(bigint_clmn AS decimal), CAST(varint_clmn AS decimal) FROM %s"),
+                   row(BigDecimal.valueOf(9223372036854775807L), BigDecimal.valueOf(1234567890123456789L)));
+    }
+
+    @Test
+    public void testTimeCastsInSelectionClause() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a timeuuid primary key, b timestamp, c date, d time)");
+
+        DateTime dateTime = DateTimeFormat.forPattern("yyyy-MM-dd hh:mm:ss")
+                .withZone(DateTimeZone.UTC)
+                .parseDateTime("2015-05-21 11:03:02");
+
+        DateTime date = DateTimeFormat.forPattern("yyyy-MM-dd")
+                .withZone(DateTimeZone.UTC)
+                .parseDateTime("2015-05-21");
+
+        long timeInMillis = dateTime.getMillis();
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, '2015-05-21 11:03:02+00', '2015-05-21', '11:03:02')",
+                UUIDGen.getTimeUUID(timeInMillis));
+
+        assertRows(execute("SELECT CAST(a AS timestamp), " +
+                           "CAST(b AS timestamp), " +
+                           "CAST(c AS timestamp) FROM %s"),
+                   row(new Date(dateTime.getMillis()), new Date(dateTime.getMillis()), new Date(date.getMillis())));
+
+        int timeInMillisToDay = SimpleDateSerializer.timeInMillisToDay(date.getMillis());
+        assertRows(execute("SELECT CAST(a AS date), " +
+                           "CAST(b AS date), " +
+                           "CAST(c AS date) FROM %s"),
+                   row(timeInMillisToDay, timeInMillisToDay, timeInMillisToDay));
+
+        assertRows(execute("SELECT CAST(b AS text), " +
+                           "CAST(c AS text), " +
+                           "CAST(d AS text) FROM %s"),
+                   row("2015-05-21T11:03:02.000Z", "2015-05-21", "11:03:02.000000000"));
+    }
+
+    @Test
+    public void testOtherTypeCastsInSelectionClause() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a ascii primary key,"
+                                   + " b inet,"
+                                   + " c boolean)");
+
+        execute("INSERT INTO %s (a, b, c) VALUES (?, '127.0.0.1', ?)",
+                "test", true);
+
+        assertRows(execute("SELECT CAST(a AS text), " +
+                "CAST(b AS text), " +
+                "CAST(c AS text) FROM %s"),
+                   row("test", "127.0.0.1", "true"));
+    }
+
+    @Test
+    public void testCastsWithReverseOrder() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int,"
+                                   + " b smallint,"
+                                   + " c double,"
+                                   + " primary key (a, b)) WITH CLUSTERING ORDER BY (b DESC);");
+
+        execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)",
+                1, (short) 2, 6.3);
+
+        assertRows(execute("SELECT CAST(a AS tinyint), " +
+                "CAST(b AS tinyint), " +
+                "CAST(c AS tinyint) FROM %s"),
+                   row((byte) 1, (byte) 2, (byte) 6));
+
+        assertRows(execute("SELECT CAST(CAST(a AS tinyint) AS smallint), " +
+                "CAST(CAST(b AS tinyint) AS smallint), " +
+                "CAST(CAST(c AS tinyint) AS smallint) FROM %s"),
+                   row((short) 1, (short) 2, (short) 6));
+
+        assertRows(execute("SELECT CAST(CAST(CAST(a AS tinyint) AS double) AS text), " +
+                "CAST(CAST(CAST(b AS tinyint) AS double) AS text), " +
+                "CAST(CAST(CAST(c AS tinyint) AS double) AS text) FROM %s"),
+                   row("1.0", "2.0", "6.0"));
+
+        String f = createFunction(KEYSPACE, "int",
+                                  "CREATE FUNCTION %s(val int) " +
+                                          "RETURNS NULL ON NULL INPUT " +
+                                          "RETURNS double " +
+                                          "LANGUAGE java " +
+                                          "AS 'return (double)val;'");
+
+        assertRows(execute("SELECT " + f + "(CAST(b AS int)) FROM %s"),
+                   row((double) 2));
+
+        assertRows(execute("SELECT CAST(" + f + "(CAST(b AS int)) AS text) FROM %s"),
+                   row("2.0"));
+    }
+
+    @Test
+    public void testCounterCastsInSelectionClause() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int primary key, b counter)");
+
+        execute("UPDATE %s SET b = b + 2 WHERE a = 1");
+
+        assertRows(execute("SELECT CAST(b AS tinyint), " +
+                "CAST(b AS smallint), " +
+                "CAST(b AS int), " +
+                "CAST(b AS bigint), " +
+                "CAST(b AS float), " +
+                "CAST(b AS double), " +
+                "CAST(b AS decimal), " +
+                "CAST(b AS ascii), " +
+                "CAST(b AS text) FROM %s"),
+                   row((byte) 2, (short) 2, 2, 2L, 2.0F, 2.0, BigDecimal.valueOf(2), "2", "2"));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/functions/TimeFctsTest.java b/test/unit/org/apache/cassandra/cql3/functions/TimeFctsTest.java
index f6fca20..5b9737f 100644
--- a/test/unit/org/apache/cassandra/cql3/functions/TimeFctsTest.java
+++ b/test/unit/org/apache/cassandra/cql3/functions/TimeFctsTest.java
@@ -27,7 +27,7 @@
 import org.apache.cassandra.db.marshal.SimpleDateType;
 import org.apache.cassandra.db.marshal.TimeUUIDType;
 import org.apache.cassandra.db.marshal.TimestampType;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.UUIDGen;
 import org.joda.time.DateTime;
@@ -201,6 +201,6 @@
     private static ByteBuffer executeFunction(Function function, ByteBuffer input)
     {
         List<ByteBuffer> params = Arrays.asList(input);
-        return ((ScalarFunction) function).execute(Server.CURRENT_VERSION, params);
+        return ((ScalarFunction) function).execute(ProtocolVersion.CURRENT, params);
     }
 }
diff --git a/test/unit/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictionsTest.java b/test/unit/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictionsTest.java
new file mode 100644
index 0000000..83c00d0
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictionsTest.java
@@ -0,0 +1,1927 @@
+/*
+ * 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.restrictions;
+
+import java.nio.ByteBuffer;
+import java.util.*;
+
+import com.google.common.collect.Iterables;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.*;
+import org.apache.cassandra.cql3.Term.MultiItemTerminal;
+import org.apache.cassandra.cql3.statements.Bound;
+
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.ReversedType;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ClusteringColumnRestrictionsTest
+{
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
+    @Test
+    public void testBoundsAsClusteringWithNoRestrictions()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC);
+
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+    }
+
+    /**
+     * Test 'clustering_0 = 1' with only one clustering column
+     */
+    @Test
+    public void testBoundsAsClusteringWithOneEqRestrictionsAndOneClusteringColumn()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC);
+
+        ByteBuffer clustering_0 = ByteBufferUtil.bytes(1);
+        Restriction eq = newSingleEq(cfMetaData, 0, clustering_0);
+
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(eq);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, clustering_0);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, clustering_0);
+    }
+
+    /**
+     * Test 'clustering_1 = 1' with 2 clustering columns
+     */
+    @Test
+    public void testBoundsAsClusteringWithOneEqRestrictionsAndTwoClusteringColumns()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
+
+        ByteBuffer clustering_0 = ByteBufferUtil.bytes(1);
+        Restriction eq = newSingleEq(cfMetaData, 0, clustering_0);
+
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(eq);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, clustering_0);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, clustering_0);
+    }
+
+    /**
+     * Test 'clustering_0 IN (1, 2, 3)' with only one clustering column
+     */
+    @Test
+    public void testBoundsAsClusteringWithOneInRestrictionsAndOneClusteringColumn()
+    {
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+        ByteBuffer value3 = ByteBufferUtil.bytes(3);
+
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
+
+        Restriction in = newSingleIN(cfMetaData, 0, value1, value2, value3);
+
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(in);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(3, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), true, value2);
+        assertStartBound(get(bounds, 2), true, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(3, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1);
+        assertEndBound(get(bounds, 1), true, value2);
+        assertEndBound(get(bounds, 2), true, value3);
+    }
+
+    /**
+     * Test slice restriction (e.g 'clustering_0 > 1') with only one clustering column
+     */
+    @Test
+    public void testBoundsAsClusteringWithSliceRestrictionsAndOneClusteringColumn()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+
+        Restriction slice = newSingleSlice(cfMetaData, 0, Bound.START, false, value1);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+        slice = newSingleSlice(cfMetaData, 0, Bound.START, true, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+        slice = newSingleSlice(cfMetaData, 0, Bound.END, true, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1);
+
+        slice = newSingleSlice(cfMetaData, 0, Bound.END, false, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+
+        slice = newSingleSlice(cfMetaData, 0, Bound.START, false, value1);
+        Restriction slice2 = newSingleSlice(cfMetaData, 0, Bound.END, false, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value2);
+
+        slice = newSingleSlice(cfMetaData, 0, Bound.START, true, value1);
+        slice2 = newSingleSlice(cfMetaData, 0, Bound.END, true, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value2);
+    }
+
+    /**
+     * Test slice restriction (e.g 'clustering_0 > 1') with only one descending clustering column
+     */
+    @Test
+    public void testBoundsAsClusteringWithSliceRestrictionsAndOneDescendingClusteringColumn()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.DESC, Sort.DESC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+
+        Restriction slice = newSingleSlice(cfMetaData, 0, Bound.START, false, value1);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+
+        slice = newSingleSlice(cfMetaData, 0, Bound.START, true, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1);
+
+        slice = newSingleSlice(cfMetaData, 0, Bound.END, true, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+        slice = newSingleSlice(cfMetaData, 0, Bound.END, false, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+        slice = newSingleSlice(cfMetaData, 0, Bound.START, false, value1);
+        Restriction slice2 = newSingleSlice(cfMetaData, 0, Bound.END, false, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+
+        slice = newSingleSlice(cfMetaData, 0, Bound.START, true, value1);
+        slice2 = newSingleSlice(cfMetaData, 0, Bound.END, true, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1);
+    }
+
+    /**
+     * Test 'clustering_0 = 1 AND clustering_1 IN (1, 2, 3)'
+     */
+    @Test
+    public void testBoundsAsClusteringWithEqAndInRestrictions()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+        ByteBuffer value3 = ByteBufferUtil.bytes(3);
+        Restriction eq = newSingleEq(cfMetaData, 0, value1);
+        Restriction in = newSingleIN(cfMetaData, 1, value1, value2, value3);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(eq).mergeWith(in);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(3, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value1);
+        assertStartBound(get(bounds, 1), true, value1, value2);
+        assertStartBound(get(bounds, 2), true, value1, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(3, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value1);
+        assertEndBound(get(bounds, 1), true, value1, value2);
+        assertEndBound(get(bounds, 2), true, value1, value3);
+    }
+
+    /**
+     * Test equal and slice restrictions (e.g 'clustering_0 = 0 clustering_1 > 1')
+     */
+    @Test
+    public void testBoundsAsClusteringWithEqAndSliceRestrictions()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+        ByteBuffer value3 = ByteBufferUtil.bytes(3);
+
+        Restriction eq = newSingleEq(cfMetaData, 0, value3);
+
+        Restriction slice = newSingleSlice(cfMetaData, 1, Bound.START, false, value1);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(eq).mergeWith(slice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value3, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value3);
+
+        slice = newSingleSlice(cfMetaData, 1, Bound.START, true, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(eq).mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value3, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value3);
+
+        slice = newSingleSlice(cfMetaData, 1, Bound.END, true, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(eq).mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value3, value1);
+
+        slice = newSingleSlice(cfMetaData, 1, Bound.END, false, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(eq).mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value3, value1);
+
+        slice = newSingleSlice(cfMetaData, 1, Bound.START, false, value1);
+        Restriction slice2 = newSingleSlice(cfMetaData, 1, Bound.END, false, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(eq).mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value3, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value3, value2);
+
+        slice = newSingleSlice(cfMetaData, 1, Bound.START, true, value1);
+        slice2 = newSingleSlice(cfMetaData, 1, Bound.END, true, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(eq).mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value3, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value3, value2);
+    }
+
+    /**
+     * Test '(clustering_0, clustering_1) = (1, 2)' with two clustering column
+     */
+    @Test
+    public void testBoundsAsClusteringWithMultiEqRestrictions()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+        Restriction eq = newMultiEq(cfMetaData, 0, value1, value2);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(eq);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2);
+    }
+
+    /**
+     * Test '(clustering_0, clustering_1) IN ((1, 2), (2, 3))' with two clustering column
+     */
+    @Test
+    public void testBoundsAsClusteringWithMultiInRestrictions()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+        ByteBuffer value3 = ByteBufferUtil.bytes(3);
+        Restriction in = newMultiIN(cfMetaData, 0, asList(value1, value2), asList(value2, value3));
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(in);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2);
+        assertStartBound(get(bounds, 1), true, value2, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2);
+        assertEndBound(get(bounds, 1), true, value2, value3);
+    }
+
+    /**
+     * Test multi-column slice restrictions (e.g '(clustering_0) > (1)') with only one clustering column
+     */
+    @Test
+    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithOneClusteringColumn()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC);
+
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+
+        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1);
+
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1);
+        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value2);
+
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1);
+        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value2);
+    }
+
+    /**
+     * Test multi-column slice restrictions (e.g '(clustering_0) > (1)') with only one clustering column in reverse
+     * order
+     */
+    @Test
+    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithOneDescendingClusteringColumn()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.DESC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+
+        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1);
+
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1);
+        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1);
+        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1);
+    }
+
+    /**
+     * Test multi-column slice restrictions (e.g '(clustering_0, clustering_1) > (1, 2)')
+     */
+    @Test
+    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithTwoClusteringColumn()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+
+        // (clustering_0, clustering1) > (1, 2)
+        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+        // (clustering_0, clustering1) >= (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+        // (clustering_0, clustering1) <= (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2);
+
+        // (clustering_0, clustering1) < (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+
+        // (clustering_0, clustering1) > (1, 2) AND (clustering_0) < (2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
+        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value2);
+
+        // (clustering_0, clustering1) >= (1, 2) AND (clustering_0, clustering1) <= (2, 1)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
+        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value2, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value2, value1);
+    }
+
+    /**
+     * Test multi-column slice restrictions with 2 descending clustering columns (e.g '(clustering_0, clustering_1) > (1, 2)')
+     */
+    @Test
+    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithTwoDescendingClusteringColumns()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.DESC, Sort.DESC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+
+        // (clustering_0, clustering1) > (1, 2)
+        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+
+        // (clustering_0, clustering1) >= (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2);
+
+        // (clustering_0, clustering1) <= (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+        // (clustering_0, clustering1) < (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+
+        // (clustering_0, clustering1) > (1, 2) AND (clustering_0) < (2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
+        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+
+        // (clustering_0, clustering1) >= (1, 2) AND (clustering_0, clustering1) <= (2, 1)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
+        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value2, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value2, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2);
+    }
+
+    /**
+     * Test multi-column slice restrictions with 1 descending clustering column and 1 ascending
+     * (e.g '(clustering_0, clustering_1) > (1, 2)')
+     */
+    @Test
+    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithOneDescendingAndOneAscendingClusteringColumns()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.DESC, Sort.ASC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+
+        // (clustering_0, clustering1) > (1, 2)
+        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+        assertStartBound(get(bounds, 1), false, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+        assertEndBound(get(bounds, 1), true, value1);
+
+        // (clustering_0, clustering1) >= (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+        assertStartBound(get(bounds, 1), true, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+        assertEndBound(get(bounds, 1), true, value1);
+
+        // (clustering_0, clustering1) <= (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2);
+        assertEmptyEnd(get(bounds, 1));
+
+        // (clustering_0, clustering1) < (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+        assertEmptyEnd(get(bounds, 1));
+
+        // (clustering_0, clustering1) > (1, 2) AND (clustering_0) < (2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
+        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), false, value2);
+        assertStartBound(get(bounds, 1), false, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+        assertEndBound(get(bounds, 1), true, value1);
+
+        // (clustering_0) > (1) AND (clustering_0, clustering1) < (2, 1)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1);
+        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value2);
+        assertStartBound(get(bounds, 1), false, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value2, value1);
+        assertEndBound(get(bounds, 1), false, value1);
+
+        // (clustering_0, clustering1) >= (1, 2) AND (clustering_0, clustering1) <= (2, 1)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
+        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value2, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(3, bounds.size());
+        assertStartBound(get(bounds, 0), true, value2);
+        assertStartBound(get(bounds, 1), false, value2);
+        assertStartBound(get(bounds, 2), true, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(3, bounds.size());
+        assertEndBound(get(bounds, 0), true, value2, value1);
+        assertEndBound(get(bounds, 1), false, value1);
+        assertEndBound(get(bounds, 2), true, value1);
+    }
+
+    /**
+     * Test multi-column slice restrictions with 1 descending clustering column and 1 ascending
+     * (e.g '(clustering_0, clustering_1) > (1, 2)')
+     */
+    @Test
+    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithOneAscendingAndOneDescendingClusteringColumns()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.DESC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+
+        // (clustering_0, clustering1) > (1, 2)
+        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+        assertEmptyEnd(get(bounds, 1));
+
+        // (clustering_0, clustering1) >= (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2);
+        assertEmptyEnd(get(bounds, 1));
+
+        // (clustering_0, clustering1) <= (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+        assertStartBound(get(bounds, 1), true, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+        assertEndBound(get(bounds, 1), true, value1);
+
+        // (clustering_0, clustering1) < (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+        assertStartBound(get(bounds, 1), false, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+        assertEndBound(get(bounds, 1), true, value1);
+
+        // (clustering_0, clustering1) > (1, 2) AND (clustering_0) < (2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
+        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+        assertEndBound(get(bounds, 1), false, value2);
+
+        // (clustering_0, clustering1) >= (1, 2) AND (clustering_0, clustering1) <= (2, 1)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
+        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value2, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(3, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), false, value1);
+        assertStartBound(get(bounds, 2), true, value2, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(3, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2);
+        assertEndBound(get(bounds, 1), false, value2);
+        assertEndBound(get(bounds, 2), true, value2);
+    }
+
+    /**
+     * Test multi-column slice restrictions with 2 ascending clustering column and 2 descending
+     * (e.g '(clustering_0, clustering1, clustering_3, clustering4) > (1, 2, 3, 4)')
+     */
+    @Test
+    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithTwoAscendingAndTwoDescendingClusteringColumns()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC, Sort.DESC, Sort.DESC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+        ByteBuffer value3 = ByteBufferUtil.bytes(3);
+        ByteBuffer value4 = ByteBufferUtil.bytes(4);
+
+        // (clustering_0, clustering1, clustering_2, clustering_3) > (1, 2, 3, 4)
+        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2, value3, value4);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2);
+        assertStartBound(get(bounds, 1), false, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2, value3, value4);
+        assertEmptyEnd(get(bounds, 1));
+
+        // clustering_0 = 1 AND (clustering_1, clustering_2, clustering_3) > (2, 3, 4)
+        Restriction eq = newSingleEq(cfMetaData, 0, value1);
+        slice = newMultiSlice(cfMetaData, 1, Bound.START, false, value2, value3, value4);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions.mergeWith(eq);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2);
+        assertStartBound(get(bounds, 1), false, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2, value3, value4);
+        assertEndBound(get(bounds, 1), true, value1);
+
+        // clustering_0 IN (1, 2) AND (clustering_1, clustering_2, clustering_3) > (2, 3, 4)
+        Restriction in = newSingleIN(cfMetaData, 0, value1, value2);
+        slice = newMultiSlice(cfMetaData, 1, Bound.START, false, value2, value3, value4);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions.mergeWith(in);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(4, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2);
+        assertStartBound(get(bounds, 1), false, value1, value2);
+        assertStartBound(get(bounds, 2), true, value2, value2);
+        assertStartBound(get(bounds, 3), false, value2, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(4, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2, value3, value4);
+        assertEndBound(get(bounds, 1), true, value1);
+        assertEndBound(get(bounds, 2), false, value2, value2, value3, value4);
+        assertEndBound(get(bounds, 3), true, value2);
+
+        // (clustering_0, clustering1) >= (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEmptyEnd(get(bounds, 0));
+
+        // (clustering_0, clustering1, clustering_2, clustering_3) >= (1, 2, 3, 4)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2, value3, value4);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2);
+        assertStartBound(get(bounds, 1), false, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2, value3, value4);
+        assertEmptyEnd(get(bounds, 1));
+
+        // (clustering_0, clustering1, clustering_2, clustering_3) <= (1, 2, 3, 4)
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1, value2, value3, value4);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+        assertStartBound(get(bounds, 1), true, value1, value2, value3, value4);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+        assertEndBound(get(bounds, 1), true, value1, value2);
+
+        // (clustering_0, clustering1, clustering_2, clustering_3) < (1, 2, 3, 4)
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1, value2, value3, value4);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+        assertStartBound(get(bounds, 1), false, value1, value2, value3, value4);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+        assertEndBound(get(bounds, 1), true, value1, value2);
+
+        // (clustering_0, clustering1, clustering_2, clustering_3) > (1, 2, 3, 4) AND (clustering_0, clustering_1) < (2, 3)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2, value3, value4);
+        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2, value3);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2);
+        assertStartBound(get(bounds, 1), false, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2, value3, value4);
+        assertEndBound(get(bounds, 1), false, value2, value3);
+
+        // (clustering_0, clustering1, clustering_2, clustering_3) >= (1, 2, 3, 4) AND (clustering_0, clustering1, clustering_2, clustering_3) <= (4, 3, 2, 1)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2, value3, value4);
+        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value4, value3, value2, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(3, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2);
+        assertStartBound(get(bounds, 1), false, value1, value2);
+        assertStartBound(get(bounds, 2), true, value4, value3, value2, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(3, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2, value3, value4);
+        assertEndBound(get(bounds, 1), false, value4, value3);
+        assertEndBound(get(bounds, 2), true, value4, value3);
+    }
+
+    /**
+     * Test multi-column slice restrictions with ascending, descending, ascending and descending columns
+     * (e.g '(clustering_0, clustering1, clustering_3, clustering4) > (1, 2, 3, 4)')
+     */
+    @Test
+    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithAscendingDescendingColumnMix()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.DESC, Sort.ASC, Sort.DESC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+        ByteBuffer value3 = ByteBufferUtil.bytes(3);
+        ByteBuffer value4 = ByteBufferUtil.bytes(4);
+
+        // (clustering_0, clustering1, clustering_2, clustering_3) > (1, 2, 3, 4)
+        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2, value3, value4);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(4, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), true, value1, value2, value3);
+        assertStartBound(get(bounds, 2), false, value1, value2, value3);
+        assertStartBound(get(bounds, 3), false, value1);
+
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(4, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+        assertEndBound(get(bounds, 1), false, value1, value2, value3, value4);
+        assertEndBound(get(bounds, 2), true, value1, value2);
+        assertEmptyEnd(get(bounds, 3));
+
+        // clustering_0 = 1 AND (clustering_1, clustering_2, clustering_3) > (2, 3, 4)
+        Restriction eq = newSingleEq(cfMetaData, 0, value1);
+        slice = newMultiSlice(cfMetaData, 1, Bound.START, false, value2, value3, value4);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions.mergeWith(eq);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(3, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), true, value1, value2, value3);
+        assertStartBound(get(bounds, 2), false, value1, value2, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(3, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+        assertEndBound(get(bounds, 1), false, value1, value2, value3, value4);
+        assertEndBound(get(bounds, 2), true, value1, value2);
+
+        // (clustering_0, clustering1) >= (1, 2)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2);
+        assertEmptyEnd(get(bounds, 1));
+
+        // (clustering_0, clustering1, clustering_2, clustering_3) >= (1, 2, 3, 4)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2, value3, value4);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(4, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), true, value1, value2, value3);
+        assertStartBound(get(bounds, 2), false, value1, value2, value3);
+        assertStartBound(get(bounds, 3), false, value1);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(4, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+        assertEndBound(get(bounds, 1), true, value1, value2, value3, value4);
+        assertEndBound(get(bounds, 2), true, value1, value2);
+        assertEmptyEnd(get(bounds, 3));
+
+        // (clustering_0, clustering1, clustering_2, clustering_3) <= (1, 2, 3, 4)
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1, value2, value3, value4);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(4, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+        assertStartBound(get(bounds, 1), true, value1, value2);
+        assertStartBound(get(bounds, 2), true, value1, value2, value3, value4);
+        assertStartBound(get(bounds, 3), false, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(4, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+        assertEndBound(get(bounds, 1), false, value1, value2, value3);
+        assertEndBound(get(bounds, 2), true, value1, value2, value3);
+        assertEndBound(get(bounds, 3), true, value1);
+
+        // (clustering_0, clustering1, clustering_2, clustering_3) < (1, 2, 3, 4)
+        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1, value2, value3, value4);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(4, bounds.size());
+        assertEmptyStart(get(bounds, 0));
+        assertStartBound(get(bounds, 1), true, value1, value2);
+        assertStartBound(get(bounds, 2), false, value1, value2, value3, value4);
+        assertStartBound(get(bounds, 3), false, value1, value2);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(4, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1);
+        assertEndBound(get(bounds, 1), false, value1, value2, value3);
+        assertEndBound(get(bounds, 2), true, value1, value2, value3);
+        assertEndBound(get(bounds, 3), true, value1);
+
+        // (clustering_0, clustering1, clustering_2, clustering_3) > (1, 2, 3, 4) AND (clustering_0, clustering_1) < (2, 3)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2, value3, value4);
+        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2, value3);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(5, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), true, value1, value2, value3);
+        assertStartBound(get(bounds, 2), false, value1, value2, value3);
+        assertStartBound(get(bounds, 3), false, value1);
+        assertStartBound(get(bounds, 4), false, value2, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(5, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+        assertEndBound(get(bounds, 1), false, value1, value2, value3, value4);
+        assertEndBound(get(bounds, 2), true, value1, value2);
+        assertEndBound(get(bounds, 3), false, value2);
+        assertEndBound(get(bounds, 4), true, value2);
+
+        // (clustering_0, clustering1, clustering_2, clustering_3) >= (1, 2, 3, 4) AND (clustering_0, clustering1, clustering_2, clustering_3) <= (4, 3, 2, 1)
+        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2, value3, value4);
+        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value4, value3, value2, value1);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(7, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1);
+        assertStartBound(get(bounds, 1), true, value1, value2, value3);
+        assertStartBound(get(bounds, 2), false, value1, value2, value3);
+        assertStartBound(get(bounds, 3), false, value1);
+        assertStartBound(get(bounds, 4), true, value4, value3);
+        assertStartBound(get(bounds, 5), true, value4, value3, value2, value1);
+        assertStartBound(get(bounds, 6), false, value4, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(7, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value2);
+        assertEndBound(get(bounds, 1), true, value1, value2, value3, value4);
+        assertEndBound(get(bounds, 2), true, value1, value2);
+        assertEndBound(get(bounds, 3), false, value4);
+        assertEndBound(get(bounds, 4), false, value4, value3, value2);
+        assertEndBound(get(bounds, 5), true, value4, value3, value2);
+        assertEndBound(get(bounds, 6), true, value4);
+    }
+
+    /**
+     * Test mixing single and multi equals restrictions (e.g. clustering_0 = 1 AND (clustering_1, clustering_2) = (2, 3))
+     */
+    @Test
+    public void testBoundsAsClusteringWithSingleEqAndMultiEqRestrictions()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC, Sort.ASC, Sort.ASC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+        ByteBuffer value3 = ByteBufferUtil.bytes(3);
+        ByteBuffer value4 = ByteBufferUtil.bytes(4);
+
+        // clustering_0 = 1 AND (clustering_1, clustering_2) = (2, 3)
+        Restriction singleEq = newSingleEq(cfMetaData, 0, value1);
+        Restriction multiEq = newMultiEq(cfMetaData, 1, value2, value3);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiEq);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2, value3);
+
+        // clustering_0 = 1 AND clustering_1 = 2 AND (clustering_2, clustering_3) = (3, 4)
+        singleEq = newSingleEq(cfMetaData, 0, value1);
+        Restriction singleEq2 = newSingleEq(cfMetaData, 1, value2);
+        multiEq = newMultiEq(cfMetaData, 2, value3, value4);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(singleEq).mergeWith(singleEq2).mergeWith(multiEq);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2, value3, value4);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2, value3, value4);
+
+        // (clustering_0, clustering_1) = (1, 2) AND clustering_2 = 3
+        singleEq = newSingleEq(cfMetaData, 2, value3);
+        multiEq = newMultiEq(cfMetaData, 0, value1, value2);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiEq);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2, value3);
+
+        // clustering_0 = 1 AND (clustering_1, clustering_2) = (2, 3) AND clustering_3 = 4
+        singleEq = newSingleEq(cfMetaData, 0, value1);
+        singleEq2 = newSingleEq(cfMetaData, 3, value4);
+        multiEq = newMultiEq(cfMetaData, 1, value2, value3);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiEq).mergeWith(singleEq2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2, value3, value4);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2, value3, value4);
+    }
+
+    /**
+     * Test clustering_0 = 1 AND (clustering_1, clustering_2) IN ((2, 3), (4, 5))
+     */
+    @Test
+    public void testBoundsAsClusteringWithSingleEqAndMultiINRestrictions()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC, Sort.ASC, Sort.ASC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+        ByteBuffer value3 = ByteBufferUtil.bytes(3);
+        ByteBuffer value4 = ByteBufferUtil.bytes(4);
+        ByteBuffer value5 = ByteBufferUtil.bytes(5);
+
+        // clustering_0 = 1 AND (clustering_1, clustering_2) IN ((2, 3), (4, 5))
+        Restriction singleEq = newSingleEq(cfMetaData, 0, value1);
+        Restriction multiIN = newMultiIN(cfMetaData, 1, asList(value2, value3), asList(value4, value5));
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiIN);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2, value3);
+        assertStartBound(get(bounds, 1), true, value1, value4, value5);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2, value3);
+        assertEndBound(get(bounds, 1), true, value1, value4, value5);
+
+        // clustering_0 = 1 AND (clustering_1, clustering_2) IN ((2, 3))
+        singleEq = newSingleEq(cfMetaData, 0, value1);
+        multiIN = newMultiIN(cfMetaData, 1, asList(value2, value3));
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(multiIN).mergeWith(singleEq);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2, value3);
+
+        // clustering_0 = 1 AND clustering_1 = 5 AND (clustering_2, clustering_3) IN ((2, 3), (4, 5))
+        singleEq = newSingleEq(cfMetaData, 0, value1);
+        Restriction singleEq2 = newSingleEq(cfMetaData, 1, value5);
+        multiIN = newMultiIN(cfMetaData, 2, asList(value2, value3), asList(value4, value5));
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiIN).mergeWith(singleEq2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value5, value2, value3);
+        assertStartBound(get(bounds, 1), true, value1, value5, value4, value5);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value5, value2, value3);
+        assertEndBound(get(bounds, 1), true, value1, value5, value4, value5);
+    }
+
+    /**
+     * Test mixing single equal restrictions with multi-column slice restrictions
+     * (e.g. clustering_0 = 1 AND (clustering_1, clustering_2) > (2, 3))
+     */
+    @Test
+    public void testBoundsAsClusteringWithSingleEqAndSliceRestrictions()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC, Sort.ASC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+        ByteBuffer value3 = ByteBufferUtil.bytes(3);
+        ByteBuffer value4 = ByteBufferUtil.bytes(4);
+        ByteBuffer value5 = ByteBufferUtil.bytes(5);
+
+        // clustering_0 = 1 AND (clustering_1, clustering_2) > (2, 3)
+        Restriction singleEq = newSingleEq(cfMetaData, 0, value1);
+        Restriction multiSlice = newMultiSlice(cfMetaData, 1, Bound.START, false, value2, value3);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiSlice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1, value2, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1);
+
+        // clustering_0 = 1 AND (clustering_1, clustering_2) > (2, 3) AND (clustering_1) < (4)
+        singleEq = newSingleEq(cfMetaData, 0, value1);
+        multiSlice = newMultiSlice(cfMetaData, 1, Bound.START, false, value2, value3);
+        Restriction multiSlice2 = newMultiSlice(cfMetaData, 1, Bound.END, false, value4);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(multiSlice2).mergeWith(singleEq).mergeWith(multiSlice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1, value2, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), false, value1, value4);
+
+        // clustering_0 = 1 AND (clustering_1, clustering_2) => (2, 3) AND (clustering_1, clustering_2) <= (4, 5)
+        singleEq = newSingleEq(cfMetaData, 0, value1);
+        multiSlice = newMultiSlice(cfMetaData, 1, Bound.START, true, value2, value3);
+        multiSlice2 = newMultiSlice(cfMetaData, 1, Bound.END, true, value4, value5);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(multiSlice2).mergeWith(singleEq).mergeWith(multiSlice);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value4, value5);
+    }
+
+    /**
+     * Test mixing multi equal restrictions with single-column slice restrictions
+     * (e.g. clustering_0 = 1 AND (clustering_1, clustering_2) > (2, 3))
+     */
+    @Test
+    public void testBoundsAsClusteringWithMultiEqAndSingleSliceRestrictions()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC, Sort.ASC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+        ByteBuffer value3 = ByteBufferUtil.bytes(3);
+
+        // (clustering_0, clustering_1) = (1, 2) AND clustering_2 > 3
+        Restriction multiEq = newMultiEq(cfMetaData, 0, value1, value2);
+        Restriction singleSlice = newSingleSlice(cfMetaData, 2, Bound.START, false, value3);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(multiEq).mergeWith(singleSlice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1, value2, value3);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2);
+    }
+
+    @Test
+    public void testBoundsAsClusteringWithSeveralMultiColumnRestrictions()
+    {
+        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC, Sort.ASC, Sort.ASC);
+
+        ByteBuffer value1 = ByteBufferUtil.bytes(1);
+        ByteBuffer value2 = ByteBufferUtil.bytes(2);
+        ByteBuffer value3 = ByteBufferUtil.bytes(3);
+        ByteBuffer value4 = ByteBufferUtil.bytes(4);
+        ByteBuffer value5 = ByteBufferUtil.bytes(5);
+
+        // (clustering_0, clustering_1) = (1, 2) AND (clustering_2, clustering_3) > (3, 4)
+        Restriction multiEq = newMultiEq(cfMetaData, 0, value1, value2);
+        Restriction multiSlice = newMultiSlice(cfMetaData, 2, Bound.START, false, value3, value4);
+        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(multiEq).mergeWith(multiSlice);
+
+        SortedSet<ClusteringBound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), false, value1, value2, value3, value4);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2);
+
+        // (clustering_0, clustering_1) = (1, 2) AND (clustering_2, clustering_3) IN ((3, 4), (4, 5))
+        multiEq = newMultiEq(cfMetaData, 0, value1, value2);
+        Restriction multiIN = newMultiIN(cfMetaData, 2, asList(value3, value4), asList(value4, value5));
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(multiEq).mergeWith(multiIN);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2, value3, value4);
+        assertStartBound(get(bounds, 1), true, value1, value2, value4, value5);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(2, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2, value3, value4);
+        assertEndBound(get(bounds, 1), true, value1, value2, value4, value5);
+
+        // (clustering_0, clustering_1) = (1, 2) AND (clustering_2, clustering_3) = (3, 4)
+        multiEq = newMultiEq(cfMetaData, 0, value1, value2);
+        Restriction multiEq2 = newMultiEq(cfMetaData, 2, value3, value4);
+        restrictions = new ClusteringColumnRestrictions(cfMetaData);
+        restrictions = restrictions.mergeWith(multiEq).mergeWith(multiEq2);
+
+        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertStartBound(get(bounds, 0), true, value1, value2, value3, value4);
+
+        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
+        assertEquals(1, bounds.size());
+        assertEndBound(get(bounds, 0), true, value1, value2, value3, value4);
+    }
+
+    /**
+     * Asserts that the specified <code>Bound</code> is an empty start.
+     *
+     * @param bound the bound to check
+     */
+    private static void assertEmptyStart(ClusteringBound bound)
+    {
+        assertEquals(ClusteringBound.BOTTOM, bound);
+    }
+
+    /**
+     * Asserts that the specified <code>Bound</code> is an empty end.
+     *
+     * @param bound the bound to check
+     */
+    private static void assertEmptyEnd(ClusteringBound bound)
+    {
+        assertEquals(ClusteringBound.TOP, bound);
+    }
+
+    /**
+     * Asserts that the specified <code>ClusteringBound</code> is a start with the specified elements.
+     *
+     * @param bound the bound to check
+     * @param isInclusive if the bound is expected to be inclusive
+     * @param elements the expected elements of the clustering
+     */
+    private static void assertStartBound(ClusteringBound bound, boolean isInclusive, ByteBuffer... elements)
+    {
+        assertBound(bound, true, isInclusive, elements);
+    }
+
+    /**
+     * Asserts that the specified <code>ClusteringBound</code> is a end with the specified elements.
+     *
+     * @param bound the bound to check
+     * @param isInclusive if the bound is expected to be inclusive
+     * @param elements the expected elements of the clustering
+     */
+    private static void assertEndBound(ClusteringBound bound, boolean isInclusive, ByteBuffer... elements)
+    {
+        assertBound(bound, false, isInclusive, elements);
+    }
+
+    private static void assertBound(ClusteringBound bound, boolean isStart, boolean isInclusive, ByteBuffer... elements)
+    {
+        assertEquals("the bound size is not the expected one:", elements.length, bound.size());
+        assertEquals("the bound should be a " + (isStart ? "start" : "end") + " but is a " + (bound.isStart() ? "start" : "end"), isStart, bound.isStart());
+        assertEquals("the bound inclusiveness is not the expected one", isInclusive, bound.isInclusive());
+        for (int i = 0, m = elements.length; i < m; i++)
+        {
+            ByteBuffer element = elements[i];
+            assertTrue(String.format("the element %s of the bound is not the expected one: expected %s but was %s",
+                                     i,
+                                     ByteBufferUtil.toInt(element),
+                                     ByteBufferUtil.toInt(bound.get(i))),
+                       element.equals(bound.get(i)));
+        }
+    }
+
+    /**
+     * Creates a new <code>CFMetaData</code> instance.
+     *
+     * @param numberOfClusteringColumns the number of clustering column
+     * @return a new <code>CFMetaData</code> instance
+     */
+    private static CFMetaData newCFMetaData(Sort... sorts)
+    {
+        List<AbstractType<?>> types = new ArrayList<>();
+
+        for (Sort sort : sorts)
+            types.add(sort == Sort.ASC ? Int32Type.instance : ReversedType.getInstance(Int32Type.instance));
+
+        CFMetaData.Builder builder = CFMetaData.Builder.create("keyspace", "test")
+                                                       .addPartitionKey("partition_key", Int32Type.instance);
+
+        for (int i = 0; i < sorts.length; i++)
+            builder.addClusteringColumn("clustering_" + i, types.get(i));
+
+        return builder.build();
+    }
+
+    /**
+     * Creates a new <code>SingleColumnRestriction.EQ</code> instance for the specified clustering column.
+     *
+     * @param cfMetaData the column family meta data
+     * @param index the clustering column index
+     * @param value the equality value
+     * @return a new <code>SingleColumnRestriction.EQ</code> instance for the specified clustering column
+     */
+    private static Restriction newSingleEq(CFMetaData cfMetaData, int index, ByteBuffer value)
+    {
+        ColumnDefinition columnDef = getClusteringColumnDefinition(cfMetaData, index);
+        return new SingleColumnRestriction.EQRestriction(columnDef, toTerm(value));
+    }
+
+    /**
+     * Creates a new <code>MultiColumnRestriction.EQ</code> instance for the specified clustering column.
+     *
+     * @param cfMetaData the column family meta data
+     * @param index the clustering column index
+     * @param value the equality value
+     * @return a new <code>MultiColumnRestriction.EQ</code> instance for the specified clustering column
+     */
+    private static Restriction newMultiEq(CFMetaData cfMetaData, int firstIndex, ByteBuffer... values)
+    {
+        List<ColumnDefinition> columnDefinitions = new ArrayList<>();
+        for (int i = 0; i < values.length; i++)
+        {
+            columnDefinitions.add(getClusteringColumnDefinition(cfMetaData, firstIndex + i));
+        }
+        return new MultiColumnRestriction.EQRestriction(columnDefinitions, toMultiItemTerminal(values));
+    }
+
+    /**
+     * Creates a new <code>MultiColumnRestriction.IN</code> instance for the specified clustering column.
+     *
+     * @param cfMetaData the column family meta data
+     * @param firstIndex the index of the first clustering column
+     * @param values the in values
+     * @return a new <code>MultiColumnRestriction.IN</code> instance for the specified clustering column
+     */
+    @SafeVarargs
+    private static Restriction newMultiIN(CFMetaData cfMetaData, int firstIndex, List<ByteBuffer>... values)
+    {
+        List<ColumnDefinition> columnDefinitions = new ArrayList<>();
+        List<Term> terms = new ArrayList<>();
+        for (int i = 0; i < values.length; i++)
+        {
+            columnDefinitions.add(getClusteringColumnDefinition(cfMetaData, firstIndex + i));
+            terms.add(toMultiItemTerminal(values[i].toArray(new ByteBuffer[0])));
+        }
+        return new MultiColumnRestriction.InRestrictionWithValues(columnDefinitions, terms);
+    }
+
+    /**
+     * Creates a new <code>SingleColumnRestriction.IN</code> instance for the specified clustering column.
+     *
+     * @param cfMetaData the column family meta data
+     * @param index the clustering column index
+     * @param values the in values
+     * @return a new <code>SingleColumnRestriction.IN</code> instance for the specified clustering column
+     */
+    private static Restriction newSingleIN(CFMetaData cfMetaData, int index, ByteBuffer... values)
+    {
+        ColumnDefinition columnDef = getClusteringColumnDefinition(cfMetaData, index);
+        return new SingleColumnRestriction.InRestrictionWithValues(columnDef, toTerms(values));
+    }
+
+    /**
+     * Returns the clustering <code>ColumnDefinition</code> for the specified position.
+     *
+     * @param cfMetaData the column family meta data
+     * @param index the clustering column index
+     * @return the clustering <code>ColumnDefinition</code> for the specified position.
+     */
+    private static ColumnDefinition getClusteringColumnDefinition(CFMetaData cfMetaData, int index)
+    {
+        return cfMetaData.clusteringColumns().get(index);
+    }
+
+    /**
+     * Creates a new <code>SingleColumnRestriction.Slice</code> instance for the specified clustering column.
+     *
+     * @param cfMetaData the column family meta data
+     * @param index the clustering column index
+     * @param bound the slice bound
+     * @param inclusive <code>true</code> if the bound is inclusive
+     * @param value the bound value
+     * @return a new <code>SingleColumnRestriction.Slice</code> instance for the specified clustering column
+     */
+    private static Restriction newSingleSlice(CFMetaData cfMetaData, int index, Bound bound, boolean inclusive, ByteBuffer value)
+    {
+        ColumnDefinition columnDef = getClusteringColumnDefinition(cfMetaData, index);
+        return new SingleColumnRestriction.SliceRestriction(columnDef, bound, inclusive, toTerm(value));
+    }
+
+    /**
+     * Creates a new <code>SingleColumnRestriction.Slice</code> instance for the specified clustering column.
+     *
+     * @param cfMetaData the column family meta data
+     * @param index the clustering column index
+     * @param bound the slice bound
+     * @param inclusive <code>true</code> if the bound is inclusive
+     * @param value the bound value
+     * @return a new <code>SingleColumnRestriction.Slice</code> instance for the specified clustering column
+     */
+    private static Restriction newMultiSlice(CFMetaData cfMetaData, int firstIndex, Bound bound, boolean inclusive, ByteBuffer... values)
+    {
+        List<ColumnDefinition> columnDefinitions = new ArrayList<>();
+        for (int i = 0; i < values.length; i++)
+        {
+            columnDefinitions.add(getClusteringColumnDefinition(cfMetaData, i + firstIndex));
+        }
+        return new MultiColumnRestriction.SliceRestriction(columnDefinitions, bound, inclusive, toMultiItemTerminal(values));
+    }
+
+    /**
+     * Converts the specified values into a <code>MultiItemTerminal</code>.
+     *
+     * @param values the values to convert.
+     * @return the term corresponding to the specified values.
+     */
+    private static MultiItemTerminal toMultiItemTerminal(ByteBuffer... values)
+    {
+        return new Tuples.Value(values);
+    }
+
+    /**
+     * Converts the specified value into a term.
+     *
+     * @param value the value to convert.
+     * @return the term corresponding to the specified value.
+     */
+    private static Term toTerm(ByteBuffer value)
+    {
+        return new Constants.Value(value);
+    }
+
+    /**
+     * Converts the specified values into a <code>List</code> of terms.
+     *
+     * @param values the values to convert.
+     * @return a <code>List</code> of terms corresponding to the specified values.
+     */
+    private static List<Term> toTerms(ByteBuffer... values)
+    {
+        List<Term> terms = new ArrayList<>();
+        for (ByteBuffer value : values)
+            terms.add(toTerm(value));
+        return terms;
+    }
+
+    private static <T> T get(SortedSet<T> set, int i)
+    {
+        return Iterables.get(set, i);
+    }
+
+    private static enum Sort
+    {
+        ASC,
+        DESC;
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictionSetTest.java b/test/unit/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictionSetTest.java
deleted file mode 100644
index abbd36b..0000000
--- a/test/unit/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictionSetTest.java
+++ /dev/null
@@ -1,1919 +0,0 @@
-/*
- * 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.restrictions;
-
-import java.nio.ByteBuffer;
-import java.util.*;
-
-import com.google.common.collect.Iterables;
-import org.junit.Test;
-
-import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.cql3.*;
-import org.apache.cassandra.cql3.Term.MultiItemTerminal;
-import org.apache.cassandra.cql3.statements.Bound;
-
-import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.db.marshal.Int32Type;
-import org.apache.cassandra.db.marshal.ReversedType;
-import org.apache.cassandra.utils.ByteBufferUtil;
-
-import static java.util.Arrays.asList;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-public class PrimaryKeyRestrictionSetTest
-{
-    @Test
-    public void testBoundsAsClusteringWithNoRestrictions()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC);
-
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-    }
-
-    /**
-     * Test 'clustering_0 = 1' with only one clustering column
-     */
-    @Test
-    public void testBoundsAsClusteringWithOneEqRestrictionsAndOneClusteringColumn()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC);
-
-        ByteBuffer clustering_0 = ByteBufferUtil.bytes(1);
-        Restriction eq = newSingleEq(cfMetaData, 0, clustering_0);
-
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(eq);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, clustering_0);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, clustering_0);
-    }
-
-    /**
-     * Test 'clustering_1 = 1' with 2 clustering columns
-     */
-    @Test
-    public void testBoundsAsClusteringWithOneEqRestrictionsAndTwoClusteringColumns()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
-
-        ByteBuffer clustering_0 = ByteBufferUtil.bytes(1);
-        Restriction eq = newSingleEq(cfMetaData, 0, clustering_0);
-
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(eq);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, clustering_0);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, clustering_0);
-    }
-
-    /**
-     * Test 'clustering_0 IN (1, 2, 3)' with only one clustering column
-     */
-    @Test
-    public void testBoundsAsClusteringWithOneInRestrictionsAndOneClusteringColumn()
-    {
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-        ByteBuffer value3 = ByteBufferUtil.bytes(3);
-
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
-
-        Restriction in = newSingleIN(cfMetaData, 0, value1, value2, value3);
-
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(in);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(3, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), true, value2);
-        assertStartBound(get(bounds, 2), true, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(3, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1);
-        assertEndBound(get(bounds, 1), true, value2);
-        assertEndBound(get(bounds, 2), true, value3);
-    }
-
-    /**
-     * Test slice restriction (e.g 'clustering_0 > 1') with only one clustering column
-     */
-    @Test
-    public void testBoundsAsClusteringWithSliceRestrictionsAndOneClusteringColumn()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-
-        Restriction slice = newSingleSlice(cfMetaData, 0, Bound.START, false, value1);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-        slice = newSingleSlice(cfMetaData, 0, Bound.START, true, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-        slice = newSingleSlice(cfMetaData, 0, Bound.END, true, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1);
-
-        slice = newSingleSlice(cfMetaData, 0, Bound.END, false, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-
-        slice = newSingleSlice(cfMetaData, 0, Bound.START, false, value1);
-        Restriction slice2 = newSingleSlice(cfMetaData, 0, Bound.END, false, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value2);
-
-        slice = newSingleSlice(cfMetaData, 0, Bound.START, true, value1);
-        slice2 = newSingleSlice(cfMetaData, 0, Bound.END, true, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value2);
-    }
-
-    /**
-     * Test slice restriction (e.g 'clustering_0 > 1') with only one descending clustering column
-     */
-    @Test
-    public void testBoundsAsClusteringWithSliceRestrictionsAndOneDescendingClusteringColumn()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.DESC, Sort.DESC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-
-        Restriction slice = newSingleSlice(cfMetaData, 0, Bound.START, false, value1);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-
-        slice = newSingleSlice(cfMetaData, 0, Bound.START, true, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1);
-
-        slice = newSingleSlice(cfMetaData, 0, Bound.END, true, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-        slice = newSingleSlice(cfMetaData, 0, Bound.END, false, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-        slice = newSingleSlice(cfMetaData, 0, Bound.START, false, value1);
-        Restriction slice2 = newSingleSlice(cfMetaData, 0, Bound.END, false, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-
-        slice = newSingleSlice(cfMetaData, 0, Bound.START, true, value1);
-        slice2 = newSingleSlice(cfMetaData, 0, Bound.END, true, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1);
-    }
-
-    /**
-     * Test 'clustering_0 = 1 AND clustering_1 IN (1, 2, 3)'
-     */
-    @Test
-    public void testBoundsAsClusteringWithEqAndInRestrictions()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-        ByteBuffer value3 = ByteBufferUtil.bytes(3);
-        Restriction eq = newSingleEq(cfMetaData, 0, value1);
-        Restriction in = newSingleIN(cfMetaData, 1, value1, value2, value3);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(eq).mergeWith(in);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(3, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value1);
-        assertStartBound(get(bounds, 1), true, value1, value2);
-        assertStartBound(get(bounds, 2), true, value1, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(3, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value1);
-        assertEndBound(get(bounds, 1), true, value1, value2);
-        assertEndBound(get(bounds, 2), true, value1, value3);
-    }
-
-    /**
-     * Test equal and slice restrictions (e.g 'clustering_0 = 0 clustering_1 > 1')
-     */
-    @Test
-    public void testBoundsAsClusteringWithEqAndSliceRestrictions()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-        ByteBuffer value3 = ByteBufferUtil.bytes(3);
-
-        Restriction eq = newSingleEq(cfMetaData, 0, value3);
-
-        Restriction slice = newSingleSlice(cfMetaData, 1, Bound.START, false, value1);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(eq).mergeWith(slice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value3, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value3);
-
-        slice = newSingleSlice(cfMetaData, 1, Bound.START, true, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(eq).mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value3, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value3);
-
-        slice = newSingleSlice(cfMetaData, 1, Bound.END, true, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(eq).mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value3, value1);
-
-        slice = newSingleSlice(cfMetaData, 1, Bound.END, false, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(eq).mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value3, value1);
-
-        slice = newSingleSlice(cfMetaData, 1, Bound.START, false, value1);
-        Restriction slice2 = newSingleSlice(cfMetaData, 1, Bound.END, false, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(eq).mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value3, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value3, value2);
-
-        slice = newSingleSlice(cfMetaData, 1, Bound.START, true, value1);
-        slice2 = newSingleSlice(cfMetaData, 1, Bound.END, true, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(eq).mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value3, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value3, value2);
-    }
-
-    /**
-     * Test '(clustering_0, clustering_1) = (1, 2)' with two clustering column
-     */
-    @Test
-    public void testBoundsAsClusteringWithMultiEqRestrictions()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-        Restriction eq = newMultiEq(cfMetaData, 0, value1, value2);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(eq);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2);
-    }
-
-    /**
-     * Test '(clustering_0, clustering_1) IN ((1, 2), (2, 3))' with two clustering column
-     */
-    @Test
-    public void testBoundsAsClusteringWithMultiInRestrictions()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-        ByteBuffer value3 = ByteBufferUtil.bytes(3);
-        Restriction in = newMultiIN(cfMetaData, 0, asList(value1, value2), asList(value2, value3));
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(in);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2);
-        assertStartBound(get(bounds, 1), true, value2, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2);
-        assertEndBound(get(bounds, 1), true, value2, value3);
-    }
-
-    /**
-     * Test multi-column slice restrictions (e.g '(clustering_0) > (1)') with only one clustering column
-     */
-    @Test
-    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithOneClusteringColumn()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC);
-
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-
-        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1);
-
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1);
-        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value2);
-
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1);
-        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value2);
-    }
-
-    /**
-     * Test multi-column slice restrictions (e.g '(clustering_0) > (1)') with only one clustering column in reverse
-     * order
-     */
-    @Test
-    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithOneDescendingClusteringColumn()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.DESC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-
-        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1);
-
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1);
-        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1);
-        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1);
-    }
-
-    /**
-     * Test multi-column slice restrictions (e.g '(clustering_0, clustering_1) > (1, 2)')
-     */
-    @Test
-    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithTwoClusteringColumn()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-
-        // (clustering_0, clustering1) > (1, 2)
-        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-        // (clustering_0, clustering1) >= (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-        // (clustering_0, clustering1) <= (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2);
-
-        // (clustering_0, clustering1) < (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-
-        // (clustering_0, clustering1) > (1, 2) AND (clustering_0) < (2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
-        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value2);
-
-        // (clustering_0, clustering1) >= (1, 2) AND (clustering_0, clustering1) <= (2, 1)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
-        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value2, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value2, value1);
-    }
-
-    /**
-     * Test multi-column slice restrictions with 2 descending clustering columns (e.g '(clustering_0, clustering_1) > (1, 2)')
-     */
-    @Test
-    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithTwoDescendingClusteringColumns()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.DESC, Sort.DESC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-
-        // (clustering_0, clustering1) > (1, 2)
-        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-
-        // (clustering_0, clustering1) >= (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2);
-
-        // (clustering_0, clustering1) <= (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-        // (clustering_0, clustering1) < (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-
-        // (clustering_0, clustering1) > (1, 2) AND (clustering_0) < (2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
-        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-
-        // (clustering_0, clustering1) >= (1, 2) AND (clustering_0, clustering1) <= (2, 1)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
-        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value2, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value2, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2);
-    }
-
-    /**
-     * Test multi-column slice restrictions with 1 descending clustering column and 1 ascending
-     * (e.g '(clustering_0, clustering_1) > (1, 2)')
-     */
-    @Test
-    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithOneDescendingAndOneAscendingClusteringColumns()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.DESC, Sort.ASC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-
-        // (clustering_0, clustering1) > (1, 2)
-        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-        assertStartBound(get(bounds, 1), false, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-        assertEndBound(get(bounds, 1), true, value1);
-
-        // (clustering_0, clustering1) >= (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-        assertStartBound(get(bounds, 1), true, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-        assertEndBound(get(bounds, 1), true, value1);
-
-        // (clustering_0, clustering1) <= (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2);
-        assertEmptyEnd(get(bounds, 1));
-
-        // (clustering_0, clustering1) < (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-        assertEmptyEnd(get(bounds, 1));
-
-        // (clustering_0, clustering1) > (1, 2) AND (clustering_0) < (2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
-        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), false, value2);
-        assertStartBound(get(bounds, 1), false, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-        assertEndBound(get(bounds, 1), true, value1);
-
-        // (clustering_0) > (1) AND (clustering_0, clustering1) < (2, 1)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1);
-        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value2);
-        assertStartBound(get(bounds, 1), false, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value2, value1);
-        assertEndBound(get(bounds, 1), false, value1);
-
-        // (clustering_0, clustering1) >= (1, 2) AND (clustering_0, clustering1) <= (2, 1)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
-        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value2, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(3, bounds.size());
-        assertStartBound(get(bounds, 0), true, value2);
-        assertStartBound(get(bounds, 1), false, value2);
-        assertStartBound(get(bounds, 2), true, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(3, bounds.size());
-        assertEndBound(get(bounds, 0), true, value2, value1);
-        assertEndBound(get(bounds, 1), false, value1);
-        assertEndBound(get(bounds, 2), true, value1);
-    }
-
-    /**
-     * Test multi-column slice restrictions with 1 descending clustering column and 1 ascending
-     * (e.g '(clustering_0, clustering_1) > (1, 2)')
-     */
-    @Test
-    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithOneAscendingAndOneDescendingClusteringColumns()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.DESC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-
-        // (clustering_0, clustering1) > (1, 2)
-        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-        assertEmptyEnd(get(bounds, 1));
-
-        // (clustering_0, clustering1) >= (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2);
-        assertEmptyEnd(get(bounds, 1));
-
-        // (clustering_0, clustering1) <= (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-        assertStartBound(get(bounds, 1), true, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-        assertEndBound(get(bounds, 1), true, value1);
-
-        // (clustering_0, clustering1) < (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-        assertStartBound(get(bounds, 1), false, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-        assertEndBound(get(bounds, 1), true, value1);
-
-        // (clustering_0, clustering1) > (1, 2) AND (clustering_0) < (2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2);
-        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-        assertEndBound(get(bounds, 1), false, value2);
-
-        // (clustering_0, clustering1) >= (1, 2) AND (clustering_0, clustering1) <= (2, 1)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
-        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value2, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(3, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), false, value1);
-        assertStartBound(get(bounds, 2), true, value2, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(3, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2);
-        assertEndBound(get(bounds, 1), false, value2);
-        assertEndBound(get(bounds, 2), true, value2);
-    }
-
-    /**
-     * Test multi-column slice restrictions with 2 ascending clustering column and 2 descending
-     * (e.g '(clustering_0, clustering1, clustering_3, clustering4) > (1, 2, 3, 4)')
-     */
-    @Test
-    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithTwoAscendingAndTwoDescendingClusteringColumns()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC, Sort.DESC, Sort.DESC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-        ByteBuffer value3 = ByteBufferUtil.bytes(3);
-        ByteBuffer value4 = ByteBufferUtil.bytes(4);
-
-        // (clustering_0, clustering1, clustering_2, clustering_3) > (1, 2, 3, 4)
-        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2, value3, value4);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2);
-        assertStartBound(get(bounds, 1), false, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2, value3, value4);
-        assertEmptyEnd(get(bounds, 1));
-
-        // clustering_0 = 1 AND (clustering_1, clustering_2, clustering_3) > (2, 3, 4)
-        Restriction eq = newSingleEq(cfMetaData, 0, value1);
-        slice = newMultiSlice(cfMetaData, 1, Bound.START, false, value2, value3, value4);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-        restrictions = restrictions.mergeWith(eq);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2);
-        assertStartBound(get(bounds, 1), false, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2, value3, value4);
-        assertEndBound(get(bounds, 1), true, value1);
-
-        // clustering_0 IN (1, 2) AND (clustering_1, clustering_2, clustering_3) > (2, 3, 4)
-        Restriction in = newSingleIN(cfMetaData, 0, value1, value2);
-        slice = newMultiSlice(cfMetaData, 1, Bound.START, false, value2, value3, value4);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-        restrictions = restrictions.mergeWith(in);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(4, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2);
-        assertStartBound(get(bounds, 1), false, value1, value2);
-        assertStartBound(get(bounds, 2), true, value2, value2);
-        assertStartBound(get(bounds, 3), false, value2, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(4, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2, value3, value4);
-        assertEndBound(get(bounds, 1), true, value1);
-        assertEndBound(get(bounds, 2), false, value2, value2, value3, value4);
-        assertEndBound(get(bounds, 3), true, value2);
-
-        // (clustering_0, clustering1) >= (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEmptyEnd(get(bounds, 0));
-
-        // (clustering_0, clustering1, clustering_2, clustering_3) >= (1, 2, 3, 4)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2, value3, value4);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2);
-        assertStartBound(get(bounds, 1), false, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2, value3, value4);
-        assertEmptyEnd(get(bounds, 1));
-
-        // (clustering_0, clustering1, clustering_2, clustering_3) <= (1, 2, 3, 4)
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1, value2, value3, value4);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-        assertStartBound(get(bounds, 1), true, value1, value2, value3, value4);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-        assertEndBound(get(bounds, 1), true, value1, value2);
-
-        // (clustering_0, clustering1, clustering_2, clustering_3) < (1, 2, 3, 4)
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1, value2, value3, value4);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-        assertStartBound(get(bounds, 1), false, value1, value2, value3, value4);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-        assertEndBound(get(bounds, 1), true, value1, value2);
-
-        // (clustering_0, clustering1, clustering_2, clustering_3) > (1, 2, 3, 4) AND (clustering_0, clustering_1) < (2, 3)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2, value3, value4);
-        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2, value3);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2);
-        assertStartBound(get(bounds, 1), false, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2, value3, value4);
-        assertEndBound(get(bounds, 1), false, value2, value3);
-
-        // (clustering_0, clustering1, clustering_2, clustering_3) >= (1, 2, 3, 4) AND (clustering_0, clustering1, clustering_2, clustering_3) <= (4, 3, 2, 1)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2, value3, value4);
-        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value4, value3, value2, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(3, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2);
-        assertStartBound(get(bounds, 1), false, value1, value2);
-        assertStartBound(get(bounds, 2), true, value4, value3, value2, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(3, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2, value3, value4);
-        assertEndBound(get(bounds, 1), false, value4, value3);
-        assertEndBound(get(bounds, 2), true, value4, value3);
-    }
-
-    /**
-     * Test multi-column slice restrictions with ascending, descending, ascending and descending columns
-     * (e.g '(clustering_0, clustering1, clustering_3, clustering4) > (1, 2, 3, 4)')
-     */
-    @Test
-    public void testBoundsAsClusteringWithMultiSliceRestrictionsWithAscendingDescendingColumnMix()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.DESC, Sort.ASC, Sort.DESC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-        ByteBuffer value3 = ByteBufferUtil.bytes(3);
-        ByteBuffer value4 = ByteBufferUtil.bytes(4);
-
-        // (clustering_0, clustering1, clustering_2, clustering_3) > (1, 2, 3, 4)
-        Restriction slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2, value3, value4);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(4, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), true, value1, value2, value3);
-        assertStartBound(get(bounds, 2), false, value1, value2, value3);
-        assertStartBound(get(bounds, 3), false, value1);
-
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(4, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-        assertEndBound(get(bounds, 1), false, value1, value2, value3, value4);
-        assertEndBound(get(bounds, 2), true, value1, value2);
-        assertEmptyEnd(get(bounds, 3));
-
-        // clustering_0 = 1 AND (clustering_1, clustering_2, clustering_3) > (2, 3, 4)
-        Restriction eq = newSingleEq(cfMetaData, 0, value1);
-        slice = newMultiSlice(cfMetaData, 1, Bound.START, false, value2, value3, value4);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-        restrictions = restrictions.mergeWith(eq);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(3, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), true, value1, value2, value3);
-        assertStartBound(get(bounds, 2), false, value1, value2, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(3, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-        assertEndBound(get(bounds, 1), false, value1, value2, value3, value4);
-        assertEndBound(get(bounds, 2), true, value1, value2);
-
-        // (clustering_0, clustering1) >= (1, 2)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2);
-        assertEmptyEnd(get(bounds, 1));
-
-        // (clustering_0, clustering1, clustering_2, clustering_3) >= (1, 2, 3, 4)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2, value3, value4);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(4, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), true, value1, value2, value3);
-        assertStartBound(get(bounds, 2), false, value1, value2, value3);
-        assertStartBound(get(bounds, 3), false, value1);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(4, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-        assertEndBound(get(bounds, 1), true, value1, value2, value3, value4);
-        assertEndBound(get(bounds, 2), true, value1, value2);
-        assertEmptyEnd(get(bounds, 3));
-
-        // (clustering_0, clustering1, clustering_2, clustering_3) <= (1, 2, 3, 4)
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, true, value1, value2, value3, value4);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(4, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-        assertStartBound(get(bounds, 1), true, value1, value2);
-        assertStartBound(get(bounds, 2), true, value1, value2, value3, value4);
-        assertStartBound(get(bounds, 3), false, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(4, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-        assertEndBound(get(bounds, 1), false, value1, value2, value3);
-        assertEndBound(get(bounds, 2), true, value1, value2, value3);
-        assertEndBound(get(bounds, 3), true, value1);
-
-        // (clustering_0, clustering1, clustering_2, clustering_3) < (1, 2, 3, 4)
-        slice = newMultiSlice(cfMetaData, 0, Bound.END, false, value1, value2, value3, value4);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(4, bounds.size());
-        assertEmptyStart(get(bounds, 0));
-        assertStartBound(get(bounds, 1), true, value1, value2);
-        assertStartBound(get(bounds, 2), false, value1, value2, value3, value4);
-        assertStartBound(get(bounds, 3), false, value1, value2);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(4, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1);
-        assertEndBound(get(bounds, 1), false, value1, value2, value3);
-        assertEndBound(get(bounds, 2), true, value1, value2, value3);
-        assertEndBound(get(bounds, 3), true, value1);
-
-        // (clustering_0, clustering1, clustering_2, clustering_3) > (1, 2, 3, 4) AND (clustering_0, clustering_1) < (2, 3)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, false, value1, value2, value3, value4);
-        Restriction slice2 = newMultiSlice(cfMetaData, 0, Bound.END, false, value2, value3);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(5, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), true, value1, value2, value3);
-        assertStartBound(get(bounds, 2), false, value1, value2, value3);
-        assertStartBound(get(bounds, 3), false, value1);
-        assertStartBound(get(bounds, 4), false, value2, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(5, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-        assertEndBound(get(bounds, 1), false, value1, value2, value3, value4);
-        assertEndBound(get(bounds, 2), true, value1, value2);
-        assertEndBound(get(bounds, 3), false, value2);
-        assertEndBound(get(bounds, 4), true, value2);
-
-        // (clustering_0, clustering1, clustering_2, clustering_3) >= (1, 2, 3, 4) AND (clustering_0, clustering1, clustering_2, clustering_3) <= (4, 3, 2, 1)
-        slice = newMultiSlice(cfMetaData, 0, Bound.START, true, value1, value2, value3, value4);
-        slice2 = newMultiSlice(cfMetaData, 0, Bound.END, true, value4, value3, value2, value1);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(7, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1);
-        assertStartBound(get(bounds, 1), true, value1, value2, value3);
-        assertStartBound(get(bounds, 2), false, value1, value2, value3);
-        assertStartBound(get(bounds, 3), false, value1);
-        assertStartBound(get(bounds, 4), true, value4, value3);
-        assertStartBound(get(bounds, 5), true, value4, value3, value2, value1);
-        assertStartBound(get(bounds, 6), false, value4, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(7, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value2);
-        assertEndBound(get(bounds, 1), true, value1, value2, value3, value4);
-        assertEndBound(get(bounds, 2), true, value1, value2);
-        assertEndBound(get(bounds, 3), false, value4);
-        assertEndBound(get(bounds, 4), false, value4, value3, value2);
-        assertEndBound(get(bounds, 5), true, value4, value3, value2);
-        assertEndBound(get(bounds, 6), true, value4);
-    }
-
-    /**
-     * Test mixing single and multi equals restrictions (e.g. clustering_0 = 1 AND (clustering_1, clustering_2) = (2, 3))
-     */
-    @Test
-    public void testBoundsAsClusteringWithSingleEqAndMultiEqRestrictions()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC, Sort.ASC, Sort.ASC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-        ByteBuffer value3 = ByteBufferUtil.bytes(3);
-        ByteBuffer value4 = ByteBufferUtil.bytes(4);
-
-        // clustering_0 = 1 AND (clustering_1, clustering_2) = (2, 3)
-        Restriction singleEq = newSingleEq(cfMetaData, 0, value1);
-        Restriction multiEq = newMultiEq(cfMetaData, 1, value2, value3);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiEq);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2, value3);
-
-        // clustering_0 = 1 AND clustering_1 = 2 AND (clustering_2, clustering_3) = (3, 4)
-        singleEq = newSingleEq(cfMetaData, 0, value1);
-        Restriction singleEq2 = newSingleEq(cfMetaData, 1, value2);
-        multiEq = newMultiEq(cfMetaData, 2, value3, value4);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(singleEq2).mergeWith(multiEq);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2, value3, value4);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2, value3, value4);
-
-        // (clustering_0, clustering_1) = (1, 2) AND clustering_2 = 3
-        singleEq = newSingleEq(cfMetaData, 2, value3);
-        multiEq = newMultiEq(cfMetaData, 0, value1, value2);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiEq);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2, value3);
-
-        // clustering_0 = 1 AND (clustering_1, clustering_2) = (2, 3) AND clustering_3 = 4
-        singleEq = newSingleEq(cfMetaData, 0, value1);
-        singleEq2 = newSingleEq(cfMetaData, 3, value4);
-        multiEq = newMultiEq(cfMetaData, 1, value2, value3);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiEq).mergeWith(singleEq2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2, value3, value4);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2, value3, value4);
-    }
-
-    /**
-     * Test clustering_0 = 1 AND (clustering_1, clustering_2) IN ((2, 3), (4, 5))
-     */
-    @Test
-    public void testBoundsAsClusteringWithSingleEqAndMultiINRestrictions()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC, Sort.ASC, Sort.ASC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-        ByteBuffer value3 = ByteBufferUtil.bytes(3);
-        ByteBuffer value4 = ByteBufferUtil.bytes(4);
-        ByteBuffer value5 = ByteBufferUtil.bytes(5);
-
-        // clustering_0 = 1 AND (clustering_1, clustering_2) IN ((2, 3), (4, 5))
-        Restriction singleEq = newSingleEq(cfMetaData, 0, value1);
-        Restriction multiIN = newMultiIN(cfMetaData, 1, asList(value2, value3), asList(value4, value5));
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiIN);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2, value3);
-        assertStartBound(get(bounds, 1), true, value1, value4, value5);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2, value3);
-        assertEndBound(get(bounds, 1), true, value1, value4, value5);
-
-        // clustering_0 = 1 AND (clustering_1, clustering_2) IN ((2, 3))
-        singleEq = newSingleEq(cfMetaData, 0, value1);
-        multiIN = newMultiIN(cfMetaData, 1, asList(value2, value3));
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(multiIN).mergeWith(singleEq);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2, value3);
-
-        // clustering_0 = 1 AND clustering_1 = 5 AND (clustering_2, clustering_3) IN ((2, 3), (4, 5))
-        singleEq = newSingleEq(cfMetaData, 0, value1);
-        Restriction singleEq2 = newSingleEq(cfMetaData, 1, value5);
-        multiIN = newMultiIN(cfMetaData, 2, asList(value2, value3), asList(value4, value5));
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiIN).mergeWith(singleEq2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value5, value2, value3);
-        assertStartBound(get(bounds, 1), true, value1, value5, value4, value5);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value5, value2, value3);
-        assertEndBound(get(bounds, 1), true, value1, value5, value4, value5);
-    }
-
-    /**
-     * Test mixing single equal restrictions with multi-column slice restrictions
-     * (e.g. clustering_0 = 1 AND (clustering_1, clustering_2) > (2, 3))
-     */
-    @Test
-    public void testBoundsAsClusteringWithSingleEqAndSliceRestrictions()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC, Sort.ASC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-        ByteBuffer value3 = ByteBufferUtil.bytes(3);
-        ByteBuffer value4 = ByteBufferUtil.bytes(4);
-        ByteBuffer value5 = ByteBufferUtil.bytes(5);
-
-        // clustering_0 = 1 AND (clustering_1, clustering_2) > (2, 3)
-        Restriction singleEq = newSingleEq(cfMetaData, 0, value1);
-        Restriction multiSlice = newMultiSlice(cfMetaData, 1, Bound.START, false, value2, value3);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiSlice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1, value2, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1);
-
-        // clustering_0 = 1 AND (clustering_1, clustering_2) > (2, 3) AND (clustering_1) < (4)
-        singleEq = newSingleEq(cfMetaData, 0, value1);
-        multiSlice = newMultiSlice(cfMetaData, 1, Bound.START, false, value2, value3);
-        Restriction multiSlice2 = newMultiSlice(cfMetaData, 1, Bound.END, false, value4);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(multiSlice2).mergeWith(singleEq).mergeWith(multiSlice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1, value2, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), false, value1, value4);
-
-        // clustering_0 = 1 AND (clustering_1, clustering_2) => (2, 3) AND (clustering_1, clustering_2) <= (4, 5)
-        singleEq = newSingleEq(cfMetaData, 0, value1);
-        multiSlice = newMultiSlice(cfMetaData, 1, Bound.START, true, value2, value3);
-        multiSlice2 = newMultiSlice(cfMetaData, 1, Bound.END, true, value4, value5);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(multiSlice2).mergeWith(singleEq).mergeWith(multiSlice);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value4, value5);
-    }
-
-    /**
-     * Test mixing multi equal restrictions with single-column slice restrictions
-     * (e.g. clustering_0 = 1 AND (clustering_1, clustering_2) > (2, 3))
-     */
-    @Test
-    public void testBoundsAsClusteringWithMultiEqAndSingleSliceRestrictions()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC, Sort.ASC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-        ByteBuffer value3 = ByteBufferUtil.bytes(3);
-
-        // (clustering_0, clustering_1) = (1, 2) AND clustering_2 > 3
-        Restriction multiEq = newMultiEq(cfMetaData, 0, value1, value2);
-        Restriction singleSlice = newSingleSlice(cfMetaData, 2, Bound.START, false, value3);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(multiEq).mergeWith(singleSlice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1, value2, value3);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2);
-    }
-
-    @Test
-    public void testBoundsAsClusteringWithSeveralMultiColumnRestrictions()
-    {
-        CFMetaData cfMetaData = newCFMetaData(Sort.ASC, Sort.ASC, Sort.ASC, Sort.ASC);
-
-        ByteBuffer value1 = ByteBufferUtil.bytes(1);
-        ByteBuffer value2 = ByteBufferUtil.bytes(2);
-        ByteBuffer value3 = ByteBufferUtil.bytes(3);
-        ByteBuffer value4 = ByteBufferUtil.bytes(4);
-        ByteBuffer value5 = ByteBufferUtil.bytes(5);
-
-        // (clustering_0, clustering_1) = (1, 2) AND (clustering_2, clustering_3) > (3, 4)
-        Restriction multiEq = newMultiEq(cfMetaData, 0, value1, value2);
-        Restriction multiSlice = newMultiSlice(cfMetaData, 2, Bound.START, false, value3, value4);
-        PrimaryKeyRestrictions restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(multiEq).mergeWith(multiSlice);
-
-        SortedSet<Slice.Bound> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), false, value1, value2, value3, value4);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2);
-
-        // (clustering_0, clustering_1) = (1, 2) AND (clustering_2, clustering_3) IN ((3, 4), (4, 5))
-        multiEq = newMultiEq(cfMetaData, 0, value1, value2);
-        Restriction multiIN = newMultiIN(cfMetaData, 2, asList(value3, value4), asList(value4, value5));
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(multiEq).mergeWith(multiIN);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2, value3, value4);
-        assertStartBound(get(bounds, 1), true, value1, value2, value4, value5);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(2, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2, value3, value4);
-        assertEndBound(get(bounds, 1), true, value1, value2, value4, value5);
-
-        // (clustering_0, clustering_1) = (1, 2) AND (clustering_2, clustering_3) = (3, 4)
-        multiEq = newMultiEq(cfMetaData, 0, value1, value2);
-        Restriction multiEq2 = newMultiEq(cfMetaData, 2, value3, value4);
-        restrictions = new PrimaryKeyRestrictionSet(cfMetaData.comparator, false);
-        restrictions = restrictions.mergeWith(multiEq).mergeWith(multiEq2);
-
-        bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertStartBound(get(bounds, 0), true, value1, value2, value3, value4);
-
-        bounds = restrictions.boundsAsClustering(Bound.END, QueryOptions.DEFAULT);
-        assertEquals(1, bounds.size());
-        assertEndBound(get(bounds, 0), true, value1, value2, value3, value4);
-    }
-
-    /**
-     * Asserts that the specified <code>Bound</code> is an empty start.
-     *
-     * @param bound the bound to check
-     */
-    private static void assertEmptyStart(Slice.Bound bound)
-    {
-        assertEquals(Slice.Bound.BOTTOM, bound);
-    }
-
-    /**
-     * Asserts that the specified <code>Bound</code> is an empty end.
-     *
-     * @param bound the bound to check
-     */
-    private static void assertEmptyEnd(Slice.Bound bound)
-    {
-        assertEquals(Slice.Bound.TOP, bound);
-    }
-
-    /**
-     * Asserts that the specified <code>Slice.Bound</code> is a start with the specified elements.
-     *
-     * @param bound the bound to check
-     * @param isInclusive if the bound is expected to be inclusive
-     * @param elements the expected elements of the clustering
-     */
-    private static void assertStartBound(Slice.Bound bound, boolean isInclusive, ByteBuffer... elements)
-    {
-        assertBound(bound, true, isInclusive, elements);
-    }
-
-    /**
-     * Asserts that the specified <code>Slice.Bound</code> is a end with the specified elements.
-     *
-     * @param bound the bound to check
-     * @param isInclusive if the bound is expected to be inclusive
-     * @param elements the expected elements of the clustering
-     */
-    private static void assertEndBound(Slice.Bound bound, boolean isInclusive, ByteBuffer... elements)
-    {
-        assertBound(bound, false, isInclusive, elements);
-    }
-
-    private static void assertBound(Slice.Bound bound, boolean isStart, boolean isInclusive, ByteBuffer... elements)
-    {
-        assertEquals("the bound size is not the expected one:", elements.length, bound.size());
-        assertEquals("the bound should be a " + (isStart ? "start" : "end") + " but is a " + (bound.isStart() ? "start" : "end"), isStart, bound.isStart());
-        assertEquals("the bound inclusiveness is not the expected one", isInclusive, bound.isInclusive());
-        for (int i = 0, m = elements.length; i < m; i++)
-        {
-            ByteBuffer element = elements[i];
-            assertTrue(String.format("the element %s of the bound is not the expected one: expected %s but was %s",
-                                     i,
-                                     ByteBufferUtil.toInt(element),
-                                     ByteBufferUtil.toInt(bound.get(i))),
-                       element.equals(bound.get(i)));
-        }
-    }
-
-    /**
-     * Creates a new <code>CFMetaData</code> instance.
-     *
-     * @param numberOfClusteringColumns the number of clustering column
-     * @return a new <code>CFMetaData</code> instance
-     */
-    private static CFMetaData newCFMetaData(Sort... sorts)
-    {
-        List<AbstractType<?>> types = new ArrayList<>();
-
-        for (Sort sort : sorts)
-            types.add(sort == Sort.ASC ? Int32Type.instance : ReversedType.getInstance(Int32Type.instance));
-
-        CFMetaData.Builder builder = CFMetaData.Builder.create("keyspace", "test")
-                                                       .addPartitionKey("partition_key", Int32Type.instance);
-
-        for (int i = 0; i < sorts.length; i++)
-            builder.addClusteringColumn("clustering_" + i, types.get(i));
-
-        return builder.build();
-    }
-
-    /**
-     * Creates a new <code>SingleColumnRestriction.EQ</code> instance for the specified clustering column.
-     *
-     * @param cfMetaData the column family meta data
-     * @param index the clustering column index
-     * @param value the equality value
-     * @return a new <code>SingleColumnRestriction.EQ</code> instance for the specified clustering column
-     */
-    private static Restriction newSingleEq(CFMetaData cfMetaData, int index, ByteBuffer value)
-    {
-        ColumnDefinition columnDef = getClusteringColumnDefinition(cfMetaData, index);
-        return new SingleColumnRestriction.EQRestriction(columnDef, toTerm(value));
-    }
-
-    /**
-     * Creates a new <code>MultiColumnRestriction.EQ</code> instance for the specified clustering column.
-     *
-     * @param cfMetaData the column family meta data
-     * @param index the clustering column index
-     * @param value the equality value
-     * @return a new <code>MultiColumnRestriction.EQ</code> instance for the specified clustering column
-     */
-    private static Restriction newMultiEq(CFMetaData cfMetaData, int firstIndex, ByteBuffer... values)
-    {
-        List<ColumnDefinition> columnDefinitions = new ArrayList<>();
-        for (int i = 0; i < values.length; i++)
-        {
-            columnDefinitions.add(getClusteringColumnDefinition(cfMetaData, firstIndex + i));
-        }
-        return new MultiColumnRestriction.EQRestriction(columnDefinitions, toMultiItemTerminal(values));
-    }
-
-    /**
-     * Creates a new <code>MultiColumnRestriction.IN</code> instance for the specified clustering column.
-     *
-     * @param cfMetaData the column family meta data
-     * @param firstIndex the index of the first clustering column
-     * @param values the in values
-     * @return a new <code>MultiColumnRestriction.IN</code> instance for the specified clustering column
-     */
-    @SafeVarargs
-    private static Restriction newMultiIN(CFMetaData cfMetaData, int firstIndex, List<ByteBuffer>... values)
-    {
-        List<ColumnDefinition> columnDefinitions = new ArrayList<>();
-        List<Term> terms = new ArrayList<>();
-        for (int i = 0; i < values.length; i++)
-        {
-            columnDefinitions.add(getClusteringColumnDefinition(cfMetaData, firstIndex + i));
-            terms.add(toMultiItemTerminal(values[i].toArray(new ByteBuffer[0])));
-        }
-        return new MultiColumnRestriction.InRestrictionWithValues(columnDefinitions, terms);
-    }
-
-    /**
-     * Creates a new <code>SingleColumnRestriction.IN</code> instance for the specified clustering column.
-     *
-     * @param cfMetaData the column family meta data
-     * @param index the clustering column index
-     * @param values the in values
-     * @return a new <code>SingleColumnRestriction.IN</code> instance for the specified clustering column
-     */
-    private static Restriction newSingleIN(CFMetaData cfMetaData, int index, ByteBuffer... values)
-    {
-        ColumnDefinition columnDef = getClusteringColumnDefinition(cfMetaData, index);
-        return new SingleColumnRestriction.InRestrictionWithValues(columnDef, toTerms(values));
-    }
-
-    /**
-     * Returns the clustering <code>ColumnDefinition</code> for the specified position.
-     *
-     * @param cfMetaData the column family meta data
-     * @param index the clustering column index
-     * @return the clustering <code>ColumnDefinition</code> for the specified position.
-     */
-    private static ColumnDefinition getClusteringColumnDefinition(CFMetaData cfMetaData, int index)
-    {
-        return cfMetaData.clusteringColumns().get(index);
-    }
-
-    /**
-     * Creates a new <code>SingleColumnRestriction.Slice</code> instance for the specified clustering column.
-     *
-     * @param cfMetaData the column family meta data
-     * @param index the clustering column index
-     * @param bound the slice bound
-     * @param inclusive <code>true</code> if the bound is inclusive
-     * @param value the bound value
-     * @return a new <code>SingleColumnRestriction.Slice</code> instance for the specified clustering column
-     */
-    private static Restriction newSingleSlice(CFMetaData cfMetaData, int index, Bound bound, boolean inclusive, ByteBuffer value)
-    {
-        ColumnDefinition columnDef = getClusteringColumnDefinition(cfMetaData, index);
-        return new SingleColumnRestriction.SliceRestriction(columnDef, bound, inclusive, toTerm(value));
-    }
-
-    /**
-     * Creates a new <code>SingleColumnRestriction.Slice</code> instance for the specified clustering column.
-     *
-     * @param cfMetaData the column family meta data
-     * @param index the clustering column index
-     * @param bound the slice bound
-     * @param inclusive <code>true</code> if the bound is inclusive
-     * @param value the bound value
-     * @return a new <code>SingleColumnRestriction.Slice</code> instance for the specified clustering column
-     */
-    private static Restriction newMultiSlice(CFMetaData cfMetaData, int firstIndex, Bound bound, boolean inclusive, ByteBuffer... values)
-    {
-        List<ColumnDefinition> columnDefinitions = new ArrayList<>();
-        for (int i = 0; i < values.length; i++)
-        {
-            columnDefinitions.add(getClusteringColumnDefinition(cfMetaData, i + firstIndex));
-        }
-        return new MultiColumnRestriction.SliceRestriction(columnDefinitions, bound, inclusive, toMultiItemTerminal(values));
-    }
-
-    /**
-     * Converts the specified values into a <code>MultiItemTerminal</code>.
-     *
-     * @param values the values to convert.
-     * @return the term corresponding to the specified values.
-     */
-    private static MultiItemTerminal toMultiItemTerminal(ByteBuffer... values)
-    {
-        return new Tuples.Value(values);
-    }
-
-    /**
-     * Converts the specified value into a term.
-     *
-     * @param value the value to convert.
-     * @return the term corresponding to the specified value.
-     */
-    private static Term toTerm(ByteBuffer value)
-    {
-        return new Constants.Value(value);
-    }
-
-    /**
-     * Converts the specified values into a <code>List</code> of terms.
-     *
-     * @param values the values to convert.
-     * @return a <code>List</code> of terms corresponding to the specified values.
-     */
-    private static List<Term> toTerms(ByteBuffer... values)
-    {
-        List<Term> terms = new ArrayList<>();
-        for (ByteBuffer value : values)
-            terms.add(toTerm(value));
-        return terms;
-    }
-
-    private static <T> T get(SortedSet<T> set, int i)
-    {
-        return Iterables.get(set, i);
-    }
-
-    private static enum Sort
-    {
-        ASC,
-        DESC;
-    }
-}
diff --git a/test/unit/org/apache/cassandra/cql3/selection/SelectionColumnMappingTest.java b/test/unit/org/apache/cassandra/cql3/selection/SelectionColumnMappingTest.java
index 2b7a197..ece2d1d 100644
--- a/test/unit/org/apache/cassandra/cql3/selection/SelectionColumnMappingTest.java
+++ b/test/unit/org/apache/cassandra/cql3/selection/SelectionColumnMappingTest.java
@@ -51,9 +51,11 @@
     String functionName;
 
     @BeforeClass
-    public static void setUpClass()
+    public static void setUpClass()     // overrides CQLTester.setUpClass()
     {
         DatabaseDescriptor.setPartitionerUnsafe(ByteOrderedPartitioner.instance);
+
+        prepareServer();
     }
 
     @Test
@@ -68,7 +70,7 @@
                                 " v1 int," +
                                 " v2 ascii," +
                                 " v3 frozen<" + typeName + ">)");
-        userType = Schema.instance.getKSMetaData(KEYSPACE).types.get(ByteBufferUtil.bytes(typeName)).get();
+        userType = Schema.instance.getKSMetaData(KEYSPACE).types.get(ByteBufferUtil.bytes(typeName)).get().freeze();
         functionName = createFunction(KEYSPACE, "int, ascii",
                                       "CREATE FUNCTION %s (i int, a ascii) " +
                                       "CALLED ON NULL INPUT " +
diff --git a/test/unit/org/apache/cassandra/cql3/selection/TermSelectionTest.java b/test/unit/org/apache/cassandra/cql3/selection/TermSelectionTest.java
new file mode 100644
index 0000000..a07f8f9
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/selection/TermSelectionTest.java
@@ -0,0 +1,336 @@
+/*
+ * 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.selection;
+
+import java.math.BigDecimal;
+import java.util.*;
+
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.*;
+import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.transport.messages.ResultMessage;
+
+import static org.junit.Assert.assertEquals;
+
+public class TermSelectionTest extends CQLTester
+{
+    // Helper method for testSelectLiteral()
+    private void assertConstantResult(UntypedResultSet result, Object constant)
+    {
+        assertRows(result,
+                   row(1, "one", constant),
+                   row(2, "two", constant),
+                   row(3, "three", constant));
+    }
+
+    @Test
+    public void testSelectLiteral() throws Throwable
+    {
+        createTable("CREATE TABLE %s (pk int, ck int, t text, PRIMARY KEY (pk, ck) )");
+        execute("INSERT INTO %s (pk, ck, t) VALUES (1, 1, 'one')");
+        execute("INSERT INTO %s (pk, ck, t) VALUES (1, 2, 'two')");
+        execute("INSERT INTO %s (pk, ck, t) VALUES (1, 3, 'three')");
+
+        assertInvalidMessage("Cannot infer type for term", "SELECT ck, t, 'a const' FROM %s");
+        assertConstantResult(execute("SELECT ck, t, (text)'a const' FROM %s"), "a const");
+
+        assertInvalidMessage("Cannot infer type for term", "SELECT ck, t, 42 FROM %s");
+        assertConstantResult(execute("SELECT ck, t, (int)42 FROM %s"), 42);
+
+        assertInvalidMessage("Cannot infer type for term", "SELECT ck, t, (1, 'foo') FROM %s");
+        assertConstantResult(execute("SELECT ck, t, (tuple<int, text>)(1, 'foo') FROM %s"), tuple(1, "foo"));
+
+        assertInvalidMessage("Cannot infer type for term", "SELECT ck, t, [1, 2, 3] FROM %s");
+        assertConstantResult(execute("SELECT ck, t, (list<int>)[1, 2, 3] FROM %s"), list(1, 2, 3));
+
+        assertInvalidMessage("Cannot infer type for term", "SELECT ck, t, {1, 2, 3} FROM %s");
+        assertConstantResult(execute("SELECT ck, t, (set<int>){1, 2, 3} FROM %s"), set(1, 2, 3));
+
+        assertInvalidMessage("Cannot infer type for term", "SELECT ck, t, {1: 'foo', 2: 'bar', 3: 'baz'} FROM %s");
+        assertConstantResult(execute("SELECT ck, t, (map<int, text>){1: 'foo', 2: 'bar', 3: 'baz'} FROM %s"), map(1, "foo", 2, "bar", 3, "baz"));
+
+        assertColumnNames(execute("SELECT ck, t, (int)42, (int)43 FROM %s"), "ck", "t", "(int)42", "(int)43");
+        assertRows(execute("SELECT ck, t, (int) 42, (int) 43 FROM %s"),
+                   row(1, "one", 42, 43),
+                   row(2, "two", 42, 43),
+                   row(3, "three", 42, 43));
+    }
+
+    @Test
+    public void testSelectUDTLiteral() throws Throwable
+    {
+        String type = createType("CREATE TYPE %s(a int, b text)");
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v " + type + ")");
+
+        execute("INSERT INTO %s(k, v) VALUES (?, ?)", 0, userType("a", 3, "b", "foo"));
+
+        assertInvalidMessage("Cannot infer type for term", "SELECT k, v, { a: 4, b: 'bar'} FROM %s");
+
+        assertRows(execute("SELECT k, v, (" + type + "){ a: 4, b: 'bar'} FROM %s"),
+            row(0, userType("a", 3, "b", "foo"), userType("a", 4, "b", "bar"))
+        );
+    }
+
+    @Test
+    public void testInvalidSelect() throws Throwable
+    {
+        // Creates a table just so we can reference it in the (invalid) SELECT below
+        createTable("CREATE TABLE %s (k int PRIMARY KEY)");
+
+        assertInvalidMessage("Cannot infer type for term", "SELECT ? FROM %s");
+        assertInvalidMessage("Cannot infer type for term", "SELECT k, ? FROM %s");
+
+        assertInvalidMessage("Cannot infer type for term", "SELECT k, null FROM %s");
+    }
+
+    private void assertColumnSpec(ColumnSpecification spec, String expectedName, AbstractType<?> expectedType)
+    {
+        assertEquals(expectedName, spec.name.toString());
+        assertEquals(expectedType, spec.type);
+    }
+
+    @Test
+    public void testSelectPrepared() throws Throwable
+    {
+        createTable("CREATE TABLE %s (pk int, ck int, t text, PRIMARY KEY (pk, ck) )");
+        execute("INSERT INTO %s (pk, ck, t) VALUES (1, 1, 'one')");
+        execute("INSERT INTO %s (pk, ck, t) VALUES (1, 2, 'two')");
+        execute("INSERT INTO %s (pk, ck, t) VALUES (1, 3, 'three')");
+
+        String query = "SELECT (int)?, (decimal):adecimal, (text)?, (tuple<int,text>):atuple, pk, ck, t FROM %s WHERE pk = ?";
+        ResultMessage.Prepared prepared = prepare(query);
+
+        List<ColumnSpecification> boundNames = prepared.metadata.names;
+
+        // 5 bound variables
+        assertEquals(5, boundNames.size());
+        assertColumnSpec(boundNames.get(0), "[selection]", Int32Type.instance);
+        assertColumnSpec(boundNames.get(1), "adecimal", DecimalType.instance);
+        assertColumnSpec(boundNames.get(2), "[selection]", UTF8Type.instance);
+        assertColumnSpec(boundNames.get(3), "atuple", TypeParser.parse("TupleType(Int32Type,UTF8Type)"));
+        assertColumnSpec(boundNames.get(4), "pk", Int32Type.instance);
+
+
+        List<ColumnSpecification> resultNames = prepared.resultMetadata.names;
+
+        // 7 result "columns"
+        assertEquals(7, resultNames.size());
+        assertColumnSpec(resultNames.get(0), "(int)?", Int32Type.instance);
+        assertColumnSpec(resultNames.get(1), "(decimal)?", DecimalType.instance);
+        assertColumnSpec(resultNames.get(2), "(text)?", UTF8Type.instance);
+        assertColumnSpec(resultNames.get(3), "(tuple<int, text>)?", TypeParser.parse("TupleType(Int32Type,UTF8Type)"));
+        assertColumnSpec(resultNames.get(4), "pk", Int32Type.instance);
+        assertColumnSpec(resultNames.get(5), "ck", Int32Type.instance);
+        assertColumnSpec(resultNames.get(6), "t", UTF8Type.instance);
+
+        assertRows(execute(query, 88, BigDecimal.TEN, "foo bar baz", tuple(42, "ursus"), 1),
+                   row(88, BigDecimal.TEN, "foo bar baz", tuple(42, "ursus"),
+                       1, 1, "one"),
+                   row(88, BigDecimal.TEN, "foo bar baz", tuple(42, "ursus"),
+                       1, 2, "two"),
+                   row(88, BigDecimal.TEN, "foo bar baz", tuple(42, "ursus"),
+                       1, 3, "three"));
+    }
+
+    @Test
+    public void testConstantFunctionArgs() throws Throwable
+    {
+        String fInt = createFunction(KEYSPACE,
+                                     "int,int",
+                                     "CREATE FUNCTION %s (val1 int, val2 int) " +
+                                     "CALLED ON NULL INPUT " +
+                                     "RETURNS int " +
+                                     "LANGUAGE java\n" +
+                                     "AS 'return Math.max(val1, val2);';");
+        String fFloat = createFunction(KEYSPACE,
+                                       "float,float",
+                                       "CREATE FUNCTION %s (val1 float, val2 float) " +
+                                       "CALLED ON NULL INPUT " +
+                                       "RETURNS float " +
+                                       "LANGUAGE java\n" +
+                                       "AS 'return Math.max(val1, val2);';");
+        String fText = createFunction(KEYSPACE,
+                                      "text,text",
+                                      "CREATE FUNCTION %s (val1 text, val2 text) " +
+                                      "CALLED ON NULL INPUT " +
+                                      "RETURNS text " +
+                                      "LANGUAGE java\n" +
+                                      "AS 'return val2;';");
+        String fAscii = createFunction(KEYSPACE,
+                                       "ascii,ascii",
+                                       "CREATE FUNCTION %s (val1 ascii, val2 ascii) " +
+                                       "CALLED ON NULL INPUT " +
+                                       "RETURNS ascii " +
+                                       "LANGUAGE java\n" +
+                                       "AS 'return val2;';");
+        String fTimeuuid = createFunction(KEYSPACE,
+                                          "timeuuid,timeuuid",
+                                          "CREATE FUNCTION %s (val1 timeuuid, val2 timeuuid) " +
+                                          "CALLED ON NULL INPUT " +
+                                          "RETURNS timeuuid " +
+                                          "LANGUAGE java\n" +
+                                          "AS 'return val2;';");
+
+        createTable("CREATE TABLE %s (pk int PRIMARY KEY, valInt int, valFloat float, valText text, valAscii ascii, valTimeuuid timeuuid)");
+        execute("INSERT INTO %s (pk, valInt, valFloat, valText, valAscii, valTimeuuid) " +
+                "VALUES (1, 10, 10.0, '100', '100', 2deb23e0-96b5-11e5-b26d-a939dd1405a3)");
+
+        assertRows(execute("SELECT pk, " + fInt + "(valInt, 100) FROM %s"),
+                   row(1, 100));
+        assertRows(execute("SELECT pk, " + fInt + "(valInt, (int)100) FROM %s"),
+                   row(1, 100));
+        assertInvalidMessage("Type error: (bigint)100 cannot be passed as argument 1 of function",
+                             "SELECT pk, " + fInt + "(valInt, (bigint)100) FROM %s");
+        assertRows(execute("SELECT pk, " + fFloat + "(valFloat, (float)100.00) FROM %s"),
+                   row(1, 100f));
+        assertRows(execute("SELECT pk, " + fText + "(valText, 'foo') FROM %s"),
+                   row(1, "foo"));
+        assertRows(execute("SELECT pk, " + fAscii + "(valAscii, (ascii)'foo') FROM %s"),
+                   row(1, "foo"));
+        assertRows(execute("SELECT pk, " + fTimeuuid + "(valTimeuuid, (timeuuid)34617f80-96b5-11e5-b26d-a939dd1405a3) FROM %s"),
+                   row(1, UUID.fromString("34617f80-96b5-11e5-b26d-a939dd1405a3")));
+
+        // ambiguous
+
+        String fAmbiguousFunc1 = createFunction(KEYSPACE,
+                                                "int,bigint",
+                                                "CREATE FUNCTION %s (val1 int, val2 bigint) " +
+                                                "CALLED ON NULL INPUT " +
+                                                "RETURNS bigint " +
+                                                "LANGUAGE java\n" +
+                                                "AS 'return Math.max((long)val1, val2);';");
+        assertRows(execute("SELECT pk, " + fAmbiguousFunc1 + "(valInt, 100) FROM %s"),
+                   row(1, 100L));
+        createFunctionOverload(fAmbiguousFunc1, "int,int",
+                                                "CREATE FUNCTION %s (val1 int, val2 int) " +
+                                                "CALLED ON NULL INPUT " +
+                                                "RETURNS bigint " +
+                                                "LANGUAGE java\n" +
+                                                "AS 'return (long)Math.max(val1, val2);';");
+        assertInvalidMessage("Ambiguous call to function cql_test_keyspace.function_",
+                             "SELECT pk, " + fAmbiguousFunc1 + "(valInt, 100) FROM %s");
+    }
+
+    @Test
+    public void testPreparedFunctionArgs() throws Throwable
+    {
+        createTable("CREATE TABLE %s (pk int, ck int, t text, i int, PRIMARY KEY (pk, ck) )");
+        execute("INSERT INTO %s (pk, ck, t, i) VALUES (1, 1, 'one', 50)");
+        execute("INSERT INTO %s (pk, ck, t, i) VALUES (1, 2, 'two', 100)");
+        execute("INSERT INTO %s (pk, ck, t, i) VALUES (1, 3, 'three', 150)");
+
+        String fIntMax = createFunction(KEYSPACE,
+                                        "int,int",
+                                        "CREATE FUNCTION %s (val1 int, val2 int) " +
+                                        "CALLED ON NULL INPUT " +
+                                        "RETURNS int " +
+                                        "LANGUAGE java\n" +
+                                        "AS 'return Math.max(val1, val2);';");
+
+        // weak typing
+
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, ?) FROM %s", 0),
+                   row(1, 1, 50),
+                   row(1, 2, 100),
+                   row(1, 3, 150));
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, ?) FROM %s", 100),
+                   row(1, 1, 100),
+                   row(1, 2, 100),
+                   row(1, 3, 150));
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, ?) FROM %s", 200),
+                   row(1, 1, 200),
+                   row(1, 2, 200),
+                   row(1, 3, 200));
+
+        // explicit typing
+
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, (int)?) FROM %s", 0),
+                   row(1, 1, 50),
+                   row(1, 2, 100),
+                   row(1, 3, 150));
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, (int)?) FROM %s", 100),
+                   row(1, 1, 100),
+                   row(1, 2, 100),
+                   row(1, 3, 150));
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, (int)?) FROM %s", 200),
+                   row(1, 1, 200),
+                   row(1, 2, 200),
+                   row(1, 3, 200));
+
+        // weak typing
+
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, ?) FROM %s WHERE pk = " + fIntMax + "(1,1)", 0),
+                   row(1, 1, 50),
+                   row(1, 2, 100),
+                   row(1, 3, 150));
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, ?) FROM %s WHERE pk = " + fIntMax + "(2,1)", 0));
+
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, ?) FROM %s WHERE pk = " + fIntMax + "(?,1)", 0, 1),
+                   row(1, 1, 50),
+                   row(1, 2, 100),
+                   row(1, 3, 150));
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, ?) FROM %s WHERE pk = " + fIntMax + "(?,1)", 0, 2));
+
+        // explicit typing
+
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, (int)?) FROM %s WHERE pk = " + fIntMax + "((int)1,(int)1)", 0),
+                   row(1, 1, 50),
+                   row(1, 2, 100),
+                   row(1, 3, 150));
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, (int)?) FROM %s WHERE pk = " + fIntMax + "((int)2,(int)1)", 0));
+
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, (int)?) FROM %s WHERE pk = " + fIntMax + "((int)?,(int)1)", 0, 1),
+                   row(1, 1, 50),
+                   row(1, 2, 100),
+                   row(1, 3, 150));
+        assertRows(execute("SELECT pk, ck, " + fIntMax + "(i, (int)?) FROM %s WHERE pk = " + fIntMax + "((int)?,(int)1)", 0, 2));
+
+        assertInvalidMessage("Invalid unset value for argument", "SELECT pk, ck, " + fIntMax + "(i, (int)?) FROM %s WHERE pk = " + fIntMax + "((int)1,(int)1)", unset());
+    }
+
+    @Test
+    public void testInsertUpdateDelete() throws Throwable
+    {
+        String fIntMax = createFunction(KEYSPACE,
+                                        "int,int",
+                                        "CREATE FUNCTION %s (val1 int, val2 int) " +
+                                        "CALLED ON NULL INPUT " +
+                                        "RETURNS int " +
+                                        "LANGUAGE java\n" +
+                                        "AS 'return Math.max(val1, val2);';");
+
+        createTable("CREATE TABLE %s (pk int, ck int, t text, i int, PRIMARY KEY (pk, ck) )");
+
+        execute("UPDATE %s SET i = " + fIntMax + "(100, 200) WHERE pk = 1 AND ck = 1");
+        assertRows(execute("SELECT i FROM %s WHERE pk = 1 AND ck = 1"),
+                   row(200));
+
+        execute("UPDATE %s SET i = " + fIntMax + "(100, 300) WHERE pk = 1 AND ck = " + fIntMax + "(1,2)");
+        assertRows(execute("SELECT i FROM %s WHERE pk = 1 AND ck = 2"),
+                   row(300));
+
+        execute("DELETE FROM %s WHERE pk = 1 AND ck = " + fIntMax + "(1,2)");
+        assertRows(execute("SELECT i FROM %s WHERE pk = 1 AND ck = 2"));
+
+        execute("INSERT INTO %s (pk, ck, i) VALUES (1, " + fIntMax + "(1,2), " + fIntMax + "(100, 300))");
+        assertRows(execute("SELECT i FROM %s WHERE pk = 1 AND ck = 2"),
+                   row(300));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/statements/PropertyDefinitionsTest.java b/test/unit/org/apache/cassandra/cql3/statements/PropertyDefinitionsTest.java
new file mode 100644
index 0000000..18487f7
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/statements/PropertyDefinitionsTest.java
@@ -0,0 +1,81 @@
+/*
+ *
+ * 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.statements;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.Before;
+
+import static org.junit.Assert.assertEquals;
+
+public class PropertyDefinitionsTest {
+    
+    PropertyDefinitions pd;
+    
+    @Before
+    public void setUp()
+    {
+        pd = new PropertyDefinitions();
+    }
+    
+    @After
+    public void clear()
+    {
+        pd = null;
+    }
+    
+
+    @Test
+    public void testGetBooleanExistant()
+    {
+        String key = "one";
+        pd.addProperty(key, "1");
+        assertEquals(Boolean.TRUE, pd.getBoolean(key, null));
+        
+        key = "TRUE";
+        pd.addProperty(key, "TrUe");
+        assertEquals(Boolean.TRUE, pd.getBoolean(key, null));
+        
+        key = "YES";
+        pd.addProperty(key, "YeS");
+        assertEquals(Boolean.TRUE, pd.getBoolean(key, null));
+   
+        key = "BAD_ONE";
+        pd.addProperty(key, " 1");
+        assertEquals(Boolean.FALSE, pd.getBoolean(key, null));
+        
+        key = "BAD_TRUE";
+        pd.addProperty(key, "true ");
+        assertEquals(Boolean.FALSE, pd.getBoolean(key, null));
+        
+        key = "BAD_YES";
+        pd.addProperty(key, "ye s");
+        assertEquals(Boolean.FALSE, pd.getBoolean(key, null));
+    }
+    
+    @Test
+    public void testGetBooleanNonexistant()
+    {
+        assertEquals(Boolean.FALSE, pd.getBoolean("nonexistant", Boolean.FALSE));
+        assertEquals(Boolean.TRUE, pd.getBoolean("nonexistant", Boolean.TRUE));
+    }
+    
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/ThriftIntegrationTest.java b/test/unit/org/apache/cassandra/cql3/validation/ThriftIntegrationTest.java
index 7741b80..c164caa 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/ThriftIntegrationTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/ThriftIntegrationTest.java
@@ -578,7 +578,7 @@
                    row(3L, 4L, "val1", "key2"),
                    row(3L, 4L, "val2", "key2"));
 
-        assertInvalidMessage("Undefined name value in selection clause",
+        assertInvalidMessage("Undefined column name value",
                              String.format("select value from %s.%s", KEYSPACE, currentSparseTable()));
 
         assertRows(execute(String.format("select * from %s.%s WHERE key = ? AND column1 = ?", KEYSPACE, currentSparseTable()), "key1", "val2"),
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/CollectionsTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/CollectionsTest.java
index fff476b..7424469 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/CollectionsTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/CollectionsTest.java
@@ -17,11 +17,7 @@
  */
 package org.apache.cassandra.cql3.validation.entities;
 
-import java.util.Map;
-import java.util.Random;
-import java.util.Set;
-import java.util.Arrays;
-import java.util.UUID;
+import java.util.*;
 
 import org.junit.Test;
 
@@ -119,6 +115,19 @@
                    row(set("v7"))
         );
 
+        execute("UPDATE %s SET s += ? WHERE k = 0", set("v5"));
+        execute("UPDATE %s SET s += ? WHERE k = 0", set("v6"));
+
+        assertRows(execute("SELECT s FROM %s WHERE k = 0"),
+                   row(set("v5", "v6", "v7"))
+        );
+
+        execute("UPDATE %s SET s -= ? WHERE k = 0", set("v6", "v5"));
+
+        assertRows(execute("SELECT s FROM %s WHERE k = 0"),
+                   row(set("v7"))
+        );
+
         execute("DELETE s[?] FROM %s WHERE k = 0", set("v7"));
 
         // Deleting an element that does not exist will succeed
@@ -167,7 +176,19 @@
                    row(map("v5", 5, "v6", 6, "v7", 7))
         );
 
-        execute("DELETE m[?] FROM %s WHERE k = 0", "v7");
+        execute("UPDATE %s SET m = m - ? WHERE k = 0", set("v7"));
+
+        assertRows(execute("SELECT m FROM %s WHERE k = 0"),
+                   row(map("v5", 5, "v6", 6))
+        );
+
+        execute("UPDATE %s SET m += ? WHERE k = 0", map("v7", 7));
+
+        assertRows(execute("SELECT m FROM %s WHERE k = 0"),
+                   row(map("v5", 5, "v6", 6, "v7", 7))
+        );
+
+        execute("UPDATE %s SET m -= ? WHERE k = 0", set("v7"));
 
         assertRows(execute("SELECT m FROM %s WHERE k = 0"),
                    row(map("v5", 5, "v6", 6))
@@ -235,6 +256,14 @@
 
         assertRows(execute("SELECT l FROM %s WHERE k = 0"), row(list("v9", "v6", "v7")));
 
+        execute("UPDATE %s SET l += ? WHERE k = 0", list("v8"));
+
+        assertRows(execute("SELECT l FROM %s WHERE k = 0"), row(list("v9", "v6", "v7", "v8")));
+
+        execute("UPDATE %s SET l -= ? WHERE k = 0", list("v6", "v8"));
+
+        assertRows(execute("SELECT l FROM %s WHERE k = 0"), row(list("v9", "v7")));
+
         execute("DELETE l FROM %s WHERE k = 0");
 
         assertRows(execute("SELECT l FROM %s WHERE k = 0"), row((Object) null));
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/CountersTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/CountersTest.java
index c9939c8..94e1c52 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/CountersTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/CountersTest.java
@@ -21,7 +21,6 @@
 import org.junit.Test;
 
 import org.apache.cassandra.cql3.CQLTester;
-import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 
 public class CountersTest extends CQLTester
@@ -50,6 +49,22 @@
         execute("UPDATE %s SET total = total -2 WHERE userid = 1 AND url = 'http://foo.com'");
         assertRows(execute("SELECT total FROM %s WHERE userid = 1 AND url = 'http://foo.com'"),
                    row(-4L));
+
+        execute("UPDATE %s SET total += 6 WHERE userid = 1 AND url = 'http://foo.com'");
+        assertRows(execute("SELECT total FROM %s WHERE userid = 1 AND url = 'http://foo.com'"),
+                   row(2L));
+
+        execute("UPDATE %s SET total -= 1 WHERE userid = 1 AND url = 'http://foo.com'");
+        assertRows(execute("SELECT total FROM %s WHERE userid = 1 AND url = 'http://foo.com'"),
+                   row(1L));
+
+        execute("UPDATE %s SET total += -2 WHERE userid = 1 AND url = 'http://foo.com'");
+        assertRows(execute("SELECT total FROM %s WHERE userid = 1 AND url = 'http://foo.com'"),
+                   row(-1L));
+
+        execute("UPDATE %s SET total -= -2 WHERE userid = 1 AND url = 'http://foo.com'");
+        assertRows(execute("SELECT total FROM %s WHERE userid = 1 AND url = 'http://foo.com'"),
+                   row(1L));
     }
 
     /**
@@ -116,7 +131,7 @@
     @Test
     public void testCounterFiltering() throws Throwable
     {
-        for (String compactStorageClause: new String[] {"", " WITH COMPACT STORAGE"})
+        for (String compactStorageClause : new String[]{ "", " WITH COMPACT STORAGE" })
         {
             createTable("CREATE TABLE %s (k int PRIMARY KEY, a counter)" + compactStorageClause);
 
@@ -196,4 +211,31 @@
         assertInvalidThrowMessage("counter type is not supported for PRIMARY KEY part a",
                                   InvalidRequestException.class, String.format("CREATE TABLE %s.%s (a counter, b int, PRIMARY KEY (b, a)) WITH CLUSTERING ORDER BY (a desc);", KEYSPACE, createTableName()));
     }
+
+    /**
+     * Test for the bug of #11726.
+     */
+    @Test
+    public void testCounterAndColumnSelection() throws Throwable
+    {
+        for (String compactStorageClause : new String[]{ "", " WITH COMPACT STORAGE" })
+        {
+            createTable("CREATE TABLE %s (k int PRIMARY KEY, c counter)" + compactStorageClause);
+
+            // Flush 2 updates in different sstable so that the following select does a merge, which is what triggers
+            // the problem from #11726
+
+            execute("UPDATE %s SET c = c + ? WHERE k = ?", 1L, 0);
+
+            flush();
+
+            execute("UPDATE %s SET c = c + ? WHERE k = ?", 1L, 0);
+
+            flush();
+
+            // Querying, but not including the counter. Pre-CASSANDRA-11726, this made us query the counter but include
+            // it's value, which broke at merge (post-CASSANDRA-11726 are special cases to never skip values).
+            assertRows(execute("SELECT k FROM %s"), row(0));
+        }
+    }
 }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/FrozenCollectionsTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/FrozenCollectionsTest.java
index 1dccc4b..186086a 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/FrozenCollectionsTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/FrozenCollectionsTest.java
@@ -41,10 +41,12 @@
 public class FrozenCollectionsTest extends CQLTester
 {
     @BeforeClass
-    public static void setUpClass()
+    public static void setUpClass()     // overrides CQLTester.setUpClass()
     {
         // Selecting partitioner for a table is not exposed on CREATE TABLE.
         StorageService.instance.setPartitionerUnsafe(ByteOrderedPartitioner.instance);
+
+        prepareServer();
     }
 
     @Test
@@ -873,17 +875,18 @@
                              "SELECT * FROM %s WHERE c CONTAINS KEY ?", 1);
 
         // normal indexes on frozen collections don't support CONTAINS or CONTAINS KEY
-        assertInvalidMessage("Cannot restrict clustering columns by a CONTAINS relation without a secondary index",
+        assertInvalidMessage("Clustering columns can only be restricted with CONTAINS with a secondary index or filtering",
                              "SELECT * FROM %s WHERE b CONTAINS ?", 1);
 
-        assertInvalidMessage("Cannot restrict clustering columns by a CONTAINS relation without a secondary index",
-                             "SELECT * FROM %s WHERE b CONTAINS ? ALLOW FILTERING", 1);
+        assertRows(execute("SELECT * FROM %s WHERE b CONTAINS ? ALLOW FILTERING", 1),
+                   row(0, list(1, 2, 3), set(1, 2, 3), map(1, "a")),
+                   row(1, list(1, 2, 3), set(4, 5, 6), map(2, "b")));
 
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
                              "SELECT * FROM %s WHERE d CONTAINS KEY ?", 1);
 
-        assertInvalidMessage("Cannot restrict clustering columns by a CONTAINS relation without a secondary index",
-                             "SELECT * FROM %s WHERE b CONTAINS ? AND d CONTAINS KEY ? ALLOW FILTERING", 1, 1);
+        assertRows(execute("SELECT * FROM %s WHERE b CONTAINS ? AND d CONTAINS KEY ? ALLOW FILTERING", 1, 1),
+                   row(0, list(1, 2, 3), set(1, 2, 3), map(1, "a")));
 
         // index lookup on b
         assertRows(execute("SELECT * FROM %s WHERE b=?", list(1, 2, 3)),
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/JsonTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/JsonTest.java
index 2d16168..b048397 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/JsonTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/JsonTest.java
@@ -18,9 +18,10 @@
 package org.apache.cassandra.cql3.validation.entities;
 
 import org.apache.cassandra.cql3.Json;
-import org.apache.cassandra.dht.ByteOrderedPartitioner;
-import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.Duration;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.dht.ByteOrderedPartitioner;
 
 import org.apache.cassandra.serializers.SimpleDateSerializer;
 import org.apache.cassandra.serializers.TimeSerializer;
@@ -37,10 +38,11 @@
 import java.text.SimpleDateFormat;
 import java.util.*;
 import java.util.concurrent.*;
+
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
-public class
-JsonTest extends CQLTester
+public class JsonTest extends CQLTester
 {
     @BeforeClass
     public static void setUp()
@@ -172,11 +174,12 @@
     public void testSelectJsonWithPagingWithFrozenUDT() throws Throwable
     {
         final UUID uuid = UUID.fromString("2dd2cd62-6af3-4cf6-96fc-91b9ab62eedc");
-        final Object partitionKey = userType(1, 2, list("1", "2"));
 
         String typeName = createType("CREATE TYPE %s (a int, b int, c list<text>)");
         createTable("CREATE TABLE %s (k1 frozen<" + typeName + ">, c1 frozen<tuple<uuid, int>>, value int, PRIMARY KEY (k1, c1))");
 
+        final Object partitionKey = userType("a", 1, "b", 2, "c", list("1", "2"));
+
         // prepare data
         for (int i = 1; i < 5; i++)
         execute("INSERT INTO %s (k1, c1, value) VALUES (?, ?, ?)", partitionKey, tuple(uuid, i), i);
@@ -231,8 +234,8 @@
                 "mapval map<ascii, int>," +
                 "frozenmapval frozen<map<ascii, int>>," +
                 "tupleval frozen<tuple<int, ascii, uuid>>," +
-                "udtval frozen<" + typeName + ">)");
-
+                "udtval frozen<" + typeName + ">," +
+                "durationval duration)");
 
         // fromJson() can only be used when the receiver type is known
         assertInvalidMessage("fromJson() cannot be used in the selection clause", "SELECT fromJson(asciival) FROM %s", 0, 0);
@@ -665,6 +668,16 @@
                 row(0, 1, UUID.fromString("6bddc89a-5644-11e4-97fc-56847afe9799"), set("bar", "foo"))
         );
 
+        // ================ duration ================
+        execute("INSERT INTO %s (k, durationval) VALUES (?, fromJson(?))", 0, "\"53us\"");
+        assertRows(execute("SELECT k, durationval FROM %s WHERE k = ?", 0), row(0, Duration.newInstance(0, 0, 53000L)));
+
+        execute("INSERT INTO %s (k, durationval) VALUES (?, fromJson(?))", 0, "\"P2W\"");
+        assertRows(execute("SELECT k, durationval FROM %s WHERE k = ?", 0), row(0, Duration.newInstance(0, 14, 0)));
+
+        assertInvalidMessage("Unable to convert 'xyz' to a duration",
+                             "INSERT INTO %s (k, durationval) VALUES (?, fromJson(?))", 0, "\"xyz\"");
+
         // order of fields shouldn't matter
         execute("INSERT INTO %s (k, udtval) VALUES (?, fromJson(?))", 0, "{\"b\": \"6bddc89a-5644-11e4-97fc-56847afe9799\", \"a\": 1, \"c\": [\"foo\", \"bar\"]}");
         assertRows(execute("SELECT k, udtval.a, udtval.b, udtval.c FROM %s WHERE k = ?", 0),
@@ -720,7 +733,8 @@
                 "mapval map<ascii, int>, " +
                 "frozenmapval frozen<map<ascii, int>>, " +
                 "tupleval frozen<tuple<int, ascii, uuid>>," +
-                "udtval frozen<" + typeName + ">)");
+                "udtval frozen<" + typeName + ">," +
+                "durationval duration)");
 
         // toJson() can only be used in selections
         assertInvalidMessage("toJson() may only be used within the selection clause",
@@ -921,6 +935,38 @@
         assertRows(execute("SELECT k, toJson(udtval) FROM %s WHERE k = ?", 0),
                 row(0, "{\"a\": 1, \"b\": \"6bddc89a-5644-11e4-97fc-56847afe9799\", \"c\": null}")
         );
+
+        // ================ duration ================
+        execute("INSERT INTO %s (k, durationval) VALUES (?, 12µs)", 0);
+        assertRows(execute("SELECT k, toJson(durationval) FROM %s WHERE k = ?", 0), row(0, "\"12us\""));
+
+        execute("INSERT INTO %s (k, durationval) VALUES (?, P1Y1M2DT10H5M)", 0);
+        assertRows(execute("SELECT k, toJson(durationval) FROM %s WHERE k = ?", 0), row(0, "\"1y1mo2d10h5m\""));
+    }
+
+    @Test
+    public void testJsonWithGroupBy() throws Throwable
+    {
+        // tests SELECT JSON statements
+        createTable("CREATE TABLE %s (k int, c int, v int, PRIMARY KEY (k, c))");
+        execute("INSERT INTO %s (k, c, v) VALUES (0, 0, 0)");
+        execute("INSERT INTO %s (k, c, v) VALUES (0, 1, 1)");
+        execute("INSERT INTO %s (k, c, v) VALUES (1, 0, 1)");
+
+        assertRows(execute("SELECT JSON * FROM %s GROUP BY k"),
+                   row("{\"k\": 0, \"c\": 0, \"v\": 0}"),
+                   row("{\"k\": 1, \"c\": 0, \"v\": 1}")
+        );
+
+        assertRows(execute("SELECT JSON k, c, v FROM %s GROUP BY k"),
+                   row("{\"k\": 0, \"c\": 0, \"v\": 0}"),
+                   row("{\"k\": 1, \"c\": 0, \"v\": 1}")
+        );
+
+        assertRows(execute("SELECT JSON count(*) FROM %s GROUP BY k"),
+                row("{\"count\": 2}"),
+                row("{\"count\": 1}")
+        );
     }
 
     @Test
@@ -1020,6 +1066,52 @@
     }
 
     @Test
+    public void testInsertJsonSyntaxDefaultUnset() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int primary key, v1 int, v2 int)");
+        execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"v1\": 0, \"v2\": 0}");
+
+        // leave v1 unset
+        execute("INSERT INTO %s JSON ? DEFAULT UNSET", "{\"k\": 0, \"v2\": 2}");
+        assertRows(execute("SELECT * FROM %s"),
+                row(0, 0, 2)
+        );
+
+        // explicit specification DEFAULT NULL
+        execute("INSERT INTO %s JSON ? DEFAULT NULL", "{\"k\": 0, \"v2\": 2}");
+        assertRows(execute("SELECT * FROM %s"),
+                row(0, null, 2)
+        );
+
+        // implicitly setting v2 to null
+        execute("INSERT INTO %s JSON ? DEFAULT NULL", "{\"k\": 0}");
+        assertRows(execute("SELECT * FROM %s"),
+                row(0, null, null)
+        );
+
+        // mix setting null explicitly with default unset:
+        // set values for all fields
+        execute("INSERT INTO %s JSON ?", "{\"k\": 1, \"v1\": 1, \"v2\": 1}");
+        // explicitly set v1 to null while leaving v2 unset which retains its value
+        execute("INSERT INTO %s JSON ? DEFAULT UNSET", "{\"k\": 1, \"v1\": null}");
+        assertRows(execute("SELECT * FROM %s WHERE k=1"),
+                row(1, null, 1)
+        );
+
+        // test string literal instead of bind marker
+        execute("INSERT INTO %s JSON '{\"k\": 2, \"v1\": 2, \"v2\": 2}'");
+        // explicitly set v1 to null while leaving v2 unset which retains its value
+        execute("INSERT INTO %s JSON '{\"k\": 2, \"v1\": null}' DEFAULT UNSET");
+        assertRows(execute("SELECT * FROM %s WHERE k=2"),
+                row(2, null, 2)
+        );
+        execute("INSERT INTO %s JSON '{\"k\": 2}' DEFAULT NULL");
+        assertRows(execute("SELECT * FROM %s WHERE k=2"),
+                row(2, null, null)
+        );
+    }
+
+    @Test
     public void testCaseSensitivity() throws Throwable
     {
         createTable("CREATE TABLE %s (k int primary key, \"Foo\" int)");
@@ -1267,6 +1359,20 @@
         Assert.assertTrue(executor.awaitTermination(30, TimeUnit.SECONDS));
     }
 
+   @Test
+    public void emptyStringJsonSerializationTest() throws Throwable
+    {
+        createTable("create table %s(id INT, name TEXT, PRIMARY KEY(id));");
+        execute("insert into %s(id, name) VALUES (0, 'Foo');");
+        execute("insert into %s(id, name) VALUES (2, '');");
+        execute("insert into %s(id, name) VALUES (3, null);");
+
+        assertRows(execute("SELECT JSON * FROM %s"),
+                   row("{\"id\": 0, \"name\": \"Foo\"}"),
+                   row("{\"id\": 2, \"name\": \"\"}"),
+                   row("{\"id\": 3, \"name\": null}"));
+    }
+
     // CASSANDRA-14286
     @Test
     public void testJsonOrdering() throws Throwable
@@ -1286,6 +1392,24 @@
         assertRows(execute("SELECT JSON a FROM %s WHERE a IN (20, 100) ORDER BY b DESC"),
                    row("{\"a\": 100}"),
                    row("{\"a\": 20}"));
+
+        // Check ordering with alias 
+        assertRows(execute("SELECT JSON a, b as c FROM %s WHERE a IN (20, 100) ORDER BY b"),
+                   row("{\"a\": 20, \"c\": 30}"),
+                   row("{\"a\": 100, \"c\": 200}"));
+
+        assertRows(execute("SELECT JSON a, b as c FROM %s WHERE a IN (20, 100) ORDER BY b DESC"),
+                   row("{\"a\": 100, \"c\": 200}"),
+                   row("{\"a\": 20, \"c\": 30}"));
+
+        // Check ordering with CAST 
+        assertRows(execute("SELECT JSON a, CAST(b AS FLOAT) FROM %s WHERE a IN (20, 100) ORDER BY b"),
+                   row("{\"a\": 20, \"cast(b as float)\": 30.0}"),
+                   row("{\"a\": 100, \"cast(b as float)\": 200.0}"));
+
+        assertRows(execute("SELECT JSON a, CAST(b AS FLOAT) FROM %s WHERE a IN (20, 100) ORDER BY b DESC"),
+                   row("{\"a\": 100, \"cast(b as float)\": 200.0}"),
+                   row("{\"a\": 20, \"cast(b as float)\": 30.0}"));
     }
 
     @Test
@@ -1328,4 +1452,19 @@
         // JSON does not support NaN, Infinity and -Infinity values. Most of the parser convert them into null.
         assertRows(execute("SELECT JSON * FROM %s"), row("{\"pk\": 1, \"d1\": null, \"d2\": null, \"d3\": null, \"f1\": null, \"f2\": null, \"f3\": null}"));
     }
+
+    @Test
+    public void testDurationJsonRoundtrip() throws Throwable
+    {
+        createTable("CREATE TABLE %s (pk int PRIMARY KEY, d duration)");
+        execute("INSERT INTO %s (pk, d) VALUES (1, 6h40m)");
+        UntypedResultSet res = execute("SELECT JSON * FROM %s WHERE pk = 1");
+        UntypedResultSet.Row r = res.one();
+        String json = r.getString("[json]");
+        execute("DELETE FROM %s WHERE pk = 1");
+        execute("INSERT INTO %s JSON '"+json+"'");
+        res = execute("SELECT JSON * FROM %s WHERE pk = 1");
+        assertEquals(json, res.one().getString("[json]"));
+
+    }
 }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/RowUpdateBuilderTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/RowUpdateBuilderTest.java
deleted file mode 100644
index afe2455..0000000
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/RowUpdateBuilderTest.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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.validation.entities;
-
-import org.junit.Test;
-
-import org.apache.cassandra.Util;
-import org.apache.cassandra.cql3.CQLTester;
-import org.apache.cassandra.db.Mutation;
-import org.apache.cassandra.db.RowUpdateBuilder;
-import org.apache.cassandra.utils.FBUtilities;
-
-// see CASSANDRA-9743, CASSANDRA-9746
-public class RowUpdateBuilderTest extends CQLTester
-{
-    @Test
-    public void testAddListEntryDurable() throws Throwable
-    {
-        testAddListEntry(false);
-    }
-
-    @Test
-    public void testAddListEntryTransient() throws Throwable
-    {
-        testAddListEntry(true);
-    }
-
-    public void testAddListEntry(boolean skipCommitLog) throws Throwable
-    {
-        createTable("CREATE TABLE %s ("
-                    + "pk text,"
-                    + "ck text,"
-                    + "l1 list<int>,"
-                    + "l2 list<int>,"
-                    + "PRIMARY KEY ((pk), ck))");
-
-        long timestamp = FBUtilities.timestampMicros();
-
-        Mutation mutation = new Mutation(keyspace(), Util.dk("test"));
-        addToMutation("row1", timestamp, mutation);
-        addToMutation("row2", timestamp, mutation);
-
-        if (skipCommitLog)
-            mutation.applyUnsafe();
-        else
-            mutation.apply();
-
-        assertRowCount(execute("SELECT ck FROM %s"), 2);
-    }
-
-    private void addToMutation(String typeName, long timestamp, Mutation mutation)
-    {
-        RowUpdateBuilder adder = new RowUpdateBuilder(getCurrentColumnFamilyStore().metadata, timestamp, mutation)
-                                 .clustering(typeName);
-
-        for (int i = 0; i < 2; i++)
-        {
-            adder.addListEntry("l1", i)
-                 .addListEntry("l2", i);
-        }
-
-        adder.build();
-    }
-}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexOnStaticColumnTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexOnStaticColumnTest.java
new file mode 100644
index 0000000..f69d8d5
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexOnStaticColumnTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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.validation.entities;
+
+import org.junit.Test;
+import org.apache.cassandra.cql3.CQLTester;
+
+public class SecondaryIndexOnStaticColumnTest extends CQLTester
+{
+    @Test
+    public void testSimpleStaticColumn() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, name text, age int static, PRIMARY KEY (id, name))");
+
+        createIndex("CREATE INDEX static_age on %s(age)");
+        int id1 = 1, id2 = 2, age1 = 24, age2 = 32;
+        String name1A = "Taylor", name1B = "Swift",
+               name2 = "Jamie";
+
+        execute("INSERT INTO %s (id, name, age) VALUES (?, ?, ?)", id1, name1A, age1);
+        execute("INSERT INTO %s (id, name, age) VALUES (?, ?, ?)", id1, name1B, age1);
+        execute("INSERT INTO %s (id, name, age) VALUES (?, ?, ?)", id2, name2, age2);
+
+        assertRows(execute("SELECT id, name, age FROM %s WHERE age=?", age1),
+              row(id1, name1B, age1), row(id1, name1A, age1));
+        assertRows(execute("SELECT id, name, age FROM %s WHERE age=?", age2),
+                row(id2, name2, age2));
+
+        // Update the rows. Validate that updated values will be reflected in the index.
+        int newAge1 = 40;
+        execute("UPDATE %s SET age = ? WHERE id = ?", newAge1, id1);
+        assertEmpty(execute("SELECT id, name, age FROM %s WHERE age=?", age1));
+        assertRows(execute("SELECT id, name, age FROM %s WHERE age=?", newAge1),
+                row(id1, name1B, newAge1), row(id1, name1A, newAge1));
+        execute("DELETE FROM %s WHERE id = ?", id2);
+        assertEmpty(execute("SELECT id, name, age FROM %s WHERE age=?", age2));
+    }
+
+    @Test
+    public void testIndexOnCompoundRowKey() throws Throwable
+    {
+        createTable("CREATE TABLE %s (interval text, seq int, id int, severity int static, PRIMARY KEY ((interval, seq), id) ) WITH CLUSTERING ORDER BY (id DESC)");
+
+        execute("CREATE INDEX ON %s (severity)");
+
+        execute("insert into %s (interval, seq, id , severity) values('t',1, 3, 10)");
+        execute("insert into %s (interval, seq, id , severity) values('t',1, 4, 10)");
+        execute("insert into %s (interval, seq, id , severity) values('t',2, 3, 10)");
+        execute("insert into %s (interval, seq, id , severity) values('t',2, 4, 10)");
+        execute("insert into %s (interval, seq, id , severity) values('m',1, 3, 11)");
+        execute("insert into %s (interval, seq, id , severity) values('m',1, 4, 11)");
+        execute("insert into %s (interval, seq, id , severity) values('m',2, 3, 11)");
+        execute("insert into %s (interval, seq, id , severity) values('m',2, 4, 11)");
+
+        assertRows(execute("select * from %s where severity = 10 and interval = 't' and seq = 1"),
+                   row("t", 1, 4, 10), row("t", 1, 3, 10));
+    }
+
+    @Test
+    public void testIndexOnCollections() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int, v int, l list<int> static, s set<text> static, m map<text, int> static, PRIMARY KEY (k, v))");
+
+        createIndex("CREATE INDEX ON %s (l)");
+        createIndex("CREATE INDEX ON %s (s)");
+        createIndex("CREATE INDEX ON %s (m)");
+        createIndex("CREATE INDEX ON %s (keys(m))");
+
+        execute("INSERT INTO %s (k, v, l, s, m) VALUES (0, 0, [1, 2],    {'a'},      {'a' : 1, 'b' : 2})");
+        execute("INSERT INTO %s (k, v)          VALUES (0, 1)                                  ");
+        execute("INSERT INTO %s (k, v, l, s, m) VALUES (1, 0, [4, 5],    {'d'},      {'b' : 1, 'c' : 4})");
+
+        // lists
+        assertRows(execute("SELECT k, v FROM %s WHERE l CONTAINS 1"), row(0, 0), row(0, 1));
+        assertEmpty(execute("SELECT k, v FROM %s WHERE k = 1 AND l CONTAINS 1"));
+        assertRows(execute("SELECT k, v FROM %s WHERE l CONTAINS 4"), row(1, 0));
+        assertEmpty(execute("SELECT k, v FROM %s WHERE l CONTAINS 6"));
+
+        // update lists
+        execute("UPDATE %s SET l = l + [3] WHERE k = ?", 0);
+        assertRows(execute("SELECT k, v FROM %s WHERE l CONTAINS 3"), row(0, 0), row(0, 1));
+
+        // sets
+        assertRows(execute("SELECT k, v FROM %s WHERE s CONTAINS 'a'"), row(0, 0), row(0, 1));
+        assertRows(execute("SELECT k, v FROM %s WHERE k = 0 AND s CONTAINS 'a'"), row(0, 0), row(0, 1));
+        assertRows(execute("SELECT k, v FROM %s WHERE s CONTAINS 'd'"), row(1, 0));
+        assertEmpty(execute("SELECT k, v FROM %s  WHERE s CONTAINS 'e'"));
+
+        // update sets
+        execute("UPDATE %s SET s = s + {'b'} WHERE k = ?", 0);
+        assertRows(execute("SELECT k, v FROM %s WHERE s CONTAINS 'b'"), row(0, 0), row(0, 1));
+        execute("UPDATE %s SET s = s - {'a'} WHERE k = ?", 0);
+        assertEmpty(execute("SELECT k, v FROM %s WHERE s CONTAINS 'a'"));
+
+        // maps
+        assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS 1"), row(1, 0), row(0, 0), row(0, 1));
+        assertRows(execute("SELECT k, v FROM %s WHERE k = 0 AND m CONTAINS 1"), row(0, 0), row(0, 1));
+        assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS 4"), row(1, 0));
+        assertEmpty(execute("SELECT k, v FROM %s  WHERE m CONTAINS 5"));
+
+        assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS KEY 'b'"), row(1, 0), row(0, 0), row(0, 1));
+        assertRows(execute("SELECT k, v FROM %s WHERE k = 0 AND m CONTAINS KEY 'b'"), row(0, 0), row(0, 1));
+        assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS KEY 'c'"), row(1, 0));
+        assertEmpty(execute("SELECT k, v FROM %s  WHERE m CONTAINS KEY 'd'"));
+
+        // update maps.
+        execute("UPDATE %s SET m['c'] = 5 WHERE k = 0");
+        assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS 5"), row(0, 0), row(0, 1));
+        assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS KEY 'c'"), row(1, 0), row(0, 0), row(0, 1));
+        execute("DELETE m['a'] FROM %s WHERE k = 0");
+        assertEmpty(execute("SELECT k, v FROM %s  WHERE m CONTAINS KEY 'a'"));
+    }
+
+    @Test
+    public void testIndexOnFrozenCollections() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int, v int, l frozen<list<int>> static, s frozen<set<text>> static, m frozen<map<text, int>> static, PRIMARY KEY (k, v))");
+
+        createIndex("CREATE INDEX ON %s (FULL(l))");
+        createIndex("CREATE INDEX ON %s (FULL(s))");
+        createIndex("CREATE INDEX ON %s (FULL(m))");
+
+        execute("INSERT INTO %s (k, v, l, s, m) VALUES (0, 0, [1, 2],    {'a'},      {'a' : 1, 'b' : 2})");
+        execute("INSERT INTO %s (k, v)          VALUES (0, 1)                                  ");
+        execute("INSERT INTO %s (k, v, l, s, m) VALUES (1, 0, [4, 5],    {'d'},      {'b' : 1, 'c' : 4})");
+        execute("UPDATE %s SET l=[3], s={'3'}, m={'3': 3} WHERE k=3" );
+
+        // lists
+        assertRows(execute("SELECT k, v FROM %s WHERE l = [1, 2]"), row(0, 0), row(0, 1));
+        assertEmpty(execute("SELECT k, v FROM %s WHERE k = 1 AND l = [1, 2]"));
+        assertEmpty(execute("SELECT k, v FROM %s WHERE l = [4]"));
+        assertRows(execute("SELECT k, v FROM %s WHERE l = [3]"), row(3, null));
+
+        // update lists
+        execute("UPDATE %s SET l = [1, 2, 3] WHERE k = ?", 0);
+        assertEmpty(execute("SELECT k, v FROM %s WHERE l = [1, 2]"));
+        assertRows(execute("SELECT k, v FROM %s WHERE l = [1, 2, 3]"), row(0, 0), row(0, 1));
+
+        // sets
+        assertRows(execute("SELECT k, v FROM %s WHERE s = {'a'}"), row(0, 0), row(0, 1));
+        assertEmpty(execute("SELECT k, v FROM %s WHERE k = 1 AND s = {'a'}"));
+        assertEmpty(execute("SELECT k, v FROM %s WHERE s = {'b'}"));
+        assertRows(execute("SELECT k, v FROM %s WHERE s = {'3'}"), row(3, null));
+
+        // update sets
+        execute("UPDATE %s SET s = {'a', 'b'} WHERE k = ?", 0);
+        assertEmpty(execute("SELECT k, v FROM %s WHERE s = {'a'}"));
+        assertRows(execute("SELECT k, v FROM %s WHERE s = {'a', 'b'}"), row(0, 0), row(0, 1));
+
+        // maps
+        assertRows(execute("SELECT k, v FROM %s WHERE m = {'a' : 1, 'b' : 2}"), row(0, 0), row(0, 1));
+        assertEmpty(execute("SELECT k, v FROM %s WHERE k = 1 AND m = {'a' : 1, 'b' : 2}"));
+        assertEmpty(execute("SELECT k, v FROM %s WHERE m = {'a' : 1, 'b' : 3}"));
+        assertEmpty(execute("SELECT k, v FROM %s WHERE m = {'a' : 1, 'c' : 2}"));
+        assertRows(execute("SELECT k, v FROM %s WHERE m = {'3': 3}"), row(3, null));
+
+        // update maps.
+        execute("UPDATE %s SET m = {'a': 2, 'b': 3} WHERE k = ?", 0);
+        assertEmpty(execute("SELECT k, v FROM %s WHERE m = {'a': 1, 'b': 2}"));
+        assertRows(execute("SELECT k, v FROM %s WHERE m = {'a': 2, 'b': 3}"), row(0, 0), row(0, 1));
+    }
+
+    @Test
+    public void testStaticIndexAndNonStaticIndex() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, company text, age int static, salary int, PRIMARY KEY(id, company))");
+        createIndex("CREATE INDEX on %s(age)");
+        createIndex("CREATE INDEX on %s(salary)");
+
+        String company1 = "company1", company2 = "company2";
+
+        execute("INSERT INTO %s(id, company, age, salary) VALUES(?, ?, ?, ?)", 1, company1, 20, 1000);
+        execute("INSERT INTO %s(id, company,      salary) VALUES(?, ?,    ?)", 1, company2,     2000);
+        execute("INSERT INTO %s(id, company, age, salary) VALUES(?, ?, ?, ?)", 2, company1, 40, 2000);
+
+        assertRows(execute("SELECT id, company, age, salary FROM %s WHERE age = 20 AND salary = 2000 ALLOW FILTERING"),
+                   row(1, company2, 20, 2000));
+    }
+
+    @Test
+    public void testIndexOnUDT() throws Throwable
+    {
+        String typeName = createType("CREATE TYPE %s (street text, city text)");
+
+        createTable(String.format(
+            "CREATE TABLE %%s (id int, company text, home frozen<%s> static, price int, PRIMARY KEY(id, company))",
+            typeName));
+        createIndex("CREATE INDEX on %s(home)");
+
+        String addressString = "{street: 'Centre', city: 'C'}";
+        String companyName = "Random";
+
+        execute("INSERT INTO %s(id, company, home, price) "
+                + "VALUES(1, '" + companyName + "', " + addressString + ", 10000)");
+        assertRows(execute("SELECT id, company FROM %s WHERE home = " + addressString), row(1, companyName));
+        String newAddressString = "{street: 'Fifth', city: 'P'}";
+
+        execute("UPDATE %s SET home = " + newAddressString + " WHERE id = 1");
+        assertEmpty(execute("SELECT id, company FROM %s WHERE home = " + addressString));
+        assertRows(execute("SELECT id, company FROM %s WHERE home = " + newAddressString), row(1, companyName));
+    }
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java
index 208b4dc..6a0c23c 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java
@@ -44,6 +44,7 @@
 import org.apache.cassandra.index.SecondaryIndexManager;
 import org.apache.cassandra.index.StubIndex;
 import org.apache.cassandra.index.internal.CustomCassandraIndex;
+import org.apache.cassandra.index.sasi.SASIIndex;
 import org.apache.cassandra.schema.IndexMetadata;
 import org.apache.cassandra.service.ClientState;
 import org.apache.cassandra.transport.messages.ResultMessage;
@@ -173,10 +174,12 @@
         execute("INSERT INTO %s (userid, firstname, lastname, age) VALUES (?, 'Frodo', 'Baggins', 32)", id1);
         execute("UPDATE %s SET firstname = 'Samwise', lastname = 'Gamgee', age = 33 WHERE userid = ?", id2);
 
-        assertEmpty(execute("SELECT firstname FROM %s WHERE userid = ? AND age = 33", id1));
+        beforeAndAfterFlush(() -> {
+            assertEmpty(execute("SELECT firstname FROM %s WHERE userid = ? AND age = 33", id1));
 
-        assertRows(execute("SELECT firstname FROM %s WHERE userid = ? AND age = 33", id2),
-                   row("Samwise"));
+            assertRows(execute("SELECT firstname FROM %s WHERE userid = ? AND age = 33", id2),
+                       row("Samwise"));
+        });
     }
 
     /**
@@ -194,9 +197,11 @@
         execute("INSERT INTO %s (id, birth_year) VALUES ('Paul', 24)");
         execute("INSERT INTO %s (id, birth_year) VALUES ('Bob', 42)");
 
-        assertRows(execute("SELECT id FROM %s WHERE birth_year = 42"),
-                   row("Tom"),
-                   row("Bob"));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT id FROM %s WHERE birth_year = 42"),
+                       row("Tom"),
+                       row("Bob"));
+        });
 
         execute("DROP INDEX %s_birth_year_idx");
 
@@ -220,8 +225,10 @@
 
         assertInvalid("SELECT * FROM %s WHERE setid = 0 AND row < 1");
 
-        assertRows(execute("SELECT * FROM %s WHERE setid = 0 AND row < 1 ALLOW FILTERING"),
-                   row(0, 0, 0));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE setid = 0 AND row < 1 ALLOW FILTERING"),
+                       row(0, 0, 0));
+        });
     }
 
     /**
@@ -266,27 +273,33 @@
 
         assertTrue(waitForIndex(keyspace(), tableName, "authoridx"));
 
-        assertRows(execute("SELECT blog_id, timestamp FROM %s WHERE author = 'bob'"),
-                   row(1, 0),
-                   row(0, 0),
-                   row(0, 2));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT blog_id, timestamp FROM %s WHERE author = 'bob'"),
+                       row(1, 0),
+                       row(0, 0),
+                       row(0, 2));
+        });
 
         execute("INSERT INTO %s (blog_id, timestamp, author, content) VALUES (?, ?, ?, ?)", 1, 1, "tom", "6th post");
         execute("INSERT INTO %s (blog_id, timestamp, author, content) VALUES (?, ?, ?, ?)", 1, 2, "tom", "7th post");
         execute("INSERT INTO %s (blog_id, timestamp, author, content) VALUES (?, ?, ?, ?)", 1, 3, "bob", "8th post");
 
-        assertRows(execute("SELECT blog_id, timestamp FROM %s WHERE author = 'bob'"),
-                   row(1, 0),
-                   row(1, 3),
-                   row(0, 0),
-                   row(0, 2));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT blog_id, timestamp FROM %s WHERE author = 'bob'"),
+                       row(1, 0),
+                       row(1, 3),
+                       row(0, 0),
+                       row(0, 2));
+        });
 
         execute("DELETE FROM %s WHERE blog_id = 0 AND timestamp = 2");
 
-        assertRows(execute("SELECT blog_id, timestamp FROM %s WHERE author = 'bob'"),
-                   row(1, 0),
-                   row(1, 3),
-                   row(0, 0));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT blog_id, timestamp FROM %s WHERE author = 'bob'"),
+                       row(1, 0),
+                       row(1, 3),
+                       row(0, 0));
+        });
     }
 
     /**
@@ -336,17 +349,19 @@
         execute("INSERT INTO %s (pk0, pk1, ck0, ck1, ck2, value) VALUES (4, 5, 0, 1, 2, 3)");
         execute("INSERT INTO %s (pk0, pk1, ck0, ck1, ck2, value) VALUES (5, 0, 1, 2, 3, 4)");
 
-        assertRows(execute("SELECT value FROM %s WHERE pk0 = 2"),
-                   row(1));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT value FROM %s WHERE pk0 = 2"),
+                       row(1));
 
-        assertRows(execute("SELECT value FROM %s WHERE ck0 = 0"),
-                   row(3));
+            assertRows(execute("SELECT value FROM %s WHERE ck0 = 0"),
+                       row(3));
 
-        assertRows(execute("SELECT value FROM %s WHERE pk0 = 3 AND pk1 = 4 AND ck1 = 0"),
-                   row(2));
+            assertRows(execute("SELECT value FROM %s WHERE pk0 = 3 AND pk1 = 4 AND ck1 = 0"),
+                       row(2));
 
-        assertRows(execute("SELECT value FROM %s WHERE pk0 = 5 AND pk1 = 0 AND ck0 = 1 AND ck2 = 3 ALLOW FILTERING"),
-                   row(4));
+            assertRows(execute("SELECT value FROM %s WHERE pk0 = 5 AND pk1 = 0 AND ck0 = 1 AND ck2 = 3 ALLOW FILTERING"),
+                       row(4));
+        });
     }
 
     /**
@@ -369,8 +384,11 @@
         execute("insert into %s (interval, seq, id , severity) values('t',2, 3, 1)");
         execute("insert into %s (interval, seq, id , severity) values('t',2, 4, 2)");
 
-        assertRows(execute("select * from %s where severity = 3 and interval = 't' and seq =1"),
-                   row("t", 1, 4, 3));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("select * from %s where severity = 3 and interval = 't' and seq =1"),
+                       row("t", 1, 4, 3));
+
+        });
     }
 
     /**
@@ -401,23 +419,25 @@
         execute("INSERT INTO %s (k, v, l, s, m) VALUES (1, 0, [1, 2, 4], {},         {'b' : 1})");
         execute("INSERT INTO %s (k, v, l, s, m) VALUES (1, 1, [4, 5],    {'d'},      {'a' : 1, 'b' : 3})");
 
-        // lists
-        assertRows(execute("SELECT k, v FROM %s WHERE l CONTAINS 1"), row(1, 0), row(0, 0), row(0, 2));
-        assertRows(execute("SELECT k, v FROM %s WHERE k = 0 AND l CONTAINS 1"), row(0, 0), row(0, 2));
-        assertRows(execute("SELECT k, v FROM %s WHERE l CONTAINS 2"), row(1, 0), row(0, 0));
-        assertEmpty(execute("SELECT k, v FROM %s WHERE l CONTAINS 6"));
+        beforeAndAfterFlush(() -> {
+            // lists
+            assertRows(execute("SELECT k, v FROM %s WHERE l CONTAINS 1"), row(1, 0), row(0, 0), row(0, 2));
+            assertRows(execute("SELECT k, v FROM %s WHERE k = 0 AND l CONTAINS 1"), row(0, 0), row(0, 2));
+            assertRows(execute("SELECT k, v FROM %s WHERE l CONTAINS 2"), row(1, 0), row(0, 0));
+            assertEmpty(execute("SELECT k, v FROM %s WHERE l CONTAINS 6"));
 
-        // sets
-        assertRows(execute("SELECT k, v FROM %s WHERE s CONTAINS 'a'"), row(0, 0), row(0, 2));
-        assertRows(execute("SELECT k, v FROM %s WHERE k = 0 AND s CONTAINS 'a'"), row(0, 0), row(0, 2));
-        assertRows(execute("SELECT k, v FROM %s WHERE s CONTAINS 'd'"), row(1, 1));
-        assertEmpty(execute("SELECT k, v FROM %s  WHERE s CONTAINS 'e'"));
+            // sets
+            assertRows(execute("SELECT k, v FROM %s WHERE s CONTAINS 'a'"), row(0, 0), row(0, 2));
+            assertRows(execute("SELECT k, v FROM %s WHERE k = 0 AND s CONTAINS 'a'"), row(0, 0), row(0, 2));
+            assertRows(execute("SELECT k, v FROM %s WHERE s CONTAINS 'd'"), row(1, 1));
+            assertEmpty(execute("SELECT k, v FROM %s  WHERE s CONTAINS 'e'"));
 
-        // maps
-        assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS 1"), row(1, 0), row(1, 1), row(0, 0), row(0, 1));
-        assertRows(execute("SELECT k, v FROM %s WHERE k = 0 AND m CONTAINS 1"), row(0, 0), row(0, 1));
-        assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS 2"), row(0, 1));
-        assertEmpty(execute("SELECT k, v FROM %s  WHERE m CONTAINS 4"));
+            // maps
+            assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS 1"), row(1, 0), row(1, 1), row(0, 0), row(0, 1));
+            assertRows(execute("SELECT k, v FROM %s WHERE k = 0 AND m CONTAINS 1"), row(0, 0), row(0, 1));
+            assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS 2"), row(0, 1));
+            assertEmpty(execute("SELECT k, v FROM %s  WHERE m CONTAINS 4"));
+        });
     }
 
     @Test
@@ -476,10 +496,12 @@
         execute("INSERT INTO %s (k, v, m) VALUES (1, 1, {'a' : 1, 'b' : 3})");
 
         // maps
-        assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS KEY 'a'"), row(1, 1), row(0, 0), row(0, 1));
-        assertRows(execute("SELECT k, v FROM %s WHERE k = 0 AND m CONTAINS KEY 'a'"), row(0, 0), row(0, 1));
-        assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS KEY 'c'"), row(0, 2));
-        assertEmpty(execute("SELECT k, v FROM %s  WHERE m CONTAINS KEY 'd'"));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS KEY 'a'"), row(1, 1), row(0, 0), row(0, 1));
+            assertRows(execute("SELECT k, v FROM %s WHERE k = 0 AND m CONTAINS KEY 'a'"), row(0, 0), row(0, 1));
+            assertRows(execute("SELECT k, v FROM %s WHERE m CONTAINS KEY 'c'"), row(0, 2));
+            assertEmpty(execute("SELECT k, v FROM %s  WHERE m CONTAINS KEY 'd'"));
+        });
     }
 
     /**
@@ -502,10 +524,12 @@
         execute("INSERT INTO %s (k1, k2, v) VALUES (2, 1, 8)");
         execute("INSERT INTO %s (k1, k2, v) VALUES (3, 0, 1)");
 
-        assertRows(execute("SELECT * FROM %s WHERE k2 = 0 AND v >= 2 ALLOW FILTERING"),
-                   row(2, 0, 7),
-                   row(0, 0, 3),
-                   row(1, 0, 4));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE k2 = 0 AND v >= 2 ALLOW FILTERING"),
+                       row(2, 0, 7),
+                       row(0, 0, 3),
+                       row(1, 0, 4));
+        });
     }
 
     /**
@@ -520,11 +544,15 @@
         createIndex("create index ON %s (app_name)");
         createIndex("create index ON %s (last_access)");
 
-        assertRows(execute("select count(*) from %s where app_name='foo' and account='bar' and last_access > 4 allow filtering"), row(0L));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("select count(*) from %s where app_name='foo' and account='bar' and last_access > 4 allow filtering"), row(0L));
+        });
 
         execute("insert into %s (username, session_id, app_name, account, last_access, created_on) values ('toto', 'foo', 'foo', 'bar', 12, 13)");
 
-        assertRows(execute("select count(*) from %s where app_name='foo' and account='bar' and last_access > 4 allow filtering"), row(1L));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("select count(*) from %s where app_name='foo' and account='bar' and last_access > 4 allow filtering"), row(1L));
+        });
     }
 
     @Test
@@ -680,6 +708,29 @@
     }
 
     @Test
+    public void testIndexOnPartitionKeyWithStaticColumnAndNoRows() throws Throwable
+    {
+        createTable("CREATE TABLE %s (pk1 int, pk2 int, c int, s int static, v int, PRIMARY KEY((pk1, pk2), c))");
+        createIndex("CREATE INDEX ON %s (pk2)");
+        execute("INSERT INTO %s (pk1, pk2, c, s, v) VALUES (?, ?, ?, ?, ?)", 1, 1, 1, 9, 1);
+        execute("INSERT INTO %s (pk1, pk2, c, s, v) VALUES (?, ?, ?, ?, ?)", 1, 1, 2, 9, 2);
+        execute("INSERT INTO %s (pk1, pk2, s) VALUES (?, ?, ?)", 2, 1, 9);
+        execute("INSERT INTO %s (pk1, pk2, c, s, v) VALUES (?, ?, ?, ?, ?)", 3, 1, 1, 9, 1);
+
+        assertRows(execute("SELECT * FROM %s WHERE pk2 = ?", 1),
+                   row(2, 1, null, 9, null),
+                   row(1, 1, 1, 9, 1),
+                   row(1, 1, 2, 9, 2),
+                   row(3, 1, 1, 9, 1));
+
+        execute("UPDATE %s SET s=?, v=? WHERE pk1=? AND pk2=? AND c=?", 9, 1, 1, 10, 2);
+        assertRows(execute("SELECT * FROM %s WHERE pk2 = ?", 10), row(1, 10, 2, 9, 1));
+
+        execute("UPDATE %s SET s=? WHERE pk1=? AND pk2=?", 9, 1, 20);
+        assertRows(execute("SELECT * FROM %s WHERE pk2 = ?", 20), row(1, 20, null, 9, null));
+    }
+
+    @Test
     public void testIndexOnClusteringColumnInsertValueOver64k() throws Throwable
     {
         createTable("CREATE TABLE %s(a int, b int, c blob, PRIMARY KEY (a, b))");
@@ -726,6 +777,65 @@
                    "APPLY BATCH", map);
     }
 
+    @Test
+    public void prepareStatementsWithLIKEClauses() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, c1 text, c2 text, v1 text, v2 text, v3 int, PRIMARY KEY (a, c1, c2))");
+        createIndex(String.format("CREATE CUSTOM INDEX c1_idx on %%s(c1) USING '%s' WITH OPTIONS = {'mode' : 'PREFIX'}",
+                                  SASIIndex.class.getName()));
+        createIndex(String.format("CREATE CUSTOM INDEX c2_idx on %%s(c2) USING '%s' WITH OPTIONS = {'mode' : 'CONTAINS'}",
+                                  SASIIndex.class.getName()));
+        createIndex(String.format("CREATE CUSTOM INDEX v1_idx on %%s(v1) USING '%s' WITH OPTIONS = {'mode' : 'PREFIX'}",
+                                  SASIIndex.class.getName()));
+        createIndex(String.format("CREATE CUSTOM INDEX v2_idx on %%s(v2) USING '%s' WITH OPTIONS = {'mode' : 'CONTAINS'}",
+                                  SASIIndex.class.getName()));
+        createIndex(String.format("CREATE CUSTOM INDEX v3_idx on %%s(v3) USING '%s'", SASIIndex.class.getName()));
+
+        forcePreparedValues();
+        // prefix mode indexes support prefix/contains/matches
+        assertInvalidMessage("c1 LIKE '%<term>' abc is only supported on properly indexed columns",
+                             "SELECT * FROM %s WHERE c1 LIKE ?",
+                             "%abc");
+        assertInvalidMessage("c1 LIKE '%<term>%' abc is only supported on properly indexed columns",
+                             "SELECT * FROM %s WHERE c1 LIKE ?",
+                             "%abc%");
+        execute("SELECT * FROM %s WHERE c1 LIKE ?", "abc%");
+        execute("SELECT * FROM %s WHERE c1 LIKE ?", "abc");
+        assertInvalidMessage("v1 LIKE '%<term>' abc is only supported on properly indexed columns",
+                             "SELECT * FROM %s WHERE v1 LIKE ?",
+                             "%abc");
+        assertInvalidMessage("v1 LIKE '%<term>%' abc is only supported on properly indexed columns",
+                             "SELECT * FROM %s WHERE v1 LIKE ?",
+                             "%abc%");
+        execute("SELECT * FROM %s WHERE v1 LIKE ?", "abc%");
+        execute("SELECT * FROM %s WHERE v1 LIKE ?", "abc");
+
+        // contains mode indexes support prefix/suffix/contains/matches
+        execute("SELECT * FROM %s WHERE c2 LIKE ?", "abc%");
+        execute("SELECT * FROM %s WHERE c2 LIKE ?", "%abc");
+        execute("SELECT * FROM %s WHERE c2 LIKE ?", "%abc%");
+        execute("SELECT * FROM %s WHERE c2 LIKE ?", "abc");
+        execute("SELECT * FROM %s WHERE v2 LIKE ?", "abc%");
+        execute("SELECT * FROM %s WHERE v2 LIKE ?", "%abc");
+        execute("SELECT * FROM %s WHERE v2 LIKE ?", "%abc%");
+        execute("SELECT * FROM %s WHERE v2 LIKE ?", "abc");
+
+        // LIKE is not supported on indexes of non-literal values
+        // this is rejected before binding, so the value isn't available in the error message
+        assertInvalidMessage("LIKE restriction is only supported on properly indexed columns. v3 LIKE ? is not valid",
+                             "SELECT * FROM %s WHERE v3 LIKE ?",
+                             "%abc");
+        assertInvalidMessage("LIKE restriction is only supported on properly indexed columns. v3 LIKE ? is not valid",
+                             "SELECT * FROM %s WHERE v3 LIKE ?",
+                             "%abc%");
+        assertInvalidMessage("LIKE restriction is only supported on properly indexed columns. v3 LIKE ? is not valid",
+                             "SELECT * FROM %s WHERE v3 LIKE ?",
+                             "%abc%");
+        assertInvalidMessage("LIKE restriction is only supported on properly indexed columns. v3 LIKE ? is not valid",
+                             "SELECT * FROM %s WHERE v3 LIKE ?",
+                             "abc");
+    }
+
     public void failInsert(String insertCQL, Object...args) throws Throwable
     {
         try
@@ -795,9 +905,6 @@
         assertInvalid("CREATE INDEX ON %s (a)");
         assertInvalid("CREATE INDEX ON %s (b)");
         assertInvalid("CREATE INDEX ON %s (c)");
-
-        createTable("CREATE TABLE %s (a int, b int, c int static , PRIMARY KEY (a, b))");
-        assertInvalid("CREATE INDEX ON %s (c)");
     }
 
     @Test
@@ -1160,76 +1267,271 @@
     }
 
     @Test
-    public void testIndexOnPartitionKeyWithStaticColumnAndNoRows() throws Throwable
+    public void testPartitionKeyWithIndex() throws Throwable
     {
-        createTable("CREATE TABLE %s (pk1 int, pk2 int, c int, s int static, v int, PRIMARY KEY((pk1, pk2), c))");
-        createIndex("CREATE INDEX ON %s (pk2)");
-        execute("INSERT INTO %s (pk1, pk2, c, s, v) VALUES (?, ?, ?, ?, ?)", 1, 1, 1, 9, 1);
-        execute("INSERT INTO %s (pk1, pk2, c, s, v) VALUES (?, ?, ?, ?, ?)", 1, 1, 2, 9, 2);
-        execute("INSERT INTO %s (pk1, pk2, s) VALUES (?, ?, ?)", 2, 1, 9);
-        execute("INSERT INTO %s (pk1, pk2, c, s, v) VALUES (?, ?, ?, ?, ?)", 3, 1, 1, 9, 1);
+        createTable("CREATE TABLE %s (a int, b int, c int, PRIMARY KEY ((a, b)))");
+        createIndex("CREATE INDEX ON %s (a);");
+        createIndex("CREATE INDEX ON %s (b);");
 
-        assertRows(execute("SELECT * FROM %s WHERE pk2 = ?", 1),
-                   row(2, 1, null, 9, null),
-                   row(1, 1, 1, 9, 1),
-                   row(1, 1, 2, 9, 2),
-                   row(3, 1, 1, 9, 1));
+        execute("INSERT INTO %s (a, b, c) VALUES (1,2,3)");
+        execute("INSERT INTO %s (a, b, c) VALUES (2,3,4)");
+        execute("INSERT INTO %s (a, b, c) VALUES (5,6,7)");
 
-        execute("UPDATE %s SET s=?, v=? WHERE pk1=? AND pk2=? AND c=?", 9, 1, 1, 10, 2);
-        assertRows(execute("SELECT * FROM %s WHERE pk2 = ?", 10), row(1, 10, 2, 9, 1));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE a = 1"),
+                       row(1, 2, 3));
+            assertRows(execute("SELECT * FROM %s WHERE b = 3"),
+                       row(2, 3, 4));
 
-        execute("UPDATE %s SET s=? WHERE pk1=? AND pk2=?", 9, 1, 20);
-        assertRows(execute("SELECT * FROM %s WHERE pk2 = ?", 20), row(1, 20, null, 9, null));
+        });
     }
 
-    private ResultMessage.Prepared prepareStatement(String cql, boolean forThrift)
+    @Test
+    public void testAllowFilteringOnPartitionKeyWithSecondaryIndex() throws Throwable
     {
-        return QueryProcessor.instance.prepare(String.format(cql, KEYSPACE, currentTable()),
-                                               ClientState.forInternalCalls(),
-                                               forThrift);
-    }
+        createTable("CREATE TABLE %s (pk1 int, pk2 int, c1 int, c2 int, v int, " +
+                    "PRIMARY KEY ((pk1, pk2), c1, c2))");
+        createIndex("CREATE INDEX v_idx_1 ON %s (v);");
 
-    private void validateCell(Cell cell, ColumnDefinition def, ByteBuffer val, long timestamp)
-    {
-        assertNotNull(cell);
-        assertEquals(0, def.type.compare(cell.value(), val));
-        assertEquals(timestamp, cell.timestamp());
-    }
-
-    private static void assertColumnValue(int expected, String name, Row row, CFMetaData cfm)
-    {
-        ColumnDefinition col = cfm.getColumnDefinition(new ColumnIdentifier(name, true));
-        AbstractType<?> type = col.type;
-        assertEquals(expected, type.compose(row.getCell(col).value()));
-    }
-
-    /**
-     * <code>CassandraIndex</code> that blocks during the initialization.
-     */
-    public static class IndexBlockingOnInitialization extends CustomCassandraIndex
-    {
-        private final CountDownLatch latch = new CountDownLatch(1);
-
-        public IndexBlockingOnInitialization(ColumnFamilyStore baseCfs, IndexMetadata indexDef)
+        for (int i = 1; i <= 5; i++)
         {
-            super(baseCfs, indexDef);
+            for (int j = 1; j <= 2; j++)
+            {
+                execute("INSERT INTO %s (pk1, pk2, c1, c2, v) VALUES (?, ?, ?, ?, ?)", j, 1, 1, 1, i);
+                execute("INSERT INTO %s (pk1, pk2, c1, c2, v) VALUES (?, ?, ?, ?, ?)", j, 1, 1, i, i);
+                execute("INSERT INTO %s (pk1, pk2, c1, c2, v) VALUES (?, ?, ?, ?, ?)", j, 1, i, i, i);
+                execute("INSERT INTO %s (pk1, pk2, c1, c2, v) VALUES (?, ?, ?, ?, ?)", j, i, i, i, i);
+            }
         }
 
-        @Override
-        public Callable<?> getInitializationTask()
-        {
-            return () -> {
-                latch.await();
-                return null;
-            };
-        }
+        beforeAndAfterFlush(() -> {
+            assertEmpty(execute("SELECT * FROM %s WHERE pk1 = 1 AND  c1 > 0 AND c1 < 5 AND c2 = 1 AND v = 3 ALLOW FILTERING;"));
 
-        @Override
-        public Callable<?> getInvalidateTask()
-        {
-            latch.countDown();
-            return super.getInvalidateTask();
-        }
+            assertRows(execute("SELECT * FROM %s WHERE pk1 = 1 AND  c1 > 0 AND c1 < 5 AND c2 = 3 AND v = 3 ALLOW FILTERING;"),
+                       row(1, 3, 3, 3, 3),
+                       row(1, 1, 1, 3, 3),
+                       row(1, 1, 3, 3, 3));
+
+            assertEmpty(execute("SELECT * FROM %s WHERE pk1 = 1 AND  c2 > 1 AND c2 < 5 AND v = 1 ALLOW FILTERING;"));
+
+            assertRows(execute("SELECT * FROM %s WHERE pk1 = 1 AND  c1 > 1 AND c2 > 2 AND v = 3 ALLOW FILTERING;"),
+                       row(1, 3, 3, 3, 3),
+                       row(1, 1, 3, 3, 3));
+
+            assertRows(execute("SELECT * FROM %s WHERE pk1 = 1 AND  pk2 > 1 AND c2 > 2 AND v = 3 ALLOW FILTERING;"),
+                       row(1, 3, 3, 3, 3));
+
+            assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE pk2 > 1 AND  c1 IN(0,1,2) AND v <= 3 ALLOW FILTERING;"),
+                                    row(1, 2, 2, 2, 2),
+                                    row(2, 2, 2, 2, 2));
+
+            assertRows(execute("SELECT * FROM %s WHERE pk1 >= 2 AND pk2 <=3 AND  c1 IN(0,1,2) AND c2 IN(0,1,2) AND v < 3  ALLOW FILTERING;"),
+                       row(2, 2, 2, 2, 2),
+                       row(2, 1, 1, 2, 2),
+                       row(2, 1, 2, 2, 2));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE pk1 >= 1 AND pk2 <=3 AND  c1 IN(0,1,2) AND c2 IN(0,1,2) AND v = 3");
+        });
+    }
+
+    @Test
+    public void testAllowFilteringOnPartitionKeyWithIndexForContains() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k1 int, k2 int, v set<int>, PRIMARY KEY ((k1, k2)))");
+        createIndex("CREATE INDEX ON %s(k2)");
+
+        execute("INSERT INTO %s (k1, k2, v) VALUES (?, ?, ?)", 0, 0, set(1, 2, 3));
+        execute("INSERT INTO %s (k1, k2, v) VALUES (?, ?, ?)", 0, 1, set(2, 3, 4));
+        execute("INSERT INTO %s (k1, k2, v) VALUES (?, ?, ?)", 1, 0, set(3, 4, 5));
+        execute("INSERT INTO %s (k1, k2, v) VALUES (?, ?, ?)", 1, 1, set(4, 5, 6));
+
+        beforeAndAfterFlush(() -> {
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE k2 > ?", 0);
+
+            assertRows(execute("SELECT * FROM %s WHERE k2 > ? ALLOW FILTERING", 0),
+                       row(0, 1, set(2, 3, 4)),
+                       row(1, 1, set(4, 5, 6)));
+
+            assertRows(execute("SELECT * FROM %s WHERE k2 >= ? AND v CONTAINS ? ALLOW FILTERING", 1, 6),
+                       row(1, 1, set(4, 5, 6)));
+
+            assertEmpty(execute("SELECT * FROM %s WHERE k2 < ? AND v CONTAINS ? ALLOW FILTERING", 0, 7));
+        });
+    }
+
+    @Test
+    public void testIndexOnStaticColumnWithPartitionWithoutRows() throws Throwable
+    {
+        createTable("CREATE TABLE %s (pk int, c int, s int static, v int, PRIMARY KEY(pk, c))");
+        createIndex("CREATE INDEX ON %s (s)");
+
+        execute("INSERT INTO %s (pk, c, s, v) VALUES (?, ?, ?, ?)", 1, 1, 9, 1);
+        execute("INSERT INTO %s (pk, c, s, v) VALUES (?, ?, ?, ?)", 1, 2, 9, 2);
+        execute("INSERT INTO %s (pk, s) VALUES (?, ?)", 2, 9);
+        execute("INSERT INTO %s (pk, c, s, v) VALUES (?, ?, ?, ?)", 3, 1, 9, 1);
+        flush();
+
+        assertRows(execute("SELECT * FROM %s WHERE s = ?", 9),
+                   row(1, 1, 9, 1),
+                   row(1, 2, 9, 2),
+                   row(2, null, 9, null),
+                   row(3, 1, 9, 1));
+
+        execute("DELETE FROM %s WHERE pk = ?", 3);
+
+        assertRows(execute("SELECT * FROM %s WHERE s = ?", 9),
+                   row(1, 1, 9, 1),
+                   row(1, 2, 9, 2),
+                   row(2, null, 9, null));
+    }
+
+    @Test
+    public void testIndexOnRegularColumnWithPartitionWithoutRows() throws Throwable
+    {
+        createTable("CREATE TABLE %s (pk int, c int, s int static, v int, PRIMARY KEY(pk, c))");
+        createIndex("CREATE INDEX ON %s (v)");
+
+        execute("INSERT INTO %s (pk, c, s, v) VALUES (?, ?, ?, ?)", 1, 1, 9, 1);
+        execute("INSERT INTO %s (pk, c, s, v) VALUES (?, ?, ?, ?)", 1, 2, 9, 2);
+        execute("INSERT INTO %s (pk, s) VALUES (?, ?)", 2, 9);
+        execute("INSERT INTO %s (pk, c, s, v) VALUES (?, ?, ?, ?)", 3, 1, 9, 1);
+        flush();
+
+        execute("DELETE FROM %s WHERE pk = ? and c = ?", 3, 1);
+
+        assertRows(execute("SELECT * FROM %s WHERE v = ?", 1),
+                   row(1, 1, 9, 1));
+    }
+
+    @Test
+    public void testIndexOnDurationColumn() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, d duration)");
+        assertInvalidMessage("Secondary indexes are not supported on duration columns",
+                             "CREATE INDEX ON %s (d)");
+
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, l list<duration>)");
+        assertInvalidMessage("Secondary indexes are not supported on collections containing durations",
+                             "CREATE INDEX ON %s (l)");
+
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, m map<int, duration>)");
+        assertInvalidMessage("Secondary indexes are not supported on collections containing durations",
+                             "CREATE INDEX ON %s (m)");
+
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, t tuple<int, duration>)");
+        assertInvalidMessage("Secondary indexes are not supported on tuples containing durations",
+                             "CREATE INDEX ON %s (t)");
+
+        String udt = createType("CREATE TYPE %s (i int, d duration)");
+
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, t " + udt + ")");
+        assertInvalidMessage("Secondary indexes are not supported on UDTs containing durations",
+                             "CREATE INDEX ON %s (t)");
+    }
+
+    @Test
+    public void testIndexOnFrozenUDT() throws Throwable
+    {
+        String type = createType("CREATE TYPE %s (a int)");
+        String tableName = createTable("CREATE TABLE %s (k int PRIMARY KEY, v frozen<" + type + ">)");
+
+        Object udt1 = userType("a", 1);
+        Object udt2 = userType("a", 2);
+
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, udt1);
+        execute("CREATE INDEX idx ON %s (v)");
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 1, udt2);
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 1, udt1);
+        assertTrue(waitForIndex(keyspace(), tableName, "idx"));
+
+        assertRows(execute("SELECT * FROM %s WHERE v = ?", udt1), row(1, udt1), row(0, udt1));
+        assertEmpty(execute("SELECT * FROM %s WHERE v = ?", udt2));
+
+        execute("DELETE FROM %s WHERE k = 0");
+        assertRows(execute("SELECT * FROM %s WHERE v = ?", udt1), row(1, udt1));
+
+        dropIndex("DROP INDEX %s.idx");
+        assertInvalidMessage("Index 'idx' could not be found", "DROP INDEX " + KEYSPACE + ".idx");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE v = ?", udt1);
+    }
+
+    @Test
+    public void testIndexOnFrozenCollectionOfUDT() throws Throwable
+    {
+        String type = createType("CREATE TYPE %s (a int)");
+        String tableName = createTable("CREATE TABLE %s (k int PRIMARY KEY, v frozen<set<frozen<" + type + ">>>)");
+
+        Object udt1 = userType("a", 1);
+        Object udt2 = userType("a", 2);
+
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 1, set(udt1, udt2));
+        assertInvalidMessage("Frozen collections only support full()", "CREATE INDEX idx ON %s (keys(v))");
+        assertInvalidMessage("Frozen collections only support full()", "CREATE INDEX idx ON %s (values(v))");
+        execute("CREATE INDEX idx ON %s (full(v))");
+
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 2, set(udt2));
+        assertTrue(waitForIndex(keyspace(), tableName, "idx"));
+
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE v CONTAINS ?", udt1);
+
+        assertRows(execute("SELECT * FROM %s WHERE v = ?", set(udt1, udt2)), row(1, set(udt1, udt2)));
+        assertRows(execute("SELECT * FROM %s WHERE v = ?", set(udt2)), row(2, set(udt2)));
+
+        execute("DELETE FROM %s WHERE k = 2");
+        assertEmpty(execute("SELECT * FROM %s WHERE v = ?", set(udt2)));
+
+        dropIndex("DROP INDEX %s.idx");
+        assertInvalidMessage("Index 'idx' could not be found", "DROP INDEX " + KEYSPACE + ".idx");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE v CONTAINS ?", udt1);
+    }
+
+    @Test
+    public void testIndexOnNonFrozenCollectionOfFrozenUDT() throws Throwable
+    {
+        String type = createType("CREATE TYPE %s (a int)");
+        String tableName = createTable("CREATE TABLE %s (k int PRIMARY KEY, v set<frozen<" + type + ">>)");
+
+        Object udt1 = userType("a", 1);
+        Object udt2 = userType("a", 2);
+
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 1, set(udt1));
+        assertInvalidMessage("Cannot create index on keys of column v with non-map type",
+                             "CREATE INDEX idx ON %s (keys(v))");
+        assertInvalidMessage("full() indexes can only be created on frozen collections",
+                             "CREATE INDEX idx ON %s (full(v))");
+        execute("CREATE INDEX idx ON %s (values(v))");
+
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 2, set(udt2));
+        execute("UPDATE %s SET v = v + ? WHERE k = ?", set(udt2), 1);
+        assertTrue(waitForIndex(keyspace(), tableName, "idx"));
+
+        assertRows(execute("SELECT * FROM %s WHERE v CONTAINS ?", udt1), row(1, set(udt1, udt2)));
+        assertRows(execute("SELECT * FROM %s WHERE v CONTAINS ?", udt2), row(1, set(udt1, udt2)), row(2, set(udt2)));
+
+        execute("DELETE FROM %s WHERE k = 1");
+        assertEmpty(execute("SELECT * FROM %s WHERE v CONTAINS ?", udt1));
+        assertRows(execute("SELECT * FROM %s WHERE v CONTAINS ?", udt2), row(2, set(udt2)));
+
+        dropIndex("DROP INDEX %s.idx");
+        assertInvalidMessage("Index 'idx' could not be found", "DROP INDEX " + KEYSPACE + ".idx");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE v CONTAINS ?", udt1);
+    }
+
+    @Test
+    public void testIndexOnNonFrozenUDT() throws Throwable
+    {
+        String type = createType("CREATE TYPE %s (a int)");
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v " + type + ")");
+        assertInvalidMessage("Secondary indexes are not supported on non-frozen UDTs", "CREATE INDEX ON %s (v)");
+        assertInvalidMessage("Non-collection columns support only simple indexes", "CREATE INDEX ON %s (keys(v))");
+        assertInvalidMessage("Non-collection columns support only simple indexes", "CREATE INDEX ON %s (values(v))");
+        assertInvalidMessage("full() indexes can only be created on frozen collections", "CREATE INDEX ON %s (full(v))");
     }
 
     @Test
@@ -1450,4 +1752,54 @@
 
         assertEmpty(execute("SELECT pk, v2 FROM %s WHERE v1 = 5"));
     }
+    
+    private ResultMessage.Prepared prepareStatement(String cql, boolean forThrift)
+    {
+        return QueryProcessor.instance.prepare(String.format(cql, KEYSPACE, currentTable()),
+                                               ClientState.forInternalCalls(),
+                                               forThrift);
+    }
+
+    private void validateCell(Cell cell, ColumnDefinition def, ByteBuffer val, long timestamp)
+    {
+        assertNotNull(cell);
+        assertEquals(0, def.type.compare(cell.value(), val));
+        assertEquals(timestamp, cell.timestamp());
+    }
+
+    private static void assertColumnValue(int expected, String name, Row row, CFMetaData cfm)
+    {
+        ColumnDefinition col = cfm.getColumnDefinition(new ColumnIdentifier(name, true));
+        AbstractType<?> type = col.type;
+        assertEquals(expected, type.compose(row.getCell(col).value()));
+    }
+
+    /**
+     * <code>CassandraIndex</code> that blocks during the initialization.
+     */
+    public static class IndexBlockingOnInitialization extends CustomCassandraIndex
+    {
+        private final CountDownLatch latch = new CountDownLatch(1);
+
+        public IndexBlockingOnInitialization(ColumnFamilyStore baseCfs, IndexMetadata indexDef)
+        {
+            super(baseCfs, indexDef);
+        }
+
+        @Override
+        public Callable<?> getInitializationTask()
+        {
+            return () -> {
+                latch.await();
+                return null;
+            };
+        }
+
+        @Override
+        public Callable<?> getInvalidateTask()
+        {
+            latch.countDown();
+            return super.getInvalidateTask();
+        }
+    }
 }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/StaticColumnsTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/StaticColumnsTest.java
index efa48ae..08e021c 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/StaticColumnsTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/StaticColumnsTest.java
@@ -24,7 +24,7 @@
 
 import org.apache.cassandra.cql3.CQLTester;
 
-import static junit.framework.Assert.assertNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -37,9 +37,16 @@
     @Test
     public void testStaticColumns() throws Throwable
     {
+        testStaticColumns(false);
+        testStaticColumns(true);
+    }
+
+    private void testStaticColumns(boolean forceFlush) throws Throwable
+    {
         createTable("CREATE TABLE %s ( k int, p int, s int static, v int, PRIMARY KEY (k, p))");
 
         execute("INSERT INTO %s(k, s) VALUES (0, 42)");
+        flush(forceFlush);
 
         assertRows(execute("SELECT * FROM %s"), row(0, null, 42, null));
 
@@ -51,6 +58,7 @@
 
         execute("INSERT INTO %s (k, p, s, v) VALUES (0, 0, 12, 0)");
         execute("INSERT INTO %s (k, p, s, v) VALUES (0, 1, 24, 1)");
+        flush(forceFlush);
 
         // Check the static columns in indeed "static"
         assertRows(execute("SELECT * FROM %s"), row(0, 0, 24, 0), row(0, 1, 24, 1));
@@ -81,10 +89,12 @@
 
         // Check that deleting a row don't implicitely deletes statics
         execute("DELETE FROM %s WHERE k=0 AND p=0");
+        flush(forceFlush);
         assertRows(execute("SELECT * FROM %s"),row(0, 1, 24, 1));
 
         // But that explicitely deleting the static column does remove it
         execute("DELETE s FROM %s WHERE k=0");
+        flush(forceFlush);
         assertRows(execute("SELECT * FROM %s"), row(0, 1, null, 1));
 
         // Check we can add a static column ...
@@ -114,8 +124,7 @@
         assertRows(execute("SELECT * FROM %s WHERE v = 1"), row(0, 0, 42, 1), row(0, 1, 42, 1));
         assertRows(execute("SELECT p, s FROM %s WHERE v = 1"), row(0, 42), row(1, 42));
         assertRows(execute("SELECT p FROM %s WHERE v = 1"), row(0), row(1));
-        // We don't support that
-        assertInvalid("SELECT s FROM %s WHERE v = 1");
+        assertRows(execute("SELECT s FROM %s WHERE v = 1"), row(42), row(42));
     }
 
     /**
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFAuthTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFAuthTest.java
index e5ecc72..3affe9a 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/UFAuthTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFAuthTest.java
@@ -29,17 +29,13 @@
 import org.apache.cassandra.auth.*;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
-import org.apache.cassandra.cql3.Attributes;
-import org.apache.cassandra.cql3.CQLStatement;
-import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.cql3.functions.FunctionName;
 import org.apache.cassandra.cql3.statements.BatchStatement;
 import org.apache.cassandra.cql3.statements.ModificationStatement;
-import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.exceptions.*;
 import org.apache.cassandra.service.ClientState;
-import org.apache.cassandra.utils.Pair;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -651,99 +647,4 @@
     {
         return String.format("%s(%s)", functionName, Joiner.on(",").join(args));
     }
-
-    static class StubAuthorizer implements IAuthorizer
-    {
-        Map<Pair<String, IResource>, Set<Permission>> userPermissions = new HashMap<>();
-
-        private void clear()
-        {
-            userPermissions.clear();
-        }
-
-        public Set<Permission> authorize(AuthenticatedUser user, IResource resource)
-        {
-            Pair<String, IResource> key = Pair.create(user.getName(), resource);
-            Set<Permission> perms = userPermissions.get(key);
-            return perms != null ? perms : Collections.<Permission>emptySet();
-        }
-
-        public void grant(AuthenticatedUser performer,
-                          Set<Permission> permissions,
-                          IResource resource,
-                          RoleResource grantee) throws RequestValidationException, RequestExecutionException
-        {
-            Pair<String, IResource> key = Pair.create(grantee.getRoleName(), resource);
-            Set<Permission> perms = userPermissions.get(key);
-            if (null == perms)
-            {
-                perms = new HashSet<>();
-                userPermissions.put(key, perms);
-            }
-            perms.addAll(permissions);
-        }
-
-        public void revoke(AuthenticatedUser performer,
-                           Set<Permission> permissions,
-                           IResource resource,
-                           RoleResource revokee) throws RequestValidationException, RequestExecutionException
-        {
-            Pair<String, IResource> key = Pair.create(revokee.getRoleName(), resource);
-            Set<Permission> perms = userPermissions.get(key);
-            if (null != perms)
-                perms.removeAll(permissions);
-            if (perms.isEmpty())
-                userPermissions.remove(key);
-        }
-
-        public Set<PermissionDetails> list(AuthenticatedUser performer,
-                                           Set<Permission> permissions,
-                                           IResource resource,
-                                           RoleResource grantee) throws RequestValidationException, RequestExecutionException
-        {
-            Pair<String, IResource> key = Pair.create(grantee.getRoleName(), resource);
-            Set<Permission> perms = userPermissions.get(key);
-            if (perms == null)
-                return Collections.emptySet();
-
-
-            Set<PermissionDetails> details = new HashSet<>();
-            for (Permission permission : perms)
-            {
-                if (permissions.contains(permission))
-                    details.add(new PermissionDetails(grantee.getRoleName(), resource, permission));
-            }
-            return details;
-        }
-
-        public void revokeAllFrom(RoleResource revokee)
-        {
-            for (Pair<String, IResource> key : userPermissions.keySet())
-                if (key.left.equals(revokee.getRoleName()))
-                    userPermissions.remove(key);
-        }
-
-        public void revokeAllOn(IResource droppedResource)
-        {
-            for (Pair<String, IResource> key : userPermissions.keySet())
-                if (key.right.equals(droppedResource))
-                    userPermissions.remove(key);
-
-        }
-
-        public Set<? extends IResource> protectedResources()
-        {
-            return Collections.emptySet();
-        }
-
-        public void validateConfiguration() throws ConfigurationException
-        {
-
-        }
-
-        public void setup()
-        {
-
-        }
-    }
 }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFJavaTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFJavaTest.java
index 15c8e18..4d46b8b 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/UFJavaTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFJavaTest.java
@@ -40,7 +40,7 @@
 import org.apache.cassandra.cql3.functions.FunctionName;
 import org.apache.cassandra.exceptions.FunctionExecutionException;
 import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 public class UFJavaTest extends CQLTester
 {
@@ -330,7 +330,7 @@
                    row(list, set, map));
 
         // same test - but via native protocol
-        for (int version : PROTOCOL_VERSIONS)
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
             assertRowsNet(version,
                           executeNet(version, "SELECT " + fList + "(lst), " + fSet + "(st), " + fMap + "(mp) FROM %s WHERE key = 1"),
                           row(list, set, map));
@@ -428,13 +428,13 @@
         // we use protocol V3 here to encode the expected version because the server
         // always serializes Collections using V3 - see CollectionSerializer's
         // serialize and deserialize methods.
-        TupleType tType = tupleTypeOf(Server.VERSION_3,
+        TupleType tType = tupleTypeOf(ProtocolVersion.V3,
                                       DataType.cdouble(),
                                       DataType.list(DataType.cdouble()),
                                       DataType.set(DataType.text()),
                                       DataType.map(DataType.cint(), DataType.cboolean()));
         TupleValue tup = tType.newValue(1d, list, set, map);
-        for (int version : PROTOCOL_VERSIONS)
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
         {
             assertRowsNet(version,
                           executeNet(version, "SELECT " + fTup0 + "(tup) FROM %s WHERE key = 1"),
@@ -461,7 +461,7 @@
         createTable("CREATE TABLE %s (key int primary key, udt frozen<" + KEYSPACE + '.' + type + ">)");
         execute("INSERT INTO %s (key, udt) VALUES (1, {txt: 'one', i:1})");
 
-        for (int version : PROTOCOL_VERSIONS)
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
         {
             executeNet(version, "USE " + KEYSPACE);
 
@@ -525,7 +525,7 @@
         assertRows(execute("SELECT " + fUdt2 + "(udt) FROM %s WHERE key = 1"),
                    row(1));
 
-        for (int version : PROTOCOL_VERSIONS)
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
         {
             List<Row> rowsNet = executeNet(version, "SELECT " + fUdt0 + "(udt) FROM %s WHERE key = 1").all();
             Assert.assertEquals(1, rowsNet.size());
@@ -750,7 +750,7 @@
         assertRows(execute("SELECT " + fName1 + "(lst), " + fName2 + "(st), " + fName3 + "(mp) FROM %s WHERE key = 1"),
                    row("three", "one", "two"));
 
-        for (int version : PROTOCOL_VERSIONS)
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
             assertRowsNet(version,
                           executeNet(version, "SELECT " + fName1 + "(lst), " + fName2 + "(st), " + fName3 + "(mp) FROM %s WHERE key = 1"),
                           row("three", "one", "two"));
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFPureScriptTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFPureScriptTest.java
new file mode 100644
index 0000000..d5f17e1
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFPureScriptTest.java
@@ -0,0 +1,407 @@
+/*
+ * 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.validation.entities;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.UUID;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.functions.FunctionName;
+import org.apache.cassandra.exceptions.FunctionExecutionException;
+import org.apache.cassandra.transport.ProtocolVersion;
+import org.apache.cassandra.utils.UUIDGen;
+
+public class UFPureScriptTest extends CQLTester
+{
+    // Just JavaScript UDFs to check how UDF - especially security/class-loading/sandboxing stuff -
+    // behaves, if no Java UDF has been executed before.
+
+    // Do not add any other test here - especially none using Java UDFs
+
+    @Test
+    public void testJavascriptSimpleCollections() throws Throwable
+    {
+        createTable("CREATE TABLE %s (key int primary key, lst list<double>, st set<text>, mp map<int, boolean>)");
+
+        String fName1 = createFunction(KEYSPACE_PER_TEST, "list<double>",
+                                       "CREATE FUNCTION %s( lst list<double> ) " +
+                                       "RETURNS NULL ON NULL INPUT " +
+                                       "RETURNS list<double> " +
+                                       "LANGUAGE javascript\n" +
+                                       "AS 'lst;';");
+        String fName2 = createFunction(KEYSPACE_PER_TEST, "set<text>",
+                                       "CREATE FUNCTION %s( st set<text> ) " +
+                                       "RETURNS NULL ON NULL INPUT " +
+                                       "RETURNS set<text> " +
+                                       "LANGUAGE javascript\n" +
+                                       "AS 'st;';");
+        String fName3 = createFunction(KEYSPACE_PER_TEST, "map<int, boolean>",
+                                       "CREATE FUNCTION %s( mp map<int, boolean> ) " +
+                                       "RETURNS NULL ON NULL INPUT " +
+                                       "RETURNS map<int, boolean> " +
+                                       "LANGUAGE javascript\n" +
+                                       "AS 'mp;';");
+
+        List<Double> list = Arrays.asList(1d, 2d, 3d);
+        Set<String> set = new TreeSet<>(Arrays.asList("one", "three", "two"));
+        Map<Integer, Boolean> map = new TreeMap<>();
+        map.put(1, true);
+        map.put(2, false);
+        map.put(3, true);
+
+        execute("INSERT INTO %s (key, lst, st, mp) VALUES (1, ?, ?, ?)", list, set, map);
+
+        assertRows(execute("SELECT lst, st, mp FROM %s WHERE key = 1"),
+                   row(list, set, map));
+
+        assertRows(execute("SELECT " + fName1 + "(lst), " + fName2 + "(st), " + fName3 + "(mp) FROM %s WHERE key = 1"),
+                   row(list, set, map));
+
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
+            assertRowsNet(version,
+                          executeNet(version, "SELECT " + fName1 + "(lst), " + fName2 + "(st), " + fName3 + "(mp) FROM %s WHERE key = 1"),
+                          row(list, set, map));
+    }
+
+    @Test
+    public void testJavascriptTupleType() throws Throwable
+    {
+        createTable("CREATE TABLE %s (key int primary key, tup frozen<tuple<double, text, int, boolean>>)");
+
+        String fName = createFunction(KEYSPACE_PER_TEST, "tuple<double, text, int, boolean>",
+                                      "CREATE FUNCTION %s( tup tuple<double, text, int, boolean> ) " +
+                                      "RETURNS NULL ON NULL INPUT " +
+                                      "RETURNS tuple<double, text, int, boolean> " +
+                                      "LANGUAGE javascript\n" +
+                                      "AS $$tup;$$;");
+
+        Object t = tuple(1d, "foo", 2, true);
+
+        execute("INSERT INTO %s (key, tup) VALUES (1, ?)", t);
+
+        assertRows(execute("SELECT tup FROM %s WHERE key = 1"),
+                   row(t));
+
+        assertRows(execute("SELECT " + fName + "(tup) FROM %s WHERE key = 1"),
+                   row(t));
+    }
+
+    @Test
+    public void testJavascriptUserType() throws Throwable
+    {
+        String type = createType("CREATE TYPE %s (txt text, i int)");
+
+        createTable("CREATE TABLE %s (key int primary key, udt frozen<" + type + ">)");
+
+        String fUdt1 = createFunction(KEYSPACE, type,
+                                      "CREATE FUNCTION %s( udt " + type + " ) " +
+                                      "RETURNS NULL ON NULL INPUT " +
+                                      "RETURNS " + type + ' ' +
+                                      "LANGUAGE javascript\n" +
+                                      "AS $$" +
+                                      "     udt;$$;");
+        String fUdt2 = createFunction(KEYSPACE, type,
+                                      "CREATE FUNCTION %s( udt " + type + " ) " +
+                                      "RETURNS NULL ON NULL INPUT " +
+                                      "RETURNS text " +
+                                      "LANGUAGE javascript\n" +
+                                      "AS $$" +
+                                      "     udt.getString(\"txt\");$$;");
+        String fUdt3 = createFunction(KEYSPACE, type,
+                                      "CREATE FUNCTION %s( udt " + type + " ) " +
+                                      "RETURNS NULL ON NULL INPUT " +
+                                      "RETURNS int " +
+                                      "LANGUAGE javascript\n" +
+                                      "AS $$" +
+                                      "     udt.getInt(\"i\");$$;");
+
+        execute("INSERT INTO %s (key, udt) VALUES (1, {txt: 'one', i:1})");
+
+        UntypedResultSet rows = execute("SELECT " + fUdt1 + "(udt) FROM %s WHERE key = 1");
+        Assert.assertEquals(1, rows.size());
+        assertRows(execute("SELECT " + fUdt2 + "(udt) FROM %s WHERE key = 1"),
+                   row("one"));
+        assertRows(execute("SELECT " + fUdt3 + "(udt) FROM %s WHERE key = 1"),
+                   row(1));
+    }
+
+    @Test
+    public void testJavascriptUTCollections() throws Throwable
+    {
+        String type = createType("CREATE TYPE %s (txt text, i int)");
+
+        createTable(String.format("CREATE TABLE %%s " +
+                                  "(key int primary key, lst list<frozen<%s>>, st set<frozen<%s>>, mp map<int, frozen<%s>>)",
+                                  type, type, type));
+
+        String fName = createFunction(KEYSPACE, "list<frozen<" + type + ">>",
+                                      "CREATE FUNCTION %s( lst list<frozen<" + type + ">> ) " +
+                                      "RETURNS NULL ON NULL INPUT " +
+                                      "RETURNS text " +
+                                      "LANGUAGE javascript\n" +
+                                      "AS $$" +
+                                      "        lst.get(1).getString(\"txt\");$$;");
+        createFunctionOverload(fName, "set<frozen<" + type + ">>",
+                               "CREATE FUNCTION %s( st set<frozen<" + type + ">> ) " +
+                               "RETURNS NULL ON NULL INPUT " +
+                               "RETURNS text " +
+                               "LANGUAGE javascript\n" +
+                               "AS $$" +
+                               "        st.iterator().next().getString(\"txt\");$$;");
+        createFunctionOverload(fName, "map<int, frozen<" + type + ">>",
+                               "CREATE FUNCTION %s( mp map<int, frozen<" + type + ">> ) " +
+                               "RETURNS NULL ON NULL INPUT " +
+                               "RETURNS text " +
+                               "LANGUAGE javascript\n" +
+                               "AS $$" +
+                               "        mp.get(java.lang.Integer.valueOf(3)).getString(\"txt\");$$;");
+
+        execute("INSERT INTO %s (key, lst, st, mp) values (1, " +
+                // list<frozen<UDT>>
+                "[ {txt: 'one', i:1}, {txt: 'three', i:1}, {txt: 'one', i:1} ] , " +
+                // set<frozen<UDT>>
+                "{ {txt: 'one', i:1}, {txt: 'three', i:3}, {txt: 'two', i:2} }, " +
+                // map<int, frozen<UDT>>
+                "{ 1: {txt: 'one', i:1}, 2: {txt: 'one', i:3}, 3: {txt: 'two', i:2} })");
+
+        assertRows(execute("SELECT " + fName + "(lst) FROM %s WHERE key = 1"),
+                   row("three"));
+        assertRows(execute("SELECT " + fName + "(st) FROM %s WHERE key = 1"),
+                   row("one"));
+        assertRows(execute("SELECT " + fName + "(mp) FROM %s WHERE key = 1"),
+                   row("two"));
+
+        String cqlSelect = "SELECT " + fName + "(lst), " + fName + "(st), " + fName + "(mp) FROM %s WHERE key = 1";
+        assertRows(execute(cqlSelect),
+                   row("three", "one", "two"));
+
+        // same test - but via native protocol
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
+            assertRowsNet(version,
+                          executeNet(version, cqlSelect),
+                          row("three", "one", "two"));
+    }
+
+    @Test
+    public void testJavascriptFunction() throws Throwable
+    {
+        createTable("CREATE TABLE %s (key int primary key, val double)");
+
+        String functionBody = '\n' +
+                              "  Math.sin(val);\n";
+
+        String fName = createFunction(KEYSPACE, "double",
+                                      "CREATE OR REPLACE FUNCTION %s(val double) " +
+                                      "RETURNS NULL ON NULL INPUT " +
+                                      "RETURNS double " +
+                                      "LANGUAGE javascript\n" +
+                                      "AS '" + functionBody + "';");
+
+        FunctionName fNameName = parseFunctionName(fName);
+
+        assertRows(execute("SELECT language, body FROM system_schema.functions WHERE keyspace_name=? AND function_name=?",
+                           fNameName.keyspace, fNameName.name),
+                   row("javascript", functionBody));
+
+        execute("INSERT INTO %s (key, val) VALUES (?, ?)", 1, 1d);
+        execute("INSERT INTO %s (key, val) VALUES (?, ?)", 2, 2d);
+        execute("INSERT INTO %s (key, val) VALUES (?, ?)", 3, 3d);
+        assertRows(execute("SELECT key, val, " + fName + "(val) FROM %s"),
+                   row(1, 1d, Math.sin(1d)),
+                   row(2, 2d, Math.sin(2d)),
+                   row(3, 3d, Math.sin(3d))
+        );
+    }
+
+    @Test
+    public void testJavascriptBadReturnType() throws Throwable
+    {
+        createTable("CREATE TABLE %s (key int primary key, val double)");
+
+        String fName = createFunction(KEYSPACE, "double",
+                                      "CREATE OR REPLACE FUNCTION %s(val double) " +
+                                      "RETURNS NULL ON NULL INPUT " +
+                                      "RETURNS double " +
+                                      "LANGUAGE javascript\n" +
+                                      "AS '\"string\";';");
+
+        execute("INSERT INTO %s (key, val) VALUES (?, ?)", 1, 1d);
+        // throws IRE with ClassCastException
+        assertInvalidMessage("Invalid value for CQL type double", "SELECT key, val, " + fName + "(val) FROM %s");
+    }
+
+    @Test
+    public void testJavascriptThrow() throws Throwable
+    {
+        createTable("CREATE TABLE %s (key int primary key, val double)");
+
+        String fName = createFunction(KEYSPACE, "double",
+                                      "CREATE OR REPLACE FUNCTION %s(val double) " +
+                                      "RETURNS NULL ON NULL INPUT " +
+                                      "RETURNS double " +
+                                      "LANGUAGE javascript\n" +
+                                      "AS 'throw \"fool\";';");
+
+        execute("INSERT INTO %s (key, val) VALUES (?, ?)", 1, 1d);
+        // throws IRE with ScriptException
+        assertInvalidThrowMessage("fool", FunctionExecutionException.class,
+                                  "SELECT key, val, " + fName + "(val) FROM %s");
+    }
+
+    @Test
+    public void testScriptReturnTypeCasting() throws Throwable
+    {
+        createTable("CREATE TABLE %s (key int primary key, val double)");
+        execute("INSERT INTO %s (key, val) VALUES (?, ?)", 1, 1d);
+
+        Object[][] variations = {
+                                new Object[]    {   "true",     "boolean",  true    },
+                                new Object[]    {   "false",    "boolean",  false   },
+                                new Object[]    {   "100",      "tinyint",  (byte)100 },
+                                new Object[]    {   "100.",     "tinyint",  (byte)100 },
+                                new Object[]    {   "100",      "smallint", (short)100 },
+                                new Object[]    {   "100.",     "smallint", (short)100 },
+                                new Object[]    {   "100",      "int",      100     },
+                                new Object[]    {   "100.",     "int",      100     },
+                                new Object[]    {   "100",      "double",   100d    },
+                                new Object[]    {   "100.",     "double",   100d    },
+                                new Object[]    {   "100",      "bigint",   100L    },
+                                new Object[]    {   "100.",     "bigint",   100L    },
+                                new Object[]    {   "100",      "varint",   BigInteger.valueOf(100L)    },
+                                new Object[]    {   "100.",     "varint",   BigInteger.valueOf(100L)    },
+                                new Object[]    {   "parseInt(\"100\");", "decimal",  BigDecimal.valueOf(100d)    },
+                                new Object[]    {   "100.",     "decimal",  BigDecimal.valueOf(100d)    },
+                                };
+
+        for (Object[] variation : variations)
+        {
+            Object functionBody = variation[0];
+            Object returnType = variation[1];
+            Object expectedResult = variation[2];
+
+            String fName = createFunction(KEYSPACE, "double",
+                                          "CREATE OR REPLACE FUNCTION %s(val double) " +
+                                          "RETURNS NULL ON NULL INPUT " +
+                                          "RETURNS " +returnType + ' ' +
+                                          "LANGUAGE javascript " +
+                                          "AS '" + functionBody + ";';");
+            assertRows(execute("SELECT key, val, " + fName + "(val) FROM %s"),
+                       row(1, 1d, expectedResult));
+        }
+    }
+
+    @Test
+    public void testScriptParamReturnTypes() throws Throwable
+    {
+        UUID ruuid = UUID.randomUUID();
+        UUID tuuid = UUIDGen.getTimeUUID();
+
+        createTable("CREATE TABLE %s (key int primary key, " +
+                    "tival tinyint, sival smallint, ival int, lval bigint, fval float, dval double, vval varint, ddval decimal, " +
+                    "timval time, dtval date, tsval timestamp, uval uuid, tuval timeuuid)");
+        execute("INSERT INTO %s (key, tival, sival, ival, lval, fval, dval, vval, ddval, timval, dtval, tsval, uval, tuval) VALUES " +
+                "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 1,
+                (byte)1, (short)1, 1, 1L, 1f, 1d, BigInteger.valueOf(1L), BigDecimal.valueOf(1d), 1L, Integer.MAX_VALUE, new Date(1), ruuid, tuuid);
+
+        Object[][] variations = {
+                                new Object[] {  "tinyint",  "tival",    (byte)1,                (byte)2  },
+                                new Object[] {  "smallint", "sival",    (short)1,               (short)2  },
+                                new Object[] {  "int",      "ival",     1,                      2  },
+                                new Object[] {  "bigint",   "lval",     1L,                     2L  },
+                                new Object[] {  "float",    "fval",     1f,                     2f  },
+                                new Object[] {  "double",   "dval",     1d,                     2d  },
+                                new Object[] {  "varint",   "vval",     BigInteger.valueOf(1L), BigInteger.valueOf(2L)  },
+                                new Object[] {  "decimal",  "ddval",    BigDecimal.valueOf(1d), BigDecimal.valueOf(2d)  },
+                                new Object[] {  "time",     "timval",   1L,                     2L  },
+                                };
+
+        for (Object[] variation : variations)
+        {
+            Object type = variation[0];
+            Object col = variation[1];
+            Object expected1 = variation[2];
+            Object expected2 = variation[3];
+            String fName = createFunction(KEYSPACE, type.toString(),
+                           "CREATE OR REPLACE FUNCTION %s(val " + type + ") " +
+                           "RETURNS NULL ON NULL INPUT " +
+                           "RETURNS " + type + ' ' +
+                           "LANGUAGE javascript " +
+                           "AS 'val+1;';");
+            assertRows(execute("SELECT key, " + col + ", " + fName + '(' + col + ") FROM %s"),
+                       row(1, expected1, expected2));
+        }
+
+        variations = new Object[][] {
+                     new Object[] {  "timestamp","tsval",    new Date(1),            new Date(1)  },
+                     new Object[] {  "uuid",     "uval",     ruuid,                  ruuid  },
+                     new Object[] {  "timeuuid", "tuval",    tuuid,                  tuuid  },
+                     new Object[] {  "date",     "dtval",    Integer.MAX_VALUE,      Integer.MAX_VALUE },
+        };
+
+        for (Object[] variation : variations)
+        {
+            Object type = variation[0];
+            Object col = variation[1];
+            Object expected1 = variation[2];
+            Object expected2 = variation[3];
+            String fName = createFunction(KEYSPACE, type.toString(),
+                                          "CREATE OR REPLACE FUNCTION %s(val " + type + ") " +
+                                          "RETURNS NULL ON NULL INPUT " +
+                                          "RETURNS " + type + ' ' +
+                                          "LANGUAGE javascript " +
+                                          "AS 'val;';");
+            assertRows(execute("SELECT key, " + col + ", " + fName + '(' + col + ") FROM %s"),
+                       row(1, expected1, expected2));
+        }
+    }
+
+    @Test
+    public void testJavascriptDisabled() throws Throwable
+    {
+        createTable("CREATE TABLE %s (key int primary key, val double)");
+
+        DatabaseDescriptor.enableScriptedUserDefinedFunctions(false);
+        try
+        {
+            assertInvalid("double",
+                          "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".assertNotEnabled(val double) " +
+                          "RETURNS NULL ON NULL INPUT " +
+                          "RETURNS double " +
+                          "LANGUAGE javascript\n" +
+                          "AS 'Math.sin(val);';");
+        }
+        finally
+        {
+            DatabaseDescriptor.enableScriptedUserDefinedFunctions(true);
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFPureScriptTupleCollectionTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFPureScriptTupleCollectionTest.java
index 7465a2a..a1801b3 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/UFPureScriptTupleCollectionTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFPureScriptTupleCollectionTest.java
@@ -31,7 +31,7 @@
 import com.datastax.driver.core.TupleType;
 import com.datastax.driver.core.TupleValue;
 import org.apache.cassandra.cql3.CQLTester;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 public class UFPureScriptTupleCollectionTest extends CQLTester
 {
@@ -109,14 +109,14 @@
         // we use protocol V3 here to encode the expected version because the server
         // always serializes Collections using V3 - see CollectionSerializer's
         // serialize and deserialize methods.
-        TupleType tType = tupleTypeOf(Server.VERSION_3,
+        TupleType tType = tupleTypeOf(ProtocolVersion.V3,
                                       DataType.cdouble(),
                                       DataType.list(DataType.cdouble()),
                                       DataType.set(DataType.text()),
                                       DataType.map(DataType.cint(),
                                                    DataType.cboolean()));
         TupleValue tup = tType.newValue(1d, list, set, map);
-        for (int version : PROTOCOL_VERSIONS)
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
         {
             assertRowsNet(version,
                           executeNet(version, "SELECT " + fTup1 + "(tup) FROM %s WHERE key = 1"),
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFScriptTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFScriptTest.java
index 9c931e8..099e42d 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/UFScriptTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFScriptTest.java
@@ -37,6 +37,7 @@
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.cql3.functions.FunctionName;
 import org.apache.cassandra.exceptions.FunctionExecutionException;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.UUIDGen;
 
 public class UFScriptTest extends CQLTester
@@ -85,7 +86,7 @@
         assertRows(execute("SELECT " + fName1 + "(lst), " + fName2 + "(st), " + fName3 + "(mp) FROM %s WHERE key = 1"),
                    row(list, set, map));
 
-        for (int version : PROTOCOL_VERSIONS)
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
             assertRowsNet(version,
                           executeNet(version, "SELECT " + fName1 + "(lst), " + fName2 + "(st), " + fName3 + "(mp) FROM %s WHERE key = 1"),
                           row(list, set, map));
@@ -204,7 +205,7 @@
                    row("three", "one", "two"));
 
         // same test - but via native protocol
-        for (int version : PROTOCOL_VERSIONS)
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
             assertRowsNet(version,
                           executeNet(version, cqlSelect),
                           row("three", "one", "two"));
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFTest.java
index e9c26d3..50d99e0 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/UFTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFTest.java
@@ -17,21 +17,24 @@
  */
 package org.apache.cassandra.cql3.validation.entities;
 
-import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
 
+import com.google.common.reflect.TypeToken;
 import org.junit.Assert;
 import org.junit.Test;
 
+import com.datastax.driver.core.TypeTokens;
+import com.datastax.driver.core.UDTValue;
 import com.datastax.driver.core.exceptions.InvalidQueryException;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.cql3.functions.FunctionName;
+import org.apache.cassandra.cql3.functions.JavaBasedUDFunction;
 import org.apache.cassandra.cql3.functions.UDFunction;
 import org.apache.cassandra.db.marshal.CollectionType;
 import org.apache.cassandra.exceptions.InvalidRequestException;
@@ -40,15 +43,23 @@
 import org.apache.cassandra.service.ClientState;
 import org.apache.cassandra.transport.Event.SchemaChange.Change;
 import org.apache.cassandra.transport.Event.SchemaChange.Target;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.transport.messages.ResultMessage;
-import org.apache.cassandra.utils.ByteBufferUtil;
 
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 public class UFTest extends CQLTester
 {
     @Test
+    public void testJavaSourceName()
+    {
+        Assert.assertEquals("String", JavaBasedUDFunction.javaSourceName(TypeToken.of(String.class)));
+        Assert.assertEquals("java.util.Map<Integer, String>", JavaBasedUDFunction.javaSourceName(TypeTokens.mapOf(Integer.class, String.class)));
+        Assert.assertEquals("com.datastax.driver.core.UDTValue", JavaBasedUDFunction.javaSourceName(TypeToken.of(UDTValue.class)));
+        Assert.assertEquals("java.util.Set<com.datastax.driver.core.UDTValue>", JavaBasedUDFunction.javaSourceName(TypeTokens.setOf(UDTValue.class)));
+    }
+
+    @Test
     public void testNonExistingOnes() throws Throwable
     {
         assertInvalidThrowMessage("Cannot drop non existing function", InvalidRequestException.class, "DROP FUNCTION " + KEYSPACE + ".func_does_not_exist");
@@ -105,6 +116,19 @@
                            Change.DROPPED, Target.FUNCTION,
                            KEYSPACE, functionName,
                            "double", "double");
+
+        // The function with nested tuple should be created without throwing InvalidRequestException. See CASSANDRA-15857
+        String fl = createFunctionName(KEYSPACE);
+        registerFunction(fl, "list<tuple<int, int>>, double");
+
+        assertSchemaChange("CREATE OR REPLACE FUNCTION " + fl + "(state list<tuple<int, int>>, val double) " +
+                           "RETURNS NULL ON NULL INPUT " +
+                           "RETURNS double " +
+                           "LANGUAGE javascript " +
+                           "AS '\"string\";';",
+                           Change.CREATED, Target.FUNCTION,
+                           KEYSPACE, shortFunctionName(fl),
+                           "list<frozen<tuple<int, int>>>", "double");
     }
 
     @Test
@@ -847,7 +871,7 @@
                                       "LANGUAGE JAVA\n" +
                                       "AS 'throw new RuntimeException();'");
 
-        for (int version : PROTOCOL_VERSIONS)
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
         {
             try
             {
@@ -858,121 +882,233 @@
             catch (com.datastax.driver.core.exceptions.FunctionExecutionException fee)
             {
                 // Java driver neither throws FunctionExecutionException nor does it set the exception code correctly
-                Assert.assertTrue(version >= Server.VERSION_4);
+                Assert.assertTrue(version.isGreaterOrEqualTo(ProtocolVersion.V4));
             }
             catch (InvalidQueryException e)
             {
-                Assert.assertTrue(version < Server.VERSION_4);
+                Assert.assertTrue(version.isSmallerThan(ProtocolVersion.V4));
             }
         }
     }
 
     @Test
-    public void testEmptyString() throws Throwable
+    public void testArgumentGenerics() throws Throwable
     {
         createTable("CREATE TABLE %s (key int primary key, sval text, aval ascii, bval blob, empty_int int)");
-        execute("INSERT INTO %s (key, sval, aval, bval, empty_int) VALUES (?, ?, ?, ?, blobAsInt(0x))", 1, "", "", ByteBuffer.allocate(0));
 
-        String fNameSRC = createFunction(KEYSPACE_PER_TEST, "text",
-                                         "CREATE OR REPLACE FUNCTION %s(val text) " +
-                                         "CALLED ON NULL INPUT " +
-                                         "RETURNS text " +
-                                         "LANGUAGE JAVA\n" +
-                                         "AS 'return val;'");
+        String typeName = createType("CREATE TYPE %s (txt text, i int)");
 
-        String fNameSCC = createFunction(KEYSPACE_PER_TEST, "text",
-                                         "CREATE OR REPLACE FUNCTION %s(val text) " +
-                                         "CALLED ON NULL INPUT " +
-                                         "RETURNS text " +
-                                         "LANGUAGE JAVA\n" +
-                                         "AS 'return \"\";'");
+        createFunction(KEYSPACE, "map<text,bigint>,list<text>",
+                       "CREATE FUNCTION IF NOT EXISTS %s(state map<text,bigint>, styles list<text>)\n" +
+                       "  RETURNS NULL ON NULL INPUT\n" +
+                       "  RETURNS map<text,bigint>\n" +
+                       "  LANGUAGE java\n" +
+                       "  AS $$\n" +
+                       "    for (String style : styles) {\n" +
+                       "      if (state.containsKey(style)) {\n" +
+                       "        state.put(style, state.get(style) + 1L);\n" +
+                       "      } else {\n" +
+                       "        state.put(style, 1L);\n" +
+                       "      }\n" +
+                       "    }\n" +
+                       "    return state;\n" +
+                       "  $$");
 
-        String fNameSRN = createFunction(KEYSPACE_PER_TEST, "text",
-                                         "CREATE OR REPLACE FUNCTION %s(val text) " +
-                                         "RETURNS NULL ON NULL INPUT " +
-                                         "RETURNS text " +
-                                         "LANGUAGE JAVA\n" +
-                                         "AS 'return val;'");
+        createFunction(KEYSPACE, "text",
+                                  "CREATE OR REPLACE FUNCTION %s("                 +
+                                  "  listText list<text>,"                         +
+                                  "  setText set<text>,"                           +
+                                  "  mapTextInt map<text, int>,"                   +
+                                  "  mapListTextSetInt map<frozen<list<text>>, frozen<set<int>>>," +
+                                  "  mapTextTuple map<text, frozen<tuple<int, text>>>," +
+                                  "  mapTextType map<text, frozen<" + typeName + ">>" +
+                                  ") "                                             +
+                                  "CALLED ON NULL INPUT "                          +
+                                  "RETURNS map<frozen<list<text>>, frozen<set<int>>> " +
+                                  "LANGUAGE JAVA\n"                                +
+                                  "AS $$" +
+                                  "     for (String s : listtext) {};" +
+                                  "     for (String s : settext) {};" +
+                                  "     for (String s : maptextint.keySet()) {};" +
+                                  "     for (Integer s : maptextint.values()) {};" +
+                                  "     for (java.util.List<String> l : maplisttextsetint.keySet()) {};" +
+                                  "     for (java.util.Set<Integer> s : maplisttextsetint.values()) {};" +
+                                  "     for (com.datastax.driver.core.TupleValue t : maptexttuple.values()) {};" +
+                                  "     for (com.datastax.driver.core.UDTValue u : maptexttype.values()) {};" +
+                                  "     return maplisttextsetint;" +
+                                  "$$");
+    }
 
-        String fNameSCN = createFunction(KEYSPACE_PER_TEST, "text",
-                                         "CREATE OR REPLACE FUNCTION %s(val text) " +
-                                         "RETURNS NULL ON NULL INPUT " +
-                                         "RETURNS text " +
-                                         "LANGUAGE JAVA\n" +
-                                         "AS 'return \"\";'");
+    @Test
+    public void testArgAndReturnTypes() throws Throwable
+    {
 
-        String fNameBRC = createFunction(KEYSPACE_PER_TEST, "blob",
-                                         "CREATE OR REPLACE FUNCTION %s(val blob) " +
-                                         "CALLED ON NULL INPUT " +
-                                         "RETURNS blob " +
-                                         "LANGUAGE JAVA\n" +
-                                         "AS 'return val;'");
+        String type = KEYSPACE + '.' + createType("CREATE TYPE %s (txt text, i int)");
 
-        String fNameBCC = createFunction(KEYSPACE_PER_TEST, "blob",
-                                         "CREATE OR REPLACE FUNCTION %s(val blob) " +
-                                         "CALLED ON NULL INPUT " +
-                                         "RETURNS blob " +
-                                         "LANGUAGE JAVA\n" +
-                                         "AS 'return ByteBuffer.allocate(0);'");
+        createTable("CREATE TABLE %s (key int primary key, udt frozen<" + type + ">)");
+        execute("INSERT INTO %s (key, udt) VALUES (1, {txt: 'foo', i: 42})");
 
-        String fNameBRN = createFunction(KEYSPACE_PER_TEST, "blob",
-                                         "CREATE OR REPLACE FUNCTION %s(val blob) " +
-                                         "RETURNS NULL ON NULL INPUT " +
-                                         "RETURNS blob " +
-                                         "LANGUAGE JAVA\n" +
-                                         "AS 'return val;'");
+        // Java UDFs
 
-        String fNameBCN = createFunction(KEYSPACE_PER_TEST, "blob",
-                                         "CREATE OR REPLACE FUNCTION %s(val blob) " +
-                                         "RETURNS NULL ON NULL INPUT " +
-                                         "RETURNS blob " +
-                                         "LANGUAGE JAVA\n" +
-                                         "AS 'return ByteBuffer.allocate(0);'");
+        String f = createFunction(KEYSPACE, "int",
+                                  "CREATE OR REPLACE FUNCTION %s(val int) " +
+                                  "RETURNS NULL ON NULL INPUT " +
+                                  "RETURNS " + type + ' ' +
+                                  "LANGUAGE JAVA\n" +
+                                  "AS 'return udfContext.newReturnUDTValue();';");
 
-        String fNameIRC = createFunction(KEYSPACE_PER_TEST, "int",
-                                         "CREATE OR REPLACE FUNCTION %s(val int) " +
-                                         "CALLED ON NULL INPUT " +
-                                         "RETURNS int " +
-                                         "LANGUAGE JAVA\n" +
-                                         "AS 'return val;'");
+        assertRows(execute("SELECT " + f + "(key) FROM %s"),
+                   row(userType("txt", null, "i", null)));
 
-        String fNameICC = createFunction(KEYSPACE_PER_TEST, "int",
-                                         "CREATE OR REPLACE FUNCTION %s(val int) " +
-                                         "CALLED ON NULL INPUT " +
-                                         "RETURNS int " +
-                                         "LANGUAGE JAVA\n" +
-                                         "AS 'return 0;'");
+        f = createFunction(KEYSPACE, "int",
+                           "CREATE OR REPLACE FUNCTION %s(val " + type + ") " +
+                           "RETURNS NULL ON NULL INPUT " +
+                           "RETURNS " + type + ' ' +
+                           "LANGUAGE JAVA\n" +
+                           "AS $$" +
+                           "   com.datastax.driver.core.UDTValue udt = udfContext.newArgUDTValue(\"val\");" +
+                           "   udt.setString(\"txt\", \"baz\");" +
+                           "   udt.setInt(\"i\", 88);" +
+                           "   return udt;" +
+                           "$$;");
 
-        String fNameIRN = createFunction(KEYSPACE_PER_TEST, "int",
-                                         "CREATE OR REPLACE FUNCTION %s(val int) " +
-                                         "RETURNS NULL ON NULL INPUT " +
-                                         "RETURNS int " +
-                                         "LANGUAGE JAVA\n" +
-                                         "AS 'return val;'");
+        assertRows(execute("SELECT " + f + "(udt) FROM %s"),
+                   row(userType("txt", "baz", "i", 88)));
 
-        String fNameICN = createFunction(KEYSPACE_PER_TEST, "int",
-                                         "CREATE OR REPLACE FUNCTION %s(val int) " +
-                                         "RETURNS NULL ON NULL INPUT " +
-                                         "RETURNS int " +
-                                         "LANGUAGE JAVA\n" +
-                                         "AS 'return 0;'");
+        f = createFunction(KEYSPACE, "int",
+                           "CREATE OR REPLACE FUNCTION %s(val " + type + ") " +
+                           "RETURNS NULL ON NULL INPUT " +
+                           "RETURNS tuple<text, int>" +
+                           "LANGUAGE JAVA\n" +
+                           "AS $$" +
+                           "   com.datastax.driver.core.TupleValue tv = udfContext.newReturnTupleValue();" +
+                           "   tv.setString(0, \"baz\");" +
+                           "   tv.setInt(1, 88);" +
+                           "   return tv;" +
+                           "$$;");
 
-        assertRows(execute("SELECT " + fNameSRC + "(sval) FROM %s"), row(""));
-        assertRows(execute("SELECT " + fNameSRN + "(sval) FROM %s"), row(""));
-        assertRows(execute("SELECT " + fNameSCC + "(sval) FROM %s"), row(""));
-        assertRows(execute("SELECT " + fNameSCN + "(sval) FROM %s"), row(""));
-        assertRows(execute("SELECT " + fNameSRC + "(aval) FROM %s"), row(""));
-        assertRows(execute("SELECT " + fNameSRN + "(aval) FROM %s"), row(""));
-        assertRows(execute("SELECT " + fNameSCC + "(aval) FROM %s"), row(""));
-        assertRows(execute("SELECT " + fNameSCN + "(aval) FROM %s"), row(""));
-        assertRows(execute("SELECT " + fNameBRC + "(bval) FROM %s"), row(ByteBufferUtil.EMPTY_BYTE_BUFFER));
-        assertRows(execute("SELECT " + fNameBRN + "(bval) FROM %s"), row(ByteBufferUtil.EMPTY_BYTE_BUFFER));
-        assertRows(execute("SELECT " + fNameBCC + "(bval) FROM %s"), row(ByteBufferUtil.EMPTY_BYTE_BUFFER));
-        assertRows(execute("SELECT " + fNameBCN + "(bval) FROM %s"), row(ByteBufferUtil.EMPTY_BYTE_BUFFER));
-        assertRows(execute("SELECT " + fNameIRC + "(empty_int) FROM %s"), row(new Object[]{ null }));
-        assertRows(execute("SELECT " + fNameIRN + "(empty_int) FROM %s"), row(new Object[]{ null }));
-        assertRows(execute("SELECT " + fNameICC + "(empty_int) FROM %s"), row(0));
-        assertRows(execute("SELECT " + fNameICN + "(empty_int) FROM %s"), row(new Object[]{ null }));
+        assertRows(execute("SELECT " + f + "(udt) FROM %s"),
+                   row(tuple("baz", 88)));
+
+        // JavaScript UDFs
+
+        f = createFunction(KEYSPACE, "int",
+                           "CREATE OR REPLACE FUNCTION %s(val int) " +
+                           "RETURNS NULL ON NULL INPUT " +
+                           "RETURNS " + type + ' ' +
+                           "LANGUAGE JAVASCRIPT\n" +
+                           "AS $$" +
+                           "   udt = udfContext.newReturnUDTValue();" +
+                           "   udt;" +
+                           "$$;");
+
+        assertRows(execute("SELECT " + f + "(key) FROM %s"),
+                   row(userType("txt", null, "i", null)));
+
+        f = createFunction(KEYSPACE, "int",
+                           "CREATE OR REPLACE FUNCTION %s(val " + type + ") " +
+                           "RETURNS NULL ON NULL INPUT " +
+                           "RETURNS " + type + ' ' +
+                           "LANGUAGE JAVASCRIPT\n" +
+                           "AS $$" +
+                           "   udt = udfContext.newArgUDTValue(0);" +
+                           "   udt.setString(\"txt\", \"baz\");" +
+                           "   udt.setInt(\"i\", 88);" +
+                           "   udt;" +
+                           "$$;");
+
+        assertRows(execute("SELECT " + f + "(udt) FROM %s"),
+                   row(userType("txt", "baz", "i", 88)));
+
+        f = createFunction(KEYSPACE, "int",
+                           "CREATE OR REPLACE FUNCTION %s(val " + type + ") " +
+                           "RETURNS NULL ON NULL INPUT " +
+                           "RETURNS tuple<text, int>" +
+                           "LANGUAGE JAVASCRIPT\n" +
+                           "AS $$" +
+                           "   tv = udfContext.newReturnTupleValue();" +
+                           "   tv.setString(0, \"baz\");" +
+                           "   tv.setInt(1, 88);" +
+                           "   tv;" +
+                           "$$;");
+
+        assertRows(execute("SELECT " + f + "(udt) FROM %s"),
+                   row(tuple("baz", 88)));
+
+        createFunction(KEYSPACE, "map",
+                       "CREATE FUNCTION %s(my_map map<text, text>)\n" +
+                       "         CALLED ON NULL INPUT\n" +
+                       "         RETURNS text\n" +
+                       "         LANGUAGE java\n" +
+                       "         AS $$\n" +
+                       "             String buffer = \"\";\n" +
+                       "             for(java.util.Map.Entry<String, String> entry: my_map.entrySet()) {\n" +
+                       "                 buffer = buffer + entry.getKey() + \": \" + entry.getValue() + \", \";\n" +
+                       "             }\n" +
+                       "             return buffer;\n" +
+                       "         $$;\n");
+    }
+
+    @Test
+    public void testImportJavaUtil() throws Throwable
+    {
+        createFunction(KEYSPACE, "list<text>",
+                "CREATE OR REPLACE FUNCTION %s(listText list<text>) "                                             +
+                        "CALLED ON NULL INPUT "                          +
+                        "RETURNS set<text> " +
+                        "LANGUAGE JAVA\n"                                +
+                        "AS $$\n" +
+                        "     Set<String> set = new HashSet<String>(); " +
+                        "     for (String s : listtext) {" +
+                        "            set.add(s);" +
+                        "     }" +
+                        "     return set;" +
+                        "$$");
+
+    }
+
+    @Test
+    public void testAnyUserTupleType() throws Throwable
+    {
+        createTable("CREATE TABLE %s (key int primary key, sval text)");
+        execute("INSERT INTO %s (key, sval) VALUES (1, 'foo')");
+
+        String udt = createType("CREATE TYPE %s (a int, b text, c bigint)");
+
+        String fUdt = createFunction(KEYSPACE, "text",
+                                     "CREATE OR REPLACE FUNCTION %s(arg text) " +
+                                     "CALLED ON NULL INPUT " +
+                                     "RETURNS " + udt + " " +
+                                     "LANGUAGE JAVA\n" +
+                                     "AS $$\n" +
+                                     "    UDTValue udt = udfContext.newUDTValue(\"" + udt + "\");" +
+                                     "    udt.setInt(\"a\", 42);" +
+                                     "    udt.setString(\"b\", \"42\");" +
+                                     "    udt.setLong(\"c\", 4242);" +
+                                     "    return udt;" +
+                                     "$$");
+
+        assertRows(execute("SELECT " + fUdt + "(sval) FROM %s"),
+                   row(userType("a", 42, "b", "42", "c", 4242L)));
+
+        String fTup = createFunction(KEYSPACE, "text",
+                                     "CREATE OR REPLACE FUNCTION %s(arg text) " +
+                                     "CALLED ON NULL INPUT " +
+                                     "RETURNS tuple<int, " + udt + "> " +
+                                     "LANGUAGE JAVA\n" +
+                                     "AS $$\n" +
+                                     "    UDTValue udt = udfContext.newUDTValue(\"" + udt + "\");" +
+                                     "    udt.setInt(\"a\", 42);" +
+                                     "    udt.setString(\"b\", \"42\");" +
+                                     "    udt.setLong(\"c\", 4242);" +
+                                     "    TupleValue tup = udfContext.newTupleValue(\"tuple<int," + udt + ">\");" +
+                                     "    tup.setInt(0, 88);" +
+                                     "    tup.setUDTValue(1, udt);" +
+                                     "    return tup;" +
+                                     "$$");
+
+        assertRows(execute("SELECT " + fTup + "(sval) FROM %s"),
+                   row(tuple(88, userType("a", 42, "b", "42", "c", 4242L))));
     }
 
     @Test(expected = SyntaxException.class)
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFTypesTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFTypesTest.java
index de98748..f789e25 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/UFTypesTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFTypesTest.java
@@ -34,6 +34,9 @@
 import com.datastax.driver.core.Row;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.transport.Event.SchemaChange.Change;
+import org.apache.cassandra.transport.Event.SchemaChange.Target;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.UUIDGen;
 
 public class UFTypesTest extends CQLTester
@@ -119,7 +122,7 @@
         Assert.assertNull(row.getBytes("t"));
         Assert.assertNull(row.getBytes("u"));
 
-        for (int version : PROTOCOL_VERSIONS)
+        for (ProtocolVersion version : PROTOCOL_VERSIONS)
         {
             Row r = executeNet(version, "SELECT " +
                                         fList + "(lst) as l, " +
@@ -451,19 +454,20 @@
         execute("INSERT INTO %s (a, b) VALUES (?, ?)", 2, tuple(4, 5));
         execute("INSERT INTO %s (a, b) VALUES (?, ?)", 3, tuple(7, 8));
 
-        assertInvalidMessage("The function arguments should not be frozen",
-                             "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".withFrozenArg(values frozen<tuple<int, int>>) " +
-                             "CALLED ON NULL INPUT " +
-                             "RETURNS text " +
-                             "LANGUAGE java\n" +
-                             "AS 'return values.toString();';");
-
-        assertInvalidMessage("The function return type should not be frozen",
-                             "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".frozenReturnType(values tuple<int, int>) " +
-                             "CALLED ON NULL INPUT " +
-                             "RETURNS frozen<tuple<int, int>> " +
-                             "LANGUAGE java\n" +
-                             "AS 'return values;';");
+        // Tuples are always frozen. Both 'tuple' and 'frozen tuple' have the same effect.
+        // So allows to create function with explicit frozen tuples as argument and return types.
+        String toDrop = createFunction(KEYSPACE,
+                                       "frozen<tuple<int, int>>",
+                                       "CREATE FUNCTION %s (values frozen<tuple<int, int>>) " +
+                                       "CALLED ON NULL INPUT " +
+                                       "RETURNS frozen<tuple<int, int>> " +
+                                       "LANGUAGE java\n" +
+                                       "AS 'return values;';");
+        // Same as above, dropping a function with explicity frozen tuple should be allowed.
+        assertSchemaChange("DROP FUNCTION " + toDrop + "(frozen<tuple<int, int>>);",
+                           Change.DROPPED, Target.FUNCTION,
+                           KEYSPACE, shortFunctionName(toDrop),
+                           "frozen<tuple<int, int>>");
 
         String functionName = createFunction(KEYSPACE,
                                              "tuple<int, int>",
@@ -489,8 +493,11 @@
         assertRows(execute("SELECT a FROM %s WHERE b = " + functionName + "(?)", tuple(1, 2)),
                    row(1));
 
-        assertInvalidMessage("The function arguments should not be frozen",
-                             "DROP FUNCTION " + functionName + "(frozen<tuple<int, int>>);");
+        assertSchemaChange("DROP FUNCTION " + functionName + "(frozen<tuple<int, int>>);",
+                           Change.DROPPED,
+                           Target.FUNCTION,
+                           KEYSPACE, shortFunctionName(functionName),
+                           "frozen<tuple<int, int>>");
     }
 
     @Test
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFVerifierTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFVerifierTest.java
index 0b78bf2..9a8e682 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/UFVerifierTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFVerifierTest.java
@@ -25,6 +25,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Set;
 
 import org.junit.Test;
 
@@ -38,7 +39,10 @@
 import org.apache.cassandra.cql3.validation.entities.udfverify.ClassWithInitializer;
 import org.apache.cassandra.cql3.validation.entities.udfverify.ClassWithInitializer2;
 import org.apache.cassandra.cql3.validation.entities.udfverify.ClassWithInitializer3;
+import org.apache.cassandra.cql3.validation.entities.udfverify.ClassWithInnerClass;
+import org.apache.cassandra.cql3.validation.entities.udfverify.ClassWithInnerClass2;
 import org.apache.cassandra.cql3.validation.entities.udfverify.ClassWithStaticInitializer;
+import org.apache.cassandra.cql3.validation.entities.udfverify.ClassWithStaticInnerClass;
 import org.apache.cassandra.cql3.validation.entities.udfverify.GoodClass;
 import org.apache.cassandra.cql3.validation.entities.udfverify.UseOfSynchronized;
 import org.apache.cassandra.cql3.validation.entities.udfverify.UseOfSynchronizedWithNotify;
@@ -46,6 +50,7 @@
 import org.apache.cassandra.cql3.validation.entities.udfverify.UseOfSynchronizedWithWait;
 import org.apache.cassandra.cql3.validation.entities.udfverify.UseOfSynchronizedWithWaitL;
 import org.apache.cassandra.cql3.validation.entities.udfverify.UseOfSynchronizedWithWaitLI;
+import org.apache.cassandra.cql3.validation.entities.udfverify.UsingMapEntry;
 
 import static org.junit.Assert.assertEquals;
 
@@ -57,14 +62,14 @@
     @Test
     public void testByteCodeVerifier()
     {
-        new UDFByteCodeVerifier().verify(readClass(GoodClass.class));
+        verify(GoodClass.class);
     }
 
     @Test
     public void testClassWithField()
     {
         assertEquals(new HashSet<>(Collections.singletonList("field declared: field")),
-                     new UDFByteCodeVerifier().verify(readClass(ClassWithField.class)));
+                     verify(ClassWithField.class));
     }
 
     @Test
@@ -72,7 +77,7 @@
     {
         assertEquals(new HashSet<>(Arrays.asList("field declared: field",
                                                  "initializer declared")),
-                     new UDFByteCodeVerifier().verify(readClass(ClassWithInitializer.class)));
+                     verify(ClassWithInitializer.class));
     }
 
     @Test
@@ -80,91 +85,129 @@
     {
         assertEquals(new HashSet<>(Arrays.asList("field declared: field",
                                                  "initializer declared")),
-                     new UDFByteCodeVerifier().verify(readClass(ClassWithInitializer2.class)));
+                     verify(ClassWithInitializer2.class));
     }
 
     @Test
     public void testClassWithInitializer3()
     {
         assertEquals(new HashSet<>(Collections.singletonList("initializer declared")),
-                     new UDFByteCodeVerifier().verify(readClass(ClassWithInitializer3.class)));
+                     verify(ClassWithInitializer3.class));
     }
 
     @Test
     public void testClassWithStaticInitializer()
     {
         assertEquals(new HashSet<>(Collections.singletonList("static initializer declared")),
-                     new UDFByteCodeVerifier().verify(readClass(ClassWithStaticInitializer.class)));
+                     verify(ClassWithStaticInitializer.class));
     }
 
     @Test
     public void testUseOfSynchronized()
     {
         assertEquals(new HashSet<>(Collections.singletonList("use of synchronized")),
-                     new UDFByteCodeVerifier().verify(readClass(UseOfSynchronized.class)));
+                     verify(UseOfSynchronized.class));
     }
 
     @Test
     public void testUseOfSynchronizedWithNotify()
     {
         assertEquals(new HashSet<>(Arrays.asList("use of synchronized", "call to java.lang.Object.notify()")),
-                     new UDFByteCodeVerifier().verify(readClass(UseOfSynchronizedWithNotify.class)));
+                     verify(UseOfSynchronizedWithNotify.class));
     }
 
     @Test
     public void testUseOfSynchronizedWithNotifyAll()
     {
         assertEquals(new HashSet<>(Arrays.asList("use of synchronized", "call to java.lang.Object.notifyAll()")),
-                     new UDFByteCodeVerifier().verify(readClass(UseOfSynchronizedWithNotifyAll.class)));
+                     verify(UseOfSynchronizedWithNotifyAll.class));
     }
 
     @Test
     public void testUseOfSynchronizedWithWait()
     {
         assertEquals(new HashSet<>(Arrays.asList("use of synchronized", "call to java.lang.Object.wait()")),
-                     new UDFByteCodeVerifier().verify(readClass(UseOfSynchronizedWithWait.class)));
+                     verify(UseOfSynchronizedWithWait.class));
     }
 
     @Test
     public void testUseOfSynchronizedWithWaitL()
     {
         assertEquals(new HashSet<>(Arrays.asList("use of synchronized", "call to java.lang.Object.wait()")),
-                     new UDFByteCodeVerifier().verify(readClass(UseOfSynchronizedWithWaitL.class)));
+                     verify(UseOfSynchronizedWithWaitL.class));
     }
 
     @Test
     public void testUseOfSynchronizedWithWaitI()
     {
         assertEquals(new HashSet<>(Arrays.asList("use of synchronized", "call to java.lang.Object.wait()")),
-                     new UDFByteCodeVerifier().verify(readClass(UseOfSynchronizedWithWaitLI.class)));
+                     verify(UseOfSynchronizedWithWaitLI.class));
     }
 
     @Test
     public void testCallClone()
     {
         assertEquals(new HashSet<>(Collections.singletonList("call to java.lang.Object.clone()")),
-                     new UDFByteCodeVerifier().verify(readClass(CallClone.class)));
+                     verify(CallClone.class));
     }
 
     @Test
     public void testCallFinalize()
     {
         assertEquals(new HashSet<>(Collections.singletonList("call to java.lang.Object.finalize()")),
-                     new UDFByteCodeVerifier().verify(readClass(CallFinalize.class)));
+                     verify(CallFinalize.class));
     }
 
     @Test
     public void testCallComDatastax()
     {
         assertEquals(new HashSet<>(Collections.singletonList("call to com.datastax.driver.core.DataType.cint()")),
-                     new UDFByteCodeVerifier().addDisallowedPackage("com/").verify(readClass(CallComDatastax.class)));
+                     verify("com/", CallComDatastax.class));
     }
 
     @Test
     public void testCallOrgApache()
     {
         assertEquals(new HashSet<>(Collections.singletonList("call to org.apache.cassandra.config.DatabaseDescriptor.getClusterName()")),
-                     new UDFByteCodeVerifier().addDisallowedPackage("org/").verify(readClass(CallOrgApache.class)));
+                     verify("org/", CallOrgApache.class));
+    }
+
+    @Test
+    public void testClassStaticInnerClass()
+    {
+        assertEquals(new HashSet<>(Collections.singletonList("class declared as inner class")),
+                     verify(ClassWithStaticInnerClass.class));
+    }
+
+    @Test
+    public void testUsingMapEntry()
+    {
+        assertEquals(Collections.emptySet(),
+                     verify(UsingMapEntry.class));
+    }
+
+    @Test
+    public void testClassInnerClass()
+    {
+        assertEquals(new HashSet<>(Collections.singletonList("class declared as inner class")),
+                     verify(ClassWithInnerClass.class));
+    }
+
+    @Test
+    public void testClassInnerClass2()
+    {
+        assertEquals(Collections.emptySet(),
+                     verify(ClassWithInnerClass2.class));
+    }
+
+    private Set<String> verify(Class cls)
+    {
+        return new UDFByteCodeVerifier().verify(cls.getName(), readClass(cls));
+    }
+
+    private Set<String> verify(String disallowedPkg, Class cls)
+    {
+        return new UDFByteCodeVerifier().addDisallowedPackage(disallowedPkg).verify(cls.getName(), readClass(cls));
     }
 
     @SuppressWarnings("resource")
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UserTypesTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UserTypesTest.java
index 68c0b8c..646484c 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/UserTypesTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UserTypesTest.java
@@ -30,10 +30,12 @@
 public class UserTypesTest extends CQLTester
 {
     @BeforeClass
-    public static void setUpClass()
+    public static void setUpClass()     // overrides CQLTester.setUpClass()
     {
         // Selecting partitioner for a table is not exposed on CREATE TABLE.
         StorageService.instance.setPartitionerUnsafe(ByteOrderedPartitioner.instance);
+
+        prepareServer();
     }
 
     @Test
@@ -60,7 +62,7 @@
         createTable("CREATE TABLE %s (k int PRIMARY KEY, t frozen<" + type + ">)");
         assertInvalidMessage("Invalid remaining data after end of tuple value",
                              "INSERT INTO %s (k, t) VALUES (0, ?)",
-                             userType(1, tuple(1, "1", 1.0, 1)));
+                             userType("a", 1, "b", tuple(1, "1", 1.0, 1)));
 
         assertInvalidMessage("Invalid user type literal for t: field b is not of type frozen<tuple<int, text, double>>",
                              "INSERT INTO %s (k, t) VALUES (0, {a: 1, b: (1, '1', 1.0, 1)})");
@@ -94,32 +96,86 @@
         execute("INSERT INTO %s(k, v) VALUES (?, {x:?})", 1, -104.99251);
         execute("UPDATE %s SET b = ? WHERE k = ?", true, 1);
 
-        assertRows(execute("SELECT v.x FROM %s WHERE k = ? AND v = {x:?}", 1, -104.99251),
-            row(-104.99251)
-        );
-
-        flush();
-
-        assertRows(execute("SELECT v.x FROM %s WHERE k = ? AND v = {x:?}", 1, -104.99251),
-                   row(-104.99251)
+        beforeAndAfterFlush(() ->
+            assertRows(execute("SELECT v.x FROM %s WHERE k = ? AND v = {x:?}", 1, -104.99251),
+                row(-104.99251)
+            )
         );
     }
 
     @Test
-    public void testCreateInvalidTablesWithUDT() throws Throwable
+    public void testInvalidUDTStatements() throws Throwable
     {
-        String myType = createType("CREATE TYPE %s (f int)");
+        String typename = createType("CREATE TYPE %s (a int)");
+        String myType = KEYSPACE + '.' + typename;
 
-        // Using a UDT without frozen shouldn't work
-        assertInvalidMessage("Non-frozen User-Defined types are not supported, please use frozen<>",
-                             "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v " + KEYSPACE + '.' + myType + ")");
+        // non-frozen UDTs in a table PK
+        assertInvalidMessage("Invalid non-frozen user-defined type for PRIMARY KEY component k",
+                "CREATE TABLE " + KEYSPACE + ".wrong (k " + myType + " PRIMARY KEY , v int)");
+        assertInvalidMessage("Invalid non-frozen user-defined type for PRIMARY KEY component k2",
+                "CREATE TABLE " + KEYSPACE + ".wrong (k1 int, k2 " + myType + ", v int, PRIMARY KEY (k1, k2))");
 
+        // non-frozen UDTs in a collection
+        assertInvalidMessage("Non-frozen UDTs are not allowed inside collections: list<" + myType + ">",
+                "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v list<" + myType + ">)");
+        assertInvalidMessage("Non-frozen UDTs are not allowed inside collections: set<" + myType + ">",
+                "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v set<" + myType + ">)");
+        assertInvalidMessage("Non-frozen UDTs are not allowed inside collections: map<" + myType + ", int>",
+                "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v map<" + myType + ", int>)");
+        assertInvalidMessage("Non-frozen UDTs are not allowed inside collections: map<int, " + myType + ">",
+                "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v map<int, " + myType + ">)");
+
+        // non-frozen UDT in a collection (as part of a UDT definition)
+        assertInvalidMessage("Non-frozen UDTs are not allowed inside collections: list<" + myType + ">",
+                "CREATE TYPE " + KEYSPACE + ".wrong (a int, b list<" + myType + ">)");
+
+        // non-frozen UDT in a UDT
+        assertInvalidMessage("A user type cannot contain non-frozen UDTs",
+                "CREATE TYPE " + KEYSPACE + ".wrong (a int, b " + myType + ")");
+
+        // referencing a UDT in another keyspace
         assertInvalidMessage("Statement on keyspace " + KEYSPACE + " cannot refer to a user type in keyspace otherkeyspace;" +
                              " user types can only be used in the keyspace they are defined in",
                              "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v frozen<otherKeyspace.myType>)");
 
+        // referencing an unknown UDT
         assertInvalidMessage("Unknown type " + KEYSPACE + ".unknowntype",
                              "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v frozen<" + KEYSPACE + '.' + "unknownType>)");
+
+        // bad deletions on frozen UDTs
+        createTable("CREATE TABLE %s (a int PRIMARY KEY, b frozen<" + myType + ">, c int)");
+        assertInvalidMessage("Frozen UDT column b does not support field deletion", "DELETE b.a FROM %s WHERE a = 0");
+        assertInvalidMessage("Invalid field deletion operation for non-UDT column c", "DELETE c.a FROM %s WHERE a = 0");
+
+        // bad updates on frozen UDTs
+        assertInvalidMessage("Invalid operation (b.a = 0) for frozen UDT column b", "UPDATE %s SET b.a = 0 WHERE a = 0");
+        assertInvalidMessage("Invalid operation (c.a = 0) for non-UDT column c", "UPDATE %s SET c.a = 0 WHERE a = 0");
+
+        // bad deletions on non-frozen UDTs
+        createTable("CREATE TABLE %s (a int PRIMARY KEY, b " + myType + ", c int)");
+        assertInvalidMessage("UDT column b does not have a field named foo", "DELETE b.foo FROM %s WHERE a = 0");
+
+        // bad updates on non-frozen UDTs
+        assertInvalidMessage("UDT column b does not have a field named foo", "UPDATE %s SET b.foo = 0 WHERE a = 0");
+
+        // bad insert on non-frozen UDTs
+        assertInvalidMessage("Unknown field 'foo' in value of user defined type", "INSERT INTO %s (a, b, c) VALUES (0, {a: 0, foo: 0}, 0)");
+        if (usePrepared())
+        {
+            assertInvalidMessage("Invalid remaining data after end of UDT value",
+                    "INSERT INTO %s (a, b, c) VALUES (0, ?, 0)", userType("a", 0, "foo", 0));
+        }
+        else
+        {
+            assertInvalidMessage("Unknown field 'foo' in value of user defined type " + typename,
+                    "INSERT INTO %s (a, b, c) VALUES (0, ?, 0)", userType("a", 0, "foo", 0));
+        }
+
+        // non-frozen UDT with non-frozen nested collection
+        String typename2 = createType("CREATE TYPE %s (bar int, foo list<int>)");
+        String myType2 = KEYSPACE + '.' + typename2;
+        assertInvalidMessage("Non-frozen UDTs with nested non-frozen collections are not supported",
+                "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v " + myType2 + ")");
     }
 
     @Test
@@ -127,24 +183,61 @@
     {
         String myType = KEYSPACE + '.' + createType("CREATE TYPE %s (a int)");
         createTable("CREATE TABLE %s (a int PRIMARY KEY, b frozen<" + myType + ">)");
-        execute("INSERT INTO %s (a, b) VALUES (1, {a: 1})");
+        execute("INSERT INTO %s (a, b) VALUES (1, ?)", userType("a", 1));
 
         assertRows(execute("SELECT b.a FROM %s"), row(1));
 
         flush();
 
-        execute("ALTER TYPE " + myType + " ADD b int");
-        execute("INSERT INTO %s (a, b) VALUES (2, {a: 2, b :2})");
+        schemaChange("ALTER TYPE " + myType + " ADD b int");
+        execute("INSERT INTO %s (a, b) VALUES (2, ?)", userType("a", 2, "b", 2));
 
-        assertRows(execute("SELECT b.a, b.b FROM %s"),
-                   row(1, null),
-                   row(2, 2));
+        beforeAndAfterFlush(() ->
+            assertRows(execute("SELECT b.a, b.b FROM %s"),
+                       row(1, null),
+                       row(2, 2))
+        );
+    }
 
-        flush();
+    @Test
+    public void testAlterNonFrozenUDT() throws Throwable
+    {
+        String myType = KEYSPACE + '.' + createType("CREATE TYPE %s (a int, b text)");
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v " + myType + ")");
+        execute("INSERT INTO %s (k, v) VALUES (0, ?)", userType("a", 1, "b", "abc"));
 
-        assertRows(execute("SELECT b.a, b.b FROM %s"),
-                   row(1, null),
-                   row(2, 2));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT v FROM %s"), row(userType("a", 1, "b", "abc")));
+            assertRows(execute("SELECT v.a FROM %s"), row(1));
+            assertRows(execute("SELECT v.b FROM %s"), row("abc"));
+        });
+
+        schemaChange("ALTER TYPE " + myType + " RENAME b TO foo");
+        assertRows(execute("SELECT v FROM %s"), row(userType("a", 1, "b", "abc")));
+        assertRows(execute("SELECT v.a FROM %s"), row(1));
+        assertRows(execute("SELECT v.foo FROM %s"), row("abc"));
+
+        execute("UPDATE %s SET v.foo = 'def' WHERE k = 0");
+        assertRows(execute("SELECT v FROM %s"), row(userType("a", 1, "foo", "def")));
+        assertRows(execute("SELECT v.a FROM %s"), row(1));
+        assertRows(execute("SELECT v.foo FROM %s"), row("def"));
+
+        execute("INSERT INTO %s (k, v) VALUES (0, ?)", userType("a", 2, "foo", "def"));
+        assertRows(execute("SELECT v FROM %s"), row(userType("a", 2, "foo", "def")));
+        assertRows(execute("SELECT v.a FROM %s"), row(2));
+        assertRows(execute("SELECT v.foo FROM %s"), row("def"));
+
+        schemaChange("ALTER TYPE " + myType + " ADD c int");
+        assertRows(execute("SELECT v FROM %s"), row(userType("a", 2, "foo", "def", "c", null)));
+        assertRows(execute("SELECT v.a FROM %s"), row(2));
+        assertRows(execute("SELECT v.foo FROM %s"), row("def"));
+        assertRows(execute("SELECT v.c FROM %s"), row(new Object[] {null}));
+
+        execute("INSERT INTO %s (k, v) VALUES (0, ?)", userType("a", 3, "foo", "abc", "c", 0));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT v FROM %s"), row(userType("a", 3, "foo", "abc", "c", 0)));
+            assertRows(execute("SELECT v.c FROM %s"), row(0));
+        });
     }
 
     @Test
@@ -155,11 +248,14 @@
         String myOtherType = createType("CREATE TYPE %s (a frozen<" + myType + ">)");
         createTable("CREATE TABLE %s (k int PRIMARY KEY, v frozen<" + myType + ">, z frozen<" + myOtherType + ">)");
 
-        assertInvalidMessage("Invalid unset value for field 'y' of user defined type " + myType,
-                             "INSERT INTO %s (k, v) VALUES (10, {x:?, y:?})", 1, unset());
+        if (usePrepared())
+        {
+            assertInvalidMessage("Invalid unset value for field 'y' of user defined type " + myType,
+                    "INSERT INTO %s (k, v) VALUES (10, {x:?, y:?})", 1, unset());
 
-        assertInvalidMessage("Invalid unset value for field 'y' of user defined type " + myType,
-                             "INSERT INTO %s (k, v, z) VALUES (10, {x:?, y:?}, {a:{x: ?, y: ?}})", 1, 1, 1, unset());
+            assertInvalidMessage("Invalid unset value for field 'y' of user defined type " + myType,
+                    "INSERT INTO %s (k, v, z) VALUES (10, {x:?, y:?}, {a:{x: ?, y: ?}})", 1, 1, 1, unset());
+        }
     }
 
     @Test
@@ -174,28 +270,22 @@
 
             createTable("CREATE TABLE %s (x int PRIMARY KEY, y " + columnType + ")");
 
-            execute("INSERT INTO %s (x, y) VALUES(1, {'firstValue':{a:1}})");
-            assertRows(execute("SELECT * FROM %s"), row(1, map("firstValue", userType(1))));
+            execute("INSERT INTO %s (x, y) VALUES(1, ?)", map("firstValue", userType("a", 1)));
+            assertRows(execute("SELECT * FROM %s"), row(1, map("firstValue", userType("a", 1))));
             flush();
 
             execute("ALTER TYPE " + KEYSPACE + "." + ut1 + " ADD b int");
-            execute("INSERT INTO %s (x, y) VALUES(2, {'secondValue':{a:2, b:2}})");
-            execute("INSERT INTO %s (x, y) VALUES(3, {'thirdValue':{a:3}})");
-            execute("INSERT INTO %s (x, y) VALUES(4, {'fourthValue':{b:4}})");
+            execute("INSERT INTO %s (x, y) VALUES(2, ?)", map("secondValue", userType("a", 2, "b", 2)));
+            execute("INSERT INTO %s (x, y) VALUES(3, ?)", map("thirdValue", userType("a", 3, "b", null)));
+            execute("INSERT INTO %s (x, y) VALUES(4, ?)", map("fourthValue", userType("a", null, "b", 4)));
 
-            assertRows(execute("SELECT * FROM %s"),
-                    row(1, map("firstValue", userType(1))),
-                    row(2, map("secondValue", userType(2, 2))),
-                    row(3, map("thirdValue", userType(3, null))),
-                    row(4, map("fourthValue", userType(null, 4))));
-
-            flush();
-
-            assertRows(execute("SELECT * FROM %s"),
-                    row(1, map("firstValue", userType(1))),
-                    row(2, map("secondValue", userType(2, 2))),
-                    row(3, map("thirdValue", userType(3, null))),
-                    row(4, map("fourthValue", userType(null, 4))));
+            beforeAndAfterFlush(() ->
+                assertRows(execute("SELECT * FROM %s"),
+                        row(1, map("firstValue", userType("a", 1))),
+                        row(2, map("secondValue", userType("a", 2, "b", 2))),
+                        row(3, map("thirdValue", userType("a", 3, "b", null))),
+                        row(4, map("fourthValue", userType("a", null, "b", 4))))
+            );
         }
     }
 
@@ -209,22 +299,18 @@
 
         execute("INSERT INTO %s (x, y) VALUES(1, {'firstValue': {a: 1}})");
         assertRows(execute("SELECT * FROM %s"),
-                   row(1, map("firstValue", userType(1))));
+                   row(1, map("firstValue", userType("a", 1))));
 
         flush();
 
         execute("ALTER TYPE " + columnType + " ADD b int");
         execute("UPDATE %s SET y['secondValue'] = {a: 2, b: 2} WHERE x = 1");
 
-        assertRows(execute("SELECT * FROM %s"),
-                   row(1, map("firstValue", userType(1),
-                              "secondValue", userType(2, 2))));
-
-        flush();
-
-        assertRows(execute("SELECT * FROM %s"),
-                   row(1, map("firstValue", userType(1),
-                              "secondValue", userType(2, 2))));
+        beforeAndAfterFlush(() ->
+                            assertRows(execute("SELECT * FROM %s"),
+                                       row(1, map("firstValue", userType("a", 1),
+                                                  "secondValue", userType("a", 2, "b", 2))))
+        );
     }
 
     @Test
@@ -239,28 +325,22 @@
 
             createTable("CREATE TABLE %s (x int PRIMARY KEY, y " + columnType + ")");
 
-            execute("INSERT INTO %s (x, y) VALUES(1, {1} )");
-            assertRows(execute("SELECT * FROM %s"), row(1, set(userType(1))));
+            execute("INSERT INTO %s (x, y) VALUES(1, ?)", set(userType("a", 1)));
+            assertRows(execute("SELECT * FROM %s"), row(1, set(userType("a", 1))));
             flush();
 
             execute("ALTER TYPE " + KEYSPACE + "." + ut1 + " ADD b int");
-            execute("INSERT INTO %s (x, y) VALUES(2, {{a:2, b:2}})");
-            execute("INSERT INTO %s (x, y) VALUES(3, {{a:3}})");
-            execute("INSERT INTO %s (x, y) VALUES(4, {{b:4}})");
+            execute("INSERT INTO %s (x, y) VALUES(2, ?)", set(userType("a", 2, "b", 2)));
+            execute("INSERT INTO %s (x, y) VALUES(3, ?)", set(userType("a", 3, "b", null)));
+            execute("INSERT INTO %s (x, y) VALUES(4, ?)", set(userType("a", null, "b", 4)));
 
-            assertRows(execute("SELECT * FROM %s"),
-                    row(1, set(userType(1))),
-                    row(2, set(userType(2, 2))),
-                    row(3, set(userType(3, null))),
-                    row(4, set(userType(null, 4))));
-
-            flush();
-
-            assertRows(execute("SELECT * FROM %s"),
-                    row(1, set(userType(1))),
-                    row(2, set(userType(2, 2))),
-                    row(3, set(userType(3, null))),
-                    row(4, set(userType(null, 4))));
+            beforeAndAfterFlush(() ->
+                assertRows(execute("SELECT * FROM %s"),
+                        row(1, set(userType("a", 1))),
+                        row(2, set(userType("a", 2, "b", 2))),
+                        row(3, set(userType("a", 3, "b", null))),
+                        row(4, set(userType("a", null, "b", 4))))
+            );
         }
     }
 
@@ -276,28 +356,22 @@
 
             createTable("CREATE TABLE %s (x int PRIMARY KEY, y " + columnType + ")");
 
-            execute("INSERT INTO %s (x, y) VALUES(1, [1] )");
-            assertRows(execute("SELECT * FROM %s"), row(1, list(userType(1))));
+            execute("INSERT INTO %s (x, y) VALUES(1, ?)", list(userType("a", 1)));
+            assertRows(execute("SELECT * FROM %s"), row(1, list(userType("a", 1))));
             flush();
 
             execute("ALTER TYPE " + KEYSPACE + "." + ut1 + " ADD b int");
-            execute("INSERT INTO %s (x, y) VALUES(2, [{a:2, b:2}])");
-            execute("INSERT INTO %s (x, y) VALUES(3, [{a:3}])");
-            execute("INSERT INTO %s (x, y) VALUES(4, [{b:4}])");
+            execute("INSERT INTO %s (x, y) VALUES (2, ?)", list(userType("a", 2, "b", 2)));
+            execute("INSERT INTO %s (x, y) VALUES (3, ?)", list(userType("a", 3, "b", null)));
+            execute("INSERT INTO %s (x, y) VALUES (4, ?)", list(userType("a", null, "b", 4)));
 
-            assertRows(execute("SELECT * FROM %s"),
-                    row(1, list(userType(1))),
-                    row(2, list(userType(2, 2))),
-                    row(3, list(userType(3, null))),
-                    row(4, list(userType(null, 4))));
-
-            flush();
-
-            assertRows(execute("SELECT * FROM %s"),
-                    row(1, list(userType(1))),
-                    row(2, list(userType(2, 2))),
-                    row(3, list(userType(3, null))),
-                    row(4, list(userType(null, 4))));
+            beforeAndAfterFlush(() ->
+                assertRows(execute("SELECT * FROM %s"),
+                        row(1, list(userType("a", 1))),
+                        row(2, list(userType("a", 2, "b", 2))),
+                        row(3, list(userType("a", 3, "b", null))),
+                        row(4, list(userType("a", null, "b", 4))))
+            );
         }
     }
 
@@ -308,28 +382,22 @@
 
         createTable("CREATE TABLE %s (a int PRIMARY KEY, b frozen<tuple<int, " + KEYSPACE + "." + type + ">>)");
 
-        execute("INSERT INTO %s (a, b) VALUES(1, (1, {a:1, b:1}))");
-        assertRows(execute("SELECT * FROM %s"), row(1, tuple(1, userType(1, 1))));
+        execute("INSERT INTO %s (a, b) VALUES(1, (1, ?))", userType("a", 1, "b", 1));
+        assertRows(execute("SELECT * FROM %s"), row(1, tuple(1, userType("a", 1, "b", 1))));
         flush();
 
         execute("ALTER TYPE " + KEYSPACE + "." + type + " ADD c int");
-        execute("INSERT INTO %s (a, b) VALUES(2, (2, {a: 2, b: 2, c: 2}))");
-        execute("INSERT INTO %s (a, b) VALUES(3, (3, {a: 3, b: 3}))");
-        execute("INSERT INTO %s (a, b) VALUES(4, (4, {b:4}))");
+        execute("INSERT INTO %s (a, b) VALUES (2, (2, ?))", userType("a", 2, "b", 2, "c", 2));
+        execute("INSERT INTO %s (a, b) VALUES (3, (3, ?))", userType("a", 3, "b", 3, "c", null));
+        execute("INSERT INTO %s (a, b) VALUES (4, (4, ?))", userType("a", null, "b", 4, "c", null));
 
-        assertRows(execute("SELECT * FROM %s"),
-                   row(1, tuple(1, userType(1, 1))),
-                   row(2, tuple(2, userType(2, 2, 2))),
-                   row(3, tuple(3, userType(3, 3, null))),
-                   row(4, tuple(4, userType(null, 4, null))));
-
-        flush();
-
-        assertRows(execute("SELECT * FROM %s"),
-                   row(1, tuple(1, userType(1, 1))),
-                   row(2, tuple(2, userType(2, 2, 2))),
-                   row(3, tuple(3, userType(3, 3, null))),
-                   row(4, tuple(4, userType(null, 4, null))));
+        beforeAndAfterFlush(() ->
+            assertRows(execute("SELECT * FROM %s"),
+                    row(1, tuple(1, userType("a", 1, "b", 1))),
+                    row(2, tuple(2, userType("a", 2, "b", 2, "c", 2))),
+                    row(3, tuple(3, userType("a", 3, "b", 3, "c", null))),
+                    row(4, tuple(4, userType("a", null, "b", 4, "c", null))))
+        );
     }
 
     @Test
@@ -339,28 +407,22 @@
 
         createTable("CREATE TABLE %s (a int PRIMARY KEY, b frozen<tuple<int, tuple<int, " + KEYSPACE + "." + type + ">>>)");
 
-        execute("INSERT INTO %s (a, b) VALUES(1, (1, (1, {a:1, b:1})))");
-        assertRows(execute("SELECT * FROM %s"), row(1, tuple(1, tuple(1, userType(1, 1)))));
+        execute("INSERT INTO %s (a, b) VALUES(1, (1, (1, ?)))", userType("a", 1, "b", 1));
+        assertRows(execute("SELECT * FROM %s"), row(1, tuple(1, tuple(1, userType("a", 1, "b", 1)))));
         flush();
 
         execute("ALTER TYPE " + KEYSPACE + "." + type + " ADD c int");
-        execute("INSERT INTO %s (a, b) VALUES(2, (2, (1, {a: 2, b: 2, c: 2})))");
-        execute("INSERT INTO %s (a, b) VALUES(3, (3, (1, {a: 3, b: 3})))");
-        execute("INSERT INTO %s (a, b) VALUES(4, (4, (1, {b:4})))");
+        execute("INSERT INTO %s (a, b) VALUES(2, (2, (1, ?)))", userType("a", 2, "b", 2, "c", 2));
+        execute("INSERT INTO %s (a, b) VALUES(3, (3, ?))", tuple(1, userType("a", 3, "b", 3, "c", null)));
+        execute("INSERT INTO %s (a, b) VALUES(4, ?)", tuple(4, tuple(1, userType("a", null, "b", 4, "c", null))));
 
-        assertRows(execute("SELECT * FROM %s"),
-                   row(1, tuple(1, tuple(1, userType(1, 1)))),
-                   row(2, tuple(2, tuple(1, userType(2, 2, 2)))),
-                   row(3, tuple(3, tuple(1, userType(3, 3, null)))),
-                   row(4, tuple(4, tuple(1, userType(null, 4, null)))));
-
-        flush();
-
-        assertRows(execute("SELECT * FROM %s"),
-                   row(1, tuple(1, tuple(1, userType(1, 1)))),
-                   row(2, tuple(2, tuple(1, userType(2, 2, 2)))),
-                   row(3, tuple(3, tuple(1, userType(3, 3, null)))),
-                   row(4, tuple(4, tuple(1, userType(null, 4, null)))));
+        beforeAndAfterFlush(() ->
+            assertRows(execute("SELECT * FROM %s"),
+                    row(1, tuple(1, tuple(1, userType("a", 1, "b", 1)))),
+                    row(2, tuple(2, tuple(1, userType("a", 2, "b", 2, "c", 2)))),
+                    row(3, tuple(3, tuple(1, userType("a", 3, "b", 3, "c", null)))),
+                    row(4, tuple(4, tuple(1, userType("a", null, "b", 4, "c", null)))))
+        );
     }
 
     @Test
@@ -371,28 +433,24 @@
 
         createTable("CREATE TABLE %s (a int PRIMARY KEY, b frozen<" + KEYSPACE + "." + otherType + ">)");
 
-        execute("INSERT INTO %s (a, b) VALUES(1, {x: {a:1, b:1}})");
+        execute("INSERT INTO %s (a, b) VALUES(1, {x: ?})", userType("a", 1, "b", 1));
+        assertRows(execute("SELECT b.x.a, b.x.b FROM %s"), row(1, 1));
+        execute("INSERT INTO %s (a, b) VALUES(1, ?)", userType("x", userType("a", 1, "b", 1)));
         assertRows(execute("SELECT b.x.a, b.x.b FROM %s"), row(1, 1));
         flush();
 
         execute("ALTER TYPE " + KEYSPACE + "." + type + " ADD c int");
-        execute("INSERT INTO %s (a, b) VALUES(2, {x: {a: 2, b: 2, c: 2}})");
-        execute("INSERT INTO %s (a, b) VALUES(3, {x: {a: 3, b: 3}})");
-        execute("INSERT INTO %s (a, b) VALUES(4, {x: {b:4}})");
+        execute("INSERT INTO %s (a, b) VALUES(2, {x: ?})", userType("a", 2, "b", 2, "c", 2));
+        execute("INSERT INTO %s (a, b) VALUES(3, {x: ?})", userType("a", 3, "b", 3));
+        execute("INSERT INTO %s (a, b) VALUES(4, {x: ?})", userType("a", null, "b", 4));
 
-        assertRows(execute("SELECT b.x.a, b.x.b, b.x.c FROM %s"),
-                   row(1, 1, null),
-                   row(2, 2, 2),
-                   row(3, 3, null),
-                   row(null, 4, null));
-
-        flush();
-
-        assertRows(execute("SELECT b.x.a, b.x.b, b.x.c FROM %s"),
-                   row(1, 1, null),
-                   row(2, 2, 2),
-                   row(3, 3, null),
-                   row(null, 4, null));
+        beforeAndAfterFlush(() ->
+            assertRows(execute("SELECT b.x.a, b.x.b, b.x.c FROM %s"),
+                       row(1, 1, null),
+                       row(2, 2, 2),
+                       row(3, 3, null),
+                       row(null, 4, null))
+        );
     }
 
     /**
@@ -432,10 +490,11 @@
 
         createTable("CREATE TABLE %s (id int PRIMARY KEY, val frozen<" + type2 + ">)");
 
-        execute("INSERT INTO %s (id, val) VALUES (0, { s : {{ s : {'foo', 'bar'}, m : { 'foo' : 'bar' }, l : ['foo', 'bar']} }})");
+        execute("INSERT INTO %s (id, val) VALUES (0, ?)",
+                userType("s", set(userType("s", set("foo", "bar"), "m", map("foo", "bar"), "l", list("foo", "bar")))));
 
-        // TODO: check result once we have an easy way to do it. For now we just check it doesn't crash
-        execute("SELECT * FROM %s");
+        assertRows(execute("SELECT * FROM %s"),
+                row(0, userType("s", set(userType("s", set("foo", "bar"), "m", map("foo", "bar"), "l", list("foo", "bar"))))));
     }
 
     /**
@@ -447,9 +506,11 @@
         String typeName = createType("CREATE TYPE %s (fooint int, fooset set <text>)");
         createTable("CREATE TABLE %s (key int PRIMARY KEY, data frozen <" + typeName + ">)");
 
-        execute("INSERT INTO %s (key, data) VALUES (1, {fooint: 1, fooset: {'2'}})");
+        execute("INSERT INTO %s (key, data) VALUES (1, ?)", userType("fooint", 1, "fooset", set("2")));
         execute("ALTER TYPE " + keyspace() + "." + typeName + " ADD foomap map <int,text>");
-        execute("INSERT INTO %s (key, data) VALUES (1, {fooint: 1, fooset: {'2'}, foomap: {3 : 'bar'}})");
+        execute("INSERT INTO %s (key, data) VALUES (1, ?)", userType("fooint", 1, "fooset", set("2"), "foomap", map(3, "bar")));
+        assertRows(execute("SELECT * FROM %s"),
+                row(1, userType("fooint", 1, "fooset", set("2"), "foomap", map(3, "bar"))));
     }
 
     @Test
@@ -513,13 +574,13 @@
 
         type1 = createType("CREATE TYPE %s (foo ascii)");
         String type2 = createType("CREATE TYPE %s (foo frozen<" + type1 + ">)");
-        assertComplexInvalidAlterDropStatements(type1, type2, "{foo: 'abc'}");
+        assertComplexInvalidAlterDropStatements(type1, type2, "{foo: {foo: 'abc'}}");
 
         type1 = createType("CREATE TYPE %s (foo ascii)");
         type2 = createType("CREATE TYPE %s (foo frozen<" + type1 + ">)");
         assertComplexInvalidAlterDropStatements(type1,
                                                 "list<frozen<" + type2 + ">>",
-                                                "[{foo: 'abc'}]");
+                                                "[{foo: {foo: 'abc'}}]");
 
         type1 = createType("CREATE TYPE %s (foo ascii)");
         type2 = createType("CREATE TYPE %s (foo frozen<set<" + type1 + ">>)");
@@ -564,6 +625,110 @@
     }
 
     @Test
+    public void testInsertNonFrozenUDT() throws Throwable
+    {
+        String typeName = createType("CREATE TYPE %s (a int, b text)");
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v " + typeName + ")");
+
+        execute("INSERT INTO %s (k, v) VALUES (?, {a: ?, b: ?})", 0, 0, "abc");
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", "abc")));
+
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc"));
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", "abc")));
+
+        execute("INSERT INTO %s (k, v) VALUES (?, {a: ?, b: ?})", 0, 0, null);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", null)));
+
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", null, "b", "abc"));
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", null, "b", "abc")));
+    }
+
+    @Test
+    public void testUpdateNonFrozenUDT() throws Throwable
+    {
+        String typeName = createType("CREATE TYPE %s (a int, b text)");
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v " + typeName + ")");
+
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc"));
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", "abc")));
+
+        // overwrite the whole UDT
+        execute("UPDATE %s SET v = ? WHERE k = ?", userType("a", 1, "b", "def"), 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 1, "b", "def")));
+
+        execute("UPDATE %s SET v = ? WHERE k = ?", userType("a", 0, "b", null), 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", null)));
+
+        execute("UPDATE %s SET v = ? WHERE k = ?", userType("a", null, "b", "abc"), 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", null, "b", "abc")));
+
+        // individually set fields to non-null values
+        execute("UPDATE %s SET v.a = ? WHERE k = ?", 1, 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 1, "b", "abc")));
+
+        execute("UPDATE %s SET v.b = ? WHERE k = ?", "def", 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 1, "b", "def")));
+
+        execute("UPDATE %s SET v.a = ?, v.b = ? WHERE k = ?", 0, "abc", 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", "abc")));
+
+        execute("UPDATE %s SET v.b = ?, v.a = ? WHERE k = ?", "abc", 0, 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", "abc")));
+
+        // individually set fields to null values
+        execute("UPDATE %s SET v.a = ? WHERE k = ?", null, 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", null, "b", "abc")));
+
+        execute("UPDATE %s SET v.a = ? WHERE k = ?", 0, 0);
+        execute("UPDATE %s SET v.b = ? WHERE k = ?", null, 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", null)));
+
+        execute("UPDATE %s SET v.a = ? WHERE k = ?", null, 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, null));
+
+        assertInvalid("UPDATE %s SET v.bad = ? FROM %s WHERE k = ?", 0, 0);
+        assertInvalid("UPDATE %s SET v = ? FROM %s WHERE k = ?", 0, 0);
+        assertInvalid("UPDATE %s SET v = ? FROM %s WHERE k = ?", userType("a", 1, "b", "abc", "bad", 123), 0);
+    }
+
+    @Test
+    public void testDeleteNonFrozenUDT() throws Throwable
+    {
+        String typeName = createType("CREATE TYPE %s (a int, b text)");
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v " + typeName + ")");
+
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc"));
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", "abc")));
+
+        execute("DELETE v.b FROM %s WHERE k = ?", 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", null)));
+
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc"));
+        execute("DELETE v.a FROM %s WHERE k = ?", 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", null, "b", "abc")));
+
+        execute("DELETE v.b FROM %s WHERE k = ?", 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, null));
+
+        // delete both fields at once
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc"));
+        execute("DELETE v.a, v.b FROM %s WHERE k = ?", 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, null));
+
+        // same, but reverse field order
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc"));
+        execute("DELETE v.b, v.a FROM %s WHERE k = ?", 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, null));
+
+        // delete the whole thing at once
+        execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc"));
+        execute("DELETE v FROM %s WHERE k = ?", 0);
+        assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, null));
+
+        assertInvalid("DELETE v.bad FROM %s WHERE k = ?", 0);
+    }
+
+    @Test
     public void testReadAfterAlteringUserTypeNestedWithinSet() throws Throwable
     {
         String columnType = typeWithKs(createType("CREATE TYPE %s (a int)"));
@@ -573,33 +738,33 @@
             createTable("CREATE TABLE %s (x int PRIMARY KEY, y set<frozen<" + columnType + ">>)");
             disableCompaction();
 
-            execute("INSERT INTO %s (x, y) VALUES(1, ?)", set(userType(1), userType(2)));
-            assertRows(execute("SELECT * FROM %s"), row(1, set(userType(1), userType(2))));
+            execute("INSERT INTO %s (x, y) VALUES(1, ?)", set(userType("a", 1), userType("a", 2)));
+            assertRows(execute("SELECT * FROM %s"), row(1, set(userType("a", 1), userType("a", 2))));
             flush();
 
             assertRows(execute("SELECT * FROM %s WHERE x = 1"),
-                       row(1, set(userType(1), userType(2))));
+                       row(1, set(userType("a", 1), userType("a", 2))));
 
             execute("ALTER TYPE " + columnType + " ADD b int");
             execute("UPDATE %s SET y = y + ? WHERE x = 1",
-                    set(userType(1, 1), userType(1, 2), userType(2, 1)));
+                    set(userType("a", 1, "b", 1), userType("a", 1, "b", 2), userType("a", 2, "b", 1)));
 
             flush();
             assertRows(execute("SELECT * FROM %s WHERE x = 1"),
-                           row(1, set(userType(1),
-                                      userType(1, 1),
-                                      userType(1, 2),
-                                      userType(2),
-                                      userType(2, 1))));
+                           row(1, set(userType("a", 1),
+                                      userType("a", 1, "b", 1),
+                                      userType("a", 1, "b", 2),
+                                      userType("a", 2),
+                                      userType("a", 2, "b", 1))));
 
             compact();
 
             assertRows(execute("SELECT * FROM %s WHERE x = 1"),
-                       row(1, set(userType(1),
-                                  userType(1, 1),
-                                  userType(1, 2),
-                                  userType(2),
-                                  userType(2, 1))));
+                       row(1, set(userType("a", 1),
+                                  userType("a", 1, "b", 1),
+                                  userType("a", 1, "b", 2),
+                                  userType("a", 2),
+                                  userType("a", 2, "b", 1))));
         }
         finally
         {
@@ -617,33 +782,33 @@
             createTable("CREATE TABLE %s (x int PRIMARY KEY, y map<frozen<" + columnType + ">, int>)");
             disableCompaction();
 
-            execute("INSERT INTO %s (x, y) VALUES(1, ?)", map(userType(1), 1, userType(2), 2));
-            assertRows(execute("SELECT * FROM %s"), row(1, map(userType(1), 1, userType(2), 2)));
+            execute("INSERT INTO %s (x, y) VALUES(1, ?)", map(userType("a", 1), 1, userType("a", 2), 2));
+            assertRows(execute("SELECT * FROM %s"), row(1, map(userType("a", 1), 1, userType("a", 2), 2)));
             flush();
 
             assertRows(execute("SELECT * FROM %s WHERE x = 1"),
-                       row(1, map(userType(1), 1, userType(2), 2)));
+                       row(1, map(userType("a", 1), 1, userType("a", 2), 2)));
 
             execute("ALTER TYPE " + columnType + " ADD b int");
             execute("UPDATE %s SET y = y + ? WHERE x = 1",
-                    map(userType(1, 1), 1, userType(1, 2), 1, userType(2, 1), 2));
+                    map(userType("a", 1, "b", 1), 1, userType("a", 1, "b", 2), 1, userType("a", 2, "b", 1), 2));
 
             flush();
             assertRows(execute("SELECT * FROM %s WHERE x = 1"),
-                           row(1, map(userType(1), 1,
-                                      userType(1, 1), 1,
-                                      userType(1, 2), 1,
-                                      userType(2), 2,
-                                      userType(2, 1), 2)));
+                           row(1, map(userType("a", 1), 1,
+                                      userType("a", 1, "b", 1), 1,
+                                      userType("a", 1, "b", 2), 1,
+                                      userType("a", 2), 2,
+                                      userType("a", 2, "b", 1), 2)));
 
             compact();
 
             assertRows(execute("SELECT * FROM %s WHERE x = 1"),
-                       row(1, map(userType(1), 1,
-                                  userType(1, 1), 1,
-                                  userType(1, 2), 1,
-                                  userType(2), 2,
-                                  userType(2, 1), 2)));
+                       row(1, map(userType("a", 1), 1,
+                                  userType("a", 1, "b", 1), 1,
+                                  userType("a", 1, "b", 2), 1,
+                                  userType("a", 2), 2,
+                                  userType("a", 2, "b", 1), 2)));
         }
         finally
         {
@@ -661,33 +826,33 @@
             createTable("CREATE TABLE %s (x int PRIMARY KEY, y list<frozen<" + columnType + ">>)");
             disableCompaction();
 
-            execute("INSERT INTO %s (x, y) VALUES(1, ?)", list(userType(1), userType(2)));
-            assertRows(execute("SELECT * FROM %s"), row(1, list(userType(1), userType(2))));
+            execute("INSERT INTO %s (x, y) VALUES(1, ?)", list(userType("a", 1), userType("a", 2)));
+            assertRows(execute("SELECT * FROM %s"), row(1, list(userType("a", 1), userType("a", 2))));
             flush();
 
             assertRows(execute("SELECT * FROM %s WHERE x = 1"),
-                       row(1, list(userType(1), userType(2))));
+                       row(1, list(userType("a", 1), userType("a", 2))));
 
             execute("ALTER TYPE " + columnType + " ADD b int");
             execute("UPDATE %s SET y = y + ? WHERE x = 1",
-                    list(userType(1, 1), userType(1, 2), userType(2, 1)));
+                    list(userType("a", 1, "b", 1), userType("a", 1, "b", 2), userType("a", 2, "b", 1)));
 
             flush();
             assertRows(execute("SELECT * FROM %s WHERE x = 1"),
-                           row(1, list(userType(1),
-                                       userType(2),
-                                       userType(1, 1),
-                                       userType(1, 2),
-                                       userType(2, 1))));
+                           row(1, list(userType("a", 1),
+                                       userType("a", 2),
+                                       userType("a", 1, "b", 1),
+                                       userType("a", 1, "b", 2),
+                                       userType("a", 2, "b", 1))));
 
             compact();
 
             assertRows(execute("SELECT * FROM %s WHERE x = 1"),
-                       row(1, list(userType(1),
-                                   userType(2),
-                                   userType(1, 1),
-                                   userType(1, 2),
-                                   userType(2, 1))));
+                       row(1, list(userType("a", 1),
+                                   userType("a", 2),
+                                   userType("a", 1, "b", 1),
+                                   userType("a", 1, "b", 2),
+                                   userType("a", 2, "b", 1))));
         }
         finally
         {
@@ -703,15 +868,15 @@
         createTable("CREATE TABLE %s (pk int, c int, v int, s set<frozen<" + columnType + ">>, PRIMARY KEY (pk, c))");
         execute("CREATE MATERIALIZED VIEW " + keyspace() + ".view1 AS SELECT c, pk, v FROM %s WHERE pk IS NOT NULL AND c IS NOT NULL AND v IS NOT NULL PRIMARY KEY (c, pk)");
 
-        execute("INSERT INTO %s (pk, c, v, s) VALUES(?, ?, ?, ?)", 1, 1, 1, set(userType(1), userType(2)));
+        execute("INSERT INTO %s (pk, c, v, s) VALUES(?, ?, ?, ?)", 1, 1, 1, set(userType("a", 1), userType("a", 2)));
         flush();
 
         execute("ALTER TYPE " + columnType + " ADD b int");
         execute("UPDATE %s SET s = s + ?, v = ? WHERE pk = ? AND c = ?",
-                set(userType(1, 1), userType(1, 2), userType(2, 1)), 2, 1, 1);
+                set(userType("a", 1, "b", 1), userType("a", 1, "b", 2), userType("a", 2, "b", 1)), 2, 1, 1);
 
         assertRows(execute("SELECT * FROM %s WHERE pk = ? AND c = ?", 1, 1),
-                       row(1, 1,set(userType(1), userType(1, 1), userType(1, 2), userType(2), userType(2, 1)), 2));
+                       row(1, 1,set(userType("a", 1), userType("a", 1, "b", 1), userType("a", 1, "b", 2), userType("a", 2), userType("a", 2, "b", 1)), 2));
     }
 
     @Test(expected = SyntaxException.class)
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/WritetimeOrTTLTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/WritetimeOrTTLTest.java
index 24ff8bc..cc6c663 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/WritetimeOrTTLTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/WritetimeOrTTLTest.java
@@ -70,7 +70,7 @@
     public void testList() throws Throwable
     {
         createTable("CREATE TABLE %s (k int PRIMARY KEY, l list<int>)");
-        assertInvalidMultiCellSelection("l");
+        assertInvalidMultiCellSelection("l", true);
     }
 
     @Test
@@ -99,7 +99,7 @@
     public void testSet() throws Throwable
     {
         createTable("CREATE TABLE %s (k int PRIMARY KEY, s set<int>)");
-        assertInvalidMultiCellSelection("s");
+        assertInvalidMultiCellSelection("s", true);
     }
 
     @Test
@@ -128,7 +128,7 @@
     public void testMap() throws Throwable
     {
         createTable("CREATE TABLE %s (k int PRIMARY KEY, m map<int, int>)");
-        assertInvalidMultiCellSelection("m");
+        assertInvalidMultiCellSelection("m", true);
     }
 
     @Test
@@ -154,6 +154,14 @@
     }
 
     @Test
+    public void testUDT() throws Throwable
+    {
+        String type = createType("CREATE TYPE %s (f1 int, f2 int)");
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, t " + type + ')');
+        assertInvalidMultiCellSelection("t", false);
+    }
+
+    @Test
     public void testFrozenUDT() throws Throwable
     {
         String type = createType("CREATE TYPE %s (f1 int, f2 int)");
@@ -242,9 +250,10 @@
                                   String.format("SELECT TTL(%s) FROM %%s", column));
     }
 
-    private void assertInvalidMultiCellSelection(String column) throws Throwable
+    private void assertInvalidMultiCellSelection(String column, boolean isCollection) throws Throwable
     {
-        String message = "Cannot use selection function %s on non-frozen collection " + column;
+        String message = format("Cannot use selection function %%s on non-frozen %s %s",
+                                isCollection ? "collection" : "UDT", column);
         assertInvalidThrowMessage(format(message, "writeTime"),
                                   InvalidRequestException.class,
                                   String.format("SELECT WRITETIME(%s) FROM %%s", column));
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallClone.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallClone.java
index c01fbe6..4e0e1d3 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallClone.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallClone.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class CallClone extends JavaUDF
 {
-    public CallClone(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public CallClone(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         try
         {
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallComDatastax.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallComDatastax.java
index 9cd799f..c4cef58 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallComDatastax.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallComDatastax.java
@@ -24,18 +24,25 @@
 import com.datastax.driver.core.DataType;
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class CallComDatastax extends JavaUDF
 {
-    public CallComDatastax(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public CallComDatastax(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         DataType.cint();
         return null;
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallFinalize.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallFinalize.java
index a16bd31..dfb523e 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallFinalize.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallFinalize.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class CallFinalize extends JavaUDF
 {
-    public CallFinalize(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public CallFinalize(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         try
         {
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallOrgApache.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallOrgApache.java
index 4f511d7..34a82ed 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallOrgApache.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/CallOrgApache.java
@@ -24,18 +24,25 @@
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class CallOrgApache extends JavaUDF
 {
-    public CallOrgApache(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public CallOrgApache(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         DatabaseDescriptor.getClusterName();
         return null;
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithField.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithField.java
index d981c18..33625fe 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithField.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithField.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class ClassWithField extends JavaUDF
 {
-    public ClassWithField(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public ClassWithField(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         return null;
     }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInitializer.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInitializer.java
index f53cc24..4f83bfe 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInitializer.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInitializer.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class ClassWithInitializer extends JavaUDF
 {
-    public ClassWithInitializer(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public ClassWithInitializer(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         return null;
     }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInitializer2.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInitializer2.java
index 134f9f9..df4c78a 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInitializer2.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInitializer2.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class ClassWithInitializer2 extends JavaUDF
 {
-    public ClassWithInitializer2(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public ClassWithInitializer2(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         return null;
     }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInitializer3.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInitializer3.java
index 9cd04fb..d30ada3 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInitializer3.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInitializer3.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class ClassWithInitializer3 extends JavaUDF
 {
-    public ClassWithInitializer3(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public ClassWithInitializer3(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         return null;
     }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInnerClass.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInnerClass.java
new file mode 100644
index 0000000..091597f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInnerClass.java
@@ -0,0 +1,53 @@
+/*
+ * 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.validation.entities.udfverify;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import com.datastax.driver.core.TypeCodec;
+import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
+
+/**
+ * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
+ */
+public final class ClassWithInnerClass extends JavaUDF
+{
+    public ClassWithInnerClass(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
+    {
+        super(returnDataType, argDataTypes, udfContext);
+    }
+
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
+    {
+        return null;
+    }
+
+    // this is NOT fine
+    final class ClassWithInner_Inner {
+
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInnerClass2.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInnerClass2.java
new file mode 100644
index 0000000..ac5b06f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithInnerClass2.java
@@ -0,0 +1,55 @@
+/*
+ * 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.validation.entities.udfverify;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import com.datastax.driver.core.TypeCodec;
+import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
+
+/**
+ * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
+ */
+public final class ClassWithInnerClass2 extends JavaUDF
+{
+    public ClassWithInnerClass2(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
+    {
+        super(returnDataType, argDataTypes, udfContext);
+    }
+
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
+    {
+        // this is fine
+        new Runnable() {
+            public void run()
+            {
+
+            }
+        }.run();
+        return null;
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithStaticInitializer.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithStaticInitializer.java
index 64470ca..c927667 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithStaticInitializer.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithStaticInitializer.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class ClassWithStaticInitializer extends JavaUDF
 {
-    public ClassWithStaticInitializer(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public ClassWithStaticInitializer(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         return null;
     }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithStaticInnerClass.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithStaticInnerClass.java
new file mode 100644
index 0000000..9ce5d71
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/ClassWithStaticInnerClass.java
@@ -0,0 +1,53 @@
+/*
+ * 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.validation.entities.udfverify;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import com.datastax.driver.core.TypeCodec;
+import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
+
+/**
+ * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
+ */
+public final class ClassWithStaticInnerClass extends JavaUDF
+{
+    public ClassWithStaticInnerClass(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
+    {
+        super(returnDataType, argDataTypes, udfContext);
+    }
+
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
+    {
+        return null;
+    }
+
+    // this is NOT fine
+    static final class ClassWithStaticInner_Inner {
+
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/GoodClass.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/GoodClass.java
index e3bc1e2..7275ef5 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/GoodClass.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/GoodClass.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class GoodClass extends JavaUDF
 {
-    public GoodClass(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public GoodClass(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         return null;
     }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronized.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronized.java
index 2927b3e..c036f63 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronized.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronized.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class UseOfSynchronized extends JavaUDF
 {
-    public UseOfSynchronized(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public UseOfSynchronized(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         synchronized (this)
         {
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithNotify.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithNotify.java
index 7ef2e1c..3eb673a 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithNotify.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithNotify.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class UseOfSynchronizedWithNotify extends JavaUDF
 {
-    public UseOfSynchronizedWithNotify(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public UseOfSynchronizedWithNotify(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         synchronized (this)
         {
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithNotifyAll.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithNotifyAll.java
index 50a3da8..d9841f7 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithNotifyAll.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithNotifyAll.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class UseOfSynchronizedWithNotifyAll extends JavaUDF
 {
-    public UseOfSynchronizedWithNotifyAll(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public UseOfSynchronizedWithNotifyAll(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         synchronized (this)
         {
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithWait.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithWait.java
index 135c550..b4e4af3 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithWait.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithWait.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class UseOfSynchronizedWithWait extends JavaUDF
 {
-    public UseOfSynchronizedWithWait(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public UseOfSynchronizedWithWait(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         synchronized (this)
         {
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithWaitL.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithWaitL.java
index 4e49e5b..24d4a21 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithWaitL.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithWaitL.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class UseOfSynchronizedWithWaitL extends JavaUDF
 {
-    public UseOfSynchronizedWithWaitL(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public UseOfSynchronizedWithWaitL(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         synchronized (this)
         {
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithWaitLI.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithWaitLI.java
index 6770e7a..5f61bf6 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithWaitLI.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UseOfSynchronizedWithWaitLI.java
@@ -23,18 +23,25 @@
 
 import com.datastax.driver.core.TypeCodec;
 import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
 
 /**
  * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
  */
 public final class UseOfSynchronizedWithWaitLI extends JavaUDF
 {
-    public UseOfSynchronizedWithWaitLI(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes)
+    public UseOfSynchronizedWithWaitLI(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
     {
-        super(returnDataType, argDataTypes);
+        super(returnDataType, argDataTypes, udfContext);
     }
 
-    protected ByteBuffer executeImpl(int protocolVersion, List<ByteBuffer> params)
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
     {
         synchronized (this)
         {
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UsingMapEntry.java b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UsingMapEntry.java
new file mode 100644
index 0000000..0b95b90
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/udfverify/UsingMapEntry.java
@@ -0,0 +1,56 @@
+/*
+ * 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.validation.entities.udfverify;
+
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.datastax.driver.core.TypeCodec;
+import org.apache.cassandra.cql3.functions.JavaUDF;
+import org.apache.cassandra.cql3.functions.UDFContext;
+import org.apache.cassandra.transport.ProtocolVersion;
+
+/**
+ * Used by {@link org.apache.cassandra.cql3.validation.entities.UFVerifierTest}.
+ */
+public final class UsingMapEntry extends JavaUDF
+{
+    public UsingMapEntry(TypeCodec<Object> returnDataType, TypeCodec<Object>[] argDataTypes, UDFContext udfContext)
+    {
+        super(returnDataType, argDataTypes, udfContext);
+    }
+
+    protected Object executeAggregateImpl(ProtocolVersion protocolVersion, Object firstParam, List<ByteBuffer> params)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    protected ByteBuffer executeImpl(ProtocolVersion protocolVersion, List<ByteBuffer> params)
+    {
+        Map<String, String> map = new HashMap<>();
+        // Map.Entry is passed in as an "inner class usage"
+        for (Map.Entry<String, String> stringStringEntry : map.entrySet())
+        {
+
+        }
+        return null;
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/CrcCheckChanceTest.java b/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/CrcCheckChanceTest.java
index d059f7d..2760ae5 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/CrcCheckChanceTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/CrcCheckChanceTest.java
@@ -30,8 +30,8 @@
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.compaction.CompactionInterruptedException;
 import org.apache.cassandra.db.compaction.CompactionManager;
-import org.apache.cassandra.io.compress.CompressedRandomAccessReader;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.util.RandomAccessReader;
 import org.apache.cassandra.utils.FBUtilities;
 
 
@@ -153,8 +153,8 @@
         // note: only compressed files currently perform crc checks, so only the dfile reader is relevant here
         SSTableReader baseSSTable = cfs.getLiveSSTables().iterator().next();
         SSTableReader idxSSTable = indexCfs.getLiveSSTables().iterator().next();
-        try (CompressedRandomAccessReader baseDataReader = (CompressedRandomAccessReader)baseSSTable.openDataReader();
-             CompressedRandomAccessReader idxDataReader = (CompressedRandomAccessReader)idxSSTable.openDataReader())
+        try (RandomAccessReader baseDataReader = baseSSTable.openDataReader();
+             RandomAccessReader idxDataReader = idxSSTable.openDataReader())
         {
             Assert.assertEquals(0.03, baseDataReader.getCrcCheckChance());
             Assert.assertEquals(0.03, idxDataReader.getCrcCheckChance());
diff --git a/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/OverflowTest.java b/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/OverflowTest.java
index 9733eb2..6fdedc2 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/OverflowTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/OverflowTest.java
@@ -21,7 +21,6 @@
 
 import org.junit.Test;
 
-import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNull;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -112,7 +111,7 @@
                     + "AND dclocal_read_repair_chance = 0.5 "
                     + "AND gc_grace_seconds = 4 "
                     + "AND bloom_filter_fp_chance = 0.01 "
-                    + "AND compaction = { 'class' : 'LeveledCompactionStrategy', 'sstable_size_in_mb' : 10 } "
+                    + "AND compaction = { 'class' : 'LeveledCompactionStrategy', 'sstable_size_in_mb' : 10, 'fanout_size' : 5 } "
                     + "AND compression = { 'enabled': false } "
                     + "AND caching = { 'keys': 'ALL', 'rows_per_partition': 'ALL' }");
 
diff --git a/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/SSTablesIteratedTest.java b/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/SSTablesIteratedTest.java
index 44233d6..17e3e73 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/SSTablesIteratedTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/SSTablesIteratedTest.java
@@ -22,6 +22,7 @@
 
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.db.ColumnFamilyStore;
@@ -139,6 +140,426 @@
     }
 
     @Test
+    public void testSSTablesOnlyASC() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, col int, val text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col ASC)");
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 10, "10");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 20, "20");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 30, "30");
+        flush();
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 1", 1, row(1, 10, "10"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 2", 2, row(1, 10, "10"), row(1, 20, "20"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 3", 3, row(1, 10, "10"), row(1, 20, "20"), row(1, 30, "30"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1", 3, row(1, 10, "10"), row(1, 20, "20"), row(1, 30, "30"));
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 25 LIMIT 1", 1, row(1, 30, "30"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col < 40 LIMIT 1", 1, row(1, 10, "10"));
+    }
+
+    @Test
+    public void testMixedMemtableSStablesASC() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, col int, val text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col ASC)");
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 30, "30");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 20, "20");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 10, "10");
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 1", 0, row(1, 10, "10"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 2", 1, row(1, 10, "10"), row(1, 20, "20"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 3", 2, row(1, 10, "10"), row(1, 20, "20"), row(1, 30, "30"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1", 2, row(1, 10, "10"), row(1, 20, "20"), row(1, 30, "30"));
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 25 LIMIT 1", 1, row(1, 30, "30"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col < 40 LIMIT 1", 0, row(1, 10, "10"));
+    }
+
+    @Test
+    public void testOverlappingSStablesASC() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, col int, val text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col ASC)");
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 10, "10");
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 30, "30");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 20, "20");
+        flush();
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 1", 1, row(1, 10, "10"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 2", 2, row(1, 10, "10"), row(1, 20, "20"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 3", 2, row(1, 10, "10"), row(1, 20, "20"), row(1, 30, "30"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1", 2, row(1, 10, "10"), row(1, 20, "20"), row(1, 30, "30"));
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 25 LIMIT 1", 1, row(1, 30, "30"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col < 40 LIMIT 1", 1, row(1, 10, "10"));
+    }
+
+    @Test
+    public void testSSTablesOnlyDESC() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, col int, val text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col DESC)");
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 10, "10");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 20, "20");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 30, "30");
+        flush();
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 1", 1, row(1, 30, "30"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 2", 2, row(1, 30, "30"), row(1, 20, "20"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 3", 3, row(1, 30, "30"), row(1, 20, "20"), row(1, 10, "10"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1", 3, row(1, 30, "30"), row(1, 20, "20"), row(1, 10, "10"));
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 25 LIMIT 1", 1, row(1, 30, "30"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col < 40 LIMIT 1", 1, row(1, 30, "30"));
+    }
+
+    @Test
+    public void testMixedMemtableSStablesDESC() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, col int, val text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col DESC)");
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 10, "10");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 20, "20");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 30, "30");
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 1", 0, row(1, 30, "30"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 2", 1, row(1, 30, "30"), row(1, 20, "20"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 3", 2, row(1, 30, "30"), row(1, 20, "20"), row(1, 10, "10"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1", 2, row(1, 30, "30"), row(1, 20, "20"), row(1, 10, "10"));
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 25 LIMIT 1", 0, row(1, 30, "30"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col < 40 LIMIT 1", 0, row(1, 30, "30"));
+    }
+
+    @Test
+    public void testOverlappingSStablesDESC() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, col int, val text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col DESC)");
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 10, "10");
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 30, "30");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 20, "20");
+        flush();
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 1", 1, row(1, 30, "30"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 2", 2, row(1, 30, "30"), row(1, 20, "20"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 3", 2, row(1, 30, "30"), row(1, 20, "20"), row(1, 10, "10"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1", 2, row(1, 30, "30"), row(1, 20, "20"), row(1, 10, "10"));
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 25 LIMIT 1", 1, row(1, 30, "30"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col < 40 LIMIT 1", 1, row(1, 30, "30"));
+    }
+
+    @Test
+    public void testDeletionOnDifferentSSTables() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, col int, val text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col DESC)");
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 10, "10");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 20, "20");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 30, "30");
+        flush();
+
+        execute("DELETE FROM %s WHERE id=1 and col=30");
+        flush();
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 1", 3, row(1, 20, "20"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 2", 4, row(1, 20, "20"), row(1, 10, "10"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 3", 4, row(1, 20, "20"), row(1, 10, "10"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1", 4, row(1, 20, "20"), row(1, 10, "10"));
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 25 LIMIT 1", 2);
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col < 40 LIMIT 1", 3, row(1, 20, "20"));
+    }
+
+    @Test
+    public void testDeletionOnSameSSTable() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, col int, val text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col DESC)");
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 10, "10");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 20, "20");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 30, "30");
+        execute("DELETE FROM %s WHERE id=1 and col=30");
+        flush();
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 1", 2, row(1, 20, "20"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 2", 3, row(1, 20, "20"), row(1, 10, "10"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 3", 3, row(1, 20, "20"), row(1, 10, "10"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1", 3, row(1, 20, "20"), row(1, 10, "10"));
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 25 LIMIT 1", 1);
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col < 40 LIMIT 1", 2, row(1, 20, "20"));
+    }
+
+    @Test
+    public void testDeletionOnMemTable() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, col int, val text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col DESC)");
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 10, "10");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 20, "20");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 30, "30");
+        execute("DELETE FROM %s WHERE id=1 and col=30");
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 1", 1, row(1, 20, "20"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 2", 2, row(1, 20, "20"), row(1, 10, "10"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 3", 2, row(1, 20, "20"), row(1, 10, "10"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1", 2, row(1, 20, "20"), row(1, 10, "10"));
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 25 LIMIT 1", 0);
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col < 40 LIMIT 1", 1, row(1, 20, "20"));
+    }
+
+    @Test
+    public void testDeletionOnIndexedSSTableDESC() throws Throwable
+    {
+        testDeletionOnIndexedSSTableDESC(true);
+        testDeletionOnIndexedSSTableDESC(false);
+    }
+
+    private void testDeletionOnIndexedSSTableDESC(boolean deleteWithRange) throws Throwable
+    {
+        // reduce the column index size so that columns get indexed during flush
+        DatabaseDescriptor.setColumnIndexSize(1);
+
+        createTable("CREATE TABLE %s (id int, col int, val text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col DESC)");
+
+        for (int i = 1; i <= 1000; i++)
+        {
+            execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, i, Integer.toString(i));
+        }
+        flush();
+
+        Object[][] allRows = new Object[1000][];
+        for (int i = 1001; i <= 2000; i++)
+        {
+            execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, i, Integer.toString(i));
+            allRows[2000 - i] = row(1, i, Integer.toString(i));
+        }
+
+        if (deleteWithRange)
+        {
+            execute("DELETE FROM %s WHERE id=1 and col <= ?", 1000);
+        }
+        else
+        {
+            for (int i = 1; i <= 1000; i++)
+                execute("DELETE FROM %s WHERE id=1 and col = ?", i);
+        }
+        flush();
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 1", 1, row(1, 2000, "2000"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 2", 1, row(1, 2000, "2000"), row(1, 1999, "1999"));
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1", 2, allRows);
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 1000 LIMIT 1", 1, row(1, 2000, "2000"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col <= 2000 LIMIT 1", 1, row(1, 2000, "2000"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 1000", 1, allRows);
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col <= 2000", 2, allRows);
+    }
+
+    @Test
+    public void testDeletionOnIndexedSSTableASC() throws Throwable
+    {
+        testDeletionOnIndexedSSTableASC(true);
+        testDeletionOnIndexedSSTableASC(false);
+    }
+
+    private void testDeletionOnIndexedSSTableASC(boolean deleteWithRange) throws Throwable
+    {
+        // reduce the column index size so that columns get indexed during flush
+        DatabaseDescriptor.setColumnIndexSize(1);
+
+        createTable("CREATE TABLE %s (id int, col int, val text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col ASC)");
+
+        for (int i = 1; i <= 1000; i++)
+        {
+            execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, i, Integer.toString(i));
+        }
+        flush();
+
+        Object[][] allRows = new Object[1000][];
+        for (int i = 1001; i <= 2000; i++)
+        {
+            execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, i, Integer.toString(i));
+            allRows[i - 1001] = row(1, i, Integer.toString(i));
+        }
+        flush();
+
+        if (deleteWithRange)
+        {
+            execute("DELETE FROM %s WHERE id =1 and col <= ?", 1000);
+        }
+        else
+        {
+            for (int i = 1; i <= 1000; i++)
+                execute("DELETE FROM %s WHERE id=1 and col = ?", i);
+        }
+        flush();
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 1", 3, row(1, 1001, "1001"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 2", 3, row(1, 1001, "1001"), row(1, 1002, "1002"));
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1", 3, allRows);
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 1000 LIMIT 1", 2, row(1, 1001, "1001"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col <= 2000 LIMIT 1", 3, row(1, 1001, "1001"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 1000", 2, allRows);
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col <= 2000", 3, allRows);
+    }
+
+    @Test
+    public void testDeletionOnOverlappingIndexedSSTable() throws Throwable
+    {
+        testDeletionOnOverlappingIndexedSSTable(true);
+        testDeletionOnOverlappingIndexedSSTable(false);
+    }
+
+    private void testDeletionOnOverlappingIndexedSSTable(boolean deleteWithRange) throws Throwable
+    {
+        // reduce the column index size so that columns get indexed during flush
+        DatabaseDescriptor.setColumnIndexSize(1);
+
+        createTable("CREATE TABLE %s (id int, col int, val1 text, val2 text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col ASC)");
+
+        for (int i = 1; i <= 500; i++)
+        {
+            if (i % 2 == 0)
+                execute("INSERT INTO %s (id, col, val1) VALUES (?, ?, ?)", 1, i, Integer.toString(i));
+            else
+                execute("INSERT INTO %s (id, col, val1, val2) VALUES (?, ?, ?, ?)", 1, i, Integer.toString(i), Integer.toString(i));
+        }
+
+        for (int i = 1001; i <= 1500; i++)
+        {
+            if (i % 2 == 0)
+                execute("INSERT INTO %s (id, col, val1) VALUES (?, ?, ?)", 1, i, Integer.toString(i));
+            else
+                execute("INSERT INTO %s (id, col, val1, val2) VALUES (?, ?, ?, ?)", 1, i, Integer.toString(i), Integer.toString(i));
+        }
+
+        flush();
+
+        for (int i = 501; i <= 1000; i++)
+        {
+            if (i % 2 == 0)
+                execute("INSERT INTO %s (id, col, val1) VALUES (?, ?, ?)", 1, i, Integer.toString(i));
+            else
+                execute("INSERT INTO %s (id, col, val1, val2) VALUES (?, ?, ?, ?)", 1, i, Integer.toString(i), Integer.toString(i));
+        }
+
+        for (int i = 1501; i <= 2000; i++)
+        {
+            if (i % 2 == 0)
+                execute("INSERT INTO %s (id, col, val1) VALUES (?, ?, ?)", 1, i, Integer.toString(i));
+            else
+                execute("INSERT INTO %s (id, col, val1, val2) VALUES (?, ?, ?, ?)", 1, i, Integer.toString(i), Integer.toString(i));
+        }
+
+        if (deleteWithRange)
+        {
+            execute("DELETE FROM %s WHERE id=1 and col > ? and col <= ?", 250, 750);
+        }
+        else
+        {
+            for (int i = 251; i <= 750; i++)
+                execute("DELETE FROM %s WHERE id=1 and col = ?", i);
+        }
+
+        flush();
+
+        Object[][] allRows = new Object[1500][]; // non deleted rows
+        for (int i = 1; i <= 2000; i++)
+        {
+            if (i > 250 && i <= 750)
+                continue; // skip deleted records
+
+            int idx = (i <= 250 ? i - 1 : i - 501);
+
+            if (i % 2 == 0)
+                allRows[idx] = row(1, i, Integer.toString(i), null);
+            else
+                allRows[idx] = row(1, i, Integer.toString(i), Integer.toString(i));
+        }
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 1", 2, row(1, 1, "1", "1"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 LIMIT 2", 2, row(1, 1, "1", "1"), row(1, 2, "2", null));
+
+        executeAndCheck("SELECT * FROM %s WHERE id=1", 2, allRows);
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 1000 LIMIT 1", 2, row(1, 1001, "1001", "1001"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col <= 2000 LIMIT 1", 2, row(1, 1, "1", "1"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col > 500 LIMIT 1", 2, row(1, 751, "751", "751"));
+        executeAndCheck("SELECT * FROM %s WHERE id=1 AND col <= 500 LIMIT 1", 2, row(1, 1, "1", "1"));
+    }
+
+    @Test
+    public void testMultiplePartitionsDESC() throws Throwable
+    {
+        createTable("CREATE TABLE %s (id int, col int, val text, PRIMARY KEY (id, col)) WITH CLUSTERING ORDER BY (col DESC)");
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 10, "10");
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 2, 10, "10");
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 3, 10, "10");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 20, "20");
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 2, 20, "20");
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 3, 20, "20");
+        flush();
+
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 1, 30, "30");
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 2, 30, "30");
+        execute("INSERT INTO %s (id, col, val) VALUES (?, ?, ?)", 3, 30, "30");
+        flush();
+
+        for (int i = 1; i <= 3; i++)
+        {
+            String base = "SELECT * FROM %s ";
+
+            executeAndCheck(base + String.format("WHERE id=%d LIMIT 1", i), 1, row(i, 30, "30"));
+            executeAndCheck(base + String.format("WHERE id=%d LIMIT 2", i), 2, row(i, 30, "30"), row(i, 20, "20"));
+            executeAndCheck(base + String.format("WHERE id=%d LIMIT 3", i), 3, row(i, 30, "30"), row(i, 20, "20"), row(i, 10, "10"));
+            executeAndCheck(base + String.format("WHERE id=%d", i), 3, row(i, 30, "30"), row(i, 20, "20"), row(i, 10, "10"));
+
+            executeAndCheck(base + String.format("WHERE id=%d AND col > 25 LIMIT 1", i), 1, row(i, 30, "30"));
+            executeAndCheck(base + String.format("WHERE id=%d AND col < 40 LIMIT 1", i), 1, row(i, 30, "30"));
+        }
+    }
+
+    @Test
     public void testNonCompactTableRowDeletion() throws Throwable
     {
         createTable("CREATE TABLE %s (pk int, ck int, v text, PRIMARY KEY (pk, ck))");
@@ -426,12 +847,12 @@
         executeAndCheck("SELECT * FROM %s WHERE pk = 1 AND c = 2", 3, row(1, 2, 3, null));
         executeAndCheck("SELECT * FROM %s WHERE pk = 1 AND c = 3", 3, row(1, 3, 3, null));
 
-        executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 1", 3, row(1, 3));
-        executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 2", 3, row(2, 3));
+        executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 1", 1, row(1, 3));
+        executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 2", 2, row(2, 3));
         executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 3", 3, row(3, 3));
 
-        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 1", 3, row(3));
-        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 2", 3, row(3));
+        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 1", 1, row(3));
+        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 2", 2, row(3));
         executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 3", 3, row(3));
 
         executeAndCheck("SELECT v1, v2 FROM %s WHERE pk = 1 AND c = 1", 3, row(3, (Integer) null));
@@ -465,11 +886,13 @@
         executeAndCheck("SELECT * FROM %s WHERE pk = 1 AND c = 2", 3, row(1, 2, 3, 1));
         executeAndCheck("SELECT * FROM %s WHERE pk = 1 AND c = 3", 3, row(1, 3, 3, 1));
 
-        executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 1", 3, row(1, 3));
-        executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 2", 3, row(2, 3));
+        // As we have the primary key liveness and all the queried columns in the first SSTable we can stop at this point
+        executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 1", 1, row(1, 3));
+        // As we have the primary key liveness and all the queried columns in the second SSTable we can stop at this point
+        executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 2", 2, row(2, 3));
 
-        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 1", 3, row(3));
-        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 2", 3, row(3));
+        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 1", 1, row(3));
+        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 2", 2, row(3));
 
         executeAndCheck("SELECT v1, v2 FROM %s WHERE pk = 1 AND c = 1", 3, row(3, 1));
         executeAndCheck("SELECT v1, v2 FROM %s WHERE pk = 1 AND c = 2", 3, row(3, 1));
@@ -501,7 +924,8 @@
         executeAndCheck("SELECT s, v FROM %s WHERE pk = 1 AND c = 1", 3, row(null, 3));
         executeAndCheck("SELECT s, v FROM %s WHERE pk = 2 AND c = 1", 2, row(1, 3));
         executeAndCheck("SELECT s, v FROM %s WHERE pk = 3 AND c = 3", 3, row(3, 1));
-        executeAndCheck("SELECT v FROM %s WHERE pk = 1 AND c = 1", 3, row(3));
+        // As we have the primary key liveness and all the queried columns in the first SSTable we can stop at this point
+        executeAndCheck("SELECT v FROM %s WHERE pk = 1 AND c = 1", 1, row(3));
         executeAndCheck("SELECT v FROM %s WHERE pk = 2 AND c = 1", 2, row(3));
         executeAndCheck("SELECT v FROM %s WHERE pk = 3 AND c = 3", 3, row(1));
         executeAndCheck("SELECT s FROM %s WHERE pk = 1", 3, row((Integer) null));
@@ -557,8 +981,8 @@
         execute("UPDATE %s USING TIMESTAMP 3001 SET v1 = ?, s = ? WHERE pk = ? AND c = ?", 3, set(3, 4), 1, 2);
         flush();
 
-        executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 1", 3, row(1, 3));
-        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 1", 3, row(3));
+        executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 1", 1, row(1, 3));
+        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 1", 1, row(3));
         executeAndCheck("SELECT * FROM %s WHERE pk = 1 AND c = 1", 3, row(1, 1, set(3, 4), 3, null));
         executeAndCheck("SELECT c, s FROM %s WHERE pk = 1 AND c = 1", 3, row(1, set(3, 4)));
 
@@ -569,6 +993,26 @@
     }
 
     @Test
+    public void testCompactAndNonCompactTableWithCounter() throws Throwable
+    {
+        for (String with : new String[]{"", " WITH COMPACT STORAGE"})
+        {
+            createTable("CREATE TABLE %s (pk int, c int, count counter, PRIMARY KEY(pk, c))" + with);
+
+            execute("UPDATE %s SET count = count + 1 WHERE pk = 1 AND c = 1");
+            flush();
+            execute("UPDATE %s SET count = count + 1 WHERE pk = 1 AND c = 1");
+            flush();
+            execute("UPDATE %s SET count = count + 1 WHERE pk = 1 AND c = 1");
+            flush();
+
+            executeAndCheck("SELECT * FROM %s WHERE pk = 1 AND c = 1", 3, row(1, 1, 3L));
+            executeAndCheck("SELECT pk, c FROM %s WHERE pk = 1 AND c = 1", 3, row(1, 1));
+            executeAndCheck("SELECT count FROM %s WHERE pk = 1 AND c = 1", 3, row(3L));
+        }
+    }
+
+    @Test
     public void testNonCompactTableWithStaticColumnValueMissingAndMulticellColumn() throws Throwable
     {
         createTable("CREATE TABLE %s (pk int, c int, s int static, v set<int>, PRIMARY KEY(pk, c))");
@@ -842,8 +1286,9 @@
         flush();
 
         executeAndCheck("SELECT * FROM %s WHERE pk = 1 AND c = 1", 3, row(1, 1, null, 1));
-        executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 1", 3, row(1, null));
-        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 1", 3, row((Integer) null));
+        // As we have the primary key liveness and all the queried columns in the second SSTable we can stop at this point
+        executeAndCheck("SELECT c, v1 FROM %s WHERE pk = 1 AND c = 1", 2, row(1, null));
+        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1 AND c = 1", 2, row((Integer) null));
 
         executeAndCheck("SELECT * FROM %s WHERE pk = 2 AND c = 1", 2, row(2, 1, 3, null));
         executeAndCheck("SELECT v1, v2 FROM %s WHERE pk = 2 AND c = 1", 2, row(3, null));
@@ -917,7 +1362,9 @@
 
         executeAndCheck("SELECT * FROM %s WHERE pk = 1", 3, row(1, null, 1));
         executeAndCheck("SELECT v1, v2 FROM %s WHERE pk = 1", 3, row((Integer) null, 1));
-        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1", 3, row((Integer) null));
+        // As the primary key liveness is found on the second SSTable, we can stop there as it it enough to ensure
+        // that we know that the row exist
+        executeAndCheck("SELECT v1 FROM %s WHERE pk = 1", 2, row((Integer) null));
 
         executeAndCheck("SELECT * FROM %s WHERE pk = 2", 2, row(2, 3, null));
         executeAndCheck("SELECT v1, v2 FROM %s WHERE pk = 2", 2, row(3, null));
@@ -990,6 +1437,52 @@
     }
 
     @Test
+    public void testNonCompactTableWithAlterTableStatement() throws Throwable
+    {
+        createTable("CREATE TABLE %s (pk int, ck int, v1 int, PRIMARY KEY(pk, ck))");
+
+        execute("INSERT INTO %s (pk, ck, v1) VALUES (?, ?, ?) USING TIMESTAMP 1000", 1, 1, 1);
+        flush();
+        execute("INSERT INTO %s (pk, ck, v1) VALUES (?, ?, ?) USING TIMESTAMP 2000", 1, 1, 2);
+        flush();
+        execute("INSERT INTO %s (pk, ck, v1) VALUES (?, ?, ?) USING TIMESTAMP 3000", 1, 1, 3);
+        flush();
+
+        executeAndCheck("SELECT pk, ck, v1 FROM %s WHERE pk = 1 AND ck = 1", 1, row(1, 1, 3));
+
+        execute("ALTER TABLE %s ADD v2 int");
+
+        executeAndCheck("SELECT pk, ck, v1 FROM %s WHERE pk = 1 AND ck = 1", 1, row(1, 1, 3));
+
+        execute("ALTER TABLE %s ADD s int static");
+
+        executeAndCheck("SELECT pk, ck, v1 FROM %s WHERE pk = 1 AND ck = 1", 1, row(1, 1, 3));
+    }
+
+    @Test
+    public void testNonCompactTableWithAlterTableStatementAndStaticColumns() throws Throwable
+    {
+        createTable("CREATE TABLE %s (pk int, ck int, s1 int static, v1 int, PRIMARY KEY(pk, ck))");
+
+        execute("INSERT INTO %s (pk, ck, v1) VALUES (?, ?, ?) USING TIMESTAMP 1000", 1, 1, 1);
+        flush();
+        execute("INSERT INTO %s (pk, ck, v1) VALUES (?, ?, ?) USING TIMESTAMP 2000", 1, 1, 2);
+        flush();
+        execute("INSERT INTO %s (pk, s1) VALUES (?, ?) USING TIMESTAMP 3000", 1, 3);
+        flush();
+
+        executeAndCheck("SELECT pk, s1 FROM %s WHERE pk = 1", 3, row(1, 3));
+        executeAndCheck("SELECT DISTINCT pk, s1 FROM %s WHERE pk = 1", 3, row(1, 3));
+        executeAndCheck("SELECT s1 FROM %s WHERE pk = 1", 3, row(3));
+
+        execute("ALTER TABLE %s ADD s2 int static");
+
+        executeAndCheck("SELECT pk, s1 FROM %s WHERE pk = 1", 3, row(1, 3));
+        executeAndCheck("SELECT DISTINCT pk, s1 FROM %s WHERE pk = 1", 3, row(1, 3));
+        executeAndCheck("SELECT s1 FROM %s WHERE pk = 1", 3, row(3));
+    }
+
+    @Test
     public void testNonCompactTableWithStaticColumnAndRowDeletion() throws Throwable
     {
         createTable("CREATE TABLE %s (pk int, c int, s int static, v int, PRIMARY KEY(pk, c))");
@@ -1009,13 +1502,11 @@
 
         executeAndCheck("SELECT * FROM %s WHERE pk = 1", 3, row(1, null, 1, null));
         executeAndCheck("SELECT * FROM %s WHERE pk = 1 AND c = 1", 3);
-        // In 3.0 the SinglePartitionReadCommand is looking for all the fetching columns which always includes the
-        // static ones so it needs to go through all the SSTables.
-        executeAndCheck("SELECT v FROM %s WHERE pk = 1 AND c = 1", 3);
+        executeAndCheck("SELECT v FROM %s WHERE pk = 1 AND c = 1", 1);
 
         executeAndCheck("SELECT * FROM %s WHERE pk = 2", 3, row(2, null, 2, null));
         executeAndCheck("SELECT * FROM %s WHERE pk = 2 AND c = 1", 3);
-        executeAndCheck("SELECT v FROM %s WHERE pk = 2 AND c = 1", 3);
+        executeAndCheck("SELECT v FROM %s WHERE pk = 2 AND c = 1", 1);
 
         executeAndCheck("SELECT * FROM %s WHERE pk = 3", 3, row(3, null, 3, null));
         executeAndCheck("SELECT * FROM %s WHERE pk = 3 AND c = 1", 2);
@@ -1042,13 +1533,11 @@
 
         executeAndCheck("SELECT * FROM %s WHERE pk = 1", 3, row(1, null, 1, null));
         executeAndCheck("SELECT * FROM %s WHERE pk = 1 AND c = 1", 3);
-        // In 3.0 the SinglePartitionReadCommand is looking for all the fetching columns which always includes the
-        // static ones so it needs to go through all the SSTables.
-        executeAndCheck("SELECT v FROM %s WHERE pk = 1 AND c = 1", 3);
+        executeAndCheck("SELECT v FROM %s WHERE pk = 1 AND c = 1", 1);
 
         executeAndCheck("SELECT * FROM %s WHERE pk = 2", 3, row(2, null, 2, null));
         executeAndCheck("SELECT * FROM %s WHERE pk = 2 AND c = 1", 3);
-        executeAndCheck("SELECT v FROM %s WHERE pk = 2 AND c = 1", 3);
+        executeAndCheck("SELECT v FROM %s WHERE pk = 2 AND c = 1", 1);
 
         executeAndCheck("SELECT * FROM %s WHERE pk = 3", 3, row(3, null, 3, null));
         executeAndCheck("SELECT * FROM %s WHERE pk = 3 AND c = 1", 2);
diff --git a/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/TombstonesTest.java b/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/TombstonesTest.java
index 5980372..72ed887 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/TombstonesTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/miscellaneous/TombstonesTest.java
@@ -42,6 +42,7 @@
     @BeforeClass
     public static void setUp() throws Throwable
     {
+        DatabaseDescriptor.daemonInitialization();
         DatabaseDescriptor.setTombstoneFailureThreshold(THRESHOLD);
     }
 
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/AggregationTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/AggregationTest.java
index 439bd4e..de9cd4d 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/AggregationTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/AggregationTest.java
@@ -43,21 +43,22 @@
 import ch.qos.logback.classic.turbo.ReconfigureOnChangeFilter;
 import ch.qos.logback.classic.turbo.TurboFilter;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.cql3.UntypedResultSet.Row;
 import org.apache.cassandra.cql3.functions.UDAggregate;
-import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.TypeParser;
 import org.apache.cassandra.exceptions.FunctionExecutionException;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.schema.KeyspaceMetadata;
 import org.apache.cassandra.service.ClientState;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.Event;
 import org.apache.cassandra.transport.Event.SchemaChange.Change;
 import org.apache.cassandra.transport.Event.SchemaChange.Target;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.transport.messages.ResultMessage;
 
 import static ch.qos.logback.core.CoreConstants.RECONFIGURE_ON_CHANGE_TASK;
@@ -130,6 +131,7 @@
         assertRows(execute("SELECT COUNT(b), count(c), count(e), count(f) FROM %s LIMIT 2"), row(4L, 3L, 3L, 3L));
         assertRows(execute("SELECT COUNT(b), count(c), count(e), count(f) FROM %s WHERE a = 1 LIMIT 2"),
                    row(4L, 3L, 3L, 3L));
+        assertRows(execute("SELECT AVG(CAST(b AS double)) FROM %s"), row(11.0/4));
     }
 
     @Test
@@ -151,9 +153,6 @@
         assertColumnNames(execute("SELECT COUNT(1) as myCount FROM %s"), "mycount");
         assertRows(execute("SELECT COUNT(1) as myCount FROM %s"), row(0L));
 
-        // Test invalid call
-        assertInvalidSyntaxMessage("Only COUNT(1) is supported, got COUNT(2)", "SELECT COUNT(2) FROM %s");
-
         // Test with other aggregates
         assertColumnNames(execute("SELECT COUNT(*), max(b), b FROM %s"), "count", "system.max(b)", "b");
         assertRows(execute("SELECT COUNT(*), max(b), b  FROM %s"), row(0L, null, null));
@@ -253,7 +252,7 @@
         assertRows(execute("SELECT count(b.x), max(b.x) as max, b.x, c.x as first FROM %s"),
                    row(3L, 8, 2, null));
 
-        assertInvalidMessage("Invalid field selection: max(b) of type blob is not a user type",
+        assertInvalidMessage("Invalid field selection: system.max(b) of type blob is not a user type",
                              "SELECT max(b).x as max FROM %s");
     }
 
@@ -356,7 +355,6 @@
 
         assertInvalidSyntax("SELECT max(b), max(c) FROM %s WHERE max(a) = 1");
         assertInvalidMessage("aggregate functions cannot be used as arguments of aggregate functions", "SELECT max(sum(c)) FROM %s");
-        assertInvalidSyntax("SELECT COUNT(2) FROM %s");
     }
 
     @Test
@@ -464,6 +462,26 @@
                            Change.DROPPED, Target.AGGREGATE,
                            KEYSPACE, aggregateName,
                            "double");
+
+        // The aggregate with nested tuple should be created without throwing InvalidRequestException. See CASSANDRA-15857
+        String f1 = createFunction(KEYSPACE,
+                                   "double, double",
+                                   "CREATE OR REPLACE FUNCTION %s(state double, val list<tuple<int, int>>) " +
+                                   "RETURNS NULL ON NULL INPUT " +
+                                   "RETURNS double " +
+                                   "LANGUAGE javascript " +
+                                   "AS '\"string\";';");
+
+        String a1 = createAggregateName(KEYSPACE);
+        registerAggregate(a1, "list<tuple<int, int>>");
+
+        assertSchemaChange("CREATE OR REPLACE AGGREGATE " + a1 + "(list<tuple<int, int>>) " +
+                           "SFUNC " + shortFunctionName(f1) + " " +
+                           "STYPE double " +
+                           "INITCOND 0",
+                           Event.SchemaChange.Change.CREATED, Event.SchemaChange.Target.AGGREGATE,
+                           KEYSPACE, parseFunctionName(a1).name,
+                           "list<frozen<tuple<int, int>>>"); // CASSANDRA-14825: remove frozen from param
     }
 
     @Test
@@ -1431,18 +1449,18 @@
                              "FINALFUNC " + shortFunctionName(fFinal) + ' ' +
                              "INITCOND 1");
 
-        assertInvalidMessage("missing EOF", // specifying a function using "keyspace.functionname" is a syntax error
+        assertInvalidMessage("expecting EOF", // specifying a function using "keyspace.functionname" is a syntax error
                              "CREATE AGGREGATE " + KEYSPACE_PER_TEST + ".test_wrong_ks(int) " +
                              "SFUNC " + shortFunctionName(fState) + ' ' +
                              "STYPE " + type + " " +
                              "FINALFUNC " + fFinalWrong + ' ' +
                              "INITCOND 1");
 
-        assertInvalidMessage("missing EOF", // specifying a function using "keyspace.functionname" is a syntax error
+        assertInvalidMessage("expecting EOF", // specifying a function using "keyspace.functionname" is a syntax error
                              "CREATE AGGREGATE " + KEYSPACE_PER_TEST + ".test_wrong_ks(int) " +
                              "SFUNC " + shortFunctionName(fState) + ' ' +
                              "STYPE " + type + ' ' +
-                             "FINALFUNC " + SystemKeyspace.NAME + ".min " +
+                             "FINALFUNC " + SchemaConstants.SYSTEM_KEYSPACE_NAME + ".min " +
                              "INITCOND 1");
     }
 
@@ -1623,12 +1641,20 @@
                                        "LANGUAGE java " +
                                        "AS 'return state;'");
 
-        assertInvalidMessage("The function state type should not be frozen",
-                             "CREATE AGGREGATE %s(tuple<int, int>) " +
-                             "SFUNC " + parseFunctionName(fState).name + ' ' +
-                             "STYPE frozen<tuple<int, int>> " +
-                             "FINALFUNC " + parseFunctionName(fFinal).name + ' ' +
-                             "INITCOND null");
+        // Tuples are always frozen. Both 'tuple' and 'frozen tuple' have the same effect.
+        // So allows to create aggregate with explicit frozen tuples as argument and state types.
+        String toDrop = createAggregate(KEYSPACE,
+                                        "frozen<tuple<int, int>>",
+                                        "CREATE AGGREGATE %s(frozen<tuple<int, int>>) " +
+                                        "SFUNC " + parseFunctionName(fState).name + ' ' +
+                                        "STYPE frozen<tuple<int, int>> " +
+                                        "FINALFUNC " + parseFunctionName(fFinal).name + ' ' +
+                                        "INITCOND null");
+        // Same as above, dropping a function with explicity frozen tuple should be allowed.
+        assertSchemaChange("DROP AGGREGATE " + toDrop + "(frozen<tuple<int, int>>);",
+                           Change.DROPPED, Target.AGGREGATE,
+                           KEYSPACE, shortFunctionName(toDrop),
+                           "frozen<tuple<int, int>>");
 
         String aggregation = createAggregate(KEYSPACE,
                                              "tuple<int, int>",
@@ -1641,8 +1667,10 @@
         assertRows(execute("SELECT " + aggregation + "(b) FROM %s"),
                    row(tuple(7, 8)));
 
-        assertInvalidMessage("The function arguments should not be frozen",
-                             "DROP AGGREGATE %s (frozen<tuple<int, int>>);");
+        assertSchemaChange("DROP AGGREGATE " + aggregation + "(frozen<tuple<int, int>>);",
+                           Change.DROPPED, Target.AGGREGATE,
+                           KEYSPACE, shortFunctionName(aggregation),
+                           "frozen<tuple<int, int>>");
     }
 
     @Test
@@ -1945,13 +1973,14 @@
                    row(finalFunc, initCond));
     }
 
+    @Test
     public void testCustomTypeInitcond() throws Throwable
     {
         try
         {
             String type = "DynamicCompositeType(s => UTF8Type, i => Int32Type)";
 
-            executeNet(Server.CURRENT_VERSION,
+            executeNet(ProtocolVersion.CURRENT,
                        "CREATE FUNCTION " + KEYSPACE + ".f11064(i 'DynamicCompositeType(s => UTF8Type, i => Int32Type)')\n" +
                        "RETURNS NULL ON NULL INPUT\n" +
                        "RETURNS '" + type + "'\n" +
@@ -1959,7 +1988,7 @@
                        "AS 'return i;'");
 
             // create aggregate using the 'composite syntax' for composite types
-            executeNet(Server.CURRENT_VERSION,
+            executeNet(ProtocolVersion.CURRENT,
                        "CREATE AGGREGATE " + KEYSPACE + ".a11064()\n" +
                        "SFUNC f11064 " +
                        "STYPE '" + type + "'\n" +
@@ -1967,7 +1996,7 @@
 
             AbstractType<?> compositeType = TypeParser.parse(type);
             ByteBuffer compositeTypeValue = compositeType.fromString("s@foo:i@32");
-            String compositeTypeString = compositeType.asCQL3Type().toCQLLiteral(compositeTypeValue, Server.CURRENT_VERSION);
+            String compositeTypeString = compositeType.asCQL3Type().toCQLLiteral(compositeTypeValue, ProtocolVersion.CURRENT);
             // ensure that the composite type is serialized using the 'blob syntax'
             assertTrue(compositeTypeString.startsWith("0x"));
 
@@ -1976,7 +2005,7 @@
                        row(compositeTypeString));
 
             // create aggregate using the 'blob syntax' for composite types
-            executeNet(Server.CURRENT_VERSION,
+            executeNet(ProtocolVersion.CURRENT,
                        "CREATE AGGREGATE " + KEYSPACE + ".a11064_2()\n" +
                        "SFUNC f11064 " +
                        "STYPE '" + type + "'\n" +
@@ -2105,6 +2134,8 @@
 
         assertRows(execute("select avg(v1), avg(v2) from %s where bucket in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);"),
                    row(Float.NaN, Double.NaN));
+        assertRows(execute("select sum(v1), sum(v2) from %s where bucket in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);"),
+                   row(Float.NaN, Double.NaN));
     }
 
     @Test
@@ -2124,6 +2155,9 @@
 
             assertRows(execute("select avg(v1), avg(v2) from %s where bucket in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);"),
                        row(FLOAT_INFINITY, DOUBLE_INFINITY));
+            assertRows(execute("select sum(v1), avg(v2) from %s where bucket in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);"),
+                       row(FLOAT_INFINITY, DOUBLE_INFINITY));
+
             execute("truncate %s");
         }
     }
@@ -2135,6 +2169,9 @@
 
         for (int i = 1; i <= 17; i++)
             execute("insert into %s (bucket, v1, v2, v3) values (?, ?, ?, ?)", i, (float) (i / 10.0), i / 10.0, BigDecimal.valueOf(i / 10.0));
+
+        assertRows(execute("select sum(v1), sum(v2), sum(v3) from %s;"),
+                   row((float) 15.3, 15.3, BigDecimal.valueOf(15.3)));
     }
 
     @Test
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/AlterTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/AlterTest.java
index 3b5a5f7..b98970c 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/AlterTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/AlterTest.java
@@ -17,10 +17,13 @@
  */
 package org.apache.cassandra.cql3.validation.operations;
 
+import java.util.List;
+
 import org.junit.Assert;
 import org.junit.Test;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
@@ -28,10 +31,13 @@
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.exceptions.SyntaxException;
-import org.apache.cassandra.schema.SchemaKeyspace;
+import org.apache.cassandra.schema.SchemaKeyspaceTables;
+import org.apache.cassandra.service.ClientWarn;
+import org.assertj.core.api.Assertions;
 
 import static java.lang.String.format;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 public class AlterTest extends CQLTester
 {
@@ -107,7 +113,7 @@
 
         // flush is necessary since otherwise the values of `todrop` will get discarded during
         // alter statement
-        flush();
+        flush(true);
         execute("ALTER TABLE %s DROP todrop USING TIMESTAMP 20000;");
         execute("ALTER TABLE %s ADD todrop int;");
         execute("INSERT INTO %s (id, c1, v1, todrop) VALUES (?, ?, ?, ?) USING TIMESTAMP ?", 1, 100, 100, 100, 30000L);
@@ -129,7 +135,7 @@
 
         // flush is necessary since otherwise the values of `todrop` will get discarded during
         // alter statement
-        flush();
+        flush(true);
         execute("ALTER TABLE %s DROP todrop USING TIMESTAMP 20000;");
         execute("ALTER TABLE %s ADD todrop int static;");
         execute("INSERT INTO %s (id, c1, v1, todrop) VALUES (?, ?, ?, ?) USING TIMESTAMP ?", 1, 100, 100, 100, 30000L);
@@ -152,9 +158,8 @@
 
         // flush is necessary since otherwise the values of `todrop1` and `todrop2` will get discarded during
         // alter statement
-        flush();
-        execute("ALTER TABLE %s DROP todrop1 USING TIMESTAMP 20000;");
-        execute("ALTER TABLE %s DROP todrop2 USING TIMESTAMP 20000;");
+        flush(true);
+        execute("ALTER TABLE %s DROP (todrop1, todrop2) USING TIMESTAMP 20000;");
         execute("ALTER TABLE %s ADD todrop1 int;");
         execute("ALTER TABLE %s ADD todrop2 int;");
 
@@ -232,6 +237,77 @@
                                   "max_threshold", "32")));
     }
 
+    @Test
+    public void testCreateAlterKeyspacesRFWarnings() throws Throwable
+    {
+        requireNetwork();
+
+        // NTS
+        ClientWarn.instance.captureWarnings();
+        String ks = createKeyspace("CREATE KEYSPACE %s WITH replication = {'class' : 'NetworkTopologyStrategy', 'datacenter1' : 3 }");
+        List<String> warnings = ClientWarn.instance.getWarnings();
+        assertEquals(1, warnings.size());
+        Assertions.assertThat(warnings.get(0)).contains("Your replication factor 3 for keyspace " + ks + " is higher than the number of nodes 1 for datacenter datacenter1");
+
+        ClientWarn.instance.captureWarnings();
+        execute("CREATE TABLE " + ks + ".t (k int PRIMARY KEY, v int)");
+        warnings = ClientWarn.instance.getWarnings();
+        assertNull(warnings);
+
+        ClientWarn.instance.captureWarnings();
+        execute("ALTER KEYSPACE " + ks + " WITH replication = {'class' : 'NetworkTopologyStrategy', 'datacenter1' : 2 }");
+        warnings = ClientWarn.instance.getWarnings();
+        assertEquals(1, warnings.size());
+        Assertions.assertThat(warnings.get(0)).contains("Your replication factor 2 for keyspace " + ks + " is higher than the number of nodes 1 for datacenter datacenter1");
+
+        ClientWarn.instance.captureWarnings();
+        execute("ALTER KEYSPACE " + ks + " WITH replication = {'class' : 'NetworkTopologyStrategy', 'datacenter1' : 1 }");
+        warnings = ClientWarn.instance.getWarnings();
+        assertNull(warnings);
+
+        // SimpleStrategy
+        ClientWarn.instance.captureWarnings();
+        ks = createKeyspace("CREATE KEYSPACE %s WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }");
+        warnings = ClientWarn.instance.getWarnings();
+        assertEquals(1, warnings.size());
+        Assertions.assertThat(warnings.get(0)).contains("Your replication factor 3 for keyspace " + ks + " is higher than the number of nodes 1");
+
+        ClientWarn.instance.captureWarnings();
+        execute("CREATE TABLE " + ks + ".t (k int PRIMARY KEY, v int)");
+        warnings = ClientWarn.instance.getWarnings();
+        assertNull(warnings);
+
+        ClientWarn.instance.captureWarnings();
+        execute("ALTER KEYSPACE " + ks + " WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 2 }");
+        warnings = ClientWarn.instance.getWarnings();
+        assertEquals(1, warnings.size());
+        Assertions.assertThat(warnings.get(0)).contains("Your replication factor 2 for keyspace " + ks + " is higher than the number of nodes 1");
+
+        ClientWarn.instance.captureWarnings();
+        execute("ALTER KEYSPACE " + ks + " WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }");
+        warnings = ClientWarn.instance.getWarnings();
+        assertNull(warnings);
+    }
+
+    @Test
+    public void testAlterKeyspaceWithMultipleInstancesOfSameDCThrowsSyntaxException() throws Throwable
+    {
+        try
+        {
+            // Create a keyspace
+            execute("CREATE KEYSPACE testABC WITH replication = {'class' : 'NetworkTopologyStrategy', 'dc1' : 2}");
+
+            // try modifying the keyspace
+            assertInvalidThrow(SyntaxException.class, "ALTER KEYSPACE testABC WITH replication = {'class' : 'NetworkTopologyStrategy', 'dc1' : 2, 'dc1' : 3 }");
+            execute("ALTER KEYSPACE testABC WITH replication = {'class' : 'NetworkTopologyStrategy', 'dc1' : 3}");
+        }
+        finally
+        {
+            // clean-up
+            execute("DROP KEYSPACE IF EXISTS testABC");
+        }
+    }
+
     /**
      * Test for bug of 5232,
      * migrated from cql_tests.py:TestCQL.alter_bug_test()
@@ -356,8 +432,8 @@
         createTable("CREATE TABLE %s (a text, b int, c int, primary key (a, b))");
 
         assertRows(execute(format("SELECT compression FROM %s.%s WHERE keyspace_name = ? and table_name = ?;",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.TABLES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.TABLES),
                            KEYSPACE,
                            currentTable()),
                    row(map("chunk_length_in_kb", "64", "class", "org.apache.cassandra.io.compress.LZ4Compressor")));
@@ -365,8 +441,8 @@
         execute("ALTER TABLE %s WITH compression = { 'class' : 'SnappyCompressor', 'chunk_length_in_kb' : 32 };");
 
         assertRows(execute(format("SELECT compression FROM %s.%s WHERE keyspace_name = ? and table_name = ?;",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.TABLES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.TABLES),
                            KEYSPACE,
                            currentTable()),
                    row(map("chunk_length_in_kb", "32", "class", "org.apache.cassandra.io.compress.SnappyCompressor")));
@@ -374,8 +450,8 @@
         execute("ALTER TABLE %s WITH compression = { 'sstable_compression' : 'LZ4Compressor', 'chunk_length_kb' : 64 };");
 
         assertRows(execute(format("SELECT compression FROM %s.%s WHERE keyspace_name = ? and table_name = ?;",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.TABLES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.TABLES),
                            KEYSPACE,
                            currentTable()),
                    row(map("chunk_length_in_kb", "64", "class", "org.apache.cassandra.io.compress.LZ4Compressor")));
@@ -383,8 +459,8 @@
         execute("ALTER TABLE %s WITH compression = { 'sstable_compression' : '', 'chunk_length_kb' : 32 };");
 
         assertRows(execute(format("SELECT compression FROM %s.%s WHERE keyspace_name = ? and table_name = ?;",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.TABLES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.TABLES),
                            KEYSPACE,
                            currentTable()),
                    row(map("enabled", "false")));
@@ -393,8 +469,8 @@
         execute("ALTER TABLE %s WITH compression = { 'enabled' : 'false'};");
 
         assertRows(execute(format("SELECT compression FROM %s.%s WHERE keyspace_name = ? and table_name = ?;",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.TABLES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.TABLES),
                            KEYSPACE,
                            currentTable()),
                    row(map("enabled", "false")));
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/BatchTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/BatchTest.java
index 87d0cde..d0bdd15 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/BatchTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/BatchTest.java
@@ -153,10 +153,10 @@
         execute("INSERT INTO %s (partitionKey, clustering_1, value) VALUES (0, 6, 6)");
 
         execute("BEGIN BATCH " +
-                "UPDATE %1$s SET value = 7 WHERE partitionKey = 0 AND clustering_1 = 1" +
-                "UPDATE %1$s SET value = 8 WHERE partitionKey = 0 AND (clustering_1) = (2)" +
-                "UPDATE %1$s SET value = 10 WHERE partitionKey = 0 AND clustering_1 IN (3, 4)" +
-                "UPDATE %1$s SET value = 20 WHERE partitionKey = 0 AND (clustering_1) IN ((5), (6))" +
+                "UPDATE %1$s SET value = 7 WHERE partitionKey = 0 AND clustering_1 = 1;" +
+                "UPDATE %1$s SET value = 8 WHERE partitionKey = 0 AND (clustering_1) = (2);" +
+                "UPDATE %1$s SET value = 10 WHERE partitionKey = 0 AND clustering_1 IN (3, 4);" +
+                "UPDATE %1$s SET value = 20 WHERE partitionKey = 0 AND (clustering_1) IN ((5), (6));" +
                 "APPLY BATCH;");
 
         assertRows(execute("SELECT * FROM %s"),
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/CreateTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/CreateTest.java
index 1f436b9..11f1e1a 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/CreateTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/CreateTest.java
@@ -17,33 +17,58 @@
  */
 package org.apache.cassandra.cql3.validation.operations;
 
+import java.io.IOException;
+import java.nio.ByteBuffer;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.UUID;
 
 import org.junit.Test;
 
-
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.Duration;
 import org.apache.cassandra.db.Mutation;
 import org.apache.cassandra.db.partitions.Partition;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.SyntaxException;
-import org.apache.cassandra.schema.SchemaKeyspace;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.schema.SchemaKeyspaceTables;
 import org.apache.cassandra.triggers.ITrigger;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 import static java.lang.String.format;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.fail;
 import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+import static org.apache.cassandra.cql3.Duration.NANOS_PER_HOUR;
+import static org.apache.cassandra.cql3.Duration.NANOS_PER_MICRO;
+import static org.apache.cassandra.cql3.Duration.NANOS_PER_MILLI;
+import static org.apache.cassandra.cql3.Duration.NANOS_PER_MINUTE;
 
 public class CreateTest extends CQLTester
 {
     @Test
+    public void testCreateTableWithNameCapitalPAndColumnDuration() throws Throwable
+    {
+        // CASSANDRA-17919
+        createTable(KEYSPACE, "CREATE TABLE %s (a INT PRIMARY KEY, b DURATION);", "P");
+        execute("INSERT INTO %s (a, b) VALUES (1, PT0S)");
+        assertRows(execute("SELECT * FROM %s"), row(1, Duration.newInstance(0, 0, 0)));
+    }
+
+    @Test
+    public void testCreateKeyspaceWithNameCapitalP() throws Throwable
+    {
+        // CASSANDRA-17919
+        executeFormattedQuery("CREATE KEYSPACE IF NOT EXISTS P WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}");
+        executeFormattedQuery("DROP KEYSPACE P");
+    }
+
+    @Test
     public void testCQL3PartitionKeyOnlyTable()
     {
         createTable("CREATE TABLE %s (id text PRIMARY KEY);");
@@ -85,6 +110,161 @@
                              "INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", "3", (byte) 1, ByteBufferUtil.EMPTY_BYTE_BUFFER);
     }
 
+    @Test
+    public void testCreateTableWithDurationColumns() throws Throwable
+    {
+        assertInvalidMessage("duration type is not supported for PRIMARY KEY part a",
+                             "CREATE TABLE test (a duration PRIMARY KEY, b int);");
+
+        assertInvalidMessage("duration type is not supported for PRIMARY KEY part b",
+                             "CREATE TABLE test (a text, b duration, c duration, primary key (a, b));");
+
+        assertInvalidMessage("duration type is not supported for PRIMARY KEY part b",
+                             "CREATE TABLE test (a text, b duration, c duration, primary key (a, b)) with clustering order by (b DESC);");
+
+        createTable("CREATE TABLE %s (a int, b int, c duration, primary key (a, b));");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 1, 1y2mo)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 2, -1y2mo)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 3, 1Y2MO)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 4, 2w)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 5, 2d10h)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 6, 30h20m)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 7, 20m)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 8, 567ms)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 9, 1950us)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 10, 1950µs)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 11, 1950000NS)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 12, -1950000ns)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 13, 1y3mo2h10m)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 14, -P1Y2M)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 15, P2D)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 16, PT20M)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 17, P2W)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 18, P1Y3MT2H10M)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 19, P0000-00-00T30:20:00)");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 20, P0001-03-00T02:10:00)");
+        execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 1, 21, duration(12, 10, 0));
+        execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 1, 22, duration(-12, -10, 0));
+
+        assertRows(execute("SELECT * FROM %s"),
+                   row(1, 1, Duration.newInstance(14, 0, 0)),
+                   row(1, 2, Duration.newInstance(-14, 0, 0)),
+                   row(1, 3, Duration.newInstance(14, 0, 0)),
+                   row(1, 4, Duration.newInstance(0, 14, 0)),
+                   row(1, 5, Duration.newInstance(0, 2, 10 * NANOS_PER_HOUR)),
+                   row(1, 6, Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR + 20 * NANOS_PER_MINUTE)),
+                   row(1, 7, Duration.newInstance(0, 0, 20 * NANOS_PER_MINUTE)),
+                   row(1, 8, Duration.newInstance(0, 0, 567 * NANOS_PER_MILLI)),
+                   row(1, 9, Duration.newInstance(0, 0, 1950 * NANOS_PER_MICRO)),
+                   row(1, 10, Duration.newInstance(0, 0, 1950 * NANOS_PER_MICRO)),
+                   row(1, 11, Duration.newInstance(0, 0, 1950000)),
+                   row(1, 12, Duration.newInstance(0, 0, -1950000)),
+                   row(1, 13, Duration.newInstance(15, 0, 130 * NANOS_PER_MINUTE)),
+                   row(1, 14, Duration.newInstance(-14, 0, 0)),
+                   row(1, 15, Duration.newInstance(0, 2, 0)),
+                   row(1, 16, Duration.newInstance(0, 0, 20 * NANOS_PER_MINUTE)),
+                   row(1, 17, Duration.newInstance(0, 14, 0)),
+                   row(1, 18, Duration.newInstance(15, 0, 130 * NANOS_PER_MINUTE)),
+                   row(1, 19, Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR + 20 * NANOS_PER_MINUTE)),
+                   row(1, 20, Duration.newInstance(15, 0, 130 * NANOS_PER_MINUTE)),
+                   row(1, 21, Duration.newInstance(12, 10, 0)),
+                   row(1, 22, Duration.newInstance(-12, -10, 0)));
+
+        assertInvalidMessage("Slice restrictions are not supported on duration columns",
+                             "SELECT * FROM %s WHERE c > 1y ALLOW FILTERING");
+
+        assertInvalidMessage("Slice restrictions are not supported on duration columns",
+                             "SELECT * FROM %s WHERE c <= 1y ALLOW FILTERING");
+
+        assertInvalidMessage("Expected at least 3 bytes for a duration (1)",
+                             "INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 2, 1, (byte) 1);
+        assertInvalidMessage("Expected at least 3 bytes for a duration (0)",
+                             "INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 2, 1, ByteBufferUtil.EMPTY_BYTE_BUFFER);
+        assertInvalidMessage("Invalid duration. The total number of days must be less or equal to 2147483647",
+                             "INSERT INTO %s (a, b, c) VALUES (1, 2, " + Long.MAX_VALUE + "d)");
+
+        assertInvalidMessage("The duration months, days and nanoseconds must be all of the same sign (2, -2, 0)",
+                             "INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 2, 1, duration(2, -2, 0));
+
+        assertInvalidMessage("The duration months, days and nanoseconds must be all of the same sign (-2, 0, 2000000)",
+                             "INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 2, 1, duration(-2, 0, 2000000));
+
+        assertInvalidMessage("The duration months must be a 32 bits integer but was: 9223372036854775807",
+                             "INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 2, 1, duration(9223372036854775807L, 1, 0));
+
+        assertInvalidMessage("The duration days must be a 32 bits integer but was: 9223372036854775807",
+                             "INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 2, 1, duration(0, 9223372036854775807L, 0));
+
+        // Test with duration column name
+        createTable("CREATE TABLE %s (a text PRIMARY KEY, duration duration);");
+
+        // Test duration within Map
+        assertInvalidMessage("Durations are not allowed as map keys: map<duration, text>",
+                             "CREATE TABLE test(pk int PRIMARY KEY, m map<duration, text>)");
+
+        createTable("CREATE TABLE %s(pk int PRIMARY KEY, m map<text, duration>)");
+        execute("INSERT INTO %s (pk, m) VALUES (1, {'one month' : 1mo, '60 days' : 60d})");
+        assertRows(execute("SELECT * FROM %s"),
+                   row(1, map("one month", Duration.from("1mo"), "60 days", Duration.from("60d"))));
+
+        assertInvalidMessage("duration type is not supported for PRIMARY KEY part m",
+                "CREATE TABLE %s(m frozen<map<text, duration>> PRIMARY KEY, v int)");
+
+        assertInvalidMessage("duration type is not supported for PRIMARY KEY part m",
+                             "CREATE TABLE %s(pk int, m frozen<map<text, duration>>, v int, PRIMARY KEY (pk, m))");
+
+        // Test duration within Set
+        assertInvalidMessage("Durations are not allowed inside sets: set<duration>",
+                             "CREATE TABLE %s(pk int PRIMARY KEY, s set<duration>)");
+
+        assertInvalidMessage("Durations are not allowed inside sets: frozen<set<duration>>",
+                             "CREATE TABLE %s(s frozen<set<duration>> PRIMARY KEY, v int)");
+
+        // Test duration within List
+        createTable("CREATE TABLE %s(pk int PRIMARY KEY, l list<duration>)");
+        execute("INSERT INTO %s (pk, l) VALUES (1, [1mo, 60d])");
+        assertRows(execute("SELECT * FROM %s"),
+                   row(1, list(Duration.from("1mo"), Duration.from("60d"))));
+
+        assertInvalidMessage("duration type is not supported for PRIMARY KEY part l",
+                             "CREATE TABLE %s(l frozen<list<duration>> PRIMARY KEY, v int)");
+
+        // Test duration within Tuple
+        createTable("CREATE TABLE %s(pk int PRIMARY KEY, t tuple<int, duration>)");
+        execute("INSERT INTO %s (pk, t) VALUES (1, (1, 1mo))");
+        assertRows(execute("SELECT * FROM %s"),
+                   row(1, tuple(1, Duration.from("1mo"))));
+
+        assertInvalidMessage("duration type is not supported for PRIMARY KEY part t",
+                             "CREATE TABLE %s(t frozen<tuple<int, duration>> PRIMARY KEY, v int)");
+
+        // Test duration within UDT
+        String typename = createType("CREATE TYPE %s (a duration)");
+        String myType = KEYSPACE + '.' + typename;
+        createTable("CREATE TABLE %s(pk int PRIMARY KEY, u " + myType + ")");
+        execute("INSERT INTO %s (pk, u) VALUES (1, {a : 1mo})");
+        assertRows(execute("SELECT * FROM %s"),
+                   row(1, userType("a", Duration.from("1mo"))));
+
+        assertInvalidMessage("duration type is not supported for PRIMARY KEY part u",
+                             "CREATE TABLE %s(pk int, u frozen<" + myType + ">, v int, PRIMARY KEY(pk, u))");
+
+        // Test duration with several level of depth
+        assertInvalidMessage("duration type is not supported for PRIMARY KEY part m",
+                "CREATE TABLE %s(pk int, m frozen<map<text, list<tuple<int, duration>>>>, v int, PRIMARY KEY (pk, m))");
+    }
+
+    private ByteBuffer duration(long months, long days, long nanoseconds) throws IOException
+    {
+        try(DataOutputBuffer output = new DataOutputBuffer())
+        {
+            output.writeVInt(months);
+            output.writeVInt(days);
+            output.writeVInt(nanoseconds);
+            return output.buffer();
+        }
+    }
+
     /**
      * Creation and basic operations on a static table,
      * migrated from cql_tests.py:TestCQL.static_cf_test()
@@ -344,6 +524,20 @@
         execute("DROP KEYSPACE testXYZ");
     }
 
+    @Test
+    public void testCreateKeyspaceWithMultipleInstancesOfSameDCThrowsException() throws Throwable
+    {
+        try
+        {
+            assertInvalidThrow(SyntaxException.class, "CREATE KEYSPACE testABC WITH replication = {'class' : 'NetworkTopologyStrategy', 'dc1' : 2, 'dc1' : 3 }");
+        }
+        finally
+        {
+            // clean-up
+            execute("DROP KEYSPACE IF EXISTS testABC");
+        }
+    }
+
     /**
      * Test create and drop table
      * migrated from cql_tests.py:TestCQL.table_test()
@@ -501,9 +695,9 @@
     public void testCreateIndextWithCompactStaticFormat() throws Throwable
     {
         createTable("CREATE TABLE %s (a int PRIMARY KEY, b int, c int) WITH COMPACT STORAGE");
-        assertInvalidMessage("No column definition found for column column1",
+        assertInvalidMessage("Undefined column name column1",
                              "CREATE INDEX column1_index on %s (column1)");
-        assertInvalidMessage("No column definition found for column value",
+        assertInvalidMessage("Undefined column name value",
                              "CREATE INDEX value_index on %s (value)");
     }
 
@@ -524,8 +718,8 @@
         createTable("CREATE TABLE %s (a text, b int, c int, primary key (a, b))");
 
         assertRows(execute(format("SELECT compression FROM %s.%s WHERE keyspace_name = ? and table_name = ?;",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.TABLES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.TABLES),
                            KEYSPACE,
                            currentTable()),
                    row(map("chunk_length_in_kb", "64", "class", "org.apache.cassandra.io.compress.LZ4Compressor")));
@@ -534,8 +728,8 @@
                 + " WITH compression = { 'class' : 'SnappyCompressor', 'chunk_length_in_kb' : 32 };");
 
         assertRows(execute(format("SELECT compression FROM %s.%s WHERE keyspace_name = ? and table_name = ?;",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.TABLES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.TABLES),
                            KEYSPACE,
                            currentTable()),
                    row(map("chunk_length_in_kb", "32", "class", "org.apache.cassandra.io.compress.SnappyCompressor")));
@@ -544,8 +738,8 @@
                 + " WITH compression = { 'class' : 'SnappyCompressor', 'chunk_length_in_kb' : 32, 'enabled' : true };");
 
         assertRows(execute(format("SELECT compression FROM %s.%s WHERE keyspace_name = ? and table_name = ?;",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.TABLES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.TABLES),
                            KEYSPACE,
                            currentTable()),
                    row(map("chunk_length_in_kb", "32", "class", "org.apache.cassandra.io.compress.SnappyCompressor")));
@@ -554,8 +748,8 @@
                 + " WITH compression = { 'sstable_compression' : 'SnappyCompressor', 'chunk_length_kb' : 32 };");
 
         assertRows(execute(format("SELECT compression FROM %s.%s WHERE keyspace_name = ? and table_name = ?;",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.TABLES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.TABLES),
                            KEYSPACE,
                            currentTable()),
                    row(map("chunk_length_in_kb", "32", "class", "org.apache.cassandra.io.compress.SnappyCompressor")));
@@ -564,8 +758,8 @@
                 + " WITH compression = { 'sstable_compression' : '', 'chunk_length_kb' : 32 };");
 
         assertRows(execute(format("SELECT compression FROM %s.%s WHERE keyspace_name = ? and table_name = ?;",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.TABLES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.TABLES),
                            KEYSPACE,
                            currentTable()),
                    row(map("enabled", "false")));
@@ -574,8 +768,8 @@
                 + " WITH compression = { 'enabled' : 'false'};");
 
         assertRows(execute(format("SELECT compression FROM %s.%s WHERE keyspace_name = ? and table_name = ?;",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.TABLES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.TABLES),
                            KEYSPACE,
                            currentTable()),
                    row(map("enabled", "false")));
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/DeleteTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/DeleteTest.java
index a0ffada..b46cc75 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/DeleteTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/DeleteTest.java
@@ -429,6 +429,27 @@
 
         assertEmpty(execute("select * from %s  where a=1 and b=1"));
     }
+
+    /** Test that two deleted rows for the same partition but on different sstables do not resurface */
+    @Test
+    public void testDeletedRowsDoNotResurface() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c text, primary key (a, b))");
+        execute("INSERT INTO %s (a, b, c) VALUES(1, 1, '1')");
+        execute("INSERT INTO %s (a, b, c) VALUES(1, 2, '2')");
+        execute("INSERT INTO %s (a, b, c) VALUES(1, 3, '3')");
+        flush();
+
+        execute("DELETE FROM %s where a=1 and b = 1");
+        flush();
+
+        execute("DELETE FROM %s where a=1 and b = 2");
+        flush();
+
+        assertRows(execute("SELECT * FROM %s WHERE a = ?", 1),
+                   row(1, 3, "3"));
+    }
+
     @Test
     public void testDeleteWithNoClusteringColumns() throws Throwable
     {
@@ -479,10 +500,10 @@
                                  "DELETE FROM %s WHERE partitionKey = ? AND partitionKey = ?", 0, 1);
 
             // unknown identifiers
-            assertInvalidMessage("Unknown identifier unknown",
+            assertInvalidMessage("Undefined column name unknown",
                                  "DELETE unknown FROM %s WHERE partitionKey = ?", 0);
 
-            assertInvalidMessage("Undefined name partitionkey1 in where clause ('partitionkey1 = ?')",
+            assertInvalidMessage("Undefined column name partitionkey1",
                                  "DELETE FROM %s WHERE partitionKey1 = ?", 0);
 
             // Invalid operator in the where clause
@@ -568,13 +589,13 @@
                                  "DELETE FROM %s WHERE partitionKey = ? AND clustering = ? AND clustering = ?", 0, 1, 1);
 
             // unknown identifiers
-            assertInvalidMessage("Unknown identifier value1",
+            assertInvalidMessage("Undefined column name value1",
                                  "DELETE value1 FROM %s WHERE partitionKey = ? AND clustering = ?", 0, 1);
 
-            assertInvalidMessage("Undefined name partitionkey1 in where clause ('partitionkey1 = ?')",
+            assertInvalidMessage("Undefined column name partitionkey1",
                                  "DELETE FROM %s WHERE partitionKey1 = ? AND clustering = ?", 0, 1);
 
-            assertInvalidMessage("Undefined name clustering_3 in where clause ('clustering_3 = ?')",
+            assertInvalidMessage("Undefined column name clustering_3",
                                  "DELETE FROM %s WHERE partitionKey = ? AND clustering_3 = ?", 0, 1);
 
             // Invalid operator in the where clause
@@ -698,13 +719,13 @@
                                  "DELETE FROM %s WHERE partitionKey = ? AND clustering_1 = ? AND clustering_2 = ? AND clustering_1 = ?", 0, 1, 1, 1);
 
             // unknown identifiers
-            assertInvalidMessage("Unknown identifier value1",
+            assertInvalidMessage("Undefined column name value1",
                                  "DELETE value1 FROM %s WHERE partitionKey = ? AND clustering_1 = ? AND clustering_2 = ?", 0, 1, 1);
 
-            assertInvalidMessage("Undefined name partitionkey1 in where clause ('partitionkey1 = ?')",
+            assertInvalidMessage("Undefined column name partitionkey1",
                                  "DELETE FROM %s WHERE partitionKey1 = ? AND clustering_1 = ? AND clustering_2 = ?", 0, 1, 1);
 
-            assertInvalidMessage("Undefined name clustering_3 in where clause ('clustering_3 = ?')",
+            assertInvalidMessage("Undefined column name clustering_3",
                                  "DELETE FROM %s WHERE partitionKey = ? AND clustering_1 = ? AND clustering_3 = ?", 0, 1, 1);
 
             // Invalid operator in the where clause
@@ -721,6 +742,39 @@
     }
 
     @Test
+    public void testDeleteWithNonoverlappingRange() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c text, primary key (a, b))");
+
+        for (int i = 0; i < 10; i++)
+            execute("INSERT INTO %s (a, b, c) VALUES(1, ?, 'abc')", i);
+        flush();
+
+        execute("DELETE FROM %s WHERE a=1 and b <= 3");
+        flush();
+
+        // this query does not overlap the tombstone range above and caused the rows to be resurrected
+        assertEmpty(execute("SELECT * FROM %s WHERE a=1 and b <= 2"));
+    }
+
+    @Test
+    public void testDeleteWithIntermediateRangeAndOneClusteringColumn() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c text, primary key (a, b))");
+        execute("INSERT INTO %s (a, b, c) VALUES(1, 1, '1')");
+        execute("INSERT INTO %s (a, b, c) VALUES(1, 3, '3')");
+        execute("DELETE FROM %s where a=1 and b >= 2 and b <= 3");
+        execute("INSERT INTO %s (a, b, c) VALUES(1, 2, '2')");
+        flush();
+
+        execute("DELETE FROM %s where a=1 and b >= 2 and b <= 3");
+        flush();
+
+        assertRows(execute("SELECT * FROM %s WHERE a = ?", 1),
+                   row(1, 1, "1"));
+    }
+
+    @Test
     public void testDeleteWithRangeAndOneClusteringColumn() throws Throwable
     {
         testDeleteWithRangeAndOneClusteringColumn(false);
@@ -1156,6 +1210,32 @@
     }
 
     @Test
+    public void testDeleteAndReverseQueries() throws Throwable
+    {
+        // This test insert rows in one sstable and a range tombstone covering some of those rows in another, and it
+        // validates we correctly get only the non-removed rows when doing reverse queries.
+
+        createTable("CREATE TABLE %s (k text, i int, PRIMARY KEY (k, i))");
+
+        for (int i = 0; i < 10; i++)
+            execute("INSERT INTO %s(k, i) values (?, ?)", "a", i);
+
+        flush();
+
+        execute("DELETE FROM %s WHERE k = ? AND i >= ? AND i <= ?", "a", 2, 7);
+
+        assertRows(execute("SELECT i FROM %s WHERE k = ? ORDER BY i DESC", "a"),
+            row(9), row(8), row(1), row(0)
+        );
+
+        flush();
+
+        assertRows(execute("SELECT i FROM %s WHERE k = ? ORDER BY i DESC", "a"),
+            row(9), row(8), row(1), row(0)
+        );
+    }
+
+    @Test
     public void testDeleteWithEmptyRestrictionValue() throws Throwable
     {
         for (String options : new String[] { "", " WITH COMPACT STORAGE" })
@@ -1263,32 +1343,6 @@
         }
     }
 
-    @Test
-    public void testDeleteAndReverseQueries() throws Throwable
-    {
-        // This test insert rows in one sstable and a range tombstone covering some of those rows in another, and it
-        // validates we correctly get only the non-removed rows when doing reverse queries.
-
-        createTable("CREATE TABLE %s (k text, i int, PRIMARY KEY (k, i))");
-
-        for (int i = 0; i < 10; i++)
-            execute("INSERT INTO %s(k, i) values (?, ?)", "a", i);
-
-        flush();
-
-        execute("DELETE FROM %s WHERE k = ? AND i >= ? AND i <= ?", "a", 2, 7);
-
-        assertRows(execute("SELECT i FROM %s WHERE k = ? ORDER BY i DESC", "a"),
-            row(9), row(8), row(1), row(0)
-        );
-
-        flush();
-
-        assertRows(execute("SELECT i FROM %s WHERE k = ? ORDER BY i DESC", "a"),
-            row(9), row(8), row(1), row(0)
-        );
-    }
-
     /**
      * Test for CASSANDRA-12829
      */
@@ -1484,20 +1538,20 @@
 
         // if column1 is present, hidden column is called column2
         createTable("CREATE TABLE %s (a int PRIMARY KEY, b int, c int, column1 int) WITH COMPACT STORAGE");
-        assertInvalidMessage("Undefined name column2 in where clause ('column2 = 1')",
+        assertInvalidMessage("Undefined column name column2",
                              "DELETE FROM %s WHERE a = 1 AND column2= 1");
-        assertInvalidMessage("Undefined name column2 in where clause ('column2 = 1')",
+        assertInvalidMessage("Undefined column name column2",
                              "DELETE FROM %s WHERE a = 1 AND column2 = 1 AND value1 = 1");
-        assertInvalidMessage("Unknown identifier column2",
+        assertInvalidMessage("Undefined column name column2",
                              "DELETE column2 FROM %s WHERE a = 1");
 
         // if value is present, hidden column is called value1
         createTable("CREATE TABLE %s (a int PRIMARY KEY, b int, c int, value int) WITH COMPACT STORAGE");
-        assertInvalidMessage("Undefined name value1 in where clause ('value1 = 1')",
+        assertInvalidMessage("Undefined column name value1",
                              "DELETE FROM %s WHERE a = 1 AND value1 = 1");
-        assertInvalidMessage("Undefined name value1 in where clause ('value1 = 1')",
+        assertInvalidMessage("Undefined column name value1",
                              "DELETE FROM %s WHERE a = 1 AND value1 = 1 AND column1 = 1");
-        assertInvalidMessage("Unknown identifier value1",
+        assertInvalidMessage("Undefined column name value1",
                              "DELETE value1 FROM %s WHERE a = 1");
     }
 
@@ -1526,15 +1580,15 @@
 
     private void testWithCompactFormat() throws Throwable
     {
-        assertInvalidMessage("Undefined name value in where clause ('value = 1')",
+        assertInvalidMessage("Undefined column name value",
                              "DELETE FROM %s WHERE a = 1 AND value = 1");
-        assertInvalidMessage("Undefined name column1 in where clause ('column1 = 1')",
+        assertInvalidMessage("Undefined column name column1",
                              "DELETE FROM %s WHERE a = 1 AND column1= 1");
-        assertInvalidMessage("Undefined name value in where clause ('value = 1')",
+        assertInvalidMessage("Undefined column name value",
                              "DELETE FROM %s WHERE a = 1 AND value = 1 AND column1 = 1");
-        assertInvalidMessage("Unknown identifier value",
+        assertInvalidMessage("Undefined column name value",
                              "DELETE value FROM %s WHERE a = 1");
-        assertInvalidMessage("Unknown identifier column1",
+        assertInvalidMessage("Undefined column name column1",
                              "DELETE column1 FROM %s WHERE a = 1");
     }
 
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/DropCompactStorageThriftTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/DropCompactStorageThriftTest.java
index 7d81018..973412a 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/DropCompactStorageThriftTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/DropCompactStorageThriftTest.java
@@ -41,7 +41,7 @@
 import org.apache.cassandra.db.marshal.Int32Type;
 import org.apache.cassandra.db.marshal.MapType;
 import org.apache.cassandra.db.marshal.UTF8Type;
-import org.apache.cassandra.index.internal.CassandraIndex;
+import org.apache.cassandra.index.TargetParser;
 import org.apache.cassandra.locator.SimpleStrategy;
 import org.apache.cassandra.serializers.MarshalException;
 import org.apache.cassandra.thrift.Cassandra;
@@ -537,18 +537,18 @@
         assertFalse(cfm.isCQLTable());
 
         List<Pair<ColumnDefinition, IndexTarget.Type>> compactTableTargets = new ArrayList<>();
-        compactTableTargets.add(CassandraIndex.parseTarget(cfm, "a"));
-        compactTableTargets.add(CassandraIndex.parseTarget(cfm, "b"));
-        compactTableTargets.add(CassandraIndex.parseTarget(cfm, "c"));
+        compactTableTargets.add(TargetParser.parse(cfm, "a"));
+        compactTableTargets.add(TargetParser.parse(cfm, "b"));
+        compactTableTargets.add(TargetParser.parse(cfm, "c"));
 
         execute(String.format("ALTER TABLE %s.%s DROP COMPACT STORAGE", KEYSPACE, TABLE));
         cfm = Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE).metadata;
         assertTrue(cfm.isCQLTable());
 
         List<Pair<ColumnDefinition, IndexTarget.Type>> cqlTableTargets = new ArrayList<>();
-        cqlTableTargets.add(CassandraIndex.parseTarget(cfm, "a"));
-        cqlTableTargets.add(CassandraIndex.parseTarget(cfm, "b"));
-        cqlTableTargets.add(CassandraIndex.parseTarget(cfm, "c"));
+        cqlTableTargets.add(TargetParser.parse(cfm, "a"));
+        cqlTableTargets.add(TargetParser.parse(cfm, "b"));
+        cqlTableTargets.add(TargetParser.parse(cfm, "c"));
 
         assertEquals(compactTableTargets, cqlTableTargets);
     }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/DropTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/DropTest.java
index 692eb45..eb51bd7 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/DropTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/DropTest.java
@@ -25,6 +25,15 @@
 public class DropTest extends CQLTester
 {
     @Test
+    public void testDropTableWithNameCapitalPAndColumnDuration() throws Throwable
+    {
+        // CASSANDRA-17919
+        createTable(KEYSPACE, "CREATE TABLE %s (a INT PRIMARY KEY, b DURATION);", "P");
+        execute("DROP TABLE %s");
+        assertRowsIgnoringOrder(execute(String.format("SELECT * FROM system_schema.dropped_columns WHERE keyspace_name = '%s' AND table_name = 'P'", keyspace())));
+    }
+
+    @Test
     public void testNonExistingOnes() throws Throwable
     {
         assertInvalidMessage("Cannot drop non existing table", "DROP TABLE " + KEYSPACE + ".table_does_not_exist");
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertTest.java
index e467291..70998bd 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertTest.java
@@ -18,15 +18,57 @@
 
 package org.apache.cassandra.cql3.validation.operations;
 
+import org.junit.Assert;
 import org.junit.Test;
 
+import org.apache.cassandra.cql3.Attributes;
 import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.Duration;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.UntypedResultSet.Row;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class InsertTest extends CQLTester
 {
     @Test
+    public void testInsertZeroDuration() throws Throwable
+    {
+        Duration expectedDuration = Duration.newInstance(0, 0, 0);
+        createTable(KEYSPACE, "CREATE TABLE %s (a INT PRIMARY KEY, b DURATION);");
+        execute("INSERT INTO %s (a, b) VALUES (1, P0Y)");
+        execute("INSERT INTO %s (a, b) VALUES (2, P0M)");
+        execute("INSERT INTO %s (a, b) VALUES (3, P0W)");
+        execute("INSERT INTO %s (a, b) VALUES (4, P0D)");
+        execute("INSERT INTO %s (a, b) VALUES (5, P0Y0M0D)");
+        execute("INSERT INTO %s (a, b) VALUES (6, PT0H)");
+        execute("INSERT INTO %s (a, b) VALUES (7, PT0M)");
+        execute("INSERT INTO %s (a, b) VALUES (8, PT0S)");
+        execute("INSERT INTO %s (a, b) VALUES (9, PT0H0M0S)");
+        execute("INSERT INTO %s (a, b) VALUES (10, P0YT0H)");
+        execute("INSERT INTO %s (a, b) VALUES (11, P0MT0M)");
+        execute("INSERT INTO %s (a, b) VALUES (12, P0DT0S)");
+        execute("INSERT INTO %s (a, b) VALUES (13, P0M0DT0H0S)");
+        execute("INSERT INTO %s (a, b) VALUES (14, P0Y0M0DT0H0M0S)");
+        assertRowsIgnoringOrder(execute("SELECT * FROM %s"),
+                                row(1, expectedDuration),
+                                row(2, expectedDuration),
+                                row(3, expectedDuration),
+                                row(4, expectedDuration),
+                                row(5, expectedDuration),
+                                row(6, expectedDuration),
+                                row(7, expectedDuration),
+                                row(8, expectedDuration),
+                                row(9, expectedDuration),
+                                row(10, expectedDuration),
+                                row(11, expectedDuration),
+                                row(12, expectedDuration),
+                                row(13, expectedDuration),
+                                row(14, expectedDuration));
+        assertInvalidMessage("no viable alternative at input ')' (... b) VALUES (15, [P]))","INSERT INTO %s (a, b) VALUES (15, P)");
+    }
+
+    @Test
     public void testInsertWithUnset() throws Throwable
     {
         createTable("CREATE TABLE %s (k int PRIMARY KEY, s text, i int)");
@@ -50,13 +92,24 @@
     }
 
     @Test
-    public void testInsertTtlWithUnset() throws Throwable
+    public void testInsertWithTtl() throws Throwable
     {
-        createTable("CREATE TABLE %s (k int PRIMARY KEY, i int)");
-        execute("INSERT INTO %s (k, i) VALUES (1, 1) USING TTL ?", unset()); // treat as 'unlimited'
-        assertRows(execute("SELECT ttl(i) FROM %s"),
-                   row(new Object[]{ null })
-        );
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)");
+
+        // test with unset
+        execute("INSERT INTO %s (k, v) VALUES (1, 1) USING TTL ?", unset()); // treat as 'unlimited'
+        assertRows(execute("SELECT ttl(v) FROM %s"), row(new Object[]{ null }));
+
+        // test with null
+        execute("INSERT INTO %s (k, v) VALUES (?, ?) USING TTL ?", 1, 1, null);
+        assertRows(execute("SELECT k, v, TTL(v) FROM %s"), row(1, 1, null));
+
+        // test error handling
+        assertInvalidMessage("A TTL must be greater or equal to 0, but was -5",
+                             "INSERT INTO %s (k, v) VALUES (?, ?) USING TTL ?", 1, 1, -5);
+
+        assertInvalidMessage("ttl is too large.",
+                             "INSERT INTO %s (k, v) VALUES (?, ?) USING TTL ?", 1, 1, Attributes.MAX_TTL + 1);
     }
 
     @Test
@@ -96,10 +149,10 @@
                              "INSERT INTO %s (partitionKey, clustering, clustering, value) VALUES (0, 0, 0, 2)");
 
         // unknown identifiers
-        assertInvalidMessage("Unknown identifier clusteringx",
+        assertInvalidMessage("Undefined column name clusteringx",
                              "INSERT INTO %s (partitionKey, clusteringx, value) VALUES (0, 0, 2)");
 
-        assertInvalidMessage("Unknown identifier valuex",
+        assertInvalidMessage("Undefined column name valuex",
                              "INSERT INTO %s (partitionKey, clustering, valuex) VALUES (0, 0, 2)");
     }
 
@@ -144,10 +197,10 @@
                              "INSERT INTO %s (partitionKey, clustering, clustering, value) VALUES (0, 0, 0, 2)");
 
         // unknown identifiers
-        assertInvalidMessage("Unknown identifier clusteringx",
+        assertInvalidMessage("Undefined column name clusteringx",
                              "INSERT INTO %s (partitionKey, clusteringx, value) VALUES (0, 0, 2)");
 
-        assertInvalidMessage("Unknown identifier valuex",
+        assertInvalidMessage("Undefined column name valuex",
                              "INSERT INTO %s (partitionKey, clustering, valuex) VALUES (0, 0, 2)");
     }
 
@@ -189,10 +242,10 @@
                              "INSERT INTO %s (partitionKey, clustering_1, clustering_1, clustering_2, value) VALUES (0, 0, 0, 0, 2)");
 
         // unknown identifiers
-        assertInvalidMessage("Unknown identifier clustering_1x",
+        assertInvalidMessage("Undefined column name clustering_1x",
                              "INSERT INTO %s (partitionKey, clustering_1x, clustering_2, value) VALUES (0, 0, 0, 2)");
 
-        assertInvalidMessage("Unknown identifier valuex",
+        assertInvalidMessage("Undefined column name valuex",
                              "INSERT INTO %s (partitionKey, clustering_1, clustering_2, valuex) VALUES (0, 0, 0, 2)");
     }
 
@@ -207,13 +260,13 @@
         // if column1 is present, hidden column is called column2
         createTable("CREATE TABLE %s (a int PRIMARY KEY, b int, c int, column1 int) WITH COMPACT STORAGE");
         execute("INSERT INTO %s (a, b, c, column1) VALUES (1, 1, 1, 1)");
-        assertInvalidMessage("Unknown identifier column2",
+        assertInvalidMessage("Undefined column name column2",
                              "INSERT INTO %s (a, b, c, column2) VALUES (1, 1, 1, 1)");
 
         // if value is present, hidden column is called value1
         createTable("CREATE TABLE %s (a int PRIMARY KEY, b int, c int, value int) WITH COMPACT STORAGE");
         execute("INSERT INTO %s (a, b, c, value) VALUES (1, 1, 1, 1)");
-        assertInvalidMessage("Unknown identifier value1",
+        assertInvalidMessage("Undefined column name value1",
                              "INSERT INTO %s (a, b, c, value1) VALUES (1, 1, 1, 1)");
     }
 
@@ -232,33 +285,33 @@
         createTable(tableQuery);
 
         // pass correct types to the hidden columns
-        assertInvalidMessage("Unknown identifier column1",
+        assertInvalidMessage("Undefined column name column1",
                              "INSERT INTO %s (a, b, column1) VALUES (?, ?, ?)",
                              1, 1, ByteBufferUtil.bytes('a'));
-        assertInvalidMessage("Unknown identifier value",
+        assertInvalidMessage("Undefined column name value",
                              "INSERT INTO %s (a, b, value) VALUES (?, ?, ?)",
                              1, 1, ByteBufferUtil.bytes('a'));
-        assertInvalidMessage("Unknown identifier column1",
+        assertInvalidMessage("Undefined column name column1",
                              "INSERT INTO %s (a, b, column1, value) VALUES (?, ?, ?, ?)",
                              1, 1, ByteBufferUtil.bytes('a'), ByteBufferUtil.bytes('b'));
-        assertInvalidMessage("Unknown identifier value",
+        assertInvalidMessage("Undefined column name value",
                              "INSERT INTO %s (a, b, value, column1) VALUES (?, ?, ?, ?)",
                              1, 1, ByteBufferUtil.bytes('a'), ByteBufferUtil.bytes('b'));
 
         // pass incorrect types to the hidden columns
-        assertInvalidMessage("Unknown identifier value",
+        assertInvalidMessage("Undefined column name value",
                              "INSERT INTO %s (a, b, value) VALUES (?, ?, ?)",
                              1, 1, 1);
-        assertInvalidMessage("Unknown identifier column1",
+        assertInvalidMessage("Undefined column name column1",
                              "INSERT INTO %s (a, b, column1) VALUES (?, ?, ?)",
                              1, 1, 1);
         assertEmpty(execute("SELECT * FROM %s"));
 
         // pass null to the hidden columns
-        assertInvalidMessage("Unknown identifier value",
+        assertInvalidMessage("Undefined column name value",
                              "INSERT INTO %s (a, b, value) VALUES (?, ?, ?)",
                              1, 1, null);
-        assertInvalidMessage("Unknown identifier column1",
+        assertInvalidMessage("Undefined column name column1",
                              "INSERT INTO %s (a, b, column1) VALUES (?, ?, ?)",
                              1, 1, null);
     }
@@ -309,10 +362,10 @@
                              "INSERT INTO %s (partitionKey, clustering_1, clustering_1, clustering_2, value) VALUES (0, 0, 0, 0, 2)");
 
         // unknown identifiers
-        assertInvalidMessage("Unknown identifier clustering_1x",
+        assertInvalidMessage("Undefined column name clustering_1x",
                              "INSERT INTO %s (partitionKey, clustering_1x, clustering_2, value) VALUES (0, 0, 0, 2)");
 
-        assertInvalidMessage("Unknown identifier valuex",
+        assertInvalidMessage("Undefined column name valuex",
                              "INSERT INTO %s (partitionKey, clustering_1, clustering_2, valuex) VALUES (0, 0, 0, 2)");
     }
 
@@ -354,6 +407,37 @@
     }
 
     @Test
+    public void testInsertWithDefaultTtl() throws Throwable
+    {
+        final int secondsPerMinute = 60;
+        createTable("CREATE TABLE %s (a int PRIMARY KEY, b int) WITH default_time_to_live = " + (10 * secondsPerMinute));
+
+        execute("INSERT INTO %s (a, b) VALUES (1, 1)");
+        UntypedResultSet resultSet = execute("SELECT ttl(b) FROM %s WHERE a = 1");
+        Assert.assertEquals(1, resultSet.size());
+        Row row = resultSet.one();
+        Assert.assertTrue(row.getInt("ttl(b)") >= (9 * secondsPerMinute));
+
+        execute("INSERT INTO %s (a, b) VALUES (2, 2) USING TTL ?", (5 * secondsPerMinute));
+        resultSet = execute("SELECT ttl(b) FROM %s WHERE a = 2");
+        Assert.assertEquals(1, resultSet.size());
+        row = resultSet.one();
+        Assert.assertTrue(row.getInt("ttl(b)") <= (5 * secondsPerMinute));
+
+        execute("INSERT INTO %s (a, b) VALUES (3, 3) USING TTL ?", 0);
+        assertRows(execute("SELECT ttl(b) FROM %s WHERE a = 3"), row(new Object[]{null}));
+
+        execute("INSERT INTO %s (a, b) VALUES (4, 4) USING TTL ?", unset());
+        resultSet = execute("SELECT ttl(b) FROM %s WHERE a = 4");
+        Assert.assertEquals(1, resultSet.size());
+        row = resultSet.one();
+        Assert.assertTrue(row.getInt("ttl(b)") >= (9 * secondsPerMinute));
+
+        execute("INSERT INTO %s (a, b) VALUES (?, ?) USING TTL ?", 4, 4, null);
+        assertRows(execute("SELECT ttl(b) FROM %s WHERE a = 4"), row(new Object[]{null}));
+    }
+
+    @Test
     public void testPKInsertWithValueOver64K() throws Throwable
     {
         createTable("CREATE TABLE %s (a text, b text, PRIMARY KEY (a, b))");
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java
index a47691a..1fb5d2b 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java
@@ -23,10 +23,13 @@
 
 import org.junit.Test;
 
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.Duration;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.exceptions.SyntaxException;
 import org.apache.cassandra.schema.SchemaKeyspace;
+import org.apache.cassandra.schema.SchemaKeyspaceTables;
 
 import static java.lang.String.format;
 import static org.junit.Assert.assertEquals;
@@ -555,6 +558,377 @@
         assertRows(execute("INSERT INTO %s (partition, key, owner) VALUES ('a', 'c', 'x') IF NOT EXISTS"), row(true));
     }
 
+    @Test
+    public void testWholeUDT() throws Throwable
+    {
+        String typename = createType("CREATE TYPE %s (a int, b text)");
+        String myType = KEYSPACE + '.' + typename;
+
+        for (boolean frozen : new boolean[] {false, true})
+        {
+            createTable(String.format("CREATE TABLE %%s (k int PRIMARY KEY, v %s)",
+                                      frozen
+                                      ? "frozen<" + myType + ">"
+                                      : myType));
+
+            Object v = userType("a", 0, "b", "abc");
+            execute("INSERT INTO %s (k, v) VALUES (0, ?)", v);
+
+            checkAppliesUDT("v = {a: 0, b: 'abc'}", v);
+            checkAppliesUDT("v != null", v);
+            checkAppliesUDT("v != {a: 1, b: 'abc'}", v);
+            checkAppliesUDT("v != {a: 0, b: 'def'}", v);
+            checkAppliesUDT("v > {a: -1, b: 'abc'}", v);
+            checkAppliesUDT("v > {a: 0, b: 'aaa'}", v);
+            checkAppliesUDT("v > {a: 0}", v);
+            checkAppliesUDT("v >= {a: 0, b: 'aaa'}", v);
+            checkAppliesUDT("v >= {a: 0, b: 'abc'}", v);
+            checkAppliesUDT("v < {a: 0, b: 'zzz'}", v);
+            checkAppliesUDT("v < {a: 1, b: 'abc'}", v);
+            checkAppliesUDT("v < {a: 1}", v);
+            checkAppliesUDT("v <= {a: 0, b: 'zzz'}", v);
+            checkAppliesUDT("v <= {a: 0, b: 'abc'}", v);
+            checkAppliesUDT("v IN (null, {a: 0, b: 'abc'}, {a: 1})", v);
+
+            // multiple conditions
+            checkAppliesUDT("v > {a: -1, b: 'abc'} AND v > {a: 0}", v);
+            checkAppliesUDT("v != null AND v IN ({a: 0, b: 'abc'})", v);
+
+            // should not apply
+            checkDoesNotApplyUDT("v = {a: 0, b: 'def'}", v);
+            checkDoesNotApplyUDT("v = {a: 1, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v = null", v);
+            checkDoesNotApplyUDT("v != {a: 0, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v > {a: 1, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v > {a: 0, b: 'zzz'}", v);
+            checkDoesNotApplyUDT("v >= {a: 1, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v >= {a: 0, b: 'zzz'}", v);
+            checkDoesNotApplyUDT("v < {a: -1, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v < {a: 0, b: 'aaa'}", v);
+            checkDoesNotApplyUDT("v <= {a: -1, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v <= {a: 0, b: 'aaa'}", v);
+            checkDoesNotApplyUDT("v IN ({a: 0}, {b: 'abc'}, {a: 0, b: 'def'}, null)", v);
+            checkDoesNotApplyUDT("v IN ()", v);
+
+            // multiple conditions
+            checkDoesNotApplyUDT("v IN () AND v IN ({a: 0, b: 'abc'})", v);
+            checkDoesNotApplyUDT("v > {a: 0, b: 'aaa'} AND v < {a: 0, b: 'aaa'}", v);
+
+            // invalid conditions
+            checkInvalidUDT("v = {a: 1, b: 'abc', c: 'foo'}", v, InvalidRequestException.class);
+            checkInvalidUDT("v = {foo: 'foo'}", v, InvalidRequestException.class);
+            checkInvalidUDT("v < {a: 1, b: 'abc', c: 'foo'}", v, InvalidRequestException.class);
+            checkInvalidUDT("v < null", v, InvalidRequestException.class);
+            checkInvalidUDT("v <= {a: 1, b: 'abc', c: 'foo'}", v, InvalidRequestException.class);
+            checkInvalidUDT("v <= null", v, InvalidRequestException.class);
+            checkInvalidUDT("v > {a: 1, b: 'abc', c: 'foo'}", v, InvalidRequestException.class);
+            checkInvalidUDT("v > null", v, InvalidRequestException.class);
+            checkInvalidUDT("v >= {a: 1, b: 'abc', c: 'foo'}", v, InvalidRequestException.class);
+            checkInvalidUDT("v >= null", v, InvalidRequestException.class);
+            checkInvalidUDT("v IN null", v, SyntaxException.class);
+            checkInvalidUDT("v IN 367", v, SyntaxException.class);
+            checkInvalidUDT("v CONTAINS KEY 123", v, SyntaxException.class);
+            checkInvalidUDT("v CONTAINS 'bar'", v, SyntaxException.class);
+
+
+            /////////////////// null suffix on stored udt ////////////////////
+            v = userType("a", 0, "b", null);
+            execute("INSERT INTO %s (k, v) VALUES (0, ?)", v);
+
+            checkAppliesUDT("v = {a: 0}", v);
+            checkAppliesUDT("v = {a: 0, b: null}", v);
+            checkAppliesUDT("v != null", v);
+            checkAppliesUDT("v != {a: 1, b: null}", v);
+            checkAppliesUDT("v != {a: 1}", v);
+            checkAppliesUDT("v != {a: 0, b: 'def'}", v);
+            checkAppliesUDT("v > {a: -1, b: 'abc'}", v);
+            checkAppliesUDT("v > {a: -1}", v);
+            checkAppliesUDT("v >= {a: 0}", v);
+            checkAppliesUDT("v >= {a: -1, b: 'abc'}", v);
+            checkAppliesUDT("v < {a: 0, b: 'zzz'}", v);
+            checkAppliesUDT("v < {a: 1, b: 'abc'}", v);
+            checkAppliesUDT("v < {a: 1}", v);
+            checkAppliesUDT("v <= {a: 0, b: 'zzz'}", v);
+            checkAppliesUDT("v <= {a: 0}", v);
+            checkAppliesUDT("v IN (null, {a: 0, b: 'abc'}, {a: 0})", v);
+
+            // multiple conditions
+            checkAppliesUDT("v > {a: -1, b: 'abc'} AND v >= {a: 0}", v);
+            checkAppliesUDT("v != null AND v IN ({a: 0}, {a: 0, b: null})", v);
+
+            // should not apply
+            checkDoesNotApplyUDT("v = {a: 0, b: 'def'}", v);
+            checkDoesNotApplyUDT("v = {a: 1}", v);
+            checkDoesNotApplyUDT("v = {b: 'abc'}", v);
+            checkDoesNotApplyUDT("v = null", v);
+            checkDoesNotApplyUDT("v != {a: 0}", v);
+            checkDoesNotApplyUDT("v != {a: 0, b: null}", v);
+            checkDoesNotApplyUDT("v > {a: 1, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v > {a: 0}", v);
+            checkDoesNotApplyUDT("v >= {a: 1, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v >= {a: 1}", v);
+            checkDoesNotApplyUDT("v < {a: -1, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v < {a: -1}", v);
+            checkDoesNotApplyUDT("v < {a: 0}", v);
+            checkDoesNotApplyUDT("v <= {a: -1, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v <= {a: -1}", v);
+            checkDoesNotApplyUDT("v IN ({a: 1}, {b: 'abc'}, {a: 0, b: 'def'}, null)", v);
+            checkDoesNotApplyUDT("v IN ()", v);
+
+            // multiple conditions
+            checkDoesNotApplyUDT("v IN () AND v IN ({a: 0})", v);
+            checkDoesNotApplyUDT("v > {a: -1} AND v < {a: 0}", v);
+
+
+            /////////////////// null prefix on stored udt ////////////////////
+            v = userType("a", null, "b", "abc");
+            execute("INSERT INTO %s (k, v) VALUES (0, ?)", v);
+
+            checkAppliesUDT("v = {a: null, b: 'abc'}", v);
+            checkAppliesUDT("v = {b: 'abc'}", v);
+            checkAppliesUDT("v != null", v);
+            checkAppliesUDT("v != {a: 0, b: 'abc'}", v);
+            checkAppliesUDT("v != {a: 0}", v);
+            checkAppliesUDT("v != {b: 'def'}", v);
+            checkAppliesUDT("v > {a: null, b: 'aaa'}", v);
+            checkAppliesUDT("v > {b: 'aaa'}", v);
+            checkAppliesUDT("v >= {a: null, b: 'aaa'}", v);
+            checkAppliesUDT("v >= {b: 'abc'}", v);
+            checkAppliesUDT("v < {a: null, b: 'zzz'}", v);
+            checkAppliesUDT("v < {a: 0, b: 'abc'}", v);
+            checkAppliesUDT("v < {a: 0}", v);
+            checkAppliesUDT("v < {b: 'zzz'}", v);
+            checkAppliesUDT("v <= {a: null, b: 'zzz'}", v);
+            checkAppliesUDT("v <= {a: 0}", v);
+            checkAppliesUDT("v <= {b: 'abc'}", v);
+            checkAppliesUDT("v IN (null, {a: null, b: 'abc'}, {a: 0})", v);
+            checkAppliesUDT("v IN (null, {a: 0, b: 'abc'}, {b: 'abc'})", v);
+
+            // multiple conditions
+            checkAppliesUDT("v > {b: 'aaa'} AND v >= {b: 'abc'}", v);
+            checkAppliesUDT("v != null AND v IN ({a: 0}, {a: null, b: 'abc'})", v);
+
+            // should not apply
+            checkDoesNotApplyUDT("v = {a: 0, b: 'def'}", v);
+            checkDoesNotApplyUDT("v = {a: 1}", v);
+            checkDoesNotApplyUDT("v = {b: 'def'}", v);
+            checkDoesNotApplyUDT("v = null", v);
+            checkDoesNotApplyUDT("v != {b: 'abc'}", v);
+            checkDoesNotApplyUDT("v != {a: null, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v > {a: 1, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v > {a: null, b: 'zzz'}", v);
+            checkDoesNotApplyUDT("v > {b: 'zzz'}", v);
+            checkDoesNotApplyUDT("v >= {a: null, b: 'zzz'}", v);
+            checkDoesNotApplyUDT("v >= {a: 1}", v);
+            checkDoesNotApplyUDT("v >= {b: 'zzz'}", v);
+            checkDoesNotApplyUDT("v < {a: null, b: 'aaa'}", v);
+            checkDoesNotApplyUDT("v < {b: 'aaa'}", v);
+            checkDoesNotApplyUDT("v <= {a: null, b: 'aaa'}", v);
+            checkDoesNotApplyUDT("v <= {b: 'aaa'}", v);
+            checkDoesNotApplyUDT("v IN ({a: 1}, {a: 1, b: 'abc'}, {a: null, b: 'def'}, null)", v);
+            checkDoesNotApplyUDT("v IN ()", v);
+
+            // multiple conditions
+            checkDoesNotApplyUDT("v IN () AND v IN ({b: 'abc'})", v);
+            checkDoesNotApplyUDT("v IN () AND v IN ({a: null, b: 'abc'})", v);
+            checkDoesNotApplyUDT("v > {a: -1} AND v < {a: 0}", v);
+
+
+            /////////////////// null udt ////////////////////
+            v = null;
+            execute("INSERT INTO %s (k, v) VALUES (0, ?)", v);
+
+            checkAppliesUDT("v = null", v);
+            checkAppliesUDT("v IN (null, {a: null, b: 'abc'}, {a: 0})", v);
+            checkAppliesUDT("v IN (null, {a: 0, b: 'abc'}, {b: 'abc'})", v);
+
+            // multiple conditions
+            checkAppliesUDT("v = null AND v IN (null, {a: 0}, {a: null, b: 'abc'})", v);
+
+            // should not apply
+            checkDoesNotApplyUDT("v = {a: 0, b: 'def'}", v);
+            checkDoesNotApplyUDT("v = {a: 1}", v);
+            checkDoesNotApplyUDT("v = {b: 'def'}", v);
+            checkDoesNotApplyUDT("v != null", v);
+            checkDoesNotApplyUDT("v > {a: 1, b: 'abc'}", v);
+            checkDoesNotApplyUDT("v > {a: null, b: 'zzz'}", v);
+            checkDoesNotApplyUDT("v > {b: 'zzz'}", v);
+            checkDoesNotApplyUDT("v >= {a: null, b: 'zzz'}", v);
+            checkDoesNotApplyUDT("v >= {a: 1}", v);
+            checkDoesNotApplyUDT("v >= {b: 'zzz'}", v);
+            checkDoesNotApplyUDT("v < {a: null, b: 'aaa'}", v);
+            checkDoesNotApplyUDT("v < {b: 'aaa'}", v);
+            checkDoesNotApplyUDT("v <= {a: null, b: 'aaa'}", v);
+            checkDoesNotApplyUDT("v <= {b: 'aaa'}", v);
+            checkDoesNotApplyUDT("v IN ({a: 1}, {a: 1, b: 'abc'}, {a: null, b: 'def'})", v);
+            checkDoesNotApplyUDT("v IN ()", v);
+
+            // multiple conditions
+            checkDoesNotApplyUDT("v IN () AND v IN ({b: 'abc'})", v);
+            checkDoesNotApplyUDT("v > {a: -1} AND v < {a: 0}", v);
+
+        }
+    }
+
+    @Test
+    public void testUDTField() throws Throwable
+    {
+        String typename = createType("CREATE TYPE %s (a int, b text)");
+        String myType = KEYSPACE + '.' + typename;
+
+        for (boolean frozen : new boolean[] {false, true})
+        {
+            createTable(String.format("CREATE TABLE %%s (k int PRIMARY KEY, v %s)",
+                                      frozen
+                                      ? "frozen<" + myType + ">"
+                                      : myType));
+
+            Object v = userType("a", 0, "b", "abc");
+            execute("INSERT INTO %s (k, v) VALUES (0, ?)", v);
+
+            checkAppliesUDT("v.a = 0", v);
+            checkAppliesUDT("v.b = 'abc'", v);
+            checkAppliesUDT("v.a < 1", v);
+            checkAppliesUDT("v.b < 'zzz'", v);
+            checkAppliesUDT("v.b <= 'bar'", v);
+            checkAppliesUDT("v.b > 'aaa'", v);
+            checkAppliesUDT("v.b >= 'abc'", v);
+            checkAppliesUDT("v.a != -1", v);
+            checkAppliesUDT("v.b != 'xxx'", v);
+            checkAppliesUDT("v.a != null", v);
+            checkAppliesUDT("v.b != null", v);
+            checkAppliesUDT("v.a IN (null, 0, 1)", v);
+            checkAppliesUDT("v.b IN (null, 'xxx', 'abc')", v);
+            checkAppliesUDT("v.b > 'aaa' AND v.b < 'zzz'", v);
+            checkAppliesUDT("v.a = 0 AND v.b > 'aaa'", v);
+
+            // do not apply
+            checkDoesNotApplyUDT("v.a = -1", v);
+            checkDoesNotApplyUDT("v.b = 'xxx'", v);
+            checkDoesNotApplyUDT("v.a < -1", v);
+            checkDoesNotApplyUDT("v.b < 'aaa'", v);
+            checkDoesNotApplyUDT("v.b <= 'aaa'", v);
+            checkDoesNotApplyUDT("v.b > 'zzz'", v);
+            checkDoesNotApplyUDT("v.b >= 'zzz'", v);
+            checkDoesNotApplyUDT("v.a != 0", v);
+            checkDoesNotApplyUDT("v.b != 'abc'", v);
+            checkDoesNotApplyUDT("v.a IN (null, -1)", v);
+            checkDoesNotApplyUDT("v.b IN (null, 'xxx')", v);
+            checkDoesNotApplyUDT("v.a IN ()", v);
+            checkDoesNotApplyUDT("v.b IN ()", v);
+            checkDoesNotApplyUDT("v.b != null AND v.b IN ()", v);
+
+            // invalid
+            checkInvalidUDT("v.c = null", v, InvalidRequestException.class);
+            checkInvalidUDT("v.a < null", v, InvalidRequestException.class);
+            checkInvalidUDT("v.a <= null", v, InvalidRequestException.class);
+            checkInvalidUDT("v.a > null", v, InvalidRequestException.class);
+            checkInvalidUDT("v.a >= null", v, InvalidRequestException.class);
+            checkInvalidUDT("v.a IN null", v, SyntaxException.class);
+            checkInvalidUDT("v.a IN 367", v, SyntaxException.class);
+            checkInvalidUDT("v.b IN (1, 2, 3)", v, InvalidRequestException.class);
+            checkInvalidUDT("v.a CONTAINS 367", v, SyntaxException.class);
+            checkInvalidUDT("v.a CONTAINS KEY 367", v, SyntaxException.class);
+
+
+            /////////////// null suffix on udt ////////////////
+            v = userType("a", 0, "b", null);
+            execute("INSERT INTO %s (k, v) VALUES (0, ?)", v);
+
+            checkAppliesUDT("v.a = 0", v);
+            checkAppliesUDT("v.b = null", v);
+            checkAppliesUDT("v.b != 'xxx'", v);
+            checkAppliesUDT("v.a != null", v);
+            checkAppliesUDT("v.a IN (null, 0, 1)", v);
+            checkAppliesUDT("v.b IN (null, 'xxx', 'abc')", v);
+            checkAppliesUDT("v.a = 0 AND v.b = null", v);
+
+            // do not apply
+            checkDoesNotApplyUDT("v.b = 'abc'", v);
+            checkDoesNotApplyUDT("v.a < -1", v);
+            checkDoesNotApplyUDT("v.b < 'aaa'", v);
+            checkDoesNotApplyUDT("v.b <= 'aaa'", v);
+            checkDoesNotApplyUDT("v.b > 'zzz'", v);
+            checkDoesNotApplyUDT("v.b >= 'zzz'", v);
+            checkDoesNotApplyUDT("v.a != 0", v);
+            checkDoesNotApplyUDT("v.b != null", v);
+            checkDoesNotApplyUDT("v.a IN (null, -1)", v);
+            checkDoesNotApplyUDT("v.b IN ('xxx', 'abc')", v);
+            checkDoesNotApplyUDT("v.a IN ()", v);
+            checkDoesNotApplyUDT("v.b IN ()", v);
+            checkDoesNotApplyUDT("v.b != null AND v.b IN ()", v);
+
+
+            /////////////// null prefix on udt ////////////////
+            v = userType("a", null, "b", "abc");
+            execute("INSERT INTO %s (k, v) VALUES (0, ?)", v);
+
+            checkAppliesUDT("v.a = null", v);
+            checkAppliesUDT("v.b = 'abc'", v);
+            checkAppliesUDT("v.a != 0", v);
+            checkAppliesUDT("v.b != null", v);
+            checkAppliesUDT("v.a IN (null, 0, 1)", v);
+            checkAppliesUDT("v.b IN (null, 'xxx', 'abc')", v);
+            checkAppliesUDT("v.a = null AND v.b = 'abc'", v);
+
+            // do not apply
+            checkDoesNotApplyUDT("v.a = 0", v);
+            checkDoesNotApplyUDT("v.a < -1", v);
+            checkDoesNotApplyUDT("v.b >= 'zzz'", v);
+            checkDoesNotApplyUDT("v.a != null", v);
+            checkDoesNotApplyUDT("v.b != 'abc'", v);
+            checkDoesNotApplyUDT("v.a IN (-1, 0)", v);
+            checkDoesNotApplyUDT("v.b IN (null, 'xxx')", v);
+            checkDoesNotApplyUDT("v.a IN ()", v);
+            checkDoesNotApplyUDT("v.b IN ()", v);
+            checkDoesNotApplyUDT("v.b != null AND v.b IN ()", v);
+
+
+            /////////////// null udt ////////////////
+            v = null;
+            execute("INSERT INTO %s (k, v) VALUES (0, ?)", v);
+
+            checkAppliesUDT("v.a = null", v);
+            checkAppliesUDT("v.b = null", v);
+            checkAppliesUDT("v.a != 0", v);
+            checkAppliesUDT("v.b != 'abc'", v);
+            checkAppliesUDT("v.a IN (null, 0, 1)", v);
+            checkAppliesUDT("v.b IN (null, 'xxx', 'abc')", v);
+            checkAppliesUDT("v.a = null AND v.b = null", v);
+
+            // do not apply
+            checkDoesNotApplyUDT("v.a = 0", v);
+            checkDoesNotApplyUDT("v.a < -1", v);
+            checkDoesNotApplyUDT("v.b >= 'zzz'", v);
+            checkDoesNotApplyUDT("v.a != null", v);
+            checkDoesNotApplyUDT("v.b != null", v);
+            checkDoesNotApplyUDT("v.a IN (-1, 0)", v);
+            checkDoesNotApplyUDT("v.b IN ('xxx', 'abc')", v);
+            checkDoesNotApplyUDT("v.a IN ()", v);
+            checkDoesNotApplyUDT("v.b IN ()", v);
+            checkDoesNotApplyUDT("v.b != null AND v.b IN ()", v);
+        }
+    }
+
+    void checkAppliesUDT(String condition, Object value) throws Throwable
+    {
+        assertRows(execute("UPDATE %s SET v = ? WHERE k = 0 IF " + condition, value), row(true));
+        assertRows(execute("SELECT * FROM %s"), row(0, value));
+    }
+
+    void checkDoesNotApplyUDT(String condition, Object value) throws Throwable
+    {
+        assertRows(execute("UPDATE %s SET v = ? WHERE k = 0 IF " + condition, value),
+                   row(false, value));
+        assertRows(execute("SELECT * FROM %s"), row(0, value));
+    }
+
+    void checkInvalidUDT(String condition, Object value, Class<? extends Throwable> expected) throws Throwable
+    {
+        assertInvalidThrow(expected, "UPDATE %s SET v = ?  WHERE k = 0 IF " + condition, value);
+        assertRows(execute("SELECT * FROM %s"), row(0, value));
+    }
+
     /**
      * Migrated from cql_tests.py:TestCQL.whole_list_conditional_test()
      */
@@ -993,8 +1367,8 @@
         // create and confirm
         schemaChange("CREATE KEYSPACE IF NOT EXISTS " + keyspace + " WITH replication = { 'class':'SimpleStrategy', 'replication_factor':1} and durable_writes = true ");
         assertRows(execute(format("select durable_writes from %s.%s where keyspace_name = ?",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.KEYSPACES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.KEYSPACES),
                            keyspace),
                    row(true));
 
@@ -1002,15 +1376,15 @@
         schemaChange("CREATE KEYSPACE IF NOT EXISTS " + keyspace + " WITH replication = {'class':'SimpleStrategy', 'replication_factor':1} and durable_writes = false ");
 
         assertRows(execute(format("select durable_writes from %s.%s where keyspace_name = ?",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.KEYSPACES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.KEYSPACES),
                            keyspace),
                    row(true));
 
         // drop and confirm
         schemaChange("DROP KEYSPACE IF EXISTS " + keyspace);
 
-        assertEmpty(execute(format("select * from %s.%s where keyspace_name = ?", SchemaKeyspace.NAME, SchemaKeyspace.KEYSPACES),
+        assertEmpty(execute(format("select * from %s.%s where keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.KEYSPACES),
                             keyspace));
     }
 
@@ -1087,8 +1461,8 @@
         // create and confirm
         execute("CREATE TYPE IF NOT EXISTS mytype (somefield int)");
         assertRows(execute(format("SELECT type_name from %s.%s where keyspace_name = ? and type_name = ?",
-                                  SchemaKeyspace.NAME,
-                                  SchemaKeyspace.TYPES),
+                                  SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                  SchemaKeyspaceTables.TYPES),
                            KEYSPACE,
                            "mytype"),
                    row("mytype"));
@@ -1100,8 +1474,8 @@
         // drop and confirm
         execute("DROP TYPE IF EXISTS mytype");
         assertEmpty(execute(format("SELECT type_name from %s.%s where keyspace_name = ? and type_name = ?",
-                                   SchemaKeyspace.NAME,
-                                   SchemaKeyspace.TYPES),
+                                   SchemaConstants.SCHEMA_KEYSPACE_NAME,
+                                   SchemaKeyspaceTables.TYPES),
                             KEYSPACE,
                             "mytype"));
     }
@@ -1615,31 +1989,31 @@
 
             createTable("CREATE TABLE %s (k int PRIMARY KEY, v frozen<" + myType + "> )");
 
-            Object v = userType(0, "abc");
+            Object v = userType("a", 0, "b", "abc");
             execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, v);
 
             // Does not apply
-            assertRows(execute("UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v IN (?, ?)", userType(1, "abc"), userType(0, "ac")),
+            assertRows(execute("UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v IN (?, ?)", userType("a", 1, "b", "abc"), userType("a", 0, "b", "ac")),
                        row(false, v));
-            assertRows(execute("UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v IN (?, ?)", userType(1, "abc"), null),
+            assertRows(execute("UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v IN (?, ?)", userType("a", 1, "b", "abc"), null),
                        row(false, v));
-            assertRows(execute("UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v IN (?, ?)", userType(1, "abc"), unset()),
+            assertRows(execute("UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v IN (?, ?)", userType("a", 1, "b", "abc"), unset()),
                        row(false, v));
             assertRows(execute("UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v IN (?, ?)", null, null),
                        row(false, v));
             assertRows(execute("UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v IN (?, ?)", unset(), unset()),
                        row(false, v));
-            assertRows(execute("UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v IN ?", list(userType(1, "abc"), userType(0, "ac"))),
+            assertRows(execute("UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v IN ?", list(userType("a", 1, "b", "abc"), userType("a", 0, "b", "ac"))),
                        row(false, v));
 
             // Does apply
-            assertRows(execute("UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v IN (?, ?)", userType(0, "abc"), userType(0, "ac")),
+            assertRows(execute("UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v IN (?, ?)", userType("a", 0, "b", "abc"), userType("a", 0, "b", "ac")),
                        row(true));
-            assertRows(execute("UPDATE %s SET v = {a: 1, b: 'bc'} WHERE k = 0 IF v IN (?, ?)", userType(0, "bc"), null),
+            assertRows(execute("UPDATE %s SET v = {a: 1, b: 'bc'} WHERE k = 0 IF v IN (?, ?)", userType("a", 0, "b", "bc"), null),
                        row(true));
-            assertRows(execute("UPDATE %s SET v = {a: 1, b: 'ac'} WHERE k = 0 IF v IN (?, ?, ?)", userType(0, "bc"), unset(), userType(1, "bc")),
+            assertRows(execute("UPDATE %s SET v = {a: 1, b: 'ac'} WHERE k = 0 IF v IN (?, ?, ?)", userType("a", 0, "b", "bc"), unset(), userType("a", 1, "b", "bc")),
                        row(true));
-            assertRows(execute("UPDATE %s SET v = {a: 0, b: 'abc'} WHERE k = 0 IF v IN ?", list(userType(1, "ac"), userType(0, "ac"))),
+            assertRows(execute("UPDATE %s SET v = {a: 0, b: 'abc'} WHERE k = 0 IF v IN ?", list(userType("a", 1, "b", "ac"), userType("a", 0, "b", "ac"))),
                        row(true));
 
             assertInvalidMessage("Invalid null list in IN condition",
@@ -1706,6 +2080,99 @@
     }
 
     @Test
+    public void testConditionalOnDurationColumns() throws Throwable
+    {
+        createTable(" CREATE TABLE %s (k int PRIMARY KEY, v int, d duration)");
+
+        assertInvalidMessage("Slice conditions are not supported on durations",
+                             "UPDATE %s SET v = 3 WHERE k = 0 IF d > 1s");
+        assertInvalidMessage("Slice conditions are not supported on durations",
+                             "UPDATE %s SET v = 3 WHERE k = 0 IF d >= 1s");
+        assertInvalidMessage("Slice conditions are not supported on durations",
+                             "UPDATE %s SET v = 3 WHERE k = 0 IF d <= 1s");
+        assertInvalidMessage("Slice conditions are not supported on durations",
+                             "UPDATE %s SET v = 3 WHERE k = 0 IF d < 1s");
+
+        execute("INSERT INTO %s (k, v, d) VALUES (1, 1, 2s)");
+
+        assertRows(execute("UPDATE %s SET v = 4 WHERE k = 1 IF d = 1s"), row(false, Duration.from("2s")));
+        assertRows(execute("UPDATE %s SET v = 3 WHERE k = 1 IF d = 2s"), row(true));
+
+        assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, Duration.from("2s"), 3));
+
+        assertRows(execute("UPDATE %s SET d = 10s WHERE k = 1 IF d != 2s"), row(false, Duration.from("2s")));
+        assertRows(execute("UPDATE %s SET v = 6 WHERE k = 1 IF d != 1s"), row(true));
+
+        assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, Duration.from("2s"), 6));
+
+        assertRows(execute("UPDATE %s SET v = 5 WHERE k = 1 IF d IN (1s, 5s)"), row(false, Duration.from("2s")));
+        assertRows(execute("UPDATE %s SET d = 10s WHERE k = 1 IF d IN (1s, 2s)"), row(true));
+
+        assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, Duration.from("10s"), 6));
+    }
+
+    @Test
+    public void testConditionalOnDurationWithinLists() throws Throwable
+    {
+        for (Boolean frozen : new Boolean[]{Boolean.FALSE, Boolean.TRUE})
+        {
+            String listType = String.format(frozen ? "frozen<%s>" : "%s", "list<duration>");
+
+            createTable("CREATE TABLE %s (k int PRIMARY KEY, v int, l " + listType + " )");
+
+            assertInvalidMessage("Slice conditions are not supported on collections containing durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF l > [1s, 2s]");
+            assertInvalidMessage("Slice conditions are not supported on collections containing durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF l >= [1s, 2s]");
+            assertInvalidMessage("Slice conditions are not supported on collections containing durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF l <= [1s, 2s]");
+            assertInvalidMessage("Slice conditions are not supported on collections containing durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF l < [1s, 2s]");
+
+            execute("INSERT INTO %s (k, v, l) VALUES (1, 1, [1s, 2s])");
+
+            assertRows(execute("UPDATE %s SET v = 4 WHERE k = 1 IF l = [2s]"), row(false, list(Duration.from("1000ms"), Duration.from("2s"))));
+            assertRows(execute("UPDATE %s SET v = 3 WHERE k = 1 IF l = [1s, 2s]"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, list(Duration.from("1000ms"), Duration.from("2s")), 3));
+
+            assertRows(execute("UPDATE %s SET l = [10s] WHERE k = 1 IF l != [1s, 2s]"), row(false, list(Duration.from("1000ms"), Duration.from("2s"))));
+            assertRows(execute("UPDATE %s SET v = 6 WHERE k = 1 IF l != [1s]"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, list(Duration.from("1000ms"), Duration.from("2s")), 6));
+
+            assertRows(execute("UPDATE %s SET v = 5 WHERE k = 1 IF l IN ([1s], [1s, 5s])"), row(false, list(Duration.from("1000ms"), Duration.from("2s"))));
+            assertRows(execute("UPDATE %s SET l = [5s, 10s] WHERE k = 1 IF l IN ([1s], [1s, 2s])"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, list(Duration.from("5s"), Duration.from("10s")), 6));
+
+            assertInvalidMessage("Slice conditions are not supported on durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF l[0] > 1s");
+            assertInvalidMessage("Slice conditions are not supported on durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF l[0] >= 1s");
+            assertInvalidMessage("Slice conditions are not supported on durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF l[0] <= 1s");
+            assertInvalidMessage("Slice conditions are not supported on durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF l[0] < 1s");
+
+            assertRows(execute("UPDATE %s SET v = 4 WHERE k = 1 IF l[0] = 2s"), row(false, list(Duration.from("5s"), Duration.from("10s"))));
+            assertRows(execute("UPDATE %s SET v = 3 WHERE k = 1 IF l[0] = 5s"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, list(Duration.from("5s"), Duration.from("10s")), 3));
+
+            assertRows(execute("UPDATE %s SET l = [10s] WHERE k = 1 IF l[1] != 10s"), row(false, list(Duration.from("5s"), Duration.from("10s"))));
+            assertRows(execute("UPDATE %s SET v = 6 WHERE k = 1 IF l[1] != 1s"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, list(Duration.from("5s"), Duration.from("10s")), 6));
+
+            assertRows(execute("UPDATE %s SET v = 5 WHERE k = 1 IF l[0] IN (2s, 10s)"), row(false, list(Duration.from("5s"), Duration.from("10s"))));
+            assertRows(execute("UPDATE %s SET l = [6s, 12s] WHERE k = 1 IF l[0] IN (5s, 10s)"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, list(Duration.from("6s"), Duration.from("12s")), 6));
+        }
+    }
+
+    @Test
     public void testInMarkerWithMaps() throws Throwable
     {
         for (boolean frozen : new boolean[] {false, true})
@@ -1761,4 +2228,160 @@
                                  "UPDATE %s SET  m = {'foo' : 'foobar'} WHERE k = 0 IF m[?] IN ?", "foo", unset());
         }
     }
+
+    @Test
+    public void testConditionalOnDurationWithinMaps() throws Throwable
+    {
+        for (Boolean frozen : new Boolean[]{Boolean.FALSE, Boolean.TRUE})
+        {
+            String mapType = String.format(frozen ? "frozen<%s>" : "%s", "map<int, duration>");
+
+            createTable("CREATE TABLE %s (k int PRIMARY KEY, v int, m " + mapType + " )");
+
+            assertInvalidMessage("Slice conditions are not supported on collections containing durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF m > {1: 1s, 2: 2s}");
+            assertInvalidMessage("Slice conditions are not supported on collections containing durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF m >= {1: 1s, 2: 2s}");
+            assertInvalidMessage("Slice conditions are not supported on collections containing durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF m <= {1: 1s, 2: 2s}");
+            assertInvalidMessage("Slice conditions are not supported on collections containing durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF m < {1: 1s, 2: 2s}");
+
+            execute("INSERT INTO %s (k, v, m) VALUES (1, 1, {1: 1s, 2: 2s})");
+
+            assertRows(execute("UPDATE %s SET v = 4 WHERE k = 1 IF m = {2: 2s}"), row(false, map(1, Duration.from("1000ms"), 2, Duration.from("2s"))));
+            assertRows(execute("UPDATE %s SET v = 3 WHERE k = 1 IF m = {1: 1s, 2: 2s}"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, map(1, Duration.from("1000ms"), 2, Duration.from("2s")), 3));
+
+            assertRows(execute("UPDATE %s SET m = {1 :10s} WHERE k = 1 IF m != {1: 1s, 2: 2s}"), row(false, map(1, Duration.from("1000ms"), 2, Duration.from("2s"))));
+            assertRows(execute("UPDATE %s SET v = 6 WHERE k = 1 IF m != {1: 1s}"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, map(1, Duration.from("1000ms"), 2, Duration.from("2s")), 6));
+
+            assertRows(execute("UPDATE %s SET v = 5 WHERE k = 1 IF m IN ({1: 1s}, {1: 5s})"), row(false, map(1, Duration.from("1000ms"), 2, Duration.from("2s"))));
+            assertRows(execute("UPDATE %s SET m = {1: 5s, 2: 10s} WHERE k = 1 IF m IN ({1: 1s}, {1: 1s, 2: 2s})"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, map(1, Duration.from("5s"), 2, Duration.from("10s")), 6));
+
+            assertInvalidMessage("Slice conditions are not supported on durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF m[1] > 1s");
+            assertInvalidMessage("Slice conditions are not supported on durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF m[1] >= 1s");
+            assertInvalidMessage("Slice conditions are not supported on durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF m[1] <= 1s");
+            assertInvalidMessage("Slice conditions are not supported on durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF m[1] < 1s");
+
+            assertRows(execute("UPDATE %s SET v = 4 WHERE k = 1 IF m[1] = 2s"), row(false, map(1, Duration.from("5s"), 2, Duration.from("10s"))));
+            assertRows(execute("UPDATE %s SET v = 3 WHERE k = 1 IF m[1] = 5s"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, map(1, Duration.from("5s"), 2, Duration.from("10s")), 3));
+
+            assertRows(execute("UPDATE %s SET m = {1: 10s} WHERE k = 1 IF m[2] != 10s"), row(false, map(1, Duration.from("5s"), 2, Duration.from("10s"))));
+            assertRows(execute("UPDATE %s SET v = 6 WHERE k = 1 IF m[2] != 1s"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, map(1, Duration.from("5s"), 2, Duration.from("10s")), 6));
+
+            assertRows(execute("UPDATE %s SET v = 5 WHERE k = 1 IF m[1] IN (2s, 10s)"), row(false, map(1, Duration.from("5s"), 2, Duration.from("10s"))));
+            assertRows(execute("UPDATE %s SET m = {1: 6s, 2: 12s} WHERE k = 1 IF m[1] IN (5s, 10s)"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, map(1, Duration.from("6s"), 2, Duration.from("12s")), 6));
+        }
+    }
+
+    @Test
+    public void testConditionalOnDurationWithinUdts() throws Throwable
+    {
+        String udt = createType("CREATE TYPE %s (i int, d duration)");
+
+        for (Boolean frozen : new Boolean[]{Boolean.FALSE, Boolean.TRUE})
+        {
+            udt = String.format(frozen ? "frozen<%s>" : "%s", udt);
+
+            createTable("CREATE TABLE %s (k int PRIMARY KEY, v int, u " + udt + " )");
+
+            assertInvalidMessage("Slice conditions are not supported on UDTs containing durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF u > {i: 1, d: 2s}");
+            assertInvalidMessage("Slice conditions are not supported on UDTs containing durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF u >= {i: 1, d: 2s}");
+            assertInvalidMessage("Slice conditions are not supported on UDTs containing durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF u <= {i: 1, d: 2s}");
+            assertInvalidMessage("Slice conditions are not supported on UDTs containing durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF u < {i: 1, d: 2s}");
+
+            execute("INSERT INTO %s (k, v, u) VALUES (1, 1, {i:1, d:2s})");
+
+            assertRows(execute("UPDATE %s SET v = 4 WHERE k = 1 IF u = {i: 2, d: 2s}"), row(false, userType("i", 1, "d", Duration.from("2s"))));
+            assertRows(execute("UPDATE %s SET v = 3 WHERE k = 1 IF u = {i: 1, d: 2s}"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, userType("i", 1, "d", Duration.from("2s")), 3));
+
+            assertRows(execute("UPDATE %s SET u = {i: 1, d: 10s} WHERE k = 1 IF u != {i: 1, d: 2s}"), row(false, userType("i", 1, "d", Duration.from("2s"))));
+            assertRows(execute("UPDATE %s SET v = 6 WHERE k = 1 IF u != {i: 1, d: 1s}"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, userType("i", 1, "d", Duration.from("2s")), 6));
+
+            assertRows(execute("UPDATE %s SET v = 5 WHERE k = 1 IF u IN ({i: 1, d: 1s}, {i: 1, d: 5s})"), row(false, userType("i", 1, "d", Duration.from("2s"))));
+            assertRows(execute("UPDATE %s SET u = {i: 1, d: 10s} WHERE k = 1 IF u IN ({i: 1, d: 1s}, {i: 1, d: 2s})"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, userType("i", 1, "d", Duration.from("10s")), 6));
+
+            assertInvalidMessage("Slice conditions are not supported on durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF u.d > 1s");
+            assertInvalidMessage("Slice conditions are not supported on durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF u.d >= 1s");
+            assertInvalidMessage("Slice conditions are not supported on durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF u.d <= 1s");
+            assertInvalidMessage("Slice conditions are not supported on durations",
+                                 "UPDATE %s SET v = 3 WHERE k = 0 IF u.d < 1s");
+
+            assertRows(execute("UPDATE %s SET v = 4 WHERE k = 1 IF u.d = 2s"), row(false, userType("i", 1, "d", Duration.from("10s"))));
+            assertRows(execute("UPDATE %s SET v = 3 WHERE k = 1 IF u.d = 10s"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, userType("i", 1, "d", Duration.from("10s")), 3));
+
+            assertRows(execute("UPDATE %s SET u = {i: 1, d: 10s} WHERE k = 1 IF u.d != 10s"), row(false, userType("i", 1, "d", Duration.from("10s"))));
+            assertRows(execute("UPDATE %s SET v = 6 WHERE k = 1 IF u.d != 1s"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, userType("i", 1, "d", Duration.from("10s")), 6));
+
+            assertRows(execute("UPDATE %s SET v = 5 WHERE k = 1 IF u.d IN (2s, 5s)"), row(false, userType("i", 1, "d", Duration.from("10s"))));
+            assertRows(execute("UPDATE %s SET u = {i: 6, d: 12s} WHERE k = 1 IF u.d IN (5s, 10s)"), row(true));
+
+            assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, userType("i", 6, "d", Duration.from("12s")), 6));
+        }
+    }
+
+    @Test
+    public void testConditionalOnDurationWithinTuples() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v int, u tuple<int, duration> )");
+
+        assertInvalidMessage("Slice conditions are not supported on tuples containing durations",
+                             "UPDATE %s SET v = 3 WHERE k = 0 IF u > (1, 2s)");
+        assertInvalidMessage("Slice conditions are not supported on tuples containing durations",
+                             "UPDATE %s SET v = 3 WHERE k = 0 IF u >= (1, 2s)");
+        assertInvalidMessage("Slice conditions are not supported on tuples containing durations",
+                             "UPDATE %s SET v = 3 WHERE k = 0 IF u <= (1, 2s)");
+        assertInvalidMessage("Slice conditions are not supported on tuples containing durations",
+                             "UPDATE %s SET v = 3 WHERE k = 0 IF u < (1, 2s)");
+
+        execute("INSERT INTO %s (k, v, u) VALUES (1, 1, (1, 2s))");
+
+        assertRows(execute("UPDATE %s SET v = 4 WHERE k = 1 IF u = (2, 2s)"), row(false, tuple(1, Duration.from("2s"))));
+        assertRows(execute("UPDATE %s SET v = 3 WHERE k = 1 IF u = (1, 2s)"), row(true));
+
+        assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, tuple(1, Duration.from("2s")), 3));
+
+        assertRows(execute("UPDATE %s SET u = (1, 10s) WHERE k = 1 IF u != (1, 2s)"), row(false, tuple(1, Duration.from("2s"))));
+        assertRows(execute("UPDATE %s SET v = 6 WHERE k = 1 IF u != (1, 1s)"), row(true));
+
+        assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, tuple(1, Duration.from("2s")), 6));
+
+        assertRows(execute("UPDATE %s SET v = 5 WHERE k = 1 IF u IN ((1, 1s), (1, 5s))"), row(false, tuple(1, Duration.from("2s"))));
+        assertRows(execute("UPDATE %s SET u = (1, 10s) WHERE k = 1 IF u IN ((1, 1s), (1, 2s))"), row(true));
+
+        assertRows(execute("SELECT * FROM %s WHERE k = 1"), row(1, tuple(1, Duration.from("10s")), 6));
+    }
 }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectGroupByTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectGroupByTest.java
new file mode 100644
index 0000000..5c51494
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectGroupByTest.java
@@ -0,0 +1,2155 @@
+/*
+ * 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.validation.operations;
+
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+
+public class SelectGroupByTest extends CQLTester
+{
+    @Test
+    public void testGroupByWithoutPaging() throws Throwable
+    {
+        for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
+        {
+            createTable("CREATE TABLE %s (a int, b int, c int, d int, e int, primary key (a, b, c, d))"
+                    + compactOption);
+
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 1, 3, 6)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 2, 6, 12)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 3, 2, 12, 24)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 4, 2, 12, 24)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 4, 2, 6, 12)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (2, 2, 3, 3, 6)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (2, 4, 3, 6, 12)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (3, 3, 2, 12, 24)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (4, 8, 2, 12, 24)");
+
+            // Makes sure that we have some tombstones
+            execute("DELETE FROM %s WHERE a = 1 AND b = 3 AND c = 2 AND d = 12");
+            execute("DELETE FROM %s WHERE a = 3");
+
+            // Range queries
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a"),
+                       row(1, 2, 6, 4L, 24),
+                       row(2, 2, 6, 2L, 12),
+                       row(4, 8, 24, 1L, 24));
+
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a, b"),
+                       row(1, 2, 6, 2L, 12),
+                       row(1, 4, 12, 2L, 24),
+                       row(2, 2, 6, 1L, 6),
+                       row(2, 4, 12, 1L, 12),
+                       row(4, 8, 24, 1L, 24));
+
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE b = 2 GROUP BY a, b ALLOW FILTERING"),
+                       row(1, 2, 6, 2L, 12),
+                       row(2, 2, 6, 1L, 6));
+
+            assertEmpty(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE b IN () GROUP BY a, b ALLOW FILTERING"));
+
+            // Range queries without aggregates
+            assertRows(execute("SELECT a, b, c, d FROM %s GROUP BY a, b, c"),
+                       row(1, 2, 1, 3),
+                       row(1, 2, 2, 6),
+                       row(1, 4, 2, 6),
+                       row(2, 2, 3, 3),
+                       row(2, 4, 3, 6),
+                       row(4, 8, 2, 12));
+
+            assertRows(execute("SELECT a, b, c, d FROM %s GROUP BY a, b"),
+                       row(1, 2, 1, 3),
+                       row(1, 4, 2, 6),
+                       row(2, 2, 3, 3),
+                       row(2, 4, 3, 6),
+                       row(4, 8, 2, 12));
+
+            // Range queries with wildcard
+            assertRows(execute("SELECT * FROM %s GROUP BY a, b, c"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 2, 2, 6, 12),
+                       row(1, 4, 2, 6, 12),
+                       row(2, 2, 3, 3, 6),
+                       row(2, 4, 3, 6, 12),
+                       row(4, 8, 2, 12, 24));
+
+            assertRows(execute("SELECT * FROM %s GROUP BY a, b"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 4, 2, 6, 12),
+                       row(2, 2, 3, 3, 6),
+                       row(2, 4, 3, 6, 12),
+                       row(4, 8, 2, 12, 24));
+
+            // Range query with LIMIT
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a, b LIMIT 2"),
+                       row(1, 2, 6, 2L, 12),
+                       row(1, 4, 12, 2L, 24));
+
+            // Range queries with PER PARTITION LIMIT
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a, b PER PARTITION LIMIT 1"),
+                       row(1, 2, 6, 2L, 12),
+                       row(2, 2, 6, 1L, 6),
+                       row(4, 8, 24, 1L, 24));
+
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a PER PARTITION LIMIT 2"),
+                       row(1, 2, 6, 4L, 24),
+                       row(2, 2, 6, 2L, 12),
+                       row(4, 8, 24, 1L, 24));
+
+            // Range query with PER PARTITION LIMIT and LIMIT
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a, b PER PARTITION LIMIT 1 LIMIT 2"),
+                       row(1, 2, 6, 2L, 12),
+                       row(2, 2, 6, 1L, 6));
+
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a PER PARTITION LIMIT 2"),
+                       row(1, 2, 6, 4L, 24),
+                       row(2, 2, 6, 2L, 12),
+                       row(4, 8, 24, 1L, 24));
+
+            // Range queries without aggregates and with LIMIT
+            assertRows(execute("SELECT a, b, c, d FROM %s GROUP BY a, b, c LIMIT 3"),
+                       row(1, 2, 1, 3),
+                       row(1, 2, 2, 6),
+                       row(1, 4, 2, 6));
+
+            assertRows(execute("SELECT a, b, c, d FROM %s GROUP BY a, b LIMIT 3"),
+                       row(1, 2, 1, 3),
+                       row(1, 4, 2, 6),
+                       row(2, 2, 3, 3));
+
+            // Range queries with wildcard and with LIMIT
+            assertRows(execute("SELECT * FROM %s GROUP BY a, b, c LIMIT 3"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 2, 2, 6, 12),
+                       row(1, 4, 2, 6, 12));
+
+            assertRows(execute("SELECT * FROM %s GROUP BY a, b LIMIT 3"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 4, 2, 6, 12),
+                       row(2, 2, 3, 3, 6));
+
+            // Range queries without aggregates and with PER PARTITION LIMIT
+            assertRows(execute("SELECT a, b, c, d FROM %s GROUP BY a, b, c PER PARTITION LIMIT 2"),
+                       row(1, 2, 1, 3),
+                       row(1, 2, 2, 6),
+                       row(2, 2, 3, 3),
+                       row(2, 4, 3, 6),
+                       row(4, 8, 2, 12));
+
+            assertRows(execute("SELECT a, b, c, d FROM %s GROUP BY a, b PER PARTITION LIMIT 1"),
+                       row(1, 2, 1, 3),
+                       row(2, 2, 3, 3),
+                       row(4, 8, 2, 12));
+
+            // Range queries with wildcard and with PER PARTITION LIMIT
+            assertRows(execute("SELECT * FROM %s GROUP BY a, b, c PER PARTITION LIMIT 2"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 2, 2, 6, 12),
+                       row(2, 2, 3, 3, 6),
+                       row(2, 4, 3, 6, 12),
+                       row(4, 8, 2, 12, 24));
+
+            assertRows(execute("SELECT * FROM %s GROUP BY a, b PER PARTITION LIMIT 1"),
+                       row(1, 2, 1, 3, 6),
+                       row(2, 2, 3, 3, 6),
+                       row(4, 8, 2, 12, 24));
+
+            // Range queries without aggregates, with PER PARTITION LIMIT and LIMIT
+            assertRows(execute("SELECT a, b, c, d FROM %s GROUP BY a, b, c PER PARTITION LIMIT 2 LIMIT 3"),
+                       row(1, 2, 1, 3),
+                       row(1, 2, 2, 6),
+                       row(2, 2, 3, 3));
+
+            // Range queries with wildcard, with PER PARTITION LIMIT and LIMIT
+            assertRows(execute("SELECT * FROM %s GROUP BY a, b, c PER PARTITION LIMIT 2 LIMIT 3"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 2, 2, 6, 12),
+                       row(2, 2, 3, 3, 6));
+
+            // Range query with DISTINCT
+            assertRows(execute("SELECT DISTINCT a, count(a)FROM %s GROUP BY a"),
+                       row(1, 1L),
+                       row(2, 1L),
+                       row(4, 1L));
+
+            assertInvalidMessage("Grouping on clustering columns is not allowed for SELECT DISTINCT queries",
+                                 "SELECT DISTINCT a, count(a)FROM %s GROUP BY a, b");
+
+            // Range query with DISTINCT and LIMIT
+            assertRows(execute("SELECT DISTINCT a, count(a)FROM %s GROUP BY a LIMIT 2"),
+                       row(1, 1L),
+                       row(2, 1L));
+
+            assertInvalidMessage("Grouping on clustering columns is not allowed for SELECT DISTINCT queries",
+                                 "SELECT DISTINCT a, count(a)FROM %s GROUP BY a, b LIMIT 2");
+
+            // Range query with ORDER BY
+            assertInvalidMessage("ORDER BY is only supported when the partition key is restricted by an EQ or an IN",
+                                 "SELECT a, b, c, count(b), max(e) FROM %s GROUP BY a, b ORDER BY b DESC, c DESC");
+
+            // Single partition queries
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c"),
+                       row(1, 2, 6, 1L, 6),
+                       row(1, 2, 12, 1L, 12),
+                       row(1, 4, 12, 2L, 24));
+
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY b, c"),
+                       row(1, 2, 6, 1L, 6),
+                       row(1, 2, 12, 1L, 12),
+                       row(1, 4, 12, 2L, 24));
+
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 AND b = 2 GROUP BY a, b, c"),
+                       row(1, 2, 6, 1L, 6),
+                       row(1, 2, 12, 1L, 12));
+
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 AND b = 2 GROUP BY a, c"),
+                       row(1, 2, 6, 1L, 6),
+                       row(1, 2, 12, 1L, 12));
+
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 AND b = 2 GROUP BY c"),
+                       row(1, 2, 6, 1L, 6),
+                       row(1, 2, 12, 1L, 12));
+
+            // Single partition queries without aggregates
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b"),
+                       row(1, 2, 1, 3),
+                       row(1, 4, 2, 6));
+
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b, c"),
+                       row(1, 2, 1, 3),
+                       row(1, 2, 2, 6),
+                       row(1, 4, 2, 6));
+
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY b, c"),
+                       row(1, 2, 1, 3),
+                       row(1, 2, 2, 6),
+                       row(1, 4, 2, 6));
+
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a = 1 and token(a) = token(1) GROUP BY b, c"),
+                       row(1, 2, 1, 3),
+                       row(1, 2, 2, 6),
+                       row(1, 4, 2, 6));
+
+            // Single partition queries with wildcard
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 GROUP BY a, b, c"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 2, 2, 6, 12),
+                       row(1, 4, 2, 6, 12));
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 GROUP BY a, b"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 4, 2, 6, 12));
+
+            // Single partition queries with DISTINCT
+            assertRows(execute("SELECT DISTINCT a, count(a)FROM %s WHERE a = 1 GROUP BY a"),
+                       row(1, 1L));
+
+            assertInvalidMessage("Grouping on clustering columns is not allowed for SELECT DISTINCT queries",
+                                 "SELECT DISTINCT a, count(a)FROM %s WHERE a = 1 GROUP BY a, b");
+
+            // Single partition queries with LIMIT
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c LIMIT 10"),
+                       row(1, 2, 6, 1L, 6),
+                       row(1, 2, 12, 1L, 12),
+                       row(1, 4, 12, 2L, 24));
+
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c LIMIT 2"),
+                       row(1, 2, 6, 1L, 6),
+                       row(1, 2, 12, 1L, 12));
+
+            assertRows(execute("SELECT count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c LIMIT 1"),
+                       row(1L, 6));
+
+            // Single partition queries with PER PARTITION LIMIT
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c PER PARTITION LIMIT 10"),
+                       row(1, 2, 6, 1L, 6),
+                       row(1, 2, 12, 1L, 12),
+                       row(1, 4, 12, 2L, 24));
+
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c PER PARTITION LIMIT 2"),
+                       row(1, 2, 6, 1L, 6),
+                       row(1, 2, 12, 1L, 12));
+
+            assertRows(execute("SELECT count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c PER PARTITION LIMIT 1"),
+                       row(1L, 6));
+
+            // Single partition queries without aggregates and with LIMIT
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b LIMIT 2"),
+                       row(1, 2, 1, 3),
+                       row(1, 4, 2, 6));
+
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b LIMIT 1"),
+                       row(1, 2, 1, 3));
+
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b, c LIMIT 2"),
+                       row(1, 2, 1, 3),
+                       row(1, 2, 2, 6));
+
+            // Single partition queries with wildcard and with LIMIT
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 GROUP BY a, b, c LIMIT 2"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 2, 2, 6, 12));
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 GROUP BY a, b LIMIT 1"),
+                       row(1, 2, 1, 3, 6));
+
+            // Single partition queries without aggregates and with PER PARTITION LIMIT
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b PER PARTITION LIMIT 2"),
+                       row(1, 2, 1, 3),
+                       row(1, 4, 2, 6));
+
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b PER PARTITION LIMIT 1"),
+                       row(1, 2, 1, 3));
+
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b, c PER PARTITION LIMIT 2"),
+                       row(1, 2, 1, 3),
+                       row(1, 2, 2, 6));
+
+            // Single partition queries with wildcard and with PER PARTITION LIMIT
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 GROUP BY a, b, c PER PARTITION LIMIT 2"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 2, 2, 6, 12));
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 GROUP BY a, b PER PARTITION LIMIT 1"),
+                       row(1, 2, 1, 3, 6));
+
+            // Single partition queries with ORDER BY
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c ORDER BY b DESC, c DESC"),
+                       row(1, 4, 24, 2L, 24),
+                       row(1, 2, 12, 1L, 12),
+                       row(1, 2, 6, 1L, 6));
+
+            // Single partition queries with ORDER BY and PER PARTITION LIMIT
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c ORDER BY b DESC, c DESC PER PARTITION LIMIT 1"),
+                       row(1, 4, 24, 2L, 24));
+
+            // Single partition queries with ORDER BY and LIMIT
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c ORDER BY b DESC, c DESC LIMIT 2"),
+                       row(1, 4, 24, 2L, 24),
+                       row(1, 2, 12, 1L, 12));
+
+            // Multi-partitions queries
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c"),
+                       row(1, 2, 6, 1L, 6),
+                       row(1, 2, 12, 1L, 12),
+                       row(1, 4, 12, 2L, 24),
+                       row(2, 2, 6, 1L, 6),
+                       row(2, 4, 12, 1L, 12),
+                       row(4, 8, 24, 1L, 24));
+
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a IN (1, 2, 4) AND b = 2 GROUP BY a, b, c"),
+                       row(1, 2, 6, 1L, 6),
+                       row(1, 2, 12, 1L, 12),
+                       row(2, 2, 6, 1L, 6));
+
+            // Multi-partitions queries without aggregates
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b"),
+                       row(1, 2, 1, 3),
+                       row(1, 4, 2, 6),
+                       row(2, 2, 3, 3),
+                       row(2, 4, 3, 6),
+                       row(4, 8, 2, 12));
+
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c"),
+                       row(1, 2, 1, 3),
+                       row(1, 2, 2, 6),
+                       row(1, 4, 2, 6),
+                       row(2, 2, 3, 3),
+                       row(2, 4, 3, 6),
+                       row(4, 8, 2, 12));
+
+            // Multi-partitions with wildcard
+            assertRows(execute("SELECT * FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 2, 2, 6, 12),
+                       row(1, 4, 2, 6, 12),
+                       row(2, 2, 3, 3, 6),
+                       row(2, 4, 3, 6, 12),
+                       row(4, 8, 2, 12, 24));
+
+            assertRows(execute("SELECT * FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 4, 2, 6, 12),
+                       row(2, 2, 3, 3, 6),
+                       row(2, 4, 3, 6, 12),
+                       row(4, 8, 2, 12, 24));
+
+            // Multi-partitions query with DISTINCT
+            assertRows(execute("SELECT DISTINCT a, count(a)FROM %s WHERE a IN (1, 2, 4) GROUP BY a"),
+                       row(1, 1L),
+                       row(2, 1L),
+                       row(4, 1L));
+
+            assertInvalidMessage("Grouping on clustering columns is not allowed for SELECT DISTINCT queries",
+                                 "SELECT DISTINCT a, count(a)FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b");
+
+            // Multi-partitions query with DISTINCT and LIMIT
+            assertRows(execute("SELECT DISTINCT a, count(a)FROM %s WHERE a IN (1, 2, 4) GROUP BY a LIMIT 2"),
+                       row(1, 1L),
+                       row(2, 1L));
+
+            // Multi-partitions queries with PER PARTITION LIMIT
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c PER PARTITION LIMIT 1"),
+                       row(1, 2, 6, 1L, 6),
+                       row(2, 2, 6, 1L, 6),
+                       row(4, 8, 24, 1L, 24));
+
+            assertRows(execute("SELECT a, b, e, count(b), max(e) FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c PER PARTITION LIMIT 2"),
+                       row(1, 2, 6, 1L, 6),
+                       row(1, 2, 12, 1L, 12),
+                       row(2, 2, 6, 1L, 6),
+                       row(2, 4, 12, 1L, 12),
+                       row(4, 8, 24, 1L, 24));
+
+            // Multi-partitions with wildcard and PER PARTITION LIMIT
+            assertRows(execute("SELECT * FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c PER PARTITION LIMIT 2"),
+                       row(1, 2, 1, 3, 6),
+                       row(1, 2, 2, 6, 12),
+                       row(2, 2, 3, 3, 6),
+                       row(2, 4, 3, 6, 12),
+                       row(4, 8, 2, 12, 24));
+
+            assertRows(execute("SELECT * FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b PER PARTITION LIMIT 1"),
+                       row(1, 2, 1, 3, 6),
+                       row(2, 2, 3, 3, 6),
+                       row(4, 8, 2, 12, 24));
+
+            // Multi-partitions queries with ORDER BY
+            assertRows(execute("SELECT a, b, c, count(b), max(e) FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b ORDER BY b DESC, c DESC"),
+                       row(4, 8, 2, 1L, 24),
+                       row(2, 4, 3, 1L, 12),
+                       row(1, 4, 2, 2L, 24),
+                       row(2, 2, 3, 1L, 6),
+                       row(1, 2, 2, 2L, 12));
+
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c ORDER BY b DESC, c DESC"),
+                       row(4, 8, 2, 12),
+                       row(2, 4, 3, 6),
+                       row(1, 4, 2, 12),
+                       row(2, 2, 3, 3),
+                       row(1, 2, 2, 6),
+                       row(1, 2, 1, 3));
+
+            // Multi-partitions queries with ORDER BY and LIMIT
+            assertRows(execute("SELECT a, b, c, d FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b ORDER BY b DESC, c DESC LIMIT 3"),
+                       row(4, 8, 2, 12),
+                       row(2, 4, 3, 6),
+                       row(1, 4, 2, 12));
+
+            // Multi-partitions with wildcard, ORDER BY and LIMIT
+            assertRows(execute("SELECT * FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c ORDER BY b DESC, c DESC LIMIT 3"),
+                       row(4, 8, 2, 12, 24),
+                       row(2, 4, 3, 6, 12),
+                       row(1, 4, 2, 12, 24));
+
+            // Invalid queries
+            assertInvalidMessage("Group by is currently only supported on the columns of the PRIMARY KEY, got e",
+                                 "SELECT a, b, d, count(b), max(c) FROM %s WHERE a = 1 GROUP BY a, e");
+
+            assertInvalidMessage("Group by currently only support groups of columns following their declared order in the PRIMARY KEY",
+                                 "SELECT a, b, d, count(b), max(c) FROM %s WHERE a = 1 GROUP BY c");
+
+            assertInvalidMessage("Group by currently only support groups of columns following their declared order in the PRIMARY KEY",
+                                 "SELECT a, b, d, count(b), max(c) FROM %s WHERE a = 1 GROUP BY a, c, b");
+
+            assertInvalidMessage("Group by currently only support groups of columns following their declared order in the PRIMARY KEY",
+                                 "SELECT a, b, d, count(b), max(c) FROM %s WHERE a = 1 GROUP BY a, a");
+
+            assertInvalidMessage("Group by currently only support groups of columns following their declared order in the PRIMARY KEY",
+                                 "SELECT a, b, c, d FROM %s WHERE token(a) = token(1) GROUP BY b, c");
+
+            assertInvalidMessage("Undefined column name clustering1",
+                                 "SELECT a, b as clustering1, max(c) FROM %s WHERE a = 1 GROUP BY a, clustering1");
+
+            assertInvalidMessage("Undefined column name z",
+                                 "SELECT a, b, max(c) FROM %s WHERE a = 1 GROUP BY a, b, z");
+
+            // Test with composite partition key
+            createTable("CREATE TABLE %s (a int, b int, c int, d int, e int, primary key ((a, b), c, d))" + compactOption);
+
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 1, 1, 3, 6)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 1, 2, 6, 12)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 1, 3, 12, 24)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 1, 12, 24)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 2, 6, 12)");
+
+            assertInvalidMessage("Group by is not supported on only a part of the partition key",
+                                 "SELECT a, b, max(d) FROM %s GROUP BY a");
+
+            assertRows(execute("SELECT a, b, max(d) FROM %s GROUP BY a, b"),
+                       row(1, 2, 12),
+                       row(1, 1, 12));
+
+            assertRows(execute("SELECT a, b, max(d) FROM %s WHERE a = 1 AND b = 1 GROUP BY b"),
+                       row(1, 1, 12));
+
+            // Test with table without clustering key
+            createTable("CREATE TABLE %s (a int primary key, b int, c int)" + compactOption);
+
+            execute("INSERT INTO %s (a, b, c) VALUES (1, 3, 6)");
+            execute("INSERT INTO %s (a, b, c) VALUES (2, 6, 12)");
+            execute("INSERT INTO %s (a, b, c) VALUES (3, 12, 24)");
+
+            assertInvalidMessage("Group by currently only support groups of columns following their declared order in the PRIMARY KEY",
+                    "SELECT a, max(c) FROM %s WHERE a = 1 GROUP BY a, a");
+        }
+    }
+
+    @Test
+    public void testGroupByWithoutPagingWithDeletions() throws Throwable
+    {
+        for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
+        {
+            createTable("CREATE TABLE %s (a int, b int, c int, d int, e int, primary key (a, b, c, d))"
+                    + compactOption);
+
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 1, 3, 6)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 1, 6, 12)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 1, 9, 18)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 1, 12, 24)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 2, 3, 6)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 2, 6, 12)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 2, 9, 18)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 2, 12, 24)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 3, 3, 6)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 3, 6, 12)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 3, 9, 18)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 3, 12, 24)");
+
+            execute("DELETE FROM %s WHERE a = 1 AND b = 2 AND c = 1 AND d = 12");
+            execute("DELETE FROM %s WHERE a = 1 AND b = 2 AND c = 2 AND d = 9");
+
+            assertRows(execute("SELECT a, b, c, count(b), max(d) FROM %s GROUP BY a, b, c"),
+                       row(1, 2, 1, 3L, 9),
+                       row(1, 2, 2, 3L, 12),
+                       row(1, 2, 3, 4L, 12));
+        }
+    }
+
+    @Test
+    public void testGroupByWithRangeNamesQueryWithoutPaging() throws Throwable
+    {
+        for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
+        {
+            createTable("CREATE TABLE %s (a int, b int, c int, d int, primary key (a, b, c))"
+                    + compactOption);
+
+            for (int i = 1; i < 5; i++)
+                for (int j = 1; j < 5; j++)
+                    for (int k = 1; k < 5; k++)
+                        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", i, j, k, i + j);
+
+            // Makes sure that we have some tombstones
+            execute("DELETE FROM %s WHERE a = 3");
+
+            // Range queries
+            assertRows(execute("SELECT a, b, d, count(b), max(d) FROM %s WHERE b = 1 and c IN (1, 2) GROUP BY a ALLOW FILTERING"),
+                       row(1, 1, 2, 2L, 2),
+                       row(2, 1, 3, 2L, 3),
+                       row(4, 1, 5, 2L, 5));
+
+            assertRows(execute("SELECT a, b, d, count(b), max(d) FROM %s WHERE b = 1 and c IN (1, 2) GROUP BY a, b ALLOW FILTERING"),
+                       row(1, 1, 2, 2L, 2),
+                       row(2, 1, 3, 2L, 3),
+                       row(4, 1, 5, 2L, 5));
+
+            assertRows(execute("SELECT a, b, d, count(b), max(d) FROM %s WHERE b IN (1, 2) and c IN (1, 2) GROUP BY a, b ALLOW FILTERING"),
+                       row(1, 1, 2, 2L, 2),
+                       row(1, 2, 3, 2L, 3),
+                       row(2, 1, 3, 2L, 3),
+                       row(2, 2, 4, 2L, 4),
+                       row(4, 1, 5, 2L, 5),
+                       row(4, 2, 6, 2L, 6));
+
+            // Range queries with LIMIT
+            assertRows(execute("SELECT a, b, d, count(b), max(d) FROM %s WHERE b = 1 and c IN (1, 2) GROUP BY a LIMIT 5 ALLOW FILTERING"),
+                       row(1, 1, 2, 2L, 2),
+                       row(2, 1, 3, 2L, 3),
+                       row(4, 1, 5, 2L, 5));
+
+            assertRows(execute("SELECT a, b, d, count(b), max(d) FROM %s WHERE b = 1 and c IN (1, 2) GROUP BY a, b LIMIT 3 ALLOW FILTERING"),
+                       row(1, 1, 2, 2L, 2),
+                       row(2, 1, 3, 2L, 3),
+                       row(4, 1, 5, 2L, 5));
+
+            assertRows(execute("SELECT a, b, d, count(b), max(d) FROM %s WHERE b IN (1, 2) and c IN (1, 2) GROUP BY a, b LIMIT 3 ALLOW FILTERING"),
+                       row(1, 1, 2, 2L, 2),
+                       row(1, 2, 3, 2L, 3),
+                       row(2, 1, 3, 2L, 3));
+
+            // Range queries with PER PARTITION LIMIT
+            assertRows(execute("SELECT a, b, d, count(b), max(d) FROM %s WHERE b = 1 and c IN (1, 2) GROUP BY a, b PER PARTITION LIMIT 2 ALLOW FILTERING"),
+                       row(1, 1, 2, 2L, 2),
+                       row(2, 1, 3, 2L, 3),
+                       row(4, 1, 5, 2L, 5));
+
+            assertRows(execute("SELECT a, b, d, count(b), max(d) FROM %s WHERE b IN (1, 2) and c IN (1, 2) GROUP BY a, b PER PARTITION LIMIT 1 ALLOW FILTERING"),
+                       row(1, 1, 2, 2L, 2),
+                       row(2, 1, 3, 2L, 3),
+                       row(4, 1, 5, 2L, 5));
+
+            // Range queries with PER PARTITION LIMIT and LIMIT
+            assertRows(execute("SELECT a, b, d, count(b), max(d) FROM %s WHERE b = 1 and c IN (1, 2) GROUP BY a, b PER PARTITION LIMIT 2 LIMIT 5 ALLOW FILTERING"),
+                       row(1, 1, 2, 2L, 2),
+                       row(2, 1, 3, 2L, 3),
+                       row(4, 1, 5, 2L, 5));
+
+            assertRows(execute("SELECT a, b, d, count(b), max(d) FROM %s WHERE b IN (1, 2) and c IN (1, 2) GROUP BY a, b PER PARTITION LIMIT 1 LIMIT 2 ALLOW FILTERING"),
+                       row(1, 1, 2, 2L, 2),
+                       row(2, 1, 3, 2L, 3));
+        }
+    }
+
+    @Test
+    public void testGroupByWithStaticColumnsWithoutPaging() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, s int static, d int, primary key (a, b, c))");
+
+        // ------------------------------------
+        // Test with non static columns empty
+        // ------------------------------------
+        execute("UPDATE %s SET s = 1 WHERE a = 1");
+        execute("UPDATE %s SET s = 2 WHERE a = 2");
+        execute("UPDATE %s SET s = 3 WHERE a = 4");
+
+        // Range queries
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a"),
+                   row(1, null, 1, 0L, 1L),
+                   row(2, null, 2, 0L, 1L),
+                   row(4, null, 3, 0L, 1L));
+
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a, b"),
+                   row(1, null, 1, 0L, 1L),
+                   row(2, null, 2, 0L, 1L),
+                   row(4, null, 3, 0L, 1L));
+
+        // Range query without aggregates
+        assertRows(execute("SELECT a, b, s FROM %s GROUP BY a, b"),
+                   row(1, null, 1),
+                   row(2, null, 2),
+                   row(4, null, 3));
+
+        // Range query with wildcard
+        assertRows(execute("SELECT * FROM %s GROUP BY a, b"),
+                   row(1, null, null, 1, null),
+                   row(2, null, null, 2, null),
+                   row(4, null, null, 3, null ));
+
+        // Range query with LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a, b LIMIT 2"),
+                   row(1, null, 1, 0L, 1L),
+                   row(2, null, 2, 0L, 1L));
+
+        // Range queries with PER PARTITION LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a PER PARTITION LIMIT 2"),
+                   row(1, null, 1, 0L, 1L),
+                   row(2, null, 2, 0L, 1L),
+                   row(4, null, 3, 0L, 1L));
+
+        // Range query with DISTINCT
+        assertRows(execute("SELECT DISTINCT a, s, count(s) FROM %s GROUP BY a"),
+                   row(1, 1, 1L),
+                   row(2, 2, 1L),
+                   row(4, 3, 1L));
+
+        // Range queries with DISTINCT and LIMIT
+        assertRows(execute("SELECT DISTINCT a, s, count(s) FROM %s GROUP BY a LIMIT 2"),
+                   row(1, 1, 1L),
+                   row(2, 2, 1L));
+
+        // Single partition queries
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 1 GROUP BY a"),
+                   row(1, null, 1, 0L, 1L));
+
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 1 GROUP BY a, b"),
+                   row(1, null, 1, 0L, 1L));
+
+        // Single partition query without aggregates
+        assertRows(execute("SELECT a, b, s FROM %s WHERE a = 1 GROUP BY a, b"),
+                   row(1, null, 1));
+
+        // Single partition query with wildcard
+        assertRows(execute("SELECT * FROM %s WHERE a = 1 GROUP BY a, b"),
+                   row(1, null, null, 1, null));
+
+        // Single partition query with LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 1 GROUP BY a, b LIMIT 2"),
+                   row(1, null, 1, 0L, 1L));
+
+        // Single partition query with PER PARTITION LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 1 GROUP BY a, b PER PARTITION LIMIT 2"),
+                   row(1, null, 1, 0L, 1L));
+
+        // Single partition query with DISTINCT
+        assertRows(execute("SELECT DISTINCT a, s, count(s) FROM %s WHERE a = 1 GROUP BY a"),
+                   row(1, 1, 1L));
+
+        // Multi-partitions queries
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a"),
+                   row(1, null, 1, 0L, 1L),
+                   row(2, null, 2, 0L, 1L),
+                   row(4, null, 3, 0L, 1L));
+
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b"),
+                   row(1, null, 1, 0L, 1L),
+                   row(2, null, 2, 0L, 1L),
+                   row(4, null, 3, 0L, 1L));
+
+        // Multi-partitions query without aggregates
+        assertRows(execute("SELECT a, b, s FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b"),
+                   row(1, null, 1),
+                   row(2, null, 2),
+                   row(4, null, 3));
+
+        // Multi-partitions query with wildcard
+        assertRows(execute("SELECT * FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b"),
+                   row(1, null, null, 1, null),
+                   row(2, null, null, 2, null),
+                   row(4, null, null, 3, null ));
+
+        // Multi-partitions query with LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b LIMIT 2"),
+                   row(1, null, 1, 0L, 1L),
+                   row(2, null, 2, 0L, 1L));
+
+        // Multi-partitions query with PER PARTITION LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b PER PARTITION LIMIT 2"),
+                   row(1, null, 1, 0L, 1L),
+                   row(2, null, 2, 0L, 1L),
+                   row(4, null, 3, 0L, 1L));
+
+        // Multi-partitions queries with DISTINCT
+        assertRows(execute("SELECT DISTINCT a, s, count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a"),
+                   row(1, 1, 1L),
+                   row(2, 2, 1L),
+                   row(4, 3, 1L));
+
+        // Multi-partitions with DISTINCT and LIMIT
+        assertRows(execute("SELECT DISTINCT a, s, count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a LIMIT 2"),
+                   row(1, 1, 1L),
+                   row(2, 2, 1L));
+
+        // ------------------------------------
+        // Test with some non static columns empty
+        // ------------------------------------
+        execute("UPDATE %s SET s = 3 WHERE a = 3");
+        execute("DELETE s FROM %s WHERE a = 4");
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 2, 1, 3)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 2, 2, 6)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 3, 2, 12)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 4, 2, 12)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 4, 3, 6)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (2, 2, 3, 3)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (2, 4, 3, 6)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (4, 8, 2, 12)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (5, 8, 2, 12)");
+
+        // Makes sure that we have some tombstones
+        execute("DELETE FROM %s WHERE a = 1 AND b = 3 AND c = 2");
+        execute("DELETE FROM %s WHERE a = 5");
+
+        // Range queries
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a"),
+                   row(1, 2, 1, 4L, 4L),
+                   row(2, 2, 2, 2L, 2L),
+                   row(4, 8, null, 1L, 0L),
+                   row(3, null, 3, 0L, 1L));
+
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a, b"),
+                   row(1, 2, 1, 2L, 2L),
+                   row(1, 4, 1, 2L, 2L),
+                   row(2, 2, 2, 1L, 1L),
+                   row(2, 4, 2, 1L, 1L),
+                   row(4, 8, null, 1L, 0L),
+                   row(3, null, 3, 0L, 1L));
+
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE b = 2 GROUP BY a, b ALLOW FILTERING"),
+                   row(1, 2, 1, 2L, 2L),
+                   row(2, 2, 2, 1L, 1L));
+
+        // Range queries without aggregates
+        assertRows(execute("SELECT a, b, s FROM %s GROUP BY a"),
+                   row(1, 2, 1),
+                   row(2, 2, 2),
+                   row(4, 8, null),
+                   row(3, null, 3));
+
+        assertRows(execute("SELECT a, b, s FROM %s GROUP BY a, b"),
+                   row(1, 2, 1),
+                   row(1, 4, 1),
+                   row(2, 2, 2),
+                   row(2, 4, 2),
+                   row(4, 8, null),
+                   row(3, null, 3));
+
+        // Range queries with wildcard
+        assertRows(execute("SELECT * FROM %s GROUP BY a"),
+                   row(1, 2, 1, 1, 3),
+                   row(2, 2, 3, 2, 3),
+                   row(4, 8, 2, null, 12),
+                   row(3, null, null, 3, null));
+
+        assertRows(execute("SELECT * FROM %s GROUP BY a, b"),
+                   row(1, 2, 1, 1, 3),
+                   row(1, 4, 2, 1, 12),
+                   row(2, 2, 3, 2, 3),
+                   row(2, 4, 3, 2, 6),
+                   row(4, 8, 2, null, 12),
+                   row(3, null, null, 3, null));
+
+        // Range query with LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a LIMIT 2"),
+                   row(1, 2, 1, 4L, 4L),
+                   row(2, 2, 2, 2L, 2L));
+
+        // Range query with PER PARTITION LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a, b PER PARTITION LIMIT 1"),
+                   row(1, 2, 1, 2L, 2L),
+                   row(2, 2, 2, 1L, 1L),
+                   row(4, 8, null, 1L, 0L),
+                   row(3, null, 3, 0L, 1L));
+
+        // Range query with PER PARTITION LIMIT and LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a, b PER PARTITION LIMIT 1 LIMIT 3"),
+                   row(1, 2, 1, 2L, 2L),
+                   row(2, 2, 2, 1L, 1L),
+                   row(4, 8, null, 1L, 0L));
+
+        // Range queries without aggregates and with LIMIT
+        assertRows(execute("SELECT a, b, s FROM %s GROUP BY a LIMIT 2"),
+                   row(1, 2, 1),
+                   row(2, 2, 2));
+
+        assertRows(execute("SELECT a, b, s FROM %s GROUP BY a, b LIMIT 10"),
+                   row(1, 2, 1),
+                   row(1, 4, 1),
+                   row(2, 2, 2),
+                   row(2, 4, 2),
+                   row(4, 8, null),
+                   row(3, null, 3));
+
+        // Range queries with wildcard and with LIMIT
+        assertRows(execute("SELECT * FROM %s GROUP BY a LIMIT 2"),
+                   row(1, 2, 1, 1, 3),
+                   row(2, 2, 3, 2, 3));
+
+        assertRows(execute("SELECT * FROM %s GROUP BY a, b LIMIT 10"),
+                   row(1, 2, 1, 1, 3),
+                   row(1, 4, 2, 1, 12),
+                   row(2, 2, 3, 2, 3),
+                   row(2, 4, 3, 2, 6),
+                   row(4, 8, 2, null, 12),
+                   row(3, null, null, 3, null));
+
+        // Range queries without aggregates and with PER PARTITION LIMIT
+        assertRows(execute("SELECT a, b, s FROM %s GROUP BY a, b PER PARTITION LIMIT 1"),
+                   row(1, 2, 1),
+                   row(2, 2, 2),
+                   row(4, 8, null),
+                   row(3, null, 3));
+
+        // Range queries with wildcard and with PER PARTITION LIMIT
+        assertRows(execute("SELECT * FROM %s GROUP BY a, b PER PARTITION LIMIT 1"),
+                   row(1, 2, 1, 1, 3),
+                   row(2, 2, 3, 2, 3),
+                   row(4, 8, 2, null, 12),
+                   row(3, null, null, 3, null));
+
+        // Range queries without aggregates, with PER PARTITION LIMIT and with LIMIT
+        assertRows(execute("SELECT a, b, s FROM %s GROUP BY a, b PER PARTITION LIMIT 1 LIMIT 2"),
+                   row(1, 2, 1),
+                   row(2, 2, 2));
+
+        // Range queries with wildcard, PER PARTITION LIMIT and LIMIT
+        assertRows(execute("SELECT * FROM %s GROUP BY a, b PER PARTITION LIMIT 1 LIMIT 2"),
+                   row(1, 2, 1, 1, 3),
+                   row(2, 2, 3, 2, 3));
+
+        // Range query with DISTINCT
+        assertRows(execute("SELECT DISTINCT a, s, count(a), count(s) FROM %s GROUP BY a"),
+                   row(1, 1, 1L, 1L),
+                   row(2, 2, 1L, 1L),
+                   row(4, null, 1L, 0L),
+                   row(3, 3, 1L, 1L));
+
+        // Range query with DISTINCT and LIMIT
+        assertRows(execute("SELECT DISTINCT a, s, count(a), count(s) FROM %s GROUP BY a LIMIT 2"),
+                   row(1, 1, 1L, 1L),
+                   row(2, 2, 1L, 1L));
+
+        // Range query with ORDER BY
+        assertInvalidMessage("ORDER BY is only supported when the partition key is restricted by an EQ or an IN",
+                             "SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a ORDER BY b DESC, c DESC");
+
+        // Single partition queries
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 1 GROUP BY a"),
+                   row(1, 2, 1, 4L, 4L));
+
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 3 GROUP BY a, b"),
+                   row(3, null, 3, 0L, 1L));
+
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 AND b = 2 GROUP BY a, b"),
+                   row(2, 2, 2, 1L, 1L));
+
+        // Single partition queries without aggregates
+        assertRows(execute("SELECT a, b, s FROM %s WHERE a = 1 GROUP BY a"),
+                   row(1, 2, 1));
+
+        assertRows(execute("SELECT a, b, s FROM %s WHERE a = 4 GROUP BY a, b"),
+                   row(4, 8, null));
+
+        // Single partition queries with wildcard
+        assertRows(execute("SELECT * FROM %s WHERE a = 1 GROUP BY a"),
+                   row(1, 2, 1, 1, 3));
+
+        assertRows(execute("SELECT * FROM %s WHERE a = 4 GROUP BY a, b"),
+                   row(4, 8, 2, null, 12));
+
+        // Single partition query with LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 GROUP BY a, b LIMIT 1"),
+                   row(2, 2, 2, 1L, 1L));
+
+        // Single partition query with PER PARTITION LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 GROUP BY a, b PER PARTITION LIMIT 1"),
+                   row(2, 2, 2, 1L, 1L));
+
+        // Single partition queries without aggregates and with LIMIT
+        assertRows(execute("SELECT a, b, s FROM %s WHERE a = 2 GROUP BY a, b LIMIT 1"),
+                   row(2, 2, 2));
+
+        assertRows(execute("SELECT a, b, s FROM %s WHERE a = 2 GROUP BY a, b LIMIT 2"),
+                   row(2, 2, 2),
+                   row(2, 4, 2));
+
+        // Single partition queries with DISTINCT
+        assertRows(execute("SELECT DISTINCT a, s, count(a), count(s) FROM %s WHERE a = 2 GROUP BY a"),
+                   row(2, 2, 1L, 1L));
+
+        assertRows(execute("SELECT DISTINCT a, s, count(a), count(s) FROM %s WHERE a = 4 GROUP BY a"),
+                   row(4, null, 1L, 0L));
+
+         // Single partition query with ORDER BY
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 GROUP BY a, b ORDER BY b DESC, c DESC"),
+                   row(2, 4, 2, 1L, 1L),
+                   row(2, 2, 2, 1L, 1L));
+
+         // Single partition queries with ORDER BY and LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 GROUP BY a, b ORDER BY b DESC, c DESC LIMIT 1"),
+                   row(2, 4, 2, 1L, 1L));
+
+        // Single partition queries with ORDER BY and PER PARTITION LIMIT
+       assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 GROUP BY a, b ORDER BY b DESC, c DESC PER PARTITION LIMIT 1"),
+                  row(2, 4, 2, 1L, 1L));
+
+        // Multi-partitions queries
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a"),
+                   row(1, 2, 1, 4L, 4L),
+                   row(2, 2, 2, 2L, 2L),
+                   row(3, null, 3, 0L, 1L),
+                   row(4, 8, null, 1L, 0L));
+
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b"),
+                   row(1, 2, 1, 2L, 2L),
+                   row(1, 4, 1, 2L, 2L),
+                   row(2, 2, 2, 1L, 1L),
+                   row(2, 4, 2, 1L, 1L),
+                   row(3, null, 3, 0L, 1L),
+                   row(4, 8, null, 1L, 0L));
+
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) AND b = 2 GROUP BY a, b"),
+                   row(1, 2, 1, 2L, 2L),
+                   row(2, 2, 2, 1L, 1L));
+
+        // Multi-partitions queries without aggregates
+        assertRows(execute("SELECT a, b, s FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a"),
+                   row(1, 2, 1),
+                   row(2, 2, 2),
+                   row(3, null, 3),
+                   row(4, 8, null));
+
+        assertRows(execute("SELECT a, b, s FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b"),
+                   row(1, 2, 1),
+                   row(1, 4, 1),
+                   row(2, 2, 2),
+                   row(2, 4, 2),
+                   row(3, null, 3),
+                   row(4, 8, null));
+
+        // Multi-partitions queries with wildcard
+        assertRows(execute("SELECT * FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a"),
+                   row(1, 2, 1, 1, 3),
+                   row(2, 2, 3, 2, 3),
+                   row(3, null, null, 3, null),
+                   row(4, 8, 2, null, 12));
+
+        assertRows(execute("SELECT * FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b"),
+                   row(1, 2, 1, 1, 3),
+                   row(1, 4, 2, 1, 12),
+                   row(2, 2, 3, 2, 3),
+                   row(2, 4, 3, 2, 6),
+                   row(3, null, null, 3, null),
+                   row(4, 8, 2, null, 12));
+
+        // Multi-partitions query with LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a LIMIT 2"),
+                   row(1, 2, 1, 4L, 4L),
+                   row(2, 2, 2, 2L, 2L));
+
+        // Multi-partitions query with PER PARTITION LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b PER PARTITION LIMIT 1"),
+                   row(1, 2, 1, 2L, 2L),
+                   row(2, 2, 2, 1L, 1L),
+                   row(3, null, 3, 0L, 1L),
+                   row(4, 8, null, 1L, 0L));
+
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b PER PARTITION LIMIT 2"),
+                   row(1, 2, 1, 2L, 2L),
+                   row(1, 4, 1, 2L, 2L),
+                   row(2, 2, 2, 1L, 1L),
+                   row(2, 4, 2, 1L, 1L),
+                   row(3, null, 3, 0L, 1L),
+                   row(4, 8, null, 1L, 0L));
+
+        // Multi-partitions queries with PER PARTITION LIMIT and LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b PER PARTITION LIMIT 1 LIMIT 3"),
+                   row(1, 2, 1, 2L, 2L),
+                   row(2, 2, 2, 1L, 1L),
+                   row(3, null, 3, 0L, 1L));
+
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b PER PARTITION LIMIT 4 LIMIT 3"),
+                   row(1, 2, 1, 2L, 2L),
+                   row(1, 4, 1, 2L, 2L),
+                   row(2, 2, 2, 1L, 1L));
+
+        // Multi-partitions queries without aggregates and with LIMIT
+        assertRows(execute("SELECT a, b, s FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a LIMIT 2"),
+                   row(1, 2, 1),
+                   row(2, 2, 2));
+
+        assertRows(execute("SELECT a, b, s FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b LIMIT 10"),
+                   row(1, 2, 1),
+                   row(1, 4, 1),
+                   row(2, 2, 2),
+                   row(2, 4, 2),
+                   row(3, null, 3),
+                   row(4, 8, null));
+
+        // Multi-partitions query with DISTINCT
+        assertRows(execute("SELECT DISTINCT a, s, count(a), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a"),
+                   row(1, 1, 1L, 1L),
+                   row(2, 2, 1L, 1L),
+                   row(3, 3, 1L, 1L),
+                   row(4, null, 1L, 0L));
+
+        // Multi-partitions query with DISTINCT and LIMIT
+        assertRows(execute("SELECT DISTINCT a, s, count(a), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a LIMIT 2"),
+                   row(1, 1, 1L, 1L),
+                   row(2, 2, 1L, 1L));
+
+         // Multi-partitions query with ORDER BY
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b ORDER BY b DESC, c DESC"),
+                   row(4, 8, null, 1L, 0L),
+                   row(1, 4, 1, 2L, 2L),
+                   row(2, 4, 2, 1L, 1L),
+                   row(2, 2, 2, 1L, 1L),
+                   row(1, 2, 1, 2L, 2L));
+
+         // Multi-partitions queries with ORDER BY and LIMIT
+        assertRows(execute("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b ORDER BY b DESC, c DESC LIMIT 2"),
+                   row(4, 8, null, 1L, 0L),
+                   row(1, 4, 1, 2L, 2L));
+    }
+
+    @Test
+    public void testGroupByWithPaging() throws Throwable
+    {
+        for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
+        {
+            createTable("CREATE TABLE %s (a int, b int, c int, d int, e int, primary key (a, b, c, d))"
+                    + compactOption);
+
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 1, 3, 6)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, 2, 6, 12)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 3, 2, 12, 24)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 4, 2, 12, 24)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 4, 2, 6, 12)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (2, 2, 3, 3, 6)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (2, 4, 3, 6, 12)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (3, 3, 2, 12, 24)");
+            execute("INSERT INTO %s (a, b, c, d, e) VALUES (4, 8, 2, 12, 24)");
+
+            // Makes sure that we have some tombstones
+            execute("DELETE FROM %s WHERE a = 1 AND b = 3 AND c = 2 AND d = 12");
+            execute("DELETE FROM %s WHERE a = 3");
+
+            for (int pageSize = 1; pageSize < 10; pageSize++)
+            {
+                // Range queries
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a", pageSize),
+                              row(1, 2, 6, 4L, 24),
+                              row(2, 2, 6, 2L, 12),
+                              row(4, 8, 24, 1L, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a, b", pageSize),
+                              row(1, 2, 6, 2L, 12),
+                              row(1, 4, 12, 2L, 24),
+                              row(2, 2, 6, 1L, 6),
+                              row(2, 4, 12, 1L, 12),
+                              row(4, 8, 24, 1L, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s", pageSize),
+                              row(1, 2, 6, 7L, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE b = 2 GROUP BY a, b ALLOW FILTERING",
+                                                   pageSize),
+                              row(1, 2, 6, 2L, 12),
+                              row(2, 2, 6, 1L, 6));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE b = 2 ALLOW FILTERING",
+                                                   pageSize),
+                              row(1, 2, 6, 3L, 12));
+
+                // Range queries without aggregates
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s GROUP BY a, b, c", pageSize),
+                              row(1, 2, 1, 3),
+                              row(1, 2, 2, 6),
+                              row(1, 4, 2, 6),
+                              row(2, 2, 3, 3),
+                              row(2, 4, 3, 6),
+                              row(4, 8, 2, 12));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s GROUP BY a, b", pageSize),
+                              row(1, 2, 1, 3),
+                              row(1, 4, 2, 6),
+                              row(2, 2, 3, 3),
+                              row(2, 4, 3, 6),
+                              row(4, 8, 2, 12));
+
+                // Range queries with wildcard
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a, b, c", pageSize),
+                              row(1, 2, 1, 3, 6),
+                              row(1, 2, 2, 6, 12),
+                              row(1, 4, 2, 6, 12),
+                              row(2, 2, 3, 3, 6),
+                              row(2, 4, 3, 6, 12),
+                              row(4, 8, 2, 12, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a, b", pageSize),
+                              row(1, 2, 1, 3, 6),
+                              row(1, 4, 2, 6, 12),
+                              row(2, 2, 3, 3, 6),
+                              row(2, 4, 3, 6, 12),
+                              row(4, 8, 2, 12, 24));
+
+                // Range query with LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a, b LIMIT 2",
+                                                   pageSize),
+                              row(1, 2, 6, 2L, 12),
+                              row(1, 4, 12, 2L, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s LIMIT 2",
+                                                   pageSize),
+                              row(1, 2, 6, 7L, 24));
+
+                // Range queries with PER PARTITION LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a, b PER PARTITION LIMIT 3", pageSize),
+                              row(1, 2, 6, 2L, 12),
+                              row(1, 4, 12, 2L, 24),
+                              row(2, 2, 6, 1L, 6),
+                              row(2, 4, 12, 1L, 12),
+                              row(4, 8, 24, 1L, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a, b PER PARTITION LIMIT 1", pageSize),
+                              row(1, 2, 6, 2L, 12),
+                              row(2, 2, 6, 1L, 6),
+                              row(4, 8, 24, 1L, 24));
+
+                // Range query with PER PARTITION LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s GROUP BY a, b PER PARTITION LIMIT 1 LIMIT 2", pageSize),
+                              row(1, 2, 6, 2L, 12),
+                              row(2, 2, 6, 1L, 6));
+
+                // Range query without aggregates and with PER PARTITION LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s GROUP BY a, b, c PER PARTITION LIMIT 2", pageSize),
+                              row(1, 2, 1, 3),
+                              row(1, 2, 2, 6),
+                              row(2, 2, 3, 3),
+                              row(2, 4, 3, 6),
+                              row(4, 8, 2, 12));
+
+                // Range queries with wildcard and with LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a, b, c LIMIT 3", pageSize),
+                              row(1, 2, 1, 3, 6),
+                              row(1, 2, 2, 6, 12),
+                              row(1, 4, 2, 6, 12));
+
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a, b LIMIT 3", pageSize),
+                              row(1, 2, 1, 3, 6),
+                              row(1, 4, 2, 6, 12),
+                              row(2, 2, 3, 3, 6));
+
+                // Range queries with wildcard and with PER PARTITION LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a, b, c PER PARTITION LIMIT 2", pageSize),
+                              row(1, 2, 1, 3, 6),
+                              row(1, 2, 2, 6, 12),
+                              row(2, 2, 3, 3, 6),
+                              row(2, 4, 3, 6, 12),
+                              row(4, 8, 2, 12, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a, b PER PARTITION LIMIT 1", pageSize),
+                              row(1, 2, 1, 3, 6),
+                              row(2, 2, 3, 3, 6),
+                              row(4, 8, 2, 12, 24));
+
+                // Range queries without aggregates and with LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s GROUP BY a, b, c LIMIT 3", pageSize),
+                              row(1, 2, 1, 3),
+                              row(1, 2, 2, 6),
+                              row(1, 4, 2, 6));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s GROUP BY a, b LIMIT 3", pageSize),
+                              row(1, 2, 1, 3),
+                              row(1, 4, 2, 6),
+                              row(2, 2, 3, 3));
+
+                // Range query without aggregates, with PER PARTITION LIMIT and with LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s GROUP BY a, b, c PER PARTITION LIMIT 2 LIMIT 3", pageSize),
+                              row(1, 2, 1, 3),
+                              row(1, 2, 2, 6),
+                              row(2, 2, 3, 3));
+
+                // Range queries with wildcard, with PER PARTITION LIMIT and LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a, b, c PER PARTITION LIMIT 2 LIMIT 3", pageSize),
+                              row(1, 2, 1, 3, 6),
+                              row(1, 2, 2, 6, 12),
+                              row(2, 2, 3, 3, 6));
+
+                // Range query with DISTINCT
+                assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, count(a)FROM %s GROUP BY a", pageSize),
+                              row(1, 1L),
+                              row(2, 1L),
+                              row(4, 1L));
+
+                assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, count(a)FROM %s", pageSize),
+                              row(1, 3L));
+
+                // Range query with DISTINCT and LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, count(a)FROM %s GROUP BY a LIMIT 2", pageSize),
+                              row(1, 1L),
+                              row(2, 1L));
+
+                assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, count(a)FROM %s LIMIT 2", pageSize),
+                              row(1, 3L));
+
+                // Range query with ORDER BY
+                assertInvalidMessage("ORDER BY is only supported when the partition key is restricted by an EQ or an IN",
+                                     "SELECT a, b, c, count(b), max(e) FROM %s GROUP BY a, b ORDER BY b DESC, c DESC");
+
+                // Single partition queries
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c",
+                                                   pageSize),
+                              row(1, 2, 6, 1L, 6),
+                              row(1, 2, 12, 1L, 12),
+                              row(1, 4, 12, 2L, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1", pageSize),
+                              row(1, 2, 6, 4L, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 AND b = 2 GROUP BY a, b, c",
+                                                   pageSize),
+                              row(1, 2, 6, 1L, 6),
+                              row(1, 2, 12, 1L, 12));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 AND b = 2",
+                                                   pageSize),
+                              row(1, 2, 6, 2L, 12));
+
+                // Single partition queries without aggregates
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b", pageSize),
+                              row(1, 2, 1, 3),
+                              row(1, 4, 2, 6));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b, c", pageSize),
+                              row(1, 2, 1, 3),
+                              row(1, 2, 2, 6),
+                              row(1, 4, 2, 6));
+
+                // Single partition queries with wildcard
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 1 GROUP BY a, b, c", pageSize),
+                           row(1, 2, 1, 3, 6),
+                           row(1, 2, 2, 6, 12),
+                           row(1, 4, 2, 6, 12));
+
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 1 GROUP BY a, b", pageSize),
+                           row(1, 2, 1, 3, 6),
+                           row(1, 4, 2, 6, 12));
+
+                // Single partition query with DISTINCT
+                assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, count(a)FROM %s WHERE a = 1 GROUP BY a",
+                                                   pageSize),
+                              row(1, 1L));
+
+                assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, count(a)FROM %s WHERE a = 1 GROUP BY a",
+                                                   pageSize),
+                              row(1, 1L));
+
+                // Single partition queries with LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c LIMIT 10",
+                                                   pageSize),
+                              row(1, 2, 6, 1L, 6),
+                              row(1, 2, 12, 1L, 12),
+                              row(1, 4, 12, 2L, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c LIMIT 2",
+                                                   pageSize),
+                              row(1, 2, 6, 1L, 6),
+                              row(1, 2, 12, 1L, 12));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 LIMIT 2",
+                                                   pageSize),
+                              row(1, 2, 6, 4L, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c LIMIT 1",
+                                                   pageSize),
+                              row(1L, 6));
+
+                // Single partition queries with wildcard and with LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 1 GROUP BY a, b, c LIMIT 2", pageSize),
+                           row(1, 2, 1, 3, 6),
+                           row(1, 2, 2, 6, 12));
+
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 1 GROUP BY a, b LIMIT 1", pageSize),
+                           row(1, 2, 1, 3, 6));
+
+                // Single partition queries with wildcard and with PER PARTITION LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 1 GROUP BY a, b, c PER PARTITION LIMIT 2", pageSize),
+                           row(1, 2, 1, 3, 6),
+                           row(1, 2, 2, 6, 12));
+
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 1 GROUP BY a, b PER PARTITION LIMIT 1", pageSize),
+                           row(1, 2, 1, 3, 6));
+
+                // Single partition query with PER PARTITION LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c PER PARTITION LIMIT 2",
+                                                   pageSize),
+                              row(1, 2, 6, 1L, 6),
+                              row(1, 2, 12, 1L, 12));
+
+                // Single partition queries without aggregates and with LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b LIMIT 2",
+                                                   pageSize),
+                              row(1, 2, 1, 3),
+                              row(1, 4, 2, 6));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b LIMIT 1",
+                                                   pageSize),
+                              row(1, 2, 1, 3));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s WHERE a = 1 GROUP BY a, b, c LIMIT 2",
+                                                   pageSize),
+                              row(1, 2, 1, 3),
+                              row(1, 2, 2, 6));
+
+                // Single partition queries with ORDER BY
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c ORDER BY b DESC, c DESC",
+                                                   pageSize),
+                              row(1, 4, 24, 2L, 24),
+                              row(1, 2, 12, 1L, 12),
+                              row(1, 2, 6, 1L, 6));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 ORDER BY b DESC, c DESC",
+                                                   pageSize),
+                              row(1, 4, 24, 4L, 24));
+
+                // Single partition queries with ORDER BY and LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c ORDER BY b DESC, c DESC LIMIT 2",
+                                                   pageSize),
+                              row(1, 4, 24, 2L, 24),
+                              row(1, 2, 12, 1L, 12));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 ORDER BY b DESC, c DESC LIMIT 2",
+                                                   pageSize),
+                              row(1, 4, 24, 4L, 24));
+
+                // Single partition queries with ORDER BY and PER PARTITION LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a = 1 GROUP BY a, b, c ORDER BY b DESC, c DESC PER PARTITION LIMIT 2",
+                                                   pageSize),
+                              row(1, 4, 24, 2L, 24),
+                              row(1, 2, 12, 1L, 12));
+
+                // Multi-partitions queries
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c",
+                                                   pageSize),
+                              row(1, 2, 6, 1L, 6),
+                              row(1, 2, 12, 1L, 12),
+                              row(1, 4, 12, 2L, 24),
+                              row(2, 2, 6, 1L, 6),
+                              row(2, 4, 12, 1L, 12),
+                              row(4, 8, 24, 1L, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a IN (1, 2, 4)",
+                                                   pageSize),
+                              row(1, 2, 6, 7L, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a IN (1, 2, 4) AND b = 2 GROUP BY a, b, c",
+                                                   pageSize),
+                              row(1, 2, 6, 1L, 6),
+                              row(1, 2, 12, 1L, 12),
+                              row(2, 2, 6, 1L, 6));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a IN (1, 2, 4) AND b = 2",
+                                                   pageSize),
+                              row(1, 2, 6, 3L, 12));
+
+                // Multi-partitions queries with PER PARTITION LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c PER PARTITION LIMIT 2",
+                                                   pageSize),
+                              row(1, 2, 6, 1L, 6),
+                              row(1, 2, 12, 1L, 12),
+                              row(2, 2, 6, 1L, 6),
+                              row(2, 4, 12, 1L, 12),
+                              row(4, 8, 24, 1L, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, e, count(b), max(e) FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c PER PARTITION LIMIT 1",
+                                                   pageSize),
+                              row(1, 2, 6, 1L, 6),
+                              row(2, 2, 6, 1L, 6),
+                              row(4, 8, 24, 1L, 24));
+
+                // Multi-partitions queries without aggregates
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b",
+                                                   pageSize),
+                              row(1, 2, 1, 3),
+                              row(1, 4, 2, 6),
+                              row(2, 2, 3, 3),
+                              row(2, 4, 3, 6),
+                              row(4, 8, 2, 12));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, c, d FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c",
+                                                   pageSize),
+                              row(1, 2, 1, 3),
+                              row(1, 2, 2, 6),
+                              row(1, 4, 2, 6),
+                              row(2, 2, 3, 3),
+                              row(2, 4, 3, 6),
+                              row(4, 8, 2, 12));
+
+                // Multi-partitions with wildcard
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b, c", pageSize),
+                           row(1, 2, 1, 3, 6),
+                           row(1, 2, 2, 6, 12),
+                           row(1, 4, 2, 6, 12),
+                           row(2, 2, 3, 3, 6),
+                           row(2, 4, 3, 6, 12),
+                           row(4, 8, 2, 12, 24));
+
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a IN (1, 2, 4) GROUP BY a, b", pageSize),
+                           row(1, 2, 1, 3, 6),
+                           row(1, 4, 2, 6, 12),
+                           row(2, 2, 3, 3, 6),
+                           row(2, 4, 3, 6, 12),
+                           row(4, 8, 2, 12, 24));
+
+                // Multi-partitions queries with DISTINCT
+                assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, count(a)FROM %s WHERE a IN (1, 2, 4) GROUP BY a",
+                                                   pageSize),
+                              row(1, 1L),
+                              row(2, 1L),
+                              row(4, 1L));
+
+                assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, count(a)FROM %s WHERE a IN (1, 2, 4)",
+                                                   pageSize),
+                              row(1, 3L));
+
+                // Multi-partitions query with DISTINCT and LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, count(a)FROM %s WHERE a IN (1, 2, 4) GROUP BY a LIMIT 2",
+                                                   pageSize),
+                              row(1, 1L),
+                              row(2, 1L));
+
+                assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, count(a)FROM %s WHERE a IN (1, 2, 4) LIMIT 2",
+                                                   pageSize),
+                              row(1, 3L));
+            }
+        }
+    }
+
+    @Test
+    public void testGroupByWithRangeNamesQueryWithPaging() throws Throwable
+    {
+        for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
+        {
+            createTable("CREATE TABLE %s (a int, b int, c int, d int, primary key (a, b, c))"
+                    + compactOption);
+
+            for (int i = 1; i < 5; i++)
+                for (int j = 1; j < 5; j++)
+                    for (int k = 1; k < 5; k++)
+                        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", i, j, k, i + j);
+
+            // Makes sure that we have some tombstones
+            execute("DELETE FROM %s WHERE a = 3");
+
+            for (int pageSize = 1; pageSize < 2; pageSize++)
+            {
+                // Range queries
+                assertRowsNet(executeNetWithPaging("SELECT a, b, d, count(b), max(d) FROM %s WHERE b = 1 and c IN (1, 2) GROUP BY a ALLOW FILTERING", pageSize),
+                              row(1, 1, 2, 2L, 2),
+                              row(2, 1, 3, 2L, 3),
+                              row(4, 1, 5, 2L, 5));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, d, count(b), max(d) FROM %s WHERE b = 1 and c IN (1, 2) GROUP BY a, b ALLOW FILTERING", pageSize),
+                              row(1, 1, 2, 2L, 2),
+                              row(2, 1, 3, 2L, 3),
+                              row(4, 1, 5, 2L, 5));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, d, count(b), max(d) FROM %s WHERE b IN (1, 2) and c IN (1, 2) GROUP BY a, b ALLOW FILTERING", pageSize),
+                              row(1, 1, 2, 2L, 2),
+                              row(1, 2, 3, 2L, 3),
+                              row(2, 1, 3, 2L, 3),
+                              row(2, 2, 4, 2L, 4),
+                              row(4, 1, 5, 2L, 5),
+                              row(4, 2, 6, 2L, 6));
+
+                // Range queries with LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, d, count(b), max(d) FROM %s WHERE b = 1 and c IN (1, 2) GROUP BY a LIMIT 5 ALLOW FILTERING", pageSize),
+                              row(1, 1, 2, 2L, 2),
+                              row(2, 1, 3, 2L, 3),
+                              row(4, 1, 5, 2L, 5));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, d, count(b), max(d) FROM %s WHERE b = 1 and c IN (1, 2) GROUP BY a, b LIMIT 3 ALLOW FILTERING", pageSize),
+                              row(1, 1, 2, 2L, 2),
+                              row(2, 1, 3, 2L, 3),
+                              row(4, 1, 5, 2L, 5));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, d, count(b), max(d) FROM %s WHERE b IN (1, 2) and c IN (1, 2) GROUP BY a, b LIMIT 3 ALLOW FILTERING", pageSize),
+                              row(1, 1, 2, 2L, 2),
+                              row(1, 2, 3, 2L, 3),
+                              row(2, 1, 3, 2L, 3));
+
+                // Range queries with PER PARTITION LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, d, count(b), max(d) FROM %s WHERE b = 1 and c IN (1, 2) GROUP BY a, b PER PARTITION LIMIT 2 ALLOW FILTERING", pageSize),
+                              row(1, 1, 2, 2L, 2),
+                              row(2, 1, 3, 2L, 3),
+                              row(4, 1, 5, 2L, 5));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, d, count(b), max(d) FROM %s WHERE b IN (1, 2) and c IN (1, 2) GROUP BY a, b PER PARTITION LIMIT 1 ALLOW FILTERING", pageSize),
+                              row(1, 1, 2, 2L, 2),
+                              row(2, 1, 3, 2L, 3),
+                              row(4, 1, 5, 2L, 5));
+
+                // Range queries with PER PARTITION LIMIT and LIMIT
+                assertRowsNet(executeNetWithPaging("SELECT a, b, d, count(b), max(d) FROM %s WHERE b = 1 and c IN (1, 2) GROUP BY a, b PER PARTITION LIMIT 2 LIMIT 5 ALLOW FILTERING", pageSize),
+                              row(1, 1, 2, 2L, 2),
+                              row(2, 1, 3, 2L, 3),
+                              row(4, 1, 5, 2L, 5));
+
+                assertRowsNet(executeNetWithPaging("SELECT a, b, d, count(b), max(d) FROM %s WHERE b IN (1, 2) and c IN (1, 2) GROUP BY a, b PER PARTITION LIMIT 1 LIMIT 2 ALLOW FILTERING", pageSize),
+                              row(1, 1, 2, 2L, 2),
+                              row(2, 1, 3, 2L, 3));
+            }
+        }
+    }
+
+    @Test
+    public void testGroupByWithStaticColumnsWithPaging() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, s int static, d int, primary key (a, b, c))");
+
+        // ------------------------------------
+        // Test with non static columns empty
+        // ------------------------------------
+        execute("UPDATE %s SET s = 1 WHERE a = 1");
+        execute("UPDATE %s SET s = 2 WHERE a = 2");
+        execute("UPDATE %s SET s = 3 WHERE a = 4");
+
+        for (int pageSize = 1; pageSize < 10; pageSize++)
+        {
+            // Range queries
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a", pageSize),
+                          row(1, null, 1, 0L, 1L),
+                          row(2, null, 2, 0L, 1L),
+                          row(4, null, 3, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a, b", pageSize),
+                          row(1, null, 1, 0L, 1L),
+                          row(2, null, 2, 0L, 1L),
+                          row(4, null, 3, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s", pageSize),
+                          row(1, null, 1, 0L, 3L));
+
+            // Range query without aggregates
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s GROUP BY a, b", pageSize),
+                          row(1, null, 1),
+                          row(2, null, 2),
+                          row(4, null, 3));
+
+            // Range query with wildcard
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a, b", pageSize),
+                       row(1, null, null, 1, null),
+                       row(2, null, null, 2, null),
+                       row(4, null, null, 3, null ));
+
+            // Range query with LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a, b LIMIT 2",
+                                               pageSize),
+                          row(1, null, 1, 0L, 1L),
+                          row(2, null, 2, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s LIMIT 2", pageSize),
+                          row(1, null, 1, 0L, 3L));
+
+            // Range query with PER PARTITION LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a PER PARTITION LIMIT 2", pageSize),
+                          row(1, null, 1, 0L, 1L),
+                          row(2, null, 2, 0L, 1L),
+                          row(4, null, 3, 0L, 1L));
+
+            // Range query with PER PARTITION LIMIT and LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a PER PARTITION LIMIT 2 LIMIT 2", pageSize),
+                          row(1, null, 1, 0L, 1L),
+                          row(2, null, 2, 0L, 1L));
+
+            // Range queries with DISTINCT
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(s) FROM %s GROUP BY a", pageSize),
+                          row(1, 1, 1L),
+                          row(2, 2, 1L),
+                          row(4, 3, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(s) FROM %s ", pageSize),
+                          row(1, 1, 3L));
+
+            // Range queries with DISTINCT and LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(s) FROM %s GROUP BY a LIMIT 2", pageSize),
+                          row(1, 1, 1L),
+                          row(2, 2, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(s) FROM %s LIMIT 2", pageSize),
+                          row(1, 1, 3L));
+
+            // Single partition queries
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 1 GROUP BY a",
+                                               pageSize),
+                          row(1, null, 1, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 1 GROUP BY a, b",
+                                               pageSize),
+                          row(1, null, 1, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 1", pageSize),
+                          row(1, null, 1, 0L, 1L));
+
+            // Single partition query without aggregates
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s WHERE a = 1 GROUP BY a, b", pageSize),
+                          row(1, null, 1));
+
+            // Single partition query with wildcard
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 1 GROUP BY a, b", pageSize),
+                       row(1, null, null, 1, null));
+
+            // Single partition queries with LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 1 GROUP BY a, b LIMIT 2",
+                                               pageSize),
+                          row(1, null, 1, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 1 LIMIT 2",
+                                               pageSize),
+                          row(1, null, 1, 0L, 1L));
+
+
+            // Single partition queries with DISTINCT
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(s) FROM %s WHERE a = 1 GROUP BY a",
+                                               pageSize),
+                          row(1, 1, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(s) FROM %s WHERE a = 1", pageSize),
+                          row(1, 1, 1L));
+
+            // Multi-partitions queries
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a",
+                                               pageSize),
+                          row(1, null, 1, 0L, 1L),
+                          row(2, null, 2, 0L, 1L),
+                          row(4, null, 3, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b",
+                                               pageSize),
+                          row(1, null, 1, 0L, 1L),
+                          row(2, null, 2, 0L, 1L),
+                          row(4, null, 3, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4)",
+                                               pageSize),
+                          row(1, null, 1, 0L, 3L));
+
+            // Multi-partitions query without aggregates
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b",
+                                               pageSize),
+                          row(1, null, 1),
+                          row(2, null, 2),
+                          row(4, null, 3));
+
+            // Multi-partitions query with LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b LIMIT 2",
+                                               pageSize),
+                          row(1, null, 1, 0L, 1L),
+                          row(2, null, 2, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) LIMIT 2",
+                                               pageSize),
+                          row(1, null, 1, 0L, 3L));
+
+            // Multi-partitions query with PER PARTITION LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a PER PARTITION LIMIT 2",
+                                               pageSize),
+                          row(1, null, 1, 0L, 1L),
+                          row(2, null, 2, 0L, 1L),
+                          row(4, null, 3, 0L, 1L));
+
+            // Multi-partitions query with PER PARTITION LIMIT and LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a PER PARTITION LIMIT 2 LIMIT 2",
+                                               pageSize),
+                          row(1, null, 1, 0L, 1L),
+                          row(2, null, 2, 0L, 1L));
+
+            // Multi-partitions queries with DISTINCT
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a",
+                                               pageSize),
+                          row(1, 1, 1L),
+                          row(2, 2, 1L),
+                          row(4, 3, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(s) FROM %s WHERE a IN (1, 2, 3, 4)",
+                                               pageSize),
+                          row(1, 1, 3L));
+
+            // Multi-partitions queries with DISTINCT and LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a LIMIT 2",
+                                               pageSize),
+                          row(1, 1, 1L),
+                          row(2, 2, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(s) FROM %s WHERE a IN (1, 2, 3, 4) LIMIT 2",
+                                               pageSize),
+                          row(1, 1, 3L));
+        }
+
+        // ------------------------------------
+        // Test with non static columns
+        // ------------------------------------
+        execute("UPDATE %s SET s = 3 WHERE a = 3");
+        execute("DELETE s FROM %s WHERE a = 4");
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 2, 1, 3)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 2, 2, 6)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 3, 2, 12)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 4, 2, 12)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 4, 3, 6)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (2, 2, 3, 3)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (2, 4, 3, 6)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (4, 8, 2, 12)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (5, 8, 2, 12)");
+
+        // Makes sure that we have some tombstones
+        execute("DELETE FROM %s WHERE a = 1 AND b = 3 AND c = 2");
+        execute("DELETE FROM %s WHERE a = 5");
+
+        for (int pageSize = 1; pageSize < 10; pageSize++)
+        {
+            // Range queries
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a", pageSize),
+                          row(1, 2, 1, 4L, 4L),
+                          row(2, 2, 2, 2L, 2L),
+                          row(4, 8, null, 1L, 0L),
+                          row(3, null, 3, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a, b", pageSize),
+                          row(1, 2, 1, 2L, 2L),
+                          row(1, 4, 1, 2L, 2L),
+                          row(2, 2, 2, 1L, 1L),
+                          row(2, 4, 2, 1L, 1L),
+                          row(4, 8, null, 1L, 0L),
+                          row(3, null, 3, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s", pageSize),
+                          row(1, 2, 1, 7L, 7L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE b = 2 GROUP BY a, b ALLOW FILTERING",
+                                               pageSize),
+                          row(1, 2, 1, 2L, 2L),
+                          row(2, 2, 2, 1L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE b = 2 ALLOW FILTERING",
+                                               pageSize),
+                          row(1, 2, 1, 3L, 3L));
+
+            // Range queries without aggregates
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s GROUP BY a", pageSize),
+                          row(1, 2, 1),
+                          row(2, 2, 2),
+                          row(4, 8, null),
+                          row(3, null, 3));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s GROUP BY a, b", pageSize),
+                          row(1, 2, 1),
+                          row(1, 4, 1),
+                          row(2, 2, 2),
+                          row(2, 4, 2),
+                          row(4, 8, null),
+                          row(3, null, 3));
+
+            // Range queries with wildcard
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a", pageSize),
+                       row(1, 2, 1, 1, 3),
+                       row(2, 2, 3, 2, 3),
+                       row(4, 8, 2, null, 12),
+                       row(3, null, null, 3, null));
+
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a, b", pageSize),
+                       row(1, 2, 1, 1, 3),
+                       row(1, 4, 2, 1, 12),
+                       row(2, 2, 3, 2, 3),
+                       row(2, 4, 3, 2, 6),
+                       row(4, 8, 2, null, 12),
+                       row(3, null, null, 3, null));
+
+            // Range query with LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a LIMIT 2",
+                                               pageSize),
+                          row(1, 2, 1, 4L, 4L),
+                          row(2, 2, 2, 2L, 2L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s LIMIT 2", pageSize),
+                          row(1, 2, 1, 7L, 7L));
+
+            // Range queries without aggregates and with LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s GROUP BY a LIMIT 2", pageSize),
+                          row(1, 2, 1),
+                          row(2, 2, 2));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s GROUP BY a, b LIMIT 10", pageSize),
+                          row(1, 2, 1),
+                          row(1, 4, 1),
+                          row(2, 2, 2),
+                          row(2, 4, 2),
+                          row(4, 8, null),
+                          row(3, null, 3));
+
+            // Range queries with wildcard and with LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a LIMIT 2", pageSize),
+                       row(1, 2, 1, 1, 3),
+                       row(2, 2, 3, 2, 3));
+
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a, b LIMIT 10", pageSize),
+                       row(1, 2, 1, 1, 3),
+                       row(1, 4, 2, 1, 12),
+                       row(2, 2, 3, 2, 3),
+                       row(2, 4, 3, 2, 6),
+                       row(4, 8, 2, null, 12),
+                       row(3, null, null, 3, null));
+
+            // Range queries with PER PARTITION LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a, b PER PARTITION LIMIT 2", pageSize),
+                          row(1, 2, 1, 2L, 2L),
+                          row(1, 4, 1, 2L, 2L),
+                          row(2, 2, 2, 1L, 1L),
+                          row(2, 4, 2, 1L, 1L),
+                          row(4, 8, null, 1L, 0L),
+                          row(3, null, 3, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a, b PER PARTITION LIMIT 1", pageSize),
+                          row(1, 2, 1, 2L, 2L),
+                          row(2, 2, 2, 1L, 1L),
+                          row(4, 8, null, 1L, 0L),
+                          row(3, null, 3, 0L, 1L));
+
+            // Range queries with wildcard and PER PARTITION LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a, b PER PARTITION LIMIT 1", pageSize),
+                       row(1, 2, 1, 1, 3),
+                       row(2, 2, 3, 2, 3),
+                       row(4, 8, 2, null, 12),
+                       row(3, null, null, 3, null));
+
+            // Range queries with PER PARTITION LIMIT and LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a, b PER PARTITION LIMIT 2 LIMIT 3", pageSize),
+                          row(1, 2, 1, 2L, 2L),
+                          row(1, 4, 1, 2L, 2L),
+                          row(2, 2, 2, 1L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s GROUP BY a, b PER PARTITION LIMIT 1 LIMIT 3", pageSize),
+                          row(1, 2, 1, 2L, 2L),
+                          row(2, 2, 2, 1L, 1L),
+                          row(4, 8, null, 1L, 0L));
+
+            // Range queries with wildcard, PER PARTITION LIMIT and LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s GROUP BY a, b PER PARTITION LIMIT 1 LIMIT 2", pageSize),
+                       row(1, 2, 1, 1, 3),
+                       row(2, 2, 3, 2, 3));
+
+            // Range query without aggregates and with PER PARTITION LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s GROUP BY a, b PER PARTITION LIMIT 1", pageSize),
+                          row(1, 2, 1),
+                          row(2, 2, 2),
+                          row(4, 8, null),
+                          row(3, null, 3));
+
+            // Range queries with DISTINCT
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(a), count(s) FROM %s GROUP BY a", pageSize),
+                          row(1, 1, 1L, 1L),
+                          row(2, 2, 1L, 1L),
+                          row(4, null, 1L, 0L),
+                          row(3, 3, 1L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(a), count(s) FROM %s", pageSize),
+                          row(1, 1, 4L, 3L));
+
+            // Range queries with DISTINCT and LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(a), count(s) FROM %s GROUP BY a LIMIT 2",
+                                               pageSize),
+                          row(1, 1, 1L, 1L),
+                          row(2, 2, 1L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(a), count(s) FROM %s LIMIT 2", pageSize),
+                          row(1, 1, 4L, 3L));
+
+            // Single partition queries
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 1 GROUP BY a",
+                                               pageSize),
+                          row(1, 2, 1, 4L, 4L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 3 GROUP BY a, b",
+                                               pageSize),
+                          row(3, null, 3, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 3",
+                                               pageSize),
+                          row(3, null, 3, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 AND b = 2 GROUP BY a, b",
+                                               pageSize),
+                          row(2, 2, 2, 1L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 AND b = 2",
+                                               pageSize),
+                          row(2, 2, 2, 1L, 1L));
+
+            // Single partition queries without aggregates
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s WHERE a = 1 GROUP BY a", pageSize),
+                          row(1, 2, 1));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s WHERE a = 4 GROUP BY a, b", pageSize),
+                          row(4, 8, null));
+
+            // Single partition queries with wildcard
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 1 GROUP BY a", pageSize),
+                       row(1, 2, 1, 1, 3));
+
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 4 GROUP BY a, b", pageSize),
+                       row(4, 8, 2, null, 12));
+
+            // Single partition queries with LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 GROUP BY a, b LIMIT 1",
+                                               pageSize),
+                          row(2, 2, 2, 1L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 LIMIT 1",
+                                               pageSize),
+                          row(2, 2, 2, 2L, 2L));
+
+            // Single partition queries without aggregates and with LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s WHERE a = 2 GROUP BY a, b LIMIT 1", pageSize),
+                          row(2, 2, 2));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s WHERE a = 2 GROUP BY a, b LIMIT 2", pageSize),
+                          row(2, 2, 2),
+                          row(2, 4, 2));
+
+            // Single partition queries with DISTINCT
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(a), count(s) FROM %s WHERE a = 2 GROUP BY a",
+                                               pageSize),
+                          row(2, 2, 1L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(a), count(s) FROM %s WHERE a = 4 GROUP BY a",
+                                               pageSize),
+                          row(4, null, 1L, 0L));
+
+            // Single partition queries with ORDER BY
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 GROUP BY a, b ORDER BY b DESC, c DESC",
+                                               pageSize),
+                          row(2, 4, 2, 1L, 1L),
+                          row(2, 2, 2, 1L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 ORDER BY b DESC, c DESC",
+                                               pageSize),
+                          row(2, 4, 2, 2L, 2L));
+
+            // Single partition queries with ORDER BY and LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 GROUP BY a, b ORDER BY b DESC, c DESC LIMIT 1",
+                                               pageSize),
+                          row(2, 4, 2, 1L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a = 2 ORDER BY b DESC, c DESC LIMIT 2",
+                                               pageSize),
+                          row(2, 4, 2, 2L, 2L));
+
+            // Multi-partitions queries
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a",
+                                               pageSize),
+                          row(1, 2, 1, 4L, 4L),
+                          row(2, 2, 2, 2L, 2L),
+                          row(3, null, 3, 0L, 1L),
+                          row(4, 8, null, 1L, 0L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b",
+                                               pageSize),
+                          row(1, 2, 1, 2L, 2L),
+                          row(1, 4, 1, 2L, 2L),
+                          row(2, 2, 2, 1L, 1L),
+                          row(2, 4, 2, 1L, 1L),
+                          row(3, null, 3, 0L, 1L),
+                          row(4, 8, null, 1L, 0L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4)",
+                                               pageSize),
+                          row(1, 2, 1, 7L, 7L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) AND b = 2 GROUP BY a, b",
+                                               pageSize),
+                          row(1, 2, 1, 2L, 2L),
+                          row(2, 2, 2, 1L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) AND b = 2",
+                                               pageSize),
+                          row(1, 2, 1, 3L, 3L));
+
+            // Multi-partitions queries without aggregates
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a", pageSize),
+                          row(1, 2, 1),
+                          row(2, 2, 2),
+                          row(3, null, 3),
+                          row(4, 8, null));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b",
+                                               pageSize),
+                          row(1, 2, 1),
+                          row(1, 4, 1),
+                          row(2, 2, 2),
+                          row(2, 4, 2),
+                          row(3, null, 3),
+                          row(4, 8, null));
+
+            // Multi-partitions queries with wildcard
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a", pageSize),
+                       row(1, 2, 1, 1, 3),
+                       row(2, 2, 3, 2, 3),
+                       row(3, null, null, 3, null),
+                       row(4, 8, 2, null, 12));
+
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b", pageSize),
+                       row(1, 2, 1, 1, 3),
+                       row(1, 4, 2, 1, 12),
+                       row(2, 2, 3, 2, 3),
+                       row(2, 4, 3, 2, 6),
+                       row(3, null, null, 3, null),
+                       row(4, 8, 2, null, 12));
+
+            // Multi-partitions queries with LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a LIMIT 2",
+                                               pageSize),
+                          row(1, 2, 1, 4L, 4L),
+                          row(2, 2, 2, 2L, 2L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) LIMIT 2",
+                                               pageSize),
+                          row(1, 2, 1, 7L, 7L));
+
+            // Multi-partitions queries without aggregates and with LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a LIMIT 2",
+                                               pageSize),
+                          row(1, 2, 1),
+                          row(2, 2, 2));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b LIMIT 10",
+                                               pageSize),
+                          row(1, 2, 1),
+                          row(1, 4, 1),
+                          row(2, 2, 2),
+                          row(2, 4, 2),
+                          row(3, null, 3),
+                          row(4, 8, null));
+
+            // Multi-partitions queries with PER PARTITION LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b PER PARTITION LIMIT 1",
+                                               pageSize),
+                          row(1, 2, 1, 2L, 2L),
+                          row(2, 2, 2, 1L, 1L),
+                          row(3, null, 3, 0L, 1L),
+                          row(4, 8, null, 1L, 0L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b PER PARTITION LIMIT 3",
+                                               pageSize),
+                          row(1, 2, 1, 2L, 2L),
+                          row(1, 4, 1, 2L, 2L),
+                          row(2, 2, 2, 1L, 1L),
+                          row(2, 4, 2, 1L, 1L),
+                          row(3, null, 3, 0L, 1L),
+                          row(4, 8, null, 1L, 0L));
+
+            // Multi-partitions queries with PER PARTITION LIMIT and LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b PER PARTITION LIMIT 1 LIMIT 3",
+                                               pageSize),
+                          row(1, 2, 1, 2L, 2L),
+                          row(2, 2, 2, 1L, 1L),
+                          row(3, null, 3, 0L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT a, b, s, count(b), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a, b PER PARTITION LIMIT 3 LIMIT 10",
+                                               pageSize),
+                          row(1, 2, 1, 2L, 2L),
+                          row(1, 4, 1, 2L, 2L),
+                          row(2, 2, 2, 1L, 1L),
+                          row(2, 4, 2, 1L, 1L),
+                          row(3, null, 3, 0L, 1L),
+                          row(4, 8, null, 1L, 0L));
+
+            // Multi-partitions queries with DISTINCT
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(a), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a",
+                                               pageSize),
+                          row(1, 1, 1L, 1L),
+                          row(2, 2, 1L, 1L),
+                          row(3, 3, 1L, 1L),
+                          row(4, null, 1L, 0L));
+
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(a), count(s) FROM %s WHERE a IN (1, 2, 3, 4)",
+                                               pageSize),
+                          row(1, 1, 4L, 3L));
+
+            // Multi-partitions query with DISTINCT and LIMIT
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(a), count(s) FROM %s WHERE a IN (1, 2, 3, 4) GROUP BY a LIMIT 2",
+                                               pageSize),
+                          row(1, 1, 1L, 1L),
+                          row(2, 2, 1L, 1L));
+
+            assertRowsNet(executeNetWithPaging("SELECT DISTINCT a, s, count(a), count(s) FROM %s WHERE a IN (1, 2, 3, 4) LIMIT 2",
+                                               pageSize),
+                          row(1, 1, 4L, 3L));
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectLimitTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectLimitTest.java
index 8ef4b58..5c45451 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectLimitTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectLimitTest.java
@@ -173,6 +173,274 @@
     }
 
     @Test
+    public void testPerPartitionLimit() throws Throwable
+    {
+        perPartitionLimitTest(false);
+    }
+
+    @Test
+    public void testPerPartitionLimitWithCompactStorage() throws Throwable
+    {
+        perPartitionLimitTest(true);
+    }
+
+    private void perPartitionLimitTest(boolean withCompactStorage) throws Throwable
+    {
+        String query = "CREATE TABLE %s (a int, b int, c int, PRIMARY KEY (a, b))";
+
+        if (withCompactStorage)
+            createTable(query + " WITH COMPACT STORAGE");
+        else
+            createTable(query);
+
+        for (int i = 0; i < 5; i++)
+        {
+            for (int j = 0; j < 5; j++)
+            {
+                execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", i, j, j);
+            }
+        }
+
+        assertInvalidMessage("LIMIT must be strictly positive",
+                             "SELECT * FROM %s PER PARTITION LIMIT ?", 0);
+        assertInvalidMessage("LIMIT must be strictly positive",
+                             "SELECT * FROM %s PER PARTITION LIMIT ?", -1);
+
+        assertRowsIgnoringOrder(execute("SELECT * FROM %s PER PARTITION LIMIT ?", 2),
+                                row(0, 0, 0),
+                                row(0, 1, 1),
+                                row(1, 0, 0),
+                                row(1, 1, 1),
+                                row(2, 0, 0),
+                                row(2, 1, 1),
+                                row(3, 0, 0),
+                                row(3, 1, 1),
+                                row(4, 0, 0),
+                                row(4, 1, 1));
+
+        // Combined Per Partition and "global" limit
+        assertRowCount(execute("SELECT * FROM %s PER PARTITION LIMIT ? LIMIT ?", 2, 6),
+                       6);
+
+        // odd amount of results
+        assertRowCount(execute("SELECT * FROM %s PER PARTITION LIMIT ? LIMIT ?", 2, 5),
+                       5);
+
+        // IN query
+        assertRows(execute("SELECT * FROM %s WHERE a IN (2,3) PER PARTITION LIMIT ?", 2),
+                   row(2, 0, 0),
+                   row(2, 1, 1),
+                   row(3, 0, 0),
+                   row(3, 1, 1));
+
+        assertRows(execute("SELECT * FROM %s WHERE a IN (2,3) PER PARTITION LIMIT ? LIMIT 3", 2),
+                   row(2, 0, 0),
+                   row(2, 1, 1),
+                   row(3, 0, 0));
+
+        assertRows(execute("SELECT * FROM %s WHERE a IN (1,2,3) PER PARTITION LIMIT ? LIMIT 3", 2),
+                   row(1, 0, 0),
+                   row(1, 1, 1),
+                   row(2, 0, 0));
+
+        // with restricted partition key
+        assertRows(execute("SELECT * FROM %s WHERE a = ? PER PARTITION LIMIT ?", 2, 3),
+                   row(2, 0, 0),
+                   row(2, 1, 1),
+                   row(2, 2, 2));
+
+        // with ordering
+        assertRows(execute("SELECT * FROM %s WHERE a IN (3, 2) ORDER BY b DESC PER PARTITION LIMIT ?", 2),
+                   row(2, 4, 4),
+                   row(3, 4, 4),
+                   row(2, 3, 3),
+                   row(3, 3, 3));
+
+        assertRows(execute("SELECT * FROM %s WHERE a IN (3, 2) ORDER BY b DESC PER PARTITION LIMIT ? LIMIT ?", 3, 4),
+                   row(2, 4, 4),
+                   row(3, 4, 4),
+                   row(2, 3, 3),
+                   row(3, 3, 3));
+
+        assertRows(execute("SELECT * FROM %s WHERE a = ? ORDER BY b DESC PER PARTITION LIMIT ?", 2, 3),
+                   row(2, 4, 4),
+                   row(2, 3, 3),
+                   row(2, 2, 2));
+
+        // with filtering
+        assertRows(execute("SELECT * FROM %s WHERE a = ? AND b > ? PER PARTITION LIMIT ? ALLOW FILTERING", 2, 0, 2),
+                   row(2, 1, 1),
+                   row(2, 2, 2));
+
+        assertRows(execute("SELECT * FROM %s WHERE a = ? AND b > ? ORDER BY b DESC PER PARTITION LIMIT ? ALLOW FILTERING", 2, 2, 2),
+                   row(2, 4, 4),
+                   row(2, 3, 3));
+
+        assertInvalidMessage("PER PARTITION LIMIT is not allowed with SELECT DISTINCT queries",
+                             "SELECT DISTINCT a FROM %s PER PARTITION LIMIT ?", 3);
+        assertInvalidMessage("PER PARTITION LIMIT is not allowed with SELECT DISTINCT queries",
+                             "SELECT DISTINCT a FROM %s PER PARTITION LIMIT ? LIMIT ?", 3, 4);
+        assertInvalidMessage("PER PARTITION LIMIT is not allowed with aggregate queries.",
+                             "SELECT COUNT(*) FROM %s PER PARTITION LIMIT ?", 3);
+    }
+
+    @Test
+    public void testPerPartitionLimitWithStaticDataAndPaging() throws Throwable
+    {
+        String query = "CREATE TABLE %s (a int, b int, s int static, c int, PRIMARY KEY (a, b))";
+
+        createTable(query);
+
+        for (int i = 0; i < 5; i++)
+        {
+            execute("INSERT INTO %s (a, s) VALUES (?, ?)", i, i);
+        }
+
+        for (int pageSize = 1; pageSize < 8; pageSize++)
+        {
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s PER PARTITION LIMIT 2", pageSize),
+                          row(0, null, 0, null),
+                          row(1, null, 1, null),
+                          row(2, null, 2, null),
+                          row(3, null, 3, null),
+                          row(4, null, 4, null));
+
+            // Combined Per Partition and "global" limit
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s PER PARTITION LIMIT 2 LIMIT 4", pageSize),
+                          row(0, null, 0, null),
+                          row(1, null, 1, null),
+                          row(2, null, 2, null),
+                          row(3, null, 3, null));
+
+            // odd amount of results
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s PER PARTITION LIMIT 2 LIMIT 3", pageSize),
+                          row(0, null, 0, null),
+                          row(1, null, 1, null),
+                          row(2, null, 2, null));
+
+            // IN query
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a IN (1,3,4) PER PARTITION LIMIT 2", pageSize),
+                          row(1, null, 1, null),
+                          row(3, null, 3, null),
+                          row(4, null, 4, null));
+
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a IN (1,3,4) PER PARTITION LIMIT 2 LIMIT 2",
+                                               pageSize),
+                          row(1, null, 1, null),
+                          row(3, null, 3, null));
+
+            // with restricted partition key
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 2 PER PARTITION LIMIT 3", pageSize),
+                          row(2, null, 2, null));
+
+            // with ordering
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 2 ORDER BY b DESC PER PARTITION LIMIT 3",
+                                               pageSize),
+                          row(2, null, 2, null));
+
+            // with filtering
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 2 AND s > 0 PER PARTITION LIMIT 2 ALLOW FILTERING",
+                                               pageSize),
+                          row(2, null, 2, null));
+        }
+
+        for (int i = 0; i < 5; i++)
+        {
+            if (i != 1)
+            {
+                for (int j = 0; j < 5; j++)
+                {
+                    execute("INSERT INTO %s (a, b, s, c) VALUES (?, ?, ?, ?)", i, j, i, j);
+                }
+            }
+        }
+
+        assertInvalidMessage("LIMIT must be strictly positive",
+                             "SELECT * FROM %s PER PARTITION LIMIT ?", 0);
+        assertInvalidMessage("LIMIT must be strictly positive",
+                             "SELECT * FROM %s PER PARTITION LIMIT ?", -1);
+
+        for (int pageSize = 1; pageSize < 8; pageSize++)
+        {
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s PER PARTITION LIMIT 2", pageSize),
+                          row(0, 0, 0, 0),
+                          row(0, 1, 0, 1),
+                          row(1, null, 1, null),
+                          row(2, 0, 2, 0),
+                          row(2, 1, 2, 1),
+                          row(3, 0, 3, 0),
+                          row(3, 1, 3, 1),
+                          row(4, 0, 4, 0),
+                          row(4, 1, 4, 1));
+
+            // Combined Per Partition and "global" limit
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s PER PARTITION LIMIT 2 LIMIT 4", pageSize),
+                          row(0, 0, 0, 0),
+                          row(0, 1, 0, 1),
+                          row(1, null, 1, null),
+                          row(2, 0, 2, 0));
+
+            // odd amount of results
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s PER PARTITION LIMIT 2 LIMIT 5", pageSize),
+                          row(0, 0, 0, 0),
+                          row(0, 1, 0, 1),
+                          row(1, null, 1, null),
+                          row(2, 0, 2, 0),
+                          row(2, 1, 2, 1));
+
+            // IN query
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a IN (2,3) PER PARTITION LIMIT 2", pageSize),
+                          row(2, 0, 2, 0),
+                          row(2, 1, 2, 1),
+                          row(3, 0, 3, 0),
+                          row(3, 1, 3, 1));
+
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a IN (2,3) PER PARTITION LIMIT 2 LIMIT 3",
+                                               pageSize),
+                          row(2, 0, 2, 0),
+                          row(2, 1, 2, 1),
+                          row(3, 0, 3, 0));
+
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a IN (1,2,3) PER PARTITION LIMIT 2 LIMIT 3",
+                                               pageSize),
+                          row(1, null, 1, null),
+                          row(2, 0, 2, 0),
+                          row(2, 1, 2, 1));
+
+            // with restricted partition key
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 2 PER PARTITION LIMIT 3", pageSize),
+                          row(2, 0, 2, 0),
+                          row(2, 1, 2, 1),
+                          row(2, 2, 2, 2));
+
+            // with ordering
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 2 ORDER BY b DESC PER PARTITION LIMIT 3",
+                                               pageSize),
+                          row(2, 4, 2, 4),
+                          row(2, 3, 2, 3),
+                          row(2, 2, 2, 2));
+
+            // with filtering
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 2 AND b > 0 PER PARTITION LIMIT 2 ALLOW FILTERING",
+                                               pageSize),
+                          row(2, 1, 2, 1),
+                          row(2, 2, 2, 2));
+
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE a = 2 AND b > 2 ORDER BY b DESC PER PARTITION LIMIT 2 ALLOW FILTERING",
+                                               pageSize),
+                          row(2, 4, 2, 4),
+                          row(2, 3, 2, 3));
+        }
+
+        assertInvalidMessage("PER PARTITION LIMIT is not allowed with SELECT DISTINCT queries",
+                             "SELECT DISTINCT a FROM %s PER PARTITION LIMIT ?", 3);
+        assertInvalidMessage("PER PARTITION LIMIT is not allowed with SELECT DISTINCT queries",
+                             "SELECT DISTINCT a FROM %s PER PARTITION LIMIT ? LIMIT ?", 3, 4);
+        assertInvalidMessage("PER PARTITION LIMIT is not allowed with aggregate queries.",
+                             "SELECT COUNT(*) FROM %s PER PARTITION LIMIT ?", 3);
+    }
+
+    @Test
     public void testLimitWithDeletedRowsAndStaticColumns() throws Throwable
     {
         createTable("CREATE TABLE %s (pk int, c int, v int, s int static, PRIMARY KEY (pk, c))");
@@ -208,7 +476,7 @@
     {
         // With only one clustering column
         createTable("CREATE TABLE %s (a int, b int, s int static, c int, primary key (a, b))"
-          + " WITH caching = {'keys': 'ALL', 'rows_per_partition' : 'ALL'}");
+             + " WITH caching = {'keys': 'ALL', 'rows_per_partition' : 'ALL'}");
 
         for (int i = 0; i < 4; i++)
         {
@@ -256,6 +524,14 @@
                 assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE b >= 1 AND b <= 1 LIMIT 2 ALLOW FILTERING", pageSize),
                               row(1, 1, 1, 2),
                               row(2, 1, 2, 3));
+
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE b = 1 GROUP BY a LIMIT 2 ALLOW FILTERING", pageSize),
+                              row(1, 1, 1, 2),
+                              row(2, 1, 2, 3));
+
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE b >= 1 AND b <= 1 GROUP BY a LIMIT 2 ALLOW FILTERING", pageSize),
+                              row(1, 1, 1, 2),
+                              row(2, 1, 2, 3));
             }
         });
 
@@ -349,6 +625,14 @@
                 assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE b IN (1, 2, 3, 4) AND c >= 1 AND c <= 1 LIMIT 2 ALLOW FILTERING", pageSize),
                               row(1, 1, 1, 1, 2),
                               row(2, 1, 1, 2, 3));
+
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE b = 1 GROUP BY a, b LIMIT 2 ALLOW FILTERING", pageSize),
+                              row(1, 1, 1, 1, 2),
+                              row(2, 1, 1, 2, 3));
+
+                assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE b IN (1, 2, 3, 4) AND c >= 1 AND c <= 1 GROUP BY a, b LIMIT 2 ALLOW FILTERING", pageSize),
+                              row(1, 1, 1, 1, 2),
+                              row(2, 1, 1, 2, 3));
             }
         });
 
@@ -407,12 +691,20 @@
                    row(1, 1, 9, 1),
                    row(4, 1, 9, 1));
 
+        assertRows(execute("SELECT * FROM %s WHERE v = ? GROUP BY pk LIMIT 2", 1),
+                   row(1, 1, 9, 1),
+                   row(4, 1, 9, 1));
+
         // Test with paging
         for (int pageSize = 1; pageSize < 4; pageSize++)
         {
             assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE v = 1 LIMIT 2", pageSize),
                           row(1, 1, 9, 1),
                           row(4, 1, 9, 1));
+
+            assertRowsNet(executeNetWithPaging("SELECT * FROM %s WHERE v = 1 GROUP BY pk LIMIT 2", pageSize),
+                          row(1, 1, 9, 1),
+                          row(4, 1, 9, 1));
         }
     }
 
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectMultiColumnRelationTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectMultiColumnRelationTest.java
index 6fec497..30c727b 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectMultiColumnRelationTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectMultiColumnRelationTest.java
@@ -80,8 +80,12 @@
                                  "SELECT * FROM %s WHERE a = 0 AND (c, d) > (?, ?)", 0, 0);
 
             // Nulls
-            assertInvalidMessage("Invalid null value in condition for columns: [b, c, d]",
+            assertInvalidMessage("Invalid null value for column d",
+                                 "SELECT * FROM %s WHERE a = 0 AND (b, c, d) = (?, ?, ?)", 1, 2, null);
+            assertInvalidMessage("Invalid null value for column d",
                                  "SELECT * FROM %s WHERE a = 0 AND (b, c, d) IN ((?, ?, ?))", 1, 2, null);
+            assertInvalidMessage("Invalid null value in condition for columns: [b, c, d]",
+                                 "SELECT * FROM %s WHERE a = 0 AND (b, c, d) IN ((?, ?, ?), (?, ?, ?))", 1, 2, null, 2, 1 ,4);
 
             // Wrong type for 'd'
             assertInvalid("SELECT * FROM %s WHERE a = 0 AND (b, c, d) = (?, ?, ?)", 1, 2, "foobar");
@@ -103,16 +107,16 @@
 
             assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
                                  "SELECT * FROM %s WHERE a = ? AND b > ?  AND (c, d) > (?, ?)", 0, 0, 0, 0);
-            assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
+            assertInvalidMessage("PRIMARY KEY column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
                                  "SELECT * FROM %s WHERE a = ? AND (c, d) > (?, ?) AND b > ?  ", 0, 0, 0, 0);
 
             assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column",
                                  "SELECT * FROM %s WHERE a = ? AND (b, c) > (?, ?) AND (b) < (?) AND (c) < (?)", 0, 0, 0, 0, 0);
             assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column",
                                  "SELECT * FROM %s WHERE a = ? AND (c) < (?) AND (b, c) > (?, ?) AND (b) < (?)", 0, 0, 0, 0, 0);
-            assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column",
+            assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
                                  "SELECT * FROM %s WHERE a = ? AND (b) < (?) AND (c) < (?) AND (b, c) > (?, ?)", 0, 0, 0, 0, 0);
-            assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column",
+            assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
                                  "SELECT * FROM %s WHERE a = ? AND (b) < (?) AND c < ? AND (b, c) > (?, ?)", 0, 0, 0, 0, 0);
 
             assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column",
@@ -885,6 +889,19 @@
     }
 
     @Test
+    public void testMultipleClusteringWithIndexAndValueOver64K() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b blob, c int, d int, PRIMARY KEY (a, b, c))");
+        createIndex("CREATE INDEX ON %s (b)");
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, ByteBufferUtil.bytes(1), 0, 0);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, ByteBufferUtil.bytes(2), 1, 0);
+
+        assertInvalidMessage("Index expression values may not be larger than 64K",
+                             "SELECT * FROM %s WHERE (b, c) = (?, ?) AND d = ?  ALLOW FILTERING", TOO_BIG, 1, 2);
+    }
+
+    @Test
     public void testMultiColumnRestrictionsWithIndex() throws Throwable
     {
         createTable("CREATE TABLE %s (a int, b int, c int, d int, e int, v int, PRIMARY KEY (a, b, c, d, e))");
@@ -912,19 +929,6 @@
     }
 
     @Test
-    public void testMultipleClusteringWithIndexAndValueOver64K() throws Throwable
-    {
-        createTable("CREATE TABLE %s (a int, b blob, c int, d int, PRIMARY KEY (a, b, c))");
-        createIndex("CREATE INDEX ON %s (b)");
-
-        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, ByteBufferUtil.bytes(1), 0, 0);
-        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, ByteBufferUtil.bytes(2), 1, 0);
-
-        assertInvalidMessage("Index expression values may not be larger than 64K",
-                             "SELECT * FROM %s WHERE (b, c) = (?, ?) AND d = ?  ALLOW FILTERING", TOO_BIG, 1, 2);
-    }
-
-    @Test
     public void testMultiplePartitionKeyAndMultiClusteringWithIndex() throws Throwable
     {
         createTable("CREATE TABLE %s (a int, b int, c int, d int, e int, f int, PRIMARY KEY ((a, b), c, d, e))");
@@ -954,17 +958,21 @@
                    row(0, 0, 1, 1, 0, 4),
                    row(0, 0, 1, 1, 1, 5));
 
-        assertInvalidMessage("Partition key parts: b must be restricted as other parts are",
-                             "SELECT * FROM %s WHERE a = ? AND (c, d) IN ((?, ?)) ALLOW FILTERING", 0, 1, 1);
+        assertRows(execute("SELECT * FROM %s WHERE a = ? AND (c, d) IN ((?, ?)) ALLOW FILTERING", 0, 1, 1),
+                row(0, 0, 1, 1, 0, 4),
+                row(0, 0, 1, 1, 1, 5));
 
-        assertInvalidMessage("Partition key parts: b must be restricted as other parts are",
-                             "SELECT * FROM %s WHERE a = ? AND (c, d) >= (?, ?) ALLOW FILTERING", 0, 1, 1);
+        assertRows(execute("SELECT * FROM %s WHERE a = ? AND (c, d) >= (?, ?) ALLOW FILTERING", 0, 1, 1),
+                row(0, 0, 1, 1, 0, 4),
+                row(0, 0, 1, 1, 1, 5),
+                row(0, 0, 2, 0, 0, 5));
 
         assertRows(execute("SELECT * FROM %s WHERE a = ? AND b = ? AND (c) IN ((?)) AND f = ?", 0, 0, 1, 5),
                    row(0, 0, 1, 1, 1, 5));
+
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE a = ? AND (c) IN ((?)) AND f = ?", 0, 1, 5);
-        assertRows(execute("SELECT * FROM %s WHERE a = ? AND (c) IN ((?)) AND f = ? ALLOW FILTERING", 0, 1, 5),
+                             "SELECT * FROM %s WHERE a = ? AND (c) IN ((?), (?)) AND f = ?", 0, 1, 3, 5);
+        assertRows(execute("SELECT * FROM %s WHERE a = ? AND (c) IN ((?), (?)) AND f = ? ALLOW FILTERING", 0, 1, 3, 5),
                    row(0, 0, 1, 1, 1, 5));
 
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
@@ -1961,11 +1969,11 @@
     public void testInvalidColumnNames() throws Throwable
     {
         createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY (a, b, c))");
-        assertInvalidMessage("Undefined name e in where clause ('(b, e) = (0, 0)')", "SELECT * FROM %s WHERE (b, e) = (0, 0)");
-        assertInvalidMessage("Undefined name e in where clause ('(b, e) IN ((0, 1), (2, 4))')", "SELECT * FROM %s WHERE (b, e) IN ((0, 1), (2, 4))");
-        assertInvalidMessage("Undefined name e in where clause ('(b, e) > (0, 1)')", "SELECT * FROM %s WHERE (b, e) > (0, 1) and b <= 2");
-        assertInvalidMessage("Aliases aren't allowed in the where clause ('(b, e) = (0, 0)')", "SELECT c AS e FROM %s WHERE (b, e) = (0, 0)");
-        assertInvalidMessage("Aliases aren't allowed in the where clause ('(b, e) IN ((0, 1), (2, 4))')", "SELECT c AS e FROM %s WHERE (b, e) IN ((0, 1), (2, 4))");
-        assertInvalidMessage("Aliases aren't allowed in the where clause ('(b, e) > (0, 1)')", "SELECT c AS e FROM %s WHERE (b, e) > (0, 1) and b <= 2");
+        assertInvalidMessage("Undefined column name e", "SELECT * FROM %s WHERE (b, e) = (0, 0)");
+        assertInvalidMessage("Undefined column name e", "SELECT * FROM %s WHERE (b, e) IN ((0, 1), (2, 4))");
+        assertInvalidMessage("Undefined column name e", "SELECT * FROM %s WHERE (b, e) > (0, 1) and b <= 2");
+        assertInvalidMessage("Undefined column name e", "SELECT c AS e FROM %s WHERE (b, e) = (0, 0)");
+        assertInvalidMessage("Undefined column name e", "SELECT c AS e FROM %s WHERE (b, e) IN ((0, 1), (2, 4))");
+        assertInvalidMessage("Undefined column name e", "SELECT c AS e FROM %s WHERE (b, e) > (0, 1) and b <= 2");
     }
  }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectOrderByTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectOrderByTest.java
index fc48928..06aa2fd 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectOrderByTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectOrderByTest.java
@@ -112,11 +112,11 @@
             beforeAndAfterFlush(() -> {
                 // order by a column not in the selection
                 assertRows(execute("SELECT c.a FROM %s WHERE a=? ORDER BY b ASC", 0),
-                        row(0), row(1), row(2));
-    
+                           row(0), row(1), row(2));
+
                 assertRows(execute("SELECT c.a FROM %s WHERE a=? ORDER BY b DESC", 0),
-                        row(2), row(1), row(0));
-    
+                           row(2), row(1), row(0));
+
                 assertRows(execute("SELECT blobAsInt(intAsBlob(c.a)) FROM %s WHERE a=? ORDER BY b DESC", 0),
                            row(2), row(1), row(0));
             });
@@ -369,34 +369,34 @@
         execute("INSERT INTO %s (pk1, pk2, c, v) VALUES (?, ?, ?, ?)", 1, 1, 4, "D");
 
         beforeAndAfterFlush(() -> {
-        assertRows(execute("SELECT v, ttl(v), c FROM %s where pk1 = ? AND pk2 IN (?, ?) ORDER BY c; ", 1, 1, 2),
-                   row("B", null, 1),
-                   row("A", null, 2),
-                   row("D", null, 4));
+            assertRows(execute("SELECT v, ttl(v), c FROM %s where pk1 = ? AND pk2 IN (?, ?) ORDER BY c; ", 1, 1, 2),
+                       row("B", null, 1),
+                       row("A", null, 2),
+                       row("D", null, 4));
 
-        assertRows(execute("SELECT v, ttl(v), c as name_1 FROM %s where pk1 = ? AND pk2 IN (?, ?) ORDER BY c; ", 1, 1, 2),
-                   row("B", null, 1),
-                   row("A", null, 2),
-                   row("D", null, 4));
+            assertRows(execute("SELECT v, ttl(v), c as name_1 FROM %s where pk1 = ? AND pk2 IN (?, ?) ORDER BY c; ", 1, 1, 2),
+                       row("B", null, 1),
+                       row("A", null, 2),
+                       row("D", null, 4));
 
-        assertRows(execute("SELECT v FROM %s where pk1 = ? AND pk2 IN (?, ?) ORDER BY c; ", 1, 1, 2),
-                   row("B"),
-                   row("A"),
-                   row("D"));
+            assertRows(execute("SELECT v FROM %s where pk1 = ? AND pk2 IN (?, ?) ORDER BY c; ", 1, 1, 2),
+                       row("B"),
+                       row("A"),
+                       row("D"));
 
-        assertRows(execute("SELECT v FROM %s where pk1 = ? AND pk2 IN (?, ?) ORDER BY c LIMIT 2; ", 1, 1, 2),
-                   row("B"),
-                   row("A"));
+            assertRows(execute("SELECT v FROM %s where pk1 = ? AND pk2 IN (?, ?) ORDER BY c LIMIT 2; ", 1, 1, 2),
+                       row("B"),
+                       row("A"));
 
-        assertRows(execute("SELECT v FROM %s where pk1 = ? AND pk2 IN (?, ?) ORDER BY c LIMIT 10; ", 1, 1, 2),
-                   row("B"),
-                   row("A"),
-                   row("D"));
+            assertRows(execute("SELECT v FROM %s where pk1 = ? AND pk2 IN (?, ?) ORDER BY c LIMIT 10; ", 1, 1, 2),
+                       row("B"),
+                       row("A"),
+                       row("D"));
 
-        assertRows(execute("SELECT v as c FROM %s where pk1 = ? AND pk2 IN (?, ?) ORDER BY c; ", 1, 1, 2),
-                   row("B"),
-                   row("A"),
-                   row("D"));
+            assertRows(execute("SELECT v as c FROM %s where pk1 = ? AND pk2 IN (?, ?) ORDER BY c; ", 1, 1, 2),
+                       row("B"),
+                       row("A"),
+                       row("D"));
         });
 
         createTable("CREATE TABLE %s (pk1 int, pk2 int, c1 int, c2 int, v text, PRIMARY KEY ((pk1, pk2), c1, c2) )");
@@ -561,11 +561,11 @@
     {
         createTable("CREATE TABLE %s (k text, c1 int, c2 int, PRIMARY KEY (k, c1, c2) ) WITH CLUSTERING ORDER BY (c1 ASC, c2 DESC)");
 
-        beforeAndAfterFlush(() -> {
-            for (int i = 0; i < 2; i++)
-                for (int j = 0; j < 2; j++)
-                    execute("INSERT INTO %s (k, c1, c2) VALUES ('foo', ?, ?)", i, j);
+        for (int i = 0; i < 2; i++)
+            for (int j = 0; j < 2; j++)
+                execute("INSERT INTO %s (k, c1, c2) VALUES ('foo', ?, ?)", i, j);
 
+        beforeAndAfterFlush(() -> {
             assertRows(execute("SELECT c1, c2 FROM %s WHERE k = 'foo'"),
                        row(0, 1), row(0, 0), row(1, 1), row(1, 0));
 
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectOrderedPartitionerTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectOrderedPartitionerTest.java
index 5e82020..003258a 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectOrderedPartitionerTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectOrderedPartitionerTest.java
@@ -27,8 +27,11 @@
 
 import static junit.framework.Assert.assertNull;
 import static org.junit.Assert.assertEquals;
+
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
 import org.apache.cassandra.dht.ByteOrderedPartitioner;
 
 /**
@@ -43,6 +46,107 @@
     }
 
     @Test
+    public void testTokenAndIndex() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY (a, b, c))");
+        createIndex("CREATE INDEX ON %s(c)");
+
+        for (int i = 0; i < 10; i++)
+        {
+            execute("INSERT INTO %s (a,b,c,d) VALUES (?, ?, ?, ?)", i, i, i, i);
+            execute("INSERT INTO %s (a,b,c,d) VALUES (?, ?, ?, ?)", i, i + 10, i + 10, i + 10);
+        }
+
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE token(a) > token(8) AND a = 9 AND c = 9 ALLOW FILTERING"),
+                       row(9, 9, 9, 9));
+
+            assertRows(execute("SELECT * FROM %s WHERE token(a) > token(8) AND a > 8 AND c = 9 ALLOW FILTERING"),
+                       row(9, 9, 9, 9));
+        });
+    }
+
+    @Test
+    public void testFilteringOnAllPartitionKeysWithTokenRestriction() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY ((a, b), c))");
+
+        for (int i = 0; i < 10; i++)
+        {
+            execute("INSERT INTO %s (a,b,c,d) VALUES (?, ?, ?, ?)", i, i, i, i);
+            execute("INSERT INTO %s (a,b,c,d) VALUES (?, ?, ?, ?)", i, i + 10, i + 10, i + 10);
+        }
+
+        assertEmpty(execute("SELECT * FROM %s WHERE token(a, b) > token(10, 10)"));
+        assertEmpty(execute("SELECT * FROM %s WHERE token(a, b) > token(10, 10) AND a < 8 AND b < 8 ALLOW FILTERING"));
+        assertRows(execute("SELECT * FROM %s WHERE token(a, b) > token(5, 5) AND a < 8 AND b < 8 ALLOW FILTERING"),
+                   row(6, 6, 6, 6),
+                   row(7, 7, 7, 7));
+    }
+
+    @Test
+    public void testFilteringOnPartitionKeyWithToken() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY ((a, b), c))");
+        createIndex("CREATE INDEX ON %s(d)");
+
+        for (int i = 0; i < 10; i++)
+        {
+            execute("INSERT INTO %s (a,b,c,d) VALUES (?, ?, ?, ?)", i, i, i, i);
+            execute("INSERT INTO %s (a,b,c,d) VALUES (?, ?, ?, ?)", i, i + 10, i + 10, i + 10);
+        }
+
+        beforeAndAfterFlush(() -> {
+            assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE token(a, b) > token(5, 10) AND b < 8 ALLOW FILTERING"),
+                                    row(6, 6, 6, 6),
+                                    row(7, 7, 7, 7));
+
+            assertRows(execute("SELECT * FROM %s WHERE token(a, b) > token(8, 10) AND a = 9 ALLOW FILTERING"),
+                       row(9, 9, 9, 9),
+                       row(9, 19, 19, 19));
+
+            assertRows(execute("SELECT * FROM %s WHERE token(a, b) > token(8, 10) AND a = 9 AND d = 9 ALLOW FILTERING"),
+                       row(9, 9, 9, 9));
+
+            assertRows(execute("SELECT * FROM %s WHERE token(a, b) > token(8, 10) AND a > 8 AND b > 8 AND d = 9 ALLOW FILTERING"),
+                       row(9, 9, 9, 9));
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 9 AND b = 9  AND token(a, b) > token(8, 10) AND d = 9 ALLOW FILTERING"),
+                       row(9, 9, 9, 9));
+
+            assertRows(execute("SELECT * FROM %s WHERE token(a, b) > token(8, 10) AND a = 9 AND c = 19 ALLOW FILTERING"),
+                       row(9, 19, 19, 19));
+
+            assertEmpty(execute("SELECT * FROM %s WHERE token(a, b) = token(8, 8) AND b = 9 ALLOW FILTERING"));
+        });
+    }
+
+    @Test
+    public void testTokenAndCollections() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a frozen<map<int, int>>, b int, c int, PRIMARY KEY (a, b))");
+
+        for (int i = 0; i < 10; i++)
+        {
+            execute("INSERT INTO %s (a,b,c) VALUES (?, ?, ?)", map(i, i), i, i);
+            execute("INSERT INTO %s (a,b,c) VALUES (?, ?, ?)", map(i, i, 100, 200), i + 10, i + 10);
+        }
+
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE token(a) > token({0: 0}) AND a CONTAINS KEY 9 ALLOW FILTERING"),
+                                    row(map(9, 9), 9, 9),
+                                    row(map(9, 9, 100, 200), 19, 19));
+
+            assertRows(execute("SELECT * FROM %s WHERE token(a) > token({0: 0}) AND a CONTAINS KEY 9 AND a CONTAINS 200 ALLOW FILTERING"),
+                       row(map(9, 9, 100, 200), 19, 19));
+
+            assertRows(execute("SELECT * FROM %s WHERE token(a) > token({0: 0}) AND a CONTAINS KEY 9 AND b = 19 ALLOW FILTERING"),
+                       row(map(9, 9, 100, 200), 19, 19));
+        });
+    }
+
+
+    @Test
     public void testTokenFunctionWithSingleColumnPartitionKey() throws Throwable
     {
         createTable("CREATE TABLE IF NOT EXISTS %s (a int PRIMARY KEY, b text)");
@@ -130,7 +234,7 @@
         assertRows(execute("SELECT * FROM %s WHERE token(a) < token(?) AND token(a) >= token(?) AND a IN (?, ?);",
                            1, 3, 1, 3),
                    row(3, 3));
-        assertInvalidMessage("Only EQ and IN relation are supported on the partition key (unless you use the token() function)",
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
                              "SELECT * FROM %s WHERE token(a) > token(?) AND token(a) <= token(?) AND a > ?;", 1, 3, 1);
 
         assertRows(execute("SELECT * FROM %s WHERE token(a) > token(?) AND token(a) <= token(?) AND a IN ?;",
@@ -206,7 +310,7 @@
         assertEmpty(execute("SELECT * FROM %s WHERE b IN (?, ?) AND token(a, b) = token(?, ?) AND a = ?;",
                             0, 1, 0, 0, 1));
 
-        assertInvalidMessage("Partition key parts: b must be restricted as other parts are",
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
                              "SELECT * FROM %s WHERE token(a, b) > token(?, ?) AND a = ?;", 0, 0, 1);
     }
 
@@ -503,9 +607,9 @@
     public void testTokenFunctionWithInvalidColumnNames() throws Throwable
     {
         createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY ((a, b), c))");
-        assertInvalidMessage("Undefined name e in where clause ('token(a, e) = token(0, 0)')", "SELECT * FROM %s WHERE token(a, e) = token(0, 0)");
-        assertInvalidMessage("Undefined name e in where clause ('token(a, e) > token(0, 1)')", "SELECT * FROM %s WHERE token(a, e) > token(0, 1)");
-        assertInvalidMessage("Aliases aren't allowed in the where clause ('token(a, e) = token(0, 0)')", "SELECT b AS e FROM %s WHERE token(a, e) = token(0, 0)");
-        assertInvalidMessage("Aliases aren't allowed in the where clause ('token(a, e) > token(0, 1)')", "SELECT b AS e FROM %s WHERE token(a, e) > token(0, 1)");
+        assertInvalidMessage("Undefined column name e", "SELECT * FROM %s WHERE token(a, e) = token(0, 0)");
+        assertInvalidMessage("Undefined column name e", "SELECT * FROM %s WHERE token(a, e) > token(0, 1)");
+        assertInvalidMessage("Undefined column name e", "SELECT b AS e FROM %s WHERE token(a, e) = token(0, 0)");
+        assertInvalidMessage("Undefined column name e", "SELECT b AS e FROM %s WHERE token(a, e) > token(0, 1)");
     }
 }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectSingleColumnRelationTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectSingleColumnRelationTest.java
index 4beb1fb..7e5afda 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectSingleColumnRelationTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectSingleColumnRelationTest.java
@@ -206,9 +206,9 @@
                    row("second", 1, 1, 1),
                    row("second", 4, 4, 4));
 
-        assertInvalidMessage("Partition key parts: b must be restricted as other parts are",
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
                              "select * from %s where a in (?, ?)", "first", "second");
-        assertInvalidMessage("Partition key parts: b must be restricted as other parts are",
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
                              "select * from %s where a = ?", "first");
         assertInvalidMessage("b cannot be restricted by more than one relation if it includes a IN",
                              "select * from %s where a = ? AND b IN (?, ?) AND b = ?", "first", 2, 2, 3);
@@ -498,15 +498,18 @@
                    row(0, 0, 1, 1, 0, 4),
                    row(0, 0, 1, 1, 1, 5));
 
-        assertInvalidMessage("Partition key parts: b must be restricted as other parts are",
-                             "SELECT * FROM %s WHERE a = ? AND c IN (?) AND  d IN (?) ALLOW FILTERING", 0, 1, 1);
+        assertRows(execute("SELECT * FROM %s WHERE a = ? AND c IN (?) AND  d IN (?) ALLOW FILTERING", 0, 1, 1),
+                row(0, 0, 1, 1, 0, 4),
+                row(0, 0, 1, 1, 1, 5));
 
-        assertInvalidMessage("Partition key parts: b must be restricted as other parts are",
-                             "SELECT * FROM %s WHERE a = ? AND (c, d) >= (?, ?) ALLOW FILTERING", 0, 1, 1);
+        assertRows(execute("SELECT * FROM %s WHERE a = ? AND (c, d) >= (?, ?) ALLOW FILTERING", 0, 1, 1),
+                row(0, 0, 1, 1, 0, 4),
+                row(0, 0, 1, 1, 1, 5),
+                row(0, 0, 2, 0, 0, 5));
 
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE a = ? AND c IN (?) AND f = ?", 0, 1, 5);
-        assertRows(execute("SELECT * FROM %s WHERE a = ? AND c IN (?) AND f = ? ALLOW FILTERING", 0, 1, 5),
+                             "SELECT * FROM %s WHERE a = ? AND c IN (?, ?) AND f = ?", 0, 0, 1, 5);
+        assertRows(execute("SELECT * FROM %s WHERE a = ? AND c IN (?, ?) AND f = ? ALLOW FILTERING", 0, 1, 3, 5),
                    row(0, 0, 1, 1, 1, 5));
 
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
@@ -516,12 +519,15 @@
                    row(0, 0, 2, 0, 0, 5));
 
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE a = ? AND c IN (?) AND d IN (?) AND f = ?", 0, 1, 0, 3);
-        assertRows(execute("SELECT * FROM %s WHERE a = ? AND c IN (?) AND d IN (?) AND f = ? ALLOW FILTERING", 0, 1, 0, 3),
+                             "SELECT * FROM %s WHERE a = ? AND c IN (?, ?) AND d IN (?) AND f = ?", 0, 1, 3, 0, 3);
+        assertRows(execute("SELECT * FROM %s WHERE a = ? AND c IN (?, ?) AND d IN (?) AND f = ? ALLOW FILTERING", 0, 1, 3, 0, 3),
                    row(0, 0, 1, 0, 0, 3));
 
-        assertInvalidMessage("Partition key parts: b must be restricted as other parts are",
-                             "SELECT * FROM %s WHERE a = ? AND c >= ? ALLOW FILTERING", 0, 1);
+        assertRows(execute("SELECT * FROM %s WHERE a = ? AND c >= ? ALLOW FILTERING", 0, 1),
+                row(0, 0, 1, 0, 0, 3),
+                row(0, 0, 1, 1, 0, 4),
+                row(0, 0, 1, 1, 1, 5),
+                row(0, 0, 2, 0, 0, 5));
 
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
                              "SELECT * FROM %s WHERE a = ? AND c >= ? AND f = ?", 0, 1, 5);
@@ -592,7 +598,7 @@
     public void testInvalidSliceRestrictionOnPartitionKey() throws Throwable
     {
         createTable("CREATE TABLE %s (a int PRIMARY KEY, b int, c text)");
-        assertInvalidMessage("Only EQ and IN relation are supported on the partition key (unless you use the token() function)",
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
                              "SELECT * FROM %s WHERE a >= 1 and a < 4");
         assertInvalidMessage("Multi-column relations can only be applied to clustering columns but was applied to: a",
                              "SELECT * FROM %s WHERE (a) >= (1) and (a) < (4)");
@@ -604,9 +610,9 @@
         createTable("CREATE TABLE %s (a int, b int, c text, PRIMARY KEY ((a, b)))");
         assertInvalidMessage("Multi-column relations can only be applied to clustering columns but was applied to: a",
                              "SELECT * FROM %s WHERE (a, b) >= (1, 1) and (a, b) < (4, 1)");
-        assertInvalidMessage("Only EQ and IN relation are supported on the partition key (unless you use the token() function)",
+        assertInvalidMessage("Multi-column relations can only be applied to clustering columns but was applied to: a",
                              "SELECT * FROM %s WHERE a >= 1 and (a, b) < (4, 1)");
-        assertInvalidMessage("Only EQ and IN relation are supported on the partition key (unless you use the token() function)",
+        assertInvalidMessage("Multi-column relations can only be applied to clustering columns but was applied to: a",
                              "SELECT * FROM %s WHERE b >= 1 and (a, b) < (4, 1)");
         assertInvalidMessage("Multi-column relations can only be applied to clustering columns but was applied to: a",
                              "SELECT * FROM %s WHERE (a, b) >= (1, 1) and (b) < (4)");
@@ -620,16 +626,40 @@
     public void testInvalidColumnNames() throws Throwable
     {
         createTable("CREATE TABLE %s (a int, b int, c map<int, int>, PRIMARY KEY (a, b))");
-        assertInvalidMessage("Undefined name d in where clause ('d = 0')", "SELECT * FROM %s WHERE d = 0");
-        assertInvalidMessage("Undefined name d in where clause ('d IN [0, 1]')", "SELECT * FROM %s WHERE d IN (0, 1)");
-        assertInvalidMessage("Undefined name d in where clause ('d > 0')", "SELECT * FROM %s WHERE d > 0 and d <= 2");
-        assertInvalidMessage("Undefined name d in where clause ('d CONTAINS 0')", "SELECT * FROM %s WHERE d CONTAINS 0");
-        assertInvalidMessage("Undefined name d in where clause ('d CONTAINS KEY 0')", "SELECT * FROM %s WHERE d CONTAINS KEY 0");
-        assertInvalidMessage("Aliases aren't allowed in the where clause ('d = 0')", "SELECT a AS d FROM %s WHERE d = 0");
-        assertInvalidMessage("Aliases aren't allowed in the where clause ('d IN [0, 1]')", "SELECT b AS d FROM %s WHERE d IN (0, 1)");
-        assertInvalidMessage("Aliases aren't allowed in the where clause ('d > 0')", "SELECT b AS d FROM %s WHERE d > 0 and d <= 2");
-        assertInvalidMessage("Aliases aren't allowed in the where clause ('d CONTAINS 0')", "SELECT c AS d FROM %s WHERE d CONTAINS 0");
-        assertInvalidMessage("Aliases aren't allowed in the where clause ('d CONTAINS KEY 0')", "SELECT c AS d FROM %s WHERE d CONTAINS KEY 0");
-        assertInvalidMessage("Undefined name d in selection clause", "SELECT d FROM %s WHERE a = 0");
+        assertInvalidMessage("Undefined column name d", "SELECT * FROM %s WHERE d = 0");
+        assertInvalidMessage("Undefined column name d", "SELECT * FROM %s WHERE d IN (0, 1)");
+        assertInvalidMessage("Undefined column name d", "SELECT * FROM %s WHERE d > 0 and d <= 2");
+        assertInvalidMessage("Undefined column name d", "SELECT * FROM %s WHERE d CONTAINS 0");
+        assertInvalidMessage("Undefined column name d", "SELECT * FROM %s WHERE d CONTAINS KEY 0");
+        assertInvalidMessage("Undefined column name d", "SELECT a AS d FROM %s WHERE d = 0");
+        assertInvalidMessage("Undefined column name d", "SELECT b AS d FROM %s WHERE d IN (0, 1)");
+        assertInvalidMessage("Undefined column name d", "SELECT b AS d FROM %s WHERE d > 0 and d <= 2");
+        assertInvalidMessage("Undefined column name d", "SELECT c AS d FROM %s WHERE d CONTAINS 0");
+        assertInvalidMessage("Undefined column name d", "SELECT c AS d FROM %s WHERE d CONTAINS KEY 0");
+        assertInvalidMessage("Undefined column name d", "SELECT d FROM %s WHERE a = 0");
+    }
+
+    @Test
+    public void testInvalidNonFrozenUDTRelation() throws Throwable
+    {
+        String type = createType("CREATE TYPE %s (a int)");
+        createTable("CREATE TABLE %s (a int PRIMARY KEY, b " + type + ")");
+        Object udt = userType("a", 1);
+
+        // All operators
+        String msg = "Non-frozen UDT column 'b' (" + type + ") cannot be restricted by any relation";
+        assertInvalidMessage(msg, "SELECT * FROM %s WHERE b = ?", udt);
+        assertInvalidMessage(msg, "SELECT * FROM %s WHERE b > ?", udt);
+        assertInvalidMessage(msg, "SELECT * FROM %s WHERE b < ?", udt);
+        assertInvalidMessage(msg, "SELECT * FROM %s WHERE b >= ?", udt);
+        assertInvalidMessage(msg, "SELECT * FROM %s WHERE b <= ?", udt);
+        assertInvalidMessage(msg, "SELECT * FROM %s WHERE b IN (?)", udt);
+        assertInvalidMessage(msg, "SELECT * FROM %s WHERE b LIKE ?", udt);
+        assertInvalidMessage("Unsupported \"!=\" relation: b != {a: 0}",
+                             "SELECT * FROM %s WHERE b != {a: 0}", udt);
+        assertInvalidMessage("Unsupported restriction: b IS NOT NULL",
+                             "SELECT * FROM %s WHERE b IS NOT NULL", udt);
+        assertInvalidMessage("Cannot use CONTAINS on non-collection column b",
+                             "SELECT * FROM %s WHERE b CONTAINS ?", udt);
     }
 }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java
index 469e8ca..1d45448 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java
@@ -21,18 +21,19 @@
 import java.util.UUID;
 
 import org.junit.Test;
+import org.junit.Assert;
 
 import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.Duration;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 
-import static org.apache.cassandra.utils.ByteBufferUtil.EMPTY_BYTE_BUFFER;
-import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.apache.cassandra.utils.ByteBufferUtil.EMPTY_BYTE_BUFFER;
+import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
 
-import junit.framework.Assert;
 
 /**
  * Test column ranges and ordering with static column in table
@@ -379,29 +380,31 @@
 
         execute("INSERT INTO %s (account, id , categories) VALUES (?, ?, ?)", "test", 5, set("lmn"));
 
-        assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "xyz", "lmn"));
+        beforeAndAfterFlush(() -> {
+            assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "xyz", "lmn"));
 
-        assertRows(execute("SELECT * FROM %s WHERE categories CONTAINS ?", "lmn"),
-                   row("test", 5, set("lmn"))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE categories CONTAINS ?", "lmn"),
+                       row("test", 5, set("lmn"))
+            );
 
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "test", "lmn"),
-                   row("test", 5, set("lmn"))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "test", "lmn"),
+                       row("test", 5, set("lmn"))
+            );
 
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, "lmn"),
-                   row("test", 5, set("lmn"))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, "lmn"),
+                       row("test", 5, set("lmn"))
+            );
 
-        assertInvalidMessage("Unsupported null value for column categories",
-                             "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, null);
+            assertInvalidMessage("Unsupported null value for column categories",
+                                 "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, null);
 
-        assertInvalidMessage("Unsupported unset value for column categories",
-                             "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, unset());
+            assertInvalidMessage("Unsupported unset value for column categories",
+                                 "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, unset());
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE account = ? AND categories CONTAINS ? AND categories CONTAINS ?", "xyz", "lmn", "notPresent");
-        assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ? AND categories CONTAINS ? ALLOW FILTERING", "xyz", "lmn", "notPresent"));
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE account = ? AND categories CONTAINS ? AND categories CONTAINS ?", "xyz", "lmn", "notPresent");
+            assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ? AND categories CONTAINS ? ALLOW FILTERING", "xyz", "lmn", "notPresent"));
+        });
     }
 
     @Test
@@ -411,32 +414,33 @@
         createIndex("CREATE INDEX ON %s(categories)");
 
         execute("INSERT INTO %s (account, id , categories) VALUES (?, ?, ?)", "test", 5, list("lmn"));
+        beforeAndAfterFlush(() -> {
+            assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "xyz", "lmn"));
 
-        assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "xyz", "lmn"));
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?;", "test", "lmn"),
+                       row("test", 5, list("lmn"))
+            );
 
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?;", "test", "lmn"),
-                   row("test", 5, list("lmn"))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE categories CONTAINS ?", "lmn"),
+                       row("test", 5, list("lmn"))
+            );
 
-        assertRows(execute("SELECT * FROM %s WHERE categories CONTAINS ?", "lmn"),
-                   row("test", 5, list("lmn"))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?;", "test", 5, "lmn"),
+                       row("test", 5, list("lmn"))
+            );
 
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?;", "test", 5, "lmn"),
-                   row("test", 5, list("lmn"))
-        );
+            assertInvalidMessage("Unsupported null value for column categories",
+                                 "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, null);
 
-        assertInvalidMessage("Unsupported null value for column categories",
-                             "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, null);
+            assertInvalidMessage("Unsupported unset value for column categories",
+                                 "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, unset());
 
-        assertInvalidMessage("Unsupported unset value for column categories",
-                             "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, unset());
-
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ? AND categories CONTAINS ?",
-                             "test", 5, "lmn", "notPresent");
-        assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ? AND categories CONTAINS ? ALLOW FILTERING",
-                            "test", 5, "lmn", "notPresent"));
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ? AND categories CONTAINS ?",
+                                 "test", 5, "lmn", "notPresent");
+            assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ? AND categories CONTAINS ? ALLOW FILTERING",
+                                "test", 5, "lmn", "notPresent"));
+        });
     }
 
     @Test
@@ -452,9 +456,12 @@
         {
             execute("INSERT INTO %s (e, f, s) VALUES (?, ?, ?)", i, list("Dubai"), 3);
         }
-        assertRows(execute("SELECT * FROM %s WHERE f CONTAINS ? AND s=? allow filtering", "Dubai", 3),
-                   row(4, list("Dubai"), 3),
-                   row(3, list("Dubai"), 3));
+
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE f CONTAINS ? AND s=? allow filtering", "Dubai", 3),
+                       row(4, list("Dubai"), 3),
+                       row(3, list("Dubai"), 3));
+        });
     }
 
     @Test
@@ -465,34 +472,36 @@
 
         execute("INSERT INTO %s (account, id , categories) VALUES (?, ?, ?)", "test", 5, map("lmn", "foo"));
 
-        assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS KEY ?", "xyz", "lmn"));
+        beforeAndAfterFlush(() -> {
+            assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS KEY ?", "xyz", "lmn"));
 
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS KEY ?", "test", "lmn"),
-                   row("test", 5, map("lmn", "foo"))
-        );
-        assertRows(execute("SELECT * FROM %s WHERE categories CONTAINS KEY ?", "lmn"),
-                   row("test", 5, map("lmn", "foo"))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS KEY ?", "test", "lmn"),
+                       row("test", 5, map("lmn", "foo"))
+            );
+            assertRows(execute("SELECT * FROM %s WHERE categories CONTAINS KEY ?", "lmn"),
+                       row("test", 5, map("lmn", "foo"))
+            );
 
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ?", "test", 5, "lmn"),
-                   row("test", 5, map("lmn", "foo"))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ?", "test", 5, "lmn"),
+                       row("test", 5, map("lmn", "foo"))
+            );
 
-        assertInvalidMessage("Unsupported null value for column categories",
-                             "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ?", "test", 5, null);
+            assertInvalidMessage("Unsupported null value for column categories",
+                                 "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ?", "test", 5, null);
 
-        assertInvalidMessage("Unsupported unset value for column categories",
-                             "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ?", "test", 5, unset());
+            assertInvalidMessage("Unsupported unset value for column categories",
+                                 "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ?", "test", 5, unset());
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ? AND categories CONTAINS KEY ?",
-                             "test", 5, "lmn", "notPresent");
-        assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ? AND categories CONTAINS KEY ? ALLOW FILTERING",
-                            "test", 5, "lmn", "notPresent"));
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ? AND categories CONTAINS KEY ?",
+                                 "test", 5, "lmn", "notPresent");
+            assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ? AND categories CONTAINS KEY ? ALLOW FILTERING",
+                                "test", 5, "lmn", "notPresent"));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ? AND categories CONTAINS ?",
-                             "test", 5, "lmn", "foo");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ? AND categories CONTAINS ?",
+                                 "test", 5, "lmn", "foo");
+        });
     }
 
     @Test
@@ -503,32 +512,34 @@
 
         execute("INSERT INTO %s (account, id , categories) VALUES (?, ?, ?)", "test", 5, map("lmn", "foo"));
 
-        assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "xyz", "foo"));
+        beforeAndAfterFlush(() -> {
+            assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "xyz", "foo"));
 
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "test", "foo"),
-                   row("test", 5, map("lmn", "foo"))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "test", "foo"),
+                       row("test", 5, map("lmn", "foo"))
+            );
 
-        assertRows(execute("SELECT * FROM %s WHERE categories CONTAINS ?", "foo"),
-                   row("test", 5, map("lmn", "foo"))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE categories CONTAINS ?", "foo"),
+                       row("test", 5, map("lmn", "foo"))
+            );
 
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, "foo"),
-                   row("test", 5, map("lmn", "foo"))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, "foo"),
+                       row("test", 5, map("lmn", "foo"))
+            );
 
-        assertInvalidMessage("Unsupported null value for column categories",
-                             "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, null);
+            assertInvalidMessage("Unsupported null value for column categories",
+                                 "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, null);
 
-        assertInvalidMessage("Unsupported unset value for column categories",
-                             "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, unset());
+            assertInvalidMessage("Unsupported unset value for column categories",
+                                 "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, unset());
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ? AND categories CONTAINS ?",
-                             "test", 5, "foo", "notPresent");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ? AND categories CONTAINS ?",
+                                 "test", 5, "foo", "notPresent");
 
-        assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ? AND categories CONTAINS ? ALLOW FILTERING",
-                            "test", 5, "foo", "notPresent"));
+            assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ? AND categories CONTAINS ? ALLOW FILTERING",
+                                "test", 5, "foo", "notPresent"));
+        });
     }
 
     // See CASSANDRA-7525
@@ -538,19 +549,19 @@
         createTable("CREATE TABLE %s (account text, id int, categories map<text,text>, PRIMARY KEY (account, id))");
 
         // create an index on
-        createIndex("CREATE INDEX id_index ON %s(id)");
-        createIndex("CREATE INDEX categories_values_index ON %s(categories)");
+        createIndex("CREATE INDEX ON %s(id)");
+        createIndex("CREATE INDEX ON %s(categories)");
 
-        execute("INSERT INTO %s (account, id , categories) VALUES (?, ?, ?)", "test", 5, map("lmn", "foo"));
+        beforeAndAfterFlush(() -> {
 
-        assertRows(execute("SELECT * FROM %s WHERE categories CONTAINS ? AND id = ? ALLOW FILTERING", "foo", 5),
-                   row("test", 5, map("lmn", "foo"))
-        );
+            execute("INSERT INTO %s (account, id , categories) VALUES (?, ?, ?)", "test", 5, map("lmn", "foo"));
 
-        assertRows(
-                  execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ? AND id = ? ALLOW FILTERING", "test", "foo", 5),
-                  row("test", 5, map("lmn", "foo"))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE categories CONTAINS ? AND id = ? ALLOW FILTERING", "foo", 5),
+                       row("test", 5, map("lmn", "foo")));
+
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ? AND id = ? ALLOW FILTERING", "test", "foo", 5),
+                       row("test", 5, map("lmn", "foo")));
+        });
     }
 
     // See CASSANDRA-8033
@@ -565,16 +576,18 @@
         execute("INSERT INTO %s (k1, k2, v) VALUES (?, ?, ?)", 1, 0, set(3, 4, 5));
         execute("INSERT INTO %s (k1, k2, v) VALUES (?, ?, ?)", 1, 1, set(4, 5, 6));
 
-        assertRows(execute("SELECT * FROM %s WHERE k2 = ?", 1),
-                   row(0, 1, set(2, 3, 4)),
-                   row(1, 1, set(4, 5, 6))
-        );
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE k2 = ?", 1),
+                       row(0, 1, set(2, 3, 4)),
+                       row(1, 1, set(4, 5, 6))
+            );
 
-        assertRows(execute("SELECT * FROM %s WHERE k2 = ? AND v CONTAINS ? ALLOW FILTERING", 1, 6),
-                   row(1, 1, set(4, 5, 6))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE k2 = ? AND v CONTAINS ? ALLOW FILTERING", 1, 6),
+                       row(1, 1, set(4, 5, 6))
+            );
 
-        assertEmpty(execute("SELECT * FROM %s WHERE k2 = ? AND v CONTAINS ? ALLOW FILTERING", 1, 7));
+            assertEmpty(execute("SELECT * FROM %s WHERE k2 = ? AND v CONTAINS ? ALLOW FILTERING", 1, 7));
+        });
     }
 
     // See CASSANDRA-8073
@@ -583,23 +596,26 @@
     {
         createTable("CREATE TABLE %s (a int, b int, c int, d set<int>, PRIMARY KEY (a, b, c))");
         createIndex("CREATE INDEX ON %s(d)");
+
         execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, 0, 0, set(1, 2, 3));
         execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, 0, 1, set(3, 4, 5));
         execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, 1, 0, set(1, 2, 3));
         execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 0, 1, 1, set(3, 4, 5));
 
-        assertRows(execute("SELECT * FROM %s WHERE a=? AND b=? AND d CONTAINS ?", 0, 1, 3),
-                   row(0, 1, 0, set(1, 2, 3)),
-                   row(0, 1, 1, set(3, 4, 5))
-        );
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE a=? AND b=? AND d CONTAINS ?", 0, 1, 3),
+                       row(0, 1, 0, set(1, 2, 3)),
+                       row(0, 1, 1, set(3, 4, 5))
+            );
 
-        assertRows(execute("SELECT * FROM %s WHERE a=? AND b=? AND d CONTAINS ?", 0, 1, 2),
-                   row(0, 1, 0, set(1, 2, 3))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE a=? AND b=? AND d CONTAINS ?", 0, 1, 2),
+                       row(0, 1, 0, set(1, 2, 3))
+            );
 
-        assertRows(execute("SELECT * FROM %s WHERE a=? AND b=? AND d CONTAINS ?", 0, 1, 5),
-                   row(0, 1, 1, set(3, 4, 5))
-        );
+            assertRows(execute("SELECT * FROM %s WHERE a=? AND b=? AND d CONTAINS ?", 0, 1, 5),
+                       row(0, 1, 1, set(3, 4, 5))
+            );
+        });
     }
 
     @Test
@@ -611,18 +627,20 @@
         execute("INSERT INTO %s (account, id , categories) VALUES (?, ?, ?)", "test", 5, map("lmn", "foo"));
         execute("INSERT INTO %s (account, id , categories) VALUES (?, ?, ?)", "test", 6, map("lmn", "foo2"));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "test", "foo");
+        beforeAndAfterFlush(() -> {
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "test", "foo");
 
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS KEY ?", "test", "lmn"),
-                   row("test", 5, map("lmn", "foo")),
-                   row("test", 6, map("lmn", "foo2")));
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS KEY ? AND categories CONTAINS ? ALLOW FILTERING",
-                           "test", "lmn", "foo"),
-                   row("test", 5, map("lmn", "foo")));
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ? AND categories CONTAINS KEY ? ALLOW FILTERING",
-                           "test", "foo", "lmn"),
-                   row("test", 5, map("lmn", "foo")));
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS KEY ?", "test", "lmn"),
+                       row("test", 5, map("lmn", "foo")),
+                       row("test", 6, map("lmn", "foo2")));
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS KEY ? AND categories CONTAINS ? ALLOW FILTERING",
+                               "test", "lmn", "foo"),
+                       row("test", 5, map("lmn", "foo")));
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ? AND categories CONTAINS KEY ? ALLOW FILTERING",
+                               "test", "foo", "lmn"),
+                       row("test", 5, map("lmn", "foo")));
+        });
     }
 
     @Test
@@ -634,18 +652,20 @@
         execute("INSERT INTO %s (account, id , categories) VALUES (?, ?, ?)", "test", 5, map("lmn", "foo"));
         execute("INSERT INTO %s (account, id , categories) VALUES (?, ?, ?)", "test", 6, map("lmn2", "foo"));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE account = ? AND categories CONTAINS KEY ?", "test", "lmn");
+        beforeAndAfterFlush(() -> {
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE account = ? AND categories CONTAINS KEY ?", "test", "lmn");
 
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "test", "foo"),
-                   row("test", 5, map("lmn", "foo")),
-                   row("test", 6, map("lmn2", "foo")));
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS KEY ? AND categories CONTAINS ? ALLOW FILTERING",
-                           "test", "lmn", "foo"),
-                   row("test", 5, map("lmn", "foo")));
-        assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ? AND categories CONTAINS KEY ? ALLOW FILTERING",
-                           "test", "foo", "lmn"),
-                   row("test", 5, map("lmn", "foo")));
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ?", "test", "foo"),
+                       row("test", 5, map("lmn", "foo")),
+                       row("test", 6, map("lmn2", "foo")));
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS KEY ? AND categories CONTAINS ? ALLOW FILTERING",
+                               "test", "lmn", "foo"),
+                       row("test", 5, map("lmn", "foo")));
+            assertRows(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ? AND categories CONTAINS KEY ? ALLOW FILTERING",
+                               "test", "foo", "lmn"),
+                       row("test", 5, map("lmn", "foo")));
+        });
     }
 
     /**
@@ -1161,15 +1181,16 @@
         assertEquals(ByteBuffer.wrap(new byte[4]), rs.one().getBlob(rs.metadata().get(0).name.toString()));
 
         // test that select throws a meaningful exception for aliases in where clause
-        assertInvalidMessage("Aliases aren't allowed in the where clause",
+        assertInvalidMessage("Undefined column name user_id",
                              "SELECT id AS user_id, name AS user_name FROM %s WHERE user_id = 0");
 
         // test that select throws a meaningful exception for aliases in order by clause
-        assertInvalidMessage("Aliases are not allowed in order by clause",
+        assertInvalidMessage("Undefined column name user_name",
                              "SELECT id AS user_id, name AS user_name FROM %s WHERE id IN (0) ORDER BY user_name");
     }
 
     /**
+>>>>>>> cassandra-3.0
      * Migrated from cql_tests.py:TestCQL.bug_6327_test()
      */
     @Test
@@ -1193,7 +1214,7 @@
     public void testSelectCountPaging() throws Throwable
     {
         createTable("create table %s (field1 text, field2 timeuuid, field3 boolean, primary key(field1, field2))");
-        createIndex("create index test_index on %s (field3)");
+        createIndex("create index on %s (field3)");
 
         execute("insert into %s (field1, field2, field3) values ('hola', now(), false)");
         execute("insert into %s (field1, field2, field3) values ('hola', now(), false)");
@@ -1245,16 +1266,45 @@
         for (int i = 0; i < 5; i++)
             execute("INSERT INTO %s (id, name) VALUES (?, ?) USING TTL 10 AND TIMESTAMP 0", i, Integer.toString(i));
 
-        assertInvalidMessage("Aliases aren't allowed in the where clause",
+        assertInvalidMessage("Undefined column name user_id",
                              "SELECT id AS user_id, name AS user_name FROM %s WHERE user_id = 0");
 
         // test that select throws a meaningful exception for aliases in order by clause
-        assertInvalidMessage("Aliases are not allowed in order by clause",
+        assertInvalidMessage("Undefined column name user_name",
                              "SELECT id AS user_id, name AS user_name FROM %s WHERE id IN (0) ORDER BY user_name");
 
     }
 
     @Test
+    public void testAllowFilteringOnPartitionKeyOnStaticColumnsWithRowsWithOnlyStaticValues() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, s int static, c int, d int, primary key (a, b))");
+
+        for (int i = 0; i < 5; i++)
+        {
+            execute("INSERT INTO %s (a, s) VALUES (?, ?)", i, i);
+            if (i != 2)
+                for (int j = 0; j < 4; j++)
+                    execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", i, j, j, i + j);
+        }
+
+        beforeAndAfterFlush(() -> {
+            assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE a >= 1 AND c = 2 AND s >= 1 ALLOW FILTERING"),
+                                    row(1, 2, 1, 2, 3),
+                                    row(3, 2, 3, 2, 5),
+                                    row(4, 2, 4, 2, 6));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND c = 2 AND s >= 1 LIMIT 2 ALLOW FILTERING"),
+                       row(1, 2, 1, 2, 3),
+                       row(4, 2, 4, 2, 6));
+
+            assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE a >= 3 AND c = 2 AND s >= 1 LIMIT 2 ALLOW FILTERING"),
+                                    row(4, 2, 4, 2, 6),
+                                    row(3, 2, 3, 2, 5));
+        });
+    }
+
+    @Test
     public void testFilteringOnStaticColumnsWithRowsWithOnlyStaticValues() throws Throwable
     {
         createTable("CREATE TABLE %s (a int, b int, s int static, c int, d int, primary key (a, b))");
@@ -1267,9 +1317,11 @@
                     execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", i, j, j, i + j);
         }
 
-        assertRows(execute("SELECT * FROM %s WHERE c = 2 AND s >= 1 LIMIT 2 ALLOW FILTERING"),
-                   row(1, 2, 1, 2, 3),
-                   row(4, 2, 4, 2, 6));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE c = 2 AND s >= 1 LIMIT 2 ALLOW FILTERING"),
+                       row(1, 2, 1, 2, 3),
+                       row(4, 2, 4, 2, 6));
+        });
     }
 
     @Test
@@ -1291,65 +1343,66 @@
         execute("DELETE FROM %s WHERE a = 1 AND b = 1");
         execute("DELETE FROM %s WHERE a = 2 AND b = 2");
 
-        flush();
+        beforeAndAfterFlush(() -> {
+            
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c = 4 AND d = 8");
 
-        // Checks filtering
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c = 4 AND d = 8");
+            assertRows(execute("SELECT * FROM %s WHERE c = 4 AND d = 8 ALLOW FILTERING"),
+                       row(1, 2, 1, 4, 8),
+                       row(1, 4, 1, 4, 8));
 
-        assertRows(execute("SELECT * FROM %s WHERE c = 4 AND d = 8 ALLOW FILTERING"),
-                   row(1, 2, 1, 4, 8),
-                   row(1, 4, 1, 4, 8));
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a = 1 AND b = 4 AND d = 8");
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE a = 1 AND b = 4 AND d = 8");
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 4 AND d = 8 ALLOW FILTERING"),
+                       row(1, 4, 1, 4, 8));
 
-        assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 4 AND d = 8 ALLOW FILTERING"),
-                   row(1, 4, 1, 4, 8));
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE s = 1 AND d = 12");
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE s = 1 AND d = 12");
+            assertRows(execute("SELECT * FROM %s WHERE s = 1 AND d = 12 ALLOW FILTERING"),
+                       row(1, 3, 1, 6, 12));
 
-        assertRows(execute("SELECT * FROM %s WHERE s = 1 AND d = 12 ALLOW FILTERING"),
-                   row(1, 3, 1, 6, 12));
+            assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
+                                 "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7)");
 
-        assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
-                             "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7)");
+            assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
+                                 "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7) ALLOW FILTERING");
 
-        assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
-                             "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7) ALLOW FILTERING");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c > 4");
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c > 4");
+            assertRows(execute("SELECT * FROM %s WHERE c > 4 ALLOW FILTERING"),
+                       row(1, 3, 1, 6, 12),
+                       row(2, 3, 2, 7, 12));
 
-        assertRows(execute("SELECT * FROM %s WHERE c > 4 ALLOW FILTERING"),
-                   row(1, 3, 1, 6, 12),
-                   row(2, 3, 2, 7, 12));
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE s > 1");
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                "SELECT * FROM %s WHERE s > 1");
+            assertRows(execute("SELECT * FROM %s WHERE s > 1 ALLOW FILTERING"),
+                       row(2, 3, 2, 7, 12),
+                       row(3, null, 3, null, null));
 
-        assertRows(execute("SELECT * FROM %s WHERE s > 1 ALLOW FILTERING"),
-                   row(2, 3, 2, 7, 12),
-                   row(3, null, 3, null, null));
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE b < 3 AND c <= 4");
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE b < 3 AND c <= 4");
+            assertRows(execute("SELECT * FROM %s WHERE b < 3 AND c <= 4 ALLOW FILTERING"),
+                       row(1, 2, 1, 4, 8));
 
-        assertRows(execute("SELECT * FROM %s WHERE b < 3 AND c <= 4 ALLOW FILTERING"),
-                   row(1, 2, 1, 4, 8));
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c >= 3 AND c <= 6");
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c >= 3 AND c <= 6");
+            assertRows(execute("SELECT * FROM %s WHERE c >= 3 AND c <= 6 ALLOW FILTERING"),
+                       row(1, 2, 1, 4, 8),
+                       row(1, 3, 1, 6, 12),
+                       row(1, 4, 1, 4, 8));
 
-        assertRows(execute("SELECT * FROM %s WHERE c >= 3 AND c <= 6 ALLOW FILTERING"),
-                   row(1, 2, 1, 4, 8),
-                   row(1, 3, 1, 6, 12),
-                   row(1, 4, 1, 4, 8));
-
-        assertRows(execute("SELECT * FROM %s WHERE s >= 1 LIMIT 2 ALLOW FILTERING"),
-                   row(1, 2, 1, 4, 8),
-                   row(1, 3, 1, 6, 12));
+            assertRows(execute("SELECT * FROM %s WHERE s >= 1 LIMIT 2 ALLOW FILTERING"),
+                       row(1, 2, 1, 4, 8),
+                       row(1, 3, 1, 6, 12));
+        });
 
         // Checks filtering with null
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
@@ -1378,24 +1431,6 @@
     }
 
     @Test
-    public void testIndexQueryWithCompositePartitionKey() throws Throwable
-    {
-        createTable("CREATE TABLE %s (p1 int, p2 int, v int, PRIMARY KEY ((p1, p2)))");
-        assertInvalidMessage("Partition key parts: p2 must be restricted as other parts are",
-                             "SELECT * FROM %s WHERE p1 = 1 AND v = 3 ALLOW FILTERING");
-
-        createIndex("CREATE INDEX ON %s(v)");
-
-        execute("INSERT INTO %s(p1, p2, v) values (?, ?, ?)", 1, 1, 3);
-        execute("INSERT INTO %s(p1, p2, v) values (?, ?, ?)", 1, 2, 3);
-        execute("INSERT INTO %s(p1, p2, v) values (?, ?, ?)", 2, 1, 3);
-
-        assertRows(execute("SELECT * FROM %s WHERE p1 = 1 AND v = 3 ALLOW FILTERING"),
-                   row(1, 2, 3),
-                   row(1, 1, 3));
-    }
-
-    @Test
     public void testFilteringOnCompactTablesWithoutIndices() throws Throwable
     {
         //----------------------------------------------
@@ -1414,41 +1449,43 @@
         execute("DELETE FROM %s WHERE a = 1 AND b = 1");
         execute("DELETE FROM %s WHERE a = 2 AND b = 2");
 
-        flush();
+        beforeAndAfterFlush(() -> {
 
-        // Checks filtering
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = 4");
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = 4");
 
-        assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = 4 ALLOW FILTERING"),
-                   row(1, 4, 4));
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = 4 ALLOW FILTERING"),
+                       row(1, 4, 4));
 
-        assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
-                             "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7)");
+            assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
+                                 "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7)");
 
-        assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
-                             "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7) ALLOW FILTERING");
+            assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
+                                 "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7) ALLOW FILTERING");
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c > 4");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c > 4");
 
-        assertRows(execute("SELECT * FROM %s WHERE c > 4 ALLOW FILTERING"),
-                   row(1, 3, 6),
-                   row(2, 3, 7));
+            assertRows(execute("SELECT * FROM %s WHERE c > 4 ALLOW FILTERING"),
+                       row(1, 3, 6),
+                       row(2, 3, 7));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE b < 3 AND c <= 4");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE b < 3 AND c <= 4");
 
-        assertRows(execute("SELECT * FROM %s WHERE b < 3 AND c <= 4 ALLOW FILTERING"),
-                   row(1, 2, 4));
+            assertRows(execute("SELECT * FROM %s WHERE b < 3 AND c <= 4 ALLOW FILTERING"),
+                       row(1, 2, 4));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c >= 3 AND c <= 6");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c >= 3 AND c <= 6");
 
-        assertRows(execute("SELECT * FROM %s WHERE c >= 3 AND c <= 6 ALLOW FILTERING"),
-                   row(1, 2, 4),
-                   row(1, 3, 6),
-                   row(1, 4, 4));
+            assertRows(execute("SELECT * FROM %s WHERE c >= 3 AND c <= 6 ALLOW FILTERING"),
+                       row(1, 2, 4),
+                       row(1, 3, 6),
+                       row(1, 4, 4));
+
+        });
 
         // Checks filtering with null
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
@@ -1460,7 +1497,7 @@
         assertInvalidMessage("Unsupported null value for column c",
                              "SELECT * FROM %s WHERE c > null ALLOW FILTERING");
 
-        // // Checks filtering with unset
+        // Checks filtering with unset
         assertInvalidMessage("Unsupported unset value for column c",
                              "SELECT * FROM %s WHERE c = ? ALLOW FILTERING",
                              unset());
@@ -1484,42 +1521,43 @@
         execute("DELETE FROM %s WHERE a = 0");
         execute("DELETE FROM %s WHERE a = 5");
 
-        flush();
+        beforeAndAfterFlush(() -> {
 
-        // Checks filtering
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = 4");
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = 4");
 
-        assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = 4 ALLOW FILTERING"),
-                   row(1, 2, 4));
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = 4 ALLOW FILTERING"),
+                       row(1, 2, 4));
 
-        assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
-                             "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7)");
+            assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
+                                 "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7)");
 
-        assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
-                             "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7) ALLOW FILTERING");
+            assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
+                                 "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7) ALLOW FILTERING");
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c > 4");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c > 4");
 
-        assertRows(execute("SELECT * FROM %s WHERE c > 4 ALLOW FILTERING"),
-                   row(2, 1, 6),
-                   row(4, 1, 7));
+            assertRows(execute("SELECT * FROM %s WHERE c > 4 ALLOW FILTERING"),
+                       row(2, 1, 6),
+                       row(4, 1, 7));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE b < 3 AND c <= 4");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE b < 3 AND c <= 4");
 
-        assertRows(execute("SELECT * FROM %s WHERE b < 3 AND c <= 4 ALLOW FILTERING"),
-                   row(1, 2, 4),
-                   row(3, 2, 4));
+            assertRows(execute("SELECT * FROM %s WHERE b < 3 AND c <= 4 ALLOW FILTERING"),
+                       row(1, 2, 4),
+                       row(3, 2, 4));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c >= 3 AND c <= 6");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c >= 3 AND c <= 6");
 
-        assertRows(execute("SELECT * FROM %s WHERE c >= 3 AND c <= 6 ALLOW FILTERING"),
-                   row(1, 2, 4),
-                   row(2, 1, 6),
-                   row(3, 2, 4));
+            assertRows(execute("SELECT * FROM %s WHERE c >= 3 AND c <= 6 ALLOW FILTERING"),
+                       row(1, 2, 4),
+                       row(2, 1, 6),
+                       row(3, 2, 4));
+        });
 
         // Checks filtering with null
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
@@ -1550,50 +1588,51 @@
         execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 4, [1, 2], {2, 4}, {1: 2})");
         execute("INSERT INTO %s (a, b, c, d, e) VALUES (2, 3, [3, 6], {6, 12}, {3: 6})");
 
-        flush();
+        beforeAndAfterFlush(() -> {
 
-        // Checks filtering for lists
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c CONTAINS 2");
+            // Checks filtering for lists
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c CONTAINS 2");
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
-                   row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 3 ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 3 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
 
-        // Checks filtering for sets
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE d CONTAINS 4");
+            // Checks filtering for sets
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE d CONTAINS 4");
 
-        assertRows(execute("SELECT * FROM %s WHERE d CONTAINS 4 ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
-                   row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE d CONTAINS 4 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE d CONTAINS 4 AND d CONTAINS 6 ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE d CONTAINS 4 AND d CONTAINS 6 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
 
-        // Checks filtering for maps
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE e CONTAINS 2");
+            // Checks filtering for maps
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE e CONTAINS 2");
 
-        assertRows(execute("SELECT * FROM %s WHERE e CONTAINS 2 ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
-                   row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE e CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE e CONTAINS KEY 1 ALLOW FILTERING"),
-                   row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
-                   row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE e CONTAINS KEY 1 ALLOW FILTERING"),
+                       row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE e[1] = 6 ALLOW FILTERING"),
-                   row(1, 2, list(1, 6), set(2, 12), map(1, 6)));
+            assertRows(execute("SELECT * FROM %s WHERE e[1] = 6 ALLOW FILTERING"),
+                       row(1, 2, list(1, 6), set(2, 12), map(1, 6)));
 
-        assertRows(execute("SELECT * FROM %s WHERE e CONTAINS KEY 1 AND e CONTAINS 2 ALLOW FILTERING"),
-                   row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE e CONTAINS KEY 1 AND e CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND d CONTAINS 4 AND e CONTAINS KEY 3 ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND d CONTAINS 4 AND e CONTAINS KEY 3 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+        });
 
         // Checks filtering with null
         assertInvalidMessage("Unsupported null value for column c",
@@ -1640,100 +1679,101 @@
         execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 4, [1, 2], {2, 4}, {1: 2})");
         execute("INSERT INTO %s (a, b, c, d, e) VALUES (2, 3, [3, 6], {6, 12}, {3: 6})");
 
-        flush();
+        beforeAndAfterFlush(() -> {
 
-        // Checks filtering for lists
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c = [3, 2]");
+            // Checks filtering for lists
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c = [3, 2]");
 
-        assertRows(execute("SELECT * FROM %s WHERE c = [3, 2] ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c = [3, 2] ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c > [1, 5] AND c < [3, 6]");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c > [1, 5] AND c < [3, 6]");
 
-        assertRows(execute("SELECT * FROM %s WHERE c > [1, 5] AND c < [3, 6] ALLOW FILTERING"),
-                   row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c > [1, 5] AND c < [3, 6] ALLOW FILTERING"),
+                       row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE c >= [1, 6] AND c < [3, 3] ALLOW FILTERING"),
-                   row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c >= [1, 6] AND c < [3, 3] ALLOW FILTERING"),
+                       row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                "SELECT * FROM %s WHERE c CONTAINS 2");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c CONTAINS 2");
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
-                   row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 3 ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 3 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
 
-        // Checks filtering for sets
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE d = {6, 4}");
+            // Checks filtering for sets
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE d = {6, 4}");
 
-        assertRows(execute("SELECT * FROM %s WHERE d = {6, 4} ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE d = {6, 4} ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE d > {4, 5} AND d < {6}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE d > {4, 5} AND d < {6}");
 
-        assertRows(execute("SELECT * FROM %s WHERE d > {4, 5} AND d < {6} ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE d > {4, 5} AND d < {6} ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE d >= {2, 12} AND d <= {4, 6} ALLOW FILTERING"),
-                   row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE d >= {2, 12} AND d <= {4, 6} ALLOW FILTERING"),
+                       row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE d CONTAINS 4");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE d CONTAINS 4");
 
-        assertRows(execute("SELECT * FROM %s WHERE d CONTAINS 4 ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
-                   row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE d CONTAINS 4 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE d CONTAINS 4 AND d CONTAINS 6 ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE d CONTAINS 4 AND d CONTAINS 6 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
 
-        // Checks filtering for maps
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE e = {1 : 2}");
+            // Checks filtering for maps
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE e = {1 : 2}");
 
-        assertRows(execute("SELECT * FROM %s WHERE e = {1 : 2} ALLOW FILTERING"),
-                   row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE e = {1 : 2} ALLOW FILTERING"),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                "SELECT * FROM %s WHERE e > {1 : 4} AND e < {3 : 6}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE e > {1 : 4} AND e < {3 : 6}");
 
-        assertRows(execute("SELECT * FROM %s WHERE e > {1 : 4} AND e < {3 : 6} ALLOW FILTERING"),
-                   row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE e > {1 : 4} AND e < {3 : 6} ALLOW FILTERING"),
+                       row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE e >= {1 : 6} AND e <= {3 : 2} ALLOW FILTERING"),
-                   row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE e >= {1 : 6} AND e <= {3 : 2} ALLOW FILTERING"),
+                       row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE e CONTAINS 2");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE e CONTAINS 2");
 
-        assertRows(execute("SELECT * FROM %s WHERE e CONTAINS 2 ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
-                   row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE e CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE e CONTAINS KEY 1 ALLOW FILTERING"),
-                   row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
-                   row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE e CONTAINS KEY 1 ALLOW FILTERING"),
+                       row(1, 2, list(1, 6), set(2, 12), map(1, 6)),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
 
-        assertInvalidMessage("Map-entry equality predicates on frozen map column e are not supported",
-                             "SELECT * FROM %s WHERE e[1] = 6 ALLOW FILTERING");
+            assertInvalidMessage("Map-entry equality predicates on frozen map column e are not supported",
+                                 "SELECT * FROM %s WHERE e[1] = 6 ALLOW FILTERING");
 
-        assertRows(execute("SELECT * FROM %s WHERE e CONTAINS KEY 1 AND e CONTAINS 2 ALLOW FILTERING"),
-                   row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE e CONTAINS KEY 1 AND e CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND d CONTAINS 4 AND e CONTAINS KEY 3 ALLOW FILTERING"),
-                   row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND d CONTAINS 4 AND e CONTAINS KEY 3 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+        });
 
         // Checks filtering with null
         assertInvalidMessage("Unsupported null value for column c",
@@ -1798,47 +1838,48 @@
         execute("INSERT INTO %s (a, b, c) VALUES (1, 4, [4, 1])");
         execute("INSERT INTO %s (a, b, c) VALUES (2, 3, [7, 1])");
 
-        flush();
+        beforeAndAfterFlush(() -> {
 
-        // Checks filtering
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = [4, 1]");
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = [4, 1]");
 
-        assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = [4, 1] ALLOW FILTERING"),
-                   row(1, 4, list(4, 1)));
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = [4, 1] ALLOW FILTERING"),
+                       row(1, 4, list(4, 1)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c > [4, 2]");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c > [4, 2]");
 
-        assertRows(execute("SELECT * FROM %s WHERE c > [4, 2] ALLOW FILTERING"),
-                   row(1, 3, list(6, 2)),
-                   row(2, 3, list(7, 1)));
+            assertRows(execute("SELECT * FROM %s WHERE c > [4, 2] ALLOW FILTERING"),
+                       row(1, 3, list(6, 2)),
+                       row(2, 3, list(7, 1)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE b <= 3 AND c < [6, 2]");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE b <= 3 AND c < [6, 2]");
 
-        assertRows(execute("SELECT * FROM %s WHERE b <= 3 AND c < [6, 2] ALLOW FILTERING"),
-                   row(1, 2, list(4, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE b <= 3 AND c < [6, 2] ALLOW FILTERING"),
+                       row(1, 2, list(4, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c >= [4, 2] AND c <= [6, 4]");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c >= [4, 2] AND c <= [6, 4]");
 
-        assertRows(execute("SELECT * FROM %s WHERE c >= [4, 2] AND c <= [6, 4] ALLOW FILTERING"),
-                   row(1, 2, list(4, 2)),
-                   row(1, 3, list(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c >= [4, 2] AND c <= [6, 4] ALLOW FILTERING"),
+                       row(1, 2, list(4, 2)),
+                       row(1, 3, list(6, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c CONTAINS 2");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c CONTAINS 2");
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
-                   row(1, 2, list(4, 2)),
-                   row(1, 3, list(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 2, list(4, 2)),
+                       row(1, 3, list(6, 2)));
 
-        assertInvalidMessage("Cannot use CONTAINS KEY on non-map column c",
-                             "SELECT * FROM %s WHERE c CONTAINS KEY 2 ALLOW FILTERING");
+            assertInvalidMessage("Cannot use CONTAINS KEY on non-map column c",
+                                 "SELECT * FROM %s WHERE c CONTAINS KEY 2 ALLOW FILTERING");
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 6 ALLOW FILTERING"),
-                   row(1, 3, list(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 6 ALLOW FILTERING"),
+                       row(1, 3, list(6, 2)));
+        });
 
         // Checks filtering with null
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
@@ -1875,47 +1916,48 @@
         execute("INSERT INTO %s (a, b, c) VALUES (3, 2, [4, 1])");
         execute("INSERT INTO %s (a, b, c) VALUES (4, 1, [7, 1])");
 
-        flush();
+        beforeAndAfterFlush(() -> {
 
-        // Checks filtering
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = [4, 2]");
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = [4, 2]");
 
-        assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = [4, 2] ALLOW FILTERING"),
-                   row(1, 2, list(4, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = [4, 2] ALLOW FILTERING"),
+                       row(1, 2, list(4, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c > [4, 2]");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c > [4, 2]");
 
-        assertRows(execute("SELECT * FROM %s WHERE c > [4, 2] ALLOW FILTERING"),
-                   row(2, 1, list(6, 2)),
-                   row(4, 1, list(7, 1)));
+            assertRows(execute("SELECT * FROM %s WHERE c > [4, 2] ALLOW FILTERING"),
+                       row(2, 1, list(6, 2)),
+                       row(4, 1, list(7, 1)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE b < 3 AND c <= [4, 2]");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE b < 3 AND c <= [4, 2]");
 
-        assertRows(execute("SELECT * FROM %s WHERE b < 3 AND c <= [4, 2] ALLOW FILTERING"),
-                   row(1, 2, list(4, 2)),
-                   row(3, 2, list(4, 1)));
+            assertRows(execute("SELECT * FROM %s WHERE b < 3 AND c <= [4, 2] ALLOW FILTERING"),
+                       row(1, 2, list(4, 2)),
+                       row(3, 2, list(4, 1)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c >= [4, 3] AND c <= [7]");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c >= [4, 3] AND c <= [7]");
 
-        assertRows(execute("SELECT * FROM %s WHERE c >= [4, 3] AND c <= [7] ALLOW FILTERING"),
-                   row(2, 1, list(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c >= [4, 3] AND c <= [7] ALLOW FILTERING"),
+                       row(2, 1, list(6, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                "SELECT * FROM %s WHERE c CONTAINS 2");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c CONTAINS 2");
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
-                   row(1, 2, list(4, 2)),
-                   row(2, 1, list(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 2, list(4, 2)),
+                       row(2, 1, list(6, 2)));
 
-        assertInvalidMessage("Cannot use CONTAINS KEY on non-map column c",
-                             "SELECT * FROM %s WHERE c CONTAINS KEY 2 ALLOW FILTERING");
+            assertInvalidMessage("Cannot use CONTAINS KEY on non-map column c",
+                                 "SELECT * FROM %s WHERE c CONTAINS KEY 2 ALLOW FILTERING");
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 6 ALLOW FILTERING"),
-                   row(2, 1, list(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 6 ALLOW FILTERING"),
+                       row(2, 1, list(6, 2)));
+        });
 
         // Checks filtering with null
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
@@ -1956,48 +1998,48 @@
         execute("INSERT INTO %s (a, b, c) VALUES (1, 4, {4, 1})");
         execute("INSERT INTO %s (a, b, c) VALUES (2, 3, {7, 1})");
 
-        flush();
+        beforeAndAfterFlush(() -> {
 
-        // Checks filtering
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = {4, 1}");
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = {4, 1}");
 
-        assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = {4, 1} ALLOW FILTERING"),
-                   row(1, 4, set(4, 1)));
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = {4, 1} ALLOW FILTERING"),
+                       row(1, 4, set(4, 1)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c > {4, 2}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c > {4, 2}");
 
-        assertRows(execute("SELECT * FROM %s WHERE c > {4, 2} ALLOW FILTERING"),
-                   row(1, 3, set(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c > {4, 2} ALLOW FILTERING"),
+                       row(1, 3, set(6, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE b <= 3 AND c < {6, 2}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE b <= 3 AND c < {6, 2}");
 
-        assertRows(execute("SELECT * FROM %s WHERE b <= 3 AND c < {6, 2} ALLOW FILTERING"),
-                   row(1, 2, set(2, 4)),
-                   row(2, 3, set(1, 7)));
+            assertRows(execute("SELECT * FROM %s WHERE b <= 3 AND c < {6, 2} ALLOW FILTERING"),
+                       row(1, 2, set(2, 4)),
+                       row(2, 3, set(1, 7)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c >= {4, 2} AND c <= {6, 4}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c >= {4, 2} AND c <= {6, 4}");
 
-        assertRows(execute("SELECT * FROM %s WHERE c >= {4, 2} AND c <= {6, 4} ALLOW FILTERING"),
-                   row(1, 2, set(4, 2)),
-                   row(1, 3, set(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c >= {4, 2} AND c <= {6, 4} ALLOW FILTERING"),
+                       row(1, 2, set(4, 2)),
+                       row(1, 3, set(6, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c CONTAINS 2");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c CONTAINS 2");
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
-                   row(1, 2, set(4, 2)),
-                   row(1, 3, set(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 2, set(4, 2)),
+                       row(1, 3, set(6, 2)));
 
-        assertInvalidMessage("Cannot use CONTAINS KEY on non-map column c",
-                             "SELECT * FROM %s WHERE c CONTAINS KEY 2 ALLOW FILTERING");
+            assertInvalidMessage("Cannot use CONTAINS KEY on non-map column c",
+                                 "SELECT * FROM %s WHERE c CONTAINS KEY 2 ALLOW FILTERING");
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 6 ALLOW FILTERING"),
-                   row(1, 3, set(6, 2)));
-
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 6 ALLOW FILTERING"),
+                       row(1, 3, set(6, 2)));
+        });
         // Checks filtering with null
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
                              "SELECT * FROM %s WHERE c = null");
@@ -2033,47 +2075,48 @@
         execute("INSERT INTO %s (a, b, c) VALUES (3, 2, {4, 1})");
         execute("INSERT INTO %s (a, b, c) VALUES (4, 1, {7, 1})");
 
-        flush();
+        beforeAndAfterFlush(() -> {
 
-        // Checks filtering
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = {4, 2}");
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = {4, 2}");
 
-        assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = {4, 2} ALLOW FILTERING"),
-                   row(1, 2, set(4, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = {4, 2} ALLOW FILTERING"),
+                       row(1, 2, set(4, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c > {4, 2}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c > {4, 2}");
 
-        assertRows(execute("SELECT * FROM %s WHERE c > {4, 2} ALLOW FILTERING"),
-                   row(2, 1, set(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c > {4, 2} ALLOW FILTERING"),
+                       row(2, 1, set(6, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE b < 3 AND c <= {4, 2}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE b < 3 AND c <= {4, 2}");
 
-        assertRows(execute("SELECT * FROM %s WHERE b < 3 AND c <= {4, 2} ALLOW FILTERING"),
-                   row(1, 2, set(4, 2)),
-                   row(4, 1, set(1, 7)),
-                   row(3, 2, set(4, 1)));
+            assertRows(execute("SELECT * FROM %s WHERE b < 3 AND c <= {4, 2} ALLOW FILTERING"),
+                       row(1, 2, set(4, 2)),
+                       row(4, 1, set(1, 7)),
+                       row(3, 2, set(4, 1)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c >= {4, 3} AND c <= {7}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c >= {4, 3} AND c <= {7}");
 
-        assertRows(execute("SELECT * FROM %s WHERE c >= {5, 2} AND c <= {7} ALLOW FILTERING"),
-                   row(2, 1, set(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c >= {5, 2} AND c <= {7} ALLOW FILTERING"),
+                       row(2, 1, set(6, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                "SELECT * FROM %s WHERE c CONTAINS 2");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c CONTAINS 2");
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
-                   row(1, 2, set(4, 2)),
-                   row(2, 1, set(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 2, set(4, 2)),
+                       row(2, 1, set(6, 2)));
 
-        assertInvalidMessage("Cannot use CONTAINS KEY on non-map column c",
-                             "SELECT * FROM %s WHERE c CONTAINS KEY 2 ALLOW FILTERING");
+            assertInvalidMessage("Cannot use CONTAINS KEY on non-map column c",
+                                 "SELECT * FROM %s WHERE c CONTAINS KEY 2 ALLOW FILTERING");
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 6 ALLOW FILTERING"),
-                   row(2, 1, set(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 6 ALLOW FILTERING"),
+                       row(2, 1, set(6, 2)));
+        });
 
         // Checks filtering with null
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
@@ -2104,8 +2147,9 @@
     @Test
     public void testIndexQueryWithValueOver64K() throws Throwable
     {
-        createTable("CREATE TABLE %s (a int, b int, c blob, PRIMARY KEY (a, b))");
-        createIndex("CREATE INDEX test ON %s (c)");
+        String tableName = createTable("CREATE TABLE %s (a int, b int, c blob, PRIMARY KEY (a, b))");
+        String idx = tableName + "_c_idx";
+        createIndex("CREATE INDEX " + idx + " ON %s (c)");
 
         execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 0, 0, bytes(1));
         execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 0, 1, bytes(2));
@@ -2113,7 +2157,7 @@
         assertInvalidMessage("Index expression values may not be larger than 64K",
                              "SELECT * FROM %s WHERE c = ?  ALLOW FILTERING", TOO_BIG);
 
-        dropIndex("DROP INDEX %s.test");
+        dropIndex("DROP INDEX %s." + idx);
         assertEmpty(execute("SELECT * FROM %s WHERE c = ?  ALLOW FILTERING", TOO_BIG));
     }
 
@@ -2135,6 +2179,1084 @@
     }
 
     @Test
+    public void testAllowFilteringOnPartitionKeyWithDistinct() throws Throwable
+    {
+        // Test a regular(CQL3) table.
+        createTable("CREATE TABLE %s (pk0 int, pk1 int, ck0 int, val int, PRIMARY KEY((pk0, pk1), ck0))");
+
+        for (int i = 0; i < 3; i++)
+        {
+            execute("INSERT INTO %s (pk0, pk1, ck0, val) VALUES (?, ?, 0, 0)", i, i);
+            execute("INSERT INTO %s (pk0, pk1, ck0, val) VALUES (?, ?, 1, 1)", i, i);
+        }
+
+        beforeAndAfterFlush(() -> {
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT DISTINCT pk0, pk1 FROM %s WHERE pk1 = 1 LIMIT 3");
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT DISTINCT pk0, pk1 FROM %s WHERE pk0 > 0 AND pk1 = 1 LIMIT 3");
+
+            assertRows(execute("SELECT DISTINCT pk0, pk1 FROM %s WHERE pk0 = 1 LIMIT 1 ALLOW FILTERING"),
+                    row(1, 1));
+
+            assertRows(execute("SELECT DISTINCT pk0, pk1 FROM %s WHERE pk1 = 1 LIMIT 3 ALLOW FILTERING"),
+                    row(1, 1));
+
+            assertEmpty(execute("SELECT DISTINCT pk0, pk1 FROM %s WHERE pk0 < 0 AND pk1 = 1 LIMIT 3 ALLOW FILTERING"));
+
+            // Test selection validation.
+            assertInvalidMessage("queries must request all the partition key columns",
+                    "SELECT DISTINCT pk0 FROM %s ALLOW FILTERING");
+            assertInvalidMessage("queries must only request partition key columns",
+                    "SELECT DISTINCT pk0, pk1, ck0 FROM %s ALLOW FILTERING");
+        });
+
+        // Test a 'compact storage' table.
+        createTable("CREATE TABLE %s (pk0 int, pk1 int, val int, PRIMARY KEY((pk0, pk1))) WITH COMPACT STORAGE");
+
+        for (int i = 0; i < 3; i++)
+            execute("INSERT INTO %s (pk0, pk1, val) VALUES (?, ?, ?)", i, i, i);
+
+        beforeAndAfterFlush(() -> {
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT DISTINCT pk0, pk1 FROM %s WHERE pk1 = 1 LIMIT 3");
+
+            assertRows(execute("SELECT DISTINCT pk0, pk1 FROM %s WHERE pk0 < 2 AND pk1 = 1 LIMIT 1 ALLOW FILTERING"),
+                    row(1, 1));
+
+            assertRows(execute("SELECT DISTINCT pk0, pk1 FROM %s WHERE pk1 > 1 LIMIT 3 ALLOW FILTERING"),
+                    row(2, 2));
+        });
+
+        // Test a 'wide row' thrift table.
+        createTable("CREATE TABLE %s (pk int, name text, val int, PRIMARY KEY(pk, name)) WITH COMPACT STORAGE");
+
+        for (int i = 0; i < 3; i++)
+        {
+            execute("INSERT INTO %s (pk, name, val) VALUES (?, 'name0', 0)", i);
+            execute("INSERT INTO %s (pk, name, val) VALUES (?, 'name1', 1)", i);
+        }
+
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT DISTINCT pk FROM %s WHERE pk > 1 LIMIT 1 ALLOW FILTERING"),
+                    row(2));
+
+            assertRows(execute("SELECT DISTINCT pk FROM %s WHERE pk > 0 LIMIT 3 ALLOW FILTERING"),
+                    row(1),
+                    row(2));
+        });
+    }
+
+    @Test
+    public void testAllowFilteringOnPartitionKey() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY ((a, b), c))");
+
+        execute("INSERT INTO %s (a,b,c,d) VALUES (11, 12, 13, 14)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (11, 15, 16, 17)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (21, 22, 23, 24)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (31, 32, 33, 34)");
+
+        beforeAndAfterFlush(() -> {
+
+            assertInvalidMessage("IN restrictions are not supported when the query involves filtering",
+                    "SELECT * FROM %s WHERE b in (11,12) ALLOW FILTERING");
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a = 11");
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a > 11");
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a > 11 and b = 1");
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 11 ALLOW FILTERING"),
+                    row(11, 12, 13, 14),
+                    row(11, 15, 16, 17));
+
+            assertRows(execute("SELECT * FROM %s WHERE a in (11) and b in (12,15,22)"),
+                    row(11, 12, 13, 14),
+                    row(11, 15, 16, 17));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE b in (12,15,22)");
+
+            assertRows(execute("SELECT * FROM %s WHERE a in (11) and b in (12,15,22) ALLOW FILTERING"),
+                    row(11, 12, 13, 14),
+                    row(11, 15, 16, 17));
+
+            assertRows(execute("SELECT * FROM %s WHERE a in (11) ALLOW FILTERING"),
+                    row(11, 12, 13, 14),
+                    row(11, 15, 16, 17));
+
+            assertRows(execute("SELECT * FROM %s WHERE b = 15 ALLOW FILTERING"),
+                    row(11, 15, 16, 17));
+
+            assertRows(execute("SELECT * FROM %s WHERE b >= 15 ALLOW FILTERING"),
+                    row(11, 15, 16, 17),
+                    row(31, 32, 33, 34),
+                    row(21, 22, 23, 24));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 11 ALLOW FILTERING"),
+                    row(11, 12, 13, 14),
+                    row(11, 15, 16, 17),
+                    row(31, 32, 33, 34),
+                    row(21, 22, 23, 24));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 11 AND b <= 15 ALLOW FILTERING"),
+                    row(11, 12, 13, 14),
+                    row(11, 15, 16, 17));
+
+            assertRows(execute("SELECT * FROM %s WHERE a <= 11 AND b >= 14 ALLOW FILTERING"),
+                    row(11, 15, 16, 17));
+
+            Object[][] res = getRows(execute("SELECT * FROM %s WHERE a < 11 ALLOW FILTERING"));
+            assertEquals(0, res.length);
+            res = getRows(execute("SELECT * FROM %s WHERE b > 32 ALLOW FILTERING"));
+            assertEquals(0, res.length);
+        });
+
+        // Checks filtering with unset
+        assertInvalidMessage("Unsupported unset value for column a",
+                             "SELECT * FROM %s WHERE a = ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column a",
+                             "SELECT * FROM %s WHERE a > ? ALLOW FILTERING",
+                             unset());
+
+        // No clustering key
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY ((a, b)))");
+
+        execute("INSERT INTO %s (a,b,c,d) VALUES (11, 12, 13, 14)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (11, 15, 16, 17)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (21, 22, 23, 24)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (31, 32, 33, 34)");
+
+        beforeAndAfterFlush(() -> {
+
+             assertInvalidMessage("IN restrictions are not supported when the query involves filtering",
+                    "SELECT * FROM %s WHERE b in (11,12) ALLOW FILTERING");
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a = 11");
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a > 11");
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a > 11 and b = 1");
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 11 ALLOW FILTERING"),
+                    row(11, 12, 13, 14),
+                    row(11, 15, 16, 17));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 11 ALLOW FILTERING"),
+                    row(11, 12, 13, 14),
+                    row(11, 15, 16, 17),
+                    row(31, 32, 33, 34),
+                    row(21, 22, 23, 24));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 11 AND b <= 15 ALLOW FILTERING"),
+                    row(11, 12, 13, 14),
+                    row(11, 15, 16, 17));
+
+            assertRows(execute("SELECT * FROM %s WHERE a <= 11 AND b >= 14 ALLOW FILTERING"),
+                    row(11, 15, 16, 17));
+        });
+
+        // ----------------------------------------------
+        // one partition key
+        // ----------------------------------------------
+        createTable("CREATE TABLE %s (a int primary key, b int, c int)");
+
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 2, 4)");
+        execute("INSERT INTO %s (a, b, c) VALUES (2, 1, 6)");
+        execute("INSERT INTO %s (a, b, c) VALUES (3, 2, 4)");
+        execute("INSERT INTO %s (a, b, c) VALUES (4, 1, 7)");
+
+        // Adds tomstones
+        execute("INSERT INTO %s (a, b, c) VALUES (0, 1, 4)");
+        execute("INSERT INTO %s (a, b, c) VALUES (5, 2, 7)");
+        execute("DELETE FROM %s WHERE a = 0");
+        execute("DELETE FROM %s WHERE a = 5");
+
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 ALLOW FILTERING"),
+                    row(1, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 ALLOW FILTERING"),
+                    row(1, 2, 4),
+                    row(2, 1, 6),
+                    row(4, 1, 7),
+                    row(3, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND b >= 2  ALLOW FILTERING"),
+                    row(1, 2, 4),
+                    row(3, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND b >= 2 AND c <= 4 ALLOW FILTERING"),
+                    row(1, 2, 4),
+                    row(3, 2, 4));
+        });
+    }
+
+    @Test
+    public void testAllowFilteringOnPartitionAndClusteringKey() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, e int, PRIMARY KEY ((a, b), c, d))");
+
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (11, 12, 13, 14, 15)");
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (11, 15, 16, 17, 18)");
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (21, 22, 23, 24, 25)");
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (31, 32, 33, 34, 35)");
+
+        beforeAndAfterFlush(() -> {
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 11 AND b = 15 AND c = 16"),
+                    row(11, 15, 16, 17, 18));
+
+            assertInvalidMessage(
+                    "Clustering column \"d\" cannot be restricted (preceding column \"c\" is restricted by a non-EQ relation)",
+                    "SELECT * FROM %s WHERE a = 11 AND b = 12 AND c > 13 AND d = 14");
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 11 AND b = 15 AND c = 16 AND d > 16"),
+                    row(11, 15, 16, 17, 18));
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 11 AND b = 15 AND c > 13 AND d >= 17 ALLOW FILTERING"),
+                    row(11, 15, 16, 17, 18));
+            assertInvalidMessage(
+                    "Clustering column \"d\" cannot be restricted (preceding column \"c\" is restricted by a non-EQ relation)",
+                    "SELECT * FROM %s WHERE a = 11 AND b = 12 AND c > 13 AND d > 17");
+
+            assertRows(execute("SELECT * FROM %s WHERE c > 30 AND d >= 34 ALLOW FILTERING"),
+                    row(31, 32, 33, 34, 35));
+
+            assertRows(execute("SELECT * FROM %s WHERE a <= 11 AND c > 15 AND d >= 16 ALLOW FILTERING"),
+                    row(11, 15, 16, 17, 18));
+
+            assertRows(execute("SELECT * FROM %s WHERE a <= 11 AND b >= 15 AND c > 15 AND d >= 16 ALLOW FILTERING"),
+                    row(11, 15, 16, 17, 18));
+
+            assertRows(execute("SELECT * FROM %s WHERE a <= 100 AND b >= 15 AND c > 0 AND d <= 100 ALLOW FILTERING"),
+                    row(11, 15, 16, 17, 18),
+                    row(31, 32, 33, 34, 35),
+                    row(21, 22, 23, 24, 25));
+
+            assertInvalidMessage(
+                    "Clustering column \"d\" cannot be restricted (preceding column \"c\" is restricted by a non-EQ relation)",
+                    "SELECT * FROM %s WHERE a <= 11 AND c > 15 AND d >= 16");
+        });
+
+        // test clutering order
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, e int, PRIMARY KEY ((a, b), c, d)) WITH CLUSTERING ORDER BY (c DESC)");
+
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (11, 11, 13, 14, 15)");
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (11, 11, 14, 17, 18)");
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (11, 12, 15, 14, 15)");
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (11, 12, 16, 17, 18)");
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (21, 11, 23, 24, 25)");
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (21, 11, 24, 34, 35)");
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (21, 12, 25, 24, 25)");
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (21, 12, 26, 34, 35)");
+
+        beforeAndAfterFlush(() -> {
+            assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE b >= 12 ALLOW FILTERING"),
+                                    row(11, 12, 15, 14, 15),
+                                    row(11, 12, 16, 17, 18),
+                                    row(21, 12, 25, 24, 25),
+                                    row(21, 12, 26, 34, 35));
+        });
+    }
+
+    @Test
+    public void testAllowFilteringOnPartitionKeyWithoutIndicesWithCollections() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c list<int>, d set<int>, e map<int, int>, PRIMARY KEY ((a, b)))");
+
+        execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 2, [1, 6], {2, 12}, {1: 6})");
+        execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 3, [3, 2], {6, 4}, {3: 2})");
+        execute("INSERT INTO %s (a, b, c, d, e) VALUES (1, 4, [1, 2], {2, 4}, {1: 2})");
+        execute("INSERT INTO %s (a, b, c, d, e) VALUES (2, 3, [3, 6], {6, 12}, {3: 6})");
+
+        beforeAndAfterFlush(() -> {
+
+            // Checks filtering for lists
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE b < 0 AND c CONTAINS 2");
+
+            assertRows(execute("SELECT * FROM %s WHERE b >= 4 AND c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+
+            assertRows(
+                    execute("SELECT * FROM %s WHERE a > 0 AND b <= 3 AND c CONTAINS 2 AND c CONTAINS 3 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+
+            // Checks filtering for sets
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a = 1 AND d CONTAINS 4");
+
+            assertRows(execute("SELECT * FROM %s WHERE d CONTAINS 4 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+
+            assertRows(execute("SELECT * FROM %s WHERE d CONTAINS 4 AND d CONTAINS 6 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+
+            // Checks filtering for maps
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE e CONTAINS 2");
+
+            assertRows(execute("SELECT * FROM %s WHERE a < 2 AND b >= 3 AND e CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND e CONTAINS KEY 1 ALLOW FILTERING"),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)),
+                       row(1, 2, list(1, 6), set(2, 12), map(1, 6)));
+
+            assertRows(execute("SELECT * FROM %s WHERE a in (1) AND b in (2) AND e[1] = 6 ALLOW FILTERING"),
+                       row(1, 2, list(1, 6), set(2, 12), map(1, 6)));
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND e CONTAINS KEY 1 AND e CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 4, list(1, 2), set(2, 4), map(1, 2)));
+
+            assertRows(
+                    execute("SELECT * FROM %s WHERE a >= 1 AND b in (3) AND c CONTAINS 2 AND d CONTAINS 4 AND e CONTAINS KEY 3 ALLOW FILTERING"),
+                       row(1, 3, list(3, 2), set(6, 4), map(3, 2)));
+        });
+
+        // Checks filtering with null
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a > 1 AND c CONTAINS null ALLOW FILTERING");
+        assertInvalidMessage("Unsupported null value for column d",
+                             "SELECT * FROM %s WHERE b < 1 AND d CONTAINS null ALLOW FILTERING");
+        assertInvalidMessage("Unsupported null value for column e",
+                             "SELECT * FROM %s WHERE a >= 1 AND b < 1 AND e CONTAINS null ALLOW FILTERING");
+        assertInvalidMessage("Unsupported null value for column e",
+                             "SELECT * FROM %s WHERE a >= 1 AND b < 1 AND e CONTAINS KEY null ALLOW FILTERING");
+        assertInvalidMessage("Unsupported null map key for column e",
+                             "SELECT * FROM %s WHERE a >= 1 AND b < 1 AND e[null] = 2 ALLOW FILTERING");
+        assertInvalidMessage("Unsupported null map value for column e",
+                             "SELECT * FROM %s WHERE a >= 1 AND b < 1 AND e[1] = null ALLOW FILTERING");
+
+        // Checks filtering with unset
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND b < 1 AND c CONTAINS ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column d",
+                             "SELECT * FROM %s WHERE a >= 1 AND b < 1 AND d CONTAINS ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column e",
+                             "SELECT * FROM %s WHERE a >= 1 AND b < 1 AND e CONTAINS ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column e",
+                             "SELECT * FROM %s WHERE a >= 1 AND b < 1 AND e CONTAINS KEY ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset map key for column e",
+                             "SELECT * FROM %s WHERE a >= 1 AND b < 1 AND e[?] = 2 ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset map value for column e",
+                             "SELECT * FROM %s WHERE a >= 1 AND b < 1 AND e[1] = ? ALLOW FILTERING",
+                             unset());
+    }
+
+    @Test
+    public void testAllowFilteringOnPartitionKeyWithCounters() throws Throwable
+    {
+        for (String compactStorageClause : new String[] { "", " WITH COMPACT STORAGE" })
+        {
+            createTable("CREATE TABLE %s (a int, b int, c int, cnt counter, PRIMARY KEY ((a, b), c))"
+                    + compactStorageClause);
+
+            execute("UPDATE %s SET cnt = cnt + ? WHERE a = ? AND b = ? AND c = ?", 14L, 11, 12, 13);
+            execute("UPDATE %s SET cnt = cnt + ? WHERE a = ? AND b = ? AND c = ?", 24L, 21, 22, 23);
+            execute("UPDATE %s SET cnt = cnt + ? WHERE a = ? AND b = ? AND c = ?", 27L, 21, 22, 26);
+            execute("UPDATE %s SET cnt = cnt + ? WHERE a = ? AND b = ? AND c = ?", 34L, 31, 32, 33);
+            execute("UPDATE %s SET cnt = cnt + ? WHERE a = ? AND b = ? AND c = ?", 24L, 41, 42, 43);
+
+            beforeAndAfterFlush(() -> {
+
+                assertRows(executeFilteringOnly("SELECT * FROM %s WHERE cnt = 24"),
+                        row(41, 42, 43, 24L),
+                        row(21, 22, 23, 24L));
+                assertRows(executeFilteringOnly("SELECT * FROM %s WHERE b > 22 AND cnt = 24"),
+                        row(41, 42, 43, 24L));
+                assertRows(executeFilteringOnly("SELECT * FROM %s WHERE b > 10 AND b < 25 AND cnt = 24"),
+                        row(21, 22, 23, 24L));
+                assertRows(executeFilteringOnly("SELECT * FROM %s WHERE b > 10 AND c < 25 AND cnt = 24"),
+                        row(21, 22, 23, 24L));
+
+                assertInvalidMessage(
+                        "ORDER BY is only supported when the partition key is restricted by an EQ or an IN.",
+                        "SELECT * FROM %s WHERE a = 21 AND b > 10 AND cnt > 23 ORDER BY c DESC ALLOW FILTERING");
+
+                assertRows(executeFilteringOnly("SELECT * FROM %s WHERE a = 21 AND b = 22 AND cnt > 23 ORDER BY c DESC"),
+                        row(21, 22, 26, 27L),
+                        row(21, 22, 23, 24L));
+
+                assertRows(executeFilteringOnly("SELECT * FROM %s WHERE cnt > 20 AND cnt < 30"),
+                        row(41, 42, 43, 24L),
+                        row(21, 22, 23, 24L),
+                        row(21, 22, 26, 27L));
+            });
+        }
+    }
+
+    @Test
+    public void testAllowFilteringOnPartitionKeyOnCompactTablesWithoutIndicesAndWithLists() throws Throwable
+    {
+        // ----------------------------------------------
+        // Test COMPACT table with clustering columns
+        // ----------------------------------------------
+        createTable("CREATE TABLE %s (a int, b int, c frozen<list<int>>, PRIMARY KEY (a, b)) WITH COMPACT STORAGE");
+
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 2, [4, 2])");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 3, [6, 2])");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 4, [4, 1])");
+        execute("INSERT INTO %s (a, b, c) VALUES (2, 3, [7, 1])");
+
+        beforeAndAfterFlush(() -> {
+
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a >= 1 AND b = 4 AND c = [4, 1]");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND b >= 4 AND c = [4, 1] ALLOW FILTERING"),
+                    row(1, 4, list(4, 1)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a > 0 AND c > [4, 2]");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND c > [4, 2] ALLOW FILTERING"),
+                    row(1, 3, list(6, 2)),
+                    row(2, 3, list(7, 1)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a > 1 AND b <= 3 AND c < [6, 2]");
+
+            assertRows(execute("SELECT * FROM %s WHERE a <= 1 AND b <= 3 AND c < [6, 2] ALLOW FILTERING"),
+                    row(1, 2, list(4, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a <= 1 AND c >= [4, 2] AND c <= [6, 4]");
+
+            assertRows(execute("SELECT * FROM %s WHERE a > 0 AND b <= 3 AND c >= [4, 2] AND c <= [6, 4] ALLOW FILTERING"),
+                    row(1, 2, list(4, 2)),
+                    row(1, 3, list(6, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a > 1 AND c CONTAINS 2");
+
+            assertRows(execute("SELECT * FROM %s WHERE a > 0 AND c CONTAINS 2 ALLOW FILTERING"),
+                    row(1, 2, list(4, 2)),
+                    row(1, 3, list(6, 2)));
+
+            assertInvalidMessage("Cannot use CONTAINS KEY on non-map column c",
+                    "SELECT * FROM %s WHERE a > 1 AND c CONTAINS KEY 2 ALLOW FILTERING");
+
+            assertRows(execute("SELECT * FROM %s WHERE a < 2 AND c CONTAINS 2 AND c CONTAINS 6 ALLOW FILTERING"),
+                    row(1, 3, list(6, 2)));
+        });
+        // Checks filtering with null
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                "SELECT * FROM %s WHERE a > 1 AND c = null");
+        assertInvalidMessage("Unsupported null value for column c",
+                "SELECT * FROM %s WHERE a > 1 AND c = null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                "SELECT * FROM %s WHERE a > 1 AND c > null");
+        assertInvalidMessage("Unsupported null value for column c",
+                "SELECT * FROM %s WHERE a > 1 AND c > null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                "SELECT * FROM %s WHERE a > 1 AND c CONTAINS null");
+        assertInvalidMessage("Unsupported null value for column c",
+                "SELECT * FROM %s WHERE a > 1 AND c CONTAINS null ALLOW FILTERING");
+
+        // Checks filtering with unset
+        assertInvalidMessage("Unsupported unset value for column c",
+                "SELECT * FROM %s WHERE a > 1 AND c = ? ALLOW FILTERING",
+                unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                "SELECT * FROM %s WHERE a > 1 AND c > ? ALLOW FILTERING",
+                unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                "SELECT * FROM %s WHERE a > 1 AND c CONTAINS ? ALLOW FILTERING",
+                unset());
+
+        // ----------------------------------------------
+        // Test COMPACT table without clustering columns
+        // ----------------------------------------------
+        createTable("CREATE TABLE %s (a int PRIMARY KEY, b int, c frozen<list<int>>) WITH COMPACT STORAGE");
+
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 2, [4, 2])");
+        execute("INSERT INTO %s (a, b, c) VALUES (2, 1, [6, 2])");
+        execute("INSERT INTO %s (a, b, c) VALUES (3, 2, [4, 1])");
+        execute("INSERT INTO %s (a, b, c) VALUES (4, 1, [7, 1])");
+
+        beforeAndAfterFlush(() -> {
+
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a >= 1 AND b = 2 AND c = [4, 2]");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND b = 2 AND c = [4, 2] ALLOW FILTERING"),
+                    row(1, 2, list(4, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a > 1 AND c > [4, 2]");
+
+            assertRows(execute("SELECT * FROM %s WHERE a > 3 AND c > [4, 2] ALLOW FILTERING"),
+                    row(4, 1, list(7, 1)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a < 1 AND b < 3 AND c <= [4, 2]");
+
+            assertRows(execute("SELECT * FROM %s WHERE a < 3 AND b < 3 AND c <= [4, 2] ALLOW FILTERING"),
+                    row(1, 2, list(4, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a > 1 AND c >= [4, 3] AND c <= [7]");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 2 AND c >= [4, 3] AND c <= [7] ALLOW FILTERING"),
+                    row(2, 1, list(6, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a > 3 AND c CONTAINS 2");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND c CONTAINS 2 ALLOW FILTERING"),
+                    row(1, 2, list(4, 2)),
+                    row(2, 1, list(6, 2)));
+
+            assertInvalidMessage("Cannot use CONTAINS KEY on non-map column c",
+                    "SELECT * FROM %s WHERE a >=1 AND c CONTAINS KEY 2 ALLOW FILTERING");
+
+            assertRows(execute("SELECT * FROM %s WHERE a < 3 AND c CONTAINS 2 AND c CONTAINS 6 ALLOW FILTERING"),
+                    row(2, 1, list(6, 2)));
+        });
+
+        // Checks filtering with null
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                "SELECT * FROM %s WHERE a > 1 AND c = null");
+        assertInvalidMessage("Unsupported null value for column c",
+                "SELECT * FROM %s WHERE a > 1 AND c = null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                "SELECT * FROM %s WHERE a > 1 AND c > null");
+        assertInvalidMessage("Unsupported null value for column c",
+                "SELECT * FROM %s WHERE a > 1 AND c > null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                "SELECT * FROM %s WHERE a > 1 AND c CONTAINS null");
+        assertInvalidMessage("Unsupported null value for column c",
+                "SELECT * FROM %s WHERE a > 1 AND c CONTAINS null ALLOW FILTERING");
+
+        // Checks filtering with unset
+        assertInvalidMessage("Unsupported unset value for column c",
+                "SELECT * FROM %s WHERE a > 1 AND c = ? ALLOW FILTERING",
+                unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                "SELECT * FROM %s WHERE a > 1 AND c > ? ALLOW FILTERING",
+                unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                "SELECT * FROM %s WHERE a > 1 AND c CONTAINS ? ALLOW FILTERING",
+                unset());
+    }
+
+
+    @Test
+    public void testAllowFilteringOnPartitionKeyOnCompactTablesWithoutIndicesAndWithMaps() throws Throwable
+    {
+        //----------------------------------------------
+        // Test COMPACT table with clustering columns
+        //----------------------------------------------
+        createTable("CREATE TABLE %s (a int, b int, c frozen<map<int, int>>, PRIMARY KEY (a, b)) WITH COMPACT STORAGE");
+
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 2, {4 : 2})");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 3, {6 : 2})");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 4, {4 : 1})");
+        execute("INSERT INTO %s (a, b, c) VALUES (2, 3, {7 : 1})");
+
+        beforeAndAfterFlush(() -> {
+
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND b = 4 AND c = {4 : 1}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a <= 1 AND b = 4 AND c = {4 : 1} ALLOW FILTERING"),
+                       row(1, 4, map(4, 1)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a > 1 AND c > {4 : 2}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a > 1 AND c > {4 : 2} ALLOW FILTERING"),
+                       row(2, 3, map(7, 1)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a > 1 AND b <= 3 AND c < {6 : 2}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND b <= 3 AND c < {6 : 2} ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a > 1 AND c >= {4 : 2} AND c <= {6 : 4}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a > 0 AND c >= {4 : 2} AND c <= {6 : 4} ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)),
+                       row(1, 3, map(6, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a > 10 AND c CONTAINS 2");
+
+            assertRows(execute("SELECT * FROM %s WHERE a > 0 AND c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)),
+                       row(1, 3, map(6, 2)));
+
+            assertRows(execute("SELECT * FROM %s WHERE a < 2 AND c CONTAINS KEY 6 ALLOW FILTERING"),
+                       row(1, 3, map(6, 2)));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND c CONTAINS 2 AND c CONTAINS KEY 6 ALLOW FILTERING"),
+                       row(1, 3, map(6, 2)));
+        });
+
+        // Checks filtering with null
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c = null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c = null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c > null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c > null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS null ALLOW FILTERING");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS KEY null ALLOW FILTERING");
+
+        // Checks filtering with unset
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c = ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c > ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS KEY ? ALLOW FILTERING",
+                             unset());
+
+        //----------------------------------------------
+        // Test COMPACT table without clustering columns
+        //----------------------------------------------
+        createTable("CREATE TABLE %s (a int PRIMARY KEY, b int, c frozen<map<int, int>>) WITH COMPACT STORAGE");
+
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 2, {4 : 2})");
+        execute("INSERT INTO %s (a, b, c) VALUES (2, 1, {6 : 2})");
+        execute("INSERT INTO %s (a, b, c) VALUES (3, 2, {4 : 1})");
+        execute("INSERT INTO %s (a, b, c) VALUES (4, 1, {7 : 1})");
+
+        beforeAndAfterFlush(() -> {
+
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND b = 2 AND c = {4 : 2}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND b = 2 AND c = {4 : 2} ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND c > {4 : 2}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND c > {4 : 2} ALLOW FILTERING"),
+                       row(2, 1, map(6, 2)),
+                       row(4, 1, map(7, 1)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND b < 3 AND c <= {4 : 2}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND b < 3 AND c <= {4 : 2} ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)),
+                       row(3, 2, map(4, 1)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND c >= {4 : 3} AND c <= {7 : 1}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 2 AND c >= {5 : 2} AND c <= {7 : 0} ALLOW FILTERING"),
+                       row(2, 1, map(6, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS 2");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)),
+                       row(2, 1, map(6, 2)));
+
+            assertRows(execute("SELECT * FROM %s WHERE a > 0 AND c CONTAINS KEY 4 ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)),
+                       row(3, 2, map(4, 1)));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 2 AND c CONTAINS 2 AND c CONTAINS KEY 6 ALLOW FILTERING"),
+                       row(2, 1, map(6, 2)));
+        });
+
+        // Checks filtering with null
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c = null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c = null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c > null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c > null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS KEY null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS KEY null ALLOW FILTERING");
+
+        // Checks filtering with unset
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c = ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c > ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS KEY ? ALLOW FILTERING",
+                             unset());
+    }
+
+    @Test
+    public void testAllowFilteringOnPartitionKeyOnCompactTablesWithoutIndicesAndWithSets() throws Throwable
+    {
+        //----------------------------------------------
+        // Test COMPACT table with clustering columns
+        //----------------------------------------------
+        createTable("CREATE TABLE %s (a int, b int, c frozen<set<int>>, PRIMARY KEY (a, b)) WITH COMPACT STORAGE");
+
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 2, {4, 2})");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 3, {6, 2})");
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 4, {4, 1})");
+        execute("INSERT INTO %s (a, b, c) VALUES (2, 3, {7, 1})");
+
+        beforeAndAfterFlush(() -> {
+
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND b = 4 AND c = {4, 1}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND b = 4 AND c = {4, 1} ALLOW FILTERING"),
+                       row(1, 4, set(4, 1)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND c > {4, 2}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND c > {4, 2} ALLOW FILTERING"),
+                       row(1, 3, set(6, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND b <= 3 AND c < {6, 2}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a > 0 AND b <= 3 AND c < {6, 2} ALLOW FILTERING"),
+                       row(1, 2, set(2, 4)),
+                       row(2, 3, set(1, 7)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND c >= {4, 2} AND c <= {6, 4}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 0 AND c >= {4, 2} AND c <= {6, 4} ALLOW FILTERING"),
+                       row(1, 2, set(4, 2)),
+                       row(1, 3, set(6, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS 2");
+
+            assertRows(execute("SELECT * FROM %s WHERE a < 2 AND c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 2, set(4, 2)),
+                       row(1, 3, set(6, 2)));
+
+            assertInvalidMessage("Cannot use CONTAINS KEY on non-map column c",
+                                 "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS KEY 2 ALLOW FILTERING");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND c CONTAINS 2 AND c CONTAINS 6 ALLOW FILTERING"),
+                       row(1, 3, set(6, 2)));
+        });
+        // Checks filtering with null
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c = null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c = null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c > null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c > null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS null ALLOW FILTERING");
+
+        // Checks filtering with unset
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c = ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c > ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS ? ALLOW FILTERING",
+                             unset());
+
+        //----------------------------------------------
+        // Test COMPACT table without clustering columns
+        //----------------------------------------------
+        createTable("CREATE TABLE %s (a int PRIMARY KEY, b int, c frozen<set<int>>) WITH COMPACT STORAGE");
+
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 2, {4, 2})");
+        execute("INSERT INTO %s (a, b, c) VALUES (2, 1, {6, 2})");
+        execute("INSERT INTO %s (a, b, c) VALUES (3, 2, {4, 1})");
+        execute("INSERT INTO %s (a, b, c) VALUES (4, 1, {7, 1})");
+
+        beforeAndAfterFlush(() -> {
+
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND b = 2 AND c = {4, 2}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND b = 2 AND c = {4, 2} ALLOW FILTERING"),
+                       row(1, 2, set(4, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND c > {4, 2}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND c > {4, 2} ALLOW FILTERING"),
+                       row(2, 1, set(6, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND b < 3 AND c <= {4, 2}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a <= 4 AND b < 3 AND c <= {4, 2} ALLOW FILTERING"),
+                       row(1, 2, set(4, 2)),
+                       row(4, 1, set(1, 7)),
+                       row(3, 2, set(4, 1)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND c >= {4, 3} AND c <= {7}");
+
+            assertRows(execute("SELECT * FROM %s WHERE a < 3 AND c >= {5, 2} AND c <= {7} ALLOW FILTERING"),
+                       row(2, 1, set(6, 2)));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS 2");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 0 AND c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 2, set(4, 2)),
+                       row(2, 1, set(6, 2)));
+
+            assertInvalidMessage("Cannot use CONTAINS KEY on non-map column c",
+                                 "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS KEY 2 ALLOW FILTERING");
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 2 AND c CONTAINS 2 AND c CONTAINS 6 ALLOW FILTERING"),
+                       row(2, 1, set(6, 2)));
+        });
+
+        // Checks filtering with null
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c = null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c = null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c > null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c > null ALLOW FILTERING");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS null");
+        assertInvalidMessage("Unsupported null value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS null ALLOW FILTERING");
+
+        // Checks filtering with unset
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c = ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c > ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column c",
+                             "SELECT * FROM %s WHERE a >= 1 AND c CONTAINS ? ALLOW FILTERING",
+                             unset());
+    }
+
+    @Test
+    public void testAllowFilteringOnPartitionKeyOnCompactTablesWithoutIndices() throws Throwable
+    {
+        // ----------------------------------------------
+        // Test COMPACT table with clustering columns
+        // ----------------------------------------------
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY ((a, b), c)) WITH COMPACT STORAGE");
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 2, 4, 5)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 3, 6, 7)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 4, 4, 5)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (2, 3, 7, 8)");
+
+        // Adds tomstones
+        execute("INSERT INTO %s (a, b, c, d) VALUES (1, 1, 4, 5)");
+        execute("INSERT INTO %s (a, b, c, d) VALUES (2, 2, 7, 8)");
+        execute("DELETE FROM %s WHERE a = 1 AND b = 1 AND c = 4");
+        execute("DELETE FROM %s WHERE a = 2 AND b = 2 AND c = 7");
+
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = 4"),
+                    row(1, 4, 4, 5));
+
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = 4 AND d = 5");
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = 4 ALLOW FILTERING"),
+                    row(1, 4, 4, 5));
+
+            assertInvalidMessage("IN predicates on non-primary-key columns (d) is not yet supported",
+                    "SELECT * FROM %s WHERE a IN (1, 2) AND b = 3 AND d IN (6, 7)");
+
+            assertInvalidMessage("IN predicates on non-primary-key columns (d) is not yet supported",
+                    "SELECT * FROM %s WHERE a IN (1, 2) AND b = 3 AND d IN (6, 7) ALLOW FILTERING");
+
+            assertRows(execute("SELECT * FROM %s WHERE a < 2 AND c > 4 AND c <= 6 ALLOW FILTERING"),
+                    row(1, 3, 6, 7));
+
+            assertRows(execute("SELECT * FROM %s WHERE a <= 1 AND b >= 2 AND c >= 4 AND d <= 8 ALLOW FILTERING"),
+                    row(1, 3, 6, 7),
+                    row(1, 4, 4, 5),
+                    row(1, 2, 4, 5));
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND c >= 4 AND d <= 8 ALLOW FILTERING"),
+                    row(1, 3, 6, 7),
+                    row(1, 4, 4, 5),
+                    row(1, 2, 4, 5));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 2 AND c >= 4 AND d <= 8 ALLOW FILTERING"),
+                    row(2, 3, 7, 8));
+        });
+
+        // Checks filtering with null
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE d = null");
+        assertInvalidMessage("Unsupported null value for column a",
+                             "SELECT * FROM %s WHERE a = null ALLOW FILTERING");
+        assertInvalidMessage("Unsupported null value for column a",
+                             "SELECT * FROM %s WHERE a > null ALLOW FILTERING");
+
+        // Checks filtering with unset
+        assertInvalidMessage("Unsupported unset value for column a",
+                             "SELECT * FROM %s WHERE a = ? ALLOW FILTERING",
+                             unset());
+        assertInvalidMessage("Unsupported unset value for column a",
+                             "SELECT * FROM %s WHERE a > ? ALLOW FILTERING",
+                             unset());
+
+        //----------------------------------------------
+        // Test COMPACT table without clustering columns
+        //----------------------------------------------
+        createTable("CREATE TABLE %s (a int primary key, b int, c int) WITH COMPACT STORAGE");
+
+        execute("INSERT INTO %s (a, b, c) VALUES (1, 2, 4)");
+        execute("INSERT INTO %s (a, b, c) VALUES (2, 1, 6)");
+        execute("INSERT INTO %s (a, b, c) VALUES (3, 2, 4)");
+        execute("INSERT INTO %s (a, b, c) VALUES (4, 1, 7)");
+
+        // Adds tomstones
+        execute("INSERT INTO %s (a, b, c) VALUES (0, 1, 4)");
+        execute("INSERT INTO %s (a, b, c) VALUES (5, 2, 7)");
+        execute("DELETE FROM %s WHERE a = 0");
+        execute("DELETE FROM %s WHERE a = 5");
+
+        beforeAndAfterFlush(() -> {
+
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                    "SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = 4");
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = 4 ALLOW FILTERING"),
+                    row(1, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 2 ALLOW FILTERING"),
+                    row(1, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE b >= 2 AND c <= 4 ALLOW FILTERING"),
+                    row(1, 2, 4),
+                    row(3, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 ALLOW FILTERING"),
+                    row(1, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE b >= 2 ALLOW FILTERING"),
+                    row(1, 2, 4),
+                    row(3, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 2 AND b <=1 ALLOW FILTERING"),
+                    row(2, 1, 6),
+                    row(4, 1, 7));
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND c >= 4 ALLOW FILTERING"),
+                    row(1, 2, 4));
+
+            assertInvalidMessage("IN predicates on non-primary-key columns (b) is not yet supported",
+                                 "SELECT * FROM %s WHERE a = 1 AND b IN (1, 2) AND c IN (6, 7)");
+
+            assertInvalidMessage("IN predicates on non-primary-key columns (c) is not yet supported",
+                                 "SELECT * FROM %s WHERE a IN (1, 2) AND c IN (6, 7) ALLOW FILTERING");
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c > 4");
+
+            assertRows(execute("SELECT * FROM %s WHERE c > 4 ALLOW FILTERING"),
+                    row(2, 1, 6),
+                    row(4, 1, 7));
+
+            assertRows(execute("SELECT * FROM %s WHERE a >= 1 AND b >= 2 AND c <= 4 ALLOW FILTERING"),
+                    row(1, 2, 4),
+                    row(3, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE a < 3 AND c <= 4 ALLOW FILTERING"),
+                    row(1, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE a < 3 AND b >= 2 AND c <= 4 ALLOW FILTERING"),
+                    row(1, 2, 4));
+
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c >= 3 AND c <= 6");
+
+            assertRows(execute("SELECT * FROM %s WHERE c <=6 ALLOW FILTERING"),
+                    row(1, 2, 4),
+                    row(2, 1, 6),
+                    row(3, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE token(a) >= token(2)"),
+                    row(2, 1, 6),
+                    row(4, 1, 7),
+                    row(3, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE token(a) >= token(2) ALLOW FILTERING"),
+                    row(2, 1, 6),
+                    row(4, 1, 7),
+                    row(3, 2, 4));
+
+            assertRows(execute("SELECT * FROM %s WHERE token(a) >= token(2) AND b = 1 ALLOW FILTERING"),
+                       row(2, 1, 6),
+                       row(4, 1, 7));
+
+        });
+    }
+
+    @Test
     public void testFilteringOnCompactTablesWithoutIndicesAndWithMaps() throws Throwable
     {
         //----------------------------------------------
@@ -2147,47 +3269,48 @@
         execute("INSERT INTO %s (a, b, c) VALUES (1, 4, {4 : 1})");
         execute("INSERT INTO %s (a, b, c) VALUES (2, 3, {7 : 1})");
 
-        flush();
+        beforeAndAfterFlush(() -> {
 
-        // Checks filtering
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = {4 : 1}");
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = {4 : 1}");
 
-        assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = {4 : 1} ALLOW FILTERING"),
-                   row(1, 4, map(4, 1)));
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 4 AND c = {4 : 1} ALLOW FILTERING"),
+                       row(1, 4, map(4, 1)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c > {4 : 2}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c > {4 : 2}");
 
-        assertRows(execute("SELECT * FROM %s WHERE c > {4 : 2} ALLOW FILTERING"),
-                   row(1, 3, map(6, 2)),
-                   row(2, 3, map(7, 1)));
+            assertRows(execute("SELECT * FROM %s WHERE c > {4 : 2} ALLOW FILTERING"),
+                       row(1, 3, map(6, 2)),
+                       row(2, 3, map(7, 1)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE b <= 3 AND c < {6 : 2}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE b <= 3 AND c < {6 : 2}");
 
-        assertRows(execute("SELECT * FROM %s WHERE b <= 3 AND c < {6 : 2} ALLOW FILTERING"),
-                   row(1, 2, map(4, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE b <= 3 AND c < {6 : 2} ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c >= {4 : 2} AND c <= {6 : 4}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c >= {4 : 2} AND c <= {6 : 4}");
 
-        assertRows(execute("SELECT * FROM %s WHERE c >= {4 : 2} AND c <= {6 : 4} ALLOW FILTERING"),
-                   row(1, 2, map(4, 2)),
-                   row(1, 3, map(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c >= {4 : 2} AND c <= {6 : 4} ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)),
+                       row(1, 3, map(6, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c CONTAINS 2");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c CONTAINS 2");
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
-                   row(1, 2, map(4, 2)),
-                   row(1, 3, map(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)),
+                       row(1, 3, map(6, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS KEY 6 ALLOW FILTERING"),
-                   row(1, 3, map(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS KEY 6 ALLOW FILTERING"),
+                       row(1, 3, map(6, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS KEY 6 ALLOW FILTERING"),
-                   row(1, 3, map(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS KEY 6 ALLOW FILTERING"),
+                       row(1, 3, map(6, 2)));
+        });
 
         // Checks filtering with null
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
@@ -2229,48 +3352,49 @@
         execute("INSERT INTO %s (a, b, c) VALUES (3, 2, {4 : 1})");
         execute("INSERT INTO %s (a, b, c) VALUES (4, 1, {7 : 1})");
 
-        flush();
+        beforeAndAfterFlush(() -> {
 
-        // Checks filtering
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = {4 : 2}");
+            // Checks filtering
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = {4 : 2}");
 
-        assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = {4 : 2} ALLOW FILTERING"),
-                   row(1, 2, map(4, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE a = 1 AND b = 2 AND c = {4 : 2} ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c > {4 : 2}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c > {4 : 2}");
 
-        assertRows(execute("SELECT * FROM %s WHERE c > {4 : 2} ALLOW FILTERING"),
-                   row(2, 1, map(6, 2)),
-                   row(4, 1, map(7, 1)));
+            assertRows(execute("SELECT * FROM %s WHERE c > {4 : 2} ALLOW FILTERING"),
+                       row(2, 1, map(6, 2)),
+                       row(4, 1, map(7, 1)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE b < 3 AND c <= {4 : 2}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE b < 3 AND c <= {4 : 2}");
 
-        assertRows(execute("SELECT * FROM %s WHERE b < 3 AND c <= {4 : 2} ALLOW FILTERING"),
-                   row(1, 2, map(4, 2)),
-                   row(3, 2, map(4, 1)));
+            assertRows(execute("SELECT * FROM %s WHERE b < 3 AND c <= {4 : 2} ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)),
+                       row(3, 2, map(4, 1)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                             "SELECT * FROM %s WHERE c >= {4 : 3} AND c <= {7 : 1}");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c >= {4 : 3} AND c <= {7 : 1}");
 
-        assertRows(execute("SELECT * FROM %s WHERE c >= {5 : 2} AND c <= {7 : 0} ALLOW FILTERING"),
-                   row(2, 1, map(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c >= {5 : 2} AND c <= {7 : 0} ALLOW FILTERING"),
+                       row(2, 1, map(6, 2)));
 
-        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
-                "SELECT * FROM %s WHERE c CONTAINS 2");
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                                 "SELECT * FROM %s WHERE c CONTAINS 2");
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
-                   row(1, 2, map(4, 2)),
-                   row(2, 1, map(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)),
+                       row(2, 1, map(6, 2)));
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS KEY 4 ALLOW FILTERING"),
-                   row(1, 2, map(4, 2)),
-                   row(3, 2, map(4, 1)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS KEY 4 ALLOW FILTERING"),
+                       row(1, 2, map(4, 2)),
+                       row(3, 2, map(4, 1)));
 
-        assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS KEY 6 ALLOW FILTERING"),
-                   row(2, 1, map(6, 2)));
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS KEY 6 ALLOW FILTERING"),
+                       row(2, 1, map(6, 2)));
+        });
 
         // Checks filtering with null
         assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
@@ -2305,6 +3429,475 @@
                              unset());
     }
 
+    @Test
+    public void filteringOnClusteringColumns() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY (a, b, c))");
+
+        execute("INSERT INTO %s (a,b,c,d) VALUES (11, 12, 13, 14)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (11, 15, 16, 17)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (21, 22, 23, 24)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (31, 32, 33, 34)");
+
+        beforeAndAfterFlush(() -> {
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 11 AND b = 15"),
+                       row(11, 15, 16, 17));
+
+            assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
+                                 "SELECT * FROM %s WHERE a = 11 AND b > 12 AND c = 15");
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 11 AND b = 15 AND c > 15"),
+                       row(11, 15, 16, 17));
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 11 AND b > 12 AND c > 13 AND d = 17 ALLOW FILTERING"),
+                       row(11, 15, 16, 17));
+            assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
+                                 "SELECT * FROM %s WHERE a = 11 AND b > 12 AND c > 13 and d = 17");
+
+            assertRows(execute("SELECT * FROM %s WHERE b > 20 AND c > 30 ALLOW FILTERING"),
+                       row(31, 32, 33, 34));
+            assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
+                                 "SELECT * FROM %s WHERE b > 20 AND c > 30");
+
+            assertRows(execute("SELECT * FROM %s WHERE b > 20 AND c < 30 ALLOW FILTERING"),
+                       row(21, 22, 23, 24));
+            assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
+                                 "SELECT * FROM %s WHERE b > 20 AND c < 30");
+
+            assertRows(execute("SELECT * FROM %s WHERE b > 20 AND c = 33 ALLOW FILTERING"),
+                       row(31, 32, 33, 34));
+            assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
+                                 "SELECT * FROM %s WHERE b > 20 AND c = 33");
+
+            assertRows(execute("SELECT * FROM %s WHERE c = 33 ALLOW FILTERING"),
+                       row(31, 32, 33, 34));
+            assertInvalidMessage("PRIMARY KEY column \"c\" cannot be restricted as preceding column \"b\" is not restricted",
+                                 "SELECT * FROM %s WHERE c = 33");
+        });
+
+        // --------------------------------------------------
+        // Clustering column within and across partition keys
+        // --------------------------------------------------
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY (a, b, c))");
+
+        execute("INSERT INTO %s (a,b,c,d) VALUES (11, 12, 13, 14)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (11, 15, 16, 17)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (11, 18, 19, 20)");
+
+        execute("INSERT INTO %s (a,b,c,d) VALUES (21, 22, 23, 24)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (21, 25, 26, 27)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (21, 28, 29, 30)");
+
+        execute("INSERT INTO %s (a,b,c,d) VALUES (31, 32, 33, 34)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (31, 35, 36, 37)");
+        execute("INSERT INTO %s (a,b,c,d) VALUES (31, 38, 39, 40)");
+
+        beforeAndAfterFlush(() -> {
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE a = 21 AND c > 23"),
+                       row(21, 25, 26, 27),
+                       row(21, 28, 29, 30));
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE a = 21 AND c > 23 ORDER BY b DESC"),
+                       row(21, 28, 29, 30),
+                       row(21, 25, 26, 27));
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE c > 16 and c < 36"),
+                       row(11, 18, 19, 20),
+                       row(21, 22, 23, 24),
+                       row(21, 25, 26, 27),
+                       row(21, 28, 29, 30),
+                       row(31, 32, 33, 34));
+        });
+    }
+
+    @Test
+    public void filteringWithMultiColumnSlices() throws Throwable
+    {
+        //----------------------------------------
+        // Multi-column slices for clustering keys
+        //----------------------------------------
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, e int, PRIMARY KEY (a, b, c, d))");
+
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (11, 12, 13, 14, 15)");
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (21, 22, 23, 24, 25)");
+        execute("INSERT INTO %s (a,b,c,d,e) VALUES (31, 32, 33, 34, 35)");
+
+        beforeAndAfterFlush(() -> {
+
+            assertRows(execute("SELECT * FROM %s WHERE b = 22 AND d = 24 ALLOW FILTERING"),
+                       row(21, 22, 23, 24, 25));
+            assertInvalidMessage("PRIMARY KEY column \"d\" cannot be restricted as preceding column \"c\" is not restricted",
+                                 "SELECT * FROM %s WHERE b = 22 AND d = 24");
+
+            assertRows(execute("SELECT * FROM %s WHERE (b, c) > (20, 30) AND d = 34 ALLOW FILTERING"),
+                       row(31, 32, 33, 34, 35));
+            assertInvalidMessage("Clustering column \"d\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
+                                 "SELECT * FROM %s WHERE (b, c) > (20, 30) AND d = 34");
+        });
+    }
+
+    @Test
+    public void containsFilteringForClusteringKeys() throws Throwable
+    {
+        //-------------------------------------------------
+        // Frozen collections filtering for clustering keys
+        //-------------------------------------------------
+
+        // first clustering column
+        createTable("CREATE TABLE %s (a int, b frozen<list<int>>, c int, PRIMARY KEY (a, b, c))");
+        execute("INSERT INTO %s (a,b,c) VALUES (?, ?, ?)", 11, list(1, 3), 14);
+        execute("INSERT INTO %s (a,b,c) VALUES (?, ?, ?)", 21, list(2, 3), 24);
+        execute("INSERT INTO %s (a,b,c) VALUES (?, ?, ?)", 21, list(3, 3), 34);
+
+        beforeAndAfterFlush(() -> {
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 21 AND b CONTAINS 2 ALLOW FILTERING"),
+                       row(21, list(2, 3), 24));
+            assertInvalidMessage("Clustering columns can only be restricted with CONTAINS with a secondary index or filtering",
+                                 "SELECT * FROM %s WHERE a = 21 AND b CONTAINS 2");
+
+            assertRows(execute("SELECT * FROM %s WHERE b CONTAINS 2 ALLOW FILTERING"),
+                       row(21, list(2, 3), 24));
+            assertInvalidMessage("Clustering columns can only be restricted with CONTAINS with a secondary index or filtering",
+                                 "SELECT * FROM %s WHERE b CONTAINS 2");
+
+            assertRows(execute("SELECT * FROM %s WHERE b CONTAINS 3 ALLOW FILTERING"),
+                       row(11, list(1, 3), 14),
+                       row(21, list(2, 3), 24),
+                       row(21, list(3, 3), 34));
+        });
+
+        // non-first clustering column
+        createTable("CREATE TABLE %s (a int, b int, c frozen<list<int>>, d int, PRIMARY KEY (a, b, c))");
+
+        execute("INSERT INTO %s (a,b,c,d) VALUES (?, ?, ?, ?)", 11, 12, list(1, 3), 14);
+        execute("INSERT INTO %s (a,b,c,d) VALUES (?, ?, ?, ?)", 21, 22, list(2, 3), 24);
+        execute("INSERT INTO %s (a,b,c,d) VALUES (?, ?, ?, ?)", 21, 22, list(3, 3), 34);
+
+        beforeAndAfterFlush(() -> {
+
+            assertRows(execute("SELECT * FROM %s WHERE a = 21 AND c CONTAINS 2 ALLOW FILTERING"),
+                       row(21, 22, list(2, 3), 24));
+            assertInvalidMessage("Clustering columns can only be restricted with CONTAINS with a secondary index or filtering",
+                                 "SELECT * FROM %s WHERE a = 21 AND c CONTAINS 2");
+
+            assertRows(execute("SELECT * FROM %s WHERE b > 20 AND c CONTAINS 2 ALLOW FILTERING"),
+                       row(21, 22, list(2, 3), 24));
+            assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
+                                 "SELECT * FROM %s WHERE b > 20 AND c CONTAINS 2");
+
+            assertRows(execute("SELECT * FROM %s WHERE c CONTAINS 3 ALLOW FILTERING"),
+                       row(11, 12, list(1, 3), 14),
+                       row(21, 22, list(2, 3), 24),
+                       row(21, 22, list(3, 3), 34));
+        });
+
+        createTable("CREATE TABLE %s (a int, b int, c frozen<map<text, text>>, d int, PRIMARY KEY (a, b, c))");
+
+        execute("INSERT INTO %s (a,b,c,d) VALUES (?, ?, ?, ?)", 11, 12, map("1", "3"), 14);
+        execute("INSERT INTO %s (a,b,c,d) VALUES (?, ?, ?, ?)", 21, 22, map("2", "3"), 24);
+        execute("INSERT INTO %s (a,b,c,d) VALUES (?, ?, ?, ?)", 21, 22, map("3", "3"), 34);
+
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE b > 20 AND c CONTAINS KEY '2' ALLOW FILTERING"),
+                       row(21, 22, map("2", "3"), 24));
+            assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)",
+                                 "SELECT * FROM %s WHERE b > 20 AND c CONTAINS KEY '2'");
+        });
+    }
+
+    @Test
+    public void testContainsOnPartitionKey() throws Throwable
+    {
+        testContainsOnPartitionKey("CREATE TABLE %s (pk frozen<map<int, int>>, ck int, v int, PRIMARY KEY (pk, ck))");
+    }
+
+    @Test
+    public void testContainsOnPartitionKeyPart() throws Throwable
+    {
+        testContainsOnPartitionKey("CREATE TABLE %s (pk frozen<map<int, int>>, ck int, v int, PRIMARY KEY ((pk, ck)))");
+    }
+
+    private void testContainsOnPartitionKey(String schema) throws Throwable
+    {
+        createTable(schema);
+
+        execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", map(1, 2), 1, 1);
+        execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", map(1, 2), 2, 2);
+
+        execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", map(1, 2, 3, 4), 1, 3);
+        execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", map(1, 2, 3, 4), 2, 3);
+
+        execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", map(5, 6), 5, 5);
+        execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", map(7, 8), 6, 6);
+
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE pk CONTAINS KEY 1");
+
+        beforeAndAfterFlush(() -> {
+            assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE pk CONTAINS KEY 1 ALLOW FILTERING"),
+                                    row(map(1, 2), 1, 1),
+                                    row(map(1, 2), 2, 2),
+                                    row(map(1, 2, 3, 4), 1, 3),
+                                    row(map(1, 2, 3, 4), 2, 3));
+
+            assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE pk CONTAINS KEY 1 AND pk CONTAINS 4 ALLOW FILTERING"),
+                                    row(map(1, 2, 3, 4), 1, 3),
+                                    row(map(1, 2, 3, 4), 2, 3));
+
+            assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE pk CONTAINS KEY 1 AND pk CONTAINS KEY 3 ALLOW FILTERING"),
+                                    row(map(1, 2, 3, 4), 1, 3),
+                                    row(map(1, 2, 3, 4), 2, 3));
+
+            assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE pk CONTAINS KEY 1 AND v = 3 ALLOW FILTERING"),
+                                    row(map(1, 2, 3, 4), 1, 3),
+                                    row(map(1, 2, 3, 4), 2, 3));
+
+            assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE pk CONTAINS KEY 1 AND ck = 1 AND v = 3 ALLOW FILTERING"),
+                                    row(map(1, 2, 3, 4), 1, 3));
+        });
+    }
+
+    @Test
+    public void filteringWithOrderClause() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, d list<int>, PRIMARY KEY (a, b, c))");
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 11, 12, 13, list(1,4));
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 21, 22, 23, list(2,4));
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 21, 25, 26, list(2,7));
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 31, 32, 33, list(3,4));
+
+        beforeAndAfterFlush(() -> {
+
+            assertRows(executeFilteringOnly("SELECT a, b, c, d FROM %s WHERE a = 21 AND c > 20 ORDER BY b DESC"),
+                       row(21, 25, 26, list(2, 7)),
+                       row(21, 22, 23, list(2, 4)));
+
+            assertRows(executeFilteringOnly("SELECT a, b, c, d FROM %s WHERE a IN(21, 31) AND c > 20 ORDER BY b DESC"),
+                       row(31, 32, 33, list(3, 4)),
+                       row(21, 25, 26, list(2, 7)),
+                       row(21, 22, 23, list(2, 4)));
+        });
+    }
+
+
+    @Test
+    public void filteringOnStaticColumnTest() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, s int static, PRIMARY KEY (a, b))");
+
+        execute("INSERT INTO %s (a, b, c, d, s) VALUES (11, 12, 13, 14, 15)");
+        execute("INSERT INTO %s (a, b, c, d, s) VALUES (21, 22, 23, 24, 25)");
+        execute("INSERT INTO %s (a, b, c, d, s) VALUES (21, 26, 27, 28, 29)");
+        execute("INSERT INTO %s (a, b, c, d, s) VALUES (31, 32, 33, 34, 35)");
+        execute("INSERT INTO %s (a, b, c, d, s) VALUES (11, 42, 43, 44, 45)");
+
+        beforeAndAfterFlush(() -> {
+
+            assertRows(executeFilteringOnly("SELECT a, b, c, d, s FROM %s WHERE s = 29"),
+                       row(21, 22, 23, 24, 29),
+                       row(21, 26, 27, 28, 29));
+            assertRows(executeFilteringOnly("SELECT a, b, c, d, s FROM %s WHERE b > 22 AND s = 29"),
+                       row(21, 26, 27, 28, 29));
+            assertRows(executeFilteringOnly("SELECT a, b, c, d, s FROM %s WHERE b > 10 and b < 26 AND s = 29"),
+                       row(21, 22, 23, 24, 29));
+            assertRows(executeFilteringOnly("SELECT a, b, c, d, s FROM %s WHERE c > 10 and c < 27 AND s = 29"),
+                       row(21, 22, 23, 24, 29));
+            assertRows(executeFilteringOnly("SELECT a, b, c, d, s FROM %s WHERE c > 10 and c < 43 AND s = 29"),
+                       row(21, 22, 23, 24, 29),
+                       row(21, 26, 27, 28, 29));
+            assertRows(executeFilteringOnly("SELECT a, b, c, d, s FROM %s WHERE c > 10 AND s > 15 AND s < 45"),
+                       row(21, 22, 23, 24, 29),
+                       row(21, 26, 27, 28, 29),
+                       row(31, 32, 33, 34, 35));
+            assertRows(executeFilteringOnly("SELECT a, b, c, d, s FROM %s WHERE a = 21 AND s > 15 AND s < 45 ORDER BY b DESC"),
+                       row(21, 26, 27, 28, 29),
+                       row(21, 22, 23, 24, 29));
+            assertRows(executeFilteringOnly("SELECT a, b, c, d, s FROM %s WHERE c > 13 and d < 44"),
+                       row(21, 22, 23, 24, 29),
+                       row(21, 26, 27, 28, 29),
+                       row(31, 32, 33, 34, 35));
+        });
+    }
+
+    @Test
+    public void containsFilteringOnNonClusteringColumn() throws Throwable {
+        createTable("CREATE TABLE %s (a int, b int, c int, d list<int>, PRIMARY KEY (a, b, c))");
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 11, 12, 13, list(1,4));
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 21, 22, 23, list(2,4));
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 21, 25, 26, list(2,7));
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 31, 32, 33, list(3,4));
+
+        beforeAndAfterFlush(() -> {
+
+            assertRows(executeFilteringOnly("SELECT a, b, c, d FROM %s WHERE b > 20 AND d CONTAINS 2"),
+                       row(21, 22, 23, list(2, 4)),
+                       row(21, 25, 26, list(2, 7)));
+
+            assertRows(executeFilteringOnly("SELECT a, b, c, d FROM %s WHERE b > 20 AND d CONTAINS 2 AND d contains 4"),
+                       row(21, 22, 23, list(2, 4)));
+        });
+    }
+
+    @Test
+    public void filteringOnCompactTable() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY (a, b, c)) WITH COMPACT STORAGE");
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 11, 12, 13, 14);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 21, 22, 23, 24);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 21, 25, 26, 27);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 31, 32, 33, 34);
+
+        beforeAndAfterFlush(() -> {
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE c > 13"),
+                       row(21, 22, 23, 24),
+                       row(21, 25, 26, 27),
+                       row(31, 32, 33, 34));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE c > 13 AND c < 33"),
+                       row(21, 22, 23, 24),
+                       row(21, 25, 26, 27));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE c > 13 AND b < 32"),
+                       row(21, 22, 23, 24),
+                       row(21, 25, 26, 27));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE a = 21 AND c > 13 AND b < 32 ORDER BY b DESC"),
+                       row(21, 25, 26, 27),
+                       row(21, 22, 23, 24));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE a IN (21, 31) AND c > 13 ORDER BY b DESC"),
+                       row(31, 32, 33, 34),
+                       row(21, 25, 26, 27),
+                       row(21, 22, 23, 24));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE c > 13 AND d < 34"),
+                       row(21, 22, 23, 24),
+                       row(21, 25, 26, 27));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE c > 13"),
+                       row(21, 22, 23, 24),
+                       row(21, 25, 26, 27),
+                       row(31, 32, 33, 34));
+        });
+
+        // with frozen in clustering key
+        createTable("CREATE TABLE %s (a int, b int, c frozen<list<int>>, d int, PRIMARY KEY (a, b, c)) WITH COMPACT STORAGE");
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 11, 12, list(1, 3), 14);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 21, 22, list(2, 3), 24);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 21, 25, list(2, 6), 27);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 31, 32, list(3, 3), 34);
+
+        beforeAndAfterFlush(() -> {
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE c CONTAINS 2"),
+                       row(21, 22, list(2, 3), 24),
+                       row(21, 25, list(2, 6), 27));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE c CONTAINS 2 AND b < 25"),
+                       row(21, 22, list(2, 3), 24));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE c CONTAINS 2 AND c CONTAINS 3"),
+                       row(21, 22, list(2, 3), 24));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE b > 12 AND c CONTAINS 2 AND d < 27"),
+                       row(21, 22, list(2, 3), 24));
+        });
+
+        // with frozen in value
+        createTable("CREATE TABLE %s (a int, b int, c int, d frozen<list<int>>, PRIMARY KEY (a, b, c)) WITH COMPACT STORAGE");
+
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 11, 12, 13, list(1, 4));
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 21, 22, 23, list(2, 4));
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 21, 25, 25, list(2, 6));
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", 31, 32, 34, list(3, 4));
+
+        beforeAndAfterFlush(() -> {
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE d CONTAINS 2"),
+                       row(21, 22, 23, list(2, 4)),
+                       row(21, 25, 25, list(2, 6)));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE d CONTAINS 2 AND b < 25"),
+                       row(21, 22, 23, list(2, 4)));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE d CONTAINS 2 AND d CONTAINS 4"),
+                       row(21, 22, 23, list(2, 4)));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE b > 12 AND c < 25 AND d CONTAINS 2"),
+                       row(21, 22, 23, list(2, 4)));
+        });
+    }
+
+    @Test
+    public void testCustomIndexWithFiltering() throws Throwable
+    {
+        // Test for CASSANDRA-11310 compatibility with 2i
+        createTable("CREATE TABLE %s (a text, b int, c text, d int, PRIMARY KEY (a, b, c));");
+        createIndex("CREATE INDEX ON %s(c)");
+        
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", "a", 0, "b", 1);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", "a", 1, "b", 2);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", "a", 2, "b", 3);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", "c", 3, "b", 4);
+        execute("INSERT INTO %s (a, b, c, d) VALUES (?, ?, ?, ?)", "d", 4, "d", 5);
+
+        beforeAndAfterFlush(() -> {
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE a='a' AND b > 0 AND c = 'b'"),
+                       row("a", 1, "b", 2),
+                       row("a", 2, "b", 3));
+
+            assertRows(executeFilteringOnly("SELECT * FROM %s WHERE c = 'b' AND d = 4"),
+                       row("c", 3, "b", 4));
+        });
+    }
+
+    @Test
+    public void testFilteringWithCounters() throws Throwable
+    {
+        for (String compactStorageClause: new String[] {"", " WITH COMPACT STORAGE"})
+        {
+            createTable("CREATE TABLE %s (a int, b int, c int, cnt counter, PRIMARY KEY (a, b, c))" + compactStorageClause);
+
+            execute("UPDATE %s SET cnt = cnt + ? WHERE a = ? AND b = ? AND c = ?", 14L, 11, 12, 13);
+            execute("UPDATE %s SET cnt = cnt + ? WHERE a = ? AND b = ? AND c = ?", 24L, 21, 22, 23);
+            execute("UPDATE %s SET cnt = cnt + ? WHERE a = ? AND b = ? AND c = ?", 27L, 21, 25, 26);
+            execute("UPDATE %s SET cnt = cnt + ? WHERE a = ? AND b = ? AND c = ?", 34L, 31, 32, 33);
+            execute("UPDATE %s SET cnt = cnt + ? WHERE a = ? AND b = ? AND c = ?", 24L, 41, 42, 43);
+
+            beforeAndAfterFlush(() -> {
+
+                assertRows(executeFilteringOnly("SELECT * FROM %s WHERE cnt = 24"),
+                           row(21, 22, 23, 24L),
+                           row(41, 42, 43, 24L));
+                assertRows(executeFilteringOnly("SELECT * FROM %s WHERE b > 22 AND cnt = 24"),
+                           row(41, 42, 43, 24L));
+                assertRows(executeFilteringOnly("SELECT * FROM %s WHERE b > 10 AND b < 25 AND cnt = 24"),
+                           row(21, 22, 23, 24L));
+                assertRows(executeFilteringOnly("SELECT * FROM %s WHERE b > 10 AND c < 25 AND cnt = 24"),
+                           row(21, 22, 23, 24L));
+                assertRows(executeFilteringOnly("SELECT * FROM %s WHERE a = 21 AND b > 10 AND cnt > 23 ORDER BY b DESC"),
+                           row(21, 25, 26, 27L),
+                           row(21, 22, 23, 24L));
+                assertRows(executeFilteringOnly("SELECT * FROM %s WHERE cnt > 20 AND cnt < 30"),
+                           row(21, 22, 23, 24L),
+                           row(21, 25, 26, 27L),
+                           row(41, 42, 43, 24L));
+            });
+        }
+    }
+
+    private UntypedResultSet executeFilteringOnly(String statement) throws Throwable
+    {
+        assertInvalid(statement);
+        return execute(statement + " ALLOW FILTERING");
+    }
+
     /**
      * Check select with and without compact storage, with different column
      * order. See CASSANDRA-10988
@@ -2397,28 +3990,43 @@
             execute("INSERT INTO %s (pk, c1, c2, c3, v) VALUES (?, ?, ?, ?, ?)", 1, i, i, i, i);
         }
 
-        assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND  c1 > 0 AND c1 < 5 AND c2 = 1 AND v = 3 ALLOW FILTERING;"),
-                   row(1, 1, 1, 3, 3));
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND  c1 > 0 AND c1 < 5 AND c2 = 1 AND v = 3 ALLOW FILTERING;"),
+                       row(1, 1, 1, 3, 3));
 
-        assertEmpty(execute("SELECT * FROM %s WHERE pk = 1 AND  c1 > 1 AND c1 < 5 AND c2 = 1 AND v = 3 ALLOW FILTERING;"));
+            assertEmpty(execute("SELECT * FROM %s WHERE pk = 1 AND  c1 > 1 AND c1 < 5 AND c2 = 1 AND v = 3 ALLOW FILTERING;"));
 
-        assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND  c1 > 1 AND c2 > 2 AND c3 > 2 AND v = 3 ALLOW FILTERING;"),
-                   row(1, 3, 3, 3, 3));
+            assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND  c1 > 1 AND c2 > 2 AND c3 > 2 AND v = 3 ALLOW FILTERING;"),
+                       row(1, 3, 3, 3, 3));
 
-        assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND  c1 > 1 AND c2 > 2 AND c3 = 3 AND v = 3 ALLOW FILTERING;"),
-                   row(1, 3, 3, 3, 3));
+            assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND  c1 > 1 AND c2 > 2 AND c3 = 3 AND v = 3 ALLOW FILTERING;"),
+                       row(1, 3, 3, 3, 3));
 
-        assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND  c1 IN(0,1,2) AND c2 = 1 AND v = 3 ALLOW FILTERING;"),
-                   row(1, 1, 1, 3, 3));
+            assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND  c1 IN(0,1,2) AND c2 = 1 AND v = 3 ALLOW FILTERING;"),
+                       row(1, 1, 1, 3, 3));
 
-        assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND  c1 IN(0,1,2) AND c2 = 1 AND v = 3"),
-                   row(1, 1, 1, 3, 3));
+            assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND  c1 IN(0,1,2) AND c2 = 1 AND v = 3"),
+                       row(1, 1, 1, 3, 3));
+        });
+    }
 
-        assertInvalidMessage("Clustering column \"c2\" cannot be restricted (preceding column \"c1\" is restricted by a non-EQ relation)",
-                             "SELECT * FROM %s WHERE pk = 1 AND  c1 > 0 AND c1 < 5 AND c2 = 1 ALLOW FILTERING;");
+    @Test
+    public void testIndexQueryWithCompositePartitionKey() throws Throwable
+    {
+        createTable("CREATE TABLE %s (p1 int, p2 int, v int, PRIMARY KEY ((p1, p2)))");
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
+                             "SELECT * FROM %s WHERE p1 = 1 AND v = 3");
+        createIndex("CREATE INDEX ON %s(v)");
 
-        assertInvalidMessage("PRIMARY KEY column \"c2\" cannot be restricted as preceding column \"c1\" is not restricted",
-                             "SELECT * FROM %s WHERE pk = 1 AND  c2 = 1 ALLOW FILTERING;");
+        execute("INSERT INTO %s(p1, p2, v) values (?, ?, ?)", 1, 1, 3);
+        execute("INSERT INTO %s(p1, p2, v) values (?, ?, ?)", 1, 2, 3);
+        execute("INSERT INTO %s(p1, p2, v) values (?, ?, ?)", 2, 1, 3);
+
+        beforeAndAfterFlush(() -> {
+            assertRows(execute("SELECT * FROM %s WHERE p1 = 1 AND v = 3 ALLOW FILTERING"),
+                       row(1, 2, 3),
+                       row(1, 1, 3));
+        });
     }
 
     @Test
@@ -2767,6 +4375,170 @@
     }
 
     @Test
+    public void testFilteringOnDurationColumn() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, d duration)");
+        execute("INSERT INTO %s (k, d) VALUES (0, 1s)");
+        execute("INSERT INTO %s (k, d) VALUES (1, 2s)");
+        execute("INSERT INTO %s (k, d) VALUES (2, 1s)");
+
+        assertRows(execute("SELECT * FROM %s WHERE d=1s ALLOW FILTERING"),
+                   row(0, Duration.from("1s")),
+                   row(2, Duration.from("1s")));
+
+        assertInvalidMessage("IN predicates on non-primary-key columns (d) is not yet supported",
+                             "SELECT * FROM %s WHERE d IN (1s, 2s) ALLOW FILTERING");
+
+        assertInvalidMessage("Slice restrictions are not supported on duration columns",
+                             "SELECT * FROM %s WHERE d > 1s ALLOW FILTERING");
+
+        assertInvalidMessage("Slice restrictions are not supported on duration columns",
+                             "SELECT * FROM %s WHERE d >= 1s ALLOW FILTERING");
+
+        assertInvalidMessage("Slice restrictions are not supported on duration columns",
+                             "SELECT * FROM %s WHERE d <= 1s ALLOW FILTERING");
+
+        assertInvalidMessage("Slice restrictions are not supported on duration columns",
+                             "SELECT * FROM %s WHERE d < 1s ALLOW FILTERING");
+    }
+
+    @Test
+    public void testFilteringOnListContainingDurations() throws Throwable
+    {
+        for (Boolean frozen : new Boolean[]{Boolean.FALSE, Boolean.TRUE})
+        {
+            String listType = String.format(frozen ? "frozen<%s>" : "%s", "list<duration>");
+
+            createTable("CREATE TABLE %s (k int PRIMARY KEY, l " + listType + ")");
+            execute("INSERT INTO %s (k, l) VALUES (0, [1s, 2s])");
+            execute("INSERT INTO %s (k, l) VALUES (1, [2s, 3s])");
+            execute("INSERT INTO %s (k, l) VALUES (2, [1s, 3s])");
+
+            if (frozen)
+                assertRows(execute("SELECT * FROM %s WHERE l = [1s, 2s] ALLOW FILTERING"),
+                           row(0, list(Duration.from("1s"), Duration.from("2s"))));
+
+            assertInvalidMessage("IN predicates on non-primary-key columns (l) is not yet supported",
+                                 "SELECT * FROM %s WHERE l IN ([1s, 2s], [2s, 3s]) ALLOW FILTERING");
+
+            assertInvalidMessage("Slice restrictions are not supported on collections containing durations",
+                                 "SELECT * FROM %s WHERE l > [2s, 3s] ALLOW FILTERING");
+
+            assertInvalidMessage("Slice restrictions are not supported on collections containing durations",
+                                 "SELECT * FROM %s WHERE l >= [2s, 3s] ALLOW FILTERING");
+
+            assertInvalidMessage("Slice restrictions are not supported on collections containing durations",
+                                 "SELECT * FROM %s WHERE l <= [2s, 3s] ALLOW FILTERING");
+
+            assertInvalidMessage("Slice restrictions are not supported on collections containing durations",
+                                 "SELECT * FROM %s WHERE l < [2s, 3s] ALLOW FILTERING");
+
+            assertRows(execute("SELECT * FROM %s WHERE l CONTAINS 1s ALLOW FILTERING"),
+                       row(0, list(Duration.from("1s"), Duration.from("2s"))),
+                       row(2, list(Duration.from("1s"), Duration.from("3s"))));
+        }
+    }
+
+    @Test
+    public void testFilteringOnMapContainingDurations() throws Throwable
+    {
+        for (Boolean frozen : new Boolean[]{Boolean.FALSE, Boolean.TRUE})
+        {
+            String mapType = String.format(frozen ? "frozen<%s>" : "%s", "map<int, duration>");
+
+            createTable("CREATE TABLE %s (k int PRIMARY KEY, m " + mapType + ")");
+            execute("INSERT INTO %s (k, m) VALUES (0, {1:1s, 2:2s})");
+            execute("INSERT INTO %s (k, m) VALUES (1, {2:2s, 3:3s})");
+            execute("INSERT INTO %s (k, m) VALUES (2, {1:1s, 3:3s})");
+
+            if (frozen)
+                assertRows(execute("SELECT * FROM %s WHERE m = {1:1s, 2:2s} ALLOW FILTERING"),
+                           row(0, map(1, Duration.from("1s"), 2, Duration.from("2s"))));
+
+            assertInvalidMessage("IN predicates on non-primary-key columns (m) is not yet supported",
+                    "SELECT * FROM %s WHERE m IN ({1:1s, 2:2s}, {1:1s, 3:3s}) ALLOW FILTERING");
+
+            assertInvalidMessage("Slice restrictions are not supported on collections containing durations",
+                    "SELECT * FROM %s WHERE m > {1:1s, 3:3s} ALLOW FILTERING");
+
+            assertInvalidMessage("Slice restrictions are not supported on collections containing durations",
+                    "SELECT * FROM %s WHERE m >= {1:1s, 3:3s} ALLOW FILTERING");
+
+            assertInvalidMessage("Slice restrictions are not supported on collections containing durations",
+                    "SELECT * FROM %s WHERE m <= {1:1s, 3:3s} ALLOW FILTERING");
+
+            assertInvalidMessage("Slice restrictions are not supported on collections containing durations",
+                    "SELECT * FROM %s WHERE m < {1:1s, 3:3s} ALLOW FILTERING");
+
+            assertRows(execute("SELECT * FROM %s WHERE m CONTAINS 1s ALLOW FILTERING"),
+                       row(0, map(1, Duration.from("1s"), 2, Duration.from("2s"))),
+                       row(2, map(1, Duration.from("1s"), 3, Duration.from("3s"))));
+        }
+    }
+
+    @Test
+    public void testFilteringOnTupleContainingDurations() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, t tuple<int, duration>)");
+        execute("INSERT INTO %s (k, t) VALUES (0, (1, 2s))");
+        execute("INSERT INTO %s (k, t) VALUES (1, (2, 3s))");
+        execute("INSERT INTO %s (k, t) VALUES (2, (1, 3s))");
+
+        assertRows(execute("SELECT * FROM %s WHERE t = (1, 2s) ALLOW FILTERING"),
+                   row(0, tuple(1, Duration.from("2s"))));
+
+        assertInvalidMessage("IN predicates on non-primary-key columns (t) is not yet supported",
+                "SELECT * FROM %s WHERE t IN ((1, 2s), (1, 3s)) ALLOW FILTERING");
+
+        assertInvalidMessage("Slice restrictions are not supported on tuples containing durations",
+                "SELECT * FROM %s WHERE t > (1, 2s) ALLOW FILTERING");
+
+        assertInvalidMessage("Slice restrictions are not supported on tuples containing durations",
+                "SELECT * FROM %s WHERE t >= (1, 2s) ALLOW FILTERING");
+
+        assertInvalidMessage("Slice restrictions are not supported on tuples containing durations",
+                "SELECT * FROM %s WHERE t <= (1, 2s) ALLOW FILTERING");
+
+        assertInvalidMessage("Slice restrictions are not supported on tuples containing durations",
+                "SELECT * FROM %s WHERE t < (1, 2s) ALLOW FILTERING");
+    }
+
+    @Test
+    public void testFilteringOnUdtContainingDurations() throws Throwable
+    {
+        String udt = createType("CREATE TYPE %s (i int, d duration)");
+
+        for (Boolean frozen : new Boolean[]{Boolean.FALSE, Boolean.TRUE})
+        {
+            udt = String.format(frozen ? "frozen<%s>" : "%s", udt);
+
+            createTable("CREATE TABLE %s (k int PRIMARY KEY, u " + udt + ")");
+            execute("INSERT INTO %s (k, u) VALUES (0, {i: 1, d:2s})");
+            execute("INSERT INTO %s (k, u) VALUES (1, {i: 2, d:3s})");
+            execute("INSERT INTO %s (k, u) VALUES (2, {i: 1, d:3s})");
+
+            if (frozen)
+                assertRows(execute("SELECT * FROM %s WHERE u = {i: 1, d:2s} ALLOW FILTERING"),
+                           row(0, userType("i", 1, "d", Duration.from("2s"))));
+
+            assertInvalidMessage("IN predicates on non-primary-key columns (u) is not yet supported",
+                    "SELECT * FROM %s WHERE u IN ({i: 2, d:3s}, {i: 1, d:3s}) ALLOW FILTERING");
+
+            assertInvalidMessage("Slice restrictions are not supported on UDTs containing durations",
+                    "SELECT * FROM %s WHERE u > {i: 1, d:3s} ALLOW FILTERING");
+
+            assertInvalidMessage("Slice restrictions are not supported on UDTs containing durations",
+                    "SELECT * FROM %s WHERE u >= {i: 1, d:3s} ALLOW FILTERING");
+
+            assertInvalidMessage("Slice restrictions are not supported on UDTs containing durations",
+                    "SELECT * FROM %s WHERE u <= {i: 1, d:3s} ALLOW FILTERING");
+
+            assertInvalidMessage("Slice restrictions are not supported on UDTs containing durations",
+                    "SELECT * FROM %s WHERE u < {i: 1, d:3s} ALLOW FILTERING");
+        }
+    }
+
+    @Test
     public void testFilteringOnCollectionsWithNull() throws Throwable
     {
         createTable(" CREATE TABLE %s ( k int, v int, l list<int>, s set<text>, m map<text, int>, PRIMARY KEY (k, v))");
@@ -2886,7 +4658,7 @@
         assertRows(execute("SELECT a, b, c, column1 FROM %s"),
                    row(1, 1, 1, 1),
                    row(2, 1, 1, 2));
-        assertInvalidMessage("Undefined name column2 in selection clause",
+        assertInvalidMessage("Undefined column name column2",
                              "SELECT a, column2, value FROM %s");
 
         // if column value is present, hidden column is called value1
@@ -2896,7 +4668,7 @@
         assertRows(execute("SELECT a, b, c, value FROM %s"),
                    row(1, 1, 1, 1),
                    row(2, 1, 1, 2));
-        assertInvalidMessage("Undefined name value1 in selection clause",
+        assertInvalidMessage("Undefined column name value1",
                              "SELECT a, value1, value FROM %s");
     }
 
@@ -2925,23 +4697,23 @@
 
     private void testWithCompactFormat() throws Throwable
     {
-        assertInvalidMessage("Order by on unknown column value",
+        assertInvalidMessage("Undefined column name value",
                              "SELECT * FROM %s WHERE a IN (1,2,3) ORDER BY value ASC");
-        assertInvalidMessage("Order by on unknown column column1",
+        assertInvalidMessage("Undefined column name column1",
                              "SELECT * FROM %s WHERE a IN (1,2,3) ORDER BY column1 ASC");
-        assertInvalidMessage("Undefined name column1 in selection clause",
+        assertInvalidMessage("Undefined column name column1",
                              "SELECT column1 FROM %s");
-        assertInvalidMessage("Undefined name value in selection clause",
+        assertInvalidMessage("Undefined column name value",
                              "SELECT value FROM %s");
-        assertInvalidMessage("Undefined name value in selection clause",
+        assertInvalidMessage("Undefined column name value",
                              "SELECT value, column1 FROM %s");
-        assertInvalid("Undefined name column1 in where clause ('column1 = NULL')",
+        assertInvalid("Undefined column name column1",
                       "SELECT * FROM %s WHERE column1 = null ALLOW FILTERING");
-        assertInvalid("Undefined name value in where clause ('value = NULL')",
+        assertInvalid("Undefined column name value",
                       "SELECT * FROM %s WHERE value = null ALLOW FILTERING");
-        assertInvalidMessage("Undefined name column1 in selection clause",
+        assertInvalidMessage("Undefined column name column1",
                              "SELECT WRITETIME(column1) FROM %s");
-        assertInvalidMessage("Undefined name value in selection clause",
+        assertInvalidMessage("Undefined column name value",
                              "SELECT WRITETIME(value) FROM %s");
     }
 }
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/TuplesWithNullsComparisonTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/TuplesWithNullsComparisonTest.java
new file mode 100644
index 0000000..5a1bc69
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/TuplesWithNullsComparisonTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.validation.operations;
+
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+
+public class TuplesWithNullsComparisonTest extends CQLTester
+{
+    @Test
+    public void testAddUDTField() throws Throwable
+    {
+        String typename = createType("create type %s (foo text);");
+        createTable("create table %s (pk int, ck frozen<" + typename + ">, v int, primary key(pk, ck));");
+        execute("insert into %s (pk, ck, v) values (0, system.fromjson('{\"foo\": \"foo\"}'), 0);");
+        execute("ALTER TYPE " + KEYSPACE + '.' + typename + " ADD bar text;");
+        execute("insert into %s (pk, ck, v) values (0, system.fromjson('{\"foo\": \"foo\"}'), 1);");
+        execute("insert into %s (pk, ck, v) values (0, system.fromjson('{\"foo\": \"foo\", \"bar\": null}'), 2);");
+        flush();
+        compact();
+        assertRows(execute("select v from %s where pk = 0 and ck=system.fromjson('{\"foo\": \"foo\"}')"),
+                   row(2));
+        assertRows(execute("select v from %s where pk = 0"),
+                   row(2));
+    }
+
+    @Test
+    public void testFieldWithData() throws Throwable
+    {
+        String typename = createType("create type %s (foo text);");
+        createTable("create table %s (pk int, ck frozen<" + typename + ">, v int, primary key(pk, ck));");
+        execute("insert into %s (pk, ck, v) values (0, system.fromjson('{\"foo\": \"foo\"}'), 1);");
+        execute("ALTER TYPE " + KEYSPACE + '.' + typename + " ADD bar text;");
+        // this row becomes inaccessible by primary key but remains visible through select *
+        execute("insert into %s (pk, ck, v) values (0, system.fromjson('{\"foo\": \"foo\", \"bar\": \"bar\"}'), 2);");
+        flush();
+        compact();
+        assertRows(execute("select v from %s where pk = 0"),
+                   row(1),
+                   row(2));
+    }
+
+    @Test
+    public void testAddUDTFields() throws Throwable
+    {
+        String typename = createType("create type %s (foo text);");
+        createTable("create table %s (pk int, ck frozen<" + typename + ">, v int, primary key(pk, ck));");
+        execute("insert into %s (pk, ck, v) values (0, system.fromjson('{\"foo\": \"foo\"}'), 0);");
+        execute("ALTER TYPE " + KEYSPACE + '.' + typename + " ADD bar text;");
+        execute("ALTER TYPE " + KEYSPACE + '.' + typename + " ADD bar2 text;");
+        execute("ALTER TYPE " + KEYSPACE + '.' + typename + " ADD bar3 text;");
+        execute("insert into %s (pk, ck, v) values (0, system.fromjson('{\"foo\": \"foo\"}'), 1);");
+        execute("insert into %s (pk, ck, v) values (0, system.fromjson('{\"foo\": \"foo\", \"bar\": null, \"bar2\": null, \"bar3\": null}'), 2);");
+        flush();
+        compact();
+        assertRows(execute("select v from %s where pk = 0 and ck=system.fromjson('{\"foo\": \"foo\"}')"),
+                   row(2));
+        assertRows(execute("select v from %s where pk = 0"),
+                   row(2));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/UpdateTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/UpdateTest.java
index 8cc0c8c..340155f 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/UpdateTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/UpdateTest.java
@@ -22,14 +22,18 @@
 
 import org.junit.Test;
 
-import static org.apache.commons.lang3.StringUtils.isEmpty;
-import static org.junit.Assert.assertTrue;
-
+import org.apache.cassandra.cql3.Attributes;
 import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.UntypedResultSet.Row;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
+import static org.apache.commons.lang3.StringUtils.isEmpty;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
 public class UpdateTest extends CQLTester
 {
     @Test
@@ -165,13 +169,13 @@
                                  "UPDATE %s SET value = ? WHERE partitionKey = ? AND clustering_1 = ? AND clustering_1 = ?", 7, 0, 1, 1);
 
             // unknown identifiers
-            assertInvalidMessage("Unknown identifier value1",
+            assertInvalidMessage("Undefined column name value1",
                                  "UPDATE %s SET value1 = ? WHERE partitionKey = ? AND clustering_1 = ?", 7, 0, 1);
 
-            assertInvalidMessage("Undefined name partitionkey1 in where clause ('partitionkey1 = ?')",
+            assertInvalidMessage("Undefined column name partitionkey1",
                                  "UPDATE %s SET value = ? WHERE partitionKey1 = ? AND clustering_1 = ?", 7, 0, 1);
 
-            assertInvalidMessage("Undefined name clustering_3 in where clause ('clustering_3 = ?')",
+            assertInvalidMessage("Undefined column name clustering_3",
                                  "UPDATE %s SET value = ? WHERE partitionKey = ? AND clustering_3 = ?", 7, 0, 1);
 
             // Invalid operator in the where clause
@@ -373,13 +377,13 @@
                                  "UPDATE %s SET value = ? WHERE partitionKey = ? AND clustering_1 = ? AND clustering_2 = ? AND clustering_1 = ?", 7, 0, 1, 1, 1);
 
             // unknown identifiers
-            assertInvalidMessage("Unknown identifier value1",
+            assertInvalidMessage("Undefined column name value1",
                                  "UPDATE %s SET value1 = ? WHERE partitionKey = ? AND clustering_1 = ? AND clustering_2 = ?", 7, 0, 1, 1);
 
-            assertInvalidMessage("Undefined name partitionkey1 in where clause ('partitionkey1 = ?')",
+            assertInvalidMessage("Undefined column name partitionkey1",
                                  "UPDATE %s SET value = ? WHERE partitionKey1 = ? AND clustering_1 = ? AND clustering_2 = ?", 7, 0, 1, 1);
 
-            assertInvalidMessage("Undefined name clustering_3 in where clause ('clustering_3 = ?')",
+            assertInvalidMessage("Undefined column name clustering_3",
                                  "UPDATE %s SET value = ? WHERE partitionKey = ? AND clustering_1 = ? AND clustering_3 = ?", 7, 0, 1, 1);
 
             // Invalid operator in the where clause
@@ -514,6 +518,62 @@
         assertRows(execute("SELECT l FROM %s WHERE k = 0"), row(list("v1", "v4", "v3")));
     }
 
+    @Test
+    public void testUpdateWithDefaultTtl() throws Throwable
+    {
+        final int secondsPerMinute = 60;
+        createTable("CREATE TABLE %s (a int PRIMARY KEY, b int) WITH default_time_to_live = " + (10 * secondsPerMinute));
+
+        execute("UPDATE %s SET b = 1 WHERE a = 1");
+        UntypedResultSet resultSet = execute("SELECT ttl(b) FROM %s WHERE a = 1");
+        assertEquals(1, resultSet.size());
+        Row row = resultSet.one();
+        assertTrue(row.getInt("ttl(b)") >= (9 * secondsPerMinute));
+
+        execute("UPDATE %s USING TTL ? SET b = 3 WHERE a = 1", 0);
+        assertRows(execute("SELECT ttl(b) FROM %s WHERE a = 1"), row(new Object[]{null}));
+
+        execute("UPDATE %s SET b = 3 WHERE a = 1");
+        resultSet = execute("SELECT ttl(b) FROM %s WHERE a = 1");
+        assertEquals(1, resultSet.size());
+        row = resultSet.one();
+        assertTrue(row.getInt("ttl(b)") >= (9 * secondsPerMinute));
+
+        execute("UPDATE %s USING TTL ? SET b = 2 WHERE a = 2", unset());
+        resultSet = execute("SELECT ttl(b) FROM %s WHERE a = 2");
+        assertEquals(1, resultSet.size());
+        row = resultSet.one();
+        assertTrue(row.getInt("ttl(b)") >= (9 * secondsPerMinute));
+
+        execute("UPDATE %s USING TTL ? SET b = ? WHERE a = ?", null, 3, 3);
+        assertRows(execute("SELECT ttl(b) FROM %s WHERE a = 3"), row(new Object[] { null }));
+    }
+
+    @Test
+    public void testUpdateWithTtl() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)");
+
+        execute("INSERT INTO %s (k, v) VALUES (1, 1) USING TTL ?", 3600);
+        execute("INSERT INTO %s (k, v) VALUES (2, 2) USING TTL ?", 3600);
+
+        // test with unset
+        execute("UPDATE %s USING TTL ? SET v = ? WHERE k = ?", unset(), 1, 1); // treat as 'unlimited'
+        assertRows(execute("SELECT ttl(v) FROM %s WHERE k = 1"), row(new Object[] { null }));
+
+        // test with null
+        execute("UPDATE %s USING TTL ? SET v = ? WHERE k = ?", unset(), 2, 2);
+        assertRows(execute("SELECT k, v, TTL(v) FROM %s WHERE k = 2"), row(2, 2, null));
+
+        // test error handling
+        assertInvalidMessage("A TTL must be greater or equal to 0, but was -5",
+                             "UPDATE %s USING TTL ? SET v = ? WHERE k = ?", -5, 1, 1);
+
+        assertInvalidMessage("ttl is too large.",
+                             "UPDATE %s USING TTL ? SET v = ? WHERE k = ?",
+                             Attributes.MAX_TTL + 1, 1, 1);
+    }
+
     /**
      * Test for CASSANDRA-12829
      */
@@ -638,10 +698,10 @@
     {
         testWithCompactFormat("CREATE TABLE %s (a int PRIMARY KEY, b int, c int) WITH COMPACT STORAGE");
 
-        assertInvalidMessage("Undefined name column1 in where clause ('column1 = ?')",
+        assertInvalidMessage("Undefined column name column1",
                              "UPDATE %s SET b = 1 WHERE column1 = ?",
                              ByteBufferUtil.bytes('a'));
-        assertInvalidMessage("Undefined name value in where clause ('value = ?')",
+        assertInvalidMessage("Undefined column name value",
                              "UPDATE %s SET b = 1 WHERE value = ?",
                              ByteBufferUtil.bytes('a'));
 
@@ -649,15 +709,15 @@
         createTable("CREATE TABLE %s (a int PRIMARY KEY, b int, c int, column1 int) WITH COMPACT STORAGE");
         execute("INSERT INTO %s (a, b, c, column1) VALUES (1, 1, 1, 1)");
         execute("UPDATE %s SET column1 = 6 WHERE a = 1");
-        assertInvalidMessage("Unknown identifier column2", "UPDATE %s SET column2 = 6 WHERE a = 0");
-        assertInvalidMessage("Unknown identifier value", "UPDATE %s SET value = 6 WHERE a = 0");
+        assertInvalidMessage("Undefined column name column2", "UPDATE %s SET column2 = 6 WHERE a = 0");
+        assertInvalidMessage("Undefined column name value", "UPDATE %s SET value = 6 WHERE a = 0");
 
         // if value is present, hidden column is called value1
         createTable("CREATE TABLE %s (a int PRIMARY KEY, b int, c int, value int) WITH COMPACT STORAGE");
         execute("INSERT INTO %s (a, b, c, value) VALUES (1, 1, 1, 1)");
         execute("UPDATE %s SET value = 6 WHERE a = 1");
-        assertInvalidMessage("Unknown identifier column1", "UPDATE %s SET column1 = 6 WHERE a = 1");
-        assertInvalidMessage("Unknown identifier value1", "UPDATE %s SET value1 = 6 WHERE a = 1");
+        assertInvalidMessage("Undefined column name column1", "UPDATE %s SET column1 = 6 WHERE a = 1");
+        assertInvalidMessage("Undefined column name value1", "UPDATE %s SET value1 = 6 WHERE a = 1");
     }
 
     /**
@@ -674,15 +734,15 @@
     {
         createTable(tableQuery);
         // pass correct types to hidden columns
-        assertInvalidMessage("Unknown identifier column1",
+        assertInvalidMessage("Undefined column name column1",
                              "UPDATE %s SET column1 = ? WHERE a = 0",
                              ByteBufferUtil.bytes('a'));
-        assertInvalidMessage("Unknown identifier value",
+        assertInvalidMessage("Undefined column name value",
                              "UPDATE %s SET value = ? WHERE a = 0",
                              ByteBufferUtil.bytes('a'));
 
         // pass incorrect types to hidden columns
-        assertInvalidMessage("Unknown identifier column1", "UPDATE %s SET column1 = 6 WHERE a = 0");
-        assertInvalidMessage("Unknown identifier value", "UPDATE %s SET value = 6 WHERE a = 0");
+        assertInvalidMessage("Undefined column name column1", "UPDATE %s SET column1 = 6 WHERE a = 0");
+        assertInvalidMessage("Undefined column name value", "UPDATE %s SET value = 6 WHERE a = 0");
     }
 }
diff --git a/test/unit/org/apache/cassandra/db/CellTest.java b/test/unit/org/apache/cassandra/db/CellTest.java
index 22f1b78..c68b4ec 100644
--- a/test/unit/org/apache/cassandra/db/CellTest.java
+++ b/test/unit/org/apache/cassandra/db/CellTest.java
@@ -6,9 +6,9 @@
  * 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
@@ -29,7 +29,9 @@
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.FieldIdentifier;
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.exceptions.ConfigurationException;
@@ -39,8 +41,15 @@
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
 
+import static java.util.Arrays.*;
+
 public class CellTest
 {
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     private static final String KEYSPACE1 = "CellTest";
     private static final String CF_STANDARD1 = "Standard1";
     private static final String CF_COLLECTION = "Collection1";
@@ -53,8 +62,6 @@
                                                              .addRegularColumn("m", MapType.getInstance(IntegerType.instance, IntegerType.instance, true))
                                                              .build();
 
-    private static final CFMetaData fakeMetadata = CFMetaData.createFake("fakeKS", "fakeTable");
-
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
@@ -64,8 +71,8 @@
 
     private static ColumnDefinition fakeColumn(String name, AbstractType<?> type)
     {
-        return new ColumnDefinition(fakeMetadata.ksName,
-                                    fakeMetadata.cfName,
+        return new ColumnDefinition("fakeKs",
+                                    "fakeTable",
                                     ColumnIdentifier.getInterned(name, false),
                                     type,
                                     ColumnDefinition.NO_POSITION,
@@ -127,8 +134,8 @@
 
         // Valid cells
         c = fakeColumn("c", Int32Type.instance);
-        assertValid(BufferCell.live(fakeMetadata, c, 0, ByteBufferUtil.EMPTY_BYTE_BUFFER));
-        assertValid(BufferCell.live(fakeMetadata, c, 0, ByteBufferUtil.bytes(4)));
+        assertValid(BufferCell.live(c, 0, ByteBufferUtil.EMPTY_BYTE_BUFFER));
+        assertValid(BufferCell.live(c, 0, ByteBufferUtil.bytes(4)));
 
         assertValid(BufferCell.expiring(c, 0, 4, 4, ByteBufferUtil.EMPTY_BYTE_BUFFER));
         assertValid(BufferCell.expiring(c, 0, 4, 4, ByteBufferUtil.bytes(4)));
@@ -137,23 +144,92 @@
 
         // Invalid value (we don't all empty values for smallint)
         c = fakeColumn("c", ShortType.instance);
-        assertInvalid(BufferCell.live(fakeMetadata, c, 0, ByteBufferUtil.EMPTY_BYTE_BUFFER));
+        assertInvalid(BufferCell.live(c, 0, ByteBufferUtil.EMPTY_BYTE_BUFFER));
         // But this should be valid even though the underlying value is an empty BB (catches bug #11618)
         assertValid(BufferCell.tombstone(c, 0, 4));
         // And of course, this should be valid with a proper value
-        assertValid(BufferCell.live(fakeMetadata, c, 0, ByteBufferUtil.bytes((short)4)));
+        assertValid(BufferCell.live(c, 0, bbs(4)));
 
         // Invalid ttl
-        assertInvalid(BufferCell.expiring(c, 0, -4, 4, ByteBufferUtil.bytes(4)));
-        // Invalid local deletion times
-        assertInvalid(BufferCell.expiring(c, 0, 4, -4, ByteBufferUtil.bytes(4)));
-        assertInvalid(BufferCell.expiring(c, 0, 4, Cell.NO_DELETION_TIME, ByteBufferUtil.bytes(4)));
+        assertInvalid(BufferCell.expiring(c, 0, -4, 4, bbs(4)));
+        // Cells with overflowed localExpirationTime are valid after CASSANDRA-14092
+        assertValid(BufferCell.expiring(c, 0, 4, -5, bbs(4)));
+        assertValid(BufferCell.expiring(c, 0, 4, Cell.NO_DELETION_TIME, bbs(4)));
 
         c = fakeColumn("c", MapType.getInstance(Int32Type.instance, Int32Type.instance, true));
         // Valid cell path
-        assertValid(BufferCell.live(fakeMetadata, c, 0, ByteBufferUtil.bytes(4), CellPath.create(ByteBufferUtil.bytes(4))));
+        assertValid(BufferCell.live(c, 0, ByteBufferUtil.bytes(4), CellPath.create(ByteBufferUtil.bytes(4))));
         // Invalid cell path (int values should be 0 or 4 bytes)
-        assertInvalid(BufferCell.live(fakeMetadata, c, 0, ByteBufferUtil.bytes(4), CellPath.create(ByteBufferUtil.bytes((long)4))));
+        assertInvalid(BufferCell.live(c, 0, ByteBufferUtil.bytes(4), CellPath.create(ByteBufferUtil.bytes((long)4))));
+    }
+
+    @Test
+    public void testValidateNonFrozenUDT()
+    {
+        FieldIdentifier f1 = field("f1");  // has field position 0
+        FieldIdentifier f2 = field("f2");  // has field position 1
+        UserType udt = new UserType("ks",
+                                    bb("myType"),
+                                    asList(f1, f2),
+                                    asList(Int32Type.instance, UTF8Type.instance),
+                                    true);
+        ColumnDefinition c;
+
+        // Valid cells
+        c = fakeColumn("c", udt);
+        assertValid(BufferCell.live(c, 0, bb(1), CellPath.create(bbs(0))));
+        assertValid(BufferCell.live(c, 0, bb("foo"), CellPath.create(bbs(1))));
+        assertValid(BufferCell.expiring(c, 0, 4, 4, bb(1), CellPath.create(bbs(0))));
+        assertValid(BufferCell.expiring(c, 0, 4, 4, bb("foo"), CellPath.create(bbs(1))));
+        assertValid(BufferCell.tombstone(c, 0, 4, CellPath.create(bbs(0))));
+
+        // Invalid value (text in an int field)
+        assertInvalid(BufferCell.live(c, 0, bb("foo"), CellPath.create(bbs(0))));
+
+        // Invalid ttl
+        assertInvalid(BufferCell.expiring(c, 0, -4, 4, bb(1), CellPath.create(bbs(0))));
+        // Cells with overflowed localExpirationTime are valid after CASSANDRA-14092
+        assertValid(BufferCell.expiring(c, 0, 4, -5, bb(1), CellPath.create(bbs(0))));
+        assertValid((BufferCell.expiring(c, 0, 4, Cell.NO_DELETION_TIME, bb(1), CellPath.create(bbs(0)))));
+
+        // Invalid cell path (int values should be 0 or 2 bytes)
+        assertInvalid(BufferCell.live(c, 0, bb(1), CellPath.create(ByteBufferUtil.bytes((long)4))));
+    }
+
+    @Test
+    public void testValidateFrozenUDT()
+    {
+        FieldIdentifier f1 = field("f1");  // has field position 0
+        FieldIdentifier f2 = field("f2");  // has field position 1
+        UserType udt = new UserType("ks",
+                                    bb("myType"),
+                                    asList(f1, f2),
+                                    asList(Int32Type.instance, UTF8Type.instance),
+                                    false);
+
+        ColumnDefinition c = fakeColumn("c", udt);
+        ByteBuffer val = udt(bb(1), bb("foo"));
+
+        // Valid cells
+        assertValid(BufferCell.live(c, 0, val));
+        assertValid(BufferCell.live(c, 0, val));
+        assertValid(BufferCell.expiring(c, 0, 4, 4, val));
+        assertValid(BufferCell.expiring(c, 0, 4, 4, val));
+        assertValid(BufferCell.tombstone(c, 0, 4));
+        // fewer values than types is accepted
+        assertValid(BufferCell.live(c, 0, udt(bb(1))));
+
+        // Invalid values
+        // invalid types
+        assertInvalid(BufferCell.live(c, 0, udt(bb("foo"), bb(1))));
+        // too many types
+        assertInvalid(BufferCell.live(c, 0, udt(bb(1), bb("foo"), bb("bar"))));
+
+        // Invalid ttl
+        assertInvalid(BufferCell.expiring(c, 0, -4, 4, val));
+        // Cells with overflowed localExpirationTime are valid after CASSANDRA-14092
+        assertValid(BufferCell.expiring(c, 0, 4, -5, val));
+        assertValid(BufferCell.expiring(c, 0, 4, Cell.NO_DELETION_TIME, val));
     }
 
     @Test
@@ -250,6 +326,26 @@
         return ByteBufferUtil.bytes(i);
     }
 
+    private static ByteBuffer bbs(int s)
+    {
+        return ByteBufferUtil.bytes((short) s);
+    }
+
+    private static ByteBuffer bb(String str)
+    {
+        return UTF8Type.instance.decompose(str);
+    }
+
+    private static ByteBuffer udt(ByteBuffer...buffers)
+    {
+        return UserType.buildValue(buffers);
+    }
+
+    private static FieldIdentifier field(String field)
+    {
+        return FieldIdentifier.forQuoted(field);
+    }
+
     @Test
     public void testComplexCellReconcile()
     {
@@ -258,15 +354,15 @@
         long ts1 = now1*1000000L;
 
 
-        Cell r1m1 = BufferCell.live(cfm2, m, ts1, bb(1), CellPath.create(bb(1)));
-        Cell r1m2 = BufferCell.live(cfm2, m, ts1, bb(2), CellPath.create(bb(2)));
+        Cell r1m1 = BufferCell.live(m, ts1, bb(1), CellPath.create(bb(1)));
+        Cell r1m2 = BufferCell.live(m, ts1, bb(2), CellPath.create(bb(2)));
         List<Cell> cells1 = Lists.newArrayList(r1m1, r1m2);
 
         int now2 = now1 + 1;
         long ts2 = now2*1000000L;
-        Cell r2m2 = BufferCell.live(cfm2, m, ts2, bb(1), CellPath.create(bb(2)));
-        Cell r2m3 = BufferCell.live(cfm2, m, ts2, bb(2), CellPath.create(bb(3)));
-        Cell r2m4 = BufferCell.live(cfm2, m, ts2, bb(3), CellPath.create(bb(4)));
+        Cell r2m2 = BufferCell.live(m, ts2, bb(1), CellPath.create(bb(2)));
+        Cell r2m3 = BufferCell.live(m, ts2, bb(2), CellPath.create(bb(3)));
+        Cell r2m4 = BufferCell.live(m, ts2, bb(3), CellPath.create(bb(4)));
         List<Cell> cells2 = Lists.newArrayList(r2m2, r2m3, r2m4);
 
         RowBuilder builder = new RowBuilder();
@@ -296,7 +392,7 @@
     private Cell regular(CFMetaData cfm, String columnName, String value, long timestamp)
     {
         ColumnDefinition cdef = cfm.getColumnDefinition(ByteBufferUtil.bytes(columnName));
-        return BufferCell.live(cfm, cdef, timestamp, ByteBufferUtil.bytes(value));
+        return BufferCell.live(cdef, timestamp, ByteBufferUtil.bytes(value));
     }
 
     private Cell expiring(CFMetaData cfm, String columnName, String value, long timestamp, int localExpirationTime)
diff --git a/test/unit/org/apache/cassandra/db/CleanupTest.java b/test/unit/org/apache/cassandra/db/CleanupTest.java
index d4c613d..552e6d1 100644
--- a/test/unit/org/apache/cassandra/db/CleanupTest.java
+++ b/test/unit/org/apache/cassandra/db/CleanupTest.java
@@ -205,10 +205,20 @@
 
         assertEquals(0, Util.getAll(Util.cmd(cfs).build()).size());
     }
-
     @Test
     public void testCleanupWithNoTokenRange() throws Exception
     {
+        testCleanupWithNoTokenRange(false);
+    }
+
+    @Test
+    public void testUserDefinedCleanupWithNoTokenRange() throws Exception
+    {
+        testCleanupWithNoTokenRange(true);
+    }
+
+    private void testCleanupWithNoTokenRange(boolean isUserDefined) throws Exception
+    {
 
         TokenMetadata tmd = StorageService.instance.getTokenMetadata();
         tmd.clearUnsafe();
@@ -229,8 +239,15 @@
         keyspace.setMetadata(KeyspaceMetadata.create(KEYSPACE2, KeyspaceParams.nts("DC1", 0)));
 
         // clear token range for localhost on DC1
-
-        CompactionManager.instance.performCleanup(cfs, 2);
+        if (isUserDefined)
+        {
+            for (SSTableReader r : cfs.getLiveSSTables())
+                CompactionManager.instance.forceUserDefinedCleanup(r.getFilename());
+        }
+        else
+        {
+            CompactionManager.instance.performCleanup(cfs, 2);
+        }
         assertEquals(0, Util.getAll(Util.cmd(cfs).build()).size());
         assertTrue(cfs.getLiveSSTables().isEmpty());
     }
@@ -271,6 +288,32 @@
 
 
     @Test
+    public void testuserDefinedCleanupWithNewToken() throws ExecutionException, InterruptedException, UnknownHostException
+    {
+        StorageService.instance.getTokenMetadata().clearUnsafe();
+
+        Keyspace keyspace = Keyspace.open(KEYSPACE1);
+        ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF_STANDARD1);
+
+        // insert data and verify we get it back w/ range query
+        fillCF(cfs, "val", LOOPS);
+
+        assertEquals(LOOPS, Util.getAll(Util.cmd(cfs).build()).size());
+        TokenMetadata tmd = StorageService.instance.getTokenMetadata();
+
+        byte[] tk1 = new byte[1], tk2 = new byte[1];
+        tk1[0] = 2;
+        tk2[0] = 1;
+        tmd.updateNormalToken(new BytesToken(tk1), InetAddress.getByName("127.0.0.1"));
+        tmd.updateNormalToken(new BytesToken(tk2), InetAddress.getByName("127.0.0.2"));
+
+        for(SSTableReader r: cfs.getLiveSSTables())
+            CompactionManager.instance.forceUserDefinedCleanup(r.getFilename());
+
+        assertEquals(0, Util.getAll(Util.cmd(cfs).build()).size());
+    }
+
+    @Test
     public void testNeedsCleanup() throws Exception
     {
         // setup
diff --git a/test/unit/org/apache/cassandra/db/ColumnFamilyStoreCQLHelperTest.java b/test/unit/org/apache/cassandra/db/ColumnFamilyStoreCQLHelperTest.java
index 9dac79b..a592936 100644
--- a/test/unit/org/apache/cassandra/db/ColumnFamilyStoreCQLHelperTest.java
+++ b/test/unit/org/apache/cassandra/db/ColumnFamilyStoreCQLHelperTest.java
@@ -36,6 +36,7 @@
 import org.apache.cassandra.cql3.statements.*;
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.exceptions.*;
+import org.apache.cassandra.index.sasi.*;
 import org.apache.cassandra.schema.*;
 import org.apache.cassandra.utils.*;
 import org.json.simple.JSONArray;
@@ -60,28 +61,31 @@
         String table = "test_table_user_types";
 
         UserType typeA = new UserType(keyspace, ByteBufferUtil.bytes("a"),
-                                      Arrays.asList(ByteBufferUtil.bytes("a1"),
-                                                    ByteBufferUtil.bytes("a2"),
-                                                    ByteBufferUtil.bytes("a3")),
+                                      Arrays.asList(FieldIdentifier.forUnquoted("a1"),
+                                                    FieldIdentifier.forUnquoted("a2"),
+                                                    FieldIdentifier.forUnquoted("a3")),
                                       Arrays.asList(IntegerType.instance,
                                                     IntegerType.instance,
-                                                    IntegerType.instance));
+                                                    IntegerType.instance),
+                                      true);
 
         UserType typeB = new UserType(keyspace, ByteBufferUtil.bytes("b"),
-                                      Arrays.asList(ByteBufferUtil.bytes("b1"),
-                                                    ByteBufferUtil.bytes("b2"),
-                                                    ByteBufferUtil.bytes("b3")),
+                                      Arrays.asList(FieldIdentifier.forUnquoted("b1"),
+                                                    FieldIdentifier.forUnquoted("b2"),
+                                                    FieldIdentifier.forUnquoted("b3")),
                                       Arrays.asList(typeA,
                                                     typeA,
-                                                    typeA));
+                                                    typeA),
+                                      true);
 
         UserType typeC = new UserType(keyspace, ByteBufferUtil.bytes("c"),
-                                      Arrays.asList(ByteBufferUtil.bytes("c1"),
-                                                    ByteBufferUtil.bytes("c2"),
-                                                    ByteBufferUtil.bytes("c3")),
+                                      Arrays.asList(FieldIdentifier.forUnquoted("c1"),
+                                                    FieldIdentifier.forUnquoted("c2"),
+                                                    FieldIdentifier.forUnquoted("c3")),
                                       Arrays.asList(typeB,
                                                     typeB,
-                                                    typeB));
+                                                    typeB),
+                                      true);
 
         CFMetaData cfm = CFMetaData.Builder.create(keyspace, table)
                                            .addPartitionKey("pk1", IntegerType.instance)
@@ -99,8 +103,8 @@
         ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
 
         assertEquals(ImmutableList.of("CREATE TYPE IF NOT EXISTS cql_test_keyspace_user_types.a(a1 varint, a2 varint, a3 varint);",
-                                      "CREATE TYPE IF NOT EXISTS cql_test_keyspace_user_types.b(b1 frozen<a>, b2 frozen<a>, b3 frozen<a>);",
-                                      "CREATE TYPE IF NOT EXISTS cql_test_keyspace_user_types.c(c1 frozen<b>, c2 frozen<b>, c3 frozen<b>);"),
+                                      "CREATE TYPE IF NOT EXISTS cql_test_keyspace_user_types.b(b1 a, b2 a, b3 a);",
+                                      "CREATE TYPE IF NOT EXISTS cql_test_keyspace_user_types.c(c1 b, c2 b, c3 b);"),
                      ColumnFamilyStoreCQLHelper.getUserTypesAsCQL(cfs.metadata));
     }
 
@@ -354,6 +358,7 @@
         "\tAND caching = { 'keys': 'ALL', 'rows_per_partition': 'NONE' }\n" +
         "\tAND compaction = { 'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'sstable_size_in_mb': '1' }\n" +
         "\tAND compression = { 'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor' }\n" +
+        "\tAND cdc = false\n" +
         "\tAND extensions = { 'ext1': 0x76616c31 };"
         ));
     }
@@ -388,7 +393,15 @@
                                                                                                       IndexTarget.Type.KEYS_AND_VALUES)),
                                                             "indexName3",
                                                             IndexMetadata.Kind.COMPOSITES,
-                                                            Collections.emptyMap())));
+                                                            Collections.emptyMap()))
+                       .with(IndexMetadata.fromIndexTargets(cfm,
+                                                            Collections.singletonList(new IndexTarget(cfm.getColumnDefinition(ByteBufferUtil.bytes("reg1")).name,
+                                                                                                      IndexTarget.Type.KEYS_AND_VALUES)),
+                                                            "indexName4",
+                                                            IndexMetadata.Kind.CUSTOM,
+                                                            Collections.singletonMap(IndexTarget.CUSTOM_INDEX_OPTION_NAME,
+                                                                                     SASIIndex.class.getName()))
+                       ));
 
 
         SchemaLoader.createKeyspace(keyspace,
@@ -399,7 +412,8 @@
 
         assertEquals(ImmutableList.of("CREATE INDEX IF NOT EXISTS \"indexName\" ON cql_test_keyspace_3.test_table_3 (reg1);",
                                       "CREATE INDEX IF NOT EXISTS \"indexName2\" ON cql_test_keyspace_3.test_table_3 (reg1);",
-                                      "CREATE INDEX IF NOT EXISTS \"indexName3\" ON cql_test_keyspace_3.test_table_3 (reg1);"),
+                                      "CREATE INDEX IF NOT EXISTS \"indexName3\" ON cql_test_keyspace_3.test_table_3 (reg1);",
+                                      "CREATE CUSTOM INDEX IF NOT EXISTS \"indexName4\" ON cql_test_keyspace_3.test_table_3 (reg1) USING 'org.apache.cassandra.index.sasi.SASIIndex';"),
                      ColumnFamilyStoreCQLHelper.getIndexesAsCQL(cfs.metadata));
     }
 
@@ -417,7 +431,7 @@
                                        "pk2 ascii," +
                                        "ck1 varint," +
                                        "ck2 varint," +
-                                       "reg1 frozen<" + typeC + ">," +
+                                       "reg1 " + typeC + "," +
                                        "reg2 int," +
                                        "reg3 int," +
                                        "PRIMARY KEY ((pk1, pk2), ck1, ck2)) WITH " +
@@ -445,9 +459,9 @@
                                      "\tpk2 ascii,\n" +
                                      "\tck1 varint,\n" +
                                      "\tck2 varint,\n" +
-                                     "\treg1 frozen<" + typeC + ">,\n" +
                                      "\treg2 int,\n" +
                                      "\treg3 int,\n" +
+                                     "\treg1 " + typeC + ",\n" +
                                      "\tPRIMARY KEY ((pk1, pk2), ck1, ck2))\n" +
                                      "\tWITH ID = " + cfs.metadata.cfId + "\n" +
                                      "\tAND CLUSTERING ORDER BY (ck1 ASC, ck2 DESC)"));
@@ -480,7 +494,7 @@
         String tableName = createTable("CREATE TABLE IF NOT EXISTS %s (" +
                                        "pk1 varint," +
                                        "ck1 varint," +
-                                       "reg1 frozen<" + typeB + ">," +
+                                       "reg1 " + typeB + "," +
                                        "reg2 varint," +
                                        "PRIMARY KEY (pk1, ck1));");
 
diff --git a/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java b/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java
index f7152ff..8e8e9c9 100644
--- a/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java
+++ b/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java
@@ -30,10 +30,12 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
 import org.json.simple.parser.JSONParser;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -48,6 +50,7 @@
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.metrics.ClearableHistogram;
 import org.apache.cassandra.schema.KeyspaceParams;
@@ -96,6 +99,15 @@
     }
 
     @Test
+    public void testMemtableTimestamp() throws Throwable
+    {
+        assertEquals(Memtable.NO_MIN_TIMESTAMP,
+                     (new Memtable(Keyspace.open(KEYSPACE1).getColumnFamilyStore(CF_STANDARD1).metadata,
+                                   EncodingStats.NO_STATS.minTimestamp))
+                     .getMinTimestamp());
+    }
+
+    @Test
     // create two sstables, and verify that we only deserialize data from the most recent one
     public void testTimeSortedQuery()
     {
@@ -311,7 +323,7 @@
         // We don't do snapshot-based repair on Windows so we don't have ephemeral snapshots from repair that need clearing.
         // This test will fail as we'll revert to the WindowsFailedSnapshotTracker and counts will be off, but since we
         // don't do snapshot-based repair on Windows, we just skip this test.
-        Assume.assumeTrue(!FBUtilities.isWindows());
+        Assume.assumeTrue(!FBUtilities.isWindows);
 
         ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(CF_INDEX1);
 
@@ -327,8 +339,8 @@
         }
         ScrubTest.fillIndexCF(cfs, false, colValues);
 
-        cfs.snapshot("nonEphemeralSnapshot", null, false);
-        cfs.snapshot("ephemeralSnapshot", null, true);
+        cfs.snapshot("nonEphemeralSnapshot", null, false, false);
+        cfs.snapshot("ephemeralSnapshot", null, true, false);
 
         Map<String, Pair<Long, Long>> snapshotDetails = cfs.getSnapshotDetails();
         assertEquals(2, snapshotDetails.size());
@@ -346,6 +358,50 @@
     }
 
     @Test
+    public void testSnapshotSize()
+    {
+        // cleanup any previous test gargbage
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(CF_STANDARD1);
+        cfs.clearSnapshot("");
+
+        // Add row
+        new RowUpdateBuilder(cfs.metadata, 0, "key1")
+        .clustering("Column1")
+        .add("val", "asdf")
+        .build()
+        .applyUnsafe();
+        cfs.forceBlockingFlush();
+
+        // snapshot
+        cfs.snapshot("basic", null, false, false);
+
+        // check snapshot was created
+        Map<String, Pair<Long, Long>> snapshotDetails = cfs.getSnapshotDetails();
+        assertThat(snapshotDetails).hasSize(1);
+        assertThat(snapshotDetails).containsKey("basic");
+
+        // check that sizeOnDisk > trueSize = 0
+        Pair<Long, Long> details = snapshotDetails.get("basic");
+        long sizeOnDisk = details.left;
+        long trueSize = details.right;
+        assertThat(sizeOnDisk).isGreaterThan(trueSize);
+        assertThat(trueSize).isZero();
+
+        // compact base table to make trueSize > 0
+        cfs.forceMajorCompaction();
+        LifecycleTransaction.waitForDeletions();
+
+        // sizeOnDisk > trueSize because trueSize does not include manifest.json
+        // Check that truesize now is > 0
+        snapshotDetails = cfs.getSnapshotDetails();
+        details = snapshotDetails.get("basic");
+        sizeOnDisk = details.left;
+        trueSize = details.right;
+        assertThat(sizeOnDisk).isGreaterThan(trueSize);
+        assertThat(trueSize).isPositive();
+    }
+
+    @Test
     public void testBackupAfterFlush() throws Throwable
     {
         ColumnFamilyStore cfs = Keyspace.open(KEYSPACE2).getColumnFamilyStore(CF_STANDARD1);
@@ -356,8 +412,9 @@
 
         for (int version = 1; version <= 2; ++version)
         {
-            Descriptor existing = new Descriptor(cfs.getDirectories().getDirectoryForNewSSTables(), KEYSPACE2, CF_STANDARD1, version);
-            Descriptor desc = new Descriptor(Directories.getBackupsDirectory(existing), KEYSPACE2, CF_STANDARD1, version);
+            Descriptor existing = new Descriptor(cfs.getDirectories().getDirectoryForNewSSTables(), KEYSPACE2, CF_STANDARD1, version,
+                                                 SSTableFormat.Type.BIG);
+            Descriptor desc = new Descriptor(Directories.getBackupsDirectory(existing), KEYSPACE2, CF_STANDARD1, version, SSTableFormat.Type.BIG);
             for (Component c : new Component[]{ Component.DATA, Component.PRIMARY_INDEX, Component.FILTER, Component.STATS })
                 assertTrue("Cannot find backed-up file:" + desc.filenameFor(c), new File(desc.filenameFor(c)).exists());
         }
@@ -550,7 +607,6 @@
         assert indexTableFile.endsWith(baseTableFile);
     }
 
-
     @Test
     public void testScrubDataDirectories() throws Throwable
     {
diff --git a/test/unit/org/apache/cassandra/db/ColumnsTest.java b/test/unit/org/apache/cassandra/db/ColumnsTest.java
index 9498e8b..1f34c88 100644
--- a/test/unit/org/apache/cassandra/db/ColumnsTest.java
+++ b/test/unit/org/apache/cassandra/db/ColumnsTest.java
@@ -36,6 +36,7 @@
 import org.apache.cassandra.MockSchema;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.SetType;
 import org.apache.cassandra.db.marshal.UTF8Type;
@@ -47,6 +48,10 @@
 
 public class ColumnsTest
 {
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
 
     private static final CFMetaData cfMetaData = MockSchema.newCFS().metadata;
 
diff --git a/test/unit/org/apache/cassandra/db/CounterCacheTest.java b/test/unit/org/apache/cassandra/db/CounterCacheTest.java
index 91157ad..4cfd848 100644
--- a/test/unit/org/apache/cassandra/db/CounterCacheTest.java
+++ b/test/unit/org/apache/cassandra/db/CounterCacheTest.java
@@ -169,10 +169,10 @@
         Clustering c2 = CBuilder.create(cfs.metadata.comparator).add(ByteBufferUtil.bytes(2)).build();
         ColumnDefinition cd = cfs.metadata.getColumnDefinition(ByteBufferUtil.bytes("c"));
 
-        assertEquals(ClockAndCount.create(1L, 1L), cfs.getCachedCounter(bytes(1), c1, cd, null));
-        assertEquals(ClockAndCount.create(1L, 2L), cfs.getCachedCounter(bytes(1), c2, cd, null));
-        assertEquals(ClockAndCount.create(1L, 1L), cfs.getCachedCounter(bytes(2), c1, cd, null));
-        assertEquals(ClockAndCount.create(1L, 2L), cfs.getCachedCounter(bytes(2), c2, cd, null));
+        assertEquals(1L, cfs.getCachedCounter(bytes(1), c1, cd, null).count);
+        assertEquals(2L, cfs.getCachedCounter(bytes(1), c2, cd, null).count);
+        assertEquals(1L, cfs.getCachedCounter(bytes(2), c1, cd, null).count);
+        assertEquals(2L, cfs.getCachedCounter(bytes(2), c2, cd, null).count);
     }
 
     @Test
diff --git a/test/unit/org/apache/cassandra/db/CounterCellTest.java b/test/unit/org/apache/cassandra/db/CounterCellTest.java
index b4c7b2a..5208cb2 100644
--- a/test/unit/org/apache/cassandra/db/CounterCellTest.java
+++ b/test/unit/org/apache/cassandra/db/CounterCellTest.java
@@ -103,20 +103,20 @@
     {
         ColumnDefinition cDef = cfs.metadata.getColumnDefinition(colName);
         ByteBuffer val = CounterContext.instance().createLocal(count);
-        return BufferCell.live(cfs.metadata, cDef, ts, val);
+        return BufferCell.live(cDef, ts, val);
     }
 
     private Cell createCounterCell(ColumnFamilyStore cfs, ByteBuffer colName, CounterId id, long count, long ts)
     {
         ColumnDefinition cDef = cfs.metadata.getColumnDefinition(colName);
         ByteBuffer val = CounterContext.instance().createGlobal(id, ts, count);
-        return BufferCell.live(cfs.metadata, cDef, ts, val);
+        return BufferCell.live(cDef, ts, val);
     }
 
     private Cell createCounterCellFromContext(ColumnFamilyStore cfs, ByteBuffer colName, ContextState context, long ts)
     {
         ColumnDefinition cDef = cfs.metadata.getColumnDefinition(colName);
-        return BufferCell.live(cfs.metadata, cDef, ts, context.context);
+        return BufferCell.live(cDef, ts, context.context);
     }
 
     private Cell createDeleted(ColumnFamilyStore cfs, ByteBuffer colName, long ts, int localDeletionTime)
@@ -276,7 +276,7 @@
         Cell original = createCounterCellFromContext(cfs, col, state, 5);
 
         ColumnDefinition cDef = cfs.metadata.getColumnDefinition(col);
-        Cell cleared = BufferCell.live(cfs.metadata, cDef, 5, CounterContext.instance().clearAllLocal(state.context));
+        Cell cleared = BufferCell.live(cDef, 5, CounterContext.instance().clearAllLocal(state.context));
 
         original.digest(digest1);
         cleared.digest(digest2);
@@ -291,10 +291,10 @@
         ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(COUNTER1);
 
         ColumnDefinition emptyColDef = cfs.metadata.getColumnDefinition(ByteBufferUtil.bytes("val2"));
-        BufferCell emptyCell = BufferCell.live(cfs.metadata, emptyColDef, 0, ByteBuffer.allocate(0));
+        BufferCell emptyCell = BufferCell.live(emptyColDef, 0, ByteBuffer.allocate(0));
 
         Row.Builder builder = BTreeRow.unsortedBuilder(0);
-        builder.newRow(new Clustering(AsciiSerializer.instance.serialize("test")));
+        builder.newRow(Clustering.make(AsciiSerializer.instance.serialize("test")));
         builder.addCell(emptyCell);
         Row row = builder.build();
 
diff --git a/test/unit/org/apache/cassandra/db/CounterMutationTest.java b/test/unit/org/apache/cassandra/db/CounterMutationTest.java
index 912dd68..c8d4703 100644
--- a/test/unit/org/apache/cassandra/db/CounterMutationTest.java
+++ b/test/unit/org/apache/cassandra/db/CounterMutationTest.java
@@ -150,11 +150,11 @@
         CBuilder cb = CBuilder.create(cfsOne.metadata.comparator);
         cb.add("cc");
 
-        assertEquals(ClockAndCount.create(1L, 1L), cfsOne.getCachedCounter(Util.dk("key1").getKey(), cb.build(), c1cfs1, null));
-        assertEquals(ClockAndCount.create(1L, -1L), cfsOne.getCachedCounter(Util.dk("key1").getKey(), cb.build(), c2cfs1, null));
+        assertEquals(1L, cfsOne.getCachedCounter(Util.dk("key1").getKey(), cb.build(), c1cfs1, null).count);
+        assertEquals(-1L, cfsOne.getCachedCounter(Util.dk("key1").getKey(), cb.build(), c2cfs1, null).count);
 
-        assertEquals(ClockAndCount.create(1L, 2L), cfsTwo.getCachedCounter(Util.dk("key1").getKey(), cb.build(), c1cfs2, null));
-        assertEquals(ClockAndCount.create(1L, -2L), cfsTwo.getCachedCounter(Util.dk("key1").getKey(), cb.build(), c2cfs2, null));
+        assertEquals(2L, cfsTwo.getCachedCounter(Util.dk("key1").getKey(), cb.build(), c1cfs2, null).count);
+        assertEquals(-2L, cfsTwo.getCachedCounter(Util.dk("key1").getKey(), cb.build(), c2cfs2, null).count);
     }
 
     @Test
diff --git a/test/unit/org/apache/cassandra/db/DirectoriesTest.java b/test/unit/org/apache/cassandra/db/DirectoriesTest.java
index 5cdfa0f..9afe492 100644
--- a/test/unit/org/apache/cassandra/db/DirectoriesTest.java
+++ b/test/unit/org/apache/cassandra/db/DirectoriesTest.java
@@ -28,6 +28,7 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 
+import com.google.common.collect.Sets;
 import org.apache.commons.lang3.StringUtils;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
@@ -44,6 +45,7 @@
 import org.apache.cassandra.io.FSWriteError;
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.schema.IndexMetadata;
 import org.apache.cassandra.service.DefaultFSErrorHandler;
@@ -70,7 +72,10 @@
     @BeforeClass
     public static void beforeClass() throws IOException
     {
+        DatabaseDescriptor.daemonInitialization();
+
         FileUtils.setFSErrorHandler(new DefaultFSErrorHandler());
+
         for (String table : TABLES)
         {
             UUID tableID = CFMetaData.generateLegacyCfId(KS, table);
@@ -121,7 +126,7 @@
 
     private static void createFakeSSTable(File dir, String cf, int gen, List<File> addTo) throws IOException
     {
-        Descriptor desc = new Descriptor(dir, KS, cf, gen);
+        Descriptor desc = new Descriptor(dir, KS, cf, gen, SSTableFormat.Type.BIG);
         for (Component c : new Component[]{ Component.DATA, Component.PRIMARY_INDEX, Component.FILTER })
         {
             File f = new File(desc.filenameFor(c));
@@ -156,7 +161,7 @@
             Directories directories = new Directories(cfm);
             assertEquals(cfDir(cfm), directories.getDirectoryForNewSSTables());
 
-            Descriptor desc = new Descriptor(cfDir(cfm), KS, cfm.cfName, 1);
+            Descriptor desc = new Descriptor(cfDir(cfm), KS, cfm.cfName, 1, SSTableFormat.Type.BIG);
             File snapshotDir = new File(cfDir(cfm),  File.separator + Directories.SNAPSHOT_SUBDIR + File.separator + "42");
             assertEquals(snapshotDir.getCanonicalFile(), Directories.getSnapshotDirectory(desc, "42"));
 
@@ -190,8 +195,8 @@
         {
             assertEquals(cfDir(INDEX_CFM), dir);
         }
-        Descriptor parentDesc = new Descriptor(parentDirectories.getDirectoryForNewSSTables(), KS, PARENT_CFM.cfName, 0);
-        Descriptor indexDesc = new Descriptor(indexDirectories.getDirectoryForNewSSTables(), KS, INDEX_CFM.cfName, 0);
+        Descriptor parentDesc = new Descriptor(parentDirectories.getDirectoryForNewSSTables(), KS, PARENT_CFM.cfName, 0, SSTableFormat.Type.BIG);
+        Descriptor indexDesc = new Descriptor(indexDirectories.getDirectoryForNewSSTables(), KS, INDEX_CFM.cfName, 0, SSTableFormat.Type.BIG);
 
         // snapshot dir should be created under its parent's
         File parentSnapshotDirectory = Directories.getSnapshotDirectory(parentDesc, "test");
@@ -208,9 +213,9 @@
                      indexDirectories.snapshotCreationTime("test"));
 
         // check true snapshot size
-        Descriptor parentSnapshot = new Descriptor(parentSnapshotDirectory, KS, PARENT_CFM.cfName, 0);
+        Descriptor parentSnapshot = new Descriptor(parentSnapshotDirectory, KS, PARENT_CFM.cfName, 0, SSTableFormat.Type.BIG);
         createFile(parentSnapshot.filenameFor(Component.DATA), 30);
-        Descriptor indexSnapshot = new Descriptor(indexSnapshotDirectory, KS, INDEX_CFM.cfName, 0);
+        Descriptor indexSnapshot = new Descriptor(indexSnapshotDirectory, KS, INDEX_CFM.cfName, 0, SSTableFormat.Type.BIG);
         createFile(indexSnapshot.filenameFor(Component.DATA), 40);
 
         assertEquals(30, parentDirectories.trueSnapshotsSize());
@@ -358,7 +363,7 @@
             final String n = Long.toString(System.nanoTime());
             Callable<File> directoryGetter = new Callable<File>() {
                 public File call() throws Exception {
-                    Descriptor desc = new Descriptor(cfDir(cfm), KS, cfm.cfName, 1);
+                    Descriptor desc = new Descriptor(cfDir(cfm), KS, cfm.cfName, 1, SSTableFormat.Type.BIG);
                     return Directories.getSnapshotDirectory(desc, n);
                 }
             };
@@ -474,10 +479,10 @@
     @Test
     public void testGetLocationForDisk()
     {
-        DataDirectory [] paths = new DataDirectory[3];
-        paths[0] = new DataDirectory(new File("/tmp/aaa"));
-        paths[1] = new DataDirectory(new File("/tmp/aa"));
-        paths[2] = new DataDirectory(new File("/tmp/a"));
+        Collection<DataDirectory> paths = new ArrayList<>();
+        paths.add(new DataDirectory(new File("/tmp/aaa")));
+        paths.add(new DataDirectory(new File("/tmp/aa")));
+        paths.add(new DataDirectory(new File("/tmp/a")));
 
         for (CFMetaData cfm : CFM)
         {
@@ -511,6 +516,110 @@
         assertTrue(dirs.getLocationForDisk(path1).toPath().startsWith(p1));
     }
 
+    @Test
+    public void getDataDirectoryForFile()
+    {
+        Collection<DataDirectory> paths = new ArrayList<>();
+        paths.add(new DataDirectory(new File("/tmp/a")));
+        paths.add(new DataDirectory(new File("/tmp/aa")));
+        paths.add(new DataDirectory(new File("/tmp/aaa")));
+
+        for (CFMetaData cfm : CFM)
+        {
+            Directories dirs = new Directories(cfm, paths);
+            for (DataDirectory dir : paths)
+            {
+                Descriptor d = Descriptor.fromFilename(new File(dir.location, getNewFilename(cfm, false)).toString());
+                String p = dirs.getDataDirectoryForFile(d).location.getAbsolutePath() + File.separator;
+                assertTrue(p.startsWith(dir.location.getAbsolutePath() + File.separator));
+            }
+        }
+    }
+
+    /**
+     * Makes sure we can find the data directory when it is a symlink
+     *
+     * creates the following data directories:
+     * <tempdir something>/datadir1
+     * <tempdir something>/datadir11 (symlink to <tempdir something>/symlinktarget)
+     *
+     * and then makes sure that we get the correct directory back.
+     */
+    @Test
+    public void testDirectoriesSymlinks() throws IOException
+    {
+        Path p = Files.createTempDirectory("something");
+        Path symlinktarget = Files.createDirectories(p.resolve("symlinktarget"));
+        Path ddir1 = Files.createDirectories(p.resolve("datadir1"));
+        Path ddir2 = Files.createSymbolicLink(p.resolve("datadir11"), symlinktarget);
+        DataDirectory dd1 = new DataDirectory(ddir1.toFile());
+        DataDirectory dd2 = new DataDirectory(ddir2.toFile());
+
+        for (CFMetaData tm : CFM)
+        {
+            Directories dirs = new Directories(tm, Sets.newHashSet(dd1, dd2));
+            Descriptor desc = Descriptor.fromFilename(ddir1.resolve(getNewFilename(tm, false)).toString());
+            assertEquals(ddir1.toFile(), dirs.getDataDirectoryForFile(desc).location);
+            desc = Descriptor.fromFilename(ddir2.resolve(getNewFilename(tm, false)).toString());
+            assertEquals(ddir2.toFile(), dirs.getDataDirectoryForFile(desc).location);
+        }
+    }
+
+    @Test
+    public void testDirectoriesOldTableSymlink() throws IOException
+    {
+        testDirectoriesSymlinksHelper(true);
+    }
+
+    @Test
+    public void testDirectoriesTableSymlink() throws IOException
+    {
+        testDirectoriesSymlinksHelper(false);
+    }
+
+    /**
+     * Makes sure we can find the data directory for a file when the table directory is a symlink
+     *
+     * if oldStyle is false we append the table id to the table directory
+     *
+     * creates the following structure
+     * <tempdir>/datadir1/<ks>/<table>
+     * <tempdir>/datadir11/<ks>/<table symlink to <tempdir>/symlinktarget>
+     *
+     * and then we create a fake descriptor to a file in the table directory and make sure we get the correct
+     * data directory back.
+     */
+    private void testDirectoriesSymlinksHelper(boolean oldStyle) throws IOException
+    {
+        Path p = Files.createTempDirectory("something");
+        Path symlinktarget = Files.createDirectories(p.resolve("symlinktarget"));
+        Path ddir1 = Files.createDirectories(p.resolve("datadir1"));
+        Path ddir2 = Files.createDirectories(p.resolve("datadir11"));
+
+        for (CFMetaData metadata : CFM)
+        {
+            Path keyspacedir = Files.createDirectories(ddir2.resolve(metadata.ksName));
+            String tabledir = metadata.cfName + (oldStyle ? "" : Component.separator +  ByteBufferUtil.bytesToHex(ByteBufferUtil.bytes(metadata.cfId)));
+            Files.createSymbolicLink(keyspacedir.resolve(tabledir), symlinktarget);
+        }
+
+        DataDirectory dd1 = new DataDirectory(ddir1.toFile());
+        DataDirectory dd2 = new DataDirectory(ddir2.toFile());
+        for (CFMetaData tm : CFM)
+        {
+            Directories dirs = new Directories(tm, Sets.newHashSet(dd1, dd2));
+            Descriptor desc = Descriptor.fromFilename(ddir1.resolve(getNewFilename(tm, oldStyle)).toFile().toString());
+            assertEquals(ddir1.toFile(), dirs.getDataDirectoryForFile(desc).location);
+            desc = Descriptor.fromFilename(ddir2.resolve(getNewFilename(tm, oldStyle)).toFile().toString());
+            assertEquals(ddir2.toFile(), dirs.getDataDirectoryForFile(desc).location);
+        }
+    }
+
+    private String getNewFilename(CFMetaData metadata, boolean oldStyle)
+    {
+        return metadata.ksName + File.separator + metadata.cfName + (oldStyle ? "" : Component.separator +  ByteBufferUtil.bytesToHex(ByteBufferUtil.bytes(metadata.cfId))) + "/na-1-big-Data.db";
+    }
+
     private List<Directories.DataDirectoryCandidate> getWriteableDirectories(DataDirectory[] dataDirectories, long writeSize)
     {
         // copied from Directories.getWriteableLocation(long)
@@ -532,4 +641,5 @@
 
         return candidates;
     }
+
 }
diff --git a/test/unit/org/apache/cassandra/db/DiskBoundaryManagerTest.java b/test/unit/org/apache/cassandra/db/DiskBoundaryManagerTest.java
new file mode 100644
index 0000000..febcfeb
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/DiskBoundaryManagerTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.db;
+
+import java.io.File;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+
+import com.google.common.collect.Lists;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.dht.BootStrapper;
+import org.apache.cassandra.locator.TokenMetadata;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.utils.FBUtilities;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class DiskBoundaryManagerTest extends CQLTester
+{
+    private DiskBoundaryManager dbm;
+    private MockCFS mock;
+    private Directories dirs;
+
+    @Before
+    public void setup()
+    {
+        DisallowedDirectories.clearUnwritableUnsafe();
+        TokenMetadata metadata = StorageService.instance.getTokenMetadata();
+        metadata.updateNormalTokens(BootStrapper.getRandomTokens(metadata, 10), FBUtilities.getBroadcastAddress());
+        createTable("create table %s (id int primary key, x text)");
+        dirs = new Directories(getCurrentColumnFamilyStore().metadata, Lists.newArrayList(new Directories.DataDirectory(new File("/tmp/1")),
+                                                                                          new Directories.DataDirectory(new File("/tmp/2")),
+                                                                                          new Directories.DataDirectory(new File("/tmp/3"))));
+        mock = new MockCFS(getCurrentColumnFamilyStore(), dirs);
+        dbm = mock.diskBoundaryManager;
+    }
+
+    @Test
+    public void getBoundariesTest()
+    {
+        DiskBoundaries dbv = dbm.getDiskBoundaries(mock);
+        Assert.assertEquals(3, dbv.positions.size());
+        assertEquals(dbv.directories, dirs.getWriteableLocations());
+    }
+
+    @Test
+    public void disallowedDirectoriesTest()
+    {
+        DiskBoundaries dbv = dbm.getDiskBoundaries(mock);
+        Assert.assertEquals(3, dbv.positions.size());
+        assertEquals(dbv.directories, dirs.getWriteableLocations());
+        DisallowedDirectories.maybeMarkUnwritable(new File("/tmp/3"));
+        dbv = dbm.getDiskBoundaries(mock);
+        Assert.assertEquals(2, dbv.positions.size());
+        Assert.assertEquals(Lists.newArrayList(new Directories.DataDirectory(new File("/tmp/1")),
+                                        new Directories.DataDirectory(new File("/tmp/2"))),
+                                 dbv.directories);
+    }
+
+    @Test
+    public void updateTokensTest() throws UnknownHostException
+    {
+        DiskBoundaries dbv1 = dbm.getDiskBoundaries(mock);
+        StorageService.instance.getTokenMetadata().updateNormalTokens(BootStrapper.getRandomTokens(StorageService.instance.getTokenMetadata(), 10), InetAddress.getByName("127.0.0.10"));
+        DiskBoundaries dbv2 = dbm.getDiskBoundaries(mock);
+        assertFalse(dbv1.equals(dbv2));
+    }
+
+    @Test
+    public void alterKeyspaceTest() throws Throwable
+    {
+        //do not use mock to since it will not be invalidated after alter keyspace
+        DiskBoundaryManager dbm = getCurrentColumnFamilyStore().diskBoundaryManager;
+        DiskBoundaries dbv1 = dbm.getDiskBoundaries(mock);
+        execute("alter keyspace "+keyspace()+" with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }");
+        DiskBoundaries dbv2 = dbm.getDiskBoundaries(mock);
+        assertNotSame(dbv1, dbv2);
+        DiskBoundaries dbv3 = dbm.getDiskBoundaries(mock);
+        assertSame(dbv2, dbv3);
+
+    }
+
+    private static void assertEquals(List<Directories.DataDirectory> dir1, Directories.DataDirectory[] dir2)
+    {
+        if (dir1.size() != dir2.length)
+            fail();
+        for (int i = 0; i < dir2.length; i++)
+        {
+            if (!dir1.get(i).equals(dir2[i]))
+                fail();
+        }
+    }
+
+    // just to be able to override the data directories
+    private static class MockCFS extends ColumnFamilyStore
+    {
+        MockCFS(ColumnFamilyStore cfs, Directories dirs)
+        {
+            super(cfs.keyspace, cfs.getTableName(), 0, cfs.metadata, dirs, false, false, true);
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/KeyCacheTest.java b/test/unit/org/apache/cassandra/db/KeyCacheTest.java
index 9cb06b9..f31df18 100644
--- a/test/unit/org/apache/cassandra/db/KeyCacheTest.java
+++ b/test/unit/org/apache/cassandra/db/KeyCacheTest.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.db;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -29,7 +30,6 @@
 import java.util.concurrent.TimeUnit;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.Uninterruptibles;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -39,9 +39,7 @@
 import org.apache.cassandra.cache.AutoSavingCache;
 import org.apache.cassandra.cache.ICache;
 import org.apache.cassandra.cache.KeyCacheKey;
-import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.db.compaction.CompactionManager;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
@@ -52,11 +50,13 @@
 import org.apache.cassandra.service.CacheService;
 import org.apache.cassandra.utils.Pair;
 import org.apache.cassandra.utils.concurrent.Refs;
+import org.hamcrest.Matchers;
 import org.mockito.Mockito;
 import org.mockito.internal.stubbing.answers.AnswersWithDelay;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
@@ -68,6 +68,9 @@
     private static final String COLUMN_FAMILY1 = "Standard1";
     private static final String COLUMN_FAMILY2 = "Standard2";
     private static final String COLUMN_FAMILY3 = "Standard3";
+    private static final String COLUMN_FAMILY4 = "Standard4";
+    private static final String COLUMN_FAMILY5 = "Standard5";
+    private static final String COLUMN_FAMILY6 = "Standard6";
     private static final String COLUMN_FAMILY7 = "Standard7";
     private static final String COLUMN_FAMILY8 = "Standard8";
     private static final String COLUMN_FAMILY9 = "Standard9";
@@ -84,6 +87,9 @@
                                     SchemaLoader.standardCFMD(KEYSPACE1, COLUMN_FAMILY1),
                                     SchemaLoader.standardCFMD(KEYSPACE1, COLUMN_FAMILY2),
                                     SchemaLoader.standardCFMD(KEYSPACE1, COLUMN_FAMILY3),
+                                    SchemaLoader.standardCFMD(KEYSPACE1, COLUMN_FAMILY4),
+                                    SchemaLoader.standardCFMD(KEYSPACE1, COLUMN_FAMILY5),
+                                    SchemaLoader.standardCFMD(KEYSPACE1, COLUMN_FAMILY6),
                                     SchemaLoader.standardCFMD(KEYSPACE1, COLUMN_FAMILY7),
                                     SchemaLoader.standardCFMD(KEYSPACE1, COLUMN_FAMILY8),
                                     SchemaLoader.standardCFMD(KEYSPACE1, COLUMN_FAMILY9));
@@ -101,42 +107,61 @@
     }
 
     @Test
-    public void testKeyCacheLoad() throws Exception
+    public void testKeyCacheLoadShallowIndexEntry() throws Exception
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(0);
+        testKeyCacheLoad(COLUMN_FAMILY2);
+    }
+
+    @Test
+    public void testKeyCacheLoadIndexInfoOnHeap() throws Exception
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(8);
+        testKeyCacheLoad(COLUMN_FAMILY5);
+    }
+
+    private void testKeyCacheLoad(String cf) throws Exception
     {
         CompactionManager.instance.disableAutoCompaction();
 
-        ColumnFamilyStore store = Keyspace.open(KEYSPACE1).getColumnFamilyStore(COLUMN_FAMILY2);
+        ColumnFamilyStore store = Keyspace.open(KEYSPACE1).getColumnFamilyStore(cf);
 
         // empty the cache
         CacheService.instance.invalidateKeyCache();
-        assertKeyCacheSize(0, KEYSPACE1, COLUMN_FAMILY2);
+        assertKeyCacheSize(0, KEYSPACE1, cf);
 
         // insert data and force to disk
-        SchemaLoader.insertData(KEYSPACE1, COLUMN_FAMILY2, 0, 100);
+        SchemaLoader.insertData(KEYSPACE1, cf, 0, 100);
         store.forceBlockingFlush();
 
         // populate the cache
-        readData(KEYSPACE1, COLUMN_FAMILY2, 0, 100);
-        assertKeyCacheSize(100, KEYSPACE1, COLUMN_FAMILY2);
+        readData(KEYSPACE1, cf, 0, 100);
+        assertKeyCacheSize(100, KEYSPACE1, cf);
 
         // really? our caches don't implement the map interface? (hence no .addAll)
-        Map<KeyCacheKey, RowIndexEntry> savedMap = new HashMap<KeyCacheKey, RowIndexEntry>();
+        Map<KeyCacheKey, RowIndexEntry> savedMap = new HashMap<>();
+        Map<KeyCacheKey, RowIndexEntry.IndexInfoRetriever> savedInfoMap = new HashMap<>();
         for (Iterator<KeyCacheKey> iter = CacheService.instance.keyCache.keyIterator();
              iter.hasNext();)
         {
             KeyCacheKey k = iter.next();
-            if (k.desc.ksname.equals(KEYSPACE1) && k.desc.cfname.equals(COLUMN_FAMILY2))
-                savedMap.put(k, CacheService.instance.keyCache.get(k));
+            if (k.desc.ksname.equals(KEYSPACE1) && k.desc.cfname.equals(cf))
+            {
+                RowIndexEntry rie = CacheService.instance.keyCache.get(k);
+                savedMap.put(k, rie);
+                SSTableReader sstr = readerForKey(k);
+                savedInfoMap.put(k, rie.openWithIndex(sstr.getIndexFile()));
+            }
         }
 
         // force the cache to disk
         CacheService.instance.keyCache.submitWrite(Integer.MAX_VALUE).get();
 
         CacheService.instance.invalidateKeyCache();
-        assertKeyCacheSize(0, KEYSPACE1, COLUMN_FAMILY2);
+        assertKeyCacheSize(0, KEYSPACE1, cf);
 
         CacheService.instance.keyCache.loadSaved();
-        assertKeyCacheSize(savedMap.size(), KEYSPACE1, COLUMN_FAMILY2);
+        assertKeyCacheSize(savedMap.size(), KEYSPACE1, cf);
 
         // probably it's better to add equals/hashCode to RowIndexEntry...
         for (Map.Entry<KeyCacheKey, RowIndexEntry> entry : savedMap.entrySet())
@@ -144,77 +169,132 @@
             RowIndexEntry expected = entry.getValue();
             RowIndexEntry actual = CacheService.instance.keyCache.get(entry.getKey());
             assertEquals(expected.position, actual.position);
-            assertEquals(expected.columnsIndex(), actual.columnsIndex());
+            assertEquals(expected.columnsIndexCount(), actual.columnsIndexCount());
+            for (int i = 0; i < expected.columnsIndexCount(); i++)
+            {
+                SSTableReader actualSstr = readerForKey(entry.getKey());
+                try (RowIndexEntry.IndexInfoRetriever actualIir = actual.openWithIndex(actualSstr.getIndexFile()))
+                {
+                    RowIndexEntry.IndexInfoRetriever expectedIir = savedInfoMap.get(entry.getKey());
+                    assertEquals(expectedIir.columnsIndex(i), actualIir.columnsIndex(i));
+                }
+            }
             if (expected.isIndexed())
             {
                 assertEquals(expected.deletionTime(), actual.deletionTime());
             }
         }
+
+        savedInfoMap.values().forEach(iir -> {
+            try
+            {
+                if (iir != null)
+                    iir.close();
+            }
+            catch (IOException e)
+            {
+                throw new RuntimeException(e);
+            }
+        });
+    }
+
+    private static SSTableReader readerForKey(KeyCacheKey k)
+    {
+        return ColumnFamilyStore.getIfExists(k.desc.ksname, k.desc.cfname).getLiveSSTables()
+                                .stream()
+                                .filter(sstreader -> sstreader.descriptor.generation == k.desc.generation)
+                                .findFirst().get();
     }
 
     @Test
-    public void testKeyCacheLoadWithLostTable() throws Exception
+    public void testKeyCacheLoadWithLostTableShallowIndexEntry() throws Exception
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(0);
+        testKeyCacheLoadWithLostTable(COLUMN_FAMILY3);
+    }
+
+    @Test
+    public void testKeyCacheLoadWithLostTableIndexInfoOnHeap() throws Exception
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(8);
+        testKeyCacheLoadWithLostTable(COLUMN_FAMILY6);
+    }
+
+    private void testKeyCacheLoadWithLostTable(String cf) throws Exception
     {
         CompactionManager.instance.disableAutoCompaction();
 
-        ColumnFamilyStore store = Keyspace.open(KEYSPACE1).getColumnFamilyStore(COLUMN_FAMILY3);
+        ColumnFamilyStore store = Keyspace.open(KEYSPACE1).getColumnFamilyStore(cf);
 
         // empty the cache
         CacheService.instance.invalidateKeyCache();
-        assertKeyCacheSize(0, KEYSPACE1, COLUMN_FAMILY3);
+        assertKeyCacheSize(0, KEYSPACE1, cf);
 
         // insert data and force to disk
-        SchemaLoader.insertData(KEYSPACE1, COLUMN_FAMILY3, 0, 100);
+        SchemaLoader.insertData(KEYSPACE1, cf, 0, 100);
         store.forceBlockingFlush();
 
         Collection<SSTableReader> firstFlushTables = ImmutableList.copyOf(store.getLiveSSTables());
 
         // populate the cache
-        readData(KEYSPACE1, COLUMN_FAMILY3, 0, 100);
-        assertKeyCacheSize(100, KEYSPACE1, COLUMN_FAMILY3);
+        readData(KEYSPACE1, cf, 0, 100);
+        assertKeyCacheSize(100, KEYSPACE1, cf);
 
         // insert some new data and force to disk
-        SchemaLoader.insertData(KEYSPACE1, COLUMN_FAMILY3, 100, 50);
+        SchemaLoader.insertData(KEYSPACE1, cf, 100, 50);
         store.forceBlockingFlush();
 
         // check that it's fine
-        readData(KEYSPACE1, COLUMN_FAMILY3, 100, 50);
-        assertKeyCacheSize(150, KEYSPACE1, COLUMN_FAMILY3);
+        readData(KEYSPACE1, cf, 100, 50);
+        assertKeyCacheSize(150, KEYSPACE1, cf);
 
         // force the cache to disk
         CacheService.instance.keyCache.submitWrite(Integer.MAX_VALUE).get();
 
         CacheService.instance.invalidateKeyCache();
-        assertKeyCacheSize(0, KEYSPACE1, COLUMN_FAMILY3);
+        assertKeyCacheSize(0, KEYSPACE1, cf);
 
         // check that the content is written correctly
         CacheService.instance.keyCache.loadSaved();
-        assertKeyCacheSize(150, KEYSPACE1, COLUMN_FAMILY3);
+        assertKeyCacheSize(150, KEYSPACE1, cf);
 
         CacheService.instance.invalidateKeyCache();
-        assertKeyCacheSize(0, KEYSPACE1, COLUMN_FAMILY3);
+        assertKeyCacheSize(0, KEYSPACE1, cf);
 
         // now remove the first sstable from the store to simulate losing the file
         store.markObsolete(firstFlushTables, OperationType.UNKNOWN);
 
         // check that reading now correctly skips over lost table and reads the rest (CASSANDRA-10219)
         CacheService.instance.keyCache.loadSaved();
-        assertKeyCacheSize(50, KEYSPACE1, COLUMN_FAMILY3);
+        assertKeyCacheSize(50, KEYSPACE1, cf);
     }
 
     @Test
-    public void testKeyCache() throws ExecutionException, InterruptedException
+    public void testKeyCacheShallowIndexEntry() throws ExecutionException, InterruptedException
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(0);
+        testKeyCache(COLUMN_FAMILY1);
+    }
+
+    @Test
+    public void testKeyCacheIndexInfoOnHeap() throws ExecutionException, InterruptedException
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(8);
+        testKeyCache(COLUMN_FAMILY4);
+    }
+
+    private void testKeyCache(String cf) throws ExecutionException, InterruptedException
     {
         CompactionManager.instance.disableAutoCompaction();
 
         Keyspace keyspace = Keyspace.open(KEYSPACE1);
-        ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(COLUMN_FAMILY1);
+        ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(cf);
 
         // just to make sure that everything is clean
         CacheService.instance.invalidateKeyCache();
 
         // KeyCache should start at size 0 if we're caching X% of zero data.
-        assertKeyCacheSize(0, KEYSPACE1, COLUMN_FAMILY1);
+        assertKeyCacheSize(0, KEYSPACE1, cf);
 
         Mutation rm;
 
@@ -229,7 +309,7 @@
         Util.getAll(Util.cmd(cfs, "key1").build());
         Util.getAll(Util.cmd(cfs, "key2").build());
 
-        assertKeyCacheSize(2, KEYSPACE1, COLUMN_FAMILY1);
+        assertKeyCacheSize(2, KEYSPACE1, cf);
 
         Set<SSTableReader> readers = cfs.getLiveSSTables();
         Refs<SSTableReader> refs = Refs.tryRef(readers);
@@ -242,20 +322,20 @@
         // after compaction cache should have entries for new SSTables,
         // but since we have kept a reference to the old sstables,
         // if we had 2 keys in cache previously it should become 4
-        assertKeyCacheSize(noEarlyOpen ? 2 : 4, KEYSPACE1, COLUMN_FAMILY1);
+        assertKeyCacheSize(noEarlyOpen ? 2 : 4, KEYSPACE1, cf);
 
         refs.release();
 
         LifecycleTransaction.waitForDeletions();
 
         // after releasing the reference this should drop to 2
-        assertKeyCacheSize(2, KEYSPACE1, COLUMN_FAMILY1);
+        assertKeyCacheSize(2, KEYSPACE1, cf);
 
         // re-read same keys to verify that key cache didn't grow further
         Util.getAll(Util.cmd(cfs, "key1").build());
         Util.getAll(Util.cmd(cfs, "key2").build());
 
-        assertKeyCacheSize(noEarlyOpen ? 4 : 2, KEYSPACE1, COLUMN_FAMILY1);
+        assertKeyCacheSize(noEarlyOpen ? 4 : 2, KEYSPACE1, cf);
     }
 
     @Test
@@ -325,8 +405,8 @@
                                             1 + TimeUnit.SECONDS.toMillis(DatabaseDescriptor.getCacheLoadTimeout()) / delayMillis);
 
         long keysLoaded = autoSavingCache.loadSaved();
-        assertTrue(keysLoaded < maxExpectedKeyCache);
-        assertTrue(0 != keysLoaded);
+        assertThat(keysLoaded, Matchers.lessThanOrEqualTo(maxExpectedKeyCache));
+        assertNotEquals(0, keysLoaded);
         Mockito.verify(keyCacheSerializerSpy, Mockito.times(1)).cleanupAfterDeserialize();
     }
 
diff --git a/test/unit/org/apache/cassandra/db/KeyspaceTest.java b/test/unit/org/apache/cassandra/db/KeyspaceTest.java
index dd11c1c..3c3b04b 100644
--- a/test/unit/org/apache/cassandra/db/KeyspaceTest.java
+++ b/test/unit/org/apache/cassandra/db/KeyspaceTest.java
@@ -24,6 +24,7 @@
 import org.apache.cassandra.Util;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.db.rows.Cell;
 import org.apache.cassandra.db.rows.Row;
 import org.apache.cassandra.db.rows.RowIterator;
@@ -41,14 +42,34 @@
 
 public class KeyspaceTest extends CQLTester
 {
+    // Test needs synchronous table drop to avoid flushes causing flaky failures of testLimitSSTables
+
+    @Override
+    protected String createTable(String query)
+    {
+        return super.createTable(KEYSPACE_PER_TEST, query);
+    }
+
+    @Override
+    protected UntypedResultSet execute(String query, Object... values) throws Throwable
+    {
+        return executeFormattedQuery(formatQuery(KEYSPACE_PER_TEST, query), values);
+    }
+
+    @Override
+    public ColumnFamilyStore getCurrentColumnFamilyStore()
+    {
+        return super.getCurrentColumnFamilyStore(KEYSPACE_PER_TEST);
+    }
+
     @Test
     public void testGetRowNoColumns() throws Throwable
     {
-        String tableName = createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
+        createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
 
         execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", "0", 0, 0);
 
-        final ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(tableName);
+        final ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
 
         for (int round = 0; round < 2; round++)
         {
@@ -69,12 +90,12 @@
     @Test
     public void testGetRowSingleColumn() throws Throwable
     {
-        String tableName = createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
+        createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
 
         for (int i = 0; i < 2; i++)
             execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", "0", i, i);
 
-        final ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(tableName);
+        final ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
 
         for (int round = 0; round < 2; round++)
         {
@@ -104,11 +125,11 @@
     @Test
     public void testGetSliceBloomFilterFalsePositive() throws Throwable
     {
-        String tableName = createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
+        createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
 
         execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", "1", 1, 1);
 
-        final ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(tableName);
+        final ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
 
         // check empty reads on the partitions before and after the existing one
         for (String key : new String[]{"0", "2"})
@@ -129,13 +150,14 @@
 
     private static void assertRowsInSlice(ColumnFamilyStore cfs, String key, int sliceStart, int sliceEnd, int limit, boolean reversed, String columnValuePrefix)
     {
-        Clustering startClustering = new Clustering(ByteBufferUtil.bytes(sliceStart));
-        Clustering endClustering = new Clustering(ByteBufferUtil.bytes(sliceEnd));
+        Clustering startClustering = Clustering.make(ByteBufferUtil.bytes(sliceStart));
+        Clustering endClustering = Clustering.make(ByteBufferUtil.bytes(sliceEnd));
         Slices slices = Slices.with(cfs.getComparator(), Slice.make(startClustering, endClustering));
         ClusteringIndexSliceFilter filter = new ClusteringIndexSliceFilter(slices, reversed);
         SinglePartitionReadCommand command = singlePartitionSlice(cfs, key, filter, limit);
 
-        try (ReadOrderGroup orderGroup = command.startOrderGroup(); PartitionIterator iterator = command.executeInternal(orderGroup))
+        try (ReadExecutionController executionController = command.executionController();
+             PartitionIterator iterator = command.executeInternal(executionController))
         {
             try (RowIterator rowIterator = iterator.next())
             {
@@ -165,14 +187,13 @@
     @Test
     public void testGetSliceWithCutoff() throws Throwable
     {
-        // tests slicing against data from one row in a memtable and then flushed to an sstable
-        String tableName = createTable("CREATE TABLE %s (a text, b int, c text, PRIMARY KEY (a, b))");
+        createTable("CREATE TABLE %s (a text, b int, c text, PRIMARY KEY (a, b))");
         String prefix = "omg!thisisthevalue!";
 
         for (int i = 0; i < 300; i++)
             execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", "0", i, prefix + i);
 
-        final ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(tableName);
+        final ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
 
         for (int round = 0; round < 2; round++)
         {
@@ -193,8 +214,8 @@
     @Test
     public void testReversedWithFlushing() throws Throwable
     {
-        String tableName = createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b)) WITH CLUSTERING ORDER BY (b DESC)");
-        final ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(tableName);
+        createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b)) WITH CLUSTERING ORDER BY (b DESC)");
+        final ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
 
         for (int i = 0; i < 10; i++)
             execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", "0", i, i);
@@ -205,10 +226,11 @@
         {
             execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", "0", i, i);
 
-            PartitionColumns columns = PartitionColumns.of(cfs.metadata.getColumnDefinition(new ColumnIdentifier("c", false)));
+            PartitionColumns.of(cfs.metadata.getColumnDefinition(new ColumnIdentifier("c", false)));
             ClusteringIndexSliceFilter filter = new ClusteringIndexSliceFilter(Slices.ALL, false);
             SinglePartitionReadCommand command = singlePartitionSlice(cfs, "0", filter, null);
-            try (ReadOrderGroup orderGroup = command.startOrderGroup(); PartitionIterator iterator = command.executeInternal(orderGroup))
+            try (ReadExecutionController executionController = command.executionController();
+                 PartitionIterator iterator = command.executeInternal(executionController))
             {
                 try (RowIterator rowIterator = iterator.next())
                 {
@@ -222,7 +244,8 @@
 
     private static void assertRowsInResult(ColumnFamilyStore cfs, SinglePartitionReadCommand command, int ... columnValues)
     {
-        try (ReadOrderGroup orderGroup = command.startOrderGroup(); PartitionIterator iterator = command.executeInternal(orderGroup))
+        try (ReadExecutionController executionController = command.executionController();
+             PartitionIterator iterator = command.executeInternal(executionController))
         {
             if (columnValues.length == 0)
             {
@@ -248,12 +271,12 @@
 
     private static ClusteringIndexSliceFilter slices(ColumnFamilyStore cfs, Integer sliceStart, Integer sliceEnd, boolean reversed)
     {
-        Slice.Bound startBound = sliceStart == null
-                               ? Slice.Bound.BOTTOM
-                               : Slice.Bound.create(ClusteringPrefix.Kind.INCL_START_BOUND, new ByteBuffer[]{ByteBufferUtil.bytes(sliceStart)});
-        Slice.Bound endBound = sliceEnd == null
-                             ? Slice.Bound.TOP
-                             : Slice.Bound.create(ClusteringPrefix.Kind.INCL_END_BOUND, new ByteBuffer[]{ByteBufferUtil.bytes(sliceEnd)});
+        ClusteringBound startBound = sliceStart == null
+                                   ? ClusteringBound.BOTTOM
+                                   : ClusteringBound.create(ClusteringPrefix.Kind.INCL_START_BOUND, new ByteBuffer[]{ByteBufferUtil.bytes(sliceStart)});
+        ClusteringBound endBound = sliceEnd == null
+                                 ? ClusteringBound.TOP
+                                 : ClusteringBound.create(ClusteringPrefix.Kind.INCL_END_BOUND, new ByteBuffer[]{ByteBufferUtil.bytes(sliceEnd)});
         Slices slices = Slices.with(cfs.getComparator(), Slice.make(startBound, endBound));
         return new ClusteringIndexSliceFilter(slices, reversed);
     }
@@ -270,9 +293,8 @@
     @Test
     public void testGetSliceFromBasic() throws Throwable
     {
-        // tests slicing against data from one row in a memtable and then flushed to an sstable
-        String tableName = createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
-        final ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(tableName);
+        createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
+        final ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
 
         for (int i = 1; i < 10; i++)
         {
@@ -319,9 +341,8 @@
     @Test
     public void testGetSliceWithExpiration() throws Throwable
     {
-        // tests slicing against data from one row with expiring column in a memtable and then flushed to an sstable
-        String tableName = createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
-        final ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(tableName);
+        createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
+        final ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
 
         execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", "0", 0, 0);
         execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?) USING TTL 60", "0", 1, 1);
@@ -343,9 +364,8 @@
     @Test
     public void testGetSliceFromAdvanced() throws Throwable
     {
-        // tests slicing against data from one row spread across two sstables
-        String tableName = createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
-        final ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(tableName);
+        createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
+        final ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
 
         for (int i = 1; i < 7; i++)
             execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", "0", i, i);
@@ -369,9 +389,8 @@
     @Test
     public void testGetSliceFromLarge() throws Throwable
     {
-        // tests slicing against 1000 rows in an sstable
-        String tableName = createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
-        final ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(tableName);
+        createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
+        final ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
 
         for (int i = 1000; i < 2000; i++)
             execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", "0", i, i);
@@ -386,8 +405,8 @@
 
         // verify that we do indeed have multiple index entries
         SSTableReader sstable = cfs.getLiveSSTables().iterator().next();
-        RowIndexEntry indexEntry = sstable.getPosition(Util.dk("0"), SSTableReader.Operator.EQ);
-        assert indexEntry.columnsIndex().size() > 2;
+        RowIndexEntry<?> indexEntry = sstable.getPosition(Util.dk("0"), SSTableReader.Operator.EQ);
+        assert indexEntry.columnsIndexCount() > 2;
 
         validateSliceLarge(cfs);
     }
@@ -395,8 +414,8 @@
     @Test
     public void testLimitSSTables() throws Throwable
     {
-        String tableName = createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
-        final ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(tableName);
+        createTable("CREATE TABLE %s (a text, b int, c int, PRIMARY KEY (a, b))");
+        final ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
         cfs.disableAutoCompaction();
 
         for (int j = 0; j < 10; j++)
diff --git a/test/unit/org/apache/cassandra/db/LegacyCellNameTest.java b/test/unit/org/apache/cassandra/db/LegacyCellNameTest.java
index 455fa9f..902c47e 100644
--- a/test/unit/org/apache/cassandra/db/LegacyCellNameTest.java
+++ b/test/unit/org/apache/cassandra/db/LegacyCellNameTest.java
@@ -17,14 +17,22 @@
  */
 package org.apache.cassandra.db;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
 
 import static junit.framework.Assert.assertTrue;
 
 public class LegacyCellNameTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testColumnSameNameAsPartitionKeyCompactStorage() throws Exception
     {
diff --git a/test/unit/org/apache/cassandra/db/LegacyLayoutTest.java b/test/unit/org/apache/cassandra/db/LegacyLayoutTest.java
index f0d2a02..d79d8a0 100644
--- a/test/unit/org/apache/cassandra/db/LegacyLayoutTest.java
+++ b/test/unit/org/apache/cassandra/db/LegacyLayoutTest.java
@@ -24,11 +24,13 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
+import org.junit.AfterClass;
 import org.apache.cassandra.db.LegacyLayout.CellGrouper;
 import org.apache.cassandra.db.LegacyLayout.LegacyBound;
 import org.apache.cassandra.db.LegacyLayout.LegacyCell;
 import org.apache.cassandra.db.LegacyLayout.LegacyRangeTombstone;
 import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.marshal.CompositeType;
 import org.apache.cassandra.db.marshal.MapType;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.db.rows.BufferCell;
@@ -50,6 +52,7 @@
 import org.junit.Test;
 
 import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.config.DatabaseDescriptor;
@@ -71,20 +74,29 @@
 
 public class LegacyLayoutTest
 {
-    static final String KEYSPACE = "Keyspace1";
+    static Util.PartitionerSwitcher sw;
+    static String KEYSPACE = "Keyspace1";
 
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
-        DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
+        DatabaseDescriptor.daemonInitialization();
+        sw = Util.switchPartitioner(Murmur3Partitioner.instance);
         SchemaLoader.loadSchema();
         SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(1));
     }
 
+    @AfterClass
+    public static void resetPartitioner()
+    {
+        sw.close();
+    }
+
     @Test
     public void testFromUnfilteredRowIterator() throws Throwable
     {
         CFMetaData table = CFMetaData.Builder.create("ks", "table")
+                                             .withPartitioner(Murmur3Partitioner.instance)
                                              .addPartitionKey("k", Int32Type.instance)
                                              .addRegularColumn("a", SetType.getInstance(Int32Type.instance, true))
                                              .addRegularColumn("b", SetType.getInstance(Int32Type.instance, true))
@@ -172,13 +184,14 @@
 
         UntypedResultSet rs = QueryProcessor.executeInternal(String.format("SELECT * FROM \"%s\".legacy_ka_repeated_rt WHERE k1=1", KEYSPACE));
         assertEquals(3, rs.size());
+
         UntypedResultSet rs2 = QueryProcessor.executeInternal(String.format("SELECT * FROM \"%s\".legacy_ka_repeated_rt WHERE k1=1 AND c1=1", KEYSPACE));
         assertEquals(3, rs2.size());
+
         for (int i = 1; i <= 3; i++)
         {
             UntypedResultSet rs3 = QueryProcessor.executeInternal(String.format("SELECT * FROM \"%s\".legacy_ka_repeated_rt WHERE k1=1 AND c1=1 AND c2=%s", KEYSPACE, i));
             assertEquals(1, rs3.size());
-
         }
 
     }
@@ -213,7 +226,7 @@
         Row staticRow = builder.build();
 
         builder = BTreeRow.unsortedBuilder(0);
-        builder.newRow(new Clustering(UTF8Serializer.instance.serialize("s"), UTF8Serializer.instance.serialize("anything")));
+        builder.newRow(new BufferClustering(UTF8Serializer.instance.serialize("s"), UTF8Serializer.instance.serialize("anything")));
         builder.addCell(new BufferCell(v, 1L, Cell.NO_TTL, Cell.NO_DELETION_TIME, Int32Serializer.instance.serialize(1), null));
         Row row = builder.build();
 
@@ -273,8 +286,8 @@
 
         Row.Builder builder;
         builder = BTreeRow.unsortedBuilder(0);
-        builder.newRow(new Clustering(UTF8Serializer.instance.serialize("a")));
-        builder.addCell(BufferCell.live(table, v, 0L, Int32Serializer.instance.serialize(1), null));
+        builder.newRow(new BufferClustering(UTF8Serializer.instance.serialize("a")));
+        builder.addCell(BufferCell.live(v, 0L, Int32Serializer.instance.serialize(1), null));
         builder.addComplexDeletion(bug, new DeletionTime(1L, 1));
         Row row = builder.build();
 
@@ -325,7 +338,7 @@
          }
          */
 
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
         Keyspace.setInitialized();
         CFMetaData table = CFMetaData.Builder.create("ks", "cf")
                                              .addPartitionKey("k", Int32Type.instance)
@@ -365,8 +378,8 @@
         ByteBuffer one = Int32Type.instance.decompose(1);
         ByteBuffer two = Int32Type.instance.decompose(2);
         PartitionUpdate p = new PartitionUpdate(table, table.decorateKey(one), table.partitionColumns(), 0);
-        p.add(new RangeTombstone(Slice.make(new Slice.Bound(ClusteringPrefix.Kind.EXCL_START_BOUND, new ByteBuffer[] { one, one }),
-                                            new Slice.Bound(ClusteringPrefix.Kind.INCL_END_BOUND, new ByteBuffer[] { two })),
+        p.add(new RangeTombstone(Slice.make(new ClusteringBound(ClusteringPrefix.Kind.EXCL_START_BOUND, new ByteBuffer[] { one, one }),
+                                            new ClusteringBound(ClusteringPrefix.Kind.INCL_END_BOUND, new ByteBuffer[] { two })),
                                  new DeletionTime(1, 1)
         ));
 
@@ -386,8 +399,8 @@
         SerializationHelper helper = new SerializationHelper(cfm, MessagingService.VERSION_22, SerializationHelper.Flag.LOCAL, ColumnFilter.all(cfm));
         LegacyLayout.CellGrouper cg = new LegacyLayout.CellGrouper(cfm, helper);
 
-        Slice.Bound startBound = Slice.Bound.create(ClusteringPrefix.Kind.INCL_START_BOUND, new ByteBuffer[] {bytes(2)});
-        Slice.Bound endBound = Slice.Bound.create(ClusteringPrefix.Kind.EXCL_END_BOUND, new ByteBuffer[] {bytes(2)});
+        ClusteringBound startBound = ClusteringBound.create(ClusteringPrefix.Kind.INCL_START_BOUND, new ByteBuffer[] {bytes(2)});
+        ClusteringBound endBound = ClusteringBound.create(ClusteringPrefix.Kind.EXCL_END_BOUND, new ByteBuffer[] {bytes(2)});
         LegacyLayout.LegacyBound start = new LegacyLayout.LegacyBound(startBound, false, cfm.getColumnDefinition(bytes("v")));
         LegacyLayout.LegacyBound end = new LegacyLayout.LegacyBound(endBound, false, cfm.getColumnDefinition(bytes("v")));
         LegacyLayout.LegacyRangeTombstone lrt = new LegacyLayout.LegacyRangeTombstone(start, end, new DeletionTime(2, 1588598040));
@@ -395,15 +408,15 @@
 
         // add a real cell
         LegacyLayout.LegacyCell cell = new LegacyLayout.LegacyCell(LegacyLayout.LegacyCell.Kind.REGULAR,
-                                                                   new LegacyLayout.LegacyCellName(new Clustering(bytes(2)),
+                                                                   new LegacyLayout.LegacyCellName(Clustering.make(bytes(2)),
                                                                                                    cfm.getColumnDefinition(bytes("v")),
                                                                                                    bytes("g")),
                                                                    bytes("v"), 3, Integer.MAX_VALUE, 0);
         assertTrue(cg.addAtom(cell));
 
         // add legacy range tombstone where collection name is null for the end bound (this gets translated to a row tombstone)
-        startBound = Slice.Bound.create(ClusteringPrefix.Kind.EXCL_START_BOUND, new ByteBuffer[] {bytes(2)});
-        endBound = Slice.Bound.create(ClusteringPrefix.Kind.EXCL_END_BOUND, new ByteBuffer[] {bytes(2)});
+        startBound = ClusteringBound.create(ClusteringPrefix.Kind.EXCL_START_BOUND, new ByteBuffer[] {bytes(2)});
+        endBound = ClusteringBound.create(ClusteringPrefix.Kind.EXCL_END_BOUND, new ByteBuffer[] {bytes(2)});
         start = new LegacyLayout.LegacyBound(startBound, false, cfm.getColumnDefinition(bytes("v")));
         end = new LegacyLayout.LegacyBound(endBound, false, null);
         assertTrue(cg.addAtom(new LegacyLayout.LegacyRangeTombstone(start, end, new DeletionTime(1, 1588598040))));
@@ -451,15 +464,15 @@
                                                              SerializationHelper.Flag.LOCAL,
                                                              filter);
         CellGrouper grouper = new CellGrouper(cfm, helper);
-        Clustering clustering = new Clustering(bytes(1));
+        Clustering clustering = new BufferClustering(bytes(1));
 
         // We add a cell for a, then a collection tombstone for b, and then a cell for c (for the same clustering).
         // All those additions should return 'true' as all belong to the same row.
         LegacyCell ca = cell(clustering, cfm.getColumnDefinition(bytes("a")), bytes("v1"), 1);
         assertTrue(grouper.addAtom(ca));
 
-        Slice.Bound startBound = Slice.Bound.inclusiveStartOf(bytes(1));
-        Slice.Bound endBound = Slice.Bound.inclusiveEndOf(bytes(1));
+        ClusteringBound startBound = ClusteringBound.inclusiveStartOf(bytes(1));
+        ClusteringBound endBound = ClusteringBound.inclusiveEndOf(bytes(1));
         ColumnDefinition bDef = cfm.getColumnDefinition(bytes("b"));
         assert bDef != null;
         LegacyBound start = new LegacyBound(startBound, false, bDef);
@@ -471,4 +484,53 @@
         LegacyCell cc = cell(clustering, cfm.getColumnDefinition(bytes("c")), bytes("v2"), 1);
         assertTrue(grouper.addAtom(cc));
     }
+
+    @Test
+    public void testSubColumnCellNameSparseTable() throws UnknownColumnException
+    {
+        // Sparse supercolumn table with statically defined subcolumn from column_metadata, "static_subcolumn".
+        CFMetaData cfm = CFMetaData.Builder.create("ks", "table", false, true, true, false)
+                                           .addPartitionKey("key", Int32Type.instance)
+                                           .addClusteringColumn("column1", Int32Type.instance)
+                                           .addRegularColumn("", MapType.getInstance(UTF8Type.instance, UTF8Type.instance, true))
+                                           .addRegularColumn("static_subcolumn", Int32Type.instance)
+                                           .build();
+
+        assertDecodedCellNameEquals(cfm, bytes("key"), cfm.compactValueColumn(), bytes("key"));
+        assertDecodedCellNameEquals(cfm, bytes("column1"), cfm.compactValueColumn(), bytes("column1"));
+        assertDecodedCellNameEquals(cfm, bytes(""), cfm.compactValueColumn(), bytes(""));
+
+        assertDecodedCellNameEquals(cfm, bytes("static_subcolumn"), cfm.getColumnDefinition(bytes("static_subcolumn")), null);
+        assertDecodedCellNameEquals(cfm, bytes("regular_cellname"), cfm.compactValueColumn(), bytes("regular_cellname"));
+    }
+
+    @Test
+    public void testSubColumnCellNameDenseTable() throws UnknownColumnException
+    {
+        // Dense supercolumn table, with no statically defined subcolumns.
+        CFMetaData cfm = CFMetaData.Builder.createSuper("ks", "table", false)
+                                           .addPartitionKey("key", Int32Type.instance)
+                                           .addClusteringColumn("column1", Int32Type.instance)
+                                           .addRegularColumn("", MapType.getInstance(UTF8Type.instance, UTF8Type.instance, true))
+                                           .build();
+
+        assertDecodedCellNameEquals(cfm, bytes("key"), cfm.compactValueColumn(), bytes("key"));
+        assertDecodedCellNameEquals(cfm, bytes("column1"), cfm.compactValueColumn(), bytes("column1"));
+        assertDecodedCellNameEquals(cfm, bytes(""), cfm.compactValueColumn(), bytes(""));
+        assertDecodedCellNameEquals(cfm, bytes("column2"), cfm.compactValueColumn(), bytes("column2"));
+        assertDecodedCellNameEquals(cfm, bytes("value"), cfm.compactValueColumn(), bytes("value"));
+    }
+
+    private void assertDecodedCellNameEquals(CFMetaData cfm,
+                                             ByteBuffer subColumn,
+                                             ColumnDefinition columnDefinition,
+                                             ByteBuffer collectionElement)
+    throws UnknownColumnException
+    {
+        ByteBuffer cellNameBuffer = CompositeType.build(bytes(1), subColumn);
+        LegacyLayout.LegacyCellName decodedCellName = LegacyLayout.decodeCellName(cfm, cellNameBuffer, false);
+        assertArrayEquals(new ByteBuffer[]{bytes(1)}, decodedCellName.clustering.getRawValues());
+        assertEquals(columnDefinition, decodedCellName.column);
+        assertEquals(collectionElement, decodedCellName.collectionElement);
+    }
 }
diff --git a/test/unit/org/apache/cassandra/db/LegacyLayoutValidationTest.java b/test/unit/org/apache/cassandra/db/LegacyLayoutValidationTest.java
index 068d2a2..4d565ca 100644
--- a/test/unit/org/apache/cassandra/db/LegacyLayoutValidationTest.java
+++ b/test/unit/org/apache/cassandra/db/LegacyLayoutValidationTest.java
@@ -41,6 +41,11 @@
 {
     static final String KEYSPACE = "ks";
 
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     private static final CFMetaData FIXED = CFMetaData.Builder.create("ks", "cf")
                                                               .addPartitionKey("k", Int32Type.instance)
                                                               .addClusteringColumn("c1", Int32Type.instance)
@@ -72,7 +77,7 @@
     @Test
     public void fixedClusteringSuccess()
     {
-        Clustering clustering = new Clustering(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2));
+        Clustering clustering = Clustering.make(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2));
         ByteBuffer serialized = LegacyLayout.encodeClustering(FIXED, clustering);
         LegacyLayout.decodeClustering(FIXED, serialized);
     }
@@ -80,7 +85,7 @@
     @Test (expected = MarshalException.class)
     public void fixedClusteringFailure()
     {
-        Clustering clustering = new Clustering(Int32Type.instance.decompose(1), hexToBytes("07000000000001"));
+        Clustering clustering = Clustering.make(Int32Type.instance.decompose(1), hexToBytes("07000000000001"));
         ByteBuffer serialized = LegacyLayout.encodeClustering(FIXED, clustering);
         LegacyLayout.decodeClustering(FIXED, serialized);
     }
@@ -88,7 +93,7 @@
     @Test
     public void variableClusteringSuccess()
     {
-        Clustering clustering = new Clustering(UTF8Type.instance.decompose("one"), UTF8Type.instance.decompose("two,three"));
+        Clustering clustering = Clustering.make(UTF8Type.instance.decompose("one"), UTF8Type.instance.decompose("two,three"));
         ByteBuffer serialized = LegacyLayout.encodeClustering(VARIABLE, clustering);
         LegacyLayout.decodeClustering(VARIABLE, serialized);
     }
@@ -96,7 +101,7 @@
     @Test
     public void fixedCompactClusteringSuccess()
     {
-        Clustering clustering = new Clustering(Int32Type.instance.decompose(2));
+        Clustering clustering = Clustering.make(Int32Type.instance.decompose(2));
         ByteBuffer serialized = LegacyLayout.encodeClustering(COMPACT_FIXED, clustering);
         LegacyLayout.decodeClustering(COMPACT_FIXED, serialized);
     }
@@ -104,7 +109,7 @@
     @Test (expected = MarshalException.class)
     public void fixedCompactClusteringFailure()
     {
-        Clustering clustering = new Clustering(hexToBytes("07000000000001"));
+        Clustering clustering = Clustering.make(hexToBytes("07000000000001"));
         ByteBuffer serialized = LegacyLayout.encodeClustering(COMPACT_FIXED, clustering);
         LegacyLayout.decodeClustering(COMPACT_FIXED, serialized);
     }
@@ -112,7 +117,7 @@
     @Test
     public void variableCompactClusteringSuccess()
     {
-        Clustering clustering = new Clustering(UTF8Type.instance.decompose("two,three"));
+        Clustering clustering = Clustering.make(UTF8Type.instance.decompose("two,three"));
         ByteBuffer serialized = LegacyLayout.encodeClustering(COMPACT_VARIABLE, clustering);
         LegacyLayout.decodeClustering(COMPACT_VARIABLE, serialized);
     }
@@ -120,7 +125,7 @@
     @Test
     public void fixedBoundSuccess()
     {
-        Clustering clustering = new Clustering(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2));
+        Clustering clustering = Clustering.make(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2));
         ByteBuffer serialized = LegacyLayout.encodeClustering(FIXED, clustering);
         LegacyLayout.decodeSliceBound(FIXED, serialized, true);
     }
@@ -128,7 +133,7 @@
     @Test (expected = MarshalException.class)
     public void fixedBoundFailure()
     {
-        Clustering clustering = new Clustering(Int32Type.instance.decompose(1), hexToBytes("07000000000001"));
+        Clustering clustering = Clustering.make(Int32Type.instance.decompose(1), hexToBytes("07000000000001"));
         ByteBuffer serialized = LegacyLayout.encodeClustering(FIXED, clustering);
         LegacyLayout.decodeSliceBound(FIXED, serialized, true);
     }
@@ -136,7 +141,7 @@
     @Test
     public void variableBoundSuccess()
     {
-        Clustering clustering = new Clustering(UTF8Type.instance.decompose("one"), UTF8Type.instance.decompose("two,three"));
+        Clustering clustering = Clustering.make(UTF8Type.instance.decompose("one"), UTF8Type.instance.decompose("two,three"));
         ByteBuffer serialized = LegacyLayout.encodeClustering(VARIABLE, clustering);
         LegacyLayout.decodeSliceBound(VARIABLE, serialized, true);
     }
@@ -144,7 +149,7 @@
     @Test
     public void fixedCompactBoundSuccess()
     {
-        Clustering clustering = new Clustering(Int32Type.instance.decompose(1));
+        Clustering clustering = Clustering.make(Int32Type.instance.decompose(1));
         ByteBuffer serialized = LegacyLayout.encodeClustering(COMPACT_FIXED, clustering);
         LegacyLayout.decodeSliceBound(COMPACT_FIXED, serialized, true);
     }
@@ -152,7 +157,7 @@
     @Test (expected = MarshalException.class)
     public void fixedCompactBoundFailure()
     {
-        Clustering clustering = new Clustering(hexToBytes("07000000000001"));
+        Clustering clustering = Clustering.make(hexToBytes("07000000000001"));
         ByteBuffer serialized = LegacyLayout.encodeClustering(COMPACT_FIXED, clustering);
         LegacyLayout.decodeSliceBound(COMPACT_FIXED, serialized, true);
     }
@@ -160,7 +165,7 @@
     @Test
     public void variableCompactBoundSuccess()
     {
-        Clustering clustering = new Clustering(UTF8Type.instance.decompose("one"));
+        Clustering clustering = Clustering.make(UTF8Type.instance.decompose("one"));
         ByteBuffer serialized = LegacyLayout.encodeClustering(COMPACT_VARIABLE, clustering);
         LegacyLayout.decodeSliceBound(COMPACT_VARIABLE, serialized, true);
     }
@@ -179,7 +184,7 @@
     {
         DecoratedKey dk = DatabaseDescriptor.getPartitioner().decorateKey(Int32Type.instance.decompose(1000000));
         LegacyLayout.LegacyDeletionInfo deletionInfo = LegacyLayout.LegacyDeletionInfo.live();
-        Clustering clustering = new Clustering(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2));
+        Clustering clustering = Clustering.make(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2));
         Iterator<LegacyCell> cells = Iterators.forArray(cell(FIXED, clustering, "v1", Int32Type.instance.decompose(3)),
                                                         cell(FIXED, clustering, "v2", Int32Type.instance.decompose(4)));
         try (UnfilteredRowIterator iter = LegacyLayout.toUnfilteredRowIterator(FIXED, dk, deletionInfo, cells))
@@ -194,7 +199,7 @@
     {
         DecoratedKey dk = DatabaseDescriptor.getPartitioner().decorateKey(Int32Type.instance.decompose(1000000));
         LegacyLayout.LegacyDeletionInfo deletionInfo = LegacyLayout.LegacyDeletionInfo.live();
-        Clustering clustering = new Clustering(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2));
+        Clustering clustering = Clustering.make(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2));
         Iterator<LegacyCell> cells = Iterators.forArray(cell(FIXED, clustering, "v1", Int32Type.instance.decompose(3)),
                                                         cell(FIXED, clustering, "v2", hexToBytes("0000")));
         try (UnfilteredRowIterator iter = LegacyLayout.toUnfilteredRowIterator(FIXED, dk, deletionInfo, cells))
@@ -209,7 +214,7 @@
     {
         DecoratedKey dk = DatabaseDescriptor.getPartitioner().decorateKey(Int32Type.instance.decompose(1000000));
         LegacyLayout.LegacyDeletionInfo deletionInfo = LegacyLayout.LegacyDeletionInfo.live();
-        Clustering clustering = new Clustering(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2));
+        Clustering clustering = Clustering.make(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2));
         Iterator<LegacyCell> cells = Iterators.forArray(cell(VARIABLE, clustering, "v1", UTF8Type.instance.decompose("3")),
                                                         cell(VARIABLE, clustering, "v2", hexToBytes("0000")));
         try (UnfilteredRowIterator iter = LegacyLayout.toUnfilteredRowIterator(VARIABLE, dk, deletionInfo, cells))
diff --git a/test/unit/org/apache/cassandra/db/LivenessInfoTest.java b/test/unit/org/apache/cassandra/db/LivenessInfoTest.java
index b08023c..83d670c 100644
--- a/test/unit/org/apache/cassandra/db/LivenessInfoTest.java
+++ b/test/unit/org/apache/cassandra/db/LivenessInfoTest.java
@@ -21,7 +21,7 @@
 import static org.junit.Assert.*;
 
 import org.apache.cassandra.utils.FBUtilities;
-
+import org.apache.hadoop.mapred.machines_jsp;
 import org.junit.Test;
 
 public class LivenessInfoTest
@@ -45,7 +45,7 @@
 
         // timestamp supersedes for mv expired liveness
         first = LivenessInfo.create(100, 0, nowInSeconds);
-        second = LivenessInfo.create(99, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds);
+        second = LivenessInfo.withExpirationTime(99, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds);
         assertSupersedes(first, second);
 
         // timestamp ties, ttl supersedes non-ttl
@@ -63,18 +63,18 @@
         assertSupersedes(second, first);
 
         // timestamp ties, mv expired liveness supersedes normal ttl
-        first = LivenessInfo.create(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds);
+        first = LivenessInfo.withExpirationTime(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds);
         second = LivenessInfo.expiring(100, 1000, nowInSeconds);
         assertSupersedes(first, second);
 
         // timestamp ties, mv expired liveness supersedes non-ttl
-        first = LivenessInfo.create(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds);
+        first = LivenessInfo.withExpirationTime(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds);
         second = LivenessInfo.create(100, 0, nowInSeconds);
         assertSupersedes(first, second);
 
         // timestamp ties, both are mv expired liveness, local deletion time win
-        first = LivenessInfo.create(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds + 1);
-        second = LivenessInfo.create(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds);
+        first = LivenessInfo.withExpirationTime(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds + 1);
+        second = LivenessInfo.withExpirationTime(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds);
         assertSupersedes(first, second);
     }
 
@@ -91,9 +91,9 @@
         assertIsLive(LivenessInfo.expiring(100, 2, nowInSeconds), nowInSeconds, true);
         assertIsLive(LivenessInfo.expiring(100, 2, nowInSeconds), nowInSeconds + 3, false);
 
-        assertIsLive(LivenessInfo.create(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds), nowInSeconds - 3, false);
-        assertIsLive(LivenessInfo.create(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds), nowInSeconds, false);
-        assertIsLive(LivenessInfo.create(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds), nowInSeconds + 3, false);
+        assertIsLive(LivenessInfo.withExpirationTime(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds), nowInSeconds - 3, false);
+        assertIsLive(LivenessInfo.withExpirationTime(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds), nowInSeconds, false);
+        assertIsLive(LivenessInfo.withExpirationTime(100, LivenessInfo.EXPIRED_LIVENESS_TTL, nowInSeconds), nowInSeconds + 3, false);
     }
 
     /**
diff --git a/test/unit/org/apache/cassandra/db/NativeCellTest.java b/test/unit/org/apache/cassandra/db/NativeCellTest.java
new file mode 100644
index 0000000..9b18f65
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/NativeCellTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.db;
+
+import java.nio.ByteBuffer;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.SetType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.utils.concurrent.OpOrder;
+import org.apache.cassandra.utils.memory.HeapAllocator;
+import org.apache.cassandra.utils.memory.NativeAllocator;
+import org.apache.cassandra.utils.memory.NativePool;
+
+public class NativeCellTest
+{
+
+    private static final Logger logger = LoggerFactory.getLogger(NativeCellTest.class);
+    private static final NativeAllocator nativeAllocator = new NativePool(Integer.MAX_VALUE,
+                                                                          Integer.MAX_VALUE,
+                                                                          1f,
+                                                                          () -> CompletableFuture.completedFuture(true)).newAllocator();
+    @SuppressWarnings("resource")
+    private static final OpOrder.Group group = new OpOrder().start();
+    private static Random rand;
+
+    @BeforeClass
+    public static void setUp()
+    {
+        long seed = System.currentTimeMillis();
+        logger.info("Seed : {}", seed);
+        rand = new Random(seed);
+    }
+
+    @Test
+    public void testCells()
+    {
+        for (int run = 0 ; run < 1000 ; run++)
+        {
+            Row.Builder builder = BTreeRow.unsortedBuilder(1);
+            builder.newRow(rndclustering());
+            int count = 1 + rand.nextInt(10);
+            for (int i = 0 ; i < count ; i++)
+                rndcd(builder);
+            test(builder.build());
+        }
+    }
+
+    private static Clustering rndclustering()
+    {
+        int count = 1 + rand.nextInt(100);
+        ByteBuffer[] values = new ByteBuffer[count];
+        int size = rand.nextInt(65535);
+        for (int i = 0 ; i < count ; i++)
+        {
+            int twiceShare = 1 + (2 * size) / (count - i);
+            int nextSize = Math.min(size, rand.nextInt(twiceShare));
+            if (nextSize < 10 && rand.nextBoolean())
+                continue;
+
+            byte[] bytes = new byte[nextSize];
+            rand.nextBytes(bytes);
+            values[i] = ByteBuffer.wrap(bytes);
+            size -= nextSize;
+        }
+        return Clustering.make(values);
+    }
+
+    private static void rndcd(Row.Builder builder)
+    {
+        ColumnDefinition col = rndcol();
+        if (!col.isComplex())
+        {
+            builder.addCell(rndcell(col));
+        }
+        else
+        {
+            int count = 1 + rand.nextInt(100);
+            for (int i = 0 ; i < count ; i++)
+                builder.addCell(rndcell(col));
+        }
+    }
+
+    private static ColumnDefinition rndcol()
+    {
+        UUID uuid = new UUID(rand.nextLong(), rand.nextLong());
+        boolean isComplex = rand.nextBoolean();
+        return new ColumnDefinition("",
+                                    "",
+                                    ColumnIdentifier.getInterned(uuid.toString(), false),
+                                    isComplex ? new SetType<>(BytesType.instance, true) : BytesType.instance,
+                                    -1,
+                                    ColumnDefinition.Kind.REGULAR);
+    }
+
+    private static Cell rndcell(ColumnDefinition col)
+    {
+        long timestamp = rand.nextLong();
+        int ttl = rand.nextInt();
+        int localDeletionTime = rand.nextInt();
+        byte[] value = new byte[rand.nextInt(sanesize(expdecay()))];
+        rand.nextBytes(value);
+        CellPath path = null;
+        if (col.isComplex())
+        {
+            byte[] pathbytes = new byte[rand.nextInt(sanesize(expdecay()))];
+            rand.nextBytes(value);
+            path = CellPath.create(ByteBuffer.wrap(pathbytes));
+        }
+
+        return new BufferCell(col, timestamp, ttl, localDeletionTime, ByteBuffer.wrap(value), path);
+    }
+
+    private static int expdecay()
+    {
+        return 1 << Integer.numberOfTrailingZeros(Integer.lowestOneBit(rand.nextInt()));
+    }
+
+    private static int sanesize(int randomsize)
+    {
+        return Math.min(Math.max(1, randomsize), 1 << 26);
+    }
+
+    private static void test(Row row)
+    {
+        Row nrow = clone(row, nativeAllocator.rowBuilder(group));
+        Row brow = clone(row, HeapAllocator.instance.cloningBTreeRowBuilder());
+        Assert.assertEquals(row, nrow);
+        Assert.assertEquals(row, brow);
+        Assert.assertEquals(nrow, brow);
+
+        Assert.assertEquals(row.clustering(), nrow.clustering());
+        Assert.assertEquals(row.clustering(), brow.clustering());
+        Assert.assertEquals(nrow.clustering(), brow.clustering());
+
+        ClusteringComparator comparator = new ClusteringComparator(UTF8Type.instance);
+        Assert.assertEquals(0, comparator.compare(row.clustering(), nrow.clustering()));
+        Assert.assertEquals(0, comparator.compare(row.clustering(), brow.clustering()));
+        Assert.assertEquals(0, comparator.compare(nrow.clustering(), brow.clustering()));
+    }
+
+    private static Row clone(Row row, Row.Builder builder)
+    {
+        return Rows.copy(row, builder).build();
+    }
+
+}
diff --git a/test/unit/org/apache/cassandra/db/OldFormatDeserializerTest.java b/test/unit/org/apache/cassandra/db/OldFormatDeserializerTest.java
index 886b191..cb37229 100644
--- a/test/unit/org/apache/cassandra/db/OldFormatDeserializerTest.java
+++ b/test/unit/org/apache/cassandra/db/OldFormatDeserializerTest.java
@@ -20,9 +20,11 @@
 
 import java.util.function.Supplier;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.UnfilteredDeserializer.OldFormatDeserializer.UnfilteredIterator;
 import org.apache.cassandra.db.marshal.Int32Type;
 import org.apache.cassandra.db.rows.RangeTombstoneMarker;
@@ -37,6 +39,12 @@
 
 public class OldFormatDeserializerTest
 {
+    @BeforeClass
+    public static void beforeClass()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testRangeTombstones() throws Exception
     {
@@ -157,8 +165,8 @@
 
     private static LegacyLayout.LegacyBound bound(int b, boolean isStart)
     {
-        return new LegacyLayout.LegacyBound(isStart ? Slice.Bound.inclusiveStartOf(ByteBufferUtil.bytes(b)) : Slice.Bound.inclusiveEndOf(ByteBufferUtil.bytes(b)),
+        return new LegacyLayout.LegacyBound(isStart ? ClusteringBound.inclusiveStartOf(ByteBufferUtil.bytes(b)) : ClusteringBound.inclusiveEndOf(ByteBufferUtil.bytes(b)),
                                             false,
                                             null);
     }
-}
\ No newline at end of file
+}
diff --git a/test/unit/org/apache/cassandra/db/PartitionRangeReadTest.java b/test/unit/org/apache/cassandra/db/PartitionRangeReadTest.java
index b567f72..c418afc 100644
--- a/test/unit/org/apache/cassandra/db/PartitionRangeReadTest.java
+++ b/test/unit/org/apache/cassandra/db/PartitionRangeReadTest.java
@@ -477,27 +477,27 @@
 
         // without range merger, there will be 2 batches requested: 1st batch with 1 range and 2nd batch with remaining ranges
         Iterator<StorageProxy.RangeForQuery> ranges = rangeIterator(command, keyspace, false);
-        StorageProxy.RangeCommandIterator data = new StorageProxy.RangeCommandIterator(ranges, command, 1, 1000, vnodeCount, keyspace, ONE);
+        StorageProxy.RangeCommandIterator data = new StorageProxy.RangeCommandIterator(ranges, command, 1, 1000, vnodeCount, keyspace, ONE, System.nanoTime());
         verifyRangeCommandIterator(data, rows, 2, vnodeCount);
 
         // without range merger and initial cf=5, there will be 1 batches requested: 5 vnode ranges for 1st batch
         ranges = rangeIterator(command, keyspace, false);
-        data = new StorageProxy.RangeCommandIterator(ranges, command, vnodeCount, 1000, vnodeCount, keyspace, ONE);
+        data = new StorageProxy.RangeCommandIterator(ranges, command, vnodeCount, 1000, vnodeCount, keyspace, ONE, System.nanoTime());
         verifyRangeCommandIterator(data, rows, 1, vnodeCount);
 
         // without range merger and max cf=1, there will be 5 batches requested: 1 vnode range per batch
         ranges = rangeIterator(command, keyspace, false);
-        data = new StorageProxy.RangeCommandIterator(ranges, command, 1, 1, vnodeCount, keyspace, ONE);
+        data = new StorageProxy.RangeCommandIterator(ranges, command, 1, 1, vnodeCount, keyspace, ONE, System.nanoTime());
         verifyRangeCommandIterator(data, rows, vnodeCount, vnodeCount);
 
         // with range merger, there will be only 1 batch requested, as all ranges share the same replica - localhost
         ranges = rangeIterator(command, keyspace, true);
-        data = new StorageProxy.RangeCommandIterator(ranges, command, 1, 1000, vnodeCount, keyspace, ONE);
+        data = new StorageProxy.RangeCommandIterator(ranges, command, 1, 1000, vnodeCount, keyspace, ONE, System.nanoTime());
         verifyRangeCommandIterator(data, rows, 1, vnodeCount);
 
         // with range merger and max cf=1, there will be only 1 batch requested, as all ranges share the same replica - localhost
         ranges = rangeIterator(command, keyspace, true);
-        data = new StorageProxy.RangeCommandIterator(ranges, command, 1, 1, vnodeCount, keyspace, ONE);
+        data = new StorageProxy.RangeCommandIterator(ranges, command, 1, 1, vnodeCount, keyspace, ONE, System.nanoTime());
         verifyRangeCommandIterator(data, rows, 1, vnodeCount);
     }
 
diff --git a/test/unit/org/apache/cassandra/db/RangeTombstoneListTest.java b/test/unit/org/apache/cassandra/db/RangeTombstoneListTest.java
index f40abe9..d3dc835 100644
--- a/test/unit/org/apache/cassandra/db/RangeTombstoneListTest.java
+++ b/test/unit/org/apache/cassandra/db/RangeTombstoneListTest.java
@@ -605,7 +605,7 @@
 
     private static Clustering clustering(int i)
     {
-        return new Clustering(bb(i));
+        return Clustering.make(bb(i));
     }
 
     private static ByteBuffer bb(int i)
@@ -620,12 +620,12 @@
 
     private static RangeTombstone rt(int start, boolean startInclusive, int end, boolean endInclusive, long tstamp)
     {
-        return new RangeTombstone(Slice.make(Slice.Bound.create(cmp, true, startInclusive, start), Slice.Bound.create(cmp, false, endInclusive, end)), new DeletionTime(tstamp, 0));
+        return new RangeTombstone(Slice.make(ClusteringBound.create(cmp, true, startInclusive, start), ClusteringBound.create(cmp, false, endInclusive, end)), new DeletionTime(tstamp, 0));
     }
 
     private static RangeTombstone rt(int start, int end, long tstamp, int delTime)
     {
-        return new RangeTombstone(Slice.make(Slice.Bound.inclusiveStartOf(bb(start)), Slice.Bound.inclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
+        return new RangeTombstone(Slice.make(ClusteringBound.inclusiveStartOf(bb(start)), ClusteringBound.inclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
     }
 
     private static RangeTombstone rtei(int start, int end, long tstamp)
@@ -635,7 +635,7 @@
 
     private static RangeTombstone rtei(int start, int end, long tstamp, int delTime)
     {
-        return new RangeTombstone(Slice.make(Slice.Bound.exclusiveStartOf(bb(start)), Slice.Bound.inclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
+        return new RangeTombstone(Slice.make(ClusteringBound.exclusiveStartOf(bb(start)), ClusteringBound.inclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
     }
 
     private static RangeTombstone rtie(int start, int end, long tstamp)
@@ -645,26 +645,26 @@
 
     private static RangeTombstone rtie(int start, int end, long tstamp, int delTime)
     {
-        return new RangeTombstone(Slice.make(Slice.Bound.inclusiveStartOf(bb(start)), Slice.Bound.exclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
+        return new RangeTombstone(Slice.make(ClusteringBound.inclusiveStartOf(bb(start)), ClusteringBound.exclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
     }
 
     private static RangeTombstone atLeast(int start, long tstamp, int delTime)
     {
-        return new RangeTombstone(Slice.make(Slice.Bound.inclusiveStartOf(bb(start)), Slice.Bound.TOP), new DeletionTime(tstamp, delTime));
+        return new RangeTombstone(Slice.make(ClusteringBound.inclusiveStartOf(bb(start)), ClusteringBound.TOP), new DeletionTime(tstamp, delTime));
     }
 
     private static RangeTombstone atMost(int end, long tstamp, int delTime)
     {
-        return new RangeTombstone(Slice.make(Slice.Bound.BOTTOM, Slice.Bound.inclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
+        return new RangeTombstone(Slice.make(ClusteringBound.BOTTOM, ClusteringBound.inclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
     }
 
     private static RangeTombstone lessThan(int end, long tstamp, int delTime)
     {
-        return new RangeTombstone(Slice.make(Slice.Bound.BOTTOM, Slice.Bound.exclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
+        return new RangeTombstone(Slice.make(ClusteringBound.BOTTOM, ClusteringBound.exclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
     }
 
     private static RangeTombstone greaterThan(int start, long tstamp, int delTime)
     {
-        return new RangeTombstone(Slice.make(Slice.Bound.exclusiveStartOf(bb(start)), Slice.Bound.TOP), new DeletionTime(tstamp, delTime));
+        return new RangeTombstone(Slice.make(ClusteringBound.exclusiveStartOf(bb(start)), ClusteringBound.TOP), new DeletionTime(tstamp, delTime));
     }
 }
diff --git a/test/unit/org/apache/cassandra/db/RangeTombstoneTest.java b/test/unit/org/apache/cassandra/db/RangeTombstoneTest.java
index 967a85c..363ef72 100644
--- a/test/unit/org/apache/cassandra/db/RangeTombstoneTest.java
+++ b/test/unit/org/apache/cassandra/db/RangeTombstoneTest.java
@@ -114,20 +114,20 @@
 
         for (int i : live)
             assertTrue("Row " + i + " should be live",
-                       partition.getRow(new Clustering(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
+                       partition.getRow(Clustering.make(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
         for (int i : dead)
             assertFalse("Row " + i + " shouldn't be live",
-                        partition.getRow(new Clustering(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
+                        partition.getRow(Clustering.make(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
 
         // Queries by slices
         partition = Util.getOnlyPartitionUnfiltered(Util.cmd(cfs, key).fromIncl(7).toIncl(30).build());
 
         for (int i : new int[]{ 7, 8, 9, 11, 13, 15, 17, 28, 29, 30 })
             assertTrue("Row " + i + " should be live",
-                       partition.getRow(new Clustering(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
+                       partition.getRow(Clustering.make(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
         for (int i : new int[]{ 10, 12, 14, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27 })
             assertFalse("Row " + i + " shouldn't be live",
-                        partition.getRow(new Clustering(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
+                        partition.getRow(Clustering.make(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
     }
 
     @Test
@@ -212,8 +212,8 @@
         assertEquals(1, rt.size());
 
         Slices.Builder sb = new Slices.Builder(cfs.getComparator());
-        sb.add(Slice.Bound.create(cfs.getComparator(), true, true, 1), Slice.Bound.create(cfs.getComparator(), false, true, 10));
-        sb.add(Slice.Bound.create(cfs.getComparator(), true, true, 16), Slice.Bound.create(cfs.getComparator(), false, true, 20));
+        sb.add(ClusteringBound.create(cfs.getComparator(), true, true, 1), ClusteringBound.create(cfs.getComparator(), false, true, 10));
+        sb.add(ClusteringBound.create(cfs.getComparator(), true, true, 16), ClusteringBound.create(cfs.getComparator(), false, true, 20));
 
         partition = Util.getOnlyPartitionUnfiltered(SinglePartitionReadCommand.create(cfs.metadata, FBUtilities.nowInSeconds(), Util.dk(key), sb.build()));
         rt = rangeTombstones(partition);
@@ -391,7 +391,6 @@
         Keyspace keyspace = Keyspace.open(KSNAME);
         ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CFNAME);
         boolean enforceStrictLiveness = cfs.metadata.enforceStrictLiveness();
-
         // Inserting data
         String key = "k2";
 
@@ -415,13 +414,13 @@
 
         for (int i = 0; i < 5; i++)
             assertTrue("Row " + i + " should be live",
-                       partition.getRow(new Clustering(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
+                       partition.getRow(Clustering.make(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
         for (int i = 16; i < 20; i++)
             assertTrue("Row " + i + " should be live",
-                       partition.getRow(new Clustering(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
+                       partition.getRow(Clustering.make(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
         for (int i = 5; i <= 15; i++)
             assertFalse("Row " + i + " shouldn't be live",
-                        partition.getRow(new Clustering(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
+                        partition.getRow(Clustering.make(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
 
         // Compact everything and re-test
         CompactionManager.instance.performMaximal(cfs, false);
@@ -429,15 +428,15 @@
 
         for (int i = 0; i < 5; i++)
             assertTrue("Row " + i + " should be live",
-                       partition.getRow(new Clustering(bb(i))).hasLiveData(FBUtilities.nowInSeconds(),
-                                                                           enforceStrictLiveness));
+                       partition.getRow(Clustering.make(bb(i))).hasLiveData(FBUtilities.nowInSeconds(),
+                                                                            enforceStrictLiveness));
         for (int i = 16; i < 20; i++)
             assertTrue("Row " + i + " should be live",
-                       partition.getRow(new Clustering(bb(i))).hasLiveData(FBUtilities.nowInSeconds(),
-                                                                           enforceStrictLiveness));
+                       partition.getRow(Clustering.make(bb(i))).hasLiveData(FBUtilities.nowInSeconds(),
+                                                                            enforceStrictLiveness));
         for (int i = 5; i <= 15; i++)
             assertFalse("Row " + i + " shouldn't be live",
-                        partition.getRow(new Clustering(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
+                        partition.getRow(Clustering.make(bb(i))).hasLiveData(nowInSec, enforceStrictLiveness));
     }
 
     @Test
diff --git a/test/unit/org/apache/cassandra/db/ReadCommandTest.java b/test/unit/org/apache/cassandra/db/ReadCommandTest.java
new file mode 100644
index 0000000..774645e
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/ReadCommandTest.java
@@ -0,0 +1,437 @@
+/*
+ * 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.db;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.Util;
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.filter.DataLimits;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.db.marshal.AsciiType;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.partitions.FilteredPartition;
+import org.apache.cassandra.db.partitions.PartitionIterator;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.RowIterator;
+import org.apache.cassandra.db.rows.SerializationHelper;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.db.rows.UnfilteredRowIterators;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.FBUtilities;
+
+import static org.junit.Assert.assertEquals;
+
+public class ReadCommandTest
+{
+    private static final String KEYSPACE = "ReadCommandTest";
+    private static final String CF1 = "Standard1";
+    private static final String CF2 = "Standard2";
+    private static final String CF3 = "Standard3";
+    private static final String CF4 = "Standard4";
+    private static final String CF5 = "Standard5";
+
+    @BeforeClass
+    public static void defineSchema() throws ConfigurationException
+    {
+        DatabaseDescriptor.daemonInitialization();
+
+        CFMetaData metadata1 = SchemaLoader.standardCFMD(KEYSPACE, CF1);
+
+        CFMetaData metadata2 = CFMetaData.Builder.create(KEYSPACE, CF2)
+                                                         .addPartitionKey("key", BytesType.instance)
+                                                         .addClusteringColumn("col", AsciiType.instance)
+                                                         .addRegularColumn("a", AsciiType.instance)
+                                                         .addRegularColumn("b", AsciiType.instance).build();
+
+        CFMetaData metadata3 = CFMetaData.Builder.create(KEYSPACE, CF3)
+                                                 .addPartitionKey("key", BytesType.instance)
+                                                 .addClusteringColumn("col", AsciiType.instance)
+                                                 .addRegularColumn("a", AsciiType.instance)
+                                                 .addRegularColumn("b", AsciiType.instance)
+                                                 .addRegularColumn("c", AsciiType.instance)
+                                                 .addRegularColumn("d", AsciiType.instance)
+                                                 .addRegularColumn("e", AsciiType.instance)
+                                                 .addRegularColumn("f", AsciiType.instance).build();
+
+        CFMetaData metadata4 = CFMetaData.Builder.create(KEYSPACE, CF4)
+                                                 .addPartitionKey("key", BytesType.instance)
+                                                 .addClusteringColumn("col", AsciiType.instance)
+                                                 .addRegularColumn("a", AsciiType.instance)
+                                                 .addRegularColumn("b", AsciiType.instance)
+                                                 .addRegularColumn("c", AsciiType.instance)
+                                                 .addRegularColumn("d", AsciiType.instance)
+                                                 .addRegularColumn("e", AsciiType.instance)
+                                                 .addRegularColumn("f", AsciiType.instance).build();
+
+        CFMetaData metadata5 = CFMetaData.Builder.create(KEYSPACE, CF5)
+                                                 .addPartitionKey("key", BytesType.instance)
+                                                 .addClusteringColumn("col", AsciiType.instance)
+                                                 .addRegularColumn("a", AsciiType.instance)
+                                                 .addRegularColumn("b", AsciiType.instance)
+                                                 .addRegularColumn("c", AsciiType.instance)
+                                                 .addRegularColumn("d", AsciiType.instance)
+                                                 .addRegularColumn("e", AsciiType.instance)
+                                                 .addRegularColumn("f", AsciiType.instance).build();
+
+
+        SchemaLoader.prepareServer();
+        SchemaLoader.createKeyspace(KEYSPACE,
+                                    KeyspaceParams.simple(1),
+                                    metadata1,
+                                    metadata2,
+                                    metadata3,
+                                    metadata4,
+                                    metadata5);
+    }
+
+    @Test
+    public void testPartitionRangeAbort() throws Exception
+    {
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(CF1);
+
+        new RowUpdateBuilder(cfs.metadata, 0, ByteBufferUtil.bytes("key1"))
+                .clustering("Column1")
+                .add("val", ByteBufferUtil.bytes("abcd"))
+                .build()
+                .apply();
+
+        cfs.forceBlockingFlush();
+
+        new RowUpdateBuilder(cfs.metadata, 0, ByteBufferUtil.bytes("key2"))
+                .clustering("Column1")
+                .add("val", ByteBufferUtil.bytes("abcd"))
+                .build()
+                .apply();
+
+        ReadCommand readCommand = Util.cmd(cfs).build();
+        assertEquals(2, Util.getAll(readCommand).size());
+
+        readCommand.abort();
+        assertEquals(0, Util.getAll(readCommand).size());
+    }
+
+    @Test
+    public void testSinglePartitionSliceAbort() throws Exception
+    {
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(CF2);
+
+        cfs.truncateBlocking();
+
+        new RowUpdateBuilder(cfs.metadata, 0, ByteBufferUtil.bytes("key"))
+                .clustering("cc")
+                .add("a", ByteBufferUtil.bytes("abcd"))
+                .build()
+                .apply();
+
+        cfs.forceBlockingFlush();
+
+        new RowUpdateBuilder(cfs.metadata, 0, ByteBufferUtil.bytes("key"))
+                .clustering("dd")
+                .add("a", ByteBufferUtil.bytes("abcd"))
+                .build()
+                .apply();
+
+        ReadCommand readCommand = Util.cmd(cfs, Util.dk("key")).build();
+
+        List<FilteredPartition> partitions = Util.getAll(readCommand);
+        assertEquals(1, partitions.size());
+        assertEquals(2, partitions.get(0).rowCount());
+
+        readCommand.abort();
+        assertEquals(0, Util.getAll(readCommand).size());
+    }
+
+    @Test
+    public void testSinglePartitionNamesAbort() throws Exception
+    {
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(CF2);
+
+        cfs.truncateBlocking();
+
+        new RowUpdateBuilder(cfs.metadata, 0, ByteBufferUtil.bytes("key"))
+                .clustering("cc")
+                .add("a", ByteBufferUtil.bytes("abcd"))
+                .build()
+                .apply();
+
+        cfs.forceBlockingFlush();
+
+        new RowUpdateBuilder(cfs.metadata, 0, ByteBufferUtil.bytes("key"))
+                .clustering("dd")
+                .add("a", ByteBufferUtil.bytes("abcd"))
+                .build()
+                .apply();
+
+        ReadCommand readCommand = Util.cmd(cfs, Util.dk("key")).includeRow("cc").includeRow("dd").build();
+
+        List<FilteredPartition> partitions = Util.getAll(readCommand);
+        assertEquals(1, partitions.size());
+        assertEquals(2, partitions.get(0).rowCount());
+
+        readCommand.abort();
+        assertEquals(0, Util.getAll(readCommand).size());
+    }
+
+    @Test
+    public void testSinglePartitionGroupMerge() throws Exception
+    {
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(CF3);
+
+        String[][][] groups = new String[][][] {
+            new String[][] {
+                new String[] { "1", "key1", "aa", "a" }, // "1" indicates to create the data, "-1" to delete the row
+                new String[] { "1", "key2", "bb", "b" },
+                new String[] { "1", "key3", "cc", "c" }
+            },
+            new String[][] {
+                new String[] { "1", "key3", "dd", "d" },
+                new String[] { "1", "key2", "ee", "e" },
+                new String[] { "1", "key1", "ff", "f" }
+            },
+            new String[][] {
+                new String[] { "1", "key6", "aa", "a" },
+                new String[] { "1", "key5", "bb", "b" },
+                new String[] { "1", "key4", "cc", "c" }
+            },
+            new String[][] {
+                new String[] { "-1", "key6", "aa", "a" },
+                new String[] { "-1", "key2", "bb", "b" }
+            }
+        };
+
+        // Given the data above, when the keys are sorted and the deletions removed, we should
+        // get these clustering rows in this order
+        String[] expectedRows = new String[] { "aa", "ff", "ee", "cc", "dd", "cc", "bb"};
+        int nowInSeconds = FBUtilities.nowInSeconds();
+
+        List<UnfilteredPartitionIterator> iterators = writeAndThenReadPartitions(cfs, groups, nowInSeconds);
+        UnfilteredPartitionIterators.MergeListener listener =
+            new UnfilteredPartitionIterators.MergeListener()
+            {
+                public UnfilteredRowIterators.MergeListener getRowMergeListener(DecoratedKey partitionKey, List<UnfilteredRowIterator> versions)
+                {
+                    return null;
+                }
+
+                public void close()
+                {
+
+                }
+            };
+
+        try (PartitionIterator partitionIterator = UnfilteredPartitionIterators.filter(UnfilteredPartitionIterators.merge(iterators, nowInSeconds, listener), nowInSeconds))
+        {
+
+            int i = 0;
+            int numPartitions = 0;
+            while (partitionIterator.hasNext())
+            {
+                numPartitions++;
+                try(RowIterator rowIterator = partitionIterator.next())
+                {
+                    while (rowIterator.hasNext())
+                    {
+                        Row row = rowIterator.next();
+                        assertEquals("col=" + expectedRows[i++], row.clustering().toString(cfs.metadata));
+                        //System.out.print(row.toString(cfs.metadata, true));
+                    }
+                }
+            }
+
+            assertEquals(5, numPartitions);
+            assertEquals(expectedRows.length, i);
+        }
+    }
+
+    /**
+     * This test will create several partitions with several rows each. Then, it will perform up to 5 row deletions on
+     * some partitions. We check that when reading the partitions, the maximum number of tombstones reported in the
+     * metrics is indeed equal to 5.
+     */
+    @Test
+    public void testCountDeletedRows() throws Exception
+    {
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(CF4);
+
+        String[][][] groups = new String[][][] {
+                new String[][] {
+                        new String[] { "1", "key1", "aa", "a" }, // "1" indicates to create the data, "-1" to delete the
+                                                                 // row
+                        new String[] { "1", "key2", "bb", "b" },
+                        new String[] { "1", "key3", "cc", "c" }
+                },
+                new String[][] {
+                        new String[] { "1", "key3", "dd", "d" },
+                        new String[] { "1", "key2", "ee", "e" },
+                        new String[] { "1", "key1", "ff", "f" }
+                },
+                new String[][] {
+                        new String[] { "1", "key6", "aa", "a" },
+                        new String[] { "1", "key5", "bb", "b" },
+                        new String[] { "1", "key4", "cc", "c" }
+                },
+                new String[][] {
+                        new String[] { "1", "key2", "aa", "a" },
+                        new String[] { "1", "key2", "cc", "c" },
+                        new String[] { "1", "key2", "dd", "d" }
+                },
+                new String[][] {
+                        new String[] { "-1", "key6", "aa", "a" },
+                        new String[] { "-1", "key2", "bb", "b" },
+                        new String[] { "-1", "key2", "ee", "e" },
+                        new String[] { "-1", "key2", "aa", "a" },
+                        new String[] { "-1", "key2", "cc", "c" },
+                        new String[] { "-1", "key2", "dd", "d" }
+                }
+        };
+        int nowInSeconds = FBUtilities.nowInSeconds();
+
+        writeAndThenReadPartitions(cfs, groups, nowInSeconds);
+
+        assertEquals(5, cfs.metric.tombstoneScannedHistogram.cf.getSnapshot().getMax());
+    }
+
+    /**
+     * This test will create several partitions with several rows each and no deletions. We check that when reading the
+     * partitions, the maximum number of tombstones reported in the metrics is equal to 1, which is apparently the
+     * default max value for histograms in the metrics lib (equivalent to having no element reported).
+     */
+    @Test
+    public void testCountWithNoDeletedRow() throws Exception
+    {
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(CF5);
+
+        String[][][] groups = new String[][][] {
+                new String[][] {
+                        new String[] { "1", "key1", "aa", "a" }, // "1" indicates to create the data, "-1" to delete the
+                                                                 // row
+                        new String[] { "1", "key2", "bb", "b" },
+                        new String[] { "1", "key3", "cc", "c" }
+                },
+                new String[][] {
+                        new String[] { "1", "key3", "dd", "d" },
+                        new String[] { "1", "key2", "ee", "e" },
+                        new String[] { "1", "key1", "ff", "f" }
+                },
+                new String[][] {
+                        new String[] { "1", "key6", "aa", "a" },
+                        new String[] { "1", "key5", "bb", "b" },
+                        new String[] { "1", "key4", "cc", "c" }
+                }
+        };
+
+        int nowInSeconds = FBUtilities.nowInSeconds();
+
+        writeAndThenReadPartitions(cfs, groups, nowInSeconds);
+
+        assertEquals(1, cfs.metric.tombstoneScannedHistogram.cf.getSnapshot().getMax());
+    }
+
+    /**
+     * Writes rows to the column family store using the groups as input and then reads them. Returns the iterators from
+     * the read.
+     */
+    private List<UnfilteredPartitionIterator> writeAndThenReadPartitions(ColumnFamilyStore cfs, String[][][] groups,
+            int nowInSeconds) throws IOException
+    {
+        List<ByteBuffer> buffers = new ArrayList<>(groups.length);
+        ColumnFilter columnFilter = ColumnFilter.allColumnsBuilder(cfs.metadata).build();
+        RowFilter rowFilter = RowFilter.create();
+        Slice slice = Slice.make(ClusteringBound.BOTTOM, ClusteringBound.TOP);
+        ClusteringIndexSliceFilter sliceFilter = new ClusteringIndexSliceFilter(
+                Slices.with(cfs.metadata.comparator, slice), false);
+
+        for (String[][] group : groups)
+        {
+            cfs.truncateBlocking();
+
+            List<SinglePartitionReadCommand> commands = new ArrayList<>(group.length);
+
+            for (String[] data : group)
+            {
+                if (data[0].equals("1"))
+                {
+                    new RowUpdateBuilder(cfs.metadata, 0, ByteBufferUtil.bytes(data[1]))
+                            .clustering(data[2])
+                            .add(data[3], ByteBufferUtil.bytes("blah"))
+                            .build()
+                            .apply();
+                }
+                else
+                {
+                    RowUpdateBuilder.deleteRow(cfs.metadata, FBUtilities.timestampMicros(),
+                            ByteBufferUtil.bytes(data[1]), data[2]).apply();
+                }
+                commands.add(SinglePartitionReadCommand.create(cfs.metadata, nowInSeconds, columnFilter, rowFilter,
+                        DataLimits.NONE, Util.dk(data[1]), sliceFilter));
+            }
+
+            cfs.forceBlockingFlush();
+
+            ReadQuery query = new SinglePartitionReadCommand.Group(commands, DataLimits.NONE);
+
+            try (ReadExecutionController executionController = query.executionController();
+                    UnfilteredPartitionIterator iter = query.executeLocally(executionController);
+                    DataOutputBuffer buffer = new DataOutputBuffer())
+            {
+                UnfilteredPartitionIterators.serializerForIntraNode().serialize(iter,
+                        columnFilter,
+                        buffer,
+                        MessagingService.current_version);
+                buffers.add(buffer.buffer());
+            }
+        }
+
+        // deserialize, merge and check the results are all there
+        List<UnfilteredPartitionIterator> iterators = new ArrayList<>();
+
+        for (ByteBuffer buffer : buffers)
+        {
+            try (DataInputBuffer in = new DataInputBuffer(buffer, true))
+            {
+                iterators.add(UnfilteredPartitionIterators.serializerForIntraNode().deserialize(in,
+                        MessagingService.current_version,
+                        cfs.metadata,
+                        columnFilter,
+                        SerializationHelper.Flag.LOCAL));
+            }
+        }
+
+        return iterators;
+    }
+
+}
diff --git a/test/unit/org/apache/cassandra/db/ReadMessageTest.java b/test/unit/org/apache/cassandra/db/ReadMessageTest.java
index d801b32..f76bf93 100644
--- a/test/unit/org/apache/cassandra/db/ReadMessageTest.java
+++ b/test/unit/org/apache/cassandra/db/ReadMessageTest.java
@@ -30,6 +30,7 @@
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.commitlog.CommitLogTestReplayer;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.db.rows.Row;
@@ -56,6 +57,8 @@
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
+        DatabaseDescriptor.daemonInitialization();
+
         CFMetaData cfForReadMetadata = CFMetaData.Builder.create(KEYSPACE1, CF_FOR_READ_TEST)
                                                             .addPartitionKey("key", BytesType.instance)
                                                             .addClusteringColumn("col1", AsciiType.instance)
@@ -195,7 +198,9 @@
 
         Checker checker = new Checker(cfs.metadata.getColumnDefinition(ByteBufferUtil.bytes("commit1")),
                                       cfsnocommit.metadata.getColumnDefinition(ByteBufferUtil.bytes("commit2")));
-        CommitLogTestReplayer.examineCommitLog(checker);
+
+        CommitLogTestReplayer replayer = new CommitLogTestReplayer(checker);
+        replayer.examineCommitLog();
 
         assertTrue(checker.commitLogMessageFound);
         assertFalse(checker.noCommitLogMessageFound);
@@ -219,7 +224,7 @@
         {
             for (PartitionUpdate upd : mutation.getPartitionUpdates())
             {
-                Row r = upd.getRow(new Clustering(ByteBufferUtil.bytes("c")));
+                Row r = upd.getRow(Clustering.make(ByteBufferUtil.bytes("c")));
                 if (r != null)
                 {
                     if (r.getCell(withCommit) != null)
diff --git a/test/unit/org/apache/cassandra/db/RecoveryManagerFlushedTest.java b/test/unit/org/apache/cassandra/db/RecoveryManagerFlushedTest.java
index 989f1e2..f48dc40 100644
--- a/test/unit/org/apache/cassandra/db/RecoveryManagerFlushedTest.java
+++ b/test/unit/org/apache/cassandra/db/RecoveryManagerFlushedTest.java
@@ -37,14 +37,16 @@
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.ParameterizedClass;
-import org.apache.cassandra.db.commitlog.CommitLog;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.db.compaction.CompactionManager;
+import org.apache.cassandra.db.commitlog.CommitLog;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.compress.DeflateCompressor;
 import org.apache.cassandra.io.compress.LZ4Compressor;
 import org.apache.cassandra.io.compress.SnappyCompressor;
 import org.apache.cassandra.schema.KeyspaceParams;
-import org.apache.cassandra.schema.SchemaKeyspace;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.security.EncryptionContextGenerator;
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.utils.FBUtilities;
 
@@ -57,6 +59,29 @@
     private static final String CF_STANDARD1 = "Standard1";
     private static final String CF_STANDARD2 = "Standard2";
 
+    public RecoveryManagerFlushedTest(ParameterizedClass commitLogCompression, EncryptionContext encryptionContext)
+    {
+        DatabaseDescriptor.setCommitLogCompression(commitLogCompression);
+        DatabaseDescriptor.setEncryptionContext(encryptionContext);
+    }
+
+    @Parameters()
+    public static Collection<Object[]> generateData()
+    {
+        return Arrays.asList(new Object[][]{
+            {null, EncryptionContextGenerator.createDisabledContext()}, // No compression, no encryption
+            {null, EncryptionContextGenerator.createContext(true)}, // Encryption
+            {new ParameterizedClass(LZ4Compressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()},
+            {new ParameterizedClass(SnappyCompressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()},
+            {new ParameterizedClass(DeflateCompressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()}});
+    }
+
+    @Before
+    public void setUp() throws IOException
+    {
+        CommitLog.instance.resetUnsafe(true);
+    }
+
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
@@ -69,34 +94,13 @@
                                     SchemaLoader.standardCFMD(KEYSPACE1, CF_STANDARD2));
     }
 
-    public RecoveryManagerFlushedTest(ParameterizedClass commitLogCompression)
-    {
-        DatabaseDescriptor.setCommitLogCompression(commitLogCompression);
-    }
-
-    @Before
-    public void setUp() throws IOException
-    {
-        CommitLog.instance.resetUnsafe(true);
-    }
-
-    @Parameters()
-    public static Collection<Object[]> generateData()
-    {
-        return Arrays.asList(new Object[][] {
-                { null }, // No compression
-                { new ParameterizedClass(LZ4Compressor.class.getName(), Collections.emptyMap()) },
-                { new ParameterizedClass(SnappyCompressor.class.getName(), Collections.emptyMap()) },
-                { new ParameterizedClass(DeflateCompressor.class.getName(), Collections.emptyMap()) } });
-    }
-
     @Test
     /* test that commit logs do not replay flushed data */
     public void testWithFlush() throws Exception
     {
         // Flush everything that may be in the commit log now to start fresh
-        FBUtilities.waitOnFutures(Keyspace.open(SystemKeyspace.NAME).flush());
-        FBUtilities.waitOnFutures(Keyspace.open(SchemaKeyspace.NAME).flush());
+        FBUtilities.waitOnFutures(Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).flush());
+        FBUtilities.waitOnFutures(Keyspace.open(SchemaConstants.SCHEMA_KEYSPACE_NAME).flush());
 
 
         CompactionManager.instance.disableAutoCompaction();
diff --git a/test/unit/org/apache/cassandra/db/RecoveryManagerMissingHeaderTest.java b/test/unit/org/apache/cassandra/db/RecoveryManagerMissingHeaderTest.java
index 8ac7c5d..8897700 100644
--- a/test/unit/org/apache/cassandra/db/RecoveryManagerMissingHeaderTest.java
+++ b/test/unit/org/apache/cassandra/db/RecoveryManagerMissingHeaderTest.java
@@ -36,14 +36,16 @@
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.ParameterizedClass;
-import org.apache.cassandra.db.commitlog.CommitLog;
 import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.db.commitlog.CommitLog;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.compress.DeflateCompressor;
 import org.apache.cassandra.io.compress.LZ4Compressor;
 import org.apache.cassandra.io.compress.SnappyCompressor;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.security.EncryptionContextGenerator;
 
 @RunWith(Parameterized.class)
 public class RecoveryManagerMissingHeaderTest
@@ -54,9 +56,21 @@
     private static final String KEYSPACE2 = "RecoveryManager3Test2";
     private static final String CF_STANDARD3 = "Standard3";
 
-    public RecoveryManagerMissingHeaderTest(ParameterizedClass commitLogCompression)
+    public RecoveryManagerMissingHeaderTest(ParameterizedClass commitLogCompression, EncryptionContext encryptionContext)
     {
         DatabaseDescriptor.setCommitLogCompression(commitLogCompression);
+        DatabaseDescriptor.setEncryptionContext(encryptionContext);
+    }
+
+    @Parameters()
+    public static Collection<Object[]> generateData()
+    {
+        return Arrays.asList(new Object[][]{
+            {null, EncryptionContextGenerator.createDisabledContext()}, // No compression, no encryption
+            {null, EncryptionContextGenerator.createContext(true)}, // Encryption
+            {new ParameterizedClass(LZ4Compressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()},
+            {new ParameterizedClass(SnappyCompressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()},
+            {new ParameterizedClass(DeflateCompressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()}});
     }
 
     @Before
@@ -65,16 +79,6 @@
         CommitLog.instance.resetUnsafe(true);
     }
 
-    @Parameters()
-    public static Collection<Object[]> generateData()
-    {
-        return Arrays.asList(new Object[][] {
-                { null }, // No compression
-                { new ParameterizedClass(LZ4Compressor.class.getName(), Collections.emptyMap()) },
-                { new ParameterizedClass(SnappyCompressor.class.getName(), Collections.emptyMap()) },
-                { new ParameterizedClass(DeflateCompressor.class.getName(), Collections.emptyMap()) } });
-    }
-
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
@@ -114,7 +118,7 @@
 
         CommitLog.instance.resetUnsafe(false);
 
-        Assert.assertTrue(Util.equal(upd1, Util.getOnlyPartitionUnfiltered(Util.cmd(keyspace1.getColumnFamilyStore(CF_STANDARD1), dk).build()).unfilteredIterator()));
-        Assert.assertTrue(Util.equal(upd2, Util.getOnlyPartitionUnfiltered(Util.cmd(keyspace2.getColumnFamilyStore(CF_STANDARD3), dk).build()).unfilteredIterator()));
+        Assert.assertTrue(Util.sameContent(upd1, Util.getOnlyPartitionUnfiltered(Util.cmd(keyspace1.getColumnFamilyStore(CF_STANDARD1), dk).build()).unfilteredIterator()));
+        Assert.assertTrue(Util.sameContent(upd2, Util.getOnlyPartitionUnfiltered(Util.cmd(keyspace2.getColumnFamilyStore(CF_STANDARD3), dk).build()).unfilteredIterator()));
     }
 }
diff --git a/test/unit/org/apache/cassandra/db/RecoveryManagerTest.java b/test/unit/org/apache/cassandra/db/RecoveryManagerTest.java
index 57bd044..cc9c667 100644
--- a/test/unit/org/apache/cassandra/db/RecoveryManagerTest.java
+++ b/test/unit/org/apache/cassandra/db/RecoveryManagerTest.java
@@ -23,7 +23,27 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.Util;
+import org.apache.cassandra.concurrent.NamedThreadFactory;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.db.context.CounterContext;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.io.compress.DeflateCompressor;
+import org.apache.cassandra.io.compress.LZ4Compressor;
+import org.apache.cassandra.io.compress.SnappyCompressor;
 
 import org.junit.Assert;
 import org.junit.Before;
@@ -33,27 +53,16 @@
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import static org.junit.Assert.assertEquals;
 
 import org.apache.cassandra.SchemaLoader;
-import org.apache.cassandra.Util;
-import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.config.ParameterizedClass;
 import org.apache.cassandra.db.commitlog.CommitLog;
 import org.apache.cassandra.db.commitlog.CommitLogArchiver;
-import org.apache.cassandra.db.context.CounterContext;
-import org.apache.cassandra.db.rows.Row;
-import org.apache.cassandra.db.rows.UnfilteredRowIterator;
-import org.apache.cassandra.exceptions.ConfigurationException;
-import org.apache.cassandra.io.compress.DeflateCompressor;
-import org.apache.cassandra.io.compress.LZ4Compressor;
-import org.apache.cassandra.io.compress.SnappyCompressor;
 import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.security.EncryptionContextGenerator;
 import org.apache.cassandra.utils.ByteBufferUtil;
-
-import static org.junit.Assert.assertEquals;
+import org.apache.cassandra.db.commitlog.CommitLogReplayer;
 
 @RunWith(Parameterized.class)
 public class RecoveryManagerTest
@@ -68,6 +77,29 @@
     private static final String KEYSPACE2 = "RecoveryManagerTest2";
     private static final String CF_STANDARD3 = "Standard3";
 
+    public RecoveryManagerTest(ParameterizedClass commitLogCompression, EncryptionContext encryptionContext)
+    {
+        DatabaseDescriptor.setCommitLogCompression(commitLogCompression);
+        DatabaseDescriptor.setEncryptionContext(encryptionContext);
+    }
+
+    @Parameters()
+    public static Collection<Object[]> generateData()
+    {
+        return Arrays.asList(new Object[][]{
+            {null, EncryptionContextGenerator.createDisabledContext()}, // No compression, no encryption
+            {null, EncryptionContextGenerator.createContext(true)}, // Encryption
+            {new ParameterizedClass(LZ4Compressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()},
+            {new ParameterizedClass(SnappyCompressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()},
+            {new ParameterizedClass(DeflateCompressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()}});
+    }
+
+    @Before
+    public void setUp() throws IOException
+    {
+        CommitLog.instance.resetUnsafe(true);
+    }
+
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
@@ -85,24 +117,11 @@
     @Before
     public void clearData()
     {
+        // clear data
         Keyspace.open(KEYSPACE1).getColumnFamilyStore(CF_STANDARD1).truncateBlocking();
         Keyspace.open(KEYSPACE1).getColumnFamilyStore(CF_COUNTER1).truncateBlocking();
         Keyspace.open(KEYSPACE2).getColumnFamilyStore(CF_STANDARD3).truncateBlocking();
     }
-    public RecoveryManagerTest(ParameterizedClass commitLogCompression)
-    {
-        DatabaseDescriptor.setCommitLogCompression(commitLogCompression);
-    }
-
-    @Parameters()
-    public static Collection<Object[]> generateData()
-    {
-        return Arrays.asList(new Object[][] {
-                { null }, // No compression
-                { new ParameterizedClass(LZ4Compressor.class.getName(), Collections.emptyMap()) },
-                { new ParameterizedClass(SnappyCompressor.class.getName(), Collections.emptyMap()) },
-                { new ParameterizedClass(DeflateCompressor.class.getName(), Collections.emptyMap()) } });
-    }
 
     @Test
     public void testNothingToRecover() throws IOException
@@ -111,6 +130,76 @@
     }
 
     @Test
+    public void testRecoverBlocksOnBytesOutstanding() throws Exception
+    {
+        long originalMaxOutstanding = CommitLogReplayer.MAX_OUTSTANDING_REPLAY_BYTES;
+        CommitLogReplayer.MAX_OUTSTANDING_REPLAY_BYTES = 1;
+        CommitLogReplayer.MutationInitiator originalInitiator = CommitLogReplayer.mutationInitiator;
+        MockInitiator mockInitiator = new MockInitiator();
+        CommitLogReplayer.mutationInitiator = mockInitiator;
+        try
+        {
+            CommitLog.instance.resetUnsafe(true);
+            Keyspace keyspace1 = Keyspace.open(KEYSPACE1);
+            Keyspace keyspace2 = Keyspace.open(KEYSPACE2);
+
+            UnfilteredRowIterator upd1 = Util.apply(new RowUpdateBuilder(keyspace1.getColumnFamilyStore(CF_STANDARD1).metadata, 1L, 0, "keymulti")
+                .clustering("col1").add("val", "1")
+                .build());
+
+            UnfilteredRowIterator upd2 = Util.apply(new RowUpdateBuilder(keyspace2.getColumnFamilyStore(CF_STANDARD3).metadata, 1L, 0, "keymulti")
+                                           .clustering("col2").add("val", "1")
+                                           .build());
+
+            keyspace1.getColumnFamilyStore("Standard1").clearUnsafe();
+            keyspace2.getColumnFamilyStore("Standard3").clearUnsafe();
+
+            DecoratedKey dk = Util.dk("keymulti");
+            Assert.assertTrue(Util.getAllUnfiltered(Util.cmd(keyspace1.getColumnFamilyStore(CF_STANDARD1), dk).build()).isEmpty());
+            Assert.assertTrue(Util.getAllUnfiltered(Util.cmd(keyspace2.getColumnFamilyStore(CF_STANDARD3), dk).build()).isEmpty());
+
+            final AtomicReference<Throwable> err = new AtomicReference<Throwable>();
+            Thread t = NamedThreadFactory.createThread(() ->
+            {
+                try
+                {
+                    CommitLog.instance.resetUnsafe(false); // disassociate segments from live CL
+                }
+                catch (Throwable x)
+                {
+                    err.set(x);
+                }
+            });
+            t.start();
+            Assert.assertTrue(mockInitiator.blocked.tryAcquire(1, 20, TimeUnit.SECONDS));
+            Thread.sleep(100);
+            Assert.assertTrue(t.isAlive());
+            mockInitiator.blocker.release(Integer.MAX_VALUE);
+            t.join(20 * 1000);
+
+            if (err.get() != null)
+                throw new RuntimeException(err.get());
+
+            if (t.isAlive())
+            {
+                Throwable toPrint = new Throwable();
+                toPrint.setStackTrace(Thread.getAllStackTraces().get(t));
+                toPrint.printStackTrace(System.out);
+            }
+            Assert.assertFalse(t.isAlive());
+
+            Assert.assertTrue(Util.sameContent(upd1, Util.getOnlyPartitionUnfiltered(Util.cmd(keyspace1.getColumnFamilyStore(CF_STANDARD1), dk).build()).unfilteredIterator()));
+            Assert.assertTrue(Util.sameContent(upd2, Util.getOnlyPartitionUnfiltered(Util.cmd(keyspace2.getColumnFamilyStore(CF_STANDARD3), dk).build()).unfilteredIterator()));
+        }
+        finally
+        {
+            CommitLogReplayer.mutationInitiator = originalInitiator;
+            CommitLogReplayer.MAX_OUTSTANDING_REPLAY_BYTES = originalMaxOutstanding;
+        }
+    }
+
+
+    @Test
     public void testOne() throws IOException
     {
         CommitLog.instance.resetUnsafe(true);
@@ -131,8 +220,8 @@
         CommitLog.instance.resetUnsafe(false);
 
         DecoratedKey dk = Util.dk("keymulti");
-        Assert.assertTrue(Util.equal(upd1, Util.getOnlyPartitionUnfiltered(Util.cmd(keyspace1.getColumnFamilyStore(CF_STANDARD1), dk).build()).unfilteredIterator()));
-        Assert.assertTrue(Util.equal(upd2, Util.getOnlyPartitionUnfiltered(Util.cmd(keyspace2.getColumnFamilyStore(CF_STANDARD3), dk).build()).unfilteredIterator()));
+        Assert.assertTrue(Util.sameContent(upd1, Util.getOnlyPartitionUnfiltered(Util.cmd(keyspace1.getColumnFamilyStore(CF_STANDARD1), dk).build()).unfilteredIterator()));
+        Assert.assertTrue(Util.sameContent(upd2, Util.getOnlyPartitionUnfiltered(Util.cmd(keyspace2.getColumnFamilyStore(CF_STANDARD3), dk).build()).unfilteredIterator()));
     }
 
     @Test
@@ -161,8 +250,8 @@
     @Test
     public void testRecoverPIT() throws Exception
     {
-        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(CF_STANDARD1);
         CommitLog.instance.resetUnsafe(true);
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(CF_STANDARD1);
         Date date = CommitLogArchiver.format.parse("2112:12:12 12:12:12");
         long timeMS = date.getTime() - 5000;
 
@@ -217,8 +306,8 @@
     @Test
     public void testRecoverPITUnordered() throws Exception
     {
-        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(CF_STANDARD1);
         CommitLog.instance.resetUnsafe(true);
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(CF_STANDARD1);
         Date date = CommitLogArchiver.format.parse("2112:12:12 12:12:12");
         long timeMS = date.getTime();
 
@@ -248,4 +337,64 @@
 
         assertEquals(2, Util.getAll(Util.cmd(cfs).build()).size());
     }
+
+    private static class MockInitiator extends CommitLogReplayer.MutationInitiator
+    {
+        final Semaphore blocker = new Semaphore(0);
+        final Semaphore blocked = new Semaphore(0);
+
+        @Override
+        protected Future<Integer> initiateMutation(final Mutation mutation,
+                final long segmentId,
+                final int serializedSize,
+                final int entryLocation,
+                final CommitLogReplayer clr)
+        {
+            final Future<Integer> toWrap = super.initiateMutation(mutation,
+                                                                  segmentId,
+                                                                  serializedSize,
+                                                                  entryLocation,
+                                                                  clr);
+            return new Future<Integer>()
+            {
+
+                @Override
+                public boolean cancel(boolean mayInterruptIfRunning)
+                {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public boolean isCancelled()
+                {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public boolean isDone()
+                {
+                    return blocker.availablePermits() > 0 && toWrap.isDone();
+                }
+
+                @Override
+                public Integer get() throws InterruptedException, ExecutionException
+                {
+                    System.out.println("Got blocker once");
+                    blocked.release();
+                    blocker.acquire();
+                    return toWrap.get();
+                }
+
+                @Override
+                public Integer get(long timeout, TimeUnit unit)
+                        throws InterruptedException, ExecutionException, TimeoutException
+                {
+                    blocked.release();
+                    blocker.tryAcquire(1, timeout, unit);
+                    return toWrap.get(timeout, unit);
+                }
+
+            };
+        }
+    };
 }
diff --git a/test/unit/org/apache/cassandra/db/RecoveryManagerTruncateTest.java b/test/unit/org/apache/cassandra/db/RecoveryManagerTruncateTest.java
index 5a59f1c..738888f 100644
--- a/test/unit/org/apache/cassandra/db/RecoveryManagerTruncateTest.java
+++ b/test/unit/org/apache/cassandra/db/RecoveryManagerTruncateTest.java
@@ -23,13 +23,6 @@
 import java.util.Collection;
 import java.util.Collections;
 
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.DatabaseDescriptor;
@@ -40,8 +33,17 @@
 import org.apache.cassandra.io.compress.LZ4Compressor;
 import org.apache.cassandra.io.compress.SnappyCompressor;
 import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.security.EncryptionContextGenerator;
 
-import static org.junit.Assert.assertTrue;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import static org.junit.Assert.*;
 
 /**
  * Test for the truncate operation.
@@ -52,9 +54,21 @@
     private static final String KEYSPACE1 = "RecoveryManagerTruncateTest";
     private static final String CF_STANDARD1 = "Standard1";
 
-    public RecoveryManagerTruncateTest(ParameterizedClass commitLogCompression)
+    public RecoveryManagerTruncateTest(ParameterizedClass commitLogCompression, EncryptionContext encryptionContext)
     {
         DatabaseDescriptor.setCommitLogCompression(commitLogCompression);
+        DatabaseDescriptor.setEncryptionContext(encryptionContext);
+    }
+
+    @Parameters()
+    public static Collection<Object[]> generateData()
+    {
+        return Arrays.asList(new Object[][]{
+            {null, EncryptionContextGenerator.createDisabledContext()}, // No compression, no encryption
+            {null, EncryptionContextGenerator.createContext(true)}, // Encryption
+            {new ParameterizedClass(LZ4Compressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()},
+            {new ParameterizedClass(SnappyCompressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()},
+            {new ParameterizedClass(DeflateCompressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()}});
     }
 
     @Before
@@ -63,16 +77,6 @@
         CommitLog.instance.resetUnsafe(true);
     }
 
-    @Parameters()
-    public static Collection<Object[]> generateData()
-    {
-        return Arrays.asList(new Object[][] {
-                { null }, // No compression
-                { new ParameterizedClass(LZ4Compressor.class.getName(), Collections.emptyMap()) },
-                { new ParameterizedClass(SnappyCompressor.class.getName(), Collections.emptyMap()) },
-                { new ParameterizedClass(DeflateCompressor.class.getName(), Collections.emptyMap()) } });
-    }
-
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
diff --git a/test/unit/org/apache/cassandra/db/RepairedDataTombstonesTest.java b/test/unit/org/apache/cassandra/db/RepairedDataTombstonesTest.java
index e0d68a4..b814ea6 100644
--- a/test/unit/org/apache/cassandra/db/RepairedDataTombstonesTest.java
+++ b/test/unit/org/apache/cassandra/db/RepairedDataTombstonesTest.java
@@ -176,14 +176,17 @@
         Thread.sleep(1000);
         ReadCommand cmd = Util.cmd(getCurrentColumnFamilyStore()).build();
         int partitionsFound = 0;
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup(); UnfilteredPartitionIterator iterator = cmd.executeLocally(orderGroup))
+        try (ReadExecutionController executionController = cmd.executionController();
+             UnfilteredPartitionIterator iterator = cmd.executeLocally(executionController))
         {
             while (iterator.hasNext())
             {
                 partitionsFound++;
-                UnfilteredRowIterator rowIter = iterator.next();
-                int val = ByteBufferUtil.toInt(rowIter.partitionKey().getKey());
-                assertTrue("val=" + val, val >= 10 && val < 20);
+                try (UnfilteredRowIterator rowIter = iterator.next())
+                {
+                    int val = ByteBufferUtil.toInt(rowIter.partitionKey().getKey());
+                    assertTrue("val=" + val, val >= 10 && val < 20);
+                }
             }
         }
         assertEquals(10, partitionsFound);
@@ -236,10 +239,10 @@
     {
         ReadCommand cmd = Util.cmd(getCurrentColumnFamilyStore()).build();
         int foundRows = 0;
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup();
+        try (ReadExecutionController executionController = cmd.executionController();
              UnfilteredPartitionIterator iterator =
-             includePurgeable ? cmd.queryStorage(getCurrentColumnFamilyStore(), orderGroup) :
-                                cmd.executeLocally(orderGroup))
+             includePurgeable ? cmd.queryStorage(getCurrentColumnFamilyStore(), executionController) :
+                                cmd.executeLocally(executionController))
         {
             while (iterator.hasNext())
             {
@@ -278,10 +281,10 @@
     {
         ReadCommand cmd = Util.cmd(getCurrentColumnFamilyStore(), Util.dk(ByteBufferUtil.bytes(key))).build();
         int foundRows = 0;
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup();
+        try (ReadExecutionController executionController = cmd.executionController();
              UnfilteredPartitionIterator iterator =
-             includePurgeable ? cmd.queryStorage(getCurrentColumnFamilyStore(), orderGroup) :
-                                cmd.executeLocally(orderGroup))
+             includePurgeable ? cmd.queryStorage(getCurrentColumnFamilyStore(), executionController) :
+                                cmd.executeLocally(executionController))
         {
             while (iterator.hasNext())
             {
diff --git a/test/unit/org/apache/cassandra/db/RowCacheTest.java b/test/unit/org/apache/cassandra/db/RowCacheTest.java
index 06aed47..558c187 100644
--- a/test/unit/org/apache/cassandra/db/RowCacheTest.java
+++ b/test/unit/org/apache/cassandra/db/RowCacheTest.java
@@ -553,7 +553,7 @@
         for (int i = offset; i < offset + numberOfRows; i++)
         {
             DecoratedKey key = Util.dk("key" + i);
-            Clustering cl = new Clustering(ByteBufferUtil.bytes("col" + i));
+            Clustering cl = Clustering.make(ByteBufferUtil.bytes("col" + i));
             Util.getAll(Util.cmd(store, key).build());
         }
     }
diff --git a/test/unit/org/apache/cassandra/db/RowIndexEntryTest.java b/test/unit/org/apache/cassandra/db/RowIndexEntryTest.java
index 62c88a0..6c8eed5 100644
--- a/test/unit/org/apache/cassandra/db/RowIndexEntryTest.java
+++ b/test/unit/org/apache/cassandra/db/RowIndexEntryTest.java
@@ -20,94 +20,379 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 
+import com.google.common.primitives.Ints;
+import org.junit.Assert;
+import org.junit.Test;
+
 import org.apache.cassandra.Util;
+import org.apache.cassandra.cache.IMeasurableMemory;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.db.columniterator.AbstractSSTableIterator;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.partitions.ImmutableBTreePartition;
+import org.apache.cassandra.db.rows.AbstractUnfilteredRowIterator;
+import org.apache.cassandra.db.rows.BTreeRow;
+import org.apache.cassandra.db.rows.BufferCell;
+import org.apache.cassandra.db.rows.ColumnData;
 import org.apache.cassandra.db.rows.EncodingStats;
-import org.apache.cassandra.db.partitions.*;
-import org.apache.cassandra.io.sstable.IndexHelper;
+import org.apache.cassandra.db.rows.RangeTombstoneMarker;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.Unfiltered;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.db.rows.UnfilteredSerializer;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.io.sstable.IndexInfo;
+import org.apache.cassandra.io.sstable.format.SSTableFlushObserver;
+import org.apache.cassandra.io.sstable.format.Version;
 import org.apache.cassandra.io.sstable.format.big.BigFormat;
-import org.apache.cassandra.io.util.DataInputBuffer;
-import org.apache.cassandra.io.util.DataOutputBuffer;
-import org.apache.cassandra.io.util.SequentialWriter;
+import org.apache.cassandra.io.util.*;
+import org.apache.cassandra.serializers.LongSerializer;
+import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
-
-import org.junit.Assert;
-import org.junit.Test;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.btree.BTree;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 
 public class RowIndexEntryTest extends CQLTester
 {
-    private static final List<AbstractType<?>> clusterTypes = Collections.<AbstractType<?>>singletonList(LongType.instance);
+    private static final List<AbstractType<?>> clusterTypes = Collections.singletonList(LongType.instance);
     private static final ClusteringComparator comp = new ClusteringComparator(clusterTypes);
-    private static ClusteringPrefix cn(long l)
+
+    private static final byte[] dummy_100k = new byte[100000];
+
+    private static Clustering cn(long l)
     {
         return Util.clustering(comp, l);
     }
 
     @Test
-    public void testArtificialIndexOf() throws IOException
+    public void testC11206AgainstPreviousArray() throws Exception
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(99999);
+        testC11206AgainstPrevious();
+    }
+
+    @Test
+    public void testC11206AgainstPreviousShallow() throws Exception
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(0);
+        testC11206AgainstPrevious();
+    }
+
+    private static void testC11206AgainstPrevious() throws Exception
+    {
+        // partition without IndexInfo
+        try (DoubleSerializer doubleSerializer = new DoubleSerializer())
+        {
+            doubleSerializer.build(null, partitionKey(42L),
+                                   Collections.singletonList(cn(42)),
+                                   0L);
+            assertEquals(doubleSerializer.rieOldSerialized, doubleSerializer.rieNewSerialized);
+        }
+
+        // partition with multiple IndexInfo
+        try (DoubleSerializer doubleSerializer = new DoubleSerializer())
+        {
+            doubleSerializer.build(null, partitionKey(42L),
+                                   Arrays.asList(cn(42), cn(43), cn(44)),
+                                   0L);
+            assertEquals(doubleSerializer.rieOldSerialized, doubleSerializer.rieNewSerialized);
+        }
+
+        // partition with multiple IndexInfo
+        try (DoubleSerializer doubleSerializer = new DoubleSerializer())
+        {
+            doubleSerializer.build(null, partitionKey(42L),
+                                   Arrays.asList(cn(42), cn(43), cn(44), cn(45), cn(46), cn(47), cn(48), cn(49), cn(50), cn(51)),
+                                   0L);
+            assertEquals(doubleSerializer.rieOldSerialized, doubleSerializer.rieNewSerialized);
+        }
+    }
+
+    private static DecoratedKey partitionKey(long l)
+    {
+        ByteBuffer key = LongSerializer.instance.serialize(l);
+        Token token = Murmur3Partitioner.instance.getToken(key);
+        return new BufferDecoratedKey(token, key);
+    }
+
+    private static class DoubleSerializer implements AutoCloseable
     {
         CFMetaData cfMeta = CFMetaData.compile("CREATE TABLE pipe.dev_null (pk bigint, ck bigint, val text, PRIMARY KEY(pk, ck))", "foo");
+        Version version = BigFormat.latestVersion;
 
         DeletionTime deletionInfo = new DeletionTime(FBUtilities.timestampMicros(), FBUtilities.nowInSeconds());
+        LivenessInfo primaryKeyLivenessInfo = LivenessInfo.EMPTY;
+        Row.Deletion deletion = Row.Deletion.LIVE;
 
         SerializationHeader header = new SerializationHeader(true, cfMeta, cfMeta.partitionColumns(), EncodingStats.NO_STATS);
-        IndexHelper.IndexInfo.Serializer indexSerializer = new IndexHelper.IndexInfo.Serializer(cfMeta, BigFormat.latestVersion, header);
 
-        DataOutputBuffer dob = new DataOutputBuffer();
-        dob.writeUnsignedVInt(0);
-        DeletionTime.serializer.serialize(DeletionTime.LIVE, dob);
-        dob.writeUnsignedVInt(3);
-        int off0 = dob.getLength();
-        indexSerializer.serialize(new IndexHelper.IndexInfo(cn(0L), cn(5L), 0, 0, deletionInfo), dob);
-        int off1 = dob.getLength();
-        indexSerializer.serialize(new IndexHelper.IndexInfo(cn(10L), cn(15L), 0, 0, deletionInfo), dob);
-        int off2 = dob.getLength();
-        indexSerializer.serialize(new IndexHelper.IndexInfo(cn(20L), cn(25L), 0, 0, deletionInfo), dob);
-        dob.writeInt(off0);
-        dob.writeInt(off1);
-        dob.writeInt(off2);
+        // create C-11206 + old serializer instances
+        RowIndexEntry.IndexSerializer rieSerializer = new RowIndexEntry.Serializer(cfMeta, version, header);
+        Pre_C_11206_RowIndexEntry.Serializer oldSerializer = new Pre_C_11206_RowIndexEntry.Serializer(cfMeta, version, header);
 
-        @SuppressWarnings("resource") DataOutputBuffer dobRie = new DataOutputBuffer();
-        dobRie.writeUnsignedVInt(42L);
-        dobRie.writeUnsignedVInt(dob.getLength());
-        dobRie.write(dob.buffer());
+        @SuppressWarnings({ "resource", "IOResourceOpenedButNotSafelyClosed" })
+        final DataOutputBuffer rieOutput = new DataOutputBuffer(1024);
+        @SuppressWarnings({ "resource", "IOResourceOpenedButNotSafelyClosed" })
+        final DataOutputBuffer oldOutput = new DataOutputBuffer(1024);
 
-        ByteBuffer buf = dobRie.buffer();
+        final SequentialWriter dataWriterNew;
+        final SequentialWriter dataWriterOld;
+        final org.apache.cassandra.db.ColumnIndex columnIndex;
 
-        RowIndexEntry<IndexHelper.IndexInfo> rie = new RowIndexEntry.Serializer(cfMeta, BigFormat.latestVersion, header).deserialize(new DataInputBuffer(buf, false));
+        RowIndexEntry rieNew;
+        ByteBuffer rieNewSerialized;
+        Pre_C_11206_RowIndexEntry rieOld;
+        ByteBuffer rieOldSerialized;
 
-        Assert.assertEquals(42L, rie.position);
+        DoubleSerializer() throws IOException
+        {
+            SequentialWriterOption option = SequentialWriterOption.newBuilder().bufferSize(1024).build();
+            File f = File.createTempFile("RowIndexEntryTest-", "db");
+            dataWriterNew = new SequentialWriter(f, option);
+            columnIndex = new org.apache.cassandra.db.ColumnIndex(header, dataWriterNew, version, Collections.emptyList(),
+                                                                  rieSerializer.indexInfoSerializer());
 
-        Assert.assertEquals(0, IndexHelper.indexFor(cn(-1L), rie.columnsIndex(), comp, false, -1));
-        Assert.assertEquals(0, IndexHelper.indexFor(cn(5L), rie.columnsIndex(), comp, false, -1));
-        Assert.assertEquals(1, IndexHelper.indexFor(cn(12L), rie.columnsIndex(), comp, false, -1));
-        Assert.assertEquals(2, IndexHelper.indexFor(cn(17L), rie.columnsIndex(), comp, false, -1));
-        Assert.assertEquals(3, IndexHelper.indexFor(cn(100L), rie.columnsIndex(), comp, false, -1));
-        Assert.assertEquals(3, IndexHelper.indexFor(cn(100L), rie.columnsIndex(), comp, false, 0));
-        Assert.assertEquals(3, IndexHelper.indexFor(cn(100L), rie.columnsIndex(), comp, false, 1));
-        Assert.assertEquals(3, IndexHelper.indexFor(cn(100L), rie.columnsIndex(), comp, false, 2));
-        Assert.assertEquals(3, IndexHelper.indexFor(cn(100L), rie.columnsIndex(), comp, false, 3));
+            f = File.createTempFile("RowIndexEntryTest-", "db");
+            dataWriterOld = new SequentialWriter(f, option);
+        }
 
-        Assert.assertEquals(-1, IndexHelper.indexFor(cn(-1L), rie.columnsIndex(), comp, true, -1));
-        Assert.assertEquals(0, IndexHelper.indexFor(cn(5L), rie.columnsIndex(), comp, true, 3));
-        Assert.assertEquals(0, IndexHelper.indexFor(cn(5L), rie.columnsIndex(), comp, true, 2));
-        Assert.assertEquals(1, IndexHelper.indexFor(cn(17L), rie.columnsIndex(), comp, true, 3));
-        Assert.assertEquals(2, IndexHelper.indexFor(cn(100L), rie.columnsIndex(), comp, true, 3));
-        Assert.assertEquals(2, IndexHelper.indexFor(cn(100L), rie.columnsIndex(), comp, true, 4));
-        Assert.assertEquals(1, IndexHelper.indexFor(cn(12L), rie.columnsIndex(), comp, true, 3));
-        Assert.assertEquals(1, IndexHelper.indexFor(cn(12L), rie.columnsIndex(), comp, true, 2));
-        Assert.assertEquals(1, IndexHelper.indexFor(cn(100L), rie.columnsIndex(), comp, true, 1));
-        Assert.assertEquals(2, IndexHelper.indexFor(cn(100L), rie.columnsIndex(), comp, true, 2));
+        public void close() throws Exception
+        {
+            dataWriterNew.close();
+            dataWriterOld.close();
+        }
+
+        void build(Row staticRow, DecoratedKey partitionKey,
+                   Collection<Clustering> clusterings, long startPosition) throws IOException
+        {
+
+            Iterator<Clustering> clusteringIter = clusterings.iterator();
+            columnIndex.buildRowIndex(makeRowIter(staticRow, partitionKey, clusteringIter, dataWriterNew));
+            rieNew = RowIndexEntry.create(startPosition, 0L,
+                                          deletionInfo, columnIndex.headerLength, columnIndex.columnIndexCount,
+                                          columnIndex.indexInfoSerializedSize(),
+                                          columnIndex.indexSamples(), columnIndex.offsets(),
+                                          rieSerializer.indexInfoSerializer());
+            rieSerializer.serialize(rieNew, rieOutput, columnIndex.buffer());
+            rieNewSerialized = rieOutput.buffer().duplicate();
+
+            Iterator<Clustering> clusteringIter2 = clusterings.iterator();
+            ColumnIndex columnIndex = RowIndexEntryTest.ColumnIndex.writeAndBuildIndex(makeRowIter(staticRow, partitionKey, clusteringIter2, dataWriterOld),
+                                                                                       dataWriterOld, header, Collections.emptySet(), BigFormat.latestVersion);
+            rieOld = Pre_C_11206_RowIndexEntry.create(startPosition, deletionInfo, columnIndex);
+            oldSerializer.serialize(rieOld, oldOutput);
+            rieOldSerialized = oldOutput.buffer().duplicate();
+        }
+
+        private AbstractUnfilteredRowIterator makeRowIter(Row staticRow, DecoratedKey partitionKey,
+                                                          Iterator<Clustering> clusteringIter, SequentialWriter dataWriter)
+        {
+            return new AbstractUnfilteredRowIterator(cfMeta, partitionKey, deletionInfo, cfMeta.partitionColumns(),
+                                                     staticRow, false, new EncodingStats(0, 0, 0))
+            {
+                protected Unfiltered computeNext()
+                {
+                    if (!clusteringIter.hasNext())
+                        return endOfData();
+                    try
+                    {
+                        // write some fake bytes to the data file to force writing the IndexInfo object
+                        dataWriter.write(dummy_100k);
+                    }
+                    catch (IOException e)
+                    {
+                        throw new RuntimeException(e);
+                    }
+                    return buildRow(clusteringIter.next());
+                }
+            };
+        }
+
+        private Unfiltered buildRow(Clustering clustering)
+        {
+            BTree.Builder<ColumnData> builder = BTree.builder(ColumnData.comparator);
+            builder.add(BufferCell.live(cfMeta.partitionColumns().iterator().next(),
+                                        1L,
+                                        ByteBuffer.allocate(0)));
+            return BTreeRow.create(clustering, primaryKeyLivenessInfo, deletion, builder.build());
+        }
+    }
+
+    /**
+     * Pre C-11206 code.
+     */
+    static final class ColumnIndex
+    {
+        final long partitionHeaderLength;
+        final List<IndexInfo> columnsIndex;
+
+        private static final ColumnIndex EMPTY = new ColumnIndex(-1, Collections.emptyList());
+
+        private ColumnIndex(long partitionHeaderLength, List<IndexInfo> columnsIndex)
+        {
+            assert columnsIndex != null;
+
+            this.partitionHeaderLength = partitionHeaderLength;
+            this.columnsIndex = columnsIndex;
+        }
+
+        static ColumnIndex writeAndBuildIndex(UnfilteredRowIterator iterator,
+                                              SequentialWriter output,
+                                              SerializationHeader header,
+                                              Collection<SSTableFlushObserver> observers,
+                                              Version version) throws IOException
+        {
+            assert !iterator.isEmpty() && version.storeRows();
+
+            Builder builder = new Builder(iterator, output, header, observers, version.correspondingMessagingVersion());
+            return builder.build();
+        }
+
+        public static ColumnIndex nothing()
+        {
+            return EMPTY;
+        }
+
+        /**
+         * Help to create an index for a column family based on size of columns,
+         * and write said columns to disk.
+         */
+        private static class Builder
+        {
+            private final UnfilteredRowIterator iterator;
+            private final SequentialWriter writer;
+            private final SerializationHeader header;
+            private final int version;
+
+            private final List<IndexInfo> columnsIndex = new ArrayList<>();
+            private final long initialPosition;
+            private long headerLength = -1;
+
+            private long startPosition = -1;
+
+            private int written;
+            private long previousRowStart;
+
+            private ClusteringPrefix firstClustering;
+            private ClusteringPrefix lastClustering;
+
+            private DeletionTime openMarker;
+
+            private final Collection<SSTableFlushObserver> observers;
+
+            Builder(UnfilteredRowIterator iterator,
+                           SequentialWriter writer,
+                           SerializationHeader header,
+                           Collection<SSTableFlushObserver> observers,
+                           int version)
+            {
+                this.iterator = iterator;
+                this.writer = writer;
+                this.header = header;
+                this.version = version;
+                this.observers = observers == null ? Collections.emptyList() : observers;
+                this.initialPosition = writer.position();
+            }
+
+            private void writePartitionHeader(UnfilteredRowIterator iterator) throws IOException
+            {
+                ByteBufferUtil.writeWithShortLength(iterator.partitionKey().getKey(), writer);
+                DeletionTime.serializer.serialize(iterator.partitionLevelDeletion(), writer);
+                if (header.hasStatic())
+                    UnfilteredSerializer.serializer.serializeStaticRow(iterator.staticRow(), header, writer, version);
+            }
+
+            public ColumnIndex build() throws IOException
+            {
+                writePartitionHeader(iterator);
+                this.headerLength = writer.position() - initialPosition;
+
+                while (iterator.hasNext())
+                    add(iterator.next());
+
+                return close();
+            }
+
+            private long currentPosition()
+            {
+                return writer.position() - initialPosition;
+            }
+
+            private void addIndexBlock()
+            {
+                IndexInfo cIndexInfo = new IndexInfo(firstClustering,
+                                                     lastClustering,
+                                                     startPosition,
+                                                                             currentPosition() - startPosition,
+                                                     openMarker);
+                columnsIndex.add(cIndexInfo);
+                firstClustering = null;
+            }
+
+            private void add(Unfiltered unfiltered) throws IOException
+            {
+                long pos = currentPosition();
+
+                if (firstClustering == null)
+                {
+                    // Beginning of an index block. Remember the start and position
+                    firstClustering = unfiltered.clustering();
+                    startPosition = pos;
+                }
+
+                UnfilteredSerializer.serializer.serialize(unfiltered, header, writer, pos - previousRowStart, version);
+
+                // notify observers about each new row
+                if (!observers.isEmpty())
+                    observers.forEach((o) -> o.nextUnfilteredCluster(unfiltered));
+
+                lastClustering = unfiltered.clustering();
+                previousRowStart = pos;
+                ++written;
+
+                if (unfiltered.kind() == Unfiltered.Kind.RANGE_TOMBSTONE_MARKER)
+                {
+                    RangeTombstoneMarker marker = (RangeTombstoneMarker)unfiltered;
+                    openMarker = marker.isOpen(false) ? marker.openDeletionTime(false) : null;
+                }
+
+                // if we hit the column index size that we have to index after, go ahead and index it.
+                if (currentPosition() - startPosition >= DatabaseDescriptor.getColumnIndexSize())
+                    addIndexBlock();
+
+            }
+
+            private ColumnIndex close() throws IOException
+            {
+                UnfilteredSerializer.serializer.writeEndOfPartition(writer);
+
+                // It's possible we add no rows, just a top level deletion
+                if (written == 0)
+                    return RowIndexEntryTest.ColumnIndex.EMPTY;
+
+                // the last column may have fallen on an index boundary already.  if not, index it explicitly.
+                if (firstClustering != null)
+                    addIndexBlock();
+
+                // we should always have at least one computed index block, but we only write it out if there is more than that.
+                assert !columnsIndex.isEmpty() && headerLength >= 0;
+                return new ColumnIndex(headerLength, columnsIndex);
+            }
+        }
     }
 
     @Test
@@ -116,11 +401,11 @@
         String tableName = createTable("CREATE TABLE %s (a int, b text, c int, PRIMARY KEY(a, b))");
         ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(tableName);
 
-        final RowIndexEntry simple = new RowIndexEntry(123);
+        Pre_C_11206_RowIndexEntry simple = new Pre_C_11206_RowIndexEntry(123);
 
         DataOutputBuffer buffer = new DataOutputBuffer();
         SerializationHeader header = new SerializationHeader(true, cfs.metadata, cfs.metadata.partitionColumns(), EncodingStats.NO_STATS);
-        RowIndexEntry.Serializer serializer = new RowIndexEntry.Serializer(cfs.metadata, BigFormat.latestVersion, header);
+        Pre_C_11206_RowIndexEntry.Serializer serializer = new Pre_C_11206_RowIndexEntry.Serializer(cfs.metadata, BigFormat.latestVersion, header);
 
         serializer.serialize(simple, buffer);
 
@@ -128,16 +413,16 @@
 
         // write enough rows to ensure we get a few column index entries
         for (int i = 0; i <= DatabaseDescriptor.getColumnIndexSize() / 4; i++)
-            execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 0, "" + i, i);
+            execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 0, String.valueOf(i), i);
 
         ImmutableBTreePartition partition = Util.getOnlyPartitionUnfiltered(Util.cmd(cfs).build());
 
         File tempFile = File.createTempFile("row_index_entry_test", null);
         tempFile.deleteOnExit();
-        SequentialWriter writer = SequentialWriter.open(tempFile);
-        ColumnIndex columnIndex = ColumnIndex.writeAndBuildIndex(partition.unfilteredIterator(), writer, header, BigFormat.latestVersion);
-        RowIndexEntry<IndexHelper.IndexInfo> withIndex = RowIndexEntry.create(0xdeadbeef, DeletionTime.LIVE, columnIndex);
-        IndexHelper.IndexInfo.Serializer indexSerializer = new IndexHelper.IndexInfo.Serializer(cfs.metadata, BigFormat.latestVersion, header);
+        SequentialWriter writer = new SequentialWriter(tempFile);
+        ColumnIndex columnIndex = RowIndexEntryTest.ColumnIndex.writeAndBuildIndex(partition.unfilteredIterator(), writer, header, Collections.emptySet(), BigFormat.latestVersion);
+        Pre_C_11206_RowIndexEntry withIndex = Pre_C_11206_RowIndexEntry.create(0xdeadbeef, DeletionTime.LIVE, columnIndex);
+        IndexInfo.Serializer indexSerializer = cfs.metadata.serializers().indexInfoSerializer(BigFormat.latestVersion, header);
 
         // sanity check
         assertTrue(columnIndex.columnsIndex.size() >= 3);
@@ -174,11 +459,11 @@
 
         bb = buffer.buffer();
         input = new DataInputBuffer(bb, false);
-        RowIndexEntry.Serializer.skip(input, BigFormat.latestVersion);
+        Pre_C_11206_RowIndexEntry.Serializer.skip(input, BigFormat.latestVersion);
         Assert.assertEquals(0, bb.remaining());
     }
 
-    private void serializationCheck(RowIndexEntry<IndexHelper.IndexInfo> withIndex, IndexHelper.IndexInfo.Serializer indexSerializer, ByteBuffer bb, DataInputBuffer input) throws IOException
+    private static void serializationCheck(Pre_C_11206_RowIndexEntry withIndex, IndexInfo.Serializer indexSerializer, ByteBuffer bb, DataInputBuffer input) throws IOException
     {
         Assert.assertEquals(0xdeadbeef, input.readUnsignedVInt());
         Assert.assertEquals(withIndex.promotedSize(indexSerializer), input.readUnsignedVInt());
@@ -193,7 +478,7 @@
         {
             int pos = bb.position();
             offsets[i] = pos - offset;
-            IndexHelper.IndexInfo info = indexSerializer.deserialize(input);
+            IndexInfo info = indexSerializer.deserialize(input);
             int end = bb.position();
 
             Assert.assertEquals(indexSerializer.serializedSize(info), end - pos);
@@ -210,4 +495,361 @@
 
         Assert.assertEquals(0, bb.remaining());
     }
+
+    static class Pre_C_11206_RowIndexEntry implements IMeasurableMemory
+    {
+        private static final long EMPTY_SIZE = ObjectSizes.measure(new Pre_C_11206_RowIndexEntry(0));
+
+        public final long position;
+
+        Pre_C_11206_RowIndexEntry(long position)
+        {
+            this.position = position;
+        }
+
+        protected int promotedSize(IndexInfo.Serializer idxSerializer)
+        {
+            return 0;
+        }
+
+        public static Pre_C_11206_RowIndexEntry create(long position, DeletionTime deletionTime, ColumnIndex index)
+        {
+            assert index != null;
+            assert deletionTime != null;
+
+            // we only consider the columns summary when determining whether to create an IndexedEntry,
+            // since if there are insufficient columns to be worth indexing we're going to seek to
+            // the beginning of the row anyway, so we might as well read the tombstone there as well.
+            if (index.columnsIndex.size() > 1)
+                return new Pre_C_11206_RowIndexEntry.IndexedEntry(position, deletionTime, index.partitionHeaderLength, index.columnsIndex);
+            else
+                return new Pre_C_11206_RowIndexEntry(position);
+        }
+
+        /**
+         * @return true if this index entry contains the row-level tombstone and column summary.  Otherwise,
+         * caller should fetch these from the row header.
+         */
+        public boolean isIndexed()
+        {
+            return !columnsIndex().isEmpty();
+        }
+
+        public DeletionTime deletionTime()
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        /**
+         * The length of the row header (partition key, partition deletion and static row).
+         * This value is only provided for indexed entries and this method will throw
+         * {@code UnsupportedOperationException} if {@code !isIndexed()}.
+         */
+        public long headerLength()
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        public List<IndexInfo> columnsIndex()
+        {
+            return Collections.emptyList();
+        }
+
+        public long unsharedHeapSize()
+        {
+            return EMPTY_SIZE;
+        }
+
+        public static class Serializer
+        {
+            private final IndexInfo.Serializer idxSerializer;
+            private final Version version;
+
+            Serializer(CFMetaData metadata, Version version, SerializationHeader header)
+            {
+                this.idxSerializer = metadata.serializers().indexInfoSerializer(version, header);
+                this.version = version;
+            }
+
+            public void serialize(Pre_C_11206_RowIndexEntry rie, DataOutputPlus out) throws IOException
+            {
+                assert version.storeRows() : "We read old index files but we should never write them";
+
+                out.writeUnsignedVInt(rie.position);
+                out.writeUnsignedVInt(rie.promotedSize(idxSerializer));
+
+                if (rie.isIndexed())
+                {
+                    out.writeUnsignedVInt(rie.headerLength());
+                    DeletionTime.serializer.serialize(rie.deletionTime(), out);
+                    out.writeUnsignedVInt(rie.columnsIndex().size());
+
+                    // Calculate and write the offsets to the IndexInfo objects.
+
+                    int[] offsets = new int[rie.columnsIndex().size()];
+
+                    if (out.hasPosition())
+                    {
+                        // Out is usually a SequentialWriter, so using the file-pointer is fine to generate the offsets.
+                        // A DataOutputBuffer also works.
+                        long start = out.position();
+                        int i = 0;
+                        for (IndexInfo info : rie.columnsIndex())
+                        {
+                            offsets[i] = i == 0 ? 0 : (int)(out.position() - start);
+                            i++;
+                            idxSerializer.serialize(info, out);
+                        }
+                    }
+                    else
+                    {
+                        // Not sure this branch will ever be needed, but if it is called, it has to calculate the
+                        // serialized sizes instead of simply using the file-pointer.
+                        int i = 0;
+                        int offset = 0;
+                        for (IndexInfo info : rie.columnsIndex())
+                        {
+                            offsets[i++] = offset;
+                            idxSerializer.serialize(info, out);
+                            offset += idxSerializer.serializedSize(info);
+                        }
+                    }
+
+                    for (int off : offsets)
+                        out.writeInt(off);
+                }
+            }
+
+            public Pre_C_11206_RowIndexEntry deserialize(DataInputPlus in) throws IOException
+            {
+                if (!version.storeRows())
+                {
+                    long position = in.readLong();
+
+                    int size = in.readInt();
+                    if (size > 0)
+                    {
+                        DeletionTime deletionTime = DeletionTime.serializer.deserialize(in);
+
+                        int entries = in.readInt();
+                        List<IndexInfo> columnsIndex = new ArrayList<>(entries);
+
+                        long headerLength = 0L;
+                        for (int i = 0; i < entries; i++)
+                        {
+                            IndexInfo info = idxSerializer.deserialize(in);
+                            columnsIndex.add(info);
+                            if (i == 0)
+                                headerLength = info.offset;
+                        }
+
+                        return new Pre_C_11206_RowIndexEntry.IndexedEntry(position, deletionTime, headerLength, columnsIndex);
+                    }
+                    else
+                    {
+                        return new Pre_C_11206_RowIndexEntry(position);
+                    }
+                }
+
+                long position = in.readUnsignedVInt();
+
+                int size = (int)in.readUnsignedVInt();
+                if (size > 0)
+                {
+                    long headerLength = in.readUnsignedVInt();
+                    DeletionTime deletionTime = DeletionTime.serializer.deserialize(in);
+                    int entries = (int)in.readUnsignedVInt();
+                    List<IndexInfo> columnsIndex = new ArrayList<>(entries);
+                    for (int i = 0; i < entries; i++)
+                        columnsIndex.add(idxSerializer.deserialize(in));
+
+                    in.skipBytesFully(entries * TypeSizes.sizeof(0));
+
+                    return new Pre_C_11206_RowIndexEntry.IndexedEntry(position, deletionTime, headerLength, columnsIndex);
+                }
+                else
+                {
+                    return new Pre_C_11206_RowIndexEntry(position);
+                }
+            }
+
+            // Reads only the data 'position' of the index entry and returns it. Note that this left 'in' in the middle
+            // of reading an entry, so this is only useful if you know what you are doing and in most case 'deserialize'
+            // should be used instead.
+            static long readPosition(DataInputPlus in, Version version) throws IOException
+            {
+                return version.storeRows() ? in.readUnsignedVInt() : in.readLong();
+            }
+
+            public static void skip(DataInputPlus in, Version version) throws IOException
+            {
+                readPosition(in, version);
+                skipPromotedIndex(in, version);
+            }
+
+            private static void skipPromotedIndex(DataInputPlus in, Version version) throws IOException
+            {
+                int size = version.storeRows() ? (int)in.readUnsignedVInt() : in.readInt();
+                if (size <= 0)
+                    return;
+
+                in.skipBytesFully(size);
+            }
+
+            public int serializedSize(Pre_C_11206_RowIndexEntry rie)
+            {
+                assert version.storeRows() : "We read old index files but we should never write them";
+
+                int indexedSize = 0;
+                if (rie.isIndexed())
+                {
+                    List<IndexInfo> index = rie.columnsIndex();
+
+                    indexedSize += TypeSizes.sizeofUnsignedVInt(rie.headerLength());
+                    indexedSize += DeletionTime.serializer.serializedSize(rie.deletionTime());
+                    indexedSize += TypeSizes.sizeofUnsignedVInt(index.size());
+
+                    for (IndexInfo info : index)
+                        indexedSize += idxSerializer.serializedSize(info);
+
+                    indexedSize += index.size() * TypeSizes.sizeof(0);
+                }
+
+                return TypeSizes.sizeofUnsignedVInt(rie.position) + TypeSizes.sizeofUnsignedVInt(indexedSize) + indexedSize;
+            }
+        }
+
+        /**
+         * An entry in the row index for a row whose columns are indexed.
+         */
+        private static final class IndexedEntry extends Pre_C_11206_RowIndexEntry
+        {
+            private final DeletionTime deletionTime;
+
+            // The offset in the file when the index entry end
+            private final long headerLength;
+            private final List<IndexInfo> columnsIndex;
+            private static final long BASE_SIZE =
+            ObjectSizes.measure(new IndexedEntry(0, DeletionTime.LIVE, 0, Arrays.asList(null, null)))
+            + ObjectSizes.measure(new ArrayList<>(1));
+
+            private IndexedEntry(long position, DeletionTime deletionTime, long headerLength, List<IndexInfo> columnsIndex)
+            {
+                super(position);
+                assert deletionTime != null;
+                assert columnsIndex != null && columnsIndex.size() > 1;
+                this.deletionTime = deletionTime;
+                this.headerLength = headerLength;
+                this.columnsIndex = columnsIndex;
+            }
+
+            @Override
+            public DeletionTime deletionTime()
+            {
+                return deletionTime;
+            }
+
+            @Override
+            public long headerLength()
+            {
+                return headerLength;
+            }
+
+            @Override
+            public List<IndexInfo> columnsIndex()
+            {
+                return columnsIndex;
+            }
+
+            @Override
+            protected int promotedSize(IndexInfo.Serializer idxSerializer)
+            {
+                long size = TypeSizes.sizeofUnsignedVInt(headerLength)
+                            + DeletionTime.serializer.serializedSize(deletionTime)
+                            + TypeSizes.sizeofUnsignedVInt(columnsIndex.size()); // number of entries
+                for (IndexInfo info : columnsIndex)
+                    size += idxSerializer.serializedSize(info);
+
+                size += columnsIndex.size() * TypeSizes.sizeof(0);
+
+                return Ints.checkedCast(size);
+            }
+
+            @Override
+            public long unsharedHeapSize()
+            {
+                long entrySize = 0;
+                for (IndexInfo idx : columnsIndex)
+                    entrySize += idx.unsharedHeapSize();
+
+                return BASE_SIZE
+                       + entrySize
+                       + deletionTime.unsharedHeapSize()
+                       + ObjectSizes.sizeOfReferenceArray(columnsIndex.size());
+            }
+        }
+    }
+
+    @Test
+    public void testIndexFor() throws IOException
+    {
+        DeletionTime deletionInfo = new DeletionTime(FBUtilities.timestampMicros(), FBUtilities.nowInSeconds());
+
+        List<IndexInfo> indexes = new ArrayList<>();
+        indexes.add(new IndexInfo(cn(0L), cn(5L), 0, 0, deletionInfo));
+        indexes.add(new IndexInfo(cn(10L), cn(15L), 0, 0, deletionInfo));
+        indexes.add(new IndexInfo(cn(20L), cn(25L), 0, 0, deletionInfo));
+
+        RowIndexEntry rie = new RowIndexEntry(0L)
+        {
+            public IndexInfoRetriever openWithIndex(FileHandle indexFile)
+            {
+                return new IndexInfoRetriever()
+                {
+                    public IndexInfo columnsIndex(int index)
+                    {
+                        return indexes.get(index);
+                    }
+
+                    public void close()
+                    {
+                    }
+                };
+            }
+
+            public int columnsIndexCount()
+            {
+                return indexes.size();
+            }
+        };
+        
+        AbstractSSTableIterator.IndexState indexState = new AbstractSSTableIterator.IndexState(
+            null, comp, rie, false, null                                                                                              
+        );
+        
+        assertEquals(0, indexState.indexFor(cn(-1L), -1));
+        assertEquals(0, indexState.indexFor(cn(5L), -1));
+        assertEquals(1, indexState.indexFor(cn(12L), -1));
+        assertEquals(2, indexState.indexFor(cn(17L), -1));
+        assertEquals(3, indexState.indexFor(cn(100L), -1));
+        assertEquals(3, indexState.indexFor(cn(100L), 0));
+        assertEquals(3, indexState.indexFor(cn(100L), 1));
+        assertEquals(3, indexState.indexFor(cn(100L), 2));
+        assertEquals(3, indexState.indexFor(cn(100L), 3));
+
+        indexState = new AbstractSSTableIterator.IndexState(
+            null, comp, rie, true, null
+        );
+
+        assertEquals(-1, indexState.indexFor(cn(-1L), -1));
+        assertEquals(0, indexState.indexFor(cn(5L), 3));
+        assertEquals(0, indexState.indexFor(cn(5L), 2));
+        assertEquals(1, indexState.indexFor(cn(17L), 3));
+        assertEquals(2, indexState.indexFor(cn(100L), 3));
+        assertEquals(2, indexState.indexFor(cn(100L), 4));
+        assertEquals(1, indexState.indexFor(cn(12L), 3));
+        assertEquals(1, indexState.indexFor(cn(12L), 2));
+        assertEquals(1, indexState.indexFor(cn(100L), 1));
+        assertEquals(2, indexState.indexFor(cn(100L), 2));
+    }
 }
diff --git a/test/unit/org/apache/cassandra/db/RowIterationTest.java b/test/unit/org/apache/cassandra/db/RowIterationTest.java
index 3b0293c..b0cd4fc 100644
--- a/test/unit/org/apache/cassandra/db/RowIterationTest.java
+++ b/test/unit/org/apache/cassandra/db/RowIterationTest.java
@@ -21,12 +21,9 @@
 import org.junit.Test;
 
 import org.apache.cassandra.cql3.CQLTester;
-import org.apache.cassandra.db.rows.UnfilteredRowIterator;
-import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
 import org.apache.cassandra.Util;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertFalse;
 
 
diff --git a/test/unit/org/apache/cassandra/db/RowTest.java b/test/unit/org/apache/cassandra/db/RowTest.java
index e3f4884..5fdb98a 100644
--- a/test/unit/org/apache/cassandra/db/RowTest.java
+++ b/test/unit/org/apache/cassandra/db/RowTest.java
@@ -31,6 +31,7 @@
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.marshal.AbstractType;
@@ -58,6 +59,7 @@
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
+        DatabaseDescriptor.daemonInitialization();
         CFMetaData cfMetadata = CFMetaData.Builder.create(KEYSPACE1, CF_STANDARD1)
                                                   .addPartitionKey("key", BytesType.instance)
                                                   .addClusteringColumn("col1", AsciiType.instance)
@@ -108,12 +110,12 @@
             while (merged.hasNext())
             {
                 RangeTombstoneBoundMarker openMarker = (RangeTombstoneBoundMarker)merged.next();
-                Slice.Bound openBound = openMarker.clustering();
+                ClusteringBound openBound = openMarker.clustering();
                 DeletionTime openDeletion = new DeletionTime(openMarker.deletionTime().markedForDeleteAt(),
                                                                    openMarker.deletionTime().localDeletionTime());
 
                 RangeTombstoneBoundMarker closeMarker = (RangeTombstoneBoundMarker)merged.next();
-                Slice.Bound closeBound = closeMarker.clustering();
+                ClusteringBound closeBound = closeMarker.clustering();
                 DeletionTime closeDeletion = new DeletionTime(closeMarker.deletionTime().markedForDeleteAt(),
                                                                     closeMarker.deletionTime().localDeletionTime());
 
@@ -185,16 +187,16 @@
         assertEquals(Integer.valueOf(1), map.get(row));
     }
 
-    private void assertRangeTombstoneMarkers(Slice.Bound start, Slice.Bound end, DeletionTime deletionTime, Object[] expected)
+    private void assertRangeTombstoneMarkers(ClusteringBound start, ClusteringBound end, DeletionTime deletionTime, Object[] expected)
     {
         AbstractType clusteringType = (AbstractType)cfm.comparator.subtype(0);
 
         assertEquals(1, start.size());
-        assertEquals(start.kind(), Slice.Bound.Kind.INCL_START_BOUND);
+        assertEquals(start.kind(), ClusteringPrefix.Kind.INCL_START_BOUND);
         assertEquals(expected[0], clusteringType.getString(start.get(0)));
 
         assertEquals(1, end.size());
-        assertEquals(end.kind(), Slice.Bound.Kind.INCL_END_BOUND);
+        assertEquals(end.kind(), ClusteringPrefix.Kind.INCL_END_BOUND);
         assertEquals(expected[1], clusteringType.getString(end.get(0)));
 
         assertEquals(expected[2], deletionTime.markedForDeleteAt());
@@ -213,6 +215,6 @@
                                       String value,
                                       long timestamp)
     {
-       builder.addCell(BufferCell.live(cfm, columnDefinition, timestamp, ((AbstractType) columnDefinition.cellValueType()).decompose(value)));
+       builder.addCell(BufferCell.live(columnDefinition, timestamp, ((AbstractType) columnDefinition.cellValueType()).decompose(value)));
     }
 }
diff --git a/test/unit/org/apache/cassandra/db/RowUpdateBuilder.java b/test/unit/org/apache/cassandra/db/RowUpdateBuilder.java
new file mode 100644
index 0000000..8535616
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/RowUpdateBuilder.java
@@ -0,0 +1,192 @@
+/*
+ * 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.db;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.BTreeRow;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.utils.FBUtilities;
+
+/**
+ * Convenience object to create single row updates for tests.
+ *
+ * This is a thin wrapper over the builders in SimpleBuilders for historical reasons.
+ * We could modify all the tests using this class to use the simple builders directly
+ * instead, but there is a fair amount of use so the value of such effort is unclear.
+ */
+public class RowUpdateBuilder
+{
+    private final PartitionUpdate.SimpleBuilder updateBuilder;
+    private Row.SimpleBuilder rowBuilder;
+    private boolean noRowMarker;
+
+    private List<RangeTombstone> rts = new ArrayList<>();
+
+    private RowUpdateBuilder(PartitionUpdate.SimpleBuilder updateBuilder)
+    {
+        this.updateBuilder = updateBuilder;
+    }
+
+    public RowUpdateBuilder(CFMetaData metadata, long timestamp, Object partitionKey)
+    {
+        this(metadata, FBUtilities.nowInSeconds(), timestamp, partitionKey);
+    }
+
+    public RowUpdateBuilder(CFMetaData metadata, int localDeletionTime, long timestamp, Object partitionKey)
+    {
+        this(metadata, localDeletionTime, timestamp, metadata.params.defaultTimeToLive, partitionKey);
+    }
+
+    public RowUpdateBuilder(CFMetaData metadata, long timestamp, int ttl, Object partitionKey)
+    {
+        this(metadata, FBUtilities.nowInSeconds(), timestamp, ttl, partitionKey);
+    }
+
+    public RowUpdateBuilder(CFMetaData metadata, int localDeletionTime, long timestamp, int ttl, Object partitionKey)
+    {
+        this(PartitionUpdate.simpleBuilder(metadata, partitionKey));
+
+        this.updateBuilder.timestamp(timestamp);
+        this.updateBuilder.ttl(ttl);
+        this.updateBuilder.nowInSec(localDeletionTime);
+    }
+
+    private Row.SimpleBuilder rowBuilder()
+    {
+        // Normally, rowBuilder is created by the call to clustering(), but we allow skipping that call for an empty
+        // clustering.
+        if (rowBuilder == null)
+        {
+            rowBuilder = updateBuilder.row();
+            if (noRowMarker)
+                rowBuilder.noPrimaryKeyLivenessInfo();
+        }
+
+        return rowBuilder;
+    }
+
+    // This must be called before any addition or deletion if used.
+    public RowUpdateBuilder noRowMarker()
+    {
+        this.noRowMarker = true;
+        if (rowBuilder != null)
+            rowBuilder.noPrimaryKeyLivenessInfo();
+        return this;
+    }
+
+    public RowUpdateBuilder clustering(Object... clusteringValues)
+    {
+        assert rowBuilder == null;
+        rowBuilder = updateBuilder.row(clusteringValues);
+        if (noRowMarker)
+            rowBuilder.noPrimaryKeyLivenessInfo();
+        return this;
+    }
+
+    public Mutation build()
+    {
+        return new Mutation(buildUpdate());
+    }
+
+    public PartitionUpdate buildUpdate()
+    {
+        PartitionUpdate update = updateBuilder.build();
+        for (RangeTombstone rt : rts)
+            update.add(rt);
+        return update;
+    }
+
+    private static void deleteRow(PartitionUpdate update, long timestamp, int localDeletionTime, Object... clusteringValues)
+    {
+        assert clusteringValues.length == update.metadata().comparator.size() || (clusteringValues.length == 0 && !update.columns().statics.isEmpty());
+
+        boolean isStatic = clusteringValues.length != update.metadata().comparator.size();
+        Row.Builder builder = BTreeRow.sortedBuilder();
+
+        if (isStatic)
+            builder.newRow(Clustering.STATIC_CLUSTERING);
+        else
+            builder.newRow(clusteringValues.length == 0 ? Clustering.EMPTY : update.metadata().comparator.make(clusteringValues));
+        builder.addRowDeletion(Row.Deletion.regular(new DeletionTime(timestamp, localDeletionTime)));
+
+        update.add(builder.build());
+    }
+
+    public static Mutation deleteRow(CFMetaData metadata, long timestamp, Object key, Object... clusteringValues)
+    {
+        return deleteRowAt(metadata, timestamp, FBUtilities.nowInSeconds(), key, clusteringValues);
+    }
+
+    public static Mutation deleteRowAt(CFMetaData metadata, long timestamp, int localDeletionTime, Object key, Object... clusteringValues)
+    {
+        PartitionUpdate update = new PartitionUpdate(metadata, makeKey(metadata, key), metadata.partitionColumns(), 0);
+        deleteRow(update, timestamp, localDeletionTime, clusteringValues);
+        // note that the created mutation may get further update later on, so we don't use the ctor that create a singletonMap
+        // underneath (this class if for convenience, not performance)
+        return new Mutation(update.metadata().ksName, update.partitionKey()).add(update);
+    }
+
+    private static DecoratedKey makeKey(CFMetaData metadata, Object... partitionKey)
+    {
+        if (partitionKey.length == 1 && partitionKey[0] instanceof DecoratedKey)
+            return (DecoratedKey)partitionKey[0];
+
+        ByteBuffer key = CFMetaData.serializePartitionKey(metadata.getKeyValidatorAsClusteringComparator().make(partitionKey));
+        return metadata.decorateKey(key);
+    }
+
+    public RowUpdateBuilder addRangeTombstone(RangeTombstone rt)
+    {
+        rts.add(rt);
+        return this;
+    }
+
+    public RowUpdateBuilder addRangeTombstone(Object start, Object end)
+    {
+        updateBuilder.addRangeTombstone().start(start).end(end);
+        return this;
+    }
+
+    public RowUpdateBuilder add(String columnName, Object value)
+    {
+        rowBuilder().add(columnName, value);
+        return this;
+    }
+
+    public RowUpdateBuilder add(ColumnDefinition columnDefinition, Object value)
+    {
+        return add(columnDefinition.name.toString(), value);
+    }
+
+    public RowUpdateBuilder delete(String columnName)
+    {
+        rowBuilder().delete(columnName);
+        return this;
+    }
+
+    public RowUpdateBuilder delete(ColumnDefinition columnDefinition)
+    {
+        return delete(columnDefinition.name.toString());
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/SSTableAndMemTableDigestMatchTest.java b/test/unit/org/apache/cassandra/db/SSTableAndMemTableDigestMatchTest.java
new file mode 100644
index 0000000..45dce64
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/SSTableAndMemTableDigestMatchTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.db;
+
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.function.Function;
+
+import com.google.common.collect.Sets;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.IntegerType;
+import org.apache.cassandra.db.partitions.SingletonUnfilteredPartitionIterator;
+import org.apache.cassandra.db.rows.CellPath;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+import static org.junit.Assert.assertEquals;
+
+public class SSTableAndMemTableDigestMatchTest extends CQLTester
+{
+    private final static long writeTime = System.currentTimeMillis() * 1000L;
+
+    @Test
+    public void testSelectAllColumns() throws Throwable
+    {
+        testWithFilter(cfs -> ColumnFilter.all(cfs.metadata));
+    }
+
+    @Test
+    public void testSelectNoColumns() throws Throwable
+    {
+        testWithFilter(cfs -> ColumnFilter.selection(cfs.metadata, PartitionColumns.NONE));
+    }
+
+    @Test
+    public void testSelectEmptyColumn() throws Throwable
+    {
+        testWithFilter(cfs -> ColumnFilter.selection(cfs.metadata, PartitionColumns.of(cfs.metadata.getColumnDefinition(ColumnIdentifier.getInterned("e", false)))));
+    }
+
+    @Test
+    public void testSelectNonEmptyColumn() throws Throwable
+    {
+        testWithFilter(cfs -> ColumnFilter.selection(cfs.metadata, PartitionColumns.of(cfs.metadata.getColumnDefinition(ColumnIdentifier.getInterned("v1", false)))));
+    }
+
+    @Test
+    public void testSelectEachNonEmptyColumn() throws Throwable
+    {
+        testWithFilter(cfs -> ColumnFilter.selection(cfs.metadata,
+                                                     PartitionColumns.builder()
+                                                                     .add(cfs.metadata.getColumnDefinition(ColumnIdentifier.getInterned("v1", false)))
+                                                                     .add(cfs.metadata.getColumnDefinition(ColumnIdentifier.getInterned("v2", false)))
+                                                                     .add(cfs.metadata.getColumnDefinition(ColumnIdentifier.getInterned("m", false)))
+                                                                     .build()));
+    }
+
+    @Test
+    public void testSelectEmptyComplexColumn() throws Throwable
+    {
+        testWithFilter(cfs -> ColumnFilter.selection(cfs.metadata,
+                                                     PartitionColumns.builder()
+                                                                     .add(cfs.metadata.getColumnDefinition(ColumnIdentifier.getInterned("em", false)))
+                                                                     .build()));
+    }
+
+    @Test
+    public void testSelectCellsFromEmptyComplexColumn() throws Throwable
+    {
+        testWithFilter(cfs -> ColumnFilter.selectionBuilder()
+                                          .select(cfs.metadata.getColumnDefinition(ColumnIdentifier.getInterned("em", false)),
+                                                  CellPath.create(Int32Type.instance.decompose(5))).build());
+    }
+
+    @Test
+    public void testSelectNonEmptyCellsFromComplexColumn() throws Throwable
+    {
+        testWithFilter(cfs -> ColumnFilter.selectionBuilder()
+                                          .select(cfs.metadata.getColumnDefinition(ColumnIdentifier.getInterned("m", false)),
+                                                  CellPath.create(Int32Type.instance.decompose(1))).build());
+    }
+
+    @Test
+    public void testSelectEmptyCellsFromNonEmptyComplexColumn() throws Throwable
+    {
+        testWithFilter(cfs -> ColumnFilter.selectionBuilder()
+                                          .select(cfs.metadata.getColumnDefinition(ColumnIdentifier.getInterned("m", false)),
+                                                  CellPath.create(Int32Type.instance.decompose(5))).build());
+    }
+
+    private void testWithFilter(Function<ColumnFamilyStore, ColumnFilter> filterFactory) throws Throwable
+    {
+        Map<Integer, Integer> m = new HashMap<>();
+        m.put(1, 10);
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, e text, m map<int, int>, em map<int, int>)");
+        execute("INSERT INTO %s (k, v1, v2, m) values (?, ?, ?, ?) USING TIMESTAMP ?", 1, 2, 3, m, writeTime);
+
+        ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+        ColumnFilter filter = filterFactory.apply(cfs);
+        String digest1 = getDigest(filter);
+        flush();
+        String digest2 = getDigest(filter);
+
+        assertEquals(digest1, digest2);
+    }
+
+    private String getDigest(ColumnFilter filter)
+    {
+        ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+        NavigableSet<Clustering> clusterings = Sets.newTreeSet(new ClusteringComparator());
+        clusterings.add(Clustering.EMPTY);
+        BufferDecoratedKey key = new BufferDecoratedKey(DatabaseDescriptor.getPartitioner().getToken(Int32Type.instance.decompose(1)),
+                                                        Int32Type.instance.decompose(1));
+        SinglePartitionReadCommand cmd = SinglePartitionReadCommand
+                                         .create(cfs.metadata,
+                                                 (int) (System.currentTimeMillis() / 1000),
+                                                 key,
+                                                 filter,
+                                                 new ClusteringIndexNamesFilter(clusterings, false)).copyAsDigestQuery();
+        cmd.setDigestVersion(MessagingService.current_version);
+        ReadResponse resp;
+        try (ReadExecutionController ctrl = ReadExecutionController.forCommand(cmd); UnfilteredRowIterator iterator = cmd.queryMemtableAndDisk(cfs, ctrl))
+        {
+            resp = ReadResponse.createDataResponse(new SingletonUnfilteredPartitionIterator(iterator, false), cmd);
+            logger.info("Response is: {}", resp.toDebugString(cmd, key));
+            ByteBuffer digest = resp.digest(cmd);
+            return ByteBufferUtil.bytesToHex(digest);
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/ScrubTest.java b/test/unit/org/apache/cassandra/db/ScrubTest.java
index 56e42c6..34d46f2 100644
--- a/test/unit/org/apache/cassandra/db/ScrubTest.java
+++ b/test/unit/org/apache/cassandra/db/ScrubTest.java
@@ -35,6 +35,7 @@
 import org.junit.runner.RunWith;
 
 import org.apache.cassandra.*;
+import org.apache.cassandra.cache.ChunkCache;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.Operator;
@@ -145,7 +146,9 @@
 
         Set<SSTableReader> liveSSTables = cfs.getLiveSSTables();
         assertThat(liveSSTables).hasSize(1);
-        Files.write(Paths.get(liveSSTables.iterator().next().getFilename()), new byte[10], StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+        String fileName = liveSSTables.iterator().next().getFilename();
+        Files.write(Paths.get(fileName), new byte[10], StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+        ChunkCache.instance.invalidateFile(fileName);
 
         CompactionManager.instance.performScrub(cfs, true, true, false, 2);
 
@@ -198,20 +201,20 @@
         boolean compression = Boolean.parseBoolean(System.getProperty("cassandra.test.compression", "false"));
         if (compression)
         {
-            assertEquals(0, scrubResult.emptyRows);
-            assertEquals(numPartitions, scrubResult.badRows + scrubResult.goodRows);
+            assertEquals(0, scrubResult.emptyPartitions);
+            assertEquals(numPartitions, scrubResult.badPartitions + scrubResult.goodPartitions);
             //because we only corrupted 1 chunk and we chose enough partitions to cover at least 3 chunks
-            assertTrue(scrubResult.goodRows >= scrubResult.badRows * 2);
+            assertTrue(scrubResult.goodPartitions >= scrubResult.badPartitions * 2);
         }
         else
         {
-            assertEquals(0, scrubResult.emptyRows);
-            assertEquals(1, scrubResult.badRows);
-            assertEquals(numPartitions-1, scrubResult.goodRows);
+            assertEquals(0, scrubResult.emptyPartitions);
+            assertEquals(1, scrubResult.badPartitions);
+            assertEquals(numPartitions-1, scrubResult.goodPartitions);
         }
         assertEquals(1, cfs.getLiveSSTables().size());
 
-        assertOrderedAll(cfs, scrubResult.goodRows);
+        assertOrderedAll(cfs, scrubResult.goodPartitions);
     }
 
     @Test
@@ -450,6 +453,8 @@
         file.seek(startPosition);
         file.writeBytes(StringUtils.repeat('z', (int) (endPosition - startPosition)));
         file.close();
+        if (ChunkCache.instance != null)
+            ChunkCache.instance.invalidateFile(sstable.getFilename());
     }
 
     private static void assertOrderedAll(ColumnFamilyStore cfs, int expectedSize)
@@ -662,18 +667,18 @@
         assertOrdered(Util.cmd(cfs).filterOn(colName, Operator.EQ, 1L).build(), numRows / 2);
     }
 
-    private static SSTableMultiWriter createTestWriter(Descriptor descriptor, long keyCount, CFMetaData metadata, LifecycleNewTracker lifecycleNewTracker)
+    private static SSTableMultiWriter createTestWriter(Descriptor descriptor, long keyCount, CFMetaData metadata, LifecycleTransaction txn)
     {
         SerializationHeader header = new SerializationHeader(true, metadata, metadata.partitionColumns(), EncodingStats.NO_STATS);
         MetadataCollector collector = new MetadataCollector(metadata.comparator).sstableLevel(0);
-        return new TestMultiWriter(new TestWriter(descriptor, keyCount, 0, metadata, collector, header, lifecycleNewTracker));
+        return new TestMultiWriter(new TestWriter(descriptor, keyCount, 0, metadata, collector, header, txn), txn);
     }
 
     private static class TestMultiWriter extends SimpleSSTableMultiWriter
     {
-        TestMultiWriter(SSTableWriter writer)
+        TestMultiWriter(SSTableWriter writer, LifecycleNewTracker lifecycleNewTracker)
         {
-            super(writer);
+            super(writer, lifecycleNewTracker);
         }
     }
 
@@ -683,9 +688,9 @@
     private static class TestWriter extends BigTableWriter
     {
         TestWriter(Descriptor descriptor, long keyCount, long repairedAt, CFMetaData metadata,
-                   MetadataCollector collector, SerializationHeader header, LifecycleNewTracker lifecycleNewTracker)
+                   MetadataCollector collector, SerializationHeader header, LifecycleTransaction txn)
         {
-            super(descriptor, keyCount, repairedAt, metadata, collector, header, lifecycleNewTracker);
+            super(descriptor, keyCount, repairedAt, metadata, collector, header, Collections.emptySet(), txn);
         }
 
         @Override
diff --git a/test/unit/org/apache/cassandra/db/SecondaryIndexTest.java b/test/unit/org/apache/cassandra/db/SecondaryIndexTest.java
index 9fb0463..e9a0db6 100644
--- a/test/unit/org/apache/cassandra/db/SecondaryIndexTest.java
+++ b/test/unit/org/apache/cassandra/db/SecondaryIndexTest.java
@@ -28,7 +28,6 @@
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
-
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.ColumnDefinition;
@@ -37,10 +36,8 @@
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.partitions.*;
 import org.apache.cassandra.db.rows.Row;
-import org.apache.cassandra.db.rows.RowIterator;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.index.Index;
-import org.apache.cassandra.index.internal.CassandraIndex;
 import org.apache.cassandra.schema.IndexMetadata;
 import org.apache.cassandra.schema.KeyspaceParams;
 import org.apache.cassandra.utils.ByteBufferUtil;
@@ -67,7 +64,7 @@
         SchemaLoader.prepareServer();
         SchemaLoader.createKeyspace(KEYSPACE1,
                                     KeyspaceParams.simple(1),
-                                    SchemaLoader.compositeIndexCFMD(KEYSPACE1, WITH_COMPOSITE_INDEX, true).gcGraceSeconds(0),
+                                    SchemaLoader.compositeIndexCFMD(KEYSPACE1, WITH_COMPOSITE_INDEX, true, true).gcGraceSeconds(0),
                                     SchemaLoader.compositeIndexCFMD(KEYSPACE1, COMPOSITE_INDEX_TO_BE_ADDED, false).gcGraceSeconds(0),
                                     SchemaLoader.compositeMultipleIndexCFMD(KEYSPACE1, WITH_MULTIPLE_COMPOSITE_INDEX).gcGraceSeconds(0),
                                     SchemaLoader.keysIndexCFMD(KEYSPACE1, WITH_KEYS_INDEX, true).gcGraceSeconds(0));
@@ -122,7 +119,8 @@
                                       .build();
 
         Index.Searcher searcher = rc.getIndex(cfs).searcherFor(rc);
-        try (ReadOrderGroup orderGroup = rc.startOrderGroup(); UnfilteredPartitionIterator pi = searcher.search(orderGroup))
+        try (ReadExecutionController executionController = rc.executionController();
+             UnfilteredPartitionIterator pi = searcher.search(executionController))
         {
             assertTrue(pi.hasNext());
             pi.next().close();
@@ -327,15 +325,30 @@
     @Test
     public void testDeleteOfInconsistentValuesFromCompositeIndex() throws Exception
     {
+        runDeleteOfInconsistentValuesFromCompositeIndexTest(false);
+    }
+
+    @Test
+    public void testDeleteOfInconsistentValuesFromCompositeIndexOnStaticColumn() throws Exception
+    {
+        runDeleteOfInconsistentValuesFromCompositeIndexTest(true);
+    }
+
+    private void runDeleteOfInconsistentValuesFromCompositeIndexTest(boolean isStatic) throws Exception
+    {
         Keyspace keyspace = Keyspace.open(KEYSPACE1);
         String cfName = WITH_COMPOSITE_INDEX;
 
         ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(cfName);
 
-        ByteBuffer col = ByteBufferUtil.bytes("birthdate");
+        String colName = isStatic ? "static" : "birthdate";
+        ByteBuffer col = ByteBufferUtil.bytes(colName);
 
         // create a row and update the author value
-        new RowUpdateBuilder(cfs.metadata, 0, "k1").clustering("c").add("birthdate", 10l).build().applyUnsafe();
+        RowUpdateBuilder builder = new RowUpdateBuilder(cfs.metadata, 0, "k1");
+        if (!isStatic)
+            builder = builder.clustering("c");
+        builder.add(colName, 10l).build().applyUnsafe();
 
         // test that the index query fetches this version
         assertIndexedOne(cfs, col, 10l);
@@ -345,9 +358,11 @@
         assertIndexedOne(cfs, col, 10l);
 
         // now apply another update, but force the index update to be skipped
-        keyspace.apply(new RowUpdateBuilder(cfs.metadata, 1, "k1").clustering("c").add("birthdate", 20l).build(),
-                       true,
-                       false);
+        builder = new RowUpdateBuilder(cfs.metadata, 0, "k1");
+        if (!isStatic)
+            builder = builder.clustering("c");
+        builder.add(colName, 20l);
+        keyspace.apply(builder.build(), true, false);
 
         // Now searching the index for either the old or new value should return 0 rows
         // because the new value was not indexed and the old value should be ignored
@@ -359,7 +374,11 @@
         // now, reset back to the original value, still skipping the index update, to
         // make sure the value was expunged from the index when it was discovered to be inconsistent
         // TODO: Figure out why this is re-inserting
-        keyspace.apply(new RowUpdateBuilder(cfs.metadata, 2, "k1").clustering("c1").add("birthdate", 10l).build(), true, false);
+        builder = new RowUpdateBuilder(cfs.metadata, 2, "k1");
+        if (!isStatic)
+            builder = builder.clustering("c");
+        builder.add(colName, 10L);
+        keyspace.apply(builder.build(), true, false);
         assertIndexedNone(cfs, col, 20l);
 
         ColumnFamilyStore indexCfs = cfs.indexManager.getAllIndexColumnFamilyStores().iterator().next();
@@ -534,8 +553,8 @@
         if (count != 0)
             assertNotNull(searcher);
 
-        try (ReadOrderGroup orderGroup = rc.startOrderGroup();
-             PartitionIterator iter = UnfilteredPartitionIterators.filter(searcher.search(orderGroup),
+        try (ReadExecutionController executionController = rc.executionController();
+             PartitionIterator iter = UnfilteredPartitionIterators.filter(searcher.search(executionController),
                                                                           FBUtilities.nowInSeconds()))
         {
             assertEquals(count, Util.size(iter));
@@ -545,8 +564,8 @@
     private void assertIndexCfsIsEmpty(ColumnFamilyStore indexCfs)
     {
         PartitionRangeReadCommand command = (PartitionRangeReadCommand)Util.cmd(indexCfs).build();
-        try (ReadOrderGroup orderGroup = command.startOrderGroup();
-             PartitionIterator iter = UnfilteredPartitionIterators.filter(Util.executeLocally(command, indexCfs, orderGroup),
+        try (ReadExecutionController controller = command.executionController();
+             PartitionIterator iter = UnfilteredPartitionIterators.filter(Util.executeLocally(command, indexCfs, controller),
                                                                           FBUtilities.nowInSeconds()))
         {
             assertFalse(iter.hasNext());
diff --git a/test/unit/org/apache/cassandra/db/SerializationHeaderTest.java b/test/unit/org/apache/cassandra/db/SerializationHeaderTest.java
index 3e9f3bc..84fb51c 100644
--- a/test/unit/org/apache/cassandra/db/SerializationHeaderTest.java
+++ b/test/unit/org/apache/cassandra/db/SerializationHeaderTest.java
@@ -21,6 +21,7 @@
 import com.google.common.io.Files;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
@@ -45,6 +46,7 @@
 
 import java.io.File;
 import java.nio.ByteBuffer;
+import java.util.Collections;
 import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.BiFunction;
@@ -54,6 +56,11 @@
 {
     private static String KEYSPACE = "SerializationHeaderTest";
 
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+    
     @Test
     public void testWrittenAsDifferentKind() throws Exception
     {
@@ -77,12 +84,12 @@
 
                 SerializationHeader header = SerializationHeader.makeWithoutStats(schema);
                 try (LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.WRITE);
-                     SSTableWriter sstableWriter = BigTableWriter.create(schema, descriptor, 1, 0L, 0, header, txn))
+                     SSTableWriter sstableWriter = BigTableWriter.create(schema, descriptor, 1, 0L, 0, header, Collections.emptyList(),  txn))
                 {
                     ColumnDefinition cd = schema.getColumnDefinition(v);
                     for (int i = 0 ; i < 5 ; ++i) {
                         final ByteBuffer value = Int32Type.instance.decompose(i);
-                        Cell cell = BufferCell.live(schema, cd, 1L, value);
+                        Cell cell = BufferCell.live(cd, 1L, value);
                         Clustering clustering = clusteringFunction.apply(value);
                         Row row = BTreeRow.singleCellRow(clustering, cell);
                         sstableWriter.append(PartitionUpdate.singleRowUpdate(schema, value, row).unfilteredIterator());
@@ -93,7 +100,7 @@
                 return descriptor;
             };
 
-            Descriptor sstableWithRegular = writer.apply(schemaWithRegular, Clustering::new).call();
+            Descriptor sstableWithRegular = writer.apply(schemaWithRegular, BufferClustering::new).call();
             Descriptor sstableWithStatic = writer.apply(schemaWithStatic, value -> Clustering.STATIC_CLUSTERING).call();
             SSTableReader readerWithStatic = SSTableReader.openNoValidation(sstableWithStatic, schemaWithRegular);
             SSTableReader readerWithRegular = SSTableReader.openNoValidation(sstableWithRegular, schemaWithStatic);
diff --git a/test/unit/org/apache/cassandra/db/SinglePartitionSliceCommandTest.java b/test/unit/org/apache/cassandra/db/SinglePartitionSliceCommandTest.java
index 940b4f9..f5a8cc8 100644
--- a/test/unit/org/apache/cassandra/db/SinglePartitionSliceCommandTest.java
+++ b/test/unit/org/apache/cassandra/db/SinglePartitionSliceCommandTest.java
@@ -43,6 +43,7 @@
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.cql3.QueryOptions;
@@ -93,6 +94,8 @@
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
+        DatabaseDescriptor.daemonInitialization();
+
         cfm = CFMetaData.Builder.create(KEYSPACE, TABLE)
                                 .addPartitionKey("k", UTF8Type.instance)
                                 .addStaticColumn("s", UTF8Type.instance)
@@ -145,7 +148,7 @@
 
         ColumnFilter columnFilter = ColumnFilter.selection(PartitionColumns.of(v));
         ByteBuffer zero = ByteBufferUtil.bytes(0);
-        Slices slices = Slices.with(cfm.comparator, Slice.make(Slice.Bound.inclusiveStartOf(zero), Slice.Bound.inclusiveEndOf(zero)));
+        Slices slices = Slices.with(cfm.comparator, Slice.make(ClusteringBound.inclusiveStartOf(zero), ClusteringBound.inclusiveEndOf(zero)));
         ClusteringIndexSliceFilter sliceFilter = new ClusteringIndexSliceFilter(slices, false);
         ReadCommand cmd = SinglePartitionReadCommand.create(true,
                                                             cfm,
@@ -162,16 +165,22 @@
         cmd = ReadCommand.legacyReadCommandSerializer.deserialize(in, MessagingService.VERSION_21);
 
         logger.debug("ReadCommand: {}", cmd);
-        UnfilteredPartitionIterator partitionIterator = cmd.executeLocally(ReadOrderGroup.emptyGroup());
-        ReadResponse response = ReadResponse.createDataResponse(partitionIterator, cmd);
+        try (ReadExecutionController controller = cmd.executionController();
+             UnfilteredPartitionIterator partitionIterator = cmd.executeLocally(controller))
+        {
+            ReadResponse response = ReadResponse.createDataResponse(partitionIterator, cmd);
 
-        logger.debug("creating response: {}", response);
-        partitionIterator = response.makeIterator(cmd);
-        assert partitionIterator.hasNext();
-        UnfilteredRowIterator partition = partitionIterator.next();
-
-        LegacyLayout.LegacyUnfilteredPartition rowIter = LegacyLayout.fromUnfilteredRowIterator(cmd, partition);
-        Assert.assertEquals(Collections.emptyList(), rowIter.cells);
+            logger.debug("creating response: {}", response);
+            try (UnfilteredPartitionIterator pIter = response.makeIterator(cmd))
+            {
+                assert pIter.hasNext();
+                try (UnfilteredRowIterator partition = pIter.next())
+                {
+                    LegacyLayout.LegacyUnfilteredPartition rowIter = LegacyLayout.fromUnfilteredRowIterator(cmd, partition);
+                    Assert.assertEquals(Collections.emptyList(), rowIter.cells);
+                }
+            }
+        }
     }
 
     @Test
@@ -243,7 +252,7 @@
                                                             key,
                                                             clusteringFilter);
 
-        UnfilteredPartitionIterator partitionIterator = cmd.executeLocally(ReadOrderGroup.emptyGroup());
+        UnfilteredPartitionIterator partitionIterator = cmd.executeLocally(cmd.executionController());
         assert partitionIterator.hasNext();
         UnfilteredRowIterator partition = partitionIterator.next();
 
@@ -270,7 +279,7 @@
                 for (int i = 0; i < CFM_SLICES.comparator.size(); i++)
                 {
                     int cmp = CFM_SLICES.comparator.compareComponent(i,
-                                                                     clustering.values[i],
+                                                                     clustering.getRawValues()[i],
                                                                      marker.clustering().values[i]);
                     assertEquals(0, cmp);
                 }
@@ -326,7 +335,7 @@
                                                             sliceFilter);
 
         // check raw iterator for static cell
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup(); UnfilteredPartitionIterator pi = cmd.executeLocally(orderGroup))
+        try (ReadExecutionController executionController = cmd.executionController(); UnfilteredPartitionIterator pi = cmd.executeLocally(executionController))
         {
             checkForS(pi);
         }
@@ -337,7 +346,7 @@
         ReadResponse dst;
 
         // check (de)serialized iterator for memtable static cell
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup(); UnfilteredPartitionIterator pi = cmd.executeLocally(orderGroup))
+        try (ReadExecutionController executionController = cmd.executionController(); UnfilteredPartitionIterator pi = cmd.executeLocally(executionController))
         {
             response = ReadResponse.createDataResponse(pi, cmd);
         }
@@ -353,7 +362,7 @@
 
         // check (de)serialized iterator for sstable static cell
         Schema.instance.getColumnFamilyStoreInstance(cfm.cfId).forceBlockingFlush();
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup(); UnfilteredPartitionIterator pi = cmd.executeLocally(orderGroup))
+        try (ReadExecutionController executionController = cmd.executionController(); UnfilteredPartitionIterator pi = cmd.executeLocally(executionController))
         {
             response = ReadResponse.createDataResponse(pi, cmd);
         }
@@ -373,7 +382,7 @@
         DecoratedKey key = cfm.decorateKey(ByteBufferUtil.bytes("k1"));
 
         ColumnFilter columnFilter = ColumnFilter.selection(PartitionColumns.of(s));
-        Slice slice = Slice.make(Slice.Bound.BOTTOM, Slice.Bound.inclusiveEndOf(ByteBufferUtil.bytes("i1")));
+        Slice slice = Slice.make(ClusteringBound.BOTTOM, ClusteringBound.inclusiveEndOf(ByteBufferUtil.bytes("i1")));
         ClusteringIndexSliceFilter sliceFilter = new ClusteringIndexSliceFilter(Slices.with(cfm.comparator, slice), false);
         ReadCommand cmd = SinglePartitionReadCommand.create(true,
                                                             cfm,
@@ -398,8 +407,8 @@
         SinglePartitionReadCommand.Group query = (SinglePartitionReadCommand.Group) stmt.getQuery(QueryOptions.DEFAULT, 0);
         Assert.assertEquals(1, query.commands.size());
         SinglePartitionReadCommand command = Iterables.getOnlyElement(query.commands);
-        try (ReadOrderGroup group = ReadOrderGroup.forCommand(command);
-             UnfilteredPartitionIterator partitions = command.executeLocally(group))
+        try (ReadExecutionController controller = ReadExecutionController.forCommand(command);
+             UnfilteredPartitionIterator partitions = command.executeLocally(controller))
         {
             assert partitions.hasNext();
             try (UnfilteredRowIterator partition = partitions.next())
@@ -452,7 +461,7 @@
         cfs.truncateBlocking();
 
         long nowMillis = System.currentTimeMillis();
-        Slice slice = Slice.make(new Clustering(bb(2), bb(3)), new Clustering(bb(10), bb(10)));
+        Slice slice = Slice.make(Clustering.make(bb(2), bb(3)), Clustering.make(bb(10), bb(10)));
         RangeTombstone rt = new RangeTombstone(slice, new DeletionTime(TimeUnit.MILLISECONDS.toMicros(nowMillis),
                                                                        Ints.checkedCast(TimeUnit.MILLISECONDS.toSeconds(nowMillis))));
         PartitionUpdate update = new PartitionUpdate(cfs.metadata, bb(100), cfs.metadata.partitionColumns(), 1);
diff --git a/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java b/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java
index 223fda2..80c21d2 100644
--- a/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java
+++ b/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java
@@ -33,6 +33,7 @@
 import org.apache.cassandra.concurrent.Stage;
 import org.apache.cassandra.concurrent.StageManager;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.dht.ByteOrderedPartitioner.BytesToken;
@@ -55,9 +56,9 @@
     @BeforeClass
     public static void prepSnapshotTracker()
     {
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
 
-        if (FBUtilities.isWindows())
+        if (FBUtilities.isWindows)
             WindowsFailedSnapshotTracker.deleteOldSnapshots();
     }
 
@@ -104,7 +105,7 @@
 
     private void assertDeletedOrDeferred(int expectedCount)
     {
-        if (FBUtilities.isWindows())
+        if (FBUtilities.isWindows)
             assertEquals(expectedCount, getDeferredDeletionCount());
         else
             assertTrue(getSystemSnapshotFiles().isEmpty());
@@ -128,9 +129,9 @@
     public void snapshotSystemKeyspaceIfUpgrading() throws IOException
     {
         // First, check that in the absence of any previous installed version, we don't create snapshots
-        for (ColumnFamilyStore cfs : Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStores())
+        for (ColumnFamilyStore cfs : Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStores())
             cfs.clearUnsafe();
-        Keyspace.clearSnapshot(null, SystemKeyspace.NAME);
+        Keyspace.clearSnapshot(null, SchemaConstants.SYSTEM_KEYSPACE_NAME);
 
         int baseline = getDeferredDeletionCount();
 
@@ -139,7 +140,7 @@
 
         // now setup system.local as if we're upgrading from a previous version
         setupReleaseVersion(getOlderVersionString());
-        Keyspace.clearSnapshot(null, SystemKeyspace.NAME);
+        Keyspace.clearSnapshot(null, SchemaConstants.SYSTEM_KEYSPACE_NAME);
         assertDeletedOrDeferred(baseline);
 
         // Compare versions again & verify that snapshots were created for all tables in the system ks
@@ -148,7 +149,7 @@
 
         // clear out the snapshots & set the previous recorded version equal to the latest, we shouldn't
         // see any new snapshots created this time.
-        Keyspace.clearSnapshot(null, SystemKeyspace.NAME);
+        Keyspace.clearSnapshot(null, SchemaConstants.SYSTEM_KEYSPACE_NAME);
         setupReleaseVersion(FBUtilities.getReleaseVersionString());
 
         SystemKeyspace.snapshotOnVersionChange();
@@ -158,7 +159,7 @@
         // 10 files expected.
         assertDeletedOrDeferred(baseline + 10);
 
-        Keyspace.clearSnapshot(null, SystemKeyspace.NAME);
+        Keyspace.clearSnapshot(null, SchemaConstants.SYSTEM_KEYSPACE_NAME);
     }
 
     @Test
@@ -342,7 +343,7 @@
     private Set<String> getSystemSnapshotFiles()
     {
         Set<String> snapshottedTableNames = new HashSet<>();
-        for (ColumnFamilyStore cfs : Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStores())
+        for (ColumnFamilyStore cfs : Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStores())
         {
             if (!cfs.getSnapshotDetails().isEmpty())
                 snapshottedTableNames.add(cfs.getColumnFamilyName());
diff --git a/test/unit/org/apache/cassandra/db/TransformerTest.java b/test/unit/org/apache/cassandra/db/TransformerTest.java
index d56d8cd..fe87af8 100644
--- a/test/unit/org/apache/cassandra/db/TransformerTest.java
+++ b/test/unit/org/apache/cassandra/db/TransformerTest.java
@@ -26,6 +26,7 @@
 import junit.framework.Assert;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.marshal.BytesType;
 import org.apache.cassandra.db.marshal.Int32Type;
 import org.apache.cassandra.db.rows.*;
@@ -38,6 +39,10 @@
 
 public class TransformerTest
 {
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
 
     static final CFMetaData metadata = metadata();
     static final DecoratedKey partitionKey = new BufferDecoratedKey(new Murmur3Partitioner.LongToken(0L), ByteBufferUtil.EMPTY_BYTE_BUFFER);
diff --git a/test/unit/org/apache/cassandra/db/VerifyTest.java b/test/unit/org/apache/cassandra/db/VerifyTest.java
index 0748270..a332f74 100644
--- a/test/unit/org/apache/cassandra/db/VerifyTest.java
+++ b/test/unit/org/apache/cassandra/db/VerifyTest.java
@@ -23,6 +23,7 @@
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
+import org.apache.cassandra.cache.ChunkCache;
 import org.apache.cassandra.UpdateBuilder;
 import org.apache.cassandra.db.compaction.AbstractCompactionStrategy;
 import org.apache.cassandra.db.compaction.CompactionManager;
@@ -281,11 +282,12 @@
         SSTableReader sstable = cfs.getLiveSSTables().iterator().next();
 
 
-        RandomAccessFile file = new RandomAccessFile(sstable.descriptor.filenameFor(sstable.descriptor.digestComponent), "rw");
-        Long correctChecksum = Long.parseLong(file.readLine());
-        file.close();
-
-        writeChecksum(++correctChecksum, sstable.descriptor.filenameFor(sstable.descriptor.digestComponent));
+        try (RandomAccessFile file = new RandomAccessFile(sstable.descriptor.filenameFor(sstable.descriptor.digestComponent), "rw"))
+        {
+            Long correctChecksum = Long.valueOf(file.readLine());
+    
+            writeChecksum(++correctChecksum, sstable.descriptor.filenameFor(sstable.descriptor.digestComponent));
+        }
 
         try (Verifier verifier = new Verifier(cfs, sstable, false))
         {
@@ -319,6 +321,8 @@
         file.seek(startPosition);
         file.writeBytes(StringUtils.repeat('z', (int) 2));
         file.close();
+        if (ChunkCache.instance != null)
+            ChunkCache.instance.invalidateFile(sstable.getFilename());
 
         // Update the Digest to have the right Checksum
         writeChecksum(simpleFullChecksum(sstable.getFilename()), sstable.descriptor.filenameFor(sstable.descriptor.digestComponent));
@@ -435,13 +439,15 @@
 
     protected long simpleFullChecksum(String filename) throws IOException
     {
-        FileInputStream inputStream = new FileInputStream(filename);
-        CRC32 checksum = new CRC32();
-        CheckedInputStream cinStream = new CheckedInputStream(inputStream, checksum);
-        byte[] b = new byte[128];
-        while (cinStream.read(b) >= 0) {
+        try (FileInputStream inputStream = new FileInputStream(filename))
+        {
+            CRC32 checksum = new CRC32();
+            CheckedInputStream cinStream = new CheckedInputStream(inputStream, checksum);
+            byte[] b = new byte[128];
+            while (cinStream.read(b) >= 0) {
+            }
+            return cinStream.getChecksum().getValue();
         }
-        return cinStream.getChecksum().getValue();
     }
 
     protected void writeChecksum(long checksum, String filePath)
diff --git a/test/unit/org/apache/cassandra/db/aggregation/GroupMakerTest.java b/test/unit/org/apache/cassandra/db/aggregation/GroupMakerTest.java
new file mode 100644
index 0000000..563a709
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/aggregation/GroupMakerTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.db.aggregation;
+
+import java.nio.ByteBuffer;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.ClusteringComparator;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.ReversedType;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class GroupMakerTest
+{
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
+    @Test
+    public void testIsNewGroupWithClusteringColumns()
+    {
+        ClusteringComparator comparator = newComparator(false, false, false);
+        GroupMaker groupMaker = GroupMaker.newInstance(comparator, 2);
+
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 1)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 2)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 3)));
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 2, 1)));
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 3, 1)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 3, 2)));
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(2, 1, 1)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(2, 1, 2)));
+
+        assertTrue(groupMaker.isNewGroup(partitionKey(2), clustering(2, 2, 1)));
+    }
+
+    @Test
+    public void testIsNewGroupWithOneClusteringColumnsPrefix()
+    {
+        ClusteringComparator comparator = newComparator(false, false, false);
+        GroupMaker groupMaker = GroupMaker.newInstance(comparator, 1);
+
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 1)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 2)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 3)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 2, 1)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 3, 1)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 3, 2)));
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(2, 1, 1)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(2, 1, 2)));
+
+        assertTrue(groupMaker.isNewGroup(partitionKey(2), clustering(2, 2, 1)));
+    }
+
+    @Test
+    public void testIsNewGroupWithReversedClusteringColumns()
+    {
+        ClusteringComparator comparator = newComparator(true, true, true);
+
+        GroupMaker groupMaker = GroupMaker.newInstance(comparator, 2);
+
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 3, 2)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 3, 1)));
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 2, 1)));
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 3)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 2)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 1)));
+
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(2, 1, 2)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(2, 1, 1)));
+
+        assertTrue(groupMaker.isNewGroup(partitionKey(2), clustering(2, 2, 1)));
+    }
+
+    @Test
+    public void testIsNewGroupWithOneReversedClusteringColumns()
+    {
+        ClusteringComparator comparator = newComparator(true, false, false);
+
+        GroupMaker groupMaker = GroupMaker.newInstance(comparator, 2);
+
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 3, 1)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 3, 2)));
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 2, 1)));
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 1)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 2)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 3)));
+
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(2, 1, 1)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(2, 1, 2)));
+
+        assertTrue(groupMaker.isNewGroup(partitionKey(2), clustering(2, 2, 1)));
+    }
+
+    @Test
+    public void testIsNewGroupWithStaticClusteringColumns()
+    {
+        ClusteringComparator comparator = newComparator(false, false, false);
+        GroupMaker groupMaker = GroupMaker.newInstance(comparator, 2);
+
+        assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 1)));
+        assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 2)));
+
+        assertTrue(groupMaker.isNewGroup(partitionKey(2), Clustering.STATIC_CLUSTERING));
+        assertTrue(groupMaker.isNewGroup(partitionKey(3), Clustering.STATIC_CLUSTERING));
+        assertTrue(groupMaker.isNewGroup(partitionKey(4), clustering(1, 1, 2)));
+    }
+
+    @Test
+    public void testIsNewGroupWithOnlyPartitionKeyComponents()
+    {
+        ClusteringComparator comparator = newComparator(false, false, false);
+        GroupMaker goupMaker = GroupMaker.newInstance(comparator, 2);
+
+        assertTrue(goupMaker.isNewGroup(partitionKey(1, 1), clustering(1, 1, 1)));
+        assertFalse(goupMaker.isNewGroup(partitionKey(1, 1), clustering(1, 1, 2)));
+
+        assertTrue(goupMaker.isNewGroup(partitionKey(1, 2), clustering(1, 1, 2)));
+        assertTrue(goupMaker.isNewGroup(partitionKey(1, 2), clustering(2, 2, 2)));
+
+        assertTrue(goupMaker.isNewGroup(partitionKey(2, 2), clustering(1, 1, 2)));
+    }
+
+    private static DecoratedKey partitionKey(int... components)
+    {
+        ByteBuffer buffer = ByteBuffer.allocate(components.length * 4);
+        for (int component : components)
+        {
+            buffer.putInt(component);
+        }
+        buffer.flip();
+        return DatabaseDescriptor.getPartitioner().decorateKey(buffer);
+    }
+
+    private static Clustering clustering(int... components)
+    {
+        return Clustering.make(toByteBufferArray(components));
+    }
+
+    private static ByteBuffer[] toByteBufferArray(int[] values)
+    {
+        ByteBuffer[] buffers = new ByteBuffer[values.length];
+
+        for (int i = 0; i < values.length; i++)
+        {
+            buffers[i] = Int32Type.instance.decompose(values[i]);
+        }
+
+        return buffers;
+    }
+
+    private static ClusteringComparator newComparator(boolean... reversed)
+    {
+        AbstractType<?>[] types = new AbstractType<?>[reversed.length];
+        for (int i = 0, m = reversed.length; i < m; i++)
+            types[i] = reversed[i] ? ReversedType.getInstance(Int32Type.instance) : Int32Type.instance;
+
+        return new ClusteringComparator(types);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/columniterator/SSTableReverseIteratorTest.java b/test/unit/org/apache/cassandra/db/columniterator/SSTableReverseIteratorTest.java
index 2f183c0..9040f11 100644
--- a/test/unit/org/apache/cassandra/db/columniterator/SSTableReverseIteratorTest.java
+++ b/test/unit/org/apache/cassandra/db/columniterator/SSTableReverseIteratorTest.java
@@ -86,7 +86,7 @@
         DecoratedKey dk = tbl.getPartitioner().decorateKey(Int32Type.instance.decompose(key));
         RowIndexEntry indexEntry = sstable.getPosition(dk, SSTableReader.Operator.EQ);
         Assert.assertTrue(indexEntry.isIndexed());
-        Assert.assertTrue(indexEntry.columnsIndex().size() > 2);
+        Assert.assertTrue(indexEntry.columnsIndexCount() > 2);
 
         // drop v1 so the first 2 index blocks only contain empty unfiltereds
         QueryProcessor.executeInternal(String.format("ALTER TABLE %s.%s DROP v1", KEYSPACE, table));
diff --git a/test/unit/org/apache/cassandra/db/commitlog/AbstractCommitLogServiceTest.java b/test/unit/org/apache/cassandra/db/commitlog/AbstractCommitLogServiceTest.java
index 6f51eaf..bc5cb29 100644
--- a/test/unit/org/apache/cassandra/db/commitlog/AbstractCommitLogServiceTest.java
+++ b/test/unit/org/apache/cassandra/db/commitlog/AbstractCommitLogServiceTest.java
@@ -23,7 +23,6 @@
 
 import org.junit.Assert;
 import org.junit.BeforeClass;
-import org.junit.Ignore;
 import org.junit.Test;
 
 import org.apache.cassandra.config.Config;
@@ -39,6 +38,7 @@
     @BeforeClass
     public static void before()
     {
+        DatabaseDescriptor.daemonInitialization();
         DatabaseDescriptor.setCommitLogSync(Config.CommitLogSync.periodic);
         DatabaseDescriptor.setCommitLogSyncPeriod(10 * 1000);
     }
@@ -48,8 +48,8 @@
     {
         long syncTimeMillis = 10 * 1000;
         FakeCommitLogService commitLogService = new FakeCommitLogService(syncTimeMillis);
-        Assert.assertEquals(DEFAULT_MARKER_INTERVAL_MILLIS, commitLogService.markerIntervalMillis);
-        Assert.assertEquals(syncTimeMillis, commitLogService.syncIntervalMillis);
+        Assert.assertEquals(toNanos(DEFAULT_MARKER_INTERVAL_MILLIS), commitLogService.markerIntervalNanos);
+        Assert.assertEquals(toNanos(syncTimeMillis), commitLogService.syncIntervalNanos);
     }
 
     @Test
@@ -57,9 +57,9 @@
     {
         long syncTimeMillis = 100;
         FakeCommitLogService commitLogService = new FakeCommitLogService(syncTimeMillis);
-        Assert.assertEquals(DEFAULT_MARKER_INTERVAL_MILLIS, commitLogService.markerIntervalMillis);
-        Assert.assertEquals(syncTimeMillis, commitLogService.syncIntervalMillis);
-        Assert.assertEquals(commitLogService.markerIntervalMillis, commitLogService.syncIntervalMillis);
+        Assert.assertEquals(toNanos(DEFAULT_MARKER_INTERVAL_MILLIS), commitLogService.markerIntervalNanos);
+        Assert.assertEquals(toNanos(syncTimeMillis), commitLogService.syncIntervalNanos);
+        Assert.assertEquals(commitLogService.markerIntervalNanos, commitLogService.syncIntervalNanos);
     }
 
     @Test
@@ -68,8 +68,8 @@
         long syncTimeMillis = 151;
         long expectedMillis = 200;
         FakeCommitLogService commitLogService = new FakeCommitLogService(syncTimeMillis);
-        Assert.assertEquals(DEFAULT_MARKER_INTERVAL_MILLIS, commitLogService.markerIntervalMillis);
-        Assert.assertEquals(expectedMillis, commitLogService.syncIntervalMillis);
+        Assert.assertEquals(toNanos(DEFAULT_MARKER_INTERVAL_MILLIS), commitLogService.markerIntervalNanos);
+        Assert.assertEquals(toNanos(expectedMillis), commitLogService.syncIntervalNanos);
     }
 
     @Test
@@ -78,18 +78,23 @@
         long syncTimeMillis = 121;
         long expectedMillis = 100;
         FakeCommitLogService commitLogService = new FakeCommitLogService(syncTimeMillis);
-        Assert.assertEquals(DEFAULT_MARKER_INTERVAL_MILLIS, commitLogService.markerIntervalMillis);
-        Assert.assertEquals(expectedMillis, commitLogService.syncIntervalMillis);
+        Assert.assertEquals(toNanos(DEFAULT_MARKER_INTERVAL_MILLIS), commitLogService.markerIntervalNanos);
+        Assert.assertEquals(toNanos(expectedMillis), commitLogService.syncIntervalNanos);
     }
 
     @Test
     public void testConstructorSyncTinyValue()
     {
         long syncTimeMillis = 10;
-        long expectedNanos = syncTimeMillis;
+        long expectedNanos = toNanos(syncTimeMillis);
         FakeCommitLogService commitLogService = new FakeCommitLogService(syncTimeMillis);
-        Assert.assertEquals(expectedNanos, commitLogService.markerIntervalMillis);
-        Assert.assertEquals(expectedNanos, commitLogService.syncIntervalMillis);
+        Assert.assertEquals(expectedNanos, commitLogService.markerIntervalNanos);
+        Assert.assertEquals(expectedNanos, commitLogService.syncIntervalNanos);
+    }
+
+    private static long toNanos(long millis)
+    {
+        return TimeUnit.MILLISECONDS.toNanos(millis);
     }
 
     private static class FakeCommitLogService extends AbstractCommitLogService
@@ -100,12 +105,6 @@
             lastSyncedAt = 0;
         }
 
-        @Override
-        void start()
-        {
-            // nop
-        }
-
         protected void maybeWaitForSync(CommitLogSegment.Allocation alloc)
         {
             // nop
@@ -153,20 +152,11 @@
 
         FakeCommitLog()
         {
-            super(DatabaseDescriptor.getCommitLogLocation(), null);
+            super(null);
         }
 
         @Override
-        CommitLog start()
-        {
-            // this is a bit dicey. we need to start the allocator, but starting the parent's executor will muck things
-            // up as it is pointing to a different executor service, not the fake one in this test class.
-            allocator.start();
-            return this;
-        }
-
-        @Override
-        public void sync(boolean syncAllSegments, boolean flush)
+        public void sync(boolean flush)
         {
             if (flush)
                 syncCount.incrementAndGet();
@@ -181,7 +171,7 @@
         long syncTimeMillis = 10;
         SyncRunnable syncRunnable = new FakeCommitLogService(syncTimeMillis).new SyncRunnable(new FreeRunningClock());
         long pollStarted = 1;
-        long now = pollStarted + (syncTimeMillis * 2);
+        long now = Integer.MAX_VALUE;
         Assert.assertTrue(syncRunnable.maybeLogFlushLag(pollStarted, now));
         Assert.assertEquals(now - pollStarted, syncRunnable.getTotalSyncDuration());
     }
@@ -216,7 +206,7 @@
             Assert.assertEquals(i * (now - pollStarted), syncRunnable.getTotalSyncDuration());
         }
 
-        now = pollStarted + (syncTimeMillis * 2);
+        now = pollStarted + Integer.MAX_VALUE;
         Assert.assertTrue(syncRunnable.maybeLogFlushLag(pollStarted, now));
         Assert.assertEquals(now - pollStarted, syncRunnable.getTotalSyncDuration());
     }
diff --git a/test/unit/org/apache/cassandra/db/commitlog/BatchCommitLogTest.java b/test/unit/org/apache/cassandra/db/commitlog/BatchCommitLogTest.java
index 07a2359..1f8dbdf 100644
--- a/test/unit/org/apache/cassandra/db/commitlog/BatchCommitLogTest.java
+++ b/test/unit/org/apache/cassandra/db/commitlog/BatchCommitLogTest.java
@@ -46,6 +46,7 @@
     @BeforeClass
     public static void before()
     {
+        DatabaseDescriptor.daemonInitialization();
         DatabaseDescriptor.setCommitLogSync(Config.CommitLogSync.batch);
         DatabaseDescriptor.setCommitLogSyncBatchWindow(CL_BATCH_SYNC_WINDOW);
 
@@ -62,9 +63,9 @@
     {
         ColumnFamilyStore cfs1 = Keyspace.open(KEYSPACE1).getColumnFamilyStore(STANDARD1);
         Mutation m = new RowUpdateBuilder(cfs1.metadata, 0, "key")
-                         .clustering("bytes")
-                         .add("val", ByteBuffer.allocate(10 * 1024))
-                         .build();
+                     .clustering("bytes")
+                     .add("val", ByteBuffer.allocate(10 * 1024))
+                     .build();
 
         long startNano = System.nanoTime();
         CommitLog.instance.add(m);
diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogCQLTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogCQLTest.java
index 47c9891..4725bcf 100644
--- a/test/unit/org/apache/cassandra/db/commitlog/CommitLogCQLTest.java
+++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogCQLTest.java
@@ -38,7 +38,7 @@
         flush();
 
         // We write something in different table to advance the commit log position. Current table remains clean.
-        execute(String.format("INSERT INTO %s.%s (idx, data) VALUES (?, ?)", keyspace(), otherTable), 16, Integer.toString(16));
+        executeFormattedQuery(String.format("INSERT INTO %s.%s (idx, data) VALUES (?, ?)", keyspace(), otherTable), 16, Integer.toString(16));
 
         ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
         assert cfs.getTracker().getView().getCurrentMemtable().isClean();
@@ -49,11 +49,11 @@
 
         execute("INSERT INTO %s (idx, data) VALUES (?, ?)", 15, Integer.toString(17));
 
-        Collection<CommitLogSegment> active = new ArrayList<>(CommitLog.instance.allocator.getActiveSegments());
+        Collection<CommitLogSegment> active = new ArrayList<>(CommitLog.instance.segmentManager.getActiveSegments());
         CommitLog.instance.forceRecycleAllSegments();
 
         // If one of the previous segments remains, it wasn't clean.
-        active.retainAll(CommitLog.instance.allocator.getActiveSegments());
+        active.retainAll(CommitLog.instance.segmentManager.getActiveSegments());
         assert active.isEmpty();
     }
 }
diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogChainedMarkersTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogChainedMarkersTest.java
index b73275b..959029a 100644
--- a/test/unit/org/apache/cassandra/db/commitlog/CommitLogChainedMarkersTest.java
+++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogChainedMarkersTest.java
@@ -21,6 +21,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
 import java.util.Random;
 
 import org.junit.Assert;
@@ -28,7 +29,6 @@
 import org.junit.runner.RunWith;
 
 import org.apache.cassandra.SchemaLoader;
-import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ColumnFamilyStore;
@@ -38,9 +38,6 @@
 import org.apache.cassandra.db.compaction.CompactionManager;
 import org.apache.cassandra.db.marshal.AsciiType;
 import org.apache.cassandra.db.marshal.BytesType;
-import org.apache.cassandra.db.rows.SerializationHelper;
-import org.apache.cassandra.io.util.DataInputBuffer;
-import org.apache.cassandra.io.util.RebufferingInputStream;
 import org.apache.cassandra.schema.KeyspaceParams;
 import org.jboss.byteman.contrib.bmunit.BMRule;
 import org.jboss.byteman.contrib.bmunit.BMUnitRunner;
@@ -62,6 +59,8 @@
     action = "$flush = false")
     public void replayCommitLogWithoutFlushing() throws IOException
     {
+        // this method is blend of CommitLogSegmentBackpressureTest & CommitLogReaderTest methods
+        DatabaseDescriptor.daemonInitialization();
         DatabaseDescriptor.setCommitLogSegmentSize(5);
         DatabaseDescriptor.setCommitLogSync(Config.CommitLogSync.periodic);
         DatabaseDescriptor.setCommitLogSyncPeriod(10000 * 1000);
@@ -85,43 +84,14 @@
         for (int i = 0; i < samples; i++)
             CommitLog.instance.add(m);
 
-        CommitLog.instance.sync(false, true);
+        CommitLog.instance.sync(false);
 
-        Replayer replayer = new Replayer(cfs1.metadata);
-        File commitLogDir = new File(DatabaseDescriptor.getCommitLogLocation());
-        replayer.recover(commitLogDir.listFiles());
-        Assert.assertEquals(samples, replayer.count);
-    }
+        ArrayList<File> toCheck = CommitLogReaderTest.getCommitLogs();
+        CommitLogReader reader = new CommitLogReader();
+        CommitLogReaderTest.TestCLRHandler testHandler = new CommitLogReaderTest.TestCLRHandler(cfs1.metadata);
+        for (File f : toCheck)
+            reader.readCommitLogSegment(testHandler, f, CommitLogReader.ALL_MUTATIONS, false);
 
-    private static class Replayer extends CommitLogReplayer
-    {
-        private final CFMetaData cfm;
-        private int count;
-
-        Replayer(CFMetaData cfm)
-        {
-            super(CommitLog.instance, ReplayPosition.NONE, null, ReplayFilter.create());
-            this.cfm = cfm;
-        }
-
-        @Override
-        void replayMutation(byte[] inputBuffer, int size, final int entryLocation, final CommitLogDescriptor desc)
-        {
-            RebufferingInputStream bufIn = new DataInputBuffer(inputBuffer, 0, size);
-            try
-            {
-                Mutation mutation = Mutation.serializer.deserialize(bufIn,
-                                                           desc.getMessagingVersion(),
-                                                           SerializationHelper.Flag.LOCAL);
-
-                if (cfm == null || mutation.get(cfm) != null)
-                    count++;
-            }
-            catch (IOException e)
-            {
-                // Test fails.
-                throw new AssertionError(e);
-            }
-        }
+        Assert.assertEquals(samples, testHandler.seenMutationCount());
     }
 }
diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogDescriptorTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogDescriptorTest.java
index 898c19f..a6bbdb6 100644
--- a/test/unit/org/apache/cassandra/db/commitlog/CommitLogDescriptorTest.java
+++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogDescriptorTest.java
@@ -15,88 +15,301 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.cassandra.db.commitlog;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
 import com.google.common.collect.ImmutableMap;
-
+import org.junit.Assert;
+import org.junit.Before;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.config.TransparentDataEncryptionOptions;
 import org.apache.cassandra.exceptions.ConfigurationException;
-import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.compress.LZ4Compressor;
+import org.apache.cassandra.io.util.FileDataInput;
+import org.apache.cassandra.io.util.FileSegmentInputStream;
 import org.apache.cassandra.net.MessagingService;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.security.EncryptionContextGenerator;
 
 public class CommitLogDescriptorTest
 {
+    private static final byte[] iv = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
+
+    ParameterizedClass compression;
+    TransparentDataEncryptionOptions enabledTdeOptions;
+
+    // Context with enabledTdeOptions enabled
+    EncryptionContext enabledEncryption;
+
+    // Context with enabledTdeOptions disabled, with the assumption that enabledTdeOptions was never previously enabled
+    EncryptionContext neverEnabledEncryption;
+
+    // Context with enabledTdeOptions disabled, with the assumption that enabledTdeOptions was previously enabled, but now disabled
+    // due to operator changing the yaml.
+    EncryptionContext previouslyEnabledEncryption;
+
+    @Before
+    public void setup()
+    {
+        Map<String,String> params = new HashMap<>();
+        compression = new ParameterizedClass(LZ4Compressor.class.getName(), params);
+
+        enabledTdeOptions = EncryptionContextGenerator.createEncryptionOptions();
+        enabledEncryption = new EncryptionContext(enabledTdeOptions, iv, false);
+        
+        neverEnabledEncryption = EncryptionContextGenerator.createDisabledContext();
+        TransparentDataEncryptionOptions disaabledTdeOptions = new TransparentDataEncryptionOptions(false, enabledTdeOptions.cipher, enabledTdeOptions.key_alias, enabledTdeOptions.key_provider);
+        previouslyEnabledEncryption = new EncryptionContext(disaabledTdeOptions);
+        
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testVersions()
     {
-        assertTrue(CommitLogDescriptor.isValid("CommitLog-1340512736956320000.log"));
-        assertTrue(CommitLogDescriptor.isValid("CommitLog-2-1340512736956320000.log"));
-        assertFalse(CommitLogDescriptor.isValid("CommitLog--1340512736956320000.log"));
-        assertFalse(CommitLogDescriptor.isValid("CommitLog--2-1340512736956320000.log"));
-        assertFalse(CommitLogDescriptor.isValid("CommitLog-2-1340512736956320000-123.log"));
+        Assert.assertTrue(CommitLogDescriptor.isValid("CommitLog-1340512736956320000.log"));
+        Assert.assertTrue(CommitLogDescriptor.isValid("CommitLog-2-1340512736956320000.log"));
+        Assert.assertFalse(CommitLogDescriptor.isValid("CommitLog--1340512736956320000.log"));
+        Assert.assertFalse(CommitLogDescriptor.isValid("CommitLog--2-1340512736956320000.log"));
+        Assert.assertFalse(CommitLogDescriptor.isValid("CommitLog-2-1340512736956320000-123.log"));
 
-        assertEquals(1340512736956320000L, CommitLogDescriptor.fromFileName("CommitLog-2-1340512736956320000.log").id);
+        Assert.assertEquals(1340512736956320000L, CommitLogDescriptor.fromFileName("CommitLog-2-1340512736956320000.log").id);
 
-        assertEquals(MessagingService.current_version, new CommitLogDescriptor(1340512736956320000L, null).getMessagingVersion());
+        Assert.assertEquals(MessagingService.current_version, new CommitLogDescriptor(1340512736956320000L, null, neverEnabledEncryption).getMessagingVersion());
         String newCLName = "CommitLog-" + CommitLogDescriptor.current_version + "-1340512736956320000.log";
-        assertEquals(MessagingService.current_version, CommitLogDescriptor.fromFileName(newCLName).getMessagingVersion());
+        Assert.assertEquals(MessagingService.current_version, CommitLogDescriptor.fromFileName(newCLName).getMessagingVersion());
     }
 
+    // migrated from CommitLogTest
     private void testDescriptorPersistence(CommitLogDescriptor desc) throws IOException
     {
         ByteBuffer buf = ByteBuffer.allocate(1024);
         CommitLogDescriptor.writeHeader(buf, desc);
+        long length = buf.position();
         // Put some extra data in the stream.
         buf.putDouble(0.1);
         buf.flip();
-
-        try (DataInputBuffer input = new DataInputBuffer(buf, false))
-        {
-            CommitLogDescriptor read = CommitLogDescriptor.readHeader(input);
-            assertEquals("Descriptors", desc, read);
-        }
+        FileDataInput input = new FileSegmentInputStream(buf, "input", 0);
+        CommitLogDescriptor read = CommitLogDescriptor.readHeader(input, neverEnabledEncryption);
+        Assert.assertEquals("Descriptor length", length, input.getFilePointer());
+        Assert.assertEquals("Descriptors", desc, read);
     }
 
+    // migrated from CommitLogTest
     @Test
     public void testDescriptorPersistence() throws IOException
     {
-        testDescriptorPersistence(new CommitLogDescriptor(11, null));
-        testDescriptorPersistence(new CommitLogDescriptor(CommitLogDescriptor.VERSION_21, 13, null));
-        testDescriptorPersistence(new CommitLogDescriptor(CommitLogDescriptor.VERSION_30, 15, null));
-        testDescriptorPersistence(new CommitLogDescriptor(CommitLogDescriptor.VERSION_30, 17, new ParameterizedClass("LZ4Compressor", null)));
-        testDescriptorPersistence(new CommitLogDescriptor(CommitLogDescriptor.VERSION_30, 19,
-                new ParameterizedClass("StubbyCompressor", ImmutableMap.of("parameter1", "value1", "flag2", "55", "argument3", "null"))));
+        testDescriptorPersistence(new CommitLogDescriptor(11, null, neverEnabledEncryption));
+        testDescriptorPersistence(new CommitLogDescriptor(CommitLogDescriptor.VERSION_21, 13, null, neverEnabledEncryption));
+        testDescriptorPersistence(new CommitLogDescriptor(CommitLogDescriptor.VERSION_22, 15, null, neverEnabledEncryption));
+        testDescriptorPersistence(new CommitLogDescriptor(CommitLogDescriptor.VERSION_22, 17, new ParameterizedClass("LZ4Compressor", null), neverEnabledEncryption));
+        testDescriptorPersistence(new CommitLogDescriptor(CommitLogDescriptor.VERSION_22, 19,
+                                                          new ParameterizedClass("StubbyCompressor", ImmutableMap.of("parameter1", "value1", "flag2", "55", "argument3", "null")
+                                                          ), neverEnabledEncryption));
     }
 
+    // migrated from CommitLogTest
     @Test
     public void testDescriptorInvalidParametersSize() throws IOException
     {
-        final int numberOfParameters = 65535;
-        Map<String, String> params = new HashMap<>(numberOfParameters);
-        for (int i=0; i<numberOfParameters; ++i)
+        Map<String, String> params = new HashMap<>();
+        for (int i=0; i<65535; ++i)
             params.put("key"+i, Integer.toString(i, 16));
         try {
-            CommitLogDescriptor desc = new CommitLogDescriptor(CommitLogDescriptor.VERSION_30,
+            CommitLogDescriptor desc = new CommitLogDescriptor(CommitLogDescriptor.VERSION_22,
                                                                21,
-                                                               new ParameterizedClass("LZ4Compressor", params));
+                                                               new ParameterizedClass("LZ4Compressor", params),
+                                                               neverEnabledEncryption);
+
             ByteBuffer buf = ByteBuffer.allocate(1024000);
             CommitLogDescriptor.writeHeader(buf, desc);
-            fail("Parameter object too long should fail on writing descriptor.");
+            Assert.fail("Parameter object too long should fail on writing descriptor.");
         } catch (ConfigurationException e)
         {
             // correct path
         }
     }
+
+    @Test
+    public void constructParametersString_NoCompressionOrEncryption()
+    {
+        String json = CommitLogDescriptor.constructParametersString(null, null, Collections.emptyMap());
+        Assert.assertFalse(json.contains(CommitLogDescriptor.COMPRESSION_CLASS_KEY));
+        Assert.assertFalse(json.contains(EncryptionContext.ENCRYPTION_CIPHER));
+
+        json = CommitLogDescriptor.constructParametersString(null, neverEnabledEncryption, Collections.emptyMap());
+        Assert.assertFalse(json.contains(CommitLogDescriptor.COMPRESSION_CLASS_KEY));
+        Assert.assertFalse(json.contains(EncryptionContext.ENCRYPTION_CIPHER));
+    }
+
+    @Test
+    public void constructParametersString_WithCompressionAndEncryption()
+    {
+        String json = CommitLogDescriptor.constructParametersString(compression, enabledEncryption, Collections.emptyMap());
+        Assert.assertTrue(json.contains(CommitLogDescriptor.COMPRESSION_CLASS_KEY));
+        Assert.assertTrue(json.contains(EncryptionContext.ENCRYPTION_CIPHER));
+    }
+
+    @Test
+    public void writeAndReadHeader_NoCompressionOrEncryption() throws IOException
+    {
+        CommitLogDescriptor descriptor = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, neverEnabledEncryption);
+        ByteBuffer buffer = ByteBuffer.allocate(16 * 1024);
+        CommitLogDescriptor.writeHeader(buffer, descriptor);
+        buffer.flip();
+        FileSegmentInputStream dataInput = new FileSegmentInputStream(buffer, null, 0);
+        CommitLogDescriptor result = CommitLogDescriptor.readHeader(dataInput, neverEnabledEncryption);
+        Assert.assertNotNull(result);
+        Assert.assertNull(result.compression);
+        Assert.assertFalse(result.getEncryptionContext().isEnabled());
+    }
+
+    @Test
+    public void writeAndReadHeader_OnlyCompression() throws IOException
+    {
+        CommitLogDescriptor descriptor = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, compression, neverEnabledEncryption);
+        ByteBuffer buffer = ByteBuffer.allocate(16 * 1024);
+        CommitLogDescriptor.writeHeader(buffer, descriptor);
+        buffer.flip();
+        FileSegmentInputStream dataInput = new FileSegmentInputStream(buffer, null, 0);
+        CommitLogDescriptor result = CommitLogDescriptor.readHeader(dataInput, neverEnabledEncryption);
+        Assert.assertNotNull(result);
+        Assert.assertEquals(compression, result.compression);
+        Assert.assertFalse(result.getEncryptionContext().isEnabled());
+    }
+
+    @Test
+    public void writeAndReadHeader_WithEncryptionHeader_EncryptionEnabledInYaml() throws IOException
+    {
+        CommitLogDescriptor descriptor = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, enabledEncryption);
+        ByteBuffer buffer = ByteBuffer.allocate(16 * 1024);
+        CommitLogDescriptor.writeHeader(buffer, descriptor);
+        buffer.flip();
+        FileSegmentInputStream dataInput = new FileSegmentInputStream(buffer, null, 0);
+        CommitLogDescriptor result = CommitLogDescriptor.readHeader(dataInput, enabledEncryption);
+        Assert.assertNotNull(result);
+        Assert.assertNull(result.compression);
+        Assert.assertTrue(result.getEncryptionContext().isEnabled());
+        Assert.assertArrayEquals(iv, result.getEncryptionContext().getIV());
+    }
+
+    /**
+     * Check that even though enabledTdeOptions is disabled in the yaml, we can still read the commit log header as encrypted.
+     */
+    @Test
+    public void writeAndReadHeader_WithEncryptionHeader_EncryptionDisabledInYaml() throws IOException
+    {
+        CommitLogDescriptor descriptor = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, enabledEncryption);
+        ByteBuffer buffer = ByteBuffer.allocate(16 * 1024);
+        CommitLogDescriptor.writeHeader(buffer, descriptor);
+        buffer.flip();
+        FileSegmentInputStream dataInput = new FileSegmentInputStream(buffer, null, 0);
+        CommitLogDescriptor result = CommitLogDescriptor.readHeader(dataInput, previouslyEnabledEncryption);
+        Assert.assertNotNull(result);
+        Assert.assertNull(result.compression);
+        Assert.assertTrue(result.getEncryptionContext().isEnabled());
+        Assert.assertArrayEquals(iv, result.getEncryptionContext().getIV());
+    }
+
+    /**
+     * Shouldn't happen in the real world (should only have either compression or enabledTdeOptions), but the header
+     * functionality should be correct
+     */
+    @Test
+    public void writeAndReadHeader_WithCompressionAndEncryption() throws IOException
+    {
+        CommitLogDescriptor descriptor = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, compression, enabledEncryption);
+        ByteBuffer buffer = ByteBuffer.allocate(16 * 1024);
+        CommitLogDescriptor.writeHeader(buffer, descriptor);
+        buffer.flip();
+        FileSegmentInputStream dataInput = new FileSegmentInputStream(buffer, null, 0);
+        CommitLogDescriptor result = CommitLogDescriptor.readHeader(dataInput, enabledEncryption);
+        Assert.assertNotNull(result);
+        Assert.assertEquals(compression, result.compression);
+        Assert.assertTrue(result.getEncryptionContext().isEnabled());
+        Assert.assertEquals(enabledEncryption, result.getEncryptionContext());
+        Assert.assertArrayEquals(iv, result.getEncryptionContext().getIV());
+    }
+
+    @Test
+    public void equals_NoCompressionOrEncryption()
+    {
+        CommitLogDescriptor desc1 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, null);
+        Assert.assertEquals(desc1, desc1);
+
+        CommitLogDescriptor desc2 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, null);
+        Assert.assertEquals(desc1, desc2);
+
+        desc1 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, neverEnabledEncryption);
+        Assert.assertEquals(desc1, desc1);
+        desc2 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, neverEnabledEncryption);
+        Assert.assertEquals(desc1, desc2);
+
+        desc1 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, previouslyEnabledEncryption);
+        Assert.assertEquals(desc1, desc1);
+        desc2 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, previouslyEnabledEncryption);
+        Assert.assertEquals(desc1, desc2);
+    }
+
+    @Test
+    public void equals_OnlyCompression()
+    {
+        CommitLogDescriptor desc1 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, compression, null);
+        Assert.assertEquals(desc1, desc1);
+
+        CommitLogDescriptor desc2 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, compression, null);
+        Assert.assertEquals(desc1, desc2);
+
+        desc1 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, compression, neverEnabledEncryption);
+        Assert.assertEquals(desc1, desc1);
+        desc2 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, compression, neverEnabledEncryption);
+        Assert.assertEquals(desc1, desc2);
+
+        desc1 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, compression, previouslyEnabledEncryption);
+        Assert.assertEquals(desc1, desc1);
+        desc2 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, compression, previouslyEnabledEncryption);
+        Assert.assertEquals(desc1, desc2);
+    }
+
+    @Test
+    public void equals_OnlyEncryption()
+    {
+        CommitLogDescriptor desc1 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, enabledEncryption);
+        Assert.assertEquals(desc1, desc1);
+
+        CommitLogDescriptor desc2 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, enabledEncryption);
+        Assert.assertEquals(desc1, desc2);
+
+        desc1 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, neverEnabledEncryption);
+        Assert.assertEquals(desc1, desc1);
+        desc2 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, neverEnabledEncryption);
+        Assert.assertEquals(desc1, desc2);
+
+        desc1 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, previouslyEnabledEncryption);
+        Assert.assertEquals(desc1, desc1);
+        desc2 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, previouslyEnabledEncryption);
+        Assert.assertEquals(desc1, desc2);
+    }
+
+    /**
+     * Shouldn't have both enabled in real life, but ensure they are correct, nonetheless
+     */
+    @Test
+    public void equals_BothCompressionAndEncryption()
+    {
+        CommitLogDescriptor desc1 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, compression, enabledEncryption);
+        Assert.assertEquals(desc1, desc1);
+
+        CommitLogDescriptor desc2 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, compression, enabledEncryption);
+        Assert.assertEquals(desc1, desc2);
+    }
 }
diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogReaderTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogReaderTest.java
new file mode 100644
index 0000000..edff3b7
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogReaderTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.db.commitlog;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.utils.JVMStabilityInspector;
+import org.apache.cassandra.utils.KillerForTests;
+
+public class CommitLogReaderTest extends CQLTester
+{
+    @BeforeClass
+    public static void beforeClass()
+    {
+        DatabaseDescriptor.setCommitFailurePolicy(Config.CommitFailurePolicy.ignore);
+        JVMStabilityInspector.replaceKiller(new KillerForTests(false));
+    }
+
+    @Before
+    public void before() throws IOException
+    {
+        CommitLog.instance.resetUnsafe(true);
+    }
+
+    @Test
+    public void testReadAll() throws Throwable
+    {
+        int samples = 1000;
+        populateData(samples);
+        ArrayList<File> toCheck = getCommitLogs();
+
+        CommitLogReader reader = new CommitLogReader();
+
+        TestCLRHandler testHandler = new TestCLRHandler(currentTableMetadata());
+        for (File f : toCheck)
+            reader.readCommitLogSegment(testHandler, f, CommitLogReader.ALL_MUTATIONS, false);
+
+        Assert.assertEquals("Expected 1000 seen mutations, got: " + testHandler.seenMutationCount(),
+                            1000, testHandler.seenMutationCount());
+
+        confirmReadOrder(testHandler, 0);
+    }
+
+    @Test
+    public void testReadCount() throws Throwable
+    {
+        int samples = 50;
+        int readCount = 10;
+        populateData(samples);
+        ArrayList<File> toCheck = getCommitLogs();
+
+        CommitLogReader reader = new CommitLogReader();
+        TestCLRHandler testHandler = new TestCLRHandler();
+
+        for (File f : toCheck)
+            reader.readCommitLogSegment(testHandler, f, readCount - testHandler.seenMutationCount(), false);
+
+        Assert.assertEquals("Expected " + readCount + " seen mutations, got: " + testHandler.seenMutations.size(),
+                            readCount, testHandler.seenMutationCount());
+    }
+
+    @Test
+    public void testReadFromMidpoint() throws Throwable
+    {
+        int samples = 1000;
+        int readCount = 500;
+        CommitLogPosition midpoint = populateData(samples);
+        ArrayList<File> toCheck = getCommitLogs();
+
+        CommitLogReader reader = new CommitLogReader();
+        TestCLRHandler testHandler = new TestCLRHandler();
+
+        // Will skip on incorrect segments due to id mismatch on midpoint
+        for (File f : toCheck)
+            reader.readCommitLogSegment(testHandler, f, midpoint, readCount, false);
+
+        // Confirm correct count on replay
+        Assert.assertEquals("Expected " + readCount + " seen mutations, got: " + testHandler.seenMutations.size(),
+                            readCount, testHandler.seenMutationCount());
+
+        confirmReadOrder(testHandler, samples / 2);
+    }
+
+    @Test
+    public void testReadFromMidpointTooMany() throws Throwable
+    {
+        int samples = 1000;
+        int readCount = 5000;
+        CommitLogPosition midpoint = populateData(samples);
+        ArrayList<File> toCheck = getCommitLogs();
+
+        CommitLogReader reader = new CommitLogReader();
+        TestCLRHandler testHandler = new TestCLRHandler(currentTableMetadata());
+
+        // Reading from mid to overflow by 4.5k
+        // Will skip on incorrect segments due to id mismatch on midpoint
+        for (File f : toCheck)
+            reader.readCommitLogSegment(testHandler, f, midpoint, readCount, false);
+
+        Assert.assertEquals("Expected " + samples / 2 + " seen mutations, got: " + testHandler.seenMutations.size(),
+                            samples / 2, testHandler.seenMutationCount());
+
+        confirmReadOrder(testHandler, samples / 2);
+    }
+
+    @Test
+    public void testReadCountFromMidpoint() throws Throwable
+    {
+        int samples = 1000;
+        int readCount = 10;
+        CommitLogPosition midpoint = populateData(samples);
+        ArrayList<File> toCheck = getCommitLogs();
+
+        CommitLogReader reader = new CommitLogReader();
+        TestCLRHandler testHandler = new TestCLRHandler();
+
+        for (File f: toCheck)
+            reader.readCommitLogSegment(testHandler, f, midpoint, readCount, false);
+
+        // Confirm correct count on replay
+        Assert.assertEquals("Expected " + readCount + " seen mutations, got: " + testHandler.seenMutations.size(),
+            readCount, testHandler.seenMutationCount());
+
+        confirmReadOrder(testHandler, samples / 2);
+    }
+
+    /**
+     * Since we have both cfm and non mixed into the CL, we ignore updates that aren't for the cfm the test handler
+     * is configured to check.
+     * @param handler
+     * @param offset integer offset of count we expect to see in record
+     */
+    private void confirmReadOrder(TestCLRHandler handler, int offset)
+    {
+        ColumnDefinition cd = currentTableMetadata().getColumnDefinition(new ColumnIdentifier("data", false));
+        int i = 0;
+        int j = 0;
+        while (i + j < handler.seenMutationCount())
+        {
+            PartitionUpdate pu = handler.seenMutations.get(i + j).get(currentTableMetadata());
+            if (pu == null)
+            {
+                j++;
+                continue;
+            }
+
+            for (Row r : pu)
+            {
+                String expected = Integer.toString(i + offset);
+                String seen = new String(r.getCell(cd).value().array());
+                if (!expected.equals(seen))
+                    Assert.fail("Mismatch at index: " + i + ". Offset: " + offset + " Expected: " + expected + " Seen: " + seen);
+            }
+            i++;
+        }
+    }
+
+    static ArrayList<File> getCommitLogs()
+    {
+        File dir = new File(DatabaseDescriptor.getCommitLogLocation());
+        File[] files = dir.listFiles();
+        ArrayList<File> results = new ArrayList<>();
+        for (File f : files)
+        {
+            if (f.isDirectory())
+                continue;
+            results.add(f);
+        }
+        Assert.assertTrue("Didn't find any commit log files.", 0 != results.size());
+        return results;
+    }
+
+    static class TestCLRHandler implements CommitLogReadHandler
+    {
+        public List<Mutation> seenMutations = new ArrayList<Mutation>();
+        public boolean sawStopOnErrorCheck = false;
+
+        private final CFMetaData cfm;
+
+        // Accept all
+        public TestCLRHandler()
+        {
+            this.cfm = null;
+        }
+
+        public TestCLRHandler(CFMetaData cfm)
+        {
+            this.cfm = cfm;
+        }
+
+        public boolean shouldSkipSegmentOnError(CommitLogReadException exception) throws IOException
+        {
+            sawStopOnErrorCheck = true;
+            return false;
+        }
+
+        public void handleUnrecoverableError(CommitLogReadException exception) throws IOException
+        {
+            sawStopOnErrorCheck = true;
+        }
+
+        public void handleMutation(Mutation m, int size, int entryLocation, CommitLogDescriptor desc)
+        {
+            if ((cfm == null) || (cfm != null && m.get(cfm) != null)) {
+                seenMutations.add(m);
+            }
+        }
+
+        public int seenMutationCount() { return seenMutations.size(); }
+    }
+
+    /**
+     * Returns offset of active written data at halfway point of data
+     */
+    CommitLogPosition populateData(int entryCount) throws Throwable
+    {
+        Assert.assertEquals("entryCount must be an even number.", 0, entryCount % 2);
+
+        createTable("CREATE TABLE %s (idx INT, data TEXT, PRIMARY KEY(idx));");
+        int midpoint = entryCount / 2;
+
+        for (int i = 0; i < midpoint; i++) {
+            execute("INSERT INTO %s (idx, data) VALUES (?, ?)", i, Integer.toString(i));
+        }
+
+        CommitLogPosition result = CommitLog.instance.getCurrentPosition();
+
+        for (int i = midpoint; i < entryCount; i++)
+            execute("INSERT INTO %s (idx, data) VALUES (?, ?)", i, Integer.toString(i));
+
+        Keyspace.open(keyspace()).getColumnFamilyStore(currentTable()).forceBlockingFlush();
+        return result;
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentBackpressureTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentBackpressureTest.java
index c615880..0076eb6 100644
--- a/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentBackpressureTest.java
+++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentBackpressureTest.java
@@ -21,6 +21,7 @@
 package org.apache.cassandra.db.commitlog;
 
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
 import java.util.Random;
 import java.util.concurrent.Semaphore;
 
@@ -66,22 +67,24 @@
     @BMRules(rules = {@BMRule(name = "Acquire Semaphore before sync",
                               targetClass = "AbstractCommitLogService$SyncRunnable",
                               targetMethod = "sync",
-                              targetLocation = "AT INVOKE org.apache.cassandra.db.commitlog.CommitLog.sync(boolean, boolean)",
+                              targetLocation = "AT INVOKE org.apache.cassandra.db.commitlog.CommitLog.sync(boolean)",
                               action = "org.apache.cassandra.db.commitlog.CommitLogSegmentBackpressureTest.allowSync.acquire()"),
                       @BMRule(name = "Release Semaphore after sync",
                               targetClass = "AbstractCommitLogService$SyncRunnable",
                               targetMethod = "sync",
-                              targetLocation = "AFTER INVOKE org.apache.cassandra.db.commitlog.CommitLog.sync(boolean, boolean)",
+                              targetLocation = "AFTER INVOKE org.apache.cassandra.db.commitlog.CommitLog.sync(boolean)",
                               action = "org.apache.cassandra.db.commitlog.CommitLogSegmentBackpressureTest.allowSync.release()")})
     public void testCompressedCommitLogBackpressure() throws Throwable
     {
         // Perform all initialization before making CommitLog.Sync blocking
         // Doing the initialization within the method guarantee that Byteman has performed its injections when we start
         new Random().nextBytes(entropy);
+        DatabaseDescriptor.daemonInitialization();
         DatabaseDescriptor.setCommitLogCompression(new ParameterizedClass("LZ4Compressor", ImmutableMap.of()));
         DatabaseDescriptor.setCommitLogSegmentSize(1);
         DatabaseDescriptor.setCommitLogSync(CommitLogSync.periodic);
         DatabaseDescriptor.setCommitLogSyncPeriod(10 * 1000);
+        DatabaseDescriptor.setCommitLogMaxCompressionBuffersPerPool(3);
         SchemaLoader.prepareServer();
         SchemaLoader.createKeyspace(KEYSPACE1,
                                     KeyspaceParams.simple(1),
@@ -108,18 +111,20 @@
 
             dummyThread.start();
 
-            CommitLogSegmentManager clsm = CommitLog.instance.allocator;
+            AbstractCommitLogSegmentManager clsm = CommitLog.instance.segmentManager;
 
             Util.spinAssertEquals(3, () -> clsm.getActiveSegments().size(), 5);
 
             Thread.sleep(1000);
 
-            // Should only be able to create 3 segments (not 7) because it blocks waiting for truncation that never
-            // comes.
+            // Should only be able to create 3 segments not 7 because it blocks waiting for truncation that never comes
             Assert.assertEquals(3, clsm.getActiveSegments().size());
 
-            clsm.getActiveSegments().forEach(segment -> clsm.recycleSegment(segment));
+            // Discard the currently active segments so allocation can continue.
+            // Take snapshot of the list, otherwise this will also discard newly allocated segments.
+            new ArrayList<>(clsm.getActiveSegments()).forEach( clsm::archiveAndDiscard );
 
+            // The allocated count should reach the limit again.
             Util.spinAssertEquals(3, () -> clsm.getActiveSegments().size(), 5);
         }
         finally
diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDCTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDCTest.java
new file mode 100644
index 0000000..92a4bf8
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDCTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.db.commitlog;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Random;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.RowUpdateBuilder;
+import org.apache.cassandra.db.commitlog.CommitLogSegment.CDCState;
+import org.apache.cassandra.exceptions.WriteTimeoutException;
+import org.apache.cassandra.io.util.FileUtils;
+
+public class CommitLogSegmentManagerCDCTest extends CQLTester
+{
+    private static Random random = new Random();
+
+    @BeforeClass
+    public static void setUpClass()
+    {
+        DatabaseDescriptor.setCDCEnabled(true);
+        CQLTester.setUpClass();
+    }
+
+    @Before
+    public void before() throws IOException
+    {
+        CommitLog.instance.resetUnsafe(true);
+        for (File f : new File(DatabaseDescriptor.getCDCLogLocation()).listFiles())
+            FileUtils.deleteWithConfirm(f);
+    }
+
+    @Test
+    public void testCDCWriteTimeout() throws Throwable
+    {
+        createTable("CREATE TABLE %s (idx int, data text, primary key(idx)) WITH cdc=true;");
+        CommitLogSegmentManagerCDC cdcMgr = (CommitLogSegmentManagerCDC)CommitLog.instance.segmentManager;
+        CFMetaData cfm = currentTableMetadata();
+
+        // Confirm that logic to check for whether or not we can allocate new CDC segments works
+        Integer originalCDCSize = DatabaseDescriptor.getCDCSpaceInMB();
+        try
+        {
+            DatabaseDescriptor.setCDCSpaceInMB(32);
+            // Spin until we hit CDC capacity and make sure we get a WriteTimeout
+            try
+            {
+                // Should trigger on anything < 20:1 compression ratio during compressed test
+                for (int i = 0; i < 100; i++)
+                {
+                    new RowUpdateBuilder(cfm, 0, i)
+                        .add("data", randomizeBuffer(DatabaseDescriptor.getCommitLogSegmentSize() / 3))
+                        .build().apply();
+                }
+                Assert.fail("Expected WriteTimeoutException from full CDC but did not receive it.");
+            }
+            catch (WriteTimeoutException e)
+            {
+                // expected, do nothing
+            }
+            expectCurrentCDCState(CDCState.FORBIDDEN);
+
+            // Confirm we can create a non-cdc table and write to it even while at cdc capacity
+            createTable("CREATE TABLE %s (idx int, data text, primary key(idx)) WITH cdc=false;");
+            execute("INSERT INTO %s (idx, data) VALUES (1, '1');");
+
+            // Confirm that, on flush+recyle, we see files show up in cdc_raw
+            Keyspace.open(keyspace()).getColumnFamilyStore(currentTable()).forceBlockingFlush();
+            CommitLog.instance.forceRecycleAllSegments();
+            cdcMgr.awaitManagementTasksCompletion();
+            Assert.assertTrue("Expected files to be moved to overflow.", getCDCRawCount() > 0);
+
+            // Simulate a CDC consumer reading files then deleting them
+            for (File f : new File(DatabaseDescriptor.getCDCLogLocation()).listFiles())
+                FileUtils.deleteWithConfirm(f);
+
+            // Update size tracker to reflect deleted files. Should flip flag on current allocatingFrom to allow.
+            cdcMgr.updateCDCTotalSize();
+            expectCurrentCDCState(CDCState.PERMITTED);
+        }
+        finally
+        {
+            DatabaseDescriptor.setCDCSpaceInMB(originalCDCSize);
+        }
+    }
+
+    @Test
+    public void testCLSMCDCDiscardLogic() throws Throwable
+    {
+        CommitLogSegmentManagerCDC cdcMgr = (CommitLogSegmentManagerCDC)CommitLog.instance.segmentManager;
+
+        createTable("CREATE TABLE %s (idx int, data text, primary key(idx)) WITH cdc=false;");
+        for (int i = 0; i < 8; i++)
+        {
+            new RowUpdateBuilder(currentTableMetadata(), 0, i)
+                .add("data", randomizeBuffer(DatabaseDescriptor.getCommitLogSegmentSize() / 4)) // fit 3 in a segment
+                .build().apply();
+        }
+
+        // Should have 4 segments CDC since we haven't flushed yet, 3 PERMITTED, one of which is active, and 1 PERMITTED, in waiting
+        Assert.assertEquals(4 * DatabaseDescriptor.getCommitLogSegmentSize(), cdcMgr.updateCDCTotalSize());
+        expectCurrentCDCState(CDCState.PERMITTED);
+        CommitLog.instance.forceRecycleAllSegments();
+
+        // on flush, these PERMITTED should be deleted
+        Assert.assertEquals(0, new File(DatabaseDescriptor.getCDCLogLocation()).listFiles().length);
+
+        createTable("CREATE TABLE %s (idx int, data text, primary key(idx)) WITH cdc=true;");
+        for (int i = 0; i < 8; i++)
+        {
+            new RowUpdateBuilder(currentTableMetadata(), 0, i)
+                .add("data", randomizeBuffer(DatabaseDescriptor.getCommitLogSegmentSize() / 4))
+                .build().apply();
+        }
+        // 4 total again, 3 CONTAINS, 1 in waiting PERMITTED
+        Assert.assertEquals(4 * DatabaseDescriptor.getCommitLogSegmentSize(), cdcMgr.updateCDCTotalSize());
+        CommitLog.instance.forceRecycleAllSegments();
+        expectCurrentCDCState(CDCState.PERMITTED);
+
+        // On flush, PERMITTED is deleted, CONTAINS is preserved.
+        cdcMgr.awaitManagementTasksCompletion();
+        int seen = getCDCRawCount();
+        Assert.assertTrue("Expected >3 files in cdc_raw, saw: " + seen, seen >= 3);
+    }
+
+    @Test
+    public void testSegmentFlaggingOnCreation() throws Throwable
+    {
+        CommitLogSegmentManagerCDC cdcMgr = (CommitLogSegmentManagerCDC)CommitLog.instance.segmentManager;
+        String ct = createTable("CREATE TABLE %s (idx int, data text, primary key(idx)) WITH cdc=true;");
+
+        int origSize = DatabaseDescriptor.getCDCSpaceInMB();
+        try
+        {
+            DatabaseDescriptor.setCDCSpaceInMB(16);
+            CFMetaData ccfm = Keyspace.open(keyspace()).getColumnFamilyStore(ct).metadata;
+            // Spin until we hit CDC capacity and make sure we get a WriteTimeout
+            try
+            {
+                for (int i = 0; i < 1000; i++)
+                {
+                    new RowUpdateBuilder(ccfm, 0, i)
+                        .add("data", randomizeBuffer(DatabaseDescriptor.getCommitLogSegmentSize() / 3))
+                        .build().apply();
+                }
+                Assert.fail("Expected WriteTimeoutException from full CDC but did not receive it.");
+            }
+            catch (WriteTimeoutException e) { }
+
+            expectCurrentCDCState(CDCState.FORBIDDEN);
+            CommitLog.instance.forceRecycleAllSegments();
+
+            cdcMgr.awaitManagementTasksCompletion();
+            new File(DatabaseDescriptor.getCDCLogLocation()).listFiles()[0].delete();
+            cdcMgr.updateCDCTotalSize();
+            // Confirm cdc update process changes flag on active segment
+            expectCurrentCDCState(CDCState.PERMITTED);
+
+            // Clear out archived CDC files
+            for (File f : new File(DatabaseDescriptor.getCDCLogLocation()).listFiles()) {
+                FileUtils.deleteWithConfirm(f);
+            }
+
+            // Set space to 0, confirm newly allocated segments are FORBIDDEN
+            DatabaseDescriptor.setCDCSpaceInMB(0);
+            CommitLog.instance.forceRecycleAllSegments();
+            CommitLog.instance.segmentManager.awaitManagementTasksCompletion();
+            expectCurrentCDCState(CDCState.FORBIDDEN);
+        }
+        finally
+        {
+            DatabaseDescriptor.setCDCSpaceInMB(origSize);
+        }
+    }
+
+    private ByteBuffer randomizeBuffer(int size)
+    {
+        byte[] toWrap = new byte[size];
+        random.nextBytes(toWrap);
+        return ByteBuffer.wrap(toWrap);
+    }
+
+    private int getCDCRawCount()
+    {
+        return new File(DatabaseDescriptor.getCDCLogLocation()).listFiles().length;
+    }
+
+    private void expectCurrentCDCState(CDCState state)
+    {
+        Assert.assertEquals("Received unexpected CDCState on current allocatingFrom segment.",
+            state, CommitLog.instance.segmentManager.allocatingFrom().getCDCState());
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogTest.java
index 3a0e892..3e82274 100644
--- a/test/unit/org/apache/cassandra/db/commitlog/CommitLogTest.java
+++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogTest.java
@@ -20,21 +20,18 @@
 
 import java.io.*;
 import java.nio.ByteBuffer;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.UUID;
+import java.util.*;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
 import java.util.zip.CRC32;
 import java.util.zip.Checksum;
 
+import com.google.common.collect.Iterables;
+
+import org.junit.*;
 import com.google.common.io.Files;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
@@ -50,6 +47,8 @@
 import org.apache.cassandra.db.compaction.CompactionManager;
 import org.apache.cassandra.db.marshal.AsciiType;
 import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.Row;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.FSWriteError;
 import org.apache.cassandra.io.compress.DeflateCompressor;
@@ -58,13 +57,17 @@
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.security.EncryptionContextGenerator;
 import org.apache.cassandra.service.StorageService;
-import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.Hex;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.KillerForTests;
+import org.apache.cassandra.utils.Pair;
 import org.apache.cassandra.utils.vint.VIntCoding;
 
+import org.junit.After;
 import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -78,29 +81,28 @@
     private static final String STANDARD2 = "Standard2";
     private static final String CUSTOM1 = "Custom1";
 
-    public CommitLogTest(ParameterizedClass commitLogCompression)
+    private static JVMStabilityInspector.Killer oldKiller;
+    private static KillerForTests testKiller;
+
+    public CommitLogTest(ParameterizedClass commitLogCompression, EncryptionContext encryptionContext)
     {
         DatabaseDescriptor.setCommitLogCompression(commitLogCompression);
-    }
-
-    @Before
-    public void setUp() throws IOException
-    {
-        CommitLog.instance.resetUnsafe(true);
+        DatabaseDescriptor.setEncryptionContext(encryptionContext);
     }
 
     @Parameters()
     public static Collection<Object[]> generateData()
     {
-        return Arrays.asList(new Object[][] {
-                { null }, // No compression
-                { new ParameterizedClass(LZ4Compressor.class.getName(), Collections.emptyMap()) },
-                { new ParameterizedClass(SnappyCompressor.class.getName(), Collections.emptyMap()) },
-                { new ParameterizedClass(DeflateCompressor.class.getName(), Collections.emptyMap()) } });
+        return Arrays.asList(new Object[][]{
+            {null, EncryptionContextGenerator.createDisabledContext()}, // No compression, no encryption
+            {null, EncryptionContextGenerator.createContext(true)}, // Encryption
+            {new ParameterizedClass(LZ4Compressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()},
+            {new ParameterizedClass(SnappyCompressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()},
+            {new ParameterizedClass(DeflateCompressor.class.getName(), Collections.emptyMap()), EncryptionContextGenerator.createDisabledContext()}});
     }
 
     @BeforeClass
-    public static void defineSchema() throws ConfigurationException
+    public static void beforeClass() throws ConfigurationException
     {
         // Disable durable writes for system keyspaces to prevent system mutations, e.g. sstable_activity,
         // to end up in CL segments and cause unexpected results in this test wrt counting CL segments,
@@ -128,26 +130,50 @@
                                     SchemaLoader.standardCFMD(KEYSPACE1, STANDARD1, 0, AsciiType.instance, BytesType.instance),
                                     SchemaLoader.standardCFMD(KEYSPACE1, STANDARD2, 0, AsciiType.instance, BytesType.instance));
         CompactionManager.instance.disableAutoCompaction();
+
+        testKiller = new KillerForTests();
+
+        // While we don't want the JVM to be nuked from under us on a test failure, we DO want some indication of
+        // an error. If we hit a "Kill the JVM" condition while working with the CL when we don't expect it, an aggressive
+        // KillerForTests will assertion out on us.
+        oldKiller = JVMStabilityInspector.replaceKiller(testKiller);
+    }
+
+    @AfterClass
+    public static void afterClass()
+    {
+        JVMStabilityInspector.replaceKiller(oldKiller);
+    }
+
+    @Before
+    public void beforeTest() throws IOException
+    {
+        CommitLog.instance.resetUnsafe(true);
+    }
+
+    @After
+    public void afterTest()
+    {
+        testKiller.reset();
     }
 
     @Test
     public void testRecoveryWithEmptyLog() throws Exception
     {
-        // The first empty file we expect to throw as it's invalid
-        // We need to pass the second as well, because allowTruncation will be set to true for the final segment
         runExpecting(() -> {
-            CommitLog.instance.recover(new File[]{
-                    tmpFile(CommitLogDescriptor.current_version),
-                    tmpFile(CommitLogDescriptor.current_version)  });
+            CommitLog.instance.recoverFiles(new File[]{
+            tmpFile(CommitLogDescriptor.current_version),
+            tmpFile(CommitLogDescriptor.current_version)
+            });
             return null;
         }, CommitLogReplayException.class);
     }
 
     @Test
-    public void testRecoveryWithEmptyFinalLog() throws Exception
+    public void testRecoveryWithFinalEmptyLog() throws Exception
     {
         // Even though it's empty, it's the last commitlog segment, so allowTruncation=true should allow it to pass
-        CommitLog.instance.recover(new File[]{ tmpFile(CommitLogDescriptor.current_version)  });
+        CommitLog.instance.recoverFiles(new File[]{tmpFile(CommitLogDescriptor.current_version)});
     }
 
     /**
@@ -160,8 +186,8 @@
     {
         File directory = Files.createTempDir();
 
-        CommitLogDescriptor desc1 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null);
-        CommitLogDescriptor desc2 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 2, null);
+        CommitLogDescriptor desc1 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 1, null, DatabaseDescriptor.getEncryptionContext());
+        CommitLogDescriptor desc2 = new CommitLogDescriptor(CommitLogDescriptor.current_version, 2, null, DatabaseDescriptor.getEncryptionContext());
 
         ByteBuffer buffer;
 
@@ -191,13 +217,13 @@
 
         // one corrupt file and one header only file should be ok
         runExpecting(() -> {
-            CommitLog.instance.recover(file1, file2);
+            CommitLog.instance.recoverFiles(file1, file2);
             return null;
         }, null);
 
         // 2 corrupt files and one header only file should fail
         runExpecting(() -> {
-            CommitLog.instance.recover(file1, file1, file2);
+            CommitLog.instance.recoverFiles(file1, file1, file2);
             return null;
         }, CommitLogReplayException.class);
     }
@@ -205,7 +231,7 @@
     @Test
     public void testRecoveryWithEmptyLog20() throws Exception
     {
-        CommitLog.instance.recover(new File[]{ tmpFile(CommitLogDescriptor.VERSION_20) });
+        CommitLog.instance.recoverFiles(tmpFile(CommitLogDescriptor.VERSION_20));
     }
 
     @Test
@@ -224,12 +250,12 @@
     @Test
     public void testRecoveryWithShortPadding() throws Exception
     {
-        // If we have 0-3 bytes remaining, commitlog replayer
-        // should pass, because there's insufficient room
-        // left in the segment for the legacy size marker.
-        testRecovery(new byte[1], null);
-        testRecovery(new byte[2], null);
-        testRecovery(new byte[3], null);
+            // If we have 0-3 bytes remaining, commitlog replayer
+            // should pass, because there's insufficient room
+            // left in the segment for the legacy size marker.
+            testRecovery(new byte[1], null);
+            testRecovery(new byte[2], null);
+            testRecovery(new byte[3], null);
     }
 
     @Test
@@ -244,14 +270,6 @@
     }
 
     @Test
-    public void testRecoveryWithShortCheckSum() throws Exception
-    {
-        byte[] data = new byte[8];
-        data[3] = 10;   // make sure this is not a legacy end marker.
-        testRecovery(data, CommitLogReplayException.class);
-    }
-
-    @Test
     public void testRecoveryWithShortMutationSize() throws Exception
     {
         testRecoveryWithBadSizeArgument(9, 10);
@@ -302,13 +320,14 @@
     @Test
     public void testDontDeleteIfDirty() throws Exception
     {
-        ColumnFamilyStore cfs1 = Keyspace.open(KEYSPACE1).getColumnFamilyStore(STANDARD1);
-        ColumnFamilyStore cfs2 = Keyspace.open(KEYSPACE1).getColumnFamilyStore(STANDARD2);
+        Keyspace ks = Keyspace.open(KEYSPACE1);
+        ColumnFamilyStore cfs1 = ks.getColumnFamilyStore(STANDARD1);
+        ColumnFamilyStore cfs2 = ks.getColumnFamilyStore(STANDARD2);
 
         // Roughly 32 MB mutation
         Mutation m = new RowUpdateBuilder(cfs1.metadata, 0, "k")
                      .clustering("bytes")
-                     .add("val", ByteBuffer.allocate(DatabaseDescriptor.getCommitLogSegmentSize()/4))
+                     .add("val", ByteBuffer.allocate(DatabaseDescriptor.getCommitLogSegmentSize() / 4))
                      .build();
 
         // Adding it 5 times
@@ -325,39 +344,40 @@
                       .build();
         CommitLog.instance.add(m2);
 
-        assert CommitLog.instance.activeSegments() == 2 : "Expecting 2 segments, got " + CommitLog.instance.activeSegments();
+        assertEquals(2, CommitLog.instance.segmentManager.getActiveSegments().size());
 
         UUID cfid2 = m2.getColumnFamilyIds().iterator().next();
-        CommitLog.instance.discardCompletedSegments(cfid2, ReplayPosition.NONE, CommitLog.instance.getContext());
+        CommitLog.instance.discardCompletedSegments(cfid2, CommitLogPosition.NONE, CommitLog.instance.getCurrentPosition());
 
-        // Assert we still have both our segment
-        assert CommitLog.instance.activeSegments() == 2 : "Expecting 2 segments, got " + CommitLog.instance.activeSegments();
+        // Assert we still have both our segments
+        assertEquals(2, CommitLog.instance.segmentManager.getActiveSegments().size());
     }
 
     @Test
     public void testDeleteIfNotDirty() throws Exception
     {
-        ColumnFamilyStore cfs1 = Keyspace.open(KEYSPACE1).getColumnFamilyStore(STANDARD1);
-        ColumnFamilyStore cfs2 = Keyspace.open(KEYSPACE1).getColumnFamilyStore(STANDARD2);
+        Keyspace ks = Keyspace.open(KEYSPACE1);
+        ColumnFamilyStore cfs1 = ks.getColumnFamilyStore(STANDARD1);
+        ColumnFamilyStore cfs2 = ks.getColumnFamilyStore(STANDARD2);
 
         // Roughly 32 MB mutation
-        Mutation rm = new RowUpdateBuilder(cfs1.metadata, 0, "k")
-                      .clustering("bytes")
-                      .add("val", ByteBuffer.allocate((DatabaseDescriptor.getCommitLogSegmentSize()/4) - 1))
-                      .build();
+         Mutation rm = new RowUpdateBuilder(cfs1.metadata, 0, "k")
+                  .clustering("bytes")
+                  .add("val", ByteBuffer.allocate((DatabaseDescriptor.getCommitLogSegmentSize()/4) - 1))
+                  .build();
 
         // Adding it twice (won't change segment)
         CommitLog.instance.add(rm);
         CommitLog.instance.add(rm);
 
-        assert CommitLog.instance.activeSegments() == 1 : "Expecting 1 segment, got " + CommitLog.instance.activeSegments();
+        assertEquals(1, CommitLog.instance.segmentManager.getActiveSegments().size());
 
         // "Flush": this won't delete anything
         UUID cfid1 = rm.getColumnFamilyIds().iterator().next();
-        CommitLog.instance.sync(true, true);
-        CommitLog.instance.discardCompletedSegments(cfid1, ReplayPosition.NONE, CommitLog.instance.getContext());
+        CommitLog.instance.sync(true);
+        CommitLog.instance.discardCompletedSegments(cfid1, CommitLogPosition.NONE, CommitLog.instance.getCurrentPosition());
 
-        assert CommitLog.instance.activeSegments() == 1 : "Expecting 1 segment, got " + CommitLog.instance.activeSegments();
+        assertEquals(1, CommitLog.instance.segmentManager.getActiveSegments().size());
 
         // Adding new mutation on another CF, large enough (including CL entry overhead) that a new segment is created
         Mutation rm2 = new RowUpdateBuilder(cfs2.metadata, 0, "k")
@@ -369,17 +389,35 @@
         CommitLog.instance.add(rm2);
         CommitLog.instance.add(rm2);
 
-        assert CommitLog.instance.activeSegments() == 3 : "Expecting 3 segments, got " + CommitLog.instance.activeSegments();
+        Collection<CommitLogSegment> segments = CommitLog.instance.segmentManager.getActiveSegments();
 
+        assertEquals(String.format("Expected 3 segments but got %d (%s)", segments.size(), getDirtyCFIds(segments)),
+                     3,
+                     segments.size());
 
         // "Flush" second cf: The first segment should be deleted since we
         // didn't write anything on cf1 since last flush (and we flush cf2)
 
         UUID cfid2 = rm2.getColumnFamilyIds().iterator().next();
-        CommitLog.instance.discardCompletedSegments(cfid2, ReplayPosition.NONE, CommitLog.instance.getContext());
+        CommitLog.instance.discardCompletedSegments(cfid2, CommitLogPosition.NONE, CommitLog.instance.getCurrentPosition());
+
+        segments = CommitLog.instance.segmentManager.getActiveSegments();
 
         // Assert we still have both our segment
-        assert CommitLog.instance.activeSegments() == 1 : "Expecting 1 segment, got " + CommitLog.instance.activeSegments();
+        assertEquals(String.format("Expected 1 segment but got %d (%s)", segments.size(), getDirtyCFIds(segments)),
+                     1,
+                     segments.size());
+    }
+
+    private String getDirtyCFIds(Collection<CommitLogSegment> segments)
+    {
+        return "Dirty cfIds: <"
+               + String.join(", ", segments.stream()
+                                           .map(CommitLogSegment::getDirtyCFIDs)
+                                           .flatMap(uuids -> uuids.stream())
+                                           .distinct()
+                                           .map(uuid -> uuid.toString()).collect(Collectors.toList()))
+               + ">";
     }
 
     private static int getMaxRecordDataSize(String keyspace, ByteBuffer key, String cfName, String colName)
@@ -420,28 +458,20 @@
                       .clustering("bytes")
                       .add("val", ByteBuffer.allocate(getMaxRecordDataSize()))
                       .build();
-
         CommitLog.instance.add(rm);
     }
 
-    @Test
+    @Test(expected = IllegalArgumentException.class)
     public void testExceedRecordLimit() throws Exception
     {
-        CommitLog.instance.resetUnsafe(true);
-        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(STANDARD1);
-        try
-        {
-            Mutation rm = new RowUpdateBuilder(cfs.metadata, 0, "k")
-                          .clustering("bytes")
-                          .add("val", ByteBuffer.allocate(1 + getMaxRecordDataSize()))
-                          .build();
-            CommitLog.instance.add(rm);
-            throw new AssertionError("mutation larger than limit was accepted");
-        }
-        catch (IllegalArgumentException e)
-        {
-            // IAE is thrown on too-large mutations
-        }
+        Keyspace ks = Keyspace.open(KEYSPACE1);
+        ColumnFamilyStore cfs = ks.getColumnFamilyStore(STANDARD1);
+        Mutation rm = new RowUpdateBuilder(cfs.metadata, 0, "k")
+                      .clustering("bytes")
+                      .add("val", ByteBuffer.allocate(1 + getMaxRecordDataSize()))
+                      .build();
+        CommitLog.instance.add(rm);
+        throw new AssertionError("mutation larger than limit was accepted");
     }
 
     protected void testRecoveryWithBadSizeArgument(int size, int dataSize) throws Exception
@@ -462,10 +492,50 @@
         testRecovery(out.toByteArray(), CommitLogReplayException.class);
     }
 
+    /**
+     * Create a temporary commit log file with an appropriate descriptor at the head.
+     *
+     * @return the commit log file reference and the first position after the descriptor in the file
+     * (so that subsequent writes happen at the correct file location).
+     */
+    protected Pair<File, Integer> tmpFile() throws IOException
+    {
+        EncryptionContext encryptionContext = DatabaseDescriptor.getEncryptionContext();
+        CommitLogDescriptor desc = new CommitLogDescriptor(CommitLogDescriptor.current_version,
+                                                           CommitLogSegment.getNextId(),
+                                                           DatabaseDescriptor.getCommitLogCompression(),
+                                                           encryptionContext);
+
+
+        ByteBuffer buf = ByteBuffer.allocate(1024);
+        CommitLogDescriptor.writeHeader(buf, desc, getAdditionalHeaders(encryptionContext));
+        buf.flip();
+        int positionAfterHeader = buf.limit() + 1;
+
+        File logFile = new File(DatabaseDescriptor.getCommitLogLocation(), desc.fileName());
+
+        try (OutputStream lout = new FileOutputStream(logFile))
+        {
+            lout.write(buf.array(), 0, buf.limit());
+        }
+
+        return Pair.create(logFile, positionAfterHeader);
+    }
+
+    private Map<String, String> getAdditionalHeaders(EncryptionContext encryptionContext)
+    {
+        if (!encryptionContext.isEnabled())
+            return Collections.emptyMap();
+
+        // if we're testing encryption, we need to write out a cipher IV to the descriptor headers
+        byte[] buf = new byte[16];
+        new Random().nextBytes(buf);
+        return Collections.singletonMap(EncryptionContext.ENCRYPTION_IV, Hex.bytesToHex(buf));
+    }
+
     protected File tmpFile(int version) throws IOException
     {
         File logFile = File.createTempFile("CommitLog-" + version + "-", ".log");
-        logFile.deleteOnExit();
         assert logFile.length() == 0;
         return logFile;
     }
@@ -487,9 +557,9 @@
         File logFile = tmpFile(desc.version);
         CommitLogDescriptor fromFile = CommitLogDescriptor.fromFileName(logFile.getName());
         // Change id to match file.
-        desc = new CommitLogDescriptor(desc.version, fromFile.id, desc.compression);
+        desc = new CommitLogDescriptor(desc.version, fromFile.id, desc.compression, desc.getEncryptionContext());
         ByteBuffer buf = ByteBuffer.allocate(1024);
-        CommitLogDescriptor.writeHeader(buf, desc);
+        CommitLogDescriptor.writeHeader(buf, desc, getAdditionalHeaders(desc.getEncryptionContext()));
         try (OutputStream lout = new FileOutputStream(logFile))
         {
             lout.write(buf.array(), 0, buf.position());
@@ -503,7 +573,7 @@
     @Test
     public void testRecoveryWithIdMismatch() throws Exception
     {
-        CommitLogDescriptor desc = new CommitLogDescriptor(4, null);
+        CommitLogDescriptor desc = new CommitLogDescriptor(4, null, EncryptionContextGenerator.createDisabledContext());
         File logFile = tmpFile(desc.version);
         ByteBuffer buf = ByteBuffer.allocate(1024);
         CommitLogDescriptor.writeHeader(buf, desc);
@@ -521,7 +591,7 @@
     @Test
     public void testRecoveryWithBadCompressor() throws Exception
     {
-        CommitLogDescriptor desc = new CommitLogDescriptor(4, new ParameterizedClass("UnknownCompressor", null));
+        CommitLogDescriptor desc = new CommitLogDescriptor(4, new ParameterizedClass("UnknownCompressor", null), EncryptionContextGenerator.createDisabledContext());
         runExpecting(() -> {
             testRecovery(desc, new byte[0]);
             return null;
@@ -530,12 +600,6 @@
 
     protected void runExpecting(Callable<Void> r, Class<?> expected)
     {
-        JVMStabilityInspector.Killer originalKiller;
-        KillerForTests killerForTests;
-
-        killerForTests = new KillerForTests();
-        originalKiller = JVMStabilityInspector.replaceKiller(killerForTests);
-
         Throwable caught = null;
         try
         {
@@ -550,14 +614,15 @@
         if (expected != null && caught == null)
             Assert.fail("Expected exception " + expected + " but call completed successfully.");
 
-        JVMStabilityInspector.replaceKiller(originalKiller);
-        assertEquals("JVM killed", expected != null, killerForTests.wasKilled());
+        assertEquals("JVM kill state doesn't match expectation.", expected != null, testKiller.wasKilled());
     }
 
     protected void testRecovery(final byte[] logData, Class<?> expected) throws Exception
     {
+        ParameterizedClass commitLogCompression = DatabaseDescriptor.getCommitLogCompression();
+        EncryptionContext encryptionContext = DatabaseDescriptor.getEncryptionContext();
         runExpecting(() -> testRecovery(logData, CommitLogDescriptor.VERSION_20), expected);
-        runExpecting(() -> testRecovery(new CommitLogDescriptor(4, null), logData), expected);
+        runExpecting(() -> testRecovery(new CommitLogDescriptor(4, commitLogCompression, encryptionContext), logData), expected);
     }
 
     @Test
@@ -566,11 +631,11 @@
         boolean originalState = DatabaseDescriptor.isAutoSnapshot();
         try
         {
-            CommitLog.instance.resetUnsafe(true);
             boolean prev = DatabaseDescriptor.isAutoSnapshot();
             DatabaseDescriptor.setAutoSnapshot(false);
-            ColumnFamilyStore cfs1 = Keyspace.open(KEYSPACE1).getColumnFamilyStore(STANDARD1);
-            ColumnFamilyStore cfs2 = Keyspace.open(KEYSPACE1).getColumnFamilyStore(STANDARD2);
+            Keyspace ks = Keyspace.open(KEYSPACE1);
+            ColumnFamilyStore cfs1 = ks.getColumnFamilyStore(STANDARD1);
+            ColumnFamilyStore cfs2 = ks.getColumnFamilyStore(STANDARD2);
 
             new RowUpdateBuilder(cfs1.metadata, 0, "k").clustering("bytes").add("val", ByteBuffer.allocate(100)).build().applyUnsafe();
             cfs1.truncateBlocking();
@@ -583,13 +648,13 @@
             for (int i = 0 ; i < 5 ; i++)
                 CommitLog.instance.add(m2);
 
-            assertEquals(2, CommitLog.instance.activeSegments());
-            ReplayPosition position = CommitLog.instance.getContext();
-            for (Keyspace ks : Keyspace.system())
-                for (ColumnFamilyStore syscfs : ks.getColumnFamilyStores())
-                    CommitLog.instance.discardCompletedSegments(syscfs.metadata.cfId, ReplayPosition.NONE, position);
-            CommitLog.instance.discardCompletedSegments(cfs2.metadata.cfId, ReplayPosition.NONE, position);
-            assertEquals(1, CommitLog.instance.activeSegments());
+            assertEquals(2, CommitLog.instance.segmentManager.getActiveSegments().size());
+            CommitLogPosition position = CommitLog.instance.getCurrentPosition();
+            for (Keyspace keyspace : Keyspace.system())
+                for (ColumnFamilyStore syscfs : keyspace.getColumnFamilyStores())
+                    CommitLog.instance.discardCompletedSegments(syscfs.metadata.cfId, CommitLogPosition.NONE, position);
+            CommitLog.instance.discardCompletedSegments(cfs2.metadata.cfId, CommitLogPosition.NONE, position);
+            assertEquals(1, CommitLog.instance.segmentManager.getActiveSegments().size());
         }
         finally
         {
@@ -609,12 +674,12 @@
 
             ColumnFamilyStore cfs = notDurableKs.getColumnFamilyStore("Standard1");
             new RowUpdateBuilder(cfs.metadata, 0, "key1")
-                .clustering("bytes").add("val", ByteBufferUtil.bytes("abcd"))
-                .build()
-                .applyUnsafe();
+            .clustering("bytes").add("val", bytes("abcd"))
+            .build()
+            .applyUnsafe();
 
             assertTrue(Util.getOnlyRow(Util.cmd(cfs).columns("val").build())
-                            .cells().iterator().next().value().equals(ByteBufferUtil.bytes("abcd")));
+                           .cells().iterator().next().value().equals(bytes("abcd")));
 
             cfs.truncateBlocking();
 
@@ -627,6 +692,114 @@
     }
 
     @Test
+    public void replaySimple() throws IOException
+    {
+        int cellCount = 0;
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(STANDARD1);
+        final Mutation rm1 = new RowUpdateBuilder(cfs.metadata, 0, "k1")
+                             .clustering("bytes")
+                             .add("val", bytes("this is a string"))
+                             .build();
+        cellCount += 1;
+        CommitLog.instance.add(rm1);
+
+        final Mutation rm2 = new RowUpdateBuilder(cfs.metadata, 0, "k2")
+                             .clustering("bytes")
+                             .add("val", bytes("this is a string"))
+                             .build();
+        cellCount += 1;
+        CommitLog.instance.add(rm2);
+
+        CommitLog.instance.sync(true);
+
+        SimpleCountingReplayer replayer = new SimpleCountingReplayer(CommitLog.instance, CommitLogPosition.NONE, cfs.metadata);
+        List<String> activeSegments = CommitLog.instance.getActiveSegmentNames();
+        Assert.assertFalse(activeSegments.isEmpty());
+
+        File[] files = new File(CommitLog.instance.segmentManager.storageDirectory).listFiles((file, name) -> activeSegments.contains(name));
+        replayer.replayFiles(files);
+
+        assertEquals(cellCount, replayer.cells);
+    }
+
+    @Test
+    public void replayWithDiscard() throws IOException
+    {
+        int cellCount = 0;
+        int max = 1024;
+        int discardPosition = (int)(max * .8); // an arbitrary number of entries that we'll skip on the replay
+        CommitLogPosition commitLogPosition = null;
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(STANDARD1);
+
+        for (int i = 0; i < max; i++)
+        {
+            final Mutation rm1 = new RowUpdateBuilder(cfs.metadata, 0, "k" + 1)
+                                 .clustering("bytes")
+                                 .add("val", bytes("this is a string"))
+                                 .build();
+            CommitLogPosition position = CommitLog.instance.add(rm1);
+
+            if (i == discardPosition)
+                commitLogPosition = position;
+            if (i > discardPosition)
+            {
+                cellCount += 1;
+            }
+        }
+
+        CommitLog.instance.sync(true);
+
+        SimpleCountingReplayer replayer = new SimpleCountingReplayer(CommitLog.instance, commitLogPosition, cfs.metadata);
+        List<String> activeSegments = CommitLog.instance.getActiveSegmentNames();
+        Assert.assertFalse(activeSegments.isEmpty());
+
+        File[] files = new File(CommitLog.instance.segmentManager.storageDirectory).listFiles((file, name) -> activeSegments.contains(name));
+        replayer.replayFiles(files);
+
+        assertEquals(cellCount, replayer.cells);
+    }
+
+    class SimpleCountingReplayer extends CommitLogReplayer
+    {
+        private final CommitLogPosition filterPosition;
+        private final CFMetaData metadata;
+        int cells;
+        int skipped;
+
+        SimpleCountingReplayer(CommitLog commitLog, CommitLogPosition filterPosition, CFMetaData cfm)
+        {
+            super(commitLog, filterPosition, Collections.emptyMap(), ReplayFilter.create());
+            this.filterPosition = filterPosition;
+            this.metadata = cfm;
+        }
+
+        @SuppressWarnings("resource")
+        @Override
+        public void handleMutation(Mutation m, int size, int entryLocation, CommitLogDescriptor desc)
+        {
+            // Filter out system writes that could flake the test.
+            if (!KEYSPACE1.equals(m.getKeyspaceName()))
+                return;
+
+            if (entryLocation <= filterPosition.position)
+            {
+                // Skip over this mutation.
+                skipped++;
+                return;
+            }
+            for (PartitionUpdate partitionUpdate : m.getPartitionUpdates())
+            {
+                // Only process mutations for the CF's we're testing against, since we can't deterministically predict
+                // whether or not system keyspaces will be mutated during a test.
+                if (partitionUpdate.metadata().cfName.equals(metadata.cfName))
+                {
+                    for (Row row : partitionUpdate)
+                        cells += Iterables.size(row.cells());
+                }
+            }
+        }
+    }
+
     public void testUnwriteableFlushRecovery() throws ExecutionException, InterruptedException, IOException
     {
         CommitLog.instance.resetUnsafe(true);
@@ -667,7 +840,7 @@
             DatabaseDescriptor.setDiskFailurePolicy(oldPolicy);
         }
 
-        CommitLog.instance.sync(true, true);
+        CommitLog.instance.sync(true);
         System.setProperty("cassandra.replayList", KEYSPACE1 + "." + STANDARD1);
         // Currently we don't attempt to re-flush a memtable that failed, thus make sure data is replayed by commitlog.
         // If retries work subsequent flushes should clear up error and this should change to expect 0.
@@ -701,7 +874,7 @@
         for (SSTableReader reader : cfs.getLiveSSTables())
             reader.reloadSSTableMetadata();
 
-        CommitLog.instance.sync(true, true);
+        CommitLog.instance.sync(true);
         System.setProperty("cassandra.replayList", KEYSPACE1 + "." + STANDARD1);
         // In the absence of error, this should be 0 because forceBlockingFlush/forceRecycleAllSegments would have
         // persisted all data in the commit log. Because we know there was an error, there must be something left to
diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogTestReplayer.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogTestReplayer.java
index 36973f2..9a22b04 100644
--- a/test/unit/org/apache/cassandra/db/commitlog/CommitLogTestReplayer.java
+++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogTestReplayer.java
@@ -22,13 +22,12 @@
 import java.io.IOException;
 
 import com.google.common.base.Predicate;
-
 import org.junit.Assert;
+
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.Mutation;
 import org.apache.cassandra.db.rows.SerializationHelper;
 import org.apache.cassandra.io.util.DataInputBuffer;
-import org.apache.cassandra.io.util.NIODataInputStream;
 import org.apache.cassandra.io.util.RebufferingInputStream;
 
 /**
@@ -36,44 +35,44 @@
  */
 public class CommitLogTestReplayer extends CommitLogReplayer
 {
-    public static void examineCommitLog(Predicate<Mutation> processor) throws IOException
+    private final Predicate<Mutation> processor;
+
+    public CommitLogTestReplayer(Predicate<Mutation> processor) throws IOException
     {
-        CommitLog.instance.sync(true, true);
+        super(CommitLog.instance, CommitLogPosition.NONE, null, ReplayFilter.create());
+        CommitLog.instance.sync(true);
 
-        CommitLogTestReplayer replayer = new CommitLogTestReplayer(CommitLog.instance, processor);
-        File commitLogDir = new File(DatabaseDescriptor.getCommitLogLocation());
-        replayer.recover(commitLogDir.listFiles());
-    }
-
-    final private Predicate<Mutation> processor;
-
-    public CommitLogTestReplayer(CommitLog log, Predicate<Mutation> processor)
-    {
-        this(log, ReplayPosition.NONE, processor);
-    }
-
-    public CommitLogTestReplayer(CommitLog log, ReplayPosition discardedPos, Predicate<Mutation> processor)
-    {
-        super(log, discardedPos, null, ReplayFilter.create());
         this.processor = processor;
+        commitLogReader = new CommitLogTestReader();
     }
 
-    @Override
-    void replayMutation(byte[] inputBuffer, int size, final int entryLocation, final CommitLogDescriptor desc)
+    public void examineCommitLog() throws IOException
     {
-        RebufferingInputStream bufIn = new DataInputBuffer(inputBuffer, 0, size);
-        Mutation mutation;
-        try
+        replayFiles(new File(DatabaseDescriptor.getCommitLogLocation()).listFiles());
+    }
+
+    private class CommitLogTestReader extends CommitLogReader
+    {
+        @Override
+        protected void readMutation(CommitLogReadHandler handler,
+                                    byte[] inputBuffer,
+                                    int size,
+                                    CommitLogPosition minPosition,
+                                    final int entryLocation,
+                                    final CommitLogDescriptor desc) throws IOException
         {
-            mutation = Mutation.serializer.deserialize(bufIn,
-                                                           desc.getMessagingVersion(),
-                                                           SerializationHelper.Flag.LOCAL);
-            Assert.assertTrue(processor.apply(mutation));
-        }
-        catch (IOException e)
-        {
-            // Test fails.
-            throw new AssertionError(e);
+            RebufferingInputStream bufIn = new DataInputBuffer(inputBuffer, 0, size);
+            Mutation mutation;
+            try
+            {
+                mutation = Mutation.serializer.deserialize(bufIn, desc.getMessagingVersion(), SerializationHelper.Flag.LOCAL);
+                Assert.assertTrue(processor.apply(mutation));
+            }
+            catch (IOException e)
+            {
+                // Test fails.
+                throw new AssertionError(e);
+            }
         }
     }
 }
diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogUpgradeTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogUpgradeTest.java
index 00a143b..d55b59f 100644
--- a/test/unit/org/apache/cassandra/db/commitlog/CommitLogUpgradeTest.java
+++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogUpgradeTest.java
@@ -37,6 +37,7 @@
 
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.Mutation;
 import org.apache.cassandra.db.rows.Cell;
@@ -45,12 +46,22 @@
 import org.apache.cassandra.db.marshal.BytesType;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.security.EncryptionContextGenerator;
 import org.apache.cassandra.utils.JVMStabilityInspector;
 import org.apache.cassandra.utils.KillerForTests;
 import org.apache.cassandra.db.commitlog.CommitLogReplayer.CommitLogReplayException;
 
+/**
+ * Note: if you are looking to create new test cases for this test, check out
+ * {@link CommitLogUpgradeTestMaker}
+ */
 public class CommitLogUpgradeTest
 {
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     static final String DATA_DIR = "test/data/legacy-commitlog/";
     static final String PROPERTIES_FILE = "hash.txt";
     static final String CFID_PROPERTY = "cfid";
@@ -65,6 +76,13 @@
     private KillerForTests killerForTests;
     private boolean shouldBeKilled = false;
 
+    static CFMetaData metadata = CFMetaData.Builder.createDense(KEYSPACE, TABLE, false, false)
+                                                   .addPartitionKey("key", AsciiType.instance)
+                                                   .addClusteringColumn("col", AsciiType.instance)
+                                                   .addRegularColumn("val", BytesType.instance)
+                                                   .build()
+                                                   .compression(SchemaLoader.getCompressionParameters());
+
     @Before
     public void prepareToBeKilled()
     {
@@ -92,7 +110,6 @@
     }
 
     @Test
-
     public void test22() throws Exception
     {
         testRestore(DATA_DIR + "2.2");
@@ -125,10 +142,13 @@
     @Test
     public void test22_bitrot_ignored() throws Exception
     {
-        try {
+        try
+        {
             System.setProperty(CommitLogReplayer.IGNORE_REPLAY_ERRORS_PROPERTY, "true");
             testRestore(DATA_DIR + "2.2-lz4-bitrot");
-        } finally {
+        }
+        finally
+        {
             System.clearProperty(CommitLogReplayer.IGNORE_REPLAY_ERRORS_PROPERTY);
         }
     }
@@ -143,27 +163,31 @@
     @Test
     public void test22_bitrot2_ignored() throws Exception
     {
-        try {
+        try
+        {
             System.setProperty(CommitLogReplayer.IGNORE_REPLAY_ERRORS_PROPERTY, "true");
             testRestore(DATA_DIR + "2.2-lz4-bitrot2");
-        } finally {
+        }
+        finally
+        {
             System.clearProperty(CommitLogReplayer.IGNORE_REPLAY_ERRORS_PROPERTY);
         }
     }
 
-    @BeforeClass
-    static public void initialize() throws FileNotFoundException, IOException, InterruptedException
+    @Test
+    public void test34_encrypted() throws Exception
     {
-        CFMetaData metadata = CFMetaData.Builder.createDense(KEYSPACE, TABLE, false, false)
-                                                .addPartitionKey("key", AsciiType.instance)
-                                                .addClusteringColumn("col", AsciiType.instance)
-                                                .addRegularColumn("val", BytesType.instance)
-                                                .build()
-                                                .compression(SchemaLoader.getCompressionParameters());
+        testRestore(DATA_DIR + "3.4-encrypted");
+    }
+
+    @BeforeClass
+    public static void initialize()
+    {
         SchemaLoader.loadSchema();
         SchemaLoader.createKeyspace(KEYSPACE,
                                     KeyspaceParams.simple(1),
                                     metadata);
+        DatabaseDescriptor.setEncryptionContext(EncryptionContextGenerator.createContext(true));
     }
 
     public void testRestore(String location) throws IOException, InterruptedException
@@ -186,9 +210,9 @@
         }
 
         Hasher hasher = new Hasher();
-        CommitLogTestReplayer replayer = new CommitLogTestReplayer(CommitLog.instance, hasher);
+        CommitLogTestReplayer replayer = new CommitLogTestReplayer(hasher);
         File[] files = new File(location).listFiles((file, name) -> name.endsWith(".log"));
-        replayer.recover(files);
+        replayer.replayFiles(files);
 
         Assert.assertEquals(cells, hasher.cells);
         Assert.assertEquals(hash, hasher.hash);
diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogUpgradeTestMaker.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogUpgradeTestMaker.java
index 3538bd1..5a03f9f 100644
--- a/test/unit/org/apache/cassandra/db/commitlog/CommitLogUpgradeTestMaker.java
+++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogUpgradeTestMaker.java
@@ -42,6 +42,7 @@
 import org.apache.cassandra.db.Mutation;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.schema.KeyspaceParams;
 import org.apache.cassandra.utils.FBUtilities;
 
 import static org.apache.cassandra.db.commitlog.CommitLogUpgradeTest.*;
@@ -91,17 +92,20 @@
         }
 
         SchemaLoader.loadSchema();
-        SchemaLoader.schemaDefinition("");
+        SchemaLoader.createKeyspace(KEYSPACE,
+                                    KeyspaceParams.simple(1),
+                                    metadata);
     }
 
     public void makeLog() throws IOException, InterruptedException
     {
         CommitLog commitLog = CommitLog.instance;
-        System.out.format("\nUsing commit log size %dmb, compressor %s, sync %s%s\n",
+        System.out.format("\nUsing commit log size: %dmb, compressor: %s, encryption: %s, sync: %s, %s\n",
                           mb(DatabaseDescriptor.getCommitLogSegmentSize()),
                           commitLog.configuration.getCompressorName(),
+                          commitLog.configuration.useEncryption(),
                           commitLog.executor.getClass().getSimpleName(),
-                          randomSize ? " random size" : "");
+                          randomSize ? "random size" : "");
         final List<CommitlogExecutor> threads = new ArrayList<>();
         ScheduledExecutorService scheduled = startThreads(commitLog, threads);
 
@@ -215,7 +219,7 @@
         int dataSize = 0;
         final CommitLog commitLog;
 
-        volatile ReplayPosition rp;
+        volatile CommitLogPosition clsp;
 
         public CommitlogExecutor(CommitLog commitLog)
         {
@@ -230,7 +234,6 @@
             {
                 if (rl != null)
                     rl.acquire();
-                String ks = KEYSPACE;
                 ByteBuffer key = randomBytes(16, tlr);
 
                 UpdateBuilder builder = UpdateBuilder.create(Schema.instance.getCFMetaData(KEYSPACE, TABLE), Util.dk(key));
@@ -245,7 +248,7 @@
                     dataSize += sz;
                 }
 
-                rp = commitLog.add((Mutation)builder.makeMutation());
+                clsp = commitLog.add((Mutation)builder.makeMutation());
                 counter.incrementAndGet();
             }
         }
diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitlogShutdownTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitlogShutdownTest.java
index ee3f111..91a3f02 100644
--- a/test/unit/org/apache/cassandra/db/commitlog/CommitlogShutdownTest.java
+++ b/test/unit/org/apache/cassandra/db/commitlog/CommitlogShutdownTest.java
@@ -62,6 +62,7 @@
     public void testShutdownWithPendingTasks() throws Exception
     {
         new Random().nextBytes(entropy);
+        DatabaseDescriptor.daemonInitialization();
         DatabaseDescriptor.setCommitLogCompression(new ParameterizedClass("LZ4Compressor", ImmutableMap.of()));
         DatabaseDescriptor.setCommitLogSegmentSize(1);
         DatabaseDescriptor.setCommitLogSync(Config.CommitLogSync.periodic);
@@ -89,10 +90,10 @@
 
         // schedule discarding completed segments and immediately issue a shutdown
         UUID cfid = m.getColumnFamilyIds().iterator().next();
-        CommitLog.instance.discardCompletedSegments(cfid, ReplayPosition.NONE, CommitLog.instance.getContext());
+        CommitLog.instance.discardCompletedSegments(cfid, CommitLogPosition.NONE, CommitLog.instance.getCurrentPosition());
         CommitLog.instance.shutdownBlocking();
 
         // the shutdown should block until all logs except the currently active one and perhaps a new, empty one are gone
-        Assert.assertTrue(new File(CommitLog.instance.location).listFiles().length <= 2);
+        Assert.assertTrue(new File(DatabaseDescriptor.getCommitLogLocation()).listFiles().length <= 2);
     }
 }
diff --git a/test/unit/org/apache/cassandra/db/commitlog/SegmentReaderTest.java b/test/unit/org/apache/cassandra/db/commitlog/SegmentReaderTest.java
new file mode 100644
index 0000000..d9d5e4c
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/commitlog/SegmentReaderTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.db.commitlog;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.Collections;
+import java.util.Random;
+import java.util.function.BiFunction;
+
+import javax.crypto.Cipher;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.commitlog.CommitLogSegmentReader.CompressedSegmenter;
+import org.apache.cassandra.db.commitlog.CommitLogSegmentReader.EncryptedSegmenter;
+import org.apache.cassandra.db.commitlog.CommitLogSegmentReader.SyncSegment;
+import org.apache.cassandra.io.compress.DeflateCompressor;
+import org.apache.cassandra.io.compress.ICompressor;
+import org.apache.cassandra.io.compress.LZ4Compressor;
+import org.apache.cassandra.io.compress.SnappyCompressor;
+import org.apache.cassandra.io.util.FileDataInput;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.security.CipherFactory;
+import org.apache.cassandra.security.EncryptionUtils;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.security.EncryptionContextGenerator;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+public class SegmentReaderTest
+{
+    static final Random random = new Random();
+
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
+    @Test
+    public void compressedSegmenter_LZ4() throws IOException
+    {
+        compressedSegmenter(LZ4Compressor.create(Collections.emptyMap()));
+    }
+
+    @Test
+    public void compressedSegmenter_Snappy() throws IOException
+    {
+        compressedSegmenter(SnappyCompressor.create(null));
+    }
+
+    @Test
+    public void compressedSegmenter_Deflate() throws IOException
+    {
+        compressedSegmenter(DeflateCompressor.create(null));
+    }
+
+    private void compressedSegmenter(ICompressor compressor) throws IOException
+    {
+        int rawSize = (1 << 15) - 137;
+        ByteBuffer plainTextBuffer = compressor.preferredBufferType().allocate(rawSize);
+        byte[] b = new byte[rawSize];
+        random.nextBytes(b);
+        plainTextBuffer.put(b);
+        plainTextBuffer.flip();
+
+        int uncompressedHeaderSize = 4;  // need to add in the plain text size to the block we write out
+        int length = compressor.initialCompressedBufferLength(rawSize);
+        ByteBuffer compBuffer = ByteBufferUtil.ensureCapacity(null, length + uncompressedHeaderSize, true, compressor.preferredBufferType());
+        compBuffer.putInt(rawSize);
+        compressor.compress(plainTextBuffer, compBuffer);
+        compBuffer.flip();
+
+        File compressedFile = File.createTempFile("compressed-segment-", ".log");
+        compressedFile.deleteOnExit();
+        FileOutputStream fos = new FileOutputStream(compressedFile);
+        fos.getChannel().write(compBuffer);
+        fos.close();
+
+        try (RandomAccessReader reader = RandomAccessReader.open(compressedFile))
+        {
+            CompressedSegmenter segmenter = new CompressedSegmenter(compressor, reader);
+            int fileLength = (int) compressedFile.length();
+            SyncSegment syncSegment = segmenter.nextSegment(0, fileLength);
+            FileDataInput fileDataInput = syncSegment.input;
+            ByteBuffer fileBuffer = readBytes(fileDataInput, rawSize);
+
+            plainTextBuffer.flip();
+            Assert.assertEquals(plainTextBuffer, fileBuffer);
+
+            // CompressedSegmenter includes the Sync header length in the syncSegment.endPosition (value)
+            Assert.assertEquals(rawSize, syncSegment.endPosition - CommitLogSegment.SYNC_MARKER_SIZE);
+        }
+    }
+
+    private ByteBuffer readBytes(FileDataInput input, int len)
+    {
+        byte[] buf = new byte[len];
+        try
+        {
+            input.readFully(buf);
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+        return ByteBuffer.wrap(buf);
+    }
+
+    private ByteBuffer readBytesSeek(FileDataInput input, int len)
+    {
+        byte[] buf = new byte[len];
+
+        /// divide output buffer into 5
+        int[] offsets = new int[] { 0, len / 5, 2 * len / 5, 3 * len / 5, 4 * len / 5, len };
+        
+        //seek offset
+        long inputStart = input.getFilePointer();
+
+        for (int i = 0; i < offsets.length - 1; i++)
+        {
+            try
+            {
+                // seek to beginning of offet
+                input.seek(inputStart + offsets[i]);
+                //read this segment
+                input.readFully(buf, offsets[i], offsets[i + 1] - offsets[i]);
+            }
+            catch (IOException e)
+            {
+                throw new RuntimeException(e);
+            }
+        }
+        return ByteBuffer.wrap(buf);
+    }
+
+    @Test
+    public void encryptedSegmenterRead() throws IOException
+    {
+        underlyingEncryptedSegmenterTest((s, t) -> readBytes(s, t));
+    }
+
+    @Test
+    public void encryptedSegmenterSeek() throws IOException
+    {
+        underlyingEncryptedSegmenterTest((s, t) -> readBytesSeek(s, t));
+    }
+
+    public void underlyingEncryptedSegmenterTest(BiFunction<FileDataInput, Integer, ByteBuffer> readFun)
+            throws IOException
+    {
+        EncryptionContext context = EncryptionContextGenerator.createContext(true);
+        CipherFactory cipherFactory = new CipherFactory(context.getTransparentDataEncryptionOptions());
+
+        int plainTextLength = (1 << 13) - 137;
+        ByteBuffer plainTextBuffer = ByteBuffer.allocate(plainTextLength);
+        random.nextBytes(plainTextBuffer.array());
+
+        ByteBuffer compressedBuffer = EncryptionUtils.compress(plainTextBuffer, null, true, context.getCompressor());
+        Cipher cipher = cipherFactory.getEncryptor(context.getTransparentDataEncryptionOptions().cipher, context.getTransparentDataEncryptionOptions().key_alias);
+        File encryptedFile = File.createTempFile("encrypted-segment-", ".log");
+        encryptedFile.deleteOnExit();
+        FileChannel channel = new RandomAccessFile(encryptedFile, "rw").getChannel();
+        channel.write(ByteBufferUtil.bytes(plainTextLength));
+        EncryptionUtils.encryptAndWrite(compressedBuffer, channel, true, cipher);
+        channel.close();
+
+        try (RandomAccessReader reader = RandomAccessReader.open(encryptedFile))
+        {
+            context = EncryptionContextGenerator.createContext(cipher.getIV(), true);
+            EncryptedSegmenter segmenter = new EncryptedSegmenter(reader, context);
+            SyncSegment syncSegment = segmenter.nextSegment(0, (int) reader.length());
+
+            // EncryptedSegmenter includes the Sync header length in the syncSegment.endPosition (value)
+            Assert.assertEquals(plainTextLength, syncSegment.endPosition - CommitLogSegment.SYNC_MARKER_SIZE);
+            ByteBuffer fileBuffer = readFun.apply(syncSegment.input, plainTextLength);
+            plainTextBuffer.position(0);
+            Assert.assertEquals(plainTextBuffer, fileBuffer);
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/commitlog/SnapshotDeletingTest.java b/test/unit/org/apache/cassandra/db/commitlog/SnapshotDeletingTest.java
index 37f1731..413e716 100644
--- a/test/unit/org/apache/cassandra/db/commitlog/SnapshotDeletingTest.java
+++ b/test/unit/org/apache/cassandra/db/commitlog/SnapshotDeletingTest.java
@@ -26,12 +26,12 @@
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.RowUpdateBuilder;
 import org.apache.cassandra.db.WindowsFailedSnapshotTracker;
-import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.sstable.SnapshotDeletingTask;
 import org.apache.cassandra.schema.KeyspaceParams;
 import org.apache.cassandra.service.GCInspector;
@@ -46,6 +46,7 @@
     @BeforeClass
     public static void defineSchema() throws Exception
     {
+        DatabaseDescriptor.daemonInitialization();
         GCInspector.register();
         // Needed to init the output file where we print failed snapshots. This is called on node startup.
         WindowsFailedSnapshotTracker.deleteOldSnapshots();
@@ -58,7 +59,7 @@
     @Test
     public void testCompactionHook() throws Exception
     {
-        Assume.assumeTrue(FBUtilities.isWindows());
+        Assume.assumeTrue(FBUtilities.isWindows);
 
         Keyspace keyspace = Keyspace.open(KEYSPACE1);
         ColumnFamilyStore store = keyspace.getColumnFamilyStore(CF_STANDARD1);
diff --git a/test/unit/org/apache/cassandra/db/compaction/AbstractCompactionStrategyTest.java b/test/unit/org/apache/cassandra/db/compaction/AbstractCompactionStrategyTest.java
new file mode 100644
index 0000000..481b394
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/compaction/AbstractCompactionStrategyTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.db.compaction;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import junit.framework.Assert;
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.Util;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.RowUpdateBuilder;
+import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.schema.CompactionParams;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.utils.FBUtilities;
+
+public class AbstractCompactionStrategyTest
+{
+    private static final String KEYSPACE1 = "Keyspace1";
+    private static final String LCS_TABLE = "LCS_TABLE";
+    private static final String STCS_TABLE = "STCS_TABLE";
+    private static final String DTCS_TABLE = "DTCS_TABLE";
+    private static final String TWCS_TABLE = "TWCS_TABLE";
+
+    @BeforeClass
+    public static void loadData() throws ConfigurationException
+    {
+        Map<String, String> stcsOptions = new HashMap<>();
+        stcsOptions.put("tombstone_compaction_interval", "1");
+
+        SchemaLoader.prepareServer();
+        SchemaLoader.createKeyspace(KEYSPACE1,
+                                    KeyspaceParams.simple(1),
+                                    SchemaLoader.standardCFMD(KEYSPACE1, LCS_TABLE)
+                                                .compaction(CompactionParams.lcs(Collections.emptyMap())),
+                                    SchemaLoader.standardCFMD(KEYSPACE1, STCS_TABLE)
+                                                .compaction(CompactionParams.scts(Collections.emptyMap())),
+                                    SchemaLoader.standardCFMD(KEYSPACE1, DTCS_TABLE)
+                                                .compaction(CompactionParams.create(DateTieredCompactionStrategy.class, Collections.emptyMap())),
+                                    SchemaLoader.standardCFMD(KEYSPACE1, TWCS_TABLE)
+                                                .compaction(CompactionParams.create(TimeWindowCompactionStrategy.class, Collections.emptyMap())));
+        Keyspace.open(KEYSPACE1).getColumnFamilyStore(LCS_TABLE).disableAutoCompaction();
+        Keyspace.open(KEYSPACE1).getColumnFamilyStore(STCS_TABLE).disableAutoCompaction();
+        Keyspace.open(KEYSPACE1).getColumnFamilyStore(DTCS_TABLE).disableAutoCompaction();
+        Keyspace.open(KEYSPACE1).getColumnFamilyStore(TWCS_TABLE).disableAutoCompaction();
+    }
+
+    @After
+    public void tearDown()
+    {
+
+        Keyspace.open(KEYSPACE1).getColumnFamilyStore(LCS_TABLE).truncateBlocking();
+        Keyspace.open(KEYSPACE1).getColumnFamilyStore(STCS_TABLE).truncateBlocking();
+        Keyspace.open(KEYSPACE1).getColumnFamilyStore(DTCS_TABLE).truncateBlocking();
+        Keyspace.open(KEYSPACE1).getColumnFamilyStore(TWCS_TABLE).truncateBlocking();
+    }
+
+    @Test(timeout=30000)
+    public void testGetNextBackgroundTaskDoesNotBlockLCS()
+    {
+        testGetNextBackgroundTaskDoesNotBlock(LCS_TABLE);
+    }
+
+    @Test(timeout=30000)
+    public void testGetNextBackgroundTaskDoesNotBlockSTCS()
+    {
+        testGetNextBackgroundTaskDoesNotBlock(STCS_TABLE);
+    }
+
+    @Test(timeout=30000)
+    public void testGetNextBackgroundTaskDoesNotBlockDTCS()
+    {
+        testGetNextBackgroundTaskDoesNotBlock(DTCS_TABLE);
+    }
+
+    @Test(timeout=30000)
+    public void testGetNextBackgroundTaskDoesNotBlockTWCS()
+    {
+        testGetNextBackgroundTaskDoesNotBlock(TWCS_TABLE);
+    }
+
+    public void testGetNextBackgroundTaskDoesNotBlock(String table)
+    {
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(table);
+        AbstractCompactionStrategy strategy = cfs.getCompactionStrategyManager().getStrategies().get(1).get(0);
+
+        // Add 4 sstables
+        for (int i = 1; i <= 4; i++)
+        {
+            insertKeyAndFlush(table, i);
+        }
+
+        // Check they are returned on the next background task
+        try (LifecycleTransaction txn = strategy.getNextBackgroundTask(FBUtilities.nowInSeconds()).transaction)
+        {
+            Assert.assertEquals(cfs.getLiveSSTables(), txn.originals());
+        }
+
+        // now remove sstables on the tracker, to simulate a concurrent transaction
+        cfs.getTracker().removeUnsafe(cfs.getLiveSSTables());
+
+        // verify the compaction strategy will return null
+        Assert.assertNull(strategy.getNextBackgroundTask(FBUtilities.nowInSeconds()));
+    }
+
+
+    private static void insertKeyAndFlush(String table, int key)
+    {
+        long timestamp = System.currentTimeMillis();
+        DecoratedKey dk = Util.dk(String.format("%03d", key));
+        ColumnFamilyStore cfs = Keyspace.open(KEYSPACE1).getColumnFamilyStore(table);
+        new RowUpdateBuilder(cfs.metadata, timestamp, dk.getKey())
+        .clustering(String.valueOf(key))
+        .add("val", "val")
+        .build()
+        .applyUnsafe();
+        cfs.forceBlockingFlush();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/compaction/CancelCompactionsTest.java b/test/unit/org/apache/cassandra/db/compaction/CancelCompactionsTest.java
index 68ba6bf..bcbe92d 100644
--- a/test/unit/org/apache/cassandra/db/compaction/CancelCompactionsTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/CancelCompactionsTest.java
@@ -19,6 +19,7 @@
 package org.apache.cassandra.db.compaction;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 
 import com.google.common.util.concurrent.Uninterruptibles;
@@ -27,6 +28,7 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.metrics.CompactionMetrics;
+
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
@@ -45,9 +47,14 @@
         }
         AbstractCompactionTask ct = null;
 
-        for (AbstractCompactionStrategy cs : getCurrentColumnFamilyStore().getCompactionStrategyManager().getStrategies())
+        for (List<AbstractCompactionStrategy> css : getCurrentColumnFamilyStore().getCompactionStrategyManager().getStrategies())
         {
-            ct = cs.getNextBackgroundTask(0);
+            for (AbstractCompactionStrategy cs : css)
+            {
+                ct = cs.getNextBackgroundTask(0);
+                if (ct != null)
+                    break;
+            }
             if (ct != null)
                 break;
         }
diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionControllerTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionControllerTest.java
index 1b400e8..052206e 100644
--- a/test/unit/org/apache/cassandra/db/compaction/CompactionControllerTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/CompactionControllerTest.java
@@ -177,6 +177,11 @@
         expired = CompactionController.getFullyExpiredSSTables(cfs, compacting, overlapping, gcBefore);
         assertNotNull(expired);
         assertEquals(0, expired.size());
+
+        // Now if we explicitly ask to ignore overlaped sstables, we should get back our expired sstable
+        expired = CompactionController.getFullyExpiredSSTables(cfs, compacting, overlapping, gcBefore, true);
+        assertNotNull(expired);
+        assertEquals(1, expired.size());
     }
 
     private void applyMutation(CFMetaData cfm, DecoratedKey key, long timestamp)
diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionExecutorTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionExecutorTest.java
index b1f29b3..918d58c 100644
--- a/test/unit/org/apache/cassandra/db/compaction/CompactionExecutorTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/CompactionExecutorTest.java
@@ -24,6 +24,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.apache.cassandra.concurrent.DebuggableThreadPoolExecutor;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.utils.concurrent.SimpleCondition;
 
 import static org.junit.Assert.assertEquals;
@@ -58,6 +59,7 @@
     @Before
     public void setup()
     {
+        DatabaseDescriptor.daemonInitialization();
         executor = new TestTaskExecutor();
         testTaskThrowable = null;
         afterExecuteCompleted = new SimpleCondition();
diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionIteratorTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionIteratorTest.java
index 549a94d..d39a2d9 100644
--- a/test/unit/org/apache/cassandra/db/compaction/CompactionIteratorTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/CompactionIteratorTest.java
@@ -18,28 +18,391 @@
 package org.apache.cassandra.db.compaction;
 
 import static org.apache.cassandra.db.transform.DuplicateRowCheckerTest.assertCommandIssued;
-import static org.apache.cassandra.db.transform.DuplicateRowCheckerTest.iter;
 import static org.apache.cassandra.db.transform.DuplicateRowCheckerTest.makeRow;
+import static org.apache.cassandra.db.transform.DuplicateRowCheckerTest.rows;
 import static org.junit.Assert.*;
 
 import java.net.InetAddress;
 import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.google.common.collect.*;
 
 import org.junit.Test;
 
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.CQLTester;
-import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.DeletionTime;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.partitions.AbstractUnfilteredPartitionIterator;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.io.sstable.ISSTableScanner;
-import org.apache.cassandra.io.sstable.format.SSTableReader;
-import org.apache.cassandra.net.*;
+import org.apache.cassandra.net.IMessageSink;
+import org.apache.cassandra.net.MessageIn;
+import org.apache.cassandra.net.MessageOut;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
 
 public class CompactionIteratorTest extends CQLTester
 {
+
+    private static final int NOW = 1000;
+    private static final int GC_BEFORE = 100;
+    private static final String KSNAME = "CompactionIteratorTest";
+    private static final String CFNAME = "Integer1";
+
+    static final DecoratedKey kk;
+    static final CFMetaData metadata;
+    private static final int RANGE = 1000;
+    private static final int COUNT = 100;
+
+    Map<List<Unfiltered>, DeletionTime> deletionTimes = new HashMap<>();
+
+    static {
+        DatabaseDescriptor.daemonInitialization();
+
+        kk = Util.dk("key");
+
+        SchemaLoader.prepareServer();
+        SchemaLoader.createKeyspace(KSNAME,
+                                    KeyspaceParams.simple(1),
+                                    metadata = SchemaLoader.standardCFMD(KSNAME,
+                                                                         CFNAME,
+                                                                         1,
+                                                                         UTF8Type.instance,
+                                                                         Int32Type.instance,
+                                                                         Int32Type.instance));
+    }
+
+    // See org.apache.cassandra.db.rows.UnfilteredRowsGenerator.parse for the syntax used in these tests.
+
+    @Test
+    public void testGcCompactionSupersedeLeft()
+    {
+        testCompaction(new String[] {
+            "5<=[140] 10[150] [140]<20 22<[130] [130]<25 30[150]"
+        }, new String[] {
+            "7<[160] 15[180] [160]<30 40[120]"
+        },
+        3);
+    }
+
+    @Test
+    public void testGcCompactionSupersedeMiddle()
+    {
+        testCompaction(new String[] {
+            "5<=[140] 10[150] [140]<40 60[150]"
+        }, new String[] {
+            "7<=[160] 15[180] [160]<=30 40[120]"
+        },
+        3);
+    }
+
+    @Test
+    public void testGcCompactionSupersedeRight()
+    {
+        testCompaction(new String[] {
+            "9<=[140] 10[150] [140]<40 60[150]"
+        }, new String[] {
+            "7<[160] 15[180] [160]<30 40[120]"
+        },
+        3);
+    }
+
+    @Test
+    public void testGcCompactionSwitchInSuperseded()
+    {
+        testCompaction(new String[] {
+            "5<=[140] 10[150] [140]<20 20<=[170] [170]<=50 60[150]"
+        }, new String[] {
+            "7<[160] 15[180] [160]<30 40[120]"
+        },
+        5);
+    }
+
+    @Test
+    public void testGcCompactionBoundaries()
+    {
+        testCompaction(new String[] {
+            "5<=[120] [120]<9 9<=[140] 10[150] [140]<40 40<=[120] 60[150] [120]<90"
+        }, new String[] {
+            "7<[160] 15[180] [160]<30 40[120] 45<[140] [140]<80 88<=[130] [130]<100"
+        },
+        7);
+    }
+
+    @Test
+    public void testGcCompactionMatches()
+    {
+        testCompaction(new String[] {
+            "5<=[120] [120]<=9 9<[140] 10[150] [140]<40 40<=[120] 60[150] [120]<90 120<=[100] [100]<130"
+        }, new String[] {
+            "9<[160] 15[180] [160]<40 40[120] 45<[140] [140]<90 90<=[110] [110]<100 120<=[100] [100]<130"
+        },
+        5);
+    }
+
+    @Test
+    public void testGcCompactionRowDeletion()
+    {
+        testCompaction(new String[] {
+            "10[150] 20[160] 25[160] 30[170] 40[120] 50[120]"
+        }, new String[] {
+            "10<=[155] 20[200D180] 30[200D160] [155]<=30 40[150D130] 50[150D100]"
+        },
+        "25[160] 30[170] 50[120]");
+    }
+
+    @Test
+    public void testGcCompactionPartitionDeletion()
+    {
+        testCompaction(new String[] {
+            "10[150] 20[160] 25[160] 30[170] 40[120] 50[120]"
+        }, new String[] {
+            // Dxx| stands for partition deletion at time xx
+            "D165|10<=[155] 20[200D180] 30[200D160] [155]<=30 40[150D130] 50[150D100]"
+        },
+        "30[170]");
+    }
+
+    void testCompaction(String[] inputs, String[] tombstones, String expected)
+    {
+        testNonGcCompaction(inputs, tombstones);
+
+        UnfilteredRowsGenerator generator = new UnfilteredRowsGenerator(metadata.comparator, false);
+        List<List<Unfiltered>> inputLists = parse(inputs, generator);
+        List<List<Unfiltered>> tombstoneLists = parse(tombstones, generator);
+        List<Unfiltered> result = compact(inputLists, tombstoneLists);
+        System.out.println("GC compaction resulted in " + size(result) + " Unfiltereds");
+        generator.verifyValid(result);
+        verifyEquivalent(inputLists, result, tombstoneLists, generator);
+        List<Unfiltered> expectedResult = generator.parse(expected, NOW - 1);
+        if (!expectedResult.equals(result))
+            fail("Expected " + expected + ", got " + generator.str(result));
+    }
+
+    void testCompaction(String[] inputs, String[] tombstones, int expectedCount)
+    {
+        testNonGcCompaction(inputs, tombstones);
+
+        UnfilteredRowsGenerator generator = new UnfilteredRowsGenerator(metadata.comparator, false);
+        List<List<Unfiltered>> inputLists = parse(inputs, generator);
+        List<List<Unfiltered>> tombstoneLists = parse(tombstones, generator);
+        List<Unfiltered> result = compact(inputLists, tombstoneLists);
+        System.out.println("GC compaction resulted in " + size(result) + " Unfiltereds");
+        generator.verifyValid(result);
+        verifyEquivalent(inputLists, result, tombstoneLists, generator);
+        if (size(result) > expectedCount)
+            fail("Expected compaction with " + expectedCount + " elements, got " + size(result) + ": " + generator.str(result));
+    }
+
+    int testNonGcCompaction(String[] inputs, String[] tombstones)
+    {
+        UnfilteredRowsGenerator generator = new UnfilteredRowsGenerator(metadata.comparator, false);
+        List<List<Unfiltered>> inputLists = parse(inputs, generator);
+        List<List<Unfiltered>> tombstoneLists = parse(tombstones, generator);
+        List<Unfiltered> result = compact(inputLists, Collections.emptyList());
+        System.out.println("Non-GC compaction resulted in " + size(result) + " Unfiltereds");
+        generator.verifyValid(result);
+        verifyEquivalent(inputLists, result, tombstoneLists, generator);
+        return size(result);
+    }
+
+    private static int size(List<Unfiltered> data)
+    {
+        return data.stream().mapToInt(x -> x instanceof RangeTombstoneBoundaryMarker ? 2 : 1).sum();
+    }
+
+    private void verifyEquivalent(List<List<Unfiltered>> sources, List<Unfiltered> result, List<List<Unfiltered>> tombstoneSources, UnfilteredRowsGenerator generator)
+    {
+        // sources + tombstoneSources must be the same as result + tombstoneSources
+        List<Unfiltered> expected = compact(Iterables.concat(sources, tombstoneSources), Collections.emptyList());
+        List<Unfiltered> actual = compact(Iterables.concat(ImmutableList.of(result), tombstoneSources), Collections.emptyList());
+        if (!expected.equals(actual))
+        {
+            System.out.println("Equivalence test failure between sources:");
+            for (List<Unfiltered> partition : sources)
+                generator.dumpList(partition);
+            System.out.println("and compacted " + generator.str(result));
+            System.out.println("with tombstone sources:");
+            for (List<Unfiltered> partition : tombstoneSources)
+                generator.dumpList(partition);
+            System.out.println("expected " + generator.str(expected));
+            System.out.println("got " + generator.str(actual));
+            fail("Failed equivalence test.");
+        }
+    }
+
+    private List<List<Unfiltered>> parse(String[] inputs, UnfilteredRowsGenerator generator)
+    {
+        return ImmutableList.copyOf(Lists.transform(Arrays.asList(inputs), x -> parse(x, generator)));
+    }
+
+    private List<Unfiltered> parse(String input, UnfilteredRowsGenerator generator)
+    {
+        Matcher m = Pattern.compile("D(\\d+)\\|").matcher(input);
+        if (m.lookingAt())
+        {
+            int del = Integer.parseInt(m.group(1));
+            input = input.substring(m.end());
+            List<Unfiltered> list = generator.parse(input, NOW - 1);
+            deletionTimes.put(list, new DeletionTime(del, del));
+            return list;
+        }
+        else
+            return generator.parse(input, NOW - 1);
+    }
+
+    private List<Unfiltered> compact(Iterable<List<Unfiltered>> sources, Iterable<List<Unfiltered>> tombstoneSources)
+    {
+        List<Iterable<UnfilteredRowIterator>> content = ImmutableList.copyOf(Iterables.transform(sources, list -> ImmutableList.of(listToIterator(list, kk))));
+        Map<DecoratedKey, Iterable<UnfilteredRowIterator>> transformedSources = new TreeMap<>();
+        transformedSources.put(kk, Iterables.transform(tombstoneSources, list -> listToIterator(list, kk)));
+        try (CompactionController controller = new Controller(Keyspace.openAndGetStore(metadata), transformedSources, GC_BEFORE);
+             CompactionIterator iter = new CompactionIterator(OperationType.COMPACTION,
+                                                              Lists.transform(content, x -> new Scanner(x)),
+                                                              controller, NOW, null))
+        {
+            List<Unfiltered> result = new ArrayList<>();
+            assertTrue(iter.hasNext());
+            try (UnfilteredRowIterator partition = iter.next())
+            {
+                Iterators.addAll(result, partition);
+            }
+            assertFalse(iter.hasNext());
+            return result;
+        }
+    }
+
+    private UnfilteredRowIterator listToIterator(List<Unfiltered> list, DecoratedKey key)
+    {
+        return UnfilteredRowsGenerator.source(list, metadata, key, deletionTimes.getOrDefault(list, DeletionTime.LIVE));
+    }
+
+    NavigableMap<DecoratedKey, List<Unfiltered>> generateContent(Random rand, UnfilteredRowsGenerator generator,
+                                                                 List<DecoratedKey> keys, int pcount, int rcount)
+    {
+        NavigableMap<DecoratedKey, List<Unfiltered>> map = new TreeMap<>();
+        for (int i = 0; i < pcount; ++i)
+        {
+            DecoratedKey key = keys.get(rand.nextInt(keys.size()));
+            map.put(key, generator.generateSource(rand, rcount, RANGE, NOW - 5, x -> NOW - 1));
+        }
+        return map;
+    }
+
+    @Test
+    public void testRandom()
+    {
+        UnfilteredRowsGenerator generator = new UnfilteredRowsGenerator(metadata.comparator, false);
+        for (int seed = 1; seed < 100; ++seed)
+        {
+            Random rand = new Random(seed);
+            List<List<Unfiltered>> sources = new ArrayList<>();
+            for (int i = 0; i < 10; ++i)
+                sources.add(generator.generateSource(rand, COUNT, RANGE, NOW - 5, x -> NOW - 15));
+            int srcSz = sources.stream().mapToInt(CompactionIteratorTest::size).sum();
+            List<List<Unfiltered>> tombSources = new ArrayList<>();
+            for (int i = 0; i < 10; ++i)
+                sources.add(generator.generateSource(rand, COUNT, RANGE, NOW - 5, x -> NOW - 15));
+            List<Unfiltered> result = compact(sources, tombSources);
+            verifyEquivalent(sources, result, tombSources, generator);
+            assertTrue(size(result) < srcSz);
+        }
+    }
+
+    class Controller extends CompactionController
+    {
+        private final Map<DecoratedKey, Iterable<UnfilteredRowIterator>> tombstoneSources;
+
+        public Controller(ColumnFamilyStore cfs, Map<DecoratedKey, Iterable<UnfilteredRowIterator>> tombstoneSources, int gcBefore)
+        {
+            super(cfs, Collections.emptySet(), gcBefore);
+            this.tombstoneSources = tombstoneSources;
+        }
+
+        @Override
+        public Iterable<UnfilteredRowIterator> shadowSources(DecoratedKey key, boolean tombstoneOnly)
+        {
+            assert tombstoneOnly;
+            return tombstoneSources.get(key);
+        }
+    }
+
+    class Scanner extends AbstractUnfilteredPartitionIterator implements ISSTableScanner
+    {
+        Iterator<UnfilteredRowIterator> iter;
+
+        Scanner(Iterable<UnfilteredRowIterator> content)
+        {
+            iter = content.iterator();
+        }
+
+        @Override
+        public boolean isForThrift()
+        {
+            return false;
+        }
+
+        @Override
+        public CFMetaData metadata()
+        {
+            return metadata;
+        }
+
+        @Override
+        public boolean hasNext()
+        {
+            return iter.hasNext();
+        }
+
+        @Override
+        public UnfilteredRowIterator next()
+        {
+            return iter.next();
+        }
+
+        @Override
+        public long getLengthInBytes()
+        {
+            return 0;
+        }
+
+        @Override
+        public long getCurrentPosition()
+        {
+            return 0;
+        }
+
+        @Override
+        public long getBytesScanned()
+        {
+            return 0;
+        }
+
+        @Override
+        public long getCompressedLengthInBytes()
+        {
+            return 0;
+        }
+
+        @Override
+        public String getBackingFiles()
+        {
+            return null;
+        }
+    }
+
     @Test
     public void duplicateRowsTest() throws Throwable
     {
@@ -52,7 +415,6 @@
         flush();
 
         DatabaseDescriptor.setSnapshotOnDuplicateRowDetection(true);
-        ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
         CFMetaData metadata = getCurrentColumnFamilyStore().metadata;
 
         final HashMap<InetAddress, MessageOut> sentMessages = new HashMap<>();
@@ -73,28 +435,26 @@
 
         // no duplicates
         sentMessages.clear();
-        iterate(cfs, iter(metadata,
-                          false,
-                          makeRow(metadata,0, 0),
-                          makeRow(metadata,0, 1),
-                          makeRow(metadata,0, 2)));
+        iterate(makeRow(metadata,0, 0),
+                makeRow(metadata,0, 1),
+                makeRow(metadata,0, 2));
         assertCommandIssued(sentMessages, false);
 
         // now test with a duplicate row and see that we issue a snapshot command
         sentMessages.clear();
-        iterate(cfs, iter(metadata,
-                          false,
-                          makeRow(metadata, 0, 0),
-                          makeRow(metadata, 0, 1),
-                          makeRow(metadata, 0, 1)));
+        iterate(makeRow(metadata, 0, 0),
+                makeRow(metadata, 0, 1),
+                makeRow(metadata, 0, 1));
         assertCommandIssued(sentMessages, true);
     }
 
-    private void iterate(ColumnFamilyStore cfs, UnfilteredPartitionIterator partitions)
+    private void iterate(Unfiltered...unfiltereds)
     {
-
-        try (CompactionController controller = new CompactionController(getCurrentColumnFamilyStore(), Integer.MAX_VALUE);
-             ISSTableScanner scanner = scanner(cfs, partitions);
+        ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+        DecoratedKey key = cfs.metadata.partitioner.decorateKey(ByteBufferUtil.bytes("key"));
+        try (CompactionController controller = new CompactionController(cfs, Integer.MAX_VALUE);
+             UnfilteredRowIterator rows = rows(cfs.metadata, key, false, unfiltereds);
+             ISSTableScanner scanner = new Scanner(Collections.singletonList(rows));
              CompactionIterator iter = new CompactionIterator(OperationType.COMPACTION,
                                                               Collections.singletonList(scanner),
                                                               controller, FBUtilities.nowInSeconds(), null))
@@ -108,27 +468,4 @@
             }
         }
     }
-
-    private ISSTableScanner scanner(final ColumnFamilyStore cfs, final UnfilteredPartitionIterator partitions)
-    {
-
-        return new ISSTableScanner()
-        {
-            public long getLengthInBytes() { return 0; }
-
-            public long getCurrentPosition() { return 0; }
-
-            public String getBackingFiles() { return cfs.getLiveSSTables().iterator().next().toString(); }
-
-            public boolean isForThrift() { return false; }
-
-            public CFMetaData metadata() { return cfs.metadata; }
-
-            public void close() { }
-
-            public boolean hasNext() { return partitions.hasNext(); }
-
-            public UnfilteredRowIterator next() { return partitions.next(); }
-        };
-    }
 }
diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionStrategyManagerTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionStrategyManagerTest.java
new file mode 100644
index 0000000..2120757
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/compaction/CompactionStrategyManagerTest.java
@@ -0,0 +1,290 @@
+/*
+ * 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.db.compaction;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.google.common.io.Files;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.Util;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.db.DiskBoundaries;
+import org.apache.cassandra.db.DiskBoundaryManager;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.RowUpdateBuilder;
+import org.apache.cassandra.dht.ByteOrderedPartitioner;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.notifications.SSTableAddedNotification;
+import org.apache.cassandra.notifications.SSTableDeletingNotification;
+import org.apache.cassandra.schema.CompactionParams;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.service.StorageService;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class CompactionStrategyManagerTest
+{
+    private static final String KS_PREFIX = "Keyspace1";
+    private static final String TABLE_PREFIX = "CF_STANDARD";
+
+    private static IPartitioner originalPartitioner;
+    private static boolean backups;
+
+    @BeforeClass
+    public static void beforeClass()
+    {
+        SchemaLoader.prepareServer();
+        backups = DatabaseDescriptor.isIncrementalBackupsEnabled();
+        DatabaseDescriptor.setIncrementalBackupsEnabled(false);
+        /**
+         * We use byte ordered partitioner in this test to be able to easily infer an SSTable
+         * disk assignment based on its generation - See {@link this#getSSTableIndex(Integer[], SSTableReader)}
+         */
+        originalPartitioner = StorageService.instance.setPartitionerUnsafe(ByteOrderedPartitioner.instance);
+    }
+
+    @AfterClass
+    public static void afterClass()
+    {
+        DatabaseDescriptor.setPartitionerUnsafe(originalPartitioner);
+        DatabaseDescriptor.setIncrementalBackupsEnabled(backups);
+    }
+
+    @Test
+    public void testSSTablesAssignedToCorrectCompactionStrategy()
+    {
+        // Creates 100 SSTables with keys 0-99
+        int numSSTables = 100;
+        SchemaLoader.createKeyspace(KS_PREFIX,
+                                    KeyspaceParams.simple(1),
+                                    SchemaLoader.standardCFMD(KS_PREFIX, TABLE_PREFIX)
+                                                .compaction(CompactionParams.scts(Collections.emptyMap())));
+        ColumnFamilyStore cfs = Keyspace.open(KS_PREFIX).getColumnFamilyStore(TABLE_PREFIX);
+        cfs.disableAutoCompaction();
+        for (int i = 0; i < numSSTables; i++)
+        {
+            createSSTableWithKey(KS_PREFIX, TABLE_PREFIX, i);
+        }
+
+        // Creates a CompactionStrategymanager with different numbers of disks and check
+        // if the SSTables are assigned to the correct compaction strategies
+        for (int numDisks = 2; numDisks < 10; numDisks++)
+        {
+            testSSTablesAssignedToCorrectCompactionStrategy(numSSTables, numDisks);
+        }
+    }
+
+    public void testSSTablesAssignedToCorrectCompactionStrategy(int numSSTables, int numDisks)
+    {
+        // Create a mock CFS with the given number of disks
+        MockCFS cfs = createJBODMockCFS(numDisks);
+        //Check that CFS will contain numSSTables
+        assertEquals(numSSTables, cfs.getLiveSSTables().size());
+
+        // Creates a compaction strategy manager with an external boundary supplier
+        final Integer[] boundaries = computeBoundaries(numSSTables, numDisks);
+
+        MockBoundaryManager mockBoundaryManager = new MockBoundaryManager(cfs, boundaries);
+        System.out.println("Boundaries for " + numDisks + " disks is " + Arrays.toString(boundaries));
+        CompactionStrategyManager csm = new CompactionStrategyManager(cfs, mockBoundaryManager::getBoundaries,
+                                                                      true);
+
+        // Check that SSTables are assigned to the correct Compaction Strategy
+        for (SSTableReader reader : cfs.getLiveSSTables())
+        {
+            verifySSTableIsAssignedToCorrectStrategy(boundaries, csm, reader);
+        }
+
+        for (int delta = 1; delta <= 3; delta++)
+        {
+            // Update disk boundaries
+            Integer[] previousBoundaries = Arrays.copyOf(boundaries, boundaries.length);
+            updateBoundaries(mockBoundaryManager, boundaries, delta);
+
+            // Check that SSTables are still assigned to the previous boundary layout
+            System.out.println("Old boundaries: " + Arrays.toString(previousBoundaries) + " New boundaries: " + Arrays.toString(boundaries));
+            for (SSTableReader reader : cfs.getLiveSSTables())
+            {
+                verifySSTableIsAssignedToCorrectStrategy(previousBoundaries, csm, reader);
+            }
+
+            // Reload CompactionStrategyManager so new disk boundaries will be loaded
+            csm.maybeReloadDiskBoundaries();
+
+            for (SSTableReader reader : cfs.getLiveSSTables())
+            {
+                // Check that SSTables are assigned to the new boundary layout
+                verifySSTableIsAssignedToCorrectStrategy(boundaries, csm, reader);
+
+                // Remove SSTable and check that it will be removed from the correct compaction strategy
+                csm.handleNotification(new SSTableDeletingNotification(reader), this);
+                assertFalse(((SizeTieredCompactionStrategy)csm.compactionStrategyFor(reader)).sstables.contains(reader));
+
+                // Add SSTable again and check that is correctly assigned
+                csm.handleNotification(new SSTableAddedNotification(Collections.singleton(reader)), this);
+                verifySSTableIsAssignedToCorrectStrategy(boundaries, csm, reader);
+            }
+        }
+    }
+
+    private MockCFS createJBODMockCFS(int disks)
+    {
+        // Create #disks data directories to simulate JBOD
+        Directories.DataDirectory[] directories = new Directories.DataDirectory[disks];
+        for (int i = 0; i < disks; ++i)
+        {
+            File tempDir = Files.createTempDir();
+            tempDir.deleteOnExit();
+            directories[i] = new Directories.DataDirectory(tempDir);
+        }
+
+        ColumnFamilyStore cfs = Keyspace.open(KS_PREFIX).getColumnFamilyStore(TABLE_PREFIX);
+        MockCFS mockCFS = new MockCFS(cfs, new Directories(cfs.metadata, directories));
+        mockCFS.disableAutoCompaction();
+        mockCFS.addSSTables(cfs.getLiveSSTables());
+        return mockCFS;
+    }
+
+    /**
+     * Updates the boundaries with a delta
+     */
+    private void updateBoundaries(MockBoundaryManager boundaryManager, Integer[] boundaries, int delta)
+    {
+        for (int j = 0; j < boundaries.length - 1; j++)
+        {
+            if ((j + delta) % 2 == 0)
+                boundaries[j] -= delta;
+            else
+                boundaries[j] += delta;
+        }
+        boundaryManager.invalidateBoundaries();
+    }
+
+    private void verifySSTableIsAssignedToCorrectStrategy(Integer[] boundaries, CompactionStrategyManager csm, SSTableReader reader)
+    {
+        // Check that sstable is assigned to correct disk
+        int index = getSSTableIndex(boundaries, reader);
+        assertEquals(index, csm.compactionStrategyIndexFor(reader));
+        // Check that compaction strategy actually contains SSTable
+        assertTrue(((SizeTieredCompactionStrategy)csm.compactionStrategyFor(reader)).sstables.contains(reader));
+    }
+
+    /**
+     * Creates disk boundaries such that each disk receives
+     * an equal amount of SSTables
+     */
+    private Integer[] computeBoundaries(int numSSTables, int numDisks)
+    {
+        Integer[] result = new Integer[numDisks];
+        int sstablesPerRange = numSSTables / numDisks;
+        result[0] = sstablesPerRange;
+        for (int i = 1; i < numDisks; i++)
+        {
+            result[i] = result[i - 1] + sstablesPerRange;
+        }
+        result[numDisks - 1] = numSSTables; // make last boundary alwyays be the number of SSTables to prevent rounding errors
+        return result;
+    }
+
+    /**
+     * Since each SSTable contains keys from 0-99, and each sstable
+     * generation is numbered from 1-100, since we are using ByteOrderedPartitioner
+     * we can compute the sstable position in the disk boundaries by finding
+     * the generation position relative to the boundaries
+     */
+    private int getSSTableIndex(Integer[] boundaries, SSTableReader reader)
+    {
+        int index = 0;
+        while (boundaries[index] < reader.descriptor.generation)
+            index++;
+        System.out.println("Index for SSTable " + reader.descriptor.generation + " on boundary " + Arrays.toString(boundaries) + " is " + index);
+        return index;
+    }
+
+
+
+    class MockBoundaryManager
+    {
+        private final ColumnFamilyStore cfs;
+        private Integer[] positions;
+        private DiskBoundaries boundaries;
+
+        public MockBoundaryManager(ColumnFamilyStore cfs, Integer[] positions)
+        {
+            this.cfs = cfs;
+            this.positions = positions;
+            this.boundaries = createDiskBoundaries(cfs, positions);
+        }
+
+        public void invalidateBoundaries()
+        {
+            boundaries.invalidate();
+        }
+
+        public DiskBoundaries getBoundaries()
+        {
+            if (boundaries.isOutOfDate())
+                boundaries = createDiskBoundaries(cfs, positions);
+            return boundaries;
+        }
+
+        private DiskBoundaries createDiskBoundaries(ColumnFamilyStore cfs, Integer[] boundaries)
+        {
+            List<PartitionPosition> positions = Arrays.stream(boundaries).map(b -> Util.token(String.format(String.format("%04d", b))).minKeyBound()).collect(Collectors.toList());
+            return new DiskBoundaries(cfs, cfs.getDirectories().getWriteableLocations(), positions, 0, 0);
+        }
+    }
+
+    private static void createSSTableWithKey(String keyspace, String table, int key)
+    {
+        long timestamp = System.currentTimeMillis();
+        DecoratedKey dk = Util.dk(String.format("%04d", key));
+        ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
+        new RowUpdateBuilder(cfs.metadata, timestamp, dk.getKey())
+        .clustering(Integer.toString(key))
+        .add("val", "val")
+        .build()
+        .applyUnsafe();
+        cfs.forceBlockingFlush();
+    }
+
+    // just to be able to override the data directories
+    private static class MockCFS extends ColumnFamilyStore
+    {
+        MockCFS(ColumnFamilyStore cfs, Directories dirs)
+        {
+            super(cfs.keyspace, cfs.getTableName(), 0, cfs.metadata, dirs, false, false, true);
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionsBytemanTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionsBytemanTest.java
index 0b391a5..d5f2800 100644
--- a/test/unit/org/apache/cassandra/db/compaction/CompactionsBytemanTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/CompactionsBytemanTest.java
@@ -18,17 +18,32 @@
 
 package org.apache.cassandra.db.compaction;
 
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.metrics.CompactionMetrics;
 import org.apache.cassandra.utils.FBUtilities;
 import org.jboss.byteman.contrib.bmunit.BMRule;
 import org.jboss.byteman.contrib.bmunit.BMUnitRunner;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 @RunWith(BMUnitRunner.class)
 public class CompactionsBytemanTest extends CQLTester
@@ -53,4 +68,88 @@
         FBUtilities.waitOnFutures(CompactionManager.instance.submitBackground(cfs));
         assertEquals(0, CompactionManager.instance.compactingCF.count(cfs));
     }
+
+    @Test
+    @BMRule(name = "Stop all compactions",
+    targetClass = "CompactionTask",
+    targetMethod = "runMayThrow",
+    targetLocation = "AT INVOKE getCompactionAwareWriter",
+    action = "$ci.stop()")
+    public void testStopUserDefinedCompactionRepaired() throws Throwable
+    {
+        testStopCompactionRepaired((cfs) -> {
+            Collection<Descriptor> files = cfs.getLiveSSTables().stream().map(s -> s.descriptor).collect(Collectors.toList());
+            FBUtilities.waitOnFuture(CompactionManager.instance.submitUserDefined(cfs, files, CompactionManager.NO_GC));
+        });
+    }
+
+    @Test
+    @BMRule(name = "Stop all compactions",
+    targetClass = "CompactionTask",
+    targetMethod = "runMayThrow",
+    targetLocation = "AT INVOKE getCompactionAwareWriter",
+    action = "$ci.stop()")
+    public void testStopSubRangeCompactionRepaired() throws Throwable
+    {
+        testStopCompactionRepaired((cfs) -> {
+            Collection<Range<Token>> ranges = Collections.singleton(new Range<>(cfs.getPartitioner().getMinimumToken(),
+                                                                                cfs.getPartitioner().getMaximumToken()));
+            CompactionManager.instance.forceCompactionForTokenRange(cfs, ranges);
+        });
+    }
+
+    public void testStopCompactionRepaired(Consumer<ColumnFamilyStore> compactionRunner) throws Throwable
+    {
+        String table = createTable("CREATE TABLE %s (k INT, c INT, v INT, PRIMARY KEY (k, c))");
+        ColumnFamilyStore cfs = Keyspace.open(CQLTester.KEYSPACE).getColumnFamilyStore(table);
+        cfs.disableAutoCompaction();
+        for (int i = 0; i < 5; i++)
+        {
+            for (int j = 0; j < 10; j++)
+            {
+                execute("insert into %s (k, c, v) values (?, ?, ?)", i, j, i*j);
+            }
+            cfs.forceBlockingFlush();
+        }
+        setRepaired(cfs, cfs.getLiveSSTables());
+        for (int i = 0; i < 5; i++)
+        {
+            for (int j = 0; j < 10; j++)
+            {
+                execute("insert into %s (k, c, v) values (?, ?, ?)", i, j, i*j);
+            }
+            cfs.forceBlockingFlush();
+        }
+
+        assertTrue(cfs.getTracker().getCompacting().isEmpty());
+        assertTrue(CompactionMetrics.getCompactions().stream().noneMatch(h -> h.getCompactionInfo().getCFMetaData().equals(cfs.metadata)));
+
+        try
+        {
+            compactionRunner.accept(cfs);
+            fail("compaction should fail");
+        }
+        catch (RuntimeException t)
+        {
+            if (!(t.getCause().getCause() instanceof CompactionInterruptedException))
+                throw t;
+            //expected
+        }
+
+        assertTrue(cfs.getTracker().getCompacting().isEmpty());
+        assertTrue(CompactionMetrics.getCompactions().stream().noneMatch(h -> h.getCompactionInfo().getCFMetaData().equals(cfs.metadata)));
+
+    }
+
+    private void setRepaired(ColumnFamilyStore cfs, Iterable<SSTableReader> sstables) throws IOException
+    {
+        Set<SSTableReader> changed = new HashSet<>();
+        for (SSTableReader sstable: sstables)
+        {
+            sstable.descriptor.getMetadataSerializer().mutateRepairedAt(sstable.descriptor, System.currentTimeMillis());
+            sstable.reloadSSTableMetadata();
+            changed.add(sstable);
+        }
+        cfs.getTracker().notifySSTableRepairedStatusChanged(changed);
+    }
 }
diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionsCQLTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionsCQLTest.java
index ce85dc5..aa29343 100644
--- a/test/unit/org/apache/cassandra/db/compaction/CompactionsCQLTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/CompactionsCQLTest.java
@@ -17,15 +17,28 @@
  */
 package org.apache.cassandra.db.compaction;
 
+import java.nio.ByteBuffer;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Random;
+import java.util.Set;
 
 import org.junit.Test;
 
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter;
+import org.apache.cassandra.db.compaction.writers.MaxSSTableSizeWriter;
+import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.schema.CompactionParams;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -49,7 +62,7 @@
     @Test
     public void testTriggerMinorCompactionLCS() throws Throwable
     {
-        createTable("CREATE TABLE %s (id text PRIMARY KEY) WITH compaction = {'class':'LeveledCompactionStrategy', 'sstable_size_in_mb':1};");
+        createTable("CREATE TABLE %s (id text PRIMARY KEY) WITH compaction = {'class':'LeveledCompactionStrategy', 'sstable_size_in_mb':1, 'fanout_size':5};");
         assertTrue(getCurrentColumnFamilyStore().getCompactionStrategyManager().isEnabled());
         execute("insert into %s (id) values ('1')");
         flush();
@@ -59,6 +72,7 @@
     }
 
 
+
     @Test
     public void testTriggerMinorCompactionDTCS() throws Throwable
     {
@@ -103,6 +117,12 @@
         assertFalse(getCurrentColumnFamilyStore().getCompactionStrategyManager().isEnabled());
         getCurrentColumnFamilyStore().enableAutoCompaction();
         assertTrue(getCurrentColumnFamilyStore().getCompactionStrategyManager().isEnabled());
+
+        // Alter keyspace replication settings to force compaction strategy reload and check strategy is still enabled
+        execute("alter keyspace "+keyspace()+" with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }");
+        getCurrentColumnFamilyStore().getCompactionStrategyManager().maybeReloadDiskBoundaries();
+        assertTrue(getCurrentColumnFamilyStore().getCompactionStrategyManager().isEnabled());
+
         execute("insert into %s (id) values ('1')");
         flush();
         execute("insert into %s (id) values ('1')");
@@ -160,17 +180,22 @@
         localOptions.put("class", "DateTieredCompactionStrategy");
         getCurrentColumnFamilyStore().setCompactionParameters(localOptions);
         assertTrue(verifyStrategies(getCurrentColumnFamilyStore().getCompactionStrategyManager(), DateTieredCompactionStrategy.class));
+        // Invalidate disk boundaries to ensure that boundary invalidation will not cause the old strategy to be reloaded
+        getCurrentColumnFamilyStore().invalidateDiskBoundaries();
         // altering something non-compaction related
         execute("ALTER TABLE %s WITH gc_grace_seconds = 1000");
         // should keep the local compaction strat
         assertTrue(verifyStrategies(getCurrentColumnFamilyStore().getCompactionStrategyManager(), DateTieredCompactionStrategy.class));
+        // Alter keyspace replication settings to force compaction strategy reload
+        execute("alter keyspace "+keyspace()+" with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }");
+        // should keep the local compaction strat
+        assertTrue(verifyStrategies(getCurrentColumnFamilyStore().getCompactionStrategyManager(), DateTieredCompactionStrategy.class));
         // altering a compaction option
         execute("ALTER TABLE %s WITH compaction = {'class':'SizeTieredCompactionStrategy', 'min_threshold':3}");
         // will use the new option
         assertTrue(verifyStrategies(getCurrentColumnFamilyStore().getCompactionStrategyManager(), SizeTieredCompactionStrategy.class));
     }
 
-
     @Test
     public void testSetLocalCompactionStrategyDisable() throws Throwable
     {
@@ -214,12 +239,145 @@
         getCurrentColumnFamilyStore().setCompactionParameters(localOptions);
     }
 
+     @Test(expected = IllegalArgumentException.class)
+     public void testBadProvidesTombstoneOption()
+     {
+         createTable("CREATE TABLE %s (id text PRIMARY KEY)");
+         Map<String, String> localOptions = new HashMap<>();
+         localOptions.put("class","SizeTieredCompactionStrategy");
+         localOptions.put("provide_overlapping_tombstones","IllegalValue");
+
+         getCurrentColumnFamilyStore().setCompactionParameters(localOptions);
+     }
+     @Test
+     public void testProvidesTombstoneOptionverifiation()
+     {
+         createTable("CREATE TABLE %s (id text PRIMARY KEY)");
+         Map<String, String> localOptions = new HashMap<>();
+         localOptions.put("class","SizeTieredCompactionStrategy");
+         localOptions.put("provide_overlapping_tombstones","row");
+
+         getCurrentColumnFamilyStore().setCompactionParameters(localOptions);
+         assertEquals(CompactionParams.TombstoneOption.ROW, getCurrentColumnFamilyStore().getCompactionStrategyManager().getCompactionParams().tombstoneOption());
+     }
+
+    @Test
+    public void testAbortNotifications() throws Throwable
+    {
+        createTable("create table %s (id int primary key, x blob) with compaction = {'class':'LeveledCompactionStrategy', 'sstable_size_in_mb':1}");
+        Random r = new Random();
+        byte [] b = new byte[100 * 1024];
+        for (int i = 0; i < 1000; i++)
+        {
+            r.nextBytes(b);
+            execute("insert into %s (id, x) values (?, ?)", i, ByteBuffer.wrap(b));
+        }
+        getCurrentColumnFamilyStore().forceBlockingFlush();
+        getCurrentColumnFamilyStore().disableAutoCompaction();
+        for (int i = 0; i < 1000; i++)
+        {
+            r.nextBytes(b);
+            execute("insert into %s (id, x) values (?, ?)", i, ByteBuffer.wrap(b));
+        }
+        getCurrentColumnFamilyStore().forceBlockingFlush();
+
+        LeveledCompactionStrategy lcs = (LeveledCompactionStrategy) getCurrentColumnFamilyStore().getCompactionStrategyManager().getStrategies().get(1).get(0);
+        LeveledCompactionTask lcsTask;
+        while (true)
+        {
+            lcsTask = (LeveledCompactionTask) lcs.getNextBackgroundTask(0);
+            if (lcsTask != null)
+            {
+                lcsTask.execute(null);
+                break;
+            }
+            Thread.sleep(1000);
+        }
+        // now all sstables are non-overlapping in L1 - we need them to be in L2:
+        for (SSTableReader sstable : getCurrentColumnFamilyStore().getLiveSSTables())
+        {
+            lcs.removeSSTable(sstable);
+            sstable.descriptor.getMetadataSerializer().mutateLevel(sstable.descriptor, 2);
+            sstable.reloadSSTableMetadata();
+            lcs.addSSTable(sstable);
+        }
+
+        for (int i = 0; i < 1000; i++)
+        {
+            r.nextBytes(b);
+            execute("insert into %s (id, x) values (?, ?)", i, ByteBuffer.wrap(b));
+        }
+        getCurrentColumnFamilyStore().forceBlockingFlush();
+        // now we have a bunch of sstables in L2 and one in L0 - bump the L0 one to L1:
+        for (SSTableReader sstable : getCurrentColumnFamilyStore().getLiveSSTables())
+        {
+            if (sstable.getSSTableLevel() == 0)
+            {
+                lcs.removeSSTable(sstable);
+                sstable.descriptor.getMetadataSerializer().mutateLevel(sstable.descriptor, 1);
+                sstable.reloadSSTableMetadata();
+                lcs.addSSTable(sstable);
+            }
+        }
+        // at this point we have a single sstable in L1, and a bunch of sstables in L2 - a background compaction should
+        // trigger an L1 -> L2 compaction which we abort after creating 5 sstables - this notifies LCS that MOVED_START
+        // sstables have been removed.
+        try
+        {
+            AbstractCompactionTask task = new NotifyingCompactionTask((LeveledCompactionTask) lcs.getNextBackgroundTask(0));
+            task.execute(null);
+            fail("task should throw exception");
+        }
+        catch (Exception ignored)
+        {
+            // ignored
+        }
+
+        lcsTask = (LeveledCompactionTask) lcs.getNextBackgroundTask(0);
+        try
+        {
+            assertNotNull(lcsTask);
+        }
+        finally
+        {
+            if (lcsTask != null)
+                lcsTask.transaction.abort();
+        }
+    }
+
+    private static class NotifyingCompactionTask extends LeveledCompactionTask
+    {
+        public NotifyingCompactionTask(LeveledCompactionTask task)
+        {
+            super(task.cfs, task.transaction, task.getLevel(), task.gcBefore, task.getLevel(), false);
+        }
+
+        @Override
+        public CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs,
+                                                              Directories directories,
+                                                              LifecycleTransaction txn,
+                                                              Set<SSTableReader> nonExpiredSSTables)
+        {
+            return new MaxSSTableSizeWriter(cfs, directories, txn, nonExpiredSSTables, 1 << 20, 1)
+            {
+                int switchCount = 0;
+                public void switchCompactionLocation(Directories.DataDirectory directory)
+                {
+                    switchCount++;
+                    if (switchCount > 5)
+                        throw new RuntimeException("Throw after a few sstables have had their starts moved");
+                    super.switchCompactionLocation(directory);
+                }
+            };
+        }
+    }
+
     public boolean verifyStrategies(CompactionStrategyManager manager, Class<? extends AbstractCompactionStrategy> expected)
     {
         boolean found = false;
-        for (AbstractCompactionStrategy actualStrategy : manager.getStrategies())
+        for (List<AbstractCompactionStrategy> strategies : manager.getStrategies())
         {
-            if (!actualStrategy.getClass().equals(expected))
+            if (!strategies.stream().allMatch((strategy) -> strategy.getClass().equals(expected)))
                 return false;
             found = true;
         }
diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionsPurgeTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionsPurgeTest.java
index f02f4c2..f5b1641 100644
--- a/test/unit/org/apache/cassandra/db/compaction/CompactionsPurgeTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/CompactionsPurgeTest.java
@@ -19,6 +19,7 @@
 package org.apache.cassandra.db.compaction;
 
 import java.util.Collection;
+import java.util.List;
 import java.util.concurrent.ExecutionException;
 
 import org.junit.BeforeClass;
@@ -297,7 +298,9 @@
                 .build().applyUnsafe();
 
         cfs.forceBlockingFlush();
-        cfs.getCompactionStrategyManager().getUserDefinedTask(sstablesIncomplete, Integer.MAX_VALUE).execute(null);
+        List<AbstractCompactionTask> tasks = cfs.getCompactionStrategyManager().getUserDefinedTasks(sstablesIncomplete, Integer.MAX_VALUE);
+        assertEquals(1, tasks.size());
+        tasks.get(0).execute(null);
 
         // verify that minor compaction does GC when key is provably not
         // present in a non-compacted sstable
@@ -346,7 +349,9 @@
         cfs.forceBlockingFlush();
 
         // compact the sstables with the c1/c2 data and the c1 tombstone
-        cfs.getCompactionStrategyManager().getUserDefinedTask(sstablesIncomplete, Integer.MAX_VALUE).execute(null);
+        List<AbstractCompactionTask> tasks = cfs.getCompactionStrategyManager().getUserDefinedTasks(sstablesIncomplete, Integer.MAX_VALUE);
+        assertEquals(1, tasks.size());
+        tasks.get(0).execute(null);
 
         // We should have both the c1 and c2 tombstones still. Since the min timestamp in the c2 tombstone
         // sstable is older than the c1 tombstone, it is invalid to throw out the c1 tombstone.
diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionsTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionsTest.java
index 28725c7..c1bddd1 100644
--- a/test/unit/org/apache/cassandra/db/compaction/CompactionsTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/CompactionsTest.java
@@ -336,8 +336,8 @@
         {
             RowUpdateBuilder deletedRowUpdateBuilder = new RowUpdateBuilder(table, 1, Util.dk(Integer.toString(dk)));
             deletedRowUpdateBuilder.clustering("01").add("val", "a"); //Range tombstone covers this (timestamp 2 > 1)
-            Clustering startClustering = new Clustering(ByteBufferUtil.bytes("0"));
-            Clustering endClustering = new Clustering(ByteBufferUtil.bytes("b"));
+            Clustering startClustering = Clustering.make(ByteBufferUtil.bytes("0"));
+            Clustering endClustering = Clustering.make(ByteBufferUtil.bytes("b"));
             deletedRowUpdateBuilder.addRangeTombstone(new RangeTombstone(Slice.make(startClustering, endClustering), new DeletionTime(2, (int) (System.currentTimeMillis() / 1000))));
             deletedRowUpdateBuilder.build().applyUnsafe();
 
@@ -389,8 +389,8 @@
         {
             k.add(p.partitionKey());
             final SinglePartitionReadCommand command = SinglePartitionReadCommand.create(cfs.metadata, FBUtilities.nowInSeconds(), ColumnFilter.all(cfs.metadata), RowFilter.NONE, DataLimits.NONE, p.partitionKey(), new ClusteringIndexSliceFilter(Slices.ALL, false));
-            try (ReadOrderGroup orderGroup = command.startOrderGroup();
-                 PartitionIterator iterator = command.executeInternal(orderGroup))
+            try (ReadExecutionController executionController = command.executionController();
+                 PartitionIterator iterator = command.executeInternal(executionController))
             {
                 try (RowIterator rowIterator = iterator.next())
                 {
@@ -597,4 +597,15 @@
                                                                        200, 209,
                                                                        300, 301)));
     }
+
+    @Test
+    public void testConcurrencySettings()
+    {
+        CompactionManager.instance.setConcurrentCompactors(2);
+        assertEquals(2, CompactionManager.instance.getCoreCompactorThreads());
+        CompactionManager.instance.setConcurrentCompactors(3);
+        assertEquals(3, CompactionManager.instance.getCoreCompactorThreads());
+        CompactionManager.instance.setConcurrentCompactors(1);
+        assertEquals(1, CompactionManager.instance.getCoreCompactorThreads());
+    }
 }
diff --git a/test/unit/org/apache/cassandra/db/compaction/CorruptedSSTablesCompactionsTest.java b/test/unit/org/apache/cassandra/db/compaction/CorruptedSSTablesCompactionsTest.java
index 1c4387c..231c2b5 100644
--- a/test/unit/org/apache/cassandra/db/compaction/CorruptedSSTablesCompactionsTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/CorruptedSSTablesCompactionsTest.java
@@ -37,6 +37,7 @@
 
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
+import org.apache.cassandra.cache.ChunkCache;
 import org.apache.cassandra.config.*;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.marshal.LongType;
@@ -191,6 +192,8 @@
                 byte[] corruption = new byte[corruptionSize];
                 random.nextBytes(corruption);
                 raf.write(corruption);
+                if (ChunkCache.instance != null)
+                    ChunkCache.instance.invalidateFile(sstable.getFilename());
 
             }
             finally
diff --git a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java
index 5bbc931..9b59e73 100644
--- a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java
@@ -31,26 +31,30 @@
 import java.util.Random;
 import java.util.Set;
 import java.util.UUID;
+import java.util.stream.Collectors;
+
+import junit.framework.Assert;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.BeforeClass;
+import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.MockSchema;
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 import org.apache.cassandra.SchemaLoader;
-import org.apache.cassandra.UpdateBuilder;
 import org.apache.cassandra.Util;
+import org.apache.cassandra.UpdateBuilder;
 import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.Keyspace;
-import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.exceptions.ConfigurationException;
@@ -66,6 +70,8 @@
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.Pair;
 
+import static java.util.Collections.singleton;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -138,10 +144,11 @@
         }
 
         waitForLeveling(cfs);
-        CompactionStrategyManager strategy =  cfs.getCompactionStrategyManager();
+        CompactionStrategyManager strategyManager = cfs.getCompactionStrategyManager();
         // Checking we're not completely bad at math
-        int l1Count = strategy.getSSTableCountPerLevel()[1];
-        int l2Count = strategy.getSSTableCountPerLevel()[2];
+
+        int l1Count = strategyManager.getSSTableCountPerLevel()[1];
+        int l2Count = strategyManager.getSSTableCountPerLevel()[2];
         if (l1Count == 0 || l2Count == 0)
         {
             logger.error("L1 or L2 has 0 sstables. Expected > 0 on both.");
@@ -165,7 +172,6 @@
                 assert groupLevel == tableLevel;
             }
         }
-
     }
 
     /*
@@ -193,10 +199,10 @@
         }
 
         waitForLeveling(cfs);
-        CompactionStrategyManager strategy =  cfs.getCompactionStrategyManager();
+        CompactionStrategyManager strategyManager = cfs.getCompactionStrategyManager();
         // Checking we're not completely bad at math
-        assertTrue(strategy.getSSTableCountPerLevel()[1] > 0);
-        assertTrue(strategy.getSSTableCountPerLevel()[2] > 0);
+        assertTrue(strategyManager.getSSTableCountPerLevel()[1] > 0);
+        assertTrue(strategyManager.getSSTableCountPerLevel()[2] > 0);
 
         Range<Token> range = new Range<>(Util.token(""), Util.token(""));
         int gcBefore = keyspace.getColumnFamilyStore(CF_STANDARDDLEVELED).gcBefore(FBUtilities.nowInSeconds());
@@ -220,16 +226,19 @@
             // so it should be good enough
             boolean allL0Empty = true;
             boolean anyL1NonEmpty = false;
-            for (AbstractCompactionStrategy strategy : strategyManager.getStrategies())
+            for (List<AbstractCompactionStrategy> strategies : strategyManager.getStrategies())
             {
-                if (!(strategy instanceof LeveledCompactionStrategy))
-                    return;
-                // note that we check > 1 here, if there is too little data in L0, we don't compact it up to L1
-                if (((LeveledCompactionStrategy)strategy).getLevelSize(0) > 1)
-                    allL0Empty = false;
-                for (int i = 1; i < 5; i++)
-                    if (((LeveledCompactionStrategy)strategy).getLevelSize(i) > 0)
-                        anyL1NonEmpty = true;
+                for (AbstractCompactionStrategy strategy : strategies)
+                {
+                    if (!(strategy instanceof LeveledCompactionStrategy))
+                        return;
+                    // note that we check > 1 here, if there is too little data in L0, we don't compact it up to L1
+                    if (((LeveledCompactionStrategy)strategy).getLevelSize(0) > 1)
+                        allL0Empty = false;
+                    for (int i = 1; i < 5; i++)
+                        if (((LeveledCompactionStrategy)strategy).getLevelSize(i) > 0)
+                            anyL1NonEmpty = true;
+                }
             }
             if (allL0Empty && anyL1NonEmpty)
                 return;
@@ -256,7 +265,7 @@
         }
 
         waitForLeveling(cfs);
-        LeveledCompactionStrategy strategy = (LeveledCompactionStrategy) (cfs.getCompactionStrategyManager()).getStrategies().get(1);
+        LeveledCompactionStrategy strategy = (LeveledCompactionStrategy) cfs.getCompactionStrategyManager().getStrategies().get(1).get(0);
         assert strategy.getLevelSize(1) > 0;
 
         // get LeveledScanner for level 1 sstables
@@ -292,7 +301,7 @@
             cfs.forceBlockingFlush();
         }
         cfs.forceBlockingFlush();
-        LeveledCompactionStrategy strategy = (LeveledCompactionStrategy) ( cfs.getCompactionStrategyManager()).getStrategies().get(1);
+        LeveledCompactionStrategy strategy = (LeveledCompactionStrategy) cfs.getCompactionStrategyManager().getStrategies().get(1).get(0);
         cfs.forceMajorCompaction();
 
         for (SSTableReader s : cfs.getLiveSSTables())
@@ -301,7 +310,7 @@
             strategy.manifest.remove(s);
             s.descriptor.getMetadataSerializer().mutateLevel(s.descriptor, 6);
             s.reloadSSTableMetadata();
-            strategy.manifest.add(s);
+            strategy.manifest.addSSTables(Collections.singleton(s));
         }
         // verify that all sstables in the changed set is level 6
         for (SSTableReader s : cfs.getLiveSSTables())
@@ -338,49 +347,396 @@
         while(CompactionManager.instance.isCompacting(Arrays.asList(cfs)))
             Thread.sleep(100);
 
-        CompactionStrategyManager strategy =  cfs.getCompactionStrategyManager();
-        List<AbstractCompactionStrategy> strategies = strategy.getStrategies();
-        LeveledCompactionStrategy repaired = (LeveledCompactionStrategy) strategies.get(0);
-        LeveledCompactionStrategy unrepaired = (LeveledCompactionStrategy) strategies.get(1);
+        CompactionStrategyManager manager = cfs.getCompactionStrategyManager();
+        List<List<AbstractCompactionStrategy>> strategies = manager.getStrategies();
+        LeveledCompactionStrategy repaired = (LeveledCompactionStrategy) strategies.get(0).get(0);
+        LeveledCompactionStrategy unrepaired = (LeveledCompactionStrategy) strategies.get(1).get(0);
         assertEquals(0, repaired.manifest.getLevelCount() );
         assertEquals(2, unrepaired.manifest.getLevelCount());
-        assertTrue(strategy.getSSTableCountPerLevel()[1] > 0);
-        assertTrue(strategy.getSSTableCountPerLevel()[2] > 0);
+        assertTrue(manager.getSSTableCountPerLevel()[1] > 0);
+        assertTrue(manager.getSSTableCountPerLevel()[2] > 0);
 
         for (SSTableReader sstable : cfs.getLiveSSTables())
             assertFalse(sstable.isRepaired());
 
-        int sstableCount = 0;
-        for (List<SSTableReader> level : unrepaired.manifest.generations)
-            sstableCount += level.size();
+        int sstableCount = unrepaired.manifest.getSSTables().size();
         // we only have unrepaired sstables:
         assertEquals(sstableCount, cfs.getLiveSSTables().size());
 
-        SSTableReader sstable1 = unrepaired.manifest.generations[2].get(0);
-        SSTableReader sstable2 = unrepaired.manifest.generations[1].get(0);
+        SSTableReader sstable1 = unrepaired.manifest.getLevel(2).iterator().next();
+        SSTableReader sstable2 = unrepaired.manifest.getLevel(1).iterator().next();
 
         sstable1.descriptor.getMetadataSerializer().mutateRepairedAt(sstable1.descriptor, System.currentTimeMillis());
         sstable1.reloadSSTableMetadata();
         assertTrue(sstable1.isRepaired());
 
-        strategy.handleNotification(new SSTableRepairStatusChanged(Arrays.asList(sstable1)), this);
+        manager.handleNotification(new SSTableRepairStatusChanged(Arrays.asList(sstable1)), this);
 
-        int repairedSSTableCount = 0;
-        for (List<SSTableReader> level : repaired.manifest.generations)
-            repairedSSTableCount += level.size();
+        int repairedSSTableCount = repaired.manifest.getSSTables().size();
         assertEquals(1, repairedSSTableCount);
         // make sure the repaired sstable ends up in the same level in the repaired manifest:
-        assertTrue(repaired.manifest.generations[2].contains(sstable1));
+        assertTrue(repaired.manifest.getLevel(2).contains(sstable1));
         // and that it is gone from unrepaired
-        assertFalse(unrepaired.manifest.generations[2].contains(sstable1));
+        assertFalse(unrepaired.manifest.getLevel(2).contains(sstable1));
 
         unrepaired.removeSSTable(sstable2);
-        strategy.handleNotification(new SSTableAddedNotification(Collections.singleton(sstable2)), this);
+        manager.handleNotification(new SSTableAddedNotification(singleton(sstable2)), this);
         assertTrue(unrepaired.manifest.getLevel(1).contains(sstable2));
         assertFalse(repaired.manifest.getLevel(1).contains(sstable2));
     }
 
     @Test
+    public void testTokenRangeCompaction() throws Exception
+    {
+        // Remove any existing data, so we can start out clean with predictable number of sstables
+        cfs.truncateBlocking();
+
+        // Disable auto compaction so cassandra does not compact
+        CompactionManager.instance.disableAutoCompaction();
+
+        ByteBuffer value = ByteBuffer.wrap(new byte[100 * 1024]); // 100 KB value, make it easy to have multiple files
+
+        DecoratedKey key1 = Util.dk(String.valueOf(1));
+        DecoratedKey key2 = Util.dk(String.valueOf(2));
+        List<DecoratedKey> keys = Arrays.asList(key1, key2);
+        int numIterations = 10;
+        int columns = 2;
+
+        // Add enough data to trigger multiple sstables.
+
+        // create 10 sstables that contain data for both key1 and key2
+        for (int i = 0; i < numIterations; i++)
+        {
+            for (DecoratedKey key : keys)
+            {
+                UpdateBuilder update = UpdateBuilder.create(cfs.metadata, key);
+                for (int c = 0; c < columns; c++)
+                    update.newRow("column" + c).add("val", value);
+                update.applyUnsafe();
+            }
+            cfs.forceBlockingFlush();
+        }
+
+        // create 20 more sstables with 10 containing data for key1 and other 10 containing data for key2
+        for (int i = 0; i < numIterations; i++)
+        {
+            for (DecoratedKey key : keys)
+            {
+                UpdateBuilder update = UpdateBuilder.create(cfs.metadata, key);
+                for (int c = 0; c < columns; c++)
+                    update.newRow("column" + c).add("val", value);
+                update.applyUnsafe();
+                cfs.forceBlockingFlush();
+            }
+        }
+
+        // We should have a total of 30 sstables by now
+        assertEquals(30, cfs.getLiveSSTables().size());
+
+        // Compact just the tables with key2. The token ranges for compaction are interpreted as closed intervals,
+        // so we can use [token, token] to select a single token.
+        Range<Token> tokenRange = new Range<>(key2.getToken(), key2.getToken());
+        Collection<Range<Token>> tokenRanges = singleton(tokenRange);
+        cfs.forceCompactionForTokenRange(tokenRanges);
+
+        while (CompactionManager.instance.isCompacting(singleton(cfs)))
+        {
+            Thread.sleep(100);
+        }
+
+        // 20 tables that have key2 should have been compacted in to 1 table resulting in 11 (30-20+1)
+        assertEquals(11, cfs.getLiveSSTables().size());
+
+        // Compact just the tables with key1. At this point all 11 tables should have key1
+        Range<Token> tokenRange2 = new Range<>(key1.getToken(), key1.getToken());
+        Collection<Range<Token>> tokenRanges2 = new ArrayList<>(singleton(tokenRange2));
+        cfs.forceCompactionForTokenRange(tokenRanges2);
+
+        while (CompactionManager.instance.isCompacting(singleton(cfs)))
+        {
+            Thread.sleep(100);
+        }
+
+        // the 11 tables containing key1 should all compact to 1 table
+        assertEquals(1, cfs.getLiveSSTables().size());
+    }
+
+    @Test
+    public void testCompactionCandidateOrdering() throws Exception
+    {
+        // add some data
+        byte [] b = new byte[100 * 1024];
+        new Random().nextBytes(b);
+        ByteBuffer value = ByteBuffer.wrap(b);
+        int rows = 4;
+        int columns = 10;
+        // Just keep sstables in L0 for this test
+        cfs.disableAutoCompaction();
+        for (int r = 0; r < rows; r++)
+        {
+            UpdateBuilder update = UpdateBuilder.create(cfs.metadata, String.valueOf(r));
+            for (int c = 0; c < columns; c++)
+                update.newRow("column" + c).add("val", value);
+            update.applyUnsafe();
+            cfs.forceBlockingFlush();
+        }
+        LeveledCompactionStrategy strategy = (LeveledCompactionStrategy) (cfs.getCompactionStrategyManager()).getStrategies().get(1).get(0);
+        // get readers for level 0 sstables
+        Collection<SSTableReader> sstables = strategy.manifest.getLevel(0);
+        Collection<SSTableReader> sortedCandidates = strategy.manifest.ageSortedSSTables(sstables);
+        assertTrue(String.format("More than 1 sstable required for test, found: %d .", sortedCandidates.size()), sortedCandidates.size() > 1);
+        long lastMaxTimeStamp = Long.MIN_VALUE;
+        for (SSTableReader sstable : sortedCandidates)
+        {
+            assertTrue(String.format("SStables not sorted into oldest to newest by maxTimestamp. Current sstable: %d , last sstable: %d", sstable.getMaxTimestamp(), lastMaxTimeStamp),
+                       sstable.getMaxTimestamp() > lastMaxTimeStamp);
+            lastMaxTimeStamp = sstable.getMaxTimestamp();
+        }
+    }
+
+    @Test
+    public void testAddingOverlapping()
+    {
+        ColumnFamilyStore cfs = MockSchema.newCFS();
+        LeveledManifest lm = new LeveledManifest(cfs, 10, 10, new SizeTieredCompactionStrategyOptions());
+        List<SSTableReader> currentLevel = new ArrayList<>();
+        int gen = 1;
+        currentLevel.add(MockSchema.sstableWithLevel(gen++, 10, 20, 1, cfs));
+        currentLevel.add(MockSchema.sstableWithLevel(gen++, 21, 30, 1, cfs));
+        currentLevel.add(MockSchema.sstableWithLevel(gen++, 51, 100, 1, cfs));
+        currentLevel.add(MockSchema.sstableWithLevel(gen++, 80, 120, 1, cfs));
+        currentLevel.add(MockSchema.sstableWithLevel(gen++, 90, 150, 1, cfs));
+
+        lm.addSSTables(currentLevel);
+        assertLevelsEqual(lm.getLevel(1), currentLevel.subList(0, 3));
+        assertLevelsEqual(lm.getLevel(0), currentLevel.subList(3, 5));
+
+        List<SSTableReader> newSSTables = new ArrayList<>();
+        // this sstable last token is the same as the first token of L1 above, should get sent to L0:
+        newSSTables.add(MockSchema.sstableWithLevel(gen++, 5, 10, 1, cfs));
+        lm.addSSTables(newSSTables);
+        assertLevelsEqual(lm.getLevel(1), currentLevel.subList(0, 3));
+        assertEquals(0, newSSTables.get(0).getSSTableLevel());
+        assertTrue(lm.getLevel(0).containsAll(newSSTables));
+
+        newSSTables.clear();
+        newSSTables.add(MockSchema.sstableWithLevel(gen++, 30, 40, 1, cfs));
+        lm.addSSTables(newSSTables);
+        assertLevelsEqual(lm.getLevel(1), currentLevel.subList(0, 3));
+        assertEquals(0, newSSTables.get(0).getSSTableLevel());
+        assertTrue(lm.getLevel(0).containsAll(newSSTables));
+
+        newSSTables.clear();
+        newSSTables.add(MockSchema.sstableWithLevel(gen++, 100, 140, 1, cfs));
+        lm.addSSTables(newSSTables);
+        assertLevelsEqual(lm.getLevel(1), currentLevel.subList(0, 3));
+        assertEquals(0, newSSTables.get(0).getSSTableLevel());
+        assertTrue(lm.getLevel(0).containsAll(newSSTables));
+
+        newSSTables.clear();
+        newSSTables.add(MockSchema.sstableWithLevel(gen++, 100, 140, 1, cfs));
+        newSSTables.add(MockSchema.sstableWithLevel(gen++, 120, 140, 1, cfs));
+        lm.addSSTables(newSSTables);
+        List<SSTableReader> newL1 = new ArrayList<>(currentLevel.subList(0, 3));
+        newL1.add(newSSTables.get(1));
+        assertLevelsEqual(lm.getLevel(1), newL1);
+        newSSTables.remove(1);
+        assertTrue(newSSTables.stream().allMatch(s -> s.getSSTableLevel() == 0));
+        assertTrue(lm.getLevel(0).containsAll(newSSTables));
+    }
+
+    @Test
+    public void singleTokenSSTableTest()
+    {
+        ColumnFamilyStore cfs = MockSchema.newCFS();
+        LeveledManifest lm = new LeveledManifest(cfs, 10, 10, new SizeTieredCompactionStrategyOptions());
+        List<SSTableReader> expectedL1 = new ArrayList<>();
+
+        int gen = 1;
+        // single sstable, single token (100)
+        expectedL1.add(MockSchema.sstableWithLevel(gen++, 100, 100, 1, cfs));
+        lm.addSSTables(expectedL1);
+
+        List<SSTableReader> expectedL0 = new ArrayList<>();
+
+        // should get moved to L0:
+        expectedL0.add(MockSchema.sstableWithLevel(gen++, 99, 101, 1, cfs));
+        expectedL0.add(MockSchema.sstableWithLevel(gen++, 100, 101, 1, cfs));
+        expectedL0.add(MockSchema.sstableWithLevel(gen++, 99, 100, 1, cfs));
+        expectedL0.add(MockSchema.sstableWithLevel(gen++, 100, 100, 1, cfs));
+        lm.addSSTables(expectedL0);
+
+        assertLevelsEqual(expectedL0, lm.getLevel(0));
+        assertTrue(expectedL0.stream().allMatch(s -> s.getSSTableLevel() == 0));
+        assertLevelsEqual(expectedL1, lm.getLevel(1));
+        assertTrue(expectedL1.stream().allMatch(s -> s.getSSTableLevel() == 1));
+
+        // should work:
+        expectedL1.add(MockSchema.sstableWithLevel(gen++, 98, 99, 1, cfs));
+        expectedL1.add(MockSchema.sstableWithLevel(gen++, 101, 101, 1, cfs));
+        lm.addSSTables(expectedL1.subList(1, expectedL1.size()));
+        assertLevelsEqual(expectedL1, lm.getLevel(1));
+    }
+
+    @Test
+    public void randomMultiLevelAddTest()
+    {
+        int iterations = 100;
+        int levelCount = 9;
+
+        ColumnFamilyStore cfs = MockSchema.newCFS();
+        LeveledManifest lm = new LeveledManifest(cfs, 10, 10, new SizeTieredCompactionStrategyOptions());
+        long seed = System.currentTimeMillis();
+        Random r = new Random(seed);
+        List<SSTableReader> newLevels = generateNewRandomLevels(cfs, 40, levelCount, 0, r);
+
+        int sstableCount = newLevels.size();
+        lm.addSSTables(newLevels);
+
+        int [] expectedLevelSizes = lm.getAllLevelSize();
+
+        for (int j = 0; j < iterations; j++)
+        {
+            newLevels = generateNewRandomLevels(cfs, 20, levelCount, sstableCount, r);
+            sstableCount += newLevels.size();
+
+            int[] canAdd = canAdd(lm, newLevels, levelCount);
+            for (int i = 0; i < levelCount; i++)
+                expectedLevelSizes[i] += canAdd[i];
+            lm.addSSTables(newLevels);
+        }
+
+        // and verify no levels overlap
+        int actualSSTableCount = 0;
+        for (int i = 0; i < levelCount; i++)
+        {
+            actualSSTableCount += lm.getLevelSize(i);
+            List<SSTableReader> level = new ArrayList<>(lm.getLevel(i));
+            int lvl = i;
+            assertTrue(level.stream().allMatch(s -> s.getSSTableLevel() == lvl));
+            if (i > 0)
+            {
+                level.sort(SSTableReader.sstableComparator);
+                SSTableReader prev = null;
+                for (SSTableReader sstable : level)
+                {
+                    if (prev != null && sstable.first.compareTo(prev.last) <= 0)
+                    {
+                        String levelStr = level.stream().map(s -> String.format("[%s, %s]", s.first, s.last)).collect(Collectors.joining(", "));
+                        String overlap = String.format("sstable [%s, %s] overlaps with [%s, %s] in level %d (%s) ", sstable.first, sstable.last, prev.first, prev.last, i, levelStr);
+                        Assert.fail("[seed = "+seed+"] overlap in level "+lvl+": " + overlap);
+                    }
+                    prev = sstable;
+                }
+            }
+        }
+        assertEquals(sstableCount, actualSSTableCount);
+        for (int i = 0; i < levelCount; i++)
+            assertEquals("[seed = " + seed + "] wrong sstable count in level = " + i, expectedLevelSizes[i], lm.getLevel(i).size());
+    }
+
+    private static List<SSTableReader> generateNewRandomLevels(ColumnFamilyStore cfs, int maxSSTableCountPerLevel, int levelCount, int startGen, Random r)
+    {
+        List<SSTableReader> newLevels = new ArrayList<>();
+        for (int level = 0; level < levelCount; level++)
+        {
+            int numLevelSSTables = r.nextInt(maxSSTableCountPerLevel) + 1;
+            List<Integer> tokens = new ArrayList<>(numLevelSSTables * 2);
+
+            for (int i = 0; i < numLevelSSTables * 2; i++)
+                tokens.add(r.nextInt(4000));
+            Collections.sort(tokens);
+            for (int i = 0; i < tokens.size() - 1; i += 2)
+            {
+                SSTableReader sstable = MockSchema.sstableWithLevel(++startGen, tokens.get(i), tokens.get(i + 1), level, cfs);
+                newLevels.add(sstable);
+            }
+        }
+        return newLevels;
+    }
+
+    /**
+     * brute-force checks if the new sstables can be added to the correct level in manifest
+     *
+     * @return count of expected sstables to add to each level
+     */
+    private static int[] canAdd(LeveledManifest lm, List<SSTableReader> newSSTables, int levelCount)
+    {
+        Map<Integer, Collection<SSTableReader>> sstableGroups = new HashMap<>();
+        newSSTables.forEach(s -> sstableGroups.computeIfAbsent(s.getSSTableLevel(), k -> new ArrayList<>()).add(s));
+
+        int[] canAdd = new int[levelCount];
+        for (Map.Entry<Integer, Collection<SSTableReader>> lvlGroup : sstableGroups.entrySet())
+        {
+            int level = lvlGroup.getKey();
+            if (level == 0)
+            {
+                canAdd[0] += lvlGroup.getValue().size();
+                continue;
+            }
+
+            List<SSTableReader> newLevel = new ArrayList<>(lm.getLevel(level));
+            for (SSTableReader sstable : lvlGroup.getValue())
+            {
+                newLevel.add(sstable);
+                newLevel.sort(SSTableReader.sstableComparator);
+
+                SSTableReader prev = null;
+                boolean kept = true;
+                for (SSTableReader sst : newLevel)
+                {
+                    if (prev != null && prev.last.compareTo(sst.first) >= 0)
+                    {
+                        newLevel.remove(sstable);
+                        kept = false;
+                        break;
+                    }
+                    prev = sst;
+                }
+                if (kept)
+                    canAdd[level] += 1;
+                else
+                    canAdd[0] += 1;
+            }
+        }
+        return canAdd;
+    }
+
+    private static void assertLevelsEqual(Collection<SSTableReader> l1, Collection<SSTableReader> l2)
+    {
+        assertEquals(l1.size(), l2.size());
+        assertEquals(new HashSet<>(l1), new HashSet<>(l2));
+    }
+
+    @Test
+    public void testHighestLevelHasMoreDataThanSupported()
+    {
+        ColumnFamilyStore cfs = MockSchema.newCFS();
+        int fanoutSize = 2; // to generate less sstables
+        LeveledManifest lm = new LeveledManifest(cfs, 1, fanoutSize, new SizeTieredCompactionStrategyOptions());
+
+        // generate data for L7 to trigger compaction
+        int l7 = 7;
+        int maxBytesForL7 = (int) (Math.pow(fanoutSize, l7) * 1024 * 1024);
+        int sstablesSizeForL7 = (int) (maxBytesForL7 * 1.001) + 1;
+        List<SSTableReader> sstablesOnL7 = Collections.singletonList(MockSchema.sstableWithLevel( 1, sstablesSizeForL7, l7, cfs));
+        lm.addSSTables(sstablesOnL7);
+
+        // generate data for L8 to trigger compaction
+        int l8 = 8;
+        int maxBytesForL8 = (int) (Math.pow(fanoutSize, l8) * 1024 * 1024);
+        int sstablesSizeForL8 = (int) (maxBytesForL8 * 1.001) + 1;
+        List<SSTableReader> sstablesOnL8 = Collections.singletonList(MockSchema.sstableWithLevel( 2, sstablesSizeForL8, l8, cfs));
+        lm.addSSTables(sstablesOnL8);
+
+        // compaction for L8 sstables is not supposed to be run because there is no upper level to promote sstables
+        // that's why we expect compaction candidates for L7 only
+        Collection<SSTableReader> compactionCandidates = lm.getCompactionCandidates().sstables;
+        assertThat(compactionCandidates).containsAll(sstablesOnL7);
+        assertThat(compactionCandidates).doesNotContainAnyElementsOf(sstablesOnL8);
+    }
+
+    @Test
     public void testReduceScopeL0L1() throws IOException
     {
         ColumnFamilyStore cfs = MockSchema.newCFS();
@@ -399,7 +755,6 @@
         List<SSTableReader> l0sstables = new ArrayList<>();
         for (int i = 10; i < 20; i++)
             l0sstables.add(MockSchema.sstable(i, (i + 1) * 1024 * 1024, cfs));
-
         try (LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.COMPACTION, Iterables.concat(l0sstables, l1sstables)))
         {
             CompactionTask task = new LeveledCompactionTask(cfs, txn, 1, 0, 1024*1024, false);
@@ -409,7 +764,7 @@
             {
                 Set<SSTableReader> before = new HashSet<>(txn.originals());
                 removed = task.reduceScopeForLimitedSpace(0);
-                SSTableReader removedSSTable = Sets.difference(before, txn.originals()).stream().findFirst().orElse(null);
+                SSTableReader removedSSTable = Iterables.getOnlyElement(Sets.difference(before, txn.originals()), null);
                 if (removed)
                 {
                     assertNotNull(removedSSTable);
@@ -506,15 +861,15 @@
     {
         Set<SSTableReader> l1after = new HashSet<>();
         Set<SSTableReader> l0after = new HashSet<>();
-        for (SSTableReader kept : sstables)
+        for (SSTableReader sstable : sstables)
         {
-            switch (kept.getSSTableLevel())
+            switch (sstable.getSSTableLevel())
             {
                 case 0:
-                    l0after.add(kept);
+                    l0after.add(sstable);
                     break;
                 case 1:
-                    l1after.add(kept);
+                    l1after.add(sstable);
                     break;
                 default:
                     throw new RuntimeException("only l0 & l1 sstables");
@@ -522,4 +877,5 @@
         }
         return Pair.create(l0after, l1after);
     }
+
 }
diff --git a/test/unit/org/apache/cassandra/db/compaction/LeveledGenerationsTest.java b/test/unit/org/apache/cassandra/db/compaction/LeveledGenerationsTest.java
new file mode 100644
index 0000000..1f17bf8
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/compaction/LeveledGenerationsTest.java
@@ -0,0 +1,199 @@
+/*
+ * 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.db.compaction;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.MockSchema;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.db.BufferDecoratedKey;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+import static junit.framework.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class LeveledGenerationsTest extends CQLTester
+{
+    @BeforeClass
+    public static void setUp()
+    {
+        DatabaseDescriptor.daemonInitialization();
+        MockSchema.cleanup();
+    }
+
+    @Test
+    public void testWrappingIterable()
+    {
+        ColumnFamilyStore cfs = MockSchema.newCFS();
+
+        LeveledGenerations gens = new LeveledGenerations();
+
+        for (int i = 0; i < 10; i++)
+        {
+            SSTableReader sstable = MockSchema.sstable(i, 5, true, i, i, 2, cfs);
+            gens.addAll(Collections.singleton(sstable));
+        }
+        int gen = 10;
+        assertIter(gens.wrappingIterator(2, sst(++gen, cfs, 5, 5)),
+                   6, 5, 10);
+        assertIter(gens.wrappingIterator(2, null),
+                   0, 9, 10);
+        assertIter(gens.wrappingIterator(2, sst(++gen, cfs, -10, 0)),
+                   1, 0, 10);
+        assertIter(gens.wrappingIterator(2, sst(++gen, cfs, 5, 9)),
+                   0, 9, 10);
+        assertIter(gens.wrappingIterator(2, sst(++gen, cfs, 0, 1000)),
+                   0, 9, 10);
+
+        gens.addAll(Collections.singleton(MockSchema.sstable(100, 5, true, 5, 10, 3, cfs)));
+        assertIter(gens.wrappingIterator(3, sst(++gen, cfs, -10, 0)),
+                   5, 5, 1);
+        assertIter(gens.wrappingIterator(3, sst(++gen, cfs, 0, 100)),
+                   5, 5, 1);
+
+        gens.addAll(Collections.singleton(MockSchema.sstable(200, 5, true, 5, 10, 4, cfs)));
+        gens.addAll(Collections.singleton(MockSchema.sstable(201, 5, true, 40, 50, 4, cfs)));
+        assertIter(gens.wrappingIterator(4, sst(++gen, cfs, 0, 0)),
+                   5, 40, 2);
+        assertIter(gens.wrappingIterator(4, sst(++gen, cfs, 0, 5)),
+                   40, 5, 2);
+        assertIter(gens.wrappingIterator(4, sst(++gen, cfs, 7, 8)),
+                   40, 5, 2);
+        assertIter(gens.wrappingIterator(4, sst(++gen, cfs, 39, 39)),
+                   40, 5, 2);
+        assertIter(gens.wrappingIterator(4, sst(++gen, cfs, 40, 40)),
+                   5, 40, 2);
+        assertIter(gens.wrappingIterator(4, sst(++gen, cfs, 100, 1000)),
+                   5, 40, 2);
+    }
+
+    @Test
+    public void testWrappingIterableWiderSSTables()
+    {
+        ColumnFamilyStore cfs = MockSchema.newCFS();
+        LeveledGenerations generations = new LeveledGenerations();
+        int gen = 0;
+        generations.addAll(Lists.newArrayList(
+            sst(++gen, cfs, 0, 50),
+            sst(++gen, cfs, 51, 100),
+            sst(++gen, cfs, 150, 200)));
+
+        assertIter(generations.wrappingIterator(2, sst(++gen, cfs, -100, -50)),
+                   0, 150, 3);
+
+        assertIter(generations.wrappingIterator(2, sst(++gen, cfs, 0, 40)),
+                   51, 0, 3);
+        assertIter(generations.wrappingIterator(2, sst(++gen, cfs, 0, 50)),
+                   51, 0, 3);
+        assertIter(generations.wrappingIterator(2, sst(++gen, cfs, 0, 51)),
+                   150, 51, 3);
+
+        assertIter(generations.wrappingIterator(2, sst(++gen, cfs, 100, 149)),
+                   150, 51, 3);
+        assertIter(generations.wrappingIterator(2, sst(++gen, cfs, 100, 300)),
+                   0, 150, 3);
+
+    }
+
+    @Test
+    public void testEmptyLevel()
+    {
+        ColumnFamilyStore cfs = MockSchema.newCFS();
+        LeveledGenerations generations = new LeveledGenerations();
+        assertFalse(generations.wrappingIterator(3, sst(0, cfs, 0, 10)).hasNext());
+        assertFalse(generations.wrappingIterator(3, null).hasNext());
+    }
+
+    @Test
+    public void testFillLevels()
+    {
+        LeveledGenerations generations = new LeveledGenerations();
+
+        ColumnFamilyStore cfs = MockSchema.newCFS();
+        for (int i = 0; i < LeveledGenerations.MAX_LEVEL_COUNT; i++)
+            generations.addAll(Collections.singleton(MockSchema.sstableWithLevel(i, i, i, i, cfs)));
+
+        for (int i = 0; i < generations.levelCount(); i++)
+            assertEquals(i, generations.get(i).iterator().next().getSSTableLevel());
+
+        assertEquals(9, generations.levelCount());
+
+        try
+        {
+            generations.get(9);
+            fail("don't have 9 generations");
+        }
+        catch (ArrayIndexOutOfBoundsException e)
+        {}
+        try
+        {
+            generations.get(-1);
+            fail("don't have -1 generations");
+        }
+        catch (ArrayIndexOutOfBoundsException e)
+        {}
+    }
+
+    private void assertIter(Iterator<SSTableReader> iter, long first, long last, int expectedCount)
+    {
+        List<SSTableReader> drained = Lists.newArrayList(iter);
+        assertEquals(expectedCount, drained.size());
+        assertEquals(dk(first).getToken(), first(drained).first.getToken());
+        assertEquals(dk(last).getToken(), last(drained).first.getToken()); // we sort by first token, so this is the first token of the last sstable in iter
+    }
+
+    private SSTableReader last(Iterable<SSTableReader> iter)
+    {
+        return Iterables.getLast(iter);
+    }
+    private SSTableReader first(Iterable<SSTableReader> iter)
+    {
+        SSTableReader first = Iterables.getFirst(iter, null);
+        if (first == null)
+            throw new RuntimeException();
+        return first;
+    }
+
+    private DecoratedKey dk(long x)
+    {
+        return new BufferDecoratedKey(new Murmur3Partitioner.LongToken(x), ByteBufferUtil.bytes(x));
+    }
+    private SSTableReader sst(int gen, ColumnFamilyStore cfs, long first, long last)
+    {
+        return MockSchema.sstable(gen, 5, true, first, last, 2, cfs);
+    }
+
+    private void print(SSTableReader sstable)
+    {
+        System.out.println(String.format("%d %s %s %d", sstable.descriptor.generation, sstable.first, sstable.last, sstable.getSSTableLevel()));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/compaction/TTLExpiryTest.java b/test/unit/org/apache/cassandra/db/compaction/TTLExpiryTest.java
index e0378f6..9fafc74 100644
--- a/test/unit/org/apache/cassandra/db/compaction/TTLExpiryTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/TTLExpiryTest.java
@@ -176,7 +176,7 @@
 
         new RowUpdateBuilder(cfs.metadata, timestamp, 1, key)
             .add("col2", ByteBufferUtil.EMPTY_BYTE_BUFFER)
-            .addMapEntry("col8", "bar", "foo")
+            .add("col8", Collections.singletonMap("bar", "foo"))
             .delete("col1")
             .build()
             .applyUnsafe();
diff --git a/test/unit/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategyTest.java b/test/unit/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategyTest.java
index 9bed7c1..15d2a2e 100644
--- a/test/unit/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategyTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategyTest.java
@@ -29,7 +29,6 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 
-
 import org.junit.BeforeClass;
 import org.junit.Test;
 import static org.junit.Assert.assertEquals;
@@ -48,8 +47,8 @@
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.schema.KeyspaceParams;
-import org.apache.cassandra.utils.Pair;
 import org.apache.cassandra.MockSchema;
+import org.apache.cassandra.utils.Pair;
 
 import static org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy.getWindowBoundsInMillis;
 import static org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy.newestBucket;
@@ -67,6 +66,7 @@
     {
         // Disable tombstone histogram rounding for tests
         System.setProperty("cassandra.streaminghistogram.roundseconds", "1");
+        System.setProperty(TimeWindowCompactionStrategyOptions.UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_PROPERTY, "true");
 
         SchemaLoader.prepareServer();
 
@@ -117,6 +117,26 @@
             options.put(TimeWindowCompactionStrategyOptions.COMPACTION_WINDOW_UNIT_KEY, "MINUTES");
         }
 
+        try
+        {
+            options.put(TimeWindowCompactionStrategyOptions.UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_KEY, "not-a-boolean");
+            validateOptions(options);
+            fail(String.format("Invalid %s should be rejected", TimeWindowCompactionStrategyOptions.UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_KEY));
+        }
+        catch (ConfigurationException e)
+        {
+            options.put(TimeWindowCompactionStrategyOptions.UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_KEY, "true");
+        }
+        
+        options.put(AbstractCompactionStrategy.UNCHECKED_TOMBSTONE_COMPACTION_OPTION, "true");
+        Keyspace keyspace = Keyspace.open(KEYSPACE1);
+        ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF_STANDARD1);
+        TimeWindowCompactionStrategy twcs = new TimeWindowCompactionStrategy(cfs, options);
+        assertFalse(twcs.disableTombstoneCompactions);
+        options.put(AbstractCompactionStrategy.UNCHECKED_TOMBSTONE_COMPACTION_OPTION, "false");
+        twcs = new TimeWindowCompactionStrategy(cfs, options);
+        assertTrue(twcs.disableTombstoneCompactions);
+
         options.put("bad_option", "1.0");
         unvalidated = validateOptions(options);
         assertTrue(unvalidated.containsKey("bad_option"));
@@ -184,19 +204,23 @@
         // We'll put 3 sstables into the newest bucket
         for (int i = 0; i < 3; i++)
         {
-            Pair<Long, Long> bounds = getWindowBoundsInMillis(TimeUnit.HOURS, 1, tstamp );
+            Pair<Long, Long> bounds = getWindowBoundsInMillis(TimeUnit.HOURS, 1, tstamp);
             buckets.put(bounds.left, sstrs.get(i));
         }
-        List<SSTableReader> newBucket = newestBucket(buckets, 4, 32, TimeUnit.HOURS, 1, new SizeTieredCompactionStrategyOptions(), getWindowBoundsInMillis(TimeUnit.HOURS, 1, System.currentTimeMillis()).left );
-        assertTrue("incoming bucket should not be accepted when it has below the min threshold SSTables", newBucket.isEmpty());
 
-        newBucket = newestBucket(buckets, 2, 32, TimeUnit.HOURS, 1, new SizeTieredCompactionStrategyOptions(), getWindowBoundsInMillis(TimeUnit.HOURS, 1, System.currentTimeMillis()).left);
-        assertFalse("incoming bucket should be accepted when it is larger than the min threshold SSTables", newBucket.isEmpty());
+        TimeWindowCompactionStrategy.NewestBucket newBucket = newestBucket(buckets, 4, 32, new SizeTieredCompactionStrategyOptions(), getWindowBoundsInMillis(TimeUnit.HOURS, 1, System.currentTimeMillis()).left);
+        assertTrue("incoming bucket should not be accepted when it has below the min threshold SSTables", newBucket.sstables.isEmpty());
+        assertEquals("there should be no estimated remaining tasks when bucket is below min threshold SSTables", 0, newBucket.estimatedRemainingTasks);
+
+
+        newBucket = newestBucket(buckets, 2, 32, new SizeTieredCompactionStrategyOptions(), getWindowBoundsInMillis(TimeUnit.HOURS, 1, System.currentTimeMillis()).left);
+        assertFalse("incoming bucket should be accepted when it is larger than the min threshold SSTables", newBucket.sstables.isEmpty());
+        assertEquals("there should be one estimated remaining task when bucket is larger than the min threshold SSTables", 1, newBucket.estimatedRemainingTasks);
 
         // And 2 into the second bucket (1 hour back)
         for (int i = 3; i < 5; i++)
         {
-            Pair<Long, Long> bounds = getWindowBoundsInMillis(TimeUnit.HOURS, 1, tstamp2 );
+            Pair<Long, Long> bounds = getWindowBoundsInMillis(TimeUnit.HOURS, 1, tstamp2);
             buckets.put(bounds.left, sstrs.get(i));
         }
 
@@ -226,8 +250,11 @@
             buckets.put(bounds.left, sstrs.get(i));
         }
 
-        newBucket = newestBucket(buckets, 4, 32, TimeUnit.DAYS, 1, new SizeTieredCompactionStrategyOptions(), getWindowBoundsInMillis(TimeUnit.HOURS, 1, System.currentTimeMillis()).left);
-        assertEquals("new bucket should be trimmed to max threshold of 32", newBucket.size(),  32);
+        newBucket = newestBucket(buckets, 4, 32, new SizeTieredCompactionStrategyOptions(), getWindowBoundsInMillis(TimeUnit.HOURS, 1, System.currentTimeMillis()).left);
+        assertEquals("new bucket should be trimmed to max threshold of 32", newBucket.sstables.size(), 32);
+
+        // one per bucket because they are all eligible and one more for the sstables that were trimmed
+        assertEquals("there should be one estimated remaining task per eligible bucket", buckets.keySet().size() + 1, newBucket.estimatedRemainingTasks);
     }
 
 
@@ -283,6 +310,70 @@
     }
 
     @Test
+    public void testDropOverlappingExpiredSSTables() throws InterruptedException
+    {
+        Keyspace keyspace = Keyspace.open(KEYSPACE1);
+        ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF_STANDARD1);
+        cfs.truncateBlocking();
+        cfs.disableAutoCompaction();
+
+        long timestamp = System.currentTimeMillis();
+        ByteBuffer value = ByteBuffer.wrap(new byte[100]);
+
+        // Create a expiring sstable with a TTL
+        DecoratedKey key = Util.dk("expired");
+        new RowUpdateBuilder(cfs.metadata, timestamp, TTL_SECONDS, key.getKey())
+            .clustering("column")
+            .add("val", value).build().applyUnsafe();
+
+        cfs.forceBlockingFlush();
+        SSTableReader expiredSSTable = cfs.getLiveSSTables().iterator().next();
+        Thread.sleep(10);
+
+        // Create a second sstable without TTL and with a row superceded by the expiring row
+        new RowUpdateBuilder(cfs.metadata, timestamp - 1000, key.getKey())
+            .clustering("column")
+            .add("val", value).build().applyUnsafe();
+        key = Util.dk("nonexpired");
+        new RowUpdateBuilder(cfs.metadata, timestamp, key.getKey())
+            .clustering("column")
+            .add("val", value).build().applyUnsafe();
+
+        cfs.forceBlockingFlush();
+        assertEquals(cfs.getLiveSSTables().size(), 2);
+
+        Map<String, String> options = new HashMap<>();
+        options.put(TimeWindowCompactionStrategyOptions.COMPACTION_WINDOW_SIZE_KEY, "30");
+        options.put(TimeWindowCompactionStrategyOptions.COMPACTION_WINDOW_UNIT_KEY, "SECONDS");
+        options.put(TimeWindowCompactionStrategyOptions.TIMESTAMP_RESOLUTION_KEY, "MILLISECONDS");
+        options.put(TimeWindowCompactionStrategyOptions.EXPIRED_SSTABLE_CHECK_FREQUENCY_SECONDS_KEY, "0");
+        TimeWindowCompactionStrategy twcs = new TimeWindowCompactionStrategy(cfs, options);
+        for (SSTableReader sstable : cfs.getLiveSSTables())
+            twcs.addSSTable(sstable);
+
+        twcs.startup();
+        assertNull(twcs.getNextBackgroundTask(nowInSeconds()));
+
+        // Wait for the expiration of the first sstable
+        Thread.sleep(TimeUnit.SECONDS.toMillis(TTL_SECONDS + 1));
+        assertNull(twcs.getNextBackgroundTask(nowInSeconds()));
+
+        options.put(TimeWindowCompactionStrategyOptions.UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION_KEY, "true");
+        twcs = new TimeWindowCompactionStrategy(cfs, options);
+        for (SSTableReader sstable : cfs.getLiveSSTables())
+            twcs.addSSTable(sstable);
+
+        twcs.startup();
+        AbstractCompactionTask t = twcs.getNextBackgroundTask(nowInSeconds());
+        assertNotNull(t);
+        assertEquals(1, Iterables.size(t.transaction.originals()));
+        SSTableReader sstable = t.transaction.originals().iterator().next();
+        assertEquals(sstable, expiredSSTable);
+        twcs.shutdown();
+        t.transaction.abort();
+    }
+
+    @Test
     public void testGroupForAntiCompaction()
     {
         ColumnFamilyStore cfs = MockSchema.newCFS("test_group_for_anticompaction");
@@ -297,7 +388,7 @@
             sstables.add(MockSchema.sstableWithTimestamp(i, curr + TimeUnit.MILLISECONDS.convert(i, TimeUnit.MINUTES), cfs));
 
         cfs.addSSTables(sstables);
-        Collection<Collection<SSTableReader>> groups = cfs.getCompactionStrategyManager().getStrategies().get(1).groupSSTablesForAntiCompaction(sstables);
+        Collection<Collection<SSTableReader>> groups = cfs.getCompactionStrategyManager().getCompactionStrategyFor(sstables.get(0)).groupSSTablesForAntiCompaction(sstables);
         assertTrue(groups.size() > 0);
         for (Collection<SSTableReader> group : groups)
             assertEquals(1, group.size());
diff --git a/test/unit/org/apache/cassandra/db/context/CounterContextTest.java b/test/unit/org/apache/cassandra/db/context/CounterContextTest.java
index a8852f7..6994046 100644
--- a/test/unit/org/apache/cassandra/db/context/CounterContextTest.java
+++ b/test/unit/org/apache/cassandra/db/context/CounterContextTest.java
@@ -55,7 +55,7 @@
     @BeforeClass
     public static void setupDD()
     {
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
     }
 
     @Test
diff --git a/test/unit/org/apache/cassandra/db/filter/ColumnFilterTest.java b/test/unit/org/apache/cassandra/db/filter/ColumnFilterTest.java
index 7977c6b..adf074f 100644
--- a/test/unit/org/apache/cassandra/db/filter/ColumnFilterTest.java
+++ b/test/unit/org/apache/cassandra/db/filter/ColumnFilterTest.java
@@ -19,20 +19,28 @@
 package org.apache.cassandra.db.filter;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.function.Consumer;
 
 import com.google.common.base.Throwables;
 import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.PartitionColumns;
 import org.apache.cassandra.db.marshal.Int32Type;
 import org.apache.cassandra.db.marshal.SetType;
 import org.apache.cassandra.db.rows.CellPath;
 import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.gms.Gossiper;
 import org.apache.cassandra.io.util.DataInputBuffer;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputBuffer;
@@ -41,6 +49,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+@RunWith(Parameterized.class)
 public class ColumnFilterTest
 {
     private static final ColumnFilter.Serializer serializer = new ColumnFilter.Serializer();
@@ -67,6 +76,27 @@
     private final CellPath path3 = CellPath.create(ByteBufferUtil.bytes(3));
     private final CellPath path4 = CellPath.create(ByteBufferUtil.bytes(4));
 
+    @Parameterized.Parameter
+    public boolean anyNodeOn30;
+
+    @Parameterized.Parameters(name = "{index}: anyNodeOn30={0}")
+    public static Collection<Object[]> data()
+    {
+        return Arrays.asList(new Object[]{ true }, new Object[]{ false });
+    }
+
+    @BeforeClass
+    public static void beforeClass()
+    {
+        DatabaseDescriptor.clientInitialization();
+    }
+
+    @Before
+    public void before()
+    {
+        Gossiper.instance.setAnyNodeOn30(anyNodeOn30);
+    }
+
     // Select all
 
     @Test
@@ -289,12 +319,22 @@
         Consumer<ColumnFilter> check = filter -> {
             testRoundTrips(filter);
             assertFetchedQueried(true, true, filter, v1);
-
-            assertEquals("*/*", filter.toString());
-            assertEquals("v1", filter.toCQLString());
-            assertFetchedQueried(true, true, filter, s1, s2, v2);
-            assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path3, path4);
-            assertCellFetchedQueried(true, true, filter, s2, path0, path1, path2, path3, path4);
+            if (anyNodeOn30)
+            {
+                assertEquals("*/*", filter.toString());
+                assertEquals("*", filter.toCQLString());
+                assertFetchedQueried(true, true, filter, s1, s2, v2);
+                assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path3, path4);
+                assertCellFetchedQueried(true, true, filter, s2, path0, path1, path2, path3, path4);
+            }
+            else
+            {
+                assertEquals("*/[v1]", filter.toString());
+                assertEquals("v1", filter.toCQLString());
+                assertFetchedQueried(true, false, filter, s1, s2, v2);
+                assertCellFetchedQueried(true, false, filter, v2, path0, path1, path2, path3, path4);
+                assertCellFetchedQueried(true, false, filter, s2, path0, path1, path2, path3, path4);
+            }
         };
 
         check.accept(ColumnFilter.selection(metadata, PartitionColumns.builder().add(v1).build()));
@@ -307,12 +347,22 @@
         Consumer<ColumnFilter> check = filter -> {
             testRoundTrips(filter);
             assertFetchedQueried(true, true, filter, s1);
-
-            assertEquals("*/*", filter.toString());
-            assertEquals("s1", filter.toCQLString());
-            assertFetchedQueried(true, true, filter, v1, v2, s2);
-            assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path3, path4);
-            assertCellFetchedQueried(true, true, filter, s2, path0, path1, path2, path3, path4);
+            if (anyNodeOn30)
+            {
+                assertEquals("*/*", filter.toString());
+                assertEquals("*", filter.toCQLString());
+                assertFetchedQueried(true, true, filter, v1, v2, s2);
+                assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path3, path4);
+                assertCellFetchedQueried(true, true, filter, s2, path0, path1, path2, path3, path4);
+            }
+            else
+            {
+                assertEquals("*/[s1]", filter.toString());
+                assertEquals("s1", filter.toCQLString());
+                assertFetchedQueried(true, false, filter, v1, v2, s2);
+                assertCellFetchedQueried(true, false, filter, v2, path0, path1, path2, path3, path4);
+                assertCellFetchedQueried(false, false, filter, s2, path0, path1, path2, path3, path4);
+            }
         };
 
         check.accept(ColumnFilter.selection(metadata, PartitionColumns.builder().add(s1).build()));
@@ -325,13 +375,24 @@
         ColumnFilter filter = ColumnFilter.allColumnsBuilder(metadata).select(v2, path1).build();
         testRoundTrips(filter);
         assertFetchedQueried(true, true, filter, v2);
-
-        assertEquals("*/*", filter.toString());
-        assertEquals("v2[1]", filter.toCQLString());
-        assertFetchedQueried(true, true, filter, s1, s2, v1);
-        assertCellFetchedQueried(true, true, filter, v2, path1);
-        assertCellFetchedQueried(true, false, filter, v2, path0, path2, path3, path4);
-        assertCellFetchedQueried(true, true, filter, s2, path0, path1, path2, path3, path4);
+        if (anyNodeOn30)
+        {
+            assertEquals("*/*", filter.toString());
+            assertEquals("*", filter.toCQLString());
+            assertFetchedQueried(true, true, filter, s1, s2, v1);
+            assertCellFetchedQueried(true, true, filter, v2, path1);
+            assertCellFetchedQueried(true, false, filter, v2, path0, path2, path3, path4);
+            assertCellFetchedQueried(true, true, filter, s2, path0, path1, path2, path3, path4);
+        }
+        else
+        {
+            assertEquals("*/[v2[1]]", filter.toString());
+            assertEquals("v2[1]", filter.toCQLString());
+            assertFetchedQueried(true, false, filter, s1, s2, v1);
+            assertCellFetchedQueried(true, true, filter, v2, path1);
+            assertCellFetchedQueried(true, false, filter, v2, path0, path2, path3, path4);
+            assertCellFetchedQueried(true, false, filter, s2, path0, path1, path2, path3, path4);
+        }
     }
 
     @Test
@@ -340,13 +401,24 @@
         ColumnFilter filter = ColumnFilter.allColumnsBuilder(metadata).select(s2, path1).build();
         testRoundTrips(filter);
         assertFetchedQueried(true, true, filter, s2);
-
-        assertEquals("*/*", filter.toString());
-        assertEquals("s2[1]", filter.toCQLString());
-        assertFetchedQueried(true, true, filter, v1, v2, s1);
-        assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path3, path4);
-        assertCellFetchedQueried(true, true, filter, s2, path1);
-        assertCellFetchedQueried(true, false, filter, s2, path0, path2, path3, path4);
+        if (anyNodeOn30)
+        {
+            assertEquals("*/*", filter.toString());
+            assertEquals("*", filter.toCQLString());
+            assertFetchedQueried(true, true, filter, v1, v2, s1);
+            assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path3, path4);
+            assertCellFetchedQueried(true, true, filter, s2, path1);
+            assertCellFetchedQueried(true, false, filter, s2, path0, path2, path3, path4);
+        }
+        else
+        {
+            assertEquals("*/[s2[1]]", filter.toString());
+            assertEquals("s2[1]", filter.toCQLString());
+            assertFetchedQueried(true, false, filter, v1, v2, s1);
+            assertCellFetchedQueried(true, false, filter, v2, path0, path1, path2, path3, path4);
+            assertCellFetchedQueried(true, true, filter, s2, path1);
+            assertCellFetchedQueried(true, false, filter, s2, path0, path2, path3, path4);
+        }
     }
 
     private void testRoundTrips(ColumnFilter cf)
@@ -372,23 +444,23 @@
         }
     }
 
-    private static void assertFetchedQueried(boolean expectedIncluded,
-                                             boolean expectedNotSkipped,
+    private static void assertFetchedQueried(boolean expectedFetched,
+                                             boolean expectedQueried,
                                              ColumnFilter filter,
                                              ColumnDefinition... columns)
     {
         for (ColumnDefinition column : columns)
         {
-            assertEquals(String.format("Expected includes(%s) to be %s", column.name, expectedIncluded),
-                         expectedIncluded, filter.includes(column));
-            if (expectedIncluded)
-                assertEquals(String.format("Expected canSkipValue(%s) to be %s", column.name, !expectedNotSkipped),
-                             !expectedNotSkipped, filter.canSkipValue(column));
+            assertEquals(String.format("Expected fetches(%s) to be %s", column.name, expectedFetched),
+                         expectedFetched, filter.fetches(column));
+            if (expectedFetched)
+                assertEquals(String.format("Expected fetchedColumnIsQueried(%s) to be %s", column.name, expectedQueried),
+                             expectedQueried, filter.fetchedColumnIsQueried(column));
         }
     }
 
-    private static void assertCellFetchedQueried(boolean expectedIncluded,
-                                                 boolean expectedNotSkipped,
+    private static void assertCellFetchedQueried(boolean expectedFetched,
+                                                 boolean expectedQueried,
                                                  ColumnFilter filter,
                                                  ColumnDefinition column,
                                                  CellPath... paths)
@@ -398,14 +470,17 @@
         for (CellPath path : paths)
         {
             int p = ByteBufferUtil.toInt(path.get(0));
+            if (expectedFetched)
+                assertEquals(String.format("Expected fetchedCellIsQueried(%s:%s) to be %s", column.name, p, expectedQueried),
+                             expectedQueried, filter.fetchedCellIsQueried(column, path));
 
             if (tester != null)
             {
-                assertEquals(String.format("Expected tester.includes(%s:%s) to be %s", column.name, p, expectedIncluded),
-                             expectedIncluded, tester.includes(path));
-                if (expectedIncluded)
-                    assertEquals(String.format("Expected tester.canSkipValue(%s:%s) to be %s", column.name, p, !expectedNotSkipped),
-                                 !expectedNotSkipped, tester.canSkipValue(path));
+                assertEquals(String.format("Expected tester.fetches(%s:%s) to be %s", column.name, p, expectedFetched),
+                             expectedFetched, tester.fetches(path));
+                if (expectedFetched)
+                    assertEquals(String.format("Expected tester.fetchedCellIsQueried(%s:%s) to be %s", column.name, p, expectedQueried),
+                                 expectedQueried, tester.fetchedCellIsQueried(path));
             }
         }
     }
diff --git a/test/unit/org/apache/cassandra/db/filter/RowFilterTest.java b/test/unit/org/apache/cassandra/db/filter/RowFilterTest.java
index 0e15013..9313c3a 100644
--- a/test/unit/org/apache/cassandra/db/filter/RowFilterTest.java
+++ b/test/unit/org/apache/cassandra/db/filter/RowFilterTest.java
@@ -25,6 +25,7 @@
 import org.junit.Assert;
 import org.junit.Test;
 
+import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.ColumnIdentifier;
@@ -54,6 +55,7 @@
     public void testCQLFilterClose()
     {
         // CASSANDRA-15126
+        SchemaLoader.prepareServer();
         CFMetaData metadata = CFMetaData.Builder.create("testks", "testcf")
                                                 .addPartitionKey("pk", Int32Type.instance)
                                                 .addStaticColumn("s", Int32Type.instance)
diff --git a/test/unit/org/apache/cassandra/db/filter/SliceTest.java b/test/unit/org/apache/cassandra/db/filter/SliceTest.java
index 606395c..9188c94 100644
--- a/test/unit/org/apache/cassandra/db/filter/SliceTest.java
+++ b/test/unit/org/apache/cassandra/db/filter/SliceTest.java
@@ -19,12 +19,10 @@
 package org.apache.cassandra.db.filter;
 
 
-import java.awt.*;
 import java.nio.ByteBuffer;
 import java.util.*;
 import java.util.List;
 
-import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.db.*;
 import org.junit.Test;
 
@@ -325,14 +323,14 @@
         assertSlicesNormalization(cc, slices(s(-1, 2), s(-1, 3), s(5, 9)), slices(s(-1, 3), s(5, 9)));
     }
 
-    private static Slice.Bound makeBound(ClusteringPrefix.Kind kind, Integer... components)
+    private static ClusteringBound makeBound(ClusteringPrefix.Kind kind, Integer... components)
     {
         ByteBuffer[] values = new ByteBuffer[components.length];
         for (int i = 0; i < components.length; i++)
         {
             values[i] = ByteBufferUtil.bytes(components[i]);
         }
-        return Slice.Bound.create(kind, values);
+        return ClusteringBound.create(kind, values);
     }
 
     private static List<ByteBuffer> columnNames(Integer ... components)
diff --git a/test/unit/org/apache/cassandra/db/lifecycle/HelpersTest.java b/test/unit/org/apache/cassandra/db/lifecycle/HelpersTest.java
index 1d9f8aa..6aa7bc4 100644
--- a/test/unit/org/apache/cassandra/db/lifecycle/HelpersTest.java
+++ b/test/unit/org/apache/cassandra/db/lifecycle/HelpersTest.java
@@ -32,6 +32,7 @@
 
 import junit.framework.Assert;
 import org.apache.cassandra.MockSchema;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
@@ -46,6 +47,7 @@
     @BeforeClass
     public static void setUp()
     {
+        DatabaseDescriptor.daemonInitialization();
         MockSchema.cleanup();
     }
 
diff --git a/test/unit/org/apache/cassandra/db/lifecycle/LifecycleTransactionTest.java b/test/unit/org/apache/cassandra/db/lifecycle/LifecycleTransactionTest.java
index 0d87cc9..4514b72 100644
--- a/test/unit/org/apache/cassandra/db/lifecycle/LifecycleTransactionTest.java
+++ b/test/unit/org/apache/cassandra/db/lifecycle/LifecycleTransactionTest.java
@@ -32,7 +32,7 @@
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Memtable;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction.ReaderState;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction.ReaderState.Action;
@@ -271,7 +271,7 @@
 
         private static Tracker tracker(ColumnFamilyStore cfs, List<SSTableReader> readers)
         {
-            Tracker tracker = new Tracker(new Memtable(new AtomicReference<>(ReplayPosition.NONE), cfs), false);
+            Tracker tracker = new Tracker(new Memtable(new AtomicReference<>(CommitLogPosition.NONE), cfs), false);
             tracker.addInitialSSTables(readers);
             return tracker;
         }
diff --git a/test/unit/org/apache/cassandra/db/lifecycle/LogTransactionTest.java b/test/unit/org/apache/cassandra/db/lifecycle/LogTransactionTest.java
index 6fb2334..09c75e1 100644
--- a/test/unit/org/apache/cassandra/db/lifecycle/LogTransactionTest.java
+++ b/test/unit/org/apache/cassandra/db/lifecycle/LogTransactionTest.java
@@ -47,15 +47,13 @@
 import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
 import org.apache.cassandra.io.sstable.metadata.MetadataType;
 import org.apache.cassandra.io.sstable.metadata.StatsMetadata;
-import org.apache.cassandra.io.util.BufferedSegmentedFile;
-import org.apache.cassandra.io.util.ChannelProxy;
+import org.apache.cassandra.io.util.FileHandle;
 import org.apache.cassandra.io.util.FileUtils;
-import org.apache.cassandra.io.util.RandomAccessReader;
-import org.apache.cassandra.io.util.SegmentedFile;
 import org.apache.cassandra.utils.AlwaysPresentFilter;
 import org.apache.cassandra.utils.concurrent.AbstractTransactionalTest;
 import org.apache.cassandra.utils.concurrent.Transactional;
@@ -560,7 +558,7 @@
                             getTemporaryFiles(dataFolder2));
 
         // normally called at startup
-        LogTransaction.removeUnfinishedLeftovers(Arrays.asList(dataFolder1, dataFolder2));
+        assertTrue(LogTransaction.removeUnfinishedLeftovers(Arrays.asList(dataFolder1, dataFolder2)));
 
         // new tables should be only table left
         assertFiles(dataFolder1.getPath(), new HashSet<>(sstables[1].getAllFilePaths()));
@@ -611,7 +609,7 @@
                             getTemporaryFiles(dataFolder2));
 
         // normally called at startup
-        LogTransaction.removeUnfinishedLeftovers(Arrays.asList(dataFolder1, dataFolder2));
+        assertTrue(LogTransaction.removeUnfinishedLeftovers(Arrays.asList(dataFolder1, dataFolder2)));
 
         // old tables should be only table left
         assertFiles(dataFolder1.getPath(), new HashSet<>(sstables[0].getAllFilePaths()));
@@ -776,7 +774,8 @@
 
         Arrays.stream(sstables).forEach(s -> s.selfRef().release());
 
-        LogTransaction.removeUnfinishedLeftovers(Arrays.asList(dataFolder1, dataFolder2));
+        // if shouldCommit is true then it should remove the leftovers and return true, false otherwise
+        assertEquals(shouldCommit, LogTransaction.removeUnfinishedLeftovers(Arrays.asList(dataFolder1, dataFolder2)));
         LogTransaction.waitForDeletions();
 
         if (shouldCommit)
@@ -969,7 +968,7 @@
                                   if (filePath.endsWith("Data.db"))
                                   {
                                       assertTrue(FileUtils.delete(filePath));
-                                      assertNull(t.txnFile().syncFolder(null));
+                                      assertNull(t.txnFile().syncDirectory(null));
                                       break;
                                   }
                               }
@@ -1050,7 +1049,7 @@
 
         // Sync the folder to make sure that later on removeUnfinishedLeftovers picks up
         // any changes to the txn files done by the modifier
-        assertNull(log.txnFile().syncFolder(null));
+        assertNull(log.txnFile().syncDirectory(null));
 
         assertNull(log.complete(null));
 
@@ -1258,7 +1257,7 @@
 
     private static SSTableReader sstable(File dataFolder, ColumnFamilyStore cfs, int generation, int size) throws IOException
     {
-        Descriptor descriptor = new Descriptor(dataFolder, cfs.keyspace.getName(), cfs.getTableName(), generation);
+        Descriptor descriptor = new Descriptor(dataFolder, cfs.keyspace.getName(), cfs.getTableName(), generation, SSTableFormat.Type.BIG);
         Set<Component> components = ImmutableSet.of(Component.DATA, Component.PRIMARY_INDEX, Component.FILTER, Component.TOC);
         for (Component component : components)
         {
@@ -1271,8 +1270,8 @@
             }
         }
 
-        SegmentedFile dFile = new BufferedSegmentedFile(new ChannelProxy(new File(descriptor.filenameFor(Component.DATA))), RandomAccessReader.DEFAULT_BUFFER_SIZE, 0);
-        SegmentedFile iFile = new BufferedSegmentedFile(new ChannelProxy(new File(descriptor.filenameFor(Component.PRIMARY_INDEX))), RandomAccessReader.DEFAULT_BUFFER_SIZE, 0);
+        FileHandle dFile = new FileHandle.Builder(descriptor.filenameFor(Component.DATA)).complete();
+        FileHandle iFile = new FileHandle.Builder(descriptor.filenameFor(Component.PRIMARY_INDEX)).complete();
 
         SerializationHeader header = SerializationHeader.make(cfs.metadata, Collections.emptyList());
         StatsMetadata metadata = (StatsMetadata) new MetadataCollector(cfs.metadata.comparator)
diff --git a/test/unit/org/apache/cassandra/db/lifecycle/RealTransactionsTest.java b/test/unit/org/apache/cassandra/db/lifecycle/RealTransactionsTest.java
index 4fbbb36..595610e 100644
--- a/test/unit/org/apache/cassandra/db/lifecycle/RealTransactionsTest.java
+++ b/test/unit/org/apache/cassandra/db/lifecycle/RealTransactionsTest.java
@@ -153,7 +153,7 @@
         int nowInSec = FBUtilities.nowInSeconds();
         try (CompactionController controller = new CompactionController(cfs, txn.originals(), cfs.gcBefore(FBUtilities.nowInSeconds())))
         {
-            try (SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, false);
+            try (SSTableRewriter rewriter = SSTableRewriter.constructKeepingOriginals(txn, false, 1000);
                  AbstractCompactionStrategy.ScannerList scanners = cfs.getCompactionStrategyManager().getScanners(txn.originals());
                  CompactionIterator ci = new CompactionIterator(txn.opType(), scanners.scanners, controller, nowInSec, txn.opId())
             )
@@ -168,6 +168,7 @@
                                                            0,
                                                            0,
                                                            SerializationHeader.make(cfs.metadata, txn.originals()),
+                                                           cfs.indexManager.listIndexes(),
                                                            txn));
                 while (ci.hasNext())
                 {
diff --git a/test/unit/org/apache/cassandra/db/lifecycle/TrackerTest.java b/test/unit/org/apache/cassandra/db/lifecycle/TrackerTest.java
index ddae1fc..7705d8b 100644
--- a/test/unit/org/apache/cassandra/db/lifecycle/TrackerTest.java
+++ b/test/unit/org/apache/cassandra/db/lifecycle/TrackerTest.java
@@ -39,7 +39,7 @@
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Memtable;
 import org.apache.cassandra.db.commitlog.CommitLog;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.notifications.*;
@@ -74,7 +74,7 @@
     @BeforeClass
     public static void setUp()
     {
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
         MockSchema.cleanup();
     }
 
@@ -274,23 +274,24 @@
         Tracker tracker = cfs.getTracker();
         tracker.subscribe(listener);
 
-        Memtable prev1 = tracker.switchMemtable(true, new Memtable(new AtomicReference<>(CommitLog.instance.getContext()), cfs));
+        Memtable prev1 = tracker.switchMemtable(true, new Memtable(new AtomicReference<>(CommitLog.instance.getCurrentPosition()), cfs));
         OpOrder.Group write1 = cfs.keyspace.writeOrder.getCurrent();
         OpOrder.Barrier barrier1 = cfs.keyspace.writeOrder.newBarrier();
-        prev1.setDiscarding(barrier1, new AtomicReference<>(CommitLog.instance.getContext()));
+        prev1.setDiscarding(barrier1, new AtomicReference<>(CommitLog.instance.getCurrentPosition()));
         barrier1.issue();
-        Memtable prev2 = tracker.switchMemtable(false, new Memtable(new AtomicReference<>(CommitLog.instance.getContext()), cfs));
+        Memtable prev2 = tracker.switchMemtable(false, new Memtable(new AtomicReference<>(CommitLog.instance.getCurrentPosition()), cfs));
         OpOrder.Group write2 = cfs.keyspace.writeOrder.getCurrent();
         OpOrder.Barrier barrier2 = cfs.keyspace.writeOrder.newBarrier();
-        prev2.setDiscarding(barrier2, new AtomicReference<>(CommitLog.instance.getContext()));
+        prev2.setDiscarding(barrier2, new AtomicReference<>(CommitLog.instance.getCurrentPosition()));
         barrier2.issue();
         Memtable cur = tracker.getView().getCurrentMemtable();
         OpOrder.Group writecur = cfs.keyspace.writeOrder.getCurrent();
-        Assert.assertEquals(prev1, tracker.getMemtableFor(write1, ReplayPosition.NONE));
-        Assert.assertEquals(prev2, tracker.getMemtableFor(write2, ReplayPosition.NONE));
-        Assert.assertEquals(cur, tracker.getMemtableFor(writecur, ReplayPosition.NONE));
-        Assert.assertEquals(1, listener.received.size());
+        Assert.assertEquals(prev1, tracker.getMemtableFor(write1, CommitLogPosition.NONE));
+        Assert.assertEquals(prev2, tracker.getMemtableFor(write2, CommitLogPosition.NONE));
+        Assert.assertEquals(cur, tracker.getMemtableFor(writecur, CommitLogPosition.NONE));
+        Assert.assertEquals(2, listener.received.size());
         Assert.assertTrue(listener.received.get(0) instanceof MemtableRenewedNotification);
+        Assert.assertTrue(listener.received.get(1) instanceof MemtableSwitchedNotification);
         listener.received.clear();
 
         tracker.markFlushing(prev2);
@@ -306,10 +307,11 @@
         Assert.assertTrue(tracker.getView().flushingMemtables.contains(prev2));
 
         SSTableReader reader = MockSchema.sstable(0, 10, false, cfs);
-        tracker.replaceFlushed(prev2, Collections.singleton(reader));
+        tracker.replaceFlushed(prev2, singleton(reader));
         Assert.assertEquals(1, tracker.getView().sstables.size());
-        Assert.assertEquals(1, listener.received.size());
-        Assert.assertEquals(singleton(reader), ((SSTableAddedNotification) listener.received.get(0)).added);
+        Assert.assertEquals(2, listener.received.size());
+        Assert.assertEquals(prev2, ((MemtableDiscardedNotification) listener.received.get(0)).memtable);
+        Assert.assertEquals(singleton(reader), ((SSTableAddedNotification) listener.received.get(1)).added);
         listener.received.clear();
         Assert.assertTrue(reader.isKeyCacheSetup());
         Assert.assertEquals(10, cfs.metric.liveDiskSpaceUsed.getCount());
@@ -319,7 +321,7 @@
         tracker = cfs.getTracker();
         listener = new MockListener(false);
         tracker.subscribe(listener);
-        prev1 = tracker.switchMemtable(false, new Memtable(new AtomicReference<>(CommitLog.instance.getContext()), cfs));
+        prev1 = tracker.switchMemtable(false, new Memtable(new AtomicReference<>(CommitLog.instance.getCurrentPosition()), cfs));
         tracker.markFlushing(prev1);
         reader = MockSchema.sstable(0, 10, true, cfs);
         cfs.invalidate(false);
@@ -327,10 +329,12 @@
         Assert.assertEquals(0, tracker.getView().sstables.size());
         Assert.assertEquals(0, tracker.getView().flushingMemtables.size());
         Assert.assertEquals(0, cfs.metric.liveDiskSpaceUsed.getCount());
-        Assert.assertEquals(3, listener.received.size());
-        Assert.assertEquals(singleton(reader), ((SSTableAddedNotification) listener.received.get(0)).added);
-        Assert.assertTrue(listener.received.get(1) instanceof SSTableDeletingNotification);
-        Assert.assertEquals(1, ((SSTableListChangedNotification) listener.received.get(2)).removed.size());
+        Assert.assertEquals(5, listener.received.size());
+        Assert.assertEquals(prev1, ((MemtableSwitchedNotification) listener.received.get(0)).memtable);
+        Assert.assertEquals(prev1, ((MemtableDiscardedNotification) listener.received.get(1)).memtable);
+        Assert.assertEquals(singleton(reader), ((SSTableAddedNotification) listener.received.get(2)).added);
+        Assert.assertTrue(listener.received.get(3) instanceof SSTableDeletingNotification);
+        Assert.assertEquals(1, ((SSTableListChangedNotification) listener.received.get(4)).removed.size());
         DatabaseDescriptor.setIncrementalBackupsEnabled(backups);
     }
 
@@ -353,7 +357,7 @@
         Assert.assertEquals(singleton(r2), ((SSTableListChangedNotification) listener.received.get(0)).added);
         listener.received.clear();
         tracker.notifySSTableRepairedStatusChanged(singleton(r1));
-        Assert.assertEquals(singleton(r1), ((SSTableRepairStatusChanged) listener.received.get(0)).sstable);
+        Assert.assertEquals(singleton(r1), ((SSTableRepairStatusChanged) listener.received.get(0)).sstables);
         listener.received.clear();
         Memtable memtable = MockSchema.memtable(cfs);
         tracker.notifyRenewed(memtable);
diff --git a/test/unit/org/apache/cassandra/db/lifecycle/ViewTest.java b/test/unit/org/apache/cassandra/db/lifecycle/ViewTest.java
index 436bf18..a0e6e5f 100644
--- a/test/unit/org/apache/cassandra/db/lifecycle/ViewTest.java
+++ b/test/unit/org/apache/cassandra/db/lifecycle/ViewTest.java
@@ -33,6 +33,7 @@
 
 import junit.framework.Assert;
 import org.apache.cassandra.MockSchema;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Memtable;
 import org.apache.cassandra.db.PartitionPosition;
@@ -42,6 +43,7 @@
 import static com.google.common.collect.ImmutableSet.copyOf;
 import static com.google.common.collect.ImmutableSet.of;
 import static com.google.common.collect.Iterables.concat;
+import static java.util.Collections.singleton;
 import static org.apache.cassandra.db.lifecycle.Helpers.emptySet;
 
 public class ViewTest
@@ -49,6 +51,7 @@
     @BeforeClass
     public static void setUp()
     {
+        DatabaseDescriptor.daemonInitialization();
         MockSchema.cleanup();
     }
 
@@ -198,7 +201,7 @@
         Assert.assertEquals(memtable3, cur.getCurrentMemtable());
 
         SSTableReader sstable = MockSchema.sstable(1, cfs);
-        cur = View.replaceFlushed(memtable1, Collections.singleton(sstable)).apply(cur);
+        cur = View.replaceFlushed(memtable1, singleton(sstable)).apply(cur);
         Assert.assertEquals(0, cur.flushingMemtables.size());
         Assert.assertEquals(1, cur.liveMemtables.size());
         Assert.assertEquals(memtable3, cur.getCurrentMemtable());
diff --git a/test/unit/org/apache/cassandra/db/marshal/AbstractCompositeTypeTest.java b/test/unit/org/apache/cassandra/db/marshal/AbstractCompositeTypeTest.java
new file mode 100644
index 0000000..dc78bb9
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/marshal/AbstractCompositeTypeTest.java
@@ -0,0 +1,55 @@
+/*
+ *
+ * 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.db.marshal;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+public class AbstractCompositeTypeTest
+{
+    
+    @Test
+    public void testEscape()
+    {
+        assertEquals("", AbstractCompositeType.escape(""));
+        assertEquals("Ab!CdXy \\Z123-345", AbstractCompositeType.escape("Ab!CdXy \\Z123-345"));
+        assertEquals("Ab!CdXy \\Z123-345!!", AbstractCompositeType.escape("Ab!CdXy \\Z123-345!"));
+        assertEquals("Ab!CdXy \\Z123-345\\!", AbstractCompositeType.escape("Ab!CdXy \\Z123-345\\"));
+        
+        assertEquals("A\\:b!CdXy \\\\:Z123-345", AbstractCompositeType.escape("A:b!CdXy \\:Z123-345"));
+        assertEquals("A\\:b!CdXy \\\\:Z123-345!!", AbstractCompositeType.escape("A:b!CdXy \\:Z123-345!"));
+        assertEquals("A\\:b!CdXy \\\\:Z123-345\\!", AbstractCompositeType.escape("A:b!CdXy \\:Z123-345\\"));
+        
+    }
+    
+    @Test
+    public void testUnescape()
+    {
+        assertEquals("", AbstractCompositeType.escape(""));
+        assertEquals("Ab!CdXy \\Z123-345", AbstractCompositeType.unescape("Ab!CdXy \\Z123-345"));
+        assertEquals("Ab!CdXy \\Z123-345!", AbstractCompositeType.unescape("Ab!CdXy \\Z123-345!!"));
+        assertEquals("Ab!CdXy \\Z123-345\\", AbstractCompositeType.unescape("Ab!CdXy \\Z123-345\\!"));
+        
+        assertEquals("A:b!CdXy \\:Z123-345", AbstractCompositeType.unescape("A\\:b!CdXy \\\\:Z123-345"));
+        assertEquals("A:b!CdXy \\:Z123-345!", AbstractCompositeType.unescape("A\\:b!CdXy \\\\:Z123-345!!"));
+        assertEquals("A:b!CdXy \\:Z123-345\\", AbstractCompositeType.unescape("A\\:b!CdXy \\\\:Z123-345\\!"));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/marshal/JsonConversionTest.java b/test/unit/org/apache/cassandra/db/marshal/JsonConversionTest.java
index f7795c7..beb37e2 100644
--- a/test/unit/org/apache/cassandra/db/marshal/JsonConversionTest.java
+++ b/test/unit/org/apache/cassandra/db/marshal/JsonConversionTest.java
@@ -22,9 +22,9 @@
 
 import java.nio.ByteBuffer;
 import org.apache.cassandra.cql3.QueryOptions;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.UUIDGen;
-import org.codehaus.jackson.map.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.junit.Test;
 
 public class JsonConversionTest
@@ -300,7 +300,7 @@
         ByteBuffer bb = type.getSerializer().serialize(value);
         int position = bb.position();
 
-        String output = type.toJSONString(bb, Server.CURRENT_VERSION);
+        String output = type.toJSONString(bb, ProtocolVersion.CURRENT);
         assertEquals(position, bb.position());
         assertEquals(json, output);
     }
@@ -313,7 +313,7 @@
         ByteBuffer bb = type.fromJSONObject(jsonObject).bindAndGet(QueryOptions.DEFAULT);
         int position = bb.position();
 
-        String output = type.toJSONString(bb, Server.CURRENT_VERSION);
+        String output = type.toJSONString(bb, ProtocolVersion.CURRENT);
         assertEquals(position, bb.position());
         assertEquals(json, output);
     }
diff --git a/test/unit/org/apache/cassandra/db/marshal/TimeUUIDTypeTest.java b/test/unit/org/apache/cassandra/db/marshal/TimeUUIDTypeTest.java
index 0054163..b6160bf 100644
--- a/test/unit/org/apache/cassandra/db/marshal/TimeUUIDTypeTest.java
+++ b/test/unit/org/apache/cassandra/db/marshal/TimeUUIDTypeTest.java
@@ -19,10 +19,7 @@
 package org.apache.cassandra.db.marshal;
 
 import java.nio.ByteBuffer;
-import java.util.Arrays;
-import java.util.Random;
 import java.util.UUID;
-import java.util.concurrent.ThreadLocalRandom;
 
 import junit.framework.Assert;
 import org.apache.cassandra.serializers.MarshalException;
diff --git a/test/unit/org/apache/cassandra/db/marshal/TypeParserTest.java b/test/unit/org/apache/cassandra/db/marshal/TypeParserTest.java
index 808a680..ecfe910 100644
--- a/test/unit/org/apache/cassandra/db/marshal/TypeParserTest.java
+++ b/test/unit/org/apache/cassandra/db/marshal/TypeParserTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.cassandra.db.marshal;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import static org.junit.Assert.assertSame;
@@ -30,6 +31,12 @@
 
 public class TypeParserTest
 {
+    @BeforeClass
+    public static void initDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testParse() throws ConfigurationException, SyntaxException
     {
diff --git a/test/unit/org/apache/cassandra/db/marshal/UUIDTypeTest.java b/test/unit/org/apache/cassandra/db/marshal/UUIDTypeTest.java
index 74e5ef3..23b1922 100644
--- a/test/unit/org/apache/cassandra/db/marshal/UUIDTypeTest.java
+++ b/test/unit/org/apache/cassandra/db/marshal/UUIDTypeTest.java
@@ -31,7 +31,7 @@
 import org.junit.Test;
 
 import junit.framework.Assert;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.UUIDGen;
 import org.slf4j.Logger;
@@ -47,7 +47,7 @@
     @Test //CASSANDRA-15896
     public void testToJsonEmptyValue()
     {
-        String res = uuidType.toJSONString(uuidType.fromJSONObject("").bindAndGet(null), Server.CURRENT_VERSION);
+        String res = uuidType.toJSONString(uuidType.fromJSONObject("").bindAndGet(null), ProtocolVersion.CURRENT);
         assertEquals("\"\"", res);
     }
 
diff --git a/test/unit/org/apache/cassandra/db/monitoring/MonitoringTaskTest.java b/test/unit/org/apache/cassandra/db/monitoring/MonitoringTaskTest.java
new file mode 100644
index 0000000..acc988f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/monitoring/MonitoringTaskTest.java
@@ -0,0 +1,459 @@
+/*
+ * 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.db.monitoring;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class MonitoringTaskTest
+{
+    private static final long timeout = 100;
+    private static final long slowTimeout = 10;
+
+    private static final long MAX_SPIN_TIME_NANOS = TimeUnit.SECONDS.toNanos(5);
+
+    private static final int REPORT_INTERVAL_MS = 600000; // long enough so that it won't check unless told to do so
+    private static final int MAX_TIMEDOUT_OPERATIONS = -1; // unlimited
+
+    @BeforeClass
+    public static void setup()
+    {
+        MonitoringTask.instance = MonitoringTask.make(REPORT_INTERVAL_MS, MAX_TIMEDOUT_OPERATIONS);
+    }
+
+    @After
+    public void cleanUp()
+    {
+        // these clear the queues of the monitorint task
+        MonitoringTask.instance.getSlowOperations();
+        MonitoringTask.instance.getFailedOperations();
+    }
+
+    private static final class TestMonitor extends MonitorableImpl
+    {
+        private final String name;
+
+        TestMonitor(String name, long timestamp, boolean isCrossNode, long timeout, long slow)
+        {
+            this.name = name;
+            setMonitoringTime(timestamp, isCrossNode, timeout, slow);
+        }
+
+        public String name()
+        {
+            return name;
+        }
+
+        @Override
+        public String toString()
+        {
+            return name();
+        }
+    }
+
+    private static void waitForOperationsToComplete(Monitorable... operations) throws InterruptedException
+    {
+        waitForOperationsToComplete(Arrays.asList(operations));
+    }
+
+    private static void waitForOperationsToComplete(List<Monitorable> operations) throws InterruptedException
+    {
+        long timeout = operations.stream().map(Monitorable::timeout).reduce(0L, Long::max);
+        Thread.sleep(timeout * 2 + ApproximateTime.precision());
+
+        long start = System.nanoTime();
+        while(System.nanoTime() - start <= MAX_SPIN_TIME_NANOS)
+        {
+            long numInProgress = operations.stream().filter(Monitorable::isInProgress).count();
+            if (numInProgress == 0)
+                return;
+        }
+    }
+
+    private static void waitForOperationsToBeReportedAsSlow(Monitorable... operations) throws InterruptedException
+    {
+        waitForOperationsToBeReportedAsSlow(Arrays.asList(operations));
+    }
+
+    private static void waitForOperationsToBeReportedAsSlow(List<Monitorable> operations) throws InterruptedException
+    {
+        long timeout = operations.stream().map(Monitorable::slowTimeout).reduce(0L, Long::max);
+        Thread.sleep(timeout * 2 + ApproximateTime.precision());
+
+        long start = System.nanoTime();
+        while(System.nanoTime() - start <= MAX_SPIN_TIME_NANOS)
+        {
+            long numSlow = operations.stream().filter(Monitorable::isSlow).count();
+            if (numSlow == operations.size())
+                return;
+        }
+    }
+
+    @Test
+    public void testAbort() throws InterruptedException
+    {
+        Monitorable operation = new TestMonitor("Test abort", System.currentTimeMillis(), false, timeout, slowTimeout);
+        waitForOperationsToComplete(operation);
+
+        assertTrue(operation.isAborted());
+        assertFalse(operation.isCompleted());
+        assertEquals(1, MonitoringTask.instance.getFailedOperations().size());
+    }
+
+    @Test
+    public void testAbortIdemPotent() throws InterruptedException
+    {
+        Monitorable operation = new TestMonitor("Test abort", System.currentTimeMillis(), false, timeout, slowTimeout);
+        waitForOperationsToComplete(operation);
+
+        assertTrue(operation.abort());
+
+        assertTrue(operation.isAborted());
+        assertFalse(operation.isCompleted());
+        assertEquals(1, MonitoringTask.instance.getFailedOperations().size());
+    }
+
+    @Test
+    public void testAbortCrossNode() throws InterruptedException
+    {
+        Monitorable operation = new TestMonitor("Test for cross node", System.currentTimeMillis(), true, timeout, slowTimeout);
+        waitForOperationsToComplete(operation);
+
+        assertTrue(operation.isAborted());
+        assertFalse(operation.isCompleted());
+        assertEquals(1, MonitoringTask.instance.getFailedOperations().size());
+    }
+
+    @Test
+    public void testComplete() throws InterruptedException
+    {
+        Monitorable operation = new TestMonitor("Test complete", System.currentTimeMillis(), false, timeout, slowTimeout);
+        operation.complete();
+        waitForOperationsToComplete(operation);
+
+        assertFalse(operation.isAborted());
+        assertTrue(operation.isCompleted());
+        assertEquals(0, MonitoringTask.instance.getFailedOperations().size());
+    }
+
+    @Test
+    public void testCompleteIdemPotent() throws InterruptedException
+    {
+        Monitorable operation = new TestMonitor("Test complete", System.currentTimeMillis(), false, timeout, slowTimeout);
+        operation.complete();
+        waitForOperationsToComplete(operation);
+
+        assertTrue(operation.complete());
+
+        assertFalse(operation.isAborted());
+        assertTrue(operation.isCompleted());
+        assertEquals(0, MonitoringTask.instance.getFailedOperations().size());
+    }
+
+    @Test
+    public void testReportSlow() throws InterruptedException
+    {
+        Monitorable operation = new TestMonitor("Test report slow", System.currentTimeMillis(), false, timeout, slowTimeout);
+        waitForOperationsToBeReportedAsSlow(operation);
+
+        assertTrue(operation.isSlow());
+        operation.complete();
+        assertFalse(operation.isAborted());
+        assertTrue(operation.isCompleted());
+        assertEquals(1, MonitoringTask.instance.getSlowOperations().size());
+    }
+
+    @Test
+    public void testNoReportSlowIfZeroSlowTimeout() throws InterruptedException
+    {
+        // when the slow timeout is set to zero then operation won't be reported as slow
+        Monitorable operation = new TestMonitor("Test report slow disabled", System.currentTimeMillis(), false, timeout, 0);
+        waitForOperationsToBeReportedAsSlow(operation);
+
+        assertTrue(operation.isSlow());
+        operation.complete();
+        assertFalse(operation.isAborted());
+        assertTrue(operation.isCompleted());
+        assertEquals(0, MonitoringTask.instance.getSlowOperations().size());
+    }
+
+    @Test
+    public void testReport() throws InterruptedException
+    {
+        Monitorable operation = new TestMonitor("Test report", System.currentTimeMillis(), false, timeout, slowTimeout);
+        waitForOperationsToComplete(operation);
+
+        assertTrue(operation.isSlow());
+        assertTrue(operation.isAborted());
+        assertFalse(operation.isCompleted());
+
+        // aborted operations are not logged as slow
+        assertFalse(MonitoringTask.instance.logSlowOperations(ApproximateTime.currentTimeMillis()));
+        assertEquals(0, MonitoringTask.instance.getSlowOperations().size());
+
+        assertTrue(MonitoringTask.instance.logFailedOperations(ApproximateTime.currentTimeMillis()));
+        assertEquals(0, MonitoringTask.instance.getFailedOperations().size());
+    }
+
+    @Test
+    public void testRealScheduling() throws InterruptedException
+    {
+        MonitoringTask.instance = MonitoringTask.make(10, -1);
+        try
+        {
+            Monitorable operation1 = new TestMonitor("Test report 1", System.currentTimeMillis(), false, timeout, slowTimeout);
+            waitForOperationsToComplete(operation1);
+
+            assertTrue(operation1.isAborted());
+            assertFalse(operation1.isCompleted());
+
+            Monitorable operation2 = new TestMonitor("Test report 2", System.currentTimeMillis(), false, timeout, slowTimeout);
+            waitForOperationsToBeReportedAsSlow(operation2);
+
+            operation2.complete();
+            assertFalse(operation2.isAborted());
+            assertTrue(operation2.isCompleted());
+
+            Thread.sleep(ApproximateTime.precision() + 500);
+            assertEquals(0, MonitoringTask.instance.getFailedOperations().size());
+            assertEquals(0, MonitoringTask.instance.getSlowOperations().size());
+        }
+        finally
+        {
+            MonitoringTask.instance = MonitoringTask.make(REPORT_INTERVAL_MS, MAX_TIMEDOUT_OPERATIONS);
+        }
+    }
+
+    @Test
+    public void testMultipleThreads() throws InterruptedException
+    {
+        final int opCount = 50;
+        final ExecutorService executorService = Executors.newFixedThreadPool(20);
+        final List<Monitorable> operations = Collections.synchronizedList(new ArrayList<>(opCount));
+
+        for (int i = 0; i < opCount; i++)
+        {
+            executorService.submit(() ->
+                operations.add(new TestMonitor(UUID.randomUUID().toString(), System.currentTimeMillis(), false, timeout, slowTimeout))
+            );
+        }
+
+        executorService.shutdown();
+        assertTrue(executorService.awaitTermination(30, TimeUnit.SECONDS));
+        assertEquals(opCount, operations.size());
+
+        waitForOperationsToComplete(operations);
+        assertEquals(opCount, MonitoringTask.instance.getFailedOperations().size());
+        assertEquals(0, MonitoringTask.instance.getSlowOperations().size());
+    }
+
+    @Test
+    public void testZeroMaxTimedoutOperations() throws InterruptedException
+    {
+        doTestMaxTimedoutOperations(0, 1, 0);
+    }
+
+    @Test
+    public void testMaxTimedoutOperationsExceeded() throws InterruptedException
+    {
+        doTestMaxTimedoutOperations(5, 10, 6);
+    }
+
+    private static void doTestMaxTimedoutOperations(int maxTimedoutOperations,
+                                                    int numThreads,
+                                                    int numExpectedOperations) throws InterruptedException
+    {
+        MonitoringTask.instance = MonitoringTask.make(REPORT_INTERVAL_MS, maxTimedoutOperations);
+        try
+        {
+            ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
+            final CountDownLatch finished = new CountDownLatch(numThreads);
+
+            for (int i = 0; i < numThreads; i++)
+            {
+                final String operationName = "Operation " + Integer.toString(i+1);
+                final int numTimes = i + 1;
+                executorService.submit(() -> {
+                    try
+                    {
+                        for (int j = 0; j < numTimes; j++)
+                        {
+                            Monitorable operation1 = new TestMonitor(operationName,
+                                                                     System.currentTimeMillis(),
+                                                                     false,
+                                                                     timeout,
+                                                                     slowTimeout);
+                            waitForOperationsToComplete(operation1);
+
+                            Monitorable operation2 = new TestMonitor(operationName,
+                                                                     System.currentTimeMillis(),
+                                                                     false,
+                                                                     timeout,
+                                                                     slowTimeout);
+                            waitForOperationsToBeReportedAsSlow(operation2);
+                            operation2.complete();
+                        }
+                    }
+                    catch (InterruptedException e)
+                    {
+                        e.printStackTrace();
+                        fail("Unexpected exception");
+                    }
+                    finally
+                    {
+                        finished.countDown();
+                    }
+                });
+            }
+
+            finished.await();
+            assertEquals(0, executorService.shutdownNow().size());
+
+            List<String> failedOperations = MonitoringTask.instance.getFailedOperations();
+            assertEquals(numExpectedOperations, failedOperations.size());
+            if (numExpectedOperations > 0)
+                assertTrue(failedOperations.get(numExpectedOperations - 1).startsWith("..."));
+        }
+        finally
+        {
+            MonitoringTask.instance = MonitoringTask.make(REPORT_INTERVAL_MS, MAX_TIMEDOUT_OPERATIONS);
+        }
+    }
+
+    @Test
+    public void testMultipleThreadsSameNameFailed() throws InterruptedException
+    {
+        final int threadCount = 50;
+        final List<Monitorable> operations = new ArrayList<>(threadCount);
+        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
+        final CountDownLatch finished = new CountDownLatch(threadCount);
+
+        for (int i = 0; i < threadCount; i++)
+        {
+            executorService.submit(() -> {
+                try
+                {
+                    Monitorable operation = new TestMonitor("Test testMultipleThreadsSameName failed",
+                                                            System.currentTimeMillis(),
+                                                            false,
+                                                            timeout,
+                                                            slowTimeout);
+                    operations.add(operation);
+                }
+                finally
+                {
+                    finished.countDown();
+                }
+            });
+        }
+
+        finished.await();
+        assertEquals(0, executorService.shutdownNow().size());
+
+        waitForOperationsToComplete(operations);
+        assertEquals(1, MonitoringTask.instance.getFailedOperations().size());
+    }
+
+    @Test
+    public void testMultipleThreadsSameNameSlow() throws InterruptedException
+    {
+        final int threadCount = 50;
+        final List<Monitorable> operations = new ArrayList<>(threadCount);
+        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
+        final CountDownLatch finished = new CountDownLatch(threadCount);
+
+        for (int i = 0; i < threadCount; i++)
+        {
+            executorService.submit(() -> {
+                try
+                {
+                    Monitorable operation = new TestMonitor("Test testMultipleThreadsSameName slow",
+                                                            System.currentTimeMillis(),
+                                                            false,
+                                                            timeout,
+                                                            slowTimeout);
+                    operations.add(operation);
+                }
+                finally
+                {
+                    finished.countDown();
+                }
+            });
+        }
+
+        finished.await();
+        assertEquals(0, executorService.shutdownNow().size());
+
+        waitForOperationsToBeReportedAsSlow(operations);
+        operations.forEach(o -> o.complete());
+
+        assertEquals(1, MonitoringTask.instance.getSlowOperations().size());
+    }
+
+    @Test
+    public void testMultipleThreadsNoFailedOps() throws InterruptedException
+    {
+        final int threadCount = 50;
+        final List<Monitorable> operations = new ArrayList<>(threadCount);
+        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
+        final CountDownLatch finished = new CountDownLatch(threadCount);
+
+        for (int i = 0; i < threadCount; i++)
+        {
+            executorService.submit(() -> {
+                try
+                {
+                    Monitorable operation = new TestMonitor("Test thread " + Thread.currentThread().getName(),
+                                                            System.currentTimeMillis(),
+                                                            false,
+                                                            timeout,
+                                                            slowTimeout);
+                    operations.add(operation);
+                    operation.complete();
+                }
+                finally
+                {
+                    finished.countDown();
+                }
+            });
+        }
+
+        finished.await();
+        assertEquals(0, executorService.shutdownNow().size());
+
+        waitForOperationsToComplete(operations);
+        assertEquals(0, MonitoringTask.instance.getFailedOperations().size());
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/partition/PartitionImplementationTest.java b/test/unit/org/apache/cassandra/db/partition/PartitionImplementationTest.java
index f4c93d6..781ec9c 100644
--- a/test/unit/org/apache/cassandra/db/partition/PartitionImplementationTest.java
+++ b/test/unit/org/apache/cassandra/db/partition/PartitionImplementationTest.java
@@ -39,7 +39,6 @@
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.Slice.Bound;
 import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.marshal.AsciiType;
 import org.apache.cassandra.db.partitions.AbstractBTreePartition;
@@ -104,7 +103,7 @@
         ColumnDefinition defCol = cfm.getColumnDefinition(new ColumnIdentifier("col", true));
         Row.Builder row = BTreeRow.unsortedBuilder(TIMESTAMP);
         row.newRow(clustering);
-        row.addCell(BufferCell.live(cfm, defCol, TIMESTAMP, ByteBufferUtil.bytes(colValue)));
+        row.addCell(BufferCell.live(defCol, TIMESTAMP, ByteBufferUtil.bytes(colValue)));
         return row.build();
     }
 
@@ -113,7 +112,7 @@
         ColumnDefinition defCol = cfm.getColumnDefinition(new ColumnIdentifier("static_col", true));
         Row.Builder row = BTreeRow.unsortedBuilder(TIMESTAMP);
         row.newRow(Clustering.STATIC_CLUSTERING);
-        row.addCell(BufferCell.live(cfm, defCol, TIMESTAMP, ByteBufferUtil.bytes("static value")));
+        row.addCell(BufferCell.live(defCol, TIMESTAMP, ByteBufferUtil.bytes("static value")));
         return row.build();
     }
 
@@ -314,10 +313,10 @@
         testSearchIterator(sortedContent, partition, cf, true);
 
         // sliceable iter
-        testSliceableIterator(sortedContent, partition, ColumnFilter.all(cfm), false);
-        testSliceableIterator(sortedContent, partition, cf, false);
-        testSliceableIterator(sortedContent, partition, ColumnFilter.all(cfm), true);
-        testSliceableIterator(sortedContent, partition, cf, true);
+        testSlicingOfIterators(sortedContent, partition, ColumnFilter.all(cfm), false);
+        testSlicingOfIterators(sortedContent, partition, cf, false);
+        testSlicingOfIterators(sortedContent, partition, ColumnFilter.all(cfm), true);
+        testSlicingOfIterators(sortedContent, partition, cf, true);
     }
 
     void testSearchIterator(NavigableSet<Clusterable> sortedContent, Partition partition, ColumnFilter cf, boolean reversed)
@@ -355,29 +354,36 @@
             Clustering start = clustering(pos);
             pos += sz;
             Clustering end = clustering(pos);
-            Slice slice = Slice.make(skip == 0 ? Bound.exclusiveStartOf(start) : Bound.inclusiveStartOf(start), Bound.inclusiveEndOf(end));
+            Slice slice = Slice.make(skip == 0 ? ClusteringBound.exclusiveStartOf(start) : ClusteringBound.inclusiveStartOf(start), ClusteringBound.inclusiveEndOf(end));
             builder.add(slice);
         }
         return builder.build();
     }
 
-    void testSliceableIterator(NavigableSet<Clusterable> sortedContent, AbstractBTreePartition partition, ColumnFilter cf, boolean reversed)
+    void testSlicingOfIterators(NavigableSet<Clusterable> sortedContent, AbstractBTreePartition partition, ColumnFilter cf, boolean reversed)
     {
         Function<? super Clusterable, ? extends Clusterable> colFilter = x -> x instanceof Row ? ((Row) x).filter(cf, cfm) : x;
         Slices slices = makeSlices();
-        try (SliceableUnfilteredRowIterator sliceableIter = partition.sliceableUnfilteredIterator(cf, reversed))
+
+        // fetch each slice in turn
+        for (Slice slice : (Iterable<Slice>) () -> directed(slices, reversed))
         {
-            for (Slice slice : (Iterable<Slice>) () -> directed(slices, reversed))
+            try (UnfilteredRowIterator slicedIter = partition.unfilteredIterator(cf, Slices.with(cfm.comparator, slice), reversed))
+            {
                 assertIteratorsEqual(streamOf(directed(slice(sortedContent, slice), reversed)).map(colFilter).iterator(),
-                                     sliceableIter.slice(slice));
+                                     slicedIter);
+            }
         }
 
-        // Try using sliceable as unfiltered iterator
-        try (SliceableUnfilteredRowIterator sliceableIter = partition.sliceableUnfilteredIterator(cf, reversed))
+        // Fetch all slices at once
+        try (UnfilteredRowIterator slicedIter = partition.unfilteredIterator(cf, slices, reversed))
         {
-            assertIteratorsEqual((reversed ? sortedContent.descendingSet() : sortedContent).
-                                     stream().map(colFilter).iterator(),
-                                 sliceableIter);
+            List<Iterator<? extends Clusterable>> slicelist = new ArrayList<>();
+            slices.forEach(slice -> slicelist.add(directed(slice(sortedContent, slice), reversed)));
+            if (reversed)
+                Collections.reverse(slicelist);
+
+            assertIteratorsEqual(Iterators.concat(slicelist.toArray(new Iterator[0])), slicedIter);
         }
     }
 
diff --git a/test/unit/org/apache/cassandra/db/partition/PartitionUpdateTest.java b/test/unit/org/apache/cassandra/db/partition/PartitionUpdateTest.java
index 2bd685c..df23e4f 100644
--- a/test/unit/org/apache/cassandra/db/partition/PartitionUpdateTest.java
+++ b/test/unit/org/apache/cassandra/db/partition/PartitionUpdateTest.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.db.partition;
 
+import org.apache.cassandra.UpdateBuilder;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -61,21 +62,17 @@
         createTable("CREATE TABLE %s (key text, clustering int, a int, s int static, PRIMARY KEY(key, clustering))");
         CFMetaData cfm = currentTableMetadata();
 
-        long timestamp = FBUtilities.timestampMicros();
-        PartitionUpdate update = new RowUpdateBuilder(cfm, timestamp, "key0").clustering(1).add("a", 1).buildUpdate();
-        Assert.assertEquals(1, update.operationCount());
+        UpdateBuilder builder = UpdateBuilder.create(cfm, "key0");
+        Assert.assertEquals(0, builder.build().operationCount());
+        Assert.assertEquals(1, builder.newRow(1).add("a", 1).build().operationCount());
 
-        update = new RowUpdateBuilder(cfm, timestamp, "key0").buildUpdate();
-        Assert.assertEquals(0, update.operationCount());
+        builder = UpdateBuilder.create(cfm, "key0");
+        Assert.assertEquals(1, builder.newRow().add("s", 1).build().operationCount());
 
-        update = new RowUpdateBuilder(cfm, timestamp, "key0").add("s", 1).buildUpdate();
-        Assert.assertEquals(1, update.operationCount());
-
-        update = new RowUpdateBuilder(cfm, timestamp, "key0").add("s", 1).buildUpdate();
-        update = new RowUpdateBuilder(update, timestamp, cfm.params.defaultTimeToLive).clustering(1)
-                                                                                      .add("a", 1)
-                                                                                      .buildUpdate();
-        Assert.assertEquals(2, update.operationCount());
+        builder = UpdateBuilder.create(cfm, "key0");
+        builder.newRow().add("s", 1);
+        builder.newRow(1).add("a", 1);
+        Assert.assertEquals(2, builder.build().operationCount());
     }
 
     @Test
@@ -84,17 +81,19 @@
         createTable("CREATE TABLE %s (key text, clustering int, a int, s int static, PRIMARY KEY(key, clustering))");
         CFMetaData cfm = currentTableMetadata();
 
-        PartitionUpdate update = new RowUpdateBuilder(cfm, FBUtilities.timestampMicros(), "key0").add("s", 1).buildUpdate();
-        int size1 = update.dataSize();
-        Assert.assertEquals(20, size1);
+        UpdateBuilder builder = UpdateBuilder.create(cfm, "key0");
+        builder.newRow().add("s", 1);
+        builder.newRow(1).add("a", 2);
+        int size1 = builder.build().dataSize();
+        Assert.assertEquals(44, size1);
 
-        update = new RowUpdateBuilder(cfm, FBUtilities.timestampMicros(), "key0").clustering(1).add("a", 2).buildUpdate();
-        int size2 = update.dataSize();
+        builder = UpdateBuilder.create(cfm, "key0");
+        builder.newRow(1).add("a", 2);
+        int size2 = builder.build().dataSize();
         Assert.assertTrue(size1 != size2);
 
-        update = new RowUpdateBuilder(cfm, FBUtilities.timestampMicros(), "key0").buildUpdate();
-        int size3 = update.dataSize();
-        Assert.assertTrue(size1 != size3);
+        builder = UpdateBuilder.create(cfm, "key0");
+        int size3 = builder.build().dataSize();
         Assert.assertTrue(size2 != size3);
 
     }
@@ -126,16 +125,16 @@
 
         List<Row> rows = new ArrayList<>();
         Row.Builder builder = BTreeRow.unsortedBuilder(FBUtilities.nowInSeconds());
-        builder.newRow(new Clustering(ByteBufferUtil.bytes(2)));
+        builder.newRow(Clustering.make(ByteBufferUtil.bytes(2)));
         builder.addComplexDeletion(cfm.getColumnDefinition(ByteBufferUtil.bytes("v")), new DeletionTime(2, 1588586647));
 
-        Cell c = BufferCell.live(cfm, cfm.getColumnDefinition(ByteBufferUtil.bytes("v")), 3, ByteBufferUtil.bytes("h"), CellPath.create(ByteBufferUtil.bytes("g")));
+        Cell c = BufferCell.live(cfm.getColumnDefinition(ByteBufferUtil.bytes("v")), 3, ByteBufferUtil.bytes("h"), CellPath.create(ByteBufferUtil.bytes("g")));
         builder.addCell(c);
 
         Row r = builder.build();
         rows.add(r);
 
-        builder.newRow(new Clustering(ByteBufferUtil.bytes(2)));
+        builder.newRow(Clustering.make(ByteBufferUtil.bytes(2)));
         builder.addRowDeletion(new Row.Deletion(new DeletionTime(1588586647, 1), false));
         r = builder.build();
         rows.add(r);
@@ -151,7 +150,7 @@
                                                                           Collections.emptyIterator(),
                                                                           true);
 
-        PartitionUpdate pu = PartitionUpdate.fromPre30Iterator(rmi);
+        PartitionUpdate pu = PartitionUpdate.fromPre30Iterator(rmi, ColumnFilter.all(cfm));
         pu.iterator();
 
         Mutation m = new Mutation(getCurrentColumnFamilyStore().keyspace.getName(), dk);
@@ -190,16 +189,16 @@
         DecoratedKey dk = Murmur3Partitioner.instance.decorateKey(ByteBufferUtil.bytes(1));
 
         Row.Builder builder = BTreeRow.unsortedBuilder(FBUtilities.nowInSeconds());
-        builder.newRow(new Clustering(ByteBufferUtil.bytes(2)));
+        builder.newRow(Clustering.make(ByteBufferUtil.bytes(2)));
         builder.addComplexDeletion(cfm.getColumnDefinition(ByteBufferUtil.bytes("v")), new DeletionTime(2, 1588586647));
-        Cell c = BufferCell.live(cfm, cfm.getColumnDefinition(ByteBufferUtil.bytes("v")), 3, ByteBufferUtil.bytes("h"), CellPath.create(ByteBufferUtil.bytes("g")));
+        Cell c = BufferCell.live(cfm.getColumnDefinition(ByteBufferUtil.bytes("v")), 3, ByteBufferUtil.bytes("h"), CellPath.create(ByteBufferUtil.bytes("g")));
         builder.addCell(c);
         Row r = builder.build();
 
         PartitionUpdate p1 = new PartitionUpdate(cfm, dk, cfm.partitionColumns(), 2);
         p1.add(r);
 
-        builder.newRow(new Clustering(ByteBufferUtil.bytes(2)));
+        builder.newRow(Clustering.make(ByteBufferUtil.bytes(2)));
         builder.addRowDeletion(new Row.Deletion(new DeletionTime(1588586647, 1), false));
         r = builder.build();
         PartitionUpdate p2 = new PartitionUpdate(cfm, dk, cfm.partitionColumns(), 2);
diff --git a/test/unit/org/apache/cassandra/db/partitions/PurgeFunctionTest.java b/test/unit/org/apache/cassandra/db/partitions/PurgeFunctionTest.java
index 1dea7f3..7f85aea 100644
--- a/test/unit/org/apache/cassandra/db/partitions/PurgeFunctionTest.java
+++ b/test/unit/org/apache/cassandra/db/partitions/PurgeFunctionTest.java
@@ -26,6 +26,7 @@
 import org.junit.Test;
 
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ClusteringPrefix.Kind;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.marshal.AbstractType;
@@ -69,6 +70,8 @@
     @Before
     public void setUp()
     {
+        DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
+
         metadata =
             CFMetaData.Builder
                       .create(KEYSPACE, TABLE)
@@ -238,7 +241,7 @@
         ByteBuffer[] clusteringByteBuffers =
             new ByteBuffer[] { decompose(metadata.clusteringColumns().get(0).type, clusteringValue) };
 
-        return new RangeTombstoneBoundMarker(new RangeTombstone.Bound(kind, clusteringByteBuffers),
+        return new RangeTombstoneBoundMarker(ClusteringBound.create(kind, clusteringByteBuffers),
                                              new DeletionTime(timestamp, localDeletionTime));
     }
 
@@ -252,7 +255,7 @@
         ByteBuffer[] clusteringByteBuffers =
             new ByteBuffer[] { decompose(metadata.clusteringColumns().get(0).type, clusteringValue) };
 
-        return new RangeTombstoneBoundaryMarker(new RangeTombstone.Bound(kind, clusteringByteBuffers),
+        return new RangeTombstoneBoundaryMarker(ClusteringBoundary.create(kind, clusteringByteBuffers),
                                                 new DeletionTime(closeTimestamp, closeLocalDeletionTime),
                                                 new DeletionTime(openTimestamp, openDeletionTime));
     }
diff --git a/test/unit/org/apache/cassandra/db/rows/AbstractTypeVersionComparatorTest.java b/test/unit/org/apache/cassandra/db/rows/AbstractTypeVersionComparatorTest.java
deleted file mode 100644
index ad0c05c..0000000
--- a/test/unit/org/apache/cassandra/db/rows/AbstractTypeVersionComparatorTest.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * 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.db.rows;
-
-import java.nio.ByteBuffer;
-import java.util.Set;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import org.apache.cassandra.db.marshal.*;
-
-import static java.util.Arrays.asList;
-import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-public class AbstractTypeVersionComparatorTest
-{
-    private UserType udtWith2Fields;
-    private UserType udtWith3Fields;
-
-    @Before
-    public void setUp()
-    {
-        udtWith2Fields = new UserType("ks",
-                                      bytes("myType"),
-                                      asList(bytes("a"), bytes("b")),
-                                      asList(Int32Type.instance, Int32Type.instance));
-        udtWith3Fields = new UserType("ks",
-                                      bytes("myType"),
-                                      asList(bytes("a"), bytes("b"), bytes("c")),
-                                      asList(Int32Type.instance, Int32Type.instance, Int32Type.instance));
-    }
-
-    @After
-    public void tearDown()
-    {
-        udtWith2Fields = null;
-        udtWith3Fields = null;
-    }
-
-    @Test
-    public void testWithTuples()
-    {
-        checkComparisonResults(new TupleType(asList(Int32Type.instance, Int32Type.instance)),
-                               new TupleType(asList(Int32Type.instance, Int32Type.instance, Int32Type.instance)));
-    }
-
-    @Test
-    public void testWithUDTs()
-    {
-        checkComparisonResults(udtWith2Fields, udtWith3Fields);
-    }
-
-    @Test
-    public void testWithUDTsNestedWithinSet()
-    {
-        for (boolean isMultiCell : new boolean[]{false, true})
-        {
-            SetType<ByteBuffer> set1 = SetType.getInstance(udtWith2Fields, isMultiCell);
-            SetType<ByteBuffer> set2 = SetType.getInstance(udtWith3Fields, isMultiCell);
-            checkComparisonResults(set1, set2);
-        }
-    }
-
-    @Test
-    public void testWithUDTsNestedWithinList()
-    {
-        for (boolean isMultiCell : new boolean[]{false, true})
-        {
-            ListType<ByteBuffer> list1 = ListType.getInstance(udtWith2Fields, isMultiCell);
-            ListType<ByteBuffer> list2 = ListType.getInstance(udtWith3Fields, isMultiCell);
-            checkComparisonResults(list1, list2);
-        }
-    }
-
-    @Test
-    public void testWithUDTsNestedWithinMap()
-    {
-        for (boolean isMultiCell : new boolean[]{false, true})
-        {
-            MapType<ByteBuffer, Integer> map1 = MapType.getInstance(udtWith2Fields, Int32Type.instance, isMultiCell);
-            MapType<ByteBuffer, Integer> map2 = MapType.getInstance(udtWith3Fields, Int32Type.instance, isMultiCell);
-            checkComparisonResults(map1, map2);
-        }
-
-        for (boolean isMultiCell : new boolean[]{false, true})
-        {
-            MapType<Integer, ByteBuffer> map1 = MapType.getInstance(Int32Type.instance, udtWith2Fields, isMultiCell);
-            MapType<Integer, ByteBuffer> map2 = MapType.getInstance(Int32Type.instance, udtWith3Fields, isMultiCell);
-            checkComparisonResults(map1, map2);
-        }
-    }
-
-    @Test
-    public void testWithUDTsNestedWithinTuple()
-    {
-        TupleType tuple1 = new TupleType(asList(udtWith2Fields, Int32Type.instance));
-        TupleType tuple2 = new TupleType(asList(udtWith3Fields, Int32Type.instance));
-        checkComparisonResults(tuple1, tuple2);
-    }
-
-    @Test
-    public void testWithUDTsNestedWithinComposite()
-    {
-        CompositeType composite1 = CompositeType.getInstance(asList(udtWith2Fields, Int32Type.instance));
-        CompositeType composite2 = CompositeType.getInstance(asList(udtWith3Fields, Int32Type.instance));
-        checkComparisonResults(composite1, composite2);
-    }
-
-    @Test
-    public void testWithDeeplyNestedUDT()
-    {
-        for (boolean isMultiCell : new boolean[]{false, true})
-        {
-            ListType<Set<ByteBuffer>> list1 = ListType.getInstance(SetType.getInstance(new TupleType(asList(udtWith2Fields, Int32Type.instance)), isMultiCell), isMultiCell);
-            ListType<Set<ByteBuffer>> list2 = ListType.getInstance(SetType.getInstance(new TupleType(asList(udtWith3Fields, Int32Type.instance)), isMultiCell), isMultiCell);
-            checkComparisonResults(list1, list2);
-        }
-    }
-
-    @Test
-    public void testInvalidComparison()
-    {
-        assertInvalidComparison("Trying to compare 2 different types: org.apache.cassandra.db.marshal.UserType(ks,6d7954797065,61:org.apache.cassandra.db.marshal.Int32Type,62:org.apache.cassandra.db.marshal.Int32Type) and org.apache.cassandra.db.marshal.Int32Type",
-                                udtWith2Fields,
-                                Int32Type.instance);
-        assertInvalidComparison("Trying to compare 2 different types: org.apache.cassandra.db.marshal.UTF8Type and org.apache.cassandra.db.marshal.InetAddressType",
-                                SetType.getInstance(UTF8Type.instance, true),
-                                SetType.getInstance(InetAddressType.instance, true));
-        assertInvalidComparison("Trying to compare 2 different types: org.apache.cassandra.db.marshal.UTF8Type and org.apache.cassandra.db.marshal.InetAddressType",
-                                ListType.getInstance(UTF8Type.instance, true),
-                                ListType.getInstance(InetAddressType.instance, true));
-        assertInvalidComparison("Trying to compare 2 different types: org.apache.cassandra.db.marshal.UTF8Type and org.apache.cassandra.db.marshal.InetAddressType",
-                                MapType.getInstance(UTF8Type.instance, IntegerType.instance, true),
-                                MapType.getInstance(InetAddressType.instance, IntegerType.instance, true));
-        assertInvalidComparison("Trying to compare 2 different types: org.apache.cassandra.db.marshal.UTF8Type and org.apache.cassandra.db.marshal.InetAddressType",
-                                MapType.getInstance(IntegerType.instance, UTF8Type.instance, true),
-                                MapType.getInstance(IntegerType.instance, InetAddressType.instance, true));
-    }
-
-    private void assertInvalidComparison(String expectedMessage, AbstractType<?> oldVersion, AbstractType<?> newVersion)
-    {
-        try
-        {
-            checkComparisonResults(oldVersion, newVersion);
-            fail("comparison doesn't throw expected IllegalArgumentException: " + expectedMessage);
-        }
-        catch (IllegalArgumentException e)
-        {
-            assertEquals(e.getMessage(), expectedMessage);
-        }
-    }
-
-    private void checkComparisonResults(AbstractType<?> oldVersion, AbstractType<?> newVersion)
-    {
-        assertEquals(0, compare(oldVersion, oldVersion));
-        assertEquals(0, compare(newVersion, newVersion));
-        assertEquals(-1, compare(oldVersion, newVersion));
-        assertEquals(1, compare(newVersion, oldVersion));
-    }
-
-    private int compare(AbstractType<?> left, AbstractType<?> right)
-    {
-        return AbstractTypeVersionComparator.INSTANCE.compare(left, right);
-    }
-}
diff --git a/test/unit/org/apache/cassandra/db/rows/ColumnDefinitionVersionComparatorTest.java b/test/unit/org/apache/cassandra/db/rows/ColumnDefinitionVersionComparatorTest.java
new file mode 100644
index 0000000..97db48f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/rows/ColumnDefinitionVersionComparatorTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.db.rows;
+
+import java.nio.ByteBuffer;
+import java.util.Set;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.db.marshal.*;
+
+import static java.util.Arrays.asList;
+import static org.apache.cassandra.cql3.FieldIdentifier.forUnquoted;
+import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class ColumnDefinitionVersionComparatorTest
+{
+    private UserType udtWith2Fields;
+    private UserType udtWith3Fields;
+
+    @Before
+    public void setUp()
+    {
+        udtWith2Fields = new UserType("ks",
+                                      bytes("myType"),
+                                      asList(forUnquoted("a"), forUnquoted("b")),
+                                      asList(Int32Type.instance, Int32Type.instance),
+                                      false);
+        udtWith3Fields = new UserType("ks",
+                                      bytes("myType"),
+                                      asList(forUnquoted("a"), forUnquoted("b"), forUnquoted("c")),
+                                      asList(Int32Type.instance, Int32Type.instance, Int32Type.instance),
+                                      false);
+    }
+
+    @After
+    public void tearDown()
+    {
+        udtWith2Fields = null;
+        udtWith3Fields = null;
+    }
+
+    @Test
+    public void testWithSimpleTypes()
+    {
+        checkComparisonResults(Int32Type.instance, BytesType.instance);
+        checkComparisonResults(EmptyType.instance, BytesType.instance);
+    }
+
+    @Test
+    public void testWithTuples()
+    {
+        checkComparisonResults(new TupleType(asList(Int32Type.instance, Int32Type.instance)),
+                               new TupleType(asList(Int32Type.instance, Int32Type.instance, Int32Type.instance)));
+    }
+
+    @Test
+    public void testWithUDTs()
+    {
+        checkComparisonResults(udtWith2Fields, udtWith3Fields);
+    }
+
+    @Test
+    public void testWithUDTsNestedWithinSet()
+    {
+        for (boolean isMultiCell : new boolean[]{false, true})
+        {
+            SetType<ByteBuffer> set1 = SetType.getInstance(udtWith2Fields, isMultiCell);
+            SetType<ByteBuffer> set2 = SetType.getInstance(udtWith3Fields, isMultiCell);
+            checkComparisonResults(set1, set2);
+        }
+    }
+
+    @Test
+    public void testWithUDTsNestedWithinList()
+    {
+        for (boolean isMultiCell : new boolean[]{false, true})
+        {
+            ListType<ByteBuffer> list1 = ListType.getInstance(udtWith2Fields, isMultiCell);
+            ListType<ByteBuffer> list2 = ListType.getInstance(udtWith3Fields, isMultiCell);
+            checkComparisonResults(list1, list2);
+        }
+    }
+
+    @Test
+    public void testWithUDTsNestedWithinMap()
+    {
+        for (boolean isMultiCell : new boolean[]{false, true})
+        {
+            MapType<ByteBuffer, Integer> map1 = MapType.getInstance(udtWith2Fields, Int32Type.instance, isMultiCell);
+            MapType<ByteBuffer, Integer> map2 = MapType.getInstance(udtWith3Fields, Int32Type.instance, isMultiCell);
+            checkComparisonResults(map1, map2);
+        }
+
+        for (boolean isMultiCell : new boolean[]{false, true})
+        {
+            MapType<Integer, ByteBuffer> map1 = MapType.getInstance(Int32Type.instance, udtWith2Fields, isMultiCell);
+            MapType<Integer, ByteBuffer> map2 = MapType.getInstance(Int32Type.instance, udtWith3Fields, isMultiCell);
+            checkComparisonResults(map1, map2);
+        }
+    }
+
+    @Test
+    public void testWithUDTsNestedWithinTuple()
+    {
+        TupleType tuple1 = new TupleType(asList(udtWith2Fields, Int32Type.instance));
+        TupleType tuple2 = new TupleType(asList(udtWith3Fields, Int32Type.instance));
+        checkComparisonResults(tuple1, tuple2);
+    }
+
+    @Test
+    public void testWithUDTsNestedWithinComposite()
+    {
+        CompositeType composite1 = CompositeType.getInstance(asList(udtWith2Fields, Int32Type.instance));
+        CompositeType composite2 = CompositeType.getInstance(asList(udtWith3Fields, Int32Type.instance));
+        checkComparisonResults(composite1, composite2);
+    }
+
+    @Test
+    public void testWithDeeplyNestedUDT()
+    {
+        for (boolean isMultiCell : new boolean[]{false, true})
+        {
+            ListType<Set<ByteBuffer>> list1 = ListType.getInstance(SetType.getInstance(new TupleType(asList(udtWith2Fields, Int32Type.instance)), isMultiCell), isMultiCell);
+            ListType<Set<ByteBuffer>> list2 = ListType.getInstance(SetType.getInstance(new TupleType(asList(udtWith3Fields, Int32Type.instance)), isMultiCell), isMultiCell);
+            checkComparisonResults(list1, list2);
+        }
+    }
+
+    @Test
+    public void testInvalidComparison()
+    {
+        assertInvalidComparison("Found 2 incompatible versions of column c in ks.t: one of type org.apache.cassandra.db.marshal.Int32Type and one of type org.apache.cassandra.db.marshal.UTF8Type (but both types are incompatible)",
+                                Int32Type.instance,
+                                UTF8Type.instance);
+        assertInvalidComparison("Found 2 incompatible versions of column c in ks.t: one of type org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.UserType(ks,6d7954797065,61:org.apache.cassandra.db.marshal.Int32Type,62:org.apache.cassandra.db.marshal.Int32Type)) and one of type org.apache.cassandra.db.marshal.Int32Type (but both types are incompatible)",
+                                udtWith2Fields,
+                                Int32Type.instance);
+        assertInvalidComparison("Found 2 incompatible versions of column c in ks.t: one of type org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type) and one of type org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.InetAddressType) (but both types are incompatible)",
+                                SetType.getInstance(UTF8Type.instance, true),
+                                SetType.getInstance(InetAddressType.instance, true));
+        assertInvalidComparison("Found 2 incompatible versions of column c in ks.t: one of type org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type) and one of type org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.InetAddressType) (but both types are incompatible)",
+                                ListType.getInstance(UTF8Type.instance, true),
+                                ListType.getInstance(InetAddressType.instance, true));
+        assertInvalidComparison("Found 2 incompatible versions of column c in ks.t: one of type org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.IntegerType) and one of type org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.InetAddressType,org.apache.cassandra.db.marshal.IntegerType) (but both types are incompatible)",
+                                MapType.getInstance(UTF8Type.instance, IntegerType.instance, true),
+                                MapType.getInstance(InetAddressType.instance, IntegerType.instance, true));
+        assertInvalidComparison("Found 2 incompatible versions of column c in ks.t: one of type org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.IntegerType,org.apache.cassandra.db.marshal.UTF8Type) and one of type org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.IntegerType,org.apache.cassandra.db.marshal.InetAddressType) (but both types are incompatible)",
+                                MapType.getInstance(IntegerType.instance, UTF8Type.instance, true),
+                                MapType.getInstance(IntegerType.instance, InetAddressType.instance, true));
+    }
+
+    private void assertInvalidComparison(String expectedMessage, AbstractType<?> oldVersion, AbstractType<?> newVersion)
+    {
+        try
+        {
+            checkComparisonResults(oldVersion, newVersion);
+            fail("comparison doesn't throw expected IllegalArgumentException: " + expectedMessage);
+        }
+        catch (IllegalArgumentException e)
+        {
+            System.out.println(e.getMessage());
+            assertEquals(expectedMessage, e.getMessage());
+        }
+    }
+
+    private void checkComparisonResults(AbstractType<?> oldVersion, AbstractType<?> newVersion)
+    {
+        assertEquals(0, compare(oldVersion, oldVersion));
+        assertEquals(0, compare(newVersion, newVersion));
+        assertEquals(-1, compare(oldVersion, newVersion));
+        assertEquals(1, compare(newVersion, oldVersion));
+    }
+
+    private static int compare(AbstractType<?> left, AbstractType<?> right)
+    {
+        ColumnDefinition v1 = ColumnDefinition.regularDef("ks", "t", "c", left);
+        ColumnDefinition v2 = ColumnDefinition.regularDef("ks", "t", "c", right);
+        return ColumnDefinitionVersionComparator.INSTANCE.compare(v1, v2);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/rows/DigestBackwardCompatibilityTest.java b/test/unit/org/apache/cassandra/db/rows/DigestBackwardCompatibilityTest.java
index c8f5cb1..a72d397 100644
--- a/test/unit/org/apache/cassandra/db/rows/DigestBackwardCompatibilityTest.java
+++ b/test/unit/org/apache/cassandra/db/rows/DigestBackwardCompatibilityTest.java
@@ -18,7 +18,6 @@
 package org.apache.cassandra.db.rows;
 
 import java.nio.ByteBuffer;
-import java.util.*;
 import java.security.MessageDigest;
 
 import org.junit.Test;
@@ -28,7 +27,6 @@
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.filter.*;
 import org.apache.cassandra.db.partitions.*;
 import org.apache.cassandra.db.context.CounterContext;
 import org.apache.cassandra.net.MessagingService;
@@ -78,7 +76,6 @@
         createTable("CREATE TABLE %s (k text, t int, v1 text, v2 int, PRIMARY KEY (k, t))");
 
         String key = "someKey";
-        int N = 10;
 
         for (int i = 0; i < 10; i++)
             execute("INSERT INTO %s(k, t, v1, v2) VALUES (?, ?, ?, ?) USING TIMESTAMP ? AND TTL ?", key, i, "v" + i, i, 1L, 200);
@@ -105,7 +102,6 @@
         createTable("CREATE TABLE %s (k text, t int, v text, PRIMARY KEY (k, t)) WITH COMPACT STORAGE");
 
         String key = "someKey";
-        int N = 10;
 
         for (int i = 0; i < 10; i++)
             execute("INSERT INTO %s(k, t, v) VALUES (?, ?, ?) USING TIMESTAMP ? AND TTL ?", key, i, "v" + i, 1L, 200);
@@ -174,7 +170,7 @@
         CFMetaData metadata = getCurrentColumnFamilyStore().metadata;
         ColumnDefinition column = metadata.getColumnDefinition(ByteBufferUtil.bytes("c"));
         ByteBuffer value = CounterContext.instance().createGlobal(CounterId.fromInt(1), 1L, 42L);
-        Row row = BTreeRow.singleCellRow(Clustering.STATIC_CLUSTERING, BufferCell.live(metadata, column, 0L, value));
+        Row row = BTreeRow.singleCellRow(Clustering.STATIC_CLUSTERING, BufferCell.live(column, 0L, value));
 
         new Mutation(PartitionUpdate.singleRowUpdate(metadata, Util.dk(key), row)).applyUnsafe();
 
diff --git a/test/unit/org/apache/cassandra/db/rows/RowAndDeletionMergeIteratorTest.java b/test/unit/org/apache/cassandra/db/rows/RowAndDeletionMergeIteratorTest.java
index dd88704..2f48000 100644
--- a/test/unit/org/apache/cassandra/db/rows/RowAndDeletionMergeIteratorTest.java
+++ b/test/unit/org/apache/cassandra/db/rows/RowAndDeletionMergeIteratorTest.java
@@ -29,7 +29,7 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
-import org.apache.cassandra.db.Slice.Bound;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ClusteringPrefix;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.db.*;
@@ -42,9 +42,6 @@
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.db.ColumnFamilyStore;
-import org.apache.cassandra.db.DecoratedKey;
-import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.marshal.AsciiType;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.schema.KeyspaceParams;
@@ -65,6 +62,7 @@
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
+        DatabaseDescriptor.daemonInitialization();
         CFMetaData cfMetadata = CFMetaData.Builder.create(KEYSPACE1, CF_STANDARD1)
                                                   .addPartitionKey("key", AsciiType.instance)
                                                   .addClusteringColumn("col1", Int32Type.instance)
@@ -131,7 +129,7 @@
         assertRtMarker(iterator.next(), ClusteringPrefix.Kind.INCL_START_BOUND, 4);
 
         assertTrue(iterator.hasNext());
-        assertRtMarker(iterator.next(), Bound.TOP);
+        assertRtMarker(iterator.next(), ClusteringBound.TOP);
 
         assertFalse(iterator.hasNext());
     }
@@ -149,7 +147,7 @@
         UnfilteredRowIterator iterator = createMergeIterator(rowIterator, rangeTombstoneIterator, false);
 
         assertTrue(iterator.hasNext());
-        assertRtMarker(iterator.next(), Bound.BOTTOM);
+        assertRtMarker(iterator.next(), ClusteringBound.BOTTOM);
 
         assertTrue(iterator.hasNext());
         assertRtMarker(iterator.next(), ClusteringPrefix.Kind.INCL_END_BOUND, 0);
@@ -194,7 +192,7 @@
         assertRtMarker(iterator.next(), ClusteringPrefix.Kind.EXCL_START_BOUND, 2);
 
         assertTrue(iterator.hasNext());
-        assertRtMarker(iterator.next(), Bound.TOP);
+        assertRtMarker(iterator.next(), ClusteringBound.TOP);
 
         assertFalse(iterator.hasNext());
     }
@@ -213,7 +211,7 @@
         UnfilteredRowIterator iterator = createMergeIterator(rowIterator, rangeTombstoneIterator, false);
 
         assertTrue(iterator.hasNext());
-        assertRtMarker(iterator.next(), Bound.BOTTOM);
+        assertRtMarker(iterator.next(), ClusteringBound.BOTTOM);
 
         assertTrue(iterator.hasNext());
         assertRtMarker(iterator.next(), ClusteringPrefix.Kind.INCL_END_BOUND, 0);
@@ -228,7 +226,7 @@
         assertRtMarker(iterator.next(), ClusteringPrefix.Kind.EXCL_START_BOUND, 2);
 
         assertTrue(iterator.hasNext());
-        assertRtMarker(iterator.next(), Bound.TOP);
+        assertRtMarker(iterator.next(), ClusteringBound.TOP);
 
         assertFalse(iterator.hasNext());
     }
@@ -256,13 +254,13 @@
         UnfilteredRowIterator iterator = createMergeIterator(rowIterator, rangeTombstoneIterator, false);
 
         assertTrue(iterator.hasNext());
-        assertRtMarker(iterator.next(), Bound.BOTTOM);
+        assertRtMarker(iterator.next(), ClusteringBound.BOTTOM);
 
         assertTrue(iterator.hasNext());
         assertRtMarker(iterator.next(), ClusteringPrefix.Kind.INCL_END_EXCL_START_BOUNDARY, 2);
 
         assertTrue(iterator.hasNext());
-        assertRtMarker(iterator.next(), Bound.TOP);
+        assertRtMarker(iterator.next(), ClusteringBound.TOP);
 
         assertFalse(iterator.hasNext());
     }
@@ -283,13 +281,13 @@
         UnfilteredRowIterator iterator = createMergeIterator(rowIterator, rangeTombstoneIterator, false);
 
         assertTrue(iterator.hasNext());
-        assertRtMarker(iterator.next(), Bound.BOTTOM);
+        assertRtMarker(iterator.next(), ClusteringBound.BOTTOM);
 
         assertTrue(iterator.hasNext());
         assertRtMarker(iterator.next(), ClusteringPrefix.Kind.EXCL_END_INCL_START_BOUNDARY, 2);
 
         assertTrue(iterator.hasNext());
-        assertRtMarker(iterator.next(), Bound.TOP);
+        assertRtMarker(iterator.next(), ClusteringBound.TOP);
 
         assertFalse(iterator.hasNext());
     }
@@ -304,7 +302,7 @@
         UnfilteredRowIterator iterator = createMergeIterator(rowIterator, rangeTombstoneIterator, false);
 
         assertTrue(iterator.hasNext());
-        assertRtMarker(iterator.next(), Bound.BOTTOM);
+        assertRtMarker(iterator.next(), ClusteringBound.BOTTOM);
 
         assertTrue(iterator.hasNext());
         assertRow(iterator.next(), 0);
@@ -349,7 +347,6 @@
         assertFalse(iterator.hasNext());
     }
 
-
     /**
      * RTL doesn't correctly merge range tombstones in some situations (see CASSANDRA-14894)
      */
@@ -373,7 +370,7 @@
         }
     }
 
-    private void assertRtMarker(Unfiltered unfiltered, Bound bound)
+    private void assertRtMarker(Unfiltered unfiltered, ClusteringBoundOrBoundary bound)
     {
         assertEquals(Unfiltered.Kind.RANGE_TOMBSTONE_MARKER, unfiltered.kind());
         assertEquals(bound, unfiltered.clustering());
@@ -428,38 +425,38 @@
 
     private void addRow(PartitionUpdate update, int col1, int a)
     {
-        update.add(BTreeRow.singleCellRow(update.metadata().comparator.make(col1), makeCell(cfm, defA, a, 0)));
+        update.add(BTreeRow.singleCellRow(update.metadata().comparator.make(col1), makeCell(defA, a, 0)));
     }
 
-    private Cell makeCell(CFMetaData cfm, ColumnDefinition columnDefinition, int value, long timestamp)
+    private Cell makeCell(ColumnDefinition columnDefinition, int value, long timestamp)
     {
-        return BufferCell.live(cfm, columnDefinition, timestamp, ((AbstractType)columnDefinition.cellValueType()).decompose(value));
+        return BufferCell.live(columnDefinition, timestamp, ((AbstractType)columnDefinition.cellValueType()).decompose(value));
     }
 
     private static RangeTombstone atLeast(int start, long tstamp, int delTime)
     {
-        return new RangeTombstone(Slice.make(Slice.Bound.inclusiveStartOf(bb(start)), Slice.Bound.TOP), new DeletionTime(tstamp, delTime));
+        return new RangeTombstone(Slice.make(ClusteringBound.inclusiveStartOf(bb(start)), ClusteringBound.TOP), new DeletionTime(tstamp, delTime));
     }
 
     private static RangeTombstone atMost(int end, long tstamp, int delTime)
     {
-        return new RangeTombstone(Slice.make(Slice.Bound.BOTTOM, Slice.Bound.inclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
+        return new RangeTombstone(Slice.make(ClusteringBound.BOTTOM, ClusteringBound.inclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
     }
 
     private static RangeTombstone lessThan(int end, long tstamp, int delTime)
     {
-        return new RangeTombstone(Slice.make(Slice.Bound.BOTTOM, Slice.Bound.exclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
+        return new RangeTombstone(Slice.make(ClusteringBound.BOTTOM, ClusteringBound.exclusiveEndOf(bb(end))), new DeletionTime(tstamp, delTime));
     }
 
     private static RangeTombstone greaterThan(int start, long tstamp, int delTime)
     {
-        return new RangeTombstone(Slice.make(Slice.Bound.exclusiveStartOf(bb(start)), Slice.Bound.TOP), new DeletionTime(tstamp, delTime));
+        return new RangeTombstone(Slice.make(ClusteringBound.exclusiveStartOf(bb(start)), ClusteringBound.TOP), new DeletionTime(tstamp, delTime));
     }
 
     private static RangeTombstone rt(int start, boolean startInclusive, int end, boolean endInclusive, long tstamp, int delTime)
     {
-        Slice.Bound startBound = startInclusive ? Slice.Bound.inclusiveStartOf(bb(start)) : Slice.Bound.exclusiveStartOf(bb(start));
-        Slice.Bound endBound = endInclusive ? Slice.Bound.inclusiveEndOf(bb(end)) : Slice.Bound.exclusiveEndOf(bb(end));
+        ClusteringBound startBound = startInclusive ? ClusteringBound.inclusiveStartOf(bb(start)) : ClusteringBound.exclusiveStartOf(bb(start));
+        ClusteringBound endBound = endInclusive ? ClusteringBound.inclusiveEndOf(bb(end)) : ClusteringBound.exclusiveEndOf(bb(end));
 
         return new RangeTombstone(Slice.make(startBound, endBound), new DeletionTime(tstamp, delTime));
     }
diff --git a/test/unit/org/apache/cassandra/db/rows/RowsTest.java b/test/unit/org/apache/cassandra/db/rows/RowsTest.java
index 8683808..accb4c9 100644
--- a/test/unit/org/apache/cassandra/db/rows/RowsTest.java
+++ b/test/unit/org/apache/cassandra/db/rows/RowsTest.java
@@ -35,6 +35,7 @@
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.Clustering;
 import org.apache.cassandra.db.DeletionTime;
@@ -56,6 +57,7 @@
 
     static
     {
+        DatabaseDescriptor.daemonInitialization();
         kcvm = CFMetaData.Builder.create(KEYSPACE, KCVM_TABLE)
                                  .addPartitionKey("k", IntegerType.instance)
                                  .addClusteringColumn("c", IntegerType.instance)
@@ -210,15 +212,15 @@
         long ts = secondToTs(now);
         Row.Builder builder = BTreeRow.unsortedBuilder(now);
         builder.newRow(c);
-        builder.addPrimaryKeyLivenessInfo(LivenessInfo.create(kcvm, ts, now));
+        builder.addPrimaryKeyLivenessInfo(LivenessInfo.create(ts, now));
         if (vVal != null)
         {
-            builder.addCell(BufferCell.live(kcvm, v, ts, vVal));
+            builder.addCell(BufferCell.live(v, ts, vVal));
         }
         if (mKey != null && mVal != null)
         {
             builder.addComplexDeletion(m, new DeletionTime(ts - 1, now));
-            builder.addCell(BufferCell.live(kcvm, m, ts, mVal, CellPath.create(mKey)));
+            builder.addCell(BufferCell.live(m, ts, mVal, CellPath.create(mKey)));
         }
 
         return builder;
@@ -231,13 +233,13 @@
         long ts = secondToTs(now);
         Row.Builder originalBuilder = BTreeRow.unsortedBuilder(now);
         originalBuilder.newRow(c1);
-        LivenessInfo liveness = LivenessInfo.create(kcvm, ts, now);
+        LivenessInfo liveness = LivenessInfo.create(ts, now);
         originalBuilder.addPrimaryKeyLivenessInfo(liveness);
         DeletionTime complexDeletion = new DeletionTime(ts-1, now);
         originalBuilder.addComplexDeletion(m, complexDeletion);
-        List<Cell> expectedCells = Lists.newArrayList(BufferCell.live(kcvm, v, secondToTs(now), BB1),
-                                                      BufferCell.live(kcvm, m, secondToTs(now), BB1, CellPath.create(BB1)),
-                                                      BufferCell.live(kcvm, m, secondToTs(now), BB2, CellPath.create(BB2)));
+        List<Cell> expectedCells = Lists.newArrayList(BufferCell.live(v, secondToTs(now), BB1),
+                                                      BufferCell.live(m, secondToTs(now), BB1, CellPath.create(BB1)),
+                                                      BufferCell.live(m, secondToTs(now), BB2, CellPath.create(BB2)));
         expectedCells.forEach(originalBuilder::addCell);
         // We need to use ts-1 so the deletion doesn't shadow what we've created
         Row.Deletion rowDeletion = new Row.Deletion(new DeletionTime(ts-1, now), false);
@@ -260,13 +262,13 @@
         long ts = secondToTs(now);
         Row.Builder builder = BTreeRow.unsortedBuilder(now);
         builder.newRow(c1);
-        LivenessInfo liveness = LivenessInfo.create(kcvm, ts, now);
+        LivenessInfo liveness = LivenessInfo.create(ts, now);
         builder.addPrimaryKeyLivenessInfo(liveness);
         DeletionTime complexDeletion = new DeletionTime(ts-1, now);
         builder.addComplexDeletion(m, complexDeletion);
-        List<Cell> expectedCells = Lists.newArrayList(BufferCell.live(kcvm, v, ts, BB1),
-                                                      BufferCell.live(kcvm, m, ts, BB1, CellPath.create(BB1)),
-                                                      BufferCell.live(kcvm, m, ts, BB2, CellPath.create(BB2)));
+        List<Cell> expectedCells = Lists.newArrayList(BufferCell.live(v, ts, BB1),
+                                                      BufferCell.live(m, ts, BB1, CellPath.create(BB1)),
+                                                      BufferCell.live(m, ts, BB2, CellPath.create(BB2)));
         expectedCells.forEach(builder::addCell);
         // We need to use ts-1 so the deletion doesn't shadow what we've created
         Row.Deletion rowDeletion = new Row.Deletion(new DeletionTime(ts-1, now), false);
@@ -298,14 +300,14 @@
         long ts1 = secondToTs(now1);
         Row.Builder r1Builder = BTreeRow.unsortedBuilder(now1);
         r1Builder.newRow(c1);
-        LivenessInfo r1Liveness = LivenessInfo.create(kcvm, ts1, now1);
+        LivenessInfo r1Liveness = LivenessInfo.create(ts1, now1);
         r1Builder.addPrimaryKeyLivenessInfo(r1Liveness);
         DeletionTime r1ComplexDeletion = new DeletionTime(ts1-1, now1);
         r1Builder.addComplexDeletion(m, r1ComplexDeletion);
 
-        Cell r1v = BufferCell.live(kcvm, v, ts1, BB1);
-        Cell r1m1 = BufferCell.live(kcvm, m, ts1, BB1, CellPath.create(BB1));
-        Cell r1m2 = BufferCell.live(kcvm, m, ts1, BB2, CellPath.create(BB2));
+        Cell r1v = BufferCell.live(v, ts1, BB1);
+        Cell r1m1 = BufferCell.live(m, ts1, BB1, CellPath.create(BB1));
+        Cell r1m2 = BufferCell.live(m, ts1, BB2, CellPath.create(BB2));
         List<Cell> r1ExpectedCells = Lists.newArrayList(r1v, r1m1, r1m2);
 
         r1ExpectedCells.forEach(r1Builder::addCell);
@@ -314,12 +316,12 @@
         long ts2 = secondToTs(now2);
         Row.Builder r2Builder = BTreeRow.unsortedBuilder(now2);
         r2Builder.newRow(c1);
-        LivenessInfo r2Liveness = LivenessInfo.create(kcvm, ts2, now2);
+        LivenessInfo r2Liveness = LivenessInfo.create(ts2, now2);
         r2Builder.addPrimaryKeyLivenessInfo(r2Liveness);
-        Cell r2v = BufferCell.live(kcvm, v, ts2, BB2);
-        Cell r2m2 = BufferCell.live(kcvm, m, ts2, BB1, CellPath.create(BB2));
-        Cell r2m3 = BufferCell.live(kcvm, m, ts2, BB2, CellPath.create(BB3));
-        Cell r2m4 = BufferCell.live(kcvm, m, ts2, BB3, CellPath.create(BB4));
+        Cell r2v = BufferCell.live(v, ts2, BB2);
+        Cell r2m2 = BufferCell.live(m, ts2, BB1, CellPath.create(BB2));
+        Cell r2m3 = BufferCell.live(m, ts2, BB2, CellPath.create(BB3));
+        Cell r2m4 = BufferCell.live(m, ts2, BB3, CellPath.create(BB4));
         List<Cell> r2ExpectedCells = Lists.newArrayList(r2v, r2m2, r2m3, r2m4);
 
         r2ExpectedCells.forEach(r2Builder::addCell);
@@ -374,7 +376,7 @@
         long ts1 = secondToTs(now1);
         Row.Builder r1Builder = BTreeRow.unsortedBuilder(now1);
         r1Builder.newRow(c1);
-        LivenessInfo r1Liveness = LivenessInfo.create(kcvm, ts1, now1);
+        LivenessInfo r1Liveness = LivenessInfo.create(ts1, now1);
         r1Builder.addPrimaryKeyLivenessInfo(r1Liveness);
 
         // mergedData == null
@@ -382,14 +384,14 @@
         long ts2 = secondToTs(now2);
         Row.Builder r2Builder = BTreeRow.unsortedBuilder(now2);
         r2Builder.newRow(c1);
-        LivenessInfo r2Liveness = LivenessInfo.create(kcvm, ts2, now2);
+        LivenessInfo r2Liveness = LivenessInfo.create(ts2, now2);
         r2Builder.addPrimaryKeyLivenessInfo(r2Liveness);
         DeletionTime r2ComplexDeletion = new DeletionTime(ts2-1, now2);
         r2Builder.addComplexDeletion(m, r2ComplexDeletion);
-        Cell r2v = BufferCell.live(kcvm, v, ts2, BB2);
-        Cell r2m2 = BufferCell.live(kcvm, m, ts2, BB1, CellPath.create(BB2));
-        Cell r2m3 = BufferCell.live(kcvm, m, ts2, BB2, CellPath.create(BB3));
-        Cell r2m4 = BufferCell.live(kcvm, m, ts2, BB3, CellPath.create(BB4));
+        Cell r2v = BufferCell.live(v, ts2, BB2);
+        Cell r2m2 = BufferCell.live(m, ts2, BB1, CellPath.create(BB2));
+        Cell r2m3 = BufferCell.live(m, ts2, BB2, CellPath.create(BB3));
+        Cell r2m4 = BufferCell.live(m, ts2, BB3, CellPath.create(BB4));
         List<Cell> r2ExpectedCells = Lists.newArrayList(r2v, r2m2, r2m3, r2m4);
 
         r2ExpectedCells.forEach(r2Builder::addCell);
@@ -428,7 +430,7 @@
         long ts1 = secondToTs(now1);
         Row.Builder r1Builder = BTreeRow.unsortedBuilder(now1);
         r1Builder.newRow(c1);
-        LivenessInfo r1Liveness = LivenessInfo.create(kcvm, ts1, now1);
+        LivenessInfo r1Liveness = LivenessInfo.create(ts1, now1);
         r1Builder.addPrimaryKeyLivenessInfo(r1Liveness);
 
         // mergedData == null
@@ -436,14 +438,14 @@
         long ts2 = secondToTs(now2);
         Row.Builder r2Builder = BTreeRow.unsortedBuilder(now2);
         r2Builder.newRow(c1);
-        LivenessInfo r2Liveness = LivenessInfo.create(kcvm, ts2, now2);
+        LivenessInfo r2Liveness = LivenessInfo.create(ts2, now2);
         r2Builder.addPrimaryKeyLivenessInfo(r2Liveness);
         DeletionTime r2ComplexDeletion = new DeletionTime(ts2-1, now2);
         r2Builder.addComplexDeletion(m, r2ComplexDeletion);
-        Cell r2v = BufferCell.live(kcvm, v, ts2, BB2);
-        Cell r2m2 = BufferCell.live(kcvm, m, ts2, BB1, CellPath.create(BB2));
-        Cell r2m3 = BufferCell.live(kcvm, m, ts2, BB2, CellPath.create(BB3));
-        Cell r2m4 = BufferCell.live(kcvm, m, ts2, BB3, CellPath.create(BB4));
+        Cell r2v = BufferCell.live(v, ts2, BB2);
+        Cell r2m2 = BufferCell.live(m, ts2, BB1, CellPath.create(BB2));
+        Cell r2m3 = BufferCell.live(m, ts2, BB2, CellPath.create(BB3));
+        Cell r2m4 = BufferCell.live(m, ts2, BB3, CellPath.create(BB4));
         List<Cell> r2ExpectedCells = Lists.newArrayList(r2v, r2m2, r2m3, r2m4);
 
         r2ExpectedCells.forEach(r2Builder::addCell);
@@ -481,8 +483,8 @@
         int now2 = now1 + 1;
         long ts2 = secondToTs(now2);
 
-        Cell expectedVCell = BufferCell.live(kcvm, v, ts2, BB2);
-        Cell expectedMCell = BufferCell.live(kcvm, m, ts2, BB2, CellPath.create(BB1));
+        Cell expectedVCell = BufferCell.live(v, ts2, BB2);
+        Cell expectedMCell = BufferCell.live(m, ts2, BB2, CellPath.create(BB1));
         DeletionTime expectedComplexDeletionTime = new DeletionTime(ts2 - 1, now2);
 
         Row.Builder updateBuilder = createBuilder(c1, now2, null, null, null);
@@ -494,7 +496,7 @@
         long td = Rows.merge(existingBuilder.build(), updateBuilder.build(), builder, now2 + 1);
 
         Assert.assertEquals(c1, builder.clustering);
-        Assert.assertEquals(LivenessInfo.create(kcvm, ts2, now2), builder.livenessInfo);
+        Assert.assertEquals(LivenessInfo.create(ts2, now2), builder.livenessInfo);
         Assert.assertEquals(Lists.newArrayList(Pair.create(m, new DeletionTime(ts2-1, now2))), builder.complexDeletions);
 
         Assert.assertEquals(2, builder.cells.size());
diff --git a/test/unit/org/apache/cassandra/db/rows/UnfilteredRowIteratorsMergeTest.java b/test/unit/org/apache/cassandra/db/rows/UnfilteredRowIteratorsMergeTest.java
index 7637fa0..4578ad1 100644
--- a/test/unit/org/apache/cassandra/db/rows/UnfilteredRowIteratorsMergeTest.java
+++ b/test/unit/org/apache/cassandra/db/rows/UnfilteredRowIteratorsMergeTest.java
@@ -32,8 +32,8 @@
 
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.Slice.Bound;
 import org.apache.cassandra.db.marshal.AsciiType;
 import org.apache.cassandra.db.marshal.Int32Type;
 import org.apache.cassandra.db.rows.Unfiltered.Kind;
@@ -41,6 +41,10 @@
 
 public class UnfilteredRowIteratorsMergeTest
 {
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
     static DecoratedKey partitionKey = Util.dk("key");
     static DeletionTime partitionLevelDeletion = DeletionTime.LIVE;
     static CFMetaData metadata = CFMetaData.Builder.create("UnfilteredRowIteratorsMergeTest", "Test").
@@ -97,9 +101,11 @@
     @SuppressWarnings("unused")
     public void testTombstoneMerge(boolean reversed, boolean iterations)
     {
+        this.reversed = reversed;
+        UnfilteredRowsGenerator generator = new UnfilteredRowsGenerator(comparator, reversed);
+
         for (int seed = 1; seed <= 100; ++seed)
         {
-            this.reversed = reversed;
             if (ITEMS <= 20)
                 System.out.println("\nSeed " + seed);
 
@@ -113,27 +119,29 @@
             if (ITEMS <= 20)
                 System.out.println("Merging");
             for (int i=0; i<ITERATORS; ++i)
-                sources.add(generateSource(r, timeGenerators.get(r.nextInt(timeGenerators.size()))));
+                sources.add(generator.generateSource(r, ITEMS, RANGE, DEL_RANGE, timeGenerators.get(r.nextInt(timeGenerators.size()))));
             List<Unfiltered> merged = merge(sources, iterations);
     
             if (ITEMS <= 20)
                 System.out.println("results in");
             if (ITEMS <= 20)
-                dumpList(merged);
-            verifyEquivalent(sources, merged);
-            verifyValid(merged);
+                generator.dumpList(merged);
+            verifyEquivalent(sources, merged, generator);
+            generator.verifyValid(merged);
             if (reversed)
             {
                 Collections.reverse(merged);
-                this.reversed = false;
-                verifyValid(merged);
+                generator.verifyValid(merged, false);
             }
         }
     }
 
     private List<Unfiltered> merge(List<List<Unfiltered>> sources, boolean iterations)
     {
-        List<UnfilteredRowIterator> us = sources.stream().map(l -> new Source(l.iterator())).collect(Collectors.toList());
+        List<UnfilteredRowIterator> us = sources.
+                stream().
+                map(l -> new UnfilteredRowsGenerator.Source(l.iterator(), metadata, partitionKey, DeletionTime.LIVE, reversed)).
+                collect(Collectors.toList());
         List<Unfiltered> merged = new ArrayList<>();
         Iterators.addAll(merged, mergeIterators(us, iterations));
         return merged;
@@ -230,9 +238,12 @@
             if (prev != null && curr != null && prev.isClose(false) && curr.isOpen(false) && prev.clustering().invert().equals(curr.clustering()))
             {
                 // Join. Prefer not to use merger to check its correctness.
-                RangeTombstone.Bound b = prev.clustering();
-                b = b.withNewKind(b.isInclusive() ? RangeTombstone.Bound.Kind.INCL_END_EXCL_START_BOUNDARY : RangeTombstone.Bound.Kind.EXCL_END_INCL_START_BOUNDARY);
-                prev = new RangeTombstoneBoundaryMarker(b, prev.closeDeletionTime(false), curr.openDeletionTime(false));
+                ClusteringBound b = ((RangeTombstoneBoundMarker) prev).clustering();
+                ClusteringBoundary boundary = ClusteringBoundary.create(b.isInclusive()
+                                                                            ? ClusteringPrefix.Kind.INCL_END_EXCL_START_BOUNDARY
+                                                                            : ClusteringPrefix.Kind.EXCL_END_INCL_START_BOUNDARY,
+                                                                        b.getRawValues());
+                prev = new RangeTombstoneBoundaryMarker(boundary, prev.closeDeletionTime(false), curr.openDeletionTime(false));
                 currUnfiltered = prev;
                 --di;
             }
@@ -283,24 +294,24 @@
         }
     }
 
-    void verifyEquivalent(List<List<Unfiltered>> sources, List<Unfiltered> merged)
+    void verifyEquivalent(List<List<Unfiltered>> sources, List<Unfiltered> merged, UnfilteredRowsGenerator generator)
     {
         try
         {
             for (int i=0; i<RANGE; ++i)
             {
-                Clusterable c = clusteringFor(i);
+                Clusterable c = UnfilteredRowsGenerator.clusteringFor(i);
                 DeletionTime dt = DeletionTime.LIVE;
                 for (List<Unfiltered> source : sources)
                 {
                     dt = deletionFor(c, source, dt);
                 }
-                Assert.assertEquals("Deletion time mismatch for position " + str(c), dt, deletionFor(c, merged));
+                Assert.assertEquals("Deletion time mismatch for position " + i, dt, deletionFor(c, merged));
                 if (dt == DeletionTime.LIVE)
                 {
                     Optional<Unfiltered> sourceOpt = sources.stream().map(source -> rowFor(c, source)).filter(x -> x != null).findAny();
                     Unfiltered mergedRow = rowFor(c, merged);
-                    Assert.assertEquals("Content mismatch for position " + str(c), str(sourceOpt.orElse(null)), str(mergedRow));
+                    Assert.assertEquals("Content mismatch for position " + i, clustering(sourceOpt.orElse(null)), clustering(mergedRow));
                 }
             }
         }
@@ -308,13 +319,20 @@
         {
             System.out.println(e);
             for (List<Unfiltered> list : sources)
-                dumpList(list);
+                generator.dumpList(list);
             System.out.println("merged");
-            dumpList(merged);
+            generator.dumpList(merged);
             throw e;
         }
     }
 
+    String clustering(Clusterable curr)
+    {
+        if (curr == null)
+            return "null";
+        return Int32Type.instance.getString(curr.clustering().get(0));
+    }
+
     private Unfiltered rowFor(Clusterable pointer, List<Unfiltered> list)
     {
         int index = Collections.binarySearch(list, pointer, reversed ? comparator.reversed() : comparator);
@@ -357,20 +375,20 @@
         return def;
     }
 
-    private static Bound boundFor(int pos, boolean start, boolean inclusive)
+    private static ClusteringBound boundFor(int pos, boolean start, boolean inclusive)
     {
-        return Bound.create(Bound.boundKind(start, inclusive), new ByteBuffer[] {Int32Type.instance.decompose(pos)});
+        return ClusteringBound.create(ClusteringBound.boundKind(start, inclusive), new ByteBuffer[] {Int32Type.instance.decompose(pos)});
     }
 
     private static Clustering clusteringFor(int i)
     {
-        return new Clustering(Int32Type.instance.decompose(i));
+        return Clustering.make(Int32Type.instance.decompose(i));
     }
 
     static Row emptyRowAt(int pos, Function<Integer, Integer> timeGenerator)
     {
         final Clustering clustering = clusteringFor(pos);
-        final LivenessInfo live = LivenessInfo.create(metadata, timeGenerator.apply(pos), nowInSec);
+        final LivenessInfo live = LivenessInfo.create(timeGenerator.apply(pos), nowInSec);
         return BTreeRow.noCellLiveRow(clustering, live);
     }
 
@@ -422,21 +440,23 @@
 
     public void testForInput(String... inputs)
     {
+        reversed = false;
+        UnfilteredRowsGenerator generator = new UnfilteredRowsGenerator(comparator, false);
+
         List<List<Unfiltered>> sources = new ArrayList<>();
         for (String input : inputs)
         {
-            List<Unfiltered> source = parse(input);
-            attachBoundaries(source);
-            dumpList(source);
-            verifyValid(source);
+            List<Unfiltered> source = generator.parse(input, DEL_RANGE);
+            generator.dumpList(source);
+            generator.verifyValid(source);
             sources.add(source);
         }
 
         List<Unfiltered> merged = merge(sources, false);
         System.out.println("Merge to:");
-        dumpList(merged);
-        verifyEquivalent(sources, merged);
-        verifyValid(merged);
+        generator.dumpList(merged);
+        verifyEquivalent(sources, merged, generator);
+        generator.verifyValid(merged);
         System.out.println();
     }
 
@@ -485,8 +505,8 @@
 
     private RangeTombstoneMarker marker(int pos, int delTime, boolean isStart, boolean inclusive)
     {
-        return new RangeTombstoneBoundMarker(Bound.create(Bound.boundKind(isStart, inclusive),
-                                                          new ByteBuffer[] {clusteringFor(pos).get(0)}),
+        return new RangeTombstoneBoundMarker(ClusteringBound.create(ClusteringBound.boundKind(isStart, inclusive),
+                                                                    new ByteBuffer[] {clusteringFor(pos).get(0)}),
                                              new DeletionTime(delTime, delTime));
     }
 }
diff --git a/test/unit/org/apache/cassandra/db/rows/UnfilteredRowIteratorsTest.java b/test/unit/org/apache/cassandra/db/rows/UnfilteredRowIteratorsTest.java
index 0387ba2..cce8599 100644
--- a/test/unit/org/apache/cassandra/db/rows/UnfilteredRowIteratorsTest.java
+++ b/test/unit/org/apache/cassandra/db/rows/UnfilteredRowIteratorsTest.java
@@ -27,6 +27,7 @@
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.BufferDecoratedKey;
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.DeletionTime;
@@ -46,6 +47,9 @@
 
     static
     {
+        // Required because of metadata creation, assertion is thrown otherwise
+        DatabaseDescriptor.daemonInitialization();
+
         metadata = CFMetaData.Builder.create("", "")
                              .addPartitionKey("pk", Int32Type.instance)
                                      .addClusteringColumn("ck", Int32Type.instance)
diff --git a/test/unit/org/apache/cassandra/db/rows/UnfilteredRowsGenerator.java b/test/unit/org/apache/cassandra/db/rows/UnfilteredRowsGenerator.java
new file mode 100644
index 0000000..7cdccdb
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/rows/UnfilteredRowsGenerator.java
@@ -0,0 +1,340 @@
+/*
+ * 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.db.rows;
+
+import java.nio.ByteBuffer;
+import java.util.*;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.junit.Assert;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.rows.Unfiltered.Kind;
+import org.apache.cassandra.utils.btree.BTree;
+
+public class UnfilteredRowsGenerator
+{
+    final boolean reversed;
+    final Comparator<Clusterable> comparator;
+
+    public UnfilteredRowsGenerator(Comparator<Clusterable> comparator, boolean reversed)
+    {
+        this.reversed = reversed;
+        this.comparator = comparator;
+    }
+
+    String str(Clusterable curr)
+    {
+        if (curr == null)
+            return "null";
+        String val = Int32Type.instance.getString(curr.clustering().get(0));
+        if (curr instanceof RangeTombstoneMarker)
+        {
+            RangeTombstoneMarker marker = (RangeTombstoneMarker) curr;
+            if (marker.isClose(reversed))
+                val = "[" + marker.closeDeletionTime(reversed).markedForDeleteAt() + "]" + (marker.closeIsInclusive(reversed) ? "<=" : "<") + val;
+            if (marker.isOpen(reversed))
+                val = val + (marker.openIsInclusive(reversed) ? "<=" : "<") + "[" + marker.openDeletionTime(reversed).markedForDeleteAt() + "]";
+        }
+        else if (curr instanceof Row)
+        {
+            Row row = (Row) curr;
+            String delTime = "";
+            if (!row.deletion().time().isLive())
+                delTime = "D" + row.deletion().time().markedForDeleteAt();
+            val = val + "[" + row.primaryKeyLivenessInfo().timestamp() + delTime + "]";
+        }
+        return val;
+    }
+
+    public void verifyValid(List<Unfiltered> list)
+    {
+        verifyValid(list, reversed);
+    }
+
+    void verifyValid(List<Unfiltered> list, boolean reversed)
+    {
+        int reversedAsMultiplier = reversed ? -1 : 1;
+        try {
+            RangeTombstoneMarker prev = null;
+            Unfiltered prevUnfiltered = null;
+            for (Unfiltered unfiltered : list)
+            {
+                Assert.assertTrue("Order violation prev " + str(prevUnfiltered) + " curr " + str(unfiltered),
+                                  prevUnfiltered == null || comparator.compare(prevUnfiltered, unfiltered) * reversedAsMultiplier < 0);
+                prevUnfiltered = unfiltered;
+
+                if (unfiltered.kind() == Kind.RANGE_TOMBSTONE_MARKER)
+                {
+                    RangeTombstoneMarker curr = (RangeTombstoneMarker) unfiltered;
+                    if (prev != null)
+                    {
+                        if (curr.isClose(reversed))
+                        {
+                            Assert.assertTrue(str(unfiltered) + " follows another close marker " + str(prev), prev.isOpen(reversed));
+                            Assert.assertEquals("Deletion time mismatch for open " + str(prev) + " and close " + str(unfiltered),
+                                                prev.openDeletionTime(reversed),
+                                                curr.closeDeletionTime(reversed));
+                        }
+                        else
+                            Assert.assertFalse(str(curr) + " follows another open marker " + str(prev), prev.isOpen(reversed));
+                    }
+
+                    prev = curr;
+                }
+            }
+            Assert.assertFalse("Cannot end in open marker " + str(prev), prev != null && prev.isOpen(reversed));
+
+        } catch (AssertionError e) {
+            System.out.println(e);
+            dumpList(list);
+            throw e;
+        }
+    }
+
+    public List<Unfiltered> generateSource(Random r, int items, int range, int del_range, Function<Integer, Integer> timeGenerator)
+    {
+        int[] positions = new int[items + 1];
+        for (int i=0; i<items; ++i)
+            positions[i] = r.nextInt(range);
+        positions[items] = range;
+        Arrays.sort(positions);
+
+        List<Unfiltered> content = new ArrayList<>(items);
+        int prev = -1;
+        for (int i=0; i<items; ++i)
+        {
+            int pos = positions[i];
+            int sz = positions[i + 1] - pos;
+            if (sz == 0 && pos == prev)
+                // Filter out more than two of the same position.
+                continue;
+            if (r.nextBoolean() || pos == prev)
+            {
+                int span;
+                boolean includesStart;
+                boolean includesEnd;
+                if (pos > prev)
+                {
+                    span = r.nextInt(sz + 1);
+                    includesStart = span > 0 ? r.nextBoolean() : true;
+                    includesEnd = span > 0 ? r.nextBoolean() : true;
+                }
+                else
+                {
+                    span = 1 + r.nextInt(sz);
+                    includesStart = false;
+                    includesEnd = r.nextBoolean();
+                }
+                int deltime = r.nextInt(del_range);
+                DeletionTime dt = new DeletionTime(deltime, deltime);
+                content.add(new RangeTombstoneBoundMarker(boundFor(pos, true, includesStart), dt));
+                content.add(new RangeTombstoneBoundMarker(boundFor(pos + span, false, includesEnd), dt));
+                prev = pos + span - (includesEnd ? 0 : 1);
+            }
+            else
+            {
+                content.add(emptyRowAt(pos, timeGenerator));
+                prev = pos;
+            }
+        }
+
+        attachBoundaries(content);
+        if (reversed)
+        {
+            Collections.reverse(content);
+        }
+        verifyValid(content);
+        if (items <= 20)
+            dumpList(content);
+        return content;
+    }
+
+    /**
+     * Constructs a list of unfiltereds with integer clustering according to the specification string.
+     *
+     * The string is a space-delimited sorted list that can contain:
+     *  * open tombstone markers, e.g. xx<[yy] where xx is the clustering, yy is the deletion time, and "<" stands for
+     *    non-inclusive (<= for inclusive).
+     *  * close tombstone markers, e.g. [yy]<=xx. Adjacent close and open markers (e.g. [yy]<=xx xx<[zz]) are combined
+     *    into boundary markers.
+     *  * empty rows, e.g. xx or xx[yy] or xx[yyDzz] where xx is the clustering, yy is the live time and zz is deletion
+     *    time.
+     *
+     * @param input Specification.
+     * @param default_liveness Liveness to use for rows if not explicitly specified.
+     * @return Parsed list.
+     */
+    public List<Unfiltered> parse(String input, int default_liveness)
+    {
+        String[] split = input.split(" ");
+        Pattern open = Pattern.compile("(\\d+)<(=)?\\[(\\d+)\\]");
+        Pattern close = Pattern.compile("\\[(\\d+)\\]<(=)?(\\d+)");
+        Pattern row = Pattern.compile("(\\d+)(\\[(\\d+)(?:D(\\d+))?\\])?");
+        List<Unfiltered> out = new ArrayList<>(split.length);
+        for (String s : split)
+        {
+            Matcher m = open.matcher(s);
+            if (m.matches())
+            {
+                out.add(openMarker(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(3)), m.group(2) != null));
+                continue;
+            }
+            m = close.matcher(s);
+            if (m.matches())
+            {
+                out.add(closeMarker(Integer.parseInt(m.group(3)), Integer.parseInt(m.group(1)), m.group(2) != null));
+                continue;
+            }
+            m = row.matcher(s);
+            if (m.matches())
+            {
+                int live = m.group(3) != null ? Integer.parseInt(m.group(3)) : default_liveness;
+                int delTime = m.group(4) != null ? Integer.parseInt(m.group(4)) : -1;
+                out.add(emptyRowAt(Integer.parseInt(m.group(1)), live, delTime));
+                continue;
+            }
+            Assert.fail("Can't parse " + s);
+        }
+        attachBoundaries(out);
+        return out;
+    }
+
+    static Row emptyRowAt(int pos, Function<Integer, Integer> timeGenerator)
+    {
+        final Clustering clustering = clusteringFor(pos);
+        final LivenessInfo live = LivenessInfo.create(timeGenerator.apply(pos), UnfilteredRowIteratorsMergeTest.nowInSec);
+        return BTreeRow.noCellLiveRow(clustering, live);
+    }
+
+    static Row emptyRowAt(int pos, int time, int deletionTime)
+    {
+        final Clustering clustering = clusteringFor(pos);
+        final LivenessInfo live = LivenessInfo.create(time, UnfilteredRowIteratorsMergeTest.nowInSec);
+        final DeletionTime delTime = deletionTime == -1 ? DeletionTime.LIVE : new DeletionTime(deletionTime, deletionTime);
+        return BTreeRow.create(clustering, live, Row.Deletion.regular(delTime), BTree.empty());
+    }
+
+    static Clustering clusteringFor(int i)
+    {
+        return Clustering.make(Int32Type.instance.decompose(i));
+    }
+
+    static ClusteringBound boundFor(int pos, boolean start, boolean inclusive)
+    {
+        return ClusteringBound.create(ClusteringBound.boundKind(start, inclusive), new ByteBuffer[] {Int32Type.instance.decompose(pos)});
+    }
+
+    static void attachBoundaries(List<Unfiltered> content)
+    {
+        int di = 0;
+        RangeTombstoneMarker prev = null;
+        for (int si = 0; si < content.size(); ++si)
+        {
+            Unfiltered currUnfiltered = content.get(si);
+            RangeTombstoneMarker curr = currUnfiltered.kind() == Kind.RANGE_TOMBSTONE_MARKER ?
+                                        (RangeTombstoneMarker) currUnfiltered :
+                                        null;
+            if (prev != null && curr != null && prev.isClose(false) && curr.isOpen(false) && prev.clustering().invert().equals(curr.clustering()))
+            {
+                // Join. Prefer not to use merger to check its correctness.
+                ClusteringBound b = (ClusteringBound) prev.clustering();
+                ClusteringBoundary boundary = ClusteringBoundary.create(
+                        b.isInclusive() ? ClusteringBound.Kind.INCL_END_EXCL_START_BOUNDARY : ClusteringBound.Kind.EXCL_END_INCL_START_BOUNDARY,
+                        b.getRawValues());
+                prev = new RangeTombstoneBoundaryMarker(boundary, prev.closeDeletionTime(false), curr.openDeletionTime(false));
+                currUnfiltered = prev;
+                --di;
+            }
+            content.set(di++, currUnfiltered);
+            prev = curr;
+        }
+        for (int pos = content.size() - 1; pos >= di; --pos)
+            content.remove(pos);
+    }
+
+    static RangeTombstoneMarker openMarker(int pos, int delTime, boolean inclusive)
+    {
+        return marker(pos, delTime, true, inclusive);
+    }
+
+    static RangeTombstoneMarker closeMarker(int pos, int delTime, boolean inclusive)
+    {
+        return marker(pos, delTime, false, inclusive);
+    }
+
+    private static RangeTombstoneMarker marker(int pos, int delTime, boolean isStart, boolean inclusive)
+    {
+        return new RangeTombstoneBoundMarker(ClusteringBound.create(ClusteringBound.boundKind(isStart, inclusive),
+                                                                    new ByteBuffer[] {clusteringFor(pos).get(0)}),
+                                             new DeletionTime(delTime, delTime));
+    }
+
+    public static UnfilteredRowIterator source(Iterable<Unfiltered> content, CFMetaData metadata, DecoratedKey partitionKey)
+    {
+        return source(content, metadata, partitionKey, DeletionTime.LIVE);
+    }
+
+    public static UnfilteredRowIterator source(Iterable<Unfiltered> content, CFMetaData metadata, DecoratedKey partitionKey, DeletionTime delTime)
+    {
+        return new Source(content.iterator(), metadata, partitionKey, delTime, false);
+    }
+
+    static class Source extends AbstractUnfilteredRowIterator implements UnfilteredRowIterator
+    {
+        Iterator<Unfiltered> content;
+
+        protected Source(Iterator<Unfiltered> content, CFMetaData metadata, DecoratedKey partitionKey, DeletionTime partitionLevelDeletion, boolean reversed)
+        {
+            super(metadata,
+                  partitionKey,
+                  partitionLevelDeletion,
+                  metadata.partitionColumns(),
+                  Rows.EMPTY_STATIC_ROW,
+                  reversed,
+                  EncodingStats.NO_STATS);
+            this.content = content;
+        }
+
+        @Override
+        protected Unfiltered computeNext()
+        {
+            return content.hasNext() ? content.next() : endOfData();
+        }
+    }
+
+    public String str(List<Unfiltered> list)
+    {
+        StringBuilder builder = new StringBuilder();
+        for (Unfiltered u : list)
+        {
+            builder.append(str(u));
+            builder.append(' ');
+        }
+        return builder.toString();
+    }
+
+    public void dumpList(List<Unfiltered> list)
+    {
+        System.out.println(str(list));
+    }
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/db/transform/DuplicateRowCheckerTest.java b/test/unit/org/apache/cassandra/db/transform/DuplicateRowCheckerTest.java
index 78a0c8c..432bce3 100644
--- a/test/unit/org/apache/cassandra/db/transform/DuplicateRowCheckerTest.java
+++ b/test/unit/org/apache/cassandra/db/transform/DuplicateRowCheckerTest.java
@@ -206,7 +206,28 @@
         for (int i = 0; i < clusteringValues.length; i++)
             clusteringByteBuffers[i] = decompose(metadata.clusteringColumns().get(i).type, clusteringValues[i]);
 
-        return BTreeRow.noCellLiveRow(new Clustering(clusteringByteBuffers), LivenessInfo.create(metadata, 0, 0));
+        return BTreeRow.noCellLiveRow(Clustering.make(clusteringByteBuffers), LivenessInfo.create(0, 0));
+    }
+
+    public static UnfilteredRowIterator rows(CFMetaData metadata,
+                                             DecoratedKey key,
+                                             boolean isReversedOrder,
+                                             Unfiltered... unfiltereds)
+    {
+        Iterator<Unfiltered> iterator = Iterators.forArray(unfiltereds);
+        return new AbstractUnfilteredRowIterator(metadata,
+                                                 key,
+                                                 DeletionTime.LIVE,
+                                                 metadata.partitionColumns(),
+                                                 Rows.EMPTY_STATIC_ROW,
+                                                 isReversedOrder,
+                                                 EncodingStats.NO_STATS)
+        {
+            protected Unfiltered computeNext()
+            {
+                return iterator.hasNext() ? iterator.next() : endOfData();
+            }
+        };
     }
 
     private static PartitionIterator applyChecker(UnfilteredPartitionIterator unfiltered)
@@ -219,22 +240,7 @@
     public static UnfilteredPartitionIterator iter(CFMetaData metadata, boolean isReversedOrder, Unfiltered... unfiltereds)
     {
         DecoratedKey key = metadata.partitioner.decorateKey(bytes("key"));
-        Iterator<Unfiltered> iterator = Iterators.forArray(unfiltereds);
-
-        UnfilteredRowIterator rowIter = new AbstractUnfilteredRowIterator(metadata,
-                                                                          key,
-                                                                          DeletionTime.LIVE,
-                                                                          metadata.partitionColumns(),
-                                                                          Rows.EMPTY_STATIC_ROW,
-                                                                          isReversedOrder,
-                                                                          EncodingStats.NO_STATS)
-        {
-            protected Unfiltered computeNext()
-            {
-                return iterator.hasNext() ? iterator.next() : endOfData();
-            }
-        };
-
+        UnfilteredRowIterator rowIter = rows(metadata, key, isReversedOrder, unfiltereds);
         return new SingletonUnfilteredPartitionIterator(rowIter, false);
     }
 }
diff --git a/test/unit/org/apache/cassandra/db/transform/RTTransformationsTest.java b/test/unit/org/apache/cassandra/db/transform/RTTransformationsTest.java
index f79b9f3..b2ec2b2 100644
--- a/test/unit/org/apache/cassandra/db/transform/RTTransformationsTest.java
+++ b/test/unit/org/apache/cassandra/db/transform/RTTransformationsTest.java
@@ -65,6 +65,7 @@
                       .addClusteringColumn("ck0", UTF8Type.instance)
                       .addClusteringColumn("ck1", UTF8Type.instance)
                       .addClusteringColumn("ck2", UTF8Type.instance)
+                      .withPartitioner(Murmur3Partitioner.instance)
                       .build();
         key = Murmur3Partitioner.instance.decorateKey(bytes("key"));
     }
@@ -375,7 +376,7 @@
         for (int i = 0; i < clusteringValues.length; i++)
             clusteringByteBuffers[i] = decompose(metadata.clusteringColumns().get(i).type, clusteringValues[i]);
 
-        return new RangeTombstoneBoundMarker(new RangeTombstone.Bound(kind, clusteringByteBuffers), new DeletionTime(timestamp, nowInSec));
+        return new RangeTombstoneBoundMarker(ClusteringBound.create(kind, clusteringByteBuffers), new DeletionTime(timestamp, nowInSec));
     }
 
     private RangeTombstoneBoundaryMarker boundary(ClusteringPrefix.Kind kind, long closeTimestamp, long openTimestamp, Object... clusteringValues)
@@ -384,7 +385,7 @@
         for (int i = 0; i < clusteringValues.length; i++)
             clusteringByteBuffers[i] = decompose(metadata.clusteringColumns().get(i).type, clusteringValues[i]);
 
-        return new RangeTombstoneBoundaryMarker(new RangeTombstone.Bound(kind, clusteringByteBuffers),
+        return new RangeTombstoneBoundaryMarker(ClusteringBoundary.create(kind, clusteringByteBuffers),
                                                 new DeletionTime(closeTimestamp, nowInSec),
                                                 new DeletionTime(openTimestamp, nowInSec));
     }
@@ -395,7 +396,7 @@
         for (int i = 0; i < clusteringValues.length; i++)
             clusteringByteBuffers[i] = decompose(metadata.clusteringColumns().get(i).type, clusteringValues[i]);
 
-        return BTreeRow.noCellLiveRow(new Clustering(clusteringByteBuffers), LivenessInfo.create(metadata, timestamp, nowInSec));
+        return BTreeRow.noCellLiveRow(Clustering.make(clusteringByteBuffers), LivenessInfo.create(timestamp, nowInSec));
     }
 
     @SuppressWarnings("unchecked")
diff --git a/test/unit/org/apache/cassandra/db/view/ViewUtilsTest.java b/test/unit/org/apache/cassandra/db/view/ViewUtilsTest.java
index c238f36..89bb44a 100644
--- a/test/unit/org/apache/cassandra/db/view/ViewUtilsTest.java
+++ b/test/unit/org/apache/cassandra/db/view/ViewUtilsTest.java
@@ -46,6 +46,7 @@
     @BeforeClass
     public static void setUp() throws ConfigurationException
     {
+        DatabaseDescriptor.daemonInitialization();
         IEndpointSnitch snitch = new PropertyFileSnitch();
         DatabaseDescriptor.setEndpointSnitch(snitch);
         Keyspace.setInitialized();
diff --git a/test/unit/org/apache/cassandra/dht/BootStrapperTest.java b/test/unit/org/apache/cassandra/dht/BootStrapperTest.java
index 3fbe106..ed15a70 100644
--- a/test/unit/org/apache/cassandra/dht/BootStrapperTest.java
+++ b/test/unit/org/apache/cassandra/dht/BootStrapperTest.java
@@ -61,6 +61,7 @@
     @BeforeClass
     public static void setup() throws ConfigurationException
     {
+        DatabaseDescriptor.daemonInitialization();
         oldPartitioner = StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance);
         SchemaLoader.startGossiper();
         SchemaLoader.prepareServer();
@@ -96,7 +97,7 @@
         InetAddress myEndpoint = InetAddress.getByName("127.0.0.1");
 
         assertEquals(numOldNodes, tmd.sortedTokens().size());
-        RangeStreamer s = new RangeStreamer(tmd, null, myEndpoint, "Bootstrap", true, DatabaseDescriptor.getEndpointSnitch(), new StreamStateStore());
+        RangeStreamer s = new RangeStreamer(tmd, null, myEndpoint, "Bootstrap", true, DatabaseDescriptor.getEndpointSnitch(), new StreamStateStore(), false);
         IFailureDetector mockFailureDetector = new IFailureDetector()
         {
             public boolean isAlive(InetAddress ep)
@@ -223,7 +224,7 @@
     private void allocateTokensForNode(int vn, String ks, TokenMetadata tm, InetAddress addr)
     {
         SummaryStatistics os = TokenAllocation.replicatedOwnershipStats(tm.cloneOnlyTokenMap(), Keyspace.open(ks).getReplicationStrategy(), addr);
-        Collection<Token> tokens = BootStrapper.allocateTokens(tm, addr, ks, vn);
+        Collection<Token> tokens = BootStrapper.allocateTokens(tm, addr, ks, vn, 0);
         assertEquals(vn, tokens.size());
         tm.updateNormalTokens(tokens, addr);
         SummaryStatistics ns = TokenAllocation.replicatedOwnershipStats(tm.cloneOnlyTokenMap(), Keyspace.open(ks).getReplicationStrategy(), addr);
diff --git a/test/unit/org/apache/cassandra/dht/ByteOrderedPartitionerTest.java b/test/unit/org/apache/cassandra/dht/ByteOrderedPartitionerTest.java
index c4896a3..f40c284 100644
--- a/test/unit/org/apache/cassandra/dht/ByteOrderedPartitionerTest.java
+++ b/test/unit/org/apache/cassandra/dht/ByteOrderedPartitionerTest.java
@@ -23,4 +23,9 @@
     {
         partitioner = ByteOrderedPartitioner.instance;
     }
+
+    protected boolean shouldStopRecursion(Token left, Token right)
+    {
+        return false;
+    }
 }
diff --git a/test/unit/org/apache/cassandra/dht/KeyCollisionTest.java b/test/unit/org/apache/cassandra/dht/KeyCollisionTest.java
index ade6ec1..c0ff9ca 100644
--- a/test/unit/org/apache/cassandra/dht/KeyCollisionTest.java
+++ b/test/unit/org/apache/cassandra/dht/KeyCollisionTest.java
@@ -54,6 +54,7 @@
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
+        DatabaseDescriptor.daemonInitialization();
         oldPartitioner = StorageService.instance.setPartitionerUnsafe(LengthPartitioner.instance);
         SchemaLoader.prepareServer();
         SchemaLoader.createKeyspace(KEYSPACE1,
diff --git a/test/unit/org/apache/cassandra/dht/LengthPartitioner.java b/test/unit/org/apache/cassandra/dht/LengthPartitioner.java
index 9cefbf2..97f2dcc 100644
--- a/test/unit/org/apache/cassandra/dht/LengthPartitioner.java
+++ b/test/unit/org/apache/cassandra/dht/LengthPartitioner.java
@@ -20,6 +20,7 @@
 import java.math.BigInteger;
 import java.nio.ByteBuffer;
 import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
 
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.Schema;
@@ -56,14 +57,30 @@
         return new BigIntegerToken(midpair.left);
     }
 
+    public Token split(Token left, Token right, double ratioToLeft)
+    {
+        throw new UnsupportedOperationException();
+    }
+
     public BigIntegerToken getMinimumToken()
     {
         return MINIMUM;
     }
 
+    @Override
+    public Token getMaximumToken()
+    {
+        return null;
+    }
+
     public BigIntegerToken getRandomToken()
     {
-        return new BigIntegerToken(BigInteger.valueOf(new Random().nextInt(15)));
+        return getRandomToken(ThreadLocalRandom.current());
+    }
+
+    public BigIntegerToken getRandomToken(Random random)
+    {
+        return new BigIntegerToken(BigInteger.valueOf(random.nextInt(15)));
     }
 
     private final Token.TokenFactory tokenFactory = new Token.TokenFactory() {
diff --git a/test/unit/org/apache/cassandra/dht/Murmur3PartitionerTest.java b/test/unit/org/apache/cassandra/dht/Murmur3PartitionerTest.java
index 19aba40..83f3dda 100644
--- a/test/unit/org/apache/cassandra/dht/Murmur3PartitionerTest.java
+++ b/test/unit/org/apache/cassandra/dht/Murmur3PartitionerTest.java
@@ -17,6 +17,8 @@
  */
 package org.apache.cassandra.dht;
 
+import org.junit.Test;
+
 public class Murmur3PartitionerTest extends PartitionerTestCase
 {
     public void initPartitioner()
@@ -34,5 +36,31 @@
         assertMidpoint(mintoken, mintoken, 62);
         assertMidpoint(tok("a"), mintoken, 16);
     }
+
+    protected boolean shouldStopRecursion(Token left, Token right)
+    {
+        return left.size(right) < Math.scalb(1, -48);
+    }
+
+    @Test
+    public void testSplit()
+    {
+        assertSplit(tok("a"), tok("b"), 16);
+        assertSplit(tok("a"), tok("bbb"), 16);
+    }
+
+    @Test
+    public void testSplitWrapping()
+    {
+        assertSplit(tok("b"), tok("a"), 16);
+        assertSplit(tok("bbb"), tok("a"), 16);
+    }
+
+    @Test
+    public void testSplitExceedMaximumCase()
+    {
+        Murmur3Partitioner.LongToken left = new Murmur3Partitioner.LongToken(Long.MAX_VALUE - 100);
+        assertSplit(left, tok("a"), 16);
+    }
 }
 
diff --git a/test/unit/org/apache/cassandra/dht/OrderPreservingPartitionerTest.java b/test/unit/org/apache/cassandra/dht/OrderPreservingPartitionerTest.java
index 57f33e7..6ab5b45 100644
--- a/test/unit/org/apache/cassandra/dht/OrderPreservingPartitionerTest.java
+++ b/test/unit/org/apache/cassandra/dht/OrderPreservingPartitionerTest.java
@@ -39,6 +39,11 @@
         partitioner = OrderPreservingPartitioner.instance;
     }
 
+    protected boolean shouldStopRecursion(Token left, Token right)
+    {
+        return false;
+    }
+
     @Test
     public void testCompare()
     {
diff --git a/test/unit/org/apache/cassandra/dht/PartitionerTestCase.java b/test/unit/org/apache/cassandra/dht/PartitionerTestCase.java
index cb892a7..33e9d60 100644
--- a/test/unit/org/apache/cassandra/dht/PartitionerTestCase.java
+++ b/test/unit/org/apache/cassandra/dht/PartitionerTestCase.java
@@ -25,8 +25,10 @@
 import java.util.Random;
 
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.service.StorageService;
 
 import static org.junit.Assert.assertEquals;
@@ -34,10 +36,19 @@
 
 public abstract class PartitionerTestCase
 {
+    private static final double SPLIT_RATIO_MIN = 0.10;
+    private static final double SPLIT_RATIO_MAX = 1 - SPLIT_RATIO_MIN;
+
     protected IPartitioner partitioner;
 
     public abstract void initPartitioner();
 
+    @BeforeClass
+    public static void initDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Before
     public void clean()
     {
@@ -110,6 +121,46 @@
         assertMidpoint(tok("bbb"), tok("a"), 16);
     }
 
+    /**
+     * Test split token ranges
+     */
+    public void assertSplit(Token left, Token right, int depth)
+    {
+        Random rand = new Random();
+        for (int i = 0; i < 1000; i++)
+        {
+            assertSplit(left, right ,rand, depth);
+        }
+    }
+
+    protected abstract boolean shouldStopRecursion(Token left, Token right);
+
+    private void assertSplit(Token left, Token right, Random rand, int depth)
+    {
+        if (shouldStopRecursion(left, right))
+        {
+            System.out.println("Stop assertSplit at depth: " + depth);
+            return;
+        }
+
+        double ratio = SPLIT_RATIO_MIN + (SPLIT_RATIO_MAX - SPLIT_RATIO_MIN) * rand.nextDouble();
+        Token newToken = partitioner.split(left, right, ratio);
+
+        assertEquals("For " + left + "," + right + ", new token: " + newToken,
+                     ratio, left.size(newToken) / left.size(right), 0.1);
+
+        assert new Range<Token>(left, right).contains(newToken)
+            : "For " + left + "," + right + ": range did not contain new token:" + newToken;
+
+        if (depth < 1)
+            return;
+
+        if (rand.nextBoolean())
+            assertSplit(left, newToken, rand, depth-1);
+        else
+            assertSplit(newToken, right, rand, depth-1);
+    }
+
     @Test
     public void testTokenFactoryBytes()
     {
diff --git a/test/unit/org/apache/cassandra/dht/RandomPartitionerTest.java b/test/unit/org/apache/cassandra/dht/RandomPartitionerTest.java
index 5e68644..273fe21 100644
--- a/test/unit/org/apache/cassandra/dht/RandomPartitionerTest.java
+++ b/test/unit/org/apache/cassandra/dht/RandomPartitionerTest.java
@@ -18,10 +18,41 @@
 
 package org.apache.cassandra.dht;
 
+import java.math.BigInteger;
+
+import org.junit.Test;
+
 public class RandomPartitionerTest extends PartitionerTestCase
 {
     public void initPartitioner()
     {
         partitioner = RandomPartitioner.instance;
     }
+
+    protected boolean shouldStopRecursion(Token left, Token right)
+    {
+        return left.size(right) < Math.scalb(1, -112);
+    }
+
+    @Test
+    public void testSplit()
+    {
+        assertSplit(tok("a"), tok("b"), 16);
+        assertSplit(tok("a"), tok("bbb"), 16);
+    }
+
+    @Test
+    public void testSplitWrapping()
+    {
+        assertSplit(tok("b"), tok("a"), 16);
+        assertSplit(tok("bbb"), tok("a"), 16);
+    }
+
+    @Test
+    public void testSplitExceedMaximumCase()
+    {
+        RandomPartitioner.BigIntegerToken left = new RandomPartitioner.BigIntegerToken(RandomPartitioner.MAXIMUM.subtract(BigInteger.valueOf(10)));
+
+        assertSplit(left, tok("a"), 16);
+    }
 }
diff --git a/test/unit/org/apache/cassandra/dht/RangeTest.java b/test/unit/org/apache/cassandra/dht/RangeTest.java
index 627253d..3657ced 100644
--- a/test/unit/org/apache/cassandra/dht/RangeTest.java
+++ b/test/unit/org/apache/cassandra/dht/RangeTest.java
@@ -28,9 +28,12 @@
 
 import com.google.common.base.Joiner;
 import org.apache.commons.lang3.StringUtils;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.commons.collections.CollectionUtils;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.PartitionPosition;
 import org.apache.cassandra.dht.ByteOrderedPartitioner.BytesToken;
 import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken;
@@ -42,6 +45,12 @@
 
 public class RangeTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testContains()
     {
diff --git a/test/unit/org/apache/cassandra/dht/SplitterTest.java b/test/unit/org/apache/cassandra/dht/SplitterTest.java
new file mode 100644
index 0000000..63e642f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/dht/SplitterTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.dht;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+import org.junit.Test;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.assertj.core.api.Assertions;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class SplitterTest
+{
+    private static final Logger logger = LoggerFactory.getLogger(SplitterTest.class);
+
+    @Test
+    public void randomSplitTestNoVNodesRandomPartitioner()
+    {
+        randomSplitTestNoVNodes(new RandomPartitioner());
+    }
+
+    @Test
+    public void randomSplitTestNoVNodesMurmur3Partitioner()
+    {
+        randomSplitTestNoVNodes(new Murmur3Partitioner());
+    }
+
+    @Test
+    public void randomSplitTestVNodesRandomPartitioner()
+    {
+        randomSplitTestVNodes(new RandomPartitioner());
+    }
+    @Test
+    public void randomSplitTestVNodesMurmur3Partitioner()
+    {
+        randomSplitTestVNodes(new Murmur3Partitioner());
+    }
+
+    // CASSANDRA-18013
+    @Test
+    public void testSplitOwnedRanges() {
+        Splitter splitter = Murmur3Partitioner.instance.splitter().get();
+        long lt = 0;
+        long rt = 31;
+        Range<Token> range = new Range<>(splitter.tokenForValue(BigInteger.valueOf(lt)),
+                                         splitter.tokenForValue(BigInteger.valueOf(rt)));
+
+        for (int i = 1; i <= (rt - lt); i++)
+        {
+            List<Token> splits = splitter.splitOwnedRanges(i, Arrays.asList(range), false);
+            logger.info("{} splits of {} are: {}", i, range, splits);
+            Assertions.assertThat(splits).hasSize(i);
+        }
+    }
+
+    public void randomSplitTestNoVNodes(IPartitioner partitioner)
+    {
+        Splitter splitter = partitioner.splitter().get();
+        Random r = new Random();
+        for (int i = 0; i < 10000; i++)
+        {
+            List<Range<Token>> localRanges = generateLocalRanges(1, r.nextInt(4)+1, splitter, r, partitioner instanceof RandomPartitioner);
+            List<Token> boundaries = splitter.splitOwnedRanges(r.nextInt(9) + 1, localRanges, false);
+            assertTrue("boundaries = "+boundaries+" ranges = "+localRanges, assertRangeSizeEqual(localRanges, boundaries, partitioner, splitter, true));
+        }
+    }
+
+    public void randomSplitTestVNodes(IPartitioner partitioner)
+    {
+        Splitter splitter = partitioner.splitter().get();
+        Random r = new Random();
+        for (int i = 0; i < 10000; i++)
+        {
+            // we need many tokens to be able to split evenly over the disks
+            int numTokens = 172 + r.nextInt(128);
+            int rf = r.nextInt(4) + 2;
+            int parts = r.nextInt(5)+1;
+            List<Range<Token>> localRanges = generateLocalRanges(numTokens, rf, splitter, r, partitioner instanceof RandomPartitioner);
+            List<Token> boundaries = splitter.splitOwnedRanges(parts, localRanges, true);
+            if (!assertRangeSizeEqual(localRanges, boundaries, partitioner, splitter, false))
+                fail(String.format("Could not split %d tokens with rf=%d into %d parts (localRanges=%s, boundaries=%s)", numTokens, rf, parts, localRanges, boundaries));
+        }
+    }
+
+    private boolean assertRangeSizeEqual(List<Range<Token>> localRanges, List<Token> tokens, IPartitioner partitioner, Splitter splitter, boolean splitIndividualRanges)
+    {
+        Token start = partitioner.getMinimumToken();
+        List<BigInteger> splits = new ArrayList<>();
+
+        for (int i = 0; i < tokens.size(); i++)
+        {
+            Token end = i == tokens.size() - 1 ? partitioner.getMaximumToken() : tokens.get(i);
+            splits.add(sumOwnedBetween(localRanges, start, end, splitter, splitIndividualRanges));
+            start = end;
+        }
+        // when we dont need to keep around full ranges, the difference is small between the partitions
+        BigDecimal delta = splitIndividualRanges ? BigDecimal.valueOf(0.001) : BigDecimal.valueOf(0.25);
+        boolean allBalanced = true;
+        for (BigInteger b : splits)
+        {
+            for (BigInteger i : splits)
+            {
+                BigDecimal bdb = new BigDecimal(b);
+                BigDecimal bdi = new BigDecimal(i);
+                BigDecimal q = bdb.divide(bdi, 2, BigDecimal.ROUND_HALF_DOWN);
+                if (q.compareTo(BigDecimal.ONE.add(delta)) > 0 || q.compareTo(BigDecimal.ONE.subtract(delta)) < 0)
+                    allBalanced = false;
+            }
+        }
+        return allBalanced;
+    }
+
+    private BigInteger sumOwnedBetween(List<Range<Token>> localRanges, Token start, Token end, Splitter splitter, boolean splitIndividualRanges)
+    {
+        BigInteger sum = BigInteger.ZERO;
+        for (Range<Token> range : localRanges)
+        {
+            if (splitIndividualRanges)
+            {
+                Set<Range<Token>> intersections = new Range<>(start, end).intersectionWith(range);
+                for (Range<Token> intersection : intersections)
+                    sum = sum.add(splitter.valueForToken(intersection.right).subtract(splitter.valueForToken(intersection.left)));
+            }
+            else
+            {
+                if (new Range<>(start, end).contains(range.left))
+                    sum = sum.add(splitter.valueForToken(range.right).subtract(splitter.valueForToken(range.left)));
+            }
+        }
+        return sum;
+    }
+
+    private List<Range<Token>> generateLocalRanges(int numTokens, int rf, Splitter splitter, Random r, boolean randomPartitioner)
+    {
+        int localTokens = numTokens * rf;
+        List<Token> randomTokens = new ArrayList<>();
+
+        for (int i = 0; i < localTokens * 2; i++)
+        {
+            Token t = splitter.tokenForValue(randomPartitioner ? new BigInteger(127, r) : BigInteger.valueOf(r.nextLong()));
+            randomTokens.add(t);
+        }
+
+        Collections.sort(randomTokens);
+
+        List<Range<Token>> localRanges = new ArrayList<>(localTokens);
+        for (int i = 0; i < randomTokens.size() - 1; i++)
+        {
+            assert randomTokens.get(i).compareTo(randomTokens.get(i+1)) < 0;
+            localRanges.add(new Range<>(randomTokens.get(i), randomTokens.get(i+1)));
+            i++;
+        }
+        return localRanges;
+    }
+}
diff --git a/test/unit/org/apache/cassandra/dht/StreamStateStoreTest.java b/test/unit/org/apache/cassandra/dht/StreamStateStoreTest.java
index bdb654a..fd9b03e 100644
--- a/test/unit/org/apache/cassandra/dht/StreamStateStoreTest.java
+++ b/test/unit/org/apache/cassandra/dht/StreamStateStoreTest.java
@@ -34,10 +34,11 @@
 
 public class StreamStateStoreTest
 {
+
     @BeforeClass
     public static void initDD()
     {
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
     }
 
     @Test
@@ -80,4 +81,4 @@
         // as well as the old one
         assertTrue(store.isDataAvailable("keyspace1", factory.fromString("50")));
     }
-}
\ No newline at end of file
+}
diff --git a/test/unit/org/apache/cassandra/gms/ArrivalWindowTest.java b/test/unit/org/apache/cassandra/gms/ArrivalWindowTest.java
index 459956b..a539ca8 100644
--- a/test/unit/org/apache/cassandra/gms/ArrivalWindowTest.java
+++ b/test/unit/org/apache/cassandra/gms/ArrivalWindowTest.java
@@ -27,10 +27,18 @@
 
 import java.net.InetAddress;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.utils.FBUtilities;
+import org.junit.BeforeClass;
 
 public class ArrivalWindowTest
 {
+    @BeforeClass
+    public static void beforeClass()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testWithNanoTime()
     {
diff --git a/test/unit/org/apache/cassandra/gms/EndpointStateTest.java b/test/unit/org/apache/cassandra/gms/EndpointStateTest.java
index b06c435..2453fe8 100644
--- a/test/unit/org/apache/cassandra/gms/EndpointStateTest.java
+++ b/test/unit/org/apache/cassandra/gms/EndpointStateTest.java
@@ -26,6 +26,7 @@
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
@@ -39,6 +40,12 @@
     public volatile VersionedValue.VersionedValueFactory valueFactory =
         new VersionedValue.VersionedValueFactory(DatabaseDescriptor.getPartitioner());
 
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testMultiThreadedReadConsistency() throws InterruptedException
     {
diff --git a/test/unit/org/apache/cassandra/gms/ExpireEndpointTest.java b/test/unit/org/apache/cassandra/gms/ExpireEndpointTest.java
index e7a1a64..f6d9fa3 100644
--- a/test/unit/org/apache/cassandra/gms/ExpireEndpointTest.java
+++ b/test/unit/org/apache/cassandra/gms/ExpireEndpointTest.java
@@ -22,8 +22,10 @@
 import java.net.UnknownHostException;
 import java.util.UUID;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.service.StorageService;
 
 import static org.junit.Assert.assertFalse;
@@ -32,6 +34,12 @@
 
 public class ExpireEndpointTest
 {
+    @BeforeClass
+    public static void setup()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testExpireEndpoint() throws UnknownHostException
     {
diff --git a/test/unit/org/apache/cassandra/gms/FailureDetectorTest.java b/test/unit/org/apache/cassandra/gms/FailureDetectorTest.java
index 83c3500..0dff95a 100644
--- a/test/unit/org/apache/cassandra/gms/FailureDetectorTest.java
+++ b/test/unit/org/apache/cassandra/gms/FailureDetectorTest.java
@@ -45,7 +45,7 @@
     {
         // slow unit tests can cause problems with FailureDetector's GC pause handling
         System.setProperty("cassandra.max_local_pause_in_ms", "20000");
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
         DatabaseDescriptor.createAllDirectories();
     }
 
diff --git a/test/unit/org/apache/cassandra/gms/GossipDigestTest.java b/test/unit/org/apache/cassandra/gms/GossipDigestTest.java
index 3191b03..36e3b27 100644
--- a/test/unit/org/apache/cassandra/gms/GossipDigestTest.java
+++ b/test/unit/org/apache/cassandra/gms/GossipDigestTest.java
@@ -28,11 +28,20 @@
 
 import java.net.InetAddress;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.net.MessagingService;
+
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 public class GossipDigestTest
 {
+    @BeforeClass
+    public static void beforeClass()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void test() throws IOException
     {
diff --git a/test/unit/org/apache/cassandra/gms/GossiperTest.java b/test/unit/org/apache/cassandra/gms/GossiperTest.java
index 42e4483..b6b3ffb 100644
--- a/test/unit/org/apache/cassandra/gms/GossiperTest.java
+++ b/test/unit/org/apache/cassandra/gms/GossiperTest.java
@@ -27,25 +27,38 @@
 
 import com.google.common.collect.ImmutableMap;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.RandomPartitioner;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.locator.TokenMetadata;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.schema.KeyspaceParams;
 import org.apache.cassandra.service.StorageService;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 public class GossiperTest
 {
-    static
+    @BeforeClass
+    public static void before()
     {
         System.setProperty(Gossiper.Props.DISABLE_THREAD_VALIDATION, "true");
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
+        SchemaLoader.prepareServer();
+        SchemaLoader.createKeyspace("schema_test_ks",
+                                    KeyspaceParams.simple(1),
+                                    SchemaLoader.standardCFMD("schema_test_ks", "schema_test_cf"));
     }
+
     static final IPartitioner partitioner = new RandomPartitioner();
     StorageService ss = StorageService.instance;
     TokenMetadata tmd = StorageService.instance.getTokenMetadata();
@@ -58,10 +71,10 @@
     public void setup()
     {
         tmd.clearUnsafe();
-    };
+    }
 
     @Test
-    public void testLargeGenerationJump() throws UnknownHostException, InterruptedException
+    public void testLargeGenerationJump() throws UnknownHostException
     {
         Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, 2);
         try
@@ -178,4 +191,54 @@
             Gossiper.instance.endpointStateMap.clear();
         }
     }
+
+    @Test
+    public void testSchemaVersionUpdate() throws UnknownHostException, InterruptedException
+    {
+        Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, 2);
+        MessagingService.instance().listen();
+        Gossiper.instance.start(1);
+        InetAddress remoteHostAddress = hosts.get(1);
+
+        EndpointState initialRemoteState = Gossiper.instance.getEndpointStateForEndpoint(remoteHostAddress);
+        // Set to any 3.0 version
+        Gossiper.instance.injectApplicationState(remoteHostAddress, ApplicationState.RELEASE_VERSION, StorageService.instance.valueFactory.releaseVersion("3.0.14"));
+
+        Gossiper.instance.applyStateLocally(ImmutableMap.of(remoteHostAddress, initialRemoteState));
+
+        // wait until the schema is set
+        VersionedValue schema = null;
+        for (int i = 0; i < 10; i++)
+        {
+            EndpointState localState = Gossiper.instance.getEndpointStateForEndpoint(hosts.get(0));
+            schema = localState.getApplicationState(ApplicationState.SCHEMA);
+            if (schema != null)
+                break;
+            Thread.sleep(1000);
+        }
+
+        // schema is set and equals to "alternative" version
+        assertTrue(schema != null);
+        assertEquals(schema.value, Schema.instance.getAltVersion().toString());
+
+        // Upgrade remote host version to the latest one (3.11)
+        Gossiper.instance.injectApplicationState(remoteHostAddress, ApplicationState.RELEASE_VERSION, StorageService.instance.valueFactory.releaseVersion());
+
+        Gossiper.instance.applyStateLocally(ImmutableMap.of(remoteHostAddress, initialRemoteState));
+
+        // wait until the schema change
+        VersionedValue newSchema = null;
+        for (int i = 0; i < 10; i++)
+        {
+            EndpointState localState = Gossiper.instance.getEndpointStateForEndpoint(hosts.get(0));
+            newSchema = localState.getApplicationState(ApplicationState.SCHEMA);
+            if (!schema.value.equals(newSchema.value))
+                break;
+            Thread.sleep(1000);
+        }
+
+        // schema is changed and equals to real version
+        assertFalse(schema.value.equals(newSchema.value));
+        assertEquals(newSchema.value, Schema.instance.getRealVersion().toString());
+    }
 }
diff --git a/test/unit/org/apache/cassandra/gms/SerializationsTest.java b/test/unit/org/apache/cassandra/gms/SerializationsTest.java
index e50b461..0df266f 100644
--- a/test/unit/org/apache/cassandra/gms/SerializationsTest.java
+++ b/test/unit/org/apache/cassandra/gms/SerializationsTest.java
@@ -19,6 +19,7 @@
 package org.apache.cassandra.gms;
 
 import org.apache.cassandra.AbstractSerializationsTester;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.io.util.DataInputPlus.DataInputStreamPlus;
@@ -26,6 +27,7 @@
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.utils.FBUtilities;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -38,6 +40,12 @@
 
 public class SerializationsTest extends AbstractSerializationsTester
 {
+    @BeforeClass
+    public static void initDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     private void testEndpointStateWrite() throws IOException
     {
         DataOutputStreamPlus out = getOutput("gms.EndpointState.bin");
diff --git a/test/unit/org/apache/cassandra/gms/ShadowRoundTest.java b/test/unit/org/apache/cassandra/gms/ShadowRoundTest.java
new file mode 100644
index 0000000..fb4ddcc
--- /dev/null
+++ b/test/unit/org/apache/cassandra/gms/ShadowRoundTest.java
@@ -0,0 +1,210 @@
+/*
+* 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.gms;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.cassandra.dht.IPartitioner;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.SystemKeyspace;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.locator.IEndpointSnitch;
+import org.apache.cassandra.locator.PropertyFileSnitch;
+import org.apache.cassandra.net.MessageIn;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.net.MockMessagingService;
+import org.apache.cassandra.net.MockMessagingSpy;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.utils.FBUtilities;
+
+import static org.apache.cassandra.net.MockMessagingService.verb;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ShadowRoundTest
+{
+    private static final Logger logger = LoggerFactory.getLogger(ShadowRoundTest.class);
+
+    @BeforeClass
+    public static void setUp() throws ConfigurationException
+    {
+        System.setProperty("cassandra.config", "cassandra-seeds.yaml");
+
+        DatabaseDescriptor.daemonInitialization();
+        IEndpointSnitch snitch = new PropertyFileSnitch();
+        DatabaseDescriptor.setEndpointSnitch(snitch);
+        Keyspace.setInitialized();
+    }
+
+    @After
+    public void cleanup()
+    {
+        MockMessagingService.cleanup();
+    }
+
+    @Test
+    public void testDelayedResponse()
+    {
+        Gossiper.instance.buildSeedsList();
+        int noOfSeeds = Gossiper.instance.seeds.size();
+
+        final AtomicBoolean ackSend = new AtomicBoolean(false);
+        MockMessagingSpy spySyn = MockMessagingService.when(verb(MessagingService.Verb.GOSSIP_DIGEST_SYN))
+                .respondN((msgOut, to) ->
+                {
+                    // ACK once to finish shadow round, then busy-spin until gossiper has been enabled
+                    // and then reply with remaining ACKs from other seeds
+                    if (!ackSend.compareAndSet(false, true))
+                    {
+                        while (!Gossiper.instance.isEnabled()) ;
+                    }
+
+                    HeartBeatState hb = new HeartBeatState(123, 456);
+                    EndpointState state = new EndpointState(hb);
+                    GossipDigestAck payload = new GossipDigestAck(
+                            Collections.singletonList(new GossipDigest(to, hb.getGeneration(), hb.getHeartBeatVersion())),
+                            Collections.singletonMap(to, state));
+
+                    logger.debug("Simulating digest ACK reply");
+                    return MessageIn.create(to, payload, Collections.emptyMap(), MessagingService.Verb.GOSSIP_DIGEST_ACK, MessagingService.current_version);
+                }, noOfSeeds);
+
+        // GossipDigestAckVerbHandler will send ack2 for each ack received (after the shadow round)
+        MockMessagingSpy spyAck2 = MockMessagingService.when(verb(MessagingService.Verb.GOSSIP_DIGEST_ACK2)).dontReply();
+
+        // Migration request messages should not be emitted during shadow round
+        MockMessagingSpy spyMigrationReq = MockMessagingService.when(verb(MessagingService.Verb.MIGRATION_REQUEST)).dontReply();
+
+        try
+        {
+            StorageService.instance.initServer();
+        }
+        catch (Exception e)
+        {
+            assertEquals("Unable to contact any seeds!", e.getMessage());
+        }
+
+        // we expect one SYN for each seed during shadow round + additional SYNs after gossiper has been enabled
+        assertTrue(spySyn.messagesIntercepted() > noOfSeeds);
+
+        // we don't expect to emit any GOSSIP_DIGEST_ACK2 or MIGRATION_REQUEST messages
+        assertEquals(0, spyAck2.messagesIntercepted());
+        assertEquals(0, spyMigrationReq.messagesIntercepted());
+    }
+
+    @Test
+    public void testBadAckInShadow()
+    {
+        final AtomicBoolean ackSend = new AtomicBoolean(false);
+        MockMessagingSpy spySyn = MockMessagingService.when(verb(MessagingService.Verb.GOSSIP_DIGEST_SYN))
+                .respondN((msgOut, to) ->
+                {
+                    // ACK with bad data in shadow round
+                    if (!ackSend.compareAndSet(false, true))
+                    {
+                        while (!Gossiper.instance.isEnabled()) ;
+                    }
+                    InetAddress junkaddr;
+                    try
+                    {
+                        junkaddr = InetAddress.getByName("1.1.1.1");
+                    }
+                    catch (UnknownHostException e)
+                    {
+                        throw new RuntimeException(e);
+                    }
+
+                    HeartBeatState hb = new HeartBeatState(123, 456);
+                    EndpointState state = new EndpointState(hb);
+                    List<GossipDigest> gDigests = new ArrayList<GossipDigest>();
+                    gDigests.add(new GossipDigest(FBUtilities.getBroadcastAddress(), hb.getGeneration(), hb.getHeartBeatVersion()));
+                    gDigests.add(new GossipDigest(junkaddr, hb.getGeneration(), hb.getHeartBeatVersion()));
+                    Map<InetAddress, EndpointState> smap = new HashMap<InetAddress, EndpointState>()
+                    {
+                        {
+                            put(FBUtilities.getBroadcastAddress(), state);
+                            put(junkaddr, state);
+                        }
+                    };
+                    GossipDigestAck payload = new GossipDigestAck(gDigests, smap);
+
+                    logger.debug("Simulating bad digest ACK reply");
+                    return MessageIn.create(to, payload, Collections.emptyMap(), MessagingService.Verb.GOSSIP_DIGEST_ACK, MessagingService.current_version);
+                }, 1);
+
+        System.setProperty(Config.PROPERTY_PREFIX + "auto_bootstrap", "false");
+        try
+        {
+            StorageService.instance.checkForEndpointCollision(SystemKeyspace.getOrInitializeLocalHostId(), SystemKeyspace.loadHostIds().keySet());
+        }
+        catch (Exception e)
+        {
+            assertEquals("Unable to gossip with any peers", e.getMessage());
+        }
+        System.clearProperty(Config.PROPERTY_PREFIX + "auto_bootstrap");
+    }
+
+    @Test
+    public void testPreviouslyAssassinatedInShadow()
+    {
+        final AtomicBoolean ackSend = new AtomicBoolean(false);
+        MockMessagingSpy spySyn = MockMessagingService.when(verb(MessagingService.Verb.GOSSIP_DIGEST_SYN))
+                .respondN((msgOut, to) ->
+                {
+                    // ACK with self assassinated in shadow round
+                    if (!ackSend.compareAndSet(false, true))
+                    {
+                        while (!Gossiper.instance.isEnabled()) ;
+                    }
+                    HeartBeatState hb = new HeartBeatState(123, 456);
+                    EndpointState state = new EndpointState(hb);
+                    state.addApplicationState(ApplicationState.STATUS,
+                            new VersionedValue.VersionedValueFactory(DatabaseDescriptor.getPartitioner()).left(
+                                    Collections.singletonList(DatabaseDescriptor.getPartitioner().getRandomToken()), 1L));
+                    GossipDigestAck payload = new GossipDigestAck(
+                        Collections.singletonList(new GossipDigest(FBUtilities.getBroadcastAddress(), hb.getGeneration(), hb.getHeartBeatVersion())),
+                        Collections.singletonMap(FBUtilities.getBroadcastAddress(), state));
+
+                    logger.debug("Simulating bad digest ACK reply");
+                    return MessageIn.create(to, payload, Collections.emptyMap(), MessagingService.Verb.GOSSIP_DIGEST_ACK, MessagingService.current_version);
+                }, 1);
+
+
+        System.setProperty(Config.PROPERTY_PREFIX + "auto_bootstrap", "false");
+        StorageService.instance.checkForEndpointCollision(SystemKeyspace.getOrInitializeLocalHostId(), SystemKeyspace.loadHostIds().keySet());
+        System.clearProperty(Config.PROPERTY_PREFIX + "auto_bootstrap");
+    }
+
+}
diff --git a/test/unit/org/apache/cassandra/hints/AlteredHints.java b/test/unit/org/apache/cassandra/hints/AlteredHints.java
new file mode 100644
index 0000000..7efe08f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/hints/AlteredHints.java
@@ -0,0 +1,146 @@
+/*
+ * 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.hints;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Files;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.RowUpdateBuilder;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.utils.UUIDGen;
+
+import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
+
+/**
+ * Base class for testing compressed and encrypted hints.
+ */
+public abstract class AlteredHints
+{
+    protected static final String KEYSPACE = "hints_compression_test";
+    private static final String TABLE = "table";
+
+    private static Mutation createMutation(int index, long timestamp)
+    {
+        CFMetaData table = Schema.instance.getCFMetaData(KEYSPACE, TABLE);
+        return new RowUpdateBuilder(table, timestamp, bytes(index))
+               .clustering(bytes(index))
+               .add("val", bytes(index))
+               .build();
+    }
+
+    private static Hint createHint(int idx, long baseTimestamp)
+    {
+        long timestamp = baseTimestamp + idx;
+        return Hint.create(createMutation(idx, TimeUnit.MILLISECONDS.toMicros(timestamp)), timestamp);
+    }
+
+    @BeforeClass
+    public static void defineSchema()
+    {
+        SchemaLoader.prepareServer();
+        SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(1), SchemaLoader.standardCFMD(KEYSPACE, TABLE));
+    }
+
+    abstract ImmutableMap<String, Object> params();
+    abstract boolean looksLegit(HintsWriter writer);
+    abstract boolean looksLegit(ChecksummedDataInput checksummedDataInput);
+
+    public void multiFlushAndDeserializeTest() throws Exception
+    {
+        int hintNum = 0;
+        int bufferSize = HintsWriteExecutor.WRITE_BUFFER_SIZE;
+        List<Hint> hints = new LinkedList<>();
+
+        UUID hostId = UUIDGen.getTimeUUID();
+        long ts = System.currentTimeMillis();
+
+        HintsDescriptor descriptor = new HintsDescriptor(hostId, ts, params());
+        File dir = Files.createTempDir();
+        try (HintsWriter writer = HintsWriter.create(dir, descriptor))
+        {
+            Assert.assertTrue(looksLegit(writer));
+
+            ByteBuffer writeBuffer = ByteBuffer.allocateDirect(bufferSize);
+            try (HintsWriter.Session session = writer.newSession(writeBuffer))
+            {
+                while (session.getBytesWritten() < bufferSize * 3)
+                {
+                    Hint hint = createHint(hintNum, ts+hintNum);
+                    session.append(hint);
+                    hints.add(hint);
+                    hintNum++;
+                }
+            }
+        }
+
+        try (HintsReader reader = HintsReader.open(new File(dir, descriptor.fileName())))
+        {
+            Assert.assertTrue(looksLegit(reader.getInput()));
+            List<Hint> deserialized = new ArrayList<>(hintNum);
+            List<InputPosition> pagePositions = new ArrayList<>(hintNum);
+
+            for (HintsReader.Page page: reader)
+            {
+                pagePositions.add(page.position);
+                Iterator<Hint> iterator = page.hintsIterator();
+                while (iterator.hasNext())
+                {
+                    deserialized.add(iterator.next());
+                }
+            }
+
+            Assert.assertEquals(hints.size(), deserialized.size());
+            hintNum = 0;
+            for (Hint expected: hints)
+            {
+                HintsTestUtil.assertHintsEqual(expected, deserialized.get(hintNum));
+                hintNum++;
+            }
+
+            // explicitely seek to each page by iterating collected page positions and check if hints still match as expected
+            int hintOffset = 0;
+            for (InputPosition pos : pagePositions)
+            {
+                reader.seek(pos);
+                HintsReader.Page page = reader.iterator().next();
+                Iterator<Hint> iterator = page.hintsIterator();
+                while (iterator.hasNext())
+                {
+                    Hint seekedHint = iterator.next();
+                    HintsTestUtil.assertHintsEqual(hints.get(hintOffset), seekedHint);
+                    hintOffset++;
+                }
+            }
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/hints/ChecksummedDataInputTest.java b/test/unit/org/apache/cassandra/hints/ChecksummedDataInputTest.java
index 323a12d..7325e74 100644
--- a/test/unit/org/apache/cassandra/hints/ChecksummedDataInputTest.java
+++ b/test/unit/org/apache/cassandra/hints/ChecksummedDataInputTest.java
@@ -24,8 +24,10 @@
 import java.util.Arrays;
 import java.util.zip.CRC32;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.io.util.RandomAccessReader;
 import org.apache.cassandra.io.util.SequentialWriter;
@@ -38,6 +40,12 @@
 
 public class ChecksummedDataInputTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testReadMethods() throws IOException
     {
@@ -77,7 +85,7 @@
         // save the buffer to file to create a RAR
         File file = File.createTempFile("testReadMethods", "1");
         file.deleteOnExit();
-        try (SequentialWriter writer = SequentialWriter.open(file))
+        try (SequentialWriter writer = new SequentialWriter(file))
         {
             writer.write(buffer);
             writer.writeInt((int) crc.getValue());
@@ -111,7 +119,7 @@
 
             // assert that the crc matches, and that we've read exactly as many bytes as expected
             assertTrue(reader.checkCrc());
-            assertEquals(0, reader.bytesRemaining());
+            assertTrue(reader.isEOF());
 
             reader.checkLimit(0);
         }
@@ -152,7 +160,7 @@
         // save the buffer to file to create a RAR
         File file = File.createTempFile("testResetCrc", "1");
         file.deleteOnExit();
-        try (SequentialWriter writer = SequentialWriter.open(file))
+        try (SequentialWriter writer = new SequentialWriter(file))
         {
             writer.write(buffer);
             writer.finish();
@@ -177,7 +185,7 @@
             assertEquals(2.2f, reader.readFloat());
             assertEquals(42, reader.readInt());
             assertTrue(reader.checkCrc());
-            assertEquals(0, reader.bytesRemaining());
+            assertTrue(reader.isEOF());
         }
     }
 
@@ -208,7 +216,7 @@
         // save the buffer to file to create a RAR
         File file = File.createTempFile("testFailedCrc", "1");
         file.deleteOnExit();
-        try (SequentialWriter writer = SequentialWriter.open(file))
+        try (SequentialWriter writer = new SequentialWriter(file))
         {
             writer.write(buffer);
             writer.finish();
@@ -227,7 +235,7 @@
             assertEquals(10, reader.readByte());
             assertEquals('t', reader.readChar());
             assertFalse(reader.checkCrc());
-            assertEquals(0, reader.bytesRemaining());
+            assertTrue(reader.isEOF());
         }
     }
 }
diff --git a/test/unit/org/apache/cassandra/hints/HintTest.java b/test/unit/org/apache/cassandra/hints/HintTest.java
index 1d486e1..e4a33fd 100644
--- a/test/unit/org/apache/cassandra/hints/HintTest.java
+++ b/test/unit/org/apache/cassandra/hints/HintTest.java
@@ -30,9 +30,7 @@
 
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
-import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.*;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.partitions.FilteredPartition;
 import org.apache.cassandra.db.partitions.PartitionIterator;
@@ -127,7 +125,7 @@
 
         // assert that we can read the inserted partitions
         for (PartitionUpdate partition : mutation.getPartitionUpdates())
-            assertPartitionsEqual(partition, readPartition(key, partition.metadata().cfName));
+            assertPartitionsEqual(partition, readPartition(key, partition.metadata().cfName, partition.columns()));
     }
 
     @Test
@@ -152,8 +150,10 @@
         assertNoPartitions(key, TABLE1);
 
         // TABLE0 and TABLE2 updates should have been applied successfully
-        assertPartitionsEqual(mutation.getPartitionUpdate(Schema.instance.getId(KEYSPACE, TABLE0)), readPartition(key, TABLE0));
-        assertPartitionsEqual(mutation.getPartitionUpdate(Schema.instance.getId(KEYSPACE, TABLE2)), readPartition(key, TABLE2));
+        PartitionUpdate upd0 = mutation.getPartitionUpdate(Schema.instance.getId(KEYSPACE, TABLE0));
+        assertPartitionsEqual(upd0, readPartition(key, TABLE0, upd0.columns()));
+        PartitionUpdate upd2 = mutation.getPartitionUpdate(Schema.instance.getId(KEYSPACE, TABLE2));
+        assertPartitionsEqual(upd2, readPartition(key, TABLE2, upd2.columns()));
     }
 
     @Test
@@ -296,43 +296,47 @@
 
     private static Mutation createMutation(String key, long now)
     {
-        Mutation mutation = new Mutation(KEYSPACE, dk(key));
+        Mutation.SimpleBuilder builder = Mutation.simpleBuilder(KEYSPACE, dk(key));
 
-        new RowUpdateBuilder(Schema.instance.getCFMetaData(KEYSPACE, TABLE0), now, mutation)
-            .clustering("column0")
-            .add("val", "value0")
-            .build();
+        builder.update(Schema.instance.getCFMetaData(KEYSPACE, TABLE0))
+               .timestamp(now)
+               .row("column0")
+               .add("val", "value0");
 
-        new RowUpdateBuilder(Schema.instance.getCFMetaData(KEYSPACE, TABLE1), now + 1, mutation)
-            .clustering("column1")
-            .add("val", "value1")
-            .build();
+        builder.update(Schema.instance.getCFMetaData(KEYSPACE, TABLE1))
+               .timestamp(now + 1)
+               .row("column1")
+               .add("val", "value1");
 
-        new RowUpdateBuilder(Schema.instance.getCFMetaData(KEYSPACE, TABLE2), now + 2, mutation)
-            .clustering("column2")
-            .add("val", "value2")
-            .build();
+        builder.update(Schema.instance.getCFMetaData(KEYSPACE, TABLE2))
+               .timestamp(now + 2)
+               .row("column2")
+               .add("val", "value2");
 
-        return mutation;
+        return builder.build();
     }
 
-    private static SinglePartitionReadCommand cmd(String key, String table)
+    private static ColumnFamilyStore cfs(String table)
     {
-        CFMetaData meta = Schema.instance.getCFMetaData(KEYSPACE, table);
-        return SinglePartitionReadCommand.fullPartitionRead(meta, FBUtilities.nowInSeconds(), bytes(key));
+        return Schema.instance.getColumnFamilyStoreInstance(Schema.instance.getCFMetaData(KEYSPACE, table).cfId);
     }
 
-    private static FilteredPartition readPartition(String key, String table)
+    private static FilteredPartition readPartition(String key, String table, PartitionColumns columns)
     {
-        return Util.getOnlyPartition(cmd(key, table));
+        String[] columnNames = new String[columns.size()];
+        int i = 0;
+        for (ColumnDefinition column : columns)
+            columnNames[i++] = column.name.toString();
+
+        return Util.getOnlyPartition(Util.cmd(cfs(table), key).columns(columnNames).build());
     }
 
     private static void assertNoPartitions(String key, String table)
     {
-        ReadCommand cmd = cmd(key, table);
+        ReadCommand cmd = Util.cmd(cfs(table), key).build();
 
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup();
-             PartitionIterator iterator = cmd.executeInternal(orderGroup))
+        try (ReadExecutionController executionController = cmd.executionController();
+             PartitionIterator iterator = cmd.executeInternal(executionController))
         {
             assertFalse(iterator.hasNext());
         }
diff --git a/test/unit/org/apache/cassandra/hints/HintsBufferTest.java b/test/unit/org/apache/cassandra/hints/HintsBufferTest.java
index 78ea4f4..08f7ec0 100644
--- a/test/unit/org/apache/cassandra/hints/HintsBufferTest.java
+++ b/test/unit/org/apache/cassandra/hints/HintsBufferTest.java
@@ -28,6 +28,7 @@
 import org.junit.Test;
 
 import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.concurrent.NamedThreadFactory;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.Mutation;
@@ -114,7 +115,7 @@
         // create HINT_THREADS_COUNT, start them, and wait for them to finish
         List<Thread> threads = new ArrayList<>(HINT_THREADS_COUNT);
         for (int i = 0; i < HINT_THREADS_COUNT; i ++)
-            threads.add(new Thread(new Writer(buffer, load, hintSize, i, baseTimestamp)));
+            threads.add(NamedThreadFactory.createThread(new Writer(buffer, load, hintSize, i, baseTimestamp)));
         threads.forEach(java.lang.Thread::start);
         for (Thread thread : threads)
             thread.join();
diff --git a/test/unit/org/apache/cassandra/hints/HintsCatalogTest.java b/test/unit/org/apache/cassandra/hints/HintsCatalogTest.java
index dcd31cf..68acd0c 100644
--- a/test/unit/org/apache/cassandra/hints/HintsCatalogTest.java
+++ b/test/unit/org/apache/cassandra/hints/HintsCatalogTest.java
@@ -69,20 +69,6 @@
         }
     }
 
-    @Test
-    public void exciseHintFiles() throws IOException
-    {
-        File directory = Files.createTempDirectory(null).toFile();
-        try
-        {
-            exciseHintFiles(directory);
-        }
-        finally
-        {
-            directory.deleteOnExit();
-        }
-    }
-
     private void loadCompletenessAndOrderTest(File directory) throws IOException
     {
         UUID hostId1 = UUID.randomUUID();
@@ -119,6 +105,51 @@
         assertNull(store2.poll());
     }
 
+    @Test
+    public void deleteHintsTest() throws IOException
+    {
+        File directory = Files.createTempDirectory(null).toFile();
+        UUID hostId1 = UUID.randomUUID();
+        UUID hostId2 = UUID.randomUUID();
+        long now = System.currentTimeMillis();
+        writeDescriptor(directory, new HintsDescriptor(hostId1, now));
+        writeDescriptor(directory, new HintsDescriptor(hostId1, now + 1));
+        writeDescriptor(directory, new HintsDescriptor(hostId2, now + 2));
+        writeDescriptor(directory, new HintsDescriptor(hostId2, now + 3));
+
+        // load catalog containing two stores (one for each host)
+        HintsCatalog catalog = HintsCatalog.load(directory, ImmutableMap.of());
+        assertEquals(2, catalog.stores().count());
+        assertTrue(catalog.hasFiles());
+
+        // delete all hints from store 1
+        assertTrue(catalog.get(hostId1).hasFiles());
+        catalog.deleteAllHints(hostId1);
+        assertFalse(catalog.get(hostId1).hasFiles());
+        // stores are still keepts for each host, even after deleting hints
+        assertEquals(2, catalog.stores().count());
+        assertTrue(catalog.hasFiles());
+
+        // delete all hints from all stores
+        catalog.deleteAllHints();
+        assertEquals(2, catalog.stores().count());
+        assertFalse(catalog.hasFiles());
+    }
+
+    @Test
+    public void exciseHintFiles() throws IOException
+    {
+        File directory = Files.createTempDirectory(null).toFile();
+        try
+        {
+            exciseHintFiles(directory);
+        }
+        finally
+        {
+            directory.deleteOnExit();
+        }
+    }
+
     private static void exciseHintFiles(File directory) throws IOException
     {
         UUID hostId = UUID.randomUUID();
@@ -152,37 +183,6 @@
         assertEquals(0, store.getDispatchQueueSize());
     }
 
-    @Test
-    public void deleteHintsTest() throws IOException
-    {
-        File directory = Files.createTempDirectory(null).toFile();
-        UUID hostId1 = UUID.randomUUID();
-        UUID hostId2 = UUID.randomUUID();
-        long now = System.currentTimeMillis();
-        writeDescriptor(directory, new HintsDescriptor(hostId1, now));
-        writeDescriptor(directory, new HintsDescriptor(hostId1, now+1));
-        writeDescriptor(directory, new HintsDescriptor(hostId2, now+2));
-        writeDescriptor(directory, new HintsDescriptor(hostId2, now+3));
-
-        // load catalog containing two stores (one for each host)
-        HintsCatalog catalog = HintsCatalog.load(directory, ImmutableMap.of());
-        assertEquals(2, catalog.stores().count());
-        assertTrue(catalog.hasFiles());
-
-        // delete all hints from store 1
-        assertTrue(catalog.get(hostId1).hasFiles());
-        catalog.deleteAllHints(hostId1);
-        assertFalse(catalog.get(hostId1).hasFiles());
-        // stores are still keepts for each host, even after deleting hints
-        assertEquals(2, catalog.stores().count());
-        assertTrue(catalog.hasFiles());
-
-        // delete all hints from all stores
-        catalog.deleteAllHints();
-        assertEquals(2, catalog.stores().count());
-        assertFalse(catalog.hasFiles());
-    }
-
     @SuppressWarnings("EmptyTryBlock")
     private static void writeDescriptor(File directory, HintsDescriptor descriptor) throws IOException
     {
@@ -193,24 +193,24 @@
 
     private static Mutation createMutation(String key, long now)
     {
-        Mutation mutation = new Mutation(KEYSPACE, dk(key));
+        Mutation.SimpleBuilder builder = Mutation.simpleBuilder(KEYSPACE, dk(key));
 
-        new RowUpdateBuilder(Schema.instance.getCFMetaData(KEYSPACE, TABLE0), now, mutation)
-                .clustering("column0")
-                .add("val", "value0")
-                .build();
+        builder.update(Schema.instance.getCFMetaData(KEYSPACE, TABLE0))
+               .timestamp(now)
+               .row("column0")
+               .add("val", "value0");
 
-        new RowUpdateBuilder(Schema.instance.getCFMetaData(KEYSPACE, TABLE1), now + 1, mutation)
-                .clustering("column1")
-                .add("val", "value1")
-                .build();
+        builder.update(Schema.instance.getCFMetaData(KEYSPACE, TABLE1))
+               .timestamp(now + 1)
+               .row("column1")
+               .add("val", "value1");
 
-        new RowUpdateBuilder(Schema.instance.getCFMetaData(KEYSPACE, TABLE2), now + 2, mutation)
-                .clustering("column2")
-                .add("val", "value2")
-                .build();
+        builder.update(Schema.instance.getCFMetaData(KEYSPACE, TABLE2))
+               .timestamp(now + 2)
+               .row("column2")
+               .add("val", "value2");
 
-        return mutation;
+        return builder.build();
     }
 
     @SuppressWarnings("EmptyTryBlock")
diff --git a/test/unit/org/apache/cassandra/hints/HintsCompressionTest.java b/test/unit/org/apache/cassandra/hints/HintsCompressionTest.java
index 656d7cd..f82db49 100644
--- a/test/unit/org/apache/cassandra/hints/HintsCompressionTest.java
+++ b/test/unit/org/apache/cassandra/hints/HintsCompressionTest.java
@@ -18,65 +18,20 @@
 
 package org.apache.cassandra.hints;
 
-import java.io.File;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-
 import com.google.common.collect.ImmutableMap;
-import com.google.common.io.Files;
-import org.junit.Assert;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
-import org.apache.cassandra.SchemaLoader;
-import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ParameterizedClass;
-import org.apache.cassandra.config.Schema;
-import org.apache.cassandra.db.Mutation;
-import org.apache.cassandra.db.RowUpdateBuilder;
 import org.apache.cassandra.io.compress.DeflateCompressor;
 import org.apache.cassandra.io.compress.ICompressor;
 import org.apache.cassandra.io.compress.LZ4Compressor;
 import org.apache.cassandra.io.compress.SnappyCompressor;
-import org.apache.cassandra.schema.KeyspaceParams;
-import org.apache.cassandra.utils.UUIDGen;
 
-import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
-
-public class HintsCompressionTest
+public class HintsCompressionTest extends AlteredHints
 {
-    private static final String KEYSPACE = "hints_compression_test";
-    private static final String TABLE = "table";
+    private Class<? extends ICompressor> compressorClass;
 
-
-    private static Mutation createMutation(int index, long timestamp)
-    {
-        CFMetaData table = Schema.instance.getCFMetaData(KEYSPACE, TABLE);
-        return new RowUpdateBuilder(table, timestamp, bytes(index))
-               .clustering(bytes(index))
-               .add("val", bytes(index))
-               .build();
-    }
-
-    private static Hint createHint(int idx, long baseTimestamp)
-    {
-        long timestamp = baseTimestamp + idx;
-        return Hint.create(createMutation(idx, TimeUnit.MILLISECONDS.toMicros(timestamp)), timestamp);
-    }
-
-    @BeforeClass
-    public static void defineSchema()
-    {
-        SchemaLoader.prepareServer();
-        SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(1), SchemaLoader.standardCFMD(KEYSPACE, TABLE));
-    }
-
-    private ImmutableMap<String, Object> params(Class<? extends ICompressor> compressorClass)
+    ImmutableMap<String, Object> params()
     {
         ImmutableMap<String, Object> compressionParams = ImmutableMap.<String, Object>builder()
                                                                      .put(ParameterizedClass.CLASS_NAME, compressorClass.getSimpleName())
@@ -86,89 +41,40 @@
                            .build();
     }
 
-    public void multiFlushAndDeserializeTest(Class<? extends ICompressor> compressorClass) throws Exception
+    boolean looksLegit(HintsWriter writer)
     {
-        int hintNum = 0;
-        int bufferSize = HintsWriteExecutor.WRITE_BUFFER_SIZE;
-        List<Hint> hints = new LinkedList<>();
+        if (!(writer instanceof CompressedHintsWriter))
+            return false;
+        CompressedHintsWriter compressedHintsWriter = (CompressedHintsWriter)writer;
+        return compressedHintsWriter.getCompressor().getClass().isAssignableFrom(compressorClass);
+    }
 
-        UUID hostId = UUIDGen.getTimeUUID();
-        long ts = System.currentTimeMillis();
-
-        HintsDescriptor descriptor = new HintsDescriptor(hostId, ts, params(compressorClass));
-        File dir = Files.createTempDir();
-        try (HintsWriter writer = HintsWriter.create(dir, descriptor))
-        {
-            assert writer instanceof CompressedHintsWriter;
-
-            ByteBuffer writeBuffer = ByteBuffer.allocateDirect(bufferSize);
-            try (HintsWriter.Session session = writer.newSession(writeBuffer))
-            {
-                while (session.getBytesWritten() < bufferSize * 3)
-                {
-                    Hint hint = createHint(hintNum, ts+hintNum);
-                    session.append(hint);
-                    hints.add(hint);
-                    hintNum++;
-                }
-            }
-        }
-
-        try (HintsReader reader = HintsReader.open(new File(dir, descriptor.fileName())))
-        {
-            List<Hint> deserialized = new ArrayList<>(hintNum);
-            List<InputPosition> pagePositions = new ArrayList<>(hintNum);
-
-            for (HintsReader.Page page: reader)
-            {
-                pagePositions.add(page.position);
-                Iterator<Hint> iterator = page.hintsIterator();
-                while (iterator.hasNext())
-                {
-                    deserialized.add(iterator.next());
-                }
-            }
-
-            Assert.assertEquals(hints.size(), deserialized.size());
-            hintNum = 0;
-            for (Hint expected: hints)
-            {
-                HintsTestUtil.assertHintsEqual(expected, deserialized.get(hintNum));
-                hintNum++;
-            }
-
-            // explicitely seek to each page by iterating collected page positions and check if hints still match as expected
-            int hintOffset = 0;
-            for (InputPosition pos : pagePositions)
-            {
-                reader.seek(pos);
-                HintsReader.Page page = reader.iterator().next();
-                Iterator<Hint> iterator = page.hintsIterator();
-                while (iterator.hasNext())
-                {
-                    Hint seekedHint = iterator.next();
-                    HintsTestUtil.assertHintsEqual(hints.get(hintOffset), seekedHint);
-                    hintOffset++;
-                }
-            }
-        }
+    boolean looksLegit(ChecksummedDataInput checksummedDataInput)
+    {
+        if (!(checksummedDataInput instanceof CompressedChecksummedDataInput))
+            return false;
+        CompressedChecksummedDataInput compressedChecksummedDataInput = (CompressedChecksummedDataInput)checksummedDataInput;
+        return compressedChecksummedDataInput.getCompressor().getClass().isAssignableFrom(compressorClass);
     }
 
     @Test
     public void lz4Compressor() throws Exception
     {
-        multiFlushAndDeserializeTest(LZ4Compressor.class);
+        compressorClass = LZ4Compressor.class;
+        multiFlushAndDeserializeTest();
     }
 
     @Test
     public void snappyCompressor() throws Exception
     {
-        multiFlushAndDeserializeTest(SnappyCompressor.class);
+        compressorClass = SnappyCompressor.class;
+        multiFlushAndDeserializeTest();
     }
 
     @Test
     public void deflateCompressor() throws Exception
     {
-        multiFlushAndDeserializeTest(DeflateCompressor.class);
+        compressorClass = DeflateCompressor.class;
+        multiFlushAndDeserializeTest();
     }
 }
diff --git a/test/unit/org/apache/cassandra/hints/HintsEncryptionTest.java b/test/unit/org/apache/cassandra/hints/HintsEncryptionTest.java
new file mode 100644
index 0000000..beb95d1
--- /dev/null
+++ b/test/unit/org/apache/cassandra/hints/HintsEncryptionTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.hints;
+
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.security.EncryptionContext;
+import org.apache.cassandra.security.EncryptionContextGenerator;
+
+public class HintsEncryptionTest extends AlteredHints
+{
+    EncryptionContext encryptionContext;
+    Cipher cipher;
+
+    @Before
+    public void setup()
+    {
+        encryptionContext = EncryptionContextGenerator.createContext(true);
+        DatabaseDescriptor.setEncryptionContext(encryptionContext);
+    }
+
+    @Test
+    public void encryptedHints() throws Exception
+    {
+        multiFlushAndDeserializeTest();
+    }
+
+    boolean looksLegit(HintsWriter writer)
+    {
+        if (!(writer instanceof EncryptedHintsWriter))
+            return false;
+
+        EncryptedHintsWriter encryptedHintsWriter = (EncryptedHintsWriter)writer;
+        cipher = encryptedHintsWriter.getCipher();
+
+        return encryptedHintsWriter.getCompressor().getClass().isAssignableFrom(encryptionContext.getCompressor().getClass());
+    }
+
+    boolean looksLegit(ChecksummedDataInput checksummedDataInput)
+    {
+        if (!(checksummedDataInput instanceof EncryptedChecksummedDataInput))
+            return false;
+
+        EncryptedChecksummedDataInput encryptedDataInput = (EncryptedChecksummedDataInput)checksummedDataInput;
+
+        return Arrays.equals(cipher.getIV(), encryptedDataInput.getCipher().getIV()) &&
+               encryptedDataInput.getCompressor().getClass().isAssignableFrom(encryptionContext.getCompressor().getClass());
+    }
+
+    ImmutableMap<String, Object> params()
+    {
+        ImmutableMap<String, Object> compressionParams = ImmutableMap.<String, Object>builder()
+                                                         .putAll(encryptionContext.toHeaderParameters())
+                                                         .build();
+        return ImmutableMap.<String, Object>builder()
+               .put(HintsDescriptor.ENCRYPTION, compressionParams)
+               .build();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/hints/HintsServiceTest.java b/test/unit/org/apache/cassandra/hints/HintsServiceTest.java
index ab1cbd0..077a9d1 100644
--- a/test/unit/org/apache/cassandra/hints/HintsServiceTest.java
+++ b/test/unit/org/apache/cassandra/hints/HintsServiceTest.java
@@ -23,25 +23,21 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-
 import javax.annotation.Nullable;
 
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
 import com.datastax.driver.core.utils.MoreFutures;
-
 import org.apache.cassandra.SchemaLoader;
-import org.apache.cassandra.UpdateBuilder;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.Schema;
 import org.apache.cassandra.db.DecoratedKey;
-import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.gms.IFailureDetectionEventListener;
 import org.apache.cassandra.gms.IFailureDetector;
 import org.apache.cassandra.metrics.StorageMetrics;
@@ -207,13 +203,9 @@
             long now = System.currentTimeMillis();
             DecoratedKey dkey = dk(String.valueOf(i));
             CFMetaData cfMetaData = Schema.instance.getCFMetaData(KEYSPACE, TABLE);
-
-            UpdateBuilder builder = UpdateBuilder.create(cfMetaData, dkey)
-                    .withTimestamp(now)
-                    .newRow("column0")
-                    .add("val", "value0");
-            Hint hint = Hint.create((Mutation) builder.makeMutation(), now);
-
+            PartitionUpdate.SimpleBuilder builder = PartitionUpdate.simpleBuilder(cfMetaData, dkey).timestamp(now);
+            builder.row("column0").add("val", "value0");
+            Hint hint = Hint.create(builder.buildAsMutation(), now);
             HintsService.instance.write(hostId, hint);
         }
         return spy;
diff --git a/test/unit/org/apache/cassandra/hints/LegacyHintsMigratorTest.java b/test/unit/org/apache/cassandra/hints/LegacyHintsMigratorTest.java
index cc97df0..78849e3 100644
--- a/test/unit/org/apache/cassandra/hints/LegacyHintsMigratorTest.java
+++ b/test/unit/org/apache/cassandra/hints/LegacyHintsMigratorTest.java
@@ -27,8 +27,10 @@
 import org.junit.Test;
 
 import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.marshal.UUIDType;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
@@ -78,7 +80,7 @@
     private static void testNothingToMigrate(File directory)
     {
         // truncate system.hints to enseure nothing inside
-        Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.LEGACY_HINTS).truncateBlocking();
+        Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.LEGACY_HINTS).truncateBlocking();
         new LegacyHintsMigrator(directory, 128 * 1024 * 1024).migrate();
         HintsCatalog catalog = HintsCatalog.load(directory, HintsService.EMPTY_PARAMS);
         assertEquals(0, catalog.stores().count());
@@ -123,7 +125,7 @@
         new LegacyHintsMigrator(directory, 128 * 1024 * 1024).migrate();
 
         // validate that the hints table is truncated now
-        assertTrue(Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStore(SystemKeyspace.LEGACY_HINTS).isEmpty());
+        assertTrue(Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.LEGACY_HINTS).isEmpty());
 
         HintsCatalog catalog = HintsCatalog.load(directory, HintsService.EMPTY_PARAMS);
 
@@ -159,7 +161,7 @@
 
                 assertEquals(timestamp, hint.creationTime);
                 assertEquals(ttl, hint.gcgs);
-                assertMutationsEqual(mutation, hint.mutation);
+                assertTrue(mutation + " != " + hint.mutation, Util.sameContent(mutation, hint.mutation));
             }
         }
     }
diff --git a/test/unit/org/apache/cassandra/index/CustomIndexTest.java b/test/unit/org/apache/cassandra/index/CustomIndexTest.java
index 1ab08fd..9fe3f65 100644
--- a/test/unit/org/apache/cassandra/index/CustomIndexTest.java
+++ b/test/unit/org/apache/cassandra/index/CustomIndexTest.java
@@ -50,7 +50,7 @@
 import org.apache.cassandra.index.transactions.IndexTransaction;
 import org.apache.cassandra.schema.IndexMetadata;
 import org.apache.cassandra.schema.Indexes;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.concurrent.OpOrder;
@@ -361,7 +361,7 @@
 
         createIndex(String.format("CREATE CUSTOM INDEX %s ON %%s(c) USING '%s'", indexName, StubIndex.class.getName()));
 
-        assertInvalidThrowMessage(Server.CURRENT_VERSION,
+        assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   String.format(IndexRestrictions.INDEX_NOT_FOUND, "no_such_index", keyspace(), currentTable()),
                                   QueryValidationException.class,
                                   "SELECT * FROM %s WHERE expr(no_such_index, 'foo bar baz ')");
@@ -372,7 +372,7 @@
         assertRows(execute(String.format("SELECT * FROM %%s WHERE expr(%s, $$foo \" ~~~ bar Baz$$)", indexName)), row);
 
         // multiple expressions on the same index
-        assertInvalidThrowMessage(Server.CURRENT_VERSION,
+        assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   IndexRestrictions.MULTIPLE_EXPRESSIONS,
                                   QueryValidationException.class,
                                   String.format("SELECT * FROM %%s WHERE expr(%1$s, 'foo') AND expr(%1$s, 'bar')",
@@ -380,13 +380,13 @@
 
         // multiple expressions on different indexes
         createIndex(String.format("CREATE CUSTOM INDEX other_custom_index ON %%s(d) USING '%s'", StubIndex.class.getName()));
-        assertInvalidThrowMessage(Server.CURRENT_VERSION,
+        assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   IndexRestrictions.MULTIPLE_EXPRESSIONS,
                                   QueryValidationException.class,
                                   String.format("SELECT * FROM %%s WHERE expr(%s, 'foo') AND expr(other_custom_index, 'bar')",
                                                 indexName));
 
-        assertInvalidThrowMessage(Server.CURRENT_VERSION,
+        assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
                                   QueryValidationException.class,
                                   String.format("SELECT * FROM %%s WHERE expr(%s, 'foo') AND d=0", indexName));
@@ -401,7 +401,7 @@
         createIndex(String.format("CREATE CUSTOM INDEX %s ON %%s(c) USING '%s'",
                                   indexName,
                                   NoCustomExpressionsIndex.class.getName()));
-        assertInvalidThrowMessage(Server.CURRENT_VERSION,
+        assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   String.format( IndexRestrictions.CUSTOM_EXPRESSION_NOT_SUPPORTED, indexName),
                                   QueryValidationException.class,
                                   String.format("SELECT * FROM %%s WHERE expr(%s, 'foo bar baz')", indexName));
@@ -415,7 +415,7 @@
         createIndex(String.format("CREATE CUSTOM INDEX %s ON %%s(c) USING '%s'",
                                   indexName,
                                   AlwaysRejectIndex.class.getName()));
-        assertInvalidThrowMessage(Server.CURRENT_VERSION,
+        assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   "None shall pass",
                                   QueryValidationException.class,
                                   String.format("SELECT * FROM %%s WHERE expr(%s, 'foo bar baz')", indexName));
@@ -426,7 +426,7 @@
     {
         createTable("CREATE TABLE %s (a int, b int, c int, d int, PRIMARY KEY (a, b))");
         createIndex("CREATE INDEX non_custom_index ON %s(c)");
-        assertInvalidThrowMessage(Server.CURRENT_VERSION,
+        assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   String.format(IndexRestrictions.NON_CUSTOM_INDEX_IN_EXPRESSION, "non_custom_index"),
                                   QueryValidationException.class,
                                   "SELECT * FROM %s WHERE expr(non_custom_index, 'c=0')");
@@ -440,11 +440,11 @@
         createIndex(String.format("CREATE CUSTOM INDEX %s ON %%s(c) USING '%s'",
                                   indexName, StubIndex.class.getName()));
 
-        assertInvalidThrowMessage(Server.CURRENT_VERSION,
+        assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   ModificationStatement.CUSTOM_EXPRESSIONS_NOT_ALLOWED,
                                   QueryValidationException.class,
                                   String.format("DELETE FROM %%s WHERE expr(%s, 'foo bar baz ')", indexName));
-        assertInvalidThrowMessage(Server.CURRENT_VERSION,
+        assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   ModificationStatement.CUSTOM_EXPRESSIONS_NOT_ALLOWED,
                                   QueryValidationException.class,
                                   String.format("UPDATE %%s SET d=0 WHERE expr(%s, 'foo bar baz ')", indexName));
@@ -522,13 +522,13 @@
                                   UTF8ExpressionIndex.class.getName()));
 
         execute("SELECT * FROM %s WHERE expr(text_index, 'foo')");
-        assertInvalidThrowMessage(Server.CURRENT_VERSION,
+        assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   "Invalid INTEGER constant (99) for \"custom index expression\" of type text",
                                   QueryValidationException.class,
                                   "SELECT * FROM %s WHERE expr(text_index, 99)");
 
         execute("SELECT * FROM %s WHERE expr(int_index, 99)");
-        assertInvalidThrowMessage(Server.CURRENT_VERSION,
+        assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   "Invalid STRING constant (foo) for \"custom index expression\" of type int",
                                   QueryValidationException.class,
                                   "SELECT * FROM %s WHERE expr(int_index, 'foo')");
@@ -567,8 +567,8 @@
         assertEquals(0, index.partitionDeletions.size());
 
         ReadCommand cmd = Util.cmd(cfs, 0).build();
-        try (ReadOrderGroup orderGroup = cmd.startOrderGroup();
-             UnfilteredPartitionIterator iterator = cmd.executeLocally(orderGroup))
+        try (ReadExecutionController executionController = cmd.executionController();
+             UnfilteredPartitionIterator iterator = cmd.executeLocally(executionController))
         {
             assertTrue(iterator.hasNext());
             cfs.indexManager.deletePartition(iterator.next(), FBUtilities.nowInSeconds());
@@ -640,7 +640,7 @@
         try
         {
             getCurrentColumnFamilyStore().forceBlockingFlush();
-            fail("Flush should have thrown an exception.");
+            fail("Exception should have been propagated");
         }
         catch (Throwable t)
         {
diff --git a/test/unit/org/apache/cassandra/index/StubIndex.java b/test/unit/org/apache/cassandra/index/StubIndex.java
index 569ce97..c80f0d9 100644
--- a/test/unit/org/apache/cassandra/index/StubIndex.java
+++ b/test/unit/org/apache/cassandra/index/StubIndex.java
@@ -31,7 +31,6 @@
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.db.partitions.PartitionIterator;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
-import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
 import org.apache.cassandra.db.rows.Row;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.index.transactions.IndexTransaction;
@@ -54,6 +53,7 @@
     public List<Row> rowsInserted = new ArrayList<>();
     public List<Row> rowsDeleted = new ArrayList<>();
     public List<Pair<Row,Row>> rowsUpdated = new ArrayList<>();
+    public volatile boolean preJoinInvocation;
     private IndexMetadata indexMetadata;
     private ColumnFamilyStore baseCfs;
 
@@ -176,6 +176,14 @@
         return null;
     }
 
+    public Callable<?> getPreJoinTask(boolean hadBootstrap)
+    {
+        return () -> {
+            preJoinInvocation = true;
+            return null;
+        };
+    }
+
     public Callable<?> getInvalidateTask()
     {
         return null;
@@ -198,7 +206,7 @@
 
     public Searcher searcherFor(final ReadCommand command)
     {
-        return (orderGroup) -> Util.executeLocally((PartitionRangeReadCommand)command, baseCfs, orderGroup);
+        return (controller) -> Util.executeLocally((PartitionRangeReadCommand)command, baseCfs, controller);
     }
 
     public BiFunction<PartitionIterator, ReadCommand, PartitionIterator> postProcessorFor(ReadCommand readCommand)
diff --git a/test/unit/org/apache/cassandra/index/internal/CassandraIndexTest.java b/test/unit/org/apache/cassandra/index/internal/CassandraIndexTest.java
index 36c0249..4ad93b4 100644
--- a/test/unit/org/apache/cassandra/index/internal/CassandraIndexTest.java
+++ b/test/unit/org/apache/cassandra/index/internal/CassandraIndexTest.java
@@ -18,7 +18,6 @@
 
 package org.apache.cassandra.index.internal;
 
-import java.nio.ByteBuffer;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
@@ -27,8 +26,10 @@
 import com.google.common.collect.*;
 import org.junit.Test;
 
+import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
@@ -136,7 +137,7 @@
                         .target("k1")
                         .withFirstRow(row(0, 0, 0, 0, 0))
                         .withSecondRow(row(1, 1, 1, 1, 1))
-                        .missingIndexMessage("Partition key parts: k2 must be restricted as other parts are")
+                        .missingIndexMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE)
                         .firstQueryExpression("k1=0")
                         .secondQueryExpression("k1=1")
                         .run();
@@ -150,7 +151,7 @@
                         .target("k2")
                         .withFirstRow(row(0, 0, 0, 0, 0))
                         .withSecondRow(row(1, 1, 1, 1, 1))
-                        .missingIndexMessage("Partition key parts: k1 must be restricted as other parts are")
+                        .missingIndexMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE)
                         .firstQueryExpression("k2=0")
                         .secondQueryExpression("k2=1")
                         .run();
@@ -367,10 +368,61 @@
     }
 
     @Test
+    public void indexOnStaticColumn() throws Throwable
+    {
+        Object[] row1 = row("k0", "c0", "s0");
+        Object[] row2 = row("k0", "c1", "s0");
+        Object[] row3 = row("k1", "c0", "s1");
+        Object[] row4 = row("k1", "c1", "s1");
+
+        createTable("CREATE TABLE %s (k text, c text, s text static, PRIMARY KEY (k, c));");
+        createIndex("CREATE INDEX sc_index on %s(s)");
+
+        execute("INSERT INTO %s (k, c, s) VALUES (?, ?, ?)", row1);
+        execute("INSERT INTO %s (k, c, s) VALUES (?, ?, ?)", row2);
+        execute("INSERT INTO %s (k, c, s) VALUES (?, ?, ?)", row3);
+        execute("INSERT INTO %s (k, c, s) VALUES (?, ?, ?)", row4);
+
+        assertRows(execute("SELECT * FROM %s WHERE s = ?", "s0"), row1, row2);
+        assertRows(execute("SELECT * FROM %s WHERE s = ?", "s1"), row3, row4);
+
+        assertRows(execute("SELECT * FROM %s WHERE s = ? AND token(k) >= token(?)", "s0", "k0"), row1, row2);
+        assertRows(execute("SELECT * FROM %s WHERE s = ? AND token(k) >= token(?)", "s1", "k1"), row3, row4);
+
+        assertEmpty(execute("SELECT * FROM %s WHERE s = ? AND token(k) < token(?)", "s0", "k0"));
+        assertEmpty(execute("SELECT * FROM %s WHERE s = ? AND token(k) < token(?)", "s1", "k1"));
+
+        row1 = row("s0");
+        row2 = row("s0");
+        row3 = row("s1");
+        row4 = row("s1");
+
+        assertRows(execute("SELECT s FROM %s WHERE s = ?", "s0"), row1, row2);
+        assertRows(execute("SELECT s FROM %s WHERE s = ?", "s1"), row3, row4);
+
+        assertRows(execute("SELECT s FROM %s WHERE s = ? AND token(k) >= token(?)", "s0", "k0"), row1, row2);
+        assertRows(execute("SELECT s FROM %s WHERE s = ? AND token(k) >= token(?)", "s1", "k1"), row3, row4);
+
+        assertEmpty(execute("SELECT s FROM %s WHERE s = ? AND token(k) < token(?)", "s0", "k0"));
+        assertEmpty(execute("SELECT s FROM %s WHERE s = ? AND token(k) < token(?)", "s1", "k1"));
+
+        dropIndex(String.format("DROP INDEX %s.sc_index", keyspace()));
+
+        assertRows(execute("SELECT s FROM %s WHERE s = ? ALLOW FILTERING", "s0"), row1, row2);
+        assertRows(execute("SELECT s FROM %s WHERE s = ? ALLOW FILTERING", "s1"), row3, row4);
+
+        assertRows(execute("SELECT s FROM %s WHERE s = ? AND token(k) >= token(?) ALLOW FILTERING", "s0", "k0"), row1, row2);
+        assertRows(execute("SELECT s FROM %s WHERE s = ? AND token(k) >= token(?) ALLOW FILTERING", "s1", "k1"), row3, row4);
+
+        assertEmpty(execute("SELECT s FROM %s WHERE s = ? AND token(k) < token(?) ALLOW FILTERING", "s0", "k0"));
+        assertEmpty(execute("SELECT s FROM %s WHERE s = ? AND token(k) < token(?) ALLOW FILTERING", "s1", "k1"));
+    }
+
+    @Test
     public void testIndexOnCompactTable() throws Throwable
     {
         createTable("CREATE TABLE %s (k int, v int, PRIMARY KEY (k)) WITH COMPACT STORAGE;");
-        assertInvalidMessage("No column definition found for column value",
+        assertInvalidMessage("Undefined column name value",
                              "CREATE INDEX idx_value ON %s(value)");
     }
 
@@ -516,35 +568,40 @@
     public void indexCorrectlyMarkedAsBuildAndRemoved() throws Throwable
     {
         String selectBuiltIndexesQuery = String.format("SELECT * FROM %s.\"%s\"",
-                                                       SystemKeyspace.NAME,
+                                                       SchemaConstants.SYSTEM_KEYSPACE_NAME,
                                                        SystemKeyspace.BUILT_INDEXES);
-        UntypedResultSet rs = execute(selectBuiltIndexesQuery);
-        int initialSize = rs.size();
+
+        // Wait for any background index clearing tasks to complete. Warn: When we used to run tests in parallel there
+        // could also be cross test class talk and have other indices pop up here.
+        Util.spinAssertEquals(0, () -> {
+            try
+            {
+                return execute(selectBuiltIndexesQuery).size();
+            }
+            catch (Throwable e)
+            {
+                throw new AssertionError(e);
+            }
+        }, 60);
 
         String indexName = "build_remove_test_idx";
         String tableName = createTable("CREATE TABLE %s (a int, b int, c int, PRIMARY KEY (a, b))");
         createIndex(String.format("CREATE INDEX %s ON %%s(c)", indexName));
         waitForIndex(KEYSPACE, tableName, indexName);
+
         // check that there are no other rows in the built indexes table
-        rs = execute(selectBuiltIndexesQuery);
-        int sizeAfterBuild = rs.size();
-        assertRowsIgnoringOrderAndExtra(rs, row(KEYSPACE, indexName));
+        assertRows(execute(selectBuiltIndexesQuery), row(KEYSPACE, indexName));
 
         // rebuild the index and verify the built status table
         getCurrentColumnFamilyStore().rebuildSecondaryIndex(indexName);
         waitForIndex(KEYSPACE, tableName, indexName);
 
         // check that there are no other rows in the built indexes table
-        rs = execute(selectBuiltIndexesQuery);
-        assertEquals(sizeAfterBuild, rs.size());
-        assertRowsIgnoringOrderAndExtra(rs, row(KEYSPACE, indexName));
+        assertRows(execute(selectBuiltIndexesQuery), row(KEYSPACE, indexName));
 
         // check that dropping the index removes it from the built indexes table
         dropIndex("DROP INDEX %s." + indexName);
-        rs = execute(selectBuiltIndexesQuery);
-        assertEquals(initialSize, rs.size());
-        rs.forEach(row -> assertFalse(row.getString("table_name").equals(KEYSPACE)  // table_name is actually keyspace
-                                      && row.getString("index_name").equals(indexName)));
+        assertRows(execute(selectBuiltIndexesQuery));
     }
 
     // this is slightly annoying, but we cannot read rows from the methods in Util as
@@ -563,8 +620,8 @@
                                                                                indexKey,
                                                                                ColumnFilter.all(indexCfs.metadata),
                                                                                filter);
-        try (ReadOrderGroup orderGroup = ReadOrderGroup.forCommand(command);
-             UnfilteredRowIterator iter = command.queryMemtableAndDisk(indexCfs, orderGroup.indexReadOpOrderGroup()))
+        try (ReadExecutionController executionController = command.executionController();
+             UnfilteredRowIterator iter = command.queryMemtableAndDisk(indexCfs, executionController))
         {
             while( iter.hasNext())
             {
diff --git a/test/unit/org/apache/cassandra/index/internal/CustomCassandraIndex.java b/test/unit/org/apache/cassandra/index/internal/CustomCassandraIndex.java
index 2124abe..1173801 100644
--- a/test/unit/org/apache/cassandra/index/internal/CustomCassandraIndex.java
+++ b/test/unit/org/apache/cassandra/index/internal/CustomCassandraIndex.java
@@ -28,6 +28,7 @@
 import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
 
+import org.apache.cassandra.index.TargetParser;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -60,7 +61,6 @@
 
 import static org.apache.cassandra.index.internal.CassandraIndex.getFunctions;
 import static org.apache.cassandra.index.internal.CassandraIndex.indexCfsMetadata;
-import static org.apache.cassandra.index.internal.CassandraIndex.parseTarget;
 
 /**
  * Clone of KeysIndex used in CassandraIndexTest#testCustomIndexWithCFS to verify
@@ -159,7 +159,7 @@
     private void setMetadata(IndexMetadata indexDef)
     {
         metadata = indexDef;
-        Pair<ColumnDefinition, IndexTarget.Type> target = parseTarget(baseCfs.metadata, indexDef);
+        Pair<ColumnDefinition, IndexTarget.Type> target = TargetParser.parse(baseCfs.metadata, indexDef);
         functions = getFunctions(indexDef, target);
         CFMetaData cfm = indexCfsMetadata(baseCfs.metadata, indexDef);
         indexCfs = ColumnFamilyStore.createColumnFamilyStore(baseCfs.keyspace,
@@ -376,7 +376,7 @@
                 insert(key.getKey(),
                        clustering,
                        cell,
-                       LivenessInfo.create(cell.timestamp(), cell.ttl(), cell.localDeletionTime()),
+                       LivenessInfo.withExpirationTime(cell.timestamp(), cell.ttl(), cell.localDeletionTime()),
                        opGroup);
             }
 
@@ -424,7 +424,7 @@
                         }
                     }
                 }
-                return LivenessInfo.create(baseCfs.metadata, timestamp, ttl, nowInSec);
+                return LivenessInfo.create(timestamp, ttl, nowInSec);
             }
         };
     }
@@ -640,9 +640,9 @@
                         metadata.name,
                         getSSTableNames(sstables));
 
-            SecondaryIndexBuilder builder = new SecondaryIndexBuilder(baseCfs,
-                                                                      Collections.singleton(this),
-                                                                      new ReducingKeyIterator(sstables));
+            SecondaryIndexBuilder builder = new CollatedViewIndexBuilder(baseCfs,
+                                                                         Collections.singleton(this),
+                                                                         new ReducingKeyIterator(sstables));
             Future<?> future = CompactionManager.instance.submitIndexBuild(builder);
             FBUtilities.waitOnFuture(future);
             indexCfs.forceBlockingFlush();
diff --git a/test/unit/org/apache/cassandra/index/sasi/SASICQLTest.java b/test/unit/org/apache/cassandra/index/sasi/SASICQLTest.java
new file mode 100644
index 0000000..695f040
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/SASICQLTest.java
@@ -0,0 +1,351 @@
+/*
+ * 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.index.sasi;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.Sets;
+import org.junit.Test;
+
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.SimpleStatement;
+import com.datastax.driver.core.exceptions.InvalidQueryException;
+import org.junit.Assert;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.service.ClientWarn;
+
+public class SASICQLTest extends CQLTester
+{
+    @Test
+    public void testPaging() throws Throwable
+    {
+        for (boolean forceFlush : new boolean[]{ false, true })
+        {
+            createTable("CREATE TABLE %s (pk int primary key, v int);");
+            createIndex("CREATE CUSTOM INDEX ON %s (v) USING 'org.apache.cassandra.index.sasi.SASIIndex';");
+
+            for (int i = 0; i < 10; i++)
+                execute("INSERT INTO %s (pk, v) VALUES (?, ?);", i, 1);
+
+            flush(forceFlush);
+
+            Session session = sessionNet();
+            SimpleStatement stmt = new SimpleStatement("SELECT * FROM " + KEYSPACE + '.' + currentTable() + " WHERE v = 1");
+            stmt.setFetchSize(5);
+            List<Row> rs = session.execute(stmt).all();
+            Assert.assertEquals(10, rs.size());
+            Assert.assertEquals(Sets.newHashSet(0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
+                                rs.stream().map((i) -> i.getInt("pk")).collect(Collectors.toSet()));
+        }
+    }
+
+    @Test
+    public void testPagingWithClustering() throws Throwable
+    {
+        for (boolean forceFlush : new boolean[]{ false, true })
+        {
+            createTable("CREATE TABLE %s (pk int, ck int, v int, PRIMARY KEY (pk, ck));");
+            createIndex("CREATE CUSTOM INDEX ON %s (v) USING 'org.apache.cassandra.index.sasi.SASIIndex';");
+
+            for (int i = 0; i < 10; i++)
+            {
+                execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?);", i, 1, 1);
+                execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?);", i, 2, 1);
+            }
+
+            flush(forceFlush);
+
+            Session session = sessionNet();
+            SimpleStatement stmt = new SimpleStatement("SELECT * FROM " + KEYSPACE + '.' + currentTable() + " WHERE v = 1");
+            stmt.setFetchSize(5);
+            List<Row> rs = session.execute(stmt).all();
+            Assert.assertEquals(20, rs.size());
+        }
+    }
+
+    /**
+     * Tests that a client warning is issued on SASI index creation.
+     */
+    @Test
+    public void testClientWarningOnCreate()
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)");
+
+        ClientWarn.instance.captureWarnings();
+        createIndex("CREATE CUSTOM INDEX ON %s (v) USING 'org.apache.cassandra.index.sasi.SASIIndex'");
+        List<String> warnings = ClientWarn.instance.getWarnings();
+
+        Assert.assertNotNull(warnings);
+        Assert.assertEquals(1, warnings.size());
+        Assert.assertEquals(SASIIndex.USAGE_WARNING, warnings.get(0));
+    }
+
+    /**
+     * Tests the configuration flag to disable SASI indexes.
+     */
+    @Test
+    public void testDisableSASIIndexes()
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)");
+
+        boolean enableSASIIndexes = DatabaseDescriptor.getEnableSASIIndexes();
+        try
+        {
+            DatabaseDescriptor.setEnableSASIIndexes(false);
+            createIndex("CREATE CUSTOM INDEX ON %s (v) USING 'org.apache.cassandra.index.sasi.SASIIndex'");
+            Assert.fail("Should not be able to create a SASI index if they are disabled");
+        }
+        catch (RuntimeException e)
+        {
+            Throwable cause = e.getCause();
+            Assert.assertNotNull(cause);
+            Assert.assertTrue(cause instanceof InvalidRequestException);
+            Assert.assertTrue(cause.getMessage().contains("SASI indexes are disabled"));
+        }
+        finally
+        {
+            DatabaseDescriptor.setEnableSASIIndexes(enableSASIIndexes);
+        }
+    }
+
+    /**
+     * Tests query condition '>' on string columns with is_literal=false.
+     */
+    @Test
+    public void testNonLiteralStringCompare() throws Throwable
+    {
+        for (String mode : new String[]{ "PREFIX", "CONTAINS", "SPARSE"})
+        {
+            for (boolean forceFlush : new boolean[]{ false, true })
+            {
+                try
+                {
+                    createTable("CREATE TABLE %s (pk int primary key, v text);");
+                    createIndex(String.format("CREATE CUSTOM INDEX ON %%s (v) USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = {'is_literal': 'false', 'mode': '%s'};", mode));
+
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 0, "a");
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 1, "abc");
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 2, "ac");
+
+                    flush(forceFlush);
+
+                    Session session = sessionNet();
+                    SimpleStatement stmt = new SimpleStatement("SELECT * FROM " + KEYSPACE + '.' + currentTable() + " WHERE v = 'ab'");
+                    stmt.setFetchSize(5);
+                    List<Row> rs = session.execute(stmt).all();
+                    Assert.assertEquals(0, rs.size());
+
+                    try
+                        {
+                        sessionNet();
+                        stmt = new SimpleStatement("SELECT * FROM " + KEYSPACE + '.' + currentTable() + " WHERE v > 'ab'");
+                        stmt.setFetchSize(5);
+                        rs = session.execute(stmt).all();
+                        Assert.assertFalse("CONTAINS mode on non-literal string type should not support RANGE operators", "CONTAINS".equals(mode));
+                        Assert.assertEquals(2, rs.size());
+                        Assert.assertEquals(1, rs.get(0).getInt("pk"));
+                    }
+                    catch (InvalidQueryException ex)
+                    {
+                        if (!"CONTAINS".equals(mode))
+                            throw ex;
+                    }
+                }
+                catch (Throwable th)
+                {
+                    throw new AssertionError(String.format("Failure with mode:%s and flush:%s ", mode, forceFlush), th);
+                }
+
+            }
+        }
+    }
+
+    /**
+     * Tests query condition '>' on string columns with is_literal=true (default).
+     */
+    @Test
+    public void testStringCompare() throws Throwable
+    {
+        for (String mode : new String[]{ "PREFIX", "CONTAINS"})
+        {
+            for (boolean forceFlush : new boolean[]{ false, true })
+            {
+                try
+                {
+                    createTable("CREATE TABLE %s (pk int primary key, v text);");
+                    createIndex(String.format("CREATE CUSTOM INDEX ON %%s (v) USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = {'mode': '%s'};", mode));
+
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 0, "a");
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 1, "abc");
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 2, "ac");
+
+                    flush(forceFlush);
+
+                    Session session = sessionNet();
+                    SimpleStatement stmt = new SimpleStatement("SELECT * FROM " + KEYSPACE + '.' + currentTable() + " WHERE v = 'ab'");
+                    stmt.setFetchSize(5);
+                    List<Row> rs = session.execute(stmt).all();
+                    Assert.assertEquals(0, rs.size());
+
+                    try
+                    {
+                        session = sessionNet();
+                        stmt = new SimpleStatement("SELECT * FROM " + KEYSPACE + '.' + currentTable() + " WHERE v > 'ab'");
+                        stmt.setFetchSize(5);
+                        rs = session.execute(stmt).all();
+                        throw new AssertionError("literal string type should not support RANGE operators");
+                    }
+                    catch (InvalidQueryException ex)
+                    {}
+                }
+                catch (Throwable th)
+                {
+                    throw new AssertionError(String.format("Failure with mode:%s and flush:%s ", mode, forceFlush), th);
+                }
+
+            }
+        }
+    }
+
+    /**
+     * Tests query condition like_prefix on string columns.
+     */
+    @Test
+    public void testStringLikePrefix() throws Throwable
+    {
+        for (String mode : new String[]{ "PREFIX", "CONTAINS"})
+        {
+            for (boolean forceFlush : new boolean[]{ false, true })
+            {
+                try
+                {
+                    createTable("CREATE TABLE %s (pk int primary key, v text);");
+                    createIndex(String.format("CREATE CUSTOM INDEX ON %%s (v) USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = {'mode': '%s'};", mode));
+
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 0, "a");
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 1, "abc");
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 2, "ac");
+
+                    flush(forceFlush);
+
+                    Session session = sessionNet();
+                    SimpleStatement stmt = new SimpleStatement("SELECT * FROM " + KEYSPACE + '.' + currentTable() + " WHERE v LIKE 'ab%'");
+                    stmt.setFetchSize(5);
+                    List<Row> rs = session.execute(stmt).all();
+                    Assert.assertEquals(1, rs.size());
+                    Assert.assertEquals(1, rs.get(0).getInt("pk"));
+                }
+                catch (Throwable th)
+                {
+                    throw new AssertionError(String.format("Failure with mode:%s and flush:%s ", mode, forceFlush), th);
+                }
+
+            }
+        }
+    }
+
+    /**
+     * Tests query condition '>' on blob columns.
+     */
+    @Test
+    public void testBlobCompare() throws Throwable
+    {
+        for (String mode : new String[]{ "PREFIX", "CONTAINS", "SPARSE"})
+        {
+            for (boolean forceFlush : new boolean[]{ false, true })
+            {
+                try
+                {
+                    createTable("CREATE TABLE %s (pk int primary key, v blob);");
+                    createIndex(String.format("CREATE CUSTOM INDEX ON %%s (v) USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = {'mode': '%s'};", mode));
+
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 0, 0x1234);
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 1, 0x12345678);
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 2, 0x12350000);
+
+                    flush(forceFlush);
+
+                    Session session = sessionNet();
+                    SimpleStatement stmt = new SimpleStatement("SELECT * FROM " + KEYSPACE + '.' + currentTable() + " WHERE v > 0x1234");
+                    stmt.setFetchSize(5);
+                    List<Row> rs = session.execute(stmt).all();
+                    Assert.assertFalse("CONTAINS mode on non-literal blob type should not support RANGE operators", "CONTAINS".equals(mode));
+                    Assert.assertEquals(2, rs.size());
+                    Assert.assertEquals(1, rs.get(0).getInt("pk"));
+                }
+                catch (InvalidQueryException ex)
+                {
+                    if (!"CONTAINS".equals(mode))
+                        throw ex;
+                }
+                catch (Throwable th)
+                {
+                    throw new AssertionError(String.format("Failure with mode:%s and flush:%s ", mode, forceFlush), th);
+                }
+
+            }
+        }
+    }
+
+    @Test
+    public void testIntCompare() throws Throwable
+    {
+        for (String mode : new String[]{ "PREFIX", "CONTAINS", "SPARSE"})
+        {
+            for (boolean forceFlush : new boolean[]{ false, true })
+            {
+                try
+                {
+                    createTable("CREATE TABLE %s (pk int primary key, v int);");
+
+                    createIndex(String.format("CREATE CUSTOM INDEX ON %%s (v) USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = {'mode': '%s'};", mode));
+
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 0, 100);
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 1, 200);
+                    execute("INSERT INTO %s (pk, v) VALUES (?, ?);", 2, 300);
+
+                    flush(forceFlush);
+
+                    Session session = sessionNet();
+                    SimpleStatement stmt = new SimpleStatement("SELECT * FROM " + KEYSPACE + '.' + currentTable() + " WHERE v > 200");
+                    stmt.setFetchSize(5);
+                    List<Row> rs = session.execute(stmt).all();
+                    Assert.assertFalse("CONTAINS mode on non-literal int type should not support RANGE operators", "CONTAINS".equals(mode));
+                    Assert.assertEquals(1, rs.size());
+                    Assert.assertEquals(2, rs.get(0).getInt("pk"));
+                }
+                catch (InvalidQueryException ex)
+                {
+                    if (!"CONTAINS".equals(mode))
+                        throw ex;
+                }
+                catch (Throwable th)
+                {
+                    throw new AssertionError(String.format("Failure with mode:%s and flush:%s ", mode, forceFlush), th);
+                }
+
+            }
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/SASIIndexTest.java b/test/unit/org/apache/cassandra/index/sasi/SASIIndexTest.java
new file mode 100644
index 0000000..5fbcd8f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/SASIIndexTest.java
@@ -0,0 +1,2856 @@
+/*
+ * 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.index.sasi;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.*;
+import org.apache.cassandra.cql3.Term;
+import org.apache.cassandra.cql3.statements.IndexTarget;
+import org.apache.cassandra.cql3.statements.SelectStatement;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.filter.DataLimits;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.db.lifecycle.SSTableSet;
+import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
+import org.apache.cassandra.index.sasi.analyzer.DelimiterAnalyzer;
+import org.apache.cassandra.index.sasi.analyzer.NoOpAnalyzer;
+import org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer;
+import org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer;
+import org.apache.cassandra.index.sasi.conf.ColumnIndex;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.exceptions.TimeQuotaExceededException;
+import org.apache.cassandra.index.sasi.memory.IndexMemtable;
+import org.apache.cassandra.index.sasi.plan.QueryController;
+import org.apache.cassandra.index.sasi.plan.QueryPlan;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.io.sstable.Component;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.IndexSummaryManager;
+import org.apache.cassandra.io.sstable.SSTable;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.Tables;
+import org.apache.cassandra.serializers.MarshalException;
+import org.apache.cassandra.serializers.TypeSerializer;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.MigrationManager;
+import org.apache.cassandra.service.QueryState;
+import org.apache.cassandra.thrift.CqlRow;
+import org.apache.cassandra.transport.messages.ResultMessage;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.Pair;
+import org.assertj.core.api.Assertions;
+
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.Uninterruptibles;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.junit.*;
+
+public class SASIIndexTest
+{
+    private static final IPartitioner PARTITIONER;
+
+    static {
+        System.setProperty("cassandra.config", "cassandra-murmur.yaml");
+        PARTITIONER = Murmur3Partitioner.instance;
+    }
+
+    private static final String KS_NAME = "sasi";
+    private static final String CF_NAME = "test_cf";
+    private static final String CLUSTERING_CF_NAME_1 = "clustering_test_cf_1";
+    private static final String CLUSTERING_CF_NAME_2 = "clustering_test_cf_2";
+    private static final String STATIC_CF_NAME = "static_sasi_test_cf";
+    private static final String FTS_CF_NAME = "full_text_search_sasi_test_cf";
+
+    private long timestamp;
+
+    @BeforeClass
+    public static void loadSchema() throws ConfigurationException
+    {
+        SchemaLoader.loadSchema();
+        MigrationManager.announceNewKeyspace(KeyspaceMetadata.create(KS_NAME,
+                                                                     KeyspaceParams.simpleTransient(1),
+                                                                     Tables.of(SchemaLoader.sasiCFMD(KS_NAME, CF_NAME),
+                                                                               SchemaLoader.clusteringSASICFMD(KS_NAME, CLUSTERING_CF_NAME_1),
+                                                                               SchemaLoader.clusteringSASICFMD(KS_NAME, CLUSTERING_CF_NAME_2, "location"),
+                                                                               SchemaLoader.staticSASICFMD(KS_NAME, STATIC_CF_NAME),
+                                                                               SchemaLoader.fullTextSearchSASICFMD(KS_NAME, FTS_CF_NAME))));
+        Keyspace ks = Keyspace.open(KS_NAME);
+        for (ColumnFamilyStore store : ks.getColumnFamilyStores())
+        {
+            store.disableAutoCompaction();
+        }
+    }
+
+    @Before
+    public void cleanUp()
+    {
+        timestamp = 0;
+        cleanupData();
+    }
+
+    @Test
+    public void testSASIComponentsAddedToSnapshot() throws Throwable
+    {
+        String snapshotName = "sasi_test";
+        Map<String, Pair<String, Integer>> data = new HashMap<>();
+        Random r = new Random();
+
+        for (int i = 0; i < 100; i++)
+            data.put(UUID.randomUUID().toString(), Pair.create(UUID.randomUUID().toString(), r.nextInt()));
+
+        ColumnFamilyStore store = loadData(data, true);
+
+        Set<SSTableReader> ssTableReaders = store.getLiveSSTables();
+        Set<Component> sasiComponents = new HashSet<>();
+
+        for (Index index : store.indexManager.listIndexes())
+            if (index instanceof SASIIndex)
+                sasiComponents.add(((SASIIndex) index).getIndex().getComponent());
+
+        Assert.assertFalse(sasiComponents.isEmpty());
+
+        try
+        {
+            store.snapshot(snapshotName);
+            // Compact to make true snapshot size != 0
+            store.forceMajorCompaction();
+            LifecycleTransaction.waitForDeletions();
+
+            FileReader reader = new FileReader(store.getDirectories().getSnapshotManifestFile(snapshotName));
+            JSONObject manifest = (JSONObject) new JSONParser().parse(reader);
+            JSONArray files = (JSONArray) manifest.get("files");
+
+            Assert.assertFalse(ssTableReaders.isEmpty());
+            Assert.assertFalse(files.isEmpty());
+            Assert.assertEquals(ssTableReaders.size(), files.size());
+
+            Map<Descriptor, Set<Component>> snapshotSSTables = store.getDirectories()
+                                                                    .sstableLister(Directories.OnTxnErr.IGNORE)
+                                                                    .snapshots(snapshotName)
+                                                                    .list();
+
+            long indexSize = 0;
+            long tableSize = 0;
+
+            for (SSTableReader sstable : ssTableReaders)
+            {
+                File snapshotDirectory = Directories.getSnapshotDirectory(sstable.descriptor, snapshotName);
+                Descriptor snapshotSSTable = new Descriptor(snapshotDirectory,
+                                                            sstable.getKeyspaceName(),
+                                                            sstable.getColumnFamilyName(),
+                                                            sstable.descriptor.generation,
+                                                            sstable.descriptor.formatType);
+
+                Set<Component> components = snapshotSSTables.get(snapshotSSTable);
+
+                Assert.assertNotNull(components);
+                Assert.assertTrue(components.containsAll(sasiComponents));
+
+                for (Component c : components)
+                {
+                    long componentSize = Files.size(Paths.get(snapshotSSTable.filenameFor(c)));
+                    if (Component.Type.fromRepresentation(c.name) == Component.Type.SECONDARY_INDEX)
+                        indexSize += componentSize;
+                    else
+                        tableSize += componentSize;
+                }
+            }
+            
+            Map<String, Pair<Long, Long>> details = store.getSnapshotDetails();
+
+            // check that SASI components are included in the computation of snapshot size
+            Assert.assertEquals(tableSize + indexSize, (long) details.get(snapshotName).right);
+        }
+        finally
+        {
+            store.clearSnapshot(snapshotName);
+        }
+    }
+
+    @Test
+    public void testSingleExpressionQueries() throws Exception
+    {
+        testSingleExpressionQueries(false);
+        cleanupData();
+        testSingleExpressionQueries(true);
+    }
+
+    private void testSingleExpressionQueries(boolean forceFlush)
+    {
+        Map<String, Pair<String, Integer>> data = new HashMap<String, Pair<String, Integer>>()
+        {{
+            put("key1", Pair.create("Pavel", 14));
+            put("key2", Pair.create("Pavel", 26));
+            put("key3", Pair.create("Pavel", 27));
+            put("key4", Pair.create("Jason", 27));
+        }};
+
+        ColumnFamilyStore store = loadData(data, forceFlush);
+
+        final ByteBuffer firstName = UTF8Type.instance.decompose("first_name");
+        final ByteBuffer age = UTF8Type.instance.decompose("age");
+
+        Set<String> rows;
+
+        rows = getIndexed(store, 10, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRows(rows, "key1", "key2", "key3", "key4");
+
+        rows = getIndexed(store, 10, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("av")));
+        assertRows(rows, "key1", "key2", "key3");
+
+        rows = getIndexed(store, 10, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("as")));
+        assertRows(rows, "key4");
+
+        rows = getIndexed(store, 10, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("aw")));
+        assertRowsSize(rows, 0);
+
+        rows = getIndexed(store, 10, buildExpression(firstName, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("avel")));
+        assertRows(rows, "key1", "key2", "key3");
+
+        rows = getIndexed(store, 10, buildExpression(firstName, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("n")));
+        assertRows(rows, "key4");
+
+        rows = getIndexed(store, 10, buildExpression(age, Operator.EQ, Int32Type.instance.decompose(27)));
+        assertRows(rows, "key3", "key4");
+
+        rows = getIndexed(store, 10, buildExpression(age, Operator.EQ, Int32Type.instance.decompose(26)));
+        assertRows(rows, "key2");
+
+        rows = getIndexed(store, 10, buildExpression(age, Operator.EQ, Int32Type.instance.decompose(13)));
+        assertRowsSize(rows, 0);
+    }
+
+    @Test
+    public void testEmptyTokenizedResults()
+    {
+        testEmptyTokenizedResults(false);
+        cleanupData();
+        testEmptyTokenizedResults(true);
+    }
+
+    private void testEmptyTokenizedResults(boolean forceFlush)
+    {
+        Map<String, Pair<String, Integer>> data = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key1", Pair.create("  ", 14));
+        }};
+
+        ColumnFamilyStore store = loadData(data, forceFlush);
+
+        Set<String> rows= getIndexed(store, 10, buildExpression(UTF8Type.instance.decompose("first_name"), Operator.LIKE_MATCHES, UTF8Type.instance.decompose("doesntmatter")));
+        assertRows(rows);
+    }
+
+    @Test
+    public void testMultiExpressionQueries()
+    {
+        testMultiExpressionQueries(false);
+        cleanupData();
+        testMultiExpressionQueries(true);
+    }
+
+    public void testMultiExpressionQueries(boolean forceFlush)
+    {
+        Map<String, Pair<String, Integer>> data = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key1", Pair.create("Pavel", 14));
+                put("key2", Pair.create("Pavel", 26));
+                put("key3", Pair.create("Pavel", 27));
+                put("key4", Pair.create("Jason", 27));
+        }};
+
+        ColumnFamilyStore store = loadData(data, forceFlush);
+
+        final ByteBuffer firstName = UTF8Type.instance.decompose("first_name");
+        final ByteBuffer age = UTF8Type.instance.decompose("age");
+
+        Set<String> rows;
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.GT, Int32Type.instance.decompose(14)));
+        assertRows(rows, "key2", "key3", "key4");
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.LT, Int32Type.instance.decompose(27)));
+        assertRows(rows, "key1", "key2");
+
+        rows = getIndexed(store, 10,
+                         buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                         buildExpression(age, Operator.GT, Int32Type.instance.decompose(14)),
+                         buildExpression(age, Operator.LT, Int32Type.instance.decompose(27)));
+        assertRows(rows, "key2");
+
+        rows = getIndexed(store, 10,
+                         buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                         buildExpression(age, Operator.GT, Int32Type.instance.decompose(12)));
+        assertRows(rows, "key1", "key2", "key3", "key4");
+
+        rows = getIndexed(store, 10,
+                         buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                         buildExpression(age, Operator.GTE, Int32Type.instance.decompose(13)));
+        assertRows(rows, "key1", "key2", "key3", "key4");
+
+        rows = getIndexed(store, 10,
+                         buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                         buildExpression(age, Operator.GTE, Int32Type.instance.decompose(16)));
+        assertRows(rows, "key2", "key3", "key4");
+
+
+        rows = getIndexed(store, 10,
+                         buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                         buildExpression(age, Operator.LT, Int32Type.instance.decompose(30)));
+        assertRows(rows, "key1", "key2", "key3", "key4");
+
+        rows = getIndexed(store, 10,
+                         buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                         buildExpression(age, Operator.LTE, Int32Type.instance.decompose(29)));
+        assertRows(rows, "key1", "key2", "key3", "key4");
+
+        rows = getIndexed(store, 10,
+                         buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                         buildExpression(age, Operator.LTE, Int32Type.instance.decompose(25)));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(firstName, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("avel")),
+                                     buildExpression(age, Operator.LTE, Int32Type.instance.decompose(25)));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(firstName, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("n")),
+                                     buildExpression(age, Operator.LTE, Int32Type.instance.decompose(25)));
+        Assert.assertTrue(rows.isEmpty());
+
+    }
+
+    @Test
+    public void testCrossSSTableQueries()
+    {
+        testCrossSSTableQueries(false);
+        cleanupData();
+        testCrossSSTableQueries(true);
+
+    }
+
+    private void testCrossSSTableQueries(boolean forceFlush)
+    {
+        Map<String, Pair<String, Integer>> part1 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key0", Pair.create("Maxie", 43));
+                put("key1", Pair.create("Chelsie", 33));
+                put("key2", Pair.create("Josephine", 43));
+                put("key3", Pair.create("Shanna", 27));
+                put("key4", Pair.create("Amiya", 36));
+            }};
+
+        loadData(part1, forceFlush); // first sstable
+
+        Map<String, Pair<String, Integer>> part2 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key5", Pair.create("Americo", 20));
+                put("key6", Pair.create("Fiona", 39));
+                put("key7", Pair.create("Francis", 41));
+                put("key8", Pair.create("Charley", 21));
+                put("key9", Pair.create("Amely", 40));
+            }};
+
+        loadData(part2, forceFlush);
+
+        Map<String, Pair<String, Integer>> part3 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key10", Pair.create("Eddie", 42));
+                put("key11", Pair.create("Oswaldo", 35));
+                put("key12", Pair.create("Susana", 35));
+                put("key13", Pair.create("Alivia", 42));
+                put("key14", Pair.create("Demario", 28));
+            }};
+
+        ColumnFamilyStore store = loadData(part3, forceFlush);
+
+        final ByteBuffer firstName = UTF8Type.instance.decompose("first_name");
+        final ByteBuffer age = UTF8Type.instance.decompose("age");
+
+        Set<String> rows;
+        rows = getIndexed(store, 10, buildExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("Fiona")),
+                                     buildExpression(age, Operator.LT, Int32Type.instance.decompose(40)));
+
+        assertRows(rows, "key6");
+
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+
+        assertRows(rows, "key0", "key11", "key12", "key13", "key14", "key3", "key4", "key6", "key7", "key8" );
+
+        rows = getIndexed(store, 5,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+
+        assertRowsSize(rows, 5);
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.GTE, Int32Type.instance.decompose(35)));
+
+        assertRows(rows, "key0", "key11", "key12", "key13", "key4", "key6", "key7");
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.LT, Int32Type.instance.decompose(32)));
+
+        assertRows(rows, "key14", "key3", "key8");
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.GT, Int32Type.instance.decompose(27)),
+                          buildExpression(age, Operator.LT, Int32Type.instance.decompose(32)));
+
+        assertRows(rows, "key14");
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.GT, Int32Type.instance.decompose(10)));
+
+        assertRowsSize(rows, 10);
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.LTE, Int32Type.instance.decompose(50)));
+
+        assertRowsSize(rows, 10);
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("ie")),
+                          buildExpression(age, Operator.LT, Int32Type.instance.decompose(43)));
+
+        assertRows(rows, "key1", "key10");
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("a")));
+
+        assertRows(rows, "key12", "key13", "key3", "key4", "key6");
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.LT, Int32Type.instance.decompose(33)));
+
+        assertRows(rows, "key3");
+    }
+
+    @Test
+    public void testQueriesThatShouldBeTokenized()
+    {
+        testQueriesThatShouldBeTokenized(false);
+        cleanupData();
+        testQueriesThatShouldBeTokenized(true);
+    }
+
+    private void testQueriesThatShouldBeTokenized(boolean forceFlush)
+    {
+        Map<String, Pair<String, Integer>> part1 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key0", Pair.create("If you can dream it, you can do it.", 43));
+                put("key1", Pair.create("What you get by achieving your goals is not " +
+                        "as important as what you become by achieving your goals, do it.", 33));
+                put("key2", Pair.create("Keep your face always toward the sunshine " +
+                        "- and shadows will fall behind you.", 43));
+                put("key3", Pair.create("We can't help everyone, but everyone can " +
+                        "help someone.", 27));
+            }};
+
+        ColumnFamilyStore store = loadData(part1, forceFlush);
+
+        final ByteBuffer firstName = UTF8Type.instance.decompose("first_name");
+        final ByteBuffer age = UTF8Type.instance.decompose("age");
+
+        Set<String> rows = getIndexed(store, 10,
+                buildExpression(firstName, Operator.LIKE_CONTAINS,
+                        UTF8Type.instance.decompose("What you get by achieving your goals")),
+                buildExpression(age, Operator.GT, Int32Type.instance.decompose(32)));
+
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10,
+                buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("do it.")));
+
+        assertRows(rows, "key0", "key1");
+    }
+
+    @Test
+    public void testPrefixSearchWithContainsMode()
+    {
+        testPrefixSearchWithContainsMode(false);
+        cleanupData();
+        testPrefixSearchWithContainsMode(true);
+    }
+
+    private void testPrefixSearchWithContainsMode(boolean forceFlush)
+    {
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(FTS_CF_NAME);
+
+        executeCQL(FTS_CF_NAME, "INSERT INTO %s.%s (song_id, title, artist) VALUES(?, ?, ?)", UUID.fromString("1a4abbcd-b5de-4c69-a578-31231e01ff09"), "Poker Face", "Lady Gaga");
+        executeCQL(FTS_CF_NAME, "INSERT INTO %s.%s (song_id, title, artist) VALUES(?, ?, ?)", UUID.fromString("9472a394-359b-4a06-b1d5-b6afce590598"), "Forgetting the Way Home", "Our Lady of Bells");
+        executeCQL(FTS_CF_NAME, "INSERT INTO %s.%s (song_id, title, artist) VALUES(?, ?, ?)", UUID.fromString("4f8dc18e-54e6-4e16-b507-c5324b61523b"), "Zamki na piasku", "Lady Pank");
+        executeCQL(FTS_CF_NAME, "INSERT INTO %s.%s (song_id, title, artist) VALUES(?, ?, ?)", UUID.fromString("eaf294fa-bad5-49d4-8f08-35ba3636a706"), "Koncertowa", "Lady Pank");
+
+
+        if (forceFlush)
+            store.forceBlockingFlush();
+
+        final UntypedResultSet results = executeCQL(FTS_CF_NAME, "SELECT * FROM %s.%s WHERE artist LIKE 'lady%%'");
+        Assert.assertNotNull(results);
+        Assert.assertEquals(3, results.size());
+    }
+
+    @Test
+    public void testMultiExpressionQueriesWhereRowSplitBetweenSSTables()
+    {
+        testMultiExpressionQueriesWhereRowSplitBetweenSSTables(false);
+        cleanupData();
+        testMultiExpressionQueriesWhereRowSplitBetweenSSTables(true);
+    }
+
+    private void testMultiExpressionQueriesWhereRowSplitBetweenSSTables(boolean forceFlush)
+    {
+        Map<String, Pair<String, Integer>> part1 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key0", Pair.create("Maxie", -1));
+                put("key1", Pair.create("Chelsie", 33));
+                put("key2", Pair.create(null, 43));
+                put("key3", Pair.create("Shanna", 27));
+                put("key4", Pair.create("Amiya", 36));
+        }};
+
+        loadData(part1, forceFlush); // first sstable
+
+        Map<String, Pair<String, Integer>> part2 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key5", Pair.create("Americo", 20));
+                put("key6", Pair.create("Fiona", 39));
+                put("key7", Pair.create("Francis", 41));
+                put("key8", Pair.create("Charley", 21));
+                put("key9", Pair.create("Amely", 40));
+                put("key14", Pair.create(null, 28));
+        }};
+
+        loadData(part2, forceFlush);
+
+        Map<String, Pair<String, Integer>> part3 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key0", Pair.create(null, 43));
+                put("key10", Pair.create("Eddie", 42));
+                put("key11", Pair.create("Oswaldo", 35));
+                put("key12", Pair.create("Susana", 35));
+                put("key13", Pair.create("Alivia", 42));
+                put("key14", Pair.create("Demario", -1));
+                put("key2", Pair.create("Josephine", -1));
+        }};
+
+        ColumnFamilyStore store = loadData(part3, forceFlush);
+
+        final ByteBuffer firstName = UTF8Type.instance.decompose("first_name");
+        final ByteBuffer age = UTF8Type.instance.decompose("age");
+
+        Set<String> rows = getIndexed(store, 10,
+                                      buildExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("Fiona")),
+                                      buildExpression(age, Operator.LT, Int32Type.instance.decompose(40)));
+
+        assertRows(rows, "key6");
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+
+        assertRows(rows, "key0", "key11", "key12", "key13", "key14", "key3", "key4", "key6", "key7", "key8");
+
+        rows = getIndexed(store, 5,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+
+        assertRowsSize(rows, 5);
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.GTE, Int32Type.instance.decompose(35)));
+
+        assertRows(rows, "key0", "key11", "key12", "key13", "key4", "key6", "key7");
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.LT, Int32Type.instance.decompose(32)));
+
+        assertRows(rows, "key14", "key3", "key8");
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.GT, Int32Type.instance.decompose(27)),
+                          buildExpression(age, Operator.LT, Int32Type.instance.decompose(32)));
+
+        assertRows(rows, "key14");
+
+        Map<String, Pair<String, Integer>> part4 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key12", Pair.create(null, 12));
+                put("key14", Pair.create("Demario", 42));
+                put("key2", Pair.create("Frank", -1));
+        }};
+
+        store = loadData(part4, forceFlush);
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("Susana")),
+                          buildExpression(age, Operator.LTE, Int32Type.instance.decompose(13)),
+                          buildExpression(age, Operator.GT, Int32Type.instance.decompose(10)));
+        assertRows(rows, "key12");
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("Demario")),
+                          buildExpression(age, Operator.LTE, Int32Type.instance.decompose(30)));
+        assertRowsSize(rows, 0);
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("Josephine")));
+        assertRowsSize(rows, 0);
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.GT, Int32Type.instance.decompose(10)));
+
+        assertRowsSize(rows, 10);
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                          buildExpression(age, Operator.LTE, Int32Type.instance.decompose(50)));
+
+        assertRowsSize(rows, 10);
+
+        rows = getIndexed(store, 10,
+                          buildExpression(firstName, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("ie")),
+                          buildExpression(age, Operator.LTE, Int32Type.instance.decompose(43)));
+
+        assertRows(rows, "key0", "key1", "key10");
+    }
+
+    @Test
+    public void testPagination()
+    {
+        testPagination(false);
+        cleanupData();
+        testPagination(true);
+    }
+
+    private void testPagination(boolean forceFlush)
+    {
+        // split data into 3 distinct SSTables to test paging with overlapping token intervals.
+
+        Map<String, Pair<String, Integer>> part1 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key01", Pair.create("Ali", 33));
+                put("key02", Pair.create("Jeremy", 41));
+                put("key03", Pair.create("Elvera", 22));
+                put("key04", Pair.create("Bailey", 45));
+                put("key05", Pair.create("Emerson", 32));
+                put("key06", Pair.create("Kadin", 38));
+                put("key07", Pair.create("Maggie", 36));
+                put("key08", Pair.create("Kailey", 36));
+                put("key09", Pair.create("Armand", 21));
+                put("key10", Pair.create("Arnold", 35));
+        }};
+
+        Map<String, Pair<String, Integer>> part2 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key11", Pair.create("Ken", 38));
+                put("key12", Pair.create("Penelope", 43));
+                put("key13", Pair.create("Wyatt", 34));
+                put("key14", Pair.create("Johnpaul", 34));
+                put("key15", Pair.create("Trycia", 43));
+                put("key16", Pair.create("Aida", 21));
+                put("key17", Pair.create("Devon", 42));
+        }};
+
+        Map<String, Pair<String, Integer>> part3 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key18", Pair.create("Christina", 20));
+                put("key19", Pair.create("Rick", 19));
+                put("key20", Pair.create("Fannie", 22));
+                put("key21", Pair.create("Keegan", 29));
+                put("key22", Pair.create("Ignatius", 36));
+                put("key23", Pair.create("Ellis", 26));
+                put("key24", Pair.create("Annamarie", 29));
+                put("key25", Pair.create("Tianna", 31));
+                put("key26", Pair.create("Dennis", 32));
+        }};
+
+        ColumnFamilyStore store = loadData(part1, forceFlush);
+
+        loadData(part2, forceFlush);
+        loadData(part3, forceFlush);
+
+        final ByteBuffer firstName = UTF8Type.instance.decompose("first_name");
+        final ByteBuffer age = UTF8Type.instance.decompose("age");
+
+        Set<DecoratedKey> uniqueKeys = getPaged(store, 4,
+                buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                buildExpression(age, Operator.GTE, Int32Type.instance.decompose(21)));
+
+
+        List<String> expected = new ArrayList<String>()
+        {{
+                add("key25");
+                add("key20");
+                add("key13");
+                add("key22");
+                add("key09");
+                add("key14");
+                add("key16");
+                add("key24");
+                add("key03");
+                add("key04");
+                add("key08");
+                add("key07");
+                add("key15");
+                add("key06");
+                add("key21");
+        }};
+
+        Assert.assertEquals(expected, convert(uniqueKeys));
+
+        // now let's test a single equals condition
+
+        uniqueKeys = getPaged(store, 4, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+
+        expected = new ArrayList<String>()
+        {{
+                add("key25");
+                add("key20");
+                add("key13");
+                add("key22");
+                add("key09");
+                add("key14");
+                add("key16");
+                add("key24");
+                add("key03");
+                add("key04");
+                add("key18");
+                add("key08");
+                add("key07");
+                add("key15");
+                add("key06");
+                add("key21");
+        }};
+
+        Assert.assertEquals(expected, convert(uniqueKeys));
+
+        // now let's test something which is smaller than a single page
+        uniqueKeys = getPaged(store, 4,
+                              buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                              buildExpression(age, Operator.EQ, Int32Type.instance.decompose(36)));
+
+        expected = new ArrayList<String>()
+        {{
+                add("key22");
+                add("key08");
+                add("key07");
+        }};
+
+        Assert.assertEquals(expected, convert(uniqueKeys));
+
+        // the same but with the page size of 2 to test minimal pagination windows
+
+        uniqueKeys = getPaged(store, 2,
+                              buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                              buildExpression(age, Operator.EQ, Int32Type.instance.decompose(36)));
+
+        Assert.assertEquals(expected, convert(uniqueKeys));
+
+        // and last but not least, test age range query with pagination
+        uniqueKeys = getPaged(store, 4,
+                buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                buildExpression(age, Operator.GT, Int32Type.instance.decompose(20)),
+                buildExpression(age, Operator.LTE, Int32Type.instance.decompose(36)));
+
+        expected = new ArrayList<String>()
+        {{
+                add("key25");
+                add("key20");
+                add("key13");
+                add("key22");
+                add("key09");
+                add("key14");
+                add("key16");
+                add("key24");
+                add("key03");
+                add("key08");
+                add("key07");
+                add("key21");
+        }};
+
+        Assert.assertEquals(expected, convert(uniqueKeys));
+
+        Set<String> rows;
+
+        rows = executeCQLWithKeys(String.format("SELECT * FROM %s.%s WHERE first_name LIKE '%%a%%' limit 10 ALLOW FILTERING;", KS_NAME, CF_NAME));
+        assertRows(rows, "key03", "key04", "key09", "key13", "key14", "key16", "key20", "key22", "key24", "key25");
+
+        rows = executeCQLWithKeys(String.format("SELECT * FROM %s.%s WHERE first_name LIKE '%%a%%' and token(id) >= token('key14') limit 5 ALLOW FILTERING;", KS_NAME, CF_NAME));
+        assertRows(rows, "key03", "key04", "key14", "key16", "key24");
+
+        rows = executeCQLWithKeys(String.format("SELECT * FROM %s.%s WHERE first_name LIKE '%%a%%' and token(id) >= token('key14') and token(id) <= token('key24') limit 5 ALLOW FILTERING;", KS_NAME, CF_NAME));
+        assertRows(rows, "key14", "key16", "key24");
+
+        rows = executeCQLWithKeys(String.format("SELECT * FROM %s.%s WHERE first_name LIKE '%%a%%' and age > 30 and token(id) >= token('key14') and token(id) <= token('key24') limit 5 ALLOW FILTERING;", KS_NAME, CF_NAME));
+        assertRows(rows, "key14");
+
+        rows = executeCQLWithKeys(String.format("SELECT * FROM %s.%s WHERE first_name like '%%ie' limit 5 ALLOW FILTERING;", KS_NAME, CF_NAME));
+        assertRows(rows, "key07", "key20", "key24");
+
+        rows = executeCQLWithKeys(String.format("SELECT * FROM %s.%s WHERE first_name like '%%ie' AND token(id) > token('key24') limit 5 ALLOW FILTERING;", KS_NAME, CF_NAME));
+        assertRows(rows, "key07", "key24");
+    }
+
+    @Test
+    public void testColumnNamesWithSlashes()
+    {
+        testColumnNamesWithSlashes(false);
+        cleanupData();
+        testColumnNamesWithSlashes(true);
+    }
+
+    private void testColumnNamesWithSlashes(boolean forceFlush)
+    {
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+
+        Mutation rm1 = new Mutation(KS_NAME, decoratedKey(AsciiType.instance.decompose("key1")));
+        rm1.add(PartitionUpdate.singleRowUpdate(store.metadata,
+                                                rm1.key(),
+                                                buildRow(buildCell(store.metadata,
+                                                                   UTF8Type.instance.decompose("/data/output/id"),
+                                                                   AsciiType.instance.decompose("jason"),
+                                                                   1000))));
+
+        Mutation rm2 = new Mutation(KS_NAME, decoratedKey(AsciiType.instance.decompose("key2")));
+        rm2.add(PartitionUpdate.singleRowUpdate(store.metadata,
+                                                rm2.key(),
+                                                buildRow(buildCell(store.metadata,
+                                                                   UTF8Type.instance.decompose("/data/output/id"),
+                                                                   AsciiType.instance.decompose("pavel"),
+                                                                   2000))));
+
+        Mutation rm3 = new Mutation(KS_NAME, decoratedKey(AsciiType.instance.decompose("key3")));
+        rm3.add(PartitionUpdate.singleRowUpdate(store.metadata,
+                                                rm3.key(),
+                                                buildRow(buildCell(store.metadata,
+                                                                   UTF8Type.instance.decompose("/data/output/id"),
+                                                                   AsciiType.instance.decompose("Aleksey"),
+                                                                   3000))));
+
+        rm1.apply();
+        rm2.apply();
+        rm3.apply();
+
+        if (forceFlush)
+            store.forceBlockingFlush();
+
+        final ByteBuffer dataOutputId = UTF8Type.instance.decompose("/data/output/id");
+
+        Set<String> rows = getIndexed(store, 10, buildExpression(dataOutputId, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRows(rows, "key1", "key2");
+
+        rows = getIndexed(store, 10, buildExpression(dataOutputId, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("A")));
+        assertRows(rows, "key3");
+
+        // doesn't really make sense to rebuild index for in-memory data
+        if (!forceFlush)
+            return;
+
+        store.indexManager.invalidateAllIndexesBlocking();
+
+        rows = getIndexed(store, 10, buildExpression(dataOutputId, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        Assert.assertTrue(rows.toString(), rows.isEmpty());
+
+        rows = getIndexed(store, 10, buildExpression(dataOutputId, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("A")));
+        Assert.assertTrue(rows.toString(), rows.isEmpty());
+
+        // now let's trigger index rebuild and check if we got the data back
+        store.indexManager.buildIndexBlocking(store.indexManager.getIndexByName("data_output_id"));
+
+        rows = getIndexed(store, 10, buildExpression(dataOutputId, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRows(rows, "key1", "key2");
+
+        // also let's try to build an index for column which has no data to make sure that doesn't fail
+        store.indexManager.buildIndexBlocking(store.indexManager.getIndexByName("first_name"));
+        store.indexManager.buildIndexBlocking(store.indexManager.getIndexByName("data_output_id"));
+
+        rows = getIndexed(store, 10, buildExpression(dataOutputId, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRows(rows, "key1", "key2");
+
+        rows = getIndexed(store, 10, buildExpression(dataOutputId, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("el")));
+        assertRows(rows, "key2");
+    }
+
+    @Test
+    public void testInvalidate()
+    {
+        testInvalidate(false);
+        cleanupData();
+        testInvalidate(true);
+    }
+
+    private void testInvalidate(boolean forceFlush)
+    {
+        Map<String, Pair<String, Integer>> part1 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key0", Pair.create("Maxie", -1));
+                put("key1", Pair.create("Chelsie", 33));
+                put("key2", Pair.create(null, 43));
+                put("key3", Pair.create("Shanna", 27));
+                put("key4", Pair.create("Amiya", 36));
+        }};
+
+        ColumnFamilyStore store = loadData(part1, forceFlush);
+
+        final ByteBuffer firstName = UTF8Type.instance.decompose("first_name");
+        final ByteBuffer age = UTF8Type.instance.decompose("age");
+
+        Set<String> rows = getIndexed(store, 10, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRows(rows, "key0", "key3", "key4");
+
+        rows = getIndexed(store, 10, buildExpression(age, Operator.EQ, Int32Type.instance.decompose(33)));
+        assertRows(rows, "key1");
+
+        store.indexManager.invalidateAllIndexesBlocking();
+
+        rows = getIndexed(store, 10, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        Assert.assertTrue(rows.toString(), rows.isEmpty());
+
+        rows = getIndexed(store, 10, buildExpression(age, Operator.EQ, Int32Type.instance.decompose(33)));
+        Assert.assertTrue(rows.toString(), rows.isEmpty());
+
+
+        Map<String, Pair<String, Integer>> part2 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key5", Pair.create("Americo", 20));
+                put("key6", Pair.create("Fiona", 39));
+                put("key7", Pair.create("Francis", 41));
+                put("key8", Pair.create("Fred", 21));
+                put("key9", Pair.create("Amely", 40));
+                put("key14", Pair.create("Dino", 28));
+        }};
+
+        loadData(part2, forceFlush);
+
+        rows = getIndexed(store, 10, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRows(rows, "key6", "key7");
+
+        rows = getIndexed(store, 10, buildExpression(age, Operator.EQ, Int32Type.instance.decompose(40)));
+        assertRows(rows, "key9");
+    }
+
+    @Test
+    public void testIndexRedistribution() throws IOException
+    {
+        Map<String, Pair<String, Integer>> part1 = new HashMap<String, Pair<String, Integer>>()
+        {{
+            put("key01", Pair.create("a", 33));
+            put("key02", Pair.create("a", 41));
+        }};
+
+        Map<String, Pair<String, Integer>> part2 = new HashMap<String, Pair<String, Integer>>()
+        {{
+            put("key03", Pair.create("a", 22));
+            put("key04", Pair.create("a", 45));
+        }};
+
+        Map<String, Pair<String, Integer>> part3 = new HashMap<String, Pair<String, Integer>>()
+        {{
+            put("key05", Pair.create("a", 32));
+            put("key06", Pair.create("a", 38));
+        }};
+
+        Map<String, Pair<String, Integer>> part4 = new HashMap<String, Pair<String, Integer>>()
+        {{
+            put("key07", Pair.create("a", 36));
+            put("key08", Pair.create("a", 36));
+        }};
+
+        Map<String, Pair<String, Integer>> part5 = new HashMap<String, Pair<String, Integer>>()
+        {{
+            put("key09", Pair.create("a", 21));
+            put("key10", Pair.create("a", 35));
+        }};
+
+        ColumnFamilyStore store = loadData(part1, true);
+        loadData(part2, true);
+        loadData(part3, true);
+
+        final ByteBuffer firstName = UTF8Type.instance.decompose("first_name");
+
+        Set<String> rows = getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRowsSize(rows, 6);
+
+        loadData(part4, true);
+        rows = getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRowsSize(rows, 8);
+
+        loadData(part5, true);
+
+        int minIndexInterval = store.metadata.params.minIndexInterval;
+        try
+        {
+            redistributeSummaries(10, store, firstName, minIndexInterval * 2);
+            redistributeSummaries(10, store, firstName, minIndexInterval * 4);
+            redistributeSummaries(10, store, firstName, minIndexInterval * 8);
+            redistributeSummaries(10, store, firstName, minIndexInterval * 16);
+        } finally
+        {
+            store.metadata.minIndexInterval(minIndexInterval);
+        }
+    }
+
+    private void redistributeSummaries(int expected, ColumnFamilyStore store, ByteBuffer firstName, int minIndexInterval) throws IOException
+    {
+        store.metadata.minIndexInterval(minIndexInterval);
+        IndexSummaryManager.instance.redistributeSummaries();
+        store.forceBlockingFlush();
+
+        Set<String> rows = store.runWithCompactionsDisabled(() -> getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a"))), true, true);
+        Assert.assertEquals(rows.toString(), expected, rows.size());
+    }
+
+    @Test
+    public void testTruncate()
+    {
+        Map<String, Pair<String, Integer>> part1 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key01", Pair.create("Ali", 33));
+                put("key02", Pair.create("Jeremy", 41));
+                put("key03", Pair.create("Elvera", 22));
+                put("key04", Pair.create("Bailey", 45));
+                put("key05", Pair.create("Emerson", 32));
+                put("key06", Pair.create("Kadin", 38));
+                put("key07", Pair.create("Maggie", 36));
+                put("key08", Pair.create("Kailey", 36));
+                put("key09", Pair.create("Armand", 21));
+                put("key10", Pair.create("Arnold", 35));
+        }};
+
+        Map<String, Pair<String, Integer>> part2 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key11", Pair.create("Ken", 38));
+                put("key12", Pair.create("Penelope", 43));
+                put("key13", Pair.create("Wyatt", 34));
+                put("key14", Pair.create("Johnpaul", 34));
+                put("key15", Pair.create("Trycia", 43));
+                put("key16", Pair.create("Aida", 21));
+                put("key17", Pair.create("Devon", 42));
+        }};
+
+        Map<String, Pair<String, Integer>> part3 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key18", Pair.create("Christina", 20));
+                put("key19", Pair.create("Rick", 19));
+                put("key20", Pair.create("Fannie", 22));
+                put("key21", Pair.create("Keegan", 29));
+                put("key22", Pair.create("Ignatius", 36));
+                put("key23", Pair.create("Ellis", 26));
+                put("key24", Pair.create("Annamarie", 29));
+                put("key25", Pair.create("Tianna", 31));
+                put("key26", Pair.create("Dennis", 32));
+        }};
+
+        ColumnFamilyStore store = loadData(part1, true, 2);
+
+        loadData(part2, true, 4);
+        loadData(part3, true, 6);
+
+        final ByteBuffer firstName = UTF8Type.instance.decompose("first_name");
+
+        Set<String> rows = getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRowsSize(rows, 16);
+
+        // make sure we don't prematurely delete anything
+        store.indexManager.truncateAllIndexesBlocking(1);
+
+        rows = getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRowsSize(rows, 16);
+
+        store.indexManager.truncateAllIndexesBlocking(3);
+
+        rows = getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRowsSize(rows, 10);
+
+        store.indexManager.truncateAllIndexesBlocking(5);
+
+        rows = getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRowsSize(rows, 6);
+
+        store.indexManager.truncateAllIndexesBlocking(7);
+
+        rows = getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRowsSize(rows, 0);
+
+        // add back in some data just to make sure it all still works
+        Map<String, Pair<String, Integer>> part4 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key40", Pair.create("Tianna", 31));
+                put("key41", Pair.create("Dennis", 32));
+        }};
+
+        loadData(part4, true, 8);
+
+        rows = getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRowsSize(rows, 1);
+    }
+
+    @Test
+    public void testConcurrentMemtableReadsAndWrites()
+    {
+        final ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+
+        ExecutorService scheduler = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
+
+        final int writeCount = 10000;
+        final AtomicInteger updates = new AtomicInteger(0);
+
+        for (int i = 0; i < writeCount; i++)
+        {
+            final String key = "key" + i;
+            final String firstName = "first_name#" + i;
+            final String lastName = "last_name#" + i;
+            final long timestamp = 1000 + i;
+
+            scheduler.submit(() -> {
+                try
+                {
+                    newMutation(key, firstName, lastName, 26, timestamp).apply();
+                    Uninterruptibles.sleepUninterruptibly(5, TimeUnit.MILLISECONDS); // back up a bit to do more reads
+                }
+                finally
+                {
+                    updates.incrementAndGet();
+                }
+            });
+        }
+
+        final ByteBuffer firstName = UTF8Type.instance.decompose("first_name");
+        final ByteBuffer age = UTF8Type.instance.decompose("age");
+
+        int previousCount = 0;
+
+        do
+        {
+            // this loop figures out if number of search results monotonically increasing
+            // to make sure that concurrent updates don't interfere with reads, uses first_name and age
+            // indexes to test correctness of both Trie and SkipList ColumnIndex implementations.
+
+            Set<DecoratedKey> rows = getPaged(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                                                          buildExpression(age, Operator.EQ, Int32Type.instance.decompose(26)));
+
+            Assert.assertTrue(previousCount <= rows.size());
+            previousCount = rows.size();
+        }
+        while (updates.get() < writeCount);
+
+        // to make sure that after all of the right are done we can read all "count" worth of rows
+        Set<DecoratedKey> rows = getPaged(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                                                      buildExpression(age, Operator.EQ, Int32Type.instance.decompose(26)));
+
+        Assert.assertEquals(writeCount, rows.size());
+    }
+
+    @Test
+    public void testSameKeyInMemtableAndSSTables()
+    {
+        final ByteBuffer firstName = UTF8Type.instance.decompose("first_name");
+        final ByteBuffer age = UTF8Type.instance.decompose("age");
+
+        Map<String, Pair<String, Integer>> data1 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key1", Pair.create("Pavel", 14));
+                put("key2", Pair.create("Pavel", 26));
+                put("key3", Pair.create("Pavel", 27));
+                put("key4", Pair.create("Jason", 27));
+        }};
+
+        ColumnFamilyStore store = loadData(data1, true);
+
+        Map<String, Pair<String, Integer>> data2 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key1", Pair.create("Pavel", 14));
+                put("key2", Pair.create("Pavel", 27));
+                put("key4", Pair.create("Jason", 28));
+        }};
+
+        loadData(data2, true);
+
+        Map<String, Pair<String, Integer>> data3 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key1", Pair.create("Pavel", 15));
+                put("key4", Pair.create("Jason", 29));
+        }};
+
+        loadData(data3, false);
+
+        Set<String> rows = getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")));
+        assertRows(rows, "key1", "key2", "key3", "key4");
+
+
+        rows = getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                                      buildExpression(age, Operator.EQ, Int32Type.instance.decompose(15)));
+
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                                      buildExpression(age, Operator.EQ, Int32Type.instance.decompose(29)));
+
+        assertRows(rows, "key4");
+
+        rows = getIndexed(store, 100, buildExpression(firstName, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("a")),
+                                      buildExpression(age, Operator.EQ, Int32Type.instance.decompose(27)));
+
+        assertRows(rows, "key2", "key3");
+    }
+
+    @Test
+    public void testUnicodeSupport()
+    {
+        testUnicodeSupport(false);
+        cleanupData();
+        testUnicodeSupport(true);
+    }
+
+    private void testUnicodeSupport(boolean forceFlush)
+    {
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+
+        final ByteBuffer comment = UTF8Type.instance.decompose("comment");
+
+        Mutation rm = new Mutation(KS_NAME, decoratedKey("key1"));
+        update(rm, comment, UTF8Type.instance.decompose("ⓈⓅⒺⒸⒾⒶⓁ ⒞⒣⒜⒭⒮ and normal ones"), 1000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key2"));
+        update(rm, comment, UTF8Type.instance.decompose("龍馭鬱"), 2000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key3"));
+        update(rm, comment, UTF8Type.instance.decompose("インディアナ"), 3000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key4"));
+        update(rm, comment, UTF8Type.instance.decompose("レストラン"), 4000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key5"));
+        update(rm, comment, UTF8Type.instance.decompose("ベンジャミン ウエスト"), 5000);
+        rm.apply();
+
+        if (forceFlush)
+            store.forceBlockingFlush();
+
+        Set<String> rows;
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("ⓈⓅⒺⒸⒾ")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("normal")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("龍")));
+        assertRows(rows, "key2");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("鬱")));
+        assertRows(rows, "key2");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("馭鬱")));
+        assertRows(rows, "key2");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("龍馭鬱")));
+        assertRows(rows, "key2");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("ベンジャミン")));
+        assertRows(rows, "key5");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("レストラ")));
+        assertRows(rows, "key4");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("インディ")));
+        assertRows(rows, "key3");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("ベンジャミ")));
+        assertRows(rows, "key5");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("ン")));
+        assertRows(rows, "key4", "key5");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("レストラン")));
+        assertRows(rows, "key4");
+    }
+
+    @Test
+    public void testUnicodeSuffixModeNoSplits()
+    {
+        testUnicodeSuffixModeNoSplits(false);
+        cleanupData();
+        testUnicodeSuffixModeNoSplits(true);
+    }
+
+    private void testUnicodeSuffixModeNoSplits(boolean forceFlush)
+    {
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+
+        final ByteBuffer comment = UTF8Type.instance.decompose("comment_suffix_split");
+
+        Mutation rm = new Mutation(KS_NAME, decoratedKey("key1"));
+        update(rm, comment, UTF8Type.instance.decompose("龍馭鬱"), 1000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key2"));
+        update(rm, comment, UTF8Type.instance.decompose("インディアナ"), 2000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key3"));
+        update(rm, comment, UTF8Type.instance.decompose("レストラン"), 3000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key4"));
+        update(rm, comment, UTF8Type.instance.decompose("ベンジャミン ウエスト"), 4000);
+        rm.apply();
+
+        if (forceFlush)
+            store.forceBlockingFlush();
+
+        Set<String> rows;
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("龍")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("鬱")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("馭鬱")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("龍馭鬱")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("ベンジャミン")));
+        assertRows(rows, "key4");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("トラン")));
+        assertRows(rows, "key3");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("ディア")));
+        assertRows(rows, "key2");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("ジャミン")));
+        assertRows(rows, "key4");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("ン")));
+        assertRows(rows, "key2", "key3", "key4");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("ン")));
+        assertRows(rows, "key3");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("ベンジャミン ウエスト")));
+        assertRows(rows, "key4");
+    }
+
+    @Test
+    public void testThatTooBigValueIsRejected()
+    {
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+
+        final ByteBuffer comment = UTF8Type.instance.decompose("comment_suffix_split");
+
+        for (int i = 0; i < 10; i++)
+        {
+            byte[] randomBytes = new byte[ThreadLocalRandom.current().nextInt(OnDiskIndexBuilder.MAX_TERM_SIZE, 5 * OnDiskIndexBuilder.MAX_TERM_SIZE)];
+            ThreadLocalRandom.current().nextBytes(randomBytes);
+
+            final ByteBuffer bigValue = UTF8Type.instance.decompose(new String(randomBytes));
+
+            Mutation rm = new Mutation(KS_NAME, decoratedKey("key1"));
+            update(rm, comment, bigValue, 1000 + i);
+            rm.apply();
+
+            Set<String> rows;
+
+            rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_MATCHES, bigValue.duplicate()));
+            Assert.assertEquals(0, rows.size());
+
+            store.forceBlockingFlush();
+
+            rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_MATCHES, bigValue.duplicate()));
+            Assert.assertEquals(0, rows.size());
+        }
+    }
+
+    @Test
+    public void testSearchTimeouts()
+    {
+        final ByteBuffer firstName = UTF8Type.instance.decompose("first_name");
+
+        Map<String, Pair<String, Integer>> data1 = new HashMap<String, Pair<String, Integer>>()
+        {{
+                put("key1", Pair.create("Pavel", 14));
+                put("key2", Pair.create("Pavel", 26));
+                put("key3", Pair.create("Pavel", 27));
+                put("key4", Pair.create("Jason", 27));
+        }};
+
+        ColumnFamilyStore store = loadData(data1, true);
+
+        RowFilter filter = RowFilter.create();
+        filter.add(store.metadata.getColumnDefinition(firstName), Operator.LIKE_CONTAINS, AsciiType.instance.fromString("a"));
+
+        ReadCommand command =
+            PartitionRangeReadCommand.create(false,
+                                             store.metadata,
+                                             FBUtilities.nowInSeconds(),
+                                             ColumnFilter.all(store.metadata),
+                                             filter,
+                                             DataLimits.NONE,
+                                             DataRange.allData(store.metadata.partitioner));
+        try
+        {
+            new QueryPlan(store, command, 0).execute(ReadExecutionController.empty());
+            Assert.fail();
+        }
+        catch (TimeQuotaExceededException e)
+        {
+            // correct behavior
+        }
+        catch (Exception e)
+        {
+            Assert.fail();
+            e.printStackTrace();
+        }
+
+        // to make sure that query doesn't fail in normal conditions
+
+        try (ReadExecutionController controller = command.executionController())
+        {
+            Set<String> rows = getKeys(new QueryPlan(store, command, DatabaseDescriptor.getRangeRpcTimeout()).execute(controller));
+            assertRows(rows, "key1", "key2", "key3", "key4");
+        }
+    }
+
+    @Test
+    public void testLowerCaseAnalyzer()
+    {
+        testLowerCaseAnalyzer(false);
+        cleanupData();
+        testLowerCaseAnalyzer(true);
+    }
+
+    @Test
+    public void testChinesePrefixSearch()
+    {
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+
+        final ByteBuffer fullName = UTF8Type.instance.decompose("/output/full-name/");
+
+        Mutation rm = new Mutation(KS_NAME, decoratedKey("key1"));
+        update(rm, fullName, UTF8Type.instance.decompose("美加 八田"), 1000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key2"));
+        update(rm, fullName, UTF8Type.instance.decompose("仁美 瀧澤"), 2000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key3"));
+        update(rm, fullName, UTF8Type.instance.decompose("晃宏 高須"), 3000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key4"));
+        update(rm, fullName, UTF8Type.instance.decompose("弘孝 大竹"), 4000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key5"));
+        update(rm, fullName, UTF8Type.instance.decompose("満枝 榎本"), 5000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key6"));
+        update(rm, fullName, UTF8Type.instance.decompose("飛鳥 上原"), 6000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key7"));
+        update(rm, fullName, UTF8Type.instance.decompose("大輝 鎌田"), 7000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key8"));
+        update(rm, fullName, UTF8Type.instance.decompose("利久 寺地"), 8000);
+        rm.apply();
+
+        store.forceBlockingFlush();
+
+
+        Set<String> rows;
+
+        rows = getIndexed(store, 10, buildExpression(fullName, Operator.EQ, UTF8Type.instance.decompose("美加 八田")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(fullName, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("美加")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(fullName, Operator.EQ, UTF8Type.instance.decompose("晃宏 高須")));
+        assertRows(rows, "key3");
+
+        rows = getIndexed(store, 10, buildExpression(fullName, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("大輝")));
+        assertRows(rows, "key7");
+    }
+
+    public void testLowerCaseAnalyzer(boolean forceFlush)
+    {
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+
+        final ByteBuffer comment = UTF8Type.instance.decompose("address");
+
+        Mutation rm = new Mutation(KS_NAME, decoratedKey("key1"));
+        update(rm, comment, UTF8Type.instance.decompose("577 Rogahn Valleys Apt. 178"), 1000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key2"));
+        update(rm, comment, UTF8Type.instance.decompose("89809 Beverly Course Suite 089"), 2000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key3"));
+        update(rm, comment, UTF8Type.instance.decompose("165 clydie oval apt. 399"), 3000);
+        rm.apply();
+
+        if (forceFlush)
+            store.forceBlockingFlush();
+
+        Set<String> rows;
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("577 Rogahn Valleys")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("577 ROgAhn VallEYs")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("577 rogahn valleys")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("577 rogahn")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("57")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("89809 Beverly Course")));
+        assertRows(rows, "key2");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("89809 BEVERly COURSE")));
+        assertRows(rows, "key2");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("89809 beverly course")));
+        assertRows(rows, "key2");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("89809 Beverly")));
+        assertRows(rows, "key2");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("8980")));
+        assertRows(rows, "key2");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("165 ClYdie OvAl APT. 399")));
+        assertRows(rows, "key3");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("165 Clydie Oval Apt. 399")));
+        assertRows(rows, "key3");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("165 clydie oval apt. 399")));
+        assertRows(rows, "key3");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("165 ClYdie OvA")));
+        assertRows(rows, "key3");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("165 ClYdi")));
+        assertRows(rows, "key3");
+
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("165")));
+        assertRows(rows, "key3");
+    }
+
+    @Test
+    public void testPrefixSSTableLookup()
+    {
+        // This test coverts particular case which interval lookup can return invalid results
+        // when queried on the prefix e.g. "j".
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+
+        final ByteBuffer name = UTF8Type.instance.decompose("first_name_prefix");
+
+        Mutation rm;
+
+        rm = new Mutation(KS_NAME, decoratedKey("key1"));
+        update(rm, name, UTF8Type.instance.decompose("Pavel"), 1000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key2"));
+        update(rm, name, UTF8Type.instance.decompose("Jordan"), 2000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key3"));
+        update(rm, name, UTF8Type.instance.decompose("Mikhail"), 3000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key4"));
+        update(rm, name, UTF8Type.instance.decompose("Michael"), 4000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key5"));
+        update(rm, name, UTF8Type.instance.decompose("Johnny"), 5000);
+        rm.apply();
+
+        // first flush would make interval for name - 'johnny' -> 'pavel'
+        store.forceBlockingFlush();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key6"));
+        update(rm, name, UTF8Type.instance.decompose("Jason"), 6000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key7"));
+        update(rm, name, UTF8Type.instance.decompose("Vijay"), 7000);
+        rm.apply();
+
+        rm = new Mutation(KS_NAME, decoratedKey("key8")); // this name is going to be tokenized
+        update(rm, name, UTF8Type.instance.decompose("Jean-Claude"), 8000);
+        rm.apply();
+
+        // this flush is going to produce range - 'jason' -> 'vijay'
+        store.forceBlockingFlush();
+
+        // make sure that overlap of the prefixes is properly handled across sstables
+        // since simple interval tree lookup is not going to cover it, prefix lookup actually required.
+
+        Set<String> rows;
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("J")));
+        assertRows(rows, "key2", "key5", "key6", "key8");
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("j")));
+        assertRows(rows, "key2", "key5", "key6", "key8");
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("m")));
+        assertRows(rows, "key3", "key4");
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("v")));
+        assertRows(rows, "key7");
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("p")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("j")),
+                                     buildExpression(name, Operator.NEQ, UTF8Type.instance.decompose("joh")));
+        assertRows(rows, "key2", "key6", "key8");
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("pavel")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.EQ, UTF8Type.instance.decompose("Pave")));
+        Assert.assertTrue(rows.isEmpty());
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.EQ, UTF8Type.instance.decompose("Pavel")));
+        assertRows(rows, "key1");
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("JeAn")));
+        assertRows(rows, "key8");
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("claUde")));
+        assertRows(rows, "key8");
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.EQ, UTF8Type.instance.decompose("Jean")));
+        Assert.assertTrue(rows.isEmpty());
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.EQ, UTF8Type.instance.decompose("Jean-Claude")));
+        assertRows(rows, "key8");
+    }
+
+    @Test
+    public void testSettingIsLiteralOption()
+    {
+
+        // special type which is UTF-8 but is only on the inside
+        AbstractType<?> stringType = new AbstractType<String>(AbstractType.ComparisonType.CUSTOM)
+        {
+            public ByteBuffer fromString(String source) throws MarshalException
+            {
+                return UTF8Type.instance.fromString(source);
+            }
+
+            public Term fromJSONObject(Object parsed) throws MarshalException
+            {
+                throw new UnsupportedOperationException();
+            }
+
+            public TypeSerializer<String> getSerializer()
+            {
+                return UTF8Type.instance.getSerializer();
+            }
+
+            public int compareCustom(ByteBuffer a, ByteBuffer b)
+            {
+                return UTF8Type.instance.compare(a, b);
+            }
+        };
+
+        // first let's check that we get 'false' for 'isLiteral' if we don't set the option with special comparator
+        ColumnDefinition columnA = ColumnDefinition.regularDef(KS_NAME, CF_NAME, "special-A", stringType);
+
+        ColumnIndex indexA = new ColumnIndex(UTF8Type.instance, columnA, IndexMetadata.fromSchemaMetadata("special-index-A", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+        {{
+            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+        }}));
+
+        Assert.assertTrue(indexA.isIndexed());
+        Assert.assertFalse(indexA.isLiteral());
+
+        // now let's double-check that we do get 'true' when we set it
+        ColumnDefinition columnB = ColumnDefinition.regularDef(KS_NAME, CF_NAME, "special-B", stringType);
+
+        ColumnIndex indexB = new ColumnIndex(UTF8Type.instance, columnB, IndexMetadata.fromSchemaMetadata("special-index-B", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+        {{
+            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+            put("is_literal", "true");
+        }}));
+
+        Assert.assertTrue(indexB.isIndexed());
+        Assert.assertTrue(indexB.isLiteral());
+
+        // and finally we should also get a 'true' if it's built-in UTF-8/ASCII comparator
+        ColumnDefinition columnC = ColumnDefinition.regularDef(KS_NAME, CF_NAME, "special-C", UTF8Type.instance);
+
+        ColumnIndex indexC = new ColumnIndex(UTF8Type.instance, columnC, IndexMetadata.fromSchemaMetadata("special-index-C", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+        {{
+            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+        }}));
+
+        Assert.assertTrue(indexC.isIndexed());
+        Assert.assertTrue(indexC.isLiteral());
+
+        ColumnDefinition columnD = ColumnDefinition.regularDef(KS_NAME, CF_NAME, "special-D", AsciiType.instance);
+
+        ColumnIndex indexD = new ColumnIndex(UTF8Type.instance, columnD, IndexMetadata.fromSchemaMetadata("special-index-D", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+        {{
+            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+        }}));
+
+        Assert.assertTrue(indexD.isIndexed());
+        Assert.assertTrue(indexD.isLiteral());
+
+        // and option should supersedes the comparator type
+        ColumnDefinition columnE = ColumnDefinition.regularDef(KS_NAME, CF_NAME, "special-E", UTF8Type.instance);
+
+        ColumnIndex indexE = new ColumnIndex(UTF8Type.instance, columnE, IndexMetadata.fromSchemaMetadata("special-index-E", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+        {{
+            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+            put("is_literal", "false");
+        }}));
+
+        Assert.assertTrue(indexE.isIndexed());
+        Assert.assertFalse(indexE.isLiteral());
+
+        // test frozen-collection
+        ColumnDefinition columnF = ColumnDefinition.regularDef(KS_NAME,
+                                                               CF_NAME,
+                                                               "special-F",
+                                                               ListType.getInstance(UTF8Type.instance, false));
+
+        ColumnIndex indexF = new ColumnIndex(UTF8Type.instance, columnF, IndexMetadata.fromSchemaMetadata("special-index-F", IndexMetadata.Kind.CUSTOM, new HashMap<String, String>()
+        {{
+            put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, SASIIndex.class.getName());
+        }}));
+
+        Assert.assertTrue(indexF.isIndexed());
+        Assert.assertFalse(indexF.isLiteral());
+    }
+
+    @Test
+    public void testClusteringIndexes()
+    {
+        testClusteringIndexes(false);
+        cleanupData();
+        testClusteringIndexes(true);
+    }
+
+    public void testClusteringIndexes(boolean forceFlush)
+    {
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CLUSTERING_CF_NAME_1);
+
+        executeCQL(CLUSTERING_CF_NAME_1, "INSERT INTO %s.%s (name, nickname, location, age, height, score) VALUES (?, ?, ?, ?, ?, ?)", "Pavel", "xedin", "US", 27, 183, 1.0);
+        executeCQL(CLUSTERING_CF_NAME_1, "INSERT INTO %s.%s (name, nickname, location, age, height, score) VALUES (?, ?, ?, ?, ?, ?)", "Pavel", "xedin", "BY", 28, 182, 2.0);
+        executeCQL(CLUSTERING_CF_NAME_1 ,"INSERT INTO %s.%s (name, nickname, location, age, height, score) VALUES (?, ?, ?, ?, ?, ?)", "Jordan", "jrwest", "US", 27, 182, 1.0);
+
+        if (forceFlush)
+            store.forceBlockingFlush();
+
+        UntypedResultSet results;
+
+        results = executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE location = ? ALLOW FILTERING", "US");
+        Assert.assertNotNull(results);
+        Assert.assertEquals(2, results.size());
+
+        results = executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE age >= ? AND height = ? ALLOW FILTERING", 27, 182);
+        Assert.assertNotNull(results);
+        Assert.assertEquals(2, results.size());
+
+        results = executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE age = ? AND height = ? ALLOW FILTERING", 28, 182);
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE age >= ? AND height = ? AND score >= ? ALLOW FILTERING", 27, 182, 1.0);
+        Assert.assertNotNull(results);
+        Assert.assertEquals(2, results.size());
+
+        results = executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE age >= ? AND height = ? AND score = ? ALLOW FILTERING", 27, 182, 1.0);
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE location = ? AND age >= ? ALLOW FILTERING", "US", 27);
+        Assert.assertNotNull(results);
+        Assert.assertEquals(2, results.size());
+
+        results = executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE location = ? ALLOW FILTERING", "BY");
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE location LIKE 'U%%' ALLOW FILTERING");
+        Assert.assertNotNull(results);
+        Assert.assertEquals(2, results.size());
+
+        results = executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE location LIKE 'U%%' AND height >= 183 ALLOW FILTERING");
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE location LIKE 'US%%' ALLOW FILTERING");
+        Assert.assertNotNull(results);
+        Assert.assertEquals(2, results.size());
+
+        results = executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE location LIKE 'US' ALLOW FILTERING");
+        Assert.assertNotNull(results);
+        Assert.assertEquals(2, results.size());
+
+        try
+        {
+            executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE location LIKE '%%U' ALLOW FILTERING");
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            Assert.assertTrue(e.getMessage().contains("only supported"));
+            // expected
+        }
+
+        try
+        {
+            executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE location LIKE '%%' ALLOW FILTERING");
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            Assert.assertTrue(e.getMessage().contains("empty"));
+            // expected
+        }
+
+        try
+        {
+            executeCQL(CLUSTERING_CF_NAME_1 ,"SELECT * FROM %s.%s WHERE location LIKE '%%%%' ALLOW FILTERING");
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            Assert.assertTrue(e.getMessage().contains("empty"));
+            // expected
+        }
+
+        // check restrictions on non-indexed clustering columns when preceding columns are indexed
+        store = Keyspace.open(KS_NAME).getColumnFamilyStore(CLUSTERING_CF_NAME_2);
+        executeCQL(CLUSTERING_CF_NAME_2 ,"INSERT INTO %s.%s (name, nickname, location, age, height, score) VALUES (?, ?, ?, ?, ?, ?)", "Tony", "tony", "US", 43, 184, 2.0);
+        executeCQL(CLUSTERING_CF_NAME_2 ,"INSERT INTO %s.%s (name, nickname, location, age, height, score) VALUES (?, ?, ?, ?, ?, ?)", "Christopher", "chis", "US", 27, 180, 1.0);
+
+        if (forceFlush)
+            store.forceBlockingFlush();
+
+        results = executeCQL(CLUSTERING_CF_NAME_2 ,"SELECT * FROM %s.%s WHERE location LIKE 'US' AND age = 43 ALLOW FILTERING");
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+        Assert.assertEquals("Tony", results.one().getString("name"));
+    }
+
+    @Test
+    public void testStaticIndex()
+    {
+        testStaticIndex(false);
+        cleanupData();
+        testStaticIndex(true);
+    }
+
+    public void testStaticIndex(boolean shouldFlush)
+    {
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(STATIC_CF_NAME);
+
+        executeCQL(STATIC_CF_NAME, "INSERT INTO %s.%s (sensor_id,sensor_type) VALUES(?, ?)", 1, "TEMPERATURE");
+        executeCQL(STATIC_CF_NAME, "INSERT INTO %s.%s (sensor_id,date,value,variance) VALUES(?, ?, ?, ?)", 1, 20160401L, 24.46, 2);
+        executeCQL(STATIC_CF_NAME, "INSERT INTO %s.%s (sensor_id,date,value,variance) VALUES(?, ?, ?, ?)", 1, 20160402L, 25.62, 5);
+        executeCQL(STATIC_CF_NAME, "INSERT INTO %s.%s (sensor_id,date,value,variance) VALUES(?, ?, ?, ?)", 1, 20160403L, 24.96, 4);
+
+        if (shouldFlush)
+            store.forceBlockingFlush();
+
+        executeCQL(STATIC_CF_NAME, "INSERT INTO %s.%s (sensor_id,sensor_type) VALUES(?, ?)", 2, "PRESSURE");
+        executeCQL(STATIC_CF_NAME, "INSERT INTO %s.%s (sensor_id,date,value,variance) VALUES(?, ?, ?, ?)", 2, 20160401L, 1.03, 9);
+        executeCQL(STATIC_CF_NAME, "INSERT INTO %s.%s (sensor_id,date,value,variance) VALUES(?, ?, ?, ?)", 2, 20160402L, 1.04, 7);
+        executeCQL(STATIC_CF_NAME, "INSERT INTO %s.%s (sensor_id,date,value,variance) VALUES(?, ?, ?, ?)", 2, 20160403L, 1.01, 4);
+
+        if (shouldFlush)
+            store.forceBlockingFlush();
+
+        UntypedResultSet results;
+
+        // Prefix search on static column only
+        results = executeCQL(STATIC_CF_NAME ,"SELECT * FROM %s.%s WHERE sensor_type LIKE 'temp%%'");
+        Assert.assertNotNull(results);
+        Assert.assertEquals(3, results.size());
+
+        Iterator<UntypedResultSet.Row> iterator = results.iterator();
+
+        UntypedResultSet.Row row1 = iterator.next();
+        Assert.assertEquals(20160401L, row1.getLong("date"));
+        Assert.assertEquals(24.46, row1.getDouble("value"), 0);
+        Assert.assertEquals(2, row1.getInt("variance"));
+
+
+        UntypedResultSet.Row row2 = iterator.next();
+        Assert.assertEquals(20160402L, row2.getLong("date"));
+        Assert.assertEquals(25.62, row2.getDouble("value"), 0);
+        Assert.assertEquals(5, row2.getInt("variance"));
+
+        UntypedResultSet.Row row3 = iterator.next();
+        Assert.assertEquals(20160403L, row3.getLong("date"));
+        Assert.assertEquals(24.96, row3.getDouble("value"), 0);
+        Assert.assertEquals(4, row3.getInt("variance"));
+
+
+        // Combined static and non static filtering
+        results = executeCQL(STATIC_CF_NAME ,"SELECT * FROM %s.%s WHERE sensor_type=? AND value >= ? AND value <= ? AND variance=? ALLOW FILTERING",
+                             "pressure", 1.02, 1.05, 7);
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        row1 = results.one();
+        Assert.assertEquals(20160402L, row1.getLong("date"));
+        Assert.assertEquals(1.04, row1.getDouble("value"), 0);
+        Assert.assertEquals(7, row1.getInt("variance"));
+
+        // Only non statc columns filtering
+        results = executeCQL(STATIC_CF_NAME ,"SELECT * FROM %s.%s WHERE value >= ? AND variance <= ? ALLOW FILTERING", 1.02, 7);
+        Assert.assertNotNull(results);
+        Assert.assertEquals(4, results.size());
+
+        iterator = results.iterator();
+
+        row1 = iterator.next();
+        Assert.assertEquals("TEMPERATURE", row1.getString("sensor_type"));
+        Assert.assertEquals(20160401L, row1.getLong("date"));
+        Assert.assertEquals(24.46, row1.getDouble("value"), 0);
+        Assert.assertEquals(2, row1.getInt("variance"));
+
+
+        row2 = iterator.next();
+        Assert.assertEquals("TEMPERATURE", row2.getString("sensor_type"));
+        Assert.assertEquals(20160402L, row2.getLong("date"));
+        Assert.assertEquals(25.62, row2.getDouble("value"), 0);
+        Assert.assertEquals(5, row2.getInt("variance"));
+
+        row3 = iterator.next();
+        Assert.assertEquals("TEMPERATURE", row3.getString("sensor_type"));
+        Assert.assertEquals(20160403L, row3.getLong("date"));
+        Assert.assertEquals(24.96, row3.getDouble("value"), 0);
+        Assert.assertEquals(4, row3.getInt("variance"));
+
+        UntypedResultSet.Row row4 = iterator.next();
+        Assert.assertEquals("PRESSURE", row4.getString("sensor_type"));
+        Assert.assertEquals(20160402L, row4.getLong("date"));
+        Assert.assertEquals(1.04, row4.getDouble("value"), 0);
+        Assert.assertEquals(7, row4.getInt("variance"));
+    }
+
+    @Test
+    public void testTableRebuild() throws Exception
+    {
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CLUSTERING_CF_NAME_1);
+
+        executeCQL(CLUSTERING_CF_NAME_1, "INSERT INTO %s.%s (name, nickname, location, age, height, score) VALUES (?, ?, ?, ?, ?, ?)", "Pavel", "xedin", "US", 27, 183, 1.0);
+        executeCQL(CLUSTERING_CF_NAME_1, "INSERT INTO %s.%s (name, location, age, height, score) VALUES (?, ?, ?, ?, ?)", "Pavel", "BY", 28, 182, 2.0);
+        executeCQL(CLUSTERING_CF_NAME_1, "INSERT INTO %s.%s (name, nickname, location, age, height, score) VALUES (?, ?, ?, ?, ?, ?)", "Jordan", "jrwest", "US", 27, 182, 1.0);
+
+        store.forceBlockingFlush();
+
+        SSTable ssTable = store.getSSTables(SSTableSet.LIVE).iterator().next();
+        Path path = FileSystems.getDefault().getPath(ssTable.getFilename().replace("-Data", "-SI_age"));
+
+        // Overwrite index file with garbage
+        try (Writer writer = new FileWriter(path.toFile(), false))
+        {
+            writer.write("garbage");
+        }
+
+        long size1 = Files.readAttributes(path, BasicFileAttributes.class).size();
+
+        // Trying to query the corrupted index file yields no results
+        Assert.assertTrue(executeCQL(CLUSTERING_CF_NAME_1, "SELECT * FROM %s.%s WHERE age = 27 AND name = 'Pavel'").isEmpty());
+
+        // Rebuld index
+        store.rebuildSecondaryIndex("age");
+
+        long size2 = Files.readAttributes(path, BasicFileAttributes.class).size();
+        // Make sure that garbage was overwriten
+        Assert.assertTrue(size2 > size1);
+
+        // Make sure that indexes work for rebuit tables
+        CQLTester.assertRows(executeCQL(CLUSTERING_CF_NAME_1, "SELECT * FROM %s.%s WHERE age = 27 AND name = 'Pavel'"),
+                             CQLTester.row("Pavel", "US", 27, "xedin", 183, 1.0));
+        CQLTester.assertRows(executeCQL(CLUSTERING_CF_NAME_1, "SELECT * FROM %s.%s WHERE age = 28"),
+                             CQLTester.row("Pavel", "BY", 28, "xedin", 182, 2.0));
+        CQLTester.assertRows(executeCQL(CLUSTERING_CF_NAME_1, "SELECT * FROM %s.%s WHERE score < 2.0 AND nickname = 'jrwest' ALLOW FILTERING"),
+                             CQLTester.row("Jordan", "US", 27, "jrwest", 182, 1.0));
+    }
+
+    @Test
+    public void testIndexRebuild()
+    {
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CLUSTERING_CF_NAME_1);
+
+        executeCQL(CLUSTERING_CF_NAME_1, "INSERT INTO %s.%s (name, nickname) VALUES (?, ?)", "Alex", "ifesdjeen");
+
+        store.forceBlockingFlush();
+
+        for (Index index : store.indexManager.listIndexes())
+        {
+            SASIIndex idx = (SASIIndex) index;
+            Assert.assertFalse(idx.getIndex().init(store.getLiveSSTables()).iterator().hasNext());
+        }
+    }
+
+    @Test
+    public void testInvalidIndexOptions()
+    {
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+
+        try
+        {
+            // unsupported partition key column
+            SASIIndex.validateOptions(Collections.singletonMap("target", "id"), store.metadata);
+            Assert.fail();
+        }
+        catch (ConfigurationException e)
+        {
+            Assert.assertTrue(e.getMessage().contains("partition key columns are not yet supported by SASI"));
+        }
+
+        try
+        {
+            // invalid index mode
+            SASIIndex.validateOptions(new HashMap<String, String>()
+                                      {{ put("target", "address"); put("mode", "NORMAL"); }},
+                                      store.metadata);
+            Assert.fail();
+        }
+        catch (ConfigurationException e)
+        {
+            Assert.assertTrue(e.getMessage().contains("Incorrect index mode"));
+        }
+
+        try
+        {
+            // invalid SPARSE on the literal index
+            SASIIndex.validateOptions(new HashMap<String, String>()
+                                      {{ put("target", "address"); put("mode", "SPARSE"); }},
+                                      store.metadata);
+            Assert.fail();
+        }
+        catch (ConfigurationException e)
+        {
+            Assert.assertTrue(e.getMessage().contains("non-literal"));
+        }
+
+        try
+        {
+            // invalid SPARSE on the explicitly literal index
+            SASIIndex.validateOptions(new HashMap<String, String>()
+                                      {{ put("target", "height"); put("mode", "SPARSE"); put("is_literal", "true"); }},
+                    store.metadata);
+            Assert.fail();
+        }
+        catch (ConfigurationException e)
+        {
+            Assert.assertTrue(e.getMessage().contains("non-literal"));
+        }
+
+        try
+        {
+            //  SPARSE with analyzer
+            SASIIndex.validateOptions(new HashMap<String, String>()
+                                      {{ put("target", "height"); put("mode", "SPARSE"); put("analyzed", "true"); }},
+                                      store.metadata);
+            Assert.fail();
+        }
+        catch (ConfigurationException e)
+        {
+            Assert.assertTrue(e.getMessage().contains("doesn't support analyzers"));
+        }
+    }
+
+    @Test
+    public void testLIKEAndEQSemanticsWithDifferenceKindsOfIndexes()
+    {
+        String containsTable = "sasi_like_contains_test";
+        String prefixTable = "sasi_like_prefix_test";
+        String analyzedPrefixTable = "sasi_like_analyzed_prefix_test";
+        String tokenizedContainsTable = "sasi_like_analyzed_contains_test";
+
+        QueryProcessor.executeOnceInternal(String.format("CREATE TABLE IF NOT EXISTS %s.%s (k int primary key, v text);", KS_NAME, containsTable));
+        QueryProcessor.executeOnceInternal(String.format("CREATE TABLE IF NOT EXISTS %s.%s (k int primary key, v text);", KS_NAME, prefixTable));
+        QueryProcessor.executeOnceInternal(String.format("CREATE TABLE IF NOT EXISTS %s.%s (k int primary key, v text);", KS_NAME, analyzedPrefixTable));
+        QueryProcessor.executeOnceInternal(String.format("CREATE TABLE IF NOT EXISTS %s.%s (k int primary key, v text);", KS_NAME, tokenizedContainsTable));
+
+        QueryProcessor.executeOnceInternal(String.format("CREATE CUSTOM INDEX IF NOT EXISTS ON %s.%s(v) " +
+                "USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = { 'mode' : 'CONTAINS', " +
+                                                         "'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer', " +
+                                                         "'case_sensitive': 'false' };",
+                                                         KS_NAME, containsTable));
+        QueryProcessor.executeOnceInternal(String.format("CREATE CUSTOM INDEX IF NOT EXISTS ON %s.%s(v) " +
+                "USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = { 'mode' : 'PREFIX' };", KS_NAME, prefixTable));
+        QueryProcessor.executeOnceInternal(String.format("CREATE CUSTOM INDEX IF NOT EXISTS ON %s.%s(v) " +
+                "USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = { 'mode' : 'PREFIX', 'analyzed': 'true' };", KS_NAME, analyzedPrefixTable));
+        QueryProcessor.executeOnceInternal(String.format("CREATE CUSTOM INDEX IF NOT EXISTS ON %s.%s(v) " +
+                                                         "USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = " +
+                                                         "{ 'mode' : 'CONTAINS', 'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer'," +
+                                                         "'analyzed': 'true', 'tokenization_enable_stemming': 'true', 'tokenization_normalize_lowercase': 'true', " +
+                                                         "'tokenization_locale': 'en' };",
+                                                         KS_NAME, tokenizedContainsTable));
+
+        testLIKEAndEQSemanticsWithDifferenceKindsOfIndexes(containsTable, prefixTable, analyzedPrefixTable, tokenizedContainsTable, false);
+        testLIKEAndEQSemanticsWithDifferenceKindsOfIndexes(containsTable, prefixTable, analyzedPrefixTable, tokenizedContainsTable, true);
+    }
+
+    private void testLIKEAndEQSemanticsWithDifferenceKindsOfIndexes(String containsTable,
+                                                                    String prefixTable,
+                                                                    String analyzedPrefixTable,
+                                                                    String tokenizedContainsTable,
+                                                                    boolean forceFlush)
+    {
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (k, v) VALUES (?, ?);", KS_NAME, containsTable), 0, "Pavel");
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (k, v) VALUES (?, ?);", KS_NAME, prefixTable), 0, "Jean-Claude");
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (k, v) VALUES (?, ?);", KS_NAME, analyzedPrefixTable), 0, "Jean-Claude");
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (k, v) VALUES (?, ?);", KS_NAME, tokenizedContainsTable), 0, "Pavel");
+
+        if (forceFlush)
+        {
+            Keyspace keyspace = Keyspace.open(KS_NAME);
+            for (String table : Arrays.asList(containsTable, prefixTable, analyzedPrefixTable))
+                keyspace.getColumnFamilyStore(table).forceBlockingFlush();
+        }
+
+        UntypedResultSet results;
+
+        // CONTAINS
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Pav';", KS_NAME, containsTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(0, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Pav%%';", KS_NAME, containsTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Pavel';", KS_NAME, containsTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v = 'Pav';", KS_NAME, containsTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(0, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v = 'Pavel';", KS_NAME, containsTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v = 'Pav';", KS_NAME, tokenizedContainsTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since CONTAINS + analyzed indexes only support LIKE
+        }
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Pav%%';", KS_NAME, tokenizedContainsTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since CONTAINS + analyzed only support LIKE
+        }
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Pav%%';", KS_NAME, containsTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE '%%Pav';", KS_NAME, containsTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(0, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE '%%Pav%%';", KS_NAME, containsTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        // PREFIX
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v = 'Jean';", KS_NAME, prefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(0, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v = 'Jean-Claude';", KS_NAME, prefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Jea';", KS_NAME, prefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(0, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Jea%%';", KS_NAME, prefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE '%%Jea';", KS_NAME, prefixTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since PREFIX indexes only support LIKE '<term>%'
+        }
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE '%%Jea%%';", KS_NAME, prefixTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since PREFIX indexes only support LIKE '<term>%'
+        }
+
+        // PREFIX + analyzer
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v = 'Jean';", KS_NAME, analyzedPrefixTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since PREFIX indexes only support EQ without tokenization
+        }
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Jean';", KS_NAME, analyzedPrefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Claude';", KS_NAME, analyzedPrefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Jean-Claude';", KS_NAME, analyzedPrefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Jean%%';", KS_NAME, analyzedPrefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Claude%%';", KS_NAME, analyzedPrefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE '%%Jean';", KS_NAME, analyzedPrefixTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since PREFIX indexes only support LIKE '<term>%' and LIKE '<term>'
+        }
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE '%%Claude%%';", KS_NAME, analyzedPrefixTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since PREFIX indexes only support LIKE '<term>%' and LIKE '<term>'
+        }
+
+        for (String table : Arrays.asList(containsTable, prefixTable, analyzedPrefixTable))
+            QueryProcessor.executeOnceInternal(String.format("TRUNCATE TABLE %s.%s", KS_NAME, table));
+    }
+
+    @Test
+    public void testConditionalsWithReversedType()
+    {
+        final String TABLE_NAME = "reversed_clustering";
+
+        QueryProcessor.executeOnceInternal(String.format("CREATE TABLE IF NOT EXISTS %s.%s (pk text, ck int, v int, PRIMARY KEY (pk, ck)) " +
+                                                         "WITH CLUSTERING ORDER BY (ck DESC);", KS_NAME, TABLE_NAME));
+        QueryProcessor.executeOnceInternal(String.format("CREATE CUSTOM INDEX ON %s.%s (ck) USING 'org.apache.cassandra.index.sasi.SASIIndex'", KS_NAME, TABLE_NAME));
+        QueryProcessor.executeOnceInternal(String.format("CREATE CUSTOM INDEX ON %s.%s (v) USING 'org.apache.cassandra.index.sasi.SASIIndex'", KS_NAME, TABLE_NAME));
+
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (pk, ck, v) VALUES ('Alex', 1, 1);", KS_NAME, TABLE_NAME));
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (pk, ck, v) VALUES ('Alex', 2, 2);", KS_NAME, TABLE_NAME));
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (pk, ck, v) VALUES ('Alex', 3, 3);", KS_NAME, TABLE_NAME));
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (pk, ck, v) VALUES ('Tom', 1, 1);", KS_NAME, TABLE_NAME));
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (pk, ck, v) VALUES ('Tom', 2, 2);", KS_NAME, TABLE_NAME));
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (pk, ck, v) VALUES ('Tom', 3, 3);", KS_NAME, TABLE_NAME));
+
+        UntypedResultSet resultSet = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE ck <= 2;", KS_NAME, TABLE_NAME));
+
+        CQLTester.assertRowsIgnoringOrder(resultSet,
+                                          CQLTester.row("Alex", 1, 1),
+                                          CQLTester.row("Alex", 2, 2),
+                                          CQLTester.row("Tom", 1, 1),
+                                          CQLTester.row("Tom", 2, 2));
+
+        resultSet = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE ck <= 2 AND v > 1 ALLOW FILTERING;", KS_NAME, TABLE_NAME));
+
+        CQLTester.assertRowsIgnoringOrder(resultSet,
+                                          CQLTester.row("Alex", 2, 2),
+                                          CQLTester.row("Tom", 2, 2));
+
+        resultSet = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE ck < 2;", KS_NAME, TABLE_NAME));
+        CQLTester.assertRowsIgnoringOrder(resultSet,
+                                          CQLTester.row("Alex", 1, 1),
+                                          CQLTester.row("Tom", 1, 1));
+
+        resultSet = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE ck >= 2;", KS_NAME, TABLE_NAME));
+        CQLTester.assertRowsIgnoringOrder(resultSet,
+                                          CQLTester.row("Alex", 2, 2),
+                                          CQLTester.row("Alex", 3, 3),
+                                          CQLTester.row("Tom", 2, 2),
+                                          CQLTester.row("Tom", 3, 3));
+
+        resultSet = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE ck >= 2 AND v < 3 ALLOW FILTERING;", KS_NAME, TABLE_NAME));
+        CQLTester.assertRowsIgnoringOrder(resultSet,
+                                          CQLTester.row("Alex", 2, 2),
+                                          CQLTester.row("Tom", 2, 2));
+
+        resultSet = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE ck > 2;", KS_NAME, TABLE_NAME));
+        CQLTester.assertRowsIgnoringOrder(resultSet,
+                                          CQLTester.row("Alex", 3, 3),
+                                          CQLTester.row("Tom", 3, 3));
+    }
+
+    @Test
+    public void testIndexMemtableSwitching() throws Exception
+    {
+        // write some data but don't flush
+        ColumnFamilyStore store = loadData(new HashMap<String, Pair<String, Integer>>()
+        {{
+            put("key1", Pair.create("Pavel", 14));
+        }}, false);
+
+        ColumnIndex index = ((SASIIndex) store.indexManager.getIndexByName("first_name")).getIndex();
+        IndexMemtable beforeFlushMemtable = index.getCurrentMemtable();
+
+        PartitionRangeReadCommand command =
+            PartitionRangeReadCommand.create(false,
+                                             store.metadata,
+                                             FBUtilities.nowInSeconds(),
+                                             ColumnFilter.all(store.metadata),
+                                             RowFilter.NONE,
+                                             DataLimits.NONE,
+                                             DataRange.allData(store.getPartitioner()));
+
+        QueryController controller = new QueryController(store, command, Integer.MAX_VALUE);
+        org.apache.cassandra.index.sasi.plan.Expression expression =
+                new org.apache.cassandra.index.sasi.plan.Expression(controller, index)
+                                                    .add(Operator.LIKE_MATCHES, UTF8Type.instance.fromString("Pavel"));
+
+        Assert.assertTrue(rangesSize(beforeFlushMemtable, expression) > 0);
+
+        store.forceBlockingFlush();
+
+        IndexMemtable afterFlushMemtable = index.getCurrentMemtable();
+
+        Assert.assertNotSame(afterFlushMemtable, beforeFlushMemtable);
+        Assert.assertEquals(rangesSize(afterFlushMemtable, expression), 0);
+        Assert.assertEquals(0, index.getPendingMemtables().size());
+
+        loadData(new HashMap<String, Pair<String, Integer>>()
+        {{
+            put("key2", Pair.create("Sam", 15));
+        }}, false);
+
+        expression = new org.apache.cassandra.index.sasi.plan.Expression(controller, index)
+                        .add(Operator.LIKE_MATCHES, UTF8Type.instance.fromString("Sam"));
+
+        beforeFlushMemtable = index.getCurrentMemtable();
+        Assert.assertTrue(rangesSize(beforeFlushMemtable, expression) > 0);
+
+        // let's emulate switching memtable and see if we can still read-data in "pending"
+        index.switchMemtable(store.getTracker().getView().getCurrentMemtable());
+
+        Assert.assertNotSame(index.getCurrentMemtable(), beforeFlushMemtable);
+        Assert.assertEquals(1, index.getPendingMemtables().size());
+
+        Assert.assertTrue(rangesSize(index, expression) > 0);
+
+        // emulate "everything is flushed" notification
+        index.discardMemtable(store.getTracker().getView().getCurrentMemtable());
+
+        Assert.assertEquals(0, index.getPendingMemtables().size());
+        Assert.assertEquals(rangesSize(index, expression), 0);
+
+        // test discarding data from memtable
+        loadData(new HashMap<String, Pair<String, Integer>>()
+        {{
+            put("key3", Pair.create("Jonathan", 16));
+        }}, false);
+
+        expression = new org.apache.cassandra.index.sasi.plan.Expression(controller, index)
+                .add(Operator.LIKE_MATCHES, UTF8Type.instance.fromString("Jonathan"));
+
+        Assert.assertTrue(rangesSize(index, expression) > 0);
+
+        index.switchMemtable();
+        Assert.assertEquals(rangesSize(index, expression), 0);
+    }
+
+    private static long rangesSize(IndexMemtable index, org.apache.cassandra.index.sasi.plan.Expression expression) throws IOException
+    {
+        try (RangeIterator<Long, Token> ranges = index.search(expression))
+        {
+            return ranges.getCount();
+        }
+    }
+
+    private static long rangesSize(ColumnIndex index, org.apache.cassandra.index.sasi.plan.Expression expression) throws IOException
+    {
+        try (RangeIterator<Long, Token> ranges = index.searchMemtable(expression))
+        {
+            return ranges.getCount();
+        }
+    }
+
+    @Test
+    public void testAnalyzerValidation()
+    {
+        final String TABLE_NAME = "analyzer_validation";
+        QueryProcessor.executeOnceInternal(String.format("CREATE TABLE %s.%s (" +
+                                                         "  pk text PRIMARY KEY, " +
+                                                         "  ascii_v ascii, " +
+                                                         "  bigint_v bigint, " +
+                                                         "  blob_v blob, " +
+                                                         "  boolean_v boolean, " +
+                                                         "  date_v date, " +
+                                                         "  decimal_v decimal, " +
+                                                         "  double_v double, " +
+                                                         "  float_v float, " +
+                                                         "  inet_v inet, " +
+                                                         "  int_v int, " +
+                                                         "  smallint_v smallint, " +
+                                                         "  text_v text, " +
+                                                         "  time_v time, " +
+                                                         "  timestamp_v timestamp, " +
+                                                         "  timeuuid_v timeuuid, " +
+                                                         "  tinyint_v tinyint, " +
+                                                         "  uuid_v uuid, " +
+                                                         "  varchar_v varchar, " +
+                                                         "  varint_v varint" +
+                                                         ");",
+                                                         KS_NAME,
+                                                         TABLE_NAME));
+
+        Columns regulars = Schema.instance.getCFMetaData(KS_NAME, TABLE_NAME).partitionColumns().regulars;
+        List<String> allColumns = regulars.stream().map(ColumnDefinition::toString).collect(Collectors.toList());
+        List<String> textColumns = Arrays.asList("text_v", "ascii_v", "varchar_v");
+
+        new HashMap<Class<? extends AbstractAnalyzer>, List<String>>()
+        {{
+            put(StandardAnalyzer.class, textColumns);
+            put(NonTokenizingAnalyzer.class, textColumns);
+            put(DelimiterAnalyzer.class, textColumns);
+            put(NoOpAnalyzer.class, allColumns);
+        }}
+        .forEach((analyzer, supportedColumns) -> {
+            for (String column : allColumns)
+            {
+                String query = String.format("CREATE CUSTOM INDEX ON %s.%s(%s) " +
+                                             "USING 'org.apache.cassandra.index.sasi.SASIIndex' " +
+                                             "WITH OPTIONS = {'analyzer_class': '%s', 'mode':'PREFIX'};",
+                                             KS_NAME, TABLE_NAME, column, analyzer.getName());
+
+                if (supportedColumns.contains(column))
+                {
+                    QueryProcessor.executeOnceInternal(query);
+                }
+                else
+                {
+                    try
+                    {
+                        QueryProcessor.executeOnceInternal(query);
+                        Assert.fail("Expected ConfigurationException");
+                    }
+                    catch (ConfigurationException e)
+                    {
+                        // expected
+                        Assert.assertTrue("Unexpected error message " + e.getMessage(),
+                                          e.getMessage().contains("does not support type"));
+                    }
+                }
+            }
+        });
+    }
+
+    @Test
+    public void testIllegalArgumentsForAnalyzerShouldFail()
+    {
+        String baseTable = "illegal_argument_test";
+        String indexName = "illegal_index";
+        QueryProcessor.executeOnceInternal(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", KS_NAME));
+        QueryProcessor.executeOnceInternal(String.format("CREATE TABLE IF NOT EXISTS %s.%s (k int primary key, v text);", KS_NAME, baseTable));
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("CREATE CUSTOM INDEX IF NOT EXISTS %s ON %s.%s(v) " +
+                            "USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = { 'mode' : 'CONTAINS', " +
+                            "'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer', " +
+                            "'case_sensitive': 'false'," +
+                            "'normalize_uppercase': 'true'};",
+                    indexName, KS_NAME, baseTable));
+
+            Assert.fail("creation of index analyzer with illegal options should fail");
+        }
+        catch (ConfigurationException e)
+        {
+            //correct behaviour
+            //confirm that it wasn't written to the schema
+            String query = String.format("SELECT * FROM system_schema.indexes WHERE keyspace_name = '%s' " +
+                                         "and table_name = '%s' and index_name = '%s';", KS_NAME, baseTable, indexName);
+            Assertions.assertThat(QueryProcessor.executeOnceInternal(query)).isEmpty();
+
+            Assert.assertEquals("case_sensitive option cannot be specified together with either normalize_lowercase or normalize_uppercase", e.getMessage());
+        }
+    }
+
+    private ColumnFamilyStore loadData(Map<String, Pair<String, Integer>> data, boolean forceFlush)
+    {
+        return loadData(data, forceFlush, ++timestamp);
+    }
+
+    private ColumnFamilyStore loadData(Map<String, Pair<String, Integer>> data, boolean forceFlush, long timestamp)
+    {
+        for (Map.Entry<String, Pair<String, Integer>> e : data.entrySet())
+            newMutation(e.getKey(), e.getValue().left, null, e.getValue().right, timestamp).apply();
+
+        ColumnFamilyStore store = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+
+        if (forceFlush)
+            store.forceBlockingFlush();
+
+        return store;
+    }
+
+    private static void cleanupData()
+    {
+        Keyspace ks = Keyspace.open(KS_NAME);
+        for (ColumnFamilyStore store : ks.getColumnFamilyStores())
+        {
+            store.truncateBlocking();
+        }
+    }
+
+    private static Set<String> getIndexed(ColumnFamilyStore store, int maxResults, Expression... expressions)
+    {
+        return getIndexed(store, ColumnFilter.all(store.metadata), maxResults, expressions);
+    }
+
+    private static Set<String> getIndexed(ColumnFamilyStore store, ColumnFilter columnFilter, int maxResults, Expression... expressions)
+    {
+        ReadCommand command = getIndexReadCommand(store, columnFilter, null, maxResults, expressions);
+        try (ReadExecutionController controller = command.executionController();
+             UnfilteredPartitionIterator rows = command.executeLocally(controller))
+        {
+            return getKeys(rows);
+        }
+    }
+
+    private static Set<DecoratedKey> getPaged(ColumnFamilyStore store, int pageSize, Expression... expressions)
+    {
+        Set<DecoratedKey> uniqueKeys = new TreeSet<>();
+
+        DecoratedKey lastKey = null;
+
+        int count;
+        do
+        {
+            count = 0;
+            ReadCommand command = getIndexReadCommand(store, ColumnFilter.all(store.metadata), lastKey, pageSize, expressions);
+
+            try (ReadExecutionController controller = command.executionController();
+                 UnfilteredPartitionIterator currentPage = command.executeLocally(controller))
+            {
+                if (currentPage == null)
+                    break;
+
+                while (currentPage.hasNext())
+                {
+                    try (UnfilteredRowIterator row = currentPage.next())
+                    {
+                        uniqueKeys.add(row.partitionKey());
+                        lastKey = row.partitionKey();
+                        count++;
+                    }
+                }
+            }
+
+        }
+        while (count == pageSize);
+
+        return uniqueKeys;
+    }
+
+    private static ReadCommand getIndexReadCommand(ColumnFamilyStore store, ColumnFilter columnFilter, DecoratedKey startKey, int maxResults, Expression[] expressions)
+    {
+        DataRange range = (startKey == null)
+                            ? DataRange.allData(PARTITIONER)
+                            : DataRange.forKeyRange(new Range<>(startKey, PARTITIONER.getMinimumToken().maxKeyBound()));
+
+        RowFilter filter = RowFilter.create();
+        for (Expression e : expressions)
+            filter.add(store.metadata.getColumnDefinition(e.name), e.op, e.value);
+
+        ReadCommand command =
+            PartitionRangeReadCommand.create(false, store.metadata,
+                                             FBUtilities.nowInSeconds(),
+                                             columnFilter,
+                                             filter,
+                                             DataLimits.cqlLimits(maxResults),
+                                             range);
+        return command;
+    }
+
+    private static Mutation newMutation(String key, String firstName, String lastName, int age, long timestamp)
+    {
+        Mutation rm = new Mutation(KS_NAME, decoratedKey(AsciiType.instance.decompose(key)));
+        List<Cell> cells = new ArrayList<>(3);
+
+        if (age >= 0)
+            cells.add(buildCell(ByteBufferUtil.bytes("age"), Int32Type.instance.decompose(age), timestamp));
+        if (firstName != null)
+            cells.add(buildCell(ByteBufferUtil.bytes("first_name"), UTF8Type.instance.decompose(firstName), timestamp));
+        if (lastName != null)
+            cells.add(buildCell(ByteBufferUtil.bytes("last_name"), UTF8Type.instance.decompose(lastName), timestamp));
+
+        update(rm, cells);
+        return rm;
+    }
+
+    private static Set<String> getKeys(final UnfilteredPartitionIterator rows)
+    {
+        try
+        {
+            return new TreeSet<String>()
+            {{
+                while (rows.hasNext())
+                {
+                    try (UnfilteredRowIterator row = rows.next())
+                    {
+                        if (!row.isEmpty())
+                            add(AsciiType.instance.compose(row.partitionKey().getKey()));
+                    }
+                }
+            }};
+        }
+        finally
+        {
+            rows.close();
+        }
+    }
+
+    private static List<String> convert(final Set<DecoratedKey> keys)
+    {
+        return new ArrayList<String>()
+        {{
+            for (DecoratedKey key : keys)
+                add(AsciiType.instance.getString(key.getKey()));
+        }};
+    }
+
+    private UntypedResultSet executeCQL(String cfName, String query, Object... values)
+    {
+        return QueryProcessor.executeOnceInternal(String.format(query, KS_NAME, cfName), values);
+    }
+
+    private Set<String> executeCQLWithKeys(String rawStatement)
+    {
+        SelectStatement statement = (SelectStatement) QueryProcessor.parseStatement(rawStatement).prepare(ClientState.forInternalCalls()).statement;
+        ResultMessage.Rows cqlRows = statement.executeInternal(QueryState.forInternalCalls(), QueryOptions.DEFAULT);
+
+        Set<String> results = new TreeSet<>();
+        for (CqlRow row : cqlRows.toThriftResult().getRows())
+        {
+            for (org.apache.cassandra.thrift.Column col : row.columns)
+            {
+                String columnName = UTF8Type.instance.getString(col.bufferForName());
+                if (columnName.equals("id"))
+                    results.add(AsciiType.instance.getString(col.bufferForValue()));
+            }
+        }
+
+        return results;
+    }
+
+    private static DecoratedKey decoratedKey(ByteBuffer key)
+    {
+        return PARTITIONER.decorateKey(key);
+    }
+
+    private static DecoratedKey decoratedKey(String key)
+    {
+        return decoratedKey(AsciiType.instance.fromString(key));
+    }
+
+    private static Row buildRow(Collection<Cell> cells)
+    {
+        return buildRow(Iterables.toArray(cells, Cell.class));
+    }
+
+    private static Row buildRow(Cell... cells)
+    {
+        Row.Builder rowBuilder = BTreeRow.sortedBuilder();
+        rowBuilder.newRow(Clustering.EMPTY);
+        for (Cell c : cells)
+            rowBuilder.addCell(c);
+        return rowBuilder.build();
+    }
+
+    private static Cell buildCell(ByteBuffer name, ByteBuffer value, long timestamp)
+    {
+        CFMetaData cfm = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME).metadata;
+        return BufferCell.live(cfm.getColumnDefinition(name), timestamp, value);
+    }
+
+    private static Cell buildCell(CFMetaData cfm, ByteBuffer name, ByteBuffer value, long timestamp)
+    {
+        ColumnDefinition column = cfm.getColumnDefinition(name);
+        assert column != null;
+        return BufferCell.live(column, timestamp, value);
+    }
+
+    private static Expression buildExpression(ByteBuffer name, Operator op, ByteBuffer value)
+    {
+        return new Expression(name, op, value);
+    }
+
+    private static void update(Mutation rm, ByteBuffer name, ByteBuffer value, long timestamp)
+    {
+        CFMetaData metadata = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME).metadata;
+        rm.add(PartitionUpdate.singleRowUpdate(metadata, rm.key(), buildRow(buildCell(metadata, name, value, timestamp))));
+    }
+
+
+    private static void update(Mutation rm, List<Cell> cells)
+    {
+        CFMetaData metadata = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME).metadata;
+        rm.add(PartitionUpdate.singleRowUpdate(metadata, rm.key(), buildRow(cells)));
+    }
+
+    private static class Expression
+    {
+        public final ByteBuffer name;
+        public final Operator op;
+        public final ByteBuffer value;
+
+        public Expression(ByteBuffer name, Operator op, ByteBuffer value)
+        {
+            this.name = name;
+            this.op = op;
+            this.value = value;
+        }
+    }
+
+    private static void assertRows(Set<String> actual, String... expected)
+    {
+        String message = String.format("Expected rows to contain %s but found %s", Arrays.toString(expected), actual);
+        Assert.assertArrayEquals(message, expected, Iterables.toArray(actual, String.class));
+    }
+
+    private static void assertRowsSize(Set<String> actual, int expectedSize)
+    {
+        String message = String.format("Expected %s to have size %d but found size %d", actual, expectedSize, actual.size());
+        Assert.assertEquals(message, expectedSize, actual.size());
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/analyzer/DelimiterAnalyzerTest.java b/test/unit/org/apache/cassandra/index/sasi/analyzer/DelimiterAnalyzerTest.java
new file mode 100644
index 0000000..f5f007f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/analyzer/DelimiterAnalyzerTest.java
@@ -0,0 +1,317 @@
+/*
+ * 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.index.sasi.analyzer;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.SetType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.commons.io.IOUtils;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class DelimiterAnalyzerTest
+{
+
+    @Test
+    public void caseSensitiveAnalizer() throws Exception
+    {
+        DelimiterAnalyzer analyzer = new DelimiterAnalyzer();
+
+        analyzer.init(
+            new HashMap()
+                {{
+                    put(DelimiterTokenizingOptions.DELIMITER, " ");
+                }},
+            UTF8Type.instance);
+
+        String testString = "Nip it in the bud";
+        ByteBuffer toAnalyze = ByteBuffer.wrap(testString.getBytes());
+        analyzer.reset(toAnalyze);
+        StringBuilder output = new StringBuilder();
+        while (analyzer.hasNext())
+            output.append(ByteBufferUtil.string(analyzer.next()) + (analyzer.hasNext() ? ' ' : ""));
+
+        Assert.assertEquals(testString, output.toString());
+        Assert.assertFalse(testString.toLowerCase().equals(output.toString()));
+    }
+
+    @Test
+    public void testBlankEntries() throws Exception
+    {
+        DelimiterAnalyzer analyzer = new DelimiterAnalyzer();
+
+        analyzer.init(
+            new HashMap()
+                {{
+                    put(DelimiterTokenizingOptions.DELIMITER, ",");
+                }},
+            UTF8Type.instance);
+
+        String testString = ",Nip,,,,it,,,in,,the,bud,,,";
+        ByteBuffer toAnalyze = ByteBuffer.wrap(testString.getBytes());
+        analyzer.reset(toAnalyze);
+        StringBuilder output = new StringBuilder();
+        while (analyzer.hasNext())
+            output.append(ByteBufferUtil.string(analyzer.next()) + (analyzer.hasNext() ? ',' : ""));
+
+        Assert.assertEquals("Nip,it,in,the,bud", output.toString());
+        Assert.assertFalse(testString.toLowerCase().equals(output.toString()));
+    }
+
+    @Test(expected = ConfigurationException.class)
+    public void ensureIncompatibleInputOnCollectionTypeSkipped()
+    {
+        new DelimiterAnalyzer().validate(Collections.emptyMap(),
+                                         ColumnDefinition.regularDef("a", "b", "c", SetType.getInstance(UTF8Type.instance, true)));
+    }
+
+    @Test(expected = ConfigurationException.class)
+    public void ensureIncompatibleInputSkipped()
+    {
+        new DelimiterAnalyzer().validate(Collections.emptyMap(),
+                                         ColumnDefinition.regularDef("a", "b", "c", Int32Type.instance));
+    }
+
+    @Test
+    public void testTokenizationLoremIpsum() throws Exception
+    {
+        ByteBuffer bb = ByteBuffer.wrap(IOUtils.toByteArray(
+                DelimiterAnalyzerTest.class.getClassLoader().getResourceAsStream("tokenization/lorem_ipsum.txt")));
+
+        DelimiterAnalyzer tokenizer = new DelimiterAnalyzer();
+
+        tokenizer.init(
+            new HashMap()
+                {{
+                    put(DelimiterTokenizingOptions.DELIMITER, " ");
+                }},
+            UTF8Type.instance);
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        tokenizer.reset(bb);
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(69, tokens.size());
+
+    }
+
+    @Test
+    public void testTokenizationJaJp1() throws Exception
+    {
+        ByteBuffer bb = ByteBuffer.wrap(IOUtils.toByteArray(
+                DelimiterAnalyzerTest.class.getClassLoader().getResourceAsStream("tokenization/ja_jp_1.txt")));
+
+        DelimiterAnalyzer tokenizer = new DelimiterAnalyzer();
+
+        tokenizer.init(
+            new HashMap()
+                {{
+                    put(DelimiterTokenizingOptions.DELIMITER, "。");
+                }},
+            UTF8Type.instance);
+
+        tokenizer.reset(bb);
+        List<ByteBuffer> tokens = new ArrayList<>();
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(4, tokens.size());
+    }
+
+    @Test
+    public void testTokenizationJaJp2() throws Exception
+    {
+        ByteBuffer bb = ByteBuffer.wrap(IOUtils.toByteArray(
+                DelimiterAnalyzerTest.class.getClassLoader().getResourceAsStream("tokenization/ja_jp_2.txt")));
+
+        DelimiterAnalyzer tokenizer = new DelimiterAnalyzer();
+
+        tokenizer.init(
+            new HashMap()
+                {{
+                    put(DelimiterTokenizingOptions.DELIMITER, "。");
+                }},
+            UTF8Type.instance);
+
+        tokenizer.reset(bb);
+        List<ByteBuffer> tokens = new ArrayList<>();
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(2, tokens.size());
+    }
+
+    @Test
+    public void testTokenizationRuRu1() throws Exception
+    {
+        ByteBuffer bb = ByteBuffer.wrap(IOUtils.toByteArray(
+                DelimiterAnalyzerTest.class.getClassLoader().getResourceAsStream("tokenization/ru_ru_1.txt")));
+
+        DelimiterAnalyzer tokenizer = new DelimiterAnalyzer();
+
+        tokenizer.init(
+            new HashMap()
+                {{
+                    put(DelimiterTokenizingOptions.DELIMITER, " ");
+                }},
+            UTF8Type.instance);
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        tokenizer.reset(bb);
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(447, tokens.size());
+    }
+
+    @Test
+    public void testTokenizationZnTw1() throws Exception
+    {
+        ByteBuffer bb = ByteBuffer.wrap(IOUtils.toByteArray(
+                DelimiterAnalyzerTest.class.getClassLoader().getResourceAsStream("tokenization/zn_tw_1.txt")));
+
+        DelimiterAnalyzer tokenizer = new DelimiterAnalyzer();
+
+        tokenizer.init(
+            new HashMap()
+                {{
+                    put(DelimiterTokenizingOptions.DELIMITER, " ");
+                }},
+            UTF8Type.instance);
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        tokenizer.reset(bb);
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(403, tokens.size());
+    }
+
+    @Test
+    public void testTokenizationAdventuresOfHuckFinn() throws Exception
+    {
+        ByteBuffer bb = ByteBuffer.wrap(IOUtils.toByteArray(
+                DelimiterAnalyzerTest.class.getClassLoader().getResourceAsStream("tokenization/adventures_of_huckleberry_finn_mark_twain.txt")));
+
+        DelimiterAnalyzer tokenizer = new DelimiterAnalyzer();
+
+        tokenizer.init(
+            new HashMap()
+                {{
+                    put(DelimiterTokenizingOptions.DELIMITER, " ");
+                }},
+            UTF8Type.instance);
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        tokenizer.reset(bb);
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(104594, tokens.size());
+    }
+
+    @Test
+    public void testWorldCities() throws Exception
+    {
+        ByteBuffer bb = ByteBuffer.wrap(IOUtils.toByteArray(
+                DelimiterAnalyzerTest.class.getClassLoader().getResourceAsStream("tokenization/world_cities_a.csv")));
+
+        DelimiterAnalyzer tokenizer = new DelimiterAnalyzer();
+
+        tokenizer.init(
+            new HashMap()
+                {{
+                    put(DelimiterTokenizingOptions.DELIMITER, ",");
+                }},
+            UTF8Type.instance);
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        tokenizer.reset(bb);
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(122265, tokens.size());
+    }
+
+    @Test
+    public void tokenizeDomainNamesAndUrls() throws Exception
+    {
+        ByteBuffer bb = ByteBuffer.wrap(IOUtils.toByteArray(
+                DelimiterAnalyzerTest.class.getClassLoader().getResourceAsStream("tokenization/top_visited_domains.txt")));
+
+        DelimiterAnalyzer tokenizer = new DelimiterAnalyzer();
+
+        tokenizer.init(
+            new HashMap()
+                {{
+                    put(DelimiterTokenizingOptions.DELIMITER, " ");
+                }},
+            UTF8Type.instance);
+
+        tokenizer.reset(bb);
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(12, tokens.size());
+    }
+
+    @Test
+    public void testReuseAndResetTokenizerInstance() throws Exception
+    {
+        List<ByteBuffer> bbToTokenize = new ArrayList<>();
+        bbToTokenize.add(ByteBuffer.wrap("Nip it in the bud".getBytes()));
+        bbToTokenize.add(ByteBuffer.wrap("I couldn’t care less".getBytes()));
+        bbToTokenize.add(ByteBuffer.wrap("One and the same".getBytes()));
+        bbToTokenize.add(ByteBuffer.wrap("The squeaky wheel gets the grease.".getBytes()));
+        bbToTokenize.add(ByteBuffer.wrap("The pen is mightier than the sword.".getBytes()));
+
+        DelimiterAnalyzer tokenizer = new DelimiterAnalyzer();
+
+        tokenizer.init(
+            new HashMap()
+                {{
+                    put(DelimiterTokenizingOptions.DELIMITER, " ");
+                }},
+            UTF8Type.instance);
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        for (ByteBuffer bb : bbToTokenize)
+        {
+            tokenizer.reset(bb);
+            while (tokenizer.hasNext())
+                tokens.add(tokenizer.next());
+        }
+        assertEquals(26, tokens.size());
+    }
+
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/analyzer/NonTokenizingAnalyzerTest.java b/test/unit/org/apache/cassandra/index/sasi/analyzer/NonTokenizingAnalyzerTest.java
new file mode 100644
index 0000000..ba67853
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/analyzer/NonTokenizingAnalyzerTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.index.sasi.analyzer;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Tests for the non-tokenizing analyzer
+ */
+public class NonTokenizingAnalyzerTest
+{
+    @Test
+    public void caseInsensitiveAnalizer() throws Exception
+    {
+        NonTokenizingAnalyzer analyzer = new NonTokenizingAnalyzer();
+        NonTokenizingOptions options = NonTokenizingOptions.getDefaultOptions();
+        options.setCaseSensitive(false);
+        analyzer.init(options, UTF8Type.instance);
+
+        String testString = "Nip it in the bud";
+        ByteBuffer toAnalyze = ByteBuffer.wrap(testString.getBytes());
+        analyzer.reset(toAnalyze);
+        ByteBuffer analyzed = null;
+        while (analyzer.hasNext())
+            analyzed = analyzer.next();
+        Assert.assertTrue(testString.toLowerCase().equals(ByteBufferUtil.string(analyzed)));
+    }
+
+    @Test
+    public void caseSensitiveAnalizer() throws Exception
+    {
+        NonTokenizingAnalyzer analyzer = new NonTokenizingAnalyzer();
+        NonTokenizingOptions options = NonTokenizingOptions.getDefaultOptions();
+        analyzer.init(options, UTF8Type.instance);
+
+        String testString = "Nip it in the bud";
+        ByteBuffer toAnalyze = ByteBuffer.wrap(testString.getBytes());
+        analyzer.reset(toAnalyze);
+        ByteBuffer analyzed = null;
+        while (analyzer.hasNext())
+            analyzed = analyzer.next();
+        Assert.assertFalse(testString.toLowerCase().equals(ByteBufferUtil.string(analyzed)));
+    }
+
+    @Test
+    public void ensureIncompatibleInputSkipped() throws Exception
+    {
+        NonTokenizingAnalyzer analyzer = new NonTokenizingAnalyzer();
+        NonTokenizingOptions options = NonTokenizingOptions.getDefaultOptions();
+        analyzer.init(options, Int32Type.instance);
+
+        ByteBuffer toAnalyze = ByteBufferUtil.bytes(1);
+        analyzer.reset(toAnalyze);
+        Assert.assertTrue(!analyzer.hasNext());
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzerTest.java b/test/unit/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzerTest.java
new file mode 100644
index 0000000..7a88a3d
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzerTest.java
@@ -0,0 +1,227 @@
+/*
+ * 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.index.sasi.analyzer;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import org.junit.Test;
+
+import org.apache.cassandra.serializers.UTF8Serializer;
+
+import static org.junit.Assert.assertEquals;
+
+public class StandardAnalyzerTest
+{
+    @Test
+    public void testTokenizationAscii() throws Exception
+    {
+        InputStream is = StandardAnalyzerTest.class.getClassLoader()
+                .getResourceAsStream("tokenization/apache_license_header.txt");
+
+        StandardTokenizerOptions options = new StandardTokenizerOptions.OptionsBuilder()
+                .maxTokenLength(5).build();
+        StandardAnalyzer tokenizer = new StandardAnalyzer();
+        tokenizer.init(options);
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        tokenizer.reset(is);
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(67, tokens.size());
+    }
+
+    @Test
+    public void testTokenizationLoremIpsum() throws Exception
+    {
+        InputStream is = StandardAnalyzerTest.class.getClassLoader()
+                .getResourceAsStream("tokenization/lorem_ipsum.txt");
+
+        StandardAnalyzer tokenizer = new StandardAnalyzer();
+        tokenizer.init(StandardTokenizerOptions.getDefaultOptions());
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        tokenizer.reset(is);
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(62, tokens.size());
+
+    }
+
+    @Test
+    public void testTokenizationJaJp1() throws Exception
+    {
+        InputStream is = StandardAnalyzerTest.class.getClassLoader()
+                .getResourceAsStream("tokenization/ja_jp_1.txt");
+
+        StandardAnalyzer tokenizer = new StandardAnalyzer();
+        tokenizer.init(StandardTokenizerOptions.getDefaultOptions());
+
+        tokenizer.reset(is);
+        List<ByteBuffer> tokens = new ArrayList<>();
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(210, tokens.size());
+    }
+
+    @Test
+    public void testTokenizationJaJp2() throws Exception
+    {
+        InputStream is = StandardAnalyzerTest.class.getClassLoader()
+                .getResourceAsStream("tokenization/ja_jp_2.txt");
+
+        StandardTokenizerOptions options = new StandardTokenizerOptions.OptionsBuilder().stemTerms(true)
+                .ignoreStopTerms(true).alwaysLowerCaseTerms(true).build();
+        StandardAnalyzer tokenizer = new StandardAnalyzer();
+        tokenizer.init(options);
+
+        tokenizer.reset(is);
+        List<ByteBuffer> tokens = new ArrayList<>();
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(57, tokens.size());
+    }
+
+    @Test
+    public void testTokenizationRuRu1() throws Exception
+    {
+        InputStream is = StandardAnalyzerTest.class.getClassLoader()
+                .getResourceAsStream("tokenization/ru_ru_1.txt");
+        StandardAnalyzer tokenizer = new StandardAnalyzer();
+        tokenizer.init(StandardTokenizerOptions.getDefaultOptions());
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        tokenizer.reset(is);
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(456, tokens.size());
+    }
+
+    @Test
+    public void testTokenizationZnTw1() throws Exception
+    {
+        InputStream is = StandardAnalyzerTest.class.getClassLoader()
+                .getResourceAsStream("tokenization/zn_tw_1.txt");
+        StandardAnalyzer tokenizer = new StandardAnalyzer();
+        tokenizer.init(StandardTokenizerOptions.getDefaultOptions());
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        tokenizer.reset(is);
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(963, tokens.size());
+    }
+
+    @Test
+    public void testTokenizationAdventuresOfHuckFinn() throws Exception
+    {
+        InputStream is = StandardAnalyzerTest.class.getClassLoader()
+                .getResourceAsStream("tokenization/adventures_of_huckleberry_finn_mark_twain.txt");
+
+        StandardTokenizerOptions options = new StandardTokenizerOptions.OptionsBuilder().stemTerms(true)
+                .ignoreStopTerms(true).useLocale(Locale.ENGLISH)
+                .alwaysLowerCaseTerms(true).build();
+        StandardAnalyzer tokenizer = new StandardAnalyzer();
+        tokenizer.init(options);
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        tokenizer.reset(is);
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(37739, tokens.size());
+    }
+
+    @Test
+    public void testSkipStopWordBeforeStemmingFrench() throws Exception
+    {
+        InputStream is = StandardAnalyzerTest.class.getClassLoader()
+               .getResourceAsStream("tokenization/french_skip_stop_words_before_stemming.txt");
+
+        StandardTokenizerOptions options = new StandardTokenizerOptions.OptionsBuilder().stemTerms(true)
+                .ignoreStopTerms(true).useLocale(Locale.FRENCH)
+                .alwaysLowerCaseTerms(true).build();
+        StandardAnalyzer tokenizer = new StandardAnalyzer();
+        tokenizer.init(options);
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        List<String> words = new ArrayList<>();
+        tokenizer.reset(is);
+        while (tokenizer.hasNext())
+        {
+            final ByteBuffer nextToken = tokenizer.next();
+            tokens.add(nextToken);
+            words.add(UTF8Serializer.instance.deserialize(nextToken.duplicate()));
+        }
+
+        assertEquals(4, tokens.size());
+        assertEquals("dans", words.get(0));
+        assertEquals("plui", words.get(1));
+        assertEquals("chanson", words.get(2));
+        assertEquals("connu", words.get(3));
+    }
+
+    @Test
+    public void tokenizeDomainNamesAndUrls() throws Exception
+    {
+        InputStream is = StandardAnalyzerTest.class.getClassLoader()
+                .getResourceAsStream("tokenization/top_visited_domains.txt");
+
+        StandardAnalyzer tokenizer = new StandardAnalyzer();
+        tokenizer.init(StandardTokenizerOptions.getDefaultOptions());
+        tokenizer.reset(is);
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        while (tokenizer.hasNext())
+            tokens.add(tokenizer.next());
+
+        assertEquals(15, tokens.size());
+    }
+
+    @Test
+    public void testReuseAndResetTokenizerInstance() throws Exception
+    {
+        List<ByteBuffer> bbToTokenize = new ArrayList<>();
+        bbToTokenize.add(ByteBuffer.wrap("Nip it in the bud".getBytes()));
+        bbToTokenize.add(ByteBuffer.wrap("I couldn’t care less".getBytes()));
+        bbToTokenize.add(ByteBuffer.wrap("One and the same".getBytes()));
+        bbToTokenize.add(ByteBuffer.wrap("The squeaky wheel gets the grease.".getBytes()));
+        bbToTokenize.add(ByteBuffer.wrap("The pen is mightier than the sword.".getBytes()));
+
+        StandardAnalyzer tokenizer = new StandardAnalyzer();
+        tokenizer.init(StandardTokenizerOptions.getDefaultOptions());
+
+        List<ByteBuffer> tokens = new ArrayList<>();
+        for (ByteBuffer bb : bbToTokenize)
+        {
+            tokenizer.reset(bb);
+            while (tokenizer.hasNext())
+                tokens.add(tokenizer.next());
+        }
+        assertEquals(10, tokens.size());
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/conf/IndexModeTest.java b/test/unit/org/apache/cassandra/index/sasi/conf/IndexModeTest.java
new file mode 100644
index 0000000..9f93f27
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/conf/IndexModeTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.index.sasi.conf;
+
+import java.util.Collections;
+import java.util.Map;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.marshal.AsciiType;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.Mode;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+
+public class IndexModeTest
+{
+
+    private final CFMetaData cfm = CFMetaData.Builder.create("ks", "cf")
+                        .addPartitionKey("pkey", AsciiType.instance)
+                        .build();
+
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
+    @Test
+    public void test_notIndexed()
+    {
+        Assert.assertEquals(IndexMode.NOT_INDEXED, IndexMode.getMode(null, (Map)null));
+        Assert.assertEquals(IndexMode.NOT_INDEXED, IndexMode.getMode(null, Collections.EMPTY_MAP));
+    }
+
+    @Test(expected = ConfigurationException.class)
+    public void test_bad_mode_option()
+    {
+        IndexMode.getMode(null, Collections.singletonMap("mode", "invalid"));
+    }
+
+    @Test
+    public void test_asciiType()
+    {
+        ColumnDefinition cd = ColumnDefinition.regularDef(cfm, ByteBufferUtil.bytes("TestColumnDefinition"), AsciiType.instance);
+
+        IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("something", "nothing"));
+        Assert.assertNull(result.analyzerClass);
+        Assert.assertFalse(result.isAnalyzed);
+        Assert.assertTrue(result.isLiteral);
+        Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes);
+        Assert.assertEquals(Mode.PREFIX, result.mode);
+    }
+
+    @Test
+    public void test_asciiType_notLiteral()
+    {
+        ColumnDefinition cd = ColumnDefinition.regularDef(cfm, ByteBufferUtil.bytes("TestColumnDefinition"), AsciiType.instance);
+
+        IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("is_literal", "false"));
+        Assert.assertNull(result.analyzerClass);
+        Assert.assertFalse(result.isAnalyzed);
+        Assert.assertFalse(result.isLiteral);
+        Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes);
+        Assert.assertEquals(Mode.PREFIX, result.mode);
+    }
+
+    @Test
+    public void test_asciiType_errLiteral()
+    {
+        ColumnDefinition cd = ColumnDefinition.regularDef(cfm, ByteBufferUtil.bytes("TestColumnDefinition"), AsciiType.instance);
+
+        IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("is_literal", "junk"));
+        Assert.assertNull(result.analyzerClass);
+        Assert.assertFalse(result.isAnalyzed);
+        Assert.assertFalse(result.isLiteral);
+        Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes);
+        Assert.assertEquals(Mode.PREFIX, result.mode);
+    }
+
+    @Test
+    public void test_asciiType_analyzed()
+    {
+        ColumnDefinition cd = ColumnDefinition.regularDef(cfm, ByteBufferUtil.bytes("TestColumnDefinition"), AsciiType.instance);
+
+        IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("analyzed", "true"));
+        Assert.assertNull(result.analyzerClass);
+        Assert.assertTrue(result.isAnalyzed);
+        Assert.assertTrue(result.isLiteral);
+        Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes);
+        Assert.assertEquals(Mode.PREFIX, result.mode);
+    }
+
+    @Test
+    public void test_utf8Type()
+    {
+        ColumnDefinition cd = ColumnDefinition.regularDef(cfm, ByteBufferUtil.bytes("TestColumnDefinition"), UTF8Type.instance);
+
+        IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("something", "nothing"));
+        Assert.assertNull(result.analyzerClass);
+        Assert.assertFalse(result.isAnalyzed);
+        Assert.assertTrue(result.isLiteral);
+        Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes);
+        Assert.assertEquals(Mode.PREFIX, result.mode);
+    }
+
+    @Test
+    public void test_bytesType()
+    {
+        ColumnDefinition cd = ColumnDefinition.regularDef(cfm, ByteBufferUtil.bytes("TestColumnDefinition"), BytesType.instance);
+
+        IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("something", "nothing"));
+        Assert.assertNull(result.analyzerClass);
+        Assert.assertFalse(result.isAnalyzed);
+        Assert.assertFalse(result.isLiteral);
+        Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes);
+        Assert.assertEquals(Mode.PREFIX, result.mode);
+    }
+
+    @Test
+    public void test_bytesType_isLiteral()
+    {
+        ColumnDefinition cd = ColumnDefinition.regularDef(cfm, ByteBufferUtil.bytes("TestColumnDefinition"), BytesType.instance);
+
+        IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("is_literal", "true"));
+        Assert.assertNull(result.analyzerClass);
+        Assert.assertFalse(result.isAnalyzed);
+        Assert.assertTrue(result.isLiteral);
+        Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes);
+        Assert.assertEquals(Mode.PREFIX, result.mode);
+    }
+
+    @Test
+    public void test_bytesType_errLiteral()
+    {
+        ColumnDefinition cd = ColumnDefinition.regularDef(cfm, ByteBufferUtil.bytes("TestColumnDefinition"), BytesType.instance);
+
+        IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("is_literal", "junk"));
+        Assert.assertNull(result.analyzerClass);
+        Assert.assertFalse(result.isAnalyzed);
+        Assert.assertFalse(result.isLiteral);
+        Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes);
+        Assert.assertEquals(Mode.PREFIX, result.mode);
+    }
+
+    @Test
+    public void test_bytesType_analyzed()
+    {
+        ColumnDefinition cd = ColumnDefinition.regularDef(cfm, ByteBufferUtil.bytes("TestColumnDefinition"), BytesType.instance);
+
+        IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("analyzed", "true"));
+        Assert.assertNull(result.analyzerClass);
+        Assert.assertTrue(result.isAnalyzed);
+        Assert.assertFalse(result.isLiteral);
+        Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes);
+        Assert.assertEquals(Mode.PREFIX, result.mode);
+    }
+
+    @Test
+    public void test_bytesType_analyzer()
+    {
+        ColumnDefinition cd = ColumnDefinition.regularDef(cfm, ByteBufferUtil.bytes("TestColumnDefinition"), BytesType.instance);
+
+        IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("analyzer_class", "java.lang.Object"));
+        Assert.assertEquals(Object.class, result.analyzerClass);
+        Assert.assertTrue(result.isAnalyzed);
+        Assert.assertFalse(result.isLiteral);
+        Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes);
+        Assert.assertEquals(Mode.PREFIX, result.mode);
+    }
+
+    @Test
+    public void test_bytesType_analyzer_unanalyzed()
+    {
+        ColumnDefinition cd = ColumnDefinition.regularDef(cfm, ByteBufferUtil.bytes("TestColumnDefinition"), BytesType.instance);
+
+        IndexMode result = IndexMode.getMode(cd, ImmutableMap.of("analyzer_class", "java.lang.Object",
+                                                                 "analyzed", "false"));
+
+        Assert.assertEquals(Object.class, result.analyzerClass);
+        Assert.assertFalse(result.isAnalyzed);
+        Assert.assertFalse(result.isLiteral);
+        Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes);
+        Assert.assertEquals(Mode.PREFIX, result.mode);
+    }
+
+    @Test
+    public void test_bytesType_maxCompactionFlushMemoryInBytes()
+    {
+        ColumnDefinition cd = ColumnDefinition.regularDef(cfm, ByteBufferUtil.bytes("TestColumnDefinition"), BytesType.instance);
+
+        IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("max_compaction_flush_memory_in_mb", "1"));
+        Assert.assertNull(result.analyzerClass);
+        Assert.assertFalse(result.isAnalyzed);
+        Assert.assertFalse(result.isLiteral);
+        Assert.assertEquals(1048576L, result.maxCompactionFlushMemoryInBytes);
+        Assert.assertEquals(Mode.PREFIX, result.mode);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/disk/OnDiskIndexTest.java b/test/unit/org/apache/cassandra/index/sasi/disk/OnDiskIndexTest.java
new file mode 100644
index 0000000..10dc7a8
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/disk/OnDiskIndexTest.java
@@ -0,0 +1,926 @@
+/*
+ * 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.index.sasi.disk;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.db.BufferDecoratedKey;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.index.sasi.plan.Expression;
+import org.apache.cassandra.index.sasi.utils.CombinedTerm;
+import org.apache.cassandra.index.sasi.utils.CombinedTermIterator;
+import org.apache.cassandra.index.sasi.utils.OnDiskIndexIterator;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.utils.MurmurHash;
+import org.apache.cassandra.utils.Pair;
+
+import com.carrotsearch.hppc.LongSet;
+import com.carrotsearch.hppc.cursors.LongCursor;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Sets;
+
+import junit.framework.Assert;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class OnDiskIndexTest
+{
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
+    @Test
+    public void testStringSAConstruction() throws Exception
+    {
+        Map<ByteBuffer, TokenTreeBuilder> data = new HashMap<ByteBuffer, TokenTreeBuilder>()
+        {{
+                put(UTF8Type.instance.decompose("scat"), keyBuilder(1L));
+                put(UTF8Type.instance.decompose("mat"),  keyBuilder(2L));
+                put(UTF8Type.instance.decompose("fat"),  keyBuilder(3L));
+                put(UTF8Type.instance.decompose("cat"),  keyBuilder(1L, 4L));
+                put(UTF8Type.instance.decompose("till"), keyBuilder(2L, 6L));
+                put(UTF8Type.instance.decompose("bill"), keyBuilder(5L));
+                put(UTF8Type.instance.decompose("foo"),  keyBuilder(7L));
+                put(UTF8Type.instance.decompose("bar"),  keyBuilder(9L, 10L));
+                put(UTF8Type.instance.decompose("michael"), keyBuilder(11L, 12L, 1L));
+                put(UTF8Type.instance.decompose("am"), keyBuilder(15L));
+        }};
+
+        OnDiskIndexBuilder builder = new OnDiskIndexBuilder(UTF8Type.instance, UTF8Type.instance, OnDiskIndexBuilder.Mode.CONTAINS);
+        for (Map.Entry<ByteBuffer, TokenTreeBuilder> e : data.entrySet())
+            addAll(builder, e.getKey(), e.getValue());
+
+        File index = File.createTempFile("on-disk-sa-string", "db");
+        index.deleteOnExit();
+
+        builder.finish(index);
+
+        OnDiskIndex onDisk = new OnDiskIndex(index, UTF8Type.instance, new KeyConverter());
+
+        // first check if we can find exact matches
+        for (Map.Entry<ByteBuffer, TokenTreeBuilder> e : data.entrySet())
+        {
+            if (UTF8Type.instance.getString(e.getKey()).equals("cat"))
+                continue; // cat is embedded into scat, we'll test it in next section
+
+            Assert.assertEquals("Key was: " + UTF8Type.instance.compose(e.getKey()), convert(e.getValue()), convert(onDisk.search(expressionFor(UTF8Type.instance, e.getKey()))));
+        }
+
+        // check that cat returns positions for scat & cat
+        Assert.assertEquals(convert(1, 4), convert(onDisk.search(expressionFor("cat"))));
+
+        // random suffix queries
+        Assert.assertEquals(convert(9, 10), convert(onDisk.search(expressionFor("ar"))));
+        Assert.assertEquals(convert(1, 2, 3, 4), convert(onDisk.search(expressionFor("at"))));
+        Assert.assertEquals(convert(1, 11, 12), convert(onDisk.search(expressionFor("mic"))));
+        Assert.assertEquals(convert(1, 11, 12), convert(onDisk.search(expressionFor("ae"))));
+        Assert.assertEquals(convert(2, 5, 6), convert(onDisk.search(expressionFor("ll"))));
+        Assert.assertEquals(convert(1, 2, 5, 6, 11, 12), convert(onDisk.search(expressionFor("l"))));
+        Assert.assertEquals(convert(7), convert(onDisk.search(expressionFor("oo"))));
+        Assert.assertEquals(convert(7), convert(onDisk.search(expressionFor("o"))));
+        Assert.assertEquals(convert(1, 2, 3, 4, 6), convert(onDisk.search(expressionFor("t"))));
+        Assert.assertEquals(convert(1, 2, 11, 12), convert(onDisk.search(expressionFor("m", Operator.LIKE_PREFIX))));
+
+        Assert.assertEquals(Collections.<DecoratedKey>emptySet(), convert(onDisk.search(expressionFor("hello"))));
+
+        onDisk.close();
+    }
+
+    @Test
+    public void testIntegerSAConstruction() throws Exception
+    {
+        final Map<ByteBuffer, TokenTreeBuilder> data = new HashMap<ByteBuffer, TokenTreeBuilder>()
+        {{
+                put(Int32Type.instance.decompose(5),  keyBuilder(1L));
+                put(Int32Type.instance.decompose(7),  keyBuilder(2L));
+                put(Int32Type.instance.decompose(1),  keyBuilder(3L));
+                put(Int32Type.instance.decompose(3),  keyBuilder(1L, 4L));
+                put(Int32Type.instance.decompose(8),  keyBuilder(2L, 6L));
+                put(Int32Type.instance.decompose(10), keyBuilder(5L));
+                put(Int32Type.instance.decompose(6),  keyBuilder(7L));
+                put(Int32Type.instance.decompose(4),  keyBuilder(9L, 10L));
+                put(Int32Type.instance.decompose(0),  keyBuilder(11L, 12L, 1L));
+        }};
+
+        OnDiskIndexBuilder builder = new OnDiskIndexBuilder(UTF8Type.instance, Int32Type.instance, OnDiskIndexBuilder.Mode.PREFIX);
+        for (Map.Entry<ByteBuffer, TokenTreeBuilder> e : data.entrySet())
+            addAll(builder, e.getKey(), e.getValue());
+
+        File index = File.createTempFile("on-disk-sa-int", "db");
+        index.deleteOnExit();
+
+        builder.finish(index);
+
+        OnDiskIndex onDisk = new OnDiskIndex(index, Int32Type.instance, new KeyConverter());
+
+        for (Map.Entry<ByteBuffer, TokenTreeBuilder> e : data.entrySet())
+        {
+            Assert.assertEquals(convert(e.getValue()), convert(onDisk.search(expressionFor(Operator.EQ, Int32Type.instance, e.getKey()))));
+        }
+
+        List<ByteBuffer> sortedNumbers = new ArrayList<ByteBuffer>()
+        {{
+            addAll(data.keySet().stream().collect(Collectors.toList()));
+        }};
+
+        Collections.sort(sortedNumbers, Int32Type.instance::compare);
+
+        // test full iteration
+        int idx = 0;
+        for (OnDiskIndex.DataTerm term : onDisk)
+        {
+            ByteBuffer number = sortedNumbers.get(idx++);
+            Assert.assertEquals(number, term.getTerm());
+            Assert.assertEquals(convert(data.get(number)), convert(term.getTokens()));
+        }
+
+        // test partial iteration (descending)
+        idx = 3; // start from the 3rd element
+        Iterator<OnDiskIndex.DataTerm> partialIter = onDisk.iteratorAt(sortedNumbers.get(idx), OnDiskIndex.IteratorOrder.DESC, true);
+        while (partialIter.hasNext())
+        {
+            OnDiskIndex.DataTerm term = partialIter.next();
+            ByteBuffer number = sortedNumbers.get(idx++);
+
+            Assert.assertEquals(number, term.getTerm());
+            Assert.assertEquals(convert(data.get(number)), convert(term.getTokens()));
+        }
+
+        idx = 3; // start from the 3rd element exclusive
+        partialIter = onDisk.iteratorAt(sortedNumbers.get(idx++), OnDiskIndex.IteratorOrder.DESC, false);
+        while (partialIter.hasNext())
+        {
+            OnDiskIndex.DataTerm term = partialIter.next();
+            ByteBuffer number = sortedNumbers.get(idx++);
+
+            Assert.assertEquals(number, term.getTerm());
+            Assert.assertEquals(convert(data.get(number)), convert(term.getTokens()));
+        }
+
+        // test partial iteration (ascending)
+        idx = 6; // start from the 6rd element
+        partialIter = onDisk.iteratorAt(sortedNumbers.get(idx), OnDiskIndex.IteratorOrder.ASC, true);
+        while (partialIter.hasNext())
+        {
+            OnDiskIndex.DataTerm term = partialIter.next();
+            ByteBuffer number = sortedNumbers.get(idx--);
+
+            Assert.assertEquals(number, term.getTerm());
+            Assert.assertEquals(convert(data.get(number)), convert(term.getTokens()));
+        }
+
+        idx = 6; // start from the 6rd element exclusive
+        partialIter = onDisk.iteratorAt(sortedNumbers.get(idx--), OnDiskIndex.IteratorOrder.ASC, false);
+        while (partialIter.hasNext())
+        {
+            OnDiskIndex.DataTerm term = partialIter.next();
+            ByteBuffer number = sortedNumbers.get(idx--);
+
+            Assert.assertEquals(number, term.getTerm());
+            Assert.assertEquals(convert(data.get(number)), convert(term.getTokens()));
+        }
+
+        onDisk.close();
+
+        List<ByteBuffer> iterCheckNums = new ArrayList<ByteBuffer>()
+        {{
+            add(Int32Type.instance.decompose(3));
+            add(Int32Type.instance.decompose(9));
+            add(Int32Type.instance.decompose(14));
+            add(Int32Type.instance.decompose(42));
+        }};
+
+        OnDiskIndexBuilder iterTest = new OnDiskIndexBuilder(UTF8Type.instance, Int32Type.instance, OnDiskIndexBuilder.Mode.PREFIX);
+        for (int i = 0; i < iterCheckNums.size(); i++)
+            iterTest.add(iterCheckNums.get(i), keyAt((long) i), i);
+
+        File iterIndex = File.createTempFile("sa-iter", ".db");
+        iterIndex.deleteOnExit();
+
+        iterTest.finish(iterIndex);
+
+        onDisk = new OnDiskIndex(iterIndex, Int32Type.instance, new KeyConverter());
+
+        ByteBuffer number = Int32Type.instance.decompose(1);
+        Assert.assertEquals(0, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.ASC, false)));
+        Assert.assertEquals(0, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.ASC, true)));
+        Assert.assertEquals(4, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.DESC, false)));
+        Assert.assertEquals(4, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.DESC, true)));
+
+        number = Int32Type.instance.decompose(44);
+        Assert.assertEquals(4, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.ASC, false)));
+        Assert.assertEquals(4, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.ASC, true)));
+        Assert.assertEquals(0, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.DESC, false)));
+        Assert.assertEquals(0, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.DESC, true)));
+
+        number = Int32Type.instance.decompose(20);
+        Assert.assertEquals(3, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.ASC, false)));
+        Assert.assertEquals(3, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.ASC, true)));
+        Assert.assertEquals(1, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.DESC, false)));
+        Assert.assertEquals(1, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.DESC, true)));
+
+        number = Int32Type.instance.decompose(5);
+        Assert.assertEquals(1, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.ASC, false)));
+        Assert.assertEquals(1, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.ASC, true)));
+        Assert.assertEquals(3, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.DESC, false)));
+        Assert.assertEquals(3, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.DESC, true)));
+
+        number = Int32Type.instance.decompose(10);
+        Assert.assertEquals(2, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.ASC, false)));
+        Assert.assertEquals(2, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.ASC, true)));
+        Assert.assertEquals(2, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.DESC, false)));
+        Assert.assertEquals(2, Iterators.size(onDisk.iteratorAt(number, OnDiskIndex.IteratorOrder.DESC, true)));
+
+        onDisk.close();
+    }
+
+    @Test
+    public void testMultiSuffixMatches() throws Exception
+    {
+        OnDiskIndexBuilder builder = new OnDiskIndexBuilder(UTF8Type.instance, UTF8Type.instance, OnDiskIndexBuilder.Mode.CONTAINS)
+        {{
+                addAll(this, UTF8Type.instance.decompose("Eliza"), keyBuilder(1L, 2L));
+                addAll(this, UTF8Type.instance.decompose("Elizabeth"), keyBuilder(3L, 4L));
+                addAll(this, UTF8Type.instance.decompose("Aliza"), keyBuilder(5L, 6L));
+                addAll(this, UTF8Type.instance.decompose("Taylor"), keyBuilder(7L, 8L));
+                addAll(this, UTF8Type.instance.decompose("Pavel"), keyBuilder(9L, 10L));
+        }};
+
+        File index = File.createTempFile("on-disk-sa-multi-suffix-match", ".db");
+        index.deleteOnExit();
+
+        builder.finish(index);
+
+        OnDiskIndex onDisk = new OnDiskIndex(index, UTF8Type.instance, new KeyConverter());
+
+        Assert.assertEquals(convert(1, 2, 3, 4, 5, 6), convert(onDisk.search(expressionFor("liz"))));
+        Assert.assertEquals(convert(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), convert(onDisk.search(expressionFor("a"))));
+        Assert.assertEquals(convert(5, 6), convert(onDisk.search(expressionFor("A"))));
+        Assert.assertEquals(convert(1, 2, 3, 4), convert(onDisk.search(expressionFor("E"))));
+        Assert.assertEquals(convert(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), convert(onDisk.search(expressionFor("l"))));
+        Assert.assertEquals(convert(3, 4), convert(onDisk.search(expressionFor("bet"))));
+        Assert.assertEquals(convert(3, 4, 9, 10), convert(onDisk.search(expressionFor("e"))));
+        Assert.assertEquals(convert(7, 8), convert(onDisk.search(expressionFor("yl"))));
+        Assert.assertEquals(convert(7, 8), convert(onDisk.search(expressionFor("T"))));
+        Assert.assertEquals(convert(1, 2, 3, 4, 5, 6), convert(onDisk.search(expressionFor("za"))));
+        Assert.assertEquals(convert(3, 4), convert(onDisk.search(expressionFor("ab"))));
+
+        Assert.assertEquals(Collections.<DecoratedKey>emptySet(), convert(onDisk.search(expressionFor("Pi"))));
+        Assert.assertEquals(Collections.<DecoratedKey>emptySet(), convert(onDisk.search(expressionFor("ethz"))));
+        Assert.assertEquals(Collections.<DecoratedKey>emptySet(), convert(onDisk.search(expressionFor("liw"))));
+        Assert.assertEquals(Collections.<DecoratedKey>emptySet(), convert(onDisk.search(expressionFor("Taw"))));
+        Assert.assertEquals(Collections.<DecoratedKey>emptySet(), convert(onDisk.search(expressionFor("Av"))));
+
+        onDisk.close();
+    }
+
+    @Test
+    public void testSparseMode() throws Exception
+    {
+        OnDiskIndexBuilder builder = new OnDiskIndexBuilder(UTF8Type.instance, LongType.instance, OnDiskIndexBuilder.Mode.SPARSE);
+
+        final long start = System.currentTimeMillis();
+        final int numIterations = 100000;
+
+        for (long i = 0; i < numIterations; i++)
+            builder.add(LongType.instance.decompose(start + i), keyAt(i), i);
+
+        File index = File.createTempFile("on-disk-sa-sparse", "db");
+        index.deleteOnExit();
+
+        builder.finish(index);
+
+        OnDiskIndex onDisk = new OnDiskIndex(index, LongType.instance, new KeyConverter());
+
+        ThreadLocalRandom random = ThreadLocalRandom.current();
+
+        for (long step = start; step < (start + numIterations); step += 1000)
+        {
+            boolean lowerInclusive = random.nextBoolean();
+            boolean upperInclusive = random.nextBoolean();
+
+            long limit = random.nextLong(step, start + numIterations);
+            RangeIterator<Long, Token> rows = onDisk.search(expressionFor(step, lowerInclusive, limit, upperInclusive));
+
+            long lowerKey = step - start;
+            long upperKey = lowerKey + (limit - step);
+
+            if (!lowerInclusive)
+                lowerKey += 1;
+
+            if (upperInclusive)
+                upperKey += 1;
+
+            Set<DecoratedKey> actual = convert(rows);
+            for (long key = lowerKey; key < upperKey; key++)
+                Assert.assertTrue("key" + key + " wasn't found", actual.contains(keyAt(key)));
+
+            Assert.assertEquals((upperKey - lowerKey), actual.size());
+        }
+
+        // let's also explicitly test whole range search
+        RangeIterator<Long, Token> rows = onDisk.search(expressionFor(start, true, start + numIterations, true));
+
+        Set<DecoratedKey> actual = convert(rows);
+        Assert.assertEquals(numIterations, actual.size());
+    }
+
+    @Test
+    public void testNotEqualsQueryForStrings() throws Exception
+    {
+        Map<ByteBuffer, TokenTreeBuilder> data = new HashMap<ByteBuffer, TokenTreeBuilder>()
+        {{
+                put(UTF8Type.instance.decompose("Pavel"),   keyBuilder(1L, 2L));
+                put(UTF8Type.instance.decompose("Jason"),   keyBuilder(3L));
+                put(UTF8Type.instance.decompose("Jordan"),  keyBuilder(4L));
+                put(UTF8Type.instance.decompose("Michael"), keyBuilder(5L, 6L));
+                put(UTF8Type.instance.decompose("Vijay"),   keyBuilder(7L));
+                put(UTF8Type.instance.decompose("Travis"),  keyBuilder(8L));
+                put(UTF8Type.instance.decompose("Aleksey"), keyBuilder(9L, 10L));
+        }};
+
+        OnDiskIndexBuilder builder = new OnDiskIndexBuilder(UTF8Type.instance, UTF8Type.instance, OnDiskIndexBuilder.Mode.PREFIX);
+        for (Map.Entry<ByteBuffer, TokenTreeBuilder> e : data.entrySet())
+            addAll(builder, e.getKey(), e.getValue());
+
+        File index = File.createTempFile("on-disk-sa-except-test", "db");
+        index.deleteOnExit();
+
+        builder.finish(index);
+
+        OnDiskIndex onDisk = new OnDiskIndex(index, UTF8Type.instance, new KeyConverter());
+
+        // test whole words first
+        Assert.assertEquals(convert(3, 4, 5, 6, 7, 8, 9, 10), convert(onDisk.search(expressionForNot("Aleksey", "Vijay", "Pavel"))));
+
+        Assert.assertEquals(convert(3, 4, 7, 8, 9, 10), convert(onDisk.search(expressionForNot("Aleksey", "Vijay", "Pavel", "Michael"))));
+
+        Assert.assertEquals(convert(3, 4, 7, 9, 10), convert(onDisk.search(expressionForNot("Aleksey", "Vijay", "Pavel", "Michael", "Travis"))));
+
+        // now test prefixes
+        Assert.assertEquals(convert(3, 4, 5, 6, 7, 8, 9, 10), convert(onDisk.search(expressionForNot("Aleksey", "Vijay", "Pav"))));
+
+        Assert.assertEquals(convert(3, 4, 7, 8, 9, 10), convert(onDisk.search(expressionForNot("Aleksey", "Vijay", "Pavel", "Mic"))));
+
+        Assert.assertEquals(convert(3, 4, 7, 9, 10), convert(onDisk.search(expressionForNot("Aleksey", "Vijay", "Pavel", "Micha", "Tr"))));
+
+        onDisk.close();
+    }
+
+    @Test
+    public void testNotEqualsQueryForNumbers() throws Exception
+    {
+        final Map<ByteBuffer, TokenTreeBuilder> data = new HashMap<ByteBuffer, TokenTreeBuilder>()
+        {{
+                put(Int32Type.instance.decompose(5),  keyBuilder(1L));
+                put(Int32Type.instance.decompose(7),  keyBuilder(2L));
+                put(Int32Type.instance.decompose(1),  keyBuilder(3L));
+                put(Int32Type.instance.decompose(3),  keyBuilder(1L, 4L));
+                put(Int32Type.instance.decompose(8),  keyBuilder(8L, 6L));
+                put(Int32Type.instance.decompose(10), keyBuilder(5L));
+                put(Int32Type.instance.decompose(6),  keyBuilder(7L));
+                put(Int32Type.instance.decompose(4),  keyBuilder(9L, 10L));
+                put(Int32Type.instance.decompose(0),  keyBuilder(11L, 12L, 1L));
+        }};
+
+        OnDiskIndexBuilder builder = new OnDiskIndexBuilder(UTF8Type.instance, Int32Type.instance, OnDiskIndexBuilder.Mode.PREFIX);
+        for (Map.Entry<ByteBuffer, TokenTreeBuilder> e : data.entrySet())
+            addAll(builder, e.getKey(), e.getValue());
+
+        File index = File.createTempFile("on-disk-sa-except-int-test", "db");
+        index.deleteOnExit();
+
+        builder.finish(index);
+
+        OnDiskIndex onDisk = new OnDiskIndex(index, Int32Type.instance, new KeyConverter());
+
+        Assert.assertEquals(convert(1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12), convert(onDisk.search(expressionForNot(0, 10, 1))));
+        Assert.assertEquals(convert(1, 2, 4, 5, 7, 9, 10, 11, 12), convert(onDisk.search(expressionForNot(0, 10, 1, 8))));
+        Assert.assertEquals(convert(1, 2, 4, 5, 7, 11, 12), convert(onDisk.search(expressionForNot(0, 10, 1, 8, 4))));
+
+        onDisk.close();
+    }
+
+    @Test
+    public void testRangeQueryWithExclusions() throws Exception
+    {
+        final long lower = 0;
+        final long upper = 100000;
+
+        OnDiskIndexBuilder builder = new OnDiskIndexBuilder(UTF8Type.instance, LongType.instance, OnDiskIndexBuilder.Mode.SPARSE);
+        for (long i = lower; i <= upper; i++)
+            builder.add(LongType.instance.decompose(i), keyAt(i), i);
+
+        File index = File.createTempFile("on-disk-sa-except-long-ranges", "db");
+        index.deleteOnExit();
+
+        builder.finish(index);
+
+        OnDiskIndex onDisk = new OnDiskIndex(index, LongType.instance, new KeyConverter());
+
+        ThreadLocalRandom random = ThreadLocalRandom.current();
+
+        // single exclusion
+
+        // let's do small range first to figure out if searchPoint works properly
+        validateExclusions(onDisk, lower, 50, Sets.newHashSet(42L));
+        // now let's do whole data set to test SPARSE searching
+        validateExclusions(onDisk, lower, upper, Sets.newHashSet(31337L));
+
+        // pair of exclusions which would generate a split
+
+        validateExclusions(onDisk, lower, random.nextInt(400, 800), Sets.newHashSet(42L, 154L));
+        validateExclusions(onDisk, lower, upper, Sets.newHashSet(31337L, 54631L));
+
+        // 3 exclusions which would generate a split and change bounds
+
+        validateExclusions(onDisk, lower, random.nextInt(400, 800), Sets.newHashSet(42L, 154L));
+        validateExclusions(onDisk, lower, upper, Sets.newHashSet(31337L, 54631L));
+
+        validateExclusions(onDisk, lower, random.nextLong(400, upper), Sets.newHashSet(42L, 55L));
+        validateExclusions(onDisk, lower, random.nextLong(400, upper), Sets.newHashSet(42L, 55L, 93L));
+        validateExclusions(onDisk, lower, random.nextLong(400, upper), Sets.newHashSet(42L, 55L, 93L, 205L));
+
+        Set<Long> exclusions = Sets.newHashSet(3L, 12L, 13L, 14L, 27L, 54L, 81L, 125L, 384L, 771L, 1054L, 2048L, 78834L);
+
+        // test that exclusions are properly bound by lower/upper of the expression
+        Assert.assertEquals(392, validateExclusions(onDisk, lower, 400, exclusions, false));
+        Assert.assertEquals(101, validateExclusions(onDisk, lower, 100, Sets.newHashSet(-10L, -5L, -1L), false));
+
+        validateExclusions(onDisk, lower, upper, exclusions);
+
+        Assert.assertEquals(100000, convert(onDisk.search(new Expression("", LongType.instance)
+                                                    .add(Operator.NEQ, LongType.instance.decompose(100L)))).size());
+
+        Assert.assertEquals(49, convert(onDisk.search(new Expression("", LongType.instance)
+                                                    .add(Operator.LT, LongType.instance.decompose(50L))
+                                                    .add(Operator.NEQ, LongType.instance.decompose(10L)))).size());
+
+        Assert.assertEquals(99998, convert(onDisk.search(new Expression("", LongType.instance)
+                                                    .add(Operator.GT, LongType.instance.decompose(1L))
+                                                    .add(Operator.NEQ, LongType.instance.decompose(20L)))).size());
+
+        onDisk.close();
+    }
+
+    private void validateExclusions(OnDiskIndex sa, long lower, long upper, Set<Long> exclusions)
+    {
+        validateExclusions(sa, lower, upper, exclusions, true);
+    }
+
+    private int validateExclusions(OnDiskIndex sa, long lower, long upper, Set<Long> exclusions, boolean checkCount)
+    {
+        int count = 0;
+        for (DecoratedKey key : convert(sa.search(rangeWithExclusions(lower, true, upper, true, exclusions))))
+        {
+            String keyId = UTF8Type.instance.getString(key.getKey()).split("key")[1];
+            Assert.assertFalse("key" + keyId + " is present.", exclusions.contains(Long.valueOf(keyId)));
+            count++;
+        }
+
+        if (checkCount)
+            Assert.assertEquals(upper - (lower == 0 ? -1 : lower) - exclusions.size(), count);
+
+        return count;
+    }
+
+    @Test
+    public void testDescriptor() throws Exception
+    {
+        final Map<ByteBuffer, Pair<DecoratedKey, Long>> data = new HashMap<ByteBuffer, Pair<DecoratedKey, Long>>()
+        {{
+                put(Int32Type.instance.decompose(5), Pair.create(keyAt(1L), 1L));
+        }};
+
+        OnDiskIndexBuilder builder1 = new OnDiskIndexBuilder(UTF8Type.instance, Int32Type.instance, OnDiskIndexBuilder.Mode.PREFIX);
+        OnDiskIndexBuilder builder2 = new OnDiskIndexBuilder(UTF8Type.instance, Int32Type.instance, OnDiskIndexBuilder.Mode.PREFIX);
+        for (Map.Entry<ByteBuffer, Pair<DecoratedKey, Long>> e : data.entrySet())
+        {
+            DecoratedKey key = e.getValue().left;
+            Long position = e.getValue().right;
+
+            builder1.add(e.getKey(), key, position);
+            builder2.add(e.getKey(), key, position);
+        }
+
+        File index1 = File.createTempFile("on-disk-sa-int", "db");
+        File index2 = File.createTempFile("on-disk-sa-int2", "db");
+        index1.deleteOnExit();
+        index2.deleteOnExit();
+
+        builder1.finish(index1);
+        builder2.finish(new Descriptor(Descriptor.VERSION_AA), index2);
+
+        OnDiskIndex onDisk1 = new OnDiskIndex(index1, Int32Type.instance, new KeyConverter());
+        OnDiskIndex onDisk2 = new OnDiskIndex(index2, Int32Type.instance, new KeyConverter());
+
+        ByteBuffer number = Int32Type.instance.decompose(5);
+
+        Assert.assertEquals(Collections.singleton(data.get(number).left), convert(onDisk1.search(expressionFor(Operator.EQ, Int32Type.instance, number))));
+        Assert.assertEquals(Collections.singleton(data.get(number).left), convert(onDisk2.search(expressionFor(Operator.EQ, Int32Type.instance, number))));
+
+        Assert.assertEquals(onDisk1.descriptor.version.version, Descriptor.CURRENT_VERSION);
+        Assert.assertEquals(onDisk2.descriptor.version.version, Descriptor.VERSION_AA);
+    }
+
+    @Test
+    public void testSuperBlocks() throws Exception
+    {
+        Map<ByteBuffer, TokenTreeBuilder> terms = new HashMap<>();
+        terms.put(UTF8Type.instance.decompose("1234"), keyBuilder(1L, 2L));
+        terms.put(UTF8Type.instance.decompose("2345"), keyBuilder(3L, 4L));
+        terms.put(UTF8Type.instance.decompose("3456"), keyBuilder(5L, 6L));
+        terms.put(UTF8Type.instance.decompose("4567"), keyBuilder(7L, 8L));
+        terms.put(UTF8Type.instance.decompose("5678"), keyBuilder(9L, 10L));
+
+        OnDiskIndexBuilder builder = new OnDiskIndexBuilder(UTF8Type.instance, Int32Type.instance, OnDiskIndexBuilder.Mode.SPARSE);
+        for (Map.Entry<ByteBuffer, TokenTreeBuilder> entry : terms.entrySet())
+            addAll(builder, entry.getKey(), entry.getValue());
+
+        File index = File.createTempFile("on-disk-sa-try-superblocks", ".db");
+        index.deleteOnExit();
+
+        builder.finish(index);
+
+        OnDiskIndex onDisk = new OnDiskIndex(index, Int32Type.instance, new KeyConverter());
+        OnDiskIndex.OnDiskSuperBlock superBlock = onDisk.dataLevel.getSuperBlock(0);
+        Iterator<Token> iter = superBlock.iterator();
+
+        Long lastToken = null;
+        while (iter.hasNext())
+        {
+            Token token = iter.next();
+
+            if (lastToken != null)
+                Assert.assertTrue(lastToken.compareTo(token.get()) < 0);
+
+            lastToken = token.get();
+        }
+    }
+
+    @Test
+    public void testSuperBlockRetrieval() throws Exception
+    {
+        OnDiskIndexBuilder builder = new OnDiskIndexBuilder(UTF8Type.instance, LongType.instance, OnDiskIndexBuilder.Mode.SPARSE);
+        for (long i = 0; i < 100000; i++)
+            builder.add(LongType.instance.decompose(i), keyAt(i), i);
+
+        File index = File.createTempFile("on-disk-sa-multi-superblock-match", ".db");
+        index.deleteOnExit();
+
+        builder.finish(index);
+
+        OnDiskIndex onDiskIndex = new OnDiskIndex(index, LongType.instance, new KeyConverter());
+
+        testSearchRangeWithSuperBlocks(onDiskIndex, 0, 500);
+        testSearchRangeWithSuperBlocks(onDiskIndex, 300, 93456);
+        testSearchRangeWithSuperBlocks(onDiskIndex, 210, 1700);
+        testSearchRangeWithSuperBlocks(onDiskIndex, 530, 3200);
+
+        Random random = new Random(0xdeadbeef);
+        for (int i = 0; i < 100000; i += random.nextInt(1500)) // random steps with max of 1500 elements
+        {
+            for (int j = 0; j < 3; j++)
+                testSearchRangeWithSuperBlocks(onDiskIndex, i, ThreadLocalRandom.current().nextInt(i, 100000));
+        }
+    }
+
+    public void putAll(SortedMap<Long, LongSet> offsets, TokenTreeBuilder ttb)
+    {
+        for (Pair<Long, LongSet> entry : ttb)
+            offsets.put(entry.left, entry.right);
+    }
+
+    @Test
+    public void testCombiningOfThePartitionedSA() throws Exception
+    {
+        OnDiskIndexBuilder builderA = new OnDiskIndexBuilder(UTF8Type.instance, LongType.instance, OnDiskIndexBuilder.Mode.PREFIX);
+        OnDiskIndexBuilder builderB = new OnDiskIndexBuilder(UTF8Type.instance, LongType.instance, OnDiskIndexBuilder.Mode.PREFIX);
+
+        TreeMap<Long, TreeMap<Long, LongSet>> expected = new TreeMap<>();
+
+        for (long i = 0; i <= 100; i++)
+        {
+            TreeMap<Long, LongSet> offsets = expected.get(i);
+            if (offsets == null)
+                expected.put(i, (offsets = new TreeMap<>()));
+
+            builderA.add(LongType.instance.decompose(i), keyAt(i), i);
+            putAll(offsets, keyBuilder(i));
+        }
+
+        for (long i = 50; i < 100; i++)
+        {
+            TreeMap<Long, LongSet> offsets = expected.get(i);
+            if (offsets == null)
+                expected.put(i, (offsets = new TreeMap<>()));
+
+            long position = 100L + i;
+            builderB.add(LongType.instance.decompose(i), keyAt(position), position);
+            putAll(offsets, keyBuilder(100L + i));
+        }
+
+        File indexA = File.createTempFile("on-disk-sa-partition-a", ".db");
+        indexA.deleteOnExit();
+
+        File indexB = File.createTempFile("on-disk-sa-partition-b", ".db");
+        indexB.deleteOnExit();
+
+        builderA.finish(indexA);
+        builderB.finish(indexB);
+
+        OnDiskIndex a = new OnDiskIndex(indexA, LongType.instance, new KeyConverter());
+        OnDiskIndex b = new OnDiskIndex(indexB, LongType.instance, new KeyConverter());
+
+        RangeIterator<OnDiskIndex.DataTerm, CombinedTerm> union = OnDiskIndexIterator.union(a, b);
+
+        TreeMap<Long, TreeMap<Long, LongSet>> actual = new TreeMap<>();
+        while (union.hasNext())
+        {
+            CombinedTerm term = union.next();
+
+            Long composedTerm = LongType.instance.compose(term.getTerm());
+
+            TreeMap<Long, LongSet> offsets = actual.get(composedTerm);
+            if (offsets == null)
+                actual.put(composedTerm, (offsets = new TreeMap<>()));
+
+            putAll(offsets, term.getTokenTreeBuilder());
+        }
+
+        Assert.assertEquals(actual, expected);
+
+        File indexC = File.createTempFile("on-disk-sa-partition-final", ".db");
+        indexC.deleteOnExit();
+
+        OnDiskIndexBuilder combined = new OnDiskIndexBuilder(UTF8Type.instance, LongType.instance, OnDiskIndexBuilder.Mode.PREFIX);
+        combined.finish(Pair.create(keyAt(0).getKey(), keyAt(100).getKey()), indexC, new CombinedTermIterator(a, b));
+
+        OnDiskIndex c = new OnDiskIndex(indexC, LongType.instance, new KeyConverter());
+        union = OnDiskIndexIterator.union(c);
+        actual.clear();
+
+        while (union.hasNext())
+        {
+            CombinedTerm term = union.next();
+
+            Long composedTerm = LongType.instance.compose(term.getTerm());
+
+            TreeMap<Long, LongSet> offsets = actual.get(composedTerm);
+            if (offsets == null)
+                actual.put(composedTerm, (offsets = new TreeMap<>()));
+
+            putAll(offsets, term.getTokenTreeBuilder());
+        }
+
+        Assert.assertEquals(actual, expected);
+
+        a.close();
+        b.close();
+    }
+
+    @Test
+    public void testPrefixSearchWithCONTAINSMode() throws Exception
+    {
+        Map<ByteBuffer, TokenTreeBuilder> data = new HashMap<ByteBuffer, TokenTreeBuilder>()
+        {{
+
+            put(UTF8Type.instance.decompose("lady gaga"), keyBuilder(1L));
+
+            // Partial term for 'lady of bells'
+            DataOutputBuffer ladyOfBellsBuffer = new DataOutputBuffer();
+            ladyOfBellsBuffer.writeShort(UTF8Type.instance.decompose("lady of bells").remaining() | (1 << OnDiskIndexBuilder.IS_PARTIAL_BIT));
+            ladyOfBellsBuffer.write(UTF8Type.instance.decompose("lady of bells"));
+            put(ladyOfBellsBuffer.asNewBuffer(), keyBuilder(2L));
+
+
+            put(UTF8Type.instance.decompose("lady pank"),  keyBuilder(3L));
+        }};
+
+        OnDiskIndexBuilder builder = new OnDiskIndexBuilder(UTF8Type.instance, UTF8Type.instance, OnDiskIndexBuilder.Mode.CONTAINS);
+        for (Map.Entry<ByteBuffer, TokenTreeBuilder> e : data.entrySet())
+            addAll(builder, e.getKey(), e.getValue());
+
+        File index = File.createTempFile("on-disk-sa-prefix-contains-search", "db");
+        index.deleteOnExit();
+
+        builder.finish(index);
+
+        OnDiskIndex onDisk = new OnDiskIndex(index, UTF8Type.instance, new KeyConverter());
+
+        // check that lady% return lady gaga (1) and lady pank (3) but not lady of bells(2)
+        Assert.assertEquals(convert(1, 3), convert(onDisk.search(expressionFor("lady", Operator.LIKE_PREFIX))));
+
+        onDisk.close();
+    }
+
+    private void testSearchRangeWithSuperBlocks(OnDiskIndex onDiskIndex, long start, long end)
+    {
+        RangeIterator<Long, Token> tokens = onDiskIndex.search(expressionFor(start, true, end, false));
+
+        // no results should be produced only if range is empty
+        if (tokens == null)
+        {
+            Assert.assertEquals(0, end - start);
+            return;
+        }
+
+        int keyCount = 0;
+        Long lastToken = null;
+        while (tokens.hasNext())
+        {
+            Token token = tokens.next();
+            Iterator<DecoratedKey> keys = token.iterator();
+
+            // each of the values should have exactly a single key
+            Assert.assertTrue(keys.hasNext());
+            keys.next();
+            Assert.assertFalse(keys.hasNext());
+
+            // and it's last should always smaller than current
+            if (lastToken != null)
+                Assert.assertTrue("last should be less than current", lastToken.compareTo(token.get()) < 0);
+
+            lastToken = token.get();
+            keyCount++;
+        }
+
+        Assert.assertEquals(end - start, keyCount);
+    }
+
+    private static DecoratedKey keyAt(long rawKey)
+    {
+        ByteBuffer key = ByteBuffer.wrap(("key" + rawKey).getBytes());
+        return new BufferDecoratedKey(new Murmur3Partitioner.LongToken(MurmurHash.hash2_64(key, key.position(), key.remaining(), 0)), key);
+    }
+
+    private static TokenTreeBuilder keyBuilder(Long... keys)
+    {
+        TokenTreeBuilder builder = new DynamicTokenTreeBuilder();
+
+        for (final Long key : keys)
+        {
+            DecoratedKey dk = keyAt(key);
+            builder.add((Long) dk.getToken().getTokenValue(), key);
+        }
+
+        return builder.finish();
+    }
+
+    private static Set<DecoratedKey> convert(TokenTreeBuilder offsets)
+    {
+        Set<DecoratedKey> result = new HashSet<>();
+
+        Iterator<Pair<Long, LongSet>> offsetIter = offsets.iterator();
+        while (offsetIter.hasNext())
+        {
+            LongSet v = offsetIter.next().right;
+
+            for (LongCursor offset : v)
+                result.add(keyAt(offset.value));
+        }
+        return result;
+    }
+
+    private static Set<DecoratedKey> convert(long... keyOffsets)
+    {
+        Set<DecoratedKey> result = new HashSet<>();
+        for (long offset : keyOffsets)
+            result.add(keyAt(offset));
+
+        return result;
+    }
+
+    private static Set<DecoratedKey> convert(RangeIterator<Long, Token> results)
+    {
+        if (results == null)
+            return Collections.emptySet();
+
+        Set<DecoratedKey> keys = new TreeSet<>(DecoratedKey.comparator);
+
+        while (results.hasNext())
+        {
+            for (DecoratedKey key : results.next())
+                keys.add(key);
+        }
+
+        return keys;
+    }
+
+    private static Expression expressionFor(long lower, boolean lowerInclusive, long upper, boolean upperInclusive)
+    {
+        Expression expression = new Expression("", LongType.instance);
+        expression.add(lowerInclusive ? Operator.GTE : Operator.GT, LongType.instance.decompose(lower));
+        expression.add(upperInclusive ? Operator.LTE : Operator.LT, LongType.instance.decompose(upper));
+        return expression;
+    }
+
+    private static Expression expressionFor(AbstractType<?> validator, ByteBuffer term)
+    {
+        return expressionFor(Operator.LIKE_CONTAINS, validator, term);
+    }
+
+    private static Expression expressionFor(Operator op, AbstractType<?> validator, ByteBuffer term)
+    {
+        Expression expression = new Expression("", validator);
+        expression.add(op, term);
+        return expression;
+    }
+
+    private static Expression expressionForNot(AbstractType<?> validator, ByteBuffer lower, ByteBuffer upper, Iterable<ByteBuffer> terms)
+    {
+        Expression expression = new Expression("", validator);
+        expression.setOp(Expression.Op.RANGE);
+        expression.setLower(new Expression.Bound(lower, true));
+        expression.setUpper(new Expression.Bound(upper, true));
+        for (ByteBuffer term : terms)
+            expression.add(Operator.NEQ, term);
+        return expression;
+
+    }
+
+    private static Expression expressionForNot(Integer lower, Integer upper, Integer... terms)
+    {
+        return expressionForNot(Int32Type.instance,
+                Int32Type.instance.decompose(lower),
+                Int32Type.instance.decompose(upper),
+                Arrays.asList(terms).stream().map(Int32Type.instance::decompose).collect(Collectors.toList()));
+    }
+
+    private static Expression rangeWithExclusions(long lower, boolean lowerInclusive, long upper, boolean upperInclusive, Set<Long> exclusions)
+    {
+        Expression expression = expressionFor(lower, lowerInclusive, upper, upperInclusive);
+        for (long e : exclusions)
+            expression.add(Operator.NEQ, LongType.instance.decompose(e));
+
+        return expression;
+    }
+
+    private static Expression expressionForNot(String lower, String upper, String... terms)
+    {
+        return expressionForNot(UTF8Type.instance,
+                UTF8Type.instance.decompose(lower),
+                UTF8Type.instance.decompose(upper),
+                Arrays.asList(terms).stream().map(UTF8Type.instance::decompose).collect(Collectors.toList()));
+    }
+
+    private static Expression expressionFor(String term)
+    {
+        return expressionFor(term, Operator.LIKE_CONTAINS);
+    }
+
+    private static Expression expressionFor(String term, Operator op)
+    {
+        return expressionFor(op, UTF8Type.instance, UTF8Type.instance.decompose(term));
+    }
+
+    private static void addAll(OnDiskIndexBuilder builder, ByteBuffer term, TokenTreeBuilder tokens)
+    {
+        for (Pair<Long, LongSet> token : tokens)
+        {
+            for (long position : token.right.toArray())
+                builder.add(term, keyAt(position), position);
+        }
+    }
+
+    private static class KeyConverter implements Function<Long, DecoratedKey>
+    {
+        @Override
+        public DecoratedKey apply(Long offset)
+        {
+            return keyAt(offset);
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriterTest.java b/test/unit/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriterTest.java
new file mode 100644
index 0000000..f19d962
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriterTest.java
@@ -0,0 +1,251 @@
+/*
+ * 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.index.sasi.disk;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.util.*;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.compaction.OperationType;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.rows.BTreeRow;
+import org.apache.cassandra.db.rows.BufferCell;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.index.sasi.SASIIndex;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.io.FSError;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.Tables;
+import org.apache.cassandra.service.MigrationManager;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+import com.google.common.util.concurrent.Futures;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class PerSSTableIndexWriterTest extends SchemaLoader
+{
+    private static final String KS_NAME = "sasi";
+    private static final String CF_NAME = "test_cf";
+
+    @BeforeClass
+    public static void loadSchema() throws ConfigurationException
+    {
+        System.setProperty("cassandra.config", "cassandra-murmur.yaml");
+        SchemaLoader.loadSchema();
+        MigrationManager.announceNewKeyspace(KeyspaceMetadata.create(KS_NAME,
+                                                                     KeyspaceParams.simpleTransient(1),
+                                                                     Tables.of(SchemaLoader.sasiCFMD(KS_NAME, CF_NAME))));
+    }
+
+    @Test
+    public void testPartialIndexWrites() throws Exception
+    {
+        final int maxKeys = 100000, numParts = 4, partSize = maxKeys / numParts;
+        final String keyFormat = "key%06d";
+        final long timestamp = System.currentTimeMillis();
+
+        ColumnFamilyStore cfs = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+        ColumnDefinition column = cfs.metadata.getColumnDefinition(UTF8Type.instance.decompose("age"));
+
+        SASIIndex sasi = (SASIIndex) cfs.indexManager.getIndexByName("age");
+
+        File directory = cfs.getDirectories().getDirectoryForNewSSTables();
+        Descriptor descriptor = Descriptor.fromFilename(cfs.getSSTablePath(directory));
+        PerSSTableIndexWriter indexWriter = (PerSSTableIndexWriter) sasi.getFlushObserver(descriptor, OperationType.FLUSH);
+
+        SortedMap<DecoratedKey, Row> expectedKeys = new TreeMap<>(DecoratedKey.comparator);
+
+        for (int i = 0; i < maxKeys; i++)
+        {
+            ByteBuffer key = ByteBufferUtil.bytes(String.format(keyFormat, i));
+            expectedKeys.put(cfs.metadata.partitioner.decorateKey(key),
+                             BTreeRow.singleCellRow(Clustering.EMPTY,
+                                                    BufferCell.live(column, timestamp, Int32Type.instance.decompose(i))));
+        }
+
+        indexWriter.begin();
+
+        Iterator<Map.Entry<DecoratedKey, Row>> keyIterator = expectedKeys.entrySet().iterator();
+        long position = 0;
+
+        Set<String> segments = new HashSet<>();
+        outer:
+        for (;;)
+        {
+            for (int i = 0; i < partSize; i++)
+            {
+                if (!keyIterator.hasNext())
+                    break outer;
+
+                Map.Entry<DecoratedKey, Row> key = keyIterator.next();
+
+                indexWriter.startPartition(key.getKey(), position++);
+                indexWriter.nextUnfilteredCluster(key.getValue());
+            }
+
+            PerSSTableIndexWriter.Index index = indexWriter.getIndex(column);
+
+            OnDiskIndex segment = index.scheduleSegmentFlush(false).call();
+            index.segments.add(Futures.immediateFuture(segment));
+            segments.add(segment.getIndexPath());
+        }
+
+        for (String segment : segments)
+            Assert.assertTrue(new File(segment).exists());
+
+        String indexFile = indexWriter.indexes.get(column).filename(true);
+
+        // final flush
+        indexWriter.complete();
+
+        for (String segment : segments)
+            Assert.assertFalse(new File(segment).exists());
+
+        OnDiskIndex index = new OnDiskIndex(new File(indexFile), Int32Type.instance, keyPosition -> {
+            ByteBuffer key = ByteBufferUtil.bytes(String.format(keyFormat, keyPosition));
+            return cfs.metadata.partitioner.decorateKey(key);
+        });
+
+        Assert.assertEquals(0, UTF8Type.instance.compare(index.minKey(), ByteBufferUtil.bytes(String.format(keyFormat, 0))));
+        Assert.assertEquals(0, UTF8Type.instance.compare(index.maxKey(), ByteBufferUtil.bytes(String.format(keyFormat, maxKeys - 1))));
+
+        Set<DecoratedKey> actualKeys = new HashSet<>();
+        int count = 0;
+        for (OnDiskIndex.DataTerm term : index)
+        {
+            RangeIterator<Long, Token> tokens = term.getTokens();
+
+            while (tokens.hasNext())
+            {
+                for (DecoratedKey key : tokens.next())
+                    actualKeys.add(key);
+            }
+
+            Assert.assertEquals(count++, (int) Int32Type.instance.compose(term.getTerm()));
+        }
+
+        Assert.assertEquals(expectedKeys.size(), actualKeys.size());
+        for (DecoratedKey key : expectedKeys.keySet())
+            Assert.assertTrue(actualKeys.contains(key));
+
+        FileUtils.closeQuietly(index);
+    }
+
+    @Test
+    public void testSparse() throws Exception
+    {
+        final String columnName = "timestamp";
+
+        ColumnFamilyStore cfs = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+        ColumnDefinition column = cfs.metadata.getColumnDefinition(UTF8Type.instance.decompose(columnName));
+
+        SASIIndex sasi = (SASIIndex) cfs.indexManager.getIndexByName(columnName);
+
+        File directory = cfs.getDirectories().getDirectoryForNewSSTables();
+        Descriptor descriptor = Descriptor.fromFilename(cfs.getSSTablePath(directory));
+        PerSSTableIndexWriter indexWriter = (PerSSTableIndexWriter) sasi.getFlushObserver(descriptor, OperationType.FLUSH);
+
+        final long now = System.currentTimeMillis();
+
+        indexWriter.begin();
+        indexWriter.indexes.put(column, indexWriter.newIndex(sasi.getIndex()));
+
+        populateSegment(cfs.metadata, indexWriter.getIndex(column), new HashMap<Long, Set<Integer>>()
+        {{
+            put(now,     new HashSet<>(Arrays.asList(0, 1)));
+            put(now + 1, new HashSet<>(Arrays.asList(2, 3)));
+            put(now + 2, new HashSet<>(Arrays.asList(4, 5, 6, 7, 8, 9)));
+        }});
+
+        Callable<OnDiskIndex> segmentBuilder = indexWriter.getIndex(column).scheduleSegmentFlush(false);
+
+        Assert.assertNull(segmentBuilder.call());
+
+        PerSSTableIndexWriter.Index index = indexWriter.getIndex(column);
+        Random random = ThreadLocalRandom.current();
+
+        Set<String> segments = new HashSet<>();
+        // now let's test multiple correct segments with yield incorrect final segment
+        for (int i = 0; i < 3; i++)
+        {
+            populateSegment(cfs.metadata, index, new HashMap<Long, Set<Integer>>()
+            {{
+                put(now,     new HashSet<>(Arrays.asList(random.nextInt(), random.nextInt(), random.nextInt())));
+                put(now + 1, new HashSet<>(Arrays.asList(random.nextInt(), random.nextInt(), random.nextInt())));
+                put(now + 2, new HashSet<>(Arrays.asList(random.nextInt(), random.nextInt(), random.nextInt())));
+            }});
+
+            try
+            {
+                // flush each of the new segments, they should all succeed
+                OnDiskIndex segment = index.scheduleSegmentFlush(false).call();
+                index.segments.add(Futures.immediateFuture(segment));
+                segments.add(segment.getIndexPath());
+            }
+            catch (Exception | FSError e)
+            {
+                e.printStackTrace();
+                Assert.fail();
+            }
+        }
+
+        // make sure that all of the segments are present of the filesystem
+        for (String segment : segments)
+            Assert.assertTrue(new File(segment).exists());
+
+        indexWriter.complete();
+
+        // make sure that individual segments have been cleaned up
+        for (String segment : segments)
+            Assert.assertFalse(new File(segment).exists());
+
+        // and combined index doesn't exist either
+        Assert.assertFalse(new File(index.outputFile).exists());
+    }
+
+    private static void populateSegment(CFMetaData metadata, PerSSTableIndexWriter.Index index, Map<Long, Set<Integer>> data)
+    {
+        for (Map.Entry<Long, Set<Integer>> value : data.entrySet())
+        {
+            ByteBuffer term = LongType.instance.decompose(value.getKey());
+            for (Integer keyPos : value.getValue())
+            {
+                ByteBuffer key = ByteBufferUtil.bytes(String.format("key%06d", keyPos));
+                index.add(term, metadata.partitioner.decorateKey(key), ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE - 1));
+            }
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/disk/TokenTreeTest.java b/test/unit/org/apache/cassandra/index/sasi/disk/TokenTreeTest.java
new file mode 100644
index 0000000..3d0850a
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/disk/TokenTreeTest.java
@@ -0,0 +1,672 @@
+/*
+ * 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.index.sasi.disk;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.*;
+
+import com.google.common.collect.Iterators;
+import com.google.common.collect.PeekingIterator;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.BufferDecoratedKey;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder.EntryType;
+import org.apache.cassandra.index.sasi.utils.CombinedTerm;
+import org.apache.cassandra.index.sasi.utils.CombinedValue;
+import org.apache.cassandra.index.sasi.utils.MappedBuffer;
+import org.apache.cassandra.index.sasi.utils.RangeIterator;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.SequentialWriterOption;
+import org.apache.cassandra.utils.MurmurHash;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.io.util.SequentialWriter;
+
+import junit.framework.Assert;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import com.carrotsearch.hppc.LongOpenHashSet;
+import com.carrotsearch.hppc.LongSet;
+import com.carrotsearch.hppc.cursors.LongCursor;
+import com.google.common.base.Function;
+
+public class TokenTreeTest
+{
+    private static final Function<Long, DecoratedKey> KEY_CONVERTER = new KeyConverter();
+
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
+    static LongSet singleOffset = new LongOpenHashSet() {{ add(1); }};
+    static LongSet bigSingleOffset = new LongOpenHashSet() {{ add(2147521562L); }};
+    static LongSet shortPackableCollision = new LongOpenHashSet() {{ add(2L); add(3L); }}; // can pack two shorts
+    static LongSet intPackableCollision = new LongOpenHashSet() {{ add(6L); add(((long) Short.MAX_VALUE) + 1); }}; // can pack int & short
+    static LongSet multiCollision =  new LongOpenHashSet() {{ add(3L); add(4L); add(5L); }}; // can't pack
+    static LongSet unpackableCollision = new LongOpenHashSet() {{ add(((long) Short.MAX_VALUE) + 1); add(((long) Short.MAX_VALUE) + 2); }}; // can't pack
+
+    final static SortedMap<Long, LongSet> simpleTokenMap = new TreeMap<Long, LongSet>()
+    {{
+            put(1L, bigSingleOffset); put(3L, shortPackableCollision); put(4L, intPackableCollision); put(6L, singleOffset);
+            put(9L, multiCollision); put(10L, unpackableCollision); put(12L, singleOffset); put(13L, singleOffset);
+            put(15L, singleOffset); put(16L, singleOffset); put(20L, singleOffset); put(22L, singleOffset);
+            put(25L, singleOffset); put(26L, singleOffset); put(27L, singleOffset); put(28L, singleOffset);
+            put(40L, singleOffset); put(50L, singleOffset); put(100L, singleOffset); put(101L, singleOffset);
+            put(102L, singleOffset); put(103L, singleOffset); put(108L, singleOffset); put(110L, singleOffset);
+            put(112L, singleOffset); put(115L, singleOffset); put(116L, singleOffset); put(120L, singleOffset);
+            put(121L, singleOffset); put(122L, singleOffset); put(123L, singleOffset); put(125L, singleOffset);
+    }};
+
+    final static SortedMap<Long, LongSet> bigTokensMap = new TreeMap<Long, LongSet>()
+    {{
+            for (long i = 0; i < 1000000; i++)
+                put(i, singleOffset);
+    }};
+
+    @FunctionalInterface
+    private static interface CheckedConsumer<C> {
+        public void accept(C c) throws Exception;
+    }
+
+    final static List<SortedMap<Long, LongSet>> tokenMaps = Arrays.asList(simpleTokenMap, bigTokensMap);
+    private void forAllTokenMaps(CheckedConsumer<SortedMap<Long, LongSet>> c) throws Exception {
+        for (SortedMap<Long, LongSet> tokens : tokenMaps)
+            c.accept(tokens);
+    }
+
+    final static SequentialWriterOption DEFAULT_OPT = SequentialWriterOption.newBuilder().bufferSize(4096).build();
+
+    @Test
+    public void testSerializedSizeDynamic() throws Exception
+    {
+        forAllTokenMaps(tokens -> testSerializedSize(new DynamicTokenTreeBuilder(tokens)));
+    }
+
+    @Test
+    public void testSerializedSizeStatic() throws Exception
+    {
+        forAllTokenMaps(tokens -> testSerializedSize(new StaticTokenTreeBuilder(new FakeCombinedTerm(tokens))));
+    }
+
+
+    public void testSerializedSize(final TokenTreeBuilder builder) throws Exception
+    {
+        builder.finish();
+        final File treeFile = File.createTempFile("token-tree-size-test", "tt");
+        treeFile.deleteOnExit();
+
+        try (SequentialWriter writer = new SequentialWriter(treeFile, DEFAULT_OPT))
+        {
+            builder.write(writer);
+            writer.sync();
+        }
+
+        final RandomAccessReader reader = RandomAccessReader.open(treeFile);
+        Assert.assertEquals((int) reader.bytesRemaining(), builder.serializedSize());
+        reader.close();
+    }
+
+    @Test
+    public void buildSerializeAndIterateDynamic() throws Exception
+    {
+        forAllTokenMaps(tokens -> buildSerializeAndIterate(new DynamicTokenTreeBuilder(tokens), tokens));
+    }
+
+    @Test
+    public void buildSerializeAndIterateStatic() throws Exception
+    {
+        forAllTokenMaps(tokens ->
+                        buildSerializeAndIterate(new StaticTokenTreeBuilder(new FakeCombinedTerm(tokens)), tokens));
+    }
+
+
+    public void buildSerializeAndIterate(TokenTreeBuilder builder, SortedMap<Long, LongSet> tokenMap) throws Exception
+    {
+
+        builder.finish();
+        final File treeFile = File.createTempFile("token-tree-iterate-test1", "tt");
+        treeFile.deleteOnExit();
+
+        try (SequentialWriter writer = new SequentialWriter(treeFile, DEFAULT_OPT))
+        {
+            builder.write(writer);
+            writer.sync();
+        }
+
+        final RandomAccessReader reader = RandomAccessReader.open(treeFile);
+        final TokenTree tokenTree = new TokenTree(new MappedBuffer(reader));
+
+        final Iterator<Token> tokenIterator = tokenTree.iterator(KEY_CONVERTER);
+        final Iterator<Map.Entry<Long, LongSet>> listIterator = tokenMap.entrySet().iterator();
+        while (tokenIterator.hasNext() && listIterator.hasNext())
+        {
+            Token treeNext = tokenIterator.next();
+            Map.Entry<Long, LongSet> listNext = listIterator.next();
+
+            Assert.assertEquals(listNext.getKey(), treeNext.get());
+            Assert.assertEquals(convert(listNext.getValue()), convert(treeNext));
+        }
+
+        Assert.assertFalse("token iterator not finished", tokenIterator.hasNext());
+        Assert.assertFalse("list iterator not finished", listIterator.hasNext());
+
+        reader.close();
+    }
+
+    @Test
+    public void buildSerializeAndGetDynamic() throws Exception
+    {
+        buildSerializeAndGet(false);
+    }
+
+    @Test
+    public void buildSerializeAndGetStatic() throws Exception
+    {
+        buildSerializeAndGet(true);
+    }
+
+    public void buildSerializeAndGet(boolean isStatic) throws Exception
+    {
+        final long tokMin = 0;
+        final long tokMax = 1000;
+
+        final TokenTree tokenTree = generateTree(tokMin, tokMax, isStatic);
+
+        for (long i = 0; i <= tokMax; i++)
+        {
+            TokenTree.OnDiskToken result = tokenTree.get(i, KEY_CONVERTER);
+            Assert.assertNotNull("failed to find object for token " + i, result);
+
+            LongSet found = result.getOffsets();
+            Assert.assertEquals(1, found.size());
+            Assert.assertEquals(i, found.toArray()[0]);
+        }
+
+        Assert.assertNull("found missing object", tokenTree.get(tokMax + 10, KEY_CONVERTER));
+    }
+
+    @Test
+    public void buildSerializeIterateAndSkipDynamic() throws Exception
+    {
+        forAllTokenMaps(tokens -> buildSerializeIterateAndSkip(new DynamicTokenTreeBuilder(tokens), tokens));
+    }
+
+    @Test
+    public void buildSerializeIterateAndSkipStatic() throws Exception
+    {
+        forAllTokenMaps(tokens ->
+                        buildSerializeIterateAndSkip(new StaticTokenTreeBuilder(new FakeCombinedTerm(tokens)), tokens));
+    }
+
+    // works with maps other than bigTokensMap but skips to a rather large token
+    // so likely for maps other than bigTokensMap skipping is not tested by this.
+    public void buildSerializeIterateAndSkip(TokenTreeBuilder builder, SortedMap<Long, LongSet> tokens) throws Exception
+    {
+        builder.finish();
+        final File treeFile = File.createTempFile("token-tree-iterate-test2", "tt");
+        treeFile.deleteOnExit();
+
+        try (SequentialWriter writer = new SequentialWriter(treeFile, DEFAULT_OPT))
+        {
+            builder.write(writer);
+            writer.sync();
+        }
+
+        final RandomAccessReader reader = RandomAccessReader.open(treeFile);
+        final TokenTree tokenTree = new TokenTree(new MappedBuffer(reader));
+
+        final RangeIterator<Long, Token> treeIterator = tokenTree.iterator(KEY_CONVERTER);
+        final RangeIterator<Long, TokenWithOffsets> listIterator = new EntrySetSkippableIterator(tokens);
+
+        long lastToken = 0L;
+        while (treeIterator.hasNext() && lastToken < 12)
+        {
+            Token treeNext = treeIterator.next();
+            TokenWithOffsets listNext = listIterator.next();
+
+            Assert.assertEquals(listNext.token, (lastToken = treeNext.get()));
+            Assert.assertEquals(convert(listNext.offsets), convert(treeNext));
+        }
+
+        treeIterator.skipTo(100548L);
+        listIterator.skipTo(100548L);
+
+        while (treeIterator.hasNext() && listIterator.hasNext())
+        {
+            Token treeNext = treeIterator.next();
+            TokenWithOffsets listNext = listIterator.next();
+
+            Assert.assertEquals(listNext.token, (long) treeNext.get());
+            Assert.assertEquals(convert(listNext.offsets), convert(treeNext));
+
+        }
+
+        Assert.assertFalse("Tree iterator not completed", treeIterator.hasNext());
+        Assert.assertFalse("List iterator not completed", listIterator.hasNext());
+
+        reader.close();
+    }
+
+    @Test
+    public void skipPastEndDynamic() throws Exception
+    {
+        skipPastEnd(new DynamicTokenTreeBuilder(simpleTokenMap), simpleTokenMap);
+    }
+
+    @Test
+    public void skipPastEndStatic() throws Exception
+    {
+        skipPastEnd(new StaticTokenTreeBuilder(new FakeCombinedTerm(simpleTokenMap)), simpleTokenMap);
+    }
+
+    public void skipPastEnd(TokenTreeBuilder builder, SortedMap<Long, LongSet> tokens) throws Exception
+    {
+        builder.finish();
+        final File treeFile = File.createTempFile("token-tree-skip-past-test", "tt");
+        treeFile.deleteOnExit();
+
+        try (SequentialWriter writer = new SequentialWriter(treeFile, DEFAULT_OPT))
+        {
+            builder.write(writer);
+            writer.sync();
+        }
+
+        final RandomAccessReader reader = RandomAccessReader.open(treeFile);
+        final RangeIterator<Long, Token> tokenTree = new TokenTree(new MappedBuffer(reader)).iterator(KEY_CONVERTER);
+
+        tokenTree.skipTo(tokens.lastKey() + 10);
+    }
+
+    @Test
+    public void testTokenMergeDyanmic() throws Exception
+    {
+        testTokenMerge(false);
+    }
+
+    @Test
+    public void testTokenMergeStatic() throws Exception
+    {
+        testTokenMerge(true);
+    }
+
+    public void testTokenMerge(boolean isStatic) throws Exception
+    {
+        final long min = 0, max = 1000;
+
+        // two different trees with the same offsets
+        TokenTree treeA = generateTree(min, max, isStatic);
+        TokenTree treeB = generateTree(min, max, isStatic);
+
+        RangeIterator<Long, Token> a = treeA.iterator(new KeyConverter());
+        RangeIterator<Long, Token> b = treeB.iterator(new KeyConverter());
+
+        long count = min;
+        while (a.hasNext() && b.hasNext())
+        {
+            final Token tokenA = a.next();
+            final Token tokenB = b.next();
+
+            // merging of two OnDiskToken
+            tokenA.merge(tokenB);
+            // merging with RAM Token with different offset
+            tokenA.merge(new TokenWithOffsets(tokenA.get(), convert(count + 1)));
+            // and RAM token with the same offset
+            tokenA.merge(new TokenWithOffsets(tokenA.get(), convert(count)));
+
+            // should fail when trying to merge different tokens
+            try
+            {
+                tokenA.merge(new TokenWithOffsets(tokenA.get() + 1, convert(count)));
+                Assert.fail();
+            }
+            catch (IllegalArgumentException e)
+            {
+                // expected
+            }
+
+            final Set<Long> offsets = new TreeSet<>();
+            for (DecoratedKey key : tokenA)
+                 offsets.add(LongType.instance.compose(key.getKey()));
+
+            Set<Long> expected = new TreeSet<>();
+            {
+                expected.add(count);
+                expected.add(count + 1);
+            }
+
+            Assert.assertEquals(expected, offsets);
+            count++;
+        }
+
+        Assert.assertEquals(max, count - 1);
+    }
+
+    @Test
+    public void testEntryTypeOrdinalLookup()
+    {
+        Assert.assertEquals(EntryType.SIMPLE, EntryType.of(EntryType.SIMPLE.ordinal()));
+        Assert.assertEquals(EntryType.PACKED, EntryType.of(EntryType.PACKED.ordinal()));
+        Assert.assertEquals(EntryType.FACTORED, EntryType.of(EntryType.FACTORED.ordinal()));
+        Assert.assertEquals(EntryType.OVERFLOW, EntryType.of(EntryType.OVERFLOW.ordinal()));
+    }
+
+    @Test
+    public void testMergingOfEqualTokenTrees() throws Exception
+    {
+        testMergingOfEqualTokenTrees(simpleTokenMap);
+        testMergingOfEqualTokenTrees(bigTokensMap);
+    }
+
+    public void testMergingOfEqualTokenTrees(SortedMap<Long, LongSet> tokensMap) throws Exception
+    {
+        TokenTreeBuilder tokensA = new DynamicTokenTreeBuilder(tokensMap);
+        TokenTreeBuilder tokensB = new DynamicTokenTreeBuilder(tokensMap);
+
+        TokenTree a = buildTree(tokensA);
+        TokenTree b = buildTree(tokensB);
+
+        TokenTreeBuilder tokensC = new StaticTokenTreeBuilder(new CombinedTerm(null, null)
+        {
+            public RangeIterator<Long, Token> getTokenIterator()
+            {
+                RangeIterator.Builder<Long, Token> union = RangeUnionIterator.builder();
+                union.add(a.iterator(new KeyConverter()));
+                union.add(b.iterator(new KeyConverter()));
+
+                return union.build();
+            }
+        });
+
+        TokenTree c = buildTree(tokensC);
+        Assert.assertEquals(tokensMap.size(), c.getCount());
+
+        Iterator<Token> tokenIterator = c.iterator(KEY_CONVERTER);
+        Iterator<Map.Entry<Long, LongSet>> listIterator = tokensMap.entrySet().iterator();
+        while (tokenIterator.hasNext() && listIterator.hasNext())
+        {
+            Token treeNext = tokenIterator.next();
+            Map.Entry<Long, LongSet> listNext = listIterator.next();
+
+            Assert.assertEquals(listNext.getKey(), treeNext.get());
+            Assert.assertEquals(convert(listNext.getValue()), convert(treeNext));
+        }
+
+        for (Map.Entry<Long, LongSet> entry : tokensMap.entrySet())
+        {
+            TokenTree.OnDiskToken result = c.get(entry.getKey(), KEY_CONVERTER);
+            Assert.assertNotNull("failed to find object for token " + entry.getKey(), result);
+
+            LongSet found = result.getOffsets();
+            Assert.assertEquals(entry.getValue(), found);
+
+        }
+    }
+
+
+    private static TokenTree buildTree(TokenTreeBuilder builder) throws Exception
+    {
+        builder.finish();
+        final File treeFile = File.createTempFile("token-tree-", "db");
+        treeFile.deleteOnExit();
+
+        try (SequentialWriter writer = new SequentialWriter(treeFile, DEFAULT_OPT))
+        {
+            builder.write(writer);
+            writer.sync();
+        }
+
+        final RandomAccessReader reader = RandomAccessReader.open(treeFile);
+        return new TokenTree(new MappedBuffer(reader));
+    }
+
+    private static class EntrySetSkippableIterator extends RangeIterator<Long, TokenWithOffsets>
+    {
+        private final PeekingIterator<Map.Entry<Long, LongSet>> elements;
+
+        EntrySetSkippableIterator(SortedMap<Long, LongSet> elms)
+        {
+            super(elms.firstKey(), elms.lastKey(), elms.size());
+            elements = Iterators.peekingIterator(elms.entrySet().iterator());
+        }
+
+        @Override
+        public TokenWithOffsets computeNext()
+        {
+            if (!elements.hasNext())
+                return endOfData();
+
+            Map.Entry<Long, LongSet> next = elements.next();
+            return new TokenWithOffsets(next.getKey(), next.getValue());
+        }
+
+        @Override
+        protected void performSkipTo(Long nextToken)
+        {
+            while (elements.hasNext())
+            {
+                if (Long.compare(elements.peek().getKey(), nextToken) >= 0)
+                {
+                    break;
+                }
+
+                elements.next();
+            }
+        }
+
+        @Override
+        public void close() throws IOException
+        {
+            // nothing to do here
+        }
+    }
+
+    public static class FakeCombinedTerm extends CombinedTerm
+    {
+        private final SortedMap<Long, LongSet> tokens;
+
+        public FakeCombinedTerm(SortedMap<Long, LongSet> tokens)
+        {
+            super(null, null);
+            this.tokens = tokens;
+        }
+
+        public RangeIterator<Long, Token> getTokenIterator()
+        {
+            return new TokenMapIterator(tokens);
+        }
+    }
+
+    public static class TokenMapIterator extends RangeIterator<Long, Token>
+    {
+        public final Iterator<Map.Entry<Long, LongSet>> iterator;
+
+        public TokenMapIterator(SortedMap<Long, LongSet> tokens)
+        {
+            super(tokens.firstKey(), tokens.lastKey(), tokens.size());
+            iterator = tokens.entrySet().iterator();
+        }
+
+        public Token computeNext()
+        {
+            if (!iterator.hasNext())
+                return endOfData();
+
+            Map.Entry<Long, LongSet> entry = iterator.next();
+            return new TokenWithOffsets(entry.getKey(), entry.getValue());
+        }
+
+        public void close() throws IOException
+        {
+
+        }
+
+        public void performSkipTo(Long next)
+        {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    public static class TokenWithOffsets extends Token
+    {
+        private final LongSet offsets;
+
+        public TokenWithOffsets(long token, final LongSet offsets)
+        {
+            super(token);
+            this.offsets = offsets;
+        }
+
+        @Override
+        public LongSet getOffsets()
+        {
+            return offsets;
+        }
+
+        @Override
+        public void merge(CombinedValue<Long> other)
+        {}
+
+        @Override
+        public int compareTo(CombinedValue<Long> o)
+        {
+            return Long.compare(token, o.get());
+        }
+
+        @Override
+        public boolean equals(Object other)
+        {
+            if (!(other instanceof TokenWithOffsets))
+                return false;
+
+            TokenWithOffsets o = (TokenWithOffsets) other;
+            return token == o.token && offsets.equals(o.offsets);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return new HashCodeBuilder().append(token).build();
+        }
+
+        @Override
+        public String toString()
+        {
+            return String.format("TokenValue(token: %d, offsets: %s)", token, offsets);
+        }
+
+        @Override
+        public Iterator<DecoratedKey> iterator()
+        {
+            List<DecoratedKey> keys = new ArrayList<>(offsets.size());
+            for (LongCursor offset : offsets)
+                 keys.add(dk(offset.value));
+
+            return keys.iterator();
+        }
+    }
+
+    private static Set<DecoratedKey> convert(LongSet offsets)
+    {
+        Set<DecoratedKey> keys = new HashSet<>();
+        for (LongCursor offset : offsets)
+            keys.add(KEY_CONVERTER.apply(offset.value));
+
+        return keys;
+    }
+
+    private static Set<DecoratedKey> convert(Token results)
+    {
+        Set<DecoratedKey> keys = new HashSet<>();
+        for (DecoratedKey key : results)
+            keys.add(key);
+
+        return keys;
+    }
+
+    private static LongSet convert(long... values)
+    {
+        LongSet result = new LongOpenHashSet(values.length);
+        for (long v : values)
+            result.add(v);
+
+        return result;
+    }
+
+    private static class KeyConverter implements Function<Long, DecoratedKey>
+    {
+        @Override
+        public DecoratedKey apply(Long offset)
+        {
+            return dk(offset);
+        }
+    }
+
+    private static DecoratedKey dk(Long token)
+    {
+        ByteBuffer buf = ByteBuffer.allocate(8);
+        buf.putLong(token);
+        buf.flip();
+        Long hashed = MurmurHash.hash2_64(buf, buf.position(), buf.remaining(), 0);
+        return new BufferDecoratedKey(new Murmur3Partitioner.LongToken(hashed), buf);
+    }
+
+    private static TokenTree generateTree(final long minToken, final long maxToken, boolean isStatic) throws IOException
+    {
+        final SortedMap<Long, LongSet> toks = new TreeMap<Long, LongSet>()
+        {{
+                for (long i = minToken; i <= maxToken; i++)
+                {
+                    LongSet offsetSet = new LongOpenHashSet();
+                    offsetSet.add(i);
+                    put(i, offsetSet);
+                }
+        }};
+
+        final TokenTreeBuilder builder = isStatic ? new StaticTokenTreeBuilder(new FakeCombinedTerm(toks)) : new DynamicTokenTreeBuilder(toks);
+        builder.finish();
+        final File treeFile = File.createTempFile("token-tree-get-test", "tt");
+        treeFile.deleteOnExit();
+
+        try (SequentialWriter writer = new SequentialWriter(treeFile, DEFAULT_OPT))
+        {
+            builder.write(writer);
+            writer.sync();
+        }
+
+        RandomAccessReader reader = null;
+
+        try
+        {
+            reader = RandomAccessReader.open(treeFile);
+            return new TokenTree(new MappedBuffer(reader));
+        }
+        finally
+        {
+            FileUtils.closeQuietly(reader);
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java b/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java
new file mode 100644
index 0000000..e388cd4
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java
@@ -0,0 +1,706 @@
+/*
+ * 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.index.sasi.plan;
+
+import java.nio.ByteBuffer;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.db.marshal.DoubleType;
+import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.index.sasi.plan.Operation.OperationType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.Tables;
+import org.apache.cassandra.service.MigrationManager;
+import org.apache.cassandra.utils.FBUtilities;
+
+import org.junit.*;
+
+public class OperationTest extends SchemaLoader
+{
+    private static final String KS_NAME = "sasi";
+    private static final String CF_NAME = "test_cf";
+    private static final String CLUSTERING_CF_NAME = "clustering_test_cf";
+    private static final String STATIC_CF_NAME = "static_sasi_test_cf";
+
+    private static ColumnFamilyStore BACKEND;
+    private static ColumnFamilyStore CLUSTERING_BACKEND;
+    private static ColumnFamilyStore STATIC_BACKEND;
+
+    @BeforeClass
+    public static void loadSchema() throws ConfigurationException
+    {
+        System.setProperty("cassandra.config", "cassandra-murmur.yaml");
+        SchemaLoader.loadSchema();
+        MigrationManager.announceNewKeyspace(KeyspaceMetadata.create(KS_NAME,
+                                                                     KeyspaceParams.simpleTransient(1),
+                                                                     Tables.of(SchemaLoader.sasiCFMD(KS_NAME, CF_NAME),
+                                                                               SchemaLoader.clusteringSASICFMD(KS_NAME, CLUSTERING_CF_NAME),
+                                                                               SchemaLoader.staticSASICFMD(KS_NAME, STATIC_CF_NAME))));
+
+        BACKEND = Keyspace.open(KS_NAME).getColumnFamilyStore(CF_NAME);
+        CLUSTERING_BACKEND = Keyspace.open(KS_NAME).getColumnFamilyStore(CLUSTERING_CF_NAME);
+        STATIC_BACKEND = Keyspace.open(KS_NAME).getColumnFamilyStore(STATIC_CF_NAME);
+    }
+
+    private QueryController controller;
+
+    @Before
+    public void beforeTest()
+    {
+        controller = new QueryController(BACKEND,
+                                         PartitionRangeReadCommand.allDataRead(BACKEND.metadata, FBUtilities.nowInSeconds()),
+                                         TimeUnit.SECONDS.toMillis(10));
+    }
+
+    @After
+    public void afterTest()
+    {
+        controller.finish();
+    }
+
+    @Test
+    public void testAnalyze() throws Exception
+    {
+        final ColumnDefinition firstName = getColumn(UTF8Type.instance.decompose("first_name"));
+        final ColumnDefinition age = getColumn(UTF8Type.instance.decompose("age"));
+        final ColumnDefinition comment = getColumn(UTF8Type.instance.decompose("comment"));
+
+        // age != 5 AND age > 1 AND age != 6 AND age <= 10
+        Map<Expression.Op, Expression> expressions = convert(Operation.analyzeGroup(controller, OperationType.AND,
+                                                                                Arrays.asList(new SimpleExpression(age, Operator.NEQ, Int32Type.instance.decompose(5)),
+                                                                                              new SimpleExpression(age, Operator.GT, Int32Type.instance.decompose(1)),
+                                                                                              new SimpleExpression(age, Operator.NEQ, Int32Type.instance.decompose(6)),
+                                                                                              new SimpleExpression(age, Operator.LTE, Int32Type.instance.decompose(10)))));
+
+        Expression expected = new Expression("age", Int32Type.instance)
+        {{
+            operation = Op.RANGE;
+            lower = new Bound(Int32Type.instance.decompose(1), false);
+            upper = new Bound(Int32Type.instance.decompose(10), true);
+
+            exclusions.add(Int32Type.instance.decompose(5));
+            exclusions.add(Int32Type.instance.decompose(6));
+        }};
+
+        Assert.assertEquals(1, expressions.size());
+        Assert.assertEquals(expected, expressions.get(Expression.Op.RANGE));
+
+        // age != 5 OR age >= 7
+        expressions = convert(Operation.analyzeGroup(controller, OperationType.OR,
+                                                    Arrays.asList(new SimpleExpression(age, Operator.NEQ, Int32Type.instance.decompose(5)),
+                                                                  new SimpleExpression(age, Operator.GTE, Int32Type.instance.decompose(7)))));
+        Assert.assertEquals(2, expressions.size());
+
+        Assert.assertEquals(new Expression("age", Int32Type.instance)
+                            {{
+                                    operation = Op.NOT_EQ;
+                                    lower = new Bound(Int32Type.instance.decompose(5), true);
+                                    upper = lower;
+                            }}, expressions.get(Expression.Op.NOT_EQ));
+
+        Assert.assertEquals(new Expression("age", Int32Type.instance)
+                            {{
+                                    operation = Op.RANGE;
+                                    lower = new Bound(Int32Type.instance.decompose(7), true);
+                            }}, expressions.get(Expression.Op.RANGE));
+
+        // age != 5 OR age < 7
+        expressions = convert(Operation.analyzeGroup(controller, OperationType.OR,
+                                                    Arrays.asList(new SimpleExpression(age, Operator.NEQ, Int32Type.instance.decompose(5)),
+                                                                  new SimpleExpression(age, Operator.LT, Int32Type.instance.decompose(7)))));
+
+        Assert.assertEquals(2, expressions.size());
+        Assert.assertEquals(new Expression("age", Int32Type.instance)
+                            {{
+                                    operation = Op.RANGE;
+                                    upper = new Bound(Int32Type.instance.decompose(7), false);
+                            }}, expressions.get(Expression.Op.RANGE));
+        Assert.assertEquals(new Expression("age", Int32Type.instance)
+                            {{
+                                    operation = Op.NOT_EQ;
+                                    lower = new Bound(Int32Type.instance.decompose(5), true);
+                                    upper = lower;
+                            }}, expressions.get(Expression.Op.NOT_EQ));
+
+        // age > 1 AND age < 7
+        expressions = convert(Operation.analyzeGroup(controller, OperationType.AND,
+                                                    Arrays.asList(new SimpleExpression(age, Operator.GT, Int32Type.instance.decompose(1)),
+                                                                  new SimpleExpression(age, Operator.LT, Int32Type.instance.decompose(7)))));
+
+        Assert.assertEquals(1, expressions.size());
+        Assert.assertEquals(new Expression("age", Int32Type.instance)
+                            {{
+                                    operation = Op.RANGE;
+                                    lower = new Bound(Int32Type.instance.decompose(1), false);
+                                    upper = new Bound(Int32Type.instance.decompose(7), false);
+                            }}, expressions.get(Expression.Op.RANGE));
+
+        // first_name = 'a' OR first_name != 'b'
+        expressions = convert(Operation.analyzeGroup(controller, OperationType.OR,
+                                                    Arrays.asList(new SimpleExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("a")),
+                                                                  new SimpleExpression(firstName, Operator.NEQ, UTF8Type.instance.decompose("b")))));
+
+        Assert.assertEquals(2, expressions.size());
+        Assert.assertEquals(new Expression("first_name", UTF8Type.instance)
+                            {{
+                                    operation = Op.NOT_EQ;
+                                    lower = new Bound(UTF8Type.instance.decompose("b"), true);
+                                    upper = lower;
+                            }}, expressions.get(Expression.Op.NOT_EQ));
+        Assert.assertEquals(new Expression("first_name", UTF8Type.instance)
+                            {{
+                                    operation = Op.EQ;
+                                    lower = upper = new Bound(UTF8Type.instance.decompose("a"), true);
+                            }}, expressions.get(Expression.Op.EQ));
+
+        // comment = 'soft eng' and comment != 'likes do'
+        ListMultimap<ColumnDefinition, Expression> e = Operation.analyzeGroup(controller, OperationType.OR,
+                                                    Arrays.asList(new SimpleExpression(comment, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("soft eng")),
+                                                                  new SimpleExpression(comment, Operator.NEQ, UTF8Type.instance.decompose("likes do"))));
+
+        List<Expression> expectedExpressions = new ArrayList<Expression>(2)
+        {{
+                add(new Expression("comment", UTF8Type.instance)
+                {{
+                        operation = Op.MATCH;
+                        lower = new Bound(UTF8Type.instance.decompose("soft"), true);
+                        upper = lower;
+                }});
+
+                add(new Expression("comment", UTF8Type.instance)
+                {{
+                        operation = Op.MATCH;
+                        lower = new Bound(UTF8Type.instance.decompose("eng"), true);
+                        upper = lower;
+                }});
+
+                add(new Expression("comment", UTF8Type.instance)
+                {{
+                        operation = Op.NOT_EQ;
+                        lower = new Bound(UTF8Type.instance.decompose("likes"), true);
+                        upper = lower;
+                }});
+
+                add(new Expression("comment", UTF8Type.instance)
+                {{
+                        operation = Op.NOT_EQ;
+                        lower = new Bound(UTF8Type.instance.decompose("do"), true);
+                        upper = lower;
+                }});
+        }};
+
+        Assert.assertEquals(expectedExpressions, e.get(comment));
+
+        // first_name = 'j' and comment != 'likes do'
+        e = Operation.analyzeGroup(controller, OperationType.OR,
+                        Arrays.asList(new SimpleExpression(comment, Operator.NEQ, UTF8Type.instance.decompose("likes do")),
+                                      new SimpleExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("j"))));
+
+        expectedExpressions = new ArrayList<Expression>(2)
+        {{
+                add(new Expression("comment", UTF8Type.instance)
+                {{
+                        operation = Op.NOT_EQ;
+                        lower = new Bound(UTF8Type.instance.decompose("likes"), true);
+                        upper = lower;
+                }});
+
+                add(new Expression("comment", UTF8Type.instance)
+                {{
+                        operation = Op.NOT_EQ;
+                        lower = new Bound(UTF8Type.instance.decompose("do"), true);
+                        upper = lower;
+                }});
+        }};
+
+        Assert.assertEquals(expectedExpressions, e.get(comment));
+
+        // age != 27 first_name = 'j' and age != 25
+        e = Operation.analyzeGroup(controller, OperationType.OR,
+                        Arrays.asList(new SimpleExpression(age, Operator.NEQ, Int32Type.instance.decompose(27)),
+                                      new SimpleExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("j")),
+                                      new SimpleExpression(age, Operator.NEQ, Int32Type.instance.decompose(25))));
+
+        expectedExpressions = new ArrayList<Expression>(2)
+        {{
+                add(new Expression("age", Int32Type.instance)
+                {{
+                        operation = Op.NOT_EQ;
+                        lower = new Bound(Int32Type.instance.decompose(27), true);
+                        upper = lower;
+                }});
+
+                add(new Expression("age", Int32Type.instance)
+                {{
+                        operation = Op.NOT_EQ;
+                        lower = new Bound(Int32Type.instance.decompose(25), true);
+                        upper = lower;
+                }});
+        }};
+
+        Assert.assertEquals(expectedExpressions, e.get(age));
+    }
+
+    @Test
+    public void testSatisfiedBy() throws Exception
+    {
+        final ColumnDefinition timestamp = getColumn(UTF8Type.instance.decompose("timestamp"));
+        final ColumnDefinition age = getColumn(UTF8Type.instance.decompose("age"));
+
+        Operation.Builder builder = new Operation.Builder(OperationType.AND, controller, new SimpleExpression(age, Operator.NEQ, Int32Type.instance.decompose(5)));
+        Operation op = builder.complete();
+
+        Unfiltered row = buildRow(buildCell(age, Int32Type.instance.decompose(6), System.currentTimeMillis()));
+        Row staticRow = buildRow(Clustering.STATIC_CLUSTERING);
+
+        Assert.assertTrue(op.satisfiedBy(row, staticRow, false));
+
+        row = buildRow(buildCell(age, Int32Type.instance.decompose(5), System.currentTimeMillis()));
+
+        // and reject incorrect value
+        Assert.assertFalse(op.satisfiedBy(row, staticRow, false));
+
+        row = buildRow(buildCell(age, Int32Type.instance.decompose(6), System.currentTimeMillis()));
+
+        Assert.assertTrue(op.satisfiedBy(row, staticRow, false));
+
+        // range with exclusions - age != 5 AND age > 1 AND age != 6 AND age <= 10
+        builder = new Operation.Builder(OperationType.AND, controller,
+                                        new SimpleExpression(age, Operator.NEQ, Int32Type.instance.decompose(5)),
+                                        new SimpleExpression(age, Operator.GT, Int32Type.instance.decompose(1)),
+                                        new SimpleExpression(age, Operator.NEQ, Int32Type.instance.decompose(6)),
+                                        new SimpleExpression(age, Operator.LTE, Int32Type.instance.decompose(10)));
+        op = builder.complete();
+
+        Set<Integer> exclusions = Sets.newHashSet(0, 1, 5, 6, 11);
+        for (int i = 0; i <= 11; i++)
+        {
+            row = buildRow(buildCell(age, Int32Type.instance.decompose(i), System.currentTimeMillis()));
+
+            boolean result = op.satisfiedBy(row, staticRow, false);
+            Assert.assertTrue(exclusions.contains(i) != result);
+        }
+
+        // now let's do something more complex - age = 5 OR age = 6
+        builder = new Operation.Builder(OperationType.OR, controller,
+                                        new SimpleExpression(age, Operator.EQ, Int32Type.instance.decompose(5)),
+                                        new SimpleExpression(age, Operator.EQ, Int32Type.instance.decompose(6)));
+
+        op = builder.complete();
+
+        exclusions = Sets.newHashSet(0, 1, 2, 3, 4, 7, 8, 9, 10);
+        for (int i = 0; i <= 10; i++)
+        {
+            row = buildRow(buildCell(age, Int32Type.instance.decompose(i), System.currentTimeMillis()));
+
+            boolean result = op.satisfiedBy(row, staticRow, false);
+            Assert.assertTrue(exclusions.contains(i) != result);
+        }
+
+        // now let's test aggregated AND commands
+        builder = new Operation.Builder(OperationType.AND, controller);
+
+        // logical should be ignored by analyzer, but we still what to make sure that it is
+        //IndexExpression logical = new IndexExpression(ByteBufferUtil.EMPTY_BYTE_BUFFER, IndexOperator.EQ, ByteBufferUtil.EMPTY_BYTE_BUFFER);
+        //logical.setLogicalOp(LogicalIndexOperator.AND);
+
+        //builder.add(logical);
+        builder.add(new SimpleExpression(age, Operator.GTE, Int32Type.instance.decompose(0)));
+        builder.add(new SimpleExpression(age, Operator.LT, Int32Type.instance.decompose(10)));
+        builder.add(new SimpleExpression(age, Operator.NEQ, Int32Type.instance.decompose(7)));
+
+        op = builder.complete();
+
+        exclusions = Sets.newHashSet(7);
+        for (int i = 0; i < 10; i++)
+        {
+            row = buildRow(buildCell(age, Int32Type.instance.decompose(i), System.currentTimeMillis()));
+
+            boolean result = op.satisfiedBy(row, staticRow, false);
+            Assert.assertTrue(exclusions.contains(i) != result);
+        }
+
+        // multiple analyzed expressions in the Operation timestamp >= 10 AND age = 5
+        builder = new Operation.Builder(OperationType.AND, controller);
+        builder.add(new SimpleExpression(timestamp, Operator.GTE, LongType.instance.decompose(10L)));
+        builder.add(new SimpleExpression(age, Operator.EQ, Int32Type.instance.decompose(5)));
+
+        op = builder.complete();
+
+        row = buildRow(buildCell(age, Int32Type.instance.decompose(6), System.currentTimeMillis()),
+                                  buildCell(timestamp, LongType.instance.decompose(11L), System.currentTimeMillis()));
+
+        Assert.assertFalse(op.satisfiedBy(row, staticRow, false));
+
+        row = buildRow(buildCell(age, Int32Type.instance.decompose(5), System.currentTimeMillis()),
+                                  buildCell(timestamp, LongType.instance.decompose(22L), System.currentTimeMillis()));
+
+        Assert.assertTrue(op.satisfiedBy(row, staticRow, false));
+
+        row = buildRow(buildCell(age, Int32Type.instance.decompose(5), System.currentTimeMillis()),
+                                  buildCell(timestamp, LongType.instance.decompose(9L), System.currentTimeMillis()));
+
+        Assert.assertFalse(op.satisfiedBy(row, staticRow, false));
+
+        // operation with internal expressions and right child
+        builder = new Operation.Builder(OperationType.OR, controller,
+                                        new SimpleExpression(timestamp, Operator.GT, LongType.instance.decompose(10L)));
+        builder.setRight(new Operation.Builder(OperationType.AND, controller,
+                                               new SimpleExpression(age, Operator.GT, Int32Type.instance.decompose(0)),
+                                               new SimpleExpression(age, Operator.LT, Int32Type.instance.decompose(10))));
+        op = builder.complete();
+
+        row = buildRow(buildCell(age, Int32Type.instance.decompose(5), System.currentTimeMillis()),
+                                  buildCell(timestamp, LongType.instance.decompose(9L), System.currentTimeMillis()));
+
+        Assert.assertTrue(op.satisfiedBy(row, staticRow, false));
+
+        row = buildRow(buildCell(age, Int32Type.instance.decompose(20), System.currentTimeMillis()),
+                                  buildCell(timestamp, LongType.instance.decompose(11L), System.currentTimeMillis()));
+
+        Assert.assertTrue(op.satisfiedBy(row, staticRow, false));
+
+        row = buildRow(buildCell(age, Int32Type.instance.decompose(0), System.currentTimeMillis()),
+                                  buildCell(timestamp, LongType.instance.decompose(9L), System.currentTimeMillis()));
+
+        Assert.assertFalse(op.satisfiedBy(row, staticRow, false));
+
+        // and for desert let's try out null and deleted rows etc.
+        builder = new Operation.Builder(OperationType.AND, controller);
+        builder.add(new SimpleExpression(age, Operator.EQ, Int32Type.instance.decompose(30)));
+        op = builder.complete();
+
+        Assert.assertFalse(op.satisfiedBy(null, staticRow, false));
+        Assert.assertFalse(op.satisfiedBy(row, null, false));
+        Assert.assertFalse(op.satisfiedBy(row, staticRow, false));
+
+        long now = System.currentTimeMillis();
+
+        row = OperationTest.buildRow(
+                Row.Deletion.regular(new DeletionTime(now - 10, (int) (now / 1000))),
+                          buildCell(age, Int32Type.instance.decompose(6), System.currentTimeMillis()));
+
+        Assert.assertFalse(op.satisfiedBy(row, staticRow, false));
+
+        row = buildRow(deletedCell(age, System.currentTimeMillis(), FBUtilities.nowInSeconds()));
+
+        Assert.assertFalse(op.satisfiedBy(row, staticRow, true));
+
+        try
+        {
+            Assert.assertFalse(op.satisfiedBy(buildRow(), staticRow, false));
+        }
+        catch (IllegalStateException e)
+        {
+            // expected
+        }
+
+        try
+        {
+            Assert.assertFalse(op.satisfiedBy(buildRow(), staticRow, true));
+        }
+        catch (IllegalStateException e)
+        {
+            Assert.fail("IllegalStateException should not be thrown when missing column and allowMissingColumns=true");
+        }
+    }
+
+    @Test
+    public void testAnalyzeNotIndexedButDefinedColumn() throws Exception
+    {
+        final ColumnDefinition firstName = getColumn(UTF8Type.instance.decompose("first_name"));
+        final ColumnDefinition height = getColumn(UTF8Type.instance.decompose("height"));
+
+        // first_name = 'a' AND height != 10
+        Map<Expression.Op, Expression> expressions;
+        expressions = convert(Operation.analyzeGroup(controller, OperationType.AND,
+                Arrays.asList(new SimpleExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("a")),
+                              new SimpleExpression(height, Operator.NEQ, Int32Type.instance.decompose(5)))));
+
+        Assert.assertEquals(2, expressions.size());
+
+        Assert.assertEquals(new Expression("height", Int32Type.instance)
+        {{
+                operation = Op.NOT_EQ;
+                lower = new Bound(Int32Type.instance.decompose(5), true);
+                upper = lower;
+        }}, expressions.get(Expression.Op.NOT_EQ));
+
+        expressions = convert(Operation.analyzeGroup(controller, OperationType.AND,
+                Arrays.asList(new SimpleExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("a")),
+                              new SimpleExpression(height, Operator.GT, Int32Type.instance.decompose(0)),
+                              new SimpleExpression(height, Operator.NEQ, Int32Type.instance.decompose(5)))));
+
+        Assert.assertEquals(2, expressions.size());
+
+        Assert.assertEquals(new Expression("height", Int32Type.instance)
+        {{
+            operation = Op.RANGE;
+            lower = new Bound(Int32Type.instance.decompose(0), false);
+            exclusions.add(Int32Type.instance.decompose(5));
+        }}, expressions.get(Expression.Op.RANGE));
+
+        expressions = convert(Operation.analyzeGroup(controller, OperationType.AND,
+                Arrays.asList(new SimpleExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("a")),
+                              new SimpleExpression(height, Operator.NEQ, Int32Type.instance.decompose(5)),
+                              new SimpleExpression(height, Operator.GTE, Int32Type.instance.decompose(0)),
+                              new SimpleExpression(height, Operator.LT, Int32Type.instance.decompose(10)))));
+
+        Assert.assertEquals(2, expressions.size());
+
+        Assert.assertEquals(new Expression("height", Int32Type.instance)
+        {{
+                operation = Op.RANGE;
+                lower = new Bound(Int32Type.instance.decompose(0), true);
+                upper = new Bound(Int32Type.instance.decompose(10), false);
+                exclusions.add(Int32Type.instance.decompose(5));
+        }}, expressions.get(Expression.Op.RANGE));
+    }
+
+    @Test
+    public void testSatisfiedByWithMultipleTerms()
+    {
+        final ColumnDefinition comment = getColumn(UTF8Type.instance.decompose("comment"));
+
+        Unfiltered row = buildRow(buildCell(comment,UTF8Type.instance.decompose("software engineer is working on a project"),System.currentTimeMillis()));
+        Row staticRow = buildRow(Clustering.STATIC_CLUSTERING);
+
+        Operation.Builder builder = new Operation.Builder(OperationType.AND, controller,
+                                            new SimpleExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("eng is a work")));
+        Operation op = builder.complete();
+
+        Assert.assertTrue(op.satisfiedBy(row, staticRow, false));
+
+        builder = new Operation.Builder(OperationType.AND, controller,
+                                            new SimpleExpression(comment, Operator.LIKE_CONTAINS, UTF8Type.instance.decompose("soft works fine")));
+        op = builder.complete();
+
+        Assert.assertTrue(op.satisfiedBy(row, staticRow, false));
+    }
+
+    @Test
+    public void testSatisfiedByWithClustering()
+    {
+        ColumnDefinition location = getColumn(CLUSTERING_BACKEND, UTF8Type.instance.decompose("location"));
+        ColumnDefinition age = getColumn(CLUSTERING_BACKEND, UTF8Type.instance.decompose("age"));
+        ColumnDefinition height = getColumn(CLUSTERING_BACKEND, UTF8Type.instance.decompose("height"));
+        ColumnDefinition score = getColumn(CLUSTERING_BACKEND, UTF8Type.instance.decompose("score"));
+
+        Unfiltered row = buildRow(Clustering.make(UTF8Type.instance.fromString("US"), Int32Type.instance.decompose(27)),
+                                  buildCell(height, Int32Type.instance.decompose(182), System.currentTimeMillis()),
+                                  buildCell(score, DoubleType.instance.decompose(1.0d), System.currentTimeMillis()));
+        Row staticRow = buildRow(Clustering.STATIC_CLUSTERING);
+
+        Operation.Builder builder = new Operation.Builder(OperationType.AND, controller);
+        builder.add(new SimpleExpression(age, Operator.EQ, Int32Type.instance.decompose(27)));
+        builder.add(new SimpleExpression(height, Operator.EQ, Int32Type.instance.decompose(182)));
+
+        Assert.assertTrue(builder.complete().satisfiedBy(row, staticRow, false));
+
+        builder = new Operation.Builder(OperationType.AND, controller);
+
+        builder.add(new SimpleExpression(age, Operator.EQ, Int32Type.instance.decompose(28)));
+        builder.add(new SimpleExpression(height, Operator.EQ, Int32Type.instance.decompose(182)));
+
+        Assert.assertFalse(builder.complete().satisfiedBy(row, staticRow, false));
+
+        builder = new Operation.Builder(OperationType.AND, controller);
+        builder.add(new SimpleExpression(location, Operator.EQ, UTF8Type.instance.decompose("US")));
+        builder.add(new SimpleExpression(age, Operator.GTE, Int32Type.instance.decompose(27)));
+
+        Assert.assertTrue(builder.complete().satisfiedBy(row, staticRow, false));
+
+        builder = new Operation.Builder(OperationType.AND, controller);
+        builder.add(new SimpleExpression(location, Operator.EQ, UTF8Type.instance.decompose("BY")));
+        builder.add(new SimpleExpression(age, Operator.GTE, Int32Type.instance.decompose(28)));
+
+        Assert.assertFalse(builder.complete().satisfiedBy(row, staticRow, false));
+
+        builder = new Operation.Builder(OperationType.AND, controller);
+        builder.add(new SimpleExpression(location, Operator.EQ, UTF8Type.instance.decompose("US")));
+        builder.add(new SimpleExpression(age, Operator.LTE, Int32Type.instance.decompose(27)));
+        builder.add(new SimpleExpression(height, Operator.GTE, Int32Type.instance.decompose(182)));
+
+        Assert.assertTrue(builder.complete().satisfiedBy(row, staticRow, false));
+
+        builder = new Operation.Builder(OperationType.AND, controller);
+        builder.add(new SimpleExpression(location, Operator.EQ, UTF8Type.instance.decompose("US")));
+        builder.add(new SimpleExpression(height, Operator.GTE, Int32Type.instance.decompose(182)));
+        builder.add(new SimpleExpression(score, Operator.EQ, DoubleType.instance.decompose(1.0d)));
+
+        Assert.assertTrue(builder.complete().satisfiedBy(row, staticRow, false));
+
+        builder = new Operation.Builder(OperationType.AND, controller);
+        builder.add(new SimpleExpression(height, Operator.GTE, Int32Type.instance.decompose(182)));
+        builder.add(new SimpleExpression(score, Operator.EQ, DoubleType.instance.decompose(1.0d)));
+
+        Assert.assertTrue(builder.complete().satisfiedBy(row, staticRow, false));
+    }
+
+    private Map<Expression.Op, Expression> convert(Multimap<ColumnDefinition, Expression> expressions)
+    {
+        Map<Expression.Op, Expression> converted = new HashMap<>();
+        for (Expression expression : expressions.values())
+        {
+            Expression column = converted.get(expression.getOp());
+            assert column == null; // sanity check
+            converted.put(expression.getOp(), expression);
+        }
+
+        return converted;
+    }
+
+    @Test
+    public void testSatisfiedByWithStatic()
+    {
+        final ColumnDefinition sensorType = getColumn(STATIC_BACKEND, UTF8Type.instance.decompose("sensor_type"));
+        final ColumnDefinition value = getColumn(STATIC_BACKEND, UTF8Type.instance.decompose("value"));
+
+        Unfiltered row = buildRow(Clustering.make(UTF8Type.instance.fromString("date"), LongType.instance.decompose(20160401L)),
+                          buildCell(value, DoubleType.instance.decompose(24.56), System.currentTimeMillis()));
+        Row staticRow = buildRow(Clustering.STATIC_CLUSTERING,
+                         buildCell(sensorType, UTF8Type.instance.decompose("TEMPERATURE"), System.currentTimeMillis()));
+
+        // sensor_type ='TEMPERATURE' AND value = 24.56
+        Operation op = new Operation.Builder(OperationType.AND, controller,
+                                        new SimpleExpression(sensorType, Operator.EQ, UTF8Type.instance.decompose("TEMPERATURE")),
+                                        new SimpleExpression(value, Operator.EQ, DoubleType.instance.decompose(24.56))).complete();
+
+        Assert.assertTrue(op.satisfiedBy(row, staticRow, false));
+
+        // sensor_type ='TEMPERATURE' AND value = 30
+        op = new Operation.Builder(OperationType.AND, controller,
+                                             new SimpleExpression(sensorType, Operator.EQ, UTF8Type.instance.decompose("TEMPERATURE")),
+                                             new SimpleExpression(value, Operator.EQ, DoubleType.instance.decompose(30.00))).complete();
+
+        Assert.assertFalse(op.satisfiedBy(row, staticRow, false));
+
+        // sensor_type ='PRESSURE' OR value = 24.56
+        op = new Operation.Builder(OperationType.OR, controller,
+                                             new SimpleExpression(sensorType, Operator.EQ, UTF8Type.instance.decompose("TEMPERATURE")),
+                                             new SimpleExpression(value, Operator.EQ, DoubleType.instance.decompose(24.56))).complete();
+
+        Assert.assertTrue(op.satisfiedBy(row, staticRow, false));
+
+        // sensor_type ='PRESSURE' OR value = 30
+        op = new Operation.Builder(OperationType.AND, controller,
+                                   new SimpleExpression(sensorType, Operator.EQ, UTF8Type.instance.decompose("PRESSURE")),
+                                   new SimpleExpression(value, Operator.EQ, DoubleType.instance.decompose(30.00))).complete();
+
+        Assert.assertFalse(op.satisfiedBy(row, staticRow, false));
+
+        // (sensor_type = 'TEMPERATURE' OR sensor_type = 'PRESSURE') AND value = 24.56
+        op = new Operation.Builder(OperationType.OR, controller,
+                                   new SimpleExpression(sensorType, Operator.EQ, UTF8Type.instance.decompose("TEMPERATURE")),
+                                   new SimpleExpression(sensorType, Operator.EQ, UTF8Type.instance.decompose("PRESSURE")))
+             .setRight(new Operation.Builder(OperationType.AND, controller,
+                                             new SimpleExpression(value, Operator.EQ, DoubleType.instance.decompose(24.56)))).complete();
+
+        Assert.assertTrue(op.satisfiedBy(row, staticRow, false));
+
+        // sensor_type = LIKE 'TEMP%'  AND value = 24.56
+        op = new Operation.Builder(OperationType.AND, controller,
+                                   new SimpleExpression(sensorType, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("TEMP")),
+                                   new SimpleExpression(value, Operator.EQ, DoubleType.instance.decompose(24.56))).complete();
+
+        Assert.assertTrue(op.satisfiedBy(row, staticRow, false));
+    }
+
+    private static class SimpleExpression extends RowFilter.Expression
+    {
+        SimpleExpression(ColumnDefinition column, Operator operator, ByteBuffer value)
+        {
+            super(column, operator, value);
+        }
+
+        @Override
+        protected Kind kind()
+        {
+            return Kind.SIMPLE;
+        }
+
+        @Override
+        public boolean isSatisfiedBy(CFMetaData metadata, DecoratedKey partitionKey, Row row)
+        {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    private static Unfiltered buildRow(Cell... cells)
+    {
+        return buildRow(Clustering.EMPTY, null, cells);
+    }
+
+    private static Row buildRow(Row.Deletion deletion, Cell... cells)
+    {
+        return buildRow(Clustering.EMPTY, deletion, cells);
+    }
+
+    private static Row buildRow(Clustering clustering, Cell... cells)
+    {
+        return buildRow(clustering, null, cells);
+    }
+
+    private static Row buildRow(Clustering clustering, Row.Deletion deletion, Cell... cells)
+    {
+        Row.Builder rowBuilder = BTreeRow.sortedBuilder();
+        rowBuilder.newRow(clustering);
+        for (Cell c : cells)
+            rowBuilder.addCell(c);
+
+        if (deletion != null)
+            rowBuilder.addRowDeletion(deletion);
+
+        return rowBuilder.build();
+    }
+
+    private static Cell buildCell(ColumnDefinition column, ByteBuffer value, long timestamp)
+    {
+        return BufferCell.live(column, timestamp, value);
+    }
+
+    private static Cell deletedCell(ColumnDefinition column, long timestamp, int nowInSeconds)
+    {
+        return BufferCell.tombstone(column, timestamp, nowInSeconds);
+    }
+
+    private static ColumnDefinition getColumn(ByteBuffer name)
+    {
+        return getColumn(BACKEND, name);
+    }
+
+    private static ColumnDefinition getColumn(ColumnFamilyStore cfs, ByteBuffer name)
+    {
+        return cfs.metadata.getColumnDefinition(name);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/utils/LongIterator.java b/test/unit/org/apache/cassandra/index/sasi/utils/LongIterator.java
new file mode 100644
index 0000000..205d28f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/utils/LongIterator.java
@@ -0,0 +1,114 @@
+/*
+ * 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.index.sasi.utils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import com.carrotsearch.hppc.LongOpenHashSet;
+import com.carrotsearch.hppc.LongSet;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.index.sasi.disk.Token;
+
+public class LongIterator extends RangeIterator<Long, Token>
+{
+    private final List<LongToken> tokens;
+    private int currentIdx = 0;
+
+    public LongIterator(long[] tokens)
+    {
+        super(tokens.length == 0 ? null : tokens[0], tokens.length == 0 ? null : tokens[tokens.length - 1], tokens.length);
+        this.tokens = new ArrayList<>(tokens.length);
+        for (long token : tokens)
+            this.tokens.add(new LongToken(token));
+    }
+
+    @Override
+    protected Token computeNext()
+    {
+        if (currentIdx >= tokens.size())
+            return endOfData();
+
+        return tokens.get(currentIdx++);
+    }
+
+    @Override
+    protected void performSkipTo(Long nextToken)
+    {
+        for (int i = currentIdx == 0 ? 0 : currentIdx - 1; i < tokens.size(); i++)
+        {
+            LongToken token = tokens.get(i);
+            if (token.get().compareTo(nextToken) >= 0)
+            {
+                currentIdx = i;
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void close() throws IOException
+    {}
+
+    public static class LongToken extends Token
+    {
+        public LongToken(long token)
+        {
+            super(token);
+        }
+
+        @Override
+        public void merge(CombinedValue<Long> other)
+        {
+            // no-op
+        }
+
+        @Override
+        public LongSet getOffsets()
+        {
+            return new LongOpenHashSet(4);
+        }
+
+        @Override
+        public Iterator<DecoratedKey> iterator()
+        {
+            return Collections.emptyIterator();
+        }
+    }
+
+    public static List<Long> convert(RangeIterator<Long, Token> tokens)
+    {
+        List<Long> results = new ArrayList<>();
+        while (tokens.hasNext())
+            results.add(tokens.next().get());
+
+        return results;
+    }
+
+    public static List<Long> convert(final long... nums)
+    {
+        return new ArrayList<Long>(nums.length)
+        {{
+                for (long n : nums)
+                    add(n);
+        }};
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/utils/LongIteratorTest.java b/test/unit/org/apache/cassandra/index/sasi/utils/LongIteratorTest.java
new file mode 100644
index 0000000..05db33a
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/utils/LongIteratorTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.index.sasi.utils;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.apache.cassandra.index.sasi.utils.LongIterator.convert;
+
+public class LongIteratorTest
+{
+    @Test
+    public void testEmptyIterator() throws IOException {
+        LongIterator it = new LongIterator(new long[] { });
+
+        Assert.assertEquals(0, it.getCount());
+        Assert.assertEquals(null, it.getCurrent());
+        Assert.assertEquals(null, it.getMaximum());
+        Assert.assertEquals(null, it.getMinimum());
+        Assert.assertFalse(it.hasNext());
+
+        it.close();
+    }
+
+    @Test
+    public void testBasicITerator() throws IOException {
+        LongIterator it = new LongIterator(new long[] { 2L, 3L, 5L, 6L });
+
+        Assert.assertEquals(4L, (long) it.getCount());
+        Assert.assertEquals(2L, (long) it.getCurrent());
+        Assert.assertEquals(6L, (long) it.getMaximum());
+        Assert.assertEquals(2L, (long) it.getMinimum());
+
+        Assert.assertEquals(2L, (long) it.next().get());
+        Assert.assertEquals(3L, (long) it.next().get());
+
+        it.close();
+    }
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/index/sasi/utils/MappedBufferTest.java b/test/unit/org/apache/cassandra/index/sasi/utils/MappedBufferTest.java
new file mode 100644
index 0000000..7ffebf1
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/utils/MappedBufferTest.java
@@ -0,0 +1,540 @@
+/*
+ * 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.index.sasi.utils;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.io.util.ChannelProxy;
+import org.apache.cassandra.io.util.FileUtils;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class MappedBufferTest
+{
+    @Test
+    public void testBasicWriteThenRead() throws Exception
+    {
+        long numLongs = 10000;
+        final MappedBuffer buffer = createTestFile(numLongs);
+
+        Assert.assertEquals(0, buffer.position());
+        for (long i = 0; i < numLongs; i++)
+        {
+            Assert.assertEquals(i * 8, buffer.position());
+            Assert.assertEquals(i, buffer.getLong());
+        }
+
+        buffer.position(0);
+        for (long i = 0; i < numLongs; i++)
+        {
+            Assert.assertEquals(i, buffer.getLong(i * 8));
+            Assert.assertEquals(0, buffer.position());
+        }
+
+        // read all the numbers as shorts (all numbers fit into four bytes)
+        for (long i = 0; i < Math.min(Integer.MAX_VALUE, numLongs); i++)
+            Assert.assertEquals(i, buffer.getInt((i * 8) + 4));
+
+        // read all the numbers as shorts (all numbers fit into two bytes)
+        for (long i = 0; i < Math.min(Short.MAX_VALUE, numLongs); i++) {
+            Assert.assertEquals(i, buffer.getShort((i * 8) + 6));
+        }
+
+        // read all the numbers that can be represented as a single byte
+        for (long i = 0; i < 128; i++)
+            Assert.assertEquals(i, buffer.get((i * 8) + 7));
+
+        buffer.close();
+    }
+
+    @Test
+    public void testDuplicate() throws Exception
+    {
+        long numLongs = 10;
+        final MappedBuffer buffer1 = createTestFile(numLongs);
+
+        Assert.assertEquals(0, buffer1.getLong());
+        Assert.assertEquals(1, buffer1.getLong());
+
+        final MappedBuffer buffer2 = buffer1.duplicate();
+
+        Assert.assertEquals(2, buffer1.getLong());
+        Assert.assertEquals(2, buffer2.getLong());
+
+        buffer2.position(0);
+        Assert.assertEquals(3, buffer1.getLong());
+        Assert.assertEquals(0, buffer2.getLong());
+    }
+
+    @Test
+    public void testLimit() throws Exception
+    {
+        long numLongs =  10;
+        final MappedBuffer buffer1 = createTestFile(numLongs);
+
+        MappedBuffer buffer2 = buffer1.duplicate().position(16).limit(32);
+        buffer1.position(0).limit(16);
+        List<Long> longs = new ArrayList<>(4);
+
+        while (buffer1.hasRemaining())
+            longs.add(buffer1.getLong());
+
+        while (buffer2.hasRemaining())
+            longs.add(buffer2.getLong());
+
+        Assert.assertArrayEquals(new Long[]{0L, 1L, 2L, 3L}, longs.toArray());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testPositionGreaterThanLimit() throws Exception
+    {
+        final MappedBuffer buffer = createTestFile(1);
+
+        buffer.limit(4);
+
+        try
+        {
+            buffer.position(buffer.limit() + 1);
+        }
+        finally
+        {
+            buffer.close();
+        }
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testNegativePosition() throws Exception
+    {
+        try (MappedBuffer buffer = createTestFile(1))
+        {
+            buffer.position(-1);
+        }
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testLimitGreaterThanCapacity() throws Exception
+    {
+        try (MappedBuffer buffer = createTestFile(1))
+        {
+            buffer.limit(buffer.capacity() + 1);
+        }
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testLimitLessThanPosition() throws Exception
+    {
+        final MappedBuffer buffer = createTestFile(1);
+
+        buffer.position(1);
+
+        try
+        {
+            buffer.limit(0);
+        }
+        finally
+        {
+            buffer.close();
+        }
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetRelativeUnderflow() throws Exception
+    {
+        final MappedBuffer buffer = createTestFile(1);
+
+        buffer.position(buffer.limit());
+        try
+        {
+            buffer.get();
+        }
+        finally
+        {
+            buffer.close();
+        }
+
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetAbsoluteGreaterThanCapacity() throws Exception
+    {
+        try (MappedBuffer buffer = createTestFile(1))
+        {
+            buffer.get(buffer.limit());
+        }
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetAbsoluteNegativePosition() throws Exception
+    {
+        try (MappedBuffer buffer = createTestFile(1))
+        {
+            buffer.get(-1);
+        }
+    }
+
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetShortRelativeUnderflow() throws Exception
+    {
+        final MappedBuffer buffer = createTestFile(1);
+
+        buffer.position(buffer.capacity() - 1);
+        try
+        {
+            buffer.getShort();
+        }
+        finally
+        {
+            buffer.close();
+        }
+
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetShortAbsoluteGreaterThanCapacity() throws Exception
+    {
+        final MappedBuffer buffer = createTestFile(1);
+
+        Assert.assertEquals(8, buffer.capacity());
+        try
+        {
+            buffer.getShort(buffer.capacity() - 1);
+        }
+        finally
+        {
+            buffer.close();
+        }
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetShortAbsoluteNegativePosition() throws Exception
+    {
+        try (MappedBuffer buffer = createTestFile(1))
+        {
+            buffer.getShort(-1);
+        }
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetIntRelativeUnderflow() throws Exception
+    {
+        final MappedBuffer buffer = createTestFile(1);
+
+        buffer.position(buffer.capacity() - 3);
+        try
+        {
+            buffer.getInt();
+        }
+        finally
+        {
+            buffer.close();
+        }
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetIntAbsoluteGreaterThanCapacity() throws Exception
+    {
+        final MappedBuffer buffer = createTestFile(1);
+
+        Assert.assertEquals(8, buffer.capacity());
+        try
+        {
+            buffer.getInt(buffer.capacity() - 3);
+        }
+        finally
+        {
+            buffer.close();
+        }
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetIntAbsoluteNegativePosition() throws Exception
+    {
+        try (MappedBuffer buffer = createTestFile(1))
+        {
+            buffer.getInt(-1);
+        }
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetLongRelativeUnderflow() throws Exception
+    {
+        final MappedBuffer buffer = createTestFile(1);
+
+        buffer.position(buffer.capacity() - 7);
+        try
+        {
+            buffer.getLong();
+        }
+        finally
+        {
+            buffer.close();
+        }
+
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetLongAbsoluteGreaterThanCapacity() throws Exception
+    {
+        final MappedBuffer buffer = createTestFile(1);
+
+        Assert.assertEquals(8, buffer.capacity());
+        try
+        {
+            buffer.getLong(buffer.capacity() - 7);
+        }
+        finally
+        {
+            buffer.close();
+        }
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetLongAbsoluteNegativePosition() throws Exception
+    {
+        try (MappedBuffer buffer = createTestFile(1))
+        {
+            buffer.getLong(-1);
+        }
+    }
+
+    @Test
+    public void testGetPageRegion() throws Exception
+    {
+        ThreadLocalRandom random = ThreadLocalRandom.current();
+
+        int numLongs = 1000;
+        int byteSize = 8;
+        int capacity = numLongs * byteSize;
+        try (MappedBuffer buffer = createTestFile(numLongs))
+        {
+            for (int i = 0; i < 1000; i++)
+            {
+                // offset, length are always aligned on sizeof(long)
+                int offset = random.nextInt(0, 1000 * byteSize - byteSize) & ~(byteSize - 1);
+                int length = Math.min(capacity, random.nextInt(byteSize, capacity - offset) & ~(byteSize - 1));
+
+                ByteBuffer region = buffer.getPageRegion(offset, length);
+                for (int j = offset; j < (offset + length); j += 8)
+                    Assert.assertEquals(j / 8, region.getLong(j));
+            }
+        }
+    }
+
+    @Test (expected = IllegalArgumentException.class)
+    public void testMisalignedRegionAccess() throws Exception
+    {
+        try (MappedBuffer buffer = createTestFile(100, 8, 4, 0))
+        {
+            buffer.getPageRegion(13, 27);
+        }
+    }
+
+    @Test
+    public void testSequentialIterationWithPadding() throws Exception
+    {
+        long numValues = 1000;
+        int maxPageBits = 6; // 64 bytes page
+        int[] paddings = new int[] { 0, 3, 5, 7, 9, 11, 13 };
+
+        // test different page sizes, with different padding and types
+        for (int numPageBits = 3; numPageBits <= maxPageBits; numPageBits++)
+        {
+            for (int typeSize = 2; typeSize <= 8; typeSize *= 2)
+            {
+                for (int padding : paddings)
+                {
+                    try (MappedBuffer buffer = createTestFile(numValues, typeSize, numPageBits, padding))
+                    {
+                        long offset = 0;
+                        for (long j = 0; j < numValues; j++)
+                        {
+                            switch (typeSize)
+                            {
+                                case 2:
+                                    Assert.assertEquals(j, buffer.getShort(offset));
+                                    break;
+
+                                case 4:
+                                    Assert.assertEquals(j, buffer.getInt(offset));
+                                    break;
+
+                                case 8:
+                                    Assert.assertEquals(j, buffer.getLong(offset));
+                                    break;
+
+                                default:
+                                    throw new AssertionError();
+                            }
+
+                            offset += typeSize + padding;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testSequentialIteration() throws IOException
+    {
+        long numValues = 1000;
+        for (int typeSize = 2; typeSize <= 8; typeSize *= 2)
+        {
+            try (MappedBuffer buffer = createTestFile(numValues, typeSize, 16, 0))
+            {
+                for (int j = 0; j < numValues; j++)
+                {
+                    Assert.assertEquals(j * typeSize, buffer.position());
+
+                    switch (typeSize)
+                    {
+                        case 2:
+                            Assert.assertEquals(j, buffer.getShort());
+                            break;
+
+                        case 4:
+                            Assert.assertEquals(j, buffer.getInt());
+                            break;
+
+                        case 8:
+                            Assert.assertEquals(j, buffer.getLong());
+                            break;
+
+                        default:
+                            throw new AssertionError();
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testCompareToPage() throws IOException
+    {
+        long numValues = 100;
+        int typeSize = 8;
+
+        try (MappedBuffer buffer = createTestFile(numValues))
+        {
+            for (long i = 0; i < numValues * typeSize; i += typeSize)
+            {
+                long value = i / typeSize;
+                Assert.assertEquals(0, buffer.comparePageTo(i, typeSize, LongType.instance, LongType.instance.decompose(value)));
+            }
+        }
+    }
+
+    @Test
+    public void testOpenWithoutPageBits() throws IOException
+    {
+        File tmp = File.createTempFile("mapped-buffer", "tmp");
+        tmp.deleteOnExit();
+
+        RandomAccessFile file = new RandomAccessFile(tmp, "rw");
+
+        long numValues = 1000;
+        for (long i = 0; i < numValues; i++)
+            file.writeLong(i);
+
+        file.getFD().sync();
+
+        try (MappedBuffer buffer = new MappedBuffer(new ChannelProxy(tmp.getAbsolutePath(), file.getChannel())))
+        {
+            Assert.assertEquals(numValues * 8, buffer.limit());
+            Assert.assertEquals(numValues * 8, buffer.capacity());
+
+            for (long i = 0; i < numValues; i++)
+            {
+                Assert.assertEquals(i * 8, buffer.position());
+                Assert.assertEquals(i, buffer.getLong());
+            }
+        }
+        finally
+        {
+            FileUtils.closeQuietly(file);
+        }
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testIncorrectPageSize() throws Exception
+    {
+        new MappedBuffer(null, 33);
+    }
+
+    private MappedBuffer createTestFile(long numCount) throws IOException
+    {
+        return createTestFile(numCount, 8, 16, 0);
+    }
+
+    private MappedBuffer createTestFile(long numCount, int typeSize, int numPageBits, int padding) throws IOException
+    {
+        final File testFile = File.createTempFile("mapped-buffer-test", "db");
+        testFile.deleteOnExit();
+
+        RandomAccessFile file = new RandomAccessFile(testFile, "rw");
+
+        for (long i = 0; i < numCount; i++)
+        {
+
+            switch (typeSize)
+            {
+                case 1:
+                    file.write((byte) i);
+                    break;
+
+                case 2:
+                    file.writeShort((short) i);
+                    break;
+
+                case 4:
+                    file.writeInt((int) i);
+                    break;
+
+                case 8:
+                    // bunch of longs
+                    file.writeLong(i);
+                    break;
+
+                default:
+                    throw new IllegalArgumentException("unknown byte size: " + typeSize);
+            }
+
+            for (int j = 0; j < padding; j++)
+                file.write(0);
+        }
+
+        file.getFD().sync();
+
+        try
+        {
+            return new MappedBuffer(new ChannelProxy(testFile.getAbsolutePath(), file.getChannel()), numPageBits);
+        }
+        finally
+        {
+            FileUtils.closeQuietly(file);
+        }
+    }
+
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/utils/RangeIntersectionIteratorTest.java b/test/unit/org/apache/cassandra/index/sasi/utils/RangeIntersectionIteratorTest.java
new file mode 100644
index 0000000..4dc9e3f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/utils/RangeIntersectionIteratorTest.java
@@ -0,0 +1,463 @@
+/*
+ * 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.index.sasi.utils;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.index.sasi.utils.RangeIntersectionIterator.Strategy;
+import org.apache.cassandra.index.sasi.utils.RangeIntersectionIterator.LookupIntersectionIterator;
+import org.apache.cassandra.index.sasi.utils.RangeIntersectionIterator.BounceIntersectionIterator;
+import org.apache.cassandra.io.util.FileUtils;
+
+import com.carrotsearch.hppc.LongOpenHashSet;
+import com.carrotsearch.hppc.LongSet;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import static org.apache.cassandra.index.sasi.utils.LongIterator.convert;
+
+public class RangeIntersectionIteratorTest
+{
+    @Test
+    public void testNoOverlappingValues()
+    {
+        for (Strategy strategy : Strategy.values())
+            testNoOverlappingValues(strategy);
+    }
+
+    private void testNoOverlappingValues(Strategy strategy)
+    {
+        RangeIterator.Builder<Long, Token> builder = RangeIntersectionIterator.builder(strategy);
+
+        builder.add(new LongIterator(new long[] { 2L, 3L, 5L, 6L }));
+        builder.add(new LongIterator(new long[] { 1L, 7L }));
+        builder.add(new LongIterator(new long[] { 4L, 8L, 9L, 10L }));
+
+        Assert.assertEquals(convert(), convert(builder.build()));
+
+        builder = RangeIntersectionIterator.builder(strategy);
+        // both ranges overlap by min/max but not by value
+        builder.add(new LongIterator(new long[] { 1L, 5L, 7L, 9L }));
+        builder.add(new LongIterator(new long[] { 6L }));
+
+        RangeIterator<Long, Token> range = builder.build();
+
+        Assert.assertNotNull(range);
+        Assert.assertFalse(range.hasNext());
+
+        builder = RangeIntersectionIterator.builder(strategy);
+        // both ranges overlap by min/max but not by value
+        builder.add(new LongIterator(new long[] { 1L, 5L, 7L, 9L }));
+        builder.add(new LongIterator(new long[] { 0L, 10L, 12L }));
+
+        range = builder.build();
+
+        Assert.assertNotNull(range);
+        Assert.assertFalse(range.hasNext());
+    }
+
+    @Test
+    public void testOverlappingValues()
+    {
+        for (Strategy strategy : Strategy.values())
+            testOverlappingValues(strategy);
+    }
+
+    private void testOverlappingValues(Strategy strategy)
+    {
+        RangeIterator.Builder<Long, Token> builder = RangeIntersectionIterator.builder(strategy);
+
+        builder.add(new LongIterator(new long[] { 1L, 4L, 6L, 7L }));
+        builder.add(new LongIterator(new long[] { 2L, 4L, 5L, 6L }));
+        builder.add(new LongIterator(new long[] { 4L, 6L, 8L, 9L, 10L }));
+
+        Assert.assertEquals(convert(4L, 6L), convert(builder.build()));
+    }
+
+    @Test
+    public void testSingleIterator()
+    {
+        for (Strategy strategy : Strategy.values())
+            testSingleIterator(strategy);
+    }
+
+    private void testSingleIterator(Strategy strategy)
+    {
+        RangeIntersectionIterator.Builder<Long, Token> builder = RangeIntersectionIterator.builder(strategy);
+
+        builder.add(new LongIterator(new long[] { 1L, 2L, 4L, 9L }));
+
+        Assert.assertEquals(convert(1L, 2L, 4L, 9L), convert(builder.build()));
+    }
+
+    @Test
+    public void testSkipTo()
+    {
+        for (Strategy strategy : Strategy.values())
+            testSkipTo(strategy);
+    }
+
+    private void testSkipTo(Strategy strategy)
+    {
+        RangeIterator.Builder<Long, Token> builder = RangeIntersectionIterator.builder(strategy);
+
+        builder.add(new LongIterator(new long[] { 1L, 4L, 6L, 7L, 9L, 10L }));
+        builder.add(new LongIterator(new long[] { 2L, 4L, 5L, 6L, 7L, 10L, 12L }));
+        builder.add(new LongIterator(new long[] { 4L, 6L, 7L, 9L, 10L }));
+
+        RangeIterator<Long, Token> range = builder.build();
+        Assert.assertNotNull(range);
+
+        // first let's skipTo something before range
+        Assert.assertEquals(4L, (long) range.skipTo(3L).get());
+        Assert.assertEquals(4L, (long) range.getCurrent());
+
+        // now let's skip right to the send value
+        Assert.assertEquals(6L, (long) range.skipTo(5L).get());
+        Assert.assertEquals(6L, (long) range.getCurrent());
+
+        // now right to the element
+        Assert.assertEquals(7L, (long) range.skipTo(7L).get());
+        Assert.assertEquals(7L, (long) range.getCurrent());
+        Assert.assertEquals(7L, (long) range.next().get());
+
+        Assert.assertTrue(range.hasNext());
+        Assert.assertEquals(10L, (long) range.getCurrent());
+
+        // now right after the last element
+        Assert.assertNull(range.skipTo(11L));
+        Assert.assertFalse(range.hasNext());
+    }
+
+    @Test
+    public void testMinMaxAndCount()
+    {
+        for (Strategy strategy : Strategy.values())
+            testMinMaxAndCount(strategy);
+    }
+
+    private void testMinMaxAndCount(Strategy strategy)
+    {
+        RangeIterator.Builder<Long, Token> builder = RangeIntersectionIterator.builder(strategy);
+
+        builder.add(new LongIterator(new long[]{1L, 2L, 9L}));
+        builder.add(new LongIterator(new long[]{4L, 5L, 9L}));
+        builder.add(new LongIterator(new long[]{7L, 8L, 9L}));
+
+        Assert.assertEquals(9L, (long) builder.getMaximum());
+        Assert.assertEquals(9L, builder.getTokenCount());
+
+        RangeIterator<Long, Token> tokens = builder.build();
+
+        Assert.assertNotNull(tokens);
+        Assert.assertEquals(7L, (long) tokens.getMinimum());
+        Assert.assertEquals(9L, (long) tokens.getMaximum());
+        Assert.assertEquals(9L, tokens.getCount());
+
+        Assert.assertEquals(convert(9L), convert(builder.build()));
+    }
+
+    @Test
+    public void testBuilder()
+    {
+        for (Strategy strategy : Strategy.values())
+            testBuilder(strategy);
+    }
+
+    private void testBuilder(Strategy strategy)
+    {
+        RangeIterator.Builder<Long, Token> builder = RangeIntersectionIterator.builder(strategy);
+
+        Assert.assertNull(builder.getMinimum());
+        Assert.assertNull(builder.getMaximum());
+        Assert.assertEquals(0L, builder.getTokenCount());
+        Assert.assertEquals(0L, builder.rangeCount());
+
+        builder.add(new LongIterator(new long[] { 1L, 2L, 6L }));
+        builder.add(new LongIterator(new long[] { 4L, 5L, 6L }));
+        builder.add(new LongIterator(new long[] { 6L, 8L, 9L }));
+
+        Assert.assertEquals(6L, (long) builder.getMinimum());
+        Assert.assertEquals(6L, (long) builder.getMaximum());
+        Assert.assertEquals(9L, builder.getTokenCount());
+        Assert.assertEquals(3L, builder.rangeCount());
+        Assert.assertFalse(builder.statistics.isDisjoint());
+
+        Assert.assertEquals(1L, (long) builder.ranges.poll().getMinimum());
+        Assert.assertEquals(4L, (long) builder.ranges.poll().getMinimum());
+        Assert.assertEquals(6L, (long) builder.ranges.poll().getMinimum());
+
+        builder.add(new LongIterator(new long[] { 1L, 2L, 6L }));
+        builder.add(new LongIterator(new long[] { 4L, 5L, 6L }));
+        builder.add(new LongIterator(new long[] { 6L, 8L, 9L }));
+
+        Assert.assertEquals(convert(6L), convert(builder.build()));
+
+        builder = RangeIntersectionIterator.builder(strategy);
+        builder.add(new LongIterator(new long[]{ 1L, 5L, 6L }));
+        builder.add(new LongIterator(new long[]{ 3L, 5L, 6L }));
+
+        RangeIterator<Long, Token> tokens = builder.build();
+
+        Assert.assertEquals(convert(5L, 6L), convert(tokens));
+
+        FileUtils.closeQuietly(tokens);
+
+        RangeIterator emptyTokens = RangeIntersectionIterator.builder(strategy).build();
+        Assert.assertEquals(0, emptyTokens.getCount());
+
+        builder = RangeIntersectionIterator.builder(strategy);
+        Assert.assertEquals(0L, builder.add((RangeIterator<Long, Token>) null).rangeCount());
+        Assert.assertEquals(0L, builder.add((List<RangeIterator<Long, Token>>) null).getTokenCount());
+        Assert.assertEquals(0L, builder.add(new LongIterator(new long[] {})).rangeCount());
+
+        RangeIterator<Long, Token> single = new LongIterator(new long[] { 1L, 2L, 3L });
+        RangeIterator<Long, Token> range = RangeIntersectionIterator.<Long, Token>builder().add(single).build();
+
+        // because build should return first element if it's only one instead of building yet another iterator
+        Assert.assertEquals(range, single);
+
+        // Make a difference between empty and null ranges.
+        builder = RangeIntersectionIterator.builder(strategy);
+        builder.add(new LongIterator(new long[] {}));
+        Assert.assertEquals(0L, builder.rangeCount());
+        builder.add(single);
+        Assert.assertEquals(1L, builder.rangeCount());
+        range = builder.build();
+        Assert.assertEquals(0, range.getCount());
+
+        // disjoint case
+        builder = RangeIntersectionIterator.builder();
+        builder.add(new LongIterator(new long[] { 1L, 2L, 3L }));
+        builder.add(new LongIterator(new long[] { 4L, 5L, 6L }));
+
+        Assert.assertTrue(builder.statistics.isDisjoint());
+
+        RangeIterator<Long, Token> disjointIntersection = builder.build();
+        Assert.assertNotNull(disjointIntersection);
+        Assert.assertFalse(disjointIntersection.hasNext());
+
+    }
+
+    @Test
+    public void emptyRangeTest() {
+        RangeIterator.Builder<Long, Token> builder;
+
+        // empty, then non-empty
+        builder = RangeIntersectionIterator.builder();
+        builder.add(new LongIterator(new long[] {}));
+        builder.add(new LongIterator(new long[] {10}));
+        assertEmpty(builder.build());
+
+        builder = RangeIntersectionIterator.builder();
+        builder.add(new LongIterator(new long[] {}));
+        for (int i = 0; i < 10; i++)
+            builder.add(new LongIterator(new long[] {0, i + 10}));
+        assertEmpty(builder.build());
+
+        // non-empty, then empty
+        builder = RangeIntersectionIterator.builder();
+        builder.add(new LongIterator(new long[] {10}));
+        builder.add(new LongIterator(new long[] {}));
+        assertEmpty(builder.build());
+
+        builder = RangeIntersectionIterator.builder();
+        for (int i = 0; i < 10; i++)
+            builder.add(new LongIterator(new long[] {0, i + 10}));
+
+        builder.add(new LongIterator(new long[] {}));
+        assertEmpty(builder.build());
+
+        // empty, then non-empty then empty again
+        builder = RangeIntersectionIterator.builder();
+        builder.add(new LongIterator(new long[] {}));
+        builder.add(new LongIterator(new long[] {0, 10}));
+        builder.add(new LongIterator(new long[] {}));
+        assertEmpty(builder.build());
+
+        builder = RangeIntersectionIterator.builder();
+        builder.add(new LongIterator(new long[] {}));
+        for (int i = 0; i < 10; i++)
+            builder.add(new LongIterator(new long[] {0, i + 10}));
+        builder.add(new LongIterator(new long[] {}));
+        assertEmpty(builder.build());
+
+        // non-empty, empty, then non-empty again
+        builder = RangeIntersectionIterator.builder();
+        builder.add(new LongIterator(new long[] {0, 10}));
+        builder.add(new LongIterator(new long[] {}));
+        builder.add(new LongIterator(new long[] {0, 10}));
+        assertEmpty(builder.build());
+
+        builder = RangeIntersectionIterator.builder();
+        for (int i = 0; i < 5; i++)
+            builder.add(new LongIterator(new long[] {0, i + 10}));
+        builder.add(new LongIterator(new long[] {}));
+        for (int i = 5; i < 10; i++)
+            builder.add(new LongIterator(new long[] {0, i + 10}));
+        assertEmpty(builder.build());
+    }
+
+    public static void assertEmpty(RangeIterator<Long, Token> range)
+    {
+        Assert.assertNull(range.getMinimum());
+        Assert.assertNull(range.getMaximum());
+        Assert.assertFalse(range.hasNext());
+        Assert.assertEquals(0, range.getCount());
+    }
+
+    @Test
+    public void testClose() throws IOException
+    {
+        for (Strategy strategy : Strategy.values())
+            testClose(strategy);
+    }
+
+    private void testClose(Strategy strategy) throws IOException
+    {
+        RangeIterator<Long, Token> tokens = RangeIntersectionIterator.<Long, Token>builder(strategy)
+                                            .add(new LongIterator(new long[] { 1L, 2L, 3L }))
+                                            .build();
+
+        Assert.assertNotNull(tokens);
+        tokens.close();
+    }
+
+    @Test
+    public void testIsOverlapping()
+    {
+        RangeIterator<Long, Token> rangeA, rangeB;
+
+        rangeA = new LongIterator(new long[] { 1L, 5L });
+        rangeB = new LongIterator(new long[] { 5L, 9L });
+        Assert.assertTrue(RangeIterator.isOverlapping(rangeA, rangeB));
+
+        rangeA = new LongIterator(new long[] { 5L, 9L });
+        rangeB = new LongIterator(new long[] { 1L, 6L });
+        Assert.assertTrue(RangeIterator.isOverlapping(rangeA, rangeB));
+
+        rangeA = new LongIterator(new long[] { 5L, 9L });
+        rangeB = new LongIterator(new long[] { 5L, 9L });
+        Assert.assertTrue(RangeIterator.isOverlapping(rangeA, rangeB));
+
+        rangeA = new LongIterator(new long[] { 1L, 4L });
+        rangeB = new LongIterator(new long[] { 5L, 9L });
+        Assert.assertFalse(RangeIterator.isOverlapping(rangeA, rangeB));
+
+        rangeA = new LongIterator(new long[] { 6L, 9L });
+        rangeB = new LongIterator(new long[] { 1L, 4L });
+        Assert.assertFalse(RangeIterator.isOverlapping(rangeA, rangeB));
+    }
+
+    @Test
+    public void testIntersectionOfRandomRanges()
+    {
+        for (Strategy strategy : Strategy.values())
+            testIntersectionOfRandomRanges(strategy);
+    }
+
+    private void testIntersectionOfRandomRanges(Strategy strategy)
+    {
+        for (int attempt = 0; attempt < 16; attempt++)
+        {
+            final ThreadLocalRandom random = ThreadLocalRandom.current();
+            final int maxRanges = random.nextInt(2, 16);
+
+            // generate randomize ranges
+            long[][] ranges = new long[maxRanges][];
+            for (int i = 0; i < ranges.length; i++)
+            {
+                int rangeSize = random.nextInt(16, 512);
+                LongSet range = new LongOpenHashSet(rangeSize);
+
+                for (int j = 0; j < rangeSize; j++)
+                    range.add(random.nextLong(0, 100));
+
+                ranges[i] = range.toArray();
+                Arrays.sort(ranges[i]);
+            }
+
+            List<Long> expected = new ArrayList<>();
+            // determine unique tokens which intersect every range
+            for (long token : ranges[0])
+            {
+                boolean intersectsAll = true;
+                for (int i = 1; i < ranges.length; i++)
+                {
+                    if (Arrays.binarySearch(ranges[i], token) < 0)
+                    {
+                        intersectsAll = false;
+                        break;
+                    }
+                }
+
+                if (intersectsAll)
+                    expected.add(token);
+            }
+
+            RangeIterator.Builder<Long, Token> builder = RangeIntersectionIterator.builder(strategy);
+            for (long[] range : ranges)
+                builder.add(new LongIterator(range));
+
+            Assert.assertEquals(expected, convert(builder.build()));
+        }
+    }
+
+    @Test
+    public void testIteratorPeeking()
+    {
+        RangeIterator.Builder<Long, Token> builder = RangeIntersectionIterator.builder();
+
+        // iterator with only one element
+        builder.add(new LongIterator(new long[] { 10L }));
+
+        // iterator with 150 elements (lookup is going to be advantageous over bound in this case)
+        long[] tokens = new long[150];
+        for (int i = 0; i < tokens.length; i++)
+            tokens[i] = i;
+
+        builder.add(new LongIterator(tokens));
+
+        RangeIterator<Long, Token> intersection = builder.build();
+
+        Assert.assertNotNull(intersection);
+        Assert.assertEquals(LookupIntersectionIterator.class, intersection.getClass());
+
+        Assert.assertTrue(intersection.hasNext());
+        Assert.assertEquals(convert(10L), convert(intersection));
+
+        builder = RangeIntersectionIterator.builder();
+
+        builder.add(new LongIterator(new long[] { 1L, 3L, 5L, 7L, 9L }));
+        builder.add(new LongIterator(new long[] { 1L, 2L, 5L, 6L }));
+
+        intersection = builder.build();
+
+        // in the situation when there is a similar number of elements inside ranges
+        // ping-pong (bounce) intersection is preferred as it covers gaps quicker then linear scan + lookup.
+        Assert.assertNotNull(intersection);
+        Assert.assertEquals(BounceIntersectionIterator.class, intersection.getClass());
+
+        Assert.assertTrue(intersection.hasNext());
+        Assert.assertEquals(convert(1L, 5L), convert(intersection));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/utils/RangeUnionIteratorTest.java b/test/unit/org/apache/cassandra/index/sasi/utils/RangeUnionIteratorTest.java
new file mode 100644
index 0000000..162b1c6
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sasi/utils/RangeUnionIteratorTest.java
@@ -0,0 +1,376 @@
+/*
+ * 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.index.sasi.utils;
+
+import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.apache.cassandra.index.sasi.disk.Token;
+import org.apache.cassandra.io.util.FileUtils;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import static org.apache.cassandra.index.sasi.utils.LongIterator.convert;
+
+public class RangeUnionIteratorTest
+{
+    @Test
+    public void testNoOverlappingValues()
+    {
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+
+        builder.add(new LongIterator(new long[] { 2L, 3L, 5L, 6L }));
+        builder.add(new LongIterator(new long[] { 1L, 7L }));
+        builder.add(new LongIterator(new long[] { 4L, 8L, 9L, 10L }));
+
+        Assert.assertEquals(convert(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L), convert(builder.build()));
+    }
+
+    @Test
+    public void testSingleIterator()
+    {
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+
+        builder.add(new LongIterator(new long[] { 1L, 2L, 4L, 9L }));
+
+        Assert.assertEquals(convert(1L, 2L, 4L, 9L), convert(builder.build()));
+    }
+
+    @Test
+    public void testOverlappingValues()
+    {
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+
+        builder.add(new LongIterator(new long[] { 1L, 4L, 6L, 7L }));
+        builder.add(new LongIterator(new long[] { 2L, 3L, 5L, 6L }));
+        builder.add(new LongIterator(new long[] { 4L, 6L, 8L, 9L, 10L }));
+
+        List<Long> values = convert(builder.build());
+
+        Assert.assertEquals(values.toString(), convert(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L), values);
+    }
+
+    @Test
+    public void testNoOverlappingRanges()
+    {
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+
+        builder.add(new LongIterator(new long[] { 1L, 2L, 3L }));
+        builder.add(new LongIterator(new long[] { 4L, 5L, 6L }));
+        builder.add(new LongIterator(new long[] { 7L, 8L, 9L }));
+
+        Assert.assertEquals(convert(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L), convert(builder.build()));
+    }
+
+    @Test
+    public void testTwoIteratorsWithSingleValues()
+    {
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+
+        builder.add(new LongIterator(new long[] { 1L }));
+        builder.add(new LongIterator(new long[] { 1L }));
+
+        Assert.assertEquals(convert(1L), convert(builder.build()));
+    }
+
+    @Test
+    public void testDifferentSizeIterators()
+    {
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+
+        builder.add(new LongIterator(new long[] { 2L, 3L, 5L, 6L, 12L, 13L }));
+        builder.add(new LongIterator(new long[] { 1L, 7L, 14L, 15 }));
+        builder.add(new LongIterator(new long[] { 4L, 5L, 8L, 9L, 10L }));
+
+        Assert.assertEquals(convert(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 12L, 13L, 14L, 15L), convert(builder.build()));
+    }
+
+    @Test
+    public void testRandomSequences()
+    {
+        ThreadLocalRandom random = ThreadLocalRandom.current();
+
+        long[][] values = new long[random.nextInt(1, 20)][];
+        int numTests = random.nextInt(10, 20);
+
+        for (int tests = 0; tests < numTests; tests++)
+        {
+            RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+            int totalCount = 0;
+
+            for (int i = 0; i < values.length; i++)
+            {
+                long[] part = new long[random.nextInt(1, 500)];
+                for (int j = 0; j < part.length; j++)
+                    part[j] = random.nextLong();
+
+                // all of the parts have to be sorted to mimic SSTable
+                Arrays.sort(part);
+
+                values[i] = part;
+                builder.add(new LongIterator(part));
+                totalCount += part.length;
+            }
+
+            long[] totalOrdering = new long[totalCount];
+            int index = 0;
+
+            for (long[] part : values)
+            {
+                for (long value : part)
+                    totalOrdering[index++] = value;
+            }
+
+            Arrays.sort(totalOrdering);
+
+            int count = 0;
+            RangeIterator<Long, Token> tokens = builder.build();
+
+            Assert.assertNotNull(tokens);
+            while (tokens.hasNext())
+                Assert.assertEquals(totalOrdering[count++], (long) tokens.next().get());
+
+            Assert.assertEquals(totalCount, count);
+        }
+    }
+
+    @Test
+    public void testMinMaxAndCount()
+    {
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+
+        builder.add(new LongIterator(new long[] { 1L, 2L, 3L }));
+        builder.add(new LongIterator(new long[] { 4L, 5L, 6L }));
+        builder.add(new LongIterator(new long[] { 7L, 8L, 9L }));
+
+        Assert.assertEquals(9L, (long) builder.getMaximum());
+        Assert.assertEquals(9L, builder.getTokenCount());
+
+        RangeIterator<Long, Token> tokens = builder.build();
+
+        Assert.assertNotNull(tokens);
+        Assert.assertEquals(1L, (long) tokens.getMinimum());
+        Assert.assertEquals(9L, (long) tokens.getMaximum());
+        Assert.assertEquals(9L, tokens.getCount());
+
+        for (long i = 1; i < 10; i++)
+        {
+            Assert.assertTrue(tokens.hasNext());
+            Assert.assertEquals(i, (long) tokens.next().get());
+        }
+
+        Assert.assertFalse(tokens.hasNext());
+        Assert.assertEquals(1L, (long) tokens.getMinimum());
+    }
+
+    @Test
+    public void testBuilder()
+    {
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+
+        Assert.assertNull(builder.getMinimum());
+        Assert.assertNull(builder.getMaximum());
+        Assert.assertEquals(0L, builder.getTokenCount());
+        Assert.assertEquals(0L, builder.rangeCount());
+
+        builder.add(new LongIterator(new long[] { 1L, 2L, 3L }));
+        builder.add(new LongIterator(new long[] { 4L, 5L, 6L }));
+        builder.add(new LongIterator(new long[] { 7L, 8L, 9L }));
+
+        Assert.assertEquals(1L, (long) builder.getMinimum());
+        Assert.assertEquals(9L, (long) builder.getMaximum());
+        Assert.assertEquals(9L, builder.getTokenCount());
+        Assert.assertEquals(3L, builder.rangeCount());
+        Assert.assertFalse(builder.statistics.isDisjoint());
+
+        Assert.assertEquals(1L, (long) builder.ranges.poll().getMinimum());
+        Assert.assertEquals(4L, (long) builder.ranges.poll().getMinimum());
+        Assert.assertEquals(7L, (long) builder.ranges.poll().getMinimum());
+
+        RangeIterator<Long, Token> tokens = RangeUnionIterator.build(new ArrayList<RangeIterator<Long, Token>>()
+        {{
+            add(new LongIterator(new long[]{1L, 2L, 4L}));
+            add(new LongIterator(new long[]{3L, 5L, 6L}));
+        }});
+
+        Assert.assertEquals(convert(1L, 2L, 3L, 4L, 5L, 6L), convert(tokens));
+
+        FileUtils.closeQuietly(tokens);
+
+        RangeIterator emptyTokens = RangeUnionIterator.builder().build();
+        Assert.assertEquals(0, emptyTokens.getCount());
+
+        builder = RangeUnionIterator.builder();
+        Assert.assertEquals(0L, builder.add((RangeIterator<Long, Token>) null).rangeCount());
+        Assert.assertEquals(0L, builder.add((List<RangeIterator<Long, Token>>) null).getTokenCount());
+        Assert.assertEquals(0L, builder.add(new LongIterator(new long[] {})).rangeCount());
+
+        RangeIterator<Long, Token> single = new LongIterator(new long[] { 1L, 2L, 3L });
+        RangeIterator<Long, Token> range = RangeIntersectionIterator.<Long, Token>builder().add(single).build();
+
+        // because build should return first element if it's only one instead of building yet another iterator
+        Assert.assertEquals(range, single);
+    }
+
+    @Test
+    public void testSkipTo()
+    {
+        RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
+
+        builder.add(new LongIterator(new long[]{1L, 2L, 3L}));
+        builder.add(new LongIterator(new long[]{4L, 5L, 6L}));
+        builder.add(new LongIterator(new long[]{7L, 8L, 9L}));
+
+        RangeIterator<Long, Token> tokens = builder.build();
+        Assert.assertNotNull(tokens);
+
+        tokens.skipTo(5L);
+        Assert.assertTrue(tokens.hasNext());
+        Assert.assertEquals(5L, (long) tokens.next().get());
+
+        tokens.skipTo(7L);
+        Assert.assertTrue(tokens.hasNext());
+        Assert.assertEquals(7L, (long) tokens.next().get());
+
+        tokens.skipTo(10L);
+        Assert.assertFalse(tokens.hasNext());
+        Assert.assertEquals(1L, (long) tokens.getMinimum());
+        Assert.assertEquals(9L, (long) tokens.getMaximum());
+    }
+
+    @Test
+    public void testMergingMultipleIterators()
+    {
+        RangeUnionIterator.Builder<Long, Token> builderA = RangeUnionIterator.builder();
+
+        builderA.add(new LongIterator(new long[] { 1L, 3L, 5L }));
+        builderA.add(new LongIterator(new long[] { 8L, 10L, 12L }));
+
+        RangeUnionIterator.Builder<Long, Token> builderB = RangeUnionIterator.builder();
+
+        builderB.add(new LongIterator(new long[] { 7L, 9L, 11L }));
+        builderB.add(new LongIterator(new long[] { 2L, 4L, 6L }));
+
+        RangeIterator<Long, Token> union = RangeUnionIterator.build(Arrays.asList(builderA.build(), builderB.build()));
+        Assert.assertEquals(convert(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L), convert(union));
+    }
+
+    @Test
+    public void testRangeIterator()
+    {
+        LongIterator tokens = new LongIterator(new long[] { 0L, 1L, 2L, 3L });
+
+        Assert.assertEquals(0L, (long) tokens.getMinimum());
+        Assert.assertEquals(3L, (long) tokens.getMaximum());
+
+        for (int i = 0; i <= 3; i++)
+        {
+            Assert.assertTrue(tokens.hasNext());
+            Assert.assertEquals(i, (long) tokens.getCurrent());
+            Assert.assertEquals(i, (long) tokens.next().get());
+        }
+
+        tokens = new LongIterator(new long[] { 0L, 1L, 3L, 5L });
+
+        Assert.assertEquals(3L, (long) tokens.skipTo(2L).get());
+        Assert.assertTrue(tokens.hasNext());
+        Assert.assertEquals(3L, (long) tokens.getCurrent());
+        Assert.assertEquals(3L, (long) tokens.next().get());
+
+        Assert.assertEquals(5L, (long) tokens.skipTo(5L).get());
+        Assert.assertTrue(tokens.hasNext());
+        Assert.assertEquals(5L, (long) tokens.getCurrent());
+        Assert.assertEquals(5L, (long) tokens.next().get());
+
+        LongIterator empty = new LongIterator(new long[0]);
+
+        Assert.assertNull(empty.skipTo(3L));
+        Assert.assertFalse(empty.hasNext());
+    }
+
+    @Test
+    public void emptyRangeTest() {
+        RangeIterator.Builder<Long, Token> builder;
+        RangeIterator<Long, Token> range;
+        // empty, then non-empty
+        builder = RangeUnionIterator.builder();
+        builder.add(new LongIterator(new long[] {}));
+        for (int i = 0; i < 10; i++)
+            builder.add(new LongIterator(new long[] {i + 10}));
+        range = builder.build();
+        Assert.assertEquals(Long.valueOf(10), range.getMinimum());
+        Assert.assertEquals(Long.valueOf(19), range.getMaximum());
+        Assert.assertTrue(range.hasNext());
+        Assert.assertEquals(10, range.getCount());
+
+        builder = RangeUnionIterator.builder();
+        builder.add(new LongIterator(new long[] {}));
+        builder.add(new LongIterator(new long[] {10}));
+        range = builder.build();
+        Assert.assertEquals(Long.valueOf(10), range.getMinimum());
+        Assert.assertEquals(Long.valueOf(10), range.getMaximum());
+        Assert.assertTrue(range.hasNext());
+        Assert.assertEquals(1, range.getCount());
+
+        // non-empty, then empty
+        builder = RangeUnionIterator.builder();
+        for (int i = 0; i < 10; i++)
+            builder.add(new LongIterator(new long[] {i + 10}));
+        builder.add(new LongIterator(new long[] {}));
+        range = builder.build();
+        Assert.assertEquals(Long.valueOf(10), range.getMinimum());
+        Assert.assertEquals(Long.valueOf(19), range.getMaximum());
+        Assert.assertTrue(range.hasNext());
+        Assert.assertEquals(10, range.getCount());
+
+        builder = RangeUnionIterator.builder();
+        builder.add(new LongIterator(new long[] {10}));
+        builder.add(new LongIterator(new long[] {}));
+        range = builder.build();
+        Assert.assertEquals(Long.valueOf(10), range.getMinimum());
+        Assert.assertEquals(Long.valueOf(10), range.getMaximum());
+        Assert.assertTrue(range.hasNext());
+        Assert.assertEquals(1, range.getCount());
+
+        // empty, then non-empty then empty again
+        builder = RangeUnionIterator.builder();
+        builder.add(new LongIterator(new long[] {}));
+        for (int i = 0; i < 10; i++)
+            builder.add(new LongIterator(new long[] {i + 10}));
+        builder.add(new LongIterator(new long[] {}));
+        range = builder.build();
+        Assert.assertEquals(Long.valueOf(10), range.getMinimum());
+        Assert.assertEquals(Long.valueOf(19), range.getMaximum());
+        Assert.assertTrue(range.hasNext());
+        Assert.assertEquals(10, range.getCount());
+
+        // non-empty, empty, then non-empty again
+        builder = RangeUnionIterator.builder();
+        for (int i = 0; i < 5; i++)
+            builder.add(new LongIterator(new long[] {i + 10}));
+        builder.add(new LongIterator(new long[] {}));
+        for (int i = 5; i < 10; i++)
+            builder.add(new LongIterator(new long[] {i + 10}));
+        range = builder.build();
+        Assert.assertEquals(Long.valueOf(10), range.getMinimum());
+        Assert.assertEquals(Long.valueOf(19), range.getMaximum());
+        Assert.assertTrue(range.hasNext());
+        Assert.assertEquals(10, range.getCount());
+    }
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/io/compress/CQLCompressionTest.java b/test/unit/org/apache/cassandra/io/compress/CQLCompressionTest.java
new file mode 100644
index 0000000..a2aff2f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/io/compress/CQLCompressionTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.io.compress;
+
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.exceptions.ConfigurationException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class CQLCompressionTest extends CQLTester
+{
+    @Test
+    public void lz4ParamsTest()
+    {
+        createTable("create table %s (id int primary key, uh text) with compression = {'class':'LZ4Compressor', 'lz4_high_compressor_level':3}");
+        assertTrue(((LZ4Compressor)getCurrentColumnFamilyStore().metadata.params.compression.getSstableCompressor()).compressorType.equals(LZ4Compressor.LZ4_FAST_COMPRESSOR));
+        createTable("create table %s (id int primary key, uh text) with compression = {'class':'LZ4Compressor', 'lz4_compressor_type':'high', 'lz4_high_compressor_level':13}");
+        assertEquals(((LZ4Compressor)getCurrentColumnFamilyStore().metadata.params.compression.getSstableCompressor()).compressorType, LZ4Compressor.LZ4_HIGH_COMPRESSOR);
+        assertEquals(((LZ4Compressor)getCurrentColumnFamilyStore().metadata.params.compression.getSstableCompressor()).compressionLevel, (Integer)13);
+        createTable("create table %s (id int primary key, uh text) with compression = {'class':'LZ4Compressor'}");
+        assertEquals(((LZ4Compressor)getCurrentColumnFamilyStore().metadata.params.compression.getSstableCompressor()).compressorType, LZ4Compressor.LZ4_FAST_COMPRESSOR);
+        assertEquals(((LZ4Compressor)getCurrentColumnFamilyStore().metadata.params.compression.getSstableCompressor()).compressionLevel, (Integer)9);
+    }
+
+    @Test(expected = ConfigurationException.class)
+    public void lz4BadParamsTest() throws Throwable
+    {
+        try
+        {
+            createTable("create table %s (id int primary key, uh text) with compression = {'class':'LZ4Compressor', 'lz4_compressor_type':'high', 'lz4_high_compressor_level':113}");
+        }
+        catch (RuntimeException e)
+        {
+            throw e.getCause();
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/io/compress/CompressedRandomAccessReaderTest.java b/test/unit/org/apache/cassandra/io/compress/CompressedRandomAccessReaderTest.java
index 34ff94f..c718147 100644
--- a/test/unit/org/apache/cassandra/io/compress/CompressedRandomAccessReaderTest.java
+++ b/test/unit/org/apache/cassandra/io/compress/CompressedRandomAccessReaderTest.java
@@ -1,4 +1,4 @@
-/**
+/*
  * 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
@@ -25,17 +25,16 @@
 import java.util.Arrays;
 import java.util.Random;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ClusteringComparator;
 import org.apache.cassandra.db.marshal.BytesType;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.sstable.CorruptSSTableException;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
-import org.apache.cassandra.io.util.ChannelProxy;
-import org.apache.cassandra.io.util.DataPosition;
-import org.apache.cassandra.io.util.MmappedRegions;
-import org.apache.cassandra.io.util.RandomAccessReader;
-import org.apache.cassandra.io.util.SequentialWriter;
+import org.apache.cassandra.io.util.*;
 import org.apache.cassandra.schema.CompressionParams;
 import org.apache.cassandra.utils.ChecksumType;
 import org.apache.cassandra.utils.SyncUtil;
@@ -48,6 +47,12 @@
 
 public class CompressedRandomAccessReaderTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testResetAndTruncate() throws IOException
     {
@@ -77,42 +82,44 @@
     {
         File f = File.createTempFile("compressed6791_", "3");
         String filename = f.getAbsolutePath();
-        try (ChannelProxy channel = new ChannelProxy(f))
+        MetadataCollector sstableMetadataCollector = new MetadataCollector(new ClusteringComparator(BytesType.instance));
+        try(CompressedSequentialWriter writer = new CompressedSequentialWriter(f, filename + ".metadata",
+                                                                               null, SequentialWriterOption.DEFAULT,
+                                                                               CompressionParams.snappy(32),
+                                                                               sstableMetadataCollector))
         {
 
-            MetadataCollector sstableMetadataCollector = new MetadataCollector(new ClusteringComparator(BytesType.instance));
-            try (CompressedSequentialWriter writer = new CompressedSequentialWriter(f, filename + ".metadata", CompressionParams.snappy(32), sstableMetadataCollector))
-            {
+            for (int i = 0; i < 20; i++)
+                writer.write("x".getBytes());
 
-                for (int i = 0; i < 20; i++)
-                    writer.write("x".getBytes());
+            DataPosition mark = writer.mark();
+            // write enough garbage to create new chunks:
+            for (int i = 0; i < 40; ++i)
+                writer.write("y".getBytes());
 
-                DataPosition mark = writer.mark();
-                // write enough garbage to create new chunks:
-                for (int i = 0; i < 40; ++i)
-                    writer.write("y".getBytes());
+            writer.resetAndTruncate(mark);
 
-                writer.resetAndTruncate(mark);
+            for (int i = 0; i < 20; i++)
+                writer.write("x".getBytes());
+            writer.finish();
+        }
 
-                for (int i = 0; i < 20; i++)
-                    writer.write("x".getBytes());
-                writer.finish();
-            }
-
-            try (RandomAccessReader reader = new CompressedRandomAccessReader.Builder(channel,
-                                                                                      new CompressionMetadata(filename + ".metadata", f.length(), ChecksumType.CRC32))
-                    .build())
-            {
-                String res = reader.readLine();
-                assertEquals(res, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
-                assertEquals(40, res.length());
-            }
+        try (FileHandle.Builder builder = new FileHandle.Builder(filename)
+                                                              .withCompressionMetadata(new CompressionMetadata(filename + ".metadata",
+                                                                                                               f.length(),
+                                                                                                               ChecksumType.CRC32));
+             FileHandle fh = builder.complete();
+             RandomAccessReader reader = fh.createReader())
+        {
+            String res = reader.readLine();
+            assertEquals(res, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
+            assertEquals(40, res.length());
         }
         finally
         {
             if (f.exists())
                 assertTrue(f.delete());
-            File metadata = new File(filename + ".metadata");
+            File metadata = new File(filename+ ".metadata");
             if (metadata.exists())
                 metadata.delete();
         }
@@ -161,34 +168,18 @@
     private static void testResetAndTruncate(File f, boolean compressed, boolean usemmap, int junkSize) throws IOException
     {
         final String filename = f.getAbsolutePath();
-        try(ChannelProxy channel = new ChannelProxy(f))
+        writeSSTable(f, compressed ? CompressionParams.snappy() : null, junkSize);
+
+        CompressionMetadata compressionMetadata = compressed ? new CompressionMetadata(filename + ".metadata", f.length(), ChecksumType.CRC32) : null;
+        try (FileHandle.Builder builder = new FileHandle.Builder(filename).mmapped(usemmap).withCompressionMetadata(compressionMetadata);
+             FileHandle fh = builder.complete();
+             RandomAccessReader reader = fh.createReader())
         {
-            writeSSTable(f, compressed ? CompressionParams.snappy() : null, junkSize);
-
-            CompressionMetadata compressionMetadata = compressed ? new CompressionMetadata(filename + ".metadata", f.length(), ChecksumType.CRC32) : null;
-            RandomAccessReader.Builder builder = compressed
-                                                 ? new CompressedRandomAccessReader.Builder(channel, compressionMetadata)
-                                                 : new RandomAccessReader.Builder(channel);
-
-            if (usemmap)
-            {
-                if (compressed)
-                    builder.regions(MmappedRegions.map(channel, compressionMetadata));
-                else
-                    builder.regions(MmappedRegions.map(channel, f.length()));
-            }
-
-            try(RandomAccessReader reader = builder.build())
-            {
-                String expected = "The quick brown fox jumps over the lazy dog";
-                assertEquals(expected.length(), reader.length());
-                byte[] b = new byte[expected.length()];
-                reader.readFully(b);
-                assert new String(b).equals(expected) : "Expecting '" + expected + "', got '" + new String(b) + '\'';
-            }
-
-            if (usemmap)
-                builder.regions.close();
+            String expected = "The quick brown fox jumps over the lazy dog";
+            assertEquals(expected.length(), reader.length());
+            byte[] b = new byte[expected.length()];
+            reader.readFully(b);
+            assert new String(b).equals(expected) : "Expecting '" + expected + "', got '" + new String(b) + '\'';
         }
         finally
         {
@@ -205,8 +196,10 @@
         final String filename = f.getAbsolutePath();
         MetadataCollector sstableMetadataCollector = new MetadataCollector(new ClusteringComparator(BytesType.instance));
         try(SequentialWriter writer = params != null
-                                      ? new CompressedSequentialWriter(f, filename + ".metadata", params, sstableMetadataCollector)
-                                      : SequentialWriter.open(f))
+                ? new CompressedSequentialWriter(f, filename + ".metadata",
+                                                 null, SequentialWriterOption.DEFAULT,
+                                                 params, sstableMetadataCollector)
+                : new SequentialWriter(f))
         {
             writer.write("The quick ".getBytes());
             DataPosition mark = writer.mark();
@@ -243,23 +236,23 @@
         assertTrue(metadata.createNewFile());
 
         MetadataCollector sstableMetadataCollector = new MetadataCollector(new ClusteringComparator(BytesType.instance));
-        try (SequentialWriter writer = new CompressedSequentialWriter(file, metadata.getPath(), CompressionParams.snappy(), sstableMetadataCollector))
+        try (SequentialWriter writer = new CompressedSequentialWriter(file, metadata.getPath(),
+                                                                      null, SequentialWriterOption.DEFAULT,
+                                                                      CompressionParams.snappy(), sstableMetadataCollector))
         {
             writer.write(CONTENT.getBytes());
             writer.finish();
         }
 
-        try(ChannelProxy channel = new ChannelProxy(file))
-        {
-            // open compression metadata and get chunk information
-            CompressionMetadata meta = new CompressionMetadata(metadata.getPath(), file.length(), ChecksumType.CRC32);
-            CompressionMetadata.Chunk chunk = meta.chunkFor(0);
+        // open compression metadata and get chunk information
+        CompressionMetadata meta = new CompressionMetadata(metadata.getPath(), file.length(), ChecksumType.CRC32);
+        CompressionMetadata.Chunk chunk = meta.chunkFor(0);
 
-            try(RandomAccessReader reader = new CompressedRandomAccessReader.Builder(channel, meta).build())
-            {// read and verify compressed data
-                assertEquals(CONTENT, reader.readLine());
-            }
-
+        try (FileHandle.Builder builder = new FileHandle.Builder(file.getPath()).withCompressionMetadata(meta);
+             FileHandle fh = builder.complete();
+             RandomAccessReader reader = fh.createReader())
+        {// read and verify compressed data
+            assertEquals(CONTENT, reader.readLine());
             Random random = new Random();
             try(RandomAccessFile checksumModifier = new RandomAccessFile(file, "rw"))
             {
@@ -278,7 +271,7 @@
 
                 updateChecksum(checksumModifier, chunk.length, corruptChecksum);
 
-                try (final RandomAccessReader r = new CompressedRandomAccessReader.Builder(channel, meta).build())
+                try (final RandomAccessReader r = fh.createReader())
                 {
                     Throwable exception = null;
                     try
@@ -298,7 +291,7 @@
                 updateChecksum(checksumModifier, chunk.length, checksum);
 
                 // read and verify compressed data
-                try (RandomAccessReader cr = new CompressedRandomAccessReader.Builder(channel, meta).build())
+                try (RandomAccessReader cr = fh.createReader())
                 {
                     assertEquals(CONTENT, cr.readLine());
                 }
diff --git a/test/unit/org/apache/cassandra/io/compress/CompressedSequentialWriterTest.java b/test/unit/org/apache/cassandra/io/compress/CompressedSequentialWriterTest.java
index f04439a..52b18a9 100644
--- a/test/unit/org/apache/cassandra/io/compress/CompressedSequentialWriterTest.java
+++ b/test/unit/org/apache/cassandra/io/compress/CompressedSequentialWriterTest.java
@@ -17,7 +17,6 @@
  */
 package org.apache.cassandra.io.compress;
 
-import com.google.common.io.Files;
 import java.io.ByteArrayInputStream;
 import java.io.DataInputStream;
 import java.io.File;
@@ -29,22 +28,19 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.google.common.io.Files;
 import org.junit.After;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import junit.framework.Assert;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ClusteringComparator;
-import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.BytesType;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
-import org.apache.cassandra.io.util.ChannelProxy;
-import org.apache.cassandra.io.util.DataPosition;
-import org.apache.cassandra.io.util.FileUtils;
-import org.apache.cassandra.io.util.RandomAccessReader;
-import org.apache.cassandra.io.util.SequentialWriter;
-import org.apache.cassandra.io.util.SequentialWriterTest;
+import org.apache.cassandra.io.util.*;
 import org.apache.cassandra.schema.CompressionParams;
 import org.apache.cassandra.utils.ChecksumType;
 
@@ -52,6 +48,12 @@
 {
     private CompressionParams compressionParameters;
 
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     private void runTests(String testName) throws IOException
     {
         // Test small < 1 chunk data set
@@ -88,46 +90,47 @@
     private void testWrite(File f, int bytesToTest) throws IOException
     {
         final String filename = f.getAbsolutePath();
-        final ChannelProxy channel = new ChannelProxy(f);
+        MetadataCollector sstableMetadataCollector = new MetadataCollector(new ClusteringComparator(Collections.singletonList(BytesType.instance)));
 
-        try
+        byte[] dataPre = new byte[bytesToTest];
+        byte[] rawPost = new byte[bytesToTest];
+        try (CompressedSequentialWriter writer = new CompressedSequentialWriter(f, filename + ".metadata",
+                null, SequentialWriterOption.DEFAULT,
+                compressionParameters,
+                sstableMetadataCollector))
         {
-            MetadataCollector sstableMetadataCollector = new MetadataCollector(new ClusteringComparator(Arrays.<AbstractType<?>>asList(BytesType.instance)));
+            Random r = new Random(42);
 
-            byte[] dataPre = new byte[bytesToTest];
-            byte[] rawPost = new byte[bytesToTest];
-            try (CompressedSequentialWriter writer = new CompressedSequentialWriter(f, filename + ".metadata", compressionParameters, sstableMetadataCollector);)
+            // Test both write with byte[] and ByteBuffer
+            r.nextBytes(dataPre);
+            r.nextBytes(rawPost);
+            ByteBuffer dataPost = makeBB(bytesToTest);
+            dataPost.put(rawPost);
+            dataPost.flip();
+
+            writer.write(dataPre);
+            DataPosition mark = writer.mark();
+
+            // Write enough garbage to transition chunk
+            for (int i = 0; i < CompressionParams.DEFAULT_CHUNK_LENGTH; i++)
             {
-                Random r = new Random(42);
-
-                // Test both write with byte[] and ByteBuffer
-                r.nextBytes(dataPre);
-                r.nextBytes(rawPost);
-                ByteBuffer dataPost = makeBB(bytesToTest);
-                dataPost.put(rawPost);
-                dataPost.flip();
-
-                writer.write(dataPre);
-                DataPosition mark = writer.mark();
-
-                // Write enough garbage to transition chunk
-                for (int i = 0; i < CompressionParams.DEFAULT_CHUNK_LENGTH; i++)
-                {
-                    writer.write((byte)i);
-                }
-
-                if (bytesToTest <= CompressionParams.DEFAULT_CHUNK_LENGTH)
-                    assertEquals(writer.getLastFlushOffset(), CompressionParams.DEFAULT_CHUNK_LENGTH);
-                else
-                    assertTrue(writer.getLastFlushOffset() % CompressionParams.DEFAULT_CHUNK_LENGTH == 0);
-
-                writer.resetAndTruncate(mark);
-                writer.write(dataPost);
-                writer.finish();
+                writer.write((byte)i);
             }
+            if (bytesToTest <= CompressionParams.DEFAULT_CHUNK_LENGTH)
+                assertEquals(writer.getLastFlushOffset(), CompressionParams.DEFAULT_CHUNK_LENGTH);
+            else
+                assertTrue(writer.getLastFlushOffset() % CompressionParams.DEFAULT_CHUNK_LENGTH == 0);
 
-            assert f.exists();
-            RandomAccessReader reader = new CompressedRandomAccessReader.Builder(channel, new CompressionMetadata(filename + ".metadata", f.length(), ChecksumType.CRC32)).build();
+            writer.resetAndTruncate(mark);
+            writer.write(dataPost);
+            writer.finish();
+        }
+
+        assert f.exists();
+        try (FileHandle.Builder builder = new FileHandle.Builder(filename).withCompressionMetadata(new CompressionMetadata(filename + ".metadata", f.length(), ChecksumType.CRC32));
+             FileHandle fh = builder.complete();
+             RandomAccessReader reader = fh.createReader())
+        {
             assertEquals(dataPre.length + rawPost.length, reader.length());
             byte[] result = new byte[(int)reader.length()];
 
@@ -143,9 +146,6 @@
         }
         finally
         {
-            // cleanup
-            channel.close();
-
             if (f.exists())
                 f.delete();
             File metadata = new File(f + ".metadata");
@@ -178,10 +178,10 @@
         final int bufferSize = 48;
         final int writeSize = 64;
         byte[] toWrite = new byte[writeSize];
-        MetadataCollector sstableMetadataCollector = new MetadataCollector(new ClusteringComparator(Arrays.<AbstractType<?>>asList(BytesType.instance)));
-
         try (SequentialWriter writer = new CompressedSequentialWriter(tempFile, offsetsFile.getPath(),
-                                                                      CompressionParams.lz4(), sstableMetadataCollector))
+                                                                      null, SequentialWriterOption.DEFAULT,
+                                                                      CompressionParams.lz4(bufferSize),
+                                                                      new MetadataCollector(new ClusteringComparator(UTF8Type.instance))))
         {
             // write bytes greather than buffer
             writer.write(toWrite);
@@ -231,10 +231,11 @@
 
         private TestableCSW(File file, File offsetsFile) throws IOException
         {
-            this(file, offsetsFile, new CompressedSequentialWriter(file,
-                                                                   offsetsFile.getPath(),
+            this(file, offsetsFile, new CompressedSequentialWriter(file, offsetsFile.getPath(),
+                                                                   null, SequentialWriterOption.DEFAULT,
                                                                    CompressionParams.lz4(BUFFER_SIZE),
                                                                    new MetadataCollector(new ClusteringComparator(UTF8Type.instance))));
+
         }
 
         private TestableCSW(File file, File offsetsFile, CompressedSequentialWriter sw) throws IOException
@@ -249,7 +250,7 @@
             Assert.assertFalse(offsetsFile.exists());
             byte[] compressed = readFileToByteArray(file);
             byte[] uncompressed = new byte[partialContents.length];
-            LZ4Compressor.instance.uncompress(compressed, 0, compressed.length - 4, uncompressed, 0);
+            LZ4Compressor.create(Collections.<String, String>emptyMap()).uncompress(compressed, 0, compressed.length - 4, uncompressed, 0);
             Assert.assertTrue(Arrays.equals(partialContents, uncompressed));
         }
 
@@ -267,8 +268,8 @@
             int offset = (int) offsets.readLong();
             byte[] compressed = readFileToByteArray(file);
             byte[] uncompressed = new byte[fullContents.length];
-            LZ4Compressor.instance.uncompress(compressed, 0, offset - 4, uncompressed, 0);
-            LZ4Compressor.instance.uncompress(compressed, offset, compressed.length - (4 + offset), uncompressed, partialContents.length);
+            LZ4Compressor.create(Collections.<String, String>emptyMap()).uncompress(compressed, 0, offset - 4, uncompressed, 0);
+            LZ4Compressor.create(Collections.<String, String>emptyMap()).uncompress(compressed, offset, compressed.length - (4 + offset), uncompressed, partialContents.length);
             Assert.assertTrue(Arrays.equals(fullContents, uncompressed));
         }
 
diff --git a/test/unit/org/apache/cassandra/io/compress/CompressorTest.java b/test/unit/org/apache/cassandra/io/compress/CompressorTest.java
index 1e24d03..617e04e 100644
--- a/test/unit/org/apache/cassandra/io/compress/CompressorTest.java
+++ b/test/unit/org/apache/cassandra/io/compress/CompressorTest.java
@@ -33,7 +33,6 @@
 
 import org.apache.cassandra.io.util.RandomAccessReader;
 import org.apache.cassandra.utils.ByteBufferUtil;
-import org.apache.cassandra.utils.memory.BufferPool;
 
 public class CompressorTest
 {
diff --git a/test/unit/org/apache/cassandra/io/sstable/CQLSSTableWriterClientTest.java b/test/unit/org/apache/cassandra/io/sstable/CQLSSTableWriterClientTest.java
index d38276f..273c400 100644
--- a/test/unit/org/apache/cassandra/io/sstable/CQLSSTableWriterClientTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/CQLSSTableWriterClientTest.java
@@ -27,15 +27,11 @@
 import org.junit.Before;
 import org.junit.Test;
 
-import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.db.Directories;
-import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.io.util.FileUtils;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
 public class CQLSSTableWriterClientTest
 {
@@ -45,19 +41,7 @@
     public void setUp()
     {
         this.testDirectory = Files.createTempDir();
-        Config.setClientMode(true);
-    }
-
-    @After
-    public void tearDown()
-    {
-        FileUtils.deleteRecursive(this.testDirectory);
-    }
-
-    @AfterClass
-    public static void cleanup() throws Exception
-    {
-        Config.setClientMode(false);
+        DatabaseDescriptor.daemonInitialization();
     }
 
     @Test
@@ -102,4 +86,10 @@
         File[] dataFiles = this.testDirectory.listFiles(filter);
         assertEquals(2, dataFiles.length);
     }
+
+    @After
+    public void tearDown()
+    {
+        FileUtils.deleteRecursive(this.testDirectory);
+    }
 }
diff --git a/test/unit/org/apache/cassandra/io/sstable/CQLSSTableWriterTest.java b/test/unit/org/apache/cassandra/io/sstable/CQLSSTableWriterTest.java
index f66b66f..57910fb 100644
--- a/test/unit/org/apache/cassandra/io/sstable/CQLSSTableWriterTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/CQLSSTableWriterTest.java
@@ -19,40 +19,41 @@
 
 import java.io.File;
 import java.io.FilenameFilter;
+import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.Files;
 
-import org.junit.AfterClass;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
 import com.datastax.driver.core.utils.UUIDs;
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
-import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.Config;
-import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.config.Schema;
-import org.apache.cassandra.cql3.QueryProcessor;
-import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.config.*;
+import org.apache.cassandra.cql3.*;
+import org.apache.cassandra.cql3.functions.UDHelper;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.dht.*;
+import org.apache.cassandra.exceptions.*;
 import org.apache.cassandra.service.StorageService;
-import org.apache.cassandra.utils.ByteBufferUtil;
-import org.apache.cassandra.utils.FBUtilities;
-import org.apache.cassandra.utils.OutputHandler;
+import org.apache.cassandra.utils.*;
+import com.datastax.driver.core.DataType;
+import com.datastax.driver.core.ProtocolVersion;
+import com.datastax.driver.core.TypeCodec;
+import com.datastax.driver.core.UDTValue;
+import com.datastax.driver.core.UserType;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -61,39 +62,55 @@
 
 public class CQLSSTableWriterTest
 {
+    private static final AtomicInteger idGen = new AtomicInteger(0);
+    private String keyspace;
+    private String table;
+    private String qualifiedTable;
+    private File dataDir;
+    private File tempDir;
+
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @BeforeClass
     public static void setup() throws Exception
     {
-        DatabaseDescriptor.setDaemonInitialized();
         SchemaLoader.cleanupAndLeaveDirs();
         Keyspace.setInitialized();
         StorageService.instance.initServer();
     }
 
-    @AfterClass
-    public static void tearDown() throws Exception
+    @Before
+    public void perTestSetup()
     {
-        Config.setClientMode(false);
+        tempDir = Files.createTempDir();
+        keyspace = "cql_keyspace" + idGen.incrementAndGet();
+        table = "table" + idGen.incrementAndGet();
+        qualifiedTable = keyspace + '.' + table;
+        dataDir = new File(tempDir.getAbsolutePath() + File.separator + keyspace + File.separator + table);
+        assert dataDir.mkdirs();
     }
 
+    @After
+    public void cleanup() throws IOException
+    {
+        FileUtils.deleteDirectory(tempDir);
+    }
+
+
     @Test
     public void testUnsortedWriter() throws Exception
     {
         try (AutoCloseable switcher = Util.switchPartitioner(ByteOrderedPartitioner.instance))
         {
-            String KS = "cql_keyspace";
-            String TABLE = "table1";
-
-            File tempdir = Files.createTempDir();
-            File dataDir = new File(tempdir.getAbsolutePath() + File.separator + KS + File.separator + TABLE);
-            assert dataDir.mkdirs();
-
-            String schema = "CREATE TABLE cql_keyspace.table1 ("
+            String schema = "CREATE TABLE " + qualifiedTable + " ("
                           + "  k int PRIMARY KEY,"
                           + "  v1 text,"
                           + "  v2 int"
                           + ")";
-            String insert = "INSERT INTO cql_keyspace.table1 (k, v1, v2) VALUES (?, ?, ?)";
+            String insert = "INSERT INTO " + qualifiedTable + " (k, v1, v2) VALUES (?, ?, ?)";
             CQLSSTableWriter writer = CQLSSTableWriter.builder()
                                                       .inDirectory(dataDir)
                                                       .forTable(schema)
@@ -106,9 +123,9 @@
 
             writer.close();
 
-            loadSSTables(dataDir, KS);
+            loadSSTables(dataDir, keyspace);
 
-            UntypedResultSet rs = QueryProcessor.executeInternal("SELECT * FROM cql_keyspace.table1;");
+            UntypedResultSet rs = QueryProcessor.executeInternal("SELECT * FROM " + qualifiedTable + ";");
             assertEquals(4, rs.size());
 
             Iterator<UntypedResultSet.Row> iter = rs.iterator();
@@ -140,19 +157,12 @@
     @Test
     public void testForbidCounterUpdates() throws Exception
     {
-        String KS = "cql_keyspace";
-        String TABLE = "counter1";
-
-        File tempdir = Files.createTempDir();
-        File dataDir = new File(tempdir.getAbsolutePath() + File.separator + KS + File.separator + TABLE);
-        assert dataDir.mkdirs();
-
-        String schema = "CREATE TABLE cql_keyspace.counter1 (" +
+        String schema = "CREATE TABLE " + qualifiedTable + " (" +
                         "  my_id int, " +
                         "  my_counter counter, " +
                         "  PRIMARY KEY (my_id)" +
                         ")";
-        String insert = String.format("UPDATE cql_keyspace.counter1 SET my_counter = my_counter - ? WHERE my_id = ?");
+        String insert = String.format("UPDATE " + qualifiedTable + " SET my_counter = my_counter - ? WHERE my_id = ?");
         try
         {
             CQLSSTableWriter.builder().inDirectory(dataDir)
@@ -173,21 +183,15 @@
         // Check that the write respect the buffer size even if we only insert rows withing the same partition (#7360)
         // To do that simply, we use a writer with a buffer of 1MB, and write 2 rows in the same partition with a value
         // > 1MB and validate that this created more than 1 sstable.
-        String KS = "ks";
-        String TABLE = "test";
-
-        File tempdir = Files.createTempDir();
-        File dataDir = new File(tempdir.getAbsolutePath() + File.separator + KS + File.separator + TABLE);
-        assert dataDir.mkdirs();
-        String schema = "CREATE TABLE ks.test ("
+        String schema = "CREATE TABLE " + qualifiedTable + " ("
                       + "  k int PRIMARY KEY,"
                       + "  v blob"
                       + ")";
-        String insert = "INSERT INTO ks.test (k, v) VALUES (?, ?)";
+        String insert = "INSERT INTO " + qualifiedTable + " (k, v) VALUES (?, ?)";
         CQLSSTableWriter writer = CQLSSTableWriter.builder()
                                                   .inDirectory(dataDir)
-                                                  .forTable(schema)
                                                   .using(insert)
+                                                  .forTable(schema)
                                                   .withBufferSizeInMB(1)
                                                   .build();
 
@@ -212,15 +216,14 @@
     public void testSyncNoEmptyRows() throws Exception
     {
         // Check that the write does not throw an empty partition error (#9071)
-        File tempdir = Files.createTempDir();
-        String schema = "CREATE TABLE ks.test2 ("
+        String schema = "CREATE TABLE " + qualifiedTable + " ("
                         + "  k UUID,"
                         + "  c int,"
                         + "  PRIMARY KEY (k)"
                         + ")";
-        String insert = "INSERT INTO ks.test2 (k, c) VALUES (?, ?)";
+        String insert = "INSERT INTO " + qualifiedTable + " (k, c) VALUES (?, ?)";
         CQLSSTableWriter writer = CQLSSTableWriter.builder()
-                                                  .inDirectory(tempdir)
+                                                  .inDirectory(dataDir)
                                                   .forTable(schema)
                                                   .using(insert)
                                                   .withBufferSizeInMB(1)
@@ -233,125 +236,32 @@
 
     }
 
-    @Test
-    public void testUpdateStatement() throws Exception
-    {
-        final String KS = "cql_keyspace6";
-        final String TABLE = "table6";
 
-        final String schema = "CREATE TABLE " + KS + "." + TABLE + " ("
-                              + "  k int,"
-                              + "  c1 int,"
-                              + "  c2 int,"
-                              + "  v text,"
-                              + "  PRIMARY KEY (k, c1, c2)"
-                              + ")";
-
-        File tempdir = Files.createTempDir();
-        File dataDir = new File(tempdir.getAbsolutePath() + File.separator + KS + File.separator + TABLE);
-        assert dataDir.mkdirs();
-
-        CQLSSTableWriter writer = CQLSSTableWriter.builder()
-                                                  .inDirectory(dataDir)
-                                                  .forTable(schema)
-                                                  .using("UPDATE " + KS + "." + TABLE + " SET v = ? " +
-                                                         "WHERE k = ? AND c1 = ? AND c2 = ?")
-                                                  .build();
-
-        writer.addRow("a", 1, 2, 3);
-        writer.addRow("b", 4, 5, 6);
-        writer.addRow(null, 7, 8, 9);
-        writer.close();
-        loadSSTables(dataDir, KS);
-
-        UntypedResultSet resultSet = QueryProcessor.executeInternal("SELECT * FROM " + KS + "." + TABLE);
-        assertEquals(2, resultSet.size());
-
-        Iterator<UntypedResultSet.Row> iter = resultSet.iterator();
-        UntypedResultSet.Row r1 = iter.next();
-        assertEquals(1, r1.getInt("k"));
-        assertEquals(2, r1.getInt("c1"));
-        assertEquals(3, r1.getInt("c2"));
-        assertEquals("a", r1.getString("v"));
-        UntypedResultSet.Row r2 = iter.next();
-        assertEquals(4, r2.getInt("k"));
-        assertEquals(5, r2.getInt("c1"));
-        assertEquals(6, r2.getInt("c2"));
-        assertEquals("b", r2.getString("v"));
-        assertFalse(iter.hasNext());
-    }
-
-    @Test
-    public void testNativeFunctions() throws Exception
-    {
-        final String KS = "cql_keyspace7";
-        final String TABLE = "table7";
-
-        final String schema = "CREATE TABLE " + KS + "." + TABLE + " ("
-                              + "  k int,"
-                              + "  c1 int,"
-                              + "  c2 int,"
-                              + "  v blob,"
-                              + "  PRIMARY KEY (k, c1, c2)"
-                              + ")";
-
-        File tempdir = Files.createTempDir();
-        File dataDir = new File(tempdir.getAbsolutePath() + File.separator + KS + File.separator + TABLE);
-        assert dataDir.mkdirs();
-
-        CQLSSTableWriter writer = CQLSSTableWriter.builder()
-                                                  .inDirectory(dataDir)
-                                                  .forTable(schema)
-                                                  .using("INSERT INTO " + KS + "." + TABLE + " (k, c1, c2, v) VALUES (?, ?, ?, textAsBlob(?))")
-                                                  .build();
-
-        writer.addRow(1, 2, 3, "abc");
-        writer.addRow(4, 5, 6, "efg");
-
-        writer.close();
-        loadSSTables(dataDir, KS);
-
-        UntypedResultSet resultSet = QueryProcessor.executeInternal("SELECT * FROM " + KS + "." + TABLE);
-        assertEquals(2, resultSet.size());
-
-        Iterator<UntypedResultSet.Row> iter = resultSet.iterator();
-        UntypedResultSet.Row r1 = iter.next();
-        assertEquals(1, r1.getInt("k"));
-        assertEquals(2, r1.getInt("c1"));
-        assertEquals(3, r1.getInt("c2"));
-        assertEquals(ByteBufferUtil.bytes("abc"), r1.getBytes("v"));
-
-        UntypedResultSet.Row r2 = iter.next();
-        assertEquals(4, r2.getInt("k"));
-        assertEquals(5, r2.getInt("c1"));
-        assertEquals(6, r2.getInt("c2"));
-        assertEquals(ByteBufferUtil.bytes("efg"), r2.getBytes("v"));
-
-        assertFalse(iter.hasNext());
-    }
 
     private static final int NUMBER_WRITES_IN_RUNNABLE = 10;
     private class WriterThread extends Thread
     {
         private final File dataDir;
         private final int id;
+        private final String qualifiedTable;
         public volatile Exception exception;
 
-        public WriterThread(File dataDir, int id)
+        public WriterThread(File dataDir, int id, String qualifiedTable)
         {
             this.dataDir = dataDir;
             this.id = id;
+            this.qualifiedTable = qualifiedTable;
         }
 
         @Override
         public void run()
         {
-            String schema = "CREATE TABLE cql_keyspace2.table2 ("
+            String schema = "CREATE TABLE " + qualifiedTable + " ("
                     + "  k int,"
                     + "  v int,"
                     + "  PRIMARY KEY (k, v)"
                     + ")";
-            String insert = "INSERT INTO cql_keyspace2.table2 (k, v) VALUES (?, ?)";
+            String insert = "INSERT INTO " + qualifiedTable + " (k, v) VALUES (?, ?)";
             CQLSSTableWriter writer = CQLSSTableWriter.builder()
                     .inDirectory(dataDir)
                     .forTable(schema)
@@ -375,17 +285,10 @@
     @Test
     public void testConcurrentWriters() throws Exception
     {
-        final String KS = "cql_keyspace2";
-        final String TABLE = "table2";
-
-        File tempdir = Files.createTempDir();
-        File dataDir = new File(tempdir.getAbsolutePath() + File.separator + KS + File.separator + TABLE);
-        assert dataDir.mkdirs();
-
         WriterThread[] threads = new WriterThread[5];
         for (int i = 0; i < threads.length; i++)
         {
-            WriterThread thread = new WriterThread(dataDir, i);
+            WriterThread thread = new WriterThread(dataDir, i, qualifiedTable);
             threads[i] = thread;
             thread.start();
         }
@@ -400,21 +303,350 @@
             }
         }
 
-        loadSSTables(dataDir, KS);
+        loadSSTables(dataDir, keyspace);
 
-        UntypedResultSet rs = QueryProcessor.executeInternal("SELECT * FROM cql_keyspace2.table2;");
+        UntypedResultSet rs = QueryProcessor.executeInternal("SELECT * FROM " + qualifiedTable + ";");
         assertEquals(threads.length * NUMBER_WRITES_IN_RUNNABLE, rs.size());
     }
 
     @Test
+    @SuppressWarnings("unchecked")
+    public void testWritesWithUdts() throws Exception
+    {
+        final String schema = "CREATE TABLE " + qualifiedTable + " ("
+                              + "  k int,"
+                              + "  v1 list<frozen<tuple2>>,"
+                              + "  v2 frozen<tuple3>,"
+                              + "  PRIMARY KEY (k)"
+                              + ")";
+
+        CQLSSTableWriter writer = CQLSSTableWriter.builder()
+                                                  .inDirectory(dataDir)
+                                                  .withType("CREATE TYPE " + keyspace + ".tuple2 (a int, b int)")
+                                                  .withType("CREATE TYPE " + keyspace + ".tuple3 (a int, b int, c int)")
+                                                  .forTable(schema)
+                                                  .using("INSERT INTO " + keyspace + "." + table + " (k, v1, v2) " +
+                                                         "VALUES (?, ?, ?)").build();
+
+        UserType tuple2Type = writer.getUDType("tuple2");
+        UserType tuple3Type = writer.getUDType("tuple3");
+        for (int i = 0; i < 100; i++)
+        {
+            writer.addRow(i,
+                          ImmutableList.builder()
+                                       .add(tuple2Type.newValue()
+                                                      .setInt("a", i * 10)
+                                                      .setInt("b", i * 20))
+                                       .add(tuple2Type.newValue()
+                                                      .setInt("a", i * 30)
+                                                      .setInt("b", i * 40))
+                                       .build(),
+                          tuple3Type.newValue()
+                                    .setInt("a", i * 100)
+                                    .setInt("b", i * 200)
+                                    .setInt("c", i * 300));
+        }
+
+        writer.close();
+        loadSSTables(dataDir, keyspace);
+
+        UntypedResultSet resultSet = QueryProcessor.executeInternal("SELECT * FROM " + keyspace + "." + table);
+        TypeCodec collectionCodec = UDHelper.codecFor(DataType.CollectionType.frozenList(tuple2Type));
+        TypeCodec tuple3Codec = UDHelper.codecFor(tuple3Type);
+
+        assertEquals(resultSet.size(), 100);
+        int cnt = 0;
+        for (UntypedResultSet.Row row: resultSet) {
+            assertEquals(cnt,
+                         row.getInt("k"));
+            List<UDTValue> values = (List<UDTValue>) collectionCodec.deserialize(row.getBytes("v1"),
+                                                                                 ProtocolVersion.NEWEST_SUPPORTED);
+            assertEquals(values.get(0).getInt("a"), cnt * 10);
+            assertEquals(values.get(0).getInt("b"), cnt * 20);
+            assertEquals(values.get(1).getInt("a"), cnt * 30);
+            assertEquals(values.get(1).getInt("b"), cnt * 40);
+
+            UDTValue v2 = (UDTValue) tuple3Codec.deserialize(row.getBytes("v2"), ProtocolVersion.NEWEST_SUPPORTED);
+
+            assertEquals(v2.getInt("a"), cnt * 100);
+            assertEquals(v2.getInt("b"), cnt * 200);
+            assertEquals(v2.getInt("c"), cnt * 300);
+            cnt++;
+        }
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testWritesWithDependentUdts() throws Exception
+    {
+        final String schema = "CREATE TABLE " + qualifiedTable + " ("
+                              + "  k int,"
+                              + "  v1 frozen<nested_tuple>,"
+                              + "  PRIMARY KEY (k)"
+                              + ")";
+
+        CQLSSTableWriter writer = CQLSSTableWriter.builder()
+                                                  .inDirectory(dataDir)
+                                                  .withType("CREATE TYPE " + keyspace + ".nested_tuple (c int, tpl frozen<tuple2>)")
+                                                  .withType("CREATE TYPE " + keyspace + ".tuple2 (a int, b int)")
+                                                  .forTable(schema)
+                                                  .using("INSERT INTO " + keyspace + "." + table + " (k, v1) " +
+                                                         "VALUES (?, ?)")
+                                                  .build();
+
+        UserType tuple2Type = writer.getUDType("tuple2");
+        UserType nestedTuple = writer.getUDType("nested_tuple");
+        TypeCodec tuple2Codec = UDHelper.codecFor(tuple2Type);
+        TypeCodec nestedTupleCodec = UDHelper.codecFor(nestedTuple);
+
+        for (int i = 0; i < 100; i++)
+        {
+            writer.addRow(i,
+                          nestedTuple.newValue()
+                                     .setInt("c", i * 100)
+                                     .set("tpl",
+                                          tuple2Type.newValue()
+                                                    .setInt("a", i * 200)
+                                                    .setInt("b", i * 300),
+                                          tuple2Codec));
+        }
+
+        writer.close();
+        loadSSTables(dataDir, keyspace);
+
+        UntypedResultSet resultSet = QueryProcessor.executeInternal("SELECT * FROM " + keyspace + "." + table);
+
+        assertEquals(resultSet.size(), 100);
+        int cnt = 0;
+        for (UntypedResultSet.Row row: resultSet) {
+            assertEquals(cnt,
+                         row.getInt("k"));
+            UDTValue nestedTpl = (UDTValue) nestedTupleCodec.deserialize(row.getBytes("v1"),
+                                                                         ProtocolVersion.NEWEST_SUPPORTED);
+            assertEquals(nestedTpl.getInt("c"), cnt * 100);
+            UDTValue tpl = nestedTpl.getUDTValue("tpl");
+            assertEquals(tpl.getInt("a"), cnt * 200);
+            assertEquals(tpl.getInt("b"), cnt * 300);
+
+            cnt++;
+        }
+    }
+
+    @Test
+    public void testUnsetValues() throws Exception
+    {
+        final String schema = "CREATE TABLE " + qualifiedTable + " ("
+                              + "  k int,"
+                              + "  c1 int,"
+                              + "  c2 int,"
+                              + "  v text,"
+                              + "  PRIMARY KEY (k, c1, c2)"
+                              + ")";
+
+        CQLSSTableWriter writer = CQLSSTableWriter.builder()
+                                                  .inDirectory(dataDir)
+                                                  .forTable(schema)
+                                                  .using("INSERT INTO " + qualifiedTable + " (k, c1, c2, v) " +
+                                                         "VALUES (?, ?, ?, ?)")
+                                                  .build();
+
+        try
+        {
+            writer.addRow(1, 1, 1);
+            fail("Passing less arguments then expected in prepared statement should not work.");
+        }
+        catch (InvalidRequestException e)
+        {
+            assertEquals("Invalid number of arguments, expecting 4 values but got 3",
+                         e.getMessage());
+        }
+
+        try
+        {
+            writer.addRow(1, 1, CQLSSTableWriter.UNSET_VALUE, "1");
+            fail("Unset values should not work with clustering columns.");
+        }
+        catch (InvalidRequestException e)
+        {
+            assertEquals("Invalid unset value for column c2",
+                         e.getMessage());
+        }
+
+        try
+        {
+            writer.addRow(ImmutableMap.<String, Object>builder().put("k", 1).put("c1", 1).put("v", CQLSSTableWriter.UNSET_VALUE).build());
+            fail("Unset or null clustering columns should not be allowed.");
+        }
+        catch (InvalidRequestException e)
+        {
+            assertEquals("Invalid null value in condition for column c2",
+                         e.getMessage());
+        }
+
+        writer.addRow(1, 1, 1, CQLSSTableWriter.UNSET_VALUE);
+        writer.addRow(2, 2, 2, null);
+        writer.addRow(Arrays.asList(3, 3, 3, CQLSSTableWriter.UNSET_VALUE));
+        writer.addRow(ImmutableMap.<String, Object>builder()
+                                  .put("k", 4)
+                                  .put("c1", 4)
+                                  .put("c2", 4)
+                                  .put("v", CQLSSTableWriter.UNSET_VALUE)
+                                  .build());
+        writer.addRow(Arrays.asList(3, 3, 3, CQLSSTableWriter.UNSET_VALUE));
+        writer.addRow(5, 5, 5, "5");
+
+        writer.close();
+        loadSSTables(dataDir, keyspace);
+
+        UntypedResultSet resultSet = QueryProcessor.executeInternal("SELECT * FROM " + qualifiedTable);
+        Iterator<UntypedResultSet.Row> iter = resultSet.iterator();
+        UntypedResultSet.Row r1 = iter.next();
+        assertEquals(1, r1.getInt("k"));
+        assertEquals(1, r1.getInt("c1"));
+        assertEquals(1, r1.getInt("c2"));
+        assertEquals(false, r1.has("v"));
+        UntypedResultSet.Row r2 = iter.next();
+        assertEquals(2, r2.getInt("k"));
+        assertEquals(2, r2.getInt("c1"));
+        assertEquals(2, r2.getInt("c2"));
+        assertEquals(false, r2.has("v"));
+        UntypedResultSet.Row r3 = iter.next();
+        assertEquals(3, r3.getInt("k"));
+        assertEquals(3, r3.getInt("c1"));
+        assertEquals(3, r3.getInt("c2"));
+        assertEquals(false, r3.has("v"));
+        UntypedResultSet.Row r4 = iter.next();
+        assertEquals(4, r4.getInt("k"));
+        assertEquals(4, r4.getInt("c1"));
+        assertEquals(4, r4.getInt("c2"));
+        assertEquals(false, r3.has("v"));
+        UntypedResultSet.Row r5 = iter.next();
+        assertEquals(5, r5.getInt("k"));
+        assertEquals(5, r5.getInt("c1"));
+        assertEquals(5, r5.getInt("c2"));
+        assertEquals(true, r5.has("v"));
+        assertEquals("5", r5.getString("v"));
+    }
+
+    @Test
+    public void testUpdateStatement() throws Exception
+    {
+        final String schema = "CREATE TABLE " + qualifiedTable + " ("
+                              + "  k int,"
+                              + "  c1 int,"
+                              + "  c2 int,"
+                              + "  v text,"
+                              + "  PRIMARY KEY (k, c1, c2)"
+                              + ")";
+
+        CQLSSTableWriter writer = CQLSSTableWriter.builder()
+                                                  .inDirectory(dataDir)
+                                                  .forTable(schema)
+                                                  .using("UPDATE " + qualifiedTable + " SET v = ? " +
+                                                         "WHERE k = ? AND c1 = ? AND c2 = ?")
+                                                  .build();
+
+        writer.addRow("a", 1, 2, 3);
+        writer.addRow("b", 4, 5, 6);
+        writer.addRow(null, 7, 8, 9);
+        writer.addRow(CQLSSTableWriter.UNSET_VALUE, 10, 11, 12);
+        writer.close();
+        loadSSTables(dataDir, keyspace);
+
+        UntypedResultSet resultSet = QueryProcessor.executeInternal("SELECT * FROM " + qualifiedTable);
+        assertEquals(2, resultSet.size());
+
+        Iterator<UntypedResultSet.Row> iter = resultSet.iterator();
+        UntypedResultSet.Row r1 = iter.next();
+        assertEquals(1, r1.getInt("k"));
+        assertEquals(2, r1.getInt("c1"));
+        assertEquals(3, r1.getInt("c2"));
+        assertEquals("a", r1.getString("v"));
+        UntypedResultSet.Row r2 = iter.next();
+        assertEquals(4, r2.getInt("k"));
+        assertEquals(5, r2.getInt("c1"));
+        assertEquals(6, r2.getInt("c2"));
+        assertEquals("b", r2.getString("v"));
+        assertFalse(iter.hasNext());
+    }
+
+    @Test
+    public void testNativeFunctions() throws Exception
+    {
+        final String schema = "CREATE TABLE " + qualifiedTable + " ("
+                              + "  k int,"
+                              + "  c1 int,"
+                              + "  c2 int,"
+                              + "  v blob,"
+                              + "  PRIMARY KEY (k, c1, c2)"
+                              + ")";
+
+        CQLSSTableWriter writer = CQLSSTableWriter.builder()
+                                                  .inDirectory(dataDir)
+                                                  .forTable(schema)
+                                                  .using("INSERT INTO " + qualifiedTable + " (k, c1, c2, v) VALUES (?, ?, ?, textAsBlob(?))")
+                                                  .build();
+
+        writer.addRow(1, 2, 3, "abc");
+        writer.addRow(4, 5, 6, "efg");
+
+        writer.close();
+        loadSSTables(dataDir, keyspace);
+
+        UntypedResultSet resultSet = QueryProcessor.executeInternal("SELECT * FROM " + qualifiedTable);
+        assertEquals(2, resultSet.size());
+
+        Iterator<UntypedResultSet.Row> iter = resultSet.iterator();
+        UntypedResultSet.Row r1 = iter.next();
+        assertEquals(1, r1.getInt("k"));
+        assertEquals(2, r1.getInt("c1"));
+        assertEquals(3, r1.getInt("c2"));
+        assertEquals(ByteBufferUtil.bytes("abc"), r1.getBytes("v"));
+
+        UntypedResultSet.Row r2 = iter.next();
+        assertEquals(4, r2.getInt("k"));
+        assertEquals(5, r2.getInt("c1"));
+        assertEquals(6, r2.getInt("c2"));
+        assertEquals(ByteBufferUtil.bytes("efg"), r2.getBytes("v"));
+
+        assertFalse(iter.hasNext());
+    }
+
+    @Test
+    public void testWriteWithNestedTupleUdt() throws Exception
+    {
+        // Check the writer does not throw "InvalidRequestException: Non-frozen tuples are not allowed inside collections: list<tuple<int, int>>"
+        // See CASSANDRA-15857
+        final String schema = "CREATE TABLE " + qualifiedTable + " ("
+                              + "  k int,"
+                              + "  v1 frozen<nested_type>,"
+                              + "  PRIMARY KEY (k)"
+                              + ")";
+
+        CQLSSTableWriter writer = CQLSSTableWriter.builder()
+                                                  .inDirectory(dataDir)
+                                                  .withType("CREATE TYPE " + keyspace + ".nested_type (a list<tuple<int, int>>)")
+                                                  .forTable(schema)
+                                                  .using("INSERT INTO " + qualifiedTable + " (k, v1) " +
+                                                         "VALUES (?, ?)").build();
+
+        UserType nestedType = writer.getUDType("nested_type");
+        for (int i = 0; i < 100; i++)
+        {
+            writer.addRow(i, nestedType.newValue()
+                                       .setList("a", Collections.emptyList()));
+        }
+
+        writer.close();
+        loadSSTables(dataDir, keyspace);
+
+        UntypedResultSet resultSet = QueryProcessor.executeInternal("SELECT * FROM " + qualifiedTable);
+        assertEquals(100, resultSet.size());
+    }
+
+    @Test
     public void testFrozenMapType() throws Exception
     {
-        final String KS = "cql_keyspace3";
-        final String TABLE = "table3";
-        final String qualifiedTable = KS + "." + TABLE;
-        File tempdir = Files.createTempDir();
-        File dataDir = new File(tempdir.getAbsolutePath() + File.separator + KS + File.separator + TABLE);
-        assert dataDir.mkdirs();
         // Test to make sure we can write to `date` fields in both old and new formats
         String schema = "CREATE TABLE " + qualifiedTable + " ("
                         + "  k text,"
@@ -443,7 +675,7 @@
             writer.addRow(String.valueOf(i), map);
         }
         writer.close();
-        loadSSTables(dataDir, KS);
+        loadSSTables(dataDir, keyspace);
 
         UntypedResultSet rs = QueryProcessor.executeInternal("SELECT * FROM " + qualifiedTable + ";");
         assertEquals(200, rs.size());
@@ -475,12 +707,6 @@
     @Test
     public void testFrozenMapTypeCustomOrdered() throws Exception
     {
-        final String KS = "cql_keyspace4";
-        final String TABLE = "table4";
-        final String qualifiedTable = KS + "." + TABLE;
-        File tempdir = Files.createTempDir();
-        File dataDir = new File(tempdir.getAbsolutePath() + File.separator + KS + File.separator + TABLE);
-        assert dataDir.mkdirs();
         // Test to make sure we can write to `date` fields in both old and new formats
         String schema = "CREATE TABLE " + qualifiedTable + " ("
                         + "  k text,"
@@ -510,7 +736,7 @@
         writer.addRow(String.valueOf(2), map2);
 
         writer.close();
-        loadSSTables(dataDir, KS);
+        loadSSTables(dataDir, keyspace);
 
         UntypedResultSet rs = QueryProcessor.executeInternal("SELECT * FROM " + qualifiedTable + ";");
         assertEquals(2, rs.size());
@@ -535,12 +761,6 @@
     @Test
     public void testFrozenSetTypeCustomOrdered() throws Exception
     {
-        final String KS = "cql_keyspace5";
-        final String TABLE = "table5";
-        final String qualifiedTable = KS + "." + TABLE;
-        File tempdir = Files.createTempDir();
-        File dataDir = new File(tempdir.getAbsolutePath() + File.separator + KS + File.separator + TABLE);
-        assert dataDir.mkdirs();
         // Test to make sure we can write to `date` fields in both old and new formats
         String schema = "CREATE TABLE " + qualifiedTable + " ("
                         + "  k text,"
@@ -568,7 +788,7 @@
         writer.addRow(String.valueOf(2), set2);
 
         writer.close();
-        loadSSTables(dataDir, KS);
+        loadSSTables(dataDir, keyspace);
 
         UntypedResultSet rs = QueryProcessor.executeInternal("SELECT * FROM " + qualifiedTable + ";");
         assertEquals(2, rs.size());
diff --git a/test/unit/org/apache/cassandra/io/sstable/DescriptorTest.java b/test/unit/org/apache/cassandra/io/sstable/DescriptorTest.java
index 184d637..9c1dc84 100644
--- a/test/unit/org/apache/cassandra/io/sstable/DescriptorTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/DescriptorTest.java
@@ -23,8 +23,10 @@
 
 import org.apache.commons.lang3.StringUtils;
 import org.junit.Assert;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.Directories;
 import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.utils.ByteBufferUtil;
@@ -45,6 +47,12 @@
         tempDataDir = File.createTempFile("DescriptorTest", null).getParentFile();
     }
 
+    @BeforeClass
+    public static void setup()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testFromFilename() throws Exception
     {
@@ -76,14 +84,14 @@
     private void testFromFilenameFor(File dir)
     {
         // normal
-        checkFromFilename(new Descriptor(dir, ksname, cfname, 1), false);
+        checkFromFilename(new Descriptor(dir, ksname, cfname, 1, SSTableFormat.Type.BIG), false);
         // skip component (for streaming lock file)
-        checkFromFilename(new Descriptor(dir, ksname, cfname, 2), true);
+        checkFromFilename(new Descriptor(dir, ksname, cfname, 2, SSTableFormat.Type.BIG), true);
 
         // secondary index
         String idxName = "myidx";
         File idxDir = new File(dir.getAbsolutePath() + File.separator + Directories.SECONDARY_INDEX_NAME_SEPARATOR + idxName);
-        checkFromFilename(new Descriptor(idxDir, ksname, cfname + Directories.SECONDARY_INDEX_NAME_SEPARATOR + idxName, 4), false);
+        checkFromFilename(new Descriptor(idxDir, ksname, cfname + Directories.SECONDARY_INDEX_NAME_SEPARATOR + idxName, 4, SSTableFormat.Type.BIG), false);
 
         // legacy version
         checkFromFilename(new Descriptor("ja", dir, ksname, cfname, 1, SSTableFormat.Type.LEGACY), false);
@@ -119,8 +127,8 @@
     {
         // Descriptor should be equal when parent directory points to the same directory
         File dir = new File(".");
-        Descriptor desc1 = new Descriptor(dir, "ks", "cf", 1);
-        Descriptor desc2 = new Descriptor(dir.getAbsoluteFile(), "ks", "cf", 1);
+        Descriptor desc1 = new Descriptor(dir, "ks", "cf", 1, SSTableFormat.Type.BIG);
+        Descriptor desc2 = new Descriptor(dir.getAbsoluteFile(), "ks", "cf", 1, SSTableFormat.Type.BIG);
         assertEquals(desc1, desc2);
         assertEquals(desc1.hashCode(), desc2.hashCode());
     }
diff --git a/test/unit/org/apache/cassandra/io/sstable/IndexHelperTest.java b/test/unit/org/apache/cassandra/io/sstable/IndexHelperTest.java
deleted file mode 100644
index e6328de..0000000
--- a/test/unit/org/apache/cassandra/io/sstable/IndexHelperTest.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
-* 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.io.sstable;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import org.junit.Test;
-
-import org.apache.cassandra.Util;
-import org.apache.cassandra.db.ClusteringComparator;
-import org.apache.cassandra.db.ClusteringPrefix;
-import org.apache.cassandra.db.DeletionTime;
-import org.apache.cassandra.db.marshal.AbstractType;
-import org.apache.cassandra.db.marshal.LongType;
-import org.apache.cassandra.utils.FBUtilities;
-
-import static org.apache.cassandra.io.sstable.IndexHelper.IndexInfo;
-import static org.junit.Assert.assertEquals;
-
-public class IndexHelperTest
-{
-
-    private static ClusteringComparator comp = new ClusteringComparator(Collections.<AbstractType<?>>singletonList(LongType.instance));
-    private static ClusteringPrefix cn(long l)
-    {
-        return Util.clustering(comp, l);
-    }
-
-    @Test
-    public void testIndexHelper()
-    {
-        DeletionTime deletionInfo = new DeletionTime(FBUtilities.timestampMicros(), FBUtilities.nowInSeconds());
-
-        List<IndexInfo> indexes = new ArrayList<>();
-        indexes.add(new IndexInfo(cn(0L), cn(5L), 0, 0, deletionInfo));
-        indexes.add(new IndexInfo(cn(10L), cn(15L), 0, 0, deletionInfo));
-        indexes.add(new IndexInfo(cn(20L), cn(25L), 0, 0, deletionInfo));
-
-        assertEquals(0, IndexHelper.indexFor(cn(-1L), indexes, comp, false, -1));
-        assertEquals(0, IndexHelper.indexFor(cn(5L), indexes, comp, false, -1));
-        assertEquals(1, IndexHelper.indexFor(cn(12L), indexes, comp, false, -1));
-        assertEquals(2, IndexHelper.indexFor(cn(17L), indexes, comp, false, -1));
-        assertEquals(3, IndexHelper.indexFor(cn(100L), indexes, comp, false, -1));
-        assertEquals(3, IndexHelper.indexFor(cn(100L), indexes, comp, false, 0));
-        assertEquals(3, IndexHelper.indexFor(cn(100L), indexes, comp, false, 1));
-        assertEquals(3, IndexHelper.indexFor(cn(100L), indexes, comp, false, 2));
-        assertEquals(3, IndexHelper.indexFor(cn(100L), indexes, comp, false, 3));
-
-        assertEquals(-1, IndexHelper.indexFor(cn(-1L), indexes, comp, true, -1));
-        assertEquals(0, IndexHelper.indexFor(cn(5L), indexes, comp, true, 3));
-        assertEquals(0, IndexHelper.indexFor(cn(5L), indexes, comp, true, 2));
-        assertEquals(1, IndexHelper.indexFor(cn(17L), indexes, comp, true, 3));
-        assertEquals(2, IndexHelper.indexFor(cn(100L), indexes, comp, true, 3));
-        assertEquals(2, IndexHelper.indexFor(cn(100L), indexes, comp, true, 4));
-        assertEquals(1, IndexHelper.indexFor(cn(12L), indexes, comp, true, 3));
-        assertEquals(1, IndexHelper.indexFor(cn(12L), indexes, comp, true, 2));
-        assertEquals(1, IndexHelper.indexFor(cn(100L), indexes, comp, true, 1));
-        assertEquals(2, IndexHelper.indexFor(cn(100L), indexes, comp, true, 2));
-    }
-}
diff --git a/test/unit/org/apache/cassandra/io/sstable/IndexSummaryManagerTest.java b/test/unit/org/apache/cassandra/io/sstable/IndexSummaryManagerTest.java
index 9eb63c5..b33ead2 100644
--- a/test/unit/org/apache/cassandra/io/sstable/IndexSummaryManagerTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/IndexSummaryManagerTest.java
@@ -37,6 +37,7 @@
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
+import org.apache.cassandra.concurrent.NamedThreadFactory;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.RowUpdateBuilder;
@@ -633,7 +634,7 @@
         // barrier to control when redistribution runs
         final CountDownLatch barrier = new CountDownLatch(1);
 
-        Thread t = new Thread(new Runnable()
+        Thread t = NamedThreadFactory.createThread(new Runnable()
         {
             public void run()
             {
diff --git a/test/unit/org/apache/cassandra/io/sstable/IndexSummaryTest.java b/test/unit/org/apache/cassandra/io/sstable/IndexSummaryTest.java
index 6f37d8f..d83b96c 100644
--- a/test/unit/org/apache/cassandra/io/sstable/IndexSummaryTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/IndexSummaryTest.java
@@ -29,6 +29,7 @@
 import org.junit.Assume;
 
 import org.apache.cassandra.Util;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.RandomPartitioner;
@@ -46,7 +47,18 @@
 public class IndexSummaryTest
 {
     private final static Random random = new Random();
-    private final static IPartitioner partitioner = Util.testPartitioner();
+
+    @BeforeClass
+    public static void initDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+
+        final long seed = System.nanoTime();
+        System.out.println("Using seed: " + seed);
+        random.setSeed(seed);
+    }
+
+    IPartitioner partitioner = Util.testPartitioner();
 
     @BeforeClass
     public static void setup()
diff --git a/test/unit/org/apache/cassandra/io/sstable/LargePartitionsTest.java b/test/unit/org/apache/cassandra/io/sstable/LargePartitionsTest.java
new file mode 100644
index 0000000..45fd712
--- /dev/null
+++ b/test/unit/org/apache/cassandra/io/sstable/LargePartitionsTest.java
@@ -0,0 +1,236 @@
+/*
+ * 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.io.sstable;
+
+import java.util.Iterator;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.metrics.CacheMetrics;
+import org.apache.cassandra.service.CacheService;
+
+/**
+ * Test intended to manually measure GC pressure to write and read partitions of different size
+ * for CASSANDRA-11206.
+ */
+@RunWith(OrderedJUnit4ClassRunner.class)
+@Ignore // all these tests take very, very long - so only run them manually
+public class LargePartitionsTest extends CQLTester
+{
+
+    @FunctionalInterface
+    interface Measured
+    {
+        void measure() throws Throwable;
+    }
+
+    private static void measured(String name, Measured measured) throws Throwable
+    {
+        long t0 = System.currentTimeMillis();
+        measured.measure();
+        long t = System.currentTimeMillis() - t0;
+        System.out.println("LargePartitionsTest-measured: " + name + " took " + t + " ms");
+    }
+
+    private static String randomText(int bytes)
+    {
+        char[] ch = new char[bytes];
+        ThreadLocalRandom r = ThreadLocalRandom.current();
+        for (int i = 0; i < bytes; i++)
+            ch[i] = (char) (32 + r.nextInt(95));
+        return new String(ch);
+    }
+
+    private static final int rowKBytes = 8;
+
+    private void withPartitionSize(long partitionKBytes, long totalMBytes) throws Throwable
+    {
+        long totalKBytes = totalMBytes * 1024L;
+
+        createTable("CREATE TABLE %s (pk text, ck text, val text, PRIMARY KEY (pk, ck))");
+
+        String name = "part=" + partitionKBytes + "k total=" + totalMBytes + 'M';
+
+        measured("INSERTs for " + name, () -> {
+            for (long writtenKBytes = 0L; writtenKBytes < totalKBytes; writtenKBytes += partitionKBytes)
+            {
+                String pk = Long.toBinaryString(writtenKBytes);
+                for (long kbytes = 0L; kbytes < partitionKBytes; kbytes += rowKBytes)
+                {
+                    String ck = Long.toBinaryString(kbytes);
+                    execute("INSERT INTO %s (pk, ck, val) VALUES (?,?,?)", pk, ck, randomText(rowKBytes * 1024));
+                }
+            }
+        });
+
+        measured("flush for " + name, () -> flush(true));
+
+        CacheService.instance.keyCache.clear();
+
+        measured("compact for " + name, () -> {
+            keyCacheMetrics("before compaction");
+            compact();
+            keyCacheMetrics("after compaction");
+        });
+
+        measured("SELECTs 1 for " + name, () -> selects(partitionKBytes, totalKBytes));
+
+        measured("SELECTs 2 for " + name, () -> selects(partitionKBytes, totalKBytes));
+
+        CacheService.instance.keyCache.clear();
+        measured("Scan for " + name, () -> scan(partitionKBytes, totalKBytes));
+    }
+
+    private void selects(long partitionKBytes, long totalKBytes) throws Throwable
+    {
+        for (int i = 0; i < 50000; i++)
+        {
+            long pk = ThreadLocalRandom.current().nextLong(totalKBytes / partitionKBytes) * partitionKBytes;
+            long ck = ThreadLocalRandom.current().nextLong(partitionKBytes / rowKBytes) * rowKBytes;
+            execute("SELECT val FROM %s WHERE pk=? AND ck=?",
+                    Long.toBinaryString(pk),
+                    Long.toBinaryString(ck)).one();
+            if (i % 1000 == 0)
+                keyCacheMetrics("after " + i + " selects");
+        }
+        keyCacheMetrics("after all selects");
+    }
+
+    private void scan(long partitionKBytes, long totalKBytes) throws Throwable
+    {
+        long pk = ThreadLocalRandom.current().nextLong(totalKBytes / partitionKBytes) * partitionKBytes;
+        Iterator<UntypedResultSet.Row> iter = execute("SELECT val FROM %s WHERE pk=?", Long.toBinaryString(pk)).iterator();
+        int i = 0;
+        while (iter.hasNext())
+        {
+            iter.next();
+            if (i++ % 1000 == 0)
+                keyCacheMetrics("after " + i + " iteration");
+        }
+        keyCacheMetrics("after all iteration");
+    }
+
+    private static void keyCacheMetrics(String title)
+    {
+        CacheMetrics metrics = CacheService.instance.keyCache.getMetrics();
+        System.out.println("Key cache metrics " + title + ": capacity:" + metrics.capacity.getValue() +
+                           " size:"+metrics.size.getValue()+
+                           " entries:" + metrics.entries.getValue() +
+                           " hit-rate:"+metrics.hitRate.getValue() +
+                           " one-min-rate:"+metrics.oneMinuteHitRate.getValue());
+    }
+
+    @Test
+    public void prepare() throws Throwable
+    {
+        for (int i = 0; i < 4; i++)
+        {
+            withPartitionSize(8L, 32L);
+        }
+    }
+
+    @Test
+    public void test_01_16k() throws Throwable
+    {
+        withPartitionSize(16L, 1024L);
+    }
+
+    @Test
+    public void test_02_512k() throws Throwable
+    {
+        withPartitionSize(512L, 1024L);
+    }
+
+    @Test
+    public void test_03_1M() throws Throwable
+    {
+        withPartitionSize(1024L, 1024L);
+    }
+
+    @Test
+    public void test_04_4M() throws Throwable
+    {
+        withPartitionSize(4L * 1024L, 1024L);
+    }
+
+    @Test
+    public void test_05_8M() throws Throwable
+    {
+        withPartitionSize(8L * 1024L, 1024L);
+    }
+
+    @Test
+    public void test_06_16M() throws Throwable
+    {
+        withPartitionSize(16L * 1024L, 1024L);
+    }
+
+    @Test
+    public void test_07_32M() throws Throwable
+    {
+        withPartitionSize(32L * 1024L, 1024L);
+    }
+
+    @Test
+    public void test_08_64M() throws Throwable
+    {
+        withPartitionSize(64L * 1024L, 1024L);
+    }
+
+    @Test
+    public void test_09_256M() throws Throwable
+    {
+        withPartitionSize(256L * 1024L, 4 * 1024L);
+    }
+
+    @Test
+    public void test_10_512M() throws Throwable
+    {
+        withPartitionSize(512L * 1024L, 4 * 1024L);
+    }
+
+    @Test
+    public void test_11_1G() throws Throwable
+    {
+        withPartitionSize(1024L * 1024L, 8 * 1024L);
+    }
+
+    @Test
+    public void test_12_2G() throws Throwable
+    {
+        withPartitionSize(2L * 1024L * 1024L, 8 * 1024L);
+    }
+
+    @Test
+    public void test_13_4G() throws Throwable
+    {
+        withPartitionSize(4L * 1024L * 1024L, 16 * 1024L);
+    }
+
+    @Test
+    public void test_14_8G() throws Throwable
+    {
+        withPartitionSize(8L * 1024L * 1024L, 32 * 1024L);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/io/sstable/LegacySSTableTest.java b/test/unit/org/apache/cassandra/io/sstable/LegacySSTableTest.java
index 201e448..6411718 100644
--- a/test/unit/org/apache/cassandra/io/sstable/LegacySSTableTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/LegacySSTableTest.java
@@ -36,6 +36,7 @@
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.cql3.QueryProcessor;
@@ -108,6 +109,12 @@
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
+        String scp = System.getProperty(LEGACY_SSTABLE_PROP);
+        Assert.assertNotNull("System property " + LEGACY_SSTABLE_PROP + " not set", scp);
+        
+        LEGACY_SSTABLE_ROOT = new File(scp).getAbsoluteFile();
+        Assert.assertTrue("System property " + LEGACY_SSTABLE_ROOT + " does not specify a directory", LEGACY_SSTABLE_ROOT.isDirectory());
+
         SchemaLoader.prepareServer();
         StorageService.instance.initServer();
         Keyspace.setInitialized();
@@ -116,10 +123,7 @@
         {
             createTables(legacyVersion);
         }
-        String scp = System.getProperty(LEGACY_SSTABLE_PROP);
-        assert scp != null;
-        LEGACY_SSTABLE_ROOT = new File(scp).getAbsoluteFile();
-        assert LEGACY_SSTABLE_ROOT.isDirectory();
+
     }
 
     @After
@@ -144,14 +148,31 @@
     @Test
     public void testLoadLegacyCqlTables() throws Exception
     {
+        DatabaseDescriptor.setColumnIndexCacheSize(99999);
+        CacheService.instance.invalidateKeyCache();
+        doTestLegacyCqlTables();
+    }
+
+    @Test
+    public void testLoadLegacyCqlTablesShallow() throws Exception
+    {
+        DatabaseDescriptor.setColumnIndexCacheSize(0);
+        CacheService.instance.invalidateKeyCache();
+        doTestLegacyCqlTables();
+    }
+
+    private void doTestLegacyCqlTables() throws Exception
+    {
         for (String legacyVersion : legacyVersions)
         {
             logger.info("Loading legacy version: {}", legacyVersion);
+            truncateLegacyTables(legacyVersion);
             loadLegacyTables(legacyVersion);
             CacheService.instance.invalidateKeyCache();
             long startCount = CacheService.instance.keyCache.size();
             verifyReads(legacyVersion);
             verifyCache(legacyVersion, startCount);
+            compactLegacyTables(legacyVersion);
         }
     }
 
@@ -313,7 +334,6 @@
         }
     }
 
-
     @Test
     public void testInaccurateSSTableMinMax() throws Exception
     {
@@ -417,6 +437,17 @@
     }
 
     @Test
+    public void test15081() throws Exception
+    {
+        QueryProcessor.executeInternal("CREATE TABLE legacy_tables.legacy_ka_15081 (id int primary key, payload text)");
+        loadLegacyTable("legacy_%s_15081%s", "ka", "");
+        UntypedResultSet results =
+            QueryProcessor.executeOnceInternal(
+                String.format("SELECT * FROM legacy_tables.legacy_ka_15081"));
+        assertRows(results, row(1, "hello world"));
+    }
+
+    @Test
     public void testReadingLegacyTablesWithIllegalCellNames() throws Exception {
         /**
          * The sstable can be generated externally with SSTableSimpleUnsortedWriter:
@@ -627,6 +658,30 @@
                                              .execute().get();
     }
 
+    private static void truncateLegacyTables(String legacyVersion) throws Exception
+    {
+        for (int compact = 0; compact <= 1; compact++)
+        {
+            logger.info("Truncating legacy version {}{}", legacyVersion, getCompactNameSuffix(compact));
+            Keyspace.open("legacy_tables").getColumnFamilyStore(String.format("legacy_%s_simple%s", legacyVersion, getCompactNameSuffix(compact))).truncateBlocking();
+            Keyspace.open("legacy_tables").getColumnFamilyStore(String.format("legacy_%s_simple_counter%s", legacyVersion, getCompactNameSuffix(compact))).truncateBlocking();
+            Keyspace.open("legacy_tables").getColumnFamilyStore(String.format("legacy_%s_clust%s", legacyVersion, getCompactNameSuffix(compact))).truncateBlocking();
+            Keyspace.open("legacy_tables").getColumnFamilyStore(String.format("legacy_%s_clust_counter%s", legacyVersion, getCompactNameSuffix(compact))).truncateBlocking();
+        }
+    }
+
+    private static void compactLegacyTables(String legacyVersion) throws Exception
+    {
+        for (int compact = 0; compact <= 1; compact++)
+        {
+            logger.info("Compacting legacy version {}{}", legacyVersion, getCompactNameSuffix(compact));
+            Keyspace.open("legacy_tables").getColumnFamilyStore(String.format("legacy_%s_simple%s", legacyVersion, getCompactNameSuffix(compact))).forceMajorCompaction();
+            Keyspace.open("legacy_tables").getColumnFamilyStore(String.format("legacy_%s_simple_counter%s", legacyVersion, getCompactNameSuffix(compact))).forceMajorCompaction();
+            Keyspace.open("legacy_tables").getColumnFamilyStore(String.format("legacy_%s_clust%s", legacyVersion, getCompactNameSuffix(compact))).forceMajorCompaction();
+            Keyspace.open("legacy_tables").getColumnFamilyStore(String.format("legacy_%s_clust_counter%s", legacyVersion, getCompactNameSuffix(compact))).forceMajorCompaction();
+        }
+    }
+
     private static void loadLegacyTables(String legacyVersion) throws Exception
     {
         for (int compact = 0; compact <= 1; compact++)
@@ -902,7 +957,9 @@
 
     private static void copySstablesToTestData(String legacyVersion, String table, File cfDir) throws IOException
     {
-        for (File file : getTableDir(legacyVersion, table).listFiles())
+        File tableDir = getTableDir(legacyVersion, table);
+        Assert.assertTrue("The table directory " + tableDir + " was not found", tableDir.isDirectory());
+        for (File file : tableDir.listFiles())
         {
             copyFile(cfDir, file);
         }
@@ -920,10 +977,11 @@
         {
             File target = new File(cfDir, file.getName());
             int rd;
-            FileInputStream is = new FileInputStream(file);
-            FileOutputStream os = new FileOutputStream(target);
-            while ((rd = is.read(buf)) >= 0)
-                os.write(buf, 0, rd);
+            try (FileInputStream is = new FileInputStream(file);
+                 FileOutputStream os = new FileOutputStream(target);) {
+                while ((rd = is.read(buf)) >= 0)
+                    os.write(buf, 0, rd);
+                }
         }
     }
 }
diff --git a/test/unit/org/apache/cassandra/io/sstable/SSTableCorruptionDetectionTest.java b/test/unit/org/apache/cassandra/io/sstable/SSTableCorruptionDetectionTest.java
index 451af25..f7ced23 100644
--- a/test/unit/org/apache/cassandra/io/sstable/SSTableCorruptionDetectionTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/SSTableCorruptionDetectionTest.java
@@ -168,6 +168,9 @@
             }
             finally
             {
+                if (ChunkCache.instance != null)
+                    ChunkCache.instance.invalidateFile(ssTableReader.getFilename());
+
                 restore(raf, corruptionPosition, backup);
             }
         }
@@ -209,6 +212,7 @@
             {
                 DecoratedKey dk = Util.dk(String.format("pkvalue_%07d", i));
                 try (UnfilteredRowIterator rowIter = sstable.iterator(dk,
+                                                                      Slices.ALL,
                                                                       ColumnFilter.all(cfs.metadata),
                                                                       false,
                                                                       false,
diff --git a/test/unit/org/apache/cassandra/io/sstable/SSTableHeaderFixTest.java b/test/unit/org/apache/cassandra/io/sstable/SSTableHeaderFixTest.java
new file mode 100644
index 0000000..c2eadc4
--- /dev/null
+++ b/test/unit/org/apache/cassandra/io/sstable/SSTableHeaderFixTest.java
@@ -0,0 +1,963 @@
+/*
+ * 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.io.sstable;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import com.google.common.collect.Sets;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.FieldIdentifier;
+import org.apache.cassandra.cql3.statements.IndexTarget;
+import org.apache.cassandra.db.SerializationHeader;
+import org.apache.cassandra.db.marshal.AbstractCompositeType;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.CollectionType;
+import org.apache.cassandra.db.marshal.CompositeType;
+import org.apache.cassandra.db.marshal.FloatType;
+import org.apache.cassandra.db.marshal.FrozenType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.ListType;
+import org.apache.cassandra.db.marshal.MapType;
+import org.apache.cassandra.db.marshal.SetType;
+import org.apache.cassandra.db.marshal.TupleType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.db.rows.EncodingStats;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.io.sstable.format.SSTableFormat;
+import org.apache.cassandra.io.sstable.format.Version;
+import org.apache.cassandra.io.sstable.format.big.BigFormat;
+import org.apache.cassandra.io.sstable.metadata.MetadataType;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.SequentialWriter;
+import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.schema.SchemaKeyspace;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.FBUtilities;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Test the functionality of {@link SSTableHeaderFix}.
+ * It writes an 'big-m' version sstable(s) and executes against these.
+ */
+public class SSTableHeaderFixTest
+{
+    static
+    {
+        DatabaseDescriptor.toolInitialization();
+        DatabaseDescriptor.applyAddressConfig();
+    }
+
+    private File temporaryFolder;
+
+    @Before
+    public void setup()
+    {
+        File f = FileUtils.createTempFile("SSTableUDTFixTest", "");
+        f.delete();
+        f.mkdirs();
+        temporaryFolder = f;
+    }
+
+    @After
+    public void teardown()
+    {
+        FileUtils.deleteRecursive(temporaryFolder);
+    }
+
+    private static final AbstractType<?> udtPK = makeUDT("udt_pk");
+    private static final AbstractType<?> udtCK = makeUDT("udt_ck");
+    private static final AbstractType<?> udtStatic = makeUDT("udt_static");
+    private static final AbstractType<?> udtRegular = makeUDT("udt_regular");
+    private static final AbstractType<?> udtInner = makeUDT("udt_inner");
+    private static final AbstractType<?> udtNested = new UserType("ks",
+                                                                  ByteBufferUtil.bytes("udt_nested"),
+                                                                  Arrays.asList(new FieldIdentifier(ByteBufferUtil.bytes("a_field")),
+                                                                                new FieldIdentifier(ByteBufferUtil.bytes("a_udt"))),
+                                                                  Arrays.asList(UTF8Type.instance,
+                                                                                udtInner),
+                                                                  true);
+    private static final AbstractType<?> tupleInTuple = makeTuple(makeTuple());
+    private static final AbstractType<?> udtInTuple = makeTuple(udtInner);
+    private static final AbstractType<?> tupleInComposite = CompositeType.getInstance(UTF8Type.instance, makeTuple());
+    private static final AbstractType<?> udtInComposite = CompositeType.getInstance(UTF8Type.instance, udtInner);
+    private static final AbstractType<?> udtInList = ListType.getInstance(udtInner, true);
+    private static final AbstractType<?> udtInSet = SetType.getInstance(udtInner, true);
+    private static final AbstractType<?> udtInMap = MapType.getInstance(UTF8Type.instance, udtInner, true);
+    private static final AbstractType<?> udtInFrozenList = ListType.getInstance(udtInner, false);
+    private static final AbstractType<?> udtInFrozenSet = SetType.getInstance(udtInner, false);
+    private static final AbstractType<?> udtInFrozenMap = MapType.getInstance(UTF8Type.instance, udtInner, false);
+
+    private static AbstractType<?> makeUDT2(String udtName, boolean multiCell)
+    {
+        return new UserType("ks",
+                            ByteBufferUtil.bytes(udtName),
+                            Arrays.asList(new FieldIdentifier(ByteBufferUtil.bytes("a_field")),
+                                          new FieldIdentifier(ByteBufferUtil.bytes("a_udt"))),
+                            Arrays.asList(UTF8Type.instance,
+                                          udtInner),
+                            multiCell);
+    }
+
+    private static AbstractType<?> makeUDT(String udtName)
+    {
+        return new UserType("ks",
+                            ByteBufferUtil.bytes(udtName),
+                            Collections.singletonList(new FieldIdentifier(ByteBufferUtil.bytes("a_field"))),
+                            Collections.singletonList(UTF8Type.instance),
+                            true);
+    }
+
+    private static TupleType makeTuple()
+    {
+        return makeTuple(Int32Type.instance);
+    }
+
+    private static TupleType makeTuple(AbstractType<?> second)
+    {
+        return new TupleType(Arrays.asList(UTF8Type.instance,
+                                           second));
+    }
+
+    private static TupleType makeTupleSimple()
+    {
+        // TODO this should create a non-frozen tuple type for the sake of handling a dropped, non-frozen UDT
+        return new TupleType(Collections.singletonList(UTF8Type.instance));
+    }
+
+    private static final Version version = BigFormat.instance.getVersion("mc");
+
+    private CFMetaData tableMetadata;
+    private final Set<String> updatedColumns = new HashSet<>();
+
+    private ColumnDefinition getColDef(String n)
+    {
+        return tableMetadata.getColumnDefinition(ByteBufferUtil.bytes(n));
+    }
+
+    /**
+     * Very basic test whether {@link SSTableHeaderFix} detect a type mismatch (regular_c 'int' vs 'float').
+     */
+    @Test
+    public void verifyTypeMismatchTest() throws Exception
+    {
+        File dir = temporaryFolder;
+        File sstable = generateFakeSSTable(dir, 1);
+
+        SerializationHeader.Component header = readHeader(sstable);
+        assertFrozenUdt(header, false, true);
+
+        ColumnDefinition cd = getColDef("regular_c");
+        tableMetadata.removeColumnDefinition(cd);
+        tableMetadata.addColumnDefinition(ColumnDefinition.regularDef("ks", "cf", "regular_c", FloatType.instance));
+
+        SSTableHeaderFix headerFix = builder().withPath(sstable.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertTrue(headerFix.hasError());
+        assertTrue(headerFix.hasChanges());
+
+        // must not have re-written the stats-component
+        header = readHeader(sstable);
+        assertFrozenUdt(header, false, true);
+    }
+
+    @Test
+    public void verifyTypeMatchTest() throws Exception
+    {
+        File dir = temporaryFolder;
+
+        List<ColumnDefinition> cols = new ArrayList<>();
+        cols.add(ColumnDefinition.partitionKeyDef("ks", "cf", "pk", udtPK, 0));
+        cols.add(ColumnDefinition.clusteringDef("ks", "cf", "ck", udtCK, 0));
+        commonColumns(cols);
+        File sstable = buildFakeSSTable(dir, 1, cols, false);
+
+        SerializationHeader.Component header = readHeader(sstable);
+        assertFrozenUdt(header, false, true);
+
+        SSTableHeaderFix headerFix = builder().withPath(sstable.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertTrue(updatedColumns.isEmpty());
+        assertFalse(headerFix.hasError());
+        assertFalse(headerFix.hasChanges());
+
+        // must not have re-written the stats-component
+        header = readHeader(sstable);
+        assertFrozenUdt(header, false, true);
+    }
+
+    /**
+     * Simulates the case when an sstable contains a column not present in the schema, which can just be ignored.
+     */
+    @Test
+    public void verifyWithUnknownColumnTest() throws Exception
+    {
+        File dir = temporaryFolder;
+        List<ColumnDefinition> cols = new ArrayList<>();
+        cols.add(ColumnDefinition.partitionKeyDef("ks", "cf", "pk", udtPK, 0));
+        cols.add(ColumnDefinition.clusteringDef("ks", "cf", "ck", udtCK, 0));
+        commonColumns(cols);
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "solr_query", UTF8Type.instance));
+        File sstable = buildFakeSSTable(dir, 1, cols, true);
+
+        SerializationHeader.Component header = readHeader(sstable);
+        assertFrozenUdt(header, false, true);
+
+        ColumnDefinition cd = getColDef("solr_query");
+        tableMetadata.removeColumnDefinition(cd);
+
+        SSTableHeaderFix headerFix = builder().withPath(sstable.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertFalse(headerFix.hasError());
+        assertTrue(headerFix.hasChanges());
+
+        // must not have re-written the stats-component
+        header = readHeader(sstable);
+        assertFrozenUdt(header, true, true);
+    }
+
+    /**
+     * Simulates the case when an sstable contains a column not present in the table but as a target for an index.
+     * It can just be ignored.
+     */
+    @Test
+    public void verifyWithIndexedUnknownColumnTest() throws Exception
+    {
+        File dir = temporaryFolder;
+        List<ColumnDefinition> cols = new ArrayList<>();
+        cols.add(ColumnDefinition.partitionKeyDef("ks", "cf", "pk", udtPK, 0));
+        cols.add(ColumnDefinition.clusteringDef("ks", "cf", "ck", udtCK, 0));
+        commonColumns(cols);
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "solr_query", UTF8Type.instance));
+        File sstable = buildFakeSSTable(dir, 1, cols, true);
+
+        SerializationHeader.Component header = readHeader(sstable);
+        assertFrozenUdt(header, false, true);
+
+        ColumnDefinition cd = getColDef("solr_query");
+        tableMetadata.indexes(tableMetadata.getIndexes().with(IndexMetadata.fromSchemaMetadata("some search index", IndexMetadata.Kind.CUSTOM, Collections.singletonMap(IndexTarget.TARGET_OPTION_NAME, "solr_query"))));
+        tableMetadata.removeColumnDefinition(cd);
+
+        SSTableHeaderFix headerFix = builder().withPath(sstable.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertFalse(headerFix.hasError());
+        assertTrue(headerFix.hasChanges());
+
+        // must not have re-written the stats-component
+        header = readHeader(sstable);
+        assertFrozenUdt(header, true, true);
+    }
+
+    @Test
+    public void complexTypeMatchTest() throws Exception
+    {
+        File dir = temporaryFolder;
+
+        List<ColumnDefinition> cols = new ArrayList<>();
+        cols.add(ColumnDefinition.partitionKeyDef("ks", "cf", "pk", udtPK, 0));
+        cols.add(ColumnDefinition.clusteringDef("ks", "cf", "ck", udtCK, 0));
+        commonColumns(cols);
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "tuple_in_tuple", tupleInTuple));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_nested", udtNested));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_tuple", udtInTuple));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "tuple_in_composite", tupleInComposite));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_composite", udtInComposite));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_list", udtInList));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_set", udtInSet));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_map", udtInMap));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_frozen_list", udtInFrozenList));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_frozen_set", udtInFrozenSet));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_frozen_map", udtInFrozenMap));
+        File sstable = buildFakeSSTable(dir, 1, cols, true);
+
+        SerializationHeader.Component header = readHeader(sstable);
+        assertFrozenUdt(header, false, true);
+
+        SSTableHeaderFix headerFix = builder().withPath(sstable.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertFalse(headerFix.hasError());
+        assertTrue(headerFix.hasChanges());
+        assertEquals(Sets.newHashSet("pk", "ck", "regular_b", "static_b",
+                                     "udt_nested", "udt_in_composite", "udt_in_list", "udt_in_set", "udt_in_map"), updatedColumns);
+
+        // must not have re-written the stats-component
+        header = readHeader(sstable);
+        assertFrozenUdt(header, true, true);
+    }
+
+    @Test
+    public void complexTypeDroppedColumnsMatchTest() throws Exception
+    {
+        File dir = temporaryFolder;
+
+        List<ColumnDefinition> cols = new ArrayList<>();
+        cols.add(ColumnDefinition.partitionKeyDef("ks", "cf", "pk", udtPK, 0));
+        cols.add(ColumnDefinition.clusteringDef("ks", "cf", "ck", udtCK, 0));
+        commonColumns(cols);
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "tuple_in_tuple", tupleInTuple));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_nested", udtNested));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_tuple", udtInTuple));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "tuple_in_composite", tupleInComposite));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_composite", udtInComposite));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_list", udtInList));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_set", udtInSet));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_map", udtInMap));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_frozen_list", udtInFrozenList));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_frozen_set", udtInFrozenSet));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "udt_in_frozen_map", udtInFrozenMap));
+        File sstable = buildFakeSSTable(dir, 1, cols, true);
+
+        for (String col : new String[]{"tuple_in_tuple", "udt_nested", "udt_in_tuple",
+                                       "tuple_in_composite", "udt_in_composite",
+                                       "udt_in_list", "udt_in_set", "udt_in_map",
+                                       "udt_in_frozen_list", "udt_in_frozen_set", "udt_in_frozen_map"})
+        {
+            ColumnDefinition cd = getColDef(col);
+            tableMetadata.removeColumnDefinition(cd);
+            AbstractType<?> dropType = SchemaKeyspace.expandUserTypes(cd.type);
+            tableMetadata.recordColumnDrop(new ColumnDefinition(cd.ksName, cd.cfName, cd.name, dropType, cd.position(), cd.kind), FBUtilities.timestampMicros());
+        }
+
+        SerializationHeader.Component header = readHeader(sstable);
+        assertFrozenUdt(header, false, true);
+
+        SSTableHeaderFix headerFix = builder().withPath(sstable.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertFalse(headerFix.hasError());
+        assertTrue(headerFix.hasChanges());
+        assertEquals(Sets.newHashSet("pk", "ck", "regular_b", "static_b", "udt_nested"), updatedColumns);
+
+        // must not have re-written the stats-component
+        header = readHeader(sstable);
+        // do not check the inner types, as the inner types were not fixed in the serialization-header (test thing)
+        assertFrozenUdt(header, true, false);
+    }
+
+    @Test
+    public void variousDroppedUserTypes() throws Exception
+    {
+        File dir = temporaryFolder;
+
+        List<ColumnDefinition> cols = new ArrayList<>();
+        cols.add(ColumnDefinition.partitionKeyDef("ks", "cf", "pk", udtPK, 0));
+        cols.add(ColumnDefinition.clusteringDef("ks", "cf", "ck", udtCK, 0));
+
+        ColSpec[] colSpecs = new ColSpec[]
+                {
+                        // 'frozen<udt>' / live
+                        new ColSpec("frozen_udt_as_frozen_udt_live",
+                                    makeUDT2("frozen_udt_as_frozen_udt_live", false),
+                                    makeUDT2("frozen_udt_as_frozen_udt_live", false),
+                                    false,
+                                    false),
+                        // 'frozen<udt>' / live / as 'udt'
+                        new ColSpec("frozen_udt_as_unfrozen_udt_live",
+                                    makeUDT2("frozen_udt_as_unfrozen_udt_live", false),
+                                    makeUDT2("frozen_udt_as_unfrozen_udt_live", true),
+                                    false,
+                                    true),
+                        // 'frozen<udt>' / dropped
+                        new ColSpec("frozen_udt_as_frozen_udt_dropped",
+                                    SchemaKeyspace.expandUserTypes(makeUDT2("frozen_udt_as_frozen_udt_dropped", true).freezeNestedMulticellTypes().freeze()),
+                                    makeUDT2("frozen_udt_as_frozen_udt_dropped", false),
+                                    makeUDT2("frozen_udt_as_frozen_udt_dropped", false),
+                                    true,
+                                    false),
+                        // 'frozen<udt>' / dropped / as 'udt'
+                        new ColSpec("frozen_udt_as_unfrozen_udt_dropped",
+                                    SchemaKeyspace.expandUserTypes(makeUDT2("frozen_udt_as_unfrozen_udt_dropped", true).freezeNestedMulticellTypes().freeze()),
+                                    makeUDT2("frozen_udt_as_unfrozen_udt_dropped", true),
+                                    makeUDT2("frozen_udt_as_unfrozen_udt_dropped", false),
+                                    true,
+                                    true),
+                        // 'udt' / live
+                        new ColSpec("unfrozen_udt_as_unfrozen_udt_live",
+                                    makeUDT2("unfrozen_udt_as_unfrozen_udt_live", true),
+                                    makeUDT2("unfrozen_udt_as_unfrozen_udt_live", true),
+                                    false,
+                                    false),
+                        // 'udt' / dropped
+// TODO unable to test dropping a non-frozen UDT, as that requires an unfrozen tuple as well
+//                        new ColSpec("unfrozen_udt_as_unfrozen_udt_dropped",
+//                                    SchemaKeyspace.expandUserTypes(makeUDT2("unfrozen_udt_as_unfrozen_udt_dropped", true).freezeNestedMulticellTypes()),
+//                                    makeUDT2("unfrozen_udt_as_unfrozen_udt_dropped", true),
+//                                    makeUDT2("unfrozen_udt_as_unfrozen_udt_dropped", true),
+//                                    true,
+//                                    false),
+                        // 'frozen<tuple>' as 'TupleType(multiCell=false' (there is nothing like 'FrozenType(TupleType(')
+                        new ColSpec("frozen_tuple_as_frozen_tuple_live",
+                                    makeTupleSimple(),
+                                    makeTupleSimple(),
+                                    false,
+                                    false),
+                        // 'frozen<tuple>' as 'TupleType(multiCell=false' (there is nothing like 'FrozenType(TupleType(')
+                        new ColSpec("frozen_tuple_as_frozen_tuple_dropped",
+                                    makeTupleSimple(),
+                                    makeTupleSimple(),
+                                    true,
+                                    false)
+                };
+
+        Arrays.stream(colSpecs).forEach(c -> cols.add(ColumnDefinition.regularDef("ks", "cf", c.name,
+                                                                              // use the initial column type for the serialization header header.
+                                                                              c.preFix)));
+
+        Map<String, ColSpec> colSpecMap = Arrays.stream(colSpecs).collect(Collectors.toMap(c -> c.name, c -> c));
+        File sstable = buildFakeSSTable(dir, 1, cols, (s) -> s.map(c -> {
+            ColSpec cs = colSpecMap.get(c.name.toString());
+            if (cs == null)
+                return c;
+            // update the column type in the schema to the "correct" one.
+            return new ColumnDefinition(c.ksName, c.cfName, c.name, cs.schema, c.position(), c.kind);
+        }));
+
+        Arrays.stream(colSpecs)
+              .filter(c -> c.dropped)
+              .forEach(c -> {
+                  ColumnDefinition cd = getColDef(c.name);
+                  tableMetadata.removeColumnDefinition(cd);
+                  tableMetadata.recordColumnDrop(cd, FBUtilities.timestampMicros());
+              });
+
+        SerializationHeader.Component header = readHeader(sstable);
+        for (ColSpec colSpec : colSpecs)
+        {
+            AbstractType<?> hdrType = header.getRegularColumns().get(ByteBufferUtil.bytes(colSpec.name));
+            assertEquals(colSpec.name, colSpec.preFix, hdrType);
+            assertEquals(colSpec.name, colSpec.preFix.isMultiCell(), hdrType.isMultiCell());
+        }
+
+        SSTableHeaderFix headerFix = builder().withPath(sstable.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertFalse(headerFix.hasError());
+        assertTrue(headerFix.hasChanges());
+        // Verify that all columns to fix are in the updatedColumns set (paranoid, yet)
+        Arrays.stream(colSpecs)
+              .filter(c -> c.mustFix)
+              .forEach(c -> assertTrue("expect " + c.name + " to be updated, but was not (" + updatedColumns + ")", updatedColumns.contains(c.name)));
+        // Verify that the number of updated columns maches the expected number of columns to fix
+        assertEquals(Arrays.stream(colSpecs).filter(c -> c.mustFix).count(), updatedColumns.size());
+
+        header = readHeader(sstable);
+        for (ColSpec colSpec : colSpecs)
+        {
+            AbstractType<?> hdrType = header.getRegularColumns().get(ByteBufferUtil.bytes(colSpec.name));
+            assertEquals(colSpec.name, colSpec.expect, hdrType);
+            assertEquals(colSpec.name, colSpec.expect.isMultiCell(), hdrType.isMultiCell());
+        }
+    }
+
+    static class ColSpec
+    {
+        final String name;
+        final AbstractType<?> schema;
+        final AbstractType<?> preFix;
+        final AbstractType<?> expect;
+        final boolean dropped;
+        final boolean mustFix;
+
+        ColSpec(String name, AbstractType<?> schema, AbstractType<?> preFix, boolean dropped, boolean mustFix)
+        {
+            this(name, schema, preFix, schema, dropped, mustFix);
+        }
+
+        ColSpec(String name, AbstractType<?> schema, AbstractType<?> preFix, AbstractType<?> expect, boolean dropped, boolean mustFix)
+        {
+            this.name = name;
+            this.schema = schema;
+            this.preFix = preFix;
+            this.expect = expect;
+            this.dropped = dropped;
+            this.mustFix = mustFix;
+        }
+    }
+
+    @Test
+    public void verifyTypeMatchCompositeKeyTest() throws Exception
+    {
+        File dir = temporaryFolder;
+
+        List<ColumnDefinition> cols = new ArrayList<>();
+        cols.add(ColumnDefinition.partitionKeyDef("ks", "cf", "pk1", UTF8Type.instance, 0));
+        cols.add(ColumnDefinition.partitionKeyDef("ks", "cf", "pk2", udtPK, 1));
+        cols.add(ColumnDefinition.clusteringDef("ks", "cf", "ck", udtCK, 0));
+        commonColumns(cols);
+        File sstable = buildFakeSSTable(dir, 1, cols, false);
+
+        SerializationHeader.Component header = readHeader(sstable);
+        assertFrozenUdt(header, false, true);
+
+        SSTableHeaderFix headerFix = builder().withPath(sstable.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertFalse(headerFix.hasError());
+        assertFalse(headerFix.hasChanges());
+        assertTrue(updatedColumns.isEmpty());
+
+        // must not have re-written the stats-component
+        header = readHeader(sstable);
+        assertFrozenUdt(header, false, true);
+    }
+
+    @Test
+    public void compositePartitionKey() throws Exception
+    {
+        List<ColumnDefinition> cols = new ArrayList<>();
+        cols.add(ColumnDefinition.partitionKeyDef("ks", "cf", "pk1", UTF8Type.instance, 0));
+        cols.add(ColumnDefinition.partitionKeyDef("ks", "cf", "pk2", udtPK, 1));
+        cols.add(ColumnDefinition.clusteringDef("ks", "cf", "ck", udtCK, 0));
+        commonColumns(cols);
+
+        File dir = temporaryFolder;
+        File sstable = buildFakeSSTable(dir, 1, cols, true);
+
+        SerializationHeader.Component header = readHeader(sstable);
+        assertTrue(header.getKeyType() instanceof CompositeType);
+        CompositeType keyType = (CompositeType) header.getKeyType();
+        assertEquals(Arrays.asList(UTF8Type.instance, udtPK), keyType.getComponents());
+
+        SSTableHeaderFix headerFix = builder().withPath(sstable.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertFalse(headerFix.hasError());
+        assertTrue(headerFix.hasChanges());
+        assertEquals(Sets.newHashSet("pk2", "ck", "regular_b", "static_b"), updatedColumns);
+
+        header = readHeader(sstable);
+        assertTrue(header.getKeyType() instanceof CompositeType);
+        keyType = (CompositeType) header.getKeyType();
+        assertEquals(Arrays.asList(UTF8Type.instance, udtPK.freeze()), keyType.getComponents());
+    }
+
+    @Test
+    public void compositeClusteringKey() throws Exception
+    {
+        List<ColumnDefinition> cols = new ArrayList<>();
+        cols.add(ColumnDefinition.partitionKeyDef("ks", "cf", "pk", udtPK, 0));
+        cols.add(ColumnDefinition.clusteringDef("ks", "cf", "ck1", Int32Type.instance, 0));
+        cols.add(ColumnDefinition.clusteringDef("ks", "cf", "ck2", udtCK, 1));
+        commonColumns(cols);
+
+        File dir = temporaryFolder;
+        File sstable = buildFakeSSTable(dir, 1, cols, true);
+
+        SerializationHeader.Component header = readHeader(sstable);
+        assertEquals(Arrays.asList(Int32Type.instance, udtCK), header.getClusteringTypes());
+
+        SSTableHeaderFix headerFix = builder().withPath(sstable.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertFalse(headerFix.hasError());
+        assertTrue(headerFix.hasChanges());
+        assertEquals(Sets.newHashSet("pk", "ck2", "regular_b", "static_b"), updatedColumns);
+
+        header = readHeader(sstable);
+        assertEquals(Arrays.asList(Int32Type.instance, udtCK.freeze()), header.getClusteringTypes());
+    }
+
+    /**
+     * Check whether {@link SSTableHeaderFix} can operate on a single file.
+     */
+    @Test
+    public void singleFileUDTFixTest() throws Exception
+    {
+        File dir = temporaryFolder;
+        File sstable = generateFakeSSTable(dir, 1);
+
+        SerializationHeader.Component header = readHeader(sstable);
+        assertFrozenUdt(header, false, true);
+
+        SSTableHeaderFix headerFix = builder().withPath(sstable.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertTrue(headerFix.hasChanges());
+        assertFalse(headerFix.hasError());
+
+        header = readHeader(sstable);
+        assertFrozenUdt(header, true, true);
+    }
+
+    /**
+     * Check whether {@link SSTableHeaderFix} can operate on a file in a directory.
+     */
+    @Test
+    public void singleDirectoryUDTFixTest() throws Exception
+    {
+        File dir = temporaryFolder;
+        List<File> sstables = IntStream.range(1, 11)
+                                       .mapToObj(g -> generateFakeSSTable(dir, g))
+                                       .collect(Collectors.toList());
+
+        for (File sstable : sstables)
+        {
+            SerializationHeader.Component header = readHeader(sstable);
+            assertFrozenUdt(header, false, true);
+        }
+
+        SSTableHeaderFix headerFix = builder().withPath(dir.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertTrue(headerFix.hasChanges());
+        assertFalse(headerFix.hasError());
+
+        for (File sstable : sstables)
+        {
+            SerializationHeader.Component header = readHeader(sstable);
+            assertFrozenUdt(header, true, true);
+        }
+    }
+
+    /**
+     * Check whether {@link SSTableHeaderFix} can operate multiple, single files.
+     */
+    @Test
+    public void multipleFilesUDTFixTest() throws Exception
+    {
+        File dir = temporaryFolder;
+        List<File> sstables = IntStream.range(1, 11)
+                                       .mapToObj(g -> generateFakeSSTable(dir, g))
+                                       .collect(Collectors.toList());
+
+        for (File sstable : sstables)
+        {
+            SerializationHeader.Component header = readHeader(sstable);
+            assertFrozenUdt(header, false, true);
+        }
+
+        SSTableHeaderFix.Builder builder = builder();
+        sstables.stream().map(File::toPath).forEach(builder::withPath);
+        SSTableHeaderFix headerFix = builder.build();
+        headerFix.execute();
+
+        assertTrue(headerFix.hasChanges());
+        assertFalse(headerFix.hasError());
+
+        for (File sstable : sstables)
+        {
+            SerializationHeader.Component header = readHeader(sstable);
+            assertFrozenUdt(header, true, true);
+        }
+    }
+
+    /**
+     * Check whether {@link SSTableHeaderFix} can operate multiple files in a directory.
+     */
+    @Test
+    public void multipleFilesInDirectoryUDTFixTest() throws Exception
+    {
+        File dir = temporaryFolder;
+        List<File> sstables = IntStream.range(1, 11)
+                                       .mapToObj(g -> generateFakeSSTable(dir, g))
+                                       .collect(Collectors.toList());
+
+        for (File sstable : sstables)
+        {
+            SerializationHeader.Component header = readHeader(sstable);
+            assertFrozenUdt(header, false, true);
+        }
+
+        SSTableHeaderFix headerFix = builder().withPath(dir.toPath())
+                                              .build();
+        headerFix.execute();
+
+        assertTrue(headerFix.hasChanges());
+        assertFalse(headerFix.hasError());
+
+        for (File sstable : sstables)
+        {
+            SerializationHeader.Component header = readHeader(sstable);
+            assertFrozenUdt(header, true, true);
+        }
+    }
+
+    private static final Pattern p = Pattern.compile(".* Column '([^']+)' needs to be updated from type .*");
+
+    private SSTableHeaderFix.Builder builder()
+    {
+        updatedColumns.clear();
+        return SSTableHeaderFix.builder()
+                               .schemaCallback(() -> (desc) -> tableMetadata)
+                               .info(ln -> {
+                                   System.out.println("INFO: " + ln);
+                                   Matcher m = p.matcher(ln);
+                                   if (m.matches())
+                                       updatedColumns.add(m.group(1));
+                               })
+                               .warn(ln -> System.out.println("WARN: " + ln))
+                               .error(ln -> System.out.println("ERROR: " + ln));
+    }
+
+    private File generateFakeSSTable(File dir, int generation)
+    {
+        List<ColumnDefinition> cols = new ArrayList<>();
+        cols.add(ColumnDefinition.partitionKeyDef("ks", "cf", "pk", udtPK, 0));
+        cols.add(ColumnDefinition.clusteringDef("ks", "cf", "ck", udtCK, 0));
+        commonColumns(cols);
+        return buildFakeSSTable(dir, generation, cols, true);
+    }
+
+    private void commonColumns(List<ColumnDefinition> cols)
+    {
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "regular_a", UTF8Type.instance));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "regular_b", udtRegular));
+        cols.add(ColumnDefinition.regularDef("ks", "cf", "regular_c", Int32Type.instance));
+        cols.add(ColumnDefinition.staticDef("ks", "cf", "static_a", UTF8Type.instance));
+        cols.add(ColumnDefinition.staticDef("ks", "cf", "static_b", udtStatic));
+        cols.add(ColumnDefinition.staticDef("ks", "cf", "static_c", Int32Type.instance));
+    }
+
+    private File buildFakeSSTable(File dir, int generation, List<ColumnDefinition> cols, boolean freezeInSchema)
+    {
+        return buildFakeSSTable(dir, generation, cols, freezeInSchema
+                                                       ? s -> s.map(c -> new ColumnDefinition(c.ksName, c.cfName, c.name, freezeUdt(c.type), c.position(), c.kind))
+                                                       : s -> s);
+    }
+
+    private File buildFakeSSTable(File dir, int generation, List<ColumnDefinition> cols, Function<Stream<ColumnDefinition>, Stream<ColumnDefinition>> freezer)
+    {
+        CFMetaData headerMetadata = CFMetaData.create("ks", "cf",
+                                                      UUID.randomUUID(),
+                                                      false, true, false, false, false,
+                                                      cols,
+                                                      new Murmur3Partitioner());
+
+        List<ColumnDefinition> schemaCols = freezer.apply(cols.stream()).collect(Collectors.toList());
+        tableMetadata = CFMetaData.create("ks", "cf",
+                                          UUID.randomUUID(),
+                                          false, true, false, false, false,
+                                          schemaCols,
+                                          new Murmur3Partitioner());
+
+        try
+        {
+            Descriptor desc = new Descriptor(version, dir, "ks", "cf", generation, SSTableFormat.Type.BIG, Component.DATA);
+
+            // Just create the component files - we don't really need those.
+            for (Component component : requiredComponents)
+                assertTrue(new File(desc.filenameFor(component)).createNewFile());
+
+            AbstractType<?> partitionKey = headerMetadata.getKeyValidator();
+            List<AbstractType<?>> clusteringKey = headerMetadata.clusteringColumns()
+                                                                .stream()
+                                                                .map(cd -> cd.type)
+                                                                .collect(Collectors.toList());
+            Map<ByteBuffer, AbstractType<?>> staticColumns = headerMetadata.allColumns()
+                                                                           .stream()
+                                                                           .filter(cd -> cd.kind == ColumnDefinition.Kind.STATIC)
+                                                                           .collect(Collectors.toMap(cd -> cd.name.bytes, cd -> cd.type, (a, b) -> a));
+            Map<ByteBuffer, AbstractType<?>> regularColumns = headerMetadata.allColumns()
+                                                                            .stream()
+                                                                            .filter(cd -> cd.kind == ColumnDefinition.Kind.REGULAR)
+                                                                            .collect(Collectors.toMap(cd -> cd.name.bytes, cd -> cd.type, (a, b) -> a));
+
+            File statsFile = new File(desc.filenameFor(Component.STATS));
+            SerializationHeader.Component header = SerializationHeader.Component.buildComponentForTools(partitionKey,
+                                                                                                        clusteringKey,
+                                                                                                        staticColumns,
+                                                                                                        regularColumns,
+                                                                                                        EncodingStats.NO_STATS);
+
+            try (SequentialWriter out = new SequentialWriter(statsFile))
+            {
+                desc.getMetadataSerializer().serialize(Collections.singletonMap(MetadataType.HEADER, header), out, version);
+                out.finish();
+            }
+
+            return new File(desc.filenameFor(Component.DATA));
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private AbstractType<?> freezeUdt(AbstractType<?> type)
+    {
+        if (type instanceof CollectionType)
+        {
+            if (type.getClass() == ListType.class)
+            {
+                ListType<?> cHeader = (ListType<?>) type;
+                return ListType.getInstance(freezeUdt(cHeader.getElementsType()), cHeader.isMultiCell());
+            }
+            else if (type.getClass() == SetType.class)
+            {
+                SetType<?> cHeader = (SetType<?>) type;
+                return SetType.getInstance(freezeUdt(cHeader.getElementsType()), cHeader.isMultiCell());
+            }
+            else if (type.getClass() == MapType.class)
+            {
+                MapType<?, ?> cHeader = (MapType<?, ?>) type;
+                return MapType.getInstance(freezeUdt(cHeader.getKeysType()), freezeUdt(cHeader.getValuesType()), cHeader.isMultiCell());
+            }
+        }
+        else if (type instanceof AbstractCompositeType)
+        {
+            if (type.getClass() == CompositeType.class)
+            {
+                CompositeType cHeader = (CompositeType) type;
+                return CompositeType.getInstance(cHeader.types.stream().map(this::freezeUdt).collect(Collectors.toList()));
+            }
+        }
+        else if (type instanceof TupleType)
+        {
+            if (type.getClass() == UserType.class)
+            {
+                UserType cHeader = (UserType) type;
+                cHeader = cHeader.freeze();
+                return new UserType(cHeader.keyspace, cHeader.name, cHeader.fieldNames(),
+                                    cHeader.allTypes().stream().map(this::freezeUdt).collect(Collectors.toList()),
+                                    cHeader.isMultiCell());
+            }
+        }
+        return type;
+    }
+
+    private void assertFrozenUdt(SerializationHeader.Component header, boolean frozen, boolean checkInner)
+    {
+        AbstractType<?> keyType = header.getKeyType();
+        if (keyType instanceof CompositeType)
+        {
+            for (AbstractType<?> component : ((CompositeType) keyType).types)
+                assertFrozenUdt("partition-key-component", component, frozen, checkInner);
+        }
+        assertFrozenUdt("partition-key", keyType, frozen, checkInner);
+
+        for (AbstractType<?> type : header.getClusteringTypes())
+            assertFrozenUdt("clustering-part", type, frozen, checkInner);
+        for (Map.Entry<ByteBuffer, AbstractType<?>> col : header.getStaticColumns().entrySet())
+            assertFrozenUdt(UTF8Type.instance.compose(col.getKey()), col.getValue(), frozen, checkInner);
+        for (Map.Entry<ByteBuffer, AbstractType<?>> col : header.getRegularColumns().entrySet())
+            assertFrozenUdt(UTF8Type.instance.compose(col.getKey()), col.getValue(), frozen, checkInner);
+    }
+
+    private void assertFrozenUdt(String name, AbstractType<?> type, boolean frozen, boolean checkInner)
+    {
+        if (type instanceof CompositeType)
+        {
+            if (checkInner)
+                for (AbstractType<?> component : ((CompositeType) type).types)
+                    assertFrozenUdt(name, component, frozen, true);
+        }
+        else if (type instanceof CollectionType)
+        {
+            if (checkInner)
+            {
+                if (type instanceof MapType)
+                {
+                    MapType map = (MapType) type;
+                    // only descend for non-frozen types (checking frozen in frozen is just stupid)
+                    if (map.isMultiCell())
+                    {
+                        assertFrozenUdt(name + "<map-key>", map.getKeysType(), frozen, true);
+                        assertFrozenUdt(name + "<map-value>", map.getValuesType(), frozen, true);
+                    }
+                }
+                else if (type instanceof SetType)
+                {
+                    SetType set = (SetType) type;
+                    // only descend for non-frozen types (checking frozen in frozen is just stupid)
+                    if (set.isMultiCell())
+                        assertFrozenUdt(name + "<set>", set.getElementsType(), frozen, true);
+                }
+                else if (type instanceof ListType)
+                {
+                    ListType list = (ListType) type;
+                    // only descend for non-frozen types (checking frozen in frozen is just stupid)
+                    if (list.isMultiCell())
+                        assertFrozenUdt(name + "<list>", list.getElementsType(), frozen, true);
+                }
+            }
+        }
+        else if (type instanceof TupleType)
+        {
+            if (checkInner)
+            {
+                TupleType tuple = (TupleType) type;
+                // only descend for non-frozen types (checking frozen in frozen is just stupid)
+                if (tuple.isMultiCell())
+                    for (AbstractType<?> component : tuple.allTypes())
+                        assertFrozenUdt(name + "<tuple>", component, frozen, true);
+            }
+        }
+
+        if (type instanceof UserType)
+        {
+            String typeString = type.toString();
+            assertEquals(name + ": " + typeString, frozen, !type.isMultiCell());
+            if (typeString.startsWith(UserType.class.getName() + '('))
+                if (frozen)
+                    fail(name + ": " + typeString);
+            if (typeString.startsWith(FrozenType.class.getName() + '(' + UserType.class.getName() + '('))
+                if (!frozen)
+                    fail(name + ": " + typeString);
+        }
+    }
+
+    private SerializationHeader.Component readHeader(File sstable) throws Exception
+    {
+        Descriptor desc = Descriptor.fromFilename(sstable.getParentFile(), sstable.getName()).left;
+        return (SerializationHeader.Component) desc.getMetadataSerializer().deserialize(desc, MetadataType.HEADER);
+    }
+
+    private static final Component[] requiredComponents = new Component[]{ Component.DATA, Component.FILTER, Component.PRIMARY_INDEX, Component.TOC };
+}
diff --git a/test/unit/org/apache/cassandra/io/sstable/SSTableLoaderTest.java b/test/unit/org/apache/cassandra/io/sstable/SSTableLoaderTest.java
index ad7523d..72c7467 100644
--- a/test/unit/org/apache/cassandra/io/sstable/SSTableLoaderTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/SSTableLoaderTest.java
@@ -142,7 +142,7 @@
 
         assertEquals(1, partitions.size());
         assertEquals("key1", AsciiType.instance.getString(partitions.get(0).partitionKey().getKey()));
-        assertEquals(ByteBufferUtil.bytes("100"), partitions.get(0).getRow(new Clustering(ByteBufferUtil.bytes("col1")))
+        assertEquals(ByteBufferUtil.bytes("100"), partitions.get(0).getRow(Clustering.make(ByteBufferUtil.bytes("col1")))
                                                                    .getCell(cfmeta.getColumnDefinition(ByteBufferUtil.bytes("val")))
                                                                    .value());
 
diff --git a/test/unit/org/apache/cassandra/io/sstable/SSTableReaderTest.java b/test/unit/org/apache/cassandra/io/sstable/SSTableReaderTest.java
index 66155e5..d225801 100644
--- a/test/unit/org/apache/cassandra/io/sstable/SSTableReaderTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/SSTableReaderTest.java
@@ -34,7 +34,6 @@
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
-import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.Operator;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.compaction.CompactionManager;
@@ -50,7 +49,6 @@
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.util.FileDataInput;
 import org.apache.cassandra.io.util.MmappedRegions;
-import org.apache.cassandra.io.util.SegmentedFile;
 import org.apache.cassandra.schema.CachingParams;
 import org.apache.cassandra.schema.KeyspaceParams;
 import org.apache.cassandra.service.CacheService;
@@ -539,13 +537,7 @@
             SSTableReader sstable = indexCfs.getLiveSSTables().iterator().next();
             assert sstable.first.getToken() instanceof LocalToken;
 
-            try (SegmentedFile.Builder ibuilder = SegmentedFile.getBuilder(DatabaseDescriptor.getIndexAccessMode(),
-                                                                           false);
-                 SegmentedFile.Builder dbuilder = SegmentedFile.getBuilder(DatabaseDescriptor.getDiskAccessMode(),
-                                                                           sstable.compression))
-            {
-                sstable.saveSummary(ibuilder, dbuilder);
-            }
+            sstable.saveSummary();
             SSTableReader reopened = SSTableReader.open(sstable.descriptor);
             assert reopened.first.getToken() instanceof LocalToken;
             reopened.selfRef().release();
@@ -751,9 +743,9 @@
                                              .build();
         Index.Searcher searcher = rc.getIndex(indexedCFS).searcherFor(rc);
         assertNotNull(searcher);
-        try (ReadOrderGroup orderGroup = ReadOrderGroup.forCommand(rc))
+        try (ReadExecutionController executionController = rc.executionController())
         {
-            assertEquals(1, Util.size(UnfilteredPartitionIterators.filter(searcher.search(orderGroup), rc.nowInSec())));
+            assertEquals(1, Util.size(UnfilteredPartitionIterators.filter(searcher.search(executionController), rc.nowInSec())));
         }
     }
 
diff --git a/test/unit/org/apache/cassandra/io/sstable/SSTableRewriterTest.java b/test/unit/org/apache/cassandra/io/sstable/SSTableRewriterTest.java
index bdc3b42..d1b4092 100644
--- a/test/unit/org/apache/cassandra/io/sstable/SSTableRewriterTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/SSTableRewriterTest.java
@@ -33,6 +33,7 @@
 
 import org.apache.cassandra.Util;
 import org.apache.cassandra.UpdateBuilder;
+import org.apache.cassandra.concurrent.NamedThreadFactory;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.DecoratedKey;
@@ -87,7 +88,7 @@
         int nowInSec = FBUtilities.nowInSeconds();
         try (AbstractCompactionStrategy.ScannerList scanners = cfs.getCompactionStrategyManager().getScanners(sstables);
              LifecycleTransaction txn = cfs.getTracker().tryModify(sstables, OperationType.UNKNOWN);
-             SSTableRewriter writer = new SSTableRewriter(txn, 1000, false);
+             SSTableRewriter writer = SSTableRewriter.constructKeepingOriginals(txn, false, 1000);
              CompactionController controller = new CompactionController(cfs, sstables, cfs.gcBefore(nowInSec));
              CompactionIterator ci = new CompactionIterator(OperationType.COMPACTION, scanners.scanners, controller, nowInSec, UUIDGen.getTimeUUID()))
         {
@@ -119,7 +120,7 @@
         int nowInSec = FBUtilities.nowInSeconds();
         try (AbstractCompactionStrategy.ScannerList scanners = cfs.getCompactionStrategyManager().getScanners(sstables);
              LifecycleTransaction txn = cfs.getTracker().tryModify(sstables, OperationType.UNKNOWN);
-             SSTableRewriter writer = new SSTableRewriter(txn, 1000, false, 10000000, false);
+             SSTableRewriter writer = new SSTableRewriter(txn, 1000, 10000000, false);
              CompactionController controller = new CompactionController(cfs, sstables, cfs.gcBefore(nowInSec));
              CompactionIterator ci = new CompactionIterator(OperationType.COMPACTION, scanners.scanners, controller, nowInSec, UUIDGen.getTimeUUID()))
         {
@@ -152,7 +153,7 @@
         boolean checked = false;
         try (AbstractCompactionStrategy.ScannerList scanners = cfs.getCompactionStrategyManager().getScanners(sstables);
              LifecycleTransaction txn = cfs.getTracker().tryModify(sstables, OperationType.UNKNOWN);
-             SSTableRewriter writer = new SSTableRewriter(txn, 1000, false, 10000000, false);
+             SSTableRewriter writer = new SSTableRewriter(txn, 1000, 10000000, false);
              CompactionController controller = new CompactionController(cfs, sstables, cfs.gcBefore(nowInSec));
              CompactionIterator ci = new CompactionIterator(OperationType.COMPACTION, scanners.scanners, controller, nowInSec, UUIDGen.getTimeUUID()))
         {
@@ -210,7 +211,7 @@
         try (ISSTableScanner scanner = s.getScanner();
              CompactionController controller = new CompactionController(cfs, compacting, 0);
              LifecycleTransaction txn = cfs.getTracker().tryModify(compacting, OperationType.UNKNOWN);
-             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, false, 10000000, false);
+             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, 10000000, false);
              CompactionIterator ci = new CompactionIterator(OperationType.COMPACTION, Collections.singletonList(scanner), controller, FBUtilities.nowInSeconds(), UUIDGen.getTimeUUID()))
         {
             rewriter.switchWriter(getWriter(cfs, s.descriptor.directory, txn));
@@ -265,7 +266,7 @@
         try (ISSTableScanner scanner = s.getScanner();
              CompactionController controller = new CompactionController(cfs, compacting, 0);
              LifecycleTransaction txn = cfs.getTracker().tryModify(compacting, OperationType.UNKNOWN);
-             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, false, 10000000, false);
+             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, 10000000, false);
              CompactionIterator ci = new CompactionIterator(OperationType.COMPACTION, Collections.singletonList(scanner), controller, FBUtilities.nowInSeconds(), UUIDGen.getTimeUUID()))
         {
             rewriter.switchWriter(getWriter(cfs, s.descriptor.directory, txn));
@@ -416,7 +417,7 @@
         try (ISSTableScanner scanner = s.getScanner();
              CompactionController controller = new CompactionController(cfs, compacting, 0);
              LifecycleTransaction txn = cfs.getTracker().tryModify(compacting, OperationType.UNKNOWN);
-             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, false, 10000000, false))
+             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, 10000000, false))
         {
             rewriter.switchWriter(getWriter(cfs, s.descriptor.directory, txn));
             test.run(scanner, controller, s, cfs, rewriter, txn);
@@ -448,7 +449,7 @@
         try (ISSTableScanner scanner = s.getScanner();
              CompactionController controller = new CompactionController(cfs, compacting, 0);
              LifecycleTransaction txn = cfs.getTracker().tryModify(compacting, OperationType.UNKNOWN);
-             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, false, 10000000, false);
+             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, 10000000, false);
              CompactionIterator ci = new CompactionIterator(OperationType.COMPACTION, Collections.singletonList(scanner), controller, FBUtilities.nowInSeconds(), UUIDGen.getTimeUUID()))
         {
             rewriter.switchWriter(getWriter(cfs, s.descriptor.directory, txn));
@@ -494,7 +495,7 @@
         try (ISSTableScanner scanner = s.getScanner();
              CompactionController controller = new CompactionController(cfs, compacting, 0);
              LifecycleTransaction txn = cfs.getTracker().tryModify(compacting, OperationType.UNKNOWN);
-             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, false, 10000000, false);
+             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, 10000000, false);
              CompactionIterator ci = new CompactionIterator(OperationType.COMPACTION, Collections.singletonList(scanner), controller, FBUtilities.nowInSeconds(), UUIDGen.getTimeUUID()))
         {
             rewriter.switchWriter(getWriter(cfs, s.descriptor.directory, txn));
@@ -534,7 +535,7 @@
         try (ISSTableScanner scanner = s.getScanner();
              CompactionController controller = new CompactionController(cfs, compacting, 0);
              LifecycleTransaction txn = cfs.getTracker().tryModify(compacting, OperationType.UNKNOWN);
-             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, false, 1000000, false);
+             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, 1000000, false);
              CompactionIterator ci = new CompactionIterator(OperationType.COMPACTION, Collections.singletonList(scanner), controller, FBUtilities.nowInSeconds(), UUIDGen.getTimeUUID()))
         {
             rewriter.switchWriter(getWriter(cfs, s.descriptor.directory, txn));
@@ -620,7 +621,7 @@
              CompactionController controller = new CompactionController(cfs, compacting, 0);
              LifecycleTransaction txn = offline ? LifecycleTransaction.offline(OperationType.UNKNOWN, compacting)
                                        : cfs.getTracker().tryModify(compacting, OperationType.UNKNOWN);
-             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, offline, 10000000, false);
+             SSTableRewriter rewriter = new SSTableRewriter(txn, 100, 10000000, false);
              CompactionIterator ci = new CompactionIterator(OperationType.COMPACTION, Collections.singletonList(scanner), controller, FBUtilities.nowInSeconds(), UUIDGen.getTimeUUID())
         )
         {
@@ -710,7 +711,7 @@
         try (ISSTableScanner scanner = compacting.iterator().next().getScanner();
              CompactionController controller = new CompactionController(cfs, compacting, 0);
              LifecycleTransaction txn = cfs.getTracker().tryModify(compacting, OperationType.UNKNOWN);
-             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, false, 1, false);
+             SSTableRewriter rewriter = new SSTableRewriter(txn, 1000, 1, false);
              CompactionIterator ci = new CompactionIterator(OperationType.COMPACTION, Collections.singletonList(scanner), controller, FBUtilities.nowInSeconds(), UUIDGen.getTimeUUID())
         )
         {
@@ -748,7 +749,7 @@
         try (ISSTableScanner scanner = sstables.iterator().next().getScanner();
              CompactionController controller = new CompactionController(cfs, sstables, 0);
              LifecycleTransaction txn = cfs.getTracker().tryModify(sstables, OperationType.UNKNOWN);
-             SSTableRewriter writer = new SSTableRewriter(txn, 1000, false, 10000000, false);
+             SSTableRewriter writer = new SSTableRewriter(txn, 1000, 10000000, false);
              CompactionIterator ci = new CompactionIterator(OperationType.COMPACTION, Collections.singletonList(scanner), controller, FBUtilities.nowInSeconds(), UUIDGen.getTimeUUID())
         )
         {
@@ -813,7 +814,7 @@
                 }
             }
         };
-        Thread t = new Thread(r);
+        Thread t = NamedThreadFactory.createThread(r);
         try
         {
             t.start();
@@ -850,8 +851,8 @@
         int nowInSec = FBUtilities.nowInSeconds();
         try (AbstractCompactionStrategy.ScannerList scanners = cfs.getCompactionStrategyManager().getScanners(sstables);
              LifecycleTransaction txn = cfs.getTracker().tryModify(sstables, OperationType.UNKNOWN);
-             SSTableRewriter writer = new SSTableRewriter(txn, 1000, false, false);
-             SSTableRewriter writer2 = new SSTableRewriter(txn, 1000, false, false);
+             SSTableRewriter writer = SSTableRewriter.constructWithoutEarlyOpening(txn, false, 1000);
+             SSTableRewriter writer2 = SSTableRewriter.constructWithoutEarlyOpening(txn, false, 1000);
              CompactionController controller = new CompactionController(cfs, sstables, cfs.gcBefore(nowInSec));
              CompactionIterator ci = new CompactionIterator(OperationType.COMPACTION, scanners.scanners, controller, nowInSec, UUIDGen.getTimeUUID())
              )
@@ -895,7 +896,7 @@
                 }
             }
         };
-        Thread t = new Thread(r);
+        Thread t = NamedThreadFactory.createThread(r);
         try
         {
             t.start();
diff --git a/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java b/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
index cf57b17..d1db09a 100644
--- a/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
@@ -183,10 +183,7 @@
         assert boundaries.length % 2 == 0;
         for (DataRange range : dataRanges(sstable.metadata, scanStart, scanEnd))
         {
-            try(ISSTableScanner scanner = sstable.getScanner(ColumnFilter.all(sstable.metadata),
-                                                             range,
-                                                             false,
-                                                             SSTableReadsListener.NOOP_LISTENER))
+            try(ISSTableScanner scanner = sstable.getScanner(ColumnFilter.all(sstable.metadata), range, false, SSTableReadsListener.NOOP_LISTENER))
             {
                 for (int b = 0; b < boundaries.length; b += 2)
                     for (int i = boundaries[b]; i <= boundaries[b + 1]; i++)
diff --git a/test/unit/org/apache/cassandra/io/sstable/SSTableUtils.java b/test/unit/org/apache/cassandra/io/sstable/SSTableUtils.java
index 5c7ff02..df9d1aa 100644
--- a/test/unit/org/apache/cassandra/io/sstable/SSTableUtils.java
+++ b/test/unit/org/apache/cassandra/io/sstable/SSTableUtils.java
@@ -28,6 +28,7 @@
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.partitions.*;
+import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.service.ActiveRepairService;
 
@@ -76,7 +77,7 @@
         File cfDir = new File(tempdir, keyspaceName + File.separator + cfname);
         cfDir.mkdirs();
         cfDir.deleteOnExit();
-        File datafile = new File(new Descriptor(cfDir, keyspaceName, cfname, generation).filenameFor(Component.DATA));
+        File datafile = new File(new Descriptor(cfDir, keyspaceName, cfname, generation, SSTableFormat.Type.BIG).filenameFor(Component.DATA));
         if (!datafile.createNewFile())
             throw new IOException("unable to create file " + datafile);
         datafile.deleteOnExit();
diff --git a/test/unit/org/apache/cassandra/io/sstable/SSTableWriterTest.java b/test/unit/org/apache/cassandra/io/sstable/SSTableWriterTest.java
index e714c60..391927c 100644
--- a/test/unit/org/apache/cassandra/io/sstable/SSTableWriterTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/SSTableWriterTest.java
@@ -80,7 +80,7 @@
 
             // These checks don't work on Windows because the writer has the channel still
             // open till .abort() is called (via the builder)
-            if (!FBUtilities.isWindows())
+            if (!FBUtilities.isWindows)
             {
                 LifecycleTransaction.waitForDeletions();
                 assertFileCounts(dir.list());
@@ -129,7 +129,7 @@
             sstable.selfRef().release();
             // These checks don't work on Windows because the writer has the channel still
             // open till .abort() is called (via the builder)
-            if (!FBUtilities.isWindows())
+            if (!FBUtilities.isWindows)
             {
                 LifecycleTransaction.waitForDeletions();
                 assertFileCounts(dir.list());
@@ -183,7 +183,7 @@
 
             // These checks don't work on Windows because the writer has the channel still
             // open till .abort() is called (via the builder)
-            if (!FBUtilities.isWindows())
+            if (!FBUtilities.isWindows)
             {
                 LifecycleTransaction.waitForDeletions();
                 assertFileCounts(dir.list());
@@ -225,6 +225,7 @@
             {
                 DecoratedKey dk = Util.dk("large_value");
                 UnfilteredRowIterator rowIter = sstable.iterator(dk,
+                                                                 Slices.ALL,
                                                                  ColumnFilter.all(cfs.metadata),
                                                                  false,
                                                                  false,
diff --git a/test/unit/org/apache/cassandra/io/sstable/SSTableWriterTestBase.java b/test/unit/org/apache/cassandra/io/sstable/SSTableWriterTestBase.java
index 26b1134..a123a22 100644
--- a/test/unit/org/apache/cassandra/io/sstable/SSTableWriterTestBase.java
+++ b/test/unit/org/apache/cassandra/io/sstable/SSTableWriterTestBase.java
@@ -31,7 +31,6 @@
 import org.junit.BeforeClass;
 
 import org.apache.cassandra.SchemaLoader;
-import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ColumnFamilyStore;
@@ -65,7 +64,9 @@
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
-        if (FBUtilities.isWindows())
+        DatabaseDescriptor.daemonInitialization();
+
+        if (FBUtilities.isWindows)
         {
             standardMode = DatabaseDescriptor.getDiskAccessMode();
             indexMode = DatabaseDescriptor.getIndexAccessMode();
@@ -163,7 +164,7 @@
     public static SSTableWriter getWriter(ColumnFamilyStore cfs, File directory, LifecycleTransaction txn)
     {
         String filename = cfs.getSSTablePath(directory);
-        return SSTableWriter.create(filename, 0, 0, new SerializationHeader(true, cfs.metadata, cfs.metadata.partitionColumns(), EncodingStats.NO_STATS), txn);
+        return SSTableWriter.create(filename, 0, 0, new SerializationHeader(true, cfs.metadata, cfs.metadata.partitionColumns(), EncodingStats.NO_STATS), cfs.indexManager.listIndexes(), txn);
     }
 
     public static ByteBuffer random(int i, int size)
diff --git a/test/unit/org/apache/cassandra/io/sstable/format/ClientModeSSTableTest.java b/test/unit/org/apache/cassandra/io/sstable/format/ClientModeSSTableTest.java
index 48a8af5..90522d6 100644
--- a/test/unit/org/apache/cassandra/io/sstable/format/ClientModeSSTableTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/format/ClientModeSSTableTest.java
@@ -28,16 +28,14 @@
 
 import org.apache.cassandra.concurrent.ScheduledExecutors;
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.Slices;
 import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.marshal.BytesType;
-import org.apache.cassandra.db.rows.SliceableUnfilteredRowIterator;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
 import org.apache.cassandra.dht.ByteOrderedPartitioner;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.sstable.Descriptor;
-import org.apache.cassandra.io.sstable.format.SSTableFormat;
-import org.apache.cassandra.io.sstable.format.SSTableReader;
-import org.apache.cassandra.io.sstable.format.Version;
 
 /**
  * Tests backwards compatibility for SSTables
@@ -55,7 +53,8 @@
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
-        Config.setClientMode(true);
+        DatabaseDescriptor.clientInitialization();
+        DatabaseDescriptor.applyAddressConfig();
 
         metadata = CFMetaData.Builder.createDense(KSNAME, CFNAME, false, false)
                                                 .addPartitionKey("key", BytesType.instance)
@@ -107,11 +106,12 @@
 
             ByteBuffer key = bytes(Integer.toString(100));
 
-            try (SliceableUnfilteredRowIterator iter = reader.iterator(metadata.decorateKey(key),
-                                                                       ColumnFilter.selection(metadata.partitionColumns()),
-                                                                       false,
-                                                                       false,
-                                                                       SSTableReadsListener.NOOP_LISTENER))
+            try (UnfilteredRowIterator iter = reader.iterator(metadata.decorateKey(key),
+                                                              Slices.ALL,
+                                                              ColumnFilter.selection(metadata.partitionColumns()),
+                                                              false,
+                                                              false,
+                                                              SSTableReadsListener.NOOP_LISTENER))
             {
                 assert iter.next().clustering().get(0).equals(key);
             }
diff --git a/test/unit/org/apache/cassandra/io/sstable/format/SSTableFlushObserverTest.java b/test/unit/org/apache/cassandra/io/sstable/format/SSTableFlushObserverTest.java
new file mode 100644
index 0000000..f4c2f46
--- /dev/null
+++ b/test/unit/org/apache/cassandra/io/sstable/format/SSTableFlushObserverTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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.io.sstable.format;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.DeletionTime;
+import org.apache.cassandra.db.SerializationHeader;
+import org.apache.cassandra.db.compaction.OperationType;
+import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.io.FSReadError;
+import org.apache.cassandra.io.FSWriteError;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.format.big.BigTableWriter;
+import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
+import org.apache.cassandra.io.util.FileDataInput;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.Pair;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+
+import junit.framework.Assert;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class SSTableFlushObserverTest
+{
+    @BeforeClass
+    public static void initDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
+    private static final String KS_NAME = "test";
+    private static final String CF_NAME = "flush_observer";
+
+    @Test
+    public void testFlushObserver()
+    {
+        CFMetaData cfm = CFMetaData.Builder.create(KS_NAME, CF_NAME)
+                                           .addPartitionKey("id", UTF8Type.instance)
+                                           .addRegularColumn("first_name", UTF8Type.instance)
+                                           .addRegularColumn("age", Int32Type.instance)
+                                           .addRegularColumn("height", LongType.instance)
+                                           .build();
+
+        LifecycleTransaction transaction = LifecycleTransaction.offline(OperationType.COMPACTION);
+        FlushObserver observer = new FlushObserver();
+
+        String sstableDirectory = DatabaseDescriptor.getAllDataFileLocations()[0];
+        File directory = new File(sstableDirectory + File.pathSeparator + KS_NAME + File.pathSeparator + CF_NAME);
+        directory.deleteOnExit();
+
+        if (!directory.exists() && !directory.mkdirs())
+            throw new FSWriteError(new IOException("failed to create tmp directory"), directory.getAbsolutePath());
+
+        SSTableFormat.Type sstableFormat = SSTableFormat.Type.current();
+
+        BigTableWriter writer = new BigTableWriter(new Descriptor(sstableFormat.info.getLatestVersion().version,
+                                                                  directory,
+                                                                  KS_NAME, CF_NAME,
+                                                                  0,
+                                                                  sstableFormat),
+                                                   10L, 0L, cfm,
+                                                   new MetadataCollector(cfm.comparator).sstableLevel(0),
+                                                   new SerializationHeader(true, cfm, cfm.partitionColumns(), EncodingStats.NO_STATS),
+                                                   Collections.singletonList(observer),
+                                                   transaction);
+
+        SSTableReader reader = null;
+        Multimap<ByteBuffer, Cell> expected = ArrayListMultimap.create();
+
+        try
+        {
+            final long now = System.currentTimeMillis();
+
+            ByteBuffer key = UTF8Type.instance.fromString("key1");
+            expected.putAll(key, Arrays.asList(BufferCell.live(getColumn(cfm, "age"), now, Int32Type.instance.decompose(27)),
+                                               BufferCell.live(getColumn(cfm, "first_name"), now,UTF8Type.instance.fromString("jack")),
+                                               BufferCell.live(getColumn(cfm, "height"), now, LongType.instance.decompose(183L))));
+
+            writer.append(new RowIterator(cfm, key.duplicate(), Collections.singletonList(buildRow(expected.get(key)))));
+
+            key = UTF8Type.instance.fromString("key2");
+            expected.putAll(key, Arrays.asList(BufferCell.live(getColumn(cfm, "age"), now, Int32Type.instance.decompose(30)),
+                                               BufferCell.live(getColumn(cfm, "first_name"), now,UTF8Type.instance.fromString("jim")),
+                                               BufferCell.live(getColumn(cfm, "height"), now, LongType.instance.decompose(180L))));
+
+            writer.append(new RowIterator(cfm, key, Collections.singletonList(buildRow(expected.get(key)))));
+
+            key = UTF8Type.instance.fromString("key3");
+            expected.putAll(key, Arrays.asList(BufferCell.live(getColumn(cfm, "age"), now, Int32Type.instance.decompose(30)),
+                                               BufferCell.live(getColumn(cfm, "first_name"), now,UTF8Type.instance.fromString("ken")),
+                                               BufferCell.live(getColumn(cfm, "height"), now, LongType.instance.decompose(178L))));
+
+            writer.append(new RowIterator(cfm, key, Collections.singletonList(buildRow(expected.get(key)))));
+
+            reader = writer.finish(true);
+        }
+        finally
+        {
+            FileUtils.closeQuietly(writer);
+        }
+
+        Assert.assertTrue(observer.isComplete);
+        Assert.assertEquals(expected.size(), observer.rows.size());
+
+        for (Pair<ByteBuffer, Long> e : observer.rows.keySet())
+        {
+            ByteBuffer key = e.left;
+            Long indexPosition = e.right;
+
+            try (FileDataInput index = reader.ifile.createReader(indexPosition))
+            {
+                ByteBuffer indexKey = ByteBufferUtil.readWithShortLength(index);
+                Assert.assertEquals(0, UTF8Type.instance.compare(key, indexKey));
+            }
+            catch (IOException ex)
+            {
+                throw new FSReadError(ex, reader.getIndexFilename());
+            }
+
+            Assert.assertEquals(expected.get(key), observer.rows.get(e));
+        }
+    }
+
+    private static class RowIterator extends AbstractUnfilteredRowIterator
+    {
+        private final Iterator<Unfiltered> rows;
+
+        public RowIterator(CFMetaData cfm, ByteBuffer key, Collection<Unfiltered> content)
+        {
+            super(cfm,
+                  DatabaseDescriptor.getPartitioner().decorateKey(key),
+                  DeletionTime.LIVE,
+                  cfm.partitionColumns(),
+                  BTreeRow.emptyRow(Clustering.STATIC_CLUSTERING),
+                  false,
+                  EncodingStats.NO_STATS);
+
+            rows = content.iterator();
+        }
+
+        @Override
+        protected Unfiltered computeNext()
+        {
+            return rows.hasNext() ? rows.next() : endOfData();
+        }
+    }
+
+    private static class FlushObserver implements SSTableFlushObserver
+    {
+        private final Multimap<Pair<ByteBuffer, Long>, Cell> rows = ArrayListMultimap.create();
+        private Pair<ByteBuffer, Long> currentKey;
+        private boolean isComplete;
+
+        @Override
+        public void begin()
+        {}
+
+        @Override
+        public void startPartition(DecoratedKey key, long indexPosition)
+        {
+            currentKey = Pair.create(key.getKey(), indexPosition);
+        }
+
+        @Override
+        public void nextUnfilteredCluster(Unfiltered row)
+        {
+            if (row.isRow())
+                ((Row) row).forEach((c) -> rows.put(currentKey, (Cell) c));
+        }
+
+        @Override
+        public void complete()
+        {
+            isComplete = true;
+        }
+    }
+
+    private static Row buildRow(Collection<Cell> cells)
+    {
+        Row.Builder rowBuilder = BTreeRow.sortedBuilder();
+        rowBuilder.newRow(Clustering.EMPTY);
+        cells.forEach(rowBuilder::addCell);
+        return rowBuilder.build();
+    }
+
+    private static ColumnDefinition getColumn(CFMetaData cfm, String name)
+    {
+        return cfm.getColumnDefinition(UTF8Type.instance.fromString(name));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/io/sstable/metadata/MetadataSerializerTest.java b/test/unit/org/apache/cassandra/io/sstable/metadata/MetadataSerializerTest.java
index 6cab237..a1b25c9 100644
--- a/test/unit/org/apache/cassandra/io/sstable/metadata/MetadataSerializerTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/metadata/MetadataSerializerTest.java
@@ -19,7 +19,6 @@
 package org.apache.cassandra.io.sstable.metadata;
 
 import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.Arrays;
@@ -27,14 +26,15 @@
 import java.util.EnumSet;
 import java.util.Map;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.db.SerializationHeader;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.SerializationHeader;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
 import org.apache.cassandra.db.commitlog.IntervalSet;
-import org.apache.cassandra.db.commitlog.ReplayPosition;
 import org.apache.cassandra.dht.RandomPartitioner;
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
@@ -52,6 +52,12 @@
 
 public class MetadataSerializerTest
 {
+    @BeforeClass
+    public static void initDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testSerialization() throws IOException
     {
@@ -60,7 +66,7 @@
         MetadataSerializer serializer = new MetadataSerializer();
         File statsFile = serialize(originalMetadata, serializer, BigFormat.latestVersion);
 
-        Descriptor desc = new Descriptor( statsFile.getParentFile(), "", "", 0);
+        Descriptor desc = new Descriptor(statsFile.getParentFile(), "", "", 0, SSTableFormat.Type.BIG);
         try (RandomAccessReader in = RandomAccessReader.open(statsFile))
         {
             Map<MetadataType, MetadataComponent> deserialized = serializer.deserialize(desc, in, EnumSet.allOf(MetadataType.class));
@@ -100,7 +106,7 @@
     }
 
     public File serialize(Map<MetadataType, MetadataComponent> metadata, MetadataSerializer serializer, Version version)
-            throws IOException, FileNotFoundException
+            throws IOException
     {
         // Serialize to tmp file
         File statsFile = File.createTempFile(Component.STATS.name, null);
@@ -113,12 +119,12 @@
 
     public Map<MetadataType, MetadataComponent> constructMetadata()
     {
-        ReplayPosition club = new ReplayPosition(11L, 12);
-        ReplayPosition cllb = new ReplayPosition(9L, 12);
+        CommitLogPosition club = new CommitLogPosition(11L, 12);
+        CommitLogPosition cllb = new CommitLogPosition(9L, 12);
 
         CFMetaData cfm = SchemaLoader.standardCFMD("ks1", "cf1");
         MetadataCollector collector = new MetadataCollector(cfm.comparator)
-                                          .commitLogIntervals(new IntervalSet(cllb, club));
+                                          .commitLogIntervals(new IntervalSet<>(cllb, club));
 
         String partitioner = RandomPartitioner.class.getCanonicalName();
         double bfFpChance = 0.1;
@@ -182,7 +188,7 @@
         File statsFileLb = serialize(originalMetadata, serializer, BigFormat.instance.getVersion(newV));
         File statsFileLa = serialize(originalMetadata, serializer, BigFormat.instance.getVersion(oldV));
         // Reading both as earlier version should yield identical results.
-        Descriptor desc = new Descriptor(oldV, statsFileLb.getParentFile(), "", "", 0, DatabaseDescriptor.getSSTableFormat());
+        Descriptor desc = new Descriptor(oldV, statsFileLb.getParentFile(), "", "", 0, SSTableFormat.Type.current());
         try (RandomAccessReader inLb = RandomAccessReader.open(statsFileLb);
              RandomAccessReader inLa = RandomAccessReader.open(statsFileLa))
         {
diff --git a/test/unit/org/apache/cassandra/io/util/BufferedRandomAccessFileTest.java b/test/unit/org/apache/cassandra/io/util/BufferedRandomAccessFileTest.java
index 360d262..2386160 100644
--- a/test/unit/org/apache/cassandra/io/util/BufferedRandomAccessFileTest.java
+++ b/test/unit/org/apache/cassandra/io/util/BufferedRandomAccessFileTest.java
@@ -1,4 +1,4 @@
-/**
+/*
  * 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
@@ -18,6 +18,8 @@
  *
  */
 package org.apache.cassandra.io.util;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.SyncUtil;
 
@@ -27,22 +29,28 @@
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 
+import org.junit.BeforeClass;
+import org.junit.Test;
+
 import static org.apache.cassandra.Util.expectEOF;
 import static org.apache.cassandra.Util.expectException;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertEquals;
-
-import org.junit.Test;
+import static org.junit.Assert.assertTrue;
 
 public class BufferedRandomAccessFileTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testReadAndWrite() throws Exception
     {
         SequentialWriter w = createTempFile("braf");
-        ChannelProxy channel = new ChannelProxy(w.getPath());
 
-        // writting string of data to the file
+        // writing string of data to the file
         byte[] data = "Hello".getBytes();
         w.write(data);
         assertEquals(data.length, w.length());
@@ -51,136 +59,146 @@
         w.sync();
 
         // reading small amount of data from file, this is handled by initial buffer
-        RandomAccessReader r = RandomAccessReader.open(channel);
-
-        byte[] buffer = new byte[data.length];
-        assertEquals(data.length, r.read(buffer));
-        assertTrue(Arrays.equals(buffer, data)); // we read exactly what we wrote
-        assertEquals(r.read(), -1); // nothing more to read EOF
-        assert r.bytesRemaining() == 0 && r.isEOF();
-
-        r.close();
-
-        // writing buffer bigger than page size, which will trigger reBuffer()
-        byte[] bigData = new byte[RandomAccessReader.DEFAULT_BUFFER_SIZE + 10];
-
-        for (int i = 0; i < bigData.length; i++)
-            bigData[i] = 'd';
-
-        long initialPosition = w.position();
-        w.write(bigData); // writing data
-        assertEquals(w.position(), initialPosition + bigData.length);
-        assertEquals(w.length(), initialPosition + bigData.length); // file size should equals to last position
-
-        w.sync();
-
-        r = RandomAccessReader.open(channel); // re-opening file in read-only mode
-
-        // reading written buffer
-        r.seek(initialPosition); // back to initial (before write) position
-        data = new byte[bigData.length];
-        long sizeRead = 0;
-        for (int i = 0; i < data.length; i++)
+        try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath()))
         {
-            data[i] = (byte) r.read();
-            sizeRead++;
+            try (FileHandle fh = builder.complete();
+                 RandomAccessReader r = fh.createReader())
+            {
+
+                byte[] buffer = new byte[data.length];
+                assertEquals(data.length, r.read(buffer));
+                assertTrue(Arrays.equals(buffer, data)); // we read exactly what we wrote
+                assertEquals(r.read(), -1); // nothing more to read EOF
+                assert r.bytesRemaining() == 0 && r.isEOF();
+            }
+
+            // writing buffer bigger than page size, which will trigger reBuffer()
+            byte[] bigData = new byte[RandomAccessReader.DEFAULT_BUFFER_SIZE + 10];
+
+            for (int i = 0; i < bigData.length; i++)
+                bigData[i] = 'd';
+
+            long initialPosition = w.position();
+            w.write(bigData); // writing data
+            assertEquals(w.position(), initialPosition + bigData.length);
+            assertEquals(w.length(), initialPosition + bigData.length); // file size should equals to last position
+
+            w.sync();
+
+            // re-opening file in read-only mode
+            try (FileHandle fh = builder.complete();
+                 RandomAccessReader r = fh.createReader())
+            {
+
+                // reading written buffer
+                r.seek(initialPosition); // back to initial (before write) position
+                data = new byte[bigData.length];
+                long sizeRead = 0;
+                for (int i = 0; i < data.length; i++)
+                {
+                    data[i] = (byte) r.read();
+                    sizeRead++;
+                }
+
+                assertEquals(sizeRead, data.length); // read exactly data.length bytes
+                assertEquals(r.getFilePointer(), initialPosition + data.length);
+                assertEquals(r.length(), initialPosition + bigData.length);
+                assertTrue(Arrays.equals(bigData, data));
+                assertTrue(r.bytesRemaining() == 0 && r.isEOF()); // we are at the of the file
+
+                // test readBytes(int) method
+                r.seek(0);
+                ByteBuffer fileContent = ByteBufferUtil.read(r, (int) w.length());
+                assertEquals(fileContent.limit(), w.length());
+                assert ByteBufferUtil.string(fileContent).equals("Hello" + new String(bigData));
+
+                // read the same buffer but using readFully(int)
+                data = new byte[bigData.length];
+                r.seek(initialPosition);
+                r.readFully(data);
+                assert r.bytesRemaining() == 0 && r.isEOF(); // we should be at EOF
+                assertTrue(Arrays.equals(bigData, data));
+
+                // try to read past mark (all methods should return -1)
+                data = new byte[10];
+                assertEquals(r.read(), -1);
+                assertEquals(r.read(data), -1);
+                assertEquals(r.read(data, 0, data.length), -1);
+
+                // test read(byte[], int, int)
+                r.seek(0);
+                data = new byte[20];
+                assertEquals(15, r.read(data, 0, 15));
+                assertTrue(new String(data).contains("Hellodddddddddd"));
+                for (int i = 16; i < data.length; i++)
+                {
+                    assert data[i] == 0;
+                }
+
+                w.finish();
+            }
         }
-
-        assertEquals(sizeRead, data.length); // read exactly data.length bytes
-        assertEquals(r.getFilePointer(), initialPosition + data.length);
-        assertEquals(r.length(), initialPosition + bigData.length);
-        assertTrue(Arrays.equals(bigData, data));
-        assertTrue(r.bytesRemaining() == 0 && r.isEOF()); // we are at the of the file
-
-        // test readBytes(int) method
-        r.seek(0);
-        ByteBuffer fileContent = ByteBufferUtil.read(r, (int) w.length());
-        assertEquals(fileContent.limit(), w.length());
-        assert ByteBufferUtil.string(fileContent).equals("Hello" + new String(bigData));
-
-        // read the same buffer but using readFully(int)
-        data = new byte[bigData.length];
-        r.seek(initialPosition);
-        r.readFully(data);
-        assert r.bytesRemaining() == 0 && r.isEOF(); // we should be at EOF
-        assertTrue(Arrays.equals(bigData, data));
-
-        // try to read past mark (all methods should return -1)
-        data = new byte[10];
-        assertEquals(r.read(), -1);
-        assertEquals(r.read(data), -1);
-        assertEquals(r.read(data, 0, data.length), -1);
-
-        // test read(byte[], int, int)
-        r.seek(0);
-        data = new byte[20];
-        assertEquals(15, r.read(data, 0, 15));
-        assertTrue(new String(data).contains("Hellodddddddddd"));
-        for (int i = 16; i < data.length; i++)
-        {
-            assert data[i] == 0;
-        }
-
-        w.finish();
-        r.close();
-        channel.close();
     }
 
     @Test
     public void testReadAndWriteOnCapacity() throws IOException
     {
         File tmpFile = File.createTempFile("readtest", "bin");
-        SequentialWriter w = SequentialWriter.open(tmpFile);
-
-        // Fully write the file and sync..
-        byte[] in = generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE);
-        w.write(in);
-
-        ChannelProxy channel = new ChannelProxy(w.getPath());
-        RandomAccessReader r = RandomAccessReader.open(channel);
-
-        // Read it into a same size array.
-        byte[] out = new byte[RandomAccessReader.DEFAULT_BUFFER_SIZE];
-        r.read(out);
-
-        // Cannot read any more.
-        int negone = r.read();
-        assert negone == -1 : "We read past the end of the file, should have gotten EOF -1. Instead, " + negone;
-
-        r.close();
-        w.finish();
-        channel.close();
+        try (SequentialWriter w = new SequentialWriter(tmpFile))
+        {
+            // Fully write the file and sync..
+            byte[] in = generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE);
+            w.write(in);
+    
+            try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
+                 FileHandle fh = builder.complete();
+                 RandomAccessReader r = fh.createReader())
+            {
+                // Read it into a same size array.
+                byte[] out = new byte[RandomAccessReader.DEFAULT_BUFFER_SIZE];
+                r.read(out);
+    
+                // Cannot read any more.
+                int negone = r.read();
+                assert negone == -1 : "We read past the end of the file, should have gotten EOF -1. Instead, " + negone;
+    
+                w.finish();
+            }
+        }
     }
 
     @Test
     public void testLength() throws IOException
     {
         File tmpFile = File.createTempFile("lengthtest", "bin");
-        SequentialWriter w = SequentialWriter.open(tmpFile);
-        assertEquals(0, w.length());
-
-        // write a chunk smaller then our buffer, so will not be flushed
-        // to disk
-        byte[] lessThenBuffer = generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE / 2);
-        w.write(lessThenBuffer);
-        assertEquals(lessThenBuffer.length, w.length());
-
-        // sync the data and check length
-        w.sync();
-        assertEquals(lessThenBuffer.length, w.length());
-
-        // write more then the buffer can hold and check length
-        byte[] biggerThenBuffer = generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE * 2);
-        w.write(biggerThenBuffer);
-        assertEquals(biggerThenBuffer.length + lessThenBuffer.length, w.length());
-
-        w.finish();
-
-        // will use cachedlength
-        try (ChannelProxy channel = new ChannelProxy(tmpFile);
-            RandomAccessReader r = RandomAccessReader.open(channel))
+        try (SequentialWriter w = new SequentialWriter(tmpFile))
         {
-            assertEquals(lessThenBuffer.length + biggerThenBuffer.length, r.length());
+            assertEquals(0, w.length());
+    
+            // write a chunk smaller then our buffer, so will not be flushed
+            // to disk
+            byte[] lessThenBuffer = generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE / 2);
+            w.write(lessThenBuffer);
+            assertEquals(lessThenBuffer.length, w.length());
+    
+            // sync the data and check length
+            w.sync();
+            assertEquals(lessThenBuffer.length, w.length());
+    
+            // write more then the buffer can hold and check length
+            byte[] biggerThenBuffer = generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE * 2);
+            w.write(biggerThenBuffer);
+            assertEquals(biggerThenBuffer.length + lessThenBuffer.length, w.length());
+    
+            w.finish();
+    
+            // will use cachedlength
+            try (FileHandle.Builder builder = new FileHandle.Builder(tmpFile.getPath());
+                 FileHandle fh = builder.complete();
+                 RandomAccessReader r = fh.createReader())
+            {
+                assertEquals(lessThenBuffer.length + biggerThenBuffer.length, r.length());
+            }
         }
     }
 
@@ -199,26 +217,27 @@
         w.write(data);
         w.sync();
 
-        final ChannelProxy channel = new ChannelProxy(w.getPath());
-        final RandomAccessReader r = RandomAccessReader.open(channel);
+        try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
+             FileHandle fh = builder.complete();
+             RandomAccessReader r = fh.createReader())
+        {
 
-        ByteBuffer content = ByteBufferUtil.read(r, (int) r.length());
+            ByteBuffer content = ByteBufferUtil.read(r, (int) r.length());
 
-        // after reading whole file we should be at EOF
-        assertEquals(0, ByteBufferUtil.compare(content, data));
-        assert r.bytesRemaining() == 0 && r.isEOF();
+            // after reading whole file we should be at EOF
+            assertEquals(0, ByteBufferUtil.compare(content, data));
+            assert r.bytesRemaining() == 0 && r.isEOF();
 
-        r.seek(0);
-        content = ByteBufferUtil.read(r, 10); // reading first 10 bytes
-        assertEquals(ByteBufferUtil.compare(content, "cccccccccc".getBytes()), 0);
-        assertEquals(r.bytesRemaining(), r.length() - content.limit());
+            r.seek(0);
+            content = ByteBufferUtil.read(r, 10); // reading first 10 bytes
+            assertEquals(ByteBufferUtil.compare(content, "cccccccccc".getBytes()), 0);
+            assertEquals(r.bytesRemaining(), r.length() - content.limit());
 
-        // trying to read more than file has right now
-        expectEOF(() -> ByteBufferUtil.read(r, (int) r.length() + 10));
+            // trying to read more than file has right now
+            expectEOF(() -> ByteBufferUtil.read(r, (int) r.length() + 10));
 
-        w.finish();
-        r.close();
-        channel.close();
+            w.finish();
+        }
     }
 
     @Test
@@ -229,24 +248,29 @@
         w.write(data);
         w.finish();
 
-        final ChannelProxy channel = new ChannelProxy(w.getPath());
-        final RandomAccessReader file = RandomAccessReader.open(channel);
+        try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
+             FileHandle fh = builder.complete();
+             RandomAccessReader file = fh.createReader())
+        {
+            file.seek(0);
+            assertEquals(file.getFilePointer(), 0);
+            assertEquals(file.bytesRemaining(), file.length());
 
-        file.seek(0);
-        assertEquals(file.getFilePointer(), 0);
-        assertEquals(file.bytesRemaining(), file.length());
+            file.seek(20);
+            assertEquals(file.getFilePointer(), 20);
+            assertEquals(file.bytesRemaining(), file.length() - 20);
 
-        file.seek(20);
-        assertEquals(file.getFilePointer(), 20);
-        assertEquals(file.bytesRemaining(), file.length() - 20);
+            // trying to seek past the end of the file should produce EOFException
+            expectException(() -> {
+                file.seek(file.length() + 30);
+                return null;
+            }, IllegalArgumentException.class);
 
-        // trying to seek past the end of the file should produce EOFException
-        expectException(() -> { file.seek(file.length() + 30); return null; }, IllegalArgumentException.class);
-
-        expectException(() -> { file.seek(-1); return null; }, IllegalArgumentException.class); // throws IllegalArgumentException
-
-        file.close();
-        channel.close();
+            expectException(() -> {
+                file.seek(-1);
+                return null;
+            }, IllegalArgumentException.class); // throws IllegalArgumentException
+        }
     }
 
     @Test
@@ -256,28 +280,28 @@
         w.write(generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE * 2));
         w.finish();
 
-        ChannelProxy channel = new ChannelProxy(w.getPath());
-        RandomAccessReader file = RandomAccessReader.open(channel);
+        try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
+             FileHandle fh = builder.complete();
+             RandomAccessReader file = fh.createReader())
+        {
 
-        file.seek(0); // back to the beginning of the file
-        assertEquals(file.skipBytes(10), 10);
-        assertEquals(file.bytesRemaining(), file.length() - 10);
+            file.seek(0); // back to the beginning of the file
+            assertEquals(file.skipBytes(10), 10);
+            assertEquals(file.bytesRemaining(), file.length() - 10);
 
-        int initialPosition = (int) file.getFilePointer();
-        // can't skip more than file size
-        assertEquals(file.skipBytes((int) file.length() + 10), file.length() - initialPosition);
-        assertEquals(file.getFilePointer(), file.length());
-        assert file.bytesRemaining() == 0 && file.isEOF();
+            int initialPosition = (int) file.getFilePointer();
+            // can't skip more than file size
+            assertEquals(file.skipBytes((int) file.length() + 10), file.length() - initialPosition);
+            assertEquals(file.getFilePointer(), file.length());
+            assert file.bytesRemaining() == 0 && file.isEOF();
 
-        file.seek(0);
+            file.seek(0);
 
-        // skipping negative amount should return 0
-        assertEquals(file.skipBytes(-1000), 0);
-        assertEquals(file.getFilePointer(), 0);
-        assertEquals(file.bytesRemaining(), file.length());
-
-        file.close();
-        channel.close();
+            // skipping negative amount should return 0
+            assertEquals(file.skipBytes(-1000), 0);
+            assertEquals(file.getFilePointer(), 0);
+            assertEquals(file.bytesRemaining(), file.length());
+        }
     }
 
     @Test
@@ -292,22 +316,23 @@
 
         w.sync();
 
-        ChannelProxy channel = new ChannelProxy(w.getPath());
-        RandomAccessReader r = RandomAccessReader.open(channel);
+        try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
+             FileHandle fh = builder.complete();
+             RandomAccessReader r = fh.createReader())
+        {
 
-        // position should change after skip bytes
-        r.seek(0);
-        r.skipBytes(15);
-        assertEquals(r.getFilePointer(), 15);
+            // position should change after skip bytes
+            r.seek(0);
+            r.skipBytes(15);
+            assertEquals(r.getFilePointer(), 15);
 
-        r.read();
-        assertEquals(r.getFilePointer(), 16);
-        r.read(new byte[4]);
-        assertEquals(r.getFilePointer(), 20);
+            r.read();
+            assertEquals(r.getFilePointer(), 16);
+            r.read(new byte[4]);
+            assertEquals(r.getFilePointer(), 20);
 
-        w.finish();
-        r.close();
-        channel.close();
+            w.finish();
+        }
     }
 
     @Test
@@ -329,10 +354,9 @@
             for (final int offset : Arrays.asList(0, 8))
             {
                 File file1 = writeTemporaryFile(new byte[16]);
-                try (final ChannelProxy channel = new ChannelProxy(file1);
-                     final RandomAccessReader file = new RandomAccessReader.Builder(channel)
-                                                     .bufferSize(bufferSize)
-                                                     .build())
+                try (FileHandle.Builder builder = new FileHandle.Builder(file1.getPath()).bufferSize(bufferSize);
+                     FileHandle fh = builder.complete();
+                     RandomAccessReader file = fh.createReader())
                 {
                     expectEOF(() -> { file.readFully(target, offset, 17); return null; });
                 }
@@ -342,8 +366,9 @@
             for (final int n : Arrays.asList(1, 2, 4, 8))
             {
                 File file1 = writeTemporaryFile(new byte[16]);
-                try (final ChannelProxy channel = new ChannelProxy(file1);
-                     final RandomAccessReader file = new RandomAccessReader.Builder(channel).bufferSize(bufferSize).build())
+                try (FileHandle.Builder builder = new FileHandle.Builder(file1.getPath()).bufferSize(bufferSize);
+                     FileHandle fh = builder.complete();
+                     RandomAccessReader file = fh.createReader())
                 {
                     expectEOF(() -> {
                         while (true)
@@ -374,24 +399,25 @@
 
         w.sync();
 
-        ChannelProxy channel = new ChannelProxy(w.getPath());
-        RandomAccessReader r = RandomAccessReader.open(channel);
-
-        assertEquals(r.bytesRemaining(), toWrite);
-
-        for (int i = 1; i <= r.length(); i++)
+        try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
+             FileHandle fh = builder.complete();
+             RandomAccessReader r = fh.createReader())
         {
-            r.read();
-            assertEquals(r.bytesRemaining(), r.length() - i);
+
+            assertEquals(r.bytesRemaining(), toWrite);
+
+            for (int i = 1; i <= r.length(); i++)
+            {
+                r.read();
+                assertEquals(r.bytesRemaining(), r.length() - i);
+            }
+
+            r.seek(0);
+            r.skipBytes(10);
+            assertEquals(r.bytesRemaining(), r.length() - 10);
+
+            w.finish();
         }
-
-        r.seek(0);
-        r.skipBytes(10);
-        assertEquals(r.bytesRemaining(), r.length() - 10);
-
-        w.finish();
-        r.close();
-        channel.close();
     }
 
     @Test
@@ -401,7 +427,9 @@
         tmpFile.deleteOnExit();
 
         // Create the BRAF by filename instead of by file.
-        try (final RandomAccessReader r = RandomAccessReader.open(new File(tmpFile.getPath())))
+        try (FileHandle.Builder builder = new FileHandle.Builder(tmpFile.getPath());
+             FileHandle fh = builder.complete();
+             RandomAccessReader r = fh.createReader())
         {
             assert tmpFile.getPath().equals(r.getPath());
 
@@ -453,33 +481,32 @@
 
         w.finish();
 
-        ChannelProxy channel = new ChannelProxy(w.getPath());
-        RandomAccessReader file = RandomAccessReader.open(channel);
+        try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
+             FileHandle fh = builder.complete();
+             RandomAccessReader file = fh.createReader())
+        {
+            file.seek(10);
+            DataPosition mark = file.mark();
 
-        file.seek(10);
-        DataPosition mark = file.mark();
+            file.seek(file.length());
+            assertTrue(file.isEOF());
 
-        file.seek(file.length());
-        assertTrue(file.isEOF());
+            file.reset();
+            assertEquals(file.bytesRemaining(), 20);
 
-        file.reset();
-        assertEquals(file.bytesRemaining(), 20);
+            file.seek(file.length());
+            assertTrue(file.isEOF());
 
-        file.seek(file.length());
-        assertTrue(file.isEOF());
+            file.reset(mark);
+            assertEquals(file.bytesRemaining(), 20);
 
-        file.reset(mark);
-        assertEquals(file.bytesRemaining(), 20);
+            file.seek(file.length());
+            assertEquals(file.bytesPastMark(), 20);
+            assertEquals(file.bytesPastMark(mark), 20);
 
-        file.seek(file.length());
-        assertEquals(file.bytesPastMark(), 20);
-        assertEquals(file.bytesPastMark(mark), 20);
-
-        file.reset(mark);
-        assertEquals(file.bytesPastMark(), 0);
-
-        file.close();
-        channel.close();
+            file.reset(mark);
+            assertEquals(file.bytesPastMark(), 0);
+        }
     }
 
     @Test(expected = AssertionError.class)
@@ -490,8 +517,9 @@
             w.write(new byte[30]);
             w.flush();
 
-            try (ChannelProxy channel = new ChannelProxy(w.getPath());
-                 RandomAccessReader r = RandomAccessReader.open(channel))
+            try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
+                 FileHandle fh = builder.complete();
+                 RandomAccessReader r = fh.createReader())
             {
                 r.seek(10);
                 r.mark();
@@ -561,7 +589,7 @@
     public void testSetNegativeLength() throws IOException, IllegalArgumentException
     {
         File tmpFile = File.createTempFile("set_negative_length", "bin");
-        try (SequentialWriter file = SequentialWriter.open(tmpFile))
+        try (SequentialWriter file = new SequentialWriter(tmpFile))
         {
             file.truncate(-8L);
         }
@@ -572,7 +600,7 @@
         File tempFile = File.createTempFile(name, null);
         tempFile.deleteOnExit();
 
-        return SequentialWriter.open(tempFile);
+        return new SequentialWriter(tempFile);
     }
 
     private File writeTemporaryFile(byte[] data) throws IOException
diff --git a/test/unit/org/apache/cassandra/io/util/ChecksummedRandomAccessReaderTest.java b/test/unit/org/apache/cassandra/io/util/ChecksummedRandomAccessReaderTest.java
index 57428af..545c3e3 100644
--- a/test/unit/org/apache/cassandra/io/util/ChecksummedRandomAccessReaderTest.java
+++ b/test/unit/org/apache/cassandra/io/util/ChecksummedRandomAccessReaderTest.java
@@ -24,16 +24,21 @@
 import java.util.Arrays;
 import java.util.concurrent.ThreadLocalRandom;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import static org.junit.Assert.*;
-import org.apache.cassandra.io.util.ChecksummedRandomAccessReader;
-import org.apache.cassandra.io.util.ChecksummedSequentialWriter;
-import org.apache.cassandra.io.util.RandomAccessReader;
-import org.apache.cassandra.io.util.SequentialWriter;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
 
 public class ChecksummedRandomAccessReaderTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void readFully() throws IOException
     {
@@ -43,21 +48,23 @@
         final byte[] expected = new byte[70 * 1024];   // bit more than crc chunk size, so we can test rebuffering.
         ThreadLocalRandom.current().nextBytes(expected);
 
-        SequentialWriter writer = ChecksummedSequentialWriter.open(data, crc);
-        writer.write(expected);
-        writer.finish();
+        try (SequentialWriter writer = new ChecksummedSequentialWriter(data, crc, null, SequentialWriterOption.DEFAULT))
+        {
+            writer.write(expected);
+            writer.finish();
+        }
 
         assert data.exists();
 
-        RandomAccessReader reader = new ChecksummedRandomAccessReader.Builder(data, crc).build();
-        byte[] b = new byte[expected.length];
-        reader.readFully(b);
+        try (RandomAccessReader reader = ChecksummedRandomAccessReader.open(data, crc))
+        {
+            byte[] b = new byte[expected.length];
+            reader.readFully(b);
 
-        assertArrayEquals(expected, b);
+            assertArrayEquals(expected, b);
 
-        assertTrue(reader.isEOF());
-
-        reader.close();
+            assertTrue(reader.isEOF());
+        }
     }
 
     @Test
@@ -69,30 +76,32 @@
         final byte[] dataBytes = new byte[70 * 1024];   // bit more than crc chunk size
         ThreadLocalRandom.current().nextBytes(dataBytes);
 
-        SequentialWriter writer = ChecksummedSequentialWriter.open(data, crc);
-        writer.write(dataBytes);
-        writer.finish();
+        try (SequentialWriter writer = new ChecksummedSequentialWriter(data, crc, null, SequentialWriterOption.DEFAULT))
+        {
+            writer.write(dataBytes);
+            writer.finish();
+        }
 
         assert data.exists();
 
-        RandomAccessReader reader = new ChecksummedRandomAccessReader.Builder(data, crc).build();
+        try (RandomAccessReader reader = ChecksummedRandomAccessReader.open(data, crc))
+        {
 
-        final int seekPosition = 66000;
-        reader.seek(seekPosition);
+            final int seekPosition = 66000;
+            reader.seek(seekPosition);
 
-        byte[] b = new byte[dataBytes.length - seekPosition];
-        reader.readFully(b);
+            byte[] b = new byte[dataBytes.length - seekPosition];
+            reader.readFully(b);
 
-        byte[] expected = Arrays.copyOfRange(dataBytes, seekPosition, dataBytes.length);
+            byte[] expected = Arrays.copyOfRange(dataBytes, seekPosition, dataBytes.length);
 
-        assertArrayEquals(expected, b);
+            assertArrayEquals(expected, b);
 
-        assertTrue(reader.isEOF());
-
-        reader.close();
+            assertTrue(reader.isEOF());
+        }
     }
 
-    @Test(expected = ChecksummedRandomAccessReader.CorruptFileException.class)
+    @Test(expected = CorruptFileException.class)
     public void corruptionDetection() throws IOException
     {
         final File data = File.createTempFile("corruptionDetection", "data");
@@ -101,9 +110,11 @@
         final byte[] expected = new byte[5 * 1024];
         Arrays.fill(expected, (byte) 0);
 
-        SequentialWriter writer = ChecksummedSequentialWriter.open(data, crc);
-        writer.write(expected);
-        writer.finish();
+        try (SequentialWriter writer = new ChecksummedSequentialWriter(data, crc, null, SequentialWriterOption.DEFAULT))
+        {
+            writer.write(expected);
+            writer.finish();
+        }
 
         assert data.exists();
 
@@ -114,14 +125,14 @@
             dataFile.write((byte) 5);
         }
 
-        RandomAccessReader reader = new ChecksummedRandomAccessReader.Builder(data, crc).build();
-        byte[] b = new byte[expected.length];
-        reader.readFully(b);
+        try (RandomAccessReader reader = ChecksummedRandomAccessReader.open(data, crc))
+        {
+            byte[] b = new byte[expected.length];
+            reader.readFully(b);
 
-        assertArrayEquals(expected, b);
+            assertArrayEquals(expected, b);
 
-        assertTrue(reader.isEOF());
-
-        reader.close();
+            assertTrue(reader.isEOF());
+        }
     }
 }
diff --git a/test/unit/org/apache/cassandra/io/util/ChecksummedSequentialWriterTest.java b/test/unit/org/apache/cassandra/io/util/ChecksummedSequentialWriterTest.java
index bea3aac..29d4eea 100644
--- a/test/unit/org/apache/cassandra/io/util/ChecksummedSequentialWriterTest.java
+++ b/test/unit/org/apache/cassandra/io/util/ChecksummedSequentialWriterTest.java
@@ -24,14 +24,22 @@
 import java.util.List;
 
 import org.junit.After;
+import org.junit.BeforeClass;
 
 import junit.framework.Assert;
+import org.apache.cassandra.config.DatabaseDescriptor;
 
 public class ChecksummedSequentialWriterTest extends SequentialWriterTest
 {
 
     private final List<TestableCSW> writers = new ArrayList<>();
 
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @After
     public void cleanup()
     {
@@ -59,7 +67,9 @@
 
         private TestableCSW(File file, File crcFile) throws IOException
         {
-            this(file, crcFile, new ChecksummedSequentialWriter(file, BUFFER_SIZE, crcFile));
+            this(file, crcFile, new ChecksummedSequentialWriter(file, crcFile, null, SequentialWriterOption.newBuilder()
+                                                                                                           .bufferSize(BUFFER_SIZE)
+                                                                                                           .build()));
         }
 
         private TestableCSW(File file, File crcFile, SequentialWriter sw) throws IOException
diff --git a/test/unit/org/apache/cassandra/io/util/DataOutputTest.java b/test/unit/org/apache/cassandra/io/util/DataOutputTest.java
index 90e77d6..c8f0087 100644
--- a/test/unit/org/apache/cassandra/io/util/DataOutputTest.java
+++ b/test/unit/org/apache/cassandra/io/util/DataOutputTest.java
@@ -38,13 +38,20 @@
 import java.util.concurrent.ThreadLocalRandom;
 
 import org.junit.Assert;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
-import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class DataOutputTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testWrappedDataOutputStreamPlus() throws IOException
     {
@@ -380,8 +387,9 @@
     public void testSequentialWriter() throws IOException
     {
         File file = FileUtils.createTempFile("dataoutput", "test");
-        final SequentialWriter writer = new SequentialWriter(file, 32, BufferType.ON_HEAP);
-        DataOutputStreamPlus write = new WrappedDataOutputStreamPlus(writer.finishOnClose());
+        SequentialWriterOption option = SequentialWriterOption.newBuilder().bufferSize(32).finishOnClose(true).build();
+        final SequentialWriter writer = new SequentialWriter(file, option);
+        DataOutputStreamPlus write = new WrappedDataOutputStreamPlus(writer);
         DataInput canon = testWrite(write);
         write.flush();
         write.close();
diff --git a/test/unit/org/apache/cassandra/io/util/DiskOptimizationStrategyTest.java b/test/unit/org/apache/cassandra/io/util/DiskOptimizationStrategyTest.java
new file mode 100644
index 0000000..ed1f948
--- /dev/null
+++ b/test/unit/org/apache/cassandra/io/util/DiskOptimizationStrategyTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.io.util;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class DiskOptimizationStrategyTest
+{
+    @Test
+    public void testRoundingBufferSize()
+    {
+        DiskOptimizationStrategy strategy = new SsdDiskOptimizationStrategy(0.95);
+        assertEquals(4096, strategy.roundBufferSize(-1L));
+        assertEquals(4096, strategy.roundBufferSize(0));
+        assertEquals(4096, strategy.roundBufferSize(1));
+        assertEquals(4096, strategy.roundBufferSize(2013));
+        assertEquals(4096, strategy.roundBufferSize(4095));
+        assertEquals(4096, strategy.roundBufferSize(4096));
+        assertEquals(8192, strategy.roundBufferSize(4097));
+        assertEquals(8192, strategy.roundBufferSize(8191));
+        assertEquals(8192, strategy.roundBufferSize(8192));
+        assertEquals(12288, strategy.roundBufferSize(8193));
+        assertEquals(65536, strategy.roundBufferSize(65535));
+        assertEquals(65536, strategy.roundBufferSize(65536));
+        assertEquals(65536, strategy.roundBufferSize(65537));
+        assertEquals(65536, strategy.roundBufferSize(10000000000000000L));
+    }
+
+    @Test
+    public void testBufferSize_ssd()
+    {
+        DiskOptimizationStrategy strategy = new SsdDiskOptimizationStrategy(0.1);
+
+        assertEquals(4096, strategy.bufferSize(0));
+        assertEquals(4096, strategy.bufferSize(10));
+        assertEquals(4096, strategy.bufferSize(100));
+        assertEquals(4096, strategy.bufferSize(4096));
+        assertEquals(8192, strategy.bufferSize(4505));   // just < (4096 + 4096 * 0.1)
+        assertEquals(12288, strategy.bufferSize(4506));  // just > (4096 + 4096 * 0.1)
+
+        strategy = new SsdDiskOptimizationStrategy(0.5);
+        assertEquals(8192, strategy.bufferSize(4506));  // just > (4096 + 4096 * 0.1)
+        assertEquals(8192, strategy.bufferSize(6143));  // < (4096 + 4096 * 0.5)
+        assertEquals(12288, strategy.bufferSize(6144));  // = (4096 + 4096 * 0.5)
+        assertEquals(12288, strategy.bufferSize(6145));  // > (4096 + 4096 * 0.5)
+
+        strategy = new SsdDiskOptimizationStrategy(1.0); // never add a page
+        assertEquals(8192, strategy.bufferSize(8191));
+        assertEquals(8192, strategy.bufferSize(8192));
+
+        strategy = new SsdDiskOptimizationStrategy(0.0); // always add a page
+        assertEquals(8192, strategy.bufferSize(10));
+        assertEquals(8192, strategy.bufferSize(4096));
+    }
+
+    @Test
+    public void testBufferSize_spinning()
+    {
+        DiskOptimizationStrategy strategy = new SpinningDiskOptimizationStrategy();
+
+        assertEquals(4096, strategy.bufferSize(0));
+        assertEquals(8192, strategy.bufferSize(10));
+        assertEquals(8192, strategy.bufferSize(100));
+        assertEquals(8192, strategy.bufferSize(4096));
+        assertEquals(12288, strategy.bufferSize(4097));
+    }
+
+    @Test
+    public void testRoundUpForCaching()
+    {
+        assertEquals(4096, DiskOptimizationStrategy.roundForCaching(-1, true));
+        assertEquals(4096, DiskOptimizationStrategy.roundForCaching(0, true));
+        assertEquals(4096, DiskOptimizationStrategy.roundForCaching(1, true));
+        assertEquals(4096, DiskOptimizationStrategy.roundForCaching(4095, true));
+        assertEquals(4096, DiskOptimizationStrategy.roundForCaching(4096, true));
+        assertEquals(8192, DiskOptimizationStrategy.roundForCaching(4097, true));
+        assertEquals(8192, DiskOptimizationStrategy.roundForCaching(4098, true));
+        assertEquals(8192, DiskOptimizationStrategy.roundForCaching(8192, true));
+        assertEquals(16384, DiskOptimizationStrategy.roundForCaching(8193, true));
+        assertEquals(16384, DiskOptimizationStrategy.roundForCaching(12288, true));
+        assertEquals(16384, DiskOptimizationStrategy.roundForCaching(16384, true));
+        assertEquals(65536, DiskOptimizationStrategy.roundForCaching(65536, true));
+        assertEquals(65536, DiskOptimizationStrategy.roundForCaching(65537, true));
+        assertEquals(65536, DiskOptimizationStrategy.roundForCaching(131072, true));
+
+        for (int cs = 4096; cs < 65536; cs <<= 1) // 4096, 8192, 12288, ..., 65536
+        {
+            for (int i = (cs - 4095); i <= cs; i++) // 1 -> 4096, 4097 -> 8192, ...
+            {
+                assertEquals(cs, DiskOptimizationStrategy.roundForCaching(i, true));
+            }
+        }
+    }
+
+    @Test
+    public void testRoundDownForCaching()
+    {
+        assertEquals(4096, DiskOptimizationStrategy.roundForCaching(-1, false));
+        assertEquals(4096, DiskOptimizationStrategy.roundForCaching(0, false));
+        assertEquals(4096, DiskOptimizationStrategy.roundForCaching(1, false));
+        assertEquals(4096, DiskOptimizationStrategy.roundForCaching(4095, false));
+        assertEquals(4096, DiskOptimizationStrategy.roundForCaching(4096, false));
+        assertEquals(4096, DiskOptimizationStrategy.roundForCaching(4097, false));
+        assertEquals(4096, DiskOptimizationStrategy.roundForCaching(4098, false));
+        assertEquals(8192, DiskOptimizationStrategy.roundForCaching(8192, false));
+        assertEquals(8192, DiskOptimizationStrategy.roundForCaching(8193, false));
+        assertEquals(8192, DiskOptimizationStrategy.roundForCaching(12288, false));
+        assertEquals(16384, DiskOptimizationStrategy.roundForCaching(16384, false));
+        assertEquals(65536, DiskOptimizationStrategy.roundForCaching(65536, false));
+        assertEquals(65536, DiskOptimizationStrategy.roundForCaching(65537, false));
+        assertEquals(65536, DiskOptimizationStrategy.roundForCaching(131072, false));
+
+        for (int cs = 4096; cs < 65536; cs <<= 1) // 4096, 8192, 12288, ..., 65536
+        {
+            for (int i = cs; i < cs * 2 - 1; i++) // 4096 -> 8191, 8192 -> 12287, ...
+            {
+                assertEquals(cs, DiskOptimizationStrategy.roundForCaching(i, false));
+            }
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/io/util/FileSegmentInputStreamTest.java b/test/unit/org/apache/cassandra/io/util/FileSegmentInputStreamTest.java
index fcee9b7..b040d27 100644
--- a/test/unit/org/apache/cassandra/io/util/FileSegmentInputStreamTest.java
+++ b/test/unit/org/apache/cassandra/io/util/FileSegmentInputStreamTest.java
@@ -88,44 +88,56 @@
     @Test(expected = UnsupportedOperationException.class)
     public void testMarkNotSupported() throws Exception
     {
-        FileSegmentInputStream reader = new FileSegmentInputStream(allocateBuffer(1024), "", 0);
-        assertFalse(reader.markSupported());
-        assertEquals(0, reader.bytesPastMark(null));
-        reader.mark();
+        try (FileSegmentInputStream reader = new FileSegmentInputStream(allocateBuffer(1024), "", 0))
+        {
+            assertFalse(reader.markSupported());
+            assertEquals(0, reader.bytesPastMark(null));
+            reader.mark();
+        }
     }
 
     @Test(expected = UnsupportedOperationException.class)
     public void testResetNotSupported() throws Exception
     {
-        FileSegmentInputStream reader = new FileSegmentInputStream(allocateBuffer(1024), "", 0);
-        reader.reset(null);
+        try (FileSegmentInputStream reader = new FileSegmentInputStream(allocateBuffer(1024), "", 0))
+        {
+            reader.reset(null);
+        }
     }
 
     @Test(expected = IllegalArgumentException.class)
     public void testSeekNegative() throws Exception
     {
-        FileSegmentInputStream reader = new FileSegmentInputStream(allocateBuffer(1024), "", 0);
-        reader.seek(-1);
+        try (FileSegmentInputStream reader = new FileSegmentInputStream(allocateBuffer(1024), "", 0))
+        {
+            reader.seek(-1);
+        }
     }
 
     @Test(expected = IllegalArgumentException.class)
     public void testSeekBeforeOffset() throws Exception
     {
-        FileSegmentInputStream reader = new FileSegmentInputStream(allocateBuffer(1024), "", 1024);
-        reader.seek(1023);
+        try (FileSegmentInputStream reader = new FileSegmentInputStream(allocateBuffer(1024), "", 1024))
+        {
+            reader.seek(1023);
+        }
     }
 
     @Test(expected = IllegalArgumentException.class)
     public void testSeekPastLength() throws Exception
     {
-        FileSegmentInputStream reader = new FileSegmentInputStream(allocateBuffer(1024), "", 1024);
-        reader.seek(2049);
+        try (FileSegmentInputStream reader = new FileSegmentInputStream(allocateBuffer(1024), "", 1024))
+        {
+            reader.seek(2049);
+        }
     }
 
     @Test(expected = EOFException.class)
     public void testReadBytesTooMany() throws Exception
     {
-        FileSegmentInputStream reader = new FileSegmentInputStream(allocateBuffer(1024), "", 1024);
-        ByteBufferUtil.read(reader, 2049);
+        try (FileSegmentInputStream reader = new FileSegmentInputStream(allocateBuffer(1024), "", 1024))
+        {
+            ByteBufferUtil.read(reader, 2049);
+        }
     }
 }
diff --git a/test/unit/org/apache/cassandra/io/util/FileUtilsTest.java b/test/unit/org/apache/cassandra/io/util/FileUtilsTest.java
index 8d1b752..2f9ccd4 100644
--- a/test/unit/org/apache/cassandra/io/util/FileUtilsTest.java
+++ b/test/unit/org/apache/cassandra/io/util/FileUtilsTest.java
@@ -20,11 +20,18 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.RandomAccessFile;
 import java.nio.charset.Charset;
 import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -32,6 +39,12 @@
 public class FileUtilsTest
 {
 
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testTruncate() throws IOException
     {
@@ -54,6 +67,29 @@
     }
 
     @Test
+    public void testFolderSize() throws Exception
+    {
+        File folder = createFolder(Paths.get(DatabaseDescriptor.getAllDataFileLocations()[0], "testFolderSize"));
+        folder.deleteOnExit();
+
+        File childFolder = createFolder(Paths.get(folder.getPath(), "child"));
+
+        File[] files = {
+                       createFile(new File(folder, "001"), 10000),
+                       createFile(new File(folder, "002"), 1000),
+                       createFile(new File(folder, "003"), 100),
+                       createFile(new File(childFolder, "001"), 1000),
+                       createFile(new File(childFolder, "002"), 2000),
+        };
+
+        assertEquals(0, FileUtils.folderSize(new File(folder, "i_dont_exist")));
+        assertEquals(files[0].length(), FileUtils.folderSize(files[0]));
+
+        long size = FileUtils.folderSize(folder);
+        assertEquals(Arrays.stream(files).mapToLong(f -> f.length()).sum(), size);
+    }
+
+    @Test
     public void testIsContained()
     {
         assertTrue(FileUtils.isContained(new File("/tmp/abc"), new File("/tmp/abc")));
@@ -62,4 +98,24 @@
         assertTrue(FileUtils.isContained(new File("/tmp/abc/../abc"), new File("/tmp/abc/d")));
         assertFalse(FileUtils.isContained(new File("/tmp/abc/../abc"), new File("/tmp/abcc")));
     }
+
+    private File createFolder(Path path)
+    {
+        File folder = path.toFile();
+        FileUtils.createDirectory(folder);
+        return folder;
+    }
+
+    private File createFile(File file, long size)
+    {
+        try (RandomAccessFile f = new RandomAccessFile(file, "rw"))
+        {
+            f.setLength(size);
+        }
+        catch (Exception e)
+        {
+            System.err.println(e);
+        }
+        return file;
+    }
 }
diff --git a/test/unit/org/apache/cassandra/io/util/MmappedRegionsTest.java b/test/unit/org/apache/cassandra/io/util/MmappedRegionsTest.java
index 7cf7bd3..39c9689 100644
--- a/test/unit/org/apache/cassandra/io/util/MmappedRegionsTest.java
+++ b/test/unit/org/apache/cassandra/io/util/MmappedRegionsTest.java
@@ -24,11 +24,13 @@
 import java.util.Random;
 
 import com.google.common.primitives.Ints;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ClusteringComparator;
 import org.apache.cassandra.db.marshal.BytesType;
 import org.apache.cassandra.io.compress.CompressedSequentialWriter;
@@ -47,6 +49,12 @@
 {
     private static final Logger logger = LoggerFactory.getLogger(MmappedRegionsTest.class);
 
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     private static ByteBuffer allocateBuffer(int size)
     {
         ByteBuffer ret = ByteBuffer.allocate(Ints.checkedCast(size));
@@ -63,7 +71,7 @@
         File ret = File.createTempFile(fileName, "1");
         ret.deleteOnExit();
 
-        try (SequentialWriter writer = SequentialWriter.open(ret))
+        try (SequentialWriter writer = new SequentialWriter(ret))
         {
             writer.write(buffer);
             writer.finish();
@@ -99,8 +107,8 @@
             {
                 MmappedRegions.Region region = regions.floor(i);
                 assertNotNull(region);
-                assertEquals(0, region.bottom());
-                assertEquals(1024, region.top());
+                assertEquals(0, region.offset());
+                assertEquals(1024, region.end());
             }
 
             regions.extend(2048);
@@ -110,13 +118,13 @@
                 assertNotNull(region);
                 if (i < 1024)
                 {
-                    assertEquals(0, region.bottom());
-                    assertEquals(1024, region.top());
+                    assertEquals(0, region.offset());
+                    assertEquals(1024, region.end());
                 }
                 else
                 {
-                    assertEquals(1024, region.bottom());
-                    assertEquals(2048, region.top());
+                    assertEquals(1024, region.offset());
+                    assertEquals(2048, region.end());
                 }
             }
         }
@@ -141,8 +149,8 @@
             {
                 MmappedRegions.Region region = regions.floor(i);
                 assertNotNull(region);
-                assertEquals(SIZE * (i / SIZE), region.bottom());
-                assertEquals(SIZE + (SIZE * (i / SIZE)), region.top());
+                assertEquals(SIZE * (i / SIZE), region.offset());
+                assertEquals(SIZE + (SIZE * (i / SIZE)), region.end());
             }
         }
         finally
@@ -169,8 +177,8 @@
             {
                 MmappedRegions.Region region = regions.floor(i);
                 assertNotNull(region);
-                assertEquals(SIZE * (i / SIZE), region.bottom());
-                assertEquals(SIZE + (SIZE * (i / SIZE)), region.top());
+                assertEquals(SIZE * (i / SIZE), region.offset());
+                assertEquals(SIZE + (SIZE * (i / SIZE)), region.end());
             }
         }
         finally
@@ -209,8 +217,8 @@
         {
             MmappedRegions.Region region = snapshot.floor(i);
             assertNotNull(region);
-            assertEquals(SIZE * (i / SIZE), region.bottom());
-            assertEquals(SIZE + (SIZE * (i / SIZE)), region.top());
+            assertEquals(SIZE * (i / SIZE), region.offset());
+            assertEquals(SIZE + (SIZE * (i / SIZE)), region.end());
 
             // check we can access the buffer
             assertNotNull(region.buffer.duplicate().getInt());
@@ -267,8 +275,8 @@
             {
                 MmappedRegions.Region region = regions.floor(i);
                 assertNotNull(region);
-                assertEquals(0, region.bottom());
-                assertEquals(4096, region.top());
+                assertEquals(0, region.offset());
+                assertEquals(4096, region.end());
             }
         }
     }
@@ -298,10 +306,9 @@
         cf.deleteOnExit();
 
         MetadataCollector sstableMetadataCollector = new MetadataCollector(new ClusteringComparator(BytesType.instance));
-        try(SequentialWriter writer = new CompressedSequentialWriter(f,
-                                                                     cf.getAbsolutePath(),
-                                                                     CompressionParams.snappy(),
-                                                                     sstableMetadataCollector))
+        try(SequentialWriter writer = new CompressedSequentialWriter(f, cf.getAbsolutePath(),
+                                                                     null, SequentialWriterOption.DEFAULT,
+                                                                     CompressionParams.snappy(), sstableMetadataCollector))
         {
             writer.write(buffer);
             writer.finish();
@@ -325,8 +332,8 @@
                 assertNotNull(compressedChunk);
                 assertEquals(chunk.length + 4, compressedChunk.capacity());
 
-                assertEquals(chunk.offset, region.bottom());
-                assertEquals(chunk.offset + chunk.length + 4, region.top());
+                assertEquals(chunk.offset, region.offset());
+                assertEquals(chunk.offset + chunk.length + 4, region.end());
 
                 i += metadata.chunkLength();
             }
diff --git a/test/unit/org/apache/cassandra/io/util/RandomAccessReaderTest.java b/test/unit/org/apache/cassandra/io/util/RandomAccessReaderTest.java
index aad5117..327c656 100644
--- a/test/unit/org/apache/cassandra/io/util/RandomAccessReaderTest.java
+++ b/test/unit/org/apache/cassandra/io/util/RandomAccessReaderTest.java
@@ -36,6 +36,7 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.slf4j.Logger;
@@ -43,6 +44,7 @@
 
 import static org.junit.Assert.*;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.io.compress.BufferType;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
@@ -50,14 +52,20 @@
 {
     private static final Logger logger = LoggerFactory.getLogger(RandomAccessReaderTest.class);
 
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     private static final class Parameters
     {
-        public final long fileLength;
-        public final int bufferSize;
+        final long fileLength;
+        final int bufferSize;
 
-        public BufferType bufferType;
-        public int maxSegmentSize;
-        public boolean mmappedRegions;
+        BufferType bufferType;
+        int maxSegmentSize;
+        boolean mmappedRegions;
         public byte[] expected;
 
         Parameters(long fileLength, int bufferSize)
@@ -70,29 +78,23 @@
             this.expected = "The quick brown fox jumps over the lazy dog".getBytes(FileUtils.CHARSET);
         }
 
-        public Parameters mmappedRegions(boolean mmappedRegions)
+        Parameters mmappedRegions(boolean mmappedRegions)
         {
             this.mmappedRegions = mmappedRegions;
             return this;
         }
 
-        public Parameters bufferType(BufferType bufferType)
+        Parameters bufferType(BufferType bufferType)
         {
             this.bufferType = bufferType;
             return this;
         }
 
-        public Parameters maxSegmentSize(int maxSegmentSize)
+        Parameters maxSegmentSize(int maxSegmentSize)
         {
             this.maxSegmentSize = maxSegmentSize;
             return this;
         }
-
-        public Parameters expected(byte[] expected)
-        {
-            this.expected = expected;
-            return this;
-        }
     }
 
     @Test
@@ -128,6 +130,7 @@
     @Test
     public void testMultipleSegments() throws IOException
     {
+        // FIXME: This is the same as above.
         testReadFully(new Parameters(8192, 4096).mmappedRegions(true).maxSegmentSize(1024));
     }
 
@@ -137,23 +140,21 @@
         final long SIZE = 1L << 32; // 2GB
         Parameters params = new Parameters(SIZE, 1 << 20); // 1MB
 
-        try(ChannelProxy channel = new ChannelProxy("abc", new FakeFileChannel(SIZE)))
+
+        try (ChannelProxy channel = new ChannelProxy("abc", new FakeFileChannel(SIZE));
+             FileHandle.Builder builder = new FileHandle.Builder(channel)
+                                                     .bufferType(params.bufferType).bufferSize(params.bufferSize);
+             FileHandle fh = builder.complete();
+             RandomAccessReader reader = fh.createReader())
         {
-            RandomAccessReader.Builder builder = new RandomAccessReader.Builder(channel)
-                                                 .bufferType(params.bufferType)
-                                                 .bufferSize(params.bufferSize);
+            assertEquals(channel.size(), reader.length());
+            assertEquals(channel.size(), reader.bytesRemaining());
+            assertEquals(Integer.MAX_VALUE, reader.available());
 
-            try(RandomAccessReader reader = builder.build())
-            {
-                assertEquals(channel.size(), reader.length());
-                assertEquals(channel.size(), reader.bytesRemaining());
-                assertEquals(Integer.MAX_VALUE, reader.available());
+            assertEquals(channel.size(), reader.skip(channel.size()));
 
-                assertEquals(channel.size(), reader.skip(channel.size()));
-
-                assertTrue(reader.isEOF());
-                assertEquals(0, reader.bytesRemaining());
-            }
+            assertTrue(reader.isEOF());
+            assertEquals(0, reader.bytesRemaining());
         }
     }
 
@@ -268,7 +269,7 @@
         final File f = File.createTempFile("testReadFully", "1");
         f.deleteOnExit();
 
-        try(SequentialWriter writer = SequentialWriter.open(f))
+        try(SequentialWriter writer = new SequentialWriter(f))
         {
             long numWritten = 0;
             while (numWritten < params.fileLength)
@@ -288,15 +289,12 @@
     private static void testReadFully(Parameters params) throws IOException
     {
         final File f = writeFile(params);
-        try(ChannelProxy channel = new ChannelProxy(f))
+        try (FileHandle.Builder builder = new FileHandle.Builder(f.getPath())
+                                                     .bufferType(params.bufferType).bufferSize(params.bufferSize))
         {
-            RandomAccessReader.Builder builder = new RandomAccessReader.Builder(channel)
-                                                 .bufferType(params.bufferType)
-                                                 .bufferSize(params.bufferSize);
-            if (params.mmappedRegions)
-                builder.regions(MmappedRegions.map(channel, f.length()));
-
-            try(RandomAccessReader reader = builder.build())
+            builder.mmapped(params.mmappedRegions);
+            try (FileHandle fh = builder.complete();
+                 RandomAccessReader reader = fh.createReader())
             {
                 assertEquals(f.getAbsolutePath(), reader.getPath());
                 assertEquals(f.length(), reader.length());
@@ -315,9 +313,6 @@
                 assertTrue(reader.isEOF());
                 assertEquals(0, reader.bytesRemaining());
             }
-
-            if (builder.regions != null)
-                assertNull(builder.regions.close(null));
         }
     }
 
@@ -327,7 +322,7 @@
         File f = File.createTempFile("testReadBytes", "1");
         final String expected = "The quick brown fox jumps over the lazy dog";
 
-        try(SequentialWriter writer = SequentialWriter.open(f))
+        try(SequentialWriter writer = new SequentialWriter(f))
         {
             writer.write(expected.getBytes());
             writer.finish();
@@ -335,8 +330,9 @@
 
         assert f.exists();
 
-        try(ChannelProxy channel = new ChannelProxy(f);
-            RandomAccessReader reader = new RandomAccessReader.Builder(channel).build())
+        try (FileHandle.Builder builder = new FileHandle.Builder(f.getPath());
+             FileHandle fh = builder.complete();
+             RandomAccessReader reader = fh.createReader())
         {
             assertEquals(f.getAbsolutePath(), reader.getPath());
             assertEquals(expected.length(), reader.length());
@@ -356,7 +352,7 @@
         final String expected = "The quick brown fox jumps over the lazy dog";
         final int numIterations = 10;
 
-        try(SequentialWriter writer = SequentialWriter.open(f))
+        try(SequentialWriter writer = new SequentialWriter(f))
         {
             for (int i = 0; i < numIterations; i++)
                 writer.write(expected.getBytes());
@@ -365,8 +361,9 @@
 
         assert f.exists();
 
-        try(ChannelProxy channel = new ChannelProxy(f);
-        RandomAccessReader reader = new RandomAccessReader.Builder(channel).build())
+        try (FileHandle.Builder builder = new FileHandle.Builder(f.getPath());
+             FileHandle fh = builder.complete();
+             RandomAccessReader reader = fh.createReader())
         {
             assertEquals(expected.length() * numIterations, reader.length());
 
@@ -436,7 +433,7 @@
         Random r = new Random(seed);
         r.nextBytes(expected);
 
-        try(SequentialWriter writer = SequentialWriter.open(f))
+        try(SequentialWriter writer = new SequentialWriter(f))
         {
             writer.write(expected);
             writer.finish();
@@ -444,11 +441,12 @@
 
         assert f.exists();
 
-        try(final ChannelProxy channel = new ChannelProxy(f))
+        try (FileHandle.Builder builder = new FileHandle.Builder(f.getPath()))
         {
             final Runnable worker = () ->
             {
-                try(RandomAccessReader reader = new RandomAccessReader.Builder(channel).build())
+                try (FileHandle fh = builder.complete();
+                     RandomAccessReader reader = fh.createReader())
                 {
                     assertEquals(expected.length, reader.length());
 
@@ -500,4 +498,81 @@
             }
         }
     }
+
+    @Test
+    public void testSkipBytesLessThanBufferSize() throws IOException
+    {
+        testSkipBytes(new Parameters(8192, 1024), 1);
+    }
+
+    @Test
+    public void testSkipBytesGreaterThanBufferSize() throws IOException
+    {
+        int bufferSize = 16;
+        Parameters params = new Parameters(8192, bufferSize);
+        int numberOfExpectationsInBufferSize = bufferSize / params.expected.length;
+        testSkipBytes(params, numberOfExpectationsInBufferSize + 1);
+    }
+
+    public void testSkipBytesNonPositive() throws IOException
+    {
+        Parameters params = new Parameters(8192, 4096);
+        final File f = writeFile(params);
+        try (FileHandle.Builder builder = new FileHandle.Builder(f.getPath())
+                                                     .bufferType(params.bufferType).bufferSize(params.bufferSize))
+        {
+            builder.mmapped(params.mmappedRegions);
+            try (FileHandle fh = builder.complete();
+                 RandomAccessReader reader = fh.createReader())
+            {
+                assertEquals(0, reader.skipBytes(0));
+                assertEquals(0, reader.skipBytes(-1));
+            }
+        }
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testSkipBytesClosed() throws IOException
+    {
+        Parameters params = new Parameters(8192, 4096);
+        final File f = writeFile(params);
+        try (FileHandle.Builder builder = new FileHandle.Builder(f.getPath())
+                                                     .bufferType(params.bufferType).bufferSize(params.bufferSize))
+        {
+            try (FileHandle fh = builder.complete();
+                 RandomAccessReader reader = fh.createReader())
+            {
+                reader.close();
+                reader.skipBytes(31415);
+            }
+        }
+    }
+
+    private static void testSkipBytes(Parameters params, int expectationMultiples) throws IOException
+    {
+        final File f = writeFile(params);
+        try (FileHandle.Builder builder = new FileHandle.Builder(f.getPath())
+                                                     .bufferType(params.bufferType).bufferSize(params.bufferSize))
+        {
+            builder.mmapped(params.mmappedRegions);
+            try (FileHandle fh = builder.complete();
+                 RandomAccessReader reader = fh.createReader())
+            {
+                int toSkip = expectationMultiples * params.expected.length;
+                byte[] b = new byte[params.expected.length];
+                long numRead = 0;
+
+                while (numRead < params.fileLength)
+                {
+                    reader.readFully(b);
+                    assertTrue(Arrays.equals(params.expected, b));
+                    numRead += b.length;
+                    int skipped = reader.skipBytes(toSkip);
+                    long expectedSkipped = Math.max(Math.min(toSkip, params.fileLength - numRead), 0);
+                    assertEquals(expectedSkipped, skipped);
+                    numRead += skipped;
+                }
+            }
+        }
+    }
 }
diff --git a/test/unit/org/apache/cassandra/io/util/SegmentedFileTest.java b/test/unit/org/apache/cassandra/io/util/SegmentedFileTest.java
deleted file mode 100644
index 03c10de..0000000
--- a/test/unit/org/apache/cassandra/io/util/SegmentedFileTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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.io.util;
-
-import org.junit.Test;
-
-import org.apache.cassandra.config.Config;
-import org.apache.cassandra.config.DatabaseDescriptor;
-
-import static org.junit.Assert.assertEquals;
-
-public class SegmentedFileTest
-{
-    @Test
-    public void testRoundingBufferSize()
-    {
-        assertEquals(4096, SegmentedFile.Builder.roundBufferSize(-1L));
-        assertEquals(4096, SegmentedFile.Builder.roundBufferSize(0));
-        assertEquals(4096, SegmentedFile.Builder.roundBufferSize(1));
-        assertEquals(4096, SegmentedFile.Builder.roundBufferSize(2013));
-        assertEquals(4096, SegmentedFile.Builder.roundBufferSize(4095));
-        assertEquals(4096, SegmentedFile.Builder.roundBufferSize(4096));
-        assertEquals(8192, SegmentedFile.Builder.roundBufferSize(4097));
-        assertEquals(8192, SegmentedFile.Builder.roundBufferSize(8191));
-        assertEquals(8192, SegmentedFile.Builder.roundBufferSize(8192));
-        assertEquals(12288, SegmentedFile.Builder.roundBufferSize(8193));
-        assertEquals(65536, SegmentedFile.Builder.roundBufferSize(65535));
-        assertEquals(65536, SegmentedFile.Builder.roundBufferSize(65536));
-        assertEquals(65536, SegmentedFile.Builder.roundBufferSize(65537));
-        assertEquals(65536, SegmentedFile.Builder.roundBufferSize(10000000000000000L));
-    }
-
-    @Test
-    public void testBufferSize_ssd()
-    {
-        DatabaseDescriptor.setDiskOptimizationStrategy(Config.DiskOptimizationStrategy.ssd);
-        DatabaseDescriptor.setDiskOptimizationPageCrossChance(0.1);
-
-        assertEquals(4096, SegmentedFile.Builder.bufferSize(0));
-        assertEquals(4096, SegmentedFile.Builder.bufferSize(10));
-        assertEquals(4096, SegmentedFile.Builder.bufferSize(100));
-        assertEquals(4096, SegmentedFile.Builder.bufferSize(4096));
-        assertEquals(8192, SegmentedFile.Builder.bufferSize(4505));   // just < (4096 + 4096 * 0.1)
-        assertEquals(12288, SegmentedFile.Builder.bufferSize(4506));  // just > (4096 + 4096 * 0.1)
-
-        DatabaseDescriptor.setDiskOptimizationPageCrossChance(0.5);
-        assertEquals(8192, SegmentedFile.Builder.bufferSize(4506));  // just > (4096 + 4096 * 0.1)
-        assertEquals(8192, SegmentedFile.Builder.bufferSize(6143));  // < (4096 + 4096 * 0.5)
-        assertEquals(12288, SegmentedFile.Builder.bufferSize(6144));  // = (4096 + 4096 * 0.5)
-        assertEquals(12288, SegmentedFile.Builder.bufferSize(6145));  // > (4096 + 4096 * 0.5)
-
-        DatabaseDescriptor.setDiskOptimizationPageCrossChance(1.0); // never add a page
-        assertEquals(8192, SegmentedFile.Builder.bufferSize(8191));
-        assertEquals(8192, SegmentedFile.Builder.bufferSize(8192));
-
-        DatabaseDescriptor.setDiskOptimizationPageCrossChance(0.0); // always add a page
-        assertEquals(8192, SegmentedFile.Builder.bufferSize(10));
-        assertEquals(8192, SegmentedFile.Builder.bufferSize(4096));
-    }
-
-    @Test
-    public void testBufferSize_spinning()
-    {
-        DatabaseDescriptor.setDiskOptimizationStrategy(Config.DiskOptimizationStrategy.spinning);
-
-        assertEquals(4096, SegmentedFile.Builder.bufferSize(0));
-        assertEquals(8192, SegmentedFile.Builder.bufferSize(10));
-        assertEquals(8192, SegmentedFile.Builder.bufferSize(100));
-        assertEquals(8192, SegmentedFile.Builder.bufferSize(4096));
-        assertEquals(12288, SegmentedFile.Builder.bufferSize(4097));
-    }
-}
diff --git a/test/unit/org/apache/cassandra/io/util/SequentialWriterTest.java b/test/unit/org/apache/cassandra/io/util/SequentialWriterTest.java
index 4d75103..002ed23 100644
--- a/test/unit/org/apache/cassandra/io/util/SequentialWriterTest.java
+++ b/test/unit/org/apache/cassandra/io/util/SequentialWriterTest.java
@@ -28,10 +28,12 @@
 
 import com.google.common.io.Files;
 import org.junit.After;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import junit.framework.Assert;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.io.compress.BufferType;
 import org.apache.cassandra.utils.concurrent.AbstractTransactionalTest;
 
@@ -43,6 +45,12 @@
 
     private final List<TestableSW> writers = new ArrayList<>();
 
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @After
     public void cleanup()
     {
@@ -72,7 +80,10 @@
 
         protected TestableSW(File file) throws IOException
         {
-            this(file, new SequentialWriter(file, 8 << 10, BufferType.OFF_HEAP));
+            this(file, new SequentialWriter(file, SequentialWriterOption.newBuilder()
+                                                                        .bufferSize(8 << 10)
+                                                                        .bufferType(BufferType.OFF_HEAP)
+                                                                        .build()));
         }
 
         protected TestableSW(File file, SequentialWriter sw) throws IOException
@@ -126,7 +137,8 @@
         final int bufferSize = 48;
         final int writeSize = 64;
         byte[] toWrite = new byte[writeSize];
-        try (SequentialWriter writer = new SequentialWriter(tempFile, bufferSize, BufferType.OFF_HEAP))
+        SequentialWriterOption option = SequentialWriterOption.newBuilder().bufferSize(bufferSize).build();
+        try (SequentialWriter writer = new SequentialWriter(tempFile, option))
         {
             // write bytes greather than buffer
             writer.write(toWrite);
@@ -168,7 +180,8 @@
         File tempFile = new File(Files.createTempDir(), "test.txt");
         Assert.assertFalse("temp file shouldn't exist yet", tempFile.exists());
 
-        try (DataOutputStream os = new DataOutputStream(SequentialWriter.open(tempFile).finishOnClose()))
+        SequentialWriterOption option = SequentialWriterOption.newBuilder().finishOnClose(true).build();
+        try (DataOutputStream os = new DataOutputStream(new SequentialWriter(tempFile, option)))
         {
             os.writeUTF("123");
         }
diff --git a/test/unit/org/apache/cassandra/locator/CloudstackSnitchTest.java b/test/unit/org/apache/cassandra/locator/CloudstackSnitchTest.java
index 5dc34df..bc3e837 100644
--- a/test/unit/org/apache/cassandra/locator/CloudstackSnitchTest.java
+++ b/test/unit/org/apache/cassandra/locator/CloudstackSnitchTest.java
@@ -28,8 +28,8 @@
 import org.junit.Test;
 
 import org.apache.cassandra.SchemaLoader;
-import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.gms.ApplicationState;
 import org.apache.cassandra.gms.Gossiper;
@@ -46,7 +46,7 @@
     public static void setup() throws Exception
     {
         System.setProperty(Gossiper.Props.DISABLE_THREAD_VALIDATION, "true");
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
         SchemaLoader.mkdirs();
         SchemaLoader.cleanup();
         Keyspace.setInitialized();
diff --git a/test/unit/org/apache/cassandra/locator/DynamicEndpointSnitchTest.java b/test/unit/org/apache/cassandra/locator/DynamicEndpointSnitchTest.java
index af7dc17..8a59a4a 100644
--- a/test/unit/org/apache/cassandra/locator/DynamicEndpointSnitchTest.java
+++ b/test/unit/org/apache/cassandra/locator/DynamicEndpointSnitchTest.java
@@ -22,8 +22,10 @@
 import java.net.InetAddress;
 import java.util.*;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.utils.FBUtilities;
@@ -32,6 +34,13 @@
 
 public class DynamicEndpointSnitchTest
 {
+
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     private static void setScores(DynamicEndpointSnitch dsnitch,  int rounds, List<InetAddress> hosts, Integer... scores) throws InterruptedException
     {
         for (int round = 0; round < rounds; round++)
diff --git a/test/unit/org/apache/cassandra/locator/EC2SnitchTest.java b/test/unit/org/apache/cassandra/locator/EC2SnitchTest.java
index 9d078ce..0c71c92 100644
--- a/test/unit/org/apache/cassandra/locator/EC2SnitchTest.java
+++ b/test/unit/org/apache/cassandra/locator/EC2SnitchTest.java
@@ -51,7 +51,7 @@
     public static void setup() throws Exception
     {
         System.setProperty(Gossiper.Props.DISABLE_THREAD_VALIDATION, "true");
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
         SchemaLoader.mkdirs();
         SchemaLoader.cleanup();
         Keyspace.setInitialized();
diff --git a/test/unit/org/apache/cassandra/locator/GoogleCloudSnitchTest.java b/test/unit/org/apache/cassandra/locator/GoogleCloudSnitchTest.java
index 04b71e9..2491ba9 100644
--- a/test/unit/org/apache/cassandra/locator/GoogleCloudSnitchTest.java
+++ b/test/unit/org/apache/cassandra/locator/GoogleCloudSnitchTest.java
@@ -47,7 +47,7 @@
     public static void setup() throws Exception
     {
         System.setProperty(Gossiper.Props.DISABLE_THREAD_VALIDATION, "true");
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
         SchemaLoader.mkdirs();
         SchemaLoader.cleanup();
         Keyspace.setInitialized();
diff --git a/test/unit/org/apache/cassandra/locator/GossipingPropertyFileSnitchTest.java b/test/unit/org/apache/cassandra/locator/GossipingPropertyFileSnitchTest.java
index 61c179f..77734f7 100644
--- a/test/unit/org/apache/cassandra/locator/GossipingPropertyFileSnitchTest.java
+++ b/test/unit/org/apache/cassandra/locator/GossipingPropertyFileSnitchTest.java
@@ -21,8 +21,10 @@
 import java.net.InetAddress;
 
 import com.google.common.net.InetAddresses;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.utils.FBUtilities;
 
 import static org.junit.Assert.*;
@@ -32,6 +34,13 @@
  */
 public class GossipingPropertyFileSnitchTest
 {
+
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     public static void checkEndpoint(final AbstractNetworkTopologySnitch snitch,
                                      final String endpointString, final String expectedDatacenter,
                                      final String expectedRack)
diff --git a/test/unit/org/apache/cassandra/locator/NetworkTopologyStrategyTest.java b/test/unit/org/apache/cassandra/locator/NetworkTopologyStrategyTest.java
index bbfdd3b..48dd573 100644
--- a/test/unit/org/apache/cassandra/locator/NetworkTopologyStrategyTest.java
+++ b/test/unit/org/apache/cassandra/locator/NetworkTopologyStrategyTest.java
@@ -21,30 +21,40 @@
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
+import java.util.stream.Collectors;
 
 import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Multimap;
+
 import org.junit.Assert;
+import org.junit.BeforeClass;
 import org.junit.Test;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.dht.Murmur3Partitioner;
 import org.apache.cassandra.dht.OrderPreservingPartitioner.StringToken;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.locator.TokenMetadata.Topology;
+import org.apache.cassandra.service.StorageService;
 
 public class NetworkTopologyStrategyTest
 {
     private String keyspaceName = "Keyspace1";
     private static final Logger logger = LoggerFactory.getLogger(NetworkTopologyStrategyTest.class);
 
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testProperties() throws IOException, ConfigurationException
     {
@@ -166,4 +176,203 @@
         InetAddress add1 = InetAddress.getByAddress(bytes);
         metadata.updateNormalToken(token1, add1);
     }
+
+    @Test
+    public void testCalculateEndpoints() throws UnknownHostException
+    {
+        final int NODES = 100;
+        final int VNODES = 64;
+        final int RUNS = 10;
+        StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance);
+        Map<String, Integer> datacenters = ImmutableMap.of("rf1", 1, "rf3", 3, "rf5_1", 5, "rf5_2", 5, "rf5_3", 5);
+        List<InetAddress> nodes = new ArrayList<>(NODES);
+        for (byte i=0; i<NODES; ++i)
+            nodes.add(InetAddress.getByAddress(new byte[]{127, 0, 0, i}));
+        for (int run=0; run<RUNS; ++run)
+        {
+            Random rand = new Random();
+            IEndpointSnitch snitch = generateSnitch(datacenters, nodes, rand);
+            DatabaseDescriptor.setEndpointSnitch(snitch);
+
+            TokenMetadata meta = new TokenMetadata();
+            for (int i=0; i<NODES; ++i)  // Nodes
+                for (int j=0; j<VNODES; ++j) // tokens/vnodes per node
+                    meta.updateNormalToken(Murmur3Partitioner.instance.getRandomToken(rand), nodes.get(i));
+            testEquivalence(meta, snitch, datacenters, rand);
+        }
+    }
+
+    void testEquivalence(TokenMetadata tokenMetadata, IEndpointSnitch snitch, Map<String, Integer> datacenters, Random rand)
+    {
+        NetworkTopologyStrategy nts = new NetworkTopologyStrategy("ks", tokenMetadata, snitch,
+                                                                  datacenters.entrySet().stream().
+                                                                      collect(Collectors.toMap(x -> x.getKey(), x -> Integer.toString(x.getValue()))));
+        for (int i=0; i<1000; ++i)
+        {
+            Token token = Murmur3Partitioner.instance.getRandomToken(rand);
+            List<InetAddress> expected = calculateNaturalEndpoints(token, tokenMetadata, datacenters, snitch);
+            List<InetAddress> actual = nts.calculateNaturalEndpoints(token, tokenMetadata);
+            if (endpointsDiffer(expected, actual))
+            {
+                System.err.println("Endpoints mismatch for token " + token);
+                System.err.println(" expected: " + expected);
+                System.err.println(" actual  : " + actual);
+                Assert.assertEquals("Endpoints for token " + token + " mismatch.", expected, actual);
+            }
+        }
+    }
+
+    private boolean endpointsDiffer(List<InetAddress> ep1, List<InetAddress> ep2)
+    {
+        // Because the old algorithm does not put the nodes in the correct order in the case where more replicas
+        // are required than there are racks in a dc, we accept different order as long as the primary
+        // replica is the same.
+        if (ep1.equals(ep2))
+            return false;
+        if (!ep1.get(0).equals(ep2.get(0)))
+            return true;
+        Set<InetAddress> s1 = new HashSet<>(ep1);
+        Set<InetAddress> s2 = new HashSet<>(ep2);
+        return !s1.equals(s2);
+    }
+
+    IEndpointSnitch generateSnitch(Map<String, Integer> datacenters, Collection<InetAddress> nodes, Random rand)
+    {
+        final Map<InetAddress, String> nodeToRack = new HashMap<>();
+        final Map<InetAddress, String> nodeToDC = new HashMap<>();
+        Map<String, List<String>> racksPerDC = new HashMap<>();
+        datacenters.forEach((dc, rf) -> racksPerDC.put(dc, randomRacks(rf, rand)));
+        int rf = datacenters.values().stream().mapToInt(x -> x).sum();
+        String[] dcs = new String[rf];
+        int pos = 0;
+        for (Map.Entry<String, Integer> dce : datacenters.entrySet())
+        {
+            for (int i = 0; i < dce.getValue(); ++i)
+                dcs[pos++] = dce.getKey();
+        }
+
+        for (InetAddress node : nodes)
+        {
+            String dc = dcs[rand.nextInt(rf)];
+            List<String> racks = racksPerDC.get(dc);
+            String rack = racks.get(rand.nextInt(racks.size()));
+            nodeToRack.put(node, rack);
+            nodeToDC.put(node, dc);
+        }
+
+        return new AbstractNetworkTopologySnitch()
+        {
+            public String getRack(InetAddress endpoint)
+            {
+                return nodeToRack.get(endpoint);
+            }
+
+            public String getDatacenter(InetAddress endpoint)
+            {
+                return nodeToDC.get(endpoint);
+            }
+        };
+    }
+
+    private List<String> randomRacks(int rf, Random rand)
+    {
+        int rc = rand.nextInt(rf * 3 - 1) + 1;
+        List<String> racks = new ArrayList<>(rc);
+        for (int i=0; i<rc; ++i)
+            racks.add(Integer.toString(i));
+        return racks;
+    }
+
+    // Copy of older endpoints calculation algorithm for comparison
+    public static List<InetAddress> calculateNaturalEndpoints(Token searchToken, TokenMetadata tokenMetadata, Map<String, Integer> datacenters, IEndpointSnitch snitch)
+    {
+        // we want to preserve insertion order so that the first added endpoint becomes primary
+        Set<InetAddress> replicas = new LinkedHashSet<>();
+        // replicas we have found in each DC
+        Map<String, Set<InetAddress>> dcReplicas = new HashMap<>(datacenters.size());
+        for (Map.Entry<String, Integer> dc : datacenters.entrySet())
+            dcReplicas.put(dc.getKey(), new HashSet<InetAddress>(dc.getValue()));
+
+        Topology topology = tokenMetadata.getTopology();
+        // all endpoints in each DC, so we can check when we have exhausted all the members of a DC
+        Multimap<String, InetAddress> allEndpoints = topology.getDatacenterEndpoints();
+        // all racks in a DC so we can check when we have exhausted all racks in a DC
+        Map<String, ImmutableMultimap<String, InetAddress>> racks = topology.getDatacenterRacks();
+        assert !allEndpoints.isEmpty() && !racks.isEmpty() : "not aware of any cluster members";
+
+        // tracks the racks we have already placed replicas in
+        Map<String, Set<String>> seenRacks = new HashMap<>(datacenters.size());
+        for (Map.Entry<String, Integer> dc : datacenters.entrySet())
+            seenRacks.put(dc.getKey(), new HashSet<String>());
+
+        // tracks the endpoints that we skipped over while looking for unique racks
+        // when we relax the rack uniqueness we can append this to the current result so we don't have to wind back the iterator
+        Map<String, Set<InetAddress>> skippedDcEndpoints = new HashMap<>(datacenters.size());
+        for (Map.Entry<String, Integer> dc : datacenters.entrySet())
+            skippedDcEndpoints.put(dc.getKey(), new LinkedHashSet<InetAddress>());
+
+        Iterator<Token> tokenIter = TokenMetadata.ringIterator(tokenMetadata.sortedTokens(), searchToken, false);
+        while (tokenIter.hasNext() && !hasSufficientReplicas(dcReplicas, allEndpoints, datacenters))
+        {
+            Token next = tokenIter.next();
+            InetAddress ep = tokenMetadata.getEndpoint(next);
+            String dc = snitch.getDatacenter(ep);
+            // have we already found all replicas for this dc?
+            if (!datacenters.containsKey(dc) || hasSufficientReplicas(dc, dcReplicas, allEndpoints, datacenters))
+                continue;
+            // can we skip checking the rack?
+            if (seenRacks.get(dc).size() == racks.get(dc).keySet().size())
+            {
+                dcReplicas.get(dc).add(ep);
+                replicas.add(ep);
+            }
+            else
+            {
+                String rack = snitch.getRack(ep);
+                // is this a new rack?
+                if (seenRacks.get(dc).contains(rack))
+                {
+                    skippedDcEndpoints.get(dc).add(ep);
+                }
+                else
+                {
+                    dcReplicas.get(dc).add(ep);
+                    replicas.add(ep);
+                    seenRacks.get(dc).add(rack);
+                    // if we've run out of distinct racks, add the hosts we skipped past already (up to RF)
+                    if (seenRacks.get(dc).size() == racks.get(dc).keySet().size())
+                    {
+                        Iterator<InetAddress> skippedIt = skippedDcEndpoints.get(dc).iterator();
+                        while (skippedIt.hasNext() && !hasSufficientReplicas(dc, dcReplicas, allEndpoints, datacenters))
+                        {
+                            InetAddress nextSkipped = skippedIt.next();
+                            dcReplicas.get(dc).add(nextSkipped);
+                            replicas.add(nextSkipped);
+                        }
+                    }
+                }
+            }
+        }
+
+        return new ArrayList<InetAddress>(replicas);
+    }
+
+    private static boolean hasSufficientReplicas(String dc, Map<String, Set<InetAddress>> dcReplicas, Multimap<String, InetAddress> allEndpoints, Map<String, Integer> datacenters)
+    {
+        return dcReplicas.get(dc).size() >= Math.min(allEndpoints.get(dc).size(), getReplicationFactor(dc, datacenters));
+    }
+
+    private static boolean hasSufficientReplicas(Map<String, Set<InetAddress>> dcReplicas, Multimap<String, InetAddress> allEndpoints, Map<String, Integer> datacenters)
+    {
+        for (String dc : datacenters.keySet())
+            if (!hasSufficientReplicas(dc, dcReplicas, allEndpoints, datacenters))
+                return false;
+        return true;
+    }
+
+    public static int getReplicationFactor(String dc, Map<String, Integer> datacenters)
+    {
+        Integer replicas = datacenters.get(dc);
+        return replicas == null ? 0 : replicas;
+    }
 }
diff --git a/test/unit/org/apache/cassandra/locator/OldNetworkTopologyStrategyTest.java b/test/unit/org/apache/cassandra/locator/OldNetworkTopologyStrategyTest.java
index 6eb08c4..e6e17cd 100644
--- a/test/unit/org/apache/cassandra/locator/OldNetworkTopologyStrategyTest.java
+++ b/test/unit/org/apache/cassandra/locator/OldNetworkTopologyStrategyTest.java
@@ -29,8 +29,10 @@
 import java.util.Set;
 
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken;
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
@@ -45,6 +47,12 @@
     private TokenMetadata tmd;
     private Map<String, ArrayList<InetAddress>> expectedResults;
 
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Before
     public void init()
     {
diff --git a/test/unit/org/apache/cassandra/locator/PropertyFileSnitchTest.java b/test/unit/org/apache/cassandra/locator/PropertyFileSnitchTest.java
index 29ea4d5..e756902 100644
--- a/test/unit/org/apache/cassandra/locator/PropertyFileSnitchTest.java
+++ b/test/unit/org/apache/cassandra/locator/PropertyFileSnitchTest.java
@@ -35,6 +35,7 @@
 
 import com.google.common.net.InetAddresses;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.RandomPartitioner;
 import org.apache.cassandra.dht.Token;
@@ -46,6 +47,7 @@
 import org.apache.cassandra.utils.FBUtilities;
 
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import static org.junit.Assert.*;
@@ -61,6 +63,12 @@
     private VersionedValue.VersionedValueFactory valueFactory;
     private Map<InetAddress, Set<Token>> tokenMap;
 
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Before
     public void setup() throws ConfigurationException, IOException
     {
diff --git a/test/unit/org/apache/cassandra/locator/TokenMetadataTest.java b/test/unit/org/apache/cassandra/locator/TokenMetadataTest.java
index 0ec8eeb..cf1b42f 100644
--- a/test/unit/org/apache/cassandra/locator/TokenMetadataTest.java
+++ b/test/unit/org/apache/cassandra/locator/TokenMetadataTest.java
@@ -57,6 +57,7 @@
     @BeforeClass
     public static void beforeClass() throws Throwable
     {
+        DatabaseDescriptor.daemonInitialization();
         tmd = StorageService.instance.getTokenMetadata();
         tmd.updateNormalToken(token(ONE), InetAddress.getByName("127.0.0.1"));
         tmd.updateNormalToken(token(SIX), InetAddress.getByName("127.0.0.6"));
diff --git a/test/unit/org/apache/cassandra/metrics/CassandraMetricsRegistryTest.java b/test/unit/org/apache/cassandra/metrics/CassandraMetricsRegistryTest.java
new file mode 100644
index 0000000..4a9b874
--- /dev/null
+++ b/test/unit/org/apache/cassandra/metrics/CassandraMetricsRegistryTest.java
@@ -0,0 +1,89 @@
+/*
+ *
+ * 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.metrics;
+
+import java.lang.management.ManagementFactory;
+import java.util.Collection;
+
+import org.junit.Test;
+
+import com.codahale.metrics.jvm.BufferPoolMetricSet;
+import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
+import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
+import org.apache.cassandra.metrics.CassandraMetricsRegistry.MetricName;
+
+import static org.junit.Assert.*;
+
+
+public class CassandraMetricsRegistryTest
+{
+    // A class with a name ending in '$'
+    private static class StrangeName$
+    {
+    }
+
+    @Test
+    public void testChooseType()
+    {
+        assertEquals("StrangeName", MetricName.chooseType(null, StrangeName$.class));
+        assertEquals("StrangeName", MetricName.chooseType("", StrangeName$.class));
+        assertEquals("String", MetricName.chooseType(null, String.class));
+        assertEquals("String", MetricName.chooseType("", String.class));
+
+        assertEquals("a", MetricName.chooseType("a", StrangeName$.class));
+        assertEquals("b", MetricName.chooseType("b", String.class));
+    }
+
+    @Test
+    public void testMetricName()
+    {
+        MetricName name = new MetricName(StrangeName$.class, "NaMe", "ScOpE");
+        assertEquals("StrangeName", name.getType());
+    }
+
+    @Test
+    public void testJvmMetricsRegistration()
+    {
+        CassandraMetricsRegistry registry = CassandraMetricsRegistry.Metrics;
+
+        // Same registration as CassandraDaemon
+        registry.register("jvm.buffers", new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer()));
+        registry.register("jvm.gc", new GarbageCollectorMetricSet());
+        registry.register("jvm.memory", new MemoryUsageGaugeSet());
+
+        Collection<String> names = registry.getNames();
+
+        // No metric with ".." in name
+        assertTrue(names.stream()
+                        .filter(name -> name.contains(".."))
+                        .count()
+                   == 0);
+
+        // There should be several metrics within each category
+        for (String category : new String[]{"jvm.buffers","jvm.gc","jvm.memory"})
+        {
+            assertTrue(names.stream()
+                            .filter(name -> name.startsWith(category+'.'))
+                            .count() > 1);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/metrics/HintedHandOffMetricsTest.java b/test/unit/org/apache/cassandra/metrics/HintedHandOffMetricsTest.java
index 1d3863a..2394e0c 100644
--- a/test/unit/org/apache/cassandra/metrics/HintedHandOffMetricsTest.java
+++ b/test/unit/org/apache/cassandra/metrics/HintedHandOffMetricsTest.java
@@ -44,7 +44,7 @@
     @BeforeClass
     public static void initDD()
     {
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
     }
 
     @Test
diff --git a/test/unit/org/apache/cassandra/net/MatcherResponse.java b/test/unit/org/apache/cassandra/net/MatcherResponse.java
index 12a8d1b..6cd8085 100644
--- a/test/unit/org/apache/cassandra/net/MatcherResponse.java
+++ b/test/unit/org/apache/cassandra/net/MatcherResponse.java
@@ -27,8 +27,6 @@
 import java.util.function.BiFunction;
 import java.util.function.Function;
 
-import org.apache.cassandra.utils.Clock;
-
 /**
  * Sends a response for an incoming message with a matching {@link Matcher}.
  * The actual behavior by any instance of this class can be inspected by
@@ -175,16 +173,22 @@
                         assert !sendResponses.contains(id) : "ID re-use for outgoing message";
                         sendResponses.add(id);
                     }
-                    MessageIn<?> response = fnResponse.apply(message, to);
-                    if (response != null)
+
+                    // create response asynchronously to match request/response communication execution behavior
+                    new Thread(() ->
                     {
-                        CallbackInfo cb = MessagingService.instance().getRegisteredCallback(id);
-                        if (cb != null)
-                            cb.callback.response(response);
-                        else
-                            MessagingService.instance().receive(response, id, Clock.instance.currentTimeMillis(), false);
-                        spy.matchingResponse(response);
-                    }
+                        MessageIn<?> response = fnResponse.apply(message, to);
+                        if (response != null)
+                        {
+                            CallbackInfo cb = MessagingService.instance().getRegisteredCallback(id);
+                            if (cb != null)
+                                cb.callback.response(response);
+                            else
+                                MessagingService.instance().receive(response, id);
+                            spy.matchingResponse(response);
+                        }
+                    }).start();
+
                     return false;
                 }
                 return true;
diff --git a/test/unit/org/apache/cassandra/net/MessagingServiceTest.java b/test/unit/org/apache/cassandra/net/MessagingServiceTest.java
index 3be1990..82630b4 100644
--- a/test/unit/org/apache/cassandra/net/MessagingServiceTest.java
+++ b/test/unit/org/apache/cassandra/net/MessagingServiceTest.java
@@ -20,22 +20,60 @@
  */
 package org.apache.cassandra.net;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.*;
+import java.util.regex.Matcher;
 
+import com.google.common.collect.Iterables;
+import com.codahale.metrics.Timer;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.monitoring.ApproximateTime;
+import org.apache.cassandra.io.util.DataInputPlus.DataInputStreamPlus;
+import org.apache.cassandra.io.util.DataOutputStreamPlus;
+import org.apache.cassandra.io.util.WrappedDataOutputStreamPlus;
+import org.apache.cassandra.utils.FBUtilities;
+import org.caffinitas.ohc.histo.EstimatedHistogram;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 
 public class MessagingServiceTest
 {
+    private final static long ONE_SECOND = TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS);
+    private final static long[] bucketOffsets = new EstimatedHistogram(160).getBucketOffsets();
     private final MessagingService messagingService = MessagingService.test();
 
+    @BeforeClass
+    public static void beforeClass() throws UnknownHostException
+    {
+        DatabaseDescriptor.daemonInitialization();
+        DatabaseDescriptor.setBackPressureStrategy(new MockBackPressureStrategy(Collections.emptyMap()));
+        DatabaseDescriptor.setBroadcastAddress(InetAddress.getByName("127.0.0.1"));
+    }
+
     private static int metricScopeId = 0;
 
     @Before
-    public void before() {
-        messagingService.resetDroppedMessagesMap(Integer.toString(metricScopeId++));;
+    public void before() throws UnknownHostException
+    {
+        messagingService.resetDroppedMessagesMap(Integer.toString(metricScopeId++));
+        MockBackPressureStrategy.applied = false;
+        messagingService.destroyConnectionPool(InetAddress.getByName("127.0.0.2"));
+        messagingService.destroyConnectionPool(InetAddress.getByName("127.0.0.3"));
     }
 
     @Test
@@ -43,23 +81,275 @@
     {
         MessagingService.Verb verb = MessagingService.Verb.READ;
 
-        for (int i = 0; i < 5000; i++)
-            messagingService.incrementDroppedMessages(verb, i % 2 == 0);
+        for (int i = 1; i <= 5000; i++)
+            messagingService.incrementDroppedMessages(verb, i, i % 2 == 0);
 
         List<String> logs = messagingService.getDroppedMessagesLogs();
         assertEquals(1, logs.size());
-        assertEquals("READ messages were dropped in last 5000 ms: 2500 for internal timeout and 2500 for cross node timeout", logs.get(0));
-        assertEquals(5000, (int)messagingService.getDroppedMessages().get(verb.toString()));
+        Pattern regexp = Pattern.compile("READ messages were dropped in last 5000 ms: (\\d+) internal and (\\d+) cross node. Mean internal dropped latency: (\\d+) ms and Mean cross-node dropped latency: (\\d+) ms");
+        Matcher matcher = regexp.matcher(logs.get(0));
+        assertTrue(matcher.find());
+        assertEquals(2500, Integer.parseInt(matcher.group(1)));
+        assertEquals(2500, Integer.parseInt(matcher.group(2)));
+        assertTrue(Integer.parseInt(matcher.group(3)) > 0);
+        assertTrue(Integer.parseInt(matcher.group(4)) > 0);
+        assertEquals(5000, (int) messagingService.getDroppedMessages().get(verb.toString()));
 
         logs = messagingService.getDroppedMessagesLogs();
         assertEquals(0, logs.size());
 
         for (int i = 0; i < 2500; i++)
-            messagingService.incrementDroppedMessages(verb, i % 2 == 0);
+            messagingService.incrementDroppedMessages(verb, i, i % 2 == 0);
 
         logs = messagingService.getDroppedMessagesLogs();
-        assertEquals("READ messages were dropped in last 5000 ms: 1250 for internal timeout and 1250 for cross node timeout", logs.get(0));
-        assertEquals(7500, (int)messagingService.getDroppedMessages().get(verb.toString()));
+        assertEquals(1, logs.size());
+        matcher = regexp.matcher(logs.get(0));
+        assertTrue(matcher.find());
+        assertEquals(1250, Integer.parseInt(matcher.group(1)));
+        assertEquals(1250, Integer.parseInt(matcher.group(2)));
+        assertTrue(Integer.parseInt(matcher.group(3)) > 0);
+        assertTrue(Integer.parseInt(matcher.group(4)) > 0);
+        assertEquals(7500, (int) messagingService.getDroppedMessages().get(verb.toString()));
     }
 
+    @Test
+    public void testDCLatency() throws Exception
+    {
+        int latency = 100;
+        ConcurrentHashMap<String, Timer> dcLatency = MessagingService.instance().metrics.dcLatency;
+        dcLatency.clear();
+
+        long now = ApproximateTime.currentTimeMillis();
+        long sentAt = now - latency;
+        assertNull(dcLatency.get("datacenter1"));
+        addDCLatency(sentAt, now);
+        assertNotNull(dcLatency.get("datacenter1"));
+        assertEquals(1, dcLatency.get("datacenter1").getCount());
+        long expectedBucket = bucketOffsets[Math.abs(Arrays.binarySearch(bucketOffsets, TimeUnit.MILLISECONDS.toNanos(latency))) - 1];
+        assertEquals(expectedBucket, dcLatency.get("datacenter1").getSnapshot().getMax());
+    }
+
+    @Test
+    public void testNegativeDCLatency() throws Exception
+    {
+        // if clocks are off should just not track anything
+        int latency = -100;
+
+        ConcurrentHashMap<String, Timer> dcLatency = MessagingService.instance().metrics.dcLatency;
+        dcLatency.clear();
+
+        long now = ApproximateTime.currentTimeMillis();
+        long sentAt = now - latency;
+
+        assertNull(dcLatency.get("datacenter1"));
+        addDCLatency(sentAt, now);
+        assertNull(dcLatency.get("datacenter1"));
+    }
+
+    @Test
+    public void testUpdatesBackPressureOnSendWhenEnabledAndWithSupportedCallback() throws UnknownHostException
+    {
+        MockBackPressureStrategy.MockBackPressureState backPressureState = (MockBackPressureStrategy.MockBackPressureState) messagingService.getConnectionPool(InetAddress.getByName("127.0.0.2")).getBackPressureState();
+        IAsyncCallback bpCallback = new BackPressureCallback();
+        IAsyncCallback noCallback = new NoBackPressureCallback();
+        MessageOut<?> ignored = null;
+
+        DatabaseDescriptor.setBackPressureEnabled(true);
+        messagingService.updateBackPressureOnSend(InetAddress.getByName("127.0.0.2"), noCallback, ignored);
+        assertFalse(backPressureState.onSend);
+
+        DatabaseDescriptor.setBackPressureEnabled(false);
+        messagingService.updateBackPressureOnSend(InetAddress.getByName("127.0.0.2"), bpCallback, ignored);
+        assertFalse(backPressureState.onSend);
+
+        DatabaseDescriptor.setBackPressureEnabled(true);
+        messagingService.updateBackPressureOnSend(InetAddress.getByName("127.0.0.2"), bpCallback, ignored);
+        assertTrue(backPressureState.onSend);
+    }
+
+    @Test
+    public void testUpdatesBackPressureOnReceiveWhenEnabledAndWithSupportedCallback() throws UnknownHostException
+    {
+        MockBackPressureStrategy.MockBackPressureState backPressureState = (MockBackPressureStrategy.MockBackPressureState) messagingService.getConnectionPool(InetAddress.getByName("127.0.0.2")).getBackPressureState();
+        IAsyncCallback bpCallback = new BackPressureCallback();
+        IAsyncCallback noCallback = new NoBackPressureCallback();
+        boolean timeout = false;
+
+        DatabaseDescriptor.setBackPressureEnabled(true);
+        messagingService.updateBackPressureOnReceive(InetAddress.getByName("127.0.0.2"), noCallback, timeout);
+        assertFalse(backPressureState.onReceive);
+        assertFalse(backPressureState.onTimeout);
+
+        DatabaseDescriptor.setBackPressureEnabled(false);
+        messagingService.updateBackPressureOnReceive(InetAddress.getByName("127.0.0.2"), bpCallback, timeout);
+        assertFalse(backPressureState.onReceive);
+        assertFalse(backPressureState.onTimeout);
+
+        DatabaseDescriptor.setBackPressureEnabled(true);
+        messagingService.updateBackPressureOnReceive(InetAddress.getByName("127.0.0.2"), bpCallback, timeout);
+        assertTrue(backPressureState.onReceive);
+        assertFalse(backPressureState.onTimeout);
+    }
+
+    @Test
+    public void testUpdatesBackPressureOnTimeoutWhenEnabledAndWithSupportedCallback() throws UnknownHostException
+    {
+        MockBackPressureStrategy.MockBackPressureState backPressureState = (MockBackPressureStrategy.MockBackPressureState) messagingService.getConnectionPool(InetAddress.getByName("127.0.0.2")).getBackPressureState();
+        IAsyncCallback bpCallback = new BackPressureCallback();
+        IAsyncCallback noCallback = new NoBackPressureCallback();
+        boolean timeout = true;
+
+        DatabaseDescriptor.setBackPressureEnabled(true);
+        messagingService.updateBackPressureOnReceive(InetAddress.getByName("127.0.0.2"), noCallback, timeout);
+        assertFalse(backPressureState.onReceive);
+        assertFalse(backPressureState.onTimeout);
+
+        DatabaseDescriptor.setBackPressureEnabled(false);
+        messagingService.updateBackPressureOnReceive(InetAddress.getByName("127.0.0.2"), bpCallback, timeout);
+        assertFalse(backPressureState.onReceive);
+        assertFalse(backPressureState.onTimeout);
+
+        DatabaseDescriptor.setBackPressureEnabled(true);
+        messagingService.updateBackPressureOnReceive(InetAddress.getByName("127.0.0.2"), bpCallback, timeout);
+        assertFalse(backPressureState.onReceive);
+        assertTrue(backPressureState.onTimeout);
+    }
+
+    @Test
+    public void testAppliesBackPressureWhenEnabled() throws UnknownHostException
+    {
+        DatabaseDescriptor.setBackPressureEnabled(false);
+        messagingService.applyBackPressure(Arrays.asList(InetAddress.getByName("127.0.0.2")), ONE_SECOND);
+        assertFalse(MockBackPressureStrategy.applied);
+
+        DatabaseDescriptor.setBackPressureEnabled(true);
+        messagingService.applyBackPressure(Arrays.asList(InetAddress.getByName("127.0.0.2")), ONE_SECOND);
+        assertTrue(MockBackPressureStrategy.applied);
+    }
+
+    @Test
+    public void testDoesntApplyBackPressureToBroadcastAddress() throws UnknownHostException
+    {
+        DatabaseDescriptor.setBackPressureEnabled(true);
+        messagingService.applyBackPressure(Arrays.asList(InetAddress.getByName("127.0.0.1")), ONE_SECOND);
+        assertFalse(MockBackPressureStrategy.applied);
+    }
+
+    private static void addDCLatency(long sentAt, long nowTime) throws IOException
+    {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (DataOutputStreamPlus out = new WrappedDataOutputStreamPlus(baos))
+        {
+            out.writeInt((int) sentAt);
+        }
+        DataInputStreamPlus in = new DataInputStreamPlus(new ByteArrayInputStream(baos.toByteArray()));
+        MessageIn.readConstructionTime(FBUtilities.getLocalAddress(), in, nowTime);
+    }
+
+    public static class MockBackPressureStrategy implements BackPressureStrategy<MockBackPressureStrategy.MockBackPressureState>
+    {
+        public static volatile boolean applied = false;
+
+        public MockBackPressureStrategy(Map<String, Object> args)
+        {
+        }
+
+        @Override
+        public void apply(Set<MockBackPressureState> states, long timeout, TimeUnit unit)
+        {
+            if (!Iterables.isEmpty(states))
+                applied = true;
+        }
+
+        @Override
+        public MockBackPressureState newState(InetAddress host)
+        {
+            return new MockBackPressureState(host);
+        }
+
+        public static class MockBackPressureState implements BackPressureState
+        {
+            private final InetAddress host;
+            public volatile boolean onSend = false;
+            public volatile boolean onReceive = false;
+            public volatile boolean onTimeout = false;
+
+            private MockBackPressureState(InetAddress host)
+            {
+                this.host = host;
+            }
+
+            @Override
+            public void onMessageSent(MessageOut<?> message)
+            {
+                onSend = true;
+            }
+
+            @Override
+            public void onResponseReceived()
+            {
+                onReceive = true;
+            }
+
+            @Override
+            public void onResponseTimeout()
+            {
+                onTimeout = true;
+            }
+
+            @Override
+            public double getBackPressureRateLimit()
+            {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
+            public InetAddress getHost()
+            {
+                return host;
+            }
+        }
+    }
+
+    private static class BackPressureCallback implements IAsyncCallback
+    {
+        @Override
+        public boolean supportsBackPressure()
+        {
+            return true;
+        }
+
+        @Override
+        public boolean isLatencyForSnitch()
+        {
+            return false;
+        }
+
+        @Override
+        public void response(MessageIn msg)
+        {
+            throw new UnsupportedOperationException("Not supported.");
+        }
+    }
+
+    private static class NoBackPressureCallback implements IAsyncCallback
+    {
+        @Override
+        public boolean supportsBackPressure()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isLatencyForSnitch()
+        {
+            return false;
+        }
+
+        @Override
+        public void response(MessageIn msg)
+        {
+            throw new UnsupportedOperationException("Not supported.");
+        }
+    }
 }
diff --git a/test/unit/org/apache/cassandra/net/MockMessagingServiceTest.java b/test/unit/org/apache/cassandra/net/MockMessagingServiceTest.java
index ed4cce8..ab97aaa 100644
--- a/test/unit/org/apache/cassandra/net/MockMessagingServiceTest.java
+++ b/test/unit/org/apache/cassandra/net/MockMessagingServiceTest.java
@@ -25,6 +25,7 @@
 import org.junit.Test;
 
 import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.Util;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.gms.EchoMessage;
 import org.apache.cassandra.service.StorageService;
@@ -59,8 +60,7 @@
                 EchoMessage.instance,
                 Collections.emptyMap(),
                 MessagingService.Verb.ECHO,
-                MessagingService.current_version
-        );
+                MessagingService.current_version);
         MockMessagingSpy spy = MockMessagingService
                 .when(
                         all(
@@ -87,10 +87,10 @@
 
         // we must have intercepted the outgoing message at this point
         MessageOut<?> msg = spy.captureMessageOut().get();
-        assertEquals(1, spy.messagesIntercepted);
+        assertEquals(1, spy.messagesIntercepted());
         assertTrue(msg == echoMessageOut);
 
         // and return a mocked response
-        assertEquals(1, spy.mockedMessageResponses);
+        Util.spinAssertEquals(1, spy::mockedMessageResponses, 60);
     }
-}
\ No newline at end of file
+}
diff --git a/test/unit/org/apache/cassandra/net/MockMessagingSpy.java b/test/unit/org/apache/cassandra/net/MockMessagingSpy.java
index 80bdb39..2219c5a 100644
--- a/test/unit/org/apache/cassandra/net/MockMessagingSpy.java
+++ b/test/unit/org/apache/cassandra/net/MockMessagingSpy.java
@@ -24,6 +24,7 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import com.google.common.util.concurrent.AbstractFuture;
 import com.google.common.util.concurrent.Futures;
@@ -40,8 +41,8 @@
 {
     private static final Logger logger = LoggerFactory.getLogger(MockMessagingSpy.class);
 
-    public int messagesIntercepted = 0;
-    public int mockedMessageResponses = 0;
+    private final AtomicInteger messagesIntercepted = new AtomicInteger();
+    private final AtomicInteger mockedMessageResponses = new AtomicInteger();
 
     private final BlockingQueue<MessageOut<?>> interceptedMessages = new LinkedBlockingQueue<>();
     private final BlockingQueue<MessageIn<?>> deliveredResponses = new LinkedBlockingQueue<>();
@@ -130,21 +131,30 @@
         return ret;
     }
 
+    public int messagesIntercepted()
+    {
+        return messagesIntercepted.get();
+    }
+
+    public int mockedMessageResponses()
+    {
+        return mockedMessageResponses.get();
+    }
+
     void matchingMessage(MessageOut<?> message)
     {
-        messagesIntercepted++;
+        messagesIntercepted.incrementAndGet();
         logger.trace("Received matching message: {}", message);
         interceptedMessages.add(message);
     }
 
     void matchingResponse(MessageIn<?> response)
     {
-        mockedMessageResponses++;
+        mockedMessageResponses.incrementAndGet();
         logger.trace("Responding to intercepted message: {}", response);
         deliveredResponses.add(response);
     }
 
-
     private static class CapturedResultsFuture<T> extends AbstractFuture<List<T>> implements Runnable
     {
         private final int waitForResults;
diff --git a/test/unit/org/apache/cassandra/net/OutboundTcpConnectionTest.java b/test/unit/org/apache/cassandra/net/OutboundTcpConnectionTest.java
index c09ae0f..e3b6817 100644
--- a/test/unit/org/apache/cassandra/net/OutboundTcpConnectionTest.java
+++ b/test/unit/org/apache/cassandra/net/OutboundTcpConnectionTest.java
@@ -39,9 +39,14 @@
 
     final static Verb VERB_DROPPABLE = Verb.MUTATION; // Droppable, 2s timeout
     final static Verb VERB_NONDROPPABLE = Verb.GOSSIP_DIGEST_ACK; // Not droppable
+    
+    final static long NANOS_FOR_TIMEOUT;
 
-    final static long NANOS_FOR_TIMEOUT = TimeUnit.MILLISECONDS.toNanos(DatabaseDescriptor.getTimeout(VERB_DROPPABLE)*2);
-
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+        NANOS_FOR_TIMEOUT = TimeUnit.MILLISECONDS.toNanos(VERB_DROPPABLE.getTimeout()*2);
+    }
     
     /**
      * Verifies our assumptions whether a Verb can be dropped or not. The tests make use of droppabilty, and
@@ -163,8 +168,8 @@
     private OutboundTcpConnection getOutboundTcpConnectionForLocalhost() throws UnknownHostException
     {
         InetAddress lo = InetAddress.getByName("127.0.0.1");
-        OutboundTcpConnectionPool otcPool = new OutboundTcpConnectionPool(lo);
-        OutboundTcpConnection otc = new OutboundTcpConnection(otcPool);
+        OutboundTcpConnectionPool otcPool = new OutboundTcpConnectionPool(lo, null);
+        OutboundTcpConnection otc = new OutboundTcpConnection(otcPool, "lo-OutboundTcpConnectionTest");
         return otc;
     }
 }
diff --git a/test/unit/org/apache/cassandra/net/RateBasedBackPressureTest.java b/test/unit/org/apache/cassandra/net/RateBasedBackPressureTest.java
new file mode 100644
index 0000000..b94b6ee
--- /dev/null
+++ b/test/unit/org/apache/cassandra/net/RateBasedBackPressureTest.java
@@ -0,0 +1,409 @@
+/*
+* 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.net;
+
+import java.net.InetAddress;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.RateLimiter;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.utils.TestTimeSource;
+import org.apache.cassandra.utils.TimeSource;
+
+import static org.apache.cassandra.net.RateBasedBackPressure.FACTOR;
+import static org.apache.cassandra.net.RateBasedBackPressure.FLOW;
+import static org.apache.cassandra.net.RateBasedBackPressure.HIGH_RATIO;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class RateBasedBackPressureTest
+{
+    @Test(expected = IllegalArgumentException.class)
+    public void testAcceptsNoLessThanThreeArguments() throws Exception
+    {
+        new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "1"), new TestTimeSource(), 10);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testHighRatioMustBeBiggerThanZero() throws Exception
+    {
+        new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "0", FACTOR, "2", FLOW, "FAST"), new TestTimeSource(), 10);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testHighRatioMustBeSmallerEqualThanOne() throws Exception
+    {
+        new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "2", FACTOR, "2", FLOW, "FAST"), new TestTimeSource(), 10);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testFactorMustBeBiggerEqualThanOne() throws Exception
+    {
+        new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "0", FLOW, "FAST"), new TestTimeSource(), 10);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testWindowSizeMustBeBiggerEqualThanTen() throws Exception
+    {
+        new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "5", FLOW, "FAST"), new TestTimeSource(), 1);
+    }
+
+    @Test
+    public void testFlowMustBeEitherFASTorSLOW() throws Exception
+    {
+        new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "1", FLOW, "FAST"), new TestTimeSource(), 10);
+        new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "1", FLOW, "SLOW"), new TestTimeSource(), 10);
+        try
+        {
+            new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "1", FLOW, "WRONG"), new TestTimeSource(), 10);
+            fail("Expected to fail with wrong flow type.");
+        }
+        catch (Exception ex)
+        {
+        }
+    }
+
+    @Test
+    public void testBackPressureStateUpdates()
+    {
+        long windowSize = 6000;
+        TestTimeSource timeSource = new TestTimeSource();
+        RateBasedBackPressure strategy = new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "10", FLOW, "FAST"), timeSource, windowSize);
+
+        RateBasedBackPressureState state = strategy.newState(InetAddress.getLoopbackAddress());
+        state.onMessageSent(null);
+        assertEquals(0, state.incomingRate.size());
+        assertEquals(0, state.outgoingRate.size());
+
+        state = strategy.newState(InetAddress.getLoopbackAddress());
+        state.onResponseReceived();
+        assertEquals(1, state.incomingRate.size());
+        assertEquals(1, state.outgoingRate.size());
+
+        state = strategy.newState(InetAddress.getLoopbackAddress());
+        state.onResponseTimeout();
+        assertEquals(0, state.incomingRate.size());
+        assertEquals(1, state.outgoingRate.size());
+    }
+
+    @Test
+    public void testBackPressureIsNotUpdatedBeyondInfinity() throws Exception
+    {
+        long windowSize = 6000;
+        TestTimeSource timeSource = new TestTimeSource();
+        RateBasedBackPressure strategy = new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "10", FLOW, "FAST"), timeSource, windowSize);
+        RateBasedBackPressureState state = strategy.newState(InetAddress.getLoopbackAddress());
+
+        // Get initial rate:
+        double initialRate = state.rateLimiter.getRate();
+        assertEquals(Double.POSITIVE_INFINITY, initialRate, 0.0);
+
+        // Update incoming and outgoing rate equally:
+        state.incomingRate.update(1);
+        state.outgoingRate.update(1);
+
+        // Move time ahead:
+        timeSource.sleep(windowSize, TimeUnit.MILLISECONDS);
+
+        // Verify the rate doesn't change because already at infinity:
+        strategy.apply(Sets.newHashSet(state), 1, TimeUnit.SECONDS);
+        assertEquals(initialRate, state.rateLimiter.getRate(), 0.0);
+    }
+
+    @Test
+    public void testBackPressureIsUpdatedOncePerWindowSize() throws Exception
+    {
+        long windowSize = 6000;
+        TestTimeSource timeSource = new TestTimeSource();
+        RateBasedBackPressure strategy = new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "10", FLOW, "FAST"), timeSource, windowSize);
+        RateBasedBackPressureState state = strategy.newState(InetAddress.getLoopbackAddress());
+
+        // Get initial time:
+        long current = state.getLastIntervalAcquire();
+        assertEquals(0, current);
+
+        // Update incoming and outgoing rate:
+        state.incomingRate.update(1);
+        state.outgoingRate.update(1);
+
+        // Move time ahead by window size:
+        timeSource.sleep(windowSize, TimeUnit.MILLISECONDS);
+
+        // Verify the timestamp changed:
+        strategy.apply(Sets.newHashSet(state), 1, TimeUnit.SECONDS);
+        current = state.getLastIntervalAcquire();
+        assertEquals(timeSource.currentTimeMillis(), current);
+
+        // Move time ahead by less than interval:
+        long previous = current;
+        timeSource.sleep(windowSize / 2, TimeUnit.MILLISECONDS);
+
+        // Verify the last timestamp didn't change because below the window size:
+        strategy.apply(Sets.newHashSet(state), 1, TimeUnit.SECONDS);
+        current = state.getLastIntervalAcquire();
+        assertEquals(previous, current);
+    }
+
+    @Test
+    public void testBackPressureWhenBelowHighRatio() throws Exception
+    {
+        long windowSize = 6000;
+        TestTimeSource timeSource = new TestTimeSource();
+        RateBasedBackPressure strategy = new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "10", FLOW, "FAST"), timeSource, windowSize);
+        RateBasedBackPressureState state = strategy.newState(InetAddress.getLoopbackAddress());
+
+        // Update incoming and outgoing rate so that the ratio is 0.5:
+        state.incomingRate.update(50);
+        state.outgoingRate.update(100);
+
+        // Move time ahead:
+        timeSource.sleep(windowSize, TimeUnit.MILLISECONDS);
+
+        // Verify the rate is decreased by factor:
+        strategy.apply(Sets.newHashSet(state), 1, TimeUnit.SECONDS);
+        assertEquals(7.4, state.rateLimiter.getRate(), 0.1);
+    }
+
+    @Test
+    public void testBackPressureRateLimiterIsIncreasedAfterGoingAgainAboveHighRatio() throws Exception
+    {
+        long windowSize = 6000;
+        TestTimeSource timeSource = new TestTimeSource();
+        RateBasedBackPressure strategy = new RateBasedBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "10", FLOW, "FAST"), timeSource, windowSize);
+        RateBasedBackPressureState state = strategy.newState(InetAddress.getLoopbackAddress());
+
+        // Update incoming and outgoing rate so that the ratio is 0.5:
+        state.incomingRate.update(50);
+        state.outgoingRate.update(100);
+
+        // Move time ahead:
+        timeSource.sleep(windowSize, TimeUnit.MILLISECONDS);
+
+        // Verify the rate decreased:
+        strategy.apply(Sets.newHashSet(state), 1, TimeUnit.SECONDS);
+        assertEquals(7.4, state.rateLimiter.getRate(), 0.1);
+
+        // Update incoming and outgoing rate back above high rate:
+        state.incomingRate.update(50);
+        state.outgoingRate.update(50);
+
+        // Move time ahead:
+        timeSource.sleep(windowSize, TimeUnit.MILLISECONDS);
+
+        // Verify rate limiter is increased by factor:
+        strategy.apply(Sets.newHashSet(state), 1, TimeUnit.SECONDS);
+        assertEquals(8.25, state.rateLimiter.getRate(), 0.1);
+
+        // Update incoming and outgoing rate to keep it below the limiter rate:
+        state.incomingRate.update(1);
+        state.outgoingRate.update(1);
+
+        // Move time ahead:
+        timeSource.sleep(windowSize, TimeUnit.MILLISECONDS);
+
+        // Verify rate limiter is not increased as already higher than the actual rate:
+        strategy.apply(Sets.newHashSet(state), 1, TimeUnit.SECONDS);
+        assertEquals(8.25, state.rateLimiter.getRate(), 0.1);
+    }
+
+    @Test
+    public void testBackPressureFastFlow() throws Exception
+    {
+        long windowSize = 6000;
+        TestTimeSource timeSource = new TestTimeSource();
+        TestableBackPressure strategy = new TestableBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "10", FLOW, "FAST"), timeSource, windowSize);
+        RateBasedBackPressureState state1 = strategy.newState(InetAddress.getByName("127.0.0.1"));
+        RateBasedBackPressureState state2 = strategy.newState(InetAddress.getByName("127.0.0.2"));
+        RateBasedBackPressureState state3 = strategy.newState(InetAddress.getByName("127.0.0.3"));
+
+        // Update incoming and outgoing rates:
+        state1.incomingRate.update(50);
+        state1.outgoingRate.update(100);
+        state2.incomingRate.update(80); // fast
+        state2.outgoingRate.update(100);
+        state3.incomingRate.update(20);
+        state3.outgoingRate.update(100);
+
+        // Move time ahead:
+        timeSource.sleep(windowSize, TimeUnit.MILLISECONDS);
+
+        // Verify the fast replica rate limiting has been applied:
+        Set<RateBasedBackPressureState> replicaGroup = Sets.newHashSet(state1, state2, state3);
+        strategy.apply(replicaGroup, 1, TimeUnit.SECONDS);
+        assertTrue(strategy.checkAcquired());
+        assertTrue(strategy.checkApplied());
+        assertEquals(12.0, strategy.getRateLimiterForReplicaGroup(replicaGroup).getRate(), 0.1);
+    }
+
+    @Test
+    public void testBackPressureSlowFlow() throws Exception
+    {
+        long windowSize = 6000;
+        TestTimeSource timeSource = new TestTimeSource();
+        TestableBackPressure strategy = new TestableBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "10", FLOW, "SLOW"), timeSource, windowSize);
+        RateBasedBackPressureState state1 = strategy.newState(InetAddress.getByName("127.0.0.1"));
+        RateBasedBackPressureState state2 = strategy.newState(InetAddress.getByName("127.0.0.2"));
+        RateBasedBackPressureState state3 = strategy.newState(InetAddress.getByName("127.0.0.3"));
+
+        // Update incoming and outgoing rates:
+        state1.incomingRate.update(50);
+        state1.outgoingRate.update(100);
+        state2.incomingRate.update(100);
+        state2.outgoingRate.update(100);
+        state3.incomingRate.update(20); // slow
+        state3.outgoingRate.update(100);
+
+        // Move time ahead:
+        timeSource.sleep(windowSize, TimeUnit.MILLISECONDS);
+
+        // Verify the slow replica rate limiting has been applied:
+        Set<RateBasedBackPressureState> replicaGroup = Sets.newHashSet(state1, state2, state3);
+        strategy.apply(replicaGroup, 1, TimeUnit.SECONDS);
+        assertTrue(strategy.checkAcquired());
+        assertTrue(strategy.checkApplied());
+        assertEquals(3.0, strategy.getRateLimiterForReplicaGroup(replicaGroup).getRate(), 0.1);
+    }
+
+    @Test
+    public void testBackPressureWithDifferentGroups() throws Exception
+    {
+        long windowSize = 6000;
+        TestTimeSource timeSource = new TestTimeSource();
+        TestableBackPressure strategy = new TestableBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "10", FLOW, "SLOW"), timeSource, windowSize);
+        RateBasedBackPressureState state1 = strategy.newState(InetAddress.getByName("127.0.0.1"));
+        RateBasedBackPressureState state2 = strategy.newState(InetAddress.getByName("127.0.0.2"));
+        RateBasedBackPressureState state3 = strategy.newState(InetAddress.getByName("127.0.0.3"));
+        RateBasedBackPressureState state4 = strategy.newState(InetAddress.getByName("127.0.0.4"));
+
+        // Update incoming and outgoing rates:
+        state1.incomingRate.update(50); // this
+        state1.outgoingRate.update(100);
+        state2.incomingRate.update(100);
+        state2.outgoingRate.update(100);
+        state3.incomingRate.update(20); // this
+        state3.outgoingRate.update(100);
+        state4.incomingRate.update(80);
+        state4.outgoingRate.update(100);
+
+        // Move time ahead:
+        timeSource.sleep(windowSize, TimeUnit.MILLISECONDS);
+
+        // Verify the first group:
+        Set<RateBasedBackPressureState> replicaGroup = Sets.newHashSet(state1, state2);
+        strategy.apply(replicaGroup, 1, TimeUnit.SECONDS);
+        assertTrue(strategy.checkAcquired());
+        assertTrue(strategy.checkApplied());
+        assertEquals(7.4, strategy.getRateLimiterForReplicaGroup(replicaGroup).getRate(), 0.1);
+
+        // Verify the second group:
+        replicaGroup = Sets.newHashSet(state3, state4);
+        strategy.apply(replicaGroup, 1, TimeUnit.SECONDS);
+        assertTrue(strategy.checkAcquired());
+        assertTrue(strategy.checkApplied());
+        assertEquals(3.0, strategy.getRateLimiterForReplicaGroup(replicaGroup).getRate(), 0.1);
+    }
+
+    @Test
+    public void testBackPressurePastTimeout() throws Exception
+    {
+        long windowSize = 10000;
+        TestTimeSource timeSource = new TestTimeSource();
+        TestableBackPressure strategy = new TestableBackPressure(ImmutableMap.of(HIGH_RATIO, "0.9", FACTOR, "10", FLOW, "SLOW"), timeSource, windowSize);
+        RateBasedBackPressureState state1 = strategy.newState(InetAddress.getByName("127.0.0.1"));
+        RateBasedBackPressureState state2 = strategy.newState(InetAddress.getByName("127.0.0.2"));
+        RateBasedBackPressureState state3 = strategy.newState(InetAddress.getByName("127.0.0.3"));
+
+        // Update incoming and outgoing rates:
+        state1.incomingRate.update(5); // slow
+        state1.outgoingRate.update(100);
+        state2.incomingRate.update(100);
+        state2.outgoingRate.update(100);
+        state3.incomingRate.update(100);
+        state3.outgoingRate.update(100);
+
+        // Move time ahead:
+        timeSource.sleep(windowSize, TimeUnit.MILLISECONDS);
+
+        // Verify the slow replica rate limiting has been applied:
+        Set<RateBasedBackPressureState> replicaGroup = Sets.newHashSet(state1, state2, state3);
+        strategy.apply(replicaGroup, 4, TimeUnit.SECONDS);
+        assertTrue(strategy.checkAcquired());
+        assertTrue(strategy.checkApplied());
+        assertEquals(0.5, strategy.getRateLimiterForReplicaGroup(replicaGroup).getRate(), 0.1);
+
+        // Make one more apply call to saturate the rate limit timeout (0.5 requests per second means 2 requests span
+        // 4 seconds, but we can only make one as we have to subtract the incoming response time):
+        strategy.apply(replicaGroup, 4, TimeUnit.SECONDS);
+
+        // Now verify another call to apply doesn't acquire the rate limit because of the max timeout of 4 seconds minus
+        // 2 seconds of response time, so the time source itself sleeps two second:
+        long start = timeSource.currentTimeMillis();
+        strategy.apply(replicaGroup, 4, TimeUnit.SECONDS);
+        assertFalse(strategy.checkAcquired());
+        assertTrue(strategy.checkApplied());
+        assertEquals(TimeUnit.NANOSECONDS.convert(2, TimeUnit.SECONDS),
+                     strategy.timeout);
+        assertEquals(strategy.timeout,
+                     TimeUnit.NANOSECONDS.convert(timeSource.currentTimeMillis() - start, TimeUnit.MILLISECONDS));
+    }
+
+    public static class TestableBackPressure extends RateBasedBackPressure
+    {
+        public volatile boolean acquired = false;
+        public volatile boolean applied = false;
+        public volatile long timeout;
+
+        public TestableBackPressure(Map<String, Object> args, TimeSource timeSource, long windowSize)
+        {
+            super(args, timeSource, windowSize);
+        }
+
+        @Override
+        public boolean doRateLimit(RateLimiter rateLimiter, long timeoutInNanos)
+        {
+            acquired = super.doRateLimit(rateLimiter, timeoutInNanos);
+            applied = true;
+            timeout = timeoutInNanos;
+            return acquired;
+        }
+
+        public boolean checkAcquired()
+        {
+            boolean checked = acquired;
+            acquired = false;
+            return checked;
+        }
+
+        public boolean checkApplied()
+        {
+            boolean checked = applied;
+            applied = false;
+            return checked;
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/net/WriteCallbackInfoTest.java b/test/unit/org/apache/cassandra/net/WriteCallbackInfoTest.java
index a994a99..70e5add 100644
--- a/test/unit/org/apache/cassandra/net/WriteCallbackInfoTest.java
+++ b/test/unit/org/apache/cassandra/net/WriteCallbackInfoTest.java
@@ -21,10 +21,12 @@
 import java.net.InetAddress;
 import java.util.UUID;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import junit.framework.Assert;
 import org.apache.cassandra.MockSchema;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.BufferDecoratedKey;
 import org.apache.cassandra.db.ConsistencyLevel;
 import org.apache.cassandra.db.Mutation;
@@ -37,6 +39,11 @@
 
 public class WriteCallbackInfoTest
 {
+    @BeforeClass
+    public static void initDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
 
     @Test
     public void testShouldHint() throws Exception
diff --git a/test/unit/org/apache/cassandra/repair/LocalSyncTaskTest.java b/test/unit/org/apache/cassandra/repair/LocalSyncTaskTest.java
index b891296..7837e6e 100644
--- a/test/unit/org/apache/cassandra/repair/LocalSyncTaskTest.java
+++ b/test/unit/org/apache/cassandra/repair/LocalSyncTaskTest.java
@@ -78,7 +78,7 @@
         TreeResponse r2 = new TreeResponse(ep2, tree2);
         LocalSyncTask task = new LocalSyncTask(desc, r1.endpoint, r2.endpoint,
                                                MerkleTrees.difference(r1.trees, r2.trees),
-                                               ActiveRepairService.UNREPAIRED_SSTABLE);
+                                               ActiveRepairService.UNREPAIRED_SSTABLE, false);
         task.run();
 
         assertEquals(0, task.get().numberOfDifferences);
@@ -113,9 +113,9 @@
         // note: we reuse the same endpoint which is bogus in theory but fine here
         TreeResponse r1 = new TreeResponse(InetAddress.getByName("127.0.0.1"), tree1);
         TreeResponse r2 = new TreeResponse(InetAddress.getByName("127.0.0.2"), tree2);
-        LocalSyncTask task = new LocalSyncTask(desc, r1.endpoint, r2.endpoint,
+        LocalSyncTask task = new LocalSyncTask(desc,  r1.endpoint, r2.endpoint,
                                                MerkleTrees.difference(r1.trees, r2.trees),
-                                               ActiveRepairService.UNREPAIRED_SSTABLE);
+                                               ActiveRepairService.UNREPAIRED_SSTABLE, false);
         task.run();
 
         // ensure that the changed range was recorded
diff --git a/test/unit/org/apache/cassandra/repair/RepairJobTest.java b/test/unit/org/apache/cassandra/repair/RepairJobTest.java
index 5269182..979ecc5 100644
--- a/test/unit/org/apache/cassandra/repair/RepairJobTest.java
+++ b/test/unit/org/apache/cassandra/repair/RepairJobTest.java
@@ -94,10 +94,11 @@
 
         private volatile boolean simulateValidationsOutstanding;
 
-        public MeasureableRepairSession(UUID parentRepairSession, UUID id, Collection<Range<Token>> ranges, String keyspace,
-                                        RepairParallelism parallelismDegree, Set<InetAddress> endpoints, long repairedAt, String... cfnames)
+        public MeasureableRepairSession(UUID parentRepairSession, UUID id, Collection<Range<Token>> ranges,
+                                        String keyspace, RepairParallelism parallelismDegree, Set<InetAddress> endpoints,
+                                        long repairedAt, boolean pullRepair, String... cfnames)
         {
-            super(parentRepairSession, id, ranges, keyspace, parallelismDegree, endpoints, repairedAt, cfnames);
+            super(parentRepairSession, id, ranges, keyspace, parallelismDegree, endpoints, repairedAt, pullRepair, cfnames);
         }
 
         // So that threads actually get recycled and we can have accurate memory accounting while testing
@@ -159,7 +160,7 @@
 
         this.session = new MeasureableRepairSession(parentRepairSession, UUIDGen.getTimeUUID(), fullRange,
                                                     KEYSPACE, RepairParallelism.SEQUENTIAL, neighbors,
-                                                    ActiveRepairService.UNREPAIRED_SSTABLE, CF);
+                                                    ActiveRepairService.UNREPAIRED_SSTABLE, false, CF);
 
         this.job = new RepairJob(session, CF);
         this.sessionJobDesc = new RepairJobDesc(session.parentRepairSession, session.getId(),
@@ -315,8 +316,8 @@
                                                     .map(k -> ((RepairMessage) k.payload).messageType)
                                                     .collect(Collectors.toList()));
     }
-    
-    private void assertExpectedDifferences(Collection<RemoteSyncTask> tasks, Integer ... differences)
+
+    private void assertExpectedDifferences(Collection<RemoteSyncTask> tasks, Integer... differences)
     {
         List<Integer> expectedDifferences = new ArrayList<>(Arrays.asList(differences));
         List<Integer> observedDifferences = tasks.stream()
@@ -371,7 +372,7 @@
                                                                   Collections.emptyMap(),
                                                                   MessagingService.Verb.REQUEST_RESPONSE,
                                                                   MessagingService.current_version);
-                        MessagingService.instance().receive(messageIn, id, System.currentTimeMillis(), false);
+                        MessagingService.instance().receive(messageIn, id);
                         break;
                     case VALIDATION_REQUEST:
                         session.validationComplete(sessionJobDesc, to, mockTrees.get(to));
diff --git a/test/unit/org/apache/cassandra/repair/RepairSessionTest.java b/test/unit/org/apache/cassandra/repair/RepairSessionTest.java
index d40982c..f65bedb 100644
--- a/test/unit/org/apache/cassandra/repair/RepairSessionTest.java
+++ b/test/unit/org/apache/cassandra/repair/RepairSessionTest.java
@@ -26,8 +26,10 @@
 import java.util.concurrent.ExecutionException;
 
 import com.google.common.collect.Sets;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.Murmur3Partitioner;
 import org.apache.cassandra.dht.Range;
@@ -42,6 +44,12 @@
 
 public class RepairSessionTest
 {
+    @BeforeClass
+    public static void initDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testConviction() throws Exception
     {
@@ -54,7 +62,7 @@
         IPartitioner p = Murmur3Partitioner.instance;
         Range<Token> repairRange = new Range<>(p.getToken(ByteBufferUtil.bytes(0)), p.getToken(ByteBufferUtil.bytes(100)));
         Set<InetAddress> endpoints = Sets.newHashSet(remote);
-        RepairSession session = new RepairSession(parentSessionId, sessionId, Arrays.asList(repairRange), "Keyspace1", RepairParallelism.SEQUENTIAL, endpoints, ActiveRepairService.UNREPAIRED_SSTABLE, "Standard1");
+        RepairSession session = new RepairSession(parentSessionId, sessionId, Arrays.asList(repairRange), "Keyspace1", RepairParallelism.SEQUENTIAL, endpoints, ActiveRepairService.UNREPAIRED_SSTABLE, false, "Standard1");
 
         // perform convict
         session.convict(remote, Double.MAX_VALUE);
diff --git a/test/unit/org/apache/cassandra/repair/messages/RepairMessageSerializationsTest.java b/test/unit/org/apache/cassandra/repair/messages/RepairMessageSerializationsTest.java
index 5dbed3f..f2dc8c7 100644
--- a/test/unit/org/apache/cassandra/repair/messages/RepairMessageSerializationsTest.java
+++ b/test/unit/org/apache/cassandra/repair/messages/RepairMessageSerializationsTest.java
@@ -49,14 +49,16 @@
 
 public class RepairMessageSerializationsTest
 {
-    private static final int PROTOCOL_VERSION = MessagingService.current_version;
     private static final int GC_BEFORE = 1000000;
 
+    private static int protocolVersion;
     private static IPartitioner originalPartitioner;
 
     @BeforeClass
     public static void before()
     {
+        DatabaseDescriptor.daemonInitialization();
+        protocolVersion = MessagingService.current_version;
         originalPartitioner = StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance);
     }
 
@@ -93,16 +95,16 @@
 
     private <T extends RepairMessage> T serializeRoundTrip(T msg, IVersionedSerializer<T> serializer) throws IOException
     {
-        long size = serializer.serializedSize(msg, PROTOCOL_VERSION);
+        long size = serializer.serializedSize(msg, protocolVersion);
 
         ByteBuffer buf = ByteBuffer.allocate((int)size);
         DataOutputPlus out = new DataOutputBufferFixed(buf);
-        serializer.serialize(msg, out, PROTOCOL_VERSION);
+        serializer.serialize(msg, out, protocolVersion);
         Assert.assertEquals(size, buf.position());
 
         buf.flip();
         DataInputPlus in = new DataInputBuffer(buf, false);
-        T deserialized = serializer.deserialize(in, PROTOCOL_VERSION);
+        T deserialized = serializer.deserialize(in, protocolVersion);
         Assert.assertEquals(msg, deserialized);
         Assert.assertEquals(msg.hashCode(), deserialized.hashCode());
         return deserialized;
diff --git a/test/unit/org/apache/cassandra/repair/messages/RepairOptionTest.java b/test/unit/org/apache/cassandra/repair/messages/RepairOptionTest.java
index b617e96..9fe8b93 100644
--- a/test/unit/org/apache/cassandra/repair/messages/RepairOptionTest.java
+++ b/test/unit/org/apache/cassandra/repair/messages/RepairOptionTest.java
@@ -38,7 +38,10 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.matchers.JUnitMatchers.containsString;
 
 public class RepairOptionTest
 {
@@ -51,7 +54,7 @@
         // parse with empty options
         RepairOption option = RepairOption.parse(new HashMap<String, String>(), partitioner);
 
-        if (FBUtilities.isWindows() && (DatabaseDescriptor.getDiskAccessMode() != Config.DiskAccessMode.standard || DatabaseDescriptor.getIndexAccessMode() != Config.DiskAccessMode.standard))
+        if (FBUtilities.isWindows && (DatabaseDescriptor.getDiskAccessMode() != Config.DiskAccessMode.standard || DatabaseDescriptor.getIndexAccessMode() != Config.DiskAccessMode.standard))
             assertTrue(option.getParallelism() == RepairParallelism.PARALLEL);
         else
             assertTrue(option.getParallelism() == RepairParallelism.SEQUENTIAL);
@@ -59,7 +62,7 @@
         assertFalse(option.isPrimaryRange());
         assertFalse(option.isIncremental());
 
-        // parse everything
+        // parse everything except hosts (hosts cannot be combined with data centers)
         Map<String, String> options = new HashMap<>();
         options.put(RepairOption.PARALLELISM_KEY, "parallel");
         options.put(RepairOption.PRIMARY_RANGE_KEY, "false");
@@ -67,7 +70,6 @@
         options.put(RepairOption.RANGES_KEY, "0:10,11:20,21:30");
         options.put(RepairOption.COLUMNFAMILIES_KEY, "cf1,cf2,cf3");
         options.put(RepairOption.DATACENTERS_KEY, "dc1,dc2,dc3");
-        options.put(RepairOption.HOSTS_KEY, "127.0.0.1,127.0.0.2,127.0.0.3");
 
         option = RepairOption.parse(options, partitioner);
         assertTrue(option.getParallelism() == RepairParallelism.PARALLEL);
@@ -92,6 +94,14 @@
         expectedDCs.add("dc3");
         assertEquals(expectedDCs, option.getDataCenters());
 
+        // expect an error when parsing with hosts as well
+        options.put(RepairOption.HOSTS_KEY, "127.0.0.1,127.0.0.2,127.0.0.3");
+        assertParseThrowsIllegalArgumentExceptionWithMessage(options, "Cannot combine -dc and -hosts options");
+
+        // remove data centers to proceed with testing parsing hosts
+        options.remove(RepairOption.DATACENTERS_KEY);
+        option = RepairOption.parse(options, partitioner);
+
         Set<String> expectedHosts = new HashSet<>(3);
         expectedHosts.add("127.0.0.1");
         expectedHosts.add("127.0.0.2");
@@ -102,7 +112,7 @@
     @Test
     public void testPrWithLocalParseOptions()
     {
-        DatabaseDescriptor.forceStaticInitialization();
+        DatabaseDescriptor.daemonInitialization();
 
         Map<String, String> options = new HashMap<>();
         options.put(RepairOption.PARALLELISM_KEY, "parallel");
@@ -120,6 +130,25 @@
     }
 
     @Test
+    public void testPullRepairParseOptions()
+    {
+        Map<String, String> options = new HashMap<>();
+
+        options.put(RepairOption.PULL_REPAIR_KEY, "true");
+        assertParseThrowsIllegalArgumentExceptionWithMessage(options, "Pull repair can only be performed between two hosts");
+
+        options.put(RepairOption.HOSTS_KEY, "127.0.0.1,127.0.0.2,127.0.0.3");
+        assertParseThrowsIllegalArgumentExceptionWithMessage(options, "Pull repair can only be performed between two hosts");
+
+        options.put(RepairOption.HOSTS_KEY, "127.0.0.1,127.0.0.2");
+        assertParseThrowsIllegalArgumentExceptionWithMessage(options, "Token ranges must be specified when performing pull repair");
+
+        options.put(RepairOption.RANGES_KEY, "0:10");
+        RepairOption option = RepairOption.parse(options, Murmur3Partitioner.instance);
+        assertTrue(option.isPullRepair());
+    }
+
+    @Test
     public void testIncrementalRepairWithSubrangesIsNotGlobal() throws Exception
     {
         RepairOption ro = RepairOption.parse(ImmutableMap.of(RepairOption.INCREMENTAL_KEY, "true", RepairOption.RANGES_KEY, "41:42"),
@@ -129,4 +158,17 @@
                 Murmur3Partitioner.instance);
         assertTrue(ro.isGlobal());
     }
+
+    private void assertParseThrowsIllegalArgumentExceptionWithMessage(Map<String, String> optionsToParse, String expectedErrorMessage)
+    {
+        try
+        {
+            RepairOption.parse(optionsToParse, Murmur3Partitioner.instance);
+            fail(String.format("Expected RepairOption.parse() to throw an IllegalArgumentException containing the message '%s'", expectedErrorMessage));
+        }
+        catch (IllegalArgumentException ex)
+        {
+            assertThat(ex.getMessage(), containsString(expectedErrorMessage));
+        }
+    }
 }
diff --git a/test/unit/org/apache/cassandra/schema/DefsTest.java b/test/unit/org/apache/cassandra/schema/DefsTest.java
index e9980f6..d4ac1dc 100644
--- a/test/unit/org/apache/cassandra/schema/DefsTest.java
+++ b/test/unit/org/apache/cassandra/schema/DefsTest.java
@@ -498,7 +498,7 @@
     public void testDropIndex() throws ConfigurationException
     {
         // persist keyspace definition in the system keyspace
-        SchemaKeyspace.makeCreateKeyspaceMutation(Schema.instance.getKSMetaData(KEYSPACE6), FBUtilities.timestampMicros()).applyUnsafe();
+        SchemaKeyspace.makeCreateKeyspaceMutation(Schema.instance.getKSMetaData(KEYSPACE6), FBUtilities.timestampMicros()).build().applyUnsafe();
         ColumnFamilyStore cfs = Keyspace.open(KEYSPACE6).getColumnFamilyStore(TABLE1i);
         String indexName = "birthdate_key_index";
 
diff --git a/test/unit/org/apache/cassandra/schema/IndexMetadataTest.java b/test/unit/org/apache/cassandra/schema/IndexMetadataTest.java
new file mode 100644
index 0000000..785ed73
--- /dev/null
+++ b/test/unit/org/apache/cassandra/schema/IndexMetadataTest.java
@@ -0,0 +1,56 @@
+/*
+ *
+ * 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.schema;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class IndexMetadataTest {
+    
+    @Test
+    public void testIsNameValidPositive()
+    {
+        assertTrue(IndexMetadata.isNameValid("abcdefghijklmnopqrstuvwxyz"));
+        assertTrue(IndexMetadata.isNameValid("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+        assertTrue(IndexMetadata.isNameValid("_01234567890"));
+    }
+    
+    @Test
+    public void testIsNameValidNegative()
+    {
+        assertFalse(IndexMetadata.isNameValid(null));
+        assertFalse(IndexMetadata.isNameValid(""));
+        assertFalse(IndexMetadata.isNameValid(" "));
+        assertFalse(IndexMetadata.isNameValid("@"));
+        assertFalse(IndexMetadata.isNameValid("!"));
+    }
+    
+    @Test
+    public void testGetDefaultIndexName()
+    {
+        Assert.assertEquals("aB4__idx", IndexMetadata.getDefaultIndexName("a B-4@!_+", null));
+        Assert.assertEquals("34_Ddd_F6_idx", IndexMetadata.getDefaultIndexName("34_()Ddd", "#F%6*"));
+        
+    }
+}
diff --git a/test/unit/org/apache/cassandra/schema/LegacySchemaMigratorTest.java b/test/unit/org/apache/cassandra/schema/LegacySchemaMigratorTest.java
index 7643456..573d109 100644
--- a/test/unit/org/apache/cassandra/schema/LegacySchemaMigratorTest.java
+++ b/test/unit/org/apache/cassandra/schema/LegacySchemaMigratorTest.java
@@ -30,12 +30,15 @@
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.FieldIdentifier;
 import org.apache.cassandra.cql3.functions.*;
 import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.rows.Row;
 import org.apache.cassandra.db.marshal.*;
-import org.apache.cassandra.index.internal.CassandraIndex;
+import org.apache.cassandra.index.TargetParser;
 import org.apache.cassandra.thrift.ThriftConversion;
 import org.apache.cassandra.utils.*;
 
@@ -83,13 +86,13 @@
         // verify that nothing's left in the old schema tables
         for (CFMetaData table : LegacySchemaMigrator.LegacySchemaTables)
         {
-            String query = format("SELECT * FROM %s.%s", SystemKeyspace.NAME, table.cfName);
+            String query = format("SELECT * FROM %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, table.cfName);
             //noinspection ConstantConditions
             assertTrue(executeOnceInternal(query).isEmpty());
         }
 
         // make sure that we've read *exactly* the same set of keyspaces/tables/types/functions
-        assertEquals(expected, actual);
+        assertEquals(expected.diff(actual).toString(), expected, actual);
 
         // check that the build status of all indexes has been updated to use the new
         // format of index name: the index_name column of system.IndexInfo used to
@@ -122,9 +125,14 @@
         }
     }
 
+    private static FieldIdentifier field(String field)
+    {
+        return FieldIdentifier.forQuoted(field);
+    }
+
     private static void loadLegacySchemaTables()
     {
-        KeyspaceMetadata systemKeyspace = Schema.instance.getKSMetaData(SystemKeyspace.NAME);
+        KeyspaceMetadata systemKeyspace = Schema.instance.getKSMetaData(SchemaConstants.SYSTEM_KEYSPACE_NAME);
 
         Tables systemTables = systemKeyspace.tables;
         for (CFMetaData table : LegacySchemaMigrator.LegacySchemaTables)
@@ -326,18 +334,21 @@
 
         UserType udt1 = new UserType(keyspace,
                                      bytes("udt1"),
-                                     new ArrayList<ByteBuffer>() {{ add(bytes("col1")); add(bytes("col2")); }},
-                                     new ArrayList<AbstractType<?>>() {{ add(UTF8Type.instance); add(Int32Type.instance); }});
+                                     new ArrayList<FieldIdentifier>() {{ add(field("col1")); add(field("col2")); }},
+                                     new ArrayList<AbstractType<?>>() {{ add(UTF8Type.instance); add(Int32Type.instance); }},
+                                     true);
 
         UserType udt2 = new UserType(keyspace,
                                      bytes("udt2"),
-                                     new ArrayList<ByteBuffer>() {{ add(bytes("col3")); add(bytes("col4")); }},
-                                     new ArrayList<AbstractType<?>>() {{ add(BytesType.instance); add(BooleanType.instance); }});
+                                     new ArrayList<FieldIdentifier>() {{ add(field("col3")); add(field("col4")); }},
+                                     new ArrayList<AbstractType<?>>() {{ add(BytesType.instance); add(BooleanType.instance); }},
+                                     true);
 
         UserType udt3 = new UserType(keyspace,
                                      bytes("udt3"),
-                                     new ArrayList<ByteBuffer>() {{ add(bytes("col5")); }},
-                                     new ArrayList<AbstractType<?>>() {{ add(AsciiType.instance); }});
+                                     new ArrayList<FieldIdentifier>() {{ add(field("col5")); }},
+                                     new ArrayList<AbstractType<?>>() {{ add(AsciiType.instance); }},
+                                     true);
 
         return KeyspaceMetadata.create(keyspace,
                                        KeyspaceParams.simple(1),
@@ -446,13 +457,15 @@
 
         UserType udt1 = new UserType(keyspace,
                                      bytes("udt1"),
-                                     new ArrayList<ByteBuffer>() {{ add(bytes("col1")); add(bytes("col2")); }},
-                                     new ArrayList<AbstractType<?>>() {{ add(UTF8Type.instance); add(Int32Type.instance); }});
+                                     new ArrayList<FieldIdentifier>() {{ add(field("col1")); add(field("col2")); }},
+                                     new ArrayList<AbstractType<?>>() {{ add(UTF8Type.instance); add(Int32Type.instance); }},
+                                     true);
 
         UserType udt2 = new UserType(keyspace,
                                      bytes("udt2"),
-                                     new ArrayList<ByteBuffer>() {{ add(bytes("col1")); add(bytes("col2")); }},
-                                     new ArrayList<AbstractType<?>>() {{ add(ListType.getInstance(udt1, false)); add(Int32Type.instance); }});
+                                     new ArrayList<FieldIdentifier>() {{ add(field("col1")); add(field("col2")); }},
+                                     new ArrayList<AbstractType<?>>() {{ add(ListType.getInstance(udt1, false)); add(Int32Type.instance); }},
+                                     true);
 
         UDFunction udf1 = UDFunction.create(new FunctionName(keyspace, "udf"),
                                             ImmutableList.of(new ColumnIdentifier("col1", false), new ColumnIdentifier("col2", false)),
@@ -493,13 +506,15 @@
 
         UserType udt1 = new UserType(keyspace,
                                      bytes("udt1"),
-                                     new ArrayList<ByteBuffer>() {{ add(bytes("col1")); add(bytes("col2")); }},
-                                     new ArrayList<AbstractType<?>>() {{ add(UTF8Type.instance); add(Int32Type.instance); }});
+                                     new ArrayList<FieldIdentifier>() {{ add(field("col1")); add(field("col2")); }},
+                                     new ArrayList<AbstractType<?>>() {{ add(UTF8Type.instance); add(Int32Type.instance); }},
+                                     true);
 
         UserType udt2 = new UserType(keyspace,
                                      bytes("udt2"),
-                                     new ArrayList<ByteBuffer>() {{ add(bytes("col1")); add(bytes("col2")); }},
-                                     new ArrayList<AbstractType<?>>() {{ add(ListType.getInstance(udt1, false)); add(Int32Type.instance); }});
+                                     new ArrayList<FieldIdentifier>() {{ add(field("col1")); add(field("col2")); }},
+                                     new ArrayList<AbstractType<?>>() {{ add(ListType.getInstance(udt1, false)); add(Int32Type.instance); }},
+                                     true);
 
         UDFunction udf1 = UDFunction.create(new FunctionName(keyspace, "udf1"),
                                             ImmutableList.of(new ColumnIdentifier("col1", false), new ColumnIdentifier("col2", false)),
@@ -567,35 +582,40 @@
         setLegacyIndexStatus(keyspace);
     }
 
+    private static DecoratedKey decorate(CFMetaData metadata, Object value)
+    {
+        return metadata.decorateKey(((AbstractType)metadata.getKeyValidator()).decompose(value));
+    }
+
     private static Mutation makeLegacyCreateKeyspaceMutation(KeyspaceMetadata keyspace, long timestamp)
     {
-        // Note that because Keyspaces is a COMPACT TABLE, we're really only setting static columns internally and shouldn't set any clustering.
-        RowUpdateBuilder adder = new RowUpdateBuilder(SystemKeyspace.LegacyKeyspaces, timestamp, keyspace.name);
+        Mutation.SimpleBuilder builder = Mutation.simpleBuilder(SchemaConstants.SYSTEM_KEYSPACE_NAME, decorate(SystemKeyspace.LegacyKeyspaces, keyspace.name))
+                                                 .timestamp(timestamp);
 
-        adder.add("durable_writes", keyspace.params.durableWrites)
-             .add("strategy_class", keyspace.params.replication.klass.getName())
-             .add("strategy_options", json(keyspace.params.replication.options));
+        builder.update(SystemKeyspace.LegacyKeyspaces)
+               .row()
+               .add("durable_writes", keyspace.params.durableWrites)
+               .add("strategy_class", keyspace.params.replication.klass.getName())
+               .add("strategy_options", json(keyspace.params.replication.options));
 
-        Mutation mutation = adder.build();
+        keyspace.tables.forEach(table -> addTableToSchemaMutation(table, true, builder));
+        keyspace.types.forEach(type -> addTypeToSchemaMutation(type, builder));
+        keyspace.functions.udfs().forEach(udf -> addFunctionToSchemaMutation(udf, builder));
+        keyspace.functions.udas().forEach(uda -> addAggregateToSchemaMutation(uda, builder));
 
-        keyspace.tables.forEach(table -> addTableToSchemaMutation(table, timestamp, true, mutation));
-        keyspace.types.forEach(type -> addTypeToSchemaMutation(type, timestamp, mutation));
-        keyspace.functions.udfs().forEach(udf -> addFunctionToSchemaMutation(udf, timestamp, mutation));
-        keyspace.functions.udas().forEach(uda -> addAggregateToSchemaMutation(uda, timestamp, mutation));
-
-        return mutation;
+        return builder.build();
     }
 
     /*
      * Serializing tables
      */
 
-    private static void addTableToSchemaMutation(CFMetaData table, long timestamp, boolean withColumnsAndTriggers, Mutation mutation)
+    private static void addTableToSchemaMutation(CFMetaData table, boolean withColumnsAndTriggers, Mutation.SimpleBuilder builder)
     {
         // For property that can be null (and can be changed), we insert tombstones, to make sure
         // we don't keep a property the user has removed
-        RowUpdateBuilder adder = new RowUpdateBuilder(SystemKeyspace.LegacyColumnfamilies, timestamp, mutation)
-                                 .clustering(table.cfName);
+        Row.SimpleBuilder adder = builder.update(SystemKeyspace.LegacyColumnfamilies)
+                                         .row(table.cfName);
 
         adder.add("cf_id", table.cfId)
              .add("type", table.isSuper() ? "Super" : "Standard");
@@ -628,12 +648,14 @@
              .add("read_repair_chance", table.params.readRepairChance)
              .add("speculative_retry", table.params.speculativeRetry.toString());
 
+        Map<String, Long> dropped = new HashMap<>();
         for (Map.Entry<ByteBuffer, CFMetaData.DroppedColumn> entry : table.getDroppedColumns().entrySet())
         {
             String name = UTF8Type.instance.getString(entry.getKey());
             CFMetaData.DroppedColumn column = entry.getValue();
-            adder.addMapEntry("dropped_columns", name, column.droppedTime);
+            dropped.put(name, column.droppedTime);
         }
+        adder.add("dropped_columns", dropped);
 
         adder.add("is_dense", table.isDense());
 
@@ -642,13 +664,11 @@
         if (withColumnsAndTriggers)
         {
             for (ColumnDefinition column : table.allColumns())
-                addColumnToSchemaMutation(table, column, timestamp, mutation);
+                addColumnToSchemaMutation(table, column, builder);
 
             for (TriggerMetadata trigger : table.getTriggers())
-                addTriggerToSchemaMutation(table, trigger, timestamp, mutation);
+                addTriggerToSchemaMutation(table, trigger, builder);
         }
-
-        adder.build();
     }
 
     private static String cachingToString(CachingParams caching)
@@ -658,14 +678,14 @@
                       caching.rowsPerPartitionAsString());
     }
 
-    private static void addColumnToSchemaMutation(CFMetaData table, ColumnDefinition column, long timestamp, Mutation mutation)
+    private static void addColumnToSchemaMutation(CFMetaData table, ColumnDefinition column, Mutation.SimpleBuilder builder)
     {
         // We need to special case pk-only dense tables. See CASSANDRA-9874.
         String name = table.isDense() && column.kind == ColumnDefinition.Kind.REGULAR && column.type instanceof EmptyType
                     ? ""
                     : column.name.toString();
 
-        final RowUpdateBuilder adder = new RowUpdateBuilder(SystemKeyspace.LegacyColumns, timestamp, mutation).clustering(table.cfName, name);
+        final Row.SimpleBuilder adder = builder.update(SystemKeyspace.LegacyColumns).row(table.cfName, name);
 
         adder.add("validator", column.type.toString())
              .add("type", serializeKind(column.kind, table.isDense()))
@@ -685,8 +705,6 @@
             adder.add("index_type", null);
             adder.add("index_options", null);
         }
-
-        adder.build();
     }
 
     private static Optional<IndexMetadata> findIndexForColumn(Indexes indexes,
@@ -697,7 +715,7 @@
         // index targets can be parsed by CassandraIndex.parseTarget
         // which should be true for any pre-3.0 index
         for (IndexMetadata index : indexes)
-          if (CassandraIndex.parseTarget(table, index).left.equals(column))
+          if (TargetParser.parse(table, index).left.equals(column))
                 return Optional.of(index);
 
         return Optional.empty();
@@ -715,71 +733,67 @@
         return kind.toString().toLowerCase();
     }
 
-    private static void addTriggerToSchemaMutation(CFMetaData table, TriggerMetadata trigger, long timestamp, Mutation mutation)
+    private static void addTriggerToSchemaMutation(CFMetaData table, TriggerMetadata trigger, Mutation.SimpleBuilder builder)
     {
-        new RowUpdateBuilder(SystemKeyspace.LegacyTriggers, timestamp, mutation)
-            .clustering(table.cfName, trigger.name)
-            .addMapEntry("trigger_options", "class", trigger.classOption)
-            .build();
+        builder.update(SystemKeyspace.LegacyTriggers)
+               .row(table.cfName, trigger.name)
+               .add("trigger_options", Collections.singletonMap("class", trigger.classOption));
     }
 
     /*
      * Serializing types
      */
 
-    private static void addTypeToSchemaMutation(UserType type, long timestamp, Mutation mutation)
+    private static void addTypeToSchemaMutation(UserType type, Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder adder = new RowUpdateBuilder(SystemKeyspace.LegacyUsertypes, timestamp, mutation)
-                                 .clustering(type.getNameAsString());
+        Row.SimpleBuilder adder = builder.update(SystemKeyspace.LegacyUsertypes)
+                                         .row(type.getNameAsString());
 
-        adder.resetCollection("field_names")
-             .resetCollection("field_types");
-
+        List<String> names = new ArrayList<>();
+        List<String> types = new ArrayList<>();
         for (int i = 0; i < type.size(); i++)
         {
-            adder.addListEntry("field_names", type.fieldName(i))
-                 .addListEntry("field_types", type.fieldType(i).toString());
+            names.add(type.fieldName(i).toString());
+            types.add(type.fieldType(i).toString());
         }
 
-        adder.build();
+        adder.add("field_names", names)
+             .add("field_types", types);
     }
 
     /*
      * Serializing functions
      */
 
-    private static void addFunctionToSchemaMutation(UDFunction function, long timestamp, Mutation mutation)
+    private static void addFunctionToSchemaMutation(UDFunction function, Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder adder = new RowUpdateBuilder(SystemKeyspace.LegacyFunctions, timestamp, mutation)
-                                 .clustering(function.name().name, functionSignatureWithTypes(function));
+        Row.SimpleBuilder adder = builder.update(SystemKeyspace.LegacyFunctions)
+                                         .row(function.name().name, functionSignatureWithTypes(function));
 
         adder.add("body", function.body())
              .add("language", function.language())
              .add("return_type", function.returnType().toString())
              .add("called_on_null_input", function.isCalledOnNullInput());
 
-        adder.resetCollection("argument_names")
-             .resetCollection("argument_types");
-
+        List<ByteBuffer> names = new ArrayList<>();
+        List<String> types = new ArrayList<>();
         for (int i = 0; i < function.argNames().size(); i++)
         {
-            adder.addListEntry("argument_names", function.argNames().get(i).bytes)
-                 .addListEntry("argument_types", function.argTypes().get(i).toString());
+            names.add(function.argNames().get(i).bytes);
+            types.add(function.argTypes().get(i).toString());
         }
-
-        adder.build();
+        adder.add("argument_names", names)
+             .add("argument_types", types);
     }
 
     /*
      * Serializing aggregates
      */
 
-    private static void addAggregateToSchemaMutation(UDAggregate aggregate, long timestamp, Mutation mutation)
+    private static void addAggregateToSchemaMutation(UDAggregate aggregate, Mutation.SimpleBuilder builder)
     {
-        RowUpdateBuilder adder = new RowUpdateBuilder(SystemKeyspace.LegacyAggregates, timestamp, mutation)
-                                 .clustering(aggregate.name().name, functionSignatureWithTypes(aggregate));
-
-        adder.resetCollection("argument_types");
+        Row.SimpleBuilder adder = builder.update(SystemKeyspace.LegacyAggregates)
+                                 .row(aggregate.name().name, functionSignatureWithTypes(aggregate));
 
         adder.add("return_type", aggregate.returnType().toString())
              .add("state_func", aggregate.stateFunction().name().name);
@@ -791,10 +805,11 @@
         if (aggregate.initialCondition() != null)
             adder.add("initcond", aggregate.initialCondition());
 
+        List<String> types = new ArrayList<>();
         for (AbstractType<?> argType : aggregate.argTypes())
-            adder.addListEntry("argument_types", argType.toString());
+            types.add(argType.toString());
 
-        adder.build();
+        adder.add("argument_types", types);
     }
 
     // We allow method overloads, so a function is not uniquely identified by its name only, but
diff --git a/test/unit/org/apache/cassandra/schema/SchemaKeyspaceTest.java b/test/unit/org/apache/cassandra/schema/SchemaKeyspaceTest.java
index f76fc4f..34590d6 100644
--- a/test/unit/org/apache/cassandra/schema/SchemaKeyspaceTest.java
+++ b/test/unit/org/apache/cassandra/schema/SchemaKeyspaceTest.java
@@ -19,22 +19,34 @@
 package org.apache.cassandra.schema;
 
 import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
 import com.google.common.collect.ImmutableMap;
 
 import org.junit.BeforeClass;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.db.ColumnFamilyStore;
@@ -45,17 +57,25 @@
 import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.db.rows.UnfilteredRowIterators;
 import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.thrift.CfDef;
 import org.apache.cassandra.thrift.ColumnDef;
 import org.apache.cassandra.thrift.IndexType;
 import org.apache.cassandra.thrift.ThriftConversion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.Pair;
+import org.jboss.byteman.contrib.bmunit.BMRule;
+import org.jboss.byteman.contrib.bmunit.BMUnitRunner;
 
 import static org.apache.cassandra.cql3.QueryProcessor.executeOnceInternal;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+@RunWith(BMUnitRunner.class)
 public class SchemaKeyspaceTest
 {
     private static final String KEYSPACE1 = "CFMetaDataTest1";
@@ -83,6 +103,83 @@
                                     SchemaLoader.standardCFMD(KEYSPACE1, CF_STANDARD1));
     }
 
+    /** See CASSANDRA-16856. Make sure schema pulls are synchronized to prevent concurrent schema pull/writes
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSchemaPullSynchoricity() throws Exception
+    {
+        for (String methodName : Arrays.asList("convertSchemaToMutations",
+                                               "truncate",
+                                               "saveSystemKeyspacesSchema"))
+        {
+            Method method = SchemaKeyspace.class.getDeclaredMethod(methodName);
+            assertTrue(methodName + " is not thread-safe", Modifier.isSynchronized(method.getModifiers()));
+        }
+
+        Method method = SchemaKeyspace.class.getDeclaredMethod("calculateSchemaDigest", Set.class);
+        assertTrue(Modifier.isSynchronized(method.getModifiers()));
+        method = SchemaKeyspace.class.getDeclaredMethod("mergeSchemaAndAnnounceVersion", Collection.class);
+        assertTrue(Modifier.isSynchronized(method.getModifiers()));
+        method = SchemaKeyspace.class.getDeclaredMethod("mergeSchema", Collection.class);
+        assertTrue(Modifier.isSynchronized(method.getModifiers()));
+        method = SchemaKeyspace.class.getDeclaredMethod("mergeSchema", Keyspaces.class, Keyspaces.class);
+        assertTrue(Modifier.isSynchronized(method.getModifiers()));        
+    }
+
+    /** See CASSANDRA-16856/16996. Make sure schema pulls are synchronized to prevent concurrent schema pull/writes */
+    @Test
+    @BMRule(name = "delay partition updates to schema tables",
+            targetClass = "ColumnFamilyStore",
+            targetMethod = "apply",
+            action = "Thread.sleep(5000);",
+            targetLocation = "AT EXIT")
+    public void testNoVisiblePartialSchemaUpdates() throws Exception
+    {
+        String keyspace = "sandbox";
+        ExecutorService pool = Executors.newFixedThreadPool(2);
+
+        SchemaKeyspace.truncate(); // Make sure there's nothing but the create we're about to do
+        CyclicBarrier barrier = new CyclicBarrier(2);
+
+        Future<Void> creation = pool.submit(() -> {
+            barrier.await();
+            createTable(keyspace, "CREATE TABLE test (a text primary key, b int, c int)");
+            return null;
+        });
+
+        Future<Collection<Mutation>> mutationsFromThread = pool.submit(() -> {
+            barrier.await();
+
+            // Make sure we actually have a mutation to check for partial modification.
+            Collection<Mutation> mutations = SchemaKeyspace.convertSchemaToMutations();
+            while (mutations.size() == 0)
+                mutations = SchemaKeyspace.convertSchemaToMutations();
+
+            return mutations;
+        });
+
+        creation.get(); // make sure the creation is finished
+
+        Collection<Mutation> mutationsFromConcurrentAccess = mutationsFromThread.get();
+        Collection<Mutation> settledMutations = SchemaKeyspace.convertSchemaToMutations();
+
+        // If the worker thread picked up the creation at all, it should have the same modifications.
+        // In other words, we should see all modifications or none.
+        if (mutationsFromConcurrentAccess.size() == settledMutations.size())
+        {
+            assertEquals(1, settledMutations.size());
+            Mutation mutationFromConcurrentAccess = mutationsFromConcurrentAccess.iterator().next();
+            Mutation settledMutation = settledMutations.iterator().next();
+
+            assertEquals("Read partial schema change!",
+                         settledMutation.getColumnFamilyIds(), mutationFromConcurrentAccess.getColumnFamilyIds());
+        }
+
+        pool.shutdownNow();
+    }
+
     @Test
     public void testThriftConversion() throws Exception
     {
@@ -165,7 +262,7 @@
     private static void updateTable(String keyspace, CFMetaData oldTable, CFMetaData newTable)
     {
         KeyspaceMetadata ksm = Schema.instance.getKeyspaceInstance(keyspace).getMetadata();
-        Mutation mutation = SchemaKeyspace.makeUpdateTableMutation(ksm, oldTable, newTable, FBUtilities.timestampMicros());
+        Mutation mutation = SchemaKeyspace.makeUpdateTableMutation(ksm, oldTable, newTable, FBUtilities.timestampMicros()).build();
         SchemaKeyspace.mergeSchema(Collections.singleton(mutation));
     }
 
@@ -174,7 +271,7 @@
         CFMetaData table = CFMetaData.compile(cql, keyspace);
 
         KeyspaceMetadata ksm = KeyspaceMetadata.create(keyspace, KeyspaceParams.simple(1), Tables.of(table));
-        Mutation mutation = SchemaKeyspace.makeCreateTableMutation(ksm, table, FBUtilities.timestampMicros());
+        Mutation mutation = SchemaKeyspace.makeCreateTableMutation(ksm, table, FBUtilities.timestampMicros()).build();
         SchemaKeyspace.mergeSchema(Collections.singleton(mutation));
     }
 
@@ -188,16 +285,16 @@
         assert before.equals(after) : String.format("%n%s%n!=%n%s", before, after);
 
         // Test schema conversion
-        Mutation rm = SchemaKeyspace.makeCreateTableMutation(keyspace, cfm, FBUtilities.timestampMicros());
-        PartitionUpdate serializedCf = rm.getPartitionUpdate(Schema.instance.getId(SchemaKeyspace.NAME, SchemaKeyspace.TABLES));
-        PartitionUpdate serializedCD = rm.getPartitionUpdate(Schema.instance.getId(SchemaKeyspace.NAME, SchemaKeyspace.COLUMNS));
+        Mutation rm = SchemaKeyspace.makeCreateTableMutation(keyspace, cfm, FBUtilities.timestampMicros()).build();
+        PartitionUpdate serializedCf = rm.getPartitionUpdate(Schema.instance.getId(SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TABLES));
+        PartitionUpdate serializedCD = rm.getPartitionUpdate(Schema.instance.getId(SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.COLUMNS));
 
-        UntypedResultSet.Row tableRow = QueryProcessor.resultify(String.format("SELECT * FROM %s.%s", SchemaKeyspace.NAME, SchemaKeyspace.TABLES),
+        UntypedResultSet.Row tableRow = QueryProcessor.resultify(String.format("SELECT * FROM %s.%s", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TABLES),
                                                                  UnfilteredRowIterators.filter(serializedCf.unfilteredIterator(), FBUtilities.nowInSeconds()))
                                                       .one();
         TableParams params = SchemaKeyspace.createTableParamsFromRow(tableRow);
 
-        UntypedResultSet columnsRows = QueryProcessor.resultify(String.format("SELECT * FROM %s.%s", SchemaKeyspace.NAME, SchemaKeyspace.COLUMNS),
+        UntypedResultSet columnsRows = QueryProcessor.resultify(String.format("SELECT * FROM %s.%s", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.COLUMNS),
                                                                 UnfilteredRowIterators.filter(serializedCD.unfilteredIterator(), FBUtilities.nowInSeconds()));
         Set<ColumnDefinition> columns = new HashSet<>();
         for (UntypedResultSet.Row row : columnsRows)
@@ -207,6 +304,100 @@
         assertEquals(new HashSet<>(cfm.allColumns()), columns);
     }
 
+    private static boolean hasCDC(Mutation m)
+    {
+        for (PartitionUpdate p : m.getPartitionUpdates())
+        {
+            for (ColumnDefinition cd : p.columns())
+            {
+                if (cd.name.toString().equals("cdc"))
+                    return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean hasSchemaTables(Mutation m)
+    {
+        for (PartitionUpdate p : m.getPartitionUpdates())
+        {
+            if (p.metadata().cfName.equals(SchemaKeyspaceTables.TABLES))
+                return true;
+        }
+        return false;
+    }
+
+    @Test
+    public void testConvertSchemaToMutationsWithoutCDC() throws IOException
+    {
+        boolean oldCDCOption = DatabaseDescriptor.isCDCEnabled();
+        try
+        {
+            DatabaseDescriptor.setCDCEnabled(false);
+            Collection<Mutation> mutations = SchemaKeyspace.convertSchemaToMutations();
+            boolean foundTables = false;
+            for (Mutation m : mutations)
+            {
+                if (hasSchemaTables(m))
+                {
+                    foundTables = true;
+                    assertFalse(hasCDC(m));
+                    try (DataOutputBuffer output = new DataOutputBuffer())
+                    {
+                        Mutation.serializer.serialize(m, output, MessagingService.current_version);
+                        try (DataInputBuffer input = new DataInputBuffer(output.getData()))
+                        {
+                            Mutation out = Mutation.serializer.deserialize(input, MessagingService.current_version);
+                            assertFalse(hasCDC(out));
+                        }
+                    }
+                }
+            }
+            assertTrue(foundTables);
+        }
+        finally
+        {
+            DatabaseDescriptor.setCDCEnabled(oldCDCOption);
+        }
+    }
+
+    @Test
+    public void testConvertSchemaToMutationsWithCDC()
+    {
+        boolean oldCDCOption = DatabaseDescriptor.isCDCEnabled();
+        try
+        {
+            DatabaseDescriptor.setCDCEnabled(true);
+            Collection<Mutation> mutations = SchemaKeyspace.convertSchemaToMutations();
+            boolean foundTables = false;
+            for (Mutation m : mutations)
+            {
+                if (hasSchemaTables(m))
+                {
+                    foundTables = true;
+                    assertTrue(hasCDC(m));
+                }
+            }
+            assertTrue(foundTables);
+        }
+        finally
+        {
+            DatabaseDescriptor.setCDCEnabled(oldCDCOption);
+        }
+    }
+
+    @Test
+    public void testSchemaDigest()
+    {
+        Set<ByteBuffer> abc = Collections.singleton(ByteBufferUtil.bytes("abc"));
+        Pair<UUID, UUID> versions = SchemaKeyspace.calculateSchemaDigest(abc);
+        assertTrue(versions.left.equals(versions.right));
+
+        Set<ByteBuffer> cdc = Collections.singleton(ByteBufferUtil.bytes("cdc"));
+        versions = SchemaKeyspace.calculateSchemaDigest(cdc);
+        assertFalse(versions.left.equals(versions.right));
+    }
+
     @Test(expected = SchemaKeyspace.MissingColumns.class)
     public void testSchemaNoPartition()
     {
@@ -216,7 +407,7 @@
                                     KeyspaceParams.simple(1),
                                     SchemaLoader.standardCFMD(testKS, testTable));
         // Delete partition column in the schema
-        String query = String.format("DELETE FROM %s.%s WHERE keyspace_name=? and table_name=? and column_name=?", SchemaKeyspace.NAME, SchemaKeyspace.COLUMNS);
+        String query = String.format("DELETE FROM %s.%s WHERE keyspace_name=? and table_name=? and column_name=?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.COLUMNS);
         executeOnceInternal(query, testKS, testTable, "key");
         SchemaKeyspace.fetchNonSystemKeyspaces();
     }
@@ -230,7 +421,7 @@
                                     KeyspaceParams.simple(1),
                                     SchemaLoader.standardCFMD(testKS, testTable));
         // Delete all colmns in the schema
-        String query = String.format("DELETE FROM %s.%s WHERE keyspace_name=? and table_name=?", SchemaKeyspace.NAME, SchemaKeyspace.COLUMNS);
+        String query = String.format("DELETE FROM %s.%s WHERE keyspace_name=? and table_name=?", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.COLUMNS);
         executeOnceInternal(query, testKS, testTable);
         SchemaKeyspace.fetchNonSystemKeyspaces();
     }
diff --git a/test/unit/org/apache/cassandra/schema/TupleTypesRepresentationTest.java b/test/unit/org/apache/cassandra/schema/TupleTypesRepresentationTest.java
new file mode 100644
index 0000000..36cba3c
--- /dev/null
+++ b/test/unit/org/apache/cassandra/schema/TupleTypesRepresentationTest.java
@@ -0,0 +1,404 @@
+/*
+ * 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.schema;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQL3Type;
+import org.apache.cassandra.cql3.CQLFragmentParser;
+import org.apache.cassandra.cql3.CqlParser;
+import org.apache.cassandra.cql3.FieldIdentifier;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.TypeParser;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Verifies that the string representations of {@link AbstractType} and {@link CQL3Type} are as expected and compatible.
+ *
+ * C* 3.0 is known to <em>not</em> enclose a frozen UDT in a "frozen bracket" in the {@link AbstractType}.
+ * The string representation of a frozuen UDT using the {@link CQL3Type} type hierarchy is correct in C* 3.0.
+ */
+public class TupleTypesRepresentationTest
+{
+    static
+    {
+        DatabaseDescriptor.toolInitialization();
+        DatabaseDescriptor.applyAddressConfig();
+    }
+
+    private static final String keyspace = "ks";
+    private static final String mcUdtName = "mc_udt";
+    private static final ByteBuffer mcUdtNameBytes = ByteBufferUtil.bytes(mcUdtName);
+    private static final String iUdtName = "i_udt";
+    private static final ByteBuffer iUdtNameBytes = ByteBufferUtil.bytes(iUdtName);
+    private static final String fUdtName = "f_udt";
+    private static final ByteBuffer fUdtNameBytes = ByteBufferUtil.bytes(fUdtName);
+    private static final String udtField1 = "a";
+    private static final String udtField2 = "b";
+    private static final AbstractType<?> udtType1 = UTF8Type.instance;
+    private static final AbstractType<?> udtType2 = UTF8Type.instance;
+
+    private static final Types types = Types.builder()
+                                            .add(new UserType(keyspace,
+                                                              mcUdtNameBytes,
+                                                              Arrays.asList(new FieldIdentifier(ByteBufferUtil.bytes(udtField1)),
+                                                                            new FieldIdentifier(ByteBufferUtil.bytes(udtField2))),
+                                                              Arrays.asList(udtType1,
+                                                                            udtType2),
+                                                              true))
+                                            .add(new UserType(keyspace,
+                                                              iUdtNameBytes,
+                                                              Arrays.asList(new FieldIdentifier(ByteBufferUtil.bytes(udtField1)),
+                                                                            new FieldIdentifier(ByteBufferUtil.bytes(udtField2))),
+                                                              Arrays.asList(udtType1,
+                                                                            udtType2),
+                                                              true))
+                                            .add(new UserType(keyspace,
+                                                              fUdtNameBytes,
+                                                              Arrays.asList(new FieldIdentifier(ByteBufferUtil.bytes(udtField1)),
+                                                                            new FieldIdentifier(ByteBufferUtil.bytes(udtField2))),
+                                                              Arrays.asList(udtType1,
+                                                                            udtType2),
+                                                              true))
+                                            .build();
+
+    static class TypeDef
+    {
+        final String typeString;
+        final String cqlTypeString;
+        final String droppedCqlTypeString;
+        final boolean multiCell;
+        final String cqlValue;
+
+        final AbstractType<?> type;
+        final CQL3Type cqlType;
+
+        final AbstractType<?> droppedType;
+        final CQL3Type droppedCqlType;
+
+        TypeDef(String typeString, String cqlTypeString, String droppedCqlTypeString, boolean multiCell, String cqlValue)
+        {
+            this.typeString = typeString;
+            this.cqlTypeString = cqlTypeString;
+            this.droppedCqlTypeString = droppedCqlTypeString;
+            this.multiCell = multiCell;
+            this.cqlValue = cqlValue;
+
+            cqlType = CQLFragmentParser.parseAny(CqlParser::comparatorType, cqlTypeString, "non-dropped type")
+                                       .prepare(keyspace, types);
+            type = cqlType.getType();
+
+            droppedCqlType = CQLFragmentParser.parseAny(CqlParser::comparatorType, droppedCqlTypeString, "dropped type")
+                                              .prepare(keyspace, types);
+            // NOTE: TupleType is *always* parsed as frozen, but never toString()'d with the surrounding FrozenType
+            droppedType = droppedCqlType.getType();
+        }
+
+        @Override
+        public String toString()
+        {
+            return "TypeDef{\n" +
+                   "typeString='" + typeString + "'\n" +
+                   ", type=" + type + '\n' +
+                   ", cqlTypeString='" + cqlTypeString + "'\n" +
+                   ", cqlType=" + cqlType + '\n' +
+                   ", droppedType=" + droppedType + '\n' +
+                   ", droppedCqlTypeString='" + droppedCqlTypeString + "'\n" +
+                   ", droppedCqlType=" + droppedCqlType + '\n' +
+                   '}';
+        }
+    }
+
+    private static final TypeDef text = new TypeDef(
+            "org.apache.cassandra.db.marshal.UTF8Type",
+            "text",
+            "text",
+            false,
+            "'foobar'");
+
+    private static final TypeDef tuple_text__text_ = new TypeDef(
+            "org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type)",
+            "tuple<text, text>",
+            "frozen<tuple<text, text>>",
+            false,
+            "('foo','bar')");
+
+    // Currently, dropped non-frozen-UDT columns are recorded as frozen<tuple<...>>, which is technically wrong
+    //private static final TypeDef mc_udt = new TypeDef(
+    //        "org.apache.cassandra.db.marshal.UserType(ks,6d635f756474,61:org.apache.cassandra.db.marshal.UTF8Type,62:org.apache.cassandra.db.marshal.UTF8Type)",
+    //        "mc_udt",
+    //        "tuple<text, text>",
+    //        true,
+    //        "{a:'foo',b:'bar'}");
+
+    private static final TypeDef frozen_f_udt_ = new TypeDef(
+            "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.UserType(ks,665f756474,61:org.apache.cassandra.db.marshal.UTF8Type,62:org.apache.cassandra.db.marshal.UTF8Type))",
+            "frozen<f_udt>",
+            "frozen<tuple<text, text>>",
+            false,
+            "{a:'foo',b:'bar'}");
+
+    private static final TypeDef list_text_ = new TypeDef(
+            "org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type)",
+            "list<text>",
+            "list<text>",
+            true,
+            "['foobar']");
+
+    private static final TypeDef frozen_list_text__ = new TypeDef(
+            "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UTF8Type))",
+            "frozen<list<text>>",
+            "frozen<list<text>>",
+            true,
+            "['foobar']");
+
+    private static final TypeDef set_text_ = new TypeDef(
+            "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type)",
+            "set<text>",
+            "set<text>",
+            true,
+            "{'foobar'}");
+
+    private static final TypeDef frozen_set_text__ = new TypeDef(
+            "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type))",
+            "frozen<set<text>>",
+            "frozen<set<text>>",
+            true,
+            "{'foobar'}");
+
+    private static final TypeDef map_text__text_ = new TypeDef(
+            "org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type)",
+            "map<text, text>",
+            "map<text, text>",
+            true,
+            "{'foo':'bar'}");
+
+    private static final TypeDef frozen_map_text__text__ = new TypeDef(
+            "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type))",
+            "frozen<map<text, text>>",
+            "frozen<map<text, text>>",
+            true,
+            "{'foo':'bar'}");
+
+    private static final TypeDef list_frozen_tuple_text__text___ = new TypeDef(
+            // in consequence, this should be:
+            // "org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type)))",
+            "org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type))",
+            "list<frozen<tuple<text, text>>>",
+            "list<frozen<tuple<text, text>>>",
+            true,
+            "[('foo','bar')]");
+
+    private static final TypeDef frozen_list_tuple_text__text___ = new TypeDef(
+            "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type)))",
+            "frozen<list<frozen<tuple<text, text>>>>",
+            "frozen<list<frozen<tuple<text, text>>>>",
+            true,
+            "[('foo','bar')]");
+
+    private static final TypeDef set_frozen_tuple_text__text___ = new TypeDef(
+            // in consequence, this should be:
+            // "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type)))",
+            "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type))",
+            "set<frozen<tuple<text, text>>>",
+            "set<frozen<tuple<text, text>>>",
+            true,
+            "{('foo','bar')}");
+
+    private static final TypeDef frozen_set_tuple_text__text___ = new TypeDef(
+            "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type)))",
+            "frozen<set<frozen<tuple<text, text>>>>",
+            "frozen<set<frozen<tuple<text, text>>>>",
+            true,
+            "{('foo','bar')}");
+
+    private static final TypeDef map_text__frozen_tuple_text__text___ = new TypeDef(
+            // in consequence, this should be:
+            // "org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type)))",
+            "org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type))",
+            "map<text, frozen<tuple<text, text>>>",
+            "map<text, frozen<tuple<text, text>>>",
+            true,
+            "{'foobar':('foo','bar')}");
+
+    private static final TypeDef frozen_map_text__tuple_text__text___ = new TypeDef(
+            "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type)))",
+            "frozen<map<text, frozen<tuple<text, text>>>>",
+            "frozen<map<text, frozen<tuple<text, text>>>>",
+            true,
+            "{'foobar':('foo','bar')}");
+
+    private static final TypeDef list_frozen_i_udt__ = new TypeDef(
+            "org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.UserType(ks,695f756474,61:org.apache.cassandra.db.marshal.UTF8Type,62:org.apache.cassandra.db.marshal.UTF8Type)))",
+            "list<frozen<i_udt>>",
+            "list<frozen<tuple<text, text>>>",
+            true,
+            "[{a:'foo',b:'bar'}]");
+
+    private static final TypeDef frozen_list_i_udt__ = new TypeDef(
+            "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.UserType(ks,695f756474,61:org.apache.cassandra.db.marshal.UTF8Type,62:org.apache.cassandra.db.marshal.UTF8Type)))",
+            "frozen<list<frozen<i_udt>>>",
+            "frozen<list<frozen<tuple<text, text>>>>",
+            true,
+            "[{a:'foo',b:'bar'}]");
+
+    private static final TypeDef set_frozen_i_udt__ = new TypeDef(
+            "org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.UserType(ks,695f756474,61:org.apache.cassandra.db.marshal.UTF8Type,62:org.apache.cassandra.db.marshal.UTF8Type)))",
+            "set<frozen<i_udt>>",
+            "set<frozen<tuple<text, text>>>",
+            true,
+            "{{a:'foo',b:'bar'}}");
+
+    private static final TypeDef frozen_set_i_udt__ = new TypeDef(
+            "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UserType(ks,695f756474,61:org.apache.cassandra.db.marshal.UTF8Type,62:org.apache.cassandra.db.marshal.UTF8Type)))",
+            "frozen<set<frozen<i_udt>>>",
+            "frozen<set<frozen<tuple<text, text>>>>",
+            true,
+            "{{a:'foo',b:'bar'}}");
+
+    private static final TypeDef map_text__frozen_i_udt__ = new TypeDef(
+            "org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.UserType(ks,695f756474,61:org.apache.cassandra.db.marshal.UTF8Type,62:org.apache.cassandra.db.marshal.UTF8Type)))",
+            "map<text, frozen<i_udt>>",
+            "map<text, frozen<tuple<text, text>>>",
+            true,
+            "{'foobar':{a:'foo',b:'bar'}}");
+
+    private static final TypeDef frozen_map_text__i_udt__ = new TypeDef(
+            "org.apache.cassandra.db.marshal.FrozenType(org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UserType(ks,695f756474,61:org.apache.cassandra.db.marshal.UTF8Type,62:org.apache.cassandra.db.marshal.UTF8Type)))",
+            "frozen<map<text, frozen<i_udt>>>",
+            "frozen<map<text, frozen<tuple<text, text>>>>",
+            true,
+            "{'foobar':{a:'foo',b:'bar'}}");
+
+    private static final TypeDef[] allTypes = {
+            text,
+            tuple_text__text_,
+            frozen_f_udt_,
+            list_text_,
+            frozen_list_text__,
+            set_text_,
+            frozen_set_text__,
+            map_text__text_,
+            frozen_map_text__text__,
+            list_frozen_tuple_text__text___,
+            frozen_list_tuple_text__text___,
+            set_frozen_tuple_text__text___,
+            frozen_set_tuple_text__text___,
+            map_text__frozen_tuple_text__text___,
+            frozen_map_text__tuple_text__text___,
+            list_frozen_i_udt__,
+            frozen_list_i_udt__,
+            set_frozen_i_udt__,
+            frozen_set_i_udt__,
+            map_text__frozen_i_udt__,
+            frozen_map_text__i_udt__,
+            };
+
+    @Ignore("Only used to ")
+    @Test
+    public void generateCqlStatements() throws InterruptedException
+    {
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+
+        pw.println("DROP TABLE sstableheaderfixtest;");
+        pw.println();
+        pw.println("CREATE TYPE i_udt (a text, b text);");
+        pw.println("CREATE TYPE f_udt (a text, b text);");
+        pw.println("CREATE TYPE mc_udt (a text, b text);");
+        pw.println();
+        pw.println("CREATE TABLE sstableheaderfixtest (");
+        pw.print("  id int PRIMARY KEY");
+        for (TypeDef typeDef : allTypes)
+        {
+            String cname = typeDef.cqlTypeString.replaceAll("[, <>]", "_");
+            pw.printf(",%n  %s %s", cname, typeDef.cqlTypeString);
+        }
+        pw.println(");");
+        pw.println();
+
+        pw.printf("INSERT INTO sstableheaderfixtest%n  (id");
+        for (TypeDef typeDef : allTypes)
+        {
+            String cname = typeDef.cqlTypeString.replaceAll("[, <>]", "_");
+            pw.printf(",%n    %s", cname);
+        }
+        pw.printf(")%n  VALUES%n  (1");
+        for (TypeDef typeDef : allTypes)
+        {
+            pw.printf(",%n    %s", typeDef.cqlValue);
+        }
+        pw.println(");");
+
+        pw.println();
+        pw.println();
+        pw.println("-- Run tools/bin/sstablemetadata data/data/<keyspace>/<table>/*-Data.db to show the sstable");
+        pw.println("-- serialization-header (types not shown in the C* 3.0 variant of the sstablemetadata tool)");
+
+        sw.flush();
+
+        System.out.println(sw.toString());
+
+        Thread.sleep(1000);
+    }
+
+    @Test
+    public void verifyTypes()
+    {
+        AssertionError master = null;
+        for (TypeDef typeDef : allTypes)
+        {
+            try
+            {
+                assertEquals(typeDef.toString() + "\n typeString vs type\n", typeDef.typeString, typeDef.type.toString());
+                assertEquals(typeDef.toString() + "\n typeString vs cqlType.getType()\n", typeDef.typeString, typeDef.cqlType.getType().toString());
+                AbstractType<?> expanded = SchemaKeyspace.expandUserTypes(typeDef.type);
+                CQL3Type expandedCQL = expanded.asCQL3Type();
+                // Note: cannot include this commented-out assertion, because the parsed CQL3Type instance for
+                // 'frozen<list<tuple<text, text>>>' returns 'frozen<list<frozen<tuple<text, text>>>>' via it's CQL3Type.toString()
+                // implementation.
+                assertEquals(typeDef.toString() + "\n droppedCqlType\n", typeDef.droppedCqlType, expandedCQL);
+                assertEquals(typeDef.toString() + "\n droppedCqlTypeString\n", typeDef.droppedCqlTypeString, expandedCQL.toString());
+                assertEquals(typeDef.toString() + "\n multiCell\n", typeDef.type.isMultiCell(), typeDef.droppedType.isMultiCell());
+
+                AbstractType<?> parsedType = TypeParser.parse(typeDef.typeString);
+                assertEquals(typeDef.toString(), typeDef.typeString, parsedType.toString());
+            }
+            catch (AssertionError ae)
+            {
+                if (master == null)
+                    master = ae;
+                else
+                    master.addSuppressed(ae);
+            }
+        }
+        if (master != null)
+            throw master;
+    }
+}
diff --git a/test/unit/org/apache/cassandra/security/CipherFactoryTest.java b/test/unit/org/apache/cassandra/security/CipherFactoryTest.java
new file mode 100644
index 0000000..29302b7
--- /dev/null
+++ b/test/unit/org/apache/cassandra/security/CipherFactoryTest.java
@@ -0,0 +1,134 @@
+/*
+ *
+ * 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.security;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+
+import com.google.common.base.Charsets;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.config.TransparentDataEncryptionOptions;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+public class CipherFactoryTest
+{
+    // http://www.gutenberg.org/files/4300/4300-h/4300-h.htm
+    static final String ULYSSEUS = "Stately, plump Buck Mulligan came from the stairhead, bearing a bowl of lather on which a mirror and a razor lay crossed. " +
+                                   "A yellow dressinggown, ungirdled, was sustained gently behind him on the mild morning air. He held the bowl aloft and intoned: " +
+                                   "-Introibo ad altare Dei.";
+    TransparentDataEncryptionOptions encryptionOptions;
+    CipherFactory cipherFactory;
+    SecureRandom secureRandom;
+
+    @Before
+    public void setup()
+    {
+        try
+        {
+            secureRandom = SecureRandom.getInstance("SHA1PRNG");
+            assertNotNull(secureRandom.getProvider());
+        }
+        catch (NoSuchAlgorithmException e)
+        {
+            fail("NoSuchAlgorithmException: SHA1PRNG not found.");
+        }
+        long seed = new java.util.Random().nextLong();
+        System.out.println("Seed: " + seed);
+        secureRandom.setSeed(seed);
+        encryptionOptions = EncryptionContextGenerator.createEncryptionOptions();
+        cipherFactory = new CipherFactory(encryptionOptions);
+    }
+
+    @Test
+    public void roundTrip() throws IOException, BadPaddingException, IllegalBlockSizeException
+    {
+        Cipher encryptor = cipherFactory.getEncryptor(encryptionOptions.cipher, encryptionOptions.key_alias);
+        byte[] original = ULYSSEUS.getBytes(Charsets.UTF_8);
+        byte[] encrypted = encryptor.doFinal(original);
+
+        Cipher decryptor = cipherFactory.getDecryptor(encryptionOptions.cipher, encryptionOptions.key_alias, encryptor.getIV());
+        byte[] decrypted = decryptor.doFinal(encrypted);
+        Assert.assertEquals(ULYSSEUS, new String(decrypted, Charsets.UTF_8));
+    }
+
+    private byte[] nextIV()
+    {
+        byte[] b = new byte[16];
+        secureRandom.nextBytes(b);
+        return b;
+    }
+
+    @Test
+    public void buildCipher_SameParams() throws Exception
+    {
+        byte[] iv = nextIV();
+        Cipher c1 = cipherFactory.buildCipher(encryptionOptions.cipher, encryptionOptions.key_alias, iv, Cipher.ENCRYPT_MODE);
+        Cipher c2 = cipherFactory.buildCipher(encryptionOptions.cipher, encryptionOptions.key_alias, iv, Cipher.ENCRYPT_MODE);
+        Assert.assertTrue(c1 == c2);
+    }
+
+    @Test
+    public void buildCipher_DifferentModes() throws Exception
+    {
+        byte[] iv = nextIV();
+        Cipher c1 = cipherFactory.buildCipher(encryptionOptions.cipher, encryptionOptions.key_alias, iv, Cipher.ENCRYPT_MODE);
+        Cipher c2 = cipherFactory.buildCipher(encryptionOptions.cipher, encryptionOptions.key_alias, iv, Cipher.DECRYPT_MODE);
+        Assert.assertFalse(c1 == c2);
+    }
+
+    @Test
+    public void buildCipher_DifferentIVs() throws Exception
+    {
+        Cipher c1 = cipherFactory.buildCipher(encryptionOptions.cipher, encryptionOptions.key_alias, nextIV(), Cipher.ENCRYPT_MODE);
+        Cipher c2 = cipherFactory.buildCipher(encryptionOptions.cipher, encryptionOptions.key_alias, nextIV(), Cipher.DECRYPT_MODE);
+        Assert.assertFalse(c1 == c2);
+    }
+
+    @Test
+    public void buildCipher_DifferentAliases() throws Exception
+    {
+        Cipher c1 = cipherFactory.buildCipher(encryptionOptions.cipher, encryptionOptions.key_alias, nextIV(), Cipher.ENCRYPT_MODE);
+        Cipher c2 = cipherFactory.buildCipher(encryptionOptions.cipher, EncryptionContextGenerator.KEY_ALIAS_2, nextIV(), Cipher.DECRYPT_MODE);
+        Assert.assertFalse(c1 == c2);
+    }
+
+    @Test(expected = AssertionError.class)
+    public void getDecryptor_NullIv() throws IOException
+    {
+        cipherFactory.getDecryptor(encryptionOptions.cipher, encryptionOptions.key_alias, null);
+    }
+
+    @Test(expected = AssertionError.class)
+    public void getDecryptor_EmptyIv() throws IOException
+    {
+        cipherFactory.getDecryptor(encryptionOptions.cipher, encryptionOptions.key_alias, new byte[0]);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/security/EncryptionContextGenerator.java b/test/unit/org/apache/cassandra/security/EncryptionContextGenerator.java
new file mode 100644
index 0000000..4719356
--- /dev/null
+++ b/test/unit/org/apache/cassandra/security/EncryptionContextGenerator.java
@@ -0,0 +1,59 @@
+/*
+ *
+ * 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.security;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.config.TransparentDataEncryptionOptions;
+
+public class EncryptionContextGenerator
+{
+    public static final String KEY_ALIAS_1 = "testing:1";
+    public static final String KEY_ALIAS_2 = "testing:2";
+
+    public static EncryptionContext createContext(boolean init)
+    {
+        return createContext(null, init);
+    }
+
+    public static EncryptionContext createContext(byte[] iv, boolean init)
+    {
+        return new EncryptionContext(createEncryptionOptions(), iv, init);
+    }
+
+    public static TransparentDataEncryptionOptions createEncryptionOptions()
+    {
+        Map<String,String> params = new HashMap<>();
+        params.put("keystore", "test/conf/cassandra.keystore");
+        params.put("keystore_password", "cassandra");
+        params.put("store_type", "JCEKS");
+        ParameterizedClass keyProvider = new ParameterizedClass(JKSKeyProvider.class.getName(), params);
+
+        return new TransparentDataEncryptionOptions("AES/CBC/PKCS5Padding", KEY_ALIAS_1, keyProvider);
+    }
+
+    public static EncryptionContext createDisabledContext()
+    {
+        return new EncryptionContext();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/security/EncryptionUtilsTest.java b/test/unit/org/apache/cassandra/security/EncryptionUtilsTest.java
new file mode 100644
index 0000000..70b327e
--- /dev/null
+++ b/test/unit/org/apache/cassandra/security/EncryptionUtilsTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.security;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.HashMap;
+import java.util.Random;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.ShortBufferException;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.TransparentDataEncryptionOptions;
+import org.apache.cassandra.io.compress.ICompressor;
+import org.apache.cassandra.io.compress.LZ4Compressor;
+import org.apache.cassandra.io.util.RandomAccessReader;
+
+public class EncryptionUtilsTest
+{
+    @BeforeClass
+    public static void initDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
+    final Random random = new Random();
+    ICompressor compressor;
+    TransparentDataEncryptionOptions tdeOptions;
+
+    @Before
+    public void setup()
+    {
+        compressor = LZ4Compressor.create(new HashMap<>());
+        tdeOptions = EncryptionContextGenerator.createEncryptionOptions();
+    }
+
+    @Test
+    public void compress() throws IOException
+    {
+        byte[] buf = new byte[(1 << 13) - 13];
+        random.nextBytes(buf);
+        ByteBuffer compressedBuffer = EncryptionUtils.compress(ByteBuffer.wrap(buf), ByteBuffer.allocate(0), true, compressor);
+        ByteBuffer uncompressedBuffer = EncryptionUtils.uncompress(compressedBuffer, ByteBuffer.allocate(0), true, compressor);
+        Assert.assertArrayEquals(buf, uncompressedBuffer.array());
+    }
+
+    @Test
+    public void encrypt() throws BadPaddingException, ShortBufferException, IllegalBlockSizeException, IOException
+    {
+        byte[] buf = new byte[(1 << 12) - 7];
+        random.nextBytes(buf);
+
+        // encrypt
+        CipherFactory cipherFactory = new CipherFactory(tdeOptions);
+        Cipher encryptor = cipherFactory.getEncryptor(tdeOptions.cipher, tdeOptions.key_alias);
+
+        File f = File.createTempFile("commitlog-enc-utils-", ".tmp");
+        f.deleteOnExit();
+        FileChannel channel = new RandomAccessFile(f, "rw").getChannel();
+        EncryptionUtils.encryptAndWrite(ByteBuffer.wrap(buf), channel, true, encryptor);
+        channel.close();
+
+        // decrypt
+        Cipher decryptor = cipherFactory.getDecryptor(tdeOptions.cipher, tdeOptions.key_alias, encryptor.getIV());
+        ByteBuffer decryptedBuffer = EncryptionUtils.decrypt(RandomAccessReader.open(f), ByteBuffer.allocate(0), true, decryptor);
+
+        // normally, we'd just call BB.array(), but that gives you the *entire* backing array, not with any of the offsets (position,limit) applied.
+        // thus, just for this test, we copy the array and perform an array-level comparison with those offsets
+        decryptedBuffer.limit(buf.length);
+        byte[] b = new byte[buf.length];
+        System.arraycopy(decryptedBuffer.array(), 0, b, 0, buf.length);
+        Assert.assertArrayEquals(buf, b);
+    }
+
+    @Test
+    public void fullRoundTrip() throws IOException, BadPaddingException, ShortBufferException, IllegalBlockSizeException
+    {
+        // compress
+        byte[] buf = new byte[(1 << 12) - 7];
+        random.nextBytes(buf);
+        ByteBuffer compressedBuffer = EncryptionUtils.compress(ByteBuffer.wrap(buf), ByteBuffer.allocate(0), true, compressor);
+
+        // encrypt
+        CipherFactory cipherFactory = new CipherFactory(tdeOptions);
+        Cipher encryptor = cipherFactory.getEncryptor(tdeOptions.cipher, tdeOptions.key_alias);
+        File f = File.createTempFile("commitlog-enc-utils-", ".tmp");
+        f.deleteOnExit();
+        FileChannel channel = new RandomAccessFile(f, "rw").getChannel();
+        EncryptionUtils.encryptAndWrite(compressedBuffer, channel, true, encryptor);
+
+        // decrypt
+        Cipher decryptor = cipherFactory.getDecryptor(tdeOptions.cipher, tdeOptions.key_alias, encryptor.getIV());
+        ByteBuffer decryptedBuffer = EncryptionUtils.decrypt(RandomAccessReader.open(f), ByteBuffer.allocate(0), true, decryptor);
+
+        // uncompress
+        ByteBuffer uncompressedBuffer = EncryptionUtils.uncompress(decryptedBuffer, ByteBuffer.allocate(0), true, compressor);
+        Assert.assertArrayEquals(buf, uncompressedBuffer.array());
+    }
+}
diff --git a/test/unit/org/apache/cassandra/security/JKSKeyProviderTest.java b/test/unit/org/apache/cassandra/security/JKSKeyProviderTest.java
new file mode 100644
index 0000000..081f688
--- /dev/null
+++ b/test/unit/org/apache/cassandra/security/JKSKeyProviderTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.security;
+
+import java.io.IOException;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.config.TransparentDataEncryptionOptions;
+
+public class JKSKeyProviderTest
+{
+    JKSKeyProvider jksKeyProvider;
+    TransparentDataEncryptionOptions tdeOptions;
+
+    @Before
+    public void setup()
+    {
+        tdeOptions = EncryptionContextGenerator.createEncryptionOptions();
+        jksKeyProvider = new JKSKeyProvider(tdeOptions);
+    }
+
+    @Test
+    public void getSecretKey_WithKeyPassword() throws IOException
+    {
+        Assert.assertNotNull(jksKeyProvider.getSecretKey(tdeOptions.key_alias));
+    }
+
+    @Test
+    public void getSecretKey_WithoutKeyPassword() throws IOException
+    {
+        tdeOptions.remove("key_password");
+        Assert.assertNotNull(jksKeyProvider.getSecretKey(tdeOptions.key_alias));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/security/SSLFactoryTest.java b/test/unit/org/apache/cassandra/security/SSLFactoryTest.java
index ca18073..ec0c810 100644
--- a/test/unit/org/apache/cassandra/security/SSLFactoryTest.java
+++ b/test/unit/org/apache/cassandra/security/SSLFactoryTest.java
@@ -28,13 +28,20 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.EncryptionOptions;
 import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions;
 import org.apache.cassandra.utils.FBUtilities;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 public class SSLFactoryTest
 {
+    @BeforeClass
+    public static void beforeClass()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
 
     @Test
     public void testFilterCipherSuites()
diff --git a/test/unit/org/apache/cassandra/serializers/ClientUtilsTest.java b/test/unit/org/apache/cassandra/serializers/ClientUtilsTest.java
deleted file mode 100644
index 563d6cb..0000000
--- a/test/unit/org/apache/cassandra/serializers/ClientUtilsTest.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package org.apache.cassandra.serializers;
-/*
- *
- * 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.
- *
- */
-
-
-import java.math.BigDecimal;
-import java.math.BigInteger;
-import java.nio.ByteBuffer;
-import java.sql.Date;
-import java.util.UUID;
-
-import org.apache.cassandra.utils.UUIDGen;
-import org.junit.Test;
-
-public class ClientUtilsTest
-{
-    /** Exercises the classes in the clientutil jar to expose missing dependencies. */
-    @Test
-    public void test()
-    {
-        AsciiSerializer.instance.deserialize(AsciiSerializer.instance.serialize("string"));
-        BooleanSerializer.instance.deserialize(BooleanSerializer.instance.serialize(true));
-        BytesSerializer.instance.deserialize(BytesSerializer.instance.serialize(ByteBuffer.wrap("string".getBytes())));
-
-        Date date = new Date(System.currentTimeMillis());
-        ByteBuffer dateBB = TimestampSerializer.instance.serialize(date);
-        TimestampSerializer.instance.deserialize(dateBB);
-
-        DecimalSerializer.instance.deserialize(DecimalSerializer.instance.serialize(new BigDecimal(1)));
-        DoubleSerializer.instance.deserialize(DoubleSerializer.instance.serialize(new Double(1.0d)));
-        FloatSerializer.instance.deserialize(FloatSerializer.instance.serialize(new Float(1.0f)));
-        Int32Serializer.instance.deserialize(Int32Serializer.instance.serialize(1));
-        IntegerSerializer.instance.deserialize(IntegerSerializer.instance.serialize(new BigInteger("1")));
-        LongSerializer.instance.deserialize(LongSerializer.instance.serialize(1L));
-        UTF8Serializer.instance.deserialize(UTF8Serializer.instance.serialize("string"));
-
-        // UUIDGen
-        UUID uuid = UUIDGen.getTimeUUID();
-        UUIDSerializer.instance.deserialize(UUIDSerializer.instance.serialize(uuid));
-    }
-}
diff --git a/test/unit/org/apache/cassandra/serializers/SimpleDateSerializerTest.java b/test/unit/org/apache/cassandra/serializers/SimpleDateSerializerTest.java
index 4c0751f..e051357 100644
--- a/test/unit/org/apache/cassandra/serializers/SimpleDateSerializerTest.java
+++ b/test/unit/org/apache/cassandra/serializers/SimpleDateSerializerTest.java
@@ -19,7 +19,6 @@
 package org.apache.cassandra.serializers;
 
 import org.apache.cassandra.db.marshal.SimpleDateType;
-import org.apache.cassandra.utils.Pair;
 import org.junit.Test;
 
 import java.nio.ByteBuffer;
diff --git a/test/unit/org/apache/cassandra/service/ActiveRepairServiceTest.java b/test/unit/org/apache/cassandra/service/ActiveRepairServiceTest.java
index adcd684..2c1a8d2 100644
--- a/test/unit/org/apache/cassandra/service/ActiveRepairServiceTest.java
+++ b/test/unit/org/apache/cassandra/service/ActiveRepairServiceTest.java
@@ -270,6 +270,7 @@
         Set<SSTableReader> original = Sets.newHashSet(store.select(View.select(SSTableSet.CANONICAL, (s) -> !s.isRepaired())).sstables);
         UUID prsId = UUID.randomUUID();
         ActiveRepairService.instance.registerParentRepairSession(prsId, FBUtilities.getBroadcastAddress(), Collections.singletonList(store), null, true, System.currentTimeMillis(), true);
+
         ActiveRepairService.ParentRepairSession prs = ActiveRepairService.instance.getParentRepairSession(prsId);
         prs.markSSTablesRepairing(store.metadata.cfId, prsId);
         try (Refs<SSTableReader> refs = prs.getActiveRepairedSSTableRefsForAntiCompaction(store.metadata.cfId, prsId))
diff --git a/test/unit/org/apache/cassandra/service/ClientWarningsTest.java b/test/unit/org/apache/cassandra/service/ClientWarningsTest.java
index cf14d55..e939df0 100644
--- a/test/unit/org/apache/cassandra/service/ClientWarningsTest.java
+++ b/test/unit/org/apache/cassandra/service/ClientWarningsTest.java
@@ -28,7 +28,7 @@
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.transport.Message;
-import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.transport.SimpleClient;
 import org.apache.cassandra.transport.messages.QueryMessage;
 
@@ -49,7 +49,7 @@
     {
         createTable("CREATE TABLE %s (pk int PRIMARY KEY, v text)");
 
-        try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, Server.VERSION_4))
+        try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, ProtocolVersion.V4))
         {
             client.connect(false);
 
@@ -68,13 +68,17 @@
     {
         createTable("CREATE TABLE %s (pk int PRIMARY KEY, v text)");
 
-        try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, Server.VERSION_4))
+        try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, ProtocolVersion.V4))
         {
             client.connect(false);
 
-            QueryMessage query = new QueryMessage(createBatchStatement(DatabaseDescriptor.getBatchSizeWarnThreshold()), QueryOptions.DEFAULT);
+            QueryMessage query = new QueryMessage(createBatchStatement2(DatabaseDescriptor.getBatchSizeWarnThreshold() / 2 + 1), QueryOptions.DEFAULT);
             Message.Response resp = client.execute(query);
             assertEquals(1, resp.getWarnings().size());
+
+            query = new QueryMessage(createBatchStatement(DatabaseDescriptor.getBatchSizeWarnThreshold()), QueryOptions.DEFAULT);
+            resp = client.execute(query);
+            assertNull(resp.getWarnings());
         }
     }
 
@@ -84,7 +88,7 @@
         final int iterations = 10000;
         createTable("CREATE TABLE %s (pk int, ck int, v int, PRIMARY KEY (pk, ck))");
 
-        try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, Server.VERSION_4))
+        try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, ProtocolVersion.V4))
         {
             client.connect(false);
 
@@ -124,7 +128,7 @@
     {
         createTable("CREATE TABLE %s (pk int PRIMARY KEY, v text)");
 
-        try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, Server.VERSION_3))
+        try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, ProtocolVersion.V3))
         {
             client.connect(false);
 
diff --git a/test/unit/org/apache/cassandra/service/DataResolverTest.java b/test/unit/org/apache/cassandra/service/DataResolverTest.java
index 65e18ce..2b1e095 100644
--- a/test/unit/org/apache/cassandra/service/DataResolverTest.java
+++ b/test/unit/org/apache/cassandra/service/DataResolverTest.java
@@ -31,6 +31,7 @@
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.marshal.ByteType;
@@ -53,7 +54,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
-import static org.apache.cassandra.db.RangeTombstone.Bound.Kind;
+import static org.apache.cassandra.db.ClusteringBound.Kind;
 
 public class DataResolverTest
 {
@@ -79,6 +80,7 @@
     @BeforeClass
     public static void defineSchema() throws ConfigurationException
     {
+        DatabaseDescriptor.daemonInitialization();
         CFMetaData cfMetadata = CFMetaData.Builder.create(KEYSPACE1, CF_STANDARD)
                                                   .addPartitionKey("key", BytesType.instance)
                                                   .addClusteringColumn("col1", AsciiType.instance)
@@ -147,7 +149,7 @@
     @Test
     public void testResolveNewerSingleRow() throws UnknownHostException
     {
-        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2, System.nanoTime());
         InetAddress peer1 = peer();
         resolver.preprocess(readResponseMessage(peer1, iter(new RowUpdateBuilder(cfm, nowInSec, 0L, dk).clustering("1")
                                                                                                        .add("c1", "v1")
@@ -179,7 +181,7 @@
     @Test
     public void testResolveDisjointSingleRow()
     {
-        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2, System.nanoTime());
         InetAddress peer1 = peer();
         resolver.preprocess(readResponseMessage(peer1, iter(new RowUpdateBuilder(cfm, nowInSec, 0L, dk).clustering("1")
                                                                                                        .add("c1", "v1")
@@ -217,7 +219,7 @@
     public void testResolveDisjointMultipleRows() throws UnknownHostException
     {
 
-        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2, System.nanoTime());
         InetAddress peer1 = peer();
         resolver.preprocess(readResponseMessage(peer1, iter(new RowUpdateBuilder(cfm, nowInSec, 0L, dk).clustering("1")
                                                                                                        .add("c1", "v1")
@@ -264,11 +266,11 @@
     @Test
     public void testResolveDisjointMultipleRowsWithRangeTombstones()
     {
-        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 4);
+        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 4, System.nanoTime());
 
         RangeTombstone tombstone1 = tombstone("1", "11", 1, nowInSec);
         RangeTombstone tombstone2 = tombstone("3", "31", 1, nowInSec);
-        PartitionUpdate update =new RowUpdateBuilder(cfm, nowInSec, 1L, dk).addRangeTombstone(tombstone1)
+        PartitionUpdate update = new RowUpdateBuilder(cfm, nowInSec, 1L, dk).addRangeTombstone(tombstone1)
                                                                                   .addRangeTombstone(tombstone2)
                                                                                   .buildUpdate();
 
@@ -345,7 +347,7 @@
     @Test
     public void testResolveWithOneEmpty()
     {
-        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2, System.nanoTime());
         InetAddress peer1 = peer();
         resolver.preprocess(readResponseMessage(peer1, iter(new RowUpdateBuilder(cfm, nowInSec, 1L, dk).clustering("1")
                                                                                                        .add("c2", "v2")
@@ -375,7 +377,7 @@
     @Test
     public void testResolveWithBothEmpty()
     {
-        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2, System.nanoTime());
         resolver.preprocess(readResponseMessage(peer(), EmptyIterators.unfilteredPartition(cfm, false)));
         resolver.preprocess(readResponseMessage(peer(), EmptyIterators.unfilteredPartition(cfm, false)));
 
@@ -391,7 +393,7 @@
     @Test
     public void testResolveDeleted()
     {
-        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2, System.nanoTime());
         // one response with columns timestamped before a delete in another response
         InetAddress peer1 = peer();
         resolver.preprocess(readResponseMessage(peer1, iter(new RowUpdateBuilder(cfm, nowInSec, 0L, dk).clustering("1")
@@ -417,7 +419,7 @@
     @Test
     public void testResolveMultipleDeleted()
     {
-        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 4);
+        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 4, System.nanoTime());
         // deletes and columns with interleaved timestamp, with out of order return sequence
         InetAddress peer1 = peer();
         resolver.preprocess(readResponseMessage(peer1, fullPartitionDelete(cfm, dk, 0, nowInSec)));
@@ -502,7 +504,7 @@
      */
     private void resolveRangeTombstonesOnBoundary(long timestamp1, long timestamp2)
     {
-        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2, System.nanoTime());
         InetAddress peer1 = peer();
         InetAddress peer2 = peer();
 
@@ -576,7 +578,7 @@
      */
     private void testRepairRangeTombstoneBoundary(int timestamp1, int timestamp2, int timestamp3) throws UnknownHostException
     {
-        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2, System.nanoTime());
         InetAddress peer1 = peer();
         InetAddress peer2 = peer();
 
@@ -628,7 +630,7 @@
     @Test
     public void testRepairRangeTombstoneWithPartitionDeletion()
     {
-        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2, System.nanoTime());
         InetAddress peer1 = peer();
         InetAddress peer2 = peer();
 
@@ -667,7 +669,7 @@
     @Test
     public void testRepairRangeTombstoneWithPartitionDeletion2()
     {
-        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, command, ConsistencyLevel.ALL, 2, System.nanoTime());
         InetAddress peer1 = peer();
         InetAddress peer2 = peer();
 
@@ -713,18 +715,26 @@
     // Forces the start to be exclusive if the condition holds
     private static RangeTombstone withExclusiveStartIf(RangeTombstone rt, boolean condition)
     {
+        if (!condition)
+            return rt;
+
         Slice slice = rt.deletedSlice();
+        ClusteringBound newStart = ClusteringBound.create(Kind.EXCL_START_BOUND, slice.start().getRawValues());
         return condition
-             ? new RangeTombstone(Slice.make(slice.start().withNewKind(Kind.EXCL_START_BOUND), slice.end()), rt.deletionTime())
+             ? new RangeTombstone(Slice.make(newStart, slice.end()), rt.deletionTime())
              : rt;
     }
 
     // Forces the end to be exclusive if the condition holds
     private static RangeTombstone withExclusiveEndIf(RangeTombstone rt, boolean condition)
     {
+        if (!condition)
+            return rt;
+
         Slice slice = rt.deletedSlice();
+        ClusteringBound newEnd = ClusteringBound.create(Kind.EXCL_END_BOUND, slice.end().getRawValues());
         return condition
-             ? new RangeTombstone(Slice.make(slice.start(), slice.end().withNewKind(Kind.EXCL_END_BOUND)), rt.deletionTime())
+             ? new RangeTombstone(Slice.make(slice.start(), newEnd), rt.deletionTime())
              : rt;
     }
 
@@ -735,14 +745,14 @@
 
     private Cell mapCell(int k, int v, long ts)
     {
-        return BufferCell.live(cfm2, m, ts, bb(v), CellPath.create(bb(k)));
+        return BufferCell.live(m, ts, bb(v), CellPath.create(bb(k)));
     }
 
     @Test
     public void testResolveComplexDelete()
     {
         ReadCommand cmd = Util.cmd(cfs2, dk).withNowInSeconds(nowInSec).build();
-        DataResolver resolver = new DataResolver(ks, cmd, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, cmd, ConsistencyLevel.ALL, 2, System.nanoTime());
 
         long[] ts = {100, 200};
 
@@ -795,7 +805,7 @@
     {
 
         ReadCommand cmd = Util.cmd(cfs2, dk).withNowInSeconds(nowInSec).build();
-        DataResolver resolver = new DataResolver(ks, cmd, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, cmd, ConsistencyLevel.ALL, 2, System.nanoTime());
 
         long[] ts = {100, 200};
 
@@ -839,7 +849,7 @@
     public void testResolveNewCollection()
     {
         ReadCommand cmd = Util.cmd(cfs2, dk).withNowInSeconds(nowInSec).build();
-        DataResolver resolver = new DataResolver(ks, cmd, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, cmd, ConsistencyLevel.ALL, 2, System.nanoTime());
 
         long[] ts = {100, 200};
 
@@ -889,7 +899,7 @@
     public void testResolveNewCollectionOverwritingDeleted()
     {
         ReadCommand cmd = Util.cmd(cfs2, dk).withNowInSeconds(nowInSec).build();
-        DataResolver resolver = new DataResolver(ks, cmd, ConsistencyLevel.ALL, 2);
+        DataResolver resolver = new DataResolver(ks, cmd, ConsistencyLevel.ALL, 2, System.nanoTime());
 
         long[] ts = {100, 200};
 
@@ -1029,26 +1039,26 @@
 
     private RangeTombstone tombstone(Object start, boolean inclusiveStart, Object end, boolean inclusiveEnd, long markedForDeleteAt, int localDeletionTime)
     {
-        RangeTombstone.Bound startBound = rtBound(start, true, inclusiveStart);
-        RangeTombstone.Bound endBound = rtBound(end, false, inclusiveEnd);
+        ClusteringBound startBound = rtBound(start, true, inclusiveStart);
+        ClusteringBound endBound = rtBound(end, false, inclusiveEnd);
         return new RangeTombstone(Slice.make(startBound, endBound), new DeletionTime(markedForDeleteAt, localDeletionTime));
     }
 
-    private RangeTombstone.Bound rtBound(Object value, boolean isStart, boolean inclusive)
+    private ClusteringBound rtBound(Object value, boolean isStart, boolean inclusive)
     {
-        RangeTombstone.Bound.Kind kind = isStart
+        ClusteringBound.Kind kind = isStart
                                          ? (inclusive ? Kind.INCL_START_BOUND : Kind.EXCL_START_BOUND)
                                          : (inclusive ? Kind.INCL_END_BOUND : Kind.EXCL_END_BOUND);
 
-        return new RangeTombstone.Bound(kind, cfm.comparator.make(value).getRawValues());
+        return ClusteringBound.create(kind, cfm.comparator.make(value).getRawValues());
     }
 
-    private RangeTombstone.Bound rtBoundary(Object value, boolean inclusiveOnEnd)
+    private ClusteringBoundary rtBoundary(Object value, boolean inclusiveOnEnd)
     {
-        RangeTombstone.Bound.Kind kind = inclusiveOnEnd
+        ClusteringBound.Kind kind = inclusiveOnEnd
                                          ? Kind.INCL_END_EXCL_START_BOUNDARY
                                          : Kind.EXCL_END_INCL_START_BOUNDARY;
-        return new RangeTombstone.Bound(kind, cfm.comparator.make(value).getRawValues());
+        return ClusteringBoundary.create(kind, cfm.comparator.make(value).getRawValues());
     }
 
     private RangeTombstoneBoundMarker marker(Object value, boolean isStart, boolean inclusive, long markedForDeleteAt, int localDeletionTime)
diff --git a/test/unit/org/apache/cassandra/service/JoinTokenRingTest.java b/test/unit/org/apache/cassandra/service/JoinTokenRingTest.java
new file mode 100644
index 0000000..866910e
--- /dev/null
+++ b/test/unit/org/apache/cassandra/service/JoinTokenRingTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.service;
+
+import java.io.IOException;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.index.SecondaryIndexManager;
+import org.apache.cassandra.index.StubIndex;
+
+public class JoinTokenRingTest
+{
+    @BeforeClass
+    public static void setup() throws ConfigurationException
+    {
+        DatabaseDescriptor.daemonInitialization();
+        SchemaLoader.startGossiper();
+        SchemaLoader.prepareServer();
+        SchemaLoader.schemaDefinition("JoinTokenRingTest");
+    }
+
+    @Test
+    public void testIndexPreJoinInvocation() throws IOException
+    {
+        StorageService ss = StorageService.instance;
+        ss.joinRing();
+
+        SecondaryIndexManager indexManager = ColumnFamilyStore.getIfExists("JoinTokenRingTestKeyspace7", "Indexed1").indexManager;
+        StubIndex stub = (StubIndex) indexManager.getIndexByName("value_index");
+        Assert.assertTrue(stub.preJoinInvocation);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/service/LeaveAndBootstrapTest.java b/test/unit/org/apache/cassandra/service/LeaveAndBootstrapTest.java
index 91a7ab2..c5f198e 100644
--- a/test/unit/org/apache/cassandra/service/LeaveAndBootstrapTest.java
+++ b/test/unit/org/apache/cassandra/service/LeaveAndBootstrapTest.java
@@ -23,7 +23,6 @@
 import java.net.UnknownHostException;
 import java.util.*;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
 
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
@@ -35,6 +34,7 @@
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.Util.PartitionerSwitcher;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.concurrent.Stage;
 import org.apache.cassandra.concurrent.StageManager;
 import org.apache.cassandra.config.Schema;
@@ -66,6 +66,7 @@
     @BeforeClass
     public static void defineSchema() throws Exception
     {
+        DatabaseDescriptor.daemonInitialization();
         partitionerSwitcher = Util.switchPartitioner(partitioner);
         SchemaLoader.loadSchema();
         SchemaLoader.schemaDefinition("LeaveAndBootstrapTest");
diff --git a/test/unit/org/apache/cassandra/service/LegacyAuthFailTest.java b/test/unit/org/apache/cassandra/service/LegacyAuthFailTest.java
index 079543f..1e93f31 100644
--- a/test/unit/org/apache/cassandra/service/LegacyAuthFailTest.java
+++ b/test/unit/org/apache/cassandra/service/LegacyAuthFailTest.java
@@ -25,7 +25,7 @@
 import com.google.common.base.Joiner;
 import org.junit.Test;
 
-import org.apache.cassandra.auth.AuthKeyspace;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.cql3.CQLTester;
 
 import static java.lang.String.format;
@@ -49,7 +49,7 @@
             Optional<String> errMsg = StartupChecks.checkLegacyAuthTablesMessage();
             assertEquals(format("Legacy auth tables %s in keyspace %s still exist and have not been properly migrated.",
                                 legacyTable,
-                                AuthKeyspace.NAME), errMsg.get());
+                                SchemaConstants.AUTH_KEYSPACE_NAME), errMsg.get());
             dropLegacyTable(legacyTable);
         }
 
@@ -62,7 +62,7 @@
             Optional<String> errMsg = StartupChecks.checkLegacyAuthTablesMessage();
             assertEquals(format("Legacy auth tables %s in keyspace %s still exist and have not been properly migrated.",
                                 Joiner.on(", ").join(legacyTables),
-                                AuthKeyspace.NAME), errMsg.get());
+                                SchemaConstants.AUTH_KEYSPACE_NAME), errMsg.get());
 
             dropLegacyTable(legacyTables.remove(0));
         }
@@ -74,16 +74,16 @@
 
     private void dropLegacyTable(String legacyTable) throws Throwable
     {
-        execute(format("DROP TABLE %s.%s", AuthKeyspace.NAME, legacyTable));
+        execute(format("DROP TABLE %s.%s", SchemaConstants.AUTH_KEYSPACE_NAME, legacyTable));
     }
 
     private void createLegacyTable(String legacyTable) throws Throwable
     {
-        execute(format("CREATE TABLE %s.%s (id int PRIMARY KEY, val text)", AuthKeyspace.NAME, legacyTable));
+        execute(format("CREATE TABLE %s.%s (id int PRIMARY KEY, val text)", SchemaConstants.AUTH_KEYSPACE_NAME, legacyTable));
     }
 
     private void createKeyspace() throws Throwable
     {
-        execute(format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}", AuthKeyspace.NAME));
+        execute(format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}", SchemaConstants.AUTH_KEYSPACE_NAME));
     }
 }
diff --git a/test/unit/org/apache/cassandra/service/MigrationCoordinatorTest.java b/test/unit/org/apache/cassandra/service/MigrationCoordinatorTest.java
index fc78d62..d85721c 100644
--- a/test/unit/org/apache/cassandra/service/MigrationCoordinatorTest.java
+++ b/test/unit/org/apache/cassandra/service/MigrationCoordinatorTest.java
@@ -70,7 +70,7 @@
             throw new AssertionError(e);
         }
 
-        DatabaseDescriptor.forceStaticInitialization();
+        DatabaseDescriptor.daemonInitialization();
     }
 
     private static class InstrumentedCoordinator extends MigrationCoordinator
diff --git a/test/unit/org/apache/cassandra/service/MigrationManagerTest.java b/test/unit/org/apache/cassandra/service/MigrationManagerTest.java
index eefc640..b9f5c22 100644
--- a/test/unit/org/apache/cassandra/service/MigrationManagerTest.java
+++ b/test/unit/org/apache/cassandra/service/MigrationManagerTest.java
@@ -30,6 +30,7 @@
 import org.apache.cassandra.schema.SchemaKeyspace;
 import org.apache.cassandra.schema.Tables;
 
+import static java.util.Collections.singleton;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -45,7 +46,7 @@
         Optional<Mutation> mutation = MigrationManager.evolveSystemKeyspace(keyspace, 0);
         assertTrue(mutation.isPresent());
 
-        MigrationManager.announce(mutation.get(), true);
+        SchemaKeyspace.mergeSchema(singleton(mutation.get()));
         assertEquals(keyspace, Schema.instance.getKSMetaData("ks0"));
     }
 
@@ -56,7 +57,7 @@
         KeyspaceMetadata keyspace = KeyspaceMetadata.create("ks1", KeyspaceParams.simple(1), Tables.of(table));
 
         // create the keyspace, verify it's there
-        MigrationManager.announce(SchemaKeyspace.makeCreateKeyspaceMutation(keyspace, 0), true);
+        SchemaKeyspace.mergeSchema(singleton(SchemaKeyspace.makeCreateKeyspaceMutation(keyspace, 0).build()));
         assertEquals(keyspace, Schema.instance.getKSMetaData("ks1"));
 
         Optional<Mutation> mutation = MigrationManager.evolveSystemKeyspace(keyspace, 0);
@@ -70,7 +71,7 @@
         KeyspaceMetadata keyspace0 = KeyspaceMetadata.create("ks2", KeyspaceParams.simple(1), Tables.of(table0));
 
         // create the keyspace, verify it's there
-        MigrationManager.announce(SchemaKeyspace.makeCreateKeyspaceMutation(keyspace0, 0), true);
+        SchemaKeyspace.mergeSchema(singleton(SchemaKeyspace.makeCreateKeyspaceMutation(keyspace0, 0).build()));
         assertEquals(keyspace0, Schema.instance.getKSMetaData("ks2"));
 
         CFMetaData table1 = table0.copy().comment("comment");
@@ -79,7 +80,7 @@
         Optional<Mutation> mutation = MigrationManager.evolveSystemKeyspace(keyspace1, 1);
         assertTrue(mutation.isPresent());
 
-        MigrationManager.announce(mutation.get(), true);
+        SchemaKeyspace.mergeSchema(singleton(mutation.get()));
         assertEquals(keyspace1, Schema.instance.getKSMetaData("ks2"));
     }
-}
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/service/MoveTest.java b/test/unit/org/apache/cassandra/service/MoveTest.java
index bc6c6d2..39ebd66 100644
--- a/test/unit/org/apache/cassandra/service/MoveTest.java
+++ b/test/unit/org/apache/cassandra/service/MoveTest.java
@@ -84,6 +84,7 @@
     @BeforeClass
     public static void setup() throws ConfigurationException
     {
+        DatabaseDescriptor.daemonInitialization();
         oldPartitioner = StorageService.instance.setPartitionerUnsafe(partitioner);
         SchemaLoader.loadSchema();
         SchemaLoader.schemaDefinition("MoveTest");
diff --git a/test/unit/org/apache/cassandra/service/NativeTransportServiceTest.java b/test/unit/org/apache/cassandra/service/NativeTransportServiceTest.java
index 8f2689a..25fac21 100644
--- a/test/unit/org/apache/cassandra/service/NativeTransportServiceTest.java
+++ b/test/unit/org/apache/cassandra/service/NativeTransportServiceTest.java
@@ -25,6 +25,7 @@
 
 import com.google.common.collect.Sets;
 import org.junit.After;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
@@ -37,6 +38,11 @@
 
 public class NativeTransportServiceTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
 
     @After
     public void resetConfig()
diff --git a/test/unit/org/apache/cassandra/service/PaxosStateTest.java b/test/unit/org/apache/cassandra/service/PaxosStateTest.java
index 9ee91dd..8054c61 100644
--- a/test/unit/org/apache/cassandra/service/PaxosStateTest.java
+++ b/test/unit/org/apache/cassandra/service/PaxosStateTest.java
@@ -27,7 +27,6 @@
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.rows.UnfilteredRowIterator;
 import org.apache.cassandra.db.rows.Row;
 import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.gms.Gossiper;
diff --git a/test/unit/org/apache/cassandra/service/ProtocolBetaVersionTest.java b/test/unit/org/apache/cassandra/service/ProtocolBetaVersionTest.java
new file mode 100644
index 0000000..0c51eb7
--- /dev/null
+++ b/test/unit/org/apache/cassandra/service/ProtocolBetaVersionTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.service;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.config.*;
+import org.apache.cassandra.cql3.*;
+import org.apache.cassandra.transport.*;
+import org.apache.cassandra.transport.messages.*;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+public class ProtocolBetaVersionTest extends CQLTester
+{
+    @BeforeClass
+    public static void setUp()
+    {
+        requireNetwork();
+        DatabaseDescriptor.setBatchSizeWarnThresholdInKB(1);
+    }
+
+    private ProtocolVersion getBetaVersion()
+    {
+        ProtocolVersion betaVersion = ProtocolVersion.BETA.orElse(null);
+        if (betaVersion == null)
+        {
+            for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
+            {
+                if (version.isBeta())
+                {
+                    betaVersion = version;
+                    break;
+                }
+            }
+        }
+        return betaVersion;
+    }
+
+    @Test
+    public void testProtocolBetaVersion() throws Exception
+    {
+        ProtocolVersion betaVersion = getBetaVersion();
+        if (betaVersion == null)
+        {
+            logger.info("No beta version found for testing");
+            return;
+        }
+
+        createTable("CREATE TABLE %s (pk int PRIMARY KEY, v int)");
+        assertTrue(betaVersion.isBeta()); // change to another beta version or remove test if no beta version
+
+        try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, betaVersion, true, new EncryptionOptions.ClientEncryptionOptions()))
+        {
+            client.connect(false);
+            for (int i = 0; i < 10; i++)
+            {
+                QueryMessage query = new QueryMessage(String.format("INSERT INTO %s.%s (pk, v) VALUES (%s, %s)",
+                                                                    KEYSPACE,
+                                                                    currentTable(),
+                                                                    i, i), QueryOptions.DEFAULT);
+                client.execute(query);
+            }
+
+            QueryMessage query = new QueryMessage(String.format("SELECT * FROM %s.%s",
+                                                                KEYSPACE,
+                                                                currentTable()), QueryOptions.DEFAULT);
+            ResultMessage.Rows resp = (ResultMessage.Rows) client.execute(query);
+            assertEquals(10, resp.result.size());
+        }
+        catch (Exception e)
+        {
+            fail("No exceptions should've been thrown: " + e.getMessage());
+        }
+    }
+
+    @Test
+    public void unforcedProtocolVersionTest() throws Exception
+    {
+        ProtocolVersion betaVersion = getBetaVersion();
+        if (betaVersion == null)
+        {
+            logger.info("No beta version found for testing");
+            return;
+        }
+
+        assertTrue(betaVersion.isBeta()); // change to another beta version or remove test if no beta version
+        try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, betaVersion, false, new EncryptionOptions.ClientEncryptionOptions()))
+        {
+            client.connect(false);
+            fail("Exception should have been thrown");
+        }
+        catch (Exception e)
+        {
+            assertEquals("Beta version of server used (5/v5-beta), but USE_BETA flag is not set",
+                         e.getMessage());
+        }
+    }
+}
+
diff --git a/test/unit/org/apache/cassandra/service/QueryPagerTest.java b/test/unit/org/apache/cassandra/service/QueryPagerTest.java
index 27c630d..4d64283 100644
--- a/test/unit/org/apache/cassandra/service/QueryPagerTest.java
+++ b/test/unit/org/apache/cassandra/service/QueryPagerTest.java
@@ -41,9 +41,9 @@
 import org.apache.cassandra.schema.KeyspaceParams;
 import org.apache.cassandra.service.pager.QueryPager;
 import org.apache.cassandra.service.pager.PagingState;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
-import org.apache.cassandra.transport.Server;
 
 import static org.apache.cassandra.cql3.QueryProcessor.executeInternal;
 import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
@@ -133,7 +133,8 @@
         StringBuilder sb = new StringBuilder();
         List<FilteredPartition> partitionList = new ArrayList<>();
         int rows = 0;
-        try (ReadOrderGroup orderGroup = pager.startOrderGroup(); PartitionIterator iterator = pager.fetchPageInternal(toQuery, orderGroup))
+        try (ReadExecutionController executionController = pager.executionController();
+             PartitionIterator iterator = pager.fetchPageInternal(toQuery, executionController))
         {
             while (iterator.hasNext())
             {
@@ -217,7 +218,7 @@
         }
     }
 
-    private QueryPager maybeRecreate(QueryPager pager, ReadQuery command, boolean testPagingState, int protocolVersion)
+    private QueryPager maybeRecreate(QueryPager pager, ReadQuery command, boolean testPagingState, ProtocolVersion protocolVersion)
     {
         if (!testPagingState)
             return pager;
@@ -229,7 +230,7 @@
     @Test
     public void namesQueryTest() throws Exception
     {
-        QueryPager pager = namesQuery("k0", "c1", "c5", "c7", "c8").getPager(null, Server.CURRENT_VERSION);
+        QueryPager pager = namesQuery("k0", "c1", "c5", "c7", "c8").getPager(null, ProtocolVersion.CURRENT);
 
         assertFalse(pager.isExhausted());
         List<FilteredPartition> partition = query(pager, 5, 4);
@@ -241,14 +242,14 @@
     @Test
     public void sliceQueryTest() throws Exception
     {
-        sliceQueryTest(false, Server.VERSION_3);
-        sliceQueryTest(true,  Server.VERSION_3);
+        sliceQueryTest(false, ProtocolVersion.V3);
+        sliceQueryTest(true,  ProtocolVersion.V3);
 
-        sliceQueryTest(false, Server.VERSION_4);
-        sliceQueryTest(true,  Server.VERSION_4);
+        sliceQueryTest(false, ProtocolVersion.V4);
+        sliceQueryTest(true,  ProtocolVersion.V4);
     }
 
-    public void sliceQueryTest(boolean testPagingState, int protocolVersion) throws Exception
+    public void sliceQueryTest(boolean testPagingState, ProtocolVersion protocolVersion) throws Exception
     {
         ReadCommand command = sliceQuery("k0", "c1", "c8", 10);
         QueryPager pager = command.getPager(null, protocolVersion);
@@ -275,14 +276,14 @@
     @Test
     public void reversedSliceQueryTest() throws Exception
     {
-        reversedSliceQueryTest(false, Server.VERSION_3);
-        reversedSliceQueryTest(true,  Server.VERSION_3);
+        reversedSliceQueryTest(false, ProtocolVersion.V3);
+        reversedSliceQueryTest(true,  ProtocolVersion.V3);
 
-        reversedSliceQueryTest(false, Server.VERSION_4);
-        reversedSliceQueryTest(true,  Server.VERSION_4);
+        reversedSliceQueryTest(false, ProtocolVersion.V4);
+        reversedSliceQueryTest(true,  ProtocolVersion.V4);
     }
 
-    public void reversedSliceQueryTest(boolean testPagingState, int protocolVersion) throws Exception
+    public void reversedSliceQueryTest(boolean testPagingState, ProtocolVersion protocolVersion) throws Exception
     {
         ReadCommand command = sliceQuery("k0", "c1", "c8", true, 10);
         QueryPager pager = command.getPager(null, protocolVersion);
@@ -309,14 +310,14 @@
     @Test
     public void multiQueryTest() throws Exception
     {
-        multiQueryTest(false, Server.VERSION_3);
-        multiQueryTest(true,  Server.VERSION_3);
+        multiQueryTest(false, ProtocolVersion.V3);
+        multiQueryTest(true,  ProtocolVersion.V3);
 
-        multiQueryTest(false, Server.VERSION_4);
-        multiQueryTest(true,  Server.VERSION_4);
+        multiQueryTest(false, ProtocolVersion.V4);
+        multiQueryTest(true,  ProtocolVersion.V4);
     }
 
-    public void multiQueryTest(boolean testPagingState, int protocolVersion) throws Exception
+    public void multiQueryTest(boolean testPagingState, ProtocolVersion protocolVersion) throws Exception
     {
         ReadQuery command = new SinglePartitionReadCommand.Group(new ArrayList<SinglePartitionReadCommand>()
         {{
@@ -348,14 +349,14 @@
     @Test
     public void rangeNamesQueryTest() throws Exception
     {
-        rangeNamesQueryTest(false, Server.VERSION_3);
-        rangeNamesQueryTest(true,  Server.VERSION_3);
+        rangeNamesQueryTest(false, ProtocolVersion.V3);
+        rangeNamesQueryTest(true,  ProtocolVersion.V3);
 
-        rangeNamesQueryTest(false, Server.VERSION_4);
-        rangeNamesQueryTest(true,  Server.VERSION_4);
+        rangeNamesQueryTest(false, ProtocolVersion.V4);
+        rangeNamesQueryTest(true,  ProtocolVersion.V4);
     }
 
-    public void rangeNamesQueryTest(boolean testPagingState, int protocolVersion) throws Exception
+    public void rangeNamesQueryTest(boolean testPagingState, ProtocolVersion protocolVersion) throws Exception
     {
         ReadCommand command = rangeNamesQuery("k0", "k5", 100, "c1", "c4", "c8");
         QueryPager pager = command.getPager(null, protocolVersion);
@@ -378,14 +379,14 @@
     @Test
     public void rangeSliceQueryTest() throws Exception
     {
-        rangeSliceQueryTest(false, Server.VERSION_3);
-        rangeSliceQueryTest(true,  Server.VERSION_3);
+        rangeSliceQueryTest(false, ProtocolVersion.V3);
+        rangeSliceQueryTest(true,  ProtocolVersion.V3);
 
-        rangeSliceQueryTest(false, Server.VERSION_4);
-        rangeSliceQueryTest(true,  Server.VERSION_4);
+        rangeSliceQueryTest(false, ProtocolVersion.V4);
+        rangeSliceQueryTest(true,  ProtocolVersion.V4);
     }
 
-    public void rangeSliceQueryTest(boolean testPagingState, int protocolVersion) throws Exception
+    public void rangeSliceQueryTest(boolean testPagingState, ProtocolVersion protocolVersion) throws Exception
     {
         ReadCommand command = rangeSliceQuery("k1", "k5", 100, "c1", "c7");
         QueryPager pager = command.getPager(null, protocolVersion);
@@ -444,7 +445,7 @@
 
         ReadCommand command = SinglePartitionReadCommand.create(cfs.metadata, nowInSec, Util.dk("k0"), Slice.ALL);
 
-        QueryPager pager = command.getPager(null, Server.CURRENT_VERSION);
+        QueryPager pager = command.getPager(null, ProtocolVersion.CURRENT);
 
         for (int i = 0; i < 5; i++)
         {
@@ -458,7 +459,7 @@
     public void pagingReversedQueriesWithStaticColumnsTest() throws Exception
     {
         // There was a bug in paging for reverse queries when the schema includes static columns in
-        // 2.1 & 2.2. This was never a problem in 3.0, this test just guards against regressions
+        // 2.1 & 2.2. This was never a problem in 3.0, so this test just guards against regressions
         // see CASSANDRA-13222
 
         // insert some rows into a single partition
@@ -475,15 +476,15 @@
     {
         ClusteringIndexFilter rowfilter = new ClusteringIndexSliceFilter(Slices.ALL, reversed);
         ReadCommand command = SinglePartitionReadCommand.create(cfm, nowInSec, Util.dk(key), ColumnFilter.all(cfm), rowfilter);
-        QueryPager pager = command.getPager(null, Server.CURRENT_VERSION);
+        QueryPager pager = command.getPager(null, ProtocolVersion.CURRENT);
 
         ColumnDefinition staticColumn = cfm.partitionColumns().statics.getSimple(0);
         assertEquals(staticColumn.name.toCQLString(), "st");
 
         for (int i=0; i<5; i++)
         {
-            try (ReadOrderGroup orderGroup = pager.startOrderGroup();
-                 PartitionIterator partitions = pager.fetchPageInternal(1, orderGroup))
+            try (ReadExecutionController controller = pager.executionController();
+                 PartitionIterator partitions = pager.fetchPageInternal(1, controller))
             {
                 try (RowIterator partition = partitions.next())
                 {
@@ -503,8 +504,8 @@
         }
 
         // After processing the 5 rows there should be no more rows to return
-        try ( ReadOrderGroup orderGroup = pager.startOrderGroup();
-              PartitionIterator partitions = pager.fetchPageInternal(1, orderGroup))
+        try ( ReadExecutionController controller = pager.executionController();
+              PartitionIterator partitions = pager.fetchPageInternal(1, controller))
         {
             assertFalse(partitions.hasNext());
         }
diff --git a/test/unit/org/apache/cassandra/service/RMIServerSocketFactoryImplTest.java b/test/unit/org/apache/cassandra/service/RMIServerSocketFactoryImplTest.java
index 393dfe1..fad3c78 100644
--- a/test/unit/org/apache/cassandra/service/RMIServerSocketFactoryImplTest.java
+++ b/test/unit/org/apache/cassandra/service/RMIServerSocketFactoryImplTest.java
@@ -36,7 +36,7 @@
     @Test
     public void testReusableAddrSocket() throws IOException
     {
-        RMIServerSocketFactory serverFactory = new RMIServerSocketFactoryImpl();
+        RMIServerSocketFactory serverFactory = new RMIServerSocketFactoryImpl(null);
         ServerSocket socket = serverFactory.createServerSocket(7199);
         assertTrue(socket.getReuseAddress());
     }
diff --git a/test/unit/org/apache/cassandra/service/RemoveTest.java b/test/unit/org/apache/cassandra/service/RemoveTest.java
index 800c904..f4b203c 100644
--- a/test/unit/org/apache/cassandra/service/RemoveTest.java
+++ b/test/unit/org/apache/cassandra/service/RemoveTest.java
@@ -22,6 +22,7 @@
 import java.io.IOException;
 import java.net.InetAddress;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
@@ -29,12 +30,16 @@
 
 import org.junit.*;
 
-import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
+import org.apache.cassandra.concurrent.NamedThreadFactory;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.RandomPartitioner;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.gms.ApplicationState;
+import org.apache.cassandra.gms.Gossiper;
+import org.apache.cassandra.gms.VersionedValue.VersionedValueFactory;
 import org.apache.cassandra.locator.TokenMetadata;
 import org.apache.cassandra.net.MessageOut;
 import org.apache.cassandra.net.MessagingService;
@@ -45,6 +50,11 @@
 
 public class RemoveTest
 {
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     static final IPartitioner partitioner = RandomPartitioner.instance;
     StorageService ss = StorageService.instance;
     TokenMetadata tmd = ss.getTokenMetadata();
@@ -60,7 +70,6 @@
     public static void setupClass() throws ConfigurationException
     {
         oldPartitioner = StorageService.instance.setPartitionerUnsafe(partitioner);
-        SchemaLoader.prepareServer();
     }
 
     @AfterClass
@@ -106,28 +115,44 @@
         ss.removeNode(hostIds.get(0).toString());
     }
 
+    @Test(expected = UnsupportedOperationException.class)
+    public void testNonmemberId()
+    {
+        VersionedValueFactory valueFactory = new VersionedValueFactory(DatabaseDescriptor.getPartitioner());
+        Collection<Token> tokens = Collections.singleton(DatabaseDescriptor.getPartitioner().getRandomToken());
+
+        InetAddress joininghost = hosts.get(4);
+        UUID joiningId = hostIds.get(4);
+
+        hosts.remove(joininghost);
+        hostIds.remove(joiningId);
+
+        // Change a node to a bootstrapping node that is not yet a member of the ring
+        Gossiper.instance.injectApplicationState(joininghost, ApplicationState.TOKENS, valueFactory.tokens(tokens));
+        ss.onChange(joininghost, ApplicationState.STATUS, valueFactory.bootstrapping(tokens));
+
+        ss.removeNode(joiningId.toString());
+    }
+
     @Test
     public void testRemoveHostId() throws InterruptedException
     {
         // start removal in background and send replication confirmations
         final AtomicBoolean success = new AtomicBoolean(false);
-        Thread remover = new Thread()
+        Thread remover = NamedThreadFactory.createThread(() ->
         {
-            public void run()
+            try
             {
-                try
-                {
-                    ss.removeNode(removalId.toString());
-                }
-                catch (Exception e)
-                {
-                    System.err.println(e);
-                    e.printStackTrace();
-                    return;
-                }
-                success.set(true);
+                ss.removeNode(removalId.toString());
             }
-        };
+            catch (Exception e)
+            {
+                System.err.println(e);
+                e.printStackTrace();
+                return;
+            }
+            success.set(true);
+        });
         remover.start();
 
         Thread.sleep(1000); // make sure removal is waiting for confirmation
diff --git a/test/unit/org/apache/cassandra/service/SSTablesGlobalTrackerTest.java b/test/unit/org/apache/cassandra/service/SSTablesGlobalTrackerTest.java
index cde7242..e4a5947 100644
--- a/test/unit/org/apache/cassandra/service/SSTablesGlobalTrackerTest.java
+++ b/test/unit/org/apache/cassandra/service/SSTablesGlobalTrackerTest.java
@@ -27,7 +27,6 @@
 
 import org.junit.Test;
 
-import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.VersionAndType;
@@ -110,17 +109,14 @@
 
     private Gen<SSTableFormat.Type> sstableFormatTypes()
     {
-        // We remap LEGACY format to BIG as is not used in real life and will not work properly because the
-        // SSTableFormat.Type.validate() method never returns it.
-        return Generate.enumValues(SSTableFormat.Type.class)
-                       .map(t -> t == SSTableFormat.Type.LEGACY ? SSTableFormat.Type.BIG : t);
+        return Generate.enumValues(SSTableFormat.Type.class);
     }
 
     private Gen<String> sstableVersionString()
     {
         // We want to somewhat favor the current version, as that is technically more realistic so we generate it 50%
         // of the time, and generate something random 50% of the time.
-        return Generate.constant(DatabaseDescriptor.getSSTableFormat().info.getLatestVersion().getVersion())
+        return Generate.constant(SSTableFormat.Type.current().info.getLatestVersion().getVersion())
                        .mix(strings().betweenCodePoints('a', 'z').ofLength(2));
     }
 
diff --git a/test/unit/org/apache/cassandra/service/SerializationsTest.java b/test/unit/org/apache/cassandra/service/SerializationsTest.java
index 847bcea..4df112a 100644
--- a/test/unit/org/apache/cassandra/service/SerializationsTest.java
+++ b/test/unit/org/apache/cassandra/service/SerializationsTest.java
@@ -31,6 +31,7 @@
 import org.apache.cassandra.AbstractSerializationsTester;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.Util.PartitionerSwitcher;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.RandomPartitioner;
 import org.apache.cassandra.dht.Range;
@@ -56,6 +57,7 @@
     @BeforeClass
     public static void defineSchema() throws Exception
     {
+        DatabaseDescriptor.daemonInitialization();
         partitionerSwitcher = Util.switchPartitioner(RandomPartitioner.instance);
         RANDOM_UUID = UUID.fromString("b5c3d033-75aa-4c2f-a819-947aac7a0c54");
         FULL_RANGE = new Range<>(Util.testPartitioner().getMinimumToken(), Util.testPartitioner().getMinimumToken());
diff --git a/test/unit/org/apache/cassandra/service/StartupChecksTest.java b/test/unit/org/apache/cassandra/service/StartupChecksTest.java
index 0f30d3c..63a11c3 100644
--- a/test/unit/org/apache/cassandra/service/StartupChecksTest.java
+++ b/test/unit/org/apache/cassandra/service/StartupChecksTest.java
@@ -27,6 +27,7 @@
 
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.exceptions.StartupException;
 import org.apache.cassandra.io.util.FileUtils;
@@ -50,9 +51,9 @@
     @Before
     public void setup() throws IOException
     {
-        for (ColumnFamilyStore cfs : Keyspace.open(SystemKeyspace.NAME).getColumnFamilyStores())
+        for (ColumnFamilyStore cfs : Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStores())
             cfs.clearUnsafe();
-        for (File dataDir : Directories.getKSChildDirectories(SystemKeyspace.NAME))
+        for (File dataDir : Directories.getKSChildDirectories(SchemaConstants.SYSTEM_KEYSPACE_NAME))
             FileUtils.deleteRecursive(dataDir);
 
         File dataDir = new File(DatabaseDescriptor.getAllDataFileLocations()[0]);
diff --git a/test/unit/org/apache/cassandra/service/StorageProxyTest.java b/test/unit/org/apache/cassandra/service/StorageProxyTest.java
index 42eb1f5..bdf45fe 100644
--- a/test/unit/org/apache/cassandra/service/StorageProxyTest.java
+++ b/test/unit/org/apache/cassandra/service/StorageProxyTest.java
@@ -78,6 +78,7 @@
     @BeforeClass
     public static void beforeClass() throws Throwable
     {
+        DatabaseDescriptor.daemonInitialization();
         DatabaseDescriptor.getHintsDirectory().mkdir();
         TokenMetadata tmd = StorageService.instance.getTokenMetadata();
         tmd.updateNormalToken(token("1"), InetAddress.getByName("127.0.0.1"));
diff --git a/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java b/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java
index a939ef7..33bdceb 100644
--- a/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java
+++ b/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java
@@ -24,11 +24,18 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.net.InetAddress;
-import java.util.*;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
 
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Multimap;
+
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -36,8 +43,8 @@
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.schema.KeyspaceMetadata;
 import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.config.SchemaConstants;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.WindowsFailedSnapshotTracker;
 import org.apache.cassandra.dht.Murmur3Partitioner;
@@ -49,9 +56,10 @@
 import org.apache.cassandra.locator.IEndpointSnitch;
 import org.apache.cassandra.locator.PropertyFileSnitch;
 import org.apache.cassandra.locator.TokenMetadata;
+import org.apache.cassandra.schema.KeyspaceMetadata;
 import org.apache.cassandra.schema.KeyspaceParams;
 import org.apache.cassandra.schema.ReplicationParams;
-import org.apache.cassandra.schema.SchemaKeyspace;
+import org.apache.cassandra.schema.SchemaKeyspaceTables;
 import org.apache.cassandra.utils.FBUtilities;
 import org.mockito.Mockito;
 
@@ -66,7 +74,7 @@
     @BeforeClass
     public static void setUp() throws ConfigurationException
     {
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
         IEndpointSnitch snitch = new PropertyFileSnitch();
         DatabaseDescriptor.setEndpointSnitch(snitch);
         Keyspace.setInitialized();
@@ -98,10 +106,10 @@
     }
 
     @Test
-    public void testSnapshot() throws IOException
+    public void testSnapshotWithFlush() throws IOException
     {
         // no need to insert extra data, even an "empty" database will have a little information in the system keyspace
-        StorageService.instance.takeSnapshot("snapshot");
+        StorageService.instance.takeSnapshot(UUID.randomUUID().toString());
     }
 
     private void checkTempFilePresence(File f, boolean exist)
@@ -121,7 +129,7 @@
     @Test
     public void testSnapshotFailureHandler() throws IOException
     {
-        assumeTrue(FBUtilities.isWindows());
+        assumeTrue(FBUtilities.isWindows);
 
         // Initial "run" of Cassandra, nothing in failed snapshot file
         WindowsFailedSnapshotTracker.deleteOldSnapshots();
@@ -177,7 +185,14 @@
     public void testTableSnapshot() throws IOException
     {
         // no need to insert extra data, even an "empty" database will have a little information in the system keyspace
-        StorageService.instance.takeTableSnapshot(SchemaKeyspace.NAME, SchemaKeyspace.KEYSPACES, "cf_snapshot");
+        StorageService.instance.takeTableSnapshot(SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.KEYSPACES, UUID.randomUUID().toString());
+    }
+
+    @Test
+    public void testSnapshot() throws IOException
+    {
+        // no need to insert extra data, even an "empty" database will have a little information in the system keyspace
+        StorageService.instance.takeSnapshot(UUID.randomUUID().toString(), SchemaConstants.SCHEMA_KEYSPACE_NAME);
     }
 
     @Test
@@ -589,19 +604,13 @@
     }
 
     @Test
-    public void testGetConcurrentCompactors() throws Exception
-    {
-        assert StorageService.instance.getConcurrentCompactors() == 4;
-    }
-
-    @Test
     public void testRebuildFailOnNonExistingDatacenter() throws Exception
     {
         String nonExistentDC = "NON_EXISTENT_DC";
 
         try
         {
-            getStorageService().rebuild(nonExistentDC);
+            getStorageService().rebuild(nonExistentDC, "StorageServiceServerTest", null, null);
             fail();
         }
         catch (IllegalArgumentException ex)
@@ -613,6 +622,20 @@
         }
     }
 
+    @Test
+    public void testRebuildingWithTokensWithoutKeyspace() throws Exception
+    {
+        try
+        {
+            getStorageService().rebuild("datacenter1", null, "123", null);
+            fail();
+        }
+        catch (IllegalArgumentException ex)
+        {
+            assertEquals("Cannot specify tokens without keyspace.", ex.getMessage());
+        }
+    }
+
     private StorageService getStorageService() throws Exception
     {
         ImmutableMultimap.Builder<String, InetAddress> builder = ImmutableMultimap.builder();
diff --git a/test/unit/org/apache/cassandra/service/pager/PagingStateTest.java b/test/unit/org/apache/cassandra/service/pager/PagingStateTest.java
index 8e48771..58e8635 100644
--- a/test/unit/org/apache/cassandra/service/pager/PagingStateTest.java
+++ b/test/unit/org/apache/cassandra/service/pager/PagingStateTest.java
@@ -21,44 +21,23 @@
 import java.io.IOException;
 import java.nio.ByteBuffer;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
-import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.ColumnDefinition;
-import org.apache.cassandra.cql3.ColumnIdentifier;
-import org.apache.cassandra.db.*;
-import org.apache.cassandra.db.rows.*;
-import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.Util;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
-import static org.apache.cassandra.transport.Server.VERSION_3;
-import static org.apache.cassandra.transport.Server.VERSION_4;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 public class PagingStateTest
 {
-    private PagingState makeSomePagingState(int protocolVersion)
+    @BeforeClass
+    public static void setupDD()
     {
-        return makeSomePagingState(protocolVersion, 0);
-    }
-
-    private PagingState makeSomePagingState(int protocolVersion, int remainingInPartition)
-    {
-        CFMetaData metadata = CFMetaData.Builder.create("ks", "tbl")
-                                                .addPartitionKey("k", AsciiType.instance)
-                                                .addClusteringColumn("c1", AsciiType.instance)
-                                                .addClusteringColumn("c1", Int32Type.instance)
-                                                .addRegularColumn("myCol", AsciiType.instance)
-                                                .build();
-
-        ByteBuffer pk = ByteBufferUtil.bytes("someKey");
-
-        ColumnDefinition def = metadata.getColumnDefinition(new ColumnIdentifier("myCol", false));
-        Clustering c = new Clustering(ByteBufferUtil.bytes("c1"), ByteBufferUtil.bytes(42));
-        Row row = BTreeRow.singleCellRow(c, BufferCell.live(metadata, def, 0, ByteBufferUtil.EMPTY_BYTE_BUFFER));
-        PagingState.RowMark mark = PagingState.RowMark.create(metadata, row, protocolVersion);
-        return new PagingState(pk, mark, 10, remainingInPartition);
+        DatabaseDescriptor.daemonInitialization();
     }
 
     @Test
@@ -76,9 +55,9 @@
          *     PagingState state = new PagingState(pk, cn.toByteBuffer(), 10);
          *     System.out.println("PagingState = " + ByteBufferUtil.bytesToHex(state.serialize()));
          */
-        PagingState state = makeSomePagingState(VERSION_3);
+        PagingState state = Util.makeSomePagingState(ProtocolVersion.V3);
 
-        String serializedState = ByteBufferUtil.bytesToHex(state.serialize(VERSION_3));
+        String serializedState = ByteBufferUtil.bytesToHex(state.serialize(ProtocolVersion.V3));
         // Note that we don't assert exact equality because we know 3.0 nodes include the "remainingInPartition" number
         // that is not present on 2.1/2.2 nodes. We know this is ok however because we know that 2.1/2.2 nodes will ignore
         // anything remaining once they have properly deserialized a paging state.
@@ -88,54 +67,54 @@
     @Test
     public void testSerializeV3DeserializeV3()
     {
-        PagingState state = makeSomePagingState(VERSION_3);
-        ByteBuffer serialized = state.serialize(VERSION_3);
-        assertEquals(serialized.remaining(), state.serializedSize(VERSION_3));
-        assertEquals(state, PagingState.deserialize(serialized, VERSION_3));
+        PagingState state = Util.makeSomePagingState(ProtocolVersion.V3);
+        ByteBuffer serialized = state.serialize(ProtocolVersion.V3);
+        assertEquals(serialized.remaining(), state.serializedSize(ProtocolVersion.V3));
+        assertEquals(state, PagingState.deserialize(serialized, ProtocolVersion.V3));
     }
 
     @Test
     public void testSerializeV4DeserializeV4()
     {
-        PagingState state = makeSomePagingState(VERSION_4);
-        ByteBuffer serialized = state.serialize(VERSION_4);
-        assertEquals(serialized.remaining(), state.serializedSize(VERSION_4));
-        assertEquals(state, PagingState.deserialize(serialized, VERSION_4));
+        PagingState state = Util.makeSomePagingState(ProtocolVersion.V4);
+        ByteBuffer serialized = state.serialize(ProtocolVersion.V4);
+        assertEquals(serialized.remaining(), state.serializedSize(ProtocolVersion.V4));
+        assertEquals(state, PagingState.deserialize(serialized, ProtocolVersion.V4));
     }
 
     @Test
     public void testSerializeV3DeserializeV4()
     {
-        PagingState state = makeSomePagingState(VERSION_3);
-        ByteBuffer serialized = state.serialize(VERSION_3);
-        assertEquals(serialized.remaining(), state.serializedSize(VERSION_3));
-        assertEquals(state, PagingState.deserialize(serialized, VERSION_4));
+        PagingState state = Util.makeSomePagingState(ProtocolVersion.V3);
+        ByteBuffer serialized = state.serialize(ProtocolVersion.V3);
+        assertEquals(serialized.remaining(), state.serializedSize(ProtocolVersion.V3));
+        assertEquals(state, PagingState.deserialize(serialized, ProtocolVersion.V4));
     }
 
     @Test
     public void testSerializeV4DeserializeV3()
     {
-        PagingState state = makeSomePagingState(VERSION_4);
-        ByteBuffer serialized = state.serialize(VERSION_4);
-        assertEquals(serialized.remaining(), state.serializedSize(VERSION_4));
-        assertEquals(state, PagingState.deserialize(serialized, VERSION_3));
+        PagingState state = Util.makeSomePagingState(ProtocolVersion.V4);
+        ByteBuffer serialized = state.serialize(ProtocolVersion.V4);
+        assertEquals(serialized.remaining(), state.serializedSize(ProtocolVersion.V4));
+        assertEquals(state, PagingState.deserialize(serialized, ProtocolVersion.V3));
     }
 
     @Test
     public void testSerializeV3WithoutRemainingInPartitionDeserializeV3() throws IOException
     {
-        PagingState state = makeSomePagingState(VERSION_3, Integer.MAX_VALUE);
+        PagingState state = Util.makeSomePagingState(ProtocolVersion.V3, Integer.MAX_VALUE);
         ByteBuffer serialized = state.legacySerialize(false);
         assertEquals(serialized.remaining(), state.legacySerializedSize(false));
-        assertEquals(state, PagingState.deserialize(serialized, VERSION_3));
+        assertEquals(state, PagingState.deserialize(serialized, ProtocolVersion.V3));
     }
 
     @Test
     public void testSerializeV3WithoutRemainingInPartitionDeserializeV4() throws IOException
     {
-        PagingState state = makeSomePagingState(VERSION_3, Integer.MAX_VALUE);
+        PagingState state = Util.makeSomePagingState(ProtocolVersion.V3, Integer.MAX_VALUE);
         ByteBuffer serialized = state.legacySerialize(false);
         assertEquals(serialized.remaining(), state.legacySerializedSize(false));
-        assertEquals(state, PagingState.deserialize(serialized, VERSION_4));
+        assertEquals(state, PagingState.deserialize(serialized, ProtocolVersion.V4));
     }
 }
diff --git a/test/unit/org/apache/cassandra/service/pager/RandomizedPagingStateTest.java b/test/unit/org/apache/cassandra/service/pager/RandomizedPagingStateTest.java
new file mode 100644
index 0000000..4c9592a
--- /dev/null
+++ b/test/unit/org/apache/cassandra/service/pager/RandomizedPagingStateTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.service.pager;
+
+import java.nio.ByteBuffer;
+import java.util.Random;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.BufferClustering;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.rows.BTreeRow;
+import org.apache.cassandra.db.rows.BufferCell;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.transport.ProtocolVersion;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+public class RandomizedPagingStateTest
+{
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
+    private static final Random rnd = new Random();
+    private static final int ROUNDS = 50_000;
+    private static final int MAX_PK_SIZE = 3000;
+    private static final int MAX_CK_SIZE = 3000;
+    private static final int MAX_REMAINING = 5000;
+
+    @Test
+    public void testFormatChecksPkOnly()
+    {
+        rnd.setSeed(1);
+        CFMetaData metadata = CFMetaData.Builder.create("ks", "tbl")
+                                                .addPartitionKey("pk", BytesType.instance)
+                                                .addRegularColumn("v", LongType.instance)
+                                                .build();
+        Row row =  BTreeRow.emptyRow(Clustering.EMPTY);
+        for (int i = 0; i < ROUNDS; i++)
+            checkState(metadata, MAX_PK_SIZE, row);
+    }
+
+    @Test
+    public void testFormatChecksPkAndCk()
+    {
+        rnd.setSeed(1);
+        CFMetaData metadata = CFMetaData.Builder.create("ks", "tbl")
+                                                .addPartitionKey("pk", BytesType.instance)
+                                                .addClusteringColumn("ck", BytesType.instance)
+                                                .addRegularColumn("v", LongType.instance)
+                                                .build();
+        for (int i = 0; i < ROUNDS; i++)
+        {
+            ByteBuffer ckBytes = ByteBuffer.allocate(rnd.nextInt(MAX_CK_SIZE) + 1);
+            for (int j = 0; j < ckBytes.limit(); j++)
+                ckBytes.put((byte) rnd.nextInt());
+            ckBytes.flip().rewind();
+
+            Clustering c = new BufferClustering(ckBytes);
+            Row row = BTreeRow.singleCellRow(c,
+                                             BufferCell.live(metadata.partitionColumns().iterator().next(),
+                                                             0,
+                                                             ByteBufferUtil.EMPTY_BYTE_BUFFER));
+
+            checkState(metadata, 1, row);
+        }
+    }
+
+    private static void checkState(CFMetaData metadata, int maxPkSize, Row row)
+    {
+        PagingState.RowMark mark = PagingState.RowMark.create(metadata, row, ProtocolVersion.V3);
+        ByteBuffer pkBytes = ByteBuffer.allocate(rnd.nextInt(maxPkSize) + 1);
+        for (int j = 0; j < pkBytes.limit(); j++)
+            pkBytes.put((byte) rnd.nextInt());
+        pkBytes.flip().rewind();
+
+        PagingState state = new PagingState(pkBytes, mark, rnd.nextInt(MAX_REMAINING) + 1, rnd.nextInt(MAX_REMAINING) + 1);
+        ByteBuffer serialized = state.serialize(ProtocolVersion.V3);
+        Assert.assertTrue(PagingState.isLegacySerialized(serialized));
+        Assert.assertFalse(PagingState.isModernSerialized(serialized));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/service/reads/DigestResolverTest.java b/test/unit/org/apache/cassandra/service/reads/DigestResolverTest.java
index d246a83..dd97aaa 100644
--- a/test/unit/org/apache/cassandra/service/reads/DigestResolverTest.java
+++ b/test/unit/org/apache/cassandra/service/reads/DigestResolverTest.java
@@ -101,7 +101,7 @@
     {
         DecoratedKey dk = Util.dk("key1");
         SinglePartitionReadCommand command = SinglePartitionReadCommand.fullPartitionRead(cfm, FBUtilities.nowInSeconds(), dk);
-        BufferCell cell = BufferCell.live(cfm, cfm.partitionColumns().regulars.getSimple(0), 1000, bytes("1"));
+        BufferCell cell = BufferCell.live(cfm.partitionColumns().regulars.getSimple(0), 1000, bytes("1"));
         PartitionUpdate response = PartitionUpdate.singleRowUpdate(cfm, dk, BTreeRow.singleCellRow(cfm.comparator.make("1"), cell));
 
         ExecutorService pool = Executors.newFixedThreadPool(2);
@@ -112,7 +112,7 @@
             while (System.nanoTime() < endTime)
             {
                 final DigestResolver resolver = new DigestResolver(ks, command, ConsistencyLevel.ONE, 2);
-                final ReadCallback callback = new ReadCallback(resolver, ConsistencyLevel.ONE, command, ImmutableList.of(EP1.address, EP2.address));
+                final ReadCallback callback = new ReadCallback(resolver, ConsistencyLevel.ONE, command, ImmutableList.of(EP1.address, EP2.address), System.nanoTime());
                 
                 final CountDownLatch startlatch = new CountDownLatch(2);
 
diff --git a/test/unit/org/apache/cassandra/streaming/SessionInfoTest.java b/test/unit/org/apache/cassandra/streaming/SessionInfoTest.java
index 80ef29d..37a6e41 100644
--- a/test/unit/org/apache/cassandra/streaming/SessionInfoTest.java
+++ b/test/unit/org/apache/cassandra/streaming/SessionInfoTest.java
@@ -25,10 +25,18 @@
 
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.utils.FBUtilities;
+import org.junit.BeforeClass;
 
 public class SessionInfoTest
 {
+    @BeforeClass
+    public static void beforeClass()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     /**
      * Test if total numbers are collect
      */
diff --git a/test/unit/org/apache/cassandra/streaming/StreamManagerTest.java b/test/unit/org/apache/cassandra/streaming/StreamManagerTest.java
index 69db960..625d9d5 100644
--- a/test/unit/org/apache/cassandra/streaming/StreamManagerTest.java
+++ b/test/unit/org/apache/cassandra/streaming/StreamManagerTest.java
@@ -40,6 +40,7 @@
         Config c = DatabaseDescriptor.loadConfig();
         defaultStreamThroughputMbPerSec = c.stream_throughput_outbound_megabits_per_sec;
         defaultInterDCStreamThroughputMbPerSec = c.inter_dc_stream_throughput_outbound_megabits_per_sec;
+        DatabaseDescriptor.daemonInitialization(() -> c);
     }
 
     @Test
diff --git a/test/unit/org/apache/cassandra/streaming/StreamTransferTaskTest.java b/test/unit/org/apache/cassandra/streaming/StreamTransferTaskTest.java
index 185498f..04be91a 100644
--- a/test/unit/org/apache/cassandra/streaming/StreamTransferTaskTest.java
+++ b/test/unit/org/apache/cassandra/streaming/StreamTransferTaskTest.java
@@ -119,7 +119,7 @@
     public void testFailSessionDuringTransferShouldNotReleaseReferences() throws Exception
     {
         InetAddress peer = FBUtilities.getBroadcastAddress();
-        StreamCoordinator streamCoordinator = new StreamCoordinator(1, true, false, null);
+        StreamCoordinator streamCoordinator = new StreamCoordinator(1, true, false, null, false);
         StreamResultFuture future = StreamResultFuture.init(UUID.randomUUID(), "", Collections.<StreamEventHandler>emptyList(), streamCoordinator);
         StreamSession session = new StreamSession(peer, peer, null, 0, true, false);
         session.init(future);
diff --git a/test/unit/org/apache/cassandra/streaming/StreamingTransferTest.java b/test/unit/org/apache/cassandra/streaming/StreamingTransferTest.java
index a89754f..8f3061a 100644
--- a/test/unit/org/apache/cassandra/streaming/StreamingTransferTest.java
+++ b/test/unit/org/apache/cassandra/streaming/StreamingTransferTest.java
@@ -62,6 +62,11 @@
 {
     private static final Logger logger = LoggerFactory.getLogger(StreamingTransferTest.class);
 
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     public static final InetAddress LOCAL = FBUtilities.getBroadcastAddress();
     public static final String KEYSPACE1 = "StreamingTransferTest1";
     public static final String CF_STANDARD = "Standard1";
@@ -299,7 +304,7 @@
     {
         final Keyspace keyspace = Keyspace.open(KEYSPACE1);
         final ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF_INDEX);
-        cfs.disableAutoCompaction();
+
         List<String> keys = createAndTransfer(cfs, new Mutator()
         {
             public void mutate(String key, String col, long timestamp) throws Exception
@@ -363,7 +368,7 @@
 
 
         updates = new RowUpdateBuilder(cfs.metadata, FBUtilities.timestampMicros() + 1, key);
-        updates.addRangeTombstone(Slice.make(comparator.make(5), comparator.make(7)))
+        updates.addRangeTombstone(5, 7)
                 .build()
                 .apply();
 
diff --git a/test/unit/org/apache/cassandra/streaming/compression/CompressedInputStreamTest.java b/test/unit/org/apache/cassandra/streaming/compression/CompressedInputStreamTest.java
index 8512d8f..c0fc277 100644
--- a/test/unit/org/apache/cassandra/streaming/compression/CompressedInputStreamTest.java
+++ b/test/unit/org/apache/cassandra/streaming/compression/CompressedInputStreamTest.java
@@ -19,14 +19,16 @@
 
 import java.io.*;
 import java.util.*;
-import java.util.concurrent.SynchronousQueue;
-import java.util.concurrent.TimeUnit;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ClusteringComparator;
 import org.apache.cassandra.db.marshal.BytesType;
 import org.apache.cassandra.io.compress.CompressedSequentialWriter;
 import org.apache.cassandra.io.compress.CompressionMetadata;
+import org.apache.cassandra.io.util.SequentialWriterOption;
 import org.apache.cassandra.schema.CompressionParams;
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
@@ -43,6 +45,12 @@
  */
 public class CompressedInputStreamTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testCompressedRead() throws Exception
     {
@@ -82,7 +90,11 @@
         MetadataCollector collector = new MetadataCollector(new ClusteringComparator(BytesType.instance));
         CompressionParams param = CompressionParams.snappy(32);
         Map<Long, Long> index = new HashMap<Long, Long>();
-        try (CompressedSequentialWriter writer = new CompressedSequentialWriter(tmp, desc.filenameFor(Component.COMPRESSION_INFO), param, collector))
+        try (CompressedSequentialWriter writer = new CompressedSequentialWriter(tmp,
+                                                                                desc.filenameFor(Component.COMPRESSION_INFO),
+                                                                                null,
+                                                                                SequentialWriterOption.DEFAULT,
+                                                                                param, collector))
         {
             for (long l = 0L; l < 1000; l++)
             {
diff --git a/test/unit/org/apache/cassandra/tools/BulkLoaderTest.java b/test/unit/org/apache/cassandra/tools/BulkLoaderTest.java
new file mode 100644
index 0000000..104f288
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/BulkLoaderTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.tools;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.datastax.driver.core.exceptions.NoHostAvailableException;
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+import static org.junit.Assert.fail;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class BulkLoaderTest extends ToolsTester
+{
+    @Test
+    public void testBulkLoader_NoArgs()
+    {
+        runTool(1, "org.apache.cassandra.tools.BulkLoader");
+        assertNoUnexpectedThreadsStarted(null, null);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testBulkLoader_WithArgs() throws Exception
+    {
+        try
+        {
+            runTool(0, "org.apache.cassandra.tools.BulkLoader", "-d", "127.9.9.1", sstableDirName("legacy_sstables", "legacy_ma_simple"));
+            fail();
+        }
+        catch (RuntimeException e)
+        {
+            if (!(e.getCause() instanceof BulkLoadException))
+                throw e;
+            if (!(e.getCause().getCause() instanceof NoHostAvailableException))
+                throw e;
+        }
+        assertNoUnexpectedThreadsStarted(null, new String[]{"globalEventExecutor-1-1", "globalEventExecutor-1-2"});
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tools/CompactionStressTest.java b/test/unit/org/apache/cassandra/tools/CompactionStressTest.java
new file mode 100644
index 0000000..cdf8ac1
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/CompactionStressTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.tools;
+
+import java.io.File;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class CompactionStressTest extends ToolsTester
+{
+    @BeforeClass
+    public static void setupTester()
+    {
+        // override ToolsTester.setupTester() to avoid `DatabaseDescriptor.toolInitialization()`
+    }
+
+    @Test
+    public void testNoArgs()
+    {
+        runTool(0, "org.apache.cassandra.stress.CompactionStress");
+    }
+
+    @Test
+    public void testWriteAndCompact()
+    {
+        ClassLoader classLoader = getClass().getClassLoader();
+        File file = new File(classLoader.getResource("blogpost.yaml").getFile());
+        String profileFile = file.getAbsolutePath();
+
+        runTool(0,
+                "org.apache.cassandra.stress.CompactionStress",
+                "write",
+                "-d", "build/test/cassandra",
+                "-g", "0",
+                "-p", profileFile,
+                "-t", "4");
+
+        runTool(0,
+                "org.apache.cassandra.stress.CompactionStress",
+                "compact",
+                "-d", "build/test/cassandra",
+                "-p", profileFile,
+                "-t", "4");
+    }
+
+}
diff --git a/test/unit/org/apache/cassandra/tools/GetVersionTest.java b/test/unit/org/apache/cassandra/tools/GetVersionTest.java
new file mode 100644
index 0000000..84e2f49
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/GetVersionTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.tools;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class GetVersionTest extends ToolsTester
+{
+    @Test
+    public void testGetVersion()
+    {
+        runTool(0, "org.apache.cassandra.tools.GetVersion");
+        assertNoUnexpectedThreadsStarted(null, null);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tools/LoaderOptionsTest.java b/test/unit/org/apache/cassandra/tools/LoaderOptionsTest.java
new file mode 100644
index 0000000..67258ad
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/LoaderOptionsTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.tools;
+import java.io.File;
+import java.nio.file.Paths;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+
+import static org.apache.cassandra.tools.ToolsTester.sstableDirName;
+import static org.junit.Assert.*;
+
+// LoaderOptionsTester for custom configuration
+public class LoaderOptionsTest
+{
+    @Test
+    public void testNativePort() throws Exception {
+        //Default Cassandra config
+        File config = Paths.get(".", "test", "conf", "cassandra.yaml").normalize().toFile();
+        String[] args = {"-d", "127.9.9.1", "-f", config.getAbsolutePath(), sstableDirName("legacy_sstables", "legacy_ma_simple")};
+        LoaderOptions options = LoaderOptions.builder().parseArgs(args).build();
+        assertEquals(9042, options.nativePort);
+
+
+        // SSL Enabled Cassandra config
+        config = Paths.get(".", "test", "conf", "unit-test-conf/test-native-port.yaml").normalize().toFile();
+        String[] args2 = {"-d", "127.9.9.1", "-f", config.getAbsolutePath(), sstableDirName("legacy_sstables", "legacy_ma_simple")};
+        options = LoaderOptions.builder().parseArgs(args2).build();
+        assertEquals(9142, options.nativePort);
+    }
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/tools/SSTableExpiredBlockersTest.java b/test/unit/org/apache/cassandra/tools/SSTableExpiredBlockersTest.java
new file mode 100644
index 0000000..1c09174
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/SSTableExpiredBlockersTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.tools;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class SSTableExpiredBlockersTest extends ToolsTester
+{
+    @Test
+    public void testSSTableExpiredBlockers_NoArgs()
+    {
+        runTool(1, "org.apache.cassandra.tools.SSTableExpiredBlockers");
+        assertNoUnexpectedThreadsStarted(null, null);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testSSTableExpiredBlockers_WithArgs()
+    {
+        // returns exit code 1, since no sstables are there
+        runTool(1, "org.apache.cassandra.tools.SSTableExpiredBlockers", "system_schema", "tables");
+        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaLoaded();
+        assertServerNotLoaded();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tools/SSTableExportTest.java b/test/unit/org/apache/cassandra/tools/SSTableExportTest.java
new file mode 100644
index 0000000..0e49bb5
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/SSTableExportTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.tools;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class SSTableExportTest extends ToolsTester
+{
+    @Test
+    public void testSSTableExport_NoArgs()
+    {
+        runTool(1, "org.apache.cassandra.tools.SSTableExport");
+        assertNoUnexpectedThreadsStarted(null, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testSSTableExport_WithArgs() throws Exception
+    {
+        runTool(0, "org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"));
+        assertNoUnexpectedThreadsStarted(null, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertServerNotLoaded();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tools/SSTableLevelResetterTest.java b/test/unit/org/apache/cassandra/tools/SSTableLevelResetterTest.java
new file mode 100644
index 0000000..b70fcef
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/SSTableLevelResetterTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.tools;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class SSTableLevelResetterTest extends ToolsTester
+{
+    @Test
+    public void testSSTableLevelResetter_NoArgs()
+    {
+        runTool(1, "org.apache.cassandra.tools.SSTableLevelResetter");
+        assertNoUnexpectedThreadsStarted(null, null);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testSSTableLevelResetter_WithArgs()
+    {
+        runTool(0, "org.apache.cassandra.tools.SSTableLevelResetter", "--really-reset", "system_schema", "tables");
+        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaLoaded();
+        assertServerNotLoaded();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java b/test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java
new file mode 100644
index 0000000..8b782e3
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.tools;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class SSTableMetadataViewerTest extends ToolsTester
+{
+    @Test
+    public void testSSTableOfflineRelevel_NoArgs()
+    {
+        runTool(1, "org.apache.cassandra.tools.SSTableMetadataViewer");
+        assertNoUnexpectedThreadsStarted(null, null);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testSSTableOfflineRelevel_WithArgs()
+    {
+        runTool(0, "org.apache.cassandra.tools.SSTableMetadataViewer", "ks", "tab");
+        assertNoUnexpectedThreadsStarted(null, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tools/SSTableOfflineRelevelTest.java b/test/unit/org/apache/cassandra/tools/SSTableOfflineRelevelTest.java
new file mode 100644
index 0000000..0c6eecf
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/SSTableOfflineRelevelTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.tools;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class SSTableOfflineRelevelTest extends ToolsTester
+{
+    @Test
+    public void testSSTableOfflineRelevel_NoArgs()
+    {
+        runTool(1, "org.apache.cassandra.tools.SSTableOfflineRelevel");
+        assertNoUnexpectedThreadsStarted(null, null);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testSSTableOfflineRelevel_WithArgs()
+    {
+        // Note: SSTableOfflineRelevel exits with code 1 if no sstables to relevel have been found
+        runTool(1, "org.apache.cassandra.tools.SSTableOfflineRelevel", "system_schema", "tables");
+        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaLoaded();
+        assertServerNotLoaded();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tools/SSTableRepairedAtSetterTest.java b/test/unit/org/apache/cassandra/tools/SSTableRepairedAtSetterTest.java
new file mode 100644
index 0000000..9dece5a
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/SSTableRepairedAtSetterTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.tools;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class SSTableRepairedAtSetterTest extends ToolsTester
+{
+    @Test
+    public void testSSTableRepairedAtSetter_NoArgs()
+    {
+        runTool(1, "org.apache.cassandra.tools.SSTableRepairedAtSetter");
+        assertNoUnexpectedThreadsStarted(null, null);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testSSTableRepairedAtSetter_WithArgs() throws Exception
+    {
+        runTool(0, "org.apache.cassandra.tools.SSTableRepairedAtSetter", "--really-set", "--is-repaired", findOneSSTable("legacy_sstables", "legacy_ma_simple"));
+        assertNoUnexpectedThreadsStarted(null, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tools/StandaloneSSTableUtilTest.java b/test/unit/org/apache/cassandra/tools/StandaloneSSTableUtilTest.java
new file mode 100644
index 0000000..2026a79
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/StandaloneSSTableUtilTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.tools;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class StandaloneSSTableUtilTest extends ToolsTester
+{
+    @Test
+    public void testStandaloneSSTableUtil_NoArgs()
+    {
+        runTool(1, "org.apache.cassandra.tools.StandaloneSSTableUtil");
+        assertNoUnexpectedThreadsStarted(null, null);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testStandaloneSSTableUtil_WithArgs()
+    {
+        runTool(0, "org.apache.cassandra.tools.StandaloneSSTableUtil", "--debug", "-c", "system_schema", "tables");
+        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaLoaded();
+        assertServerNotLoaded();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tools/StandaloneScrubberTest.java b/test/unit/org/apache/cassandra/tools/StandaloneScrubberTest.java
new file mode 100644
index 0000000..6aef5eb
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/StandaloneScrubberTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.tools;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class StandaloneScrubberTest extends ToolsTester
+{
+    @Test
+    public void testStandaloneScrubber_NoArgs()
+    {
+        runTool(1, "org.apache.cassandra.tools.StandaloneScrubber");
+        assertNoUnexpectedThreadsStarted(null, null);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testStandaloneScrubber_WithArgs()
+    {
+        runTool(0, "org.apache.cassandra.tools.StandaloneScrubber", "--debug", "system_schema", "tables");
+        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaLoaded();
+        assertServerNotLoaded();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java b/test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java
new file mode 100644
index 0000000..c0f8593
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.tools;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class StandaloneSplitterTest extends ToolsTester
+{
+    @BeforeClass
+    public static void before()
+    {
+        // the legacy tables use a different partitioner :(
+        // (Don't use ByteOrderedPartitioner.class.getName() as that would initialize the class and work
+        // against the goal of this test to check classes and threads initialized by the tool.)
+        System.setProperty("cassandra.partitioner", "org.apache.cassandra.dht.ByteOrderedPartitioner");
+    }
+
+    @Test
+    public void testStandaloneSplitter_NoArgs()
+    {
+        runTool(1, "org.apache.cassandra.tools.StandaloneSplitter");
+        assertNoUnexpectedThreadsStarted(null, null);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    // Note: StandaloneSplitter modifies sstables
+}
diff --git a/test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java b/test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java
new file mode 100644
index 0000000..f703a49
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.tools;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class StandaloneUpgraderTest extends ToolsTester
+{
+    @Test
+    public void testStandaloneUpgrader_NoArgs()
+    {
+        runTool(1, "org.apache.cassandra.tools.StandaloneUpgrader");
+        assertNoUnexpectedThreadsStarted(null, null);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testStandaloneUpgrader_WithArgs()
+    {
+        runTool(0, "org.apache.cassandra.tools.StandaloneUpgrader", "--debug", "system_schema", "tables");
+        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaLoaded();
+        assertServerNotLoaded();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java b/test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
new file mode 100644
index 0000000..38418f3
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.tools;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class StandaloneVerifierTest extends ToolsTester
+{
+    @Test
+    public void testStandaloneVerifier_NoArgs()
+    {
+        runTool(1, "org.apache.cassandra.tools.StandaloneVerifier");
+        assertNoUnexpectedThreadsStarted(null, null);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testStandaloneVerifier_WithArgs()
+    {
+        runTool(0, "org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables");
+        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaLoaded();
+        assertServerNotLoaded();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tools/ToolsTester.java b/test/unit/org/apache/cassandra/tools/ToolsTester.java
new file mode 100644
index 0000000..a690ca7
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/ToolsTester.java
@@ -0,0 +1,298 @@
+/*
+ * 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.tools;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.Permission;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.BeforeClass;
+
+import org.slf4j.LoggerFactory;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Base unit test class for standalone tools
+ */
+public abstract class ToolsTester
+{
+    private static List<ThreadInfo> initialThreads;
+
+    static final String[] EXPECTED_THREADS_WITH_SCHEMA = {
+    "PerDiskMemtableFlushWriter_0:[1-9]",
+    "MemtablePostFlush:[1-9]",
+    "MemtableFlushWriter:[1-9]",
+    "MemtableReclaimMemory:[1-9]",
+    };
+    static final String[] OPTIONAL_THREADS_WITH_SCHEMA = {
+    "ScheduledTasks:[1-9]",
+    "OptionalTasks:[1-9]",
+    "Reference-Reaper:[1-9]",
+    "LocalPool-Cleaner:[1-9]",
+    "CacheCleanupExecutor:[1-9]",
+    "CompactionExecutor:[1-9]",
+    "ValidationExecutor:[1-9]",
+    "NonPeriodicTasks:[1-9]",
+    "Sampler:[1-9]",
+    "SecondaryIndexManagement:[1-9]",
+    "Strong-Reference-Leak-Detector:[1-9]",
+    "Background_Reporter:[1-9]",
+    "EXPIRING-MAP-REAPER:[1-9]",
+    };
+
+    public void assertNoUnexpectedThreadsStarted(String[] expectedThreadNames, String[] optionalThreadNames)
+    {
+        ThreadMXBean threads = ManagementFactory.getThreadMXBean();
+
+        Set<String> initial = initialThreads
+                              .stream()
+                              .map(ThreadInfo::getThreadName)
+                              .collect(Collectors.toSet());
+
+        Set<String> current = Arrays.stream(threads.getThreadInfo(threads.getAllThreadIds()))
+                                    .map(ThreadInfo::getThreadName)
+                                    .collect(Collectors.toSet());
+
+        List<Pattern> expected = expectedThreadNames != null
+                                 ? Arrays.stream(expectedThreadNames).map(Pattern::compile).collect(Collectors.toList())
+                                 : Collections.emptyList();
+
+        List<Pattern> optional = optionalThreadNames != null
+                                 ? Arrays.stream(optionalThreadNames).map(Pattern::compile).collect(Collectors.toList())
+                                 : Collections.emptyList();
+
+        current.removeAll(initial);
+
+        List<Pattern> notPresent = expected.stream()
+                                           .filter(threadNamePattern -> !current.stream().anyMatch(threadName -> threadNamePattern.matcher(threadName).matches()))
+                                           .collect(Collectors.toList());
+
+        Set<String> remain = current.stream()
+                                    .filter(threadName -> expected.stream().anyMatch(pattern -> pattern.matcher(threadName).matches()))
+                                    .filter(threadName -> optional.stream().anyMatch(pattern -> pattern.matcher(threadName).matches()))
+                                    .collect(Collectors.toSet());
+
+        if (!current.isEmpty())
+            System.err.println("Unexpected thread names: " + remain);
+        if (!notPresent.isEmpty())
+            System.err.println("Mandatory thread missing: " + notPresent);
+
+        assertTrue("Wrong thread status", remain.isEmpty() && notPresent.isEmpty());
+    }
+
+    public void assertSchemaNotLoaded()
+    {
+        assertClassNotLoaded("org.apache.cassandra.config.Schema");
+    }
+
+    public void assertSchemaLoaded()
+    {
+        assertClassLoaded("org.apache.cassandra.config.Schema");
+    }
+
+    public void assertKeyspaceNotLoaded()
+    {
+        assertClassNotLoaded("org.apache.cassandra.db.Keyspace");
+    }
+
+    public void assertKeyspaceLoaded()
+    {
+        assertClassLoaded("org.apache.cassandra.db.Keyspace");
+    }
+
+    public void assertServerNotLoaded()
+    {
+        assertClassNotLoaded("org.apache.cassandra.transport.Server");
+    }
+
+    public void assertSystemKSNotLoaded()
+    {
+        assertClassNotLoaded("org.apache.cassandra.db.SystemKeyspace");
+    }
+
+    public void assertCLSMNotLoaded()
+    {
+        assertClassNotLoaded("org.apache.cassandra.db.commitlog.CommitLogSegmentManager");
+    }
+
+    public void assertClassLoaded(String clazz)
+    {
+        assertClassLoadedStatus(clazz, true);
+    }
+
+    public void assertClassNotLoaded(String clazz)
+    {
+        assertClassLoadedStatus(clazz, false);
+    }
+
+    private void assertClassLoadedStatus(String clazz, boolean expected)
+    {
+        for (ClassLoader cl = Thread.currentThread().getContextClassLoader(); cl != null; cl = cl.getParent())
+        {
+            try
+            {
+                Method mFindLoadedClass = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class);
+                mFindLoadedClass.setAccessible(true);
+                boolean loaded = mFindLoadedClass.invoke(cl, clazz) != null;
+
+                if (expected)
+                {
+                    if (loaded)
+                        return;
+                }
+                else
+                    assertFalse(clazz + " has been loaded", loaded);
+            }
+            catch (Exception e)
+            {
+                throw new RuntimeException(e);
+            }
+        }
+
+        if (expected)
+            fail(clazz + " has not been loaded");
+    }
+
+    public void runTool(int expectedExitCode, String clazz, String... args)
+    {
+        try
+        {
+            // install security manager to get informed about the exit-code
+            System.setSecurityManager(new SecurityManager()
+            {
+                public void checkExit(int status)
+                {
+                    throw new SystemExitException(status);
+                }
+
+                public void checkPermission(Permission perm)
+                {
+                }
+
+                public void checkPermission(Permission perm, Object context)
+                {
+                }
+            });
+
+            try
+            {
+                Class.forName(clazz).getDeclaredMethod("main", String[].class).invoke(null, (Object) args);
+            }
+            catch (InvocationTargetException e)
+            {
+                Throwable cause = e.getCause();
+                if (cause instanceof Error)
+                    throw (Error) cause;
+                if (cause instanceof RuntimeException)
+                    throw (RuntimeException) cause;
+                throw e;
+            }
+
+            assertEquals("Unexpected exit code", expectedExitCode, 0);
+        }
+        catch (SystemExitException e)
+        {
+            assertEquals("Unexpected exit code", expectedExitCode, e.status);
+        }
+        catch (InvocationTargetException e)
+        {
+            throw new RuntimeException(e.getTargetException());
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException(e);
+        }
+        finally
+        {
+            // uninstall security manager
+            System.setSecurityManager(null);
+        }
+    }
+
+    @BeforeClass
+    public static void setupTester()
+    {
+        System.setProperty("cassandra.partitioner", "org.apache.cassandra.dht.Murmur3Partitioner");
+
+        // may start an async appender
+        LoggerFactory.getLogger(ToolsTester.class);
+
+        ThreadMXBean threads = ManagementFactory.getThreadMXBean();
+        initialThreads = Arrays.asList(threads.getThreadInfo(threads.getAllThreadIds()));
+
+        DatabaseDescriptor.toolInitialization();
+        DatabaseDescriptor.applyAddressConfig();
+    }
+
+    public static class SystemExitException extends Error
+    {
+        public final int status;
+
+        public SystemExitException(int status)
+        {
+            this.status = status;
+        }
+    }
+
+    public static String findOneSSTable(String ks, String cf) throws IOException
+    {
+        File cfDir = sstableDir(ks, cf);
+        File[] sstableFiles = cfDir.listFiles((file) -> file.isFile() && file.getName().endsWith("-Data.db"));
+        return sstableFiles[0].getAbsolutePath();
+    }
+
+    public static String sstableDirName(String ks, String cf) throws IOException
+    {
+        return sstableDir(ks, cf).getAbsolutePath();
+    }
+
+    public static File sstableDir(String ks, String cf) throws IOException
+    {
+        File dataDir = copySSTables();
+        File ksDir = new File(dataDir, ks);
+        File[] cfDirs = ksDir.listFiles((dir, name) -> cf.equals(name) || name.startsWith(cf + '-'));
+        return cfDirs[0];
+    }
+
+    public static File copySSTables() throws IOException
+    {
+        File dataDir = new File("build/test/cassandra/data");
+        File srcDir = new File("test/data/legacy-sstables/ma");
+        FileUtils.copyDirectory(new File(srcDir, "legacy_tables"), new File(dataDir, "legacy_sstables"));
+        return dataDir;
+    }
+}
diff --git a/test/unit/org/apache/cassandra/tracing/TracingTest.java b/test/unit/org/apache/cassandra/tracing/TracingTest.java
new file mode 100644
index 0000000..f546496
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tracing/TracingTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.tracing;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.utils.progress.ProgressEvent;
+import org.apache.commons.lang3.StringUtils;
+
+public final class TracingTest
+{
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
+    @Test
+    public void test()
+    {
+        List<String> traces = new ArrayList<>();
+        Tracing tracing = new TracingImpl(traces);
+        tracing.newSession(Tracing.TraceType.NONE);
+        TraceState state = tracing.begin("test-request", Collections.<String,String>emptyMap());
+        state.trace("test-1");
+        state.trace("test-2");
+        state.trace("test-3");
+        tracing.stopSession();
+
+        assert null == tracing.get();
+        assert 4 == traces.size();
+        assert "test-request".equals(traces.get(0));
+        assert "test-1".equals(traces.get(1));
+        assert "test-2".equals(traces.get(2));
+        assert "test-3".equals(traces.get(3));
+    }
+
+    @Test
+    public void test_get()
+    {
+        List<String> traces = new ArrayList<>();
+        Tracing tracing = new TracingImpl(traces);
+        tracing.newSession(Tracing.TraceType.NONE);
+        tracing.begin("test-request", Collections.<String,String>emptyMap());
+        tracing.get().trace("test-1");
+        tracing.get().trace("test-2");
+        tracing.get().trace("test-3");
+        tracing.stopSession();
+
+        assert null == tracing.get();
+        assert 4 == traces.size();
+        assert "test-request".equals(traces.get(0));
+        assert "test-1".equals(traces.get(1));
+        assert "test-2".equals(traces.get(2));
+        assert "test-3".equals(traces.get(3));
+    }
+
+    @Test
+    public void test_get_uuid()
+    {
+        List<String> traces = new ArrayList<>();
+        Tracing tracing = new TracingImpl(traces);
+        UUID uuid = tracing.newSession(Tracing.TraceType.NONE);
+        tracing.begin("test-request", Collections.<String,String>emptyMap());
+        tracing.get(uuid).trace("test-1");
+        tracing.get(uuid).trace("test-2");
+        tracing.get(uuid).trace("test-3");
+        tracing.stopSession();
+
+        assert null == tracing.get();
+        assert 4 == traces.size();
+        assert "test-request".equals(traces.get(0));
+        assert "test-1".equals(traces.get(1));
+        assert "test-2".equals(traces.get(2));
+        assert "test-3".equals(traces.get(3));
+    }
+
+    @Test
+    public void test_customPayload()
+    {
+        List<String> traces = new ArrayList<>();
+        ByteBuffer customPayloadValue = ByteBuffer.wrap("test-value".getBytes());
+
+        Map<String,ByteBuffer> customPayload = Collections.singletonMap("test-key", customPayloadValue);
+
+        TracingImpl tracing = new TracingImpl(traces);
+        tracing.newSession(customPayload);
+        TraceState state = tracing.begin("test-custom_payload", Collections.<String,String>emptyMap());
+        state.trace("test-1");
+        state.trace("test-2");
+        state.trace("test-3");
+        tracing.stopSession();
+
+        assert null == tracing.get();
+        assert 4 == traces.size();
+        assert "test-custom_payload".equals(traces.get(0));
+        assert "test-1".equals(traces.get(1));
+        assert "test-2".equals(traces.get(2));
+        assert "test-3".equals(traces.get(3));
+        assert tracing.payloads.containsKey("test-key");
+        assert customPayloadValue.equals(tracing.payloads.get("test-key"));
+    }
+
+    @Test
+    public void test_states()
+    {
+        List<String> traces = new ArrayList<>();
+        Tracing tracing = new TracingImpl(traces);
+        tracing.newSession(Tracing.TraceType.REPAIR);
+        tracing.begin("test-request", Collections.<String,String>emptyMap());
+        tracing.get().enableActivityNotification("test-tag");
+        assert TraceState.Status.IDLE == tracing.get().waitActivity(1);
+        tracing.get().trace("test-1");
+        assert TraceState.Status.ACTIVE == tracing.get().waitActivity(1);
+        tracing.get().stop();
+        assert TraceState.Status.STOPPED == tracing.get().waitActivity(1);
+        tracing.stopSession();
+        assert null == tracing.get();
+    }
+
+    @Test
+    public void test_progress_listener()
+    {
+        List<String> traces = new ArrayList<>();
+        Tracing tracing = new TracingImpl(traces);
+        tracing.newSession(Tracing.TraceType.REPAIR);
+        tracing.begin("test-request", Collections.<String,String>emptyMap());
+        tracing.get().enableActivityNotification("test-tag");
+
+        tracing.get().addProgressListener((String tag, ProgressEvent pe) -> {
+            assert "test-tag".equals(tag);
+            assert "test-trace".equals(pe.getMessage());
+        });
+
+        tracing.get().trace("test-trace");
+        tracing.stopSession();
+        assert null == tracing.get();
+    }
+
+    private static final class TracingImpl extends Tracing
+    {
+        private final List<String> traces;
+        private final Map<String,ByteBuffer> payloads = new HashMap<>();
+
+        public TracingImpl()
+        {
+            this(new ArrayList<>());
+        }
+
+        public TracingImpl(List<String> traces)
+        {
+            this.traces = traces;
+        }
+
+        public void stopSessionImpl()
+        {}
+
+        public TraceState begin(String request, InetAddress ia, Map<String, String> map)
+        {
+            traces.add(request);
+            return get();
+        }
+
+        protected UUID newSession(UUID sessionId, TraceType traceType, Map<String,ByteBuffer> customPayload)
+        {
+            if (!customPayload.isEmpty())
+                logger.info("adding custom payload items {}", StringUtils.join(customPayload.keySet(), ','));
+
+            payloads.putAll(customPayload);
+            return super.newSession(sessionId, traceType, customPayload);
+        }
+
+        protected TraceState newTraceState(InetAddress ia, UUID uuid, Tracing.TraceType tt)
+        {
+            return new TraceState(ia, uuid, tt)
+            {
+                protected void traceImpl(String string)
+                {
+                    traces.add(string);
+                }
+
+                protected void waitForPendingEvents()
+                {
+                }
+            };
+        }
+
+        public void trace(ByteBuffer bb, String string, int i)
+        {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/transport/DataTypeTest.java b/test/unit/org/apache/cassandra/transport/DataTypeTest.java
index dc2c4e2..6f086c9 100644
--- a/test/unit/org/apache/cassandra/transport/DataTypeTest.java
+++ b/test/unit/org/apache/cassandra/transport/DataTypeTest.java
@@ -26,10 +26,13 @@
 import org.junit.Test;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
 import org.apache.cassandra.db.TypeSizes;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.AsciiType;
 import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.utils.Pair;
 
 import static org.junit.Assert.assertEquals;
 
@@ -43,8 +46,8 @@
             if (isComplexType(type))
                 continue;
 
-            Map<DataType, Object> options = Collections.singletonMap(type, (Object)type.toString());
-            for (int version = 1; version < 5; version++)
+            Pair<DataType, Object> options = Pair.create(type, (Object)type.toString());
+            for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
                 testEncodeDecode(type, options, version);
         }
     }
@@ -53,8 +56,8 @@
     public void TestListDataTypeSerialization()
     {
         DataType type = DataType.LIST;
-        Map<DataType, Object> options =  Collections.singletonMap(type, (Object)LongType.instance);
-        for (int version = 1; version < 5; version++)
+        Pair<DataType, Object> options = Pair.create(type, (Object)LongType.instance);
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
             testEncodeDecode(type, options, version);
     }
 
@@ -65,44 +68,44 @@
         List<AbstractType> value = new ArrayList<>();
         value.add(LongType.instance);
         value.add(AsciiType.instance);
-        Map<DataType, Object> options = Collections.singletonMap(type, (Object)value);
-        for (int version = 1; version < 5; version++)
+        Pair<DataType, Object> options = Pair.create(type, (Object)value);
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
             testEncodeDecode(type, options, version);
     }
 
-    private void testEncodeDecode(DataType type, Map<DataType, Object> options, int version)
+    private void testEncodeDecode(DataType type, Pair<DataType, Object> options, ProtocolVersion version)
     {
-        ByteBuf dest = type.codec.encode(options, version);
-        Map<DataType, Object> results = type.codec.decode(dest, version);
+        int optLength = DataType.codec.oneSerializedSize(options, version);
+        ByteBuf dest = Unpooled.buffer(optLength);
+        DataType.codec.writeOne(options, dest, version);
+        Pair<DataType, Object> result = DataType.codec.decodeOne(dest, version);
 
-        for (DataType key : results.keySet())
+        System.out.println(result + "version " + version);
+        int ssize = type.serializedValueSize(result.right, version);
+        int esize = version.isSmallerThan(type.getProtocolVersion()) ? 2 + TypeSizes.encodedUTF8Length(result.right.toString()) : 0;
+        switch (type)
         {
-            int ssize = type.serializedValueSize(results.get(key), version);
-            int esize = version < type.getProtocolVersion() ? 2 + TypeSizes.encodedUTF8Length(results.get(key).toString()) : 0;
-            switch (type)
-            {
-                case LIST:
-                case SET:
-                    esize += 2;
-                    break;
-                case MAP:
-                    esize += 4;
-                    break;
-                case CUSTOM:
-                    esize = 8;
-                    break;
-            }
-            assertEquals(esize, ssize);
-
-            DataType expected = version < type.getProtocolVersion()
-                ? DataType.CUSTOM
-                : type;
-            assertEquals(expected, key);
+            case LIST:
+            case SET:
+                esize += 2;
+                break;
+            case MAP:
+                esize += 4;
+                break;
+            case CUSTOM:
+                esize = 8;
+                break;
         }
-    }
+        assertEquals(esize, ssize);
+
+        DataType expected = version.isSmallerThan(type.getProtocolVersion())
+            ? DataType.CUSTOM
+            : type;
+        assertEquals(expected, result.left);
+   }
 
     private boolean isComplexType(DataType type)
     {
-        return type.getId(Server.CURRENT_VERSION) >= 32;
+        return type.getId(ProtocolVersion.CURRENT) >= 32;
     }
 }
diff --git a/test/unit/org/apache/cassandra/transport/DynamicLimitTest.java b/test/unit/org/apache/cassandra/transport/DynamicLimitTest.java
index 83a0dd9..df06fba 100644
--- a/test/unit/org/apache/cassandra/transport/DynamicLimitTest.java
+++ b/test/unit/org/apache/cassandra/transport/DynamicLimitTest.java
@@ -56,16 +56,16 @@
         {
             peer = setupPeer("127.1.0.1", "2.2.0");
             ConfiguredLimit limit = ConfiguredLimit.newLimit();
-            assertEquals(Server.CURRENT_VERSION, limit.getMaxVersion());
+            assertEquals(ProtocolVersion.MAX_SUPPORTED_VERSION, limit.getMaxVersion());
 
             // clearing the property after the limit has been returned has no effect
             System.clearProperty(ConfiguredLimit.DISABLE_MAX_PROTOCOL_AUTO_OVERRIDE);
             limit.updateMaxSupportedVersion();
-            assertEquals(Server.CURRENT_VERSION, limit.getMaxVersion());
+            assertEquals(ProtocolVersion.MAX_SUPPORTED_VERSION, limit.getMaxVersion());
 
             // a new limit should now be dynamic
             limit = ConfiguredLimit.newLimit();
-            assertEquals(Server.VERSION_3, limit.getMaxVersion());
+            assertEquals(ProtocolVersion.V3, limit.getMaxVersion());
         }
         finally
         {
@@ -88,21 +88,21 @@
             // ensure that no static limit is configured
             setStaticLimitInConfig(null);
             ConfiguredLimit limit = ConfiguredLimit.newLimit();
-            assertEquals(Server.CURRENT_VERSION, limit.getMaxVersion());
+            assertEquals(ProtocolVersion.MAX_SUPPORTED_VERSION, limit.getMaxVersion());
 
             peer = setupPeer("127.1.0.1", "3.0.0");
             limit.updateMaxSupportedVersion();
-            assertEquals(Server.CURRENT_VERSION, limit.getMaxVersion());
+            assertEquals(ProtocolVersion.MAX_SUPPORTED_VERSION, limit.getMaxVersion());
 
             // learn that peer doesn't actually fully support V4, behaviour should remain the same
             updatePeerInfo(peer, "2.2.0");
             limit.updateMaxSupportedVersion();
-            assertEquals(Server.CURRENT_VERSION, limit.getMaxVersion());
+            assertEquals(ProtocolVersion.MAX_SUPPORTED_VERSION, limit.getMaxVersion());
 
             // finally learn that peer2 has been upgraded, just for completeness
             updatePeerInfo(peer, "3.3.0");
             limit.updateMaxSupportedVersion();
-            assertEquals(Server.CURRENT_VERSION, limit.getMaxVersion());
+            assertEquals(ProtocolVersion.MAX_SUPPORTED_VERSION, limit.getMaxVersion());
 
         } finally {
             cleanupPeers(peer);
diff --git a/test/unit/org/apache/cassandra/transport/ErrorMessageTest.java b/test/unit/org/apache/cassandra/transport/ErrorMessageTest.java
new file mode 100644
index 0000000..2dcd2ac
--- /dev/null
+++ b/test/unit/org/apache/cassandra/transport/ErrorMessageTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.transport;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.WriteType;
+import org.apache.cassandra.exceptions.ReadFailureException;
+import org.apache.cassandra.exceptions.RequestFailureReason;
+import org.apache.cassandra.exceptions.WriteFailureException;
+import org.apache.cassandra.transport.messages.ErrorMessage;
+
+import static org.junit.Assert.assertEquals;
+
+public class ErrorMessageTest
+{
+    private static Map<InetAddress, RequestFailureReason> failureReasonMap1;
+    private static Map<InetAddress, RequestFailureReason> failureReasonMap2;
+
+    @BeforeClass
+    public static void setUpFixtures() throws UnknownHostException
+    {
+        failureReasonMap1 = new HashMap<>();
+        failureReasonMap1.put(InetAddress.getByName("127.0.0.1"), RequestFailureReason.READ_TOO_MANY_TOMBSTONES);
+        failureReasonMap1.put(InetAddress.getByName("127.0.0.2"), RequestFailureReason.READ_TOO_MANY_TOMBSTONES);
+        failureReasonMap1.put(InetAddress.getByName("127.0.0.3"), RequestFailureReason.UNKNOWN);
+
+        failureReasonMap2 = new HashMap<>();
+        failureReasonMap2.put(InetAddress.getByName("127.0.0.1"), RequestFailureReason.UNKNOWN);
+        failureReasonMap2.put(InetAddress.getByName("127.0.0.2"), RequestFailureReason.UNKNOWN);
+    }
+
+    @Test
+    public void testV5ReadFailureSerDeser()
+    {
+        int receivedBlockFor = 3;
+        ConsistencyLevel consistencyLevel = ConsistencyLevel.ALL;
+        boolean dataPresent = false;
+        ReadFailureException rfe = new ReadFailureException(consistencyLevel, receivedBlockFor, receivedBlockFor, dataPresent, failureReasonMap1);
+
+        ErrorMessage deserialized = serializeAndGetDeserializedErrorMessage(ErrorMessage.fromException(rfe), ProtocolVersion.V5);
+        ReadFailureException deserializedRfe = (ReadFailureException) deserialized.error;
+
+        assertEquals(failureReasonMap1, deserializedRfe.failureReasonByEndpoint);
+        assertEquals(receivedBlockFor, deserializedRfe.received);
+        assertEquals(receivedBlockFor, deserializedRfe.blockFor);
+        assertEquals(consistencyLevel, deserializedRfe.consistency);
+        assertEquals(dataPresent, deserializedRfe.dataPresent);
+    }
+
+    @Test
+    public void testV5WriteFailureSerDeser()
+    {
+        int receivedBlockFor = 3;
+        ConsistencyLevel consistencyLevel = ConsistencyLevel.ALL;
+        WriteType writeType = WriteType.SIMPLE;
+        WriteFailureException wfe = new WriteFailureException(consistencyLevel, receivedBlockFor, receivedBlockFor, writeType, failureReasonMap2);
+
+        ErrorMessage deserialized = serializeAndGetDeserializedErrorMessage(ErrorMessage.fromException(wfe), ProtocolVersion.V5);
+        WriteFailureException deserializedWfe = (WriteFailureException) deserialized.error;
+
+        assertEquals(failureReasonMap2, deserializedWfe.failureReasonByEndpoint);
+        assertEquals(receivedBlockFor, deserializedWfe.received);
+        assertEquals(receivedBlockFor, deserializedWfe.blockFor);
+        assertEquals(consistencyLevel, deserializedWfe.consistency);
+        assertEquals(writeType, deserializedWfe.writeType);
+    }
+
+    /**
+     * Make sure that the map passed in to create a Read/WriteFailureException is copied
+     * so later modifications to the map passed in don't affect the map in the exception.
+     *
+     * This is to prevent potential issues in serialization if the map created in
+     * ReadCallback/AbstractWriteResponseHandler is modified due to a delayed failure
+     * response after the exception is created.
+     */
+    @Test
+    public void testRequestFailureExceptionMakesCopy() throws UnknownHostException
+    {
+        Map<InetAddress, RequestFailureReason> modifiableFailureReasons = new HashMap<>(failureReasonMap1);
+        ReadFailureException rfe = new ReadFailureException(ConsistencyLevel.ALL, 3, 3, false, modifiableFailureReasons);
+        WriteFailureException wfe = new WriteFailureException(ConsistencyLevel.ALL, 3, 3, WriteType.SIMPLE, modifiableFailureReasons);
+
+        modifiableFailureReasons.put(InetAddress.getByName("127.0.0.4"), RequestFailureReason.UNKNOWN);
+
+        assertEquals(failureReasonMap1, rfe.failureReasonByEndpoint);
+        assertEquals(failureReasonMap1, wfe.failureReasonByEndpoint);
+    }
+
+    private ErrorMessage serializeAndGetDeserializedErrorMessage(ErrorMessage message, ProtocolVersion version)
+    {
+        ByteBuf buffer = Unpooled.buffer(ErrorMessage.codec.encodedSize(message, version));
+        ErrorMessage.codec.encode(message, buffer, version);
+        return ErrorMessage.codec.decode(buffer, version);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/transport/InflightRequestPayloadTrackerTest.java b/test/unit/org/apache/cassandra/transport/InflightRequestPayloadTrackerTest.java
index e4d335b..21dfed8 100644
--- a/test/unit/org/apache/cassandra/transport/InflightRequestPayloadTrackerTest.java
+++ b/test/unit/org/apache/cassandra/transport/InflightRequestPayloadTrackerTest.java
@@ -70,7 +70,7 @@
     {
         SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(),
                                                nativePort,
-                                               Server.CURRENT_VERSION,
+                                               ProtocolVersion.V4,
                                                new EncryptionOptions.ClientEncryptionOptions());
 
         try
@@ -83,7 +83,7 @@
             QueryOptions.DEFAULT.getPageSize(),
             QueryOptions.DEFAULT.getPagingState(),
             QueryOptions.DEFAULT.getSerialConsistency(),
-            Server.CURRENT_VERSION);
+            ProtocolVersion.V4);
 
             QueryMessage queryMessage = new QueryMessage(String.format("CREATE TABLE %s.atable (pk1 int PRIMARY KEY, v text)", KEYSPACE),
                                                          queryOptions);
@@ -100,7 +100,7 @@
     {
         SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(),
                                                nativePort,
-                                               Server.CURRENT_VERSION,
+                                               ProtocolVersion.V4,
                                                new EncryptionOptions.ClientEncryptionOptions());
 
         try
@@ -113,7 +113,7 @@
             QueryOptions.DEFAULT.getPageSize(),
             QueryOptions.DEFAULT.getPagingState(),
             QueryOptions.DEFAULT.getSerialConsistency(),
-            Server.CURRENT_VERSION);
+            ProtocolVersion.V4);
 
             QueryMessage queryMessage = new QueryMessage(String.format("CREATE TABLE %s.atable (pk int PRIMARY KEY, v text)", KEYSPACE),
                                                          queryOptions);
@@ -133,7 +133,7 @@
     {
         SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(),
                                                nativePort,
-                                               Server.CURRENT_VERSION,
+                                               ProtocolVersion.V4,
                                                new EncryptionOptions.ClientEncryptionOptions());
 
         try
@@ -146,7 +146,7 @@
             QueryOptions.DEFAULT.getPageSize(),
             QueryOptions.DEFAULT.getPagingState(),
             QueryOptions.DEFAULT.getSerialConsistency(),
-            Server.CURRENT_VERSION);
+            ProtocolVersion.V4);
 
             QueryMessage queryMessage = new QueryMessage(String.format("CREATE TABLE %s.atable (pk int PRIMARY KEY, v text)", KEYSPACE),
                                                          queryOptions);
@@ -167,7 +167,7 @@
     {
         SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(),
                                                nativePort,
-                                               Server.CURRENT_VERSION,
+                                               ProtocolVersion.V4,
                                                new EncryptionOptions.ClientEncryptionOptions());
 
         try
@@ -180,7 +180,7 @@
             QueryOptions.DEFAULT.getPageSize(),
             QueryOptions.DEFAULT.getPagingState(),
             QueryOptions.DEFAULT.getSerialConsistency(),
-            Server.CURRENT_VERSION);
+            ProtocolVersion.V4);
 
             QueryMessage queryMessage = new QueryMessage(String.format("CREATE TABLE %s.atable (pk int PRIMARY KEY, v text)", KEYSPACE),
                                                          queryOptions);
@@ -209,7 +209,7 @@
     {
         SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(),
                                                nativePort,
-                                               Server.CURRENT_VERSION,
+                                               ProtocolVersion.V4,
                                                new EncryptionOptions.ClientEncryptionOptions());
 
         try
@@ -222,7 +222,7 @@
             QueryOptions.DEFAULT.getPageSize(),
             QueryOptions.DEFAULT.getPagingState(),
             QueryOptions.DEFAULT.getSerialConsistency(),
-            Server.CURRENT_VERSION);
+            ProtocolVersion.V4);
 
             QueryMessage queryMessage = new QueryMessage(String.format("CREATE TABLE %s.atable (pk int PRIMARY KEY, v text)", KEYSPACE),
                                                          queryOptions);
diff --git a/test/unit/org/apache/cassandra/transport/MessagePayloadTest.java b/test/unit/org/apache/cassandra/transport/MessagePayloadTest.java
index e88126f..711c4d7 100644
--- a/test/unit/org/apache/cassandra/transport/MessagePayloadTest.java
+++ b/test/unit/org/apache/cassandra/transport/MessagePayloadTest.java
@@ -29,7 +29,6 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
-import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.BatchQueryOptions;
 import org.apache.cassandra.cql3.CQLStatement;
 import org.apache.cassandra.cql3.QueryHandler;
@@ -38,7 +37,6 @@
 import org.apache.cassandra.cql3.statements.BatchStatement;
 import org.apache.cassandra.cql3.statements.ParsedStatement;
 import org.apache.cassandra.cql3.CQLTester;
-import org.apache.cassandra.dht.ByteOrderedPartitioner;
 import org.apache.cassandra.exceptions.RequestExecutionException;
 import org.apache.cassandra.exceptions.RequestValidationException;
 import org.apache.cassandra.service.ClientState;
@@ -193,7 +191,7 @@
 
             Assert.assertSame(TestQueryHandler.class, ClientState.getCQLQueryHandler().getClass());
 
-            SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, Server.VERSION_3);
+            SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, ProtocolVersion.V3);
             try
             {
                 client.connect(false);
@@ -329,12 +327,13 @@
         public ResultMessage process(String query,
                                      QueryState state,
                                      QueryOptions options,
-                                     Map<String, ByteBuffer> customPayload)
-                                             throws RequestExecutionException, RequestValidationException
+                                     Map<String, ByteBuffer> customPayload,
+                                     long queryStartNanoTime)
+                                            throws RequestExecutionException, RequestValidationException
         {
             if (customPayload != null)
                 requestPayload = customPayload;
-            ResultMessage result = QueryProcessor.instance.process(query, state, options, customPayload);
+            ResultMessage result = QueryProcessor.instance.process(query, state, options, customPayload, queryStartNanoTime);
             if (customPayload != null)
             {
                 result.setCustomPayload(responsePayload);
@@ -346,12 +345,13 @@
         public ResultMessage processBatch(BatchStatement statement,
                                           QueryState state,
                                           BatchQueryOptions options,
-                                          Map<String, ByteBuffer> customPayload)
+                                          Map<String, ByteBuffer> customPayload,
+                                          long queryStartNanoTime)
                                                   throws RequestExecutionException, RequestValidationException
         {
             if (customPayload != null)
                 requestPayload = customPayload;
-            ResultMessage result = QueryProcessor.instance.processBatch(statement, state, options, customPayload);
+            ResultMessage result = QueryProcessor.instance.processBatch(statement, state, options, customPayload, queryStartNanoTime);
             if (customPayload != null)
             {
                 result.setCustomPayload(responsePayload);
@@ -363,12 +363,13 @@
         public ResultMessage processPrepared(CQLStatement statement,
                                              QueryState state,
                                              QueryOptions options,
-                                             Map<String, ByteBuffer> customPayload)
-                                                     throws RequestExecutionException, RequestValidationException
+                                             Map<String, ByteBuffer> customPayload,
+                                             long queryStartNanoTime)
+                                                    throws RequestExecutionException, RequestValidationException
         {
             if (customPayload != null)
                 requestPayload = customPayload;
-            ResultMessage result = QueryProcessor.instance.processPrepared(statement, state, options, customPayload);
+            ResultMessage result = QueryProcessor.instance.processPrepared(statement, state, options, customPayload, queryStartNanoTime);
             if (customPayload != null)
             {
                 result.setCustomPayload(responsePayload);
diff --git a/test/unit/org/apache/cassandra/transport/ProtocolErrorTest.java b/test/unit/org/apache/cassandra/transport/ProtocolErrorTest.java
index e212c4c..5041a94 100644
--- a/test/unit/org/apache/cassandra/transport/ProtocolErrorTest.java
+++ b/test/unit/org/apache/cassandra/transport/ProtocolErrorTest.java
@@ -19,9 +19,11 @@
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.transport.messages.ErrorMessage;
 
 import org.junit.Assert;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -31,13 +33,20 @@
 
 public class ProtocolErrorTest {
 
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testInvalidProtocolVersion() throws Exception
     {
-        // test using a protocol version higher than the current version
-        testInvalidProtocolVersion(Server.CURRENT_VERSION + 1);
-     // test using a protocol version lower than the lowest version
-        testInvalidProtocolVersion(Server.MIN_SUPPORTED_VERSION - 1);
+        // test using a protocol 2 version higher than the current version (1 version higher is current beta)
+        testInvalidProtocolVersion(ProtocolVersion.CURRENT.asInt() + 2); //
+        // test using a protocol version lower than the lowest version
+        for (ProtocolVersion version : ProtocolVersion.UNSUPPORTED)
+            testInvalidProtocolVersion(version.asInt());
 
     }
 
@@ -99,7 +108,7 @@
         // should generate a protocol exception for using a response frame with
         // a prepare op, ensure that it comes back with stream ID 1
         byte[] frame = new byte[] {
-                (byte) RESPONSE.addToVersion(Server.CURRENT_VERSION),  // direction & version
+                (byte) RESPONSE.addToVersion(ProtocolVersion.CURRENT.asInt()),  // direction & version
                 0x00,  // flags
                 0x00, 0x01,  // stream ID
                 0x09,  // opcode
@@ -128,7 +137,7 @@
 
         List<Object> results = new ArrayList<>();
         byte[] frame = new byte[] {
-                (byte) REQUEST.addToVersion(Server.CURRENT_VERSION),  // direction & version
+                (byte) REQUEST.addToVersion(ProtocolVersion.CURRENT.asInt()),  // direction & version
                 0x00,  // flags
                 0x00, 0x01,  // stream ID
                 0x09,  // opcode
@@ -152,9 +161,9 @@
         // test for CASSANDRA-11167
         ErrorMessage msg = ErrorMessage.fromException(new ServerError((String) null));
         assert msg.toString().endsWith("null") : msg.toString();
-        int size = ErrorMessage.codec.encodedSize(msg, Server.CURRENT_VERSION);
+        int size = ErrorMessage.codec.encodedSize(msg, ProtocolVersion.CURRENT);
         ByteBuf buf = Unpooled.buffer(size);
-        ErrorMessage.codec.encode(msg, buf, Server.CURRENT_VERSION);
+        ErrorMessage.codec.encode(msg, buf, ProtocolVersion.CURRENT);
 
         ByteBuf expected = Unpooled.wrappedBuffer(new byte[]{
                 0x00, 0x00, 0x00, 0x00,  // int error code
diff --git a/test/unit/org/apache/cassandra/transport/ProtocolVersionTest.java b/test/unit/org/apache/cassandra/transport/ProtocolVersionTest.java
new file mode 100644
index 0000000..234435e
--- /dev/null
+++ b/test/unit/org/apache/cassandra/transport/ProtocolVersionTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.transport;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ProtocolVersionTest
+{
+    @Test
+    public void testDecode()
+    {
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
+            Assert.assertEquals(version, ProtocolVersion.decode(version.asInt(), ProtocolVersionLimit.SERVER_DEFAULT));
+
+        for (ProtocolVersion version : ProtocolVersion.UNSUPPORTED)
+        { // unsupported old versions
+            try
+            {
+                Assert.assertEquals(version, ProtocolVersion.decode(version.asInt(), ProtocolVersionLimit.SERVER_DEFAULT));
+                Assert.fail("Expected invalid protocol exception");
+            }
+            catch (ProtocolException ex)
+            {
+                Assert.assertNotNull(ex.getForcedProtocolVersion());
+                Assert.assertEquals(version, ex.getForcedProtocolVersion());
+            }
+        }
+
+        try
+        { // unsupported newer version
+            Assert.assertEquals(null, ProtocolVersion.decode(63, ProtocolVersionLimit.SERVER_DEFAULT));
+            Assert.fail("Expected invalid protocol exception");
+        }
+        catch (ProtocolException ex)
+        {
+            Assert.assertNull(ex.getForcedProtocolVersion());
+        }
+    }
+
+    @Test
+    public void testSupportedVersions()
+    {
+        Assert.assertTrue(ProtocolVersion.supportedVersions().size() >= 2); // at least one OS and one DSE
+        Assert.assertNotNull(ProtocolVersion.CURRENT);
+
+        Assert.assertFalse(ProtocolVersion.V4.isBeta());
+        Assert.assertTrue(ProtocolVersion.V5.isBeta());
+    }
+
+    @Test
+    public void testComparisons()
+    {
+        Assert.assertTrue(ProtocolVersion.V1.isSmallerOrEqualTo(ProtocolVersion.V1));
+        Assert.assertTrue(ProtocolVersion.V2.isSmallerOrEqualTo(ProtocolVersion.V2));
+        Assert.assertTrue(ProtocolVersion.V3.isSmallerOrEqualTo(ProtocolVersion.V3));
+        Assert.assertTrue(ProtocolVersion.V4.isSmallerOrEqualTo(ProtocolVersion.V4));
+
+        Assert.assertTrue(ProtocolVersion.V1.isGreaterOrEqualTo(ProtocolVersion.V1));
+        Assert.assertTrue(ProtocolVersion.V2.isGreaterOrEqualTo(ProtocolVersion.V2));
+        Assert.assertTrue(ProtocolVersion.V3.isGreaterOrEqualTo(ProtocolVersion.V3));
+        Assert.assertTrue(ProtocolVersion.V4.isGreaterOrEqualTo(ProtocolVersion.V4));
+
+        Assert.assertTrue(ProtocolVersion.V1.isSmallerThan(ProtocolVersion.V2));
+        Assert.assertTrue(ProtocolVersion.V2.isSmallerThan(ProtocolVersion.V3));
+        Assert.assertTrue(ProtocolVersion.V3.isSmallerThan(ProtocolVersion.V4));
+
+        Assert.assertFalse(ProtocolVersion.V1.isGreaterThan(ProtocolVersion.V2));
+        Assert.assertFalse(ProtocolVersion.V2.isGreaterThan(ProtocolVersion.V3));
+        Assert.assertFalse(ProtocolVersion.V3.isGreaterThan(ProtocolVersion.V4));
+
+        Assert.assertTrue(ProtocolVersion.V4.isGreaterThan(ProtocolVersion.V3));
+        Assert.assertTrue(ProtocolVersion.V3.isGreaterThan(ProtocolVersion.V2));
+        Assert.assertTrue(ProtocolVersion.V2.isGreaterThan(ProtocolVersion.V1));
+
+        Assert.assertFalse(ProtocolVersion.V4.isSmallerThan(ProtocolVersion.V3));
+        Assert.assertFalse(ProtocolVersion.V3.isSmallerThan(ProtocolVersion.V2));
+        Assert.assertFalse(ProtocolVersion.V2.isSmallerThan(ProtocolVersion.V1));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/transport/SerDeserTest.java b/test/unit/org/apache/cassandra/transport/SerDeserTest.java
index fdb346e..1eaa5ac 100644
--- a/test/unit/org/apache/cassandra/transport/SerDeserTest.java
+++ b/test/unit/org/apache/cassandra/transport/SerDeserTest.java
@@ -23,10 +23,16 @@
 import io.netty.buffer.Unpooled;
 import io.netty.buffer.ByteBuf;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
+
+import org.apache.cassandra.Util;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.*;
+import org.apache.cassandra.db.ConsistencyLevel;
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.serializers.CollectionSerializer;
+import org.apache.cassandra.service.pager.PagingState;
 import org.apache.cassandra.transport.Event.TopologyChange;
 import org.apache.cassandra.transport.Event.SchemaChange;
 import org.apache.cassandra.transport.Event.StatusChange;
@@ -35,6 +41,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 
 /**
@@ -42,14 +49,21 @@
  */
 public class SerDeserTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        // required for making the paging state
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void collectionSerDeserTest() throws Exception
     {
-        collectionSerDeserTest(3);
-        collectionSerDeserTest(4);
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
+            collectionSerDeserTest(version);
     }
 
-    public void collectionSerDeserTest(int version) throws Exception
+    public void collectionSerDeserTest(ProtocolVersion version) throws Exception
     {
         // Lists
         ListType<?> lt = ListType.getInstance(Int32Type.instance, true);
@@ -92,11 +106,11 @@
     @Test
     public void eventSerDeserTest() throws Exception
     {
-        eventSerDeserTest(3);
-        eventSerDeserTest(4);
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
+            eventSerDeserTest(version);
     }
 
-    public void eventSerDeserTest(int version) throws Exception
+    public void eventSerDeserTest(ProtocolVersion version) throws Exception
     {
         List<Event> events = new ArrayList<>();
 
@@ -115,14 +129,14 @@
         events.add(new SchemaChange(SchemaChange.Change.UPDATED, SchemaChange.Target.TABLE, "ks", "table"));
         events.add(new SchemaChange(SchemaChange.Change.DROPPED, SchemaChange.Target.TABLE, "ks", "table"));
 
-        if (version >= 3)
+        if (version.isGreaterOrEqualTo(ProtocolVersion.V3))
         {
             events.add(new SchemaChange(SchemaChange.Change.CREATED, SchemaChange.Target.TYPE, "ks", "type"));
             events.add(new SchemaChange(SchemaChange.Change.UPDATED, SchemaChange.Target.TYPE, "ks", "type"));
             events.add(new SchemaChange(SchemaChange.Change.DROPPED, SchemaChange.Target.TYPE, "ks", "type"));
         }
 
-        if (version >= 4)
+        if (version.isGreaterOrEqualTo(ProtocolVersion.V4))
         {
             List<String> moreTypes = Arrays.asList("text", "bigint");
 
@@ -148,6 +162,11 @@
         return UTF8Type.instance.decompose(str);
     }
 
+    private static FieldIdentifier field(String field)
+    {
+        return FieldIdentifier.forQuoted(field);
+    }
+
     private static ColumnIdentifier ci(String name)
     {
         return new ColumnIdentifier(name, false);
@@ -171,11 +190,12 @@
     @Test
     public void udtSerDeserTest() throws Exception
     {
-        udtSerDeserTest(3);
-        udtSerDeserTest(4);
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
+            udtSerDeserTest(version);
     }
 
-    public void udtSerDeserTest(int version) throws Exception
+
+    public void udtSerDeserTest(ProtocolVersion version) throws Exception
     {
         ListType<?> lt = ListType.getInstance(Int32Type.instance, true);
         SetType<?> st = SetType.getInstance(UTF8Type.instance, true);
@@ -183,14 +203,15 @@
 
         UserType udt = new UserType("ks",
                                     bb("myType"),
-                                    Arrays.asList(bb("f1"), bb("f2"), bb("f3"), bb("f4")),
-                                    Arrays.asList(LongType.instance, lt, st, mt));
+                                    Arrays.asList(field("f1"), field("f2"), field("f3"), field("f4")),
+                                    Arrays.asList(LongType.instance, lt, st, mt),
+                                    true);
 
-        Map<ColumnIdentifier, Term.Raw> value = new HashMap<>();
-        value.put(ci("f1"), lit(42));
-        value.put(ci("f2"), new Lists.Literal(Arrays.<Term.Raw>asList(lit(3), lit(1))));
-        value.put(ci("f3"), new Sets.Literal(Arrays.<Term.Raw>asList(lit("foo"), lit("bar"))));
-        value.put(ci("f4"), new Maps.Literal(Arrays.<Pair<Term.Raw, Term.Raw>>asList(
+        Map<FieldIdentifier, Term.Raw> value = new HashMap<>();
+        value.put(field("f1"), lit(42));
+        value.put(field("f2"), new Lists.Literal(Arrays.<Term.Raw>asList(lit(3), lit(1))));
+        value.put(field("f3"), new Sets.Literal(Arrays.<Term.Raw>asList(lit("foo"), lit("bar"))));
+        value.put(field("f4"), new Maps.Literal(Arrays.<Pair<Term.Raw, Term.Raw>>asList(
                                    Pair.<Term.Raw, Term.Raw>create(lit("foo"), lit(24)),
                                    Pair.<Term.Raw, Term.Raw>create(lit("bar"), lit(12)))));
 
@@ -211,45 +232,109 @@
         // a UDT should alway be serialized with version 3 of the protocol. Which is why we don't use 'version'
         // on purpose below.
 
-        assertEquals(Arrays.asList(3, 1), lt.getSerializer().deserializeForNativeProtocol(fields[1], 3));
+        assertEquals(Arrays.asList(3, 1), lt.getSerializer().deserializeForNativeProtocol(fields[1], ProtocolVersion.V3));
 
         LinkedHashSet<String> s = new LinkedHashSet<>();
         s.addAll(Arrays.asList("bar", "foo"));
-        assertEquals(s, st.getSerializer().deserializeForNativeProtocol(fields[2], 3));
+        assertEquals(s, st.getSerializer().deserializeForNativeProtocol(fields[2], ProtocolVersion.V3));
 
         LinkedHashMap<String, Long> m = new LinkedHashMap<>();
         m.put("bar", 12L);
         m.put("foo", 24L);
-        assertEquals(m, mt.getSerializer().deserializeForNativeProtocol(fields[3], 3));
+        assertEquals(m, mt.getSerializer().deserializeForNativeProtocol(fields[3], ProtocolVersion.V3));
     }
 
     @Test
     public void preparedMetadataSerializationTest()
     {
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
+            preparedMetadataSerializationTest(version);
+    }
+
+    private void preparedMetadataSerializationTest(ProtocolVersion version)
+    {
         List<ColumnSpecification> columnNames = new ArrayList<>();
         for (int i = 0; i < 3; i++)
             columnNames.add(new ColumnSpecification("ks", "cf", new ColumnIdentifier("col" + i, false), Int32Type.instance));
 
-        ResultSet.PreparedMetadata meta = new ResultSet.PreparedMetadata(columnNames, new Short[]{2, 1});
-        ByteBuf buf = Unpooled.buffer(meta.codec.encodedSize(meta, Server.VERSION_4));
-        meta.codec.encode(meta, buf, Server.VERSION_4);
-        ResultSet.PreparedMetadata decodedMeta = meta.codec.decode(buf, Server.VERSION_4);
+        if (version == ProtocolVersion.V3)
+        {
+            // v3 encoding doesn't include partition key bind indexes
+            ResultSet.PreparedMetadata meta = new ResultSet.PreparedMetadata(columnNames, new short[]{ 2, 1 });
+            ByteBuf buf = Unpooled.buffer(ResultSet.PreparedMetadata.codec.encodedSize(meta, version));
+            ResultSet.PreparedMetadata.codec.encode(meta, buf, version);
+            ResultSet.PreparedMetadata decodedMeta = ResultSet.PreparedMetadata.codec.decode(buf, version);
+
+            assertNotSame(meta, decodedMeta);
+
+            // however, if there are no partition key indexes, they should be the same
+            ResultSet.PreparedMetadata metaWithoutIndexes = new ResultSet.PreparedMetadata(columnNames, null);
+            buf = Unpooled.buffer(metaWithoutIndexes.codec.encodedSize(metaWithoutIndexes, version));
+            metaWithoutIndexes.codec.encode(metaWithoutIndexes, buf, version);
+            ResultSet.PreparedMetadata decodedMetaWithoutIndexes = metaWithoutIndexes.codec.decode(buf, version);
+
+            assertEquals(decodedMeta, decodedMetaWithoutIndexes);
+        }
+        else
+        {
+            ResultSet.PreparedMetadata meta = new ResultSet.PreparedMetadata(columnNames, new short[]{ 2, 1 });
+            ByteBuf buf = Unpooled.buffer(ResultSet.PreparedMetadata.codec.encodedSize(meta, version));
+            ResultSet.PreparedMetadata.codec.encode(meta, buf, version);
+            ResultSet.PreparedMetadata decodedMeta = ResultSet.PreparedMetadata.codec.decode(buf, version);
+            assertEquals(meta, decodedMeta);
+        }
+    }
+
+    @Test
+    public void metadataSerializationTest()
+    {
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
+            metadataSerializationTest(version);
+    }
+
+    private void metadataSerializationTest(ProtocolVersion version)
+    {
+        List<ColumnSpecification> columnNames = new ArrayList<>();
+        for (int i = 0; i < 3; i++)
+            columnNames.add(new ColumnSpecification("ks", "cf", new ColumnIdentifier("col" + i, false), Int32Type.instance));
+
+        ResultSet.ResultMetadata meta = new ResultSet.ResultMetadata(columnNames);
+        ByteBuf buf = Unpooled.buffer(meta.codec.encodedSize(meta, version));
+        meta.codec.encode(meta, buf, version);
+        ResultSet.ResultMetadata decodedMeta = meta.codec.decode(buf, version);
 
         assertEquals(meta, decodedMeta);
+    }
 
-        // v3 encoding doesn't include partition key bind indexes
-        buf = Unpooled.buffer(meta.codec.encodedSize(meta, Server.VERSION_3));
-        meta.codec.encode(meta, buf, Server.VERSION_3);
-        decodedMeta = meta.codec.decode(buf, Server.VERSION_3);
+    @Test
+    public void queryOptionsSerDeserTest() throws Exception
+    {
+        for (ProtocolVersion version : ProtocolVersion.SUPPORTED)
+            queryOptionsSerDeserTest(version);
+    }
 
-        assertNotSame(meta, decodedMeta);
+    private void queryOptionsSerDeserTest(ProtocolVersion version) throws Exception
+    {
+        QueryOptions options = QueryOptions.create(ConsistencyLevel.ALL,
+                                                   Collections.singletonList(ByteBuffer.wrap(new byte[] { 0x00, 0x01, 0x02 })),
+                                                   false,
+                                                   5000,
+                                                   Util.makeSomePagingState(version),
+                                                   ConsistencyLevel.SERIAL,
+                                                   version
+                                                   );
 
-        // however, if there are no partition key indexes, they should be the same
-        ResultSet.PreparedMetadata metaWithoutIndexes = new ResultSet.PreparedMetadata(columnNames, null);
-        buf = Unpooled.buffer(metaWithoutIndexes.codec.encodedSize(metaWithoutIndexes, Server.VERSION_4));
-        metaWithoutIndexes.codec.encode(metaWithoutIndexes, buf, Server.VERSION_4);
-        ResultSet.PreparedMetadata decodedMetaWithoutIndexes = metaWithoutIndexes.codec.decode(buf, Server.VERSION_4);
+        ByteBuf buf = Unpooled.buffer(QueryOptions.codec.encodedSize(options, version));
+        QueryOptions.codec.encode(options, buf, version);
+        QueryOptions decodedOptions = QueryOptions.codec.decode(buf, version);
 
-        assertEquals(decodedMeta, decodedMetaWithoutIndexes);
+        assertNotNull(decodedOptions);
+        assertEquals(options.getConsistency(), decodedOptions.getConsistency());
+        assertEquals(options.getSerialConsistency(), decodedOptions.getSerialConsistency());
+        assertEquals(options.getPageSize(), decodedOptions.getPageSize());
+        assertEquals(options.getProtocolVersion(), decodedOptions.getProtocolVersion());
+        assertEquals(options.getValues(), decodedOptions.getValues());
+        assertEquals(options.getPagingState(), decodedOptions.getPagingState());
+        assertEquals(options.skipMetadata(), decodedOptions.skipMetadata());
     }
 }
diff --git a/test/unit/org/apache/cassandra/transport/WrappedSimpleClient.java b/test/unit/org/apache/cassandra/transport/WrappedSimpleClient.java
index af7c1d7..151d284 100644
--- a/test/unit/org/apache/cassandra/transport/WrappedSimpleClient.java
+++ b/test/unit/org/apache/cassandra/transport/WrappedSimpleClient.java
@@ -27,7 +27,7 @@
  */
 public class WrappedSimpleClient extends SimpleClient
 {
-    public WrappedSimpleClient(String host, int port, int version, EncryptionOptions.ClientEncryptionOptions encryptionOptions)
+    public WrappedSimpleClient(String host, int port, ProtocolVersion version, EncryptionOptions.ClientEncryptionOptions encryptionOptions)
     {
         super(host, port, version, encryptionOptions);
     }
@@ -37,11 +37,16 @@
         super(host, port, encryptionOptions);
     }
 
-    public WrappedSimpleClient(String host, int port, int version)
+    public WrappedSimpleClient(String host, int port, ProtocolVersion version)
     {
         super(host, port, version);
     }
 
+    public WrappedSimpleClient(String host, int port, ProtocolVersion version, boolean useBeta, EncryptionOptions.ClientEncryptionOptions encryptionOptions)
+    {
+        super(host, port, version, useBeta, encryptionOptions);
+    }
+
     public WrappedSimpleClient(String host, int port)
     {
         super(host, port);
diff --git a/test/unit/org/apache/cassandra/triggers/TriggerExecutorTest.java b/test/unit/org/apache/cassandra/triggers/TriggerExecutorTest.java
index d3c6961..0e4130d 100644
--- a/test/unit/org/apache/cassandra/triggers/TriggerExecutorTest.java
+++ b/test/unit/org/apache/cassandra/triggers/TriggerExecutorTest.java
@@ -19,10 +19,12 @@
 
 import java.util.*;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.cassandra.Util;
 import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.marshal.UTF8Type;
@@ -40,6 +42,12 @@
 
 public class TriggerExecutorTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void sameKeySameCfColumnFamilies() throws ConfigurationException, InvalidRequestException
     {
@@ -287,9 +295,9 @@
         builder.newRow(Clustering.EMPTY);
         long ts = FBUtilities.timestampMicros();
         if (columnValue1 != null)
-            builder.addCell(BufferCell.live(metadata, metadata.getColumnDefinition(bytes("c1")), ts, bytes(columnValue1)));
+            builder.addCell(BufferCell.live(metadata.getColumnDefinition(bytes("c1")), ts, bytes(columnValue1)));
         if (columnValue2 != null)
-            builder.addCell(BufferCell.live(metadata, metadata.getColumnDefinition(bytes("c2")), ts, bytes(columnValue2)));
+            builder.addCell(BufferCell.live(metadata.getColumnDefinition(bytes("c2")), ts, bytes(columnValue2)));
 
         return PartitionUpdate.singleRowUpdate(metadata, Util.dk(key), builder.build());
     }
@@ -363,12 +371,4 @@
             return cmp != 0 ? cmp : m1.key().compareTo(m2.key());
         }
     }
-
-    private static class CfComparator implements Comparator<Partition>
-    {
-        public int compare(Partition cf1, Partition cf2)
-        {
-            return cf1.metadata().cfName.compareTo(cf2.metadata().cfName);
-        }
-    }
 }
diff --git a/test/unit/org/apache/cassandra/triggers/TriggersTest.java b/test/unit/org/apache/cassandra/triggers/TriggersTest.java
index 70e040b..5f2a553 100644
--- a/test/unit/org/apache/cassandra/triggers/TriggersTest.java
+++ b/test/unit/org/apache/cassandra/triggers/TriggersTest.java
@@ -17,7 +17,6 @@
  */
 package org.apache.cassandra.triggers;
 
-import java.nio.ByteBuffer;
 import java.util.Collection;
 import java.util.Collections;
 
@@ -34,7 +33,6 @@
 import org.apache.cassandra.db.ConsistencyLevel;
 import org.apache.cassandra.db.Mutation;
 import org.apache.cassandra.db.partitions.Partition;
-import org.apache.cassandra.db.partitions.PartitionUpdate;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.RequestExecutionException;
 import org.apache.cassandra.service.StorageService;
@@ -196,9 +194,7 @@
         assertUpdateIsAugmented(6, "v1", 6);
     }
 
-    // Unfortunately, an IRE thrown from StorageProxy.cas
-    // results in a RuntimeException from QueryProcessor.process
-    @Test(expected=RuntimeException.class)
+    @Test(expected=org.apache.cassandra.exceptions.InvalidRequestException.class)
     public void onCqlUpdateWithConditionsRejectGeneratedUpdatesForDifferentPartition() throws Exception
     {
         String cf = "cf" + System.nanoTime();
@@ -214,9 +210,7 @@
         }
     }
 
-    // Unfortunately, an IRE thrown from StorageProxy.cas
-    // results in a RuntimeException from QueryProcessor.process
-    @Test(expected=RuntimeException.class)
+    @Test(expected=org.apache.cassandra.exceptions.InvalidRequestException.class)
     public void onCqlUpdateWithConditionsRejectGeneratedUpdatesForDifferentTable() throws Exception
     {
         String cf = "cf" + System.nanoTime();
@@ -282,6 +276,27 @@
         }
     }
 
+    @Test(expected=org.apache.cassandra.exceptions.InvalidRequestException.class)
+    public void ifTriggerThrowsErrorNoMutationsAreApplied() throws Exception
+    {
+        String cf = "cf" + System.nanoTime();
+        try
+        {
+            setupTableWithTrigger(cf, ErrorTrigger.class);
+            String cql = String.format("INSERT INTO %s.%s (k, v1) VALUES (11, 11)", ksName, cf);
+            QueryProcessor.process(cql, ConsistencyLevel.ONE);
+        }
+        catch (Exception e)
+        {
+            assertTrue(e.getMessage().equals(ErrorTrigger.MESSAGE));
+            throw e;
+        }
+        finally
+        {
+            assertUpdateNotExecuted(cf, 11);
+        }
+    }
+
     private void setupTableWithTrigger(String cf, Class<? extends ITrigger> triggerImpl)
     throws RequestExecutionException
     {
@@ -357,4 +372,13 @@
             return Collections.singletonList(update.build());
         }
     }
+
+    public static class ErrorTrigger implements ITrigger
+    {
+        public static final String MESSAGE = "Thrown by ErrorTrigger";
+        public Collection<Mutation> augment(Partition partition)
+        {
+            throw new org.apache.cassandra.exceptions.InvalidRequestException(MESSAGE);
+        }
+    }
 }
diff --git a/test/unit/org/apache/cassandra/utils/BTreeTest.java b/test/unit/org/apache/cassandra/utils/BTreeTest.java
index ffd7315..9a59e3a 100644
--- a/test/unit/org/apache/cassandra/utils/BTreeTest.java
+++ b/test/unit/org/apache/cassandra/utils/BTreeTest.java
@@ -21,6 +21,7 @@
 import java.util.concurrent.ThreadLocalRandom;
 
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import org.junit.Test;
 
 import junit.framework.Assert;
@@ -128,6 +129,30 @@
             checkResult(i, BTree.update(BTree.build(seq(i), noOp), CMP, seq(i), updateF));
     }
 
+    @Test
+    public void testApplyForwards()
+    {
+        List<Integer> input = seq(71);
+        Object[] btree = BTree.build(input, noOp);
+
+        final List<Integer> result = new ArrayList<>();
+        BTree.<Integer>apply(btree, i -> result.add(i), false);
+
+        org.junit.Assert.assertArrayEquals(input.toArray(),result.toArray());
+    }
+
+    @Test
+    public void testApplyReverse()
+    {
+        List<Integer> input = seq(71);
+        Object[] btree = BTree.build(input, noOp);
+
+        final List<Integer> result = new ArrayList<>();
+        BTree.<Integer>apply(btree, i -> result.add(i), true);
+
+        org.junit.Assert.assertArrayEquals(Lists.reverse(input).toArray(),result.toArray());
+    }
+
     /**
      * Tests that the apply method of the <code>UpdateFunction</code> is only called once with each key update.
      * (see CASSANDRA-8018).
@@ -214,14 +239,16 @@
                 builder.add(i);
             // for sorted input, check non-resolve path works before checking resolution path
             checkResolverOutput(count, builder.build(), BTree.Dir.ASC);
-            builder.reuse();
+            builder = BTree.builder(Comparator.naturalOrder());
+            builder.setQuickResolver(resolver);
             for (int i = 0 ; i < 10 ; i++)
             {
                 // now do a few runs of randomized inputs
                 for (Accumulator j : resolverInput(count, true))
                     builder.add(j);
                 checkResolverOutput(count, builder.build(), BTree.Dir.ASC);
-                builder.reuse();
+                builder = BTree.builder(Comparator.naturalOrder());
+                builder.setQuickResolver(resolver);
             }
             for (List<Accumulator> add : splitResolverInput(count))
             {
@@ -231,10 +258,35 @@
                     builder.addAll(new TreeSet<>(add));
             }
             checkResolverOutput(count, builder.build(), BTree.Dir.ASC);
-            builder.reuse();
         }
     }
 
+    @Test
+    public void testBuilderReuse()
+    {
+        List<Integer> sorted = seq(20);
+        BTree.Builder<Integer> builder = BTree.builder(Comparator.naturalOrder());
+        builder.auto(false);
+        for (int i : sorted)
+            builder.add(i);
+        checkResult(20, builder.build());
+
+        builder.reuse();
+        assertTrue(builder.build() == BTree.empty());
+        for (int i = 0; i < 12; i++)
+            builder.add(sorted.get(i));
+        checkResult(12, builder.build());
+
+        builder.auto(true);
+        builder.reuse(Comparator.reverseOrder());
+        for (int i = 0; i < 12; i++)
+            builder.add(sorted.get(i));
+        checkResult(12, builder.build(), BTree.Dir.DESC);
+
+        builder.reuse();
+        assertTrue(builder.build() == BTree.empty());
+    }
+
     private static class Accumulator extends Number implements Comparable<Accumulator>
     {
         final int base;
@@ -278,7 +330,14 @@
                 builder.add(i);
             // for sorted input, check non-resolve path works before checking resolution path
             Assert.assertTrue(Iterables.elementsEqual(sorted, BTree.iterable(builder.build())));
+
+            builder = BTree.builder(Comparator.naturalOrder());
+            builder.auto(false);
+            for (Accumulator i : sorted)
+                builder.add(i);
+            // check resolution path
             checkResolverOutput(count, builder.resolve(resolver).build(), BTree.Dir.ASC);
+
             builder = BTree.builder(Comparator.naturalOrder());
             builder.auto(false);
             for (int i = 0 ; i < 10 ; i++)
@@ -287,11 +346,13 @@
                 for (Accumulator j : resolverInput(count, true))
                     builder.add(j);
                 checkResolverOutput(count, builder.sort().resolve(resolver).build(), BTree.Dir.ASC);
-                builder.reuse();
+                builder = BTree.builder(Comparator.naturalOrder());
+                builder.auto(false);
                 for (Accumulator j : resolverInput(count, true))
                     builder.add(j);
                 checkResolverOutput(count, builder.sort().reverse().resolve(resolver).build(), BTree.Dir.DESC);
-                builder.reuse();
+                builder = BTree.builder(Comparator.naturalOrder());
+                builder.auto(false);
             }
         }
     }
@@ -350,7 +411,12 @@
 
     private static void checkResult(int count, Object[] btree)
     {
-        Iterator<Integer> iter = BTree.slice(btree, CMP, BTree.Dir.ASC);
+        checkResult(count, btree, BTree.Dir.ASC);
+    }
+
+    private static void checkResult(int count, Object[] btree, BTree.Dir dir)
+    {
+        Iterator<Integer> iter = BTree.slice(btree, CMP, dir);
         int i = 0;
         while (iter.hasNext())
             assertEquals(iter.next(), ints[i++]);
diff --git a/test/unit/org/apache/cassandra/utils/BloomFilterTest.java b/test/unit/org/apache/cassandra/utils/BloomFilterTest.java
index 2e76e0e..818af9c 100644
--- a/test/unit/org/apache/cassandra/utils/BloomFilterTest.java
+++ b/test/unit/org/apache/cassandra/utils/BloomFilterTest.java
@@ -20,16 +20,13 @@
 
 import java.io.*;
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.List;
 import java.util.Random;
 import java.util.Set;
 
 import org.junit.*;
 
-import org.apache.cassandra.db.marshal.Int32Type;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.Murmur3Partitioner;
 import org.apache.cassandra.io.util.BufferedDataOutputStreamPlus;
diff --git a/test/unit/org/apache/cassandra/utils/ByteBufferUtilTest.java b/test/unit/org/apache/cassandra/utils/ByteBufferUtilTest.java
index 3f34102..f2746f6 100644
--- a/test/unit/org/apache/cassandra/utils/ByteBufferUtilTest.java
+++ b/test/unit/org/apache/cassandra/utils/ByteBufferUtilTest.java
@@ -24,7 +24,9 @@
 import java.nio.ByteBuffer;
 import java.nio.charset.CharacterCodingException;
 import java.util.Arrays;
+import java.util.concurrent.ThreadLocalRandom;
 
+import org.junit.Assert;
 import org.junit.Test;
 
 import org.apache.cassandra.io.util.DataOutputBuffer;
@@ -247,4 +249,55 @@
         assertEquals(bb, bb2);
         assertEquals("0102", s);
     }
+
+    @Test
+    public void testStartsAndEndsWith()
+    {
+        byte[] bytes = new byte[512];
+        ThreadLocalRandom random = ThreadLocalRandom.current();
+
+        random.nextBytes(bytes);
+
+        ByteBuffer a = ByteBuffer.wrap(bytes);
+        ByteBuffer b = a.duplicate();
+
+        // let's take random slices of a and match
+        for (int i = 0; i < 512; i++)
+        {
+            // prefix from the original offset
+            b.position(0).limit(a.remaining() - random.nextInt(0, a.remaining() - 1));
+            Assert.assertTrue(ByteBufferUtil.startsWith(a, b));
+            Assert.assertTrue(ByteBufferUtil.startsWith(a, b.slice()));
+
+            // prefix from random position inside of array
+            int pos = random.nextInt(1, a.remaining() - 5);
+            a.position(pos);
+            b.limit(bytes.length - 1).position(pos);
+
+            Assert.assertTrue(ByteBufferUtil.startsWith(a, b));
+
+            a.position(0);
+
+            // endsWith at random position
+            b.limit(a.remaining()).position(random.nextInt(0, a.remaining() - 1));
+            Assert.assertTrue(ByteBufferUtil.endsWith(a, b));
+            Assert.assertTrue(ByteBufferUtil.endsWith(a, b.slice()));
+
+        }
+
+        a.limit(bytes.length - 1).position(0);
+        b.limit(bytes.length - 1).position(1);
+
+        Assert.assertFalse(ByteBufferUtil.startsWith(a, b));
+        Assert.assertFalse(ByteBufferUtil.startsWith(a, b.slice()));
+
+        Assert.assertTrue(ByteBufferUtil.endsWith(a, b));
+        Assert.assertTrue(ByteBufferUtil.endsWith(a, b.slice()));
+
+
+        a.position(5);
+
+        Assert.assertFalse(ByteBufferUtil.startsWith(a, b));
+        Assert.assertFalse(ByteBufferUtil.endsWith(a, b));
+    }
 }
diff --git a/test/unit/org/apache/cassandra/utils/CassandraVersionTest.java b/test/unit/org/apache/cassandra/utils/CassandraVersionTest.java
index 145b735..da7e266 100644
--- a/test/unit/org/apache/cassandra/utils/CassandraVersionTest.java
+++ b/test/unit/org/apache/cassandra/utils/CassandraVersionTest.java
@@ -17,9 +17,17 @@
  */
 package org.apache.cassandra.utils;
 
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
 import org.junit.Test;
 
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.matchers.JUnitMatchers.containsString;
 
 public class CassandraVersionTest
 {
@@ -29,14 +37,18 @@
         CassandraVersion version;
 
         version = new CassandraVersion("1.2.3");
-        assert version.major == 1 && version.minor == 2 && version.patch == 3;
+        assertTrue(version.major == 1 && version.minor == 2 && version.patch == 3);
 
         version = new CassandraVersion("1.2.3-foo.2+Bar");
-        assert version.major == 1 && version.minor == 2 && version.patch == 3;
+        assertTrue(version.major == 1 && version.minor == 2 && version.patch == 3);
 
         // CassandraVersion can parse 4th '.' as build number
         version = new CassandraVersion("1.2.3.456");
-        assert version.major == 1 && version.minor == 2 && version.patch == 3;
+        assertTrue(version.major == 1 && version.minor == 2 && version.patch == 3);
+
+        // support for tick-tock release
+        version = new CassandraVersion("3.2");
+        assertTrue(version.major == 3 && version.minor == 2 && version.patch == 0);
     }
 
     @Test
@@ -46,32 +58,32 @@
 
         v1 = new CassandraVersion("1.2.3");
         v2 = new CassandraVersion("1.2.4");
-        assert v1.compareTo(v2) == -1;
+        assertTrue(v1.compareTo(v2) == -1);
 
         v1 = new CassandraVersion("1.2.3");
         v2 = new CassandraVersion("1.2.3");
-        assert v1.compareTo(v2) == 0;
+        assertTrue(v1.compareTo(v2) == 0);
 
         v1 = new CassandraVersion("1.2.3");
         v2 = new CassandraVersion("2.0.0");
-        assert v1.compareTo(v2) == -1;
-        assert v2.compareTo(v1) == 1;
+        assertTrue(v1.compareTo(v2) == -1);
+        assertTrue(v2.compareTo(v1) == 1);
 
         v1 = new CassandraVersion("1.2.3");
         v2 = new CassandraVersion("1.2.3-alpha");
-        assert v1.compareTo(v2) == 1;
+        assertTrue(v1.compareTo(v2) == 1);
 
         v1 = new CassandraVersion("1.2.3");
         v2 = new CassandraVersion("1.2.3+foo");
-        assert v1.compareTo(v2) == -1;
+        assertTrue(v1.compareTo(v2) == -1);
 
         v1 = new CassandraVersion("1.2.3");
         v2 = new CassandraVersion("1.2.3-alpha+foo");
-        assert v1.compareTo(v2) == 1;
+        assertTrue(v1.compareTo(v2) == 1);
 
         v1 = new CassandraVersion("1.2.3-alpha+1");
         v2 = new CassandraVersion("1.2.3-alpha+2");
-        assert v1.compareTo(v2) == -1;
+        assertTrue(v1.compareTo(v2) == -1);
     }
 
     @Test
@@ -80,33 +92,48 @@
         CassandraVersion v1, v2;
 
         v1 = new CassandraVersion("3.0.2");
-        assert v1.isSupportedBy(v1);
+        assertTrue(v1.isSupportedBy(v1));
 
         v1 = new CassandraVersion("1.2.3");
         v2 = new CassandraVersion("1.2.4");
-        assert v1.isSupportedBy(v2);
-        assert !v2.isSupportedBy(v1);
+        assertTrue(v1.isSupportedBy(v2));
+        assertTrue(!v2.isSupportedBy(v1));
 
         v1 = new CassandraVersion("1.2.3");
         v2 = new CassandraVersion("1.3.3");
-        assert v1.isSupportedBy(v2);
-        assert !v2.isSupportedBy(v1);
+        assertTrue(v1.isSupportedBy(v2));
+        assertTrue(!v2.isSupportedBy(v1));
 
         v1 = new CassandraVersion("2.2.3");
         v2 = new CassandraVersion("1.3.3");
-        assert !v1.isSupportedBy(v2);
-        assert !v2.isSupportedBy(v1);
+        assertTrue(!v1.isSupportedBy(v2));
+        assertTrue(!v2.isSupportedBy(v1));
 
         v1 = new CassandraVersion("3.1.0");
         v2 = new CassandraVersion("3.0.1");
-        assert !v1.isSupportedBy(v2);
-        assert v2.isSupportedBy(v1);
+        assertTrue(!v1.isSupportedBy(v2));
+        assertTrue(v2.isSupportedBy(v1));
+
+        v1 = new CassandraVersion("3.7");
+        v2 = new CassandraVersion("3.8");
+        assertTrue(v1.isSupportedBy(v2));
+        assertTrue(!v2.isSupportedBy(v1));
+
+        v1 = new CassandraVersion("3.0.8");
+        v2 = new CassandraVersion("3.8");
+        assertTrue(v1.isSupportedBy(v2));
+        assertTrue(!v2.isSupportedBy(v1));
+        assertTrue(v2.isSupportedBy(v2));
+
+        v1 = new CassandraVersion("3.8");
+        v2 = new CassandraVersion("3.8-SNAPSHOT");
+        assertTrue(v1.isSupportedBy(v2));
+        assertTrue(v2.isSupportedBy(v1));
     }
 
     @Test
     public void testInvalid()
     {
-        assertThrows("1.0");
         assertThrows("1.0.0a");
         assertThrows("1.a.4");
         assertThrows("1.0.0-foo&");
@@ -132,6 +159,22 @@
         prev = next;
         next = new CassandraVersion("2.2.0");
         assertTrue(prev.compareTo(next) < 0);
+
+        prev = next;
+        next = new CassandraVersion("3.1");
+        assertTrue(prev.compareTo(next) < 0);
+
+        prev = next;
+        next = new CassandraVersion("3.1.1");
+        assertTrue(prev.compareTo(next) < 0);
+
+        prev = next;
+        next = new CassandraVersion("3.2-rc1-SNAPSHOT");
+        assertTrue(prev.compareTo(next) < 0);
+
+        prev = next;
+        next = new CassandraVersion("3.2");
+        assertTrue(prev.compareTo(next) < 0);
     }
 
     private static void assertThrows(String str)
@@ -139,8 +182,51 @@
         try
         {
             new CassandraVersion(str);
-            assert false;
+            fail();
         }
         catch (IllegalArgumentException e) {}
     }
+
+    @Test
+    public void testParseIdentifiersPositive() throws Throwable
+    {
+        String[] result = parseIdentifiers("DUMMY", "+a.b.cde.f_g.");
+        String[] expected = {"a", "b", "cde", "f_g"};
+        assertArrayEquals(expected, result);
+    }
+
+    @Test
+    public void testParseIdentifiersNegative() throws Throwable
+    {
+        String version = "DUMMY";
+        try
+        {
+            parseIdentifiers(version, "+a. .b");
+
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertThat(e.getMessage(), containsString(version));
+        }
+    }
+    private static String[] parseIdentifiers(String version, String str) throws Throwable
+    {
+        String name = "parseIdentifiers";
+        Class[] args = {String.class, String.class};
+        for (Method m: CassandraVersion.class.getDeclaredMethods())
+        {
+            if (name.equals(m.getName()) &&
+                    Arrays.equals(args, m.getParameterTypes()))
+            {
+                m.setAccessible(true);
+                try
+                {
+                return (String[]) m.invoke(null, version, str);
+                } catch (InvocationTargetException e){
+                    throw e.getTargetException();
+                }
+            }
+        }
+        throw new NoSuchMethodException(CassandraVersion.class + "." + name + Arrays.toString(args));
+    }
 }
diff --git a/test/unit/org/apache/cassandra/utils/CoalescingStrategiesTest.java b/test/unit/org/apache/cassandra/utils/CoalescingStrategiesTest.java
index 26b6b3a..b10d70b 100644
--- a/test/unit/org/apache/cassandra/utils/CoalescingStrategiesTest.java
+++ b/test/unit/org/apache/cassandra/utils/CoalescingStrategiesTest.java
@@ -106,7 +106,7 @@
     @BeforeClass
     public static void initDD()
     {
-        DatabaseDescriptor.forceStaticInitialization();
+        DatabaseDescriptor.daemonInitialization();
     }
 
     @SuppressWarnings({ "serial" })
diff --git a/test/unit/org/apache/cassandra/utils/FBUtilitiesTest.java b/test/unit/org/apache/cassandra/utils/FBUtilitiesTest.java
index f13d076..ecdb03e 100644
--- a/test/unit/org/apache/cassandra/utils/FBUtilitiesTest.java
+++ b/test/unit/org/apache/cassandra/utils/FBUtilitiesTest.java
@@ -1,4 +1,4 @@
-/**
+/*
  * 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
@@ -23,10 +23,11 @@
 import java.nio.ByteBuffer;
 import java.nio.charset.CharacterCodingException;
 import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TreeMap;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -35,10 +36,12 @@
 import java.util.concurrent.TimeUnit;
 
 import com.google.common.primitives.Ints;
+
+import org.junit.Assert;
 import org.junit.Test;
 
-import java.util.Map;
-import java.util.TreeMap;
+import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.dht.*;
 
 import org.apache.cassandra.config.Config;
 import org.apache.cassandra.config.DatabaseDescriptor;
@@ -110,6 +113,53 @@
         ByteBufferUtil.string(bytes, StandardCharsets.UTF_8);
     }
 
+    private static void assertPartitioner(String name, Class expected)
+    {
+        Assert.assertTrue(String.format("%s != %s", name, expected.toString()),
+                          expected.isInstance(FBUtilities.newPartitioner(name)));
+    }
+
+    /**
+     * Check that given a name, the correct partitioner instance is created.
+     *
+     * If the assertions in this test start failing, it likely means the sstabledump/sstablemetadata tools will
+     * also fail to read existing sstables.
+     */
+    @Test
+    public void testNewPartitionerNoArgConstructors()
+    {
+        assertPartitioner("ByteOrderedPartitioner", ByteOrderedPartitioner.class);
+        assertPartitioner("LengthPartitioner", LengthPartitioner.class);
+        assertPartitioner("Murmur3Partitioner", Murmur3Partitioner.class);
+        assertPartitioner("OrderPreservingPartitioner", OrderPreservingPartitioner.class);
+        assertPartitioner("RandomPartitioner", RandomPartitioner.class);
+        assertPartitioner("org.apache.cassandra.dht.ByteOrderedPartitioner", ByteOrderedPartitioner.class);
+        assertPartitioner("org.apache.cassandra.dht.LengthPartitioner", LengthPartitioner.class);
+        assertPartitioner("org.apache.cassandra.dht.Murmur3Partitioner", Murmur3Partitioner.class);
+        assertPartitioner("org.apache.cassandra.dht.OrderPreservingPartitioner", OrderPreservingPartitioner.class);
+        assertPartitioner("org.apache.cassandra.dht.RandomPartitioner", RandomPartitioner.class);
+    }
+
+    /**
+     * Check that we can instantiate local partitioner correctly and that we can pass the correct type
+     * to it as a constructor argument.
+     *
+     * If the assertions in this test start failing, it likely means the sstabledump/sstablemetadata tools will
+     * also fail to read existing sstables.
+     */
+    @Test
+    public void testNewPartitionerLocalPartitioner()
+    {
+        for (String name : new String[] {"LocalPartitioner", "org.apache.cassandra.dht.LocalPartitioner"})
+            for (AbstractType<?> type : new AbstractType<?>[] {UUIDType.instance, ListType.getInstance(Int32Type.instance, true)})
+            {
+                IPartitioner partitioner = FBUtilities.newPartitioner(name, Optional.of(type));
+                Assert.assertTrue(String.format("%s != LocalPartitioner", partitioner.toString()),
+                                  LocalPartitioner.class.isInstance(partitioner));
+                Assert.assertEquals(partitioner.partitionOrdering(), type);
+            }
+    }
+
     @Test
     public void testGetBroadcastRpcAddress() throws Exception
     {
diff --git a/test/unit/org/apache/cassandra/utils/JVMStabilityInspectorTest.java b/test/unit/org/apache/cassandra/utils/JVMStabilityInspectorTest.java
index 1a2a864..1f58ab0 100644
--- a/test/unit/org/apache/cassandra/utils/JVMStabilityInspectorTest.java
+++ b/test/unit/org/apache/cassandra/utils/JVMStabilityInspectorTest.java
@@ -21,6 +21,7 @@
 import java.io.IOException;
 import java.net.SocketException;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.cassandra.config.Config;
@@ -36,6 +37,12 @@
 
 public class JVMStabilityInspectorTest
 {
+    @BeforeClass
+    public static void initDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testKill() throws Exception
     {
diff --git a/test/unit/org/apache/cassandra/utils/KeyGenerator.java b/test/unit/org/apache/cassandra/utils/KeyGenerator.java
index 8a9d8b8..df958678 100644
--- a/test/unit/org/apache/cassandra/utils/KeyGenerator.java
+++ b/test/unit/org/apache/cassandra/utils/KeyGenerator.java
@@ -24,7 +24,8 @@
 
 public class KeyGenerator
 {
-    private static ByteBuffer randomKey(Random r) {
+    private static ByteBuffer randomKey(Random r) 
+    {
         byte[] bytes = new byte[48];
         r.nextBytes(bytes);
         return ByteBuffer.wrap(bytes);
@@ -35,31 +36,37 @@
         int i, n, seed;
         Random random;
 
-        RandomStringGenerator(int seed, int n) {
+        RandomStringGenerator(int seed, int n) 
+        {
             i = 0;
             this.seed = seed;
             this.n = n;
             reset();
         }
 
-        public int size() {
+        public int size() 
+        {
             return n;
         }
 
-        public void reset() {
+        public void reset() 
+        {
             random = new Random(seed);
         }
 
-        public boolean hasNext() {
+        public boolean hasNext() 
+        {
             return i < n;
         }
 
-        public ByteBuffer next() {
+        public ByteBuffer next() 
+        {
             i++;
             return randomKey(random);
         }
 
-        public void remove() {
+        public void remove() 
+        {
             throw new UnsupportedOperationException();
         }
     }
@@ -68,33 +75,40 @@
     {
         private int i, start, n;
 
-        IntGenerator(int n) {
+        IntGenerator(int n) 
+        {
             this(0, n);
         }
 
-        IntGenerator(int start, int n) {
+        IntGenerator(int start, int n) 
+        {
             this.start = start;
             this.n = n;
             reset();
         }
 
-        public int size() {
+        public int size() 
+        {
             return n - start;
         }
 
-        public void reset() {
+        public void reset() 
+        {
             i = start;
         }
 
-        public boolean hasNext() {
+        public boolean hasNext() 
+        {
             return i < n;
         }
 
-        public ByteBuffer next() {
+        public ByteBuffer next() 
+        {
             return ByteBufferUtil.bytes(Integer.toString(i++));
         }
 
-        public void remove() {
+        public void remove() 
+        {
             throw new UnsupportedOperationException();
         }
     }
@@ -103,14 +117,18 @@
     {
         static int WORDS;
 
-        static {
-            try {
-                BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("/usr/share/dict/words")));
-                while (br.ready()) {
+        static 
+        {
+            try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("/usr/share/dict/words")))) 
+            {
+                while (br.ready()) 
+                {
                     br.readLine();
                     WORDS++;
                 }
-            } catch (IOException e) {
+            } 
+            catch (IOException e) 
+            {
                 WORDS = 0;
             }
         }
@@ -120,50 +138,67 @@
         private int skip;
         byte[] next;
 
-        WordGenerator(int skip, int modulo) {
+        WordGenerator(int skip, int modulo) 
+        {
             this.skip = skip;
             this.modulo = modulo;
             reset();
         }
 
-        public int size() {
+        public int size() 
+        {
             return (1 + WORDS - skip) / modulo;
         }
 
-        public void reset() {
-            try {
+        public void reset() 
+        {
+            try 
+            {
                 reader = new BufferedReader(new InputStreamReader(new FileInputStream("/usr/share/dict/words")));
-            } catch (FileNotFoundException e) {
+            } 
+            catch (FileNotFoundException e) 
+            {
                 throw new RuntimeException(e);
             }
-            for (int i = 0; i < skip; i++) {
-                try {
+            for (int i = 0; i < skip; i++) 
+            {
+                try 
+                {
                     reader.readLine();
-                } catch (IOException e) {
+                } 
+                catch (IOException e) 
+                {
                     throw new RuntimeException(e);
                 }
             }
             next();
         }
 
-        public boolean hasNext() {
+        public boolean hasNext() 
+        {
             return next != null;
         }
 
-        public ByteBuffer next() {
-            try {
+        public ByteBuffer next() 
+        {
+            try 
+            {
                 byte[] s = next;
-                for (int i = 0; i < modulo; i++) {
+                for (int i = 0; i < modulo; i++) 
+                {
                     String line = reader.readLine();
                     next = line == null ? null : line.getBytes();
                 }
                 return s == null ? null : ByteBuffer.wrap(s);
-            } catch (IOException e) {
+            } 
+            catch (IOException e) 
+            {
                 throw new RuntimeException(e);
             }
         }
 
-        public void remove() {
+        public void remove() 
+        {
             throw new UnsupportedOperationException();
         }
     }
diff --git a/test/unit/org/apache/cassandra/utils/KillerForTests.java b/test/unit/org/apache/cassandra/utils/KillerForTests.java
index ad3a2743..a802809 100644
--- a/test/unit/org/apache/cassandra/utils/KillerForTests.java
+++ b/test/unit/org/apache/cassandra/utils/KillerForTests.java
@@ -18,6 +18,8 @@
 
 package org.apache.cassandra.utils;
 
+import org.junit.Assert;
+
 /**
  * Responsible for stubbing out the System.exit() logic during unit tests.
  */
@@ -25,15 +27,30 @@
 {
     private boolean killed = false;
     private boolean quiet = false;
+    private final boolean expected;
+
+    public KillerForTests()
+    {
+        expected = true;
+    }
+
+    public KillerForTests(boolean expectFailure)
+    {
+        expected = expectFailure;
+    }
 
     @Override
     protected void killCurrentJVM(Throwable t, boolean quiet)
     {
+        if (!expected)
+            Assert.fail("Saw JVM Kill but did not expect it.");
+
         if (killed)
         {
             // Can only be killed once
             return;
         }
+
         this.killed = true;
         this.quiet = quiet;
     }
diff --git a/test/unit/org/apache/cassandra/utils/MergeIteratorComparisonTest.java b/test/unit/org/apache/cassandra/utils/MergeIteratorComparisonTest.java
index 5f2de73..6d9d2f6 100644
--- a/test/unit/org/apache/cassandra/utils/MergeIteratorComparisonTest.java
+++ b/test/unit/org/apache/cassandra/utils/MergeIteratorComparisonTest.java
@@ -36,7 +36,6 @@
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.TimeUUIDType;
 import org.apache.cassandra.db.marshal.UUIDType;
-import org.apache.cassandra.utils.MergeIterator.Candidate;
 import org.apache.cassandra.utils.MergeIterator.Reducer;
 
 public class MergeIteratorComparisonTest
diff --git a/test/unit/org/apache/cassandra/utils/MerkleTreeTest.java b/test/unit/org/apache/cassandra/utils/MerkleTreeTest.java
index 64aea24..fb517d7 100644
--- a/test/unit/org/apache/cassandra/utils/MerkleTreeTest.java
+++ b/test/unit/org/apache/cassandra/utils/MerkleTreeTest.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.Lists;
 
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.dht.IPartitioner;
@@ -61,6 +62,12 @@
         return new Range<>(partitioner.getMinimumToken(), partitioner.getMinimumToken());
     }
 
+    @BeforeClass
+    public static void beforeClass()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Before
     public void clear()
     {
diff --git a/test/unit/org/apache/cassandra/utils/MerkleTreesTest.java b/test/unit/org/apache/cassandra/utils/MerkleTreesTest.java
index ec8fd68..8d7284c 100644
--- a/test/unit/org/apache/cassandra/utils/MerkleTreesTest.java
+++ b/test/unit/org/apache/cassandra/utils/MerkleTreesTest.java
@@ -26,6 +26,8 @@
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.dht.*;
 import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken;
 import org.apache.cassandra.io.util.DataInputBuffer;
@@ -60,6 +62,7 @@
     @BeforeClass
     public static void setUp()
     {
+        DatabaseDescriptor.daemonInitialization();
         StorageService.instance.setPartitionerUnsafe(partitioner);
     }
     @Before
diff --git a/test/unit/org/apache/cassandra/utils/NanoTimeToCurrentTimeMillisTest.java b/test/unit/org/apache/cassandra/utils/NanoTimeToCurrentTimeMillisTest.java
index 1662e77..25aeada 100644
--- a/test/unit/org/apache/cassandra/utils/NanoTimeToCurrentTimeMillisTest.java
+++ b/test/unit/org/apache/cassandra/utils/NanoTimeToCurrentTimeMillisTest.java
@@ -34,21 +34,18 @@
             now = Math.max(now, System.currentTimeMillis());
             if (ii % 10000 == 0)
             {
-                synchronized (NanoTimeToCurrentTimeMillis.TIMESTAMP_UPDATE)
-                {
-                    NanoTimeToCurrentTimeMillis.TIMESTAMP_UPDATE.notify();
-                }
+                NanoTimeToCurrentTimeMillis.updateNow();
                 Thread.sleep(1);
             }
 
             nowNanos = Math.max(nowNanos, System.nanoTime());
             long convertedNow = NanoTimeToCurrentTimeMillis.convert(nowNanos);
 
-            int maxDiff = FBUtilities.isWindows()? 15 : 1;
+            int maxDiff = FBUtilities.isWindows ? 15 : 1;
             assertTrue("convertedNow = " + convertedNow + " lastConverted = " + lastConverted + " in iteration " + ii,
                        convertedNow >= (lastConverted - maxDiff));
 
-            maxDiff = FBUtilities.isWindows()? 25 : 2;
+            maxDiff = FBUtilities.isWindows ? 25 : 2;
             assertTrue("now = " + now + " convertedNow = " + convertedNow + " in iteration " + ii,
                        (maxDiff - 2) <= convertedNow);
 
diff --git a/test/unit/org/apache/cassandra/utils/NoSpamLoggerTest.java b/test/unit/org/apache/cassandra/utils/NoSpamLoggerTest.java
index 702fa98..fe5d58e 100644
--- a/test/unit/org/apache/cassandra/utils/NoSpamLoggerTest.java
+++ b/test/unit/org/apache/cassandra/utils/NoSpamLoggerTest.java
@@ -39,7 +39,7 @@
 {
     Map<Level, Queue<Pair<String, Object[]>>> logged = new HashMap<>();
 
-   Logger mock = new SubstituteLogger(null)
+   Logger mock = new SubstituteLogger(null, null, true)
    {
 
        @Override
diff --git a/test/unit/org/apache/cassandra/utils/OverlapIteratorTest.java b/test/unit/org/apache/cassandra/utils/OverlapIteratorTest.java
index 5bbe267..6f99603 100644
--- a/test/unit/org/apache/cassandra/utils/OverlapIteratorTest.java
+++ b/test/unit/org/apache/cassandra/utils/OverlapIteratorTest.java
@@ -21,11 +21,6 @@
  */
 
 
-import java.io.ByteArrayInputStream;
-import java.io.DataInput;
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.lang.reflect.Constructor;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -34,13 +29,6 @@
 
 import org.junit.Test;
 
-import org.apache.cassandra.db.TypeSizes;
-import org.apache.cassandra.io.ISerializer;
-import org.apache.cassandra.io.IVersionedSerializer;
-import org.apache.cassandra.io.util.DataOutputBuffer;
-import org.apache.cassandra.io.util.DataOutputPlus;
-
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 public class OverlapIteratorTest
diff --git a/test/unit/org/apache/cassandra/utils/SerializationsTest.java b/test/unit/org/apache/cassandra/utils/SerializationsTest.java
index 9accaa6..5a8d7f9 100644
--- a/test/unit/org/apache/cassandra/utils/SerializationsTest.java
+++ b/test/unit/org/apache/cassandra/utils/SerializationsTest.java
@@ -22,10 +22,12 @@
 import java.io.IOException;
 
 import org.junit.Assert;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.cassandra.AbstractSerializationsTester;
 import org.apache.cassandra.Util;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.marshal.Int32Type;
 import org.apache.cassandra.io.util.DataInputPlus.DataInputStreamPlus;
@@ -38,6 +40,12 @@
 
 public class SerializationsTest extends AbstractSerializationsTester
 {
+    @BeforeClass
+    public static void initDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     private static void testBloomFilterWrite(boolean offheap, boolean oldBfHashOrder) throws IOException
     {
         IPartitioner partitioner = Util.testPartitioner();
diff --git a/test/unit/org/apache/cassandra/utils/SlidingTimeRateTest.java b/test/unit/org/apache/cassandra/utils/SlidingTimeRateTest.java
new file mode 100644
index 0000000..c83c748
--- /dev/null
+++ b/test/unit/org/apache/cassandra/utils/SlidingTimeRateTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.utils;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SlidingTimeRateTest
+{
+    @Test
+    public void testUpdateAndGet()
+    {
+        SlidingTimeRate rate = new SlidingTimeRate(new TestTimeSource(), 10, 1, TimeUnit.SECONDS);
+        int updates = 100;
+        for (int i = 0; i < updates; i++)
+        {
+            rate.update(1);
+        }
+        Assert.assertEquals(updates, rate.get(TimeUnit.SECONDS), 0.0);
+    }
+
+    @Test
+    public void testUpdateAndGetBetweenWindows() throws InterruptedException
+    {
+        TestTimeSource time = new TestTimeSource();
+        SlidingTimeRate rate = new SlidingTimeRate(time, 5, 1, TimeUnit.SECONDS);
+        int updates = 100;
+        for (int i = 0; i < updates; i++)
+        {
+            rate.update(1);
+            time.sleep(100, TimeUnit.MILLISECONDS);
+        }
+        Assert.assertEquals(10, rate.get(TimeUnit.SECONDS), 0.0);
+    }
+
+    @Test
+    public void testUpdateAndGetPastWindowSize() throws InterruptedException
+    {
+        TestTimeSource time = new TestTimeSource();
+        SlidingTimeRate rate = new SlidingTimeRate(time, 5, 1, TimeUnit.SECONDS);
+        int updates = 100;
+        for (int i = 0; i < updates; i++)
+        {
+            rate.update(1);
+        }
+
+        time.sleep(6, TimeUnit.SECONDS);
+
+        Assert.assertEquals(0, rate.get(TimeUnit.SECONDS), 0.0);
+    }
+
+    @Test
+    public void testUpdateAndGetToPointInTime() throws InterruptedException
+    {
+        TestTimeSource time = new TestTimeSource();
+        SlidingTimeRate rate = new SlidingTimeRate(time, 5, 1, TimeUnit.SECONDS);
+        int updates = 10;
+        for (int i = 0; i < updates; i++)
+        {
+            rate.update(1);
+            time.sleep(100, TimeUnit.MILLISECONDS);
+        }
+
+        time.sleep(1, TimeUnit.SECONDS);
+
+        Assert.assertEquals(5, rate.get(TimeUnit.SECONDS), 0.0);
+        Assert.assertEquals(10, rate.get(1, TimeUnit.SECONDS), 0.0);
+    }
+
+    @Test
+    public void testDecay() throws InterruptedException
+    {
+        TestTimeSource time = new TestTimeSource();
+        SlidingTimeRate rate = new SlidingTimeRate(time, 5, 1, TimeUnit.SECONDS);
+        int updates = 10;
+        for (int i = 0; i < updates; i++)
+        {
+            rate.update(1);
+            time.sleep(100, TimeUnit.MILLISECONDS);
+        }
+        Assert.assertEquals(10, rate.get(TimeUnit.SECONDS), 0.0);
+
+        time.sleep(1, TimeUnit.SECONDS);
+
+        Assert.assertEquals(5, rate.get(TimeUnit.SECONDS), 0.0);
+
+        time.sleep(2, TimeUnit.SECONDS);
+
+        Assert.assertEquals(2.5, rate.get(TimeUnit.SECONDS), 0.0);
+    }
+
+    @Test
+    public void testPruning() throws InterruptedException
+    {
+        TestTimeSource time = new TestTimeSource();
+        SlidingTimeRate rate = new SlidingTimeRate(time, 5, 1, TimeUnit.SECONDS);
+
+        rate.update(1);
+        Assert.assertEquals(1, rate.size());
+
+        time.sleep(6, TimeUnit.SECONDS);
+
+        rate.prune();
+        Assert.assertEquals(0, rate.size());
+    }
+
+    @Test
+    public void testConcurrentUpdateAndGet() throws InterruptedException
+    {
+        int nproc = Math.min(4, FBUtilities.getAvailableProcessors());
+        final ExecutorService executor = Executors.newFixedThreadPool(nproc);
+        final TestTimeSource time = new TestTimeSource();
+        final SlidingTimeRate rate = new SlidingTimeRate(time, 5, 1, TimeUnit.SECONDS);
+        int updates = 100000;
+        for (int i = 0; i < updates; i++)
+        {
+            executor.submit(() -> {
+                time.sleep(1, TimeUnit.MILLISECONDS);
+                rate.update(1);
+            });
+        }
+
+        executor.shutdown();
+
+        Assert.assertTrue(executor.awaitTermination(1, TimeUnit.MINUTES));
+        Assert.assertEquals(1000, rate.get(TimeUnit.SECONDS), 150.0);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/utils/StreamingHistogramTest.java b/test/unit/org/apache/cassandra/utils/StreamingHistogramTest.java
index b107600..dcb6703 100644
--- a/test/unit/org/apache/cassandra/utils/StreamingHistogramTest.java
+++ b/test/unit/org/apache/cassandra/utils/StreamingHistogramTest.java
@@ -22,6 +22,7 @@
 import java.util.Map;
 
 import org.junit.Test;
+
 import org.apache.cassandra.io.util.DataInputBuffer;
 import org.apache.cassandra.io.util.DataOutputBuffer;
 
@@ -32,13 +33,13 @@
     @Test
     public void testFunction() throws Exception
     {
-        StreamingHistogram.StreamingHistogramBuilder hist = new StreamingHistogram.StreamingHistogramBuilder(5, 5, 1);
+        StreamingHistogram.StreamingHistogramBuilder histogramBuilder = new StreamingHistogram.StreamingHistogramBuilder(5, 0, 1);
         long[] samples = new long[]{23, 19, 10, 16, 36, 2, 9, 32, 30, 45};
 
         // add 7 points to histogram of 5 bins
         for (int i = 0; i < 7; i++)
         {
-            hist.update(samples[i]);
+            histogramBuilder.update(samples[i]);
         }
 
         // should end up (2,1),(9.5,2),(17.5,2),(23,1),(36,1)
@@ -50,21 +51,20 @@
         expected1.put(36.0, 1L);
 
         Iterator<Map.Entry<Double, Long>> expectedItr = expected1.entrySet().iterator();
-        for (Map.Entry<Double, Long> actual : hist.build().getAsMap().entrySet())
+        for (Map.Entry<Number, long[]> actual : histogramBuilder.build().getAsMap().entrySet())
         {
             Map.Entry<Double, Long> entry = expectedItr.next();
-            assertEquals(entry.getKey(), actual.getKey(), 0.01);
-            assertEquals(entry.getValue(), actual.getValue());
+            assertEquals(entry.getKey(), actual.getKey().doubleValue(), 0.01);
+            assertEquals(entry.getValue().longValue(), actual.getValue()[0]);
         }
 
         // merge test
-        StreamingHistogram.StreamingHistogramBuilder hist2 = new StreamingHistogram.StreamingHistogramBuilder(3, 0, 1);
+        StreamingHistogram.StreamingHistogramBuilder hist2 = new StreamingHistogram.StreamingHistogramBuilder(3, 3, 1);
         for (int i = 7; i < samples.length; i++)
         {
             hist2.update(samples[i]);
         }
-        hist.merge(hist2.build());
-        StreamingHistogram histBuilt = hist.build();
+        histogramBuilder.merge(hist2.build());
         // should end up (2,1),(9.5,2),(19.33,3),(32.67,3),(45,1)
         Map<Double, Long> expected2 = new LinkedHashMap<Double, Long>(5);
         expected2.put(2.0, 1L);
@@ -73,34 +73,34 @@
         expected2.put(32.67, 3L);
         expected2.put(45.0, 1L);
         expectedItr = expected2.entrySet().iterator();
-        for (Map.Entry<Double, Long> actual : histBuilt.getAsMap().entrySet())
+        StreamingHistogram hist = histogramBuilder.build();
+        for (Map.Entry<Number, long[]> actual : hist.getAsMap().entrySet())
         {
             Map.Entry<Double, Long> entry = expectedItr.next();
-            assertEquals(entry.getKey(), actual.getKey(), 0.01);
-            assertEquals(entry.getValue(), actual.getValue());
+            assertEquals(entry.getKey(), actual.getKey().doubleValue(), 0.01);
+            assertEquals(entry.getValue().longValue(), actual.getValue()[0]);
         }
 
         // sum test
-        assertEquals(3.28, histBuilt.sum(15), 0.01);
+        assertEquals(3.28, hist.sum(15), 0.01);
         // sum test (b > max(hist))
-        assertEquals(10.0, histBuilt.sum(50), 0.01);
+        assertEquals(10.0, hist.sum(50), 0.01);
     }
 
     @Test
     public void testSerDe() throws Exception
     {
-        StreamingHistogram.StreamingHistogramBuilder histogramBuilder = new StreamingHistogram.StreamingHistogramBuilder(5, 0, 1);
+        StreamingHistogram.StreamingHistogramBuilder hist = new StreamingHistogram.StreamingHistogramBuilder(5, 0, 1);
         long[] samples = new long[]{23, 19, 10, 16, 36, 2, 9};
 
         // add 7 points to histogram of 5 bins
         for (int i = 0; i < samples.length; i++)
         {
-            histogramBuilder.update(samples[i]);
+            hist.update(samples[i]);
         }
-        StreamingHistogram hist = histogramBuilder.build();
 
         DataOutputBuffer out = new DataOutputBuffer();
-        StreamingHistogram.serializer.serialize(hist, out);
+        StreamingHistogram.serializer.serialize(hist.build(), out);
         byte[] bytes = out.toByteArray();
 
         StreamingHistogram deserialized = StreamingHistogram.serializer.deserialize(new DataInputBuffer(bytes));
@@ -114,45 +114,76 @@
         expected1.put(36.0, 1L);
 
         Iterator<Map.Entry<Double, Long>> expectedItr = expected1.entrySet().iterator();
-        for (Map.Entry<Double, Long> actual : deserialized.getAsMap().entrySet())
+        for (Map.Entry<Number, long[]> actual : deserialized.getAsMap().entrySet())
         {
             Map.Entry<Double, Long> entry = expectedItr.next();
-            assertEquals(entry.getKey(), actual.getKey(), 0.01);
-            assertEquals(entry.getValue(), actual.getValue());
+            assertEquals(entry.getKey(), actual.getKey().doubleValue(), 0.01);
+            assertEquals(entry.getValue().longValue(), actual.getValue()[0]);
         }
     }
 
+
+    @Test
+    public void testNumericTypes() throws Exception
+    {
+        StreamingHistogram.StreamingHistogramBuilder hist = new StreamingHistogram.StreamingHistogramBuilder(5, 0, 1);
+
+        hist.update(2);
+        hist.update(2.0);
+        hist.update(2L);
+
+        Map<Number, long[]> asMap = hist.build().getAsMap();
+
+        assertEquals(1, asMap.size());
+        assertEquals(3L, asMap.get(2)[0]);
+
+        //Make sure it's working with Serde
+        DataOutputBuffer out = new DataOutputBuffer();
+        StreamingHistogram.serializer.serialize(hist.build(), out);
+        byte[] bytes = out.toByteArray();
+
+        StreamingHistogram deserialized = StreamingHistogram.serializer.deserialize(new DataInputBuffer(bytes));
+
+        StreamingHistogram.StreamingHistogramBuilder hist2Builder = new StreamingHistogram.StreamingHistogramBuilder(5, 0, 1);
+        hist2Builder.merge(deserialized);
+        hist2Builder.update(2L);
+
+        asMap = hist2Builder.build().getAsMap();
+        assertEquals(1, asMap.size());
+        assertEquals(4L, asMap.get(2)[0]);
+    }
+
     @Test
     public void testOverflow() throws Exception
     {
-        StreamingHistogram.StreamingHistogramBuilder histogramBuilder = new StreamingHistogram.StreamingHistogramBuilder(5, 10, 1);
+        StreamingHistogram.StreamingHistogramBuilder hist = new StreamingHistogram.StreamingHistogramBuilder(5, 10, 1);
         long[] samples = new long[]{23, 19, 10, 16, 36, 2, 9, 32, 30, 45, 31,
-                32, 32, 33, 34, 35, 70, 78, 80, 90, 100,
-                32, 32, 33, 34, 35, 70, 78, 80, 90, 100
-        };
+                                    32, 32, 33, 34, 35, 70, 78, 80, 90, 100,
+                                    32, 32, 33, 34, 35, 70, 78, 80, 90, 100
+                                    };
 
         // Hit the spool cap, force it to make bins
         for (int i = 0; i < samples.length; i++)
         {
-            histogramBuilder.update(samples[i]);
+            hist.update(samples[i]);
         }
-        assertEquals(5, histogramBuilder.build().getAsMap().keySet().size());
+        StreamingHistogram histogram = hist.build();
+        assertEquals(5, histogram.getAsMap().keySet().size());
 
     }
 
     @Test
     public void testRounding() throws Exception
     {
-        StreamingHistogram.StreamingHistogramBuilder histogramBuilder = new StreamingHistogram.StreamingHistogramBuilder(5, 10, 60);
+        StreamingHistogram.StreamingHistogramBuilder hist = new StreamingHistogram.StreamingHistogramBuilder(5, 10, 60);
         long[] samples = new long[] { 59, 60, 119, 180, 181, 300 }; // 60, 60, 120, 180, 240, 300
         for (int i = 0 ; i < samples.length ; i++)
-            histogramBuilder.update(samples[i]);
+            hist.update(samples[i]);
 
-        StreamingHistogram hist = histogramBuilder.build();
-        assertEquals(hist.getAsMap().keySet().size(), (int) 5);
-        assertEquals((long) hist.getAsMap().get(Double.valueOf(60)), (long) 2L);
-        assertEquals((long) hist.getAsMap().get(Double.valueOf(120)), (long) 1L);
+        StreamingHistogram histogram = hist.build();
+        assertEquals(histogram.getAsMap().keySet().size(), 5);
+        assertEquals(histogram.getAsMap().get(60)[0], 2);
+        assertEquals(histogram.getAsMap().get(120)[0], 1);
 
     }
-
 }
diff --git a/test/unit/org/apache/cassandra/utils/TestTimeSource.java b/test/unit/org/apache/cassandra/utils/TestTimeSource.java
new file mode 100644
index 0000000..4ecd086
--- /dev/null
+++ b/test/unit/org/apache/cassandra/utils/TestTimeSource.java
@@ -0,0 +1,72 @@
+/*
+ * 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.utils;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class TestTimeSource implements TimeSource
+{
+    private final AtomicLong timeInMillis = new AtomicLong(System.currentTimeMillis());
+
+    @Override
+    public long currentTimeMillis()
+    {
+        return timeInMillis.get();
+    }
+
+    @Override
+    public long nanoTime()
+    {
+        return timeInMillis.get() * 1_000_000;
+    }
+
+    @Override
+    public TimeSource sleep(long sleepFor, TimeUnit unit)
+    {
+        long current = timeInMillis.get();
+        long sleepInMillis = TimeUnit.MILLISECONDS.convert(sleepFor, unit);
+        boolean elapsed;
+        do
+        {
+            long newTime = current + sleepInMillis;
+            elapsed = timeInMillis.compareAndSet(current, newTime);
+            if (!elapsed)
+            {
+                long updated = timeInMillis.get();
+                if (updated - current >= sleepInMillis)
+                {
+                    elapsed = true;
+                }
+                else
+                {
+                    sleepInMillis -= updated - current;
+                    current = updated;
+                }
+            }
+        }
+        while (!elapsed);
+        return this;
+    }
+
+    @Override
+    public TimeSource sleepUninterruptibly(long sleepFor, TimeUnit unit)
+    {
+        return sleep(sleepFor, unit);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/utils/TopKSamplerTest.java b/test/unit/org/apache/cassandra/utils/TopKSamplerTest.java
index bb6e3a8..d1fdf47 100644
--- a/test/unit/org/apache/cassandra/utils/TopKSamplerTest.java
+++ b/test/unit/org/apache/cassandra/utils/TopKSamplerTest.java
@@ -29,16 +29,25 @@
 
 import com.google.common.collect.Maps;
 import com.google.common.util.concurrent.Uninterruptibles;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import com.clearspring.analytics.hash.MurmurHash;
 import com.clearspring.analytics.stream.Counter;
 import junit.framework.Assert;
+import org.apache.cassandra.concurrent.NamedThreadFactory;
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.utils.TopKSampler.SamplerResult;
 
 public class TopKSamplerTest
 {
 
+    @BeforeClass
+    public static void beforeClass()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Test
     public void testSamplerSingleInsertionsEqualMulti() throws TimeoutException
     {
@@ -82,7 +91,7 @@
         final CountDownLatch latch = new CountDownLatch(1);
         final TopKSampler<String> sampler = new TopKSampler<String>();
 
-        new Thread(new Runnable()
+        NamedThreadFactory.createThread(new Runnable()
         {
             public void run()
             {
@@ -99,7 +108,7 @@
             }
 
         }
-        ,"inserter").start();
+        , "inserter").start();
         try
         {
             // start/stop in fast iterations
diff --git a/test/unit/org/apache/cassandra/utils/UUIDTest.java b/test/unit/org/apache/cassandra/utils/UUIDTest.java
index 5043eab..9fa4957 100644
--- a/test/unit/org/apache/cassandra/utils/UUIDTest.java
+++ b/test/unit/org/apache/cassandra/utils/UUIDTest.java
@@ -22,11 +22,19 @@
 
 
 import java.nio.ByteBuffer;
+import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.junit.Test;
 
+import com.google.common.collect.Sets;
+
 import org.apache.cassandra.db.marshal.TimeUUIDType;
+import org.cliffc.high_scale_lib.NonBlockingHashMap;
 
 
 public class UUIDTest
@@ -47,7 +55,6 @@
         assert one.timestamp() < two.timestamp();
     }
 
-
     @Test
     public void testDecomposeAndRaw()
     {
@@ -58,6 +65,15 @@
     }
 
     @Test
+    public void testToFromByteBuffer()
+    {
+        UUID a = UUIDGen.getTimeUUID();
+        ByteBuffer bb = UUIDGen.toByteBuffer(a);
+        UUID b = UUIDGen.getUUID(bb);
+        assert a.equals(b);
+    }
+
+    @Test
     public void testTimeUUIDType()
     {
         TimeUUIDType comp = TimeUUIDType.instance;
@@ -79,4 +95,53 @@
         // I'll be damn is the uuid timestamp is more than 10ms after now
         assert now <= tstamp && now >= tstamp - 10 : "now = " + now + ", timestamp = " + tstamp;
     }
+
+    /*
+     * Don't ignore spurious failures of this test since it is testing concurrent access
+     * and might not fail reliably.
+     */
+    @Test
+    public void verifyConcurrentUUIDGeneration() throws Throwable
+    {
+        long iterations = 250000;
+        int threads = 4;
+        ExecutorService es = Executors.newFixedThreadPool(threads);
+        try
+        {
+            AtomicBoolean failedOrdering = new AtomicBoolean(false);
+            AtomicBoolean failedDuplicate = new AtomicBoolean(false);
+            Set<UUID> generated = Sets.newSetFromMap(new NonBlockingHashMap<>());
+            Runnable task = () -> {
+                long lastTimestamp = 0;
+                long newTimestamp = 0;
+
+                for (long i = 0; i < iterations; i++)
+                {
+                    UUID uuid = UUIDGen.getTimeUUID();
+                    newTimestamp = uuid.timestamp();
+
+                    if (lastTimestamp >= newTimestamp)
+                        failedOrdering.set(true);
+                    if (!generated.add(uuid))
+                        failedDuplicate.set(true);
+
+                    lastTimestamp = newTimestamp;
+                }
+            };
+
+            for (int i = 0; i < threads; i++)
+            {
+                es.execute(task);
+            }
+            es.shutdown();
+            es.awaitTermination(10, TimeUnit.MINUTES);
+
+            assert !failedOrdering.get();
+            assert !failedDuplicate.get();
+        }
+        finally
+        {
+            es.shutdown();
+        }
+    }
 }
diff --git a/test/unit/org/apache/cassandra/utils/btree/BTreeRemovalTest.java b/test/unit/org/apache/cassandra/utils/btree/BTreeRemovalTest.java
new file mode 100644
index 0000000..a9cf383
--- /dev/null
+++ b/test/unit/org/apache/cassandra/utils/btree/BTreeRemovalTest.java
@@ -0,0 +1,385 @@
+/*
+ * 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.utils.btree;
+
+import static org.apache.cassandra.utils.btree.BTreeRemoval.remove;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Comparator;
+import java.util.Random;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.junit.Test;
+
+import com.google.common.collect.Iterables;
+
+public class BTreeRemovalTest
+{
+    static
+    {
+        System.setProperty("cassandra.btree.fanfactor", "8");
+    }
+
+    private static final Comparator<Integer> CMP = new Comparator<Integer>()
+    {
+        public int compare(Integer o1, Integer o2)
+        {
+            return Integer.compare(o1, o2);
+        }
+    };
+
+    private static Object[] copy(final Object[] btree)
+    {
+        final Object[] result = new Object[btree.length];
+        System.arraycopy(btree, 0, result, 0, btree.length);
+        if (!BTree.isLeaf(btree))
+        {
+            for (int i = BTree.getChildStart(btree); i < BTree.getChildEnd(btree); ++i)
+                result[i] = copy((Object[]) btree[i]);
+            final int[] sizeMap = BTree.getSizeMap(btree);
+            final int[] resultSizeMap = new int[sizeMap.length];
+            System.arraycopy(sizeMap, 0, resultSizeMap, 0, sizeMap.length);
+            result[result.length - 1] = resultSizeMap;
+        }
+        return result;
+    }
+
+    private static Object[] assertRemove(final Object[] btree, final int key)
+    {
+        final Object[] btreeBeforeRemoval = copy(btree);
+        final Object[] result = remove(btree, CMP, key);
+        assertBTree(btreeBeforeRemoval, btree);
+        assertTrue(BTree.isWellFormed(result, CMP));
+        assertEquals(BTree.size(btree) - 1, BTree.size(result));
+        assertNull(BTree.find(result, CMP, key));
+
+        for (Integer k : BTree.<Integer>iterable(btree))
+            if (k != key)
+                assertNotNull(BTree.find(result, CMP, k));
+
+        return result;
+    }
+
+    private static void assertBTree(final Object[] expected, final Object[] result)
+    {
+        assertEquals(BTree.isEmpty(expected), BTree.isEmpty(result));
+        assertEquals(BTree.isLeaf(expected), BTree.isLeaf(result));
+        assertEquals(expected.length, result.length);
+        if (BTree.isLeaf(expected))
+        {
+            assertArrayEquals(expected, result);
+        }
+        else
+        {
+            for (int i = 0; i < BTree.getBranchKeyEnd(expected); ++i)
+                assertEquals(expected[i], result[i]);
+            for (int i = BTree.getChildStart(expected); i < BTree.getChildEnd(expected); ++i)
+                assertBTree((Object[]) expected[i], (Object[]) result[i]);
+            assertArrayEquals(BTree.getSizeMap(expected), BTree.getSizeMap(result));
+        }
+    }
+
+    private static Object[] generateLeaf(int from, int size)
+    {
+        final Object[] result = new Object[(size & 1) == 1 ? size : size + 1];
+        for (int i = 0; i < size; ++i)
+            result[i] = from + i;
+        return result;
+    }
+
+    private static Object[] generateBranch(int[] keys, Object[][] children)
+    {
+        assert keys.length > 0;
+        assert children.length > 1;
+        assert children.length == keys.length + 1;
+        final Object[] result = new Object[keys.length + children.length + 1];
+        for (int i = 0; i < keys.length; ++i)
+            result[i] = keys[i];
+        for (int i = 0; i < children.length; ++i)
+            result[keys.length + i] = children[i];
+        final int[] sizeMap = new int[children.length];
+        sizeMap[0] = BTree.size(children[0]);
+        for (int i = 1; i < children.length; ++i)
+            sizeMap[i] = sizeMap[i - 1] + BTree.size(children[i]) + 1;
+        result[result.length - 1] = sizeMap;
+        return result;
+    }
+
+    private static Object[] generateSampleTwoLevelsTree(final int[] leafSizes)
+    {
+        assert leafSizes.length > 1;
+        final Object[][] leaves = new Object[leafSizes.length][];
+        for (int i = 0; i < leaves.length; ++i)
+            leaves[i] = generateLeaf(10 * i + 1, leafSizes[i]);
+        final int[] keys = new int[leafSizes.length - 1];
+        for (int i = 0; i < keys.length; ++i)
+            keys[i] = 10 * (i + 1);
+        final Object[] btree = generateBranch(keys, leaves);
+        assertTrue(BTree.isWellFormed(btree, CMP));
+        return btree;
+    }
+
+    private static Object[] generateSampleThreeLevelsTree(final int[] middleNodeSizes)
+    {
+        assert middleNodeSizes.length > 1;
+        final Object[][] middleNodes = new Object[middleNodeSizes.length][];
+        for (int i = 0; i < middleNodes.length; ++i)
+        {
+            final Object[][] leaves = new Object[middleNodeSizes[i]][];
+            for (int j = 0; j < middleNodeSizes[i]; ++j)
+                leaves[j] = generateLeaf(100 * i + 10 * j + 1, 4);
+            final int[] keys = new int[middleNodeSizes[i] - 1];
+            for (int j = 0; j < keys.length; ++j)
+                keys[j] = 100 * i + 10 * (j + 1);
+            middleNodes[i] = generateBranch(keys, leaves);
+        }
+        final int[] keys = new int[middleNodeSizes.length - 1];
+        for (int i = 0; i < keys.length; ++i)
+            keys[i] = 100 * (i + 1);
+        final Object[] btree = generateBranch(keys, middleNodes);
+        assertTrue(BTree.isWellFormed(btree, CMP));
+        return btree;
+    }
+
+    @Test
+    public void testRemoveFromEmpty()
+    {
+        assertBTree(BTree.empty(), remove(BTree.empty(), CMP, 1));
+    }
+
+    @Test
+    public void testRemoveNonexistingElement()
+    {
+        final Object[] btree = new Object[] {1, 2, 3, 4, null};
+        assertBTree(btree, remove(btree, CMP, 5));
+    }
+
+    @Test
+    public void testRemoveLastElement()
+    {
+        final Object[] btree = new Object[] {1};
+        assertBTree(BTree.empty(), remove(btree, CMP, 1));
+    }
+
+    @Test
+    public void testRemoveFromRootWhichIsALeaf()
+    {
+        for (int size = 1; size < 9; ++size)
+        {
+            final Object[] btree = new Object[(size & 1) == 1 ? size : size + 1];
+            for (int i = 0; i < size; ++i)
+                btree[i] = i + 1;
+            for (int i = 0; i < size; ++i)
+            {
+                final Object[] result = remove(btree, CMP, i + 1);
+                assertTrue("size " + size, BTree.isWellFormed(result, CMP));
+                for (int j = 0; j < i; ++j)
+                    assertEquals("size " + size + "elem " + j, btree[j], result[j]);
+                for (int j = i; j < size - 1; ++j)
+                    assertEquals("size " + size + "elem " + j, btree[j + 1], result[j]);
+                for (int j = size - 1; j < result.length; ++j)
+                    assertNull("size " + size + "elem " + j, result[j]);
+            }
+
+            {
+                final Object[] result = remove(btree, CMP, 0);
+                assertTrue("size " + size, BTree.isWellFormed(result, CMP));
+                assertBTree(btree, result);
+            }
+
+            {
+                final Object[] result = remove(btree, CMP, size + 1);
+                assertTrue("size " + size, BTree.isWellFormed(result, CMP));
+                assertBTree(btree, result);
+            }
+        }
+    }
+
+    @Test
+    public void testRemoveFromNonMinimalLeaf()
+    {
+        for (int size = 5; size < 9; ++size)
+        {
+            final Object[] btree = generateSampleTwoLevelsTree(new int[] {size, 4, 4, 4, 4});
+
+            for (int i = 1; i < size + 1; ++i)
+                assertRemove(btree, i);
+        }
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafRotateLeft()
+    {
+        final Object[] btree = generateSampleTwoLevelsTree(new int[] {4, 5, 5, 5, 5});
+
+        for (int i = 11; i < 15; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafRotateRight1()
+    {
+        final Object[] btree = generateSampleTwoLevelsTree(new int[] {4, 5, 5, 5, 5});
+
+        for (int i = 1; i < 5; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafRotateRight2()
+    {
+        final Object[] btree = generateSampleTwoLevelsTree(new int[] {4, 4, 5, 5, 5});
+
+        for (int i = 11; i < 15; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafMergeWithLeft1()
+    {
+        final Object[] btree = generateSampleTwoLevelsTree(new int[] {4, 4, 4, 4, 4});
+
+        for (int i = 11; i < 15; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafMergeWithLeft2()
+    {
+        final Object[] btree = generateSampleTwoLevelsTree(new int[] {4, 4, 4, 4, 4});
+
+        for (int i = 41; i < 45; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafMergeWithRight()
+    {
+        final Object[] btree = generateSampleTwoLevelsTree(new int[] {4, 4, 4, 4, 4});
+
+        for (int i = 1; i < 5; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafWhenSingleKeyRootMergeWithLeft()
+    {
+        final Object[] btree = generateSampleTwoLevelsTree(new int[] {4, 4});
+
+        for (int i = 1; i < 5; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafWhenSingleKeyRootMergeWithRight()
+    {
+        final Object[] btree = generateSampleTwoLevelsTree(new int[] {4, 4});
+
+        for (int i = 11; i < 15; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafWithBranchLeftRotation()
+    {
+        final Object[] btree = generateSampleThreeLevelsTree(new int[] {6, 5, 5, 5, 5});
+        for (int i = 101; i < 105; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafWithBranchRightRotation1()
+    {
+        final Object[] btree = generateSampleThreeLevelsTree(new int[] {5, 6, 5, 5, 5});
+        for (int i = 1; i < 5; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafWithBranchRightRotation2()
+    {
+        final Object[] btree = generateSampleThreeLevelsTree(new int[] {5, 5, 6, 5, 5});
+        for (int i = 101; i < 105; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafWithBranchMergeWithLeft1()
+    {
+        final Object[] btree = generateSampleThreeLevelsTree(new int[] {5, 5, 5, 5, 5});
+        for (int i = 101; i < 105; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafWithBranchMergeWithLeft2()
+    {
+        final Object[] btree = generateSampleThreeLevelsTree(new int[] {5, 5, 5, 5, 5});
+        for (int i = 401; i < 405; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMinimalLeafWithBranchMergeWithRight()
+    {
+        final Object[] btree = generateSampleThreeLevelsTree(new int[] {5, 5, 5, 5, 5});
+        for (int i = 1; i < 5; ++i)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromMiddleBranch()
+    {
+        final Object[] btree = generateSampleThreeLevelsTree(new int[] {5, 5, 5, 5, 5});
+        for (int i = 10; i < 50; i += 10)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void testRemoveFromRootBranch()
+    {
+        final Object[] btree = generateSampleThreeLevelsTree(new int[] {5, 5, 5, 5, 5});
+        for (int i = 100; i < 500; i += 100)
+            assertRemove(btree, i);
+    }
+
+    @Test
+    public void randomizedTest()
+    {
+        Random rand = new Random(2);
+        SortedSet<Integer> data = new TreeSet<>();
+        for (int i = 0; i < 1000; ++i)
+            data.add(rand.nextInt());
+        Object[] btree = BTree.build(data, UpdateFunction.<Integer>noOp());
+
+        assertTrue(BTree.isWellFormed(btree, CMP));
+        assertTrue(Iterables.elementsEqual(data, BTree.iterable(btree)));
+        while (btree != BTree.empty())
+        {
+            int idx = rand.nextInt(BTree.size(btree));
+            Integer val = BTree.findByIndex(btree, idx);
+            assertTrue(data.remove(val));
+            btree = assertRemove(btree, val);
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/utils/concurrent/AbstractTransactionalTest.java b/test/unit/org/apache/cassandra/utils/concurrent/AbstractTransactionalTest.java
index bb2b9b0..5a20d67 100644
--- a/test/unit/org/apache/cassandra/utils/concurrent/AbstractTransactionalTest.java
+++ b/test/unit/org/apache/cassandra/utils/concurrent/AbstractTransactionalTest.java
@@ -31,7 +31,7 @@
     @BeforeClass
     public static void setupDD()
     {
-        DatabaseDescriptor.setDaemonInitialized();
+        DatabaseDescriptor.daemonInitialization();
     }
 
     protected abstract TestableTransaction newTest() throws Exception;
diff --git a/test/unit/org/apache/cassandra/utils/memory/BufferPoolTest.java b/test/unit/org/apache/cassandra/utils/memory/BufferPoolTest.java
index 208cd32..74889a1 100644
--- a/test/unit/org/apache/cassandra/utils/memory/BufferPoolTest.java
+++ b/test/unit/org/apache/cassandra/utils/memory/BufferPoolTest.java
@@ -25,9 +25,11 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Ignore;
 import org.junit.Test;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.io.compress.BufferType;
 import org.apache.cassandra.io.util.RandomAccessReader;
 
@@ -35,6 +37,12 @@
 
 public class BufferPoolTest
 {
+    @BeforeClass
+    public static void setupDD()
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
     @Before
     public void setUp()
     {
diff --git a/tools/bin/compaction-stress b/tools/bin/compaction-stress
new file mode 100755
index 0000000..f169f2f
--- /dev/null
+++ b/tools/bin/compaction-stress
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+# 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.
+
+if [ "x$CASSANDRA_INCLUDE" = "x" ]; then
+    # Locations (in order) to use when searching for an include file.
+    for include in "`dirname "$0"`/cassandra.in.sh" \
+                   "$HOME/.cassandra.in.sh" \
+                   /usr/share/cassandra/cassandra.in.sh \
+                   /usr/local/share/cassandra/cassandra.in.sh \
+                   /opt/cassandra/cassandra.in.sh; do
+        if [ -r "$include" ]; then
+            . "$include"
+            break
+        fi
+    done
+elif [ -r "$CASSANDRA_INCLUDE" ]; then
+    . "$CASSANDRA_INCLUDE"
+fi
+
+# Use JAVA_HOME if set, otherwise look for java in PATH
+if [ -x "$JAVA_HOME/bin/java" ]; then
+    JAVA="$JAVA_HOME/bin/java"
+else
+    JAVA="`which java`"
+fi
+
+if [ "x$JAVA" = "x" ]; then
+    echo "Java executable not found (hint: set JAVA_HOME)" >&2
+    exit 1
+fi
+
+if [ -z "$CLASSPATH" ]; then
+    echo "You must set the CLASSPATH var" >&2
+    exit 1
+fi
+
+"$JAVA" -server -ea -cp "$CLASSPATH" $JVM_OPTS \
+        -Dcassandra.storagedir="$cassandra_storagedir" \
+        -Dlogback.configurationFile=logback-tools.xml \
+         org.apache.cassandra.stress.CompactionStress $@
+
+# vi:ai sw=4 ts=4 tw=0 et
diff --git a/tools/cqlstress-counter-example.yaml b/tools/cqlstress-counter-example.yaml
index f8f70ea..2430e50 100644
--- a/tools/cqlstress-counter-example.yaml
+++ b/tools/cqlstress-counter-example.yaml
@@ -48,10 +48,10 @@
 #      GAUSSIAN(min..max,mean,stdev)        A gaussian/normal distribution, with explicitly defined mean and stdev
 #      UNIFORM(min..max)                    A uniform distribution over the range [min, max]
 #      FIXED(val)                           A fixed distribution, always returning the same value
+#      SEQ(min..max)                        A fixed sequence, returning values in the range min to max sequentially (starting based on seed), wrapping if necessary.
 #      Aliases: extr, gauss, normal, norm, weibull
 #
 #      If preceded by ~, the distribution is inverted
-#
 # Defaults for all columns are size: uniform(4..8), population: uniform(1..100B), cluster: fixed(1)
 #
 
diff --git a/tools/cqlstress-example.yaml b/tools/cqlstress-example.yaml
index 835a4cb..cde345a 100644
--- a/tools/cqlstress-example.yaml
+++ b/tools/cqlstress-example.yaml
@@ -59,6 +59,7 @@
 #      GAUSSIAN(min..max,mean,stdev)        A gaussian/normal distribution, with explicitly defined mean and stdev
 #      UNIFORM(min..max)                    A uniform distribution over the range [min, max]
 #      FIXED(val)                           A fixed distribution, always returning the same value
+#      SEQ(min..max)                        A fixed sequence, returning values in the range min to max sequentially (starting based on seed), wrapping if necessary.
 #      Aliases: extr, gauss, normal, norm, weibull
 #
 #      If preceded by ~, the distribution is inverted
diff --git a/tools/cqlstress-insanity-example.yaml b/tools/cqlstress-insanity-example.yaml
index a286625..5eb4fec 100644
--- a/tools/cqlstress-insanity-example.yaml
+++ b/tools/cqlstress-insanity-example.yaml
@@ -58,6 +58,7 @@
 #      GAUSSIAN(min..max,mean,stdev)        A gaussian/normal distribution, with explicitly defined mean and stdev
 #      UNIFORM(min..max)                    A uniform distribution over the range [min, max]
 #      FIXED(val)                           A fixed distribution, always returning the same value
+#      SEQ(min..max)                        A fixed sequence, returning values in the range min to max sequentially (starting based on seed), wrapping if necessary.
 #      Aliases: extr, gauss, normal, norm, weibull
 #
 #      If preceded by ~, the distribution is inverted
diff --git a/tools/stress/README.txt b/tools/stress/README.txt
index e560c08..aa89dab 100644
--- a/tools/stress/README.txt
+++ b/tools/stress/README.txt
@@ -72,6 +72,8 @@
         The port to connect to cassandra nodes on
     -sendto:
         Specify a stress server to send this command to
+    -graph:
+        Graph recorded metrics
     -tokenrange:
         Token range settings
 
diff --git a/tools/stress/src/org/apache/cassandra/io/sstable/StressCQLSSTableWriter.java b/tools/stress/src/org/apache/cassandra/io/sstable/StressCQLSSTableWriter.java
new file mode 100644
index 0000000..5a285e1
--- /dev/null
+++ b/tools/stress/src/org/apache/cassandra/io/sstable/StressCQLSSTableWriter.java
@@ -0,0 +1,676 @@
+/*
+ * 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.io.sstable;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import com.datastax.driver.core.ProtocolVersion;
+import com.datastax.driver.core.TypeCodec;
+import org.antlr.runtime.RecognitionException;
+import org.apache.cassandra.config.CFMetaData;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.cql3.CQLFragmentParser;
+import org.apache.cassandra.cql3.ColumnSpecification;
+import org.apache.cassandra.cql3.CqlParser;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.UpdateParameters;
+import org.apache.cassandra.cql3.functions.UDHelper;
+import org.apache.cassandra.cql3.statements.CreateTableStatement;
+import org.apache.cassandra.cql3.statements.CreateTypeStatement;
+import org.apache.cassandra.cql3.statements.ParsedStatement;
+import org.apache.cassandra.cql3.statements.UpdateStatement;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.db.marshal.UserType;
+import org.apache.cassandra.db.partitions.Partition;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.exceptions.RequestValidationException;
+import org.apache.cassandra.exceptions.SyntaxException;
+import org.apache.cassandra.io.sstable.format.SSTableFormat;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.Types;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.Pair;
+
+/**
+ * Utility to write SSTables.
+ * <p>
+ * Typical usage looks like:
+ * <pre>
+ *   String type = CREATE TYPE myKs.myType (a int, b int)";
+ *   String schema = "CREATE TABLE myKs.myTable ("
+ *                 + "  k int PRIMARY KEY,"
+ *                 + "  v1 text,"
+ *                 + "  v2 int,"
+ *                 + "  v3 myType,"
+ *                 + ")";
+ *   String insert = "INSERT INTO myKs.myTable (k, v1, v2, v3) VALUES (?, ?, ?, ?)";
+ *
+ *   // Creates a new writer. You need to provide at least the directory where to write the created sstable,
+ *   // the schema for the sstable to write and a (prepared) insert statement to use. If you do not use the
+ *   // default partitioner (Murmur3Partitioner), you will also need to provide the partitioner in use, see
+ *   // StressCQLSSTableWriter.Builder for more details on the available options.
+ *   StressCQLSSTableWriter writer = StressCQLSSTableWriter.builder()
+ *                                             .inDirectory("path/to/directory")
+ *                                             .withType(type)
+ *                                             .forTable(schema)
+ *                                             .using(insert).build();
+ *
+ *   UserType myType = writer.getUDType("myType");
+ *   // Adds a nember of rows to the resulting sstable
+ *   writer.addRow(0, "test1", 24, myType.newValue().setInt("a", 10).setInt("b", 20));
+ *   writer.addRow(1, "test2", null, null);
+ *   writer.addRow(2, "test3", 42, myType.newValue().setInt("a", 30).setInt("b", 40));
+ *
+ *   // Close the writer, finalizing the sstable
+ *   writer.close();
+ * </pre>
+ *
+ * Please note that {@code StressCQLSSTableWriter} is <b>not</b> thread-safe (multiple threads cannot access the
+ * same instance). It is however safe to use multiple instances in parallel (even if those instance write
+ * sstables for the same table).
+ */
+public class StressCQLSSTableWriter implements Closeable
+{
+    public static final ByteBuffer UNSET_VALUE = ByteBufferUtil.UNSET_BYTE_BUFFER;
+
+    static
+    {
+        DatabaseDescriptor.clientInitialization(false);
+        // Partitioner is not set in client mode.
+        if (DatabaseDescriptor.getPartitioner() == null)
+            DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
+    }
+
+    private final AbstractSSTableSimpleWriter writer;
+    private final UpdateStatement insert;
+    private final List<ColumnSpecification> boundNames;
+    private final List<TypeCodec> typeCodecs;
+    private final ColumnFamilyStore cfs;
+
+    private StressCQLSSTableWriter(ColumnFamilyStore cfs, AbstractSSTableSimpleWriter writer, UpdateStatement insert, List<ColumnSpecification> boundNames)
+    {
+        this.cfs = cfs;
+        this.writer = writer;
+        this.insert = insert;
+        this.boundNames = boundNames;
+        this.typeCodecs = boundNames.stream().map(bn ->  UDHelper.codecFor(UDHelper.driverType(bn.type)))
+                                             .collect(Collectors.toList());
+    }
+
+    /**
+     * Returns a new builder for a StressCQLSSTableWriter.
+     *
+     * @return the new builder.
+     */
+    public static Builder builder()
+    {
+        return new Builder();
+    }
+
+    /**
+     * Adds a new row to the writer.
+     * <p>
+     * This is a shortcut for {@code addRow(Arrays.asList(values))}.
+     *
+     * @param values the row values (corresponding to the bind variables of the
+     * insertion statement used when creating by this writer).
+     * @return this writer.
+     */
+    public StressCQLSSTableWriter addRow(Object... values)
+    throws InvalidRequestException, IOException
+    {
+        return addRow(Arrays.asList(values));
+    }
+
+    /**
+     * Adds a new row to the writer.
+     * <p>
+     * Each provided value type should correspond to the types of the CQL column
+     * the value is for. The correspondance between java type and CQL type is the
+     * same one than the one documented at
+     * www.datastax.com/drivers/java/2.0/apidocs/com/datastax/driver/core/DataType.Name.html#asJavaClass().
+     * <p>
+     * If you prefer providing the values directly as binary, use
+     * {@link #rawAddRow} instead.
+     *
+     * @param values the row values (corresponding to the bind variables of the
+     * insertion statement used when creating by this writer).
+     * @return this writer.
+     */
+    public StressCQLSSTableWriter addRow(List<Object> values)
+    throws InvalidRequestException, IOException
+    {
+        int size = Math.min(values.size(), boundNames.size());
+        List<ByteBuffer> rawValues = new ArrayList<>(size);
+
+        for (int i = 0; i < size; i++)
+        {
+            Object value = values.get(i);
+            rawValues.add(serialize(value, typeCodecs.get(i)));
+        }
+
+        return rawAddRow(rawValues);
+    }
+
+    /**
+     * Adds a new row to the writer.
+     * <p>
+     * This is equivalent to the other addRow methods, but takes a map whose
+     * keys are the names of the columns to add instead of taking a list of the
+     * values in the order of the insert statement used during construction of
+     * this write.
+     * <p>
+     * Please note that the column names in the map keys must be in lowercase unless
+     * the declared column name is a
+     * <a href="http://cassandra.apache.org/doc/cql3/CQL.html#identifiers">case-sensitive quoted identifier</a>
+     * (in which case the map key must use the exact case of the column).
+     *
+     * @param values a map of colum name to column values representing the new
+     * row to add. Note that if a column is not part of the map, it's value will
+     * be {@code null}. If the map contains keys that does not correspond to one
+     * of the column of the insert statement used when creating this writer, the
+     * the corresponding value is ignored.
+     * @return this writer.
+     */
+    public StressCQLSSTableWriter addRow(Map<String, Object> values)
+    throws InvalidRequestException, IOException
+    {
+        int size = boundNames.size();
+        List<ByteBuffer> rawValues = new ArrayList<>(size);
+        for (int i = 0; i < size; i++)
+        {
+            ColumnSpecification spec = boundNames.get(i);
+            Object value = values.get(spec.name.toString());
+            rawValues.add(serialize(value, typeCodecs.get(i)));
+        }
+        return rawAddRow(rawValues);
+    }
+
+    /**
+     * Adds a new row to the writer given already serialized values.
+     *
+     * @param values the row values (corresponding to the bind variables of the
+     * insertion statement used when creating by this writer) as binary.
+     * @return this writer.
+     */
+    public StressCQLSSTableWriter rawAddRow(ByteBuffer... values)
+    throws InvalidRequestException, IOException
+    {
+        return rawAddRow(Arrays.asList(values));
+    }
+
+    /**
+     * Adds a new row to the writer given already serialized values.
+     * <p>
+     * This is a shortcut for {@code rawAddRow(Arrays.asList(values))}.
+     *
+     * @param values the row values (corresponding to the bind variables of the
+     * insertion statement used when creating by this writer) as binary.
+     * @return this writer.
+     */
+    public StressCQLSSTableWriter rawAddRow(List<ByteBuffer> values)
+    throws InvalidRequestException, IOException
+    {
+        if (values.size() != boundNames.size())
+            throw new InvalidRequestException(String.format("Invalid number of arguments, expecting %d values but got %d", boundNames.size(), values.size()));
+
+        QueryOptions options = QueryOptions.forInternalCalls(null, values);
+        List<ByteBuffer> keys = insert.buildPartitionKeyNames(options);
+        SortedSet<Clustering> clusterings = insert.createClustering(options);
+
+        long now = System.currentTimeMillis() * 1000;
+        // Note that we asks indexes to not validate values (the last 'false' arg below) because that triggers a 'Keyspace.open'
+        // and that forces a lot of initialization that we don't want.
+        UpdateParameters params = new UpdateParameters(insert.cfm,
+                                                       insert.updatedColumns(),
+                                                       options,
+                                                       insert.getTimestamp(now, options),
+                                                       insert.getTimeToLive(options),
+                                                       Collections.<DecoratedKey, Partition>emptyMap());
+
+        try
+        {
+            for (ByteBuffer key : keys)
+            {
+                for (Clustering clustering : clusterings)
+                    insert.addUpdateForKey(writer.getUpdateFor(key), clustering, params);
+            }
+            return this;
+        }
+        catch (SSTableSimpleUnsortedWriter.SyncException e)
+        {
+            // If we use a BufferedWriter and had a problem writing to disk, the IOException has been
+            // wrapped in a SyncException (see BufferedWriter below). We want to extract that IOE.
+            throw (IOException)e.getCause();
+        }
+    }
+
+    /**
+     * Adds a new row to the writer given already serialized values.
+     * <p>
+     * This is equivalent to the other rawAddRow methods, but takes a map whose
+     * keys are the names of the columns to add instead of taking a list of the
+     * values in the order of the insert statement used during construction of
+     * this write.
+     *
+     * @param values a map of colum name to column values representing the new
+     * row to add. Note that if a column is not part of the map, it's value will
+     * be {@code null}. If the map contains keys that does not correspond to one
+     * of the column of the insert statement used when creating this writer, the
+     * the corresponding value is ignored.
+     * @return this writer.
+     */
+    public StressCQLSSTableWriter rawAddRow(Map<String, ByteBuffer> values)
+    throws InvalidRequestException, IOException
+    {
+        int size = Math.min(values.size(), boundNames.size());
+        List<ByteBuffer> rawValues = new ArrayList<>(size);
+        for (int i = 0; i < size; i++) 
+        {
+            ColumnSpecification spec = boundNames.get(i);
+            rawValues.add(values.get(spec.name.toString()));
+        }
+        return rawAddRow(rawValues);
+    }
+
+    /**
+     * Returns the User Defined type, used in this SSTable Writer, that can
+     * be used to create UDTValue instances.
+     *
+     * @param dataType name of the User Defined type
+     * @return user defined type
+     */
+    public com.datastax.driver.core.UserType getUDType(String dataType)
+    {
+        KeyspaceMetadata ksm = Schema.instance.getKSMetaData(insert.keyspace());
+        UserType userType = ksm.types.getNullable(ByteBufferUtil.bytes(dataType));
+        return (com.datastax.driver.core.UserType) UDHelper.driverType(userType);
+    }
+
+    /**
+     * Close this writer.
+     * <p>
+     * This method should be called, otherwise the produced sstables are not
+     * guaranteed to be complete (and won't be in practice).
+     */
+    public void close() throws IOException
+    {
+        writer.close();
+    }
+
+    private ByteBuffer serialize(Object value, TypeCodec codec)
+    {
+        if (value == null || value == UNSET_VALUE)
+            return (ByteBuffer) value;
+
+        return codec.serialize(value, ProtocolVersion.NEWEST_SUPPORTED);
+    }
+    /**
+     * The writer loads data in directories corresponding to how they laid out on the server.
+     * <p>
+     * {keyspace}/{table-cfid}/
+     *
+     * This method can be used to fetch the innermost directory with the sstable components
+     * @return The directory containing the sstable components
+     */
+    public File getInnermostDirectory()
+    {
+        return cfs.getDirectories().getDirectoryForNewSSTables();
+    }
+
+    /**
+     * A Builder for a StressCQLSSTableWriter object.
+     */
+    public static class Builder
+    {
+        private final List<File> directoryList;
+        private ColumnFamilyStore cfs;
+
+        protected SSTableFormat.Type formatType = null;
+
+        private Boolean makeRangeAware = false;
+
+        private CreateTableStatement.RawStatement schemaStatement;
+        private final List<CreateTypeStatement> typeStatements;
+        private UpdateStatement.ParsedInsert insertStatement;
+        private IPartitioner partitioner;
+
+        private boolean sorted = false;
+        private long bufferSizeInMB = 128;
+
+        protected Builder()
+        {
+            this.typeStatements = new ArrayList<>();
+            this.directoryList = new ArrayList<>();
+        }
+
+        /**
+         * The directory where to write the sstables.
+         * <p>
+         * This is a mandatory option.
+         *
+         * @param directory the directory to use, which should exists and be writable.
+         * @return this builder.
+         *
+         * @throws IllegalArgumentException if {@code directory} doesn't exist or is not writable.
+         */
+        public Builder inDirectory(String directory)
+        {
+            return inDirectory(new File(directory));
+        }
+
+        /**
+         * The directory where to write the sstables (mandatory option).
+         * <p>
+         * This is a mandatory option.
+         *
+         * @param directory the directory to use, which should exist and be writable.
+         * @return this builder.
+         *
+         * @throws IllegalArgumentException if {@code directory} doesn't exist or is not writable.
+         */
+        public Builder inDirectory(File directory)
+        {
+            if (!directory.exists())
+                throw new IllegalArgumentException(directory + " doesn't exists");
+            if (!directory.canWrite())
+                throw new IllegalArgumentException(directory + " exists but is not writable");
+
+            directoryList.add(directory);
+            return this;
+        }
+
+        /**
+         * A pre-instanciated ColumnFamilyStore
+         * <p>
+         * This is can be used in place of inDirectory and forTable
+         *
+         * @see #inDirectory(File)
+         *
+         * @param cfs the list of directories to use, which should exist and be writable.
+         * @return this builder.
+         *
+         * @throws IllegalArgumentException if a directory doesn't exist or is not writable.
+         */
+        public Builder withCfs(ColumnFamilyStore cfs)
+        {
+            this.cfs = cfs;
+            return this;
+        }
+
+
+        public Builder withType(String typeDefinition) throws SyntaxException
+        {
+            typeStatements.add(parseStatement(typeDefinition, CreateTypeStatement.class, "CREATE TYPE"));
+            return this;
+        }
+
+        /**
+         * The schema (CREATE TABLE statement) for the table for which sstable are to be created.
+         * <p>
+         * Please note that the provided CREATE TABLE statement <b>must</b> use a fully-qualified
+         * table name, one that include the keyspace name.
+         * <p>
+         * This is a mandatory option.
+         *
+         * @param schema the schema of the table for which sstables are to be created.
+         * @return this builder.
+         *
+         * @throws IllegalArgumentException if {@code schema} is not a valid CREATE TABLE statement
+         * or does not have a fully-qualified table name.
+         */
+        public Builder forTable(String schema)
+        {
+            this.schemaStatement = parseStatement(schema, CreateTableStatement.RawStatement.class, "CREATE TABLE");
+            return this;
+        }
+
+        /**
+         * The partitioner to use.
+         * <p>
+         * By default, {@code Murmur3Partitioner} will be used. If this is not the partitioner used
+         * by the cluster for which the SSTables are created, you need to use this method to
+         * provide the correct partitioner.
+         *
+         * @param partitioner the partitioner to use.
+         * @return this builder.
+         */
+        public Builder withPartitioner(IPartitioner partitioner)
+        {
+            this.partitioner = partitioner;
+            return this;
+        }
+
+
+        /**
+         * Specify if the sstable writer should be vnode range aware.
+         * This will create a sstable per vnode range.
+         *
+         * @param makeRangeAware
+         * @return
+         */
+        public Builder rangeAware(boolean makeRangeAware)
+        {
+            this.makeRangeAware = makeRangeAware;
+            return this;
+        }
+
+        /**
+         * The INSERT statement defining the order of the values to add for a given CQL row.
+         * <p>
+         * Please note that the provided INSERT statement <b>must</b> use a fully-qualified
+         * table name, one that include the keyspace name. Morewover, said statement must use
+         * bind variables since it is those bind variables that will be bound to values by the
+         * resulting writer.
+         * <p>
+         * This is a mandatory option, and this needs to be called after foTable().
+         *
+         * @param insert an insertion statement that defines the order
+         * of column values to use.
+         * @return this builder.
+         *
+         * @throws IllegalArgumentException if {@code insertStatement} is not a valid insertion
+         * statement, does not have a fully-qualified table name or have no bind variables.
+         */
+        public Builder using(String insert)
+        {
+            this.insertStatement = parseStatement(insert, UpdateStatement.ParsedInsert.class, "INSERT");
+            return this;
+        }
+
+        /**
+         * The size of the buffer to use.
+         * <p>
+         * This defines how much data will be buffered before being written as
+         * a new SSTable. This correspond roughly to the data size that will have the created
+         * sstable.
+         * <p>
+         * The default is 128MB, which should be reasonable for a 1GB heap. If you experience
+         * OOM while using the writer, you should lower this value.
+         *
+         * @param size the size to use in MB.
+         * @return this builder.
+         */
+        public Builder withBufferSizeInMB(int size)
+        {
+            this.bufferSizeInMB = size;
+            return this;
+        }
+
+        /**
+         * Creates a StressCQLSSTableWriter that expects sorted inputs.
+         * <p>
+         * If this option is used, the resulting writer will expect rows to be
+         * added in SSTable sorted order (and an exception will be thrown if that
+         * is not the case during insertion). The SSTable sorted order means that
+         * rows are added such that their partition key respect the partitioner
+         * order.
+         * <p>
+         * You should thus only use this option is you know that you can provide
+         * the rows in order, which is rarely the case. If you can provide the
+         * rows in order however, using this sorted might be more efficient.
+         * <p>
+         * Note that if used, some option like withBufferSizeInMB will be ignored.
+         *
+         * @return this builder.
+         */
+        public Builder sorted()
+        {
+            this.sorted = true;
+            return this;
+        }
+
+        @SuppressWarnings("resource")
+        public StressCQLSSTableWriter build()
+        {
+            if (directoryList.isEmpty() && cfs == null)
+                throw new IllegalStateException("No output directories specified, you should provide a directory with inDirectory()");
+            if (schemaStatement == null && cfs == null)
+                throw new IllegalStateException("Missing schema, you should provide the schema for the SSTable to create with forTable()");
+            if (insertStatement == null)
+                throw new IllegalStateException("No insert statement specified, you should provide an insert statement through using()");
+
+            synchronized (StressCQLSSTableWriter.class)
+            {
+                if (cfs == null)
+                    cfs = createOfflineTable(schemaStatement, typeStatements, directoryList);
+
+                if (partitioner == null)
+                    partitioner = cfs.getPartitioner();
+
+                Pair<UpdateStatement, List<ColumnSpecification>> preparedInsert = prepareInsert();
+                AbstractSSTableSimpleWriter writer = sorted
+                                                     ? new SSTableSimpleWriter(cfs.getDirectories().getDirectoryForNewSSTables(), cfs.metadata, preparedInsert.left.updatedColumns())
+                                                     : new SSTableSimpleUnsortedWriter(cfs.getDirectories().getDirectoryForNewSSTables(), cfs.metadata, preparedInsert.left.updatedColumns(), bufferSizeInMB);
+
+                if (formatType != null)
+                    writer.setSSTableFormatType(formatType);
+
+                writer.setRangeAwareWriting(makeRangeAware);
+
+                return new StressCQLSSTableWriter(cfs, writer, preparedInsert.left, preparedInsert.right);
+            }
+        }
+
+        private static void createTypes(String keyspace, List<CreateTypeStatement> typeStatements)
+        {
+            KeyspaceMetadata ksm = Schema.instance.getKSMetaData(keyspace);
+            Types.RawBuilder builder = Types.rawBuilder(keyspace);
+            for (CreateTypeStatement st : typeStatements)
+                st.addToRawBuilder(builder);
+
+            ksm = ksm.withSwapped(builder.build());
+            Schema.instance.setKeyspaceMetadata(ksm);
+        }
+
+        public static ColumnFamilyStore createOfflineTable(String schema, List<File> directoryList)
+        {
+            return createOfflineTable(parseStatement(schema, CreateTableStatement.RawStatement.class, "CREATE TABLE"), Collections.EMPTY_LIST, directoryList);
+        }
+
+        /**
+         * Creates the table according to schema statement
+         * with specified data directories
+         */
+        public static ColumnFamilyStore createOfflineTable(CreateTableStatement.RawStatement schemaStatement, List<CreateTypeStatement> typeStatements, List<File> directoryList)
+        {
+            String keyspace = schemaStatement.keyspace();
+
+            if (Schema.instance.getKSMetaData(keyspace) == null)
+                Schema.instance.load(KeyspaceMetadata.create(keyspace, KeyspaceParams.simple(1)));
+
+            createTypes(keyspace, typeStatements);
+
+            KeyspaceMetadata ksm = Schema.instance.getKSMetaData(keyspace);
+
+            CFMetaData cfMetaData = ksm.tables.getNullable(schemaStatement.columnFamily());
+
+            if (cfMetaData != null)
+                return Schema.instance.getColumnFamilyStoreInstance(cfMetaData.cfId);
+
+            CreateTableStatement statement = (CreateTableStatement) schemaStatement.prepare(ksm.types).statement;
+            statement.validate(ClientState.forInternalCalls());
+
+            //Build metatdata with a portable cfId
+            cfMetaData = statement.metadataBuilder()
+                                  .withId(CFMetaData.generateLegacyCfId(keyspace, statement.columnFamily()))
+                                  .build()
+                                  .params(statement.params());
+
+            Keyspace.setInitialized();
+            Directories directories = new Directories(cfMetaData, directoryList.stream().map(Directories.DataDirectory::new).collect(Collectors.toList()));
+
+            Keyspace ks = Keyspace.openWithoutSSTables(keyspace);
+            ColumnFamilyStore cfs =  ColumnFamilyStore.createColumnFamilyStore(ks, cfMetaData.cfName, cfMetaData, directories, false, false, true);
+
+            ks.initCfCustom(cfs);
+            Schema.instance.load(cfs.metadata);
+            Schema.instance.setKeyspaceMetadata(ksm.withSwapped(ksm.tables.with(cfs.metadata)));
+
+            return cfs;
+        }
+
+        /**
+         * Prepares insert statement for writing data to SSTable
+         *
+         * @return prepared Insert statement and it's bound names
+         */
+        private Pair<UpdateStatement, List<ColumnSpecification>> prepareInsert()
+        {
+            ParsedStatement.Prepared cqlStatement = insertStatement.prepare(ClientState.forInternalCalls());
+            UpdateStatement insert = (UpdateStatement) cqlStatement.statement;
+            insert.validate(ClientState.forInternalCalls());
+
+            if (insert.hasConditions())
+                throw new IllegalArgumentException("Conditional statements are not supported");
+            if (insert.isCounter())
+                throw new IllegalArgumentException("Counter update statements are not supported");
+            if (cqlStatement.boundNames.isEmpty())
+                throw new IllegalArgumentException("Provided insert statement has no bind variables");
+
+            return Pair.create(insert, cqlStatement.boundNames);
+        }
+    }
+
+    public static <T extends ParsedStatement> T parseStatement(String query, Class<T> klass, String type)
+    {
+        try
+        {
+            ParsedStatement stmt = CQLFragmentParser.parseAnyUnhandled(CqlParser::query, query);
+
+            if (!stmt.getClass().equals(klass))
+                throw new IllegalArgumentException("Invalid query, must be a " + type + " statement but was: " + stmt.getClass());
+
+            return klass.cast(stmt);
+        }
+        catch (RecognitionException | RequestValidationException e)
+        {
+            throw new IllegalArgumentException(e.getMessage(), e);
+        }
+    }
+}
diff --git a/tools/stress/src/org/apache/cassandra/stress/CompactionStress.java b/tools/stress/src/org/apache/cassandra/stress/CompactionStress.java
new file mode 100644
index 0000000..96e2a40
--- /dev/null
+++ b/tools/stress/src/org/apache/cassandra/stress/CompactionStress.java
@@ -0,0 +1,361 @@
+/*
+ * 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.stress;
+
+import java.io.File;
+import java.io.IOError;
+import java.net.InetAddress;
+import java.net.URI;
+import java.util.*;
+import java.util.concurrent.*;
+import javax.inject.Inject;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Uninterruptibles;
+
+import io.airlift.command.*;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.statements.CreateTableStatement;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.db.SystemKeyspace;
+import org.apache.cassandra.db.compaction.CompactionManager;
+import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.io.sstable.StressCQLSSTableWriter;
+import org.apache.cassandra.io.sstable.Component;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.locator.TokenMetadata;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.stress.generate.PartitionGenerator;
+import org.apache.cassandra.stress.generate.SeedManager;
+import org.apache.cassandra.stress.operations.userdefined.SchemaInsert;
+import org.apache.cassandra.stress.settings.StressSettings;
+import org.apache.cassandra.tools.nodetool.CompactionStats;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.JVMStabilityInspector;
+
+/**
+ * Tool that allows fast route to loading data for arbitrary schemas to disk
+ * and compacting them.
+ */
+public abstract class CompactionStress implements Runnable
+{
+    @Inject
+    public HelpOption helpOption;
+
+    @Option(name = { "-p", "--profile" }, description = "Path to stress yaml file", required = true)
+    String profile;
+
+    @Option(name = { "-d", "--datadir" }, description = "Data directory (can be used many times to specify multiple data dirs)", required = true)
+    List<String> dataDirs;
+
+    @Option(name = {"-v", "--vnodes"}, description = "number of local tokens to generate (default 256)")
+    Integer numTokens = 256;
+
+    static
+    {
+        DatabaseDescriptor.daemonInitialization();
+    }
+
+    List<File> getDataDirectories()
+    {
+        List<File> dataDirectories = new ArrayList<>(dataDirs.size());
+        for (String dataDir : dataDirs)
+        {
+            File outputDir = new File(dataDir);
+
+            if (!outputDir.exists())
+            {
+                System.err.println("Invalid output dir (missing): " + outputDir);
+                System.exit(1);
+            }
+
+            if (!outputDir.isDirectory())
+            {
+                System.err.println("Invalid output dir (not a directory): " + outputDir);
+                System.exit(2);
+            }
+
+            if (!outputDir.canWrite())
+            {
+                System.err.println("Invalid output dir (no write permissions): " + outputDir);
+                System.exit(3);
+            }
+
+            dataDirectories.add(outputDir);
+        }
+
+        return dataDirectories;
+    }
+
+    ColumnFamilyStore initCf(StressProfile stressProfile, boolean loadSSTables)
+    {
+        generateTokens(stressProfile.seedStr, StorageService.instance.getTokenMetadata(), numTokens);
+
+        CreateTableStatement.RawStatement createStatement = stressProfile.getCreateStatement();
+        List<File> dataDirectories = getDataDirectories();
+
+        ColumnFamilyStore cfs = StressCQLSSTableWriter.Builder.createOfflineTable(createStatement, Collections.EMPTY_LIST, dataDirectories);
+
+        if (loadSSTables)
+        {
+            Directories.SSTableLister lister = cfs.getDirectories().sstableLister(Directories.OnTxnErr.IGNORE).skipTemporary(true);
+            List<SSTableReader> sstables = new ArrayList<>();
+
+            //Offline open sstables
+            for (Map.Entry<Descriptor, Set<Component>> entry : lister.list().entrySet())
+            {
+                Set<Component> components = entry.getValue();
+                if (!components.contains(Component.DATA))
+                    continue;
+
+                try
+                {
+                    SSTableReader sstable = SSTableReader.openNoValidation(entry.getKey(), components, cfs);
+                    sstables.add(sstable);
+                }
+                catch (Exception e)
+                {
+                    JVMStabilityInspector.inspectThrowable(e);
+                    System.err.println(String.format("Error Loading %s: %s", entry.getKey(), e.getMessage()));
+                }
+            }
+
+            cfs.disableAutoCompaction();
+
+            //Register with cfs
+            cfs.addSSTables(sstables);
+        }
+
+        return cfs;
+    }
+
+    StressProfile getStressProfile()
+    {
+        try
+        {
+            File yamlFile = new File(profile);
+            return StressProfile.load(yamlFile.exists() ? yamlFile.toURI() : URI.create(profile));
+        }
+        catch ( IOError e)
+        {
+            e.printStackTrace();
+            System.err.print("Invalid profile URI : " + profile);
+            System.exit(4);
+        }
+
+        return null;
+    }
+
+    /**
+     * Populate tokenMetadata consistently across runs.
+     *
+     * We need consistency to write and compact the same data offline
+     * in the case of a range aware sstable writer.
+     */
+    private void generateTokens(String seed, TokenMetadata tokenMetadata, Integer numTokens)
+    {
+        Random random = new Random(seed.hashCode());
+
+        IPartitioner p = tokenMetadata.partitioner;
+        tokenMetadata.clearUnsafe();
+        for (int i = 1; i <= numTokens; i++)
+        {
+            InetAddress addr = FBUtilities.getBroadcastAddress();
+            List<Token> tokens = Lists.newArrayListWithCapacity(numTokens);
+            for (int j = 0; j < numTokens; ++j)
+                tokens.add(p.getRandomToken(random));
+
+            tokenMetadata.updateNormalTokens(tokens, addr);
+        }
+    }
+
+    public abstract void run();
+
+
+    @Command(name = "compact", description = "Compact data in directory")
+    public static class Compaction extends CompactionStress
+    {
+
+        @Option(name = {"-m", "--maximal"}, description = "Force maximal compaction (default true)")
+        Boolean maximal = false;
+
+        @Option(name = {"-t", "--threads"}, description = "Number of compactor threads to use for bg compactions (default 4)")
+        Integer threads = 4;
+
+        public void run()
+        {
+            //Setup
+            SystemKeyspace.finishStartup(); //needed for early-open
+            CompactionManager.instance.setMaximumCompactorThreads(threads);
+            CompactionManager.instance.setCoreCompactorThreads(threads);
+            CompactionManager.instance.setRate(0);
+
+            StressProfile stressProfile = getStressProfile();
+            ColumnFamilyStore cfs = initCf(stressProfile, true);
+            cfs.getCompactionStrategyManager().compactionLogger.enable();
+
+            List<Future<?>> futures = new ArrayList<>(threads);
+            if (maximal)
+            {
+                futures = CompactionManager.instance.submitMaximal(cfs, FBUtilities.nowInSeconds(), false);
+            }
+            else
+            {
+                cfs.enableAutoCompaction();
+                cfs.getCompactionStrategyManager().enable();
+                for (int i = 0; i < threads; i++)
+                    futures.addAll(CompactionManager.instance.submitBackground(cfs));
+            }
+
+            long working;
+            //Report compaction stats while working
+            while ((working = futures.stream().filter(f -> !f.isDone()).count()) > 0 || CompactionManager.instance.getActiveCompactions() > 0 || (!maximal && cfs.getCompactionStrategyManager().getEstimatedRemainingTasks() > 0))
+            {
+                //Re-up any bg jobs
+                if (!maximal)
+                {
+                    for (long i = working; i < threads; i++)
+                        futures.addAll(CompactionManager.instance.submitBackground(cfs));
+                }
+
+                reportCompactionStats();
+                Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS);
+            }
+
+            System.out.println("Finished! Shutting down...");
+            CompactionManager.instance.forceShutdown();
+
+            //Wait for cleanup to finish before forcing
+            Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
+            LifecycleTransaction.removeUnfinishedLeftovers(cfs);
+        }
+    }
+
+    void reportCompactionStats()
+    {
+        System.out.println("========");
+        System.out.println(String.format("Pending compactions: %d\n", CompactionManager.instance.getPendingTasks()));
+        CompactionStats.reportCompactionTable(CompactionManager.instance.getCompactions(), 0, true, System.out);
+    }
+
+
+    @Command(name = "write", description = "write data directly to disk")
+    public static class DataWriter extends CompactionStress
+    {
+        private static double BYTES_IN_GB = 1024 * 1014 * 1024;
+
+        @Option(name = { "-g", "--gbsize"}, description = "Total GB size on disk you wish to write", required = true)
+        Integer totalSizeGb;
+
+        @Option(name = { "-t", "--threads" }, description = "Number of sstable writer threads (default 2)")
+        Integer threads = 2;
+
+        @Option(name = { "-c", "--partition-count"}, description = "Number of partitions to loop over (default 1000000)")
+        Integer partitions = 1000000;
+
+        @Option(name = { "-b", "--buffer-size-mb"}, description = "Buffer in MB writes before writing new sstable (default 128)")
+        Integer bufferSize = 128;
+
+        @Option(name = { "-r", "--range-aware"}, description = "Splits the local ranges in number of data directories and makes sure we never write the same token in two different directories (default true)")
+        Boolean makeRangeAware = true;
+
+        public void run()
+        {
+            StressProfile stressProfile = getStressProfile();
+            ColumnFamilyStore cfs = initCf(stressProfile, false);
+            Directories directories = cfs.getDirectories();
+
+            StressSettings settings = StressSettings.parse(new String[]{ "write", "-pop seq=1.." + partitions });
+            SeedManager seedManager = new SeedManager(settings);
+            PartitionGenerator generator = stressProfile.getOfflineGenerator();
+            WorkManager workManager = new WorkManager.FixedWorkManager(Long.MAX_VALUE);
+
+            ExecutorService executorService = Executors.newFixedThreadPool(threads);
+            CountDownLatch finished = new CountDownLatch(threads);
+
+            for (int i = 0; i < threads; i++)
+            {
+                //Every thread needs it's own writer
+                final SchemaInsert insert = stressProfile.getOfflineInsert(null, generator, seedManager, settings);
+                final StressCQLSSTableWriter tableWriter = insert.createWriter(cfs, bufferSize, makeRangeAware);
+                executorService.submit(() -> {
+                    try
+                    {
+                        insert.runOffline(tableWriter, workManager);
+                    }
+                    catch (Exception e)
+                    {
+                        e.printStackTrace();
+                    }
+                    finally
+                    {
+                        FileUtils.closeQuietly(tableWriter);
+                        finished.countDown();
+                    }
+                });
+            }
+
+            double currentSizeGB;
+            while ((currentSizeGB = directories.getRawDiretoriesSize() / BYTES_IN_GB) < totalSizeGb)
+            {
+                if (finished.getCount() == 0)
+                    break;
+
+                System.out.println(String.format("Written %.2fGB of %dGB", currentSizeGB, totalSizeGb));
+
+                Uninterruptibles.sleepUninterruptibly(3, TimeUnit.SECONDS);
+            }
+
+            workManager.stop();
+            Uninterruptibles.awaitUninterruptibly(finished);
+
+            currentSizeGB = directories.getRawDiretoriesSize() / BYTES_IN_GB;
+            System.out.println(String.format("Finished writing %.2fGB", currentSizeGB));
+        }
+    }
+
+    public static void main(String[] args)
+    {
+        Cli.CliBuilder<Runnable> builder = Cli.<Runnable>builder("compaction-stress")
+                                           .withDescription("benchmark for compaction")
+                                           .withDefaultCommand(Help.class)
+                                           .withCommands(Help.class, DataWriter.class, Compaction.class);
+
+        Cli<Runnable> stress = builder.build();
+
+        try
+        {
+            stress.parse(args).run();
+        }
+        catch (Throwable t)
+        {
+            t.printStackTrace();
+            System.exit(6);
+        }
+
+        System.exit(0);
+    }
+}
+
+
diff --git a/tools/stress/src/org/apache/cassandra/stress/Operation.java b/tools/stress/src/org/apache/cassandra/stress/Operation.java
index 8054482..dc5bd2f 100644
--- a/tools/stress/src/org/apache/cassandra/stress/Operation.java
+++ b/tools/stress/src/org/apache/cassandra/stress/Operation.java
@@ -1,4 +1,4 @@
-/**
+/*
  * 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
@@ -15,24 +15,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.cassandra.stress;
 
 import java.io.IOException;
 
-import com.google.common.util.concurrent.RateLimiter;
-
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.SettingsLog;
 import org.apache.cassandra.stress.settings.StressSettings;
 import org.apache.cassandra.stress.util.JavaDriverClient;
 import org.apache.cassandra.stress.util.ThriftClient;
-import org.apache.cassandra.stress.util.Timer;
 import org.apache.cassandra.thrift.InvalidRequestException;
 import org.apache.cassandra.transport.SimpleClient;
 
 public abstract class Operation
 {
     public final StressSettings settings;
-    public final Timer timer;
+    private final Timer timer;
 
     public Operation(Timer timer, StressSettings settings)
     {
@@ -47,7 +46,7 @@
         public int rowCount();
     }
 
-    public abstract boolean ready(WorkManager permits, RateLimiter rateLimiter);
+    public abstract int ready(WorkManager permits);
 
     public boolean isWrite()
     {
@@ -61,15 +60,17 @@
      */
     public abstract void run(ThriftClient client) throws IOException;
 
-    public void run(SimpleClient client) throws IOException {
+    public void run(SimpleClient client) throws IOException
+    {
         throw new UnsupportedOperationException();
     }
 
-    public void run(JavaDriverClient client) throws IOException {
+    public void run(JavaDriverClient client) throws IOException
+    {
         throw new UnsupportedOperationException();
     }
 
-    public void timeWithRetry(RunOp run) throws IOException
+    public final void timeWithRetry(RunOp run) throws IOException
     {
         timer.start();
 
@@ -137,4 +138,8 @@
             System.err.println(message);
     }
 
+    public void intendedStartNs(long intendedTime)
+    {
+        timer.intendedTimeNs(intendedTime);
+    }
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/Stress.java b/tools/stress/src/org/apache/cassandra/stress/Stress.java
index bc6d027..3c0fa96 100644
--- a/tools/stress/src/org/apache/cassandra/stress/Stress.java
+++ b/tools/stress/src/org/apache/cassandra/stress/Stress.java
@@ -21,7 +21,9 @@
 import java.net.Socket;
 import java.net.SocketException;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.stress.settings.StressSettings;
+import org.apache.cassandra.stress.util.MultiResultLogger;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.WindowsTimer;
 
@@ -54,25 +56,49 @@
 
     public static void main(String[] arguments) throws Exception
     {
-        if (FBUtilities.isWindows())
+        if (FBUtilities.isWindows)
             WindowsTimer.startTimerPeriod(1);
 
+        int exitCode = run(arguments);
+
+        if (FBUtilities.isWindows)
+            WindowsTimer.endTimerPeriod(1);
+
+        System.exit(exitCode);
+    }
+
+
+    private static int run(String[] arguments)
+    {
         try
         {
+            DatabaseDescriptor.clientInitialization();
 
             final StressSettings settings;
             try
             {
                 settings = StressSettings.parse(arguments);
+                if (settings == null)
+                    return 0; // special settings action
             }
             catch (IllegalArgumentException e)
             {
+                System.out.printf("%s%n", e.getMessage());
                 printHelpMessage();
-                e.printStackTrace();
-                return;
+                return 1;
             }
 
-            PrintStream logout = settings.log.getOutput();
+            MultiResultLogger logout = settings.log.getOutput();
+
+            if (! settings.log.noSettings)
+            {
+                settings.printSettings(logout);
+            }
+
+            if (settings.graph.inGraphMode())
+            {
+                logout.addStream(new PrintStream(settings.graph.temporaryLogFile));
+            }
 
             if (settings.sendToDaemon != null)
             {
@@ -115,20 +141,19 @@
             {
                 StressAction stressAction = new StressAction(settings, logout);
                 stressAction.run();
+                logout.flush();
+                if (settings.graph.inGraphMode())
+                    new StressGraph(settings, arguments).generateGraph();
             }
 
         }
         catch (Throwable t)
         {
             t.printStackTrace();
-        }
-        finally
-        {
-            if (FBUtilities.isWindows())
-                WindowsTimer.endTimerPeriod(1);
-            System.exit(0);
+            return 1;
         }
 
+        return 0;
     }
 
     /**
diff --git a/tools/stress/src/org/apache/cassandra/stress/StressAction.java b/tools/stress/src/org/apache/cassandra/stress/StressAction.java
index 657117c..4e268eb 100644
--- a/tools/stress/src/org/apache/cassandra/stress/StressAction.java
+++ b/tools/stress/src/org/apache/cassandra/stress/StressAction.java
@@ -17,33 +17,35 @@
  */
 package org.apache.cassandra.stress;
 
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Queue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
-
-import com.google.common.util.concurrent.RateLimiter;
-import com.google.common.util.concurrent.Uninterruptibles;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.LockSupport;
 
 import org.apache.cassandra.stress.operations.OpDistribution;
 import org.apache.cassandra.stress.operations.OpDistributionFactory;
+import org.apache.cassandra.stress.report.StressMetrics;
+import org.apache.cassandra.stress.settings.ConnectionAPI;
 import org.apache.cassandra.stress.settings.SettingsCommand;
 import org.apache.cassandra.stress.settings.StressSettings;
 import org.apache.cassandra.stress.util.JavaDriverClient;
+import org.apache.cassandra.stress.util.ResultLogger;
 import org.apache.cassandra.stress.util.ThriftClient;
-import org.apache.cassandra.stress.util.TimingInterval;
 import org.apache.cassandra.transport.SimpleClient;
+import org.jctools.queues.SpscArrayQueue;
+import org.jctools.queues.SpscUnboundedArrayQueue;
+
+import com.google.common.util.concurrent.Uninterruptibles;
 
 public class StressAction implements Runnable
 {
 
     private final StressSettings settings;
-    private final PrintStream output;
-
-    public StressAction(StressSettings settings, PrintStream out)
+    private final ResultLogger output;
+    public StressAction(StressSettings settings, ResultLogger out)
     {
         this.settings = settings;
         output = out;
@@ -66,13 +68,19 @@
 
         if (!settings.command.noWarmup)
             warmup(settings.command.getFactory(settings));
-        if (settings.command.truncate == SettingsCommand.TruncateWhen.ONCE)
+
+        if ((settings.command.truncate == SettingsCommand.TruncateWhen.ONCE) ||
+            ((settings.rate.threadCount != -1) && (settings.command.truncate == SettingsCommand.TruncateWhen.ALWAYS)))
             settings.command.truncateTables(settings);
 
+        // Required for creating a graph from the output file
+        if (settings.rate.threadCount == -1)
+            output.println("Thread count was not specified");
+
         // TODO : move this to a new queue wrapper that gates progress based on a poisson (or configurable) distribution
-        RateLimiter rateLimiter = null;
-        if (settings.rate.opRateTargetPerSecond > 0)
-            rateLimiter = RateLimiter.create(settings.rate.opRateTargetPerSecond);
+        UniformRateLimiter rateLimiter = null;
+        if (settings.rate.opsPerSecond > 0)
+            rateLimiter = new UniformRateLimiter(settings.rate.opsPerSecond);
 
         boolean success;
         if (settings.rate.minThreads > 0)
@@ -87,12 +95,15 @@
             output.println("FAILURE");
 
         settings.disconnect();
+
+        if (!success)
+            throw new RuntimeException("Failed to execute stress action");
     }
 
     // type provided separately to support recursive call for mixed command with each command type it is performing
+    @SuppressWarnings("resource") // warmupOutput doesn't need closing
     private void warmup(OpDistributionFactory operations)
     {
-        PrintStream warmupOutput = new PrintStream(new OutputStream() { @Override public void write(int b) throws IOException { } } );
         // do 25% of iterations as warmup but no more than 50k (by default hotspot compiles methods after 10k invocations)
         int iterations = (settings.command.count >= 0
                           ? Math.min(50000, (int)(settings.command.count * 0.25))
@@ -111,13 +122,16 @@
             // we need to warm up all the nodes in the cluster ideally, but we may not be the only stress instance;
             // so warm up all the nodes we're speaking to only.
             output.println(String.format("Warming up %s with %d iterations...", single.desc(), iterations));
-            run(single, threads, iterations, 0, null, null, warmupOutput, true);
+            boolean success = null != run(single, threads, iterations, 0, null, null, ResultLogger.NOOP, true);
+            if (!success)
+                throw new RuntimeException("Failed to execute warmup");
         }
+
     }
 
     // TODO : permit varying more than just thread count
     // TODO : vary thread count based on percentage improvement of previous increment, not by fixed amounts
-    private boolean runMulti(boolean auto, RateLimiter rateLimiter)
+    private boolean runMulti(boolean auto, UniformRateLimiter rateLimiter)
     {
         if (settings.command.targetUncertainty >= 0)
             output.println("WARNING: uncertainty mode (err<) results in uneven workload between thread runs, so should be used for high level analysis only");
@@ -127,6 +141,7 @@
         List<String> runIds = new ArrayList<>();
         do
         {
+            output.println("");
             output.println(String.format("Running with %d threadCount", threadCount));
 
             if (settings.command.truncate == SettingsCommand.TruncateWhen.ALWAYS)
@@ -169,7 +184,7 @@
         } while (!auto || (hasAverageImprovement(results, 3, 0) && hasAverageImprovement(results, 5, settings.command.targetUncertainty)));
 
         // summarise all results
-        StressMetrics.summarise(runIds, results, output, settings.samples.historyCount);
+        StressMetrics.summarise(runIds, results, output);
         return true;
     }
 
@@ -183,8 +198,8 @@
         double improvement = 0;
         for (int i = results.size() - count ; i < results.size() ; i++)
         {
-            double prev = results.get(i - 1).getTiming().getHistory().opRate();
-            double cur = results.get(i).getTiming().getHistory().opRate();
+            double prev = results.get(i - 1).opRate();
+            double cur = results.get(i).opRate();
             improvement += (cur - prev) / prev;
         }
         return improvement / count;
@@ -194,9 +209,9 @@
                               int threadCount,
                               long opCount,
                               long duration,
-                              RateLimiter rateLimiter,
+                              UniformRateLimiter rateLimiter,
                               TimeUnit durationUnits,
-                              PrintStream output,
+                              ResultLogger output,
                               boolean isWarmup)
     {
         output.println(String.format("Running %s with %d threads %s",
@@ -213,20 +228,37 @@
 
         final StressMetrics metrics = new StressMetrics(output, settings.log.intervalMillis, settings);
 
+        final CountDownLatch releaseConsumers = new CountDownLatch(1);
         final CountDownLatch done = new CountDownLatch(threadCount);
+        final CountDownLatch start = new CountDownLatch(threadCount);
         final Consumer[] consumers = new Consumer[threadCount];
-        int sampleCount = settings.samples.liveCount / threadCount;
         for (int i = 0; i < threadCount; i++)
         {
-
-            consumers[i] = new Consumer(operations.get(metrics.getTiming(), sampleCount, isWarmup),
-                                        done, workManager, metrics, rateLimiter);
+            consumers[i] = new Consumer(operations, isWarmup,
+                                        done, start, releaseConsumers, workManager, metrics, rateLimiter);
         }
 
         // starting worker threadCount
         for (int i = 0; i < threadCount; i++)
             consumers[i].start();
 
+        // wait for the lot of them to get their pants on
+        try
+        {
+            start.await();
+        }
+        catch (InterruptedException e)
+        {
+            throw new RuntimeException("Unexpected interruption", e);
+        }
+        // start counting from NOW!
+        if(rateLimiter != null)
+        {
+            rateLimiter.start();
+        }
+        // release the hounds!!!
+        releaseConsumers.countDown();
+
         metrics.start();
 
         if (durationUnits != null)
@@ -267,64 +299,174 @@
         return metrics;
     }
 
-    private class Consumer extends Thread
+    /**
+     * Provides a 'next operation time' for rate limited operation streams. The rate limiter is thread safe and is to be
+     * shared by all consumer threads.
+     */
+    private static class UniformRateLimiter
     {
+        long start = Long.MIN_VALUE;
+        final long intervalNs;
+        final AtomicLong opIndex = new AtomicLong();
 
-        private final OpDistribution operations;
-        private final StressMetrics metrics;
-        private final RateLimiter rateLimiter;
-        private volatile boolean success = true;
-        private final WorkManager workManager;
-        private final CountDownLatch done;
-
-        public Consumer(OpDistribution operations,
-                        CountDownLatch done,
-                        WorkManager workManager,
-                        StressMetrics metrics,
-                        RateLimiter rateLimiter)
+        UniformRateLimiter(int opsPerSec)
         {
-            this.done = done;
+            intervalNs = 1000000000 / opsPerSec;
+        }
+
+        void start()
+        {
+            start = System.nanoTime();
+        }
+
+        /**
+         * @param partitionCount
+         * @return expect start time in ns for the operation
+         */
+        long acquire(int partitionCount)
+        {
+            long currOpIndex = opIndex.getAndAdd(partitionCount);
+            return start + currOpIndex * intervalNs;
+        }
+    }
+
+    /**
+     * Provides a blocking stream of operations per consumer.
+     */
+    private static class StreamOfOperations
+    {
+        private final OpDistribution operations;
+        private final UniformRateLimiter rateLimiter;
+        private final WorkManager workManager;
+
+        public StreamOfOperations(OpDistribution operations, UniformRateLimiter rateLimiter, WorkManager workManager)
+        {
+            this.operations = operations;
             this.rateLimiter = rateLimiter;
             this.workManager = workManager;
-            this.metrics = metrics;
-            this.operations = operations;
         }
 
+        /**
+         * This method will block until the next operation becomes available.
+         *
+         * @return next operation or null if no more ops are coming
+         */
+        Operation nextOp()
+        {
+            Operation op = operations.next();
+            final int partitionCount = op.ready(workManager);
+            if (partitionCount == 0)
+                return null;
+            if (rateLimiter != null)
+            {
+                long intendedTime = rateLimiter.acquire(partitionCount);
+                op.intendedStartNs(intendedTime);
+                long now;
+                while ((now = System.nanoTime()) < intendedTime)
+                {
+                    LockSupport.parkNanos(intendedTime - now);
+                }
+            }
+            return op;
+        }
+
+        void abort()
+        {
+            workManager.stop();
+        }
+    }
+    public static class OpMeasurement
+    {
+        public String opType;
+        public long intended,started,ended,rowCnt,partitionCnt;
+        public boolean err;
+        @Override
+        public String toString()
+        {
+            return "OpMeasurement [opType=" + opType + ", intended=" + intended + ", started=" + started + ", ended="
+                    + ended + ", rowCnt=" + rowCnt + ", partitionCnt=" + partitionCnt + ", err=" + err + "]";
+        }
+    }
+    public interface MeasurementSink
+    {
+        void record(String opType,long intended, long started, long ended, long rowCnt, long partitionCnt, boolean err);
+    }
+    public class Consumer extends Thread implements MeasurementSink
+    {
+        private final StreamOfOperations opStream;
+        private final StressMetrics metrics;
+        private volatile boolean success = true;
+        private final CountDownLatch done;
+        private final CountDownLatch start;
+        private final CountDownLatch releaseConsumers;
+        public final Queue<OpMeasurement> measurementsRecycling;
+        public final Queue<OpMeasurement> measurementsReporting;
+        public Consumer(OpDistributionFactory operations,
+                        boolean isWarmup,
+                        CountDownLatch done,
+                        CountDownLatch start,
+                        CountDownLatch releaseConsumers,
+                        WorkManager workManager,
+                        StressMetrics metrics,
+                        UniformRateLimiter rateLimiter)
+        {
+            OpDistribution opDistribution = operations.get(isWarmup, this);
+            this.done = done;
+            this.start = start;
+            this.releaseConsumers = releaseConsumers;
+            this.metrics = metrics;
+            this.opStream = new StreamOfOperations(opDistribution, rateLimiter, workManager);
+            this.measurementsRecycling =  new SpscArrayQueue<OpMeasurement>(8*1024);
+            this.measurementsReporting =  new SpscUnboundedArrayQueue<OpMeasurement>(2048);
+            metrics.add(this);
+        }
+
+
         public void run()
         {
-            operations.initTimers();
-
             try
             {
                 SimpleClient sclient = null;
                 ThriftClient tclient = null;
                 JavaDriverClient jclient = null;
+                final ConnectionAPI clientType = settings.mode.api;
 
-                switch (settings.mode.api)
+                try
                 {
-                    case JAVA_DRIVER_NATIVE:
-                        jclient = settings.getJavaDriverClient();
-                        break;
-                    case SIMPLE_NATIVE:
-                        sclient = settings.getSimpleNativeClient();
-                        break;
-                    case THRIFT:
-                    case THRIFT_SMART:
-                        tclient = settings.getThriftClient();
-                        break;
-                    default:
-                        throw new IllegalStateException();
+                    switch (clientType)
+                    {
+                        case JAVA_DRIVER_NATIVE:
+                            jclient = settings.getJavaDriverClient();
+                            break;
+                        case SIMPLE_NATIVE:
+                            sclient = settings.getSimpleNativeClient();
+                            break;
+                        case THRIFT:
+                        case THRIFT_SMART:
+                            tclient = settings.getThriftClient();
+                            break;
+                        default:
+                            throw new IllegalStateException();
+                    }
                 }
+                finally
+                {
+                    // synchronize the start of all the consumer threads
+                    start.countDown();
+                }
+
+                releaseConsumers.await();
 
                 while (true)
                 {
-                    Operation op = operations.next();
-                    if (!op.ready(workManager, rateLimiter))
+                    // Assumption: All ops are thread local, operations are never shared across threads.
+                    Operation op = opStream.nextOp();
+                    if (op == null)
                         break;
 
                     try
                     {
-                        switch (settings.mode.api)
+                        switch (clientType)
                         {
                             case JAVA_DRIVER_NATIVE:
                                 op.run(jclient);
@@ -341,25 +483,43 @@
                     catch (Exception e)
                     {
                         if (output == null)
-                        {
                             System.err.println(e.getMessage());
-                            success = false;
-                            System.exit(-1);
-                        }
+                        else
+                            output.printException(e);
 
-                        e.printStackTrace(output);
                         success = false;
-                        workManager.stop();
+                        opStream.abort();
                         metrics.cancel();
                         return;
                     }
                 }
             }
+            catch (Exception e)
+            {
+                System.err.println(e.getMessage());
+                success = false;
+            }
             finally
             {
                 done.countDown();
-                operations.closeTimers();
             }
         }
+
+        @Override
+        public void record(String opType, long intended, long started, long ended, long rowCnt, long partitionCnt, boolean err)
+        {
+            OpMeasurement opMeasurement = measurementsRecycling.poll();
+            if(opMeasurement == null) {
+                opMeasurement = new OpMeasurement();
+            }
+            opMeasurement.opType = opType;
+            opMeasurement.intended = intended;
+            opMeasurement.started = started;
+            opMeasurement.ended = ended;
+            opMeasurement.rowCnt = rowCnt;
+            opMeasurement.partitionCnt = partitionCnt;
+            opMeasurement.err = err;
+            measurementsReporting.offer(opMeasurement);
+        }
     }
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/StressGraph.java b/tools/stress/src/org/apache/cassandra/stress/StressGraph.java
new file mode 100644
index 0000000..663bde6
--- /dev/null
+++ b/tools/stress/src/org/apache/cassandra/stress/StressGraph.java
@@ -0,0 +1,262 @@
+/**
+ * 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
+ * <p/>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p/>
+ * 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.stress;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.google.common.io.ByteStreams;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.cassandra.stress.report.StressMetrics;
+import org.apache.cassandra.stress.settings.StressSettings;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+
+
+public class StressGraph
+{
+    private StressSettings stressSettings;
+    private enum ReadingMode
+    {
+        START,
+        METRICS,
+        AGGREGATES,
+        NEXTITERATION
+    }
+    private String[] stressArguments;
+
+    public StressGraph(StressSettings stressSetttings, String[] stressArguments)
+    {
+        this.stressSettings = stressSetttings;
+        this.stressArguments = stressArguments;
+    }
+
+    public void generateGraph()
+    {
+        File htmlFile = new File(stressSettings.graph.file);
+        JSONObject stats;
+        if (htmlFile.isFile())
+        {
+            try
+            {
+                String html = new String(Files.readAllBytes(Paths.get(htmlFile.toURI())), StandardCharsets.UTF_8);
+                stats = parseExistingStats(html);
+            }
+            catch (IOException e)
+            {
+                throw new RuntimeException("Couldn't load existing stats html.");
+            }
+            stats = this.createJSONStats(stats);
+        }
+        else
+        {
+            stats = this.createJSONStats(null);
+        }
+
+        try
+        {
+            PrintWriter out = new PrintWriter(htmlFile);
+            String statsBlock = "/* stats start */\nstats = " + stats.toJSONString() + ";\n/* stats end */\n";
+            String html = getGraphHTML().replaceFirst("/\\* stats start \\*/\n\n/\\* stats end \\*/\n", statsBlock);
+            out.write(html);
+            out.close();
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException("Couldn't write stats html.");
+        }
+    }
+
+    private JSONObject parseExistingStats(String html)
+    {
+        JSONObject stats;
+
+        Pattern pattern = Pattern.compile("(?s).*/\\* stats start \\*/\\nstats = (.*);\\n/\\* stats end \\*/.*");
+        Matcher matcher = pattern.matcher(html);
+        matcher.matches();
+        stats = (JSONObject) JSONValue.parse(matcher.group(1));
+
+        return stats;
+    }
+
+    private String getGraphHTML()
+    {
+        InputStream graphHTMLRes = StressGraph.class.getClassLoader().getResourceAsStream("org/apache/cassandra/stress/graph/graph.html");
+        String graphHTML;
+        try
+        {
+            graphHTML = new String(ByteStreams.toByteArray(graphHTMLRes));
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+        return graphHTML;
+    }
+
+    /** Parse log and append to stats array */
+    private JSONArray parseLogStats(InputStream log, JSONArray stats) {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(log));
+        JSONObject json = new JSONObject();
+        JSONArray intervals = new JSONArray();
+        boolean runningMultipleThreadCounts = false;
+        String currentThreadCount = null;
+        Pattern threadCountMessage = Pattern.compile("Running ([A-Z]+) with ([0-9]+) threads .*");
+        ReadingMode mode = ReadingMode.START;
+
+        try
+        {
+            String line;
+            while ((line = reader.readLine()) != null)
+            {
+                // Detect if we are running multiple thread counts:
+                if (line.startsWith("Thread count was not specified"))
+                    runningMultipleThreadCounts = true;
+
+                if (runningMultipleThreadCounts)
+                {
+                    // Detect thread count:
+                    Matcher tc = threadCountMessage.matcher(line);
+                    if (tc.matches())
+                    {
+                        currentThreadCount = tc.group(2);
+                    }
+                }
+                
+                // Detect mode changes
+                if (line.equals(StressMetrics.HEAD))
+                {
+                    mode = ReadingMode.METRICS;
+                    continue;
+                }
+                else if (line.equals("Results:"))
+                {
+                    mode = ReadingMode.AGGREGATES;
+                    continue;
+                }
+                else if (mode == ReadingMode.AGGREGATES && line.equals(""))
+                {
+                    mode = ReadingMode.NEXTITERATION;
+                }
+                else if (line.equals("END") || line.equals("FAILURE"))
+                {
+                    break;
+                }
+
+                // Process lines
+                if (mode == ReadingMode.METRICS)
+                {
+                    JSONArray metrics = new JSONArray();
+                    String[] parts = line.split(",");
+                    if (parts.length != StressMetrics.HEADMETRICS.length)
+                    {
+                        continue;
+                    }
+                    for (String m : parts)
+                    {
+                        try
+                        {
+                            metrics.add(new BigDecimal(m.trim()));
+                        }
+                        catch (NumberFormatException e)
+                        {
+                            metrics.add(null);
+                        }
+                    }
+                    intervals.add(metrics);
+                }
+                else if (mode == ReadingMode.AGGREGATES)
+                {
+                    String[] parts = line.split(":",2);
+                    if (parts.length != 2)
+                    {
+                        continue;
+                    }
+                    // the graphing js expects lower case names
+                    json.put(parts[0].trim().toLowerCase(), parts[1].trim());
+                }
+                else if (mode == ReadingMode.NEXTITERATION)
+                {
+                    //Wrap up the results of this test and append to the array.
+                    json.put("metrics", Arrays.asList(StressMetrics.HEADMETRICS));
+                    json.put("test", stressSettings.graph.operation);
+                    if (currentThreadCount == null)
+                        json.put("revision", stressSettings.graph.revision);
+                    else
+                        json.put("revision", String.format("%s - %s threads", stressSettings.graph.revision, currentThreadCount));
+                    String command = StringUtils.join(stressArguments, " ").replaceAll("password=.*? ", "password=******* ");
+                    json.put("command", command);
+                    json.put("intervals", intervals);
+                    stats.add(json);
+
+                    //Start fresh for next iteration:
+                    json = new JSONObject();
+                    intervals = new JSONArray();
+                    mode = ReadingMode.START;
+                }
+            }
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException("Couldn't read from temporary stress log file");
+        }
+        if (json.size() != 0) stats.add(json);
+        return stats;
+    }
+
+    private JSONObject createJSONStats(JSONObject json)
+    {
+        try (InputStream logStream = new FileInputStream(stressSettings.graph.temporaryLogFile))
+        {
+            JSONArray stats;
+            if (json == null)
+            {
+                json = new JSONObject();
+                stats = new JSONArray();
+            }
+            else
+            {
+                stats = (JSONArray) json.get("stats");
+            }
+
+            stats = parseLogStats(logStream, stats);
+
+            json.put("title", stressSettings.graph.title);
+            json.put("stats", stats);
+            return json;
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/tools/stress/src/org/apache/cassandra/stress/StressMetrics.java b/tools/stress/src/org/apache/cassandra/stress/StressMetrics.java
deleted file mode 100644
index a4f280e..0000000
--- a/tools/stress/src/org/apache/cassandra/stress/StressMetrics.java
+++ /dev/null
@@ -1,279 +0,0 @@
-package org.apache.cassandra.stress;
-/*
- * 
- * 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.
- * 
- */
-
-
-import java.io.PrintStream;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ThreadFactory;
-
-import org.apache.cassandra.stress.util.*;
-import org.apache.commons.lang3.time.DurationFormatUtils;
-import org.apache.cassandra.concurrent.NamedThreadFactory;
-import org.apache.cassandra.stress.settings.StressSettings;
-
-public class StressMetrics
-{
-
-    private static final ThreadFactory tf = new NamedThreadFactory("StressMetrics");
-
-    private final PrintStream output;
-    private final Thread thread;
-    private volatile boolean stop = false;
-    private volatile boolean cancelled = false;
-    private final Uncertainty rowRateUncertainty = new Uncertainty();
-    private final CountDownLatch stopped = new CountDownLatch(1);
-    private final Timing timing;
-    private final Callable<JmxCollector.GcStats> gcStatsCollector;
-    private volatile JmxCollector.GcStats totalGcStats;
-    private final StressSettings settings;
-
-    public StressMetrics(PrintStream output, final long logIntervalMillis, StressSettings settings)
-    {
-        this.output = output;
-        this.settings = settings;
-        Callable<JmxCollector.GcStats> gcStatsCollector;
-        totalGcStats = new JmxCollector.GcStats(0);
-        try
-        {
-            gcStatsCollector = new JmxCollector(settings.node.resolveAllPermitted(settings), settings.port.jmxPort);
-        }
-        catch (Throwable t)
-        {
-            switch (settings.log.level)
-            {
-                case VERBOSE:
-                    t.printStackTrace();
-            }
-            System.err.println("Failed to connect over JMX; not collecting these stats");
-            gcStatsCollector = new Callable<JmxCollector.GcStats>()
-            {
-                public JmxCollector.GcStats call() throws Exception
-                {
-                    return totalGcStats;
-                }
-            };
-        }
-        this.gcStatsCollector = gcStatsCollector;
-        this.timing = new Timing(settings.samples.historyCount, settings.samples.reportCount);
-
-        printHeader("", output);
-        thread = tf.newThread(new Runnable()
-        {
-            @Override
-            public void run()
-            {
-                timing.start();
-                try {
-
-                    while (!stop)
-                    {
-                        try
-                        {
-                            long sleepNanos = timing.getHistory().endNanos() - System.nanoTime();
-                            long sleep = (sleepNanos / 1000000) + logIntervalMillis;
-
-                            if (sleep < logIntervalMillis >>> 3)
-                                // if had a major hiccup, sleep full interval
-                                Thread.sleep(logIntervalMillis);
-                            else
-                                Thread.sleep(sleep);
-
-                            update();
-                        } catch (InterruptedException e)
-                        {
-                            break;
-                        }
-                    }
-
-                    update();
-                }
-                catch (InterruptedException e)
-                {}
-                catch (Exception e)
-                {
-                    cancel();
-                    e.printStackTrace(StressMetrics.this.output);
-                }
-                finally
-                {
-                    rowRateUncertainty.wakeAll();
-                    stopped.countDown();
-                }
-            }
-        });
-    }
-
-    public void start()
-    {
-        thread.start();
-    }
-
-    public void waitUntilConverges(double targetUncertainty, int minMeasurements, int maxMeasurements) throws InterruptedException
-    {
-        rowRateUncertainty.await(targetUncertainty, minMeasurements, maxMeasurements);
-    }
-
-    public void cancel()
-    {
-        cancelled = true;
-        stop = true;
-        thread.interrupt();
-        rowRateUncertainty.wakeAll();
-    }
-
-    public void stop() throws InterruptedException
-    {
-        stop = true;
-        thread.interrupt();
-        stopped.await();
-    }
-
-    private void update() throws InterruptedException
-    {
-        Timing.TimingResult<JmxCollector.GcStats> result = timing.snap(gcStatsCollector);
-        totalGcStats = JmxCollector.GcStats.aggregate(Arrays.asList(totalGcStats, result.extra));
-        TimingInterval current = result.intervals.combine(settings.samples.reportCount);
-        TimingInterval history = timing.getHistory().combine(settings.samples.historyCount);
-        rowRateUncertainty.update(current.adjustedRowRate());
-        if (current.operationCount != 0)
-        {
-            if (result.intervals.intervals().size() > 1)
-            {
-                for (Map.Entry<String, TimingInterval> type : result.intervals.intervals().entrySet())
-                    printRow("", type.getKey(), type.getValue(), timing.getHistory().get(type.getKey()), result.extra, rowRateUncertainty, output);
-            }
-
-            printRow("", "total", current, history, result.extra, rowRateUncertainty, output);
-        }
-        if (timing.done())
-            stop = true;
-    }
-
-
-    // PRINT FORMATTING
-
-    public static final String HEADFORMAT = "%-10s%10s,%8s,%8s,%8s,%8s,%8s,%8s,%8s,%8s,%8s,%7s,%9s,%7s,%7s,%8s,%8s,%8s,%8s";
-    public static final String ROWFORMAT =  "%-10s%10d,%8.0f,%8.0f,%8.0f,%8.1f,%8.1f,%8.1f,%8.1f,%8.1f,%8.1f,%7.1f,%9.5f,%7d,%7.0f,%8.0f,%8.0f,%8.0f,%8.0f";
-
-    private static void printHeader(String prefix, PrintStream output)
-    {
-        output.println(prefix + String.format(HEADFORMAT, "type,", "total ops","op/s","pk/s","row/s","mean","med",".95",".99",".999","max","time","stderr", "errors", "gc: #", "max ms", "sum ms", "sdv ms", "mb"));
-    }
-
-    private static void printRow(String prefix, String type, TimingInterval interval, TimingInterval total, JmxCollector.GcStats gcStats, Uncertainty opRateUncertainty, PrintStream output)
-    {
-        output.println(prefix + String.format(ROWFORMAT,
-                type + ",",
-                total.operationCount,
-                interval.opRate(),
-                interval.partitionRate(),
-                interval.rowRate(),
-                interval.meanLatency(),
-                interval.medianLatency(),
-                interval.rankLatency(0.95f),
-                interval.rankLatency(0.99f),
-                interval.rankLatency(0.999f),
-                interval.maxLatency(),
-                total.runTime() / 1000f,
-                opRateUncertainty.getUncertainty(),
-                interval.errorCount,
-                gcStats.count,
-                gcStats.maxms,
-                gcStats.summs,
-                gcStats.sdvms,
-                gcStats.bytes / (1 << 20)
-        ));
-    }
-
-    public void summarise()
-    {
-        output.println("\n");
-        output.println("Results:");
-
-        TimingIntervals opHistory = timing.getHistory();
-        TimingInterval history = opHistory.combine(settings.samples.historyCount);
-        output.println(String.format("op rate                   : %.0f %s", history.opRate(), opHistory.opRates()));
-        output.println(String.format("partition rate            : %.0f %s", history.partitionRate(), opHistory.partitionRates()));
-        output.println(String.format("row rate                  : %.0f %s", history.rowRate(), opHistory.rowRates()));
-        output.println(String.format("latency mean              : %.1f %s", history.meanLatency(), opHistory.meanLatencies()));
-        output.println(String.format("latency median            : %.1f %s", history.medianLatency(), opHistory.medianLatencies()));
-        output.println(String.format("latency 95th percentile   : %.1f %s", history.rankLatency(.95f), opHistory.rankLatencies(0.95f)));
-        output.println(String.format("latency 99th percentile   : %.1f %s", history.rankLatency(0.99f), opHistory.rankLatencies(0.99f)));
-        output.println(String.format("latency 99.9th percentile : %.1f %s", history.rankLatency(0.999f), opHistory.rankLatencies(0.999f)));
-        output.println(String.format("latency max               : %.1f %s", history.maxLatency(), opHistory.maxLatencies()));
-        output.println(String.format("Total partitions          : %d %s",   history.partitionCount, opHistory.partitionCounts()));
-        output.println(String.format("Total errors              : %d %s",   history.errorCount, opHistory.errorCounts()));
-        output.println(String.format("total gc count            : %.0f", totalGcStats.count));
-        output.println(String.format("total gc mb               : %.0f", totalGcStats.bytes / (1 << 20)));
-        output.println(String.format("total gc time (s)         : %.0f", totalGcStats.summs / 1000));
-        output.println(String.format("avg gc time(ms)           : %.0f", totalGcStats.summs / totalGcStats.count));
-        output.println(String.format("stdev gc time(ms)         : %.0f", totalGcStats.sdvms));
-        output.println("Total operation time      : " + DurationFormatUtils.formatDuration(
-                history.runTime(), "HH:mm:ss", true));
-    }
-
-    public static void summarise(List<String> ids, List<StressMetrics> summarise, PrintStream out, int historySampleCount)
-    {
-        int idLen = 0;
-        for (String id : ids)
-            idLen = Math.max(id.length(), idLen);
-        String formatstr = "%" + idLen + "s, ";
-        printHeader(String.format(formatstr, "id"), out);
-        for (int i = 0 ; i < ids.size() ; i++)
-        {
-            for (Map.Entry<String, TimingInterval> type : summarise.get(i).timing.getHistory().intervals().entrySet())
-            {
-                printRow(String.format(formatstr, ids.get(i)),
-                         type.getKey(),
-                         type.getValue(),
-                         type.getValue(),
-                         summarise.get(i).totalGcStats,
-                         summarise.get(i).rowRateUncertainty,
-                         out);
-            }
-            TimingInterval hist = summarise.get(i).timing.getHistory().combine(historySampleCount);
-            printRow(String.format(formatstr, ids.get(i)),
-                    "total",
-                    hist,
-                    hist,
-                    summarise.get(i).totalGcStats,
-                    summarise.get(i).rowRateUncertainty,
-                    out
-            );
-        }
-    }
-
-    public Timing getTiming()
-    {
-        return timing;
-    }
-
-    public boolean wasCancelled()
-    {
-        return cancelled;
-    }
-
-}
diff --git a/tools/stress/src/org/apache/cassandra/stress/StressProfile.java b/tools/stress/src/org/apache/cassandra/stress/StressProfile.java
index 0b0d4e9..ad10499 100644
--- a/tools/stress/src/org/apache/cassandra/stress/StressProfile.java
+++ b/tools/stress/src/org/apache/cassandra/stress/StressProfile.java
@@ -28,16 +28,20 @@
 import java.net.URI;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import com.google.common.base.Function;
 import com.google.common.util.concurrent.Uninterruptibles;
 
 import com.datastax.driver.core.*;
 import com.datastax.driver.core.exceptions.AlreadyExistsException;
+import org.antlr.runtime.RecognitionException;
 import org.apache.cassandra.config.CFMetaData;
-import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.ColumnDefinition;
+import org.apache.cassandra.cql3.CQLFragmentParser;
+import org.apache.cassandra.cql3.CqlParser;
 import org.apache.cassandra.cql3.QueryProcessor;
-import org.apache.cassandra.cql3.statements.CreateKeyspaceStatement;
 import org.apache.cassandra.cql3.statements.CreateTableStatement;
 import org.apache.cassandra.exceptions.RequestValidationException;
 import org.apache.cassandra.exceptions.SyntaxException;
@@ -47,10 +51,11 @@
 import org.apache.cassandra.stress.operations.userdefined.SchemaInsert;
 import org.apache.cassandra.stress.operations.userdefined.SchemaQuery;
 import org.apache.cassandra.stress.operations.userdefined.ValidatingSchemaQuery;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.*;
 import org.apache.cassandra.stress.util.JavaDriverClient;
+import org.apache.cassandra.stress.util.ResultLogger;
 import org.apache.cassandra.stress.util.ThriftClient;
-import org.apache.cassandra.stress.util.Timer;
 import org.apache.cassandra.thrift.Compression;
 import org.apache.cassandra.thrift.ThriftConversion;
 import org.apache.thrift.TException;
@@ -63,7 +68,7 @@
     private String keyspaceCql;
     private String tableCql;
     private List<String> extraSchemaDefinitions;
-    private String seedStr;
+    public final String seedStr = "seed for stress";
 
     public String keyspaceName;
     public String tableName;
@@ -71,6 +76,7 @@
     private Map<String, StressYaml.QueryDef> queries;
     public Map<String, StressYaml.TokenRangeQueryDef> tokenRangeQueries;
     private Map<String, String> insert;
+    private boolean schemaCreated=false;
 
     transient volatile TableMetadata tableMetaData;
     transient volatile Set<TokenRange> tokenRanges;
@@ -89,17 +95,58 @@
     transient volatile Map<String, PreparedStatement> queryStatements;
     transient volatile Map<String, Integer> thriftQueryIds;
 
+    private static final Pattern lowercaseAlphanumeric = Pattern.compile("[a-z0-9_]+");
+
+
+    public void printSettings(ResultLogger out, StressSettings stressSettings)
+    {
+        out.printf("  Keyspace Name: %s%n", keyspaceName);
+        out.printf("  Keyspace CQL: %n***%n%s***%n%n", keyspaceCql);
+        out.printf("  Table Name: %s%n", tableName);
+        out.printf("  Table CQL: %n***%n%s***%n%n", tableCql);
+        out.printf("  Extra Schema Definitions: %s%n", extraSchemaDefinitions);
+        if (columnConfigs != null)
+        {
+            out.printf("  Generator Configs:%n");
+            columnConfigs.forEach((k, v) -> out.printf("    %s: %s%n", k, v.getConfigAsString()));
+        }
+        if(queries != null)
+        {
+            out.printf("  Query Definitions:%n");
+            queries.forEach((k, v) -> out.printf("    %s: %s%n", k, v.getConfigAsString()));
+        }
+        if (tokenRangeQueries != null)
+        {
+            out.printf("  Token Range Queries:%n");
+            tokenRangeQueries.forEach((k, v) -> out.printf("    %s: %s%n", k, v.getConfigAsString()));
+        }
+        if (insert != null)
+        {
+            out.printf("  Insert Settings:%n");
+            insert.forEach((k, v) -> out.printf("    %s: %s%n", k, v));
+        }
+
+        PartitionGenerator generator = newGenerator(stressSettings);
+        Distribution visits = stressSettings.insert.visits.get();
+        SchemaInsert tmp = getInsert(null, generator, null, stressSettings); //just calling this to initialize selectchance and partitions vals for calc below
+
+        double minBatchSize = selectchance.get().min() * partitions.get().minValue() * generator.minRowCount * (1d / visits.maxValue());
+        double maxBatchSize = selectchance.get().max() * partitions.get().maxValue() * generator.maxRowCount * (1d / visits.minValue());
+        out.printf("Generating batches with [%d..%d] partitions and [%.0f..%.0f] rows (of [%.0f..%.0f] total rows in the partitions)%n",
+                          partitions.get().minValue(), partitions.get().maxValue(),
+                          minBatchSize, maxBatchSize,
+                          partitions.get().minValue() * generator.minRowCount,
+                          partitions.get().maxValue() * generator.maxRowCount);
+
+    }
+
+
     private void init(StressYaml yaml) throws RequestValidationException
     {
-        // Use client mode. Otherwise, users with no read permission on /var/lib/commitlog won't be able to
-        // use cassandra-stress...
-        Config.setClientMode(true);
-
         keyspaceName = yaml.keyspace;
         keyspaceCql = yaml.keyspace_definition;
         tableName = yaml.table;
         tableCql = yaml.table_definition;
-        seedStr = "seed for stress";
         queries = yaml.queries;
         tokenRangeQueries = yaml.token_range_queries;
         insert = yaml.insert;
@@ -118,10 +165,10 @@
         {
             try
             {
-                String name = ((CreateKeyspaceStatement) QueryProcessor.parseStatement(keyspaceCql)).keyspace();
+                String name = CQLFragmentParser.parseAnyUnhandled(CqlParser::createKeyspaceStatement, keyspaceCql).keyspace();
                 assert name.equalsIgnoreCase(keyspaceName) : "Name in keyspace_definition doesn't match keyspace property: '" + name + "' != '" + keyspaceName + "'";
             }
-            catch (SyntaxException e)
+            catch (RecognitionException | SyntaxException e)
             {
                 throw new IllegalArgumentException("There was a problem parsing the keyspace cql: " + e.getMessage());
             }
@@ -135,10 +182,10 @@
         {
             try
             {
-                String name = ((CreateTableStatement.RawStatement) QueryProcessor.parseStatement(tableCql)).columnFamily();
+                String name = CQLFragmentParser.parseAnyUnhandled(CqlParser::createTableStatement, tableCql).columnFamily();
                 assert name.equalsIgnoreCase(tableName) : "Name in table_definition doesn't match table property: '" + name + "' != '" + tableName + "'";
             }
-            catch (RuntimeException e)
+            catch (RecognitionException | RuntimeException e)
             {
                 throw new IllegalArgumentException("There was a problem parsing the table cql: " + e.getMessage());
             }
@@ -173,54 +220,58 @@
 
     public void maybeCreateSchema(StressSettings settings)
     {
-        JavaDriverClient client = settings.getJavaDriverClient(false);
-
-        if (keyspaceCql != null)
+        if (!schemaCreated)
         {
-            try
-            {
-                client.execute(keyspaceCql, org.apache.cassandra.db.ConsistencyLevel.ONE);
-            }
-            catch (AlreadyExistsException e)
-            {
-            }
-        }
+            JavaDriverClient client = settings.getJavaDriverClient(false);
 
-        client.execute("use " + keyspaceName, org.apache.cassandra.db.ConsistencyLevel.ONE);
-
-        if (tableCql != null)
-        {
-            try
+            if (keyspaceCql != null)
             {
-                client.execute(tableCql, org.apache.cassandra.db.ConsistencyLevel.ONE);
-            }
-            catch (AlreadyExistsException e)
-            {
-            }
-
-            System.out.println(String.format("Created schema. Sleeping %ss for propagation.", settings.node.nodes.size()));
-            Uninterruptibles.sleepUninterruptibly(settings.node.nodes.size(), TimeUnit.SECONDS);
-        }
-
-        if (extraSchemaDefinitions != null)
-        {
-            for (String extraCql : extraSchemaDefinitions)
-            {
-
                 try
                 {
-                    client.execute(extraCql, org.apache.cassandra.db.ConsistencyLevel.ONE);
+                    client.execute(keyspaceCql, org.apache.cassandra.db.ConsistencyLevel.ONE);
                 }
                 catch (AlreadyExistsException e)
                 {
                 }
             }
 
-            System.out.println(String.format("Created extra schema. Sleeping %ss for propagation.", settings.node.nodes.size()));
-            Uninterruptibles.sleepUninterruptibly(settings.node.nodes.size(), TimeUnit.SECONDS);
-        }
+            client.execute("use " + keyspaceName, org.apache.cassandra.db.ConsistencyLevel.ONE);
 
+            if (tableCql != null)
+            {
+                try
+                {
+                    client.execute(tableCql, org.apache.cassandra.db.ConsistencyLevel.ONE);
+                }
+                catch (AlreadyExistsException e)
+                {
+                }
+
+                System.out.println(String.format("Created schema. Sleeping %ss for propagation.", settings.node.nodes.size()));
+                Uninterruptibles.sleepUninterruptibly(settings.node.nodes.size(), TimeUnit.SECONDS);
+            }
+
+            if (extraSchemaDefinitions != null)
+            {
+                for (String extraCql : extraSchemaDefinitions)
+                {
+
+                    try
+                    {
+                        client.execute(extraCql, org.apache.cassandra.db.ConsistencyLevel.ONE);
+                    }
+                    catch (AlreadyExistsException e)
+                    {
+                    }
+                }
+
+                System.out.println(String.format("Created extra schema. Sleeping %ss for propagation.", settings.node.nodes.size()));
+                Uninterruptibles.sleepUninterruptibly(settings.node.nodes.size(), TimeUnit.SECONDS);
+            }
+        schemaCreated = true;
+        }
         maybeLoadSchemaInfo(settings);
+
     }
 
     public void truncateTable(StressSettings settings)
@@ -249,7 +300,7 @@
                 TableMetadata metadata = client.getCluster()
                                                .getMetadata()
                                                .getKeyspace(keyspaceName)
-                                               .getTable(tableName);
+                                               .getTable(quoteIdentifier(tableName));
 
                 if (metadata == null)
                     throw new RuntimeException("Unable to find table " + keyspaceName + "." + tableName);
@@ -363,6 +414,87 @@
         return new TokenRangeQuery(timer, settings, tableMetaData, tokenRangeIterator, def, isWarmup);
     }
 
+
+    public PartitionGenerator getOfflineGenerator()
+    {
+        CFMetaData cfMetaData = CFMetaData.compile(tableCql, keyspaceName);
+
+        //Add missing column configs
+        Iterator<ColumnDefinition> it = cfMetaData.allColumnsInSelectOrder();
+        while (it.hasNext())
+        {
+            ColumnDefinition c = it.next();
+            if (!columnConfigs.containsKey(c.name.toString()))
+                columnConfigs.put(c.name.toString(), new GeneratorConfig(seedStr + c.name.toString(), null, null, null));
+        }
+
+        List<Generator> partitionColumns = cfMetaData.partitionKeyColumns().stream()
+                                                     .map(c -> new ColumnInfo(c.name.toString(), c.type.asCQL3Type().toString(), "", columnConfigs.get(c.name.toString())))
+                                                     .map(c -> c.getGenerator())
+                                                     .collect(Collectors.toList());
+
+        List<Generator> clusteringColumns = cfMetaData.clusteringColumns().stream()
+                                                             .map(c -> new ColumnInfo(c.name.toString(), c.type.asCQL3Type().toString(), "", columnConfigs.get(c.name.toString())))
+                                                             .map(c -> c.getGenerator())
+                                                             .collect(Collectors.toList());
+
+        List<Generator> regularColumns = com.google.common.collect.Lists.newArrayList(cfMetaData.partitionColumns().selectOrderIterator()).stream()
+                                                                                                             .map(c -> new ColumnInfo(c.name.toString(), c.type.asCQL3Type().toString(), "", columnConfigs.get(c.name.toString())))
+                                                                                                             .map(c -> c.getGenerator())
+                                                                                                             .collect(Collectors.toList());
+
+        return new PartitionGenerator(partitionColumns, clusteringColumns, regularColumns, PartitionGenerator.Order.ARBITRARY);
+    }
+
+    public CreateTableStatement.RawStatement getCreateStatement()
+    {
+        CreateTableStatement.RawStatement createStatement = QueryProcessor.parseStatement(tableCql, CreateTableStatement.RawStatement.class, "CREATE TABLE");
+        createStatement.prepareKeyspace(keyspaceName);
+
+        return createStatement;
+    }
+
+    public SchemaInsert getOfflineInsert(Timer timer, PartitionGenerator generator, SeedManager seedManager, StressSettings settings)
+    {
+        assert tableCql != null;
+
+        CFMetaData cfMetaData = CFMetaData.compile(tableCql, keyspaceName);
+
+        List<ColumnDefinition> allColumns = com.google.common.collect.Lists.newArrayList(cfMetaData.allColumnsInSelectOrder());
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("INSERT INTO ").append(quoteIdentifier(keyspaceName)).append(".").append(quoteIdentifier(tableName)).append(" (");
+        StringBuilder value = new StringBuilder();
+        for (ColumnDefinition c : allColumns)
+        {
+            sb.append(quoteIdentifier(c.name.toString())).append(", ");
+            value.append("?, ");
+        }
+        sb.delete(sb.lastIndexOf(","), sb.length());
+        value.delete(value.lastIndexOf(","), value.length());
+        sb.append(") ").append("values(").append(value).append(')');
+
+
+        if (insert == null)
+            insert = new HashMap<>();
+        lowerCase(insert);
+
+        partitions = select(settings.insert.batchsize, "partitions", "fixed(1)", insert, OptionDistribution.BUILDER);
+        selectchance = select(settings.insert.selectRatio, "select", "fixed(1)/1", insert, OptionRatioDistribution.BUILDER);
+        rowPopulation = select(settings.insert.rowPopulationRatio, "row-population", "fixed(1)/1", insert, OptionRatioDistribution.BUILDER);
+
+        if (generator.maxRowCount > 100 * 1000 * 1000)
+            System.err.printf("WARNING: You have defined a schema that permits very large partitions (%.0f max rows (>100M))%n", generator.maxRowCount);
+
+        String statement = sb.toString();
+
+        //CQLTableWriter requires the keyspace name be in the create statement
+        String tableCreate = tableCql.replaceFirst("\\s+\"?"+tableName+"\"?\\s+", " \""+keyspaceName+"\".\""+tableName+"\" ");
+
+
+        return new SchemaInsert(timer, settings, generator, seedManager, selectchance.get(), rowPopulation.get(), thriftInsertId, statement, tableCreate);
+    }
+
     public SchemaInsert getInsert(Timer timer, PartitionGenerator generator, SeedManager seedManager, StressSettings settings)
     {
         if (insertStatement == null)
@@ -374,41 +506,50 @@
                     maybeLoadSchemaInfo(settings);
 
                     Set<ColumnMetadata> keyColumns = com.google.common.collect.Sets.newHashSet(tableMetaData.getPrimaryKey());
+                    Set<ColumnMetadata> allColumns = com.google.common.collect.Sets.newHashSet(tableMetaData.getColumns());
+                    boolean isKeyOnlyTable = (keyColumns.size() == allColumns.size());
+                    //With compact storage
+                    if (!isKeyOnlyTable && (keyColumns.size() == (allColumns.size() - 1)))
+                    {
+                        com.google.common.collect.Sets.SetView diff = com.google.common.collect.Sets.difference(allColumns, keyColumns);
+                        for (Object obj : diff)
+                        {
+                            ColumnMetadata col = (ColumnMetadata)obj;
+                            isKeyOnlyTable = col.getName().isEmpty();
+                            break;
+                        }
+                    }
 
                     //Non PK Columns
                     StringBuilder sb = new StringBuilder();
-
-                    sb.append("UPDATE \"").append(tableName).append("\" SET ");
-
-                    //PK Columns
-                    StringBuilder pred = new StringBuilder();
-                    pred.append(" WHERE ");
-
-                    boolean firstCol = true;
-                    boolean firstPred = true;
-                    for (ColumnMetadata c : tableMetaData.getColumns())
+                    if (!isKeyOnlyTable)
                     {
+                        sb.append("UPDATE ").append(quoteIdentifier(tableName)).append(" SET ");
+                        //PK Columns
+                        StringBuilder pred = new StringBuilder();
+                        pred.append(" WHERE ");
 
-                        if (keyColumns.contains(c))
-                        {
-                            if (firstPred)
-                                firstPred = false;
-                            else
-                                pred.append(" AND ");
+                        boolean firstCol = true;
+                        boolean firstPred = true;
+                        for (ColumnMetadata c : tableMetaData.getColumns()) {
 
-                            pred.append(c.getName()).append(" = ?");
-                        }
-                        else
-                        {
-                            if (firstCol)
-                                firstCol = false;
-                            else
-                                sb.append(",");
+                            if (keyColumns.contains(c)) {
+                                if (firstPred)
+                                    firstPred = false;
+                                else
+                                    pred.append(" AND ");
 
-                            sb.append(c.getName()).append(" = ");
+                                pred.append(quoteIdentifier(c.getName())).append(" = ?");
+                            } else {
+                                if (firstCol)
+                                    firstCol = false;
+                                else
+                                    sb.append(',');
 
-                            switch (c.getType().getName())
-                            {
+                                sb.append(quoteIdentifier(c.getName())).append(" = ");
+
+                                switch (c.getType().getName())
+                                {
                                 case SET:
                                 case LIST:
                                     if (c.getType().isFrozen())
@@ -417,17 +558,31 @@
                                         break;
                                     }
                                 case COUNTER:
-                                    sb.append(c.getName()).append(" + ?");
+                                    sb.append(quoteIdentifier(c.getName())).append(" + ?");
                                     break;
                                 default:
                                     sb.append("?");
                                     break;
+                                }
                             }
                         }
-                    }
 
-                    //Put PK predicates at the end
-                    sb.append(pred);
+                        //Put PK predicates at the end
+                        sb.append(pred);
+                    }
+                    else
+                    {
+                        sb.append("INSERT INTO ").append(quoteIdentifier(tableName)).append(" (");
+                        StringBuilder value = new StringBuilder();
+                        for (ColumnMetadata c : tableMetaData.getPrimaryKey())
+                        {
+                            sb.append(quoteIdentifier(c.getName())).append(", ");
+                            value.append("?, ");
+                        }
+                        sb.delete(sb.lastIndexOf(","), sb.length());
+                        value.delete(value.lastIndexOf(","), value.length());
+                        sb.append(") ").append("values(").append(value).append(')');
+                    }
 
                     if (insert == null)
                         insert = new HashMap<>();
@@ -449,11 +604,7 @@
                     // guarantee the vast majority of actions occur in these bounds
                     double minBatchSize = selectchance.get().min() * partitions.get().minValue() * generator.minRowCount * (1d / visits.maxValue());
                     double maxBatchSize = selectchance.get().max() * partitions.get().maxValue() * generator.maxRowCount * (1d / visits.minValue());
-                    System.out.printf("Generating batches with [%d..%d] partitions and [%.0f..%.0f] rows (of [%.0f..%.0f] total rows in the partitions)%n",
-                                      partitions.get().minValue(), partitions.get().maxValue(),
-                                      minBatchSize, maxBatchSize,
-                                      partitions.get().minValue() * generator.minRowCount,
-                                      partitions.get().maxValue() * generator.maxRowCount);
+
                     if (generator.maxRowCount > 100 * 1000 * 1000)
                         System.err.printf("WARNING: You have defined a schema that permits very large partitions (%.0f max rows (>100M))%n", generator.maxRowCount);
                     if (batchType == BatchStatement.Type.LOGGED && maxBatchSize > 65535)
@@ -517,7 +668,7 @@
             return first;
         if (val != null && val.trim().length() > 0)
             return builder.apply(val);
-        
+
         return builder.apply(defValue);
     }
 
@@ -527,6 +678,7 @@
         {
             synchronized (this)
             {
+                maybeCreateSchema(settings);
                 maybeLoadSchemaInfo(settings);
                 if (generatorFactory == null)
                     generatorFactory = new GeneratorFactory();
@@ -547,12 +699,18 @@
             Set<ColumnMetadata> keyColumns = com.google.common.collect.Sets.newHashSet(tableMetaData.getPrimaryKey());
 
             for (ColumnMetadata metadata : tableMetaData.getPartitionKey())
-                partitionKeys.add(new ColumnInfo(metadata.getName(), metadata.getType(), columnConfigs.get(metadata.getName())));
+                partitionKeys.add(new ColumnInfo(metadata.getName(), metadata.getType().getName().toString(),
+                                                 metadata.getType().isCollection() ? metadata.getType().getTypeArguments().get(0).getName().toString() : "",
+                                                 columnConfigs.get(metadata.getName())));
             for (ColumnMetadata metadata : tableMetaData.getClusteringColumns())
-                clusteringColumns.add(new ColumnInfo(metadata.getName(), metadata.getType(), columnConfigs.get(metadata.getName())));
+                clusteringColumns.add(new ColumnInfo(metadata.getName(), metadata.getType().getName().toString(),
+                                                     metadata.getType().isCollection() ? metadata.getType().getTypeArguments().get(0).getName().toString() : "",
+                                                     columnConfigs.get(metadata.getName())));
             for (ColumnMetadata metadata : tableMetaData.getColumns())
                 if (!keyColumns.contains(metadata))
-                    valueColumns.add(new ColumnInfo(metadata.getName(), metadata.getType(), columnConfigs.get(metadata.getName())));
+                    valueColumns.add(new ColumnInfo(metadata.getName(), metadata.getType().getName().toString(),
+                                                    metadata.getType().isCollection() ? metadata.getType().getTypeArguments().get(0).getName().toString() : "",
+                                                    columnConfigs.get(metadata.getName())));
         }
 
         PartitionGenerator newGenerator(StressSettings settings)
@@ -572,66 +730,68 @@
     static class ColumnInfo
     {
         final String name;
-        final DataType type;
+        final String type;
+        final String collectionType;
         final GeneratorConfig config;
 
-        ColumnInfo(String name, DataType type, GeneratorConfig config)
+        ColumnInfo(String name, String type, String collectionType, GeneratorConfig config)
         {
             this.name = name;
             this.type = type;
+            this.collectionType = collectionType;
             this.config = config;
         }
 
         Generator getGenerator()
         {
-            return getGenerator(name, type, config);
+            return getGenerator(name, type, collectionType, config);
         }
 
-        static Generator getGenerator(final String name, final DataType type, GeneratorConfig config)
+        static Generator getGenerator(final String name, final String type, final String collectionType, GeneratorConfig config)
         {
-            switch (type.getName())
+            switch (type.toUpperCase())
             {
-                case ASCII:
-                case TEXT:
-                case VARCHAR:
+                case "ASCII":
+                case "TEXT":
+                case "VARCHAR":
                     return new Strings(name, config);
-                case BIGINT:
-                case COUNTER:
+                case "BIGINT":
+                case "COUNTER":
                     return new Longs(name, config);
-                case BLOB:
+                case "BLOB":
                     return new Bytes(name, config);
-                case BOOLEAN:
+                case "BOOLEAN":
                     return new Booleans(name, config);
-                case DECIMAL:
+                case "DECIMAL":
                     return new BigDecimals(name, config);
-                case DOUBLE:
+                case "DOUBLE":
                     return new Doubles(name, config);
-                case FLOAT:
+                case "FLOAT":
                     return new Floats(name, config);
-                case INET:
+                case "INET":
                     return new Inets(name, config);
-                case INT:
+                case "INT":
                     return new Integers(name, config);
-                case VARINT:
+                case "VARINT":
                     return new BigIntegers(name, config);
-                case TIMESTAMP:
+                case "TIMESTAMP":
                     return new Dates(name, config);
-                case UUID:
+                case "UUID":
                     return new UUIDs(name, config);
-                case TIMEUUID:
+                case "TIMEUUID":
                     return new TimeUUIDs(name, config);
-                case TINYINT:
+                case "TINYINT":
                     return new TinyInts(name, config);
-                case SMALLINT:
+                case "SMALLINT":
                     return new SmallInts(name, config);
-                case TIME:
+                case "TIME":
                     return new Times(name, config);
-                case DATE:
+                case "DATE":
                     return new LocalDates(name, config);
-                case SET:
-                    return new Sets(name, getGenerator(name, type.getTypeArguments().get(0), config), config);
-                case LIST:
-                    return new Lists(name, getGenerator(name, type.getTypeArguments().get(0), config), config);
+                case "SET":
+                    return new Sets(name, getGenerator(name, collectionType, null, config), config);
+                case "LIST":
+                    return new Lists(name, getGenerator(name, collectionType, null, config), config);
                 default:
                     throw new UnsupportedOperationException("Because of this name: "+name+" if you removed it from the yaml and are still seeing this, make sure to drop table");
             }
@@ -680,4 +840,10 @@
         for (Map.Entry<String, V> e : reinsert)
             map.put(e.getKey().toLowerCase(), e.getValue());
     }
+
+    /* Quote a identifier if it contains uppercase letters */
+    private static String quoteIdentifier(String identifier)
+    {
+        return lowercaseAlphanumeric.matcher(identifier).matches() ? identifier : '\"'+identifier+ '\"';
+    }
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/StressServer.java b/tools/stress/src/org/apache/cassandra/stress/StressServer.java
index a6dfaf4..c00fb54 100644
--- a/tools/stress/src/org/apache/cassandra/stress/StressServer.java
+++ b/tools/stress/src/org/apache/cassandra/stress/StressServer.java
@@ -23,10 +23,14 @@
 import java.net.InetAddress;
 import java.net.ServerSocket;
 import java.net.Socket;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.commons.cli.*;
 
+import org.apache.cassandra.concurrent.NamedThreadFactory;
 import org.apache.cassandra.stress.settings.StressSettings;
+import org.apache.cassandra.stress.util.MultiResultLogger;
+import org.apache.cassandra.stress.util.ResultLogger;
 
 public class StressServer
 {
@@ -37,6 +41,8 @@
         availableOptions.addOption("h", "host", true, "Host to listen for connections.");
     }
 
+    private static final AtomicInteger threadCounter = new AtomicInteger(1);
+
     public static void main(String[] args) throws Exception
     {
         ServerSocket serverSocket = null;
@@ -88,9 +94,10 @@
             {
                 ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
                 PrintStream out = new PrintStream(socket.getOutputStream());
+                ResultLogger log = new MultiResultLogger(out);
 
-                StressAction action = new StressAction((StressSettings) in.readObject(), out);
-                Thread actionThread = new Thread(action);
+                StressAction action = new StressAction((StressSettings) in.readObject(), log);
+                Thread actionThread = NamedThreadFactory.createThread(action, "stress-" + threadCounter.incrementAndGet());
                 actionThread.start();
 
                 while (actionThread.isAlive())
diff --git a/tools/stress/src/org/apache/cassandra/stress/StressYaml.java b/tools/stress/src/org/apache/cassandra/stress/StressYaml.java
index 214e56a..6727f62 100644
--- a/tools/stress/src/org/apache/cassandra/stress/StressYaml.java
+++ b/tools/stress/src/org/apache/cassandra/stress/StressYaml.java
@@ -42,12 +42,20 @@
     {
         public String cql;
         public String fields;
+        public String getConfigAsString()
+        {
+            return String.format("CQL:%s;Fields:%s;", cql, fields);
+        }
     }
 
     public static class TokenRangeQueryDef
     {
         public String columns;
         public int page_size = 5000;
+        public String getConfigAsString()
+        {
+            return String.format("Columns:%s;", columns);
+        }
     }
 
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/generate/DistributionFactory.java b/tools/stress/src/org/apache/cassandra/stress/generate/DistributionFactory.java
index d0dfa89..4fa46fa 100644
--- a/tools/stress/src/org/apache/cassandra/stress/generate/DistributionFactory.java
+++ b/tools/stress/src/org/apache/cassandra/stress/generate/DistributionFactory.java
@@ -27,5 +27,6 @@
 {
 
     Distribution get();
+    String getConfigAsString();
 
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/generate/DistributionSequence.java b/tools/stress/src/org/apache/cassandra/stress/generate/DistributionSequence.java
new file mode 100644
index 0000000..0546897
--- /dev/null
+++ b/tools/stress/src/org/apache/cassandra/stress/generate/DistributionSequence.java
@@ -0,0 +1,72 @@
+/*
+ *
+ * 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.stress.generate;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+public class DistributionSequence extends Distribution
+{
+
+    private final long start;
+    private final long totalCount;
+    private final AtomicLong next = new AtomicLong();
+
+    public DistributionSequence(long start, long end)
+    {
+        if (start > end)
+            throw new IllegalStateException();
+        this.start = start;
+        this.totalCount = 1 + end - start;
+    }
+
+    private long nextWithWrap()
+    {
+        long next = this.next.getAndIncrement();
+        return start + (next % totalCount);
+    }
+
+    @Override
+    public long next()
+    {
+        return nextWithWrap();
+    }
+
+    @Override
+    public double nextDouble()
+    {
+        return nextWithWrap();
+    }
+
+    @Override
+    public long inverseCumProb(double cumProb)
+    {
+        return (long) (start + (totalCount-1) * cumProb);
+    }
+
+    @Override
+    public void setSeed(long seed)
+    {
+        next.set(seed);
+    }
+
+}
+
diff --git a/tools/stress/src/org/apache/cassandra/stress/generate/PartitionGenerator.java b/tools/stress/src/org/apache/cassandra/stress/generate/PartitionGenerator.java
index a7297c5..1230065 100644
--- a/tools/stress/src/org/apache/cassandra/stress/generate/PartitionGenerator.java
+++ b/tools/stress/src/org/apache/cassandra/stress/generate/PartitionGenerator.java
@@ -22,10 +22,8 @@
 
 
 import java.nio.ByteBuffer;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.NoSuchElementException;
+import java.util.*;
+import java.util.stream.Collectors;
 
 import com.google.common.collect.Iterables;
 
@@ -71,7 +69,7 @@
         }
         this.maxRowCount = maxRowCount;
         this.minRowCount = minRowCount;
-        this.indexMap = new HashMap<>();
+        this.indexMap = new LinkedHashMap<>();
         int i = 0;
         for (Generator generator : partitionKey)
             indexMap.put(generator.name, --i);
@@ -110,4 +108,9 @@
             return clusteringComponents.get(c).type.compose(v);
         return valueComponents.get(c - clusteringComponents.size()).type.compose(v);
     }
+
+    public List<String> getColumnNames()
+    {
+        return indexMap.keySet().stream().collect(Collectors.toList());
+    }
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/generate/RatioDistributionFactory.java b/tools/stress/src/org/apache/cassandra/stress/generate/RatioDistributionFactory.java
index 16474d8..5a01d04 100644
--- a/tools/stress/src/org/apache/cassandra/stress/generate/RatioDistributionFactory.java
+++ b/tools/stress/src/org/apache/cassandra/stress/generate/RatioDistributionFactory.java
@@ -27,5 +27,6 @@
 {
 
     RatioDistribution get();
+    String getConfigAsString();
 
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/generate/values/GeneratorConfig.java b/tools/stress/src/org/apache/cassandra/stress/generate/values/GeneratorConfig.java
index 3522338..52977db 100644
--- a/tools/stress/src/org/apache/cassandra/stress/generate/values/GeneratorConfig.java
+++ b/tools/stress/src/org/apache/cassandra/stress/generate/values/GeneratorConfig.java
@@ -62,4 +62,18 @@
         return (sizeDistributions == null ? deflt : sizeDistributions).get();
     }
 
+    public String getConfigAsString()
+    {
+        StringBuilder sb = new StringBuilder();
+        if (clusteringDistributions != null){
+            sb.append(String.format("Clustering: %s;", clusteringDistributions.getConfigAsString()));
+        }
+        if (sizeDistributions != null){
+            sb.append(String.format("Size: %s;", sizeDistributions.getConfigAsString()));
+        }
+        if (identityDistributions != null){
+            sb.append(String.format("Identity: %s;", identityDistributions.getConfigAsString()));
+        }
+        return sb.toString();
+    }
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/generate/values/LocalDates.java b/tools/stress/src/org/apache/cassandra/stress/generate/values/LocalDates.java
index f079d35..e5f7703 100644
--- a/tools/stress/src/org/apache/cassandra/stress/generate/values/LocalDates.java
+++ b/tools/stress/src/org/apache/cassandra/stress/generate/values/LocalDates.java
@@ -20,11 +20,7 @@
  */
 package org.apache.cassandra.stress.generate.values;
 
-import com.datastax.driver.core.LocalDate;
 import org.apache.cassandra.db.marshal.SimpleDateType;
-import org.joda.time.DateTimeZone;
-import org.joda.time.format.DateTimeFormat;
-import org.joda.time.format.DateTimeFormatter;
 
 public class LocalDates extends Generator<Integer>
 {
diff --git a/tools/stress/src/org/apache/cassandra/stress/generate/values/SmallInts.java b/tools/stress/src/org/apache/cassandra/stress/generate/values/SmallInts.java
index 702b6dc..a89fe12 100644
--- a/tools/stress/src/org/apache/cassandra/stress/generate/values/SmallInts.java
+++ b/tools/stress/src/org/apache/cassandra/stress/generate/values/SmallInts.java
@@ -20,7 +20,6 @@
  */
 package org.apache.cassandra.stress.generate.values;
 
-import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.ShortType;
 
 public class SmallInts extends Generator<Short>
diff --git a/tools/stress/src/org/apache/cassandra/stress/generate/values/Times.java b/tools/stress/src/org/apache/cassandra/stress/generate/values/Times.java
index 35bac86..b33d6de 100644
--- a/tools/stress/src/org/apache/cassandra/stress/generate/values/Times.java
+++ b/tools/stress/src/org/apache/cassandra/stress/generate/values/Times.java
@@ -20,7 +20,6 @@
  */
 package org.apache.cassandra.stress.generate.values;
 
-import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.TimeType;
 
 public class Times extends Generator<Long>
diff --git a/tools/stress/src/org/apache/cassandra/stress/generate/values/TinyInts.java b/tools/stress/src/org/apache/cassandra/stress/generate/values/TinyInts.java
index 0fe3f35..e5d46ec 100644
--- a/tools/stress/src/org/apache/cassandra/stress/generate/values/TinyInts.java
+++ b/tools/stress/src/org/apache/cassandra/stress/generate/values/TinyInts.java
@@ -20,15 +20,7 @@
  */
 package org.apache.cassandra.stress.generate.values;
 
-import java.math.BigDecimal;
-import java.nio.ByteBuffer;
-import java.util.Arrays;
-
 import org.apache.cassandra.db.marshal.ByteType;
-import org.apache.cassandra.db.marshal.DecimalType;
-import org.apache.cassandra.db.marshal.IntegerType;
-import org.apache.cassandra.db.marshal.ShortType;
-import org.apache.cassandra.stress.generate.FasterRandom;
 
 public class TinyInts extends Generator<Byte>
 {
diff --git a/tools/stress/src/org/apache/cassandra/stress/generate/values/UUIDs.java b/tools/stress/src/org/apache/cassandra/stress/generate/values/UUIDs.java
index faa58c6..b90bb74 100644
--- a/tools/stress/src/org/apache/cassandra/stress/generate/values/UUIDs.java
+++ b/tools/stress/src/org/apache/cassandra/stress/generate/values/UUIDs.java
@@ -34,6 +34,7 @@
     @Override
     public UUID generate()
     {
-        return new UUID(identityDistribution.next(), identityDistribution.next());
+        long seed = identityDistribution.next();
+        return new UUID(seed, seed);
     }
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/FixedOpDistribution.java b/tools/stress/src/org/apache/cassandra/stress/operations/FixedOpDistribution.java
index f2616cf..aa5ddfd 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/FixedOpDistribution.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/FixedOpDistribution.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.stress.operations;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
@@ -36,14 +36,4 @@
     {
         return operation;
     }
-
-    public void initTimers()
-    {
-        operation.timer.init();
-    }
-
-    public void closeTimers()
-    {
-        operation.timer.close();
-    }
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/OpDistribution.java b/tools/stress/src/org/apache/cassandra/stress/operations/OpDistribution.java
index e09300a..bef8954 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/OpDistribution.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/OpDistribution.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.stress.operations;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
@@ -25,9 +25,5 @@
 
 public interface OpDistribution
 {
-
     Operation next();
-
-    public void initTimers();
-    public void closeTimers();
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/OpDistributionFactory.java b/tools/stress/src/org/apache/cassandra/stress/operations/OpDistributionFactory.java
index 5fbb0f9..2477c36 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/OpDistributionFactory.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/OpDistributionFactory.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.stress.operations;
 /*
- * 
+ *
  * 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
@@ -8,24 +8,24 @@
  * 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.
- * 
+ *
  */
 
 
-import org.apache.cassandra.stress.util.Timing;
+import org.apache.cassandra.stress.StressAction.MeasurementSink;
 
 public interface OpDistributionFactory
 {
-    public OpDistribution get(Timing timing, int sampleCount, boolean isWarmup);
+    public OpDistribution get(boolean isWarmup, MeasurementSink sink);
     public String desc();
     Iterable<OpDistributionFactory> each();
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/PartitionOperation.java b/tools/stress/src/org/apache/cassandra/stress/operations/PartitionOperation.java
index 45c36f2..bad0a94 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/PartitionOperation.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/PartitionOperation.java
@@ -21,8 +21,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import com.google.common.util.concurrent.RateLimiter;
-
 import org.apache.cassandra.stress.Operation;
 import org.apache.cassandra.stress.WorkManager;
 import org.apache.cassandra.stress.generate.Distribution;
@@ -31,9 +29,9 @@
 import org.apache.cassandra.stress.generate.RatioDistribution;
 import org.apache.cassandra.stress.generate.Seed;
 import org.apache.cassandra.stress.generate.SeedManager;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.OptionRatioDistribution;
 import org.apache.cassandra.stress.settings.StressSettings;
-import org.apache.cassandra.stress.util.Timer;
 
 public abstract class PartitionOperation extends Operation
 {
@@ -77,14 +75,14 @@
         this.spec = spec;
     }
 
-    public boolean ready(WorkManager permits, RateLimiter rateLimiter)
+    public int ready(WorkManager permits)
     {
         int partitionCount = (int) spec.partitionCount.next();
         if (partitionCount <= 0)
-            return false;
+            return 0;
         partitionCount = permits.takePermits(partitionCount);
         if (partitionCount <= 0)
-            return false;
+            return 0;
 
         int i = 0;
         boolean success = true;
@@ -105,11 +103,8 @@
         }
         partitionCount = i;
 
-        if (rateLimiter != null)
-            rateLimiter.acquire(partitionCount);
-
         partitions = partitionCache.subList(0, partitionCount);
-        return !partitions.isEmpty();
+        return partitions.size();
     }
 
     protected boolean reset(Seed seed, PartitionIterator iterator)
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/SampledOpDistribution.java b/tools/stress/src/org/apache/cassandra/stress/operations/SampledOpDistribution.java
index 9698421..6d7f9e4 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/SampledOpDistribution.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/SampledOpDistribution.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.stress.operations;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
@@ -51,20 +51,4 @@
         remaining--;
         return cur;
     }
-
-    public void initTimers()
-    {
-        for (Pair<Operation, Double> op : operations.getPmf())
-        {
-            op.getFirst().timer.init();
-        }
-    }
-
-    public void closeTimers()
-    {
-        for (Pair<Operation, Double> op : operations.getPmf())
-        {
-            op.getFirst().timer.close();
-        }
-    }
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/SampledOpDistributionFactory.java b/tools/stress/src/org/apache/cassandra/stress/operations/SampledOpDistributionFactory.java
index a10585d..1800039 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/SampledOpDistributionFactory.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/SampledOpDistributionFactory.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.stress.operations;
 /*
- * 
+ *
  * 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
@@ -8,30 +8,32 @@
  * 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.
- * 
+ *
  */
 
 
-import java.util.*;
-
-import org.apache.cassandra.stress.generate.*;
-import org.apache.cassandra.stress.util.Timing;
-import org.apache.commons.math3.distribution.EnumeratedDistribution;
-import org.apache.commons.math3.util.Pair;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 
 import org.apache.cassandra.stress.Operation;
+import org.apache.cassandra.stress.StressAction.MeasurementSink;
 import org.apache.cassandra.stress.generate.DistributionFactory;
+import org.apache.cassandra.stress.generate.DistributionFixed;
 import org.apache.cassandra.stress.generate.PartitionGenerator;
-import org.apache.cassandra.stress.util.Timer;
+import org.apache.cassandra.stress.report.Timer;
+import org.apache.commons.math3.distribution.EnumeratedDistribution;
+import org.apache.commons.math3.util.Pair;
 
 public abstract class SampledOpDistributionFactory<T> implements OpDistributionFactory
 {
@@ -47,13 +49,13 @@
     protected abstract List<? extends Operation> get(Timer timer, PartitionGenerator generator, T key, boolean isWarmup);
     protected abstract PartitionGenerator newGenerator();
 
-    public OpDistribution get(Timing timing, int sampleCount, boolean isWarmup)
+    public OpDistribution get(boolean isWarmup, MeasurementSink sink)
     {
         PartitionGenerator generator = newGenerator();
         List<Pair<Operation, Double>> operations = new ArrayList<>();
         for (Map.Entry<T, Double> ratio : ratios.entrySet())
         {
-            List<? extends Operation> ops = get(timing.newTimer(ratio.getKey().toString(), sampleCount),
+            List<? extends Operation> ops = get(new Timer(ratio.getKey().toString(), sink),
                                                 generator, ratio.getKey(), isWarmup);
             for (Operation op : ops)
                 operations.add(new Pair<>(op, ratio.getValue() / ops.size()));
@@ -76,9 +78,9 @@
         {
             out.add(new OpDistributionFactory()
             {
-                public OpDistribution get(Timing timing, int sampleCount, boolean isWarmup)
+                public OpDistribution get(boolean isWarmup, MeasurementSink sink)
                 {
-                    List<? extends Operation> ops = SampledOpDistributionFactory.this.get(timing.newTimer(ratio.getKey().toString(), sampleCount),
+                    List<? extends Operation> ops = SampledOpDistributionFactory.this.get(new Timer(ratio.getKey().toString(), sink),
                                                                                           newGenerator(),
                                                                                           ratio.getKey(),
                                                                                           isWarmup);
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlCounterAdder.java b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlCounterAdder.java
index bb8135c..2874b4e 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlCounterAdder.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlCounterAdder.java
@@ -29,9 +29,9 @@
 import org.apache.cassandra.stress.generate.DistributionFactory;
 import org.apache.cassandra.stress.generate.PartitionGenerator;
 import org.apache.cassandra.stress.generate.SeedManager;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.Command;
 import org.apache.cassandra.stress.settings.StressSettings;
-import org.apache.cassandra.stress.util.Timer;
 
 public class CqlCounterAdder extends CqlOperation<Integer>
 {
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlCounterGetter.java b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlCounterGetter.java
index d91ab37..eb908b0 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlCounterGetter.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlCounterGetter.java
@@ -27,9 +27,9 @@
 
 import org.apache.cassandra.stress.generate.PartitionGenerator;
 import org.apache.cassandra.stress.generate.SeedManager;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.Command;
 import org.apache.cassandra.stress.settings.StressSettings;
-import org.apache.cassandra.stress.util.Timer;
 
 public class CqlCounterGetter extends CqlOperation<Integer>
 {
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlInserter.java b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlInserter.java
index fdf5007..3fda6c2 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlInserter.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlInserter.java
@@ -27,9 +27,9 @@
 
 import org.apache.cassandra.stress.generate.PartitionGenerator;
 import org.apache.cassandra.stress.generate.SeedManager;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.Command;
 import org.apache.cassandra.stress.settings.StressSettings;
-import org.apache.cassandra.stress.util.Timer;
 
 public class CqlInserter extends CqlOperation<Integer>
 {
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlOperation.java b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlOperation.java
index afdc0b1..807bb49 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlOperation.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlOperation.java
@@ -31,12 +31,12 @@
 import com.datastax.driver.core.Row;
 import org.apache.cassandra.stress.generate.PartitionGenerator;
 import org.apache.cassandra.stress.generate.SeedManager;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.Command;
 import org.apache.cassandra.stress.settings.ConnectionStyle;
 import org.apache.cassandra.stress.settings.StressSettings;
 import org.apache.cassandra.stress.util.JavaDriverClient;
 import org.apache.cassandra.stress.util.ThriftClient;
-import org.apache.cassandra.stress.util.Timer;
 import org.apache.cassandra.thrift.Compression;
 import org.apache.cassandra.thrift.CqlResult;
 import org.apache.cassandra.thrift.CqlRow;
@@ -49,6 +49,9 @@
 public abstract class CqlOperation<V> extends PredefinedOperation
 {
 
+    public static final ByteBuffer[][] EMPTY_BYTE_BUFFERS = new ByteBuffer[0][];
+    public static final byte[][] EMPTY_BYTE_ARRAYS = new byte[0][];
+
     protected abstract List<Object> getQueryParameters(byte[] key);
     protected abstract String buildQuery();
     protected abstract CqlRunOp<V> buildRunOp(ClientWrapper client, String query, Object queryId, List<Object> params, ByteBuffer key);
@@ -193,11 +196,14 @@
 
         public boolean validate(ByteBuffer[][] result)
         {
-            if (result.length != expect.size())
-                return false;
-            for (int i = 0 ; i < result.length ; i++)
-                if (expect.get(i) != null && !expect.get(i).equals(Arrays.asList(result[i])))
+            if (!settings.errors.skipReadValidation)
+            {
+                if (result.length != expect.size())
                     return false;
+                for (int i = 0; i < result.length; i++)
+                    if (expect.get(i) != null && !expect.get(i).equals(Arrays.asList(result[i])))
+                        return false;
+            }
             return true;
         }
     }
@@ -455,7 +461,7 @@
                 public ByteBuffer[][] apply(ResultSet result)
                 {
                     if (result == null)
-                        return new ByteBuffer[0][];
+                        return EMPTY_BYTE_BUFFERS;
                     List<Row> rows = result.all();
 
                     ByteBuffer[][] r = new ByteBuffer[rows.size()][];
@@ -481,7 +487,7 @@
                 public ByteBuffer[][] apply(ResultMessage result)
                 {
                     if (!(result instanceof ResultMessage.Rows))
-                        return new ByteBuffer[0][];
+                        return EMPTY_BYTE_BUFFERS;
 
                     ResultMessage.Rows rows = ((ResultMessage.Rows) result);
                     ByteBuffer[][] r = new ByteBuffer[rows.result.size()][];
@@ -536,7 +542,7 @@
                 {
 
                     if (result == null)
-                        return new byte[0][];
+                        return EMPTY_BYTE_ARRAYS;
                     List<Row> rows = result.all();
                     byte[][] r = new byte[rows.size()][];
                     for (int i = 0 ; i < r.length ; i++)
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlReader.java b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlReader.java
index 0ee17e9..61c6553 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlReader.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/CqlReader.java
@@ -28,9 +28,9 @@
 
 import org.apache.cassandra.stress.generate.PartitionGenerator;
 import org.apache.cassandra.stress.generate.SeedManager;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.Command;
 import org.apache.cassandra.stress.settings.StressSettings;
-import org.apache.cassandra.stress.util.Timer;
 
 public class CqlReader extends CqlOperation<ByteBuffer[][]>
 {
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/PredefinedOperation.java b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/PredefinedOperation.java
index 3767401..db35504 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/PredefinedOperation.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/PredefinedOperation.java
@@ -26,15 +26,16 @@
 import org.apache.cassandra.stress.Operation;
 import org.apache.cassandra.stress.generate.*;
 import org.apache.cassandra.stress.operations.PartitionOperation;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.Command;
 import org.apache.cassandra.stress.settings.CqlVersion;
 import org.apache.cassandra.stress.settings.StressSettings;
-import org.apache.cassandra.stress.util.Timer;
 import org.apache.cassandra.thrift.SlicePredicate;
 import org.apache.cassandra.thrift.SliceRange;
 
 public abstract class PredefinedOperation extends PartitionOperation
 {
+    public static final byte[] EMPTY_BYTE_ARRAY = {};
     public final Command type;
     private final Distribution columnCount;
     private Object cqlCache;
@@ -107,7 +108,7 @@
             {
                 predicate.setSlice_range(new SliceRange()
                                          .setStart(settings.columns.names.get(lb))
-                                         .setFinish(new byte[] {})
+                                         .setFinish(EMPTY_BYTE_ARRAY)
                                          .setReversed(false)
                                          .setCount(count())
                 );
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftCounterAdder.java b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftCounterAdder.java
index be34a07..42f8bc9 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftCounterAdder.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftCounterAdder.java
@@ -28,10 +28,10 @@
 import org.apache.cassandra.stress.generate.DistributionFactory;
 import org.apache.cassandra.stress.generate.PartitionGenerator;
 import org.apache.cassandra.stress.generate.SeedManager;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.Command;
 import org.apache.cassandra.stress.settings.StressSettings;
 import org.apache.cassandra.stress.util.ThriftClient;
-import org.apache.cassandra.stress.util.Timer;
 import org.apache.cassandra.thrift.ColumnOrSuperColumn;
 import org.apache.cassandra.thrift.CounterColumn;
 import org.apache.cassandra.thrift.Mutation;
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftCounterGetter.java b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftCounterGetter.java
index ca81fe9..4bec3b2 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftCounterGetter.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftCounterGetter.java
@@ -23,10 +23,10 @@
 
 import org.apache.cassandra.stress.generate.PartitionGenerator;
 import org.apache.cassandra.stress.generate.SeedManager;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.Command;
 import org.apache.cassandra.stress.settings.StressSettings;
 import org.apache.cassandra.stress.util.ThriftClient;
-import org.apache.cassandra.stress.util.Timer;
 import org.apache.cassandra.thrift.ColumnParent;
 import org.apache.cassandra.thrift.SlicePredicate;
 
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftInserter.java b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftInserter.java
index 1827c06..ecaa140 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftInserter.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftInserter.java
@@ -26,10 +26,10 @@
 
 import org.apache.cassandra.stress.generate.PartitionGenerator;
 import org.apache.cassandra.stress.generate.SeedManager;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.Command;
 import org.apache.cassandra.stress.settings.StressSettings;
 import org.apache.cassandra.stress.util.ThriftClient;
-import org.apache.cassandra.stress.util.Timer;
 import org.apache.cassandra.thrift.Column;
 import org.apache.cassandra.thrift.ColumnOrSuperColumn;
 import org.apache.cassandra.thrift.Mutation;
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftReader.java b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftReader.java
index d77dc6a..4d530b9 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftReader.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/predefined/ThriftReader.java
@@ -23,10 +23,10 @@
 
 import org.apache.cassandra.stress.generate.PartitionGenerator;
 import org.apache.cassandra.stress.generate.SeedManager;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.Command;
 import org.apache.cassandra.stress.settings.StressSettings;
 import org.apache.cassandra.stress.util.ThriftClient;
-import org.apache.cassandra.stress.util.Timer;
 import org.apache.cassandra.thrift.ColumnOrSuperColumn;
 import org.apache.cassandra.thrift.ColumnParent;
 
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/SchemaInsert.java b/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/SchemaInsert.java
index d9fcac8..2c717a1 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/SchemaInsert.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/SchemaInsert.java
@@ -24,27 +24,46 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import com.datastax.driver.core.BatchStatement;
 import com.datastax.driver.core.BoundStatement;
 import com.datastax.driver.core.PreparedStatement;
 import com.datastax.driver.core.Statement;
+import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.io.sstable.StressCQLSSTableWriter;
+import org.apache.cassandra.stress.WorkManager;
 import org.apache.cassandra.stress.generate.*;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.StressSettings;
 import org.apache.cassandra.stress.util.JavaDriverClient;
 import org.apache.cassandra.stress.util.ThriftClient;
-import org.apache.cassandra.stress.util.Timer;
 
 public class SchemaInsert extends SchemaStatement
 {
 
+    private final String tableSchema;
+    private final String insertStatement;
     private final BatchStatement.Type batchType;
 
     public SchemaInsert(Timer timer, StressSettings settings, PartitionGenerator generator, SeedManager seedManager, Distribution batchSize, RatioDistribution useRatio, RatioDistribution rowPopulation, Integer thriftId, PreparedStatement statement, ConsistencyLevel cl, BatchStatement.Type batchType)
     {
-        super(timer, settings, new DataSpec(generator, seedManager, batchSize, useRatio, rowPopulation), statement, thriftId, cl);
+        super(timer, settings, new DataSpec(generator, seedManager, batchSize, useRatio, rowPopulation), statement, statement.getVariables().asList().stream().map(d -> d.getName()).collect(Collectors.toList()), thriftId, cl);
         this.batchType = batchType;
+        this.insertStatement = null;
+        this.tableSchema = null;
+    }
+
+    /**
+     * Special constructor for offline use
+     */
+    public SchemaInsert(Timer timer, StressSettings settings, PartitionGenerator generator, SeedManager seedManager, RatioDistribution useRatio, RatioDistribution rowPopulation, Integer thriftId, String statement, String tableSchema)
+    {
+        super(timer, settings, new DataSpec(generator, seedManager, new DistributionFixed(1), useRatio, rowPopulation), null, generator.getColumnNames(), thriftId, ConsistencyLevel.ONE);
+        this.batchType = BatchStatement.Type.UNLOGGED;
+        this.insertStatement = statement;
+        this.tableSchema = tableSchema;
     }
 
     private class JavaDriverRun extends Runner
@@ -113,6 +132,31 @@
         }
     }
 
+    private class OfflineRun extends Runner
+    {
+        final StressCQLSSTableWriter writer;
+
+        OfflineRun(StressCQLSSTableWriter writer)
+        {
+            this.writer = writer;
+        }
+
+        public boolean run() throws Exception
+        {
+            for (PartitionIterator iterator : partitions)
+            {
+                while (iterator.hasNext())
+                {
+                    Row row = iterator.next();
+                    writer.rawAddRow(thriftRowArgs(row));
+                    rowCount += 1;
+                }
+            }
+
+            return true;
+        }
+    }
+
     @Override
     public void run(JavaDriverClient client) throws IOException
     {
@@ -130,4 +174,27 @@
         timeWithRetry(new ThriftRun(client));
     }
 
+    public StressCQLSSTableWriter createWriter(ColumnFamilyStore cfs, int bufferSize, boolean makeRangeAware)
+    {
+        return StressCQLSSTableWriter.builder()
+                               .withCfs(cfs)
+                               .withBufferSizeInMB(bufferSize)
+                               .forTable(tableSchema)
+                               .using(insertStatement)
+                               .rangeAware(makeRangeAware)
+                               .build();
+    }
+
+    public void runOffline(StressCQLSSTableWriter writer, WorkManager workManager) throws Exception
+    {
+        OfflineRun offline = new OfflineRun(writer);
+
+        while (true)
+        {
+            if (ready(workManager) == 0)
+                break;
+
+            offline.run();
+        }
+    }
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/SchemaQuery.java b/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/SchemaQuery.java
index 9b5c4ae..2764704 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/SchemaQuery.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/SchemaQuery.java
@@ -26,16 +26,17 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Random;
+import java.util.stream.Collectors;
 
 import com.datastax.driver.core.BoundStatement;
 import com.datastax.driver.core.PreparedStatement;
 import com.datastax.driver.core.ResultSet;
 import org.apache.cassandra.db.ConsistencyLevel;
 import org.apache.cassandra.stress.generate.*;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.StressSettings;
 import org.apache.cassandra.stress.util.JavaDriverClient;
 import org.apache.cassandra.stress.util.ThriftClient;
-import org.apache.cassandra.stress.util.Timer;
 import org.apache.cassandra.thrift.CqlResult;
 import org.apache.cassandra.thrift.ThriftConversion;
 
@@ -53,7 +54,8 @@
 
     public SchemaQuery(Timer timer, StressSettings settings, PartitionGenerator generator, SeedManager seedManager, Integer thriftId, PreparedStatement statement, ConsistencyLevel cl, ArgSelect argSelect)
     {
-        super(timer, settings, new DataSpec(generator, seedManager, new DistributionFixed(1), settings.insert.rowPopulationRatio.get(), argSelect == ArgSelect.MULTIROW ? statement.getVariables().size() : 1), statement, thriftId, cl);
+        super(timer, settings, new DataSpec(generator, seedManager, new DistributionFixed(1), settings.insert.rowPopulationRatio.get(), argSelect == ArgSelect.MULTIROW ? statement.getVariables().size() : 1), statement,
+              statement.getVariables().asList().stream().map(d -> d.getName()).collect(Collectors.toList()), thriftId, cl);
         this.argSelect = argSelect;
         randomBuffer = new Object[argumentIndex.length][argumentIndex.length];
     }
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/SchemaStatement.java b/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/SchemaStatement.java
index c9ead12..ca1f5fa 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/SchemaStatement.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/SchemaStatement.java
@@ -20,8 +20,6 @@
  * 
  */
 
-
-import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.List;
@@ -32,17 +30,14 @@
 import com.datastax.driver.core.LocalDate;
 import com.datastax.driver.core.PreparedStatement;
 import org.apache.cassandra.db.ConsistencyLevel;
-import org.apache.cassandra.stress.Operation;
 import org.apache.cassandra.stress.generate.Row;
 import org.apache.cassandra.stress.operations.PartitionOperation;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.StressSettings;
 import org.apache.cassandra.stress.util.JavaDriverClient;
-import org.apache.cassandra.stress.util.Timer;
-import org.apache.cassandra.transport.SimpleClient;
 
 public abstract class SchemaStatement extends PartitionOperation
 {
-
     final PreparedStatement statement;
     final Integer thriftId;
     final ConsistencyLevel cl;
@@ -51,24 +46,27 @@
     final ColumnDefinitions definitions;
 
     public SchemaStatement(Timer timer, StressSettings settings, DataSpec spec,
-                           PreparedStatement statement, Integer thriftId, ConsistencyLevel cl)
+                           PreparedStatement statement, List<String> bindNames, Integer thriftId, ConsistencyLevel cl)
     {
         super(timer, settings, spec);
         this.statement = statement;
         this.thriftId = thriftId;
         this.cl = cl;
-        argumentIndex = new int[statement.getVariables().size()];
+        argumentIndex = new int[bindNames.size()];
         bindBuffer = new Object[argumentIndex.length];
-        definitions = statement.getVariables();
+        definitions = statement != null ? statement.getVariables() : null;
         int i = 0;
-        for (ColumnDefinitions.Definition definition : definitions)
-            argumentIndex[i++] = spec.partitionGenerator.indexOf(definition.getName());
+        for (String name : bindNames)
+            argumentIndex[i++] = spec.partitionGenerator.indexOf(name);
 
-        statement.setConsistencyLevel(JavaDriverClient.from(cl));
+        if (statement != null)
+            statement.setConsistencyLevel(JavaDriverClient.from(cl));
     }
 
     BoundStatement bindRow(Row row)
     {
+        assert statement != null;
+
         for (int i = 0 ; i < argumentIndex.length ; i++)
         {
             Object value = row.get(argumentIndex[i]);
@@ -92,12 +90,6 @@
         return args;
     }
 
-    @Override
-    public void run(SimpleClient client) throws IOException
-    {
-        throw new UnsupportedOperationException();
-    }
-
     abstract class Runner implements RunOp
     {
         int partitionCount;
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/TokenRangeQuery.java b/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/TokenRangeQuery.java
index 60a6c48..ff8b27f 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/TokenRangeQuery.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/TokenRangeQuery.java
@@ -26,8 +26,6 @@
 
 import javax.naming.OperationNotSupportedException;
 
-import com.google.common.util.concurrent.RateLimiter;
-
 import com.datastax.driver.core.ColumnMetadata;
 import com.datastax.driver.core.PagingState;
 import com.datastax.driver.core.ResultSet;
@@ -37,18 +35,19 @@
 import com.datastax.driver.core.TableMetadata;
 import com.datastax.driver.core.Token;
 import com.datastax.driver.core.TokenRange;
+import io.netty.util.concurrent.FastThreadLocal;
 import org.apache.cassandra.stress.Operation;
 import org.apache.cassandra.stress.StressYaml;
 import org.apache.cassandra.stress.WorkManager;
 import org.apache.cassandra.stress.generate.TokenRangeIterator;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.StressSettings;
 import org.apache.cassandra.stress.util.JavaDriverClient;
 import org.apache.cassandra.stress.util.ThriftClient;
-import org.apache.cassandra.stress.util.Timer;
 
 public class TokenRangeQuery extends Operation
 {
-    private final ThreadLocal<State> currentState = new ThreadLocal<>();
+    private final FastThreadLocal<State> currentState = new FastThreadLocal<>();
 
     private final TableMetadata tableMetadata;
     private final TokenRangeIterator tokenRangeIterator;
@@ -248,18 +247,16 @@
         timeWithRetry(new ThriftRun(client));
     }
 
-    public boolean ready(WorkManager workManager, RateLimiter rateLimiter)
+    public int ready(WorkManager workManager)
     {
         tokenRangeIterator.update();
 
         if (tokenRangeIterator.exhausted() && currentState.get() == null)
-            return false;
+            return 0;
 
         int numLeft = workManager.takePermits(1);
-        if (rateLimiter != null && numLeft > 0 )
-            rateLimiter.acquire(numLeft);
 
-        return numLeft > 0;
+        return numLeft > 0 ? 1 : 0;
     }
 
     public String key()
diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/ValidatingSchemaQuery.java b/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/ValidatingSchemaQuery.java
index 02a9ca8..a731b99 100644
--- a/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/ValidatingSchemaQuery.java
+++ b/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/ValidatingSchemaQuery.java
@@ -26,30 +26,26 @@
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Random;
 import java.util.concurrent.ThreadLocalRandom;
 
 import com.datastax.driver.core.*;
 import org.apache.cassandra.db.ConsistencyLevel;
-import org.apache.cassandra.stress.Operation;
 import org.apache.cassandra.stress.generate.*;
 import org.apache.cassandra.stress.generate.Row;
 import org.apache.cassandra.stress.operations.PartitionOperation;
+import org.apache.cassandra.stress.report.Timer;
 import org.apache.cassandra.stress.settings.StressSettings;
 import org.apache.cassandra.stress.util.JavaDriverClient;
 import org.apache.cassandra.stress.util.ThriftClient;
-import org.apache.cassandra.stress.util.Timer;
 import org.apache.cassandra.thrift.Compression;
 import org.apache.cassandra.thrift.CqlResult;
 import org.apache.cassandra.thrift.CqlRow;
 import org.apache.cassandra.thrift.ThriftConversion;
-import org.apache.cassandra.transport.SimpleClient;
 import org.apache.cassandra.utils.Pair;
 import org.apache.thrift.TException;
 
 public class ValidatingSchemaQuery extends PartitionOperation
 {
-    final Random random = new Random();
     private Pair<Row, Row> bounds;
 
     final int clusteringComponents;
@@ -58,12 +54,6 @@
     final int[] argumentIndex;
     final Object[] bindBuffer;
 
-    @Override
-    public void run(SimpleClient client) throws IOException
-    {
-        throw new UnsupportedOperationException();
-    }
-
     private ValidatingSchemaQuery(Timer timer, StressSettings settings, PartitionGenerator generator, SeedManager seedManager, ValidatingStatement[] statements, ConsistencyLevel cl, int clusteringComponents)
     {
         super(timer, settings, new DataSpec(generator, seedManager, new DistributionFixed(1), settings.insert.rowPopulationRatio.get(), 1));
@@ -281,14 +271,14 @@
         {
             StringBuilder cc = new StringBuilder();
             StringBuilder arg = new StringBuilder();
-            cc.append("("); arg.append("(");
+            cc.append('('); arg.append('(');
             for (int d = 0 ; d <= depth ; d++)
             {
-                if (d > 0) { cc.append(","); arg.append(","); }
+                if (d > 0) { cc.append(','); arg.append(','); }
                 cc.append(metadata.getClusteringColumns().get(d).getName());
-                arg.append("?");
+                arg.append('?');
             }
-            cc.append(")"); arg.append(")");
+            cc.append(')'); arg.append(')');
 
             ValidatingStatement[] statements = new ValidatingStatement[depth < maxDepth ? 1 : 4];
             int i = 0;
diff --git a/tools/stress/src/org/apache/cassandra/stress/report/StressMetrics.java b/tools/stress/src/org/apache/cassandra/stress/report/StressMetrics.java
new file mode 100644
index 0000000..52e90e2
--- /dev/null
+++ b/tools/stress/src/org/apache/cassandra/stress/report/StressMetrics.java
@@ -0,0 +1,452 @@
+package org.apache.cassandra.stress.report;
+/*
+ *
+ * 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.
+ *
+ */
+
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import java.io.FileNotFoundException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Queue;
+import java.util.TreeMap;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.LockSupport;
+
+import org.HdrHistogram.Histogram;
+import org.HdrHistogram.HistogramLogWriter;
+import org.apache.cassandra.stress.StressAction.Consumer;
+import org.apache.cassandra.stress.StressAction.MeasurementSink;
+import org.apache.cassandra.stress.StressAction.OpMeasurement;
+import org.apache.cassandra.stress.settings.SettingsLog.Level;
+import org.apache.cassandra.stress.settings.StressSettings;
+import org.apache.cassandra.stress.util.JmxCollector;
+import org.apache.cassandra.stress.util.ResultLogger;
+import org.apache.cassandra.stress.util.JmxCollector.GcStats;
+import org.apache.cassandra.stress.util.Uncertainty;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.commons.lang3.time.DurationFormatUtils;
+
+public class StressMetrics implements MeasurementSink
+{
+    private final List<Consumer> consumers = new ArrayList<>();
+    private final ResultLogger output;
+    private final Thread thread;
+    private final Uncertainty rowRateUncertainty = new Uncertainty();
+    private final CountDownLatch stopped = new CountDownLatch(1);
+    private final Callable<JmxCollector.GcStats> gcStatsCollector;
+    private final HistogramLogWriter histogramWriter;
+    private final long epochNs = System.nanoTime();
+    private final long epochMs = System.currentTimeMillis();
+
+    private volatile JmxCollector.GcStats totalGcStats = new GcStats(0);
+
+    private volatile boolean stop = false;
+    private volatile boolean cancelled = false;
+
+
+    // collected data for intervals and summary
+    private final Map<String, TimingInterval> opTypeToCurrentTimingInterval = new TreeMap<>();
+    private final Map<String, TimingInterval> opTypeToSummaryTimingInterval = new TreeMap<>();
+    private final Queue<OpMeasurement> leftovers = new ArrayDeque<>();
+    private final TimingInterval totalCurrentInterval;
+    private final TimingInterval totalSummaryInterval;
+
+    public StressMetrics(ResultLogger output, final long logIntervalMillis, StressSettings settings)
+    {
+        this.output = output;
+        if(settings.log.hdrFile != null)
+        {
+            try
+            {
+                histogramWriter = new HistogramLogWriter(settings.log.hdrFile);
+                histogramWriter.outputComment("Logging op latencies for Cassandra Stress");
+                histogramWriter.outputLogFormatVersion();
+                final long roundedEpoch = epochMs - (epochMs%1000);
+                histogramWriter.outputBaseTime(roundedEpoch);
+                histogramWriter.setBaseTime(roundedEpoch);
+                histogramWriter.outputStartTime(roundedEpoch);
+                histogramWriter.outputLegend();
+            }
+            catch (FileNotFoundException e)
+            {
+                throw new IllegalArgumentException(e);
+            }
+        }
+        else
+        {
+            histogramWriter = null;
+        }
+        Callable<JmxCollector.GcStats> gcStatsCollector;
+        totalGcStats = new JmxCollector.GcStats(0);
+        try
+        {
+            gcStatsCollector = new JmxCollector(settings.node.resolveAllPermitted(settings), settings.port.jmxPort);
+        }
+        catch (Throwable t)
+        {
+            if (settings.log.level == Level.VERBOSE)
+            {
+                t.printStackTrace();
+            }
+            System.err.println("Failed to connect over JMX; not collecting these stats");
+            gcStatsCollector = () -> totalGcStats;
+        }
+        this.gcStatsCollector = gcStatsCollector;
+        this.totalCurrentInterval = new TimingInterval(settings.rate.isFixed);
+        this.totalSummaryInterval = new TimingInterval(settings.rate.isFixed);
+        printHeader("", output);
+        thread = new Thread(() -> {
+            reportingLoop(logIntervalMillis);
+        });
+        thread.setName("StressMetrics");
+    }
+    public void start()
+    {
+        thread.start();
+    }
+
+    public void waitUntilConverges(double targetUncertainty, int minMeasurements, int maxMeasurements) throws InterruptedException
+    {
+        rowRateUncertainty.await(targetUncertainty, minMeasurements, maxMeasurements);
+    }
+
+    public void cancel()
+    {
+        cancelled = true;
+        stop = true;
+        thread.interrupt();
+        rowRateUncertainty.wakeAll();
+    }
+
+    public void stop() throws InterruptedException
+    {
+        stop = true;
+        thread.interrupt();
+        stopped.await();
+    }
+
+
+    private void reportingLoop(final long logIntervalMillis)
+    {
+        // align report timing to the nearest second
+        final long currentTimeMs = System.currentTimeMillis();
+        final long startTimeMs = currentTimeMs - (currentTimeMs % 1000);
+        // reporting interval starts rounded to the second
+        long reportingStartNs = (System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(currentTimeMs - startTimeMs));
+        final long parkIntervalNs = TimeUnit.MILLISECONDS.toNanos(logIntervalMillis);
+        try
+        {
+            while (!stop)
+            {
+                final long wakupTarget = reportingStartNs + parkIntervalNs;
+                sleepUntil(wakupTarget);
+                if (stop)
+                {
+                    break;
+                }
+                recordInterval(wakupTarget, parkIntervalNs);
+                reportingStartNs += parkIntervalNs;
+            }
+
+            final long end = System.nanoTime();
+            recordInterval(end, end - reportingStartNs);
+        }
+        catch (Exception e)
+        {
+            e.printStackTrace();
+            cancel();
+        }
+        finally
+        {
+            rowRateUncertainty.wakeAll();
+            stopped.countDown();
+        }
+    }
+
+
+    private void sleepUntil(final long until)
+    {
+        long parkFor;
+        while (!stop &&
+               (parkFor = until - System.nanoTime()) > 0)
+        {
+            LockSupport.parkNanos(parkFor);
+        }
+    }
+
+    @Override
+    public void record(String opType, long intended, long started, long ended, long rowCnt, long partitionCnt, boolean err)
+    {
+        TimingInterval current = opTypeToCurrentTimingInterval.computeIfAbsent(opType, k -> new TimingInterval(totalCurrentInterval.isFixed));
+        record(current, intended, started, ended, rowCnt, partitionCnt, err);
+    }
+
+    private void record(TimingInterval t, long intended, long started, long ended, long rowCnt, long partitionCnt, boolean err)
+    {
+        t.rowCount += rowCnt;
+        t.partitionCount += partitionCnt;
+        if (err)
+            t.errorCount++;
+        if (intended != 0) {
+            t.responseTime().recordValue(ended-intended);
+            t.waitTime().recordValue(started-intended);
+        }
+        final long sTime = ended-started;
+        t.serviceTime().recordValue(sTime);
+    }
+
+    private void recordInterval(long intervalEnd, long parkIntervalNs)
+    {
+
+        drainConsumerMeasurements(intervalEnd, parkIntervalNs);
+
+        GcStats gcStats = null;
+        try
+        {
+            gcStats = gcStatsCollector.call();
+        }
+        catch (Exception e)
+        {
+            gcStats = new GcStats(0);
+        }
+        totalGcStats = JmxCollector.GcStats.aggregate(Arrays.asList(totalGcStats, gcStats));
+
+        rowRateUncertainty.update(totalCurrentInterval.adjustedRowRate());
+        if (totalCurrentInterval.operationCount() != 0)
+        {
+            // if there's a single operation we only print the total
+            final boolean logPerOpSummaryLine = opTypeToCurrentTimingInterval.size() > 1;
+
+            for (Map.Entry<String, TimingInterval> type : opTypeToCurrentTimingInterval.entrySet())
+            {
+                final String opName = type.getKey();
+                final TimingInterval opInterval = type.getValue();
+                if (logPerOpSummaryLine)
+                {
+                    printRow("", opName, opInterval, opTypeToSummaryTimingInterval.get(opName), gcStats, rowRateUncertainty, output);
+                }
+                logHistograms(opName, opInterval);
+                opInterval.reset();
+            }
+
+            printRow("", "total", totalCurrentInterval, totalSummaryInterval, gcStats, rowRateUncertainty, output);
+            totalCurrentInterval.reset();
+        }
+    }
+
+    private void drainConsumerMeasurements(long intervalEnd, long parkIntervalNs)
+    {
+        // record leftover measurements if any
+        int leftoversSize = leftovers.size();
+        for (int i=0;i<leftoversSize;i++)
+        {
+            OpMeasurement last = leftovers.poll();
+            if (last.ended <= intervalEnd)
+            {
+                record(last.opType, last.intended, last.started, last.ended, last.rowCnt, last.partitionCnt, last.err);
+                // round robin-ish redistribution of leftovers
+                consumers.get(i%consumers.size()).measurementsRecycling.offer(last);
+            }
+            else
+            {
+                // no record for you! wait one interval!
+                leftovers.offer(last);
+            }
+        }
+        // record interval collected measurements
+        for (Consumer c: consumers) {
+            Queue<OpMeasurement> in = c.measurementsReporting;
+            Queue<OpMeasurement> out = c.measurementsRecycling;
+            OpMeasurement last;
+            while ((last = in.poll()) != null)
+            {
+                if (last.ended > intervalEnd)
+                {
+                    // measurements for any given consumer are ordered, we stop when we stop.
+                    leftovers.add(last);
+                    break;
+                }
+                record(last.opType, last.intended, last.started, last.ended, last.rowCnt, last.partitionCnt, last.err);
+                out.offer(last);
+            }
+        }
+        // set timestamps and summarize
+        for (Entry<String, TimingInterval> currPerOp : opTypeToCurrentTimingInterval.entrySet()) {
+            currPerOp.getValue().endNanos(intervalEnd);
+            currPerOp.getValue().startNanos(intervalEnd-parkIntervalNs);
+            TimingInterval summaryPerOp = opTypeToSummaryTimingInterval.computeIfAbsent(currPerOp.getKey(), k -> new TimingInterval(totalCurrentInterval.isFixed));
+            summaryPerOp.add(currPerOp.getValue());
+            totalCurrentInterval.add(currPerOp.getValue());
+        }
+        totalCurrentInterval.endNanos(intervalEnd);
+        totalCurrentInterval.startNanos(intervalEnd-parkIntervalNs);
+
+        totalSummaryInterval.add(totalCurrentInterval);
+    }
+
+
+    private void logHistograms(String opName, TimingInterval opInterval)
+    {
+        if (histogramWriter == null)
+            return;
+        final long startNs = opInterval.startNanos();
+        final long endNs = opInterval.endNanos();
+
+        logHistogram(opName + "-st", startNs, endNs, opInterval.serviceTime());
+        logHistogram(opName + "-rt", startNs, endNs, opInterval.responseTime());
+        logHistogram(opName + "-wt", startNs, endNs, opInterval.waitTime());
+    }
+
+    private void logHistogram(String opName, final long startNs, final long endNs, final Histogram histogram)
+    {
+        if (histogram.getTotalCount() != 0)
+        {
+            histogram.setTag(opName);
+            final long relativeStartNs = startNs - epochNs;
+            final long startMs = (long) (1000 *((epochMs + NANOSECONDS.toMillis(relativeStartNs))/1000.0));
+            histogram.setStartTimeStamp(startMs);
+            final long relativeEndNs = endNs - epochNs;
+            final long endMs = (long) (1000 *((epochMs + NANOSECONDS.toMillis(relativeEndNs))/1000.0));
+            histogram.setEndTimeStamp(endMs);
+            histogramWriter.outputIntervalHistogram(histogram);
+        }
+    }
+
+
+    // PRINT FORMATTING
+
+    public static final String HEADFORMAT = "%-10s%10s,%8s,%8s,%8s,%8s,%8s,%8s,%8s,%8s,%8s,%7s,%9s,%7s,%7s,%8s,%8s,%8s,%8s";
+    public static final String ROWFORMAT =  "%-10s%10d,%8.0f,%8.0f,%8.0f,%8.1f,%8.1f,%8.1f,%8.1f,%8.1f,%8.1f,%7.1f,%9.5f,%7d,%7.0f,%8.0f,%8.0f,%8.0f,%8.0f";
+    public static final String[] HEADMETRICS = new String[]{"type", "total ops","op/s","pk/s","row/s","mean","med",".95",".99",".999","max","time","stderr", "errors", "gc: #", "max ms", "sum ms", "sdv ms", "mb"};
+    public static final String HEAD = String.format(HEADFORMAT, (Object[]) HEADMETRICS);
+
+    private static void printHeader(String prefix, ResultLogger output)
+    {
+        output.println(prefix + HEAD);
+    }
+
+    private static void printRow(String prefix, String type, TimingInterval interval, TimingInterval total,
+                                 JmxCollector.GcStats gcStats, Uncertainty opRateUncertainty, ResultLogger output)
+    {
+        output.println(prefix + String.format(ROWFORMAT,
+                type + ",",
+                total.operationCount(),
+                interval.opRate(),
+                interval.partitionRate(),
+                interval.rowRate(),
+                interval.meanLatencyMs(),
+                interval.medianLatencyMs(),
+                interval.latencyAtPercentileMs(95.0),
+                interval.latencyAtPercentileMs(99.0),
+                interval.latencyAtPercentileMs(99.9),
+                interval.maxLatencyMs(),
+                total.runTimeMs() / 1000f,
+                opRateUncertainty.getUncertainty(),
+                interval.errorCount,
+                gcStats.count,
+                gcStats.maxms,
+                gcStats.summs,
+                gcStats.sdvms,
+                gcStats.bytes / (1 << 20)
+        ));
+    }
+
+    public void summarise()
+    {
+        output.println("\n");
+        output.println("Results:");
+
+        TimingIntervals opHistory = new TimingIntervals(opTypeToSummaryTimingInterval);
+        TimingInterval history = this.totalSummaryInterval;
+        output.println(String.format("Op rate                   : %,8.0f op/s  %s", history.opRate(), opHistory.opRates()));
+        output.println(String.format("Partition rate            : %,8.0f pk/s  %s", history.partitionRate(), opHistory.partitionRates()));
+        output.println(String.format("Row rate                  : %,8.0f row/s %s", history.rowRate(), opHistory.rowRates()));
+        output.println(String.format("Latency mean              : %6.1f ms %s", history.meanLatencyMs(), opHistory.meanLatencies()));
+        output.println(String.format("Latency median            : %6.1f ms %s", history.medianLatencyMs(), opHistory.medianLatencies()));
+        output.println(String.format("Latency 95th percentile   : %6.1f ms %s", history.latencyAtPercentileMs(95.0), opHistory.latenciesAtPercentile(95.0)));
+        output.println(String.format("Latency 99th percentile   : %6.1f ms %s", history.latencyAtPercentileMs(99.0), opHistory.latenciesAtPercentile(99.0)));
+        output.println(String.format("Latency 99.9th percentile : %6.1f ms %s", history.latencyAtPercentileMs(99.9), opHistory.latenciesAtPercentile(99.9)));
+        output.println(String.format("Latency max               : %6.1f ms %s", history.maxLatencyMs(), opHistory.maxLatencies()));
+        output.println(String.format("Total partitions          : %,10d %s",   history.partitionCount, opHistory.partitionCounts()));
+        output.println(String.format("Total errors              : %,10d %s",   history.errorCount, opHistory.errorCounts()));
+        output.println(String.format("Total GC count            : %,1.0f", totalGcStats.count));
+        output.println(String.format("Total GC memory           : %s", FBUtilities.prettyPrintMemory((long)totalGcStats.bytes, true)));
+        output.println(String.format("Total GC time             : %,6.1f seconds", totalGcStats.summs / 1000));
+        output.println(String.format("Avg GC time               : %,6.1f ms", totalGcStats.summs / totalGcStats.count));
+        output.println(String.format("StdDev GC time            : %,6.1f ms", totalGcStats.sdvms));
+        output.println("Total operation time      : " + DurationFormatUtils.formatDuration(
+                history.runTimeMs(), "HH:mm:ss", true));
+        output.println(""); // Newline is important here to separate the aggregates section from the END or the next stress iteration
+    }
+
+    public static void summarise(List<String> ids, List<StressMetrics> summarise, ResultLogger out)
+    {
+        int idLen = 0;
+        for (String id : ids)
+            idLen = Math.max(id.length(), idLen);
+        String formatstr = "%" + idLen + "s, ";
+        printHeader(String.format(formatstr, "id"), out);
+        for (int i = 0 ; i < ids.size() ; i++)
+        {
+            for (Map.Entry<String, TimingInterval> type : summarise.get(i).opTypeToSummaryTimingInterval.entrySet())
+            {
+                printRow(String.format(formatstr, ids.get(i)),
+                         type.getKey(),
+                         type.getValue(),
+                         type.getValue(),
+                         summarise.get(i).totalGcStats,
+                         summarise.get(i).rowRateUncertainty,
+                         out);
+            }
+            TimingInterval hist = summarise.get(i).totalSummaryInterval;
+            printRow(String.format(formatstr, ids.get(i)),
+                    "total",
+                    hist,
+                    hist,
+                    summarise.get(i).totalGcStats,
+                    summarise.get(i).rowRateUncertainty,
+                    out
+            );
+        }
+    }
+
+    public boolean wasCancelled()
+    {
+        return cancelled;
+    }
+
+    public void add(Consumer consumer)
+    {
+        consumers.add(consumer);
+    }
+
+    public double opRate()
+    {
+        return totalSummaryInterval.opRate();
+    }
+}
diff --git a/tools/stress/src/org/apache/cassandra/stress/report/Timer.java b/tools/stress/src/org/apache/cassandra/stress/report/Timer.java
new file mode 100644
index 0000000..b3df52f
--- /dev/null
+++ b/tools/stress/src/org/apache/cassandra/stress/report/Timer.java
@@ -0,0 +1,63 @@
+package org.apache.cassandra.stress.report;
+/*
+ *
+ * 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.
+ *
+ */
+
+
+import org.apache.cassandra.stress.StressAction.MeasurementSink;
+
+// a timer - this timer must be used by a single thread, and co-ordinates with other timers by
+public final class Timer
+{
+    private final String opType;
+    private final MeasurementSink sink;
+
+    // event timing info
+    private long intendedTimeNs;
+    private long startTimeNs;
+
+    public Timer(String opType, MeasurementSink sink)
+    {
+        this.opType = opType;
+        this.sink = sink;
+    }
+
+
+    public void stop(long partitionCount, long rowCount, boolean error)
+    {
+        sink.record(opType, intendedTimeNs, startTimeNs, System.nanoTime(), rowCount, partitionCount, error);
+        resetTimes();
+    }
+
+    private void resetTimes()
+    {
+        intendedTimeNs = startTimeNs = 0;
+    }
+
+    public void intendedTimeNs(long v)
+    {
+        intendedTimeNs = v;
+    }
+
+    public void start()
+    {
+        startTimeNs = System.nanoTime();
+    }
+}
\ No newline at end of file
diff --git a/tools/stress/src/org/apache/cassandra/stress/report/TimingInterval.java b/tools/stress/src/org/apache/cassandra/stress/report/TimingInterval.java
new file mode 100644
index 0000000..4d124a2
--- /dev/null
+++ b/tools/stress/src/org/apache/cassandra/stress/report/TimingInterval.java
@@ -0,0 +1,234 @@
+package org.apache.cassandra.stress.report;
+/*
+ *
+ * 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.
+ *
+ */
+
+import org.HdrHistogram.Histogram;
+
+// represents measurements taken over an interval of time
+// used for both single timer results and merged timer results
+public final class TimingInterval
+{
+    private final Histogram responseTime = new Histogram(3);
+    private final Histogram serviceTime = new Histogram(3);
+    private final Histogram waitTime = new Histogram(3);
+
+    public static final long[] EMPTY_SAMPLE = new long[0];
+    // nanos
+    private long startNs = Long.MAX_VALUE;
+    private long endNs = Long.MIN_VALUE;
+
+    // discrete
+    public long partitionCount;
+    public long rowCount;
+    public long errorCount;
+    public final boolean isFixed;
+
+    public TimingInterval(boolean isFixed){
+        this.isFixed = isFixed;
+    }
+
+    public String toString()
+    {
+        return String.format("Start: %d end: %d maxLatency: %d pCount: %d rcount: %d opCount: %d errors: %d",
+                             startNs, endNs, getLatencyHistogram().getMaxValue(),
+                             partitionCount, rowCount, getLatencyHistogram().getTotalCount(), errorCount);
+    }
+
+
+    public double opRate()
+    {
+        return getLatencyHistogram().getTotalCount() / ((endNs - startNs) * 0.000000001d);
+    }
+
+    public double adjustedRowRate()
+    {
+        return rowCount / ((endNs - (startNs + getLatencyHistogram().getMaxValue())) * 0.000000001d);
+    }
+
+    public double partitionRate()
+    {
+        return partitionCount / ((endNs - startNs) * 0.000000001d);
+    }
+
+    public double rowRate()
+    {
+        return rowCount / ((endNs - startNs) * 0.000000001d);
+    }
+
+    public double meanLatencyMs()
+    {
+        return getLatencyHistogram().getMean() * 0.000001d;
+    }
+
+    public double maxLatencyMs()
+    {
+        return getLatencyHistogram().getMaxValue() * 0.000001d;
+    }
+
+    public double medianLatencyMs()
+    {
+        return getLatencyHistogram().getValueAtPercentile(50.0) * 0.000001d;
+    }
+
+
+    /**
+     * @param percentile between 0.0 and 100.0
+     * @return latency in milliseconds at percentile
+     */
+    public double latencyAtPercentileMs(double percentile)
+    {
+        return getLatencyHistogram().getValueAtPercentile(percentile) * 0.000001d;
+    }
+
+    public long runTimeMs()
+    {
+        return (endNs - startNs) / 1000000;
+    }
+
+    public long endNanos()
+    {
+        return endNs;
+    }
+
+    public long startNanos()
+    {
+        return startNs;
+    }
+
+    public Histogram responseTime()
+    {
+        return responseTime;
+    }
+
+    public Histogram serviceTime()
+    {
+        return serviceTime;
+    }
+
+    public Histogram waitTime()
+    {
+        return waitTime;
+    }
+
+    private Histogram getLatencyHistogram()
+    {
+        if (!isFixed || responseTime.getTotalCount() == 0)
+            return serviceTime;
+        else
+            return responseTime;
+    }
+
+    public static enum TimingParameter
+    {
+        OPRATE, ROWRATE, ADJROWRATE, PARTITIONRATE, MEANLATENCY, MAXLATENCY, MEDIANLATENCY, RANKLATENCY,
+        ERRORCOUNT, PARTITIONCOUNT
+    }
+
+    String getStringValue(TimingParameter value)
+    {
+        return getStringValue(value, Float.NaN);
+    }
+
+    String getStringValue(TimingParameter value, double rank)
+    {
+        switch (value)
+        {
+            case OPRATE:         return String.format("%,.0f", opRate());
+            case ROWRATE:        return String.format("%,.0f", rowRate());
+            case ADJROWRATE:     return String.format("%,.0f", adjustedRowRate());
+            case PARTITIONRATE:  return String.format("%,.0f", partitionRate());
+            case MEANLATENCY:    return String.format("%,.1f", meanLatencyMs());
+            case MAXLATENCY:     return String.format("%,.1f", maxLatencyMs());
+            case MEDIANLATENCY:  return String.format("%,.1f", medianLatencyMs());
+            case RANKLATENCY:    return String.format("%,.1f", latencyAtPercentileMs(rank));
+            case ERRORCOUNT:     return String.format("%,d", errorCount);
+            case PARTITIONCOUNT: return String.format("%,d", partitionCount);
+            default:             throw new IllegalStateException();
+        }
+    }
+
+    public long operationCount()
+    {
+        return getLatencyHistogram().getTotalCount();
+    }
+
+
+    public void startNanos(long started)
+    {
+        this.startNs = started;
+    }
+    public void endNanos(long ended)
+    {
+        this.endNs = ended;
+    }
+
+
+    public void reset()
+    {
+        this.endNs = Long.MIN_VALUE;
+        this.startNs = Long.MAX_VALUE;
+        this.errorCount = 0;
+        this.rowCount = 0;
+        this.partitionCount = 0;
+        if(this.responseTime.getTotalCount() != 0)
+        {
+            this.responseTime.reset();
+        }
+        if(this.serviceTime.getTotalCount() != 0)
+        {
+            this.serviceTime.reset();
+        }
+        if(this.waitTime.getTotalCount() != 0)
+        {
+            this.waitTime.reset();
+        }
+    }
+
+    public void add(TimingInterval value)
+    {
+        if(this.startNs > value.startNs)
+        {
+            this.startNs = value.startNs;
+        }
+        if(this.endNs < value.endNs)
+        {
+            this.endNs = value.endNs;
+        }
+
+        this.errorCount += value.errorCount;
+        this.rowCount += value.rowCount;
+        this.partitionCount += value.partitionCount;
+
+        if (value.responseTime.getTotalCount() != 0)
+        {
+            this.responseTime.add(value.responseTime);
+        }
+        if (value.serviceTime.getTotalCount() != 0)
+        {
+            this.serviceTime.add(value.serviceTime);
+        }
+        if (value.waitTime.getTotalCount() != 0)
+        {
+            this.waitTime.add(value.waitTime);
+        }
+    }
+ }
+
diff --git a/tools/stress/src/org/apache/cassandra/stress/report/TimingIntervals.java b/tools/stress/src/org/apache/cassandra/stress/report/TimingIntervals.java
new file mode 100644
index 0000000..400fab9
--- /dev/null
+++ b/tools/stress/src/org/apache/cassandra/stress/report/TimingIntervals.java
@@ -0,0 +1,144 @@
+/*
+ * 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.stress.report;
+
+import java.util.Map;
+
+public class TimingIntervals
+{
+    final Map<String, TimingInterval> intervals;
+
+    public TimingIntervals(Map<String, TimingInterval> intervals)
+    {
+        this.intervals = intervals;
+    }
+
+    public TimingInterval get(String opType)
+    {
+        return intervals.get(opType);
+    }
+
+
+    public String str(TimingInterval.TimingParameter value, String unit)
+    {
+        return str(value, Double.NaN, unit);
+    }
+
+    public String str(TimingInterval.TimingParameter value, double rank, String unit)
+    {
+        if (intervals.size() == 0)
+        {
+            return "[]";
+        }
+
+        StringBuilder sb = new StringBuilder("[");
+
+        for (Map.Entry<String, TimingInterval> entry : intervals.entrySet())
+        {
+            sb.append(entry.getKey());
+            sb.append(": ");
+            sb.append(entry.getValue().getStringValue(value, rank));
+            if (unit.length() > 0)
+            {
+                sb.append(" ");
+                sb.append(unit);
+            }
+            sb.append(", ");
+        }
+
+        sb.setLength(sb.length()-2);
+        sb.append("]");
+
+        return sb.toString();
+    }
+
+    public String opRates()
+    {
+        return str(TimingInterval.TimingParameter.OPRATE, "op/s");
+    }
+
+    public String partitionRates()
+    {
+        return str(TimingInterval.TimingParameter.PARTITIONRATE, "pk/s");
+    }
+
+    public String rowRates()
+    {
+        return str(TimingInterval.TimingParameter.ROWRATE, "row/s");
+    }
+
+    public String meanLatencies()
+    {
+        return str(TimingInterval.TimingParameter.MEANLATENCY, "ms");
+    }
+
+    public String maxLatencies()
+    {
+        return str(TimingInterval.TimingParameter.MAXLATENCY, "ms");
+    }
+
+    public String medianLatencies()
+    {
+        return str(TimingInterval.TimingParameter.MEDIANLATENCY, "ms");
+    }
+
+    public String latenciesAtPercentile(double rank)
+    {
+        return str(TimingInterval.TimingParameter.RANKLATENCY, rank, "ms");
+    }
+
+    public String errorCounts()
+    {
+        return str(TimingInterval.TimingParameter.ERRORCOUNT, "");
+    }
+
+    public String partitionCounts()
+    {
+        return str(TimingInterval.TimingParameter.PARTITIONCOUNT, "");
+    }
+
+    public long opRate()
+    {
+        long v = 0;
+        for (TimingInterval interval : intervals.values())
+            v += interval.opRate();
+        return v;
+    }
+
+    public long startNanos()
+    {
+        long start = Long.MAX_VALUE;
+        for (TimingInterval interval : intervals.values())
+            start = Math.min(start, interval.startNanos());
+        return start;
+    }
+
+    public long endNanos()
+    {
+        long end = Long.MIN_VALUE;
+        for (TimingInterval interval : intervals.values())
+            end = Math.max(end, interval.startNanos());
+        return end;
+    }
+
+    public Map<String, TimingInterval> intervals()
+    {
+        return intervals;
+    }
+}
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/CliOption.java b/tools/stress/src/org/apache/cassandra/stress/settings/CliOption.java
index eb286ee..36284ab 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/CliOption.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/CliOption.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.stress.settings;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
@@ -32,13 +32,13 @@
     RATE("Thread count, rate limit or automatic mode (default is auto)", SettingsRate.helpPrinter()),
     MODE("Thrift or CQL with options", SettingsMode.helpPrinter()),
     ERRORS("How to handle errors when encountered during stress", SettingsErrors.helpPrinter()),
-    SAMPLE("Specify the number of samples to collect for measuring latency", SettingsSamples.helpPrinter()),
     SCHEMA("Replication settings, compression, compaction, etc.", SettingsSchema.helpPrinter()),
     NODE("Nodes to connect to", SettingsNode.helpPrinter()),
     LOG("Where to log progress to, and the interval at which to do it", SettingsLog.helpPrinter()),
     TRANSPORT("Custom transport factories", SettingsTransport.helpPrinter()),
     PORT("The port to connect to cassandra nodes on", SettingsPort.helpPrinter()),
     SENDTO("-send-to", "Specify a stress server to send this command to", SettingsMisc.sendToDaemonHelpPrinter()),
+    GRAPH("-graph", "Graph recorded metrics", SettingsGraph.helpPrinter()),
     TOKENRANGE("Token range settings", SettingsTokenRange.helpPrinter())
     ;
 
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/Command.java b/tools/stress/src/org/apache/cassandra/stress/settings/Command.java
index c47c5d2..d8ac5d1 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/Command.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/Command.java
@@ -61,7 +61,8 @@
 
     HELP(false, null, "-?", "Print help for a command or option", null),
     PRINT(false, null, "Inspect the output of a distribution definition", null),
-    LEGACY(false, null, "Legacy support mode", null)
+    LEGACY(false, null, "Legacy support mode", null),
+    VERSION(false, null, "Print the version of cassandra stress", null)
     ;
 
     private static final Map<String, Command> LOOKUP;
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/GroupedOptions.java b/tools/stress/src/org/apache/cassandra/stress/settings/GroupedOptions.java
index f3924c0..403eaac 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/GroupedOptions.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/GroupedOptions.java
@@ -22,13 +22,14 @@
 
 
 import java.io.PrintStream;
+import java.io.Serializable;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 import com.google.common.collect.ImmutableList;
 
-public abstract class GroupedOptions
+public abstract class GroupedOptions implements Serializable
 {
 
     int accepted = 0;
@@ -114,6 +115,18 @@
         }
     }
 
+    public String getOptionAsString()
+    {
+        StringBuilder sb = new StringBuilder();
+        for (Option option : options())
+        {
+            sb.append(option.getOptionAsString());
+            sb.append("; ");
+        }
+        return sb.toString();
+    }
+
+
     public static List<? extends Option> merge(List<? extends Option> ... optionss)
     {
         ImmutableList.Builder<Option> builder = ImmutableList.builder();
@@ -128,12 +141,12 @@
         return builder.build();
     }
 
-    public static String formatLong(String longDisplay, String description)
+    static String formatLong(String longDisplay, String description)
     {
         return String.format("%-40s %s", longDisplay, description);
     }
 
-    public static String formatMultiLine(String longDisplay, String description)
+    static String formatMultiLine(String longDisplay, String description)
     {
         return String.format("%-36s %s", longDisplay, description);
     }
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/Option.java b/tools/stress/src/org/apache/cassandra/stress/settings/Option.java
index b9e402e..24eb33a 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/Option.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/Option.java
@@ -21,15 +21,17 @@
  */
 
 
+import java.io.Serializable;
 import java.util.List;
 
-abstract class Option
+abstract class Option implements Serializable
 {
 
     abstract boolean accept(String param);
     abstract boolean happy();
     abstract String shortDisplay();
     abstract String longDisplay();
+    abstract String getOptionAsString(); // short and longDisplay print help text getOptionAsString prints value
     abstract List<String> multiLineDisplay();
     abstract boolean setByUser();
     abstract boolean present();
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/OptionAnyProbabilities.java b/tools/stress/src/org/apache/cassandra/stress/settings/OptionAnyProbabilities.java
index 1a444a0..0d045c0 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/OptionAnyProbabilities.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/OptionAnyProbabilities.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.stress.settings;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
@@ -58,6 +58,15 @@
         {
             return null;
         }
+        public String getOptionAsString()
+        {
+            StringBuilder sb = new StringBuilder();
+            for (Map.Entry<String, Double> entry : options.entrySet())
+            {
+                sb.append(entry.getKey()).append("=").append(entry.getValue()).append(",");
+            }
+            return sb.toString();
+        }
 
         String longDisplay()
         {
@@ -91,5 +100,14 @@
     {
         return ratios.options;
     }
+    public String getOptionAsString()
+    {
+        StringBuilder sb = new StringBuilder(super.getOptionAsString());
+        sb.append(" [Ratios: ");
+        sb.append(ratios.getOptionAsString());
+        sb.append("];");
+        return sb.toString();
+    }
+
 }
 
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/OptionDistribution.java b/tools/stress/src/org/apache/cassandra/stress/settings/OptionDistribution.java
index 713f5a6..cc93323 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/OptionDistribution.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/OptionDistribution.java
@@ -124,6 +124,7 @@
                 GroupedOptions.formatMultiLine("GAUSSIAN(min..max,mean,stdev)", "A gaussian/normal distribution, with explicitly defined mean and stdev"),
                 GroupedOptions.formatMultiLine("UNIFORM(min..max)", "A uniform distribution over the range [min, max]"),
                 GroupedOptions.formatMultiLine("FIXED(val)", "A fixed distribution, always returning the same value"),
+                GroupedOptions.formatMultiLine("SEQ(min..max)", "A fixed sequence, returning values in the range min to max sequentially (starting based on seed), wrapping if necessary."),
                 "Preceding the name with ~ will invert the distribution, e.g. ~exp(1..10) will yield 10 most, instead of least, often",
                 "Aliases: extr, qextr, gauss, normal, norm, weibull"
         );
@@ -145,6 +146,11 @@
         return (defaultSpec != null ? "[" : "") + prefix + "DIST(?)" + (defaultSpec != null ? "]" : "");
     }
 
+    public String getOptionAsString()
+    {
+        return prefix + (spec == null ? defaultSpec : spec);
+    }
+
     private static final Map<String, Impl> LOOKUP;
     static
     {
@@ -161,6 +167,7 @@
         lookup.put("norm", lookup.get("gaussian"));
         lookup.put("uniform", new UniformImpl());
         lookup.put("fixed", new FixedImpl());
+        lookup.put("seq", new SequenceImpl());
         LOOKUP = lookup;
     }
 
@@ -334,18 +341,49 @@
         public DistributionFactory getFactory(List<String> params)
         {
             if (params.size() != 1)
-                throw new IllegalArgumentException("Invalid parameter list for uniform distribution: " + params);
+                throw new IllegalArgumentException("Invalid parameter list for fixed distribution: " + params);
             try
             {
                 final long key = parseLong(params.get(0));
                 return new FixedFactory(key);
             } catch (Exception ignore)
             {
-                throw new IllegalArgumentException("Invalid parameter list for uniform distribution: " + params);
+                throw new IllegalArgumentException("Invalid parameter list for fixed distribution: " + params);
             }
         }
     }
 
+    private static final class SequenceImpl implements Impl
+    {
+
+        @Override
+        public DistributionFactory getFactory(List<String> params)
+        {
+            if (params.size() != 1)
+                throw new IllegalArgumentException("Invalid parameter list for sequence distribution: " + params);
+            final long min;
+            final long max;
+            try
+            {
+                String[] bounds = params.get(0).split("\\.\\.+");
+                min = parseLong(bounds[0]);
+                max = parseLong(bounds[1]);
+            } catch (Exception ignore)
+            {
+                throw new IllegalArgumentException("Invalid parameter list for sequence distribution: " + params);
+            }
+            if (min == max)
+                throw new IllegalArgumentException("Invalid parameter list for sequence distribution (min==max): " + params);
+
+            if (min > max)
+                throw new IllegalArgumentException("Invalid parameter list for sequence distribution (min>max): " + params);
+
+            return new SequenceFactory(min, max);
+
+        }
+    }
+
+
     private static final class InverseFactory implements DistributionFactory
     {
         final DistributionFactory wrapped;
@@ -358,6 +396,8 @@
         {
             return new DistributionInverted(wrapped.get());
         }
+        public String getConfigAsString(){return "Inverse: " + wrapped.getConfigAsString();};
+
     }
 
     // factories
@@ -378,6 +418,10 @@
         {
             return new DistributionOffsetApache(new ExponentialDistribution(new JDKRandomGenerator(), mean, ExponentialDistribution.DEFAULT_INVERSE_ABSOLUTE_ACCURACY), min, max);
         }
+
+        @Override
+        public String getConfigAsString(){return String.format("Exponential:  min=%d,max=%d,mean=%f", min, max, mean);}
+
     }
 
     private static class ExtremeFactory implements DistributionFactory
@@ -397,6 +441,10 @@
         {
             return new DistributionOffsetApache(new WeibullDistribution(new JDKRandomGenerator(), shape, scale, WeibullDistribution.DEFAULT_INVERSE_ABSOLUTE_ACCURACY), min, max);
         }
+
+        @Override
+        public String getConfigAsString(){return String.format("Extreme:  min=%d,max=%d,shape=%f, scale=%f", min, max, shape, scale);}
+
     }
 
     private static final class QuantizedExtremeFactory extends ExtremeFactory
@@ -432,6 +480,10 @@
         {
             return new DistributionBoundApache(new NormalDistribution(new JDKRandomGenerator(), mean, stdev, NormalDistribution.DEFAULT_INVERSE_ABSOLUTE_ACCURACY), min, max);
         }
+
+        @Override
+        public String getConfigAsString(){return String.format("Gaussian:  min=%d,max=%d,mean=%f,stdev=%f", min, max, mean, stdev);}
+
     }
 
     private static final class UniformFactory implements DistributionFactory
@@ -448,6 +500,10 @@
         {
             return new DistributionBoundApache(new UniformRealDistribution(new JDKRandomGenerator(), min, max + 1), min, max);
         }
+
+        @Override
+        public String getConfigAsString(){return String.format("Uniform:  min=%d,max=%d", min, max);}
+
     }
 
     private static final class FixedFactory implements DistributionFactory
@@ -463,8 +519,35 @@
         {
             return new DistributionFixed(key);
         }
+
+        @Override
+        public String getConfigAsString(){return String.format("Fixed:  key=%d", key);}
+
     }
 
+    private static final class SequenceFactory implements DistributionFactory
+    {
+        final long start;
+        final long end;
+
+        private SequenceFactory(long start, long end)
+        {
+            this.start=start;
+            this.end = end;
+        }
+
+        @Override
+        public Distribution get()
+        {
+            return new DistributionSequence(start, end);
+        }
+
+        @Override
+        public String getConfigAsString(){return String.format("Sequence:  start=%d,end=%d", start, end);}
+
+    }
+
+
     @Override
     public int hashCode()
     {
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/OptionMulti.java b/tools/stress/src/org/apache/cassandra/stress/settings/OptionMulti.java
index ad89a5b..1d7ae72 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/OptionMulti.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/OptionMulti.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.stress.settings;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
@@ -109,6 +109,21 @@
     {
         return (happy() ? "[" : "") + name + "(?)" + (happy() ? "]" : "");
     }
+    public String getOptionAsString()
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.append(name).append(": ");
+        sb.append(delegate.getOptionAsString());
+        sb.append(";");
+        if (collectAsMap != null)
+        {
+            sb.append("[");
+            sb.append(collectAsMap.getOptionAsString());
+            sb.append("];");
+        }
+        return sb.toString();
+    }
+
 
     @Override
     public String longDisplay()
@@ -168,6 +183,17 @@
             return "[<option 1..N>=?]";
         }
 
+        public String getOptionAsString()
+        {
+            StringBuilder sb = new StringBuilder();
+            for (Map.Entry<String, String> entry : options.entrySet())
+            {
+                sb.append(entry.getKey()).append("=").append(entry.getValue()).append(",");
+            }
+            return sb.toString();
+        }
+
+
         String longDisplay()
         {
             return GroupedOptions.formatLong(shortDisplay(), description);
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/OptionRatioDistribution.java b/tools/stress/src/org/apache/cassandra/stress/settings/OptionRatioDistribution.java
index 0bd1bfa..1a8aa83 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/OptionRatioDistribution.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/OptionRatioDistribution.java
@@ -134,6 +134,11 @@
     {
         return delegate.shortDisplay();
     }
+    public String getOptionAsString()
+    {
+        return delegate.getOptionAsString();
+    }
+
 
     // factories
 
@@ -153,6 +158,10 @@
         {
             return new RatioDistribution(delegate.get(), divisor);
         }
+
+        @Override
+        public String getConfigAsString(){return String.format("Ratio: divisor=%f;delegate=%s",divisor, delegate.getConfigAsString());};
+
     }
 
     @Override
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/OptionSimple.java b/tools/stress/src/org/apache/cassandra/stress/settings/OptionSimple.java
index ba26c2a..2dd3512 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/OptionSimple.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/OptionSimple.java
@@ -162,6 +162,20 @@
         return GroupedOptions.formatLong(sb.toString(), description);
     }
 
+    public String getOptionAsString()
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.append(displayPrefix);
+
+        if (!(displayPrefix.endsWith("=") || displayPrefix.endsWith("<") || displayPrefix.endsWith(">")))
+        {
+            sb.append(setByUser() ? ":*set*" : ":*not set*");
+        }else{
+            sb.append(value == null ? defaultValue : value);
+        }
+        return sb.toString();
+    }
+
     public List<String> multiLineDisplay()
     {
         return Collections.emptyList();
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsColumn.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsColumn.java
index bf78bf9..79d8d25 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsColumn.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsColumn.java
@@ -33,6 +33,7 @@
 import org.apache.cassandra.stress.generate.Distribution;
 import org.apache.cassandra.stress.generate.DistributionFactory;
 import org.apache.cassandra.stress.generate.DistributionFixed;
+import org.apache.cassandra.stress.util.ResultLogger;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 /**
@@ -114,6 +115,8 @@
                 {
                     return new DistributionFixed(nameCount);
                 }
+                @Override
+                public String getConfigAsString(){return String.format("Count:  fixed=%d", nameCount);}
             };
         }
         else
@@ -175,6 +178,22 @@
     }
 
     // CLI Utility Methods
+    public void printSettings(ResultLogger out)
+    {
+        out.printf("  Max Columns Per Key: %d%n",maxColumnsPerKey);
+        out.printf("  Column Names: %s%n",namestrs);
+        out.printf("  Comparator: %s%n", comparator);
+        out.printf("  Timestamp: %s%n", timestamp);
+        out.printf("  Variable Column Count: %b%n", variableColumnCount);
+        out.printf("  Slice: %b%n", slice);
+        if (sizeDistribution != null){
+            out.println("  Size Distribution: " + sizeDistribution.getConfigAsString());
+        };
+        if (sizeDistribution != null){
+            out.println("  Count Distribution: " + countDistribution.getConfigAsString());
+        };
+    }
+
 
     static SettingsColumn get(Map<String, String[]> clArgs)
     {
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommand.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommand.java
index bfd8529..c3a171e 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommand.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommand.java
@@ -31,6 +31,7 @@
 
 import org.apache.cassandra.stress.operations.OpDistributionFactory;
 import org.apache.cassandra.stress.util.JavaDriverClient;
+import org.apache.cassandra.stress.util.ResultLogger;
 import org.apache.cassandra.thrift.ConsistencyLevel;
 
 // Generic command settings - common to read/write/etc
@@ -172,6 +173,27 @@
 
     // CLI Utility Methods
 
+    public void printSettings(ResultLogger out)
+    {
+        out.printf("  Type: %s%n", type.toString().toLowerCase());
+        out.printf("  Count: %,d%n", count);
+        if (durationUnits != null)
+        {
+            out.printf("  Duration: %,d %s%n", duration, durationUnits.toString());
+        }
+        out.printf("  No Warmup: %s%n", noWarmup);
+        out.printf("  Consistency Level: %s%n", consistencyLevel.toString());
+        if (targetUncertainty != -1)
+        {
+            out.printf("  Target Uncertainty: %.3f%n", targetUncertainty);
+            out.printf("  Minimum Uncertainty Measurements: %,d%n", minimumUncertaintyMeasurements);
+            out.printf("  Maximum Uncertainty Measurements: %,d%n", maximumUncertaintyMeasurements);
+        } else {
+            out.printf("  Target Uncertainty: not applicable%n");
+        }
+    }
+
+
     static SettingsCommand get(Map<String, String[]> clArgs)
     {
         for (Command cmd : Command.values())
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommandPreDefined.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommandPreDefined.java
index c2f2591..8875576 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommandPreDefined.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommandPreDefined.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.stress.settings;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
@@ -26,6 +26,7 @@
 import java.util.Collections;
 import java.util.List;
 
+import org.apache.cassandra.stress.StressAction.MeasurementSink;
 import org.apache.cassandra.stress.generate.DistributionFactory;
 import org.apache.cassandra.stress.generate.PartitionGenerator;
 import org.apache.cassandra.stress.generate.SeedManager;
@@ -37,7 +38,8 @@
 import org.apache.cassandra.stress.operations.OpDistribution;
 import org.apache.cassandra.stress.operations.OpDistributionFactory;
 import org.apache.cassandra.stress.operations.predefined.PredefinedOperation;
-import org.apache.cassandra.stress.util.Timing;
+import org.apache.cassandra.stress.report.Timer;
+import org.apache.cassandra.stress.util.ResultLogger;
 
 // Settings unique to the mixed command type
 public class SettingsCommandPreDefined extends SettingsCommand
@@ -45,15 +47,18 @@
 
     public final DistributionFactory add;
     public final int keySize;
+    public final Options options;
 
     public OpDistributionFactory getFactory(final StressSettings settings)
     {
         final SeedManager seeds = new SeedManager(settings);
         return new OpDistributionFactory()
         {
-            public OpDistribution get(Timing timing, int sampleCount, boolean isWarmup)
+            public OpDistribution get(boolean isWarmup, MeasurementSink sink)
             {
-                return new FixedOpDistribution(PredefinedOperation.operation(type, timing.newTimer(type.toString(), sampleCount),
+                final Timer timer1 = new Timer(type.toString(), sink);
+                final Timer timer = timer1;
+                return new FixedOpDistribution(PredefinedOperation.operation(type, timer,
                                                newGenerator(settings), seeds, settings, add));
             }
 
@@ -85,6 +90,7 @@
     public SettingsCommandPreDefined(Command type, Options options)
     {
         super(type, options.parent);
+        this.options = options;
         add = options.add.get();
         keySize = Integer.parseInt(options.keysize.value());
     }
@@ -116,6 +122,13 @@
 
     // CLI utility methods
 
+    public void printSettings(ResultLogger out)
+    {
+        super.printSettings(out);
+        out.printf("  Key Size (bytes): %d%n", keySize);
+        out.printf("  Counter Increment Distibution: %s%n", options.add.getOptionAsString());
+    }
+
     public static SettingsCommandPreDefined build(Command type, String[] params)
     {
         GroupedOptions options = GroupedOptions.select(params,
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommandPreDefinedMixed.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommandPreDefinedMixed.java
index dd11452..72f6c86 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommandPreDefinedMixed.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommandPreDefinedMixed.java
@@ -30,7 +30,8 @@
 import org.apache.cassandra.stress.operations.OpDistributionFactory;
 import org.apache.cassandra.stress.operations.SampledOpDistributionFactory;
 import org.apache.cassandra.stress.operations.predefined.PredefinedOperation;
-import org.apache.cassandra.stress.util.Timer;
+import org.apache.cassandra.stress.report.Timer;
+import org.apache.cassandra.stress.util.ResultLogger;
 
 // Settings unique to the mixed command type
 public class SettingsCommandPreDefinedMixed extends SettingsCommandPreDefined
@@ -39,6 +40,7 @@
     // Ratios for selecting commands - index for each Command, NaN indicates the command is not requested
     private final Map<Command, Double> ratios;
     private final DistributionFactory clustering;
+    private final Options options;
 
     public SettingsCommandPreDefinedMixed(Options options)
     {
@@ -46,6 +48,7 @@
 
         clustering = options.clustering.get();
         ratios = options.probabilities.ratios();
+        this.options = options;
         if (ratios.size() == 0)
             throw new IllegalArgumentException("Must specify at least one command with a non-zero ratio");
     }
@@ -109,6 +112,13 @@
 
     }
 
+    public void printSettings(ResultLogger out)
+    {
+        super.printSettings(out);
+        out.printf("  Command Ratios: %s%n", ratios);
+        out.printf("  Command Clustering Distribution: %s%n", options.clustering.getOptionAsString());
+    }
+
     // CLI utility methods
 
     public static SettingsCommandPreDefinedMixed build(String[] params)
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommandUser.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommandUser.java
index 36cbefe..4c7ad91 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommandUser.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsCommandUser.java
@@ -36,7 +36,8 @@
 import org.apache.cassandra.stress.generate.TokenRangeIterator;
 import org.apache.cassandra.stress.operations.OpDistributionFactory;
 import org.apache.cassandra.stress.operations.SampledOpDistributionFactory;
-import org.apache.cassandra.stress.util.Timer;
+import org.apache.cassandra.stress.report.Timer;
+import org.apache.cassandra.stress.util.ResultLogger;
 
 // Settings unique to the mixed command type
 public class SettingsCommandUser extends SettingsCommand
@@ -46,11 +47,13 @@
     private final Map<String, Double> ratios;
     private final DistributionFactory clustering;
     public final StressProfile profile;
+    private final Options options;
 
     public SettingsCommandUser(Options options)
     {
         super(Command.USER, options.parent);
 
+        this.options = options;
         clustering = options.clustering.get();
         ratios = options.ops.ratios();
 
@@ -122,6 +125,16 @@
 
     // CLI utility methods
 
+    public void printSettings(ResultLogger out)
+    {
+        super.printSettings(out);
+        out.printf("  Command Ratios: %s%n", ratios);
+        out.printf("  Command Clustering Distribution: %s%n", options.clustering.getOptionAsString());
+        out.printf("  Profile File: %s%n", options.profile.value());
+        // profile.noSettings(out);
+    }
+
+
     public static SettingsCommandUser build(String[] params)
     {
         GroupedOptions options = GroupedOptions.select(params,
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsErrors.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsErrors.java
index 66daac4..41a3dbd 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsErrors.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsErrors.java
@@ -26,16 +26,20 @@
 import java.util.List;
 import java.util.Map;
 
+import org.apache.cassandra.stress.util.ResultLogger;
+
 public class SettingsErrors implements Serializable
 {
 
     public final boolean ignore;
     public final int tries;
+    public final boolean skipReadValidation;
 
     public SettingsErrors(Options options)
     {
         ignore = options.ignore.setByUser();
         this.tries = Math.max(1, Integer.parseInt(options.retries.value()) + 1);
+        skipReadValidation = options.skipReadValidation.setByUser();
     }
 
     // Option Declarations
@@ -44,15 +48,21 @@
     {
         final OptionSimple retries = new OptionSimple("retries=", "[0-9]+", "9", "Number of tries to perform for each operation before failing", false);
         final OptionSimple ignore = new OptionSimple("ignore", "", null, "Do not fail on errors", false);
-
+        final OptionSimple skipReadValidation = new OptionSimple("skip-read-validation", "", null, "Skip read validation and message output", false);
         @Override
         public List<? extends Option> options()
         {
-            return Arrays.asList(retries, ignore);
+            return Arrays.asList(retries, ignore, skipReadValidation);
         }
     }
 
     // CLI Utility Methods
+    public void printSettings(ResultLogger out)
+    {
+        out.printf("  Ignore: %b%n", ignore);
+        out.printf("  Tries: %d%n", tries);
+    }
+
 
     public static SettingsErrors get(Map<String, String[]> clArgs)
     {
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsGraph.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsGraph.java
new file mode 100644
index 0000000..90bb99a
--- /dev/null
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsGraph.java
@@ -0,0 +1,136 @@
+package org.apache.cassandra.stress.settings;
+/*
+ *
+ * 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.
+ *
+ */
+
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cassandra.stress.util.ResultLogger;
+
+public class SettingsGraph implements Serializable
+{
+    public final String file;
+    public final String revision;
+    public final String title;
+    public final String operation;
+    public final File temporaryLogFile;
+
+    public SettingsGraph(GraphOptions options, SettingsCommand stressCommand)
+    {
+        file = options.file.value();
+        revision = options.revision.value();
+        title = options.revision.value() == null
+            ? "cassandra-stress - " + new SimpleDateFormat("yyyy-mm-dd hh:mm:ss").format(new Date())
+            : options.title.value();
+
+        operation = options.operation.value() == null
+            ? stressCommand.type.name()
+            : options.operation.value();
+
+        if (inGraphMode())
+        {
+            try
+            {
+                temporaryLogFile = File.createTempFile("cassandra-stress", ".log");
+            }
+            catch (IOException e)
+            {
+                throw new RuntimeException("Cannot open temporary file");
+            }
+        }
+        else
+        {
+            temporaryLogFile = null;
+        }
+    }
+
+    public boolean inGraphMode()
+    {
+        return this.file == null ? false : true;
+    }
+
+    // Option Declarations
+    private static final class GraphOptions extends GroupedOptions
+    {
+        final OptionSimple file = new OptionSimple("file=", ".*", null, "HTML file to create or append to", true);
+        final OptionSimple revision = new OptionSimple("revision=", ".*", "unknown", "Unique name to assign to the current configuration being stressed", false);
+        final OptionSimple title = new OptionSimple("title=", ".*", null, "Title for chart (current date by default)", false);
+        final OptionSimple operation = new OptionSimple("op=", ".*", null, "Alternative name for current operation (stress op name used by default)", false);
+
+        @Override
+        public List<? extends Option> options()
+        {
+            return Arrays.asList(file, revision, title, operation);
+        }
+    }
+
+    // CLI Utility Methods
+    public void printSettings(ResultLogger out)
+    {
+        out.println("  File: " + file);
+        out.println("  Revision: " + revision);
+        out.println("  Title: " + title);
+        out.println("  Operation: " + operation);
+    }
+
+
+    public static SettingsGraph get(Map<String, String[]> clArgs, SettingsCommand stressCommand)
+    {
+        String[] params = clArgs.remove("-graph");
+        if (params == null)
+        {
+            return new SettingsGraph(new GraphOptions(), stressCommand);
+        }
+        GraphOptions options = GroupedOptions.select(params, new GraphOptions());
+        if (options == null)
+        {
+            printHelp();
+            System.out.println("Invalid -graph options provided, see output for valid options");
+            System.exit(1);
+        }
+        return new SettingsGraph(options, stressCommand);
+    }
+
+    public static void printHelp()
+    {
+        GroupedOptions.printOptions(System.out, "-graph", new GraphOptions());
+    }
+
+    public static Runnable helpPrinter()
+    {
+        return new Runnable()
+        {
+            @Override
+            public void run()
+            {
+                printHelp();
+            }
+        };
+    }
+}
+
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsInsert.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsInsert.java
index e999e4b..59edf77 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsInsert.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsInsert.java
@@ -29,6 +29,7 @@
 import com.datastax.driver.core.BatchStatement;
 import org.apache.cassandra.stress.generate.DistributionFactory;
 import org.apache.cassandra.stress.generate.RatioDistributionFactory;
+import org.apache.cassandra.stress.util.ResultLogger;
 
 public class SettingsInsert implements Serializable
 {
@@ -72,6 +73,37 @@
     }
 
     // CLI Utility Methods
+    public void printSettings(ResultLogger out)
+    {
+
+        if (revisit != null)
+        {
+            out.println("  Revisits: " +revisit.getConfigAsString());
+        }
+        if (visits != null)
+        {
+            out.println("  Visits: " + visits.getConfigAsString());
+        }
+        if (batchsize != null)
+        {
+            out.println("  Batchsize: " +batchsize.getConfigAsString());
+        }
+        if (batchsize != null)
+        {
+            out.println("  Select Ratio: " +selectRatio.getConfigAsString());
+        }
+        if (rowPopulationRatio != null)
+        {
+            out.println("  Row Population Ratio: " +rowPopulationRatio.getConfigAsString());
+        }
+        if (batchType != null)
+        {
+            out.printf("  Batch Type: %s%n", batchType);
+        } else {
+            out.println("  Batch Type: not batching");
+        }
+    }
+
 
     public static SettingsInsert get(Map<String, String[]> clArgs)
     {
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsLog.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsLog.java
index 5657fb2..ae77e0a 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsLog.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsLog.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.stress.settings;
 /*
- * 
+ *
  * 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
@@ -8,27 +8,27 @@
  * 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.
- * 
+ *
  */
 
 
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.PrintStream;
-import java.io.Serializable;
+import java.io.*;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.cassandra.stress.util.MultiResultLogger;
+import org.apache.cassandra.stress.util.ResultLogger;
+
 public class SettingsLog implements Serializable
 {
     public static enum Level
@@ -37,19 +37,26 @@
     }
 
     public final boolean noSummary;
+    public final boolean noSettings;
     public final File file;
+    public final File hdrFile;
     public final int intervalMillis;
     public final Level level;
 
     public SettingsLog(Options options)
     {
+
         noSummary = options.noSummmary.setByUser();
+        noSettings = options.noSettings.setByUser();
 
         if (options.outputFile.setByUser())
             file = new File(options.outputFile.value());
         else
             file = null;
-
+        if (options.hdrOutputFile.setByUser())
+            hdrFile = new File(options.hdrOutputFile.value());
+        else
+            hdrFile = null;
         String interval = options.interval.value();
         if (interval.endsWith("ms"))
             intervalMillis = Integer.parseInt(interval.substring(0, interval.length() - 2));
@@ -62,9 +69,15 @@
         level = Level.valueOf(options.level.value().toUpperCase());
     }
 
-    public PrintStream getOutput() throws FileNotFoundException
+    public MultiResultLogger getOutput() throws FileNotFoundException
     {
-        return file == null ? new PrintStream(System.out) : new PrintStream(file);
+        // Always print to stdout regardless of whether we're graphing or not
+        MultiResultLogger stream = new MultiResultLogger(new PrintStream(System.out));
+
+        if (file != null)
+            stream.addStream(new PrintStream(file));
+
+        return stream;
     }
 
     // Option Declarations
@@ -72,18 +85,29 @@
     public static final class Options extends GroupedOptions
     {
         final OptionSimple noSummmary = new OptionSimple("no-summary", "", null, "Disable printing of aggregate statistics at the end of a test", false);
+        final OptionSimple noSettings = new OptionSimple("no-settings", "", null, "Disable printing of settings values at start of test", false);
         final OptionSimple outputFile = new OptionSimple("file=", ".*", null, "Log to a file", false);
+        final OptionSimple hdrOutputFile = new OptionSimple("hdrfile=", ".*", null, "Log to a file", false);
         final OptionSimple interval = new OptionSimple("interval=", "[0-9]+(ms|s|)", "1s", "Log progress every <value> seconds or milliseconds", false);
         final OptionSimple level = new OptionSimple("level=", "(minimal|normal|verbose)", "normal", "Logging level (minimal, normal or verbose)", false);
 
         @Override
         public List<? extends Option> options()
         {
-            return Arrays.asList(level, noSummmary, outputFile, interval);
+            return Arrays.asList(level, noSummmary, outputFile, hdrOutputFile, interval, noSettings);
         }
     }
 
     // CLI Utility Methods
+    public void printSettings(ResultLogger out)
+    {
+        out.printf("  No Summary: %b%n", noSummary);
+        out.printf("  No Settings: %b%n", noSettings);
+        out.printf("  File: %s%n", file);
+        out.printf("  Interval Millis: %d%n", intervalMillis);
+        out.printf("  Level: %s%n", level);
+    }
+
 
     public static SettingsLog get(Map<String, String[]> clArgs)
     {
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsMisc.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsMisc.java
index 5334f25..3e69754 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsMisc.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsMisc.java
@@ -21,15 +21,22 @@
  */
 
 
+import java.io.IOException;
 import java.io.PrintStream;
 import java.io.Serializable;
+import java.net.URL;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Resources;
 
 import org.apache.cassandra.stress.generate.Distribution;
 
-public class SettingsMisc implements Serializable
+class SettingsMisc implements Serializable
 {
 
     static boolean maybeDoSpecial(Map<String, String[]> clArgs)
@@ -38,10 +45,12 @@
             return true;
         if (maybePrintDistribution(clArgs))
             return true;
+        if (maybePrintVersion(clArgs))
+            return true;
         return false;
     }
 
-    static final class PrintDistribution extends GroupedOptions
+    private static final class PrintDistribution extends GroupedOptions
     {
         final OptionDistribution dist = new OptionDistribution("dist=", null, "A mathematical distribution");
 
@@ -52,7 +61,8 @@
         }
     }
 
-    static boolean maybePrintDistribution(Map<String, String[]> clArgs)
+
+    private static boolean maybePrintDistribution(Map<String, String[]> clArgs)
     {
         final String[] args = clArgs.get("print");
         if (args == null)
@@ -68,17 +78,17 @@
         return true;
     }
 
-    static void printDistribution(Distribution dist)
+    private static void printDistribution(Distribution dist)
     {
         PrintStream out = System.out;
         out.println("% of samples    Range       % of total");
         String format = "%-16.1f%-12d%12.1f";
         double rangemax = dist.inverseCumProb(1d) / 100d;
-        for (double d : new double[] { 0.1d, 0.2d, 0.3d, 0.4d, 0.5d, 0.6d, 0.7d, 0.8d, 0.9d, 0.95d, 0.99d, 1d })
+        for (double d : new double[]{ 0.1d, 0.2d, 0.3d, 0.4d, 0.5d, 0.6d, 0.7d, 0.8d, 0.9d, 0.95d, 0.99d, 1d })
         {
             double sampleperc = d * 100;
             long max = dist.inverseCumProb(d);
-            double rangeperc = max/ rangemax;
+            double rangeperc = max / rangemax;
             out.println(String.format(format, sampleperc, max, rangeperc));
         }
     }
@@ -98,7 +108,7 @@
                 {
                     String p = clArgs.keySet().iterator().next();
                     if (clArgs.get(p).length == 0)
-                        params = new String[] {p};
+                        params = new String[]{ p };
                 }
             }
             else
@@ -115,6 +125,37 @@
         throw new IllegalArgumentException("Invalid command/option provided to help");
     }
 
+    private static boolean maybePrintVersion(Map<String, String[]> clArgs)
+    {
+        if (clArgs.containsKey("version"))
+        {
+            try
+            {
+                URL url = Resources.getResource("org/apache/cassandra/config/version.properties");
+                System.out.println(parseVersionFile(Resources.toString(url, Charsets.UTF_8)));
+            }
+            catch (IOException e)
+            {
+                e.printStackTrace(System.err);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    static String parseVersionFile(String versionFileContents)
+    {
+        Matcher matcher = Pattern.compile(".*?CassandraVersion=(.*?)$").matcher(versionFileContents);
+        if (matcher.find())
+        {
+            return "Version: " + matcher.group(1);
+        }
+        else
+        {
+            return "Unable to find version information";
+        }
+    }
+
     public static void printHelp()
     {
         System.out.println("Usage:      cassandra-stress <command> [options]");
@@ -151,7 +192,7 @@
         throw new IllegalArgumentException("Invalid command or option provided to command help");
     }
 
-    public static Runnable helpHelpPrinter()
+    static Runnable helpHelpPrinter()
     {
         return () -> {
             System.out.println("Usage: ./bin/cassandra-stress help <command|option>");
@@ -164,7 +205,7 @@
         };
     }
 
-    public static Runnable printHelpPrinter()
+    static Runnable printHelpPrinter()
     {
         return () -> GroupedOptions.printOptions(System.out, "print", new GroupedOptions()
         {
@@ -176,7 +217,7 @@
         });
     }
 
-    public static Runnable sendToDaemonHelpPrinter()
+    static Runnable sendToDaemonHelpPrinter()
     {
         return () -> {
             System.out.println("Usage: -sendto <host>");
@@ -185,7 +226,7 @@
         };
     }
 
-    public static String getSendToDaemon(Map<String, String[]> clArgs)
+    static String getSendToDaemon(Map<String, String[]> clArgs)
     {
         String[] params = clArgs.remove("-send-to");
         if (params == null)
@@ -199,7 +240,5 @@
             System.exit(1);
         }
         return params[0];
-
     }
-
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsMode.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsMode.java
index 8f0ab25..bebfa5f 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsMode.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsMode.java
@@ -30,6 +30,7 @@
 import com.datastax.driver.core.PlainTextAuthProvider;
 import com.datastax.driver.core.ProtocolOptions;
 import com.datastax.driver.core.ProtocolVersion;
+import org.apache.cassandra.stress.util.ResultLogger;
 
 public class SettingsMode implements Serializable
 {
@@ -49,6 +50,7 @@
 
     private final String compression;
 
+
     public SettingsMode(GroupedOptions options)
     {
         if (options instanceof Cql3Options)
@@ -162,8 +164,8 @@
         final OptionSimple user = new OptionSimple("user=", ".+", null, "username", false);
         final OptionSimple password = new OptionSimple("password=", ".+", null, "password", false);
         final OptionSimple authProvider = new OptionSimple("auth-provider=", ".*", null, "Fully qualified implementation of com.datastax.driver.core.AuthProvider", false);
-        final OptionSimple maxPendingPerConnection = new OptionSimple("maxPending=", "[0-9]+", "", "Maximum pending requests per connection", false);
-        final OptionSimple connectionsPerHost = new OptionSimple("connectionsPerHost=", "[0-9]+", "", "Number of connections per host", false);
+        final OptionSimple maxPendingPerConnection = new OptionSimple("maxPending=", "[0-9]+", "128", "Maximum pending requests per connection", false);
+        final OptionSimple connectionsPerHost = new OptionSimple("connectionsPerHost=", "[0-9]+", "8", "Number of connections per host", false);
 
         abstract OptionSimple mode();
         @Override
@@ -205,6 +207,21 @@
     }
 
     // CLI Utility Methods
+    public void printSettings(ResultLogger out)
+    {
+        out.printf("  API: %s%n", api);
+        out.printf("  Connection Style: %s%n", style);
+        out.printf("  CQL Version: %s%n", cqlVersion);
+        out.printf("  Protocol Version: %s%n", protocolVersion);
+        out.printf("  Username: %s%n", username);
+        out.printf("  Password: %s%n", (password==null?password:"*suppressed*"));
+        out.printf("  Auth Provide Class: %s%n", authProviderClassname);
+        out.printf("  Max Pending Per Connection: %d%n", maxPendingPerConnection);
+        out.printf("  Connections Per Host: %d%n", connectionsPerHost);
+        out.printf("  Compression: %s%n", compression);
+
+    }
+
 
     public static SettingsMode get(Map<String, String[]> clArgs)
     {
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsNode.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsNode.java
index ba1fcb5..95339e3 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsNode.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsNode.java
@@ -28,11 +28,13 @@
 import java.util.*;
 
 import com.datastax.driver.core.Host;
+import org.apache.cassandra.stress.util.ResultLogger;
 
 public class SettingsNode implements Serializable
 {
     public final List<String> nodes;
     public final boolean isWhiteList;
+    public final String datacenter;
 
     public SettingsNode(Options options)
     {
@@ -41,9 +43,8 @@
             try
             {
                 String node;
-                List<String> tmpNodes = new ArrayList<String>();
-                BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(options.file.value())));
-                try
+                List<String> tmpNodes = new ArrayList<>();
+                try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(options.file.value()))))
                 {
                     while ((node = in.readLine()) != null)
                     {
@@ -52,10 +53,6 @@
                     }
                     nodes = Arrays.asList(tmpNodes.toArray(new String[tmpNodes.size()]));
                 }
-                finally
-                {
-                    in.close();
-                }
             }
             catch(IOException ioe)
             {
@@ -64,8 +61,12 @@
 
         }
         else
+        {
             nodes = Arrays.asList(options.list.value().split(","));
+        }
+
         isWhiteList = options.whitelist.setByUser();
+        datacenter = options.datacenter.value();
     }
 
     public Set<String> resolveAllPermitted(StressSettings settings)
@@ -135,6 +136,7 @@
 
     public static final class Options extends GroupedOptions
     {
+        final OptionSimple datacenter = new OptionSimple("datacenter=", ".*", null, "Datacenter used for DCAwareRoundRobinLoadPolicy", false);
         final OptionSimple whitelist = new OptionSimple("whitelist", "", null, "Limit communications to the provided nodes", false);
         final OptionSimple file = new OptionSimple("file=", ".*", null, "Node file (one per line)", false);
         final OptionSimple list = new OptionSimple("", "[^=,]+(,[^=,]+)*", "localhost", "comma delimited list of nodes", false);
@@ -142,11 +144,17 @@
         @Override
         public List<? extends Option> options()
         {
-            return Arrays.asList(whitelist, file, list);
+            return Arrays.asList(datacenter, whitelist, file, list);
         }
     }
 
     // CLI Utility Methods
+    public void printSettings(ResultLogger out)
+    {
+        out.println("  Nodes: " + nodes);
+        out.println("  Is White List: " + isWhiteList);
+        out.println("  Datacenter: " + datacenter);
+    }
 
     public static SettingsNode get(Map<String, String[]> clArgs)
     {
@@ -171,13 +179,6 @@
 
     public static Runnable helpPrinter()
     {
-        return new Runnable()
-        {
-            @Override
-            public void run()
-            {
-                printHelp();
-            }
-        };
+        return SettingsNode::printHelp;
     }
 }
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsPopulation.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsPopulation.java
index 53c45ab..66984ed 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsPopulation.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsPopulation.java
@@ -30,6 +30,7 @@
 
 import org.apache.cassandra.stress.generate.DistributionFactory;
 import org.apache.cassandra.stress.generate.PartitionGenerator;
+import org.apache.cassandra.stress.util.ResultLogger;
 
 public class SettingsPopulation implements Serializable
 {
@@ -126,6 +127,26 @@
 
     // CLI Utility Methods
 
+    public void printSettings(ResultLogger out)
+    {
+        if (distribution != null)
+        {
+            out.println("  Distribution: " +distribution.getConfigAsString());
+        }
+
+        if (sequence != null)
+        {
+            out.printf("  Sequence: %d..%d%n", sequence[0], sequence[1]);
+        }
+        if (readlookback != null)
+        {
+            out.println("  Read Look Back: " + readlookback.getConfigAsString());
+        }
+
+        out.printf("  Order: %s%n", order);
+        out.printf("  Wrap: %b%n", wrap);
+    }
+
     public static SettingsPopulation get(Map<String, String[]> clArgs, SettingsCommand command)
     {
         // set default size to number of commands requested, unless set to err convergence, then use 1M
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsPort.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsPort.java
index 88ff9a6..73a4fb4 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsPort.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsPort.java
@@ -26,6 +26,8 @@
 import java.util.List;
 import java.util.Map;
 
+import org.apache.cassandra.stress.util.ResultLogger;
+
 public class SettingsPort implements Serializable
 {
 
@@ -56,6 +58,13 @@
     }
 
     // CLI Utility Methods
+    public void printSettings(ResultLogger out)
+    {
+        out.printf("  Native Port: %d%n", nativePort);
+        out.printf("  Thrift Port: %d%n", thriftPort);
+        out.printf("  JMX Port: %d%n", jmxPort);
+    }
+
 
     public static SettingsPort get(Map<String, String[]> clArgs)
     {
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsRate.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsRate.java
index 0486678..c7d322a 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsRate.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsRate.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.stress.settings;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
@@ -26,6 +26,8 @@
 import java.util.List;
 import java.util.Map;
 
+import org.apache.cassandra.stress.util.ResultLogger;
+
 public class SettingsRate implements Serializable
 {
 
@@ -33,14 +35,22 @@
     public final int minThreads;
     public final int maxThreads;
     public final int threadCount;
-    public final int opRateTargetPerSecond;
+    public final int opsPerSecond;
+    public final boolean isFixed;
 
     public SettingsRate(ThreadOptions options)
     {
         auto = false;
         threadCount = Integer.parseInt(options.threads.value());
-        String rateOpt = options.rate.value();
-        opRateTargetPerSecond = Integer.parseInt(rateOpt.substring(0, rateOpt.length() - 2));
+        String throttleOpt = options.throttle.value();
+        String fixedOpt = options.fixed.value();
+        int throttle = Integer.parseInt(throttleOpt.substring(0, throttleOpt.length() - 2));
+        int fixed = Integer.parseInt(fixedOpt.substring(0, fixedOpt.length() - 2));
+        if(throttle != 0 && fixed != 0)
+            throw new IllegalArgumentException("can't have both fixed and throttle set, choose one.");
+        opsPerSecond = Math.max(fixed, throttle);
+        isFixed = (opsPerSecond == fixed);
+
         minThreads = -1;
         maxThreads = -1;
     }
@@ -51,7 +61,8 @@
         this.minThreads = Integer.parseInt(auto.minThreads.value());
         this.maxThreads = Integer.parseInt(auto.maxThreads.value());
         this.threadCount = -1;
-        this.opRateTargetPerSecond = 0;
+        this.opsPerSecond = 0;
+        isFixed = false;
     }
 
 
@@ -73,17 +84,31 @@
     private static final class ThreadOptions extends GroupedOptions
     {
         final OptionSimple threads = new OptionSimple("threads=", "[0-9]+", null, "run this many clients concurrently", true);
-        final OptionSimple rate = new OptionSimple("limit=", "[0-9]+/s", "0/s", "limit operations per second across all clients", false);
+        final OptionSimple throttle = new OptionSimple("throttle=", "[0-9]+/s", "0/s", "throttle operations per second across all clients to a maximum rate (or less) with no implied schedule", false);
+        final OptionSimple fixed = new OptionSimple("fixed=", "[0-9]+/s", "0/s", "expect fixed rate of operations per second across all clients with implied schedule", false);
 
         @Override
         public List<? extends Option> options()
         {
-            return Arrays.asList(threads, rate);
+            return Arrays.asList(threads, throttle, fixed);
         }
     }
 
     // CLI Utility Methods
 
+    public void printSettings(ResultLogger out)
+    {
+        out.printf("  Auto: %b%n", auto);
+        if (auto)
+        {
+            out.printf("  Min Threads: %d%n", minThreads);
+            out.printf("  Max Threads: %d%n", maxThreads);
+        } else {
+            out.printf("  Thread Count: %d%n", threadCount);
+            out.printf("  OpsPer Sec: %d%n", opsPerSecond);
+        }
+    }
+
     public static SettingsRate get(Map<String, String[]> clArgs, SettingsCommand command)
     {
         String[] params = clArgs.remove("-rate");
@@ -126,14 +151,7 @@
 
     public static Runnable helpPrinter()
     {
-        return new Runnable()
-        {
-            @Override
-            public void run()
-            {
-                printHelp();
-            }
-        };
+        return SettingsRate::printHelp;
     }
 }
 
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsSamples.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsSamples.java
deleted file mode 100644
index 7a9f484..0000000
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsSamples.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package org.apache.cassandra.stress.settings;
-/*
- * 
- * 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.
- * 
- */
-
-
-import java.io.Serializable;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-
-public class SettingsSamples implements Serializable
-{
-
-    public final int liveCount;
-    public final int historyCount;
-    public final int reportCount;
-
-    public SettingsSamples(SampleOptions options)
-    {
-        liveCount = (int) OptionDistribution.parseLong(options.liveCount.value());
-        historyCount = (int) OptionDistribution.parseLong(options.historyCount.value());
-        reportCount = (int) OptionDistribution.parseLong(options.reportCount.value());
-    }
-
-    // Option Declarations
-
-    private static final class SampleOptions extends GroupedOptions
-    {
-        final OptionSimple historyCount = new OptionSimple("history=", "[0-9]+[bmk]?", "50K", "The number of samples to save across the whole run", false);
-        final OptionSimple liveCount = new OptionSimple("live=", "[0-9]+[bmk]?", "1M", "The number of samples to save between reports", false);
-        final OptionSimple reportCount = new OptionSimple("report=", "[0-9]+[bmk]?", "100K", "The maximum number of samples to use when building a report", false);
-
-        @Override
-        public List<? extends Option> options()
-        {
-            return Arrays.asList(historyCount, liveCount, reportCount);
-        }
-    }
-
-    // CLI Utility Methods
-
-    public static SettingsSamples get(Map<String, String[]> clArgs)
-    {
-        String[] params = clArgs.remove("-sample");
-        if (params == null)
-        {
-            return new SettingsSamples(new SampleOptions());
-        }
-        SampleOptions options = GroupedOptions.select(params, new SampleOptions());
-        if (options == null)
-        {
-            printHelp();
-            System.out.println("Invalid -sample options provided, see output for valid options");
-            System.exit(1);
-        }
-        return new SettingsSamples(options);
-    }
-
-    public static void printHelp()
-    {
-        GroupedOptions.printOptions(System.out, "-sample", new SampleOptions());
-    }
-
-    public static Runnable helpPrinter()
-    {
-        return new Runnable()
-        {
-            @Override
-            public void run()
-            {
-                printHelp();
-            }
-        };
-    }
-}
-
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsSchema.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsSchema.java
index 5c437a9..fc65c9a 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsSchema.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsSchema.java
@@ -27,8 +27,8 @@
 
 import com.datastax.driver.core.exceptions.AlreadyExistsException;
 import org.apache.cassandra.stress.util.JavaDriverClient;
+import org.apache.cassandra.stress.util.ResultLogger;
 import org.apache.cassandra.thrift.*;
-import org.apache.cassandra.thrift.ConsistencyLevel;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 public class SettingsSchema implements Serializable
@@ -300,6 +300,17 @@
     }
 
     // CLI Utility Methods
+    public void printSettings(ResultLogger out)
+    {
+        out.println("  Keyspace: " + keyspace);
+        out.println("  Replication Strategy: " + replicationStrategy);
+        out.println("  Replication Strategy Pptions: " + replicationStrategyOptions);
+
+        out.println("  Table Compression: " + compression);
+        out.println("  Table Compaction Strategy: " + compactionStrategy);
+        out.println("  Table Compaction Strategy Options: " + compactionStrategyOptions);
+    }
+
 
     public static SettingsSchema get(Map<String, String[]> clArgs, SettingsCommand command)
     {
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsTokenRange.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsTokenRange.java
index 8fb0048..d8b6701 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsTokenRange.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsTokenRange.java
@@ -18,19 +18,24 @@
 
 package org.apache.cassandra.stress.settings;
 
+import java.io.Serializable;
 import java.util.List;
 import java.util.Map;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Ints;
 
-public class SettingsTokenRange
+import org.apache.cassandra.stress.util.ResultLogger;
+
+public class SettingsTokenRange implements Serializable
 {
     public final boolean wrap;
     public final int splitFactor;
+    private final TokenRangeOptions options;
 
-    public SettingsTokenRange(TokenRangeOptions options)
+    private SettingsTokenRange(TokenRangeOptions options)
     {
+        this.options = options;
         this.wrap = options.wrap.setByUser();
         this.splitFactor = Ints.checkedCast(OptionDistribution.parseLong(options.splitFactor.value()));
     }
@@ -65,6 +70,12 @@
         return new SettingsTokenRange(options);
     }
 
+    public void printSettings(ResultLogger out)
+    {
+        out.printf("  Wrap: %b%n", wrap);
+        out.printf("  Split Factor: %d%n", splitFactor);
+    }
+
     public static void printHelp()
     {
         GroupedOptions.printOptions(System.out, "-tokenrange", new TokenRangeOptions());
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsTransport.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsTransport.java
index a253c07..4981a87 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsTransport.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsTransport.java
@@ -28,6 +28,7 @@
 import java.util.Map;
 
 import org.apache.cassandra.config.EncryptionOptions;
+import org.apache.cassandra.stress.util.ResultLogger;
 import org.apache.cassandra.thrift.ITransportFactory;
 import org.apache.cassandra.thrift.SSLTransportFactory;
 import org.apache.cassandra.thrift.TFramedTransportFactory;
@@ -146,6 +147,10 @@
     }
 
     // CLI Utility Methods
+    public void printSettings(ResultLogger out)
+    {
+        out.println("  " + options.getOptionAsString());
+    }
 
     public static SettingsTransport get(Map<String, String[]> clArgs)
     {
diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/StressSettings.java b/tools/stress/src/org/apache/cassandra/stress/settings/StressSettings.java
index f16f1aa..deb67db 100644
--- a/tools/stress/src/org/apache/cassandra/stress/settings/StressSettings.java
+++ b/tools/stress/src/org/apache/cassandra/stress/settings/StressSettings.java
@@ -1,6 +1,6 @@
 package org.apache.cassandra.stress.settings;
 /*
- * 
+ *
  * 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
@@ -8,16 +8,16 @@
  * 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.
- * 
+ *
  */
 
 
@@ -28,6 +28,7 @@
 import com.google.common.collect.ImmutableMap;
 import org.apache.cassandra.config.EncryptionOptions;
 import org.apache.cassandra.stress.util.JavaDriverClient;
+import org.apache.cassandra.stress.util.ResultLogger;
 import org.apache.cassandra.stress.util.SimpleThriftClient;
 import org.apache.cassandra.stress.util.SmartThriftClient;
 import org.apache.cassandra.stress.util.ThriftClient;
@@ -45,7 +46,6 @@
     public final SettingsPopulation generate;
     public final SettingsInsert insert;
     public final SettingsColumn columns;
-    public final SettingsSamples samples;
     public final SettingsErrors errors;
     public final SettingsLog log;
     public final SettingsMode mode;
@@ -54,16 +54,30 @@
     public final SettingsTransport transport;
     public final SettingsPort port;
     public final String sendToDaemon;
-
+    public final SettingsGraph graph;
     public final SettingsTokenRange tokenRange;
-    public StressSettings(SettingsCommand command, SettingsRate rate, SettingsPopulation generate, SettingsInsert insert, SettingsColumn columns, SettingsSamples samples, SettingsErrors errors, SettingsLog log, SettingsMode mode, SettingsNode node, SettingsSchema schema, SettingsTransport transport, SettingsPort port, String sendToDaemon, SettingsTokenRange tokenRange)
+
+    public StressSettings(SettingsCommand command,
+                          SettingsRate rate,
+                          SettingsPopulation generate,
+                          SettingsInsert insert,
+                          SettingsColumn columns,
+                          SettingsErrors errors,
+                          SettingsLog log,
+                          SettingsMode mode,
+                          SettingsNode node,
+                          SettingsSchema schema,
+                          SettingsTransport transport,
+                          SettingsPort port,
+                          String sendToDaemon,
+                          SettingsGraph graph,
+                          SettingsTokenRange tokenRange)
     {
         this.command = command;
         this.rate = rate;
         this.insert = insert;
         this.generate = generate;
         this.columns = columns;
-        this.samples = samples;
         this.errors = errors;
         this.log = log;
         this.mode = mode;
@@ -72,6 +86,7 @@
         this.transport = transport;
         this.port = port;
         this.sendToDaemon = sendToDaemon;
+        this.graph = graph;
         this.tokenRange = tokenRange;
     }
 
@@ -167,6 +182,8 @@
     }
 
     private static volatile JavaDriverClient client;
+    private static volatile int numFailures;
+    private static int MAX_NUM_FAILURES = 10;
 
     public JavaDriverClient getJavaDriverClient()
     {
@@ -178,9 +195,12 @@
         if (client != null)
             return client;
 
-        try
+        synchronized (this)
         {
-            synchronized (this)
+            if (numFailures >= MAX_NUM_FAILURES)
+                throw new RuntimeException("Failed to create client too many times");
+
+            try
             {
                 if (client != null)
                     return client;
@@ -193,10 +213,11 @@
 
                 return client = c;
             }
-        }
-        catch (Exception e)
-        {
-            throw new RuntimeException(e);
+            catch (Exception e)
+            {
+                numFailures +=1;
+                throw new RuntimeException(e);
+            }
         }
     }
 
@@ -210,22 +231,14 @@
 
     public static StressSettings parse(String[] args)
     {
-        try
-        {
-            args = repairParams(args);
-            final Map<String, String[]> clArgs = parseMap(args);
-            if (clArgs.containsKey("legacy"))
-                return Legacy.build(Arrays.copyOfRange(args, 1, args.length));
-            if (SettingsMisc.maybeDoSpecial(clArgs))
-                System.exit(1);
-            return get(clArgs);
-        }
-        catch (IllegalArgumentException e)
-        {
-            System.out.println(e.getMessage());
-            System.exit(1);
-            throw new AssertionError();
-        }
+        args = repairParams(args);
+        final Map<String, String[]> clArgs = parseMap(args);
+        if (clArgs.containsKey("legacy"))
+            return Legacy.build(Arrays.copyOfRange(args, 1, args.length));
+        if (SettingsMisc.maybeDoSpecial(clArgs))
+            return null;
+        return get(clArgs);
+
     }
 
     private static String[] repairParams(String[] args)
@@ -257,13 +270,13 @@
         SettingsTokenRange tokenRange = SettingsTokenRange.get(clArgs);
         SettingsInsert insert = SettingsInsert.get(clArgs);
         SettingsColumn columns = SettingsColumn.get(clArgs);
-        SettingsSamples samples = SettingsSamples.get(clArgs);
         SettingsErrors errors = SettingsErrors.get(clArgs);
         SettingsLog log = SettingsLog.get(clArgs);
         SettingsMode mode = SettingsMode.get(clArgs);
         SettingsNode node = SettingsNode.get(clArgs);
         SettingsSchema schema = SettingsSchema.get(clArgs, command);
         SettingsTransport transport = SettingsTransport.get(clArgs);
+        SettingsGraph graph = SettingsGraph.get(clArgs, command);
         if (!clArgs.isEmpty())
         {
             printHelp();
@@ -280,7 +293,8 @@
             }
             System.exit(1);
         }
-        return new StressSettings(command, rate, generate, insert, columns, samples, errors, log, mode, node, schema, transport, port, sendToDaemon, tokenRange);
+
+        return new StressSettings(command, rate, generate, insert, columns, errors, log, mode, node, schema, transport, port, sendToDaemon, graph, tokenRange);
     }
 
     private static Map<String, String[]> parseMap(String[] args)
@@ -323,6 +337,54 @@
         SettingsMisc.printHelp();
     }
 
+    public void printSettings(ResultLogger out)
+    {
+        out.println("******************** Stress Settings ********************");
+        // done
+        out.println("Command:");
+        command.printSettings(out);
+        out.println("Rate:");
+        rate.printSettings(out);
+        out.println("Population:");
+        generate.printSettings(out);
+        out.println("Insert:");
+        insert.printSettings(out);
+        if (command.type != Command.USER)
+        {
+            out.println("Columns:");
+            columns.printSettings(out);
+        }
+        out.println("Errors:");
+        errors.printSettings(out);
+        out.println("Log:");
+        log.printSettings(out);
+        out.println("Mode:");
+        mode.printSettings(out);
+        out.println("Node:");
+        node.printSettings(out);
+        out.println("Schema:");
+        schema.printSettings(out);
+        out.println("Transport:");
+        transport.printSettings(out);
+        out.println("Port:");
+        port.printSettings(out);
+        out.println("Send To Daemon:");
+        out.printf("  " + (sendToDaemon != null ? sendToDaemon : "*not set*") + "%n");
+        out.println("Graph:");
+        graph.printSettings(out);
+        out.println("TokenRange:");
+        tokenRange.printSettings(out);
+
+        if (command.type == Command.USER)
+        {
+            out.println();
+            out.println("******************** Profile ********************");
+            ((SettingsCommandUser) command).profile.printSettings(out, this);
+        }
+        out.println();
+
+    }
+
     public synchronized void disconnect()
     {
         if (client == null)
diff --git a/tools/stress/src/org/apache/cassandra/stress/util/JavaDriverClient.java b/tools/stress/src/org/apache/cassandra/stress/util/JavaDriverClient.java
index 2720d96..c8b26ab 100644
--- a/tools/stress/src/org/apache/cassandra/stress/util/JavaDriverClient.java
+++ b/tools/stress/src/org/apache/cassandra/stress/util/JavaDriverClient.java
@@ -29,6 +29,8 @@
 import com.google.common.net.HostAndPort;
 import com.datastax.driver.core.*;
 import com.datastax.driver.core.policies.DCAwareRoundRobinPolicy;
+import com.datastax.driver.core.policies.LoadBalancingPolicy;
+import com.datastax.driver.core.policies.TokenAwarePolicy;
 import com.datastax.driver.core.policies.WhiteListPolicy;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 import io.netty.util.internal.logging.Slf4JLoggerFactory;
@@ -56,7 +58,7 @@
     private final EncryptionOptions.ClientEncryptionOptions encryptionOptions;
     private Cluster cluster;
     private Session session;
-    private final WhiteListPolicy whitelist;
+    private final LoadBalancingPolicy loadBalancingPolicy;
 
     private static final ConcurrentMap<String, PreparedStatement> stmts = new ConcurrentHashMap<>();
 
@@ -74,11 +76,8 @@
         this.password = settings.mode.password;
         this.authProvider = settings.mode.authProvider;
         this.encryptionOptions = encryptionOptions;
-        if (settings.node.isWhiteList)
-            whitelist = new WhiteListPolicy(DCAwareRoundRobinPolicy.builder().build(), settings.node.resolveAll(settings.port.nativePort));
-        else
-            whitelist = null;
-        connectionsPerHost = settings.mode.connectionsPerHost == null ? 8 : settings.mode.connectionsPerHost;
+        this.loadBalancingPolicy = loadBalancingPolicy(settings);
+        this.connectionsPerHost = settings.mode.connectionsPerHost == null ? 8 : settings.mode.connectionsPerHost;
 
         int maxThreadCount = 0;
         if (settings.rate.auto)
@@ -93,6 +92,22 @@
         maxPendingPerConnection = settings.mode.maxPendingPerConnection == null ? Math.max(128, requestsPerConnection ) : settings.mode.maxPendingPerConnection;
     }
 
+    private LoadBalancingPolicy loadBalancingPolicy(StressSettings settings)
+    {
+        DCAwareRoundRobinPolicy.Builder policyBuilder = DCAwareRoundRobinPolicy.builder();
+        if (settings.node.datacenter != null)
+            policyBuilder.withLocalDc(settings.node.datacenter);
+
+        LoadBalancingPolicy ret = null;
+        if (settings.node.datacenter != null)
+            ret = policyBuilder.build();
+
+        if (settings.node.isWhiteList)
+            ret = new WhiteListPolicy(ret == null ? policyBuilder.build() : ret, settings.node.resolveAll(settings.port.nativePort));
+
+        return new TokenAwarePolicy(ret == null ? policyBuilder.build() : ret);
+    }
+
     public PreparedStatement prepare(String query)
     {
         PreparedStatement stmt = stmts.get(query);
@@ -131,8 +146,8 @@
                                                 .withoutJMXReporting()
                                                 .withProtocolVersion(protocolVersion)
                                                 .withoutMetrics(); // The driver uses metrics 3 with conflict with our version
-        if (whitelist != null)
-            clusterBuilder.withLoadBalancingPolicy(whitelist);
+        if (loadBalancingPolicy != null)
+            clusterBuilder.withLoadBalancingPolicy(loadBalancingPolicy);
         clusterBuilder.withCompression(compression);
         if (encryptionOptions.enabled)
         {
@@ -188,7 +203,6 @@
 
     public ResultSet executePrepared(PreparedStatement stmt, List<Object> queryParams, org.apache.cassandra.db.ConsistencyLevel consistency)
     {
-
         stmt.setConsistencyLevel(from(consistency));
         BoundStatement bstmt = stmt.bind((Object[]) queryParams.toArray(new Object[queryParams.size()]));
         return getSession().execute(bstmt);
diff --git a/tools/stress/src/org/apache/cassandra/stress/util/MultiResultLogger.java b/tools/stress/src/org/apache/cassandra/stress/util/MultiResultLogger.java
new file mode 100644
index 0000000..3d2103c
--- /dev/null
+++ b/tools/stress/src/org/apache/cassandra/stress/util/MultiResultLogger.java
@@ -0,0 +1,78 @@
+/*
+ * 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.stress.util;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MultiResultLogger implements ResultLogger
+{
+    private final List<PrintStream> streams = new ArrayList<>();
+
+    public MultiResultLogger(PrintStream printStream)
+    {
+        streams.add(printStream);
+    }
+
+    public void println(String line)
+    {
+        for (PrintStream stream : streams)
+        {
+            stream.println(line);
+        }
+    }
+
+    public void println()
+    {
+        for (PrintStream stream : streams)
+        {
+            stream.println();
+        }
+    }
+
+    public void printException(Exception e)
+    {
+        for (PrintStream stream : streams)
+        {
+            e.printStackTrace(stream);
+        }
+    }
+
+    public void flush()
+    {
+        for (PrintStream stream : streams)
+        {
+            stream.flush();
+        }
+    }
+
+    public void printf(String s, Object... args)
+    {
+        for (PrintStream stream : streams)
+        {
+            stream.printf(s, args);
+        }
+    }
+
+    public void addStream(PrintStream additionalPrintStream)
+    {
+        streams.add(additionalPrintStream);
+    }
+}
diff --git a/tools/stress/src/org/apache/cassandra/stress/util/NoopResultLogger.java b/tools/stress/src/org/apache/cassandra/stress/util/NoopResultLogger.java
new file mode 100644
index 0000000..f1b2d8c
--- /dev/null
+++ b/tools/stress/src/org/apache/cassandra/stress/util/NoopResultLogger.java
@@ -0,0 +1,44 @@
+/*
+ * 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.stress.util;
+
+public class NoopResultLogger implements ResultLogger
+{
+    NoopResultLogger() { }
+
+    public void println(String line)
+    {
+    }
+
+    public void println()
+    {
+    }
+
+    public void printException(Exception e)
+    {
+    }
+
+    public void flush()
+    {
+    }
+
+    public void printf(String s, Object... args)
+    {
+    }
+}
diff --git a/tools/stress/src/org/apache/cassandra/stress/util/ResultLogger.java b/tools/stress/src/org/apache/cassandra/stress/util/ResultLogger.java
new file mode 100644
index 0000000..4097417
--- /dev/null
+++ b/tools/stress/src/org/apache/cassandra/stress/util/ResultLogger.java
@@ -0,0 +1,30 @@
+/*
+ * 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.stress.util;
+
+public interface ResultLogger
+{
+    static final ResultLogger NOOP = new NoopResultLogger();
+
+    void println(String line);
+    void println();
+    void printException(Exception e);
+    void flush();
+    void printf(String s, Object... args);
+}
diff --git a/tools/stress/src/org/apache/cassandra/stress/util/SampleOfLongs.java b/tools/stress/src/org/apache/cassandra/stress/util/SampleOfLongs.java
deleted file mode 100644
index ed54ee0..0000000
--- a/tools/stress/src/org/apache/cassandra/stress/util/SampleOfLongs.java
+++ /dev/null
@@ -1,111 +0,0 @@
-package org.apache.cassandra.stress.util;
-/*
- * 
- * 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.
- * 
- */
-
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Random;
-
-// represents a sample of long (latencies) together with the probability of selection of each sample (i.e. the ratio of
-// samples to total number of events). This is used to ensure that, when merging, the result has samples from each
-// with equal probability
-public final class SampleOfLongs
-{
-
-    // nanos
-    final long[] sample;
-
-    // probability with which each sample was selected
-    final double p;
-
-    SampleOfLongs(long[] sample, int p)
-    {
-        this.sample = sample;
-        this.p = 1 / (float) p;
-    }
-
-    SampleOfLongs(long[] sample, double p)
-    {
-        this.sample = sample;
-        this.p = p;
-    }
-
-    static SampleOfLongs merge(Random rnd, List<SampleOfLongs> merge, int maxSamples)
-    {
-        // grab the lowest probability of selection, and normalise all samples to that
-        double targetp = 1;
-        for (SampleOfLongs sampleOfLongs : merge)
-            targetp = Math.min(targetp, sampleOfLongs.p);
-
-        // calculate how many samples we should encounter
-        int maxLength = 0;
-        for (SampleOfLongs sampleOfLongs : merge)
-            maxLength += sampleOfLongs.sample.length * (targetp / sampleOfLongs.p);
-
-        if (maxLength > maxSamples)
-        {
-            targetp *= maxSamples / (double) maxLength;
-            maxLength = maxSamples;
-        }
-
-        long[] sample = new long[maxLength];
-        int count = 0;
-        out: for (SampleOfLongs latencies : merge)
-        {
-            long[] in = latencies.sample;
-            double p = targetp / latencies.p;
-            for (int i = 0 ; i < in.length ; i++)
-            {
-                if (rnd.nextDouble() < p)
-                {
-                    sample[count++] = in[i];
-                    if (count == maxLength)
-                        break out;
-                }
-            }
-        }
-        if (count != maxLength)
-            sample = Arrays.copyOf(sample, count);
-        Arrays.sort(sample);
-        return new SampleOfLongs(sample, targetp);
-    }
-
-    public double medianLatency()
-    {
-        if (sample.length == 0)
-            return 0;
-        return sample[sample.length >> 1] * 0.000001d;
-    }
-
-    // 0 < rank < 1
-    public double rankLatency(float rank)
-    {
-        if (sample.length == 0)
-            return 0;
-        int index = (int)(rank * sample.length);
-        if (index >= sample.length)
-            index = sample.length - 1;
-        return sample[index] * 0.000001d;
-    }
-
-}
-
diff --git a/tools/stress/src/org/apache/cassandra/stress/util/Timer.java b/tools/stress/src/org/apache/cassandra/stress/util/Timer.java
deleted file mode 100644
index 88e8020..0000000
--- a/tools/stress/src/org/apache/cassandra/stress/util/Timer.java
+++ /dev/null
@@ -1,167 +0,0 @@
-package org.apache.cassandra.stress.util;
-/*
- * 
- * 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.
- * 
- */
-
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ThreadLocalRandom;
-
-// a timer - this timer must be used by a single thread, and co-ordinates with other timers by
-public final class Timer
-{
-    private ThreadLocalRandom rnd;
-
-    // in progress snap start
-    private long sampleStartNanos;
-
-    // each entry is present with probability 1/p(opCount) or 1/(p(opCount)-1)
-    private final long[] sample;
-    private int opCount;
-
-    // aggregate info
-    private long errorCount;
-    private long partitionCount;
-    private long rowCount;
-    private long total;
-    private long max;
-    private long maxStart;
-    private long upToDateAsOf;
-    private long lastSnap = System.nanoTime();
-
-    // communication with summary/logging thread
-    private volatile CountDownLatch reportRequest;
-    volatile TimingInterval report;
-    private volatile TimingInterval finalReport;
-
-    public Timer(int sampleCount)
-    {
-        int powerOf2 = 32 - Integer.numberOfLeadingZeros(sampleCount - 1);
-        this.sample = new long[1 << powerOf2];
-    }
-
-    public void init()
-    {
-        rnd = ThreadLocalRandom.current();
-    }
-
-    public void start(){
-        // decide if we're logging this event
-        sampleStartNanos = System.nanoTime();
-    }
-
-    private int p(int index)
-    {
-        return 1 + (index / sample.length);
-    }
-
-    public boolean running()
-    {
-        return finalReport == null;
-    }
-
-    public void stop(long partitionCount, long rowCount, boolean error)
-    {
-        maybeReport();
-        long now = System.nanoTime();
-        long time = now - sampleStartNanos;
-        if (rnd.nextInt(p(opCount)) == 0)
-            sample[index(opCount)] = time;
-        if (time > max)
-        {
-            maxStart = sampleStartNanos;
-            max = time;
-        }
-        total += time;
-        opCount += 1;
-        this.partitionCount += partitionCount;
-        this.rowCount += rowCount;
-        if (error)
-            this.errorCount++;
-        upToDateAsOf = now;
-    }
-
-    private int index(int count)
-    {
-        return count & (sample.length - 1);
-    }
-
-    private TimingInterval buildReport()
-    {
-        final List<SampleOfLongs> sampleLatencies = Arrays.asList
-                (       new SampleOfLongs(Arrays.copyOf(sample, index(opCount)), p(opCount)),
-                        new SampleOfLongs(Arrays.copyOfRange(sample, index(opCount), Math.min(opCount, sample.length)), p(opCount) - 1)
-                );
-        final TimingInterval report = new TimingInterval(lastSnap, upToDateAsOf, max, maxStart, max, partitionCount,
-                rowCount, total, opCount, errorCount, SampleOfLongs.merge(rnd, sampleLatencies, Integer.MAX_VALUE));
-        // reset counters
-        opCount = 0;
-        partitionCount = 0;
-        rowCount = 0;
-        total = 0;
-        max = 0;
-        errorCount = 0;
-        lastSnap = upToDateAsOf;
-        return report;
-    }
-
-    // checks to see if a report has been requested, and if so produces the report, signals and clears the request
-    private void maybeReport()
-    {
-        if (reportRequest != null)
-        {
-            synchronized (this)
-            {
-                report = buildReport();
-                reportRequest.countDown();
-                reportRequest = null;
-            }
-        }
-    }
-
-    // checks to see if the timer is dead; if not requests a report, and otherwise fulfills the request itself
-    synchronized void requestReport(CountDownLatch signal)
-    {
-        if (finalReport != null)
-        {
-            report = finalReport;
-            finalReport = new TimingInterval(0);
-            signal.countDown();
-        }
-        else
-            reportRequest = signal;
-    }
-
-    // closes the timer; if a request is outstanding, it furnishes the request, otherwise it populates finalReport
-    public synchronized void close()
-    {
-        if (reportRequest == null)
-            finalReport = buildReport();
-        else
-        {
-            finalReport = new TimingInterval(0);
-            report = buildReport();
-            reportRequest.countDown();
-            reportRequest = null;
-        }
-    }
-}
\ No newline at end of file
diff --git a/tools/stress/src/org/apache/cassandra/stress/util/Timing.java b/tools/stress/src/org/apache/cassandra/stress/util/Timing.java
deleted file mode 100644
index 403bee0..0000000
--- a/tools/stress/src/org/apache/cassandra/stress/util/Timing.java
+++ /dev/null
@@ -1,150 +0,0 @@
-package org.apache.cassandra.stress.util;
-/*
- * 
- * 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.
- * 
- */
-
-
-import java.util.*;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-// relatively simple timing class for getting a uniform sample of latencies, and saving other metrics
-// ensures accuracy of timing by having single threaded timers that are check-pointed by the snapping thread,
-// which waits for them to report back. They report back the data up to the last event prior to the check-point.
-// if the threads are blocked/paused this may mean a period of time longer than the checkpoint elapses, but that all
-// metrics calculated over the interval are accurate
-public class Timing
-{
-    // concurrency: this should be ok as the consumers are created serially by StressAction.run / warmup
-    // Probably the CopyOnWriteArrayList could be changed to an ordinary list as well.
-    private final Map<String, List<Timer>> timers = new TreeMap<>();
-    private volatile TimingIntervals history;
-    private final int historySampleCount;
-    private final int reportSampleCount;
-    private boolean done;
-
-    public Timing(int historySampleCount, int reportSampleCount)
-    {
-        this.historySampleCount = historySampleCount;
-        this.reportSampleCount = reportSampleCount;
-    }
-
-    // TIMING
-
-    public static class TimingResult<E>
-    {
-        public final E extra;
-        public final TimingIntervals intervals;
-        public TimingResult(E extra, TimingIntervals intervals)
-        {
-            this.extra = extra;
-            this.intervals = intervals;
-        }
-    }
-
-    public <E> TimingResult<E> snap(Callable<E> call) throws InterruptedException
-    {
-        // Count up total # of timers
-        int timerCount = 0;
-        for (List<Timer> timersForOperation : timers.values())
-        {
-            timerCount += timersForOperation.size();
-        }
-        final CountDownLatch ready = new CountDownLatch(timerCount);
-
-        // request reports
-        for (List <Timer> timersForOperation : timers.values())
-        {
-            for(Timer timer : timersForOperation)
-            {
-                timer.requestReport(ready);
-            }
-        }
-
-        E extra;
-        try
-        {
-            extra = call.call();
-        }
-        catch (Exception e)
-        {
-            if (e instanceof InterruptedException)
-                throw (InterruptedException) e;
-            throw new RuntimeException(e);
-        }
-
-        // TODO fail gracefully after timeout if a thread is stuck
-        if (!ready.await(5L, TimeUnit.MINUTES))
-        {
-            throw new RuntimeException("Timed out waiting for a timer thread - seems one got stuck. Check GC/Heap size");
-        }
-
-        boolean done = true;
-
-        // reports have been filled in by timer threadCount, so merge
-        Map<String, TimingInterval> intervals = new TreeMap<>();
-        for (Map.Entry<String, List<Timer>> entry : timers.entrySet())
-        {
-            List<TimingInterval> operationIntervals = new ArrayList<>();
-            for (Timer timer : entry.getValue())
-            {
-                operationIntervals.add(timer.report);
-                done &= !timer.running();
-            }
-
-            intervals.put(entry.getKey(), TimingInterval.merge(operationIntervals, reportSampleCount,
-                                                              history.get(entry.getKey()).endNanos()));
-        }
-
-        TimingIntervals result = new TimingIntervals(intervals);
-        this.done = done;
-        history = history.merge(result, historySampleCount, history.startNanos());
-        return new TimingResult<>(extra, result);
-    }
-
-    // build a new timer and add it to the set of running timers.
-    public Timer newTimer(String opType, int sampleCount)
-    {
-        final Timer timer = new Timer(sampleCount);
-
-        if (!timers.containsKey(opType))
-            timers.put(opType, new ArrayList<Timer>());
-
-        timers.get(opType).add(timer);
-        return timer;
-    }
-
-    public void start()
-    {
-        history = new TimingIntervals(timers.keySet());
-    }
-
-    public boolean done()
-    {
-        return done;
-    }
-
-    public TimingIntervals getHistory()
-    {
-        return history;
-    }
-}
diff --git a/tools/stress/src/org/apache/cassandra/stress/util/TimingInterval.java b/tools/stress/src/org/apache/cassandra/stress/util/TimingInterval.java
deleted file mode 100644
index 6be71c8..0000000
--- a/tools/stress/src/org/apache/cassandra/stress/util/TimingInterval.java
+++ /dev/null
@@ -1,202 +0,0 @@
-package org.apache.cassandra.stress.util;
-/*
- * 
- * 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.
- * 
- */
-
-import java.util.*;
-import java.util.concurrent.ThreadLocalRandom;
-
-// represents measurements taken over an interval of time
-// used for both single timer results and merged timer results
-public final class TimingInterval
-{
-    // nanos
-    private final long start;
-    private final long end;
-    public final long maxLatency;
-    public final long pauseLength;
-    public final long pauseStart;
-    public final long totalLatency;
-
-    // discrete
-    public final long partitionCount;
-    public final long rowCount;
-    public final long operationCount;
-    public final long errorCount;
-
-    final SampleOfLongs sample;
-
-    public String toString()
-    {
-        return String.format("Start: %d end: %d maxLatency: %d pauseLength: %d pauseStart: %d totalLatency: %d" +
-                             " pCount: %d rcount: %d opCount: %d errors: %d", start, end, maxLatency, pauseLength,
-                             pauseStart, totalLatency, partitionCount, rowCount, operationCount, errorCount);
-    }
-
-    TimingInterval(long time)
-    {
-        start = end = time;
-        maxLatency = totalLatency = 0;
-        partitionCount = rowCount = operationCount = errorCount = 0;
-        pauseStart = pauseLength = 0;
-        sample = new SampleOfLongs(new long[0], 1d);
-    }
-
-    TimingInterval(long start, long end, long maxLatency, long pauseStart, long pauseLength, long partitionCount,
-                   long rowCount, long totalLatency, long operationCount, long errorCount, SampleOfLongs sample)
-    {
-        this.start = start;
-        this.end = Math.max(end, start);
-        this.maxLatency = maxLatency;
-        this.partitionCount = partitionCount;
-        this.rowCount = rowCount;
-        this.totalLatency = totalLatency;
-        this.errorCount = errorCount;
-        this.operationCount = operationCount;
-        this.pauseStart = pauseStart;
-        this.pauseLength = pauseLength;
-        this.sample = sample;
-    }
-
-    // merge multiple timer intervals together
-    static TimingInterval merge(Iterable<TimingInterval> intervals, int maxSamples, long start)
-    {
-        ThreadLocalRandom rnd = ThreadLocalRandom.current();
-        long operationCount = 0, partitionCount = 0, rowCount = 0, errorCount = 0;
-        long maxLatency = 0, totalLatency = 0;
-        List<SampleOfLongs> latencies = new ArrayList<>();
-        long end = 0;
-        long pauseStart = 0, pauseEnd = Long.MAX_VALUE;
-        for (TimingInterval interval : intervals)
-        {
-            if (interval != null)
-            {
-                end = Math.max(end, interval.end);
-                operationCount += interval.operationCount;
-                maxLatency = Math.max(interval.maxLatency, maxLatency);
-                totalLatency += interval.totalLatency;
-                partitionCount += interval.partitionCount;
-                rowCount += interval.rowCount;
-                errorCount += interval.errorCount;
-                latencies.addAll(Arrays.asList(interval.sample));
-                if (interval.pauseLength > 0)
-                {
-                    pauseStart = Math.max(pauseStart, interval.pauseStart);
-                    pauseEnd = Math.min(pauseEnd, interval.pauseStart + interval.pauseLength);
-                }
-            }
-        }
-
-        if (pauseEnd < pauseStart || pauseStart <= 0)
-        {
-            pauseEnd = pauseStart = 0;
-        }
-
-        return new TimingInterval(start, end, maxLatency, pauseStart, pauseEnd - pauseStart, partitionCount, rowCount,
-                                  totalLatency, operationCount, errorCount, SampleOfLongs.merge(rnd, latencies, maxSamples));
-
-    }
-
-    public double opRate()
-    {
-        return operationCount / ((end - start) * 0.000000001d);
-    }
-
-    public double adjustedRowRate()
-    {
-        return rowCount / ((end - (start + pauseLength)) * 0.000000001d);
-    }
-
-    public double partitionRate()
-    {
-        return partitionCount / ((end - start) * 0.000000001d);
-    }
-
-    public double rowRate()
-    {
-        return rowCount / ((end - start) * 0.000000001d);
-    }
-
-    public double meanLatency()
-    {
-        return (totalLatency / (double) operationCount) * 0.000001d;
-    }
-
-    public double maxLatency()
-    {
-        return maxLatency * 0.000001d;
-    }
-
-    public double medianLatency()
-    {
-        return sample.medianLatency();
-    }
-
-    // 0 < rank < 1
-    public double rankLatency(float rank)
-    {
-        return sample.rankLatency(rank);
-    }
-
-    public long runTime()
-    {
-        return (end - start) / 1000000;
-    }
-
-    public final long endNanos()
-    {
-        return end;
-    }
-
-    public long startNanos()
-    {
-        return start;
-    }
-
-    public static enum TimingParameter
-    {
-        OPRATE, ROWRATE, ADJROWRATE, PARTITIONRATE, MEANLATENCY, MAXLATENCY, MEDIANLATENCY, RANKLATENCY,
-        ERRORCOUNT, PARTITIONCOUNT
-    }
-
-    String getStringValue(TimingParameter value)
-    {
-        return getStringValue(value, Float.NaN);
-    }
-
-    String getStringValue(TimingParameter value, float rank)
-    {
-        switch (value)
-        {
-            case OPRATE:         return String.format("%.0f", opRate());
-            case ROWRATE:        return String.format("%.0f", rowRate());
-            case ADJROWRATE:     return String.format("%.0f", adjustedRowRate());
-            case PARTITIONRATE:  return String.format("%.0f", partitionRate());
-            case MEANLATENCY:    return String.format("%.1f", meanLatency());
-            case MAXLATENCY:     return String.format("%.1f", maxLatency());
-            case MEDIANLATENCY:  return String.format("%.1f", medianLatency());
-            case RANKLATENCY:    return String.format("%.1f", rankLatency(rank));
-            case ERRORCOUNT:     return String.format("%d", errorCount);
-            case PARTITIONCOUNT: return String.format("%d", partitionCount);
-            default:             throw new IllegalStateException();
-        }
-    }
- }
-
diff --git a/tools/stress/src/org/apache/cassandra/stress/util/TimingIntervals.java b/tools/stress/src/org/apache/cassandra/stress/util/TimingIntervals.java
deleted file mode 100644
index b4c69e9..0000000
--- a/tools/stress/src/org/apache/cassandra/stress/util/TimingIntervals.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * 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.stress.util;
-
-import java.util.Arrays;
-import java.util.Map;
-import java.util.TreeMap;
-
-public class TimingIntervals
-{
-    final Map<String, TimingInterval> intervals;
-    TimingIntervals(Iterable<String> opTypes)
-    {
-        long now = System.nanoTime();
-        intervals = new TreeMap<>();
-        for (String opType : opTypes)
-            intervals.put(opType, new TimingInterval(now));
-    }
-
-    TimingIntervals(Map<String, TimingInterval> intervals)
-    {
-        this.intervals = intervals;
-    }
-
-    public TimingIntervals merge(TimingIntervals with, int maxSamples, long start)
-    {
-        assert intervals.size() == with.intervals.size();
-        TreeMap<String, TimingInterval> ret = new TreeMap<>();
-
-        for (String opType : intervals.keySet())
-        {
-            assert with.intervals.containsKey(opType);
-            ret.put(opType, TimingInterval.merge(Arrays.asList(intervals.get(opType), with.intervals.get(opType)), maxSamples, start));
-        }
-
-        return new TimingIntervals(ret);
-    }
-
-    public TimingInterval get(String opType)
-    {
-        return intervals.get(opType);
-    }
-
-    public TimingInterval combine(int maxSamples)
-    {
-        long start = Long.MAX_VALUE;
-        for (TimingInterval ti : intervals.values())
-            start = Math.min(start, ti.startNanos());
-
-        return TimingInterval.merge(intervals.values(), maxSamples, start);
-    }
-
-    public String str(TimingInterval.TimingParameter value)
-    {
-        return str(value, Float.NaN);
-    }
-
-    public String str(TimingInterval.TimingParameter value, float rank)
-    {
-        StringBuilder sb = new StringBuilder("[");
-
-        for (Map.Entry<String, TimingInterval> entry : intervals.entrySet())
-        {
-            sb.append(entry.getKey());
-            sb.append(":");
-            sb.append(entry.getValue().getStringValue(value, rank));
-            sb.append(", ");
-        }
-
-        sb.setLength(sb.length()-2);
-        sb.append("]");
-
-        return sb.toString();
-    }
-
-    public String opRates()
-    {
-        return str(TimingInterval.TimingParameter.OPRATE);
-    }
-    public String partitionRates()
-    {
-        return str(TimingInterval.TimingParameter.PARTITIONRATE);
-    }
-    public String rowRates()
-    {
-        return str(TimingInterval.TimingParameter.ROWRATE);
-    }
-    public String meanLatencies()
-    {
-        return str(TimingInterval.TimingParameter.MEANLATENCY);
-    }
-    public String maxLatencies()
-    {
-        return str(TimingInterval.TimingParameter.MAXLATENCY);
-    }
-    public String medianLatencies()
-    {
-        return str(TimingInterval.TimingParameter.MEDIANLATENCY);
-    }
-    public String rankLatencies(float rank)
-    {
-        return str(TimingInterval.TimingParameter.RANKLATENCY, rank);
-    }
-    public String errorCounts()
-    {
-        return str(TimingInterval.TimingParameter.ERRORCOUNT);
-    }
-    public String partitionCounts()
-    {
-        return str(TimingInterval.TimingParameter.PARTITIONCOUNT);
-    }
-
-    public long opRate()
-    {
-        long v = 0;
-        for (TimingInterval interval : intervals.values())
-            v += interval.opRate();
-        return v;
-    }
-
-    public long startNanos()
-    {
-        long start = Long.MAX_VALUE;
-        for (TimingInterval interval : intervals.values())
-            start = Math.min(start, interval.startNanos());
-        return start;
-    }
-
-    public long endNanos()
-    {
-        long end = Long.MIN_VALUE;
-        for (TimingInterval interval : intervals.values())
-            end = Math.max(end, interval.startNanos());
-        return end;
-    }
-
-    public Map<String, TimingInterval> intervals()
-    {
-        return intervals;
-    }
-}
diff --git a/tools/stress/src/resources/org/apache/cassandra/stress/graph/graph.html b/tools/stress/src/resources/org/apache/cassandra/stress/graph/graph.html
new file mode 100644
index 0000000..e75a06d
--- /dev/null
+++ b/tools/stress/src/resources/org/apache/cassandra/stress/graph/graph.html
@@ -0,0 +1,673 @@
+<!DOCTYPE html>
+<!--
+  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
+  limitatins under the License.
+-->
+<!-- cstar_perf (https://github.com/datastax/cstar_perf) graphing
+utility adapted for use directly with command line cassandra-stress -->
+
+<head>
+  <meta charset="utf-8">
+  <script language="javascript" type="text/javascript">
+    <!--
+
+/* stats start */
+
+/* stats end */
+
+/*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="<div class='a'></div><div class='a i'></div>",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="<select msallowclip=''><option selected=''></option></select>",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=lb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=mb(b);function pb(){}pb.prototype=d.filters=d.pseudos,d.setFilters=new pb,g=fb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fb.error(a):z(a,i).slice(0)};function qb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;
+if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?m.queue(this[0],a):void 0===b?this:this.each(function(){var c=m.queue(this,a,b);m._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&m.dequeue(this,a)})},dequeue:function(a){return this.each(function(){m.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=m.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=m._data(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var S=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=["Top","Right","Bottom","Left"],U=function(a,b){return a=b||a,"none"===m.css(a,"display")||!m.contains(a.ownerDocument,a)},V=m.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===m.type(c)){e=!0;for(h in c)m.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,m.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(m(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav></:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="<input type='radio' checked='checked' name='t'/>",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},fix:function(a){if(a[m.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=Z.test(e)?this.mouseHooks:Y.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new m.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=f.srcElement||y),3===a.target.nodeType&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,g.filter?g.filter(a,f):a},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button,g=b.fromElement;return null==a.pageX&&null!=b.clientX&&(d=a.target.ownerDocument||y,e=d.documentElement,c=d.body,a.pageX=b.clientX+(e&&e.scrollLeft||c&&c.scrollLeft||0)-(e&&e.clientLeft||c&&c.clientLeft||0),a.pageY=b.clientY+(e&&e.scrollTop||c&&c.scrollTop||0)-(e&&e.clientTop||c&&c.clientTop||0)),!a.relatedTarget&&g&&(a.relatedTarget=g===a.target?b.toElement:g),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==cb()&&this.focus)try{return this.focus(),!1}catch(a){}},delegateType:"focusin"},blur:{trigger:function(){return this===cb()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return m.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(a){return m.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=m.extend(new m.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?m.event.trigger(e,null,b):m.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},m.removeEvent=y.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]===K&&(a[d]=null),a.detachEvent(d,c))},m.Event=function(a,b){return this instanceof m.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ab:bb):this.type=a,b&&m.extend(this,b),this.timeStamp=a&&a.timeStamp||m.now(),void(this[m.expando]=!0)):new m.Event(a,b)},m.Event.prototype={isDefaultPrevented:bb,isPropagationStopped:bb,isImmediatePropagationStopped:bb,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ab,a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ab,a&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ab,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},m.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){m.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!m.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.submitBubbles||(m.event.special.submit={setup:function(){return m.nodeName(this,"form")?!1:void m.event.add(this,"click._submit keypress._submit",function(a){var b=a.target,c=m.nodeName(b,"input")||m.nodeName(b,"button")?b.form:void 0;c&&!m._data(c,"submitBubbles")&&(m.event.add(c,"submit._submit",function(a){a._submit_bubble=!0}),m._data(c,"submitBubbles",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&m.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){return m.nodeName(this,"form")?!1:void m.event.remove(this,"._submit")}}),k.changeBubbles||(m.event.special.change={setup:function(){return X.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(m.event.add(this,"propertychange._change",function(a){"checked"===a.originalEvent.propertyName&&(this._just_changed=!0)}),m.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),m.event.simulate("change",this,a,!0)})),!1):void m.event.add(this,"beforeactivate._change",function(a){var b=a.target;X.test(b.nodeName)&&!m._data(b,"changeBubbles")&&(m.event.add(b,"change._change",function(a){!this.parentNode||a.isSimulated||a.isTrigger||m.event.simulate("change",this.parentNode,a,!0)}),m._data(b,"changeBubbles",!0))})},handle:function(a){var b=a.target;return this!==b||a.isSimulated||a.isTrigger||"radio"!==b.type&&"checkbox"!==b.type?a.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return m.event.remove(this,"._change"),!X.test(this.nodeName)}}),k.focusinBubbles||m.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){m.event.simulate(b,a.target,m.event.fix(a),!0)};m.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=m._data(d,b);e||d.addEventListener(a,c,!0),m._data(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=m._data(d,b)-1;e?m._data(d,b,e):(d.removeEventListener(a,c,!0),m._removeData(d,b))}}}),m.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(f in a)this.on(f,b,c,a[f],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=bb;else if(!d)return this;return 1===e&&(g=d,d=function(a){return m().off(a),g.apply(this,arguments)},d.guid=g.guid||(g.guid=m.guid++)),this.each(function(){m.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,m(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=bb),this.each(function(){m.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){m.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?m.event.trigger(a,b,c,!0):void 0}});function db(a){var b=eb.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}var eb="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",fb=/ jQuery\d+="(?:null|\d+)"/g,gb=new RegExp("<(?:"+eb+")[\\s/>]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/<tbody/i,lb=/<|&#?\w+;/,mb=/<(?:script|style|link)/i,nb=/checked\s*(?:[^=]|=\s*.checked.)/i,ob=/^$|\/(?:java|ecma)script/i,pb=/^true\/(.*)/,qb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,rb={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:k.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1></$2>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?"<table>"!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=(Cb[0].contentWindow||Cb[0].contentDocument).document,b.write(),b.close(),c=Eb(a,b),Cb.detach()),Db[a]=c),c}!function(){var a;k.shrinkWrapBlocks=function(){if(null!=a)return a;a=!1;var b,c,d;return c=y.getElementsByTagName("body")[0],c&&c.style?(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",b.appendChild(y.createElement("div")).style.width="5px",a=3!==b.offsetWidth),c.removeChild(d),a):void 0}}();var Gb=/^margin/,Hb=new RegExp("^("+S+")(?!px)[a-z%]+$","i"),Ib,Jb,Kb=/^(top|right|bottom|left)$/;a.getComputedStyle?(Ib=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c.getPropertyValue(b)||c[b]:void 0,c&&(""!==g||m.contains(a.ownerDocument,a)||(g=m.style(a,b)),Hb.test(g)&&Gb.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0===g?g:g+""}):y.documentElement.currentStyle&&(Ib=function(a){return a.currentStyle},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Hb.test(g)&&!Kb.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left="fontSize"===b?"1em":g,g=h.pixelLeft+"px",h.left=d,f&&(e.left=f)),void 0===g?g:g+""||"auto"});function Lb(a,b){return{get:function(){var c=a();if(null!=c)return c?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d,e,f,g,h;if(b=y.createElement("div"),b.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",d=b.getElementsByTagName("a")[0],c=d&&d.style){c.cssText="float:left;opacity:.5",k.opacity="0.5"===c.opacity,k.cssFloat=!!c.cssFloat,b.style.backgroundClip="content-box",b.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===b.style.backgroundClip,k.boxSizing=""===c.boxSizing||""===c.MozBoxSizing||""===c.WebkitBoxSizing,m.extend(k,{reliableHiddenOffsets:function(){return null==g&&i(),g},boxSizingReliable:function(){return null==f&&i(),f},pixelPosition:function(){return null==e&&i(),e},reliableMarginRight:function(){return null==h&&i(),h}});function i(){var b,c,d,i;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),b.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",e=f=!1,h=!0,a.getComputedStyle&&(e="1%"!==(a.getComputedStyle(b,null)||{}).top,f="4px"===(a.getComputedStyle(b,null)||{width:"4px"}).width,i=b.appendChild(y.createElement("div")),i.style.cssText=b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",b.style.width="1px",h=!parseFloat((a.getComputedStyle(i,null)||{}).marginRight)),b.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=b.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",g=0===i[0].offsetHeight,g&&(i[0].style.display="",i[1].style.display="none",g=0===i[0].offsetHeight),c.removeChild(d))}}}(),m.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var Mb=/alpha\([^)]*\)/i,Nb=/opacity\s*=\s*([^)]*)/,Ob=/^(none|table(?!-c[ea]).+)/,Pb=new RegExp("^("+S+")(.*)$","i"),Qb=new RegExp("^([+-])=("+S+")","i"),Rb={position:"absolute",visibility:"hidden",display:"block"},Sb={letterSpacing:"0",fontWeight:"400"},Tb=["Webkit","O","Moz","ms"];function Ub(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=Tb.length;while(e--)if(b=Tb[e]+c,b in a)return b;return d}function Vb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=m._data(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&U(d)&&(f[g]=m._data(d,"olddisplay",Fb(d.nodeName)))):(e=U(d),(c&&"none"!==c||!e)&&m._data(d,"olddisplay",e?c:m.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}function Wb(a,b,c){var d=Pb.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Xb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=m.css(a,c+T[f],!0,e)),d?("content"===c&&(g-=m.css(a,"padding"+T[f],!0,e)),"margin"!==c&&(g-=m.css(a,"border"+T[f]+"Width",!0,e))):(g+=m.css(a,"padding"+T[f],!0,e),"padding"!==c&&(g+=m.css(a,"border"+T[f]+"Width",!0,e)));return g}function Yb(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Ib(a),g=k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Jb(a,b,f),(0>e||null==e)&&(e=a.style[b]),Hb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Xb(a,b,c||(g?"border":"content"),d,f)+"px"}m.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Jb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":k.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=m.camelCase(b),i=a.style;if(b=m.cssProps[h]||(m.cssProps[h]=Ub(i,h)),g=m.cssHooks[b]||m.cssHooks[h],void 0===c)return g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,"string"===f&&(e=Qb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(m.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||m.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),!(g&&"set"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=m.camelCase(b);return b=m.cssProps[h]||(m.cssProps[h]=Ub(a.style,h)),g=m.cssHooks[b]||m.cssHooks[h],g&&"get"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Jb(a,b,d)),"normal"===f&&b in Sb&&(f=Sb[b]),""===c||c?(e=parseFloat(f),c===!0||m.isNumeric(e)?e||0:f):f}}),m.each(["height","width"],function(a,b){m.cssHooks[b]={get:function(a,c,d){return c?Ob.test(m.css(a,"display"))&&0===a.offsetWidth?m.swap(a,Rb,function(){return Yb(a,b,d)}):Yb(a,b,d):void 0},set:function(a,c,d){var e=d&&Ib(a);return Wb(a,c,d?Xb(a,b,d,k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,e),e):0)}}}),k.opacity||(m.cssHooks.opacity={get:function(a,b){return Nb.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=m.isNumeric(b)?"alpha(opacity="+100*b+")":"",f=d&&d.filter||c.filter||"";c.zoom=1,(b>=1||""===b)&&""===m.trim(f.replace(Mb,""))&&c.removeAttribute&&(c.removeAttribute("filter"),""===b||d&&!d.filter)||(c.filter=Mb.test(f)?f.replace(Mb,e):f+" "+e)}}),m.cssHooks.marginRight=Lb(k.reliableMarginRight,function(a,b){return b?m.swap(a,{display:"inline-block"},Jb,[a,"marginRight"]):void 0}),m.each({margin:"",padding:"",border:"Width"},function(a,b){m.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+T[d]+b]=f[d]||f[d-2]||f[0];return e}},Gb.test(a)||(m.cssHooks[a+b].set=Wb)}),m.fn.extend({css:function(a,b){return V(this,function(a,b,c){var d,e,f={},g=0;if(m.isArray(b)){for(d=Ib(a),e=b.length;e>g;g++)f[b[g]]=m.css(a,b[g],!1,d);return f}return void 0!==c?m.style(a,b,c):m.css(a,b)},a,b,arguments.length>1)},show:function(){return Vb(this,!0)},hide:function(){return Vb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){U(this)?m(this).show():m(this).hide()})}});function Zb(a,b,c,d,e){return new Zb.prototype.init(a,b,c,d,e)}m.Tween=Zb,Zb.prototype={constructor:Zb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(m.cssNumber[c]?"":"px")
+},cur:function(){var a=Zb.propHooks[this.prop];return a&&a.get?a.get(this):Zb.propHooks._default.get(this)},run:function(a){var b,c=Zb.propHooks[this.prop];return this.pos=b=this.options.duration?m.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Zb.propHooks._default.set(this),this}},Zb.prototype.init.prototype=Zb.prototype,Zb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=m.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){m.fx.step[a.prop]?m.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[m.cssProps[a.prop]]||m.cssHooks[a.prop])?m.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Zb.propHooks.scrollTop=Zb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},m.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},m.fx=Zb.prototype.init,m.fx.step={};var $b,_b,ac=/^(?:toggle|show|hide)$/,bc=new RegExp("^(?:([+-])=|)("+S+")([a-z%]*)$","i"),cc=/queueHooks$/,dc=[ic],ec={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=bc.exec(b),f=e&&e[3]||(m.cssNumber[a]?"":"px"),g=(m.cssNumber[a]||"px"!==f&&+d)&&bc.exec(m.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,m.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function fc(){return setTimeout(function(){$b=void 0}),$b=m.now()}function gc(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=T[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function hc(a,b,c){for(var d,e=(ec[b]||[]).concat(ec["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ic(a,b,c){var d,e,f,g,h,i,j,l,n=this,o={},p=a.style,q=a.nodeType&&U(a),r=m._data(a,"fxshow");c.queue||(h=m._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,n.always(function(){n.always(function(){h.unqueued--,m.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=m.css(a,"display"),l="none"===j?m._data(a,"olddisplay")||Fb(a.nodeName):j,"inline"===l&&"none"===m.css(a,"float")&&(k.inlineBlockNeedsLayout&&"inline"!==Fb(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",k.shrinkWrapBlocks()||n.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],ac.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||m.style(a,d)}else j=void 0;if(m.isEmptyObject(o))"inline"===("none"===j?Fb(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=m._data(a,"fxshow",{}),f&&(r.hidden=!q),q?m(a).show():n.done(function(){m(a).hide()}),n.done(function(){var b;m._removeData(a,"fxshow");for(b in o)m.style(a,b,o[b])});for(d in o)g=hc(q?r[d]:0,d,n),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function jc(a,b){var c,d,e,f,g;for(c in a)if(d=m.camelCase(c),e=b[d],f=a[c],m.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=m.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kc(a,b,c){var d,e,f=0,g=dc.length,h=m.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=$b||fc(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:m.extend({},b),opts:m.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:$b||fc(),duration:c.duration,tweens:[],createTween:function(b,c){var d=m.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jc(k,j.opts.specialEasing);g>f;f++)if(d=dc[f].call(j,a,k,j.opts))return d;return m.map(k,hc,j),m.isFunction(j.opts.start)&&j.opts.start.call(a,j),m.fx.timer(m.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}m.Animation=m.extend(kc,{tweener:function(a,b){m.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],ec[c]=ec[c]||[],ec[c].unshift(b)},prefilter:function(a,b){b?dc.unshift(a):dc.push(a)}}),m.speed=function(a,b,c){var d=a&&"object"==typeof a?m.extend({},a):{complete:c||!c&&b||m.isFunction(a)&&a,duration:a,easing:c&&b||b&&!m.isFunction(b)&&b};return d.duration=m.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in m.fx.speeds?m.fx.speeds[d.duration]:m.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){m.isFunction(d.old)&&d.old.call(this),d.queue&&m.dequeue(this,d.queue)},d},m.fn.extend({fadeTo:function(a,b,c,d){return this.filter(U).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=m.isEmptyObject(a),f=m.speed(b,c,d),g=function(){var b=kc(this,m.extend({},a),f);(e||m._data(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=m.timers,g=m._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&cc.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&m.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=m._data(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=m.timers,g=d?d.length:0;for(c.finish=!0,m.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),m.each(["toggle","show","hide"],function(a,b){var c=m.fn[b];m.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gc(b,!0),a,d,e)}}),m.each({slideDown:gc("show"),slideUp:gc("hide"),slideToggle:gc("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){m.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),m.timers=[],m.fx.tick=function(){var a,b=m.timers,c=0;for($b=m.now();c<b.length;c++)a=b[c],a()||b[c]!==a||b.splice(c--,1);b.length||m.fx.stop(),$b=void 0},m.fx.timer=function(a){m.timers.push(a),a()?m.fx.start():m.timers.pop()},m.fx.interval=13,m.fx.start=function(){_b||(_b=setInterval(m.fx.tick,m.fx.interval))},m.fx.stop=function(){clearInterval(_b),_b=null},m.fx.speeds={slow:600,fast:200,_default:400},m.fn.delay=function(a,b){return a=m.fx?m.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a,b,c,d,e;b=y.createElement("div"),b.setAttribute("className","t"),b.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",d=b.getElementsByTagName("a")[0],c=y.createElement("select"),e=c.appendChild(y.createElement("option")),a=b.getElementsByTagName("input")[0],d.style.cssText="top:1px",k.getSetAttribute="t"!==b.className,k.style=/top/.test(d.getAttribute("style")),k.hrefNormalized="/a"===d.getAttribute("href"),k.checkOn=!!a.value,k.optSelected=e.selected,k.enctype=!!y.createElement("form").enctype,c.disabled=!0,k.optDisabled=!e.disabled,a=y.createElement("input"),a.setAttribute("value",""),k.input=""===a.getAttribute("value"),a.value="t",a.setAttribute("type","radio"),k.radioValue="t"===a.value}();var lc=/\r/g;m.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=m.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,m(this).val()):a,null==e?e="":"number"==typeof e?e+="":m.isArray(e)&&(e=m.map(e,function(a){return null==a?"":a+""})),b=m.valHooks[this.type]||m.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=m.valHooks[e.type]||m.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(lc,""):null==c?"":c)}}}),m.extend({valHooks:{option:{get:function(a){var b=m.find.attr(a,"value");return null!=b?b:m.trim(m.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&m.nodeName(c.parentNode,"optgroup"))){if(b=m(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=m.makeArray(b),g=e.length;while(g--)if(d=e[g],m.inArray(m.valHooks.option.get(d),f)>=0)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),m.each(["radio","checkbox"],function(){m.valHooks[this]={set:function(a,b){return m.isArray(b)?a.checked=m.inArray(m(a).val(),b)>=0:void 0}},k.checkOn||(m.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var mc,nc,oc=m.expr.attrHandle,pc=/^(?:checked|selected)$/i,qc=k.getSetAttribute,rc=k.input;m.fn.extend({attr:function(a,b){return V(this,m.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){m.removeAttr(this,a)})}}),m.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===K?m.prop(a,b,c):(1===f&&m.isXMLDoc(a)||(b=b.toLowerCase(),d=m.attrHooks[b]||(m.expr.match.bool.test(b)?nc:mc)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=m.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void m.removeAttr(a,b))},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=m.propFix[c]||c,m.expr.match.bool.test(c)?rc&&qc||!pc.test(c)?a[d]=!1:a[m.camelCase("default-"+c)]=a[d]=!1:m.attr(a,c,""),a.removeAttribute(qc?c:d)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&m.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),nc={set:function(a,b,c){return b===!1?m.removeAttr(a,c):rc&&qc||!pc.test(c)?a.setAttribute(!qc&&m.propFix[c]||c,c):a[m.camelCase("default-"+c)]=a[c]=!0,c}},m.each(m.expr.match.bool.source.match(/\w+/g),function(a,b){var c=oc[b]||m.find.attr;oc[b]=rc&&qc||!pc.test(b)?function(a,b,d){var e,f;return d||(f=oc[b],oc[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,oc[b]=f),e}:function(a,b,c){return c?void 0:a[m.camelCase("default-"+b)]?b.toLowerCase():null}}),rc&&qc||(m.attrHooks.value={set:function(a,b,c){return m.nodeName(a,"input")?void(a.defaultValue=b):mc&&mc.set(a,b,c)}}),qc||(mc={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+="","value"===c||b===a.getAttribute(c)?b:void 0}},oc.id=oc.name=oc.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&""!==d.value?d.value:null},m.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:mc.set},m.attrHooks.contenteditable={set:function(a,b,c){mc.set(a,""===b?!1:b,c)}},m.each(["width","height"],function(a,b){m.attrHooks[b]={set:function(a,c){return""===c?(a.setAttribute(b,"auto"),c):void 0}}})),k.style||(m.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+""}});var sc=/^(?:input|select|textarea|button|object)$/i,tc=/^(?:a|area)$/i;m.fn.extend({prop:function(a,b){return V(this,m.prop,a,b,arguments.length>1)},removeProp:function(a){return a=m.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),m.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!m.isXMLDoc(a),f&&(b=m.propFix[b]||b,e=m.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=m.find.attr(a,"tabindex");return b?parseInt(b,10):sc.test(a.nodeName)||tc.test(a.nodeName)&&a.href?0:-1}}}}),k.hrefNormalized||m.each(["href","src"],function(a,b){m.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),k.optSelected||(m.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}}),m.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){m.propFix[this.toLowerCase()]=this}),k.enctype||(m.propFix.enctype="encoding");var uc=/[\t\r\n\f]/g;m.fn.extend({addClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j="string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).addClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=m.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j=0===arguments.length||"string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).removeClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?m.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(m.isFunction(a)?function(c){m(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=m(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===K||"boolean"===c)&&(this.className&&m._data(this,"__className__",this.className),this.className=this.className||a===!1?"":m._data(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(uc," ").indexOf(b)>=0)return!0;return!1}}),m.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){m.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),m.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var vc=m.now(),wc=/\?/,xc=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;m.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+"");var c,d=null,e=m.trim(b+"");return e&&!m.trim(e.replace(xc,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,"")}))?Function("return "+e)():m.error("Invalid JSON: "+b)},m.parseXML=function(b){var c,d;if(!b||"string"!=typeof b)return null;try{a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName("parsererror").length||m.error("Invalid XML: "+b),c};var yc,zc,Ac=/#.*$/,Bc=/([?&])_=[^&]*/,Cc=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Dc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Ec=/^(?:GET|HEAD)$/,Fc=/^\/\//,Gc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Hc={},Ic={},Jc="*/".concat("*");try{zc=location.href}catch(Kc){zc=y.createElement("a"),zc.href="",zc=zc.href}yc=Gc.exec(zc.toLowerCase())||[];function Lc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(m.isFunction(c))while(d=f[e++])"+"===d.charAt(0)?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Mc(a,b,c,d){var e={},f=a===Ic;function g(h){var i;return e[h]=!0,m.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Nc(a,b){var c,d,e=m.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&m.extend(!0,a,c),a}function Oc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader("Content-Type"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+" "+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Pc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}m.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:zc,type:"GET",isLocal:Dc.test(yc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Jc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":m.parseJSON,"text xml":m.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Nc(Nc(a,m.ajaxSettings),b):Nc(m.ajaxSettings,a)},ajaxPrefilter:Lc(Hc),ajaxTransport:Lc(Ic),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=m.ajaxSetup({},b),l=k.context||k,n=k.context&&(l.nodeType||l.jquery)?m(l):m.event,o=m.Deferred(),p=m.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!j){j={};while(b=Cc.exec(f))j[b[1].toLowerCase()]=b[2]}b=j[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?f:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return i&&i.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||zc)+"").replace(Ac,"").replace(Fc,yc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=m.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(c=Gc.exec(k.url.toLowerCase()),k.crossDomain=!(!c||c[1]===yc[1]&&c[2]===yc[2]&&(c[3]||("http:"===c[1]?"80":"443"))===(yc[3]||("http:"===yc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=m.param(k.data,k.traditional)),Mc(Hc,k,b,v),2===t)return v;h=k.global,h&&0===m.active++&&m.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!Ec.test(k.type),e=k.url,k.hasContent||(k.data&&(e=k.url+=(wc.test(e)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=Bc.test(e)?e.replace(Bc,"$1_="+vc++):e+(wc.test(e)?"&":"?")+"_="+vc++)),k.ifModified&&(m.lastModified[e]&&v.setRequestHeader("If-Modified-Since",m.lastModified[e]),m.etag[e]&&v.setRequestHeader("If-None-Match",m.etag[e])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+Jc+"; q=0.01":""):k.accepts["*"]);for(d in k.headers)v.setRequestHeader(d,k.headers[d]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(d in{success:1,error:1,complete:1})v[d](k[d]);if(i=Mc(Ic,k,b,v)){v.readyState=1,h&&n.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,i.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,c,d){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),i=void 0,f=d||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,c&&(u=Oc(k,v,c)),u=Pc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(m.lastModified[e]=w),w=v.getResponseHeader("etag"),w&&(m.etag[e]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,h&&n.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),h&&(n.trigger("ajaxComplete",[v,k]),--m.active||m.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return m.get(a,b,c,"json")},getScript:function(a,b){return m.get(a,void 0,b,"script")}}),m.each(["get","post"],function(a,b){m[b]=function(a,c,d,e){return m.isFunction(c)&&(e=e||d,d=c,c=void 0),m.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),m.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){m.fn[b]=function(a){return this.on(b,a)}}),m._evalUrl=function(a){return m.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},m.fn.extend({wrapAll:function(a){if(m.isFunction(a))return this.each(function(b){m(this).wrapAll(a.call(this,b))});if(this[0]){var b=m(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return this.each(m.isFunction(a)?function(b){m(this).wrapInner(a.call(this,b))}:function(){var b=m(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=m.isFunction(a);return this.each(function(c){m(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){m.nodeName(this,"body")||m(this).replaceWith(this.childNodes)}).end()}}),m.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0||!k.reliableHiddenOffsets()&&"none"===(a.style&&a.style.display||m.css(a,"display"))},m.expr.filters.visible=function(a){return!m.expr.filters.hidden(a)};var Qc=/%20/g,Rc=/\[\]$/,Sc=/\r?\n/g,Tc=/^(?:submit|button|image|reset|file)$/i,Uc=/^(?:input|select|textarea|keygen)/i;function Vc(a,b,c,d){var e;if(m.isArray(b))m.each(b,function(b,e){c||Rc.test(a)?d(a,e):Vc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==m.type(b))d(a,b);else for(e in b)Vc(a+"["+e+"]",b[e],c,d)}m.param=function(a,b){var c,d=[],e=function(a,b){b=m.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=m.ajaxSettings&&m.ajaxSettings.traditional),m.isArray(a)||a.jquery&&!m.isPlainObject(a))m.each(a,function(){e(this.name,this.value)});else for(c in a)Vc(c,a[c],b,e);return d.join("&").replace(Qc,"+")},m.fn.extend({serialize:function(){return m.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=m.prop(this,"elements");return a?m.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!m(this).is(":disabled")&&Uc.test(this.nodeName)&&!Tc.test(a)&&(this.checked||!W.test(a))}).map(function(a,b){var c=m(this).val();return null==c?null:m.isArray(c)?m.map(c,function(a){return{name:b.name,value:a.replace(Sc,"\r\n")}}):{name:b.name,value:c.replace(Sc,"\r\n")}}).get()}}),m.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&Zc()||$c()}:Zc;var Wc=0,Xc={},Yc=m.ajaxSettings.xhr();a.ActiveXObject&&m(a).on("unload",function(){for(var a in Xc)Xc[a](void 0,!0)}),k.cors=!!Yc&&"withCredentials"in Yc,Yc=k.ajax=!!Yc,Yc&&m.ajaxTransport(function(a){if(!a.crossDomain||k.cors){var b;return{send:function(c,d){var e,f=a.xhr(),g=++Wc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)void 0!==c[e]&&f.setRequestHeader(e,c[e]+"");f.send(a.hasContent&&a.data||null),b=function(c,e){var h,i,j;if(b&&(e||4===f.readyState))if(delete Xc[g],b=void 0,f.onreadystatechange=m.noop,e)4!==f.readyState&&f.abort();else{j={},h=f.status,"string"==typeof f.responseText&&(j.text=f.responseText);try{i=f.statusText}catch(k){i=""}h||!a.isLocal||a.crossDomain?1223===h&&(h=204):h=j.text?200:404}j&&d(h,i,j,f.getAllResponseHeaders())},a.async?4===f.readyState?setTimeout(b):f.onreadystatechange=Xc[g]=b:b()},abort:function(){b&&b(void 0,!0)}}}});function Zc(){try{return new a.XMLHttpRequest}catch(b){}}function $c(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}m.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return m.globalEval(a),a}}}),m.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),m.ajaxTransport("script",function(a){if(a.crossDomain){var b,c=y.head||m("head")[0]||y.documentElement;return{send:function(d,e){b=y.createElement("script"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||e(200,"success"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var _c=[],ad=/(=)\?(?=&|$)|\?\?/;m.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=_c.pop()||m.expando+"_"+vc++;return this[a]=!0,a}}),m.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(ad.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&ad.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=m.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(ad,"$1"+e):b.jsonp!==!1&&(b.url+=(wc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||m.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,_c.push(e)),g&&m.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),m.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||y;var d=u.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=m.buildFragment([a],b,e),e&&e.length&&m(e).remove(),m.merge([],d.childNodes))};var bd=m.fn.load;m.fn.load=function(a,b,c){if("string"!=typeof a&&bd)return bd.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=m.trim(a.slice(h,a.length)),a=a.slice(0,h)),m.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(f="POST"),g.length>0&&m.ajax({url:a,type:f,dataType:"html",data:b}).done(function(a){e=arguments,g.html(d?m("<div>").append(m.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},m.expr.filters.animated=function(a){return m.grep(m.timers,function(b){return a===b.elem}).length};var cd=a.document.documentElement;function dd(a){return m.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}m.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=m.css(a,"position"),l=m(a),n={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=m.css(a,"top"),i=m.css(a,"left"),j=("absolute"===k||"fixed"===k)&&m.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),m.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(n.top=b.top-h.top+g),null!=b.left&&(n.left=b.left-h.left+e),"using"in b?b.using.call(a,n):l.css(n)}},m.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){m.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,m.contains(b,e)?(typeof e.getBoundingClientRect!==K&&(d=e.getBoundingClientRect()),c=dd(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===m.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),m.nodeName(a[0],"html")||(c=a.offset()),c.top+=m.css(a[0],"borderTopWidth",!0),c.left+=m.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-m.css(d,"marginTop",!0),left:b.left-c.left-m.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||cd;while(a&&!m.nodeName(a,"html")&&"static"===m.css(a,"position"))a=a.offsetParent;return a||cd})}}),m.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);m.fn[a]=function(d){return V(this,function(a,d,e){var f=dd(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?m(f).scrollLeft():e,c?e:m(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),m.each(["top","left"],function(a,b){m.cssHooks[b]=Lb(k.pixelPosition,function(a,c){return c?(c=Jb(a,b),Hb.test(c)?m(a).position()[b]+"px":c):void 0})}),m.each({Height:"height",Width:"width"},function(a,b){m.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){m.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return V(this,function(b,c,d){var e;return m.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?m.css(b,c,g):m.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),m.fn.size=function(){return this.length},m.fn.andSelf=m.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return m});var ed=a.jQuery,fd=a.$;return m.noConflict=function(b){return a.$===m&&(a.$=fd),b&&a.jQuery===m&&(a.jQuery=ed),m},typeof b===K&&(a.jQuery=a.$=m),m});
+
+
+
+/** d3js **/
+!function(){function n(n,t){return t>n?-1:n>t?1:n>=t?0:0/0}function t(n){return null!=n&&!isNaN(n)}function e(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)<0?r=i+1:u=i}return r},right:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)>0?u=i:r=i+1}return r}}}function r(n){return n.length}function u(n){for(var t=1;n*t%1;)t*=10;return t}function i(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function o(){}function a(n){return ia+n in this}function c(n){return n=ia+n,n in this&&delete this[n]}function s(){var n=[];return this.forEach(function(t){n.push(t)}),n}function l(){var n=0;for(var t in this)t.charCodeAt(0)===oa&&++n;return n}function f(){for(var n in this)if(n.charCodeAt(0)===oa)return!1;return!0}function h(){}function g(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function p(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.substring(1);for(var e=0,r=aa.length;r>e;++e){var u=aa[e]+t;if(u in n)return u}}function v(){}function d(){}function m(n){function t(){for(var t,r=e,u=-1,i=r.length;++u<i;)(t=r[u].on)&&t.apply(this,arguments);return n}var e=[],r=new o;return t.on=function(t,u){var i,o=r.get(t);return arguments.length<2?o&&o.on:(o&&(o.on=null,e=e.slice(0,i=e.indexOf(o)).concat(e.slice(i+1)),r.remove(t)),u&&e.push(r.set(t,{on:u})),n)},t}function y(){Zo.event.preventDefault()}function x(){for(var n,t=Zo.event;n=t.sourceEvent;)t=n;return t}function M(n){for(var t=new d,e=0,r=arguments.length;++e<r;)t[arguments[e]]=m(t);return t.of=function(e,r){return function(u){try{var i=u.sourceEvent=Zo.event;u.target=n,Zo.event=u,t[u.type].apply(e,r)}finally{Zo.event=i}}},t}function _(n){return sa(n,pa),n}function b(n){return"function"==typeof n?n:function(){return la(n,this)}}function w(n){return"function"==typeof n?n:function(){return fa(n,this)}}function S(n,t){function e(){this.removeAttribute(n)}function r(){this.removeAttributeNS(n.space,n.local)}function u(){this.setAttribute(n,t)}function i(){this.setAttributeNS(n.space,n.local,t)}function o(){var e=t.apply(this,arguments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function a(){var e=t.apply(this,arguments);null==e?this.removeAttributeNS(n.space,n.local):this.setAttributeNS(n.space,n.local,e)}return n=Zo.ns.qualify(n),null==t?n.local?r:e:"function"==typeof t?n.local?a:o:n.local?i:u}function k(n){return n.trim().replace(/\s+/g," ")}function E(n){return new RegExp("(?:^|\\s+)"+Zo.requote(n)+"(?:\\s+|$)","g")}function A(n){return(n+"").trim().split(/^|\s+/)}function C(n,t){function e(){for(var e=-1;++e<u;)n[e](this,t)}function r(){for(var e=-1,r=t.apply(this,arguments);++e<u;)n[e](this,r)}n=A(n).map(N);var u=n.length;return"function"==typeof t?r:e}function N(n){var t=E(n);return function(e,r){if(u=e.classList)return r?u.add(n):u.remove(n);var u=e.getAttribute("class")||"";r?(t.lastIndex=0,t.test(u)||e.setAttribute("class",k(u+" "+n))):e.setAttribute("class",k(u.replace(t," ")))}}function z(n,t,e){function r(){this.style.removeProperty(n)}function u(){this.style.setProperty(n,t,e)}function i(){var r=t.apply(this,arguments);null==r?this.style.removeProperty(n):this.style.setProperty(n,r,e)}return null==t?r:"function"==typeof t?i:u}function L(n,t){function e(){delete this[n]}function r(){this[n]=t}function u(){var e=t.apply(this,arguments);null==e?delete this[n]:this[n]=e}return null==t?e:"function"==typeof t?u:r}function T(n){return"function"==typeof n?n:(n=Zo.ns.qualify(n)).local?function(){return this.ownerDocument.createElementNS(n.space,n.local)}:function(){return this.ownerDocument.createElementNS(this.namespaceURI,n)}}function q(n){return{__data__:n}}function R(n){return function(){return ga(this,n)}}function D(t){return arguments.length||(t=n),function(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}}function P(n,t){for(var e=0,r=n.length;r>e;e++)for(var u,i=n[e],o=0,a=i.length;a>o;o++)(u=i[o])&&t(u,o,e);return n}function U(n){return sa(n,da),n}function j(n){var t,e;return function(r,u,i){var o,a=n[i].update,c=a.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(o=a[t])&&++t<c;);return o}}function H(){var n=this.__transition__;n&&++n.active}function F(n,t,e){function r(){var t=this[o];t&&(this.removeEventListener(n,t,t.$),delete this[o])}function u(){var u=c(t,Xo(arguments));r.call(this),this.addEventListener(n,this[o]=u,u.$=e),u._=t}function i(){var t,e=new RegExp("^__on([^.]+)"+Zo.requote(n)+"$");for(var r in this)if(t=r.match(e)){var u=this[r];this.removeEventListener(t[1],u,u.$),delete this[r]}}var o="__on"+n,a=n.indexOf("."),c=O;a>0&&(n=n.substring(0,a));var s=ya.get(n);return s&&(n=s,c=Y),a?t?u:r:t?v:i}function O(n,t){return function(e){var r=Zo.event;Zo.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{Zo.event=r}}}function Y(n,t){var e=O(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function I(){var n=".dragsuppress-"+ ++Ma,t="click"+n,e=Zo.select(Wo).on("touchmove"+n,y).on("dragstart"+n,y).on("selectstart"+n,y);if(xa){var r=Bo.style,u=r[xa];r[xa]="none"}return function(i){function o(){e.on(t,null)}e.on(n,null),xa&&(r[xa]=u),i&&(e.on(t,function(){y(),o()},!0),setTimeout(o,0))}}function Z(n,t){t.changedTouches&&(t=t.changedTouches[0]);var e=n.ownerSVGElement||n;if(e.createSVGPoint){var r=e.createSVGPoint();if(0>_a&&(Wo.scrollX||Wo.scrollY)){e=Zo.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var u=e[0][0].getScreenCTM();_a=!(u.f||u.e),e.remove()}return _a?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(n.getScreenCTM().inverse()),[r.x,r.y]}var i=n.getBoundingClientRect();return[t.clientX-i.left-n.clientLeft,t.clientY-i.top-n.clientTop]}function V(){return Zo.event.changedTouches[0].identifier}function X(){return Zo.event.target}function $(){return Wo}function B(n){return n>0?1:0>n?-1:0}function W(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function J(n){return n>1?0:-1>n?ba:Math.acos(n)}function G(n){return n>1?Sa:-1>n?-Sa:Math.asin(n)}function K(n){return((n=Math.exp(n))-1/n)/2}function Q(n){return((n=Math.exp(n))+1/n)/2}function nt(n){return((n=Math.exp(2*n))-1)/(n+1)}function tt(n){return(n=Math.sin(n/2))*n}function et(){}function rt(n,t,e){return this instanceof rt?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof rt?new rt(n.h,n.s,n.l):mt(""+n,yt,rt):new rt(n,t,e)}function ut(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(o-i)*n/60:180>n?o:240>n?i+(o-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,i=2*e-o,new gt(u(n+120),u(n),u(n-120))}function it(n,t,e){return this instanceof it?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof it?new it(n.h,n.c,n.l):n instanceof at?st(n.l,n.a,n.b):st((n=xt((n=Zo.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new it(n,t,e)}function ot(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new at(e,Math.cos(n*=Aa)*t,Math.sin(n)*t)}function at(n,t,e){return this instanceof at?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof at?new at(n.l,n.a,n.b):n instanceof it?ot(n.l,n.c,n.h):xt((n=gt(n)).r,n.g,n.b):new at(n,t,e)}function ct(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=lt(u)*ja,r=lt(r)*Ha,i=lt(i)*Fa,new gt(ht(3.2404542*u-1.5371385*r-.4985314*i),ht(-.969266*u+1.8760108*r+.041556*i),ht(.0556434*u-.2040259*r+1.0572252*i))}function st(n,t,e){return n>0?new it(Math.atan2(e,t)*Ca,Math.sqrt(t*t+e*e),n):new it(0/0,0/0,n)}function lt(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function ft(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function ht(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function gt(n,t,e){return this instanceof gt?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof gt?new gt(n.r,n.g,n.b):mt(""+n,gt,ut):new gt(n,t,e)}function pt(n){return new gt(n>>16,255&n>>8,255&n)}function vt(n){return pt(n)+""}function dt(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function mt(n,t,e){var r,u,i,o=0,a=0,c=0;if(r=/([a-z]+)\((.*)\)/i.exec(n))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(_t(u[0]),_t(u[1]),_t(u[2]))}return(i=Ia.get(n))?t(i.r,i.g,i.b):(null==n||"#"!==n.charAt(0)||isNaN(i=parseInt(n.substring(1),16))||(4===n.length?(o=(3840&i)>>4,o=o>>4|o,a=240&i,a=a>>4|a,c=15&i,c=c<<4|c):7===n.length&&(o=(16711680&i)>>16,a=(65280&i)>>8,c=255&i)),t(o,a,c))}function yt(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-i,c=(o+i)/2;return a?(u=.5>c?a/(o+i):a/(2-o-i),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=0/0,u=c>0&&1>c?0:r),new rt(r,u,c)}function xt(n,t,e){n=Mt(n),t=Mt(t),e=Mt(e);var r=ft((.4124564*n+.3575761*t+.1804375*e)/ja),u=ft((.2126729*n+.7151522*t+.072175*e)/Ha),i=ft((.0193339*n+.119192*t+.9503041*e)/Fa);return at(116*u-16,500*(r-u),200*(u-i))}function Mt(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function _t(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function bt(n){return"function"==typeof n?n:function(){return n}}function wt(n){return n}function St(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),kt(t,e,n,r)}}function kt(n,t,e,r){function u(){var n,t=c.status;if(!t&&c.responseText||t>=200&&300>t||304===t){try{n=e.call(i,c)}catch(r){return o.error.call(i,r),void 0}o.load.call(i,n)}else o.error.call(i,c)}var i={},o=Zo.dispatch("beforesend","progress","load","error"),a={},c=new XMLHttpRequest,s=null;return!Wo.XDomainRequest||"withCredentials"in c||!/^(http(s)?:)?\/\//.test(n)||(c=new XDomainRequest),"onload"in c?c.onload=c.onerror=u:c.onreadystatechange=function(){c.readyState>3&&u()},c.onprogress=function(n){var t=Zo.event;Zo.event=n;try{o.progress.call(i,c)}finally{Zo.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(s=n,i):s},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(Xo(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),c.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),c.setRequestHeader)for(var l in a)c.setRequestHeader(l,a[l]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=s&&(c.responseType=s),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),o.beforesend.call(i,c),c.send(null==r?null:r),i},i.abort=function(){return c.abort(),i},Zo.rebind(i,o,"on"),null==r?i:i.get(Et(r))}function Et(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function At(){var n=Ct(),t=Nt()-n;t>24?(isFinite(t)&&(clearTimeout($a),$a=setTimeout(At,t)),Xa=0):(Xa=1,Wa(At))}function Ct(){var n=Date.now();for(Ba=Za;Ba;)n>=Ba.t&&(Ba.f=Ba.c(n-Ba.t)),Ba=Ba.n;return n}function Nt(){for(var n,t=Za,e=1/0;t;)t.f?t=n?n.n=t.n:Za=t.n:(t.t<e&&(e=t.t),t=(n=t).n);return Va=n,e}function zt(n,t){return t-(n?Math.ceil(Math.log(n)/Math.LN10):1)}function Lt(n,t){var e=Math.pow(10,3*ua(8-t));return{scale:t>8?function(n){return n/e}:function(n){return n*e},symbol:n}}function Tt(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r?function(n){for(var t=n.length,u=[],i=0,o=r[0];t>0&&o>0;)u.push(n.substring(t-=o,t+o)),o=r[i=(i+1)%r.length];return u.reverse().join(e)}:wt;return function(n){var e=Ga.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"",c=e[4]||"",s=e[5],l=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1;switch(h&&(h=+h.substring(1)),(s||"0"===r&&"="===o)&&(s=r="0",o="=",f&&(l-=Math.floor((l-1)/4))),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===c&&(v="0"+g.toLowerCase());case"c":case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===c&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=Ka.get(g)||qt;var y=s&&f;return function(n){var e=d;if(m&&n%1)return"";var u=0>n||0===n&&0>1/n?(n=-n,"-"):a;if(0>p){var c=Zo.formatPrefix(n,h);n=c.scale(n),e=c.symbol+d}else n*=p;n=g(n,h);var x=n.lastIndexOf("."),M=0>x?n:n.substring(0,x),_=0>x?"":t+n.substring(x+1);!s&&f&&(M=i(M));var b=v.length+M.length+_.length+(y?0:u.length),w=l>b?new Array(b=l-b+1).join(r):"";return y&&(M=i(w+M)),u+=v,n=M+_,("<"===o?u+n+w:">"===o?w+u+n:"^"===o?w.substring(0,b>>=1)+u+n+w.substring(b):u+(y?n:w+n))+e}}}function qt(n){return n+""}function Rt(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function Dt(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new nc(e-1)),1),e}function i(n,e){return t(n=new nc(+n),e),n}function o(n,r,i){var o=u(n),a=[];if(i>1)for(;r>o;)e(o)%i||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{nc=Rt;var r=new Rt;return r._=n,o(r,t,e)}finally{nc=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=o;var c=n.utc=Pt(n);return c.floor=c,c.round=Pt(r),c.ceil=Pt(u),c.offset=Pt(i),c.range=a,n}function Pt(n){return function(t,e){try{nc=Rt;var r=new Rt;return r._=t,n(r,e)._}finally{nc=Date}}}function Ut(n){function t(n){function t(t){for(var e,u,i,o=[],a=-1,c=0;++a<r;)37===n.charCodeAt(a)&&(o.push(n.substring(c,a)),null!=(u=ec[e=n.charAt(++a)])&&(e=n.charAt(++a)),(i=C[e])&&(e=i(t,null==u?"e"===e?" ":"0":u)),o.push(e),c=a+1);return o.push(n.substring(c,a)),o.join("")}var r=n.length;return t.parse=function(t){var r={y:1900,m:0,d:1,H:0,M:0,S:0,L:0,Z:null},u=e(r,n,t,0);if(u!=t.length)return null;"p"in r&&(r.H=r.H%12+12*r.p);var i=null!=r.Z&&nc!==Rt,o=new(i?Rt:nc);return"j"in r?o.setFullYear(r.y,0,r.j):"w"in r&&("W"in r||"U"in r)?(o.setFullYear(r.y,0,1),o.setFullYear(r.y,0,"W"in r?(r.w+6)%7+7*r.W-(o.getDay()+5)%7:r.w+7*r.U-(o.getDay()+6)%7)):o.setFullYear(r.y,r.m,r.d),o.setHours(r.H+Math.floor(r.Z/100),r.M+r.Z%100,r.S,r.L),i?o._:o},t.toString=function(){return n},t}function e(n,t,e,r){for(var u,i,o,a=0,c=t.length,s=e.length;c>a;){if(r>=s)return-1;if(u=t.charCodeAt(a++),37===u){if(o=t.charAt(a++),i=N[o in ec?t.charAt(a++):o],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){b.lastIndex=0;var r=b.exec(t.substring(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){M.lastIndex=0;var r=M.exec(t.substring(e));return r?(n.w=_.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){E.lastIndex=0;var r=E.exec(t.substring(e));return r?(n.m=A.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.substring(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,C.c.toString(),t,r)}function c(n,t,r){return e(n,C.x.toString(),t,r)}function s(n,t,r){return e(n,C.X.toString(),t,r)}function l(n,t,e){var r=x.get(t.substring(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{nc=Rt;var t=new nc;return t._=n,r(t)}finally{nc=Date}}var r=t(n);return e.parse=function(n){try{nc=Rt;var t=r.parse(n);return t&&t._}finally{nc=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=re;var x=Zo.map(),M=Ht(v),_=Ft(v),b=Ht(d),w=Ft(d),S=Ht(m),k=Ft(m),E=Ht(y),A=Ft(y);p.forEach(function(n,t){x.set(n.toLowerCase(),t)});var C={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return jt(n.getDate(),t,2)},e:function(n,t){return jt(n.getDate(),t,2)},H:function(n,t){return jt(n.getHours(),t,2)},I:function(n,t){return jt(n.getHours()%12||12,t,2)},j:function(n,t){return jt(1+Qa.dayOfYear(n),t,3)},L:function(n,t){return jt(n.getMilliseconds(),t,3)},m:function(n,t){return jt(n.getMonth()+1,t,2)},M:function(n,t){return jt(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return jt(n.getSeconds(),t,2)},U:function(n,t){return jt(Qa.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return jt(Qa.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return jt(n.getFullYear()%100,t,2)},Y:function(n,t){return jt(n.getFullYear()%1e4,t,4)},Z:te,"%":function(){return"%"}},N={a:r,A:u,b:i,B:o,c:a,d:Wt,e:Wt,H:Gt,I:Gt,j:Jt,L:ne,m:Bt,M:Kt,p:l,S:Qt,U:Yt,w:Ot,W:It,x:c,X:s,y:Vt,Y:Zt,Z:Xt,"%":ee};return t}function jt(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function Ht(n){return new RegExp("^(?:"+n.map(Zo.requote).join("|")+")","i")}function Ft(n){for(var t=new o,e=-1,r=n.length;++e<r;)t.set(n[e].toLowerCase(),e);return t}function Ot(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+1));return r?(n.w=+r[0],e+r[0].length):-1}function Yt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e));return r?(n.U=+r[0],e+r[0].length):-1}function It(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e));return r?(n.W=+r[0],e+r[0].length):-1}function Zt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+4));return r?(n.y=+r[0],e+r[0].length):-1}function Vt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+2));return r?(n.y=$t(+r[0]),e+r[0].length):-1}function Xt(n,t,e){return/^[+-]\d{4}$/.test(t=t.substring(e,e+5))?(n.Z=-t,e+5):-1}function $t(n){return n+(n>68?1900:2e3)}function Bt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function Wt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function Jt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function Gt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function Kt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function Qt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ne(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function te(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=~~(ua(t)/60),u=ua(t)%60;return e+jt(r,"0",2)+jt(u,"0",2)}function ee(n,t,e){uc.lastIndex=0;var r=uc.exec(t.substring(e,e+1));return r?e+r[0].length:-1}function re(n){for(var t=n.length,e=-1;++e<t;)n[e][0]=this(n[e][0]);return function(t){for(var e=0,r=n[e];!r[1](t);)r=n[++e];return r[0](t)}}function ue(){}function ie(n,t,e){var r=e.s=n+t,u=r-n,i=r-u;e.t=n-i+(t-u)}function oe(n,t){n&&cc.hasOwnProperty(n.type)&&cc[n.type](n,t)}function ae(n,t,e){var r,u=-1,i=n.length-e;for(t.lineStart();++u<i;)r=n[u],t.point(r[0],r[1],r[2]);t.lineEnd()}function ce(n,t){var e=-1,r=n.length;for(t.polygonStart();++e<r;)ae(n[e],t,1);t.polygonEnd()}function se(){function n(n,t){n*=Aa,t=t*Aa/2+ba/4;var e=n-r,o=e>=0?1:-1,a=o*e,c=Math.cos(t),s=Math.sin(t),l=i*s,f=u*c+l*Math.cos(a),h=l*o*Math.sin(a);lc.add(Math.atan2(h,f)),r=n,u=c,i=s}var t,e,r,u,i;fc.point=function(o,a){fc.point=n,r=(t=o)*Aa,u=Math.cos(a=(e=a)*Aa/2+ba/4),i=Math.sin(a)},fc.lineEnd=function(){n(t,e)}}function le(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function fe(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function he(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function ge(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function pe(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function ve(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function de(n){return[Math.atan2(n[1],n[0]),G(n[2])]}function me(n,t){return ua(n[0]-t[0])<ka&&ua(n[1]-t[1])<ka}function ye(n,t){n*=Aa;var e=Math.cos(t*=Aa);xe(e*Math.cos(n),e*Math.sin(n),Math.sin(t))}function xe(n,t,e){++hc,pc+=(n-pc)/hc,vc+=(t-vc)/hc,dc+=(e-dc)/hc}function Me(){function n(n,u){n*=Aa;var i=Math.cos(u*=Aa),o=i*Math.cos(n),a=i*Math.sin(n),c=Math.sin(u),s=Math.atan2(Math.sqrt((s=e*c-r*a)*s+(s=r*o-t*c)*s+(s=t*a-e*o)*s),t*o+e*a+r*c);gc+=s,mc+=s*(t+(t=o)),yc+=s*(e+(e=a)),xc+=s*(r+(r=c)),xe(t,e,r)}var t,e,r;wc.point=function(u,i){u*=Aa;var o=Math.cos(i*=Aa);t=o*Math.cos(u),e=o*Math.sin(u),r=Math.sin(i),wc.point=n,xe(t,e,r)}}function _e(){wc.point=ye}function be(){function n(n,t){n*=Aa;var e=Math.cos(t*=Aa),o=e*Math.cos(n),a=e*Math.sin(n),c=Math.sin(t),s=u*c-i*a,l=i*o-r*c,f=r*a-u*o,h=Math.sqrt(s*s+l*l+f*f),g=r*o+u*a+i*c,p=h&&-J(g)/h,v=Math.atan2(h,g);Mc+=p*s,_c+=p*l,bc+=p*f,gc+=v,mc+=v*(r+(r=o)),yc+=v*(u+(u=a)),xc+=v*(i+(i=c)),xe(r,u,i)}var t,e,r,u,i;wc.point=function(o,a){t=o,e=a,wc.point=n,o*=Aa;var c=Math.cos(a*=Aa);r=c*Math.cos(o),u=c*Math.sin(o),i=Math.sin(a),xe(r,u,i)},wc.lineEnd=function(){n(t,e),wc.lineEnd=_e,wc.point=ye}}function we(){return!0}function Se(n,t,e,r,u){var i=[],o=[];if(n.forEach(function(n){if(!((t=n.length-1)<=0)){var t,e=n[0],r=n[t];if(me(e,r)){u.lineStart();for(var a=0;t>a;++a)u.point((e=n[a])[0],e[1]);return u.lineEnd(),void 0}var c=new Ee(e,n,null,!0),s=new Ee(e,null,c,!1);c.o=s,i.push(c),o.push(s),c=new Ee(r,n,null,!1),s=new Ee(r,null,c,!0),c.o=s,i.push(c),o.push(s)}}),o.sort(t),ke(i),ke(o),i.length){for(var a=0,c=e,s=o.length;s>a;++a)o[a].e=c=!c;for(var l,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;l=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var a=0,s=l.length;s>a;++a)u.point((f=l[a])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){l=g.p.z;for(var a=l.length-1;a>=0;--a)u.point((f=l[a])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,l=g.z,p=!p}while(!g.v);u.lineEnd()}}}function ke(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r<t;)u.n=e=n[r],e.p=u,u=e;u.n=e=n[0],e.p=u}}function Ee(n,t,e,r){this.x=n,this.z=t,this.o=e,this.e=r,this.v=!1,this.n=this.p=null}function Ae(n,t,e,r){return function(u,i){function o(t,e){var r=u(t,e);n(t=r[0],e=r[1])&&i.point(t,e)}function a(n,t){var e=u(n,t);d.point(e[0],e[1])}function c(){y.point=a,d.lineStart()}function s(){y.point=o,d.lineEnd()}function l(n,t){v.push([n,t]);var e=u(n,t);M.point(e[0],e[1])}function f(){M.lineStart(),v=[]}function h(){l(v[0][0],v[0][1]),M.lineEnd();var n,t=M.clean(),e=x.buffer(),r=e.length;if(v.pop(),p.push(v),v=null,r)if(1&t){n=e[0];var u,r=n.length-1,o=-1;if(r>0){for(_||(i.polygonStart(),_=!0),i.lineStart();++o<r;)i.point((u=n[o])[0],u[1]);i.lineEnd()}}else r>1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Ce))}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:o,lineStart:c,lineEnd:s,polygonStart:function(){y.point=l,y.lineStart=f,y.lineEnd=h,g=[],p=[]},polygonEnd:function(){y.point=o,y.lineStart=c,y.lineEnd=s,g=Zo.merge(g);var n=Le(m,p);g.length?(_||(i.polygonStart(),_=!0),Se(g,ze,n,e,i)):n&&(_||(i.polygonStart(),_=!0),i.lineStart(),e(null,null,1,i),i.lineEnd()),_&&(i.polygonEnd(),_=!1),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},x=Ne(),M=t(x),_=!1;return y}}function Ce(n){return n.length>1}function Ne(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:v,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function ze(n,t){return((n=n.x)[0]<0?n[1]-Sa-ka:Sa-n[1])-((t=t.x)[0]<0?t[1]-Sa-ka:Sa-t[1])}function Le(n,t){var e=n[0],r=n[1],u=[Math.sin(e),-Math.cos(e),0],i=0,o=0;lc.reset();for(var a=0,c=t.length;c>a;++a){var s=t[a],l=s.length;if(l)for(var f=s[0],h=f[0],g=f[1]/2+ba/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===l&&(d=0),n=s[d];var m=n[0],y=n[1]/2+ba/4,x=Math.sin(y),M=Math.cos(y),_=m-h,b=_>=0?1:-1,w=b*_,S=w>ba,k=p*x;if(lc.add(Math.atan2(k*b*Math.sin(w),v*M+k*Math.cos(w))),i+=S?_+b*wa:_,S^h>=e^m>=e){var E=he(le(f),le(n));ve(E);var A=he(u,E);ve(A);var C=(S^_>=0?-1:1)*G(A[2]);(r>C||r===C&&(E[0]||E[1]))&&(o+=S^_>=0?1:-1)}if(!d++)break;h=m,p=x,v=M,f=n}}return(-ka>i||ka>i&&0>lc)^1&o}function Te(n){var t,e=0/0,r=0/0,u=0/0;return{lineStart:function(){n.lineStart(),t=1},point:function(i,o){var a=i>0?ba:-ba,c=ua(i-e);ua(c-ba)<ka?(n.point(e,r=(r+o)/2>0?Sa:-Sa),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(i,r),t=0):u!==a&&c>=ba&&(ua(e-u)<ka&&(e-=u*ka),ua(i-a)<ka&&(i-=a*ka),r=qe(e,r,i,o),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),t=0),n.point(e=i,r=o),u=a},lineEnd:function(){n.lineEnd(),e=r=0/0},clean:function(){return 2-t}}}function qe(n,t,e,r){var u,i,o=Math.sin(n-e);return ua(o)>ka?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*o)):(t+r)/2}function Re(n,t,e,r){var u;if(null==n)u=e*Sa,r.point(-ba,u),r.point(0,u),r.point(ba,u),r.point(ba,0),r.point(ba,-u),r.point(0,-u),r.point(-ba,-u),r.point(-ba,0),r.point(-ba,u);else if(ua(n[0]-t[0])>ka){var i=n[0]<t[0]?ba:-ba;u=e*i/2,r.point(-i,u),r.point(0,u),r.point(i,u)}else r.point(t[0],t[1])}function De(n){function t(n,t){return Math.cos(n)*Math.cos(t)>i}function e(n){var e,i,c,s,l;return{lineStart:function(){s=c=!1,l=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=o?v?0:u(f,h):v?u(f+(0>f?ba:-ba),h):0;if(!e&&(s=c=v)&&n.lineStart(),v!==c&&(g=r(e,p),(me(e,g)||me(p,g))&&(p[0]+=ka,p[1]+=ka,v=t(p[0],p[1]))),v!==c)l=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(a&&e&&o^v){var m;d&i||!(m=r(p,e,!0))||(l=0,o?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&me(e,p)||n.point(p[0],p[1]),e=p,c=v,i=d},lineEnd:function(){c&&n.lineEnd(),e=null},clean:function(){return l|(s&&c)<<1}}}function r(n,t,e){var r=le(n),u=le(t),o=[1,0,0],a=he(r,u),c=fe(a,a),s=a[0],l=c-s*s;if(!l)return!e&&n;var f=i*c/l,h=-i*s/l,g=he(o,a),p=pe(o,f),v=pe(a,h);ge(p,v);var d=g,m=fe(p,d),y=fe(d,d),x=m*m-y*(fe(p,p)-1);if(!(0>x)){var M=Math.sqrt(x),_=pe(d,(-m-M)/y);if(ge(_,p),_=de(_),!e)return _;var b,w=n[0],S=t[0],k=n[1],E=t[1];w>S&&(b=w,w=S,S=b);var A=S-w,C=ua(A-ba)<ka,N=C||ka>A;if(!C&&k>E&&(b=k,k=E,E=b),N?C?k+E>0^_[1]<(ua(_[0]-w)<ka?k:E):k<=_[1]&&_[1]<=E:A>ba^(w<=_[0]&&_[0]<=S)){var z=pe(d,(-m+M)/y);return ge(z,p),[_,de(z)]}}}function u(t,e){var r=o?n:ba-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),o=i>0,a=ua(i)>ka,c=sr(n,6*Aa);return Ae(t,e,c,o?[0,-n]:[-ba,n-ba])}function Pe(n,t,e,r){return function(u){var i,o=u.a,a=u.b,c=o.x,s=o.y,l=a.x,f=a.y,h=0,g=1,p=l-c,v=f-s;if(i=n-c,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-c,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-s,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-s,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:c+h*p,y:s+h*v}),1>g&&(u.b={x:c+g*p,y:s+g*v}),u}}}}}}function Ue(n,t,e,r){function u(r,u){return ua(r[0]-n)<ka?u>0?0:3:ua(r[0]-e)<ka?u>0?2:1:ua(r[1]-t)<ka?u>0?1:0:u>0?3:2}function i(n,t){return o(n.x,t.x)}function o(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function c(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,o=1,a=d[u],c=a.length,s=a[0];c>o;++o)i=a[o],s[1]<=r?i[1]>r&&W(s,i,n)>0&&++t:i[1]<=r&&W(s,i,n)<0&&--t,s=i;return 0!==t}function s(i,a,c,s){var l=0,f=0;if(null==i||(l=u(i,c))!==(f=u(a,c))||o(i,a)<0^c>0){do s.point(0===l||3===l?n:e,l>1?r:t);while((l=(l+c+4)%4)!==f)}else s.point(a[0],a[1])}function l(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){l(n,t)&&a.point(n,t)}function h(){N.point=p,d&&d.push(m=[]),S=!0,w=!1,_=b=0/0}function g(){v&&(p(y,x),M&&w&&A.rejoin(),v.push(A.buffer())),N.point=f,w&&a.lineEnd()}function p(n,t){n=Math.max(-kc,Math.min(kc,n)),t=Math.max(-kc,Math.min(kc,t));var e=l(n,t);if(d&&m.push([n,t]),S)y=n,x=t,M=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:_,y:b},b:{x:n,y:t}};C(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}_=n,b=t,w=e}var v,d,m,y,x,M,_,b,w,S,k,E=a,A=Ne(),C=Pe(n,t,e,r),N={point:f,lineStart:h,lineEnd:g,polygonStart:function(){a=A,v=[],d=[],k=!0},polygonEnd:function(){a=E,v=Zo.merge(v);var t=c([n,r]),e=k&&t,u=v.length;(e||u)&&(a.polygonStart(),e&&(a.lineStart(),s(null,null,1,a),a.lineEnd()),u&&Se(v,i,t,s,a),a.polygonEnd()),v=d=m=null}};return N}}function je(n,t){function e(e,r){return e=n(e,r),t(e[0],e[1])}return n.invert&&t.invert&&(e.invert=function(e,r){return e=t.invert(e,r),e&&n.invert(e[0],e[1])}),e}function He(n){var t=0,e=ba/3,r=tr(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*ba/180,e=n[1]*ba/180):[180*(t/ba),180*(e/ba)]},u}function Fe(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),o-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),o=Math.sqrt(i)/u;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/u,G((i-(n*n+e*e)*u*u)/(2*u))]},e}function Oe(){function n(n,t){Ac+=u*n-r*t,r=n,u=t}var t,e,r,u;Tc.point=function(i,o){Tc.point=n,t=r=i,e=u=o},Tc.lineEnd=function(){n(t,e)}}function Ye(n,t){Cc>n&&(Cc=n),n>zc&&(zc=n),Nc>t&&(Nc=t),t>Lc&&(Lc=t)}function Ie(){function n(n,t){o.push("M",n,",",t,i)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function u(){o.push("Z")}var i=Ze(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return i=Ze(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Ze(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Ve(n,t){pc+=n,vc+=t,++dc}function Xe(){function n(n,r){var u=n-t,i=r-e,o=Math.sqrt(u*u+i*i);mc+=o*(t+n)/2,yc+=o*(e+r)/2,xc+=o,Ve(t=n,e=r)}var t,e;Rc.point=function(r,u){Rc.point=n,Ve(t=r,e=u)}}function $e(){Rc.point=Ve}function Be(){function n(n,t){var e=n-r,i=t-u,o=Math.sqrt(e*e+i*i);mc+=o*(r+n)/2,yc+=o*(u+t)/2,xc+=o,o=u*n-r*t,Mc+=o*(r+n),_c+=o*(u+t),bc+=3*o,Ve(r=n,u=t)}var t,e,r,u;Rc.point=function(i,o){Rc.point=n,Ve(t=r=i,e=u=o)},Rc.lineEnd=function(){n(t,e)}}function We(n){function t(t,e){n.moveTo(t,e),n.arc(t,e,o,0,wa)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function u(){a.point=t}function i(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:u,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=u,a.point=t},pointRadius:function(n){return o=n,a},result:v};return a}function Je(n){function t(n){return(a?r:e)(n)}function e(t){return Qe(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){x=0/0,S.point=i,t.lineStart()}function i(e,r){var i=le([e,r]),o=n(e,r);u(x,M,y,_,b,w,x=o[0],M=o[1],y=e,_=i[0],b=i[1],w=i[2],a,t),t.point(x,M)}function o(){S.point=e,t.lineEnd()}function c(){r(),S.point=s,S.lineEnd=l}function s(n,t){i(f=n,h=t),g=x,p=M,v=_,d=b,m=w,S.point=i}function l(){u(x,M,y,_,b,w,g,p,f,v,d,m,a,t),S.lineEnd=o,o()}var f,h,g,p,v,d,m,y,x,M,_,b,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=c},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,a,c,s,l,f,h,g,p,v,d,m){var y=l-t,x=f-e,M=y*y+x*x;if(M>4*i&&d--){var _=a+g,b=c+p,w=s+v,S=Math.sqrt(_*_+b*b+w*w),k=Math.asin(w/=S),E=ua(ua(w)-1)<ka||ua(r-h)<ka?(r+h)/2:Math.atan2(b,_),A=n(E,k),C=A[0],N=A[1],z=C-t,L=N-e,T=x*z-y*L;(T*T/M>i||ua((y*z+x*L)/M-.5)>.3||o>a*g+c*p+s*v)&&(u(t,e,r,a,c,s,C,N,E,_/=S,b/=S,w,d,m),m.point(C,N),u(C,N,E,_,b,w,l,f,h,g,p,v,d,m))}}var i=.5,o=Math.cos(30*Aa),a=16;
+return t.precision=function(n){return arguments.length?(a=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function Ge(n){var t=Je(function(t,e){return n([t*Ca,e*Ca])});return function(n){return er(t(n))}}function Ke(n){this.stream=n}function Qe(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function nr(n){return tr(function(){return n})()}function tr(n){function t(n){return n=a(n[0]*Aa,n[1]*Aa),[n[0]*h+c,s-n[1]*h]}function e(n){return n=a.invert((n[0]-c)/h,(s-n[1])/h),n&&[n[0]*Ca,n[1]*Ca]}function r(){a=je(o=ir(m,y,x),i);var n=i(v,d);return c=g-n[0]*h,s=p+n[1]*h,u()}function u(){return l&&(l.valid=!1,l=null),t}var i,o,a,c,s,l,f=Je(function(n,t){return n=i(n,t),[n[0]*h+c,s-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,y=0,x=0,M=Sc,_=wt,b=null,w=null;return t.stream=function(n){return l&&(l.valid=!1),l=er(M(o,f(_(n)))),l.valid=!0,l},t.clipAngle=function(n){return arguments.length?(M=null==n?(b=n,Sc):De((b=+n)*Aa),u()):b},t.clipExtent=function(n){return arguments.length?(w=n,_=n?Ue(n[0][0],n[0][1],n[1][0],n[1][1]):wt,u()):w},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*Aa,d=n[1]%360*Aa,r()):[v*Ca,d*Ca]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Aa,y=n[1]%360*Aa,x=n.length>2?n[2]%360*Aa:0,r()):[m*Ca,y*Ca,x*Ca]},Zo.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function er(n){return Qe(n,function(t,e){n.point(t*Aa,e*Aa)})}function rr(n,t){return[n,t]}function ur(n,t){return[n>ba?n-wa:-ba>n?n+wa:n,t]}function ir(n,t,e){return n?t||e?je(ar(n),cr(t,e)):ar(n):t||e?cr(t,e):ur}function or(n){return function(t,e){return t+=n,[t>ba?t-wa:-ba>t?t+wa:t,e]}}function ar(n){var t=or(n);return t.invert=or(-n),t}function cr(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*r+a*u;return[Math.atan2(c*i-l*o,a*r-s*u),G(l*i+c*o)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*i-c*o;return[Math.atan2(c*i+s*o,a*r+l*u),G(l*r-a*u)]},e}function sr(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,o,a){var c=o*t;null!=u?(u=lr(e,u),i=lr(e,i),(o>0?i>u:u>i)&&(u+=o*wa)):(u=n+o*wa,i=n-.5*c);for(var s,l=u;o>0?l>i:i>l;l-=c)a.point((s=de([e,-r*Math.cos(l),-r*Math.sin(l)]))[0],s[1])}}function lr(n,t){var e=le(t);e[0]-=n,ve(e);var r=J(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-ka)%(2*Math.PI)}function fr(n,t,e){var r=Zo.range(n,t-ka,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function hr(n,t,e){var r=Zo.range(n,t-ka,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function gr(n){return n.source}function pr(n){return n.target}function vr(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),o=Math.cos(r),a=Math.sin(r),c=u*Math.cos(n),s=u*Math.sin(n),l=o*Math.cos(e),f=o*Math.sin(e),h=2*Math.asin(Math.sqrt(tt(r-t)+u*o*tt(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*c+t*l,u=e*s+t*f,o=e*i+t*a;return[Math.atan2(u,r)*Ca,Math.atan2(o,Math.sqrt(r*r+u*u))*Ca]}:function(){return[n*Ca,t*Ca]};return p.distance=h,p}function dr(){function n(n,u){var i=Math.sin(u*=Aa),o=Math.cos(u),a=ua((n*=Aa)-t),c=Math.cos(a);Dc+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*i-e*o*c)*a),e*i+r*o*c),t=n,e=i,r=o}var t,e,r;Pc.point=function(u,i){t=u*Aa,e=Math.sin(i*=Aa),r=Math.cos(i),Pc.point=n},Pc.lineEnd=function(){Pc.point=Pc.lineEnd=v}}function mr(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),o=Math.cos(u);return[Math.atan2(n*i,r*o),Math.asin(r&&e*i/r)]},e}function yr(n,t){function e(n,t){o>0?-Sa+ka>t&&(t=-Sa+ka):t>Sa-ka&&(t=Sa-ka);var e=o/Math.pow(u(t),i);return[e*Math.sin(i*n),o-e*Math.cos(i*n)]}var r=Math.cos(n),u=function(n){return Math.tan(ba/4+n/2)},i=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(u(t)/u(n)),o=r*Math.pow(u(n),i)/i;return i?(e.invert=function(n,t){var e=o-t,r=B(i)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/i,2*Math.atan(Math.pow(o/r,1/i))-Sa]},e):Mr}function xr(n,t){function e(n,t){var e=i-t;return[e*Math.sin(u*n),i-e*Math.cos(u*n)]}var r=Math.cos(n),u=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),i=r/u+n;return ua(u)<ka?rr:(e.invert=function(n,t){var e=i-t;return[Math.atan2(n,e)/u,i-B(u)*Math.sqrt(n*n+e*e)]},e)}function Mr(n,t){return[n,Math.log(Math.tan(ba/4+t/2))]}function _r(n){var t,e=nr(n),r=e.scale,u=e.translate,i=e.clipExtent;return e.scale=function(){var n=r.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.translate=function(){var n=u.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.clipExtent=function(n){var o=i.apply(e,arguments);if(o===e){if(t=null==n){var a=ba*r(),c=u();i([[c[0]-a,c[1]-a],[c[0]+a,c[1]+a]])}}else t&&(o=null);return o},e.clipExtent(null)}function br(n,t){return[Math.log(Math.tan(ba/4+t/2)),-n]}function wr(n){return n[0]}function Sr(n){return n[1]}function kr(n){for(var t=n.length,e=[0,1],r=2,u=2;t>u;u++){for(;r>1&&W(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function Er(n,t){return n[0]-t[0]||n[1]-t[1]}function Ar(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Cr(n,t,e,r){var u=n[0],i=e[0],o=t[0]-u,a=r[0]-i,c=n[1],s=e[1],l=t[1]-c,f=r[1]-s,h=(a*(c-s)-f*(u-i))/(f*o-a*l);return[u+h*o,c+h*l]}function Nr(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function zr(){Gr(this),this.edge=this.site=this.circle=null}function Lr(n){var t=Bc.pop()||new zr;return t.site=n,t}function Tr(n){Yr(n),Vc.remove(n),Bc.push(n),Gr(n)}function qr(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,o=n.N,a=[n];Tr(n);for(var c=i;c.circle&&ua(e-c.circle.x)<ka&&ua(r-c.circle.cy)<ka;)i=c.P,a.unshift(c),Tr(c),c=i;a.unshift(c),Yr(c);for(var s=o;s.circle&&ua(e-s.circle.x)<ka&&ua(r-s.circle.cy)<ka;)o=s.N,a.push(s),Tr(s),s=o;a.push(s),Yr(s);var l,f=a.length;for(l=1;f>l;++l)s=a[l],c=a[l-1],Br(s.edge,c.site,s.site,u);c=a[0],s=a[f-1],s.edge=Xr(c.site,s.site,null,u),Or(c),Or(s)}function Rr(n){for(var t,e,r,u,i=n.x,o=n.y,a=Vc._;a;)if(r=Dr(a,o)-i,r>ka)a=a.L;else{if(u=i-Pr(a,o),!(u>ka)){r>-ka?(t=a.P,e=a):u>-ka?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var c=Lr(n);if(Vc.insert(t,c),t||e){if(t===e)return Yr(t),e=Lr(t.site),Vc.insert(c,e),c.edge=e.edge=Xr(t.site,c.site),Or(t),Or(e),void 0;if(!e)return c.edge=Xr(t.site,c.site),void 0;Yr(t),Yr(e);var s=t.site,l=s.x,f=s.y,h=n.x-l,g=n.y-f,p=e.site,v=p.x-l,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,x=v*v+d*d,M={x:(d*y-g*x)/m+l,y:(h*x-v*y)/m+f};Br(e.edge,s,p,M),c.edge=Xr(s,n,null,M),e.edge=Xr(n,p,null,M),Or(t),Or(e)}}function Dr(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var o=n.P;if(!o)return-1/0;e=o.site;var a=e.x,c=e.y,s=c-t;if(!s)return a;var l=a-r,f=1/i-1/s,h=l/s;return f?(-h+Math.sqrt(h*h-2*f*(l*l/(-2*s)-c+s/2+u-i/2)))/f+r:(r+a)/2}function Pr(n,t){var e=n.N;if(e)return Dr(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ur(n){this.site=n,this.edges=[]}function jr(n){for(var t,e,r,u,i,o,a,c,s,l,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=Zc,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(a=i.edges,c=a.length,o=0;c>o;)l=a[o].end(),r=l.x,u=l.y,s=a[++o%c].start(),t=s.x,e=s.y,(ua(r-t)>ka||ua(u-e)>ka)&&(a.splice(o,0,new Wr($r(i.site,l,ua(r-f)<ka&&p-u>ka?{x:f,y:ua(t-f)<ka?e:p}:ua(u-p)<ka&&h-r>ka?{x:ua(e-p)<ka?t:h,y:p}:ua(r-h)<ka&&u-g>ka?{x:h,y:ua(t-h)<ka?e:g}:ua(u-g)<ka&&r-f>ka?{x:ua(e-g)<ka?t:f,y:g}:null),i.site,null)),++c)}function Hr(n,t){return t.angle-n.angle}function Fr(){Gr(this),this.x=this.y=this.arc=this.site=this.cy=null}function Or(n){var t=n.P,e=n.N;if(t&&e){var r=t.site,u=n.site,i=e.site;if(r!==i){var o=u.x,a=u.y,c=r.x-o,s=r.y-a,l=i.x-o,f=i.y-a,h=2*(c*f-s*l);if(!(h>=-Ea)){var g=c*c+s*s,p=l*l+f*f,v=(f*g-s*p)/h,d=(c*p-l*g)/h,f=d+a,m=Wc.pop()||new Fr;m.arc=n,m.site=u,m.x=v+o,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,x=$c._;x;)if(m.y<x.y||m.y===x.y&&m.x<=x.x){if(!x.L){y=x.P;break}x=x.L}else{if(!x.R){y=x;break}x=x.R}$c.insert(y,m),y||(Xc=m)}}}}function Yr(n){var t=n.circle;t&&(t.P||(Xc=t.N),$c.remove(t),Wc.push(t),Gr(t),n.circle=null)}function Ir(n){for(var t,e=Ic,r=Pe(n[0][0],n[0][1],n[1][0],n[1][1]),u=e.length;u--;)t=e[u],(!Zr(t,n)||!r(t)||ua(t.a.x-t.b.x)<ka&&ua(t.a.y-t.b.y)<ka)&&(t.a=t.b=null,e.splice(u,1))}function Zr(n,t){var e=n.b;if(e)return!0;var r,u,i=n.a,o=t[0][0],a=t[1][0],c=t[0][1],s=t[1][1],l=n.l,f=n.r,h=l.x,g=l.y,p=f.x,v=f.y,d=(h+p)/2,m=(g+v)/2;if(v===g){if(o>d||d>=a)return;if(h>p){if(i){if(i.y>=s)return}else i={x:d,y:c};e={x:d,y:s}}else{if(i){if(i.y<c)return}else i={x:d,y:s};e={x:d,y:c}}}else if(r=(h-p)/(v-g),u=m-r*d,-1>r||r>1)if(h>p){if(i){if(i.y>=s)return}else i={x:(c-u)/r,y:c};e={x:(s-u)/r,y:s}}else{if(i){if(i.y<c)return}else i={x:(s-u)/r,y:s};e={x:(c-u)/r,y:c}}else if(v>g){if(i){if(i.x>=a)return}else i={x:o,y:r*o+u};e={x:a,y:r*a+u}}else{if(i){if(i.x<o)return}else i={x:a,y:r*a+u};e={x:o,y:r*o+u}}return n.a=i,n.b=e,!0}function Vr(n,t){this.l=n,this.r=t,this.a=this.b=null}function Xr(n,t,e,r){var u=new Vr(n,t);return Ic.push(u),e&&Br(u,n,t,e),r&&Br(u,t,n,r),Zc[n.i].edges.push(new Wr(u,n,t)),Zc[t.i].edges.push(new Wr(u,t,n)),u}function $r(n,t,e){var r=new Vr(n,null);return r.a=t,r.b=e,Ic.push(r),r}function Br(n,t,e,r){n.a||n.b?n.l===e?n.b=r:n.a=r:(n.a=r,n.l=t,n.r=e)}function Wr(n,t,e){var r=n.a,u=n.b;this.edge=n,this.site=t,this.angle=e?Math.atan2(e.y-t.y,e.x-t.x):n.l===t?Math.atan2(u.x-r.x,r.y-u.y):Math.atan2(r.x-u.x,u.y-r.y)}function Jr(){this._=null}function Gr(n){n.U=n.C=n.L=n.R=n.P=n.N=null}function Kr(n,t){var e=t,r=t.R,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.R=r.L,e.R&&(e.R.U=e),r.L=e}function Qr(n,t){var e=t,r=t.L,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.L=r.R,e.L&&(e.L.U=e),r.R=e}function nu(n){for(;n.L;)n=n.L;return n}function tu(n,t){var e,r,u,i=n.sort(eu).pop();for(Ic=[],Zc=new Array(n.length),Vc=new Jr,$c=new Jr;;)if(u=Xc,i&&(!u||i.y<u.y||i.y===u.y&&i.x<u.x))(i.x!==e||i.y!==r)&&(Zc[i.i]=new Ur(i),Rr(i),e=i.x,r=i.y),i=n.pop();else{if(!u)break;qr(u.arc)}t&&(Ir(t),jr(t));var o={cells:Zc,edges:Ic};return Vc=$c=Ic=Zc=null,o}function eu(n,t){return t.y-n.y||t.x-n.x}function ru(n,t,e){return(n.x-e.x)*(t.y-n.y)-(n.x-t.x)*(e.y-n.y)}function uu(n){return n.x}function iu(n){return n.y}function ou(){return{leaf:!0,nodes:[],point:null,x:null,y:null}}function au(n,t,e,r,u,i){if(!n(t,e,r,u,i)){var o=.5*(e+u),a=.5*(r+i),c=t.nodes;c[0]&&au(n,c[0],e,r,o,a),c[1]&&au(n,c[1],o,r,u,a),c[2]&&au(n,c[2],e,a,o,i),c[3]&&au(n,c[3],o,a,u,i)}}function cu(n,t){n=Zo.rgb(n),t=Zo.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,o=t.g-r,a=t.b-u;return function(n){return"#"+dt(Math.round(e+i*n))+dt(Math.round(r+o*n))+dt(Math.round(u+a*n))}}function su(n,t){var e,r={},u={};for(e in n)e in t?r[e]=hu(n[e],t[e]):u[e]=n[e];for(e in t)e in n||(u[e]=t[e]);return function(n){for(e in r)u[e]=r[e](n);return u}}function lu(n,t){return t-=n=+n,function(e){return n+t*e}}function fu(n,t){var e,r,u,i=Gc.lastIndex=Kc.lastIndex=0,o=-1,a=[],c=[];for(n+="",t+="";(e=Gc.exec(n))&&(r=Kc.exec(t));)(u=r.index)>i&&(u=t.substring(i,u),a[o]?a[o]+=u:a[++o]=u),(e=e[0])===(r=r[0])?a[o]?a[o]+=r:a[++o]=r:(a[++o]=null,c.push({i:o,x:lu(e,r)})),i=Kc.lastIndex;return i<t.length&&(u=t.substring(i),a[o]?a[o]+=u:a[++o]=u),a.length<2?c[0]?(t=c[0].x,function(n){return t(n)+""}):function(){return t}:(t=c.length,function(n){for(var e,r=0;t>r;++r)a[(e=c[r]).i]=e.x(n);return a.join("")})}function hu(n,t){for(var e,r=Zo.interpolators.length;--r>=0&&!(e=Zo.interpolators[r](n,t)););return e}function gu(n,t){var e,r=[],u=[],i=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(hu(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;o>e;++e)u[e]=t[e];return function(n){for(e=0;a>e;++e)u[e]=r[e](n);return u}}function pu(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function vu(n){return function(t){return 1-n(1-t)}}function du(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function mu(n){return n*n}function yu(n){return n*n*n}function xu(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Mu(n){return function(t){return Math.pow(t,n)}}function _u(n){return 1-Math.cos(n*Sa)}function bu(n){return Math.pow(2,10*(n-1))}function wu(n){return 1-Math.sqrt(1-n*n)}function Su(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/wa*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*wa/t)}}function ku(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Eu(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Au(n,t){n=Zo.hcl(n),t=Zo.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,o=t.c-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return ot(e+i*n,r+o*n,u+a*n)+""}}function Cu(n,t){n=Zo.hsl(n),t=Zo.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,o=t.s-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return ut(e+i*n,r+o*n,u+a*n)+""}}function Nu(n,t){n=Zo.lab(n),t=Zo.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,o=t.a-r,a=t.b-u;return function(n){return ct(e+i*n,r+o*n,u+a*n)+""}}function zu(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Lu(n){var t=[n.a,n.b],e=[n.c,n.d],r=qu(t),u=Tu(t,e),i=qu(Ru(e,t,-u))||0;t[0]*e[1]<e[0]*t[1]&&(t[0]*=-1,t[1]*=-1,r*=-1,u*=-1),this.rotate=(r?Math.atan2(t[1],t[0]):Math.atan2(-e[0],e[1]))*Ca,this.translate=[n.e,n.f],this.scale=[r,i],this.skew=i?Math.atan2(u,i)*Ca:0}function Tu(n,t){return n[0]*t[0]+n[1]*t[1]}function qu(n){var t=Math.sqrt(Tu(n,n));return t&&(n[0]/=t,n[1]/=t),t}function Ru(n,t,e){return n[0]+=e*t[0],n[1]+=e*t[1],n}function Du(n,t){var e,r=[],u=[],i=Zo.transform(n),o=Zo.transform(t),a=i.translate,c=o.translate,s=i.rotate,l=o.rotate,f=i.skew,h=o.skew,g=i.scale,p=o.scale;return a[0]!=c[0]||a[1]!=c[1]?(r.push("translate(",null,",",null,")"),u.push({i:1,x:lu(a[0],c[0])},{i:3,x:lu(a[1],c[1])})):c[0]||c[1]?r.push("translate("+c+")"):r.push(""),s!=l?(s-l>180?l+=360:l-s>180&&(s+=360),u.push({i:r.push(r.pop()+"rotate(",null,")")-2,x:lu(s,l)})):l&&r.push(r.pop()+"rotate("+l+")"),f!=h?u.push({i:r.push(r.pop()+"skewX(",null,")")-2,x:lu(f,h)}):h&&r.push(r.pop()+"skewX("+h+")"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+"scale(",null,",",null,")"),u.push({i:e-4,x:lu(g[0],p[0])},{i:e-2,x:lu(g[1],p[1])})):(1!=p[0]||1!=p[1])&&r.push(r.pop()+"scale("+p+")"),e=u.length,function(n){for(var t,i=-1;++i<e;)r[(t=u[i]).i]=t.x(n);return r.join("")}}function Pu(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return(e-n)*t}}function Uu(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return Math.max(0,Math.min(1,(e-n)*t))}}function ju(n){for(var t=n.source,e=n.target,r=Fu(t,e),u=[t];t!==r;)t=t.parent,u.push(t);for(var i=u.length;e!==r;)u.splice(i,0,e),e=e.parent;return u}function Hu(n){for(var t=[],e=n.parent;null!=e;)t.push(n),n=e,e=e.parent;return t.push(n),t}function Fu(n,t){if(n===t)return n;for(var e=Hu(n),r=Hu(t),u=e.pop(),i=r.pop(),o=null;u===i;)o=u,u=e.pop(),i=r.pop();return o}function Ou(n){n.fixed|=2}function Yu(n){n.fixed&=-7}function Iu(n){n.fixed|=4,n.px=n.x,n.py=n.y}function Zu(n){n.fixed&=-5}function Vu(n,t,e){var r=0,u=0;if(n.charge=0,!n.leaf)for(var i,o=n.nodes,a=o.length,c=-1;++c<a;)i=o[c],null!=i&&(Vu(i,t,e),n.charge+=i.charge,r+=i.charge*i.cx,u+=i.charge*i.cy);if(n.point){n.leaf||(n.point.x+=Math.random()-.5,n.point.y+=Math.random()-.5);var s=t*e[n.point.index];n.charge+=n.pointCharge=s,r+=s*n.point.x,u+=s*n.point.y}n.cx=r/n.charge,n.cy=u/n.charge}function Xu(n,t){return Zo.rebind(n,t,"sort","children","value"),n.nodes=n,n.links=Ku,n}function $u(n,t){for(var e=[n];null!=(n=e.pop());)if(t(n),(u=n.children)&&(r=u.length))for(var r,u;--r>=0;)e.push(u[r])}function Bu(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(i=n.children)&&(u=i.length))for(var u,i,o=-1;++o<u;)e.push(i[o]);for(;null!=(n=r.pop());)t(n)}function Wu(n){return n.children}function Ju(n){return n.value}function Gu(n,t){return t.value-n.value}function Ku(n){return Zo.merge(n.map(function(n){return(n.children||[]).map(function(t){return{source:n,target:t}})}))}function Qu(n){return n.x}function ni(n){return n.y}function ti(n,t,e){n.y0=t,n.y=e}function ei(n){return Zo.range(n.length)}function ri(n){for(var t=-1,e=n[0].length,r=[];++t<e;)r[t]=0;return r}function ui(n){for(var t,e=1,r=0,u=n[0][1],i=n.length;i>e;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function ii(n){return n.reduce(oi,0)}function oi(n,t){return n+t[1]}function ai(n,t){return ci(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function ci(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function si(n){return[Zo.min(n),Zo.max(n)]}function li(n,t){return n.value-t.value}function fi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function hi(n,t){n._pack_next=t,t._pack_prev=n}function gi(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function pi(n){function t(n){l=Math.min(n.x-n.r,l),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(s=e.length)){var e,r,u,i,o,a,c,s,l=1/0,f=-1/0,h=1/0,g=-1/0;if(e.forEach(vi),r=e[0],r.x=-r.r,r.y=0,t(r),s>1&&(u=e[1],u.x=u.r,u.y=0,t(u),s>2))for(i=e[2],yi(r,u,i),t(i),fi(r,i),r._pack_prev=i,fi(i,u),u=r._pack_next,o=3;s>o;o++){yi(r,u,i=e[o]);var p=0,v=1,d=1;for(a=u._pack_next;a!==u;a=a._pack_next,v++)if(gi(a,i)){p=1;break}if(1==p)for(c=r._pack_prev;c!==a._pack_prev&&!gi(c,i);c=c._pack_prev,d++);p?(d>v||v==d&&u.r<r.r?hi(r,u=a):hi(r=c,u),o--):(fi(r,i),u=i,t(i))}var m=(l+f)/2,y=(h+g)/2,x=0;for(o=0;s>o;o++)i=e[o],i.x-=m,i.y-=y,x=Math.max(x,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=x,e.forEach(di)}}function vi(n){n._pack_next=n._pack_prev=n}function di(n){delete n._pack_next,delete n._pack_prev}function mi(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,o=u.length;++i<o;)mi(u[i],t,e,r)}function yi(n,t,e){var r=n.r+e.r,u=t.x-n.x,i=t.y-n.y;if(r&&(u||i)){var o=t.r+e.r,a=u*u+i*i;o*=o,r*=r;var c=.5+(r-o)/(2*a),s=Math.sqrt(Math.max(0,2*o*(r+a)-(r-=a)*r-o*o))/(2*a);e.x=n.x+c*u+s*i,e.y=n.y+c*i-s*u}else e.x=n.x+r,e.y=n.y}function xi(n,t){return n.parent==t.parent?1:2}function Mi(n){var t=n.children;return t.length?t[0]:n.t}function _i(n){var t,e=n.children;return(t=e.length)?e[t-1]:n.t}function bi(n,t,e){var r=e/(t.i-n.i);t.c-=r,t.s+=e,n.c+=r,t.z+=e,t.m+=e}function wi(n){for(var t,e=0,r=0,u=n.children,i=u.length;--i>=0;)t=u[i],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Si(n,t,e){return n.a.parent===t.parent?n.a:e}function ki(n){return 1+Zo.max(n,function(n){return n.y})}function Ei(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Ai(n){var t=n.children;return t&&t.length?Ai(t[0]):n}function Ci(n){var t,e=n.children;return e&&(t=e.length)?Ci(e[t-1]):n}function Ni(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function zi(n,t){var e=n.x+t[3],r=n.y+t[0],u=n.dx-t[1]-t[3],i=n.dy-t[0]-t[2];return 0>u&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Li(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Ti(n){return n.rangeExtent?n.rangeExtent():Li(n.range())}function qi(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Ri(n,t){var e,r=0,u=n.length-1,i=n[r],o=n[u];return i>o&&(e=r,r=u,u=e,e=i,i=o,o=e),n[r]=t.floor(i),n[u]=t.ceil(o),n}function Di(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:ss}function Pi(n,t,e,r){var u=[],i=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]<n[0]&&(n=n.slice().reverse(),t=t.slice().reverse());++o<=a;)u.push(e(n[o-1],n[o])),i.push(r(t[o-1],t[o]));return function(t){var e=Zo.bisect(n,t,1,a)-1;return i[e](u[e](t))}}function Ui(n,t,e,r){function u(){var u=Math.min(n.length,t.length)>2?Pi:qi,c=r?Uu:Pu;return o=u(n,t,c,e),a=u(t,n,c,hu),i}function i(n){return o(n)}var o,a;return i.invert=function(n){return a(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(zu)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Oi(n,t)},i.tickFormat=function(t,e){return Yi(n,t,e)},i.nice=function(t){return Hi(n,t),u()},i.copy=function(){return Ui(n,t,e,r)},u()}function ji(n,t){return Zo.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Hi(n,t){return Ri(n,Di(Fi(n,t)[2]))}function Fi(n,t){null==t&&(t=10);var e=Li(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Oi(n,t){return Zo.range.apply(Zo,Fi(n,t))}function Yi(n,t,e){var r=Fi(n,t);if(e){var u=Ga.exec(e);if(u.shift(),"s"===u[8]){var i=Zo.formatPrefix(Math.max(ua(r[0]),ua(r[1])));return u[7]||(u[7]="."+Ii(i.scale(r[2]))),u[8]="f",e=Zo.format(u.join("")),function(n){return e(i.scale(n))+i.symbol}}u[7]||(u[7]="."+Zi(u[8],r)),e=u.join("")}else e=",."+Ii(r[2])+"f";return Zo.format(e)}function Ii(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function Zi(n,t){var e=Ii(t[2]);return n in ls?Math.abs(e-Ii(Math.max(ua(t[0]),ua(t[1]))))+ +("e"!==n):e-2*("%"===n)}function Vi(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(u(t))}return o.invert=function(t){return i(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),o):t},o.nice=function(){var t=Ri(r.map(u),e?Math:hs);return n.domain(t),r=t.map(i),o},o.ticks=function(){var n=Li(r),o=[],a=n[0],c=n[1],s=Math.floor(u(a)),l=Math.ceil(u(c)),f=t%1?2:t;if(isFinite(l-s)){if(e){for(;l>s;s++)for(var h=1;f>h;h++)o.push(i(s)*h);o.push(i(s))}else for(o.push(i(s));s++<l;)for(var h=f-1;h>0;h--)o.push(i(s)*h);for(s=0;o[s]<a;s++);for(l=o.length;o[l-1]>c;l--);o=o.slice(s,l)}return o},o.tickFormat=function(n,t){if(!arguments.length)return fs;arguments.length<2?t=fs:"function"!=typeof t&&(t=Zo.format(t));var r,a=Math.max(.1,n/o.ticks().length),c=e?(r=1e-12,Math.ceil):(r=-1e-12,Math.floor);return function(n){return n/i(c(u(n)+r))<=a?t(n):""}},o.copy=function(){return Vi(n.copy(),t,e,r)},ji(o,n)}function Xi(n,t,e){function r(t){return n(u(t))}var u=$i(t),i=$i(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Oi(e,n)},r.tickFormat=function(n,t){return Yi(e,n,t)},r.nice=function(n){return r.domain(Hi(e,n))},r.exponent=function(o){return arguments.length?(u=$i(t=o),i=$i(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return Xi(n.copy(),t,e)},ji(r,n)}function $i(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function Bi(n,t){function e(e){return i[((u.get(e)||("range"===t.t?u.set(e,n.push(e)):0/0))-1)%i.length]}function r(t,e){return Zo.range(n.length).map(function(n){return t+e*n})}var u,i,a;return e.domain=function(r){if(!arguments.length)return n;n=[],u=new o;for(var i,a=-1,c=r.length;++a<c;)u.has(i=r[a])||u.set(i,n.push(i));return e[t.t].apply(e,t.a)},e.range=function(n){return arguments.length?(i=n,a=0,t={t:"range",a:arguments},e):i},e.rangePoints=function(u,o){arguments.length<2&&(o=0);var c=u[0],s=u[1],l=(s-c)/(Math.max(1,n.length-1)+o);return i=r(n.length<2?(c+s)/2:c+l*o/2,l),a=0,t={t:"rangePoints",a:arguments},e},e.rangeBands=function(u,o,c){arguments.length<2&&(o=0),arguments.length<3&&(c=o);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=(f-l)/(n.length-o+2*c);return i=r(l+h*c,h),s&&i.reverse(),a=h*(1-o),t={t:"rangeBands",a:arguments},e},e.rangeRoundBands=function(u,o,c){arguments.length<2&&(o=0),arguments.length<3&&(c=o);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=Math.floor((f-l)/(n.length-o+2*c)),g=f-l-(n.length-o)*h;return i=r(l+Math.round(g/2),h),s&&i.reverse(),a=Math.round(h*(1-o)),t={t:"rangeRoundBands",a:arguments},e},e.rangeBand=function(){return a},e.rangeExtent=function(){return Li(t.a[0])},e.copy=function(){return Bi(n,t)},e.domain(n)}function Wi(e,r){function u(){var n=0,t=r.length;for(o=[];++n<t;)o[n-1]=Zo.quantile(e,n/t);return i}function i(n){return isNaN(n=+n)?void 0:r[Zo.bisect(o,n)]}var o;return i.domain=function(r){return arguments.length?(e=r.filter(t).sort(n),u()):e},i.range=function(n){return arguments.length?(r=n,u()):r},i.quantiles=function(){return o},i.invertExtent=function(n){return n=r.indexOf(n),0>n?[0/0,0/0]:[n>0?o[n-1]:e[0],n<o.length?o[n]:e[e.length-1]]},i.copy=function(){return Wi(e,r)},u()}function Ji(n,t,e){function r(t){return e[Math.max(0,Math.min(o,Math.floor(i*(t-n))))]}function u(){return i=e.length/(t-n),o=e.length-1,r}var i,o;return r.domain=function(e){return arguments.length?(n=+e[0],t=+e[e.length-1],u()):[n,t]},r.range=function(n){return arguments.length?(e=n,u()):e},r.invertExtent=function(t){return t=e.indexOf(t),t=0>t?0/0:t/i+n,[t,t+1/i]},r.copy=function(){return Ji(n,t,e)},u()}function Gi(n,t){function e(e){return e>=e?t[Zo.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return Gi(n,t)},e}function Ki(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Oi(n,t)},t.tickFormat=function(t,e){return Yi(n,t,e)},t.copy=function(){return Ki(n)},t}function Qi(n){return n.innerRadius}function no(n){return n.outerRadius}function to(n){return n.startAngle}function eo(n){return n.endAngle}function ro(n){function t(t){function o(){s.push("M",i(n(l),a))}for(var c,s=[],l=[],f=-1,h=t.length,g=bt(e),p=bt(r);++f<h;)u.call(this,c=t[f],f)?l.push([+g.call(this,c,f),+p.call(this,c,f)]):l.length&&(o(),l=[]);return l.length&&o(),s.length?s.join(""):null}var e=wr,r=Sr,u=we,i=uo,o=i.key,a=.7;return t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t.defined=function(n){return arguments.length?(u=n,t):u},t.interpolate=function(n){return arguments.length?(o="function"==typeof n?i=n:(i=xs.get(n)||uo).key,t):o},t.tension=function(n){return arguments.length?(a=n,t):a},t}function uo(n){return n.join("L")}function io(n){return uo(n)+"Z"}function oo(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r[0]+(r=n[t])[0])/2,"V",r[1]);return e>1&&u.push("H",r[0]),u.join("")}function ao(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("V",(r=n[t])[1],"H",r[0]);return u.join("")}function co(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r=n[t])[0],"V",r[1]);return u.join("")}function so(n,t){return n.length<4?uo(n):n[1]+ho(n.slice(1,n.length-1),go(n,t))}function lo(n,t){return n.length<3?uo(n):n[0]+ho((n.push(n[0]),n),go([n[n.length-2]].concat(n,[n[1]]),t))}function fo(n,t){return n.length<3?uo(n):n[0]+ho(n,go(n,t))}function ho(n,t){if(t.length<1||n.length!=t.length&&n.length!=t.length+2)return uo(n);var e=n.length!=t.length,r="",u=n[0],i=n[1],o=t[0],a=o,c=1;if(e&&(r+="Q"+(i[0]-2*o[0]/3)+","+(i[1]-2*o[1]/3)+","+i[0]+","+i[1],u=n[1],c=2),t.length>1){a=t[1],i=n[c],c++,r+="C"+(u[0]+o[0])+","+(u[1]+o[1])+","+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1];for(var s=2;s<t.length;s++,c++)i=n[c],a=t[s],r+="S"+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1]}if(e){var l=n[c];r+="Q"+(i[0]+2*a[0]/3)+","+(i[1]+2*a[1]/3)+","+l[0]+","+l[1]}return r}function go(n,t){for(var e,r=[],u=(1-t)/2,i=n[0],o=n[1],a=1,c=n.length;++a<c;)e=i,i=o,o=n[a],r.push([u*(o[0]-e[0]),u*(o[1]-e[1])]);return r}function po(n){if(n.length<3)return uo(n);var t=1,e=n.length,r=n[0],u=r[0],i=r[1],o=[u,u,u,(r=n[1])[0]],a=[i,i,i,r[1]],c=[u,",",i,"L",xo(bs,o),",",xo(bs,a)];for(n.push(n[e-1]);++t<=e;)r=n[t],o.shift(),o.push(r[0]),a.shift(),a.push(r[1]),Mo(c,o,a);return n.pop(),c.push("L",r),c.join("")}function vo(n){if(n.length<4)return uo(n);for(var t,e=[],r=-1,u=n.length,i=[0],o=[0];++r<3;)t=n[r],i.push(t[0]),o.push(t[1]);for(e.push(xo(bs,i)+","+xo(bs,o)),--r;++r<u;)t=n[r],i.shift(),i.push(t[0]),o.shift(),o.push(t[1]),Mo(e,i,o);return e.join("")}function mo(n){for(var t,e,r=-1,u=n.length,i=u+4,o=[],a=[];++r<4;)e=n[r%u],o.push(e[0]),a.push(e[1]);for(t=[xo(bs,o),",",xo(bs,a)],--r;++r<i;)e=n[r%u],o.shift(),o.push(e[0]),a.shift(),a.push(e[1]),Mo(t,o,a);return t.join("")}function yo(n,t){var e=n.length-1;if(e)for(var r,u,i=n[0][0],o=n[0][1],a=n[e][0]-i,c=n[e][1]-o,s=-1;++s<=e;)r=n[s],u=s/e,r[0]=t*r[0]+(1-t)*(i+u*a),r[1]=t*r[1]+(1-t)*(o+u*c);return po(n)}function xo(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]+n[3]*t[3]}function Mo(n,t,e){n.push("C",xo(Ms,t),",",xo(Ms,e),",",xo(_s,t),",",xo(_s,e),",",xo(bs,t),",",xo(bs,e))}function _o(n,t){return(t[1]-n[1])/(t[0]-n[0])}function bo(n){for(var t=0,e=n.length-1,r=[],u=n[0],i=n[1],o=r[0]=_o(u,i);++t<e;)r[t]=(o+(o=_o(u=i,i=n[t+1])))/2;return r[t]=o,r}function wo(n){for(var t,e,r,u,i=[],o=bo(n),a=-1,c=n.length-1;++a<c;)t=_o(n[a],n[a+1]),ua(t)<ka?o[a]=o[a+1]=0:(e=o[a]/t,r=o[a+1]/t,u=e*e+r*r,u>9&&(u=3*t/Math.sqrt(u),o[a]=u*e,o[a+1]=u*r));for(a=-1;++a<=c;)u=(n[Math.min(c,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),i.push([u||0,o[a]*u||0]);return i}function So(n){return n.length<3?uo(n):n[0]+ho(n,wo(n))}function ko(n){for(var t,e,r,u=-1,i=n.length;++u<i;)t=n[u],e=t[0],r=t[1]+ms,t[0]=e*Math.cos(r),t[1]=e*Math.sin(r);return n}function Eo(n){function t(t){function c(){v.push("M",a(n(m),f),l,s(n(d.reverse()),f),"Z")}for(var h,g,p,v=[],d=[],m=[],y=-1,x=t.length,M=bt(e),_=bt(u),b=e===r?function(){return g}:bt(r),w=u===i?function(){return p}:bt(i);++y<x;)o.call(this,h=t[y],y)?(d.push([g=+M.call(this,h,y),p=+_.call(this,h,y)]),m.push([+b.call(this,h,y),+w.call(this,h,y)])):d.length&&(c(),d=[],m=[]);return d.length&&c(),v.length?v.join(""):null}var e=wr,r=wr,u=0,i=Sr,o=we,a=uo,c=a.key,s=a,l="L",f=.7;return t.x=function(n){return arguments.length?(e=r=n,t):r},t.x0=function(n){return arguments.length?(e=n,t):e},t.x1=function(n){return arguments.length?(r=n,t):r},t.y=function(n){return arguments.length?(u=i=n,t):i},t.y0=function(n){return arguments.length?(u=n,t):u},t.y1=function(n){return arguments.length?(i=n,t):i},t.defined=function(n){return arguments.length?(o=n,t):o},t.interpolate=function(n){return arguments.length?(c="function"==typeof n?a=n:(a=xs.get(n)||uo).key,s=a.reverse||a,l=a.closed?"M":"L",t):c},t.tension=function(n){return arguments.length?(f=n,t):f},t}function Ao(n){return n.radius}function Co(n){return[n.x,n.y]}function No(n){return function(){var t=n.apply(this,arguments),e=t[0],r=t[1]+ms;return[e*Math.cos(r),e*Math.sin(r)]}}function zo(){return 64}function Lo(){return"circle"}function To(n){var t=Math.sqrt(n/ba);return"M0,"+t+"A"+t+","+t+" 0 1,1 0,"+-t+"A"+t+","+t+" 0 1,1 0,"+t+"Z"}function qo(n,t){return sa(n,Cs),n.id=t,n}function Ro(n,t,e,r){var u=n.id;return P(n,"function"==typeof e?function(n,i,o){n.__transition__[u].tween.set(t,r(e.call(n,n.__data__,i,o)))}:(e=r(e),function(n){n.__transition__[u].tween.set(t,e)}))}function Do(n){return null==n&&(n=""),function(){this.textContent=n}}function Po(n,t,e,r){var u=n.__transition__||(n.__transition__={active:0,count:0}),i=u[e];if(!i){var a=r.time;i=u[e]={tween:new o,time:a,ease:r.ease,delay:r.delay,duration:r.duration},++u.count,Zo.timer(function(r){function o(r){return u.active>e?s():(u.active=e,i.event&&i.event.start.call(n,l,t),i.tween.forEach(function(e,r){(r=r.call(n,l,t))&&v.push(r)}),Zo.timer(function(){return p.c=c(r||1)?we:c,1},0,a),void 0)}function c(r){if(u.active!==e)return s();for(var o=r/g,a=f(o),c=v.length;c>0;)v[--c].call(n,a);
+return o>=1?(i.event&&i.event.end.call(n,l,t),s()):void 0}function s(){return--u.count?delete u[e]:delete n.__transition__,1}var l=n.__data__,f=i.ease,h=i.delay,g=i.duration,p=Ba,v=[];return p.t=h+a,r>=h?o(r-h):(p.c=o,void 0)},0,a)}}function Uo(n,t){n.attr("transform",function(n){return"translate("+t(n)+",0)"})}function jo(n,t){n.attr("transform",function(n){return"translate(0,"+t(n)+")"})}function Ho(n){return n.toISOString()}function Fo(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=Zo.bisect(Us,u);return i==Us.length?[t.year,Fi(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/Us[i-1]<Us[i]/u?i-1:i]:[Fs,Fi(n,e)[2]]}return r.invert=function(t){return Oo(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain(t),r):n.domain().map(Oo)},r.nice=function(n,t){function e(e){return!isNaN(e)&&!n.range(e,Oo(+e+1),t).length}var i=r.domain(),o=Li(i),a=null==n?u(o,10):"number"==typeof n&&u(o,n);return a&&(n=a[0],t=a[1]),r.domain(Ri(i,t>1?{floor:function(t){for(;e(t=n.floor(t));)t=Oo(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=Oo(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Li(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],Oo(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return Fo(n.copy(),t,e)},ji(r,n)}function Oo(n){return new Date(n)}function Yo(n){return JSON.parse(n.responseText)}function Io(n){var t=$o.createRange();return t.selectNode($o.body),t.createContextualFragment(n.responseText)}var Zo={version:"3.4.11"};Date.now||(Date.now=function(){return+new Date});var Vo=[].slice,Xo=function(n){return Vo.call(n)},$o=document,Bo=$o.documentElement,Wo=window;try{Xo(Bo.childNodes)[0].nodeType}catch(Jo){Xo=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}try{$o.createElement("div").style.setProperty("opacity",0,"")}catch(Go){var Ko=Wo.Element.prototype,Qo=Ko.setAttribute,na=Ko.setAttributeNS,ta=Wo.CSSStyleDeclaration.prototype,ea=ta.setProperty;Ko.setAttribute=function(n,t){Qo.call(this,n,t+"")},Ko.setAttributeNS=function(n,t,e){na.call(this,n,t,e+"")},ta.setProperty=function(n,t,e){ea.call(this,n,t+"",e)}}Zo.ascending=n,Zo.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},Zo.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&e>r&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&e>r&&(e=r)}return e},Zo.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&r>e&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&r>e&&(e=r)}return e},Zo.extent=function(n,t){var e,r,u,i=-1,o=n.length;if(1===arguments.length){for(;++i<o&&!(null!=(e=u=n[i])&&e>=e);)e=u=void 0;for(;++i<o;)null!=(r=n[i])&&(e>r&&(e=r),r>u&&(u=r))}else{for(;++i<o&&!(null!=(e=u=t.call(n,n[i],i))&&e>=e);)e=void 0;for(;++i<o;)null!=(r=t.call(n,n[i],i))&&(e>r&&(e=r),r>u&&(u=r))}return[e,u]},Zo.sum=function(n,t){var e,r=0,u=n.length,i=-1;if(1===arguments.length)for(;++i<u;)isNaN(e=+n[i])||(r+=e);else for(;++i<u;)isNaN(e=+t.call(n,n[i],i))||(r+=e);return r},Zo.mean=function(n,e){var r,u=0,i=n.length,o=-1,a=i;if(1===arguments.length)for(;++o<i;)t(r=n[o])?u+=r:--a;else for(;++o<i;)t(r=e.call(n,n[o],o))?u+=r:--a;return a?u/a:void 0},Zo.quantile=function(n,t){var e=(n.length-1)*t+1,r=Math.floor(e),u=+n[r-1],i=e-r;return i?u+i*(n[r]-u):u},Zo.median=function(e,r){return arguments.length>1&&(e=e.map(r)),e=e.filter(t),e.length?Zo.quantile(e.sort(n),.5):void 0};var ra=e(n);Zo.bisectLeft=ra.left,Zo.bisect=Zo.bisectRight=ra.right,Zo.bisector=function(t){return e(1===t.length?function(e,r){return n(t(e),r)}:t)},Zo.shuffle=function(n){for(var t,e,r=n.length;r;)e=0|Math.random()*r--,t=n[r],n[r]=n[e],n[e]=t;return n},Zo.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},Zo.pairs=function(n){for(var t,e=0,r=n.length-1,u=n[0],i=new Array(0>r?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},Zo.zip=function(){if(!(u=arguments.length))return[];for(var n=-1,t=Zo.min(arguments,r),e=new Array(t);++n<t;)for(var u,i=-1,o=e[n]=new Array(u);++i<u;)o[i]=arguments[i][n];return e},Zo.transpose=function(n){return Zo.zip.apply(Zo,n)},Zo.keys=function(n){var t=[];for(var e in n)t.push(e);return t},Zo.values=function(n){var t=[];for(var e in n)t.push(n[e]);return t},Zo.entries=function(n){var t=[];for(var e in n)t.push({key:e,value:n[e]});return t},Zo.merge=function(n){for(var t,e,r,u=n.length,i=-1,o=0;++i<u;)o+=n[i].length;for(e=new Array(o);--u>=0;)for(r=n[u],t=r.length;--t>=0;)e[--o]=r[t];return e};var ua=Math.abs;Zo.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),1/0===(t-n)/e)throw new Error("infinite range");var r,i=[],o=u(ua(e)),a=-1;if(n*=o,t*=o,e*=o,0>e)for(;(r=n+e*++a)>t;)i.push(r/o);else for(;(r=n+e*++a)<t;)i.push(r/o);return i},Zo.map=function(n){var t=new o;if(n instanceof o)n.forEach(function(n,e){t.set(n,e)});else for(var e in n)t.set(e,n[e]);return t},i(o,{has:a,get:function(n){return this[ia+n]},set:function(n,t){return this[ia+n]=t},remove:c,keys:s,values:function(){var n=[];return this.forEach(function(t,e){n.push(e)}),n},entries:function(){var n=[];return this.forEach(function(t,e){n.push({key:t,value:e})}),n},size:l,empty:f,forEach:function(n){for(var t in this)t.charCodeAt(0)===oa&&n.call(this,t.substring(1),this[t])}});var ia="\x00",oa=ia.charCodeAt(0);Zo.nest=function(){function n(t,a,c){if(c>=i.length)return r?r.call(u,a):e?a.sort(e):a;for(var s,l,f,h,g=-1,p=a.length,v=i[c++],d=new o;++g<p;)(h=d.get(s=v(l=a[g])))?h.push(l):d.set(s,[l]);return t?(l=t(),f=function(e,r){l.set(e,n(t,r,c))}):(l={},f=function(e,r){l[e]=n(t,r,c)}),d.forEach(f),l}function t(n,e){if(e>=i.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,u={},i=[],a=[];return u.map=function(t,e){return n(e,t,0)},u.entries=function(e){return t(n(Zo.map,e,0),0)},u.key=function(n){return i.push(n),u},u.sortKeys=function(n){return a[i.length-1]=n,u},u.sortValues=function(n){return e=n,u},u.rollup=function(n){return r=n,u},u},Zo.set=function(n){var t=new h;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},i(h,{has:a,add:function(n){return this[ia+n]=!0,n},remove:function(n){return n=ia+n,n in this&&delete this[n]},values:s,size:l,empty:f,forEach:function(n){for(var t in this)t.charCodeAt(0)===oa&&n.call(this,t.substring(1))}}),Zo.behavior={},Zo.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r<u;)n[e=arguments[r]]=g(n,t,t[e]);return n};var aa=["webkit","ms","moz","Moz","o","O"];Zo.dispatch=function(){for(var n=new d,t=-1,e=arguments.length;++t<e;)n[arguments[t]]=m(n);return n},d.prototype.on=function(n,t){var e=n.indexOf("."),r="";if(e>=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},Zo.event=null,Zo.requote=function(n){return n.replace(ca,"\\$&")};var ca=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,sa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},la=function(n,t){return t.querySelector(n)},fa=function(n,t){return t.querySelectorAll(n)},ha=Bo.matches||Bo[p(Bo,"matchesSelector")],ga=function(n,t){return ha.call(n,t)};"function"==typeof Sizzle&&(la=function(n,t){return Sizzle(n,t)[0]||null},fa=Sizzle,ga=Sizzle.matchesSelector),Zo.selection=function(){return ma};var pa=Zo.selection.prototype=[];pa.select=function(n){var t,e,r,u,i=[];n=b(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]),t.parentNode=(r=this[o]).parentNode;for(var c=-1,s=r.length;++c<s;)(u=r[c])?(t.push(e=n.call(u,u.__data__,c,o)),e&&"__data__"in u&&(e.__data__=u.__data__)):t.push(null)}return _(i)},pa.selectAll=function(n){var t,e,r=[];n=w(n);for(var u=-1,i=this.length;++u<i;)for(var o=this[u],a=-1,c=o.length;++a<c;)(e=o[a])&&(r.push(t=Xo(n.call(e,e.__data__,a,u))),t.parentNode=e);return _(r)};var va={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};Zo.ns={prefix:va,qualify:function(n){var t=n.indexOf(":"),e=n;return t>=0&&(e=n.substring(0,t),n=n.substring(t+1)),va.hasOwnProperty(e)?{space:va[e],local:n}:n}},pa.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=Zo.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(S(t,n[t]));return this}return this.each(S(n,t))},pa.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=A(n)).length,u=-1;if(t=e.classList){for(;++u<r;)if(!t.contains(n[u]))return!1}else for(t=e.getAttribute("class");++u<r;)if(!E(n[u]).test(t))return!1;return!0}for(t in n)this.each(C(t,n[t]));return this}return this.each(C(n,t))},pa.style=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t="");for(e in n)this.each(z(e,n[e],t));return this}if(2>r)return Wo.getComputedStyle(this.node(),null).getPropertyValue(n);e=""}return this.each(z(n,t,e))},pa.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(L(t,n[t]));return this}return this.each(L(n,t))},pa.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},pa.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},pa.append=function(n){return n=T(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},pa.insert=function(n,t){return n=T(n),t=b(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},pa.remove=function(){return this.each(function(){var n=this.parentNode;n&&n.removeChild(this)})},pa.data=function(n,t){function e(n,e){var r,u,i,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new o,y=new o,x=[];for(r=-1;++r<a;)d=t.call(u=n[r],u.__data__,r),m.has(d)?v[r]=u:m.set(d,u),x.push(d);for(r=-1;++r<f;)d=t.call(e,i=e[r],r),(u=m.get(d))?(g[r]=u,u.__data__=i):y.has(d)||(p[r]=q(i)),y.set(d,i),m.remove(d);for(r=-1;++r<a;)m.has(x[r])&&(v[r]=n[r])}else{for(r=-1;++r<h;)u=n[r],i=e[r],u?(u.__data__=i,g[r]=u):p[r]=q(i);for(;f>r;++r)p[r]=q(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,c.push(p),s.push(g),l.push(v)}var r,u,i=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++i<a;)(u=r[i])&&(n[i]=u.__data__);return n}var c=U([]),s=_([]),l=_([]);if("function"==typeof n)for(;++i<a;)e(r=this[i],n.call(r,r.parentNode.__data__,i));else for(;++i<a;)e(r=this[i],n);return s.enter=function(){return c},s.exit=function(){return l},s},pa.datum=function(n){return arguments.length?this.property("__data__",n):this.property("__data__")},pa.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=R(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return _(u)},pa.order=function(){for(var n=-1,t=this.length;++n<t;)for(var e,r=this[n],u=r.length-1,i=r[u];--u>=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},pa.sort=function(n){n=D.apply(this,arguments);for(var t=-1,e=this.length;++t<e;)this[t].sort(n);return this.order()},pa.each=function(n){return P(this,function(t,e,r){n.call(t,t.__data__,e,r)})},pa.call=function(n){var t=Xo(arguments);return n.apply(t[0]=this,t),this},pa.empty=function(){return!this.node()},pa.node=function(){for(var n=0,t=this.length;t>n;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},pa.size=function(){var n=0;return this.each(function(){++n}),n};var da=[];Zo.selection.enter=U,Zo.selection.enter.prototype=da,da.append=pa.append,da.empty=pa.empty,da.node=pa.node,da.call=pa.call,da.size=pa.size,da.select=function(n){for(var t,e,r,u,i,o=[],a=-1,c=this.length;++a<c;){r=(u=this[a]).update,o.push(t=[]),t.parentNode=u.parentNode;for(var s=-1,l=u.length;++s<l;)(i=u[s])?(t.push(r[s]=e=n.call(u.parentNode,i.__data__,s,a)),e.__data__=i.__data__):t.push(null)}return _(o)},da.insert=function(n,t){return arguments.length<2&&(t=j(this)),pa.insert.call(this,n,t)},pa.transition=function(){for(var n,t,e=Ss||++Ns,r=[],u=ks||{time:Date.now(),ease:xu,delay:0,duration:250},i=-1,o=this.length;++i<o;){r.push(n=[]);for(var a=this[i],c=-1,s=a.length;++c<s;)(t=a[c])&&Po(t,c,e,u),n.push(t)}return qo(r,e)},pa.interrupt=function(){return this.each(H)},Zo.select=function(n){var t=["string"==typeof n?la(n,$o):n];return t.parentNode=Bo,_([t])},Zo.selectAll=function(n){var t=Xo("string"==typeof n?fa(n,$o):n);return t.parentNode=Bo,_([t])};var ma=Zo.select(Bo);pa.on=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(F(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(F(n,t,e))};var ya=Zo.map({mouseenter:"mouseover",mouseleave:"mouseout"});ya.forEach(function(n){"on"+n in $o&&ya.remove(n)});var xa="onselectstart"in $o?null:p(Bo.style,"userSelect"),Ma=0;Zo.mouse=function(n){return Z(n,x())};var _a=/WebKit/.test(Wo.navigator.userAgent)?-1:0;Zo.touches=function(n,t){return arguments.length<2&&(t=x().touches),t?Xo(t).map(function(t){var e=Z(n,t);return e.identifier=t.identifier,e}):[]},Zo.behavior.drag=function(){function n(){this.on("mousedown.drag",u).on("touchstart.drag",i)}function t(n,t,u,i,o){return function(){function a(){var n,e,r=t(h,v);r&&(n=r[0]-x[0],e=r[1]-x[1],p|=n|e,x=r,g({type:"drag",x:r[0]+s[0],y:r[1]+s[1],dx:n,dy:e}))}function c(){t(h,v)&&(m.on(i+d,null).on(o+d,null),y(p&&Zo.event.target===f),g({type:"dragend"}))}var s,l=this,f=Zo.event.target,h=l.parentNode,g=e.of(l,arguments),p=0,v=n(),d=".drag"+(null==v?"":"-"+v),m=Zo.select(u()).on(i+d,a).on(o+d,c),y=I(),x=t(h,v);r?(s=r.apply(l,arguments),s=[s.x-x[0],s.y-x[1]]):s=[0,0],g({type:"dragstart"})}}var e=M(n,"drag","dragstart","dragend"),r=null,u=t(v,Zo.mouse,$,"mousemove","mouseup"),i=t(V,Zo.touch,X,"touchmove","touchend");return n.origin=function(t){return arguments.length?(r=t,n):r},Zo.rebind(n,e,"on")};var ba=Math.PI,wa=2*ba,Sa=ba/2,ka=1e-6,Ea=ka*ka,Aa=ba/180,Ca=180/ba,Na=Math.SQRT2,za=2,La=4;Zo.interpolateZoom=function(n,t){function e(n){var t=n*y;if(m){var e=Q(v),o=i/(za*h)*(e*nt(Na*t+v)-K(v));return[r+o*s,u+o*l,i*e/Q(Na*t+v)]}return[r+n*s,u+n*l,i*Math.exp(Na*t)]}var r=n[0],u=n[1],i=n[2],o=t[0],a=t[1],c=t[2],s=o-r,l=a-u,f=s*s+l*l,h=Math.sqrt(f),g=(c*c-i*i+La*f)/(2*i*za*h),p=(c*c-i*i-La*f)/(2*c*za*h),v=Math.log(Math.sqrt(g*g+1)-g),d=Math.log(Math.sqrt(p*p+1)-p),m=d-v,y=(m||Math.log(c/i))/Na;return e.duration=1e3*y,e},Zo.behavior.zoom=function(){function n(n){n.on(A,s).on(Ra+".zoom",f).on("dblclick.zoom",h).on(z,l)}function t(n){return[(n[0]-S.x)/S.k,(n[1]-S.y)/S.k]}function e(n){return[n[0]*S.k+S.x,n[1]*S.k+S.y]}function r(n){S.k=Math.max(E[0],Math.min(E[1],n))}function u(n,t){t=e(t),S.x+=n[0]-t[0],S.y+=n[1]-t[1]}function i(){_&&_.domain(x.range().map(function(n){return(n-S.x)/S.k}).map(x.invert)),w&&w.domain(b.range().map(function(n){return(n-S.y)/S.k}).map(b.invert))}function o(n){n({type:"zoomstart"})}function a(n){i(),n({type:"zoom",scale:S.k,translate:[S.x,S.y]})}function c(n){n({type:"zoomend"})}function s(){function n(){l=1,u(Zo.mouse(r),h),a(s)}function e(){f.on(C,null).on(N,null),g(l&&Zo.event.target===i),c(s)}var r=this,i=Zo.event.target,s=L.of(r,arguments),l=0,f=Zo.select(Wo).on(C,n).on(N,e),h=t(Zo.mouse(r)),g=I();H.call(r),o(s)}function l(){function n(){var n=Zo.touches(g);return h=S.k,n.forEach(function(n){n.identifier in v&&(v[n.identifier]=t(n))}),n}function e(){var t=Zo.event.target;Zo.select(t).on(M,i).on(_,f),b.push(t);for(var e=Zo.event.changedTouches,o=0,c=e.length;c>o;++o)v[e[o].identifier]=null;var s=n(),l=Date.now();if(1===s.length){if(500>l-m){var h=s[0],g=v[h.identifier];r(2*S.k),u(h,g),y(),a(p)}m=l}else if(s.length>1){var h=s[0],x=s[1],w=h[0]-x[0],k=h[1]-x[1];d=w*w+k*k}}function i(){for(var n,t,e,i,o=Zo.touches(g),c=0,s=o.length;s>c;++c,i=null)if(e=o[c],i=v[e.identifier]){if(t)break;n=e,t=i}if(i){var l=(l=e[0]-n[0])*l+(l=e[1]-n[1])*l,f=d&&Math.sqrt(l/d);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+i[0])/2,(t[1]+i[1])/2],r(f*h)}m=null,u(n,t),a(p)}function f(){if(Zo.event.touches.length){for(var t=Zo.event.changedTouches,e=0,r=t.length;r>e;++e)delete v[t[e].identifier];for(var u in v)return void n()}Zo.selectAll(b).on(x,null),w.on(A,s).on(z,l),k(),c(p)}var h,g=this,p=L.of(g,arguments),v={},d=0,x=".zoom-"+Zo.event.changedTouches[0].identifier,M="touchmove"+x,_="touchend"+x,b=[],w=Zo.select(g).on(A,null).on(z,e),k=I();H.call(g),e(),o(p)}function f(){var n=L.of(this,arguments);d?clearTimeout(d):(g=t(p=v||Zo.mouse(this)),H.call(this),o(n)),d=setTimeout(function(){d=null,c(n)},50),y(),r(Math.pow(2,.002*Ta())*S.k),u(p,g),a(n)}function h(){var n=L.of(this,arguments),e=Zo.mouse(this),i=t(e),s=Math.log(S.k)/Math.LN2;o(n),r(Math.pow(2,Zo.event.shiftKey?Math.ceil(s)-1:Math.floor(s)+1)),u(e,i),a(n),c(n)}var g,p,v,d,m,x,_,b,w,S={x:0,y:0,k:1},k=[960,500],E=qa,A="mousedown.zoom",C="mousemove.zoom",N="mouseup.zoom",z="touchstart.zoom",L=M(n,"zoomstart","zoom","zoomend");return n.event=function(n){n.each(function(){var n=L.of(this,arguments),t=S;Ss?Zo.select(this).transition().each("start.zoom",function(){S=this.__chart__||{x:0,y:0,k:1},o(n)}).tween("zoom:zoom",function(){var e=k[0],r=k[1],u=e/2,i=r/2,o=Zo.interpolateZoom([(u-S.x)/S.k,(i-S.y)/S.k,e/S.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=o(t),c=e/r[2];this.__chart__=S={x:u-r[0]*c,y:i-r[1]*c,k:c},a(n)}}).each("end.zoom",function(){c(n)}):(this.__chart__=S,o(n),a(n),c(n))})},n.translate=function(t){return arguments.length?(S={x:+t[0],y:+t[1],k:S.k},i(),n):[S.x,S.y]},n.scale=function(t){return arguments.length?(S={x:S.x,y:S.y,k:+t},i(),n):S.k},n.scaleExtent=function(t){return arguments.length?(E=null==t?qa:[+t[0],+t[1]],n):E},n.center=function(t){return arguments.length?(v=t&&[+t[0],+t[1]],n):v},n.size=function(t){return arguments.length?(k=t&&[+t[0],+t[1]],n):k},n.x=function(t){return arguments.length?(_=t,x=t.copy(),S={x:0,y:0,k:1},n):_},n.y=function(t){return arguments.length?(w=t,b=t.copy(),S={x:0,y:0,k:1},n):w},Zo.rebind(n,L,"on")};var Ta,qa=[0,1/0],Ra="onwheel"in $o?(Ta=function(){return-Zo.event.deltaY*(Zo.event.deltaMode?120:1)},"wheel"):"onmousewheel"in $o?(Ta=function(){return Zo.event.wheelDelta},"mousewheel"):(Ta=function(){return-Zo.event.detail},"MozMousePixelScroll");Zo.color=et,et.prototype.toString=function(){return this.rgb()+""},Zo.hsl=rt;var Da=rt.prototype=new et;Da.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new rt(this.h,this.s,this.l/n)},Da.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new rt(this.h,this.s,n*this.l)},Da.rgb=function(){return ut(this.h,this.s,this.l)},Zo.hcl=it;var Pa=it.prototype=new et;Pa.brighter=function(n){return new it(this.h,this.c,Math.min(100,this.l+Ua*(arguments.length?n:1)))},Pa.darker=function(n){return new it(this.h,this.c,Math.max(0,this.l-Ua*(arguments.length?n:1)))},Pa.rgb=function(){return ot(this.h,this.c,this.l).rgb()},Zo.lab=at;var Ua=18,ja=.95047,Ha=1,Fa=1.08883,Oa=at.prototype=new et;Oa.brighter=function(n){return new at(Math.min(100,this.l+Ua*(arguments.length?n:1)),this.a,this.b)},Oa.darker=function(n){return new at(Math.max(0,this.l-Ua*(arguments.length?n:1)),this.a,this.b)},Oa.rgb=function(){return ct(this.l,this.a,this.b)},Zo.rgb=gt;var Ya=gt.prototype=new et;Ya.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),new gt(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new gt(u,u,u)},Ya.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new gt(n*this.r,n*this.g,n*this.b)},Ya.hsl=function(){return yt(this.r,this.g,this.b)},Ya.toString=function(){return"#"+dt(this.r)+dt(this.g)+dt(this.b)};var Ia=Zo.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});Ia.forEach(function(n,t){Ia.set(n,pt(t))}),Zo.functor=bt,Zo.xhr=St(wt),Zo.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var o=kt(n,t,null==e?r:u(e),i);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:u(n)):e},o}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),c=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(l>=s)return o;if(u)return u=!1,i;var t=l;if(34===n.charCodeAt(t)){for(var e=t;e++<s;)if(34===n.charCodeAt(e)){if(34!==n.charCodeAt(e+1))break;++e}l=e+2;var r=n.charCodeAt(e+1);return 13===r?(u=!0,10===n.charCodeAt(e+2)&&++l):10===r&&(u=!0),n.substring(t+1,e).replace(/""/g,'"')}for(;s>l;){var r=n.charCodeAt(l++),a=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(l)&&(++l,++a);else if(r!==c)continue;return n.substring(t,l-a)}return n.substring(t)}for(var r,u,i={},o={},a=[],s=n.length,l=0,f=0;(r=e())!==o;){for(var h=[];r!==i&&r!==o;)h.push(r),r=e();(!t||(h=t(h,f++)))&&a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new h,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(o).join(n)].concat(t.map(function(t){return u.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},Zo.csv=Zo.dsv(",","text/csv"),Zo.tsv=Zo.dsv("	","text/tab-separated-values"),Zo.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=x().changedTouches),t)for(var r,u=0,i=t.length;i>u;++u)if((r=t[u]).identifier===e)return Z(n,r)};var Za,Va,Xa,$a,Ba,Wa=Wo[p(Wo,"requestAnimationFrame")]||function(n){setTimeout(n,17)};Zo.timer=function(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,f:!1,n:null};Va?Va.n=i:Za=i,Va=i,Xa||($a=clearTimeout($a),Xa=1,Wa(At))},Zo.timer.flush=function(){Ct(),Nt()},Zo.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var Ja=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Lt);Zo.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=Zo.round(n,zt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),Ja[8+e/3]};var Ga=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,Ka=Zo.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=Zo.round(n,zt(n,t))).toFixed(Math.max(0,Math.min(20,zt(n*(1+1e-15),t))))}}),Qa=Zo.time={},nc=Date;Rt.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){tc.setUTCDate.apply(this._,arguments)},setDay:function(){tc.setUTCDay.apply(this._,arguments)},setFullYear:function(){tc.setUTCFullYear.apply(this._,arguments)},setHours:function(){tc.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){tc.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){tc.setUTCMinutes.apply(this._,arguments)},setMonth:function(){tc.setUTCMonth.apply(this._,arguments)},setSeconds:function(){tc.setUTCSeconds.apply(this._,arguments)},setTime:function(){tc.setTime.apply(this._,arguments)}};var tc=Date.prototype;Qa.year=Dt(function(n){return n=Qa.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),Qa.years=Qa.year.range,Qa.years.utc=Qa.year.utc.range,Qa.day=Dt(function(n){var t=new nc(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),Qa.days=Qa.day.range,Qa.days.utc=Qa.day.utc.range,Qa.dayOfYear=function(n){var t=Qa.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=Qa[n]=Dt(function(n){return(n=Qa.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=Qa.year(n).getDay();return Math.floor((Qa.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});Qa[n+"s"]=e.range,Qa[n+"s"].utc=e.utc.range,Qa[n+"OfYear"]=function(n){var e=Qa.year(n).getDay();return Math.floor((Qa.dayOfYear(n)+(e+t)%7)/7)}}),Qa.week=Qa.sunday,Qa.weeks=Qa.sunday.range,Qa.weeks.utc=Qa.sunday.utc.range,Qa.weekOfYear=Qa.sundayOfYear;var ec={"-":"",_:" ",0:"0"},rc=/^\s*\d+/,uc=/^%/;Zo.locale=function(n){return{numberFormat:Tt(n),timeFormat:Ut(n)}};var ic=Zo.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});Zo.format=ic.numberFormat,Zo.geo={},ue.prototype={s:0,t:0,add:function(n){ie(n,this.t,oc),ie(oc.s,this.s,this),this.s?this.t+=oc.t:this.s=oc.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var oc=new ue;Zo.geo.stream=function(n,t){n&&ac.hasOwnProperty(n.type)?ac[n.type](n,t):oe(n,t)};var ac={Feature:function(n,t){oe(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++r<u;)oe(e[r].geometry,t)}},cc={Sphere:function(n,t){t.sphere()},Point:function(n,t){n=n.coordinates,t.point(n[0],n[1],n[2])},MultiPoint:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)n=e[r],t.point(n[0],n[1],n[2])},LineString:function(n,t){ae(n.coordinates,t,0)},MultiLineString:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)ae(e[r],t,0)},Polygon:function(n,t){ce(n.coordinates,t)},MultiPolygon:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)ce(e[r],t)},GeometryCollection:function(n,t){for(var e=n.geometries,r=-1,u=e.length;++r<u;)oe(e[r],t)}};Zo.geo.area=function(n){return sc=0,Zo.geo.stream(n,fc),sc};var sc,lc=new ue,fc={sphere:function(){sc+=4*ba},point:v,lineStart:v,lineEnd:v,polygonStart:function(){lc.reset(),fc.lineStart=se},polygonEnd:function(){var n=2*lc;sc+=0>n?4*ba+n:n,fc.lineStart=fc.lineEnd=fc.point=v}};Zo.geo.bounds=function(){function n(n,t){x.push(M=[l=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=le([t*Aa,e*Aa]);if(m){var u=he(m,r),i=[u[1],-u[0],0],o=he(i,u);ve(o),o=de(o);var c=t-p,s=c>0?1:-1,v=o[0]*Ca*s,d=ua(c)>180;if(d^(v>s*p&&s*t>v)){var y=o[1]*Ca;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>s*p&&s*t>v)){var y=-o[1]*Ca;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t):h>=l?(l>t&&(l=t),t>h&&(h=t)):t>p?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t)}else n(t,e);m=r,p=t}function e(){_.point=t}function r(){M[0]=l,M[1]=h,_.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=ua(r)>180?r+(r>0?360:-360):r}else v=n,d=e;fc.point(n,e),t(n,e)}function i(){fc.lineStart()}function o(){u(v,d),fc.lineEnd(),ua(y)>ka&&(l=-(h=180)),M[0]=l,M[1]=h,m=null}function a(n,t){return(t-=n)<0?t+360:t}function c(n,t){return n[0]-t[0]}function s(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:n<t[0]||t[1]<n}var l,f,h,g,p,v,d,m,y,x,M,_={point:n,lineStart:e,lineEnd:r,polygonStart:function(){_.point=u,_.lineStart=i,_.lineEnd=o,y=0,fc.polygonStart()},polygonEnd:function(){fc.polygonEnd(),_.point=n,_.lineStart=e,_.lineEnd=r,0>lc?(l=-(h=180),f=-(g=90)):y>ka?g=90:-ka>y&&(f=-90),M[0]=l,M[1]=h}};return function(n){g=h=-(l=f=1/0),x=[],Zo.geo.stream(n,_);var t=x.length;if(t){x.sort(c);for(var e,r=1,u=x[0],i=[u];t>r;++r)e=x[r],s(e[0],u)||s(e[1],u)?(a(u[0],e[1])>a(u[0],u[1])&&(u[1]=e[1]),a(e[0],u[1])>a(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);
+for(var o,e,p=-1/0,t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(o=a(u[1],e[0]))>p&&(p=o,l=e[0],h=u[1])}return x=M=null,1/0===l||1/0===f?[[0/0,0/0],[0/0,0/0]]:[[l,f],[h,g]]}}(),Zo.geo.centroid=function(n){hc=gc=pc=vc=dc=mc=yc=xc=Mc=_c=bc=0,Zo.geo.stream(n,wc);var t=Mc,e=_c,r=bc,u=t*t+e*e+r*r;return Ea>u&&(t=mc,e=yc,r=xc,ka>gc&&(t=pc,e=vc,r=dc),u=t*t+e*e+r*r,Ea>u)?[0/0,0/0]:[Math.atan2(e,t)*Ca,G(r/Math.sqrt(u))*Ca]};var hc,gc,pc,vc,dc,mc,yc,xc,Mc,_c,bc,wc={sphere:v,point:ye,lineStart:Me,lineEnd:_e,polygonStart:function(){wc.lineStart=be},polygonEnd:function(){wc.lineStart=Me}},Sc=Ae(we,Te,Re,[-ba,-ba/2]),kc=1e9;Zo.geo.clipExtent=function(){var n,t,e,r,u,i,o={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(a){return arguments.length?(i=Ue(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),u&&(u.valid=!1,u=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(Zo.geo.conicEqualArea=function(){return He(Fe)}).raw=Fe,Zo.geo.albers=function(){return Zo.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},Zo.geo.albersUsa=function(){function n(n){var i=n[0],o=n[1];return t=null,e(i,o),t||(r(i,o),t)||u(i,o),t}var t,e,r,u,i=Zo.geo.albers(),o=Zo.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=Zo.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),c={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?o:u>=.166&&.234>u&&r>=-.214&&-.115>r?a:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),o.precision(t),a.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),o.scale(.35*t),a.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var s=i.scale(),l=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[l-.455*s,f-.238*s],[l+.455*s,f+.238*s]]).stream(c).point,r=o.translate([l-.307*s,f+.201*s]).clipExtent([[l-.425*s+ka,f+.12*s+ka],[l-.214*s-ka,f+.234*s-ka]]).stream(c).point,u=a.translate([l-.205*s,f+.212*s]).clipExtent([[l-.214*s+ka,f+.166*s+ka],[l-.115*s-ka,f+.234*s-ka]]).stream(c).point,n},n.scale(1070)};var Ec,Ac,Cc,Nc,zc,Lc,Tc={point:v,lineStart:v,lineEnd:v,polygonStart:function(){Ac=0,Tc.lineStart=Oe},polygonEnd:function(){Tc.lineStart=Tc.lineEnd=Tc.point=v,Ec+=ua(Ac/2)}},qc={point:Ye,lineStart:v,lineEnd:v,polygonStart:v,polygonEnd:v},Rc={point:Ve,lineStart:Xe,lineEnd:$e,polygonStart:function(){Rc.lineStart=Be},polygonEnd:function(){Rc.point=Ve,Rc.lineStart=Xe,Rc.lineEnd=$e}};Zo.geo.path=function(){function n(n){return n&&("function"==typeof a&&i.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=u(i)),Zo.geo.stream(n,o)),i.result()}function t(){return o=null,n}var e,r,u,i,o,a=4.5;return n.area=function(n){return Ec=0,Zo.geo.stream(n,u(Tc)),Ec},n.centroid=function(n){return pc=vc=dc=mc=yc=xc=Mc=_c=bc=0,Zo.geo.stream(n,u(Rc)),bc?[Mc/bc,_c/bc]:xc?[mc/xc,yc/xc]:dc?[pc/dc,vc/dc]:[0/0,0/0]},n.bounds=function(n){return zc=Lc=-(Cc=Nc=1/0),Zo.geo.stream(n,u(qc)),[[Cc,Nc],[zc,Lc]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||Ge(n):wt,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Ie:new We(n),"function"!=typeof a&&i.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(i.pointRadius(+t),+t),n):a},n.projection(Zo.geo.albersUsa()).context(null)},Zo.geo.transform=function(n){return{stream:function(t){var e=new Ke(t);for(var r in n)e[r]=n[r];return e}}},Ke.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},Zo.geo.projection=nr,Zo.geo.projectionMutator=tr,(Zo.geo.equirectangular=function(){return nr(rr)}).raw=rr.invert=rr,Zo.geo.rotation=function(n){function t(t){return t=n(t[0]*Aa,t[1]*Aa),t[0]*=Ca,t[1]*=Ca,t}return n=ir(n[0]%360*Aa,n[1]*Aa,n.length>2?n[2]*Aa:0),t.invert=function(t){return t=n.invert(t[0]*Aa,t[1]*Aa),t[0]*=Ca,t[1]*=Ca,t},t},ur.invert=rr,Zo.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=ir(-n[0]*Aa,-n[1]*Aa,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=Ca,n[1]*=Ca}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=sr((t=+r)*Aa,u*Aa),n):t},n.precision=function(r){return arguments.length?(e=sr(t*Aa,(u=+r)*Aa),n):u},n.angle(90)},Zo.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Aa,u=n[1]*Aa,i=t[1]*Aa,o=Math.sin(r),a=Math.cos(r),c=Math.sin(u),s=Math.cos(u),l=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*o)*e+(e=s*l-c*f*a)*e),c*l+s*f*a)},Zo.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return Zo.range(Math.ceil(i/d)*d,u,d).map(h).concat(Zo.range(Math.ceil(s/m)*m,c,m).map(g)).concat(Zo.range(Math.ceil(r/p)*p,e,p).filter(function(n){return ua(n%d)>ka}).map(l)).concat(Zo.range(Math.ceil(a/v)*v,o,v).filter(function(n){return ua(n%m)>ka}).map(f))}var e,r,u,i,o,a,c,s,l,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(c).slice(1),h(u).reverse().slice(1),g(s).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],s=+t[0][1],c=+t[1][1],i>u&&(t=i,i=u,u=t),s>c&&(t=s,s=c,c=t),n.precision(y)):[[i,s],[u,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(y)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,l=fr(a,o,90),f=hr(r,e,y),h=fr(s,c,90),g=hr(i,u,y),n):y},n.majorExtent([[-180,-90+ka],[180,90-ka]]).minorExtent([[-180,-80-ka],[180,80+ka]])},Zo.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=gr,u=pr;return n.distance=function(){return Zo.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},Zo.geo.interpolate=function(n,t){return vr(n[0]*Aa,n[1]*Aa,t[0]*Aa,t[1]*Aa)},Zo.geo.length=function(n){return Dc=0,Zo.geo.stream(n,Pc),Dc};var Dc,Pc={sphere:v,point:v,lineStart:dr,lineEnd:v,polygonStart:v,polygonEnd:v},Uc=mr(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(Zo.geo.azimuthalEqualArea=function(){return nr(Uc)}).raw=Uc;var jc=mr(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},wt);(Zo.geo.azimuthalEquidistant=function(){return nr(jc)}).raw=jc,(Zo.geo.conicConformal=function(){return He(yr)}).raw=yr,(Zo.geo.conicEquidistant=function(){return He(xr)}).raw=xr;var Hc=mr(function(n){return 1/n},Math.atan);(Zo.geo.gnomonic=function(){return nr(Hc)}).raw=Hc,Mr.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Sa]},(Zo.geo.mercator=function(){return _r(Mr)}).raw=Mr;var Fc=mr(function(){return 1},Math.asin);(Zo.geo.orthographic=function(){return nr(Fc)}).raw=Fc;var Oc=mr(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(Zo.geo.stereographic=function(){return nr(Oc)}).raw=Oc,br.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Sa]},(Zo.geo.transverseMercator=function(){var n=_r(br),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=br,Zo.geom={},Zo.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=bt(e),i=bt(r),o=n.length,a=[],c=[];for(t=0;o>t;t++)a.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(a.sort(Er),t=0;o>t;t++)c.push([a[t][0],-a[t][1]]);var s=kr(a),l=kr(c),f=l[0]===s[0],h=l[l.length-1]===s[s.length-1],g=[];for(t=s.length-1;t>=0;--t)g.push(n[a[s[t]][2]]);for(t=+f;t<l.length-h;++t)g.push(n[a[l[t]][2]]);return g}var e=wr,r=Sr;return arguments.length?t(n):(t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t)},Zo.geom.polygon=function(n){return sa(n,Yc),n};var Yc=Zo.geom.polygon.prototype=[];Yc.area=function(){for(var n,t=-1,e=this.length,r=this[e-1],u=0;++t<e;)n=r,r=this[t],u+=n[1]*r[0]-n[0]*r[1];return.5*u},Yc.centroid=function(n){var t,e,r=-1,u=this.length,i=0,o=0,a=this[u-1];for(arguments.length||(n=-1/(6*this.area()));++r<u;)t=a,a=this[r],e=t[0]*a[1]-a[0]*t[1],i+=(t[0]+a[0])*e,o+=(t[1]+a[1])*e;return[i*n,o*n]},Yc.clip=function(n){for(var t,e,r,u,i,o,a=Nr(n),c=-1,s=this.length-Nr(this),l=this[s-1];++c<s;){for(t=n.slice(),n.length=0,u=this[c],i=t[(r=t.length-a)-1],e=-1;++e<r;)o=t[e],Ar(o,l,u)?(Ar(i,l,u)||n.push(Cr(i,o,l,u)),n.push(o)):Ar(i,l,u)&&n.push(Cr(i,o,l,u)),i=o;a&&n.push(n[0]),l=u}return n};var Ic,Zc,Vc,Xc,$c,Bc=[],Wc=[];Ur.prototype.prepare=function(){for(var n,t=this.edges,e=t.length;e--;)n=t[e].edge,n.b&&n.a||t.splice(e,1);return t.sort(Hr),t.length},Wr.prototype={start:function(){return this.edge.l===this.site?this.edge.a:this.edge.b},end:function(){return this.edge.l===this.site?this.edge.b:this.edge.a}},Jr.prototype={insert:function(n,t){var e,r,u;if(n){if(t.P=n,t.N=n.N,n.N&&(n.N.P=t),n.N=t,n.R){for(n=n.R;n.L;)n=n.L;n.L=t}else n.R=t;e=n}else this._?(n=nu(this._),t.P=null,t.N=n,n.P=n.L=t,e=n):(t.P=t.N=null,this._=t,e=null);for(t.L=t.R=null,t.U=e,t.C=!0,n=t;e&&e.C;)r=e.U,e===r.L?(u=r.R,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.R&&(Kr(this,e),n=e,e=n.U),e.C=!1,r.C=!0,Qr(this,r))):(u=r.L,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.L&&(Qr(this,e),n=e,e=n.U),e.C=!1,r.C=!0,Kr(this,r))),e=n.U;this._.C=!1},remove:function(n){n.N&&(n.N.P=n.P),n.P&&(n.P.N=n.N),n.N=n.P=null;var t,e,r,u=n.U,i=n.L,o=n.R;if(e=i?o?nu(o):i:o,u?u.L===n?u.L=e:u.R=e:this._=e,i&&o?(r=e.C,e.C=n.C,e.L=i,i.U=e,e!==o?(u=e.U,e.U=n.U,n=e.R,u.L=n,e.R=o,o.U=e):(e.U=u,u=e,n=e.R)):(r=n.C,n=e),n&&(n.U=u),!r){if(n&&n.C)return n.C=!1,void 0;do{if(n===this._)break;if(n===u.L){if(t=u.R,t.C&&(t.C=!1,u.C=!0,Kr(this,u),t=u.R),t.L&&t.L.C||t.R&&t.R.C){t.R&&t.R.C||(t.L.C=!1,t.C=!0,Qr(this,t),t=u.R),t.C=u.C,u.C=t.R.C=!1,Kr(this,u),n=this._;break}}else if(t=u.L,t.C&&(t.C=!1,u.C=!0,Qr(this,u),t=u.L),t.L&&t.L.C||t.R&&t.R.C){t.L&&t.L.C||(t.R.C=!1,t.C=!0,Kr(this,t),t=u.L),t.C=u.C,u.C=t.L.C=!1,Qr(this,u),n=this._;break}t.C=!0,n=u,u=u.U}while(!n.C);n&&(n.C=!1)}}},Zo.geom.voronoi=function(n){function t(n){var t=new Array(n.length),r=a[0][0],u=a[0][1],i=a[1][0],o=a[1][1];return tu(e(n),a).cells.forEach(function(e,a){var c=e.edges,s=e.site,l=t[a]=c.length?c.map(function(n){var t=n.start();return[t.x,t.y]}):s.x>=r&&s.x<=i&&s.y>=u&&s.y<=o?[[r,o],[i,o],[i,u],[r,u]]:[];l.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/ka)*ka,y:Math.round(o(n,t)/ka)*ka,i:t}})}var r=wr,u=Sr,i=r,o=u,a=Jc;return n?t(n):(t.links=function(n){return tu(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return tu(e(n)).cells.forEach(function(e,r){for(var u,i,o=e.site,a=e.edges.sort(Hr),c=-1,s=a.length,l=a[s-1].edge,f=l.l===o?l.r:l.l;++c<s;)u=l,i=f,l=a[c].edge,f=l.l===o?l.r:l.l,r<i.i&&r<f.i&&ru(o,i,f)<0&&t.push([n[r],n[i.i],n[f.i]])}),t},t.x=function(n){return arguments.length?(i=bt(r=n),t):r},t.y=function(n){return arguments.length?(o=bt(u=n),t):u},t.clipExtent=function(n){return arguments.length?(a=null==n?Jc:n,t):a===Jc?null:a},t.size=function(n){return arguments.length?t.clipExtent(n&&[[0,0],n]):a===Jc?null:a&&a[1]},t)};var Jc=[[-1e6,-1e6],[1e6,1e6]];Zo.geom.delaunay=function(n){return Zo.geom.voronoi().triangles(n)},Zo.geom.quadtree=function(n,t,e,r,u){function i(n){function i(n,t,e,r,u,i,o,a){if(!isNaN(e)&&!isNaN(r))if(n.leaf){var c=n.x,l=n.y;if(null!=c)if(ua(c-e)+ua(l-r)<.01)s(n,t,e,r,u,i,o,a);else{var f=n.point;n.x=n.y=n.point=null,s(n,f,c,l,u,i,o,a),s(n,t,e,r,u,i,o,a)}else n.x=e,n.y=r,n.point=t}else s(n,t,e,r,u,i,o,a)}function s(n,t,e,r,u,o,a,c){var s=.5*(u+a),l=.5*(o+c),f=e>=s,h=r>=l,g=(h<<1)+f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=ou()),f?u=s:a=s,h?o=l:c=l,i(n,t,e,r,u,o,a,c)}var l,f,h,g,p,v,d,m,y,x=bt(a),M=bt(c);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,o)for(g=0;p>g;++g)l=n[g],l.x<v&&(v=l.x),l.y<d&&(d=l.y),l.x>m&&(m=l.x),l.y>y&&(y=l.y),f.push(l.x),h.push(l.y);else for(g=0;p>g;++g){var _=+x(l=n[g],g),b=+M(l,g);v>_&&(v=_),d>b&&(d=b),_>m&&(m=_),b>y&&(y=b),f.push(_),h.push(b)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=ou();if(k.add=function(n){i(k,n,+x(n,++g),+M(n,g),v,d,m,y)},k.visit=function(n){au(n,k,v,d,m,y)},g=-1,null==t){for(;++g<p;)i(k,n[g],f[g],h[g],v,d,m,y);--g}else n.forEach(k.add);return f=h=n=l=null,k}var o,a=wr,c=Sr;return(o=arguments.length)?(a=uu,c=iu,3===o&&(u=e,r=t,e=t=0),i(n)):(i.x=function(n){return arguments.length?(a=n,i):a},i.y=function(n){return arguments.length?(c=n,i):c},i.extent=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=+n[0][0],e=+n[0][1],r=+n[1][0],u=+n[1][1]),i):null==t?null:[[t,e],[r,u]]},i.size=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=e=0,r=+n[0],u=+n[1]),i):null==t?null:[r-t,u-e]},i)},Zo.interpolateRgb=cu,Zo.interpolateObject=su,Zo.interpolateNumber=lu,Zo.interpolateString=fu;var Gc=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,Kc=new RegExp(Gc.source,"g");Zo.interpolate=hu,Zo.interpolators=[function(n,t){var e=typeof t;return("string"===e?Ia.has(t)||/^(#|rgb\(|hsl\()/.test(t)?cu:fu:t instanceof et?cu:Array.isArray(t)?gu:"object"===e&&isNaN(t)?su:lu)(n,t)}],Zo.interpolateArray=gu;var Qc=function(){return wt},ns=Zo.map({linear:Qc,poly:Mu,quad:function(){return mu},cubic:function(){return yu},sin:function(){return _u},exp:function(){return bu},circle:function(){return wu},elastic:Su,back:ku,bounce:function(){return Eu}}),ts=Zo.map({"in":wt,out:vu,"in-out":du,"out-in":function(n){return du(vu(n))}});Zo.ease=function(n){var t=n.indexOf("-"),e=t>=0?n.substring(0,t):n,r=t>=0?n.substring(t+1):"in";return e=ns.get(e)||Qc,r=ts.get(r)||wt,pu(r(e.apply(null,Vo.call(arguments,1))))},Zo.interpolateHcl=Au,Zo.interpolateHsl=Cu,Zo.interpolateLab=Nu,Zo.interpolateRound=zu,Zo.transform=function(n){var t=$o.createElementNS(Zo.ns.prefix.svg,"g");return(Zo.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new Lu(e?e.matrix:es)})(n)},Lu.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var es={a:1,b:0,c:0,d:1,e:0,f:0};Zo.interpolateTransform=Du,Zo.layout={},Zo.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e<r;)t.push(ju(n[e]));return t}},Zo.layout.chord=function(){function n(){var n,s,f,h,g,p={},v=[],d=Zo.range(i),m=[];for(e=[],r=[],n=0,h=-1;++h<i;){for(s=0,g=-1;++g<i;)s+=u[h][g];v.push(s),m.push(Zo.range(i)),n+=s}for(o&&d.sort(function(n,t){return o(v[n],v[t])}),a&&m.forEach(function(n,t){n.sort(function(n,e){return a(u[t][n],u[t][e])})}),n=(wa-l*i)/n,s=0,h=-1;++h<i;){for(f=s,g=-1;++g<i;){var y=d[h],x=m[y][g],M=u[y][x],_=s,b=s+=M*n;p[y+"-"+x]={index:y,subindex:x,startAngle:_,endAngle:b,value:M}}r[y]={index:y,startAngle:f,endAngle:s,value:(s-f)/n},s+=l}for(h=-1;++h<i;)for(g=h-1;++g<i;){var w=p[h+"-"+g],S=p[g+"-"+h];(w.value||S.value)&&e.push(w.value<S.value?{source:S,target:w}:{source:w,target:S})}c&&t()}function t(){e.sort(function(n,t){return c((n.source.value+n.target.value)/2,(t.source.value+t.target.value)/2)})}var e,r,u,i,o,a,c,s={},l=0;return s.matrix=function(n){return arguments.length?(i=(u=n)&&u.length,e=r=null,s):u},s.padding=function(n){return arguments.length?(l=n,e=r=null,s):l},s.sortGroups=function(n){return arguments.length?(o=n,e=r=null,s):o},s.sortSubgroups=function(n){return arguments.length?(a=n,e=null,s):a},s.sortChords=function(n){return arguments.length?(c=n,e&&t(),s):c},s.chords=function(){return e||n(),e},s.groups=function(){return r||n(),r},s},Zo.layout.force=function(){function n(n){return function(t,e,r,u){if(t.point!==n){var i=t.cx-n.x,o=t.cy-n.y,a=u-e,c=i*i+o*o;if(c>a*a/d){if(p>c){var s=t.charge/c;n.px-=i*s,n.py-=o*s}return!0}if(t.point&&c&&p>c){var s=t.pointCharge/c;n.px-=i*s,n.py-=o*s}}return!t.charge}}function t(n){n.px=Zo.event.x,n.py=Zo.event.y,a.resume()}var e,r,u,i,o,a={},c=Zo.dispatch("start","tick","end"),s=[1,1],l=.9,f=rs,h=us,g=-30,p=is,v=.1,d=.64,m=[],y=[];return a.tick=function(){if((r*=.99)<.005)return c.end({type:"end",alpha:r=0}),!0;var t,e,a,f,h,p,d,x,M,_=m.length,b=y.length;for(e=0;b>e;++e)a=y[e],f=a.source,h=a.target,x=h.x-f.x,M=h.y-f.y,(p=x*x+M*M)&&(p=r*i[e]*((p=Math.sqrt(p))-u[e])/p,x*=p,M*=p,h.x-=x*(d=f.weight/(h.weight+f.weight)),h.y-=M*d,f.x+=x*(d=1-d),f.y+=M*d);if((d=r*v)&&(x=s[0]/2,M=s[1]/2,e=-1,d))for(;++e<_;)a=m[e],a.x+=(x-a.x)*d,a.y+=(M-a.y)*d;if(g)for(Vu(t=Zo.geom.quadtree(m),r,o),e=-1;++e<_;)(a=m[e]).fixed||t.visit(n(a));for(e=-1;++e<_;)a=m[e],a.fixed?(a.x=a.px,a.y=a.py):(a.x-=(a.px-(a.px=a.x))*l,a.y-=(a.py-(a.py=a.y))*l);c.tick({type:"tick",alpha:r})},a.nodes=function(n){return arguments.length?(m=n,a):m},a.links=function(n){return arguments.length?(y=n,a):y},a.size=function(n){return arguments.length?(s=n,a):s},a.linkDistance=function(n){return arguments.length?(f="function"==typeof n?n:+n,a):f},a.distance=a.linkDistance,a.linkStrength=function(n){return arguments.length?(h="function"==typeof n?n:+n,a):h},a.friction=function(n){return arguments.length?(l=+n,a):l},a.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,a):g},a.chargeDistance=function(n){return arguments.length?(p=n*n,a):Math.sqrt(p)},a.gravity=function(n){return arguments.length?(v=+n,a):v},a.theta=function(n){return arguments.length?(d=n*n,a):Math.sqrt(d)},a.alpha=function(n){return arguments.length?(n=+n,r?r=n>0?n:0:n>0&&(c.start({type:"start",alpha:r=n}),Zo.timer(a.tick)),a):r},a.start=function(){function n(n,r){if(!e){for(e=new Array(c),a=0;c>a;++a)e[a]=[];for(a=0;s>a;++a){var u=y[a];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var i,o=e[t],a=-1,s=o.length;++a<s;)if(!isNaN(i=o[a][n]))return i;return Math.random()*r}var t,e,r,c=m.length,l=y.length,p=s[0],v=s[1];for(t=0;c>t;++t)(r=m[t]).index=t,r.weight=0;for(t=0;l>t;++t)r=y[t],"number"==typeof r.source&&(r.source=m[r.source]),"number"==typeof r.target&&(r.target=m[r.target]),++r.source.weight,++r.target.weight;for(t=0;c>t;++t)r=m[t],isNaN(r.x)&&(r.x=n("x",p)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof f)for(t=0;l>t;++t)u[t]=+f.call(this,y[t],t);else for(t=0;l>t;++t)u[t]=f;if(i=[],"function"==typeof h)for(t=0;l>t;++t)i[t]=+h.call(this,y[t],t);else for(t=0;l>t;++t)i[t]=h;if(o=[],"function"==typeof g)for(t=0;c>t;++t)o[t]=+g.call(this,m[t],t);else for(t=0;c>t;++t)o[t]=g;return a.resume()},a.resume=function(){return a.alpha(.1)},a.stop=function(){return a.alpha(0)},a.drag=function(){return e||(e=Zo.behavior.drag().origin(wt).on("dragstart.force",Ou).on("drag.force",t).on("dragend.force",Yu)),arguments.length?(this.on("mouseover.force",Iu).on("mouseout.force",Zu).call(e),void 0):e},Zo.rebind(a,c,"on")};var rs=20,us=1,is=1/0;Zo.layout.hierarchy=function(){function n(u){var i,o=[u],a=[];for(u.depth=0;null!=(i=o.pop());)if(a.push(i),(s=e.call(n,i,i.depth))&&(c=s.length)){for(var c,s,l;--c>=0;)o.push(l=s[c]),l.parent=i,l.depth=i.depth+1;r&&(i.value=0),i.children=s}else r&&(i.value=+r.call(n,i,i.depth)||0),delete i.children;return Bu(u,function(n){var e,u;t&&(e=n.children)&&e.sort(t),r&&(u=n.parent)&&(u.value+=n.value)}),a}var t=Gu,e=Wu,r=Ju;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&($u(t,function(n){n.children&&(n.value=0)}),Bu(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},Zo.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(o=i.length)){var o,a,c,s=-1;for(r=t.value?r/t.value:0;++s<o;)n(a=i[s],e,c=a.value*r,u),e+=c}}function t(n){var e=n.children,r=0;if(e&&(u=e.length))for(var u,i=-1;++i<u;)r=Math.max(r,t(e[i]));return 1+r}function e(e,i){var o=r.call(this,e,i);return n(o[0],0,u[0],u[1]/t(o[0])),o}var r=Zo.layout.hierarchy(),u=[1,1];return e.size=function(n){return arguments.length?(u=n,e):u},Xu(e,r)},Zo.layout.pie=function(){function n(i){var o=i.map(function(e,r){return+t.call(n,e,r)}),a=+("function"==typeof r?r.apply(this,arguments):r),c=(("function"==typeof u?u.apply(this,arguments):u)-a)/Zo.sum(o),s=Zo.range(i.length);null!=e&&s.sort(e===os?function(n,t){return o[t]-o[n]}:function(n,t){return e(i[n],i[t])});var l=[];return s.forEach(function(n){var t;l[n]={data:i[n],value:t=o[n],startAngle:a,endAngle:a+=t*c}}),l}var t=Number,e=os,r=0,u=wa;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n};var os={};Zo.layout.stack=function(){function n(a,c){var s=a.map(function(e,r){return t.call(n,e,r)}),l=s.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),o.call(n,t,e)]})}),f=e.call(n,l,c);s=Zo.permute(s,f),l=Zo.permute(l,f);var h,g,p,v=r.call(n,l,c),d=s.length,m=s[0].length;for(g=0;m>g;++g)for(u.call(n,s[0][g],p=v[g],l[0][g][1]),h=1;d>h;++h)u.call(n,s[h][g],p+=l[h-1][g][1],l[h][g][1]);return a}var t=wt,e=ei,r=ri,u=ti,i=Qu,o=ni;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:as.get(t)||ei,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:cs.get(t)||ri,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(u=t,n):u},n};var as=Zo.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(ui),i=n.map(ii),o=Zo.range(r).sort(function(n,t){return u[n]-u[t]}),a=0,c=0,s=[],l=[];for(t=0;r>t;++t)e=o[t],c>a?(a+=i[e],s.push(e)):(c+=i[e],l.push(e));return l.reverse().concat(s)},reverse:function(n){return Zo.range(n.length).reverse()},"default":ei}),cs=Zo.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,o=[],a=0,c=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;i>e;++e)c[e]=(a-o[e])/2;return c},wiggle:function(n){var t,e,r,u,i,o,a,c,s,l=n.length,f=n[0],h=f.length,g=[];for(g[0]=c=s=0,e=1;h>e;++e){for(t=0,u=0;l>t;++t)u+=n[t][e][1];for(t=0,i=0,a=f[e][0]-f[e-1][0];l>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;i+=o*n[t][e][1]}g[e]=c-=u?i/u*a:0,s>c&&(s=c)}for(e=0;h>e;++e)g[e]-=s;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,o=1/u,a=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=o}for(e=0;i>e;++e)a[e]=0;return a},zero:ri});Zo.layout.histogram=function(){function n(n,i){for(var o,a,c=[],s=n.map(e,this),l=r.call(this,s,i),f=u.call(this,l,s,i),i=-1,h=s.length,g=f.length-1,p=t?1:1/h;++i<g;)o=c[i]=[],o.dx=f[i+1]-(o.x=f[i]),o.y=0;if(g>0)for(i=-1;++i<h;)a=s[i],a>=l[0]&&a<=l[1]&&(o=c[Zo.bisect(f,a,1,g)-1],o.y+=p,o.push(n[i]));return c}var t=!0,e=Number,r=si,u=ai;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=bt(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return ci(n,t)}:bt(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},Zo.layout.pack=function(){function n(n,i){var o=e.call(this,n,i),a=o[0],c=u[0],s=u[1],l=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,Bu(a,function(n){n.r=+l(n.value)}),Bu(a,pi),r){var f=r*(t?1:Math.max(2*a.r/c,2*a.r/s))/2;Bu(a,function(n){n.r+=f}),Bu(a,pi),Bu(a,function(n){n.r-=f})}return mi(a,c/2,s/2,t?1:1/Math.max(2*a.r/c,2*a.r/s)),o}var t,e=Zo.layout.hierarchy().sort(li),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},Xu(n,e)},Zo.layout.tree=function(){function n(n,u){var l=o.call(this,n,u),f=l[0],h=t(f);if(Bu(h,e),h.parent.m=-h.z,$u(h,r),s)$u(f,i);else{var g=f,p=f,v=f;$u(f,function(n){n.x<g.x&&(g=n),n.x>p.x&&(p=n),n.depth>v.depth&&(v=n)});var d=a(g,p)/2-g.x,m=c[0]/(p.x+a(p,g)/2+d),y=c[1]/(v.depth||1);$u(f,function(n){n.x=(n.x+d)*m,n.y=n.depth*y})}return l}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var u,i=t.children,o=0,a=i.length;a>o;++o)r.push((i[o]=u={_:i[o],parent:t,children:(u=i[o].children)&&u.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:o}).a=u);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){wi(n);var i=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+a(n._,r._),n.m=n.z-i):n.z=i}else r&&(n.z=r.z+a(n._,r._));n.parent.A=u(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function u(n,t,e){if(t){for(var r,u=n,i=n,o=t,c=u.parent.children[0],s=u.m,l=i.m,f=o.m,h=c.m;o=_i(o),u=Mi(u),o&&u;)c=Mi(c),i=_i(i),i.a=n,r=o.z+f-u.z-s+a(o._,u._),r>0&&(bi(Si(o,n,e),n,r),s+=r,l+=r),f+=o.m,s+=u.m,h+=c.m,l+=i.m;o&&!_i(i)&&(i.t=o,i.m+=f-l),u&&!Mi(c)&&(c.t=u,c.m+=s-h,e=n)}return e}function i(n){n.x*=c[0],n.y=n.depth*c[1]}var o=Zo.layout.hierarchy().sort(null).value(null),a=xi,c=[1,1],s=null;return n.separation=function(t){return arguments.length?(a=t,n):a},n.size=function(t){return arguments.length?(s=null==(c=t)?i:null,n):s?null:c},n.nodeSize=function(t){return arguments.length?(s=null==(c=t)?null:i,n):s?c:null},Xu(n,o)},Zo.layout.cluster=function(){function n(n,i){var o,a=t.call(this,n,i),c=a[0],s=0;Bu(c,function(n){var t=n.children;t&&t.length?(n.x=Ei(t),n.y=ki(t)):(n.x=o?s+=e(n,o):0,n.y=0,o=n)});var l=Ai(c),f=Ci(c),h=l.x-e(l,f)/2,g=f.x+e(f,l)/2;return Bu(c,u?function(n){n.x=(n.x-c.x)*r[0],n.y=(c.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(c.y?n.y/c.y:1))*r[1]}),a}var t=Zo.layout.hierarchy().sort(null).value(null),e=xi,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Xu(n,t)},Zo.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++u<i;)r=(e=n[u]).value*(0>t?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var o,a,c,s=f(e),l=[],h=i.slice(),p=1/0,v="slice"===g?s.dx:"dice"===g?s.dy:"slice-dice"===g?1&e.depth?s.dy:s.dx:Math.min(s.dx,s.dy);for(n(h,s.dx*s.dy/e.value),l.area=0;(c=h.length)>0;)l.push(o=h[c-1]),l.area+=o.area,"squarify"!==g||(a=r(l,v))<=p?(h.pop(),p=a):(l.area-=l.pop().area,u(l,v,s,!1),v=Math.min(s.dx,s.dy),l.length=l.area=0,p=1/0);l.length&&(u(l,v,s,!0),l.length=l.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,o=f(t),a=r.slice(),c=[];for(n(a,o.dx*o.dy/t.value),c.area=0;i=a.pop();)c.push(i),c.area+=i.area,null!=i.z&&(u(c,i.z?o.dx:o.dy,o,!a.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,o=-1,a=n.length;++o<a;)(e=n[o].area)&&(i>e&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,o=n.length,a=e.x,s=e.y,l=t?c(n.area/t):0;if(t==e.dx){for((r||l>e.dy)&&(l=e.dy);++i<o;)u=n[i],u.x=a,u.y=s,u.dy=l,a+=u.dx=Math.min(e.x+e.dx-a,l?c(u.area/l):0);u.z=!0,u.dx+=e.x+e.dx-a,e.y+=l,e.dy-=l}else{for((r||l>e.dx)&&(l=e.dx);++i<o;)u=n[i],u.x=a,u.y=s,u.dx=l,s+=u.dy=Math.min(e.y+e.dy-s,l?c(u.area/l):0);u.z=!1,u.dy+=e.y+e.dy-s,e.x+=l,e.dx-=l}}function i(r){var u=o||a(r),i=u[0];return i.x=0,i.y=0,i.dx=s[0],i.dy=s[1],o&&a.revalue(i),n([i],i.dx*i.dy/i.value),(o?e:t)(i),h&&(o=u),u}var o,a=Zo.layout.hierarchy(),c=Math.round,s=[1,1],l=null,f=Ni,h=!1,g="squarify",p=.5*(1+Math.sqrt(5));return i.size=function(n){return arguments.length?(s=n,i):s},i.padding=function(n){function t(t){var e=n.call(i,t,t.depth);return null==e?Ni(t):zi(t,"number"==typeof e?[e,e,e,e]:e)}function e(t){return zi(t,n)}if(!arguments.length)return l;var r;return f=null==(l=n)?Ni:"function"==(r=typeof n)?t:"number"===r?(n=[n,n,n,n],e):e,i},i.round=function(n){return arguments.length?(c=n?Math.round:Number,i):c!=Number},i.sticky=function(n){return arguments.length?(h=n,o=null,i):h},i.ratio=function(n){return arguments.length?(p=n,i):p},i.mode=function(n){return arguments.length?(g=n+"",i):g},Xu(i,a)},Zo.random={normal:function(n,t){var e=arguments.length;return 2>e&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=Zo.random.normal.apply(Zo,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=Zo.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},Zo.scale={};var ss={floor:wt,ceil:wt};Zo.scale.linear=function(){return Ui([0,1],[0,1],hu,!1)};var ls={s:1,g:1,p:1,r:1,e:1};Zo.scale.log=function(){return Vi(Zo.scale.linear().domain([0,1]),10,!0,[1,10])};var fs=Zo.format(".0e"),hs={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};Zo.scale.pow=function(){return Xi(Zo.scale.linear(),1,[0,1])},Zo.scale.sqrt=function(){return Zo.scale.pow().exponent(.5)},Zo.scale.ordinal=function(){return Bi([],{t:"range",a:[[]]})},Zo.scale.category10=function(){return Zo.scale.ordinal().range(gs)},Zo.scale.category20=function(){return Zo.scale.ordinal().range(ps)},Zo.scale.category20b=function(){return Zo.scale.ordinal().range(vs)},Zo.scale.category20c=function(){return Zo.scale.ordinal().range(ds)};var gs=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(vt),ps=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(vt),vs=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(vt),ds=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(vt);Zo.scale.quantile=function(){return Wi([],[])},Zo.scale.quantize=function(){return Ji(0,1,[0,1])},Zo.scale.threshold=function(){return Gi([.5],[0,1])},Zo.scale.identity=function(){return Ki([0,1])},Zo.svg={},Zo.svg.arc=function(){function n(){var n=t.apply(this,arguments),i=e.apply(this,arguments),o=r.apply(this,arguments)+ms,a=u.apply(this,arguments)+ms,c=(o>a&&(c=o,o=a,a=c),a-o),s=ba>c?"0":"1",l=Math.cos(o),f=Math.sin(o),h=Math.cos(a),g=Math.sin(a);
+return c>=ys?n?"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"M0,"+n+"A"+n+","+n+" 0 1,0 0,"+-n+"A"+n+","+n+" 0 1,0 0,"+n+"Z":"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"Z":n?"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L"+n*h+","+n*g+"A"+n+","+n+" 0 "+s+",0 "+n*l+","+n*f+"Z":"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L0,0"+"Z"}var t=Qi,e=no,r=to,u=eo;return n.innerRadius=function(e){return arguments.length?(t=bt(e),n):t},n.outerRadius=function(t){return arguments.length?(e=bt(t),n):e},n.startAngle=function(t){return arguments.length?(r=bt(t),n):r},n.endAngle=function(t){return arguments.length?(u=bt(t),n):u},n.centroid=function(){var n=(t.apply(this,arguments)+e.apply(this,arguments))/2,i=(r.apply(this,arguments)+u.apply(this,arguments))/2+ms;return[Math.cos(i)*n,Math.sin(i)*n]},n};var ms=-Sa,ys=wa-ka;Zo.svg.line=function(){return ro(wt)};var xs=Zo.map({linear:uo,"linear-closed":io,step:oo,"step-before":ao,"step-after":co,basis:po,"basis-open":vo,"basis-closed":mo,bundle:yo,cardinal:fo,"cardinal-open":so,"cardinal-closed":lo,monotone:So});xs.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Ms=[0,2/3,1/3,0],_s=[0,1/3,2/3,0],bs=[0,1/6,2/3,1/6];Zo.svg.line.radial=function(){var n=ro(ko);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},ao.reverse=co,co.reverse=ao,Zo.svg.area=function(){return Eo(wt)},Zo.svg.area.radial=function(){var n=Eo(ko);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},Zo.svg.chord=function(){function n(n,a){var c=t(this,i,n,a),s=t(this,o,n,a);return"M"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,s)?u(c.r,c.p1,c.r,c.p0):u(c.r,c.p1,s.r,s.p0)+r(s.r,s.p1,s.a1-s.a0)+u(s.r,s.p1,c.r,c.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=a.call(n,u,r),o=c.call(n,u,r)+ms,l=s.call(n,u,r)+ms;return{r:i,a0:o,a1:l,p0:[i*Math.cos(o),i*Math.sin(o)],p1:[i*Math.cos(l),i*Math.sin(l)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>ba)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=gr,o=pr,a=Ao,c=to,s=eo;return n.radius=function(t){return arguments.length?(a=bt(t),n):a},n.source=function(t){return arguments.length?(i=bt(t),n):i},n.target=function(t){return arguments.length?(o=bt(t),n):o},n.startAngle=function(t){return arguments.length?(c=bt(t),n):c},n.endAngle=function(t){return arguments.length?(s=bt(t),n):s},n},Zo.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),o=e.call(this,n,u),a=(i.y+o.y)/2,c=[i,{x:i.x,y:a},{x:o.x,y:a},o];return c=c.map(r),"M"+c[0]+"C"+c[1]+" "+c[2]+" "+c[3]}var t=gr,e=pr,r=Co;return n.source=function(e){return arguments.length?(t=bt(e),n):t},n.target=function(t){return arguments.length?(e=bt(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},Zo.svg.diagonal.radial=function(){var n=Zo.svg.diagonal(),t=Co,e=n.projection;return n.projection=function(n){return arguments.length?e(No(t=n)):t},n},Zo.svg.symbol=function(){function n(n,r){return(ws.get(t.call(this,n,r))||To)(e.call(this,n,r))}var t=Lo,e=zo;return n.type=function(e){return arguments.length?(t=bt(e),n):t},n.size=function(t){return arguments.length?(e=bt(t),n):e},n};var ws=Zo.map({circle:To,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*As)),e=t*As;return"M0,"+-t+"L"+e+",0"+" 0,"+t+" "+-e+",0"+"Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/Es),e=t*Es/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/Es),e=t*Es/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});Zo.svg.symbolTypes=ws.keys();var Ss,ks,Es=Math.sqrt(3),As=Math.tan(30*Aa),Cs=[],Ns=0;Cs.call=pa.call,Cs.empty=pa.empty,Cs.node=pa.node,Cs.size=pa.size,Zo.transition=function(n){return arguments.length?Ss?n.transition():n:ma.transition()},Zo.transition.prototype=Cs,Cs.select=function(n){var t,e,r,u=this.id,i=[];n=b(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]);for(var c=this[o],s=-1,l=c.length;++s<l;)(r=c[s])&&(e=n.call(r,r.__data__,s,o))?("__data__"in r&&(e.__data__=r.__data__),Po(e,s,u,r.__transition__[u]),t.push(e)):t.push(null)}return qo(i,u)},Cs.selectAll=function(n){var t,e,r,u,i,o=this.id,a=[];n=w(n);for(var c=-1,s=this.length;++c<s;)for(var l=this[c],f=-1,h=l.length;++f<h;)if(r=l[f]){i=r.__transition__[o],e=n.call(r,r.__data__,f,c),a.push(t=[]);for(var g=-1,p=e.length;++g<p;)(u=e[g])&&Po(u,g,o,i),t.push(u)}return qo(a,o)},Cs.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=R(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]);for(var e=this[i],a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return qo(u,this.id)},Cs.tween=function(n,t){var e=this.id;return arguments.length<2?this.node().__transition__[e].tween.get(n):P(this,null==t?function(t){t.__transition__[e].tween.remove(n)}:function(r){r.__transition__[e].tween.set(n,t)})},Cs.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?Du:hu,a=Zo.ns.qualify(n);return Ro(this,"attr."+n,t,a.local?i:u)},Cs.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=Zo.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Cs.style=function(n,t,e){function r(){this.style.removeProperty(n)}function u(t){return null==t?r:(t+="",function(){var r,u=Wo.getComputedStyle(this,null).getPropertyValue(n);return u!==t&&(r=hu(u,t),function(t){this.style.setProperty(n,r(t),e)})})}var i=arguments.length;if(3>i){if("string"!=typeof n){2>i&&(t="");for(e in n)this.style(e,n[e],t);return this}e=""}return Ro(this,"style."+n,t,u)},Cs.styleTween=function(n,t,e){function r(r,u){var i=t.call(this,r,u,Wo.getComputedStyle(this,null).getPropertyValue(n));return i&&function(t){this.style.setProperty(n,i(t),e)}}return arguments.length<3&&(e=""),this.tween("style."+n,r)},Cs.text=function(n){return Ro(this,"text",n,Do)},Cs.remove=function(){return this.each("end.transition",function(){var n;this.__transition__.count<2&&(n=this.parentNode)&&n.removeChild(this)})},Cs.ease=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].ease:("function"!=typeof n&&(n=Zo.ease.apply(Zo,arguments)),P(this,function(e){e.__transition__[t].ease=n}))},Cs.delay=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].delay:P(this,"function"==typeof n?function(e,r,u){e.__transition__[t].delay=+n.call(e,e.__data__,r,u)}:(n=+n,function(e){e.__transition__[t].delay=n}))},Cs.duration=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].duration:P(this,"function"==typeof n?function(e,r,u){e.__transition__[t].duration=Math.max(1,n.call(e,e.__data__,r,u))}:(n=Math.max(1,n),function(e){e.__transition__[t].duration=n}))},Cs.each=function(n,t){var e=this.id;if(arguments.length<2){var r=ks,u=Ss;Ss=e,P(this,function(t,r,u){ks=t.__transition__[e],n.call(t,t.__data__,r,u)}),ks=r,Ss=u}else P(this,function(r){var u=r.__transition__[e];(u.event||(u.event=Zo.dispatch("start","end"))).on(n,t)});return this},Cs.transition=function(){for(var n,t,e,r,u=this.id,i=++Ns,o=[],a=0,c=this.length;c>a;a++){o.push(n=[]);for(var t=this[a],s=0,l=t.length;l>s;s++)(e=t[s])&&(r=Object.create(e.__transition__[u]),r.delay+=r.duration,Po(e,s,i,r)),n.push(e)}return qo(o,i)},Zo.svg.axis=function(){function n(n){n.each(function(){var n,s=Zo.select(this),l=this.__chart__||e,f=this.__chart__=e.copy(),h=null==c?f.ticks?f.ticks.apply(f,a):f.domain():c,g=null==t?f.tickFormat?f.tickFormat.apply(f,a):wt:t,p=s.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",ka),d=Zo.transition(p.exit()).style("opacity",ka).remove(),m=Zo.transition(p.order()).style("opacity",1),y=Ti(f),x=s.selectAll(".domain").data([0]),M=(x.enter().append("path").attr("class","domain"),Zo.transition(x));v.append("line"),v.append("text");var _=v.select("line"),b=m.select("line"),w=p.select("text").text(g),S=v.select("text"),k=m.select("text");switch(r){case"bottom":n=Uo,_.attr("y2",u),S.attr("y",Math.max(u,0)+o),b.attr("x2",0).attr("y2",u),k.attr("x",0).attr("y",Math.max(u,0)+o),w.attr("dy",".71em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+i+"V0H"+y[1]+"V"+i);break;case"top":n=Uo,_.attr("y2",-u),S.attr("y",-(Math.max(u,0)+o)),b.attr("x2",0).attr("y2",-u),k.attr("x",0).attr("y",-(Math.max(u,0)+o)),w.attr("dy","0em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+-i+"V0H"+y[1]+"V"+-i);break;case"left":n=jo,_.attr("x2",-u),S.attr("x",-(Math.max(u,0)+o)),b.attr("x2",-u).attr("y2",0),k.attr("x",-(Math.max(u,0)+o)).attr("y",0),w.attr("dy",".32em").style("text-anchor","end"),M.attr("d","M"+-i+","+y[0]+"H0V"+y[1]+"H"+-i);break;case"right":n=jo,_.attr("x2",u),S.attr("x",Math.max(u,0)+o),b.attr("x2",u).attr("y2",0),k.attr("x",Math.max(u,0)+o).attr("y",0),w.attr("dy",".32em").style("text-anchor","start"),M.attr("d","M"+i+","+y[0]+"H0V"+y[1]+"H"+i)}if(f.rangeBand){var E=f,A=E.rangeBand()/2;l=f=function(n){return E(n)+A}}else l.rangeBand?l=f:d.call(n,f);v.call(n,l),m.call(n,f)})}var t,e=Zo.scale.linear(),r=zs,u=6,i=6,o=3,a=[10],c=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Ls?t+"":zs,n):r},n.ticks=function(){return arguments.length?(a=arguments,n):a},n.tickValues=function(t){return arguments.length?(c=t,n):c},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var zs="bottom",Ls={top:1,right:1,bottom:1,left:1};Zo.svg.brush=function(){function n(i){i.each(function(){var i=Zo.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=i.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),i.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=i.selectAll(".resize").data(p,wt);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Ts[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,f=Zo.transition(i),h=Zo.transition(o);c&&(l=Ti(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),e(f)),s&&(l=Ti(s),h.attr("y",l[0]).attr("height",l[1]-l[0]),r(f)),t(f)})}function t(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+l[+/e$/.test(n)]+","+f[+/^s/.test(n)]+")"})}function e(n){n.select(".extent").attr("x",l[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",l[1]-l[0])}function r(n){n.select(".extent").attr("y",f[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",f[1]-f[0])}function u(){function u(){32==Zo.event.keyCode&&(C||(x=null,z[0]-=l[1],z[1]-=f[1],C=2),y())}function p(){32==Zo.event.keyCode&&2==C&&(z[0]+=l[1],z[1]+=f[1],C=0,y())}function v(){var n=Zo.mouse(_),u=!1;M&&(n[0]+=M[0],n[1]+=M[1]),C||(Zo.event.altKey?(x||(x=[(l[0]+l[1])/2,(f[0]+f[1])/2]),z[0]=l[+(n[0]<x[0])],z[1]=f[+(n[1]<x[1])]):x=null),E&&d(n,c,0)&&(e(S),u=!0),A&&d(n,s,1)&&(r(S),u=!0),u&&(t(S),w({type:"brush",mode:C?"move":"resize"}))}function d(n,t,e){var r,u,a=Ti(t),c=a[0],s=a[1],p=z[e],v=e?f:l,d=v[1]-v[0];return C&&(c-=p,s-=d+p),r=(e?g:h)?Math.max(c,Math.min(s,n[e])):n[e],C?u=(r+=p)+d:(x&&(p=Math.max(c,Math.min(s,2*x[e]-r))),r>p?(u=r,r=p):u=p),v[0]!=r||v[1]!=u?(e?o=null:i=null,v[0]=r,v[1]=u,!0):void 0}function m(){v(),S.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),Zo.select("body").style("cursor",null),L.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),N(),w({type:"brushend"})}var x,M,_=this,b=Zo.select(Zo.event.target),w=a.of(_,arguments),S=Zo.select(_),k=b.datum(),E=!/^(n|s)$/.test(k)&&c,A=!/^(e|w)$/.test(k)&&s,C=b.classed("extent"),N=I(),z=Zo.mouse(_),L=Zo.select(Wo).on("keydown.brush",u).on("keyup.brush",p);if(Zo.event.changedTouches?L.on("touchmove.brush",v).on("touchend.brush",m):L.on("mousemove.brush",v).on("mouseup.brush",m),S.interrupt().selectAll("*").interrupt(),C)z[0]=l[0]-z[0],z[1]=f[0]-z[1];else if(k){var T=+/w$/.test(k),q=+/^n/.test(k);M=[l[1-T]-z[0],f[1-q]-z[1]],z[0]=l[T],z[1]=f[q]}else Zo.event.altKey&&(x=z.slice());S.style("pointer-events","none").selectAll(".resize").style("display",null),Zo.select("body").style("cursor",b.style("cursor")),w({type:"brushstart"}),v()}var i,o,a=M(n,"brushstart","brush","brushend"),c=null,s=null,l=[0,0],f=[0,0],h=!0,g=!0,p=qs[0];return n.event=function(n){n.each(function(){var n=a.of(this,arguments),t={x:l,y:f,i:i,j:o},e=this.__chart__||t;this.__chart__=t,Ss?Zo.select(this).transition().each("start.brush",function(){i=e.i,o=e.j,l=e.x,f=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=gu(l,t.x),r=gu(f,t.y);return i=o=null,function(u){l=t.x=e(u),f=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){i=t.i,o=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,p=qs[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,p=qs[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(h=!!t[0],g=!!t[1]):c?h=!!t:s&&(g=!!t),n):c&&s?[h,g]:c?h:s?g:null},n.extent=function(t){var e,r,u,a,h;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),i=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(h=e,e=r,r=h),(e!=l[0]||r!=l[1])&&(l=[e,r])),s&&(u=t[0],a=t[1],c&&(u=u[1],a=a[1]),o=[u,a],s.invert&&(u=s(u),a=s(a)),u>a&&(h=u,u=a,a=h),(u!=f[0]||a!=f[1])&&(f=[u,a])),n):(c&&(i?(e=i[0],r=i[1]):(e=l[0],r=l[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(h=e,e=r,r=h))),s&&(o?(u=o[0],a=o[1]):(u=f[0],a=f[1],s.invert&&(u=s.invert(u),a=s.invert(a)),u>a&&(h=u,u=a,a=h))),c&&s?[[e,u],[r,a]]:c?[e,r]:s&&[u,a])},n.clear=function(){return n.empty()||(l=[0,0],f=[0,0],i=o=null),n},n.empty=function(){return!!c&&l[0]==l[1]||!!s&&f[0]==f[1]},Zo.rebind(n,a,"on")};var Ts={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},qs=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Rs=Qa.format=ic.timeFormat,Ds=Rs.utc,Ps=Ds("%Y-%m-%dT%H:%M:%S.%LZ");Rs.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?Ho:Ps,Ho.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},Ho.toString=Ps.toString,Qa.second=Dt(function(n){return new nc(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),Qa.seconds=Qa.second.range,Qa.seconds.utc=Qa.second.utc.range,Qa.minute=Dt(function(n){return new nc(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),Qa.minutes=Qa.minute.range,Qa.minutes.utc=Qa.minute.utc.range,Qa.hour=Dt(function(n){var t=n.getTimezoneOffset()/60;return new nc(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),Qa.hours=Qa.hour.range,Qa.hours.utc=Qa.hour.utc.range,Qa.month=Dt(function(n){return n=Qa.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),Qa.months=Qa.month.range,Qa.months.utc=Qa.month.utc.range;var Us=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],js=[[Qa.second,1],[Qa.second,5],[Qa.second,15],[Qa.second,30],[Qa.minute,1],[Qa.minute,5],[Qa.minute,15],[Qa.minute,30],[Qa.hour,1],[Qa.hour,3],[Qa.hour,6],[Qa.hour,12],[Qa.day,1],[Qa.day,2],[Qa.week,1],[Qa.month,1],[Qa.month,3],[Qa.year,1]],Hs=Rs.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",we]]),Fs={range:function(n,t,e){return Zo.range(Math.ceil(n/e)*e,+t,e).map(Oo)},floor:wt,ceil:wt};js.year=Qa.year,Qa.scale=function(){return Fo(Zo.scale.linear(),js,Hs)};var Os=js.map(function(n){return[n[0].utc,n[1]]}),Ys=Ds.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",we]]);Os.year=Qa.year.utc,Qa.scale.utc=function(){return Fo(Zo.scale.linear(),Os,Ys)},Zo.text=St(function(n){return n.responseText}),Zo.json=function(n,t){return kt(n,"application/json",Yo,t)},Zo.html=function(n,t){return kt(n,"text/html",Io,t)},Zo.xml=St(function(n){return n.responseXML}),"function"==typeof define&&define.amd?define(Zo):"object"==typeof module&&module.exports&&(module.exports=Zo),this.d3=Zo}();
+
+// parseUri 1.2.2
+// (c) Steven Levithan <stevenlevithan.com>
+// MIT License
+
+function parseUri (str) {
+	var	o   = parseUri.options,
+		m   = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
+		uri = {},
+		i   = 14;
+
+	while (i--) uri[o.key[i]] = m[i] || "";
+
+	uri[o.q.name] = {};
+	uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
+		if ($1) uri[o.q.name][$1] = $2;
+	});
+
+	return uri;
+};
+
+parseUri.options = {
+	strictMode: false,
+	key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
+	q:   {
+		name:   "queryKey",
+		parser: /(?:^|&)([^&=]*)=?([^&]*)/g
+	},
+	parser: {
+		strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
+		loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
+	}
+};
+
+
+// graph.js
+var drawGraph = function() {
+
+    $("svg").remove();
+
+    //Dataset and metric to draw is passed via query option:
+    query = parseUri(location).queryKey;
+    query.stats = unescape(query.stats);
+    stats_db = '/tests/artifacts/' + query.stats + '/stats';
+    var metric = query.metric;
+    var operation = query.operation;
+    var smoothing = query.smoothing;
+    var show_aggregates = query.show_aggregates;
+
+    xmin = query.xmin;
+    xmax = query.xmax;
+    ymin = query.ymin;
+    ymax = query.ymax;
+
+    //Pull metrics from the stats json:
+    stress_metrics = $.extend([], stats['stats'][0]['metrics']);
+    $.each(stress_metrics, function(i,v) {
+        stress_metrics[i] = v.replace(/\W/g,"_");
+    });
+    stress_metric_names = {};
+    $.each(stress_metrics, function(i,v) {
+        stress_metric_names[v] = stats['stats'][0]['metrics'][i];
+    });
+    //Replace names of shorthand metric names with longer ones:
+    $.extend(stress_metric_names, {
+       "mean": "latency mean",
+       "med" : "latency median",
+       "_95" : "latency 95th pct",
+       "_99" : "latency 99th pct",
+       "_999": "latency 99.9th pct",
+       "max" : "latency max",
+       "max_ms" : "gc max (ms)",
+       "sum_ms" : "gc sum (ms)",
+       "sdv_ms" : "gc sdv (ms)",
+       "mb"     : "gc MB"
+    });
+
+    var updateURLBar = function() {
+        //Update the URL bar with the current parameters:
+        window.history.replaceState(null,null,parseUri(location).path + "?" + $.param(query));
+    };
+    
+    //Check query parameters:
+    if (metric == undefined) {
+        metric = query.metric = 'op_s';
+    }
+    if (operation == undefined) {
+        operation = query.operation = stats['stats'][0]['test'];
+    }
+    if (smoothing == undefined) {
+        smoothing = query.smoothing = 1;
+    }
+    if (show_aggregates == undefined || query.show_aggregates == 'true') {
+        show_aggregates = query.show_aggregates = true;
+    } else {
+        show_aggregates = query.show_aggregates = false;
+    }
+    updateURLBar();
+
+    var metric_index = stress_metrics.indexOf(metric);
+    var time_index = stress_metrics.indexOf('time');
+
+    /// Add dropdown controls to select chart criteria / options:
+    var chart_controls = $('<div id="chart_controls"/>');
+    var chart_controls_tbl = $('<table/>');
+    chart_controls.append(chart_controls_tbl);
+    $('body').append(chart_controls);
+    var metric_selector = $('<select id="metric_selector"/>');
+    $.each(stress_metric_names, function(k,v) {
+        if (k == 'time') {
+            return; //Elapsed time makes no sense to graph, skip it.
+        }
+        var option = $('<option/>').attr('value', k).text(v);
+        if (metric == k) {
+            option.attr('selected','selected');
+        }
+        metric_selector.append(option);
+
+    });
+    chart_controls_tbl.append('<tr><td><label for="metric_selector"/>Choose metric:</label></td><td id="metric_selector_td"></td></tr>')
+    $('#metric_selector_td').append(metric_selector);
+
+    var operation_selector = $('<select id="operation_selector"/>')
+    chart_controls_tbl.append('<tr><td><label for="operation_selector"/>Choose operation:</label></td><td id="operation_selector_td"></td></tr>')
+    $('#operation_selector_td').append(operation_selector);
+
+
+    var smoothing_selector = $('<select id="smoothing_selector"/>')
+    $.each([1,2,3,4,5,6,7,8], function(i, v) {
+        var option = $('<option/>').attr('value', v).text(v);
+        if (smoothing == v) {
+            option.attr('selected','selected');
+        }
+        smoothing_selector.append(option);
+    });
+    chart_controls_tbl.append('<tr><td style="width:150px"><label for="smoothing_selector"/>Data smoothing:</label></td><td id="smoothing_selector_td"></td></tr>')
+    $("#smoothing_selector_td").append(smoothing_selector);
+
+    var show_aggregates_checkbox = $('<input type="checkbox" id="show_aggregates_checkbox"/>');
+    chart_controls_tbl.append('<tr><td style="padding-top:10px"><label for="show_aggregates_checkbox">Show aggregates</label></td><td id="show_aggregates_td"></td></tr>');
+    $("#show_aggregates_td").append(show_aggregates_checkbox);
+    show_aggregates_checkbox.attr("checked", show_aggregates);
+
+    chart_controls_tbl.append('<tr><td colspan="100%">Zoom: <a href="#" id="reset_zoom">reset</a><table id="zoom"><tr><td><label for="xmin"/>x min</label></td><td><input id="xmin"/></td><td><label for="xmax"/>x max</label></td><td><input id="xmax"/></td></tr><tr><td><label for="ymin"/>y min</label></td><td><input id="ymin"/></td><td><label for="ymax"/>y max</label></td><td><input id="ymax"/></td></tr></table></td></tr>');
+
+    chart_controls_tbl.append('<tr><td style="padding-top:10px" colspan="100%">To hide/show a dataset click on the associated colored box</td></tr>');
+
+    var raw_data;
+
+    //Callback to draw graph once we have json data.
+    var graph_callback = function() {
+        var data = [];
+        var trials = {};
+        var data_by_title = {};
+        //Keep track of what operations are availble from the test:
+        var operations = {};
+
+        raw_data.stats.forEach(function(d) {
+            // Make a copy of d so we never modify raw_data
+            d = $.extend({}, d);
+            operations[d.test] = true;
+            if (d.test!=operation) {
+                return;
+            }
+            d.title = d['label'] != undefined ? d['label'] : d['revision'];
+            data_by_title[d.title] = d;
+            data.push(d);
+            trials[d.title] = d;
+            //Clean up the intervals:
+            //Remove every other item, so as to smooth the line:
+            var new_intervals = [];
+            d.intervals.forEach(function(i, x) {
+                if (x % smoothing == 0) {
+                    new_intervals.push(i);
+                }
+            });
+            d.intervals = new_intervals;
+        });
+
+        //Fill operations available from test:
+        operation_selector.children().remove();
+        $.each(operations, function(k) {
+            var option = $('<option/>').attr('value', k).text(k);
+            if (operation == k) {
+                option.attr('selected','selected');
+            }
+            operation_selector.append(option);
+        });
+
+        var getMetricValue = function(d) {
+            if (metric_index >= 0) {
+                //This is one of the metrics directly reported by stress:
+                return d[metric_index];
+            } else {
+                //This metric is not reported by stress, so compute it ourselves:
+                if (metric == 'num_timeouts') {
+                    return d[stress_metrics.indexOf('interval_op_rate')] - d[stress_metrics.indexOf('interval_key_rate')];
+                }
+            }
+        };
+
+        //Parse the dates:
+        data.forEach(function(d) {
+            d.date = new Date(Date.parse(d.date));
+        });
+
+
+        $("svg").remove();
+        //Setup initial zoom level:
+        defaultZoom = function(initialize) {
+            if (!initialize) {
+                //Reset zoom query params:
+                query.xmin = xmin = undefined;
+                query.xmax = xmax = undefined;
+                query.ymin = ymin = undefined;
+                query.ymax = ymax = undefined;
+            }
+            query.xmin = xmin = query.xmin ? query.xmin : 0;
+            query.xmax = xmax = query.xmax ? query.xmax : Math.round(d3.max(data, function(d) {
+                if (d.intervals.length > 0) {
+                    return d.intervals[d.intervals.length-1][time_index];
+                }
+            }) * 1.1 * 100) / 100;
+            query.ymin = ymin = query.ymin ? query.ymin : 0;
+            query.ymax = ymax = query.ymax ? query.ymax : Math.round(d3.max(data, function(d) {
+                return d3.max(d.intervals, function(i) {
+                    return getMetricValue(i);
+                });
+            }) * 1.1 * 100) / 100;
+            $("#xmin").val(xmin);
+            $("#xmax").val(xmax);
+            $("#ymin").val(ymin);
+            $("#ymax").val(ymax);
+            var updateX = function() {
+                query.xmin = xmin = $("#xmin").val();
+                query.xmax = xmax = $("#xmax").val();
+                x.domain([xmin,xmax]);
+                updateURLBar();
+            };
+            var updateY = function() {
+                query.ymin = ymin = $("#ymin").val();
+                query.ymax = ymax = $("#ymax").val();
+                y.domain([ymin, ymax]);
+                updateURLBar();
+            };
+            $("#xmin,#xmax").unbind().change(function(e) {
+                updateX();
+                redrawLines();
+            });
+            $("#ymin,#ymax").unbind().change(function(e) {
+                updateY();
+                redrawLines();
+            });
+            // The first time defaultZoom is called, we pass
+            // initialize=true, and we do not call the change() method
+            // yet. On subsequent calls, without initialize, we do.
+            if (!initialize) {
+                updateX();
+                updateY();
+                redrawLines();
+            }
+        }
+        defaultZoom(true);
+
+        $("#reset_zoom").click(function(e) {
+            defaultZoom();
+            e.preventDefault();
+        });
+
+        //Setup chart:
+        var margin = {top: 20, right: 1180, bottom: 4240, left: 60};
+        var width = 2060 - margin.left - margin.right;
+        var height = 4700 - margin.top - margin.bottom;
+
+        var x = d3.scale.linear()
+            .domain([xmin, xmax])
+            .range([0, width]);
+
+        var y = d3.scale.linear()
+            .domain([ymin, ymax])
+            .range([height, 0]);
+
+        var color = d3.scale.category10();
+        color.domain(data.map(function(d){return d.title}));
+
+        var xAxis = d3.svg.axis()
+            .scale(x)
+            .orient("bottom");
+
+        var yAxis = d3.svg.axis()
+            .scale(y)
+            .orient("left");
+
+        var line = d3.svg.line()
+            .interpolate("basis")
+            .x(function(d) {
+                return x(d[time_index]); //time in seconds
+            })
+            .y(function(d) {
+                return y(getMetricValue(d));
+            });
+
+        $("body").append("<div id='svg_container'>");
+
+        var redrawLines = function() {
+            svg.select(".x.axis").call(xAxis);
+            svg.select(".y.axis").call(yAxis);
+            svg.selectAll(".line")
+                .attr("class","line")
+                .attr("d", function(d) {
+                    return line(d.intervals);
+                })
+            $("#xmin").val(x.domain()[0]);
+            $("#xmax").val(x.domain()[1]);
+            $("#ymin").val(y.domain()[0]);
+            $("#ymax").val(y.domain()[1]);
+        }
+
+        var zoom = d3.behavior.zoom()
+            .x(x)
+            .y(y)
+            .on("zoom", redrawLines);
+
+        var svg = d3.select("div#svg_container").append("svg")
+            .attr("width", width + margin.left + margin.right + 250)
+            .attr("height", height + margin.top + margin.bottom)
+            .append("g")
+            .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
+
+        // Clip Path
+        svg.append("svg:clipPath")
+            .attr("id", "chart_clip")
+            .append("svg:rect")
+            .attr("width", width)
+            .attr("height", height);
+
+        // Chart title
+        svg.append("text")
+            .attr("x", width / 2 )
+            .attr("y", 0 )
+            .style('font-size', '2em')
+            .style("text-anchor", "middle")
+            .text(raw_data.title + ' - ' + operation);
+
+        // Chart subtitle
+        svg.append("text")
+            .attr("x", width / 2 )
+            .attr("y", 15 )
+            .style('font-size', '1.2em')
+            .style("text-anchor", "middle")
+            .text((raw_data.subtitle ? raw_data.subtitle : ''));
+
+        // x-axis - time
+        svg.append("g")
+            .attr("class", "x axis")
+            .attr("transform", "translate(0," + height + ")")
+            .call(xAxis);
+
+        // x-axis label
+        svg.append("text")
+            .attr("x", width / 2 )
+            .attr("y", height + 30 )
+            .style("text-anchor", "middle")
+            .style("font-size", "1.2em")
+            .text(stress_metric_names['time']);
+
+        // y-axis
+        svg.append("g")
+            .attr("class", "y axis")
+            .call(yAxis)
+            .append("text")
+            .attr("transform", "rotate(-90)")
+            .attr("y", -60)
+            .attr("dy", ".91em")
+            .style("font-size", "1.2em")
+            .style("text-anchor", "end")
+            .text(stress_metric_names[metric]);
+
+        var trial = svg.selectAll(".trial")
+            .data(data)
+            .enter().append("g")
+            .attr("class", "trial")
+            .attr("title", function(d) {
+                return d.title;
+            });
+
+        // Draw benchmarked data:
+        trial.append("path")
+            .attr("class", "line")
+            .attr("clip-path", "url(#chart_clip)")
+            .attr("d", function(d) {
+                return line(d.intervals);
+            })
+            .style("stroke", function(d) { return color(d.title); });
+
+        var legend = svg.selectAll(".legend")
+            .data(color.domain())
+            .enter().append("g")
+            .attr("class", "legend")
+            .attr("transform", function(d, i) {
+                if (show_aggregates == true) {
+                    var y_offset = 425 + (i*240) + 70;
+                } else {
+                    var y_offset = 425 + (i*25) + 70;
+                }
+                var x_offset = -550;
+                return "translate(" + x_offset + "," + y_offset + ")";
+            });
+
+        var renderLegendText = function(linenum, getTextCallback) {
+            legend.append("text")
+                .attr("x", width - 24 - 250)
+                .attr("y", 12*linenum)
+                .attr("dy", ".35em")
+                .style("font-family", "monospace")
+                .style("font-size", "1.2em")
+                .style("text-anchor", "start")
+                .text(function(d) {
+                    return getTextCallback(d);
+                });
+        };
+
+        var padTextEnd = function(text, length) {
+            for(var x=text.length; x<length; x++) {
+                text = text + '\u00A0';
+            }
+            return text;
+        };
+        var padTextStart = function(text, length) {
+            for(var x=text.length; x<length; x++) {
+                text = '\u00A0' + text;
+            }
+            return text;
+        };
+
+        renderLegendText(1, function(title) {
+            return padTextStart(title, title.length + 5);
+        });
+
+        if (show_aggregates === true) {
+            renderLegendText(2, function(title) {
+                return '---------------------------------------';
+            });
+
+            renderLegendText(3, function(title) {
+                return padTextEnd('op rate', 26) + " : " + data_by_title[title]['op rate'];
+            });
+
+            renderLegendText(4, function(title) {
+                return padTextEnd('partition rate', 26) + " : " + data_by_title[title]['partition rate'];
+            });
+
+            renderLegendText(5, function(title) {
+                return padTextEnd('row rate', 26) + ' : ' + data_by_title[title]['row rate'];
+            });
+
+            renderLegendText(6, function(title) {
+                return padTextEnd('latency mean', 26) + ' : ' + data_by_title[title]['latency mean'];
+            });
+
+            renderLegendText(7, function(title) {
+                return padTextEnd('latency median', 26) + ' : ' + data_by_title[title]['latency median'];
+            });
+
+            renderLegendText(8, function(title) {
+                return padTextEnd('latency 95th percentile', 26) + ' : ' + data_by_title[title]['latency 95th percentile'];
+            });
+
+            renderLegendText(9, function(title) {
+                return padTextEnd('latency 99th percentile', 26) + ' : ' + data_by_title[title]['latency 99th percentile'];
+            });
+
+            renderLegendText(10, function(title) {
+                return padTextEnd('latency 99.9th percentile', 26) + ' : ' + data_by_title[title]['latency 99.9th percentile'];
+            });
+
+            renderLegendText(11, function(title) {
+                return padTextEnd('latency max', 26) + ' : ' + data_by_title[title]['latency max'];
+            });
+
+            renderLegendText(12, function(title) {
+                return padTextEnd('total gc count', 26) + ' : ' + data_by_title[title]['total gc count'];
+            });
+
+            renderLegendText(13, function(title) {
+                return padTextEnd('total gc memory', 26) + ' : ' + data_by_title[title]['total gc memory'];
+            });
+
+            renderLegendText(14, function(title) {
+                return padTextEnd('total gc time', 26) + ' : ' + data_by_title[title]['total gc time'];
+            });
+
+            renderLegendText(15, function(title) {
+                return padTextEnd('avg gc time', 26) + ' : ' + data_by_title[title]['avg gc time'];
+            });
+
+            renderLegendText(16, function(title) {
+                return padTextEnd('stddev gc time', 26) + ' : ' + data_by_title[title]['stddev gc time'];
+            });
+
+            renderLegendText(17, function(title) {
+                return padTextEnd('Total operation time', 26) + ' : ' + data_by_title[title]['total operation time'];
+            });
+
+            renderLegendText(18, function(title) {
+                var cmd = data_by_title[title]['command'];
+                return 'cmd: ' + cmd;
+            });
+        }
+        legend.append("rect")
+            .attr("x", width - 270)
+            .attr("width", 18)
+            .attr("height", 18)
+            .attr("class", "legend-rect")
+            .attr("title", function(title) {
+                return title;
+            })
+            .style("fill", color);
+
+        //Make trials hideable by double clicking on the colored legend box
+        $("rect.legend-rect").click(function() {
+            $("g.trial[title='" + $(this).attr('title') + "']").toggle();
+        });
+
+
+        // Chart control callbacks:
+        metric_selector.unbind().change(function(e) {
+            // change the metric in the url to reload the page:
+            metric = query.metric = this.value;
+            metric_index = stress_metrics.indexOf(metric);
+            graph_callback();
+            defaultZoom();
+        });
+        operation_selector.unbind().change(function(e) {
+            // change the metric in the url to reload the page:
+            operation = query.operation = this.value;
+            graph_callback();
+            defaultZoom();
+        });
+        smoothing_selector.unbind().change(function(e) {
+            // change the metric in the url to reload the page:
+            smoothing = query.smoothing = this.value;
+            graph_callback();
+            defaultZoom();
+        });
+        show_aggregates_checkbox.unbind().change(function(e) {
+            show_aggregates = query.show_aggregates = this.checked;
+            graph_callback();
+        });
+
+        updateURLBar();
+
+        $("#dl-test-data").attr("href",stats_db);
+
+        // Chart zoom/drag surface
+        // This should always be last, so it's on top of everything else
+        svg.append("svg:rect")
+            .attr("id", "zoom_drag_surface")
+            .attr("width", width)
+            .attr("height", height);
+    }
+
+    raw_data = stats;
+    graph_callback();
+
+}
+
+$(document).ready(function(){
+    
+    drawGraph();
+    
+});
+      -->
+  </script>
+  <style type="text/css">
+div#chart_controls {
+    margin-left: 900px;
+    position: absolute;
+}
+
+#chart_controls > table {
+    width: 640px;
+}
+
+#chart_controls td {
+    padding: 2px;
+}
+
+#chart_controls #zoom input {
+    width: 50px;
+}
+
+#chart_controls table#zoom {
+    padding-left: 20px;
+}
+
+#chart_controls table#zoom td {
+    padding-left: 20px;
+}
+
+#zoom_drag_surface {
+    fill: rgba(250, 250, 255, 0.0);
+    z-index: 100;
+}
+
+svg {
+  font: 10px sans-serif;
+}
+
+.axis path,
+.axis line {
+  fill: none;
+  stroke: #000;
+  shape-rendering: crispEdges;
+}
+
+.x.axis path {
+  display: none;
+}
+
+.line {
+  fill: none;
+  stroke: steelblue;
+  stroke-width: 1.5px;
+}
+
+  </style>
+</head>
+<body>
+</body>
+
diff --git a/tools/stress/test/unit/org/apache/cassandra/stress/generate/DistributionSequenceTest.java b/tools/stress/test/unit/org/apache/cassandra/stress/generate/DistributionSequenceTest.java
new file mode 100644
index 0000000..591833c
--- /dev/null
+++ b/tools/stress/test/unit/org/apache/cassandra/stress/generate/DistributionSequenceTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.stress.generate;
+
+import org.junit.Test;
+
+import org.apache.cassandra.stress.settings.OptionDistribution;
+
+import static org.junit.Assert.*;
+
+public class DistributionSequenceTest
+{
+    @Test
+    public void simpleSequence() throws Exception
+    {
+        Distribution dist = OptionDistribution.get("seq(1..10)").get();
+        assertTrue(dist instanceof DistributionSequence);
+
+        assertEquals(1, dist.minValue());
+        assertEquals(10, dist.maxValue());
+        assertEquals(5, dist.average());
+
+        assertEquals(1, dist.inverseCumProb(0d));
+        assertEquals(10, dist.inverseCumProb(1d));
+
+        long min = dist.next();
+        assertEquals(1,min);
+
+        long last = min;
+        for (int i=0; i<9; i++)
+        {
+            long next = dist.next();
+            assertEquals(next, last+1); //increase by one each step
+            last = next;
+        }
+
+        assertEquals(1, dist.next()); // wrapping
+    }
+
+
+    @Test
+    public void negValueSequence() throws Exception
+    {
+        Distribution dist = OptionDistribution.get("seq(-1000..-10)").get();
+        assertTrue(dist instanceof DistributionSequence);
+
+        assertEquals(-1000, dist.minValue());
+        assertEquals( -10, dist.maxValue());
+        assertEquals(-504, dist.average());
+
+        assertEquals(-1000, dist.inverseCumProb(0d));
+        assertEquals(-10, dist.inverseCumProb(1d));
+
+        long min = dist.next();
+        assertEquals(-1000, min);
+
+        long last = min;
+        long next = dist.next();
+        while (last<next)
+        {
+            assertEquals(next, last+1); //increase by one each step
+            last = next;
+            next = dist.next();
+        }
+
+        assertEquals(-10, last); // wrapping
+        assertEquals(-1000, next); // wrapping
+    }
+
+    @Test
+    public void bigSequence() throws Exception
+    {
+        Distribution dist = OptionDistribution.get(String.format("seq(1..%d)", Long.MAX_VALUE)).get();
+        assertTrue(dist instanceof DistributionSequence);
+
+        assertEquals(1, dist.minValue());
+        assertEquals(Long.MAX_VALUE, dist.maxValue());
+
+        assertEquals(1, dist.inverseCumProb(0d));
+        assertEquals(Long.MAX_VALUE, dist.inverseCumProb(1d));
+
+    }
+
+    @Test
+    public void setSeed() throws Exception
+    {
+        Distribution dist = OptionDistribution.get("seq(1..10)").get();
+        assertTrue(dist instanceof DistributionSequence);
+
+        for (int seed=1; seed<500; seed+=seed)
+        {
+            dist.setSeed(seed);
+            assertEquals(1, dist.minValue());
+            assertEquals(10, dist.maxValue());
+            assertEquals(5, dist.average());
+
+            assertEquals(1, dist.inverseCumProb(0d));
+            assertEquals(10, dist.inverseCumProb(1d));
+
+            long last = dist.next();
+            for (int i = 0; i < 9; i++)
+            {
+                long next = dist.next();
+                if (next>1)
+                {
+                    assertEquals(next, last + 1); //increase by one each step
+                }else{
+                    assertEquals(last, 10); //wrap after the end
+                }
+                last = next;
+            }
+        }
+    }
+}
diff --git a/tools/stress/test/unit/org/apache/cassandra/stress/settings/OptionReplicationTest.java b/tools/stress/test/unit/org/apache/cassandra/stress/settings/OptionReplicationTest.java
new file mode 100644
index 0000000..803ee18
--- /dev/null
+++ b/tools/stress/test/unit/org/apache/cassandra/stress/settings/OptionReplicationTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.stress.settings;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class OptionReplicationTest
+{
+    @Test
+    public void defaultsToReplicationFactorOfOne() throws Exception
+    {
+        OptionReplication defaults = new OptionReplication();
+        assertEquals(ImmutableMap.of("replication_factor", "1"), defaults.getOptions());
+    }
+}
diff --git a/tools/stress/test/unit/org/apache/cassandra/stress/settings/SettingsMiscTest.java b/tools/stress/test/unit/org/apache/cassandra/stress/settings/SettingsMiscTest.java
new file mode 100644
index 0000000..78d9817
--- /dev/null
+++ b/tools/stress/test/unit/org/apache/cassandra/stress/settings/SettingsMiscTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.stress.settings;
+
+import java.util.Collections;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class SettingsMiscTest
+{
+    @Test
+    public void versionTriggersSpecialOption() throws Exception
+    {
+        assertTrue(SettingsMisc.maybeDoSpecial(ImmutableMap.of("version", new String[] {})));
+    }
+
+    @Test
+    public void noSpecialOptions() throws Exception
+    {
+        assertFalse(SettingsMisc.maybeDoSpecial(Collections.emptyMap()));
+    }
+
+    @Test
+    public void parsesVersionMatch() throws Exception
+    {
+        String versionString = SettingsMisc.parseVersionFile("CassandraVersion=TheBestVersion\n");
+        assertEquals("Version: TheBestVersion", versionString);
+    }
+
+    @Test
+    public void parsesVersionNoMatch() throws Exception
+    {
+        String versionString = SettingsMisc.parseVersionFile("VersionFileChangedFormat :(");
+        assertEquals("Unable to find version information", versionString);
+    }
+}
diff --git a/tools/stress/test/unit/org/apache/cassandra/stress/settings/SettingsNodeTest.java b/tools/stress/test/unit/org/apache/cassandra/stress/settings/SettingsNodeTest.java
new file mode 100644
index 0000000..ce56d27
--- /dev/null
+++ b/tools/stress/test/unit/org/apache/cassandra/stress/settings/SettingsNodeTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.stress.settings;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class SettingsNodeTest
+{
+    @Test
+    public void testDefaults() throws Exception
+    {
+        SettingsNode settingsNode = new SettingsNode(new SettingsNode.Options());
+        assertEquals(null, settingsNode.datacenter);
+    }
+
+    @Test
+    public void testOveridingDataCenter() throws Exception
+    {
+        SettingsNode.Options options = new SettingsNode.Options();
+        options.accept("datacenter=dc1");
+        SettingsNode settingsNode = new SettingsNode(options);
+        assertEquals("dc1", settingsNode.datacenter);
+    }
+}
diff --git a/tools/stress/test/unit/org/apache/cassandra/stress/settings/StressSettingsTest.java b/tools/stress/test/unit/org/apache/cassandra/stress/settings/StressSettingsTest.java
new file mode 100644
index 0000000..c664aee
--- /dev/null
+++ b/tools/stress/test/unit/org/apache/cassandra/stress/settings/StressSettingsTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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.stress.settings;
+
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectOutputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+
+public class StressSettingsTest
+{
+    @Test
+    public void isSerializable() throws Exception
+    {
+        Map<String, String[]> args = new HashMap<>();
+        args.put("write", new String[] {});
+        StressSettings settings = StressSettings.get(args);
+        // Will throw if not all settings are Serializable
+        new ObjectOutputStream(new ByteArrayOutputStream()).writeObject(settings);
+    }
+}
diff --git a/tools/stress/test/unit/org/apache/cassandra/stress/util/MultiResultLoggerTest.java b/tools/stress/test/unit/org/apache/cassandra/stress/util/MultiResultLoggerTest.java
new file mode 100644
index 0000000..f0c99b8
--- /dev/null
+++ b/tools/stress/test/unit/org/apache/cassandra/stress/util/MultiResultLoggerTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.stress.util;
+
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class MultiResultLoggerTest
+{
+
+    public static final OutputStream NOOP = new OutputStream()
+    {
+        public void write(int b) throws IOException
+        {
+        }
+    };
+
+    @Test
+    public void delegatesToInitialPrintStream() throws Exception
+    {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        PrintStream printStream = new PrintStream(output, true);
+        MultiResultLogger underTest = new MultiResultLogger(printStream);
+
+        underTest.println("Very important result");
+
+        assertEquals("Very important result\n", output.toString());
+    }
+
+    @Test
+    public void printingExceptions() throws Exception
+    {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        PrintStream printStream = new PrintStream(output, true);
+        MultiResultLogger underTest = new MultiResultLogger(printStream);
+
+        underTest.printException(new RuntimeException("Bad things"));
+
+        String stackTrace = output.toString();
+        assertTrue("Expected strack trace to be printed but got: " + stackTrace, stackTrace.startsWith("java.lang.RuntimeException: Bad things\n" +
+                                                "\tat org.apache.cassandra.stress.util.MultiResultLoggerTest.printingExceptions"));
+    }
+
+    @Test
+    public void delegatesToAdditionalPrintStreams() throws Exception
+    {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        PrintStream additionalPrintStream = new PrintStream(output, true);
+        MultiResultLogger underTest = new MultiResultLogger(new PrintStream(NOOP));
+
+        underTest.addStream(additionalPrintStream);
+        underTest.println("Very important result");
+
+        assertEquals("Very important result\n", output.toString());
+    }
+
+    @Test
+    public void delegatesPrintfToAdditionalPrintStreams() throws Exception
+    {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        PrintStream additionalPrintStream = new PrintStream(output, true);
+        MultiResultLogger underTest = new MultiResultLogger(new PrintStream(NOOP));
+
+        underTest.addStream(additionalPrintStream);
+        underTest.printf("%s %s %s", "one", "two", "three");
+
+        assertEquals("one two three", output.toString());
+    }
+
+    @Test
+    public void delegatesPrintlnToAdditionalPrintStreams() throws Exception
+    {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        PrintStream additionalPrintStream = new PrintStream(output, true);
+        MultiResultLogger underTest = new MultiResultLogger(new PrintStream(NOOP));
+
+        underTest.addStream(additionalPrintStream);
+        underTest.println();
+
+        assertEquals("\n", output.toString());
+    }
+}
\ No newline at end of file